@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.
@@ -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
- 'lint',
76
+ 'check',
77
+ 'doctor',
79
78
  'diff',
80
- 'doctor'
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
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sage-protocol/cli",
3
- "version": "0.2.9",
3
+ "version": "0.3.0",
4
4
  "description": "Sage Protocol CLI for managing AI prompt libraries",
5
5
  "bin": {
6
6
  "sage": "./bin/sage.js"