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