@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 +1 -0
- package/dist/index.js +3 -1
- package/dist/src/channel.d.ts +48 -5
- package/dist/src/channel.js +204 -42
- package/package.json +1 -4
package/dist/index.d.ts
CHANGED
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
|
},
|
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,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
|
-
|
|
46
|
-
|
|
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
|
|
99
|
+
export declare function setRuntime(runtime: any): void;
|
|
100
|
+
export declare function setConfig(config: any): void;
|
|
58
101
|
export {};
|
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) {
|
|
@@ -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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
// ──
|
|
334
|
+
// ── Setters for plugin entry point ─────────────────────────────
|
|
176
335
|
export function setLogger(logger) {
|
|
177
336
|
pluginLogger = logger;
|
|
178
337
|
}
|
|
179
|
-
export function
|
|
180
|
-
|
|
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.
|
|
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"
|