@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.
- package/dist/src/channel.d.ts +46 -4
- package/dist/src/channel.js +167 -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,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>;
|
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,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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
// ──
|
|
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.
|
|
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",
|