@sherwoodagent/cli 0.2.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/index.js ADDED
@@ -0,0 +1,2726 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ AGENT_REGISTRY,
4
+ EAS_ABI,
5
+ EAS_CONTRACTS,
6
+ EAS_SCHEMAS,
7
+ ERC20_ABI,
8
+ MOONWELL,
9
+ SHERWOOD,
10
+ STRATEGY_REGISTRY_ABI,
11
+ SYNDICATE_FACTORY_ABI,
12
+ SYNDICATE_VAULT_ABI,
13
+ TOKENS,
14
+ UNISWAP,
15
+ UNISWAP_QUOTER_V2_ABI,
16
+ VENICE,
17
+ VENICE_STAKING_ABI,
18
+ addTarget,
19
+ approveDepositor,
20
+ cacheGroupId,
21
+ deposit,
22
+ executeBatch,
23
+ getAccount,
24
+ getAgentId,
25
+ getAllowedTargets,
26
+ getAssetDecimals,
27
+ getBalance,
28
+ getChain,
29
+ getChainContracts,
30
+ getExplorerUrl,
31
+ getNetwork,
32
+ getPublicClient,
33
+ getRpcUrl,
34
+ getVaultAddress,
35
+ getVaultInfo,
36
+ getVeniceApiKey,
37
+ getWalletClient,
38
+ loadConfig,
39
+ ragequit,
40
+ registerAgent,
41
+ removeDepositor,
42
+ removeTarget,
43
+ resolveSyndicate,
44
+ resolveVaultSyndicate,
45
+ setAgentId,
46
+ setChainContract,
47
+ setNetwork,
48
+ setPrivateKey,
49
+ setTextRecord,
50
+ setVaultAddress,
51
+ setVeniceApiKey,
52
+ simulateBatch
53
+ } from "./chunk-CR3ZNOB7.js";
54
+
55
+ // src/index.ts
56
+ import { config as loadDotenv } from "dotenv";
57
+ import { Command } from "commander";
58
+ import { parseUnits as parseUnits8 } from "viem";
59
+ import chalk5 from "chalk";
60
+ import ora5 from "ora";
61
+ import { input, confirm } from "@inquirer/prompts";
62
+
63
+ // src/providers/moonwell.ts
64
+ import { base, baseSepolia } from "viem/chains";
65
+ var MoonwellProvider = class {
66
+ info() {
67
+ return {
68
+ name: "moonwell",
69
+ type: "lending",
70
+ capabilities: [
71
+ "lend.deposit",
72
+ "lend.borrow",
73
+ "lend.repay",
74
+ "lend.withdraw",
75
+ "lend.positions"
76
+ ],
77
+ supportedChains: [base, baseSepolia]
78
+ };
79
+ }
80
+ async depositCollateral(params) {
81
+ throw new Error("Not implemented \u2014 wire up viem client");
82
+ }
83
+ async borrow(params) {
84
+ throw new Error("Not implemented");
85
+ }
86
+ async repay(params) {
87
+ throw new Error("Not implemented");
88
+ }
89
+ async withdrawCollateral(params) {
90
+ throw new Error("Not implemented");
91
+ }
92
+ async getPosition(account) {
93
+ throw new Error("Not implemented");
94
+ }
95
+ };
96
+
97
+ // src/providers/uniswap.ts
98
+ import { base as base2, baseSepolia as baseSepolia2 } from "viem/chains";
99
+ var UniswapProvider = class {
100
+ info() {
101
+ return {
102
+ name: "uniswap",
103
+ type: "trading",
104
+ capabilities: [
105
+ "swap.exact-input",
106
+ "swap.quote"
107
+ ],
108
+ supportedChains: [base2, baseSepolia2]
109
+ };
110
+ }
111
+ async swap(params) {
112
+ throw new Error("Not implemented \u2014 wire up viem client");
113
+ }
114
+ async quote(params) {
115
+ throw new Error("Not implemented");
116
+ }
117
+ };
118
+
119
+ // src/commands/strategy-run.ts
120
+ import { parseUnits as parseUnits2, formatUnits, isAddress } from "viem";
121
+ import chalk from "chalk";
122
+ import ora from "ora";
123
+
124
+ // src/strategies/levered-swap.ts
125
+ import {
126
+ encodeFunctionData,
127
+ parseUnits,
128
+ parseEther
129
+ } from "viem";
130
+ var ERC20_ABI2 = [
131
+ {
132
+ name: "approve",
133
+ type: "function",
134
+ inputs: [
135
+ { name: "spender", type: "address" },
136
+ { name: "amount", type: "uint256" }
137
+ ],
138
+ outputs: [{ name: "", type: "bool" }]
139
+ }
140
+ ];
141
+ var MTOKEN_ABI = [
142
+ {
143
+ name: "mint",
144
+ type: "function",
145
+ inputs: [{ name: "mintAmount", type: "uint256" }],
146
+ outputs: [{ name: "", type: "uint256" }]
147
+ },
148
+ {
149
+ name: "borrow",
150
+ type: "function",
151
+ inputs: [{ name: "borrowAmount", type: "uint256" }],
152
+ outputs: [{ name: "", type: "uint256" }]
153
+ },
154
+ {
155
+ name: "repayBorrow",
156
+ type: "function",
157
+ inputs: [{ name: "repayAmount", type: "uint256" }],
158
+ outputs: [{ name: "", type: "uint256" }]
159
+ },
160
+ {
161
+ name: "redeemUnderlying",
162
+ type: "function",
163
+ inputs: [{ name: "redeemAmount", type: "uint256" }],
164
+ outputs: [{ name: "", type: "uint256" }]
165
+ }
166
+ ];
167
+ var COMPTROLLER_ABI = [
168
+ {
169
+ name: "enterMarkets",
170
+ type: "function",
171
+ inputs: [{ name: "mTokens", type: "address[]" }],
172
+ outputs: [{ name: "", type: "uint256[]" }]
173
+ }
174
+ ];
175
+ var SWAP_ROUTER_ABI = [
176
+ {
177
+ name: "exactInputSingle",
178
+ type: "function",
179
+ inputs: [
180
+ {
181
+ name: "params",
182
+ type: "tuple",
183
+ components: [
184
+ { name: "tokenIn", type: "address" },
185
+ { name: "tokenOut", type: "address" },
186
+ { name: "fee", type: "uint24" },
187
+ { name: "recipient", type: "address" },
188
+ { name: "amountIn", type: "uint256" },
189
+ { name: "amountOutMinimum", type: "uint256" },
190
+ { name: "sqrtPriceLimitX96", type: "uint160" }
191
+ ]
192
+ }
193
+ ],
194
+ outputs: [{ name: "amountOut", type: "uint256" }]
195
+ }
196
+ ];
197
+ function buildEntryBatch(config, vaultAddress, amountOutMinimum, borrowDecimals) {
198
+ const ZERO = "0x0000000000000000000000000000000000000000";
199
+ if (MOONWELL().mWETH === ZERO || MOONWELL().mUSDC === ZERO || MOONWELL().COMPTROLLER === ZERO) {
200
+ throw new Error("Moonwell is not deployed on this network \u2014 levered swap requires Moonwell lending markets");
201
+ }
202
+ const collateral = parseEther(config.collateralAmount);
203
+ const borrow = parseUnits(config.borrowAmount, borrowDecimals);
204
+ const calls = [
205
+ // 1. Approve mWETH to pull WETH from executor
206
+ {
207
+ target: TOKENS().WETH,
208
+ data: encodeFunctionData({
209
+ abi: ERC20_ABI2,
210
+ functionName: "approve",
211
+ args: [MOONWELL().mWETH, collateral]
212
+ }),
213
+ value: 0n
214
+ },
215
+ // 2. Deposit WETH as collateral (mint mWETH tokens)
216
+ {
217
+ target: MOONWELL().mWETH,
218
+ data: encodeFunctionData({
219
+ abi: MTOKEN_ABI,
220
+ functionName: "mint",
221
+ args: [collateral]
222
+ }),
223
+ value: 0n
224
+ },
225
+ // 3. Enter WETH market (enable as collateral for borrowing)
226
+ {
227
+ target: MOONWELL().COMPTROLLER,
228
+ data: encodeFunctionData({
229
+ abi: COMPTROLLER_ABI,
230
+ functionName: "enterMarkets",
231
+ args: [[MOONWELL().mWETH]]
232
+ }),
233
+ value: 0n
234
+ },
235
+ // 4. Borrow USDC against WETH collateral
236
+ {
237
+ target: MOONWELL().mUSDC,
238
+ data: encodeFunctionData({
239
+ abi: MTOKEN_ABI,
240
+ functionName: "borrow",
241
+ args: [borrow]
242
+ }),
243
+ value: 0n
244
+ },
245
+ // 5. Approve SwapRouter to pull borrowed USDC
246
+ {
247
+ target: TOKENS().USDC,
248
+ data: encodeFunctionData({
249
+ abi: ERC20_ABI2,
250
+ functionName: "approve",
251
+ args: [UNISWAP().SWAP_ROUTER, borrow]
252
+ }),
253
+ value: 0n
254
+ },
255
+ // 6. Swap USDC → target token
256
+ {
257
+ target: UNISWAP().SWAP_ROUTER,
258
+ data: encodeFunctionData({
259
+ abi: SWAP_ROUTER_ABI,
260
+ functionName: "exactInputSingle",
261
+ args: [
262
+ {
263
+ tokenIn: TOKENS().USDC,
264
+ tokenOut: config.targetToken,
265
+ fee: config.fee,
266
+ recipient: vaultAddress,
267
+ // Tokens stay in vault (delegatecall)
268
+ amountIn: borrow,
269
+ amountOutMinimum,
270
+ sqrtPriceLimitX96: 0n
271
+ }
272
+ ]
273
+ }),
274
+ value: 0n
275
+ }
276
+ ];
277
+ return calls;
278
+ }
279
+
280
+ // src/lib/quote.ts
281
+ import { encodeFunctionData as encodeFunctionData2, decodeFunctionResult, concat, pad, numberToHex } from "viem";
282
+ async function getQuote(params) {
283
+ const client = getPublicClient();
284
+ const calldata = encodeFunctionData2({
285
+ abi: UNISWAP_QUOTER_V2_ABI,
286
+ functionName: "quoteExactInputSingle",
287
+ args: [
288
+ {
289
+ tokenIn: params.tokenIn,
290
+ tokenOut: params.tokenOut,
291
+ amountIn: params.amountIn,
292
+ fee: params.fee,
293
+ sqrtPriceLimitX96: 0n
294
+ }
295
+ ]
296
+ });
297
+ const { data } = await client.call({
298
+ to: UNISWAP().QUOTER_V2,
299
+ data: calldata
300
+ });
301
+ if (!data) {
302
+ throw new Error("Quoter returned no data \u2014 pool may not exist for this pair/fee");
303
+ }
304
+ const [amountOut, sqrtPriceX96After, , gasEstimate] = decodeFunctionResult({
305
+ abi: UNISWAP_QUOTER_V2_ABI,
306
+ functionName: "quoteExactInputSingle",
307
+ data
308
+ });
309
+ return { amountOut, sqrtPriceX96After, gasEstimate };
310
+ }
311
+ function applySlippage(amountOut, slippageBps) {
312
+ return amountOut * BigInt(1e4 - slippageBps) / 10000n;
313
+ }
314
+ function encodeSwapPath(tokens, fees) {
315
+ if (tokens.length < 2 || fees.length !== tokens.length - 1) {
316
+ throw new Error("Invalid path: need at least 2 tokens and (tokens-1) fees");
317
+ }
318
+ const parts = [];
319
+ for (let i = 0; i < tokens.length; i++) {
320
+ parts.push(tokens[i].toLowerCase());
321
+ if (i < fees.length) {
322
+ parts.push(pad(numberToHex(fees[i]), { size: 3 }));
323
+ }
324
+ }
325
+ return concat(parts);
326
+ }
327
+ async function getMultiHopQuote(params) {
328
+ const client = getPublicClient();
329
+ const calldata = encodeFunctionData2({
330
+ abi: UNISWAP_QUOTER_V2_ABI,
331
+ functionName: "quoteExactInput",
332
+ args: [params.path, params.amountIn]
333
+ });
334
+ const { data } = await client.call({
335
+ to: UNISWAP().QUOTER_V2,
336
+ data: calldata
337
+ });
338
+ if (!data) {
339
+ throw new Error("Quoter returned no data \u2014 pool may not exist for this path");
340
+ }
341
+ const [amountOut, , , gasEstimate] = decodeFunctionResult({
342
+ abi: UNISWAP_QUOTER_V2_ABI,
343
+ functionName: "quoteExactInput",
344
+ data
345
+ });
346
+ return { amountOut, sqrtPriceX96After: 0n, gasEstimate };
347
+ }
348
+
349
+ // src/lib/batch.ts
350
+ function formatBatch(calls) {
351
+ return calls.map((call, i) => {
352
+ const selector = call.data.slice(0, 10);
353
+ return ` ${i + 1}. ${call.target} :: ${selector}... (${call.value > 0n ? call.value + " wei" : "no value"})`;
354
+ }).join("\n");
355
+ }
356
+
357
+ // src/commands/strategy-run.ts
358
+ var VALID_FEES = [500, 3e3, 1e4];
359
+ async function runLeveredSwap(opts) {
360
+ const vaultAddress = opts.vault;
361
+ if (!isAddress(opts.token)) {
362
+ console.error(chalk.red(`Invalid token address: ${opts.token}`));
363
+ process.exit(1);
364
+ }
365
+ const targetToken = opts.token;
366
+ const feeTier = Number(opts.fee);
367
+ if (!VALID_FEES.includes(feeTier)) {
368
+ console.error(chalk.red(`Invalid fee tier: ${opts.fee}. Valid: ${VALID_FEES.join(", ")}`));
369
+ process.exit(1);
370
+ }
371
+ const slippageBps = Number(opts.slippage);
372
+ const client = getPublicClient();
373
+ let targetDecimals;
374
+ let borrowDecimals;
375
+ try {
376
+ [targetDecimals, borrowDecimals] = await Promise.all([
377
+ client.readContract({
378
+ address: targetToken,
379
+ abi: ERC20_ABI,
380
+ functionName: "decimals"
381
+ }),
382
+ client.readContract({
383
+ address: TOKENS().USDC,
384
+ abi: ERC20_ABI,
385
+ functionName: "decimals"
386
+ })
387
+ ]);
388
+ } catch {
389
+ console.error(chalk.red(`Could not read token decimals \u2014 are the addresses valid ERC20s?`));
390
+ process.exit(1);
391
+ }
392
+ console.log();
393
+ console.log(chalk.bold("Levered Swap Strategy"));
394
+ console.log(chalk.dim("\u2500".repeat(40)));
395
+ console.log(` Collateral: ${opts.collateral} WETH (agent-provided)`);
396
+ console.log(` Borrow: ${opts.borrow} USDC (from Moonwell)`);
397
+ console.log(` Buy: ${targetToken} (${targetDecimals} decimals)`);
398
+ console.log(` Fee tier: ${(feeTier / 1e4 * 100).toFixed(2)}%`);
399
+ console.log(` Slippage: ${(slippageBps / 100).toFixed(2)}%`);
400
+ console.log(` Vault: ${vaultAddress}`);
401
+ console.log();
402
+ const spinner = ora("Fetching Uniswap quote...").start();
403
+ let amountOut;
404
+ let minOut;
405
+ try {
406
+ const borrowAmount = parseUnits2(opts.borrow, borrowDecimals);
407
+ const quote = await getQuote({
408
+ tokenIn: TOKENS().USDC,
409
+ tokenOut: targetToken,
410
+ amountIn: borrowAmount,
411
+ fee: feeTier
412
+ });
413
+ amountOut = quote.amountOut;
414
+ minOut = applySlippage(amountOut, slippageBps);
415
+ spinner.succeed(
416
+ `Quote: ${formatUnits(amountOut, targetDecimals)} tokens (min: ${formatUnits(minOut, targetDecimals)}, gas est: ${quote.gasEstimate})`
417
+ );
418
+ } catch (err) {
419
+ spinner.fail("Failed to fetch quote");
420
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
421
+ process.exit(1);
422
+ }
423
+ const config = {
424
+ collateralAmount: opts.collateral,
425
+ borrowAmount: opts.borrow,
426
+ targetToken,
427
+ fee: feeTier,
428
+ slippageBps,
429
+ profitTargetBps: 2e3,
430
+ // 20% default
431
+ stopLossBps: 1e3
432
+ // 10% default
433
+ };
434
+ const calls = buildEntryBatch(config, vaultAddress, minOut, borrowDecimals);
435
+ console.log();
436
+ console.log(chalk.bold("Batch calls (6):"));
437
+ console.log(formatBatch(calls));
438
+ console.log();
439
+ const assetAmount = 0n;
440
+ const simSpinner = ora("Simulating via vault...").start();
441
+ try {
442
+ const results = await simulateBatch(calls);
443
+ const allSucceeded = results.every((r) => r.success);
444
+ if (allSucceeded) {
445
+ simSpinner.succeed("Simulation passed");
446
+ } else {
447
+ simSpinner.fail("Simulation: some calls failed");
448
+ for (let i = 0; i < results.length; i++) {
449
+ const status = results[i].success ? "\u2713" : "\u2717";
450
+ console.log(` ${status} Call ${i + 1}`);
451
+ }
452
+ if (!opts.execute) {
453
+ process.exit(1);
454
+ }
455
+ console.log(chalk.yellow("Continuing to execution despite simulation failure..."));
456
+ }
457
+ } catch (err) {
458
+ simSpinner.fail("Simulation failed");
459
+ const msg = err instanceof Error ? err.message : String(err);
460
+ console.error(chalk.red(msg));
461
+ if (!opts.execute) {
462
+ process.exit(1);
463
+ }
464
+ console.log(chalk.yellow("Continuing to execution despite simulation failure..."));
465
+ }
466
+ if (!opts.execute) {
467
+ console.log();
468
+ console.log(chalk.yellow("Dry run complete. Add --execute to submit on-chain."));
469
+ console.log(chalk.dim(" Prerequisite: send WETH to vault before executing."));
470
+ return;
471
+ }
472
+ const execSpinner = ora("Executing batch via vault...").start();
473
+ try {
474
+ const txHash = await executeBatch(calls, assetAmount);
475
+ execSpinner.succeed(`Batch executed: ${txHash}`);
476
+ console.log(chalk.dim(` ${getExplorerUrl(txHash)}`));
477
+ } catch (err) {
478
+ execSpinner.fail("Execution failed");
479
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
480
+ process.exit(1);
481
+ }
482
+ }
483
+
484
+ // src/lib/factory.ts
485
+ import { decodeEventLog } from "viem";
486
+ function getFactoryAddress() {
487
+ return SHERWOOD().FACTORY;
488
+ }
489
+ async function createSyndicate(params) {
490
+ const wallet = getWalletClient();
491
+ const client = getPublicClient();
492
+ const hash = await wallet.writeContract({
493
+ account: getAccount(),
494
+ chain: getChain(),
495
+ address: getFactoryAddress(),
496
+ abi: SYNDICATE_FACTORY_ABI,
497
+ functionName: "createSyndicate",
498
+ args: [
499
+ params.creatorAgentId,
500
+ {
501
+ metadataURI: params.metadataURI,
502
+ asset: params.asset,
503
+ name: params.name,
504
+ symbol: params.symbol,
505
+ caps: {
506
+ maxPerTx: params.maxPerTx,
507
+ maxDailyTotal: params.maxDailyTotal,
508
+ maxBorrowRatio: params.maxBorrowRatio
509
+ },
510
+ initialTargets: params.initialTargets,
511
+ openDeposits: params.openDeposits,
512
+ subdomain: params.subdomain
513
+ }
514
+ ]
515
+ });
516
+ const receipt = await client.waitForTransactionReceipt({ hash });
517
+ for (const log of receipt.logs) {
518
+ try {
519
+ const event = decodeEventLog({
520
+ abi: SYNDICATE_FACTORY_ABI,
521
+ data: log.data,
522
+ topics: log.topics
523
+ });
524
+ if (event.eventName === "SyndicateCreated") {
525
+ const args = event.args;
526
+ return {
527
+ hash,
528
+ syndicateId: args.id,
529
+ vault: args.vault
530
+ };
531
+ }
532
+ } catch {
533
+ }
534
+ }
535
+ const count = await getSyndicateCount();
536
+ const info = await getSyndicate(count);
537
+ return {
538
+ hash,
539
+ syndicateId: count,
540
+ vault: info.vault
541
+ };
542
+ }
543
+ async function getSyndicate(id) {
544
+ const client = getPublicClient();
545
+ const result = await client.readContract({
546
+ address: getFactoryAddress(),
547
+ abi: SYNDICATE_FACTORY_ABI,
548
+ functionName: "syndicates",
549
+ args: [id]
550
+ });
551
+ return {
552
+ id: result[0],
553
+ vault: result[1],
554
+ creator: result[2],
555
+ metadataURI: result[3],
556
+ createdAt: result[4],
557
+ active: result[5],
558
+ subdomain: result[6]
559
+ };
560
+ }
561
+ async function getSyndicateCount() {
562
+ const client = getPublicClient();
563
+ return client.readContract({
564
+ address: getFactoryAddress(),
565
+ abi: SYNDICATE_FACTORY_ABI,
566
+ functionName: "syndicateCount"
567
+ });
568
+ }
569
+ async function getActiveSyndicates() {
570
+ const client = getPublicClient();
571
+ const result = await client.readContract({
572
+ address: getFactoryAddress(),
573
+ abi: SYNDICATE_FACTORY_ABI,
574
+ functionName: "getActiveSyndicates"
575
+ });
576
+ return result.map((s) => ({
577
+ id: s.id,
578
+ vault: s.vault,
579
+ creator: s.creator,
580
+ metadataURI: s.metadataURI,
581
+ createdAt: s.createdAt,
582
+ active: s.active,
583
+ subdomain: s.subdomain
584
+ }));
585
+ }
586
+ async function updateMetadata(syndicateId, metadataURI) {
587
+ const wallet = getWalletClient();
588
+ return wallet.writeContract({
589
+ account: getAccount(),
590
+ chain: getChain(),
591
+ address: getFactoryAddress(),
592
+ abi: SYNDICATE_FACTORY_ABI,
593
+ functionName: "updateMetadata",
594
+ args: [syndicateId, metadataURI]
595
+ });
596
+ }
597
+
598
+ // src/lib/subgraph.ts
599
+ function getSubgraphUrl() {
600
+ const url = process.env.SUBGRAPH_URL;
601
+ if (!url) {
602
+ throw new Error(
603
+ "SUBGRAPH_URL env var is required. Set it to your The Graph Studio query endpoint."
604
+ );
605
+ }
606
+ return url;
607
+ }
608
+ async function query(graphql, variables) {
609
+ const url = getSubgraphUrl();
610
+ const response = await fetch(url, {
611
+ method: "POST",
612
+ headers: { "Content-Type": "application/json" },
613
+ body: JSON.stringify({ query: graphql, variables })
614
+ });
615
+ if (!response.ok) {
616
+ const text = await response.text();
617
+ throw new Error(`Subgraph query failed (${response.status}): ${text}`);
618
+ }
619
+ const result = await response.json();
620
+ if (result.errors?.length) {
621
+ throw new Error(`Subgraph query error: ${result.errors.map((e) => e.message).join(", ")}`);
622
+ }
623
+ if (!result.data) {
624
+ throw new Error("Subgraph returned no data");
625
+ }
626
+ return result.data;
627
+ }
628
+ async function getActiveSyndicates2(creator) {
629
+ const where = creator ? `where: { active: true, creator: "${creator.toLowerCase()}" }` : `where: { active: true }`;
630
+ const data = await query(`
631
+ {
632
+ syndicates(${where}, orderBy: createdAt, orderDirection: desc, first: 100) {
633
+ id
634
+ vault
635
+ creator
636
+ metadataURI
637
+ createdAt
638
+ active
639
+ totalDeposits
640
+ totalWithdrawals
641
+ }
642
+ }
643
+ `);
644
+ return data.syndicates;
645
+ }
646
+
647
+ // src/lib/registry.ts
648
+ function getRegistryAddress() {
649
+ return SHERWOOD().STRATEGY_REGISTRY;
650
+ }
651
+ async function registerStrategy(implementation, strategyTypeId, name, metadataURI) {
652
+ const wallet = getWalletClient();
653
+ return wallet.writeContract({
654
+ account: getAccount(),
655
+ chain: getChain(),
656
+ address: getRegistryAddress(),
657
+ abi: STRATEGY_REGISTRY_ABI,
658
+ functionName: "registerStrategy",
659
+ args: [implementation, strategyTypeId, name, metadataURI]
660
+ });
661
+ }
662
+ async function getStrategy(id) {
663
+ const client = getPublicClient();
664
+ const result = await client.readContract({
665
+ address: getRegistryAddress(),
666
+ abi: STRATEGY_REGISTRY_ABI,
667
+ functionName: "getStrategy",
668
+ args: [id]
669
+ });
670
+ return {
671
+ id,
672
+ implementation: result.implementation,
673
+ creator: result.creator,
674
+ strategyTypeId: result.strategyTypeId,
675
+ active: result.active,
676
+ name: result.name,
677
+ metadataURI: result.metadataURI
678
+ };
679
+ }
680
+ async function listStrategies(typeId) {
681
+ const client = getPublicClient();
682
+ const registryAddress = getRegistryAddress();
683
+ let ids;
684
+ if (typeId !== void 0) {
685
+ ids = await client.readContract({
686
+ address: registryAddress,
687
+ abi: STRATEGY_REGISTRY_ABI,
688
+ functionName: "getStrategiesByType",
689
+ args: [typeId]
690
+ });
691
+ } else {
692
+ const count = await client.readContract({
693
+ address: registryAddress,
694
+ abi: STRATEGY_REGISTRY_ABI,
695
+ functionName: "strategyCount"
696
+ });
697
+ ids = Array.from({ length: Number(count) }, (_, i) => BigInt(i + 1));
698
+ }
699
+ const strategies = [];
700
+ for (const id of ids) {
701
+ const s = await getStrategy(id);
702
+ strategies.push(s);
703
+ }
704
+ return strategies;
705
+ }
706
+
707
+ // src/lib/ipfs.ts
708
+ var BUNDLED_PINATA_JWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySW5mb3JtYXRpb24iOnsiaWQiOiI2NDQ0MGViOC1hYTYyLTQzY2EtOGYwNC04MDZjZmNjY2Y4YTUiLCJlbWFpbCI6ImltdGhhdGNhcmxvc0BnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwicGluX3BvbGljeSI6eyJyZWdpb25zIjpbeyJkZXNpcmVkUmVwbGljYXRpb25Db3VudCI6MSwiaWQiOiJGUkExIn1dLCJ2ZXJzaW9uIjoxfSwibWZhX2VuYWJsZWQiOmZhbHNlLCJzdGF0dXMiOiJBQ1RJVkUifSwiYXV0aGVudGljYXRpb25UeXBlIjoic2NvcGVkS2V5Iiwic2NvcGVkS2V5S2V5IjoiMWJhZWFmMzQwODM3MGQ0NGZkZWEiLCJzY29wZWRLZXlTZWNyZXQiOiIzNDcxMmU5MTkyYTgxNWFhMGRmNjUyYjYyMDQzODQ1MDJjMmU0YWE0MDhkZTJmOTU2NWYwOTk3YTNlY2U3NGU3IiwiZXhwIjoxODAxMjc2ODExfQ.7OMJiOATpqkSwe7Orrpt2b8H_-czH-W61vBm4AHtqfA";
709
+ function getPinataApiKey() {
710
+ return process.env.PINATA_API_KEY || BUNDLED_PINATA_JWT;
711
+ }
712
+ async function uploadMetadata(metadata) {
713
+ const apiKey = getPinataApiKey();
714
+ const response = await fetch("https://api.pinata.cloud/pinning/pinJSONToIPFS", {
715
+ method: "POST",
716
+ headers: {
717
+ "Content-Type": "application/json",
718
+ Authorization: `Bearer ${apiKey}`
719
+ },
720
+ body: JSON.stringify({
721
+ pinataContent: metadata,
722
+ pinataMetadata: {
723
+ name: `sherwood-syndicate-${metadata.name.toLowerCase().replace(/\s+/g, "-")}`
724
+ }
725
+ })
726
+ });
727
+ if (!response.ok) {
728
+ const text = await response.text();
729
+ throw new Error(`Pinata upload failed (${response.status}): ${text}`);
730
+ }
731
+ const result = await response.json();
732
+ return `ipfs://${result.IpfsHash}`;
733
+ }
734
+
735
+ // src/commands/venice.ts
736
+ import { parseUnits as parseUnits5, formatUnits as formatUnits3, isAddress as isAddress2 } from "viem";
737
+ import chalk2 from "chalk";
738
+ import ora2 from "ora";
739
+
740
+ // src/strategies/venice-fund.ts
741
+ import { encodeFunctionData as encodeFunctionData3, parseUnits as parseUnits4 } from "viem";
742
+ var ERC20_ABI3 = [
743
+ {
744
+ name: "approve",
745
+ type: "function",
746
+ inputs: [
747
+ { name: "spender", type: "address" },
748
+ { name: "amount", type: "uint256" }
749
+ ],
750
+ outputs: [{ name: "", type: "bool" }]
751
+ }
752
+ ];
753
+ var SWAP_ROUTER_EXACT_INPUT_SINGLE_ABI = [
754
+ {
755
+ name: "exactInputSingle",
756
+ type: "function",
757
+ inputs: [
758
+ {
759
+ name: "params",
760
+ type: "tuple",
761
+ components: [
762
+ { name: "tokenIn", type: "address" },
763
+ { name: "tokenOut", type: "address" },
764
+ { name: "fee", type: "uint24" },
765
+ { name: "recipient", type: "address" },
766
+ { name: "amountIn", type: "uint256" },
767
+ { name: "amountOutMinimum", type: "uint256" },
768
+ { name: "sqrtPriceLimitX96", type: "uint160" }
769
+ ]
770
+ }
771
+ ],
772
+ outputs: [{ name: "amountOut", type: "uint256" }]
773
+ }
774
+ ];
775
+ var SWAP_ROUTER_EXACT_INPUT_ABI = [
776
+ {
777
+ name: "exactInput",
778
+ type: "function",
779
+ inputs: [
780
+ {
781
+ name: "params",
782
+ type: "tuple",
783
+ components: [
784
+ { name: "path", type: "bytes" },
785
+ { name: "recipient", type: "address" },
786
+ { name: "amountIn", type: "uint256" },
787
+ { name: "amountOutMinimum", type: "uint256" }
788
+ ]
789
+ }
790
+ ],
791
+ outputs: [{ name: "amountOut", type: "uint256" }]
792
+ }
793
+ ];
794
+ var STAKING_ABI = [
795
+ {
796
+ name: "stake",
797
+ type: "function",
798
+ inputs: [
799
+ { name: "recipient", type: "address" },
800
+ { name: "amount", type: "uint256" }
801
+ ],
802
+ outputs: []
803
+ }
804
+ ];
805
+ function buildFundBatch(config, vaultAddress, agents, assetAddress, assetDecimals, minVVV, swapPath) {
806
+ const ZERO = "0x0000000000000000000000000000000000000000";
807
+ if (VENICE().VVV === ZERO || VENICE().STAKING === ZERO) {
808
+ throw new Error("Venice (VVV/sVVV) is not deployed on this network \u2014 venice fund requires Venice staking contracts");
809
+ }
810
+ const assetAmount = parseUnits4(config.amount, assetDecimals);
811
+ const isWeth = assetAddress.toLowerCase() === TOKENS().WETH.toLowerCase();
812
+ const calls = [];
813
+ calls.push({
814
+ target: assetAddress,
815
+ data: encodeFunctionData3({
816
+ abi: ERC20_ABI3,
817
+ functionName: "approve",
818
+ args: [UNISWAP().SWAP_ROUTER, assetAmount]
819
+ }),
820
+ value: 0n
821
+ });
822
+ if (isWeth) {
823
+ calls.push({
824
+ target: UNISWAP().SWAP_ROUTER,
825
+ data: encodeFunctionData3({
826
+ abi: SWAP_ROUTER_EXACT_INPUT_SINGLE_ABI,
827
+ functionName: "exactInputSingle",
828
+ args: [
829
+ {
830
+ tokenIn: TOKENS().WETH,
831
+ tokenOut: VENICE().VVV,
832
+ fee: config.fee2,
833
+ recipient: vaultAddress,
834
+ amountIn: assetAmount,
835
+ amountOutMinimum: minVVV,
836
+ sqrtPriceLimitX96: 0n
837
+ }
838
+ ]
839
+ }),
840
+ value: 0n
841
+ });
842
+ } else {
843
+ calls.push({
844
+ target: UNISWAP().SWAP_ROUTER,
845
+ data: encodeFunctionData3({
846
+ abi: SWAP_ROUTER_EXACT_INPUT_ABI,
847
+ functionName: "exactInput",
848
+ args: [
849
+ {
850
+ path: swapPath,
851
+ recipient: vaultAddress,
852
+ amountIn: assetAmount,
853
+ amountOutMinimum: minVVV
854
+ }
855
+ ]
856
+ }),
857
+ value: 0n
858
+ });
859
+ }
860
+ calls.push({
861
+ target: VENICE().VVV,
862
+ data: encodeFunctionData3({
863
+ abi: ERC20_ABI3,
864
+ functionName: "approve",
865
+ args: [VENICE().STAKING, minVVV]
866
+ }),
867
+ value: 0n
868
+ });
869
+ const perAgent = minVVV / BigInt(agents.length);
870
+ for (const agent of agents) {
871
+ calls.push({
872
+ target: VENICE().STAKING,
873
+ data: encodeFunctionData3({
874
+ abi: STAKING_ABI,
875
+ functionName: "stake",
876
+ args: [agent, perAgent]
877
+ }),
878
+ value: 0n
879
+ });
880
+ }
881
+ return calls;
882
+ }
883
+
884
+ // src/lib/venice.ts
885
+ var VENICE_API_BASE = "https://api.venice.ai/api/v1";
886
+ async function provisionApiKey() {
887
+ const account = getAccount();
888
+ const tokenRes = await fetch(`${VENICE_API_BASE}/api_keys/generate_web3_key`);
889
+ if (!tokenRes.ok) {
890
+ throw new Error(`Failed to get validation token: ${tokenRes.status} ${tokenRes.statusText}`);
891
+ }
892
+ const tokenData = await tokenRes.json();
893
+ const token = tokenData.data.token;
894
+ const signature = await account.signMessage({ message: token });
895
+ const keyRes = await fetch(`${VENICE_API_BASE}/api_keys/generate_web3_key`, {
896
+ method: "POST",
897
+ headers: { "Content-Type": "application/json" },
898
+ body: JSON.stringify({
899
+ address: account.address,
900
+ signature,
901
+ token,
902
+ apiKeyType: "INFERENCE",
903
+ description: "Sherwood syndicate agent"
904
+ })
905
+ });
906
+ if (!keyRes.ok) {
907
+ const body = await keyRes.text();
908
+ throw new Error(`Failed to generate API key: ${keyRes.status} ${body}`);
909
+ }
910
+ const keyData = await keyRes.json();
911
+ const apiKey = keyData.data.apiKey;
912
+ setVeniceApiKey(apiKey);
913
+ return apiKey;
914
+ }
915
+ async function checkApiKeyValid() {
916
+ const apiKey = getVeniceApiKey();
917
+ if (!apiKey) return false;
918
+ try {
919
+ const res = await fetch(`${VENICE_API_BASE}/models`, {
920
+ headers: { Authorization: `Bearer ${apiKey}` }
921
+ });
922
+ return res.ok;
923
+ } catch {
924
+ return false;
925
+ }
926
+ }
927
+
928
+ // src/commands/venice.ts
929
+ var VALID_FEES2 = [500, 3e3, 1e4];
930
+ function registerVeniceCommands(program2) {
931
+ const venice = program2.command("venice").description("Venice private inference \u2014 stake VVV, provision API keys");
932
+ venice.command("fund").description("Swap vault profits \u2192 VVV \u2192 stake \u2192 distribute sVVV to agents").requiredOption("--vault <address>", "Vault address").requiredOption("--amount <amount>", "Deposit token amount to convert (e.g. 500)").option("--fee1 <tier>", "Fee tier for asset \u2192 WETH hop (500, 3000, 10000)", "3000").option("--fee2 <tier>", "Fee tier for WETH \u2192 VVV hop", "10000").option("--slippage <bps>", "Slippage tolerance in bps", "100").option("--execute", "Execute on-chain (default: simulate only)", false).action(async (opts) => {
933
+ const vaultAddress = opts.vault;
934
+ if (!isAddress2(vaultAddress)) {
935
+ console.error(chalk2.red(`Invalid vault address: ${opts.vault}`));
936
+ process.exit(1);
937
+ }
938
+ const fee1 = Number(opts.fee1);
939
+ const fee2 = Number(opts.fee2);
940
+ if (!VALID_FEES2.includes(fee1) || !VALID_FEES2.includes(fee2)) {
941
+ console.error(chalk2.red(`Invalid fee tier. Valid: ${VALID_FEES2.join(", ")}`));
942
+ process.exit(1);
943
+ }
944
+ const slippageBps = Number(opts.slippage);
945
+ const client = getPublicClient();
946
+ const spinner = ora2("Reading vault state...").start();
947
+ let assetAddress;
948
+ let assetDecimals;
949
+ let assetSymbol;
950
+ let totalDeposited;
951
+ let assetBalance;
952
+ let agents;
953
+ try {
954
+ [assetAddress, totalDeposited, agents] = await Promise.all([
955
+ client.readContract({ address: vaultAddress, abi: SYNDICATE_VAULT_ABI, functionName: "asset" }),
956
+ client.readContract({ address: vaultAddress, abi: SYNDICATE_VAULT_ABI, functionName: "totalDeposited" }),
957
+ client.readContract({ address: vaultAddress, abi: SYNDICATE_VAULT_ABI, functionName: "getAgentOperators" })
958
+ ]);
959
+ [assetDecimals, assetSymbol, assetBalance] = await Promise.all([
960
+ client.readContract({ address: assetAddress, abi: ERC20_ABI, functionName: "decimals" }),
961
+ client.readContract({ address: assetAddress, abi: ERC20_ABI, functionName: "symbol" }),
962
+ client.readContract({ address: assetAddress, abi: ERC20_ABI, functionName: "balanceOf", args: [vaultAddress] })
963
+ ]);
964
+ spinner.stop();
965
+ } catch (err) {
966
+ spinner.fail("Failed to read vault state");
967
+ console.error(chalk2.red(err instanceof Error ? err.message : String(err)));
968
+ process.exit(1);
969
+ }
970
+ if (agents.length === 0) {
971
+ console.error(chalk2.red("No agents registered in vault. Register agents first."));
972
+ process.exit(1);
973
+ }
974
+ const requestedAmount = parseUnits5(opts.amount, assetDecimals);
975
+ const profit = assetBalance > totalDeposited ? assetBalance - totalDeposited : 0n;
976
+ const isWeth = assetAddress.toLowerCase() === TOKENS().WETH.toLowerCase();
977
+ console.log();
978
+ console.log(chalk2.bold("Venice Fund"));
979
+ console.log(chalk2.dim("\u2500".repeat(40)));
980
+ console.log(` Asset: ${assetSymbol} (${assetDecimals} decimals)`);
981
+ console.log(` Amount: ${opts.amount} ${assetSymbol}`);
982
+ console.log(` Vault balance: ${formatUnits3(assetBalance, assetDecimals)} ${assetSymbol}`);
983
+ console.log(` Deposited: ${formatUnits3(totalDeposited, assetDecimals)} ${assetSymbol}`);
984
+ console.log(` Profit: ${formatUnits3(profit, assetDecimals)} ${assetSymbol}`);
985
+ console.log(` Agents: ${agents.length} (sVVV will be split equally)`);
986
+ console.log(` Routing: ${isWeth ? `WETH \u2192 VVV (fee ${fee2})` : `${assetSymbol} \u2192 WETH (fee ${fee1}) \u2192 VVV (fee ${fee2})`}`);
987
+ console.log(` Slippage: ${(slippageBps / 100).toFixed(2)}%`);
988
+ console.log(` Vault: ${vaultAddress}`);
989
+ console.log();
990
+ if (requestedAmount > profit) {
991
+ console.warn(chalk2.yellow(` Warning: amount (${opts.amount}) exceeds available profit (${formatUnits3(profit, assetDecimals)})`));
992
+ console.warn(chalk2.yellow(" This will use deposited capital, not just profits."));
993
+ console.log();
994
+ }
995
+ const quoteSpinner = ora2("Fetching Uniswap quote...").start();
996
+ let amountOut;
997
+ let minOut;
998
+ let swapPath = null;
999
+ try {
1000
+ if (isWeth) {
1001
+ const quote = await getQuote({
1002
+ tokenIn: TOKENS().WETH,
1003
+ tokenOut: VENICE().VVV,
1004
+ amountIn: requestedAmount,
1005
+ fee: fee2
1006
+ });
1007
+ amountOut = quote.amountOut;
1008
+ } else {
1009
+ swapPath = encodeSwapPath(
1010
+ [assetAddress, TOKENS().WETH, VENICE().VVV],
1011
+ [fee1, fee2]
1012
+ );
1013
+ const quote = await getMultiHopQuote({
1014
+ path: swapPath,
1015
+ amountIn: requestedAmount
1016
+ });
1017
+ amountOut = quote.amountOut;
1018
+ }
1019
+ minOut = applySlippage(amountOut, slippageBps);
1020
+ quoteSpinner.succeed(
1021
+ `Quote: ${formatUnits3(amountOut, 18)} VVV (min: ${formatUnits3(minOut, 18)}, per agent: ${formatUnits3(minOut / BigInt(agents.length), 18)})`
1022
+ );
1023
+ } catch (err) {
1024
+ quoteSpinner.fail("Failed to fetch quote");
1025
+ console.error(chalk2.red(err instanceof Error ? err.message : String(err)));
1026
+ process.exit(1);
1027
+ }
1028
+ const config = {
1029
+ amount: opts.amount,
1030
+ fee1,
1031
+ fee2,
1032
+ slippageBps
1033
+ };
1034
+ const calls = buildFundBatch(config, vaultAddress, agents, assetAddress, assetDecimals, minOut, swapPath);
1035
+ console.log();
1036
+ console.log(chalk2.bold(`Batch calls (${calls.length}):`));
1037
+ console.log(formatBatch(calls));
1038
+ console.log();
1039
+ const simSpinner = ora2("Simulating via vault...").start();
1040
+ try {
1041
+ const results = await simulateBatch(calls);
1042
+ const allSucceeded = results.every((r) => r.success);
1043
+ if (allSucceeded) {
1044
+ simSpinner.succeed("Simulation passed");
1045
+ } else {
1046
+ simSpinner.fail("Simulation: some calls failed");
1047
+ for (let i = 0; i < results.length; i++) {
1048
+ const status = results[i].success ? "ok" : "FAIL";
1049
+ console.log(` ${status} Call ${i + 1}`);
1050
+ }
1051
+ if (!opts.execute) process.exit(1);
1052
+ console.log(chalk2.yellow("Continuing to execution despite simulation failure..."));
1053
+ }
1054
+ } catch (err) {
1055
+ simSpinner.fail("Simulation failed");
1056
+ console.error(chalk2.red(err instanceof Error ? err.message : String(err)));
1057
+ if (!opts.execute) process.exit(1);
1058
+ console.log(chalk2.yellow("Continuing to execution despite simulation failure..."));
1059
+ }
1060
+ if (!opts.execute) {
1061
+ console.log();
1062
+ console.log(chalk2.yellow("Dry run complete. Add --execute to submit on-chain."));
1063
+ return;
1064
+ }
1065
+ const execSpinner = ora2("Executing batch via vault...").start();
1066
+ try {
1067
+ const txHash = await executeBatch(calls, requestedAmount);
1068
+ execSpinner.succeed(`Batch executed: ${txHash}`);
1069
+ console.log(chalk2.dim(` ${getExplorerUrl(txHash)}`));
1070
+ } catch (err) {
1071
+ execSpinner.fail("Execution failed");
1072
+ console.error(chalk2.red(err instanceof Error ? err.message : String(err)));
1073
+ process.exit(1);
1074
+ }
1075
+ });
1076
+ venice.command("provision").description("Self-provision a Venice API key (requires sVVV in wallet)").action(async () => {
1077
+ const account = getAccount();
1078
+ const client = getPublicClient();
1079
+ const checkSpinner = ora2("Checking sVVV balance...").start();
1080
+ try {
1081
+ const sVvvBalance = await client.readContract({
1082
+ address: VENICE().STAKING,
1083
+ abi: VENICE_STAKING_ABI,
1084
+ functionName: "balanceOf",
1085
+ args: [account.address]
1086
+ });
1087
+ if (sVvvBalance === 0n) {
1088
+ checkSpinner.fail("No sVVV found in wallet");
1089
+ console.log(chalk2.yellow(" Your wallet must hold staked VVV (sVVV) to provision a Venice API key."));
1090
+ console.log(chalk2.yellow(" Run 'sherwood venice fund' first to distribute sVVV to agents."));
1091
+ process.exit(1);
1092
+ }
1093
+ checkSpinner.succeed(`sVVV balance: ${formatUnits3(sVvvBalance, 18)}`);
1094
+ } catch (err) {
1095
+ checkSpinner.fail("Failed to check sVVV balance");
1096
+ console.error(chalk2.red(err instanceof Error ? err.message : String(err)));
1097
+ process.exit(1);
1098
+ }
1099
+ const keySpinner = ora2("Provisioning Venice API key...").start();
1100
+ try {
1101
+ const apiKey = await provisionApiKey();
1102
+ keySpinner.succeed("Venice API key provisioned");
1103
+ console.log(chalk2.dim(` Key: ${apiKey.slice(0, 8)}...${apiKey.slice(-4)}`));
1104
+ console.log(chalk2.dim(" Saved to ~/.sherwood/config.json"));
1105
+ } catch (err) {
1106
+ keySpinner.fail("Failed to provision API key");
1107
+ console.error(chalk2.red(err instanceof Error ? err.message : String(err)));
1108
+ process.exit(1);
1109
+ }
1110
+ });
1111
+ venice.command("status").description("Show Venice inference status: sVVV balances, DIEM, API key").requiredOption("--vault <address>", "Vault address").action(async (opts) => {
1112
+ const vaultAddress = opts.vault;
1113
+ if (!isAddress2(vaultAddress)) {
1114
+ console.error(chalk2.red(`Invalid vault address: ${opts.vault}`));
1115
+ process.exit(1);
1116
+ }
1117
+ const client = getPublicClient();
1118
+ const account = getAccount();
1119
+ const spinner = ora2("Loading Venice status...").start();
1120
+ try {
1121
+ const [assetAddress, totalDeposited, agents] = await Promise.all([
1122
+ client.readContract({ address: vaultAddress, abi: SYNDICATE_VAULT_ABI, functionName: "asset" }),
1123
+ client.readContract({ address: vaultAddress, abi: SYNDICATE_VAULT_ABI, functionName: "totalDeposited" }),
1124
+ client.readContract({ address: vaultAddress, abi: SYNDICATE_VAULT_ABI, functionName: "getAgentOperators" })
1125
+ ]);
1126
+ const [assetDecimals, assetSymbol, assetBalance] = await Promise.all([
1127
+ client.readContract({ address: assetAddress, abi: ERC20_ABI, functionName: "decimals" }),
1128
+ client.readContract({ address: assetAddress, abi: ERC20_ABI, functionName: "symbol" }),
1129
+ client.readContract({ address: assetAddress, abi: ERC20_ABI, functionName: "balanceOf", args: [vaultAddress] })
1130
+ ]);
1131
+ const vaultVvvBalance = await client.readContract({
1132
+ address: VENICE().VVV,
1133
+ abi: ERC20_ABI,
1134
+ functionName: "balanceOf",
1135
+ args: [vaultAddress]
1136
+ });
1137
+ const agentBalances = await Promise.all(
1138
+ agents.map(async (agent) => {
1139
+ const bal = await client.readContract({
1140
+ address: VENICE().STAKING,
1141
+ abi: VENICE_STAKING_ABI,
1142
+ functionName: "balanceOf",
1143
+ args: [agent]
1144
+ });
1145
+ return { agent, balance: bal };
1146
+ })
1147
+ );
1148
+ const [mySvvv, myPending] = await Promise.all([
1149
+ client.readContract({
1150
+ address: VENICE().STAKING,
1151
+ abi: VENICE_STAKING_ABI,
1152
+ functionName: "balanceOf",
1153
+ args: [account.address]
1154
+ }),
1155
+ client.readContract({
1156
+ address: VENICE().STAKING,
1157
+ abi: VENICE_STAKING_ABI,
1158
+ functionName: "pendingRewards",
1159
+ args: [account.address]
1160
+ })
1161
+ ]);
1162
+ const apiKeyValid = await checkApiKeyValid();
1163
+ const apiKey = getVeniceApiKey();
1164
+ spinner.stop();
1165
+ const profit = assetBalance > totalDeposited ? assetBalance - totalDeposited : 0n;
1166
+ console.log();
1167
+ console.log(chalk2.bold("Venice Inference Status"));
1168
+ console.log(chalk2.dim("\u2500".repeat(50)));
1169
+ console.log(chalk2.bold("\n Vault"));
1170
+ console.log(` Profit available: ${formatUnits3(profit, assetDecimals)} ${assetSymbol}`);
1171
+ console.log(` VVV (unstaked): ${formatUnits3(vaultVvvBalance, 18)}`);
1172
+ console.log(chalk2.bold("\n Agent sVVV Balances"));
1173
+ for (const { agent, balance } of agentBalances) {
1174
+ const isMe = agent.toLowerCase() === account.address.toLowerCase();
1175
+ const label = isMe ? chalk2.green(`${agent} (you)`) : agent;
1176
+ console.log(` ${label}: ${formatUnits3(balance, 18)} sVVV`);
1177
+ }
1178
+ console.log(chalk2.bold("\n Your Wallet"));
1179
+ console.log(` sVVV: ${formatUnits3(mySvvv, 18)}`);
1180
+ console.log(` Pending rewards: ${formatUnits3(myPending, 18)} VVV`);
1181
+ console.log(chalk2.bold("\n Venice API"));
1182
+ console.log(` Key: ${apiKey ? `${apiKey.slice(0, 8)}...${apiKey.slice(-4)}` : chalk2.dim("not provisioned")}`);
1183
+ console.log(` Status: ${apiKeyValid ? chalk2.green("valid") : chalk2.red("invalid/missing")}`);
1184
+ console.log();
1185
+ } catch (err) {
1186
+ spinner.fail("Failed to load status");
1187
+ console.error(chalk2.red(err instanceof Error ? err.message : String(err)));
1188
+ process.exit(1);
1189
+ }
1190
+ });
1191
+ }
1192
+
1193
+ // src/commands/allowance.ts
1194
+ import { parseUnits as parseUnits7, formatUnits as formatUnits4, isAddress as isAddress3 } from "viem";
1195
+ import chalk3 from "chalk";
1196
+ import ora3 from "ora";
1197
+
1198
+ // src/strategies/allowance-disburse.ts
1199
+ import { encodeFunctionData as encodeFunctionData4, parseUnits as parseUnits6 } from "viem";
1200
+ var ERC20_ABI4 = [
1201
+ {
1202
+ name: "approve",
1203
+ type: "function",
1204
+ inputs: [
1205
+ { name: "spender", type: "address" },
1206
+ { name: "amount", type: "uint256" }
1207
+ ],
1208
+ outputs: [{ name: "", type: "bool" }]
1209
+ },
1210
+ {
1211
+ name: "transfer",
1212
+ type: "function",
1213
+ inputs: [
1214
+ { name: "to", type: "address" },
1215
+ { name: "amount", type: "uint256" }
1216
+ ],
1217
+ outputs: [{ name: "", type: "bool" }]
1218
+ }
1219
+ ];
1220
+ var SWAP_ROUTER_EXACT_INPUT_SINGLE_ABI2 = [
1221
+ {
1222
+ name: "exactInputSingle",
1223
+ type: "function",
1224
+ inputs: [
1225
+ {
1226
+ name: "params",
1227
+ type: "tuple",
1228
+ components: [
1229
+ { name: "tokenIn", type: "address" },
1230
+ { name: "tokenOut", type: "address" },
1231
+ { name: "fee", type: "uint24" },
1232
+ { name: "recipient", type: "address" },
1233
+ { name: "amountIn", type: "uint256" },
1234
+ { name: "amountOutMinimum", type: "uint256" },
1235
+ { name: "sqrtPriceLimitX96", type: "uint160" }
1236
+ ]
1237
+ }
1238
+ ],
1239
+ outputs: [{ name: "amountOut", type: "uint256" }]
1240
+ }
1241
+ ];
1242
+ var SWAP_ROUTER_EXACT_INPUT_ABI2 = [
1243
+ {
1244
+ name: "exactInput",
1245
+ type: "function",
1246
+ inputs: [
1247
+ {
1248
+ name: "params",
1249
+ type: "tuple",
1250
+ components: [
1251
+ { name: "path", type: "bytes" },
1252
+ { name: "recipient", type: "address" },
1253
+ { name: "amountIn", type: "uint256" },
1254
+ { name: "amountOutMinimum", type: "uint256" }
1255
+ ]
1256
+ }
1257
+ ],
1258
+ outputs: [{ name: "amountOut", type: "uint256" }]
1259
+ }
1260
+ ];
1261
+ function buildDisburseBatch(config, vaultAddress, agents, assetAddress, assetDecimals, minUsdc, swapPath) {
1262
+ const assetAmount = parseUnits6(config.amount, assetDecimals);
1263
+ const isUsdc = assetAddress.toLowerCase() === TOKENS().USDC.toLowerCase();
1264
+ const isWeth = assetAddress.toLowerCase() === TOKENS().WETH.toLowerCase();
1265
+ const calls = [];
1266
+ if (!isUsdc) {
1267
+ calls.push({
1268
+ target: assetAddress,
1269
+ data: encodeFunctionData4({
1270
+ abi: ERC20_ABI4,
1271
+ functionName: "approve",
1272
+ args: [UNISWAP().SWAP_ROUTER, assetAmount]
1273
+ }),
1274
+ value: 0n
1275
+ });
1276
+ if (isWeth) {
1277
+ calls.push({
1278
+ target: UNISWAP().SWAP_ROUTER,
1279
+ data: encodeFunctionData4({
1280
+ abi: SWAP_ROUTER_EXACT_INPUT_SINGLE_ABI2,
1281
+ functionName: "exactInputSingle",
1282
+ args: [
1283
+ {
1284
+ tokenIn: TOKENS().WETH,
1285
+ tokenOut: TOKENS().USDC,
1286
+ fee: config.fee,
1287
+ recipient: vaultAddress,
1288
+ amountIn: assetAmount,
1289
+ amountOutMinimum: minUsdc,
1290
+ sqrtPriceLimitX96: 0n
1291
+ }
1292
+ ]
1293
+ }),
1294
+ value: 0n
1295
+ });
1296
+ } else {
1297
+ calls.push({
1298
+ target: UNISWAP().SWAP_ROUTER,
1299
+ data: encodeFunctionData4({
1300
+ abi: SWAP_ROUTER_EXACT_INPUT_ABI2,
1301
+ functionName: "exactInput",
1302
+ args: [
1303
+ {
1304
+ path: swapPath,
1305
+ recipient: vaultAddress,
1306
+ amountIn: assetAmount,
1307
+ amountOutMinimum: minUsdc
1308
+ }
1309
+ ]
1310
+ }),
1311
+ value: 0n
1312
+ });
1313
+ }
1314
+ }
1315
+ const perAgent = minUsdc / BigInt(agents.length);
1316
+ for (const agent of agents) {
1317
+ calls.push({
1318
+ target: TOKENS().USDC,
1319
+ data: encodeFunctionData4({
1320
+ abi: ERC20_ABI4,
1321
+ functionName: "transfer",
1322
+ args: [agent, perAgent]
1323
+ }),
1324
+ value: 0n
1325
+ });
1326
+ }
1327
+ return calls;
1328
+ }
1329
+
1330
+ // src/commands/allowance.ts
1331
+ var VALID_FEES3 = [500, 3e3, 1e4];
1332
+ function registerAllowanceCommands(program2) {
1333
+ const allowance = program2.command("allowance").description("Disburse vault profits to agent wallets");
1334
+ allowance.command("disburse").description("Swap vault profits \u2192 USDC \u2192 distribute to all agent operator wallets").requiredOption("--vault <address>", "Vault address").requiredOption("--amount <amount>", "Deposit token amount to convert & distribute (e.g. 500)").option("--fee <tier>", "Fee tier for asset \u2192 USDC swap (500, 3000, 10000)", "3000").option("--slippage <bps>", "Slippage tolerance in bps", "100").option("--execute", "Execute on-chain (default: simulate only)", false).action(async (opts) => {
1335
+ const vaultAddress = opts.vault;
1336
+ if (!isAddress3(vaultAddress)) {
1337
+ console.error(chalk3.red(`Invalid vault address: ${opts.vault}`));
1338
+ process.exit(1);
1339
+ }
1340
+ const fee = Number(opts.fee);
1341
+ if (!VALID_FEES3.includes(fee)) {
1342
+ console.error(chalk3.red(`Invalid fee tier. Valid: ${VALID_FEES3.join(", ")}`));
1343
+ process.exit(1);
1344
+ }
1345
+ const slippageBps = Number(opts.slippage);
1346
+ const client = getPublicClient();
1347
+ const spinner = ora3("Reading vault state...").start();
1348
+ let assetAddress;
1349
+ let assetDecimals;
1350
+ let assetSymbol;
1351
+ let totalDeposited;
1352
+ let assetBalance;
1353
+ let agents;
1354
+ try {
1355
+ [assetAddress, totalDeposited, agents] = await Promise.all([
1356
+ client.readContract({ address: vaultAddress, abi: SYNDICATE_VAULT_ABI, functionName: "asset" }),
1357
+ client.readContract({ address: vaultAddress, abi: SYNDICATE_VAULT_ABI, functionName: "totalDeposited" }),
1358
+ client.readContract({ address: vaultAddress, abi: SYNDICATE_VAULT_ABI, functionName: "getAgentOperators" })
1359
+ ]);
1360
+ [assetDecimals, assetSymbol, assetBalance] = await Promise.all([
1361
+ client.readContract({ address: assetAddress, abi: ERC20_ABI, functionName: "decimals" }),
1362
+ client.readContract({ address: assetAddress, abi: ERC20_ABI, functionName: "symbol" }),
1363
+ client.readContract({ address: assetAddress, abi: ERC20_ABI, functionName: "balanceOf", args: [vaultAddress] })
1364
+ ]);
1365
+ spinner.stop();
1366
+ } catch (err) {
1367
+ spinner.fail("Failed to read vault state");
1368
+ console.error(chalk3.red(err instanceof Error ? err.message : String(err)));
1369
+ process.exit(1);
1370
+ }
1371
+ if (agents.length === 0) {
1372
+ console.error(chalk3.red("No agents registered in vault. Register agents first."));
1373
+ process.exit(1);
1374
+ }
1375
+ const requestedAmount = parseUnits7(opts.amount, assetDecimals);
1376
+ const profit = assetBalance > totalDeposited ? assetBalance - totalDeposited : 0n;
1377
+ const isUsdc = assetAddress.toLowerCase() === TOKENS().USDC.toLowerCase();
1378
+ const isWeth = assetAddress.toLowerCase() === TOKENS().WETH.toLowerCase();
1379
+ console.log();
1380
+ console.log(chalk3.bold("Allowance Disburse"));
1381
+ console.log(chalk3.dim("\u2500".repeat(40)));
1382
+ console.log(` Asset: ${assetSymbol} (${assetDecimals} decimals)`);
1383
+ console.log(` Amount: ${opts.amount} ${assetSymbol}`);
1384
+ console.log(` Vault balance: ${formatUnits4(assetBalance, assetDecimals)} ${assetSymbol}`);
1385
+ console.log(` Deposited: ${formatUnits4(totalDeposited, assetDecimals)} ${assetSymbol}`);
1386
+ console.log(` Profit: ${formatUnits4(profit, assetDecimals)} ${assetSymbol}`);
1387
+ console.log(` Agents: ${agents.length} (USDC will be split equally)`);
1388
+ if (!isUsdc) {
1389
+ console.log(` Routing: ${isWeth ? `WETH \u2192 USDC (fee ${fee})` : `${assetSymbol} \u2192 WETH \u2192 USDC (fee ${fee})`}`);
1390
+ console.log(` Slippage: ${(slippageBps / 100).toFixed(2)}%`);
1391
+ }
1392
+ console.log(` Vault: ${vaultAddress}`);
1393
+ console.log();
1394
+ if (requestedAmount > profit) {
1395
+ console.warn(chalk3.yellow(` Warning: amount (${opts.amount}) exceeds available profit (${formatUnits4(profit, assetDecimals)})`));
1396
+ console.warn(chalk3.yellow(" This will use deposited capital, not just profits."));
1397
+ console.log();
1398
+ }
1399
+ let minUsdc;
1400
+ let swapPath = null;
1401
+ if (isUsdc) {
1402
+ minUsdc = requestedAmount;
1403
+ } else {
1404
+ const quoteSpinner = ora3("Fetching Uniswap quote...").start();
1405
+ try {
1406
+ let amountOut;
1407
+ if (isWeth) {
1408
+ const quote = await getQuote({
1409
+ tokenIn: TOKENS().WETH,
1410
+ tokenOut: TOKENS().USDC,
1411
+ amountIn: requestedAmount,
1412
+ fee
1413
+ });
1414
+ amountOut = quote.amountOut;
1415
+ } else {
1416
+ swapPath = encodeSwapPath(
1417
+ [assetAddress, TOKENS().WETH, TOKENS().USDC],
1418
+ [fee, 500]
1419
+ // WETH→USDC typically uses 500 (0.05%) fee tier
1420
+ );
1421
+ const quote = await getMultiHopQuote({
1422
+ path: swapPath,
1423
+ amountIn: requestedAmount
1424
+ });
1425
+ amountOut = quote.amountOut;
1426
+ }
1427
+ minUsdc = applySlippage(amountOut, slippageBps);
1428
+ quoteSpinner.succeed(
1429
+ `Quote: ${formatUnits4(amountOut, 6)} USDC (min: ${formatUnits4(minUsdc, 6)}, per agent: ${formatUnits4(minUsdc / BigInt(agents.length), 6)})`
1430
+ );
1431
+ } catch (err) {
1432
+ quoteSpinner.fail("Failed to fetch quote");
1433
+ console.error(chalk3.red(err instanceof Error ? err.message : String(err)));
1434
+ process.exit(1);
1435
+ }
1436
+ }
1437
+ const perAgent = minUsdc / BigInt(agents.length);
1438
+ if (isUsdc) {
1439
+ console.log(chalk3.dim(` Per agent: ${formatUnits4(perAgent, 6)} USDC`));
1440
+ console.log();
1441
+ }
1442
+ const config = {
1443
+ amount: opts.amount,
1444
+ fee,
1445
+ slippageBps
1446
+ };
1447
+ const calls = buildDisburseBatch(config, vaultAddress, agents, assetAddress, assetDecimals, minUsdc, swapPath);
1448
+ console.log();
1449
+ console.log(chalk3.bold(`Batch calls (${calls.length}):`));
1450
+ console.log(formatBatch(calls));
1451
+ console.log();
1452
+ const simSpinner = ora3("Simulating via vault...").start();
1453
+ try {
1454
+ const results = await simulateBatch(calls);
1455
+ const allSucceeded = results.every((r) => r.success);
1456
+ if (allSucceeded) {
1457
+ simSpinner.succeed("Simulation passed");
1458
+ } else {
1459
+ simSpinner.fail("Simulation: some calls failed");
1460
+ for (let i = 0; i < results.length; i++) {
1461
+ const status = results[i].success ? "ok" : "FAIL";
1462
+ console.log(` ${status} Call ${i + 1}`);
1463
+ }
1464
+ if (!opts.execute) process.exit(1);
1465
+ console.log(chalk3.yellow("Continuing to execution despite simulation failure..."));
1466
+ }
1467
+ } catch (err) {
1468
+ simSpinner.fail("Simulation failed");
1469
+ console.error(chalk3.red(err instanceof Error ? err.message : String(err)));
1470
+ if (!opts.execute) process.exit(1);
1471
+ console.log(chalk3.yellow("Continuing to execution despite simulation failure..."));
1472
+ }
1473
+ if (!opts.execute) {
1474
+ console.log();
1475
+ console.log(chalk3.yellow("Dry run complete. Add --execute to submit on-chain."));
1476
+ return;
1477
+ }
1478
+ const execSpinner = ora3("Executing batch via vault...").start();
1479
+ try {
1480
+ const txHash = await executeBatch(calls, requestedAmount);
1481
+ execSpinner.succeed(`Batch executed: ${txHash}`);
1482
+ console.log(chalk3.dim(` ${getExplorerUrl(txHash)}`));
1483
+ } catch (err) {
1484
+ execSpinner.fail("Execution failed");
1485
+ console.error(chalk3.red(err instanceof Error ? err.message : String(err)));
1486
+ process.exit(1);
1487
+ }
1488
+ });
1489
+ allowance.command("status").description("Show vault profit and agent USDC balances").requiredOption("--vault <address>", "Vault address").action(async (opts) => {
1490
+ const vaultAddress = opts.vault;
1491
+ if (!isAddress3(vaultAddress)) {
1492
+ console.error(chalk3.red(`Invalid vault address: ${opts.vault}`));
1493
+ process.exit(1);
1494
+ }
1495
+ const client = getPublicClient();
1496
+ const spinner = ora3("Loading allowance status...").start();
1497
+ try {
1498
+ const [assetAddress, totalDeposited, agents] = await Promise.all([
1499
+ client.readContract({ address: vaultAddress, abi: SYNDICATE_VAULT_ABI, functionName: "asset" }),
1500
+ client.readContract({ address: vaultAddress, abi: SYNDICATE_VAULT_ABI, functionName: "totalDeposited" }),
1501
+ client.readContract({ address: vaultAddress, abi: SYNDICATE_VAULT_ABI, functionName: "getAgentOperators" })
1502
+ ]);
1503
+ const [assetDecimals, assetSymbol, assetBalance] = await Promise.all([
1504
+ client.readContract({ address: assetAddress, abi: ERC20_ABI, functionName: "decimals" }),
1505
+ client.readContract({ address: assetAddress, abi: ERC20_ABI, functionName: "symbol" }),
1506
+ client.readContract({ address: assetAddress, abi: ERC20_ABI, functionName: "balanceOf", args: [vaultAddress] })
1507
+ ]);
1508
+ const agentBalances = await Promise.all(
1509
+ agents.map(async (agent) => {
1510
+ const bal = await client.readContract({
1511
+ address: TOKENS().USDC,
1512
+ abi: ERC20_ABI,
1513
+ functionName: "balanceOf",
1514
+ args: [agent]
1515
+ });
1516
+ return { agent, balance: bal };
1517
+ })
1518
+ );
1519
+ spinner.stop();
1520
+ const profit = assetBalance > totalDeposited ? assetBalance - totalDeposited : 0n;
1521
+ const account = getAccount();
1522
+ console.log();
1523
+ console.log(chalk3.bold("Allowance Status"));
1524
+ console.log(chalk3.dim("\u2500".repeat(50)));
1525
+ console.log(chalk3.bold("\n Vault"));
1526
+ console.log(` Asset: ${assetSymbol}`);
1527
+ console.log(` Balance: ${formatUnits4(assetBalance, assetDecimals)} ${assetSymbol}`);
1528
+ console.log(` Deposited: ${formatUnits4(totalDeposited, assetDecimals)} ${assetSymbol}`);
1529
+ console.log(` Profit: ${formatUnits4(profit, assetDecimals)} ${assetSymbol}`);
1530
+ console.log(chalk3.bold("\n Agent USDC Balances"));
1531
+ for (const { agent, balance } of agentBalances) {
1532
+ const isMe = agent.toLowerCase() === account.address.toLowerCase();
1533
+ const label = isMe ? chalk3.green(`${agent} (you)`) : agent;
1534
+ console.log(` ${label}: ${formatUnits4(balance, 6)} USDC`);
1535
+ }
1536
+ console.log();
1537
+ } catch (err) {
1538
+ spinner.fail("Failed to load status");
1539
+ console.error(chalk3.red(err instanceof Error ? err.message : String(err)));
1540
+ process.exit(1);
1541
+ }
1542
+ });
1543
+ }
1544
+
1545
+ // src/commands/identity.ts
1546
+ import chalk4 from "chalk";
1547
+ import ora4 from "ora";
1548
+ import { SDK } from "agent0-sdk";
1549
+ var IDENTITY_REGISTRY_ABI = [
1550
+ {
1551
+ name: "balanceOf",
1552
+ type: "function",
1553
+ stateMutability: "view",
1554
+ inputs: [{ name: "owner", type: "address" }],
1555
+ outputs: [{ name: "", type: "uint256" }]
1556
+ },
1557
+ {
1558
+ name: "ownerOf",
1559
+ type: "function",
1560
+ stateMutability: "view",
1561
+ inputs: [{ name: "tokenId", type: "uint256" }],
1562
+ outputs: [{ name: "", type: "address" }]
1563
+ }
1564
+ ];
1565
+ function getAgent0SDK() {
1566
+ const config = loadConfig();
1567
+ const key = config.privateKey || process.env.PRIVATE_KEY;
1568
+ if (!key) {
1569
+ throw new Error(
1570
+ "Private key not found. Run 'sherwood config set --private-key <key>' or set PRIVATE_KEY env var."
1571
+ );
1572
+ }
1573
+ return new SDK({
1574
+ chainId: getChain().id,
1575
+ rpcUrl: getRpcUrl(),
1576
+ privateKey: key.startsWith("0x") ? key : `0x${key}`
1577
+ });
1578
+ }
1579
+ function registerIdentityCommands(program2) {
1580
+ const identity = program2.command("identity").description("Manage ERC-8004 agent identity (via Agent0 SDK)");
1581
+ identity.command("mint").description("Register a new ERC-8004 agent identity (required before creating/joining syndicates)").requiredOption("--name <name>", "Agent name (e.g. 'Alpha Seeker Agent')").option("--description <desc>", "Agent description", "Sherwood syndicate agent").option("--image <uri>", "Agent image URI (IPFS recommended)").action(async (opts) => {
1582
+ const account = getAccount();
1583
+ const existingId = getAgentId();
1584
+ if (existingId) {
1585
+ console.log(chalk4.yellow(`You already have an agent identity saved: #${existingId}`));
1586
+ console.log(chalk4.dim(" Minting a new one anyway. The old ID is not affected."));
1587
+ console.log();
1588
+ }
1589
+ const spinner = ora4("Initializing Agent0 SDK...").start();
1590
+ try {
1591
+ const sdk = getAgent0SDK();
1592
+ spinner.text = "Creating agent profile...";
1593
+ const agent = sdk.createAgent(opts.name, opts.description, opts.image);
1594
+ spinner.text = "Registering on-chain (minting ERC-8004 identity)...";
1595
+ const txHandle = await agent.registerOnChain();
1596
+ spinner.text = "Waiting for confirmation...";
1597
+ await txHandle.waitMined();
1598
+ const agentId = agent.agentId;
1599
+ if (!agentId) {
1600
+ spinner.warn("Identity registered but could not read agentId");
1601
+ console.log(chalk4.dim(" Check the transaction on the explorer."));
1602
+ return;
1603
+ }
1604
+ const tokenId = Number(agentId.includes(":") ? agentId.split(":")[1] : agentId);
1605
+ setAgentId(tokenId);
1606
+ spinner.succeed(`Agent identity registered: #${tokenId}`);
1607
+ console.log(chalk4.dim(` Agent0 ID: ${agentId}`));
1608
+ console.log(chalk4.dim(` Name: ${opts.name}`));
1609
+ console.log(chalk4.dim(` Owner: ${account.address}`));
1610
+ console.log(chalk4.dim(` Saved to ~/.sherwood/config.json`));
1611
+ console.log();
1612
+ console.log(chalk4.green("You can now create syndicates:"));
1613
+ console.log(chalk4.dim(` sherwood syndicate create --agent-id ${tokenId} --subdomain <name> --name <name>`));
1614
+ } catch (err) {
1615
+ spinner.fail("Failed to register identity");
1616
+ console.error(chalk4.red(err instanceof Error ? err.message : String(err)));
1617
+ process.exit(1);
1618
+ }
1619
+ });
1620
+ identity.command("load").description("Load an existing ERC-8004 agent identity into your config").requiredOption("--id <tokenId>", "Agent token ID to load").action(async (opts) => {
1621
+ const account = getAccount();
1622
+ const client = getPublicClient();
1623
+ const registry = AGENT_REGISTRY().IDENTITY_REGISTRY;
1624
+ const tokenId = Number(opts.id);
1625
+ const spinner = ora4(`Verifying ownership of agent #${tokenId}...`).start();
1626
+ try {
1627
+ const owner = await client.readContract({
1628
+ address: registry,
1629
+ abi: IDENTITY_REGISTRY_ABI,
1630
+ functionName: "ownerOf",
1631
+ args: [BigInt(tokenId)]
1632
+ });
1633
+ if (owner.toLowerCase() !== account.address.toLowerCase()) {
1634
+ spinner.fail(`Agent #${tokenId} is owned by ${owner}, not your wallet`);
1635
+ process.exit(1);
1636
+ }
1637
+ setAgentId(tokenId);
1638
+ spinner.succeed(`Agent #${tokenId} loaded and saved to config`);
1639
+ console.log(chalk4.dim(` Owner: ${account.address}`));
1640
+ console.log(chalk4.dim(` Saved to ~/.sherwood/config.json`));
1641
+ } catch (err) {
1642
+ spinner.fail("Failed to load identity");
1643
+ console.error(chalk4.red(err instanceof Error ? err.message : String(err)));
1644
+ process.exit(1);
1645
+ }
1646
+ });
1647
+ identity.command("status").description("Show your agent identity status").action(async () => {
1648
+ const account = getAccount();
1649
+ const registry = AGENT_REGISTRY().IDENTITY_REGISTRY;
1650
+ const client = getPublicClient();
1651
+ const spinner = ora4("Checking identity...").start();
1652
+ try {
1653
+ const balance = await client.readContract({
1654
+ address: registry,
1655
+ abi: IDENTITY_REGISTRY_ABI,
1656
+ functionName: "balanceOf",
1657
+ args: [account.address]
1658
+ });
1659
+ spinner.stop();
1660
+ const savedId = getAgentId();
1661
+ console.log();
1662
+ console.log(chalk4.bold("Agent Identity (ERC-8004)"));
1663
+ console.log(chalk4.dim("\u2500".repeat(40)));
1664
+ console.log(` Wallet: ${account.address}`);
1665
+ console.log(` Registry: ${registry}`);
1666
+ console.log(` NFTs owned: ${balance.toString()}`);
1667
+ if (savedId) {
1668
+ try {
1669
+ const owner = await client.readContract({
1670
+ address: registry,
1671
+ abi: IDENTITY_REGISTRY_ABI,
1672
+ functionName: "ownerOf",
1673
+ args: [BigInt(savedId)]
1674
+ });
1675
+ const isOwner = owner.toLowerCase() === account.address.toLowerCase();
1676
+ console.log(` Saved ID: #${savedId} ${isOwner ? chalk4.green("(verified)") : chalk4.red("(owned by " + owner + ")")}`);
1677
+ if (isOwner) {
1678
+ try {
1679
+ const sdk = getAgent0SDK();
1680
+ const agent = await sdk.loadAgent(`${getChain().id}:${savedId}`);
1681
+ if (agent.name) console.log(` Name: ${agent.name}`);
1682
+ if (agent.description) console.log(` Desc: ${chalk4.dim(agent.description)}`);
1683
+ if (agent.walletAddress) console.log(` Wallet: ${agent.walletAddress}`);
1684
+ } catch {
1685
+ }
1686
+ }
1687
+ } catch {
1688
+ console.log(` Saved ID: #${savedId} ${chalk4.red("(token not found)")}`);
1689
+ }
1690
+ } else {
1691
+ console.log(` Saved ID: ${chalk4.dim("none \u2014 run 'sherwood identity mint --name <name>'")}`);
1692
+ }
1693
+ console.log();
1694
+ } catch (err) {
1695
+ spinner.fail("Failed to check identity");
1696
+ console.error(chalk4.red(err instanceof Error ? err.message : String(err)));
1697
+ process.exit(1);
1698
+ }
1699
+ });
1700
+ }
1701
+
1702
+ // src/lib/eas.ts
1703
+ import { encodeAbiParameters, parseAbiParameters, decodeAbiParameters } from "viem";
1704
+ var ZERO_BYTES32 = "0x0000000000000000000000000000000000000000000000000000000000000000";
1705
+ var JOIN_REQUEST_PARAMS = parseAbiParameters("uint256, uint256, address, string");
1706
+ var AGENT_APPROVED_PARAMS = parseAbiParameters("uint256, uint256, address");
1707
+ function assertSchemasRegistered() {
1708
+ const schemas = EAS_SCHEMAS();
1709
+ if (schemas.SYNDICATE_JOIN_REQUEST === ZERO_BYTES32 || schemas.AGENT_APPROVED === ZERO_BYTES32) {
1710
+ throw new Error(
1711
+ "EAS schemas not registered. Run: npx tsx scripts/register-eas-schemas.ts --testnet"
1712
+ );
1713
+ }
1714
+ }
1715
+ function getEasGraphqlUrl() {
1716
+ return getNetwork() === "base" ? "https://base.easscan.org/graphql" : "https://base-sepolia.easscan.org/graphql";
1717
+ }
1718
+ function getEasScanUrl(uid) {
1719
+ const host = getNetwork() === "base" ? "base.easscan.org" : "base-sepolia.easscan.org";
1720
+ return `https://${host}/attestation/view/${uid}`;
1721
+ }
1722
+ function extractAttestationUid(receipt) {
1723
+ for (const log of receipt.logs) {
1724
+ if (log.topics.length === 4 && log.data.length >= 66) {
1725
+ return "0x" + log.data.slice(2, 66);
1726
+ }
1727
+ }
1728
+ throw new Error("Could not extract attestation UID from transaction receipt");
1729
+ }
1730
+ async function createJoinRequest(syndicateId, agentId, vault, creatorAddress, message) {
1731
+ assertSchemasRegistered();
1732
+ const wallet = getWalletClient();
1733
+ const client = getPublicClient();
1734
+ const data = encodeAbiParameters(JOIN_REQUEST_PARAMS, [
1735
+ syndicateId,
1736
+ agentId,
1737
+ vault,
1738
+ message
1739
+ ]);
1740
+ const hash = await wallet.writeContract({
1741
+ account: getAccount(),
1742
+ chain: getChain(),
1743
+ address: EAS_CONTRACTS().EAS,
1744
+ abi: EAS_ABI,
1745
+ functionName: "attest",
1746
+ args: [{
1747
+ schema: EAS_SCHEMAS().SYNDICATE_JOIN_REQUEST,
1748
+ data: {
1749
+ recipient: creatorAddress,
1750
+ expirationTime: 0n,
1751
+ revocable: true,
1752
+ refUID: ZERO_BYTES32,
1753
+ data,
1754
+ value: 0n
1755
+ }
1756
+ }],
1757
+ value: 0n
1758
+ });
1759
+ const receipt = await client.waitForTransactionReceipt({ hash });
1760
+ const uid = extractAttestationUid(receipt);
1761
+ return { uid, hash };
1762
+ }
1763
+ async function createApproval(syndicateId, agentId, vault, agentAddress) {
1764
+ assertSchemasRegistered();
1765
+ const wallet = getWalletClient();
1766
+ const client = getPublicClient();
1767
+ const data = encodeAbiParameters(AGENT_APPROVED_PARAMS, [
1768
+ syndicateId,
1769
+ agentId,
1770
+ vault
1771
+ ]);
1772
+ const hash = await wallet.writeContract({
1773
+ account: getAccount(),
1774
+ chain: getChain(),
1775
+ address: EAS_CONTRACTS().EAS,
1776
+ abi: EAS_ABI,
1777
+ functionName: "attest",
1778
+ args: [{
1779
+ schema: EAS_SCHEMAS().AGENT_APPROVED,
1780
+ data: {
1781
+ recipient: agentAddress,
1782
+ expirationTime: 0n,
1783
+ revocable: true,
1784
+ refUID: ZERO_BYTES32,
1785
+ data,
1786
+ value: 0n
1787
+ }
1788
+ }],
1789
+ value: 0n
1790
+ });
1791
+ const receipt = await client.waitForTransactionReceipt({ hash });
1792
+ const uid = extractAttestationUid(receipt);
1793
+ return { uid, hash };
1794
+ }
1795
+ async function revokeAttestation(schemaUid, attestationUid) {
1796
+ const wallet = getWalletClient();
1797
+ return wallet.writeContract({
1798
+ account: getAccount(),
1799
+ chain: getChain(),
1800
+ address: EAS_CONTRACTS().EAS,
1801
+ abi: EAS_ABI,
1802
+ functionName: "revoke",
1803
+ args: [{
1804
+ schema: schemaUid,
1805
+ data: {
1806
+ uid: attestationUid,
1807
+ value: 0n
1808
+ }
1809
+ }],
1810
+ value: 0n
1811
+ });
1812
+ }
1813
+ async function queryJoinRequests(recipient) {
1814
+ assertSchemasRegistered();
1815
+ const schemaUid = EAS_SCHEMAS().SYNDICATE_JOIN_REQUEST;
1816
+ const url = getEasGraphqlUrl();
1817
+ const query2 = `
1818
+ query JoinRequests($schemaId: String!, $recipient: String!) {
1819
+ attestations(
1820
+ where: {
1821
+ schemaId: { equals: $schemaId }
1822
+ recipient: { equals: $recipient }
1823
+ revoked: { equals: false }
1824
+ }
1825
+ orderBy: [{ time: desc }]
1826
+ ) {
1827
+ id
1828
+ attester
1829
+ recipient
1830
+ time
1831
+ data
1832
+ }
1833
+ }
1834
+ `;
1835
+ const response = await fetch(url, {
1836
+ method: "POST",
1837
+ headers: { "Content-Type": "application/json" },
1838
+ body: JSON.stringify({
1839
+ query: query2,
1840
+ variables: { schemaId: schemaUid, recipient }
1841
+ })
1842
+ });
1843
+ if (!response.ok) {
1844
+ throw new Error(`EAS GraphQL query failed: ${response.statusText}`);
1845
+ }
1846
+ const json = await response.json();
1847
+ if (!json.data?.attestations) return [];
1848
+ return json.data.attestations.map((a) => {
1849
+ const decoded = decodeAbiParameters(JOIN_REQUEST_PARAMS, a.data);
1850
+ return {
1851
+ uid: a.id,
1852
+ attester: a.attester,
1853
+ recipient: a.recipient,
1854
+ time: a.time,
1855
+ decoded: {
1856
+ syndicateId: decoded[0],
1857
+ agentId: decoded[1],
1858
+ vault: decoded[2],
1859
+ message: decoded[3]
1860
+ }
1861
+ };
1862
+ });
1863
+ }
1864
+
1865
+ // src/index.ts
1866
+ try {
1867
+ loadDotenv();
1868
+ } catch {
1869
+ }
1870
+ async function loadXmtp() {
1871
+ return import("./xmtp-2UPH45XE.js");
1872
+ }
1873
+ var G = chalk5.green;
1874
+ var W = chalk5.white;
1875
+ var DIM = chalk5.gray;
1876
+ var BOLD = chalk5.white.bold;
1877
+ var LABEL = chalk5.green.bold;
1878
+ var SEP = () => console.log(DIM("\u2500".repeat(60)));
1879
+ function resolveVault(opts) {
1880
+ if (opts.vault) {
1881
+ setVaultAddress(opts.vault);
1882
+ }
1883
+ }
1884
+ var program = new Command();
1885
+ program.name("sherwood").description("CLI for agent-managed investment syndicates").version("0.1.0").option("--testnet", "Use Base Sepolia testnet instead of Base mainnet", false).hook("preAction", (thisCommand) => {
1886
+ const opts = thisCommand.optsWithGlobals();
1887
+ setNetwork(opts.testnet ? "base-sepolia" : "base");
1888
+ if (opts.testnet) {
1889
+ console.log(chalk5.yellow("[testnet] Base Sepolia"));
1890
+ }
1891
+ });
1892
+ var syndicate = program.command("syndicate");
1893
+ syndicate.command("create").description("Create a new syndicate via the factory (interactive)").option("--subdomain <name>", "ENS subdomain (skip prompt)").option("--name <name>", "Syndicate name (skip prompt)").option("--agent-id <id>", "ERC-8004 agent identity token ID (skip prompt)").option("--asset <address>", "Underlying asset address").option("--max-per-tx <amount>", "Max per transaction (in asset units)").option("--max-daily <amount>", "Max daily combined spend (in asset units)").option("--borrow-ratio <bps>", "Max borrow ratio in basis points").option("--targets <addresses>", "Comma-separated allowlisted target addresses").option("--description <text>", "Short description").option("--metadata-uri <uri>", "Override metadata URI (skip IPFS upload)").option("--open-deposits", "Allow anyone to deposit (no whitelist)").option("--public-chat", "Enable dashboard spectator mode", false).action(async (opts) => {
1894
+ try {
1895
+ console.log();
1896
+ console.log(LABEL(" \u25C6 Create Syndicate"));
1897
+ SEP();
1898
+ const wallet = getAccount();
1899
+ console.log(DIM(` Wallet: ${wallet.address}`));
1900
+ console.log(DIM(` Network: ${getChain().name}`));
1901
+ SEP();
1902
+ const savedAgentId = getAgentId();
1903
+ const name = opts.name || await input({
1904
+ message: G("Syndicate name"),
1905
+ validate: (v) => v.length > 0 || "Name is required"
1906
+ });
1907
+ const subdomain = opts.subdomain || await input({
1908
+ message: G("ENS subdomain"),
1909
+ default: name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, ""),
1910
+ validate: (v) => v.length >= 3 || "Must be at least 3 characters"
1911
+ });
1912
+ const description = opts.description || await input({
1913
+ message: G("Description"),
1914
+ default: `${name} \u2014 a Sherwood syndicate`
1915
+ });
1916
+ const agentIdStr = opts.agentId || (savedAgentId ? await input({ message: G("Agent ID (ERC-8004)"), default: String(savedAgentId) }) : await input({ message: G("Agent ID (ERC-8004)"), validate: (v) => /^\d+$/.test(v) || "Must be a number" }));
1917
+ const openDeposits = opts.openDeposits !== void 0 ? opts.openDeposits : await confirm({
1918
+ message: G("Open deposits? (anyone can deposit)"),
1919
+ default: true
1920
+ });
1921
+ const maxPerTx = opts.maxPerTx || await input({
1922
+ message: G("Max per transaction (USDC)"),
1923
+ default: "10000"
1924
+ });
1925
+ const maxDaily = opts.maxDaily || await input({
1926
+ message: G("Max daily spend (USDC)"),
1927
+ default: "50000"
1928
+ });
1929
+ const borrowRatio = opts.borrowRatio || await input({
1930
+ message: G("Max borrow ratio (bps, 7500 = 75%)"),
1931
+ default: "7500"
1932
+ });
1933
+ const asset = opts.asset || TOKENS().USDC;
1934
+ const publicClient = getPublicClient();
1935
+ const [decimals, assetSymbol] = await Promise.all([
1936
+ publicClient.readContract({ address: asset, abi: ERC20_ABI, functionName: "decimals" }),
1937
+ publicClient.readContract({ address: asset, abi: ERC20_ABI, functionName: "symbol" })
1938
+ ]);
1939
+ const symbol = `sw${assetSymbol}`;
1940
+ const targets = opts.targets ? opts.targets.split(",").map((a) => a.trim()) : [];
1941
+ console.log();
1942
+ console.log(LABEL(" \u25C6 Review"));
1943
+ SEP();
1944
+ console.log(W(` Name: ${BOLD(name)}`));
1945
+ console.log(W(` ENS: ${G(`${subdomain}.sherwoodagent.eth`)}`));
1946
+ console.log(W(` Description: ${DIM(description)}`));
1947
+ console.log(W(` Agent ID: #${agentIdStr}`));
1948
+ console.log(W(` Asset: ${assetSymbol} (${asset.slice(0, 10)}...)`));
1949
+ console.log(W(` Share token: ${symbol}`));
1950
+ console.log(W(` Max per tx: ${maxPerTx} ${assetSymbol}`));
1951
+ console.log(W(` Max daily: ${maxDaily} ${assetSymbol}`));
1952
+ console.log(W(` Borrow ratio: ${(Number(borrowRatio) / 100).toFixed(1)}%`));
1953
+ console.log(W(` Open deposits: ${openDeposits ? G("yes") : chalk5.red("no (whitelist)")}`));
1954
+ if (targets.length > 0) {
1955
+ console.log(W(` Targets: ${targets.length} address(es)`));
1956
+ }
1957
+ SEP();
1958
+ const go = await confirm({ message: G("Deploy syndicate?"), default: true });
1959
+ if (!go) {
1960
+ console.log(DIM(" Cancelled."));
1961
+ return;
1962
+ }
1963
+ let metadataURI = opts.metadataUri || "";
1964
+ if (!metadataURI) {
1965
+ const spinner2 = ora5({ text: W("Uploading metadata to IPFS..."), color: "green" }).start();
1966
+ try {
1967
+ const metadata = {
1968
+ schema: "sherwood/syndicate/v1",
1969
+ name,
1970
+ description,
1971
+ chain: getChain().name,
1972
+ strategies: [],
1973
+ terms: {
1974
+ ragequitEnabled: true,
1975
+ feeModel: "none"
1976
+ },
1977
+ links: {}
1978
+ };
1979
+ metadataURI = await uploadMetadata(metadata);
1980
+ spinner2.succeed(G(`Metadata pinned: ${DIM(metadataURI)}`));
1981
+ } catch (err) {
1982
+ spinner2.warn(chalk5.yellow(`IPFS upload failed \u2014 using inline metadata`));
1983
+ const json = JSON.stringify({ name, description, subdomain, asset: assetSymbol, openDeposits, createdBy: "@sherwoodagent/cli" });
1984
+ metadataURI = `data:application/json;base64,${Buffer.from(json).toString("base64")}`;
1985
+ }
1986
+ }
1987
+ const spinner = ora5({ text: W("Deploying vault via factory..."), color: "green" }).start();
1988
+ const result = await createSyndicate({
1989
+ creatorAgentId: BigInt(agentIdStr),
1990
+ metadataURI,
1991
+ asset,
1992
+ name,
1993
+ symbol,
1994
+ maxPerTx: parseUnits8(maxPerTx, decimals),
1995
+ maxDailyTotal: parseUnits8(maxDaily, decimals),
1996
+ maxBorrowRatio: BigInt(borrowRatio),
1997
+ initialTargets: targets,
1998
+ openDeposits,
1999
+ subdomain
2000
+ });
2001
+ setChainContract(getChain().id, "vault", result.vault);
2002
+ spinner.text = W("Registering creator as agent...");
2003
+ try {
2004
+ setVaultAddress(result.vault);
2005
+ const creatorAddress = getAccount().address;
2006
+ await registerAgent(
2007
+ BigInt(agentIdStr),
2008
+ creatorAddress,
2009
+ // pkp = creator EOA (direct execution)
2010
+ creatorAddress,
2011
+ // operator = creator EOA
2012
+ parseUnits8(maxPerTx, decimals),
2013
+ parseUnits8(maxDaily, decimals)
2014
+ );
2015
+ } catch (regErr) {
2016
+ console.warn(chalk5.yellow("\n \u26A0 Could not auto-register creator as agent \u2014 register manually with `syndicate add`"));
2017
+ }
2018
+ spinner.text = W("Setting up chat...");
2019
+ try {
2020
+ const xmtp = await loadXmtp();
2021
+ const xmtpClient = await xmtp.getXmtpClient();
2022
+ const groupId = await xmtp.createSyndicateGroup(xmtpClient, subdomain, opts.publicChat);
2023
+ await setTextRecord(subdomain, "xmtpGroupId", groupId, result.vault);
2024
+ cacheGroupId(subdomain, groupId);
2025
+ } catch {
2026
+ console.warn(chalk5.yellow("\n \u26A0 Could not create XMTP chat group"));
2027
+ console.warn(chalk5.dim(` Recover later with: sherwood chat ${subdomain} init`));
2028
+ }
2029
+ spinner.stop();
2030
+ console.log();
2031
+ console.log(LABEL(" \u25C6 Syndicate Created"));
2032
+ SEP();
2033
+ console.log(W(` ID: ${G(`#${result.syndicateId}`)}`));
2034
+ console.log(W(` Vault: ${G(result.vault)}`));
2035
+ console.log(W(` ENS: ${G(`${subdomain}.sherwoodagent.eth`)}`));
2036
+ console.log(W(` Metadata: ${DIM(metadataURI.length > 50 ? metadataURI.slice(0, 50) + "..." : metadataURI)}`));
2037
+ console.log(W(` Explorer: ${DIM(getExplorerUrl(result.hash))}`));
2038
+ console.log(W(` Chat: ${DIM(`sherwood chat ${subdomain}`)}`));
2039
+ SEP();
2040
+ console.log(G(" \u2713 Vault saved to ~/.sherwood/config.json"));
2041
+ console.log();
2042
+ } catch (err) {
2043
+ console.error(chalk5.red(`
2044
+ \u2716 ${err instanceof Error ? err.message : String(err)}`));
2045
+ process.exit(1);
2046
+ }
2047
+ });
2048
+ syndicate.command("list").description("List active syndicates (queries subgraph, falls back to on-chain)").option("--creator <address>", "Filter by creator address").action(async (opts) => {
2049
+ const spinner = ora5("Loading syndicates...").start();
2050
+ try {
2051
+ let syndicates;
2052
+ if (process.env.SUBGRAPH_URL) {
2053
+ const result = await getActiveSyndicates2(opts.creator);
2054
+ syndicates = result;
2055
+ } else {
2056
+ const result = await getActiveSyndicates();
2057
+ syndicates = result.map((s) => ({
2058
+ id: s.id.toString(),
2059
+ vault: s.vault,
2060
+ creator: s.creator,
2061
+ metadataURI: s.metadataURI,
2062
+ createdAt: s.createdAt.toString(),
2063
+ subdomain: s.subdomain
2064
+ }));
2065
+ }
2066
+ spinner.stop();
2067
+ if (syndicates.length === 0) {
2068
+ console.log(chalk5.dim("No active syndicates found."));
2069
+ return;
2070
+ }
2071
+ console.log();
2072
+ console.log(chalk5.bold(`Active Syndicates (${syndicates.length})`));
2073
+ if (!process.env.SUBGRAPH_URL) {
2074
+ console.log(chalk5.dim(" (Set SUBGRAPH_URL for faster indexed queries)"));
2075
+ }
2076
+ console.log(chalk5.dim("\u2500".repeat(70)));
2077
+ for (const s of syndicates) {
2078
+ const ts = typeof s.createdAt === "string" ? Number(s.createdAt) : Number(s.createdAt);
2079
+ const date = new Date(ts * 1e3).toLocaleDateString();
2080
+ const ensName = s.subdomain ? `${s.subdomain}.sherwoodagent.eth` : "";
2081
+ console.log(` #${s.id} ${chalk5.bold(ensName || String(s.vault))}`);
2082
+ if (ensName) console.log(` Vault: ${chalk5.cyan(String(s.vault))}`);
2083
+ console.log(` Creator: ${s.creator}`);
2084
+ console.log(` Created: ${date}`);
2085
+ if (s.totalDeposits) {
2086
+ console.log(` Deposits: ${s.totalDeposits} USDC`);
2087
+ }
2088
+ if (s.metadataURI) {
2089
+ console.log(` Metadata: ${chalk5.dim(s.metadataURI)}`);
2090
+ }
2091
+ console.log();
2092
+ }
2093
+ } catch (err) {
2094
+ spinner.fail("Failed to load syndicates");
2095
+ console.error(chalk5.red(err instanceof Error ? err.message : String(err)));
2096
+ process.exit(1);
2097
+ }
2098
+ });
2099
+ syndicate.command("info").description("Display syndicate details by ID").argument("<id>", "Syndicate ID").action(async (idStr) => {
2100
+ const spinner = ora5("Loading syndicate info...").start();
2101
+ try {
2102
+ const id = BigInt(idStr);
2103
+ const info = await getSyndicate(id);
2104
+ spinner.stop();
2105
+ if (!info.vault || info.vault === "0x0000000000000000000000000000000000000000") {
2106
+ console.log(chalk5.red(`Syndicate #${id} not found.`));
2107
+ process.exit(1);
2108
+ }
2109
+ const date = new Date(Number(info.createdAt) * 1e3).toLocaleDateString();
2110
+ console.log();
2111
+ console.log(chalk5.bold(`Syndicate #${info.id}`));
2112
+ console.log(chalk5.dim("\u2500".repeat(40)));
2113
+ if (info.subdomain) {
2114
+ console.log(` ENS: ${chalk5.bold(`${info.subdomain}.sherwoodagent.eth`)}`);
2115
+ }
2116
+ console.log(` Vault: ${chalk5.cyan(info.vault)}`);
2117
+ console.log(` Creator: ${info.creator}`);
2118
+ console.log(` Created: ${date}`);
2119
+ console.log(` Active: ${info.active ? chalk5.green("yes") : chalk5.red("no")}`);
2120
+ if (info.metadataURI) {
2121
+ console.log(` Metadata: ${chalk5.dim(info.metadataURI)}`);
2122
+ }
2123
+ setVaultAddress(info.vault);
2124
+ const vaultInfo = await getVaultInfo();
2125
+ console.log();
2126
+ console.log(chalk5.bold(" Vault Stats"));
2127
+ console.log(` Total Assets: ${vaultInfo.totalAssets}`);
2128
+ console.log(` Agent Count: ${vaultInfo.agentCount}`);
2129
+ console.log(` Daily Spend: ${vaultInfo.dailySpendTotal}`);
2130
+ console.log(` Max Per Tx: ${vaultInfo.syndicateCaps.maxPerTx}`);
2131
+ console.log(` Max Daily: ${vaultInfo.syndicateCaps.maxDailyTotal}`);
2132
+ console.log(` Max Borrow: ${vaultInfo.syndicateCaps.maxBorrowRatio}`);
2133
+ console.log();
2134
+ } catch (err) {
2135
+ spinner.fail("Failed to load syndicate info");
2136
+ console.error(chalk5.red(err instanceof Error ? err.message : String(err)));
2137
+ process.exit(1);
2138
+ }
2139
+ });
2140
+ syndicate.command("update-metadata").description("Update syndicate metadata (creator only)").requiredOption("--id <id>", "Syndicate ID").option("--name <name>", "Syndicate name").option("--description <text>", "Short description").option("--uri <uri>", "Direct metadata URI (skips IPFS upload)").action(async (opts) => {
2141
+ const spinner = ora5({ text: W("Loading syndicate..."), color: "green" }).start();
2142
+ try {
2143
+ const syndicateId = BigInt(opts.id);
2144
+ let metadataURI = opts.uri;
2145
+ if (!metadataURI) {
2146
+ const info = await getSyndicate(syndicateId);
2147
+ if (!info.vault || info.vault === "0x0000000000000000000000000000000000000000") {
2148
+ spinner.fail(`Syndicate #${opts.id} not found.`);
2149
+ process.exit(1);
2150
+ }
2151
+ const name = opts.name || info.subdomain;
2152
+ const description = opts.description || `${name} \u2014 a Sherwood syndicate on ${info.subdomain}.sherwoodagent.eth`;
2153
+ spinner.text = W("Uploading metadata to IPFS...");
2154
+ const metadata = {
2155
+ schema: "sherwood/syndicate/v1",
2156
+ name,
2157
+ description,
2158
+ chain: getChain().name,
2159
+ strategies: [],
2160
+ terms: { ragequitEnabled: true },
2161
+ links: {}
2162
+ };
2163
+ metadataURI = await uploadMetadata(metadata);
2164
+ spinner.text = W("Updating on-chain metadata...");
2165
+ }
2166
+ const hash = await updateMetadata(syndicateId, metadataURI);
2167
+ spinner.succeed(G(`Metadata updated`));
2168
+ console.log(DIM(` IPFS: ${metadataURI}`));
2169
+ console.log(DIM(` ${getExplorerUrl(hash)}`));
2170
+ } catch (err) {
2171
+ spinner.fail("Metadata update failed");
2172
+ console.error(chalk5.red(err instanceof Error ? err.message : String(err)));
2173
+ process.exit(1);
2174
+ }
2175
+ });
2176
+ syndicate.command("approve-depositor").description("Approve an address to deposit (owner only)").option("--vault <address>", "Vault address (default: from config)").requiredOption("--depositor <address>", "Address to approve").action(async (opts) => {
2177
+ resolveVault(opts);
2178
+ const spinner = ora5("Approving depositor...").start();
2179
+ try {
2180
+ const hash = await approveDepositor(opts.depositor);
2181
+ spinner.succeed(`Depositor approved: ${hash}`);
2182
+ console.log(chalk5.dim(` ${getExplorerUrl(hash)}`));
2183
+ } catch (err) {
2184
+ spinner.fail("Approval failed");
2185
+ console.error(chalk5.red(err instanceof Error ? err.message : String(err)));
2186
+ process.exit(1);
2187
+ }
2188
+ });
2189
+ syndicate.command("remove-depositor").description("Remove an address from the depositor whitelist (owner only)").option("--vault <address>", "Vault address (default: from config)").requiredOption("--depositor <address>", "Address to remove").action(async (opts) => {
2190
+ resolveVault(opts);
2191
+ const spinner = ora5("Removing depositor...").start();
2192
+ try {
2193
+ const hash = await removeDepositor(opts.depositor);
2194
+ spinner.succeed(`Depositor removed: ${hash}`);
2195
+ console.log(chalk5.dim(` ${getExplorerUrl(hash)}`));
2196
+ } catch (err) {
2197
+ spinner.fail("Removal failed");
2198
+ console.error(chalk5.red(err instanceof Error ? err.message : String(err)));
2199
+ process.exit(1);
2200
+ }
2201
+ });
2202
+ syndicate.command("add").description("Register an agent on a syndicate vault (creator only)").option("--vault <address>", "Vault address (default: from config)").requiredOption("--agent-id <id>", "Agent's ERC-8004 identity token ID").requiredOption("--pkp <address>", "Agent PKP address").requiredOption("--eoa <address>", "Operator EOA address").requiredOption("--max-per-tx <amount>", "Max per transaction (in asset units)").requiredOption("--daily-limit <amount>", "Daily limit (in asset units)").action(async (opts) => {
2203
+ const spinner = ora5("Verifying creator...").start();
2204
+ try {
2205
+ resolveVault(opts);
2206
+ const vaultAddress = getVaultAddress();
2207
+ const { creator, subdomain } = await resolveVaultSyndicate(vaultAddress);
2208
+ const callerAddress = getAccount().address.toLowerCase();
2209
+ if (creator.toLowerCase() !== callerAddress) {
2210
+ spinner.fail("Only the syndicate creator can add agents");
2211
+ process.exit(1);
2212
+ }
2213
+ const decimals = await getAssetDecimals();
2214
+ const maxPerTx = parseUnits8(opts.maxPerTx, decimals);
2215
+ const dailyLimit = parseUnits8(opts.dailyLimit, decimals);
2216
+ spinner.text = "Registering agent...";
2217
+ const hash = await registerAgent(
2218
+ BigInt(opts.agentId),
2219
+ opts.pkp,
2220
+ opts.eoa,
2221
+ maxPerTx,
2222
+ dailyLimit
2223
+ );
2224
+ spinner.succeed(`Agent registered: ${hash}`);
2225
+ console.log(chalk5.dim(` ${getExplorerUrl(hash)}`));
2226
+ try {
2227
+ const xmtp = await loadXmtp();
2228
+ const xmtpClient = await xmtp.getXmtpClient();
2229
+ const group = await xmtp.getGroup(xmtpClient, subdomain);
2230
+ await xmtp.addMember(group, opts.pkp);
2231
+ await xmtp.sendEnvelope(group, {
2232
+ type: "AGENT_REGISTERED",
2233
+ agent: { erc8004Id: Number(opts.agentId), address: opts.pkp },
2234
+ syndicate: subdomain,
2235
+ timestamp: Math.floor(Date.now() / 1e3)
2236
+ });
2237
+ console.log(chalk5.dim(` Added to chat: ${subdomain}`));
2238
+ } catch {
2239
+ console.warn(chalk5.yellow(" \u26A0 Could not add agent to chat group"));
2240
+ console.warn(chalk5.dim(` If no group exists, run: sherwood chat ${subdomain} init`));
2241
+ }
2242
+ } catch (err) {
2243
+ spinner.fail("Registration failed");
2244
+ console.error(chalk5.red(err instanceof Error ? err.message : String(err)));
2245
+ process.exit(1);
2246
+ }
2247
+ });
2248
+ syndicate.command("spectator").description("Toggle dashboard spectator mode for a syndicate chat").argument("<subdomain>", "Syndicate subdomain").option("--on", "Add spectator bot").option("--off", "Remove spectator bot").action(async (subdomain, opts) => {
2249
+ if (!opts.on && !opts.off) {
2250
+ console.error(chalk5.red("Specify --on or --off"));
2251
+ process.exit(1);
2252
+ }
2253
+ const spectatorAddress = process.env.DASHBOARD_SPECTATOR_ADDRESS;
2254
+ if (!spectatorAddress) {
2255
+ console.error(chalk5.red("DASHBOARD_SPECTATOR_ADDRESS env var is required"));
2256
+ process.exit(1);
2257
+ }
2258
+ const spinner = ora5(`${opts.on ? "Enabling" : "Disabling"} spectator mode...`).start();
2259
+ try {
2260
+ const xmtp = await loadXmtp();
2261
+ const xmtpClient = await xmtp.getXmtpClient();
2262
+ const group = await xmtp.getGroup(xmtpClient, subdomain);
2263
+ if (opts.on) {
2264
+ await xmtp.addMember(group, spectatorAddress);
2265
+ spinner.succeed("Spectator mode enabled");
2266
+ } else {
2267
+ await xmtp.removeMember(group, spectatorAddress);
2268
+ spinner.succeed("Spectator mode disabled");
2269
+ }
2270
+ } catch (err) {
2271
+ spinner.fail("Failed to toggle spectator mode");
2272
+ console.error(chalk5.red(err instanceof Error ? err.message : String(err)));
2273
+ process.exit(1);
2274
+ }
2275
+ });
2276
+ syndicate.command("join").description("Request to join a syndicate (creates an EAS attestation)").requiredOption("--subdomain <name>", "Syndicate subdomain to join").option("--message <text>", "Message to the creator", "Requesting to join your syndicate").action(async (opts) => {
2277
+ const spinner = ora5("Resolving syndicate...").start();
2278
+ try {
2279
+ const agentId = getAgentId();
2280
+ if (!agentId) {
2281
+ spinner.fail("No agent identity found. Run 'sherwood identity mint' first.");
2282
+ process.exit(1);
2283
+ }
2284
+ const syndicate2 = await resolveSyndicate(opts.subdomain);
2285
+ spinner.text = "Creating join request attestation...";
2286
+ const { uid, hash } = await createJoinRequest(
2287
+ syndicate2.id,
2288
+ BigInt(agentId),
2289
+ syndicate2.vault,
2290
+ syndicate2.creator,
2291
+ opts.message
2292
+ );
2293
+ try {
2294
+ spinner.text = "Registering XMTP identity...";
2295
+ const xmtp = await loadXmtp();
2296
+ await xmtp.getXmtpClient();
2297
+ spinner.succeed("Join request created (XMTP identity ready)");
2298
+ } catch {
2299
+ spinner.succeed("Join request created");
2300
+ console.warn(chalk5.yellow(" \u26A0 Could not initialize XMTP identity \u2014 creator may not be able to auto-add you to chat"));
2301
+ }
2302
+ console.log();
2303
+ console.log(LABEL(" \u25C6 Join Request Submitted"));
2304
+ SEP();
2305
+ console.log(W(` Syndicate: ${G(`${opts.subdomain}.sherwoodagent.eth`)}`));
2306
+ console.log(W(` Agent ID: #${agentId}`));
2307
+ console.log(W(` Creator: ${DIM(syndicate2.creator)}`));
2308
+ console.log(W(` Attestation: ${DIM(uid)}`));
2309
+ console.log(W(` EAS Scan: ${DIM(getEasScanUrl(uid))}`));
2310
+ console.log(W(` Explorer: ${DIM(getExplorerUrl(hash))}`));
2311
+ SEP();
2312
+ console.log(G(" \u2713 The creator can review with:"));
2313
+ console.log(DIM(` sherwood syndicate requests --subdomain ${opts.subdomain}`));
2314
+ console.log();
2315
+ } catch (err) {
2316
+ spinner.fail("Join request failed");
2317
+ console.error(chalk5.red(err instanceof Error ? err.message : String(err)));
2318
+ process.exit(1);
2319
+ }
2320
+ });
2321
+ syndicate.command("requests").description("View pending join requests for a syndicate (creator only)").option("--subdomain <name>", "Syndicate subdomain").option("--vault <address>", "Vault address (default: from config)").action(async (opts) => {
2322
+ const spinner = ora5("Loading join requests...").start();
2323
+ try {
2324
+ let creatorAddress;
2325
+ let subdomain;
2326
+ if (opts.subdomain) {
2327
+ const syndicateInfo = await resolveSyndicate(opts.subdomain);
2328
+ creatorAddress = syndicateInfo.creator;
2329
+ subdomain = opts.subdomain;
2330
+ } else {
2331
+ resolveVault(opts);
2332
+ const vaultAddress = getVaultAddress();
2333
+ const syndicateInfo = await resolveVaultSyndicate(vaultAddress);
2334
+ creatorAddress = syndicateInfo.creator;
2335
+ subdomain = syndicateInfo.subdomain;
2336
+ }
2337
+ const callerAddress = getAccount().address.toLowerCase();
2338
+ if (creatorAddress.toLowerCase() !== callerAddress) {
2339
+ spinner.fail("Only the syndicate creator can view join requests");
2340
+ process.exit(1);
2341
+ }
2342
+ spinner.text = "Querying EAS attestations...";
2343
+ const requests = await queryJoinRequests(creatorAddress);
2344
+ spinner.stop();
2345
+ if (requests.length === 0) {
2346
+ console.log(DIM("\n No pending join requests.\n"));
2347
+ return;
2348
+ }
2349
+ console.log();
2350
+ console.log(LABEL(` \u25C6 Pending Join Requests (${requests.length})`));
2351
+ SEP();
2352
+ for (let i = 0; i < requests.length; i++) {
2353
+ const req = requests[i];
2354
+ const date = new Date(req.time * 1e3).toLocaleString();
2355
+ console.log(W(` ${i + 1}. Agent #${req.decoded.agentId} ${DIM(`(${req.attester})`)}`));
2356
+ console.log(DIM(` Message: "${req.decoded.message}"`));
2357
+ console.log(DIM(` Requested: ${date}`));
2358
+ console.log(DIM(` Attestation: ${req.uid}`));
2359
+ console.log();
2360
+ }
2361
+ console.log(G(" To approve:"));
2362
+ console.log(DIM(` sherwood syndicate approve --agent-id <id> --pkp <addr> --eoa <addr> --max-per-tx <amt> --daily-limit <amt>`));
2363
+ console.log(G(" To reject:"));
2364
+ console.log(DIM(` sherwood syndicate reject --attestation <uid>`));
2365
+ console.log();
2366
+ } catch (err) {
2367
+ spinner.fail("Failed to load requests");
2368
+ console.error(chalk5.red(err instanceof Error ? err.message : String(err)));
2369
+ process.exit(1);
2370
+ }
2371
+ });
2372
+ syndicate.command("approve").description("Approve an agent join request (registers agent + creates EAS approval)").option("--vault <address>", "Vault address (default: from config)").option("--subdomain <name>", "Syndicate subdomain (alternative to --vault)").requiredOption("--agent-id <id>", "Agent's ERC-8004 identity token ID").requiredOption("--pkp <address>", "Agent PKP address").requiredOption("--eoa <address>", "Operator EOA address").requiredOption("--max-per-tx <amount>", "Max per transaction (in asset units)").requiredOption("--daily-limit <amount>", "Daily limit (in asset units)").option("--revoke-request <uid>", "Revoke the join request attestation after approval").action(async (opts) => {
2373
+ const spinner = ora5("Verifying creator...").start();
2374
+ try {
2375
+ if (opts.subdomain && !opts.vault) {
2376
+ const syndicateInfo = await resolveSyndicate(opts.subdomain);
2377
+ setVaultAddress(syndicateInfo.vault);
2378
+ } else {
2379
+ resolveVault(opts);
2380
+ }
2381
+ const vaultAddress = getVaultAddress();
2382
+ const { creator, subdomain, id: syndicateId } = await resolveVaultSyndicate(vaultAddress);
2383
+ const callerAddress = getAccount().address.toLowerCase();
2384
+ if (creator.toLowerCase() !== callerAddress) {
2385
+ spinner.fail("Only the syndicate creator can approve agents");
2386
+ process.exit(1);
2387
+ }
2388
+ const decimals = await getAssetDecimals();
2389
+ const maxPerTx = parseUnits8(opts.maxPerTx, decimals);
2390
+ const dailyLimit = parseUnits8(opts.dailyLimit, decimals);
2391
+ spinner.text = "Registering agent on vault...";
2392
+ try {
2393
+ const regHash = await registerAgent(
2394
+ BigInt(opts.agentId),
2395
+ opts.pkp,
2396
+ opts.eoa,
2397
+ maxPerTx,
2398
+ dailyLimit
2399
+ );
2400
+ console.log(DIM(` Agent registered: ${getExplorerUrl(regHash)}`));
2401
+ } catch (regErr) {
2402
+ const msg = regErr instanceof Error ? regErr.message : String(regErr);
2403
+ if (msg.includes("0xe098d3ee") || msg.includes("AgentAlreadyRegistered")) {
2404
+ console.log(DIM(" Agent already registered on vault \u2014 skipping"));
2405
+ } else {
2406
+ throw regErr;
2407
+ }
2408
+ }
2409
+ spinner.text = "Creating approval attestation...";
2410
+ const { uid: approvalUid } = await createApproval(
2411
+ syndicateId,
2412
+ BigInt(opts.agentId),
2413
+ vaultAddress,
2414
+ opts.eoa
2415
+ );
2416
+ if (opts.revokeRequest) {
2417
+ spinner.text = "Revoking join request...";
2418
+ await revokeAttestation(
2419
+ EAS_SCHEMAS().SYNDICATE_JOIN_REQUEST,
2420
+ opts.revokeRequest
2421
+ );
2422
+ }
2423
+ try {
2424
+ spinner.text = "Adding to chat...";
2425
+ const xmtp = await loadXmtp();
2426
+ const xmtpClient = await xmtp.getXmtpClient();
2427
+ const group = await xmtp.getGroup(xmtpClient, subdomain);
2428
+ await xmtp.addMember(group, opts.pkp);
2429
+ await xmtp.sendEnvelope(group, {
2430
+ type: "AGENT_REGISTERED",
2431
+ agent: { erc8004Id: Number(opts.agentId), address: opts.pkp },
2432
+ syndicate: subdomain,
2433
+ timestamp: Math.floor(Date.now() / 1e3)
2434
+ });
2435
+ console.log(DIM(` Added to chat: ${subdomain}`));
2436
+ } catch {
2437
+ console.warn(chalk5.yellow(" \u26A0 Could not add agent to chat group"));
2438
+ console.warn(chalk5.dim(` If no group exists, run: sherwood chat ${subdomain} init`));
2439
+ }
2440
+ spinner.succeed("Agent approved and registered");
2441
+ console.log();
2442
+ console.log(LABEL(" \u25C6 Agent Approved"));
2443
+ SEP();
2444
+ console.log(W(` Agent ID: #${opts.agentId}`));
2445
+ console.log(W(` PKP: ${G(opts.pkp)}`));
2446
+ console.log(W(` EOA: ${G(opts.eoa)}`));
2447
+ console.log(W(` Approval: ${DIM(approvalUid)}`));
2448
+ console.log(W(` EAS Scan: ${DIM(getEasScanUrl(approvalUid))}`));
2449
+ SEP();
2450
+ } catch (err) {
2451
+ spinner.fail("Approval failed");
2452
+ console.error(chalk5.red(err instanceof Error ? err.message : String(err)));
2453
+ process.exit(1);
2454
+ }
2455
+ });
2456
+ syndicate.command("reject").description("Reject a join request by revoking its attestation").requiredOption("--attestation <uid>", "Join request attestation UID to revoke").action(async (opts) => {
2457
+ const spinner = ora5("Revoking attestation...").start();
2458
+ try {
2459
+ const hash = await revokeAttestation(
2460
+ EAS_SCHEMAS().SYNDICATE_JOIN_REQUEST,
2461
+ opts.attestation
2462
+ );
2463
+ spinner.succeed("Join request rejected");
2464
+ console.log(DIM(` ${getExplorerUrl(hash)}`));
2465
+ } catch (err) {
2466
+ spinner.fail("Rejection failed");
2467
+ console.error(chalk5.red(err instanceof Error ? err.message : String(err)));
2468
+ process.exit(1);
2469
+ }
2470
+ });
2471
+ var vaultCmd = program.command("vault");
2472
+ vaultCmd.command("deposit").description("Deposit into a vault").option("--vault <address>", "Vault address (default: from config)").requiredOption("--amount <amount>", "Amount to deposit (in asset units)").action(async (opts) => {
2473
+ resolveVault(opts);
2474
+ const decimals = await getAssetDecimals();
2475
+ const amount = parseUnits8(opts.amount, decimals);
2476
+ const spinner = ora5(`Depositing ${opts.amount}...`).start();
2477
+ try {
2478
+ const hash = await deposit(amount);
2479
+ spinner.succeed(`Deposited: ${hash}`);
2480
+ console.log(chalk5.dim(` ${getExplorerUrl(hash)}`));
2481
+ } catch (err) {
2482
+ spinner.fail("Deposit failed");
2483
+ console.error(chalk5.red(err instanceof Error ? err.message : String(err)));
2484
+ process.exit(1);
2485
+ }
2486
+ });
2487
+ vaultCmd.command("ragequit").description("Withdraw all shares from a vault").option("--vault <address>", "Vault address (default: from config)").action(async (opts) => {
2488
+ resolveVault(opts);
2489
+ const spinner = ora5("Ragequitting...").start();
2490
+ try {
2491
+ const hash = await ragequit();
2492
+ spinner.succeed(`Ragequit: ${hash}`);
2493
+ console.log(chalk5.dim(` ${getExplorerUrl(hash)}`));
2494
+ } catch (err) {
2495
+ spinner.fail("Ragequit failed");
2496
+ console.error(chalk5.red(err instanceof Error ? err.message : String(err)));
2497
+ process.exit(1);
2498
+ }
2499
+ });
2500
+ vaultCmd.command("info").description("Display vault state").option("--vault <address>", "Vault address (default: from config)").action(async (opts) => {
2501
+ resolveVault(opts);
2502
+ const spinner = ora5("Loading vault info...").start();
2503
+ try {
2504
+ const info = await getVaultInfo();
2505
+ spinner.stop();
2506
+ console.log();
2507
+ console.log(chalk5.bold("Vault Info"));
2508
+ console.log(chalk5.dim("\u2500".repeat(40)));
2509
+ console.log(` Address: ${info.address}`);
2510
+ console.log(` Total Assets: ${info.totalAssets}`);
2511
+ console.log(` Agent Count: ${info.agentCount}`);
2512
+ console.log(` Daily Spend: ${info.dailySpendTotal}`);
2513
+ console.log();
2514
+ console.log(chalk5.bold(" Syndicate Caps"));
2515
+ console.log(` Max Per Tx: ${info.syndicateCaps.maxPerTx}`);
2516
+ console.log(` Max Daily: ${info.syndicateCaps.maxDailyTotal}`);
2517
+ console.log(` Max Borrow: ${info.syndicateCaps.maxBorrowRatio}`);
2518
+ console.log();
2519
+ } catch (err) {
2520
+ spinner.fail("Failed to load vault info");
2521
+ console.error(chalk5.red(err instanceof Error ? err.message : String(err)));
2522
+ process.exit(1);
2523
+ }
2524
+ });
2525
+ vaultCmd.command("balance").description("Show LP share balance and asset value").option("--vault <address>", "Vault address (default: from config)").option("--address <address>", "Address to check (default: your wallet)").action(async (opts) => {
2526
+ resolveVault(opts);
2527
+ const spinner = ora5("Loading balance...").start();
2528
+ try {
2529
+ const balance = await getBalance(opts.address);
2530
+ spinner.stop();
2531
+ console.log();
2532
+ console.log(chalk5.bold("LP Position"));
2533
+ console.log(chalk5.dim("\u2500".repeat(40)));
2534
+ console.log(` Shares: ${balance.shares.toString()}`);
2535
+ console.log(` Asset Value: ${balance.assetsValue}`);
2536
+ console.log(` % of Vault: ${balance.percentOfVault}`);
2537
+ console.log();
2538
+ } catch (err) {
2539
+ spinner.fail("Failed to load balance");
2540
+ console.error(chalk5.red(err instanceof Error ? err.message : String(err)));
2541
+ process.exit(1);
2542
+ }
2543
+ });
2544
+ vaultCmd.command("add-target").description("Add a target to the vault allowlist (owner only)").option("--vault <address>", "Vault address (default: from config)").requiredOption("--target <address>", "Target address to allow").action(async (opts) => {
2545
+ resolveVault(opts);
2546
+ const spinner = ora5("Adding target...").start();
2547
+ try {
2548
+ const hash = await addTarget(opts.target);
2549
+ spinner.succeed(`Target added: ${hash}`);
2550
+ console.log(chalk5.dim(` ${getExplorerUrl(hash)}`));
2551
+ } catch (err) {
2552
+ spinner.fail("Failed to add target");
2553
+ console.error(chalk5.red(err instanceof Error ? err.message : String(err)));
2554
+ process.exit(1);
2555
+ }
2556
+ });
2557
+ vaultCmd.command("remove-target").description("Remove a target from the vault allowlist (owner only)").option("--vault <address>", "Vault address (default: from config)").requiredOption("--target <address>", "Target address to remove").action(async (opts) => {
2558
+ resolveVault(opts);
2559
+ const spinner = ora5("Removing target...").start();
2560
+ try {
2561
+ const hash = await removeTarget(opts.target);
2562
+ spinner.succeed(`Target removed: ${hash}`);
2563
+ console.log(chalk5.dim(` ${getExplorerUrl(hash)}`));
2564
+ } catch (err) {
2565
+ spinner.fail("Failed to remove target");
2566
+ console.error(chalk5.red(err instanceof Error ? err.message : String(err)));
2567
+ process.exit(1);
2568
+ }
2569
+ });
2570
+ vaultCmd.command("targets").description("List allowed targets for a vault").option("--vault <address>", "Vault address (default: from config)").action(async (opts) => {
2571
+ resolveVault(opts);
2572
+ const spinner = ora5("Loading targets...").start();
2573
+ try {
2574
+ const targets = await getAllowedTargets();
2575
+ spinner.stop();
2576
+ console.log();
2577
+ console.log(chalk5.bold(`Allowed Targets (${targets.length})`));
2578
+ console.log(chalk5.dim("\u2500".repeat(50)));
2579
+ for (const t of targets) {
2580
+ console.log(` ${t}`);
2581
+ }
2582
+ console.log();
2583
+ } catch (err) {
2584
+ spinner.fail("Failed to load targets");
2585
+ console.error(chalk5.red(err instanceof Error ? err.message : String(err)));
2586
+ process.exit(1);
2587
+ }
2588
+ });
2589
+ var strategy = program.command("strategy");
2590
+ strategy.command("list").description("List registered strategies").option("--type <id>", "Filter by strategy type").action(async (opts) => {
2591
+ const spinner = ora5("Loading strategies...").start();
2592
+ try {
2593
+ const strategies = await listStrategies(
2594
+ opts.type ? BigInt(opts.type) : void 0
2595
+ );
2596
+ spinner.stop();
2597
+ if (strategies.length === 0) {
2598
+ console.log(chalk5.dim("No strategies registered."));
2599
+ return;
2600
+ }
2601
+ console.log();
2602
+ console.log(chalk5.bold(`Strategies (${strategies.length})`));
2603
+ console.log(chalk5.dim("\u2500".repeat(70)));
2604
+ for (const s of strategies) {
2605
+ const status = s.active ? chalk5.green("active") : chalk5.red("inactive");
2606
+ console.log(` #${s.id} ${chalk5.bold(s.name)} [type: ${s.strategyTypeId}] ${status}`);
2607
+ console.log(` Creator: ${s.creator}`);
2608
+ console.log(` Implementation: ${s.implementation}`);
2609
+ if (s.metadataURI) {
2610
+ console.log(` Metadata: ${chalk5.dim(s.metadataURI)}`);
2611
+ }
2612
+ console.log();
2613
+ }
2614
+ } catch (err) {
2615
+ spinner.fail("Failed to load strategies");
2616
+ console.error(chalk5.red(err instanceof Error ? err.message : String(err)));
2617
+ process.exit(1);
2618
+ }
2619
+ });
2620
+ strategy.command("info").description("Show strategy details").argument("<id>", "Strategy ID").action(async (idStr) => {
2621
+ const spinner = ora5("Loading strategy...").start();
2622
+ try {
2623
+ const s = await getStrategy(BigInt(idStr));
2624
+ spinner.stop();
2625
+ console.log();
2626
+ console.log(chalk5.bold(`Strategy #${s.id}`));
2627
+ console.log(chalk5.dim("\u2500".repeat(40)));
2628
+ console.log(` Name: ${s.name}`);
2629
+ console.log(` Type: ${s.strategyTypeId}`);
2630
+ console.log(` Active: ${s.active ? chalk5.green("yes") : chalk5.red("no")}`);
2631
+ console.log(` Creator: ${s.creator}`);
2632
+ console.log(` Implementation: ${s.implementation}`);
2633
+ if (s.metadataURI) {
2634
+ console.log(` Metadata: ${chalk5.dim(s.metadataURI)}`);
2635
+ }
2636
+ console.log();
2637
+ } catch (err) {
2638
+ spinner.fail("Failed to load strategy");
2639
+ console.error(chalk5.red(err instanceof Error ? err.message : String(err)));
2640
+ process.exit(1);
2641
+ }
2642
+ });
2643
+ strategy.command("register").description("Register a new strategy on-chain").requiredOption("--implementation <address>", "Strategy contract address").requiredOption("--type <id>", "Strategy type ID").requiredOption("--name <name>", "Strategy name").option("--metadata <uri>", "Metadata URI (IPFS/Arweave)", "").action(async (opts) => {
2644
+ const spinner = ora5("Registering strategy...").start();
2645
+ try {
2646
+ const hash = await registerStrategy(
2647
+ opts.implementation,
2648
+ BigInt(opts.type),
2649
+ opts.name,
2650
+ opts.metadata
2651
+ );
2652
+ spinner.succeed(`Strategy registered: ${hash}`);
2653
+ console.log(chalk5.dim(` ${getExplorerUrl(hash)}`));
2654
+ } catch (err) {
2655
+ spinner.fail("Registration failed");
2656
+ console.error(chalk5.red(err instanceof Error ? err.message : String(err)));
2657
+ process.exit(1);
2658
+ }
2659
+ });
2660
+ strategy.command("run").description("Execute the levered swap strategy").option("--vault <address>", "Vault address (default: from config)").requiredOption("--collateral <amount>", "WETH collateral amount (e.g. 1.0)").requiredOption("--borrow <amount>", "USDC to borrow against collateral").requiredOption("--token <address>", "Target token address to buy").option("--fee <tier>", "Uniswap fee tier in bps (500, 3000, 10000)", "500").option("--slippage <bps>", "Slippage tolerance in bps", "100").option("--execute", "Actually execute on-chain (default: simulate only)", false).action(async (opts) => {
2661
+ resolveVault(opts);
2662
+ await runLeveredSwap(opts);
2663
+ });
2664
+ program.command("providers").description("List available DeFi providers").action(() => {
2665
+ const providers = [new MoonwellProvider(), new UniswapProvider()];
2666
+ for (const p of providers) {
2667
+ const info = p.info();
2668
+ console.log(`
2669
+ ${info.name} (${info.type})`);
2670
+ console.log(` Capabilities: ${info.capabilities.join(", ")}`);
2671
+ console.log(` Chains: ${info.supportedChains.map((c) => c.name).join(", ")}`);
2672
+ }
2673
+ });
2674
+ try {
2675
+ const { registerChatCommands } = await import("./commands/chat.js");
2676
+ registerChatCommands(program);
2677
+ } catch {
2678
+ program.command("chat <name> [action] [actionArgs...]").description("Syndicate chat (XMTP) \u2014 requires native bindings").action(() => {
2679
+ console.error(chalk5.red("XMTP native bindings not available."));
2680
+ console.error(chalk5.dim("Chat requires native dependencies that can't be embedded in standalone binaries."));
2681
+ console.error(chalk5.dim("Install via npm: npm i -g @sherwoodagent/cli"));
2682
+ console.error(chalk5.dim("Or from source: cd cli && npm i && npm run dev -- chat <name>"));
2683
+ process.exit(1);
2684
+ });
2685
+ }
2686
+ registerVeniceCommands(program);
2687
+ registerAllowanceCommands(program);
2688
+ registerIdentityCommands(program);
2689
+ var configCmd = program.command("config");
2690
+ 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").action((opts) => {
2691
+ let saved = false;
2692
+ if (opts.privateKey) {
2693
+ setPrivateKey(opts.privateKey);
2694
+ const account = getAccount();
2695
+ console.log(chalk5.green("Private key saved to ~/.sherwood/config.json"));
2696
+ console.log(chalk5.dim(` Wallet: ${account.address}`));
2697
+ saved = true;
2698
+ }
2699
+ if (opts.vault) {
2700
+ const chainId = getChain().id;
2701
+ setChainContract(chainId, "vault", opts.vault);
2702
+ console.log(chalk5.green(`Vault saved to ~/.sherwood/config.json (chain ${chainId})`));
2703
+ console.log(chalk5.dim(` Vault: ${opts.vault}`));
2704
+ saved = true;
2705
+ }
2706
+ if (!saved) {
2707
+ console.log(chalk5.red("Provide at least one of: --private-key, --vault"));
2708
+ process.exit(1);
2709
+ }
2710
+ });
2711
+ configCmd.command("show").description("Display current config for the active network").action(() => {
2712
+ const chainId = getChain().id;
2713
+ const contracts = getChainContracts(chainId);
2714
+ const config = loadConfig();
2715
+ console.log();
2716
+ console.log(chalk5.bold(`Sherwood Config (chain ${chainId})`));
2717
+ console.log(chalk5.dim("\u2500".repeat(50)));
2718
+ console.log(` Wallet: ${config.privateKey ? chalk5.green("configured") : chalk5.dim("not set")}`);
2719
+ console.log(` Agent ID: ${config.agentId ?? chalk5.dim("not set")}`);
2720
+ console.log(` Vault: ${contracts.vault ?? chalk5.dim("not set")}`);
2721
+ console.log();
2722
+ console.log(chalk5.dim(" Config file: ~/.sherwood/config.json"));
2723
+ console.log();
2724
+ });
2725
+ program.parse();
2726
+ //# sourceMappingURL=index.js.map