@palmyr/cli 1.3.1 → 1.5.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 CHANGED
@@ -365,7 +365,11 @@ palmyr compute exec my-vps -- bash -c 'cloud-init clean && cloud-init init --all
365
365
  | `palmyr domain check --name example.dev` | free | Availability check. |
366
366
  | `palmyr domain pricing --name example.dev` | free | TLD pricing. |
367
367
  | `palmyr domain buy --name example.dev` | $20.00 | One-year registration. Renewals are charged annually. |
368
- | `palmyr domain dns --name example.dev` | free | View DNS records. |
368
+ | `palmyr domain list` | $0.0001 *(ownership proof)* | List domains your wallet owns plus any shared with you. Each row tagged `access: owner | shared`. |
369
+ | `palmyr domain dns --name example.dev` | $0.0001 *(ownership proof)* | View DNS records. Owners and shared wallets allowed. |
370
+ | `palmyr domain transfer-ownership --name example.dev --to <wallet>` | $0.0001 *(ownership proof)* | Hand the domain to another wallet. Clears `shared_with` — the prior owner's collaborators don't travel with the domain. |
371
+ | `palmyr domain share --name example.dev --with <wallet>` | $0.0001 *(ownership proof)* | Grant another wallet shared access (visible in `domain list`, can edit DNS). Owner-only. |
372
+ | `palmyr domain unshare --name example.dev --from <wallet>` | $0.0001 *(ownership proof)* | Revoke a shared wallet's access. Owner-only. |
369
373
 
370
374
  ### Wallet
371
375
 
@@ -518,6 +522,10 @@ Local credentials are encrypted with AES-256-GCM (per-account session secret in
518
522
  | `palmyr twitter pfp <username> --file path.png` *(or `--url https://...`)* | $0.005 | PNG / JPG / WebP / GIF. Local file is base64-encoded; URL is fetched server-side with SSRF guard. |
519
523
  | `palmyr twitter banner <username> --file path.png` *(or `--url ...`)* | $0.005 | |
520
524
  | `palmyr twitter username <username> --to <new-handle>` | $0.005 | Pre-flight validates handle (4–15 chars, `[A-Za-z0-9_]`) before payment. May trigger X's password re-auth modal — handled automatically. |
525
+ | `palmyr twitter transfer <username> --to <wallet> --confirm` | $0.0001 *(ownership proof)* | Atomically hand the X account to another wallet. Server rotates the password and revokes other sessions before flipping `sold_to`, so the local copy of credentials becomes useless. Requires `--confirm`. Local vault entry is removed on success — receiver picks up fresh creds with `palmyr twitter claim`. |
526
+ | `palmyr twitter share <username> --with <wallet>` | $0.0001 *(ownership proof)* | Grant another wallet shared access — same login, no credential rotation. Both wallets see the account via `palmyr twitter claim`. Owner-only. |
527
+ | `palmyr twitter unshare <username> --from <wallet> [--rotate]` | $0.0001 *(ownership proof; rotation runs through Playwright when `--rotate`)* | Revoke a wallet's shared access. Without `--rotate`, the wallet is removed from `shared_with` but their previously exported cookies / password remain valid until X-side expiry. With `--rotate`, the server also rotates the password and revokes other sessions, then the CLI updates the local vault in place. Owner-only. |
528
+ | `palmyr twitter claim` | $0.0001 *(ownership proof)* | Pull every X account on the server bound to your wallet (owner or shared) into the local vault, with session cookies pre-warmed. The fast path for a wallet that just received a transferred account. |
521
529
 
522
530
  **Verification.** Operations are confirmed at the network layer — the server intercepts X's actual API responses (`CreateTweet`, `FavoriteTweet`, `update_profile`, etc.) before reporting success. No false positives.
523
531
 
package/dist/cli.js CHANGED
@@ -17,7 +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 { theme as t, icon, Spinner, table, kv, section, setAgentMode as setUiAgentMode } from './ui.js';
20
+ import { theme as t, icon, Spinner, warn, table, kv, section, setAgentMode as setUiAgentMode } from './ui.js';
21
21
  import { existsSync, readFileSync } from 'fs';
22
22
  import { homedir } from 'os';
23
23
  import { fileURLToPath } from 'url';
@@ -1462,9 +1462,11 @@ async function main() {
1462
1462
  { name: 'check', description: 'Check availability', hint: '--name example.dev' },
1463
1463
  { name: 'pricing', description: 'Get TLD pricing', hint: '--name example' },
1464
1464
  { name: 'buy', description: 'Register a domain', hint: '--name example.dev' },
1465
- { name: 'list', description: 'List domains owned by your wallet', hint: '' },
1465
+ { name: 'list', description: 'List domains owned or shared with your wallet', hint: '' },
1466
1466
  { name: 'dns', description: 'Get DNS records', hint: '--name example.dev' },
1467
1467
  { name: 'transfer-ownership', description: 'Transfer domain to another wallet', hint: '--name example.dev --to <wallet>' },
1468
+ { name: 'share', description: 'Grant another wallet shared access', hint: '--name example.dev --with <wallet>' },
1469
+ { name: 'unshare', description: 'Revoke a wallet’s shared access', hint: '--name example.dev --from <wallet>' },
1468
1470
  ],
1469
1471
  fromHome,
1470
1472
  });
@@ -1582,6 +1584,28 @@ async function main() {
1582
1584
  const data = await ao.domainTransferOwnership(name, to);
1583
1585
  return print(data);
1584
1586
  }
1587
+ case 'share': {
1588
+ const name = flags.name || positional[0];
1589
+ const withWallet = flags.with || flags.wallet;
1590
+ if (!name)
1591
+ err('--name domain.dev required');
1592
+ if (!withWallet)
1593
+ err('--with <wallet> required');
1594
+ const data = await ao.domainShare(name, withWallet);
1595
+ log(`domain share: ${name} → ${withWallet}`);
1596
+ return print(data);
1597
+ }
1598
+ case 'unshare': {
1599
+ const name = flags.name || positional[0];
1600
+ const targetWallet = flags.from || flags.wallet;
1601
+ if (!name)
1602
+ err('--name domain.dev required');
1603
+ if (!targetWallet)
1604
+ err('--from <wallet> required');
1605
+ const data = await ao.domainUnshare(name, targetWallet);
1606
+ log(`domain unshare: ${name} ✗ ${targetWallet}`);
1607
+ return print(data);
1608
+ }
1585
1609
  case 'dns': {
1586
1610
  const name = flags.name || positional[0];
1587
1611
  if (!name)
@@ -1600,7 +1624,7 @@ async function main() {
1600
1624
  }));
1601
1625
  break;
1602
1626
  }
1603
- default: err(`Unknown domain command: ${subcommand}. Try: check, pricing, buy, list, dns, transfer-ownership`);
1627
+ default: err(`Unknown domain command: ${subcommand}. Try: check, pricing, buy, list, dns, transfer-ownership, share, unshare`);
1604
1628
  }
1605
1629
  break;
1606
1630
  }
@@ -1636,6 +1660,8 @@ async function main() {
1636
1660
  { name: 'brief', description: 'Show thesis + PnL brief for a position', hint: '<CA>' },
1637
1661
  { name: 'doctor', description: 'Health check for the wallet-trading subsystem', hint: '[--wallet <ref>]' },
1638
1662
  { name: 'smoke-test', description: 'End-to-end validation of wallet trading on Solana + Base', hint: '--wallet <ref> [--chain solana|base|all]' },
1663
+ { name: 'readiness', description: 'Go/no-go autonomous-trading readiness — sign, gas, quotes, daemon, open positions', hint: '--wallet <ref>' },
1664
+ { name: 'live-test', description: 'Execute tiny real round trips on Solana + Base, verify no leftover positions', hint: '--wallet <ref> --budget Nusdc [--chain ...]' },
1639
1665
  { name: 'daemon', description: 'Auto-monitor positions for trigger-based exits', hint: 'tick | start [--auto] | stop | status' },
1640
1666
  { name: 'triggers', description: 'List pending trigger fires from the daemon', hint: '[--ca X] [--since ISO] [--clear]' },
1641
1667
  { name: 'trading-keystore', description: 'Encrypted BIP39 keystore for HD-derived trading wallets', hint: 'init | list | status | derive | export' },
@@ -3021,14 +3047,19 @@ async function main() {
3021
3047
  const p = readPosition(inferredChain, ca, scopedAddr);
3022
3048
  if (!p)
3023
3049
  err(`Position not found: ${ca}`, EXIT.NOT_FOUND);
3050
+ // `--evaluate` degrades gracefully: a missing ANTHROPIC_API_KEY or
3051
+ // a model-API failure must NOT take down the whole brief. We surface
3052
+ // the LLM error as `llmError` so agents can branch on it, and still
3053
+ // print the deterministic brief fields.
3024
3054
  let llm;
3055
+ let llmError;
3025
3056
  if (evaluate) {
3026
3057
  const { evaluateBriefWithLLM } = await import('./wallet-brief-llm.js');
3027
3058
  try {
3028
3059
  llm = await evaluateBriefWithLLM(p);
3029
3060
  }
3030
3061
  catch (e) {
3031
- err(e.message || 'brief --evaluate failed', EXIT.GENERAL);
3062
+ llmError = e?.message ?? 'brief --evaluate failed';
3032
3063
  }
3033
3064
  }
3034
3065
  if (!AGENT_MODE) {
@@ -3061,6 +3092,10 @@ async function main() {
3061
3092
  console.log(` ${t.muted}Reasoning:${t.reset} ${llm.reasoning}`);
3062
3093
  console.log(` ${t.muted}Watch for:${t.reset} ${llm.watchFor}`);
3063
3094
  }
3095
+ else if (llmError) {
3096
+ console.log();
3097
+ console.log(` ${t.warn}LLM eval skipped: ${llmError}${t.reset}`);
3098
+ }
3064
3099
  else {
3065
3100
  console.log();
3066
3101
  console.log(` ${t.muted}Add --evaluate for an LLM thesis-health check.${t.reset}`);
@@ -3078,6 +3113,7 @@ async function main() {
3078
3113
  pnl: p.pnl,
3079
3114
  sellsCount: p.sells.length,
3080
3115
  llm,
3116
+ llmError,
3081
3117
  });
3082
3118
  }
3083
3119
  break;
@@ -3176,6 +3212,111 @@ async function main() {
3176
3212
  process.exit(EXIT.GENERAL);
3177
3213
  break;
3178
3214
  }
3215
+ case 'readiness': {
3216
+ const readyWalletRef = flags.wallet || undefined;
3217
+ if (!readyWalletRef)
3218
+ err('--wallet required. Use a vault wallet name/id or `trading:N`.', EXIT.BAD_INPUT);
3219
+ const { runWalletReadiness } = await import('./wallet-readiness.js');
3220
+ const report = await runWalletReadiness({ walletRef: readyWalletRef });
3221
+ if (AGENT_MODE) {
3222
+ print(report);
3223
+ if (!report.safeForAutonomousTrading)
3224
+ process.exit(EXIT.GENERAL);
3225
+ break;
3226
+ }
3227
+ console.log();
3228
+ section('Wallet readiness');
3229
+ kv('Wallet', report.wallet);
3230
+ if (report.solanaAddress)
3231
+ kv('Solana', report.solanaAddress);
3232
+ if (report.evmAddress)
3233
+ kv('EVM', report.evmAddress);
3234
+ kv('Verdict', report.safeForAutonomousTrading
3235
+ ? `${t.success}safe for autonomous trading${t.reset}`
3236
+ : `${t.error}NOT safe — see failing checks${t.reset}`);
3237
+ if (report.balances.solana)
3238
+ kv('SOL balance', `${report.balances.solana.sol.toFixed(6)} SOL`);
3239
+ if (report.balances.base)
3240
+ kv('ETH balance', `${report.balances.base.eth.toFixed(8)} ETH`);
3241
+ kv('Open positions', `solana=${report.openPositions.solana} base=${report.openPositions.base}`);
3242
+ kv('Daemon', report.daemon.running
3243
+ ? `${t.success}running${t.reset} (pid ${report.daemon.pid}${report.daemon.autoExecute ? ', auto-execute' : ''})`
3244
+ : `${t.warn}not running${t.reset}`);
3245
+ console.log();
3246
+ section('Checks');
3247
+ for (const c of report.checks) {
3248
+ const dot = c.status === 'pass' ? `${t.success}✓${t.reset}`
3249
+ : c.status === 'warn' ? `${t.warn}!${t.reset}`
3250
+ : c.status === 'skip' ? `${t.muted}-${t.reset}`
3251
+ : `${t.error}✗${t.reset}`;
3252
+ const tail = [c.value !== undefined ? String(c.value) : null, c.message].filter(Boolean).join(' — ');
3253
+ console.log(` ${dot} ${c.name}${tail ? `: ${tail}` : ''}`);
3254
+ }
3255
+ console.log();
3256
+ if (!report.safeForAutonomousTrading)
3257
+ process.exit(EXIT.GENERAL);
3258
+ break;
3259
+ }
3260
+ case 'live-test': {
3261
+ const liveWalletRef = flags.wallet || undefined;
3262
+ if (!liveWalletRef)
3263
+ err('--wallet required. Use a vault wallet name/id or `trading:N`.', EXIT.BAD_INPUT);
3264
+ const budgetRaw = flags.budget;
3265
+ if (!budgetRaw)
3266
+ err('--budget required, e.g. --budget 1usdc (caps total trade exposure).', EXIT.BAD_INPUT);
3267
+ const budgetMatch = (budgetRaw ?? '').trim().match(/^(\d+(?:\.\d+)?)\s*usdc$/i);
3268
+ if (!budgetMatch)
3269
+ err(`--budget must be in USDC (e.g. "0.5usdc", "1usdc"), got "${budgetRaw}".`, EXIT.BAD_INPUT);
3270
+ const budgetUsdc = Number(budgetMatch[1]);
3271
+ const liveChainFlag = (flags.chain || 'all').toLowerCase();
3272
+ if (liveChainFlag !== 'solana' && liveChainFlag !== 'base' && liveChainFlag !== 'all') {
3273
+ err(`--chain must be solana, base, or all (got ${liveChainFlag})`, EXIT.BAD_INPUT);
3274
+ }
3275
+ const { runWalletLiveTest } = await import('./wallet-live-test.js');
3276
+ let report;
3277
+ try {
3278
+ report = await runWalletLiveTest({
3279
+ walletRef: liveWalletRef,
3280
+ budgetUsdc,
3281
+ chain: liveChainFlag,
3282
+ });
3283
+ }
3284
+ catch (e) {
3285
+ err(e.message || 'live-test failed', EXIT.GENERAL);
3286
+ }
3287
+ if (AGENT_MODE) {
3288
+ print(report);
3289
+ if (!report.safeForAutonomousTrading)
3290
+ process.exit(EXIT.GENERAL);
3291
+ break;
3292
+ }
3293
+ console.log();
3294
+ section('Wallet live-test');
3295
+ kv('Wallet', report.wallet);
3296
+ kv('Budget', `${report.budgetUsdc.toFixed(6)} USDC (per leg ${report.perLegUsdc.toFixed(6)} USDC)`);
3297
+ kv('Verdict', report.safeForAutonomousTrading
3298
+ ? `${t.success}safe for autonomous trading${t.reset}`
3299
+ : `${t.error}NOT safe — see failing legs${t.reset}`);
3300
+ kv('Total realized', `${report.totalRealizedUsdc >= 0 ? '+' : ''}${report.totalRealizedUsdc.toFixed(6)} USDC`);
3301
+ kv('Open positions after', String(report.openPositionsAfter));
3302
+ console.log();
3303
+ section('Legs');
3304
+ for (const leg of report.legs) {
3305
+ const dot = leg.status === 'pass' ? `${t.success}✓${t.reset}`
3306
+ : leg.status === 'skip' ? `${t.muted}-${t.reset}`
3307
+ : `${t.error}✗${t.reset}`;
3308
+ const realizedStr = leg.realized
3309
+ ? `; realized ${leg.realized.amount >= 0 ? '+' : ''}${leg.realized.amount.toFixed(6)} ${leg.realized.asset}`
3310
+ : '';
3311
+ const txStr = leg.txHash ? ` (${leg.txHash.slice(0, 10)}…)` : '';
3312
+ const detail = [leg.message, leg.durationMs !== undefined ? `${leg.durationMs}ms` : null].filter(Boolean).join(' — ');
3313
+ console.log(` ${dot} ${leg.chain}/${leg.name}${txStr}${realizedStr}${detail ? `: ${detail}` : ''}`);
3314
+ }
3315
+ console.log();
3316
+ if (!report.safeForAutonomousTrading)
3317
+ process.exit(EXIT.GENERAL);
3318
+ break;
3319
+ }
3179
3320
  case 'daemon': {
3180
3321
  const sub = positional[0];
3181
3322
  if (!sub)
@@ -3876,6 +4017,10 @@ async function main() {
3876
4017
  { name: 'login', description: 'Force a fresh server-side session (requires browser runtime)', hint: '<username>' },
3877
4018
  { name: 'post', description: 'Post a tweet (requires server browser runtime)', hint: '<username> --body "..."' },
3878
4019
  { name: 'status', description: 'Check if the account is alive / shadow-banned', hint: '<username>' },
4020
+ { name: 'transfer', description: 'Hand an account to another wallet (rotates password)', hint: '<username> --to <wallet>' },
4021
+ { name: 'share', description: 'Grant another wallet shared access', hint: '<username> --with <wallet>' },
4022
+ { name: 'unshare', description: 'Revoke a wallet’s shared access', hint: '<username> --from <wallet> [--rotate]' },
4023
+ { name: 'claim', description: 'Import server-side accounts owned by your wallet into the local vault' },
3879
4024
  ],
3880
4025
  fromHome,
3881
4026
  });
@@ -4772,8 +4917,163 @@ async function main() {
4772
4917
  case 'status': {
4773
4918
  err(`twitter status: not wired yet. Phase 3 will add it.`, EXIT.GENERAL);
4774
4919
  }
4920
+ case 'transfer': {
4921
+ // Hand the X account to another wallet. Server rotates the password
4922
+ // and revokes other sessions before flipping ownership, so the local
4923
+ // copy of credentials we keep is intentionally invalidated. After
4924
+ // success, we wipe the local vault entry — receiver will pick up
4925
+ // fresh credentials via `palmyr twitter claim`.
4926
+ const username = positional[0] || flags.username;
4927
+ const to = flags.to;
4928
+ if (!username)
4929
+ err('<username> required');
4930
+ if (!to)
4931
+ err('--to <wallet> required');
4932
+ const acc = sv.getAccount(platform, username);
4933
+ if (!acc)
4934
+ err(`twitter account "${username}" not found locally — can only transfer accounts you own`, EXIT.NOT_FOUND);
4935
+ if (!flags.confirm) {
4936
+ const tail = to.slice(-6);
4937
+ err(`This rotates @${username} on X and hands it to a wallet ending in …${tail}. ` +
4938
+ `You will lose access immediately and irreversibly. ` +
4939
+ `Re-run with --confirm:\n` +
4940
+ ` palmyr twitter transfer ${username} --to ${to} --confirm`);
4941
+ }
4942
+ const spin = new Spinner();
4943
+ spin.start(`Rotating @${username} password and transferring…`);
4944
+ let data;
4945
+ try {
4946
+ data = await ao.xAccountTransfer(acc.id, to);
4947
+ }
4948
+ catch (e) {
4949
+ spin.stop('Transfer failed', false);
4950
+ err(`Transfer failed: ${e.message}`, EXIT.GENERAL);
4951
+ }
4952
+ spin.stop('Transferred', true);
4953
+ // The local vault still holds the OLD password / cookies which are
4954
+ // now useless. Drop the entry so we don't confuse the user with a
4955
+ // ghost account they can't log into.
4956
+ try {
4957
+ sv.removeAccount(platform, username);
4958
+ }
4959
+ catch { /* best effort */ }
4960
+ return print({ ...data, local_vault_cleared: true });
4961
+ }
4962
+ case 'share': {
4963
+ const username = positional[0] || flags.username;
4964
+ const withWallet = flags.with || flags.wallet;
4965
+ if (!username)
4966
+ err('<username> required');
4967
+ if (!withWallet)
4968
+ err('--with <wallet> required');
4969
+ const acc = sv.getAccount(platform, username);
4970
+ if (!acc)
4971
+ err(`twitter account "${username}" not found locally`, EXIT.NOT_FOUND);
4972
+ const data = await ao.xAccountShare(acc.id, withWallet);
4973
+ log(`twitter share: @${username} → ${withWallet}`);
4974
+ return print(data);
4975
+ }
4976
+ case 'unshare': {
4977
+ const username = positional[0] || flags.username;
4978
+ const targetWallet = flags.from || flags.wallet;
4979
+ const rotate = !!flags.rotate;
4980
+ if (!username)
4981
+ err('<username> required');
4982
+ if (!targetWallet)
4983
+ err('--from <wallet> required');
4984
+ const acc = sv.getAccount(platform, username);
4985
+ if (!acc)
4986
+ err(`twitter account "${username}" not found locally`, EXIT.NOT_FOUND);
4987
+ const spin = rotate ? new Spinner() : null;
4988
+ if (spin)
4989
+ spin.start(`Unsharing @${username} and rotating password…`);
4990
+ let data;
4991
+ try {
4992
+ data = await ao.xAccountUnshare(acc.id, targetWallet, { rotate });
4993
+ }
4994
+ catch (e) {
4995
+ if (spin)
4996
+ spin.stop('Unshare failed', false);
4997
+ err(`Unshare failed: ${e.message}`, EXIT.GENERAL);
4998
+ }
4999
+ if (spin)
5000
+ spin.stop(data?.rotated ? 'Unshared and rotated' : 'Unshared (rotation skipped)', !!data?.rotated);
5001
+ // If rotation succeeded, sync the local vault so the next op
5002
+ // doesn't try to log in with the now-defunct password / cookies.
5003
+ if (rotate && data?.rotated && data?.credentials) {
5004
+ try {
5005
+ const existing = sv.unlockCredentials(platform, username);
5006
+ const next = {
5007
+ ...existing,
5008
+ password: data.credentials.password,
5009
+ auth_token: data.credentials.auth_token || undefined,
5010
+ ct0: (data.credentials.cookies || []).find((c) => c.name === 'ct0')?.value || existing.ct0,
5011
+ };
5012
+ sv.replaceCredentials(platform, username, next);
5013
+ if (Array.isArray(data.credentials.cookies) && data.credentials.cookies.length > 0) {
5014
+ sv.saveSession(acc.id, platform, data.credentials.cookies);
5015
+ }
5016
+ // Don't leak the new password into the printed JSON output —
5017
+ // it's already persisted locally.
5018
+ data = { ...data, credentials: { rotated: true, persisted_locally: true } };
5019
+ }
5020
+ catch (e) {
5021
+ warn(`Local vault sync failed: ${e.message}. Run 'palmyr twitter claim' to refresh from server.`);
5022
+ }
5023
+ }
5024
+ log(`twitter unshare: @${username} ✗ ${targetWallet}${rotate ? ' (rotated)' : ''}`);
5025
+ return print(data);
5026
+ }
5027
+ case 'claim': {
5028
+ // Fetch every server-side X account this wallet owns or has shared
5029
+ // access to. Optionally import any not yet in the local vault —
5030
+ // typical use is the new owner of a transferred account picking it
5031
+ // up for the first time.
5032
+ const data = await ao.xAccountsMine();
5033
+ const accounts = data?.accounts || [];
5034
+ if (accounts.length === 0) {
5035
+ log('No X accounts associated with your wallet on the server.');
5036
+ return print({ count: 0, claimed: 0, accounts: [] });
5037
+ }
5038
+ const imported = [];
5039
+ const skipped = [];
5040
+ for (const a of accounts) {
5041
+ const existing = sv.getAccount(platform, a.username);
5042
+ if (existing) {
5043
+ skipped.push({ username: a.username, reason: 'already in local vault' });
5044
+ continue;
5045
+ }
5046
+ try {
5047
+ const ct0 = (a.cookies || []).find((c) => c.name === 'ct0')?.value;
5048
+ const creds = {
5049
+ login: a.email || a.username,
5050
+ password: a.password,
5051
+ email: a.email,
5052
+ auth_token: a.auth_token || undefined,
5053
+ ct0,
5054
+ };
5055
+ const summary = sv.importAccount(platform, a.username, creds, { source: 'claim' });
5056
+ // Save the cookies so `palmyr twitter login` can use the
5057
+ // cookie-fast-path instead of re-driving the login form.
5058
+ if (Array.isArray(a.cookies) && a.cookies.length > 0) {
5059
+ sv.saveSession(summary.id, platform, a.cookies);
5060
+ }
5061
+ imported.push({ username: a.username, id: summary.id, access: a.access });
5062
+ }
5063
+ catch (e) {
5064
+ skipped.push({ username: a.username, reason: e.message });
5065
+ }
5066
+ }
5067
+ log(`twitter claim: imported ${imported.length}, skipped ${skipped.length} (of ${accounts.length})`);
5068
+ return print({
5069
+ count: accounts.length,
5070
+ claimed: imported.length,
5071
+ imported,
5072
+ skipped,
5073
+ });
5074
+ }
4775
5075
  default:
4776
- err(`Unknown twitter command: ${subcommand}. Try: import, list, info, rename, remove, totp, login, manual-login, session, post, reply, like, retweet, follow, unfollow, delete, list-tweets, bio, name, location, website, pfp, banner, username, buy`);
5076
+ err(`Unknown twitter command: ${subcommand}. Try: import, list, info, rename, remove, totp, login, manual-login, session, post, reply, like, retweet, follow, unfollow, delete, list-tweets, bio, name, location, website, pfp, banner, username, buy, transfer, share, unshare, claim`);
4777
5077
  }
4778
5078
  break;
4779
5079
  }