diff --git a/README.md b/README.md index 884d46e..0afc364 100644 --- a/README.md +++ b/README.md @@ -1 +1,51 @@ # Fabric.OpenclawPlugin + +An **OpenClaw channel plugin** that connects OpenClaw agents to a Fabric guild. + +## Model + +- `kind: "channel"` plugin (like the bundled discord channel). OpenClaw **core + owns dispatch** (inbound → agent run) and the reply pipeline via the channel + turn kernel `runtime.channel.turn.run(...)`. +- Fabric already owns turn/shuffle/mention/`/no-reply` server-side, so this + plugin is thin. Fabric's per-recipient **`wakeup`** maps to channel-turn + **admission**: + - `wakeup === true` → `dispatch` (agent runs, may reply) + - otherwise → `{ kind: "drop", recordHistory: true }` (kept as context) +- **No sidecar, no fake no-reply model, no `before_model_resolve` gating.** + +## Auth + +Each agent has a Fabric Center **API key** (mint via Center CLI: +`node dist/cli.js user apikey --email `). The key is exchanged for +a user session (`POST /auth/agent/login`) used to receive (socket) and post +replies. Bind a key to an agent via the `fabric-register` tool, or pre-populate +the identity file. + +## Config + +- `channels.fabric.centerApiBase` — e.g. `http://localhost:7001/api` +- plugin `identityFilePath` — default `~/.openclaw/fabric-identity.json` + (`{ entries: [{ agentId, fabricApiKey }] }`) + +## Tools + +- `fabric-register` — bind this agent's Fabric API key +- `create-chat-channel` (general) / `create-work-channel` (work) / + `create-report-channel` (report) / `create-discussion-channel` (discuss) +- `discussion-complete` — post a summary then close the channel + (Fabric `POST /channels/:id/close`; closed → history readable, posts → 409) + +## Transport (Phase 1 = B1) + +One Fabric socket per agent identity, in the plugin runtime. Firehose (B2) is +a later drop-in behind the same `dispatch()` seam. + +## Build + +```bash +npm install && npm run build +``` + +> The plugin compiles against the host's OpenClaw SDK +> (`openclaw/plugin-sdk/*`); build inside the OpenClaw plugin environment. diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..3e207f2 --- /dev/null +++ b/index.ts @@ -0,0 +1,67 @@ +import path from 'node:path'; +import os from 'node:os'; +import { defineChannelPluginEntry } from 'openclaw/plugin-sdk/channel-core'; +import type { OpenClawPluginApi } from 'openclaw/plugin-sdk/core'; +import { FabricClient } from './src/fabric-client.js'; +import { IdentityRegistry } from './src/identity.js'; +import { FabricInbound } from './src/inbound.js'; +import { buildFabricChannelPlugin } from './src/channel.js'; +import { registerFabricTools } from './src/tools.js'; + +type PluginConfig = { identityFilePath?: string; debugMode?: boolean }; + +function centerApiBase(api: OpenClawPluginApi): string { + const section = ((api.config as Record)?.channels as Record)?.[ + 'fabric' + ]; + return section?.centerApiBase ?? 'http://localhost:7001/api'; +} + +let inbound: FabricInbound | null = null; + +export default defineChannelPluginEntry({ + id: 'fabric', + name: 'Fabric', + description: 'Fabric channel plugin — OpenClaw agents speak in Fabric guilds', + + // Channel object: config/security/outbound. Visible turn replies flow + // through the inbound channel-turn delivery adapter; outbound.sendText + // covers proactive sends via the shared message tool. + plugin: buildFabricChannelPlugin(async () => ({ messageId: undefined })), + + // registerFull: runtime pieces (transport, tools). Guarded so the long-lived + // Fabric connections start once per gateway process. + registerFull(api: OpenClawPluginApi) { + const cfg = (api.pluginConfig ?? {}) as PluginConfig; + const identityFilePath = + cfg.identityFilePath ?? path.join(os.homedir(), '.openclaw', 'fabric-identity.json'); + + const client = new FabricClient(centerApiBase(api)); + const identity = new IdentityRegistry(identityFilePath); + + registerFabricTools( + { registerTool: (d) => api.registerTool(d as never), logger: api.logger }, + client, + identity, + ); + + api.on('gateway_start', () => { + const _G = globalThis as Record; + if (_G._fabricInboundStarted) return; + _G._fabricInboundStarted = true; + inbound = new FabricInbound( + (api as unknown as { runtime: any }).runtime, + client, + identity, + api.logger, + ); + void inbound.start(); + api.logger.info('fabric: inbound transport started'); + }); + + api.on('gateway_stop', () => { + inbound?.stop(); + inbound = null; + }); + }, +}); diff --git a/openclaw.plugin.json b/openclaw.plugin.json new file mode 100644 index 0000000..6040725 --- /dev/null +++ b/openclaw.plugin.json @@ -0,0 +1,51 @@ +{ + "id": "fabric", + "kind": "channel", + "channels": ["fabric"], + "name": "Fabric", + "description": "Fabric channel plugin — OpenClaw agents speak in Fabric guilds", + "activation": { + "onStartup": true + }, + "contracts": { + "tools": [ + "fabric-register", + "create-chat-channel", + "create-work-channel", + "create-report-channel", + "create-discussion-channel", + "discussion-complete" + ] + }, + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "identityFilePath": { + "type": "string", + "description": "Path to the agent identity registry JSON (agentId -> Fabric API key). Default ~/.openclaw/fabric-identity.json" + }, + "debugMode": { "type": "boolean", "default": false } + }, + "required": [] + }, + "channelConfigs": { + "fabric": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "centerApiBase": { + "type": "string", + "description": "Fabric Center API base, e.g. http://localhost:7001/api" + }, + "dmSecurity": { "type": "string" }, + "allowFrom": { "type": "array", "items": { "type": "string" } } + } + }, + "uiHints": { + "centerApiBase": { "label": "Center API base" } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4ab2c9e --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "fabric-openclaw-plugin", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Fabric channel plugin for OpenClaw", + "openclaw": { + "extensions": ["./index.ts"], + "setupEntry": "./src/setup-entry.ts", + "channel": { + "id": "fabric", + "label": "Fabric", + "blurb": "Connect OpenClaw agents to a Fabric guild." + } + }, + "dependencies": { + "socket.io-client": "^4.8.3" + }, + "scripts": { + "build": "tsc -p tsconfig.json" + } +} diff --git a/src/channel.ts b/src/channel.ts new file mode 100644 index 0000000..c33a41d --- /dev/null +++ b/src/channel.ts @@ -0,0 +1,63 @@ +import { + createChatChannelPlugin, + createChannelPluginBase, +} from 'openclaw/plugin-sdk/channel-core'; +import type { OpenClawConfig } from 'openclaw/plugin-sdk/channel-core'; + +export type ResolvedFabricAccount = { + accountId: string | null; + centerApiBase: string; + allowFrom: string[]; + dmPolicy: string | undefined; +}; + +export function resolveFabricAccount( + cfg: OpenClawConfig, + accountId?: string | null, +): ResolvedFabricAccount { + const section = (cfg.channels as Record)?.['fabric']; + const centerApiBase: string | undefined = section?.centerApiBase; + if (!centerApiBase) throw new Error('fabric: channels.fabric.centerApiBase is required'); + return { + accountId: accountId ?? null, + centerApiBase, + allowFrom: section?.allowFrom ?? [], + dmPolicy: section?.dmSecurity, + }; +} + +// Outbound is wired by the entry (it needs the identity registry + client to +// post as the right agent). Channel-turn visible replies go through the +// inbound adapter's delivery callback; this object owns config/security only. +export function buildFabricChannelPlugin( + sendText: (params: { accountId?: string | null; to: string; text: string }) => Promise<{ messageId?: string }>, +) { + return createChatChannelPlugin({ + base: createChannelPluginBase({ + id: 'fabric', + setup: { + resolveAccount: resolveFabricAccount, + inspectAccount(cfg, accountId) { + const section = (cfg.channels as Record)?.['fabric']; + const ok = Boolean(section?.centerApiBase); + return { enabled: ok, configured: ok, tokenStatus: ok ? 'available' : 'missing' }; + }, + }, + }), + security: { + dm: { + channelKey: 'fabric', + resolvePolicy: (a) => a.dmPolicy, + resolveAllowFrom: (a) => a.allowFrom, + defaultPolicy: 'allowlist', + }, + }, + // Fabric replies thread by being posted into the same channel. + threading: { topLevelReplyToMode: 'channel' }, + outbound: { + attachedResults: { + sendText: async (params) => sendText(params), + }, + }, + }); +} diff --git a/src/fabric-client.ts b/src/fabric-client.ts new file mode 100644 index 0000000..8c82c22 --- /dev/null +++ b/src/fabric-client.ts @@ -0,0 +1,89 @@ +// Thin Fabric REST client. One auth concept: an agent's Center API key is +// exchanged for a normal user session (POST /auth/agent/login); the returned +// guild access tokens are used to post messages and call channel APIs. + +export type FabricSession = { + accessToken: string; + refreshToken: string; + user: { id: string; email: string; name: string }; + guilds: Array<{ nodeId: string; name: string; endpoint: string; status: string }>; + guildAccessTokens: Array<{ guildNodeId: string; token: string }>; +}; + +export class FabricClient { + constructor(private readonly centerApiBase: string) {} + + private async post(url: string, body: unknown, auth?: string): Promise { + const res = await fetch(url, { + method: 'POST', + headers: { + 'content-type': 'application/json', + ...(auth ? { authorization: `Bearer ${auth}` } : {}), + }, + body: JSON.stringify(body), + }); + if (!res.ok) { + const text = await res.text().catch(() => ''); + throw new Error(`POST ${url} -> ${res.status} ${text}`); + } + return (await res.json()) as T; + } + + // Exchange an agent API key for a Fabric user session (+ guild tokens). + agentLogin(apiKey: string): Promise { + return this.post(`${this.centerApiBase}/auth/agent/login`, { apiKey }); + } + + // Refresh the center access token (guild tokens are re-fetched via /auth/me/guilds). + async refresh(refreshToken: string): Promise<{ accessToken: string; refreshToken: string }> { + return this.post(`${this.centerApiBase}/auth/refresh`, { refreshToken }); + } + + async meGuilds(accessToken: string): Promise> { + const res = await fetch(`${this.centerApiBase}/auth/me/guilds`, { + headers: { authorization: `Bearer ${accessToken}` }, + }); + if (!res.ok) throw new Error(`me/guilds -> ${res.status}`); + return (await res.json()) as Pick; + } + + // ---- guild-scoped (use the per-guild access token) ---- + + postMessage( + guildEndpoint: string, + guildToken: string, + channelId: string, + content: string, + authorUserId: string, + ): Promise { + return this.post( + `${guildEndpoint}/api/channels/${channelId}/messages`, + { content, authorUserId }, + guildToken, + ); + } + + createChannel( + guildEndpoint: string, + guildToken: string, + body: { + guildId: string; + name: string; + xType: string; + isPublic?: boolean; + memberUserIds?: string[]; + onDuty?: string; + listeners?: string[]; + }, + ): Promise<{ id: string }> { + return this.post(`${guildEndpoint}/api/channels`, body, guildToken); + } + + closeChannel(guildEndpoint: string, guildToken: string, channelId: string): Promise { + return this.post(`${guildEndpoint}/api/channels/${channelId}/close`, {}, guildToken); + } + + joinChannel(guildEndpoint: string, guildToken: string, channelId: string): Promise { + return this.post(`${guildEndpoint}/api/channels/${channelId}/join`, {}, guildToken); + } +} diff --git a/src/identity.ts b/src/identity.ts new file mode 100644 index 0000000..70d9f4b --- /dev/null +++ b/src/identity.ts @@ -0,0 +1,54 @@ +import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'; +import { dirname } from 'node:path'; + +// Maps an OpenClaw agentId to its Fabric machine credential (Center API key). +// One key per agent; minted via Center CLI: `node dist/cli.js user apikey`. +export type IdentityEntry = { + agentId: string; + fabricApiKey: string; + // cached after first agent/login (for logging / display only) + fabricUserId?: string; + displayName?: string; +}; + +type IdentityFile = { entries: IdentityEntry[] }; + +export class IdentityRegistry { + private entries = new Map(); + + constructor(private readonly filePath: string) { + this.load(); + } + + private load(): void { + if (!existsSync(this.filePath)) return; + try { + const data = JSON.parse(readFileSync(this.filePath, 'utf8')) as IdentityFile; + for (const e of data.entries ?? []) { + if (e.agentId && e.fabricApiKey) this.entries.set(e.agentId, e); + } + } catch { + // corrupt file -> start empty; upsert will rewrite + } + } + + private persist(): void { + mkdirSync(dirname(this.filePath), { recursive: true }); + const data: IdentityFile = { entries: [...this.entries.values()] }; + writeFileSync(this.filePath, JSON.stringify(data, null, 2)); + } + + list(): IdentityEntry[] { + return [...this.entries.values()]; + } + + findByAgentId(agentId: string): IdentityEntry | undefined { + return this.entries.get(agentId); + } + + upsert(entry: IdentityEntry): void { + const prev = this.entries.get(entry.agentId); + this.entries.set(entry.agentId, { ...prev, ...entry }); + this.persist(); + } +} diff --git a/src/inbound.ts b/src/inbound.ts new file mode 100644 index 0000000..14b9736 --- /dev/null +++ b/src/inbound.ts @@ -0,0 +1,171 @@ +import { io, type Socket } from 'socket.io-client'; +import type { FabricClient, FabricSession } from './fabric-client.js'; +import type { IdentityRegistry } from './identity.js'; + +// OpenClaw plugin runtime — only the channel-turn kernel surface we use. +// Typed loosely on purpose: the concrete shapes come from +// openclaw/plugin-sdk/core at the host's SDK version. +type PluginRuntime = { + channel: { + turn: { + run(args: unknown): Promise; + }; + }; + log?: { debug?: (m: string, x?: unknown) => void }; +}; + +type Logger = { info: (m: string) => void; warn: (m: string) => void }; + +type FabricMessage = { + messageId: string; + seq: number; + content: string; + authorUserId?: string; + createdAt?: string; + // per-recipient metadata Fabric attaches at push time (this agent's view) + wakeup?: boolean; +}; + +// One live Fabric connection per agent identity (Phase 1 = B1). Lives in the +// channel-plugin runtime (no separate sidecar). Firehose (B2) would replace +// this class behind the same dispatch() call. +export class FabricInbound { + private sockets: Socket[] = []; + private timers: NodeJS.Timeout[] = []; + private seen = new Set(); + + constructor( + private readonly runtime: PluginRuntime, + private readonly client: FabricClient, + private readonly identity: IdentityRegistry, + private readonly log: Logger, + ) {} + + async start(): Promise { + for (const entry of this.identity.list()) { + try { + const session = await this.client.agentLogin(entry.fabricApiKey); + this.identity.upsert({ + agentId: entry.agentId, + fabricApiKey: entry.fabricApiKey, + fabricUserId: session.user.id, + displayName: session.user.name, + }); + await this.connectAgent(entry.agentId, session); + this.log.info(`fabric: agent ${entry.agentId} connected as ${session.user.email}`); + } catch (err) { + this.log.warn(`fabric: agent ${entry.agentId} connect failed: ${String(err)}`); + } + } + } + + stop(): void { + for (const t of this.timers) clearInterval(t); + for (const s of this.sockets) s.disconnect(); + this.sockets = []; + this.timers = []; + } + + private async connectAgent(agentId: string, session: FabricSession): Promise { + const selfUserId = session.user.id; + for (const g of session.guilds) { + const tok = session.guildAccessTokens.find((t) => t.guildNodeId === g.nodeId)?.token; + if (!tok) continue; + + const socket = io(`${g.endpoint}/realtime`, { + transports: ['websocket'], + auth: { token: tok }, + autoConnect: false, + }); + + const joinAll = async () => { + try { + const res = await fetch(`${g.endpoint}/api/channels?guildId=${encodeURIComponent(g.nodeId)}`, { + headers: { authorization: `Bearer ${tok}` }, + }); + const channels = res.ok ? ((await res.json()) as Array<{ id: string }>) : []; + for (const c of channels) socket.emit('join_channel', { channelId: c.id }); + } catch { + /* best effort */ + } + }; + + socket.on('connect', () => void joinAll()); + socket.on('message.created', (m: FabricMessage & { channelId?: string }) => { + const channelId = m.channelId ?? ''; + if (!channelId) return; + // self-echo guard + dedupe + if (m.authorUserId && m.authorUserId === selfUserId) return; + const key = `${agentId}:${m.messageId}`; + if (this.seen.has(key)) return; + this.seen.add(key); + if (this.seen.size > 5000) this.seen.clear(); + void this.dispatch(agentId, g, channelId, m); + }); + + socket.connect(); + this.sockets.push(socket); + } + } + + // Hand the inbound Fabric message to OpenClaw's channel-turn kernel. + // wakeup === true -> dispatch (agent runs, may reply) + // wakeup !== true -> drop but keep as group history/context + private async dispatch( + agentId: string, + guild: { nodeId: string; endpoint: string }, + channelId: string, + m: FabricMessage, + ): Promise { + const admit = m.wakeup === true; + try { + await this.runtime.channel.turn.run({ + channel: 'fabric', + accountId: agentId, + raw: m, + adapter: { + ingest: (raw: FabricMessage) => ({ + id: raw.messageId, + timestamp: raw.createdAt ? Date.parse(raw.createdAt) : Date.now(), + rawText: raw.content, + textForAgent: raw.content, + }), + classify: () => ({ kind: 'message', canStartAgentTurn: admit }), + preflight: () => + admit ? {} : { admission: { kind: 'drop', reason: 'no-wakeup', recordHistory: true } }, + resolveTurn: (input: { id: string }) => ({ + route: { + agentId, + routeSessionKey: `agent:${agentId}:fabric:channel:${channelId}`, + createIfMissing: true, + }, + conversation: { kind: 'channel', id: channelId, label: `fabric:${guild.nodeId}` }, + reply: { to: channelId, nativeChannelId: channelId }, + message: { + body: m.content, + rawBody: m.content, + bodyForAgent: m.content, + envelopeFrom: m.authorUserId ?? 'fabric', + }, + delivery: { + deliver: async (payload: { text?: string }) => { + const text = typeof payload?.text === 'string' ? payload.text : ''; + if (!text.trim()) return { visibleReplySent: false }; + const entry = this.identity.findByAgentId(agentId); + const session = entry ? await this.client.agentLogin(entry.fabricApiKey) : null; + const gt = session?.guildAccessTokens.find((t) => t.guildNodeId === guild.nodeId)?.token; + if (!session || !gt) return { visibleReplySent: false }; + await this.client.postMessage(guild.endpoint, gt, channelId, text, session.user.id); + return { visibleReplySent: true }; + }, + }, + meta: { admission: admit ? { kind: 'dispatch' } : { kind: 'drop', recordHistory: true } }, + }), + }, + log: (e: { stage?: string }) => this.runtime.log?.debug?.(`fabric.turn.${e?.stage}`), + }); + } catch (err) { + this.log.warn(`fabric: turn.run failed agent=${agentId} channel=${channelId}: ${String(err)}`); + } + } +} diff --git a/src/setup-entry.ts b/src/setup-entry.ts new file mode 100644 index 0000000..c9bfc36 --- /dev/null +++ b/src/setup-entry.ts @@ -0,0 +1,14 @@ +// Setup-safe entry: returns channel metadata for read-only command paths +// (status, channels list) before the plugin runtime starts. Must NOT start +// clients, listeners, or transports. +export default { + channel: { + id: 'fabric', + label: 'Fabric', + blurb: 'Connect OpenClaw agents to a Fabric guild.', + }, + inspect(cfg: { channels?: Record }) { + const ok = Boolean(cfg?.channels?.['fabric']?.centerApiBase); + return { enabled: ok, configured: ok }; + }, +}; diff --git a/src/tools.ts b/src/tools.ts new file mode 100644 index 0000000..cbd90d2 --- /dev/null +++ b/src/tools.ts @@ -0,0 +1,139 @@ +import type { FabricClient } from './fabric-client.js'; +import type { IdentityRegistry } from './identity.js'; + +// OpenClaw tool registration api (loose typing — concrete shape from +// openclaw/plugin-sdk/core at host SDK version). +type ToolApi = { + registerTool: (def: unknown) => void; + logger: { info: (m: string) => void; warn: (m: string) => void }; +}; + +type Ctx = { agentId?: string }; + +const X_BY_KIND: Record = { + chat: 'general', + work: 'work', + report: 'report', + discussion: 'discuss', +}; + +export function registerFabricTools( + api: ToolApi, + client: FabricClient, + identity: IdentityRegistry, +): void { + // Resolve the calling agent's Fabric session + a guild's token/endpoint. + const ctxGuild = async (agentId: string, guildNodeId: string) => { + const entry = identity.findByAgentId(agentId); + if (!entry) throw new Error(`agent ${agentId} not registered (call fabric-register)`); + const session = await client.agentLogin(entry.fabricApiKey); + const guild = session.guilds.find((g) => g.nodeId === guildNodeId); + const token = session.guildAccessTokens.find((t) => t.guildNodeId === guildNodeId)?.token; + if (!guild || !token) throw new Error(`agent not a member of guild ${guildNodeId}`); + return { session, guild, token }; + }; + + // fabric-register: bind this agent to a Fabric API key. + api.registerTool((ctx: Ctx) => ({ + name: 'fabric-register', + description: "Register this agent's Fabric API key (minted via Center CLI `user apikey`).", + inputSchema: { + type: 'object', + additionalProperties: false, + required: ['fabricApiKey'], + properties: { + fabricApiKey: { type: 'string', description: 'Fabric Center API key (fak_…)' }, + }, + }, + handler: async (params: { fabricApiKey: string }) => { + const agentId = ctx.agentId; + if (!agentId) return { ok: false, error: 'no agent context' }; + const session = await client.agentLogin(params.fabricApiKey); + identity.upsert({ + agentId, + fabricApiKey: params.fabricApiKey, + fabricUserId: session.user.id, + displayName: session.user.name, + }); + return { ok: true, user: session.user }; + }, + })); + + const makeCreate = (kind: 'chat' | 'work' | 'report' | 'discussion') => + api.registerTool((ctx: Ctx) => ({ + name: `create-${kind}-channel`, + description: `Create a Fabric ${kind} channel (x_type=${X_BY_KIND[kind]}).`, + inputSchema: { + type: 'object', + additionalProperties: false, + required: ['guildNodeId', 'name'], + properties: { + guildNodeId: { type: 'string' }, + name: { type: 'string' }, + isPublic: { type: 'boolean' }, + memberUserIds: { type: 'array', items: { type: 'string' } }, + onDuty: { type: 'string', description: 'required for triage-like flows (unused for these kinds)' }, + listeners: { type: 'array', items: { type: 'string' } }, + }, + }, + handler: async (p: { + guildNodeId: string; + name: string; + isPublic?: boolean; + memberUserIds?: string[]; + }) => { + const agentId = ctx.agentId; + if (!agentId) return { ok: false, error: 'no agent context' }; + const { guild, token } = await ctxGuild(agentId, p.guildNodeId); + const ch = await client.createChannel(guild.endpoint, token, { + guildId: p.guildNodeId, + name: p.name, + xType: X_BY_KIND[kind], + isPublic: p.isPublic ?? false, + memberUserIds: p.memberUserIds ?? [], + }); + return { ok: true, channelId: ch.id }; + }, + })); + + makeCreate('chat'); + makeCreate('work'); + makeCreate('report'); + makeCreate('discussion'); + + // discussion-complete: post a summary then close the channel (Guild + // /channels/:id/close — history stays readable, new posts -> 409). + api.registerTool((ctx: Ctx) => ({ + name: 'discussion-complete', + description: 'Conclude a discussion: post a summary then close the channel.', + inputSchema: { + type: 'object', + additionalProperties: false, + required: ['guildNodeId', 'channelId', 'summary'], + properties: { + guildNodeId: { type: 'string' }, + channelId: { type: 'string' }, + summary: { type: 'string' }, + callbackChannelId: { type: 'string', description: 'optional channel to also post the summary to' }, + }, + }, + handler: async (p: { + guildNodeId: string; + channelId: string; + summary: string; + callbackChannelId?: string; + }) => { + const agentId = ctx.agentId; + if (!agentId) return { ok: false, error: 'no agent context' }; + const { session, guild, token } = await ctxGuild(agentId, p.guildNodeId); + await client.postMessage(guild.endpoint, token, p.channelId, p.summary, session.user.id); + if (p.callbackChannelId) { + await client + .postMessage(guild.endpoint, token, p.callbackChannelId, p.summary, session.user.id) + .catch(() => undefined); + } + await client.closeChannel(guild.endpoint, token, p.channelId); + return { ok: true, closed: true }; + }, + })); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e37cd99 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": false, + "outDir": "dist", + "rootDir": "." + }, + "include": ["index.ts", "src/**/*.ts"] +}