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