@palmyr/cli 1.8.3 → 1.9.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/README.md +10 -18
- package/dist/cli.js +716 -17
- package/dist/cli.js.map +1 -1
- package/dist/sdk.d.ts +11 -1
- package/dist/sdk.js +35 -2
- package/dist/sdk.js.map +1 -1
- package/dist/telemetry.d.ts +49 -0
- package/dist/telemetry.js +150 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/vault.d.ts +5 -4
- package/dist/vault.js +5 -4
- 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
|
@@ -17,6 +17,7 @@ import { render as inkRender } from 'ink';
|
|
|
17
17
|
import { ConfigScreen, Dashboard, DoctorScreen, DomainCheckScreen, DomainPricingScreen, ErrorScreen, HealthScreen, MenuScreen, PricingScreen, RecordsScreen, SetupScreen, StatusScreen, SuccessScreen, WalletCreateScreen, WalletStatusScreen, WalletListScreen } from './app.js';
|
|
18
18
|
import { Palmyr } from './sdk.js';
|
|
19
19
|
import { loadConfig, saveConfig, ensureDirs, log, addPhone, addDomain, addNote } from './config.js';
|
|
20
|
+
import { getState as getTelemetryState, setEnabled as setTelemetryEnabled, queuedCount as telemetryQueuedCount, appendEventSync as telemetryAppendEvent, flushQueue as telemetryFlushQueue } from './telemetry.js';
|
|
20
21
|
import { theme as t, icon, Spinner, warn, table, kv, section, setAgentMode as setUiAgentMode } from './ui.js';
|
|
21
22
|
import { existsSync, readFileSync } from 'fs';
|
|
22
23
|
import { homedir } from 'os';
|
|
@@ -229,11 +230,10 @@ function emitSessionOnlyWarning(write) {
|
|
|
229
230
|
const WALLET_HELP = {
|
|
230
231
|
create: [
|
|
231
232
|
{ 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
233
|
{ flag: '--solana', desc: 'Materialize the Solana account only', hint: 'default: both chains' },
|
|
234
234
|
{ flag: '--base', desc: 'Materialize the Base/EVM account only', hint: 'pair with --solana for both (default)' },
|
|
235
235
|
{ 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: '
|
|
236
|
+
{ flag: '--count <N>', desc: 'Bulk-create N wallets in one call (1-500)', hint: 'requires --tag' },
|
|
237
237
|
{ flag: '--name-prefix <p>', desc: 'Bulk name prefix; suffixed `-001..-N`', hint: 'default: same as --tag' },
|
|
238
238
|
{ 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
239
|
{ 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 +241,6 @@ const WALLET_HELP = {
|
|
|
241
241
|
import: [
|
|
242
242
|
{ flag: '--mnemonic <words>', desc: 'BIP-39 mnemonic phrase (required)' },
|
|
243
243
|
{ flag: '--name <name>', desc: 'Wallet name', hint: 'default: "Imported Wallet"' },
|
|
244
|
-
{ flag: '--managed', desc: 'Import as managed wallet' },
|
|
245
244
|
{ flag: '--solana', desc: 'Materialize the Solana account only' },
|
|
246
245
|
{ flag: '--base', desc: 'Materialize the Base/EVM account only' },
|
|
247
246
|
{ flag: '--tag <name>', desc: 'Assign a tag at import time' },
|
|
@@ -449,6 +448,15 @@ const PHONE_HELP = {
|
|
|
449
448
|
{ flag: '(price)', desc: '$0.02 per call' },
|
|
450
449
|
{ flag: '(example)', desc: 'palmyr phone messages --id PN_abc' },
|
|
451
450
|
],
|
|
451
|
+
message: [
|
|
452
|
+
{ flag: '--id <MESSAGE_ID>', desc: 'SMS message id (Telnyx-supplied; positional also accepted)' },
|
|
453
|
+
{ flag: '(price)', desc: '$0.005 per readback — cheap so agents can poll until delivery_status is terminal' },
|
|
454
|
+
{ flag: '(example)', desc: 'palmyr phone message <message-id-from-sms-response>' },
|
|
455
|
+
],
|
|
456
|
+
'sms-status': [
|
|
457
|
+
{ flag: '<message-id>', desc: 'Alias for `palmyr phone message <id>` — readback by id' },
|
|
458
|
+
{ flag: '(price)', desc: '$0.005 per readback' },
|
|
459
|
+
],
|
|
452
460
|
calls: [
|
|
453
461
|
{ flag: '--id <PHONE_ID>', desc: 'Phone number id to list calls for (required; positional also accepted)' },
|
|
454
462
|
{ flag: '(price)', desc: '$0.02 per call' },
|
|
@@ -527,7 +535,7 @@ const EMAIL_HELP = {
|
|
|
527
535
|
create: [
|
|
528
536
|
{ flag: '--name <name>', desc: 'Inbox name (required)' },
|
|
529
537
|
{ flag: '--domain <domain>', desc: 'Wallet-owned domain to host the inbox on (optional)', hint: 'default: Palmyr-hosted domain' },
|
|
530
|
-
{ flag: '--wallet <id|name>', desc: '
|
|
538
|
+
{ 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
539
|
{ flag: '(price)', desc: '$2.00 per inbox provisioned' },
|
|
532
540
|
{ flag: '(example)', desc: 'palmyr email create --name agent --domain example.com' },
|
|
533
541
|
],
|
|
@@ -733,6 +741,337 @@ const CHAT_HELP = {
|
|
|
733
741
|
{ flag: '(example)', desc: 'palmyr chat providers --capability web_search' },
|
|
734
742
|
],
|
|
735
743
|
};
|
|
744
|
+
// Help tables for the two social subsystems. Their presence is what gates
|
|
745
|
+
// `--help` from dispatching paid actions — see the `case 'twitter'` and
|
|
746
|
+
// `case 'tiktok'` blocks below. 1.8.3 had no entries here and the
|
|
747
|
+
// `case 'buy'` arm immediately called the $5 paid endpoint when a user
|
|
748
|
+
// (reasonably) ran `palmyr twitter buy --help`. Every entry below MUST
|
|
749
|
+
// flag the price for paid subcommands so future readers can scan it.
|
|
750
|
+
const TWITTER_HELP = {
|
|
751
|
+
import: [
|
|
752
|
+
{ flag: '<username>', desc: 'Twitter handle to import' },
|
|
753
|
+
{ flag: '--credentials-line "..."', desc: 'login:password:email:email_pw:totp_seed:ct0:auth_token format' },
|
|
754
|
+
{ flag: '--username --password ...', desc: 'Alternative: individual flags for each field' },
|
|
755
|
+
{ flag: '(price)', desc: 'Free — local vault only' },
|
|
756
|
+
],
|
|
757
|
+
list: [
|
|
758
|
+
{ flag: '--local', desc: 'Skip server check; show only locally-vaulted accounts' },
|
|
759
|
+
{ flag: '(price)', desc: 'Free local listing + paid lookups when --local is omitted (~$0.001 to enumerate server-side access)' },
|
|
760
|
+
],
|
|
761
|
+
info: [
|
|
762
|
+
{ flag: '<username>', desc: 'Account to inspect' },
|
|
763
|
+
{ flag: '(price)', desc: 'Free — local vault read' },
|
|
764
|
+
],
|
|
765
|
+
rename: [
|
|
766
|
+
{ flag: '<old>', desc: 'Current local handle' },
|
|
767
|
+
{ flag: '--to <new>', desc: 'New handle (after a real-server rename)' },
|
|
768
|
+
{ flag: '(price)', desc: 'Free — local-only metadata update' },
|
|
769
|
+
],
|
|
770
|
+
remove: [
|
|
771
|
+
{ flag: '<username>', desc: 'Account to remove' },
|
|
772
|
+
{ flag: '--confirm', desc: 'Required — local delete is irreversible' },
|
|
773
|
+
{ flag: '(price)', desc: 'Free — local vault only' },
|
|
774
|
+
],
|
|
775
|
+
totp: [
|
|
776
|
+
{ flag: '<username>', desc: 'Account whose current TOTP code to print' },
|
|
777
|
+
{ flag: '(price)', desc: 'Free — local TOTP generation' },
|
|
778
|
+
],
|
|
779
|
+
buy: [
|
|
780
|
+
{ flag: '(no args)', desc: 'Purchase the oldest ready X account from the supplier pool. Default for every filter below is random.' },
|
|
781
|
+
{ flag: '--country <CC>', desc: 'Filter to a specific country (ISO alpha-2: US, GB, DE, …). Run `pool-prices` first to see what is priced.' },
|
|
782
|
+
{ flag: '--source web|mobile', desc: 'Filter by registration source from X about-profile. Multiplies country price by source multiplier (default 1.0).' },
|
|
783
|
+
{ flag: '--max-renames N', desc: 'Cap username-change count. --max-renames 0 = never renamed. NULL on row means unknown → does not match.' },
|
|
784
|
+
{ flag: '--age 1y|2y|...', desc: 'Optional age category filter' },
|
|
785
|
+
{ flag: '(price)', desc: '$5 USDC default; country_price * source_multiplier when filters are passed.' },
|
|
786
|
+
{ flag: '(example)', desc: 'palmyr twitter buy --country US --source web --max-renames 0' },
|
|
787
|
+
],
|
|
788
|
+
login: [
|
|
789
|
+
{ flag: '<username>', desc: 'Force a fresh server-side session (browser runtime)' },
|
|
790
|
+
{ flag: '(price)', desc: '$0.005 USDC' },
|
|
791
|
+
],
|
|
792
|
+
'manual-login': [
|
|
793
|
+
{ flag: '<username>', desc: 'Open a remote browser session you sign in to manually' },
|
|
794
|
+
{ flag: '(price)', desc: 'Variable — server-side browser session cost' },
|
|
795
|
+
],
|
|
796
|
+
session: [
|
|
797
|
+
{ flag: '<username>', desc: 'Inspect cached server-side session status' },
|
|
798
|
+
{ flag: '(price)', desc: 'Free' },
|
|
799
|
+
],
|
|
800
|
+
post: [
|
|
801
|
+
{ flag: '<username>', desc: 'Account to post from' },
|
|
802
|
+
{ flag: '--body "..."', desc: 'Tweet body (required)' },
|
|
803
|
+
{ flag: '(price)', desc: '$0.001 USDC per post' },
|
|
804
|
+
],
|
|
805
|
+
reply: [
|
|
806
|
+
{ flag: '<username>', desc: 'Account to reply from' },
|
|
807
|
+
{ flag: '--to <url>', desc: 'Tweet URL to reply to' },
|
|
808
|
+
{ flag: '--body "..."', desc: 'Reply body' },
|
|
809
|
+
{ flag: '(price)', desc: '$0.001 USDC' },
|
|
810
|
+
],
|
|
811
|
+
like: [
|
|
812
|
+
{ flag: '<username>', desc: 'Account doing the like' },
|
|
813
|
+
{ flag: '--tweet <url>', desc: 'Tweet to like' },
|
|
814
|
+
{ flag: '(price)', desc: '$0.001 USDC' },
|
|
815
|
+
],
|
|
816
|
+
retweet: [
|
|
817
|
+
{ flag: '<username>', desc: 'Account doing the retweet' },
|
|
818
|
+
{ flag: '--tweet <url>', desc: 'Tweet to retweet' },
|
|
819
|
+
{ flag: '(price)', desc: '$0.001 USDC' },
|
|
820
|
+
],
|
|
821
|
+
follow: [
|
|
822
|
+
{ flag: '<username>', desc: 'Account doing the follow' },
|
|
823
|
+
{ flag: '--user @handle', desc: 'User to follow' },
|
|
824
|
+
{ flag: '(price)', desc: '$0.001 USDC' },
|
|
825
|
+
],
|
|
826
|
+
unfollow: [
|
|
827
|
+
{ flag: '<username>', desc: 'Account doing the unfollow' },
|
|
828
|
+
{ flag: '--user @handle', desc: 'User to unfollow' },
|
|
829
|
+
{ flag: '(price)', desc: '$0.001 USDC' },
|
|
830
|
+
],
|
|
831
|
+
delete: [
|
|
832
|
+
{ flag: '<username>', desc: 'Account that posted the tweet' },
|
|
833
|
+
{ flag: '--tweet <url>', desc: 'Tweet to delete' },
|
|
834
|
+
{ flag: '(price)', desc: '$0.001 USDC' },
|
|
835
|
+
],
|
|
836
|
+
'list-tweets': [
|
|
837
|
+
{ flag: '<username>', desc: 'Account whose timeline to fetch' },
|
|
838
|
+
{ flag: '(price)', desc: '$0.001 USDC' },
|
|
839
|
+
],
|
|
840
|
+
bio: [
|
|
841
|
+
{ flag: '<username>', desc: 'Account whose bio to update' },
|
|
842
|
+
{ flag: '--text "..."', desc: 'New bio (<=160 chars)' },
|
|
843
|
+
{ flag: '(price)', desc: '$0.001 USDC' },
|
|
844
|
+
],
|
|
845
|
+
name: [
|
|
846
|
+
{ flag: '<username>', desc: 'Account whose display name to update' },
|
|
847
|
+
{ flag: '--display "..."', desc: 'New display name' },
|
|
848
|
+
{ flag: '(price)', desc: '$0.001 USDC' },
|
|
849
|
+
],
|
|
850
|
+
location: [
|
|
851
|
+
{ flag: '<username>', desc: 'Account whose location to update' },
|
|
852
|
+
{ flag: '--location "..."', desc: 'New location string' },
|
|
853
|
+
{ flag: '(price)', desc: '$0.001 USDC' },
|
|
854
|
+
],
|
|
855
|
+
website: [
|
|
856
|
+
{ flag: '<username>', desc: 'Account whose profile website to update' },
|
|
857
|
+
{ flag: '--url https://...', desc: 'New website URL' },
|
|
858
|
+
{ flag: '(price)', desc: '$0.001 USDC' },
|
|
859
|
+
],
|
|
860
|
+
pfp: [
|
|
861
|
+
{ flag: '<username>', desc: 'Account whose avatar to update' },
|
|
862
|
+
{ flag: '--file pic.png', desc: 'Image file (jpeg / png)' },
|
|
863
|
+
{ flag: '(price)', desc: '$0.005 USDC' },
|
|
864
|
+
],
|
|
865
|
+
banner: [
|
|
866
|
+
{ flag: '<username>', desc: 'Account whose banner to update' },
|
|
867
|
+
{ flag: '--file banner.png', desc: 'Image file' },
|
|
868
|
+
{ flag: '(price)', desc: '$0.005 USDC' },
|
|
869
|
+
],
|
|
870
|
+
username: [
|
|
871
|
+
{ flag: '<username>', desc: 'Current account handle' },
|
|
872
|
+
{ flag: '--to <new>', desc: 'New handle' },
|
|
873
|
+
{ flag: '(price)', desc: '$0.005 USDC' },
|
|
874
|
+
],
|
|
875
|
+
transfer: [
|
|
876
|
+
{ flag: '<username>', desc: 'Account to transfer' },
|
|
877
|
+
{ flag: '--to <wallet>', desc: 'Destination wallet address' },
|
|
878
|
+
{ flag: '--confirm', desc: 'Required — rotates password, revokes other sessions' },
|
|
879
|
+
{ flag: '(price)', desc: 'Free for vaulted accounts; ~$0.01 USDC to auto-register an imported-only account first' },
|
|
880
|
+
],
|
|
881
|
+
share: [
|
|
882
|
+
{ flag: '<username>', desc: 'Account to share' },
|
|
883
|
+
{ flag: '--with <wallet>', desc: 'Wallet to grant access to' },
|
|
884
|
+
{ flag: '(price)', desc: 'Free for shared access (no password rotation)' },
|
|
885
|
+
],
|
|
886
|
+
unshare: [
|
|
887
|
+
{ flag: '<username>', desc: 'Account to revoke share on' },
|
|
888
|
+
{ flag: '--from <wallet>', desc: 'Wallet to revoke' },
|
|
889
|
+
{ flag: '--rotate', desc: 'Also rotate password so cached cookies stop working' },
|
|
890
|
+
{ flag: '(price)', desc: 'Free; --rotate runs async like transfer (~30-90s)' },
|
|
891
|
+
],
|
|
892
|
+
claim: [
|
|
893
|
+
{ flag: '(no args)', desc: 'Pull every server-side X account the wallet can access into the local vault' },
|
|
894
|
+
{ flag: '(price)', desc: '~$0.001 USDC per account claimed (creds-decryption fee)' },
|
|
895
|
+
],
|
|
896
|
+
// Server-backed account registration. Without these entries, `palmyr twitter
|
|
897
|
+
// register --help` (and friends) fell through to the top-level menu instead
|
|
898
|
+
// of explaining their flags. Subcommands listed in the parent switch must
|
|
899
|
+
// always have an entry here so the help guard fires before the case body.
|
|
900
|
+
thread: [
|
|
901
|
+
{ flag: '<username>', desc: 'Account to post the thread from' },
|
|
902
|
+
{ flag: '--texts \'["...","..."]\'', desc: 'JSON array of tweets (in order)' },
|
|
903
|
+
{ flag: '--file path.json', desc: 'Alternative: read the JSON array from a file' },
|
|
904
|
+
{ flag: '(price)', desc: '$0.005 USDC' },
|
|
905
|
+
],
|
|
906
|
+
register: [
|
|
907
|
+
{ flag: '<username>', desc: 'Account handle' },
|
|
908
|
+
{ flag: '--password <pw>', desc: 'Required if the account is not already in the local vault' },
|
|
909
|
+
{ flag: '--login --email --totp-seed --auth-token --ct0', desc: 'Optional; auto-pulled from local vault when not passed' },
|
|
910
|
+
{ flag: '--country <CC>', desc: 'Optional residency hint stored alongside the encrypted credentials' },
|
|
911
|
+
{ flag: '(price)', desc: 'Free — server tests login + encrypts creds at rest. Enables scheduling.' },
|
|
912
|
+
],
|
|
913
|
+
unregister: [
|
|
914
|
+
{ flag: '<username-or-id>', desc: 'Handle or 32-char hex account id' },
|
|
915
|
+
{ flag: '(price)', desc: 'Free — wipes server-side credentials, account no longer schedulable' },
|
|
916
|
+
],
|
|
917
|
+
registered: [
|
|
918
|
+
{ flag: '(no args)', desc: 'List every server-registered X account this wallet owns' },
|
|
919
|
+
{ flag: '(price)', desc: 'Free' },
|
|
920
|
+
],
|
|
921
|
+
schedule: [
|
|
922
|
+
{ flag: '<username>', desc: 'Account to post from (must be `register`-ed)' },
|
|
923
|
+
{ flag: '--at "ISO8601"', desc: 'When to fire (e.g. --at "2026-05-15T14:00:00Z")' },
|
|
924
|
+
{ flag: '--body "..."', desc: 'Text-only post' },
|
|
925
|
+
{ flag: '--texts \'["..."]\' / --file path.json', desc: 'Thread' },
|
|
926
|
+
{ flag: '--image / --video / --media-json', desc: 'Media attachments' },
|
|
927
|
+
{ flag: '--community <id>', desc: 'Post into an X Community' },
|
|
928
|
+
{ flag: '(price)', desc: '$0.001 USDC (text) or $0.005 USDC (thread / media) — paid up front' },
|
|
929
|
+
],
|
|
930
|
+
queue: [
|
|
931
|
+
{ flag: '--status pending|in_progress|completed|failed|cancelled', desc: 'Filter by status' },
|
|
932
|
+
{ flag: '--from / --to "ISO8601"', desc: 'Filter by post-at window' },
|
|
933
|
+
{ flag: '--account-id <id>', desc: 'Filter to one account' },
|
|
934
|
+
{ flag: '--limit <n>', desc: 'Cap result count' },
|
|
935
|
+
{ flag: '(price)', desc: 'Free' },
|
|
936
|
+
],
|
|
937
|
+
cancel: [
|
|
938
|
+
{ flag: '<schedule-id>', desc: 'Id from `palmyr twitter queue`' },
|
|
939
|
+
{ flag: '(price)', desc: 'Free — only cancels pending posts; in-flight ones are already settled' },
|
|
940
|
+
],
|
|
941
|
+
status: [
|
|
942
|
+
{ flag: '<username>', desc: 'Account to inspect' },
|
|
943
|
+
{ flag: '(price)', desc: 'Server-side liveness/shadow-ban check (not yet wired — see `palmyr twitter session` for cached login state)' },
|
|
944
|
+
],
|
|
945
|
+
'pool-add': [
|
|
946
|
+
{ flag: '--credentials-line "..."', desc: 'Single account creds (login:pw:email:email_pw[:2fa[:ct0:auth_token]])' },
|
|
947
|
+
{ flag: '--file path.txt', desc: 'Bulk: one credentials-line per row (# = comment)' },
|
|
948
|
+
{ flag: '--price <USDC>', desc: 'Required — per-account sale_price_usdc (legacy fallback when no country price row exists)' },
|
|
949
|
+
{ flag: '--country <CC>', desc: 'Optional override; twitterapi.io detects country + source + rename count from about_profile at seed time. Admin wins on country mismatch (flagged in response).' },
|
|
950
|
+
{ flag: '--age 1y|2y|3y|...', desc: 'Optional age category metadata' },
|
|
951
|
+
{ flag: '(auth)', desc: 'Admin-signed call — requires PALMYR_ADMIN_KEY' },
|
|
952
|
+
{ flag: '(price)', desc: 'Free — server-side seeding by pool operator' },
|
|
953
|
+
],
|
|
954
|
+
'pool-prices': [
|
|
955
|
+
{ flag: '(no args)', desc: 'List per-country prices set by the pool admin (public, free).' },
|
|
956
|
+
{ flag: '(price)', desc: 'Free' },
|
|
957
|
+
],
|
|
958
|
+
'pool-set-price': [
|
|
959
|
+
{ flag: '--country <CC>', desc: 'ISO 3166-1 alpha-2 country code (US, GB, DE, …)' },
|
|
960
|
+
{ flag: '--price <USDC>', desc: 'USDC amount the `buy --country <CC>` route will charge' },
|
|
961
|
+
{ flag: '(auth)', desc: 'Admin-signed call — requires PALMYR_ADMIN_KEY' },
|
|
962
|
+
{ flag: '(price)', desc: 'Free' },
|
|
963
|
+
],
|
|
964
|
+
'pool-delete-price': [
|
|
965
|
+
{ flag: '--country <CC>', desc: 'Country code to remove pricing for' },
|
|
966
|
+
{ flag: '(auth)', desc: 'Admin-signed call — requires PALMYR_ADMIN_KEY' },
|
|
967
|
+
{ flag: '(price)', desc: 'Free' },
|
|
968
|
+
],
|
|
969
|
+
'pool-set-source-multiplier': [
|
|
970
|
+
{ flag: '--source web|mobile|<id>', desc: 'Source identifier (matches the `source` column populated by twitterapi.io)' },
|
|
971
|
+
{ flag: '--multiplier <number>', desc: 'Positive scaling factor applied on top of country price when --source is passed at buy time. 1.0 = no change.' },
|
|
972
|
+
{ flag: '(auth)', desc: 'Admin-signed call — requires PALMYR_ADMIN_KEY' },
|
|
973
|
+
{ flag: '(price)', desc: 'Free' },
|
|
974
|
+
],
|
|
975
|
+
'pool-delete-source-multiplier': [
|
|
976
|
+
{ flag: '--source <id>', desc: 'Source identifier to remove the multiplier for (buy still works, just reverts to multiplier=1.0)' },
|
|
977
|
+
{ flag: '(auth)', desc: 'Admin-signed call — requires PALMYR_ADMIN_KEY' },
|
|
978
|
+
{ flag: '(price)', desc: 'Free' },
|
|
979
|
+
],
|
|
980
|
+
dispute: [
|
|
981
|
+
{ flag: '<account_id>', desc: 'Account id returned by `twitter buy` (the 32-char hex)' },
|
|
982
|
+
{ flag: '--reason suspended|other', desc: 'Default "suspended" — triggers auto-verify via twitterapi.io' },
|
|
983
|
+
{ flag: '--evidence "..."', desc: 'Optional note shown to the admin if the dispute ends up in admin_review' },
|
|
984
|
+
{ flag: '(price)', desc: '$0.01 USDC ownership-proof. 7-day window from purchase.' },
|
|
985
|
+
{ flag: '(example)', desc: 'palmyr twitter dispute abcd1234… --reason suspended' },
|
|
986
|
+
],
|
|
987
|
+
disputes: [
|
|
988
|
+
{ flag: '<dispute_id>', desc: 'Look up the status of a previously filed dispute' },
|
|
989
|
+
{ flag: '(price)', desc: '$0.001 USDC' },
|
|
990
|
+
],
|
|
991
|
+
'pool-disputes': [
|
|
992
|
+
{ flag: '(no args)', desc: 'List every dispute in the system (admin)' },
|
|
993
|
+
{ flag: '--status admin_review|pending|replaced|refunded|rejected', desc: 'Filter by status' },
|
|
994
|
+
{ flag: '(auth)', desc: 'Admin-signed call — requires PALMYR_ADMIN_KEY' },
|
|
995
|
+
{ flag: '(price)', desc: 'Free' },
|
|
996
|
+
],
|
|
997
|
+
'pool-resolve-dispute': [
|
|
998
|
+
{ flag: '<dispute_id>', desc: 'Id from `pool-disputes`' },
|
|
999
|
+
{ flag: '--action replace|refund|reject', desc: 'replace = grant same-country swap; refund = USDC back to payer; reject = decline' },
|
|
1000
|
+
{ flag: '--note "..."', desc: 'Optional admin note appended to the resolution' },
|
|
1001
|
+
{ flag: '(auth)', desc: 'Admin-signed call — requires PALMYR_ADMIN_KEY' },
|
|
1002
|
+
{ flag: '(price)', desc: 'Free' },
|
|
1003
|
+
],
|
|
1004
|
+
'pool-status': [
|
|
1005
|
+
{ flag: '(no args)', desc: 'Available / sold / reserved counts in the X account pool' },
|
|
1006
|
+
{ flag: '(auth)', desc: 'Admin-signed call — requires PALMYR_ADMIN_KEY' },
|
|
1007
|
+
{ flag: '(price)', desc: 'Free' },
|
|
1008
|
+
],
|
|
1009
|
+
};
|
|
1010
|
+
const TIKTOK_HELP = {
|
|
1011
|
+
import: [
|
|
1012
|
+
{ flag: '<username>', desc: 'TikTok handle to import' },
|
|
1013
|
+
{ flag: '--sessionid <s> --csrf <c> --webid <w>', desc: 'Cookies from a logged-in TikTok browser' },
|
|
1014
|
+
{ flag: '--credentials-line "..."', desc: 'Marketplace login:pw:email:email_pw format' },
|
|
1015
|
+
{ flag: '(price)', desc: 'Free — local vault only' },
|
|
1016
|
+
],
|
|
1017
|
+
list: [
|
|
1018
|
+
{ flag: '(no args)', desc: 'List all local TikTok accounts' },
|
|
1019
|
+
{ flag: '(price)', desc: 'Free' },
|
|
1020
|
+
],
|
|
1021
|
+
info: [{ flag: '<username>', desc: 'Show one account' }, { flag: '(price)', desc: 'Free' }],
|
|
1022
|
+
rename: [
|
|
1023
|
+
{ flag: '<old>', desc: 'Current local handle' },
|
|
1024
|
+
{ flag: '--to <new>', desc: 'New handle' },
|
|
1025
|
+
{ flag: '(price)', desc: 'Free — local-only metadata update' },
|
|
1026
|
+
],
|
|
1027
|
+
remove: [
|
|
1028
|
+
{ flag: '<username>', desc: 'Account to delete from local vault' },
|
|
1029
|
+
{ flag: '--confirm', desc: 'Required' },
|
|
1030
|
+
{ flag: '(price)', desc: 'Free' },
|
|
1031
|
+
],
|
|
1032
|
+
totp: [{ flag: '<username>', desc: 'Print current TOTP code' }, { flag: '(price)', desc: 'Free' }],
|
|
1033
|
+
login: [
|
|
1034
|
+
{ flag: '<username>', desc: 'Validate cookies and cache the session' },
|
|
1035
|
+
{ flag: '(price)', desc: '$0.005 USDC' },
|
|
1036
|
+
],
|
|
1037
|
+
session: [{ flag: '<username>', desc: 'Check cached session' }, { flag: '(price)', desc: 'Free' }],
|
|
1038
|
+
post: [
|
|
1039
|
+
{ flag: '<username>', desc: 'Account to post from' },
|
|
1040
|
+
{ flag: '--file video.mp4', desc: 'Video file' },
|
|
1041
|
+
{ flag: '--caption "..."', desc: 'Caption' },
|
|
1042
|
+
{ flag: '(price)', desc: '$0.001 USDC' },
|
|
1043
|
+
],
|
|
1044
|
+
follow: [
|
|
1045
|
+
{ flag: '<username>', desc: 'Account doing the follow' },
|
|
1046
|
+
{ flag: '--user @handle', desc: 'User to follow' },
|
|
1047
|
+
{ flag: '(price)', desc: '$0.001 USDC' },
|
|
1048
|
+
],
|
|
1049
|
+
like: [
|
|
1050
|
+
{ flag: '<username>', desc: 'Account doing the like' },
|
|
1051
|
+
{ flag: '--video <url>', desc: 'Video URL to like' },
|
|
1052
|
+
{ flag: '(price)', desc: '$0.001 USDC' },
|
|
1053
|
+
],
|
|
1054
|
+
delete: [
|
|
1055
|
+
{ flag: '<username>', desc: 'Account that posted the video' },
|
|
1056
|
+
{ flag: '--video <url>', desc: 'Video to delete' },
|
|
1057
|
+
{ flag: '(price)', desc: '$0.001 USDC' },
|
|
1058
|
+
],
|
|
1059
|
+
bio: [
|
|
1060
|
+
{ flag: '<username>', desc: 'Account whose bio to update' },
|
|
1061
|
+
{ flag: '--text "..."', desc: 'New bio (<=80 chars)' },
|
|
1062
|
+
{ flag: '(price)', desc: '$0.001 USDC' },
|
|
1063
|
+
],
|
|
1064
|
+
name: [
|
|
1065
|
+
{ flag: '<username>', desc: 'Account whose display name to update' },
|
|
1066
|
+
{ flag: '--display "..."', desc: 'New display name (<=30 chars)' },
|
|
1067
|
+
{ flag: '(price)', desc: '$0.001 USDC' },
|
|
1068
|
+
],
|
|
1069
|
+
pfp: [
|
|
1070
|
+
{ flag: '<username>', desc: 'Account whose avatar to update' },
|
|
1071
|
+
{ flag: '--file pic.png', desc: 'Image file' },
|
|
1072
|
+
{ flag: '(price)', desc: '$0.005 USDC' },
|
|
1073
|
+
],
|
|
1074
|
+
};
|
|
736
1075
|
/**
|
|
737
1076
|
* Render a per-command menu (no subcommand given). On a TTY → Ink MenuScreen
|
|
738
1077
|
* with the Palmyr aesthetic. In agent mode → flat JSON listing the available
|
|
@@ -849,6 +1188,7 @@ const TOP_LEVEL_COMMANDS = [
|
|
|
849
1188
|
{ name: 'doctor', description: 'Verify system health (cred store, vault, API)' },
|
|
850
1189
|
{ name: 'pricing', description: 'All service prices' },
|
|
851
1190
|
{ name: 'health', description: 'API status + version check' },
|
|
1191
|
+
{ name: 'telemetry', description: 'on · off · status (opt-in anonymous usage stats)' },
|
|
852
1192
|
];
|
|
853
1193
|
// ─── Help ───
|
|
854
1194
|
function help() {
|
|
@@ -942,6 +1282,22 @@ async function main() {
|
|
|
942
1282
|
const startTime = Date.now();
|
|
943
1283
|
// No first-time banner — agent-first CLI should never pollute output.
|
|
944
1284
|
const url = process.env.PALMYR_API || config.api;
|
|
1285
|
+
// Opt-in telemetry. If the user has explicitly enabled it, queue this run's
|
|
1286
|
+
// exit-code + duration on shutdown (sync — async work in 'exit' is dropped)
|
|
1287
|
+
// and fire-and-forget any previously-queued events to the API right now.
|
|
1288
|
+
// Both no-ops when telemetry is off. Never blocks the user's command —
|
|
1289
|
+
// the flush races their work and Node exits once both finish.
|
|
1290
|
+
process.on('exit', (code) => {
|
|
1291
|
+
telemetryAppendEvent({
|
|
1292
|
+
cmd: subcommand ? `${command} ${subcommand}` : command,
|
|
1293
|
+
exitCode: code,
|
|
1294
|
+
durationMs: Date.now() - startTime,
|
|
1295
|
+
cliVersion: VERSION,
|
|
1296
|
+
nodeVersion: process.version,
|
|
1297
|
+
platform: process.platform,
|
|
1298
|
+
});
|
|
1299
|
+
});
|
|
1300
|
+
void telemetryFlushQueue(url);
|
|
945
1301
|
const token = flags.token || config.apiKey || process.env.PALMYR_TOKEN || process.env.PALMYR_API_KEY;
|
|
946
1302
|
const passphrase = flags.passphrase || process.env.PALMYR_WALLET_PASSPHRASE;
|
|
947
1303
|
const ao = new Palmyr(url, true, token, passphrase);
|
|
@@ -1042,6 +1398,7 @@ async function main() {
|
|
|
1042
1398
|
{ name: 'release', description: 'Release a phone number', hint: '--id PHONE_ID' },
|
|
1043
1399
|
{ name: 'sms', description: 'Send an SMS', hint: '--id ID --to +1... --body "hi"' },
|
|
1044
1400
|
{ name: 'messages', description: 'Read SMS messages received on a number', hint: '--id PHONE_ID' },
|
|
1401
|
+
{ name: 'message', description: 'Get one SMS message by id (incl. delivery status)', hint: '--id MESSAGE_ID' },
|
|
1045
1402
|
{ name: 'call', description: 'Place a voice call', hint: '--id ID --to +1... --tts "hello"' },
|
|
1046
1403
|
{ name: 'calls', description: 'List calls placed/received on a number', hint: '--id PHONE_ID' },
|
|
1047
1404
|
{ name: 'call-info', description: 'Get details on a single call', hint: '--call CALL_CONTROL_ID' },
|
|
@@ -1149,6 +1506,18 @@ async function main() {
|
|
|
1149
1506
|
const data = await ao.phoneMessages(id);
|
|
1150
1507
|
return print(data);
|
|
1151
1508
|
}
|
|
1509
|
+
case 'message':
|
|
1510
|
+
case 'sms-status': {
|
|
1511
|
+
// Per-message readback. Mirrors `phone call-info` for SMS:
|
|
1512
|
+
// the Telnyx webhook updates delivery_status on the row, and this
|
|
1513
|
+
// endpoint serves it back. Useful when the immediate sms response
|
|
1514
|
+
// is 'queued' and the caller wants to confirm delivery.
|
|
1515
|
+
const messageId = flags.id || flags.message || positional[0];
|
|
1516
|
+
if (!messageId)
|
|
1517
|
+
err('Usage: palmyr phone message <message-id>');
|
|
1518
|
+
const data = await ao.phoneMessage(messageId);
|
|
1519
|
+
return print(data);
|
|
1520
|
+
}
|
|
1152
1521
|
case 'calls': {
|
|
1153
1522
|
const id = flags.id || positional[0];
|
|
1154
1523
|
if (!id)
|
|
@@ -1287,13 +1656,58 @@ async function main() {
|
|
|
1287
1656
|
switch (subcommand) {
|
|
1288
1657
|
case 'create': {
|
|
1289
1658
|
const name = flags.name || positional[0];
|
|
1290
|
-
const
|
|
1659
|
+
const walletInput = flags.wallet;
|
|
1291
1660
|
const domain = flags.domain;
|
|
1292
1661
|
if (!name)
|
|
1293
1662
|
err('--name required (e.g. palmyr email create --name hello [--domain example.com])');
|
|
1663
|
+
// --wallet accepts three forms: vault id, vault name, or a raw
|
|
1664
|
+
// Solana base58 pubkey. The server only accepts a Solana pubkey
|
|
1665
|
+
// (E2E encryption is Ed25519→X25519), so resolve id/name to a
|
|
1666
|
+
// pubkey here before the request. Raw pubkeys pass through.
|
|
1667
|
+
// Doing this resolution client-side also means a Base-paying user
|
|
1668
|
+
// doesn't need a Solana wallet — their vault already has one
|
|
1669
|
+
// (single mnemonic, both chains), and we can find it without
|
|
1670
|
+
// making the server reach back for client-side keys.
|
|
1671
|
+
let walletAddress;
|
|
1672
|
+
if (walletInput) {
|
|
1673
|
+
const looksLikeSolPubkey = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(walletInput);
|
|
1674
|
+
if (looksLikeSolPubkey) {
|
|
1675
|
+
walletAddress = walletInput;
|
|
1676
|
+
}
|
|
1677
|
+
else {
|
|
1678
|
+
const { listVaultWallets } = await import('./vault.js');
|
|
1679
|
+
const wallets = listVaultWallets();
|
|
1680
|
+
const match = wallets.find(w => w.id === walletInput || w.name === walletInput);
|
|
1681
|
+
if (!match)
|
|
1682
|
+
err(`--wallet "${walletInput}" did not match any vault id, name, or look like a Solana pubkey`);
|
|
1683
|
+
if (!match.solanaAddress)
|
|
1684
|
+
err(`Wallet "${walletInput}" has no Solana address — email inboxes require one (E2E uses Ed25519). Re-create with: palmyr wallet create`);
|
|
1685
|
+
walletAddress = match.solanaAddress;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
else {
|
|
1689
|
+
// No --wallet: the server would normally default the inbox owner
|
|
1690
|
+
// to the x402 payer. That works for Solana-paid calls but
|
|
1691
|
+
// 400s on Base because the payer is an EVM address. Auto-fill
|
|
1692
|
+
// the *paying wallet's* Solana address here so a single
|
|
1693
|
+
// mnemonic-derived vault wallet works on either pay chain.
|
|
1694
|
+
const cfg = loadConfig();
|
|
1695
|
+
const payChain = (cfg.defaultPayChain || 'solana');
|
|
1696
|
+
if (payChain === 'base') {
|
|
1697
|
+
const { listVaultWallets } = await import('./vault.js');
|
|
1698
|
+
const wallets = listVaultWallets();
|
|
1699
|
+
const targetId = cfg.defaultPayWalletId || process.env.PALMYR_PAY_WALLET;
|
|
1700
|
+
const paying = (targetId && wallets.find(w => w.id === targetId)) || wallets.find(w => w.evmAddress && w.solanaAddress);
|
|
1701
|
+
if (paying?.solanaAddress)
|
|
1702
|
+
walletAddress = paying.solanaAddress;
|
|
1703
|
+
// If no Solana address is reachable, fall through and let the
|
|
1704
|
+
// server return its actionable 400 — silent failure would be
|
|
1705
|
+
// worse than a clear error.
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1294
1708
|
const spin = new Spinner();
|
|
1295
1709
|
spin.start('Creating inbox...');
|
|
1296
|
-
const data = await ao.emailCreate(name,
|
|
1710
|
+
const data = await ao.emailCreate(name, walletAddress, domain);
|
|
1297
1711
|
spin.stop('Inbox created', true);
|
|
1298
1712
|
return print(data);
|
|
1299
1713
|
}
|
|
@@ -1589,6 +2003,26 @@ async function main() {
|
|
|
1589
2003
|
const localKeyPath = generatedKeyMeta?.privateKeyPath
|
|
1590
2004
|
|| (pubkeyFile ? pubkeyFile.replace(/\.pub$/, '').replace('~', homedir()) : undefined)
|
|
1591
2005
|
|| (explicitKeyPath ? explicitKeyPath.replace('~', homedir()) : undefined);
|
|
2006
|
+
// --ssh-key <id> uploads a server-side key but doesn't tell us where
|
|
2007
|
+
// the matching private key lives on this machine. Without
|
|
2008
|
+
// --key-path, the SSH readiness gate later silently skips and the
|
|
2009
|
+
// deploy reports `ssh: skipped` while still marking the server as
|
|
2010
|
+
// "ready" — a real foot-gun (dogfood report 2026-05-25 hit this
|
|
2011
|
+
// exact path). Be loud about it now, before anyone pays.
|
|
2012
|
+
if (sshKeyIds && !localKeyPath) {
|
|
2013
|
+
const msg = 'Warning: --ssh-key <id> uploaded the public key server-side, ' +
|
|
2014
|
+
'but no matching private key was passed to this CLI. ' +
|
|
2015
|
+
'SSH readiness cannot be verified locally — `compute wait` and the ' +
|
|
2016
|
+
'inline --wait check will skip the SSH gate. Pass ' +
|
|
2017
|
+
'`--key-path /path/to/private_key` (or `--private-key`) to ' +
|
|
2018
|
+
'enable the verification.';
|
|
2019
|
+
if (AGENT_MODE) {
|
|
2020
|
+
process.stderr.write(JSON.stringify({ event: 'warning', code: 'ssh_key_no_local_key', message: msg }) + '\n');
|
|
2021
|
+
}
|
|
2022
|
+
else {
|
|
2023
|
+
process.stderr.write(`\n${msg}\n\n`);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
1592
2026
|
// Progress events to stderr — default ON in agent mode so a
|
|
1593
2027
|
// long deploy isn't a 10-minute silence. Pass --no-progress to
|
|
1594
2028
|
// opt out. Stdout still gets one final JSON object, so jq
|
|
@@ -2169,7 +2603,7 @@ async function main() {
|
|
|
2169
2603
|
subtitle: 'Non-custodial HD wallet',
|
|
2170
2604
|
footerLeft: 'Solana + Base wallet operations',
|
|
2171
2605
|
commands: [
|
|
2172
|
-
{ name: 'create', description: 'Create one or many wallets', hint: '[--tag X --count 100] [--solana|--base]
|
|
2606
|
+
{ name: 'create', description: 'Create one or many wallets', hint: '[--tag X --count 100] [--solana|--base]' },
|
|
2173
2607
|
{ name: 'import', description: 'Import from mnemonic', hint: '--mnemonic "..." [--tag X]' },
|
|
2174
2608
|
{ name: 'list', description: 'List all wallets', hint: '[--tag <name>]' },
|
|
2175
2609
|
{ name: 'info', description: 'Wallet details', hint: 'WALLET_ID' },
|
|
@@ -2183,7 +2617,6 @@ async function main() {
|
|
|
2183
2617
|
{ name: 'api-key', description: 'Create agent API key', hint: 'WALLET_ID --name my-agent' },
|
|
2184
2618
|
{ name: 'config', description: 'Get agent config', hint: 'WALLET_ID' },
|
|
2185
2619
|
{ 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
2620
|
{ name: 'buy', description: 'Open a trading position', hint: 'solana <CA> --amount 0.5sol --thesis "..."' },
|
|
2188
2621
|
{ name: 'cohort', description: 'Split a buy across N derived wallets with jitter (Phase 4c)', hint: 'buy <CHAIN> <CA> --total ... --split N' },
|
|
2189
2622
|
{ name: 'template', description: 'Manage YAML strategy templates', hint: 'list | show <name> | path <name> | delete <name>' },
|
|
@@ -4978,7 +5411,12 @@ async function main() {
|
|
|
4978
5411
|
log(`auto-imported @${username} from server (${sourceLabel} → local vault)`);
|
|
4979
5412
|
return summary;
|
|
4980
5413
|
};
|
|
4981
|
-
|
|
5414
|
+
// Help guard. `palmyr twitter buy --help` MUST never dispatch to the
|
|
5415
|
+
// paid `case 'buy'` below — 1.8.3 had no guard here and a real user
|
|
5416
|
+
// got charged $5 for a help command. Falls back to the top-level menu
|
|
5417
|
+
// when the subcommand has no per-subcommand help entry, so even an
|
|
5418
|
+
// unrecognized `palmyr twitter <whatever> --help` is safe to run.
|
|
5419
|
+
if (!subcommand || (flags.help && !TWITTER_HELP[subcommand])) {
|
|
4982
5420
|
showMenu({
|
|
4983
5421
|
command: 'twitter',
|
|
4984
5422
|
title: 'twitter',
|
|
@@ -4994,7 +5432,10 @@ async function main() {
|
|
|
4994
5432
|
{ name: 'buy', description: 'Purchase an aged account (requires server supplier config)', hint: '--age 1y --country US' },
|
|
4995
5433
|
{ name: 'login', description: 'Force a fresh server-side session (requires browser runtime)', hint: '<username>' },
|
|
4996
5434
|
{ name: 'post', description: 'Post a tweet (requires server browser runtime)', hint: '<username> --body "..."' },
|
|
4997
|
-
|
|
5435
|
+
// `status` is not wired yet (Phase 3). Hidden from this menu so
|
|
5436
|
+
// users don't try a command that will only error; `session`
|
|
5437
|
+
// covers the most useful subset (cached server-side login state).
|
|
5438
|
+
{ name: 'session', description: 'Inspect cached server-side session for an account', hint: '<username>' },
|
|
4998
5439
|
{ name: 'transfer', description: 'Hand an account to another wallet (rotates password; auto-registers if needed)', hint: '<username> --to <wallet> --confirm' },
|
|
4999
5440
|
{ name: 'share', description: 'Grant another wallet shared access', hint: '<username> --with <wallet>' },
|
|
5000
5441
|
{ name: 'unshare', description: 'Revoke a wallet’s shared access', hint: '<username> --from <wallet> [--rotate]' },
|
|
@@ -5004,6 +5445,10 @@ async function main() {
|
|
|
5004
5445
|
});
|
|
5005
5446
|
return;
|
|
5006
5447
|
}
|
|
5448
|
+
if (flags.help && subcommand && TWITTER_HELP[subcommand]) {
|
|
5449
|
+
subcommandHelp('twitter', subcommand, TWITTER_HELP[subcommand]);
|
|
5450
|
+
return;
|
|
5451
|
+
}
|
|
5007
5452
|
switch (subcommand) {
|
|
5008
5453
|
case 'import': {
|
|
5009
5454
|
// Option 1: --credentials-line "login:password:email:email_pw:2fa:ct0:auth_token"
|
|
@@ -5829,10 +6274,27 @@ async function main() {
|
|
|
5829
6274
|
return print({ success: true, platform, username, op: subcommand, ...(data?.data || {}) });
|
|
5830
6275
|
}
|
|
5831
6276
|
case 'buy': {
|
|
5832
|
-
// Agents
|
|
6277
|
+
// Agents say "buy" with optional --country / --source / --max-renames.
|
|
6278
|
+
// Each filter is independent (default: random across that
|
|
6279
|
+
// dimension). Pricing = country_price * source_multiplier:
|
|
6280
|
+
// - --country US → country_prices.US (e.g. $8)
|
|
6281
|
+
// - --source web → multiplied by web's row in
|
|
6282
|
+
// source_multipliers (e.g. 1.2)
|
|
6283
|
+
// - --max-renames 0 → filter only, no price impact
|
|
6284
|
+
// Without --country, falls back to the legacy $5 flat rate.
|
|
6285
|
+
const country = (flags.country || '').trim().toUpperCase() || undefined;
|
|
6286
|
+
const ageCategory = flags.age || flags['age-category'] || undefined;
|
|
6287
|
+
const source = (flags.source || '').trim().toLowerCase() || undefined;
|
|
6288
|
+
const maxRenamesRaw = flags['max-renames'];
|
|
6289
|
+
const maxUsernameChanges = maxRenamesRaw === undefined || maxRenamesRaw === ''
|
|
6290
|
+
? undefined
|
|
6291
|
+
: Number(maxRenamesRaw);
|
|
6292
|
+
if (maxUsernameChanges !== undefined && (!Number.isFinite(maxUsernameChanges) || maxUsernameChanges < 0)) {
|
|
6293
|
+
err('--max-renames must be a non-negative integer (e.g. 0 = never renamed)');
|
|
6294
|
+
}
|
|
5833
6295
|
let data;
|
|
5834
6296
|
try {
|
|
5835
|
-
data = await ao.socialTwitterBuy();
|
|
6297
|
+
data = await ao.socialTwitterBuy(country, ageCategory, { source, maxUsernameChanges });
|
|
5836
6298
|
}
|
|
5837
6299
|
catch (e) {
|
|
5838
6300
|
err(`Buy failed: ${e.message}`, EXIT.GENERAL);
|
|
@@ -5844,10 +6306,15 @@ async function main() {
|
|
|
5844
6306
|
// Auto-import into the local vault + prime the session cache so
|
|
5845
6307
|
// the buyer can post immediately with the cookies the admin
|
|
5846
6308
|
// pre-seasoned at pool-add time.
|
|
6309
|
+
const filterTags = [
|
|
6310
|
+
country && `country=${country}`,
|
|
6311
|
+
source && `source=${source}`,
|
|
6312
|
+
maxUsernameChanges !== undefined && `max_renames=${maxUsernameChanges}`,
|
|
6313
|
+
].filter(Boolean).join(', ');
|
|
5847
6314
|
const summary = sv.importAccount(platform, account.username, account.credentials, {
|
|
5848
6315
|
source: 'pool',
|
|
5849
6316
|
proxy_session_id: account.proxy_session_id,
|
|
5850
|
-
notes: 'Bought from pool',
|
|
6317
|
+
notes: filterTags ? `Bought from pool (${filterTags})` : 'Bought from pool',
|
|
5851
6318
|
});
|
|
5852
6319
|
sv.saveSession(summary.id, platform, account.cookies || []);
|
|
5853
6320
|
sv.updateMeta(platform, summary.username, { last_action_at: new Date().toISOString() });
|
|
@@ -5855,8 +6322,177 @@ async function main() {
|
|
|
5855
6322
|
success: true,
|
|
5856
6323
|
platform,
|
|
5857
6324
|
username: summary.username,
|
|
5858
|
-
|
|
6325
|
+
country: account.country,
|
|
6326
|
+
source: account.source,
|
|
6327
|
+
username_change_count: account.username_change_count,
|
|
6328
|
+
account_based_in: account.account_based_in,
|
|
6329
|
+
account_id: account.id,
|
|
6330
|
+
hint: `Ready to post — try: palmyr twitter post ${summary.username} --body "gm". ` +
|
|
6331
|
+
`If the account is suspended within 7 days, run: palmyr twitter dispute ${account.id}`,
|
|
6332
|
+
});
|
|
6333
|
+
}
|
|
6334
|
+
case 'pool-prices': {
|
|
6335
|
+
// Public: which countries are priced and what they cost. Run
|
|
6336
|
+
// before `buy --country X` to confirm the country is available.
|
|
6337
|
+
let data;
|
|
6338
|
+
try {
|
|
6339
|
+
data = await ao.socialTwitterPoolPrices();
|
|
6340
|
+
}
|
|
6341
|
+
catch (e) {
|
|
6342
|
+
err(`pool-prices failed: ${e.message}`, EXIT.GENERAL);
|
|
6343
|
+
}
|
|
6344
|
+
return print(data);
|
|
6345
|
+
}
|
|
6346
|
+
case 'pool-set-price': {
|
|
6347
|
+
// Admin: set USDC price for a single country code. Idempotent.
|
|
6348
|
+
const { buildAdminHeaders } = await import('./admin-auth.js');
|
|
6349
|
+
const country = (flags.country || '').trim().toUpperCase();
|
|
6350
|
+
const price = flags.price !== undefined ? Number(flags.price) : NaN;
|
|
6351
|
+
if (!country)
|
|
6352
|
+
err('--country <CC> required (ISO 3166-1 alpha-2: US, GB, DE, …)');
|
|
6353
|
+
if (!Number.isFinite(price) || price <= 0)
|
|
6354
|
+
err('--price <USDC> required (positive number)');
|
|
6355
|
+
const path = `/social/twitter/pool/prices/${encodeURIComponent(country)}`;
|
|
6356
|
+
const headers = buildAdminHeaders('PUT', path);
|
|
6357
|
+
const res = await fetch(ao.api + path, {
|
|
6358
|
+
method: 'PUT',
|
|
6359
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
6360
|
+
body: JSON.stringify({ price_usdc: price }),
|
|
5859
6361
|
});
|
|
6362
|
+
const data = await res.json();
|
|
6363
|
+
if (!res.ok || !data.success)
|
|
6364
|
+
err(`pool-set-price failed: ${data.error || `HTTP ${res.status}`}`, EXIT.GENERAL);
|
|
6365
|
+
return print(data);
|
|
6366
|
+
}
|
|
6367
|
+
case 'pool-delete-price': {
|
|
6368
|
+
// Admin: remove the row for a country. Subsequent `buy --country X`
|
|
6369
|
+
// will return 400 "Country not priced" until set again.
|
|
6370
|
+
const { buildAdminHeaders } = await import('./admin-auth.js');
|
|
6371
|
+
const country = (flags.country || '').trim().toUpperCase();
|
|
6372
|
+
if (!country)
|
|
6373
|
+
err('--country <CC> required');
|
|
6374
|
+
const path = `/social/twitter/pool/prices/${encodeURIComponent(country)}`;
|
|
6375
|
+
const headers = buildAdminHeaders('DELETE', path);
|
|
6376
|
+
const res = await fetch(ao.api + path, { method: 'DELETE', headers });
|
|
6377
|
+
const data = await res.json();
|
|
6378
|
+
if (!res.ok)
|
|
6379
|
+
err(`pool-delete-price failed: ${data.error || `HTTP ${res.status}`}`, EXIT.GENERAL);
|
|
6380
|
+
return print(data);
|
|
6381
|
+
}
|
|
6382
|
+
case 'pool-set-source-multiplier': {
|
|
6383
|
+
// Admin: scale the country price for buys filtered by a given
|
|
6384
|
+
// source ('web', 'mobile', …). e.g. mult=1.2 → web buys cost
|
|
6385
|
+
// 20% more than the base country price.
|
|
6386
|
+
const { buildAdminHeaders } = await import('./admin-auth.js');
|
|
6387
|
+
const source = (flags.source || '').trim().toLowerCase();
|
|
6388
|
+
const mult = flags.multiplier !== undefined ? Number(flags.multiplier) : NaN;
|
|
6389
|
+
if (!source)
|
|
6390
|
+
err('--source <name> required (e.g. web, mobile)');
|
|
6391
|
+
if (!Number.isFinite(mult) || mult <= 0)
|
|
6392
|
+
err('--multiplier <number> required (positive)');
|
|
6393
|
+
const path = `/social/twitter/pool/source-multipliers/${encodeURIComponent(source)}`;
|
|
6394
|
+
const headers = buildAdminHeaders('PUT', path);
|
|
6395
|
+
const res = await fetch(ao.api + path, {
|
|
6396
|
+
method: 'PUT',
|
|
6397
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
6398
|
+
body: JSON.stringify({ multiplier: mult }),
|
|
6399
|
+
});
|
|
6400
|
+
const data = await res.json();
|
|
6401
|
+
if (!res.ok || !data.success)
|
|
6402
|
+
err(`pool-set-source-multiplier failed: ${data.error || `HTTP ${res.status}`}`, EXIT.GENERAL);
|
|
6403
|
+
return print(data);
|
|
6404
|
+
}
|
|
6405
|
+
case 'pool-delete-source-multiplier': {
|
|
6406
|
+
// Admin: drop the multiplier for a source. Subsequent `buy
|
|
6407
|
+
// --source X` reverts to using 1.0 (filter only, no price scaling).
|
|
6408
|
+
const { buildAdminHeaders } = await import('./admin-auth.js');
|
|
6409
|
+
const source = (flags.source || '').trim().toLowerCase();
|
|
6410
|
+
if (!source)
|
|
6411
|
+
err('--source <name> required');
|
|
6412
|
+
const path = `/social/twitter/pool/source-multipliers/${encodeURIComponent(source)}`;
|
|
6413
|
+
const headers = buildAdminHeaders('DELETE', path);
|
|
6414
|
+
const res = await fetch(ao.api + path, { method: 'DELETE', headers });
|
|
6415
|
+
const data = await res.json();
|
|
6416
|
+
if (!res.ok)
|
|
6417
|
+
err(`pool-delete-source-multiplier failed: ${data.error || `HTTP ${res.status}`}`, EXIT.GENERAL);
|
|
6418
|
+
return print(data);
|
|
6419
|
+
}
|
|
6420
|
+
case 'dispute': {
|
|
6421
|
+
// Buyer: file a dispute for a pool-bought account that got
|
|
6422
|
+
// suspended. Server auto-verifies via twitterapi.io and either
|
|
6423
|
+
// hands over a same-country replacement, refunds USDC, or queues
|
|
6424
|
+
// for admin review when the signal is ambiguous.
|
|
6425
|
+
const accountId = positional[0] || flags['account-id'] || flags.id;
|
|
6426
|
+
const reason = (flags.reason || 'suspended');
|
|
6427
|
+
const evidence = flags.evidence || flags.note || undefined;
|
|
6428
|
+
if (!accountId)
|
|
6429
|
+
err('<account_id> required (the id returned by `palmyr twitter buy`)');
|
|
6430
|
+
if (reason !== 'suspended' && reason !== 'other')
|
|
6431
|
+
err('--reason must be "suspended" or "other"');
|
|
6432
|
+
let data;
|
|
6433
|
+
try {
|
|
6434
|
+
data = await ao.socialTwitterDispute(accountId, { reason, evidence });
|
|
6435
|
+
}
|
|
6436
|
+
catch (e) {
|
|
6437
|
+
err(`Dispute failed: ${e.message}`, EXIT.GENERAL);
|
|
6438
|
+
}
|
|
6439
|
+
if (!data?.success)
|
|
6440
|
+
err(`Dispute failed: ${data?.error || 'unknown'}`, EXIT.GENERAL);
|
|
6441
|
+
return print(data);
|
|
6442
|
+
}
|
|
6443
|
+
case 'disputes': {
|
|
6444
|
+
// Buyer: list ONE specific dispute by id. Listing all your
|
|
6445
|
+
// disputes isn't supported by the buyer surface today — track
|
|
6446
|
+
// the id printed by `dispute` and call `disputes <id>`.
|
|
6447
|
+
const id = positional[0] || flags.id;
|
|
6448
|
+
if (!id)
|
|
6449
|
+
err('<dispute_id> required');
|
|
6450
|
+
let data;
|
|
6451
|
+
try {
|
|
6452
|
+
data = await ao.socialTwitterDisputeGet(id);
|
|
6453
|
+
}
|
|
6454
|
+
catch (e) {
|
|
6455
|
+
err(`Get dispute failed: ${e.message}`, EXIT.GENERAL);
|
|
6456
|
+
}
|
|
6457
|
+
return print(data);
|
|
6458
|
+
}
|
|
6459
|
+
case 'pool-disputes': {
|
|
6460
|
+
// Admin: list every dispute, optionally filter by status. The
|
|
6461
|
+
// `admin_review` queue is the human-decision backlog.
|
|
6462
|
+
const { buildAdminHeaders } = await import('./admin-auth.js');
|
|
6463
|
+
const status = flags.status || undefined;
|
|
6464
|
+
const path = '/social/twitter/pool/disputes' + (status ? `?status=${encodeURIComponent(status)}` : '');
|
|
6465
|
+
const headers = buildAdminHeaders('GET', path);
|
|
6466
|
+
const res = await fetch(ao.api + path, { headers });
|
|
6467
|
+
const data = await res.json();
|
|
6468
|
+
if (!res.ok)
|
|
6469
|
+
err(`pool-disputes failed: ${data.error || `HTTP ${res.status}`}`, EXIT.GENERAL);
|
|
6470
|
+
return print(data);
|
|
6471
|
+
}
|
|
6472
|
+
case 'pool-resolve-dispute': {
|
|
6473
|
+
// Admin: resolve an admin_review dispute. action ∈ replace |
|
|
6474
|
+
// refund | reject. `replace` needs same-country stock; `refund`
|
|
6475
|
+
// needs payment provenance on the row (auto-saved at buy time).
|
|
6476
|
+
const { buildAdminHeaders } = await import('./admin-auth.js');
|
|
6477
|
+
const id = positional[0] || flags.id;
|
|
6478
|
+
const action = flags.action;
|
|
6479
|
+
const note = flags.note || undefined;
|
|
6480
|
+
if (!id)
|
|
6481
|
+
err('<dispute_id> required');
|
|
6482
|
+
if (action !== 'replace' && action !== 'refund' && action !== 'reject') {
|
|
6483
|
+
err('--action must be "replace", "refund", or "reject"');
|
|
6484
|
+
}
|
|
6485
|
+
const path = `/social/twitter/pool/disputes/${encodeURIComponent(id)}/resolve`;
|
|
6486
|
+
const headers = buildAdminHeaders('POST', path);
|
|
6487
|
+
const res = await fetch(ao.api + path, {
|
|
6488
|
+
method: 'POST',
|
|
6489
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
6490
|
+
body: JSON.stringify({ action, ...(note ? { note } : {}) }),
|
|
6491
|
+
});
|
|
6492
|
+
const data = await res.json();
|
|
6493
|
+
if (!res.ok || !data.success)
|
|
6494
|
+
err(`pool-resolve-dispute failed: ${data.error || `HTTP ${res.status}`}`, EXIT.GENERAL);
|
|
6495
|
+
return print(data);
|
|
5860
6496
|
}
|
|
5861
6497
|
case 'pool-add': {
|
|
5862
6498
|
const { buildAdminHeaders } = await import('./admin-auth.js');
|
|
@@ -5941,14 +6577,22 @@ async function main() {
|
|
|
5941
6577
|
return print(data);
|
|
5942
6578
|
}
|
|
5943
6579
|
case 'status': {
|
|
5944
|
-
|
|
6580
|
+
// `twitter status` was meant to check live shadow-ban / suspension
|
|
6581
|
+
// state via a server-side probe. That's a Phase 3 build (needs a
|
|
6582
|
+
// browser runtime on the server to render the profile). Until
|
|
6583
|
+
// then, point users at `session` for the cached login state and
|
|
6584
|
+
// `info` for vault metadata — together they cover the practical
|
|
6585
|
+
// "is this account still usable from the agent's side" question.
|
|
6586
|
+
err(`twitter status: not wired yet (Phase 3). Closest equivalents available today: ` +
|
|
6587
|
+
`\`palmyr twitter session <username>\` (cached server-side login validity) and ` +
|
|
6588
|
+
`\`palmyr twitter info <username>\` (local vault record).`, EXIT.GENERAL);
|
|
5945
6589
|
}
|
|
5946
6590
|
case 'transfer': {
|
|
5947
6591
|
// Hand the X account to another wallet. End-to-end one-command:
|
|
5948
6592
|
// 1. If the account is only in the local vault, auto-register it
|
|
5949
6593
|
// with the server (uploads encrypted creds; $0.01 USDC).
|
|
5950
6594
|
// 2. Server rotates the password and revokes other sessions
|
|
5951
|
-
// ($0.
|
|
6595
|
+
// ($0.01 USDC ownership proof).
|
|
5952
6596
|
// 3. Atomically flips ownership in the DB.
|
|
5953
6597
|
// Receiver picks up the rotated credentials via `palmyr twitter
|
|
5954
6598
|
// list` (which now surfaces server-side accounts) and/or `claim`.
|
|
@@ -6320,7 +6964,9 @@ async function main() {
|
|
|
6320
6964
|
case 'tiktok': {
|
|
6321
6965
|
const sv = await import('./social-vault.js');
|
|
6322
6966
|
const platform = 'tiktok';
|
|
6323
|
-
|
|
6967
|
+
// Same help guard as `twitter` — prevents `--help` from dispatching
|
|
6968
|
+
// a paid subcommand. Same bug class lived here too in 1.8.3.
|
|
6969
|
+
if (!subcommand || (flags.help && !TIKTOK_HELP[subcommand])) {
|
|
6324
6970
|
showMenu({
|
|
6325
6971
|
command: 'tiktok',
|
|
6326
6972
|
title: 'tiktok',
|
|
@@ -6347,6 +6993,10 @@ async function main() {
|
|
|
6347
6993
|
});
|
|
6348
6994
|
return;
|
|
6349
6995
|
}
|
|
6996
|
+
if (flags.help && subcommand && TIKTOK_HELP[subcommand]) {
|
|
6997
|
+
subcommandHelp('tiktok', subcommand, TIKTOK_HELP[subcommand]);
|
|
6998
|
+
return;
|
|
6999
|
+
}
|
|
6350
7000
|
switch (subcommand) {
|
|
6351
7001
|
case 'import': {
|
|
6352
7002
|
// Two formats:
|
|
@@ -6693,6 +7343,55 @@ async function main() {
|
|
|
6693
7343
|
'The Palmyr server fires them at post_at without any client process.', EXIT.BAD_INPUT);
|
|
6694
7344
|
break;
|
|
6695
7345
|
}
|
|
7346
|
+
case 'telemetry': {
|
|
7347
|
+
// Off by default. Opt-in only. We never auto-enable, never prompt at
|
|
7348
|
+
// startup, never write to stdout outside this command. Captured fields
|
|
7349
|
+
// and storage location are documented in cli/telemetry.ts.
|
|
7350
|
+
const action = (subcommand || 'status').toLowerCase();
|
|
7351
|
+
if (action !== 'on' && action !== 'off' && action !== 'status') {
|
|
7352
|
+
err(`Unknown telemetry action: ${action}. Use: on | off | status`, EXIT.BAD_INPUT);
|
|
7353
|
+
}
|
|
7354
|
+
let state;
|
|
7355
|
+
if (action === 'on')
|
|
7356
|
+
state = setTelemetryEnabled(true);
|
|
7357
|
+
else if (action === 'off')
|
|
7358
|
+
state = setTelemetryEnabled(false);
|
|
7359
|
+
else
|
|
7360
|
+
state = getTelemetryState();
|
|
7361
|
+
const payload = {
|
|
7362
|
+
enabled: state.enabled,
|
|
7363
|
+
installId: state.installId || null,
|
|
7364
|
+
optedInAt: state.optedInAt || null,
|
|
7365
|
+
queuedEvents: telemetryQueuedCount(),
|
|
7366
|
+
captures: ['cmd', 'exitCode', 'durationMs', 'cliVersion', 'nodeVersion', 'platform'],
|
|
7367
|
+
neverCaptures: ['flag values', 'positional args', 'stdout/stderr', 'wallet addresses', 'phone numbers', 'any user input'],
|
|
7368
|
+
};
|
|
7369
|
+
if (AGENT_MODE) {
|
|
7370
|
+
print(payload);
|
|
7371
|
+
}
|
|
7372
|
+
else {
|
|
7373
|
+
const status = state.enabled ? `${t.success}on${t.reset}` : `${t.muted}off${t.reset}`;
|
|
7374
|
+
console.log(`Telemetry: ${status}`);
|
|
7375
|
+
if (state.installId)
|
|
7376
|
+
console.log(`Install ID: ${t.muted}${state.installId}${t.reset}`);
|
|
7377
|
+
if (state.optedInAt)
|
|
7378
|
+
console.log(`Opted in: ${state.optedInAt}`);
|
|
7379
|
+
if (payload.queuedEvents)
|
|
7380
|
+
console.log(`Queued: ${payload.queuedEvents} event(s) waiting to send`);
|
|
7381
|
+
console.log('');
|
|
7382
|
+
console.log(`Captures: ${payload.captures.join(', ')}`);
|
|
7383
|
+
console.log(`Never: flag values, positional args, stdout, user input`);
|
|
7384
|
+
if (!state.enabled) {
|
|
7385
|
+
console.log('');
|
|
7386
|
+
console.log(`Opt in: ${t.accent}palmyr telemetry on${t.reset}`);
|
|
7387
|
+
}
|
|
7388
|
+
else {
|
|
7389
|
+
console.log('');
|
|
7390
|
+
console.log(`Opt out: ${t.accent}palmyr telemetry off${t.reset} (drops any queued events)`);
|
|
7391
|
+
}
|
|
7392
|
+
}
|
|
7393
|
+
break;
|
|
7394
|
+
}
|
|
6696
7395
|
case 'config': {
|
|
6697
7396
|
const cfg = loadConfig();
|
|
6698
7397
|
const { homedir } = await import('os');
|