@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
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { findWorkspaceSkills, resolveSkillFileByKey, readFrontmatter } = require('../services/skills/discovery');
|
|
6
|
+
const secrets = require('../services/secrets');
|
|
7
|
+
const https = require('https');
|
|
8
|
+
const { readWorkspace, DEFAULT_DIR } = require('../services/prompts/workspace');
|
|
9
|
+
|
|
10
|
+
function generateSkillsXml(skills) {
|
|
11
|
+
const mk = (s) => `<skill>\n<name>${s.name}</name>\n<description>${(s.summary || '').replace(/</g, '<')}</description>\n<location>project</location>\n</skill>`;
|
|
12
|
+
const inner = skills.map(mk).join('\n\n');
|
|
13
|
+
return `<skills_system priority="1">\n\n## Available Skills\n\n<usage>\nUse these skills when they are relevant to the user's task.\n- Only use skills listed in <available_skills>\n- Do not duplicate content already in context\n- Load a single skill at a time as needed\n</usage>\n\n<available_skills>\n\n${inner}\n\n</available_skills>\n\n</skills_system>`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function replaceSkillsSection(content, newSection) {
|
|
17
|
+
const startMarker = '<skills_system';
|
|
18
|
+
const endMarker = '</skills_system>';
|
|
19
|
+
if (content.includes(startMarker)) {
|
|
20
|
+
const regex = /<skills_system[^>]*>[\s\S]*?<\/skills_system>/;
|
|
21
|
+
return content.replace(regex, newSection);
|
|
22
|
+
}
|
|
23
|
+
const htmlStartMarker = '<!-- SKILLS_TABLE_START -->';
|
|
24
|
+
const htmlEndMarker = '<!-- SKILLS_TABLE_END -->';
|
|
25
|
+
if (content.includes(htmlStartMarker)) {
|
|
26
|
+
const inner = newSection.replace(/<skills_system[^>]*>|<\/skills_system>/g, '');
|
|
27
|
+
const regex = new RegExp(`${htmlStartMarker}[\\s\\S]*?${htmlEndMarker}`, 'g');
|
|
28
|
+
return content.replace(regex, `${htmlStartMarker}\n${inner}\n${htmlEndMarker}`);
|
|
29
|
+
}
|
|
30
|
+
return content.trimEnd() + '\n\n' + newSection + '\n';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function renderForTarget({ title, body }, target) {
|
|
34
|
+
const header = `# ${title}`;
|
|
35
|
+
if (target === 'cursor') {
|
|
36
|
+
return `${header}\n\n${body.trim()}\n`;
|
|
37
|
+
}
|
|
38
|
+
if (target === 'claude') {
|
|
39
|
+
return `${header}\n\n${body.trim()}\n`;
|
|
40
|
+
}
|
|
41
|
+
// browser / generic
|
|
42
|
+
return `${header}\n\n${body.trim()}\n`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function register(program) {
|
|
46
|
+
const cmd = new Command('skills').description('Skills workflow (discover, iterate, export, and sync)');
|
|
47
|
+
|
|
48
|
+
cmd
|
|
49
|
+
.command('init')
|
|
50
|
+
.description('Initialize skills workspace')
|
|
51
|
+
.action(() => {
|
|
52
|
+
try {
|
|
53
|
+
const cwd = process.cwd();
|
|
54
|
+
const sageDir = path.join(cwd, '.sage');
|
|
55
|
+
const wsFile = path.join(sageDir, 'workspace.json');
|
|
56
|
+
const promptsDir = path.join(cwd, 'prompts');
|
|
57
|
+
const skillsDir = path.join(promptsDir, 'skills');
|
|
58
|
+
|
|
59
|
+
if (!fs.existsSync(sageDir)) fs.mkdirSync(sageDir);
|
|
60
|
+
if (!fs.existsSync(wsFile)) {
|
|
61
|
+
fs.writeFileSync(wsFile, JSON.stringify({ promptsDir: 'prompts' }, null, 2));
|
|
62
|
+
console.log('✅ Created .sage/workspace.json');
|
|
63
|
+
} else {
|
|
64
|
+
console.log('ℹ️ .sage/workspace.json already exists');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!fs.existsSync(promptsDir)) fs.mkdirSync(promptsDir);
|
|
68
|
+
if (!fs.existsSync(skillsDir)) {
|
|
69
|
+
fs.mkdirSync(skillsDir);
|
|
70
|
+
console.log('✅ Created prompts/skills/');
|
|
71
|
+
} else {
|
|
72
|
+
console.log('ℹ️ prompts/skills/ already exists');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log('Skills workspace initialized.');
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.error('❌ init failed:', e.message);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
cmd
|
|
83
|
+
.command('list')
|
|
84
|
+
.description('List workspace skills (flat files and SKILL.md directories)')
|
|
85
|
+
.option('--json', 'Output JSON', false)
|
|
86
|
+
.action((opts) => {
|
|
87
|
+
try {
|
|
88
|
+
const ws = readWorkspace() || {};
|
|
89
|
+
const promptsDir = ws.promptsDir || DEFAULT_DIR || 'prompts';
|
|
90
|
+
const skills = findWorkspaceSkills({ promptsDir });
|
|
91
|
+
if (opts.json) {
|
|
92
|
+
console.log(JSON.stringify({ skills }, null, 2));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (!skills.length) {
|
|
96
|
+
console.log('No skills found. Create prompts/skills/<name>.md or prompts/skills/<name>/SKILL.md');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
skills.forEach((s, i) => {
|
|
100
|
+
console.log(`${i + 1}. ${s.name} (${s.key})`);
|
|
101
|
+
if (s.summary) console.log(` ${s.summary}`);
|
|
102
|
+
console.log(` ${path.relative(process.cwd(), s.path)}`);
|
|
103
|
+
});
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.error('❌ list failed:', e.message);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
cmd
|
|
111
|
+
.command('variant')
|
|
112
|
+
.description('Clone a skill into a new local variant (suffix appended)')
|
|
113
|
+
.argument('<key>', 'Skill key (e.g., skills/my-skill or skills/cat/my-skill)')
|
|
114
|
+
.argument('[suffix]', 'Suffix (e.g., v2)', 'v2')
|
|
115
|
+
.action((key, suffix) => {
|
|
116
|
+
try {
|
|
117
|
+
const ws = readWorkspace();
|
|
118
|
+
if (!ws) throw new Error('Workspace not found. Run `sage prompts init`.');
|
|
119
|
+
const resolved = resolveSkillFileByKey({ promptsDir: ws.promptsDir, key });
|
|
120
|
+
if (!resolved) throw new Error(`Skill not found for key '${key}'`);
|
|
121
|
+
const src = resolved.path;
|
|
122
|
+
const base = resolved.baseDir;
|
|
123
|
+
if (resolved.type === 'flat') {
|
|
124
|
+
const srcName = path.basename(src, '.md');
|
|
125
|
+
const dest = path.join(base, `${srcName}.${suffix}.md`);
|
|
126
|
+
fs.copyFileSync(src, dest);
|
|
127
|
+
// Update or create frontmatter with lineage
|
|
128
|
+
try {
|
|
129
|
+
const content = fs.readFileSync(dest, 'utf8');
|
|
130
|
+
if (content.startsWith('---')) {
|
|
131
|
+
const end = content.indexOf('\n---', 3);
|
|
132
|
+
if (end !== -1) {
|
|
133
|
+
let front = content.slice(3, end);
|
|
134
|
+
if (!/^\s*version\s*:/m.test(front)) front += `\nversion: ${suffix}`;
|
|
135
|
+
else front = front.replace(/^(\s*version\s*:\s*).+$/m, `$1${suffix}`);
|
|
136
|
+
if (!/^\s*forked_from\s*:/m.test(front)) front += `\nforked_from: ${key}`;
|
|
137
|
+
else front = front.replace(/^(\s*forked_from\s*:\s*).+$/m, `$1${key}`);
|
|
138
|
+
const updated = `---\n${front}\n---` + content.slice(end + 4);
|
|
139
|
+
fs.writeFileSync(dest, updated, 'utf8');
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
const updated = `---\nversion: ${suffix}\nforked_from: ${key}\n---\n\n${content}`;
|
|
143
|
+
fs.writeFileSync(dest, updated, 'utf8');
|
|
144
|
+
}
|
|
145
|
+
} catch (_) { /* best effort */ }
|
|
146
|
+
console.log(`✅ Created variant: ${path.relative(process.cwd(), dest)}`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// directory skill
|
|
150
|
+
const dirName = path.basename(base);
|
|
151
|
+
const parent = path.dirname(base);
|
|
152
|
+
const destDir = path.join(parent, `${dirName}.${suffix}`);
|
|
153
|
+
fs.cpSync(base, destDir, { recursive: true });
|
|
154
|
+
// Adjust frontmatter 'name' if present
|
|
155
|
+
const skillMd = path.join(destDir, 'SKILL.md');
|
|
156
|
+
if (fs.existsSync(skillMd)) {
|
|
157
|
+
const raw = fs.readFileSync(skillMd, 'utf8');
|
|
158
|
+
let replaced = raw.replace(/^(\s*name\s*:\s*)(.+)$/m, (_, p1, p2) => `${p1}${p2}.${suffix}`);
|
|
159
|
+
if (!/^\s*version\s*:/m.test(replaced)) replaced = replaced.replace(/^---\s*\n/, `---\nversion: ${suffix}\n`);
|
|
160
|
+
else replaced = replaced.replace(/^(\s*version\s*:\s*).+$/m, `$1${suffix}`);
|
|
161
|
+
if (!/^\s*forked_from\s*:/m.test(replaced)) replaced = replaced.replace(/^---\s*\n/, `---\nforked_from: ${key}\n`);
|
|
162
|
+
else replaced = replaced.replace(/^(\s*forked_from\s*:\s*).+$/m, `$1${key}`);
|
|
163
|
+
fs.writeFileSync(skillMd, replaced, 'utf8');
|
|
164
|
+
}
|
|
165
|
+
console.log(`✅ Created variant: ${path.relative(process.cwd(), destDir)}`);
|
|
166
|
+
} catch (e) {
|
|
167
|
+
console.error('❌ variant failed:', e.message);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
cmd
|
|
173
|
+
.command('agents-sync')
|
|
174
|
+
.description('Update or append a skills section in AGENTS.md describing local skills')
|
|
175
|
+
.option('--json', 'Output JSON', false)
|
|
176
|
+
.action((opts) => {
|
|
177
|
+
try {
|
|
178
|
+
const ws = readWorkspace() || {};
|
|
179
|
+
const promptsDir = ws.promptsDir || DEFAULT_DIR || 'prompts';
|
|
180
|
+
const skills = findWorkspaceSkills({ promptsDir });
|
|
181
|
+
const xml = generateSkillsXml(skills);
|
|
182
|
+
const file = path.join(process.cwd(), 'AGENTS.md');
|
|
183
|
+
let content = '';
|
|
184
|
+
if (fs.existsSync(file)) content = fs.readFileSync(file, 'utf8');
|
|
185
|
+
const updated = replaceSkillsSection(content, xml);
|
|
186
|
+
fs.writeFileSync(file, updated, 'utf8');
|
|
187
|
+
if (opts.json) {
|
|
188
|
+
console.log(JSON.stringify({ ok: true, count: skills.length }, null, 2));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
console.log(`✅ Synced ${skills.length} skill(s) to AGENTS.md`);
|
|
192
|
+
} catch (e) {
|
|
193
|
+
console.error('❌ agents-sync failed:', e.message);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
cmd
|
|
199
|
+
.command('export')
|
|
200
|
+
.description('Render a skill for a target environment')
|
|
201
|
+
.argument('<key>', 'Skill key')
|
|
202
|
+
.option('--as <target>', 'Target (cursor|claude|browser)', 'cursor')
|
|
203
|
+
.option('--write', 'Write to project defaults when supported (e.g., .cursor/rules)', false)
|
|
204
|
+
.action((key, opts) => {
|
|
205
|
+
try {
|
|
206
|
+
const ws = readWorkspace();
|
|
207
|
+
if (!ws) throw new Error('Workspace not found. Run `sage prompts init`.');
|
|
208
|
+
const resolved = resolveSkillFileByKey({ promptsDir: ws.promptsDir, key });
|
|
209
|
+
if (!resolved) throw new Error(`Skill not found for key '${key}'`);
|
|
210
|
+
const raw = fs.readFileSync(resolved.path, 'utf8');
|
|
211
|
+
const { meta, body } = readFrontmatter(raw);
|
|
212
|
+
const title = meta.title || path.basename(resolved.type === 'flat' ? resolved.path : path.dirname(resolved.path));
|
|
213
|
+
const out = renderForTarget({ title, body }, String(opts.as || 'cursor').toLowerCase());
|
|
214
|
+
if (opts.write && opts.as === 'cursor') {
|
|
215
|
+
const rulesDir = fs.existsSync('.cursor/rules') ? '.cursor/rules' : null;
|
|
216
|
+
if (!rulesDir) {
|
|
217
|
+
console.log(out);
|
|
218
|
+
console.warn('ℹ️ No .cursor/rules directory found. Printed to stdout instead.');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const safe = key.replace(/[^a-z0-9/_\-.]/gi, '_').replace(/^skills\//, '').replace(/\//g, '__');
|
|
222
|
+
const dest = path.join(rulesDir, `sage--${safe}.md`);
|
|
223
|
+
fs.writeFileSync(dest, out, 'utf8');
|
|
224
|
+
console.log(`✅ Wrote Cursor rule: ${dest}`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
console.log(out);
|
|
228
|
+
} catch (e) {
|
|
229
|
+
console.error('❌ export failed:', e.message);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
cmd
|
|
235
|
+
.command('import')
|
|
236
|
+
.description('Import a skill from GitHub, OpenSkills dirs, or a local directory into prompts/skills')
|
|
237
|
+
.argument('<source>', 'Skill source: owner/repo[/path] | https URL | local dir | installed skill name')
|
|
238
|
+
.option('--from-dir <dir>', 'Explicit directory containing SKILL.md')
|
|
239
|
+
.option('-y, --yes', 'Skip interactive selection when importing a repo with multiple skills', false)
|
|
240
|
+
.action(async (source, opts) => {
|
|
241
|
+
try {
|
|
242
|
+
const ws = readWorkspace();
|
|
243
|
+
if (!ws) throw new Error('Workspace not found. Run `sage prompts init`.');
|
|
244
|
+
|
|
245
|
+
const toWorkspace = async (dir, origin = null) => {
|
|
246
|
+
const skillMd = path.join(dir, 'SKILL.md');
|
|
247
|
+
if (!fs.existsSync(skillMd)) throw new Error('SKILL.md not found in provided directory');
|
|
248
|
+
const raw = fs.readFileSync(skillMd, 'utf8');
|
|
249
|
+
|
|
250
|
+
// Security: Size Check
|
|
251
|
+
if (raw.length > 100 * 1024) {
|
|
252
|
+
console.warn(`⚠️ WARNING: Skill file is large (${Math.round(raw.length/1024)}KB).`);
|
|
253
|
+
if (process.stdout.isTTY && !opts.yes) {
|
|
254
|
+
const inquirer = require('inquirer').default || require('inquirer');
|
|
255
|
+
const { confirm } = await inquirer.prompt([{ type: 'confirm', name: 'confirm', message: 'Import large file?', default: false }]);
|
|
256
|
+
if (!confirm) { console.log('Skipped.'); return; }
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const { meta, body } = readFrontmatter(raw);
|
|
261
|
+
|
|
262
|
+
// Security: License Warning
|
|
263
|
+
if (!meta.license && !opts.yes) {
|
|
264
|
+
console.warn('⚠️ WARNING: No license specified in frontmatter.');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const skillName = (meta.title || path.basename(dir)).trim();
|
|
268
|
+
const destDir = path.join(ws.promptsDir, 'skills', skillName);
|
|
269
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
270
|
+
// Annotate source metadata into frontmatter
|
|
271
|
+
const sourceComment = `source: ${origin || dir}`;
|
|
272
|
+
fs.writeFileSync(
|
|
273
|
+
path.join(destDir, 'SKILL.md'),
|
|
274
|
+
`---\nname: ${skillName}\n${meta.summary ? `description: ${meta.summary}\n` : ''}${sourceComment ? `${sourceComment}\n` : ''}---\n\n${body.trim()}\n`,
|
|
275
|
+
'utf8',
|
|
276
|
+
);
|
|
277
|
+
console.log(`✅ Imported ${skillName} → ${path.relative(process.cwd(), destDir)}`);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// 1) Explicit dir override
|
|
281
|
+
if (opts.fromDir) return await toWorkspace(opts.fromDir);
|
|
282
|
+
|
|
283
|
+
// 2) If 'source' is a local directory path
|
|
284
|
+
if (fs.existsSync(source) && fs.statSync(source).isDirectory()) return await toWorkspace(source);
|
|
285
|
+
|
|
286
|
+
// 3) Try installed skill dirs (.agent/.claude), using provided name as directory
|
|
287
|
+
const installedRoots = [
|
|
288
|
+
path.join(process.cwd(), '.agent', 'skills'),
|
|
289
|
+
path.join(process.cwd(), '.claude', 'skills'),
|
|
290
|
+
path.join(os.homedir(), '.agent', 'skills'),
|
|
291
|
+
path.join(os.homedir(), '.claude', 'skills'),
|
|
292
|
+
];
|
|
293
|
+
for (const r of installedRoots) {
|
|
294
|
+
const candidate = path.join(r, source);
|
|
295
|
+
if (fs.existsSync(path.join(candidate, 'SKILL.md'))) return await toWorkspace(candidate);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 4) GitHub import: owner/repo[/path] or https URL
|
|
299
|
+
const isUrl = /^(https?:\/\/|git@)/i.test(source);
|
|
300
|
+
const isOwnerRepo = /^[^\s/]+\/[^\s/]+(\/.+)?$/.test(source);
|
|
301
|
+
if (!isUrl && !isOwnerRepo) {
|
|
302
|
+
throw new Error('Source not found. Provide a local dir, installed skill name, or owner/repo[/path].');
|
|
303
|
+
}
|
|
304
|
+
const { spawnSync } = require('child_process');
|
|
305
|
+
const osTmp = os.tmpdir();
|
|
306
|
+
const tmpRoot = path.join(osTmp, `sage-skill-${Date.now()}`);
|
|
307
|
+
fs.mkdirSync(tmpRoot, { recursive: true });
|
|
308
|
+
|
|
309
|
+
let repoUrl = source;
|
|
310
|
+
let subpath = '';
|
|
311
|
+
let owner = null;
|
|
312
|
+
let repo = null;
|
|
313
|
+
if (isOwnerRepo && !isUrl) {
|
|
314
|
+
const parts = source.split('/');
|
|
315
|
+
owner = parts[0];
|
|
316
|
+
repo = parts[1];
|
|
317
|
+
const base = `https://github.com/${owner}/${repo}`;
|
|
318
|
+
repoUrl = base;
|
|
319
|
+
if (parts.length > 2) subpath = parts.slice(2).join('/');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const cloneDir = path.join(tmpRoot, 'repo');
|
|
323
|
+
const envToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN || (await secrets.get('github.token', {})) || '';
|
|
324
|
+
const gitPresent = spawnSync('git', ['--version'], { stdio: 'ignore' }).status === 0;
|
|
325
|
+
let clonedOk = false;
|
|
326
|
+
if (gitPresent) {
|
|
327
|
+
const gitArgs = ['clone', '--depth', '1'];
|
|
328
|
+
const r = envToken && /github\.com/i.test(repoUrl)
|
|
329
|
+
? spawnSync('git', ['-c', `http.extraheader=AUTHORIZATION: bearer ${envToken}`, ...gitArgs, repoUrl, cloneDir], { stdio: 'inherit' })
|
|
330
|
+
: spawnSync('git', [...gitArgs, repoUrl, cloneDir], { stdio: 'inherit' });
|
|
331
|
+
clonedOk = (r.status === 0);
|
|
332
|
+
}
|
|
333
|
+
if (!clonedOk) {
|
|
334
|
+
// ZIP fallback using codeload.github.com
|
|
335
|
+
if (!owner || !repo) {
|
|
336
|
+
const m = repoUrl.match(/github\.com\/(.+?)\/(.+?)(?:$|\/)/i);
|
|
337
|
+
if (m) { owner = m[1]; repo = m[2]; }
|
|
338
|
+
}
|
|
339
|
+
if (!owner || !repo) throw new Error('Could not parse owner/repo for ZIP fallback');
|
|
340
|
+
const zipPath = path.join(tmpRoot, 'archive.zip');
|
|
341
|
+
const tryDownload = (ref) => new Promise((resolve, reject) => {
|
|
342
|
+
const url = `https://codeload.github.com/${owner}/${repo}/zip/refs/heads/${ref}`;
|
|
343
|
+
const file = fs.createWriteStream(zipPath);
|
|
344
|
+
const req = https.get(url, { headers: envToken ? { Authorization: `Bearer ${envToken}` } : {} }, (res) => {
|
|
345
|
+
if (res.statusCode && res.statusCode >= 400) { reject(new Error(`ZIP download failed: ${res.statusCode}`)); return; }
|
|
346
|
+
res.pipe(file);
|
|
347
|
+
file.on('finish', () => file.close(() => resolve({ ok: true, ref })));
|
|
348
|
+
});
|
|
349
|
+
req.on('error', reject);
|
|
350
|
+
});
|
|
351
|
+
let ok = false; let usedRef = 'main';
|
|
352
|
+
try { await tryDownload('main'); ok = true; usedRef = 'main'; } catch (_) {}
|
|
353
|
+
if (!ok) { try { await tryDownload('master'); ok = true; usedRef = 'master'; } catch (e) { throw e; } }
|
|
354
|
+
const unzip = spawnSync('unzip', ['-q', zipPath, '-d', tmpRoot], { stdio: 'inherit' });
|
|
355
|
+
if (unzip.status !== 0) throw new Error('unzip failed (install git or unzip, or use --from-dir)');
|
|
356
|
+
const expectedDir = path.join(tmpRoot, `${repo}-${usedRef}`);
|
|
357
|
+
if (!fs.existsSync(expectedDir)) {
|
|
358
|
+
// Fallback: find first directory
|
|
359
|
+
const entry = fs.readdirSync(tmpRoot, { withFileTypes: true }).find((e) => e.isDirectory() && e.name !== 'repo');
|
|
360
|
+
if (!entry) throw new Error('ZIP extract succeeded but repo dir not found');
|
|
361
|
+
fs.renameSync(path.join(tmpRoot, entry.name), cloneDir);
|
|
362
|
+
} else {
|
|
363
|
+
fs.renameSync(expectedDir, cloneDir);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const resolvedBase = subpath ? path.join(cloneDir, subpath) : cloneDir;
|
|
368
|
+
if (!fs.existsSync(resolvedBase)) throw new Error('Specified subpath not found in repository');
|
|
369
|
+
|
|
370
|
+
// Find skills (directories containing SKILL.md)
|
|
371
|
+
const candidates = [];
|
|
372
|
+
const stack = [resolvedBase];
|
|
373
|
+
while (stack.length) {
|
|
374
|
+
const curr = stack.pop();
|
|
375
|
+
const entries = fs.readdirSync(curr, { withFileTypes: true });
|
|
376
|
+
let hasSkill = false;
|
|
377
|
+
for (const e of entries) {
|
|
378
|
+
if (e.isFile() && e.name === 'SKILL.md') hasSkill = true;
|
|
379
|
+
}
|
|
380
|
+
if (hasSkill) {
|
|
381
|
+
candidates.push(curr);
|
|
382
|
+
} else {
|
|
383
|
+
for (const e of entries) if (e.isDirectory()) stack.push(path.join(curr, e.name));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (candidates.length === 0) throw new Error('No SKILL.md directories found in repository');
|
|
388
|
+
|
|
389
|
+
let selected = candidates;
|
|
390
|
+
if (!opts.yes && candidates.length > 1 && process.stdout.isTTY) {
|
|
391
|
+
const inquirer = require('inquirer');
|
|
392
|
+
const { checkbox } = await inquirer.prompt([
|
|
393
|
+
{
|
|
394
|
+
type: 'checkbox',
|
|
395
|
+
name: 'checkbox',
|
|
396
|
+
message: 'Select skills to import',
|
|
397
|
+
choices: candidates.map((p) => ({ name: path.relative(cloneDir, p), value: p })),
|
|
398
|
+
pageSize: 12,
|
|
399
|
+
},
|
|
400
|
+
]);
|
|
401
|
+
selected = checkbox;
|
|
402
|
+
if (!selected || !selected.length) {
|
|
403
|
+
console.log('Cancelled.');
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const originUrl = `https://github.com/${owner || 'unknown'}/${repo || 'unknown'}${subpath ? `/${subpath}` : ''}`;
|
|
409
|
+
for (const dir of selected) await toWorkspace(dir, originUrl);
|
|
410
|
+
} catch (e) {
|
|
411
|
+
console.error('❌ import failed:', e.message);
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
cmd
|
|
417
|
+
.command('publish')
|
|
418
|
+
.description('Publish local skills to the on-chain library (Safe/Tally aware)')
|
|
419
|
+
.argument('[key]', 'Specific skill key to publish (default: all)')
|
|
420
|
+
.option('--subdao <address>', 'SubDAO/library address')
|
|
421
|
+
.option('--force', 'Ignore doctor warnings')
|
|
422
|
+
.option('--dry-run', 'Skip IPFS upload and transaction generation')
|
|
423
|
+
.action(async (key, opts) => {
|
|
424
|
+
try {
|
|
425
|
+
const fs = require('fs');
|
|
426
|
+
const { diagnoseSubDAO } = require('../services/governance/doctor');
|
|
427
|
+
const WalletManager = require('../wallet-manager');
|
|
428
|
+
const { readWorkspace } = require('../services/prompts/workspace');
|
|
429
|
+
const { findWorkspaceSkills } = require('../services/skills/discovery');
|
|
430
|
+
|
|
431
|
+
// 1. Init Wallet & Context
|
|
432
|
+
const wallet = new WalletManager();
|
|
433
|
+
await wallet.connect();
|
|
434
|
+
const provider = wallet.getProvider();
|
|
435
|
+
const signer = wallet.getSigner();
|
|
436
|
+
|
|
437
|
+
// 2. Doctor Preflight
|
|
438
|
+
console.log('🩺 Running Governance Doctor...');
|
|
439
|
+
const doctor = await diagnoseSubDAO({ subdao: opts.subdao, provider, signer });
|
|
440
|
+
|
|
441
|
+
if (doctor.recommendations.length > 0) {
|
|
442
|
+
console.log('⚠️ Issues detected:');
|
|
443
|
+
doctor.recommendations.forEach(r => {
|
|
444
|
+
const msg = typeof r==='string' ? r : r.msg;
|
|
445
|
+
const fix = typeof r==='string' ? '' : (r.fix || r.fixCmd);
|
|
446
|
+
console.log(` - ${msg} ${fix ? `(Fix: ${fix})` : ''}`);
|
|
447
|
+
});
|
|
448
|
+
if (!opts.force && !process.env.SAGE_FORCE) {
|
|
449
|
+
console.error('❌ Aborting publish due to governance issues. Use --force to ignore.');
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
console.log('✅ Governance checks passed.');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const subdaoAddr = doctor.subdao;
|
|
457
|
+
if (!subdaoAddr) throw new Error('SubDAO not resolved.');
|
|
458
|
+
|
|
459
|
+
// 3. Build Manifest
|
|
460
|
+
console.log('📦 Building manifest...');
|
|
461
|
+
const ws = readWorkspace() || {};
|
|
462
|
+
const skills = findWorkspaceSkills({ promptsDir: ws.promptsDir || 'prompts' });
|
|
463
|
+
// Filter if key provided? "sage skills publish my-skill" usually implies updating just that one?
|
|
464
|
+
// But manifest is usually holistic for the library.
|
|
465
|
+
// For now, publish ALL workspace skills as the new version of the library.
|
|
466
|
+
|
|
467
|
+
if (skills.length === 0) throw new Error('No skills found to publish.');
|
|
468
|
+
|
|
469
|
+
const manifest = {
|
|
470
|
+
version: '1.0.0',
|
|
471
|
+
timestamp: new Date().toISOString(),
|
|
472
|
+
skills: skills.map(s => ({
|
|
473
|
+
id: s.key,
|
|
474
|
+
metadata: s.meta,
|
|
475
|
+
// content? usually we upload content separately or include it?
|
|
476
|
+
// For a "skills library", we probably want content accessible.
|
|
477
|
+
// Let's assume we pin the SKILL.md files or include content in manifest.
|
|
478
|
+
// Including content in manifest is easier for MVP.
|
|
479
|
+
content: fs.readFileSync(s.path, 'utf8')
|
|
480
|
+
}))
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
if (opts.dryRun) {
|
|
484
|
+
console.log('Dry run: Manifest would contain', skills.length, 'skills.');
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 4. IPFS Upload
|
|
489
|
+
console.log('☁️ Uploading to IPFS...');
|
|
490
|
+
// Use IPFS Manager or Worker directly?
|
|
491
|
+
// Try to require IpfsManager
|
|
492
|
+
let cid;
|
|
493
|
+
try {
|
|
494
|
+
const IpfsManager = require('../ipfs-manager');
|
|
495
|
+
const ipfs = new IpfsManager();
|
|
496
|
+
// ipfs.uploadJson(json) returns CID string directly
|
|
497
|
+
cid = await ipfs.uploadJson(manifest, 'skills-manifest');
|
|
498
|
+
if (!cid) throw new Error('IPFS upload returned no CID');
|
|
499
|
+
} catch (e) {
|
|
500
|
+
// Fallback or error
|
|
501
|
+
throw new Error(`IPFS upload failed: ${e.message}`);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
console.log(`✅ Manifest uploaded: ${cid}`);
|
|
505
|
+
|
|
506
|
+
// 5. Generate Transaction Payload
|
|
507
|
+
console.log('🚀 Generating Governance Payload...');
|
|
508
|
+
|
|
509
|
+
// We need to call updateLibrary(cid) on the Registry or SubDAO?
|
|
510
|
+
// SubDAOs usually have a `updateLibrary` or `publishManifest` method?
|
|
511
|
+
// Or LibraryRegistry?
|
|
512
|
+
// "Creator Playbook": updateLibraryForSubDAO(subdao, libraryId, cid, ...)
|
|
513
|
+
|
|
514
|
+
// Let's assume SubDAO has a `publish(cid)` or we call Registry.
|
|
515
|
+
// The architecture says: LibraryRegistry.
|
|
516
|
+
|
|
517
|
+
const RegistryABI = require('../utils/factory-abi'); // Wait, need LibraryRegistry ABI
|
|
518
|
+
// We can construct Interface manually for updateLibrary
|
|
519
|
+
// function updateLibraryForSubDAO(address subdao, string calldata manifestCid)
|
|
520
|
+
const { ethers } = require('ethers');
|
|
521
|
+
const iface = new ethers.Interface(['function updateLibraryForSubDAO(address subdao, string manifestCid)']);
|
|
522
|
+
const calldata = iface.encodeFunctionData('updateLibraryForSubDAO', [subdaoAddr, cid]);
|
|
523
|
+
const config = require('../config');
|
|
524
|
+
// Resolve from env first, then active profile addresses (aliases supported)
|
|
525
|
+
const resolvedFromConfig = (() => {
|
|
526
|
+
try { return config.resolveAddress('LIBRARY_REGISTRY_ADDRESS', null); } catch (_) { return null; }
|
|
527
|
+
})();
|
|
528
|
+
const target = process.env.LIBRARY_REGISTRY_ADDRESS || resolvedFromConfig;
|
|
529
|
+
if (!process.env.LIBRARY_REGISTRY_ADDRESS && resolvedFromConfig) {
|
|
530
|
+
try {
|
|
531
|
+
const profiles = config.readProfiles?.() || {};
|
|
532
|
+
const active = profiles.activeProfile || 'default';
|
|
533
|
+
console.log(`ℹ️ Using LIBRARY_REGISTRY_ADDRESS from profile '${active}': ${target}`);
|
|
534
|
+
} catch (_) {}
|
|
535
|
+
}
|
|
536
|
+
// We need to ensure target is known. Doctor found it?
|
|
537
|
+
// doctor.registry might be PromptRegistry, NOT LibraryRegistry.
|
|
538
|
+
// We need LIBRARY_REGISTRY_ADDRESS.
|
|
539
|
+
if (!target) throw new Error('LIBRARY_REGISTRY_ADDRESS not set in env or active profile. Set env: export LIBRARY_REGISTRY_ADDRESS=0x... or run: sage config addresses (and add LIBRARY_REGISTRY_ADDRESS).');
|
|
540
|
+
|
|
541
|
+
// 6. Route based on Mode
|
|
542
|
+
if (doctor.mode === 1) {
|
|
543
|
+
// Community -> Tally
|
|
544
|
+
console.log('\n🗳️ Community Governance Detected (Token Voting)');
|
|
545
|
+
// Generate Tally URL
|
|
546
|
+
// Tally URL format: https://www.tally.xyz/gov/[slug]/proposal/create? ...
|
|
547
|
+
// We need the DAO slug or address. Tally uses address or slug.
|
|
548
|
+
// Let's output the data for now.
|
|
549
|
+
|
|
550
|
+
console.log('\nProposal Details:');
|
|
551
|
+
console.log(`Target: ${target}`);
|
|
552
|
+
console.log(`Method: updateLibrary(${subdaoAddr}, ${cid})`);
|
|
553
|
+
console.log(`Calldata: ${calldata}`);
|
|
554
|
+
|
|
555
|
+
console.log('\nCreate Proposal on Tally:');
|
|
556
|
+
console.log(`https://www.tally.xyz/gov/${subdaoAddr}/proposal/create`);
|
|
557
|
+
console.log('(Copy/paste the calldata above into the "Custom Action" field)');
|
|
558
|
+
|
|
559
|
+
} else {
|
|
560
|
+
// Creator/Squad -> Safe / Direct
|
|
561
|
+
// Check if we are owner/admin
|
|
562
|
+
// If Squad (Multisig), output Safe JSON.
|
|
563
|
+
|
|
564
|
+
console.log('\n🛡️ Team/Creator Governance Detected');
|
|
565
|
+
|
|
566
|
+
// Output Safe JSON
|
|
567
|
+
const safeBatch = {
|
|
568
|
+
version: "1.0",
|
|
569
|
+
chainId: (await provider.getNetwork()).chainId.toString(),
|
|
570
|
+
createdAt: Date.now(),
|
|
571
|
+
meta: { name: "Update Skills Library", description: `Publish version ${cid}` },
|
|
572
|
+
transactions: [{
|
|
573
|
+
to: target,
|
|
574
|
+
value: "0",
|
|
575
|
+
data: calldata,
|
|
576
|
+
operation: 0 // Call
|
|
577
|
+
}]
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const filename = `safe-tx-${Date.now()}.json`;
|
|
581
|
+
fs.writeFileSync(filename, JSON.stringify(safeBatch, null, 2));
|
|
582
|
+
console.log(`✅ Safe Transaction Builder JSON saved to ${filename}`);
|
|
583
|
+
console.log('\n📋 Next steps:');
|
|
584
|
+
console.log(` 1. Import ${filename} in Safe Transaction Builder`);
|
|
585
|
+
console.log(` 2. Review and sign the transaction`);
|
|
586
|
+
console.log(` 3. Execute the transaction`);
|
|
587
|
+
if (subdaoAddr) {
|
|
588
|
+
console.log(` 4. Verify: sage library status --subdao ${subdaoAddr}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
} catch (e) {
|
|
593
|
+
console.error('❌ publish failed:', e.message);
|
|
594
|
+
process.exit(1);
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// QoL: publish-manifest (pin only, no governance)
|
|
599
|
+
cmd
|
|
600
|
+
.command('publish-manifest')
|
|
601
|
+
.description('Build and pin a manifest from workspace skills without proposing')
|
|
602
|
+
.option('--json', 'Output CID as JSON')
|
|
603
|
+
.action(async (opts) => {
|
|
604
|
+
try {
|
|
605
|
+
const { readWorkspace } = require('../services/prompts/workspace');
|
|
606
|
+
const { findWorkspaceSkills } = require('../services/skills/discovery');
|
|
607
|
+
const IpfsManager = require('../ipfs-manager');
|
|
608
|
+
const fs = require('fs');
|
|
609
|
+
|
|
610
|
+
console.log('📦 Building manifest...');
|
|
611
|
+
const ws = readWorkspace() || {};
|
|
612
|
+
const skills = findWorkspaceSkills({ promptsDir: ws.promptsDir || 'prompts' });
|
|
613
|
+
if (skills.length === 0) throw new Error('No skills found in workspace');
|
|
614
|
+
const manifest = {
|
|
615
|
+
version: '1.0.0',
|
|
616
|
+
timestamp: new Date().toISOString(),
|
|
617
|
+
skills: skills.map((s) => ({ id: s.key, metadata: s.meta, content: fs.readFileSync(s.path, 'utf8') })),
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
console.log('☁️ Uploading to IPFS...');
|
|
621
|
+
const ipfs = new IpfsManager();
|
|
622
|
+
const { cid } = await ipfs.uploadJson(manifest, 'skills-manifest');
|
|
623
|
+
if (opts.json) {
|
|
624
|
+
console.log(JSON.stringify({ ok: true, cid }, null, 2));
|
|
625
|
+
} else {
|
|
626
|
+
console.log(`✅ Manifest CID: ${cid}`);
|
|
627
|
+
}
|
|
628
|
+
} catch (e) {
|
|
629
|
+
console.error('❌ publish-manifest failed:', e.message);
|
|
630
|
+
process.exit(1);
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
// QoL: doctor wrapper for skills/governance readiness
|
|
635
|
+
cmd
|
|
636
|
+
.command('doctor')
|
|
637
|
+
.description('Validate workspace skills and diagnose SubDAO governance readiness')
|
|
638
|
+
.argument('[key]', 'Optional skill key to validate frontmatter')
|
|
639
|
+
.option('--subdao <address>', 'SubDAO address for governance checks')
|
|
640
|
+
.action(async (key, opts) => {
|
|
641
|
+
try {
|
|
642
|
+
const { readWorkspace } = require('../services/prompts/workspace');
|
|
643
|
+
const { resolveSkillFileByKey, readFrontmatter, findWorkspaceSkills } = require('../services/skills/discovery');
|
|
644
|
+
const { diagnoseSubDAO } = require('../services/governance/doctor');
|
|
645
|
+
const WalletManager = require('../wallet-manager');
|
|
646
|
+
|
|
647
|
+
// Validate skill frontmatter if requested
|
|
648
|
+
if (key) {
|
|
649
|
+
const ws = readWorkspace() || {};
|
|
650
|
+
const resolved = resolveSkillFileByKey({ promptsDir: ws.promptsDir || 'prompts', key });
|
|
651
|
+
if (!resolved) throw new Error(`Skill not found for key '${key}'`);
|
|
652
|
+
const fm = readFrontmatter(resolved.path);
|
|
653
|
+
console.log(`✅ Skill frontmatter OK for ${key}`);
|
|
654
|
+
console.log(fm);
|
|
655
|
+
} else {
|
|
656
|
+
// List count as a quick sanity
|
|
657
|
+
const ws = readWorkspace() || {};
|
|
658
|
+
const list = findWorkspaceSkills({ promptsDir: ws.promptsDir || 'prompts' });
|
|
659
|
+
console.log(`✅ Workspace skills found: ${list.length}`);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (opts.subdao) {
|
|
663
|
+
const wallet = new WalletManager();
|
|
664
|
+
await wallet.connect();
|
|
665
|
+
const provider = wallet.getProvider();
|
|
666
|
+
const signer = wallet.getSigner();
|
|
667
|
+
console.log('🩺 Running Governance Doctor...');
|
|
668
|
+
const doctor = await diagnoseSubDAO({ subdao: opts.subdao, provider, signer });
|
|
669
|
+
console.log(JSON.stringify({
|
|
670
|
+
status: doctor.recommendations.length ? 'warn' : 'ok',
|
|
671
|
+
canPropose: doctor.canPropose === true,
|
|
672
|
+
subdao: doctor.subdao,
|
|
673
|
+
issues: doctor.recommendations,
|
|
674
|
+
}, null, 2));
|
|
675
|
+
}
|
|
676
|
+
} catch (e) {
|
|
677
|
+
console.error('❌ doctor failed:', e.message);
|
|
678
|
+
process.exit(1);
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
// QoL: use (MCP helper)
|
|
683
|
+
cmd
|
|
684
|
+
.command('use')
|
|
685
|
+
.description('Help use a skill with MCP (prints config; optional server start)')
|
|
686
|
+
.argument('<key>', 'Skill key (e.g., skills/my-skill)')
|
|
687
|
+
.option('--server-start', 'Start MCP HTTP server')
|
|
688
|
+
.option('--stdio-config', 'Print Claude Desktop stdio config snippet instead')
|
|
689
|
+
.action(async (key, opts) => {
|
|
690
|
+
try {
|
|
691
|
+
const path = require('path');
|
|
692
|
+
const chalk = require('chalk');
|
|
693
|
+
const { readWorkspace } = require('../services/prompts/workspace');
|
|
694
|
+
const { resolveSkillFileByKey } = require('../services/skills/discovery');
|
|
695
|
+
|
|
696
|
+
const ws = readWorkspace() || {};
|
|
697
|
+
const resolved = resolveSkillFileByKey({ promptsDir: ws.promptsDir || 'prompts', key });
|
|
698
|
+
if (!resolved) throw new Error(`Skill not found for key '${key}'`);
|
|
699
|
+
|
|
700
|
+
if (opts.stdioConfig) {
|
|
701
|
+
const stdioPath = path.resolve(__dirname, '..', 'mcp-server-stdio.js');
|
|
702
|
+
const snippet = {
|
|
703
|
+
mcpServers: {
|
|
704
|
+
sage: {
|
|
705
|
+
command: process.execPath,
|
|
706
|
+
args: [stdioPath],
|
|
707
|
+
env: {},
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
console.log(JSON.stringify(snippet, null, 2));
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (opts.serverStart) {
|
|
716
|
+
const MCPServer = require('../mcp-server');
|
|
717
|
+
const server = new MCPServer({ port: 3333 });
|
|
718
|
+
await server.start();
|
|
719
|
+
console.log(chalk.green('✅ MCP server started on http://localhost:3333'));
|
|
720
|
+
console.log(chalk.cyan('Use in Cursor/Claude by configuring the HTTP/WS MCP server.'));
|
|
721
|
+
console.log(chalk.gray('Press Ctrl+C to stop.'));
|
|
722
|
+
} else {
|
|
723
|
+
console.log(chalk.cyan('To start the MCP server, run:'));
|
|
724
|
+
console.log(' sage mcp start');
|
|
725
|
+
console.log(chalk.cyan('For Claude stdio config, run:'));
|
|
726
|
+
console.log(' sage skills use', key, '--stdio-config');
|
|
727
|
+
}
|
|
728
|
+
} catch (e) {
|
|
729
|
+
console.error('❌ use failed:', e.message);
|
|
730
|
+
process.exit(1);
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
program.addCommand(cmd);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
module.exports = { register };
|