@palmyr/cli 1.4.0 → 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 +187 -4
- 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/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
|
}
|
|
@@ -3993,6 +4017,10 @@ async function main() {
|
|
|
3993
4017
|
{ name: 'login', description: 'Force a fresh server-side session (requires browser runtime)', hint: '<username>' },
|
|
3994
4018
|
{ name: 'post', description: 'Post a tweet (requires server browser runtime)', hint: '<username> --body "..."' },
|
|
3995
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' },
|
|
3996
4024
|
],
|
|
3997
4025
|
fromHome,
|
|
3998
4026
|
});
|
|
@@ -4889,8 +4917,163 @@ async function main() {
|
|
|
4889
4917
|
case 'status': {
|
|
4890
4918
|
err(`twitter status: not wired yet. Phase 3 will add it.`, EXIT.GENERAL);
|
|
4891
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
|
+
}
|
|
4892
5075
|
default:
|
|
4893
|
-
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`);
|
|
4894
5077
|
}
|
|
4895
5078
|
break;
|
|
4896
5079
|
}
|