@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.
- 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 +1655 -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 +66 -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
|
@@ -23,6 +23,12 @@ const { resolveDiscoverySettings } = require('./services/ipfs/discovery-config')
|
|
|
23
23
|
process.env.MCP_MODE = 'true';
|
|
24
24
|
process.env.IPFS_SILENT = 'true';
|
|
25
25
|
|
|
26
|
+
// CRITICAL: Redirect console.log to stderr to prevent libraries from polluting stdout
|
|
27
|
+
// JSON-RPC responses must be the ONLY thing written to stdout.
|
|
28
|
+
const originalConsoleLog = console.log;
|
|
29
|
+
console.log = console.error;
|
|
30
|
+
|
|
31
|
+
|
|
26
32
|
// Load environment variables without dotenv promotional messages
|
|
27
33
|
const LibraryManager = require('./library-manager');
|
|
28
34
|
const metapromptDesigner = require('./utils/metaprompt-designer');
|
|
@@ -63,19 +69,147 @@ class SageMCPServer {
|
|
|
63
69
|
name: 'sage-protocol-mcp',
|
|
64
70
|
version: '1.0.0'
|
|
65
71
|
};
|
|
66
|
-
|
|
72
|
+
|
|
67
73
|
this.discoverySettings = resolveDiscoverySettings();
|
|
68
74
|
this.discoveryClient = undefined;
|
|
69
|
-
|
|
75
|
+
|
|
70
76
|
this.capabilities = {
|
|
71
77
|
tools: {},
|
|
72
78
|
prompts: {}
|
|
73
79
|
};
|
|
74
80
|
|
|
75
|
-
|
|
81
|
+
this.tools = [
|
|
82
|
+
{
|
|
83
|
+
name: 'quick_create_prompt',
|
|
84
|
+
description: `Quickly create a new prompt. Handles library creation automatically.
|
|
85
|
+
When to use:
|
|
86
|
+
- Starting a new prompt from scratch
|
|
87
|
+
- Creating a prompt in "My Library" (default)
|
|
88
|
+
|
|
89
|
+
Prerequisites:
|
|
90
|
+
- None! Just need a name and content.
|
|
91
|
+
|
|
92
|
+
Next steps:
|
|
93
|
+
- quick_test_prompt() to verify
|
|
94
|
+
- quick_iterate_prompt() to refine`,
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
properties: {
|
|
98
|
+
name: { type: 'string', description: 'Name of the prompt' },
|
|
99
|
+
content: { type: 'string', description: 'Prompt content (supports ${variable} syntax)' },
|
|
100
|
+
description: { type: 'string', description: 'Optional description' },
|
|
101
|
+
library: { type: 'string', description: 'Optional library name (defaults to "My Library")' }
|
|
102
|
+
},
|
|
103
|
+
required: ['name', 'content']
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'quick_iterate_prompt',
|
|
108
|
+
description: `Update an existing prompt. Creates a backup of the previous version.
|
|
109
|
+
When to use:
|
|
110
|
+
- Refining prompt content
|
|
111
|
+
- Renaming a prompt
|
|
112
|
+
- Safe editing (auto-backup)
|
|
113
|
+
|
|
114
|
+
Prerequisites:
|
|
115
|
+
- Key of the prompt (from list_prompts)
|
|
116
|
+
|
|
117
|
+
Next steps:
|
|
118
|
+
- quick_test_prompt() to verify changes`,
|
|
119
|
+
inputSchema: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
properties: {
|
|
122
|
+
key: { type: 'string', description: 'Key of the prompt to update' },
|
|
123
|
+
content: { type: 'string', description: 'New content' },
|
|
124
|
+
name: { type: 'string', description: 'New name' }
|
|
125
|
+
},
|
|
126
|
+
required: ['key']
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'improve_prompt',
|
|
131
|
+
description: `Analyze an existing prompt and suggest improvement areas and interview questions.
|
|
132
|
+
When to use:
|
|
133
|
+
- You have a working prompt and want to refine it
|
|
134
|
+
- You want structured feedback and question prompts for the user
|
|
135
|
+
|
|
136
|
+
Prerequisites:
|
|
137
|
+
- Key of the prompt`,
|
|
138
|
+
inputSchema: {
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties: {
|
|
141
|
+
key: { type: 'string', description: 'Prompt key' },
|
|
142
|
+
library: { type: 'string', description: 'Optional library name or CID to narrow search' },
|
|
143
|
+
focus: { type: 'string', description: 'Optional focus area (e.g. "clarity", "variables", "edge-cases")' }
|
|
144
|
+
},
|
|
145
|
+
required: ['key']
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'rename_prompt',
|
|
150
|
+
description: `Rename a prompt's key and/or display name.
|
|
151
|
+
When to use:
|
|
152
|
+
- You want a cleaner key or display name
|
|
153
|
+
- You want to align keys across related prompts
|
|
154
|
+
|
|
155
|
+
Notes:
|
|
156
|
+
- Keys are stable identifiers; renaming a key will affect how you reference the prompt in tools.`,
|
|
157
|
+
inputSchema: {
|
|
158
|
+
type: 'object',
|
|
159
|
+
properties: {
|
|
160
|
+
key: { type: 'string', description: 'Existing prompt key' },
|
|
161
|
+
newKey: { type: 'string', description: 'New key (optional, stable identifier)' },
|
|
162
|
+
name: { type: 'string', description: 'New display name (optional)' }
|
|
163
|
+
},
|
|
164
|
+
required: ['key']
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: 'quick_test_prompt',
|
|
169
|
+
description: `Alias for test_prompt. Test a prompt with variables.
|
|
170
|
+
When to use:
|
|
171
|
+
- Verifying prompt behavior
|
|
172
|
+
- Checking variable substitution
|
|
173
|
+
- Debugging prompt logic
|
|
174
|
+
|
|
175
|
+
Prerequisites:
|
|
176
|
+
- Key of the prompt
|
|
177
|
+
|
|
178
|
+
Next steps:
|
|
179
|
+
- quick_iterate_prompt() if changes needed`,
|
|
180
|
+
inputSchema: {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: {
|
|
183
|
+
key: { type: 'string', description: 'Prompt key' },
|
|
184
|
+
variables: { type: 'object', description: 'Variables for substitution' }
|
|
185
|
+
},
|
|
186
|
+
required: ['key']
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: 'help',
|
|
191
|
+
description: 'Get help on how to use Sage MCP tools and workflows',
|
|
192
|
+
inputSchema: {
|
|
193
|
+
type: 'object',
|
|
194
|
+
properties: {
|
|
195
|
+
topic: { type: 'string', description: 'Specific topic (e.g., "create", "publish", "versioning")' }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
|
|
76
200
|
{
|
|
77
201
|
name: 'search_prompts',
|
|
78
|
-
description:
|
|
202
|
+
description: `Unified prompt search across local pinned libraries and on-chain registries.
|
|
203
|
+
When to use:
|
|
204
|
+
- Finding prompts by topic, tag, or content
|
|
205
|
+
- Searching across BOTH local and on-chain sources
|
|
206
|
+
|
|
207
|
+
Prerequisites:
|
|
208
|
+
- None
|
|
209
|
+
|
|
210
|
+
Next steps:
|
|
211
|
+
- get_prompt(key) to view details
|
|
212
|
+
- quick_test_prompt(key) to try it`,
|
|
79
213
|
inputSchema: {
|
|
80
214
|
type: 'object',
|
|
81
215
|
properties: {
|
|
@@ -102,6 +236,77 @@ class SageMCPServer {
|
|
|
102
236
|
required: []
|
|
103
237
|
}
|
|
104
238
|
},
|
|
239
|
+
{
|
|
240
|
+
name: 'list_prompts',
|
|
241
|
+
description: `List all available prompts from local libraries.
|
|
242
|
+
When to use:
|
|
243
|
+
- Browsing your local collection
|
|
244
|
+
- Seeing what's available in a specific library
|
|
245
|
+
|
|
246
|
+
Prerequisites:
|
|
247
|
+
- None
|
|
248
|
+
|
|
249
|
+
Next steps:
|
|
250
|
+
- get_prompt(key) to view details`,
|
|
251
|
+
inputSchema: {
|
|
252
|
+
type: 'object',
|
|
253
|
+
properties: {
|
|
254
|
+
source: { type: 'string', enum: ['local', 'all'], description: 'Data source (default: local)' },
|
|
255
|
+
library: { type: 'string', description: 'Optional: filter by library name or CID' },
|
|
256
|
+
limit: { type: 'number', description: 'Max prompts to return (default: 20)' }
|
|
257
|
+
},
|
|
258
|
+
required: []
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: 'get_prompt',
|
|
263
|
+
description: `Get a specific prompt by key, including its full content.
|
|
264
|
+
When to use:
|
|
265
|
+
- Viewing/editing a known prompt
|
|
266
|
+
- Testing prompt with variables
|
|
267
|
+
- Retrieving prompt for copying/forking
|
|
268
|
+
|
|
269
|
+
Prerequisites:
|
|
270
|
+
- Know the prompt key (from list_prompts or search_prompts)
|
|
271
|
+
|
|
272
|
+
Next steps:
|
|
273
|
+
- quick_test_prompt() to fill variables
|
|
274
|
+
- quick_iterate_prompt() to edit`,
|
|
275
|
+
inputSchema: {
|
|
276
|
+
type: 'object',
|
|
277
|
+
properties: {
|
|
278
|
+
key: { type: 'string', description: 'Prompt key (e.g. "daily-reflection")' },
|
|
279
|
+
library: { type: 'string', description: 'Optional: library name or CID to narrow search' }
|
|
280
|
+
},
|
|
281
|
+
required: ['key']
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: 'test_prompt',
|
|
286
|
+
description: `Test a prompt by filling in \${variable} placeholders and seeing the rendered result.
|
|
287
|
+
When to use:
|
|
288
|
+
- Verifying prompt behavior
|
|
289
|
+
- Checking variable substitution
|
|
290
|
+
- Debugging prompt logic
|
|
291
|
+
|
|
292
|
+
Prerequisites:
|
|
293
|
+
- Key of the prompt
|
|
294
|
+
|
|
295
|
+
Next steps:
|
|
296
|
+
- quick_iterate_prompt() if changes needed`,
|
|
297
|
+
inputSchema: {
|
|
298
|
+
type: 'object',
|
|
299
|
+
properties: {
|
|
300
|
+
key: { type: 'string', description: 'Prompt key' },
|
|
301
|
+
library: { type: 'string', description: 'Optional: library name or CID' },
|
|
302
|
+
variables: {
|
|
303
|
+
type: 'object',
|
|
304
|
+
description: 'Key-value pairs for ${variable} placeholders (e.g. {"language": "Python"})'
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
required: ['key']
|
|
308
|
+
}
|
|
309
|
+
},
|
|
105
310
|
{
|
|
106
311
|
name: 'search_onchain_prompts',
|
|
107
312
|
description: 'Search for prompts directly on-chain from LibraryRegistry and SubDAO registries',
|
|
@@ -163,7 +368,8 @@ class SageMCPServer {
|
|
|
163
368
|
properties: {
|
|
164
369
|
goal: { type: 'string', description: 'High-level objective for the resulting system prompt' },
|
|
165
370
|
model: { type: 'string', description: 'Target model identifier (default: gpt-5)' },
|
|
166
|
-
interviewStyle: { type: 'string', description: 'Cadence preference (default: one-question-at-a-time)' }
|
|
371
|
+
interviewStyle: { type: 'string', description: 'Cadence preference (default: one-question-at-a-time)' },
|
|
372
|
+
additionalInstructions: { type: 'string', description: 'Any extra instructions for the interviewer (e.g. "Be concise", "Focus on security")' }
|
|
167
373
|
},
|
|
168
374
|
required: []
|
|
169
375
|
}
|
|
@@ -307,22 +513,23 @@ class SageMCPServer {
|
|
|
307
513
|
},
|
|
308
514
|
{
|
|
309
515
|
name: 'propose_manifest',
|
|
310
|
-
description:
|
|
516
|
+
description: `Generate the proposal payload and CLI commands for a SubDAO update.
|
|
517
|
+
Note: This tool does NOT sign transactions. It generates the hex data and CLI commands for you to execute in your terminal.`,
|
|
311
518
|
inputSchema: {
|
|
312
519
|
type: 'object',
|
|
313
520
|
properties: {
|
|
314
|
-
cid: { type: 'string', description: '
|
|
315
|
-
subdao: { type: 'string', description: '
|
|
316
|
-
description: { type: 'string', description: 'Proposal description
|
|
317
|
-
promptCount: { type: 'number', description: '
|
|
318
|
-
manifest: { type: 'object', description: '
|
|
521
|
+
cid: { type: 'string', description: 'IPFS CID of the manifest' },
|
|
522
|
+
subdao: { type: 'string', description: 'SubDAO address' },
|
|
523
|
+
description: { type: 'string', description: 'Proposal description' },
|
|
524
|
+
promptCount: { type: 'number', description: 'Number of prompts (optional override)' },
|
|
525
|
+
manifest: { type: 'object', description: 'Optional manifest object for context' }
|
|
319
526
|
},
|
|
320
|
-
required: ['cid']
|
|
527
|
+
required: ['cid', 'subdao']
|
|
321
528
|
}
|
|
322
529
|
},
|
|
323
530
|
{
|
|
324
531
|
name: 'pin_library',
|
|
325
|
-
description: '
|
|
532
|
+
description: 'Pin a library manifest to local IPFS node (simulated or real)',
|
|
326
533
|
inputSchema: {
|
|
327
534
|
type: 'object',
|
|
328
535
|
properties: {
|
|
@@ -333,35 +540,35 @@ class SageMCPServer {
|
|
|
333
540
|
},
|
|
334
541
|
{
|
|
335
542
|
name: 'pin_by_cid',
|
|
336
|
-
description: 'Fetch a manifest by CID
|
|
543
|
+
description: 'Fetch a manifest by CID and pin it locally',
|
|
337
544
|
inputSchema: {
|
|
338
545
|
type: 'object',
|
|
339
546
|
properties: {
|
|
340
|
-
cid: { type: 'string', description: '
|
|
547
|
+
cid: { type: 'string', description: 'CID to fetch and pin' }
|
|
341
548
|
},
|
|
342
549
|
required: ['cid']
|
|
343
550
|
}
|
|
344
551
|
},
|
|
345
552
|
{
|
|
346
553
|
name: 'diff_manifests',
|
|
347
|
-
description: '
|
|
554
|
+
description: 'Compare two manifests (previous CID vs new manifest object)',
|
|
348
555
|
inputSchema: {
|
|
349
556
|
type: 'object',
|
|
350
557
|
properties: {
|
|
351
|
-
previousCid: { type: 'string', description: '
|
|
352
|
-
nextManifest: { type: 'object', description: '
|
|
558
|
+
previousCid: { type: 'string', description: 'Base CID' },
|
|
559
|
+
nextManifest: { type: 'object', description: 'New manifest object' }
|
|
353
560
|
},
|
|
354
561
|
required: ['previousCid', 'nextManifest']
|
|
355
562
|
}
|
|
356
563
|
},
|
|
357
564
|
{
|
|
358
565
|
name: 'list_proposals',
|
|
359
|
-
description: 'List proposals
|
|
566
|
+
description: 'List active proposals for a SubDAO',
|
|
360
567
|
inputSchema: {
|
|
361
568
|
type: 'object',
|
|
362
569
|
properties: {
|
|
363
|
-
subdao: { type: 'string', description: '
|
|
364
|
-
state: { type: 'string', description: '
|
|
570
|
+
subdao: { type: 'string', description: 'SubDAO address' },
|
|
571
|
+
state: { type: 'string', description: 'Filter by state (Active, Executed, etc)' },
|
|
365
572
|
limit: { type: 'number', description: 'Max items (default 10)' }
|
|
366
573
|
},
|
|
367
574
|
required: []
|
|
@@ -369,17 +576,209 @@ class SageMCPServer {
|
|
|
369
576
|
},
|
|
370
577
|
{
|
|
371
578
|
name: 'publish_manifest_flow',
|
|
372
|
-
description:
|
|
579
|
+
description: `Prepare a manifest for on-chain publishing.
|
|
580
|
+
1. Validates the manifest
|
|
581
|
+
2. Pins it to IPFS
|
|
582
|
+
3. Generates CLI commands for you to sign & broadcast
|
|
583
|
+
|
|
584
|
+
Note: This tool does NOT sign transactions. It prepares everything so you can execute the final step in your terminal.`,
|
|
373
585
|
inputSchema: {
|
|
374
586
|
type: 'object',
|
|
375
587
|
properties: {
|
|
376
588
|
manifest: { type: 'object', description: 'Manifest JSON' },
|
|
377
589
|
subdao: { type: 'string', description: 'Optional SubDAO address for hints' },
|
|
378
|
-
description: { type: 'string', description: 'Optional description override' }
|
|
590
|
+
description: { type: 'string', description: 'Optional description override' },
|
|
591
|
+
dry_run: { type: 'boolean', description: 'Validate and build proposal payload without uploading to IPFS (no network calls)' }
|
|
379
592
|
},
|
|
380
593
|
required: ['manifest']
|
|
381
594
|
}
|
|
382
595
|
},
|
|
596
|
+
{
|
|
597
|
+
name: 'list_templates',
|
|
598
|
+
description: 'List available prompt templates for quick creation.',
|
|
599
|
+
inputSchema: {
|
|
600
|
+
type: 'object',
|
|
601
|
+
properties: {
|
|
602
|
+
category: { type: 'string', description: 'Optional category filter (design, development, analysis, etc.)' },
|
|
603
|
+
search: { type: 'string', description: 'Optional text search over template key/name/description' }
|
|
604
|
+
},
|
|
605
|
+
required: []
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
name: 'get_template',
|
|
610
|
+
description: 'Get detailed information about a specific prompt template.',
|
|
611
|
+
inputSchema: {
|
|
612
|
+
type: 'object',
|
|
613
|
+
properties: {
|
|
614
|
+
key: { type: 'string', description: 'Template key' }
|
|
615
|
+
},
|
|
616
|
+
required: ['key']
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
name: 'create_from_template',
|
|
621
|
+
description: 'Create a new prompt from a template and save it into a local library via quick_create_prompt.',
|
|
622
|
+
inputSchema: {
|
|
623
|
+
type: 'object',
|
|
624
|
+
properties: {
|
|
625
|
+
template: { type: 'string', description: 'Template key' },
|
|
626
|
+
customize: {
|
|
627
|
+
type: 'object',
|
|
628
|
+
description: 'Values to substitute into the template variables'
|
|
629
|
+
},
|
|
630
|
+
library: {
|
|
631
|
+
type: 'string',
|
|
632
|
+
description: 'Target library name or CID (optional, default: My Library)'
|
|
633
|
+
},
|
|
634
|
+
name: {
|
|
635
|
+
type: 'string',
|
|
636
|
+
description: 'Override prompt display name (optional)'
|
|
637
|
+
}
|
|
638
|
+
},
|
|
639
|
+
required: ['template', 'customize']
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
name: 'analyze_dependencies',
|
|
644
|
+
description: 'Analyze variable usage across all prompts in a pinned library.',
|
|
645
|
+
inputSchema: {
|
|
646
|
+
type: 'object',
|
|
647
|
+
properties: {
|
|
648
|
+
library: {
|
|
649
|
+
type: 'string',
|
|
650
|
+
description: 'Pinned library name or CID'
|
|
651
|
+
},
|
|
652
|
+
analysis_type: {
|
|
653
|
+
type: 'string',
|
|
654
|
+
enum: ['variables', 'all'],
|
|
655
|
+
description: 'Type of analysis (v1 supports variables)',
|
|
656
|
+
default: 'variables'
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
required: ['library']
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
name: 'update_library_metadata',
|
|
664
|
+
description: 'Update library-level metadata (name/description/tags) and optionally propagate tags to prompts.',
|
|
665
|
+
inputSchema: {
|
|
666
|
+
type: 'object',
|
|
667
|
+
properties: {
|
|
668
|
+
library: {
|
|
669
|
+
type: 'string',
|
|
670
|
+
description: 'Pinned library name or CID (case-insensitive name match)'
|
|
671
|
+
},
|
|
672
|
+
name: { type: 'string', description: 'New library name (optional)' },
|
|
673
|
+
description: { type: 'string', description: 'New library description (optional)' },
|
|
674
|
+
tags: {
|
|
675
|
+
type: 'array',
|
|
676
|
+
items: { type: 'string' },
|
|
677
|
+
description: 'Library-level tags (replaces existing)'
|
|
678
|
+
},
|
|
679
|
+
apply_to_prompts: {
|
|
680
|
+
type: 'boolean',
|
|
681
|
+
description: 'If true and tags provided, propagate tags to prompts using merge_mode',
|
|
682
|
+
default: false
|
|
683
|
+
},
|
|
684
|
+
merge_mode: {
|
|
685
|
+
type: 'string',
|
|
686
|
+
enum: ['replace', 'merge'],
|
|
687
|
+
description: 'Prompt tag strategy when apply_to_prompts=true',
|
|
688
|
+
default: 'merge'
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
required: ['library']
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
name: 'bulk_update_prompts',
|
|
696
|
+
description: 'Apply multiple small updates (name/description/tags/content) to prompts in a single library.',
|
|
697
|
+
inputSchema: {
|
|
698
|
+
type: 'object',
|
|
699
|
+
properties: {
|
|
700
|
+
library: {
|
|
701
|
+
type: 'string',
|
|
702
|
+
description: 'Pinned library name or CID'
|
|
703
|
+
},
|
|
704
|
+
updates: {
|
|
705
|
+
type: 'array',
|
|
706
|
+
items: {
|
|
707
|
+
type: 'object',
|
|
708
|
+
properties: {
|
|
709
|
+
key: {
|
|
710
|
+
type: 'string',
|
|
711
|
+
description: 'Prompt key within the manifest'
|
|
712
|
+
},
|
|
713
|
+
name: { type: 'string', description: 'New prompt display name (optional)' },
|
|
714
|
+
description: { type: 'string', description: 'New prompt description (optional)' },
|
|
715
|
+
tags: {
|
|
716
|
+
type: 'array',
|
|
717
|
+
items: { type: 'string' },
|
|
718
|
+
description: 'If provided, replaces the prompt tags'
|
|
719
|
+
},
|
|
720
|
+
content: {
|
|
721
|
+
type: 'string',
|
|
722
|
+
description: 'New prompt content body (optional)'
|
|
723
|
+
}
|
|
724
|
+
},
|
|
725
|
+
required: ['key']
|
|
726
|
+
},
|
|
727
|
+
minItems: 1
|
|
728
|
+
},
|
|
729
|
+
dry_run: {
|
|
730
|
+
type: 'boolean',
|
|
731
|
+
description: 'If true, return a preview of changes without writing to disk',
|
|
732
|
+
default: false
|
|
733
|
+
}
|
|
734
|
+
},
|
|
735
|
+
required: ['library', 'updates']
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
name: 'suggest_subdaos_for_library',
|
|
740
|
+
description: 'Suggest SubDAOs that might be a good fit for publishing a given local library, and provide CLI workflows for creating your own SubDAO and pushing the library.',
|
|
741
|
+
inputSchema: {
|
|
742
|
+
type: 'object',
|
|
743
|
+
properties: {
|
|
744
|
+
library: {
|
|
745
|
+
type: 'string',
|
|
746
|
+
description: 'Local pinned library name or CID'
|
|
747
|
+
},
|
|
748
|
+
limit: {
|
|
749
|
+
type: 'number',
|
|
750
|
+
description: 'Max SubDAOs to return (default 5)',
|
|
751
|
+
default: 5
|
|
752
|
+
},
|
|
753
|
+
mode_filter: {
|
|
754
|
+
type: 'string',
|
|
755
|
+
enum: ['any', 'creator', 'squad', 'community'],
|
|
756
|
+
description: 'Optional governance mode preference',
|
|
757
|
+
default: 'any'
|
|
758
|
+
}
|
|
759
|
+
},
|
|
760
|
+
required: ['library']
|
|
761
|
+
}
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
name: 'generate_publishing_commands',
|
|
765
|
+
description: 'Generate ready-to-run CLI commands for validating, uploading, and proposing a library manifest on-chain.',
|
|
766
|
+
inputSchema: {
|
|
767
|
+
type: 'object',
|
|
768
|
+
properties: {
|
|
769
|
+
library: {
|
|
770
|
+
type: 'string',
|
|
771
|
+
description: 'Local pinned library name or CID'
|
|
772
|
+
},
|
|
773
|
+
target: {
|
|
774
|
+
type: 'string',
|
|
775
|
+
description: 'Target SubDAO address or "auto" to pick a likely candidate',
|
|
776
|
+
default: 'auto'
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
required: ['library']
|
|
780
|
+
}
|
|
781
|
+
},
|
|
383
782
|
{
|
|
384
783
|
name: 'list_workspace_skills',
|
|
385
784
|
description: 'List local workspace skills from prompts/ (e.g. prompts/skills/*.md)',
|
|
@@ -407,10 +806,10 @@ class SageMCPServer {
|
|
|
407
806
|
|
|
408
807
|
// Structured logger to stderr (stdout must remain JSON-RPC only)
|
|
409
808
|
this.log = pino ? pino({ level: process.env.MCP_DEBUG === 'true' ? 'debug' : 'info' }, pino.destination(2)) : {
|
|
410
|
-
debug: (...a) => { if (process.env.MCP_DEBUG === 'true') try { console.error(...a); } catch(_){} },
|
|
411
|
-
info: (...a) => { try { console.error(...a); } catch(_){} },
|
|
412
|
-
warn: (...a) => { try { console.error(...a); } catch(_){} },
|
|
413
|
-
error: (...a) => { try { console.error(...a); } catch(_){} },
|
|
809
|
+
debug: (...a) => { if (process.env.MCP_DEBUG === 'true') try { console.error(...a); } catch (_) { } },
|
|
810
|
+
info: (...a) => { try { console.error(...a); } catch (_) { } },
|
|
811
|
+
warn: (...a) => { try { console.error(...a); } catch (_) { } },
|
|
812
|
+
error: (...a) => { try { console.error(...a); } catch (_) { } },
|
|
414
813
|
};
|
|
415
814
|
|
|
416
815
|
// Shared provider + ABIs
|
|
@@ -500,6 +899,30 @@ class SageMCPServer {
|
|
|
500
899
|
formatUnifiedResults: this.formatUnifiedResults,
|
|
501
900
|
});
|
|
502
901
|
|
|
902
|
+
const { createQuickStart } = require('./services/mcp/quick-start');
|
|
903
|
+
this.quickStartHandler = createQuickStart({
|
|
904
|
+
libraryManager: this.libraryManager,
|
|
905
|
+
logger: this.log,
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
const { createBulkOperations } = require('./services/mcp/bulk-operations');
|
|
909
|
+
this.bulkOperations = createBulkOperations({
|
|
910
|
+
libraryManager: this.libraryManager,
|
|
911
|
+
logger: this.log,
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
const { createTemplateManager } = require('./services/mcp/template-manager');
|
|
915
|
+
this.templateManager = createTemplateManager({
|
|
916
|
+
quickStart: this.quickStartHandler,
|
|
917
|
+
logger: this.log,
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
const { createDependencyAnalyzer } = require('./services/mcp/dependency-analyzer');
|
|
921
|
+
this.dependencyAnalyzer = createDependencyAnalyzer({
|
|
922
|
+
libraryManager: this.libraryManager,
|
|
923
|
+
logger: this.log,
|
|
924
|
+
});
|
|
925
|
+
|
|
503
926
|
this.libraryBindingsManager = createLibraryBindingsManager({
|
|
504
927
|
provider: this.provider,
|
|
505
928
|
subdaoAbi: this.subdaoAbi,
|
|
@@ -573,6 +996,15 @@ class SageMCPServer {
|
|
|
573
996
|
'tool:search_prompts': (params) => this.searchPromptsUnified(params),
|
|
574
997
|
'tool:search_onchain_prompts': (params) => this.searchOnchainPrompts(params),
|
|
575
998
|
'tool:trending_prompts': (params) => this.trendingPrompts(params),
|
|
999
|
+
'tool:list_prompts': (params) => this.listPrompts(params),
|
|
1000
|
+
'tool:get_prompt': (params) => this.getPrompt(params),
|
|
1001
|
+
'tool:test_prompt': (params) => this.testPrompt(params),
|
|
1002
|
+
'tool:quick_create_prompt': (params) => this.quickCreatePrompt(params),
|
|
1003
|
+
'tool:quick_iterate_prompt': (params) => this.quickIteratePrompt(params),
|
|
1004
|
+
'tool:improve_prompt': (params) => this.improvePrompt(params),
|
|
1005
|
+
'tool:rename_prompt': (params) => this.renamePrompt(params),
|
|
1006
|
+
'tool:quick_test_prompt': (params) => this.testPrompt(params),
|
|
1007
|
+
'tool:help': (params) => this.getHelp(params),
|
|
576
1008
|
'tool:list_libraries': (params) => this.listLibraries(params),
|
|
577
1009
|
'tool:get_prompt_content': (params) => this.getPromptContent(params),
|
|
578
1010
|
'tool:list_subdaos': () => this.listSubDAOs(),
|
|
@@ -595,6 +1027,14 @@ class SageMCPServer {
|
|
|
595
1027
|
'tool:refresh_library_bindings': (params) => this.refreshLibraryBindings(params),
|
|
596
1028
|
'tool:list_workspace_skills': (params) => this.listWorkspaceSkills(params),
|
|
597
1029
|
'tool:get_workspace_skill': (params) => this.getWorkspaceSkill(params),
|
|
1030
|
+
'tool:update_library_metadata': (params) => this.updateLibraryMetadata(params),
|
|
1031
|
+
'tool:bulk_update_prompts': (params) => this.bulkUpdatePrompts(params),
|
|
1032
|
+
'tool:list_templates': (params) => this.listTemplates(params),
|
|
1033
|
+
'tool:get_template': (params) => this.getTemplate(params),
|
|
1034
|
+
'tool:create_from_template': (params) => this.createPromptFromTemplate(params),
|
|
1035
|
+
'tool:analyze_dependencies': (params) => this.analyzeDependencies(params),
|
|
1036
|
+
'tool:suggest_subdaos_for_library': (params) => this.suggestSubdaosForLibrary(params),
|
|
1037
|
+
'tool:generate_publishing_commands': (params) => this.generatePublishingCommands(params),
|
|
598
1038
|
});
|
|
599
1039
|
|
|
600
1040
|
const toolHandlers = {
|
|
@@ -628,6 +1068,50 @@ class SageMCPServer {
|
|
|
628
1068
|
return this.libraryBindingsManager.buildBindingsForSubdao(subdaoAddr);
|
|
629
1069
|
}
|
|
630
1070
|
|
|
1071
|
+
async _enrichSubdaos(subdaos) {
|
|
1072
|
+
return Promise.all(subdaos.map(async (s) => {
|
|
1073
|
+
const derivedTags = new Set();
|
|
1074
|
+
let libraryCount = 0;
|
|
1075
|
+
|
|
1076
|
+
try {
|
|
1077
|
+
const libraries = await this.libraryBindingsManager.buildBindingsForSubdao(s.address);
|
|
1078
|
+
libraryCount = libraries.length;
|
|
1079
|
+
|
|
1080
|
+
// Check up to 3 libraries per SubDAO to gather context
|
|
1081
|
+
for (const lib of libraries.slice(0, 3)) {
|
|
1082
|
+
try {
|
|
1083
|
+
const registry = new ethers.Contract(lib.registry, this.libraryRegistryAbi, this.provider);
|
|
1084
|
+
const count = await registry.getManifestCIDCount();
|
|
1085
|
+
if (count > 0) {
|
|
1086
|
+
const cid = await registry.getManifestCID(count - 1n);
|
|
1087
|
+
if (cid) {
|
|
1088
|
+
const { manifest } = await this.manifestFetcher.fetchManifest(cid);
|
|
1089
|
+
if (manifest?.library?.tags) {
|
|
1090
|
+
manifest.library.tags.forEach(t => derivedTags.add(String(t).toLowerCase().trim()));
|
|
1091
|
+
}
|
|
1092
|
+
if (manifest?.prompts) {
|
|
1093
|
+
manifest.prompts.forEach(p => {
|
|
1094
|
+
if (p.tags) p.tags.forEach(t => derivedTags.add(String(t).toLowerCase().trim()));
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
} catch (e) {
|
|
1100
|
+
// ignore individual library fetch errors
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
} catch (e) {
|
|
1104
|
+
// ignore binding fetch errors
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
return {
|
|
1108
|
+
...s,
|
|
1109
|
+
derivedTags: Array.from(derivedTags),
|
|
1110
|
+
libraryCount
|
|
1111
|
+
};
|
|
1112
|
+
}));
|
|
1113
|
+
}
|
|
1114
|
+
|
|
631
1115
|
|
|
632
1116
|
// ───────────────────────────── MCP Prompts helpers ─────────────────────────────
|
|
633
1117
|
getDiscoveryClient() {
|
|
@@ -646,7 +1130,7 @@ class SageMCPServer {
|
|
|
646
1130
|
if (process.env.SAGE_DEBUG_DISCOVERY === '1') {
|
|
647
1131
|
try {
|
|
648
1132
|
console.warn('mcp_discovery_client_failed', error.message);
|
|
649
|
-
} catch (_) {}
|
|
1133
|
+
} catch (_) { }
|
|
650
1134
|
}
|
|
651
1135
|
}
|
|
652
1136
|
return this.discoveryClient;
|
|
@@ -683,7 +1167,7 @@ class SageMCPServer {
|
|
|
683
1167
|
if (process.env.SAGE_DEBUG_DISCOVERY === '1') {
|
|
684
1168
|
try {
|
|
685
1169
|
console.warn('mcp_discovery_event_failed', error.message);
|
|
686
|
-
} catch (_) {}
|
|
1170
|
+
} catch (_) { }
|
|
687
1171
|
}
|
|
688
1172
|
}
|
|
689
1173
|
}
|
|
@@ -740,22 +1224,21 @@ class SageMCPServer {
|
|
|
740
1224
|
type: 'text',
|
|
741
1225
|
text: 'No skills found. Create prompts/skills/<name>.md or prompts/skills/<name>/SKILL.md to define skills for this repo.',
|
|
742
1226
|
},
|
|
743
|
-
{ type: '
|
|
1227
|
+
{ type: 'text', text: '```json\n' + JSON.stringify({ skills: [] }, null, 2) + '\n```' },
|
|
744
1228
|
],
|
|
745
1229
|
};
|
|
746
1230
|
}
|
|
747
1231
|
const textLines = results
|
|
748
1232
|
.map(
|
|
749
1233
|
(s, idx) =>
|
|
750
|
-
`${idx + 1}. **${s.name}** (${s.key})\n 📁 ${path.relative(process.cwd(), s.path)}${
|
|
751
|
-
s.summary ? `\n 📝 ${s.summary}` : ''
|
|
1234
|
+
`${idx + 1}. **${s.name}** (${s.key})\n 📁 ${path.relative(process.cwd(), s.path)}${s.summary ? `\n 📝 ${s.summary}` : ''
|
|
752
1235
|
}${s.tags && s.tags.length ? `\n 🔖 ${s.tags.join(', ')}` : ''}`,
|
|
753
1236
|
)
|
|
754
1237
|
.join('\n\n');
|
|
755
1238
|
return {
|
|
756
1239
|
content: [
|
|
757
1240
|
{ type: 'text', text: `Workspace skills (${results.length})\n\n${textLines}` },
|
|
758
|
-
{ type: '
|
|
1241
|
+
{ type: 'text', text: '```json\n' + JSON.stringify({ skills: results }, null, 2) + '\n```' },
|
|
759
1242
|
],
|
|
760
1243
|
};
|
|
761
1244
|
} catch (error) {
|
|
@@ -799,7 +1282,7 @@ class SageMCPServer {
|
|
|
799
1282
|
type: 'text',
|
|
800
1283
|
text: `Loaded workspace skill '${safeKey}' from ${path.relative(process.cwd(), resolved.path)}.\n\n${body}`,
|
|
801
1284
|
},
|
|
802
|
-
{ type: '
|
|
1285
|
+
{ type: 'text', text: '```json\n' + JSON.stringify({ key: safeKey, path: resolved.path, baseDir: resolved.baseDir, body }, null, 2) + '\n```' },
|
|
803
1286
|
],
|
|
804
1287
|
};
|
|
805
1288
|
} catch (error) {
|
|
@@ -880,7 +1363,7 @@ class SageMCPServer {
|
|
|
880
1363
|
libraryRegistryAddress: addressResolution.resolveRegistryAddress().registry,
|
|
881
1364
|
searchSubDAOPrompts: this.searchSubDAOPrompts.bind(this),
|
|
882
1365
|
fetchManifestPrompts: this.fetchManifestPrompts.bind(this),
|
|
883
|
-
|
|
1366
|
+
});
|
|
884
1367
|
return buildOnchainSearchResponse(prompts);
|
|
885
1368
|
} catch (error) {
|
|
886
1369
|
return {
|
|
@@ -898,6 +1381,1056 @@ class SageMCPServer {
|
|
|
898
1381
|
return this.searchPromptsUnifiedHandler(options);
|
|
899
1382
|
}
|
|
900
1383
|
|
|
1384
|
+
async listPrompts({ source = 'local', library = '', limit = 20 } = {}) {
|
|
1385
|
+
try {
|
|
1386
|
+
const results = await this.searchPromptsUnifiedHandler({
|
|
1387
|
+
query: '',
|
|
1388
|
+
source,
|
|
1389
|
+
includeContent: false,
|
|
1390
|
+
page: 1,
|
|
1391
|
+
pageSize: limit
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
if (library && results?.content?.[1]?.text) {
|
|
1395
|
+
const jsonMatch = results.content[1].text.match(/```json\n([\s\S]+)\n```/);
|
|
1396
|
+
if (jsonMatch) {
|
|
1397
|
+
const data = JSON.parse(jsonMatch[1]);
|
|
1398
|
+
const libraryLower = library.toLowerCase();
|
|
1399
|
+
data.results = data.results.filter(r =>
|
|
1400
|
+
r.library?.name?.toLowerCase().includes(libraryLower) ||
|
|
1401
|
+
r.library?.cid?.toLowerCase().includes(libraryLower)
|
|
1402
|
+
);
|
|
1403
|
+
data.total = data.results.length;
|
|
1404
|
+
|
|
1405
|
+
const formatted = this.formatUnifiedResults(data.results, { total: data.total, page: 1, pageSize: limit });
|
|
1406
|
+
return {
|
|
1407
|
+
content: [
|
|
1408
|
+
{ type: 'text', text: formatted },
|
|
1409
|
+
{ type: 'text', text: '```json\n' + JSON.stringify(data, null, 2) + '\n```' }
|
|
1410
|
+
]
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
return results;
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
return { content: [{ type: 'text', text: `Error listing prompts: ${error.message}` }] };
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
async getPrompt({ key, library = '' } = {}) {
|
|
1422
|
+
try {
|
|
1423
|
+
if (!key) {
|
|
1424
|
+
return { content: [{ type: 'text', text: 'Error: key parameter is required' }] };
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
const results = await this.searchPromptsUnifiedHandler({
|
|
1428
|
+
query: key,
|
|
1429
|
+
source: 'local',
|
|
1430
|
+
includeContent: true,
|
|
1431
|
+
pageSize: 50
|
|
1432
|
+
});
|
|
1433
|
+
|
|
1434
|
+
if (results?.content?.[1]?.text) {
|
|
1435
|
+
const jsonMatch = results.content[1].text.match(/```json\n([\s\S]+)\n```/);
|
|
1436
|
+
if (jsonMatch) {
|
|
1437
|
+
const data = JSON.parse(jsonMatch[1]);
|
|
1438
|
+
let prompt = data.results.find(r => r.key === key);
|
|
1439
|
+
|
|
1440
|
+
if (library && !prompt) {
|
|
1441
|
+
const libraryLower = library.toLowerCase();
|
|
1442
|
+
prompt = data.results.find(r =>
|
|
1443
|
+
r.key === key && (r.library?.name?.toLowerCase().includes(libraryLower) || r.library?.cid?.toLowerCase().includes(libraryLower))
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
if (!prompt) {
|
|
1448
|
+
prompt = data.results.find(r => r.key?.includes(key));
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
if (!prompt) {
|
|
1452
|
+
return { content: [{ type: 'text', text: `No prompt found with key "${key}"` }] };
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
const text = `**${prompt.name}**\n\n🔑 Key: ${prompt.key}\n📚 Library: ${prompt.library?.name || 'Unknown'}\n📄 ${prompt.description || 'No description'}\n🏷️ Tags: ${prompt.tags?.join(', ') || 'None'}\n\n**Content:**\n\`\`\`\n${prompt.content || '(No content)'}\n\`\`\``;
|
|
1456
|
+
|
|
1457
|
+
return {
|
|
1458
|
+
content: [
|
|
1459
|
+
{ type: 'text', text },
|
|
1460
|
+
{ type: 'text', text: '```json\n' + JSON.stringify(prompt, null, 2) + '\n```' }
|
|
1461
|
+
]
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
return { content: [{ type: 'text', text: `No prompt found with key "${key}"` }] };
|
|
1467
|
+
} catch (error) {
|
|
1468
|
+
return { content: [{ type: 'text', text: `Error getting prompt: ${error.message}` }] };
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
async testPrompt({ key, library = '', variables = {} } = {}) {
|
|
1473
|
+
try {
|
|
1474
|
+
if (!key) {
|
|
1475
|
+
return { content: [{ type: 'text', text: 'Error: key parameter is required' }] };
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
const promptResult = await this.getPrompt({ key, library });
|
|
1479
|
+
if (!promptResult?.content?.[1]?.text) {
|
|
1480
|
+
return promptResult;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
const jsonMatch = promptResult.content[1].text.match(/```json\n([\s\S]+)\n```/);
|
|
1484
|
+
if (!jsonMatch) {
|
|
1485
|
+
return { content: [{ type: 'text', text: 'Error: Failed to parse prompt data' }] };
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
const prompt = JSON.parse(jsonMatch[1]);
|
|
1489
|
+
let content = prompt.content || '';
|
|
1490
|
+
|
|
1491
|
+
if (!content) {
|
|
1492
|
+
return { content: [{ type: 'text', text: `Prompt "${key}" has no content to test` }] };
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
const variablePattern = /\$\{([^}]+)\}/g;
|
|
1496
|
+
const foundVariables = [];
|
|
1497
|
+
let match;
|
|
1498
|
+
while ((match = variablePattern.exec(content)) !== null) {
|
|
1499
|
+
if (!foundVariables.includes(match[1])) {
|
|
1500
|
+
foundVariables.push(match[1]);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
let renderedContent = content;
|
|
1505
|
+
const substitutions = {};
|
|
1506
|
+
|
|
1507
|
+
for (const varName of foundVariables) {
|
|
1508
|
+
if (variables[varName] !== undefined) {
|
|
1509
|
+
renderedContent = renderedContent.replace(new RegExp(`\\$\\{${varName}\\}`, 'g'), variables[varName]);
|
|
1510
|
+
substitutions[varName] = variables[varName];
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
const missingVars = foundVariables.filter(v => variables[v] === undefined);
|
|
1515
|
+
|
|
1516
|
+
let result = `**Testing: ${prompt.name}**\n\n`;
|
|
1517
|
+
if (foundVariables.length === 0) {
|
|
1518
|
+
result += '✅ No variables\n\n';
|
|
1519
|
+
} else {
|
|
1520
|
+
result += `**Variables:** ${foundVariables.join(', ')}\n`;
|
|
1521
|
+
if (Object.keys(substitutions).length > 0) {
|
|
1522
|
+
result += `**Substituted:** ${Object.keys(substitutions).join(', ')}\n`;
|
|
1523
|
+
}
|
|
1524
|
+
if (missingVars.length > 0) {
|
|
1525
|
+
result += `**⚠️ Missing:** ${missingVars.join(', ')}\n`;
|
|
1526
|
+
}
|
|
1527
|
+
result += '\n';
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
result += `**Rendered:**\n\`\`\`\n${renderedContent}\n\`\`\``;
|
|
1531
|
+
|
|
1532
|
+
return {
|
|
1533
|
+
content: [
|
|
1534
|
+
{ type: 'text', text: result },
|
|
1535
|
+
{ type: 'text', text: '```json\n' + JSON.stringify({ prompt: { key: prompt.key, name: prompt.name }, variables: { found: foundVariables, substituted: Object.keys(substitutions), missing: missingVars }, rendered: renderedContent }, null, 2) + '\n```' }
|
|
1536
|
+
]
|
|
1537
|
+
};
|
|
1538
|
+
} catch (error) {
|
|
1539
|
+
return { content: [{ type: 'text', text: `Error testing prompt: ${error.message}` }] };
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
async updateLibraryMetadata(params) {
|
|
1544
|
+
try {
|
|
1545
|
+
const result = await this.bulkOperations.updateLibraryMetadata(params);
|
|
1546
|
+
const textLines = [
|
|
1547
|
+
'✅ Updated library metadata',
|
|
1548
|
+
`Library: ${result.library}`,
|
|
1549
|
+
`CID: ${result.cid}`,
|
|
1550
|
+
`Fields: ${result.updatedLibraryFields.length ? result.updatedLibraryFields.join(', ') : '(none)'}`,
|
|
1551
|
+
`Prompts touched: ${result.updatedPromptCount}`,
|
|
1552
|
+
];
|
|
1553
|
+
return {
|
|
1554
|
+
content: [
|
|
1555
|
+
{ type: 'text', text: textLines.join('\n') },
|
|
1556
|
+
{ type: 'text', text: '```json\n' + JSON.stringify(result, null, 2) + '\n```' },
|
|
1557
|
+
],
|
|
1558
|
+
};
|
|
1559
|
+
} catch (error) {
|
|
1560
|
+
return { content: [{ type: 'text', text: `Error updating library metadata: ${error.message}` }] };
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
async bulkUpdatePrompts(params) {
|
|
1565
|
+
try {
|
|
1566
|
+
const result = await this.bulkOperations.bulkUpdatePrompts(params);
|
|
1567
|
+
const header = result.dryRun ? '✅ Dry run: no changes written' : '✅ Bulk prompt update complete';
|
|
1568
|
+
const textLines = [
|
|
1569
|
+
header,
|
|
1570
|
+
`Library: ${result.library}`,
|
|
1571
|
+
`CID: ${result.cid}`,
|
|
1572
|
+
`Updated prompts: ${result.updatedCount}`,
|
|
1573
|
+
];
|
|
1574
|
+
if (result.missingKeys?.length) {
|
|
1575
|
+
textLines.push(`Missing keys: ${result.missingKeys.join(', ')}`);
|
|
1576
|
+
}
|
|
1577
|
+
return {
|
|
1578
|
+
content: [
|
|
1579
|
+
{ type: 'text', text: textLines.join('\n') },
|
|
1580
|
+
{ type: 'text', text: '```json\n' + JSON.stringify(result, null, 2) + '\n```' },
|
|
1581
|
+
],
|
|
1582
|
+
};
|
|
1583
|
+
} catch (error) {
|
|
1584
|
+
return { content: [{ type: 'text', text: `Error bulk updating prompts: ${error.message}` }] };
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
async listTemplates(params = {}) {
|
|
1589
|
+
try {
|
|
1590
|
+
const result = this.templateManager.listTemplates(params || {});
|
|
1591
|
+
const items = result.templates || [];
|
|
1592
|
+
if (!items.length) {
|
|
1593
|
+
return { content: [{ type: 'text', text: 'No templates available.' }] };
|
|
1594
|
+
}
|
|
1595
|
+
const lines = [];
|
|
1596
|
+
lines.push(`Available templates (${items.length}):`);
|
|
1597
|
+
lines.push('');
|
|
1598
|
+
for (const tpl of items) {
|
|
1599
|
+
lines.push(`- **${tpl.name}** (${tpl.key}) [${tpl.category || 'uncategorized'}]`);
|
|
1600
|
+
if (tpl.summary) {
|
|
1601
|
+
lines.push(` ${tpl.summary}`);
|
|
1602
|
+
}
|
|
1603
|
+
if (tpl.requiredVariables?.length) {
|
|
1604
|
+
lines.push(` Required: ${tpl.requiredVariables.join(', ')}`);
|
|
1605
|
+
}
|
|
1606
|
+
lines.push('');
|
|
1607
|
+
}
|
|
1608
|
+
return {
|
|
1609
|
+
content: [
|
|
1610
|
+
{ type: 'text', text: lines.join('\n') },
|
|
1611
|
+
{ type: 'text', text: '```json\n' + JSON.stringify(result, null, 2) + '\n```' },
|
|
1612
|
+
],
|
|
1613
|
+
};
|
|
1614
|
+
} catch (error) {
|
|
1615
|
+
return { content: [{ type: 'text', text: `Error listing templates: ${error.message}` }] };
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
async getTemplate(params) {
|
|
1620
|
+
try {
|
|
1621
|
+
const result = this.templateManager.getTemplate(params || {});
|
|
1622
|
+
return {
|
|
1623
|
+
content: [
|
|
1624
|
+
{
|
|
1625
|
+
type: 'text',
|
|
1626
|
+
text:
|
|
1627
|
+
`Template: ${result.template.name} (${result.template.key})\n` +
|
|
1628
|
+
`Category: ${result.template.category || 'uncategorized'}\n\n` +
|
|
1629
|
+
`${result.template.description || ''}`,
|
|
1630
|
+
},
|
|
1631
|
+
{ type: 'text', text: '```json\n' + JSON.stringify(result.template, null, 2) + '\n```' },
|
|
1632
|
+
],
|
|
1633
|
+
};
|
|
1634
|
+
} catch (error) {
|
|
1635
|
+
return { content: [{ type: 'text', text: `Error getting template: ${error.message}` }] };
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
async createPromptFromTemplate(params) {
|
|
1640
|
+
try {
|
|
1641
|
+
const result = await this.templateManager.createFromTemplate(params || {});
|
|
1642
|
+
const quick = result.quickCreate || {};
|
|
1643
|
+
const lines = [];
|
|
1644
|
+
lines.push('✅ Created prompt from template');
|
|
1645
|
+
lines.push(`Template: ${result.template}`);
|
|
1646
|
+
lines.push(`Key: ${quick.key || '(unknown key)'}`);
|
|
1647
|
+
lines.push(`Library: ${quick.library || '(unknown library)'}`);
|
|
1648
|
+
return {
|
|
1649
|
+
content: [
|
|
1650
|
+
{ type: 'text', text: lines.join('\n') },
|
|
1651
|
+
{ type: 'text', text: '```json\n' + JSON.stringify(result, null, 2) + '\n```' },
|
|
1652
|
+
],
|
|
1653
|
+
};
|
|
1654
|
+
} catch (error) {
|
|
1655
|
+
return { content: [{ type: 'text', text: `Error creating prompt from template: ${error.message}` }] };
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
async analyzeDependencies(params) {
|
|
1660
|
+
try {
|
|
1661
|
+
return await this.dependencyAnalyzer.analyzeDependencies(params || {});
|
|
1662
|
+
} catch (error) {
|
|
1663
|
+
return { content: [{ type: 'text', text: `Error analyzing dependencies: ${error.message}` }] };
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
async suggestSubdaosForLibrary({ library, limit = 5, mode_filter = 'any' } = {}) {
|
|
1668
|
+
try {
|
|
1669
|
+
const lm = this.libraryManager;
|
|
1670
|
+
const pinned = lm.listPinned() || [];
|
|
1671
|
+
const id = String(library || '').trim();
|
|
1672
|
+
if (!id) {
|
|
1673
|
+
return { content: [{ type: 'text', text: 'Error: library parameter is required' }] };
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
let selected = null;
|
|
1677
|
+
let row = pinned.find((r) => r.cid === id);
|
|
1678
|
+
if (row) {
|
|
1679
|
+
selected = lm.loadPinned(row.cid);
|
|
1680
|
+
selected.row = row;
|
|
1681
|
+
} else {
|
|
1682
|
+
const lower = id.toLowerCase();
|
|
1683
|
+
const matches = [];
|
|
1684
|
+
for (const r of pinned) {
|
|
1685
|
+
try {
|
|
1686
|
+
const loaded = lm.loadPinned(r.cid);
|
|
1687
|
+
const libName = String(loaded.manifest?.library?.name || r.name || '').toLowerCase();
|
|
1688
|
+
if (libName === lower) {
|
|
1689
|
+
matches.push({ row: r, ...loaded });
|
|
1690
|
+
}
|
|
1691
|
+
} catch (_) {
|
|
1692
|
+
// ignore corrupt
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
if (matches.length === 0) {
|
|
1696
|
+
return {
|
|
1697
|
+
content: [
|
|
1698
|
+
{
|
|
1699
|
+
type: 'text',
|
|
1700
|
+
text: `No pinned library found matching "${library}". Use list_libraries(source="local") to see available libraries.`,
|
|
1701
|
+
},
|
|
1702
|
+
],
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
if (matches.length > 1) {
|
|
1706
|
+
const lines = matches.map(
|
|
1707
|
+
(m) => `- ${m.row.cid} (${m.manifest?.library?.name || m.row.name || 'unnamed'})`,
|
|
1708
|
+
);
|
|
1709
|
+
return {
|
|
1710
|
+
content: [
|
|
1711
|
+
{
|
|
1712
|
+
type: 'text',
|
|
1713
|
+
text:
|
|
1714
|
+
`Ambiguous library match for "${library}". Candidates:\n` +
|
|
1715
|
+
`${lines.join('\n')}\n\nPass an explicit CID to disambiguate.`,
|
|
1716
|
+
},
|
|
1717
|
+
],
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
selected = matches[0];
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
const manifest = selected.manifest;
|
|
1724
|
+
const libName = manifest.library?.name || selected.row?.name || library;
|
|
1725
|
+
const libDescription = manifest.library?.description || '';
|
|
1726
|
+
const tagsSet = new Set(
|
|
1727
|
+
(manifest.library?.tags || []).map((t) => String(t).toLowerCase().trim()).filter(Boolean),
|
|
1728
|
+
);
|
|
1729
|
+
for (const p of manifest.prompts || []) {
|
|
1730
|
+
if (!p || !Array.isArray(p.tags)) continue;
|
|
1731
|
+
for (const t of p.tags) {
|
|
1732
|
+
const v = String(t).toLowerCase().trim();
|
|
1733
|
+
if (v) tagsSet.add(v);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
const libTags = Array.from(tagsSet);
|
|
1737
|
+
|
|
1738
|
+
// Fetch candidates - limit to 20 to allow for deeper analysis
|
|
1739
|
+
const subdaos = await this.getSubDAOList({ limit: 20 });
|
|
1740
|
+
if (!subdaos || !subdaos.length) {
|
|
1741
|
+
const fallbackText =
|
|
1742
|
+
'No SubDAOs discovered from subgraph/factory.\n\n' +
|
|
1743
|
+
'You can still create your own personal SubDAO for this library:\n\n' +
|
|
1744
|
+
`1) Create personal SubDAO:\n sage subdao create --type personal --name "${libName}" --description "${libDescription || libName}" --bootstrap\n\n` +
|
|
1745
|
+
'2) Scaffold and push a manifest:\n' +
|
|
1746
|
+
' sage library scaffold-manifest courtyard-lib/manifest.json \\\n' +
|
|
1747
|
+
` --name "${libName}" \\\n` +
|
|
1748
|
+
` --description "${libDescription || libName}"\n` +
|
|
1749
|
+
' # Add prompts with: sage library add-prompt --manifest courtyard-lib/manifest.json --file prompts/<file>.md --key <key>\n' +
|
|
1750
|
+
' sage library push courtyard-lib/manifest.json --subdao 0xYourSubDAO --pin --wait\n';
|
|
1751
|
+
return { content: [{ type: 'text', text: fallbackText }] };
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// Enrich SubDAOs with tags from their attached libraries
|
|
1755
|
+
const enrichedSubdaos = await this._enrichSubdaos(subdaos);
|
|
1756
|
+
|
|
1757
|
+
const modeFilter = mode_filter || 'any';
|
|
1758
|
+
const scored = [];
|
|
1759
|
+
for (const s of enrichedSubdaos) {
|
|
1760
|
+
const name = String(s.name || '').toLowerCase();
|
|
1761
|
+
const desc = String(s.description || '').toLowerCase();
|
|
1762
|
+
const tokens = new Set(
|
|
1763
|
+
(name + ' ' + desc)
|
|
1764
|
+
.split(/[^a-z0-9]+/i)
|
|
1765
|
+
.map((t) => t.toLowerCase().trim())
|
|
1766
|
+
.filter(Boolean),
|
|
1767
|
+
);
|
|
1768
|
+
|
|
1769
|
+
// Add derived tags to tokens for matching
|
|
1770
|
+
s.derivedTags.forEach(t => tokens.add(t));
|
|
1771
|
+
|
|
1772
|
+
let matchCount = 0;
|
|
1773
|
+
const matchedTags = [];
|
|
1774
|
+
for (const tag of libTags) {
|
|
1775
|
+
if (tokens.has(tag)) {
|
|
1776
|
+
matchCount += 1;
|
|
1777
|
+
matchedTags.push(tag);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
if (modeFilter !== 'any') {
|
|
1782
|
+
// Mode-specific filtering can be added in a future iteration.
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
// Boost score if we matched on derived tags (stronger signal than just name)
|
|
1786
|
+
const fitScore = matchCount + s.libraryCount * 0.1;
|
|
1787
|
+
scored.push({
|
|
1788
|
+
address: s.address,
|
|
1789
|
+
name: s.name || `SubDAO-${String(s.address).slice(-6)}`,
|
|
1790
|
+
description: s.description || '',
|
|
1791
|
+
registryAddress: s.registryAddress || null,
|
|
1792
|
+
defaultLibraryId: s.defaultLibraryId || 'main',
|
|
1793
|
+
libraryCount: s.libraryCount,
|
|
1794
|
+
matchedTags,
|
|
1795
|
+
fitScore,
|
|
1796
|
+
derivedTags: s.derivedTags // Include for debugging/info
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
const anyTagMatches = scored.some((s) => (s.matchedTags || []).length > 0);
|
|
1801
|
+
scored.sort((a, b) => (b.fitScore || 0) - (a.fitScore || 0));
|
|
1802
|
+
const top = scored.slice(0, limit);
|
|
1803
|
+
|
|
1804
|
+
const lines = [];
|
|
1805
|
+
lines.push(`🎯 Suggested SubDAOs for library "${libName}"`);
|
|
1806
|
+
lines.push('');
|
|
1807
|
+
|
|
1808
|
+
if (!top.length || !libTags.length || !anyTagMatches) {
|
|
1809
|
+
lines.push('No strong SubDAO matches found based on tags and names.');
|
|
1810
|
+
if (top.length) {
|
|
1811
|
+
lines.push('');
|
|
1812
|
+
lines.push('Here are some recent SubDAOs you may consider, but scoring is neutral:');
|
|
1813
|
+
lines.push('');
|
|
1814
|
+
top.forEach((s, idx) => {
|
|
1815
|
+
lines.push(`${idx + 1}. ${s.name} (${s.address})`);
|
|
1816
|
+
lines.push(` • Libraries: ${s.libraryCount}`);
|
|
1817
|
+
if (s.description) {
|
|
1818
|
+
lines.push(` • Description: ${s.description}`);
|
|
1819
|
+
}
|
|
1820
|
+
lines.push('');
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
} else {
|
|
1824
|
+
top.forEach((s, idx) => {
|
|
1825
|
+
lines.push(`${idx + 1}. ${s.name} (${s.address})`);
|
|
1826
|
+
if (s.matchedTags.length) {
|
|
1827
|
+
lines.push(` • Tag overlap: ${s.matchedTags.join(', ')}`);
|
|
1828
|
+
}
|
|
1829
|
+
if (s.derivedTags.length > 0) {
|
|
1830
|
+
// Show a few derived tags to explain why it matched
|
|
1831
|
+
const displayTags = s.derivedTags.slice(0, 5).join(', ');
|
|
1832
|
+
lines.push(` • Context: ${displayTags}${s.derivedTags.length > 5 ? '...' : ''}`);
|
|
1833
|
+
}
|
|
1834
|
+
lines.push(` • Libraries: ${s.libraryCount}`);
|
|
1835
|
+
if (s.description) {
|
|
1836
|
+
lines.push(` • Description: ${s.description}`);
|
|
1837
|
+
}
|
|
1838
|
+
lines.push('');
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
lines.push('---');
|
|
1843
|
+
lines.push('💡 If none of these feel right, you can create your own personal SubDAO and publish there:');
|
|
1844
|
+
lines.push('');
|
|
1845
|
+
lines.push('1) Create a personal SubDAO for this library:');
|
|
1846
|
+
lines.push(
|
|
1847
|
+
` sage subdao create --type personal --name "${libName}" --description "${libDescription || libName}" --bootstrap`,
|
|
1848
|
+
);
|
|
1849
|
+
lines.push('');
|
|
1850
|
+
lines.push('2) Scaffold a manifest and push it:');
|
|
1851
|
+
lines.push(' sage library scaffold-manifest courtyard-lib/manifest.json \\');
|
|
1852
|
+
lines.push(` --name "${libName}" \\`);
|
|
1853
|
+
lines.push(` --description "${libDescription || libName}"`);
|
|
1854
|
+
lines.push(
|
|
1855
|
+
' # Add prompts with: sage library add-prompt --manifest courtyard-lib/manifest.json --file prompts/<file>.md --key <key>',
|
|
1856
|
+
);
|
|
1857
|
+
lines.push(
|
|
1858
|
+
' sage library push courtyard-lib/manifest.json --subdao 0xYourSubDAO --pin --wait',
|
|
1859
|
+
);
|
|
1860
|
+
|
|
1861
|
+
const cliWorkflows = {
|
|
1862
|
+
createPersonalSubdao: {
|
|
1863
|
+
description: 'Create a personal SubDAO for this library.',
|
|
1864
|
+
commands: [
|
|
1865
|
+
`sage subdao create --type personal --name "${libName}" --description "${libDescription || libName}" --bootstrap`,
|
|
1866
|
+
],
|
|
1867
|
+
},
|
|
1868
|
+
createLibraryAndPush: {
|
|
1869
|
+
description: 'Scaffold a manifest from your local prompts and push it to a SubDAO.',
|
|
1870
|
+
commands: [
|
|
1871
|
+
'# Scaffold manifest (adjust path/name as needed):',
|
|
1872
|
+
'sage library scaffold-manifest courtyard-lib/manifest.json \\',
|
|
1873
|
+
` --name "${libName}" \\`,
|
|
1874
|
+
` --description "${libDescription || libName}"`,
|
|
1875
|
+
'',
|
|
1876
|
+
'# Add prompts (repeat per prompt):',
|
|
1877
|
+
'sage library add-prompt --manifest courtyard-lib/manifest.json \\',
|
|
1878
|
+
' --file prompts/<file>.md \\',
|
|
1879
|
+
' --key <key> \\',
|
|
1880
|
+
' --name "<Prompt Name>"',
|
|
1881
|
+
'',
|
|
1882
|
+
'# Push to your SubDAO (replace 0xYourSubDAO):',
|
|
1883
|
+
'sage library push courtyard-lib/manifest.json --subdao 0xYourSubDAO --pin --wait',
|
|
1884
|
+
],
|
|
1885
|
+
},
|
|
1886
|
+
};
|
|
1887
|
+
|
|
1888
|
+
const jsonPayload = {
|
|
1889
|
+
library: {
|
|
1890
|
+
cid: selected.row?.cid,
|
|
1891
|
+
name: libName,
|
|
1892
|
+
tags: libTags,
|
|
1893
|
+
},
|
|
1894
|
+
suggestions: top,
|
|
1895
|
+
cliWorkflows,
|
|
1896
|
+
};
|
|
1897
|
+
|
|
1898
|
+
return {
|
|
1899
|
+
content: [
|
|
1900
|
+
{ type: 'text', text: lines.join('\n') },
|
|
1901
|
+
{ type: 'text', text: '```json\n' + JSON.stringify(jsonPayload, null, 2) + '\n```' },
|
|
1902
|
+
],
|
|
1903
|
+
};
|
|
1904
|
+
} catch (error) {
|
|
1905
|
+
return { content: [{ type: 'text', text: `Error suggesting SubDAOs: ${error.message}` }] };
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
async generatePublishingCommands({ library, target = 'auto' } = {}) {
|
|
1910
|
+
try {
|
|
1911
|
+
const lm = this.libraryManager;
|
|
1912
|
+
const pinned = lm.listPinned() || [];
|
|
1913
|
+
const id = String(library || '').trim();
|
|
1914
|
+
if (!id) {
|
|
1915
|
+
return { content: [{ type: 'text', text: 'Error: library parameter is required' }] };
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
let selected = null;
|
|
1919
|
+
let row = pinned.find((r) => r.cid === id);
|
|
1920
|
+
if (row) {
|
|
1921
|
+
selected = lm.loadPinned(row.cid);
|
|
1922
|
+
selected.row = row;
|
|
1923
|
+
} else {
|
|
1924
|
+
const lower = id.toLowerCase();
|
|
1925
|
+
const matches = [];
|
|
1926
|
+
for (const r of pinned) {
|
|
1927
|
+
try {
|
|
1928
|
+
const loaded = lm.loadPinned(r.cid);
|
|
1929
|
+
const libName = String(loaded.manifest?.library?.name || r.name || '').toLowerCase();
|
|
1930
|
+
if (libName === lower) {
|
|
1931
|
+
matches.push({ row: r, ...loaded });
|
|
1932
|
+
}
|
|
1933
|
+
} catch (_) {
|
|
1934
|
+
// ignore corrupt
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
if (matches.length === 0) {
|
|
1938
|
+
return {
|
|
1939
|
+
content: [
|
|
1940
|
+
{
|
|
1941
|
+
type: 'text',
|
|
1942
|
+
text: `No pinned library found matching "${library}". Use list_libraries(source="local") to see available libraries.`,
|
|
1943
|
+
},
|
|
1944
|
+
],
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
if (matches.length > 1) {
|
|
1948
|
+
const lines = matches.map(
|
|
1949
|
+
(m) => `- ${m.row.cid} (${m.manifest?.library?.name || m.row.name || 'unnamed'})`,
|
|
1950
|
+
);
|
|
1951
|
+
return {
|
|
1952
|
+
content: [
|
|
1953
|
+
{
|
|
1954
|
+
type: 'text',
|
|
1955
|
+
text:
|
|
1956
|
+
`Ambiguous library match for "${library}". Candidates:\n` +
|
|
1957
|
+
`${lines.join('\n')}\n\nPass an explicit CID to disambiguate.`,
|
|
1958
|
+
},
|
|
1959
|
+
],
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
selected = matches[0];
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
const manifest = selected.manifest;
|
|
1966
|
+
const manifestPath = selected.path;
|
|
1967
|
+
const libName = manifest.library?.name || selected.row?.name || library;
|
|
1968
|
+
const libDescription = manifest.library?.description || '';
|
|
1969
|
+
|
|
1970
|
+
let chosenSubdao = '';
|
|
1971
|
+
let subdaoReason = '';
|
|
1972
|
+
if (target && target !== 'auto') {
|
|
1973
|
+
chosenSubdao = target;
|
|
1974
|
+
subdaoReason = 'Target provided explicitly.';
|
|
1975
|
+
} else {
|
|
1976
|
+
const tagsSet = new Set(
|
|
1977
|
+
(manifest.library?.tags || []).map((t) => String(t).toLowerCase().trim()).filter(Boolean),
|
|
1978
|
+
);
|
|
1979
|
+
for (const p of manifest.prompts || []) {
|
|
1980
|
+
if (!p || !Array.isArray(p.tags)) continue;
|
|
1981
|
+
for (const t of p.tags) {
|
|
1982
|
+
const v = String(t).toLowerCase().trim();
|
|
1983
|
+
if (v) tagsSet.add(v);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
const libTags = Array.from(tagsSet);
|
|
1987
|
+
|
|
1988
|
+
const subdaos = await this.getSubDAOList({ limit: 20 });
|
|
1989
|
+
if (subdaos && subdaos.length && libTags.length) {
|
|
1990
|
+
const enrichedSubdaos = await this._enrichSubdaos(subdaos);
|
|
1991
|
+
const scored = enrichedSubdaos.map((s) => {
|
|
1992
|
+
const name = String(s.name || '').toLowerCase();
|
|
1993
|
+
const desc = String(s.description || '').toLowerCase();
|
|
1994
|
+
const tokens = new Set(
|
|
1995
|
+
(name + ' ' + desc)
|
|
1996
|
+
.split(/[^a-z0-9]+/i)
|
|
1997
|
+
.map((t) => t.toLowerCase().trim())
|
|
1998
|
+
.filter(Boolean),
|
|
1999
|
+
);
|
|
2000
|
+
s.derivedTags.forEach(t => tokens.add(t));
|
|
2001
|
+
|
|
2002
|
+
let matchCount = 0;
|
|
2003
|
+
for (const tag of libTags) {
|
|
2004
|
+
if (tokens.has(tag)) matchCount += 1;
|
|
2005
|
+
}
|
|
2006
|
+
const fitScore = matchCount + s.libraryCount * 0.1;
|
|
2007
|
+
return { subdao: s, fitScore, matchCount, libraryCount: s.libraryCount };
|
|
2008
|
+
});
|
|
2009
|
+
// Only auto-pick if there is at least one SubDAO with tag matches
|
|
2010
|
+
const withMatches = scored.filter((s) => s.matchCount > 0);
|
|
2011
|
+
if (withMatches.length) {
|
|
2012
|
+
withMatches.sort((a, b) => (b.fitScore || 0) - (a.fitScore || 0));
|
|
2013
|
+
const top = withMatches[0];
|
|
2014
|
+
if (top && top.subdao) {
|
|
2015
|
+
chosenSubdao = top.subdao.address;
|
|
2016
|
+
subdaoReason = `Auto-selected based on tag/name overlap and existing libraries (${top.matchCount} matching tag(s)).`;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
const manifestPathHint = manifestPath || `~/.sage/libraries/${selected.row.cid}.json`;
|
|
2023
|
+
const inspectCmd = `sage library cache view ${selected.row.cid}`;
|
|
2024
|
+
const pushCmd = chosenSubdao
|
|
2025
|
+
? `sage library push ${manifestPathHint} --subdao ${chosenSubdao} --pin --wait`
|
|
2026
|
+
: `sage library push ${manifestPathHint} --subdao 0xYourSubDAO --pin --wait`;
|
|
2027
|
+
const proposalsCmd = chosenSubdao
|
|
2028
|
+
? `sage governance proposals --subdao ${chosenSubdao}`
|
|
2029
|
+
: 'sage governance proposals --subdao 0xYourSubDAO';
|
|
2030
|
+
|
|
2031
|
+
const lines = [];
|
|
2032
|
+
lines.push(`📦 Publishing Guide for "${libName}"`);
|
|
2033
|
+
lines.push('');
|
|
2034
|
+
lines.push(`Manifest CID (local cache): ${selected.row.cid}`);
|
|
2035
|
+
lines.push(`Manifest path: ${manifestPathHint}`);
|
|
2036
|
+
if (chosenSubdao) {
|
|
2037
|
+
lines.push(`Target SubDAO: ${chosenSubdao}`);
|
|
2038
|
+
} else {
|
|
2039
|
+
lines.push('Target SubDAO: (not auto-detected; you must choose an address)');
|
|
2040
|
+
}
|
|
2041
|
+
if (subdaoReason) {
|
|
2042
|
+
lines.push(`Reason: ${subdaoReason}`);
|
|
2043
|
+
}
|
|
2044
|
+
lines.push('');
|
|
2045
|
+
lines.push('Step 1: Inspect manifest (optional)');
|
|
2046
|
+
lines.push(` ${inspectCmd}`);
|
|
2047
|
+
lines.push('');
|
|
2048
|
+
lines.push('Step 2: Upload manifest and schedule/propose update');
|
|
2049
|
+
lines.push(` ${pushCmd}`);
|
|
2050
|
+
lines.push('');
|
|
2051
|
+
lines.push('Step 3: Governance follow-up');
|
|
2052
|
+
lines.push(` ${proposalsCmd}`);
|
|
2053
|
+
|
|
2054
|
+
const payload = {
|
|
2055
|
+
library: {
|
|
2056
|
+
cid: selected.row.cid,
|
|
2057
|
+
name: libName,
|
|
2058
|
+
description: libDescription,
|
|
2059
|
+
},
|
|
2060
|
+
targetSubdao: chosenSubdao || null,
|
|
2061
|
+
steps: [
|
|
2062
|
+
{ step: 1, title: 'Inspect manifest', command: inspectCmd },
|
|
2063
|
+
{ step: 2, title: 'Upload & propose', command: pushCmd },
|
|
2064
|
+
],
|
|
2065
|
+
extra: {
|
|
2066
|
+
governanceCommands: [proposalsCmd],
|
|
2067
|
+
},
|
|
2068
|
+
};
|
|
2069
|
+
|
|
2070
|
+
return {
|
|
2071
|
+
content: [
|
|
2072
|
+
{ type: 'text', text: lines.join('\n') },
|
|
2073
|
+
{ type: 'text', text: '```json\n' + JSON.stringify(payload, null, 2) + '\n```' },
|
|
2074
|
+
],
|
|
2075
|
+
};
|
|
2076
|
+
} catch (error) {
|
|
2077
|
+
return { content: [{ type: 'text', text: `Error generating publishing commands: ${error.message}` }] };
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
async improvePrompt({ key, library = '', pass = 'single', focus = 'all' } = {}) {
|
|
2082
|
+
try {
|
|
2083
|
+
if (!key) {
|
|
2084
|
+
return { content: [{ type: 'text', text: 'Error: key parameter is required' }] };
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
const promptResult = await this.getPrompt({ key, library });
|
|
2088
|
+
if (!promptResult?.content?.[1]?.text) {
|
|
2089
|
+
return promptResult;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
const jsonMatch = promptResult.content[1].text.match(/```json\n([\s\S]+)\n```/);
|
|
2093
|
+
if (!jsonMatch) {
|
|
2094
|
+
return { content: [{ type: 'text', text: 'Error: Failed to parse prompt data for improvement' }] };
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
const prompt = JSON.parse(jsonMatch[1]);
|
|
2098
|
+
const content = String(prompt.content || '');
|
|
2099
|
+
const description = String(prompt.description || '');
|
|
2100
|
+
|
|
2101
|
+
if (!content.trim()) {
|
|
2102
|
+
return { content: [{ type: 'text', text: `Prompt "${key}" has no content to analyze` }] };
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
// Variable analysis
|
|
2106
|
+
const varPattern = /\$\{([^}]+)\}/g;
|
|
2107
|
+
const foundVars = [];
|
|
2108
|
+
let m;
|
|
2109
|
+
while ((m = varPattern.exec(content)) !== null) {
|
|
2110
|
+
const v = m[1];
|
|
2111
|
+
if (!foundVars.includes(v)) foundVars.push(v);
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
// Simple heuristics
|
|
2115
|
+
const lengthChars = content.length;
|
|
2116
|
+
const lines = content.split(/\r?\n/).length;
|
|
2117
|
+
const mentionsOutput =
|
|
2118
|
+
/output|format|respond|response|return\b/i.test(content);
|
|
2119
|
+
const mentionsRole =
|
|
2120
|
+
/you are\b|as an? /i.test(content);
|
|
2121
|
+
|
|
2122
|
+
const improvementAreas = [];
|
|
2123
|
+
if (!mentionsOutput) {
|
|
2124
|
+
improvementAreas.push('Specify the expected output format (structure, tone, and level of detail).');
|
|
2125
|
+
}
|
|
2126
|
+
if (!mentionsRole) {
|
|
2127
|
+
improvementAreas.push('Clarify the assistant role (e.g. "You are a landscape design assistant...").');
|
|
2128
|
+
}
|
|
2129
|
+
if (!foundVars.length) {
|
|
2130
|
+
improvementAreas.push('Consider parameterizing important details with ${variables} to make the prompt reusable.');
|
|
2131
|
+
}
|
|
2132
|
+
if (foundVars.length && !description.toLowerCase().includes('variable')) {
|
|
2133
|
+
improvementAreas.push('Document variables in the description so users know what to provide.');
|
|
2134
|
+
}
|
|
2135
|
+
if (lengthChars < 300) {
|
|
2136
|
+
improvementAreas.push('Prompt is quite short; consider adding more context and edge-case handling.');
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
const focusNote = focus && focus !== 'all'
|
|
2140
|
+
? `\nFocus requested: **${focus}**\n`
|
|
2141
|
+
: '';
|
|
2142
|
+
|
|
2143
|
+
const baseInterviewQuestions = [
|
|
2144
|
+
'What edge cases or failure modes do you most want this prompt to handle?',
|
|
2145
|
+
'Should the prompt enforce a specific output format (e.g. bullet list, JSON, sections)?',
|
|
2146
|
+
'Are there any important variables (budget, constraints, region, style, etc.) that are currently missing?',
|
|
2147
|
+
'Should the prompt ask clarifying questions when information is missing, or make reasonable assumptions?',
|
|
2148
|
+
'Is this prompt meant for a single use-case, or should it be adaptable across multiple related tasks?',
|
|
2149
|
+
];
|
|
2150
|
+
|
|
2151
|
+
const variableQuestions = [
|
|
2152
|
+
'Are the variable names clear and self-explanatory to future users?',
|
|
2153
|
+
'Should any currently hard-coded values be turned into variables?',
|
|
2154
|
+
];
|
|
2155
|
+
|
|
2156
|
+
const edgeCaseQuestions = [
|
|
2157
|
+
'What should happen if key inputs are missing, contradictory, or clearly invalid?',
|
|
2158
|
+
'Are there any \"must not happen\" outcomes you want to guard against?',
|
|
2159
|
+
];
|
|
2160
|
+
|
|
2161
|
+
const outputQualityQuestions = [
|
|
2162
|
+
'Do you want the model to always respond with a consistent structure (sections, headings, bullet points)?',
|
|
2163
|
+
'Would example outputs help clarify what a great response looks like?',
|
|
2164
|
+
];
|
|
2165
|
+
|
|
2166
|
+
const reusabilityQuestions = [
|
|
2167
|
+
'Should this prompt be reusable across multiple projects or tightly scoped to one?',
|
|
2168
|
+
'Are there any project-specific details that should be parameterized instead of hard-coded?',
|
|
2169
|
+
];
|
|
2170
|
+
|
|
2171
|
+
// Select interview questions based on focus
|
|
2172
|
+
let interviewQuestions = baseInterviewQuestions.slice();
|
|
2173
|
+
if (focus === 'variables' || focus === 'all') interviewQuestions = interviewQuestions.concat(variableQuestions);
|
|
2174
|
+
if (focus === 'edge-cases' || focus === 'all') interviewQuestions = interviewQuestions.concat(edgeCaseQuestions);
|
|
2175
|
+
if (focus === 'output-quality' || focus === 'all') interviewQuestions = interviewQuestions.concat(outputQualityQuestions);
|
|
2176
|
+
if (focus === 'reusability' || focus === 'all') interviewQuestions = interviewQuestions.concat(reusabilityQuestions);
|
|
2177
|
+
|
|
2178
|
+
let text = `**Prompt Improvement Analysis**\n\n`;
|
|
2179
|
+
text += `**Name:** ${prompt.name || key}\n`;
|
|
2180
|
+
text += `**Key (stable ID):** ${prompt.key || key}\n`;
|
|
2181
|
+
text += `**Library:** ${prompt.library?.name || 'Unknown'}\n`;
|
|
2182
|
+
text += `**Length:** ${lengthChars} characters, ${lines} line(s)\n`;
|
|
2183
|
+
text += `**Variables:** ${foundVars.length ? foundVars.join(', ') : 'None'}\n`;
|
|
2184
|
+
text += `**Has role guidance:** ${mentionsRole ? 'Yes' : 'No'}\n`;
|
|
2185
|
+
text += `**Has output format guidance:** ${mentionsOutput ? 'Yes' : 'No'}\n`;
|
|
2186
|
+
text += `**Pass:** ${pass}\n`;
|
|
2187
|
+
text += focusNote;
|
|
2188
|
+
|
|
2189
|
+
if (improvementAreas.length) {
|
|
2190
|
+
text += `\n**Suggested Improvement Areas:**\n`;
|
|
2191
|
+
improvementAreas.forEach((area) => {
|
|
2192
|
+
text += `- ${area}\n`;
|
|
2193
|
+
});
|
|
2194
|
+
} else {
|
|
2195
|
+
text += `\n✅ No obvious structural issues detected. Focus on domain-specific refinements.\n`;
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
text += `\n**Interview Questions to Ask the User:**\n`;
|
|
2199
|
+
interviewQuestions.forEach((q, i) => {
|
|
2200
|
+
text += `${i + 1}. ${q}\n`;
|
|
2201
|
+
});
|
|
2202
|
+
|
|
2203
|
+
text += `\n**Next Steps (for the agent):**\n`;
|
|
2204
|
+
text += `- Ask the user some of the questions above.\n`;
|
|
2205
|
+
text += `- Use the answers to propose changes via quick_iterate_prompt (updating content, description, and tags).\n`;
|
|
2206
|
+
text += `- Optionally use rename_prompt to align key and display name if needed.\n`;
|
|
2207
|
+
|
|
2208
|
+
const metrics = {
|
|
2209
|
+
prompt: {
|
|
2210
|
+
key: prompt.key,
|
|
2211
|
+
name: prompt.name,
|
|
2212
|
+
library: prompt.library?.name || null,
|
|
2213
|
+
},
|
|
2214
|
+
metrics: {
|
|
2215
|
+
lengthChars,
|
|
2216
|
+
lines,
|
|
2217
|
+
variables: foundVars,
|
|
2218
|
+
hasRoleGuidance: mentionsRole,
|
|
2219
|
+
hasOutputGuidance: mentionsOutput,
|
|
2220
|
+
},
|
|
2221
|
+
};
|
|
2222
|
+
|
|
2223
|
+
let deep = null;
|
|
2224
|
+
if (pass === 'deep') {
|
|
2225
|
+
// Very lightweight scoring per dimension; all heuristic.
|
|
2226
|
+
const structureIssues = [];
|
|
2227
|
+
if (!mentionsRole) structureIssues.push('missing_role');
|
|
2228
|
+
if (!mentionsOutput) structureIssues.push('missing_output');
|
|
2229
|
+
if (lengthChars < 300) structureIssues.push('too_short');
|
|
2230
|
+
|
|
2231
|
+
const structureScore = 100
|
|
2232
|
+
- (structureIssues.includes('missing_role') ? 20 : 0)
|
|
2233
|
+
- (structureIssues.includes('missing_output') ? 20 : 0)
|
|
2234
|
+
- (structureIssues.includes('too_short') ? 10 : 0);
|
|
2235
|
+
|
|
2236
|
+
const variableScore = (() => {
|
|
2237
|
+
if (!foundVars.length) return 60;
|
|
2238
|
+
let score = 90;
|
|
2239
|
+
if (!description.toLowerCase().includes('variable')) score -= 10;
|
|
2240
|
+
return score;
|
|
2241
|
+
})();
|
|
2242
|
+
|
|
2243
|
+
const edgeMention = /edge[- ]case|failure|error|invalid|fallback/i.test(content);
|
|
2244
|
+
const edgeCasesScore = edgeMention ? 80 : 65;
|
|
2245
|
+
|
|
2246
|
+
const reusabilityScore = (() => {
|
|
2247
|
+
let score = 80;
|
|
2248
|
+
if (!foundVars.length) score -= 10;
|
|
2249
|
+
if (lengthChars < 300) score -= 10;
|
|
2250
|
+
return score;
|
|
2251
|
+
})();
|
|
2252
|
+
|
|
2253
|
+
deep = {
|
|
2254
|
+
dimensions: {
|
|
2255
|
+
structure: {
|
|
2256
|
+
score: Math.max(0, Math.min(100, structureScore)),
|
|
2257
|
+
findings: structureIssues,
|
|
2258
|
+
},
|
|
2259
|
+
variables: {
|
|
2260
|
+
score: Math.max(0, Math.min(100, variableScore)),
|
|
2261
|
+
findings: foundVars,
|
|
2262
|
+
},
|
|
2263
|
+
edgeCases: {
|
|
2264
|
+
score: Math.max(0, Math.min(100, edgeCasesScore)),
|
|
2265
|
+
findings: edgeMention ? ['mentions edge cases or error handling'] : ['no explicit edge-case/error-handling guidance'],
|
|
2266
|
+
},
|
|
2267
|
+
reusability: {
|
|
2268
|
+
score: Math.max(0, Math.min(100, reusabilityScore)),
|
|
2269
|
+
findings: [],
|
|
2270
|
+
},
|
|
2271
|
+
},
|
|
2272
|
+
};
|
|
2273
|
+
|
|
2274
|
+
text += `\n**Deep Analysis (scores are heuristic):**\n`;
|
|
2275
|
+
text += `- Structure: ${deep.dimensions.structure.score}/100\n`;
|
|
2276
|
+
text += `- Variables: ${deep.dimensions.variables.score}/100\n`;
|
|
2277
|
+
text += `- Edge Cases: ${deep.dimensions.edgeCases.score}/100\n`;
|
|
2278
|
+
text += `- Reusability: ${deep.dimensions.reusability.score}/100\n`;
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
const analysisJson = {
|
|
2282
|
+
...metrics,
|
|
2283
|
+
improvementAreas,
|
|
2284
|
+
interviewQuestions,
|
|
2285
|
+
focus: focus || null,
|
|
2286
|
+
pass,
|
|
2287
|
+
deep,
|
|
2288
|
+
};
|
|
2289
|
+
|
|
2290
|
+
return {
|
|
2291
|
+
content: [
|
|
2292
|
+
{ type: 'text', text },
|
|
2293
|
+
{ type: 'text', text: '```json\n' + JSON.stringify(analysisJson, null, 2) + '\n```' },
|
|
2294
|
+
],
|
|
2295
|
+
};
|
|
2296
|
+
} catch (error) {
|
|
2297
|
+
return { content: [{ type: 'text', text: `Error improving prompt: ${error.message}` }] };
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
async renamePrompt(params = {}) {
|
|
2302
|
+
try {
|
|
2303
|
+
const result = await this.quickStartHandler.renamePrompt(params);
|
|
2304
|
+
return {
|
|
2305
|
+
content: [
|
|
2306
|
+
{ type: 'text', text: `✅ ${result.message}` },
|
|
2307
|
+
{ type: 'text', text: '```json\n' + JSON.stringify(result, null, 2) + '\n```' },
|
|
2308
|
+
],
|
|
2309
|
+
};
|
|
2310
|
+
} catch (error) {
|
|
2311
|
+
return { content: [{ type: 'text', text: `Error renaming prompt: ${error.message}` }] };
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
async quickCreatePrompt(params) {
|
|
2316
|
+
try {
|
|
2317
|
+
const result = await this.quickStartHandler.quickCreatePrompt(params);
|
|
2318
|
+
return {
|
|
2319
|
+
content: [
|
|
2320
|
+
{ type: 'text', text: `✅ ${result.message}\n\nNext steps:\n- Test it: quick_test_prompt(key="${result.key}")\n- Edit it: quick_iterate_prompt(key="${result.key}")` },
|
|
2321
|
+
{ type: 'text', text: '```json\n' + JSON.stringify(result, null, 2) + '\n```' }
|
|
2322
|
+
]
|
|
2323
|
+
};
|
|
2324
|
+
} catch (error) {
|
|
2325
|
+
return { content: [{ type: 'text', text: `Error creating prompt: ${error.message}` }] };
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
async quickIteratePrompt(params) {
|
|
2330
|
+
try {
|
|
2331
|
+
const result = await this.quickStartHandler.quickIteratePrompt(params);
|
|
2332
|
+
return {
|
|
2333
|
+
content: [
|
|
2334
|
+
{ type: 'text', text: `✅ ${result.message}\n\n(Backup of previous version saved)` },
|
|
2335
|
+
{ type: 'text', text: '```json\n' + JSON.stringify(result, null, 2) + '\n```' }
|
|
2336
|
+
]
|
|
2337
|
+
};
|
|
2338
|
+
} catch (error) {
|
|
2339
|
+
return { content: [{ type: 'text', text: `Error updating prompt: ${error.message}` }] };
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
getHelp({ topic } = {}) {
|
|
2344
|
+
const topics = {
|
|
2345
|
+
create: `
|
|
2346
|
+
**Creating Prompts**
|
|
2347
|
+
1. Use \`quick_create_prompt(name="my-prompt", content="...")\`
|
|
2348
|
+
2. It will be added to "My Library" by default.
|
|
2349
|
+
3. Use \`quick_test_prompt(key="my-prompt")\` to verify it.
|
|
2350
|
+
4. Use \`quick_iterate_prompt(key="...", name="...", description="...", tags=[...])\` to refine content & metadata.
|
|
2351
|
+
`,
|
|
2352
|
+
publish: `
|
|
2353
|
+
**Publishing Prompts**
|
|
2354
|
+
1. Ensure your prompts are ready in a local library.
|
|
2355
|
+
2. Use \`publish_manifest_flow\` to validate and build a governance payload.
|
|
2356
|
+
3. When ready, use \`generate_publishing_commands(library=\"...\")\` to get copy-paste CLI commands for \`sage library push\` and proposal follow-up.
|
|
2357
|
+
4. This makes them available on-chain for others to discover.
|
|
2358
|
+
`,
|
|
2359
|
+
versioning: `
|
|
2360
|
+
**Versioning**
|
|
2361
|
+
- \`quick_iterate_prompt\` automatically creates backups of the previous version.
|
|
2362
|
+
- Prompts are stored as files in \`~/.sage/libraries/prompts/\`, so you can use Git for version control.
|
|
2363
|
+
- The \`key\` is a stable identifier used to reference the prompt; \`name\` is the human-readable display name.
|
|
2364
|
+
- Use \`rename_prompt(key="old", newKey="new", name="New Name")\` when you truly need to change the key; otherwise, prefer updating \`name\` only.
|
|
2365
|
+
`,
|
|
2366
|
+
improve: `
|
|
2367
|
+
**Improve Prompt**
|
|
2368
|
+
- \`improve_prompt\` analyzes an existing prompt and returns advisory output only. It does not modify files.
|
|
2369
|
+
- Parameters:
|
|
2370
|
+
- \`key\`: prompt key (required)
|
|
2371
|
+
- \`library\`: optional library name or CID for disambiguation
|
|
2372
|
+
- \`pass\` or \`depth\`: "single" (default) or "deep"
|
|
2373
|
+
- \`focus\`: "variables" | "edge-cases" | "output-quality" | "reusability" | "all"
|
|
2374
|
+
|
|
2375
|
+
Behavior:
|
|
2376
|
+
- \`pass="single"\`: light structural checks + suggested improvement areas + generic interview questions.
|
|
2377
|
+
- \`pass="deep"\`: adds heuristic per-dimension scores (structure, variables, edge cases, reusability) plus richer questions.
|
|
2378
|
+
|
|
2379
|
+
Not implemented (advisory only):
|
|
2380
|
+
- There is currently **no** \`auto_apply\` or \`compare_to\` behavior. Use \`improve_prompt\` to design changes, then apply them via \`quick_iterate_prompt\` or \`bulk_update_prompts\`.
|
|
2381
|
+
`,
|
|
2382
|
+
manifest: `
|
|
2383
|
+
**Manifest Structure (v2)**
|
|
2384
|
+
|
|
2385
|
+
Minimal example:
|
|
2386
|
+
|
|
2387
|
+
\`\`\`json
|
|
2388
|
+
{
|
|
2389
|
+
"version": 2,
|
|
2390
|
+
"library": {
|
|
2391
|
+
"name": "My Library",
|
|
2392
|
+
"description": "Optional description"
|
|
2393
|
+
},
|
|
2394
|
+
"prompts": [
|
|
2395
|
+
{
|
|
2396
|
+
"key": "hello-world",
|
|
2397
|
+
"name": "Hello World",
|
|
2398
|
+
"description": "A simple test prompt",
|
|
2399
|
+
"tags": ["test"],
|
|
2400
|
+
"cid": "Qm...optional-ipfs-cid",
|
|
2401
|
+
"files": ["prompts/hello-world.md"]
|
|
2402
|
+
}
|
|
2403
|
+
]
|
|
2404
|
+
}
|
|
2405
|
+
\`\`\`
|
|
2406
|
+
|
|
2407
|
+
Use \`validate_manifest(manifest=...)\` to see detailed errors and hints if anything is missing.
|
|
2408
|
+
`
|
|
2409
|
+
};
|
|
2410
|
+
|
|
2411
|
+
if (topic && topics[topic]) {
|
|
2412
|
+
return { content: [{ type: 'text', text: topics[topic] }] };
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
const generalHelp = `
|
|
2416
|
+
**Sage MCP Tools Help**
|
|
2417
|
+
|
|
2418
|
+
**Quick Start:**
|
|
2419
|
+
- Create: \`quick_create_prompt(name="...", content="...")\`
|
|
2420
|
+
- Edit: \`quick_iterate_prompt(key="...", content="...")\`
|
|
2421
|
+
- Test: \`quick_test_prompt(key="...", variables={...})\`
|
|
2422
|
+
|
|
2423
|
+
**Discovery:**
|
|
2424
|
+
- Browse: \`list_prompts()\`
|
|
2425
|
+
- Search: \`search_prompts(query="...")\`
|
|
2426
|
+
- Inspect: \`get_prompt(key="...")\`
|
|
2427
|
+
|
|
2428
|
+
**Next Steps:**
|
|
2429
|
+
Use \`help(topic="create")\` for more details.
|
|
2430
|
+
`;
|
|
2431
|
+
return { content: [{ type: 'text', text: generalHelp }] };
|
|
2432
|
+
}
|
|
2433
|
+
|
|
901
2434
|
async listLibraries(options = {}) {
|
|
902
2435
|
return this.listLibrariesHandler(options);
|
|
903
2436
|
}
|
|
@@ -907,22 +2440,23 @@ class SageMCPServer {
|
|
|
907
2440
|
if (!rows.length) {
|
|
908
2441
|
return { content: [{ type: 'text', text: 'No metaprompts saved yet. Use save_metaprompt to create one.' }] };
|
|
909
2442
|
}
|
|
910
|
-
const textLines = rows.map((row, idx) => `${idx + 1
|
|
2443
|
+
const textLines = rows.map((row, idx) => `${idx + 1
|
|
2444
|
+
}. ** ${row.title}** (${row.slug}) \n 🕒 Updated: ${row.updatedAt} \n 🔖 Tags: ${row.tags.join(', ') || 'none'} \n 📝 ${row.summary || 'No summary provided.'} \n`).join('\n');
|
|
911
2445
|
return {
|
|
912
2446
|
content: [
|
|
913
|
-
{ type: 'text', text: `Metaprompts
|
|
914
|
-
{ type: '
|
|
2447
|
+
{ type: 'text', text: `Metaprompts(${rows.length}) \n\n${textLines} ` },
|
|
2448
|
+
{ type: 'text', text: '```json\n' + JSON.stringify({ metaprompts: rows }, null, 2) + '\n```' }
|
|
915
2449
|
]
|
|
916
2450
|
};
|
|
917
2451
|
}
|
|
918
2452
|
|
|
919
|
-
startMetapromptInterview({ goal, model, interviewStyle }) {
|
|
920
|
-
const primer = metapromptDesigner.generateInterviewPrimer({ goal, model, interviewStyle });
|
|
921
|
-
const instructions = `Use the following guidance as your first message to the LLM interviewer
|
|
2453
|
+
startMetapromptInterview({ goal, model, interviewStyle, additionalInstructions }) {
|
|
2454
|
+
const primer = metapromptDesigner.generateInterviewPrimer({ goal, model, interviewStyle, additionalInstructions });
|
|
2455
|
+
const instructions = `Use the following guidance as your first message to the LLM interviewer: \n\n${primer} `;
|
|
922
2456
|
return {
|
|
923
2457
|
content: [
|
|
924
2458
|
{ type: 'text', text: instructions },
|
|
925
|
-
{ type: '
|
|
2459
|
+
{ type: 'text', text: '```json\n' + JSON.stringify({ primer, suggestedUse: 'Send as system message to initiate one - question - at - a - time interview.' }, null, 2) + '\n```' }
|
|
926
2460
|
]
|
|
927
2461
|
};
|
|
928
2462
|
}
|
|
@@ -949,7 +2483,7 @@ class SageMCPServer {
|
|
|
949
2483
|
const agentsPath = metapromptDesigner.getAgentsNotebookPath();
|
|
950
2484
|
try {
|
|
951
2485
|
fs.mkdirSync(path.dirname(agentsPath), { recursive: true });
|
|
952
|
-
const entry = `\n## ${title}\n\n${body}\n`;
|
|
2486
|
+
const entry = `\n## ${title} \n\n${body} \n`;
|
|
953
2487
|
fs.appendFileSync(agentsPath, entry, 'utf8');
|
|
954
2488
|
extras.appendedToAgents = agentsPath;
|
|
955
2489
|
} catch (e) {
|
|
@@ -965,14 +2499,14 @@ class SageMCPServer {
|
|
|
965
2499
|
...extras,
|
|
966
2500
|
};
|
|
967
2501
|
const lines = [];
|
|
968
|
-
lines.push(`✅ Saved metaprompt '${title}' at ${saved.path}`);
|
|
2502
|
+
lines.push(`✅ Saved metaprompt '${title}' at ${saved.path} `);
|
|
969
2503
|
if (destination && destination !== saved.path) {
|
|
970
|
-
lines.push(`📚 Workspace prompt: ${destination}`);
|
|
2504
|
+
lines.push(`📚 Workspace prompt: ${destination} `);
|
|
971
2505
|
}
|
|
972
2506
|
return {
|
|
973
2507
|
content: [
|
|
974
2508
|
{ type: 'text', text: lines.join('\n') },
|
|
975
|
-
{ type: '
|
|
2509
|
+
{ type: 'text', text: '```json\n' + JSON.stringify(payload, null, 2) + '\n```' }
|
|
976
2510
|
]
|
|
977
2511
|
};
|
|
978
2512
|
}
|
|
@@ -983,13 +2517,13 @@ class SageMCPServer {
|
|
|
983
2517
|
const preview = loaded.body.slice(0, 800);
|
|
984
2518
|
return {
|
|
985
2519
|
content: [
|
|
986
|
-
{ type: 'text', text: `Loaded metaprompt '${slug}'\n\n${preview}${loaded.body.length > 800 ? '\n... (truncated)' : ''}` },
|
|
987
|
-
{ type: '
|
|
2520
|
+
{ type: 'text', text: `Loaded metaprompt '${slug}'\n\n${preview}${loaded.body.length > 800 ? '\n... (truncated)' : ''} ` },
|
|
2521
|
+
{ type: 'text', text: '```json\n' + JSON.stringify({ slug, meta: loaded.meta, body: loaded.body, path: loaded.path }, null, 2) + '\n```' }
|
|
988
2522
|
]
|
|
989
2523
|
};
|
|
990
2524
|
} catch (error) {
|
|
991
2525
|
return {
|
|
992
|
-
content: [{ type: 'text', text: `Error loading metaprompt: ${error.message}` }]
|
|
2526
|
+
content: [{ type: 'text', text: `Error loading metaprompt: ${error.message} ` }]
|
|
993
2527
|
};
|
|
994
2528
|
}
|
|
995
2529
|
}
|
|
@@ -997,12 +2531,12 @@ class SageMCPServer {
|
|
|
997
2531
|
generateMetapromptLink({ body }) {
|
|
998
2532
|
const link = metapromptDesigner.generateChatGPTLink(body);
|
|
999
2533
|
const text = link
|
|
1000
|
-
? `ChatGPT link ready: ${link}`
|
|
2534
|
+
? `ChatGPT link ready: ${link} `
|
|
1001
2535
|
: '⚠️ Prompt too long to produce a ChatGPT launch link. Copy the prompt manually.';
|
|
1002
2536
|
return {
|
|
1003
2537
|
content: [
|
|
1004
2538
|
{ type: 'text', text },
|
|
1005
|
-
{ type: '
|
|
2539
|
+
{ type: 'text', text: '```json\n' + JSON.stringify({ provider: 'chatgpt', link }, null, 2) + '\n```' }
|
|
1006
2540
|
]
|
|
1007
2541
|
};
|
|
1008
2542
|
}
|
|
@@ -1013,7 +2547,9 @@ class SageMCPServer {
|
|
|
1013
2547
|
let content = '';
|
|
1014
2548
|
const preferred = process.env.SAGE_IPFS_GATEWAY || '';
|
|
1015
2549
|
const gateways = [
|
|
1016
|
-
preferred ? `${preferred.replace(/\/$/, '')}/ipfs/${cid}` : null,
|
|
2550
|
+
preferred ? `${preferred.replace(/\/$/, '')} /ipfs/${cid} ` : null,
|
|
2551
|
+
`https://cloudflare-ipfs.com/ipfs/${cid}`,
|
|
2552
|
+
`https://gateway.pinata.cloud/ipfs/${cid}`,
|
|
1017
2553
|
`https://dweb.link/ipfs/${cid}`,
|
|
1018
2554
|
`https://nftstorage.link/ipfs/${cid}`,
|
|
1019
2555
|
`https://ipfs.io/ipfs/${cid}`
|
|
@@ -1040,7 +2576,7 @@ class SageMCPServer {
|
|
|
1040
2576
|
if (!content) {
|
|
1041
2577
|
return { content: [{ type: 'text', text: `Error downloading prompt content: ${lastErr ? lastErr.message : 'unknown error'}` }] };
|
|
1042
2578
|
}
|
|
1043
|
-
|
|
2579
|
+
|
|
1044
2580
|
return {
|
|
1045
2581
|
content: [
|
|
1046
2582
|
{
|
|
@@ -1061,41 +2597,70 @@ class SageMCPServer {
|
|
|
1061
2597
|
}
|
|
1062
2598
|
}
|
|
1063
2599
|
|
|
1064
|
-
async listSubDAOs({ limit = 20 }) {
|
|
2600
|
+
async listSubDAOs({ limit = 20 } = {}) {
|
|
1065
2601
|
try {
|
|
1066
|
-
const subDAOs = await this.getSubDAOList();
|
|
2602
|
+
const subDAOs = await this.getSubDAOList({ limit });
|
|
2603
|
+
|
|
2604
|
+
if (!subDAOs || subDAOs.length === 0) {
|
|
2605
|
+
return {
|
|
2606
|
+
content: [{
|
|
2607
|
+
type: 'text',
|
|
2608
|
+
text: 'No SubDAOs found.\n\n' +
|
|
2609
|
+
'This may be because:\n' +
|
|
2610
|
+
'• The subgraph is not configured (check SUBGRAPH_URL in .env)\n' +
|
|
2611
|
+
'• The factory contract has no SubDAOs deployed yet\n' +
|
|
2612
|
+
'• Network connectivity issues with the RPC endpoint\n\n' +
|
|
2613
|
+
'Configuration:\n' +
|
|
2614
|
+
`• SUBGRAPH_URL: ${process.env.SUBGRAPH_URL || 'not set'}\n` +
|
|
2615
|
+
`• SUBDAO_FACTORY_ADDRESS: ${process.env.SUBDAO_FACTORY_ADDRESS || 'not set'}\n` +
|
|
2616
|
+
`• RPC_URL: ${process.env.RPC_URL ? process.env.RPC_URL.substring(0, 50) + '...' : 'not set'}`
|
|
2617
|
+
}]
|
|
2618
|
+
};
|
|
2619
|
+
}
|
|
2620
|
+
|
|
1067
2621
|
const limitedSubDAOs = subDAOs.slice(0, limit);
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
`${i + 1}. **${subdao.name}**\n` +
|
|
2622
|
+
const resultText = `Found ${limitedSubDAOs.length} SubDAO(s):\n\n` +
|
|
2623
|
+
limitedSubDAOs.map((subdao, i) =>
|
|
2624
|
+
`${i + 1}. **${subdao.name || 'Unnamed SubDAO'}**\n` +
|
|
1072
2625
|
` 📍 Address: ${subdao.address}\n` +
|
|
1073
|
-
` 📋 Registry: ${subdao.registryAddress}\n
|
|
2626
|
+
` 📋 Registry: ${subdao.registryAddress || 'N/A'}\n` +
|
|
2627
|
+
` 📚 Libraries: ${subdao.registries?.length || 0}\n\n`
|
|
1074
2628
|
).join('');
|
|
1075
2629
|
|
|
1076
2630
|
return {
|
|
1077
|
-
content: [
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
}
|
|
1082
|
-
]
|
|
2631
|
+
content: [{
|
|
2632
|
+
type: 'text',
|
|
2633
|
+
text: resultText
|
|
2634
|
+
}]
|
|
1083
2635
|
};
|
|
1084
2636
|
} catch (error) {
|
|
2637
|
+
this.log?.error?.('list_subdaos_failed', {
|
|
2638
|
+
error: error.message,
|
|
2639
|
+
stack: error.stack,
|
|
2640
|
+
subgraphUrl: process.env.SUBGRAPH_URL,
|
|
2641
|
+
factoryAddress: process.env.SUBDAO_FACTORY_ADDRESS
|
|
2642
|
+
});
|
|
2643
|
+
|
|
1085
2644
|
return {
|
|
1086
|
-
content: [
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
2645
|
+
content: [{
|
|
2646
|
+
type: 'text',
|
|
2647
|
+
text: `Error listing SubDAOs: ${error.message}\n\n` +
|
|
2648
|
+
'This tool requires one of the following configurations:\n' +
|
|
2649
|
+
'• SUBGRAPH_URL (recommended) - for fast SubDAO discovery\n' +
|
|
2650
|
+
'• SUBDAO_FACTORY_ADDRESS + RPC_URL - for on-chain event scanning\n\n' +
|
|
2651
|
+
'Current configuration:\n' +
|
|
2652
|
+
`• SUBGRAPH_URL: ${process.env.SUBGRAPH_URL || '❌ not set'}\n` +
|
|
2653
|
+
`• SUBDAO_FACTORY_ADDRESS: ${process.env.SUBDAO_FACTORY_ADDRESS || '❌ not set'}\n` +
|
|
2654
|
+
`• RPC_URL: ${process.env.RPC_URL ? '✅ set' : '❌ not set'}\n\n` +
|
|
2655
|
+
`Debug info: ${error.stack?.split('\n').slice(0, 3).join('\n')}`
|
|
2656
|
+
}]
|
|
1092
2657
|
};
|
|
1093
2658
|
}
|
|
1094
2659
|
}
|
|
1095
2660
|
|
|
1096
2661
|
async listSubdaoLibraries({ subdao }) {
|
|
1097
2662
|
const list = await this.getSubDAOList();
|
|
1098
|
-
const found = list.find(x => (x.address || '').toLowerCase() === String(subdao||'').toLowerCase());
|
|
2663
|
+
const found = list.find(x => (x.address || '').toLowerCase() === String(subdao || '').toLowerCase());
|
|
1099
2664
|
if (!found) {
|
|
1100
2665
|
// Attempt to rebuild from chain/subgraph
|
|
1101
2666
|
const regs = await this._buildBindingsForSubdao(subdao);
|
|
@@ -1174,8 +2739,8 @@ class SageMCPServer {
|
|
|
1174
2739
|
}
|
|
1175
2740
|
}
|
|
1176
2741
|
|
|
1177
|
-
async publishManifestFlow({ manifest, subdao = '', description = '' }) {
|
|
1178
|
-
return this.manifestWorkflows.publishManifestFlow({ manifest, subdao, description });
|
|
2742
|
+
async publishManifestFlow({ manifest, subdao = '', description = '', dry_run = false }) {
|
|
2743
|
+
return this.manifestWorkflows.publishManifestFlow({ manifest, subdao, description, dry_run });
|
|
1179
2744
|
}
|
|
1180
2745
|
|
|
1181
2746
|
// Helper methods (same as HTTP server implementation)
|
|
@@ -1186,7 +2751,7 @@ class SageMCPServer {
|
|
|
1186
2751
|
async searchSubDAOPrompts(allPrompts, targetSubDAO = '', includeContent = false) {
|
|
1187
2752
|
try {
|
|
1188
2753
|
const subDAOs = await this.getSubDAOList();
|
|
1189
|
-
|
|
2754
|
+
|
|
1190
2755
|
for (const subDAO of subDAOs) {
|
|
1191
2756
|
if (targetSubDAO && subDAO.address.toLowerCase() !== targetSubDAO.toLowerCase()) {
|
|
1192
2757
|
continue;
|
|
@@ -1218,11 +2783,11 @@ class SageMCPServer {
|
|
|
1218
2783
|
const from = Math.max(earliest, to - WINDOW);
|
|
1219
2784
|
const evsUpd = await contract.queryFilter(contract.filters.PromptUpdated(), from, to).catch(() => []);
|
|
1220
2785
|
for (const e of evsUpd) {
|
|
1221
|
-
try { const { key, cid } = e.args; latestPrompts.set(key, { cid: cid.toString() }); } catch {}
|
|
2786
|
+
try { const { key, cid } = e.args; latestPrompts.set(key, { cid: cid.toString() }); } catch { }
|
|
1222
2787
|
}
|
|
1223
2788
|
const evsAdd = await contract.queryFilter(contract.filters.PromptAdded(), from, to).catch(() => []);
|
|
1224
2789
|
for (const e of evsAdd) {
|
|
1225
|
-
try { const cid = e.args.contentCID.toString(); const key = cid; if (!latestPrompts.has(key)) latestPrompts.set(key, { cid }); } catch {}
|
|
2790
|
+
try { const cid = e.args.contentCID.toString(); const key = cid; if (!latestPrompts.has(key)) latestPrompts.set(key, { cid }); } catch { }
|
|
1226
2791
|
}
|
|
1227
2792
|
to = from - 1;
|
|
1228
2793
|
}
|
|
@@ -1230,7 +2795,15 @@ class SageMCPServer {
|
|
|
1230
2795
|
for (const [key, { cid }] of latestPrompts) {
|
|
1231
2796
|
let content = '';
|
|
1232
2797
|
if (includeContent && cid) {
|
|
1233
|
-
try {
|
|
2798
|
+
try {
|
|
2799
|
+
const IPFSManager = require('./ipfs-manager');
|
|
2800
|
+
const ipfs = new IPFSManager();
|
|
2801
|
+
await ipfs.initialize();
|
|
2802
|
+
const data = await ipfs.downloadJson(cid);
|
|
2803
|
+
content = data?.content || data?.prompt?.content || JSON.stringify(data);
|
|
2804
|
+
} catch (_) {
|
|
2805
|
+
// Best-effort: leave content empty on failure
|
|
2806
|
+
}
|
|
1234
2807
|
}
|
|
1235
2808
|
allPrompts.push({
|
|
1236
2809
|
id: `${subDAO.address}-${r.libraryId}-${key}`,
|
|
@@ -1269,7 +2842,7 @@ class SageMCPServer {
|
|
|
1269
2842
|
hexToString(hex) {
|
|
1270
2843
|
try {
|
|
1271
2844
|
hex = hex.replace(/^0x/, '');
|
|
1272
|
-
|
|
2845
|
+
|
|
1273
2846
|
if (hex.length > 128) {
|
|
1274
2847
|
// Skip the offset (first 64 chars)
|
|
1275
2848
|
// Get the length from the next 64 chars
|
|
@@ -1277,7 +2850,7 @@ class SageMCPServer {
|
|
|
1277
2850
|
const length = parseInt(lengthHex, 16) * 2;
|
|
1278
2851
|
// Get the actual content
|
|
1279
2852
|
const contentHex = hex.slice(128, 128 + length);
|
|
1280
|
-
|
|
2853
|
+
|
|
1281
2854
|
let result = '';
|
|
1282
2855
|
for (let i = 0; i < contentHex.length; i += 2) {
|
|
1283
2856
|
const byte = parseInt(contentHex.substr(i, 2), 16);
|
|
@@ -1299,7 +2872,7 @@ class SageMCPServer {
|
|
|
1299
2872
|
if (require.main === module) {
|
|
1300
2873
|
const server = new SageMCPServer();
|
|
1301
2874
|
server.run().catch((error) => {
|
|
1302
|
-
try { console.error('MCP Server error:', error); } catch(_) {}
|
|
2875
|
+
try { console.error('MCP Server error:', error); } catch (_) { }
|
|
1303
2876
|
process.exit(1);
|
|
1304
2877
|
});
|
|
1305
2878
|
}
|