@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.
- package/dist/cli/commands/config.js +28 -0
- package/dist/cli/commands/doctor.js +87 -8
- package/dist/cli/commands/gov-config.js +81 -0
- package/dist/cli/commands/governance.js +152 -72
- package/dist/cli/commands/library.js +9 -0
- package/dist/cli/commands/proposals.js +187 -17
- package/dist/cli/commands/skills.js +737 -0
- package/dist/cli/commands/subdao.js +96 -132
- package/dist/cli/config/playbooks.json +62 -0
- package/dist/cli/config.js +15 -0
- package/dist/cli/governance-manager.js +25 -4
- package/dist/cli/index.js +6 -7
- package/dist/cli/library-manager.js +79 -0
- package/dist/cli/mcp-server-stdio.js +1387 -166
- package/dist/cli/schemas/manifest.schema.json +55 -0
- package/dist/cli/services/doctor/fixers.js +134 -0
- package/dist/cli/services/governance/doctor.js +140 -0
- package/dist/cli/services/governance/playbooks.js +97 -0
- package/dist/cli/services/mcp/bulk-operations.js +272 -0
- package/dist/cli/services/mcp/dependency-analyzer.js +202 -0
- package/dist/cli/services/mcp/library-listing.js +2 -2
- package/dist/cli/services/mcp/local-prompt-collector.js +1 -0
- package/dist/cli/services/mcp/manifest-downloader.js +5 -3
- package/dist/cli/services/mcp/manifest-fetcher.js +17 -1
- package/dist/cli/services/mcp/manifest-workflows.js +127 -15
- package/dist/cli/services/mcp/quick-start.js +287 -0
- package/dist/cli/services/mcp/stdio-runner.js +30 -5
- package/dist/cli/services/mcp/template-manager.js +156 -0
- package/dist/cli/services/mcp/templates/default-templates.json +84 -0
- package/dist/cli/services/mcp/tool-args-validator.js +56 -0
- package/dist/cli/services/mcp/trending-formatter.js +1 -1
- package/dist/cli/services/mcp/unified-prompt-search.js +2 -2
- package/dist/cli/services/metaprompt/designer.js +12 -5
- package/dist/cli/services/skills/discovery.js +99 -0
- package/dist/cli/services/subdao/applier.js +229 -0
- package/dist/cli/services/subdao/planner.js +142 -0
- package/dist/cli/subdao-manager.js +14 -0
- package/dist/cli/utils/aliases.js +28 -6
- package/dist/cli/utils/contract-error-decoder.js +61 -0
- package/dist/cli/utils/suggestions.js +25 -13
- package/package.json +3 -2
- package/src/schemas/manifest.schema.json +55 -0
|
@@ -92,6 +92,34 @@ function register(program) {
|
|
|
92
92
|
}
|
|
93
93
|
})
|
|
94
94
|
)
|
|
95
|
+
.addCommand(
|
|
96
|
+
new Command('set-github')
|
|
97
|
+
.description('Persist GitHub token for private repository access (stored in keychain by default)')
|
|
98
|
+
.option('--token <value>', 'GitHub token (classic or fine-grained)')
|
|
99
|
+
.option('--profile <name>', 'Profile name to update (default: active)')
|
|
100
|
+
.option('--global', 'Persist in XDG config (~/.config/sage/config.json)', false)
|
|
101
|
+
.option('--allow-plain-secret', 'Store plaintext in config instead of keychain (not recommended)', false)
|
|
102
|
+
.action(async (opts) => {
|
|
103
|
+
try {
|
|
104
|
+
if (!opts.token) throw new Error('Provide --token');
|
|
105
|
+
const payload = {};
|
|
106
|
+
if (opts.allowPlainSecret) {
|
|
107
|
+
payload.githubToken = opts.token;
|
|
108
|
+
} else {
|
|
109
|
+
const secrets = require('../services/secrets');
|
|
110
|
+
const profile = opts.profile;
|
|
111
|
+
await secrets.set('github.token', opts.token, { profile }).catch(() => {});
|
|
112
|
+
payload.githubTokenRef = 'github.token';
|
|
113
|
+
}
|
|
114
|
+
config.writeGitConfig(payload, { global: !!opts.global, profile: opts.profile });
|
|
115
|
+
console.log(`✅ Saved GitHub token to ${opts.profile || 'active'} profile${opts.global ? ' (global)' : ''}`);
|
|
116
|
+
console.log('Tip: CLI also respects GITHUB_TOKEN/GH_TOKEN environment variables at runtime.');
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error('❌ Failed to save GitHub token:', error.message);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
)
|
|
95
123
|
)
|
|
96
124
|
.addCommand(
|
|
97
125
|
new Command('governance-helper')
|
|
@@ -32,6 +32,9 @@ async function checkArtifacts(roots) {
|
|
|
32
32
|
async function runDoctor(opts = {}) {
|
|
33
33
|
const out = [];
|
|
34
34
|
const warn = [];
|
|
35
|
+
const diagnostics = [];
|
|
36
|
+
const { fixRpcUrl, fixIpfsKeys, fixAllowance } = require('../services/doctor/fixers');
|
|
37
|
+
|
|
35
38
|
const useSpinner = opts.disableSpinner !== true;
|
|
36
39
|
const runWithProgress = async (label, task) => {
|
|
37
40
|
if (!useSpinner) return task();
|
|
@@ -55,7 +58,7 @@ async function runDoctor(opts = {}) {
|
|
|
55
58
|
warn.push('Test mode: diagnostics skipped');
|
|
56
59
|
console.log('Warnings:');
|
|
57
60
|
warn.forEach((w) => console.log(`- ${w}`));
|
|
58
|
-
return;
|
|
61
|
+
return { diagnostics: [] };
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
// 1) Cast availability
|
|
@@ -64,7 +67,14 @@ async function runDoctor(opts = {}) {
|
|
|
64
67
|
out.push(`${hasCast ? '✅' : '❌'} Foundry (cast)`);
|
|
65
68
|
|
|
66
69
|
// 2) RPC reachability + chainId (env vs profile)
|
|
67
|
-
|
|
70
|
+
// Prefer env RPC over profile for testing/debugging
|
|
71
|
+
let profileRpc = null;
|
|
72
|
+
try {
|
|
73
|
+
const prof = config.readProfiles();
|
|
74
|
+
const ap = prof.activeProfile || 'default';
|
|
75
|
+
profileRpc = prof?.profiles?.[ap]?.rpcUrl || null;
|
|
76
|
+
} catch (_) {}
|
|
77
|
+
const rpcUrl = process.env.RPC_URL || profileRpc || 'https://sepolia.base.org';
|
|
68
78
|
const expectedChainId = Number(process.env.CHAIN_ID || 84532);
|
|
69
79
|
|
|
70
80
|
// Check RPC health
|
|
@@ -74,15 +84,19 @@ async function runDoctor(opts = {}) {
|
|
|
74
84
|
out.push(`${c.green('✅')} RPC healthy (${rpcHealth.latency}ms)`);
|
|
75
85
|
} else {
|
|
76
86
|
out.push(`${c.red('❌')} RPC unhealthy: ${rpcHealth.errors.join(', ')}`);
|
|
87
|
+
diagnostics.push({
|
|
88
|
+
level: 'error',
|
|
89
|
+
code: 'RPC_UNHEALTHY',
|
|
90
|
+
message: `RPC endpoint is unhealthy: ${rpcHealth.errors.join(', ')}`,
|
|
91
|
+
fix: fixRpcUrl
|
|
92
|
+
});
|
|
77
93
|
}
|
|
78
94
|
rpcHealth.warnings.forEach(w => warn.push(`${c.yellow('⚠️')} RPC: ${w}`));
|
|
79
|
-
let profileRpc = null;
|
|
80
95
|
let profileChainId = null;
|
|
81
96
|
try {
|
|
82
97
|
const prof = config.readProfiles();
|
|
83
98
|
const ap = prof.activeProfile || 'default';
|
|
84
99
|
const p = prof?.profiles?.[ap] || {};
|
|
85
|
-
profileRpc = p.rpcUrl || null;
|
|
86
100
|
profileChainId = p.chainId ? Number(p.chainId) : null;
|
|
87
101
|
} catch (_) {}
|
|
88
102
|
let provider, network;
|
|
@@ -103,6 +117,15 @@ async function runDoctor(opts = {}) {
|
|
|
103
117
|
} catch (e) {
|
|
104
118
|
out.push(`❌ RPC unreachable: ${rpcUrl}`);
|
|
105
119
|
warn.push(`RPC error: ${e.message}`);
|
|
120
|
+
// Duplicate check but safe
|
|
121
|
+
if (!diagnostics.find(d => d.code === 'RPC_UNHEALTHY')) {
|
|
122
|
+
diagnostics.push({
|
|
123
|
+
level: 'error',
|
|
124
|
+
code: 'RPC_UNREACHABLE',
|
|
125
|
+
message: `RPC unreachable: ${e.message}`,
|
|
126
|
+
fix: fixRpcUrl
|
|
127
|
+
});
|
|
128
|
+
}
|
|
106
129
|
}
|
|
107
130
|
|
|
108
131
|
// 2b) Subgraph status (URL + last indexed block, with lag vs RPC)
|
|
@@ -308,7 +331,15 @@ async function runDoctor(opts = {}) {
|
|
|
308
331
|
const tok = new ethers.Contract(sxxxAddr, ERC20, provider);
|
|
309
332
|
const allowance = await tok.allowance(proposer, governorAddr);
|
|
310
333
|
out.push(`💳 SXXX allowance to Governor: ${ethers.formatEther(allowance)} SXXX`);
|
|
311
|
-
if (allowance === 0n)
|
|
334
|
+
if (allowance === 0n) {
|
|
335
|
+
warn.push('SXXX allowance is 0. Approve before proposing library updates.');
|
|
336
|
+
diagnostics.push({
|
|
337
|
+
level: 'warn',
|
|
338
|
+
code: 'ALLOWANCE_ZERO',
|
|
339
|
+
message: 'SXXX allowance to Governor is 0',
|
|
340
|
+
fix: () => fixAllowance(sxxxAddr, governorAddr, 'Governor')
|
|
341
|
+
});
|
|
342
|
+
}
|
|
312
343
|
}
|
|
313
344
|
if (sxxxAddr) {
|
|
314
345
|
const BurnChk = ['function authorizedBurners(address) view returns (bool)'];
|
|
@@ -372,7 +403,7 @@ async function runDoctor(opts = {}) {
|
|
|
372
403
|
|
|
373
404
|
// 7) LibraryRegistry role diagnostics (optional)
|
|
374
405
|
try {
|
|
375
|
-
if (
|
|
406
|
+
if (opts.registry) {
|
|
376
407
|
const { resolveRegistryAddress } = require('../utils/address-resolution');
|
|
377
408
|
const { registry: regAddr } = resolveRegistryAddress();
|
|
378
409
|
const facAddr = process.env.SUBDAO_FACTORY_ADDRESS;
|
|
@@ -600,6 +631,26 @@ async function runDoctor(opts = {}) {
|
|
|
600
631
|
}
|
|
601
632
|
} catch (e) { warn.push('SimpleKeyStore checks failed: ' + e.message); }
|
|
602
633
|
|
|
634
|
+
// 12b) IPFS configuration (Pinata keys)
|
|
635
|
+
try {
|
|
636
|
+
const ipfsCfg = config.readIpfsConfig ? config.readIpfsConfig() : {};
|
|
637
|
+
if (!ipfsCfg.provider) {
|
|
638
|
+
warn.push('IPFS provider not configured');
|
|
639
|
+
diagnostics.push({ level: 'warn', code: 'IPFS_KEYS_MISSING', message: 'IPFS not configured', fix: fixIpfsKeys });
|
|
640
|
+
} else if (String(ipfsCfg.provider).toLowerCase() === 'pinata') {
|
|
641
|
+
const hasJwt = !!ipfsCfg.pinataJwt;
|
|
642
|
+
const hasKeys = !!ipfsCfg.pinataApiKey && !!ipfsCfg.pinataSecret;
|
|
643
|
+
if (!hasJwt && !hasKeys) {
|
|
644
|
+
warn.push('IPFS (pinata) lacks credentials (JWT or API key/secret)');
|
|
645
|
+
diagnostics.push({ level: 'warn', code: 'IPFS_KEYS_MISSING', message: 'IPFS Pinata keys not configured', fix: fixIpfsKeys });
|
|
646
|
+
} else {
|
|
647
|
+
out.push(`✅ IPFS configured: pinata (${hasJwt ? 'jwt' : 'api-key'})`);
|
|
648
|
+
}
|
|
649
|
+
} else {
|
|
650
|
+
out.push(`✅ IPFS configured: ${ipfsCfg.provider}`);
|
|
651
|
+
}
|
|
652
|
+
} catch (e) { warn.push('IPFS config check failed: ' + e.message); }
|
|
653
|
+
|
|
603
654
|
// Print report
|
|
604
655
|
console.log(c.cyan('🔎 Sage Doctor Report'));
|
|
605
656
|
// SAGE_HOME/project-local path
|
|
@@ -615,9 +666,10 @@ async function runDoctor(opts = {}) {
|
|
|
615
666
|
}
|
|
616
667
|
console.log('');
|
|
617
668
|
console.log('Tips:');
|
|
618
|
-
console.log(' - Set addresses via: sage config addresses import --file packages
|
|
669
|
+
console.log(' - Set addresses via: sage config addresses import --file packages/contracts/deployment/addresses/sepolia.json');
|
|
619
670
|
console.log(' - Set working SubDAO: sage subdao use <0x...>');
|
|
620
671
|
console.log(' - Verify env: sage config env show');
|
|
672
|
+
return { diagnostics };
|
|
621
673
|
}
|
|
622
674
|
|
|
623
675
|
function _resolveSageHome() {
|
|
@@ -650,6 +702,8 @@ function register(program) {
|
|
|
650
702
|
.option('--registry', 'Also check LibraryRegistry roles (FACTORY_ROLE, LIBRARY_ADMIN_ROLE)', false)
|
|
651
703
|
.option('--cid <cid>', 'Optional CID to warm-test IPFS gateways')
|
|
652
704
|
.option('--subdao <address>', 'Target SubDAO for mode/roles diagnostics')
|
|
705
|
+
.option('--fix', 'Attempt to fix detected issues interactively', false)
|
|
706
|
+
.option('--auto-fix', 'Attempt to fix non-interactively with safe defaults', false)
|
|
653
707
|
.action(async (opts) => {
|
|
654
708
|
const previousQuiet = process.env.SAGE_QUIET_JSON;
|
|
655
709
|
if (opts.json) process.env.SAGE_QUIET_JSON = '1';
|
|
@@ -659,7 +713,9 @@ function register(program) {
|
|
|
659
713
|
if (opts.json) {
|
|
660
714
|
console.log = (...args) => capture.push(args.join(' '));
|
|
661
715
|
}
|
|
662
|
-
|
|
716
|
+
|
|
717
|
+
const { diagnostics } = await runDoctor({ ...opts, disableSpinner: opts.json });
|
|
718
|
+
|
|
663
719
|
if (opts.cid) {
|
|
664
720
|
try {
|
|
665
721
|
let normalizeCidV1Base32;
|
|
@@ -680,6 +736,28 @@ function register(program) {
|
|
|
680
736
|
}
|
|
681
737
|
} catch (e) { console.log('⚠️ Gateway warm test skipped:', e.message); }
|
|
682
738
|
}
|
|
739
|
+
|
|
740
|
+
// Handle Fixes
|
|
741
|
+
if (opts.fix && diagnostics && diagnostics.length > 0) {
|
|
742
|
+
console.log('\n🔧 Scanning for fixable issues...');
|
|
743
|
+
const fixable = diagnostics.filter(d => d.fix);
|
|
744
|
+
if (fixable.length === 0) {
|
|
745
|
+
console.log(' No auto-fixable issues found.');
|
|
746
|
+
} else {
|
|
747
|
+
for (const diag of fixable) {
|
|
748
|
+
// Ensure user context
|
|
749
|
+
if (!opts.json) {
|
|
750
|
+
console.log(`\nFound issue: ${diag.message}`);
|
|
751
|
+
}
|
|
752
|
+
try {
|
|
753
|
+
await diag.fix({ auto: opts.autoFix });
|
|
754
|
+
} catch (e) {
|
|
755
|
+
console.error(` ❌ Fix failed: ${e.message}`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
683
761
|
if (opts.json) {
|
|
684
762
|
// Build JSON object with key facts
|
|
685
763
|
const rpcUrl = process.env.RPC_URL || 'https://sepolia.base.org';
|
|
@@ -697,6 +775,7 @@ function register(program) {
|
|
|
697
775
|
TIMELOCK: process.env.TIMELOCK || null
|
|
698
776
|
},
|
|
699
777
|
sageHome,
|
|
778
|
+
diagnostics: diagnostics?.map(d => ({ level: d.level, message: d.message, code: d.code, fixable: !!d.fix })) || [],
|
|
700
779
|
logs: capture
|
|
701
780
|
};
|
|
702
781
|
console.log = oldLog; // restore
|
|
@@ -188,6 +188,87 @@ function register(program) {
|
|
|
188
188
|
})
|
|
189
189
|
);
|
|
190
190
|
|
|
191
|
+
// Voting configuration proposal (set voting period/quorum/threshold via governance)
|
|
192
|
+
cmd.addCommand(
|
|
193
|
+
new Command('voting')
|
|
194
|
+
.description('Propose updates to voting parameters (voting period, quorum bps, proposal threshold)')
|
|
195
|
+
.option('--voting-period <duration>', 'Voting period duration (e.g., 7 days, 36 hours)')
|
|
196
|
+
.option('--quorum-bps <bps>', 'Quorum basis points (e.g., 200 for 2%)')
|
|
197
|
+
.option('--proposal-threshold <amount>', 'Proposal threshold (string/wei)')
|
|
198
|
+
.option('--block-time-sec <n>', 'Override block time (seconds per block); auto-detected if omitted')
|
|
199
|
+
.option('--subdao <address>', 'Resolve governor from this SubDAO')
|
|
200
|
+
.option('--gov <address>', 'Governor address (overrides --subdao resolution)')
|
|
201
|
+
.option('--title <text>', 'Proposal title/description', 'Update voting parameters')
|
|
202
|
+
.action(async (opts) => {
|
|
203
|
+
try {
|
|
204
|
+
const { ethers } = require('ethers');
|
|
205
|
+
const { resolveGovContext } = require('../utils/gov-context');
|
|
206
|
+
const { parseDurationToBlocks } = require('../services/subdao/planner');
|
|
207
|
+
const config = require('../config');
|
|
208
|
+
const wm = new (require('@sage-protocol/wallet-manager'))(); await wm.connect();
|
|
209
|
+
const signer = wm.getSigner(); const provider = wm.getProvider();
|
|
210
|
+
const ctx = await resolveGovContext({ govOpt: opts.gov, subdaoOpt: opts.subdao, provider });
|
|
211
|
+
const govAddr = ctx.governor; if (!govAddr) throw new Error('Governor not resolved; pass --subdao or --gov');
|
|
212
|
+
|
|
213
|
+
const targets = []; const values = []; const calldatas = [];
|
|
214
|
+
const iface = new ethers.Interface([
|
|
215
|
+
'function setVotingPeriod(uint256)',
|
|
216
|
+
'function setProposalThreshold(uint256)',
|
|
217
|
+
'function setQuorumBps(uint256)',
|
|
218
|
+
// fallback OZ method name when available
|
|
219
|
+
'function updateQuorumNumerator(uint256)'
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
const rpc = (config.resolveRpcUrl ? config.resolveRpcUrl() : (process.env.RPC_URL || 'https://base-sepolia.publicnode.com'));
|
|
223
|
+
const providerTmp = new ethers.JsonRpcProvider(rpc);
|
|
224
|
+
const detectBlockTimeSec = async () => {
|
|
225
|
+
try {
|
|
226
|
+
const latest = await providerTmp.getBlockNumber();
|
|
227
|
+
const older = latest > 100 ? latest - 100 : (latest > 1 ? 1 : 0);
|
|
228
|
+
const [bLatest, bOlder] = await Promise.all([providerTmp.getBlock(latest), providerTmp.getBlock(older)]);
|
|
229
|
+
const dt = Number(bLatest.timestamp) - Number(bOlder.timestamp);
|
|
230
|
+
if (dt <= 0) return 12; return Math.max(1, Math.round(dt / Math.max(1, latest - older)));
|
|
231
|
+
} catch (_) { return 12; }
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// votingPeriod → blocks
|
|
235
|
+
if (opts.votingPeriod) {
|
|
236
|
+
const bt = opts.blockTimeSec ? Number(opts.blockTimeSec) : await detectBlockTimeSec();
|
|
237
|
+
const blocks = parseDurationToBlocks(String(opts.votingPeriod), bt);
|
|
238
|
+
targets.push(govAddr); values.push(0);
|
|
239
|
+
calldatas.push(iface.encodeFunctionData('setVotingPeriod', [BigInt(blocks)]));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (opts.proposalThreshold) {
|
|
243
|
+
targets.push(govAddr); values.push(0);
|
|
244
|
+
calldatas.push(iface.encodeFunctionData('setProposalThreshold', [BigInt(String(opts.proposalThreshold))]));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (opts.quorumBps) {
|
|
248
|
+
// Prefer setQuorumBps; fallback to OZ updateQuorumNumerator
|
|
249
|
+
try {
|
|
250
|
+
calldatas.push(iface.encodeFunctionData('setQuorumBps', [Number(opts.quorumBps)]));
|
|
251
|
+
targets.push(govAddr); values.push(0);
|
|
252
|
+
} catch (_) {
|
|
253
|
+
targets.push(govAddr); values.push(0);
|
|
254
|
+
calldatas.push(iface.encodeFunctionData('updateQuorumNumerator', [Number(opts.quorumBps)]));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!targets.length) throw new Error('Provide at least one of --voting-period, --quorum-bps, --proposal-threshold');
|
|
259
|
+
|
|
260
|
+
const gov = new ethers.Contract(govAddr, ['function propose(address[],uint256[],bytes[],string) returns (uint256)'], signer);
|
|
261
|
+
const desc = opts.title || 'Update voting parameters';
|
|
262
|
+
const tx = await gov.propose(targets, values, calldatas, desc);
|
|
263
|
+
console.log('🗳️ propose tx:', tx.hash);
|
|
264
|
+
console.log('➡️ Submit votes, then queue and execute to apply new parameters');
|
|
265
|
+
} catch (e) {
|
|
266
|
+
console.error('❌ voting config failed:', e.message);
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
);
|
|
271
|
+
|
|
191
272
|
program.addCommand(cmd);
|
|
192
273
|
}
|
|
193
274
|
|
|
@@ -3,6 +3,7 @@ const { Command } = require('commander');
|
|
|
3
3
|
const SXXXManager = require('../sxxx-manager');
|
|
4
4
|
const SubDAOManager = require('../subdao-manager');
|
|
5
5
|
const LibraryManager = require('../library-manager');
|
|
6
|
+
const GovernanceManager = require('../governance-manager');
|
|
6
7
|
const { ethers } = require('ethers');
|
|
7
8
|
const axios = require('axios');
|
|
8
9
|
const { execSync } = require('child_process');
|
|
@@ -15,51 +16,6 @@ const { handleCLIError } = require('../utils/error-handler');
|
|
|
15
16
|
const logger = require('../utils/logger');
|
|
16
17
|
const hooks = require('../utils/hooks');
|
|
17
18
|
|
|
18
|
-
async function resolveGovContext(options) {
|
|
19
|
-
let subdao = options.subdao || process.env.WORKING_SUBDAO_ADDRESS || process.env.SUBDAO;
|
|
20
|
-
let governorAddr = process.env.GOV;
|
|
21
|
-
|
|
22
|
-
if (subdao) {
|
|
23
|
-
// Get governor address from SubDAO contract
|
|
24
|
-
const rpcUrl = process.env.RPC_URL || 'https://base-sepolia.publicnode.com';
|
|
25
|
-
try {
|
|
26
|
-
governorAddr = execSync(`cast call ${subdao} "governor()" --rpc-url ${rpcUrl}`, { encoding: 'utf8' }).trim();
|
|
27
|
-
} catch (e) {
|
|
28
|
-
console.log(`⚠️ Could not get governor from SubDAO: ${e.message}`);
|
|
29
|
-
governorAddr = process.env.GOV;
|
|
30
|
-
}
|
|
31
|
-
} else {
|
|
32
|
-
// Fallback to wizard to select a SubDAO or main gov
|
|
33
|
-
const inquirer = (require('inquirer').default || require('inquirer'));
|
|
34
|
-
const { scope } = await inquirer.prompt([
|
|
35
|
-
{ type: 'list', name: 'scope', message: 'Select governance context', choices: [
|
|
36
|
-
{ name: 'A SubDAO (recommended)', value: 'subdao' },
|
|
37
|
-
{ name: 'Main protocol (use GOV from env)', value: 'main' }
|
|
38
|
-
]}
|
|
39
|
-
]);
|
|
40
|
-
|
|
41
|
-
if (scope === 'subdao') {
|
|
42
|
-
// For now, use a simple prompt for SubDAO address
|
|
43
|
-
const { subdaoAddr } = await inquirer.prompt([
|
|
44
|
-
{ type: 'input', name: 'subdaoAddr', message: 'Enter SubDAO address:' }
|
|
45
|
-
]);
|
|
46
|
-
subdao = subdaoAddr;
|
|
47
|
-
const rpcUrl = process.env.RPC_URL || 'https://base-sepolia.publicnode.com';
|
|
48
|
-
try {
|
|
49
|
-
governorAddr = execSync(`cast call ${subdao} "governor()" --rpc-url ${rpcUrl}`, { encoding: 'utf8' }).trim();
|
|
50
|
-
} catch (e) {
|
|
51
|
-
console.log(`⚠️ Could not get governor from SubDAO: ${e.message}`);
|
|
52
|
-
governorAddr = process.env.GOV;
|
|
53
|
-
}
|
|
54
|
-
process.env.SUBDAO = subdao;
|
|
55
|
-
process.env.WORKING_SUBDAO_ADDRESS = subdao;
|
|
56
|
-
cliConfig.writeAddresses({ SUBDAO: subdao, WORKING_SUBDAO_ADDRESS: subdao, GOV: governorAddr });
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return { governorAddr, subdao };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
19
|
function register(program) {
|
|
64
20
|
const governanceCommand = new Command('governance')
|
|
65
21
|
.description('Governance management commands')
|
|
@@ -861,6 +817,8 @@ function register(program) {
|
|
|
861
817
|
.action(async () => {
|
|
862
818
|
try {
|
|
863
819
|
const inquirer = (require('inquirer').default || require('inquirer'));
|
|
820
|
+
const GovernanceManager = require('../governance-manager');
|
|
821
|
+
const SubDAOManager = require('../subdao-manager');
|
|
864
822
|
const gm = new GovernanceManager();
|
|
865
823
|
const sdm = new SubDAOManager();
|
|
866
824
|
await sdm.initialize();
|
|
@@ -871,16 +829,18 @@ function register(program) {
|
|
|
871
829
|
{ name: 'Main protocol (use GOV from env)', value: 'main' }
|
|
872
830
|
]}
|
|
873
831
|
]);
|
|
832
|
+
let subdaoAddr = null;
|
|
874
833
|
if (ctx.scope === 'subdao') {
|
|
875
|
-
|
|
834
|
+
subdaoAddr = await sdm.resolveSubDAOAddress(null, { title: 'Select a SubDAO' });
|
|
876
835
|
await gm.initialize(subdaoAddr);
|
|
877
836
|
// Persist SUBDAO and derived GOV for future runs
|
|
878
837
|
try {
|
|
838
|
+
const LibraryManager = require('../library-manager');
|
|
879
839
|
const lm = new LibraryManager();
|
|
880
840
|
process.env.SUBDAO = subdaoAddr;
|
|
881
841
|
process.env.WORKING_SUBDAO_ADDRESS = subdaoAddr;
|
|
882
842
|
const gov = await lm.getGovernorAddress();
|
|
883
|
-
|
|
843
|
+
require('../config').writeAddresses({ SUBDAO: subdaoAddr, WORKING_SUBDAO_ADDRESS: subdaoAddr, GOV: gov });
|
|
884
844
|
} catch (_) {}
|
|
885
845
|
} else {
|
|
886
846
|
await gm.initialize();
|
|
@@ -1810,7 +1770,10 @@ function register(program) {
|
|
|
1810
1770
|
}
|
|
1811
1771
|
|
|
1812
1772
|
// Update configuration via governance proposal
|
|
1813
|
-
const {
|
|
1773
|
+
const { resolveGovContext } = require('../utils/gov-context');
|
|
1774
|
+
const _prov = new ethers.JsonRpcProvider(process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
|
|
1775
|
+
const _ctx = await resolveGovContext({ govOpt: null, subdaoOpt: opts.subdao, provider: _prov });
|
|
1776
|
+
const governorAddr = _ctx.governor;
|
|
1814
1777
|
const targets = [];
|
|
1815
1778
|
const values = [];
|
|
1816
1779
|
const calldatas = [];
|
|
@@ -1959,7 +1922,9 @@ function register(program) {
|
|
|
1959
1922
|
.option('--subdao <address>', 'SubDAO context')
|
|
1960
1923
|
.action(async (proposalId, support, opts) => {
|
|
1961
1924
|
try {
|
|
1962
|
-
const {
|
|
1925
|
+
const { resolveGovContext } = require('../utils/gov-context');
|
|
1926
|
+
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
|
|
1927
|
+
const ctx = await resolveGovContext({ govOpt: opts.gov, subdaoOpt: opts.subdao, provider });
|
|
1963
1928
|
const rpcUrl = process.env.RPC_URL || 'https://base-sepolia.publicnode.com';
|
|
1964
1929
|
|
|
1965
1930
|
const supportInt = parseInt(support);
|
|
@@ -2008,14 +1973,24 @@ function register(program) {
|
|
|
2008
1973
|
.option('--description <description>', 'Proposal description')
|
|
2009
1974
|
.option('--targets <addresses>', 'Comma-separated target contract addresses')
|
|
2010
1975
|
.option('--values <amounts>', 'Comma-separated ETH values (in ether)')
|
|
2011
|
-
.option('--calldatas <data>', 'Comma-separated calldata
|
|
1976
|
+
.option('--calldatas <data>', 'Comma-separated calldata (0x hex)')
|
|
1977
|
+
.option('--target <address>', 'Target address (repeatable)', (v, p = []) => { p.push(v); return p; }, [])
|
|
1978
|
+
.option('--value <eth>', 'ETH value in ether (repeatable)', (v, p = []) => { p.push(v); return p; }, [])
|
|
1979
|
+
.option('--calldata <data>', 'Calldata 0x hex (repeatable)', (v, p = []) => { p.push(v); return p; }, [])
|
|
1980
|
+
.option('--sigs <signatures>', 'Comma-separated function signatures (optional)')
|
|
2012
1981
|
.option('--subdao <address>', 'SubDAO context')
|
|
1982
|
+
.option('--gov <address>', 'Governor address (overrides SubDAO)')
|
|
1983
|
+
.option('--dry-run', 'Validate proposal without creating it', false)
|
|
1984
|
+
.option('--yes', 'Skip confirmation prompt', false)
|
|
1985
|
+
.option('--verbose', 'Enable verbose logging', false)
|
|
2013
1986
|
.action(async (opts) => {
|
|
2014
1987
|
try {
|
|
2015
|
-
const {
|
|
1988
|
+
const { resolveGovContext } = require('../utils/gov-context');
|
|
1989
|
+
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
|
|
1990
|
+
const ctx = await resolveGovContext({ govOpt: opts.gov, subdaoOpt: opts.subdao, provider });
|
|
2016
1991
|
|
|
2017
1992
|
// If no options provided, use interactive mode
|
|
2018
|
-
if (!opts.
|
|
1993
|
+
if (!opts.targets && (!opts.target || opts.target.length === 0) && !opts.calldatas && (!opts.calldata || opts.calldata.length === 0)) {
|
|
2019
1994
|
console.log('🏛️ Interactive proposal creation');
|
|
2020
1995
|
console.log('Tip: Use the governance wizard for guided proposal creation: sage governance wizard');
|
|
2021
1996
|
|
|
@@ -2031,44 +2006,149 @@ function register(program) {
|
|
|
2031
2006
|
Object.assign(opts, answers);
|
|
2032
2007
|
}
|
|
2033
2008
|
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2009
|
+
const splitCsv = (s) => String(s || '').split(',').map(x => x.trim()).filter(Boolean);
|
|
2010
|
+
const targetsRaw = [ ...(opts.target || []), ...(opts.targets ? splitCsv(opts.targets) : []) ];
|
|
2011
|
+
const valuesRaw = [ ...(opts.value || []), ...(opts.values ? splitCsv(opts.values) : []) ];
|
|
2012
|
+
const calldatasRaw = [ ...(opts.calldata || []), ...(opts.calldatas ? splitCsv(opts.calldatas) : []) ];
|
|
2037
2013
|
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
const calldatas = opts.calldatas.split(',').map(s => s.trim());
|
|
2041
|
-
|
|
2042
|
-
if (targets.length !== values.length || targets.length !== calldatas.length) {
|
|
2043
|
-
throw new Error('Targets, values, and calldatas arrays must have the same length');
|
|
2014
|
+
if (targetsRaw.length === 0 || calldatasRaw.length === 0) {
|
|
2015
|
+
throw new Error('Missing required fields: targets and calldatas are required');
|
|
2044
2016
|
}
|
|
2045
2017
|
|
|
2046
|
-
|
|
2018
|
+
let targets;
|
|
2019
|
+
try {
|
|
2020
|
+
targets = targetsRaw.map((t) => ethers.getAddress(t));
|
|
2021
|
+
} catch (_) {
|
|
2022
|
+
throw new Error('Invalid target address provided');
|
|
2023
|
+
}
|
|
2024
|
+
let values = valuesRaw.length ? valuesRaw.map((v) => ethers.parseEther(String(v))) : Array(targets.length).fill(0n);
|
|
2025
|
+
if (values.length !== targets.length) {
|
|
2026
|
+
throw new Error('Values length must equal targets length (or omit values to default to 0)');
|
|
2027
|
+
}
|
|
2028
|
+
const isHex = (s) => /^0x[0-9a-fA-F]*$/.test(String(s));
|
|
2029
|
+
const calldatas = calldatasRaw.map(String);
|
|
2030
|
+
if (calldatas.length !== targets.length) {
|
|
2031
|
+
throw new Error('Calldatas length must equal targets length');
|
|
2032
|
+
}
|
|
2033
|
+
if (opts.sigs && calldatas.some((c) => !isHex(c))) {
|
|
2034
|
+
throw new Error('Non-hex calldata with --sigs is not supported. Provide 0x-encoded calldatas.');
|
|
2035
|
+
}
|
|
2036
|
+
if (calldatas.some((c) => !isHex(c))) {
|
|
2037
|
+
throw new Error('Calldata entries must be 0x hex-encoded');
|
|
2038
|
+
}
|
|
2039
|
+
const description = opts.description || opts.title || '';
|
|
2047
2040
|
|
|
2048
2041
|
console.log('--- Proposal Preview ---');
|
|
2049
|
-
console.log('Title:', opts.title);
|
|
2050
|
-
console.log('Description:', description);
|
|
2042
|
+
if (opts.title) console.log('Title:', opts.title);
|
|
2043
|
+
if (description) console.log('Description:', description);
|
|
2051
2044
|
console.log('Targets:', targets);
|
|
2052
2045
|
console.log('Values:', values.map(v => v.toString()));
|
|
2053
2046
|
console.log('Calldatas:', calldatas.map(c => c.substring(0, 20) + '...'));
|
|
2054
2047
|
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2048
|
+
if (opts.dryRun) {
|
|
2049
|
+
console.log('✅ Proposal validation passed (dry-run)');
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2058
2052
|
|
|
2059
|
-
|
|
2060
|
-
|
|
2053
|
+
if (!opts.yes && String(process.env.SAGE_FORCE || '').trim() !== '1') {
|
|
2054
|
+
const inquirer = (require('inquirer').default || require('inquirer'));
|
|
2055
|
+
const { confirm } = await inquirer.prompt([{ type: 'confirm', name: 'confirm', message: 'Create proposal?', default: true }]);
|
|
2056
|
+
if (!confirm) return;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
if (opts.verbose || process.env.SAGE_VERBOSE === '1') {
|
|
2060
|
+
console.log('🔗 Connecting wallet...');
|
|
2061
|
+
}
|
|
2062
|
+
const gm = new GovernanceManager();
|
|
2063
|
+
await gm.initialize(ctx.subdao || opts.subdao || null);
|
|
2061
2064
|
|
|
2062
|
-
|
|
2063
|
-
|
|
2065
|
+
if (opts.verbose || process.env.SAGE_VERBOSE === '1') {
|
|
2066
|
+
console.log('📝 Creating proposal...');
|
|
2067
|
+
}
|
|
2068
|
+
await gm.createProposal(targets, values, calldatas, description);
|
|
2064
2069
|
console.log('✅ Proposal created successfully');
|
|
2065
2070
|
|
|
2066
2071
|
} catch (e) {
|
|
2067
|
-
|
|
2072
|
+
try {
|
|
2073
|
+
const { handleCLIError } = require('../utils/error-handler');
|
|
2074
|
+
handleCLIError('governance:propose-custom', e, { context: { subdao: opts?.subdao, gov: opts?.gov } });
|
|
2075
|
+
} catch (_) {
|
|
2076
|
+
console.error('❌ Failed to create proposal:', e.message);
|
|
2077
|
+
}
|
|
2078
|
+
process.exit(1);
|
|
2079
|
+
}
|
|
2080
|
+
})
|
|
2081
|
+
)
|
|
2082
|
+
.addCommand(
|
|
2083
|
+
new Command('wait')
|
|
2084
|
+
.description('Wait for a proposal to reach a target state')
|
|
2085
|
+
.argument('<proposalId>', 'Proposal ID')
|
|
2086
|
+
.option('--target <state>', 'Target state (Active, Canceled, Defeated, Succeeded, Queued, Expired, Executed)', 'Executed')
|
|
2087
|
+
.option('--timeout <seconds>', 'Timeout in seconds', '300')
|
|
2088
|
+
.option('--interval <seconds>', 'Poll interval in seconds', '5')
|
|
2089
|
+
.option('--subdao <address>', 'SubDAO address')
|
|
2090
|
+
.action(async (proposalId, opts) => {
|
|
2091
|
+
try {
|
|
2092
|
+
const { ethers } = require('ethers');
|
|
2093
|
+
const { resolveGovContext } = require('../utils/gov-context');
|
|
2094
|
+
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
|
|
2095
|
+
const ctx = await resolveGovContext({ govOpt: null, subdaoOpt: opts.subdao, provider });
|
|
2096
|
+
const GovABI = require('../utils/artifacts').resolveArtifact('contracts/cloneable/PromptGovernorCloneable.sol/PromptGovernorCloneable.json').abi;
|
|
2097
|
+
const gov = new ethers.Contract(ctx.governor, GovABI, provider);
|
|
2098
|
+
|
|
2099
|
+
const states = ['Pending', 'Active', 'Canceled', 'Defeated', 'Succeeded', 'Queued', 'Expired', 'Executed'];
|
|
2100
|
+
const targetState = String(opts.target).charAt(0).toUpperCase() + String(opts.target).slice(1).toLowerCase();
|
|
2101
|
+
const targetIdx = states.indexOf(targetState);
|
|
2102
|
+
if (targetIdx === -1) throw new Error(`Invalid target state: ${targetState}. Valid: ${states.join(', ')}`);
|
|
2103
|
+
|
|
2104
|
+
const pid = BigInt(proposalId);
|
|
2105
|
+
const timeout = Number(opts.timeout) * 1000;
|
|
2106
|
+
const interval = Number(opts.interval) * 1000;
|
|
2107
|
+
const start = Date.now();
|
|
2108
|
+
|
|
2109
|
+
console.log(`⏳ Waiting for proposal ${proposalId} to reach ${targetState}...`);
|
|
2110
|
+
|
|
2111
|
+
while (true) {
|
|
2112
|
+
if (Date.now() - start > timeout) {
|
|
2113
|
+
throw new Error(`Timeout waiting for state ${targetState}`);
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
let currentStateIdx;
|
|
2117
|
+
try {
|
|
2118
|
+
currentStateIdx = Number(await gov.state(pid));
|
|
2119
|
+
} catch (e) {
|
|
2120
|
+
// Ignore transient RPC errors
|
|
2121
|
+
await new Promise(r => setTimeout(r, interval));
|
|
2122
|
+
continue;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
const currentState = states[currentStateIdx] || 'Unknown';
|
|
2126
|
+
|
|
2127
|
+
// Success?
|
|
2128
|
+
if (currentStateIdx === targetIdx) {
|
|
2129
|
+
console.log(`\n✅ Proposal reached target state: ${currentState}`);
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
// Failure check (terminal states)
|
|
2134
|
+
if (targetIdx > currentStateIdx) {
|
|
2135
|
+
if (currentState === 'Canceled' || currentState === 'Defeated' || currentState === 'Expired') {
|
|
2136
|
+
throw new Error(`Proposal reached terminal state ${currentState} before ${targetState}`);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
process.stdout.write(`\r Current state: ${currentState} (${Math.round((timeout - (Date.now() - start))/1000)}s remaining) `);
|
|
2141
|
+
|
|
2142
|
+
await new Promise(r => setTimeout(r, interval));
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
} catch (e) {
|
|
2146
|
+
console.error(`\n❌ Wait failed: ${e.message}`);
|
|
2068
2147
|
process.exit(1);
|
|
2069
2148
|
}
|
|
2070
2149
|
})
|
|
2071
2150
|
);
|
|
2151
|
+
|
|
2072
2152
|
program.addCommand(governanceCommand);
|
|
2073
2153
|
}
|
|
2074
2154
|
|
|
@@ -1311,6 +1311,15 @@ function register(program) {
|
|
|
1311
1311
|
{ exec: !!options.exec, libraryId: options.libraryId || 'main', ctx: { govOpt: ctx.governor || undefined, subdaoOpt: ctx.subdao || undefined, timelockOpt: ctx.timelock || undefined } }
|
|
1312
1312
|
);
|
|
1313
1313
|
console.log(`🆔 Proposal ID: ${proposalId}`);
|
|
1314
|
+
// Print next steps hints via suggestions system
|
|
1315
|
+
printSuggestions('library:push', {
|
|
1316
|
+
manifestCID,
|
|
1317
|
+
proposalId: proposalId.toString(),
|
|
1318
|
+
subdao: ctx.subdao || options.subdao || null,
|
|
1319
|
+
exec: !!options.exec,
|
|
1320
|
+
route: 'governance',
|
|
1321
|
+
manifestPath: manifestPath
|
|
1322
|
+
});
|
|
1314
1323
|
} catch (error) {
|
|
1315
1324
|
const { handleCLIError } = require('../utils/error-handler');
|
|
1316
1325
|
handleCLIError('library:propose', error, { context: { manifest: manifestPath, subdao: options.subdao, gov: options.gov } });
|