@pyai/sdk 0.5.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 +8 -4
- package/CLI.md +29 -4
- package/CLI.schema.json +242 -0
- package/README.md +6 -0
- package/dist/cli-dx.d.ts +1 -0
- package/dist/cli-dx.js +8 -2
- package/dist/cli-http.js +11 -2
- package/dist/cli-init.js +6 -3
- package/dist/cli-routes.d.ts +3 -0
- package/dist/cli-routes.js +13 -0
- package/dist/cli.js +11 -2
- package/dist/index.d.ts +22 -4
- package/dist/index.js +13 -0
- package/package.json +1 -1
- package/src/cli-dx.ts +9 -3
- package/src/cli-http.ts +9 -2
- package/src/cli-init.ts +6 -3
- package/src/cli-routes.ts +16 -0
- package/src/cli.ts +10 -2
- package/src/index.ts +30 -5
package/AGENT_GUIDE.md
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
# PyAI CLI integration guide for coding agents
|
|
2
2
|
|
|
3
|
+
Speech and calling decision tree: https://pyai.com/agents/speech-calling.md
|
|
4
|
+
Omni frame contract: https://api.pyai.com/omni-frames.json
|
|
5
|
+
|
|
6
|
+
|
|
3
7
|
Use this guide when building an application against PyAI, automating a file
|
|
4
8
|
workflow, or adding PyAI to an existing repository. It is readable as plain
|
|
5
9
|
Markdown and does not require the marketing website renderer.
|
|
6
10
|
|
|
7
11
|
## Availability and sources of truth
|
|
8
12
|
|
|
9
|
-
The CLI is included in `@pyai/sdk` 0.
|
|
10
|
-
`npm install -g @pyai/sdk@0.
|
|
13
|
+
The CLI is included in `@pyai/sdk` 0.6.0. Install it with
|
|
14
|
+
`npm install -g @pyai/sdk@0.6.0`. Browser login supports existing PyAI accounts;
|
|
11
15
|
unattended agents should use an authorized environment key. Inspect the
|
|
12
16
|
installed command schema before acting.
|
|
13
17
|
|
|
@@ -31,7 +35,7 @@ do not invent an endpoint or present the feature as deployed.
|
|
|
31
35
|
Install the published CLI with Node.js 22 or newer recommended:
|
|
32
36
|
|
|
33
37
|
```bash
|
|
34
|
-
npm install -g @pyai/sdk@0.
|
|
38
|
+
npm install -g @pyai/sdk@0.6.0
|
|
35
39
|
pyai --version
|
|
36
40
|
pyai schema --json
|
|
37
41
|
```
|
|
@@ -47,7 +51,7 @@ npm install -g .
|
|
|
47
51
|
```
|
|
48
52
|
|
|
49
53
|
Alternatively install a supplied build with
|
|
50
|
-
`npm install -g /path/to/pyai-sdk-0.
|
|
54
|
+
`npm install -g /path/to/pyai-sdk-0.6.0.tgz`. The compiled executable supports
|
|
51
55
|
Node.js 18 or newer. Use `node dist/cli.js` explicitly when another package's
|
|
52
56
|
`pyai` shadows this executable.
|
|
53
57
|
|
package/CLI.md
CHANGED
|
@@ -6,8 +6,8 @@ scripts and in coding agents with shell access, including Cursor, Claude Code,
|
|
|
6
6
|
and Codex. `--json` provides structured results; `--help` and `schema` describe
|
|
7
7
|
the installed command surface.
|
|
8
8
|
|
|
9
|
-
**Version:** the expanded CLI is included in `@pyai/sdk` 0.
|
|
10
|
-
npm with `npm install -g @pyai/sdk@0.
|
|
9
|
+
**Version:** the expanded CLI is included in `@pyai/sdk` 0.6.0. Install it from
|
|
10
|
+
npm with `npm install -g @pyai/sdk@0.6.0`, then run `pyai login` for browser
|
|
11
11
|
sign-in or provide `PYAI_API_KEY` for unattended automation.
|
|
12
12
|
|
|
13
13
|
## Contents
|
|
@@ -31,7 +31,7 @@ sign-in or provide `PYAI_API_KEY` for unattended automation.
|
|
|
31
31
|
Install the published package (Node.js 22 or newer recommended):
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
npm install -g @pyai/sdk@0.
|
|
34
|
+
npm install -g @pyai/sdk@0.6.0
|
|
35
35
|
pyai --version
|
|
36
36
|
pyai login
|
|
37
37
|
pyai speak "Hello from PyAI." -o hello.wav
|
|
@@ -56,7 +56,7 @@ also be packaged and installed on another machine:
|
|
|
56
56
|
# In sdk/typescript after building
|
|
57
57
|
npm pack --ignore-scripts
|
|
58
58
|
# On the destination machine, using the actual tarball path
|
|
59
|
-
npm install -g /path/to/pyai-sdk-0.
|
|
59
|
+
npm install -g /path/to/pyai-sdk-0.6.0.tgz
|
|
60
60
|
pyai schema --json
|
|
61
61
|
```
|
|
62
62
|
|
|
@@ -607,3 +607,28 @@ exit/error code, request ID, and a redacted reproduction. Never include the key.
|
|
|
607
607
|
Realtime microphone sessions, live Hear streams, Omni conversations, and live
|
|
608
608
|
AMD streams require a realtime client. Use the SDK and canonical protocol docs
|
|
609
609
|
for those workflows; the CLI covers files, REST operations, and job lifecycles.
|
|
610
|
+
|
|
611
|
+
## Speech and calling
|
|
612
|
+
|
|
613
|
+
Read the [speech and calling decision tree](https://pyai.com/agents/speech-calling.md),
|
|
614
|
+
or run `pyai recipes calling --json` / `pyai recipes omni --json`.
|
|
615
|
+
|
|
616
|
+
- `numbers search`, `numbers list`, `numbers buy`, `numbers bind ID`: discover,
|
|
617
|
+
purchase and route managed numbers. `numbers bind` takes `--data` with
|
|
618
|
+
`agent_id`; null unassigns.
|
|
619
|
+
- `calls create`, `calls list`, `calls get ID`, `calls wait ID`: real outbound
|
|
620
|
+
calling. Creation requires a request body from the live contract.
|
|
621
|
+
- `omni calls`, `omni get ID`, `omni summary ID`, `omni transcript ID`,
|
|
622
|
+
`omni recording ID --out call.wav`: read session artifacts, including inbound
|
|
623
|
+
calls. Use `--session-label` on `omni calls` to find an Agent's sessions.
|
|
624
|
+
|
|
625
|
+
Purchases and outbound calls require `--confirm` and `--idempotency-key`. Use
|
|
626
|
+
`--dry-run` for inspection before authorized execution. `needs_human` (exit 2)
|
|
627
|
+
means confirmation is missing and no submission was made. Outbound test calls
|
|
628
|
+
are real calls; sandbox keys do not grant dialing or purchase permissions.
|
|
629
|
+
The API enforces deployment availability, scopes, billing and policy gates.
|
|
630
|
+
|
|
631
|
+
`calls wait` accepts `--wait-timeout` and `--poll-interval`; preserve the original
|
|
632
|
+
ID after a deadline. `dispatch_unknown` stops waiting with a failure result for
|
|
633
|
+
inspection, never redial. Follow `calls get`'s `artifacts.omni_call_id` to session
|
|
634
|
+
artifacts. A completed call does not guarantee a recording or summary.
|
package/CLI.schema.json
CHANGED
|
@@ -232,6 +232,131 @@
|
|
|
232
232
|
"command": "doctor",
|
|
233
233
|
"description": "Diagnose key, catalogs, and a Speak to Hear round-trip"
|
|
234
234
|
},
|
|
235
|
+
{
|
|
236
|
+
"command": "omni calls",
|
|
237
|
+
"description": "List Omni sessions including inbound calls. Scope omni:read.",
|
|
238
|
+
"args": [],
|
|
239
|
+
"flags": {
|
|
240
|
+
"session-label": "string",
|
|
241
|
+
"limit": "string",
|
|
242
|
+
"cursor": "string"
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"command": "omni get",
|
|
247
|
+
"description": "Read an Omni call's transcript, summary and recording availability.",
|
|
248
|
+
"args": [
|
|
249
|
+
"id"
|
|
250
|
+
],
|
|
251
|
+
"flags": {}
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
"command": "omni summary",
|
|
255
|
+
"description": "Read the structured summary of an Omni session when generated.",
|
|
256
|
+
"args": [
|
|
257
|
+
"id"
|
|
258
|
+
],
|
|
259
|
+
"flags": {}
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"command": "omni transcript",
|
|
263
|
+
"description": "Read the complete Omni session transcript.",
|
|
264
|
+
"args": [
|
|
265
|
+
"id"
|
|
266
|
+
],
|
|
267
|
+
"flags": {}
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"command": "omni recording",
|
|
271
|
+
"description": "Download the recording with --out; recording must have been enabled for this call.",
|
|
272
|
+
"args": [
|
|
273
|
+
"id"
|
|
274
|
+
],
|
|
275
|
+
"flags": {
|
|
276
|
+
"out": "string",
|
|
277
|
+
"force": "boolean"
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
"command": "numbers search",
|
|
282
|
+
"description": "Search managed numbers; inspect provisioning availability before buying. Scope telephony:manage.",
|
|
283
|
+
"args": [],
|
|
284
|
+
"flags": {
|
|
285
|
+
"country": "string",
|
|
286
|
+
"area-code": "string",
|
|
287
|
+
"contains": "string",
|
|
288
|
+
"limit": "string"
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
"command": "numbers list",
|
|
293
|
+
"description": "List the organization's managed numbers and current Agent bindings.",
|
|
294
|
+
"args": [],
|
|
295
|
+
"flags": {
|
|
296
|
+
"include-released": "boolean",
|
|
297
|
+
"limit": "string",
|
|
298
|
+
"cursor": "string"
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
"command": "numbers buy",
|
|
303
|
+
"description": "Purchase a number from JSON. Requires --confirm and --idempotency-key; may incur recurring charges.",
|
|
304
|
+
"args": [],
|
|
305
|
+
"flags": {
|
|
306
|
+
"data": "string",
|
|
307
|
+
"idempotency-key": "string",
|
|
308
|
+
"confirm": "boolean"
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
"command": "numbers bind",
|
|
313
|
+
"description": "Bind an existing number using JSON {agent_id}; null unassigns. Read numbers list first.",
|
|
314
|
+
"args": [
|
|
315
|
+
"id"
|
|
316
|
+
],
|
|
317
|
+
"flags": {
|
|
318
|
+
"data": "string",
|
|
319
|
+
"idempotency-key": "string"
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
"command": "calls list",
|
|
324
|
+
"description": "List outbound call requests and their normalized status.",
|
|
325
|
+
"args": [],
|
|
326
|
+
"flags": {
|
|
327
|
+
"limit": "string",
|
|
328
|
+
"cursor": "string"
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
"command": "calls create",
|
|
333
|
+
"description": "Dial a real destination from JSON. Requires --confirm and --idempotency-key; this is not a simulated test call.",
|
|
334
|
+
"args": [],
|
|
335
|
+
"flags": {
|
|
336
|
+
"data": "string",
|
|
337
|
+
"idempotency-key": "string",
|
|
338
|
+
"confirm": "boolean"
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
"command": "calls get",
|
|
343
|
+
"description": "Get an outbound call by its returned ID; retain artifact identifiers separately.",
|
|
344
|
+
"args": [
|
|
345
|
+
"id"
|
|
346
|
+
],
|
|
347
|
+
"flags": {}
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
"command": "calls wait",
|
|
351
|
+
"description": "Wait with a deadline. dispatch_unknown stops polling for manual inspection; never redial automatically.",
|
|
352
|
+
"args": [
|
|
353
|
+
"id"
|
|
354
|
+
],
|
|
355
|
+
"flags": {
|
|
356
|
+
"wait-timeout": "string",
|
|
357
|
+
"poll-interval": "string"
|
|
358
|
+
}
|
|
359
|
+
},
|
|
235
360
|
{
|
|
236
361
|
"command": "agents list",
|
|
237
362
|
"description": "List managed Agent profiles.",
|
|
@@ -660,6 +785,123 @@
|
|
|
660
785
|
}
|
|
661
786
|
],
|
|
662
787
|
"routes": [
|
|
788
|
+
{
|
|
789
|
+
"command": "omni calls",
|
|
790
|
+
"method": "GET",
|
|
791
|
+
"path": "/v1/omni/calls",
|
|
792
|
+
"query": {
|
|
793
|
+
"session-label": "string",
|
|
794
|
+
"limit": "string",
|
|
795
|
+
"cursor": "string"
|
|
796
|
+
},
|
|
797
|
+
"description": "List Omni sessions including inbound calls. Scope omni:read."
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
"command": "omni get",
|
|
801
|
+
"method": "GET",
|
|
802
|
+
"path": "/v1/omni/calls/{id}",
|
|
803
|
+
"id": true,
|
|
804
|
+
"description": "Read an Omni call's transcript, summary and recording availability."
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
"command": "omni summary",
|
|
808
|
+
"method": "GET",
|
|
809
|
+
"path": "/v1/omni/calls/{id}/summary",
|
|
810
|
+
"id": true,
|
|
811
|
+
"description": "Read the structured summary of an Omni session when generated."
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
"command": "omni transcript",
|
|
815
|
+
"method": "GET",
|
|
816
|
+
"path": "/v1/omni/calls/{id}/transcript",
|
|
817
|
+
"id": true,
|
|
818
|
+
"description": "Read the complete Omni session transcript."
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
"command": "omni recording",
|
|
822
|
+
"method": "GET",
|
|
823
|
+
"path": "/v1/omni/calls/{id}/recording",
|
|
824
|
+
"id": true,
|
|
825
|
+
"binary": true,
|
|
826
|
+
"description": "Download the recording with --out; recording must have been enabled for this call."
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
"command": "numbers search",
|
|
830
|
+
"method": "GET",
|
|
831
|
+
"path": "/v1/telephony/available",
|
|
832
|
+
"query": {
|
|
833
|
+
"country": "string",
|
|
834
|
+
"area-code": "string",
|
|
835
|
+
"contains": "string",
|
|
836
|
+
"limit": "string"
|
|
837
|
+
},
|
|
838
|
+
"description": "Search managed numbers; inspect provisioning availability before buying. Scope telephony:manage."
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
"command": "numbers list",
|
|
842
|
+
"method": "GET",
|
|
843
|
+
"path": "/v1/telephony/numbers",
|
|
844
|
+
"query": {
|
|
845
|
+
"include-released": "boolean"
|
|
846
|
+
},
|
|
847
|
+
"description": "List the organization's managed numbers and current Agent bindings."
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
"command": "numbers buy",
|
|
851
|
+
"method": "POST",
|
|
852
|
+
"path": "/v1/telephony/numbers",
|
|
853
|
+
"body": true,
|
|
854
|
+
"confirm": true,
|
|
855
|
+
"idempotencyRequired": true,
|
|
856
|
+
"description": "Purchase a number from JSON. Requires --confirm and --idempotency-key; may incur recurring charges."
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
"command": "numbers bind",
|
|
860
|
+
"method": "POST",
|
|
861
|
+
"path": "/v1/telephony/numbers/{id}/assign",
|
|
862
|
+
"id": true,
|
|
863
|
+
"body": true,
|
|
864
|
+
"description": "Bind an existing number using JSON {agent_id}; null unassigns. Read numbers list first."
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
"command": "calls list",
|
|
868
|
+
"method": "GET",
|
|
869
|
+
"path": "/v1/telephony/calls",
|
|
870
|
+
"description": "List outbound call requests and their normalized status."
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
"command": "calls create",
|
|
874
|
+
"method": "POST",
|
|
875
|
+
"path": "/v1/telephony/calls",
|
|
876
|
+
"body": true,
|
|
877
|
+
"confirm": true,
|
|
878
|
+
"idempotencyRequired": true,
|
|
879
|
+
"description": "Dial a real destination from JSON. Requires --confirm and --idempotency-key; this is not a simulated test call."
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
"command": "calls get",
|
|
883
|
+
"method": "GET",
|
|
884
|
+
"path": "/v1/telephony/calls/{id}",
|
|
885
|
+
"id": true,
|
|
886
|
+
"description": "Get an outbound call by its returned ID; retain artifact identifiers separately."
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
"command": "calls wait",
|
|
890
|
+
"method": "GET",
|
|
891
|
+
"path": "/v1/telephony/calls/{id}",
|
|
892
|
+
"id": true,
|
|
893
|
+
"wait": {
|
|
894
|
+
"success": [
|
|
895
|
+
"completed"
|
|
896
|
+
],
|
|
897
|
+
"failure": [
|
|
898
|
+
"failed",
|
|
899
|
+
"canceled",
|
|
900
|
+
"dispatch_unknown"
|
|
901
|
+
]
|
|
902
|
+
},
|
|
903
|
+
"description": "Wait with a deadline. dispatch_unknown stops polling for manual inspection; never redial automatically."
|
|
904
|
+
},
|
|
663
905
|
{
|
|
664
906
|
"command": "agents list",
|
|
665
907
|
"method": "GET",
|
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.
|
package/dist/cli-dx.d.ts
CHANGED
package/dist/cli-dx.js
CHANGED
|
@@ -8,6 +8,12 @@ export const shortFlags = Object.assign(Object.create(null), {
|
|
|
8
8
|
"-t": "--text", "-p": "--profile", "-j": "--json",
|
|
9
9
|
});
|
|
10
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."] },
|
|
11
17
|
{ name: "auth", title: "Sign in and check access", description: "Save a named profile and inspect the current credential.",
|
|
12
18
|
commands: ["pyai login --profile work", "pyai whoami --profile work --json", "pyai use work"],
|
|
13
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."] },
|
|
@@ -28,5 +34,5 @@ export const recipes = [
|
|
|
28
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."] },
|
|
29
35
|
{ name: "ci", title: "Use PyAI in a build", description: "Check access and produce an audio artifact with machine-readable output.",
|
|
30
36
|
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.
|
|
32
|
-
];
|
|
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" }));
|
package/dist/cli-http.js
CHANGED
|
@@ -234,12 +234,15 @@ export class CliHttp {
|
|
|
234
234
|
const duration = timeout(options.timeoutMs ?? this.timeoutMs);
|
|
235
235
|
const deadline = Date.now() + duration;
|
|
236
236
|
const abort = new AbortController();
|
|
237
|
-
const
|
|
237
|
+
const timedOut = () => new CliError("timeout", `Request timed out after ${duration} ms.`, 4);
|
|
238
|
+
const timer = setTimeout(() => abort.abort(timedOut()), duration);
|
|
238
239
|
const cleanup = () => clearTimeout(timer);
|
|
239
240
|
const retries = method === "GET" || method === "HEAD" ? this.maxRetries : 0;
|
|
240
241
|
let streaming = false;
|
|
241
242
|
try {
|
|
242
243
|
for (let attempt = 0;; attempt++) {
|
|
244
|
+
if (Date.now() >= deadline)
|
|
245
|
+
throw timedOut();
|
|
243
246
|
let delay = retryDelay(null, attempt);
|
|
244
247
|
try {
|
|
245
248
|
const response = await fetch(url, { method, headers, body, signal: abort.signal, redirect: "manual" });
|
|
@@ -275,7 +278,13 @@ export class CliHttp {
|
|
|
275
278
|
if (abort.signal.aborted || failure.code !== "network_error" || failure.exitCode !== 4 || attempt >= retries)
|
|
276
279
|
throw failure;
|
|
277
280
|
}
|
|
278
|
-
|
|
281
|
+
const remaining = Math.max(0, deadline - Date.now());
|
|
282
|
+
const retryExceedsDeadline = delay >= remaining;
|
|
283
|
+
await pause(Math.min(delay, remaining), abort.signal);
|
|
284
|
+
// A clipped wait is terminal even if its timer runs before abort.
|
|
285
|
+
// Otherwise the next request can ignore Retry-After at the deadline.
|
|
286
|
+
if (retryExceedsDeadline)
|
|
287
|
+
throw timedOut();
|
|
279
288
|
}
|
|
280
289
|
}
|
|
281
290
|
finally {
|
package/dist/cli-init.js
CHANGED
|
@@ -22,6 +22,8 @@ Read it before generating PyAI calls. Do not place credentials in this file.
|
|
|
22
22
|
- Canonical live OpenAPI: https://api.pyai.com/openapi.json
|
|
23
23
|
- API docs: https://docs.pyai.com
|
|
24
24
|
- Agent index: https://api.pyai.com/llms.txt
|
|
25
|
+
- Speech and calling recipes: https://pyai.com/agents/speech-calling.md
|
|
26
|
+
- Omni frame contract: https://api.pyai.com/omni-frames.json
|
|
25
27
|
- API origin: https://api.pyai.com; REST routes below include /v1.
|
|
26
28
|
|
|
27
29
|
CLI schema describes the installed executable; live OpenAPI describes the
|
|
@@ -102,7 +104,8 @@ async jobs' language field is a Recap summarization hint.
|
|
|
102
104
|
The CLI handles REST and job workflows; it does not run a realtime microphone
|
|
103
105
|
or WebSocket session. Use the SDK and protocol docs for Hear streaming or Omni.
|
|
104
106
|
Omni connects to \`wss://api.pyai.com/v1/omni?format=pcm16&rate=24000\`.
|
|
105
|
-
|
|
107
|
+
Use \`pyai.omni.connect({ rate, configure })\`; a managed Agent is optional.
|
|
108
|
+
Wait for configured before non-silent audio and clear playback on barge-in/flush. Never place
|
|
106
109
|
long-lived API keys in a public frontend. Use the documented ephemeral session
|
|
107
110
|
flow for browser apps. Do not add unsupported grounding/Cue fields.
|
|
108
111
|
|
|
@@ -124,7 +127,7 @@ const install = `## Install the CLI
|
|
|
124
127
|
Install the published CLI with Node.js 22 or newer recommended:
|
|
125
128
|
|
|
126
129
|
\`\`\`bash
|
|
127
|
-
npm install -g @pyai/sdk@0.
|
|
130
|
+
npm install -g @pyai/sdk@0.6.0
|
|
128
131
|
pyai --version
|
|
129
132
|
\`\`\`
|
|
130
133
|
|
|
@@ -217,7 +220,7 @@ and product calls below are separate explicit steps. Read \`PYAI.md\` first.
|
|
|
217
220
|
Install the official SDK:
|
|
218
221
|
|
|
219
222
|
\`\`\`bash
|
|
220
|
-
npm install @pyai/sdk@0.
|
|
223
|
+
npm install @pyai/sdk@0.6.0
|
|
221
224
|
cp .env.example .env
|
|
222
225
|
\`\`\`
|
|
223
226
|
|
package/dist/cli-routes.d.ts
CHANGED
package/dist/cli-routes.js
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
export const routes = [
|
|
2
|
+
{ command: "omni calls", method: "GET", path: "/v1/omni/calls", query: { "session-label": "string", limit: "string", cursor: "string" }, description: "List Omni sessions including inbound calls. Scope omni:read." },
|
|
3
|
+
{ command: "omni get", method: "GET", path: "/v1/omni/calls/{id}", id: true, description: "Read an Omni call's transcript, summary and recording availability." },
|
|
4
|
+
{ command: "omni summary", method: "GET", path: "/v1/omni/calls/{id}/summary", id: true, description: "Read the structured summary of an Omni session when generated." },
|
|
5
|
+
{ command: "omni transcript", method: "GET", path: "/v1/omni/calls/{id}/transcript", id: true, description: "Read the complete Omni session transcript." },
|
|
6
|
+
{ command: "omni recording", method: "GET", path: "/v1/omni/calls/{id}/recording", id: true, binary: true, description: "Download the recording with --out; recording must have been enabled for this call." },
|
|
7
|
+
{ command: "numbers search", method: "GET", path: "/v1/telephony/available", query: { country: "string", "area-code": "string", contains: "string", limit: "string" }, description: "Search managed numbers; inspect provisioning availability before buying. Scope telephony:manage." },
|
|
8
|
+
{ command: "numbers list", method: "GET", path: "/v1/telephony/numbers", query: { "include-released": "boolean" }, description: "List the organization's managed numbers and current Agent bindings." },
|
|
9
|
+
{ command: "numbers buy", method: "POST", path: "/v1/telephony/numbers", body: true, confirm: true, idempotencyRequired: true, description: "Purchase a number from JSON. Requires --confirm and --idempotency-key; may incur recurring charges." },
|
|
10
|
+
{ command: "numbers bind", method: "POST", path: "/v1/telephony/numbers/{id}/assign", id: true, body: true, description: "Bind an existing number using JSON {agent_id}; null unassigns. Read numbers list first." },
|
|
11
|
+
{ command: "calls list", method: "GET", path: "/v1/telephony/calls", description: "List outbound call requests and their normalized status." },
|
|
12
|
+
{ command: "calls create", method: "POST", path: "/v1/telephony/calls", body: true, confirm: true, idempotencyRequired: true, description: "Dial a real destination from JSON. Requires --confirm and --idempotency-key; this is not a simulated test call." },
|
|
13
|
+
{ command: "calls get", method: "GET", path: "/v1/telephony/calls/{id}", id: true, description: "Get an outbound call by its returned ID; retain artifact identifiers separately." },
|
|
14
|
+
{ command: "calls wait", method: "GET", path: "/v1/telephony/calls/{id}", id: true, wait: { success: ["completed"], failure: ["failed", "canceled", "dispatch_unknown"] }, description: "Wait with a deadline. dispatch_unknown stops polling for manual inspection; never redial automatically." },
|
|
2
15
|
{ command: "agents list", method: "GET", path: "/v1/agents", description: "List managed Agent profiles." },
|
|
3
16
|
{ command: "agents get", method: "GET", path: "/v1/agents/{id}", id: true, description: "Get an Agent profile." },
|
|
4
17
|
{ command: "agents create", method: "POST", path: "/v1/agents", body: true, description: "Create an Agent profile from JSON; name is required." },
|
package/dist/cli.js
CHANGED
|
@@ -44,6 +44,7 @@ const commands = [
|
|
|
44
44
|
{ command: "doctor", description: "Diagnose key, catalogs, and a Speak to Hear round-trip" },
|
|
45
45
|
...routes.map((r) => ({ command: r.command, description: r.description, args: r.id ? ["id"] : [], flags: {
|
|
46
46
|
...(r.body ? bodyFlags : {}), ...(r.binary ? outputFlags : {}), ...(r.wait ? waitFlags : {}),
|
|
47
|
+
...(r.query ?? {}), ...(r.confirm ? { confirm: "boolean" } : {}),
|
|
47
48
|
...(r.command.endsWith(" list") || r.command === "amd calls" ? { limit: "string", cursor: "string" } : {}),
|
|
48
49
|
} })),
|
|
49
50
|
];
|
|
@@ -404,7 +405,7 @@ async function main() {
|
|
|
404
405
|
if (flags.json)
|
|
405
406
|
output({ name: "pyai", globals, aliases, short_flags: shortFlags, commands: visible }, flags, undefined, false);
|
|
406
407
|
else if (!spec && !group)
|
|
407
|
-
process.stdout.write(`PyAI CLI — speech, agents, and automation\n\nStart here\n pyai login Sign in through your browser\n pyai speak "Hello from PyAI" -o hello.wav Turn text into audio\n pyai hear call.wav --text-only Print a transcript\n pyai dub call.wav --from en --to hi -o hi.wav\n Submit, wait, and save dubbed audio\n pyai init voice-demo --template typescript Create a starter project offline\n\nDiscover\n pyai recipes [name] Copyable workflows\n pyai help <command> Command options and examples\n pyai help all Every command\n pyai schema [command] -j Machine-readable command definitions\n\nGroups\n auth profiles models voices agents jobs clones design\n cast dub recap trace tools vocabulary amd\n\nShortcuts: login, logout, whoami, use, say (speak), hear (transcribe)\nOptions: -o FILE -f FILE -t TEXT -p PROFILE -j (JSON)\nGlobal: --base-url URL --api-key KEY --timeout SECONDS --retries N --dry-run\nCredentials: pyai login or PYAI_API_KEY. Run pyai doctor for a usage-consuming diagnostic.\n`);
|
|
408
|
+
process.stdout.write(`PyAI CLI — speech, agents, and automation\n\nStart here\n pyai login Sign in through your browser\n pyai speak "Hello from PyAI" -o hello.wav Turn text into audio\n pyai hear call.wav --text-only Print a transcript\n pyai dub call.wav --from en --to hi -o hi.wav\n Submit, wait, and save dubbed audio\n pyai init voice-demo --template typescript Create a starter project offline\n\nDiscover\n pyai recipes [name] Copyable workflows\n pyai help <command> Command options and examples\n pyai help all Every command\n pyai schema [command] -j Machine-readable command definitions\n\nGroups\n auth profiles models voices agents jobs clones design\n cast dub recap trace tools vocabulary amd numbers calls omni\n\nShortcuts: login, logout, whoami, use, say (speak), hear (transcribe)\nOptions: -o FILE -f FILE -t TEXT -p PROFILE -j (JSON)\nGlobal: --base-url URL --api-key KEY --timeout SECONDS --retries N --dry-run\nCredentials: pyai login or PYAI_API_KEY. Run pyai doctor for a usage-consuming diagnostic.\n`);
|
|
408
409
|
else
|
|
409
410
|
process.stdout.write(`PyAI CLI — speech, agents, and automation\n\n${visible.map(c => ` pyai ${c.command}${(c.args ?? []).map(a => c.optionalArgs ? ` [${a}${c.variadic ? "..." : ""}]` : ` <${a}>`).join("")}\n ${c.description}${c.flags ? `\n Options: ${Object.keys(c.flags).map(k => `--${k}${c.flags[k] === "boolean" ? "" : " VALUE"}`).join(" ")}` : ""}${c.example ? `\n ${c.example}` : ""}`).join("\n")}\n\nGlobal: --profile NAME --json --timeout SECONDS --retries N --dry-run\n --base-url URL --api-key KEY --help --version\nShort flags: -o FILE -f FILE -t TEXT -p PROFILE -j (JSON)\nAuth: pyai login or PYAI_API_KEY. Discover: pyai schema --json\n`);
|
|
410
411
|
return;
|
|
@@ -666,13 +667,21 @@ async function main() {
|
|
|
666
667
|
return;
|
|
667
668
|
}
|
|
668
669
|
const route = routes.find(r => r.command === cmd);
|
|
670
|
+
if (route.confirm && !flags.confirm && !flags["dry-run"]) {
|
|
671
|
+
throw new CliError("needs_human", "Review the request and supply --confirm only after purchase or dialing is authorized.", 2, { docs_url: "https://pyai.com/agents/speech-calling.md", console_url: "https://console.pyai.com/telephony" });
|
|
672
|
+
}
|
|
673
|
+
if (route.idempotencyRequired) {
|
|
674
|
+
const key = required(flags, "idempotency-key");
|
|
675
|
+
if (!key.trim() || key.length > 255 || /[\r\n]/.test(key))
|
|
676
|
+
usage("--idempotency-key must be a non-empty header-safe string of at most 255 characters");
|
|
677
|
+
}
|
|
669
678
|
let path = route.path.replace("{id}", route.id ? safeId(args[0]) : "");
|
|
670
679
|
if (value(flags, "limit") !== undefined) {
|
|
671
680
|
const limit = number(flags, "limit", 20, 1, 100);
|
|
672
681
|
if (!Number.isInteger(limit))
|
|
673
682
|
usage("--limit must be an integer");
|
|
674
683
|
}
|
|
675
|
-
path = withQuery(path, { limit: value(flags, "limit"), cursor: value(flags, "cursor") });
|
|
684
|
+
path = withQuery(path, { limit: value(flags, "limit"), cursor: value(flags, "cursor"), ...Object.fromEntries(Object.keys(route.query ?? {}).filter(k => flags[k] !== undefined).map(k => [k.replaceAll("-", "_"), String(flags[k])])) });
|
|
676
685
|
const data = route.body ? await jsonInput(flags, true) : undefined;
|
|
677
686
|
const out = route.binary ? required(flags, "out") : undefined;
|
|
678
687
|
if (out)
|
package/dist/index.d.ts
CHANGED
|
@@ -134,8 +134,19 @@ export interface SpeechParams {
|
|
|
134
134
|
*/
|
|
135
135
|
temperature?: number;
|
|
136
136
|
}
|
|
137
|
-
export
|
|
137
|
+
export type CreateJobParams = CreateJobOptions & ({
|
|
138
138
|
audio_url: string;
|
|
139
|
+
gpt_live?: never;
|
|
140
|
+
} | {
|
|
141
|
+
gpt_live: {
|
|
142
|
+
session_id: string;
|
|
143
|
+
};
|
|
144
|
+
audio_url?: never;
|
|
145
|
+
});
|
|
146
|
+
export interface CreateJobOptions {
|
|
147
|
+
/** Requires Trace entitlement; incompatible with channel/diarize. */
|
|
148
|
+
trace?: boolean;
|
|
149
|
+
rule_pack?: Record<string, unknown>;
|
|
139
150
|
model?: string;
|
|
140
151
|
diarize?: boolean;
|
|
141
152
|
channel?: boolean;
|
|
@@ -535,6 +546,8 @@ export declare const OmniEvent: {
|
|
|
535
546
|
readonly Hello: "hello";
|
|
536
547
|
/** Ack for your `configure` frame (echoes the resolved `voice_id`). */
|
|
537
548
|
readonly Configured: "configured";
|
|
549
|
+
/** Served voice capabilities changed, for example after synthesis fallback. */
|
|
550
|
+
readonly VoiceCapabilities: "voice_capabilities";
|
|
538
551
|
/** Session is live; includes the resolved agent + audio caps. */
|
|
539
552
|
readonly SessionStarted: "session_started";
|
|
540
553
|
/** Turn boundary (user/assistant speaking). */
|
|
@@ -1085,16 +1098,21 @@ export interface AmdCall extends AmdCallSummary {
|
|
|
1085
1098
|
}
|
|
1086
1099
|
/**
|
|
1087
1100
|
* A mid-call AMD decision event pushed on the stream (and to the per-call
|
|
1088
|
-
* TwiML `webhook`). Carries the
|
|
1089
|
-
*
|
|
1090
|
-
* (`AmdCall`) and the `amd.call.completed` webhook instead.
|
|
1101
|
+
* TwiML `webhook`). Carries the routing class and an optional machine subtype.
|
|
1102
|
+
* Stored `AmdCall` records fold the subtype into `answered_by`.
|
|
1091
1103
|
*/
|
|
1092
1104
|
export interface AmdDecisionEvent {
|
|
1093
1105
|
event?: "amd";
|
|
1094
1106
|
call_id?: string;
|
|
1095
1107
|
answered_by?: AmdWireAnsweredBy;
|
|
1096
1108
|
answered_by_twilio?: string | null;
|
|
1109
|
+
subtype?: string;
|
|
1110
|
+
/** A human or automated answering party was identified. */
|
|
1111
|
+
party_detected?: boolean;
|
|
1112
|
+
/** Classification events return false; detection does not establish recording readiness. */
|
|
1113
|
+
voicemail_ready?: boolean;
|
|
1097
1114
|
confidence?: number | null;
|
|
1115
|
+
/** Processed inbound audio through the decision, not time since carrier answer. */
|
|
1098
1116
|
decision_ms?: number | null;
|
|
1099
1117
|
reason?: string | null;
|
|
1100
1118
|
[k: string]: unknown;
|
package/dist/index.js
CHANGED
|
@@ -286,6 +286,8 @@ export const OmniEvent = {
|
|
|
286
286
|
Hello: "hello",
|
|
287
287
|
/** Ack for your `configure` frame (echoes the resolved `voice_id`). */
|
|
288
288
|
Configured: "configured",
|
|
289
|
+
/** Served voice capabilities changed, for example after synthesis fallback. */
|
|
290
|
+
VoiceCapabilities: "voice_capabilities",
|
|
289
291
|
/** Session is live; includes the resolved agent + audio caps. */
|
|
290
292
|
SessionStarted: "session_started",
|
|
291
293
|
/** Turn boundary (user/assistant speaking). */
|
|
@@ -495,6 +497,17 @@ export class OmniConnection {
|
|
|
495
497
|
try {
|
|
496
498
|
const parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(1)));
|
|
497
499
|
if (parsed?.event === OmniEvent.Transcript) {
|
|
500
|
+
// The deployed engine also sends this bounded synthesis-input
|
|
501
|
+
// advisory. It is not caller ASR or proof that speech was played.
|
|
502
|
+
const keys = Object.keys(parsed);
|
|
503
|
+
const text = omniTranscriptText(parsed.text);
|
|
504
|
+
if (parsed.role === "assistant" && parsed.final === true && text?.trim()
|
|
505
|
+
&& keys.length === 4
|
|
506
|
+
&& keys.every((key) => ["event", "role", "text", "final"].includes(key))) {
|
|
507
|
+
this.opts.onEvent?.(parsed);
|
|
508
|
+
this.opts.onTranscript?.({ event: "transcript", role: "assistant", text, final: true, mode: "replace" });
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
498
511
|
this.opts.onError?.(new Error("Omni transcript events must use a binary 0x02 frame"));
|
|
499
512
|
return;
|
|
500
513
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyai/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Official TypeScript/JavaScript SDK for PyAI, speech-to-text (Hear), text-to-speech (Speak), realtime voice agents (Omni), and call compliance (Trace).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
package/src/cli-dx.ts
CHANGED
|
@@ -7,8 +7,14 @@ export const shortFlags: Record<string, string> = Object.assign(Object.create(nu
|
|
|
7
7
|
"-h": "--help", "-v": "--version", "-o": "--out", "-f": "--file",
|
|
8
8
|
"-t": "--text", "-p": "--profile", "-j": "--json",
|
|
9
9
|
});
|
|
10
|
-
export interface Recipe { name: string; title: string; description: string; commands: string[]; notes: string[] }
|
|
10
|
+
export interface Recipe { name: string; title: string; description: string; commands: string[]; notes: string[]; docs_url: string }
|
|
11
11
|
export const recipes: Recipe[] = [
|
|
12
|
+
{ name: "calling", title: "Set up a managed phone Agent", description: "Inspect numbers, bind a profile, and retrieve call artifacts through the public API.",
|
|
13
|
+
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"],
|
|
14
|
+
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."] },
|
|
15
|
+
{ name: "omni", title: "Build a realtime conversation with the SDK", description: "Use the official SDK for binary framing, subprotocol auth, and typed events.",
|
|
16
|
+
commands: ["pyai schema --openapi --json", "pyai init voice-demo --template typescript", "pyai voices --json"],
|
|
17
|
+
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."] },
|
|
12
18
|
{ name: "auth", title: "Sign in and check access", description: "Save a named profile and inspect the current credential.",
|
|
13
19
|
commands: ["pyai login --profile work", "pyai whoami --profile work --json", "pyai use work"],
|
|
14
20
|
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."] },
|
|
@@ -29,5 +35,5 @@ export const recipes: Recipe[] = [
|
|
|
29
35
|
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."] },
|
|
30
36
|
{ name: "ci", title: "Use PyAI in a build", description: "Check access and produce an audio artifact with machine-readable output.",
|
|
31
37
|
commands: ["pyai whoami --json", 'pyai speak "Your build is ready" -o build.wav --json', "pyai doctor --json"],
|
|
32
|
-
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.
|
|
33
|
-
];
|
|
38
|
+
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."] },
|
|
39
|
+
].map(recipe => ({ ...recipe, docs_url: "https://pyai.com/agents/speech-calling.md" }));
|
package/src/cli-http.ts
CHANGED
|
@@ -232,12 +232,14 @@ export class CliHttp {
|
|
|
232
232
|
const duration = timeout(options.timeoutMs ?? this.timeoutMs);
|
|
233
233
|
const deadline = Date.now() + duration;
|
|
234
234
|
const abort = new AbortController();
|
|
235
|
-
const
|
|
235
|
+
const timedOut = () => new CliError("timeout", `Request timed out after ${duration} ms.`, 4);
|
|
236
|
+
const timer = setTimeout(() => abort.abort(timedOut()), duration);
|
|
236
237
|
const cleanup = () => clearTimeout(timer);
|
|
237
238
|
const retries = method === "GET" || method === "HEAD" ? this.maxRetries : 0;
|
|
238
239
|
let streaming = false;
|
|
239
240
|
try {
|
|
240
241
|
for (let attempt = 0; ; attempt++) {
|
|
242
|
+
if (Date.now() >= deadline) throw timedOut();
|
|
241
243
|
let delay = retryDelay(null, attempt);
|
|
242
244
|
try {
|
|
243
245
|
const response = await fetch(url, { method, headers, body, signal: abort.signal, redirect: "manual" });
|
|
@@ -264,7 +266,12 @@ export class CliHttp {
|
|
|
264
266
|
const failure = this.transportError(error, abort.signal);
|
|
265
267
|
if (abort.signal.aborted || failure.code !== "network_error" || failure.exitCode !== 4 || attempt >= retries) throw failure;
|
|
266
268
|
}
|
|
267
|
-
|
|
269
|
+
const remaining = Math.max(0, deadline - Date.now());
|
|
270
|
+
const retryExceedsDeadline = delay >= remaining;
|
|
271
|
+
await pause(Math.min(delay, remaining), abort.signal);
|
|
272
|
+
// A clipped wait is terminal even if its timer runs before abort.
|
|
273
|
+
// Otherwise the next request can ignore Retry-After at the deadline.
|
|
274
|
+
if (retryExceedsDeadline) throw timedOut();
|
|
268
275
|
}
|
|
269
276
|
} finally {
|
|
270
277
|
if (!streaming) cleanup();
|
package/src/cli-init.ts
CHANGED
|
@@ -43,6 +43,8 @@ Read it before generating PyAI calls. Do not place credentials in this file.
|
|
|
43
43
|
- Canonical live OpenAPI: https://api.pyai.com/openapi.json
|
|
44
44
|
- API docs: https://docs.pyai.com
|
|
45
45
|
- Agent index: https://api.pyai.com/llms.txt
|
|
46
|
+
- Speech and calling recipes: https://pyai.com/agents/speech-calling.md
|
|
47
|
+
- Omni frame contract: https://api.pyai.com/omni-frames.json
|
|
46
48
|
- API origin: https://api.pyai.com; REST routes below include /v1.
|
|
47
49
|
|
|
48
50
|
CLI schema describes the installed executable; live OpenAPI describes the
|
|
@@ -123,7 +125,8 @@ async jobs' language field is a Recap summarization hint.
|
|
|
123
125
|
The CLI handles REST and job workflows; it does not run a realtime microphone
|
|
124
126
|
or WebSocket session. Use the SDK and protocol docs for Hear streaming or Omni.
|
|
125
127
|
Omni connects to \`wss://api.pyai.com/v1/omni?format=pcm16&rate=24000\`.
|
|
126
|
-
|
|
128
|
+
Use \`pyai.omni.connect({ rate, configure })\`; a managed Agent is optional.
|
|
129
|
+
Wait for configured before non-silent audio and clear playback on barge-in/flush. Never place
|
|
127
130
|
long-lived API keys in a public frontend. Use the documented ephemeral session
|
|
128
131
|
flow for browser apps. Do not add unsupported grounding/Cue fields.
|
|
129
132
|
|
|
@@ -146,7 +149,7 @@ const install = `## Install the CLI
|
|
|
146
149
|
Install the published CLI with Node.js 22 or newer recommended:
|
|
147
150
|
|
|
148
151
|
\`\`\`bash
|
|
149
|
-
npm install -g @pyai/sdk@0.
|
|
152
|
+
npm install -g @pyai/sdk@0.6.0
|
|
150
153
|
pyai --version
|
|
151
154
|
\`\`\`
|
|
152
155
|
|
|
@@ -239,7 +242,7 @@ and product calls below are separate explicit steps. Read \`PYAI.md\` first.
|
|
|
239
242
|
Install the official SDK:
|
|
240
243
|
|
|
241
244
|
\`\`\`bash
|
|
242
|
-
npm install @pyai/sdk@0.
|
|
245
|
+
npm install @pyai/sdk@0.6.0
|
|
243
246
|
cp .env.example .env
|
|
244
247
|
\`\`\`
|
|
245
248
|
|
package/src/cli-routes.ts
CHANGED
|
@@ -8,9 +8,25 @@ export interface CliRoute {
|
|
|
8
8
|
id?: boolean;
|
|
9
9
|
binary?: boolean;
|
|
10
10
|
wait?: { success: string[]; failure: string[] };
|
|
11
|
+
query?: Record<string, "string" | "boolean">;
|
|
12
|
+
confirm?: boolean;
|
|
13
|
+
idempotencyRequired?: boolean;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
export const routes: CliRoute[] = [
|
|
17
|
+
{ command: "omni calls", method: "GET", path: "/v1/omni/calls", query: { "session-label": "string", limit: "string", cursor: "string" }, description: "List Omni sessions including inbound calls. Scope omni:read." },
|
|
18
|
+
{ command: "omni get", method: "GET", path: "/v1/omni/calls/{id}", id: true, description: "Read an Omni call's transcript, summary and recording availability." },
|
|
19
|
+
{ command: "omni summary", method: "GET", path: "/v1/omni/calls/{id}/summary", id: true, description: "Read the structured summary of an Omni session when generated." },
|
|
20
|
+
{ command: "omni transcript", method: "GET", path: "/v1/omni/calls/{id}/transcript", id: true, description: "Read the complete Omni session transcript." },
|
|
21
|
+
{ command: "omni recording", method: "GET", path: "/v1/omni/calls/{id}/recording", id: true, binary: true, description: "Download the recording with --out; recording must have been enabled for this call." },
|
|
22
|
+
{ command: "numbers search", method: "GET", path: "/v1/telephony/available", query: { country: "string", "area-code": "string", contains: "string", limit: "string" }, description: "Search managed numbers; inspect provisioning availability before buying. Scope telephony:manage." },
|
|
23
|
+
{ command: "numbers list", method: "GET", path: "/v1/telephony/numbers", query: { "include-released": "boolean" }, description: "List the organization's managed numbers and current Agent bindings." },
|
|
24
|
+
{ command: "numbers buy", method: "POST", path: "/v1/telephony/numbers", body: true, confirm: true, idempotencyRequired: true, description: "Purchase a number from JSON. Requires --confirm and --idempotency-key; may incur recurring charges." },
|
|
25
|
+
{ command: "numbers bind", method: "POST", path: "/v1/telephony/numbers/{id}/assign", id: true, body: true, description: "Bind an existing number using JSON {agent_id}; null unassigns. Read numbers list first." },
|
|
26
|
+
{ command: "calls list", method: "GET", path: "/v1/telephony/calls", description: "List outbound call requests and their normalized status." },
|
|
27
|
+
{ command: "calls create", method: "POST", path: "/v1/telephony/calls", body: true, confirm: true, idempotencyRequired: true, description: "Dial a real destination from JSON. Requires --confirm and --idempotency-key; this is not a simulated test call." },
|
|
28
|
+
{ command: "calls get", method: "GET", path: "/v1/telephony/calls/{id}", id: true, description: "Get an outbound call by its returned ID; retain artifact identifiers separately." },
|
|
29
|
+
{ command: "calls wait", method: "GET", path: "/v1/telephony/calls/{id}", id: true, wait: { success: ["completed"], failure: ["failed", "canceled", "dispatch_unknown"] }, description: "Wait with a deadline. dispatch_unknown stops polling for manual inspection; never redial automatically." },
|
|
14
30
|
{ command: "agents list", method: "GET", path: "/v1/agents", description: "List managed Agent profiles." },
|
|
15
31
|
{ command: "agents get", method: "GET", path: "/v1/agents/{id}", id: true, description: "Get an Agent profile." },
|
|
16
32
|
{ command: "agents create", method: "POST", path: "/v1/agents", body: true, description: "Create an Agent profile from JSON; name is required." },
|
package/src/cli.ts
CHANGED
|
@@ -48,6 +48,7 @@ const commands: Command[] = [
|
|
|
48
48
|
{ command: "doctor", description: "Diagnose key, catalogs, and a Speak to Hear round-trip" },
|
|
49
49
|
...routes.map((r): Command => ({ command: r.command, description: r.description, args: r.id ? ["id"] : [], flags: {
|
|
50
50
|
...(r.body ? bodyFlags : {}), ...(r.binary ? outputFlags : {}), ...(r.wait ? waitFlags : {}),
|
|
51
|
+
...(r.query ?? {}), ...(r.confirm ? { confirm: "boolean" as const } : {}),
|
|
51
52
|
...(r.command.endsWith(" list") || r.command === "amd calls" ? { limit: "string" as const, cursor: "string" as const } : {}),
|
|
52
53
|
} })),
|
|
53
54
|
];
|
|
@@ -269,7 +270,7 @@ async function main(): Promise<void> {
|
|
|
269
270
|
const grouped = group ? commands.filter(c => c.command.startsWith(`${group} `)) : commands;
|
|
270
271
|
const visible = spec ? [spec, ...commands.filter(c => c.command.startsWith(`${spec.command} `))] : grouped.length ? grouped : commands;
|
|
271
272
|
if (flags.json) output({ name: "pyai", globals, aliases, short_flags: shortFlags, commands: visible }, flags, undefined, false);
|
|
272
|
-
else if (!spec && !group) process.stdout.write(`PyAI CLI — speech, agents, and automation\n\nStart here\n pyai login Sign in through your browser\n pyai speak "Hello from PyAI" -o hello.wav Turn text into audio\n pyai hear call.wav --text-only Print a transcript\n pyai dub call.wav --from en --to hi -o hi.wav\n Submit, wait, and save dubbed audio\n pyai init voice-demo --template typescript Create a starter project offline\n\nDiscover\n pyai recipes [name] Copyable workflows\n pyai help <command> Command options and examples\n pyai help all Every command\n pyai schema [command] -j Machine-readable command definitions\n\nGroups\n auth profiles models voices agents jobs clones design\n cast dub recap trace tools vocabulary amd\n\nShortcuts: login, logout, whoami, use, say (speak), hear (transcribe)\nOptions: -o FILE -f FILE -t TEXT -p PROFILE -j (JSON)\nGlobal: --base-url URL --api-key KEY --timeout SECONDS --retries N --dry-run\nCredentials: pyai login or PYAI_API_KEY. Run pyai doctor for a usage-consuming diagnostic.\n`);
|
|
273
|
+
else if (!spec && !group) process.stdout.write(`PyAI CLI — speech, agents, and automation\n\nStart here\n pyai login Sign in through your browser\n pyai speak "Hello from PyAI" -o hello.wav Turn text into audio\n pyai hear call.wav --text-only Print a transcript\n pyai dub call.wav --from en --to hi -o hi.wav\n Submit, wait, and save dubbed audio\n pyai init voice-demo --template typescript Create a starter project offline\n\nDiscover\n pyai recipes [name] Copyable workflows\n pyai help <command> Command options and examples\n pyai help all Every command\n pyai schema [command] -j Machine-readable command definitions\n\nGroups\n auth profiles models voices agents jobs clones design\n cast dub recap trace tools vocabulary amd numbers calls omni\n\nShortcuts: login, logout, whoami, use, say (speak), hear (transcribe)\nOptions: -o FILE -f FILE -t TEXT -p PROFILE -j (JSON)\nGlobal: --base-url URL --api-key KEY --timeout SECONDS --retries N --dry-run\nCredentials: pyai login or PYAI_API_KEY. Run pyai doctor for a usage-consuming diagnostic.\n`);
|
|
273
274
|
else process.stdout.write(`PyAI CLI — speech, agents, and automation\n\n${visible.map(c => ` pyai ${c.command}${(c.args ?? []).map(a => c.optionalArgs ? ` [${a}${c.variadic ? "..." : ""}]` : ` <${a}>`).join("")}\n ${c.description}${c.flags ? `\n Options: ${Object.keys(c.flags).map(k => `--${k}${c.flags![k] === "boolean" ? "" : " VALUE"}`).join(" ")}` : ""}${c.example ? `\n ${c.example}` : ""}`).join("\n")}\n\nGlobal: --profile NAME --json --timeout SECONDS --retries N --dry-run\n --base-url URL --api-key KEY --help --version\nShort flags: -o FILE -f FILE -t TEXT -p PROFILE -j (JSON)\nAuth: pyai login or PYAI_API_KEY. Discover: pyai schema --json\n`);
|
|
274
275
|
return;
|
|
275
276
|
}
|
|
@@ -436,9 +437,16 @@ async function main(): Promise<void> {
|
|
|
436
437
|
output(await http.json("GET", path, { auth: cmd !== "schema" }), flags, undefined, cmd !== "schema"); return;
|
|
437
438
|
}
|
|
438
439
|
const route = routes.find(r => r.command === cmd)!;
|
|
440
|
+
if (route.confirm && !flags.confirm && !flags["dry-run"]) {
|
|
441
|
+
throw new CliError("needs_human", "Review the request and supply --confirm only after purchase or dialing is authorized.", 2, { docs_url: "https://pyai.com/agents/speech-calling.md", console_url: "https://console.pyai.com/telephony" });
|
|
442
|
+
}
|
|
443
|
+
if (route.idempotencyRequired) {
|
|
444
|
+
const key = required(flags, "idempotency-key");
|
|
445
|
+
if (!key.trim() || key.length > 255 || /[\r\n]/.test(key)) usage("--idempotency-key must be a non-empty header-safe string of at most 255 characters");
|
|
446
|
+
}
|
|
439
447
|
let path = route.path.replace("{id}", route.id ? safeId(args[0]!) : "");
|
|
440
448
|
if (value(flags, "limit") !== undefined) { const limit = number(flags, "limit", 20, 1, 100); if (!Number.isInteger(limit)) usage("--limit must be an integer"); }
|
|
441
|
-
path = withQuery(path, { limit: value(flags, "limit"), cursor: value(flags, "cursor") });
|
|
449
|
+
path = withQuery(path, { limit: value(flags, "limit"), cursor: value(flags, "cursor"), ...Object.fromEntries(Object.keys(route.query ?? {}).filter(k => flags[k] !== undefined).map(k => [k.replaceAll("-", "_"), String(flags[k])])) });
|
|
442
450
|
const data = route.body ? await jsonInput(flags, true) : undefined;
|
|
443
451
|
const out = route.binary ? required(flags, "out") : undefined;
|
|
444
452
|
if (out) await checkOutput(out, flags);
|
package/src/index.ts
CHANGED
|
@@ -165,8 +165,15 @@ function assertActiveSpeechParams(params: SpeechParams): void {
|
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
export
|
|
169
|
-
audio_url: string;
|
|
168
|
+
export type CreateJobParams = CreateJobOptions & (
|
|
169
|
+
| { audio_url: string; gpt_live?: never }
|
|
170
|
+
| { gpt_live: { session_id: string }; audio_url?: never }
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
export interface CreateJobOptions {
|
|
174
|
+
/** Requires Trace entitlement; incompatible with channel/diarize. */
|
|
175
|
+
trace?: boolean;
|
|
176
|
+
rule_pack?: Record<string, unknown>;
|
|
170
177
|
model?: string;
|
|
171
178
|
diarize?: boolean;
|
|
172
179
|
channel?: boolean;
|
|
@@ -731,6 +738,8 @@ export const OmniEvent = {
|
|
|
731
738
|
Hello: "hello",
|
|
732
739
|
/** Ack for your `configure` frame (echoes the resolved `voice_id`). */
|
|
733
740
|
Configured: "configured",
|
|
741
|
+
/** Served voice capabilities changed, for example after synthesis fallback. */
|
|
742
|
+
VoiceCapabilities: "voice_capabilities",
|
|
734
743
|
/** Session is live; includes the resolved agent + audio caps. */
|
|
735
744
|
SessionStarted: "session_started",
|
|
736
745
|
/** Turn boundary (user/assistant speaking). */
|
|
@@ -1066,6 +1075,17 @@ export class OmniConnection {
|
|
|
1066
1075
|
try {
|
|
1067
1076
|
const parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(1))) as OmniServerFrame;
|
|
1068
1077
|
if (parsed?.event === OmniEvent.Transcript) {
|
|
1078
|
+
// The deployed engine also sends this bounded synthesis-input
|
|
1079
|
+
// advisory. It is not caller ASR or proof that speech was played.
|
|
1080
|
+
const keys = Object.keys(parsed);
|
|
1081
|
+
const text = omniTranscriptText(parsed.text);
|
|
1082
|
+
if (parsed.role === "assistant" && parsed.final === true && text?.trim()
|
|
1083
|
+
&& keys.length === 4
|
|
1084
|
+
&& keys.every((key) => ["event", "role", "text", "final"].includes(key))) {
|
|
1085
|
+
this.opts.onEvent?.(parsed);
|
|
1086
|
+
this.opts.onTranscript?.({ event: "transcript", role: "assistant", text, final: true, mode: "replace" });
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1069
1089
|
this.opts.onError?.(new Error("Omni transcript events must use a binary 0x02 frame"));
|
|
1070
1090
|
return;
|
|
1071
1091
|
}
|
|
@@ -1593,16 +1613,21 @@ export interface AmdCall extends AmdCallSummary {
|
|
|
1593
1613
|
|
|
1594
1614
|
/**
|
|
1595
1615
|
* A mid-call AMD decision event pushed on the stream (and to the per-call
|
|
1596
|
-
* TwiML `webhook`). Carries the
|
|
1597
|
-
*
|
|
1598
|
-
* (`AmdCall`) and the `amd.call.completed` webhook instead.
|
|
1616
|
+
* TwiML `webhook`). Carries the routing class and an optional machine subtype.
|
|
1617
|
+
* Stored `AmdCall` records fold the subtype into `answered_by`.
|
|
1599
1618
|
*/
|
|
1600
1619
|
export interface AmdDecisionEvent {
|
|
1601
1620
|
event?: "amd";
|
|
1602
1621
|
call_id?: string;
|
|
1603
1622
|
answered_by?: AmdWireAnsweredBy;
|
|
1604
1623
|
answered_by_twilio?: string | null;
|
|
1624
|
+
subtype?: string;
|
|
1625
|
+
/** A human or automated answering party was identified. */
|
|
1626
|
+
party_detected?: boolean;
|
|
1627
|
+
/** Classification events return false; detection does not establish recording readiness. */
|
|
1628
|
+
voicemail_ready?: boolean;
|
|
1605
1629
|
confidence?: number | null;
|
|
1630
|
+
/** Processed inbound audio through the decision, not time since carrier answer. */
|
|
1606
1631
|
decision_ms?: number | null;
|
|
1607
1632
|
reason?: string | null;
|
|
1608
1633
|
[k: string]: unknown;
|