@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.
- package/AGENT_GUIDE.md +296 -0
- package/CLI.md +609 -0
- package/CLI.schema.json +1085 -0
- package/README.md +19 -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/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/README.md
CHANGED
|
@@ -330,33 +330,30 @@ Common codes: `unauthorized`, `forbidden`, `credit_exhausted`,
|
|
|
330
330
|
|
|
331
331
|
## CLI (`pyai`)
|
|
332
332
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
333
|
+
The package provides a `pyai` executable for engineers, CI, and coding agents.
|
|
334
|
+
Install version 0.5.0 with `npm install -g @pyai/sdk@0.5.0`, then use
|
|
335
|
+
`pyai login` for browser sign-in. Environment API keys work for unattended
|
|
336
|
+
automation.
|
|
337
337
|
|
|
338
338
|
```bash
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
npx pyai smoke # lighter: models + voices + speak
|
|
339
|
+
pyai speak "Your appointment is confirmed." -o confirmation.wav
|
|
340
|
+
pyai hear confirmation.wav --text-only
|
|
341
|
+
pyai login -p work
|
|
342
|
+
pyai whoami -j
|
|
343
|
+
pyai schema agents create -j
|
|
344
|
+
pyai agents create --data @agent.json --dry-run -j
|
|
345
|
+
pyai recipes speak
|
|
346
|
+
pyai init voice-project --template agent
|
|
348
347
|
```
|
|
349
348
|
|
|
350
|
-
|
|
349
|
+
The [detailed CLI handbook](./CLI.md) covers installation, profiles, speech,
|
|
350
|
+
transcription, Dub submission through download, Cast, resource configuration,
|
|
351
|
+
JSON and stdin contracts, exit codes, and troubleshooting. For coding agents,
|
|
352
|
+
use the [raw integration guide](https://pyai.com/cli-agent-guide.md) and live
|
|
353
|
+
[OpenAPI contract](https://api.pyai.com/openapi.json).
|
|
351
354
|
|
|
352
|
-
|
|
353
|
-
pyai
|
|
354
|
-
pyai voices --gender female --region en_us
|
|
355
|
-
pyai speak --text "Hello" --voice stock_emma_en_gb --out hello.wav
|
|
356
|
-
pyai transcribe --url https://example.com/call.wav --diarize --poll
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
Auth comes from `PYAI_API_KEY` / `PYAI_BASE_URL` (or `--api-key` / `--base-url`).
|
|
355
|
+
`pyai doctor` checks the key, catalogs, and a Speak-to-Hear round trip;
|
|
356
|
+
`pyai smoke` checks catalogs and synthesis. Both make real API calls.
|
|
360
357
|
|
|
361
358
|
## Develop
|
|
362
359
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export declare const DEFAULT_BASE_URL = "https://api.pyai.com";
|
|
2
|
+
export declare class CliConfigError extends Error {
|
|
3
|
+
readonly code: string;
|
|
4
|
+
constructor(code: string, message: string);
|
|
5
|
+
}
|
|
6
|
+
export interface CliProfile {
|
|
7
|
+
api_key?: string;
|
|
8
|
+
base_url?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface CliConfig {
|
|
11
|
+
version: 1;
|
|
12
|
+
active_profile: string | null;
|
|
13
|
+
profiles: Record<string, CliProfile>;
|
|
14
|
+
}
|
|
15
|
+
export interface ResolvedConfig {
|
|
16
|
+
apiKey?: string;
|
|
17
|
+
baseURL: string;
|
|
18
|
+
profile: string;
|
|
19
|
+
/** Provenance of the API key. The base URL is resolved independently. */
|
|
20
|
+
source: "explicit" | "environment" | "profile" | "none";
|
|
21
|
+
}
|
|
22
|
+
export interface ProfileSummary {
|
|
23
|
+
name: string;
|
|
24
|
+
active: boolean;
|
|
25
|
+
base_url: string;
|
|
26
|
+
has_api_key: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** Keys are opaque: validation only ensures a bounded, header-safe value. */
|
|
29
|
+
export declare function validateApiKey(input: string): string;
|
|
30
|
+
/** The SDK adds /v1, so accept both documented API base URL forms. */
|
|
31
|
+
export declare function normalizeBaseURL(input: string): string;
|
|
32
|
+
export declare function validateProfileName(name: string): string;
|
|
33
|
+
/** Raw configuration is for internal use; use listProfiles for display. */
|
|
34
|
+
export declare function readConfig(): Promise<CliConfig>;
|
|
35
|
+
/** Merge supplied fields and make this the active profile. */
|
|
36
|
+
export declare function saveProfile(name: string, fields: CliProfile): Promise<void>;
|
|
37
|
+
export declare function removeProfile(name: string): Promise<void>;
|
|
38
|
+
export declare function useProfile(name: string): Promise<void>;
|
|
39
|
+
export declare function resolveConfig(options?: {
|
|
40
|
+
apiKey?: string;
|
|
41
|
+
baseURL?: string;
|
|
42
|
+
profile?: string;
|
|
43
|
+
allowMissingProfile?: boolean;
|
|
44
|
+
ignoreApiKey?: boolean;
|
|
45
|
+
}): Promise<ResolvedConfig>;
|
|
46
|
+
/** Never reveal a key, including its prefix or suffix, in profile listings. */
|
|
47
|
+
export declare function listProfiles(): Promise<ProfileSummary[]>;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { chmod, lstat, mkdir, open, rename, unlink } from "node:fs/promises";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
export const DEFAULT_BASE_URL = "https://api.pyai.com";
|
|
7
|
+
export class CliConfigError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(code, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "CliConfigError";
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Keys are opaque: validation only ensures a bounded, header-safe value. */
|
|
16
|
+
export function validateApiKey(input) {
|
|
17
|
+
if (typeof input !== "string" || input.length === 0 || input.length > 512 || /[\s\p{Cc}]/u.test(input)) {
|
|
18
|
+
throw new CliConfigError("invalid_api_key", "API keys must contain 1–512 characters without whitespace or control characters.");
|
|
19
|
+
}
|
|
20
|
+
return input;
|
|
21
|
+
}
|
|
22
|
+
/** The SDK adds /v1, so accept both documented API base URL forms. */
|
|
23
|
+
export function normalizeBaseURL(input) {
|
|
24
|
+
const invalid = () => new CliConfigError("invalid_base_url", "Base URL must use HTTPS (or HTTP on localhost), without credentials, a query, or a fragment.");
|
|
25
|
+
if (typeof input !== "string" || !/^https?:\/\//i.test(input) || /[\s\p{Cc}?#]/u.test(input))
|
|
26
|
+
throw invalid();
|
|
27
|
+
let url;
|
|
28
|
+
try {
|
|
29
|
+
url = new URL(input);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw invalid();
|
|
33
|
+
}
|
|
34
|
+
const authority = input.slice(input.indexOf("://") + 3).split("/")[0];
|
|
35
|
+
if (url.username || url.password || authority.includes("@"))
|
|
36
|
+
throw invalid();
|
|
37
|
+
const hostname = url.hostname.toLowerCase();
|
|
38
|
+
const loopback = hostname === "localhost" || hostname === "localhost." || hostname === "[::1]" || /^127\.\d+\.\d+\.\d+$/.test(hostname);
|
|
39
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback))
|
|
40
|
+
throw invalid();
|
|
41
|
+
const path = url.pathname.replace(/\/+$/, "").replace(/\/v1$/, "").replace(/\/+$/, "");
|
|
42
|
+
return `${url.origin}${path}`;
|
|
43
|
+
}
|
|
44
|
+
export function validateProfileName(name) {
|
|
45
|
+
if (typeof name !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(name)) {
|
|
46
|
+
throw new CliConfigError("invalid_profile", "Profile names must be 1–64 letters, numbers, dots, underscores, or hyphens, starting with a letter or number.");
|
|
47
|
+
}
|
|
48
|
+
return name;
|
|
49
|
+
}
|
|
50
|
+
function configDirectory() {
|
|
51
|
+
if (process.env.PYAI_CONFIG_DIR)
|
|
52
|
+
return resolve(process.env.PYAI_CONFIG_DIR);
|
|
53
|
+
return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "pyai");
|
|
54
|
+
}
|
|
55
|
+
function emptyConfig() {
|
|
56
|
+
return { version: 1, active_profile: null, profiles: {} };
|
|
57
|
+
}
|
|
58
|
+
function hasCode(error, code) {
|
|
59
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
60
|
+
}
|
|
61
|
+
function isRecord(value) {
|
|
62
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
63
|
+
}
|
|
64
|
+
function invalidConfig() {
|
|
65
|
+
return new CliConfigError("config_invalid", "PyAI config.json is invalid or uses an unsupported version. Fix it or move it aside before retrying.");
|
|
66
|
+
}
|
|
67
|
+
function parseConfig(text) {
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(text);
|
|
70
|
+
if (!isRecord(parsed) || parsed.version !== 1 || !isRecord(parsed.profiles))
|
|
71
|
+
throw invalidConfig();
|
|
72
|
+
if (parsed.active_profile !== null && typeof parsed.active_profile !== "string")
|
|
73
|
+
throw invalidConfig();
|
|
74
|
+
const profiles = {};
|
|
75
|
+
for (const [name, value] of Object.entries(parsed.profiles)) {
|
|
76
|
+
validateProfileName(name);
|
|
77
|
+
if (!isRecord(value) || Object.keys(value).some((key) => key !== "api_key" && key !== "base_url"))
|
|
78
|
+
throw invalidConfig();
|
|
79
|
+
const profile = {};
|
|
80
|
+
if ("api_key" in value)
|
|
81
|
+
profile.api_key = validateApiKey(value.api_key);
|
|
82
|
+
if ("base_url" in value)
|
|
83
|
+
profile.base_url = normalizeBaseURL(value.base_url);
|
|
84
|
+
profiles[name] = profile;
|
|
85
|
+
}
|
|
86
|
+
if (parsed.active_profile !== null) {
|
|
87
|
+
validateProfileName(parsed.active_profile);
|
|
88
|
+
if (!Object.hasOwn(profiles, parsed.active_profile))
|
|
89
|
+
throw invalidConfig();
|
|
90
|
+
}
|
|
91
|
+
return { version: 1, active_profile: parsed.active_profile, profiles };
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Never include JSON parser errors: they can contain the credential itself.
|
|
95
|
+
throw invalidConfig();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function verifyOwner(uid) {
|
|
99
|
+
if (typeof process.getuid === "function" && uid !== process.getuid()) {
|
|
100
|
+
throw new CliConfigError("config_unsafe", "PyAI configuration must be owned by the current user.");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function inspectDirectory(directory) {
|
|
104
|
+
try {
|
|
105
|
+
const stat = await lstat(directory);
|
|
106
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
107
|
+
throw new CliConfigError("config_unsafe", "PyAI configuration directory must be a real directory, not a symbolic link.");
|
|
108
|
+
}
|
|
109
|
+
verifyOwner(stat.uid);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (hasCode(error, "ENOENT"))
|
|
114
|
+
return false;
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function readConfigAt(directory) {
|
|
119
|
+
let file;
|
|
120
|
+
try {
|
|
121
|
+
if (!(await inspectDirectory(directory)))
|
|
122
|
+
return emptyConfig();
|
|
123
|
+
const path = join(directory, "config.json");
|
|
124
|
+
const stat = await lstat(path);
|
|
125
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
126
|
+
throw new CliConfigError("config_unsafe", "PyAI config.json must be a regular file, not a symbolic link.");
|
|
127
|
+
}
|
|
128
|
+
file = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
129
|
+
const openedStat = await file.stat();
|
|
130
|
+
verifyOwner(openedStat.uid);
|
|
131
|
+
if (!openedStat.isFile() || openedStat.size > 1024 * 1024)
|
|
132
|
+
throw invalidConfig();
|
|
133
|
+
return parseConfig(await file.readFile("utf8"));
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (hasCode(error, "ENOENT"))
|
|
137
|
+
return emptyConfig();
|
|
138
|
+
if (error instanceof CliConfigError)
|
|
139
|
+
throw error;
|
|
140
|
+
throw new CliConfigError("config_read_failed", "Unable to read PyAI config.json. Check the configuration directory and file permissions.");
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
await file?.close();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Raw configuration is for internal use; use listProfiles for display. */
|
|
147
|
+
export async function readConfig() {
|
|
148
|
+
return readConfigAt(configDirectory());
|
|
149
|
+
}
|
|
150
|
+
async function writeConfigAt(directory, config) {
|
|
151
|
+
const temporary = join(directory, `.config-${randomUUID()}.tmp`);
|
|
152
|
+
let file;
|
|
153
|
+
try {
|
|
154
|
+
file = await open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600);
|
|
155
|
+
await file.writeFile(`${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
156
|
+
await file.sync();
|
|
157
|
+
await file.close();
|
|
158
|
+
file = undefined;
|
|
159
|
+
await rename(temporary, join(directory, "config.json"));
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
await file?.close();
|
|
163
|
+
await unlink(temporary).catch((error) => {
|
|
164
|
+
if (!hasCode(error, "ENOENT"))
|
|
165
|
+
throw error;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function updateConfig(update) {
|
|
170
|
+
const directory = configDirectory();
|
|
171
|
+
const lockPath = join(directory, "config.json.lock");
|
|
172
|
+
let lock;
|
|
173
|
+
try {
|
|
174
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
175
|
+
await inspectDirectory(directory);
|
|
176
|
+
await chmod(directory, 0o700);
|
|
177
|
+
// Every writer holds an exclusive lock across read, modification, and rename.
|
|
178
|
+
// A crashed writer leaves a visible lock instead of risking lost updates.
|
|
179
|
+
const deadline = Date.now() + 5000;
|
|
180
|
+
while (!lock) {
|
|
181
|
+
try {
|
|
182
|
+
lock = await open(lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
if (!hasCode(error, "EEXIST"))
|
|
186
|
+
throw error;
|
|
187
|
+
if (Date.now() >= deadline) {
|
|
188
|
+
throw new CliConfigError("config_busy", "PyAI configuration is locked by another process. Retry shortly; remove config.json.lock only if no PyAI configuration command is running.");
|
|
189
|
+
}
|
|
190
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const config = await readConfigAt(directory);
|
|
194
|
+
update(config);
|
|
195
|
+
await writeConfigAt(directory, config);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (error instanceof CliConfigError)
|
|
199
|
+
throw error;
|
|
200
|
+
throw new CliConfigError("config_write_failed", "Unable to save PyAI configuration. Check the configuration directory and file permissions.");
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
if (lock) {
|
|
204
|
+
await lock.close();
|
|
205
|
+
await unlink(lockPath);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** Merge supplied fields and make this the active profile. */
|
|
210
|
+
export async function saveProfile(name, fields) {
|
|
211
|
+
validateProfileName(name);
|
|
212
|
+
const normalized = {};
|
|
213
|
+
if (fields.api_key !== undefined)
|
|
214
|
+
normalized.api_key = validateApiKey(fields.api_key);
|
|
215
|
+
if (fields.base_url !== undefined)
|
|
216
|
+
normalized.base_url = normalizeBaseURL(fields.base_url);
|
|
217
|
+
await updateConfig((config) => {
|
|
218
|
+
const previous = Object.hasOwn(config.profiles, name) ? config.profiles[name] : undefined;
|
|
219
|
+
config.profiles[name] = { ...previous, ...normalized };
|
|
220
|
+
config.active_profile = name;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
export async function removeProfile(name) {
|
|
224
|
+
validateProfileName(name);
|
|
225
|
+
await updateConfig((config) => {
|
|
226
|
+
if (!Object.hasOwn(config.profiles, name))
|
|
227
|
+
throw new CliConfigError("profile_not_found", "The selected PyAI profile does not exist. Use auth login to create it.");
|
|
228
|
+
delete config.profiles[name];
|
|
229
|
+
if (config.active_profile === name) {
|
|
230
|
+
config.active_profile = Object.hasOwn(config.profiles, "default") ? "default" : Object.keys(config.profiles).sort()[0] ?? null;
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
export async function useProfile(name) {
|
|
235
|
+
validateProfileName(name);
|
|
236
|
+
await updateConfig((config) => {
|
|
237
|
+
if (!Object.hasOwn(config.profiles, name))
|
|
238
|
+
throw new CliConfigError("profile_not_found", "The selected PyAI profile does not exist. Use auth login to create it.");
|
|
239
|
+
config.active_profile = name;
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
export async function resolveConfig(options = {}) {
|
|
243
|
+
const config = await readConfig();
|
|
244
|
+
const selected = options.profile ?? (process.env.PYAI_PROFILE || undefined);
|
|
245
|
+
const profile = validateProfileName(selected ?? config.active_profile ?? "default");
|
|
246
|
+
const stored = Object.hasOwn(config.profiles, profile) ? config.profiles[profile] : undefined;
|
|
247
|
+
if (selected !== undefined && !stored && !options.allowMissingProfile) {
|
|
248
|
+
throw new CliConfigError("profile_not_found", "The selected PyAI profile does not exist. Use auth login to create it.");
|
|
249
|
+
}
|
|
250
|
+
const environmentKey = process.env.PYAI_API_KEY || undefined;
|
|
251
|
+
const apiKey = options.ignoreApiKey ? undefined : options.apiKey ?? environmentKey ?? stored?.api_key;
|
|
252
|
+
const baseURL = normalizeBaseURL(options.baseURL ?? (process.env.PYAI_BASE_URL || undefined) ?? stored?.base_url ?? DEFAULT_BASE_URL);
|
|
253
|
+
const source = options.ignoreApiKey ? "none" : options.apiKey !== undefined ? "explicit" : environmentKey !== undefined ? "environment" : stored?.api_key !== undefined ? "profile" : "none";
|
|
254
|
+
return { ...(apiKey !== undefined ? { apiKey: validateApiKey(apiKey) } : {}), baseURL, profile, source };
|
|
255
|
+
}
|
|
256
|
+
/** Never reveal a key, including its prefix or suffix, in profile listings. */
|
|
257
|
+
export async function listProfiles() {
|
|
258
|
+
const config = await readConfig();
|
|
259
|
+
return Object.keys(config.profiles).sort().map((name) => ({
|
|
260
|
+
name,
|
|
261
|
+
active: config.active_profile === name,
|
|
262
|
+
base_url: config.profiles[name].base_url ?? DEFAULT_BASE_URL,
|
|
263
|
+
has_api_key: config.profiles[name].api_key !== undefined,
|
|
264
|
+
}));
|
|
265
|
+
}
|
package/dist/cli-dx.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Offline discovery and copyable workflows; recipes never execute commands. */
|
|
2
|
+
export declare const aliases: Record<string, string>;
|
|
3
|
+
export declare const shortFlags: Record<string, string>;
|
|
4
|
+
export interface Recipe {
|
|
5
|
+
name: string;
|
|
6
|
+
title: string;
|
|
7
|
+
description: string;
|
|
8
|
+
commands: string[];
|
|
9
|
+
notes: string[];
|
|
10
|
+
}
|
|
11
|
+
export declare const recipes: Recipe[];
|
package/dist/cli-dx.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Offline discovery and copyable workflows; recipes never execute commands. */
|
|
2
|
+
export const aliases = Object.assign(Object.create(null), {
|
|
3
|
+
login: "auth login", logout: "auth logout", whoami: "auth status", use: "profiles use",
|
|
4
|
+
say: "speak", hear: "transcribe",
|
|
5
|
+
});
|
|
6
|
+
export const shortFlags = Object.assign(Object.create(null), {
|
|
7
|
+
"-h": "--help", "-v": "--version", "-o": "--out", "-f": "--file",
|
|
8
|
+
"-t": "--text", "-p": "--profile", "-j": "--json",
|
|
9
|
+
});
|
|
10
|
+
export const recipes = [
|
|
11
|
+
{ name: "auth", title: "Sign in and check access", description: "Save a named profile and inspect the current credential.",
|
|
12
|
+
commands: ["pyai login --profile work", "pyai whoami --profile work --json", "pyai use work"],
|
|
13
|
+
notes: ["Browser login requires the CLI login API and console deployment. Use --no-browser on remote terminals.", "For unattended runs, supply PYAI_API_KEY through the environment. Environment values override saved profiles."] },
|
|
14
|
+
{ name: "speak", title: "Turn text into audio", description: "Speak a line, narrate a text file, or pipe text from another command.",
|
|
15
|
+
commands: ['pyai speak "Hello from PyAI" -o hello.wav', "pyai speak --text-file script.txt -o narration.wav", 'printf "Build complete.\\n" | pyai speak -o build.wav'],
|
|
16
|
+
notes: ["Files must have a new output name unless you pass --force. Default output is pyai-speak.wav.", "Use pyai voices --json to choose a current voice; add --voice ID."] },
|
|
17
|
+
{ name: "transcribe", title: "Get a transcript", description: "Print just the words or retain structured output for automation.",
|
|
18
|
+
commands: ["pyai hear call.wav --text-only > transcript.txt", "pyai transcribe call.wav --json > transcript.json", 'pyai transcribe "https://example.com/call.wav" --wait --text-only'],
|
|
19
|
+
notes: ["Replace the example URL with a reachable audio URL. Hosted audio uses an asynchronous job.", "--text-only requires --wait for URLs; large offloaded results retain a job reference instead of following an external URL with credentials."] },
|
|
20
|
+
{ name: "dub", title: "Dub audio in one command", description: "Submit a Dub job, wait for completion, and download the audio.",
|
|
21
|
+
commands: ["pyai request GET /healthz/dub --json", "pyai dub original.wav --from en --to hi -o dubbed.wav --wait-timeout 600"],
|
|
22
|
+
notes: ["Check the live health response for available input and output languages before choosing them. Requires dub:render.", "On a timeout, resume with pyai dub wait JOB_ID and pyai dub audio JOB_ID -o dubbed.wav; do not resubmit the source."] },
|
|
23
|
+
{ name: "agent", title: "Start a project for a coding agent", description: "Create local context and request examples without a network call.",
|
|
24
|
+
commands: ["pyai init voice-demo --template agent", "pyai schema agents create --json", "pyai agents create --data @voice-demo/agent.json --dry-run --json"],
|
|
25
|
+
notes: ["The destination directory must be new. The starter writes no credentials and installs no dependencies.", "Read the generated PYAI.md first. Fetch live OpenAPI before adding fields or endpoints; remove --dry-run only when ready to create the profile."] },
|
|
26
|
+
{ name: "inspect", title: "Discover before you call", description: "Inspect commands offline and fetch the current API contract.",
|
|
27
|
+
commands: ["pyai schema speak --json", "pyai schema agents --json", "pyai schema --openapi --json > openapi.json", "pyai request GET /v1/me --json"],
|
|
28
|
+
notes: ["Local schema and recipes work without credentials. Live OpenAPI needs a network connection but no API key.", "The CLI manages Agent profiles; use the SDK or WebSocket API for a live Omni audio session."] },
|
|
29
|
+
{ name: "ci", title: "Use PyAI in a build", description: "Check access and produce an audio artifact with machine-readable output.",
|
|
30
|
+
commands: ["pyai whoami --json", 'pyai speak "Your build is ready" -o build.wav --json', "pyai doctor --json"],
|
|
31
|
+
notes: ["Set PYAI_API_KEY through the CI secret store; do not put it in source or shell arguments.", "doctor synthesizes and transcribes a sample, which consumes usage. Use whoami for an authentication-only check.", "Install the CLI with npm install -g @pyai/sdk@0.5.0 and pin that version in repeatable builds."] },
|
|
32
|
+
];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** HTTP transport for the CLI. SDK request behavior remains independent. */
|
|
2
|
+
export declare class CliError extends Error {
|
|
3
|
+
readonly code: string;
|
|
4
|
+
readonly exitCode: number;
|
|
5
|
+
readonly details?: Record<string, unknown>;
|
|
6
|
+
constructor(code: string, message: string, exitCode?: number, details?: Record<string, unknown>);
|
|
7
|
+
}
|
|
8
|
+
export interface CliHttpOptions {
|
|
9
|
+
baseURL: string;
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
maxRetries?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface CliRequestOptions {
|
|
15
|
+
json?: unknown;
|
|
16
|
+
body?: BodyInit;
|
|
17
|
+
headers?: Record<string, string>;
|
|
18
|
+
auth?: boolean;
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare class CliHttp {
|
|
22
|
+
private readonly baseURL;
|
|
23
|
+
private readonly apiKey?;
|
|
24
|
+
private readonly timeoutMs;
|
|
25
|
+
private readonly maxRetries;
|
|
26
|
+
constructor(options: CliHttpOptions);
|
|
27
|
+
request(method: string, path: string, options?: CliRequestOptions): Promise<Response>;
|
|
28
|
+
json(method: string, path: string, options?: CliRequestOptions): Promise<any>;
|
|
29
|
+
/** Validate previews through the same URL rules used by live requests. */
|
|
30
|
+
validatePath(path: string): string;
|
|
31
|
+
private redact;
|
|
32
|
+
private url;
|
|
33
|
+
private transportError;
|
|
34
|
+
private apiError;
|
|
35
|
+
/** Preserve the request deadline after headers, including paused or stalled downloads. */
|
|
36
|
+
private stream;
|
|
37
|
+
private execute;
|
|
38
|
+
}
|