@alfe.ai/openclaw-voice 0.0.6 → 0.0.7

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.cjs ADDED
@@ -0,0 +1,2 @@
1
+ const require_plugin = require("./plugin.cjs");
2
+ module.exports = require_plugin;
@@ -0,0 +1,2 @@
1
+ import plugin from "./plugin.cjs";
2
+ export { plugin as default };
@@ -0,0 +1,193 @@
1
+ let _sinclair_typebox = require("@sinclair/typebox");
2
+ let _alfe_ai_config = require("@alfe.ai/config");
3
+ //#region src/plugin.ts
4
+ /**
5
+ * @alfe/voice-plugin — OpenClaw native plugin
6
+ *
7
+ * Thin client that registers voice tools with OpenClaw and forwards
8
+ * TTS/STT operations to the voice service's public API.
9
+ *
10
+ * In the new architecture, voice service is a channel-agnostic pipeline
11
+ * brain. Channel-specific operations (hangup, transfer, DTMF) are handled
12
+ * by the respective channel services (Discord, Twilio, etc.), not voice.
13
+ *
14
+ * This plugin provides:
15
+ * - voice.speak RPC — converts text to audio via POST /voice/tts
16
+ * - voice_hangup tool — placeholder (requires channel service)
17
+ * - voice_transfer tool — placeholder (requires Twilio service)
18
+ * - voice_dtmf tool �� placeholder (requires Twilio service)
19
+ */
20
+ const VOICE_CAPABILITIES = [
21
+ "voice.call",
22
+ "voice.answer",
23
+ "voice.dtmf",
24
+ "voice.hangup"
25
+ ];
26
+ let voiceServiceUrl = "";
27
+ let voiceServiceApiKey = "";
28
+ async function voiceApi(method, path, body) {
29
+ const url = `${voiceServiceUrl}${path}`;
30
+ const headers = { "Content-Type": "application/json" };
31
+ if (voiceServiceApiKey) headers["x-api-key"] = voiceServiceApiKey;
32
+ const res = await fetch(url, {
33
+ method,
34
+ headers,
35
+ body: body ? JSON.stringify(body) : void 0
36
+ });
37
+ const json = await res.json();
38
+ if (!res.ok) {
39
+ const errorMsg = typeof json.error === "string" ? json.error : `Voice service returned ${String(res.status)}`;
40
+ throw new Error(errorMsg);
41
+ }
42
+ return json;
43
+ }
44
+ let daemonIpcClient = null;
45
+ async function connectToDaemon(socketPath, log) {
46
+ try {
47
+ const IPCClientCtor = (await import("@alfe.ai/openclaw")).IPCClient;
48
+ const client = new IPCClientCtor(socketPath, log);
49
+ client.on("connected", async () => {
50
+ log.info("Connected to Alfe daemon — registering voice capabilities...");
51
+ const response = await client.request("capability.register", {
52
+ plugin: "@alfe.ai/openclaw-voice",
53
+ capabilities: [...VOICE_CAPABILITIES]
54
+ });
55
+ if (response.ok) log.info("Voice capabilities registered with daemon");
56
+ else log.warn(`Failed to register voice capabilities: ${response.error?.message ?? "unknown"}`);
57
+ });
58
+ client.on("disconnected", (reason) => {
59
+ log.warn(`Disconnected from Alfe daemon: ${String(reason)}`);
60
+ });
61
+ client.on("error", (err) => {
62
+ log.debug(`Daemon IPC error: ${err.message}`);
63
+ });
64
+ client.start();
65
+ return client;
66
+ } catch {
67
+ log.info("Alfe daemon not available — voice plugin running standalone");
68
+ return null;
69
+ }
70
+ }
71
+ function ok(data) {
72
+ return {
73
+ content: [{
74
+ type: "text",
75
+ text: JSON.stringify(data)
76
+ }],
77
+ details: data
78
+ };
79
+ }
80
+ function errResult(message) {
81
+ return {
82
+ content: [{
83
+ type: "text",
84
+ text: JSON.stringify({ error: message })
85
+ }],
86
+ details: { error: message }
87
+ };
88
+ }
89
+ function defineTool(def) {
90
+ return {
91
+ name: def.name,
92
+ description: def.description,
93
+ label: def.name,
94
+ parameters: def.parameters,
95
+ execute: async (_toolCallId, params) => {
96
+ try {
97
+ return ok(await def.handler(params));
98
+ } catch (e) {
99
+ return errResult(e.message);
100
+ }
101
+ }
102
+ };
103
+ }
104
+ const voiceTools = [
105
+ defineTool({
106
+ name: "voice_hangup",
107
+ 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.",
108
+ parameters: _sinclair_typebox.Type.Object({ sessionId: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Voice session ID." })) }),
109
+ handler: () => {
110
+ throw new Error("voice_hangup requires a channel service (Discord/Twilio). The voice service no longer manages channels directly.");
111
+ }
112
+ }),
113
+ defineTool({
114
+ name: "voice_transfer",
115
+ description: "Transfer the current phone call to another number. Only works for Twilio calls.",
116
+ parameters: _sinclair_typebox.Type.Object({
117
+ targetNumber: _sinclair_typebox.Type.String({ description: "Phone number to transfer to (E.164 format)" }),
118
+ sessionId: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Voice session ID." }))
119
+ }),
120
+ handler: () => {
121
+ throw new Error("voice_transfer requires the Twilio service. The voice service no longer manages phone calls directly.");
122
+ }
123
+ }),
124
+ defineTool({
125
+ name: "voice_dtmf",
126
+ description: "Send DTMF tones (dial pad digits) on the current phone call. Only works for Twilio calls.",
127
+ parameters: _sinclair_typebox.Type.Object({
128
+ digits: _sinclair_typebox.Type.String({ description: "DTMF digits to send (0-9, *, #, w for pause)" }),
129
+ sessionId: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Voice session ID." }))
130
+ }),
131
+ handler: () => {
132
+ throw new Error("voice_dtmf requires the Twilio service. The voice service no longer manages phone calls directly.");
133
+ }
134
+ })
135
+ ];
136
+ const plugin = {
137
+ id: "@alfe.ai/openclaw-voice",
138
+ name: "Alfe Voice Plugin",
139
+ description: "Voice integration — TTS/STT via the voice service, channel-specific operations via channel services",
140
+ version: "0.2.0",
141
+ activate(api) {
142
+ const log = api.logger;
143
+ for (const tool of voiceTools) api.registerTool(tool);
144
+ log.info(`Registered ${String(voiceTools.length)} voice tools: ${voiceTools.map((t) => t.name).join(", ")}`);
145
+ if (!globalThis.__voiceGatewayActivated) {
146
+ globalThis.__voiceGatewayActivated = true;
147
+ log.info("Alfe Voice plugin activating...");
148
+ const fullConfig = api.config ?? {};
149
+ const pluginConfig = fullConfig.plugins?.entries?.["@alfe.ai/openclaw-voice"]?.config ?? fullConfig.plugins?.entries?.["voice-gateway"]?.config ?? {};
150
+ voiceServiceUrl = pluginConfig.voiceServiceUrl ?? `http://localhost:${String(pluginConfig.voiceServicePort ?? "3100")}`;
151
+ voiceServiceApiKey = pluginConfig.voiceServiceApiKey ?? "";
152
+ log.info(`Voice service: ${voiceServiceUrl}`);
153
+ let alfeConfig = null;
154
+ try {
155
+ alfeConfig = (0, _alfe_ai_config.resolveConfig)();
156
+ } catch {}
157
+ connectToDaemon(pluginConfig.daemonSocket ?? alfeConfig?.socketPath ?? _alfe_ai_config.DEFAULT_SOCKET_PATH, log).then((client) => {
158
+ daemonIpcClient = client;
159
+ }).catch((err) => {
160
+ log.debug(`Daemon connect failed: ${err.message}`);
161
+ });
162
+ }
163
+ api.registerGatewayMethod("voice.speak", async (...args) => {
164
+ const { text } = args[0];
165
+ log.info(`voice.speak RPC → text=${text?.slice(0, 50) ?? "(none)"}...`);
166
+ if (!text) throw new Error("voice.speak requires text");
167
+ return await voiceApi("POST", "/voice/tts", { text });
168
+ });
169
+ log.info("Registered gateway RPC method: voice.speak");
170
+ api.on("message_received", (...eventArgs) => {
171
+ const event = eventArgs[0];
172
+ if (eventArgs[1].channelId.includes("voice")) log.debug(`Voice-related message from ${event.from}: ${event.content.slice(0, 100)}`);
173
+ });
174
+ log.info("Alfe Voice plugin activated");
175
+ },
176
+ deactivate(api) {
177
+ globalThis.__voiceGatewayActivated = false;
178
+ const log = api.logger;
179
+ log.info("Alfe Voice plugin deactivating...");
180
+ if (daemonIpcClient) {
181
+ try {
182
+ daemonIpcClient.stop();
183
+ log.info("Disconnected from Alfe daemon");
184
+ } catch (err) {
185
+ log.debug(`Error disconnecting from daemon: ${err.message}`);
186
+ }
187
+ daemonIpcClient = null;
188
+ }
189
+ log.info("Alfe Voice plugin deactivated");
190
+ }
191
+ };
192
+ //#endregion
193
+ module.exports = plugin;
@@ -0,0 +1,59 @@
1
+ import { TSchema } from "@sinclair/typebox";
2
+
3
+ //#region src/plugin.d.ts
4
+
5
+ interface Logger {
6
+ info(msg: string, ...args: unknown[]): void;
7
+ warn(msg: string, ...args: unknown[]): void;
8
+ error(msg: string, ...args: unknown[]): void;
9
+ debug(msg: string, ...args: unknown[]): void;
10
+ }
11
+ interface VoicePluginConfig {
12
+ voiceServiceUrl?: string;
13
+ voiceServicePort?: string | number;
14
+ voiceServiceApiKey?: string;
15
+ daemonSocket?: string;
16
+ [key: string]: unknown;
17
+ }
18
+ interface OpenClawConfig {
19
+ plugins?: {
20
+ entries?: Record<string, {
21
+ config?: VoicePluginConfig;
22
+ [key: string]: unknown;
23
+ }>;
24
+ [key: string]: unknown;
25
+ };
26
+ [key: string]: unknown;
27
+ }
28
+ interface OpenClawPluginApi {
29
+ logger: Logger;
30
+ config?: OpenClawConfig;
31
+ registerTool(tool: ToolDef): void;
32
+ registerGatewayMethod(name: string, handler: (...args: unknown[]) => Promise<unknown>): void;
33
+ on(event: string, handler: (...args: unknown[]) => void | Promise<void>, options?: {
34
+ priority?: number;
35
+ }): void;
36
+ }
37
+ interface ToolDef {
38
+ name: string;
39
+ description: string;
40
+ label: string;
41
+ parameters: TSchema;
42
+ execute: (toolCallId: string, params: Record<string, unknown>) => Promise<{
43
+ content: {
44
+ type: 'text';
45
+ text: string;
46
+ }[];
47
+ details: unknown;
48
+ }>;
49
+ }
50
+ declare const plugin: {
51
+ id: string;
52
+ name: string;
53
+ description: string;
54
+ version: string;
55
+ activate(api: OpenClawPluginApi): void;
56
+ deactivate(api: OpenClawPluginApi): void;
57
+ };
58
+ //#endregion
59
+ export { plugin as default };
package/package.json CHANGED
@@ -1,18 +1,20 @@
1
1
  {
2
2
  "name": "@alfe.ai/openclaw-voice",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "OpenClaw voice plugin for Alfe — Discord audio, Twilio, Recall.ai",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "import": "./dist/index.js",
11
- "types": "./dist/index.d.ts"
10
+ "types": "./dist/index.d.ts",
11
+ "require": "./dist/index.cjs",
12
+ "import": "./dist/index.js"
12
13
  },
13
14
  "./plugin": {
14
- "import": "./dist/plugin.js",
15
- "types": "./dist/plugin.d.ts"
15
+ "types": "./dist/plugin.d.ts",
16
+ "require": "./dist/plugin.cjs",
17
+ "import": "./dist/plugin.js"
16
18
  }
17
19
  },
18
20
  "openclaw": {
@@ -26,7 +28,7 @@
26
28
  ],
27
29
  "dependencies": {
28
30
  "@sinclair/typebox": "^0.34.48",
29
- "@alfe.ai/config": "0.0.6"
31
+ "@alfe.ai/config": "0.0.7"
30
32
  },
31
33
  "scripts": {
32
34
  "build": "tsdown",