@sage-protocol/cli 0.8.0 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +12 -11
  2. package/dist/cli/commands/boost.js +339 -62
  3. package/dist/cli/commands/bounty.js +28 -4
  4. package/dist/cli/commands/config.js +10 -1
  5. package/dist/cli/commands/contributor.js +16 -6
  6. package/dist/cli/commands/dao.js +1 -1
  7. package/dist/cli/commands/discover.js +3 -3
  8. package/dist/cli/commands/governance.js +141 -58
  9. package/dist/cli/commands/install.js +178 -36
  10. package/dist/cli/commands/ipfs.js +12 -2
  11. package/dist/cli/commands/library.js +277 -268
  12. package/dist/cli/commands/members.js +132 -18
  13. package/dist/cli/commands/multiplier.js +101 -13
  14. package/dist/cli/commands/nft.js +16 -3
  15. package/dist/cli/commands/personal.js +69 -2
  16. package/dist/cli/commands/prompt.js +1 -1
  17. package/dist/cli/commands/proposals.js +153 -3
  18. package/dist/cli/commands/stake-status.js +130 -56
  19. package/dist/cli/commands/sxxx.js +37 -4
  20. package/dist/cli/commands/wallet.js +5 -10
  21. package/dist/cli/contracts/index.js +2 -1
  22. package/dist/cli/index.js +5 -0
  23. package/dist/cli/privy-auth-wallet-manager.js +3 -2
  24. package/dist/cli/services/config/chain-defaults.js +1 -1
  25. package/dist/cli/services/config/manager.js +3 -0
  26. package/dist/cli/services/config/schema.js +1 -0
  27. package/dist/cli/services/ipfs/onboarding.js +11 -0
  28. package/dist/cli/utils/aliases.js +62 -3
  29. package/dist/cli/utils/cli-ui.js +1 -1
  30. package/dist/cli/utils/provider.js +7 -3
  31. package/dist/cli/wallet-manager.js +7 -12
  32. package/dist/prompts/e2e-test-prompt.md +22 -0
  33. package/dist/prompts/skills/build-web3/plugin.json +11 -0
  34. package/package.json +1 -1
package/README.md CHANGED
@@ -9,7 +9,7 @@ sage --help
9
9
 
10
10
  ## Quick Start
11
11
 
12
- ### Personal Library (No Governance)
12
+ ### Personal/Vault Library (No Governance)
13
13
 
14
14
  Create a wallet-owned library for your prompts without DAO overhead:
15
15
 
@@ -20,13 +20,13 @@ Create a wallet-owned library for your prompts without DAO overhead:
20
20
  sage wallet connect
21
21
 
22
22
  # 2. Create a personal library
23
- sage library personal create --name "My Prompts"
23
+ sage library vault create --name "My Prompts"
24
24
 
25
25
  # 3. Push prompts to your library
26
- sage library personal push <libraryId> --dir ./prompts
26
+ sage library vault push <libraryId> --dir ./prompts
27
27
 
28
28
  # 4. List your libraries
29
- sage library personal list
29
+ sage library vault list
30
30
  ```
31
31
 
32
32
  ### DAO Library (With Governance)
@@ -70,6 +70,7 @@ sage install github:user/repo
70
70
  | `sage library personal list` | List your personal libraries |
71
71
  | `sage library personal push` | Push files to a personal library |
72
72
  | `sage library personal delete` | Delete a personal library |
73
+ | `sage library vault ...` | Alias for personal libraries (clearer SIWE vault naming) |
73
74
 
74
75
  ### Prompt Workspace
75
76
 
@@ -133,7 +134,7 @@ SAGE_WALLET_TYPE=privy sage wallet connect
133
134
  | `sage gov execute <proposalId>` | Execute a queued proposal |
134
135
  | `sage proposals list` | List proposals for current DAO |
135
136
 
136
- ## Personal Libraries
137
+ ## Personal/Vault Libraries
137
138
 
138
139
  Personal libraries are wallet-owned collections that don't require DAO governance. They're ideal for:
139
140
 
@@ -144,20 +145,20 @@ Personal libraries are wallet-owned collections that don't require DAO governanc
144
145
  ### Create and Manage
145
146
 
146
147
  ```bash
147
- # Create a new personal library
148
- sage library personal create --name "My AI Prompts" --description "Curated prompts for development"
148
+ # Create a new vault library (alias of personal)
149
+ sage library vault create --name "My AI Prompts" --description "Curated prompts for development"
149
150
 
150
151
  # View your libraries
151
- sage library personal list
152
+ sage library vault list
152
153
 
153
154
  # Push content from a directory
154
- sage library personal push lib_abc123 --dir ./my-prompts
155
+ sage library vault push lib_abc123 --dir ./my-prompts
155
156
 
156
157
  # Get library details
157
- sage library personal info lib_abc123
158
+ sage library vault info lib_abc123
158
159
 
159
160
  # Delete a library
160
- sage library personal delete lib_abc123
161
+ sage library vault delete lib_abc123
161
162
  ```
162
163
 
163
164
  ### Premium Content
@@ -9,6 +9,45 @@ function register(program) {
9
9
  .description('USDC governance boosts (direct or policy-based)')
10
10
  .addHelpText('after', `\nAgent/Automation Support:\n Use -y/--yes flag or SAGE_YES=1 environment variable to skip all interactive prompts.\n\nExamples:\n $ sage boost create --proposal-id 1 --governor 0x... --per-voter 1000000 --max-voters 100 --yes\n $ sage boost fund --proposal-id 1 --amount 10000000 -y\n`);
11
11
 
12
+ const resolveSubgraphUrl = () => (cliConfig.resolveSubgraphUrl ? cliConfig.resolveSubgraphUrl() : (
13
+ process.env.SUBGRAPH_URL || process.env.SAGE_SUBGRAPH_URL || process.env.NEXT_PUBLIC_GRAPH_ENDPOINT || process.env.NEXT_PUBLIC_SUBGRAPH_URL || null
14
+ ));
15
+
16
+ const parseProofInput = (value) => {
17
+ const fs = require('fs');
18
+ if (!value) return null;
19
+ let raw = String(value).trim();
20
+ if (!raw.startsWith('[')) {
21
+ raw = fs.readFileSync(raw, 'utf8');
22
+ }
23
+ return JSON.parse(raw);
24
+ };
25
+
26
+ const detectManagerType = async (provider, address) => {
27
+ const directIface = new ethers.Interface([
28
+ 'function getBoost(uint256) view returns (tuple(address creator, address governor, uint256 proposalId, uint256 snapshot, uint256 perVoter, uint256 maxVoters, uint256 votersPaid, uint96 minVotes, uint8 payoutMode, uint8 support, uint256 startAt, uint256 expiresAt, uint256 totalPool, uint256 totalPaid, uint8 kind, address policy, bool active, bool paused) boost)'
29
+ ]);
30
+ const merkleIface = new ethers.Interface([
31
+ 'function getBoost(uint256) view returns (uint256,uint256,bytes32,bool,bool,address)'
32
+ ]);
33
+ try {
34
+ const callData = directIface.encodeFunctionData('getBoost', [0n]);
35
+ const ret = await provider.call({ to: address, data: callData });
36
+ directIface.decodeFunctionResult('getBoost', ret);
37
+ return 'direct';
38
+ } catch (_) {
39
+ try {
40
+ const callDataM = merkleIface.encodeFunctionData('getBoost', [0n]);
41
+ const retM = await provider.call({ to: address, data: callDataM });
42
+ merkleIface.decodeFunctionResult('getBoost', retM);
43
+ return 'merkle';
44
+ } catch (_) {
45
+ return 'unknown';
46
+ }
47
+ }
48
+ };
49
+
50
+
12
51
  boost
13
52
  .command('create')
14
53
  .description('Create a boost (direct/Merkle/custom) with payout mode and time window')
@@ -49,31 +88,7 @@ function register(program) {
49
88
  if (!usdcAddr) throw new Error('Missing USDC token (env USDC_ADDRESS/MOCK_USDC_ADDRESS) and manager.usdc() not readable');
50
89
 
51
90
  // ===== Manager-type preflight (DIRECT vs MERKLE) =====
52
- const detectManagerType = async (address) => {
53
- const directIface = new ethers.Interface([
54
- 'function getBoost(uint256) view returns (tuple(address creator, address governor, uint256 proposalId, uint256 snapshot, uint256 perVoter, uint256 maxVoters, uint256 votersPaid, uint96 minVotes, uint8 payoutMode, uint8 support, uint256 startAt, uint256 expiresAt, uint256 totalPool, uint256 totalPaid, uint8 kind, address policy, bool active, bool paused) boost)'
55
- ]);
56
- const merkleIface = new ethers.Interface([
57
- 'function getBoost(uint256) view returns (uint256,uint256,bytes32,bool,bool,address)'
58
- ]);
59
- try {
60
- const callData = directIface.encodeFunctionData('getBoost', [0n]);
61
- const ret = await provider.call({ to: address, data: callData });
62
- directIface.decodeFunctionResult('getBoost', ret); // will throw if not direct
63
- return 'direct';
64
- } catch (_) {
65
- try {
66
- const callDataM = merkleIface.encodeFunctionData('getBoost', [0n]);
67
- const retM = await provider.call({ to: address, data: callDataM });
68
- merkleIface.decodeFunctionResult('getBoost', retM);
69
- return 'merkle';
70
- } catch (_) {
71
- return 'unknown';
72
- }
73
- }
74
- };
75
-
76
- const mgrType = await detectManagerType(mgrAddr);
91
+ const mgrType = await detectManagerType(provider, mgrAddr);
77
92
  if (mgrType === 'merkle') {
78
93
  ui.error(`Detected Merkle boost manager at ${mgrAddr}`);
79
94
  ui.output(' This command uses the DIRECT manager ABI (createBoost(address,uint256,...)).');
@@ -288,18 +303,266 @@ function register(program) {
288
303
  } catch (e) { handleCLIError(e, 'set-root'); }
289
304
  });
290
305
 
306
+ boost
307
+ .command('list')
308
+ .description('List boosts (direct/merkle) using subgraph or on-chain logs')
309
+ .option('--type <type>', 'direct|merkle|auto (default: auto)', 'auto')
310
+ .option('--manager <address>', 'Boost manager address (overrides default for the selected type)')
311
+ .option('--governor <address>', 'Filter by governor (direct only)')
312
+ .option('--proposal-id <id>', 'Filter by proposal ID')
313
+ .option('--active', 'Only show active boosts', false)
314
+ .option('--from-block <n>', 'Start block for on-chain log scan (fallback)')
315
+ .option('--limit <n>', 'Maximum number of results (default: 25)', '25')
316
+ .option('--json', 'Output JSON', false)
317
+ .action(async (opts) => {
318
+ try {
319
+ const { enterJsonMode, withJsonVersion } = require('../utils/json-output');
320
+ enterJsonMode(opts);
321
+ ui.configure({ json: opts.json });
322
+ const WM = require('../wallet-manager');
323
+ const wm = new WM(); await wm.connect();
324
+ const provider = wm.getProvider();
325
+
326
+ const limit = Math.max(1, Number(opts.limit || 25));
327
+ let type = String(opts.type || 'auto').toLowerCase();
328
+ const proposalId = opts.proposalId ? BigInt(opts.proposalId) : null;
329
+ const governor = opts.governor ? String(opts.governor).toLowerCase() : null;
330
+
331
+ let directManager = cliConfig.resolveAddress('BOOST_MANAGER_DIRECT_ADDRESS', process.env.BOOST_MANAGER_DIRECT_ADDRESS) || cliConfig.resolveAddress('BOOST_MANAGER_ADDRESS');
332
+ let merkleManager = cliConfig.resolveAddress('BOOST_MANAGER_ADDRESS');
333
+ const loggerGlue = cliConfig.resolveAddress('BOOST_LOGGER_GLUE_ADDRESS', process.env.BOOST_LOGGER_GLUE_ADDRESS);
334
+
335
+ const results = [];
336
+ const sources = [];
337
+
338
+ if (opts.manager) {
339
+ if (type === 'direct') {
340
+ directManager = opts.manager;
341
+ } else if (type === 'merkle') {
342
+ merkleManager = opts.manager;
343
+ } else {
344
+ const detected = await detectManagerType(provider, opts.manager);
345
+ if (detected === 'direct') {
346
+ directManager = opts.manager;
347
+ type = 'direct';
348
+ } else if (detected === 'merkle') {
349
+ merkleManager = opts.manager;
350
+ type = 'merkle';
351
+ } else {
352
+ throw new Error(`Unknown boost manager type at ${opts.manager}. Pass --type direct|merkle.`);
353
+ }
354
+ }
355
+ }
356
+
357
+ const fetchDirectFromSubgraph = async () => {
358
+ const subgraphUrl = resolveSubgraphUrl();
359
+ if (!subgraphUrl) return null;
360
+ const { querySubgraph } = require('../utils/subgraph-client');
361
+ const where = {};
362
+ if (proposalId != null) where.proposalId = proposalId.toString();
363
+ if (governor) where.governor = governor;
364
+ const query = `
365
+ query BoostEvents($first: Int!, $skip: Int!, $orderBy: String!, $orderDirection: String!, $where: BoostEvent_filter) {
366
+ boostEvents(first: $first, skip: $skip, orderBy: $orderBy, orderDirection: $orderDirection, where: $where) {
367
+ id
368
+ proposalId
369
+ governor
370
+ perVoter
371
+ maxVoters
372
+ snapshot
373
+ kind
374
+ policy
375
+ minVotes
376
+ blockTimestamp
377
+ transactionHash
378
+ }
379
+ }
380
+ `;
381
+ const data = await querySubgraph(query, {
382
+ first: limit,
383
+ skip: 0,
384
+ orderBy: 'blockTimestamp',
385
+ orderDirection: 'desc',
386
+ where
387
+ }, { subgraphUrl });
388
+ return data?.boostEvents || [];
389
+ };
390
+
391
+ const fetchDirectFromLogs = async () => {
392
+ if (!loggerGlue) throw new Error('Missing BOOST_LOGGER_GLUE_ADDRESS for log scan fallback');
393
+ const iface = new ethers.Interface([
394
+ 'event BoostCreated(uint256 indexed proposalId, address indexed governor, uint256 perVoter, uint256 maxVoters, uint256 snapshot, uint8 kind, address policy, uint96 minVotes)'
395
+ ]);
396
+ const latest = await provider.getBlockNumber();
397
+ const fromBlock = opts.fromBlock ? Number(opts.fromBlock) : Math.max(0, latest - 200000);
398
+ const topics = [iface.getEvent('BoostCreated').topicHash, null, null];
399
+ if (proposalId != null) topics[1] = ethers.zeroPadValue(ethers.toBeHex(proposalId), 32);
400
+ if (governor) topics[2] = ethers.zeroPadValue(governor, 32);
401
+ const logs = await provider.getLogs({ address: loggerGlue, fromBlock, toBlock: latest, topics });
402
+ return logs.map((log) => {
403
+ const parsed = iface.parseLog(log);
404
+ return {
405
+ id: `${log.transactionHash}-${log.logIndex}`,
406
+ proposalId: parsed.args.proposalId.toString(),
407
+ governor: parsed.args.governor,
408
+ perVoter: parsed.args.perVoter.toString(),
409
+ maxVoters: parsed.args.maxVoters.toString(),
410
+ snapshot: parsed.args.snapshot.toString(),
411
+ kind: Number(parsed.args.kind),
412
+ policy: parsed.args.policy,
413
+ minVotes: parsed.args.minVotes.toString(),
414
+ blockTimestamp: String(log.blockNumber || ''),
415
+ transactionHash: log.transactionHash
416
+ };
417
+ }).slice(0, limit);
418
+ };
419
+
420
+ const fetchMerkleFromLogs = async () => {
421
+ if (!merkleManager) throw new Error('Missing BOOST_MANAGER_ADDRESS for merkle listing');
422
+ const iface = new ethers.Interface([
423
+ 'event BoostCreated(uint256 indexed proposalId, address indexed creator, uint256 totalPool)'
424
+ ]);
425
+ const latest = await provider.getBlockNumber();
426
+ const fromBlock = opts.fromBlock ? Number(opts.fromBlock) : Math.max(0, latest - 200000);
427
+ const topics = [iface.getEvent('BoostCreated').topicHash];
428
+ if (proposalId != null) topics.push(ethers.zeroPadValue(ethers.toBeHex(proposalId), 32));
429
+ const logs = await provider.getLogs({ address: merkleManager, fromBlock, toBlock: latest, topics });
430
+ return logs.map((log) => {
431
+ const parsed = iface.parseLog(log);
432
+ return {
433
+ id: `${log.transactionHash}-${log.logIndex}`,
434
+ proposalId: parsed.args.proposalId.toString(),
435
+ creator: parsed.args.creator,
436
+ totalPool: parsed.args.totalPool.toString(),
437
+ transactionHash: log.transactionHash
438
+ };
439
+ }).slice(0, limit);
440
+ };
441
+
442
+ const enrichDirect = async (items) => {
443
+ if (!directManager) return items;
444
+ const ifDirect = new ethers.Interface([
445
+ 'function getBoost(uint256) view returns (tuple(address creator, address governor, uint256 proposalId, uint256 snapshot, uint256 perVoter, uint256 maxVoters, uint256 votersPaid, uint96 minVotes, uint8 payoutMode, uint8 support, uint256 startAt, uint256 expiresAt, uint256 totalPool, uint256 totalPaid, uint8 kind, address policy, bool active, bool paused) boost)'
446
+ ]);
447
+ const enriched = await Promise.all(items.map(async (row) => {
448
+ try {
449
+ const ret = await provider.call({ to: directManager, data: ifDirect.encodeFunctionData('getBoost', [BigInt(row.proposalId)]) });
450
+ const [b] = ifDirect.decodeFunctionResult('getBoost', ret);
451
+ return {
452
+ ...row,
453
+ managerType: 'direct',
454
+ manager: directManager,
455
+ active: b.active,
456
+ paused: b.paused,
457
+ totalPool: b.totalPool.toString(),
458
+ totalPaid: b.totalPaid.toString(),
459
+ votersPaid: b.votersPaid.toString()
460
+ };
461
+ } catch (_) {
462
+ return { ...row, managerType: 'direct', manager: directManager };
463
+ }
464
+ }));
465
+ return enriched;
466
+ };
467
+
468
+ const enrichMerkle = async (items) => {
469
+ if (!merkleManager) return items;
470
+ const ifMerkle = new ethers.Interface([
471
+ 'function getBoost(uint256) view returns (uint256,uint256,bytes32,bool,bool,address)'
472
+ ]);
473
+ const enriched = await Promise.all(items.map(async (row) => {
474
+ try {
475
+ const ret = await provider.call({ to: merkleManager, data: ifMerkle.encodeFunctionData('getBoost', [BigInt(row.proposalId)]) });
476
+ const [totalPool, totalClaimed, merkleRoot, active, finalized, creator] = ifMerkle.decodeFunctionResult('getBoost', ret);
477
+ return {
478
+ ...row,
479
+ managerType: 'merkle',
480
+ manager: merkleManager,
481
+ totalPool: totalPool.toString(),
482
+ totalClaimed: totalClaimed.toString(),
483
+ merkleRoot,
484
+ active,
485
+ finalized,
486
+ creator
487
+ };
488
+ } catch (_) {
489
+ return { ...row, managerType: 'merkle', manager: merkleManager };
490
+ }
491
+ }));
492
+ return enriched;
493
+ };
494
+
495
+ if (type === 'direct' || type === 'auto') {
496
+ let directItems = null;
497
+ try {
498
+ directItems = await fetchDirectFromSubgraph();
499
+ if (directItems?.length) sources.push('subgraph');
500
+ } catch (e) {
501
+ ui.warn(`Subgraph query failed: ${e.message}. Falling back to logs.`);
502
+ }
503
+ if (!directItems || !directItems.length) {
504
+ directItems = await fetchDirectFromLogs();
505
+ sources.push('logs');
506
+ }
507
+ const enriched = await enrichDirect(directItems || []);
508
+ results.push(...enriched);
509
+ }
510
+
511
+ if (type === 'merkle' || type === 'auto') {
512
+ if (governor && type !== 'direct') {
513
+ ui.warn('Governor filter is only supported for direct boosts; ignoring for merkle listing.');
514
+ }
515
+ const merkleItems = await fetchMerkleFromLogs();
516
+ sources.push('logs');
517
+ const enriched = await enrichMerkle(merkleItems || []);
518
+ results.push(...enriched);
519
+ }
520
+
521
+ const filtered = opts.active ? results.filter(r => r.active) : results;
522
+ if (opts.json) {
523
+ ui.json(withJsonVersion({ count: filtered.length, source: sources, boosts: filtered }));
524
+ return;
525
+ }
526
+
527
+ if (!filtered.length) {
528
+ ui.info('No boosts found matching the criteria.');
529
+ return;
530
+ }
531
+ ui.output(`Found ${filtered.length} boosts (${sources.join(', ')}):\n`);
532
+ for (const b of filtered) {
533
+ if (b.managerType === 'merkle') {
534
+ ui.output(`[${b.proposalId}] merkle boost`);
535
+ ui.output(` Manager: ${b.manager || 'unknown'}`);
536
+ if (b.totalPool) ui.output(` Pool: ${b.totalPool} | Claimed: ${b.totalClaimed ?? '0'}`);
537
+ if (b.active != null) ui.output(` Active: ${b.active} | Finalized: ${b.finalized ?? false}`);
538
+ } else {
539
+ ui.output(`[${b.proposalId}] direct boost`);
540
+ ui.output(` Governor: ${b.governor || 'n/a'} | Manager: ${b.manager || 'unknown'}`);
541
+ ui.output(` Per Voter: ${b.perVoter} | Max Voters: ${b.maxVoters}`);
542
+ if (b.totalPool) ui.output(` Pool: ${b.totalPool} | Paid: ${b.totalPaid ?? '0'} | Voters Paid: ${b.votersPaid ?? '0'}`);
543
+ if (b.active != null) ui.output(` Active: ${b.active} | Paused: ${b.paused ?? false}`);
544
+ }
545
+ ui.output('');
546
+ }
547
+ } catch (e) { handleCLIError(e, 'list'); }
548
+ });
549
+
291
550
  boost
292
551
  .command('status')
293
552
  .description('Show boost status for a proposal')
294
- .requiredOption('--proposal-id <id>', 'Proposal ID')
553
+ .option('--proposal-id <id>', 'Proposal ID')
554
+ .option('--boost-id <id>', 'Alias for proposal ID')
295
555
  .option('--manager <address>', 'Boost manager')
296
556
  .action(async (opts) => {
297
557
  try {
558
+ if (!opts.proposalId && !opts.boostId) {
559
+ throw new Error('Missing --proposal-id (or --boost-id)');
560
+ }
298
561
  const WM = require('../wallet-manager');
299
562
  const wm = new WM(); await wm.connect();
300
563
  const provider = wm.getProvider();
301
564
  const mgrAddr = opts.manager || cliConfig.resolveAddress('BOOST_MANAGER_ADDRESS');
302
- const id = BigInt(opts.proposalId);
565
+ const id = BigInt(opts.proposalId || opts.boostId);
303
566
  const ifMerkle = new ethers.Interface(['function getBoost(uint256) view returns (uint256,uint256,bytes32,bool,bool,address)']);
304
567
  const ifDirect = new ethers.Interface(['function getBoost(uint256) view returns (tuple(address creator, address governor, uint256 proposalId, uint256 snapshot, uint256 perVoter, uint256 maxVoters, uint256 votersPaid, uint96 minVotes, uint8 payoutMode, uint8 support, uint256 startAt, uint256 expiresAt, uint256 totalPool, uint256 totalPaid, uint8 kind, address policy, bool active, bool paused) boost)']);
305
568
  // Try DIRECT first
@@ -307,7 +570,7 @@ function register(program) {
307
570
  const retD = await provider.call({ to: mgrAddr, data: ifDirect.encodeFunctionData('getBoost', [id]) });
308
571
  const [b] = ifDirect.decodeFunctionResult('getBoost', retD);
309
572
  const boostData = {
310
- managerType: 'direct', proposalId: opts.proposalId,
573
+ managerType: 'direct', proposalId: String(opts.proposalId || opts.boostId),
311
574
  creator: b.creator, governor: b.governor, snapshot: b.snapshot.toString(), perVoter: b.perVoter.toString(),
312
575
  maxVoters: b.maxVoters.toString(), votersPaid: b.votersPaid.toString(), minVotes: Number(b.minVotes),
313
576
  kind: Number(b.kind), policy: b.policy, active: b.active, paused: b.paused,
@@ -321,7 +584,7 @@ function register(program) {
321
584
  const retM = await provider.call({ to: mgrAddr, data: ifMerkle.encodeFunctionData('getBoost', [id]) });
322
585
  const [totalPool, totalClaimed, merkleRoot, active, finalized, creator] = ifMerkle.decodeFunctionResult('getBoost', retM);
323
586
  const boostData = {
324
- managerType: 'merkle', proposalId: opts.proposalId,
587
+ managerType: 'merkle', proposalId: String(opts.proposalId || opts.boostId),
325
588
  totalPool: totalPool.toString(), totalClaimed: totalClaimed.toString(), merkleRoot, active, finalized, creator
326
589
  };
327
590
  ui.json(boostData);
@@ -347,27 +610,7 @@ function register(program) {
347
610
  const mgrAddr = opts.manager || cliConfig.resolveAddress('BOOST_MANAGER_ADDRESS');
348
611
 
349
612
  // Manager-type preflight + helpful hint if mismatched
350
- const detectManagerType = async (address) => {
351
- const directIface = new ethers.Interface([
352
- 'function getBoost(uint256) view returns (tuple(address creator, address governor, uint256 proposalId, uint256 snapshot, uint256 perVoter, uint256 maxVoters, uint256 votersPaid, uint96 minVotes, uint8 payoutMode, uint8 support, uint256 startAt, uint256 expiresAt, uint256 totalPool, uint256 totalPaid, uint8 kind, address policy, bool active, bool paused) boost)'
353
- ]);
354
- const merkleIface = new ethers.Interface([
355
- 'function getBoost(uint256) view returns (uint256,uint256,bytes32,bool,bool,address)'
356
- ]);
357
- try {
358
- const retD = await provider.call({ to: address, data: directIface.encodeFunctionData('getBoost', [0n]) });
359
- directIface.decodeFunctionResult('getBoost', retD);
360
- return 'direct';
361
- } catch (_) {
362
- try {
363
- const retM = await provider.call({ to: address, data: merkleIface.encodeFunctionData('getBoost', [0n]) });
364
- merkleIface.decodeFunctionResult('getBoost', retM);
365
- return 'merkle';
366
- } catch (_) { return 'unknown'; }
367
- }
368
- };
369
-
370
- const mgrType = await detectManagerType(mgrAddr);
613
+ const mgrType = await detectManagerType(provider, mgrAddr);
371
614
  const id = BigInt(opts.proposalId);
372
615
  let looksEmpty = false;
373
616
  if (mgrType === 'direct') {
@@ -404,10 +647,12 @@ function register(program) {
404
647
 
405
648
  boost
406
649
  .command('claim')
407
- .description('Claim a boost rebate with Merkle proof')
650
+ .description('Claim a boost rebate (direct or merkle)')
408
651
  .requiredOption('--proposal-id <id>', 'Proposal ID')
409
- .requiredOption('--amount <usdc>', 'USDC amount (6 decimals)')
410
- .requiredOption('--proof <json>', 'JSON array of hex nodes or path to json file')
652
+ .option('--type <type>', 'direct|merkle|auto (default: auto)', 'auto')
653
+ .option('--amount <usdc>', 'USDC amount (required for merkle or variable payouts)')
654
+ .option('--proof <json>', 'JSON array of hex nodes or path to json file')
655
+ .option('--data <hex>', 'ABI-encoded bytes (overrides proof/amount for direct custom eligibility)')
411
656
  .option('--manager <address>', 'Boost manager')
412
657
  .option('--json', 'Output JSON', false)
413
658
  .action(async (opts) => {
@@ -416,19 +661,51 @@ function register(program) {
416
661
  enterJsonMode(opts);
417
662
  ui.configure({ json: opts.json });
418
663
  const WM = require('../wallet-manager');
419
- const fs = require('fs');
420
664
  const wm = new WM(); await wm.connect();
421
665
  const signer = wm.getSigner();
422
- const mgrAddr = opts.manager || cliConfig.resolveAddress('BOOST_MANAGER_ADDRESS');
423
- let proofStr = opts.proof;
424
- if (!proofStr.startsWith('[')) {
425
- proofStr = fs.readFileSync(proofStr, 'utf8');
666
+ const provider = wm.getProvider();
667
+ const mgrAddr = opts.manager || cliConfig.resolveAddress('BOOST_MANAGER_ADDRESS') || cliConfig.resolveAddress('BOOST_MANAGER_DIRECT_ADDRESS');
668
+
669
+ const type = String(opts.type || 'auto').toLowerCase();
670
+ const managerType = type === 'auto' ? await detectManagerType(provider, mgrAddr) : type;
671
+
672
+ if (managerType === 'merkle') {
673
+ if (!opts.amount) throw new Error('--amount is required for merkle claims');
674
+ if (!opts.proof) throw new Error('--proof is required for merkle claims');
675
+ const proof = parseProofInput(opts.proof);
676
+ const iface = new ethers.Interface(['function claim(uint256,address,uint256,bytes32[])']);
677
+ const tx = await signer.sendTransaction({ to: mgrAddr, data: iface.encodeFunctionData('claim', [BigInt(opts.proposalId), await signer.getAddress(), BigInt(String(opts.amount).replace(/_/g, '')), proof]) });
678
+ if (opts.json) {
679
+ printOk({ proposalId: String(opts.proposalId), boostId: String(opts.proposalId), manager: mgrAddr, amount: String(opts.amount), txHash: tx.hash });
680
+ return;
681
+ }
682
+ ui.success(`Transaction sent: ${tx.hash}`);
683
+ return;
684
+ }
685
+
686
+ if (managerType !== 'direct') {
687
+ throw new Error(`Unknown boost manager type at ${mgrAddr}. Pass --type direct|merkle.`);
688
+ }
689
+
690
+ let data = opts.data;
691
+ if (!data) {
692
+ if (opts.proof) {
693
+ const proof = parseProofInput(opts.proof);
694
+ const coder = ethers.AbiCoder.defaultAbiCoder();
695
+ if (opts.amount) {
696
+ data = coder.encode(['bytes32[]', 'uint256'], [proof, BigInt(String(opts.amount).replace(/_/g, ''))]);
697
+ } else {
698
+ data = coder.encode(['bytes32[]'], [proof]);
699
+ }
700
+ } else {
701
+ data = '0x';
702
+ }
426
703
  }
427
- const proof = JSON.parse(proofStr);
428
- const iface = new ethers.Interface(['function claim(uint256,address,uint256,bytes32[])']);
429
- const tx = await signer.sendTransaction({ to: mgrAddr, data: iface.encodeFunctionData('claim', [BigInt(opts.proposalId), await signer.getAddress(), BigInt(opts.amount), proof]) });
704
+
705
+ const iface = new ethers.Interface(['function claim(uint256,bytes)']);
706
+ const tx = await signer.sendTransaction({ to: mgrAddr, data: iface.encodeFunctionData('claim', [BigInt(opts.proposalId), data]) });
430
707
  if (opts.json) {
431
- printOk({ proposalId: String(opts.proposalId), boostId: String(opts.proposalId), manager: mgrAddr, amount: String(opts.amount), txHash: tx.hash });
708
+ printOk({ proposalId: String(opts.proposalId), boostId: String(opts.proposalId), manager: mgrAddr, txHash: tx.hash, managerType: 'direct' });
432
709
  return;
433
710
  }
434
711
  ui.success(`Transaction sent: ${tx.hash}`);
@@ -1709,6 +1709,7 @@ function register(program) {
1709
1709
  .option('--limit <number>', 'Maximum number of bounties to show', '10')
1710
1710
  .option('--dao <address>', 'Filter by DAO/SubDAO/Governor address')
1711
1711
  .option('--subdao <address>', 'Alias for --dao')
1712
+ .option('--json', 'Output JSON', false)
1712
1713
  .action(async (opts) => {
1713
1714
  try {
1714
1715
  const bountySystem = cliConfig.resolveAddress('SIMPLE_BOUNTY_SYSTEM_ADDRESS');
@@ -1718,7 +1719,7 @@ function register(program) {
1718
1719
  return;
1719
1720
  }
1720
1721
 
1721
- const rpcUrl = cliConfig.resolveRpcUrl();
1722
+ const rpcUrl = cliConfig.resolveRpcUrl({ allowEnv: true });
1722
1723
 
1723
1724
  // Resolve dao filter to governor address if provided
1724
1725
  let governorFilter = null;
@@ -1745,14 +1746,17 @@ function register(program) {
1745
1746
  const nextIdBigInt = await contracts.getNextBountyId(bountySystem, rpcUrl);
1746
1747
  const totalBounties = Number(nextIdBigInt);
1747
1748
 
1748
- ui.info(`Found ${totalBounties} total bounties`);
1749
- ui.header('Bounty List');
1749
+ if (!opts.json) {
1750
+ ui.info(`Found ${totalBounties} total bounties`);
1751
+ ui.header('Bounty List');
1752
+ }
1750
1753
 
1751
1754
  const limit = Math.min(parseInt(opts.limit), totalBounties);
1752
1755
  const statusFilter = opts.status?.toUpperCase();
1753
1756
  const statusMap = { 'ACTIVE': 0, 'CLAIMED': 1, 'COMPLETED': 2, 'CANCELLED': 3, 'EXPIRED': 4, 'UNDER_REVIEW': 5 };
1754
1757
 
1755
1758
  let displayed = 0;
1759
+ const results = [];
1756
1760
  for (let i = 0; i < totalBounties && displayed < limit; i++) {
1757
1761
  try {
1758
1762
  const bounty = await contracts.getBounty(bountySystem, i, rpcUrl);
@@ -1773,7 +1777,22 @@ function register(program) {
1773
1777
  const statusNames = ['ACTIVE', 'CLAIMED', 'COMPLETED', 'CANCELLED', 'EXPIRED', 'UNDER_REVIEW'];
1774
1778
  const rewardEther = ethers.formatEther(reward);
1775
1779
 
1776
- ui.output(`ID: ${i} | Status: ${statusNames[status]} | Reward: ${rewardEther} SXXX`);
1780
+ results.push({
1781
+ id: i,
1782
+ status: statusNames[status] || String(status),
1783
+ rewardWei: reward?.toString?.() || String(reward),
1784
+ rewardSxxx: rewardEther,
1785
+ creator: bounty.creator || null,
1786
+ deadline: bounty.deadline?.toString?.() || null,
1787
+ minContributorLevel: bounty.minContributorLevel?.toString?.() || null,
1788
+ minTokenBalance: bounty.minTokenBalance?.toString?.() || null,
1789
+ requiredBadgeId: bounty.requiredBadgeId?.toString?.() || null,
1790
+ libraryAction: bounty.libraryAction?.toString?.() || null
1791
+ });
1792
+
1793
+ if (!opts.json) {
1794
+ ui.output(`ID: ${i} | Status: ${statusNames[status]} | Reward: ${rewardEther} SXXX`);
1795
+ }
1777
1796
  displayed++;
1778
1797
 
1779
1798
  } catch (e) {
@@ -1782,6 +1801,11 @@ function register(program) {
1782
1801
  }
1783
1802
  }
1784
1803
 
1804
+ if (opts.json) {
1805
+ ui.json({ ok: true, total: totalBounties, count: results.length, bounties: results });
1806
+ return;
1807
+ }
1808
+
1785
1809
  if (displayed === 0) {
1786
1810
  ui.info('No bounties found matching the criteria');
1787
1811
  }
@@ -762,7 +762,16 @@ function register(program) {
762
762
  .addCommand(
763
763
  new Command('show')
764
764
  .description('Show current configuration')
765
- .action(() => {
765
+ .option('--json', 'Output JSON', false)
766
+ .action((opts) => {
767
+ if (opts.json) {
768
+ ui.json({
769
+ ok: true,
770
+ projectDir: config.getProjectDir(),
771
+ envPath: path.join(config.getProjectDir(), '.env')
772
+ });
773
+ return;
774
+ }
766
775
  ui.keyValue({
767
776
  'Current project directory': config.getProjectDir(),
768
777
  '.env file location': path.join(config.getProjectDir(), '.env')