@sage-protocol/cli 0.2.9 → 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 (42) hide show
  1. package/dist/cli/commands/config.js +28 -0
  2. package/dist/cli/commands/doctor.js +87 -8
  3. package/dist/cli/commands/gov-config.js +81 -0
  4. package/dist/cli/commands/governance.js +152 -72
  5. package/dist/cli/commands/library.js +9 -0
  6. package/dist/cli/commands/proposals.js +187 -17
  7. package/dist/cli/commands/skills.js +737 -0
  8. package/dist/cli/commands/subdao.js +96 -132
  9. package/dist/cli/config/playbooks.json +62 -0
  10. package/dist/cli/config.js +15 -0
  11. package/dist/cli/governance-manager.js +25 -4
  12. package/dist/cli/index.js +6 -7
  13. package/dist/cli/library-manager.js +79 -0
  14. package/dist/cli/mcp-server-stdio.js +1387 -166
  15. package/dist/cli/schemas/manifest.schema.json +55 -0
  16. package/dist/cli/services/doctor/fixers.js +134 -0
  17. package/dist/cli/services/governance/doctor.js +140 -0
  18. package/dist/cli/services/governance/playbooks.js +97 -0
  19. package/dist/cli/services/mcp/bulk-operations.js +272 -0
  20. package/dist/cli/services/mcp/dependency-analyzer.js +202 -0
  21. package/dist/cli/services/mcp/library-listing.js +2 -2
  22. package/dist/cli/services/mcp/local-prompt-collector.js +1 -0
  23. package/dist/cli/services/mcp/manifest-downloader.js +5 -3
  24. package/dist/cli/services/mcp/manifest-fetcher.js +17 -1
  25. package/dist/cli/services/mcp/manifest-workflows.js +127 -15
  26. package/dist/cli/services/mcp/quick-start.js +287 -0
  27. package/dist/cli/services/mcp/stdio-runner.js +30 -5
  28. package/dist/cli/services/mcp/template-manager.js +156 -0
  29. package/dist/cli/services/mcp/templates/default-templates.json +84 -0
  30. package/dist/cli/services/mcp/tool-args-validator.js +56 -0
  31. package/dist/cli/services/mcp/trending-formatter.js +1 -1
  32. package/dist/cli/services/mcp/unified-prompt-search.js +2 -2
  33. package/dist/cli/services/metaprompt/designer.js +12 -5
  34. package/dist/cli/services/skills/discovery.js +99 -0
  35. package/dist/cli/services/subdao/applier.js +229 -0
  36. package/dist/cli/services/subdao/planner.js +142 -0
  37. package/dist/cli/subdao-manager.js +14 -0
  38. package/dist/cli/utils/aliases.js +28 -6
  39. package/dist/cli/utils/contract-error-decoder.js +61 -0
  40. package/dist/cli/utils/suggestions.js +25 -13
  41. package/package.json +3 -2
  42. package/src/schemas/manifest.schema.json +55 -0
@@ -208,6 +208,74 @@ function register(program) {
208
208
  .description('SubDAO management commands')
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
+ .addCommand(
212
+ new Command('create-playbook')
213
+ .description('Create a new SubDAO using a governance playbook (Plan/Apply)')
214
+ .option('--playbook <id>', 'Playbook ID (creator, squad, community, community-long)')
215
+ .option('--name <name>', 'SubDAO name')
216
+ .option('--description <desc>', 'SubDAO description')
217
+ .option('--owners <addresses>', 'Safe owners (comma-separated)')
218
+ .option('--threshold <n>', 'Safe threshold')
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')
225
+ .option('--yes', 'Skip confirmation')
226
+ .option('--dry-run', 'Generate plan only (do not execute)')
227
+ .option('--apply <file>', 'Apply a previously generated plan file')
228
+ .action(async (opts) => {
229
+ try {
230
+ if (opts.apply) {
231
+ const { applyPlan } = require('../services/subdao/applier');
232
+ await applyPlan(opts.apply);
233
+ return;
234
+ }
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
+
251
+ const { planSubDAO } = require('../services/subdao/planner');
252
+ const plan = await planSubDAO(opts);
253
+
254
+ console.log(JSON.stringify(plan, null, 2));
255
+
256
+ if (opts.dryRun) {
257
+ const fs = require('fs');
258
+ const filename = `subdao-plan-${Date.now()}.json`;
259
+ fs.writeFileSync(filename, JSON.stringify(plan, null, 2));
260
+ console.log(`\n✅ Plan saved to ${filename}`);
261
+ return;
262
+ }
263
+
264
+ const inquirer = (require('inquirer').default || require('inquirer'));
265
+ if (!opts.yes) {
266
+ const { confirm } = await inquirer.prompt([{ type: 'confirm', name: 'confirm', message: 'Apply this plan?', default: true }]);
267
+ if (!confirm) return;
268
+ }
269
+
270
+ const { applyPlan } = require('../services/subdao/applier');
271
+ await applyPlan(plan);
272
+
273
+ } catch (e) {
274
+ console.error('❌ Create failed:', e.message);
275
+ process.exit(1);
276
+ }
277
+ })
278
+ )
211
279
  .addCommand(
212
280
  new Command('create-wizard')
213
281
  .description('Interactive SubDAO creation wizard (choose governance type and access model)')
@@ -556,140 +624,36 @@ function register(program) {
556
624
  .option('--json', 'Output JSON', false)
557
625
  .action(async (opts) => {
558
626
  try {
559
- const { ethers } = require('ethers');
560
- const { resolveGovContext } = require('../utils/gov-context');
627
+ const { diagnoseSubDAO } = require('../services/governance/doctor');
628
+ const { getSignerSession } = require('../utils/cli-session');
561
629
  const { signer, provider } = await getSignerSession();
562
- const ctx = await resolveGovContext({ govOpt: undefined, subdaoOpt: opts.subdao, provider });
563
- const subdao = ctx.subdao;
564
- if (!subdao) throw new Error('SubDAO not resolved. Pass --subdao or save one in profile via `sage context use --subdao <addr>`.');
565
- const governor = ctx.governor; const timelock = ctx.timelock;
566
- if (!governor || !timelock) throw new Error('Failed to resolve governor/timelock from --subdao');
567
- const out = { subdao, governor, timelock };
568
- try {
569
- const sd = new ethers.Contract(subdao, [
570
- 'function getGovernanceMode() view returns (uint8)',
571
- 'function promptRegistry() view returns (address)'
572
- ], provider);
573
- out.mode = Number(await sd.getGovernanceMode());
574
- out.registry = await sd.promptRegistry();
575
- } catch(_){}
576
- try { const tl = new ethers.Contract(timelock, ['function getMinDelay() view returns (uint256)','function PROPOSER_ROLE() view returns (bytes32)','function EXECUTOR_ROLE() view returns (bytes32)','function DEFAULT_ADMIN_ROLE() view returns (bytes32)','function hasRole(bytes32,address) view returns (bool)'], provider);
577
- out.minDelay = String(await tl.getMinDelay().catch(()=>0n));
578
- const PROPOSER = await tl.PROPOSER_ROLE();
579
- const EXECUTOR = await tl.EXECUTOR_ROLE();
580
- const ADMIN = await tl.DEFAULT_ADMIN_ROLE();
581
- out.govIsProposer = await tl.hasRole(PROPOSER, governor).catch(()=>false);
582
- out.anyoneExec = await tl.hasRole(EXECUTOR, ethers.ZeroAddress).catch(()=>false);
583
- out.signerIsAdmin = await tl.hasRole(ADMIN, await signer.getAddress()).catch(()=>false);
584
- } catch(_){}
585
- // Prompt registry governance handover / timelock wiring
586
- try {
587
- if (out.registry) {
588
- const reg = new ethers.Contract(out.registry, [
589
- 'function adminHandoverComplete() view returns (bool)',
590
- 'function governor() view returns (address)',
591
- 'function subDAO() view returns (address)'
592
- ], provider);
593
- out.registryAdminHandoverComplete = await reg.adminHandoverComplete().catch(()=>false);
594
- out.registryGovernor = await reg.governor().catch(()=>ethers.ZeroAddress);
595
- out.registrySubDAO = await reg.subDAO().catch(()=>ethers.ZeroAddress);
596
- }
597
- } catch(_) {}
598
-
599
- // LibraryRegistry scoped roles (if configured)
600
- try {
601
- const cfg = require('../config');
602
- const libRegAddr = cfg.resolveAddress ? cfg.resolveAddress('LIBRARY_REGISTRY_ADDRESS', null) : (process.env.LIBRARY_REGISTRY_ADDRESS || null);
603
- if (libRegAddr && out.timelock) {
604
- const lib = new ethers.Contract(libRegAddr, [
605
- 'function libraryAdminRole(address subdao) view returns (bytes32)',
606
- 'function hasRole(bytes32,address) view returns (bool)'
607
- ], provider);
608
- const scopedRole = await lib.libraryAdminRole(subdao);
609
- out.libraryRegistryAddress = libRegAddr;
610
- out.libraryAdminRole = scopedRole;
611
- out.libraryTimelockHasRole = await lib.hasRole(scopedRole, out.timelock).catch(()=>false);
612
- out.libraryGovernorHasRole = await lib.hasRole(scopedRole, out.governor).catch(()=>false);
613
- }
614
- } catch(_) {}
615
-
616
- const promptsAddr = opts.prompts || process.env.PREMIUM_PROMPTS_ADDRESS;
617
- if (promptsAddr) {
618
- try {
619
- const pr = new ethers.Contract(ethers.getAddress(promptsAddr), ['function factory() view returns (address)'], provider);
620
- const factory = await pr.factory(); out.prompts = promptsAddr; out.factory = factory;
621
- if (factory && /^0x/.test(factory)) {
622
- const f = new ethers.Contract(factory, ['function isSubDAO(address) view returns (bool)'], provider);
623
- out.factoryRecognizesSubDAO = await f.isSubDAO(subdao).catch(()=>false);
624
- }
625
- } catch(_){}
626
- }
627
- if (opts.json) { console.log(JSON.stringify(out, null, 2)); return; }
628
- console.log('┌─ SubDAO Doctor ────────────────────────────┐');
629
- console.log(`│ SubDAO : ${subdao}`);
630
- console.log(`│ Governor : ${governor}`);
631
- console.log(`│ Timelock : ${timelock}`);
632
- // Effective mode label (treat non-proposer as Team-only UX)
633
- if (out.mode !== undefined) {
634
- let label = (out.mode===0?'Personal':'Community');
635
- if (out.govIsProposer === false) label = 'Team-only';
636
- console.log(`│ Mode : ${out.mode} (${label})`);
637
- }
638
- if (out.minDelay !== undefined) console.log(`│ MinDelay : ${out.minDelay}s`);
639
- if (out.govIsProposer !== undefined) console.log(`│ Gov PROPOSER on TL: ${out.govIsProposer?'yes':'no'}`);
640
- if (out.registry) console.log(`│ Registry : ${out.registry}`);
641
- if (out.registryAdminHandoverComplete !== undefined) {
642
- console.log(`│ Registry handover : ${out.registryAdminHandoverComplete ? 'complete' : 'NOT COMPLETE'}`);
643
- }
644
- if (out.libraryRegistryAddress) {
645
- console.log(`│ LibraryRegistry : ${out.libraryRegistryAddress}`);
646
- console.log(`│ TL has LIB_ADMIN : ${out.libraryTimelockHasRole ? 'yes' : 'no'}`);
647
- console.log(`│ GOV has LIB_ADMIN: ${out.libraryGovernorHasRole ? 'yes' : 'no'}`);
648
- }
649
- if (out.prompts) console.log(`│ Premiums : ${out.prompts}`);
650
- if (out.factory) console.log(`│ Factory : ${out.factory}`);
651
- if (out.factoryRecognizesSubDAO !== undefined) console.log(`│ Factory isSubDAO(SubDAO): ${out.factoryRecognizesSubDAO?'yes':'no'}`);
652
- console.log('└────────────────────────────────────────────┘');
653
- console.log('\nRecommendations:');
654
- if (!out.govIsProposer) {
655
- console.log('- Grant PROPOSER_ROLE to the Governor on the SubDAO Timelock.');
656
- if (process.env.SAGE_SHOW_CAST === '1') {
657
- console.log(' Encode calldata:');
658
- console.log(` cast calldata "grantRole(bytes32,address)" $(cast call ${timelock} "PROPOSER_ROLE()") ${governor}`);
659
- }
660
- console.log(' Schedule on timelock:');
661
- console.log(` sage timelock schedule-call --subdao ${subdao} --to ${timelock} --sig 'grantRole(bytes32,address)' --args "$(cast call ${timelock} 'PROPOSER_ROLE()'),${governor}"`);
662
- console.log(' Or run one-shot helper:');
663
- console.log(` sage governance enable-proposals --subdao ${subdao}`);
664
- }
665
- if (out.registry && out.registryAdminHandoverComplete === false) {
666
- console.log('- Complete PromptRegistry admin handover to the Timelock to avoid deployer EOAs retaining control.');
667
- }
668
- if (out.libraryRegistryAddress && !out.libraryTimelockHasRole) {
669
- console.log('- Grant the scoped library admin role to the Timelock in LibraryRegistry for this SubDAO.');
670
- }
671
- console.log('- Prefer encoded helpers to avoid hex mistakes:');
672
- console.log(' - Build data: sage timelock build-data --sig "grantRole(bytes32,address)" --args "PROPOSER_ROLE,', governor, '"');
673
- console.log(' - Execute call: sage timelock execute-call --subdao', subdao, '--to', timelock, '--sig "grantRole(bytes32,address)" --args "PROPOSER_ROLE,', governor, '" --salt 0x...');
674
- if (out.factoryRecognizesSubDAO === false) {
675
- console.log('- PremiumPrompts factory does not recognize this SubDAO. Use a factory-created SubDAO for premium registration or extend the factory with an allowlist and redeploy.');
630
+
631
+ const report = await diagnoseSubDAO({ subdao: opts.subdao, provider, signer });
632
+
633
+ if (opts.json) {
634
+ console.log(JSON.stringify(report, null, 2));
635
+ return;
676
636
  }
677
- if (opts.fix) {
678
- if (!out.signerIsAdmin) {
679
- console.log('❌ Cannot fix: signer lacks DEFAULT_ADMIN_ROLE on Timelock');
680
- } else {
681
- const tlw = new ethers.Contract(timelock, ['function PROPOSER_ROLE() view returns (bytes32)','function EXECUTOR_ROLE() view returns (bytes32)','function grantRole(bytes32,address)','function hasRole(bytes32,address) view returns (bool)'], signer);
682
- const PROPOSER = await tlw.PROPOSER_ROLE();
683
- const EXECUTOR = await tlw.EXECUTOR_ROLE();
684
- if (!out.govIsProposer) {
685
- console.log('⚙️ Granting PROPOSER_ROLE to Governor...');
686
- const tx = await tlw.grantRole(PROPOSER, governor); console.log('Tx:', tx.hash); { const { waitForReceipt } = require('../utils/tx-wait'); const { ethers } = require('ethers'); const pr = new ethers.JsonRpcProvider(process.env.RPC_URL || process.env.BASE_SEPOLIA_RPC || process.env.BASE_RPC_URL || 'https://base-sepolia.publicnode.com'); await waitForReceipt(pr, tx, Number(process.env.SAGE_TX_WAIT_MS || 60000)); } console.log('✅ Granted');
687
- } else { console.log(' Governor already has PROPOSER_ROLE'); }
688
- if (!out.anyoneExec) {
689
- console.log('⚙️ Granting EXECUTOR_ROLE to address(0)...');
690
- const tx2 = await tlw.grantRole(EXECUTOR, ethers.ZeroAddress); console.log('Tx:', tx2.hash); { const { waitForReceipt } = require('../utils/tx-wait'); const { ethers } = require('ethers'); const pr = new ethers.JsonRpcProvider(process.env.RPC_URL || process.env.BASE_SEPOLIA_RPC || process.env.BASE_RPC_URL || 'https://base-sepolia.publicnode.com'); await waitForReceipt(pr, tx2, Number(process.env.SAGE_TX_WAIT_MS || 60000)); } console.log('✅ Granted');
691
- } else { console.log('✅ address(0) already EXECUTOR'); }
692
- }
637
+
638
+ console.log('🔍 Governance Doctor Report');
639
+ console.log(`SubDAO: ${report.subdao}`);
640
+ console.log(`Mode: ${report.mode === 1 ? 'Community' : 'Personal/Team'}`);
641
+ console.log(`Governor: ${report.governor}`);
642
+ console.log(`Timelock: ${report.timelock}`);
643
+
644
+ if (report.recommendations.length === 0) {
645
+ console.log(' All checks passed.');
646
+ } else {
647
+ console.log('⚠️ Issues Found:');
648
+ report.recommendations.forEach(rec => {
649
+ console.log(` - ${rec.msg}`);
650
+ if (rec.fixCmd) console.log(` Fix Cmd: ${rec.fixCmd}`);
651
+ });
652
+
653
+ if (opts.fix) {
654
+ console.log('\n⚠️ Automatic --fix is deprecated. Please use the commands above.');
655
+ console.log(' Or for Safe/Tally payloads, rely on "sage skills publish" or "sage timelock" tools.');
656
+ }
693
657
  }
694
658
  } catch (e) {
695
659
  console.error('❌ Doctor failed:', e.message);
@@ -0,0 +1,62 @@
1
+ [
2
+ {
3
+ "id": "creator",
4
+ "name": "Creator Playbook (Solo)",
5
+ "version": "1.0.0",
6
+ "description": "For publishing your own work. Direct control.",
7
+ "governance": "operator",
8
+ "params": {
9
+ "proposalThreshold": "0",
10
+ "votingPeriod": "0",
11
+ "quorumBps": 0
12
+ },
13
+ "roles": {
14
+ "description": "Owner has all roles. No voting."
15
+ }
16
+ },
17
+ {
18
+ "id": "squad",
19
+ "name": "Squad Playbook (Small Team)",
20
+ "version": "1.0.0",
21
+ "description": "Collaborate with a small team via a Safe multisig.",
22
+ "governance": "operator",
23
+ "params": {
24
+ "proposalThreshold": "0",
25
+ "votingPeriod": "0",
26
+ "quorumBps": 0
27
+ },
28
+ "roles": {
29
+ "description": "Safe multisig holds Admin/Proposer roles."
30
+ }
31
+ },
32
+ {
33
+ "id": "community",
34
+ "name": "Community Playbook (Decentralized)",
35
+ "version": "1.0.0",
36
+ "description": "Token-based voting governance.",
37
+ "governance": "token",
38
+ "params": {
39
+ "proposalThreshold": "10000",
40
+ "votingPeriod": "3 days",
41
+ "quorumBps": 400
42
+ },
43
+ "roles": {
44
+ "description": "Token holders propose/vote. Timelock executes."
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
+ }
61
+ }
62
+ ]
@@ -322,6 +322,10 @@ function createLocalConfig() {
322
322
  if (ipfs.warmGateway !== undefined && !process.env.SAGE_IPFS_WARM) {
323
323
  process.env.SAGE_IPFS_WARM = String(!!ipfs.warmGateway);
324
324
  }
325
+ const git = profile.git || {};
326
+ if (git.githubToken && !process.env.GITHUB_TOKEN && !process.env.GH_TOKEN) {
327
+ process.env.GITHUB_TOKEN = git.githubToken;
328
+ }
325
329
  }
326
330
  if (!isQuiet()) console.log(`🧭 Loaded config profile: ${active}`);
327
331
  } catch (e) {
@@ -410,6 +414,17 @@ function createLocalConfig() {
410
414
  this.writeProfileSettings({ ipfs: settings }, options);
411
415
  },
412
416
 
417
+ readGitConfig() {
418
+ const profiles = this.readProfiles();
419
+ const active = profiles.activeProfile || 'default';
420
+ const profile = profiles.profiles?.[active] || {};
421
+ return mergeObjects({}, profile.git || {});
422
+ },
423
+
424
+ writeGitConfig(settings = {}, options = {}) {
425
+ this.writeProfileSettings({ git: settings }, options);
426
+ },
427
+
413
428
  getActiveProfile(options = {}) {
414
429
  const profiles = this.readProfiles();
415
430
  const profileName = options.profile || profiles.activeProfile || 'default';
@@ -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
 
package/dist/cli/index.js CHANGED
@@ -42,7 +42,7 @@ try {
42
42
  };
43
43
  const getArg = (name) => {
44
44
  const i = process.argv.indexOf(name);
45
- return i >= 0 && process.argv[i+1] && !process.argv[i+1].startsWith('-') ? process.argv[i+1] : null;
45
+ return i >= 0 && process.argv[i + 1] && !process.argv[i + 1].startsWith('-') ? process.argv[i + 1] : null;
46
46
  };
47
47
  const netArg = getArg('--net') || process.env.SAGE_NET || null;
48
48
  if (netArg) {
@@ -86,10 +86,10 @@ program
86
86
 
87
87
  // Reordered to highlight consolidated namespaces ('prompts' and 'gov')
88
88
  const commandGroups = [
89
- { title: 'Content (new)', modules: ['prompts', 'prompt', 'library', 'premium', 'premium-pre', 'creator', 'contributor', 'bounty'] },
89
+ { title: 'Content (new)', modules: ['skills', 'prompts', 'prompt', 'library', 'premium', 'premium-pre', 'creator', 'contributor', 'bounty'] },
90
90
  { title: 'Governance (new)', modules: ['proposals', 'governance', 'subdao', 'timelock', 'roles', 'members', 'gov-config', 'stake-status'] },
91
91
  { title: 'Treasury', modules: ['treasury', 'safe', 'boost'] },
92
- { title: 'Ops & Utilities', modules: ['config', 'wallet', 'sxxx', 'ipfs', 'ipns', 'context', 'doctor', 'factory', 'upgrade', 'resolve', 'dry-run-queue', 'mcp', 'start', 'wizard', 'init', 'subdao-config', 'pin', 'council', 'sbt', 'completion', 'help', 'hook', 'ipns'] },
92
+ { title: 'Ops & Utilities', modules: ['config', 'wallet', 'sxxx', 'ipfs', 'ipns', 'context', 'doctor', 'factory', 'upgrade', 'resolve', 'dry-run-queue', 'mcp', 'start', 'wizard', 'init', 'subdao-config', 'pin', 'council', 'sbt', 'completion', 'help', 'hook'] },
93
93
  { title: 'Developer', modules: ['dev', 'registry'] }
94
94
  ];
95
95
 
@@ -116,7 +116,7 @@ program.addHelpText('beforeAll', () => {
116
116
  if (!hasProjectConfig && !hasWorkspace) {
117
117
  firstRun = '\nTip: first time here? Try:\n - sage wizard\n - or: sage ipfs setup && sage prompts init\n';
118
118
  }
119
- } catch (_) {}
119
+ } catch (_) { }
120
120
  return `Top workflows: ${highlights}\n${lines}\n${note}${firstRun}\n`;
121
121
  });
122
122
 
@@ -148,7 +148,6 @@ const additionalModules = [
148
148
  'completion',
149
149
  'help',
150
150
  'hook'
151
- , 'ipns'
152
151
  ];
153
152
  additionalModules.forEach((mod) => moduleSet.add(mod));
154
153
 
@@ -168,7 +167,7 @@ if (cliTestMode) {
168
167
 
169
168
  program.configureOutput({
170
169
  writeOut: (str) => process.stdout.write(str),
171
- writeErr: () => {},
170
+ writeErr: () => { },
172
171
  outputError: (str, write) => {
173
172
  if (!aliasSupport.handleOutputError(str)) {
174
173
  write(str);
@@ -202,7 +201,7 @@ process.on('exit', () => {
202
201
  process.stderr.write = originalStderrWrite;
203
202
  aliasSupport.setStderrWriter(originalStderrWrite);
204
203
  if (global.walletManager) {
205
- global.walletManager.disconnect().catch(() => {});
204
+ global.walletManager.disconnect().catch(() => { });
206
205
  }
207
206
  });
208
207
 
@@ -79,6 +79,85 @@ class LibraryManager {
79
79
  return searchLocalManifests({ config: cliConfig, query });
80
80
  }
81
81
 
82
+ /**
83
+ * Create a new local library manifest in the pinned libraries directory.
84
+ * This is used by MCP quick-start flows for "My Library" and ad-hoc libraries.
85
+ * Returns a pseudo-CID (local_*) that can be used with the local manifest loader.
86
+ */
87
+ createLibrary(name, description = '', options = {}) {
88
+ const dir = this.ensureLibrariesDir();
89
+ const baseName = String(name || 'Library').trim() || 'Library';
90
+ const slugBase = baseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'library';
91
+ let cid = `local_${slugBase}`;
92
+ let suffix = 1;
93
+ // Ensure uniqueness within the libraries dir
94
+ while (fs.existsSync(path.join(dir, `${cid}.json`))) {
95
+ cid = `local_${slugBase}_${suffix++}`;
96
+ }
97
+
98
+ const manifest = {
99
+ version: 2,
100
+ library: {
101
+ name: baseName,
102
+ description: String(description || ''),
103
+ ...(options.metadata || {}),
104
+ },
105
+ prompts: [],
106
+ };
107
+
108
+ const manifestPath = path.join(dir, `${cid}.json`);
109
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
110
+
111
+ return { cid, path: manifestPath, manifest };
112
+ }
113
+
114
+ /**
115
+ * Pin (or re-pin) a manifest into the local libraries directory.
116
+ * Accepts either a manifest object or { manifestPath, manifestData } from callers.
117
+ * Returns { cid, path } where cid may be provided or auto-assigned as local_*.
118
+ */
119
+ pinLibrary(manifestOrOptions) {
120
+ const dir = this.ensureLibrariesDir();
121
+ let manifest = manifestOrOptions;
122
+
123
+ // Support callers that pass { manifestPath, manifestData }
124
+ if (manifestOrOptions && typeof manifestOrOptions === 'object' && (manifestOrOptions.manifest || manifestOrOptions.manifestData)) {
125
+ manifest = manifestOrOptions.manifest || manifestOrOptions.manifestData;
126
+ }
127
+
128
+ if (!manifest || typeof manifest !== 'object') {
129
+ throw new Error('pinLibrary requires a manifest object');
130
+ }
131
+
132
+ const libName = String(manifest.library?.name || 'Library').trim() || 'Library';
133
+ const slugBase = libName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'library';
134
+
135
+ let cid = manifest.cid || manifest.library?.cid || null;
136
+ if (!cid) {
137
+ cid = `local_${slugBase}`;
138
+ let suffix = 1;
139
+ while (fs.existsSync(path.join(dir, `${cid}.json`))) {
140
+ cid = `local_${slugBase}_${suffix++}`;
141
+ }
142
+ }
143
+
144
+ // Ensure manifest has expected top-level fields
145
+ if (!manifest.version) {
146
+ manifest.version = 2;
147
+ }
148
+ if (!manifest.library) {
149
+ manifest.library = { name: libName };
150
+ }
151
+ if (!Array.isArray(manifest.prompts)) {
152
+ manifest.prompts = [];
153
+ }
154
+
155
+ const manifestPath = path.join(dir, `${cid}.json`);
156
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
157
+
158
+ return { cid, path: manifestPath };
159
+ }
160
+
82
161
  // Compute CID locally from bytes using ipfs-only-hash (UnixFS DAG-PB, CIDv0 by default)
83
162
  // Compute CID locally from bytes using ipfs-only-hash (UnixFS DAG-PB, CIDv0 by default)
84
163
  async _computeCidFromBuffer(buffer) {