@palmyr/cli 1.8.3 → 1.8.5

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
@@ -229,11 +229,10 @@ function emitSessionOnlyWarning(write) {
229
229
  const WALLET_HELP = {
230
230
  create: [
231
231
  { flag: '--name <name>', desc: 'Wallet name', hint: 'default: "My Wallet"' },
232
- { flag: '--managed', desc: 'Create managed wallet with human oversight via passkey (single-create only)' },
233
232
  { flag: '--solana', desc: 'Materialize the Solana account only', hint: 'default: both chains' },
234
233
  { flag: '--base', desc: 'Materialize the Base/EVM account only', hint: 'pair with --solana for both (default)' },
235
234
  { flag: '--tag <name>', desc: 'Folder-like grouping tag', hint: 'e.g. palmyr-demo — required with --count' },
236
- { flag: '--count <N>', desc: 'Bulk-create N wallets in one call (1-500)', hint: 'unmanaged only; requires --tag' },
235
+ { flag: '--count <N>', desc: 'Bulk-create N wallets in one call (1-500)', hint: 'requires --tag' },
237
236
  { flag: '--name-prefix <p>', desc: 'Bulk name prefix; suffixed `-001..-N`', hint: 'default: same as --tag' },
238
237
  { flag: '--passphrase <p>', desc: 'Seal the mnemonic with this passphrase (≥8 chars) for durable recovery across reboot / OS-keychain loss / host migration', hint: 'or PALMYR_WALLET_PASSPHRASE env (env preferred — keeps phrase out of shell history). Interactive prompt on TTY when neither set.' },
239
238
  { flag: '--session-only', desc: 'OPT OUT of the passphrase fallback. Wallet is bound to this machine\'s OS keychain — dies on reboot/keyring loss/migration.', hint: 'use only for ephemeral / throwaway wallets where loss is acceptable' },
@@ -241,7 +240,6 @@ const WALLET_HELP = {
241
240
  import: [
242
241
  { flag: '--mnemonic <words>', desc: 'BIP-39 mnemonic phrase (required)' },
243
242
  { flag: '--name <name>', desc: 'Wallet name', hint: 'default: "Imported Wallet"' },
244
- { flag: '--managed', desc: 'Import as managed wallet' },
245
243
  { flag: '--solana', desc: 'Materialize the Solana account only' },
246
244
  { flag: '--base', desc: 'Materialize the Base/EVM account only' },
247
245
  { flag: '--tag <name>', desc: 'Assign a tag at import time' },
@@ -449,6 +447,15 @@ const PHONE_HELP = {
449
447
  { flag: '(price)', desc: '$0.02 per call' },
450
448
  { flag: '(example)', desc: 'palmyr phone messages --id PN_abc' },
451
449
  ],
450
+ message: [
451
+ { flag: '--id <MESSAGE_ID>', desc: 'SMS message id (Telnyx-supplied; positional also accepted)' },
452
+ { flag: '(price)', desc: '$0.005 per readback — cheap so agents can poll until delivery_status is terminal' },
453
+ { flag: '(example)', desc: 'palmyr phone message <message-id-from-sms-response>' },
454
+ ],
455
+ 'sms-status': [
456
+ { flag: '<message-id>', desc: 'Alias for `palmyr phone message <id>` — readback by id' },
457
+ { flag: '(price)', desc: '$0.005 per readback' },
458
+ ],
452
459
  calls: [
453
460
  { flag: '--id <PHONE_ID>', desc: 'Phone number id to list calls for (required; positional also accepted)' },
454
461
  { flag: '(price)', desc: '$0.02 per call' },
@@ -527,7 +534,7 @@ const EMAIL_HELP = {
527
534
  create: [
528
535
  { flag: '--name <name>', desc: 'Inbox name (required)' },
529
536
  { flag: '--domain <domain>', desc: 'Wallet-owned domain to host the inbox on (optional)', hint: 'default: Palmyr-hosted domain' },
530
- { flag: '--wallet <id|name>', desc: 'Wallet to own the inbox (optional)' },
537
+ { flag: '--wallet <id|name|sol_pubkey>', desc: 'Inbox owner — vault id/name (resolves to its Solana address) or a raw Solana pubkey. Omit to use the paying wallet.', hint: 'E2E encryption is Ed25519, so the owner must always be a Solana address — Base addresses cannot own an inbox' },
531
538
  { flag: '(price)', desc: '$2.00 per inbox provisioned' },
532
539
  { flag: '(example)', desc: 'palmyr email create --name agent --domain example.com' },
533
540
  ],
@@ -733,6 +740,283 @@ const CHAT_HELP = {
733
740
  { flag: '(example)', desc: 'palmyr chat providers --capability web_search' },
734
741
  ],
735
742
  };
743
+ // Help tables for the two social subsystems. Their presence is what gates
744
+ // `--help` from dispatching paid actions — see the `case 'twitter'` and
745
+ // `case 'tiktok'` blocks below. 1.8.3 had no entries here and the
746
+ // `case 'buy'` arm immediately called the $5 paid endpoint when a user
747
+ // (reasonably) ran `palmyr twitter buy --help`. Every entry below MUST
748
+ // flag the price for paid subcommands so future readers can scan it.
749
+ const TWITTER_HELP = {
750
+ import: [
751
+ { flag: '<username>', desc: 'Twitter handle to import' },
752
+ { flag: '--credentials-line "..."', desc: 'login:password:email:email_pw:totp_seed:ct0:auth_token format' },
753
+ { flag: '--username --password ...', desc: 'Alternative: individual flags for each field' },
754
+ { flag: '(price)', desc: 'Free — local vault only' },
755
+ ],
756
+ list: [
757
+ { flag: '--local', desc: 'Skip server check; show only locally-vaulted accounts' },
758
+ { flag: '(price)', desc: 'Free local listing + paid lookups when --local is omitted (~$0.001 to enumerate server-side access)' },
759
+ ],
760
+ info: [
761
+ { flag: '<username>', desc: 'Account to inspect' },
762
+ { flag: '(price)', desc: 'Free — local vault read' },
763
+ ],
764
+ rename: [
765
+ { flag: '<old>', desc: 'Current local handle' },
766
+ { flag: '--to <new>', desc: 'New handle (after a real-server rename)' },
767
+ { flag: '(price)', desc: 'Free — local-only metadata update' },
768
+ ],
769
+ remove: [
770
+ { flag: '<username>', desc: 'Account to remove' },
771
+ { flag: '--confirm', desc: 'Required — local delete is irreversible' },
772
+ { flag: '(price)', desc: 'Free — local vault only' },
773
+ ],
774
+ totp: [
775
+ { flag: '<username>', desc: 'Account whose current TOTP code to print' },
776
+ { flag: '(price)', desc: 'Free — local TOTP generation' },
777
+ ],
778
+ buy: [
779
+ { flag: '(no args)', desc: 'Purchase the oldest ready X account from the supplier pool' },
780
+ { flag: '(price)', desc: '$5 USDC — paid via x402. Account auto-imported into the local vault and session primed.' },
781
+ { flag: '(example)', desc: 'palmyr twitter buy' },
782
+ ],
783
+ login: [
784
+ { flag: '<username>', desc: 'Force a fresh server-side session (browser runtime)' },
785
+ { flag: '(price)', desc: '$0.005 USDC' },
786
+ ],
787
+ 'manual-login': [
788
+ { flag: '<username>', desc: 'Open a remote browser session you sign in to manually' },
789
+ { flag: '(price)', desc: 'Variable — server-side browser session cost' },
790
+ ],
791
+ session: [
792
+ { flag: '<username>', desc: 'Inspect cached server-side session status' },
793
+ { flag: '(price)', desc: 'Free' },
794
+ ],
795
+ post: [
796
+ { flag: '<username>', desc: 'Account to post from' },
797
+ { flag: '--body "..."', desc: 'Tweet body (required)' },
798
+ { flag: '(price)', desc: '$0.001 USDC per post' },
799
+ ],
800
+ reply: [
801
+ { flag: '<username>', desc: 'Account to reply from' },
802
+ { flag: '--to <url>', desc: 'Tweet URL to reply to' },
803
+ { flag: '--body "..."', desc: 'Reply body' },
804
+ { flag: '(price)', desc: '$0.001 USDC' },
805
+ ],
806
+ like: [
807
+ { flag: '<username>', desc: 'Account doing the like' },
808
+ { flag: '--tweet <url>', desc: 'Tweet to like' },
809
+ { flag: '(price)', desc: '$0.001 USDC' },
810
+ ],
811
+ retweet: [
812
+ { flag: '<username>', desc: 'Account doing the retweet' },
813
+ { flag: '--tweet <url>', desc: 'Tweet to retweet' },
814
+ { flag: '(price)', desc: '$0.001 USDC' },
815
+ ],
816
+ follow: [
817
+ { flag: '<username>', desc: 'Account doing the follow' },
818
+ { flag: '--user @handle', desc: 'User to follow' },
819
+ { flag: '(price)', desc: '$0.001 USDC' },
820
+ ],
821
+ unfollow: [
822
+ { flag: '<username>', desc: 'Account doing the unfollow' },
823
+ { flag: '--user @handle', desc: 'User to unfollow' },
824
+ { flag: '(price)', desc: '$0.001 USDC' },
825
+ ],
826
+ delete: [
827
+ { flag: '<username>', desc: 'Account that posted the tweet' },
828
+ { flag: '--tweet <url>', desc: 'Tweet to delete' },
829
+ { flag: '(price)', desc: '$0.001 USDC' },
830
+ ],
831
+ 'list-tweets': [
832
+ { flag: '<username>', desc: 'Account whose timeline to fetch' },
833
+ { flag: '(price)', desc: '$0.001 USDC' },
834
+ ],
835
+ bio: [
836
+ { flag: '<username>', desc: 'Account whose bio to update' },
837
+ { flag: '--text "..."', desc: 'New bio (<=160 chars)' },
838
+ { flag: '(price)', desc: '$0.001 USDC' },
839
+ ],
840
+ name: [
841
+ { flag: '<username>', desc: 'Account whose display name to update' },
842
+ { flag: '--display "..."', desc: 'New display name' },
843
+ { flag: '(price)', desc: '$0.001 USDC' },
844
+ ],
845
+ location: [
846
+ { flag: '<username>', desc: 'Account whose location to update' },
847
+ { flag: '--location "..."', desc: 'New location string' },
848
+ { flag: '(price)', desc: '$0.001 USDC' },
849
+ ],
850
+ website: [
851
+ { flag: '<username>', desc: 'Account whose profile website to update' },
852
+ { flag: '--url https://...', desc: 'New website URL' },
853
+ { flag: '(price)', desc: '$0.001 USDC' },
854
+ ],
855
+ pfp: [
856
+ { flag: '<username>', desc: 'Account whose avatar to update' },
857
+ { flag: '--file pic.png', desc: 'Image file (jpeg / png)' },
858
+ { flag: '(price)', desc: '$0.005 USDC' },
859
+ ],
860
+ banner: [
861
+ { flag: '<username>', desc: 'Account whose banner to update' },
862
+ { flag: '--file banner.png', desc: 'Image file' },
863
+ { flag: '(price)', desc: '$0.005 USDC' },
864
+ ],
865
+ username: [
866
+ { flag: '<username>', desc: 'Current account handle' },
867
+ { flag: '--to <new>', desc: 'New handle' },
868
+ { flag: '(price)', desc: '$0.005 USDC' },
869
+ ],
870
+ transfer: [
871
+ { flag: '<username>', desc: 'Account to transfer' },
872
+ { flag: '--to <wallet>', desc: 'Destination wallet address' },
873
+ { flag: '--confirm', desc: 'Required — rotates password, revokes other sessions' },
874
+ { flag: '(price)', desc: 'Free for vaulted accounts; ~$0.01 USDC to auto-register an imported-only account first' },
875
+ ],
876
+ share: [
877
+ { flag: '<username>', desc: 'Account to share' },
878
+ { flag: '--with <wallet>', desc: 'Wallet to grant access to' },
879
+ { flag: '(price)', desc: 'Free for shared access (no password rotation)' },
880
+ ],
881
+ unshare: [
882
+ { flag: '<username>', desc: 'Account to revoke share on' },
883
+ { flag: '--from <wallet>', desc: 'Wallet to revoke' },
884
+ { flag: '--rotate', desc: 'Also rotate password so cached cookies stop working' },
885
+ { flag: '(price)', desc: 'Free; --rotate runs async like transfer (~30-90s)' },
886
+ ],
887
+ claim: [
888
+ { flag: '(no args)', desc: 'Pull every server-side X account the wallet can access into the local vault' },
889
+ { flag: '(price)', desc: '~$0.001 USDC per account claimed (creds-decryption fee)' },
890
+ ],
891
+ // Server-backed account registration. Without these entries, `palmyr twitter
892
+ // register --help` (and friends) fell through to the top-level menu instead
893
+ // of explaining their flags. Subcommands listed in the parent switch must
894
+ // always have an entry here so the help guard fires before the case body.
895
+ thread: [
896
+ { flag: '<username>', desc: 'Account to post the thread from' },
897
+ { flag: '--texts \'["...","..."]\'', desc: 'JSON array of tweets (in order)' },
898
+ { flag: '--file path.json', desc: 'Alternative: read the JSON array from a file' },
899
+ { flag: '(price)', desc: '$0.005 USDC' },
900
+ ],
901
+ register: [
902
+ { flag: '<username>', desc: 'Account handle' },
903
+ { flag: '--password <pw>', desc: 'Required if the account is not already in the local vault' },
904
+ { flag: '--login --email --totp-seed --auth-token --ct0', desc: 'Optional; auto-pulled from local vault when not passed' },
905
+ { flag: '--country <CC>', desc: 'Optional residency hint stored alongside the encrypted credentials' },
906
+ { flag: '(price)', desc: 'Free — server tests login + encrypts creds at rest. Enables scheduling.' },
907
+ ],
908
+ unregister: [
909
+ { flag: '<username-or-id>', desc: 'Handle or 32-char hex account id' },
910
+ { flag: '(price)', desc: 'Free — wipes server-side credentials, account no longer schedulable' },
911
+ ],
912
+ registered: [
913
+ { flag: '(no args)', desc: 'List every server-registered X account this wallet owns' },
914
+ { flag: '(price)', desc: 'Free' },
915
+ ],
916
+ schedule: [
917
+ { flag: '<username>', desc: 'Account to post from (must be `register`-ed)' },
918
+ { flag: '--at "ISO8601"', desc: 'When to fire (e.g. --at "2026-05-15T14:00:00Z")' },
919
+ { flag: '--body "..."', desc: 'Text-only post' },
920
+ { flag: '--texts \'["..."]\' / --file path.json', desc: 'Thread' },
921
+ { flag: '--image / --video / --media-json', desc: 'Media attachments' },
922
+ { flag: '--community <id>', desc: 'Post into an X Community' },
923
+ { flag: '(price)', desc: '$0.001 USDC (text) or $0.005 USDC (thread / media) — paid up front' },
924
+ ],
925
+ queue: [
926
+ { flag: '--status pending|in_progress|completed|failed|cancelled', desc: 'Filter by status' },
927
+ { flag: '--from / --to "ISO8601"', desc: 'Filter by post-at window' },
928
+ { flag: '--account-id <id>', desc: 'Filter to one account' },
929
+ { flag: '--limit <n>', desc: 'Cap result count' },
930
+ { flag: '(price)', desc: 'Free' },
931
+ ],
932
+ cancel: [
933
+ { flag: '<schedule-id>', desc: 'Id from `palmyr twitter queue`' },
934
+ { flag: '(price)', desc: 'Free — only cancels pending posts; in-flight ones are already settled' },
935
+ ],
936
+ status: [
937
+ { flag: '<username>', desc: 'Account to inspect' },
938
+ { flag: '(price)', desc: 'Server-side liveness/shadow-ban check (not yet wired — see `palmyr twitter session` for cached login state)' },
939
+ ],
940
+ 'pool-add': [
941
+ { flag: '--credentials-line "..."', desc: 'Single account creds (login:pw:email:email_pw[:2fa[:ct0:auth_token]])' },
942
+ { flag: '--file path.txt', desc: 'Bulk: one credentials-line per row (# = comment)' },
943
+ { flag: '--price <USDC>', desc: 'Required — what `twitter buy` will charge per account' },
944
+ { flag: '--country <CC>', desc: 'Optional metadata' },
945
+ { flag: '--age 1y|2y|3y|...', desc: 'Optional age category metadata' },
946
+ { flag: '(auth)', desc: 'Admin-signed call — requires PALMYR_ADMIN_KEY' },
947
+ { flag: '(price)', desc: 'Free — server-side seeding by pool operator' },
948
+ ],
949
+ 'pool-status': [
950
+ { flag: '(no args)', desc: 'Available / sold / reserved counts in the X account pool' },
951
+ { flag: '(auth)', desc: 'Admin-signed call — requires PALMYR_ADMIN_KEY' },
952
+ { flag: '(price)', desc: 'Free' },
953
+ ],
954
+ };
955
+ const TIKTOK_HELP = {
956
+ import: [
957
+ { flag: '<username>', desc: 'TikTok handle to import' },
958
+ { flag: '--sessionid <s> --csrf <c> --webid <w>', desc: 'Cookies from a logged-in TikTok browser' },
959
+ { flag: '--credentials-line "..."', desc: 'Marketplace login:pw:email:email_pw format' },
960
+ { flag: '(price)', desc: 'Free — local vault only' },
961
+ ],
962
+ list: [
963
+ { flag: '(no args)', desc: 'List all local TikTok accounts' },
964
+ { flag: '(price)', desc: 'Free' },
965
+ ],
966
+ info: [{ flag: '<username>', desc: 'Show one account' }, { flag: '(price)', desc: 'Free' }],
967
+ rename: [
968
+ { flag: '<old>', desc: 'Current local handle' },
969
+ { flag: '--to <new>', desc: 'New handle' },
970
+ { flag: '(price)', desc: 'Free — local-only metadata update' },
971
+ ],
972
+ remove: [
973
+ { flag: '<username>', desc: 'Account to delete from local vault' },
974
+ { flag: '--confirm', desc: 'Required' },
975
+ { flag: '(price)', desc: 'Free' },
976
+ ],
977
+ totp: [{ flag: '<username>', desc: 'Print current TOTP code' }, { flag: '(price)', desc: 'Free' }],
978
+ login: [
979
+ { flag: '<username>', desc: 'Validate cookies and cache the session' },
980
+ { flag: '(price)', desc: '$0.005 USDC' },
981
+ ],
982
+ session: [{ flag: '<username>', desc: 'Check cached session' }, { flag: '(price)', desc: 'Free' }],
983
+ post: [
984
+ { flag: '<username>', desc: 'Account to post from' },
985
+ { flag: '--file video.mp4', desc: 'Video file' },
986
+ { flag: '--caption "..."', desc: 'Caption' },
987
+ { flag: '(price)', desc: '$0.001 USDC' },
988
+ ],
989
+ follow: [
990
+ { flag: '<username>', desc: 'Account doing the follow' },
991
+ { flag: '--user @handle', desc: 'User to follow' },
992
+ { flag: '(price)', desc: '$0.001 USDC' },
993
+ ],
994
+ like: [
995
+ { flag: '<username>', desc: 'Account doing the like' },
996
+ { flag: '--video <url>', desc: 'Video URL to like' },
997
+ { flag: '(price)', desc: '$0.001 USDC' },
998
+ ],
999
+ delete: [
1000
+ { flag: '<username>', desc: 'Account that posted the video' },
1001
+ { flag: '--video <url>', desc: 'Video to delete' },
1002
+ { flag: '(price)', desc: '$0.001 USDC' },
1003
+ ],
1004
+ bio: [
1005
+ { flag: '<username>', desc: 'Account whose bio to update' },
1006
+ { flag: '--text "..."', desc: 'New bio (<=80 chars)' },
1007
+ { flag: '(price)', desc: '$0.001 USDC' },
1008
+ ],
1009
+ name: [
1010
+ { flag: '<username>', desc: 'Account whose display name to update' },
1011
+ { flag: '--display "..."', desc: 'New display name (<=30 chars)' },
1012
+ { flag: '(price)', desc: '$0.001 USDC' },
1013
+ ],
1014
+ pfp: [
1015
+ { flag: '<username>', desc: 'Account whose avatar to update' },
1016
+ { flag: '--file pic.png', desc: 'Image file' },
1017
+ { flag: '(price)', desc: '$0.005 USDC' },
1018
+ ],
1019
+ };
736
1020
  /**
737
1021
  * Render a per-command menu (no subcommand given). On a TTY → Ink MenuScreen
738
1022
  * with the Palmyr aesthetic. In agent mode → flat JSON listing the available
@@ -1042,6 +1326,7 @@ async function main() {
1042
1326
  { name: 'release', description: 'Release a phone number', hint: '--id PHONE_ID' },
1043
1327
  { name: 'sms', description: 'Send an SMS', hint: '--id ID --to +1... --body "hi"' },
1044
1328
  { name: 'messages', description: 'Read SMS messages received on a number', hint: '--id PHONE_ID' },
1329
+ { name: 'message', description: 'Get one SMS message by id (incl. delivery status)', hint: '--id MESSAGE_ID' },
1045
1330
  { name: 'call', description: 'Place a voice call', hint: '--id ID --to +1... --tts "hello"' },
1046
1331
  { name: 'calls', description: 'List calls placed/received on a number', hint: '--id PHONE_ID' },
1047
1332
  { name: 'call-info', description: 'Get details on a single call', hint: '--call CALL_CONTROL_ID' },
@@ -1149,6 +1434,18 @@ async function main() {
1149
1434
  const data = await ao.phoneMessages(id);
1150
1435
  return print(data);
1151
1436
  }
1437
+ case 'message':
1438
+ case 'sms-status': {
1439
+ // Per-message readback. Mirrors `phone call-info` for SMS:
1440
+ // the Telnyx webhook updates delivery_status on the row, and this
1441
+ // endpoint serves it back. Useful when the immediate sms response
1442
+ // is 'queued' and the caller wants to confirm delivery.
1443
+ const messageId = flags.id || flags.message || positional[0];
1444
+ if (!messageId)
1445
+ err('Usage: palmyr phone message <message-id>');
1446
+ const data = await ao.phoneMessage(messageId);
1447
+ return print(data);
1448
+ }
1152
1449
  case 'calls': {
1153
1450
  const id = flags.id || positional[0];
1154
1451
  if (!id)
@@ -1287,13 +1584,58 @@ async function main() {
1287
1584
  switch (subcommand) {
1288
1585
  case 'create': {
1289
1586
  const name = flags.name || positional[0];
1290
- const wallet = flags.wallet;
1587
+ const walletInput = flags.wallet;
1291
1588
  const domain = flags.domain;
1292
1589
  if (!name)
1293
1590
  err('--name required (e.g. palmyr email create --name hello [--domain example.com])');
1591
+ // --wallet accepts three forms: vault id, vault name, or a raw
1592
+ // Solana base58 pubkey. The server only accepts a Solana pubkey
1593
+ // (E2E encryption is Ed25519→X25519), so resolve id/name to a
1594
+ // pubkey here before the request. Raw pubkeys pass through.
1595
+ // Doing this resolution client-side also means a Base-paying user
1596
+ // doesn't need a Solana wallet — their vault already has one
1597
+ // (single mnemonic, both chains), and we can find it without
1598
+ // making the server reach back for client-side keys.
1599
+ let walletAddress;
1600
+ if (walletInput) {
1601
+ const looksLikeSolPubkey = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(walletInput);
1602
+ if (looksLikeSolPubkey) {
1603
+ walletAddress = walletInput;
1604
+ }
1605
+ else {
1606
+ const { listVaultWallets } = await import('./vault.js');
1607
+ const wallets = listVaultWallets();
1608
+ const match = wallets.find(w => w.id === walletInput || w.name === walletInput);
1609
+ if (!match)
1610
+ err(`--wallet "${walletInput}" did not match any vault id, name, or look like a Solana pubkey`);
1611
+ if (!match.solanaAddress)
1612
+ err(`Wallet "${walletInput}" has no Solana address — email inboxes require one (E2E uses Ed25519). Re-create with: palmyr wallet create`);
1613
+ walletAddress = match.solanaAddress;
1614
+ }
1615
+ }
1616
+ else {
1617
+ // No --wallet: the server would normally default the inbox owner
1618
+ // to the x402 payer. That works for Solana-paid calls but
1619
+ // 400s on Base because the payer is an EVM address. Auto-fill
1620
+ // the *paying wallet's* Solana address here so a single
1621
+ // mnemonic-derived vault wallet works on either pay chain.
1622
+ const cfg = loadConfig();
1623
+ const payChain = (cfg.defaultPayChain || 'solana');
1624
+ if (payChain === 'base') {
1625
+ const { listVaultWallets } = await import('./vault.js');
1626
+ const wallets = listVaultWallets();
1627
+ const targetId = cfg.defaultPayWalletId || process.env.PALMYR_PAY_WALLET;
1628
+ const paying = (targetId && wallets.find(w => w.id === targetId)) || wallets.find(w => w.evmAddress && w.solanaAddress);
1629
+ if (paying?.solanaAddress)
1630
+ walletAddress = paying.solanaAddress;
1631
+ // If no Solana address is reachable, fall through and let the
1632
+ // server return its actionable 400 — silent failure would be
1633
+ // worse than a clear error.
1634
+ }
1635
+ }
1294
1636
  const spin = new Spinner();
1295
1637
  spin.start('Creating inbox...');
1296
- const data = await ao.emailCreate(name, wallet, domain);
1638
+ const data = await ao.emailCreate(name, walletAddress, domain);
1297
1639
  spin.stop('Inbox created', true);
1298
1640
  return print(data);
1299
1641
  }
@@ -1589,6 +1931,26 @@ async function main() {
1589
1931
  const localKeyPath = generatedKeyMeta?.privateKeyPath
1590
1932
  || (pubkeyFile ? pubkeyFile.replace(/\.pub$/, '').replace('~', homedir()) : undefined)
1591
1933
  || (explicitKeyPath ? explicitKeyPath.replace('~', homedir()) : undefined);
1934
+ // --ssh-key <id> uploads a server-side key but doesn't tell us where
1935
+ // the matching private key lives on this machine. Without
1936
+ // --key-path, the SSH readiness gate later silently skips and the
1937
+ // deploy reports `ssh: skipped` while still marking the server as
1938
+ // "ready" — a real foot-gun (dogfood report 2026-05-25 hit this
1939
+ // exact path). Be loud about it now, before anyone pays.
1940
+ if (sshKeyIds && !localKeyPath) {
1941
+ const msg = 'Warning: --ssh-key <id> uploaded the public key server-side, ' +
1942
+ 'but no matching private key was passed to this CLI. ' +
1943
+ 'SSH readiness cannot be verified locally — `compute wait` and the ' +
1944
+ 'inline --wait check will skip the SSH gate. Pass ' +
1945
+ '`--key-path /path/to/private_key` (or `--private-key`) to ' +
1946
+ 'enable the verification.';
1947
+ if (AGENT_MODE) {
1948
+ process.stderr.write(JSON.stringify({ event: 'warning', code: 'ssh_key_no_local_key', message: msg }) + '\n');
1949
+ }
1950
+ else {
1951
+ process.stderr.write(`\n${msg}\n\n`);
1952
+ }
1953
+ }
1592
1954
  // Progress events to stderr — default ON in agent mode so a
1593
1955
  // long deploy isn't a 10-minute silence. Pass --no-progress to
1594
1956
  // opt out. Stdout still gets one final JSON object, so jq
@@ -2169,7 +2531,7 @@ async function main() {
2169
2531
  subtitle: 'Non-custodial HD wallet',
2170
2532
  footerLeft: 'Solana + Base wallet operations',
2171
2533
  commands: [
2172
- { name: 'create', description: 'Create one or many wallets', hint: '[--tag X --count 100] [--solana|--base] [--managed]' },
2534
+ { name: 'create', description: 'Create one or many wallets', hint: '[--tag X --count 100] [--solana|--base]' },
2173
2535
  { name: 'import', description: 'Import from mnemonic', hint: '--mnemonic "..." [--tag X]' },
2174
2536
  { name: 'list', description: 'List all wallets', hint: '[--tag <name>]' },
2175
2537
  { name: 'info', description: 'Wallet details', hint: 'WALLET_ID' },
@@ -2183,7 +2545,6 @@ async function main() {
2183
2545
  { name: 'api-key', description: 'Create agent API key', hint: 'WALLET_ID --name my-agent' },
2184
2546
  { name: 'config', description: 'Get agent config', hint: 'WALLET_ID' },
2185
2547
  { name: 'use', description: 'Set default pay wallet', hint: 'WALLET_ID' },
2186
- { name: 'request-approval', description: 'Request human approval (managed)', hint: 'WALLET_ID --action limits --daily 100' },
2187
2548
  { name: 'buy', description: 'Open a trading position', hint: 'solana <CA> --amount 0.5sol --thesis "..."' },
2188
2549
  { name: 'cohort', description: 'Split a buy across N derived wallets with jitter (Phase 4c)', hint: 'buy <CHAIN> <CA> --total ... --split N' },
2189
2550
  { name: 'template', description: 'Manage YAML strategy templates', hint: 'list | show <name> | path <name> | delete <name>' },
@@ -4978,7 +5339,12 @@ async function main() {
4978
5339
  log(`auto-imported @${username} from server (${sourceLabel} → local vault)`);
4979
5340
  return summary;
4980
5341
  };
4981
- if (!subcommand) {
5342
+ // Help guard. `palmyr twitter buy --help` MUST never dispatch to the
5343
+ // paid `case 'buy'` below — 1.8.3 had no guard here and a real user
5344
+ // got charged $5 for a help command. Falls back to the top-level menu
5345
+ // when the subcommand has no per-subcommand help entry, so even an
5346
+ // unrecognized `palmyr twitter <whatever> --help` is safe to run.
5347
+ if (!subcommand || (flags.help && !TWITTER_HELP[subcommand])) {
4982
5348
  showMenu({
4983
5349
  command: 'twitter',
4984
5350
  title: 'twitter',
@@ -4994,7 +5360,10 @@ async function main() {
4994
5360
  { name: 'buy', description: 'Purchase an aged account (requires server supplier config)', hint: '--age 1y --country US' },
4995
5361
  { name: 'login', description: 'Force a fresh server-side session (requires browser runtime)', hint: '<username>' },
4996
5362
  { name: 'post', description: 'Post a tweet (requires server browser runtime)', hint: '<username> --body "..."' },
4997
- { name: 'status', description: 'Check if the account is alive / shadow-banned', hint: '<username>' },
5363
+ // `status` is not wired yet (Phase 3). Hidden from this menu so
5364
+ // users don't try a command that will only error; `session`
5365
+ // covers the most useful subset (cached server-side login state).
5366
+ { name: 'session', description: 'Inspect cached server-side session for an account', hint: '<username>' },
4998
5367
  { name: 'transfer', description: 'Hand an account to another wallet (rotates password; auto-registers if needed)', hint: '<username> --to <wallet> --confirm' },
4999
5368
  { name: 'share', description: 'Grant another wallet shared access', hint: '<username> --with <wallet>' },
5000
5369
  { name: 'unshare', description: 'Revoke a wallet’s shared access', hint: '<username> --from <wallet> [--rotate]' },
@@ -5004,6 +5373,10 @@ async function main() {
5004
5373
  });
5005
5374
  return;
5006
5375
  }
5376
+ if (flags.help && subcommand && TWITTER_HELP[subcommand]) {
5377
+ subcommandHelp('twitter', subcommand, TWITTER_HELP[subcommand]);
5378
+ return;
5379
+ }
5007
5380
  switch (subcommand) {
5008
5381
  case 'import': {
5009
5382
  // Option 1: --credentials-line "login:password:email:email_pw:2fa:ct0:auth_token"
@@ -5941,14 +6314,22 @@ async function main() {
5941
6314
  return print(data);
5942
6315
  }
5943
6316
  case 'status': {
5944
- err(`twitter status: not wired yet. Phase 3 will add it.`, EXIT.GENERAL);
6317
+ // `twitter status` was meant to check live shadow-ban / suspension
6318
+ // state via a server-side probe. That's a Phase 3 build (needs a
6319
+ // browser runtime on the server to render the profile). Until
6320
+ // then, point users at `session` for the cached login state and
6321
+ // `info` for vault metadata — together they cover the practical
6322
+ // "is this account still usable from the agent's side" question.
6323
+ err(`twitter status: not wired yet (Phase 3). Closest equivalents available today: ` +
6324
+ `\`palmyr twitter session <username>\` (cached server-side login validity) and ` +
6325
+ `\`palmyr twitter info <username>\` (local vault record).`, EXIT.GENERAL);
5945
6326
  }
5946
6327
  case 'transfer': {
5947
6328
  // Hand the X account to another wallet. End-to-end one-command:
5948
6329
  // 1. If the account is only in the local vault, auto-register it
5949
6330
  // with the server (uploads encrypted creds; $0.01 USDC).
5950
6331
  // 2. Server rotates the password and revokes other sessions
5951
- // ($0.0001 USDC ownership proof).
6332
+ // ($0.01 USDC ownership proof).
5952
6333
  // 3. Atomically flips ownership in the DB.
5953
6334
  // Receiver picks up the rotated credentials via `palmyr twitter
5954
6335
  // list` (which now surfaces server-side accounts) and/or `claim`.
@@ -6320,7 +6701,9 @@ async function main() {
6320
6701
  case 'tiktok': {
6321
6702
  const sv = await import('./social-vault.js');
6322
6703
  const platform = 'tiktok';
6323
- if (!subcommand) {
6704
+ // Same help guard as `twitter` — prevents `--help` from dispatching
6705
+ // a paid subcommand. Same bug class lived here too in 1.8.3.
6706
+ if (!subcommand || (flags.help && !TIKTOK_HELP[subcommand])) {
6324
6707
  showMenu({
6325
6708
  command: 'tiktok',
6326
6709
  title: 'tiktok',
@@ -6347,6 +6730,10 @@ async function main() {
6347
6730
  });
6348
6731
  return;
6349
6732
  }
6733
+ if (flags.help && subcommand && TIKTOK_HELP[subcommand]) {
6734
+ subcommandHelp('tiktok', subcommand, TIKTOK_HELP[subcommand]);
6735
+ return;
6736
+ }
6350
6737
  switch (subcommand) {
6351
6738
  case 'import': {
6352
6739
  // Two formats: