@sage-protocol/cli 0.2.9 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/config.js +28 -0
- package/dist/cli/commands/skills.js +583 -0
- package/dist/cli/commands/subdao.js +76 -132
- package/dist/cli/config/playbooks.json +47 -0
- package/dist/cli/config.js +15 -0
- package/dist/cli/index.js +1 -1
- package/dist/cli/mcp-server-stdio.js +14 -85
- package/dist/cli/services/governance/doctor.js +140 -0
- package/dist/cli/services/governance/playbooks.js +97 -0
- package/dist/cli/services/skills/discovery.js +99 -0
- package/dist/cli/services/subdao/applier.js +217 -0
- package/dist/cli/services/subdao/planner.js +107 -0
- package/dist/cli/utils/aliases.js +11 -4
- package/dist/cli/utils/suggestions.js +8 -1
- package/package.json +1 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const semver = require('semver');
|
|
4
|
+
const Ajv = require('ajv');
|
|
5
|
+
|
|
6
|
+
const ajv = new Ajv();
|
|
7
|
+
|
|
8
|
+
const schema = {
|
|
9
|
+
type: 'array',
|
|
10
|
+
items: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
required: ['id', 'name', 'version', 'governance', 'params'],
|
|
13
|
+
properties: {
|
|
14
|
+
id: { type: 'string' },
|
|
15
|
+
name: { type: 'string' },
|
|
16
|
+
version: { type: 'string' }, // We'll validate semver manually too
|
|
17
|
+
governance: { type: 'string', enum: ['operator', 'token'] },
|
|
18
|
+
description: { type: 'string' },
|
|
19
|
+
params: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
required: ['proposalThreshold', 'votingPeriod', 'quorumBps'],
|
|
22
|
+
properties: {
|
|
23
|
+
proposalThreshold: { type: 'string' }, // BigInt as string
|
|
24
|
+
votingPeriod: { type: ['string', 'integer'] },
|
|
25
|
+
quorumBps: { type: 'integer', minimum: 0, maximum: 10000 },
|
|
26
|
+
membershipPolicy: { type: 'string' },
|
|
27
|
+
forkingPolicy: { type: 'string' }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Lazy load or require
|
|
35
|
+
let playbooksData = null;
|
|
36
|
+
|
|
37
|
+
function loadPlaybooks() {
|
|
38
|
+
if (playbooksData) return playbooksData;
|
|
39
|
+
const p = path.join(__dirname, '../../config/playbooks.json');
|
|
40
|
+
if (!fs.existsSync(p)) return [];
|
|
41
|
+
try {
|
|
42
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
43
|
+
|
|
44
|
+
// Schema Validation
|
|
45
|
+
const validate = ajv.compile(schema);
|
|
46
|
+
if (!validate(raw)) {
|
|
47
|
+
console.error('❌ Playbook Schema Validation Failed:', validate.errors);
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
playbooksData = raw.filter(p => {
|
|
52
|
+
// Semver check
|
|
53
|
+
if (!semver.valid(p.version)) {
|
|
54
|
+
console.error(`⚠️ Skipping invalid playbook version for ${p.id}: ${p.version}`);
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
});
|
|
59
|
+
return playbooksData;
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.error('Error loading playbooks:', e.message);
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get a playbook by ID and optional version (semver range).
|
|
68
|
+
* If version is not provided, returns the latest version (highest semver).
|
|
69
|
+
* @param {string} id - Playbook ID (creator, squad, community)
|
|
70
|
+
* @param {string} [versionRange] - Specific version or range (e.g. "^1.0.0")
|
|
71
|
+
*/
|
|
72
|
+
function getPlaybook(id, versionRange) {
|
|
73
|
+
const all = loadPlaybooks();
|
|
74
|
+
const matches = all.filter(p => p.id === id);
|
|
75
|
+
|
|
76
|
+
if (matches.length === 0) return null;
|
|
77
|
+
|
|
78
|
+
if (versionRange) {
|
|
79
|
+
// Find max satisfying version
|
|
80
|
+
const versions = matches.map(p => p.version);
|
|
81
|
+
const maxVer = semver.maxSatisfying(versions, versionRange);
|
|
82
|
+
if (!maxVer) return null;
|
|
83
|
+
return matches.find(p => p.version === maxVer);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Find latest if no range
|
|
87
|
+
return matches.sort((a, b) => semver.rcompare(a.version, b.version))[0];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function listPlaybooks() {
|
|
91
|
+
return loadPlaybooks();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = {
|
|
95
|
+
getPlaybook,
|
|
96
|
+
listPlaybooks
|
|
97
|
+
};
|
|
@@ -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,217 @@
|
|
|
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
|
+
// 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.');
|
|
89
|
+
}
|
|
90
|
+
|
|
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
|
+
}
|
|
102
|
+
|
|
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).');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log(` Executor: ${executor}`);
|
|
119
|
+
|
|
120
|
+
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
|
+
|
|
199
|
+
} catch (e) {
|
|
200
|
+
console.error('❌ Operator creation failed:', e.message);
|
|
201
|
+
throw e;
|
|
202
|
+
}
|
|
203
|
+
|
|
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
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = { applyPlan };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const inquirer = require('inquirer');
|
|
4
|
+
const playbooksService = require('../governance/playbooks');
|
|
5
|
+
|
|
6
|
+
function parseDurationToBlocks(durationStr, blockTimeSec = 12) {
|
|
7
|
+
if (!durationStr) return Math.ceil(3 * 24 * 3600 / blockTimeSec); // Default ~3 days
|
|
8
|
+
|
|
9
|
+
const str = String(durationStr).toLowerCase().trim();
|
|
10
|
+
|
|
11
|
+
// If already integer
|
|
12
|
+
if (/^\d+$/.test(str)) return parseInt(str, 10);
|
|
13
|
+
|
|
14
|
+
const match = str.match(/^(\d+)\s*(d|days?|h|hours?|m|minutes?|blocks?)$/);
|
|
15
|
+
if (!match) return Math.ceil(3 * 24 * 3600 / blockTimeSec); // Fallback
|
|
16
|
+
|
|
17
|
+
const val = parseInt(match[1], 10);
|
|
18
|
+
const unit = match[2];
|
|
19
|
+
|
|
20
|
+
if (unit.startsWith('block')) return val;
|
|
21
|
+
|
|
22
|
+
let seconds = 0;
|
|
23
|
+
if (unit.startsWith('d')) seconds = val * 24 * 3600;
|
|
24
|
+
else if (unit.startsWith('h')) seconds = val * 3600;
|
|
25
|
+
else if (unit.startsWith('m')) seconds = val * 60;
|
|
26
|
+
|
|
27
|
+
return Math.ceil(seconds / blockTimeSec);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function planSubDAO(options) {
|
|
31
|
+
// 1. Select Playbook
|
|
32
|
+
let playbookId = options.playbook;
|
|
33
|
+
if (!playbookId && !options.yes) {
|
|
34
|
+
const playbooks = playbooksService.listPlaybooks();
|
|
35
|
+
const choices = playbooks.map(p => ({
|
|
36
|
+
name: `${p.name} - ${p.description}`,
|
|
37
|
+
value: p.id
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
const ans = await inquirer.prompt([{
|
|
41
|
+
type: 'list',
|
|
42
|
+
name: 'playbookId',
|
|
43
|
+
message: 'Select a Governance Playbook:',
|
|
44
|
+
choices
|
|
45
|
+
}]);
|
|
46
|
+
playbookId = ans.playbookId;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!playbookId) throw new Error('Playbook is required');
|
|
50
|
+
|
|
51
|
+
const playbook = playbooksService.getPlaybook(playbookId);
|
|
52
|
+
if (!playbook) throw new Error(`Unknown playbook: ${playbookId}`);
|
|
53
|
+
|
|
54
|
+
console.log(`\nUsing Playbook: ${playbook.name} (${playbook.version})\n`);
|
|
55
|
+
|
|
56
|
+
// 2. Gather Inputs (merged with playbook defaults)
|
|
57
|
+
const questions = [];
|
|
58
|
+
if (!options.name) questions.push({ type: 'input', name: 'name', message: 'SubDAO Name:' });
|
|
59
|
+
if (!options.description) questions.push({ type: 'input', name: 'description', message: 'Description:' });
|
|
60
|
+
|
|
61
|
+
// Playbook specific inputs
|
|
62
|
+
if (playbook.governance === 'operator' && !options.owners) {
|
|
63
|
+
questions.push({ type: 'input', name: 'owners', message: 'Safe Owners (comma separated):', default: options.owners });
|
|
64
|
+
questions.push({ type: 'input', name: 'threshold', message: 'Safe Threshold:', default: '1' });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Token governance inputs
|
|
68
|
+
if (playbook.governance === 'token' && !options.minStake) {
|
|
69
|
+
questions.push({ type: 'input', name: 'minStake', message: 'Minimum Stake (SXXX):', default: '0' });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const answers = questions.length > 0 ? await inquirer.prompt(questions) : {};
|
|
73
|
+
|
|
74
|
+
const merged = { ...options, ...answers };
|
|
75
|
+
|
|
76
|
+
// 3. Construct Plan
|
|
77
|
+
const plan = {
|
|
78
|
+
version: '1.0.0',
|
|
79
|
+
timestamp: new Date().toISOString(),
|
|
80
|
+
playbook: {
|
|
81
|
+
id: playbook.id,
|
|
82
|
+
version: playbook.version,
|
|
83
|
+
name: playbook.name,
|
|
84
|
+
governance: playbook.governance
|
|
85
|
+
},
|
|
86
|
+
config: {
|
|
87
|
+
name: merged.name,
|
|
88
|
+
description: merged.description,
|
|
89
|
+
accessModel: 'governance',
|
|
90
|
+
minStake: merged.minStake,
|
|
91
|
+
owners: merged.owners,
|
|
92
|
+
threshold: merged.threshold,
|
|
93
|
+
proposalThreshold: playbook.params.proposalThreshold,
|
|
94
|
+
votingPeriod: playbook.params.votingPeriod,
|
|
95
|
+
quorumBps: playbook.params.quorumBps,
|
|
96
|
+
forkingPolicy: 'gov_only',
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Normalize voting period to blocks with override
|
|
101
|
+
const blockTime = options.blockTimeSec ? parseInt(options.blockTimeSec, 10) : 12;
|
|
102
|
+
plan.config.votingPeriodBlocks = parseDurationToBlocks(plan.config.votingPeriod, blockTime).toString();
|
|
103
|
+
|
|
104
|
+
return plan;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { planSubDAO, parseDurationToBlocks };
|
|
@@ -71,13 +71,20 @@ const COMMAND_CATALOG = {
|
|
|
71
71
|
needsSubcommand: true,
|
|
72
72
|
subcommands: [
|
|
73
73
|
'push',
|
|
74
|
-
'propose',
|
|
75
|
-
'status',
|
|
76
74
|
'cache',
|
|
77
75
|
'check',
|
|
78
|
-
'
|
|
76
|
+
'check',
|
|
77
|
+
'doctor',
|
|
79
78
|
'diff',
|
|
80
|
-
'
|
|
79
|
+
'execute',
|
|
80
|
+
'latest-prompts',
|
|
81
|
+
'lint',
|
|
82
|
+
'list',
|
|
83
|
+
'propose',
|
|
84
|
+
'search',
|
|
85
|
+
'status',
|
|
86
|
+
'sync-alt',
|
|
87
|
+
'view-prompts'
|
|
81
88
|
],
|
|
82
89
|
subcommandAliases: {
|
|
83
90
|
ls: 'status',
|
|
@@ -43,6 +43,11 @@ const catalog = {
|
|
|
43
43
|
const manifestCID = ctx.manifestCID || ctx.cid || '<manifest-cid>';
|
|
44
44
|
const proposalId = ctx.proposalId && ctx.proposalId !== 'scheduled' ? ctx.proposalId : null;
|
|
45
45
|
const subdao = normalizeAddress(ctx.subdao);
|
|
46
|
+
const appBase = ensureString(process.env.SAGE_APP_BASE_URL || 'https://app.sageprotocol.io');
|
|
47
|
+
const daoUrl = subdao && appBase ? `${appBase}/daos/${subdao}` : null;
|
|
48
|
+
const proposalUrl = proposalId && appBase && ctx.governor
|
|
49
|
+
? `${appBase}/governance/${proposalId}?gov=${normalizeAddress(ctx.governor) || ctx.governor}`
|
|
50
|
+
: null;
|
|
46
51
|
const baseSuggestions = [
|
|
47
52
|
`sage library status ${manifestCID}`
|
|
48
53
|
];
|
|
@@ -63,7 +68,9 @@ const catalog = {
|
|
|
63
68
|
proposalId ? { label: 'Check proposal status', command: `sage governance status ${proposalId}` } : null,
|
|
64
69
|
proposalId ? { label: 'Inspect proposal details', command: `sage governance inspect ${proposalId}` } : null,
|
|
65
70
|
!proposalId ? { label: 'Monitor timelock queue', command: subdao ? `sage timelock list --subdao ${subdao}` : 'sage timelock list --subdao <subdao-address>' } : null,
|
|
66
|
-
manifestCID && ctx.previousCid ? { label: 'Review manifest diff', command: `sage library diff ${ctx.previousCid} ${manifestCID}` } : null
|
|
71
|
+
manifestCID && ctx.previousCid ? { label: 'Review manifest diff', command: `sage library diff ${ctx.previousCid} ${manifestCID}` } : null,
|
|
72
|
+
daoUrl ? { label: 'Open DAO in app', command: daoUrl } : null,
|
|
73
|
+
proposalUrl ? { label: 'Open proposal in app', command: proposalUrl } : null
|
|
67
74
|
]
|
|
68
75
|
};
|
|
69
76
|
},
|