@alfe.ai/openclaw-voice 0.1.13 → 0.1.15

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.cts CHANGED
@@ -1,2 +1,2 @@
1
- import plugin from "./plugin.cjs";
1
+ import { t as plugin } from "./plugin.cjs";
2
2
  export { plugin as default };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import plugin from "./plugin.js";
1
+ import { t as plugin } from "./plugin.js";
2
2
  export { plugin as default };
package/dist/plugin.d.cts CHANGED
@@ -1,5 +1,43 @@
1
- import { TSchema } from "@sinclair/typebox";
1
+ //#region ../openclaw-plugin-kit/dist/index.d.ts
2
2
 
3
+ //# sourceMappingURL=types.d.ts.map
4
+ //#endregion
5
+ //#region src/tools.d.ts
6
+ /**
7
+ * Tool-definition helpers — the `defineTool` / `ok` / `errResult` trio that
8
+ * was copy-pasted across 8 plugins (identity, google, teams, mobile,
9
+ * whatsapp, voice, chat a2a-tools, base openclaw).
10
+ *
11
+ * Error handling is standardized on the openclaw-google variant — the only
12
+ * copy that survived non-`Error` throws (`e instanceof Error ? e.message :
13
+ * "Unknown error"`). The other copies did `(e as Error).message`, which
14
+ * crashes the tool executor when a handler throws a string/object.
15
+ */
16
+ /** Shape returned to OpenClaw from a tool `execute`. */
17
+ interface ToolResult {
18
+ content: {
19
+ type: "text";
20
+ text: string;
21
+ }[];
22
+ details: unknown;
23
+ }
24
+ /**
25
+ * An OpenClaw tool definition.
26
+ *
27
+ * `parameters` is generic because the fleet is split between TypeBox
28
+ * `TSchema` schemas (identity/google/teams/mobile/whatsapp) and plain
29
+ * JSON-Schema objects (chat a2a-tools). Instantiate with whichever schema
30
+ * type the plugin uses — the kit itself has no schema dependency.
31
+ */
32
+ interface ToolDef<TParameters = unknown> {
33
+ name: string;
34
+ description: string;
35
+ label: string;
36
+ parameters: TParameters;
37
+ execute: (toolCallId: string, params: Record<string, unknown>) => Promise<ToolResult>;
38
+ }
39
+ /** Wrap a successful handler result in the OpenClaw tool-result envelope. */
40
+ //#endregion
3
41
  //#region src/plugin.d.ts
4
42
 
5
43
  interface Logger {
@@ -46,19 +84,6 @@ interface OpenClawPluginApi {
46
84
  priority?: number;
47
85
  }): void;
48
86
  }
49
- interface ToolDef {
50
- name: string;
51
- description: string;
52
- label: string;
53
- parameters: TSchema;
54
- execute: (toolCallId: string, params: Record<string, unknown>) => Promise<{
55
- content: {
56
- type: 'text';
57
- text: string;
58
- }[];
59
- details: unknown;
60
- }>;
61
- }
62
87
  declare const plugin: {
63
88
  id: string;
64
89
  name: string;
@@ -68,4 +93,4 @@ declare const plugin: {
68
93
  deactivate(api: OpenClawPluginApi): void;
69
94
  };
70
95
  //#endregion
71
- export { plugin as default };
96
+ export { plugin as t };
package/dist/plugin.d.ts CHANGED
@@ -1,5 +1,43 @@
1
- import { TSchema } from "@sinclair/typebox";
1
+ //#region ../openclaw-plugin-kit/dist/index.d.ts
2
2
 
3
+ //# sourceMappingURL=types.d.ts.map
4
+ //#endregion
5
+ //#region src/tools.d.ts
6
+ /**
7
+ * Tool-definition helpers — the `defineTool` / `ok` / `errResult` trio that
8
+ * was copy-pasted across 8 plugins (identity, google, teams, mobile,
9
+ * whatsapp, voice, chat a2a-tools, base openclaw).
10
+ *
11
+ * Error handling is standardized on the openclaw-google variant — the only
12
+ * copy that survived non-`Error` throws (`e instanceof Error ? e.message :
13
+ * "Unknown error"`). The other copies did `(e as Error).message`, which
14
+ * crashes the tool executor when a handler throws a string/object.
15
+ */
16
+ /** Shape returned to OpenClaw from a tool `execute`. */
17
+ interface ToolResult {
18
+ content: {
19
+ type: "text";
20
+ text: string;
21
+ }[];
22
+ details: unknown;
23
+ }
24
+ /**
25
+ * An OpenClaw tool definition.
26
+ *
27
+ * `parameters` is generic because the fleet is split between TypeBox
28
+ * `TSchema` schemas (identity/google/teams/mobile/whatsapp) and plain
29
+ * JSON-Schema objects (chat a2a-tools). Instantiate with whichever schema
30
+ * type the plugin uses — the kit itself has no schema dependency.
31
+ */
32
+ interface ToolDef<TParameters = unknown> {
33
+ name: string;
34
+ description: string;
35
+ label: string;
36
+ parameters: TParameters;
37
+ execute: (toolCallId: string, params: Record<string, unknown>) => Promise<ToolResult>;
38
+ }
39
+ /** Wrap a successful handler result in the OpenClaw tool-result envelope. */
40
+ //#endregion
3
41
  //#region src/plugin.d.ts
4
42
 
5
43
  interface Logger {
@@ -46,19 +84,6 @@ interface OpenClawPluginApi {
46
84
  priority?: number;
47
85
  }): void;
48
86
  }
49
- interface ToolDef {
50
- name: string;
51
- description: string;
52
- label: string;
53
- parameters: TSchema;
54
- execute: (toolCallId: string, params: Record<string, unknown>) => Promise<{
55
- content: {
56
- type: 'text';
57
- text: string;
58
- }[];
59
- details: unknown;
60
- }>;
61
- }
62
87
  declare const plugin: {
63
88
  id: string;
64
89
  name: string;
@@ -68,4 +93,4 @@ declare const plugin: {
68
93
  deactivate(api: OpenClawPluginApi): void;
69
94
  };
70
95
  //#endregion
71
- export { plugin as default };
96
+ export { plugin as t };
package/dist/plugin2.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  let _sinclair_typebox = require("@sinclair/typebox");
2
2
  let _alfe_ai_config = require("@alfe.ai/config");
3
3
  let _alfe_ai_agent_api_client = require("@alfe.ai/agent-api-client");
4
+ let _alfe_ai_openclaw_plugin_kit = require("@alfe.ai/openclaw-plugin-kit");
4
5
  let node_fs_promises = require("node:fs/promises");
5
6
  let node_path = require("node:path");
6
7
  let node_module = require("node:module");
@@ -79,9 +80,11 @@ function parseWav(buf) {
79
80
  * - voice_tts tool — text → audio file via POST /voice/tts (agent-authed)
80
81
  * - voice_stt tool — audio file → transcript via POST /voice/stt (agent-authed)
81
82
  * - voice.speak RPC — legacy gateway RPC (reads plugin config; vestigial)
82
- * - voice_hangup tool — placeholder (requires channel service)
83
- * - voice_transfer tool placeholder (requires Twilio service)
84
- * - voice_dtmf tool — placeholder (requires Twilio service)
83
+ *
84
+ * voice_hangup / voice_transfer / voice_dtmf were removed deliberately — they
85
+ * threw unconditionally ("requires a channel service"), which poisons the
86
+ * model's tool selection with always-failing options. Re-add per-channel when
87
+ * the twilio-adapter/Discord dispatch actually lands (voice-engineer roadmap).
85
88
  *
86
89
  * The voice_tts/voice_stt tools source URL + agent key from ~/.alfe/config.toml
87
90
  * via `@alfe.ai/config` (resolveConfig) and call through `AgentApiClient` — the
@@ -95,6 +98,7 @@ const VOICE_CAPABILITIES = [
95
98
  "voice.dtmf",
96
99
  "voice.hangup"
97
100
  ];
101
+ const VOICE_ACTIVATION_KEY = (0, _alfe_ai_openclaw_plugin_kit.getActivationKey)("voice");
98
102
  let voiceServiceUrl = "";
99
103
  let voiceServiceApiKey = "";
100
104
  async function voiceApi(method, path, body) {
@@ -114,65 +118,6 @@ async function voiceApi(method, path, body) {
114
118
  return json;
115
119
  }
116
120
  let daemonIpcClient = null;
117
- async function connectToDaemon(socketPath, log) {
118
- try {
119
- const IPCClientCtor = (await import("@alfe.ai/openclaw")).IPCClient;
120
- const client = new IPCClientCtor(socketPath, log);
121
- client.on("connected", async () => {
122
- log.info("Connected to Alfe daemon — registering voice capabilities...");
123
- const response = await client.request("capability.register", {
124
- plugin: "@alfe.ai/openclaw-voice",
125
- capabilities: [...VOICE_CAPABILITIES]
126
- });
127
- if (response.ok) log.info("Voice capabilities registered with daemon");
128
- else log.warn(`Failed to register voice capabilities: ${response.error?.message ?? "unknown"}`);
129
- });
130
- client.on("disconnected", (reason) => {
131
- log.warn(`Disconnected from Alfe daemon: ${String(reason)}`);
132
- });
133
- client.on("error", (err) => {
134
- log.debug(`Daemon IPC error: ${err.message}`);
135
- });
136
- client.start();
137
- return client;
138
- } catch {
139
- log.info("Alfe daemon not available — voice plugin running standalone");
140
- return null;
141
- }
142
- }
143
- function ok(data) {
144
- return {
145
- content: [{
146
- type: "text",
147
- text: JSON.stringify(data)
148
- }],
149
- details: data
150
- };
151
- }
152
- function errResult(message) {
153
- return {
154
- content: [{
155
- type: "text",
156
- text: JSON.stringify({ error: message })
157
- }],
158
- details: { error: message }
159
- };
160
- }
161
- function defineTool(def) {
162
- return {
163
- name: def.name,
164
- description: def.description,
165
- label: def.name,
166
- parameters: def.parameters,
167
- execute: async (_toolCallId, params) => {
168
- try {
169
- return ok(await def.handler(params));
170
- } catch (e) {
171
- return errResult(e.message);
172
- }
173
- }
174
- };
175
- }
176
121
  let voiceClient = null;
177
122
  function getVoiceClient() {
178
123
  if (voiceClient) return voiceClient;
@@ -187,102 +132,69 @@ function getVoiceClient() {
187
132
  function resolveWorkspacePath(p) {
188
133
  return (0, node_path.isAbsolute)(p) ? p : (0, node_path.resolve)(process.cwd(), p);
189
134
  }
190
- const voiceTools = [
191
- defineTool({
192
- name: "voice_tts",
193
- description: "Convert text to speech using the Alfe voice service (ElevenLabs). Writes a playable audio file to the workspace and returns its path. Billed per character to your tenant credit pool.",
194
- parameters: _sinclair_typebox.Type.Object({
195
- text: _sinclair_typebox.Type.String({ description: "Text to synthesize (1–5000 characters)." }),
196
- outputPath: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Where to write the audio file (absolute, or relative to the working directory). Defaults to alfe-tts-<timestamp>.wav in the working directory." })),
197
- format: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.Union([_sinclair_typebox.Type.Literal("wav"), _sinclair_typebox.Type.Literal("pcm")], { description: "Output container. 'wav' (default) is a playable file; 'pcm' is headerless 24kHz/mono/16-bit raw PCM." })),
198
- voiceId: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "ElevenLabs voice ID. Platform default when omitted." })),
199
- model: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.Union([_sinclair_typebox.Type.Literal("eleven_turbo_v2_5"), _sinclair_typebox.Type.Literal("eleven_multilingual_v2")], { description: "TTS model. eleven_turbo_v2_5 (lower latency) when omitted." }))
200
- }),
201
- handler: async (params) => {
202
- const text = params.text;
203
- const format = params.format ?? "wav";
204
- const result = await getVoiceClient().tts({
205
- text,
206
- voiceId: params.voiceId,
207
- model: params.model
208
- });
209
- const bytes = format === "wav" ? pcmToWav(result.audio, {
210
- sampleRate: result.sampleRate,
211
- channels: result.channels,
212
- bitDepth: result.bitDepth
213
- }) : result.audio;
214
- const outputPath = params.outputPath ?? `alfe-tts-${String(Date.now())}.${format}`;
215
- const absolutePath = resolveWorkspacePath(outputPath);
216
- await (0, node_fs_promises.writeFile)(absolutePath, bytes);
217
- return {
218
- path: outputPath,
219
- absolutePath,
220
- format,
221
- sampleRate: result.sampleRate,
222
- channels: result.channels,
223
- bitDepth: result.bitDepth,
224
- bytes: bytes.length,
225
- characters: text.length
226
- };
227
- }
135
+ const voiceTools = [(0, _alfe_ai_openclaw_plugin_kit.defineTool)({
136
+ name: "voice_tts",
137
+ description: "Convert text to speech using the Alfe voice service (ElevenLabs). Writes a playable audio file to the workspace and returns its path. Billed per character to your tenant credit pool.",
138
+ parameters: _sinclair_typebox.Type.Object({
139
+ text: _sinclair_typebox.Type.String({ description: "Text to synthesize (1–5000 characters)." }),
140
+ outputPath: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Where to write the audio file (absolute, or relative to the working directory). Defaults to alfe-tts-<timestamp>.wav in the working directory." })),
141
+ format: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.Union([_sinclair_typebox.Type.Literal("wav"), _sinclair_typebox.Type.Literal("pcm")], { description: "Output container. 'wav' (default) is a playable file; 'pcm' is headerless 24kHz/mono/16-bit raw PCM." })),
142
+ voiceId: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "ElevenLabs voice ID. Platform default when omitted." })),
143
+ model: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.Union([_sinclair_typebox.Type.Literal("eleven_turbo_v2_5"), _sinclair_typebox.Type.Literal("eleven_multilingual_v2")], { description: "TTS model. eleven_turbo_v2_5 (lower latency) when omitted." }))
228
144
  }),
229
- defineTool({
230
- name: "voice_stt",
231
- description: "Transcribe an audio file to text using the Alfe voice service (Deepgram). Accepts a WAV file or headerless mono 16-bit PCM. Billed per audio-second to your tenant credit pool.",
232
- parameters: _sinclair_typebox.Type.Object({
233
- path: _sinclair_typebox.Type.String({ description: "Path to the audio file (absolute, or relative to the working directory). WAV or raw linear16 mono PCM." }),
234
- sampleRate: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.Number({ description: "Sample rate in Hz (8000–48000). Only used for headerless PCM input; ignored for WAV (its header wins). Defaults to 24000." }))
235
- }),
236
- handler: async (params) => {
237
- const raw = await (0, node_fs_promises.readFile)(resolveWorkspacePath(params.path));
238
- const wav = parseWav(raw);
239
- let audio;
240
- let sampleRate;
241
- if (wav) {
242
- if (wav.bitDepth !== 16 || wav.channels !== 1) throw new Error(`voice_stt needs 16-bit mono audio; got ${String(wav.bitDepth)}-bit / ${String(wav.channels)}-channel WAV. Re-export as 16-bit mono.`);
243
- audio = wav.pcm;
244
- sampleRate = wav.sampleRate;
245
- } else {
246
- audio = raw;
247
- sampleRate = params.sampleRate ?? 24e3;
248
- }
249
- return getVoiceClient().stt({
250
- audio,
251
- sampleRate
252
- });
253
- }
254
- }),
255
- defineTool({
256
- name: "voice_hangup",
257
- description: "Hang up the current voice call or leave the voice channel. Use when the conversation is done or the user asks you to leave.",
258
- parameters: _sinclair_typebox.Type.Object({ sessionId: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Voice session ID." })) }),
259
- handler: () => {
260
- throw new Error("voice_hangup requires a channel service (Discord/Twilio). The voice service no longer manages channels directly.");
261
- }
262
- }),
263
- defineTool({
264
- name: "voice_transfer",
265
- description: "Transfer the current phone call to another number. Only works for Twilio calls.",
266
- parameters: _sinclair_typebox.Type.Object({
267
- targetNumber: _sinclair_typebox.Type.String({ description: "Phone number to transfer to (E.164 format)" }),
268
- sessionId: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Voice session ID." }))
269
- }),
270
- handler: () => {
271
- throw new Error("voice_transfer requires the Twilio service. The voice service no longer manages phone calls directly.");
272
- }
145
+ handler: async (params) => {
146
+ const text = params.text;
147
+ const format = params.format ?? "wav";
148
+ const result = await getVoiceClient().tts({
149
+ text,
150
+ voiceId: params.voiceId,
151
+ model: params.model
152
+ });
153
+ const bytes = format === "wav" ? pcmToWav(result.audio, {
154
+ sampleRate: result.sampleRate,
155
+ channels: result.channels,
156
+ bitDepth: result.bitDepth
157
+ }) : result.audio;
158
+ const outputPath = params.outputPath ?? `alfe-tts-${String(Date.now())}.${format}`;
159
+ const absolutePath = resolveWorkspacePath(outputPath);
160
+ await (0, node_fs_promises.writeFile)(absolutePath, bytes);
161
+ return {
162
+ path: outputPath,
163
+ absolutePath,
164
+ format,
165
+ sampleRate: result.sampleRate,
166
+ channels: result.channels,
167
+ bitDepth: result.bitDepth,
168
+ bytes: bytes.length,
169
+ characters: text.length
170
+ };
171
+ }
172
+ }), (0, _alfe_ai_openclaw_plugin_kit.defineTool)({
173
+ name: "voice_stt",
174
+ description: "Transcribe an audio file to text using the Alfe voice service (Deepgram). Accepts a WAV file or headerless mono 16-bit PCM. Billed per audio-second to your tenant credit pool.",
175
+ parameters: _sinclair_typebox.Type.Object({
176
+ path: _sinclair_typebox.Type.String({ description: "Path to the audio file (absolute, or relative to the working directory). WAV or raw linear16 mono PCM." }),
177
+ sampleRate: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.Number({ description: "Sample rate in Hz (8000–48000). Only used for headerless PCM input; ignored for WAV (its header wins). Defaults to 24000." }))
273
178
  }),
274
- defineTool({
275
- name: "voice_dtmf",
276
- description: "Send DTMF tones (dial pad digits) on the current phone call. Only works for Twilio calls.",
277
- parameters: _sinclair_typebox.Type.Object({
278
- digits: _sinclair_typebox.Type.String({ description: "DTMF digits to send (0-9, *, #, w for pause)" }),
279
- sessionId: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Voice session ID." }))
280
- }),
281
- handler: () => {
282
- throw new Error("voice_dtmf requires the Twilio service. The voice service no longer manages phone calls directly.");
179
+ handler: async (params) => {
180
+ const raw = await (0, node_fs_promises.readFile)(resolveWorkspacePath(params.path));
181
+ const wav = parseWav(raw);
182
+ let audio;
183
+ let sampleRate;
184
+ if (wav) {
185
+ if (wav.bitDepth !== 16 || wav.channels !== 1) throw new Error(`voice_stt needs 16-bit mono audio; got ${String(wav.bitDepth)}-bit / ${String(wav.channels)}-channel WAV. Re-export as 16-bit mono.`);
186
+ audio = wav.pcm;
187
+ sampleRate = wav.sampleRate;
188
+ } else {
189
+ audio = raw;
190
+ sampleRate = params.sampleRate ?? 24e3;
283
191
  }
284
- })
285
- ];
192
+ return getVoiceClient().stt({
193
+ audio,
194
+ sampleRate
195
+ });
196
+ }
197
+ })];
286
198
  const plugin = {
287
199
  id: "@alfe.ai/openclaw-voice",
288
200
  name: "Alfe Voice Plugin",
@@ -296,27 +208,27 @@ const plugin = {
296
208
  const fullConfig = api.config ?? {};
297
209
  const pluginConfig = fullConfig.plugins?.entries?.["@alfe.ai/openclaw-voice"]?.config ?? fullConfig.plugins?.entries?.["voice-gateway"]?.config ?? {};
298
210
  const startVoiceService = () => {
299
- if (globalThis.__voiceGatewayActivated === true) {
300
- log.debug("Alfe Voice plugin already activated — skipping duplicate");
301
- return;
302
- }
303
- globalThis.__voiceGatewayActivated = true;
304
- log.info("Alfe Voice plugin activating...");
305
- voiceServiceUrl = pluginConfig.voiceServiceUrl ?? `http://localhost:${String(pluginConfig.voiceServicePort ?? "3100")}`;
306
- voiceServiceApiKey = pluginConfig.voiceServiceApiKey ?? "";
307
- log.info(`Voice service: ${voiceServiceUrl}`);
308
- let alfeConfig = null;
309
- try {
310
- alfeConfig = (0, _alfe_ai_config.resolveConfig)();
311
- } catch {}
312
- connectToDaemon(pluginConfig.daemonSocket ?? alfeConfig?.socketPath ?? _alfe_ai_config.DEFAULT_SOCKET_PATH, log).then((client) => {
313
- daemonIpcClient = client;
314
- }).catch((err) => {
315
- log.debug(`Daemon connect failed: ${err.message}`);
211
+ (0, _alfe_ai_openclaw_plugin_kit.guardedStart)(VOICE_ACTIVATION_KEY, log, () => {
212
+ log.info("Alfe Voice plugin activating...");
213
+ voiceServiceUrl = pluginConfig.voiceServiceUrl ?? `http://localhost:${String(pluginConfig.voiceServicePort ?? "3100")}`;
214
+ voiceServiceApiKey = pluginConfig.voiceServiceApiKey ?? "";
215
+ log.info(`Voice service: ${voiceServiceUrl}`);
216
+ let alfeConfig = null;
217
+ try {
218
+ alfeConfig = (0, _alfe_ai_config.resolveConfig)();
219
+ } catch {}
220
+ (0, _alfe_ai_openclaw_plugin_kit.connectToDaemon)(pluginConfig.daemonSocket ?? alfeConfig?.socketPath ?? _alfe_ai_config.DEFAULT_SOCKET_PATH, log, {
221
+ pluginId: "@alfe.ai/openclaw-voice",
222
+ capabilities: VOICE_CAPABILITIES,
223
+ standaloneNote: "Alfe daemon not available — voice plugin running standalone"
224
+ }).then((client) => {
225
+ daemonIpcClient = client;
226
+ }).catch((err) => {
227
+ log.debug(`Daemon connect failed: ${err.message}`);
228
+ });
316
229
  });
317
230
  };
318
231
  const stopVoiceService = () => {
319
- globalThis.__voiceGatewayActivated = false;
320
232
  if (daemonIpcClient) {
321
233
  try {
322
234
  daemonIpcClient.stop();
@@ -326,6 +238,7 @@ const plugin = {
326
238
  }
327
239
  daemonIpcClient = null;
328
240
  }
241
+ (0, _alfe_ai_openclaw_plugin_kit.resetActivation)(VOICE_ACTIVATION_KEY);
329
242
  log.info("Alfe Voice plugin deactivated");
330
243
  };
331
244
  api.registerGatewayMethod("voice.speak", async (...args) => {
@@ -351,7 +264,6 @@ const plugin = {
351
264
  log.info("Alfe Voice plugin activated");
352
265
  },
353
266
  deactivate(api) {
354
- globalThis.__voiceGatewayActivated = false;
355
267
  const log = api.logger;
356
268
  log.info("Alfe Voice plugin deactivating...");
357
269
  if (daemonIpcClient) {
@@ -363,6 +275,7 @@ const plugin = {
363
275
  }
364
276
  daemonIpcClient = null;
365
277
  }
278
+ (0, _alfe_ai_openclaw_plugin_kit.resetActivation)(VOICE_ACTIVATION_KEY);
366
279
  log.info("Alfe Voice plugin deactivated");
367
280
  }
368
281
  };
@@ -0,0 +1,2 @@
1
+ import { t as plugin } from "./plugin.cjs";
2
+ export { plugin as default };
@@ -0,0 +1,2 @@
1
+ import { t as plugin } from "./plugin.js";
2
+ export { plugin as default };
package/dist/plugin2.js CHANGED
@@ -2,6 +2,7 @@ import { createRequire } from "node:module";
2
2
  import { Type } from "@sinclair/typebox";
3
3
  import { DEFAULT_SOCKET_PATH, resolveConfig } from "@alfe.ai/config";
4
4
  import { AgentApiClient, installToolErrorCapture } from "@alfe.ai/agent-api-client";
5
+ import { connectToDaemon, defineTool, getActivationKey, guardedStart, resetActivation } from "@alfe.ai/openclaw-plugin-kit";
5
6
  import { readFile, writeFile } from "node:fs/promises";
6
7
  import { isAbsolute, resolve } from "node:path";
7
8
  //#region src/audio.ts
@@ -79,9 +80,11 @@ function parseWav(buf) {
79
80
  * - voice_tts tool — text → audio file via POST /voice/tts (agent-authed)
80
81
  * - voice_stt tool — audio file → transcript via POST /voice/stt (agent-authed)
81
82
  * - voice.speak RPC — legacy gateway RPC (reads plugin config; vestigial)
82
- * - voice_hangup tool — placeholder (requires channel service)
83
- * - voice_transfer tool placeholder (requires Twilio service)
84
- * - voice_dtmf tool — placeholder (requires Twilio service)
83
+ *
84
+ * voice_hangup / voice_transfer / voice_dtmf were removed deliberately — they
85
+ * threw unconditionally ("requires a channel service"), which poisons the
86
+ * model's tool selection with always-failing options. Re-add per-channel when
87
+ * the twilio-adapter/Discord dispatch actually lands (voice-engineer roadmap).
85
88
  *
86
89
  * The voice_tts/voice_stt tools source URL + agent key from ~/.alfe/config.toml
87
90
  * via `@alfe.ai/config` (resolveConfig) and call through `AgentApiClient` — the
@@ -95,6 +98,7 @@ const VOICE_CAPABILITIES = [
95
98
  "voice.dtmf",
96
99
  "voice.hangup"
97
100
  ];
101
+ const VOICE_ACTIVATION_KEY = getActivationKey("voice");
98
102
  let voiceServiceUrl = "";
99
103
  let voiceServiceApiKey = "";
100
104
  async function voiceApi(method, path, body) {
@@ -114,65 +118,6 @@ async function voiceApi(method, path, body) {
114
118
  return json;
115
119
  }
116
120
  let daemonIpcClient = null;
117
- async function connectToDaemon(socketPath, log) {
118
- try {
119
- const IPCClientCtor = (await import("@alfe.ai/openclaw")).IPCClient;
120
- const client = new IPCClientCtor(socketPath, log);
121
- client.on("connected", async () => {
122
- log.info("Connected to Alfe daemon — registering voice capabilities...");
123
- const response = await client.request("capability.register", {
124
- plugin: "@alfe.ai/openclaw-voice",
125
- capabilities: [...VOICE_CAPABILITIES]
126
- });
127
- if (response.ok) log.info("Voice capabilities registered with daemon");
128
- else log.warn(`Failed to register voice capabilities: ${response.error?.message ?? "unknown"}`);
129
- });
130
- client.on("disconnected", (reason) => {
131
- log.warn(`Disconnected from Alfe daemon: ${String(reason)}`);
132
- });
133
- client.on("error", (err) => {
134
- log.debug(`Daemon IPC error: ${err.message}`);
135
- });
136
- client.start();
137
- return client;
138
- } catch {
139
- log.info("Alfe daemon not available — voice plugin running standalone");
140
- return null;
141
- }
142
- }
143
- function ok(data) {
144
- return {
145
- content: [{
146
- type: "text",
147
- text: JSON.stringify(data)
148
- }],
149
- details: data
150
- };
151
- }
152
- function errResult(message) {
153
- return {
154
- content: [{
155
- type: "text",
156
- text: JSON.stringify({ error: message })
157
- }],
158
- details: { error: message }
159
- };
160
- }
161
- function defineTool(def) {
162
- return {
163
- name: def.name,
164
- description: def.description,
165
- label: def.name,
166
- parameters: def.parameters,
167
- execute: async (_toolCallId, params) => {
168
- try {
169
- return ok(await def.handler(params));
170
- } catch (e) {
171
- return errResult(e.message);
172
- }
173
- }
174
- };
175
- }
176
121
  let voiceClient = null;
177
122
  function getVoiceClient() {
178
123
  if (voiceClient) return voiceClient;
@@ -187,102 +132,69 @@ function getVoiceClient() {
187
132
  function resolveWorkspacePath(p) {
188
133
  return isAbsolute(p) ? p : resolve(process.cwd(), p);
189
134
  }
190
- const voiceTools = [
191
- defineTool({
192
- name: "voice_tts",
193
- description: "Convert text to speech using the Alfe voice service (ElevenLabs). Writes a playable audio file to the workspace and returns its path. Billed per character to your tenant credit pool.",
194
- parameters: Type.Object({
195
- text: Type.String({ description: "Text to synthesize (1–5000 characters)." }),
196
- outputPath: Type.Optional(Type.String({ description: "Where to write the audio file (absolute, or relative to the working directory). Defaults to alfe-tts-<timestamp>.wav in the working directory." })),
197
- format: Type.Optional(Type.Union([Type.Literal("wav"), Type.Literal("pcm")], { description: "Output container. 'wav' (default) is a playable file; 'pcm' is headerless 24kHz/mono/16-bit raw PCM." })),
198
- voiceId: Type.Optional(Type.String({ description: "ElevenLabs voice ID. Platform default when omitted." })),
199
- model: Type.Optional(Type.Union([Type.Literal("eleven_turbo_v2_5"), Type.Literal("eleven_multilingual_v2")], { description: "TTS model. eleven_turbo_v2_5 (lower latency) when omitted." }))
200
- }),
201
- handler: async (params) => {
202
- const text = params.text;
203
- const format = params.format ?? "wav";
204
- const result = await getVoiceClient().tts({
205
- text,
206
- voiceId: params.voiceId,
207
- model: params.model
208
- });
209
- const bytes = format === "wav" ? pcmToWav(result.audio, {
210
- sampleRate: result.sampleRate,
211
- channels: result.channels,
212
- bitDepth: result.bitDepth
213
- }) : result.audio;
214
- const outputPath = params.outputPath ?? `alfe-tts-${String(Date.now())}.${format}`;
215
- const absolutePath = resolveWorkspacePath(outputPath);
216
- await writeFile(absolutePath, bytes);
217
- return {
218
- path: outputPath,
219
- absolutePath,
220
- format,
221
- sampleRate: result.sampleRate,
222
- channels: result.channels,
223
- bitDepth: result.bitDepth,
224
- bytes: bytes.length,
225
- characters: text.length
226
- };
227
- }
135
+ const voiceTools = [defineTool({
136
+ name: "voice_tts",
137
+ description: "Convert text to speech using the Alfe voice service (ElevenLabs). Writes a playable audio file to the workspace and returns its path. Billed per character to your tenant credit pool.",
138
+ parameters: Type.Object({
139
+ text: Type.String({ description: "Text to synthesize (1–5000 characters)." }),
140
+ outputPath: Type.Optional(Type.String({ description: "Where to write the audio file (absolute, or relative to the working directory). Defaults to alfe-tts-<timestamp>.wav in the working directory." })),
141
+ format: Type.Optional(Type.Union([Type.Literal("wav"), Type.Literal("pcm")], { description: "Output container. 'wav' (default) is a playable file; 'pcm' is headerless 24kHz/mono/16-bit raw PCM." })),
142
+ voiceId: Type.Optional(Type.String({ description: "ElevenLabs voice ID. Platform default when omitted." })),
143
+ model: Type.Optional(Type.Union([Type.Literal("eleven_turbo_v2_5"), Type.Literal("eleven_multilingual_v2")], { description: "TTS model. eleven_turbo_v2_5 (lower latency) when omitted." }))
228
144
  }),
229
- defineTool({
230
- name: "voice_stt",
231
- description: "Transcribe an audio file to text using the Alfe voice service (Deepgram). Accepts a WAV file or headerless mono 16-bit PCM. Billed per audio-second to your tenant credit pool.",
232
- parameters: Type.Object({
233
- path: Type.String({ description: "Path to the audio file (absolute, or relative to the working directory). WAV or raw linear16 mono PCM." }),
234
- sampleRate: Type.Optional(Type.Number({ description: "Sample rate in Hz (8000–48000). Only used for headerless PCM input; ignored for WAV (its header wins). Defaults to 24000." }))
235
- }),
236
- handler: async (params) => {
237
- const raw = await readFile(resolveWorkspacePath(params.path));
238
- const wav = parseWav(raw);
239
- let audio;
240
- let sampleRate;
241
- if (wav) {
242
- if (wav.bitDepth !== 16 || wav.channels !== 1) throw new Error(`voice_stt needs 16-bit mono audio; got ${String(wav.bitDepth)}-bit / ${String(wav.channels)}-channel WAV. Re-export as 16-bit mono.`);
243
- audio = wav.pcm;
244
- sampleRate = wav.sampleRate;
245
- } else {
246
- audio = raw;
247
- sampleRate = params.sampleRate ?? 24e3;
248
- }
249
- return getVoiceClient().stt({
250
- audio,
251
- sampleRate
252
- });
253
- }
254
- }),
255
- defineTool({
256
- name: "voice_hangup",
257
- description: "Hang up the current voice call or leave the voice channel. Use when the conversation is done or the user asks you to leave.",
258
- parameters: Type.Object({ sessionId: Type.Optional(Type.String({ description: "Voice session ID." })) }),
259
- handler: () => {
260
- throw new Error("voice_hangup requires a channel service (Discord/Twilio). The voice service no longer manages channels directly.");
261
- }
262
- }),
263
- defineTool({
264
- name: "voice_transfer",
265
- description: "Transfer the current phone call to another number. Only works for Twilio calls.",
266
- parameters: Type.Object({
267
- targetNumber: Type.String({ description: "Phone number to transfer to (E.164 format)" }),
268
- sessionId: Type.Optional(Type.String({ description: "Voice session ID." }))
269
- }),
270
- handler: () => {
271
- throw new Error("voice_transfer requires the Twilio service. The voice service no longer manages phone calls directly.");
272
- }
145
+ handler: async (params) => {
146
+ const text = params.text;
147
+ const format = params.format ?? "wav";
148
+ const result = await getVoiceClient().tts({
149
+ text,
150
+ voiceId: params.voiceId,
151
+ model: params.model
152
+ });
153
+ const bytes = format === "wav" ? pcmToWav(result.audio, {
154
+ sampleRate: result.sampleRate,
155
+ channels: result.channels,
156
+ bitDepth: result.bitDepth
157
+ }) : result.audio;
158
+ const outputPath = params.outputPath ?? `alfe-tts-${String(Date.now())}.${format}`;
159
+ const absolutePath = resolveWorkspacePath(outputPath);
160
+ await writeFile(absolutePath, bytes);
161
+ return {
162
+ path: outputPath,
163
+ absolutePath,
164
+ format,
165
+ sampleRate: result.sampleRate,
166
+ channels: result.channels,
167
+ bitDepth: result.bitDepth,
168
+ bytes: bytes.length,
169
+ characters: text.length
170
+ };
171
+ }
172
+ }), defineTool({
173
+ name: "voice_stt",
174
+ description: "Transcribe an audio file to text using the Alfe voice service (Deepgram). Accepts a WAV file or headerless mono 16-bit PCM. Billed per audio-second to your tenant credit pool.",
175
+ parameters: Type.Object({
176
+ path: Type.String({ description: "Path to the audio file (absolute, or relative to the working directory). WAV or raw linear16 mono PCM." }),
177
+ sampleRate: Type.Optional(Type.Number({ description: "Sample rate in Hz (8000–48000). Only used for headerless PCM input; ignored for WAV (its header wins). Defaults to 24000." }))
273
178
  }),
274
- defineTool({
275
- name: "voice_dtmf",
276
- description: "Send DTMF tones (dial pad digits) on the current phone call. Only works for Twilio calls.",
277
- parameters: Type.Object({
278
- digits: Type.String({ description: "DTMF digits to send (0-9, *, #, w for pause)" }),
279
- sessionId: Type.Optional(Type.String({ description: "Voice session ID." }))
280
- }),
281
- handler: () => {
282
- throw new Error("voice_dtmf requires the Twilio service. The voice service no longer manages phone calls directly.");
179
+ handler: async (params) => {
180
+ const raw = await readFile(resolveWorkspacePath(params.path));
181
+ const wav = parseWav(raw);
182
+ let audio;
183
+ let sampleRate;
184
+ if (wav) {
185
+ if (wav.bitDepth !== 16 || wav.channels !== 1) throw new Error(`voice_stt needs 16-bit mono audio; got ${String(wav.bitDepth)}-bit / ${String(wav.channels)}-channel WAV. Re-export as 16-bit mono.`);
186
+ audio = wav.pcm;
187
+ sampleRate = wav.sampleRate;
188
+ } else {
189
+ audio = raw;
190
+ sampleRate = params.sampleRate ?? 24e3;
283
191
  }
284
- })
285
- ];
192
+ return getVoiceClient().stt({
193
+ audio,
194
+ sampleRate
195
+ });
196
+ }
197
+ })];
286
198
  const plugin = {
287
199
  id: "@alfe.ai/openclaw-voice",
288
200
  name: "Alfe Voice Plugin",
@@ -296,27 +208,27 @@ const plugin = {
296
208
  const fullConfig = api.config ?? {};
297
209
  const pluginConfig = fullConfig.plugins?.entries?.["@alfe.ai/openclaw-voice"]?.config ?? fullConfig.plugins?.entries?.["voice-gateway"]?.config ?? {};
298
210
  const startVoiceService = () => {
299
- if (globalThis.__voiceGatewayActivated === true) {
300
- log.debug("Alfe Voice plugin already activated — skipping duplicate");
301
- return;
302
- }
303
- globalThis.__voiceGatewayActivated = true;
304
- log.info("Alfe Voice plugin activating...");
305
- voiceServiceUrl = pluginConfig.voiceServiceUrl ?? `http://localhost:${String(pluginConfig.voiceServicePort ?? "3100")}`;
306
- voiceServiceApiKey = pluginConfig.voiceServiceApiKey ?? "";
307
- log.info(`Voice service: ${voiceServiceUrl}`);
308
- let alfeConfig = null;
309
- try {
310
- alfeConfig = resolveConfig();
311
- } catch {}
312
- connectToDaemon(pluginConfig.daemonSocket ?? alfeConfig?.socketPath ?? DEFAULT_SOCKET_PATH, log).then((client) => {
313
- daemonIpcClient = client;
314
- }).catch((err) => {
315
- log.debug(`Daemon connect failed: ${err.message}`);
211
+ guardedStart(VOICE_ACTIVATION_KEY, log, () => {
212
+ log.info("Alfe Voice plugin activating...");
213
+ voiceServiceUrl = pluginConfig.voiceServiceUrl ?? `http://localhost:${String(pluginConfig.voiceServicePort ?? "3100")}`;
214
+ voiceServiceApiKey = pluginConfig.voiceServiceApiKey ?? "";
215
+ log.info(`Voice service: ${voiceServiceUrl}`);
216
+ let alfeConfig = null;
217
+ try {
218
+ alfeConfig = resolveConfig();
219
+ } catch {}
220
+ connectToDaemon(pluginConfig.daemonSocket ?? alfeConfig?.socketPath ?? DEFAULT_SOCKET_PATH, log, {
221
+ pluginId: "@alfe.ai/openclaw-voice",
222
+ capabilities: VOICE_CAPABILITIES,
223
+ standaloneNote: "Alfe daemon not available — voice plugin running standalone"
224
+ }).then((client) => {
225
+ daemonIpcClient = client;
226
+ }).catch((err) => {
227
+ log.debug(`Daemon connect failed: ${err.message}`);
228
+ });
316
229
  });
317
230
  };
318
231
  const stopVoiceService = () => {
319
- globalThis.__voiceGatewayActivated = false;
320
232
  if (daemonIpcClient) {
321
233
  try {
322
234
  daemonIpcClient.stop();
@@ -326,6 +238,7 @@ const plugin = {
326
238
  }
327
239
  daemonIpcClient = null;
328
240
  }
241
+ resetActivation(VOICE_ACTIVATION_KEY);
329
242
  log.info("Alfe Voice plugin deactivated");
330
243
  };
331
244
  api.registerGatewayMethod("voice.speak", async (...args) => {
@@ -351,7 +264,6 @@ const plugin = {
351
264
  log.info("Alfe Voice plugin activated");
352
265
  },
353
266
  deactivate(api) {
354
- globalThis.__voiceGatewayActivated = false;
355
267
  const log = api.logger;
356
268
  log.info("Alfe Voice plugin deactivating...");
357
269
  if (daemonIpcClient) {
@@ -363,6 +275,7 @@ const plugin = {
363
275
  }
364
276
  daemonIpcClient = null;
365
277
  }
278
+ resetActivation(VOICE_ACTIVATION_KEY);
366
279
  log.info("Alfe Voice plugin deactivated");
367
280
  }
368
281
  };
@@ -5,7 +5,7 @@
5
5
  "entry": "./dist/plugin.js",
6
6
  "activation": { "onStartup": true },
7
7
  "contracts": {
8
- "tools": ["voice_tts", "voice_stt", "voice_dtmf", "voice_hangup", "voice_transfer"]
8
+ "tools": ["voice_tts", "voice_stt"]
9
9
  },
10
10
  "configSchema": {
11
11
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/openclaw-voice",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "OpenClaw voice plugin for Alfe — Discord audio, Twilio, Recall.ai",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -28,8 +28,9 @@
28
28
  ],
29
29
  "dependencies": {
30
30
  "@sinclair/typebox": "^0.34.48",
31
- "@alfe.ai/agent-api-client": "0.12.0",
32
- "@alfe.ai/config": "0.3.0"
31
+ "@alfe.ai/agent-api-client": "0.14.0",
32
+ "@alfe.ai/config": "0.4.0",
33
+ "@alfe.ai/openclaw-plugin-kit": "0.1.0"
33
34
  },
34
35
  "peerDependencies": {
35
36
  "openclaw": ">=2026.3.0"