@vibearound/plugin-channel-sdk 0.3.0 → 0.4.1

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.
package/src/index.ts CHANGED
@@ -1,94 +1,75 @@
1
1
  /**
2
2
  * @vibearound/plugin-channel-sdk
3
3
  *
4
- * Base classes and utilities for building VibeAround channel plugins.
4
+ * SDK for building VibeAround channel plugins.
5
5
  *
6
6
  * ## Quick start
7
7
  *
8
8
  * ```ts
9
9
  * import {
10
- * connectToHost,
10
+ * runChannelPlugin,
11
11
  * BlockRenderer,
12
- * normalizeExtMethod,
13
- * type Agent,
14
- * type SessionNotification,
12
+ * type ChannelBot,
13
+ * type BlockKind,
15
14
  * } from "@vibearound/plugin-channel-sdk";
16
15
  *
17
16
  * 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
- * }
17
+ * protected async sendText(chatId: string, text: string) { ... }
18
+ * protected async sendBlock(chatId: string, kind: BlockKind, content: string) { ... }
25
19
  * }
26
20
  *
27
- * let agent!: Agent;
28
- * const renderer = new MyRenderer();
21
+ * runChannelPlugin({
22
+ * name: "vibearound-mybot",
23
+ * version: "0.1.0",
24
+ * requiredConfig: ["bot_token"],
25
+ * createBot: ({ config, agent, log, cacheDir }) => new MyBot(...),
26
+ * createRenderer: (bot, log, verbose) => new MyRenderer(bot, log, verbose),
27
+ * });
28
+ * ```
29
+ *
30
+ * ## Advanced / low-level usage
29
31
  *
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
- * );
32
+ * For plugins that need custom ACP lifecycle control (e.g. weixin-openclaw-bridge),
33
+ * import from the `advanced` subpath:
49
34
  *
50
- * const botToken = meta.config.bot_token as string;
51
- * // start your platform bot …
52
- * await conn.closed;
35
+ * ```ts
36
+ * import { connectToHost } from "@vibearound/plugin-channel-sdk/advanced";
53
37
  * ```
54
38
  */
55
39
 
56
- // Connection helpers
57
- export { connectToHost, normalizeExtMethod, redirectConsoleToStderr } from "./connection.js";
58
- export type { PluginInfo, ConnectResult, AgentInfo } from "./connection.js";
40
+ // ---------------------------------------------------------------------------
41
+ // High-level API what plugin developers use
42
+ // ---------------------------------------------------------------------------
59
43
 
60
- // Error normalization
61
- export { extractErrorMessage } from "./errors.js";
44
+ // Entry point
45
+ export { runChannelPlugin } from "./plugin.js";
62
46
 
63
- // Plugin runner (absorbs the main.ts boilerplate)
64
- export { runChannelPlugin } from "./run-plugin.js";
47
+ // Base class for stream rendering
48
+ export { BlockRenderer } from "./renderer.js";
49
+
50
+ // Interfaces the plugin implements
65
51
  export type {
66
52
  ChannelBot,
67
53
  ChannelPluginLogger,
68
- ChannelStreamHandler,
69
54
  CreateBotContext,
70
55
  RunChannelPluginSpec,
71
56
  VerboseOptions,
72
- } from "./run-plugin.js";
73
-
74
- // Block renderer
75
- export { BlockRenderer } from "./renderer.js";
57
+ } from "./plugin.js";
76
58
 
59
+ // Types used in BlockRenderer overrides
60
+ export type {
61
+ BlockKind,
62
+ CommandEntry,
63
+ VerboseConfig,
64
+ BlockRendererOptions,
65
+ } from "./types.js";
77
66
 
78
- // Types (re-exports ACP SDK types + SDK-specific types)
67
+ // ACP types the plugin needs for prompt content
79
68
  export type {
80
- // ACP SDK
81
69
  Agent,
82
- Client,
83
70
  ContentBlock,
84
71
  SessionNotification,
85
- RequestPermissionRequest,
86
- RequestPermissionResponse,
87
- // SDK
88
- BlockKind,
89
- VerboseConfig,
90
- BlockRendererOptions,
91
- PluginCapabilities,
92
- PluginManifest,
93
- PluginInitMeta,
94
72
  } from "./types.js";
73
+
74
+ // Error utility
75
+ export { extractErrorMessage } from "./errors.js";
@@ -1,34 +1,23 @@
1
1
  /**
2
- * runChannelPlugin — shared main.ts boilerplate for every channel plugin.
2
+ * runChannelPlugin — the SDK entry point for every channel plugin.
3
3
  *
4
- * Each channel plugin used to have a ~120-line `main.ts` that was 85%
5
- * identical across Slack, Telegram, Discord, DingTalk, WeCom, etc:
6
- * connect to host, validate config keys, wire the standard sessionUpdate
7
- * and extNotification handlers, create the stream handler, start the bot,
8
- * wait for disconnect, stop. This helper absorbs that boilerplate so each
9
- * plugin's `main.ts` reduces to ~20 lines — a factory for the bot and a
10
- * factory for the stream handler.
4
+ * Handles the full ACP lifecycle: connect to host, validate config, create
5
+ * bot + renderer, start the bot, then await disconnect and stop. The plugin
6
+ * only implements platform-specific transport (sendText, sendBlock, editBlock).
11
7
  *
12
8
  * ## Usage
13
9
  *
14
10
  * ```ts
15
11
  * import { runChannelPlugin } from "@vibearound/plugin-channel-sdk";
16
- * import { SlackBot } from "./bot.js";
17
- * import { AgentStreamHandler } from "./agent-stream.js";
18
12
  *
19
13
  * runChannelPlugin({
20
14
  * name: "vibearound-slack",
21
15
  * version: "0.1.0",
22
16
  * requiredConfig: ["bot_token", "app_token"],
23
17
  * createBot: ({ config, agent, log, cacheDir }) =>
24
- * new SlackBot(
25
- * { bot_token: config.bot_token as string, app_token: config.app_token as string },
26
- * agent,
27
- * log,
28
- * cacheDir,
29
- * ),
30
- * createStreamHandler: (bot, log, verbose) =>
31
- * new AgentStreamHandler(bot, log, verbose),
18
+ * new SlackBot({ ... }, agent, log, cacheDir),
19
+ * createRenderer: (bot, log, verbose) =>
20
+ * new SlackRenderer(bot, log, verbose),
32
21
  * });
33
22
  * ```
34
23
  */
@@ -37,8 +26,9 @@ import os from "node:os";
37
26
  import path from "node:path";
38
27
  import type { Agent } from "@agentclientprotocol/sdk";
39
28
 
40
- import { connectToHost, normalizeExtMethod } from "./connection.js";
29
+ import { connectToHost, stripExtPrefix } from "./connection.js";
41
30
  import { extractErrorMessage } from "./errors.js";
31
+ import { BlockRenderer } from "./renderer.js";
42
32
  import type {
43
33
  RequestPermissionRequest,
44
34
  RequestPermissionResponse,
@@ -51,16 +41,19 @@ import type {
51
41
 
52
42
  export type ChannelPluginLogger = (level: string, msg: string) => void;
53
43
 
54
- export interface ChannelStreamHandler {
55
- onSessionUpdate(params: SessionNotification): void;
56
- onSystemText(text: string): void;
57
- onAgentReady(agent: string, version: string): void;
58
- onSessionReady(sessionId: string): void;
59
- }
60
-
61
- export interface ChannelBot<THandler extends ChannelStreamHandler = ChannelStreamHandler> {
62
- setStreamHandler(handler: THandler): void;
44
+ /**
45
+ * The platform bot — handles IM connectivity and message transport.
46
+ *
47
+ * Plugins implement this interface on their bot class. The SDK calls these
48
+ * methods during the plugin lifecycle.
49
+ */
50
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
+ export interface ChannelBot<TRenderer extends BlockRenderer<any> = BlockRenderer<any>> {
52
+ /** Wire the renderer to receive streaming events. */
53
+ setStreamHandler(handler: TRenderer): void;
54
+ /** Connect to the IM platform and start receiving messages. */
63
55
  start(): Promise<void> | void;
56
+ /** Disconnect and clean up. */
64
57
  stop(): Promise<void> | void;
65
58
  }
66
59
 
@@ -77,8 +70,8 @@ export interface VerboseOptions {
77
70
  }
78
71
 
79
72
  export interface RunChannelPluginSpec<
80
- TBot extends ChannelBot<THandler>,
81
- THandler extends ChannelStreamHandler,
73
+ TBot extends ChannelBot<TRenderer>,
74
+ TRenderer extends BlockRenderer<any>,
82
75
  > {
83
76
  /** Plugin name reported during ACP initialize (e.g. "vibearound-slack"). */
84
77
  name: string;
@@ -87,30 +80,25 @@ export interface RunChannelPluginSpec<
87
80
  version: string;
88
81
 
89
82
  /**
90
- * Config keys that MUST be present on `meta.config`. Plugin startup
91
- * fails with a clear error if any are missing. Keep to primitives
92
- * (strings/booleans); deeper validation belongs in the bot constructor.
83
+ * Config keys that MUST be present. Plugin fails fast if any are missing.
93
84
  */
94
85
  requiredConfig?: string[];
95
86
 
96
- /** Factory: build the platform bot from host-supplied config + agent. */
87
+ /** Factory: build the platform bot. */
97
88
  createBot: (ctx: CreateBotContext) => TBot | Promise<TBot>;
98
89
 
99
90
  /**
100
- * Factory: build the agent stream handler for this plugin. The handler
101
- * is wired to the bot via `bot.setStreamHandler(handler)` before the
102
- * bot is started.
91
+ * Factory: build the renderer (extends BlockRenderer).
92
+ * Only implements platform-specific sendText/sendBlock/editBlock.
103
93
  */
104
- createStreamHandler: (
94
+ createRenderer: (
105
95
  bot: TBot,
106
96
  log: ChannelPluginLogger,
107
97
  verbose: VerboseOptions,
108
- ) => THandler;
98
+ ) => TRenderer;
109
99
 
110
100
  /**
111
- * Optional hook invoked after the bot has been constructed but before
112
- * `start()` is called. Use this for one-off initialization that needs
113
- * to log diagnostic info (e.g. Telegram's `probe()`).
101
+ * Optional hook invoked after bot constructed but before start().
114
102
  */
115
103
  afterCreate?: (bot: TBot, log: ChannelPluginLogger) => Promise<void> | void;
116
104
  }
@@ -120,18 +108,16 @@ export interface RunChannelPluginSpec<
120
108
  // ---------------------------------------------------------------------------
121
109
 
122
110
  /**
123
- * Run a channel plugin to completion.
124
- *
125
- * Performs the ACP initialize handshake, validates required config,
126
- * constructs the bot + stream handler, starts the bot, waits for the host
127
- * connection to close, then stops the bot and exits the process.
111
+ * Run a channel plugin.
128
112
  *
129
- * Never returns under normal operation the process exits at the end.
113
+ * Handles the full ACP lifecycle: connect to host, validate config,
114
+ * construct bot + renderer, start the bot, then wait for the host
115
+ * to disconnect before stopping and exiting.
130
116
  */
131
117
  export async function runChannelPlugin<
132
- TBot extends ChannelBot<THandler>,
133
- THandler extends ChannelStreamHandler,
134
- >(spec: RunChannelPluginSpec<TBot, THandler>): Promise<void> {
118
+ TBot extends ChannelBot<TRenderer>,
119
+ TRenderer extends BlockRenderer<any>,
120
+ >(spec: RunChannelPluginSpec<TBot, TRenderer>): Promise<void> {
135
121
  const prefix = `[${spec.name.replace(/^vibearound-/, "")}-plugin]`;
136
122
  const log: ChannelPluginLogger = (level, msg) => {
137
123
  process.stderr.write(`${prefix}[${level}] ${msg}\n`);
@@ -146,21 +132,21 @@ export async function runChannelPlugin<
146
132
  }
147
133
 
148
134
  async function runInner<
149
- TBot extends ChannelBot<THandler>,
150
- THandler extends ChannelStreamHandler,
135
+ TBot extends ChannelBot<TRenderer>,
136
+ TRenderer extends BlockRenderer<any>,
151
137
  >(
152
- spec: RunChannelPluginSpec<TBot, THandler>,
138
+ spec: RunChannelPluginSpec<TBot, TRenderer>,
153
139
  log: ChannelPluginLogger,
154
140
  ): Promise<void> {
155
141
  log("info", "initializing ACP connection...");
156
142
 
157
- let streamHandler: THandler | null = null;
143
+ let renderer: TRenderer | null = null;
158
144
 
159
145
  const { agent, meta, agentInfo, conn } = await connectToHost(
160
146
  { name: spec.name, version: spec.version },
161
147
  () => ({
162
148
  async sessionUpdate(params: SessionNotification): Promise<void> {
163
- streamHandler?.onSessionUpdate(params);
149
+ renderer?.onSessionUpdate(params);
164
150
  },
165
151
 
166
152
  async requestPermission(
@@ -177,23 +163,38 @@ async function runInner<
177
163
  method: string,
178
164
  params: Record<string, unknown>,
179
165
  ): Promise<void> {
180
- switch (normalizeExtMethod(method)) {
181
- case "channel/system_text": {
182
- const text = params.text as string;
183
- streamHandler?.onSystemText(text);
166
+ const chatId = typeof params.chatId === "string" ? params.chatId : undefined;
167
+ switch (stripExtPrefix(method)) {
168
+ case "va/system_text": {
169
+ const text = typeof params.text === "string" ? params.text : "";
170
+ if (chatId && renderer) {
171
+ renderer.onSystemText(chatId, text);
172
+ }
184
173
  break;
185
174
  }
186
- case "channel/agent_ready": {
187
- const agentName = params.agent as string;
188
- const version = params.version as string;
175
+ case "va/agent_ready": {
176
+ const agentName = typeof params.agent === "string" ? params.agent : "unknown";
177
+ const version = typeof params.version === "string" ? params.version : "";
189
178
  log("info", `agent_ready: ${agentName} v${version}`);
190
- streamHandler?.onAgentReady(agentName, version);
179
+ if (chatId && renderer) {
180
+ renderer.onAgentReady(chatId, agentName, version);
181
+ }
191
182
  break;
192
183
  }
193
- case "channel/session_ready": {
194
- const sessionId = params.sessionId as string;
184
+ case "va/session_ready": {
185
+ const sessionId = typeof params.sessionId === "string" ? params.sessionId : "";
195
186
  log("info", `session_ready: ${sessionId}`);
196
- streamHandler?.onSessionReady(sessionId);
187
+ if (chatId && renderer) {
188
+ renderer.onSessionReady(chatId, sessionId);
189
+ }
190
+ break;
191
+ }
192
+ case "va/command_menu": {
193
+ const systemCommands = Array.isArray(params.systemCommands) ? params.systemCommands : [];
194
+ const agentCommands = Array.isArray(params.agentCommands) ? params.agentCommands : [];
195
+ if (chatId && renderer) {
196
+ renderer.onCommandMenu(chatId, systemCommands, agentCommands);
197
+ }
197
198
  break;
198
199
  }
199
200
  default:
@@ -205,9 +206,6 @@ async function runInner<
205
206
 
206
207
  const config = meta.config;
207
208
 
208
- // Validate required config keys up front so a misconfigured plugin fails
209
- // with a clear error instead of some downstream "undefined is not a
210
- // string" crash in the bot constructor.
211
209
  for (const key of spec.requiredConfig ?? []) {
212
210
  if (config[key] === undefined || config[key] === null || config[key] === "") {
213
211
  throw new Error(`${key} is required in config`);
@@ -236,8 +234,8 @@ async function runInner<
236
234
  showToolUse: verboseRaw?.show_tool_use ?? false,
237
235
  };
238
236
 
239
- streamHandler = spec.createStreamHandler(bot, log, verbose);
240
- bot.setStreamHandler(streamHandler);
237
+ renderer = spec.createRenderer(bot, log, verbose);
238
+ bot.setStreamHandler(renderer);
241
239
 
242
240
  await bot.start();
243
241
  log("info", "plugin started");