@palmyr/cli 1.5.6 → 1.7.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
@@ -213,13 +213,35 @@ function subcommandHelp(command, subcommand, options) {
213
213
  const WALLET_HELP = {
214
214
  create: [
215
215
  { flag: '--name <name>', desc: 'Wallet name', hint: 'default: "My Wallet"' },
216
- { flag: '--managed', desc: 'Create managed wallet with human oversight via passkey' },
217
- { flag: '--chains <list>', desc: 'Supported chains (comma-separated)', hint: 'default: solana,evm' },
216
+ { flag: '--managed', desc: 'Create managed wallet with human oversight via passkey (single-create only)' },
217
+ { flag: '--solana', desc: 'Materialize the Solana account only', hint: 'default: both chains' },
218
+ { flag: '--base', desc: 'Materialize the Base/EVM account only', hint: 'pair with --solana for both (default)' },
219
+ { flag: '--tag <name>', desc: 'Folder-like grouping tag', hint: 'e.g. palmyr-demo — required with --count' },
220
+ { flag: '--count <N>', desc: 'Bulk-create N wallets in one call (1-500)', hint: 'unmanaged only; requires --tag' },
221
+ { flag: '--name-prefix <p>', desc: 'Bulk name prefix; suffixed `-001..-N`', hint: 'default: same as --tag' },
218
222
  ],
219
223
  import: [
220
224
  { flag: '--mnemonic <words>', desc: 'BIP-39 mnemonic phrase (required)' },
221
225
  { flag: '--name <name>', desc: 'Wallet name', hint: 'default: "Imported Wallet"' },
222
226
  { flag: '--managed', desc: 'Import as managed wallet' },
227
+ { flag: '--solana', desc: 'Materialize the Solana account only' },
228
+ { flag: '--base', desc: 'Materialize the Base/EVM account only' },
229
+ { flag: '--tag <name>', desc: 'Assign a tag at import time' },
230
+ ],
231
+ tags: [
232
+ { flag: '(no args)', desc: 'List all tags with wallet count, chains, and date range' },
233
+ ],
234
+ tag: [
235
+ { flag: '<WALLET_ID>', desc: 'Wallet id or name (positional or --id)' },
236
+ { flag: '<TAG>', desc: 'Tag to assign (positional)' },
237
+ { flag: '--clear', desc: 'Remove the tag from this wallet instead of assigning one' },
238
+ ],
239
+ 'tag-delete': [
240
+ { flag: '<TAG>', desc: 'Tag to wipe (positional)' },
241
+ { flag: '--confirm', desc: 'Required — deletes every wallet sharing this tag (irreversible)' },
242
+ ],
243
+ list: [
244
+ { flag: '--tag <name>', desc: 'Filter to wallets in this tag' },
223
245
  ],
224
246
  'sign-message': [
225
247
  { flag: '<WALLET_ID>', desc: 'Wallet ID (positional or --id)' },
@@ -355,6 +377,327 @@ const WALLET_HELP = {
355
377
  { flag: '--dst-decimals <n>', desc: 'Dest token decimals', hint: 'default 6 (USDC-like)' },
356
378
  ],
357
379
  };
380
+ const PHONE_HELP = {
381
+ search: [
382
+ { flag: '--country <ISO>', desc: 'Country code', hint: 'default US (e.g. US, GB, AE)' },
383
+ { flag: '--limit <N>', desc: 'Max results to return' },
384
+ { flag: '(price)', desc: 'Free — no payment required' },
385
+ { flag: '(example)', desc: 'palmyr phone search --country US --json' },
386
+ ],
387
+ buy: [
388
+ { flag: '--country <ISO>', desc: 'Country code (required)', hint: 'e.g. US, GB' },
389
+ { flag: '--area <code>', desc: 'Preferred area code (optional, US only)' },
390
+ { flag: '(price)', desc: '$3.00 per number provisioned' },
391
+ { flag: '(example)', desc: 'palmyr phone buy --country US' },
392
+ ],
393
+ sms: [
394
+ { flag: '--id <PHONE_ID>', desc: 'Source phone number id (required)' },
395
+ { flag: '--to <+E.164>', desc: 'Destination phone number (required)', hint: 'e.g. +15551234567' },
396
+ { flag: '--body <text>', desc: 'Message body (required)' },
397
+ { flag: '(price)', desc: '$0.05 per SMS sent' },
398
+ { flag: '(example)', desc: 'palmyr phone sms --id PN_abc --to +15551234567 --body "hi"' },
399
+ ],
400
+ call: [
401
+ { flag: '--id <PHONE_ID>', desc: 'Source phone number id (required)' },
402
+ { flag: '--to <+E.164>', desc: 'Destination phone number (required)' },
403
+ { flag: '--tts <text>', desc: 'Text-to-speech to play on answer (optional)' },
404
+ { flag: '(price)', desc: '$0.10 per call placed' },
405
+ { flag: '(example)', desc: 'palmyr phone call --id PN_abc --to +15551234567 --tts "hello"' },
406
+ ],
407
+ list: [
408
+ { flag: '(no args)', desc: 'List phone numbers owned by your wallet' },
409
+ { flag: '(price)', desc: '$0.01 per call' },
410
+ { flag: '(example)', desc: 'palmyr phone list --json' },
411
+ ],
412
+ messages: [
413
+ { flag: '--id <PHONE_ID>', desc: 'Phone number id to read SMS for (required; positional also accepted)' },
414
+ { flag: '(price)', desc: '$0.02 per call' },
415
+ { flag: '(example)', desc: 'palmyr phone messages --id PN_abc' },
416
+ ],
417
+ calls: [
418
+ { flag: '--id <PHONE_ID>', desc: 'Phone number id to list calls for (required; positional also accepted)' },
419
+ { flag: '(price)', desc: '$0.02 per call' },
420
+ { flag: '(example)', desc: 'palmyr phone calls --id PN_abc' },
421
+ ],
422
+ release: [
423
+ { flag: '--id <PHONE_ID>', desc: 'Phone number id to release (required; positional also accepted)' },
424
+ { flag: '(price)', desc: '$0.01 per release (stops monthly Telnyx billing)' },
425
+ { flag: '(example)', desc: 'palmyr phone release --id PN_abc' },
426
+ ],
427
+ 'call-info': [
428
+ { flag: '--call <CALL_ID>', desc: 'Call control id (required; --id and positional also accepted)' },
429
+ { flag: '(price)', desc: '$0.02 per lookup' },
430
+ { flag: '(example)', desc: 'palmyr phone call-info --call CC_abc' },
431
+ ],
432
+ speak: [
433
+ { flag: '--call <CALL_ID>', desc: 'Call control id of a live call (required)' },
434
+ { flag: '--text <text>', desc: 'TTS text to speak (required; --tts alias accepted)' },
435
+ { flag: '--voice <name>', desc: 'TTS voice (optional, provider default otherwise)' },
436
+ { flag: '--language <code>', desc: 'TTS language code (optional, e.g. en-US)' },
437
+ { flag: '(price)', desc: '$0.08 per speak action' },
438
+ { flag: '(example)', desc: 'palmyr phone speak --call CC_abc --text "please hold"' },
439
+ ],
440
+ play: [
441
+ { flag: '--call <CALL_ID>', desc: 'Call control id of a live call (required)' },
442
+ { flag: '--url <audio_url>', desc: 'Public audio URL to play (required; --audio-url alias accepted)' },
443
+ { flag: '(price)', desc: '$0.08 per playback' },
444
+ { flag: '(example)', desc: 'palmyr phone play --call CC_abc --url https://example.com/hold.mp3' },
445
+ ],
446
+ dtmf: [
447
+ { flag: '--call <CALL_ID>', desc: 'Call control id of a live call (required)' },
448
+ { flag: '--digits <seq>', desc: 'DTMF digit sequence (required; positional also accepted)', hint: 'e.g. "1234#"' },
449
+ { flag: '(price)', desc: '$0.02 per DTMF send' },
450
+ { flag: '(example)', desc: 'palmyr phone dtmf --call CC_abc --digits "1234#"' },
451
+ ],
452
+ gather: [
453
+ { flag: '--call <CALL_ID>', desc: 'Call control id of a live call (required)' },
454
+ { flag: '--min-digits <N>', desc: 'Minimum digits to collect (optional)' },
455
+ { flag: '--max-digits <N>', desc: 'Maximum digits to collect (optional)' },
456
+ { flag: '--timeout <ms>', desc: 'Per-input timeout in milliseconds (optional)' },
457
+ { flag: '--terminating-digit <d>', desc: 'Digit that ends collection (optional, e.g. "#")' },
458
+ { flag: '--prompt <text>', desc: 'Optional TTS prompt to play before gathering' },
459
+ { flag: '--prompt-voice <name>', desc: 'TTS voice for the prompt (optional)' },
460
+ { flag: '(price)', desc: '$0.08 per gather action' },
461
+ { flag: '(example)', desc: 'palmyr phone gather --call CC_abc --max-digits 4 --terminating-digit "#" --prompt "Enter PIN"' },
462
+ ],
463
+ record: [
464
+ { flag: '--call <CALL_ID>', desc: 'Call control id of a live call (required)' },
465
+ { flag: '--format <fmt>', desc: 'Recording format (optional, provider default otherwise)' },
466
+ { flag: '(price)', desc: '$0.10 per record start' },
467
+ { flag: '(example)', desc: 'palmyr phone record --call CC_abc --format mp3' },
468
+ ],
469
+ 'record-stop': [
470
+ { flag: '--call <CALL_ID>', desc: 'Call control id of a live call (required)' },
471
+ { flag: '(price)', desc: '$0.02 per stop' },
472
+ { flag: '(example)', desc: 'palmyr phone record-stop --call CC_abc' },
473
+ ],
474
+ hangup: [
475
+ { flag: '--call <CALL_ID>', desc: 'Call control id of a live call (required)' },
476
+ { flag: '(price)', desc: '$0.02 per hangup' },
477
+ { flag: '(example)', desc: 'palmyr phone hangup --call CC_abc' },
478
+ ],
479
+ answer: [
480
+ { flag: '--call <CALL_ID>', desc: 'Call control id of an inbound call (required)' },
481
+ { flag: '(price)', desc: '$0.02 per answer' },
482
+ { flag: '(example)', desc: 'palmyr phone answer --call CC_abc' },
483
+ ],
484
+ transfer: [
485
+ { flag: '--call <CALL_ID>', desc: 'Call control id of a live call (required)' },
486
+ { flag: '--to <+E.164>', desc: 'Destination phone number to bridge into (required)' },
487
+ { flag: '(price)', desc: '$0.10 per transfer' },
488
+ { flag: '(example)', desc: 'palmyr phone transfer --call CC_abc --to +15557654321' },
489
+ ],
490
+ };
491
+ const EMAIL_HELP = {
492
+ create: [
493
+ { flag: '--name <name>', desc: 'Inbox name (required)' },
494
+ { flag: '--domain <domain>', desc: 'Wallet-owned domain to host the inbox on (optional)', hint: 'default: Palmyr-hosted domain' },
495
+ { flag: '--wallet <id|name>', desc: 'Wallet to own the inbox (optional)' },
496
+ { flag: '(price)', desc: '$2.00 per inbox provisioned' },
497
+ { flag: '(example)', desc: 'palmyr email create --name agent --domain example.com' },
498
+ ],
499
+ list: [
500
+ { flag: '(no args)', desc: 'List inboxes owned by your wallet' },
501
+ { flag: '(price)', desc: '$0.01 per call' },
502
+ { flag: '(example)', desc: 'palmyr email list --json' },
503
+ ],
504
+ status: [
505
+ { flag: '<domain>', desc: 'Domain to check (positional or --domain)', hint: 'e.g. example.com' },
506
+ { flag: '(price)', desc: '$0.01 per call' },
507
+ { flag: '(example)', desc: 'palmyr email status example.com' },
508
+ ],
509
+ register: [
510
+ { flag: '<domain>', desc: 'Wallet-owned domain to (re-)register with Mailgun (positional or --domain)' },
511
+ { flag: '(price)', desc: '$0.05 per registration' },
512
+ { flag: '(example)', desc: 'palmyr email register example.com' },
513
+ ],
514
+ read: [
515
+ { flag: '--id <INBOX_ID>', desc: 'Inbox id (required; positional also accepted)' },
516
+ { flag: '(price)', desc: '$0.02 per call' },
517
+ { flag: '(example)', desc: 'palmyr email read --id INB_abc123' },
518
+ ],
519
+ send: [
520
+ { flag: '--id <INBOX_ID>', desc: 'Source inbox id (required)' },
521
+ { flag: '--to <addr>', desc: 'Destination email (required)' },
522
+ { flag: '--subject <text>', desc: 'Subject line (required)' },
523
+ { flag: '--body <text>', desc: 'Message body (required)' },
524
+ { flag: '(price)', desc: '$0.08 per email sent' },
525
+ { flag: '(example)', desc: 'palmyr email send --id INB_abc --to user@x.com --subject Hi --body "..."' },
526
+ ],
527
+ threads: [
528
+ { flag: '--id <INBOX_ID>', desc: 'Inbox id (required; positional also accepted)' },
529
+ { flag: '(price)', desc: '$0.02 per call' },
530
+ { flag: '(example)', desc: 'palmyr email threads --id INB_abc123' },
531
+ ],
532
+ };
533
+ const COMPUTE_HELP = {
534
+ plans: [
535
+ { flag: '--location <loc>', desc: 'Filter to types deployable in this datacenter (optional)', hint: 'e.g. fsn1, nbg1, hel1, ash, hil' },
536
+ { flag: '(price)', desc: 'Free — live discovery from Hetzner' },
537
+ { flag: '(example)', desc: 'palmyr compute plans --location fsn1 --json' },
538
+ ],
539
+ locations: [
540
+ { flag: '(no args)', desc: 'List Hetzner datacenters + per-location server-type availability' },
541
+ { flag: '(price)', desc: 'Free' },
542
+ ],
543
+ 'install-recipes': [
544
+ { flag: '(no args)', desc: 'List available agent install recipes (hermes, openclaw, …)' },
545
+ { flag: '(price)', desc: 'Free' },
546
+ ],
547
+ 'ssh-key': [
548
+ { flag: 'add <pubkey-file>', desc: 'Upload a key to Hetzner', hint: '[--name "label"]' },
549
+ { flag: 'list', desc: 'List uploaded Hetzner SSH keys' },
550
+ { flag: 'delete <id>', desc: 'Remove a Hetzner SSH key' },
551
+ { flag: '(price)', desc: 'add $0.10 · list $0.01 · delete $0.01' },
552
+ ],
553
+ deploy: [
554
+ { flag: '--type <name>', desc: 'Hetzner server type', hint: 'default cx23' },
555
+ { flag: '--name <name>', desc: 'Server name', hint: 'default agent-<timestamp>' },
556
+ { flag: '--location <loc>', desc: 'Hetzner datacenter (optional)', hint: 'e.g. fsn1, nbg1' },
557
+ { flag: '--install <recipes>', desc: 'Comma-separated install recipes', hint: 'e.g. hermes,openclaw' },
558
+ { flag: '--no-install', desc: 'Skip cloud-init entirely (vanilla Ubuntu)' },
559
+ { flag: '--generate-ssh-key', desc: 'GOLDEN PATH (default): generate a fresh keypair locally' },
560
+ { flag: '--pubkey-file <path>', desc: 'Use an existing public key from disk' },
561
+ { flag: '--pubkey "ssh-..."', desc: 'Use an inline public key string' },
562
+ { flag: '--ssh-key <id>', desc: 'Numeric Hetzner key id (already uploaded)' },
563
+ { flag: '--no-wait', desc: 'Return immediately instead of waiting for SSH-ready' },
564
+ { flag: '--wait-timeout <s>', desc: 'Override readiness timeout (30–900s)' },
565
+ { flag: '(price)', desc: '$6.00 per deploy (Hetzner billing flows through)' },
566
+ { flag: '(example)', desc: 'palmyr compute deploy --type cx23 --install hermes' },
567
+ ],
568
+ wait: [
569
+ { flag: '<name|id>', desc: 'Server (positional, name or numeric id)' },
570
+ { flag: '--install <recipes>', desc: 'Also gate on the install marker file (e.g. hermes)' },
571
+ { flag: '--key <path>', desc: 'Path to the private key for SSH verification' },
572
+ { flag: '--wait-timeout <s>', desc: 'Override readiness timeout (30–900s)' },
573
+ { flag: '(price)', desc: '$0.01 per readiness poll (server status check)' },
574
+ { flag: '(example)', desc: 'palmyr compute wait my-vps --install hermes' },
575
+ ],
576
+ ssh: [
577
+ { flag: '<name|id>', desc: 'Server (positional)' },
578
+ { flag: '(price)', desc: 'Free — local cache lookup only' },
579
+ { flag: '(example)', desc: 'palmyr compute ssh my-vps' },
580
+ ],
581
+ exec: [
582
+ { flag: '<name|id> -- <cmd> [args...]', desc: 'Run a one-shot command via Palmyr SSH bridge' },
583
+ { flag: '--timeout <s>', desc: 'Command timeout (1–120s)' },
584
+ { flag: '(price)', desc: '$0.05 per command' },
585
+ { flag: '(example)', desc: 'palmyr compute exec my-vps -- systemctl status openclaw' },
586
+ ],
587
+ rename: [
588
+ { flag: '<name|id> <new-name>', desc: 'Rename server (metadata-only, no reboot)' },
589
+ { flag: '(price)', desc: '$0.01 per rename' },
590
+ ],
591
+ 'reset-password': [
592
+ { flag: '<name|id>', desc: 'Rotate the root password (Hetzner-side)' },
593
+ { flag: '(price)', desc: '$0.10 per action' },
594
+ ],
595
+ console: [
596
+ { flag: '<name|id>', desc: 'Get a noVNC console URL (break-glass)' },
597
+ { flag: '(price)', desc: '$0.10 per action' },
598
+ ],
599
+ reboot: [
600
+ { flag: '<name|id>', desc: 'Reboot the server' },
601
+ { flag: '(price)', desc: '$0.10 per action' },
602
+ ],
603
+ 'setup-ssh': [
604
+ { flag: '--id <SERVER_ID>', desc: 'Server id (required; positional also accepted)' },
605
+ { flag: '--pubkey-file <path>', desc: 'Public key file to inject' },
606
+ { flag: '--pubkey "ssh-..."', desc: 'Inline public key string' },
607
+ { flag: '(price)', desc: '$0.01 per call' },
608
+ { flag: '(example)', desc: 'palmyr compute setup-ssh --id 12345 --pubkey-file ~/.ssh/id_ed25519.pub' },
609
+ ],
610
+ list: [
611
+ { flag: '(no args)', desc: 'List your deployed servers' },
612
+ { flag: '(price)', desc: '$0.01 per call' },
613
+ ],
614
+ delete: [
615
+ { flag: '--id <SERVER_ID>', desc: 'Server id (required; positional also accepted)' },
616
+ { flag: '(price)', desc: '$0.10 per deletion (Hetzner billing stops on confirm)' },
617
+ ],
618
+ };
619
+ const DOMAIN_HELP = {
620
+ check: [
621
+ { flag: '--name <domain>', desc: 'Domain or root name (positional also accepted)' },
622
+ { flag: '(price)', desc: 'Free — availability lookup only' },
623
+ { flag: '(example)', desc: 'palmyr domain check example.dev' },
624
+ ],
625
+ pricing: [
626
+ { flag: '--name <root>', desc: 'Root name to price across TLDs (positional also accepted)' },
627
+ { flag: '(price)', desc: 'Free' },
628
+ { flag: '(example)', desc: 'palmyr domain pricing example' },
629
+ ],
630
+ buy: [
631
+ { flag: '--name <domain>', desc: 'Fully-qualified domain (required, e.g. example.dev)' },
632
+ { flag: '(price)', desc: 'Dynamic — registrar cost × 1.25 markup (charged via x402)' },
633
+ { flag: '(example)', desc: 'palmyr domain buy --name example.dev' },
634
+ ],
635
+ list: [
636
+ { flag: '(no args)', desc: 'List domains owned or shared with your wallet' },
637
+ { flag: '(price)', desc: '$0.0001 ownership-proof micro-payment' },
638
+ ],
639
+ dns: [
640
+ { flag: '--name <domain>', desc: 'Domain to read DNS for (positional also accepted)' },
641
+ { flag: '(price)', desc: '$0.0001 ownership-proof micro-payment' },
642
+ ],
643
+ 'transfer-ownership': [
644
+ { flag: '--name <domain>', desc: 'Domain to transfer (required)' },
645
+ { flag: '--to <wallet>', desc: 'Recipient wallet address (required)' },
646
+ { flag: '(price)', desc: '$0.0001 ownership-proof micro-payment' },
647
+ ],
648
+ share: [
649
+ { flag: '--name <domain>', desc: 'Domain to share (required)' },
650
+ { flag: '--with <wallet>', desc: 'Wallet to grant shared access (required)' },
651
+ { flag: '(price)', desc: '$0.0001 ownership-proof micro-payment' },
652
+ ],
653
+ unshare: [
654
+ { flag: '--name <domain>', desc: 'Domain to revoke from (required)' },
655
+ { flag: '--from <wallet>', desc: 'Wallet to revoke (required)' },
656
+ { flag: '(price)', desc: '$0.0001 ownership-proof micro-payment' },
657
+ ],
658
+ };
659
+ const CHAT_HELP = {
660
+ run: [
661
+ { flag: '"<intent>"', desc: 'Plain-string intent (positional or --intent)' },
662
+ { flag: '--budget <USDC>', desc: 'Max spend cap (required, positive USDC)' },
663
+ { flag: '--quality <q>', desc: 'Quality tier', hint: 'fast | cheap | best (default best)' },
664
+ { flag: '--execute', desc: 'Auto-execute the plan once generated' },
665
+ { flag: '--auto-approve-under <USDC>', desc: 'Skip approval prompts for steps cheaper than this' },
666
+ { flag: '(price)', desc: '$0.10 orchestration fee + sum of per-step costs (capped by --budget)' },
667
+ { flag: '(example)', desc: 'palmyr chat run "launch a sneaker brand" --budget 50' },
668
+ ],
669
+ resume: [
670
+ { flag: '<session_id>', desc: 'Existing session id (positional)' },
671
+ { flag: '"<follow-up>"', desc: 'Follow-up intent (positional remainder or --intent)' },
672
+ { flag: '--approve', desc: 'Approve a previously-generated plan' },
673
+ { flag: '--plan-id <id>', desc: 'Plan id to approve (pair with --approve)' },
674
+ { flag: '--budget <USDC>', desc: 'Override session budget (default $20)' },
675
+ { flag: '--execute', desc: 'Auto-execute the new plan' },
676
+ { flag: '(price)', desc: '$0.10 orchestration fee per new plan + per-step costs' },
677
+ { flag: '(example)', desc: 'palmyr chat resume sess_abc "now post 3 videos"' },
678
+ ],
679
+ status: [
680
+ { flag: '<session_id>', desc: 'Session id (positional)' },
681
+ { flag: '(price)', desc: 'Free — session inspection' },
682
+ ],
683
+ cancel: [
684
+ { flag: '<session_id>', desc: 'Session id (positional)' },
685
+ { flag: '(price)', desc: 'Free — halts execution and refunds remaining escrow' },
686
+ ],
687
+ sessions: [
688
+ { flag: '(no args)', desc: 'List your active i402 sessions' },
689
+ { flag: '(price)', desc: 'Free' },
690
+ ],
691
+ capabilities: [
692
+ { flag: '(no args)', desc: 'List canonical capability classes (e.g. web_search, mint_nft)' },
693
+ { flag: '(price)', desc: 'Free' },
694
+ ],
695
+ providers: [
696
+ { flag: '--capability <name>', desc: 'Filter providers by capability (optional)' },
697
+ { flag: '(price)', desc: 'Free' },
698
+ { flag: '(example)', desc: 'palmyr chat providers --capability web_search' },
699
+ ],
700
+ };
358
701
  /**
359
702
  * Render a per-command menu (no subcommand given). On a TTY → Ink MenuScreen
360
703
  * with the Palmyr aesthetic. In agent mode → flat JSON listing the available
@@ -651,7 +994,7 @@ async function main() {
651
994
  break;
652
995
  }
653
996
  case 'phone': {
654
- if (!subcommand || flags.help) {
997
+ if (!subcommand || (flags.help && !PHONE_HELP[subcommand])) {
655
998
  showMenu({
656
999
  command: 'phone',
657
1000
  title: 'phone',
@@ -660,13 +1003,31 @@ async function main() {
660
1003
  commands: [
661
1004
  { name: 'search', description: 'Search available numbers', hint: '--country US' },
662
1005
  { name: 'buy', description: 'Buy a phone number', hint: '--country US' },
1006
+ { name: 'list', description: 'List numbers owned by your wallet' },
1007
+ { name: 'release', description: 'Release a phone number', hint: '--id PHONE_ID' },
663
1008
  { name: 'sms', description: 'Send an SMS', hint: '--id ID --to +1... --body "hi"' },
1009
+ { name: 'messages', description: 'Read SMS messages received on a number', hint: '--id PHONE_ID' },
664
1010
  { name: 'call', description: 'Place a voice call', hint: '--id ID --to +1... --tts "hello"' },
1011
+ { name: 'calls', description: 'List calls placed/received on a number', hint: '--id PHONE_ID' },
1012
+ { name: 'call-info', description: 'Get details on a single call', hint: '--call CALL_CONTROL_ID' },
1013
+ { name: 'speak', description: 'TTS into a live call', hint: '--call ID --text "..." [--voice V]' },
1014
+ { name: 'play', description: 'Play an audio URL into a live call', hint: '--call ID --url https://...' },
1015
+ { name: 'dtmf', description: 'Send DTMF tones to a live call', hint: '--call ID --digits "1234#"' },
1016
+ { name: 'gather', description: 'Collect DTMF input from caller', hint: '--call ID [--min-digits N --max-digits N --timeout MS --prompt "..."]' },
1017
+ { name: 'record', description: 'Start recording a live call', hint: '--call ID [--format mp3]' },
1018
+ { name: 'record-stop', description: 'Stop recording a live call', hint: '--call ID' },
1019
+ { name: 'hangup', description: 'End a live call', hint: '--call ID' },
1020
+ { name: 'answer', description: 'Answer an inbound call', hint: '--call ID' },
1021
+ { name: 'transfer', description: 'Transfer a live call to another number', hint: '--call ID --to +1...' },
665
1022
  ],
666
1023
  fromHome,
667
1024
  });
668
1025
  break;
669
1026
  }
1027
+ if (flags.help && subcommand && PHONE_HELP[subcommand]) {
1028
+ subcommandHelp('phone', subcommand, PHONE_HELP[subcommand]);
1029
+ break;
1030
+ }
670
1031
  switch (subcommand) {
671
1032
  case 'search': {
672
1033
  const country = flags.country || 'US';
@@ -735,12 +1096,130 @@ async function main() {
735
1096
  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' }));
736
1097
  break;
737
1098
  }
738
- default: err(`Unknown phone command: ${subcommand}. Try: search, buy, sms, call`);
1099
+ case 'list': {
1100
+ const data = await ao.phoneListNumbers();
1101
+ return print(data);
1102
+ }
1103
+ case 'messages': {
1104
+ const id = flags.id || positional[0];
1105
+ if (!id)
1106
+ err('--id PHONE_ID required');
1107
+ const data = await ao.phoneMessages(id);
1108
+ return print(data);
1109
+ }
1110
+ case 'calls': {
1111
+ const id = flags.id || positional[0];
1112
+ if (!id)
1113
+ err('--id PHONE_ID required');
1114
+ const data = await ao.phoneCalls(id);
1115
+ return print(data);
1116
+ }
1117
+ case 'release': {
1118
+ const id = flags.id || positional[0];
1119
+ if (!id)
1120
+ err('--id PHONE_ID required');
1121
+ const data = await ao.phoneRelease(id);
1122
+ return print(data);
1123
+ }
1124
+ case 'call-info': {
1125
+ const callId = flags.call || flags.id || positional[0];
1126
+ if (!callId)
1127
+ err('--call CALL_CONTROL_ID required');
1128
+ const data = await ao.phoneCallInfo(callId);
1129
+ return print(data);
1130
+ }
1131
+ case 'speak': {
1132
+ const callId = flags.call || flags.id;
1133
+ const text = flags.text || flags.tts;
1134
+ if (!callId || !text)
1135
+ err('--call <id>, --text <text> required');
1136
+ const voice = flags.voice;
1137
+ const language = flags.language;
1138
+ const data = await ao.phoneSpeak(callId, text, { ...(voice ? { voice } : {}), ...(language ? { language } : {}) });
1139
+ return print(data);
1140
+ }
1141
+ case 'play': {
1142
+ const callId = flags.call || flags.id;
1143
+ const audioUrl = flags.url || flags['audio-url'];
1144
+ if (!callId || !audioUrl)
1145
+ err('--call <id>, --url <https://...> required');
1146
+ const data = await ao.phonePlay(callId, audioUrl);
1147
+ return print(data);
1148
+ }
1149
+ case 'dtmf': {
1150
+ const callId = flags.call || flags.id;
1151
+ const digits = flags.digits || positional[0];
1152
+ if (!callId || !digits)
1153
+ err('--call <id>, --digits "1234#" required');
1154
+ const data = await ao.phoneDtmf(callId, digits);
1155
+ return print(data);
1156
+ }
1157
+ case 'gather': {
1158
+ const callId = flags.call || flags.id;
1159
+ if (!callId)
1160
+ err('--call <id> required');
1161
+ const parseInt0 = (v) => (typeof v === 'string' ? parseInt(v, 10) : undefined);
1162
+ const opts = {};
1163
+ const minDigits = parseInt0(flags['min-digits']);
1164
+ if (minDigits !== undefined && Number.isFinite(minDigits))
1165
+ opts.minDigits = minDigits;
1166
+ const maxDigits = parseInt0(flags['max-digits']);
1167
+ if (maxDigits !== undefined && Number.isFinite(maxDigits))
1168
+ opts.maxDigits = maxDigits;
1169
+ const timeoutMillis = parseInt0(flags.timeout);
1170
+ if (timeoutMillis !== undefined && Number.isFinite(timeoutMillis))
1171
+ opts.timeoutMillis = timeoutMillis;
1172
+ if (flags['terminating-digit'])
1173
+ opts.terminatingDigit = flags['terminating-digit'];
1174
+ if (flags.prompt)
1175
+ opts.prompt = flags.prompt;
1176
+ if (flags['prompt-voice'])
1177
+ opts.promptVoice = flags['prompt-voice'];
1178
+ const data = await ao.phoneGather(callId, opts);
1179
+ return print(data);
1180
+ }
1181
+ case 'record': {
1182
+ const callId = flags.call || flags.id;
1183
+ if (!callId)
1184
+ err('--call <id> required');
1185
+ const data = await ao.phoneRecord(callId, flags.format);
1186
+ return print(data);
1187
+ }
1188
+ case 'record-stop': {
1189
+ const callId = flags.call || flags.id;
1190
+ if (!callId)
1191
+ err('--call <id> required');
1192
+ const data = await ao.phoneRecordStop(callId);
1193
+ return print(data);
1194
+ }
1195
+ case 'hangup': {
1196
+ const callId = flags.call || flags.id;
1197
+ if (!callId)
1198
+ err('--call <id> required');
1199
+ const data = await ao.phoneHangup(callId);
1200
+ return print(data);
1201
+ }
1202
+ case 'answer': {
1203
+ const callId = flags.call || flags.id;
1204
+ if (!callId)
1205
+ err('--call <id> required');
1206
+ const data = await ao.phoneAnswer(callId);
1207
+ return print(data);
1208
+ }
1209
+ case 'transfer': {
1210
+ const callId = flags.call || flags.id;
1211
+ const to = flags.to;
1212
+ if (!callId || !to)
1213
+ err('--call <id>, --to <+E.164> required');
1214
+ const data = await ao.phoneTransfer(callId, to);
1215
+ return print(data);
1216
+ }
1217
+ 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`);
739
1218
  }
740
1219
  break;
741
1220
  }
742
1221
  case 'email': {
743
- if (!subcommand || flags.help) {
1222
+ if (!subcommand || (flags.help && !EMAIL_HELP[subcommand])) {
744
1223
  showMenu({
745
1224
  command: 'email',
746
1225
  title: 'email',
@@ -759,6 +1238,10 @@ async function main() {
759
1238
  });
760
1239
  break;
761
1240
  }
1241
+ if (flags.help && subcommand && EMAIL_HELP[subcommand]) {
1242
+ subcommandHelp('email', subcommand, EMAIL_HELP[subcommand]);
1243
+ break;
1244
+ }
762
1245
  switch (subcommand) {
763
1246
  case 'create': {
764
1247
  const name = flags.name || positional[0];
@@ -850,7 +1333,7 @@ async function main() {
850
1333
  break;
851
1334
  }
852
1335
  case 'compute': {
853
- if (!subcommand || flags.help) {
1336
+ if (!subcommand || (flags.help && !COMPUTE_HELP[subcommand])) {
854
1337
  showMenu({
855
1338
  command: 'compute',
856
1339
  title: 'compute',
@@ -877,6 +1360,10 @@ async function main() {
877
1360
  });
878
1361
  break;
879
1362
  }
1363
+ if (flags.help && subcommand && COMPUTE_HELP[subcommand]) {
1364
+ subcommandHelp('compute', subcommand, COMPUTE_HELP[subcommand]);
1365
+ break;
1366
+ }
880
1367
  switch (subcommand) {
881
1368
  case 'plans': {
882
1369
  // --location filters to types deployable in that location. Each
@@ -1452,7 +1939,7 @@ async function main() {
1452
1939
  break;
1453
1940
  }
1454
1941
  case 'domain': {
1455
- if (!subcommand || flags.help) {
1942
+ if (!subcommand || (flags.help && !DOMAIN_HELP[subcommand])) {
1456
1943
  showMenu({
1457
1944
  command: 'domain',
1458
1945
  title: 'domain',
@@ -1472,6 +1959,10 @@ async function main() {
1472
1959
  });
1473
1960
  break;
1474
1961
  }
1962
+ if (flags.help && subcommand && DOMAIN_HELP[subcommand]) {
1963
+ subcommandHelp('domain', subcommand, DOMAIN_HELP[subcommand]);
1964
+ break;
1965
+ }
1475
1966
  switch (subcommand) {
1476
1967
  case 'check': {
1477
1968
  const name = flags.name || positional[0];
@@ -1636,11 +2127,14 @@ async function main() {
1636
2127
  subtitle: 'Non-custodial HD wallet',
1637
2128
  footerLeft: 'Solana + Base wallet operations',
1638
2129
  commands: [
1639
- { name: 'create', description: 'Create a new wallet', hint: '[--managed]' },
1640
- { name: 'import', description: 'Import from mnemonic', hint: '--mnemonic "..."' },
1641
- { name: 'list', description: 'List all wallets' },
2130
+ { name: 'create', description: 'Create one or many wallets', hint: '[--tag X --count 100] [--solana|--base] [--managed]' },
2131
+ { name: 'import', description: 'Import from mnemonic', hint: '--mnemonic "..." [--tag X]' },
2132
+ { name: 'list', description: 'List all wallets', hint: '[--tag <name>]' },
1642
2133
  { name: 'info', description: 'Wallet details', hint: 'WALLET_ID' },
1643
2134
  { name: 'addresses', description: 'Show all chain addresses', hint: 'WALLET_ID' },
2135
+ { name: 'tags', description: 'List wallet tags with counts' },
2136
+ { name: 'tag', description: 'Assign / change / clear a wallet tag', hint: 'WALLET_ID TAG | WALLET_ID --clear' },
2137
+ { name: 'tag-delete', description: 'Cascade-delete every wallet under a tag', hint: 'TAG --confirm' },
1644
2138
  { name: 'sign-message', description: 'Sign a message', hint: 'WALLET_ID --chain evm --msg "hello"' },
1645
2139
  { name: 'export', description: 'Export mnemonic for backup', hint: 'WALLET_ID --confirm' },
1646
2140
  { name: 'api-key', description: 'Create agent API key', hint: 'WALLET_ID --name my-agent' },
@@ -1679,16 +2173,72 @@ async function main() {
1679
2173
  switch (subcommand) {
1680
2174
  case 'create': {
1681
2175
  const isManaged = !!flags.managed;
2176
+ const tagRaw = flags.tag || undefined;
2177
+ const countRaw = flags.count;
2178
+ const count = countRaw !== undefined ? parseInt(String(countRaw), 10) : 1;
2179
+ if (!Number.isFinite(count) || count < 1)
2180
+ err('--count must be a positive integer', EXIT.BAD_INPUT);
2181
+ // Chain filter: --solana / --base. Default (neither) → both chains.
2182
+ const wantSol = !!flags.solana;
2183
+ const wantBase = !!flags.base;
2184
+ const chains = (wantSol && !wantBase) ? ['solana']
2185
+ : (wantBase && !wantSol) ? ['base']
2186
+ : ['solana', 'base'];
2187
+ // ─── Bulk path ───
2188
+ if (count > 1) {
2189
+ if (isManaged)
2190
+ err('Bulk wallet creation only supports unmanaged wallets — managed wallets need per-wallet passkey setup.', EXIT.BAD_INPUT);
2191
+ if (!tagRaw)
2192
+ err('--tag is required when --count > 1', EXIT.BAD_INPUT);
2193
+ if (count > 500)
2194
+ err('--count must be ≤ 500 per call', EXIT.BAD_INPUT);
2195
+ const prefix = flags['name-prefix'] || tagRaw;
2196
+ const { createLocalWalletsBatch } = await import('./vault.js');
2197
+ const { storeSecretsBatch } = await import('./credential-store.js');
2198
+ // Progress to stderr so JSON on stdout stays clean
2199
+ if (!AGENT_MODE)
2200
+ process.stderr.write(`creating ${count} wallets under tag "${tagRaw}"...\n`);
2201
+ const results = createLocalWalletsBatch(prefix, count, 'unmanaged', { tag: tagRaw, chains });
2202
+ if (!AGENT_MODE)
2203
+ process.stderr.write(`sealing ${count} session secrets in OS credential store...\n`);
2204
+ storeSecretsBatch(results.map(r => ({ account: r.id, secret: r.sessionSecret })));
2205
+ log(`wallet create: ${count} wallets under tag "${tagRaw}" (chains=${chains.join(',')})`);
2206
+ if (AGENT_MODE) {
2207
+ print({
2208
+ count: results.length,
2209
+ tag: tagRaw,
2210
+ chains,
2211
+ wallets: results.map(r => ({
2212
+ id: r.id,
2213
+ name: r.name,
2214
+ mode: r.mode,
2215
+ tag: r.tag,
2216
+ chains: r.chains,
2217
+ solana: r.solanaAddress,
2218
+ base: r.evmAddress,
2219
+ })),
2220
+ });
2221
+ }
2222
+ else {
2223
+ console.log(`\n ${t.success}✔${t.reset} Created ${count} wallets under tag ${t.accent}${tagRaw}${t.reset}`);
2224
+ console.log(` ${t.muted}chains:${t.reset} ${chains.join(', ')}`);
2225
+ console.log(` ${t.muted}names: ${t.reset}${results[0].name} … ${results[results.length - 1].name}`);
2226
+ console.log(`\n ${t.muted}List them: ${t.reset}palmyr wallet list --tag ${tagRaw}`);
2227
+ console.log(` ${t.muted}Delete all: ${t.reset}palmyr wallet tag-delete ${tagRaw} --confirm\n`);
2228
+ }
2229
+ break;
2230
+ }
2231
+ // ─── Single-create path ───
1682
2232
  // Accept --name (primary) or --label (alias)
1683
2233
  const name = flags.name || flags.label || 'My Wallet';
1684
2234
  const mode = isManaged ? 'managed' : 'unmanaged';
1685
2235
  // Create locally — no server needed for the key material
1686
2236
  const { createLocalWallet } = await import('./vault.js');
1687
- const w = createLocalWallet(name, mode);
2237
+ const w = createLocalWallet(name, mode, { tag: tagRaw, chains });
1688
2238
  // Store session secret in OS credential store
1689
2239
  const { storeSecret } = await import('./credential-store.js');
1690
2240
  storeSecret(w.id, w.sessionSecret);
1691
- log(`wallet create: ${w.id} (${mode})`);
2241
+ log(`wallet create: ${w.id} (${mode}${tagRaw ? `, tag=${tagRaw}` : ''}, chains=${chains.join(',')})`);
1692
2242
  // For managed wallets, register metadata with the server to get a setup link
1693
2243
  let setupLink;
1694
2244
  if (isManaged) {
@@ -1723,6 +2273,7 @@ async function main() {
1723
2273
  mode: w.mode,
1724
2274
  solana: w.solanaAddress,
1725
2275
  base: w.evmAddress,
2276
+ tag: w.tag,
1726
2277
  }));
1727
2278
  if (setupLink) {
1728
2279
  console.log(`\n ${t.accent}Setup link${t.reset} — send to the human who will manage this wallet:`);
@@ -1741,8 +2292,14 @@ async function main() {
1741
2292
  err('--mnemonic "your twelve words..." required');
1742
2293
  const name = flags.name || flags.label || 'Imported Wallet';
1743
2294
  const mode = flags.managed ? 'managed' : 'unmanaged';
2295
+ const tagRaw = flags.tag || undefined;
2296
+ const wantSol = !!flags.solana;
2297
+ const wantBase = !!flags.base;
2298
+ const chains = (wantSol && !wantBase) ? ['solana']
2299
+ : (wantBase && !wantSol) ? ['base']
2300
+ : ['solana', 'base'];
1744
2301
  const { importLocalWallet } = await import('./vault.js');
1745
- const w = importLocalWallet(name, mnemonic, mode);
2302
+ const w = importLocalWallet(name, mnemonic, mode, { tag: tagRaw, chains });
1746
2303
  // Store session secret
1747
2304
  const { storeSecret } = await import('./credential-store.js');
1748
2305
  storeSecret(w.id, w.sessionSecret);
@@ -1755,6 +2312,7 @@ async function main() {
1755
2312
  mode: w.mode,
1756
2313
  solana: w.solanaAddress,
1757
2314
  base: w.evmAddress,
2315
+ tag: w.tag,
1758
2316
  }));
1759
2317
  }
1760
2318
  else {
@@ -1765,7 +2323,10 @@ async function main() {
1765
2323
  case 'list': {
1766
2324
  // List from local vault — no server needed
1767
2325
  const { listVaultWallets } = await import('./vault.js');
1768
- const wallets = listVaultWallets();
2326
+ const tagFilter = flags.tag;
2327
+ let wallets = listVaultWallets();
2328
+ if (tagFilter)
2329
+ wallets = wallets.filter(w => w.tag === tagFilter);
1769
2330
  if (!AGENT_MODE) {
1770
2331
  render(React.createElement(WalletListScreen, {
1771
2332
  version: VERSION,
@@ -1775,14 +2336,93 @@ async function main() {
1775
2336
  mode: w.mode,
1776
2337
  solana: w.solanaAddress,
1777
2338
  base: w.evmAddress,
2339
+ tag: w.tag,
1778
2340
  })),
1779
2341
  }));
1780
2342
  }
1781
2343
  else {
1782
- print({ wallets });
2344
+ print({ wallets, ...(tagFilter ? { tag: tagFilter } : {}) });
2345
+ }
2346
+ break;
2347
+ }
2348
+ case 'tags': {
2349
+ const { listTags } = await import('./vault.js');
2350
+ const tags = listTags();
2351
+ if (AGENT_MODE) {
2352
+ print({ tags });
2353
+ }
2354
+ else {
2355
+ if (tags.length === 0) {
2356
+ console.log(`\n ${t.muted}No tagged wallets yet.${t.reset}`);
2357
+ console.log(` ${t.muted}Create some: ${t.reset}palmyr wallet create --tag demo --count 5\n`);
2358
+ }
2359
+ else {
2360
+ console.log(`\n ${t.accent}wallet tags${t.reset}`);
2361
+ for (const tg of tags) {
2362
+ console.log(` ${t.bold}${tg.name}${t.reset} ${t.muted}·${t.reset} ${tg.count} wallet(s) ${t.muted}·${t.reset} ${tg.chains.join(',')}`);
2363
+ }
2364
+ console.log('');
2365
+ }
1783
2366
  }
1784
2367
  break;
1785
2368
  }
2369
+ case 'tag': {
2370
+ const walletId = positional[0] || flags.id;
2371
+ if (!walletId)
2372
+ err('Wallet ID required: palmyr wallet tag <WALLET_ID> <TAG> | --clear', EXIT.BAD_INPUT);
2373
+ const wantClear = !!flags.clear;
2374
+ const newTag = positional[1] || flags.tag;
2375
+ if (!wantClear && !newTag)
2376
+ err('Pass a TAG or --clear', EXIT.BAD_INPUT);
2377
+ if (wantClear && newTag)
2378
+ err('Cannot pass both a TAG and --clear', EXIT.BAD_INPUT);
2379
+ const { tagWallet } = await import('./vault.js');
2380
+ const out = tagWallet(walletId, wantClear ? null : newTag);
2381
+ log(`wallet tag: ${out.id} → ${out.tag ?? '(cleared)'}`);
2382
+ print({ success: true, ...out });
2383
+ break;
2384
+ }
2385
+ case 'tag-delete': {
2386
+ const tagArg = positional[0] || flags.tag;
2387
+ if (!tagArg)
2388
+ err('Tag required: palmyr wallet tag-delete <TAG> --confirm', EXIT.BAD_INPUT);
2389
+ if (!flags.confirm) {
2390
+ err(`This will permanently delete every wallet tagged "${tagArg}" and their session secrets.\n\n` +
2391
+ ' Re-run with --confirm to proceed:\n' +
2392
+ ` palmyr wallet tag-delete ${tagArg} --confirm`, EXIT.BAD_INPUT);
2393
+ }
2394
+ const { walletsByTag, deleteLocalWallet } = await import('./vault.js');
2395
+ const { deleteSecret } = await import('./credential-store.js');
2396
+ const targets = walletsByTag(tagArg);
2397
+ if (targets.length === 0)
2398
+ err(`No wallets found with tag "${tagArg}"`, EXIT.NOT_FOUND);
2399
+ const deleted = [];
2400
+ const failed = [];
2401
+ for (const w of targets) {
2402
+ try {
2403
+ const out = deleteLocalWallet(w.id);
2404
+ try {
2405
+ deleteSecret(w.id);
2406
+ }
2407
+ catch { }
2408
+ deleted.push(out);
2409
+ if (!AGENT_MODE)
2410
+ process.stderr.write(`deleted ${out.name} (${out.id.slice(0, 8)}...)\n`);
2411
+ }
2412
+ catch (e) {
2413
+ failed.push({ id: w.id, name: w.name, error: e?.message || String(e) });
2414
+ }
2415
+ }
2416
+ log(`wallet tag-delete: ${deleted.length} deleted, ${failed.length} failed (tag=${tagArg})`);
2417
+ print({
2418
+ success: failed.length === 0,
2419
+ tag: tagArg,
2420
+ deleted: deleted.length,
2421
+ wallets: deleted,
2422
+ ...(failed.length > 0 ? { failed } : {}),
2423
+ });
2424
+ break;
2425
+ }
1786
2426
  case 'info': {
1787
2427
  const walletId = positional[0] || flags.id;
1788
2428
  if (!walletId)
@@ -3741,12 +4381,12 @@ async function main() {
3741
4381
  }
3742
4382
  err(`Unknown trading-keystore subcommand: ${sub}. Try: init, unlock, lock, list, status, derive, export`, EXIT.BAD_INPUT);
3743
4383
  }
3744
- default: err(`Unknown wallet command: ${subcommand}. Try: create, import, list, info, export, addresses, sign-message, api-key, config, use, request-approval, buy, cohort, template, positions, position, sell, sync, pnl, journal, watch, brief, daemon, triggers, trading-keystore, evm-quote`);
4384
+ default: err(`Unknown wallet command: ${subcommand}. Try: create, import, list, info, tags, tag, tag-delete, export, addresses, sign-message, api-key, config, use, request-approval, buy, cohort, template, positions, position, sell, sync, pnl, journal, watch, brief, daemon, triggers, trading-keystore, evm-quote`);
3745
4385
  }
3746
4386
  break;
3747
4387
  }
3748
4388
  case 'chat': {
3749
- if (!subcommand || flags.help) {
4389
+ if (!subcommand || (flags.help && !CHAT_HELP[subcommand])) {
3750
4390
  showMenu({
3751
4391
  command: 'chat',
3752
4392
  title: 'chat',
@@ -3765,6 +4405,10 @@ async function main() {
3765
4405
  });
3766
4406
  break;
3767
4407
  }
4408
+ if (flags.help && subcommand && CHAT_HELP[subcommand]) {
4409
+ subcommandHelp('chat', subcommand, CHAT_HELP[subcommand]);
4410
+ break;
4411
+ }
3768
4412
  switch (subcommand) {
3769
4413
  case 'run': {
3770
4414
  const intent = (positional.join(' ') || flags.intent || '').trim();
@@ -4001,14 +4645,18 @@ async function main() {
4001
4645
  const sv = await import('./social-vault.js');
4002
4646
  const platform = 'twitter';
4003
4647
  // Resolve a username to its server-side account_id and which table
4004
- // holds it. X accounts can live in two places: x_accounts (pool-bought)
4005
- // and social_registered_accounts (BYO-registered). The local vault
4006
- // doesn't track which, so we query both in parallel and find a match.
4007
- // Used by transfer / share / unshare to dispatch to the correct
4008
- // endpoint family. Returns null if the username isn't on the server.
4648
+ // holds it. X accounts can live in THREE places:
4649
+ // - x_accounts (legacy pool, rarely used now)
4650
+ // - social_account_pool (where `palmyr twitter buy` writes)
4651
+ // - social_registered_accounts (BYO-registered)
4652
+ // The local vault doesn't track which, so we query all three in
4653
+ // parallel and find a match. Used by transfer / share / unshare
4654
+ // to dispatch to the correct endpoint family. Returns null if the
4655
+ // username isn't on any of them.
4009
4656
  const resolveServerAccount = async (username) => {
4010
- const [xMine, reg] = await Promise.allSettled([
4657
+ const [xMine, poolMine, reg] = await Promise.allSettled([
4011
4658
  ao.xAccountsMine(),
4659
+ ao.socialTwitterPoolMine(),
4012
4660
  ao.socialTwitterListRegistered(),
4013
4661
  ]);
4014
4662
  if (xMine.status === 'fulfilled') {
@@ -4016,6 +4664,11 @@ async function main() {
4016
4664
  if (m)
4017
4665
  return { kind: 'x_accounts', id: m.id };
4018
4666
  }
4667
+ if (poolMine.status === 'fulfilled') {
4668
+ const m = (poolMine.value?.accounts || []).find((a) => a.username === username);
4669
+ if (m)
4670
+ return { kind: 'pool', id: m.id };
4671
+ }
4019
4672
  if (reg.status === 'fulfilled') {
4020
4673
  const m = (reg.value?.accounts || []).find((a) => a.username === username);
4021
4674
  if (m)
@@ -4035,26 +4688,32 @@ async function main() {
4035
4688
  const existing = sv.getAccount(platform, username);
4036
4689
  if (existing)
4037
4690
  return existing;
4038
- const [xRes, regRes] = await Promise.allSettled([
4691
+ const [xRes, poolRes, regRes] = await Promise.allSettled([
4039
4692
  ao.xAccountsMine(),
4693
+ ao.socialTwitterPoolMine(),
4040
4694
  ao.socialTwitterRegisteredMine(),
4041
4695
  ]);
4042
4696
  const xMatch = xRes.status === 'fulfilled'
4043
4697
  ? (xRes.value?.accounts || []).find((a) => a.username === username)
4044
4698
  : null;
4699
+ const poolMatch = poolRes.status === 'fulfilled'
4700
+ ? (poolRes.value?.accounts || []).find((a) => a.username === username)
4701
+ : null;
4045
4702
  const regMatch = regRes.status === 'fulfilled'
4046
4703
  ? (regRes.value?.accounts || []).find((a) => a.username === username)
4047
4704
  : null;
4048
- const match = xMatch || regMatch;
4705
+ const match = xMatch || poolMatch || regMatch;
4049
4706
  if (!match) {
4050
4707
  err(`twitter account "${username}" not found locally or on the server (this wallet has no access)`, EXIT.NOT_FOUND);
4051
4708
  }
4709
+ // Pool-mine and registered-mine return creds nested under
4710
+ // `credentials`; legacy x_accounts/mine returns them flat alongside
4711
+ // cookies. Normalize so the import works the same in either case.
4052
4712
  const cookies = (match.cookies || []);
4053
4713
  const ct0 = cookies.find((c) => c.name === 'ct0')?.value;
4054
- // Registered accounts return creds nested under `credentials`;
4055
- // pool accounts return them flat alongside cookies.
4056
- const creds = regMatch
4057
- ? regMatch.credentials
4714
+ const hasNestedCreds = poolMatch || regMatch;
4715
+ const creds = hasNestedCreds
4716
+ ? match.credentials
4058
4717
  : {
4059
4718
  login: match.email || match.username,
4060
4719
  password: match.password,
@@ -4062,11 +4721,16 @@ async function main() {
4062
4721
  auth_token: match.auth_token || undefined,
4063
4722
  ct0,
4064
4723
  };
4065
- const summary = sv.importAccount(platform, username, creds, { source: 'auto-claim' });
4724
+ const summary = sv.importAccount(platform, username, creds, {
4725
+ source: 'auto-claim',
4726
+ proxy_session_id: poolMatch?.proxy_session_id,
4727
+ country: poolMatch?.country || undefined,
4728
+ });
4066
4729
  if (cookies.length > 0) {
4067
4730
  sv.saveSession(summary.id, platform, cookies);
4068
4731
  }
4069
- log(`auto-imported @${username} from server (${regMatch ? 'registered' : 'pool'} → local vault)`);
4732
+ const sourceLabel = poolMatch ? 'pool' : regMatch ? 'registered' : 'x_accounts';
4733
+ log(`auto-imported @${username} from server (${sourceLabel} → local vault)`);
4070
4734
  return summary;
4071
4735
  };
4072
4736
  if (!subcommand) {
@@ -4171,11 +4835,13 @@ async function main() {
4171
4835
  if (skipRemote) {
4172
4836
  return print({ accounts: localAccounts, count: localAccounts.length, source: 'local' });
4173
4837
  }
4174
- const [xRes, regRes] = await Promise.allSettled([
4838
+ const [xRes, poolRes, regRes] = await Promise.allSettled([
4175
4839
  ao.xAccountsMine(),
4840
+ ao.socialTwitterPoolMine(),
4176
4841
  ao.socialTwitterRegisteredMine(),
4177
4842
  ]);
4178
4843
  const xAccounts = xRes.status === 'fulfilled' ? (xRes.value?.accounts || []) : [];
4844
+ const poolAccounts = poolRes.status === 'fulfilled' ? (poolRes.value?.accounts || []) : [];
4179
4845
  const regAccounts = regRes.status === 'fulfilled' ? (regRes.value?.accounts || []) : [];
4180
4846
  const localUsernames = new Set(localAccounts.map(a => a.username));
4181
4847
  const serverOnly = [];
@@ -4189,6 +4855,16 @@ async function main() {
4189
4855
  });
4190
4856
  }
4191
4857
  }
4858
+ for (const a of poolAccounts) {
4859
+ if (a.username && !localUsernames.has(a.username)) {
4860
+ serverOnly.push({
4861
+ username: a.username,
4862
+ access: a.access || 'owner',
4863
+ source_table: 'pool',
4864
+ status: 'server-only — run `palmyr twitter claim` to import',
4865
+ });
4866
+ }
4867
+ }
4192
4868
  for (const a of regAccounts) {
4193
4869
  if (a.username && !localUsernames.has(a.username)) {
4194
4870
  serverOnly.push({
@@ -5088,6 +5764,13 @@ async function main() {
5088
5764
  spinReg.stop('Registered', true);
5089
5765
  resolved = { kind: 'registered', id: regData.id };
5090
5766
  }
5767
+ // Transfer-on-pool isn't wired yet (pool table has no transfer
5768
+ // endpoint with the async rotation machinery). Surface a clear
5769
+ // workaround instead of a silent 404.
5770
+ if (resolved.kind === 'pool') {
5771
+ err(`@${username} is a pool-bought account and transfer isn't supported on the pool table yet — share/unshare work.\n\n` +
5772
+ `Workaround: \`palmyr twitter register ${username}\` first (uploads to the registered table), then re-run the transfer.`, EXIT.GENERAL);
5773
+ }
5091
5774
  const spin = new Spinner();
5092
5775
  spin.start(`Rotating @${username} password and transferring…`);
5093
5776
  // Kick off the transfer. Server responds 202 with a transfer_id;
@@ -5169,7 +5852,9 @@ async function main() {
5169
5852
  }
5170
5853
  const data = resolved.kind === 'x_accounts'
5171
5854
  ? await ao.xAccountShare(resolved.id, withWallet)
5172
- : await ao.socialTwitterRegisteredShare(resolved.id, withWallet);
5855
+ : resolved.kind === 'pool'
5856
+ ? await ao.socialTwitterPoolShare(resolved.id, withWallet)
5857
+ : await ao.socialTwitterRegisteredShare(resolved.id, withWallet);
5173
5858
  log(`twitter share: @${username} → ${withWallet}`);
5174
5859
  return print({ ...data, source_table: resolved.kind });
5175
5860
  }
@@ -5193,9 +5878,20 @@ async function main() {
5193
5878
  spin.start(`Unsharing @${username} and rotating password…`);
5194
5879
  let data;
5195
5880
  try {
5196
- data = resolved.kind === 'x_accounts'
5197
- ? await ao.xAccountUnshare(resolved.id, targetWallet, { rotate })
5198
- : await ao.socialTwitterRegisteredUnshare(resolved.id, targetWallet, { rotate });
5881
+ if (resolved.kind === 'x_accounts') {
5882
+ data = await ao.xAccountUnshare(resolved.id, targetWallet, { rotate });
5883
+ }
5884
+ else if (resolved.kind === 'pool') {
5885
+ // The pool unshare endpoint doesn't support rotate today
5886
+ // (rotation infra lives on the transfers async pipeline and
5887
+ // hasn't been wired to pool yet). Warn and call without rotate.
5888
+ if (rotate)
5889
+ warn(`--rotate not supported on pool-bought accounts yet — performing unshare only`);
5890
+ data = await ao.socialTwitterPoolUnshare(resolved.id, targetWallet);
5891
+ }
5892
+ else {
5893
+ data = await ao.socialTwitterRegisteredUnshare(resolved.id, targetWallet, { rotate });
5894
+ }
5199
5895
  }
5200
5896
  catch (e) {
5201
5897
  if (spin)
@@ -5284,20 +5980,23 @@ async function main() {
5284
5980
  }
5285
5981
  case 'claim': {
5286
5982
  // Fetch server-side accounts the calling wallet owns or has shared
5287
- // access to — from BOTH tables. x_accounts (pool-bought) and
5983
+ // access to — from all THREE tables. x_accounts (legacy pool),
5984
+ // social_account_pool (where `palmyr twitter buy` writes), and
5288
5985
  // social_registered_accounts (BYO-registered) are queried in
5289
- // parallel; both contribute to the claim list. Import any not
5290
- // already in the local vault so the receiver of a transfer can
5291
- // pick up the account in one command.
5292
- const [xRes, regRes] = await Promise.allSettled([
5986
+ // parallel; all three contribute to the claim list. Import any
5987
+ // not already in the local vault so the receiver of a transfer
5988
+ // or share can pick up the account in one command.
5989
+ const [xRes, poolRes, regRes] = await Promise.allSettled([
5293
5990
  ao.xAccountsMine(),
5991
+ ao.socialTwitterPoolMine(),
5294
5992
  ao.socialTwitterRegisteredMine(),
5295
5993
  ]);
5296
5994
  const xAccounts = xRes.status === 'fulfilled' ? (xRes.value?.accounts || []) : [];
5995
+ const poolAccountsRaw = poolRes.status === 'fulfilled' ? (poolRes.value?.accounts || []) : [];
5297
5996
  const regAccountsRaw = regRes.status === 'fulfilled' ? (regRes.value?.accounts || []) : [];
5298
- // Normalize the registered shape (creds + cookies live in nested
5299
- // fields) to the same flat shape as x_accounts so the loop below
5300
- // doesn't have to branch on source.
5997
+ // Normalize the registered + pool shape (creds + cookies live in
5998
+ // nested fields) to the same flat shape as x_accounts so the
5999
+ // loop below doesn't have to branch on source.
5301
6000
  const regAccounts = regAccountsRaw.map(a => ({
5302
6001
  username: a.username,
5303
6002
  email: a.credentials?.email,
@@ -5307,8 +6006,20 @@ async function main() {
5307
6006
  access: a.access,
5308
6007
  source_table: 'registered',
5309
6008
  }));
6009
+ const poolAccounts = poolAccountsRaw.map(a => ({
6010
+ username: a.username,
6011
+ email: a.credentials?.email,
6012
+ password: a.credentials?.password,
6013
+ auth_token: a.credentials?.auth_token,
6014
+ cookies: a.cookies || [],
6015
+ access: a.access,
6016
+ proxy_session_id: a.proxy_session_id,
6017
+ country: a.country,
6018
+ source_table: 'pool',
6019
+ }));
5310
6020
  const accounts = [
5311
6021
  ...xAccounts.map(a => ({ ...a, source_table: 'x_accounts' })),
6022
+ ...poolAccounts,
5312
6023
  ...regAccounts,
5313
6024
  ];
5314
6025
  if (accounts.length === 0) {
@@ -5332,7 +6043,11 @@ async function main() {
5332
6043
  auth_token: a.auth_token || undefined,
5333
6044
  ct0,
5334
6045
  };
5335
- const summary = sv.importAccount(platform, a.username, creds, { source: 'claim' });
6046
+ const summary = sv.importAccount(platform, a.username, creds, {
6047
+ source: 'claim',
6048
+ proxy_session_id: a.proxy_session_id,
6049
+ country: a.country || undefined,
6050
+ });
5336
6051
  // Save the cookies so `palmyr twitter login` can use the
5337
6052
  // cookie-fast-path instead of re-driving the login form.
5338
6053
  if (Array.isArray(a.cookies) && a.cookies.length > 0) {