@palmyr/cli 1.8.2 → 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/README.md +32 -27
- package/dist/cli.js +610 -47
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +13 -1
- package/dist/config.js +15 -1
- package/dist/config.js.map +1 -1
- package/dist/passphrase-prompt.d.ts +4 -1
- package/dist/passphrase-prompt.js +5 -2
- package/dist/passphrase-prompt.js.map +1 -1
- package/dist/sdk.d.ts +1 -0
- package/dist/sdk.js +14 -1
- package/dist/sdk.js.map +1 -1
- package/dist/vault.d.ts +12 -6
- package/dist/vault.js +13 -7
- package/dist/vault.js.map +1 -1
- package/dist/wallet-live-test.js +23 -8
- package/dist/wallet-live-test.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -209,26 +209,42 @@ function subcommandHelp(command, subcommand, options) {
|
|
|
209
209
|
}
|
|
210
210
|
console.log();
|
|
211
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Loud, single-shot warning printed when `--session-only` was chosen. Goes to
|
|
214
|
+
* stderr so JSON on stdout stays clean. Caller supplies the write fn so we
|
|
215
|
+
* route through the right stream in agent vs TTY mode.
|
|
216
|
+
*
|
|
217
|
+
* Why this is here: 1.8.2 and earlier defaulted to session-only without
|
|
218
|
+
* warning. A user lost three wallets to a routine keyring change on a
|
|
219
|
+
* headless box because the JSON file alone is mathematically useless without
|
|
220
|
+
* the keychain secret. 1.8.3 makes the choice explicit; this warning is the
|
|
221
|
+
* reminder for anyone who picks the foot-gun anyway.
|
|
222
|
+
*/
|
|
223
|
+
function emitSessionOnlyWarning(write) {
|
|
224
|
+
write(`\n ${t.warn}⚠ session-only wallet — NOT recoverable from the JSON file alone.${t.reset}\n`);
|
|
225
|
+
write(` Reboot, OS-keychain password change, or host copy permanently breaks decryption.\n`);
|
|
226
|
+
write(` Back up the mnemonic externally, or run \`palmyr wallet rekey <id> --passphrase <p>\` later.\n\n`);
|
|
227
|
+
}
|
|
212
228
|
// ─── Subcommand help definitions ───
|
|
213
229
|
const WALLET_HELP = {
|
|
214
230
|
create: [
|
|
215
231
|
{ flag: '--name <name>', desc: 'Wallet name', hint: 'default: "My Wallet"' },
|
|
216
|
-
{ flag: '--managed', desc: 'Create managed wallet with human oversight via passkey (single-create only)' },
|
|
217
232
|
{ flag: '--solana', desc: 'Materialize the Solana account only', hint: 'default: both chains' },
|
|
218
233
|
{ flag: '--base', desc: 'Materialize the Base/EVM account only', hint: 'pair with --solana for both (default)' },
|
|
219
234
|
{ 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: '
|
|
235
|
+
{ flag: '--count <N>', desc: 'Bulk-create N wallets in one call (1-500)', hint: 'requires --tag' },
|
|
221
236
|
{ flag: '--name-prefix <p>', desc: 'Bulk name prefix; suffixed `-001..-N`', hint: 'default: same as --tag' },
|
|
222
|
-
{ flag: '--passphrase <p>', desc: '
|
|
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.' },
|
|
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' },
|
|
223
239
|
],
|
|
224
240
|
import: [
|
|
225
241
|
{ flag: '--mnemonic <words>', desc: 'BIP-39 mnemonic phrase (required)' },
|
|
226
242
|
{ flag: '--name <name>', desc: 'Wallet name', hint: 'default: "Imported Wallet"' },
|
|
227
|
-
{ flag: '--managed', desc: 'Import as managed wallet' },
|
|
228
243
|
{ flag: '--solana', desc: 'Materialize the Solana account only' },
|
|
229
244
|
{ flag: '--base', desc: 'Materialize the Base/EVM account only' },
|
|
230
245
|
{ flag: '--tag <name>', desc: 'Assign a tag at import time' },
|
|
231
|
-
{ flag: '--passphrase <p>', desc: '
|
|
246
|
+
{ flag: '--passphrase <p>', desc: 'Seal the mnemonic with this passphrase (≥8 chars) for durable recovery', hint: 'or PALMYR_WALLET_PASSPHRASE env (env preferred). Interactive prompt on TTY when neither set.' },
|
|
247
|
+
{ flag: '--session-only', desc: 'OPT OUT of the passphrase fallback. Wallet is bound to this machine\'s OS keychain.', hint: 'use only for ephemeral / throwaway wallets' },
|
|
232
248
|
],
|
|
233
249
|
rekey: [
|
|
234
250
|
{ flag: '<WALLET_ID>', desc: 'Wallet ID or name (positional or --id)' },
|
|
@@ -431,6 +447,15 @@ const PHONE_HELP = {
|
|
|
431
447
|
{ flag: '(price)', desc: '$0.02 per call' },
|
|
432
448
|
{ flag: '(example)', desc: 'palmyr phone messages --id PN_abc' },
|
|
433
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
|
+
],
|
|
434
459
|
calls: [
|
|
435
460
|
{ flag: '--id <PHONE_ID>', desc: 'Phone number id to list calls for (required; positional also accepted)' },
|
|
436
461
|
{ flag: '(price)', desc: '$0.02 per call' },
|
|
@@ -509,7 +534,7 @@ const EMAIL_HELP = {
|
|
|
509
534
|
create: [
|
|
510
535
|
{ flag: '--name <name>', desc: 'Inbox name (required)' },
|
|
511
536
|
{ flag: '--domain <domain>', desc: 'Wallet-owned domain to host the inbox on (optional)', hint: 'default: Palmyr-hosted domain' },
|
|
512
|
-
{ flag: '--wallet <id|name>', desc: '
|
|
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' },
|
|
513
538
|
{ flag: '(price)', desc: '$2.00 per inbox provisioned' },
|
|
514
539
|
{ flag: '(example)', desc: 'palmyr email create --name agent --domain example.com' },
|
|
515
540
|
],
|
|
@@ -715,6 +740,283 @@ const CHAT_HELP = {
|
|
|
715
740
|
{ flag: '(example)', desc: 'palmyr chat providers --capability web_search' },
|
|
716
741
|
],
|
|
717
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
|
+
};
|
|
718
1020
|
/**
|
|
719
1021
|
* Render a per-command menu (no subcommand given). On a TTY → Ink MenuScreen
|
|
720
1022
|
* with the Palmyr aesthetic. In agent mode → flat JSON listing the available
|
|
@@ -1024,6 +1326,7 @@ async function main() {
|
|
|
1024
1326
|
{ name: 'release', description: 'Release a phone number', hint: '--id PHONE_ID' },
|
|
1025
1327
|
{ name: 'sms', description: 'Send an SMS', hint: '--id ID --to +1... --body "hi"' },
|
|
1026
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' },
|
|
1027
1330
|
{ name: 'call', description: 'Place a voice call', hint: '--id ID --to +1... --tts "hello"' },
|
|
1028
1331
|
{ name: 'calls', description: 'List calls placed/received on a number', hint: '--id PHONE_ID' },
|
|
1029
1332
|
{ name: 'call-info', description: 'Get details on a single call', hint: '--call CALL_CONTROL_ID' },
|
|
@@ -1131,6 +1434,18 @@ async function main() {
|
|
|
1131
1434
|
const data = await ao.phoneMessages(id);
|
|
1132
1435
|
return print(data);
|
|
1133
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
|
+
}
|
|
1134
1449
|
case 'calls': {
|
|
1135
1450
|
const id = flags.id || positional[0];
|
|
1136
1451
|
if (!id)
|
|
@@ -1269,13 +1584,58 @@ async function main() {
|
|
|
1269
1584
|
switch (subcommand) {
|
|
1270
1585
|
case 'create': {
|
|
1271
1586
|
const name = flags.name || positional[0];
|
|
1272
|
-
const
|
|
1587
|
+
const walletInput = flags.wallet;
|
|
1273
1588
|
const domain = flags.domain;
|
|
1274
1589
|
if (!name)
|
|
1275
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
|
+
}
|
|
1276
1636
|
const spin = new Spinner();
|
|
1277
1637
|
spin.start('Creating inbox...');
|
|
1278
|
-
const data = await ao.emailCreate(name,
|
|
1638
|
+
const data = await ao.emailCreate(name, walletAddress, domain);
|
|
1279
1639
|
spin.stop('Inbox created', true);
|
|
1280
1640
|
return print(data);
|
|
1281
1641
|
}
|
|
@@ -1571,6 +1931,26 @@ async function main() {
|
|
|
1571
1931
|
const localKeyPath = generatedKeyMeta?.privateKeyPath
|
|
1572
1932
|
|| (pubkeyFile ? pubkeyFile.replace(/\.pub$/, '').replace('~', homedir()) : undefined)
|
|
1573
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
|
+
}
|
|
1574
1954
|
// Progress events to stderr — default ON in agent mode so a
|
|
1575
1955
|
// long deploy isn't a 10-minute silence. Pass --no-progress to
|
|
1576
1956
|
// opt out. Stdout still gets one final JSON object, so jq
|
|
@@ -2151,7 +2531,7 @@ async function main() {
|
|
|
2151
2531
|
subtitle: 'Non-custodial HD wallet',
|
|
2152
2532
|
footerLeft: 'Solana + Base wallet operations',
|
|
2153
2533
|
commands: [
|
|
2154
|
-
{ name: 'create', description: 'Create one or many wallets', hint: '[--tag X --count 100] [--solana|--base]
|
|
2534
|
+
{ name: 'create', description: 'Create one or many wallets', hint: '[--tag X --count 100] [--solana|--base]' },
|
|
2155
2535
|
{ name: 'import', description: 'Import from mnemonic', hint: '--mnemonic "..." [--tag X]' },
|
|
2156
2536
|
{ name: 'list', description: 'List all wallets', hint: '[--tag <name>]' },
|
|
2157
2537
|
{ name: 'info', description: 'Wallet details', hint: 'WALLET_ID' },
|
|
@@ -2165,7 +2545,6 @@ async function main() {
|
|
|
2165
2545
|
{ name: 'api-key', description: 'Create agent API key', hint: 'WALLET_ID --name my-agent' },
|
|
2166
2546
|
{ name: 'config', description: 'Get agent config', hint: 'WALLET_ID' },
|
|
2167
2547
|
{ name: 'use', description: 'Set default pay wallet', hint: 'WALLET_ID' },
|
|
2168
|
-
{ name: 'request-approval', description: 'Request human approval (managed)', hint: 'WALLET_ID --action limits --daily 100' },
|
|
2169
2548
|
{ name: 'buy', description: 'Open a trading position', hint: 'solana <CA> --amount 0.5sol --thesis "..."' },
|
|
2170
2549
|
{ name: 'cohort', description: 'Split a buy across N derived wallets with jitter (Phase 4c)', hint: 'buy <CHAIN> <CA> --total ... --split N' },
|
|
2171
2550
|
{ name: 'template', description: 'Manage YAML strategy templates', hint: 'list | show <name> | path <name> | delete <name>' },
|
|
@@ -2210,10 +2589,37 @@ async function main() {
|
|
|
2210
2589
|
const chains = (wantSol && !wantBase) ? ['solana']
|
|
2211
2590
|
: (wantBase && !wantSol) ? ['base']
|
|
2212
2591
|
: ['solana', 'base'];
|
|
2213
|
-
//
|
|
2214
|
-
//
|
|
2215
|
-
//
|
|
2216
|
-
|
|
2592
|
+
// Passphrase resolution — recoverable-by-default.
|
|
2593
|
+
// Three paths:
|
|
2594
|
+
// 1. `--passphrase <p>` or `PALMYR_WALLET_PASSPHRASE` env → seal with scrypt
|
|
2595
|
+
// 2. `--session-only` → explicit opt-out, OS-keychain-only (warned)
|
|
2596
|
+
// 3. nothing + TTY → interactive prompt
|
|
2597
|
+
// 4. nothing + non-TTY → error with the three options
|
|
2598
|
+
// Session-only wallets are recoverable ONLY from this machine's OS
|
|
2599
|
+
// keychain; reboot / keyring change / host migration breaks them
|
|
2600
|
+
// permanently. We pushed agents into that foot-gun in 1.8.2 and
|
|
2601
|
+
// earlier; 1.8.3 forces the choice up front.
|
|
2602
|
+
const sessionOnly = !!flags['session-only'];
|
|
2603
|
+
let passphrase = flags.passphrase || process.env.PALMYR_WALLET_PASSPHRASE || undefined;
|
|
2604
|
+
if (passphrase && sessionOnly) {
|
|
2605
|
+
err('Pass either --passphrase / PALMYR_WALLET_PASSPHRASE OR --session-only, not both.', EXIT.BAD_INPUT);
|
|
2606
|
+
}
|
|
2607
|
+
if (!passphrase && !sessionOnly) {
|
|
2608
|
+
if (process.stdin.isTTY) {
|
|
2609
|
+
const { promptNewPassphrase } = await import('./passphrase-prompt.js');
|
|
2610
|
+
if (!AGENT_MODE)
|
|
2611
|
+
process.stderr.write('\n Wallet creation needs a passphrase fallback so the wallet survives a reboot / OS-keychain change / host migration.\n' +
|
|
2612
|
+
' (Re-run with --session-only to opt out — ephemeral wallets only.)\n\n');
|
|
2613
|
+
passphrase = await promptNewPassphrase('vault wallet');
|
|
2614
|
+
}
|
|
2615
|
+
else {
|
|
2616
|
+
err('Wallet creation requires a recoverable passphrase fallback OR an explicit opt-out:\n\n' +
|
|
2617
|
+
' PALMYR_WALLET_PASSPHRASE="<phrase>" palmyr wallet create [...] # recommended (env keeps phrase out of shell history)\n' +
|
|
2618
|
+
' palmyr wallet create --passphrase "<phrase>" [...] # equivalent\n' +
|
|
2619
|
+
' palmyr wallet create --session-only [...] # OPT OUT — wallet dies with this machine\'s OS keychain\n\n' +
|
|
2620
|
+
'Session-only wallets are NOT recoverable from the JSON file alone — reboot, keyring change, or host copy renders them unusable.', EXIT.BAD_INPUT);
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2217
2623
|
// ─── Bulk path ───
|
|
2218
2624
|
if (count > 1) {
|
|
2219
2625
|
if (isManaged)
|
|
@@ -2227,17 +2633,36 @@ async function main() {
|
|
|
2227
2633
|
const { storeSecretsBatch } = await import('./credential-store.js');
|
|
2228
2634
|
// Progress to stderr so JSON on stdout stays clean
|
|
2229
2635
|
if (!AGENT_MODE)
|
|
2230
|
-
process.stderr.write(`creating ${count} wallets under tag "${tagRaw}"${passphrase ? ' (+ passphrase fallback)' : ''}...\n`);
|
|
2636
|
+
process.stderr.write(`creating ${count} wallets under tag "${tagRaw}"${passphrase ? ' (+ passphrase fallback)' : ' (session-only)'}...\n`);
|
|
2231
2637
|
const results = createLocalWalletsBatch(prefix, count, 'unmanaged', { tag: tagRaw, chains, passphrase });
|
|
2232
2638
|
if (!AGENT_MODE)
|
|
2233
2639
|
process.stderr.write(`sealing ${count} session secrets in OS credential store...\n`);
|
|
2234
|
-
|
|
2235
|
-
|
|
2640
|
+
// Keychain failure is non-fatal IFF a passphrase fallback was
|
|
2641
|
+
// written — the wallets are still recoverable via the env var.
|
|
2642
|
+
let keychainStoreWarning = null;
|
|
2643
|
+
try {
|
|
2644
|
+
storeSecretsBatch(results.map(r => ({ account: r.id, secret: r.sessionSecret })));
|
|
2645
|
+
}
|
|
2646
|
+
catch (e) {
|
|
2647
|
+
if (passphrase) {
|
|
2648
|
+
keychainStoreWarning = e?.message || 'keychain store failed';
|
|
2649
|
+
if (!AGENT_MODE)
|
|
2650
|
+
process.stderr.write(` warning: OS keychain unavailable (${keychainStoreWarning}); wallets remain decryptable via PALMYR_WALLET_PASSPHRASE\n`);
|
|
2651
|
+
}
|
|
2652
|
+
else {
|
|
2653
|
+
throw e;
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
if (sessionOnly && !AGENT_MODE)
|
|
2657
|
+
emitSessionOnlyWarning(process.stderr.write.bind(process.stderr));
|
|
2658
|
+
log(`wallet create: ${count} wallets under tag "${tagRaw}" (chains=${chains.join(',')}, mode=${passphrase ? 'passphrase' : 'session-only'}${keychainStoreWarning ? ', keychain=failed' : ''})`);
|
|
2236
2659
|
if (AGENT_MODE) {
|
|
2237
2660
|
print({
|
|
2238
2661
|
count: results.length,
|
|
2239
2662
|
tag: tagRaw,
|
|
2240
2663
|
chains,
|
|
2664
|
+
recoverable: !!passphrase,
|
|
2665
|
+
...(keychainStoreWarning ? { keychainWarning: keychainStoreWarning } : {}),
|
|
2241
2666
|
wallets: results.map(r => ({
|
|
2242
2667
|
id: r.id,
|
|
2243
2668
|
name: r.name,
|
|
@@ -2253,6 +2678,7 @@ async function main() {
|
|
|
2253
2678
|
console.log(`\n ${t.success}✔${t.reset} Created ${count} wallets under tag ${t.accent}${tagRaw}${t.reset}`);
|
|
2254
2679
|
console.log(` ${t.muted}chains:${t.reset} ${chains.join(', ')}`);
|
|
2255
2680
|
console.log(` ${t.muted}names: ${t.reset}${results[0].name} … ${results[results.length - 1].name}`);
|
|
2681
|
+
console.log(` ${t.muted}recoverable:${t.reset} ${passphrase ? 'yes (passphrase fallback set)' : 'NO — session-only'}`);
|
|
2256
2682
|
console.log(`\n ${t.muted}List them: ${t.reset}palmyr wallet list --tag ${tagRaw}`);
|
|
2257
2683
|
console.log(` ${t.muted}Delete all: ${t.reset}palmyr wallet tag-delete ${tagRaw} --confirm\n`);
|
|
2258
2684
|
}
|
|
@@ -2265,10 +2691,26 @@ async function main() {
|
|
|
2265
2691
|
// Create locally — no server needed for the key material
|
|
2266
2692
|
const { createLocalWallet } = await import('./vault.js');
|
|
2267
2693
|
const w = createLocalWallet(name, mode, { tag: tagRaw, chains, passphrase });
|
|
2268
|
-
// Store session secret in OS credential store
|
|
2694
|
+
// Store session secret in OS credential store. Keychain failure is
|
|
2695
|
+
// non-fatal when a passphrase fallback was written.
|
|
2269
2696
|
const { storeSecret } = await import('./credential-store.js');
|
|
2270
|
-
|
|
2271
|
-
|
|
2697
|
+
let keychainStoreWarning = null;
|
|
2698
|
+
try {
|
|
2699
|
+
storeSecret(w.id, w.sessionSecret);
|
|
2700
|
+
}
|
|
2701
|
+
catch (e) {
|
|
2702
|
+
if (passphrase) {
|
|
2703
|
+
keychainStoreWarning = e?.message || 'keychain store failed';
|
|
2704
|
+
if (!AGENT_MODE)
|
|
2705
|
+
process.stderr.write(` warning: OS keychain unavailable (${keychainStoreWarning}); wallet remains decryptable via PALMYR_WALLET_PASSPHRASE\n`);
|
|
2706
|
+
}
|
|
2707
|
+
else {
|
|
2708
|
+
throw e;
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
if (sessionOnly && !AGENT_MODE)
|
|
2712
|
+
emitSessionOnlyWarning(process.stderr.write.bind(process.stderr));
|
|
2713
|
+
log(`wallet create: ${w.id} (${mode}${tagRaw ? `, tag=${tagRaw}` : ''}, chains=${chains.join(',')}, mode=${passphrase ? 'passphrase' : 'session-only'}${keychainStoreWarning ? ', keychain=failed' : ''})`);
|
|
2272
2714
|
// For managed wallets, register metadata with the server to get a setup link
|
|
2273
2715
|
let setupLink;
|
|
2274
2716
|
if (isManaged) {
|
|
@@ -2312,7 +2754,7 @@ async function main() {
|
|
|
2312
2754
|
}
|
|
2313
2755
|
}
|
|
2314
2756
|
else {
|
|
2315
|
-
print({ ...w, setupLink });
|
|
2757
|
+
print({ ...w, setupLink, recoverable: !!passphrase, ...(keychainStoreWarning ? { keychainWarning: keychainStoreWarning } : {}) });
|
|
2316
2758
|
}
|
|
2317
2759
|
break;
|
|
2318
2760
|
}
|
|
@@ -2328,13 +2770,52 @@ async function main() {
|
|
|
2328
2770
|
const chains = (wantSol && !wantBase) ? ['solana']
|
|
2329
2771
|
: (wantBase && !wantSol) ? ['base']
|
|
2330
2772
|
: ['solana', 'base'];
|
|
2331
|
-
|
|
2773
|
+
// Same recoverability gate as `create`. Import is even more
|
|
2774
|
+
// commonly run on a "new machine" after losing access on the
|
|
2775
|
+
// original — going session-only here would re-trap the user in
|
|
2776
|
+
// the same hole they're recovering from.
|
|
2777
|
+
const importSessionOnly = !!flags['session-only'];
|
|
2778
|
+
let importPassphrase = flags.passphrase || process.env.PALMYR_WALLET_PASSPHRASE || undefined;
|
|
2779
|
+
if (importPassphrase && importSessionOnly) {
|
|
2780
|
+
err('Pass either --passphrase / PALMYR_WALLET_PASSPHRASE OR --session-only, not both.', EXIT.BAD_INPUT);
|
|
2781
|
+
}
|
|
2782
|
+
if (!importPassphrase && !importSessionOnly) {
|
|
2783
|
+
if (process.stdin.isTTY) {
|
|
2784
|
+
const { promptNewPassphrase } = await import('./passphrase-prompt.js');
|
|
2785
|
+
if (!AGENT_MODE)
|
|
2786
|
+
process.stderr.write('\n Import needs a passphrase fallback so the wallet survives a reboot / OS-keychain change / host migration.\n' +
|
|
2787
|
+
' (Re-run with --session-only to opt out — ephemeral wallets only.)\n\n');
|
|
2788
|
+
importPassphrase = await promptNewPassphrase('vault wallet');
|
|
2789
|
+
}
|
|
2790
|
+
else {
|
|
2791
|
+
err('Wallet import requires a recoverable passphrase fallback OR an explicit opt-out:\n\n' +
|
|
2792
|
+
' PALMYR_WALLET_PASSPHRASE="<phrase>" palmyr wallet import --mnemonic "..." # recommended\n' +
|
|
2793
|
+
' palmyr wallet import --mnemonic "..." --passphrase "<phrase>" # equivalent\n' +
|
|
2794
|
+
' palmyr wallet import --mnemonic "..." --session-only # OPT OUT — wallet dies with this machine\'s OS keychain', EXIT.BAD_INPUT);
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2332
2797
|
const { importLocalWallet } = await import('./vault.js');
|
|
2333
|
-
const w = importLocalWallet(name, mnemonic, mode, { tag: tagRaw, chains, passphrase });
|
|
2334
|
-
// Store session secret
|
|
2798
|
+
const w = importLocalWallet(name, mnemonic, mode, { tag: tagRaw, chains, passphrase: importPassphrase });
|
|
2799
|
+
// Store session secret. Keychain failure is non-fatal when a
|
|
2800
|
+
// passphrase fallback was written.
|
|
2335
2801
|
const { storeSecret } = await import('./credential-store.js');
|
|
2336
|
-
|
|
2337
|
-
|
|
2802
|
+
let importKeychainWarning = null;
|
|
2803
|
+
try {
|
|
2804
|
+
storeSecret(w.id, w.sessionSecret);
|
|
2805
|
+
}
|
|
2806
|
+
catch (e) {
|
|
2807
|
+
if (importPassphrase) {
|
|
2808
|
+
importKeychainWarning = e?.message || 'keychain store failed';
|
|
2809
|
+
if (!AGENT_MODE)
|
|
2810
|
+
process.stderr.write(` warning: OS keychain unavailable (${importKeychainWarning}); wallet remains decryptable via PALMYR_WALLET_PASSPHRASE\n`);
|
|
2811
|
+
}
|
|
2812
|
+
else {
|
|
2813
|
+
throw e;
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
if (importSessionOnly && !AGENT_MODE)
|
|
2817
|
+
emitSessionOnlyWarning(process.stderr.write.bind(process.stderr));
|
|
2818
|
+
log(`wallet import: ${w.id} (mode=${importPassphrase ? 'passphrase' : 'session-only'}${importKeychainWarning ? ', keychain=failed' : ''})`);
|
|
2338
2819
|
if (!AGENT_MODE) {
|
|
2339
2820
|
render(React.createElement(WalletCreateScreen, {
|
|
2340
2821
|
version: VERSION,
|
|
@@ -2347,7 +2828,7 @@ async function main() {
|
|
|
2347
2828
|
}));
|
|
2348
2829
|
}
|
|
2349
2830
|
else {
|
|
2350
|
-
print(w);
|
|
2831
|
+
print({ ...w, recoverable: !!importPassphrase, ...(importKeychainWarning ? { keychainWarning: importKeychainWarning } : {}) });
|
|
2351
2832
|
}
|
|
2352
2833
|
break;
|
|
2353
2834
|
}
|
|
@@ -2517,9 +2998,13 @@ async function main() {
|
|
|
2517
2998
|
const msg = flags.msg || flags.message;
|
|
2518
2999
|
if (!chain || !msg)
|
|
2519
3000
|
err('--chain and --msg required');
|
|
2520
|
-
// Sign locally — no server needed
|
|
3001
|
+
// Sign locally — no server needed.
|
|
3002
|
+
// Read the same passphrase channel as pay / export / rekey so a
|
|
3003
|
+
// passphrase-backed wallet signs from any machine the env var
|
|
3004
|
+
// reaches (was missing in 1.8.2 — inconsistent with other commands).
|
|
3005
|
+
const signPass = flags.passphrase || process.env.PALMYR_WALLET_PASSPHRASE || undefined;
|
|
2521
3006
|
const { signMessageLocal } = await import('./vault.js');
|
|
2522
|
-
const data = signMessageLocal(walletId, chain, msg);
|
|
3007
|
+
const data = signMessageLocal(walletId, chain, msg, signPass);
|
|
2523
3008
|
return print({ success: true, ...data });
|
|
2524
3009
|
render(React.createElement(SuccessScreen, {
|
|
2525
3010
|
version: VERSION,
|
|
@@ -4854,7 +5339,12 @@ async function main() {
|
|
|
4854
5339
|
log(`auto-imported @${username} from server (${sourceLabel} → local vault)`);
|
|
4855
5340
|
return summary;
|
|
4856
5341
|
};
|
|
4857
|
-
|
|
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])) {
|
|
4858
5348
|
showMenu({
|
|
4859
5349
|
command: 'twitter',
|
|
4860
5350
|
title: 'twitter',
|
|
@@ -4870,7 +5360,10 @@ async function main() {
|
|
|
4870
5360
|
{ name: 'buy', description: 'Purchase an aged account (requires server supplier config)', hint: '--age 1y --country US' },
|
|
4871
5361
|
{ name: 'login', description: 'Force a fresh server-side session (requires browser runtime)', hint: '<username>' },
|
|
4872
5362
|
{ name: 'post', description: 'Post a tweet (requires server browser runtime)', hint: '<username> --body "..."' },
|
|
4873
|
-
|
|
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>' },
|
|
4874
5367
|
{ name: 'transfer', description: 'Hand an account to another wallet (rotates password; auto-registers if needed)', hint: '<username> --to <wallet> --confirm' },
|
|
4875
5368
|
{ name: 'share', description: 'Grant another wallet shared access', hint: '<username> --with <wallet>' },
|
|
4876
5369
|
{ name: 'unshare', description: 'Revoke a wallet’s shared access', hint: '<username> --from <wallet> [--rotate]' },
|
|
@@ -4880,6 +5373,10 @@ async function main() {
|
|
|
4880
5373
|
});
|
|
4881
5374
|
return;
|
|
4882
5375
|
}
|
|
5376
|
+
if (flags.help && subcommand && TWITTER_HELP[subcommand]) {
|
|
5377
|
+
subcommandHelp('twitter', subcommand, TWITTER_HELP[subcommand]);
|
|
5378
|
+
return;
|
|
5379
|
+
}
|
|
4883
5380
|
switch (subcommand) {
|
|
4884
5381
|
case 'import': {
|
|
4885
5382
|
// Option 1: --credentials-line "login:password:email:email_pw:2fa:ct0:auth_token"
|
|
@@ -5817,14 +6314,22 @@ async function main() {
|
|
|
5817
6314
|
return print(data);
|
|
5818
6315
|
}
|
|
5819
6316
|
case 'status': {
|
|
5820
|
-
|
|
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);
|
|
5821
6326
|
}
|
|
5822
6327
|
case 'transfer': {
|
|
5823
6328
|
// Hand the X account to another wallet. End-to-end one-command:
|
|
5824
6329
|
// 1. If the account is only in the local vault, auto-register it
|
|
5825
6330
|
// with the server (uploads encrypted creds; $0.01 USDC).
|
|
5826
6331
|
// 2. Server rotates the password and revokes other sessions
|
|
5827
|
-
// ($0.
|
|
6332
|
+
// ($0.01 USDC ownership proof).
|
|
5828
6333
|
// 3. Atomically flips ownership in the DB.
|
|
5829
6334
|
// Receiver picks up the rotated credentials via `palmyr twitter
|
|
5830
6335
|
// list` (which now surfaces server-side accounts) and/or `claim`.
|
|
@@ -6196,7 +6701,9 @@ async function main() {
|
|
|
6196
6701
|
case 'tiktok': {
|
|
6197
6702
|
const sv = await import('./social-vault.js');
|
|
6198
6703
|
const platform = 'tiktok';
|
|
6199
|
-
|
|
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])) {
|
|
6200
6707
|
showMenu({
|
|
6201
6708
|
command: 'tiktok',
|
|
6202
6709
|
title: 'tiktok',
|
|
@@ -6223,6 +6730,10 @@ async function main() {
|
|
|
6223
6730
|
});
|
|
6224
6731
|
return;
|
|
6225
6732
|
}
|
|
6733
|
+
if (flags.help && subcommand && TIKTOK_HELP[subcommand]) {
|
|
6734
|
+
subcommandHelp('tiktok', subcommand, TIKTOK_HELP[subcommand]);
|
|
6735
|
+
return;
|
|
6736
|
+
}
|
|
6226
6737
|
switch (subcommand) {
|
|
6227
6738
|
case 'import': {
|
|
6228
6739
|
// Two formats:
|
|
@@ -6575,12 +7086,18 @@ async function main() {
|
|
|
6575
7086
|
const { join } = await import('path');
|
|
6576
7087
|
const vaultDir = process.env.PALMYR_WALLET_PATH || join(homedir(), '.palmyr', 'wallet');
|
|
6577
7088
|
const { isCredentialStoreAvailable } = await import('./credential-store.js');
|
|
7089
|
+
const { hasLegacyKeyfileWallet } = await import('./config.js');
|
|
7090
|
+
// `defaultChain` is legacy keyfile-flow state — only show it when a
|
|
7091
|
+
// keyfile wallet is actually configured. The functional field is
|
|
7092
|
+
// `payChain` (renamed from the misleadingly-similar `defaultPayChain`
|
|
7093
|
+
// disk key), which the x402 pay path reads.
|
|
7094
|
+
const showLegacyChain = hasLegacyKeyfileWallet(cfg);
|
|
6578
7095
|
const configData = {
|
|
6579
7096
|
api: cfg.api,
|
|
6580
|
-
defaultChain: cfg.defaultChain,
|
|
6581
7097
|
setupDone: cfg.setupDone,
|
|
7098
|
+
...(showLegacyChain ? { legacyKeyfileChain: cfg.defaultChain || 'solana' } : {}),
|
|
6582
7099
|
defaultPayWalletId: cfg.defaultPayWalletId || null,
|
|
6583
|
-
|
|
7100
|
+
payChain: cfg.defaultPayChain || 'solana',
|
|
6584
7101
|
vaultPath: vaultDir,
|
|
6585
7102
|
credentialStore: isCredentialStoreAvailable() ? 'available' : 'unavailable',
|
|
6586
7103
|
configPath: join(homedir(), '.palmyr', 'config.json'),
|
|
@@ -6611,21 +7128,67 @@ async function main() {
|
|
|
6611
7128
|
const { listVaultWallets } = await import('./vault.js');
|
|
6612
7129
|
const wallets = listVaultWallets();
|
|
6613
7130
|
checks.push({ name: 'Local wallets', status: wallets.length > 0 ? 'pass' : 'warn', detail: `${wallets.length} wallet(s) found` });
|
|
6614
|
-
// 4.
|
|
7131
|
+
// 4. Decryption readiness for each wallet — tri-state.
|
|
7132
|
+
//
|
|
7133
|
+
// pass → wallet has keychain secret, OR has a passphrase fallback
|
|
7134
|
+
// AND PALMYR_WALLET_PASSPHRASE is set
|
|
7135
|
+
// warn → has passphrase fallback but env unset (recoverable, but
|
|
7136
|
+
// the next command will fail until env is set)
|
|
7137
|
+
// fail → session-only AND keychain secret is gone (unrecoverable
|
|
7138
|
+
// from this machine — needs mnemonic re-import or
|
|
7139
|
+
// rekey-on-original)
|
|
6615
7140
|
const { retrieveSecret } = await import('./credential-store.js');
|
|
6616
|
-
|
|
7141
|
+
const { hasPassphraseFallback } = await import('./vault.js');
|
|
7142
|
+
const envSet = !!process.env.PALMYR_WALLET_PASSPHRASE;
|
|
7143
|
+
let keychainOk = 0;
|
|
7144
|
+
let needsEnv = 0;
|
|
7145
|
+
let unrecoverable = [];
|
|
6617
7146
|
for (const w of wallets) {
|
|
6618
|
-
if (retrieveSecret(w.id))
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
7147
|
+
if (retrieveSecret(w.id)) {
|
|
7148
|
+
keychainOk++;
|
|
7149
|
+
continue;
|
|
7150
|
+
}
|
|
7151
|
+
let hasFallback = false;
|
|
7152
|
+
try {
|
|
7153
|
+
hasFallback = hasPassphraseFallback(w.id);
|
|
7154
|
+
}
|
|
7155
|
+
catch { }
|
|
7156
|
+
if (hasFallback) {
|
|
7157
|
+
needsEnv++;
|
|
7158
|
+
}
|
|
7159
|
+
else {
|
|
7160
|
+
unrecoverable.push(w.name || w.id.slice(0, 8));
|
|
7161
|
+
}
|
|
6622
7162
|
}
|
|
6623
7163
|
if (wallets.length > 0) {
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
7164
|
+
if (unrecoverable.length > 0) {
|
|
7165
|
+
checks.push({
|
|
7166
|
+
name: 'Wallet decryption',
|
|
7167
|
+
status: 'fail',
|
|
7168
|
+
detail: `${unrecoverable.length} session-only wallet(s) UNRECOVERABLE from this machine — keychain secret missing and no passphrase fallback (${unrecoverable.slice(0, 3).join(', ')}${unrecoverable.length > 3 ? ', …' : ''}). Recover by importing the mnemonic here, or running \`palmyr wallet rekey <id> --passphrase <p>\` on the original machine.`,
|
|
7169
|
+
});
|
|
7170
|
+
}
|
|
7171
|
+
else if (needsEnv > 0 && !envSet) {
|
|
7172
|
+
checks.push({
|
|
7173
|
+
name: 'Wallet decryption',
|
|
7174
|
+
status: 'warn',
|
|
7175
|
+
detail: `${needsEnv} wallet(s) need PALMYR_WALLET_PASSPHRASE to decrypt (keychain secret missing, passphrase fallback present). Set the env var to unblock pay / sign / export.`,
|
|
7176
|
+
});
|
|
7177
|
+
}
|
|
7178
|
+
else if (needsEnv > 0) {
|
|
7179
|
+
checks.push({
|
|
7180
|
+
name: 'Wallet decryption',
|
|
7181
|
+
status: 'pass',
|
|
7182
|
+
detail: `${keychainOk} via OS keychain, ${needsEnv} via PALMYR_WALLET_PASSPHRASE (env set)`,
|
|
7183
|
+
});
|
|
7184
|
+
}
|
|
7185
|
+
else {
|
|
7186
|
+
checks.push({
|
|
7187
|
+
name: 'Wallet decryption',
|
|
7188
|
+
status: 'pass',
|
|
7189
|
+
detail: `All ${keychainOk} wallet(s) have keychain secrets`,
|
|
7190
|
+
});
|
|
7191
|
+
}
|
|
6629
7192
|
}
|
|
6630
7193
|
// 5. API connectivity
|
|
6631
7194
|
try {
|