@spekoai/mcp-calls 0.4.5 → 0.4.6
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.js +599 -19
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- package/server.json +2 -2
package/dist/index.js
CHANGED
|
@@ -13,12 +13,12 @@ var __export = (target, all) => {
|
|
|
13
13
|
import { createHash as createHash2 } from "crypto";
|
|
14
14
|
import { existsSync as existsSync3 } from "fs";
|
|
15
15
|
import { dirname as dirname3, resolve as resolve3 } from "path";
|
|
16
|
-
import { fileURLToPath as
|
|
16
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
17
17
|
function loadDotenv() {
|
|
18
18
|
const load = process.loadEnvFile;
|
|
19
19
|
if (!load)
|
|
20
20
|
return;
|
|
21
|
-
const here = dirname3(
|
|
21
|
+
const here = dirname3(fileURLToPath4(import.meta.url));
|
|
22
22
|
const candidates = [
|
|
23
23
|
resolve3(process.cwd(), ".env"),
|
|
24
24
|
resolve3(process.cwd(), "..", ".env"),
|
|
@@ -111,7 +111,7 @@ var init_config = __esm({
|
|
|
111
111
|
});
|
|
112
112
|
|
|
113
113
|
// ../server/dist/speko/client.js
|
|
114
|
-
import { Speko, SpekoApiError, SpekoAuthError, SpekoRateLimitError } from "@spekoai/sdk";
|
|
114
|
+
import { Speko as Speko2, SpekoApiError, SpekoAuthError, SpekoRateLimitError } from "@spekoai/sdk";
|
|
115
115
|
function isAuthFailure(e) {
|
|
116
116
|
return e instanceof SpekoAuthError || e instanceof SpekoApiError && (e.status === 401 || e.status === 403);
|
|
117
117
|
}
|
|
@@ -127,7 +127,7 @@ var init_client = __esm({
|
|
|
127
127
|
constructor(cfg) {
|
|
128
128
|
this.apiKey = cfg.speko.apiKey;
|
|
129
129
|
this.baseUrl = (cfg.speko.baseUrl ?? DEFAULT_API_BASE).replace(/\/+$/, "");
|
|
130
|
-
this.speko = new
|
|
130
|
+
this.speko = new Speko2({
|
|
131
131
|
apiKey: cfg.speko.apiKey,
|
|
132
132
|
...cfg.speko.baseUrl ? { baseUrl: cfg.speko.baseUrl } : {},
|
|
133
133
|
timeout: 3e4
|
|
@@ -925,9 +925,9 @@ var init_objective = __esm({
|
|
|
925
925
|
});
|
|
926
926
|
|
|
927
927
|
// ../server/dist/safety/prompt.js
|
|
928
|
-
import { randomBytes as
|
|
928
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
929
929
|
function delimitedBlock(label, content) {
|
|
930
|
-
const nonce =
|
|
930
|
+
const nonce = randomBytes3(8).toString("hex");
|
|
931
931
|
return `${BLOCK_RULE} ${label} ${nonce} ${BLOCK_RULE}
|
|
932
932
|
${content}
|
|
933
933
|
${BLOCK_RULE} END ${label} ${nonce} ${BLOCK_RULE}`;
|
|
@@ -1645,9 +1645,9 @@ function openBrowser(url) {
|
|
|
1645
1645
|
if (["1", "true", "yes"].includes((process.env.SPEKO_NO_BROWSER ?? "").toLowerCase())) return;
|
|
1646
1646
|
try {
|
|
1647
1647
|
const p = platform();
|
|
1648
|
-
const
|
|
1648
|
+
const cmd = p === "darwin" ? "open" : p === "win32" ? "cmd" : "xdg-open";
|
|
1649
1649
|
const args = p === "win32" ? ["/c", "start", "", url] : [url];
|
|
1650
|
-
const child = spawn(
|
|
1650
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
1651
1651
|
child.on("error", () => {
|
|
1652
1652
|
});
|
|
1653
1653
|
child.unref();
|
|
@@ -1872,9 +1872,9 @@ function askSecret(query) {
|
|
|
1872
1872
|
function openBrowser2(url) {
|
|
1873
1873
|
try {
|
|
1874
1874
|
const p = platform2();
|
|
1875
|
-
const
|
|
1875
|
+
const cmd = p === "darwin" ? "open" : p === "win32" ? "cmd" : "xdg-open";
|
|
1876
1876
|
const args = p === "win32" ? ["/c", "start", "", url] : [url];
|
|
1877
|
-
const child = spawn2(
|
|
1877
|
+
const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
|
|
1878
1878
|
child.on("error", () => {
|
|
1879
1879
|
});
|
|
1880
1880
|
child.unref();
|
|
@@ -1983,9 +1983,9 @@ function installSkill() {
|
|
|
1983
1983
|
return false;
|
|
1984
1984
|
}
|
|
1985
1985
|
}
|
|
1986
|
-
async function runInit(argv,
|
|
1986
|
+
async function runInit(argv, mode2 = "init") {
|
|
1987
1987
|
const f = parseFlags(argv);
|
|
1988
|
-
const quick =
|
|
1988
|
+
const quick = mode2 === "login";
|
|
1989
1989
|
console.log(c.bold(quick ? "\n Speko Calls \u2014 sign in\n" : "\n Speko Calls \u2014 setup\n"));
|
|
1990
1990
|
if (!quick) {
|
|
1991
1991
|
console.log(" This MCP places " + c.bold("real, disclosed") + " outbound phone calls to " + c.bold("businesses") + ",");
|
|
@@ -2047,6 +2047,14 @@ async function runInit(argv, mode = "init") {
|
|
|
2047
2047
|
console.log(c.dim(" set MCP_TIMEOUT=60000 and retry. Re-run this wizard anytime to reconfigure.\n"));
|
|
2048
2048
|
}
|
|
2049
2049
|
|
|
2050
|
+
// src/cli/audio/speak.ts
|
|
2051
|
+
import { parseArgs } from "util";
|
|
2052
|
+
import { statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
2053
|
+
import { resolve as resolvePath } from "path";
|
|
2054
|
+
|
|
2055
|
+
// src/cli/_shared/speko.ts
|
|
2056
|
+
import { Speko } from "@spekoai/sdk";
|
|
2057
|
+
|
|
2050
2058
|
// src/lib/env.ts
|
|
2051
2059
|
import { existsSync as existsSync2 } from "fs";
|
|
2052
2060
|
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
@@ -2078,6 +2086,539 @@ function serverEndpoint() {
|
|
|
2078
2086
|
return { baseUrl, internalKey };
|
|
2079
2087
|
}
|
|
2080
2088
|
|
|
2089
|
+
// src/cli/_shared/speko.ts
|
|
2090
|
+
var MissingKeyError = class extends Error {
|
|
2091
|
+
name = "MissingKeyError";
|
|
2092
|
+
};
|
|
2093
|
+
function resolveApiKey() {
|
|
2094
|
+
const raw = (process.env.SPEKO_API_KEY ?? process.env.SPEKOAI_API_KEY ?? "").trim();
|
|
2095
|
+
return raw.startsWith("Bearer ") ? raw.slice(7) : raw;
|
|
2096
|
+
}
|
|
2097
|
+
function makeSpeko() {
|
|
2098
|
+
loadEnv();
|
|
2099
|
+
const apiKey = resolveApiKey();
|
|
2100
|
+
if (!apiKey) {
|
|
2101
|
+
throw new MissingKeyError(
|
|
2102
|
+
"SPEKO_API_KEY is not set. Get one at https://platform.speko.dev, then run `npx @spekoai/mcp-calls login` (or export SPEKO_API_KEY=sk_...)."
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
return new Speko({ apiKey });
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
// src/cli/_shared/audio.ts
|
|
2109
|
+
function pcmSampleRate(contentType) {
|
|
2110
|
+
const m = /rate=(\d+)/i.exec(contentType);
|
|
2111
|
+
const n = m ? Number(m[1]) : NaN;
|
|
2112
|
+
return Number.isFinite(n) && n > 0 ? n : 24e3;
|
|
2113
|
+
}
|
|
2114
|
+
function extForContentType(contentType) {
|
|
2115
|
+
const ct = contentType.toLowerCase();
|
|
2116
|
+
if (ct.includes("mpeg") || ct.includes("mp3")) return "mp3";
|
|
2117
|
+
if (ct.includes("wav")) return "wav";
|
|
2118
|
+
if (ct.includes("pcm")) return "wav";
|
|
2119
|
+
if (ct.includes("opus")) return "opus";
|
|
2120
|
+
if (ct.includes("ogg")) return "ogg";
|
|
2121
|
+
if (ct.includes("aac")) return "aac";
|
|
2122
|
+
if (ct.includes("flac")) return "flac";
|
|
2123
|
+
return "audio";
|
|
2124
|
+
}
|
|
2125
|
+
function pcmToWav(pcm, sampleRate = 24e3) {
|
|
2126
|
+
const header = Buffer.alloc(44);
|
|
2127
|
+
const dataLen = pcm.length;
|
|
2128
|
+
header.write("RIFF", 0);
|
|
2129
|
+
header.writeUInt32LE(36 + dataLen, 4);
|
|
2130
|
+
header.write("WAVE", 8);
|
|
2131
|
+
header.write("fmt ", 12);
|
|
2132
|
+
header.writeUInt32LE(16, 16);
|
|
2133
|
+
header.writeUInt16LE(1, 20);
|
|
2134
|
+
header.writeUInt16LE(1, 22);
|
|
2135
|
+
header.writeUInt32LE(sampleRate, 24);
|
|
2136
|
+
header.writeUInt32LE(sampleRate * 2, 28);
|
|
2137
|
+
header.writeUInt16LE(2, 32);
|
|
2138
|
+
header.writeUInt16LE(16, 34);
|
|
2139
|
+
header.write("data", 36);
|
|
2140
|
+
header.writeUInt32LE(dataLen, 40);
|
|
2141
|
+
return Buffer.concat([header, Buffer.from(pcm)]);
|
|
2142
|
+
}
|
|
2143
|
+
function toPlayable(audio, contentType) {
|
|
2144
|
+
const ct = contentType.toLowerCase();
|
|
2145
|
+
if (ct.includes("pcm")) {
|
|
2146
|
+
return { bytes: pcmToWav(audio, pcmSampleRate(contentType)), ext: "wav" };
|
|
2147
|
+
}
|
|
2148
|
+
return { bytes: audio, ext: extForContentType(contentType) };
|
|
2149
|
+
}
|
|
2150
|
+
function guessAudioContentType(pathOrExt) {
|
|
2151
|
+
const ext = pathOrExt.toLowerCase().split(".").pop() ?? "";
|
|
2152
|
+
const map = {
|
|
2153
|
+
wav: "audio/wav",
|
|
2154
|
+
mp3: "audio/mpeg",
|
|
2155
|
+
mpeg: "audio/mpeg",
|
|
2156
|
+
m4a: "audio/mp4",
|
|
2157
|
+
mp4: "audio/mp4",
|
|
2158
|
+
ogg: "audio/ogg",
|
|
2159
|
+
oga: "audio/ogg",
|
|
2160
|
+
opus: "audio/opus",
|
|
2161
|
+
flac: "audio/flac",
|
|
2162
|
+
aac: "audio/aac",
|
|
2163
|
+
webm: "audio/webm"
|
|
2164
|
+
};
|
|
2165
|
+
return map[ext];
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
// src/cli/_shared/artifact.ts
|
|
2169
|
+
import { join as join2 } from "path";
|
|
2170
|
+
function resolveOutTarget(a) {
|
|
2171
|
+
const name = `${a.id}.${a.ext}`;
|
|
2172
|
+
if (a.out !== void 0 && a.out !== "") {
|
|
2173
|
+
if (a.outIsDir || a.out.endsWith("/") || a.out.endsWith("\\")) {
|
|
2174
|
+
return { mode: "file", path: join2(a.out, name) };
|
|
2175
|
+
}
|
|
2176
|
+
return { mode: "file", path: a.out };
|
|
2177
|
+
}
|
|
2178
|
+
if (!a.isTTY) return { mode: "stdout" };
|
|
2179
|
+
const dir = a.outputDir && a.outputDir.trim() ? a.outputDir.trim() : a.cwd;
|
|
2180
|
+
return { mode: "file", path: join2(dir, name) };
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
// src/cli/_shared/io.ts
|
|
2184
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
2185
|
+
function readStreamBytes(stream = process.stdin) {
|
|
2186
|
+
return new Promise((resolve4, reject) => {
|
|
2187
|
+
const chunks = [];
|
|
2188
|
+
stream.on("data", (c2) => chunks.push(Buffer.from(c2)));
|
|
2189
|
+
stream.on("end", () => resolve4(Buffer.concat(chunks)));
|
|
2190
|
+
stream.on("error", reject);
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
async function readStdinText(stream = process.stdin) {
|
|
2194
|
+
return Buffer.from(await readStreamBytes(stream)).toString("utf-8");
|
|
2195
|
+
}
|
|
2196
|
+
function readStdinBytes(stream = process.stdin) {
|
|
2197
|
+
return readStreamBytes(stream);
|
|
2198
|
+
}
|
|
2199
|
+
function randomId() {
|
|
2200
|
+
return randomBytes2(4).toString("hex");
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
// src/cli/_shared/play.ts
|
|
2204
|
+
import { spawn as spawn3, spawnSync as spawnSync2 } from "child_process";
|
|
2205
|
+
function pickPlayer(platform3, has) {
|
|
2206
|
+
const ffplay = { cmd: "ffplay", args: (f) => ["-nodisp", "-autoexit", "-loglevel", "quiet", f] };
|
|
2207
|
+
if (platform3 === "darwin") {
|
|
2208
|
+
if (has("afplay")) return { cmd: "afplay", args: (f) => [f] };
|
|
2209
|
+
if (has("ffplay")) return ffplay;
|
|
2210
|
+
return null;
|
|
2211
|
+
}
|
|
2212
|
+
if (platform3 === "win32") {
|
|
2213
|
+
if (has("ffplay")) return ffplay;
|
|
2214
|
+
if (has("powershell")) {
|
|
2215
|
+
return {
|
|
2216
|
+
cmd: "powershell",
|
|
2217
|
+
args: (f) => ["-NoProfile", "-Command", `(New-Object Media.SoundPlayer '${f.replace(/'/g, "''")}').PlaySync();`]
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
return null;
|
|
2221
|
+
}
|
|
2222
|
+
const candidates = [
|
|
2223
|
+
["ffplay", ffplay.args],
|
|
2224
|
+
["mpv", (f) => ["--no-video", "--really-quiet", f]],
|
|
2225
|
+
["aplay", (f) => [f]],
|
|
2226
|
+
["paplay", (f) => [f]],
|
|
2227
|
+
["mpg123", (f) => ["-q", f]]
|
|
2228
|
+
];
|
|
2229
|
+
for (const [bin, mk] of candidates) {
|
|
2230
|
+
if (has(bin)) return { cmd: bin, args: mk };
|
|
2231
|
+
}
|
|
2232
|
+
return null;
|
|
2233
|
+
}
|
|
2234
|
+
function onPath(bin) {
|
|
2235
|
+
const probe = process.platform === "win32" ? spawnSync2("where", [bin]) : spawnSync2("which", [bin]);
|
|
2236
|
+
return probe.status === 0;
|
|
2237
|
+
}
|
|
2238
|
+
async function playFile(path, deps = {}) {
|
|
2239
|
+
const platform3 = deps.platform ?? process.platform;
|
|
2240
|
+
const has = deps.has ?? onPath;
|
|
2241
|
+
const player = pickPlayer(platform3, has);
|
|
2242
|
+
if (!player) return false;
|
|
2243
|
+
await new Promise((resolve4) => {
|
|
2244
|
+
try {
|
|
2245
|
+
const p = spawn3(player.cmd, player.args(path), { stdio: "ignore" });
|
|
2246
|
+
p.on("close", () => resolve4());
|
|
2247
|
+
p.on("error", () => resolve4());
|
|
2248
|
+
} catch {
|
|
2249
|
+
resolve4();
|
|
2250
|
+
}
|
|
2251
|
+
});
|
|
2252
|
+
return true;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
// src/cli/audio/speak.ts
|
|
2256
|
+
var OPTIMIZE = /* @__PURE__ */ new Set(["balanced", "accuracy", "latency", "cost"]);
|
|
2257
|
+
var OPTIONS = {
|
|
2258
|
+
lang: { type: "string" },
|
|
2259
|
+
"optimize-for": { type: "string" },
|
|
2260
|
+
voice: { type: "string" },
|
|
2261
|
+
model: { type: "string" },
|
|
2262
|
+
provider: { type: "string" },
|
|
2263
|
+
speed: { type: "string" },
|
|
2264
|
+
region: { type: "string" },
|
|
2265
|
+
output: { type: "string", short: "o" },
|
|
2266
|
+
format: { type: "string", short: "f" },
|
|
2267
|
+
"no-play": { type: "boolean" },
|
|
2268
|
+
"no-waveform": { type: "boolean" },
|
|
2269
|
+
json: { type: "boolean" },
|
|
2270
|
+
quiet: { type: "boolean", short: "q" }
|
|
2271
|
+
};
|
|
2272
|
+
async function runSpeak(argv, deps = {}) {
|
|
2273
|
+
const stderr = deps.stderr ?? ((l) => process.stderr.write(l + "\n"));
|
|
2274
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
2275
|
+
let values;
|
|
2276
|
+
let positionals;
|
|
2277
|
+
try {
|
|
2278
|
+
const parsed = parseArgs({ args: argv, options: OPTIONS, allowPositionals: true });
|
|
2279
|
+
values = parsed.values;
|
|
2280
|
+
positionals = parsed.positionals;
|
|
2281
|
+
} catch (e) {
|
|
2282
|
+
stderr(`speak: ${e.message}`);
|
|
2283
|
+
return 2;
|
|
2284
|
+
}
|
|
2285
|
+
const stdinIsTTY = deps.stdinIsTTY ?? Boolean(process.stdin.isTTY);
|
|
2286
|
+
let text = positionals.join(" ").trim();
|
|
2287
|
+
if (!text && !stdinIsTTY) {
|
|
2288
|
+
text = (await (deps.readStdin ?? readStdinText)()).trim();
|
|
2289
|
+
}
|
|
2290
|
+
if (!text) {
|
|
2291
|
+
stderr('speak: no text given. usage: speko-calls audio speak "your text" (or pipe text via stdin)');
|
|
2292
|
+
return 2;
|
|
2293
|
+
}
|
|
2294
|
+
const optimizeFor = values["optimize-for"];
|
|
2295
|
+
if (optimizeFor && !OPTIMIZE.has(optimizeFor)) {
|
|
2296
|
+
stderr(`speak: --optimize-for must be one of ${[...OPTIMIZE].join(" | ")}`);
|
|
2297
|
+
return 2;
|
|
2298
|
+
}
|
|
2299
|
+
let speed;
|
|
2300
|
+
if (values.speed !== void 0) {
|
|
2301
|
+
speed = Number(values.speed);
|
|
2302
|
+
if (!Number.isFinite(speed) || speed <= 0) {
|
|
2303
|
+
stderr("speak: --speed must be a positive number");
|
|
2304
|
+
return 2;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
const opts = { language: values.lang || "en" };
|
|
2308
|
+
if (optimizeFor) opts.optimizeFor = optimizeFor;
|
|
2309
|
+
if (values.region) opts.region = values.region;
|
|
2310
|
+
if (values.voice) opts.voice = values.voice;
|
|
2311
|
+
if (values.model) opts.model = values.model;
|
|
2312
|
+
if (speed !== void 0) opts.speed = speed;
|
|
2313
|
+
if (values.provider) opts.constraints = { allowedProviders: { tts: [values.provider] } };
|
|
2314
|
+
let speko = deps.speko;
|
|
2315
|
+
if (!speko) {
|
|
2316
|
+
try {
|
|
2317
|
+
speko = makeSpeko();
|
|
2318
|
+
} catch (e) {
|
|
2319
|
+
stderr(e instanceof MissingKeyError ? e.message : `speak: ${e.message}`);
|
|
2320
|
+
return 1;
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
let result;
|
|
2324
|
+
try {
|
|
2325
|
+
result = await speko.synthesize(text, opts);
|
|
2326
|
+
} catch (e) {
|
|
2327
|
+
stderr(`speak failed: ${e.message}`);
|
|
2328
|
+
return 1;
|
|
2329
|
+
}
|
|
2330
|
+
const { bytes, ext: derivedExt } = toPlayable(result.audio, result.contentType);
|
|
2331
|
+
const ext = values.format || derivedExt;
|
|
2332
|
+
const routed = `via ${result.provider}:${result.model} \xB7 failover ${result.failoverCount}`;
|
|
2333
|
+
const isTTY = deps.isTTY ?? Boolean(process.stdout.isTTY);
|
|
2334
|
+
let outIsDir = false;
|
|
2335
|
+
if (values.output) {
|
|
2336
|
+
try {
|
|
2337
|
+
outIsDir = statSync(values.output).isDirectory();
|
|
2338
|
+
} catch {
|
|
2339
|
+
outIsDir = false;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
const target = resolveOutTarget({
|
|
2343
|
+
out: values.output,
|
|
2344
|
+
outIsDir,
|
|
2345
|
+
isTTY,
|
|
2346
|
+
ext,
|
|
2347
|
+
id: deps.id ?? randomId(),
|
|
2348
|
+
outputDir: process.env.SPEKO_OUTPUT_DIR,
|
|
2349
|
+
cwd: deps.cwd ?? process.cwd()
|
|
2350
|
+
});
|
|
2351
|
+
if (target.mode === "stdout") {
|
|
2352
|
+
stdout.write(bytes);
|
|
2353
|
+
if (!values.quiet) stderr(routed);
|
|
2354
|
+
return 0;
|
|
2355
|
+
}
|
|
2356
|
+
const path = resolvePath(target.path);
|
|
2357
|
+
(deps.writeFile ?? ((p, b) => writeFileSync2(p, b)))(path, bytes);
|
|
2358
|
+
if (values.json) {
|
|
2359
|
+
stdout.write(
|
|
2360
|
+
JSON.stringify({
|
|
2361
|
+
file: path,
|
|
2362
|
+
provider: result.provider,
|
|
2363
|
+
model: result.model,
|
|
2364
|
+
contentType: result.contentType,
|
|
2365
|
+
failoverCount: result.failoverCount
|
|
2366
|
+
}) + "\n"
|
|
2367
|
+
);
|
|
2368
|
+
} else if (!values.quiet) {
|
|
2369
|
+
stderr(`\u2713 ${path} (${routed})`);
|
|
2370
|
+
}
|
|
2371
|
+
if (isTTY && !values["no-play"]) {
|
|
2372
|
+
let played = false;
|
|
2373
|
+
try {
|
|
2374
|
+
played = await (deps.play ?? ((p) => playFile(p)))(path);
|
|
2375
|
+
} catch {
|
|
2376
|
+
played = false;
|
|
2377
|
+
}
|
|
2378
|
+
if (!played && !values.quiet) stderr("(no audio player on PATH \u2014 saved the file above)");
|
|
2379
|
+
}
|
|
2380
|
+
return 0;
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
// src/cli/audio/transcribe.ts
|
|
2384
|
+
import { parseArgs as parseArgs2 } from "util";
|
|
2385
|
+
import { readFileSync as readFileSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
2386
|
+
import { resolve as resolvePath2 } from "path";
|
|
2387
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2388
|
+
var OPTIMIZE2 = /* @__PURE__ */ new Set(["balanced", "accuracy", "latency", "cost"]);
|
|
2389
|
+
var OPTIONS2 = {
|
|
2390
|
+
lang: { type: "string" },
|
|
2391
|
+
"optimize-for": { type: "string" },
|
|
2392
|
+
"content-type": { type: "string" },
|
|
2393
|
+
keywords: { type: "string" },
|
|
2394
|
+
provider: { type: "string" },
|
|
2395
|
+
output: { type: "string", short: "o" },
|
|
2396
|
+
format: { type: "string", short: "f" },
|
|
2397
|
+
json: { type: "boolean" },
|
|
2398
|
+
quiet: { type: "boolean", short: "q" }
|
|
2399
|
+
};
|
|
2400
|
+
async function defaultFetch(url) {
|
|
2401
|
+
const r = await fetch(url);
|
|
2402
|
+
if (!r.ok) throw new Error(`fetch ${url} \u2192 HTTP ${r.status}`);
|
|
2403
|
+
return new Uint8Array(await r.arrayBuffer());
|
|
2404
|
+
}
|
|
2405
|
+
async function runTranscribe(argv, deps = {}) {
|
|
2406
|
+
const stderr = deps.stderr ?? ((l) => process.stderr.write(l + "\n"));
|
|
2407
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
2408
|
+
let values;
|
|
2409
|
+
let positionals;
|
|
2410
|
+
try {
|
|
2411
|
+
const parsed = parseArgs2({ args: argv, options: OPTIONS2, allowPositionals: true });
|
|
2412
|
+
values = parsed.values;
|
|
2413
|
+
positionals = parsed.positionals;
|
|
2414
|
+
} catch (e) {
|
|
2415
|
+
stderr(`transcribe: ${e.message}`);
|
|
2416
|
+
return 2;
|
|
2417
|
+
}
|
|
2418
|
+
const input = positionals[0];
|
|
2419
|
+
const stdinIsTTY = deps.stdinIsTTY ?? Boolean(process.stdin.isTTY);
|
|
2420
|
+
if (!input && stdinIsTTY) {
|
|
2421
|
+
stderr("transcribe: no input. usage: speko-calls audio transcribe <file|url> (or pipe audio via stdin)");
|
|
2422
|
+
return 2;
|
|
2423
|
+
}
|
|
2424
|
+
const optimizeFor = values["optimize-for"];
|
|
2425
|
+
if (optimizeFor && !OPTIMIZE2.has(optimizeFor)) {
|
|
2426
|
+
stderr(`transcribe: --optimize-for must be one of ${[...OPTIMIZE2].join(" | ")}`);
|
|
2427
|
+
return 2;
|
|
2428
|
+
}
|
|
2429
|
+
let audio;
|
|
2430
|
+
let sourceForCt;
|
|
2431
|
+
try {
|
|
2432
|
+
if (input) {
|
|
2433
|
+
if (/^https?:\/\//i.test(input)) {
|
|
2434
|
+
audio = await (deps.fetchUrl ?? defaultFetch)(input);
|
|
2435
|
+
sourceForCt = input;
|
|
2436
|
+
} else {
|
|
2437
|
+
const path = input.startsWith("file://") ? fileURLToPath3(input) : input;
|
|
2438
|
+
audio = (deps.readFile ?? ((p) => readFileSync2(p)))(path);
|
|
2439
|
+
sourceForCt = path;
|
|
2440
|
+
}
|
|
2441
|
+
} else {
|
|
2442
|
+
audio = await (deps.readStdin ?? readStdinBytes)();
|
|
2443
|
+
}
|
|
2444
|
+
} catch (e) {
|
|
2445
|
+
stderr(`transcribe: could not read audio: ${e.message}`);
|
|
2446
|
+
return 1;
|
|
2447
|
+
}
|
|
2448
|
+
if (!audio || audio.length === 0) {
|
|
2449
|
+
stderr("transcribe: empty audio input");
|
|
2450
|
+
return 2;
|
|
2451
|
+
}
|
|
2452
|
+
const contentType = values["content-type"] || (sourceForCt ? guessAudioContentType(sourceForCt) : void 0);
|
|
2453
|
+
const opts = { language: values.lang || "en" };
|
|
2454
|
+
if (optimizeFor) opts.optimizeFor = optimizeFor;
|
|
2455
|
+
if (contentType) opts.contentType = contentType;
|
|
2456
|
+
if (values.keywords) {
|
|
2457
|
+
const kw = values.keywords.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2458
|
+
if (kw.length) opts.keywords = kw;
|
|
2459
|
+
}
|
|
2460
|
+
if (values.provider) opts.constraints = { allowedProviders: { stt: [values.provider] } };
|
|
2461
|
+
let speko = deps.speko;
|
|
2462
|
+
if (!speko) {
|
|
2463
|
+
try {
|
|
2464
|
+
speko = makeSpeko();
|
|
2465
|
+
} catch (e) {
|
|
2466
|
+
stderr(e instanceof MissingKeyError ? e.message : `transcribe: ${e.message}`);
|
|
2467
|
+
return 1;
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
let result;
|
|
2471
|
+
try {
|
|
2472
|
+
result = await speko.transcribe(audio, opts);
|
|
2473
|
+
} catch (e) {
|
|
2474
|
+
stderr(`transcribe failed: ${e.message}`);
|
|
2475
|
+
return 1;
|
|
2476
|
+
}
|
|
2477
|
+
const text = result.text ?? "";
|
|
2478
|
+
const conf = typeof result.confidence === "number" ? ` \xB7 conf ${result.confidence.toFixed(2)}` : "";
|
|
2479
|
+
const routed = `via ${result.provider}:${result.model}${conf} \xB7 failover ${result.failoverCount}`;
|
|
2480
|
+
const isTTY = deps.isTTY ?? Boolean(process.stdout.isTTY);
|
|
2481
|
+
let outIsDir = false;
|
|
2482
|
+
if (values.output) {
|
|
2483
|
+
try {
|
|
2484
|
+
outIsDir = statSync2(values.output).isDirectory();
|
|
2485
|
+
} catch {
|
|
2486
|
+
outIsDir = false;
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
const ext = values.format === "md" ? "md" : "txt";
|
|
2490
|
+
const target = resolveOutTarget({
|
|
2491
|
+
out: values.output,
|
|
2492
|
+
outIsDir,
|
|
2493
|
+
isTTY,
|
|
2494
|
+
ext,
|
|
2495
|
+
id: deps.id ?? randomId(),
|
|
2496
|
+
outputDir: process.env.SPEKO_OUTPUT_DIR,
|
|
2497
|
+
cwd: deps.cwd ?? process.cwd()
|
|
2498
|
+
});
|
|
2499
|
+
let writtenPath;
|
|
2500
|
+
if (target.mode === "file" && (Boolean(values.output) || !values.json)) {
|
|
2501
|
+
writtenPath = resolvePath2(target.path);
|
|
2502
|
+
(deps.writeFile ?? ((p, t) => writeFileSync3(p, t)))(writtenPath, text);
|
|
2503
|
+
}
|
|
2504
|
+
if (values.json) {
|
|
2505
|
+
stdout.write(
|
|
2506
|
+
JSON.stringify({
|
|
2507
|
+
text,
|
|
2508
|
+
provider: result.provider,
|
|
2509
|
+
model: result.model,
|
|
2510
|
+
confidence: result.confidence,
|
|
2511
|
+
failoverCount: result.failoverCount,
|
|
2512
|
+
...writtenPath ? { file: writtenPath } : {}
|
|
2513
|
+
}) + "\n"
|
|
2514
|
+
);
|
|
2515
|
+
return 0;
|
|
2516
|
+
}
|
|
2517
|
+
stdout.write(text.endsWith("\n") ? text : text + "\n");
|
|
2518
|
+
if (writtenPath && !values.quiet) stderr(`\u2713 ${writtenPath} (${routed})`);
|
|
2519
|
+
else if (!values.quiet) stderr(routed);
|
|
2520
|
+
return 0;
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
// src/cli/audio/index.ts
|
|
2524
|
+
var HELP = 'speko-calls audio \u2014 voice from your terminal (Speko auto-routes to the best provider)\n\nUsage:\n speko-calls audio speak "<text>" [--voice <id>] [--optimize-for latency|balanced|accuracy|cost]\n [--provider <p>] [--model <m>] [--speed <n>] [--lang <code>]\n [-o <out>] [--format wav|mp3] [--no-play] [--json] [-q]\n speko-calls audio transcribe <file|url|-> [--lang <code>] [--keywords a,b,c] [--content-type <mime>]\n [--optimize-for ...] [--provider <p>] [-o <out>] [--format txt|md] [--json] [-q]\n\nPipes:\n echo "ship it" | speko-calls audio speak\n cat rec.wav | speko-calls audio transcribe\n speko-calls audio speak "read this back" | speko-calls audio transcribe\n';
|
|
2525
|
+
async function runAudio(argv) {
|
|
2526
|
+
const sub = argv[0];
|
|
2527
|
+
if (sub === "speak") return runSpeak(argv.slice(1));
|
|
2528
|
+
if (sub === "transcribe") return runTranscribe(argv.slice(1));
|
|
2529
|
+
if (!sub || sub === "--help" || sub === "-h") {
|
|
2530
|
+
process.stderr.write(HELP);
|
|
2531
|
+
return sub ? 0 : 1;
|
|
2532
|
+
}
|
|
2533
|
+
process.stderr.write(`speko-calls audio: unknown subcommand '${sub}'. try: speak | transcribe
|
|
2534
|
+
`);
|
|
2535
|
+
return 2;
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
// src/cli/voices.ts
|
|
2539
|
+
import { parseArgs as parseArgs3 } from "util";
|
|
2540
|
+
var OPTIONS3 = {
|
|
2541
|
+
provider: { type: "string" },
|
|
2542
|
+
json: { type: "boolean" },
|
|
2543
|
+
quiet: { type: "boolean", short: "q" }
|
|
2544
|
+
};
|
|
2545
|
+
async function runVoices(argv, deps = {}) {
|
|
2546
|
+
const stderr = deps.stderr ?? ((l) => process.stderr.write(l + "\n"));
|
|
2547
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
2548
|
+
let values;
|
|
2549
|
+
try {
|
|
2550
|
+
values = parseArgs3({ args: argv, options: OPTIONS3, allowPositionals: false }).values;
|
|
2551
|
+
} catch (e) {
|
|
2552
|
+
stderr(`voices: ${e.message}`);
|
|
2553
|
+
return 2;
|
|
2554
|
+
}
|
|
2555
|
+
let speko = deps.speko;
|
|
2556
|
+
if (!speko) {
|
|
2557
|
+
try {
|
|
2558
|
+
speko = makeSpeko();
|
|
2559
|
+
} catch (e) {
|
|
2560
|
+
stderr(e instanceof MissingKeyError ? e.message : `voices: ${e.message}`);
|
|
2561
|
+
return 1;
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
let result;
|
|
2565
|
+
try {
|
|
2566
|
+
result = await speko.voices.list(values.provider ? { provider: values.provider } : {});
|
|
2567
|
+
} catch (e) {
|
|
2568
|
+
stderr(`voices failed: ${e.message}`);
|
|
2569
|
+
return 1;
|
|
2570
|
+
}
|
|
2571
|
+
if (values.json) {
|
|
2572
|
+
stdout.write(JSON.stringify(result) + "\n");
|
|
2573
|
+
return 0;
|
|
2574
|
+
}
|
|
2575
|
+
const providers = result.providers ?? [];
|
|
2576
|
+
const voices = result.voices ?? [];
|
|
2577
|
+
const lines = [];
|
|
2578
|
+
if (providers.length) {
|
|
2579
|
+
lines.push("Providers (the router auto-picks the best per --optimize-for):");
|
|
2580
|
+
for (const p of providers) {
|
|
2581
|
+
const models = p.models?.length ? p.models.join(", ") : "-";
|
|
2582
|
+
const note = p.voicesFetchedLive ? " (voices are account-scoped \u2014 pass --voice <id>)" : "";
|
|
2583
|
+
lines.push(` ${p.key.padEnd(14)} ${p.name}${note}`);
|
|
2584
|
+
lines.push(` ${" ".repeat(14)} models: ${models}`);
|
|
2585
|
+
}
|
|
2586
|
+
lines.push("");
|
|
2587
|
+
}
|
|
2588
|
+
if (voices.length) {
|
|
2589
|
+
lines.push(`Voices (${voices.length}):`);
|
|
2590
|
+
lines.push(` ${"vendor".padEnd(14)} ${"id".padEnd(28)} name`);
|
|
2591
|
+
for (const v of voices) {
|
|
2592
|
+
lines.push(` ${v.vendor.padEnd(14)} ${v.id.padEnd(28)} ${v.name}`);
|
|
2593
|
+
}
|
|
2594
|
+
} else {
|
|
2595
|
+
lines.push("No standalone voice ids returned (ElevenLabs voices are account-scoped \u2014 pass --voice <id> to speak).");
|
|
2596
|
+
}
|
|
2597
|
+
stdout.write(lines.join("\n") + "\n");
|
|
2598
|
+
return 0;
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
// src/cli/router.ts
|
|
2602
|
+
var CLI_COMMANDS = [
|
|
2603
|
+
"init",
|
|
2604
|
+
"setup",
|
|
2605
|
+
"login",
|
|
2606
|
+
"audio",
|
|
2607
|
+
"voices",
|
|
2608
|
+
"models",
|
|
2609
|
+
"--help",
|
|
2610
|
+
"-h",
|
|
2611
|
+
"--version",
|
|
2612
|
+
"-V"
|
|
2613
|
+
];
|
|
2614
|
+
function resolveMode(argv) {
|
|
2615
|
+
const cmd = argv[2];
|
|
2616
|
+
if (cmd && CLI_COMMANDS.includes(cmd)) {
|
|
2617
|
+
return { kind: "cli", name: cmd };
|
|
2618
|
+
}
|
|
2619
|
+
return { kind: "server" };
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2081
2622
|
// src/tools/CallMeTool.ts
|
|
2082
2623
|
import { MCPTool } from "mcp-framework";
|
|
2083
2624
|
import { z } from "zod";
|
|
@@ -2108,7 +2649,7 @@ import { MCPTool as MCPTool2 } from "mcp-framework";
|
|
|
2108
2649
|
import { z as z2 } from "zod";
|
|
2109
2650
|
|
|
2110
2651
|
// src/http/serverClient.ts
|
|
2111
|
-
import { randomBytes as
|
|
2652
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
2112
2653
|
var DemoServerError = class extends Error {
|
|
2113
2654
|
name = "DemoServerError";
|
|
2114
2655
|
};
|
|
@@ -2178,7 +2719,7 @@ var InProcessBackend = class {
|
|
|
2178
2719
|
if (!this.ready) {
|
|
2179
2720
|
this.ready = (async () => {
|
|
2180
2721
|
if (!(process.env.SPEKO_DIAL_TOKEN_SECRET ?? "").trim()) {
|
|
2181
|
-
process.env.SPEKO_DIAL_TOKEN_SECRET =
|
|
2722
|
+
process.env.SPEKO_DIAL_TOKEN_SECRET = randomBytes4(32).toString("hex");
|
|
2182
2723
|
}
|
|
2183
2724
|
const core = await Promise.resolve().then(() => (init_core(), core_exports));
|
|
2184
2725
|
const cfg = core.loadConfig();
|
|
@@ -2586,15 +3127,54 @@ var MakeCallTool = class extends MCPTool6 {
|
|
|
2586
3127
|
};
|
|
2587
3128
|
|
|
2588
3129
|
// src/index.ts
|
|
2589
|
-
var
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
3130
|
+
var VERSION = "0.4.6";
|
|
3131
|
+
function printHelp() {
|
|
3132
|
+
process.stderr.write(
|
|
3133
|
+
`speko-calls ${VERSION} \u2014 call real businesses + speak/transcribe from your terminal; also an MCP server for coding agents.
|
|
3134
|
+
|
|
3135
|
+
Usage:
|
|
3136
|
+
speko-calls start the MCP stdio server (Claude Code, etc.)
|
|
3137
|
+
speko-calls init | setup | login onboarding & auth
|
|
3138
|
+
speko-calls audio speak "<text>" text-to-speech (TTS)
|
|
3139
|
+
speko-calls audio transcribe <f|-> speech-to-text (STT)
|
|
3140
|
+
speko-calls voices [--provider <p>] list available voices
|
|
3141
|
+
speko-calls --help | --version
|
|
3142
|
+
`
|
|
3143
|
+
);
|
|
3144
|
+
return 0;
|
|
3145
|
+
}
|
|
3146
|
+
function printVersion() {
|
|
3147
|
+
process.stdout.write(VERSION + "\n");
|
|
3148
|
+
return 0;
|
|
3149
|
+
}
|
|
3150
|
+
var rest = process.argv.slice(3);
|
|
3151
|
+
var CLI = {
|
|
3152
|
+
init: async () => (await runInit(rest, "init"), 0),
|
|
3153
|
+
setup: async () => (await runInit(rest, "setup"), 0),
|
|
3154
|
+
login: async () => (await runInit(rest, "login"), 0),
|
|
3155
|
+
audio: () => runAudio(rest),
|
|
3156
|
+
voices: () => runVoices(rest),
|
|
3157
|
+
models: () => runVoices(rest),
|
|
3158
|
+
"--help": printHelp,
|
|
3159
|
+
"-h": printHelp,
|
|
3160
|
+
"--version": printVersion,
|
|
3161
|
+
"-V": printVersion
|
|
3162
|
+
};
|
|
3163
|
+
var mode = resolveMode(process.argv);
|
|
3164
|
+
if (mode.kind === "cli") {
|
|
3165
|
+
try {
|
|
3166
|
+
const code = await CLI[mode.name]();
|
|
3167
|
+
process.exit(typeof code === "number" ? code : 0);
|
|
3168
|
+
} catch (e) {
|
|
3169
|
+
process.stderr.write(`${mode.name}: ${e.message}
|
|
3170
|
+
`);
|
|
3171
|
+
process.exit(1);
|
|
3172
|
+
}
|
|
2593
3173
|
}
|
|
2594
3174
|
loadEnv();
|
|
2595
3175
|
var server = new MCPServer({
|
|
2596
3176
|
name: "speko-calls",
|
|
2597
|
-
version:
|
|
3177
|
+
version: VERSION,
|
|
2598
3178
|
transport: { type: "stdio" }
|
|
2599
3179
|
});
|
|
2600
3180
|
server.addTool(LookupBusinessTool);
|