@voiceclaw/voiceclaw-plugin 1.1.8 → 1.2.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.
@@ -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,36 @@ 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
+ channel: any;
57
+ getStatus: (ctx: any) => Promise<{
58
+ channel: any;
59
+ configured: boolean;
60
+ statusLines: string[];
61
+ selectionHint: string;
62
+ quickstartScore: number;
63
+ }>;
64
+ configure: (ctx: any) => Promise<{
65
+ cfg: any;
66
+ accountId: string;
67
+ }>;
38
68
  };
39
69
  outbound: {
40
70
  deliveryMode: "direct";
@@ -42,11 +72,31 @@ export declare const voiceClawPlugin: {
42
72
  ok: boolean;
43
73
  }>;
44
74
  };
45
- onboarding: {
46
- configure: (ctx: any) => Promise<"skip" | {
47
- cfg: any;
75
+ status: {
76
+ defaultRuntime: {
48
77
  accountId: string;
78
+ running: boolean;
79
+ connected: boolean;
80
+ lastMessageAt: null;
81
+ lastError: null;
82
+ };
83
+ buildChannelSummary: ({ account, snapshot }: any) => Promise<{
84
+ configured: boolean;
85
+ running: any;
86
+ connected: any;
87
+ lastMessageAt: any;
88
+ lastError: any;
89
+ }>;
90
+ buildAccountSnapshot: ({ account, runtime }: any) => Promise<{
91
+ accountId: any;
92
+ enabled: boolean;
93
+ configured: boolean;
94
+ running: any;
95
+ connected: any;
96
+ lastMessageAt: any;
97
+ lastError: any;
49
98
  }>;
99
+ resolveAccountState: ({ configured }: any) => "paired" | "not paired";
50
100
  };
51
101
  gateway: {
52
102
  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,14 +74,144 @@ 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
+ },
120
+ },
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
+ };
161
+ },
162
+ },
163
+ // ── Onboarding adapter (ChannelOnboardingAdapter shape) ──────
164
+ onboarding: {
165
+ channel: 'voiceclaw',
166
+ getStatus: async (ctx) => {
167
+ const cfg = ctx.cfg;
168
+ const accountIds = resolveAccountIds(cfg);
169
+ const hasAnyAccount = accountIds.length > 0;
170
+ const configured = hasAnyAccount && accountIds.some((id) => {
171
+ const account = resolveVoiceClawAccount(cfg, id);
172
+ return account && (loadSavedToken(id) !== null || Boolean(account.pairingCode));
173
+ });
174
+ const statusLabel = configured ? 'configured' : 'not configured';
175
+ return {
176
+ channel: 'voiceclaw',
177
+ configured,
178
+ statusLines: [`VoiceClaw: ${statusLabel}`],
179
+ selectionHint: configured ? 'configured' : 'not configured',
180
+ quickstartScore: 0,
181
+ };
182
+ },
183
+ configure: async (ctx) => {
184
+ let pairingCode = ctx.options?.token;
185
+ if (!pairingCode && ctx.prompter) {
186
+ pairingCode = await ctx.prompter.text({
187
+ message: 'Enter the pairing code from the VoiceClaw app:',
188
+ validate: (v) => v.trim().length >= 4
189
+ ? undefined
190
+ : 'Pairing code must be at least 4 characters',
191
+ });
192
+ }
193
+ if (!pairingCode || pairingCode.trim() === '') {
194
+ // Return a no-op result instead of 'skip' since configure must return { cfg, accountId }
195
+ return { cfg: ctx.cfg, accountId: 'default' };
196
+ }
197
+ const cfg = { ...ctx.cfg };
198
+ if (!cfg.channels)
199
+ cfg.channels = {};
200
+ if (!cfg.channels.voiceclaw)
201
+ cfg.channels.voiceclaw = {};
202
+ if (!cfg.channels.voiceclaw.accounts)
203
+ cfg.channels.voiceclaw.accounts = {};
204
+ cfg.channels.voiceclaw.accounts.default = {
205
+ pairingCode: pairingCode.trim(),
206
+ enabled: true,
207
+ };
208
+ return { cfg, accountId: 'default' };
209
+ },
77
210
  },
78
211
  outbound: {
79
212
  deliveryMode: 'direct',
80
213
  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';
214
+ const accountId = params.accountId ?? 'default';
85
215
  const text = params.text ?? '';
86
216
  pluginLogger?.info(`VoiceClaw sendText: accountId="${accountId}", text="${text.slice(0, 50)}..."`);
87
217
  const client = clients.get(accountId);
@@ -100,30 +230,37 @@ export const voiceClawPlugin = {
100
230
  return { ok: true };
101
231
  },
102
232
  },
103
- onboarding: {
104
- configure: async (ctx) => {
105
- // Support: openclaw channels add --channel voiceclaw --token <code>
106
- let pairingCode = ctx.options?.token;
107
- if (!pairingCode && ctx.prompter) {
108
- pairingCode = await ctx.prompter.text({
109
- message: 'Enter the 6-digit pairing code from the VoiceClaw app:',
110
- validate: (v) => v.trim().length >= 4
111
- ? undefined
112
- : 'Pairing code must be at least 4 characters',
113
- });
114
- }
115
- if (!pairingCode || pairingCode.trim() === '') {
116
- return 'skip';
117
- }
118
- const cfg = { ...ctx.config };
119
- if (!cfg.channels)
120
- cfg.channels = {};
121
- if (!cfg.channels.voiceclaw)
122
- cfg.channels.voiceclaw = {};
123
- cfg.channels.voiceclaw.pairingCode = pairingCode.trim();
124
- return { cfg, accountId: 'default' };
233
+ // ── Status adapter (matching WhatsApp's shape) ───────────────
234
+ status: {
235
+ defaultRuntime: {
236
+ accountId: 'default',
237
+ running: false,
238
+ connected: false,
239
+ lastMessageAt: null,
240
+ lastError: null,
125
241
  },
242
+ buildChannelSummary: async ({ account, snapshot }) => {
243
+ const configured = loadSavedToken(account.accountId) !== null || Boolean(account.pairingCode);
244
+ return {
245
+ configured,
246
+ running: snapshot.running ?? false,
247
+ connected: snapshot.connected ?? false,
248
+ lastMessageAt: snapshot.lastMessageAt ?? null,
249
+ lastError: snapshot.lastError ?? null,
250
+ };
251
+ },
252
+ buildAccountSnapshot: async ({ account, runtime }) => ({
253
+ accountId: account.accountId,
254
+ enabled: account.enabled !== false,
255
+ configured: loadSavedToken(account.accountId) !== null || Boolean(account.pairingCode),
256
+ running: runtime?.running ?? false,
257
+ connected: runtime?.connected ?? false,
258
+ lastMessageAt: runtime?.lastMessageAt ?? null,
259
+ lastError: runtime?.lastError ?? null,
260
+ }),
261
+ resolveAccountState: ({ configured }) => configured ? 'paired' : 'not paired',
126
262
  },
263
+ // ── Gateway (per-account start/stop) ─────────────────────────
127
264
  gateway: {
128
265
  startAccount: async (ctx) => {
129
266
  const account = ctx.account;
@@ -137,7 +274,7 @@ export const voiceClawPlugin = {
137
274
  workerUrl,
138
275
  onUserMessage: async (msg) => {
139
276
  logger.info(`VoiceClaw: User message (${msg.id}): ${msg.text.slice(0, 50)}...`);
140
- // Dispatch inbound message via OpenClaw runtime (same as LINE plugin)
277
+ // Dispatch inbound message via OpenClaw runtime
141
278
  try {
142
279
  const inboundCtx = pluginRuntime.channel.reply.finalizeInboundContext({
143
280
  channelId: 'voiceclaw',
@@ -153,7 +290,6 @@ export const voiceClawPlugin = {
153
290
  cfg: pluginConfig,
154
291
  dispatcherOptions: {
155
292
  deliver: async (_outPayload) => {
156
- // Delivery handled by outbound.sendText adapter
157
293
  void _outPayload;
158
294
  },
159
295
  onError: (err) => {
@@ -161,6 +297,13 @@ export const voiceClawPlugin = {
161
297
  },
162
298
  },
163
299
  });
300
+ // Update status
301
+ ctx.setStatus?.({
302
+ accountId: account.accountId,
303
+ running: true,
304
+ connected: true,
305
+ lastMessageAt: new Date().toISOString(),
306
+ });
164
307
  logger.info('VoiceClaw: Message dispatched to agent');
165
308
  }
166
309
  catch (err) {
@@ -169,22 +312,30 @@ export const voiceClawPlugin = {
169
312
  },
170
313
  onStateChange: (state) => {
171
314
  logger.info(`VoiceClaw: [${account.accountId}] ${state}`);
315
+ ctx.setStatus?.({
316
+ accountId: account.accountId,
317
+ running: true,
318
+ connected: state === 'connected',
319
+ });
172
320
  },
173
321
  logger,
174
322
  });
175
323
  client.start();
176
324
  clients.set(account.accountId, client);
177
325
  ctx.setStatus?.({
178
- ok: true,
179
- label: `VoiceClaw (${account.accountId})`,
326
+ accountId: account.accountId,
327
+ running: true,
328
+ connected: false,
180
329
  });
181
330
  logger.info(`VoiceClaw: Started relay for "${account.accountId}"`);
182
331
  }
183
332
  catch (err) {
184
333
  logger.error(`VoiceClaw: Failed to start "${account.accountId}": ${err}`);
185
334
  ctx.setStatus?.({
186
- ok: false,
187
- label: `VoiceClaw error: ${err}`,
335
+ accountId: account.accountId,
336
+ running: false,
337
+ connected: false,
338
+ lastError: String(err),
188
339
  });
189
340
  }
190
341
  },
@@ -199,7 +350,7 @@ export const voiceClawPlugin = {
199
350
  },
200
351
  },
201
352
  };
202
- // ── Plugin entry point (object form) ───────────────────────────
353
+ // ── Setters for plugin entry point ─────────────────────────────
203
354
  export function setLogger(logger) {
204
355
  pluginLogger = logger;
205
356
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voiceclaw/voiceclaw-plugin",
3
- "version": "1.1.8",
3
+ "version": "1.2.0",
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",