@shogo-ai/voice 1.4.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/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/chunk-D2EVSUZX.js +27 -0
- package/dist/chunk-Q3TJAJ54.js +165 -0
- package/dist/chunk-QHDIEE7O.js +453 -0
- package/dist/chunk-S3S2RNO6.js +671 -0
- package/dist/chunk-ZGYHDC5G.js +423 -0
- package/dist/index.cjs +1316 -0
- package/dist/index.d.cts +432 -0
- package/dist/index.d.ts +432 -0
- package/dist/index.js +735 -0
- package/dist/native/index.cjs +1667 -0
- package/dist/native/index.d.cts +285 -0
- package/dist/native/index.d.ts +285 -0
- package/dist/native/index.js +949 -0
- package/dist/organicParticlesShaders-DIva0vBO.d.cts +693 -0
- package/dist/organicParticlesShaders-DIva0vBO.d.ts +693 -0
- package/dist/react/index.cjs +1670 -0
- package/dist/react/index.d.cts +423 -0
- package/dist/react/index.d.ts +423 -0
- package/dist/react/index.js +952 -0
- package/dist/route/agent.cjs +981 -0
- package/dist/route/agent.d.cts +20 -0
- package/dist/route/agent.d.ts +20 -0
- package/dist/route/agent.js +17 -0
- package/dist/route/audio-tags.cjs +975 -0
- package/dist/route/audio-tags.d.cts +16 -0
- package/dist/route/audio-tags.d.ts +16 -0
- package/dist/route/audio-tags.js +13 -0
- package/dist/route/index.cjs +987 -0
- package/dist/route/index.d.cts +115 -0
- package/dist/route/index.d.ts +115 -0
- package/dist/route/index.js +12 -0
- package/dist/route/signed-url.cjs +975 -0
- package/dist/route/signed-url.d.cts +12 -0
- package/dist/route/signed-url.d.ts +12 -0
- package/dist/route/signed-url.js +13 -0
- package/dist/route/tts-preview.cjs +975 -0
- package/dist/route/tts-preview.d.cts +17 -0
- package/dist/route/tts-preview.d.ts +17 -0
- package/dist/route/tts-preview.js +13 -0
- package/dist/server.cjs +999 -0
- package/dist/server.d.cts +88 -0
- package/dist/server.d.ts +88 -0
- package/dist/server.js +11 -0
- package/dist/types-BKJtOUbE.d.cts +352 -0
- package/dist/types-BKJtOUbE.d.ts +352 -0
- package/package.json +148 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1316 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AUDIO_TAGS: () => AUDIO_TAGS,
|
|
24
|
+
AUDIO_TAG_GROUPS: () => AUDIO_TAG_GROUPS,
|
|
25
|
+
CONVAI_SUPPORTED_TTS_MODELS: () => CONVAI_SUPPORTED_TTS_MODELS,
|
|
26
|
+
CONVAI_TTS_MODEL_FALLBACK: () => CONVAI_TTS_MODEL_FALLBACK,
|
|
27
|
+
DEFAULT_ALLOWED_TAGS: () => DEFAULT_ALLOWED_TAGS,
|
|
28
|
+
DEFAULT_ELEVENLABS_BASE_URL: () => DEFAULT_ELEVENLABS_BASE_URL,
|
|
29
|
+
DEFAULT_MEMORY_BLOCK: () => DEFAULT_MEMORY_BLOCK,
|
|
30
|
+
DEFAULT_TWILIO_BASE_URL: () => DEFAULT_TWILIO_BASE_URL,
|
|
31
|
+
DEFAULT_VOICE_SETTINGS: () => DEFAULT_VOICE_SETTINGS,
|
|
32
|
+
DirectTelephonyClient: () => DirectTelephonyClient,
|
|
33
|
+
EXPRESSIVITY_BLOCK_CLOSE: () => EXPRESSIVITY_BLOCK_CLOSE,
|
|
34
|
+
EXPRESSIVITY_BLOCK_OPEN: () => EXPRESSIVITY_BLOCK_OPEN,
|
|
35
|
+
EXPRESSIVITY_OPTIONS: () => EXPRESSIVITY_OPTIONS,
|
|
36
|
+
ElevenLabsApiError: () => ElevenLabsApiError,
|
|
37
|
+
ElevenLabsClient: () => ElevenLabsClient,
|
|
38
|
+
HostedRuntimeTokenClient: () => HostedRuntimeTokenClient,
|
|
39
|
+
HostedTelephonyClient: () => HostedTelephonyClient,
|
|
40
|
+
MEMORY_CLIENT_TOOLS: () => MEMORY_CLIENT_TOOLS,
|
|
41
|
+
MockTelephonyClient: () => MockTelephonyClient,
|
|
42
|
+
TelephonyApiError: () => TelephonyApiError,
|
|
43
|
+
TelephonyConfigError: () => TelephonyConfigError,
|
|
44
|
+
TwilioApiError: () => TwilioApiError,
|
|
45
|
+
TwilioClient: () => TwilioClient,
|
|
46
|
+
buildPreviewLine: () => buildPreviewLine,
|
|
47
|
+
composeAgentPrompt: () => composeAgentPrompt,
|
|
48
|
+
composeExpressivityBlock: () => composeExpressivityBlock,
|
|
49
|
+
createTelephonyClient: () => createTelephonyClient,
|
|
50
|
+
extractBasePrompt: () => extractBasePrompt,
|
|
51
|
+
isVoiceMockEnv: () => isVoiceMockEnv,
|
|
52
|
+
normalizeAudioTags: () => normalizeAudioTags,
|
|
53
|
+
normalizeExpressivity: () => normalizeExpressivity,
|
|
54
|
+
normalizeVoiceSettings: () => normalizeVoiceSettings,
|
|
55
|
+
readAudioTags: () => readAudioTags,
|
|
56
|
+
readExpressivity: () => readExpressivity,
|
|
57
|
+
readVoiceSettings: () => readVoiceSettings,
|
|
58
|
+
resolveConvaiTtsModel: () => resolveConvaiTtsModel,
|
|
59
|
+
stripAudioTags: () => stripAudioTags,
|
|
60
|
+
stripExpressivityBlock: () => stripExpressivityBlock,
|
|
61
|
+
stripMemoryBlock: () => stripMemoryBlock
|
|
62
|
+
});
|
|
63
|
+
module.exports = __toCommonJS(index_exports);
|
|
64
|
+
|
|
65
|
+
// src/audioTags.ts
|
|
66
|
+
var AUDIO_TAGS = [
|
|
67
|
+
// Emotions
|
|
68
|
+
{ tag: "happy", group: "emotion", label: "Happy", description: "Warm, upbeat delivery.", example: "[happy] Oh, I\u2019m so glad you stopped by." },
|
|
69
|
+
{ tag: "sad", group: "emotion", label: "Sad", description: "Subdued, heavy-hearted tone.", example: "[sad] I wish things had gone differently." },
|
|
70
|
+
{ tag: "excited", group: "emotion", label: "Excited", description: "Energized, eager delivery.", example: "[excited] Wait until you hear this!" },
|
|
71
|
+
{ tag: "curious", group: "emotion", label: "Curious", description: "Inquisitive, rising inflection.", example: "[curious] Hmm, what did they look like?" },
|
|
72
|
+
{ tag: "sarcastic", group: "emotion", label: "Sarcastic", description: "Dry, ironic tone.", example: "[sarcastic] Oh, fantastic. Another Monday." },
|
|
73
|
+
{ tag: "angry", group: "emotion", label: "Angry", description: "Sharp, irritated edge.", example: "[angry] I told you not to touch that." },
|
|
74
|
+
{ tag: "nervous", group: "emotion", label: "Nervous", description: "Uneasy, faltering delivery.", example: "[nervous] I\u2014 I didn\u2019t think you\u2019d actually come." },
|
|
75
|
+
{ tag: "confident", group: "emotion", label: "Confident", description: "Grounded, assured delivery.", example: "[confident] Trust me. I\u2019ve done this before." },
|
|
76
|
+
// Delivery
|
|
77
|
+
{ tag: "whispers", group: "delivery", label: "Whispers", description: "Hushed, close-mic delivery.", example: "[whispers] Don\u2019t say it too loud." },
|
|
78
|
+
{ tag: "shouts", group: "delivery", label: "Shouts", description: "Loud, projected delivery.", example: "[shouts] Look out behind you!" },
|
|
79
|
+
{ tag: "softly", group: "delivery", label: "Softly", description: "Gentle, quiet tone.", example: "[softly] It\u2019s okay. I\u2019m here." },
|
|
80
|
+
{ tag: "slowly", group: "delivery", label: "Slowly", description: "Deliberate pacing.", example: "[slowly] Tell me\u2026 exactly\u2026 what happened." },
|
|
81
|
+
{ tag: "quickly", group: "delivery", label: "Quickly", description: "Rapid, urgent pacing.", example: "[quickly] We need to go, right now." },
|
|
82
|
+
// Reactions (non-verbal sounds)
|
|
83
|
+
{ tag: "laughs", group: "reaction", label: "Laughs", description: "A genuine laugh.", example: "That\u2019s ridiculous [laughs]." },
|
|
84
|
+
{ tag: "chuckles", group: "reaction", label: "Chuckles", description: "A small, warm chuckle.", example: "[chuckles] You always say that." },
|
|
85
|
+
{ tag: "sighs", group: "reaction", label: "Sighs", description: "An audible sigh.", example: "[sighs] Alright, fine. Let\u2019s do it." },
|
|
86
|
+
{ tag: "gasps", group: "reaction", label: "Gasps", description: "A sharp inhale of surprise.", example: "[gasps] You didn\u2019t!" },
|
|
87
|
+
{ tag: "hesitates", group: "reaction", label: "Hesitates", description: "A pause with uncertainty.", example: "[hesitates] I\u2019m not sure I should say." },
|
|
88
|
+
{ tag: "clears throat", group: "reaction", label: "Clears throat", description: "Throat-clearing beat.", example: "[clears throat] As I was saying\u2026" }
|
|
89
|
+
];
|
|
90
|
+
var AUDIO_TAG_GROUPS = [
|
|
91
|
+
{ id: "emotion", label: "Emotions", description: "Overall feeling carried through the line." },
|
|
92
|
+
{ id: "delivery", label: "Delivery", description: "How the words are spoken (pace, volume)." },
|
|
93
|
+
{ id: "reaction", label: "Reactions", description: "Non-verbal sounds mixed into speech." }
|
|
94
|
+
];
|
|
95
|
+
var EXPRESSIVITY_OPTIONS = [
|
|
96
|
+
{ id: "off", label: "Off", description: "No audio tags. Plain, natural delivery." },
|
|
97
|
+
{ id: "subtle", label: "Subtle", description: "Occasional tag when it genuinely fits. Default." },
|
|
98
|
+
{ id: "full", label: "Full", description: "Freely expressive \u2014 tags sprinkled to carry emotion and reactions." }
|
|
99
|
+
];
|
|
100
|
+
var DEFAULT_ALLOWED_TAGS = [
|
|
101
|
+
"whispers",
|
|
102
|
+
"laughs",
|
|
103
|
+
"sighs",
|
|
104
|
+
"excited",
|
|
105
|
+
"curious",
|
|
106
|
+
"softly"
|
|
107
|
+
];
|
|
108
|
+
var DEFAULT_VOICE_SETTINGS = {
|
|
109
|
+
stability: 0.5,
|
|
110
|
+
similarity_boost: 0.8,
|
|
111
|
+
style: 0,
|
|
112
|
+
use_speaker_boost: true
|
|
113
|
+
};
|
|
114
|
+
var KNOWN_TAG_SET = new Set(AUDIO_TAGS.map((t) => t.tag.toLowerCase()));
|
|
115
|
+
function stripAudioTags(text) {
|
|
116
|
+
if (!text) return "";
|
|
117
|
+
return text.replace(/\[([a-z][a-z ]{0,24})\]/gi, (match, inner) => {
|
|
118
|
+
const key = inner.trim().toLowerCase();
|
|
119
|
+
return KNOWN_TAG_SET.has(key) ? "" : match;
|
|
120
|
+
}).replace(/[ \t]{2,}/g, " ").replace(/\s+([,.!?;:])/g, "$1").trim();
|
|
121
|
+
}
|
|
122
|
+
var EXPRESSIVITY_BLOCK_OPEN = "<!--expressivity-->";
|
|
123
|
+
var EXPRESSIVITY_BLOCK_CLOSE = "<!--/expressivity-->";
|
|
124
|
+
function stripExpressivityBlock(prompt) {
|
|
125
|
+
if (!prompt) return prompt;
|
|
126
|
+
const open = EXPRESSIVITY_BLOCK_OPEN;
|
|
127
|
+
const close = EXPRESSIVITY_BLOCK_CLOSE;
|
|
128
|
+
const openIdx = prompt.indexOf(open);
|
|
129
|
+
if (openIdx === -1) return prompt;
|
|
130
|
+
const closeIdx = prompt.indexOf(close, openIdx);
|
|
131
|
+
if (closeIdx === -1) return prompt;
|
|
132
|
+
const before = prompt.slice(0, openIdx).replace(/\s+$/, "");
|
|
133
|
+
const after = prompt.slice(closeIdx + close.length).replace(/^\s+/, "");
|
|
134
|
+
return [before, after].filter(Boolean).join("\n\n");
|
|
135
|
+
}
|
|
136
|
+
function composeExpressivityBlock(expressivity, allowedTags) {
|
|
137
|
+
if (expressivity === "off") return "";
|
|
138
|
+
const allowed = (allowedTags && allowedTags.length > 0 ? allowedTags : DEFAULT_ALLOWED_TAGS).filter((t) => AUDIO_TAGS.some((a) => a.tag === t));
|
|
139
|
+
if (allowed.length === 0) return "";
|
|
140
|
+
const intensity = expressivity === "full" ? "Use audio tags liberally to color your speech with emotion, delivery, and non-verbal reactions. Most substantive replies should include at least one tag. Never add so many tags that a line becomes unnatural." : "Use audio tags sparingly \u2014 only when one genuinely fits the emotion or reaction of the moment. Most lines will have no tag. At most one or two tags per reply.";
|
|
141
|
+
const tagList = allowed.map((t) => {
|
|
142
|
+
const entry = AUDIO_TAGS.find((a) => a.tag === t);
|
|
143
|
+
return ` - [${entry.tag}] \u2014 ${entry.description} Example: ${entry.example}`;
|
|
144
|
+
}).join("\n");
|
|
145
|
+
const body = [
|
|
146
|
+
"You speak through an expressive TTS engine that understands inline audio tags.",
|
|
147
|
+
'Audio tags are lowercase words in square brackets placed directly in your spoken text \u2014 e.g. "[whispers] don\u2019t tell anyone" or "that was wild [laughs]".',
|
|
148
|
+
'Tags are performance cues, not words. Never describe a tag (do not say "asterisk laughs asterisk" or "*laughs*"). Never read a tag aloud. Never put tags inside quoted speech from another character.',
|
|
149
|
+
intensity,
|
|
150
|
+
"You may only use tags from this allow-list:",
|
|
151
|
+
tagList,
|
|
152
|
+
"If no tag fits, use none. Tags are optional flavor, not filler."
|
|
153
|
+
].join("\n");
|
|
154
|
+
return `${EXPRESSIVITY_BLOCK_OPEN}
|
|
155
|
+
${body}
|
|
156
|
+
${EXPRESSIVITY_BLOCK_CLOSE}`;
|
|
157
|
+
}
|
|
158
|
+
function buildPreviewLine(allowedTags, characterName = "your companion") {
|
|
159
|
+
const tags = (allowedTags ?? []).filter((t) => AUDIO_TAGS.some((a) => a.tag === t));
|
|
160
|
+
if (tags.length === 0) {
|
|
161
|
+
return `Hi there. This is a quick preview of how ${characterName} sounds.`;
|
|
162
|
+
}
|
|
163
|
+
const pick = tags.slice(0, 3);
|
|
164
|
+
const lead = pick[0];
|
|
165
|
+
const mid = pick[1];
|
|
166
|
+
const tail = pick[2];
|
|
167
|
+
if (pick.length === 1) {
|
|
168
|
+
return `[${lead}] Hey \u2014 this is how ${characterName} sounds when the mood takes over.`;
|
|
169
|
+
}
|
|
170
|
+
if (pick.length === 2) {
|
|
171
|
+
return `[${lead}] Oh, this is fun. [${mid}] Let me know what you think.`;
|
|
172
|
+
}
|
|
173
|
+
return `[${lead}] Okay, listen. [${mid}] This is the new voice. [${tail}] How does it feel?`;
|
|
174
|
+
}
|
|
175
|
+
function normalizeAudioTags(raw) {
|
|
176
|
+
if (!Array.isArray(raw)) return null;
|
|
177
|
+
const cleaned = raw.filter((x) => typeof x === "string").map((x) => x.trim().toLowerCase()).filter((x) => AUDIO_TAGS.some((a) => a.tag === x));
|
|
178
|
+
return Array.from(new Set(cleaned));
|
|
179
|
+
}
|
|
180
|
+
function normalizeExpressivity(raw) {
|
|
181
|
+
if (raw === "off" || raw === "subtle" || raw === "full") return raw;
|
|
182
|
+
return void 0;
|
|
183
|
+
}
|
|
184
|
+
function normalizeVoiceSettings(raw) {
|
|
185
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
186
|
+
const src = raw;
|
|
187
|
+
const out = {};
|
|
188
|
+
const clamp = (v) => typeof v === "number" && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : void 0;
|
|
189
|
+
const stability = clamp(src.stability);
|
|
190
|
+
const similarity = clamp(src.similarity_boost ?? src.similarityBoost);
|
|
191
|
+
const style = clamp(src.style);
|
|
192
|
+
const boost = src.use_speaker_boost ?? src.useSpeakerBoost;
|
|
193
|
+
if (stability !== void 0) out.stability = stability;
|
|
194
|
+
if (similarity !== void 0) out.similarity_boost = similarity;
|
|
195
|
+
if (style !== void 0) out.style = style;
|
|
196
|
+
if (typeof boost === "boolean") out.use_speaker_boost = boost;
|
|
197
|
+
return Object.keys(out).length ? out : void 0;
|
|
198
|
+
}
|
|
199
|
+
function readAudioTags(raw) {
|
|
200
|
+
const norm = normalizeAudioTags(raw);
|
|
201
|
+
return norm && norm.length > 0 ? norm : DEFAULT_ALLOWED_TAGS;
|
|
202
|
+
}
|
|
203
|
+
function readExpressivity(raw) {
|
|
204
|
+
return normalizeExpressivity(raw) ?? "subtle";
|
|
205
|
+
}
|
|
206
|
+
function readVoiceSettings(raw) {
|
|
207
|
+
return normalizeVoiceSettings(raw) ?? { ...DEFAULT_VOICE_SETTINGS };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/prompt.ts
|
|
211
|
+
var DEFAULT_MEMORY_BLOCK = `# Memory
|
|
212
|
+
|
|
213
|
+
Known context about this user from past conversations:
|
|
214
|
+
{{user_context}}
|
|
215
|
+
|
|
216
|
+
Memory retrieval is automatic \u2014 the top matching facts are already injected above. Never say "let me check my memory" or "searching memories."
|
|
217
|
+
|
|
218
|
+
You have one tool:
|
|
219
|
+
- add_memory(fact): save a concise, canonical fact about the user \u2014 preferences, decisions, personal details, follow-ups. Use short normalized phrasings like "favorite color: green" or "lives in Honolulu". Always call it when the user explicitly asks you to remember something.`;
|
|
220
|
+
function stripMemoryBlock(prompt) {
|
|
221
|
+
if (!prompt) return prompt;
|
|
222
|
+
return prompt.replace(/\n*<!-- zix-memory-v\d+ -->[\s\S]*$/m, "").replace(/\n*# Memory\s*\n\s*Known context about this user[\s\S]*$/m, "").trimEnd();
|
|
223
|
+
}
|
|
224
|
+
function extractBasePrompt(prompt) {
|
|
225
|
+
return stripExpressivityBlock(stripMemoryBlock(prompt));
|
|
226
|
+
}
|
|
227
|
+
function composeAgentPrompt(basePrompt, options = {}) {
|
|
228
|
+
const { expressivity = "subtle", audioTags = null, memoryBlock = DEFAULT_MEMORY_BLOCK } = options;
|
|
229
|
+
const stripped = extractBasePrompt(basePrompt);
|
|
230
|
+
const expr = composeExpressivityBlock(expressivity, audioTags);
|
|
231
|
+
const parts = [stripped];
|
|
232
|
+
if (expr) parts.push(expr);
|
|
233
|
+
if (memoryBlock) parts.push(memoryBlock);
|
|
234
|
+
return parts.join("\n\n");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/elevenlabs.ts
|
|
238
|
+
var DEFAULT_ELEVENLABS_BASE_URL = "https://api.elevenlabs.io";
|
|
239
|
+
var CONVAI_SUPPORTED_TTS_MODELS = /* @__PURE__ */ new Set([
|
|
240
|
+
"eleven_turbo_v2",
|
|
241
|
+
"eleven_turbo_v2_5",
|
|
242
|
+
"eleven_flash_v2",
|
|
243
|
+
"eleven_flash_v2_5",
|
|
244
|
+
"eleven_multilingual_v2"
|
|
245
|
+
]);
|
|
246
|
+
var CONVAI_TTS_MODEL_FALLBACK = "eleven_turbo_v2_5";
|
|
247
|
+
function resolveConvaiTtsModel(requested) {
|
|
248
|
+
if (requested && CONVAI_SUPPORTED_TTS_MODELS.has(requested)) return requested;
|
|
249
|
+
return CONVAI_TTS_MODEL_FALLBACK;
|
|
250
|
+
}
|
|
251
|
+
var ElevenLabsApiError = class extends Error {
|
|
252
|
+
constructor(message, status, body) {
|
|
253
|
+
super(message);
|
|
254
|
+
this.status = status;
|
|
255
|
+
this.body = body;
|
|
256
|
+
this.name = "ElevenLabsApiError";
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
var ElevenLabsClient = class {
|
|
260
|
+
constructor(config) {
|
|
261
|
+
this.config = config;
|
|
262
|
+
if (!config.apiKey) throw new Error("ElevenLabsClient: apiKey is required");
|
|
263
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_ELEVENLABS_BASE_URL).replace(/\/+$/, "");
|
|
264
|
+
this.fetchImpl = config.fetch ?? globalThis.fetch;
|
|
265
|
+
if (typeof this.fetchImpl !== "function") {
|
|
266
|
+
throw new Error("ElevenLabsClient: global fetch is unavailable; pass config.fetch");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
headers(extra = {}) {
|
|
270
|
+
return {
|
|
271
|
+
"xi-api-key": this.config.apiKey,
|
|
272
|
+
...extra
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/** Create a new convai agent. Returns the new `agent_id`. */
|
|
276
|
+
async createAgent(params) {
|
|
277
|
+
const prompt = composeAgentPrompt(params.systemPrompt, {
|
|
278
|
+
expressivity: params.expressivity ?? "subtle",
|
|
279
|
+
audioTags: params.audioTags ?? null,
|
|
280
|
+
memoryBlock: params.memoryBlock
|
|
281
|
+
});
|
|
282
|
+
const ttsConfig = {
|
|
283
|
+
voice_id: params.voiceId,
|
|
284
|
+
model_id: resolveConvaiTtsModel(params.ttsModelId)
|
|
285
|
+
};
|
|
286
|
+
if (params.voiceSettings) ttsConfig.voice_settings = params.voiceSettings;
|
|
287
|
+
const body = {
|
|
288
|
+
name: `Companion-${params.characterName}-for-${params.displayName}`,
|
|
289
|
+
conversation_config: {
|
|
290
|
+
agent: {
|
|
291
|
+
prompt: {
|
|
292
|
+
prompt,
|
|
293
|
+
...params.tools ? { tools: params.tools } : {}
|
|
294
|
+
},
|
|
295
|
+
first_message: params.firstMessage,
|
|
296
|
+
language: params.language ?? "en"
|
|
297
|
+
},
|
|
298
|
+
tts: ttsConfig
|
|
299
|
+
},
|
|
300
|
+
platform_settings: {
|
|
301
|
+
overrides: {
|
|
302
|
+
conversation_config_override: {
|
|
303
|
+
agent: { prompt: { prompt: true }, first_message: true, language: true },
|
|
304
|
+
tts: { voice_id: true },
|
|
305
|
+
conversation: { text_only: true }
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
dynamic_variables: {
|
|
309
|
+
dynamic_variable_placeholders: {
|
|
310
|
+
user_context: "No prior memories yet.",
|
|
311
|
+
character_name: "",
|
|
312
|
+
...params.dynamicVariablePlaceholders ?? {}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
const res = await this.fetchImpl(`${this.baseUrl}/v1/convai/agents/create`, {
|
|
318
|
+
method: "POST",
|
|
319
|
+
headers: this.headers({ "content-type": "application/json" }),
|
|
320
|
+
body: JSON.stringify(body)
|
|
321
|
+
});
|
|
322
|
+
if (!res.ok) {
|
|
323
|
+
const text = await res.text();
|
|
324
|
+
throw new ElevenLabsApiError(`createAgent failed: ${res.status}`, res.status, text);
|
|
325
|
+
}
|
|
326
|
+
const data = await res.json();
|
|
327
|
+
return data.agent_id;
|
|
328
|
+
}
|
|
329
|
+
/** PATCH any subset of agent settings. Idempotent; only sends the keys you supply. */
|
|
330
|
+
async patchAgent(agentId, params) {
|
|
331
|
+
const body = {};
|
|
332
|
+
if (params.displayName !== void 0 || params.characterName !== void 0) {
|
|
333
|
+
body.name = `Companion-${params.characterName ?? ""}-for-${params.displayName ?? ""}`.replace(
|
|
334
|
+
/-for-$/,
|
|
335
|
+
""
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
const conv = {};
|
|
339
|
+
const agent = {};
|
|
340
|
+
const prompt = {};
|
|
341
|
+
if (params.systemPrompt !== void 0) {
|
|
342
|
+
prompt.prompt = composeAgentPrompt(params.systemPrompt, {
|
|
343
|
+
expressivity: params.expressivity ?? "subtle",
|
|
344
|
+
audioTags: params.audioTags ?? null,
|
|
345
|
+
memoryBlock: params.memoryBlock
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
if (params.tools) prompt.tools = params.tools;
|
|
349
|
+
if (Object.keys(prompt).length) agent.prompt = prompt;
|
|
350
|
+
if (params.firstMessage !== void 0) agent.first_message = params.firstMessage;
|
|
351
|
+
if (Object.keys(agent).length) conv.agent = agent;
|
|
352
|
+
const tts = {};
|
|
353
|
+
if (params.voiceId !== void 0) tts.voice_id = params.voiceId;
|
|
354
|
+
if (params.voiceSettings) tts.voice_settings = params.voiceSettings;
|
|
355
|
+
if (params.ttsModelId !== void 0) tts.model_id = resolveConvaiTtsModel(params.ttsModelId);
|
|
356
|
+
if (Object.keys(tts).length) conv.tts = tts;
|
|
357
|
+
if (Object.keys(conv).length) body.conversation_config = conv;
|
|
358
|
+
if (params.enableUserContextOverride) {
|
|
359
|
+
body.platform_settings = {
|
|
360
|
+
overrides: {
|
|
361
|
+
conversation_config_override: {
|
|
362
|
+
agent: { prompt: { prompt: true }, first_message: true, language: true },
|
|
363
|
+
tts: { voice_id: true },
|
|
364
|
+
conversation: { text_only: true }
|
|
365
|
+
},
|
|
366
|
+
custom_llm_extra_body: false,
|
|
367
|
+
enable_conversation_initiation_client_data_from_webhook: false
|
|
368
|
+
},
|
|
369
|
+
dynamic_variables: {
|
|
370
|
+
dynamic_variable_placeholders: {
|
|
371
|
+
user_context: "No prior memories yet.",
|
|
372
|
+
character_name: ""
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
if (!Object.keys(body).length) return;
|
|
378
|
+
const res = await this.fetchImpl(`${this.baseUrl}/v1/convai/agents/${encodeURIComponent(agentId)}`, {
|
|
379
|
+
method: "PATCH",
|
|
380
|
+
headers: this.headers({ "content-type": "application/json" }),
|
|
381
|
+
body: JSON.stringify(body)
|
|
382
|
+
});
|
|
383
|
+
if (!res.ok) {
|
|
384
|
+
const text = await res.text();
|
|
385
|
+
throw new ElevenLabsApiError(`patchAgent failed: ${res.status}`, res.status, text);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Delete a convai agent. Best-effort: swallow errors so teardown during a
|
|
390
|
+
* 500 response doesn't cascade into a second failure.
|
|
391
|
+
*/
|
|
392
|
+
async deleteAgent(agentId) {
|
|
393
|
+
try {
|
|
394
|
+
await this.fetchImpl(`${this.baseUrl}/v1/convai/agents/${encodeURIComponent(agentId)}`, {
|
|
395
|
+
method: "DELETE",
|
|
396
|
+
headers: this.headers()
|
|
397
|
+
});
|
|
398
|
+
} catch {
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/** Fetch a short-lived signed URL the browser can use to start a session. */
|
|
402
|
+
async getSignedUrl(agentId) {
|
|
403
|
+
const res = await this.fetchImpl(
|
|
404
|
+
`${this.baseUrl}/v1/convai/conversation/get-signed-url?agent_id=${encodeURIComponent(agentId)}`,
|
|
405
|
+
{ headers: this.headers() }
|
|
406
|
+
);
|
|
407
|
+
if (!res.ok) {
|
|
408
|
+
const text = await res.text();
|
|
409
|
+
throw new ElevenLabsApiError(`getSignedUrl failed: ${res.status}`, res.status, text);
|
|
410
|
+
}
|
|
411
|
+
const data = await res.json();
|
|
412
|
+
return data.signed_url;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* One-shot TTS. Returns the raw audio bytes plus the model id that was
|
|
416
|
+
* actually used (some models aren't available for every account, so we
|
|
417
|
+
* fall back to `eleven_turbo_v2_5` on the first failure).
|
|
418
|
+
*/
|
|
419
|
+
async textToSpeech(params) {
|
|
420
|
+
const modelId = params.modelId && params.modelId.length > 0 ? params.modelId : "eleven_v3";
|
|
421
|
+
const accept = params.accept ?? "audio/mpeg";
|
|
422
|
+
const attempt = async (m) => {
|
|
423
|
+
const res2 = await this.fetchImpl(
|
|
424
|
+
`${this.baseUrl}/v1/text-to-speech/${encodeURIComponent(params.voiceId)}`,
|
|
425
|
+
{
|
|
426
|
+
method: "POST",
|
|
427
|
+
headers: this.headers({ "content-type": "application/json", accept }),
|
|
428
|
+
body: JSON.stringify({
|
|
429
|
+
text: params.text,
|
|
430
|
+
model_id: m,
|
|
431
|
+
voice_settings: params.voiceSettings
|
|
432
|
+
})
|
|
433
|
+
}
|
|
434
|
+
);
|
|
435
|
+
return res2;
|
|
436
|
+
};
|
|
437
|
+
let res = await attempt(modelId);
|
|
438
|
+
let used = modelId;
|
|
439
|
+
if (!res.ok && modelId !== "eleven_turbo_v2_5") {
|
|
440
|
+
res = await attempt("eleven_turbo_v2_5");
|
|
441
|
+
used = "eleven_turbo_v2_5";
|
|
442
|
+
}
|
|
443
|
+
if (!res.ok) {
|
|
444
|
+
const text = await res.text();
|
|
445
|
+
throw new ElevenLabsApiError(`textToSpeech failed: ${res.status}`, res.status, text);
|
|
446
|
+
}
|
|
447
|
+
const audio = await res.arrayBuffer();
|
|
448
|
+
const contentType = res.headers.get("content-type") ?? accept;
|
|
449
|
+
return { audio, modelId: used, contentType };
|
|
450
|
+
}
|
|
451
|
+
/** Probe whether a given voice id still exists on the caller's account. */
|
|
452
|
+
async voiceExists(voiceId) {
|
|
453
|
+
const res = await this.fetchImpl(
|
|
454
|
+
`${this.baseUrl}/v1/voices/${encodeURIComponent(voiceId)}`,
|
|
455
|
+
{ headers: this.headers() }
|
|
456
|
+
);
|
|
457
|
+
return res.ok;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Register a Twilio phone number with an ElevenLabs agent. EL becomes
|
|
461
|
+
* the SIP target for inbound calls to that number — Twilio bridges
|
|
462
|
+
* the call via EL's native integration and we never have to run a
|
|
463
|
+
* Media Streams ↔ convai WebSocket bridge ourselves.
|
|
464
|
+
*
|
|
465
|
+
* Returns the ElevenLabs `phone_number_id` (distinct from the Twilio
|
|
466
|
+
* `sid`) which is the handle used by `outboundCall` + `deletePhoneNumber`.
|
|
467
|
+
*/
|
|
468
|
+
async createPhoneNumberTwilio(params) {
|
|
469
|
+
const body = {
|
|
470
|
+
phone_number: params.phoneNumber,
|
|
471
|
+
label: params.label ?? params.phoneNumber,
|
|
472
|
+
provider: "twilio",
|
|
473
|
+
sid: params.twilioAccountSid,
|
|
474
|
+
token: params.twilioAuthToken
|
|
475
|
+
};
|
|
476
|
+
const res = await this.fetchImpl(
|
|
477
|
+
`${this.baseUrl}/v1/convai/phone-numbers`,
|
|
478
|
+
{
|
|
479
|
+
method: "POST",
|
|
480
|
+
headers: this.headers({ "content-type": "application/json" }),
|
|
481
|
+
body: JSON.stringify(body)
|
|
482
|
+
}
|
|
483
|
+
);
|
|
484
|
+
if (!res.ok) {
|
|
485
|
+
const text = await res.text();
|
|
486
|
+
throw new ElevenLabsApiError(
|
|
487
|
+
`createPhoneNumberTwilio failed: ${res.status}`,
|
|
488
|
+
res.status,
|
|
489
|
+
text
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
const data = await res.json();
|
|
493
|
+
const id = data.phone_number_id ?? data.id;
|
|
494
|
+
if (!id) {
|
|
495
|
+
throw new ElevenLabsApiError(
|
|
496
|
+
"createPhoneNumberTwilio returned no id",
|
|
497
|
+
500,
|
|
498
|
+
JSON.stringify(data)
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
const patchRes = await this.fetchImpl(
|
|
502
|
+
`${this.baseUrl}/v1/convai/phone-numbers/${encodeURIComponent(id)}`,
|
|
503
|
+
{
|
|
504
|
+
method: "PATCH",
|
|
505
|
+
headers: this.headers({ "content-type": "application/json" }),
|
|
506
|
+
body: JSON.stringify({ agent_id: params.agentId })
|
|
507
|
+
}
|
|
508
|
+
);
|
|
509
|
+
if (!patchRes.ok) {
|
|
510
|
+
const text = await patchRes.text();
|
|
511
|
+
try {
|
|
512
|
+
await this.deletePhoneNumber(id);
|
|
513
|
+
} catch {
|
|
514
|
+
}
|
|
515
|
+
throw new ElevenLabsApiError(
|
|
516
|
+
`assignAgentToPhoneNumber failed: ${patchRes.status}`,
|
|
517
|
+
patchRes.status,
|
|
518
|
+
text
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
return { phoneNumberId: id };
|
|
522
|
+
}
|
|
523
|
+
/** Detach a previously-registered phone number from ElevenLabs. */
|
|
524
|
+
async deletePhoneNumber(phoneNumberId) {
|
|
525
|
+
const res = await this.fetchImpl(
|
|
526
|
+
`${this.baseUrl}/v1/convai/phone-numbers/${encodeURIComponent(phoneNumberId)}`,
|
|
527
|
+
{ method: "DELETE", headers: this.headers() }
|
|
528
|
+
);
|
|
529
|
+
if (!res.ok && res.status !== 404) {
|
|
530
|
+
const text = await res.text();
|
|
531
|
+
throw new ElevenLabsApiError(
|
|
532
|
+
`deletePhoneNumber failed: ${res.status}`,
|
|
533
|
+
res.status,
|
|
534
|
+
text
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Place an outbound PSTN call via a previously-registered EL phone
|
|
540
|
+
* number. EL drives Twilio (or a SIP trunk) under the hood — we just
|
|
541
|
+
* hand it the destination number and the agent to bridge to. Returns
|
|
542
|
+
* the Twilio `callSid` + EL `conversationId` for metering / tracing.
|
|
543
|
+
*
|
|
544
|
+
* As of 2026-Q2 the EL API uses provider-scoped routes:
|
|
545
|
+
* - Twilio numbers → POST /v1/convai/twilio/outbound-call
|
|
546
|
+
* - SIP numbers → POST /v1/convai/sip-trunk/outbound-call
|
|
547
|
+
* and `agent_phone_number_id` is a body field (no longer a path
|
|
548
|
+
* segment). The previous `/v1/convai/phone-numbers/{id}/outbound-call`
|
|
549
|
+
* route now returns 404. Callers that don't know the provider can
|
|
550
|
+
* look it up from `GET /v1/convai/phone-numbers/{id}` (`provider:
|
|
551
|
+
* "twilio" | "sip_trunk"`); most hosted setups are Twilio, so that's
|
|
552
|
+
* the default here.
|
|
553
|
+
*/
|
|
554
|
+
async outboundCall(params) {
|
|
555
|
+
const provider = params.provider ?? "twilio";
|
|
556
|
+
const body = {
|
|
557
|
+
agent_id: params.agentId,
|
|
558
|
+
agent_phone_number_id: params.phoneNumberId,
|
|
559
|
+
to_number: params.toNumber
|
|
560
|
+
};
|
|
561
|
+
if (params.dynamicVariables) {
|
|
562
|
+
body.conversation_initiation_client_data = {
|
|
563
|
+
dynamic_variables: params.dynamicVariables
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
const res = await this.fetchImpl(
|
|
567
|
+
`${this.baseUrl}/v1/convai/${provider}/outbound-call`,
|
|
568
|
+
{
|
|
569
|
+
method: "POST",
|
|
570
|
+
headers: this.headers({ "content-type": "application/json" }),
|
|
571
|
+
body: JSON.stringify(body)
|
|
572
|
+
}
|
|
573
|
+
);
|
|
574
|
+
if (!res.ok) {
|
|
575
|
+
const text = await res.text();
|
|
576
|
+
throw new ElevenLabsApiError(
|
|
577
|
+
`outboundCall failed: ${res.status}`,
|
|
578
|
+
res.status,
|
|
579
|
+
text
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
const data = await res.json();
|
|
583
|
+
const callSid = data.call_sid ?? data.callSid;
|
|
584
|
+
const conversationId = data.conversation_id ?? data.conversationId;
|
|
585
|
+
if (!callSid || !conversationId) {
|
|
586
|
+
throw new ElevenLabsApiError(
|
|
587
|
+
"outboundCall missing callSid or conversationId",
|
|
588
|
+
500,
|
|
589
|
+
JSON.stringify(data)
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
return { callSid, conversationId };
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
var MEMORY_CLIENT_TOOLS = [
|
|
596
|
+
{
|
|
597
|
+
type: "client",
|
|
598
|
+
name: "add_memory",
|
|
599
|
+
description: "Save a concise canonical fact about the user (preferences, decisions, personal details, follow-ups). Use whenever the user shares something worth remembering.",
|
|
600
|
+
expects_response: true,
|
|
601
|
+
parameters: {
|
|
602
|
+
type: "object",
|
|
603
|
+
properties: {
|
|
604
|
+
fact: {
|
|
605
|
+
type: "string",
|
|
606
|
+
description: 'A short normalized fact, e.g. "prefers window seats", "favorite color: green". Max 500 chars.'
|
|
607
|
+
}
|
|
608
|
+
},
|
|
609
|
+
required: ["fact"]
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
];
|
|
613
|
+
|
|
614
|
+
// src/twilio.ts
|
|
615
|
+
var DEFAULT_TWILIO_BASE_URL = "https://api.twilio.com";
|
|
616
|
+
var TwilioApiError = class extends Error {
|
|
617
|
+
constructor(message, status, body) {
|
|
618
|
+
super(message);
|
|
619
|
+
this.status = status;
|
|
620
|
+
this.body = body;
|
|
621
|
+
this.name = "TwilioApiError";
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
function toBase64(s) {
|
|
625
|
+
if (typeof btoa === "function") return btoa(s);
|
|
626
|
+
const nodeBuffer = globalThis.Buffer;
|
|
627
|
+
if (nodeBuffer) return nodeBuffer.from(s, "utf-8").toString("base64");
|
|
628
|
+
throw new Error("twilio.ts: no base64 encoder available in this runtime");
|
|
629
|
+
}
|
|
630
|
+
var TwilioClient = class {
|
|
631
|
+
constructor(config) {
|
|
632
|
+
this.config = config;
|
|
633
|
+
if (!config.accountSid) throw new Error("TwilioClient: accountSid is required");
|
|
634
|
+
if (!config.authToken) throw new Error("TwilioClient: authToken is required");
|
|
635
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_TWILIO_BASE_URL).replace(/\/+$/, "");
|
|
636
|
+
this.fetchImpl = config.fetch ?? globalThis.fetch;
|
|
637
|
+
if (typeof this.fetchImpl !== "function") {
|
|
638
|
+
throw new Error("TwilioClient: global fetch is unavailable; pass config.fetch");
|
|
639
|
+
}
|
|
640
|
+
this.basicAuth = `Basic ${toBase64(`${config.accountSid}:${config.authToken}`)}`;
|
|
641
|
+
}
|
|
642
|
+
accountUrl(path) {
|
|
643
|
+
return `${this.baseUrl}/2010-04-01/Accounts/${encodeURIComponent(
|
|
644
|
+
this.config.accountSid
|
|
645
|
+
)}${path}`;
|
|
646
|
+
}
|
|
647
|
+
headers(extra = {}) {
|
|
648
|
+
return { authorization: this.basicAuth, ...extra };
|
|
649
|
+
}
|
|
650
|
+
async searchAvailable(params = {}) {
|
|
651
|
+
const country = (params.country ?? "US").toUpperCase();
|
|
652
|
+
const search = new URLSearchParams();
|
|
653
|
+
if (params.areaCode) search.set("AreaCode", params.areaCode);
|
|
654
|
+
if (params.contains) search.set("Contains", params.contains);
|
|
655
|
+
search.set("PageSize", String(params.limit ?? 10));
|
|
656
|
+
const url = `${this.accountUrl(
|
|
657
|
+
`/AvailablePhoneNumbers/${country}/Local.json`
|
|
658
|
+
)}?${search.toString()}`;
|
|
659
|
+
const res = await this.fetchImpl(url, { headers: this.headers() });
|
|
660
|
+
if (!res.ok) {
|
|
661
|
+
throw new TwilioApiError(
|
|
662
|
+
`searchAvailable failed: ${res.status}`,
|
|
663
|
+
res.status,
|
|
664
|
+
await res.text()
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
const data = await res.json();
|
|
668
|
+
return (data.available_phone_numbers ?? []).map((n) => ({
|
|
669
|
+
phoneNumber: n.phone_number,
|
|
670
|
+
friendlyName: n.friendly_name ?? n.phone_number,
|
|
671
|
+
region: n.region,
|
|
672
|
+
locality: n.locality,
|
|
673
|
+
isoCountry: n.iso_country
|
|
674
|
+
}));
|
|
675
|
+
}
|
|
676
|
+
async purchaseNumber(params) {
|
|
677
|
+
const body = new URLSearchParams();
|
|
678
|
+
body.set("PhoneNumber", params.phoneNumber);
|
|
679
|
+
if (params.friendlyName) body.set("FriendlyName", params.friendlyName);
|
|
680
|
+
if (params.voiceUrl) body.set("VoiceUrl", params.voiceUrl);
|
|
681
|
+
const res = await this.fetchImpl(this.accountUrl("/IncomingPhoneNumbers.json"), {
|
|
682
|
+
method: "POST",
|
|
683
|
+
headers: this.headers({ "content-type": "application/x-www-form-urlencoded" }),
|
|
684
|
+
body: body.toString()
|
|
685
|
+
});
|
|
686
|
+
if (!res.ok) {
|
|
687
|
+
throw new TwilioApiError(
|
|
688
|
+
`purchaseNumber failed: ${res.status}`,
|
|
689
|
+
res.status,
|
|
690
|
+
await res.text()
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
const data = await res.json();
|
|
694
|
+
return {
|
|
695
|
+
sid: data.sid,
|
|
696
|
+
phoneNumber: data.phone_number,
|
|
697
|
+
friendlyName: data.friendly_name ?? data.phone_number,
|
|
698
|
+
voiceUrl: data.voice_url
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
async releaseNumber(sid) {
|
|
702
|
+
const res = await this.fetchImpl(
|
|
703
|
+
this.accountUrl(`/IncomingPhoneNumbers/${encodeURIComponent(sid)}.json`),
|
|
704
|
+
{ method: "DELETE", headers: this.headers() }
|
|
705
|
+
);
|
|
706
|
+
if (!res.ok && res.status !== 404) {
|
|
707
|
+
throw new TwilioApiError(
|
|
708
|
+
`releaseNumber failed: ${res.status}`,
|
|
709
|
+
res.status,
|
|
710
|
+
await res.text()
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
// src/telephony.ts
|
|
717
|
+
var TelephonyConfigError = class extends Error {
|
|
718
|
+
constructor(message) {
|
|
719
|
+
super(message);
|
|
720
|
+
this.name = "TelephonyConfigError";
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
var TelephonyApiError = class extends Error {
|
|
724
|
+
constructor(message, status, body) {
|
|
725
|
+
super(message);
|
|
726
|
+
this.status = status;
|
|
727
|
+
this.body = body;
|
|
728
|
+
this.name = "TelephonyApiError";
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
var HostedTelephonyClient = class {
|
|
732
|
+
constructor(opts) {
|
|
733
|
+
this.opts = opts;
|
|
734
|
+
this.mode = "hosted";
|
|
735
|
+
if (!opts.shogoApiKey) {
|
|
736
|
+
throw new TelephonyConfigError("Hosted telephony requires shogoApiKey");
|
|
737
|
+
}
|
|
738
|
+
if (!opts.projectId) {
|
|
739
|
+
throw new TelephonyConfigError("Hosted telephony requires projectId");
|
|
740
|
+
}
|
|
741
|
+
if (!opts.apiUrl) {
|
|
742
|
+
throw new TelephonyConfigError("Hosted telephony requires apiUrl");
|
|
743
|
+
}
|
|
744
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
745
|
+
if (typeof this.fetchImpl !== "function") {
|
|
746
|
+
throw new TelephonyConfigError("global fetch is unavailable; pass opts.fetch");
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
url(path) {
|
|
750
|
+
return `${this.opts.apiUrl.replace(/\/+$/, "")}${path}`;
|
|
751
|
+
}
|
|
752
|
+
async request(path, init = {}) {
|
|
753
|
+
const headers = {
|
|
754
|
+
authorization: `Bearer ${this.opts.shogoApiKey}`,
|
|
755
|
+
...init.headers
|
|
756
|
+
};
|
|
757
|
+
let body = init.body;
|
|
758
|
+
if (init.json !== void 0) {
|
|
759
|
+
headers["content-type"] = "application/json";
|
|
760
|
+
body = JSON.stringify(init.json);
|
|
761
|
+
}
|
|
762
|
+
const res = await this.fetchImpl(this.url(path), {
|
|
763
|
+
...init,
|
|
764
|
+
headers,
|
|
765
|
+
body,
|
|
766
|
+
credentials: "omit"
|
|
767
|
+
});
|
|
768
|
+
const text = await res.text();
|
|
769
|
+
let parsed = void 0;
|
|
770
|
+
if (text) {
|
|
771
|
+
try {
|
|
772
|
+
parsed = JSON.parse(text);
|
|
773
|
+
} catch {
|
|
774
|
+
parsed = text;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (!res.ok) {
|
|
778
|
+
const message = parsed?.error ?? parsed?.message ?? `Shogo API error: ${res.status}`;
|
|
779
|
+
throw new TelephonyApiError(message, res.status, parsed);
|
|
780
|
+
}
|
|
781
|
+
return parsed;
|
|
782
|
+
}
|
|
783
|
+
async provisionNumber(opts = {}) {
|
|
784
|
+
const data = await this.request(
|
|
785
|
+
`/api/voice/twilio/provision-number/${encodeURIComponent(
|
|
786
|
+
this.opts.projectId
|
|
787
|
+
)}`,
|
|
788
|
+
{
|
|
789
|
+
method: "POST",
|
|
790
|
+
json: {
|
|
791
|
+
areaCode: opts.areaCode,
|
|
792
|
+
country: opts.country,
|
|
793
|
+
friendlyName: opts.friendlyName
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
);
|
|
797
|
+
return {
|
|
798
|
+
phoneNumber: data.phoneNumber,
|
|
799
|
+
twilioPhoneSid: data.twilioPhoneSid,
|
|
800
|
+
elevenlabsPhoneId: data.elevenlabsPhoneId,
|
|
801
|
+
setupBilledUsd: data.setupBilledUsd,
|
|
802
|
+
monthlyBilledUsd: data.monthlyBilledUsd,
|
|
803
|
+
usageDebited: data.usageDebited
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
async outboundCall(opts) {
|
|
807
|
+
const data = await this.request(
|
|
808
|
+
`/api/voice/twilio/outbound/${encodeURIComponent(this.opts.projectId)}`,
|
|
809
|
+
{
|
|
810
|
+
method: "POST",
|
|
811
|
+
json: { to: opts.to, dynamicVariables: opts.dynamicVariables }
|
|
812
|
+
}
|
|
813
|
+
);
|
|
814
|
+
return {
|
|
815
|
+
callSid: data.callSid,
|
|
816
|
+
conversationId: data.conversationId,
|
|
817
|
+
estimatedBilledUsd: data.estimatedBilledUsd,
|
|
818
|
+
billedUsdPerMinute: data.billedUsdPerMinute
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
async releaseNumber() {
|
|
822
|
+
await this.request(
|
|
823
|
+
`/api/voice/twilio/number/${encodeURIComponent(this.opts.projectId)}`,
|
|
824
|
+
{ method: "DELETE" }
|
|
825
|
+
);
|
|
826
|
+
return { released: true };
|
|
827
|
+
}
|
|
828
|
+
async getUsage(range = {}) {
|
|
829
|
+
const q = new URLSearchParams();
|
|
830
|
+
if (range.from) q.set("from", range.from);
|
|
831
|
+
if (range.to) q.set("to", range.to);
|
|
832
|
+
const path = `/api/voice/usage/${encodeURIComponent(this.opts.projectId)}${q.toString() ? `?${q.toString()}` : ""}`;
|
|
833
|
+
return this.request(path, { method: "GET" });
|
|
834
|
+
}
|
|
835
|
+
async listCalls(opts = {}) {
|
|
836
|
+
const q = new URLSearchParams();
|
|
837
|
+
if (opts.limit) q.set("limit", String(opts.limit));
|
|
838
|
+
if (opts.includeTranscript) q.set("includeTranscript", "1");
|
|
839
|
+
const path = `/api/voice/calls/${encodeURIComponent(this.opts.projectId)}${q.toString() ? `?${q.toString()}` : ""}`;
|
|
840
|
+
const res = await this.request(
|
|
841
|
+
path,
|
|
842
|
+
{ method: "GET" }
|
|
843
|
+
);
|
|
844
|
+
return res.calls;
|
|
845
|
+
}
|
|
846
|
+
async getCall(callId) {
|
|
847
|
+
const path = `/api/voice/calls/${encodeURIComponent(
|
|
848
|
+
this.opts.projectId
|
|
849
|
+
)}/${encodeURIComponent(callId)}`;
|
|
850
|
+
return this.request(path, { method: "GET" });
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
var HostedRuntimeTokenClient = class {
|
|
854
|
+
constructor(opts) {
|
|
855
|
+
this.opts = opts;
|
|
856
|
+
this.mode = "hosted";
|
|
857
|
+
if (!opts.runtimeToken) {
|
|
858
|
+
throw new TelephonyConfigError(
|
|
859
|
+
"Runtime-token telephony requires runtimeToken (process.env.RUNTIME_AUTH_SECRET)"
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
if (!opts.projectId) {
|
|
863
|
+
throw new TelephonyConfigError("Runtime-token telephony requires projectId");
|
|
864
|
+
}
|
|
865
|
+
if (!opts.apiUrl) {
|
|
866
|
+
throw new TelephonyConfigError("Runtime-token telephony requires apiUrl");
|
|
867
|
+
}
|
|
868
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
869
|
+
if (typeof this.fetchImpl !== "function") {
|
|
870
|
+
throw new TelephonyConfigError("global fetch is unavailable; pass opts.fetch");
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
url(path) {
|
|
874
|
+
const base = this.opts.apiUrl.replace(/\/+$/, "");
|
|
875
|
+
const sep = path.includes("?") ? "&" : "?";
|
|
876
|
+
return `${base}${path}${sep}projectId=${encodeURIComponent(this.opts.projectId)}`;
|
|
877
|
+
}
|
|
878
|
+
async request(path, init = {}) {
|
|
879
|
+
const headers = {
|
|
880
|
+
"x-runtime-token": this.opts.runtimeToken,
|
|
881
|
+
...init.headers
|
|
882
|
+
};
|
|
883
|
+
let body = init.body;
|
|
884
|
+
if (init.json !== void 0) {
|
|
885
|
+
headers["content-type"] = "application/json";
|
|
886
|
+
body = JSON.stringify(init.json);
|
|
887
|
+
}
|
|
888
|
+
const res = await this.fetchImpl(this.url(path), {
|
|
889
|
+
...init,
|
|
890
|
+
headers,
|
|
891
|
+
body,
|
|
892
|
+
credentials: "omit"
|
|
893
|
+
});
|
|
894
|
+
const text = await res.text();
|
|
895
|
+
let parsed = void 0;
|
|
896
|
+
if (text) {
|
|
897
|
+
try {
|
|
898
|
+
parsed = JSON.parse(text);
|
|
899
|
+
} catch {
|
|
900
|
+
parsed = text;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (!res.ok) {
|
|
904
|
+
const message = parsed?.error ?? parsed?.message ?? `Shogo API error: ${res.status}`;
|
|
905
|
+
throw new TelephonyApiError(message, res.status, parsed);
|
|
906
|
+
}
|
|
907
|
+
return parsed;
|
|
908
|
+
}
|
|
909
|
+
async provisionNumber(opts = {}) {
|
|
910
|
+
const data = await this.request(
|
|
911
|
+
`/api/voice/twilio/provision-number/${encodeURIComponent(
|
|
912
|
+
this.opts.projectId
|
|
913
|
+
)}`,
|
|
914
|
+
{
|
|
915
|
+
method: "POST",
|
|
916
|
+
json: {
|
|
917
|
+
areaCode: opts.areaCode,
|
|
918
|
+
country: opts.country,
|
|
919
|
+
friendlyName: opts.friendlyName
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
);
|
|
923
|
+
return {
|
|
924
|
+
phoneNumber: data.phoneNumber,
|
|
925
|
+
twilioPhoneSid: data.twilioPhoneSid,
|
|
926
|
+
elevenlabsPhoneId: data.elevenlabsPhoneId,
|
|
927
|
+
setupBilledUsd: data.setupBilledUsd,
|
|
928
|
+
monthlyBilledUsd: data.monthlyBilledUsd,
|
|
929
|
+
usageDebited: data.usageDebited
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
async outboundCall(opts) {
|
|
933
|
+
const data = await this.request(
|
|
934
|
+
`/api/voice/twilio/outbound/${encodeURIComponent(this.opts.projectId)}`,
|
|
935
|
+
{
|
|
936
|
+
method: "POST",
|
|
937
|
+
json: { to: opts.to, dynamicVariables: opts.dynamicVariables }
|
|
938
|
+
}
|
|
939
|
+
);
|
|
940
|
+
return {
|
|
941
|
+
callSid: data.callSid,
|
|
942
|
+
conversationId: data.conversationId,
|
|
943
|
+
estimatedBilledUsd: data.estimatedBilledUsd,
|
|
944
|
+
billedUsdPerMinute: data.billedUsdPerMinute
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
async releaseNumber() {
|
|
948
|
+
await this.request(
|
|
949
|
+
`/api/voice/twilio/number/${encodeURIComponent(this.opts.projectId)}`,
|
|
950
|
+
{ method: "DELETE" }
|
|
951
|
+
);
|
|
952
|
+
return { released: true };
|
|
953
|
+
}
|
|
954
|
+
async getUsage(range = {}) {
|
|
955
|
+
const q = new URLSearchParams();
|
|
956
|
+
if (range.from) q.set("from", range.from);
|
|
957
|
+
if (range.to) q.set("to", range.to);
|
|
958
|
+
const path = `/api/voice/usage/${encodeURIComponent(this.opts.projectId)}${q.toString() ? `?${q.toString()}` : ""}`;
|
|
959
|
+
return this.request(path, { method: "GET" });
|
|
960
|
+
}
|
|
961
|
+
async listCalls(opts = {}) {
|
|
962
|
+
const q = new URLSearchParams();
|
|
963
|
+
if (opts.limit) q.set("limit", String(opts.limit));
|
|
964
|
+
if (opts.includeTranscript) q.set("includeTranscript", "1");
|
|
965
|
+
const path = `/api/voice/calls/${encodeURIComponent(this.opts.projectId)}${q.toString() ? `?${q.toString()}` : ""}`;
|
|
966
|
+
const res = await this.request(
|
|
967
|
+
path,
|
|
968
|
+
{ method: "GET" }
|
|
969
|
+
);
|
|
970
|
+
return res.calls;
|
|
971
|
+
}
|
|
972
|
+
async getCall(callId) {
|
|
973
|
+
const path = `/api/voice/calls/${encodeURIComponent(
|
|
974
|
+
this.opts.projectId
|
|
975
|
+
)}/${encodeURIComponent(callId)}`;
|
|
976
|
+
return this.request(path, { method: "GET" });
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
var DirectTelephonyClient = class {
|
|
980
|
+
constructor(opts) {
|
|
981
|
+
this.opts = opts;
|
|
982
|
+
this.mode = "direct";
|
|
983
|
+
if (!opts.elevenlabs?.apiKey) {
|
|
984
|
+
throw new TelephonyConfigError("Direct telephony requires elevenlabs.apiKey");
|
|
985
|
+
}
|
|
986
|
+
if (!opts.elevenlabs?.agentId) {
|
|
987
|
+
throw new TelephonyConfigError("Direct telephony requires elevenlabs.agentId");
|
|
988
|
+
}
|
|
989
|
+
if (!opts.twilio?.accountSid || !opts.twilio?.authToken) {
|
|
990
|
+
throw new TelephonyConfigError(
|
|
991
|
+
"Direct telephony requires twilio.accountSid + twilio.authToken"
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
this.el = new ElevenLabsClient({
|
|
995
|
+
apiKey: opts.elevenlabs.apiKey,
|
|
996
|
+
baseUrl: opts.elevenlabs.baseUrl,
|
|
997
|
+
fetch: opts.fetch
|
|
998
|
+
});
|
|
999
|
+
this.tw = new TwilioClient({
|
|
1000
|
+
accountSid: opts.twilio.accountSid,
|
|
1001
|
+
authToken: opts.twilio.authToken,
|
|
1002
|
+
baseUrl: opts.twilio.baseUrl,
|
|
1003
|
+
fetch: opts.fetch
|
|
1004
|
+
});
|
|
1005
|
+
this.state = {
|
|
1006
|
+
phoneNumber: opts.twilio.fromNumber,
|
|
1007
|
+
twilioSid: opts.twilio.phoneSid,
|
|
1008
|
+
elevenlabsPhoneId: opts.elevenlabs.phoneNumberId
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
async provisionNumber(opts = {}) {
|
|
1012
|
+
const candidates = await this.tw.searchAvailable({
|
|
1013
|
+
country: opts.country,
|
|
1014
|
+
areaCode: opts.areaCode,
|
|
1015
|
+
limit: 1
|
|
1016
|
+
});
|
|
1017
|
+
if (candidates.length === 0) {
|
|
1018
|
+
throw new TelephonyApiError(
|
|
1019
|
+
"No Twilio numbers available matching criteria",
|
|
1020
|
+
404,
|
|
1021
|
+
{ areaCode: opts.areaCode, country: opts.country }
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
const purchased = await this.tw.purchaseNumber({
|
|
1025
|
+
phoneNumber: candidates[0].phoneNumber,
|
|
1026
|
+
friendlyName: opts.friendlyName
|
|
1027
|
+
});
|
|
1028
|
+
try {
|
|
1029
|
+
const { phoneNumberId } = await this.el.createPhoneNumberTwilio({
|
|
1030
|
+
phoneNumber: purchased.phoneNumber,
|
|
1031
|
+
label: opts.friendlyName ?? purchased.phoneNumber,
|
|
1032
|
+
agentId: this.opts.elevenlabs.agentId,
|
|
1033
|
+
twilioAccountSid: this.opts.twilio.accountSid,
|
|
1034
|
+
twilioAuthToken: this.opts.twilio.authToken
|
|
1035
|
+
});
|
|
1036
|
+
this.state = {
|
|
1037
|
+
phoneNumber: purchased.phoneNumber,
|
|
1038
|
+
twilioSid: purchased.sid,
|
|
1039
|
+
elevenlabsPhoneId: phoneNumberId
|
|
1040
|
+
};
|
|
1041
|
+
return {
|
|
1042
|
+
phoneNumber: purchased.phoneNumber,
|
|
1043
|
+
twilioPhoneSid: purchased.sid,
|
|
1044
|
+
elevenlabsPhoneId: phoneNumberId
|
|
1045
|
+
};
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
try {
|
|
1048
|
+
await this.tw.releaseNumber(purchased.sid);
|
|
1049
|
+
} catch {
|
|
1050
|
+
}
|
|
1051
|
+
throw err;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
async outboundCall(opts) {
|
|
1055
|
+
if (!this.state.elevenlabsPhoneId) {
|
|
1056
|
+
throw new TelephonyConfigError(
|
|
1057
|
+
"outboundCall requires an ElevenLabs phone id; call provisionNumber() or pass elevenlabs.phoneNumberId"
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
const res = await this.el.outboundCall({
|
|
1061
|
+
phoneNumberId: this.state.elevenlabsPhoneId,
|
|
1062
|
+
agentId: this.opts.elevenlabs.agentId,
|
|
1063
|
+
toNumber: opts.to,
|
|
1064
|
+
dynamicVariables: opts.dynamicVariables
|
|
1065
|
+
});
|
|
1066
|
+
return { callSid: res.callSid, conversationId: res.conversationId };
|
|
1067
|
+
}
|
|
1068
|
+
async releaseNumber() {
|
|
1069
|
+
let released = false;
|
|
1070
|
+
if (this.state.elevenlabsPhoneId) {
|
|
1071
|
+
await this.el.deletePhoneNumber(this.state.elevenlabsPhoneId);
|
|
1072
|
+
released = true;
|
|
1073
|
+
}
|
|
1074
|
+
if (this.state.twilioSid) {
|
|
1075
|
+
await this.tw.releaseNumber(this.state.twilioSid);
|
|
1076
|
+
released = true;
|
|
1077
|
+
}
|
|
1078
|
+
this.state = {};
|
|
1079
|
+
return { released };
|
|
1080
|
+
}
|
|
1081
|
+
async getUsage() {
|
|
1082
|
+
throw new TelephonyConfigError(
|
|
1083
|
+
"getUsage() is unavailable in Mode A (self-hosted). Query Twilio + ElevenLabs directly."
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
async listCalls() {
|
|
1087
|
+
throw new TelephonyConfigError(
|
|
1088
|
+
"listCalls() is unavailable in Mode A (self-hosted). Query ElevenLabs conversations directly."
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
async getCall() {
|
|
1092
|
+
throw new TelephonyConfigError(
|
|
1093
|
+
"getCall() is unavailable in Mode A (self-hosted). Query ElevenLabs conversations directly."
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
function createTelephonyClient(opts) {
|
|
1098
|
+
if (opts.mode === "hosted") return new HostedTelephonyClient(opts);
|
|
1099
|
+
if (opts.mode === "runtime-token") return new HostedRuntimeTokenClient(opts);
|
|
1100
|
+
return new DirectTelephonyClient(opts);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// src/mock-telephony.ts
|
|
1104
|
+
var DEFAULT_PHONE = "+14155550143";
|
|
1105
|
+
var DEFAULT_TWILIO_SID = "PNmock0000000000000000000000000001";
|
|
1106
|
+
var DEFAULT_EL_PHONE_ID = "phn_mock_0000000001";
|
|
1107
|
+
var DEFAULT_CALL_FIXTURES = [
|
|
1108
|
+
{
|
|
1109
|
+
id: "mock_call_001",
|
|
1110
|
+
conversationId: "mock_conv_001",
|
|
1111
|
+
callSid: "CAmock00000000000000000000000000001",
|
|
1112
|
+
direction: "outbound",
|
|
1113
|
+
durationSeconds: 184,
|
|
1114
|
+
billedMinutes: 4,
|
|
1115
|
+
startedAt: "2026-05-06T22:01:00.000Z",
|
|
1116
|
+
endedAt: "2026-05-06T22:04:04.000Z",
|
|
1117
|
+
createdAt: "2026-05-06T22:01:00.000Z",
|
|
1118
|
+
billed: true,
|
|
1119
|
+
hasTranscript: true,
|
|
1120
|
+
transcriptSummary: "Prospect agreed to a 30-min demo on Thursday at 2pm PT. Booked via Calendly link."
|
|
1121
|
+
},
|
|
1122
|
+
{
|
|
1123
|
+
id: "mock_call_002",
|
|
1124
|
+
conversationId: "mock_conv_002",
|
|
1125
|
+
callSid: "CAmock00000000000000000000000000002",
|
|
1126
|
+
direction: "outbound",
|
|
1127
|
+
durationSeconds: 41,
|
|
1128
|
+
billedMinutes: 1,
|
|
1129
|
+
startedAt: "2026-05-06T22:05:00.000Z",
|
|
1130
|
+
endedAt: "2026-05-06T22:05:41.000Z",
|
|
1131
|
+
createdAt: "2026-05-06T22:05:00.000Z",
|
|
1132
|
+
billed: true,
|
|
1133
|
+
hasTranscript: true,
|
|
1134
|
+
transcriptSummary: "Voicemail. Left a 25-second AI-disclosed message with callback number."
|
|
1135
|
+
},
|
|
1136
|
+
{
|
|
1137
|
+
id: "mock_call_003",
|
|
1138
|
+
conversationId: "mock_conv_003",
|
|
1139
|
+
callSid: "CAmock00000000000000000000000000003",
|
|
1140
|
+
direction: "outbound",
|
|
1141
|
+
durationSeconds: 22,
|
|
1142
|
+
billedMinutes: 1,
|
|
1143
|
+
startedAt: "2026-05-06T22:06:30.000Z",
|
|
1144
|
+
endedAt: "2026-05-06T22:06:52.000Z",
|
|
1145
|
+
createdAt: "2026-05-06T22:06:30.000Z",
|
|
1146
|
+
billed: true,
|
|
1147
|
+
hasTranscript: true,
|
|
1148
|
+
transcriptSummary: 'Prospect said "remove me" 8 seconds in. Honored, persisted to do-not-call list.'
|
|
1149
|
+
}
|
|
1150
|
+
];
|
|
1151
|
+
var DEFAULT_TRANSCRIPTS = {
|
|
1152
|
+
mock_conv_001: [
|
|
1153
|
+
{ role: "agent", message: "Hi, this is an AI assistant calling on behalf of Russell at Shogo. Do you have 30 seconds?", time_in_call_secs: 1 },
|
|
1154
|
+
{ role: "user", message: "Sure, what is this about?", time_in_call_secs: 6 },
|
|
1155
|
+
{ role: "agent", message: "Shogo is a universal AI agent that runs every job-to-be-done with real interfaces, not chat. We help teams like yours replace point tools with one platform.", time_in_call_secs: 10 },
|
|
1156
|
+
{ role: "user", message: "Interesting. We've been looking at workflow automation. How is this different from Zapier?", time_in_call_secs: 24 },
|
|
1157
|
+
{ role: "agent", message: "Zapier connects steps. Shogo runs the whole workflow with an agent that can use tools, build interfaces, and learn from your data.", time_in_call_secs: 30 },
|
|
1158
|
+
{ role: "user", message: "OK I'd like to see it. Send me a demo invite.", time_in_call_secs: 52 },
|
|
1159
|
+
{ role: "agent", message: "Great. I'm sending the Calendly link now. Does Thursday at 2pm Pacific work?", time_in_call_secs: 58 },
|
|
1160
|
+
{ role: "user", message: "Yes, perfect.", time_in_call_secs: 66 },
|
|
1161
|
+
{ role: "agent", message: "Booked. You'll get a confirmation email in 30 seconds. Thanks for your time.", time_in_call_secs: 70 }
|
|
1162
|
+
],
|
|
1163
|
+
mock_conv_002: [
|
|
1164
|
+
{ role: "agent", message: "Hi, this is an AI assistant calling on behalf of Russell at Shogo. Leaving a voicemail \u2014 we'd love to show you our universal AI agent platform. Call us back at +1 415 555 0143 or visit shogo.ai. Have a great day.", time_in_call_secs: 1 }
|
|
1165
|
+
],
|
|
1166
|
+
mock_conv_003: [
|
|
1167
|
+
{ role: "agent", message: "Hi, this is an AI assistant calling on behalf of Russell at Shogo \u2014", time_in_call_secs: 1 },
|
|
1168
|
+
{ role: "user", message: "Remove me from your list.", time_in_call_secs: 8 },
|
|
1169
|
+
{ role: "agent", message: "Understood. You're removed and we won't call again. Thank you.", time_in_call_secs: 11 }
|
|
1170
|
+
]
|
|
1171
|
+
};
|
|
1172
|
+
var warned = false;
|
|
1173
|
+
var MockTelephonyClient = class {
|
|
1174
|
+
constructor(opts = {}) {
|
|
1175
|
+
this.mode = "mock";
|
|
1176
|
+
this.callCounter = 0;
|
|
1177
|
+
this.projectId = opts.projectId ?? "demo-project";
|
|
1178
|
+
this.phoneNumber = opts.phoneNumber ?? DEFAULT_PHONE;
|
|
1179
|
+
this.fixtures = opts.callFixtures ?? DEFAULT_CALL_FIXTURES;
|
|
1180
|
+
if (!warned) {
|
|
1181
|
+
console.warn(
|
|
1182
|
+
"[shogo] voice mock mode active \u2014 telephony returns canned data, no real provider calls are made."
|
|
1183
|
+
);
|
|
1184
|
+
warned = true;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
async provisionNumber(_opts = {}) {
|
|
1188
|
+
return {
|
|
1189
|
+
phoneNumber: this.phoneNumber,
|
|
1190
|
+
twilioPhoneSid: DEFAULT_TWILIO_SID,
|
|
1191
|
+
elevenlabsPhoneId: DEFAULT_EL_PHONE_ID,
|
|
1192
|
+
setupBilledUsd: 0,
|
|
1193
|
+
monthlyBilledUsd: 0,
|
|
1194
|
+
usageDebited: { setup: false, monthly: false }
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
async outboundCall(_opts) {
|
|
1198
|
+
this.callCounter++;
|
|
1199
|
+
const seq = String(this.callCounter).padStart(32, "0").slice(-32);
|
|
1200
|
+
return {
|
|
1201
|
+
callSid: `CAmock${seq}`,
|
|
1202
|
+
conversationId: `mock_conv_${String(this.callCounter).padStart(3, "0")}`,
|
|
1203
|
+
estimatedBilledUsd: 0,
|
|
1204
|
+
billedUsdPerMinute: 0
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
async releaseNumber() {
|
|
1208
|
+
return { released: true };
|
|
1209
|
+
}
|
|
1210
|
+
async getUsage(_range) {
|
|
1211
|
+
const outboundCalls = this.fixtures.filter((c) => c.direction === "outbound").length;
|
|
1212
|
+
const inboundCalls = this.fixtures.length - outboundCalls;
|
|
1213
|
+
const totalMinutes = this.fixtures.reduce((sum, c) => sum + c.billedMinutes, 0);
|
|
1214
|
+
return {
|
|
1215
|
+
projectId: this.projectId,
|
|
1216
|
+
range: { from: null, to: null },
|
|
1217
|
+
totals: {
|
|
1218
|
+
minutesInbound: 0,
|
|
1219
|
+
minutesOutbound: totalMinutes,
|
|
1220
|
+
billedUsdInbound: 0,
|
|
1221
|
+
billedUsdOutbound: 0,
|
|
1222
|
+
billedUsdNumbers: 0,
|
|
1223
|
+
billedUsd: 0,
|
|
1224
|
+
calls: this.fixtures.length,
|
|
1225
|
+
inboundCalls,
|
|
1226
|
+
outboundCalls
|
|
1227
|
+
},
|
|
1228
|
+
events: []
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
async listCalls(opts = {}) {
|
|
1232
|
+
const limit = opts.limit ?? this.fixtures.length;
|
|
1233
|
+
const slice = this.fixtures.slice(0, limit);
|
|
1234
|
+
if (!opts.includeTranscript) return slice.map((c) => ({ ...c }));
|
|
1235
|
+
return slice.map((c) => ({
|
|
1236
|
+
...c,
|
|
1237
|
+
transcript: DEFAULT_TRANSCRIPTS[c.conversationId ?? ""] ?? null
|
|
1238
|
+
}));
|
|
1239
|
+
}
|
|
1240
|
+
async getCall(callId) {
|
|
1241
|
+
const found = this.fixtures.find(
|
|
1242
|
+
(c) => c.id === callId || c.conversationId === callId || c.callSid === callId
|
|
1243
|
+
);
|
|
1244
|
+
if (!found) {
|
|
1245
|
+
return {
|
|
1246
|
+
id: callId,
|
|
1247
|
+
conversationId: callId,
|
|
1248
|
+
callSid: callId,
|
|
1249
|
+
direction: "outbound",
|
|
1250
|
+
durationSeconds: 0,
|
|
1251
|
+
billedMinutes: 0,
|
|
1252
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1253
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1254
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1255
|
+
billed: false,
|
|
1256
|
+
transcriptSummary: null,
|
|
1257
|
+
transcript: null
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
const { hasTranscript: _hasTranscript, ...rest } = found;
|
|
1261
|
+
return {
|
|
1262
|
+
...rest,
|
|
1263
|
+
transcript: DEFAULT_TRANSCRIPTS[found.conversationId ?? ""] ?? null
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
function isVoiceMockEnv() {
|
|
1268
|
+
if (typeof process === "undefined") return false;
|
|
1269
|
+
const value = process.env?.SHOGO_VOICE_MODE;
|
|
1270
|
+
if (!value) return false;
|
|
1271
|
+
const v = value.toLowerCase();
|
|
1272
|
+
return v === "mock" || v === "demo" || v === "fake";
|
|
1273
|
+
}
|
|
1274
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1275
|
+
0 && (module.exports = {
|
|
1276
|
+
AUDIO_TAGS,
|
|
1277
|
+
AUDIO_TAG_GROUPS,
|
|
1278
|
+
CONVAI_SUPPORTED_TTS_MODELS,
|
|
1279
|
+
CONVAI_TTS_MODEL_FALLBACK,
|
|
1280
|
+
DEFAULT_ALLOWED_TAGS,
|
|
1281
|
+
DEFAULT_ELEVENLABS_BASE_URL,
|
|
1282
|
+
DEFAULT_MEMORY_BLOCK,
|
|
1283
|
+
DEFAULT_TWILIO_BASE_URL,
|
|
1284
|
+
DEFAULT_VOICE_SETTINGS,
|
|
1285
|
+
DirectTelephonyClient,
|
|
1286
|
+
EXPRESSIVITY_BLOCK_CLOSE,
|
|
1287
|
+
EXPRESSIVITY_BLOCK_OPEN,
|
|
1288
|
+
EXPRESSIVITY_OPTIONS,
|
|
1289
|
+
ElevenLabsApiError,
|
|
1290
|
+
ElevenLabsClient,
|
|
1291
|
+
HostedRuntimeTokenClient,
|
|
1292
|
+
HostedTelephonyClient,
|
|
1293
|
+
MEMORY_CLIENT_TOOLS,
|
|
1294
|
+
MockTelephonyClient,
|
|
1295
|
+
TelephonyApiError,
|
|
1296
|
+
TelephonyConfigError,
|
|
1297
|
+
TwilioApiError,
|
|
1298
|
+
TwilioClient,
|
|
1299
|
+
buildPreviewLine,
|
|
1300
|
+
composeAgentPrompt,
|
|
1301
|
+
composeExpressivityBlock,
|
|
1302
|
+
createTelephonyClient,
|
|
1303
|
+
extractBasePrompt,
|
|
1304
|
+
isVoiceMockEnv,
|
|
1305
|
+
normalizeAudioTags,
|
|
1306
|
+
normalizeExpressivity,
|
|
1307
|
+
normalizeVoiceSettings,
|
|
1308
|
+
readAudioTags,
|
|
1309
|
+
readExpressivity,
|
|
1310
|
+
readVoiceSettings,
|
|
1311
|
+
resolveConvaiTtsModel,
|
|
1312
|
+
stripAudioTags,
|
|
1313
|
+
stripExpressivityBlock,
|
|
1314
|
+
stripMemoryBlock
|
|
1315
|
+
});
|
|
1316
|
+
//# sourceMappingURL=index.cjs.map
|