@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
|
@@ -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
|
-
|
|
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
|
-
]
|
|
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
|
-
'
|
|
75
|
-
'
|
|
81
|
+
'template',
|
|
82
|
+
'add-prompt',
|
|
83
|
+
'preview',
|
|
84
|
+
'examples',
|
|
85
|
+
'propose-import',
|
|
76
86
|
'cache',
|
|
77
87
|
'check',
|
|
78
|
-
'
|
|
88
|
+
'check',
|
|
89
|
+
'doctor',
|
|
79
90
|
'diff',
|
|
80
|
-
'
|
|
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
|
-
|
|
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
|
+
|