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