@vibearound/plugin-channel-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,317 @@
1
+ /**
2
+ * BlockRenderer — abstract base class for block-based message rendering.
3
+ *
4
+ * ## How it works
5
+ *
6
+ * ACP streams agent responses as a sequence of typed events:
7
+ * text chunk, text chunk, tool call, tool update, text chunk, …
8
+ *
9
+ * Each contiguous run of the **same kind** (text / thinking / tool) is grouped
10
+ * into one "block". When the kind changes, the current block is **sealed**
11
+ * (no more edits) and a new block starts.
12
+ *
13
+ * Blocks are rendered to the platform by subclass-implemented `sendBlock` and
14
+ * `editBlock`. The renderer handles:
15
+ *
16
+ * - **Debounced flushing** — batches rapid deltas before sending (avoids
17
+ * excessive API calls during fast streaming).
18
+ * - **Edit throttling** — enforces a minimum interval between edits to
19
+ * respect platform rate limits.
20
+ * - **Ordered delivery** — a `sendChain` Promise serializes all send/edit
21
+ * calls so messages always arrive in the correct order.
22
+ * - **Sentinel guard** — prevents concurrent creates for the same block.
23
+ * - **Verbose filtering** — thinking / tool blocks can be suppressed without
24
+ * creating phantom block boundaries.
25
+ *
26
+ * ## Usage
27
+ *
28
+ * ```ts
29
+ * class MyRenderer extends BlockRenderer<string> {
30
+ * protected async sendBlock(channelId, kind, content) {
31
+ * const msg = await myApi.sendMessage(channelId, content);
32
+ * return msg.id;
33
+ * }
34
+ * protected async editBlock(channelId, ref, kind, content, sealed) {
35
+ * await myApi.editMessage(ref, content);
36
+ * }
37
+ * }
38
+ *
39
+ * // In main.ts:
40
+ * const renderer = new MyRenderer({ verbose: { showThinking: false } });
41
+ *
42
+ * // When user sends a message:
43
+ * renderer.onPromptSent(channelId);
44
+ * try {
45
+ * await agent.prompt({ sessionId, content });
46
+ * await renderer.onTurnEnd(channelId);
47
+ * } catch (e) {
48
+ * await renderer.onTurnError(channelId, String(e));
49
+ * }
50
+ *
51
+ * // In the ACP client's sessionUpdate handler:
52
+ * renderer.onSessionUpdate(notification);
53
+ * ```
54
+ */
55
+ // ---------------------------------------------------------------------------
56
+ // Constants
57
+ // ---------------------------------------------------------------------------
58
+ const DEFAULT_FLUSH_INTERVAL_MS = 500;
59
+ const DEFAULT_MIN_EDIT_INTERVAL_MS = 1000;
60
+ // ---------------------------------------------------------------------------
61
+ // BlockRenderer
62
+ // ---------------------------------------------------------------------------
63
+ /**
64
+ * Abstract base class for block-based rendering of ACP session streams.
65
+ *
66
+ * @typeParam TRef - Platform-specific message reference type (e.g. `number`
67
+ * for Telegram message IDs, `string` for Feishu message IDs). Used as the
68
+ * return type of `sendBlock` and the first argument of `editBlock`.
69
+ */
70
+ export class BlockRenderer {
71
+ flushIntervalMs;
72
+ minEditIntervalMs;
73
+ verbose;
74
+ states = new Map();
75
+ constructor(options = {}) {
76
+ this.flushIntervalMs = options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
77
+ this.minEditIntervalMs = options.minEditIntervalMs ?? DEFAULT_MIN_EDIT_INTERVAL_MS;
78
+ this.verbose = {
79
+ showThinking: options.verbose?.showThinking ?? false,
80
+ showToolUse: options.verbose?.showToolUse ?? false,
81
+ };
82
+ }
83
+ /**
84
+ * Format block content before sending or editing.
85
+ *
86
+ * Default applies standard emoji prefixes:
87
+ * - `thinking` → `💭 <content>`
88
+ * - `tool` → trimmed content
89
+ * - `text` → content as-is
90
+ *
91
+ * Override to apply platform-specific formatting (e.g. markdown escaping).
92
+ */
93
+ formatContent(kind, content, _sealed) {
94
+ switch (kind) {
95
+ case "thinking": return `💭 ${content}`;
96
+ case "tool": return content.trim();
97
+ case "text": return content;
98
+ }
99
+ }
100
+ /**
101
+ * Called after the last block has been flushed and the turn is complete.
102
+ * Override to perform cleanup (e.g. remove a "typing" indicator, a
103
+ * processing reaction, etc.).
104
+ */
105
+ onAfterTurnEnd(_channelId) {
106
+ return Promise.resolve();
107
+ }
108
+ /**
109
+ * Called after a turn error, once state has been cleaned up.
110
+ * Override to send an error message to the user.
111
+ */
112
+ onAfterTurnError(_channelId, _error) {
113
+ return Promise.resolve();
114
+ }
115
+ /**
116
+ * Map an ACP `sessionId` to the channel ID used internally.
117
+ *
118
+ * Default: identity (sessionId === channelId).
119
+ *
120
+ * Override if your plugin namespaces channel IDs (e.g. Feishu uses
121
+ * `"feishu:<sessionId>"`, WeChat uses `"weixin-openclaw-bridge:<sessionId>"`).
122
+ */
123
+ sessionIdToChannelId(sessionId) {
124
+ return sessionId;
125
+ }
126
+ // ---------------------------------------------------------------------------
127
+ // Public API
128
+ // ---------------------------------------------------------------------------
129
+ /**
130
+ * Process an ACP `sessionUpdate` notification from the host.
131
+ *
132
+ * Routes the event to the correct block based on its variant, appending
133
+ * deltas to the current block or starting a new one when the kind changes.
134
+ *
135
+ * Call this from the ACP `Client.sessionUpdate` handler.
136
+ */
137
+ onSessionUpdate(notification) {
138
+ const sessionId = notification.sessionId;
139
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
140
+ const update = notification.update;
141
+ const variant = update.sessionUpdate;
142
+ const channelId = this.sessionIdToChannelId(sessionId);
143
+ switch (variant) {
144
+ case "agent_message_chunk": {
145
+ const delta = (update.content?.text ?? "");
146
+ if (delta)
147
+ this.appendToBlock(channelId, "text", delta);
148
+ break;
149
+ }
150
+ case "agent_thought_chunk": {
151
+ if (!this.verbose.showThinking)
152
+ return; // skip — no block, no boundary
153
+ const delta = (update.content?.text ?? "");
154
+ if (delta)
155
+ this.appendToBlock(channelId, "thinking", delta);
156
+ break;
157
+ }
158
+ case "tool_call": {
159
+ if (!this.verbose.showToolUse)
160
+ return; // skip
161
+ const title = update.title;
162
+ if (title)
163
+ this.appendToBlock(channelId, "tool", `🔧 ${title}\n`);
164
+ break;
165
+ }
166
+ case "tool_call_update": {
167
+ if (!this.verbose.showToolUse)
168
+ return; // skip
169
+ const title = (update.title ?? "tool");
170
+ const status = update.status;
171
+ if (status === "completed" || status === "error") {
172
+ this.appendToBlock(channelId, "tool", `✅ ${title}\n`);
173
+ }
174
+ break;
175
+ }
176
+ }
177
+ }
178
+ /**
179
+ * Call this before sending a prompt to the agent.
180
+ *
181
+ * Clears any leftover state from a previous turn so the new turn starts
182
+ * with a clean slate.
183
+ */
184
+ onPromptSent(channelId) {
185
+ const old = this.states.get(channelId);
186
+ if (old?.flushTimer)
187
+ clearTimeout(old.flushTimer);
188
+ this.states.set(channelId, {
189
+ blocks: [],
190
+ flushTimer: null,
191
+ lastEditMs: 0,
192
+ sendChain: Promise.resolve(),
193
+ });
194
+ }
195
+ /**
196
+ * Call this after `agent.prompt()` resolves (turn complete).
197
+ *
198
+ * Seals and flushes the last block, then waits for all pending sends/edits
199
+ * to complete before calling `onAfterTurnEnd`.
200
+ */
201
+ async onTurnEnd(channelId) {
202
+ const state = this.states.get(channelId);
203
+ if (!state)
204
+ return;
205
+ if (state.flushTimer) {
206
+ clearTimeout(state.flushTimer);
207
+ state.flushTimer = null;
208
+ }
209
+ const last = state.blocks.at(-1);
210
+ if (last && !last.sealed) {
211
+ last.sealed = true;
212
+ this.enqueueFlush(state, last);
213
+ }
214
+ // Wait for the entire chain to drain before cleanup
215
+ await state.sendChain;
216
+ this.states.delete(channelId);
217
+ await this.onAfterTurnEnd(channelId);
218
+ }
219
+ /**
220
+ * Call this when `agent.prompt()` throws (turn error).
221
+ *
222
+ * Discards pending state and calls `onAfterTurnError` so the subclass can
223
+ * send an error message to the user.
224
+ */
225
+ async onTurnError(channelId, error) {
226
+ const state = this.states.get(channelId);
227
+ if (state?.flushTimer)
228
+ clearTimeout(state.flushTimer);
229
+ this.states.delete(channelId);
230
+ await this.onAfterTurnError(channelId, error);
231
+ }
232
+ // ---------------------------------------------------------------------------
233
+ // Internal — block management
234
+ // ---------------------------------------------------------------------------
235
+ appendToBlock(channelId, kind, delta) {
236
+ let state = this.states.get(channelId);
237
+ if (!state) {
238
+ // Auto-create state if onPromptSent wasn't called (e.g. host-initiated turns)
239
+ state = { blocks: [], flushTimer: null, lastEditMs: 0, sendChain: Promise.resolve() };
240
+ this.states.set(channelId, state);
241
+ }
242
+ const last = state.blocks.at(-1);
243
+ if (last && !last.sealed && last.kind === kind) {
244
+ // Same kind — accumulate
245
+ last.content += delta;
246
+ }
247
+ else {
248
+ // Kind changed — seal current block and start a new one
249
+ if (last && !last.sealed) {
250
+ last.sealed = true;
251
+ // Clear the debounce timer: we're doing an immediate flush of the sealed block
252
+ if (state.flushTimer) {
253
+ clearTimeout(state.flushTimer);
254
+ state.flushTimer = null;
255
+ }
256
+ this.enqueueFlush(state, last);
257
+ }
258
+ state.blocks.push({ channelId, kind, content: delta, ref: null, creating: false, sealed: false });
259
+ }
260
+ this.scheduleFlush(channelId, state);
261
+ }
262
+ scheduleFlush(channelId, state) {
263
+ if (state.flushTimer)
264
+ return; // already scheduled
265
+ state.flushTimer = setTimeout(() => {
266
+ state.flushTimer = null;
267
+ this.flush(channelId, state);
268
+ }, this.flushIntervalMs);
269
+ }
270
+ flush(channelId, state) {
271
+ const block = state.blocks.at(-1);
272
+ if (!block || block.sealed || !block.content)
273
+ return;
274
+ const now = Date.now();
275
+ if (now - state.lastEditMs < this.minEditIntervalMs) {
276
+ // Throttled — reschedule for the remaining window
277
+ const delay = this.minEditIntervalMs - (now - state.lastEditMs);
278
+ if (!state.flushTimer) {
279
+ state.flushTimer = setTimeout(() => {
280
+ state.flushTimer = null;
281
+ this.flush(channelId, state);
282
+ }, delay);
283
+ }
284
+ return;
285
+ }
286
+ this.enqueueFlush(state, block);
287
+ }
288
+ enqueueFlush(state, block) {
289
+ state.sendChain = state.sendChain
290
+ .then(() => this.flushBlock(state, block))
291
+ .catch(() => { }); // errors are handled inside flushBlock
292
+ }
293
+ async flushBlock(state, block) {
294
+ const content = this.formatContent(block.kind, block.content, block.sealed);
295
+ if (!content)
296
+ return;
297
+ try {
298
+ if (block.ref === null && !block.creating) {
299
+ // First send — use sentinel to prevent concurrent creates
300
+ block.creating = true;
301
+ block.ref = await this.sendBlock(block.channelId, block.kind, content);
302
+ block.creating = false;
303
+ state.lastEditMs = Date.now();
304
+ }
305
+ else if (block.ref !== null && !block.creating && this.editBlock) {
306
+ // Subsequent update — edit in-place
307
+ await this.editBlock(block.channelId, block.ref, block.kind, content, block.sealed);
308
+ state.lastEditMs = Date.now();
309
+ }
310
+ // else: create is in-flight (creating === true) — skip
311
+ }
312
+ catch {
313
+ block.creating = false;
314
+ }
315
+ }
316
+ }
317
+ //# sourceMappingURL=renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer.js","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Shared types for VibeAround channel plugins.
3
+ *
4
+ * Re-exports the ACP SDK types plugins commonly need, plus SDK-specific types
5
+ * for block rendering, plugin manifests, and verbose configuration.
6
+ */
7
+ export type { Agent, Client, SessionNotification, RequestPermissionRequest, RequestPermissionResponse, } from "@agentclientprotocol/sdk";
8
+ export interface PluginCapabilities {
9
+ /** Plugin supports real-time streaming updates. */
10
+ streaming?: boolean;
11
+ /** Platform supports rich interactive cards (e.g. Feishu). */
12
+ interactiveCards?: boolean;
13
+ /** Platform supports editing already-sent messages. */
14
+ editMessage?: boolean;
15
+ /** Platform supports file upload/download. */
16
+ media?: boolean;
17
+ auth?: {
18
+ methods?: string[];
19
+ };
20
+ }
21
+ /** Shape of plugin.json — the plugin manifest file. */
22
+ export interface PluginManifest {
23
+ id: string;
24
+ name: string;
25
+ kind: "channel";
26
+ runtime: "node";
27
+ entry: string;
28
+ build?: string;
29
+ configSchema?: Record<string, unknown>;
30
+ capabilities?: PluginCapabilities;
31
+ }
32
+ /** The three kinds of content blocks a plugin renders. */
33
+ export type BlockKind = "text" | "thinking" | "tool";
34
+ export interface VerboseConfig {
35
+ /** Show agent thinking/reasoning blocks. Default: false. */
36
+ showThinking: boolean;
37
+ /** Show tool call / tool result blocks. Default: false. */
38
+ showToolUse: boolean;
39
+ }
40
+ export interface BlockRendererOptions {
41
+ /**
42
+ * Debounce interval before flushing an unsealed block (ms).
43
+ * Controls how often in-progress blocks are sent to the platform.
44
+ * Default: 500.
45
+ */
46
+ flushIntervalMs?: number;
47
+ /**
48
+ * Minimum interval between consecutive edits to the same message (ms).
49
+ * Prevents hitting platform API rate limits.
50
+ * Default: 1000.
51
+ */
52
+ minEditIntervalMs?: number;
53
+ verbose?: Partial<VerboseConfig>;
54
+ }
55
+ /**
56
+ * Plugin config and metadata passed by the host in `_meta` during initialize.
57
+ */
58
+ export interface PluginInitMeta {
59
+ /** Plugin-specific config object from settings.json. */
60
+ config: Record<string, unknown>;
61
+ /** Host-provided cache directory path for temporary files. */
62
+ cacheDir?: string;
63
+ }
64
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,YAAY,EACV,KAAK,EACL,MAAM,EACN,mBAAmB,EACnB,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,0BAA0B,CAAC;AAMlC,MAAM,WAAW,kBAAkB;IACjC,mDAAmD;IACnD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,uDAAuD;IACvD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,8CAA8C;IAC9C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;CAC/B;AAED,uDAAuD;AACvD,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,YAAY,CAAC,EAAE,kBAAkB,CAAC;CACnC;AAMD,0DAA0D;AAC1D,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;AAErD,MAAM,WAAW,aAAa;IAC5B,4DAA4D;IAC5D,YAAY,EAAE,OAAO,CAAC;IACtB,2DAA2D;IAC3D,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;CAClC;AAMD;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
package/dist/types.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Shared types for VibeAround channel plugins.
3
+ *
4
+ * Re-exports the ACP SDK types plugins commonly need, plus SDK-specific types
5
+ * for block rendering, plugin manifests, and verbose configuration.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@vibearound/plugin-channel-sdk",
3
+ "version": "0.1.0",
4
+ "description": "VibeAround Plugin SDK — base classes and utilities for building channel plugins",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "files": ["dist", "src"],
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "dev": "tsc --watch",
19
+ "typecheck": "tsc --noEmit"
20
+ },
21
+ "dependencies": {
22
+ "@agentclientprotocol/sdk": "^0.17.1"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^22.0.0",
26
+ "typescript": "^5.7.0"
27
+ },
28
+ "engines": {
29
+ "node": ">=20.0.0"
30
+ }
31
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * ACP stdio connection helpers.
3
+ *
4
+ * Handles the boilerplate of wiring Node.js stdio streams to an ACP
5
+ * ClientSideConnection, performing the initialize handshake, and extracting
6
+ * plugin config from the host's _meta response.
7
+ */
8
+
9
+ import { Readable, Writable } from "node:stream";
10
+ import {
11
+ ClientSideConnection,
12
+ ndJsonStream,
13
+ PROTOCOL_VERSION,
14
+ type Agent,
15
+ type Client,
16
+ } from "@agentclientprotocol/sdk";
17
+ import type { PluginInitMeta } from "./types.js";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Public API
21
+ // ---------------------------------------------------------------------------
22
+
23
+ export interface PluginInfo {
24
+ name: string;
25
+ version: string;
26
+ }
27
+
28
+ export interface AgentInfo {
29
+ name?: string;
30
+ version?: string;
31
+ }
32
+
33
+ export interface ConnectResult {
34
+ /** ACP agent reference — call agent.prompt() to send messages. */
35
+ agent: Agent;
36
+ /** Plugin config and cache dir provided by the host at startup. */
37
+ meta: PluginInitMeta;
38
+ /** Info about the host agent (name, version) reported during initialize. */
39
+ agentInfo: AgentInfo;
40
+ /**
41
+ * The ACP connection. Await `conn.closed` to keep the process alive until
42
+ * the host disconnects.
43
+ */
44
+ conn: { readonly closed: Promise<void> };
45
+ }
46
+
47
+ /**
48
+ * Connect to the VibeAround host via stdio ACP.
49
+ *
50
+ * Sets up Node.js stdio streams → ACP transport, calls `initialize`, and
51
+ * returns the agent reference plus plugin config/meta from the host.
52
+ *
53
+ * @param pluginInfo - Name and version reported to the host
54
+ * @param makeClient - Factory called with the Agent; returns the Client
55
+ * implementation (sessionUpdate, requestPermission, extNotification).
56
+ * Capture the agent argument here if you need it before this function resolves.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * let agent!: Agent;
61
+ * const { meta, conn } = await connectToHost(
62
+ * { name: "vibearound-mybot", version: "0.1.0" },
63
+ * (a) => { agent = a; return myClient; },
64
+ * );
65
+ * ```
66
+ */
67
+ export async function connectToHost(
68
+ pluginInfo: PluginInfo,
69
+ makeClient: (agent: Agent) => Client,
70
+ ): Promise<ConnectResult> {
71
+ // Keep stdout clean for JSON-RPC — redirect all console output to stderr
72
+ redirectConsoleToStderr();
73
+
74
+ const inputStream = Readable.toWeb(process.stdin) as ReadableStream<Uint8Array>;
75
+ const outputStream = Writable.toWeb(process.stdout) as WritableStream<Uint8Array>;
76
+ const stream = ndJsonStream(outputStream, inputStream);
77
+
78
+ let capturedAgent!: Agent;
79
+ const wrappedMakeClient = (a: Agent): Client => {
80
+ capturedAgent = a;
81
+ return makeClient(a);
82
+ };
83
+
84
+ const conn = new ClientSideConnection(wrappedMakeClient, stream);
85
+
86
+ const initResponse = await conn.initialize({
87
+ protocolVersion: PROTOCOL_VERSION,
88
+ clientInfo: { name: pluginInfo.name, version: pluginInfo.version },
89
+ capabilities: {},
90
+ });
91
+
92
+ const rawMeta = (initResponse as Record<string, unknown>)._meta as
93
+ | Record<string, unknown>
94
+ | undefined;
95
+
96
+ const meta: PluginInitMeta = {
97
+ config: (rawMeta?.config ?? {}) as Record<string, unknown>,
98
+ cacheDir: rawMeta?.cacheDir as string | undefined,
99
+ };
100
+
101
+ const agentInfo: AgentInfo = {
102
+ name: initResponse.agentInfo?.name,
103
+ version: initResponse.agentInfo?.version,
104
+ };
105
+
106
+ return { agent: capturedAgent, meta, agentInfo, conn };
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Utilities
111
+ // ---------------------------------------------------------------------------
112
+
113
+ /**
114
+ * Normalize ACP ext notification method names.
115
+ *
116
+ * The ACP SDK prepends "_" to ext method names (e.g. "_channel/system_text").
117
+ * Strip the prefix to get the canonical method name used in plugin.json.
118
+ */
119
+ export function normalizeExtMethod(method: string): string {
120
+ return method.startsWith("_") ? method.slice(1) : method;
121
+ }
122
+
123
+ /**
124
+ * Redirect all console.* output to stderr.
125
+ *
126
+ * ACP uses stdout for JSON-RPC framing — any stray console output corrupts
127
+ * the protocol. Call this once at plugin startup (connectToHost does this
128
+ * automatically).
129
+ */
130
+ let _consoleRedirected = false;
131
+
132
+ export function redirectConsoleToStderr(): void {
133
+ if (_consoleRedirected) return;
134
+ _consoleRedirected = true;
135
+
136
+ const toStderr = (...args: unknown[]) =>
137
+ process.stderr.write(args.map(String).join(" ") + "\n");
138
+
139
+ console.log = toStderr;
140
+ console.info = toStderr;
141
+ console.warn = (...args: unknown[]) =>
142
+ process.stderr.write("[warn] " + args.map(String).join(" ") + "\n");
143
+ console.error = (...args: unknown[]) =>
144
+ process.stderr.write("[error] " + args.map(String).join(" ") + "\n");
145
+ console.debug = (...args: unknown[]) =>
146
+ process.stderr.write("[debug] " + args.map(String).join(" ") + "\n");
147
+ }
package/src/index.ts ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * @vibearound/plugin-channel-sdk
3
+ *
4
+ * Base classes and utilities for building VibeAround channel plugins.
5
+ *
6
+ * ## Quick start
7
+ *
8
+ * ```ts
9
+ * import {
10
+ * connectToHost,
11
+ * BlockRenderer,
12
+ * normalizeExtMethod,
13
+ * type Agent,
14
+ * type SessionNotification,
15
+ * } from "@vibearound/plugin-channel-sdk";
16
+ *
17
+ * class MyRenderer extends BlockRenderer<string> {
18
+ * protected async sendBlock(channelId, kind, content) {
19
+ * const msg = await myPlatform.send(channelId, content);
20
+ * return msg.id;
21
+ * }
22
+ * protected async editBlock(channelId, ref, kind, content, sealed) {
23
+ * await myPlatform.edit(ref, content);
24
+ * }
25
+ * }
26
+ *
27
+ * let agent!: Agent;
28
+ * const renderer = new MyRenderer();
29
+ *
30
+ * const { meta, conn } = await connectToHost(
31
+ * { name: "vibearound-mybot", version: "0.1.0" },
32
+ * (a) => {
33
+ * agent = a;
34
+ * return {
35
+ * sessionUpdate: async (n) => renderer.onSessionUpdate(n),
36
+ * requestPermission: async (p) => ({
37
+ * outcome: { outcome: "selected", optionId: p.options![0].optionId },
38
+ * }),
39
+ * extNotification: async (method, params) => {
40
+ * switch (normalizeExtMethod(method)) {
41
+ * case "channel/system_text":
42
+ * await myPlatform.send(params.channelId as string, params.text as string);
43
+ * break;
44
+ * }
45
+ * },
46
+ * };
47
+ * },
48
+ * );
49
+ *
50
+ * const botToken = meta.config.bot_token as string;
51
+ * // … start your platform bot …
52
+ * await conn.closed;
53
+ * ```
54
+ */
55
+
56
+ // Connection helpers
57
+ export { connectToHost, normalizeExtMethod, redirectConsoleToStderr } from "./connection.js";
58
+ export type { PluginInfo, ConnectResult, AgentInfo } from "./connection.js";
59
+
60
+ // Block renderer
61
+ export { BlockRenderer } from "./renderer.js";
62
+
63
+ // Types (re-exports ACP SDK types + SDK-specific types)
64
+ export type {
65
+ // ACP SDK
66
+ Agent,
67
+ Client,
68
+ SessionNotification,
69
+ RequestPermissionRequest,
70
+ RequestPermissionResponse,
71
+ // SDK
72
+ BlockKind,
73
+ VerboseConfig,
74
+ BlockRendererOptions,
75
+ PluginCapabilities,
76
+ PluginManifest,
77
+ PluginInitMeta,
78
+ } from "./types.js";