@palmyr/cli 1.8.5 → 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/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';
@@ -776,9 +777,13 @@ const TWITTER_HELP = {
776
777
  { flag: '(price)', desc: 'Free — local TOTP generation' },
777
778
  ],
778
779
  buy: [
779
- { flag: '(no args)', desc: 'Purchase the oldest ready X account from the supplier pool' },
780
- { flag: '(price)', desc: '$5 USDC paid via x402. Account auto-imported into the local vault and session primed.' },
781
- { flag: '(example)', desc: 'palmyr twitter buy' },
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' },
782
787
  ],
783
788
  login: [
784
789
  { flag: '<username>', desc: 'Force a fresh server-side session (browser runtime)' },
@@ -940,12 +945,62 @@ const TWITTER_HELP = {
940
945
  'pool-add': [
941
946
  { flag: '--credentials-line "..."', desc: 'Single account creds (login:pw:email:email_pw[:2fa[:ct0:auth_token]])' },
942
947
  { flag: '--file path.txt', desc: 'Bulk: one credentials-line per row (# = comment)' },
943
- { flag: '--price <USDC>', desc: 'Required — what `twitter buy` will charge per account' },
944
- { flag: '--country <CC>', desc: 'Optional metadata' },
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).' },
945
950
  { flag: '--age 1y|2y|3y|...', desc: 'Optional age category metadata' },
946
951
  { flag: '(auth)', desc: 'Admin-signed call — requires PALMYR_ADMIN_KEY' },
947
952
  { flag: '(price)', desc: 'Free — server-side seeding by pool operator' },
948
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
+ ],
949
1004
  'pool-status': [
950
1005
  { flag: '(no args)', desc: 'Available / sold / reserved counts in the X account pool' },
951
1006
  { flag: '(auth)', desc: 'Admin-signed call — requires PALMYR_ADMIN_KEY' },
@@ -1133,6 +1188,7 @@ const TOP_LEVEL_COMMANDS = [
1133
1188
  { name: 'doctor', description: 'Verify system health (cred store, vault, API)' },
1134
1189
  { name: 'pricing', description: 'All service prices' },
1135
1190
  { name: 'health', description: 'API status + version check' },
1191
+ { name: 'telemetry', description: 'on · off · status (opt-in anonymous usage stats)' },
1136
1192
  ];
1137
1193
  // ─── Help ───
1138
1194
  function help() {
@@ -1226,6 +1282,22 @@ async function main() {
1226
1282
  const startTime = Date.now();
1227
1283
  // No first-time banner — agent-first CLI should never pollute output.
1228
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);
1229
1301
  const token = flags.token || config.apiKey || process.env.PALMYR_TOKEN || process.env.PALMYR_API_KEY;
1230
1302
  const passphrase = flags.passphrase || process.env.PALMYR_WALLET_PASSPHRASE;
1231
1303
  const ao = new Palmyr(url, true, token, passphrase);
@@ -6202,10 +6274,27 @@ async function main() {
6202
6274
  return print({ success: true, platform, username, op: subcommand, ...(data?.data || {}) });
6203
6275
  }
6204
6276
  case 'buy': {
6205
- // Agents just say "buy." Server picks the oldest ready account.
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
+ }
6206
6295
  let data;
6207
6296
  try {
6208
- data = await ao.socialTwitterBuy();
6297
+ data = await ao.socialTwitterBuy(country, ageCategory, { source, maxUsernameChanges });
6209
6298
  }
6210
6299
  catch (e) {
6211
6300
  err(`Buy failed: ${e.message}`, EXIT.GENERAL);
@@ -6217,10 +6306,15 @@ async function main() {
6217
6306
  // Auto-import into the local vault + prime the session cache so
6218
6307
  // the buyer can post immediately with the cookies the admin
6219
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(', ');
6220
6314
  const summary = sv.importAccount(platform, account.username, account.credentials, {
6221
6315
  source: 'pool',
6222
6316
  proxy_session_id: account.proxy_session_id,
6223
- notes: 'Bought from pool',
6317
+ notes: filterTags ? `Bought from pool (${filterTags})` : 'Bought from pool',
6224
6318
  });
6225
6319
  sv.saveSession(summary.id, platform, account.cookies || []);
6226
6320
  sv.updateMeta(platform, summary.username, { last_action_at: new Date().toISOString() });
@@ -6228,8 +6322,177 @@ async function main() {
6228
6322
  success: true,
6229
6323
  platform,
6230
6324
  username: summary.username,
6231
- hint: `Ready to post — try: palmyr twitter post ${summary.username} --body "gm"`,
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 }),
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 }),
6232
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);
6233
6496
  }
6234
6497
  case 'pool-add': {
6235
6498
  const { buildAdminHeaders } = await import('./admin-auth.js');
@@ -7080,6 +7343,55 @@ async function main() {
7080
7343
  'The Palmyr server fires them at post_at without any client process.', EXIT.BAD_INPUT);
7081
7344
  break;
7082
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
+ }
7083
7395
  case 'config': {
7084
7396
  const cfg = loadConfig();
7085
7397
  const { homedir } = await import('os');