@sage-protocol/cli 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/cli/commands/doctor.js +87 -8
  2. package/dist/cli/commands/gov-config.js +81 -0
  3. package/dist/cli/commands/governance.js +152 -72
  4. package/dist/cli/commands/library.js +9 -0
  5. package/dist/cli/commands/proposals.js +187 -17
  6. package/dist/cli/commands/skills.js +175 -21
  7. package/dist/cli/commands/subdao.js +22 -2
  8. package/dist/cli/config/playbooks.json +15 -0
  9. package/dist/cli/governance-manager.js +25 -4
  10. package/dist/cli/index.js +5 -6
  11. package/dist/cli/library-manager.js +79 -0
  12. package/dist/cli/mcp-server-stdio.js +1374 -82
  13. package/dist/cli/schemas/manifest.schema.json +55 -0
  14. package/dist/cli/services/doctor/fixers.js +134 -0
  15. package/dist/cli/services/mcp/bulk-operations.js +272 -0
  16. package/dist/cli/services/mcp/dependency-analyzer.js +202 -0
  17. package/dist/cli/services/mcp/library-listing.js +2 -2
  18. package/dist/cli/services/mcp/local-prompt-collector.js +1 -0
  19. package/dist/cli/services/mcp/manifest-downloader.js +5 -3
  20. package/dist/cli/services/mcp/manifest-fetcher.js +17 -1
  21. package/dist/cli/services/mcp/manifest-workflows.js +127 -15
  22. package/dist/cli/services/mcp/quick-start.js +287 -0
  23. package/dist/cli/services/mcp/stdio-runner.js +30 -5
  24. package/dist/cli/services/mcp/template-manager.js +156 -0
  25. package/dist/cli/services/mcp/templates/default-templates.json +84 -0
  26. package/dist/cli/services/mcp/tool-args-validator.js +56 -0
  27. package/dist/cli/services/mcp/trending-formatter.js +1 -1
  28. package/dist/cli/services/mcp/unified-prompt-search.js +2 -2
  29. package/dist/cli/services/metaprompt/designer.js +12 -5
  30. package/dist/cli/services/subdao/applier.js +208 -196
  31. package/dist/cli/services/subdao/planner.js +41 -6
  32. package/dist/cli/subdao-manager.js +14 -0
  33. package/dist/cli/utils/aliases.js +17 -2
  34. package/dist/cli/utils/contract-error-decoder.js +61 -0
  35. package/dist/cli/utils/suggestions.js +17 -12
  36. package/package.json +3 -2
  37. package/src/schemas/manifest.schema.json +55 -0
@@ -41,7 +41,8 @@ function register(program) {
41
41
  try {
42
42
  const HelperABI = ['event ProposalTupleIndexed(uint256 indexed proposalId,address[] targets,uint256[] values,bytes[] calldatas,bytes32 descriptionHash)'];
43
43
  const helper = new ethers.Contract(helperAddr, HelperABI, provider);
44
- const topic = helper.interface.getEventTopic('ProposalTupleIndexed');
44
+ // Ethers v6 compatibility: compute topic hash directly from signature
45
+ const topic = (ethers.id || ((s)=>require('ethers').id(s)))('ProposalTupleIndexed(uint256,address[],uint256[],bytes[],bytes32)');
45
46
  const address = helperAddr;
46
47
  const filter = { address, topics: [topic] };
47
48
  // Respect from-block and max-range by chunking if needed
@@ -152,18 +153,37 @@ function register(program) {
152
153
  }
153
154
  }
154
155
  if (!emitted) {
155
- const GovernanceManager = require('../governance-manager');
156
- const gm = new GovernanceManager();
157
- await gm.configureContext({ provider, governor: ctx.governor, timelock: ctx.timelock, subdao: ctx.subdao });
158
- const summary = await gm.describeProposal?.(id).catch(async () => ({ id, description: '(unable to summarize with current provider limits)' }));
159
- if (opts.json) console.log(JSON.stringify(withJsonVersion(summary), null, 2));
160
- else {
161
- console.log(`Proposal ${summary.id}`);
162
- if (summary.title) console.log(summary.title);
163
- if (summary.description) console.log(summary.description);
164
- if (Array.isArray(summary.targets)) {
165
- console.log('Targets:', summary.targets.length);
156
+ // Fallback: use SDK getProposalDetails
157
+ try {
158
+ const { getProposalDetails } = require('@sage-protocol/sdk');
159
+ const details = await getProposalDetails({
160
+ governorAddress: ctx.governor,
161
+ proposalId: pid,
162
+ provider,
163
+ helperAddress: helperAddr
164
+ });
165
+ const summary = {
166
+ id: pid.toString(),
167
+ source: 'sdk',
168
+ actionCount: details.targets?.length || 0,
169
+ targets: details.targets || [],
170
+ values: (details.values || []).map(v => v.toString()),
171
+ selectors: (details.calldatas || []).map(d => (typeof d === 'string' ? d.slice(0, 10) : '0x')),
172
+ description: details.description || ''
173
+ };
174
+ if (opts.json) console.log(JSON.stringify(withJsonVersion(summary), null, 2));
175
+ else {
176
+ console.log(`Proposal ${summary.id} (SDK)`);
177
+ console.log(` Actions: ${summary.actionCount}`);
178
+ if (summary.targets.length) console.log(` First target: ${summary.targets[0]}`);
179
+ if (summary.selectors.length) console.log(` First selector: ${summary.selectors[0]}`);
180
+ if (summary.description) console.log(` Description: ${summary.description.slice(0, 100)}...`);
166
181
  }
182
+ } catch (e) {
183
+ // Final fallback: minimal info
184
+ const summary = { id: pid.toString(), description: '(unable to fetch proposal details)' };
185
+ if (opts.json) console.log(JSON.stringify(withJsonVersion(summary), null, 2));
186
+ else console.log(`Proposal ${summary.id} - ${summary.description}`);
167
187
  }
168
188
  }
169
189
  } catch (e) {
@@ -252,9 +272,17 @@ function register(program) {
252
272
  const ctx = await resolveGovContext({ govOpt: opts.gov, subdaoOpt: opts.subdao, provider });
253
273
  try { const net = await provider.getNetwork(); ctx.chainId = Number(net.chainId || 0); } catch (_) {}
254
274
  printContextBanner(ctx, opts);
275
+ // Initialize wallet first, then GovernanceManager
276
+ const WalletManager = require('../wallet-manager');
277
+ const wm = new WalletManager();
278
+ await wm.connect();
279
+ const signer = wm.getSigner();
280
+ const walletProvider = wm.getProvider();
281
+
255
282
  const GovernanceManager = require('../governance-manager');
256
283
  const gm = new GovernanceManager();
257
- await gm.configureContext({ provider, governor: ctx.governor, timelock: ctx.timelock, subdao: ctx.subdao });
284
+ // Initialize with wallet connection
285
+ await gm.initialize(ctx.subdao || null, { governorOverride: ctx.governor });
258
286
  await gm.vote(id, v);
259
287
  console.log('✅ Vote submitted');
260
288
  } catch (e) {
@@ -269,6 +297,7 @@ function register(program) {
269
297
  .argument('<id>', 'Proposal ID')
270
298
  .option('--gov <address>', 'Governor address (override)')
271
299
  .option('--subdao <address>', 'SubDAO address (derive governor)')
300
+ .option('--simulate', 'Simulate execution without broadcasting a transaction', false)
272
301
  .action(async (id, opts) => {
273
302
  try {
274
303
  enterJsonMode(opts);
@@ -277,15 +306,31 @@ function register(program) {
277
306
  const provider = new ethers.JsonRpcProvider(process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
278
307
  const ctx = await resolveGovContext({ govOpt: opts.gov, subdaoOpt: opts.subdao, provider });
279
308
  try { const net = await provider.getNetwork(); ctx.chainId = Number(net.chainId || 0); } catch (_) {}
280
- printContextBanner(ctx, opts);
309
+ if (!opts.simulate) printContextBanner(ctx, opts);
310
+
311
+ // Initialize wallet first, then GovernanceManager
312
+ const WalletManager = require('../wallet-manager');
313
+ const wm = new WalletManager();
314
+ if (!opts.simulate) {
315
+ await wm.connect();
316
+ }
317
+
281
318
  const GovernanceManager = require('../governance-manager');
282
319
  const gm = new GovernanceManager();
283
- await gm.configureContext({ provider, governor: ctx.governor, timelock: ctx.timelock, subdao: ctx.subdao });
320
+ // Initialize with wallet connection (or just provider for simulation)
321
+ await gm.initialize(ctx.subdao || null, { governorOverride: ctx.governor, providerOverride: provider, signerOverride: opts.simulate ? null : undefined });
322
+
323
+ // If simulate, pass flag
324
+ if (opts.simulate) {
325
+ await gm.executeProposal(id, { simulate: true });
326
+ return;
327
+ }
328
+
284
329
  // Delegate to existing library-manager execute flow if it is a library update; otherwise, try queue/execute via gm
285
330
  try {
286
- await gm.queue(id);
331
+ await gm.queueProposal(id);
287
332
  } catch (_) {}
288
- await gm.execute(id);
333
+ await gm.executeProposal(id);
289
334
  console.log('✅ Execute complete');
290
335
  } catch (e) {
291
336
  const { handleCLIError } = require('../utils/error-handler');
@@ -293,6 +338,131 @@ function register(program) {
293
338
  }
294
339
  });
295
340
 
341
+ cmd
342
+ .command('status')
343
+ .description('Show inbox + next recommended action for proposals')
344
+ .option('--gov <address>', 'Governor address (override)')
345
+ .option('--subdao <address>', 'SubDAO address (derive governor)')
346
+ .option('--json', 'Emit JSON output', false)
347
+ .action(async (opts) => {
348
+ try {
349
+ enterJsonMode(opts);
350
+ const { ethers } = require('ethers');
351
+ const { resolveGovContext } = require('../utils/gov-context');
352
+ const provider = new ethers.JsonRpcProvider(process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
353
+ const ctx = await resolveGovContext({ govOpt: opts.gov, subdaoOpt: opts.subdao, provider });
354
+
355
+ // Get inbox
356
+ const inboxCmd = cmd.commands.find(c => c.name() === 'inbox');
357
+ if (!inboxCmd) {
358
+ throw new Error('inbox command not found');
359
+ }
360
+
361
+ // Call inbox logic inline
362
+ const latest = await provider.getBlockNumber();
363
+ const windowDefault = Number(process.env.SAGE_INBOX_BLOCK_WINDOW || 50000);
364
+ const fromBlock = Math.max(0, Number(latest) - windowDefault);
365
+ const toBlock = 'latest';
366
+
367
+ const out = [];
368
+ const helperAddr = opts.helper || process.env.GOVERNANCE_HELPER_ADDRESS || process.env.SAGE_GOVERNANCE_HELPER || null;
369
+ if (helperAddr && /^0x[0-9a-fA-F]{40}$/.test(helperAddr)) {
370
+ try {
371
+ const HelperABI = ['event ProposalTupleIndexed(uint256 indexed proposalId,address[] targets,uint256[] values,bytes[] calldatas,bytes32 descriptionHash)'];
372
+ const helper = new ethers.Contract(helperAddr, HelperABI, provider);
373
+ const topic = (ethers.id || ((s)=>require('ethers').id(s)))('ProposalTupleIndexed(uint256,address[],uint256[],bytes[],bytes32)');
374
+ const filter = { address: helperAddr, topics: [topic] };
375
+ const logs = await provider.getLogs({ ...filter, fromBlock, toBlock });
376
+ for (const log of logs) {
377
+ try {
378
+ const parsed = helper.interface.parseLog(log);
379
+ const id = parsed?.args?.proposalId?.toString?.() || String(parsed?.args?.[0] || '');
380
+ const GovABI = require('../utils/artifacts').resolveArtifact('contracts/cloneable/PromptGovernorCloneable.sol/PromptGovernorCloneable.json').abi;
381
+ const gov = new ethers.Contract(ctx.governor, GovABI, provider);
382
+ let state = 'unknown';
383
+ try { state = String(await gov.state(BigInt(id))); } catch (_) {}
384
+ out.push({ id, state, title: '(indexed)', description: '' });
385
+ } catch (_) {}
386
+ }
387
+ } catch (e) {
388
+ console.log(`⚠️ Helper scan failed: ${e.message}. Falling back to Governor logs.`);
389
+ }
390
+ }
391
+ if (!out.length) {
392
+ const GovABI = require('../utils/artifacts').resolveArtifact('contracts/cloneable/PromptGovernorCloneable.sol/PromptGovernorCloneable.json').abi;
393
+ const gov = new ethers.Contract(ctx.governor, GovABI, provider);
394
+ const filter = gov.filters?.ProposalCreated ? gov.filters.ProposalCreated() : null;
395
+ if (filter) {
396
+ const events = await gov.queryFilter(filter, fromBlock, toBlock);
397
+ for (const ev of events) {
398
+ try {
399
+ const parsed = ev.args || {};
400
+ const id = parsed.proposalId?.toString?.() || parsed[0]?.toString?.() || String(ev.topics?.[1] || '');
401
+ const desc = parsed.description || '';
402
+ const title = (desc.split('\n')[0] || '').slice(0, 80);
403
+ let state = 'unknown';
404
+ try { state = String(await gov.state(BigInt(id))); } catch (_) {}
405
+ out.push({ id, state, title, description: desc });
406
+ } catch (_) {}
407
+ }
408
+ }
409
+ }
410
+
411
+ if (opts.json) {
412
+ console.log(JSON.stringify({ version: '1.0', results: out, nextAction: out.length ? 'vote' : 'none' }, null, 2));
413
+ } else {
414
+ if (!out.length) {
415
+ console.log('No active proposals found.');
416
+ console.log('\n💡 Next: Create a proposal with:');
417
+ console.log(' sage library push <manifest.json> --subdao <subdao-address>');
418
+ return;
419
+ }
420
+ console.log(`Found ${out.length} proposal(s):`);
421
+ out.forEach((r, idx) => {
422
+ const stateName = { '0': 'Pending', '1': 'Active', '2': 'Canceled', '3': 'Defeated', '4': 'Succeeded', '5': 'Queued', '6': 'Expired', '7': 'Executed' }[r.state] || r.state;
423
+ console.log(`\n${idx + 1}. #${r.id} [${stateName}] ${r.title || '(no title)'}`);
424
+ });
425
+
426
+ // Recommend next action
427
+ const active = out.filter(r => r.state === '1');
428
+ const succeeded = out.filter(r => r.state === '4');
429
+ const queued = out.filter(r => r.state === '5');
430
+
431
+ console.log('\n📋 Recommended next actions:');
432
+ if (active.length) {
433
+ const first = active[0];
434
+ const subdao = ctx.subdao || opts.subdao;
435
+ if (subdao) {
436
+ console.log(` sage proposals vote ${first.id} for --subdao ${subdao}`);
437
+ } else {
438
+ console.log(` sage proposals vote ${first.id} for`);
439
+ }
440
+ } else if (succeeded.length) {
441
+ const first = succeeded[0];
442
+ const subdao = ctx.subdao || opts.subdao;
443
+ if (subdao) {
444
+ console.log(` sage proposals execute ${first.id} --subdao ${subdao}`);
445
+ } else {
446
+ console.log(` sage proposals execute ${first.id}`);
447
+ }
448
+ } else if (queued.length) {
449
+ const first = queued[0];
450
+ const subdao = ctx.subdao || opts.subdao;
451
+ if (subdao) {
452
+ console.log(` sage proposals execute ${first.id} --subdao ${subdao}`);
453
+ } else {
454
+ console.log(` sage proposals execute ${first.id}`);
455
+ }
456
+ } else {
457
+ console.log(' No actionable proposals at this time.');
458
+ }
459
+ }
460
+ } catch (e) {
461
+ const { handleCLIError } = require('../utils/error-handler');
462
+ handleCLIError('proposals:status', e, { context: { gov: opts.gov, subdao: opts.subdao } });
463
+ }
464
+ });
465
+
296
466
  program.addCommand(cmd);
297
467
  }
298
468
 
@@ -422,7 +422,7 @@ function register(program) {
422
422
  .option('--dry-run', 'Skip IPFS upload and transaction generation')
423
423
  .action(async (key, opts) => {
424
424
  try {
425
- const chalk = require('chalk'); // Assuming chalk is available
425
+ const fs = require('fs');
426
426
  const { diagnoseSubDAO } = require('../services/governance/doctor');
427
427
  const WalletManager = require('../wallet-manager');
428
428
  const { readWorkspace } = require('../services/prompts/workspace');
@@ -435,29 +435,29 @@ function register(program) {
435
435
  const signer = wallet.getSigner();
436
436
 
437
437
  // 2. Doctor Preflight
438
- console.log(chalk.blue('🩺 Running Governance Doctor...'));
438
+ console.log('🩺 Running Governance Doctor...');
439
439
  const doctor = await diagnoseSubDAO({ subdao: opts.subdao, provider, signer });
440
440
 
441
441
  if (doctor.recommendations.length > 0) {
442
- console.log(chalk.yellow('⚠️ Issues detected:'));
442
+ console.log('⚠️ Issues detected:');
443
443
  doctor.recommendations.forEach(r => {
444
444
  const msg = typeof r==='string' ? r : r.msg;
445
445
  const fix = typeof r==='string' ? '' : (r.fix || r.fixCmd);
446
446
  console.log(` - ${msg} ${fix ? `(Fix: ${fix})` : ''}`);
447
447
  });
448
- if (!opts.force) {
449
- console.error(chalk.red('❌ Aborting publish due to governance issues. Use --force to ignore.'));
448
+ if (!opts.force && !process.env.SAGE_FORCE) {
449
+ console.error('❌ Aborting publish due to governance issues. Use --force to ignore.');
450
450
  process.exit(1);
451
451
  }
452
452
  } else {
453
- console.log(chalk.green('✅ Governance checks passed.'));
453
+ console.log('✅ Governance checks passed.');
454
454
  }
455
455
 
456
456
  const subdaoAddr = doctor.subdao;
457
457
  if (!subdaoAddr) throw new Error('SubDAO not resolved.');
458
458
 
459
459
  // 3. Build Manifest
460
- console.log(chalk.blue('📦 Building manifest...'));
460
+ console.log('📦 Building manifest...');
461
461
  const ws = readWorkspace() || {};
462
462
  const skills = findWorkspaceSkills({ promptsDir: ws.promptsDir || 'prompts' });
463
463
  // Filter if key provided? "sage skills publish my-skill" usually implies updating just that one?
@@ -486,25 +486,25 @@ function register(program) {
486
486
  }
487
487
 
488
488
  // 4. IPFS Upload
489
- console.log(chalk.blue('☁️ Uploading to IPFS...'));
489
+ console.log('☁️ Uploading to IPFS...');
490
490
  // Use IPFS Manager or Worker directly?
491
491
  // Try to require IpfsManager
492
492
  let cid;
493
493
  try {
494
494
  const IpfsManager = require('../ipfs-manager');
495
495
  const ipfs = new IpfsManager();
496
- // ipfs.upload(json) returns { cid }
497
- const res = await ipfs.upload(manifest);
498
- cid = res.cid;
496
+ // ipfs.uploadJson(json) returns CID string directly
497
+ cid = await ipfs.uploadJson(manifest, 'skills-manifest');
498
+ if (!cid) throw new Error('IPFS upload returned no CID');
499
499
  } catch (e) {
500
500
  // Fallback or error
501
501
  throw new Error(`IPFS upload failed: ${e.message}`);
502
502
  }
503
503
 
504
- console.log(chalk.green(`✅ Manifest uploaded: ${cid}`));
504
+ console.log(`✅ Manifest uploaded: ${cid}`);
505
505
 
506
506
  // 5. Generate Transaction Payload
507
- console.log(chalk.blue('🚀 Generating Governance Payload...'));
507
+ console.log('🚀 Generating Governance Payload...');
508
508
 
509
509
  // We need to call updateLibrary(cid) on the Registry or SubDAO?
510
510
  // SubDAOs usually have a `updateLibrary` or `publishManifest` method?
@@ -516,20 +516,32 @@ function register(program) {
516
516
 
517
517
  const RegistryABI = require('../utils/factory-abi'); // Wait, need LibraryRegistry ABI
518
518
  // We can construct Interface manually for updateLibrary
519
- // function updateLibrary(address subdao, string calldata manifestCid)
519
+ // function updateLibraryForSubDAO(address subdao, string calldata manifestCid)
520
520
  const { ethers } = require('ethers');
521
- const iface = new ethers.Interface(['function updateLibrary(address subdao, string manifestCid)']);
522
- const calldata = iface.encodeFunctionData('updateLibrary', [subdaoAddr, cid]);
523
- const target = process.env.LIBRARY_REGISTRY_ADDRESS;
521
+ const iface = new ethers.Interface(['function updateLibraryForSubDAO(address subdao, string manifestCid)']);
522
+ const calldata = iface.encodeFunctionData('updateLibraryForSubDAO', [subdaoAddr, cid]);
523
+ const config = require('../config');
524
+ // Resolve from env first, then active profile addresses (aliases supported)
525
+ const resolvedFromConfig = (() => {
526
+ try { return config.resolveAddress('LIBRARY_REGISTRY_ADDRESS', null); } catch (_) { return null; }
527
+ })();
528
+ const target = process.env.LIBRARY_REGISTRY_ADDRESS || resolvedFromConfig;
529
+ if (!process.env.LIBRARY_REGISTRY_ADDRESS && resolvedFromConfig) {
530
+ try {
531
+ const profiles = config.readProfiles?.() || {};
532
+ const active = profiles.activeProfile || 'default';
533
+ console.log(`ℹ️ Using LIBRARY_REGISTRY_ADDRESS from profile '${active}': ${target}`);
534
+ } catch (_) {}
535
+ }
524
536
  // We need to ensure target is known. Doctor found it?
525
537
  // doctor.registry might be PromptRegistry, NOT LibraryRegistry.
526
538
  // We need LIBRARY_REGISTRY_ADDRESS.
527
- if (!target) throw new Error('LIBRARY_REGISTRY_ADDRESS not set.');
539
+ if (!target) throw new Error('LIBRARY_REGISTRY_ADDRESS not set in env or active profile. Set env: export LIBRARY_REGISTRY_ADDRESS=0x... or run: sage config addresses (and add LIBRARY_REGISTRY_ADDRESS).');
528
540
 
529
541
  // 6. Route based on Mode
530
542
  if (doctor.mode === 1) {
531
543
  // Community -> Tally
532
- console.log(chalk.cyan('\n🗳️ Community Governance Detected (Token Voting)'));
544
+ console.log('\n🗳️ Community Governance Detected (Token Voting)');
533
545
  // Generate Tally URL
534
546
  // Tally URL format: https://www.tally.xyz/gov/[slug]/proposal/create? ...
535
547
  // We need the DAO slug or address. Tally uses address or slug.
@@ -549,7 +561,7 @@ function register(program) {
549
561
  // Check if we are owner/admin
550
562
  // If Squad (Multisig), output Safe JSON.
551
563
 
552
- console.log(chalk.cyan('\n🛡️ Team/Creator Governance Detected'));
564
+ console.log('\n🛡️ Team/Creator Governance Detected');
553
565
 
554
566
  // Output Safe JSON
555
567
  const safeBatch = {
@@ -568,7 +580,13 @@ function register(program) {
568
580
  const filename = `safe-tx-${Date.now()}.json`;
569
581
  fs.writeFileSync(filename, JSON.stringify(safeBatch, null, 2));
570
582
  console.log(`✅ Safe Transaction Builder JSON saved to ${filename}`);
571
- console.log('Upload this file to your Safe to execute the update.');
583
+ console.log('\n📋 Next steps:');
584
+ console.log(` 1. Import ${filename} in Safe Transaction Builder`);
585
+ console.log(` 2. Review and sign the transaction`);
586
+ console.log(` 3. Execute the transaction`);
587
+ if (subdaoAddr) {
588
+ console.log(` 4. Verify: sage library status --subdao ${subdaoAddr}`);
589
+ }
572
590
  }
573
591
 
574
592
  } catch (e) {
@@ -577,6 +595,142 @@ function register(program) {
577
595
  }
578
596
  });
579
597
 
598
+ // QoL: publish-manifest (pin only, no governance)
599
+ cmd
600
+ .command('publish-manifest')
601
+ .description('Build and pin a manifest from workspace skills without proposing')
602
+ .option('--json', 'Output CID as JSON')
603
+ .action(async (opts) => {
604
+ try {
605
+ const { readWorkspace } = require('../services/prompts/workspace');
606
+ const { findWorkspaceSkills } = require('../services/skills/discovery');
607
+ const IpfsManager = require('../ipfs-manager');
608
+ const fs = require('fs');
609
+
610
+ console.log('📦 Building manifest...');
611
+ const ws = readWorkspace() || {};
612
+ const skills = findWorkspaceSkills({ promptsDir: ws.promptsDir || 'prompts' });
613
+ if (skills.length === 0) throw new Error('No skills found in workspace');
614
+ const manifest = {
615
+ version: '1.0.0',
616
+ timestamp: new Date().toISOString(),
617
+ skills: skills.map((s) => ({ id: s.key, metadata: s.meta, content: fs.readFileSync(s.path, 'utf8') })),
618
+ };
619
+
620
+ console.log('☁️ Uploading to IPFS...');
621
+ const ipfs = new IpfsManager();
622
+ const { cid } = await ipfs.uploadJson(manifest, 'skills-manifest');
623
+ if (opts.json) {
624
+ console.log(JSON.stringify({ ok: true, cid }, null, 2));
625
+ } else {
626
+ console.log(`✅ Manifest CID: ${cid}`);
627
+ }
628
+ } catch (e) {
629
+ console.error('❌ publish-manifest failed:', e.message);
630
+ process.exit(1);
631
+ }
632
+ });
633
+
634
+ // QoL: doctor wrapper for skills/governance readiness
635
+ cmd
636
+ .command('doctor')
637
+ .description('Validate workspace skills and diagnose SubDAO governance readiness')
638
+ .argument('[key]', 'Optional skill key to validate frontmatter')
639
+ .option('--subdao <address>', 'SubDAO address for governance checks')
640
+ .action(async (key, opts) => {
641
+ try {
642
+ const { readWorkspace } = require('../services/prompts/workspace');
643
+ const { resolveSkillFileByKey, readFrontmatter, findWorkspaceSkills } = require('../services/skills/discovery');
644
+ const { diagnoseSubDAO } = require('../services/governance/doctor');
645
+ const WalletManager = require('../wallet-manager');
646
+
647
+ // Validate skill frontmatter if requested
648
+ if (key) {
649
+ const ws = readWorkspace() || {};
650
+ const resolved = resolveSkillFileByKey({ promptsDir: ws.promptsDir || 'prompts', key });
651
+ if (!resolved) throw new Error(`Skill not found for key '${key}'`);
652
+ const fm = readFrontmatter(resolved.path);
653
+ console.log(`✅ Skill frontmatter OK for ${key}`);
654
+ console.log(fm);
655
+ } else {
656
+ // List count as a quick sanity
657
+ const ws = readWorkspace() || {};
658
+ const list = findWorkspaceSkills({ promptsDir: ws.promptsDir || 'prompts' });
659
+ console.log(`✅ Workspace skills found: ${list.length}`);
660
+ }
661
+
662
+ if (opts.subdao) {
663
+ const wallet = new WalletManager();
664
+ await wallet.connect();
665
+ const provider = wallet.getProvider();
666
+ const signer = wallet.getSigner();
667
+ console.log('🩺 Running Governance Doctor...');
668
+ const doctor = await diagnoseSubDAO({ subdao: opts.subdao, provider, signer });
669
+ console.log(JSON.stringify({
670
+ status: doctor.recommendations.length ? 'warn' : 'ok',
671
+ canPropose: doctor.canPropose === true,
672
+ subdao: doctor.subdao,
673
+ issues: doctor.recommendations,
674
+ }, null, 2));
675
+ }
676
+ } catch (e) {
677
+ console.error('❌ doctor failed:', e.message);
678
+ process.exit(1);
679
+ }
680
+ });
681
+
682
+ // QoL: use (MCP helper)
683
+ cmd
684
+ .command('use')
685
+ .description('Help use a skill with MCP (prints config; optional server start)')
686
+ .argument('<key>', 'Skill key (e.g., skills/my-skill)')
687
+ .option('--server-start', 'Start MCP HTTP server')
688
+ .option('--stdio-config', 'Print Claude Desktop stdio config snippet instead')
689
+ .action(async (key, opts) => {
690
+ try {
691
+ const path = require('path');
692
+ const chalk = require('chalk');
693
+ const { readWorkspace } = require('../services/prompts/workspace');
694
+ const { resolveSkillFileByKey } = require('../services/skills/discovery');
695
+
696
+ const ws = readWorkspace() || {};
697
+ const resolved = resolveSkillFileByKey({ promptsDir: ws.promptsDir || 'prompts', key });
698
+ if (!resolved) throw new Error(`Skill not found for key '${key}'`);
699
+
700
+ if (opts.stdioConfig) {
701
+ const stdioPath = path.resolve(__dirname, '..', 'mcp-server-stdio.js');
702
+ const snippet = {
703
+ mcpServers: {
704
+ sage: {
705
+ command: process.execPath,
706
+ args: [stdioPath],
707
+ env: {},
708
+ }
709
+ }
710
+ };
711
+ console.log(JSON.stringify(snippet, null, 2));
712
+ return;
713
+ }
714
+
715
+ if (opts.serverStart) {
716
+ const MCPServer = require('../mcp-server');
717
+ const server = new MCPServer({ port: 3333 });
718
+ await server.start();
719
+ console.log(chalk.green('✅ MCP server started on http://localhost:3333'));
720
+ console.log(chalk.cyan('Use in Cursor/Claude by configuring the HTTP/WS MCP server.'));
721
+ console.log(chalk.gray('Press Ctrl+C to stop.'));
722
+ } else {
723
+ console.log(chalk.cyan('To start the MCP server, run:'));
724
+ console.log(' sage mcp start');
725
+ console.log(chalk.cyan('For Claude stdio config, run:'));
726
+ console.log(' sage skills use', key, '--stdio-config');
727
+ }
728
+ } catch (e) {
729
+ console.error('❌ use failed:', e.message);
730
+ process.exit(1);
731
+ }
732
+ });
733
+
580
734
  program.addCommand(cmd);
581
735
  }
582
736
 
@@ -209,14 +209,19 @@ function register(program) {
209
209
  .addHelpText('afterAll', '\nGuided launch walkthrough: sage help workflow subdao-launch\n')
210
210
  .addHelpText('after', `\nExamples:\n $ sage subdao save 0xAbc... --alias team-alpha\n $ sage subdao pick\n $ sage subdao use team-alpha\n $ sage subdao info team-alpha --json\n\nTemplates:\n 1. rapid-iteration - Fast community voting (testnet)\n 2. standard-secure - Balanced community governance\n 3. team - Safe multisig for small teams (was: operator)\n`)
211
211
  .addCommand(
212
- new Command('create')
212
+ new Command('create-playbook')
213
213
  .description('Create a new SubDAO using a governance playbook (Plan/Apply)')
214
- .option('--playbook <id>', 'Playbook ID (creator, squad, community)')
214
+ .option('--playbook <id>', 'Playbook ID (creator, squad, community, community-long)')
215
215
  .option('--name <name>', 'SubDAO name')
216
216
  .option('--description <desc>', 'SubDAO description')
217
217
  .option('--owners <addresses>', 'Safe owners (comma-separated)')
218
218
  .option('--threshold <n>', 'Safe threshold')
219
219
  .option('--min-stake <amount>', 'Minimum stake amount (SXXX)')
220
+ .option('--voting-period <duration>', 'Voting period (e.g., "3 days", "36 hours", "720 minutes")')
221
+ .option('--quorum-bps <bps>', 'Quorum basis points (e.g., 200 for 2%)')
222
+ .option('--proposal-threshold <amount>', 'Proposal threshold (wei or token units as string)')
223
+ .option('--block-time-sec <n>', 'Override block time (seconds per block); auto-detected when omitted')
224
+ .option('--wizard', 'Prompt for governance parameters interactively')
220
225
  .option('--yes', 'Skip confirmation')
221
226
  .option('--dry-run', 'Generate plan only (do not execute)')
222
227
  .option('--apply <file>', 'Apply a previously generated plan file')
@@ -228,6 +233,21 @@ function register(program) {
228
233
  return;
229
234
  }
230
235
 
236
+ // Optional wizard for governance parameters
237
+ if (opts.wizard) {
238
+ const inquirer = (require('inquirer').default || require('inquirer'));
239
+ const answers = await inquirer.prompt([
240
+ { type: 'input', name: 'votingPeriod', message: 'Voting period (e.g., 7 days)', default: opts.votingPeriod || '' },
241
+ { type: 'input', name: 'quorumBps', message: 'Quorum (bps, e.g., 200 for 2%)', default: opts.quorumBps || '' },
242
+ { type: 'input', name: 'proposalThreshold', message: 'Proposal threshold (string/wei)', default: opts.proposalThreshold || '' },
243
+ { type: 'input', name: 'blockTimeSec', message: 'Seconds per block (blank = auto-detect)', default: opts.blockTimeSec || '' },
244
+ ]);
245
+ opts.votingPeriod = answers.votingPeriod || opts.votingPeriod;
246
+ opts.quorumBps = answers.quorumBps || opts.quorumBps;
247
+ opts.proposalThreshold = answers.proposalThreshold || opts.proposalThreshold;
248
+ opts.blockTimeSec = answers.blockTimeSec || opts.blockTimeSec;
249
+ }
250
+
231
251
  const { planSubDAO } = require('../services/subdao/planner');
232
252
  const plan = await planSubDAO(opts);
233
253
 
@@ -43,5 +43,20 @@
43
43
  "roles": {
44
44
  "description": "Token holders propose/vote. Timelock executes."
45
45
  }
46
+ },
47
+ {
48
+ "id": "community-long",
49
+ "name": "Community Playbook (Long Voting)",
50
+ "version": "1.0.0",
51
+ "description": "Lower quorum (2%) with a longer voting window (7 days) for broader participation.",
52
+ "governance": "token",
53
+ "params": {
54
+ "proposalThreshold": "10000",
55
+ "votingPeriod": "7 days",
56
+ "quorumBps": 200
57
+ },
58
+ "roles": {
59
+ "description": "Token holders propose/vote. Timelock executes. Lower quorum, longer window."
60
+ }
46
61
  }
47
62
  ]
@@ -1283,9 +1283,14 @@ class GovernanceManager {
1283
1283
  }
1284
1284
  }
1285
1285
 
1286
- async executeProposal(id) {
1286
+ async executeProposal(id, opts = {}) {
1287
1287
  try {
1288
- console.log(colors.blue('⚡ Executing proposal...'));
1288
+ const { simulate } = opts;
1289
+ if (simulate) {
1290
+ console.log(colors.blue('🔮 Simulating proposal execution...'));
1291
+ } else {
1292
+ console.log(colors.blue('⚡ Executing proposal...'));
1293
+ }
1289
1294
  console.log('Proposal ID:', id);
1290
1295
 
1291
1296
  // Convert proposalId to BigInt for consistency
@@ -1309,9 +1314,9 @@ class GovernanceManager {
1309
1314
  throw new Error(`Governor contract at ${governorAddr} is not deployed or has no code. This may be a network/fork issue.`);
1310
1315
  }
1311
1316
 
1312
- // Optional wait until ETA is reached
1317
+ // Optional wait until ETA is reached (only if not simulating)
1313
1318
  try {
1314
- if (process.env.SAGE_EXEC_WAIT === '1') {
1319
+ if (!simulate && process.env.SAGE_EXEC_WAIT === '1') {
1315
1320
  const eta = await this.governor.proposalEta(this.normalizeProposalId(proposalId)).catch(() => 0n);
1316
1321
  if (eta && eta > 0n) {
1317
1322
  const now = BigInt(Math.floor(Date.now() / 1000));
@@ -1361,6 +1366,21 @@ class GovernanceManager {
1361
1366
  if (eta && eta > 0n) console.log(`🔍 Debug: ETA=${eta.toString()}`);
1362
1367
  console.log(`🔍 Debug: Proposal state: ${nm} (${st})`);
1363
1368
  } catch {}
1369
+
1370
+ if (simulate) {
1371
+ // Simulation via call
1372
+ try {
1373
+ await this.governor.execute.staticCall(targets, values, calldatas, descriptionHash);
1374
+ console.log('✅ Simulation successful: Proposal would execute without reverting.');
1375
+ return;
1376
+ } catch (err) {
1377
+ const { decodeContractError } = require('./utils/contract-error-decoder');
1378
+ const reason = decodeContractError(err, [this.governor, this.timelock]);
1379
+ console.error('❌ Simulation failed:', reason);
1380
+ throw new Error(`Simulation failed: ${reason}`);
1381
+ }
1382
+ }
1383
+
1364
1384
  try {
1365
1385
  const tx = await this.governor.execute(targets, values, calldatas, descriptionHash);
1366
1386
  console.log('⏳ Waiting for transaction confirmation...');
@@ -1393,6 +1413,7 @@ class GovernanceManager {
1393
1413
  }
1394
1414
  }
1395
1415
  } catch (e) {
1416
+ if (simulate && e.message.includes('Simulation failed')) throw e;
1396
1417
  console.log('⚠️ Cache fast-path failed, falling back to log query:', e.message);
1397
1418
  }
1398
1419