@sage-protocol/cli 0.3.0 → 0.3.3

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.
Files changed (37) hide show
  1. package/dist/cli/commands/doctor.js +87 -8
  2. package/dist/cli/commands/gov-config.js +81 -0
  3. package/dist/cli/commands/governance.js +152 -72
  4. package/dist/cli/commands/library.js +9 -0
  5. package/dist/cli/commands/proposals.js +187 -17
  6. package/dist/cli/commands/skills.js +175 -21
  7. package/dist/cli/commands/subdao.js +22 -2
  8. package/dist/cli/config/playbooks.json +15 -0
  9. package/dist/cli/governance-manager.js +25 -4
  10. package/dist/cli/index.js +5 -6
  11. package/dist/cli/library-manager.js +79 -0
  12. package/dist/cli/mcp-server-stdio.js +1655 -82
  13. package/dist/cli/schemas/manifest.schema.json +55 -0
  14. package/dist/cli/services/doctor/fixers.js +134 -0
  15. package/dist/cli/services/mcp/bulk-operations.js +272 -0
  16. package/dist/cli/services/mcp/dependency-analyzer.js +202 -0
  17. package/dist/cli/services/mcp/library-listing.js +2 -2
  18. package/dist/cli/services/mcp/local-prompt-collector.js +1 -0
  19. package/dist/cli/services/mcp/manifest-downloader.js +5 -3
  20. package/dist/cli/services/mcp/manifest-fetcher.js +17 -1
  21. package/dist/cli/services/mcp/manifest-workflows.js +127 -15
  22. package/dist/cli/services/mcp/quick-start.js +287 -0
  23. package/dist/cli/services/mcp/stdio-runner.js +30 -5
  24. package/dist/cli/services/mcp/template-manager.js +156 -0
  25. package/dist/cli/services/mcp/templates/default-templates.json +84 -0
  26. package/dist/cli/services/mcp/tool-args-validator.js +66 -0
  27. package/dist/cli/services/mcp/trending-formatter.js +1 -1
  28. package/dist/cli/services/mcp/unified-prompt-search.js +2 -2
  29. package/dist/cli/services/metaprompt/designer.js +12 -5
  30. package/dist/cli/services/subdao/applier.js +208 -196
  31. package/dist/cli/services/subdao/planner.js +41 -6
  32. package/dist/cli/subdao-manager.js +14 -0
  33. package/dist/cli/utils/aliases.js +17 -2
  34. package/dist/cli/utils/contract-error-decoder.js +61 -0
  35. package/dist/cli/utils/suggestions.js +17 -12
  36. package/package.json +3 -2
  37. 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
- content = await ipfsManager.downloadPrompt(prompt.cid);
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
- throw new Error('Manifest schema not found (docs/schemas/manifest.schema.json)');
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 lines = (validate.errors || []).map((err) => `- ${err.instancePath || '/'} ${err.message || 'invalid'}`).join('\n');
71
- return { content: [{ type: 'text', text: `❌ Manifest validation failed:\n${lines}` }] };
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
- return { content: [{ type: 'text', text: `Error uploading manifest: ${error.message}` }] };
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
- const pushed = await pushManifestToIpfs({ manifest });
214
- const match = /CID:\s*([^\s]+)/.exec(pushed.content?.[0]?.text || '');
215
- const cid = match?.[1] || pushed.cid;
216
- if (!cid) {
217
- return { content: [{ type: 'text', text: '❌ Failed to upload manifest (no CID)' }] };
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 text = `✅ Manifest valid\nCID: ${cid}\n\n${proposed.content?.[0]?.text || ''}`;
228
- return { content: [{ type: 'text', text }], cid, payload: proposed.payload };
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
- const text = typeof payload === 'string' ? payload : JSON.stringify(payload);
40
- if (typeof output?.write === 'function') {
41
- output.write(`${text}\n`);
42
- } else {
43
- console.log(text); // eslint-disable-line no-console
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