@palmyr/cli 1.6.0 → 1.8.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/dist/cli.js CHANGED
@@ -376,6 +376,336 @@ const WALLET_HELP = {
376
376
  { flag: '--src-decimals <n>', desc: 'Source token decimals', hint: 'default 18' },
377
377
  { flag: '--dst-decimals <n>', desc: 'Dest token decimals', hint: 'default 6 (USDC-like)' },
378
378
  ],
379
+ 'pay-preflight': [
380
+ { flag: '--chain <c>', desc: 'Override the default pay chain', hint: 'solana | base (default: config.defaultPayChain)' },
381
+ { flag: '--wallet <ID>', desc: 'Override the wallet to check', hint: 'default: config.defaultPayWalletId / PALMYR_PAY_WALLET / auto-pick' },
382
+ { flag: '--min-usdc <N>', desc: 'Required USDC balance to pass (default 0 — just check the wallet exists)' },
383
+ { flag: '--passphrase <p>', desc: 'Wallet passphrase if no OS-keychain session secret', hint: 'or PALMYR_WALLET_PASSPHRASE env' },
384
+ { flag: '(price)', desc: 'Free — one RPC call to read USDC balance' },
385
+ { flag: '(example)', desc: 'palmyr wallet pay-preflight --chain base --min-usdc 3 --json' },
386
+ { flag: '(note)', desc: 'Local-only version runs automatically before every paid command (set PALMYR_NO_PREFLIGHT=1 to disable)' },
387
+ ],
388
+ };
389
+ const PHONE_HELP = {
390
+ search: [
391
+ { flag: '--country <ISO>', desc: 'Country code', hint: 'default US (e.g. US, GB, AE)' },
392
+ { flag: '--limit <N>', desc: 'Max results to return' },
393
+ { flag: '(price)', desc: 'Free — no payment required' },
394
+ { flag: '(example)', desc: 'palmyr phone search --country US --json' },
395
+ ],
396
+ buy: [
397
+ { flag: '--country <ISO>', desc: 'Country code (required)', hint: 'e.g. US, GB' },
398
+ { flag: '--area <code>', desc: 'Preferred area code (optional, US only)' },
399
+ { flag: '(price)', desc: '$3.00 per number provisioned' },
400
+ { flag: '(example)', desc: 'palmyr phone buy --country US' },
401
+ ],
402
+ sms: [
403
+ { flag: '--id <PHONE_ID>', desc: 'Source phone number id (required)' },
404
+ { flag: '--to <+E.164>', desc: 'Destination phone number (required)', hint: 'e.g. +15551234567' },
405
+ { flag: '--body <text>', desc: 'Message body (required)' },
406
+ { flag: '(price)', desc: '$0.05 per SMS sent' },
407
+ { flag: '(example)', desc: 'palmyr phone sms --id PN_abc --to +15551234567 --body "hi"' },
408
+ ],
409
+ call: [
410
+ { flag: '--id <PHONE_ID>', desc: 'Source phone number id (required)' },
411
+ { flag: '--to <+E.164>', desc: 'Destination phone number (required)' },
412
+ { flag: '--tts <text>', desc: 'Text-to-speech to play on answer (optional)' },
413
+ { flag: '(price)', desc: '$0.10 per call placed' },
414
+ { flag: '(example)', desc: 'palmyr phone call --id PN_abc --to +15551234567 --tts "hello"' },
415
+ ],
416
+ list: [
417
+ { flag: '(no args)', desc: 'List phone numbers owned by your wallet' },
418
+ { flag: '(price)', desc: '$0.01 per call' },
419
+ { flag: '(example)', desc: 'palmyr phone list --json' },
420
+ ],
421
+ messages: [
422
+ { flag: '--id <PHONE_ID>', desc: 'Phone number id to read SMS for (required; positional also accepted)' },
423
+ { flag: '(price)', desc: '$0.02 per call' },
424
+ { flag: '(example)', desc: 'palmyr phone messages --id PN_abc' },
425
+ ],
426
+ calls: [
427
+ { flag: '--id <PHONE_ID>', desc: 'Phone number id to list calls for (required; positional also accepted)' },
428
+ { flag: '(price)', desc: '$0.02 per call' },
429
+ { flag: '(example)', desc: 'palmyr phone calls --id PN_abc' },
430
+ ],
431
+ release: [
432
+ { flag: '--id <PHONE_ID>', desc: 'Phone number id to release (required; positional also accepted)' },
433
+ { flag: '(price)', desc: '$0.01 per release (stops monthly Telnyx billing)' },
434
+ { flag: '(example)', desc: 'palmyr phone release --id PN_abc' },
435
+ ],
436
+ 'call-info': [
437
+ { flag: '--call <CALL_ID>', desc: 'Call control id (required; --id and positional also accepted)' },
438
+ { flag: '(price)', desc: '$0.02 per lookup' },
439
+ { flag: '(example)', desc: 'palmyr phone call-info --call CC_abc' },
440
+ ],
441
+ speak: [
442
+ { flag: '--call <CALL_ID>', desc: 'Call control id of a live call (required)' },
443
+ { flag: '--text <text>', desc: 'TTS text to speak (required; --tts alias accepted)' },
444
+ { flag: '--voice <name>', desc: 'TTS voice (optional, provider default otherwise)' },
445
+ { flag: '--language <code>', desc: 'TTS language code (optional, e.g. en-US)' },
446
+ { flag: '(price)', desc: '$0.08 per speak action' },
447
+ { flag: '(example)', desc: 'palmyr phone speak --call CC_abc --text "please hold"' },
448
+ ],
449
+ play: [
450
+ { flag: '--call <CALL_ID>', desc: 'Call control id of a live call (required)' },
451
+ { flag: '--url <audio_url>', desc: 'Public audio URL to play (required; --audio-url alias accepted)' },
452
+ { flag: '(price)', desc: '$0.08 per playback' },
453
+ { flag: '(example)', desc: 'palmyr phone play --call CC_abc --url https://example.com/hold.mp3' },
454
+ ],
455
+ dtmf: [
456
+ { flag: '--call <CALL_ID>', desc: 'Call control id of a live call (required)' },
457
+ { flag: '--digits <seq>', desc: 'DTMF digit sequence (required; positional also accepted)', hint: 'e.g. "1234#"' },
458
+ { flag: '(price)', desc: '$0.02 per DTMF send' },
459
+ { flag: '(example)', desc: 'palmyr phone dtmf --call CC_abc --digits "1234#"' },
460
+ ],
461
+ gather: [
462
+ { flag: '--call <CALL_ID>', desc: 'Call control id of a live call (required)' },
463
+ { flag: '--min-digits <N>', desc: 'Minimum digits to collect (optional)' },
464
+ { flag: '--max-digits <N>', desc: 'Maximum digits to collect (optional)' },
465
+ { flag: '--timeout <ms>', desc: 'Per-input timeout in milliseconds (optional)' },
466
+ { flag: '--terminating-digit <d>', desc: 'Digit that ends collection (optional, e.g. "#")' },
467
+ { flag: '--prompt <text>', desc: 'Optional TTS prompt to play before gathering' },
468
+ { flag: '--prompt-voice <name>', desc: 'TTS voice for the prompt (optional)' },
469
+ { flag: '(price)', desc: '$0.08 per gather action' },
470
+ { flag: '(example)', desc: 'palmyr phone gather --call CC_abc --max-digits 4 --terminating-digit "#" --prompt "Enter PIN"' },
471
+ ],
472
+ record: [
473
+ { flag: '--call <CALL_ID>', desc: 'Call control id of a live call (required)' },
474
+ { flag: '--format <fmt>', desc: 'Recording format (optional, provider default otherwise)' },
475
+ { flag: '(price)', desc: '$0.10 per record start' },
476
+ { flag: '(example)', desc: 'palmyr phone record --call CC_abc --format mp3' },
477
+ ],
478
+ 'record-stop': [
479
+ { flag: '--call <CALL_ID>', desc: 'Call control id of a live call (required)' },
480
+ { flag: '(price)', desc: '$0.02 per stop' },
481
+ { flag: '(example)', desc: 'palmyr phone record-stop --call CC_abc' },
482
+ ],
483
+ hangup: [
484
+ { flag: '--call <CALL_ID>', desc: 'Call control id of a live call (required)' },
485
+ { flag: '(price)', desc: '$0.02 per hangup' },
486
+ { flag: '(example)', desc: 'palmyr phone hangup --call CC_abc' },
487
+ ],
488
+ answer: [
489
+ { flag: '--call <CALL_ID>', desc: 'Call control id of an inbound call (required)' },
490
+ { flag: '(price)', desc: '$0.02 per answer' },
491
+ { flag: '(example)', desc: 'palmyr phone answer --call CC_abc' },
492
+ ],
493
+ transfer: [
494
+ { flag: '--call <CALL_ID>', desc: 'Call control id of a live call (required)' },
495
+ { flag: '--to <+E.164>', desc: 'Destination phone number to bridge into (required)' },
496
+ { flag: '(price)', desc: '$0.10 per transfer' },
497
+ { flag: '(example)', desc: 'palmyr phone transfer --call CC_abc --to +15557654321' },
498
+ ],
499
+ };
500
+ const EMAIL_HELP = {
501
+ create: [
502
+ { flag: '--name <name>', desc: 'Inbox name (required)' },
503
+ { flag: '--domain <domain>', desc: 'Wallet-owned domain to host the inbox on (optional)', hint: 'default: Palmyr-hosted domain' },
504
+ { flag: '--wallet <id|name>', desc: 'Wallet to own the inbox (optional)' },
505
+ { flag: '(price)', desc: '$2.00 per inbox provisioned' },
506
+ { flag: '(example)', desc: 'palmyr email create --name agent --domain example.com' },
507
+ ],
508
+ list: [
509
+ { flag: '(no args)', desc: 'List inboxes owned by your wallet' },
510
+ { flag: '(price)', desc: '$0.01 per call' },
511
+ { flag: '(example)', desc: 'palmyr email list --json' },
512
+ ],
513
+ status: [
514
+ { flag: '<domain>', desc: 'Domain to check (positional or --domain)', hint: 'e.g. example.com' },
515
+ { flag: '(price)', desc: '$0.01 per call' },
516
+ { flag: '(example)', desc: 'palmyr email status example.com' },
517
+ ],
518
+ register: [
519
+ { flag: '<domain>', desc: 'Wallet-owned domain to (re-)register with Mailgun (positional or --domain)' },
520
+ { flag: '(price)', desc: '$0.05 per registration' },
521
+ { flag: '(example)', desc: 'palmyr email register example.com' },
522
+ ],
523
+ read: [
524
+ { flag: '--id <INBOX_ID>', desc: 'Inbox id (required; positional also accepted)' },
525
+ { flag: '(price)', desc: '$0.02 per call' },
526
+ { flag: '(example)', desc: 'palmyr email read --id INB_abc123' },
527
+ ],
528
+ send: [
529
+ { flag: '--id <INBOX_ID>', desc: 'Source inbox id (required)' },
530
+ { flag: '--to <addr>', desc: 'Destination email (required)' },
531
+ { flag: '--subject <text>', desc: 'Subject line (required)' },
532
+ { flag: '--body <text>', desc: 'Message body (required)' },
533
+ { flag: '(price)', desc: '$0.08 per email sent' },
534
+ { flag: '(example)', desc: 'palmyr email send --id INB_abc --to user@x.com --subject Hi --body "..."' },
535
+ ],
536
+ threads: [
537
+ { flag: '--id <INBOX_ID>', desc: 'Inbox id (required; positional also accepted)' },
538
+ { flag: '(price)', desc: '$0.02 per call' },
539
+ { flag: '(example)', desc: 'palmyr email threads --id INB_abc123' },
540
+ ],
541
+ };
542
+ const COMPUTE_HELP = {
543
+ plans: [
544
+ { flag: '--location <loc>', desc: 'Filter to types deployable in this datacenter (optional)', hint: 'e.g. fsn1, nbg1, hel1, ash, hil' },
545
+ { flag: '(price)', desc: 'Free — live discovery from Hetzner' },
546
+ { flag: '(example)', desc: 'palmyr compute plans --location fsn1 --json' },
547
+ ],
548
+ locations: [
549
+ { flag: '(no args)', desc: 'List Hetzner datacenters + per-location server-type availability' },
550
+ { flag: '(price)', desc: 'Free' },
551
+ ],
552
+ 'install-recipes': [
553
+ { flag: '(no args)', desc: 'List available agent install recipes (hermes, openclaw, …)' },
554
+ { flag: '(price)', desc: 'Free' },
555
+ ],
556
+ 'ssh-key': [
557
+ { flag: 'add <pubkey-file>', desc: 'Upload a key to Hetzner', hint: '[--name "label"]' },
558
+ { flag: 'list', desc: 'List uploaded Hetzner SSH keys' },
559
+ { flag: 'delete <id>', desc: 'Remove a Hetzner SSH key' },
560
+ { flag: '(price)', desc: 'add $0.10 · list $0.01 · delete $0.01' },
561
+ ],
562
+ deploy: [
563
+ { flag: '--type <name>', desc: 'Hetzner server type', hint: 'default cx23' },
564
+ { flag: '--name <name>', desc: 'Server name', hint: 'default agent-<timestamp>' },
565
+ { flag: '--location <loc>', desc: 'Hetzner datacenter (optional)', hint: 'e.g. fsn1, nbg1' },
566
+ { flag: '--install <recipes>', desc: 'Comma-separated install recipes', hint: 'e.g. hermes,openclaw' },
567
+ { flag: '--no-install', desc: 'Skip cloud-init entirely (vanilla Ubuntu)' },
568
+ { flag: '--generate-ssh-key', desc: 'GOLDEN PATH (default): generate a fresh keypair locally' },
569
+ { flag: '--pubkey-file <path>', desc: 'Use an existing public key from disk' },
570
+ { flag: '--pubkey "ssh-..."', desc: 'Use an inline public key string' },
571
+ { flag: '--ssh-key <id>', desc: 'Numeric Hetzner key id (already uploaded)' },
572
+ { flag: '--no-wait', desc: 'Return immediately instead of waiting for SSH-ready' },
573
+ { flag: '--wait-timeout <s>', desc: 'Override readiness timeout (30–900s)' },
574
+ { flag: '(price)', desc: '$6.00 per deploy (Hetzner billing flows through)' },
575
+ { flag: '(example)', desc: 'palmyr compute deploy --type cx23 --install hermes' },
576
+ ],
577
+ wait: [
578
+ { flag: '<name|id>', desc: 'Server (positional, name or numeric id)' },
579
+ { flag: '--install <recipes>', desc: 'Also gate on the install marker file (e.g. hermes)' },
580
+ { flag: '--key <path>', desc: 'Path to the private key for SSH verification' },
581
+ { flag: '--wait-timeout <s>', desc: 'Override readiness timeout (30–900s)' },
582
+ { flag: '(price)', desc: '$0.01 per readiness poll (server status check)' },
583
+ { flag: '(example)', desc: 'palmyr compute wait my-vps --install hermes' },
584
+ ],
585
+ ssh: [
586
+ { flag: '<name|id>', desc: 'Server (positional)' },
587
+ { flag: '(price)', desc: 'Free — local cache lookup only' },
588
+ { flag: '(example)', desc: 'palmyr compute ssh my-vps' },
589
+ ],
590
+ exec: [
591
+ { flag: '<name|id> -- <cmd> [args...]', desc: 'Run a one-shot command via Palmyr SSH bridge' },
592
+ { flag: '--timeout <s>', desc: 'Command timeout (1–120s)' },
593
+ { flag: '(price)', desc: '$0.05 per command' },
594
+ { flag: '(example)', desc: 'palmyr compute exec my-vps -- systemctl status openclaw' },
595
+ ],
596
+ rename: [
597
+ { flag: '<name|id> <new-name>', desc: 'Rename server (metadata-only, no reboot)' },
598
+ { flag: '(price)', desc: '$0.01 per rename' },
599
+ ],
600
+ 'reset-password': [
601
+ { flag: '<name|id>', desc: 'Rotate the root password (Hetzner-side)' },
602
+ { flag: '(price)', desc: '$0.10 per action' },
603
+ ],
604
+ console: [
605
+ { flag: '<name|id>', desc: 'Get a noVNC console URL (break-glass)' },
606
+ { flag: '(price)', desc: '$0.10 per action' },
607
+ ],
608
+ reboot: [
609
+ { flag: '<name|id>', desc: 'Reboot the server' },
610
+ { flag: '(price)', desc: '$0.10 per action' },
611
+ ],
612
+ 'setup-ssh': [
613
+ { flag: '--id <SERVER_ID>', desc: 'Server id (required; positional also accepted)' },
614
+ { flag: '--pubkey-file <path>', desc: 'Public key file to inject' },
615
+ { flag: '--pubkey "ssh-..."', desc: 'Inline public key string' },
616
+ { flag: '(price)', desc: '$0.01 per call' },
617
+ { flag: '(example)', desc: 'palmyr compute setup-ssh --id 12345 --pubkey-file ~/.ssh/id_ed25519.pub' },
618
+ ],
619
+ list: [
620
+ { flag: '(no args)', desc: 'List your deployed servers' },
621
+ { flag: '(price)', desc: '$0.01 per call' },
622
+ ],
623
+ delete: [
624
+ { flag: '--id <SERVER_ID>', desc: 'Server id (required; positional also accepted)' },
625
+ { flag: '(price)', desc: '$0.10 per deletion (Hetzner billing stops on confirm)' },
626
+ ],
627
+ };
628
+ const DOMAIN_HELP = {
629
+ check: [
630
+ { flag: '--name <domain>', desc: 'Domain or root name (positional also accepted)' },
631
+ { flag: '(price)', desc: 'Free — availability lookup only' },
632
+ { flag: '(example)', desc: 'palmyr domain check example.dev' },
633
+ ],
634
+ pricing: [
635
+ { flag: '--name <root>', desc: 'Root name to price across TLDs (positional also accepted)' },
636
+ { flag: '(price)', desc: 'Free' },
637
+ { flag: '(example)', desc: 'palmyr domain pricing example' },
638
+ ],
639
+ buy: [
640
+ { flag: '--name <domain>', desc: 'Fully-qualified domain (required, e.g. example.dev)' },
641
+ { flag: '(price)', desc: 'Dynamic — registrar cost × 1.25 markup (charged via x402)' },
642
+ { flag: '(example)', desc: 'palmyr domain buy --name example.dev' },
643
+ ],
644
+ list: [
645
+ { flag: '(no args)', desc: 'List domains owned or shared with your wallet' },
646
+ { flag: '(price)', desc: '$0.0001 ownership-proof micro-payment' },
647
+ ],
648
+ dns: [
649
+ { flag: '--name <domain>', desc: 'Domain to read DNS for (positional also accepted)' },
650
+ { flag: '(price)', desc: '$0.0001 ownership-proof micro-payment' },
651
+ ],
652
+ 'transfer-ownership': [
653
+ { flag: '--name <domain>', desc: 'Domain to transfer (required)' },
654
+ { flag: '--to <wallet>', desc: 'Recipient wallet address (required)' },
655
+ { flag: '(price)', desc: '$0.0001 ownership-proof micro-payment' },
656
+ ],
657
+ share: [
658
+ { flag: '--name <domain>', desc: 'Domain to share (required)' },
659
+ { flag: '--with <wallet>', desc: 'Wallet to grant shared access (required)' },
660
+ { flag: '(price)', desc: '$0.0001 ownership-proof micro-payment' },
661
+ ],
662
+ unshare: [
663
+ { flag: '--name <domain>', desc: 'Domain to revoke from (required)' },
664
+ { flag: '--from <wallet>', desc: 'Wallet to revoke (required)' },
665
+ { flag: '(price)', desc: '$0.0001 ownership-proof micro-payment' },
666
+ ],
667
+ };
668
+ const CHAT_HELP = {
669
+ run: [
670
+ { flag: '"<intent>"', desc: 'Plain-string intent (positional or --intent)' },
671
+ { flag: '--budget <USDC>', desc: 'Max spend cap (required, positive USDC)' },
672
+ { flag: '--quality <q>', desc: 'Quality tier', hint: 'fast | cheap | best (default best)' },
673
+ { flag: '--execute', desc: 'Auto-execute the plan once generated' },
674
+ { flag: '--auto-approve-under <USDC>', desc: 'Skip approval prompts for steps cheaper than this' },
675
+ { flag: '(price)', desc: '$0.10 orchestration fee + sum of per-step costs (capped by --budget)' },
676
+ { flag: '(example)', desc: 'palmyr chat run "launch a sneaker brand" --budget 50' },
677
+ ],
678
+ resume: [
679
+ { flag: '<session_id>', desc: 'Existing session id (positional)' },
680
+ { flag: '"<follow-up>"', desc: 'Follow-up intent (positional remainder or --intent)' },
681
+ { flag: '--approve', desc: 'Approve a previously-generated plan' },
682
+ { flag: '--plan-id <id>', desc: 'Plan id to approve (pair with --approve)' },
683
+ { flag: '--budget <USDC>', desc: 'Override session budget (default $20)' },
684
+ { flag: '--execute', desc: 'Auto-execute the new plan' },
685
+ { flag: '(price)', desc: '$0.10 orchestration fee per new plan + per-step costs' },
686
+ { flag: '(example)', desc: 'palmyr chat resume sess_abc "now post 3 videos"' },
687
+ ],
688
+ status: [
689
+ { flag: '<session_id>', desc: 'Session id (positional)' },
690
+ { flag: '(price)', desc: 'Free — session inspection' },
691
+ ],
692
+ cancel: [
693
+ { flag: '<session_id>', desc: 'Session id (positional)' },
694
+ { flag: '(price)', desc: 'Free — halts execution and refunds remaining escrow' },
695
+ ],
696
+ sessions: [
697
+ { flag: '(no args)', desc: 'List your active i402 sessions' },
698
+ { flag: '(price)', desc: 'Free' },
699
+ ],
700
+ capabilities: [
701
+ { flag: '(no args)', desc: 'List canonical capability classes (e.g. web_search, mint_nft)' },
702
+ { flag: '(price)', desc: 'Free' },
703
+ ],
704
+ providers: [
705
+ { flag: '--capability <name>', desc: 'Filter providers by capability (optional)' },
706
+ { flag: '(price)', desc: 'Free' },
707
+ { flag: '(example)', desc: 'palmyr chat providers --capability web_search' },
708
+ ],
379
709
  };
380
710
  /**
381
711
  * Render a per-command menu (no subcommand given). On a TTY → Ink MenuScreen
@@ -673,7 +1003,7 @@ async function main() {
673
1003
  break;
674
1004
  }
675
1005
  case 'phone': {
676
- if (!subcommand || flags.help) {
1006
+ if (!subcommand || (flags.help && !PHONE_HELP[subcommand])) {
677
1007
  showMenu({
678
1008
  command: 'phone',
679
1009
  title: 'phone',
@@ -682,17 +1012,42 @@ async function main() {
682
1012
  commands: [
683
1013
  { name: 'search', description: 'Search available numbers', hint: '--country US' },
684
1014
  { name: 'buy', description: 'Buy a phone number', hint: '--country US' },
1015
+ { name: 'list', description: 'List numbers owned by your wallet' },
1016
+ { name: 'release', description: 'Release a phone number', hint: '--id PHONE_ID' },
685
1017
  { name: 'sms', description: 'Send an SMS', hint: '--id ID --to +1... --body "hi"' },
1018
+ { name: 'messages', description: 'Read SMS messages received on a number', hint: '--id PHONE_ID' },
686
1019
  { name: 'call', description: 'Place a voice call', hint: '--id ID --to +1... --tts "hello"' },
1020
+ { name: 'calls', description: 'List calls placed/received on a number', hint: '--id PHONE_ID' },
1021
+ { name: 'call-info', description: 'Get details on a single call', hint: '--call CALL_CONTROL_ID' },
1022
+ { name: 'speak', description: 'TTS into a live call', hint: '--call ID --text "..." [--voice V]' },
1023
+ { name: 'play', description: 'Play an audio URL into a live call', hint: '--call ID --url https://...' },
1024
+ { name: 'dtmf', description: 'Send DTMF tones to a live call', hint: '--call ID --digits "1234#"' },
1025
+ { name: 'gather', description: 'Collect DTMF input from caller', hint: '--call ID [--min-digits N --max-digits N --timeout MS --prompt "..."]' },
1026
+ { name: 'record', description: 'Start recording a live call', hint: '--call ID [--format mp3]' },
1027
+ { name: 'record-stop', description: 'Stop recording a live call', hint: '--call ID' },
1028
+ { name: 'hangup', description: 'End a live call', hint: '--call ID' },
1029
+ { name: 'answer', description: 'Answer an inbound call', hint: '--call ID' },
1030
+ { name: 'transfer', description: 'Transfer a live call to another number', hint: '--call ID --to +1...' },
687
1031
  ],
688
1032
  fromHome,
689
1033
  });
690
1034
  break;
691
1035
  }
1036
+ if (flags.help && subcommand && PHONE_HELP[subcommand]) {
1037
+ subcommandHelp('phone', subcommand, PHONE_HELP[subcommand]);
1038
+ break;
1039
+ }
692
1040
  switch (subcommand) {
693
1041
  case 'search': {
694
1042
  const country = flags.country || 'US';
695
1043
  const data = await ao.phoneSearch(country, flags.limit ? parseInt(flags.limit) : undefined);
1044
+ // Empty result is a valid response but `{numbers: []}` alone made it
1045
+ // ambiguous whether the API failed or the country has no inventory.
1046
+ // Add a non-breaking `note` field — agents that already key off
1047
+ // `.numbers.length` keep working; new readers get a clear signal.
1048
+ if (data && Array.isArray(data.numbers) && data.numbers.length === 0 && !data.note) {
1049
+ data.note = `No numbers available for ${country}. Try a different country code (US, GB, CA, DE, etc.).`;
1050
+ }
696
1051
  return print(data);
697
1052
  render(React.createElement(RecordsScreen, {
698
1053
  version: VERSION,
@@ -757,12 +1112,130 @@ async function main() {
757
1112
  render(React.createElement(SuccessScreen, { version: VERSION, title: 'calling', subtitle: to, details: [{ label: 'To', value: to }, { label: 'Call ID', value: data.callControlId || data.id || '' }], footerLeft: 'Call initiated' }));
758
1113
  break;
759
1114
  }
760
- default: err(`Unknown phone command: ${subcommand}. Try: search, buy, sms, call`);
1115
+ case 'list': {
1116
+ const data = await ao.phoneListNumbers();
1117
+ return print(data);
1118
+ }
1119
+ case 'messages': {
1120
+ const id = flags.id || positional[0];
1121
+ if (!id)
1122
+ err('--id PHONE_ID required');
1123
+ const data = await ao.phoneMessages(id);
1124
+ return print(data);
1125
+ }
1126
+ case 'calls': {
1127
+ const id = flags.id || positional[0];
1128
+ if (!id)
1129
+ err('--id PHONE_ID required');
1130
+ const data = await ao.phoneCalls(id);
1131
+ return print(data);
1132
+ }
1133
+ case 'release': {
1134
+ const id = flags.id || positional[0];
1135
+ if (!id)
1136
+ err('--id PHONE_ID required');
1137
+ const data = await ao.phoneRelease(id);
1138
+ return print(data);
1139
+ }
1140
+ case 'call-info': {
1141
+ const callId = flags.call || flags.id || positional[0];
1142
+ if (!callId)
1143
+ err('--call CALL_CONTROL_ID required');
1144
+ const data = await ao.phoneCallInfo(callId);
1145
+ return print(data);
1146
+ }
1147
+ case 'speak': {
1148
+ const callId = flags.call || flags.id;
1149
+ const text = flags.text || flags.tts;
1150
+ if (!callId || !text)
1151
+ err('--call <id>, --text <text> required');
1152
+ const voice = flags.voice;
1153
+ const language = flags.language;
1154
+ const data = await ao.phoneSpeak(callId, text, { ...(voice ? { voice } : {}), ...(language ? { language } : {}) });
1155
+ return print(data);
1156
+ }
1157
+ case 'play': {
1158
+ const callId = flags.call || flags.id;
1159
+ const audioUrl = flags.url || flags['audio-url'];
1160
+ if (!callId || !audioUrl)
1161
+ err('--call <id>, --url <https://...> required');
1162
+ const data = await ao.phonePlay(callId, audioUrl);
1163
+ return print(data);
1164
+ }
1165
+ case 'dtmf': {
1166
+ const callId = flags.call || flags.id;
1167
+ const digits = flags.digits || positional[0];
1168
+ if (!callId || !digits)
1169
+ err('--call <id>, --digits "1234#" required');
1170
+ const data = await ao.phoneDtmf(callId, digits);
1171
+ return print(data);
1172
+ }
1173
+ case 'gather': {
1174
+ const callId = flags.call || flags.id;
1175
+ if (!callId)
1176
+ err('--call <id> required');
1177
+ const parseInt0 = (v) => (typeof v === 'string' ? parseInt(v, 10) : undefined);
1178
+ const opts = {};
1179
+ const minDigits = parseInt0(flags['min-digits']);
1180
+ if (minDigits !== undefined && Number.isFinite(minDigits))
1181
+ opts.minDigits = minDigits;
1182
+ const maxDigits = parseInt0(flags['max-digits']);
1183
+ if (maxDigits !== undefined && Number.isFinite(maxDigits))
1184
+ opts.maxDigits = maxDigits;
1185
+ const timeoutMillis = parseInt0(flags.timeout);
1186
+ if (timeoutMillis !== undefined && Number.isFinite(timeoutMillis))
1187
+ opts.timeoutMillis = timeoutMillis;
1188
+ if (flags['terminating-digit'])
1189
+ opts.terminatingDigit = flags['terminating-digit'];
1190
+ if (flags.prompt)
1191
+ opts.prompt = flags.prompt;
1192
+ if (flags['prompt-voice'])
1193
+ opts.promptVoice = flags['prompt-voice'];
1194
+ const data = await ao.phoneGather(callId, opts);
1195
+ return print(data);
1196
+ }
1197
+ case 'record': {
1198
+ const callId = flags.call || flags.id;
1199
+ if (!callId)
1200
+ err('--call <id> required');
1201
+ const data = await ao.phoneRecord(callId, flags.format);
1202
+ return print(data);
1203
+ }
1204
+ case 'record-stop': {
1205
+ const callId = flags.call || flags.id;
1206
+ if (!callId)
1207
+ err('--call <id> required');
1208
+ const data = await ao.phoneRecordStop(callId);
1209
+ return print(data);
1210
+ }
1211
+ case 'hangup': {
1212
+ const callId = flags.call || flags.id;
1213
+ if (!callId)
1214
+ err('--call <id> required');
1215
+ const data = await ao.phoneHangup(callId);
1216
+ return print(data);
1217
+ }
1218
+ case 'answer': {
1219
+ const callId = flags.call || flags.id;
1220
+ if (!callId)
1221
+ err('--call <id> required');
1222
+ const data = await ao.phoneAnswer(callId);
1223
+ return print(data);
1224
+ }
1225
+ case 'transfer': {
1226
+ const callId = flags.call || flags.id;
1227
+ const to = flags.to;
1228
+ if (!callId || !to)
1229
+ err('--call <id>, --to <+E.164> required');
1230
+ const data = await ao.phoneTransfer(callId, to);
1231
+ return print(data);
1232
+ }
1233
+ default: err(`Unknown phone command: ${subcommand}. Try: search, buy, list, release, sms, messages, call, calls, call-info, speak, play, dtmf, gather, record, record-stop, hangup, answer, transfer`);
761
1234
  }
762
1235
  break;
763
1236
  }
764
1237
  case 'email': {
765
- if (!subcommand || flags.help) {
1238
+ if (!subcommand || (flags.help && !EMAIL_HELP[subcommand])) {
766
1239
  showMenu({
767
1240
  command: 'email',
768
1241
  title: 'email',
@@ -781,6 +1254,10 @@ async function main() {
781
1254
  });
782
1255
  break;
783
1256
  }
1257
+ if (flags.help && subcommand && EMAIL_HELP[subcommand]) {
1258
+ subcommandHelp('email', subcommand, EMAIL_HELP[subcommand]);
1259
+ break;
1260
+ }
784
1261
  switch (subcommand) {
785
1262
  case 'create': {
786
1263
  const name = flags.name || positional[0];
@@ -872,7 +1349,7 @@ async function main() {
872
1349
  break;
873
1350
  }
874
1351
  case 'compute': {
875
- if (!subcommand || flags.help) {
1352
+ if (!subcommand || (flags.help && !COMPUTE_HELP[subcommand])) {
876
1353
  showMenu({
877
1354
  command: 'compute',
878
1355
  title: 'compute',
@@ -899,6 +1376,10 @@ async function main() {
899
1376
  });
900
1377
  break;
901
1378
  }
1379
+ if (flags.help && subcommand && COMPUTE_HELP[subcommand]) {
1380
+ subcommandHelp('compute', subcommand, COMPUTE_HELP[subcommand]);
1381
+ break;
1382
+ }
902
1383
  switch (subcommand) {
903
1384
  case 'plans': {
904
1385
  // --location filters to types deployable in that location. Each
@@ -1474,7 +1955,7 @@ async function main() {
1474
1955
  break;
1475
1956
  }
1476
1957
  case 'domain': {
1477
- if (!subcommand || flags.help) {
1958
+ if (!subcommand || (flags.help && !DOMAIN_HELP[subcommand])) {
1478
1959
  showMenu({
1479
1960
  command: 'domain',
1480
1961
  title: 'domain',
@@ -1494,6 +1975,10 @@ async function main() {
1494
1975
  });
1495
1976
  break;
1496
1977
  }
1978
+ if (flags.help && subcommand && DOMAIN_HELP[subcommand]) {
1979
+ subcommandHelp('domain', subcommand, DOMAIN_HELP[subcommand]);
1980
+ break;
1981
+ }
1497
1982
  switch (subcommand) {
1498
1983
  case 'check': {
1499
1984
  const name = flags.name || positional[0];
@@ -1684,6 +2169,7 @@ async function main() {
1684
2169
  { name: 'watch', description: 'Maintain a watchlist of CAs to monitor', hint: 'add <CA> --trigger "..." | list' },
1685
2170
  { name: 'brief', description: 'Show thesis + PnL brief for a position', hint: '<CA>' },
1686
2171
  { name: 'doctor', description: 'Health check for the wallet-trading subsystem', hint: '[--wallet <ref>]' },
2172
+ { name: 'pay-preflight', description: 'Check the x402 pay flow is ready (chain, wallet, signing, USDC balance)', hint: '[--chain solana|base] [--min-usdc N]' },
1687
2173
  { name: 'smoke-test', description: 'End-to-end validation of wallet trading on Solana + Base', hint: '--wallet <ref> [--chain solana|base|all]' },
1688
2174
  { name: 'readiness', description: 'Go/no-go autonomous-trading readiness — sign, gas, quotes, daemon, open positions', hint: '--wallet <ref>' },
1689
2175
  { name: 'live-test', description: 'Execute tiny real round trips on Solana + Base, verify no leftover positions', hint: '--wallet <ref> --budget Nusdc [--chain ...]' },
@@ -3337,6 +3823,59 @@ async function main() {
3337
3823
  process.exit(EXIT.GENERAL);
3338
3824
  break;
3339
3825
  }
3826
+ case 'pay-preflight': {
3827
+ // Five-question readiness check for the x402 pay flow. Local-only
3828
+ // siblings of this same module run automatically before every paid
3829
+ // command (see paidRequest/paidStreamRequest); this command is the
3830
+ // explicit, full-fat version with the RPC USDC balance check.
3831
+ const ppChain = flags.chain?.toLowerCase();
3832
+ if (ppChain && ppChain !== 'solana' && ppChain !== 'base') {
3833
+ err(`--chain must be solana or base (got ${ppChain})`, EXIT.BAD_INPUT);
3834
+ }
3835
+ const ppWallet = flags.wallet || undefined;
3836
+ const ppMinUsdcRaw = flags['min-usdc'];
3837
+ const ppMinUsdc = typeof ppMinUsdcRaw === 'string' ? Number(ppMinUsdcRaw) : undefined;
3838
+ if (ppMinUsdc !== undefined && (!Number.isFinite(ppMinUsdc) || ppMinUsdc < 0)) {
3839
+ err(`--min-usdc must be a non-negative number (got ${String(ppMinUsdcRaw)})`, EXIT.BAD_INPUT);
3840
+ }
3841
+ const { fullPreflight } = await import('./pay-preflight.js');
3842
+ const report = await fullPreflight({
3843
+ ...(ppChain ? { chain: ppChain } : {}),
3844
+ ...(ppWallet ? { walletRef: ppWallet } : {}),
3845
+ ...(ppMinUsdc !== undefined ? { minUsdc: ppMinUsdc } : {}),
3846
+ ...(passphrase ? { passphrase } : {}),
3847
+ });
3848
+ if (AGENT_MODE) {
3849
+ print(report);
3850
+ if (!report.ok)
3851
+ process.exit(EXIT.GENERAL);
3852
+ break;
3853
+ }
3854
+ // TTY rendering — mirrors the doctor command's layout.
3855
+ console.log();
3856
+ section('Pay preflight');
3857
+ kv('Verdict', report.ok ? `${t.success}ready${t.reset}` : `${t.error}not ready${t.reset}`);
3858
+ kv('Pay chain', report.payChain);
3859
+ kv('Wallet ID', report.walletId || `${t.muted}(none)${t.reset}`);
3860
+ kv('Address', report.walletAddress || `${t.muted}(unknown)${t.reset}`);
3861
+ kv('Can sign', report.canSign ? `${t.success}yes${t.reset}` : `${t.error}no${t.reset}`);
3862
+ if (report.usdc) {
3863
+ const bal = report.usdc.balance;
3864
+ kv('USDC balance', bal === null ? `${t.muted}(unknown)${t.reset}` : `${bal.toFixed(6)} USDC`);
3865
+ if (report.usdc.requiredMin > 0)
3866
+ kv('Required min', `${report.usdc.requiredMin.toFixed(6)} USDC`);
3867
+ if (report.usdc.ataStatus)
3868
+ kv('Solana ATA', report.usdc.ataStatus);
3869
+ }
3870
+ if (report.fix) {
3871
+ console.log();
3872
+ console.log(` ${t.warn}Fix:${t.reset} ${report.fix}`);
3873
+ }
3874
+ console.log();
3875
+ if (!report.ok)
3876
+ process.exit(EXIT.GENERAL);
3877
+ break;
3878
+ }
3340
3879
  case 'smoke-test': {
3341
3880
  const smokeWalletRef = flags.wallet || undefined;
3342
3881
  if (!smokeWalletRef)
@@ -3917,7 +4456,7 @@ async function main() {
3917
4456
  break;
3918
4457
  }
3919
4458
  case 'chat': {
3920
- if (!subcommand || flags.help) {
4459
+ if (!subcommand || (flags.help && !CHAT_HELP[subcommand])) {
3921
4460
  showMenu({
3922
4461
  command: 'chat',
3923
4462
  title: 'chat',
@@ -3936,6 +4475,10 @@ async function main() {
3936
4475
  });
3937
4476
  break;
3938
4477
  }
4478
+ if (flags.help && subcommand && CHAT_HELP[subcommand]) {
4479
+ subcommandHelp('chat', subcommand, CHAT_HELP[subcommand]);
4480
+ break;
4481
+ }
3939
4482
  switch (subcommand) {
3940
4483
  case 'run': {
3941
4484
  const intent = (positional.join(' ') || flags.intent || '').trim();