@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 +9 -1
- package/dist/cli.js +305 -5
- package/dist/cli.js.map +1 -1
- package/dist/sdk.d.ts +9 -0
- package/dist/sdk.js +23 -0
- package/dist/sdk.js.map +1 -1
- package/dist/social-vault.d.ts +8 -0
- package/dist/social-vault.js +20 -0
- package/dist/social-vault.js.map +1 -1
- package/dist/wallet-daemon.d.ts +2 -0
- package/dist/wallet-daemon.js +5 -2
- package/dist/wallet-daemon.js.map +1 -1
- package/dist/wallet-live-test.d.ts +49 -0
- package/dist/wallet-live-test.js +162 -0
- package/dist/wallet-live-test.js.map +1 -0
- package/dist/wallet-readiness.d.ts +51 -0
- package/dist/wallet-readiness.js +183 -0
- package/dist/wallet-readiness.js.map +1 -0
- package/dist/wallet-trading.d.ts +9 -0
- package/dist/wallet-trading.js +11 -0
- package/dist/wallet-trading.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|