@sherwoodagent/cli 0.13.2 → 0.14.2

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.
Files changed (49) hide show
  1. package/dist/{chat-6QVLBEUX.js → chat-X3IKMSNS.js} +11 -8
  2. package/dist/{chat-6QVLBEUX.js.map → chat-X3IKMSNS.js.map} +1 -1
  3. package/dist/{chunk-Q25EYYCS.js → chunk-2RYZU7EP.js} +6 -4
  4. package/dist/chunk-2RYZU7EP.js.map +1 -0
  5. package/dist/chunk-4CV4JOE5.js +27 -0
  6. package/dist/{chunk-IVHEPKUX.js → chunk-5NA345RJ.js} +23 -10
  7. package/dist/chunk-5NA345RJ.js.map +1 -0
  8. package/dist/{chunk-IIDZ2TK5.js → chunk-BCOTZTTH.js} +39 -10
  9. package/dist/chunk-BCOTZTTH.js.map +1 -0
  10. package/dist/{chunk-7CN3TSAA.js → chunk-BGEVQQES.js} +5 -3
  11. package/dist/{chunk-7CN3TSAA.js.map → chunk-BGEVQQES.js.map} +1 -1
  12. package/dist/{chunk-FWJBUK57.js → chunk-CGSXIYYZ.js} +5 -3
  13. package/dist/{chunk-FWJBUK57.js.map → chunk-CGSXIYYZ.js.map} +1 -1
  14. package/dist/{chunk-KAZRNDZQ.js → chunk-QIOS7I7I.js} +2 -2
  15. package/dist/{chunk-F3CBGW3T.js → chunk-UBBR7QVJ.js} +8 -11
  16. package/dist/chunk-UBBR7QVJ.js.map +1 -0
  17. package/dist/{chunk-LEQLX7XF.js → chunk-YSYX555I.js} +27 -10
  18. package/dist/chunk-YSYX555I.js.map +1 -0
  19. package/dist/{config-U74QT4SC.js → config-5C7QJMPR.js} +5 -2
  20. package/dist/{cron-RG46PYWA.js → cron-IKBNUL3Q.js} +3 -1
  21. package/dist/{cron-RG46PYWA.js.map → cron-IKBNUL3Q.js.map} +1 -1
  22. package/dist/{eas-WPZR4SGS.js → eas-7U7MX24T.js} +7 -6
  23. package/dist/index.js +74 -33
  24. package/dist/index.js.map +1 -1
  25. package/dist/ipfs-AYE4J4OX.js +13 -0
  26. package/dist/{network-ROF3SSAA.js → network-QU2D677V.js} +4 -3
  27. package/dist/research-7RI7VFPK.js +15 -0
  28. package/dist/research-7RI7VFPK.js.map +1 -0
  29. package/dist/{research-EYDNPRAF.js → research-ZDTM73C7.js} +9 -8
  30. package/dist/{research-EYDNPRAF.js.map → research-ZDTM73C7.js.map} +1 -1
  31. package/dist/{session-B5G2CQOK.js → session-2VK25CSW.js} +14 -12
  32. package/dist/session-2VK25CSW.js.map +1 -0
  33. package/dist/{xmtp-S3Z7QWSI.js → xmtp-Y3LAZKOC.js} +10 -7
  34. package/dist/{xmtp-S3Z7QWSI.js.map → xmtp-Y3LAZKOC.js.map} +1 -1
  35. package/package.json +1 -1
  36. package/dist/chunk-F3CBGW3T.js.map +0 -1
  37. package/dist/chunk-IIDZ2TK5.js.map +0 -1
  38. package/dist/chunk-IVHEPKUX.js.map +0 -1
  39. package/dist/chunk-LEQLX7XF.js.map +0 -1
  40. package/dist/chunk-Q25EYYCS.js.map +0 -1
  41. package/dist/ipfs-LUJHZGKF.js +0 -11
  42. package/dist/research-HEZP7VPY.js +0 -14
  43. package/dist/session-B5G2CQOK.js.map +0 -1
  44. /package/dist/{config-U74QT4SC.js.map → chunk-4CV4JOE5.js.map} +0 -0
  45. /package/dist/{chunk-KAZRNDZQ.js.map → chunk-QIOS7I7I.js.map} +0 -0
  46. /package/dist/{eas-WPZR4SGS.js.map → config-5C7QJMPR.js.map} +0 -0
  47. /package/dist/{ipfs-LUJHZGKF.js.map → eas-7U7MX24T.js.map} +0 -0
  48. /package/dist/{network-ROF3SSAA.js.map → ipfs-AYE4J4OX.js.map} +0 -0
  49. /package/dist/{research-HEZP7VPY.js.map → network-QU2D677V.js.map} +0 -0
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  fetchMetadata,
4
4
  uploadMetadata
5
- } from "./chunk-LEQLX7XF.js";
5
+ } from "./chunk-YSYX555I.js";
6
6
  import {
7
7
  createApproval,
8
8
  createJoinRequest,
@@ -10,7 +10,7 @@ import {
10
10
  queryApprovals,
11
11
  queryJoinRequests,
12
12
  revokeAttestation
13
- } from "./chunk-Q25EYYCS.js";
13
+ } from "./chunk-2RYZU7EP.js";
14
14
  import {
15
15
  approveDepositor,
16
16
  deposit,
@@ -26,7 +26,7 @@ import {
26
26
  resolveVaultSyndicate,
27
27
  setTextRecord,
28
28
  setVaultAddress
29
- } from "./chunk-IVHEPKUX.js";
29
+ } from "./chunk-5NA345RJ.js";
30
30
  import {
31
31
  AGENT_REGISTRY,
32
32
  EAS_SCHEMAS,
@@ -42,12 +42,12 @@ import {
42
42
  UNISWAP_QUOTER_V2_ABI,
43
43
  VENICE,
44
44
  VENICE_STAKING_ABI
45
- } from "./chunk-F3CBGW3T.js";
45
+ } from "./chunk-UBBR7QVJ.js";
46
46
  import {
47
47
  getAccount,
48
48
  getPublicClient,
49
49
  getWalletClient
50
- } from "./chunk-FWJBUK57.js";
50
+ } from "./chunk-CGSXIYYZ.js";
51
51
  import {
52
52
  VALID_NETWORKS,
53
53
  getChain,
@@ -56,13 +56,14 @@ import {
56
56
  getRpcUrl,
57
57
  isTestnet,
58
58
  setNetwork
59
- } from "./chunk-7CN3TSAA.js";
59
+ } from "./chunk-BGEVQQES.js";
60
60
  import {
61
61
  cacheGroupId,
62
62
  getAgentId,
63
63
  getChainContracts,
64
64
  getNotifyTo,
65
65
  getVeniceApiKey,
66
+ init_config,
66
67
  loadConfig,
67
68
  setAgentId,
68
69
  setChainContract,
@@ -70,12 +71,14 @@ import {
70
71
  setNotifyTo,
71
72
  setPrivateKey,
72
73
  setVeniceApiKey
73
- } from "./chunk-IIDZ2TK5.js";
74
+ } from "./chunk-BCOTZTTH.js";
75
+ import "./chunk-4CV4JOE5.js";
74
76
 
75
77
  // src/index.ts
76
78
  import { config as loadDotenv } from "dotenv";
79
+ import { createRequire } from "module";
77
80
  import { Command, Option } from "commander";
78
- import { parseUnits as parseUnits8 } from "viem";
81
+ import { parseUnits as parseUnits8, isAddress as isAddress5 } from "viem";
79
82
  import chalk7 from "chalk";
80
83
  import ora7 from "ora";
81
84
  import { input, confirm, select } from "@inquirer/prompts";
@@ -572,7 +575,8 @@ async function getActiveSyndicates() {
572
575
  }
573
576
  async function updateMetadata(syndicateId, metadataURI) {
574
577
  const wallet = getWalletClient();
575
- return wallet.writeContract({
578
+ const client = getPublicClient();
579
+ const hash = await wallet.writeContract({
576
580
  account: getAccount(),
577
581
  chain: getChain(),
578
582
  address: getFactoryAddress(),
@@ -580,6 +584,8 @@ async function updateMetadata(syndicateId, metadataURI) {
580
584
  functionName: "updateMetadata",
581
585
  args: [syndicateId, metadataURI]
582
586
  });
587
+ await client.waitForTransactionReceipt({ hash });
588
+ return hash;
583
589
  }
584
590
 
585
591
  // src/lib/subgraph.ts
@@ -613,10 +619,27 @@ async function query(graphql, variables) {
613
619
  return result.data;
614
620
  }
615
621
  async function getActiveSyndicates2(creator) {
616
- const where = creator ? `where: { active: true, creator: "${creator.toLowerCase()}" }` : `where: { active: true }`;
622
+ if (creator) {
623
+ const data2 = await query(
624
+ `query($creator: String!) {
625
+ syndicates(where: { active: true, creator: $creator }, orderBy: createdAt, orderDirection: desc, first: 100) {
626
+ id
627
+ vault
628
+ creator
629
+ metadataURI
630
+ createdAt
631
+ active
632
+ totalDeposits
633
+ totalWithdrawals
634
+ }
635
+ }`,
636
+ { creator: creator.toLowerCase() }
637
+ );
638
+ return data2.syndicates;
639
+ }
617
640
  const data = await query(`
618
641
  {
619
- syndicates(${where}, orderBy: createdAt, orderDirection: desc, first: 100) {
642
+ syndicates(where: { active: true }, orderBy: createdAt, orderDirection: desc, first: 100) {
620
643
  id
621
644
  vault
622
645
  creator
@@ -841,6 +864,7 @@ function buildFundBatch(config, vaultAddress, agents, assetAddress, assetDecimal
841
864
  }
842
865
 
843
866
  // src/lib/venice.ts
867
+ init_config();
844
868
  var VENICE_API_BASE = "https://api.venice.ai/api/v1";
845
869
  async function provisionApiKey() {
846
870
  const account = getAccount();
@@ -885,6 +909,7 @@ async function checkApiKeyValid() {
885
909
  }
886
910
 
887
911
  // src/commands/venice.ts
912
+ init_config();
888
913
  var VALID_FEES2 = [500, 3e3, 1e4];
889
914
  function registerVeniceCommands(program2) {
890
915
  const venice = program2.command("venice").description("Venice private inference \u2014 stake VVV, provision API keys");
@@ -1463,6 +1488,7 @@ function registerAllowanceCommands(program2) {
1463
1488
  import chalk4 from "chalk";
1464
1489
  import ora4 from "ora";
1465
1490
  import { SDK } from "agent0-sdk";
1491
+ init_config();
1466
1492
  var IDENTITY_REGISTRY_ABI = [
1467
1493
  {
1468
1494
  name: "balanceOf",
@@ -1978,8 +2004,8 @@ function formatDurationLong(seconds) {
1978
2004
  if (s >= 60) return `${(s / 60).toFixed(0)} min`;
1979
2005
  return `${s}s`;
1980
2006
  }
1981
- function formatShares(raw) {
1982
- const num = Number(raw) / 1e6;
2007
+ function formatShares(raw, decimals = 6) {
2008
+ const num = Number(raw) / 10 ** decimals;
1983
2009
  return num.toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: 2 });
1984
2010
  }
1985
2011
  function formatUSDC(raw) {
@@ -2401,7 +2427,7 @@ function registerGovernorCommands(program2) {
2401
2427
  const spinner = ora6("Setting voting period...").start();
2402
2428
  try {
2403
2429
  const hash = await setVotingPeriod(parseBigIntArg(opts.seconds, "seconds"));
2404
- spinner.succeed(G2(`Voting period updated to ${opts.seconds}s`));
2430
+ spinner.succeed(G2(`Voting period change queued (${opts.seconds}s). Finalize after the timelock delay with \`sherwood governor finalize-param\`.`));
2405
2431
  console.log(DIM2(` ${getExplorerUrl(hash)}`));
2406
2432
  } catch (err) {
2407
2433
  spinner.fail("Failed to set voting period");
@@ -2413,7 +2439,7 @@ function registerGovernorCommands(program2) {
2413
2439
  const spinner = ora6("Setting execution window...").start();
2414
2440
  try {
2415
2441
  const hash = await setExecutionWindow(parseBigIntArg(opts.seconds, "seconds"));
2416
- spinner.succeed(G2(`Execution window updated to ${opts.seconds}s`));
2442
+ spinner.succeed(G2(`Execution window change queued (${opts.seconds}s). Finalize after the timelock delay with \`sherwood governor finalize-param\`.`));
2417
2443
  console.log(DIM2(` ${getExplorerUrl(hash)}`));
2418
2444
  } catch (err) {
2419
2445
  spinner.fail("Failed to set execution window");
@@ -2425,7 +2451,7 @@ function registerGovernorCommands(program2) {
2425
2451
  const spinner = ora6("Setting veto threshold...").start();
2426
2452
  try {
2427
2453
  const hash = await setVetoThresholdBps(parseBigIntArg(opts.bps, "bps"));
2428
- spinner.succeed(G2(`Veto threshold updated to ${Number(opts.bps) / 100}%`));
2454
+ spinner.succeed(G2(`Veto threshold change queued (${Number(opts.bps) / 100}%). Finalize after the timelock delay with \`sherwood governor finalize-param\`.`));
2429
2455
  console.log(DIM2(` ${getExplorerUrl(hash)}`));
2430
2456
  } catch (err) {
2431
2457
  spinner.fail("Failed to set veto threshold");
@@ -2437,7 +2463,7 @@ function registerGovernorCommands(program2) {
2437
2463
  const spinner = ora6("Setting max fee...").start();
2438
2464
  try {
2439
2465
  const hash = await setMaxPerformanceFeeBps(parseBigIntArg(opts.bps, "bps"));
2440
- spinner.succeed(G2(`Max performance fee updated to ${Number(opts.bps) / 100}%`));
2466
+ spinner.succeed(G2(`Max performance fee change queued (${Number(opts.bps) / 100}%). Finalize after the timelock delay with \`sherwood governor finalize-param\`.`));
2441
2467
  console.log(DIM2(` ${getExplorerUrl(hash)}`));
2442
2468
  } catch (err) {
2443
2469
  spinner.fail("Failed to set max fee");
@@ -2449,7 +2475,7 @@ function registerGovernorCommands(program2) {
2449
2475
  const spinner = ora6("Setting max duration...").start();
2450
2476
  try {
2451
2477
  const hash = await setMaxStrategyDuration(parseBigIntArg(opts.seconds, "seconds"));
2452
- spinner.succeed(G2(`Max strategy duration updated to ${opts.seconds}s`));
2478
+ spinner.succeed(G2(`Max strategy duration change queued (${opts.seconds}s). Finalize after the timelock delay with \`sherwood governor finalize-param\`.`));
2453
2479
  console.log(DIM2(` ${getExplorerUrl(hash)}`));
2454
2480
  } catch (err) {
2455
2481
  spinner.fail("Failed to set max duration");
@@ -2461,7 +2487,7 @@ function registerGovernorCommands(program2) {
2461
2487
  const spinner = ora6("Setting cooldown...").start();
2462
2488
  try {
2463
2489
  const hash = await setCooldownPeriod(parseBigIntArg(opts.seconds, "seconds"));
2464
- spinner.succeed(G2(`Cooldown period updated to ${opts.seconds}s`));
2490
+ spinner.succeed(G2(`Cooldown period change queued (${opts.seconds}s). Finalize after the timelock delay with \`sherwood governor finalize-param\`.`));
2465
2491
  console.log(DIM2(` ${getExplorerUrl(hash)}`));
2466
2492
  } catch (err) {
2467
2493
  spinner.fail("Failed to set cooldown");
@@ -2473,7 +2499,7 @@ function registerGovernorCommands(program2) {
2473
2499
  const spinner = ora6("Setting protocol fee...").start();
2474
2500
  try {
2475
2501
  const hash = await setProtocolFeeBps(parseBigIntArg(opts.bps, "bps"));
2476
- spinner.succeed(G2(`Protocol fee updated to ${Number(opts.bps) / 100}%`));
2502
+ spinner.succeed(G2(`Protocol fee change queued (${Number(opts.bps) / 100}%). Finalize after the timelock delay with \`sherwood governor finalize-param\`.`));
2477
2503
  console.log(DIM2(` ${getExplorerUrl(hash)}`));
2478
2504
  } catch (err) {
2479
2505
  spinner.fail("Failed to set protocol fee");
@@ -2484,15 +2510,18 @@ function registerGovernorCommands(program2) {
2484
2510
  }
2485
2511
 
2486
2512
  // src/index.ts
2513
+ init_config();
2487
2514
  try {
2488
2515
  loadDotenv();
2489
2516
  } catch {
2490
2517
  }
2518
+ var require2 = createRequire(import.meta.url);
2519
+ var { version: CLI_VERSION } = require2("../package.json");
2491
2520
  async function loadXmtp() {
2492
- return import("./xmtp-S3Z7QWSI.js");
2521
+ return import("./xmtp-Y3LAZKOC.js");
2493
2522
  }
2494
2523
  async function loadCron() {
2495
- return import("./cron-RG46PYWA.js");
2524
+ return import("./cron-IKBNUL3Q.js");
2496
2525
  }
2497
2526
  var G3 = chalk7.green;
2498
2527
  var W3 = chalk7.white;
@@ -2500,18 +2529,26 @@ var DIM3 = chalk7.gray;
2500
2529
  var BOLD3 = chalk7.white.bold;
2501
2530
  var LABEL3 = chalk7.green.bold;
2502
2531
  var SEP3 = () => console.log(DIM3("\u2500".repeat(60)));
2532
+ function validateAddress(value, name) {
2533
+ if (!isAddress5(value)) {
2534
+ console.error(chalk7.red(`Invalid ${name} address: ${value}`));
2535
+ process.exit(1);
2536
+ }
2537
+ return value;
2538
+ }
2503
2539
  function resolveVault(opts) {
2504
2540
  if (opts.vault) {
2505
- setVaultAddress(opts.vault);
2541
+ setVaultAddress(validateAddress(opts.vault, "vault"));
2506
2542
  }
2507
2543
  }
2508
2544
  var program = new Command();
2509
- program.name("sherwood").description("CLI for agent-managed investment syndicates").version("0.1.0").addOption(
2545
+ program.name("sherwood").description("CLI for agent-managed investment syndicates").version(CLI_VERSION).addOption(
2510
2546
  new Option("--chain <network>", "Target network").choices(VALID_NETWORKS).default("base")
2511
2547
  ).option("--testnet", "Alias for --chain base-sepolia (deprecated)", false).hook("preAction", (thisCommand) => {
2512
2548
  const opts = thisCommand.optsWithGlobals();
2513
2549
  let network = opts.chain;
2514
2550
  if (opts.testnet) {
2551
+ process.env.ENABLE_TESTNET = "true";
2515
2552
  if (network !== "base") {
2516
2553
  console.warn(
2517
2554
  chalk7.yellow("[warn] --testnet ignored, --chain takes precedence")
@@ -2826,7 +2863,8 @@ syndicate.command("approve-depositor").description("Approve an address to deposi
2826
2863
  resolveVault(opts);
2827
2864
  const spinner = ora7("Approving depositor...").start();
2828
2865
  try {
2829
- const hash = await approveDepositor(opts.depositor);
2866
+ const depositor = validateAddress(opts.depositor, "depositor");
2867
+ const hash = await approveDepositor(depositor);
2830
2868
  spinner.succeed(`Depositor approved: ${hash}`);
2831
2869
  console.log(chalk7.dim(` ${getExplorerUrl(hash)}`));
2832
2870
  } catch (err) {
@@ -2839,7 +2877,8 @@ syndicate.command("remove-depositor").description("Remove an address from the de
2839
2877
  resolveVault(opts);
2840
2878
  const spinner = ora7("Removing depositor...").start();
2841
2879
  try {
2842
- const hash = await removeDepositor(opts.depositor);
2880
+ const depositor = validateAddress(opts.depositor, "depositor");
2881
+ const hash = await removeDepositor(depositor);
2843
2882
  spinner.succeed(`Depositor removed: ${hash}`);
2844
2883
  console.log(chalk7.dim(` ${getExplorerUrl(hash)}`));
2845
2884
  } catch (err) {
@@ -2859,10 +2898,11 @@ syndicate.command("add").description("Register an agent on a syndicate vault (cr
2859
2898
  spinner.fail("Only the syndicate creator can add agents");
2860
2899
  process.exit(1);
2861
2900
  }
2901
+ const agentWallet = validateAddress(opts.wallet, "wallet");
2862
2902
  spinner.text = "Registering agent...";
2863
2903
  const hash = await registerAgent(
2864
2904
  BigInt(opts.agentId),
2865
- opts.wallet
2905
+ agentWallet
2866
2906
  );
2867
2907
  spinner.succeed(`Agent registered: ${hash}`);
2868
2908
  console.log(chalk7.dim(` ${getExplorerUrl(hash)}`));
@@ -3054,6 +3094,7 @@ syndicate.command("approve").description("Approve an agent join request (registe
3054
3094
  resolveVault(opts);
3055
3095
  }
3056
3096
  const vaultAddress = getVaultAddress();
3097
+ const agentWallet = validateAddress(opts.wallet, "wallet");
3057
3098
  const { creator, subdomain, id: syndicateId } = await resolveVaultSyndicate(vaultAddress);
3058
3099
  const callerAddress = getAccount().address.toLowerCase();
3059
3100
  if (creator.toLowerCase() !== callerAddress) {
@@ -3065,7 +3106,7 @@ syndicate.command("approve").description("Approve an agent join request (registe
3065
3106
  try {
3066
3107
  const regHash = await registerAgent(
3067
3108
  BigInt(opts.agentId),
3068
- opts.wallet
3109
+ agentWallet
3069
3110
  );
3070
3111
  agentWasRegistered = true;
3071
3112
  console.log(DIM3(` Agent registered: ${getExplorerUrl(regHash)}`));
@@ -3095,7 +3136,7 @@ syndicate.command("approve").description("Approve an agent join request (registe
3095
3136
  syndicateId,
3096
3137
  BigInt(opts.agentId),
3097
3138
  vaultAddress,
3098
- opts.wallet
3139
+ agentWallet
3099
3140
  );
3100
3141
  approvalUid = result.uid;
3101
3142
  }
@@ -3278,7 +3319,7 @@ strategy.command("run").description("Execute the levered swap strategy").option(
3278
3319
  await runLeveredSwap(opts);
3279
3320
  });
3280
3321
  program.command("providers").description("List available DeFi providers").action(async () => {
3281
- const { MessariProvider, NansenProvider } = await import("./research-HEZP7VPY.js");
3322
+ const { MessariProvider, NansenProvider } = await import("./research-7RI7VFPK.js");
3282
3323
  const providers = [new MoonwellProvider(), new UniswapProvider(), new MessariProvider(), new NansenProvider()];
3283
3324
  for (const p of providers) {
3284
3325
  const info = p.info();
@@ -3289,7 +3330,7 @@ ${info.name} (${info.type})`);
3289
3330
  }
3290
3331
  });
3291
3332
  try {
3292
- const { registerChatCommands } = await import("./chat-6QVLBEUX.js");
3333
+ const { registerChatCommands } = await import("./chat-X3IKMSNS.js");
3293
3334
  registerChatCommands(program);
3294
3335
  } catch {
3295
3336
  program.command("chat <name> [action] [actionArgs...]").description("Syndicate chat (XMTP) \u2014 requires @xmtp/cli").action(() => {
@@ -3299,14 +3340,14 @@ try {
3299
3340
  process.exit(1);
3300
3341
  });
3301
3342
  }
3302
- var { registerSessionCommands } = await import("./session-B5G2CQOK.js");
3343
+ var { registerSessionCommands } = await import("./session-2VK25CSW.js");
3303
3344
  registerSessionCommands(program);
3304
3345
  registerVeniceCommands(program);
3305
3346
  registerAllowanceCommands(program);
3306
3347
  registerIdentityCommands(program);
3307
3348
  registerProposalCommands(program);
3308
3349
  registerGovernorCommands(program);
3309
- var { registerResearchCommands } = await import("./research-EYDNPRAF.js");
3350
+ var { registerResearchCommands } = await import("./research-ZDTM73C7.js");
3310
3351
  registerResearchCommands(program);
3311
3352
  var configCmd = program.command("config");
3312
3353
  configCmd.command("set").description("Save settings to ~/.sherwood/config.json (persists across sessions)").option("--private-key <key>", "Wallet private key (0x-prefixed)").option("--vault <address>", "Default SyndicateVault address").option("--rpc <url>", "Custom RPC URL for the active --chain network").option("--notify-to <id>", "Destination for cron summaries (Telegram chat ID, phone, etc.)").action((opts) => {