@palmyr/cli 1.8.5 → 1.9.1

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,15 @@ 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 by RESIDENCY (X about_profile.account_based_in). ISO alpha-2 US, GB, DE, …. Run `pool-prices` to see what is priced.' },
782
+ { flag: '--registered-country <CC>', desc: 'Filter by REGISTRATION country — where the X account was created from (parsed from X "Connected via" string). May differ from --country.' },
783
+ { flag: '--platform android|ios|web', desc: 'Filter by registration platform (also parsed from X "Connected via" string).' },
784
+ { flag: '--max-renames N', desc: 'Cap username-change count. --max-renames 0 = never renamed. Rows with unknown rename count do not match.' },
785
+ { flag: '--source "raw string"', desc: 'Power-user: exact-match against the lowercased raw "Connected via" string. Used for fine-grained source_multiplier pricing.' },
786
+ { flag: '--age 1y|2y|...', desc: 'Optional age category filter' },
787
+ { flag: '(price)', desc: '$5 USDC default; country_price * source_multiplier when filters are passed.' },
788
+ { flag: '(example)', desc: 'palmyr twitter buy --country GB --registered-country GB --platform android --max-renames 0' },
782
789
  ],
783
790
  login: [
784
791
  { flag: '<username>', desc: 'Force a fresh server-side session (browser runtime)' },
@@ -940,12 +947,62 @@ const TWITTER_HELP = {
940
947
  'pool-add': [
941
948
  { flag: '--credentials-line "..."', desc: 'Single account creds (login:pw:email:email_pw[:2fa[:ct0:auth_token]])' },
942
949
  { 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' },
950
+ { flag: '--price <USDC>', desc: 'Required — per-account sale_price_usdc (legacy fallback when no country price row exists)' },
951
+ { 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
952
  { flag: '--age 1y|2y|3y|...', desc: 'Optional age category metadata' },
946
953
  { flag: '(auth)', desc: 'Admin-signed call — requires PALMYR_ADMIN_KEY' },
947
954
  { flag: '(price)', desc: 'Free — server-side seeding by pool operator' },
948
955
  ],
956
+ 'pool-prices': [
957
+ { flag: '(no args)', desc: 'List per-country prices set by the pool admin (public, free).' },
958
+ { flag: '(price)', desc: 'Free' },
959
+ ],
960
+ 'pool-set-price': [
961
+ { flag: '--country <CC>', desc: 'ISO 3166-1 alpha-2 country code (US, GB, DE, …)' },
962
+ { flag: '--price <USDC>', desc: 'USDC amount the `buy --country <CC>` route will charge' },
963
+ { flag: '(auth)', desc: 'Admin-signed call — requires PALMYR_ADMIN_KEY' },
964
+ { flag: '(price)', desc: 'Free' },
965
+ ],
966
+ 'pool-delete-price': [
967
+ { flag: '--country <CC>', desc: 'Country code to remove pricing for' },
968
+ { flag: '(auth)', desc: 'Admin-signed call — requires PALMYR_ADMIN_KEY' },
969
+ { flag: '(price)', desc: 'Free' },
970
+ ],
971
+ 'pool-set-source-multiplier': [
972
+ { flag: '--source web|mobile|<id>', desc: 'Source identifier (matches the `source` column populated by twitterapi.io)' },
973
+ { flag: '--multiplier <number>', desc: 'Positive scaling factor applied on top of country price when --source is passed at buy time. 1.0 = no change.' },
974
+ { flag: '(auth)', desc: 'Admin-signed call — requires PALMYR_ADMIN_KEY' },
975
+ { flag: '(price)', desc: 'Free' },
976
+ ],
977
+ 'pool-delete-source-multiplier': [
978
+ { flag: '--source <id>', desc: 'Source identifier to remove the multiplier for (buy still works, just reverts to multiplier=1.0)' },
979
+ { flag: '(auth)', desc: 'Admin-signed call — requires PALMYR_ADMIN_KEY' },
980
+ { flag: '(price)', desc: 'Free' },
981
+ ],
982
+ dispute: [
983
+ { flag: '<account_id>', desc: 'Account id returned by `twitter buy` (the 32-char hex)' },
984
+ { flag: '--reason suspended|other', desc: 'Default "suspended" — triggers auto-verify via twitterapi.io' },
985
+ { flag: '--evidence "..."', desc: 'Optional note shown to the admin if the dispute ends up in admin_review' },
986
+ { flag: '(price)', desc: '$0.01 USDC ownership-proof. 7-day window from purchase.' },
987
+ { flag: '(example)', desc: 'palmyr twitter dispute abcd1234… --reason suspended' },
988
+ ],
989
+ disputes: [
990
+ { flag: '<dispute_id>', desc: 'Look up the status of a previously filed dispute' },
991
+ { flag: '(price)', desc: '$0.001 USDC' },
992
+ ],
993
+ 'pool-disputes': [
994
+ { flag: '(no args)', desc: 'List every dispute in the system (admin)' },
995
+ { flag: '--status admin_review|pending|replaced|refunded|rejected', desc: 'Filter by status' },
996
+ { flag: '(auth)', desc: 'Admin-signed call — requires PALMYR_ADMIN_KEY' },
997
+ { flag: '(price)', desc: 'Free' },
998
+ ],
999
+ 'pool-resolve-dispute': [
1000
+ { flag: '<dispute_id>', desc: 'Id from `pool-disputes`' },
1001
+ { flag: '--action replace|refund|reject', desc: 'replace = grant same-country swap; refund = USDC back to payer; reject = decline' },
1002
+ { flag: '--note "..."', desc: 'Optional admin note appended to the resolution' },
1003
+ { flag: '(auth)', desc: 'Admin-signed call — requires PALMYR_ADMIN_KEY' },
1004
+ { flag: '(price)', desc: 'Free' },
1005
+ ],
949
1006
  'pool-status': [
950
1007
  { flag: '(no args)', desc: 'Available / sold / reserved counts in the X account pool' },
951
1008
  { flag: '(auth)', desc: 'Admin-signed call — requires PALMYR_ADMIN_KEY' },
@@ -1133,6 +1190,7 @@ const TOP_LEVEL_COMMANDS = [
1133
1190
  { name: 'doctor', description: 'Verify system health (cred store, vault, API)' },
1134
1191
  { name: 'pricing', description: 'All service prices' },
1135
1192
  { name: 'health', description: 'API status + version check' },
1193
+ { name: 'telemetry', description: 'on · off · status (opt-in anonymous usage stats)' },
1136
1194
  ];
1137
1195
  // ─── Help ───
1138
1196
  function help() {
@@ -1226,6 +1284,22 @@ async function main() {
1226
1284
  const startTime = Date.now();
1227
1285
  // No first-time banner — agent-first CLI should never pollute output.
1228
1286
  const url = process.env.PALMYR_API || config.api;
1287
+ // Opt-in telemetry. If the user has explicitly enabled it, queue this run's
1288
+ // exit-code + duration on shutdown (sync — async work in 'exit' is dropped)
1289
+ // and fire-and-forget any previously-queued events to the API right now.
1290
+ // Both no-ops when telemetry is off. Never blocks the user's command —
1291
+ // the flush races their work and Node exits once both finish.
1292
+ process.on('exit', (code) => {
1293
+ telemetryAppendEvent({
1294
+ cmd: subcommand ? `${command} ${subcommand}` : command,
1295
+ exitCode: code,
1296
+ durationMs: Date.now() - startTime,
1297
+ cliVersion: VERSION,
1298
+ nodeVersion: process.version,
1299
+ platform: process.platform,
1300
+ });
1301
+ });
1302
+ void telemetryFlushQueue(url);
1229
1303
  const token = flags.token || config.apiKey || process.env.PALMYR_TOKEN || process.env.PALMYR_API_KEY;
1230
1304
  const passphrase = flags.passphrase || process.env.PALMYR_WALLET_PASSPHRASE;
1231
1305
  const ao = new Palmyr(url, true, token, passphrase);
@@ -6202,10 +6276,42 @@ async function main() {
6202
6276
  return print({ success: true, platform, username, op: subcommand, ...(data?.data || {}) });
6203
6277
  }
6204
6278
  case 'buy': {
6205
- // Agents just say "buy." Server picks the oldest ready account.
6279
+ // Agents say "buy" with optional filters. Each is independent
6280
+ // (default: random across that dimension).
6281
+ //
6282
+ // --country GB account_based_in = United Kingdom (residency)
6283
+ // --registered-country GB X's "Connected via …" country
6284
+ // --platform android|ios|web X's "Connected via" platform
6285
+ // --max-renames 0 never-renamed accounts only
6286
+ // --source "raw string" exact-match on the raw "Connected via" string (power user)
6287
+ //
6288
+ // Pricing = country_price * source_multiplier (multiplier
6289
+ // defaults to 1.0 when no source row exists). Without --country,
6290
+ // falls back to the legacy $5 flat rate.
6291
+ const country = (flags.country || '').trim().toUpperCase() || undefined;
6292
+ const ageCategory = flags.age || flags['age-category'] || undefined;
6293
+ const source = (flags.source || '').trim().toLowerCase() || undefined;
6294
+ const registeredCountry = (flags['registered-country'] || '').trim().toUpperCase() || undefined;
6295
+ const platformFlag = (flags.platform || '').trim().toLowerCase() || undefined;
6296
+ if (platformFlag && !['android', 'ios', 'web'].includes(platformFlag)) {
6297
+ err('--platform must be one of: android, ios, web');
6298
+ }
6299
+ const registeredPlatform = platformFlag;
6300
+ const maxRenamesRaw = flags['max-renames'];
6301
+ const maxUsernameChanges = maxRenamesRaw === undefined || maxRenamesRaw === ''
6302
+ ? undefined
6303
+ : Number(maxRenamesRaw);
6304
+ if (maxUsernameChanges !== undefined && (!Number.isFinite(maxUsernameChanges) || maxUsernameChanges < 0)) {
6305
+ err('--max-renames must be a non-negative integer (e.g. 0 = never renamed)');
6306
+ }
6206
6307
  let data;
6207
6308
  try {
6208
- data = await ao.socialTwitterBuy();
6309
+ data = await ao.socialTwitterBuy(country, ageCategory, {
6310
+ source,
6311
+ registeredCountry,
6312
+ registeredPlatform,
6313
+ maxUsernameChanges,
6314
+ });
6209
6315
  }
6210
6316
  catch (e) {
6211
6317
  err(`Buy failed: ${e.message}`, EXIT.GENERAL);
@@ -6217,10 +6323,17 @@ async function main() {
6217
6323
  // Auto-import into the local vault + prime the session cache so
6218
6324
  // the buyer can post immediately with the cookies the admin
6219
6325
  // pre-seasoned at pool-add time.
6326
+ const filterTags = [
6327
+ country && `country=${country}`,
6328
+ registeredCountry && `registered_country=${registeredCountry}`,
6329
+ registeredPlatform && `platform=${registeredPlatform}`,
6330
+ source && `source=${source}`,
6331
+ maxUsernameChanges !== undefined && `max_renames=${maxUsernameChanges}`,
6332
+ ].filter(Boolean).join(', ');
6220
6333
  const summary = sv.importAccount(platform, account.username, account.credentials, {
6221
6334
  source: 'pool',
6222
6335
  proxy_session_id: account.proxy_session_id,
6223
- notes: 'Bought from pool',
6336
+ notes: filterTags ? `Bought from pool (${filterTags})` : 'Bought from pool',
6224
6337
  });
6225
6338
  sv.saveSession(summary.id, platform, account.cookies || []);
6226
6339
  sv.updateMeta(platform, summary.username, { last_action_at: new Date().toISOString() });
@@ -6228,8 +6341,179 @@ async function main() {
6228
6341
  success: true,
6229
6342
  platform,
6230
6343
  username: summary.username,
6231
- hint: `Ready to post — try: palmyr twitter post ${summary.username} --body "gm"`,
6344
+ country: account.country,
6345
+ registered_country: account.registered_country,
6346
+ registered_platform: account.registered_platform,
6347
+ source: account.source,
6348
+ username_change_count: account.username_change_count,
6349
+ account_based_in: account.account_based_in,
6350
+ account_id: account.id,
6351
+ hint: `Ready to post — try: palmyr twitter post ${summary.username} --body "gm". ` +
6352
+ `If the account is suspended within 7 days, run: palmyr twitter dispute ${account.id}`,
6353
+ });
6354
+ }
6355
+ case 'pool-prices': {
6356
+ // Public: which countries are priced and what they cost. Run
6357
+ // before `buy --country X` to confirm the country is available.
6358
+ let data;
6359
+ try {
6360
+ data = await ao.socialTwitterPoolPrices();
6361
+ }
6362
+ catch (e) {
6363
+ err(`pool-prices failed: ${e.message}`, EXIT.GENERAL);
6364
+ }
6365
+ return print(data);
6366
+ }
6367
+ case 'pool-set-price': {
6368
+ // Admin: set USDC price for a single country code. Idempotent.
6369
+ const { buildAdminHeaders } = await import('./admin-auth.js');
6370
+ const country = (flags.country || '').trim().toUpperCase();
6371
+ const price = flags.price !== undefined ? Number(flags.price) : NaN;
6372
+ if (!country)
6373
+ err('--country <CC> required (ISO 3166-1 alpha-2: US, GB, DE, …)');
6374
+ if (!Number.isFinite(price) || price <= 0)
6375
+ err('--price <USDC> required (positive number)');
6376
+ const path = `/social/twitter/pool/prices/${encodeURIComponent(country)}`;
6377
+ const headers = buildAdminHeaders('PUT', path);
6378
+ const res = await fetch(ao.api + path, {
6379
+ method: 'PUT',
6380
+ headers: { 'Content-Type': 'application/json', ...headers },
6381
+ body: JSON.stringify({ price_usdc: price }),
6232
6382
  });
6383
+ const data = await res.json();
6384
+ if (!res.ok || !data.success)
6385
+ err(`pool-set-price failed: ${data.error || `HTTP ${res.status}`}`, EXIT.GENERAL);
6386
+ return print(data);
6387
+ }
6388
+ case 'pool-delete-price': {
6389
+ // Admin: remove the row for a country. Subsequent `buy --country X`
6390
+ // will return 400 "Country not priced" until set again.
6391
+ const { buildAdminHeaders } = await import('./admin-auth.js');
6392
+ const country = (flags.country || '').trim().toUpperCase();
6393
+ if (!country)
6394
+ err('--country <CC> required');
6395
+ const path = `/social/twitter/pool/prices/${encodeURIComponent(country)}`;
6396
+ const headers = buildAdminHeaders('DELETE', path);
6397
+ const res = await fetch(ao.api + path, { method: 'DELETE', headers });
6398
+ const data = await res.json();
6399
+ if (!res.ok)
6400
+ err(`pool-delete-price failed: ${data.error || `HTTP ${res.status}`}`, EXIT.GENERAL);
6401
+ return print(data);
6402
+ }
6403
+ case 'pool-set-source-multiplier': {
6404
+ // Admin: scale the country price for buys filtered by a given
6405
+ // source ('web', 'mobile', …). e.g. mult=1.2 → web buys cost
6406
+ // 20% more than the base country price.
6407
+ const { buildAdminHeaders } = await import('./admin-auth.js');
6408
+ const source = (flags.source || '').trim().toLowerCase();
6409
+ const mult = flags.multiplier !== undefined ? Number(flags.multiplier) : NaN;
6410
+ if (!source)
6411
+ err('--source <name> required (e.g. web, mobile)');
6412
+ if (!Number.isFinite(mult) || mult <= 0)
6413
+ err('--multiplier <number> required (positive)');
6414
+ const path = `/social/twitter/pool/source-multipliers/${encodeURIComponent(source)}`;
6415
+ const headers = buildAdminHeaders('PUT', path);
6416
+ const res = await fetch(ao.api + path, {
6417
+ method: 'PUT',
6418
+ headers: { 'Content-Type': 'application/json', ...headers },
6419
+ body: JSON.stringify({ multiplier: mult }),
6420
+ });
6421
+ const data = await res.json();
6422
+ if (!res.ok || !data.success)
6423
+ err(`pool-set-source-multiplier failed: ${data.error || `HTTP ${res.status}`}`, EXIT.GENERAL);
6424
+ return print(data);
6425
+ }
6426
+ case 'pool-delete-source-multiplier': {
6427
+ // Admin: drop the multiplier for a source. Subsequent `buy
6428
+ // --source X` reverts to using 1.0 (filter only, no price scaling).
6429
+ const { buildAdminHeaders } = await import('./admin-auth.js');
6430
+ const source = (flags.source || '').trim().toLowerCase();
6431
+ if (!source)
6432
+ err('--source <name> required');
6433
+ const path = `/social/twitter/pool/source-multipliers/${encodeURIComponent(source)}`;
6434
+ const headers = buildAdminHeaders('DELETE', path);
6435
+ const res = await fetch(ao.api + path, { method: 'DELETE', headers });
6436
+ const data = await res.json();
6437
+ if (!res.ok)
6438
+ err(`pool-delete-source-multiplier failed: ${data.error || `HTTP ${res.status}`}`, EXIT.GENERAL);
6439
+ return print(data);
6440
+ }
6441
+ case 'dispute': {
6442
+ // Buyer: file a dispute for a pool-bought account that got
6443
+ // suspended. Server auto-verifies via twitterapi.io and either
6444
+ // hands over a same-country replacement, refunds USDC, or queues
6445
+ // for admin review when the signal is ambiguous.
6446
+ const accountId = positional[0] || flags['account-id'] || flags.id;
6447
+ const reason = (flags.reason || 'suspended');
6448
+ const evidence = flags.evidence || flags.note || undefined;
6449
+ if (!accountId)
6450
+ err('<account_id> required (the id returned by `palmyr twitter buy`)');
6451
+ if (reason !== 'suspended' && reason !== 'other')
6452
+ err('--reason must be "suspended" or "other"');
6453
+ let data;
6454
+ try {
6455
+ data = await ao.socialTwitterDispute(accountId, { reason, evidence });
6456
+ }
6457
+ catch (e) {
6458
+ err(`Dispute failed: ${e.message}`, EXIT.GENERAL);
6459
+ }
6460
+ if (!data?.success)
6461
+ err(`Dispute failed: ${data?.error || 'unknown'}`, EXIT.GENERAL);
6462
+ return print(data);
6463
+ }
6464
+ case 'disputes': {
6465
+ // Buyer: list ONE specific dispute by id. Listing all your
6466
+ // disputes isn't supported by the buyer surface today — track
6467
+ // the id printed by `dispute` and call `disputes <id>`.
6468
+ const id = positional[0] || flags.id;
6469
+ if (!id)
6470
+ err('<dispute_id> required');
6471
+ let data;
6472
+ try {
6473
+ data = await ao.socialTwitterDisputeGet(id);
6474
+ }
6475
+ catch (e) {
6476
+ err(`Get dispute failed: ${e.message}`, EXIT.GENERAL);
6477
+ }
6478
+ return print(data);
6479
+ }
6480
+ case 'pool-disputes': {
6481
+ // Admin: list every dispute, optionally filter by status. The
6482
+ // `admin_review` queue is the human-decision backlog.
6483
+ const { buildAdminHeaders } = await import('./admin-auth.js');
6484
+ const status = flags.status || undefined;
6485
+ const path = '/social/twitter/pool/disputes' + (status ? `?status=${encodeURIComponent(status)}` : '');
6486
+ const headers = buildAdminHeaders('GET', path);
6487
+ const res = await fetch(ao.api + path, { headers });
6488
+ const data = await res.json();
6489
+ if (!res.ok)
6490
+ err(`pool-disputes failed: ${data.error || `HTTP ${res.status}`}`, EXIT.GENERAL);
6491
+ return print(data);
6492
+ }
6493
+ case 'pool-resolve-dispute': {
6494
+ // Admin: resolve an admin_review dispute. action ∈ replace |
6495
+ // refund | reject. `replace` needs same-country stock; `refund`
6496
+ // needs payment provenance on the row (auto-saved at buy time).
6497
+ const { buildAdminHeaders } = await import('./admin-auth.js');
6498
+ const id = positional[0] || flags.id;
6499
+ const action = flags.action;
6500
+ const note = flags.note || undefined;
6501
+ if (!id)
6502
+ err('<dispute_id> required');
6503
+ if (action !== 'replace' && action !== 'refund' && action !== 'reject') {
6504
+ err('--action must be "replace", "refund", or "reject"');
6505
+ }
6506
+ const path = `/social/twitter/pool/disputes/${encodeURIComponent(id)}/resolve`;
6507
+ const headers = buildAdminHeaders('POST', path);
6508
+ const res = await fetch(ao.api + path, {
6509
+ method: 'POST',
6510
+ headers: { 'Content-Type': 'application/json', ...headers },
6511
+ body: JSON.stringify({ action, ...(note ? { note } : {}) }),
6512
+ });
6513
+ const data = await res.json();
6514
+ if (!res.ok || !data.success)
6515
+ err(`pool-resolve-dispute failed: ${data.error || `HTTP ${res.status}`}`, EXIT.GENERAL);
6516
+ return print(data);
6233
6517
  }
6234
6518
  case 'pool-add': {
6235
6519
  const { buildAdminHeaders } = await import('./admin-auth.js');
@@ -7080,6 +7364,55 @@ async function main() {
7080
7364
  'The Palmyr server fires them at post_at without any client process.', EXIT.BAD_INPUT);
7081
7365
  break;
7082
7366
  }
7367
+ case 'telemetry': {
7368
+ // Off by default. Opt-in only. We never auto-enable, never prompt at
7369
+ // startup, never write to stdout outside this command. Captured fields
7370
+ // and storage location are documented in cli/telemetry.ts.
7371
+ const action = (subcommand || 'status').toLowerCase();
7372
+ if (action !== 'on' && action !== 'off' && action !== 'status') {
7373
+ err(`Unknown telemetry action: ${action}. Use: on | off | status`, EXIT.BAD_INPUT);
7374
+ }
7375
+ let state;
7376
+ if (action === 'on')
7377
+ state = setTelemetryEnabled(true);
7378
+ else if (action === 'off')
7379
+ state = setTelemetryEnabled(false);
7380
+ else
7381
+ state = getTelemetryState();
7382
+ const payload = {
7383
+ enabled: state.enabled,
7384
+ installId: state.installId || null,
7385
+ optedInAt: state.optedInAt || null,
7386
+ queuedEvents: telemetryQueuedCount(),
7387
+ captures: ['cmd', 'exitCode', 'durationMs', 'cliVersion', 'nodeVersion', 'platform'],
7388
+ neverCaptures: ['flag values', 'positional args', 'stdout/stderr', 'wallet addresses', 'phone numbers', 'any user input'],
7389
+ };
7390
+ if (AGENT_MODE) {
7391
+ print(payload);
7392
+ }
7393
+ else {
7394
+ const status = state.enabled ? `${t.success}on${t.reset}` : `${t.muted}off${t.reset}`;
7395
+ console.log(`Telemetry: ${status}`);
7396
+ if (state.installId)
7397
+ console.log(`Install ID: ${t.muted}${state.installId}${t.reset}`);
7398
+ if (state.optedInAt)
7399
+ console.log(`Opted in: ${state.optedInAt}`);
7400
+ if (payload.queuedEvents)
7401
+ console.log(`Queued: ${payload.queuedEvents} event(s) waiting to send`);
7402
+ console.log('');
7403
+ console.log(`Captures: ${payload.captures.join(', ')}`);
7404
+ console.log(`Never: flag values, positional args, stdout, user input`);
7405
+ if (!state.enabled) {
7406
+ console.log('');
7407
+ console.log(`Opt in: ${t.accent}palmyr telemetry on${t.reset}`);
7408
+ }
7409
+ else {
7410
+ console.log('');
7411
+ console.log(`Opt out: ${t.accent}palmyr telemetry off${t.reset} (drops any queued events)`);
7412
+ }
7413
+ }
7414
+ break;
7415
+ }
7083
7416
  case 'config': {
7084
7417
  const cfg = loadConfig();
7085
7418
  const { homedir } = await import('os');