feat(plugin): fabric-canvas tool; fabric-register env=AGENT_ID only
- bin/fabric-register.mjs: only AGENT_ID is read from the environment; --api-key is flag-only (no FABRIC_API_KEY); dropped FABRIC_CENTER_API_BASE / FABRIC_IDENTITY_FILE / OPENCLAW_PATH env fallbacks (flags + sensible defaults; --center still falls back to openclaw.json). - New fabric-canvas tool (one tool, four actions): read / share / update / close the channel's single pinned canvas. Backed by FabricClient get/share/update/removeCanvas (GET/PUT/PATCH/DELETE; empty 2xx body -> null). update/close are sharer-only server-side. - README updated. Verified: client-level smoke against the running guild — read(empty→null) → share(v1) → read → update(v2) → close(→null) all pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
38
dist/fabric/src/fabric-client.js
vendored
38
dist/fabric/src/fabric-client.js
vendored
@@ -21,6 +21,24 @@ export class FabricClient {
|
||||
}
|
||||
return (await res.json());
|
||||
}
|
||||
// Generic JSON request (GET/PUT/PATCH/DELETE). Empty 2xx body -> null
|
||||
// (Fabric returns an empty body when a channel has no canvas).
|
||||
async req(method, url, auth, body) {
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
...(body !== undefined ? { 'content-type': 'application/json' } : {}),
|
||||
...(auth ? { authorization: `Bearer ${auth}` } : {}),
|
||||
},
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(`${method} ${url} -> ${res.status} ${text}`);
|
||||
}
|
||||
const text = await res.text();
|
||||
return (text ? JSON.parse(text) : null);
|
||||
}
|
||||
// Exchange an agent API key for a Fabric user session (+ guild tokens).
|
||||
agentLogin(apiKey) {
|
||||
return this.post(`${this.centerApiBase}/auth/agent/login`, { apiKey });
|
||||
@@ -50,4 +68,24 @@ export class FabricClient {
|
||||
joinChannel(guildEndpoint, guildToken, channelId) {
|
||||
return this.post(`${guildEndpoint}/api/channels/${channelId}/join`, {}, guildToken);
|
||||
}
|
||||
// ---- channel canvas (one pinned doc per channel) ----
|
||||
canvasUrl(endpoint, channelId) {
|
||||
return `${endpoint}/api/channels/${channelId}/canvas`;
|
||||
}
|
||||
// null when the channel has no canvas
|
||||
getCanvas(endpoint, token, channelId) {
|
||||
return this.req('GET', this.canvasUrl(endpoint, channelId), token);
|
||||
}
|
||||
// share / replace (caller becomes the sharer)
|
||||
shareCanvas(endpoint, token, channelId, body) {
|
||||
return this.req('PUT', this.canvasUrl(endpoint, channelId), token, body);
|
||||
}
|
||||
// update in place (original sharer only — else the guild returns 403)
|
||||
updateCanvas(endpoint, token, channelId, body) {
|
||||
return this.req('PATCH', this.canvasUrl(endpoint, channelId), token, body);
|
||||
}
|
||||
// remove ("close") the canvas (original sharer only)
|
||||
removeCanvas(endpoint, token, channelId) {
|
||||
return this.req('DELETE', this.canvasUrl(endpoint, channelId), token);
|
||||
}
|
||||
}
|
||||
|
||||
74
dist/fabric/src/tools.js
vendored
74
dist/fabric/src/tools.js
vendored
@@ -89,4 +89,78 @@ export function registerFabricTools(api, client, identity) {
|
||||
return { ok: true, closed: true };
|
||||
},
|
||||
}));
|
||||
// fabric-canvas: share / update / read / close the channel's single
|
||||
// pinned canvas document (one tool, four actions). update/close are
|
||||
// sharer-only server-side (the guild returns 403 otherwise).
|
||||
api.registerTool((ctx) => ({
|
||||
name: 'fabric-canvas',
|
||||
description: "Manage a channel's pinned canvas document. action: " +
|
||||
"read (current canvas or null) | share (create/replace; you become " +
|
||||
'the sharer) | update (edit in place; sharer only) | close (remove; ' +
|
||||
'sharer only).',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['action', 'guildNodeId', 'channelId'],
|
||||
properties: {
|
||||
action: { type: 'string', enum: ['read', 'share', 'update', 'close'] },
|
||||
guildNodeId: { type: 'string' },
|
||||
channelId: { type: 'string' },
|
||||
title: { type: 'string', description: 'share: required; update: optional' },
|
||||
format: {
|
||||
type: 'string',
|
||||
enum: ['md', 'html', 'text'],
|
||||
description: 'share: required; update: optional',
|
||||
},
|
||||
source: {
|
||||
type: 'string',
|
||||
description: 'document body. share: required; update: optional',
|
||||
},
|
||||
},
|
||||
},
|
||||
execute: async (p) => {
|
||||
const agentId = ctx.agentId;
|
||||
if (!agentId)
|
||||
return { ok: false, error: 'no agent context' };
|
||||
const { guild, token } = await ctxGuild(agentId, p.guildNodeId);
|
||||
const ep = guild.endpoint;
|
||||
switch (p.action) {
|
||||
case 'read': {
|
||||
const canvas = await client.getCanvas(ep, token, p.channelId);
|
||||
return { ok: true, canvas };
|
||||
}
|
||||
case 'share': {
|
||||
if (!p.title || !p.format || p.source === undefined) {
|
||||
return { ok: false, error: 'share requires title, format, and source' };
|
||||
}
|
||||
const canvas = await client.shareCanvas(ep, token, p.channelId, {
|
||||
title: p.title,
|
||||
format: p.format,
|
||||
source: p.source,
|
||||
});
|
||||
return { ok: true, canvas };
|
||||
}
|
||||
case 'update': {
|
||||
const body = {};
|
||||
if (p.title !== undefined)
|
||||
body.title = p.title;
|
||||
if (p.format !== undefined)
|
||||
body.format = p.format;
|
||||
if (p.source !== undefined)
|
||||
body.source = p.source;
|
||||
if (Object.keys(body).length === 0) {
|
||||
return { ok: false, error: 'update needs at least one of title/format/source' };
|
||||
}
|
||||
const canvas = await client.updateCanvas(ep, token, p.channelId, body);
|
||||
return { ok: true, canvas };
|
||||
}
|
||||
case 'close': {
|
||||
await client.removeCanvas(ep, token, p.channelId);
|
||||
return { ok: true, removed: true };
|
||||
}
|
||||
default:
|
||||
return { ok: false, error: `unknown action ${String(p.action)}` };
|
||||
}
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user