@vibearound/plugin-channel-sdk 0.1.2 → 0.4.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.
package/src/index.ts CHANGED
@@ -1,79 +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
+ // ---------------------------------------------------------------------------
43
+
44
+ // Entry point
45
+ export { runChannelPlugin } from "./plugin.js";
59
46
 
60
- // Block renderer
47
+ // Base class for stream rendering
61
48
  export { BlockRenderer } from "./renderer.js";
62
49
 
63
- // Types (re-exports ACP SDK types + SDK-specific types)
50
+ // Interfaces the plugin implements
51
+ export type {
52
+ ChannelBot,
53
+ ChannelPluginLogger,
54
+ CreateBotContext,
55
+ RunChannelPluginSpec,
56
+ VerboseOptions,
57
+ } from "./plugin.js";
58
+
59
+ // Types used in BlockRenderer overrides
64
60
  export type {
65
- // ACP SDK
66
- Agent,
67
- Client,
68
- ContentBlock,
69
- SessionNotification,
70
- RequestPermissionRequest,
71
- RequestPermissionResponse,
72
- // SDK
73
61
  BlockKind,
62
+ CommandEntry,
74
63
  VerboseConfig,
75
64
  BlockRendererOptions,
76
- PluginCapabilities,
77
- PluginManifest,
78
- PluginInitMeta,
79
65
  } from "./types.js";
66
+
67
+ // ACP types the plugin needs for prompt content
68
+ export type {
69
+ Agent,
70
+ ContentBlock,
71
+ SessionNotification,
72
+ } from "./types.js";
73
+
74
+ // Error utility
75
+ export { extractErrorMessage } from "./errors.js";
package/src/plugin.ts ADDED
@@ -0,0 +1,247 @@
1
+ /**
2
+ * runChannelPlugin — the SDK entry point for every channel plugin.
3
+ *
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).
7
+ *
8
+ * ## Usage
9
+ *
10
+ * ```ts
11
+ * import { runChannelPlugin } from "@vibearound/plugin-channel-sdk";
12
+ *
13
+ * runChannelPlugin({
14
+ * name: "vibearound-slack",
15
+ * version: "0.1.0",
16
+ * requiredConfig: ["bot_token", "app_token"],
17
+ * createBot: ({ config, agent, log, cacheDir }) =>
18
+ * new SlackBot({ ... }, agent, log, cacheDir),
19
+ * createRenderer: (bot, log, verbose) =>
20
+ * new SlackRenderer(bot, log, verbose),
21
+ * });
22
+ * ```
23
+ */
24
+
25
+ import os from "node:os";
26
+ import path from "node:path";
27
+ import type { Agent } from "@agentclientprotocol/sdk";
28
+
29
+ import { connectToHost, stripExtPrefix } from "./connection.js";
30
+ import { extractErrorMessage } from "./errors.js";
31
+ import { BlockRenderer } from "./renderer.js";
32
+ import type {
33
+ RequestPermissionRequest,
34
+ RequestPermissionResponse,
35
+ SessionNotification,
36
+ } from "./types.js";
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Public types
40
+ // ---------------------------------------------------------------------------
41
+
42
+ export type ChannelPluginLogger = (level: string, msg: string) => void;
43
+
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. */
55
+ start(): Promise<void> | void;
56
+ /** Disconnect and clean up. */
57
+ stop(): Promise<void> | void;
58
+ }
59
+
60
+ export interface CreateBotContext {
61
+ config: Record<string, unknown>;
62
+ agent: Agent;
63
+ log: ChannelPluginLogger;
64
+ cacheDir: string;
65
+ }
66
+
67
+ export interface VerboseOptions {
68
+ showThinking: boolean;
69
+ showToolUse: boolean;
70
+ }
71
+
72
+ export interface RunChannelPluginSpec<
73
+ TBot extends ChannelBot<TRenderer>,
74
+ TRenderer extends BlockRenderer<any>,
75
+ > {
76
+ /** Plugin name reported during ACP initialize (e.g. "vibearound-slack"). */
77
+ name: string;
78
+
79
+ /** Plugin version reported during ACP initialize. */
80
+ version: string;
81
+
82
+ /**
83
+ * Config keys that MUST be present. Plugin fails fast if any are missing.
84
+ */
85
+ requiredConfig?: string[];
86
+
87
+ /** Factory: build the platform bot. */
88
+ createBot: (ctx: CreateBotContext) => TBot | Promise<TBot>;
89
+
90
+ /**
91
+ * Factory: build the renderer (extends BlockRenderer).
92
+ * Only implements platform-specific sendText/sendBlock/editBlock.
93
+ */
94
+ createRenderer: (
95
+ bot: TBot,
96
+ log: ChannelPluginLogger,
97
+ verbose: VerboseOptions,
98
+ ) => TRenderer;
99
+
100
+ /**
101
+ * Optional hook invoked after bot constructed but before start().
102
+ */
103
+ afterCreate?: (bot: TBot, log: ChannelPluginLogger) => Promise<void> | void;
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Implementation
108
+ // ---------------------------------------------------------------------------
109
+
110
+ /**
111
+ * Run a channel plugin.
112
+ *
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.
116
+ */
117
+ export async function runChannelPlugin<
118
+ TBot extends ChannelBot<TRenderer>,
119
+ TRenderer extends BlockRenderer<any>,
120
+ >(spec: RunChannelPluginSpec<TBot, TRenderer>): Promise<void> {
121
+ const prefix = `[${spec.name.replace(/^vibearound-/, "")}-plugin]`;
122
+ const log: ChannelPluginLogger = (level, msg) => {
123
+ process.stderr.write(`${prefix}[${level}] ${msg}\n`);
124
+ };
125
+
126
+ try {
127
+ await runInner(spec, log);
128
+ } catch (err) {
129
+ log("error", `fatal: ${extractErrorMessage(err)}`);
130
+ process.exit(1);
131
+ }
132
+ }
133
+
134
+ async function runInner<
135
+ TBot extends ChannelBot<TRenderer>,
136
+ TRenderer extends BlockRenderer<any>,
137
+ >(
138
+ spec: RunChannelPluginSpec<TBot, TRenderer>,
139
+ log: ChannelPluginLogger,
140
+ ): Promise<void> {
141
+ log("info", "initializing ACP connection...");
142
+
143
+ let renderer: TRenderer | null = null;
144
+
145
+ const { agent, meta, agentInfo, conn } = await connectToHost(
146
+ { name: spec.name, version: spec.version },
147
+ () => ({
148
+ async sessionUpdate(params: SessionNotification): Promise<void> {
149
+ renderer?.onSessionUpdate(params);
150
+ },
151
+
152
+ async requestPermission(
153
+ params: RequestPermissionRequest,
154
+ ): Promise<RequestPermissionResponse> {
155
+ const first = params.options?.[0];
156
+ if (first) {
157
+ return { outcome: { outcome: "selected", optionId: first.optionId } };
158
+ }
159
+ throw new Error("No permission options provided");
160
+ },
161
+
162
+ async extNotification(
163
+ method: string,
164
+ params: Record<string, unknown>,
165
+ ): Promise<void> {
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
+ }
173
+ break;
174
+ }
175
+ case "va/agent_ready": {
176
+ const agentName = typeof params.agent === "string" ? params.agent : "unknown";
177
+ const version = typeof params.version === "string" ? params.version : "";
178
+ log("info", `agent_ready: ${agentName} v${version}`);
179
+ if (chatId && renderer) {
180
+ renderer.onAgentReady(chatId, agentName, version);
181
+ }
182
+ break;
183
+ }
184
+ case "va/session_ready": {
185
+ const sessionId = typeof params.sessionId === "string" ? params.sessionId : "";
186
+ log("info", `session_ready: ${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
+ }
198
+ break;
199
+ }
200
+ default:
201
+ log("warn", `unhandled ext_notification: ${method}`);
202
+ }
203
+ },
204
+ }),
205
+ );
206
+
207
+ const config = meta.config;
208
+
209
+ for (const key of spec.requiredConfig ?? []) {
210
+ if (config[key] === undefined || config[key] === null || config[key] === "") {
211
+ throw new Error(`${key} is required in config`);
212
+ }
213
+ }
214
+
215
+ const cacheDir =
216
+ meta.cacheDir ?? path.join(os.homedir(), ".vibearound", ".cache");
217
+
218
+ log(
219
+ "info",
220
+ `initialized, host=${agentInfo.name ?? "unknown"} cacheDir=${cacheDir}`,
221
+ );
222
+
223
+ const bot = await spec.createBot({ config, agent, log, cacheDir });
224
+
225
+ if (spec.afterCreate) {
226
+ await spec.afterCreate(bot, log);
227
+ }
228
+
229
+ const verboseRaw = config.verbose as
230
+ | { show_thinking?: boolean; show_tool_use?: boolean }
231
+ | undefined;
232
+ const verbose: VerboseOptions = {
233
+ showThinking: verboseRaw?.show_thinking ?? false,
234
+ showToolUse: verboseRaw?.show_tool_use ?? false,
235
+ };
236
+
237
+ renderer = spec.createRenderer(bot, log, verbose);
238
+ bot.setStreamHandler(renderer);
239
+
240
+ await bot.start();
241
+ log("info", "plugin started");
242
+
243
+ await conn.closed;
244
+ log("info", "connection closed, shutting down");
245
+ await bot.stop();
246
+ process.exit(0);
247
+ }