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:
@@ -29,6 +29,30 @@ export class FabricClient {
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
// Generic JSON request (GET/PUT/PATCH/DELETE). Empty 2xx body -> null
|
||||
// (Fabric returns an empty body when a channel has no canvas).
|
||||
private async req<T>(
|
||||
method: string,
|
||||
url: string,
|
||||
auth?: string,
|
||||
body?: unknown,
|
||||
): Promise<T> {
|
||||
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) as T;
|
||||
}
|
||||
|
||||
// Exchange an agent API key for a Fabric user session (+ guild tokens).
|
||||
agentLogin(apiKey: string): Promise<FabricSession> {
|
||||
return this.post<FabricSession>(`${this.centerApiBase}/auth/agent/login`, { apiKey });
|
||||
@@ -86,4 +110,57 @@ export class FabricClient {
|
||||
joinChannel(guildEndpoint: string, guildToken: string, channelId: string): Promise<unknown> {
|
||||
return this.post(`${guildEndpoint}/api/channels/${channelId}/join`, {}, guildToken);
|
||||
}
|
||||
|
||||
// ---- channel canvas (one pinned doc per channel) ----
|
||||
|
||||
private canvasUrl(endpoint: string, channelId: string): string {
|
||||
return `${endpoint}/api/channels/${channelId}/canvas`;
|
||||
}
|
||||
|
||||
// null when the channel has no canvas
|
||||
getCanvas(
|
||||
endpoint: string,
|
||||
token: string,
|
||||
channelId: string,
|
||||
): Promise<FabricCanvas | null> {
|
||||
return this.req('GET', this.canvasUrl(endpoint, channelId), token);
|
||||
}
|
||||
|
||||
// share / replace (caller becomes the sharer)
|
||||
shareCanvas(
|
||||
endpoint: string,
|
||||
token: string,
|
||||
channelId: string,
|
||||
body: CanvasInput,
|
||||
): Promise<FabricCanvas> {
|
||||
return this.req('PUT', this.canvasUrl(endpoint, channelId), token, body);
|
||||
}
|
||||
|
||||
// update in place (original sharer only — else the guild returns 403)
|
||||
updateCanvas(
|
||||
endpoint: string,
|
||||
token: string,
|
||||
channelId: string,
|
||||
body: Partial<CanvasInput>,
|
||||
): Promise<FabricCanvas> {
|
||||
return this.req('PATCH', this.canvasUrl(endpoint, channelId), token, body);
|
||||
}
|
||||
|
||||
// remove ("close") the canvas (original sharer only)
|
||||
removeCanvas(endpoint: string, token: string, channelId: string): Promise<unknown> {
|
||||
return this.req('DELETE', this.canvasUrl(endpoint, channelId), token);
|
||||
}
|
||||
}
|
||||
|
||||
export type CanvasFormat = 'md' | 'html' | 'text';
|
||||
export type CanvasInput = { title: string; format: CanvasFormat; source: string };
|
||||
export type FabricCanvas = {
|
||||
channelId: string;
|
||||
sharerUserId: string;
|
||||
title: string;
|
||||
format: CanvasFormat;
|
||||
source: string;
|
||||
version: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user