@clawvoice/voice-assistant 1.0.0
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/.env.example +125 -0
- package/CHANGELOG.md +112 -0
- package/LICENSE +21 -0
- package/README.md +215 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +272 -0
- package/dist/config.d.ts +42 -0
- package/dist/config.js +182 -0
- package/dist/diagnostics/health.d.ts +14 -0
- package/dist/diagnostics/health.js +182 -0
- package/dist/hooks.d.ts +16 -0
- package/dist/hooks.js +113 -0
- package/dist/inbound/classifier.d.ts +5 -0
- package/dist/inbound/classifier.js +72 -0
- package/dist/inbound/types.d.ts +30 -0
- package/dist/inbound/types.js +2 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +52 -0
- package/dist/routes.d.ts +6 -0
- package/dist/routes.js +89 -0
- package/dist/services/memory-extraction.d.ts +42 -0
- package/dist/services/memory-extraction.js +117 -0
- package/dist/services/post-call.d.ts +56 -0
- package/dist/services/post-call.js +112 -0
- package/dist/services/relay.d.ts +9 -0
- package/dist/services/relay.js +19 -0
- package/dist/services/voice-call.d.ts +61 -0
- package/dist/services/voice-call.js +189 -0
- package/dist/telephony/telnyx.d.ts +12 -0
- package/dist/telephony/telnyx.js +60 -0
- package/dist/telephony/twilio.d.ts +12 -0
- package/dist/telephony/twilio.js +63 -0
- package/dist/telephony/types.d.ts +15 -0
- package/dist/telephony/types.js +2 -0
- package/dist/telephony/util.d.ts +2 -0
- package/dist/telephony/util.js +25 -0
- package/dist/tools.d.ts +5 -0
- package/dist/tools.js +167 -0
- package/dist/voice/bridge.d.ts +47 -0
- package/dist/voice/bridge.js +411 -0
- package/dist/voice/types.d.ts +168 -0
- package/dist/voice/types.js +42 -0
- package/dist/webhooks/verify.d.ts +30 -0
- package/dist/webhooks/verify.js +95 -0
- package/docs/FEATURES.md +36 -0
- package/docs/OPENCLAW_PLUGIN_GUIDE.md +1202 -0
- package/docs/SETUP.md +303 -0
- package/openclaw.plugin.json +137 -0
- package/package.json +37 -0
- package/skills/voice-assistant/SKILL.md +15 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runSetupWizard = runSetupWizard;
|
|
4
|
+
exports.registerCLI = registerCLI;
|
|
5
|
+
const health_1 = require("./diagnostics/health");
|
|
6
|
+
function maskSecret(value) {
|
|
7
|
+
if (!value) {
|
|
8
|
+
return "(not set)";
|
|
9
|
+
}
|
|
10
|
+
if (value.length <= 4) {
|
|
11
|
+
return "****";
|
|
12
|
+
}
|
|
13
|
+
return `${value.slice(0, 4)}...`;
|
|
14
|
+
}
|
|
15
|
+
function normalizeChoice(value, options) {
|
|
16
|
+
const lowered = value.trim().toLowerCase();
|
|
17
|
+
return options.includes(lowered) ? lowered : "";
|
|
18
|
+
}
|
|
19
|
+
async function askNonEmpty(prompter, question) {
|
|
20
|
+
while (true) {
|
|
21
|
+
const answer = (await prompter.ask(question)).trim();
|
|
22
|
+
if (answer.length > 0) {
|
|
23
|
+
return answer;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function askChoice(prompter, question, choices) {
|
|
28
|
+
while (true) {
|
|
29
|
+
const answer = normalizeChoice(await prompter.ask(question), choices);
|
|
30
|
+
if (answer.length > 0) {
|
|
31
|
+
return answer;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function createReadlinePrompter() {
|
|
36
|
+
const readline = require("node:readline/promises");
|
|
37
|
+
const rl = readline.createInterface({
|
|
38
|
+
input: process.stdin,
|
|
39
|
+
output: process.stdout
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
ask: async (question) => rl.question(question),
|
|
43
|
+
close: () => rl.close()
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async function saveConfig(api, values) {
|
|
47
|
+
const configStore = api.config;
|
|
48
|
+
if (typeof configStore.setMany === "function") {
|
|
49
|
+
await configStore.setMany(values);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (typeof configStore.set === "function") {
|
|
53
|
+
const entries = Object.entries(values);
|
|
54
|
+
for (const [key, value] of entries) {
|
|
55
|
+
await configStore.set(key, value);
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
throw new Error("Config store is not writable in this runtime");
|
|
60
|
+
}
|
|
61
|
+
async function runSetupWizard(api, args, prompter = createReadlinePrompter()) {
|
|
62
|
+
const values = {};
|
|
63
|
+
const telephonyProvider = await askChoice(prompter, "Telephony provider (telnyx/twilio): ", ["telnyx", "twilio"]);
|
|
64
|
+
values.telephonyProvider = telephonyProvider;
|
|
65
|
+
if (telephonyProvider === "telnyx") {
|
|
66
|
+
values.telnyxApiKey = await askNonEmpty(prompter, "Telnyx API key: ");
|
|
67
|
+
values.telnyxConnectionId = await askNonEmpty(prompter, "Telnyx connection ID: ");
|
|
68
|
+
values.telnyxPhoneNumber = await askNonEmpty(prompter, "Telnyx phone number (E.164): ");
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
values.twilioAccountSid = await askNonEmpty(prompter, "Twilio Account SID: ");
|
|
72
|
+
values.twilioAuthToken = await askNonEmpty(prompter, "Twilio auth token: ");
|
|
73
|
+
values.twilioPhoneNumber = await askNonEmpty(prompter, "Twilio phone number (E.164): ");
|
|
74
|
+
}
|
|
75
|
+
const voiceProvider = await askChoice(prompter, "Voice provider (deepgram-agent/elevenlabs-conversational): ", ["deepgram-agent", "elevenlabs-conversational"]);
|
|
76
|
+
values.voiceProvider = voiceProvider;
|
|
77
|
+
values.deepgramApiKey = await askNonEmpty(prompter, "Deepgram API key: ");
|
|
78
|
+
if (voiceProvider === "elevenlabs-conversational") {
|
|
79
|
+
values.elevenlabsApiKey = await askNonEmpty(prompter, "ElevenLabs API key: ");
|
|
80
|
+
values.elevenlabsAgentId = await askNonEmpty(prompter, "ElevenLabs agent ID: ");
|
|
81
|
+
}
|
|
82
|
+
await saveConfig(api, values);
|
|
83
|
+
api.log.info("ClawVoice setup complete", {
|
|
84
|
+
telephonyProvider,
|
|
85
|
+
voiceProvider,
|
|
86
|
+
deepgramApiKey: maskSecret(String(values.deepgramApiKey)),
|
|
87
|
+
telnyxApiKey: maskSecret(typeof values.telnyxApiKey === "string" ? values.telnyxApiKey : undefined),
|
|
88
|
+
twilioAccountSid: maskSecret(typeof values.twilioAccountSid === "string" ? values.twilioAccountSid : undefined),
|
|
89
|
+
elevenlabsApiKey: maskSecret(typeof values.elevenlabsApiKey === "string" ? values.elevenlabsApiKey : undefined)
|
|
90
|
+
});
|
|
91
|
+
prompter.close();
|
|
92
|
+
}
|
|
93
|
+
function parseFlag(args, flag) {
|
|
94
|
+
const inline = args.find((a) => a.startsWith(`--${flag}=`));
|
|
95
|
+
if (inline)
|
|
96
|
+
return inline.slice(`--${flag}=`.length).trim() || undefined;
|
|
97
|
+
const idx = args.indexOf(`--${flag}`);
|
|
98
|
+
if (idx >= 0 && typeof args[idx + 1] === "string")
|
|
99
|
+
return args[idx + 1].trim() || undefined;
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
function formatDuration(ms) {
|
|
103
|
+
const seconds = Math.floor(ms / 1000);
|
|
104
|
+
const minutes = Math.floor(seconds / 60);
|
|
105
|
+
const remaining = seconds % 60;
|
|
106
|
+
return minutes > 0 ? `${minutes}m ${remaining}s` : `${seconds}s`;
|
|
107
|
+
}
|
|
108
|
+
function registerCLI(api, config, callService, memoryService) {
|
|
109
|
+
api.cli.register({
|
|
110
|
+
name: "clawvoice setup",
|
|
111
|
+
description: "Set up ClawVoice (configure telephony and voice providers)",
|
|
112
|
+
run: async (args) => {
|
|
113
|
+
await runSetupWizard(api, args);
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
api.cli.register({
|
|
117
|
+
name: "clawvoice call",
|
|
118
|
+
description: "Initiate an outbound phone call",
|
|
119
|
+
run: async (args) => {
|
|
120
|
+
const phoneNumber = args.find((a) => !a.startsWith("--"));
|
|
121
|
+
if (!phoneNumber) {
|
|
122
|
+
api.log.info("Usage: clawvoice call <phone-number> [--greeting \"...\"] [--purpose \"...\"]");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const greeting = parseFlag(args, "greeting");
|
|
126
|
+
const purpose = parseFlag(args, "purpose");
|
|
127
|
+
api.log.info("Initiating call...", { to: phoneNumber });
|
|
128
|
+
try {
|
|
129
|
+
const result = await callService.startCall({ phoneNumber, greeting, purpose });
|
|
130
|
+
api.log.info("Call started", {
|
|
131
|
+
callId: result.callId,
|
|
132
|
+
to: result.to,
|
|
133
|
+
greeting: result.openingGreeting,
|
|
134
|
+
status: result.message,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
api.log.info("Call failed", { error: err instanceof Error ? err.message : String(err) });
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
api.cli.register({
|
|
143
|
+
name: "clawvoice status",
|
|
144
|
+
description: "Show active calls and configuration health diagnostics",
|
|
145
|
+
run: async () => {
|
|
146
|
+
const report = (0, health_1.runDiagnostics)(config);
|
|
147
|
+
api.log.info(`Diagnostics: ${report.overall.toUpperCase()}`, {});
|
|
148
|
+
for (const check of report.checks) {
|
|
149
|
+
const icon = check.status === "pass" ? "✓" : check.status === "warn" ? "⚠" : "✗";
|
|
150
|
+
api.log.info(` ${icon} ${check.name}: ${check.detail}`, {});
|
|
151
|
+
if (check.remediation) {
|
|
152
|
+
api.log.info(` → ${check.remediation}`, {});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const active = callService.getActiveCalls();
|
|
156
|
+
if (active.length > 0) {
|
|
157
|
+
api.log.info(`Active calls: ${active.length}`, {});
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
api.cli.register({
|
|
162
|
+
name: "clawvoice promote",
|
|
163
|
+
description: "Review and promote voice memories to main MEMORY.md",
|
|
164
|
+
run: async (args) => {
|
|
165
|
+
if (!memoryService) {
|
|
166
|
+
api.log.info("Memory extraction service not available.");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const memoryId = args.find((a) => !a.startsWith("--"));
|
|
170
|
+
if (memoryId) {
|
|
171
|
+
const candidate = memoryService.getCandidate(memoryId);
|
|
172
|
+
if (!candidate) {
|
|
173
|
+
api.log.info("Memory candidate not found", { memoryId });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (parseFlag(args, "--yes")) {
|
|
177
|
+
const result = await memoryService.approveAndPromote(memoryId);
|
|
178
|
+
api.log.info(result.promoted ? "Promoted" : `Failed: ${result.reason}`, { memoryId });
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
api.log.info(`[${candidate.status}] ${candidate.category}: "${candidate.content}" (confidence: ${candidate.confidence})`);
|
|
182
|
+
api.log.info("Run again with --yes to promote.");
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const pending = memoryService.getPendingCandidates();
|
|
187
|
+
if (pending.length === 0) {
|
|
188
|
+
api.log.info("No pending memory candidates.");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
api.log.info(`${pending.length} pending memory candidate(s):`);
|
|
192
|
+
for (const c of pending) {
|
|
193
|
+
api.log.info(` ${c.id}: [${c.category}] "${c.content}" (confidence: ${c.confidence})`);
|
|
194
|
+
}
|
|
195
|
+
api.log.info("Run `clawvoice promote <memoryId> --yes` to promote.");
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
api.cli.register({
|
|
199
|
+
name: "clawvoice history",
|
|
200
|
+
description: "Show recent call history",
|
|
201
|
+
run: async (args) => {
|
|
202
|
+
const callId = args.find((a) => !a.startsWith("--"));
|
|
203
|
+
if (callId) {
|
|
204
|
+
const summary = callService.getCallSummary(callId);
|
|
205
|
+
if (!summary) {
|
|
206
|
+
api.log.info("No summary found for call", { callId });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const transcript = summary.transcriptLength > 0
|
|
210
|
+
? `${summary.transcriptLength} transcript entries`
|
|
211
|
+
: "No transcript";
|
|
212
|
+
api.log.info("Call detail", {
|
|
213
|
+
callId: summary.callId,
|
|
214
|
+
outcome: summary.outcome,
|
|
215
|
+
duration: formatDuration(summary.durationMs),
|
|
216
|
+
transcript,
|
|
217
|
+
failures: summary.failures.length > 0
|
|
218
|
+
? summary.failures.map((f) => `${f.type}: ${f.description}`).join("; ")
|
|
219
|
+
: "none",
|
|
220
|
+
pendingActions: summary.pendingActions.length > 0
|
|
221
|
+
? summary.pendingActions.join(", ")
|
|
222
|
+
: "none",
|
|
223
|
+
retryContext: summary.retryContext
|
|
224
|
+
? summary.retryContext.suggestedApproach
|
|
225
|
+
: "none",
|
|
226
|
+
});
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const active = callService.getActiveCalls();
|
|
230
|
+
if (active.length === 0) {
|
|
231
|
+
api.log.info("No recent calls.");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
for (const call of active) {
|
|
235
|
+
api.log.info("Call", {
|
|
236
|
+
callId: call.callId,
|
|
237
|
+
to: call.to,
|
|
238
|
+
provider: call.provider,
|
|
239
|
+
status: call.status,
|
|
240
|
+
started: call.startedAt,
|
|
241
|
+
ended: call.endedAt ?? "ongoing",
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
api.cli.register({
|
|
247
|
+
name: "clawvoice test",
|
|
248
|
+
description: "Test voice pipeline connectivity and provider readiness",
|
|
249
|
+
run: async () => {
|
|
250
|
+
const report = (0, health_1.runDiagnostics)(config);
|
|
251
|
+
const failures = report.checks.filter((c) => c.status === "fail");
|
|
252
|
+
if (failures.length > 0) {
|
|
253
|
+
api.log.info("Connectivity test FAILED — fix these issues first:", {});
|
|
254
|
+
for (const f of failures) {
|
|
255
|
+
api.log.info(` ✗ ${f.name}: ${f.detail}`, {});
|
|
256
|
+
if (f.remediation) {
|
|
257
|
+
api.log.info(` → ${f.remediation}`, {});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
api.log.info("Connectivity test PASSED — all providers configured.", {});
|
|
263
|
+
const warnings = report.checks.filter((c) => c.status === "warn");
|
|
264
|
+
if (warnings.length > 0) {
|
|
265
|
+
api.log.info("Warnings:", {});
|
|
266
|
+
for (const w of warnings) {
|
|
267
|
+
api.log.info(` ⚠ ${w.name}: ${w.detail}`, {});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type TelephonyProvider = "telnyx" | "twilio";
|
|
2
|
+
export type VoiceProvider = "deepgram-agent" | "elevenlabs-conversational";
|
|
3
|
+
export type MainMemoryAccess = "read" | "none";
|
|
4
|
+
export interface ClawVoiceConfig {
|
|
5
|
+
telephonyProvider: TelephonyProvider;
|
|
6
|
+
voiceProvider: VoiceProvider;
|
|
7
|
+
voiceSystemPrompt: string;
|
|
8
|
+
inboundEnabled: boolean;
|
|
9
|
+
telnyxApiKey?: string;
|
|
10
|
+
telnyxConnectionId?: string;
|
|
11
|
+
telnyxPhoneNumber?: string;
|
|
12
|
+
telnyxWebhookSecret?: string;
|
|
13
|
+
twilioAccountSid?: string;
|
|
14
|
+
twilioAuthToken?: string;
|
|
15
|
+
twilioPhoneNumber?: string;
|
|
16
|
+
deepgramApiKey?: string;
|
|
17
|
+
deepgramVoice: string;
|
|
18
|
+
elevenlabsApiKey?: string;
|
|
19
|
+
elevenlabsAgentId?: string;
|
|
20
|
+
elevenlabsVoiceId?: string;
|
|
21
|
+
openaiApiKey?: string;
|
|
22
|
+
analysisModel: string;
|
|
23
|
+
mainMemoryAccess: MainMemoryAccess;
|
|
24
|
+
autoExtractMemories: boolean;
|
|
25
|
+
maxCallDuration: number;
|
|
26
|
+
disclosureEnabled: boolean;
|
|
27
|
+
disclosureStatement: string;
|
|
28
|
+
dailyCallLimit: number;
|
|
29
|
+
recordCalls: boolean;
|
|
30
|
+
amdEnabled: boolean;
|
|
31
|
+
restrictTools: boolean;
|
|
32
|
+
deniedTools: string[];
|
|
33
|
+
notifyTelegram: boolean;
|
|
34
|
+
notifyDiscord: boolean;
|
|
35
|
+
notifySlack: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface ValidationResult {
|
|
38
|
+
ok: boolean;
|
|
39
|
+
errors: string[];
|
|
40
|
+
}
|
|
41
|
+
export declare function resolveConfig(pluginConfig?: Record<string, unknown>, env?: NodeJS.ProcessEnv): ClawVoiceConfig;
|
|
42
|
+
export declare function validateConfig(config: ClawVoiceConfig): ValidationResult;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveConfig = resolveConfig;
|
|
4
|
+
exports.validateConfig = validateConfig;
|
|
5
|
+
const DEFAULT_CONFIG = {
|
|
6
|
+
telephonyProvider: "twilio",
|
|
7
|
+
voiceProvider: "deepgram-agent",
|
|
8
|
+
voiceSystemPrompt: "",
|
|
9
|
+
inboundEnabled: true,
|
|
10
|
+
deepgramVoice: "aura-asteria-en",
|
|
11
|
+
analysisModel: "gpt-4o-mini",
|
|
12
|
+
mainMemoryAccess: "read",
|
|
13
|
+
autoExtractMemories: true,
|
|
14
|
+
maxCallDuration: 1800,
|
|
15
|
+
disclosureEnabled: true,
|
|
16
|
+
disclosureStatement: "Hello, this call is from an AI assistant calling on behalf of a user.",
|
|
17
|
+
dailyCallLimit: 50,
|
|
18
|
+
recordCalls: false,
|
|
19
|
+
amdEnabled: true,
|
|
20
|
+
restrictTools: true,
|
|
21
|
+
deniedTools: [
|
|
22
|
+
"exec",
|
|
23
|
+
"browser",
|
|
24
|
+
"web_fetch",
|
|
25
|
+
"gateway",
|
|
26
|
+
"cron",
|
|
27
|
+
"sessions_spawn"
|
|
28
|
+
],
|
|
29
|
+
notifyTelegram: false,
|
|
30
|
+
notifyDiscord: false,
|
|
31
|
+
notifySlack: false,
|
|
32
|
+
};
|
|
33
|
+
function parseBoolean(value, fallback) {
|
|
34
|
+
if (typeof value === "boolean") {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
if (typeof value === "string") {
|
|
38
|
+
const normalized = value.trim().toLowerCase();
|
|
39
|
+
if (normalized === "true") {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if (normalized === "false") {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return fallback;
|
|
47
|
+
}
|
|
48
|
+
function parseMainMemoryAccess(value) {
|
|
49
|
+
return value === "read" || value === "none" ? value : undefined;
|
|
50
|
+
}
|
|
51
|
+
function parseNumber(value, fallback) {
|
|
52
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
if (typeof value === "string") {
|
|
56
|
+
const parsed = Number.parseInt(value, 10);
|
|
57
|
+
if (Number.isFinite(parsed)) {
|
|
58
|
+
return parsed;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return fallback;
|
|
62
|
+
}
|
|
63
|
+
function parseStringArray(value, fallback) {
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
const filtered = value.filter((entry) => typeof entry === "string");
|
|
66
|
+
return filtered.length > 0 ? filtered : fallback;
|
|
67
|
+
}
|
|
68
|
+
if (typeof value === "string") {
|
|
69
|
+
const items = value
|
|
70
|
+
.split(",")
|
|
71
|
+
.map((item) => item.trim())
|
|
72
|
+
.filter((item) => item.length > 0);
|
|
73
|
+
return items.length > 0 ? items : fallback;
|
|
74
|
+
}
|
|
75
|
+
return fallback;
|
|
76
|
+
}
|
|
77
|
+
function envString(env, key) {
|
|
78
|
+
const value = env[key];
|
|
79
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
80
|
+
}
|
|
81
|
+
function getValue(envValue, configValue, fallback) {
|
|
82
|
+
if (typeof envValue !== "undefined") {
|
|
83
|
+
return envValue;
|
|
84
|
+
}
|
|
85
|
+
if (typeof configValue !== "undefined") {
|
|
86
|
+
return configValue;
|
|
87
|
+
}
|
|
88
|
+
return fallback;
|
|
89
|
+
}
|
|
90
|
+
function parseTelephonyProvider(value) {
|
|
91
|
+
return value === "telnyx" || value === "twilio" ? value : undefined;
|
|
92
|
+
}
|
|
93
|
+
function parseVoiceProvider(value) {
|
|
94
|
+
return value === "deepgram-agent" || value === "elevenlabs-conversational" ? value : undefined;
|
|
95
|
+
}
|
|
96
|
+
function resolveConfig(pluginConfig = {}, env = process.env) {
|
|
97
|
+
const envTelephony = parseTelephonyProvider(envString(env, "CLAWVOICE_TELEPHONY_PROVIDER"));
|
|
98
|
+
const envVoice = parseVoiceProvider(envString(env, "CLAWVOICE_VOICE_PROVIDER"));
|
|
99
|
+
const envTelnyxApiKey = envString(env, "TELNYX_API_KEY");
|
|
100
|
+
const envTelnyxConnectionId = envString(env, "TELNYX_CONNECTION_ID");
|
|
101
|
+
const envTelnyxPhoneNumber = envString(env, "TELNYX_PHONE_NUMBER");
|
|
102
|
+
const envTelnyxWebhookSecret = envString(env, "TELNYX_WEBHOOK_SECRET");
|
|
103
|
+
const envTwilioAccountSid = envString(env, "TWILIO_ACCOUNT_SID");
|
|
104
|
+
const envTwilioAuthToken = envString(env, "TWILIO_AUTH_TOKEN");
|
|
105
|
+
const envTwilioPhoneNumber = envString(env, "TWILIO_PHONE_NUMBER");
|
|
106
|
+
const envDeepgramApiKey = envString(env, "DEEPGRAM_API_KEY");
|
|
107
|
+
const envDeepgramVoice = envString(env, "CLAWVOICE_DEEPGRAM_VOICE");
|
|
108
|
+
const envElevenlabsApiKey = envString(env, "ELEVENLABS_API_KEY");
|
|
109
|
+
const envElevenlabsAgentId = envString(env, "ELEVENLABS_AGENT_ID");
|
|
110
|
+
const envElevenlabsVoiceId = envString(env, "ELEVENLABS_VOICE_ID");
|
|
111
|
+
const envOpenaiApiKey = envString(env, "OPENAI_API_KEY");
|
|
112
|
+
const envAnalysisModel = envString(env, "CLAWVOICE_ANALYSIS_MODEL");
|
|
113
|
+
const envMainMemoryAccess = parseMainMemoryAccess(envString(env, "CLAWVOICE_MAIN_MEMORY_ACCESS"));
|
|
114
|
+
const envAutoExtractMemories = envString(env, "CLAWVOICE_AUTO_EXTRACT_MEMORIES");
|
|
115
|
+
const envMaxCallDuration = envString(env, "CLAWVOICE_MAX_CALL_DURATION");
|
|
116
|
+
const envRecordCalls = envString(env, "CLAWVOICE_RECORD_CALLS");
|
|
117
|
+
const envDisclosureEnabled = envString(env, "CLAWVOICE_DISCLOSURE_ENABLED");
|
|
118
|
+
const envDisclosureStatement = envString(env, "CLAWVOICE_DISCLOSURE_STATEMENT");
|
|
119
|
+
const envAmdEnabled = envString(env, "CLAWVOICE_AMD_ENABLED");
|
|
120
|
+
const envRestrictTools = envString(env, "CLAWVOICE_RESTRICT_TOOLS");
|
|
121
|
+
const envDeniedTools = envString(env, "CLAWVOICE_DENIED_TOOLS");
|
|
122
|
+
const envVoiceSystemPrompt = envString(env, "CLAWVOICE_VOICE_SYSTEM_PROMPT");
|
|
123
|
+
const envInboundEnabled = envString(env, "CLAWVOICE_INBOUND_ENABLED");
|
|
124
|
+
const envDailyCallLimit = envString(env, "CLAWVOICE_DAILY_CALL_LIMIT");
|
|
125
|
+
const configTelephony = parseTelephonyProvider(pluginConfig.telephonyProvider);
|
|
126
|
+
const configVoice = parseVoiceProvider(pluginConfig.voiceProvider);
|
|
127
|
+
const configMainMemoryAccess = parseMainMemoryAccess(pluginConfig.mainMemoryAccess);
|
|
128
|
+
return {
|
|
129
|
+
telephonyProvider: getValue(envTelephony, configTelephony, DEFAULT_CONFIG.telephonyProvider),
|
|
130
|
+
voiceProvider: getValue(envVoice, configVoice, DEFAULT_CONFIG.voiceProvider),
|
|
131
|
+
telnyxApiKey: getValue(envTelnyxApiKey, typeof pluginConfig.telnyxApiKey === "string" ? pluginConfig.telnyxApiKey : undefined, undefined),
|
|
132
|
+
telnyxConnectionId: getValue(envTelnyxConnectionId, typeof pluginConfig.telnyxConnectionId === "string" ? pluginConfig.telnyxConnectionId : undefined, undefined),
|
|
133
|
+
telnyxPhoneNumber: getValue(envTelnyxPhoneNumber, typeof pluginConfig.telnyxPhoneNumber === "string" ? pluginConfig.telnyxPhoneNumber : undefined, undefined),
|
|
134
|
+
telnyxWebhookSecret: getValue(envTelnyxWebhookSecret, typeof pluginConfig.telnyxWebhookSecret === "string" ? pluginConfig.telnyxWebhookSecret : undefined, undefined),
|
|
135
|
+
twilioAccountSid: getValue(envTwilioAccountSid, typeof pluginConfig.twilioAccountSid === "string" ? pluginConfig.twilioAccountSid : undefined, undefined),
|
|
136
|
+
twilioAuthToken: getValue(envTwilioAuthToken, typeof pluginConfig.twilioAuthToken === "string" ? pluginConfig.twilioAuthToken : undefined, undefined),
|
|
137
|
+
twilioPhoneNumber: getValue(envTwilioPhoneNumber, typeof pluginConfig.twilioPhoneNumber === "string" ? pluginConfig.twilioPhoneNumber : undefined, undefined),
|
|
138
|
+
deepgramApiKey: getValue(envDeepgramApiKey, typeof pluginConfig.deepgramApiKey === "string" ? pluginConfig.deepgramApiKey : undefined, undefined),
|
|
139
|
+
deepgramVoice: getValue(envDeepgramVoice, typeof pluginConfig.deepgramVoice === "string" ? pluginConfig.deepgramVoice : undefined, DEFAULT_CONFIG.deepgramVoice),
|
|
140
|
+
elevenlabsApiKey: getValue(envElevenlabsApiKey, typeof pluginConfig.elevenlabsApiKey === "string" ? pluginConfig.elevenlabsApiKey : undefined, undefined),
|
|
141
|
+
elevenlabsAgentId: getValue(envElevenlabsAgentId, typeof pluginConfig.elevenlabsAgentId === "string" ? pluginConfig.elevenlabsAgentId : undefined, undefined),
|
|
142
|
+
elevenlabsVoiceId: getValue(envElevenlabsVoiceId, typeof pluginConfig.elevenlabsVoiceId === "string" ? pluginConfig.elevenlabsVoiceId : undefined, undefined),
|
|
143
|
+
openaiApiKey: getValue(envOpenaiApiKey, typeof pluginConfig.openaiApiKey === "string" ? pluginConfig.openaiApiKey : undefined, undefined),
|
|
144
|
+
analysisModel: getValue(envAnalysisModel, typeof pluginConfig.analysisModel === "string" ? pluginConfig.analysisModel : undefined, DEFAULT_CONFIG.analysisModel),
|
|
145
|
+
mainMemoryAccess: getValue(envMainMemoryAccess, configMainMemoryAccess, DEFAULT_CONFIG.mainMemoryAccess),
|
|
146
|
+
autoExtractMemories: parseBoolean(getValue(envAutoExtractMemories, typeof pluginConfig.autoExtractMemories === "undefined" ? undefined : String(pluginConfig.autoExtractMemories), String(DEFAULT_CONFIG.autoExtractMemories)), DEFAULT_CONFIG.autoExtractMemories),
|
|
147
|
+
maxCallDuration: parseNumber(getValue(envMaxCallDuration, typeof pluginConfig.maxCallDuration === "undefined" ? undefined : String(pluginConfig.maxCallDuration), String(DEFAULT_CONFIG.maxCallDuration)), DEFAULT_CONFIG.maxCallDuration),
|
|
148
|
+
disclosureEnabled: parseBoolean(getValue(envDisclosureEnabled, typeof pluginConfig.disclosureEnabled === "undefined"
|
|
149
|
+
? undefined
|
|
150
|
+
: String(pluginConfig.disclosureEnabled), String(DEFAULT_CONFIG.disclosureEnabled)), DEFAULT_CONFIG.disclosureEnabled),
|
|
151
|
+
disclosureStatement: getValue(envDisclosureStatement, typeof pluginConfig.disclosureStatement === "string"
|
|
152
|
+
? pluginConfig.disclosureStatement
|
|
153
|
+
: undefined, DEFAULT_CONFIG.disclosureStatement),
|
|
154
|
+
dailyCallLimit: parseNumber(getValue(envDailyCallLimit, typeof pluginConfig.dailyCallLimit === "undefined" ? undefined : String(pluginConfig.dailyCallLimit), String(DEFAULT_CONFIG.dailyCallLimit)), DEFAULT_CONFIG.dailyCallLimit),
|
|
155
|
+
recordCalls: parseBoolean(getValue(envRecordCalls, typeof pluginConfig.recordCalls === "undefined" ? undefined : String(pluginConfig.recordCalls), String(DEFAULT_CONFIG.recordCalls)), DEFAULT_CONFIG.recordCalls),
|
|
156
|
+
amdEnabled: parseBoolean(getValue(envAmdEnabled, typeof pluginConfig.amdEnabled === "undefined" ? undefined : String(pluginConfig.amdEnabled), String(DEFAULT_CONFIG.amdEnabled)), DEFAULT_CONFIG.amdEnabled),
|
|
157
|
+
voiceSystemPrompt: getValue(envVoiceSystemPrompt, typeof pluginConfig.voiceSystemPrompt === "string" ? pluginConfig.voiceSystemPrompt : undefined, DEFAULT_CONFIG.voiceSystemPrompt),
|
|
158
|
+
inboundEnabled: parseBoolean(getValue(envInboundEnabled, typeof pluginConfig.inboundEnabled === "undefined" ? undefined : String(pluginConfig.inboundEnabled), String(DEFAULT_CONFIG.inboundEnabled)), DEFAULT_CONFIG.inboundEnabled),
|
|
159
|
+
restrictTools: parseBoolean(getValue(envRestrictTools, typeof pluginConfig.restrictTools === "undefined" ? undefined : String(pluginConfig.restrictTools), String(DEFAULT_CONFIG.restrictTools)), DEFAULT_CONFIG.restrictTools),
|
|
160
|
+
deniedTools: parseStringArray(getValue(envDeniedTools, pluginConfig.deniedTools, DEFAULT_CONFIG.deniedTools), DEFAULT_CONFIG.deniedTools),
|
|
161
|
+
notifyTelegram: parseBoolean(getValue(envString(env, "CLAWVOICE_NOTIFY_TELEGRAM"), typeof pluginConfig.notifyTelegram === "undefined" ? undefined : String(pluginConfig.notifyTelegram), String(DEFAULT_CONFIG.notifyTelegram)), DEFAULT_CONFIG.notifyTelegram),
|
|
162
|
+
notifyDiscord: parseBoolean(getValue(envString(env, "CLAWVOICE_NOTIFY_DISCORD"), typeof pluginConfig.notifyDiscord === "undefined" ? undefined : String(pluginConfig.notifyDiscord), String(DEFAULT_CONFIG.notifyDiscord)), DEFAULT_CONFIG.notifyDiscord),
|
|
163
|
+
notifySlack: parseBoolean(getValue(envString(env, "CLAWVOICE_NOTIFY_SLACK"), typeof pluginConfig.notifySlack === "undefined" ? undefined : String(pluginConfig.notifySlack), String(DEFAULT_CONFIG.notifySlack)), DEFAULT_CONFIG.notifySlack),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function validateConfig(config) {
|
|
167
|
+
const validationErrors = [];
|
|
168
|
+
if (!Number.isFinite(config.maxCallDuration) || config.maxCallDuration <= 0) {
|
|
169
|
+
validationErrors.push("maxCallDuration must be a positive number of seconds");
|
|
170
|
+
}
|
|
171
|
+
if (config.disclosureEnabled &&
|
|
172
|
+
config.disclosureStatement.trim().length === 0) {
|
|
173
|
+
validationErrors.push("disclosureStatement must be non-empty when disclosureEnabled is true");
|
|
174
|
+
}
|
|
175
|
+
if (validationErrors.length === 0) {
|
|
176
|
+
return { ok: true, errors: [] };
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
errors: validationErrors,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ClawVoiceConfig } from "../config";
|
|
2
|
+
export type CheckStatus = "pass" | "warn" | "fail";
|
|
3
|
+
export interface HealthCheck {
|
|
4
|
+
name: string;
|
|
5
|
+
status: CheckStatus;
|
|
6
|
+
detail: string;
|
|
7
|
+
remediation?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface DiagnosticReport {
|
|
10
|
+
overall: CheckStatus;
|
|
11
|
+
checks: HealthCheck[];
|
|
12
|
+
generatedAt: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function runDiagnostics(config: ClawVoiceConfig): DiagnosticReport;
|