@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.
Files changed (42) hide show
  1. package/dist/cli/commands/config.js +28 -0
  2. package/dist/cli/commands/doctor.js +87 -8
  3. package/dist/cli/commands/gov-config.js +81 -0
  4. package/dist/cli/commands/governance.js +152 -72
  5. package/dist/cli/commands/library.js +9 -0
  6. package/dist/cli/commands/proposals.js +187 -17
  7. package/dist/cli/commands/skills.js +737 -0
  8. package/dist/cli/commands/subdao.js +96 -132
  9. package/dist/cli/config/playbooks.json +62 -0
  10. package/dist/cli/config.js +15 -0
  11. package/dist/cli/governance-manager.js +25 -4
  12. package/dist/cli/index.js +6 -7
  13. package/dist/cli/library-manager.js +79 -0
  14. package/dist/cli/mcp-server-stdio.js +1387 -166
  15. package/dist/cli/schemas/manifest.schema.json +55 -0
  16. package/dist/cli/services/doctor/fixers.js +134 -0
  17. package/dist/cli/services/governance/doctor.js +140 -0
  18. package/dist/cli/services/governance/playbooks.js +97 -0
  19. package/dist/cli/services/mcp/bulk-operations.js +272 -0
  20. package/dist/cli/services/mcp/dependency-analyzer.js +202 -0
  21. package/dist/cli/services/mcp/library-listing.js +2 -2
  22. package/dist/cli/services/mcp/local-prompt-collector.js +1 -0
  23. package/dist/cli/services/mcp/manifest-downloader.js +5 -3
  24. package/dist/cli/services/mcp/manifest-fetcher.js +17 -1
  25. package/dist/cli/services/mcp/manifest-workflows.js +127 -15
  26. package/dist/cli/services/mcp/quick-start.js +287 -0
  27. package/dist/cli/services/mcp/stdio-runner.js +30 -5
  28. package/dist/cli/services/mcp/template-manager.js +156 -0
  29. package/dist/cli/services/mcp/templates/default-templates.json +84 -0
  30. package/dist/cli/services/mcp/tool-args-validator.js +56 -0
  31. package/dist/cli/services/mcp/trending-formatter.js +1 -1
  32. package/dist/cli/services/mcp/unified-prompt-search.js +2 -2
  33. package/dist/cli/services/metaprompt/designer.js +12 -5
  34. package/dist/cli/services/skills/discovery.js +99 -0
  35. package/dist/cli/services/subdao/applier.js +229 -0
  36. package/dist/cli/services/subdao/planner.js +142 -0
  37. package/dist/cli/subdao-manager.js +14 -0
  38. package/dist/cli/utils/aliases.js +28 -6
  39. package/dist/cli/utils/contract-error-decoder.js +61 -0
  40. package/dist/cli/utils/suggestions.js +25 -13
  41. package/package.json +3 -2
  42. package/src/schemas/manifest.schema.json +55 -0
@@ -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
 
@@ -0,0 +1,156 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Simple prompt template manager for MCP flows.
6
+ *
7
+ * Templates are defined as JSON data and rendered into concrete prompts
8
+ * which are persisted using quickStart.quickCreatePrompt.
9
+ */
10
+ function createTemplateManager({
11
+ quickStart,
12
+ fsModule = fs,
13
+ pathModule = path,
14
+ logger = console,
15
+ } = {}) {
16
+ if (!quickStart || typeof quickStart.quickCreatePrompt !== 'function') {
17
+ throw new Error('TemplateManager requires quickStart.quickCreatePrompt');
18
+ }
19
+
20
+ const defaultTemplatePath = pathModule.join(__dirname, 'templates', 'default-templates.json');
21
+ let templatesCache = null;
22
+
23
+ function loadTemplates() {
24
+ if (templatesCache) return templatesCache;
25
+ try {
26
+ const raw = fsModule.readFileSync(defaultTemplatePath, 'utf8');
27
+ const parsed = JSON.parse(raw);
28
+ const list = Array.isArray(parsed) ? parsed : parsed.templates || [];
29
+ templatesCache = list;
30
+ } catch (error) {
31
+ logger?.warn?.('template_manager_load_failed', { path: defaultTemplatePath, err: error.message || String(error) });
32
+ templatesCache = [];
33
+ }
34
+ return templatesCache;
35
+ }
36
+
37
+ function listTemplates({ category, search } = {}) {
38
+ const all = loadTemplates();
39
+ const cat = (category || '').toLowerCase();
40
+ const term = (search || '').toLowerCase();
41
+ const filtered = all.filter((tpl) => {
42
+ if (cat && String(tpl.category || '').toLowerCase() !== cat) return false;
43
+ if (!term) return true;
44
+ const haystack = [
45
+ tpl.key || '',
46
+ tpl.name || '',
47
+ tpl.description || '',
48
+ (tpl.metadata && tpl.metadata.suggested_description) || '',
49
+ ]
50
+ .join(' ')
51
+ .toLowerCase();
52
+ return haystack.includes(term);
53
+ });
54
+
55
+ const items = filtered.map((tpl) => {
56
+ const vars = Array.isArray(tpl.variables) ? tpl.variables : [];
57
+ return {
58
+ key: tpl.key,
59
+ name: tpl.name,
60
+ category: tpl.category || '',
61
+ summary: tpl.description || (tpl.metadata && tpl.metadata.suggested_description) || '',
62
+ requiredVariables: vars.filter((v) => v && v.required).map((v) => v.name),
63
+ optionalVariables: vars.filter((v) => v && !v.required).map((v) => v.name),
64
+ };
65
+ });
66
+
67
+ return { templates: items };
68
+ }
69
+
70
+ function getTemplate({ key }) {
71
+ const all = loadTemplates();
72
+ const id = String(key || '').trim();
73
+ if (!id) {
74
+ throw new Error('template key is required');
75
+ }
76
+ const tpl = all.find((t) => t.key === id);
77
+ if (!tpl) {
78
+ throw new Error(`Template not found for key "${key}"`);
79
+ }
80
+ return { template: tpl };
81
+ }
82
+
83
+ function renderWithVars(templateString, customize, variables) {
84
+ let result = String(templateString || '');
85
+ const vars = Array.isArray(variables) ? variables : [];
86
+ for (const v of vars) {
87
+ if (!v || !v.name) continue;
88
+ const value = customize && Object.prototype.hasOwnProperty.call(customize, v.name)
89
+ ? String(customize[v.name])
90
+ : (v.default !== undefined ? String(v.default) : `\${${v.name}}`);
91
+ const pattern = new RegExp(`\\$\\{${v.name}\\}`, 'g');
92
+ result = result.replace(pattern, value);
93
+ }
94
+ return result;
95
+ }
96
+
97
+ async function createFromTemplate({ template, customize, library, name } = {}) {
98
+ const { template: tpl } = getTemplate({ key: template });
99
+ const vars = Array.isArray(tpl.variables) ? tpl.variables : [];
100
+ const missing = vars
101
+ .filter((v) => v && v.required)
102
+ .filter((v) => !customize || customize[v.name] === undefined || customize[v.name] === null || customize[v.name] === '');
103
+
104
+ if (missing.length) {
105
+ const names = missing.map((v) => v.name).join(', ');
106
+ throw new Error(`Missing required variables for template "${tpl.key}": ${names}`);
107
+ }
108
+
109
+ const content = renderWithVars(tpl.content_template || '', customize, vars);
110
+
111
+ const promptName =
112
+ name ||
113
+ tpl.name ||
114
+ (customize && customize.title) ||
115
+ (customize && customize.project_type && `${customize.project_type} Prompt`) ||
116
+ tpl.key;
117
+
118
+ let description = '';
119
+ if (tpl.metadata && tpl.metadata.suggested_description) {
120
+ description = renderWithVars(tpl.metadata.suggested_description, customize, vars);
121
+ } else {
122
+ description = tpl.description || '';
123
+ }
124
+
125
+ const defaultTags = Array.isArray(tpl.metadata?.default_tags)
126
+ ? tpl.metadata.default_tags.map((t) => String(t).trim()).filter(Boolean)
127
+ : [];
128
+ const tags = defaultTags;
129
+
130
+ const quickResult = await quickStart.quickCreatePrompt({
131
+ name: promptName,
132
+ content,
133
+ description,
134
+ library,
135
+ tags,
136
+ });
137
+
138
+ return {
139
+ success: true,
140
+ template: tpl.key,
141
+ variables: customize || {},
142
+ tags,
143
+ quickCreate: quickResult,
144
+ };
145
+ }
146
+
147
+ return {
148
+ listTemplates,
149
+ getTemplate,
150
+ createFromTemplate,
151
+ };
152
+ }
153
+
154
+ module.exports = {
155
+ createTemplateManager,
156
+ };
@@ -0,0 +1,84 @@
1
+ [
2
+ {
3
+ "key": "landscape-design-planner",
4
+ "name": "Landscape Design Planner",
5
+ "category": "design",
6
+ "description": "Structured planner for landscape and courtyard design projects.",
7
+ "variables": [
8
+ { "name": "project_type", "required": true, "description": "courtyard, garden, patio, deck, etc." },
9
+ { "name": "region", "required": true, "description": "location or climate zone" },
10
+ { "name": "style", "required": false, "description": "modern, traditional, cottage, etc.", "default": "modern" },
11
+ { "name": "budget_range", "required": false, "description": "low, medium, high, or numeric range" }
12
+ ],
13
+ "content_template": "You are an experienced landscape designer specializing in ${project_type} projects in ${region}.\n\nYour tasks:\n1. Analyze the site conditions and constraints.\n2. Propose a high-level layout for the ${project_type}.\n3. Recommend materials, planting zones, and functional zones.\n4. Highlight tradeoffs given the budget range (${budget_range}).\n\nRespond with:\n- Site assumptions\n- Layout description\n- Material and planting recommendations\n- Risks and open questions.",
14
+ "metadata": {
15
+ "default_tags": ["design", "landscape", "courtyard"],
16
+ "suggested_description": "${project_type} planning prompt for projects in ${region}."
17
+ }
18
+ },
19
+ {
20
+ "key": "code-review-assistant",
21
+ "name": "Code Review Assistant",
22
+ "category": "development",
23
+ "description": "Prompt for structured, actionable code review feedback.",
24
+ "variables": [
25
+ { "name": "language", "required": true, "description": "primary language or stack" },
26
+ { "name": "context", "required": false, "description": "repository or feature context", "default": "general" },
27
+ { "name": "focus_areas", "required": false, "description": "performance, readability, security, etc." }
28
+ ],
29
+ "content_template": "You are a senior ${language} engineer performing a code review.\n\nContext: ${context}\nFocus areas: ${focus_areas}\n\nFor the given code, provide:\n1. High-level summary of what the code does.\n2. Strengths.\n3. Issues grouped by severity (critical, important, minor).\n4. Concrete suggestions and examples for improvement.\n5. Any follow-up questions.\n\nBe concise but specific; avoid restating the code.",
30
+ "metadata": {
31
+ "default_tags": ["code-review", "development"],
32
+ "suggested_description": "Structured ${language} code review assistant prompt."
33
+ }
34
+ },
35
+ {
36
+ "key": "data-analysis-brief",
37
+ "name": "Data Analysis Brief",
38
+ "category": "analysis",
39
+ "description": "Briefing prompt for exploratory or explanatory data analysis.",
40
+ "variables": [
41
+ { "name": "dataset_description", "required": true, "description": "what the data represents" },
42
+ { "name": "audience", "required": false, "description": "decision makers, technical team, etc.", "default": "mixed" },
43
+ { "name": "primary_question", "required": true, "description": "main question to answer" }
44
+ ],
45
+ "content_template": "You are a data analyst preparing a structured analysis plan.\n\nDataset: ${dataset_description}\nAudience: ${audience}\nPrimary question: ${primary_question}\n\nProvide:\n1. Clarifying questions you would ask before analysis.\n2. Proposed analysis steps and methods.\n3. Key metrics and visualizations you would produce.\n4. Potential risks, caveats, or biases.\n5. How you would communicate results to the audience.",
46
+ "metadata": {
47
+ "default_tags": ["analysis", "planning"],
48
+ "suggested_description": "Plan an analysis for ${dataset_description} focusing on ${primary_question}."
49
+ }
50
+ },
51
+ {
52
+ "key": "writing-assistant",
53
+ "name": "Writing Assistant",
54
+ "category": "writing",
55
+ "description": "General-purpose structured writing assistant prompt.",
56
+ "variables": [
57
+ { "name": "piece_type", "required": true, "description": "blog post, email, announcement, etc." },
58
+ { "name": "topic", "required": true, "description": "subject of the piece" },
59
+ { "name": "tone", "required": false, "description": "professional, friendly, playful, etc.", "default": "professional" }
60
+ ],
61
+ "content_template": "You are an assistant helping draft a ${piece_type} about ${topic} in a ${tone} tone.\n\nProvide:\n1. A suggested outline.\n2. A draft introduction paragraph.\n3. 2–3 key points with short supporting paragraphs.\n4. A concise conclusion or call to action.\n\nKeep the language clear and accessible.",
62
+ "metadata": {
63
+ "default_tags": ["writing", "content"],
64
+ "suggested_description": "${piece_type} assistant prompt for ${topic}."
65
+ }
66
+ },
67
+ {
68
+ "key": "governance-library-publish-helper",
69
+ "name": "Governance Library Publish Helper",
70
+ "category": "governance",
71
+ "description": "Helper prompt to plan publishing a Sage library to a SubDAO.",
72
+ "variables": [
73
+ { "name": "library_name", "required": true, "description": "name of the library being published" },
74
+ { "name": "subdao_name", "required": false, "description": "name of the target SubDAO" },
75
+ { "name": "risk_level", "required": false, "description": "low, medium, high", "default": "medium" }
76
+ ],
77
+ "content_template": "You are assisting with publishing the \"${library_name}\" library to a Sage SubDAO (${subdao_name}).\n\nFor this publication, outline:\n1. Why this library is valuable to the SubDAO.\n2. Any risks or tradeoffs (risk level: ${risk_level}).\n3. Preconditions for a safe rollout (tests, review, etc.).\n4. A short, clear proposal description for governance.\n\nRespond as a structured brief the proposer can paste into their governance proposal.",
78
+ "metadata": {
79
+ "default_tags": ["governance", "library", "publish"],
80
+ "suggested_description": "Governance brief helper for publishing ${library_name}."
81
+ }
82
+ }
83
+ ]
84
+
@@ -61,6 +61,7 @@ function createToolArgsValidator({ zodModule } = {}) {
61
61
  manifest: Z.record(Z.any()),
62
62
  subdao: Z.string().regex(/^0x[a-fA-F0-9]{40}$/).optional().or(Z.literal('')).default(''),
63
63
  description: Z.string().max(280).optional().default(''),
64
+ dry_run: Z.boolean().optional().default(false),
64
65
  }),
65
66
  propose_manifest: Z.object({
66
67
  cid: Z.string().min(10).max(200),
@@ -69,6 +70,61 @@ function createToolArgsValidator({ zodModule } = {}) {
69
70
  promptCount: Z.number().int().min(0).optional(),
70
71
  manifest: Z.record(Z.any()).optional(),
71
72
  }),
73
+ improve_prompt: Z.object({
74
+ key: Z.string().min(1).max(200),
75
+ library: Z.string().min(1).max(200).optional(),
76
+ pass: Z.enum(['single', 'deep']).optional().default('single'),
77
+ focus: Z.enum(['variables', 'edge-cases', 'output-quality', 'reusability', 'all']).optional().default('all'),
78
+ }),
79
+ rename_prompt: Z.object({
80
+ key: Z.string().min(1).max(200),
81
+ newKey: Z.string().min(1).max(200).optional(),
82
+ name: Z.string().min(1).max(200).optional(),
83
+ }),
84
+ update_library_metadata: Z.object({
85
+ library: Z.string().min(1).max(200),
86
+ name: Z.string().min(1).max(200).optional(),
87
+ description: Z.string().max(1000).optional(),
88
+ tags: Z.array(Z.string().max(64)).optional(),
89
+ apply_to_prompts: Z.boolean().optional().default(false),
90
+ merge_mode: Z.enum(['replace', 'merge']).optional().default('merge'),
91
+ }),
92
+ bulk_update_prompts: Z.object({
93
+ library: Z.string().min(1).max(200),
94
+ updates: Z.array(
95
+ Z.object({
96
+ key: Z.string().min(1).max(200),
97
+ name: Z.string().min(1).max(200).optional(),
98
+ description: Z.string().max(2000).optional(),
99
+ tags: Z.array(Z.string().max(64)).optional(),
100
+ content: Z.string().optional(),
101
+ }),
102
+ )
103
+ .min(1),
104
+ dry_run: Z.boolean().optional().default(false),
105
+ }),
106
+ list_templates: Z.object({
107
+ category: Z.string().max(200).optional(),
108
+ search: Z.string().max(500).optional(),
109
+ }),
110
+ get_template: Z.object({
111
+ key: Z.string().min(1).max(200),
112
+ }),
113
+ create_from_template: Z.object({
114
+ template: Z.string().min(1).max(200),
115
+ customize: Z.record(Z.any()),
116
+ library: Z.string().min(1).max(200).optional(),
117
+ name: Z.string().min(1).max(200).optional(),
118
+ }),
119
+ analyze_dependencies: Z.object({
120
+ library: Z.string().min(1).max(200),
121
+ analysis_type: Z.enum(['variables', 'all']).optional().default('variables'),
122
+ }),
123
+ suggest_subdaos_for_library: Z.object({
124
+ library: Z.string().min(1).max(200),
125
+ limit: Z.number().int().min(1).max(20).optional().default(5),
126
+ mode_filter: Z.enum(['any', 'creator', 'squad', 'community']).optional().default('any'),
127
+ }),
72
128
  };
73
129
 
74
130
  return function validate(name, args) {
@@ -14,7 +14,7 @@ function buildTrendingResponse(suggestions = [], limit = 10) {
14
14
  return {
15
15
  content: [
16
16
  { type: 'text', text },
17
- { type: 'json', text: JSON.stringify({ items }, null, 2) },
17
+ { type: 'text', text: '```json\n' + JSON.stringify({ items }, null, 2) + '\n```' },
18
18
  ],
19
19
  };
20
20
  }
@@ -76,8 +76,8 @@ function createUnifiedPromptSearcher({
76
76
  content: [
77
77
  { type: 'text', text },
78
78
  {
79
- type: 'json',
80
- text: JSON.stringify({ total, page: normalizedPage, pageSize: normalizedPageSize, results: pageResults }, null, DEFAULT_INDENT),
79
+ type: 'text',
80
+ text: '```json\n' + JSON.stringify({ total, page: normalizedPage, pageSize: normalizedPageSize, results: pageResults }, null, DEFAULT_INDENT) + '\n```',
81
81
  },
82
82
  ],
83
83
  };