@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.
- package/AGENT_GUIDE.md +300 -0
- package/CLI.md +634 -0
- package/CLI.schema.json +1327 -0
- package/README.md +25 -22
- package/dist/cli-config.d.ts +47 -0
- package/dist/cli-config.js +265 -0
- package/dist/cli-dx.d.ts +12 -0
- package/dist/cli-dx.js +38 -0
- package/dist/cli-http.d.ts +38 -0
- package/dist/cli-http.js +295 -0
- package/dist/cli-init.d.ts +27 -0
- package/dist/cli-init.js +433 -0
- package/dist/cli-routes.d.ts +18 -0
- package/dist/cli-routes.js +65 -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 +683 -270
- package/dist/index.d.ts +22 -4
- package/dist/index.js +13 -0
- package/package.json +7 -2
- package/src/cli-config.ts +283 -0
- package/src/cli-dx.ts +39 -0
- package/src/cli-http.ts +280 -0
- package/src/cli-init.ts +433 -0
- package/src/cli-routes.ts +88 -0
- package/src/cli-runtime.ts +30 -0
- package/src/cli-web-auth.ts +148 -0
- package/src/cli.ts +439 -295
- package/src/index.ts +30 -5
package/README.md
CHANGED
|
@@ -99,6 +99,12 @@ Live `0x02` transcript bodies are plain UTF-8 caller-text deltas, not JSON.
|
|
|
99
99
|
Coalesce successive deltas for the current caller turn. Bounded direct JSON
|
|
100
100
|
bodies remain accepted for older bridges.
|
|
101
101
|
|
|
102
|
+
Since version 0.5.1, the engine's four-field assistant synthesis advisory on
|
|
103
|
+
`0x03` also reaches `onTranscript` with `role: "assistant"`, `final: true`,
|
|
104
|
+
and `mode: "replace"`.
|
|
105
|
+
This is text submitted for synthesis; it does not prove that playback completed.
|
|
106
|
+
Caller transcripts continue to require the `0x02` carrier.
|
|
107
|
+
|
|
102
108
|
`rate` configures caller input. A `rate: 16000` session still receives 24 kHz
|
|
103
109
|
agent audio; `rate: 8000` receives 8 kHz. Read `hello.audio_out` before playback.
|
|
104
110
|
Omni has no commit frame—keep streaming silence during caller pauses.
|
|
@@ -330,33 +336,30 @@ Common codes: `unauthorized`, `forbidden`, `credit_exhausted`,
|
|
|
330
336
|
|
|
331
337
|
## CLI (`pyai`)
|
|
332
338
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
339
|
+
The package provides a `pyai` executable for engineers, CI, and coding agents.
|
|
340
|
+
Install version 0.5.0 with `npm install -g @pyai/sdk@0.5.0`, then use
|
|
341
|
+
`pyai login` for browser sign-in. Environment API keys work for unattended
|
|
342
|
+
automation.
|
|
337
343
|
|
|
338
344
|
```bash
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
npx pyai smoke # lighter: models + voices + speak
|
|
345
|
+
pyai speak "Your appointment is confirmed." -o confirmation.wav
|
|
346
|
+
pyai hear confirmation.wav --text-only
|
|
347
|
+
pyai login -p work
|
|
348
|
+
pyai whoami -j
|
|
349
|
+
pyai schema agents create -j
|
|
350
|
+
pyai agents create --data @agent.json --dry-run -j
|
|
351
|
+
pyai recipes speak
|
|
352
|
+
pyai init voice-project --template agent
|
|
348
353
|
```
|
|
349
354
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
pyai
|
|
354
|
-
pyai
|
|
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
|
-
```
|
|
355
|
+
The [detailed CLI handbook](./CLI.md) covers installation, profiles, speech,
|
|
356
|
+
transcription, Dub submission through download, Cast, resource configuration,
|
|
357
|
+
JSON and stdin contracts, exit codes, and troubleshooting. For coding agents,
|
|
358
|
+
use the [raw integration guide](https://pyai.com/cli-agent-guide.md) and live
|
|
359
|
+
[OpenAPI contract](https://api.pyai.com/openapi.json).
|
|
358
360
|
|
|
359
|
-
|
|
361
|
+
`pyai doctor` checks the key, catalogs, and a Speak-to-Hear round trip;
|
|
362
|
+
`pyai smoke` checks catalogs and synthesis. Both make real API calls.
|
|
360
363
|
|
|
361
364
|
## Develop
|
|
362
365
|
|
|
@@ -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,12 @@
|
|
|
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
|
+
docs_url: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const recipes: Recipe[];
|
package/dist/cli-dx.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
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: "calling", title: "Set up a managed phone Agent", description: "Inspect numbers, bind a profile, and retrieve call artifacts through the public API.",
|
|
12
|
+
commands: ["pyai whoami --json", "pyai numbers list --json", "pyai numbers search --country US --limit 5 --json", "pyai agents list --json", "pyai numbers bind NUMBER_ID --data @binding.json --dry-run --json", "pyai calls create --data @call.json --idempotency-key CALL_REQUEST_KEY --dry-run --json", "pyai calls get RETURNED_CALL_ID --json", "pyai omni get OMNI_CALL_ID --json"],
|
|
13
|
+
notes: ["Replace all placeholder IDs with returned values. binding.json is {agent_id: RETURNED_AGENT_ID}; call.json requires from_number_id, to, agent_id and use_case from live OpenAPI.", "Remove --dry-run for authorized changes. numbers buy and calls create require --confirm and --idempotency-key. Calls reach real destinations; sandbox keys do not grant telephony permissions.", "Requires telephony:manage; session artifacts require omni:read. Preserve artifacts.omni_call_id. Read the decision tree for purchase, polling, recording, and inbound-call steps.", "Use calls wait ID --wait-timeout 120 --poll-interval 3 --json for bounded waiting. dispatch_unknown requires inspection, never automatic redial."] },
|
|
14
|
+
{ name: "omni", title: "Build a realtime conversation with the SDK", description: "Use the official SDK for binary framing, subprotocol auth, and typed events.",
|
|
15
|
+
commands: ["pyai schema --openapi --json", "pyai init voice-demo --template typescript", "pyai voices --json"],
|
|
16
|
+
notes: ["The starter supplies Hear/Speak examples. Add realtime through pyai.omni.connect({ rate, configure }); see the linked decision tree's SDK pattern.", "Wait for configured before non-silent input. Stream paced digital silence during pauses. Read hello.audio_out and clear queued playback on barge_in/flush.", "Browser code uses an ephemeral token minted by omni.createSession on your server. Never put live keys in browser code or URLs.", "Machine framing contract: https://api.pyai.com/omni-frames.json. For an existing Twilio number use @pyai/twilio and the official twilio-omni-voice-agent example."] },
|
|
17
|
+
{ name: "auth", title: "Sign in and check access", description: "Save a named profile and inspect the current credential.",
|
|
18
|
+
commands: ["pyai login --profile work", "pyai whoami --profile work --json", "pyai use work"],
|
|
19
|
+
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."] },
|
|
20
|
+
{ name: "speak", title: "Turn text into audio", description: "Speak a line, narrate a text file, or pipe text from another command.",
|
|
21
|
+
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'],
|
|
22
|
+
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."] },
|
|
23
|
+
{ name: "transcribe", title: "Get a transcript", description: "Print just the words or retain structured output for automation.",
|
|
24
|
+
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'],
|
|
25
|
+
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."] },
|
|
26
|
+
{ name: "dub", title: "Dub audio in one command", description: "Submit a Dub job, wait for completion, and download the audio.",
|
|
27
|
+
commands: ["pyai request GET /healthz/dub --json", "pyai dub original.wav --from en --to hi -o dubbed.wav --wait-timeout 600"],
|
|
28
|
+
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."] },
|
|
29
|
+
{ name: "agent", title: "Start a project for a coding agent", description: "Create local context and request examples without a network call.",
|
|
30
|
+
commands: ["pyai init voice-demo --template agent", "pyai schema agents create --json", "pyai agents create --data @voice-demo/agent.json --dry-run --json"],
|
|
31
|
+
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."] },
|
|
32
|
+
{ name: "inspect", title: "Discover before you call", description: "Inspect commands offline and fetch the current API contract.",
|
|
33
|
+
commands: ["pyai schema speak --json", "pyai schema agents --json", "pyai schema --openapi --json > openapi.json", "pyai request GET /v1/me --json"],
|
|
34
|
+
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."] },
|
|
35
|
+
{ name: "ci", title: "Use PyAI in a build", description: "Check access and produce an audio artifact with machine-readable output.",
|
|
36
|
+
commands: ["pyai whoami --json", 'pyai speak "Your build is ready" -o build.wav --json', "pyai doctor --json"],
|
|
37
|
+
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.6.0 and pin that version in repeatable builds."] },
|
|
38
|
+
].map(recipe => ({ ...recipe, docs_url: "https://pyai.com/agents/speech-calling.md" }));
|
|
@@ -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
|
+
}
|