@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.
- package/dist/src/channel.d.ts +54 -4
- package/dist/src/channel.js +186 -35
- package/package.json +1 -1
package/dist/src/channel.d.ts
CHANGED
|
@@ -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
|
-
|
|
46
|
-
|
|
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>;
|
package/dist/src/channel.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
// ──
|
|
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.
|
|
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",
|