@pyai/sdk 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,433 @@
1
+ import { constants, type Stats } from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import { basename, dirname, isAbsolute, join, parse, resolve } from "node:path";
4
+
5
+ export type CliTemplate = "agent" | "typescript" | "python";
6
+ export interface CliTemplateManifest {
7
+ id: CliTemplate;
8
+ description: string;
9
+ files: readonly string[];
10
+ }
11
+ export interface ScaffoldOptions {
12
+ directory: string;
13
+ template?: CliTemplate;
14
+ dryRun?: boolean;
15
+ cwd?: string;
16
+ }
17
+ export interface ScaffoldResult {
18
+ directory: string;
19
+ template: CliTemplate;
20
+ files: string[];
21
+ created: boolean;
22
+ next_steps: string[];
23
+ }
24
+ export class CliInitError extends Error {
25
+ readonly code: string;
26
+ constructor(code: string, message: string) {
27
+ super(message);
28
+ this.name = "CliInitError";
29
+ this.code = code;
30
+ }
31
+ }
32
+
33
+ const context = `# Build with PyAI
34
+
35
+ This file is a project-local API integration guide for engineers and coding agents.
36
+ Read it before generating PyAI calls. Do not place credentials in this file.
37
+
38
+ ## Discover the contract
39
+
40
+ - Run \`pyai schema --json\` for the installed CLI's offline commands, flags,
41
+ routes, output conventions, and exit codes.
42
+ - Run \`pyai schema --openapi --json\` to retrieve the deployed API contract.
43
+ - Canonical live OpenAPI: https://api.pyai.com/openapi.json
44
+ - API docs: https://docs.pyai.com
45
+ - Agent index: https://api.pyai.com/llms.txt
46
+ - Speech and calling recipes: https://pyai.com/agents/speech-calling.md
47
+ - Omni frame contract: https://api.pyai.com/omni-frames.json
48
+ - API origin: https://api.pyai.com; REST routes below include /v1.
49
+
50
+ CLI schema describes the installed executable; live OpenAPI describes the
51
+ currently deployed API. Verify live fields before building a new integration.
52
+ Use \`pyai request METHOD /v1/path --data @request.json --json\` for JSON routes
53
+ that do not yet have a dedicated command. Do not invent endpoint aliases.
54
+
55
+ ## Authentication and secrets
56
+
57
+ For human use: \`pyai auth login\` opens browser approval; add \`--no-browser\`
58
+ for SSH. Match the terminal code, select a project, and approve. Browser login
59
+ requires the matching backend and console deployment. Use an existing key
60
+ through \`PYAI_API_KEY\` when web login is unavailable.
61
+
62
+ For unattended agents and CI: inject \`PYAI_API_KEY\` from a secret manager.
63
+ \`pyai auth sandbox --profile sandbox\` creates a separate sandbox organization
64
+ and saves its key locally; each invocation creates a new organization.
65
+ Inspect \`pyai auth status --json\` before making product calls. Sandbox scopes,
66
+ limits, and expiry come from the returned metadata; live calls may incur usage.
67
+
68
+ Keys are opaque strings. Never parse them, print them, commit them, put them in
69
+ URLs, expose them in browser code, or include them in prompts or issue reports.
70
+ Use the bearer Authorization header for server-side REST. CLI profiles are
71
+ plaintext private local files; SDK examples read the environment and do not
72
+ read CLI profiles. An exported PYAI_API_KEY overrides the CLI's saved profile.
73
+ \`auth logout\` removes a local profile; revoke the key in the console separately.
74
+
75
+ ## Canonical operations
76
+
77
+ | Task | API | CLI |
78
+ | --- | --- | --- |
79
+ | List voices | GET /v1/voices | pyai voices --json |
80
+ | List models | GET /v1/models | pyai models --json |
81
+ | Speak | POST /v1/audio/speech | pyai speak --text-file message.txt --out speech.wav --json |
82
+ | Hear, local audio | POST /v1/audio/transcriptions (multipart file) | pyai transcribe --file recording.wav --json |
83
+ | Transcription jobs | POST/GET /v1/transcription/jobs | pyai transcribe --url HTTPS_URL --wait --json |
84
+ | Managed Agents | /v1/agents | pyai agents list --json |
85
+ | Voice cloning | /v1/voice/clones | pyai clones list --json |
86
+ | Voice design | /v1/voice/design | pyai design --help |
87
+ | Cast | /v1/cast/* | pyai cast --help |
88
+ | Dub | POST /v1/dub; /v1/dub/jobs/{job_id} | pyai dub --help |
89
+ | Hear vocabulary | GET/PUT /v1/hear/vocabulary | pyai vocabulary --help |
90
+
91
+ Speak uses model \`pyai-speak\`, input text, and a voice from the catalog;
92
+ \`alloy\` is an accepted compatibility voice. The speech example requests WAV
93
+ and buffered delivery. Hear uses multipart file upload and model \`pyai-hear\`.
94
+ Never send local filesystem paths as remote audio URLs. Request examples use
95
+ placeholder HTTPS URLs: replace them with accessible audio you are authorized
96
+ to process before submission. Synchronous Hear's language is an STT hint;
97
+ async jobs' language field is a Recap summarization hint.
98
+
99
+ ## Files, jobs, retries, and errors
100
+
101
+ - Prefer \`--json\` for automation. Success is stdout, structured errors are
102
+ stderr, and the process exits nonzero on failure. Browser login may emit a
103
+ public authorization event to stderr while waiting for approval.
104
+ - Audio is binary. Use \`--out audio.wav --json\` for an output receipt, or
105
+ \`--out -\` for a binary pipe without \`--json\`. Output files are protected
106
+ from overwrites unless the user explicitly requests \`--force\`.
107
+ - Preview supported calls with \`--dry-run --json\`; this does not prove that
108
+ a deployed API accepts a payload or that the key has permission.
109
+ - Use \`--wait --wait-timeout 300\` when creating URL transcription jobs, or
110
+ \`pyai jobs wait JOB_ID --wait-timeout 300 --json\` to resume bounded polling.
111
+ A local timeout does not cancel a server-side job. Save the returned job ID.
112
+ - Reuse an \`--idempotency-key\` for retries of the same operation only on
113
+ routes whose live contract supports it (for example transcription jobs).
114
+ Do not assume every POST is idempotent or automatically repeat billable
115
+ calls after an ambiguous network failure. These SDK starters disable
116
+ automatic retries; deliberately decide whether a failed call is safe to repeat.
117
+ - Read structured error codes, HTTP status, and request IDs. Fix 400 input,
118
+ 401 authentication, 403 scope, and 402 credit errors before retrying. Honor
119
+ Retry-After on 429 responses and use bounded backoff for safe operations.
120
+ - Keep project boundaries explicit. Use the intended \`--profile NAME\` and
121
+ verify its organization/project through auth status before changing resources.
122
+
123
+ ## Realtime is an SDK/application workflow
124
+
125
+ The CLI handles REST and job workflows; it does not run a realtime microphone
126
+ or WebSocket session. Use the SDK and protocol docs for Hear streaming or Omni.
127
+ Omni connects to \`wss://api.pyai.com/v1/omni?format=pcm16&rate=24000\`.
128
+ Use \`pyai.omni.connect({ rate, configure })\`; a managed Agent is optional.
129
+ Wait for configured before non-silent audio and clear playback on barge-in/flush. Never place
130
+ long-lived API keys in a public frontend. Use the documented ephemeral session
131
+ flow for browser apps. Do not add unsupported grounding/Cue fields.
132
+
133
+ ## Suggested workflow
134
+
135
+ 1. Read this file and inspect CLI/schema help.
136
+ 2. Authenticate, inspect the intended project, and choose catalog voice IDs.
137
+ 3. Preview a request, then run one small product call with authorized input.
138
+ 4. Save useful job IDs and request IDs; keep generated audio and secrets out of git.
139
+ 5. Add bounded error handling and tests before expanding the integration.
140
+ `;
141
+
142
+ const speech = `${JSON.stringify({ model: "pyai-speak", input: "Hello from PyAI.", voice: "alloy", response_format: "wav", stream: false }, null, 2)}\n`;
143
+ const job = `${JSON.stringify({ audio_url: "https://example.com/replace-with-your-recording.wav", model: "pyai-hear", output_formats: ["json", "srt"] }, null, 2)}\n`;
144
+ const agent = `${JSON.stringify({ name: "Support assistant", persona_system_prompt: "Help the caller clearly and briefly. Ask one question at a time.", greeting: "Hello, how can I help you?", voice_id: "stock_emma_en_gb", language: "en" }, null, 2)}\n`;
145
+ const env = "# Set locally or inject through your secret manager. Never commit the real key.\nPYAI_API_KEY=\nPYAI_BASE_URL=https://api.pyai.com\nPYAI_VOICE=alloy\n";
146
+ const ignore = ".env\n.env.*\n!.env.example\nnode_modules/\n.venv/\n__pycache__/\n*.pyc\n*.wav\n*.mp3\n*.raw\ntranscript.json\n";
147
+ const install = `## Install the CLI
148
+
149
+ Install the published CLI with Node.js 22 or newer recommended:
150
+
151
+ \`\`\`bash
152
+ npm install -g @pyai/sdk@0.6.0
153
+ pyai --version
154
+ \`\`\`
155
+
156
+ The Python SDK can also provide a command named pyai. Check
157
+ \`pyai --version\` and \`pyai schema --json\` to confirm this executable.
158
+ For development, build with \`npm ci && npm run build\` in the platform's
159
+ sdk/typescript directory and run \`node dist/cli.js\` explicitly.
160
+
161
+ Use \`pyai login\` for browser sign-in (or \`pyai login --no-browser\`
162
+ over SSH). For automation, inject \`PYAI_API_KEY\` and verify
163
+ \`pyai whoami --json\`.
164
+ `;
165
+ const agentReadme = `# PyAI agent starter
166
+
167
+ This project contains API context and editable JSON requests. Initialization
168
+ is offline: it creates files only and does not install packages, authenticate,
169
+ create an organization, or make a product call.
170
+
171
+ ${install}
172
+ ## Make the first calls
173
+
174
+ \`\`\`bash
175
+ pyai schema --json
176
+ pyai voices --json
177
+ pyai request POST /v1/audio/speech --data @speech.json --out hello.wav --dry-run --json
178
+ pyai request POST /v1/audio/speech --data @speech.json --out hello.wav --json
179
+ pyai transcribe --file hello.wav --json
180
+ \`\`\`
181
+
182
+ Edit \`job.json\` to use an accessible HTTPS recording URL, then submit a job
183
+ with \`pyai jobs create --data @job.json --json\`. Wait on its returned ID with
184
+ \`pyai jobs wait JOB_ID --wait-timeout 300 --json\`.
185
+
186
+ Edit \`agent.json\` for your use case and confirm the voice in the live catalog.
187
+ Preview it with \`pyai agents create --data @agent.json --dry-run --json\`, then
188
+ remove \`--dry-run\` to create a managed Agent profile. This creates a profile;
189
+ use the SDK and Omni protocol docs to build the realtime application.
190
+
191
+ Point Cursor, Claude Code, Codex, or another coding agent at \`PYAI.md\`.
192
+ Inspect the installed command schema instead of guessing flags or endpoints.
193
+ `;
194
+ const tsMain = `import { open, readFile, unlink } from "node:fs/promises";
195
+ import { basename } from "node:path";
196
+ import { PyAI, PyAIError } from "@pyai/sdk";
197
+
198
+ async function main() {
199
+ const [command, input, output = "hello.wav"] = process.argv.slice(2);
200
+ if ((command !== "speak" && command !== "hear") || !input) {
201
+ throw new Error('Usage: npm run speak -- "Hello" [hello.wav] OR npm run hear -- recording.wav');
202
+ }
203
+ const apiKey = process.env.PYAI_API_KEY;
204
+ if (!apiKey) throw new Error("Set PYAI_API_KEY in your environment or ignored .env file.");
205
+ const client = new PyAI({ apiKey, baseURL: process.env.PYAI_BASE_URL || "https://api.pyai.com", maxRetries: 0 });
206
+ if (command === "speak") {
207
+ // Reserve a new private output before making a billable request.
208
+ const file = await open(output, "wx", 0o600);
209
+ try {
210
+ const audio = await client.audio.speech({ input, voice: process.env.PYAI_VOICE || "alloy", response_format: "wav", stream: false });
211
+ await file.writeFile(new Uint8Array(audio));
212
+ } catch (error) {
213
+ await file.close();
214
+ await unlink(output);
215
+ throw error;
216
+ }
217
+ await file.close();
218
+ process.stdout.write(JSON.stringify({ output }) + "\\n");
219
+ } else {
220
+ const bytes = await readFile(input);
221
+ const result = await client.audio.transcriptions.create({ file: new Blob([bytes]), filename: basename(input), model: "pyai-hear" });
222
+ process.stdout.write(JSON.stringify(result) + "\\n");
223
+ }
224
+ }
225
+
226
+ main().catch((error: unknown) => {
227
+ // API error messages can reflect request contents; report stable metadata only.
228
+ const detail = error instanceof PyAIError
229
+ ? { code: error.code, status: error.status, request_id: error.requestId }
230
+ : { code: "local_error", message: error instanceof Error && (error.message.startsWith("Usage:") || error.message.startsWith("Set PYAI_API_KEY")) ? error.message : "Check input/output paths and permissions; existing output files are refused." };
231
+ process.stderr.write(JSON.stringify({ error: detail }) + "\\n");
232
+ process.exitCode = 1;
233
+ });
234
+ `;
235
+ const tsReadme = `# PyAI TypeScript starter
236
+
237
+ Requires Node.js 22.6+ and npm. Initialization is offline; dependency installation
238
+ and product calls below are separate explicit steps. Read \`PYAI.md\` first.
239
+
240
+ ## Install the SDK and configure the environment
241
+
242
+ Install the official SDK:
243
+
244
+ \`\`\`bash
245
+ npm install @pyai/sdk@0.6.0
246
+ cp .env.example .env
247
+ \`\`\`
248
+
249
+ Put your key in the ignored \`.env\`, or inject it through a secret manager.
250
+ These scripts read the environment and do not read credentials saved by
251
+ \`pyai login\`.
252
+ An existing environment variable takes precedence over values in \`.env\`.
253
+
254
+ ## Speak, then Hear
255
+
256
+ \`\`\`bash
257
+ npm run speak -- "Hello from PyAI." hello.wav
258
+ npm run hear -- hello.wav
259
+ \`\`\`
260
+
261
+ Speak reserves a new private output file and refuses to overwrite existing
262
+ files. Hear uploads a local audio file and prints JSON. Both make real API
263
+ calls and use your key's scopes, limits, and billing. Automatic SDK retries are
264
+ disabled to avoid repeating ambiguous product calls. Extend \`main.ts\` after
265
+ checking the live contract; add bounded timeouts for your application's needs.
266
+
267
+ ${install}`;
268
+ const pythonMain = `import json
269
+ import os
270
+ from pathlib import Path
271
+ import sys
272
+
273
+ from pyai import PyAI, PyAIError
274
+
275
+
276
+ def main():
277
+ args = sys.argv[1:]
278
+ if len(args) < 2 or args[0] not in ("speak", "hear"):
279
+ raise ValueError('Usage: python main.py speak "Hello" [hello.wav] OR python main.py hear recording.wav')
280
+ api_key = os.environ.get("PYAI_API_KEY")
281
+ if not api_key:
282
+ raise ValueError("Set PYAI_API_KEY in your environment.")
283
+ with PyAI(api_key=api_key, base_url=os.environ.get("PYAI_BASE_URL") or "https://api.pyai.com", max_retries=0, timeout=60) as client:
284
+ if args[0] == "speak":
285
+ output = Path(args[2] if len(args) > 2 else "hello.wav")
286
+ # Exclusive creation reserves the path before a billable request.
287
+ descriptor = os.open(output, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
288
+ try:
289
+ with os.fdopen(descriptor, "wb") as target:
290
+ audio = client.audio.speech(input=args[1], voice=os.environ.get("PYAI_VOICE") or "alloy", response_format="wav")
291
+ target.write(audio)
292
+ except Exception:
293
+ output.unlink(missing_ok=True)
294
+ raise
295
+ print(json.dumps({"output": str(output)}))
296
+ else:
297
+ with open(args[1], "rb") as source:
298
+ result = client.audio.transcriptions.create(file=source, filename=Path(args[1]).name, model="pyai-hear")
299
+ print(json.dumps(result))
300
+
301
+
302
+ if __name__ == "__main__":
303
+ try:
304
+ main()
305
+ except PyAIError as error:
306
+ print(json.dumps({"error": {"code": error.code, "status": error.status, "request_id": error.request_id}}), file=sys.stderr)
307
+ sys.exit(1)
308
+ except (OSError, ValueError) as error:
309
+ message = str(error) if isinstance(error, ValueError) and (str(error).startswith("Usage:") or str(error).startswith("Set PYAI_API_KEY")) else "Check input/output paths and permissions; existing output files are refused."
310
+ print(json.dumps({"error": {"code": "local_error", "message": message}}), file=sys.stderr)
311
+ sys.exit(1)
312
+ `;
313
+ const pythonReadme = `# PyAI Python starter
314
+
315
+ Requires Python 3.9+. Initialization is offline. Dependency installation and
316
+ product calls below are separate explicit steps. Read \`PYAI.md\` first.
317
+
318
+ ## Install the SDK and configure the environment
319
+
320
+ Create a virtual environment with \`python3 -m venv .venv\`, activate it using
321
+ your shell's normal command, and install the matching SDK checkout:
322
+
323
+ \`\`\`bash
324
+ python -m pip install /absolute/path/to/platform/sdk/python
325
+ \`\`\`
326
+
327
+ Inject \`PYAI_API_KEY\` through your secret manager or shell environment. The
328
+ \`.env.example\` file documents supported variables; this script deliberately
329
+ does not load .env files or read credentials saved by \`pyai auth login\`.
330
+ Do not paste a real key into shell history or source control.
331
+
332
+ ## Speak, then Hear
333
+
334
+ \`\`\`bash
335
+ python main.py speak "Hello from PyAI." hello.wav
336
+ python main.py hear hello.wav
337
+ \`\`\`
338
+
339
+ Speak creates a new private file and refuses existing output paths. Hear
340
+ uploads a local file and prints JSON. Calls use your key's scopes, limits,
341
+ and billing. The SDK timeout is 60 seconds and automatic retries are disabled.
342
+ Check the live contract before expanding the example.
343
+
344
+ ${install}`;
345
+
346
+ const content: Readonly<Record<CliTemplate, Readonly<Record<string, string>>>> = Object.freeze({
347
+ agent: Object.freeze({ "README.md": agentReadme, "PYAI.md": context, "speech.json": speech, "job.json": job, "agent.json": agent, ".env.example": env, ".gitignore": ignore }),
348
+ typescript: Object.freeze({ "README.md": tsReadme, "PYAI.md": context, "main.ts": tsMain, "package.json": JSON.stringify({ name: "pyai-starter", version: "0.0.0", private: true, type: "module", engines: { node: ">=22.6.0" }, scripts: { speak: "node --experimental-strip-types --env-file=.env main.ts speak", hear: "node --experimental-strip-types --env-file=.env main.ts hear" } }, null, 2) + "\n", ".env.example": env, ".gitignore": ignore }),
349
+ python: Object.freeze({ "README.md": pythonReadme, "PYAI.md": context, "main.py": pythonMain, ".env.example": env, ".gitignore": ignore }),
350
+ });
351
+
352
+ /** Offline discovery; file contents are static and never include local credentials. */
353
+ export const CLI_TEMPLATES: readonly CliTemplateManifest[] = Object.freeze(([
354
+ { id: "agent", description: "API context and JSON requests for coding agents" },
355
+ { id: "typescript", description: "Runnable Node.js Speak and Hear examples using @pyai/sdk" },
356
+ { id: "python", description: "Runnable Python Speak and Hear examples using pyai-sdk" },
357
+ ] as const).map((entry) => Object.freeze({ ...entry, files: Object.freeze(Object.keys(content[entry.id])) })));
358
+
359
+ function hasCode(error: unknown, code: string): boolean {
360
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
361
+ }
362
+ function sameFile(a: Stats, b: Stats): boolean { return a.dev === b.dev && a.ino === b.ino; }
363
+ async function exists(path: string): Promise<boolean> {
364
+ try { await fs.lstat(path); return true; }
365
+ catch (error) { if (hasCode(error, "ENOENT")) return false; throw error; }
366
+ }
367
+
368
+ /** Create a new project only. Never authenticates, installs packages, or runs a shell. */
369
+ export async function scaffoldProject(options: ScaffoldOptions): Promise<ScaffoldResult> {
370
+ const template = options.template ?? "agent";
371
+ if (!Object.hasOwn(content, template)) throw new CliInitError("invalid_template", "Choose agent, typescript, or python with --template.");
372
+ if (typeof options.directory !== "string" || !options.directory.trim() || /[\p{Cc}]/u.test(options.directory)) {
373
+ throw new CliInitError("invalid_directory", "Provide a new project directory without control characters.");
374
+ }
375
+ const requested = resolve(options.cwd ?? process.cwd(), options.directory);
376
+ if (requested === parse(requested).root) throw new CliInitError("invalid_directory", "The filesystem root cannot be a project destination.");
377
+ let directory: string;
378
+ try {
379
+ if (await exists(requested)) throw new CliInitError("destination_exists", "The destination already exists. Choose a new directory; existing directories, files, and symbolic links are never overwritten.");
380
+ // Resolve parent aliases once (including platform aliases such as /tmp),
381
+ // then use the canonical path consistently for creation and rollback.
382
+ const parent = await fs.realpath(dirname(requested));
383
+ if (!(await fs.lstat(parent)).isDirectory()) throw new CliInitError("invalid_directory", "The destination parent must be an existing directory.");
384
+ directory = join(parent, basename(requested));
385
+ } catch (error) {
386
+ if (error instanceof CliInitError) throw error;
387
+ throw new CliInitError("invalid_directory", "The destination parent must exist and be accessible. Create parent directories first.");
388
+ }
389
+ const entries = Object.entries(content[template]);
390
+ for (const [file] of entries) {
391
+ if (isAbsolute(file) || basename(file) !== file || file === "." || file === "..") throw new CliInitError("invalid_template", "The template contains an unsafe file path.");
392
+ }
393
+ const result: ScaffoldResult = {
394
+ directory, template, files: entries.map(([file]) => file), created: !options.dryRun,
395
+ next_steps: ["Open README.md in the created project for setup and examples.", "Give PYAI.md to your coding agent before building against the API.", "Inspect available commands with pyai schema --json."],
396
+ };
397
+ if (options.dryRun) return result;
398
+
399
+ let ownedDirectory: Stats | undefined;
400
+ const ownedFiles: Array<{ path: string; stat: Stats }> = [];
401
+ try {
402
+ await fs.mkdir(directory, { mode: 0o700 });
403
+ ownedDirectory = await fs.lstat(directory);
404
+ if (!ownedDirectory.isDirectory() || ownedDirectory.isSymbolicLink()) throw new Error("Destination changed");
405
+ for (const [name, text] of entries) {
406
+ const current = await fs.lstat(directory);
407
+ if (!current.isDirectory() || current.isSymbolicLink() || !sameFile(current, ownedDirectory)) throw new Error("Destination changed");
408
+ const path = join(directory, name);
409
+ const file = await fs.open(path, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0), 0o600);
410
+ try {
411
+ ownedFiles.push({ path, stat: await file.stat() });
412
+ await file.writeFile(text, "utf8");
413
+ } finally { await file.close(); }
414
+ }
415
+ return result;
416
+ } catch (error) {
417
+ // No recursive delete: remove only exact files created by this invocation.
418
+ // Preserve another process's additions or replacements, even during failure.
419
+ if (ownedDirectory) {
420
+ try {
421
+ const current = await fs.lstat(directory);
422
+ if (current.isDirectory() && !current.isSymbolicLink() && sameFile(current, ownedDirectory)) {
423
+ for (const owned of ownedFiles.reverse()) {
424
+ try { if (sameFile(await fs.lstat(owned.path), owned.stat)) await fs.unlink(owned.path); } catch { /* preserve unknown paths */ }
425
+ }
426
+ await fs.rmdir(directory).catch(() => undefined);
427
+ }
428
+ } catch { /* no longer our destination */ }
429
+ }
430
+ if (hasCode(error, "EEXIST") && !ownedDirectory) throw new CliInitError("destination_exists", "The destination already exists. Choose a new directory.");
431
+ throw new CliInitError("scaffold_failed", "Unable to create the starter. Check filesystem permissions and free space; any files added by another process were preserved.");
432
+ }
433
+ }
@@ -0,0 +1,88 @@
1
+ /** API-backed commands shared by dispatch, help, and offline discovery. */
2
+ export interface CliRoute {
3
+ command: string;
4
+ method: string;
5
+ path: string;
6
+ description: string;
7
+ body?: boolean;
8
+ id?: boolean;
9
+ binary?: boolean;
10
+ wait?: { success: string[]; failure: string[] };
11
+ query?: Record<string, "string" | "boolean">;
12
+ confirm?: boolean;
13
+ idempotencyRequired?: boolean;
14
+ }
15
+
16
+ export const routes: CliRoute[] = [
17
+ { command: "omni calls", method: "GET", path: "/v1/omni/calls", query: { "session-label": "string", limit: "string", cursor: "string" }, description: "List Omni sessions including inbound calls. Scope omni:read." },
18
+ { command: "omni get", method: "GET", path: "/v1/omni/calls/{id}", id: true, description: "Read an Omni call's transcript, summary and recording availability." },
19
+ { command: "omni summary", method: "GET", path: "/v1/omni/calls/{id}/summary", id: true, description: "Read the structured summary of an Omni session when generated." },
20
+ { command: "omni transcript", method: "GET", path: "/v1/omni/calls/{id}/transcript", id: true, description: "Read the complete Omni session transcript." },
21
+ { command: "omni recording", method: "GET", path: "/v1/omni/calls/{id}/recording", id: true, binary: true, description: "Download the recording with --out; recording must have been enabled for this call." },
22
+ { command: "numbers search", method: "GET", path: "/v1/telephony/available", query: { country: "string", "area-code": "string", contains: "string", limit: "string" }, description: "Search managed numbers; inspect provisioning availability before buying. Scope telephony:manage." },
23
+ { command: "numbers list", method: "GET", path: "/v1/telephony/numbers", query: { "include-released": "boolean" }, description: "List the organization's managed numbers and current Agent bindings." },
24
+ { command: "numbers buy", method: "POST", path: "/v1/telephony/numbers", body: true, confirm: true, idempotencyRequired: true, description: "Purchase a number from JSON. Requires --confirm and --idempotency-key; may incur recurring charges." },
25
+ { command: "numbers bind", method: "POST", path: "/v1/telephony/numbers/{id}/assign", id: true, body: true, description: "Bind an existing number using JSON {agent_id}; null unassigns. Read numbers list first." },
26
+ { command: "calls list", method: "GET", path: "/v1/telephony/calls", description: "List outbound call requests and their normalized status." },
27
+ { command: "calls create", method: "POST", path: "/v1/telephony/calls", body: true, confirm: true, idempotencyRequired: true, description: "Dial a real destination from JSON. Requires --confirm and --idempotency-key; this is not a simulated test call." },
28
+ { command: "calls get", method: "GET", path: "/v1/telephony/calls/{id}", id: true, description: "Get an outbound call by its returned ID; retain artifact identifiers separately." },
29
+ { command: "calls wait", method: "GET", path: "/v1/telephony/calls/{id}", id: true, wait: { success: ["completed"], failure: ["failed", "canceled", "dispatch_unknown"] }, description: "Wait with a deadline. dispatch_unknown stops polling for manual inspection; never redial automatically." },
30
+ { command: "agents list", method: "GET", path: "/v1/agents", description: "List managed Agent profiles." },
31
+ { command: "agents get", method: "GET", path: "/v1/agents/{id}", id: true, description: "Get an Agent profile." },
32
+ { command: "agents create", method: "POST", path: "/v1/agents", body: true, description: "Create an Agent profile from JSON; name is required." },
33
+ { command: "agents update", method: "POST", path: "/v1/agents/{id}", id: true, body: true, description: "Update present Agent fields; null clears a field." },
34
+ { command: "agents delete", method: "DELETE", path: "/v1/agents/{id}", id: true, description: "Delete an Agent profile." },
35
+
36
+ { command: "jobs list", method: "GET", path: "/v1/transcription/jobs", description: "List transcription jobs with --limit and --cursor pagination." },
37
+ { command: "jobs create", method: "POST", path: "/v1/transcription/jobs", body: true, description: "Create a transcription job from JSON with audio_url." },
38
+ { command: "jobs get", method: "GET", path: "/v1/transcription/jobs/{id}", id: true, description: "Get transcription status and result." },
39
+ { command: "jobs cancel", method: "DELETE", path: "/v1/transcription/jobs/{id}", id: true, description: "Cancel a pending transcription job; completed results are retained." },
40
+ { command: "jobs wait", method: "GET", path: "/v1/transcription/jobs/{id}", id: true, wait: { success: ["completed"], failure: ["failed", "cancelled"] }, description: "Wait for a transcription job to finish." },
41
+
42
+ { command: "clones list", method: "GET", path: "/v1/voice/clones", description: "List cloned voices belonging to the current organization." },
43
+ { command: "clones delete", method: "DELETE", path: "/v1/voice/clones/{id}", id: true, description: "Delete a cloned voice." },
44
+ { command: "design create", method: "POST", path: "/v1/voice/design", body: true, description: "Generate voice candidates from a JSON prompt." },
45
+ { command: "design get", method: "GET", path: "/v1/voice/design/{id}", id: true, description: "Get voice design status and candidate previews." },
46
+ { command: "design wait", method: "GET", path: "/v1/voice/design/{id}", id: true, wait: { success: ["completed"], failure: ["failed"] }, description: "Wait for voice design candidates." },
47
+ { command: "design save", method: "POST", path: "/v1/voice/design/{id}/save", id: true, body: true, description: "Save a candidate using candidate_id and name." },
48
+
49
+ // These are the public gateway adapter routes, including its render lifecycle.
50
+ { command: "cast capabilities", method: "GET", path: "/v1/cast/capabilities", description: "Discover currently supported Cast voices and directions." },
51
+ { command: "cast direct", method: "POST", path: "/v1/cast/direct", body: true, description: "Assign emotion and intensity to script text." },
52
+ { command: "cast speech", method: "POST", path: "/v1/cast/speech", body: true, binary: true, description: "Render one directed line to a WAV file." },
53
+ { command: "cast render", method: "POST", path: "/v1/cast/render_jobs", body: true, description: "Create a render with voice and script or directed lines." },
54
+ { command: "cast get", method: "GET", path: "/v1/cast/render_jobs/{id}", id: true, description: "Get Cast render status." },
55
+ { command: "cast wait", method: "GET", path: "/v1/cast/render_jobs/{id}", id: true, wait: { success: ["done"], failure: ["error"] }, description: "Wait for a Cast render to finish." },
56
+ { command: "cast audio", method: "GET", path: "/v1/cast/render_jobs/{id}/audio", id: true, binary: true, description: "Download completed Cast audio to a WAV file." },
57
+
58
+ { command: "dub get", method: "GET", path: "/v1/dub/jobs/{id}", id: true, description: "Get Dub progress and output details." },
59
+ { command: "dub wait", method: "GET", path: "/v1/dub/jobs/{id}", id: true, wait: { success: ["done"], failure: ["error"] }, description: "Wait for a Dub job to finish." },
60
+ { command: "dub audio", method: "GET", path: "/v1/dub/jobs/{id}/audio", id: true, binary: true, description: "Download completed dubbed audio." },
61
+ { command: "dub video", method: "GET", path: "/v1/dub/jobs/{id}/video", id: true, binary: true, description: "Download a completed video-source job's dubbed video." },
62
+
63
+ { command: "recap list", method: "GET", path: "/v1/recap/calls", description: "List calls with Recap summaries." },
64
+ { command: "recap get", method: "GET", path: "/v1/recap/calls/{id}", id: true, description: "Get a call's Recap summary." },
65
+ { command: "recap config", method: "GET", path: "/v1/recap/config", description: "Read Recap configuration." },
66
+ { command: "recap configure", method: "PUT", path: "/v1/recap/config", body: true, description: "Update Recap configuration from JSON." },
67
+
68
+ { command: "trace list", method: "GET", path: "/v1/trace/interactions", description: "List Trace interactions." },
69
+ { command: "trace get", method: "GET", path: "/v1/trace/interactions/{id}", id: true, description: "Get a Trace interaction's findings and timeline." },
70
+ { command: "trace config", method: "GET", path: "/v1/trace/config", description: "Read Trace configuration." },
71
+ { command: "trace configure", method: "PUT", path: "/v1/trace/config", body: true, description: "Update Trace configuration from JSON." },
72
+ { command: "trace findings", method: "GET", path: "/v1/trace/findings", description: "List Trace findings." },
73
+ { command: "trace violations", method: "GET", path: "/v1/trace/violations", description: "List Trace violations." },
74
+ { command: "trace exposure", method: "GET", path: "/v1/trace/exposure", description: "Get aggregate Trace exposure." },
75
+
76
+ { command: "tools list", method: "GET", path: "/v1/tools", description: "List hosted and registered tools." },
77
+ { command: "tools get", method: "GET", path: "/v1/tools/{id}", id: true, description: "Get a tool definition." },
78
+ { command: "tools create", method: "POST", path: "/v1/tools", body: true, description: "Register a tool from JSON." },
79
+ { command: "tools update", method: "POST", path: "/v1/tools/{id}", id: true, body: true, description: "Update a registered tool." },
80
+ { command: "tools delete", method: "DELETE", path: "/v1/tools/{id}", id: true, description: "Delete a registered tool." },
81
+
82
+ { command: "vocabulary get", method: "GET", path: "/v1/hear/vocabulary", description: "Read organization Hear vocabulary and activation profiles." },
83
+ { command: "vocabulary set", method: "PUT", path: "/v1/hear/vocabulary", body: true, description: "Replace terms and enabled_for profiles from JSON." },
84
+ { command: "amd calls", method: "GET", path: "/v1/amd/calls", description: "List answering-machine detection decisions." },
85
+ { command: "amd get", method: "GET", path: "/v1/amd/calls/{id}", id: true, description: "Get one answering-machine detection decision." },
86
+ { command: "amd config", method: "GET", path: "/v1/amd/config", description: "Read answering-machine detection configuration." },
87
+ { command: "amd configure", method: "POST", path: "/v1/amd/config", body: true, description: "Configure AMD aggressiveness and webhook from JSON." },
88
+ ];
@@ -0,0 +1,30 @@
1
+ /** Keep machine-readable errors clean on runtimes with an experimental File. */
2
+ interface WarningRuntime {
3
+ versions: { node: string };
4
+ argv: readonly string[];
5
+ emitWarning: typeof process.emitWarning;
6
+ }
7
+
8
+ const installed = new WeakSet<WarningRuntime>();
9
+ const FILE_WARNING = "buffer.File is an experimental feature and might change at any time";
10
+
11
+ export function installRuntimeCompatibility(runtime: WarningRuntime = process): void {
12
+ const major = Number(runtime.versions.node.split(".", 1)[0]);
13
+ const delimiter = runtime.argv.indexOf("--", 2);
14
+ const args = runtime.argv.slice(2, delimiter < 0 ? undefined : delimiter);
15
+ if (![18, 19].includes(major) || !(args.includes("--json") || args.includes("-j")) || installed.has(runtime)) return;
16
+ const emitWarning = runtime.emitWarning;
17
+ runtime.emitWarning = function (this: unknown, warning: string | Error, ...args: unknown[]): void {
18
+ const message = typeof warning === "string" ? warning : warning instanceof Error ? warning.message : undefined;
19
+ const options = args[0];
20
+ const type = warning instanceof Error ? warning.name : typeof options === "string" ? options
21
+ : options !== null && typeof options === "object" ? (options as { type?: unknown }).type : undefined;
22
+ if (message === FILE_WARNING && type === "ExperimentalWarning") return;
23
+ // Forward untouched arguments so Node retains every emitWarning overload,
24
+ // its validation, warning codes, constructors, and error object identity.
25
+ Reflect.apply(emitWarning, this, [warning, ...args]);
26
+ } as typeof process.emitWarning;
27
+ installed.add(runtime);
28
+ }
29
+
30
+ installRuntimeCompatibility();