@voiceclaw/voiceclaw-plugin 1.1.8 → 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.
@@ -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>;
@@ -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) {
@@ -74,39 +74,99 @@ export const voiceClawPlugin = {
74
74
  config: {
75
75
  listAccountIds: (cfg) => resolveAccountIds(cfg),
76
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
+ },
77
120
  },
78
- outbound: {
79
- deliveryMode: 'direct',
80
- sendText: async (params) => {
81
- pluginLogger?.info(`VoiceClaw sendText called, keys: ${JSON.stringify(Object.keys(params))}`);
82
- const ctx = params.context;
83
- const account = ctx?.account;
84
- const accountId = account?.accountId ?? params.accountId ?? 'default';
85
- const text = params.text ?? '';
86
- pluginLogger?.info(`VoiceClaw sendText: accountId="${accountId}", text="${text.slice(0, 50)}..."`);
87
- const client = clients.get(accountId);
88
- if (!client || client.connectionState !== 'connected') {
89
- pluginLogger?.warn(`VoiceClaw: No connection for "${accountId}", reply dropped. clients: [${[...clients.keys()].join(',')}]`);
90
- return { ok: false };
91
- }
92
- client.send({
93
- type: 'agent_reply',
94
- id: crypto.randomUUID(),
95
- replyTo: '',
96
- text,
97
- ts: new Date().toISOString(),
98
- });
99
- pluginLogger?.info(`VoiceClaw: Reply sent to "${accountId}"`);
100
- 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
+ };
101
161
  },
102
162
  },
163
+ // ── Onboarding (interactive setup via `openclaw channels add`) ─
103
164
  onboarding: {
104
165
  configure: async (ctx) => {
105
- // Support: openclaw channels add --channel voiceclaw --token <code>
106
166
  let pairingCode = ctx.options?.token;
107
167
  if (!pairingCode && ctx.prompter) {
108
168
  pairingCode = await ctx.prompter.text({
109
- message: 'Enter the 6-digit pairing code from the VoiceClaw app:',
169
+ message: 'Enter the pairing code from the VoiceClaw app:',
110
170
  validate: (v) => v.trim().length >= 4
111
171
  ? undefined
112
172
  : 'Pairing code must be at least 4 characters',
@@ -120,10 +180,68 @@ export const voiceClawPlugin = {
120
180
  cfg.channels = {};
121
181
  if (!cfg.channels.voiceclaw)
122
182
  cfg.channels.voiceclaw = {};
123
- 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
+ };
124
189
  return { cfg, accountId: 'default' };
125
190
  },
126
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) ─────────────────────────
127
245
  gateway: {
128
246
  startAccount: async (ctx) => {
129
247
  const account = ctx.account;
@@ -137,7 +255,7 @@ export const voiceClawPlugin = {
137
255
  workerUrl,
138
256
  onUserMessage: async (msg) => {
139
257
  logger.info(`VoiceClaw: User message (${msg.id}): ${msg.text.slice(0, 50)}...`);
140
- // Dispatch inbound message via OpenClaw runtime (same as LINE plugin)
258
+ // Dispatch inbound message via OpenClaw runtime
141
259
  try {
142
260
  const inboundCtx = pluginRuntime.channel.reply.finalizeInboundContext({
143
261
  channelId: 'voiceclaw',
@@ -153,7 +271,6 @@ export const voiceClawPlugin = {
153
271
  cfg: pluginConfig,
154
272
  dispatcherOptions: {
155
273
  deliver: async (_outPayload) => {
156
- // Delivery handled by outbound.sendText adapter
157
274
  void _outPayload;
158
275
  },
159
276
  onError: (err) => {
@@ -161,6 +278,13 @@ export const voiceClawPlugin = {
161
278
  },
162
279
  },
163
280
  });
281
+ // Update status
282
+ ctx.setStatus?.({
283
+ accountId: account.accountId,
284
+ running: true,
285
+ connected: true,
286
+ lastMessageAt: new Date().toISOString(),
287
+ });
164
288
  logger.info('VoiceClaw: Message dispatched to agent');
165
289
  }
166
290
  catch (err) {
@@ -169,22 +293,30 @@ export const voiceClawPlugin = {
169
293
  },
170
294
  onStateChange: (state) => {
171
295
  logger.info(`VoiceClaw: [${account.accountId}] ${state}`);
296
+ ctx.setStatus?.({
297
+ accountId: account.accountId,
298
+ running: true,
299
+ connected: state === 'connected',
300
+ });
172
301
  },
173
302
  logger,
174
303
  });
175
304
  client.start();
176
305
  clients.set(account.accountId, client);
177
306
  ctx.setStatus?.({
178
- ok: true,
179
- label: `VoiceClaw (${account.accountId})`,
307
+ accountId: account.accountId,
308
+ running: true,
309
+ connected: false,
180
310
  });
181
311
  logger.info(`VoiceClaw: Started relay for "${account.accountId}"`);
182
312
  }
183
313
  catch (err) {
184
314
  logger.error(`VoiceClaw: Failed to start "${account.accountId}": ${err}`);
185
315
  ctx.setStatus?.({
186
- ok: false,
187
- label: `VoiceClaw error: ${err}`,
316
+ accountId: account.accountId,
317
+ running: false,
318
+ connected: false,
319
+ lastError: String(err),
188
320
  });
189
321
  }
190
322
  },
@@ -199,7 +331,7 @@ export const voiceClawPlugin = {
199
331
  },
200
332
  },
201
333
  };
202
- // ── Plugin entry point (object form) ───────────────────────────
334
+ // ── Setters for plugin entry point ─────────────────────────────
203
335
  export function setLogger(logger) {
204
336
  pluginLogger = logger;
205
337
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voiceclaw/voiceclaw-plugin",
3
- "version": "1.1.8",
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",