@voiceclaw/voiceclaw-plugin 1.0.9 → 1.1.1
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 +22 -2
- package/dist/index.js +13 -172
- package/dist/src/channel.d.ts +62 -0
- package/dist/src/channel.js +163 -0
- package/dist/src/types.d.ts +57 -0
- package/dist/src/types.js +46 -0
- package/package.json +28 -25
- package/dist/types.d.ts +0 -45
- package/dist/types.js +0 -4
- /package/dist/{ws-client.d.ts → src/ws-client.d.ts} +0 -0
- /package/dist/{ws-client.js → src/ws-client.js} +0 -0
package/dist/index.d.ts
CHANGED
|
@@ -9,5 +9,25 @@ interface PluginApi {
|
|
|
9
9
|
plugin: any;
|
|
10
10
|
}) => void;
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
declare const _default: {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
configSchema: import("zod").ZodObject<{
|
|
17
|
+
channels: import("zod").ZodOptional<import("zod").ZodObject<{
|
|
18
|
+
voiceclaw: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodObject<{
|
|
19
|
+
pairingCode: import("zod").ZodOptional<import("zod").ZodString>;
|
|
20
|
+
workerUrl: import("zod").ZodOptional<import("zod").ZodString>;
|
|
21
|
+
enabled: import("zod").ZodOptional<import("zod").ZodBoolean>;
|
|
22
|
+
}, import("zod/v4/core").$strip>, import("zod").ZodObject<{
|
|
23
|
+
accounts: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodObject<{
|
|
24
|
+
pairingCode: import("zod").ZodOptional<import("zod").ZodString>;
|
|
25
|
+
workerUrl: import("zod").ZodOptional<import("zod").ZodString>;
|
|
26
|
+
enabled: import("zod").ZodOptional<import("zod").ZodBoolean>;
|
|
27
|
+
}, import("zod/v4/core").$strip>>;
|
|
28
|
+
}, import("zod/v4/core").$strip>]>>;
|
|
29
|
+
}, import("zod/v4/core").$strip>>;
|
|
30
|
+
}, import("zod/v4/core").$strip>;
|
|
31
|
+
register(api: PluginApi): void;
|
|
32
|
+
};
|
|
33
|
+
export default _default;
|
package/dist/index.js
CHANGED
|
@@ -1,172 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const p = tokenPath(accountId);
|
|
15
|
-
if (existsSync(p)) {
|
|
16
|
-
const token = readFileSync(p, 'utf-8').trim();
|
|
17
|
-
if (token.length > 0)
|
|
18
|
-
return token;
|
|
19
|
-
}
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
function saveToken(accountId, token) {
|
|
23
|
-
if (!existsSync(DATA_DIR)) {
|
|
24
|
-
mkdirSync(DATA_DIR, { recursive: true });
|
|
25
|
-
}
|
|
26
|
-
writeFileSync(tokenPath(accountId), token, 'utf-8');
|
|
27
|
-
}
|
|
28
|
-
// ── Pairing code exchange ──────────────────────────────────────
|
|
29
|
-
async function exchangePairingCode(code, workerUrl, logger) {
|
|
30
|
-
logger.info(`VoiceClaw: Exchanging pairing code for session token...`);
|
|
31
|
-
const url = `${workerUrl}/pair?code=${encodeURIComponent(code)}`;
|
|
32
|
-
const resp = await fetch(url, { method: 'POST' });
|
|
33
|
-
if (!resp.ok) {
|
|
34
|
-
const body = await resp.text();
|
|
35
|
-
throw new Error(`Pairing failed (${resp.status}): ${body}`);
|
|
36
|
-
}
|
|
37
|
-
const data = (await resp.json());
|
|
38
|
-
if (!data.sessionToken) {
|
|
39
|
-
throw new Error('Pairing response missing sessionToken');
|
|
40
|
-
}
|
|
41
|
-
logger.info('VoiceClaw: Pairing successful, session token saved');
|
|
42
|
-
return data.sessionToken;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Resolve the session token for an account:
|
|
46
|
-
* 1. If a saved session token exists on disk, use it.
|
|
47
|
-
* 2. Otherwise, exchange the pairing code and save the result.
|
|
48
|
-
*/
|
|
49
|
-
async function resolveToken(accountId, account, logger) {
|
|
50
|
-
const saved = loadSavedToken(accountId);
|
|
51
|
-
if (saved) {
|
|
52
|
-
logger.info(`VoiceClaw: Using saved session token for "${accountId}"`);
|
|
53
|
-
return saved;
|
|
54
|
-
}
|
|
55
|
-
if (!account.pairingCode) {
|
|
56
|
-
throw new Error(`No pairing code configured for "${accountId}" and no saved session token found`);
|
|
57
|
-
}
|
|
58
|
-
const workerUrl = account.workerUrl ?? DEFAULT_WORKER_URL;
|
|
59
|
-
const sessionToken = await exchangePairingCode(account.pairingCode, workerUrl, logger);
|
|
60
|
-
saveToken(accountId, sessionToken);
|
|
61
|
-
return sessionToken;
|
|
62
|
-
}
|
|
63
|
-
// ── Plugin registration ────────────────────────────────────────
|
|
64
|
-
export default function register(api) {
|
|
65
|
-
const channelId = 'voiceclaw';
|
|
66
|
-
const voiceClawChannel = {
|
|
67
|
-
id: channelId,
|
|
68
|
-
meta: {
|
|
69
|
-
id: channelId,
|
|
70
|
-
label: 'VoiceClaw',
|
|
71
|
-
selectionLabel: 'VoiceClaw (Siri / iOS / macOS)',
|
|
72
|
-
docsPath: '/channels/voiceclaw',
|
|
73
|
-
blurb: 'Voice entry for OpenClaw via Siri & native iOS/macOS app.',
|
|
74
|
-
aliases: ['vc', 'siri'],
|
|
75
|
-
},
|
|
76
|
-
capabilities: { chatTypes: ['direct'] },
|
|
77
|
-
config: {
|
|
78
|
-
listAccountIds: (cfg) => Object.keys(cfg.channels?.voiceclaw?.accounts ?? {}),
|
|
79
|
-
resolveAccount: (cfg, accountId) => cfg.channels?.voiceclaw?.accounts?.[accountId ?? 'default'] ?? {
|
|
80
|
-
accountId,
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
outbound: {
|
|
84
|
-
deliveryMode: 'direct',
|
|
85
|
-
sendText: async ({ text, accountId, }) => {
|
|
86
|
-
const id = accountId ?? 'default';
|
|
87
|
-
const client = clients.get(id);
|
|
88
|
-
if (!client || client.connectionState !== 'connected') {
|
|
89
|
-
api.logger.warn(`VoiceClaw: No connection for "${id}", reply dropped`);
|
|
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
|
-
return { ok: true };
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
onboarding: {
|
|
103
|
-
configure: async (ctx) => {
|
|
104
|
-
// Support: openclaw channels add --channel voiceclaw --token <code>
|
|
105
|
-
let pairingCode = ctx.options?.token;
|
|
106
|
-
if (!pairingCode) {
|
|
107
|
-
pairingCode = await ctx.prompter.text({
|
|
108
|
-
message: 'Enter the 6-digit pairing code from the VoiceClaw app:',
|
|
109
|
-
validate: (v) => v.trim().length >= 4 ? undefined : 'Pairing code must be at least 4 characters',
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
if (!pairingCode || pairingCode.trim() === '') {
|
|
113
|
-
return 'skip';
|
|
114
|
-
}
|
|
115
|
-
const cfg = { ...ctx.config };
|
|
116
|
-
if (!cfg.channels)
|
|
117
|
-
cfg.channels = {};
|
|
118
|
-
if (!cfg.channels.voiceclaw)
|
|
119
|
-
cfg.channels.voiceclaw = {};
|
|
120
|
-
if (!cfg.channels.voiceclaw.accounts)
|
|
121
|
-
cfg.channels.voiceclaw.accounts = {};
|
|
122
|
-
cfg.channels.voiceclaw.accounts.default = {
|
|
123
|
-
pairingCode: pairingCode.trim(),
|
|
124
|
-
};
|
|
125
|
-
return { cfg, accountId: 'default' };
|
|
126
|
-
},
|
|
127
|
-
},
|
|
128
|
-
gateway: {
|
|
129
|
-
start: async (ctx) => {
|
|
130
|
-
api.logger.info('VoiceClaw: gateway.start() called');
|
|
131
|
-
handleInbound = ctx.processInbound;
|
|
132
|
-
const accounts = ctx.config.channels?.voiceclaw?.accounts ?? {};
|
|
133
|
-
for (const [accountId, account] of Object.entries(accounts)) {
|
|
134
|
-
if (account.enabled === false)
|
|
135
|
-
continue;
|
|
136
|
-
try {
|
|
137
|
-
const sessionToken = await resolveToken(accountId, account, api.logger);
|
|
138
|
-
const workerUrl = account.workerUrl ?? DEFAULT_WORKER_URL;
|
|
139
|
-
const client = new RelayWsClient({
|
|
140
|
-
token: sessionToken,
|
|
141
|
-
workerUrl,
|
|
142
|
-
onUserMessage: (msg) => {
|
|
143
|
-
api.logger.info(`VoiceClaw: User message (${msg.id}): ${msg.text.slice(0, 50)}...`);
|
|
144
|
-
handleInbound?.(msg.text, accountId);
|
|
145
|
-
},
|
|
146
|
-
onStateChange: (state) => {
|
|
147
|
-
api.logger.info(`VoiceClaw: [${accountId}] ${state}`);
|
|
148
|
-
},
|
|
149
|
-
logger: api.logger,
|
|
150
|
-
});
|
|
151
|
-
client.start();
|
|
152
|
-
clients.set(accountId, client);
|
|
153
|
-
api.logger.info(`VoiceClaw: Started relay for "${accountId}"`);
|
|
154
|
-
}
|
|
155
|
-
catch (err) {
|
|
156
|
-
api.logger.error(`VoiceClaw: Failed to start "${accountId}": ${err}`);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
},
|
|
160
|
-
stop: () => {
|
|
161
|
-
for (const [accountId, client] of clients) {
|
|
162
|
-
client.stop();
|
|
163
|
-
api.logger.info(`VoiceClaw: Stopped "${accountId}"`);
|
|
164
|
-
}
|
|
165
|
-
clients.clear();
|
|
166
|
-
handleInbound = null;
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
};
|
|
170
|
-
api.registerChannel({ plugin: voiceClawChannel });
|
|
171
|
-
api.logger.info('VoiceClaw channel plugin registered');
|
|
172
|
-
}
|
|
1
|
+
import { voiceClawPlugin, setLogger } from './src/channel.js';
|
|
2
|
+
import { voiceClawConfigSchema } from './src/types.js';
|
|
3
|
+
export default {
|
|
4
|
+
id: 'voiceclaw-plugin',
|
|
5
|
+
name: 'VoiceClaw',
|
|
6
|
+
description: 'VoiceClaw channel plugin — Siri & iOS/macOS voice entry for OpenClaw',
|
|
7
|
+
configSchema: voiceClawConfigSchema,
|
|
8
|
+
register(api) {
|
|
9
|
+
setLogger(api.logger);
|
|
10
|
+
api.registerChannel({ plugin: voiceClawPlugin });
|
|
11
|
+
api.logger.info('VoiceClaw channel plugin registered');
|
|
12
|
+
},
|
|
13
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ResolvedAccount } from './types';
|
|
2
|
+
interface Logger {
|
|
3
|
+
info: (msg: string) => void;
|
|
4
|
+
error: (msg: string) => void;
|
|
5
|
+
warn: (msg: string) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare const voiceClawPlugin: {
|
|
8
|
+
id: string;
|
|
9
|
+
meta: {
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
selectionLabel: string;
|
|
13
|
+
docsPath: string;
|
|
14
|
+
blurb: string;
|
|
15
|
+
aliases: string[];
|
|
16
|
+
};
|
|
17
|
+
capabilities: {
|
|
18
|
+
chatTypes: string[];
|
|
19
|
+
};
|
|
20
|
+
configSchema: import("zod").ZodObject<{
|
|
21
|
+
channels: import("zod").ZodOptional<import("zod").ZodObject<{
|
|
22
|
+
voiceclaw: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodObject<{
|
|
23
|
+
pairingCode: import("zod").ZodOptional<import("zod").ZodString>;
|
|
24
|
+
workerUrl: import("zod").ZodOptional<import("zod").ZodString>;
|
|
25
|
+
enabled: import("zod").ZodOptional<import("zod").ZodBoolean>;
|
|
26
|
+
}, import("zod/v4/core").$strip>, import("zod").ZodObject<{
|
|
27
|
+
accounts: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodObject<{
|
|
28
|
+
pairingCode: import("zod").ZodOptional<import("zod").ZodString>;
|
|
29
|
+
workerUrl: import("zod").ZodOptional<import("zod").ZodString>;
|
|
30
|
+
enabled: import("zod").ZodOptional<import("zod").ZodBoolean>;
|
|
31
|
+
}, import("zod/v4/core").$strip>>;
|
|
32
|
+
}, import("zod/v4/core").$strip>]>>;
|
|
33
|
+
}, import("zod/v4/core").$strip>>;
|
|
34
|
+
}, import("zod/v4/core").$strip>;
|
|
35
|
+
config: {
|
|
36
|
+
listAccountIds: (cfg: Record<string, any>) => string[];
|
|
37
|
+
resolveAccount: (cfg: Record<string, any>, accountId?: string) => ResolvedAccount | undefined;
|
|
38
|
+
};
|
|
39
|
+
outbound: {
|
|
40
|
+
deliveryMode: "direct";
|
|
41
|
+
sendText: (params: any) => Promise<{
|
|
42
|
+
ok: boolean;
|
|
43
|
+
}>;
|
|
44
|
+
};
|
|
45
|
+
gateway: {
|
|
46
|
+
startAccount: (ctx: any) => Promise<void>;
|
|
47
|
+
stopAccount: (ctx: any) => Promise<void>;
|
|
48
|
+
};
|
|
49
|
+
status: {
|
|
50
|
+
getStatus: (ctx: any) => Promise<{
|
|
51
|
+
ok: boolean;
|
|
52
|
+
label: string;
|
|
53
|
+
detail: {
|
|
54
|
+
accountId: string;
|
|
55
|
+
connected: string;
|
|
56
|
+
};
|
|
57
|
+
}>;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
export declare function setLogger(logger: Logger): void;
|
|
61
|
+
export declare function setInboundHandler(handler: (text: string, accountId: string) => Promise<void>): void;
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { DEFAULT_WORKER_URL, resolveAccountIds, resolveVoiceClawAccount, voiceClawConfigSchema, } from './types';
|
|
5
|
+
import { RelayWsClient } from './ws-client';
|
|
6
|
+
// ── Token persistence ──────────────────────────────────────────
|
|
7
|
+
const DATA_DIR = join(homedir(), '.voiceclaw');
|
|
8
|
+
function tokenPath(accountId) {
|
|
9
|
+
return join(DATA_DIR, `session-${accountId}.token`);
|
|
10
|
+
}
|
|
11
|
+
function loadSavedToken(accountId) {
|
|
12
|
+
const p = tokenPath(accountId);
|
|
13
|
+
if (existsSync(p)) {
|
|
14
|
+
const token = readFileSync(p, 'utf-8').trim();
|
|
15
|
+
if (token.length > 0)
|
|
16
|
+
return token;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
function saveToken(accountId, token) {
|
|
21
|
+
if (!existsSync(DATA_DIR)) {
|
|
22
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
writeFileSync(tokenPath(accountId), token, 'utf-8');
|
|
25
|
+
}
|
|
26
|
+
async function exchangePairingCode(code, workerUrl, logger) {
|
|
27
|
+
logger.info('VoiceClaw: Exchanging pairing code for session token...');
|
|
28
|
+
const url = `${workerUrl}/pair?code=${encodeURIComponent(code)}`;
|
|
29
|
+
const resp = await fetch(url, { method: 'POST' });
|
|
30
|
+
if (!resp.ok) {
|
|
31
|
+
const body = await resp.text();
|
|
32
|
+
throw new Error(`Pairing failed (${resp.status}): ${body}`);
|
|
33
|
+
}
|
|
34
|
+
const data = (await resp.json());
|
|
35
|
+
if (!data.sessionToken) {
|
|
36
|
+
throw new Error('Pairing response missing sessionToken');
|
|
37
|
+
}
|
|
38
|
+
logger.info('VoiceClaw: Pairing successful, session token saved');
|
|
39
|
+
return data.sessionToken;
|
|
40
|
+
}
|
|
41
|
+
async function resolveToken(accountId, account, logger) {
|
|
42
|
+
const saved = loadSavedToken(accountId);
|
|
43
|
+
if (saved) {
|
|
44
|
+
logger.info(`VoiceClaw: Using saved session token for "${accountId}"`);
|
|
45
|
+
return saved;
|
|
46
|
+
}
|
|
47
|
+
if (!account.pairingCode) {
|
|
48
|
+
throw new Error(`No pairing code configured for "${accountId}" and no saved session token found`);
|
|
49
|
+
}
|
|
50
|
+
const workerUrl = account.workerUrl ?? DEFAULT_WORKER_URL;
|
|
51
|
+
const sessionToken = await exchangePairingCode(account.pairingCode, workerUrl, logger);
|
|
52
|
+
saveToken(accountId, sessionToken);
|
|
53
|
+
return sessionToken;
|
|
54
|
+
}
|
|
55
|
+
// ── Channel plugin ─────────────────────────────────────────────
|
|
56
|
+
const clients = new Map();
|
|
57
|
+
let handleInbound = null;
|
|
58
|
+
let pluginLogger = null;
|
|
59
|
+
export const voiceClawPlugin = {
|
|
60
|
+
id: 'voiceclaw',
|
|
61
|
+
meta: {
|
|
62
|
+
id: 'voiceclaw',
|
|
63
|
+
label: 'VoiceClaw',
|
|
64
|
+
selectionLabel: 'VoiceClaw (Siri / iOS / macOS)',
|
|
65
|
+
docsPath: '/channels/voiceclaw',
|
|
66
|
+
blurb: 'Voice entry for OpenClaw via Siri & native iOS/macOS app.',
|
|
67
|
+
aliases: ['vc', 'siri'],
|
|
68
|
+
},
|
|
69
|
+
capabilities: { chatTypes: ['direct'] },
|
|
70
|
+
configSchema: voiceClawConfigSchema,
|
|
71
|
+
config: {
|
|
72
|
+
listAccountIds: (cfg) => resolveAccountIds(cfg),
|
|
73
|
+
resolveAccount: (cfg, accountId) => resolveVoiceClawAccount(cfg, accountId),
|
|
74
|
+
},
|
|
75
|
+
outbound: {
|
|
76
|
+
deliveryMode: 'direct',
|
|
77
|
+
sendText: async (params) => {
|
|
78
|
+
const ctx = params.context;
|
|
79
|
+
const account = ctx?.account;
|
|
80
|
+
const accountId = account?.accountId ?? 'default';
|
|
81
|
+
const client = clients.get(accountId);
|
|
82
|
+
if (!client || client.connectionState !== 'connected') {
|
|
83
|
+
pluginLogger?.warn(`VoiceClaw: No connection for "${accountId}", reply dropped`);
|
|
84
|
+
return { ok: false };
|
|
85
|
+
}
|
|
86
|
+
client.send({
|
|
87
|
+
type: 'agent_reply',
|
|
88
|
+
id: crypto.randomUUID(),
|
|
89
|
+
replyTo: '',
|
|
90
|
+
text: params.text ?? '',
|
|
91
|
+
ts: new Date().toISOString(),
|
|
92
|
+
});
|
|
93
|
+
return { ok: true };
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
gateway: {
|
|
97
|
+
startAccount: async (ctx) => {
|
|
98
|
+
const account = ctx.account;
|
|
99
|
+
const logger = pluginLogger;
|
|
100
|
+
logger.info(`VoiceClaw: Starting account "${account.accountId}"...`);
|
|
101
|
+
try {
|
|
102
|
+
const sessionToken = await resolveToken(account.accountId, account, logger);
|
|
103
|
+
const workerUrl = account.workerUrl ?? DEFAULT_WORKER_URL;
|
|
104
|
+
const client = new RelayWsClient({
|
|
105
|
+
token: sessionToken,
|
|
106
|
+
workerUrl,
|
|
107
|
+
onUserMessage: (msg) => {
|
|
108
|
+
logger.info(`VoiceClaw: User message (${msg.id}): ${msg.text.slice(0, 50)}...`);
|
|
109
|
+
handleInbound?.(msg.text, account.accountId);
|
|
110
|
+
},
|
|
111
|
+
onStateChange: (state) => {
|
|
112
|
+
logger.info(`VoiceClaw: [${account.accountId}] ${state}`);
|
|
113
|
+
},
|
|
114
|
+
logger,
|
|
115
|
+
});
|
|
116
|
+
client.start();
|
|
117
|
+
clients.set(account.accountId, client);
|
|
118
|
+
ctx.setStatus?.({
|
|
119
|
+
ok: true,
|
|
120
|
+
label: `VoiceClaw (${account.accountId})`,
|
|
121
|
+
});
|
|
122
|
+
logger.info(`VoiceClaw: Started relay for "${account.accountId}"`);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
logger.error(`VoiceClaw: Failed to start "${account.accountId}": ${err}`);
|
|
126
|
+
ctx.setStatus?.({
|
|
127
|
+
ok: false,
|
|
128
|
+
label: `VoiceClaw error: ${err}`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
stopAccount: async (ctx) => {
|
|
133
|
+
const account = ctx.account;
|
|
134
|
+
const client = clients.get(account.accountId);
|
|
135
|
+
if (client) {
|
|
136
|
+
client.stop();
|
|
137
|
+
clients.delete(account.accountId);
|
|
138
|
+
pluginLogger?.info(`VoiceClaw: Stopped "${account.accountId}"`);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
status: {
|
|
143
|
+
getStatus: async (ctx) => {
|
|
144
|
+
const account = ctx.account;
|
|
145
|
+
const client = clients.get(account.accountId);
|
|
146
|
+
return {
|
|
147
|
+
ok: client?.connectionState === 'connected',
|
|
148
|
+
label: `VoiceClaw (${account.accountId})`,
|
|
149
|
+
detail: {
|
|
150
|
+
accountId: account.accountId,
|
|
151
|
+
connected: client?.connectionState ?? 'not started',
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
// ── Plugin entry point (object form) ───────────────────────────
|
|
158
|
+
export function setLogger(logger) {
|
|
159
|
+
pluginLogger = logger;
|
|
160
|
+
}
|
|
161
|
+
export function setInboundHandler(handler) {
|
|
162
|
+
handleInbound = handler;
|
|
163
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
declare const voiceClawAccountSchema: z.ZodObject<{
|
|
3
|
+
pairingCode: z.ZodOptional<z.ZodString>;
|
|
4
|
+
workerUrl: z.ZodOptional<z.ZodString>;
|
|
5
|
+
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
export declare const voiceClawConfigSchema: z.ZodObject<{
|
|
8
|
+
channels: z.ZodOptional<z.ZodObject<{
|
|
9
|
+
voiceclaw: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
10
|
+
pairingCode: z.ZodOptional<z.ZodString>;
|
|
11
|
+
workerUrl: z.ZodOptional<z.ZodString>;
|
|
12
|
+
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
13
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
14
|
+
accounts: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
15
|
+
pairingCode: z.ZodOptional<z.ZodString>;
|
|
16
|
+
workerUrl: z.ZodOptional<z.ZodString>;
|
|
17
|
+
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
18
|
+
}, z.core.$strip>>;
|
|
19
|
+
}, z.core.$strip>]>>;
|
|
20
|
+
}, z.core.$strip>>;
|
|
21
|
+
}, z.core.$strip>;
|
|
22
|
+
export type VoiceClawConfig = z.infer<typeof voiceClawConfigSchema>;
|
|
23
|
+
export type ResolvedAccount = z.infer<typeof voiceClawAccountSchema> & {
|
|
24
|
+
accountId: string;
|
|
25
|
+
};
|
|
26
|
+
export declare function resolveAccountIds(cfg: VoiceClawConfig): string[];
|
|
27
|
+
export declare function resolveVoiceClawAccount(cfg: VoiceClawConfig, accountId?: string): ResolvedAccount | undefined;
|
|
28
|
+
export declare const DEFAULT_WORKER_URL = "https://voiceclaw-api.techartisan.site";
|
|
29
|
+
export interface UserMessage {
|
|
30
|
+
type: 'user_message';
|
|
31
|
+
id: string;
|
|
32
|
+
text: string;
|
|
33
|
+
ts: string;
|
|
34
|
+
}
|
|
35
|
+
export interface AgentReplyMessage {
|
|
36
|
+
type: 'agent_reply';
|
|
37
|
+
id: string;
|
|
38
|
+
replyTo: string;
|
|
39
|
+
text: string;
|
|
40
|
+
ts: string;
|
|
41
|
+
}
|
|
42
|
+
export interface PingMessage {
|
|
43
|
+
type: 'ping';
|
|
44
|
+
}
|
|
45
|
+
export interface PongMessage {
|
|
46
|
+
type: 'pong';
|
|
47
|
+
}
|
|
48
|
+
export interface AuthOkMessage {
|
|
49
|
+
type: 'auth_ok';
|
|
50
|
+
}
|
|
51
|
+
export interface AuthErrorMessage {
|
|
52
|
+
type: 'auth_error';
|
|
53
|
+
reason: string;
|
|
54
|
+
}
|
|
55
|
+
export type InboundRelayMessage = AuthOkMessage | AuthErrorMessage | UserMessage | PongMessage;
|
|
56
|
+
export type OutboundRelayMessage = AgentReplyMessage | PingMessage;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// ── Account schema ─────────────────────────────────────────────
|
|
3
|
+
const voiceClawAccountSchema = z.object({
|
|
4
|
+
pairingCode: z.string().min(1).optional(),
|
|
5
|
+
workerUrl: z.string().url().optional(),
|
|
6
|
+
enabled: z.boolean().optional(),
|
|
7
|
+
});
|
|
8
|
+
const voiceClawMultiAccountSchema = z.object({
|
|
9
|
+
accounts: z.record(z.string(), voiceClawAccountSchema),
|
|
10
|
+
});
|
|
11
|
+
export const voiceClawConfigSchema = z.object({
|
|
12
|
+
channels: z
|
|
13
|
+
.object({
|
|
14
|
+
voiceclaw: z
|
|
15
|
+
.union([voiceClawAccountSchema, voiceClawMultiAccountSchema])
|
|
16
|
+
.optional(),
|
|
17
|
+
})
|
|
18
|
+
.optional(),
|
|
19
|
+
});
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
+
function isMultiAccount(cfg) {
|
|
22
|
+
return cfg && typeof cfg === 'object' && 'accounts' in cfg;
|
|
23
|
+
}
|
|
24
|
+
export function resolveAccountIds(cfg) {
|
|
25
|
+
const vc = cfg.channels?.voiceclaw;
|
|
26
|
+
if (!vc)
|
|
27
|
+
return [];
|
|
28
|
+
if (isMultiAccount(vc))
|
|
29
|
+
return Object.keys(vc.accounts);
|
|
30
|
+
return ['default'];
|
|
31
|
+
}
|
|
32
|
+
export function resolveVoiceClawAccount(cfg, accountId) {
|
|
33
|
+
const vc = cfg.channels?.voiceclaw;
|
|
34
|
+
if (!vc)
|
|
35
|
+
return undefined;
|
|
36
|
+
if ('accounts' in vc) {
|
|
37
|
+
const id = accountId ?? Object.keys(vc.accounts)[0];
|
|
38
|
+
const acc = vc.accounts[id];
|
|
39
|
+
if (!acc)
|
|
40
|
+
return undefined;
|
|
41
|
+
return { ...acc, accountId: id };
|
|
42
|
+
}
|
|
43
|
+
return { ...vc, accountId: accountId ?? 'default' };
|
|
44
|
+
}
|
|
45
|
+
// ── Message protocol types ─────────────────────────────────────
|
|
46
|
+
export const DEFAULT_WORKER_URL = 'https://voiceclaw-api.techartisan.site';
|
package/package.json
CHANGED
|
@@ -1,25 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voiceclaw/voiceclaw-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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",
|
|
7
7
|
"license": "MIT",
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "https://github.com/anthropics/voiceclaw"
|
|
11
|
-
},
|
|
12
|
-
"homepage": "https://voiceclaw.techartisan.site",
|
|
13
|
-
"keywords": [
|
|
14
|
-
"openclaw",
|
|
15
|
-
"openclaw-plugin",
|
|
16
|
-
"openclaw-channel",
|
|
17
|
-
"siri",
|
|
18
|
-
"voiceclaw",
|
|
19
|
-
"ios",
|
|
20
|
-
"macos",
|
|
21
|
-
"voice"
|
|
22
|
-
],
|
|
23
8
|
"files": [
|
|
24
9
|
"dist",
|
|
25
10
|
"openclaw.plugin.json",
|
|
@@ -41,22 +26,40 @@
|
|
|
41
26
|
]
|
|
42
27
|
},
|
|
43
28
|
"install": {
|
|
44
|
-
"npmSpec": "@voiceclaw/voiceclaw-plugin"
|
|
45
|
-
"defaultChoice": "npm"
|
|
29
|
+
"npmSpec": "@voiceclaw/voiceclaw-plugin"
|
|
46
30
|
}
|
|
47
31
|
},
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/anthropics/voiceclaw"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://voiceclaw.techartisan.site",
|
|
37
|
+
"keywords": [
|
|
38
|
+
"openclaw",
|
|
39
|
+
"openclaw-plugin",
|
|
40
|
+
"openclaw-channel",
|
|
41
|
+
"siri",
|
|
42
|
+
"voiceclaw",
|
|
43
|
+
"ios",
|
|
44
|
+
"macos",
|
|
45
|
+
"voice"
|
|
46
|
+
],
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"openclaw": ">=2026.1.0"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"ws": "^8.18.0",
|
|
52
|
+
"zod": "^3.24.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/node": "^22.0.0",
|
|
56
56
|
"@types/ws": "^8.18.1",
|
|
57
57
|
"typescript": "^5.7.0"
|
|
58
58
|
},
|
|
59
|
-
"
|
|
60
|
-
"
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsc",
|
|
61
|
+
"dev": "tsc --watch",
|
|
62
|
+
"typecheck": "tsc --noEmit",
|
|
63
|
+
"prepublishOnly": "npm run build"
|
|
61
64
|
}
|
|
62
65
|
}
|
package/dist/types.d.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/** Role of the WebSocket client. */
|
|
2
|
-
export type ClientRole = 'app' | 'plugin';
|
|
3
|
-
/** Auth success (sent by server on connect). */
|
|
4
|
-
export interface AuthOkMessage {
|
|
5
|
-
type: 'auth_ok';
|
|
6
|
-
}
|
|
7
|
-
/** Auth failure. */
|
|
8
|
-
export interface AuthErrorMessage {
|
|
9
|
-
type: 'auth_error';
|
|
10
|
-
reason: string;
|
|
11
|
-
}
|
|
12
|
-
/** User message from app → plugin. */
|
|
13
|
-
export interface UserMessage {
|
|
14
|
-
type: 'user_message';
|
|
15
|
-
id: string;
|
|
16
|
-
text: string;
|
|
17
|
-
ts: string;
|
|
18
|
-
}
|
|
19
|
-
/** Agent reply from plugin → app. */
|
|
20
|
-
export interface AgentReplyMessage {
|
|
21
|
-
type: 'agent_reply';
|
|
22
|
-
id: string;
|
|
23
|
-
replyTo: string;
|
|
24
|
-
text: string;
|
|
25
|
-
ts: string;
|
|
26
|
-
}
|
|
27
|
-
/** Heartbeat. */
|
|
28
|
-
export interface PingMessage {
|
|
29
|
-
type: 'ping';
|
|
30
|
-
}
|
|
31
|
-
export interface PongMessage {
|
|
32
|
-
type: 'pong';
|
|
33
|
-
}
|
|
34
|
-
/** Plugin config from channels.voiceclaw.accounts.<id> */
|
|
35
|
-
export interface VoiceClawAccountConfig {
|
|
36
|
-
pairingCode: string;
|
|
37
|
-
workerUrl?: string;
|
|
38
|
-
enabled?: boolean;
|
|
39
|
-
}
|
|
40
|
-
/** Default relay server URL. */
|
|
41
|
-
export declare const DEFAULT_WORKER_URL = "https://voiceclaw-api.techartisan.site";
|
|
42
|
-
/** Messages the plugin receives from the relay. */
|
|
43
|
-
export type InboundRelayMessage = AuthOkMessage | AuthErrorMessage | UserMessage | PongMessage;
|
|
44
|
-
/** Messages the plugin sends to the relay. */
|
|
45
|
-
export type OutboundRelayMessage = AgentReplyMessage | PingMessage;
|
package/dist/types.js
DELETED
|
File without changes
|
|
File without changes
|