@sage-protocol/cli 0.3.0 → 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/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 +175 -21
- package/dist/cli/commands/subdao.js +22 -2
- package/dist/cli/config/playbooks.json +15 -0
- package/dist/cli/governance-manager.js +25 -4
- package/dist/cli/index.js +5 -6
- package/dist/cli/library-manager.js +79 -0
- package/dist/cli/mcp-server-stdio.js +1374 -82
- package/dist/cli/schemas/manifest.schema.json +55 -0
- package/dist/cli/services/doctor/fixers.js +134 -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/subdao/applier.js +208 -196
- package/dist/cli/services/subdao/planner.js +41 -6
- package/dist/cli/subdao-manager.js +14 -0
- package/dist/cli/utils/aliases.js +17 -2
- package/dist/cli/utils/contract-error-decoder.js +61 -0
- package/dist/cli/utils/suggestions.js +17 -12
- package/package.json +3 -2
- package/src/schemas/manifest.schema.json +55 -0
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
const DEFAULT_GATEWAYS = [
|
|
2
|
+
'https://cloudflare-ipfs.com',
|
|
3
|
+
'https://gateway.pinata.cloud',
|
|
4
|
+
'https://ipfs.io',
|
|
2
5
|
'https://dweb.link',
|
|
3
6
|
'https://nftstorage.link',
|
|
4
|
-
'https://ipfs.io',
|
|
5
7
|
];
|
|
6
8
|
|
|
7
9
|
function buildGatewayList({ cid, preferredGateway }) {
|
|
8
10
|
const trimmedPreferred = preferredGateway ? preferredGateway.replace(/\/$/, '') : '';
|
|
9
11
|
const candidates = [];
|
|
10
12
|
if (trimmedPreferred) {
|
|
11
|
-
candidates.push(`${trimmedPreferred}/ipfs/${cid}`);
|
|
13
|
+
candidates.push(`${trimmedPreferred} /ipfs/${cid} `);
|
|
12
14
|
}
|
|
13
15
|
for (const base of DEFAULT_GATEWAYS) {
|
|
14
16
|
const formattedBase = base.replace(/\/$/, '');
|
|
15
|
-
const url = `${formattedBase}/ipfs/${cid}`;
|
|
17
|
+
const url = `${formattedBase} /ipfs/${cid} `;
|
|
16
18
|
if (!candidates.includes(url)) {
|
|
17
19
|
candidates.push(url);
|
|
18
20
|
}
|
|
@@ -22,6 +22,20 @@ function createManifestFetcher({
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
async function downloadManifest(manifestCid) {
|
|
25
|
+
// Check if this is a local manifest (stored in ~/.sage/libraries/)
|
|
26
|
+
if (manifestCid && manifestCid.startsWith('local_')) {
|
|
27
|
+
try {
|
|
28
|
+
const { loadPinnedManifest } = require('../library/local-cache');
|
|
29
|
+
const cliConfig = require('../../utils/cli-config');
|
|
30
|
+
const { manifest } = loadPinnedManifest({ config: cliConfig, cid: manifestCid });
|
|
31
|
+
return manifest;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
logger?.debug?.('local_manifest_load_failed', { cid: manifestCid, err: error.message || String(error) });
|
|
34
|
+
throw new Error(`Failed to load local manifest ${manifestCid}: ${error.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// For IPFS CIDs, try gateways
|
|
25
39
|
const gateways = buildGatewayList(manifestCid);
|
|
26
40
|
let lastError;
|
|
27
41
|
for (const gateway of gateways) {
|
|
@@ -84,7 +98,9 @@ function createManifestFetcher({
|
|
|
84
98
|
let content = '';
|
|
85
99
|
if (includeContent && prompt?.cid && ipfsManager) {
|
|
86
100
|
try {
|
|
87
|
-
|
|
101
|
+
// Download prompt JSON directly from IPFS and extract content
|
|
102
|
+
const data = await ipfsManager.downloadJson(prompt.cid);
|
|
103
|
+
content = data?.content || data?.prompt?.content || JSON.stringify(data);
|
|
88
104
|
} catch (error) {
|
|
89
105
|
logger?.debug?.('prompt_content_fetch_failed', { cid: prompt.cid, err: error.message || String(error) });
|
|
90
106
|
}
|
|
@@ -18,6 +18,63 @@ function defaultAddFormats(ajv) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// Lightweight embedded fallback schema for environments where docs/schemas are not present
|
|
22
|
+
const FALLBACK_MANIFEST_SCHEMA = {
|
|
23
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
24
|
+
$id: 'https://sage-protocol.org/schemas/manifest.schema.json',
|
|
25
|
+
title: 'Sage Library Manifest',
|
|
26
|
+
type: 'object',
|
|
27
|
+
required: ['version', 'library', 'prompts'],
|
|
28
|
+
additionalProperties: true,
|
|
29
|
+
properties: {
|
|
30
|
+
version: {
|
|
31
|
+
anyOf: [
|
|
32
|
+
{ type: 'string', const: '2.0.0' },
|
|
33
|
+
{ type: 'integer', const: 2 },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
library: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
required: ['name'],
|
|
39
|
+
additionalProperties: true,
|
|
40
|
+
properties: {
|
|
41
|
+
name: { type: 'string', minLength: 1 },
|
|
42
|
+
description: { type: 'string' },
|
|
43
|
+
previous: { type: 'string' },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
prompts: {
|
|
47
|
+
type: 'array',
|
|
48
|
+
minItems: 0,
|
|
49
|
+
items: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
required: ['key'],
|
|
52
|
+
additionalProperties: true,
|
|
53
|
+
properties: {
|
|
54
|
+
key: { type: 'string', minLength: 1 },
|
|
55
|
+
cid: { type: 'string', minLength: 46 },
|
|
56
|
+
name: { type: 'string' },
|
|
57
|
+
description: { type: 'string' },
|
|
58
|
+
tags: {
|
|
59
|
+
type: 'array',
|
|
60
|
+
items: { type: 'string', minLength: 1 },
|
|
61
|
+
},
|
|
62
|
+
files: {
|
|
63
|
+
type: 'array',
|
|
64
|
+
items: { type: 'string', minLength: 1 },
|
|
65
|
+
minItems: 0,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
compositions: { type: 'object' },
|
|
71
|
+
dependencies: {
|
|
72
|
+
type: 'array',
|
|
73
|
+
items: { type: 'string' },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
21
78
|
function createManifestWorkflows({
|
|
22
79
|
provider,
|
|
23
80
|
addressResolution,
|
|
@@ -27,8 +84,11 @@ function createManifestWorkflows({
|
|
|
27
84
|
logger = console,
|
|
28
85
|
processEnv = process.env,
|
|
29
86
|
schemaPaths = [
|
|
87
|
+
// Package-relative path (for published CLI)
|
|
88
|
+
path.join(__dirname, '..', '..', 'schemas', 'manifest.schema.json'),
|
|
89
|
+
// Monorepo docs locations (dev)
|
|
30
90
|
path.join(process.cwd(), 'docs', 'schemas', 'manifest.schema.json'),
|
|
31
|
-
path.join(__dirname, '..', '..', '..', 'docs', 'schemas', 'manifest.schema.json'),
|
|
91
|
+
path.join(__dirname, '..', '..', '..', '..', 'docs', 'schemas', 'manifest.schema.json'),
|
|
32
92
|
],
|
|
33
93
|
fsModule = fs,
|
|
34
94
|
pathModule = path,
|
|
@@ -53,7 +113,9 @@ function createManifestWorkflows({
|
|
|
53
113
|
logger?.warn?.('manifest_schema_load_failed', { path: schemaPath, err: error.message || String(error) });
|
|
54
114
|
}
|
|
55
115
|
}
|
|
56
|
-
|
|
116
|
+
// Fallback: embedded schema so MCP/CLI can still validate outside the monorepo.
|
|
117
|
+
logger?.warn?.('manifest_schema_fallback_used');
|
|
118
|
+
return FALLBACK_MANIFEST_SCHEMA;
|
|
57
119
|
}
|
|
58
120
|
|
|
59
121
|
async function validateManifest({ manifest }) {
|
|
@@ -67,8 +129,23 @@ function createManifestWorkflows({
|
|
|
67
129
|
if (ok) {
|
|
68
130
|
return { content: [{ type: 'text', text: '✅ Manifest is valid' }] };
|
|
69
131
|
}
|
|
70
|
-
const
|
|
71
|
-
|
|
132
|
+
const errors = validate.errors || [];
|
|
133
|
+
const lines = errors.map((err) => `- ${err.instancePath || '/'} ${err.message || 'invalid'}`).join('\n');
|
|
134
|
+
|
|
135
|
+
// Add targeted hints for common pitfalls (version, library)
|
|
136
|
+
const hasVersionError = errors.some((e) => (e.instancePath === '/version'));
|
|
137
|
+
const hasLibraryError = errors.some((e) => e.instancePath === '' && /library/.test(e.message || ''));
|
|
138
|
+
|
|
139
|
+
let hint = '';
|
|
140
|
+
if (hasVersionError) {
|
|
141
|
+
hint += '\n\nHint: use "version": 2 or "version": "2.0.0" for v2 manifests.';
|
|
142
|
+
}
|
|
143
|
+
if (hasLibraryError || !manifest?.library) {
|
|
144
|
+
hint += '\nHint: manifest must include a top-level "library" object, e.g.:\n' +
|
|
145
|
+
'{\n "version": 2,\n "library": { "name": "My Library", "description": "..." },\n "prompts": []\n}';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { content: [{ type: 'text', text: `❌ Manifest validation failed:\n${lines}${hint}` }] };
|
|
72
149
|
} catch (error) {
|
|
73
150
|
return { content: [{ type: 'text', text: `Error validating manifest: ${error.message}` }] };
|
|
74
151
|
}
|
|
@@ -81,7 +158,15 @@ function createManifestWorkflows({
|
|
|
81
158
|
const cid = await ipfs.uploadJson(manifest, name);
|
|
82
159
|
return { content: [{ type: 'text', text: `✅ Manifest uploaded to IPFS\nCID: ${cid}` }], cid };
|
|
83
160
|
} catch (error) {
|
|
84
|
-
|
|
161
|
+
const msg = error?.message || String(error);
|
|
162
|
+
let help = msg;
|
|
163
|
+
if (/All IPFS providers failed/i.test(msg)) {
|
|
164
|
+
help = 'All IPFS providers failed. Ensure your IPFS settings are configured.\n' +
|
|
165
|
+
'- For worker: set SAGE_IPFS_WORKER_URL (and SAGE_IPFS_UPLOAD_TOKEN if required)\n' +
|
|
166
|
+
'- For Pinata: set PINATA_JWT or PINATA_API_KEY/SECRET\n' +
|
|
167
|
+
'- Or switch provider via SAGE_IPFS_PROVIDER=pinata|worker';
|
|
168
|
+
}
|
|
169
|
+
return { content: [{ type: 'text', text: `Error uploading manifest: ${help}` }] };
|
|
85
170
|
}
|
|
86
171
|
}
|
|
87
172
|
|
|
@@ -117,7 +202,7 @@ function createManifestWorkflows({
|
|
|
117
202
|
let mode = { operator: false, governance: 'Unknown' };
|
|
118
203
|
try {
|
|
119
204
|
mode = await detectGovMode({ provider, subdao, governor, timelock });
|
|
120
|
-
} catch (_) {}
|
|
205
|
+
} catch (_) { }
|
|
121
206
|
|
|
122
207
|
const payload = { targets, values, calldatas, description: desc };
|
|
123
208
|
const hints = [];
|
|
@@ -202,7 +287,7 @@ function createManifestWorkflows({
|
|
|
202
287
|
}
|
|
203
288
|
}
|
|
204
289
|
|
|
205
|
-
async function publishManifestFlow({ manifest, subdao = '', description = '' }) {
|
|
290
|
+
async function publishManifestFlow({ manifest, subdao = '', description = '', dry_run = false }) {
|
|
206
291
|
try {
|
|
207
292
|
const validation = await validateManifest({ manifest });
|
|
208
293
|
const firstText = validation.content?.[0]?.text || '';
|
|
@@ -210,22 +295,49 @@ function createManifestWorkflows({
|
|
|
210
295
|
return { content: [{ type: 'text', text: `❌ Validation failed. Fix issues first.\n\n${firstText}` }] };
|
|
211
296
|
}
|
|
212
297
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
298
|
+
let cid = null;
|
|
299
|
+
let pushText = '';
|
|
300
|
+
if (!dry_run) {
|
|
301
|
+
const pushed = await pushManifestToIpfs({ manifest });
|
|
302
|
+
pushText = pushed.content?.[0]?.text || '';
|
|
303
|
+
const match = /CID:\s*([^\s]+)/.exec(pushText);
|
|
304
|
+
cid = match?.[1] || pushed.cid;
|
|
305
|
+
if (!cid) {
|
|
306
|
+
return { content: [{ type: 'text', text: '❌ Failed to upload manifest (no CID)' }] };
|
|
307
|
+
}
|
|
218
308
|
}
|
|
219
309
|
|
|
220
310
|
const proposed = await proposeManifest({
|
|
221
|
-
cid,
|
|
311
|
+
cid: cid || '(dry-run)',
|
|
222
312
|
subdao,
|
|
223
313
|
description,
|
|
224
314
|
promptCount: Array.isArray(manifest?.prompts) ? manifest.prompts.length : 0,
|
|
225
315
|
manifest,
|
|
226
316
|
});
|
|
227
|
-
const
|
|
228
|
-
|
|
317
|
+
const textLines = [];
|
|
318
|
+
textLines.push('✅ Manifest valid');
|
|
319
|
+
if (cid) textLines.push(`CID: ${cid}`);
|
|
320
|
+
if (dry_run) {
|
|
321
|
+
textLines.push('');
|
|
322
|
+
textLines.push('Dry run: no IPFS upload performed. Use this payload and CLI hints to publish when ready.');
|
|
323
|
+
textLines.push('');
|
|
324
|
+
textLines.push('When you are ready to publish from your terminal:');
|
|
325
|
+
textLines.push('1) Save this manifest JSON to a file (e.g. ./manifest.json).');
|
|
326
|
+
if (subdao) {
|
|
327
|
+
textLines.push(`2) Upload & schedule/propose via CLI:`);
|
|
328
|
+
textLines.push(` sage library push ./manifest.json --subdao ${subdao} --pin --wait`);
|
|
329
|
+
} else {
|
|
330
|
+
textLines.push('2) Upload & schedule/propose via CLI (replace SUBDAO):');
|
|
331
|
+
textLines.push(' sage library push ./manifest.json --subdao 0xYourSubDAO --pin --wait');
|
|
332
|
+
}
|
|
333
|
+
} else if (pushText) {
|
|
334
|
+
textLines.push('');
|
|
335
|
+
textLines.push(pushText);
|
|
336
|
+
}
|
|
337
|
+
textLines.push('');
|
|
338
|
+
textLines.push(proposed.content?.[0]?.text || '');
|
|
339
|
+
|
|
340
|
+
return { content: [{ type: 'text', text: textLines.join('\n') }], cid, payload: proposed.payload };
|
|
229
341
|
} catch (error) {
|
|
230
342
|
return { content: [{ type: 'text', text: `Error publishing manifest flow: ${error.message}` }] };
|
|
231
343
|
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const { LibraryManager } = require('../../library-manager');
|
|
4
|
+
|
|
5
|
+
function createQuickStart({
|
|
6
|
+
libraryManager = new LibraryManager(),
|
|
7
|
+
logger = console,
|
|
8
|
+
}) {
|
|
9
|
+
|
|
10
|
+
function ensureDefaultLibrary() {
|
|
11
|
+
const pinned = libraryManager.listPinned();
|
|
12
|
+
// Look for "My Library" or "default"
|
|
13
|
+
let defaultLib = pinned.find(p => p.name === 'My Library' || p.name === 'default');
|
|
14
|
+
|
|
15
|
+
if (!defaultLib) {
|
|
16
|
+
// Create "My Library"
|
|
17
|
+
const result = libraryManager.createLibrary('My Library', 'Default library for quick prompts');
|
|
18
|
+
defaultLib = { cid: result.cid, name: 'My Library' };
|
|
19
|
+
}
|
|
20
|
+
return defaultLib;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function generateKey(name) {
|
|
24
|
+
return name.toLowerCase()
|
|
25
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
26
|
+
.replace(/^-+|-+$/g, '');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function quickCreatePrompt({ name, content, description = '', library = '', tags } = {}) {
|
|
30
|
+
if (!name || !content) {
|
|
31
|
+
throw new Error('Name and content are required');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let targetLib;
|
|
35
|
+
if (library) {
|
|
36
|
+
const pinned = libraryManager.listPinned();
|
|
37
|
+
targetLib = pinned.find(p =>
|
|
38
|
+
p.name.toLowerCase() === library.toLowerCase() ||
|
|
39
|
+
p.cid === library
|
|
40
|
+
);
|
|
41
|
+
if (!targetLib) {
|
|
42
|
+
// If library doesn't exist, create it
|
|
43
|
+
const result = libraryManager.createLibrary(library, '');
|
|
44
|
+
targetLib = { cid: result.cid, name: library };
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
targetLib = ensureDefaultLibrary();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const key = generateKey(name);
|
|
51
|
+
|
|
52
|
+
// Check if key exists
|
|
53
|
+
const { manifest } = libraryManager.loadPinned(targetLib.cid);
|
|
54
|
+
if (manifest.prompts?.some(p => p.key === key)) {
|
|
55
|
+
throw new Error(`Prompt with key "${key}" already exists in library "${targetLib.name}". Use quick_iterate_prompt to update it.`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Add prompt
|
|
59
|
+
// We need to use the library manager's internal methods or replicate logic
|
|
60
|
+
// Since LibraryManager doesn't expose "addPrompt", we'll implement it here for now
|
|
61
|
+
// In a real refactor, we should move this to LibraryManager
|
|
62
|
+
|
|
63
|
+
const libDir = libraryManager.ensureLibrariesDir();
|
|
64
|
+
const manifestPath = path.join(libDir, `${targetLib.cid}.json`);
|
|
65
|
+
|
|
66
|
+
// Create prompt file
|
|
67
|
+
// We'll store it in a 'prompts' subdirectory next to the manifest if possible,
|
|
68
|
+
// but for local libraries, they are flat in ~/.sage/libraries/
|
|
69
|
+
// Let's create a prompts directory inside ~/.sage/libraries/prompts/
|
|
70
|
+
const promptsDir = path.join(libDir, 'prompts');
|
|
71
|
+
if (!fs.existsSync(promptsDir)) {
|
|
72
|
+
fs.mkdirSync(promptsDir, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const promptFileName = `${targetLib.cid}_${key}.md`;
|
|
76
|
+
const promptFilePath = path.join(promptsDir, promptFileName);
|
|
77
|
+
fs.writeFileSync(promptFilePath, content, 'utf8');
|
|
78
|
+
|
|
79
|
+
// Normalise tags (optional)
|
|
80
|
+
let promptTags = [];
|
|
81
|
+
if (tags !== undefined) {
|
|
82
|
+
let normalized = tags;
|
|
83
|
+
if (typeof normalized === 'string') {
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(normalized);
|
|
86
|
+
if (Array.isArray(parsed)) {
|
|
87
|
+
normalized = parsed;
|
|
88
|
+
} else {
|
|
89
|
+
normalized = String(normalized)
|
|
90
|
+
.split(/[,;]/)
|
|
91
|
+
.map((t) => t.trim())
|
|
92
|
+
.filter(Boolean);
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
normalized = String(normalized)
|
|
96
|
+
.split(/[,;]/)
|
|
97
|
+
.map((t) => t.trim())
|
|
98
|
+
.filter(Boolean);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(normalized)) {
|
|
102
|
+
promptTags = normalized.map((t) => String(t));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Update manifest
|
|
107
|
+
if (!manifest.prompts) manifest.prompts = [];
|
|
108
|
+
manifest.prompts.push({
|
|
109
|
+
key,
|
|
110
|
+
name,
|
|
111
|
+
description,
|
|
112
|
+
files: [`prompts/${promptFileName}`], // Relative path
|
|
113
|
+
tags: promptTags,
|
|
114
|
+
cid: '' // Local prompt
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
success: true,
|
|
121
|
+
key,
|
|
122
|
+
library: targetLib.name,
|
|
123
|
+
cid: targetLib.cid,
|
|
124
|
+
message: `Created prompt "${name}" (${key}) in "${targetLib.name}"`
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function quickIteratePrompt({ key, content, name, description, tags }) {
|
|
129
|
+
if (!key) throw new Error('Key is required');
|
|
130
|
+
|
|
131
|
+
// Find the prompt
|
|
132
|
+
const pinned = libraryManager.listPinned();
|
|
133
|
+
let found = null;
|
|
134
|
+
let foundLib = null;
|
|
135
|
+
|
|
136
|
+
for (const lib of pinned) {
|
|
137
|
+
const { manifest } = libraryManager.loadPinned(lib.cid);
|
|
138
|
+
const prompt = manifest.prompts?.find(p => p.key === key);
|
|
139
|
+
if (prompt) {
|
|
140
|
+
found = prompt;
|
|
141
|
+
const libDir = libraryManager.ensureLibrariesDir();
|
|
142
|
+
foundLib = { ...lib, manifest, manifestPath: path.join(libDir, `${lib.cid}.json`) };
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!found) {
|
|
148
|
+
throw new Error(`Prompt with key "${key}" not found. Use quick_create_prompt to create it.`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Update content if provided
|
|
152
|
+
if (content) {
|
|
153
|
+
const libDir = libraryManager.ensureLibrariesDir();
|
|
154
|
+
let promptFilePath;
|
|
155
|
+
|
|
156
|
+
if (found.files && found.files.length > 0) {
|
|
157
|
+
// Use existing file
|
|
158
|
+
const relativePath = found.files[0];
|
|
159
|
+
// Handle potential path issues
|
|
160
|
+
promptFilePath = path.join(libDir, relativePath);
|
|
161
|
+
|
|
162
|
+
// Backup existing
|
|
163
|
+
if (fs.existsSync(promptFilePath)) {
|
|
164
|
+
const backupPath = `${promptFilePath}.v${Date.now()}.bak`;
|
|
165
|
+
fs.copyFileSync(promptFilePath, backupPath);
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
// Create new file if none existed (legacy/imported)
|
|
169
|
+
const promptsDir = path.join(libDir, 'prompts');
|
|
170
|
+
if (!fs.existsSync(promptsDir)) fs.mkdirSync(promptsDir, { recursive: true });
|
|
171
|
+
const promptFileName = `${foundLib.cid}_${key}.md`;
|
|
172
|
+
promptFilePath = path.join(promptsDir, promptFileName);
|
|
173
|
+
found.files = [`prompts/${promptFileName}`];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fs.writeFileSync(promptFilePath, content, 'utf8');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Update metadata
|
|
180
|
+
if (name) {
|
|
181
|
+
found.name = name;
|
|
182
|
+
}
|
|
183
|
+
if (typeof description === 'string') {
|
|
184
|
+
found.description = description;
|
|
185
|
+
}
|
|
186
|
+
if (tags !== undefined) {
|
|
187
|
+
let normalized = tags;
|
|
188
|
+
if (typeof normalized === 'string') {
|
|
189
|
+
// Accept JSON array string or comma/space separated list
|
|
190
|
+
try {
|
|
191
|
+
const parsed = JSON.parse(normalized);
|
|
192
|
+
if (Array.isArray(parsed)) {
|
|
193
|
+
normalized = parsed;
|
|
194
|
+
} else {
|
|
195
|
+
normalized = String(normalized)
|
|
196
|
+
.split(/[,;]/)
|
|
197
|
+
.map((t) => t.trim())
|
|
198
|
+
.filter(Boolean);
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
normalized = String(normalized)
|
|
202
|
+
.split(/[,;]/)
|
|
203
|
+
.map((t) => t.trim())
|
|
204
|
+
.filter(Boolean);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (Array.isArray(normalized)) {
|
|
208
|
+
found.tags = normalized.map((t) => String(t));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Save manifest
|
|
213
|
+
fs.writeFileSync(foundLib.manifestPath, JSON.stringify(foundLib.manifest, null, 2), 'utf8');
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
success: true,
|
|
217
|
+
key,
|
|
218
|
+
library: foundLib.name,
|
|
219
|
+
message: `Updated prompt "${key}" (name: ${found.name || name || key})`
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function renamePrompt({ key, newKey, name }) {
|
|
224
|
+
if (!key) throw new Error('Key is required');
|
|
225
|
+
if (!newKey && !name) throw new Error('newKey or name required');
|
|
226
|
+
|
|
227
|
+
const pinned = libraryManager.listPinned();
|
|
228
|
+
let found = null;
|
|
229
|
+
let foundLib = null;
|
|
230
|
+
|
|
231
|
+
for (const lib of pinned) {
|
|
232
|
+
const { manifest } = libraryManager.loadPinned(lib.cid);
|
|
233
|
+
const prompt = manifest.prompts?.find((p) => p.key === key);
|
|
234
|
+
if (prompt) {
|
|
235
|
+
const libDir = libraryManager.ensureLibrariesDir();
|
|
236
|
+
found = prompt;
|
|
237
|
+
foundLib = {
|
|
238
|
+
...lib,
|
|
239
|
+
manifest,
|
|
240
|
+
manifestPath: path.join(libDir, `${lib.cid}.json`),
|
|
241
|
+
};
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!found || !foundLib) {
|
|
247
|
+
throw new Error(`Prompt with key "${key}" not found. Use list_prompts or search_prompts to discover keys.`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Handle key change with collision check
|
|
251
|
+
let finalKey = key;
|
|
252
|
+
if (newKey && newKey !== key) {
|
|
253
|
+
const exists = (foundLib.manifest.prompts || []).some(
|
|
254
|
+
(p) => p !== found && p.key === newKey,
|
|
255
|
+
);
|
|
256
|
+
if (exists) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`Prompt with key "${newKey}" already exists in library "${foundLib.name}". Choose a different key.`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
found.key = newKey;
|
|
262
|
+
finalKey = newKey;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (name) {
|
|
266
|
+
found.name = name;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
fs.writeFileSync(foundLib.manifestPath, JSON.stringify(foundLib.manifest, null, 2));
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
success: true,
|
|
273
|
+
oldKey: key,
|
|
274
|
+
key: finalKey,
|
|
275
|
+
library: foundLib.name,
|
|
276
|
+
message: `Renamed prompt "${key}" to "${finalKey}" (display name: ${found.name || finalKey})`,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
quickCreatePrompt,
|
|
282
|
+
quickIteratePrompt,
|
|
283
|
+
renamePrompt,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
module.exports = { createQuickStart };
|
|
@@ -35,12 +35,37 @@ function createStdIoRunner(baseOptions = {}) {
|
|
|
35
35
|
terminal: false,
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
let outputClosed = false;
|
|
39
|
+
if (output && typeof output.on === 'function') {
|
|
40
|
+
output.on('error', (err) => {
|
|
41
|
+
if (err && err.code === 'EPIPE') {
|
|
42
|
+
// Client closed the pipe; mark closed and let the process exit cleanly.
|
|
43
|
+
outputClosed = true;
|
|
44
|
+
try { rl.close(); } catch (_) {}
|
|
45
|
+
} else if (debugEnabled) {
|
|
46
|
+
errorWriter(`stdout error: ${err.message || String(err)}`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
const writeJson = (payload) => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
52
|
+
if (outputClosed) return;
|
|
53
|
+
try {
|
|
54
|
+
const text = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
55
|
+
if (typeof output?.write === 'function') {
|
|
56
|
+
output.write(`${text}\n`);
|
|
57
|
+
} else {
|
|
58
|
+
console.log(text); // eslint-disable-line no-console
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (err && err.code === 'EPIPE') {
|
|
62
|
+
outputClosed = true;
|
|
63
|
+
try { rl.close(); } catch (_) {}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (debugEnabled) {
|
|
67
|
+
errorWriter(`writeJson error: ${err.message || String(err)}`);
|
|
68
|
+
}
|
|
44
69
|
}
|
|
45
70
|
};
|
|
46
71
|
|