@spekoai/mcp-calls 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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 fileURLToPath3 } from "url";
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(fileURLToPath3(import.meta.url));
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 Speko({
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 randomBytes2 } from "crypto";
928
+ import { randomBytes as randomBytes3 } from "crypto";
929
929
  function delimitedBlock(label, content) {
930
- const nonce = randomBytes2(8).toString("hex");
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 cmd2 = p === "darwin" ? "open" : p === "win32" ? "cmd" : "xdg-open";
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(cmd2, args, { stdio: "ignore", detached: true });
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 cmd2 = p === "darwin" ? "open" : p === "win32" ? "cmd" : "xdg-open";
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(cmd2, args, { stdio: "ignore", detached: true });
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, mode = "init") {
1986
+ async function runInit(argv, mode2 = "init") {
1987
1987
  const f = parseFlags(argv);
1988
- const quick = mode === "login";
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,540 @@ 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 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 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 audio \u2014 voice from your terminal (Speko auto-routes to the best provider)\n\nUsage:\n speko 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 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 audio speak\n cat rec.wav | speko audio transcribe\n speko audio speak "read this back" | speko 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 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, opts = {}) {
2615
+ const cmd = argv[2];
2616
+ if (cmd && CLI_COMMANDS.includes(cmd)) {
2617
+ return { kind: "cli", name: cmd };
2618
+ }
2619
+ if (opts.stdinIsTTY) return { kind: "help" };
2620
+ return { kind: "server" };
2621
+ }
2622
+
2081
2623
  // src/tools/CallMeTool.ts
2082
2624
  import { MCPTool } from "mcp-framework";
2083
2625
  import { z } from "zod";
@@ -2108,7 +2650,7 @@ import { MCPTool as MCPTool2 } from "mcp-framework";
2108
2650
  import { z as z2 } from "zod";
2109
2651
 
2110
2652
  // src/http/serverClient.ts
2111
- import { randomBytes as randomBytes3 } from "crypto";
2653
+ import { randomBytes as randomBytes4 } from "crypto";
2112
2654
  var DemoServerError = class extends Error {
2113
2655
  name = "DemoServerError";
2114
2656
  };
@@ -2178,7 +2720,7 @@ var InProcessBackend = class {
2178
2720
  if (!this.ready) {
2179
2721
  this.ready = (async () => {
2180
2722
  if (!(process.env.SPEKO_DIAL_TOKEN_SECRET ?? "").trim()) {
2181
- process.env.SPEKO_DIAL_TOKEN_SECRET = randomBytes3(32).toString("hex");
2723
+ process.env.SPEKO_DIAL_TOKEN_SECRET = randomBytes4(32).toString("hex");
2182
2724
  }
2183
2725
  const core = await Promise.resolve().then(() => (init_core(), core_exports));
2184
2726
  const cfg = core.loadConfig();
@@ -2586,15 +3128,58 @@ var MakeCallTool = class extends MCPTool6 {
2586
3128
  };
2587
3129
 
2588
3130
  // src/index.ts
2589
- var cmd = process.argv[2];
2590
- if (cmd === "init" || cmd === "setup" || cmd === "login") {
2591
- await runInit(process.argv.slice(3), cmd);
3131
+ var VERSION = "0.4.7";
3132
+ function printHelp() {
3133
+ process.stderr.write(
3134
+ `speko ${VERSION} \u2014 call real businesses + speak/transcribe from your terminal; also an MCP server for coding agents.
3135
+
3136
+ Usage:
3137
+ speko (when launched by an MCP host) the stdio MCP server
3138
+ speko init | setup | login onboarding & auth
3139
+ speko audio speak "<text>" text-to-speech (TTS)
3140
+ speko audio transcribe <f|-> speech-to-text (STT)
3141
+ speko voices [--provider <p>] list available voices
3142
+ speko --help | --version
3143
+ `
3144
+ );
3145
+ return 0;
3146
+ }
3147
+ function printVersion() {
3148
+ process.stdout.write(VERSION + "\n");
3149
+ return 0;
3150
+ }
3151
+ var rest = process.argv.slice(3);
3152
+ var CLI = {
3153
+ init: async () => (await runInit(rest, "init"), 0),
3154
+ setup: async () => (await runInit(rest, "setup"), 0),
3155
+ login: async () => (await runInit(rest, "login"), 0),
3156
+ audio: () => runAudio(rest),
3157
+ voices: () => runVoices(rest),
3158
+ models: () => runVoices(rest),
3159
+ "--help": printHelp,
3160
+ "-h": printHelp,
3161
+ "--version": printVersion,
3162
+ "-V": printVersion
3163
+ };
3164
+ var mode = resolveMode(process.argv, { stdinIsTTY: Boolean(process.stdin.isTTY) });
3165
+ if (mode.kind === "cli") {
3166
+ try {
3167
+ const code = await CLI[mode.name]();
3168
+ process.exit(typeof code === "number" ? code : 0);
3169
+ } catch (e) {
3170
+ process.stderr.write(`${mode.name}: ${e.message}
3171
+ `);
3172
+ process.exit(1);
3173
+ }
3174
+ }
3175
+ if (mode.kind === "help") {
3176
+ printHelp();
2592
3177
  process.exit(0);
2593
3178
  }
2594
3179
  loadEnv();
2595
3180
  var server = new MCPServer({
2596
3181
  name: "speko-calls",
2597
- version: "0.4.5",
3182
+ version: VERSION,
2598
3183
  transport: { type: "stdio" }
2599
3184
  });
2600
3185
  server.addTool(LookupBusinessTool);