@sage-protocol/cli 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/cli/commands/doctor.js +87 -8
  2. package/dist/cli/commands/gov-config.js +81 -0
  3. package/dist/cli/commands/governance.js +152 -72
  4. package/dist/cli/commands/library.js +9 -0
  5. package/dist/cli/commands/proposals.js +187 -17
  6. package/dist/cli/commands/skills.js +175 -21
  7. package/dist/cli/commands/subdao.js +22 -2
  8. package/dist/cli/config/playbooks.json +15 -0
  9. package/dist/cli/governance-manager.js +25 -4
  10. package/dist/cli/index.js +5 -6
  11. package/dist/cli/library-manager.js +79 -0
  12. package/dist/cli/mcp-server-stdio.js +1374 -82
  13. package/dist/cli/schemas/manifest.schema.json +55 -0
  14. package/dist/cli/services/doctor/fixers.js +134 -0
  15. package/dist/cli/services/mcp/bulk-operations.js +272 -0
  16. package/dist/cli/services/mcp/dependency-analyzer.js +202 -0
  17. package/dist/cli/services/mcp/library-listing.js +2 -2
  18. package/dist/cli/services/mcp/local-prompt-collector.js +1 -0
  19. package/dist/cli/services/mcp/manifest-downloader.js +5 -3
  20. package/dist/cli/services/mcp/manifest-fetcher.js +17 -1
  21. package/dist/cli/services/mcp/manifest-workflows.js +127 -15
  22. package/dist/cli/services/mcp/quick-start.js +287 -0
  23. package/dist/cli/services/mcp/stdio-runner.js +30 -5
  24. package/dist/cli/services/mcp/template-manager.js +156 -0
  25. package/dist/cli/services/mcp/templates/default-templates.json +84 -0
  26. package/dist/cli/services/mcp/tool-args-validator.js +56 -0
  27. package/dist/cli/services/mcp/trending-formatter.js +1 -1
  28. package/dist/cli/services/mcp/unified-prompt-search.js +2 -2
  29. package/dist/cli/services/metaprompt/designer.js +12 -5
  30. package/dist/cli/services/subdao/applier.js +208 -196
  31. package/dist/cli/services/subdao/planner.js +41 -6
  32. package/dist/cli/subdao-manager.js +14 -0
  33. package/dist/cli/utils/aliases.js +17 -2
  34. package/dist/cli/utils/contract-error-decoder.js +61 -0
  35. package/dist/cli/utils/suggestions.js +17 -12
  36. package/package.json +3 -2
  37. package/src/schemas/manifest.schema.json +55 -0
@@ -7,211 +7,223 @@ const FactoryABI = require('../../utils/factory-abi');
7
7
  const { resolveArtifact } = require('../../utils/artifacts');
8
8
 
9
9
  async function applyPlan(planOrPath) {
10
- let plan;
11
- if (typeof planOrPath === 'string') {
12
- const content = fs.readFileSync(planOrPath, 'utf8');
13
- plan = JSON.parse(content);
14
- } else {
15
- plan = planOrPath;
16
- }
17
-
18
- console.log(`Applying Plan: ${plan.config.name} (${plan.playbook.name})`);
19
-
20
- // Instantiate manager
21
- const manager = new SubDAOManager();
22
- const WalletManager = require('../../wallet-manager');
23
- const wm = new WalletManager();
24
- await wm.connect();
25
- manager.signer = wm.getSigner();
26
- manager.walletManager = wm;
27
- await manager.initialize();
28
- const provider = wm.getProvider();
29
-
30
- // Idempotency Check: Check if SubDAO with this name is already registered in Factory
31
- // Or locally in subdao store.
32
- // The Factory doesn't expose `isSubDAO(name)`. It exposes `isSubDAO(address)`.
33
- // But we can't know address until we deploy.
34
- // Best effort: check local profiles for name collision?
35
- // Or rely on user?
36
- // "Preflight: if a SubDAO with same name/owner exists, skip create" - User Requirement
37
- // Factory events scan is too slow.
38
- // We will warn if we see a similar entry in .sage/subdaos.json.
39
- // And/Or if user passed --subdao override? No, create generates one.
40
- // We will skip this heavy scan and rely on "check-then-set" via create2 if possible, but Factory uses create.
41
- // For Operator mode, we can check if the Safe is already an admin of a SubDAO? Hard.
42
- // Let's implement a simple check: if file "subdao-plan-<timestamp>.json" exists? No.
43
- // User: "if a SubDAO with same name/owner exists".
44
- // We will skip if we find a local alias with same name.
45
- const SubDAOStore = require('../../utils/subdao-store');
46
- const store = new SubDAOStore();
47
- const existing = store.list().find(s => s.alias === plan.config.name);
48
- if (existing) {
49
- console.warn(`⚠️ SubDAO with name "${plan.config.name}" already exists locally (${existing.address}).`);
50
- // We proceed but warn.
51
- }
52
-
53
- // Stamping: Pin Playbook to IPFS
54
- console.log('📌 Stamping Playbook to IPFS...');
55
- let playbookCid = '';
56
- try {
57
- const ipfs = new IpfsManager();
58
- const res = await ipfs.upload(plan.playbook);
59
- playbookCid = res.cid;
60
- console.log(` CID: ${playbookCid}`);
61
- } catch (e) {
62
- console.warn(`⚠️ Failed to pin playbook: ${e.message}`);
63
- // Fail fast if provenance is critical? User said "No IPFS stamping... Fix: Stamping".
64
- // We should probably fail or retry.
65
- }
66
-
67
- // Convert accessModel string to int
68
- const accessMap = { 'free': 0, 'governance': 1, 'hybrid': 2 };
69
- const accessModel = accessMap[plan.config.accessModel || 'governance'] || 1;
70
-
71
- // Creation options
72
- const creationOpts = {
73
- quorumPercentage: plan.config.quorumBps ? BigInt(plan.config.quorumBps) / 100n : 4n,
74
- votingPeriodBlocks: plan.config.votingPeriodBlocks,
75
- proposalThreshold: plan.config.proposalThreshold,
76
- forkingPolicy: plan.config.forkingPolicy,
77
- membershipPolicy: plan.playbook.governance === 'operator' ? 'admin' : 'stake',
78
- profileCid: playbookCid // Stamp CID into profileCid field of SubDAO
79
- };
80
-
81
- if (plan.playbook.governance === 'operator') {
82
- console.log('🏗️ Creating Operator/Team SubDAO...');
83
-
84
- const factory = new ethers.Contract(manager.factoryAddress, FactoryABI, manager.signer);
85
-
86
- // Validate Factory Support (ABI check)
87
- if (typeof factory.createSubDAOOperator !== 'function') {
88
- throw new Error('Factory does not support createSubDAOOperator. Upgrade factory or use Community playbook.');
10
+ let plan;
11
+ if (typeof planOrPath === 'string') {
12
+ const content = fs.readFileSync(planOrPath, 'utf8');
13
+ plan = JSON.parse(content);
14
+ } else {
15
+ plan = planOrPath;
89
16
  }
90
17
 
91
- // Resolve Executor (Safe)
92
- let executor = plan.config.owners;
93
- if (!executor) executor = await manager.signer.getAddress();
94
-
95
- // If comma separated, warn and pick first.
96
- if (executor.includes(',')) {
97
- console.warn('⚠️ Multiple owners provided. Operator mode requires a SINGLE executor address (Safe or EOA).');
98
- const parts = executor.split(',');
99
- executor = parts[0].trim();
100
- console.warn(` Using first address: ${executor}`);
101
- }
18
+ console.log(`Applying Plan: ${plan.config.name} (${plan.playbook.name})`);
102
19
 
103
- // Enforce Safe (Code Check)
104
- const code = await provider.getCode(executor);
105
- if (code === '0x') {
106
- // EOA.
107
- console.warn('⚠️ Executor is an EOA (External Account), not a Safe Contract.');
108
- console.warn(' "Squad" playbook implies a Safe. Ensure you trust this single key.');
109
- // User requirement: "Require a Safe address... or a created Safe".
110
- // If playbook is 'squad', we MUST have a Safe.
111
- if (plan.playbook.id === 'squad') {
112
- throw new Error('Squad playbook requires a Safe contract address. Use "sage safe create" (if available) or create at app.safe.global and provide via --owners.');
113
- }
114
- } else {
115
- console.log('✅ Executor is a Contract (Likely Safe).');
20
+ // Instantiate manager
21
+ const manager = new SubDAOManager();
22
+ const WalletManager = require('../../wallet-manager');
23
+ const wm = new WalletManager();
24
+ await wm.connect();
25
+ manager.signer = wm.getSigner();
26
+ manager.walletManager = wm;
27
+ await manager.initialize();
28
+ const provider = wm.getProvider();
29
+
30
+ // Idempotency Check: Check if SubDAO with this name is already registered in Factory
31
+ // Or locally in subdao store.
32
+ const store = require('../../utils/subdao-store');
33
+ const existing = (store.listFavorites?.() || []).find(f => (f.alias || '').toLowerCase() === String(plan.config.name || '').toLowerCase());
34
+ if (existing) {
35
+ console.warn(`⚠️ SubDAO with name "${plan.config.name}" already exists locally (${existing.address}).`);
36
+ // We proceed but warn.
116
37
  }
117
38
 
118
- console.log(` Executor: ${executor}`);
119
-
39
+ // Stamping: Pin Playbook to IPFS
40
+ console.log('📌 Stamping Playbook to IPFS...');
41
+ let playbookCid = '';
120
42
  try {
121
- // Stamping? createSubDAOOperator arguments:
122
- // name, desc, accessModel, executor, admin, minStake, burnAmount
123
- // It lacks profileCid.
124
- // We must call setProfileCid AFTER creation if factory doesn't support it.
125
- // OR use createSubDAOOperatorWithParams if available?
126
- // Assuming standard factory.
127
-
128
- const tx = await factory.createSubDAOOperator(
129
- plan.config.name,
130
- plan.config.description,
131
- accessModel,
132
- executor, // executor (Timelock role)
133
- executor, // admin (SubDAO admin)
134
- plan.config.minStake || 0,
135
- plan.config.burnAmount || 0
136
- );
137
- console.log('⏳ Waiting for tx:', tx.hash);
138
- const receipt = await tx.wait();
139
-
140
- // Parse logs to find SubDAO address
141
- // Event: SubDAOCreated(address indexed subDAO, ...)
142
- // We reuse SubDAOManager logic or parse manually
143
- let subdaoAddr = null;
144
- for (const log of receipt.logs) {
145
- try {
146
- const parsed = factory.interface.parseLog(log);
147
- if (parsed.name === 'SubDAOCreated') {
148
- subdaoAddr = parsed.args[0];
149
- break;
150
- }
151
- } catch(e){}
152
- }
153
-
154
- if (subdaoAddr) {
155
- console.log(`✅ SubDAO created: ${subdaoAddr}`);
156
-
157
- // Post-Create Stamping (if CID exists)
158
- if (playbookCid) {
159
- console.log('📌 Applying Playbook Stamp (Profile CID)...');
160
- // We need to call setProfileCid via the Admin.
161
- // If WE (signer) are not the admin (Safe is), we can't directly set it.
162
- // We must PROPOSE it to the Safe if we are an owner.
163
- // Or assume user will do it.
164
- // "Fix: ...stamp the playbook manifest CID into SubDAO metadata."
165
- // If executor == Safe, we can't easily set it now synchronously.
166
- // We warn and output the instruction.
167
-
168
- // Check if setProfileCid exists on SubDAO contract
169
- const subdao = new ethers.Contract(subdaoAddr, ['function setProfileCid(string)'], manager.signer);
170
- // Checking function existence without call is hard on ethers v6 without ABI check or call.
171
- // We can try to estimateGas or call static.
172
- // Or check code? No.
173
- // Check if ABI fragment matches?
174
- // If standard SubDAO doesn't have it, we skip.
175
- // Let's try to see if `setProfileCid` is in the contract by calling it? No, that reverts.
176
- // We rely on the artifact/ABI we used.
177
- // If the artifact has it, we try. If it reverts (method missing), we catch.
178
-
179
- try {
180
- if (typeof subdao.setProfileCid === 'function') {
181
- if (executor.toLowerCase() === (await manager.signer.getAddress()).toLowerCase()) {
182
- // We are admin
183
- const tx2 = await subdao.setProfileCid(playbookCid);
184
- await tx2.wait();
185
- console.log('✅ Stamped.');
186
- } else {
187
- console.warn('⚠️ Cannot stamp profileCid automatically (Admin is Safe/Other).');
188
- console.warn(` Please propose call: setProfileCid("${playbookCid}") on ${subdaoAddr}`);
189
- }
190
- } else {
191
- console.warn('⚠️ SubDAO contract does not support setProfileCid.');
192
- }
193
- } catch (e) {
194
- console.warn('⚠️ Failed to stamp profileCid (likely not supported by contract):', e.message);
195
- }
196
- }
197
- }
198
-
43
+ const ipfs = new IpfsManager();
44
+ const res = await ipfs.uploadJson(plan.playbook, 'playbook');
45
+ playbookCid = res;
46
+ console.log(` CID: ${playbookCid}`);
199
47
  } catch (e) {
200
- console.error('❌ Operator creation failed:', e.message);
201
- throw e;
48
+ console.warn(`⚠️ Failed to pin playbook: ${e.message}`);
49
+ // Fail fast if provenance is critical? User said "No IPFS stamping... Fix: Stamping".
50
+ // We should probably fail or retry.
202
51
  }
203
52
 
204
- } else {
205
- // Token Governance (Community)
206
- await manager.createSubDAO(
207
- plan.config.name,
208
- plan.config.description,
209
- accessModel,
210
- plan.config.minStake || '0',
211
- plan.config.burnAmount || '0',
212
- creationOpts
213
- );
214
- }
53
+ // Convert accessModel string to int
54
+ const accessMap = { 'free': 0, 'governance': 1, 'hybrid': 2 };
55
+ const accessModel = accessMap[plan.config.accessModel || 'governance'] || 1;
56
+
57
+ // Creation options
58
+ const creationOpts = {
59
+ quorumPercentage: plan.config.quorumBps ? BigInt(plan.config.quorumBps) / 100n : 4n,
60
+ votingPeriodBlocks: plan.config.votingPeriodBlocks,
61
+ proposalThreshold: plan.config.proposalThreshold || '0',
62
+ forkingPolicy: plan.config.forkingPolicy || 'gov_only',
63
+ membershipPolicy: plan.playbook.governance === 'operator' ? 'admin' : 'stake',
64
+ profileCid: playbookCid // Stamp CID into profileCid field of SubDAO
65
+ };
66
+
67
+ if (plan.playbook.governance === 'operator') {
68
+ console.log('🏗️ Creating Operator/Team SubDAO...');
69
+
70
+ const factory = new ethers.Contract(manager.factoryAddress, FactoryABI, manager.signer);
71
+
72
+ // Validate Factory Support (ABI check)
73
+ if (typeof factory.createSubDAOOperator !== 'function') {
74
+ throw new Error('Factory does not support createSubDAOOperator. Upgrade factory or use Community playbook.');
75
+ }
76
+
77
+ // Resolve Executor (Safe)
78
+ let executor = plan.config.owners;
79
+ if (!executor) executor = await manager.signer.getAddress();
80
+
81
+ // If comma separated, warn and pick first.
82
+ if (executor.includes(',')) {
83
+ console.warn('⚠️ Multiple owners provided. Operator mode requires a SINGLE executor address (Safe or EOA).');
84
+ const parts = executor.split(',');
85
+ executor = parts[0].trim();
86
+ console.warn(` Using first address: ${executor}`);
87
+ }
88
+
89
+ // Enforce Safe (Code Check)
90
+ const code = await provider.getCode(executor);
91
+ if (code === '0x') {
92
+ // EOA.
93
+ console.warn('⚠️ Executor is an EOA (External Account), not a Safe Contract.');
94
+ console.warn(' "Squad" playbook implies a Safe. Ensure you trust this single key.');
95
+ // User requirement: "Require a Safe address... or a created Safe".
96
+ // If playbook is 'squad', we MUST have a Safe.
97
+ if (plan.playbook.id === 'squad') {
98
+ throw new Error('Squad playbook requires a Safe contract address. Use "sage safe create" (if available) or create at app.safe.global and provide via --owners.');
99
+ }
100
+ } else {
101
+ console.log('✅ Executor is a Contract (Likely Safe).');
102
+ }
103
+
104
+ console.log(` Executor: ${executor}`);
105
+
106
+ try {
107
+ // Check for createSubDAOOperatorWithParams (Prioritize Factory Path)
108
+ const hasNewOperatorCreate = factory.interface.fragments.some(
109
+ f => f.name === 'createSubDAOOperatorWithParams'
110
+ );
111
+
112
+ let tx;
113
+ if (hasNewOperatorCreate) {
114
+ console.log('✨ Using factory.createSubDAOOperatorWithParams (stamping supported)...');
115
+ tx = await factory.createSubDAOOperatorWithParams(
116
+ plan.config.name,
117
+ plan.config.description,
118
+ accessModel,
119
+ executor, // executor
120
+ executor, // admin
121
+ plan.config.minStake || 0,
122
+ plan.config.burnAmount || 0,
123
+ playbookCid || ''
124
+ );
125
+ } else {
126
+ console.log('⚠️ Factory does not support atomic stamping. Using legacy createSubDAOOperator...');
127
+ tx = await factory.createSubDAOOperator(
128
+ plan.config.name,
129
+ plan.config.description,
130
+ accessModel,
131
+ executor, // executor
132
+ executor, // admin
133
+ plan.config.minStake || 0,
134
+ plan.config.burnAmount || 0
135
+ );
136
+ }
137
+
138
+ console.log('⏳ Waiting for tx:', tx.hash);
139
+ const receipt = await tx.wait();
140
+
141
+ // Parse logs to find SubDAO address
142
+ let subdaoAddr = null;
143
+ for (const log of receipt.logs) {
144
+ try {
145
+ const parsed = factory.interface.parseLog(log);
146
+ if (parsed.name === 'SubDAOCreated') {
147
+ subdaoAddr = parsed.args[0];
148
+ break;
149
+ }
150
+ } catch (e) { }
151
+ }
152
+
153
+ if (subdaoAddr) {
154
+ console.log(`✅ SubDAO created: ${subdaoAddr}`);
155
+
156
+ // Post-Create Stamping (only if NOT stamped via params)
157
+ if (playbookCid && !hasNewOperatorCreate) {
158
+ console.log('📌 Applying Playbook Stamp (Profile CID)...');
159
+
160
+ const subdao = new ethers.Contract(subdaoAddr, ['function setProfileCid(string)'], manager.signer);
161
+
162
+ try {
163
+ // Check if we are the admin (executor)
164
+ const myAddr = await manager.signer.getAddress();
165
+ if (executor.toLowerCase() === myAddr.toLowerCase()) {
166
+ // We are admin
167
+ console.log(' Directly calling setProfileCid...');
168
+ const tx2 = await subdao.setProfileCid(playbookCid);
169
+ await tx2.wait();
170
+ console.log('✅ Stamped.');
171
+ } else {
172
+ console.warn('⚠️ Cannot stamp profileCid automatically (Admin is Safe/Other).');
173
+
174
+ // Generate Safe Payload (Post-Create Fallback)
175
+ const iface = new ethers.Interface(['function setProfileCid(string)']);
176
+ const calldata = iface.encodeFunctionData('setProfileCid', [playbookCid]);
177
+
178
+ const safeBatch = {
179
+ version: "1.0",
180
+ chainId: (await provider.getNetwork()).chainId.toString(),
181
+ createdAt: Date.now(),
182
+ meta: { name: "Stamp Playbook CID", description: `Set profileCid to ${playbookCid}` },
183
+ transactions: [{
184
+ to: subdaoAddr,
185
+ value: "0",
186
+ data: calldata,
187
+ operation: 0 // Call
188
+ }]
189
+ };
190
+
191
+ const filename = `safe-tx-stamp-${Date.now()}.json`;
192
+ fs.writeFileSync(filename, JSON.stringify(safeBatch, null, 2));
193
+
194
+ console.log(`\n📋 Safe Transaction Payload saved to ${filename}`);
195
+ console.log(JSON.stringify(safeBatch, null, 2));
196
+ console.log('\n Use this JSON in the Safe Transaction Builder to stamp the playbook.');
197
+ }
198
+ } catch (e) {
199
+ console.warn('⚠️ Failed to stamp profileCid:', e.message);
200
+ console.warn(' This SubDAO contract version might not support setProfileCid.');
201
+ }
202
+ }
203
+ }
204
+
205
+ } catch (e) {
206
+ console.error('❌ Operator creation failed:', e.message);
207
+ throw e;
208
+ }
209
+
210
+ } else {
211
+ // Token Governance (Community)
212
+ // Community playbook uses createSubDAO (legacy) or createSubDAOWithParams (new) logic.
213
+ // If token mode, we usually rely on `createSubDAO` which calls internal logic.
214
+ // But check if we can stamp there too.
215
+ // The `creationOpts` already has `profileCid`.
216
+ // If SubDAOManager.createSubDAO supports it, we are good.
217
+
218
+ await manager.createSubDAO(
219
+ plan.config.name,
220
+ plan.config.description,
221
+ accessModel,
222
+ plan.config.minStake || '0',
223
+ plan.config.burnAmount || '0',
224
+ creationOpts // Ensure manager uses this
225
+ );
226
+ }
215
227
  }
216
228
 
217
229
  module.exports = { applyPlan };
@@ -2,6 +2,8 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const inquirer = require('inquirer');
4
4
  const playbooksService = require('../governance/playbooks');
5
+ const { ethers } = require('ethers');
6
+ const config = require('../../config');
5
7
 
6
8
  function parseDurationToBlocks(durationStr, blockTimeSec = 12) {
7
9
  if (!durationStr) return Math.ceil(3 * 24 * 3600 / blockTimeSec); // Default ~3 days
@@ -27,6 +29,25 @@ function parseDurationToBlocks(durationStr, blockTimeSec = 12) {
27
29
  return Math.ceil(seconds / blockTimeSec);
28
30
  }
29
31
 
32
+ async function detectBlockTimeSec(rpcUrl) {
33
+ try {
34
+ const provider = new ethers.JsonRpcProvider(rpcUrl);
35
+ const latest = await provider.getBlockNumber();
36
+ const older = latest > 100 ? latest - 100 : (latest > 1 ? 1 : 0);
37
+ const [bLatest, bOlder] = await Promise.all([
38
+ provider.getBlock(latest),
39
+ provider.getBlock(older),
40
+ ]);
41
+ if (!bLatest || !bOlder || !bLatest.timestamp || !bOlder.timestamp) return 12;
42
+ const dt = Number(bLatest.timestamp) - Number(bOlder.timestamp);
43
+ if (dt <= 0) return 12;
44
+ const sec = Math.max(1, Math.round(dt / Math.max(1, latest - older)));
45
+ return sec;
46
+ } catch (_) {
47
+ return 12;
48
+ }
49
+ }
50
+
30
51
  async function planSubDAO(options) {
31
52
  // 1. Select Playbook
32
53
  let playbookId = options.playbook;
@@ -69,6 +90,13 @@ async function planSubDAO(options) {
69
90
  questions.push({ type: 'input', name: 'minStake', message: 'Minimum Stake (SXXX):', default: '0' });
70
91
  }
71
92
 
93
+ // Governance tuning (overrides) — prompt only in wizard mode or when overrides are missing
94
+ if (options.wizard) {
95
+ if (!options.votingPeriod) questions.push({ type: 'input', name: 'votingPeriod', message: 'Voting period (e.g., 7 days):', default: (playbook.params?.votingPeriod || '') });
96
+ if (!options.quorumBps) questions.push({ type: 'input', name: 'quorumBps', message: 'Quorum (bps, e.g., 200 = 2%):', default: (playbook.params?.quorumBps ?? '') });
97
+ if (!options.proposalThreshold) questions.push({ type: 'input', name: 'proposalThreshold', message: 'Proposal threshold (string/wei):', default: (playbook.params?.proposalThreshold || '') });
98
+ }
99
+
72
100
  const answers = questions.length > 0 ? await inquirer.prompt(questions) : {};
73
101
 
74
102
  const merged = { ...options, ...answers };
@@ -87,18 +115,25 @@ async function planSubDAO(options) {
87
115
  name: merged.name,
88
116
  description: merged.description,
89
117
  accessModel: 'governance',
90
- minStake: merged.minStake,
118
+ minStake: merged.minStake || '0',
91
119
  owners: merged.owners,
92
120
  threshold: merged.threshold,
93
- proposalThreshold: playbook.params.proposalThreshold,
94
- votingPeriod: playbook.params.votingPeriod,
95
- quorumBps: playbook.params.quorumBps,
121
+ // Default to 1000 SXXX (minimum burn) if not provided
122
+ burnAmount: merged.burnAmount || '1000000000000000000000',
123
+ proposalThreshold: merged.proposalThreshold || playbook.params?.proposalThreshold || '0',
124
+ votingPeriod: merged.votingPeriod || playbook.params?.votingPeriod || '3 days',
125
+ quorumBps: merged.quorumBps != null && merged.quorumBps !== '' ? Number(merged.quorumBps) : (playbook.params?.quorumBps ?? 400),
96
126
  forkingPolicy: 'gov_only',
97
127
  }
98
128
  };
99
129
 
100
- // Normalize voting period to blocks with override
101
- const blockTime = options.blockTimeSec ? parseInt(options.blockTimeSec, 10) : 12;
130
+ // Normalize voting period to blocks with override or auto-detect
131
+ let blockTime = 12;
132
+ if (options.blockTimeSec) blockTime = parseInt(options.blockTimeSec, 10);
133
+ else {
134
+ const rpc = config.resolveRpcUrl ? config.resolveRpcUrl() : (process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
135
+ blockTime = await detectBlockTimeSec(rpc);
136
+ }
102
137
  plan.config.votingPeriodBlocks = parseDurationToBlocks(plan.config.votingPeriod, blockTime).toString();
103
138
 
104
139
  return plan;
@@ -15,6 +15,7 @@ const GAS_FLAGS = '--legacy --gas-limit 3000000 --gas-price 2gwei';
15
15
  const { resolvePermitOrApprove, normalizePermit, zeroPermit } = require('./utils/permit');
16
16
  const { ethers } = require('ethers');
17
17
  const FactoryABI = require('./utils/factory-abi');
18
+ const { decodeContractError } = require('./utils/contract-error-decoder');
18
19
 
19
20
  const DEFAULT_TIMELOCK_MIN_DELAY = process.env.SAGE_TIMELOCK_MIN_DELAY_SECONDS || '1';
20
21
  const DEFAULT_TIMELOCK_DELAY_FLOOR =
@@ -31,6 +32,19 @@ function toBigInt(value) {
31
32
 
32
33
  function formatFactoryCreationError(err) {
33
34
  if (!err) return 'Unknown error';
35
+
36
+ // Try precise decoding first
37
+ try {
38
+ const iface = new ethers.Interface(FactoryABI);
39
+ const decoded = decodeContractError(err, iface);
40
+ if (decoded && decoded !== 'Unknown Error' && !decoded.startsWith('Transaction failed')) {
41
+ const reason = decoded;
42
+ if (reason.includes('BurnOutOfBounds')) return `${reason} (Burn amount is outside the allowed range)`;
43
+ if (reason.includes('InsufficientSXXXBalance')) return `${reason} (Check SXXX balance and allowance)`;
44
+ return reason;
45
+ }
46
+ } catch (_) {}
47
+
34
48
  const name = err.error?.errorName || err.errorName || '';
35
49
  const reason = err.reason || err.shortMessage || err.message || '';
36
50
  const details = reason || name;
@@ -19,6 +19,7 @@ const COMMAND_CATALOG = {
19
19
  needsSubcommand: true,
20
20
  subcommands: [
21
21
  'create',
22
+ 'create-playbook',
22
23
  'create-template',
23
24
  'list',
24
25
  'list-all',
@@ -50,14 +51,20 @@ const COMMAND_CATALOG = {
50
51
  'info',
51
52
  'list',
52
53
  'vote',
54
+ 'vote-with-reason',
53
55
  'queue',
54
56
  'execute',
55
57
  'status',
56
58
  'inspect',
57
59
  'watch',
58
60
  'power',
61
+ 'preflight',
59
62
  'doctor',
60
- 'propose'
63
+ 'propose',
64
+ 'propose-custom',
65
+ 'list-all-subdaos',
66
+ 'wait',
67
+ 'wizard'
61
68
  ],
62
69
  subcommandAliases: {
63
70
  ls: 'list',
@@ -71,6 +78,11 @@ const COMMAND_CATALOG = {
71
78
  needsSubcommand: true,
72
79
  subcommands: [
73
80
  'push',
81
+ 'template',
82
+ 'add-prompt',
83
+ 'preview',
84
+ 'examples',
85
+ 'propose-import',
74
86
  'cache',
75
87
  'check',
76
88
  'check',
@@ -89,7 +101,10 @@ const COMMAND_CATALOG = {
89
101
  subcommandAliases: {
90
102
  ls: 'status',
91
103
  status: 'status',
92
- propose: 'propose'
104
+ tpl: 'template',
105
+ add: 'add-prompt',
106
+ view: 'preview',
107
+ ex: 'examples'
93
108
  }
94
109
  },
95
110
  prompt: {
@@ -0,0 +1,61 @@
1
+ const { ethers } = require('ethers');
2
+
3
+ /**
4
+ * Attempts to decode a contract error using provided interfaces.
5
+ * @param {Error} error - The error object caught from ethers.js
6
+ * @param {ethers.Interface|ethers.Contract|Array<ethers.Interface|ethers.Contract>} contractsOrInterfaces - Context to decode against
7
+ * @returns {string} - Human readable error message or original message
8
+ */
9
+ function decodeContractError(error, contractsOrInterfaces = []) {
10
+ if (!error) return 'Unknown Error';
11
+
12
+ // Normalize inputs
13
+ const contexts = Array.isArray(contractsOrInterfaces) ? contractsOrInterfaces : [contractsOrInterfaces];
14
+ const interfaces = contexts.map(c => {
15
+ if (c instanceof ethers.Interface) return c;
16
+ if (c.interface instanceof ethers.Interface) return c.interface;
17
+ return null;
18
+ }).filter(Boolean);
19
+
20
+ // Extract data
21
+ // Ethers v6 often puts the revert data in error.data, error.info.error.data, or error.revert.data
22
+ let data = error.data;
23
+ if (!data && error.info?.error?.data) data = error.info.error.data;
24
+ if (!data && error.revert?.data) data = error.revert.data;
25
+ if (!data && typeof error.message === 'string') {
26
+ // Sometimes it's buried in the message string: "execution reverted: ..." or hex
27
+ // But parsing message is brittle.
28
+ }
29
+
30
+ // If we have data, try to decode
31
+ if (data && data !== '0x') {
32
+ for (const iface of interfaces) {
33
+ try {
34
+ const parsed = iface.parseError(data);
35
+ if (parsed) {
36
+ // Format: "ErrorName(arg1, arg2)"
37
+ const args = parsed.args ? Array.from(parsed.args).join(', ') : '';
38
+ return `Reverted: ${parsed.name}(${args})`;
39
+ }
40
+ } catch (e) {
41
+ // Ignore parse errors, try next interface
42
+ }
43
+ }
44
+ }
45
+
46
+ // Fallback: check for string revert reasons in the error object
47
+ if (error.reason) return `Reverted: ${error.reason}`;
48
+ if (error.shortMessage) return `Error: ${error.shortMessage}`;
49
+ if (error.message) {
50
+ // Clean up common noise
51
+ if (error.message.includes('execution reverted')) {
52
+ return error.message.split('execution reverted')[1].trim() || 'Execution Reverted';
53
+ }
54
+ return error.message;
55
+ }
56
+
57
+ return 'Transaction failed with unknown error';
58
+ }
59
+
60
+ module.exports = { decodeContractError };
61
+