@voiceclaw/voiceclaw-plugin 1.1.6 → 1.1.9

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/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  interface PluginApi {
2
2
  config: Record<string, any>;
3
+ runtime: any;
3
4
  logger: {
4
5
  info: (msg: string) => void;
5
6
  error: (msg: string) => void;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { voiceClawPlugin, setLogger } from './src/channel.js';
1
+ import { voiceClawPlugin, setLogger, setRuntime, setConfig } from './src/channel.js';
2
2
  import { voiceClawConfigSchema } from './src/types.js';
3
3
  export default {
4
4
  id: 'voiceclaw-plugin',
@@ -7,6 +7,8 @@ export default {
7
7
  configSchema: voiceClawConfigSchema,
8
8
  register(api) {
9
9
  setLogger(api.logger);
10
+ setRuntime(api.runtime);
11
+ setConfig(api.config);
10
12
  api.registerChannel({ plugin: voiceClawPlugin });
11
13
  api.logger.info('VoiceClaw channel plugin registered');
12
14
  },
@@ -1,4 +1,4 @@
1
- import type { ResolvedAccount } from './types';
1
+ import type { ResolvedAccount } from './types.js';
2
2
  interface Logger {
3
3
  info: (msg: string) => void;
4
4
  error: (msg: string) => void;
@@ -35,6 +35,28 @@ export declare const voiceClawPlugin: {
35
35
  config: {
36
36
  listAccountIds: (cfg: Record<string, any>) => string[];
37
37
  resolveAccount: (cfg: Record<string, any>, accountId?: string) => ResolvedAccount | undefined;
38
+ isConfigured: (account: ResolvedAccount) => Promise<boolean>;
39
+ unconfiguredReason: () => string;
40
+ isEnabled: (account: ResolvedAccount) => boolean;
41
+ disabledReason: () => string;
42
+ describeAccount: (account: ResolvedAccount) => {
43
+ accountId: string;
44
+ enabled: boolean;
45
+ configured: boolean;
46
+ };
47
+ setAccountEnabled: ({ cfg, accountId, enabled }: any) => any;
48
+ deleteAccount: ({ cfg, accountId }: any) => any;
49
+ };
50
+ setup: {
51
+ resolveAccountId: ({ accountId }: any) => any;
52
+ applyAccountName: ({ cfg, accountId, name }: any) => any;
53
+ applyAccountConfig: ({ cfg, accountId, input }: any) => any;
54
+ };
55
+ onboarding: {
56
+ configure: (ctx: any) => Promise<"skip" | {
57
+ cfg: any;
58
+ accountId: string;
59
+ }>;
38
60
  };
39
61
  outbound: {
40
62
  deliveryMode: "direct";
@@ -42,11 +64,31 @@ export declare const voiceClawPlugin: {
42
64
  ok: boolean;
43
65
  }>;
44
66
  };
45
- onboarding: {
46
- configure: (ctx: any) => Promise<"skip" | {
47
- cfg: any;
67
+ status: {
68
+ defaultRuntime: {
48
69
  accountId: string;
70
+ running: boolean;
71
+ connected: boolean;
72
+ lastMessageAt: null;
73
+ lastError: null;
74
+ };
75
+ buildChannelSummary: ({ account, snapshot }: any) => Promise<{
76
+ configured: boolean;
77
+ running: any;
78
+ connected: any;
79
+ lastMessageAt: any;
80
+ lastError: any;
81
+ }>;
82
+ buildAccountSnapshot: ({ account, runtime }: any) => Promise<{
83
+ accountId: any;
84
+ enabled: boolean;
85
+ configured: boolean;
86
+ running: any;
87
+ connected: any;
88
+ lastMessageAt: any;
89
+ lastError: any;
49
90
  }>;
91
+ resolveAccountState: ({ configured }: any) => "paired" | "not paired";
50
92
  };
51
93
  gateway: {
52
94
  startAccount: (ctx: any) => Promise<void>;
@@ -54,5 +96,6 @@ export declare const voiceClawPlugin: {
54
96
  };
55
97
  };
56
98
  export declare function setLogger(logger: Logger): void;
57
- export declare function setInboundHandler(handler: (text: string, accountId: string) => Promise<void>): void;
99
+ export declare function setRuntime(runtime: any): void;
100
+ export declare function setConfig(config: any): void;
58
101
  export {};
@@ -1,8 +1,8 @@
1
1
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
- import { DEFAULT_WORKER_URL, resolveAccountIds, resolveVoiceClawAccount, voiceClawConfigSchema, } from './types';
5
- import { RelayWsClient } from './ws-client';
4
+ import { DEFAULT_WORKER_URL, resolveAccountIds, resolveVoiceClawAccount, voiceClawConfigSchema, } from './types.js';
5
+ import { RelayWsClient } from './ws-client.js';
6
6
  // ── Token persistence ──────────────────────────────────────────
7
7
  const DATA_DIR = join(homedir(), '.voiceclaw');
8
8
  function tokenPath(accountId) {
@@ -54,8 +54,11 @@ async function resolveToken(accountId, account, logger) {
54
54
  }
55
55
  // ── Channel plugin ─────────────────────────────────────────────
56
56
  const clients = new Map();
57
- let handleInbound = null;
58
57
  let pluginLogger = null;
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
+ let pluginRuntime = null;
60
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
+ let pluginConfig = null;
59
62
  export const voiceClawPlugin = {
60
63
  id: 'voiceclaw',
61
64
  meta: {
@@ -71,39 +74,99 @@ export const voiceClawPlugin = {
71
74
  config: {
72
75
  listAccountIds: (cfg) => resolveAccountIds(cfg),
73
76
  resolveAccount: (cfg, accountId) => resolveVoiceClawAccount(cfg, accountId),
77
+ isConfigured: async (account) => {
78
+ // Configured if there's a saved token or a pairing code
79
+ return loadSavedToken(account.accountId) !== null || Boolean(account.pairingCode);
80
+ },
81
+ unconfiguredReason: () => 'no pairing code',
82
+ isEnabled: (account) => account.enabled !== false,
83
+ disabledReason: () => 'disabled',
84
+ describeAccount: (account) => ({
85
+ accountId: account.accountId,
86
+ enabled: account.enabled !== false,
87
+ configured: loadSavedToken(account.accountId) !== null || Boolean(account.pairingCode),
88
+ }),
89
+ setAccountEnabled: ({ cfg, accountId, enabled }) => {
90
+ const accounts = { ...cfg.channels?.voiceclaw?.accounts };
91
+ const existing = accounts[accountId] ?? {};
92
+ return {
93
+ ...cfg,
94
+ channels: {
95
+ ...cfg.channels,
96
+ voiceclaw: {
97
+ ...cfg.channels?.voiceclaw,
98
+ accounts: {
99
+ ...accounts,
100
+ [accountId]: { ...existing, enabled },
101
+ },
102
+ },
103
+ },
104
+ };
105
+ },
106
+ deleteAccount: ({ cfg, accountId }) => {
107
+ const accounts = { ...cfg.channels?.voiceclaw?.accounts };
108
+ delete accounts[accountId];
109
+ return {
110
+ ...cfg,
111
+ channels: {
112
+ ...cfg.channels,
113
+ voiceclaw: {
114
+ ...cfg.channels?.voiceclaw,
115
+ accounts: Object.keys(accounts).length ? accounts : undefined,
116
+ },
117
+ },
118
+ };
119
+ },
74
120
  },
75
- outbound: {
76
- deliveryMode: 'direct',
77
- sendText: async (params) => {
78
- pluginLogger?.info(`VoiceClaw sendText called, keys: ${JSON.stringify(Object.keys(params))}`);
79
- const ctx = params.context;
80
- const account = ctx?.account;
81
- const accountId = account?.accountId ?? params.accountId ?? 'default';
82
- const text = params.text ?? '';
83
- pluginLogger?.info(`VoiceClaw sendText: accountId="${accountId}", text="${text.slice(0, 50)}..."`);
84
- const client = clients.get(accountId);
85
- if (!client || client.connectionState !== 'connected') {
86
- pluginLogger?.warn(`VoiceClaw: No connection for "${accountId}", reply dropped. clients: [${[...clients.keys()].join(',')}]`);
87
- return { ok: false };
88
- }
89
- client.send({
90
- type: 'agent_reply',
91
- id: crypto.randomUUID(),
92
- replyTo: '',
93
- text,
94
- ts: new Date().toISOString(),
95
- });
96
- pluginLogger?.info(`VoiceClaw: Reply sent to "${accountId}"`);
97
- return { ok: true };
121
+ // ── Setup adapter (required for `openclaw channels add`) ─────
122
+ setup: {
123
+ resolveAccountId: ({ accountId }) => accountId || 'default',
124
+ applyAccountName: ({ cfg, accountId, name }) => {
125
+ const accounts = { ...cfg.channels?.voiceclaw?.accounts };
126
+ const existing = accounts[accountId] ?? {};
127
+ return {
128
+ ...cfg,
129
+ channels: {
130
+ ...cfg.channels,
131
+ voiceclaw: {
132
+ ...cfg.channels?.voiceclaw,
133
+ accounts: {
134
+ ...accounts,
135
+ [accountId]: { ...existing, name },
136
+ },
137
+ },
138
+ },
139
+ };
140
+ },
141
+ applyAccountConfig: ({ cfg, accountId, input }) => {
142
+ const accounts = { ...cfg.channels?.voiceclaw?.accounts };
143
+ const existing = accounts[accountId] ?? {};
144
+ return {
145
+ ...cfg,
146
+ channels: {
147
+ ...cfg.channels,
148
+ voiceclaw: {
149
+ ...cfg.channels?.voiceclaw,
150
+ accounts: {
151
+ ...accounts,
152
+ [accountId]: {
153
+ ...existing,
154
+ ...(input.pairingCode ? { pairingCode: input.pairingCode } : {}),
155
+ enabled: true,
156
+ },
157
+ },
158
+ },
159
+ },
160
+ };
98
161
  },
99
162
  },
163
+ // ── Onboarding (interactive setup via `openclaw channels add`) ─
100
164
  onboarding: {
101
165
  configure: async (ctx) => {
102
- // Support: openclaw channels add --channel voiceclaw --token <code>
103
166
  let pairingCode = ctx.options?.token;
104
167
  if (!pairingCode && ctx.prompter) {
105
168
  pairingCode = await ctx.prompter.text({
106
- message: 'Enter the 6-digit pairing code from the VoiceClaw app:',
169
+ message: 'Enter the pairing code from the VoiceClaw app:',
107
170
  validate: (v) => v.trim().length >= 4
108
171
  ? undefined
109
172
  : 'Pairing code must be at least 4 characters',
@@ -117,18 +180,72 @@ export const voiceClawPlugin = {
117
180
  cfg.channels = {};
118
181
  if (!cfg.channels.voiceclaw)
119
182
  cfg.channels.voiceclaw = {};
120
- cfg.channels.voiceclaw.pairingCode = pairingCode.trim();
183
+ if (!cfg.channels.voiceclaw.accounts)
184
+ cfg.channels.voiceclaw.accounts = {};
185
+ cfg.channels.voiceclaw.accounts.default = {
186
+ pairingCode: pairingCode.trim(),
187
+ enabled: true,
188
+ };
121
189
  return { cfg, accountId: 'default' };
122
190
  },
123
191
  },
192
+ outbound: {
193
+ deliveryMode: 'direct',
194
+ sendText: async (params) => {
195
+ const accountId = params.accountId ?? 'default';
196
+ const text = params.text ?? '';
197
+ pluginLogger?.info(`VoiceClaw sendText: accountId="${accountId}", text="${text.slice(0, 50)}..."`);
198
+ const client = clients.get(accountId);
199
+ if (!client || client.connectionState !== 'connected') {
200
+ pluginLogger?.warn(`VoiceClaw: No connection for "${accountId}", reply dropped. clients: [${[...clients.keys()].join(',')}]`);
201
+ return { ok: false };
202
+ }
203
+ client.send({
204
+ type: 'agent_reply',
205
+ id: crypto.randomUUID(),
206
+ replyTo: '',
207
+ text,
208
+ ts: new Date().toISOString(),
209
+ });
210
+ pluginLogger?.info(`VoiceClaw: Reply sent to "${accountId}"`);
211
+ return { ok: true };
212
+ },
213
+ },
214
+ // ── Status adapter (matching WhatsApp's shape) ───────────────
215
+ status: {
216
+ defaultRuntime: {
217
+ accountId: 'default',
218
+ running: false,
219
+ connected: false,
220
+ lastMessageAt: null,
221
+ lastError: null,
222
+ },
223
+ buildChannelSummary: async ({ account, snapshot }) => {
224
+ const configured = loadSavedToken(account.accountId) !== null || Boolean(account.pairingCode);
225
+ return {
226
+ configured,
227
+ running: snapshot.running ?? false,
228
+ connected: snapshot.connected ?? false,
229
+ lastMessageAt: snapshot.lastMessageAt ?? null,
230
+ lastError: snapshot.lastError ?? null,
231
+ };
232
+ },
233
+ buildAccountSnapshot: async ({ account, runtime }) => ({
234
+ accountId: account.accountId,
235
+ enabled: account.enabled !== false,
236
+ configured: loadSavedToken(account.accountId) !== null || Boolean(account.pairingCode),
237
+ running: runtime?.running ?? false,
238
+ connected: runtime?.connected ?? false,
239
+ lastMessageAt: runtime?.lastMessageAt ?? null,
240
+ lastError: runtime?.lastError ?? null,
241
+ }),
242
+ resolveAccountState: ({ configured }) => configured ? 'paired' : 'not paired',
243
+ },
244
+ // ── Gateway (per-account start/stop) ─────────────────────────
124
245
  gateway: {
125
246
  startAccount: async (ctx) => {
126
247
  const account = ctx.account;
127
248
  const logger = pluginLogger;
128
- // Capture the inbound handler from OpenClaw
129
- if (ctx.processInbound) {
130
- handleInbound = ctx.processInbound;
131
- }
132
249
  logger.info(`VoiceClaw: Starting account "${account.accountId}"...`);
133
250
  try {
134
251
  const sessionToken = await resolveToken(account.accountId, account, logger);
@@ -136,28 +253,70 @@ export const voiceClawPlugin = {
136
253
  const client = new RelayWsClient({
137
254
  token: sessionToken,
138
255
  workerUrl,
139
- onUserMessage: (msg) => {
256
+ onUserMessage: async (msg) => {
140
257
  logger.info(`VoiceClaw: User message (${msg.id}): ${msg.text.slice(0, 50)}...`);
141
- handleInbound?.(msg.text, account.accountId);
258
+ // Dispatch inbound message via OpenClaw runtime
259
+ try {
260
+ const inboundCtx = pluginRuntime.channel.reply.finalizeInboundContext({
261
+ channelId: 'voiceclaw',
262
+ accountId: account.accountId,
263
+ sender: 'voiceclaw-user',
264
+ chatType: 'dm',
265
+ chatId: `voiceclaw:${account.accountId}`,
266
+ text: msg.text,
267
+ source: 'voiceclaw',
268
+ });
269
+ await pluginRuntime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
270
+ ctx: inboundCtx,
271
+ cfg: pluginConfig,
272
+ dispatcherOptions: {
273
+ deliver: async (_outPayload) => {
274
+ void _outPayload;
275
+ },
276
+ onError: (err) => {
277
+ logger.error(`VoiceClaw: Reply dispatch error: ${String(err)}`);
278
+ },
279
+ },
280
+ });
281
+ // Update status
282
+ ctx.setStatus?.({
283
+ accountId: account.accountId,
284
+ running: true,
285
+ connected: true,
286
+ lastMessageAt: new Date().toISOString(),
287
+ });
288
+ logger.info('VoiceClaw: Message dispatched to agent');
289
+ }
290
+ catch (err) {
291
+ logger.error(`VoiceClaw: Failed to dispatch message: ${String(err)}`);
292
+ }
142
293
  },
143
294
  onStateChange: (state) => {
144
295
  logger.info(`VoiceClaw: [${account.accountId}] ${state}`);
296
+ ctx.setStatus?.({
297
+ accountId: account.accountId,
298
+ running: true,
299
+ connected: state === 'connected',
300
+ });
145
301
  },
146
302
  logger,
147
303
  });
148
304
  client.start();
149
305
  clients.set(account.accountId, client);
150
306
  ctx.setStatus?.({
151
- ok: true,
152
- label: `VoiceClaw (${account.accountId})`,
307
+ accountId: account.accountId,
308
+ running: true,
309
+ connected: false,
153
310
  });
154
311
  logger.info(`VoiceClaw: Started relay for "${account.accountId}"`);
155
312
  }
156
313
  catch (err) {
157
314
  logger.error(`VoiceClaw: Failed to start "${account.accountId}": ${err}`);
158
315
  ctx.setStatus?.({
159
- ok: false,
160
- label: `VoiceClaw error: ${err}`,
316
+ accountId: account.accountId,
317
+ running: false,
318
+ connected: false,
319
+ lastError: String(err),
161
320
  });
162
321
  }
163
322
  },
@@ -172,10 +331,13 @@ export const voiceClawPlugin = {
172
331
  },
173
332
  },
174
333
  };
175
- // ── Plugin entry point (object form) ───────────────────────────
334
+ // ── Setters for plugin entry point ─────────────────────────────
176
335
  export function setLogger(logger) {
177
336
  pluginLogger = logger;
178
337
  }
179
- export function setInboundHandler(handler) {
180
- handleInbound = handler;
338
+ export function setRuntime(runtime) {
339
+ pluginRuntime = runtime;
340
+ }
341
+ export function setConfig(config) {
342
+ pluginConfig = config;
181
343
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voiceclaw/voiceclaw-plugin",
3
- "version": "1.1.6",
3
+ "version": "1.1.9",
4
4
  "description": "OpenClaw channel plugin for VoiceClaw — relay messages between your AI agent and the VoiceClaw iOS/macOS app via Siri",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -44,9 +44,6 @@
44
44
  "macos",
45
45
  "voice"
46
46
  ],
47
- "peerDependencies": {
48
- "openclaw": ">=2026.1.0"
49
- },
50
47
  "dependencies": {
51
48
  "ws": "^8.18.0",
52
49
  "zod": "^3.24.0"