@sage-protocol/cli 0.2.9 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/cli/commands/config.js +28 -0
  2. package/dist/cli/commands/doctor.js +87 -8
  3. package/dist/cli/commands/gov-config.js +81 -0
  4. package/dist/cli/commands/governance.js +152 -72
  5. package/dist/cli/commands/library.js +9 -0
  6. package/dist/cli/commands/proposals.js +187 -17
  7. package/dist/cli/commands/skills.js +737 -0
  8. package/dist/cli/commands/subdao.js +96 -132
  9. package/dist/cli/config/playbooks.json +62 -0
  10. package/dist/cli/config.js +15 -0
  11. package/dist/cli/governance-manager.js +25 -4
  12. package/dist/cli/index.js +6 -7
  13. package/dist/cli/library-manager.js +79 -0
  14. package/dist/cli/mcp-server-stdio.js +1387 -166
  15. package/dist/cli/schemas/manifest.schema.json +55 -0
  16. package/dist/cli/services/doctor/fixers.js +134 -0
  17. package/dist/cli/services/governance/doctor.js +140 -0
  18. package/dist/cli/services/governance/playbooks.js +97 -0
  19. package/dist/cli/services/mcp/bulk-operations.js +272 -0
  20. package/dist/cli/services/mcp/dependency-analyzer.js +202 -0
  21. package/dist/cli/services/mcp/library-listing.js +2 -2
  22. package/dist/cli/services/mcp/local-prompt-collector.js +1 -0
  23. package/dist/cli/services/mcp/manifest-downloader.js +5 -3
  24. package/dist/cli/services/mcp/manifest-fetcher.js +17 -1
  25. package/dist/cli/services/mcp/manifest-workflows.js +127 -15
  26. package/dist/cli/services/mcp/quick-start.js +287 -0
  27. package/dist/cli/services/mcp/stdio-runner.js +30 -5
  28. package/dist/cli/services/mcp/template-manager.js +156 -0
  29. package/dist/cli/services/mcp/templates/default-templates.json +84 -0
  30. package/dist/cli/services/mcp/tool-args-validator.js +56 -0
  31. package/dist/cli/services/mcp/trending-formatter.js +1 -1
  32. package/dist/cli/services/mcp/unified-prompt-search.js +2 -2
  33. package/dist/cli/services/metaprompt/designer.js +12 -5
  34. package/dist/cli/services/skills/discovery.js +99 -0
  35. package/dist/cli/services/subdao/applier.js +229 -0
  36. package/dist/cli/services/subdao/planner.js +142 -0
  37. package/dist/cli/subdao-manager.js +14 -0
  38. package/dist/cli/utils/aliases.js +28 -6
  39. package/dist/cli/utils/contract-error-decoder.js +61 -0
  40. package/dist/cli/utils/suggestions.js +25 -13
  41. package/package.json +3 -2
  42. package/src/schemas/manifest.schema.json +55 -0
@@ -57,7 +57,7 @@ class MetapromptDesigner {
57
57
  const history = this.path.join(base, 'history');
58
58
  try {
59
59
  this.fs.mkdirSync(history, { recursive: true });
60
- } catch (_) {}
60
+ } catch (_) { }
61
61
  return history;
62
62
  }
63
63
 
@@ -103,7 +103,7 @@ class MetapromptDesigner {
103
103
  if (value.startsWith('[') && value.endsWith(']')) {
104
104
  try {
105
105
  value = JSON.parse(value.replace(/'/g, '"'));
106
- } catch (_) {}
106
+ } catch (_) { }
107
107
  }
108
108
  meta[key] = value;
109
109
  });
@@ -197,9 +197,10 @@ class MetapromptDesigner {
197
197
  goal = 'Design a system prompt for an AI assistant that specialises in Sage Protocol development and prompt operations.',
198
198
  model = 'gpt-5',
199
199
  interviewStyle = 'one-question-at-a-time',
200
+ additionalInstructions = '',
200
201
  } = options;
201
202
 
202
- return [
203
+ const lines = [
203
204
  'You are facilitating a metaprompt interview.',
204
205
  'Interview me one question at a time.',
205
206
  'Use each answer to decide the very next question.',
@@ -211,13 +212,19 @@ class MetapromptDesigner {
211
212
  'Offer optional follow-up artifacts (e.g., user prompt template, evaluation checklist).',
212
213
  'End by suggesting how to save or launch the prompt (e.g., via ChatGPT link).',
213
214
  `Interview cadence preference: ${interviewStyle}.`,
214
- ].join('\n');
215
+ ];
216
+
217
+ if (additionalInstructions) {
218
+ lines.push(additionalInstructions);
219
+ }
220
+
221
+ return lines.join('\n');
215
222
  }
216
223
 
217
224
  saveTranscript(transcriptPath, payload) {
218
225
  try {
219
226
  this.fs.mkdirSync(this.path.dirname(transcriptPath), { recursive: true });
220
- } catch (_) {}
227
+ } catch (_) { }
221
228
  this.fs.writeFileSync(transcriptPath, JSON.stringify(payload, null, 2));
222
229
  }
223
230
  }
@@ -0,0 +1,99 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ function readFrontmatter(raw) {
5
+ const meta = { title: null, summary: null, tags: [], targets: [] };
6
+ if (!raw || typeof raw !== 'string') return { meta, body: raw || '' };
7
+ if (!raw.startsWith('---')) return { meta, body: raw };
8
+ const end = raw.indexOf('\n---', 3);
9
+ if (end === -1) return { meta, body: raw };
10
+ const header = raw.slice(3, end).split(/\r?\n/);
11
+ for (const line of header) {
12
+ const idx = line.indexOf(':');
13
+ if (idx === -1) continue;
14
+ const k = line.slice(0, idx).trim().toLowerCase();
15
+ let v = line.slice(idx + 1).trim();
16
+ if (!v) continue;
17
+ if (k === 'title' || k === 'name') {
18
+ meta.title = v;
19
+ } else if (k === 'summary' || k === 'description') {
20
+ meta.summary = v;
21
+ } else if (k === 'tags' || k === 'targets') {
22
+ try {
23
+ const arr = JSON.parse(v.replace(/'/g, '"'));
24
+ if (Array.isArray(arr)) meta[k] = arr;
25
+ } catch (_) {
26
+ const arr = String(v)
27
+ .split(/[,|\s]+/)
28
+ .filter(Boolean);
29
+ meta[k] = arr;
30
+ }
31
+ }
32
+ }
33
+ const body = raw.slice(end + 4);
34
+ return { meta, body };
35
+ }
36
+
37
+ function walk(dir, out) {
38
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
39
+ for (const entry of entries) {
40
+ const full = path.join(dir, entry.name);
41
+ if (entry.isDirectory()) walk(full, out);
42
+ else out.push(full);
43
+ }
44
+ }
45
+
46
+ function findWorkspaceSkills({ promptsDir = 'prompts' } = {}) {
47
+ const root = path.join(process.cwd(), promptsDir, 'skills');
48
+ const results = [];
49
+ if (!fs.existsSync(root)) return results;
50
+
51
+ const files = [];
52
+ walk(root, files);
53
+
54
+ for (const filePath of files) {
55
+ const relFromPrompts = path.relative(path.join(process.cwd(), promptsDir), filePath).replace(/\\/g, '/');
56
+ const isDirSkill = /\/SKILL\.md$/i.test(relFromPrompts) && /(^|\/)skills\//.test(relFromPrompts);
57
+ const isFlatMd = /(^|\/)skills\//.test(relFromPrompts) && filePath.toLowerCase().endsWith('.md') && !/\/skill\.md$/i.test(filePath);
58
+ if (!isDirSkill && !isFlatMd) continue;
59
+
60
+ const raw = fs.readFileSync(filePath, 'utf8');
61
+ const { meta } = readFrontmatter(raw);
62
+ const nameGuess = isDirSkill
63
+ ? path.basename(path.dirname(filePath))
64
+ : path.basename(filePath, '.md');
65
+ const name = meta.title || nameGuess;
66
+ const summary = meta.summary || '';
67
+ const tags = Array.isArray(meta.tags) ? meta.tags : [];
68
+ const key = isDirSkill
69
+ ? relFromPrompts.replace(/\/SKILL\.md$/i, '')
70
+ : relFromPrompts.replace(/\.md$/i, '');
71
+ const baseDir = isDirSkill ? path.dirname(filePath) : path.dirname(filePath);
72
+ results.push({
73
+ key,
74
+ name,
75
+ summary,
76
+ tags,
77
+ path: filePath,
78
+ baseDir,
79
+ type: isDirSkill ? 'directory' : 'flat',
80
+ });
81
+ }
82
+ return results;
83
+ }
84
+
85
+ function resolveSkillFileByKey({ promptsDir = 'prompts', key }) {
86
+ const normalized = String(key || '').replace(/^\/+/, '').replace(/\.md$/i, '');
87
+ const flat = path.join(process.cwd(), promptsDir, `${normalized}.md`);
88
+ const dirSkill = path.join(process.cwd(), promptsDir, normalized, 'SKILL.md');
89
+ if (fs.existsSync(flat)) return { path: flat, baseDir: path.dirname(flat), type: 'flat' };
90
+ if (fs.existsSync(dirSkill)) return { path: dirSkill, baseDir: path.dirname(dirSkill), type: 'directory' };
91
+ return null;
92
+ }
93
+
94
+ module.exports = {
95
+ readFrontmatter,
96
+ findWorkspaceSkills,
97
+ resolveSkillFileByKey,
98
+ };
99
+
@@ -0,0 +1,229 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const SubDAOManager = require('../../subdao-manager');
4
+ const IpfsManager = require('../../ipfs-manager');
5
+ const { ethers } = require('ethers');
6
+ const FactoryABI = require('../../utils/factory-abi');
7
+ const { resolveArtifact } = require('../../utils/artifacts');
8
+
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
+ 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.
37
+ }
38
+
39
+ // Stamping: Pin Playbook to IPFS
40
+ console.log('📌 Stamping Playbook to IPFS...');
41
+ let playbookCid = '';
42
+ try {
43
+ const ipfs = new IpfsManager();
44
+ const res = await ipfs.uploadJson(plan.playbook, 'playbook');
45
+ playbookCid = res;
46
+ console.log(` CID: ${playbookCid}`);
47
+ } catch (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.
51
+ }
52
+
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
+ }
227
+ }
228
+
229
+ module.exports = { applyPlan };
@@ -0,0 +1,142 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const inquirer = require('inquirer');
4
+ const playbooksService = require('../governance/playbooks');
5
+ const { ethers } = require('ethers');
6
+ const config = require('../../config');
7
+
8
+ function parseDurationToBlocks(durationStr, blockTimeSec = 12) {
9
+ if (!durationStr) return Math.ceil(3 * 24 * 3600 / blockTimeSec); // Default ~3 days
10
+
11
+ const str = String(durationStr).toLowerCase().trim();
12
+
13
+ // If already integer
14
+ if (/^\d+$/.test(str)) return parseInt(str, 10);
15
+
16
+ const match = str.match(/^(\d+)\s*(d|days?|h|hours?|m|minutes?|blocks?)$/);
17
+ if (!match) return Math.ceil(3 * 24 * 3600 / blockTimeSec); // Fallback
18
+
19
+ const val = parseInt(match[1], 10);
20
+ const unit = match[2];
21
+
22
+ if (unit.startsWith('block')) return val;
23
+
24
+ let seconds = 0;
25
+ if (unit.startsWith('d')) seconds = val * 24 * 3600;
26
+ else if (unit.startsWith('h')) seconds = val * 3600;
27
+ else if (unit.startsWith('m')) seconds = val * 60;
28
+
29
+ return Math.ceil(seconds / blockTimeSec);
30
+ }
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
+
51
+ async function planSubDAO(options) {
52
+ // 1. Select Playbook
53
+ let playbookId = options.playbook;
54
+ if (!playbookId && !options.yes) {
55
+ const playbooks = playbooksService.listPlaybooks();
56
+ const choices = playbooks.map(p => ({
57
+ name: `${p.name} - ${p.description}`,
58
+ value: p.id
59
+ }));
60
+
61
+ const ans = await inquirer.prompt([{
62
+ type: 'list',
63
+ name: 'playbookId',
64
+ message: 'Select a Governance Playbook:',
65
+ choices
66
+ }]);
67
+ playbookId = ans.playbookId;
68
+ }
69
+
70
+ if (!playbookId) throw new Error('Playbook is required');
71
+
72
+ const playbook = playbooksService.getPlaybook(playbookId);
73
+ if (!playbook) throw new Error(`Unknown playbook: ${playbookId}`);
74
+
75
+ console.log(`\nUsing Playbook: ${playbook.name} (${playbook.version})\n`);
76
+
77
+ // 2. Gather Inputs (merged with playbook defaults)
78
+ const questions = [];
79
+ if (!options.name) questions.push({ type: 'input', name: 'name', message: 'SubDAO Name:' });
80
+ if (!options.description) questions.push({ type: 'input', name: 'description', message: 'Description:' });
81
+
82
+ // Playbook specific inputs
83
+ if (playbook.governance === 'operator' && !options.owners) {
84
+ questions.push({ type: 'input', name: 'owners', message: 'Safe Owners (comma separated):', default: options.owners });
85
+ questions.push({ type: 'input', name: 'threshold', message: 'Safe Threshold:', default: '1' });
86
+ }
87
+
88
+ // Token governance inputs
89
+ if (playbook.governance === 'token' && !options.minStake) {
90
+ questions.push({ type: 'input', name: 'minStake', message: 'Minimum Stake (SXXX):', default: '0' });
91
+ }
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
+
100
+ const answers = questions.length > 0 ? await inquirer.prompt(questions) : {};
101
+
102
+ const merged = { ...options, ...answers };
103
+
104
+ // 3. Construct Plan
105
+ const plan = {
106
+ version: '1.0.0',
107
+ timestamp: new Date().toISOString(),
108
+ playbook: {
109
+ id: playbook.id,
110
+ version: playbook.version,
111
+ name: playbook.name,
112
+ governance: playbook.governance
113
+ },
114
+ config: {
115
+ name: merged.name,
116
+ description: merged.description,
117
+ accessModel: 'governance',
118
+ minStake: merged.minStake || '0',
119
+ owners: merged.owners,
120
+ threshold: merged.threshold,
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),
126
+ forkingPolicy: 'gov_only',
127
+ }
128
+ };
129
+
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
+ }
137
+ plan.config.votingPeriodBlocks = parseDurationToBlocks(plan.config.votingPeriod, blockTime).toString();
138
+
139
+ return plan;
140
+ }
141
+
142
+ module.exports = { planSubDAO, parseDurationToBlocks };
@@ -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,18 +78,33 @@ const COMMAND_CATALOG = {
71
78
  needsSubcommand: true,
72
79
  subcommands: [
73
80
  'push',
74
- 'propose',
75
- 'status',
81
+ 'template',
82
+ 'add-prompt',
83
+ 'preview',
84
+ 'examples',
85
+ 'propose-import',
76
86
  'cache',
77
87
  'check',
78
- 'lint',
88
+ 'check',
89
+ 'doctor',
79
90
  'diff',
80
- 'doctor'
91
+ 'execute',
92
+ 'latest-prompts',
93
+ 'lint',
94
+ 'list',
95
+ 'propose',
96
+ 'search',
97
+ 'status',
98
+ 'sync-alt',
99
+ 'view-prompts'
81
100
  ],
82
101
  subcommandAliases: {
83
102
  ls: 'status',
84
103
  status: 'status',
85
- propose: 'propose'
104
+ tpl: 'template',
105
+ add: 'add-prompt',
106
+ view: 'preview',
107
+ ex: 'examples'
86
108
  }
87
109
  },
88
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
+