@sage-protocol/cli 0.2.9 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/config.js +28 -0
- package/dist/cli/commands/skills.js +583 -0
- package/dist/cli/commands/subdao.js +76 -132
- package/dist/cli/config/playbooks.json +47 -0
- package/dist/cli/config.js +15 -0
- package/dist/cli/index.js +1 -1
- package/dist/cli/mcp-server-stdio.js +14 -85
- package/dist/cli/services/governance/doctor.js +140 -0
- package/dist/cli/services/governance/playbooks.js +97 -0
- package/dist/cli/services/skills/discovery.js +99 -0
- package/dist/cli/services/subdao/applier.js +217 -0
- package/dist/cli/services/subdao/planner.js +107 -0
- package/dist/cli/utils/aliases.js +11 -4
- package/dist/cli/utils/suggestions.js +8 -1
- package/package.json +1 -1
|
@@ -208,6 +208,54 @@ 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')
|
|
213
|
+
.description('Create a new SubDAO using a governance playbook (Plan/Apply)')
|
|
214
|
+
.option('--playbook <id>', 'Playbook ID (creator, squad, community)')
|
|
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('--yes', 'Skip confirmation')
|
|
221
|
+
.option('--dry-run', 'Generate plan only (do not execute)')
|
|
222
|
+
.option('--apply <file>', 'Apply a previously generated plan file')
|
|
223
|
+
.action(async (opts) => {
|
|
224
|
+
try {
|
|
225
|
+
if (opts.apply) {
|
|
226
|
+
const { applyPlan } = require('../services/subdao/applier');
|
|
227
|
+
await applyPlan(opts.apply);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const { planSubDAO } = require('../services/subdao/planner');
|
|
232
|
+
const plan = await planSubDAO(opts);
|
|
233
|
+
|
|
234
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
235
|
+
|
|
236
|
+
if (opts.dryRun) {
|
|
237
|
+
const fs = require('fs');
|
|
238
|
+
const filename = `subdao-plan-${Date.now()}.json`;
|
|
239
|
+
fs.writeFileSync(filename, JSON.stringify(plan, null, 2));
|
|
240
|
+
console.log(`\n✅ Plan saved to ${filename}`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const inquirer = (require('inquirer').default || require('inquirer'));
|
|
245
|
+
if (!opts.yes) {
|
|
246
|
+
const { confirm } = await inquirer.prompt([{ type: 'confirm', name: 'confirm', message: 'Apply this plan?', default: true }]);
|
|
247
|
+
if (!confirm) return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const { applyPlan } = require('../services/subdao/applier');
|
|
251
|
+
await applyPlan(plan);
|
|
252
|
+
|
|
253
|
+
} catch (e) {
|
|
254
|
+
console.error('❌ Create failed:', e.message);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
)
|
|
211
259
|
.addCommand(
|
|
212
260
|
new Command('create-wizard')
|
|
213
261
|
.description('Interactive SubDAO creation wizard (choose governance type and access model)')
|
|
@@ -556,140 +604,36 @@ function register(program) {
|
|
|
556
604
|
.option('--json', 'Output JSON', false)
|
|
557
605
|
.action(async (opts) => {
|
|
558
606
|
try {
|
|
559
|
-
const {
|
|
560
|
-
const {
|
|
607
|
+
const { diagnoseSubDAO } = require('../services/governance/doctor');
|
|
608
|
+
const { getSignerSession } = require('../utils/cli-session');
|
|
561
609
|
const { signer, provider } = await getSignerSession();
|
|
562
|
-
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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.');
|
|
610
|
+
|
|
611
|
+
const report = await diagnoseSubDAO({ subdao: opts.subdao, provider, signer });
|
|
612
|
+
|
|
613
|
+
if (opts.json) {
|
|
614
|
+
console.log(JSON.stringify(report, null, 2));
|
|
615
|
+
return;
|
|
676
616
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
}
|
|
692
|
-
|
|
617
|
+
|
|
618
|
+
console.log('🔍 Governance Doctor Report');
|
|
619
|
+
console.log(`SubDAO: ${report.subdao}`);
|
|
620
|
+
console.log(`Mode: ${report.mode === 1 ? 'Community' : 'Personal/Team'}`);
|
|
621
|
+
console.log(`Governor: ${report.governor}`);
|
|
622
|
+
console.log(`Timelock: ${report.timelock}`);
|
|
623
|
+
|
|
624
|
+
if (report.recommendations.length === 0) {
|
|
625
|
+
console.log('✅ All checks passed.');
|
|
626
|
+
} else {
|
|
627
|
+
console.log('⚠️ Issues Found:');
|
|
628
|
+
report.recommendations.forEach(rec => {
|
|
629
|
+
console.log(` - ${rec.msg}`);
|
|
630
|
+
if (rec.fixCmd) console.log(` Fix Cmd: ${rec.fixCmd}`);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
if (opts.fix) {
|
|
634
|
+
console.log('\n⚠️ Automatic --fix is deprecated. Please use the commands above.');
|
|
635
|
+
console.log(' Or for Safe/Tally payloads, rely on "sage skills publish" or "sage timelock" tools.');
|
|
636
|
+
}
|
|
693
637
|
}
|
|
694
638
|
} catch (e) {
|
|
695
639
|
console.error('❌ Doctor failed:', e.message);
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
]
|
package/dist/cli/config.js
CHANGED
|
@@ -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';
|
package/dist/cli/index.js
CHANGED
|
@@ -86,7 +86,7 @@ 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
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'] },
|
|
@@ -727,89 +727,23 @@ class SageMCPServer {
|
|
|
727
727
|
* - Skill files live under promptsDir/skills/*.md
|
|
728
728
|
*/
|
|
729
729
|
listWorkspaceSkills() {
|
|
730
|
-
const skills = [];
|
|
731
730
|
try {
|
|
732
731
|
const { readWorkspace, DEFAULT_DIR } = require('./services/prompts/workspace');
|
|
732
|
+
const { findWorkspaceSkills } = require('./services/skills/discovery');
|
|
733
733
|
const ws = readWorkspace() || {};
|
|
734
734
|
const promptsDir = ws.promptsDir || DEFAULT_DIR || 'prompts';
|
|
735
|
-
const
|
|
736
|
-
if (!fs.existsSync(baseDir)) {
|
|
737
|
-
return {
|
|
738
|
-
content: [
|
|
739
|
-
{
|
|
740
|
-
type: 'text',
|
|
741
|
-
text: 'No workspace skills directory found (expected prompts/skills). Create prompts/skills/<name>.md to define skills for this repo.',
|
|
742
|
-
},
|
|
743
|
-
],
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
const walk = (dir) => {
|
|
747
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
748
|
-
for (const entry of entries) {
|
|
749
|
-
const full = path.join(dir, entry.name);
|
|
750
|
-
if (entry.isDirectory()) {
|
|
751
|
-
walk(full);
|
|
752
|
-
} else if (entry.isFile() && full.toLowerCase().endsWith('.md')) {
|
|
753
|
-
skills.push(full);
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
};
|
|
757
|
-
walk(baseDir);
|
|
758
|
-
const results = skills.map((filePath) => {
|
|
759
|
-
const relFromPrompts = path.relative(path.join(process.cwd(), promptsDir), filePath);
|
|
760
|
-
const key = relFromPrompts.replace(/\\/g, '/').replace(/\.md$/i, '');
|
|
761
|
-
let title = path.basename(filePath, '.md');
|
|
762
|
-
let summary = '';
|
|
763
|
-
let tags = [];
|
|
764
|
-
try {
|
|
765
|
-
const raw = fs.readFileSync(filePath, 'utf8');
|
|
766
|
-
if (raw.startsWith('---')) {
|
|
767
|
-
const end = raw.indexOf('\n---', 3);
|
|
768
|
-
if (end !== -1) {
|
|
769
|
-
const front = raw.slice(3, end).split(/\r?\n/);
|
|
770
|
-
for (const line of front) {
|
|
771
|
-
const idx = line.indexOf(':');
|
|
772
|
-
if (idx === -1) continue;
|
|
773
|
-
const k = line.slice(0, idx).trim().toLowerCase();
|
|
774
|
-
let v = line.slice(idx + 1).trim();
|
|
775
|
-
if (k === 'title' && v) title = v;
|
|
776
|
-
if (k === 'summary' && v) summary = v;
|
|
777
|
-
if (k === 'tags' && v) {
|
|
778
|
-
try {
|
|
779
|
-
tags = JSON.parse(v.replace(/'/g, '"'));
|
|
780
|
-
} catch (_) {
|
|
781
|
-
tags = String(v)
|
|
782
|
-
.split(/[,|\s]+/)
|
|
783
|
-
.filter(Boolean);
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
} catch (_) {
|
|
790
|
-
// ignore parse errors; fall back to filename
|
|
791
|
-
}
|
|
792
|
-
return {
|
|
793
|
-
key,
|
|
794
|
-
name: title,
|
|
795
|
-
summary,
|
|
796
|
-
tags,
|
|
797
|
-
path: filePath,
|
|
798
|
-
};
|
|
799
|
-
});
|
|
800
|
-
|
|
735
|
+
const results = findWorkspaceSkills({ promptsDir });
|
|
801
736
|
if (!results.length) {
|
|
802
737
|
return {
|
|
803
738
|
content: [
|
|
804
739
|
{
|
|
805
740
|
type: 'text',
|
|
806
|
-
text: 'No skills found
|
|
741
|
+
text: 'No skills found. Create prompts/skills/<name>.md or prompts/skills/<name>/SKILL.md to define skills for this repo.',
|
|
807
742
|
},
|
|
808
743
|
{ type: 'json', text: JSON.stringify({ skills: [] }, null, 2) },
|
|
809
744
|
],
|
|
810
745
|
};
|
|
811
746
|
}
|
|
812
|
-
|
|
813
747
|
const textLines = results
|
|
814
748
|
.map(
|
|
815
749
|
(s, idx) =>
|
|
@@ -818,7 +752,6 @@ class SageMCPServer {
|
|
|
818
752
|
}${s.tags && s.tags.length ? `\n 🔖 ${s.tags.join(', ')}` : ''}`,
|
|
819
753
|
)
|
|
820
754
|
.join('\n\n');
|
|
821
|
-
|
|
822
755
|
return {
|
|
823
756
|
content: [
|
|
824
757
|
{ type: 'text', text: `Workspace skills (${results.length})\n\n${textLines}` },
|
|
@@ -839,42 +772,38 @@ class SageMCPServer {
|
|
|
839
772
|
getWorkspaceSkill({ key }) {
|
|
840
773
|
try {
|
|
841
774
|
if (!key || !String(key).trim()) {
|
|
842
|
-
return {
|
|
843
|
-
content: [{ type: 'text', text: 'get_workspace_skill: key is required' }],
|
|
844
|
-
};
|
|
775
|
+
return { content: [{ type: 'text', text: 'get_workspace_skill: key is required' }] };
|
|
845
776
|
}
|
|
846
777
|
const { readWorkspace, DEFAULT_DIR } = require('./services/prompts/workspace');
|
|
778
|
+
const { resolveSkillFileByKey } = require('./services/skills/discovery');
|
|
847
779
|
const ws = readWorkspace() || {};
|
|
848
780
|
const promptsDir = ws.promptsDir || DEFAULT_DIR || 'prompts';
|
|
849
781
|
const safeKey = String(key).trim().replace(/^\/+/, '').replace(/\.md$/i, '');
|
|
850
|
-
const
|
|
851
|
-
if (!fs.existsSync(
|
|
782
|
+
const resolved = resolveSkillFileByKey({ promptsDir, key: safeKey });
|
|
783
|
+
if (!resolved || !fs.existsSync(resolved.path)) {
|
|
784
|
+
const expectedFlat = path.join(process.cwd(), promptsDir, `${safeKey}.md`);
|
|
785
|
+
const expectedDir = path.join(process.cwd(), promptsDir, safeKey, 'SKILL.md');
|
|
852
786
|
return {
|
|
853
787
|
content: [
|
|
854
788
|
{
|
|
855
789
|
type: 'text',
|
|
856
|
-
text: `Workspace skill not found for key '${safeKey}'. Expected
|
|
857
|
-
process.cwd(),
|
|
858
|
-
filePath,
|
|
859
|
-
)}`,
|
|
790
|
+
text: `Workspace skill not found for key '${safeKey}'. Expected at ${path.relative(process.cwd(), expectedFlat)} or ${path.relative(process.cwd(), expectedDir)}`,
|
|
860
791
|
},
|
|
861
792
|
],
|
|
862
793
|
};
|
|
863
794
|
}
|
|
864
|
-
const body = fs.readFileSync(
|
|
795
|
+
const body = fs.readFileSync(resolved.path, 'utf8');
|
|
865
796
|
return {
|
|
866
797
|
content: [
|
|
867
798
|
{
|
|
868
799
|
type: 'text',
|
|
869
|
-
text: `Loaded workspace skill '${safeKey}' from ${path.relative(process.cwd(),
|
|
800
|
+
text: `Loaded workspace skill '${safeKey}' from ${path.relative(process.cwd(), resolved.path)}.\n\n${body}`,
|
|
870
801
|
},
|
|
871
|
-
{ type: 'json', text: JSON.stringify({ key: safeKey, path:
|
|
802
|
+
{ type: 'json', text: JSON.stringify({ key: safeKey, path: resolved.path, baseDir: resolved.baseDir, body }, null, 2) },
|
|
872
803
|
],
|
|
873
804
|
};
|
|
874
805
|
} catch (error) {
|
|
875
|
-
return {
|
|
876
|
-
content: [{ type: 'text', text: `Error loading workspace skill: ${error.message}` }],
|
|
877
|
-
};
|
|
806
|
+
return { content: [{ type: 'text', text: `Error loading workspace skill: ${error.message}` }] };
|
|
878
807
|
}
|
|
879
808
|
}
|
|
880
809
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const { ethers } = require('ethers');
|
|
2
|
+
const { resolveGovContext } = require('../../utils/gov-context');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Comprehensive Governance Diagnosis
|
|
6
|
+
* @param {object} opts - { subdao, provider, signer }
|
|
7
|
+
* @returns {object} Report with recommendations and structured fix payloads
|
|
8
|
+
*/
|
|
9
|
+
async function diagnoseSubDAO({ subdao, provider, signer }) {
|
|
10
|
+
const out = {
|
|
11
|
+
subdao,
|
|
12
|
+
governor: null,
|
|
13
|
+
timelock: null,
|
|
14
|
+
mode: null,
|
|
15
|
+
checks: {},
|
|
16
|
+
recommendations: [], // { type, msg, fixCmd, fixPayload }
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// 1. Resolve Context
|
|
20
|
+
try {
|
|
21
|
+
const ctx = await resolveGovContext({ subdaoOpt: subdao, govOpt: undefined, provider });
|
|
22
|
+
out.governor = ctx.governor;
|
|
23
|
+
out.timelock = ctx.timelock;
|
|
24
|
+
out.checks.context = 'ok';
|
|
25
|
+
} catch (e) {
|
|
26
|
+
out.checks.context = 'failed';
|
|
27
|
+
out.checks.contextError = e.message;
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!out.governor || !out.timelock) {
|
|
32
|
+
out.recommendations.push({ msg: 'Could not resolve Governor or Timelock. Verify SubDAO address.' });
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 2. Detect Mode
|
|
37
|
+
try {
|
|
38
|
+
const SubABI = ['function getGovernanceMode() view returns (uint8)', 'function promptRegistry() view returns (address)'];
|
|
39
|
+
const sd = new ethers.Contract(subdao, SubABI, provider);
|
|
40
|
+
const mode = await sd.getGovernanceMode();
|
|
41
|
+
out.mode = Number(mode); // 0=Operator/Personal, 1=Community
|
|
42
|
+
out.registry = await sd.promptRegistry();
|
|
43
|
+
} catch (e) {
|
|
44
|
+
out.checks.mode = 'failed';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 3. Roles Check (Timelock)
|
|
48
|
+
try {
|
|
49
|
+
const tl = new ethers.Contract(out.timelock, [
|
|
50
|
+
'function PROPOSER_ROLE() view returns (bytes32)',
|
|
51
|
+
'function EXECUTOR_ROLE() view returns (bytes32)',
|
|
52
|
+
'function hasRole(bytes32,address) view returns (bool)',
|
|
53
|
+
'function grantRole(bytes32,address)'
|
|
54
|
+
], provider);
|
|
55
|
+
|
|
56
|
+
const PROPOSER = await tl.PROPOSER_ROLE();
|
|
57
|
+
const EXECUTOR = await tl.EXECUTOR_ROLE();
|
|
58
|
+
|
|
59
|
+
out.govIsProposer = await tl.hasRole(PROPOSER, out.governor);
|
|
60
|
+
out.anyoneExec = await tl.hasRole(EXECUTOR, ethers.ZeroAddress);
|
|
61
|
+
|
|
62
|
+
if (!out.govIsProposer) {
|
|
63
|
+
const iface = tl.interface;
|
|
64
|
+
const calldata = iface.encodeFunctionData('grantRole', [PROPOSER, out.governor]);
|
|
65
|
+
out.recommendations.push({
|
|
66
|
+
type: 'role_missing',
|
|
67
|
+
msg: 'Governor lacks PROPOSER_ROLE on Timelock.',
|
|
68
|
+
fixCmd: `sage timelock execute-call --subdao ${subdao} --to ${out.timelock} --sig "grantRole(bytes32,address)" --args "${PROPOSER},${out.governor}"`,
|
|
69
|
+
fixPayload: { to: out.timelock, data: calldata, description: 'Grant PROPOSER to Governor' }
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (!out.anyoneExec) {
|
|
73
|
+
const iface = tl.interface;
|
|
74
|
+
const calldata = iface.encodeFunctionData('grantRole', [EXECUTOR, ethers.ZeroAddress]);
|
|
75
|
+
out.recommendations.push({
|
|
76
|
+
type: 'role_missing',
|
|
77
|
+
msg: 'Timelock execution is restricted (executor != address(0)).',
|
|
78
|
+
fixCmd: `sage timelock execute-call --subdao ${subdao} --to ${out.timelock} --sig "grantRole(bytes32,address)" --args "${EXECUTOR},${ethers.ZeroAddress}"`,
|
|
79
|
+
fixPayload: { to: out.timelock, data: calldata, description: 'Open Execution (Grant EXECUTOR to 0x0)' }
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
} catch (e) {
|
|
83
|
+
out.checks.roles = 'failed';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 4. PromptRegistry Governance
|
|
87
|
+
if (out.registry && out.registry !== ethers.ZeroAddress) {
|
|
88
|
+
try {
|
|
89
|
+
const reg = new ethers.Contract(out.registry, [
|
|
90
|
+
'function GOVERNANCE_ROLE() view returns (bytes32)',
|
|
91
|
+
'function hasRole(bytes32,address) view returns (bool)',
|
|
92
|
+
'function grantRole(bytes32,address)'
|
|
93
|
+
], provider);
|
|
94
|
+
const GOV_ROLE = await reg.GOVERNANCE_ROLE().catch(() => null);
|
|
95
|
+
if (GOV_ROLE) {
|
|
96
|
+
const has = await reg.hasRole(GOV_ROLE, subdao).catch(() => false);
|
|
97
|
+
if (!has) {
|
|
98
|
+
const calldata = reg.interface.encodeFunctionData('grantRole', [GOV_ROLE, subdao]);
|
|
99
|
+
out.recommendations.push({
|
|
100
|
+
type: 'registry_role',
|
|
101
|
+
msg: 'SubDAO missing GOVERNANCE_ROLE on PromptRegistry.',
|
|
102
|
+
fixCmd: `sage timelock execute-call --subdao ${subdao} --to ${out.registry} --sig "grantRole(bytes32,address)" --args "${GOV_ROLE},${subdao}"`,
|
|
103
|
+
fixPayload: { to: out.registry, data: calldata, description: 'Grant GOVERNANCE_ROLE to SubDAO' }
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch (e) {
|
|
108
|
+
out.checks.registry = 'failed';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 5. LibraryRegistry (Scoped)
|
|
113
|
+
const libRegAddr = process.env.LIBRARY_REGISTRY_ADDRESS;
|
|
114
|
+
if (libRegAddr && out.timelock) {
|
|
115
|
+
try {
|
|
116
|
+
const lib = new ethers.Contract(libRegAddr, [
|
|
117
|
+
'function libraryAdminRole(address subdao) view returns (bytes32)',
|
|
118
|
+
'function hasRole(bytes32,address) view returns (bool)',
|
|
119
|
+
'function grantRole(bytes32,address)'
|
|
120
|
+
], provider);
|
|
121
|
+
const scopedRole = await lib.libraryAdminRole(subdao);
|
|
122
|
+
const has = await lib.hasRole(scopedRole, out.timelock).catch(()=>false);
|
|
123
|
+
if (!has) {
|
|
124
|
+
const calldata = lib.interface.encodeFunctionData('grantRole', [scopedRole, out.timelock]);
|
|
125
|
+
out.recommendations.push({
|
|
126
|
+
type: 'library_role',
|
|
127
|
+
msg: 'Timelock missing LIBRARY_ADMIN_ROLE (scoped) on LibraryRegistry.',
|
|
128
|
+
fixCmd: `sage safe propose --safe <TREASURY> --to ${libRegAddr} --data ${calldata} --operation 0`,
|
|
129
|
+
fixPayload: { to: libRegAddr, data: calldata, description: 'Grant Scoped LIBRARY_ADMIN to Timelock' }
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
} catch (e) {
|
|
133
|
+
out.checks.library = 'failed';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = { diagnoseSubDAO };
|