@sage-protocol/cli 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/cli/browser-wallet-integration.js +0 -1
  2. package/dist/cli/cast-wallet-manager.js +0 -1
  3. package/dist/cli/commands/interview.js +149 -0
  4. package/dist/cli/commands/personal.js +138 -79
  5. package/dist/cli/commands/prompts.js +242 -87
  6. package/dist/cli/commands/stake-status.js +0 -2
  7. package/dist/cli/config.js +28 -8
  8. package/dist/cli/governance-manager.js +28 -19
  9. package/dist/cli/index.js +32 -8
  10. package/dist/cli/library-manager.js +16 -6
  11. package/dist/cli/mcp-server-stdio.js +759 -156
  12. package/dist/cli/mcp-server.js +4 -30
  13. package/dist/cli/metamask-integration.js +0 -1
  14. package/dist/cli/privy-wallet-manager.js +2 -2
  15. package/dist/cli/prompt-manager.js +0 -1
  16. package/dist/cli/services/artifact-manager.js +198 -0
  17. package/dist/cli/services/doctor/fixers.js +1 -1
  18. package/dist/cli/services/mcp/env-loader.js +2 -0
  19. package/dist/cli/services/mcp/prompt-result-formatter.js +8 -1
  20. package/dist/cli/services/mcp/quick-start.js +14 -15
  21. package/dist/cli/services/mcp/sage-tool-registry.js +322 -0
  22. package/dist/cli/services/mcp/tool-args-validator.js +43 -0
  23. package/dist/cli/services/metaprompt/anthropic-client.js +87 -0
  24. package/dist/cli/services/metaprompt/interview-driver.js +161 -0
  25. package/dist/cli/services/metaprompt/model-client.js +49 -0
  26. package/dist/cli/services/metaprompt/openai-client.js +67 -0
  27. package/dist/cli/services/metaprompt/persistence.js +86 -0
  28. package/dist/cli/services/metaprompt/prompt-builder.js +186 -0
  29. package/dist/cli/services/metaprompt/session.js +18 -80
  30. package/dist/cli/services/metaprompt/slot-planner.js +115 -0
  31. package/dist/cli/services/metaprompt/templates.json +130 -0
  32. package/dist/cli/services/project-context.js +98 -0
  33. package/dist/cli/subdao.js +0 -3
  34. package/dist/cli/sxxx-manager.js +0 -1
  35. package/dist/cli/utils/aliases.js +0 -6
  36. package/dist/cli/utils/tx-wait.js +0 -3
  37. package/dist/cli/wallet-manager.js +18 -19
  38. package/dist/cli/walletconnect-integration.js +0 -1
  39. package/dist/cli/wizard-manager.js +0 -1
  40. package/package.json +3 -1
  41. package/dist/cli/commands/prompt-test.js +0 -176
  42. package/dist/cli/commands/prompt.js +0 -2531
@@ -53,7 +53,13 @@ const { createLibraryLister, formatLibraryList } = require('./services/mcp/libra
53
53
  const { createSubdaoDiscovery } = require('./services/mcp/subdao-discovery');
54
54
  const { createUnifiedPromptSearcher } = require('./services/mcp/unified-prompt-search');
55
55
  const { createToolArgsValidator } = require('./services/mcp/tool-args-validator');
56
+ const {
57
+ TOOL_REGISTRY,
58
+ getToolsForCategory,
59
+ getToolMeta,
60
+ } = require('./services/mcp/sage-tool-registry');
56
61
  const { createToolDispatcher } = require('./services/mcp/tool-dispatcher');
62
+ const ProjectContextService = require('./services/project-context');
57
63
  const { createProposalLister } = require('./services/mcp/proposal-lister');
58
64
  const fs = require('fs');
59
65
  const path = require('path');
@@ -79,6 +85,53 @@ class SageMCPServer {
79
85
  };
80
86
 
81
87
  this.tools = [
88
+ {
89
+ name: 'get_project_context',
90
+ description: `Get context about the current Sage project (root, DAO, counts, etc).
91
+ When to use:
92
+ - At the start of a session to understand where you are
93
+ - Before suggesting tools to check if workspace/wallet is available`,
94
+ inputSchema: {
95
+ type: 'object',
96
+ properties: {},
97
+ required: []
98
+ }
99
+ },
100
+ {
101
+ name: 'suggest_sage_tools',
102
+ description: `Suggest the most relevant Sage MCP tools for a given goal.
103
+ When to use:
104
+ - You have a vague goal and want help choosing the right Sage tools
105
+ - You want a ranked list of 3–5 tools with rationale and required parameters
106
+
107
+ This tool does not execute any actions; it only recommends which tools to call next.`,
108
+ inputSchema: {
109
+ type: 'object',
110
+ properties: {
111
+ goal: { type: 'string', description: 'Free-text description of what you are trying to do' },
112
+ stage: {
113
+ type: 'string',
114
+ enum: ['prompt_workspace', 'persona', 'libraries', 'governance', 'treasury', 'discovery'],
115
+ description: 'Optional hint about which area you are working in',
116
+ },
117
+ context: {
118
+ type: 'object',
119
+ description: 'Optional execution context (workspace, wallet, etc.)',
120
+ properties: {
121
+ hasWorkspace: { type: 'boolean', description: 'Whether a Sage prompt workspace is present' },
122
+ hasWallet: { type: 'boolean', description: 'Whether a wallet is configured/available' },
123
+ currentTask: { type: 'string', description: 'Short description of what you just did' },
124
+ },
125
+ },
126
+ limit: { type: 'number', description: 'Maximum number of primary recommendations (default: 5)' },
127
+ includeAlternatives: {
128
+ type: 'boolean',
129
+ description: 'Whether to include a secondary list of alternative tools (default: false)',
130
+ },
131
+ },
132
+ required: ['goal'],
133
+ },
134
+ },
82
135
  {
83
136
  name: 'quick_create_prompt',
84
137
  description: `Quickly create a new prompt. Handles library creation automatically.
@@ -215,7 +268,7 @@ Next steps:
215
268
  properties: {
216
269
  query: { type: 'string', description: 'Free-text search applied to names, descriptions, and tags' },
217
270
  source: { type: 'string', enum: ['local', 'onchain', 'all'], description: 'Data source preference (default: all)' },
218
- subdao: { type: 'string', description: 'Optional SubDAO address when source=onchain' },
271
+ subdao: { type: 'string', description: 'Optional DAO address when source=onchain' },
219
272
  tags: { type: 'array', items: { type: 'string' }, description: 'Optional tag filters (matches any tag)' },
220
273
  includeContent: { type: 'boolean', description: 'Include raw prompt content when supported (default: false)' },
221
274
  page: { type: 'number', description: 'Results page (default: 1)' },
@@ -309,7 +362,7 @@ Next steps:
309
362
  },
310
363
  {
311
364
  name: 'search_onchain_prompts',
312
- description: 'Search for prompts directly on-chain from LibraryRegistry and SubDAO registries',
365
+ description: 'Search for prompts directly on-chain from LibraryRegistry and DAO registries',
313
366
  inputSchema: {
314
367
  type: 'object',
315
368
  properties: {
@@ -319,7 +372,7 @@ Next steps:
319
372
  },
320
373
  subdao: {
321
374
  type: 'string',
322
- description: 'Filter by specific SubDAO address (optional)'
375
+ description: 'Filter by specific DAO address (optional)'
323
376
  },
324
377
  tags: {
325
378
  type: 'array',
@@ -374,6 +427,42 @@ Next steps:
374
427
  required: []
375
428
  }
376
429
  },
430
+ {
431
+ name: 'list_persona_templates',
432
+ description: 'List available persona templates for the Metaprompt Engine',
433
+ inputSchema: {
434
+ type: 'object',
435
+ properties: {},
436
+ required: []
437
+ }
438
+ },
439
+ {
440
+ name: 'run_persona_interview',
441
+ description: 'Generate a persona system prompt from structured answers (One-Shot, no human loop)',
442
+ inputSchema: {
443
+ type: 'object',
444
+ properties: {
445
+ template: { type: 'string', description: 'Template key (e.g., coding-assistant, governance-helper)' },
446
+ answers: { type: 'object', description: 'Key-value map of slot answers' },
447
+ save: { type: 'boolean', description: 'Whether to save the artifact to disk (default: false)' },
448
+ saveKey: { type: 'string', description: 'Optional filename for saving' }
449
+ },
450
+ required: ['template', 'answers']
451
+ }
452
+ },
453
+ {
454
+ name: 'persona_interview_step',
455
+ description: 'Stateful persona interview. Returns slot metadata for the host agent to phrase questions. Pass answers back with the stateToken to progress through slots.',
456
+ inputSchema: {
457
+ type: 'object',
458
+ properties: {
459
+ template: { type: 'string', description: 'Template key (coding-assistant, governance-helper, research-analyst, custom). Required on first call.' },
460
+ stateToken: { type: 'string', description: 'Base64-encoded state from previous response. Omit on first call.' },
461
+ answer: { type: 'string', description: 'Answer for the slot identified by currentSlotKey in the previous response.' }
462
+ },
463
+ required: []
464
+ }
465
+ },
377
466
  {
378
467
  name: 'save_metaprompt',
379
468
  description: 'Persist a metaprompt to the local Sage workspace and optionally append to agents.md',
@@ -427,13 +516,13 @@ Next steps:
427
516
  },
428
517
  {
429
518
  name: 'list_subdaos',
430
- description: 'List all available SubDAOs in the Sage Protocol',
519
+ description: 'List all available DAOs in the Sage Protocol',
431
520
  inputSchema: {
432
521
  type: 'object',
433
522
  properties: {
434
523
  limit: {
435
524
  type: 'number',
436
- description: 'Maximum number of SubDAOs to return (default: 20)'
525
+ description: 'Maximum number of DAOs to return (default: 20)'
437
526
  }
438
527
  },
439
528
  required: []
@@ -455,10 +544,10 @@ Next steps:
455
544
  },
456
545
  {
457
546
  name: 'refresh_library_bindings',
458
- description: 'Refresh cached per-library registry mappings (optional: target a specific SubDAO)',
547
+ description: 'Refresh cached per-library registry mappings (optional: target a specific DAO)',
459
548
  inputSchema: {
460
549
  type: 'object',
461
- properties: { subdao: { type: 'string', description: 'Optional SubDAO address (0x...)' } },
550
+ properties: { subdao: { type: 'string', description: 'Optional DAO address (0x...)' } },
462
551
  required: []
463
552
  }
464
553
  },
@@ -482,10 +571,10 @@ Next steps:
482
571
  },
483
572
  {
484
573
  name: 'list_subdao_libraries',
485
- description: 'List per-library PromptRegistry mappings for a SubDAO',
574
+ description: 'List per-library PromptRegistry mappings for a DAO',
486
575
  inputSchema: {
487
576
  type: 'object',
488
- properties: { subdao: { type: 'string', description: 'SubDAO address (0x...)' } },
577
+ properties: { subdao: { type: 'string', description: 'DAO address (0x...)' } },
489
578
  required: ['subdao']
490
579
  }
491
580
  },
@@ -513,13 +602,13 @@ Next steps:
513
602
  },
514
603
  {
515
604
  name: 'propose_manifest',
516
- description: `Generate the proposal payload and CLI commands for a SubDAO update.
605
+ description: `Generate the proposal payload and CLI commands for a DAO governance update.
517
606
  Note: This tool does NOT sign transactions. It generates the hex data and CLI commands for you to execute in your terminal.`,
518
607
  inputSchema: {
519
608
  type: 'object',
520
609
  properties: {
521
610
  cid: { type: 'string', description: 'IPFS CID of the manifest' },
522
- subdao: { type: 'string', description: 'SubDAO address' },
611
+ subdao: { type: 'string', description: 'DAO address' },
523
612
  description: { type: 'string', description: 'Proposal description' },
524
613
  promptCount: { type: 'number', description: 'Number of prompts (optional override)' },
525
614
  manifest: { type: 'object', description: 'Optional manifest object for context' }
@@ -563,11 +652,11 @@ Note: This tool does NOT sign transactions. It generates the hex data and CLI co
563
652
  },
564
653
  {
565
654
  name: 'list_proposals',
566
- description: 'List active proposals for a SubDAO',
655
+ description: 'List active governance proposals for a DAO',
567
656
  inputSchema: {
568
657
  type: 'object',
569
658
  properties: {
570
- subdao: { type: 'string', description: 'SubDAO address' },
659
+ subdao: { type: 'string', description: 'DAO address' },
571
660
  state: { type: 'string', description: 'Filter by state (Active, Executed, etc)' },
572
661
  limit: { type: 'number', description: 'Max items (default 10)' }
573
662
  },
@@ -586,7 +675,7 @@ Note: This tool does NOT sign transactions. It prepares everything so you can ex
586
675
  type: 'object',
587
676
  properties: {
588
677
  manifest: { type: 'object', description: 'Manifest JSON' },
589
- subdao: { type: 'string', description: 'Optional SubDAO address for hints' },
678
+ subdao: { type: 'string', description: 'Optional DAO address for hints' },
590
679
  description: { type: 'string', description: 'Optional description override' },
591
680
  dry_run: { type: 'boolean', description: 'Validate and build proposal payload without uploading to IPFS (no network calls)' }
592
681
  },
@@ -737,7 +826,7 @@ Note: This tool does NOT sign transactions. It prepares everything so you can ex
737
826
  },
738
827
  {
739
828
  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.',
829
+ description: 'Suggest DAOs that might be a good fit for publishing a given local library, and provide CLI workflows for creating your own DAO and pushing the library.',
741
830
  inputSchema: {
742
831
  type: 'object',
743
832
  properties: {
@@ -747,7 +836,7 @@ Note: This tool does NOT sign transactions. It prepares everything so you can ex
747
836
  },
748
837
  limit: {
749
838
  type: 'number',
750
- description: 'Max SubDAOs to return (default 5)',
839
+ description: 'Max DAOs to return (default 5)',
751
840
  default: 5
752
841
  },
753
842
  mode_filter: {
@@ -772,36 +861,15 @@ Note: This tool does NOT sign transactions. It prepares everything so you can ex
772
861
  },
773
862
  target: {
774
863
  type: 'string',
775
- description: 'Target SubDAO address or "auto" to pick a likely candidate',
864
+ description: 'Target DAO address or "auto" to pick a likely candidate',
776
865
  default: 'auto'
777
866
  }
778
867
  },
779
868
  required: ['library']
780
869
  }
781
870
  },
782
- {
783
- name: 'list_workspace_skills',
784
- description: 'List local workspace skills from prompts/ (e.g. prompts/skills/*.md)',
785
- inputSchema: {
786
- type: 'object',
787
- properties: {},
788
- required: []
789
- }
790
- },
791
- {
792
- name: 'get_workspace_skill',
793
- description: 'Load a workspace skill by key and return its content',
794
- inputSchema: {
795
- type: 'object',
796
- properties: {
797
- key: {
798
- type: 'string',
799
- description: 'Skill key relative to prompts dir (e.g. skills/backend-dev-guidelines)'
800
- }
801
- },
802
- required: ['key']
803
- }
804
- }
871
+ // NOTE: list_workspace_skills and get_workspace_skill have been removed.
872
+ // Use list_prompts({ kind: 'skill' }) and get_prompt({ key }) instead.
805
873
  ];
806
874
 
807
875
  // Structured logger to stderr (stdout must remain JSON-RPC only)
@@ -835,6 +903,9 @@ Note: This tool does NOT sign transactions. It prepares everything so you can ex
835
903
  // Local library helper for unified search
836
904
  this.libraryManager = new LibraryManager();
837
905
 
906
+ // Project context service for get_project_context tool
907
+ this.projectContextService = new ProjectContextService();
908
+
838
909
  // Simple rate limiter (token bucket per tool)
839
910
  this.rateConfig = { capacity: 20, refillPerSec: 0.33 }; // ~20/min
840
911
  this.rateLimiter = createRateLimiter(this.rateConfig);
@@ -1013,6 +1084,9 @@ Note: This tool does NOT sign transactions. It prepares everything so you can ex
1013
1084
  'tool:get_prompts_from_manifest': (params) => this.getPromptsFromManifest(params),
1014
1085
  'tool:list_metaprompts': () => this.listMetaprompts(),
1015
1086
  'tool:start_metaprompt_interview': (params) => this.startMetapromptInterview(params),
1087
+ 'tool:list_persona_templates': () => this.listPersonaTemplates(),
1088
+ 'tool:run_persona_interview': (params) => this.runPersonaInterview(params),
1089
+ 'tool:persona_interview_step': (params) => this.personaInterviewStep(params),
1016
1090
  'tool:save_metaprompt': (params) => this.saveMetaprompt(params),
1017
1091
  'tool:get_metaprompt': (params) => this.getMetaprompt(params),
1018
1092
  'tool:generate_metaprompt_link': (params) => this.generateMetapromptLink(params),
@@ -1025,8 +1099,6 @@ Note: This tool does NOT sign transactions. It prepares everything so you can ex
1025
1099
  'tool:list_proposals': (params) => this.listProposals(params),
1026
1100
  'tool:publish_manifest_flow': (params) => this.publishManifestFlow(params),
1027
1101
  'tool:refresh_library_bindings': (params) => this.refreshLibraryBindings(params),
1028
- 'tool:list_workspace_skills': (params) => this.listWorkspaceSkills(params),
1029
- 'tool:get_workspace_skill': (params) => this.getWorkspaceSkill(params),
1030
1102
  'tool:update_library_metadata': (params) => this.updateLibraryMetadata(params),
1031
1103
  'tool:bulk_update_prompts': (params) => this.bulkUpdatePrompts(params),
1032
1104
  'tool:list_templates': (params) => this.listTemplates(params),
@@ -1035,6 +1107,8 @@ Note: This tool does NOT sign transactions. It prepares everything so you can ex
1035
1107
  'tool:analyze_dependencies': (params) => this.analyzeDependencies(params),
1036
1108
  'tool:suggest_subdaos_for_library': (params) => this.suggestSubdaosForLibrary(params),
1037
1109
  'tool:generate_publishing_commands': (params) => this.generatePublishingCommands(params),
1110
+ 'tool:suggest_sage_tools': (params) => this.suggestSageTools(params),
1111
+ 'tool:get_project_context': (params) => this.getProjectContext(params),
1038
1112
  });
1039
1113
 
1040
1114
  const toolHandlers = {
@@ -1202,93 +1276,9 @@ Note: This tool does NOT sign transactions. It prepares everything so you can ex
1202
1276
  }
1203
1277
 
1204
1278
  // ───────────────────────── Workspace skills (repo-tied skills) ─────────────────────────
1205
-
1206
- /**
1207
- * List skills defined in the current project's prompt workspace.
1208
- * Convention:
1209
- * - Workspace file: .sage/workspace.json (optional)
1210
- * - Prompts dir: workspace.promptsDir or "prompts"
1211
- * - Skill files live under promptsDir/skills/*.md
1212
- */
1213
- listWorkspaceSkills() {
1214
- try {
1215
- const { readWorkspace, DEFAULT_DIR } = require('./services/prompts/workspace');
1216
- const { findWorkspaceSkills } = require('./services/skills/discovery');
1217
- const ws = readWorkspace() || {};
1218
- const promptsDir = ws.promptsDir || DEFAULT_DIR || 'prompts';
1219
- const results = findWorkspaceSkills({ promptsDir });
1220
- if (!results.length) {
1221
- return {
1222
- content: [
1223
- {
1224
- type: 'text',
1225
- text: 'No skills found. Create prompts/skills/<name>.md or prompts/skills/<name>/SKILL.md to define skills for this repo.',
1226
- },
1227
- { type: 'text', text: '```json\n' + JSON.stringify({ skills: [] }, null, 2) + '\n```' },
1228
- ],
1229
- };
1230
- }
1231
- const textLines = results
1232
- .map(
1233
- (s, idx) =>
1234
- `${idx + 1}. **${s.name}** (${s.key})\n 📁 ${path.relative(process.cwd(), s.path)}${s.summary ? `\n 📝 ${s.summary}` : ''
1235
- }${s.tags && s.tags.length ? `\n 🔖 ${s.tags.join(', ')}` : ''}`,
1236
- )
1237
- .join('\n\n');
1238
- return {
1239
- content: [
1240
- { type: 'text', text: `Workspace skills (${results.length})\n\n${textLines}` },
1241
- { type: 'text', text: '```json\n' + JSON.stringify({ skills: results }, null, 2) + '\n```' },
1242
- ],
1243
- };
1244
- } catch (error) {
1245
- return {
1246
- content: [{ type: 'text', text: `Error listing workspace skills: ${error.message}` }],
1247
- };
1248
- }
1249
- }
1250
-
1251
- /**
1252
- * Load a workspace skill by key and return its content.
1253
- * Key is relative to prompts dir, e.g. "skills/backend-dev-guidelines".
1254
- */
1255
- getWorkspaceSkill({ key }) {
1256
- try {
1257
- if (!key || !String(key).trim()) {
1258
- return { content: [{ type: 'text', text: 'get_workspace_skill: key is required' }] };
1259
- }
1260
- const { readWorkspace, DEFAULT_DIR } = require('./services/prompts/workspace');
1261
- const { resolveSkillFileByKey } = require('./services/skills/discovery');
1262
- const ws = readWorkspace() || {};
1263
- const promptsDir = ws.promptsDir || DEFAULT_DIR || 'prompts';
1264
- const safeKey = String(key).trim().replace(/^\/+/, '').replace(/\.md$/i, '');
1265
- const resolved = resolveSkillFileByKey({ promptsDir, key: safeKey });
1266
- if (!resolved || !fs.existsSync(resolved.path)) {
1267
- const expectedFlat = path.join(process.cwd(), promptsDir, `${safeKey}.md`);
1268
- const expectedDir = path.join(process.cwd(), promptsDir, safeKey, 'SKILL.md');
1269
- return {
1270
- content: [
1271
- {
1272
- type: 'text',
1273
- text: `Workspace skill not found for key '${safeKey}'. Expected at ${path.relative(process.cwd(), expectedFlat)} or ${path.relative(process.cwd(), expectedDir)}`,
1274
- },
1275
- ],
1276
- };
1277
- }
1278
- const body = fs.readFileSync(resolved.path, 'utf8');
1279
- return {
1280
- content: [
1281
- {
1282
- type: 'text',
1283
- text: `Loaded workspace skill '${safeKey}' from ${path.relative(process.cwd(), resolved.path)}.\n\n${body}`,
1284
- },
1285
- { type: 'text', text: '```json\n' + JSON.stringify({ key: safeKey, path: resolved.path, baseDir: resolved.baseDir, body }, null, 2) + '\n```' },
1286
- ],
1287
- };
1288
- } catch (error) {
1289
- return { content: [{ type: 'text', text: `Error loading workspace skill: ${error.message}` }] };
1290
- }
1291
- }
1279
+ // NOTE: listWorkspaceSkills and getWorkspaceSkill have been removed.
1280
+ // Use list_prompts({ kind: 'skill' }) and get_prompt({ key }) instead.
1281
+ // ─────────────────────────────────────────────────────────────────────────────────────────
1292
1282
 
1293
1283
  async getPromptByName(params = {}) {
1294
1284
  const name = String(params.name || '').trim();
@@ -1381,38 +1371,87 @@ Note: This tool does NOT sign transactions. It prepares everything so you can ex
1381
1371
  return this.searchPromptsUnifiedHandler(options);
1382
1372
  }
1383
1373
 
1384
- async listPrompts({ source = 'local', library = '', limit = 20 } = {}) {
1374
+ async listPrompts({ source = 'local', library = '', limit = 20, kind, publishable } = {}) {
1385
1375
  try {
1386
- const results = await this.searchPromptsUnifiedHandler({
1387
- query: '',
1388
- source,
1389
- includeContent: false,
1390
- page: 1,
1391
- pageSize: limit
1392
- });
1376
+ // If source is 'workspace' or we want to include workspace artifacts, use ArtifactManager
1377
+ const includeWorkspace = source === 'local' || source === 'workspace' || source === 'all';
1378
+
1379
+ let workspaceResults = [];
1380
+ if (includeWorkspace) {
1381
+ const ArtifactManager = require('./services/artifact-manager');
1382
+ const artifactManager = new ArtifactManager();
1383
+ const filter = {};
1384
+ if (kind) filter.kind = kind;
1385
+ if (publishable !== undefined) filter.publishable = publishable;
1386
+
1387
+ const artifacts = await artifactManager.listArtifacts(filter);
1388
+ workspaceResults = artifacts.map(a => ({
1389
+ resultType: 'prompt',
1390
+ origin: 'workspace',
1391
+ source: 'Workspace',
1392
+ key: a.key,
1393
+ name: a.meta?.title || a.key,
1394
+ description: a.meta?.summary || '',
1395
+ tags: a.meta?.tags || [],
1396
+ content: a.body,
1397
+ kind: a.kind,
1398
+ publishable: a.publishable,
1399
+ publishing: a.publishing,
1400
+ targets: a.targets,
1401
+ filePath: a.filePath,
1402
+ }));
1403
+ }
1393
1404
 
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;
1405
+ // Also get library prompts if not workspace-only
1406
+ let libraryResults = [];
1407
+ if (source !== 'workspace') {
1408
+ const results = await this.searchPromptsUnifiedHandler({
1409
+ query: '',
1410
+ source: source === 'workspace' ? 'local' : source,
1411
+ includeContent: false,
1412
+ page: 1,
1413
+ pageSize: limit
1414
+ });
1404
1415
 
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
- };
1416
+ if (results?.content?.[1]?.text) {
1417
+ const jsonMatch = results.content[1].text.match(/```json\n([\s\S]+)\n```/);
1418
+ if (jsonMatch) {
1419
+ const data = JSON.parse(jsonMatch[1]);
1420
+ libraryResults = (data.results || []).map(r => ({
1421
+ ...r,
1422
+ kind: r.kind || 'prompt',
1423
+ publishable: true, // library prompts are published
1424
+ publishing: { status: 'published' }
1425
+ }));
1426
+ }
1412
1427
  }
1413
1428
  }
1414
1429
 
1415
- return results;
1430
+ // Merge and filter
1431
+ let allResults = [...workspaceResults, ...libraryResults];
1432
+
1433
+ // Filter by library if specified
1434
+ if (library) {
1435
+ const libraryLower = library.toLowerCase();
1436
+ allResults = allResults.filter(r =>
1437
+ r.library?.name?.toLowerCase().includes(libraryLower) ||
1438
+ r.library?.cid?.toLowerCase().includes(libraryLower) ||
1439
+ r.origin === 'workspace' // workspace prompts don't have library
1440
+ );
1441
+ }
1442
+
1443
+ // Apply limit
1444
+ allResults = allResults.slice(0, limit);
1445
+
1446
+ const data = { results: allResults, total: allResults.length, page: 1, pageSize: limit };
1447
+ const formatted = this.formatUnifiedResults(allResults, { total: data.total, page: 1, pageSize: limit });
1448
+
1449
+ return {
1450
+ content: [
1451
+ { type: 'text', text: formatted },
1452
+ { type: 'text', text: '```json\n' + JSON.stringify(data, null, 2) + '\n```' }
1453
+ ]
1454
+ };
1416
1455
  } catch (error) {
1417
1456
  return { content: [{ type: 'text', text: `Error listing prompts: ${error.message}` }] };
1418
1457
  }
@@ -1424,6 +1463,41 @@ Note: This tool does NOT sign transactions. It prepares everything so you can ex
1424
1463
  return { content: [{ type: 'text', text: 'Error: key parameter is required' }] };
1425
1464
  }
1426
1465
 
1466
+ // First try workspace artifacts (ArtifactManager)
1467
+ const ArtifactManager = require('./services/artifact-manager');
1468
+ const artifactManager = new ArtifactManager();
1469
+ const artifact = await artifactManager.getArtifact(key);
1470
+
1471
+ if (artifact) {
1472
+ const prompt = {
1473
+ resultType: 'prompt',
1474
+ origin: 'workspace',
1475
+ source: 'Workspace',
1476
+ key: artifact.key,
1477
+ name: artifact.meta?.title || artifact.key,
1478
+ description: artifact.meta?.summary || '',
1479
+ tags: artifact.meta?.tags || [],
1480
+ content: artifact.body,
1481
+ kind: artifact.kind,
1482
+ publishable: artifact.publishable,
1483
+ publishing: artifact.publishing,
1484
+ targets: artifact.targets,
1485
+ filePath: artifact.filePath,
1486
+ };
1487
+
1488
+ const kindBadge = prompt.kind !== 'prompt' ? ` [${prompt.kind}]` : '';
1489
+ const publishBadge = prompt.publishable ? '' : ' (local only)';
1490
+ const text = `**${prompt.name}**${kindBadge}${publishBadge}\n\n🔑 Key: ${prompt.key}\n📂 Source: Workspace\n📄 ${prompt.description || 'No description'}\n🏷️ Tags: ${prompt.tags?.join(', ') || 'None'}\n📌 Kind: ${prompt.kind}\n📤 Publishable: ${prompt.publishable}\n📊 Status: ${prompt.publishing?.status || 'unknown'}\n\n**Content:**\n\`\`\`\n${prompt.content || '(No content)'}\n\`\`\``;
1491
+
1492
+ return {
1493
+ content: [
1494
+ { type: 'text', text },
1495
+ { type: 'text', text: '```json\n' + JSON.stringify(prompt, null, 2) + '\n```' }
1496
+ ]
1497
+ };
1498
+ }
1499
+
1500
+ // Fall back to library search
1427
1501
  const results = await this.searchPromptsUnifiedHandler({
1428
1502
  query: key,
1429
1503
  source: 'local',
@@ -1452,7 +1526,12 @@ Note: This tool does NOT sign transactions. It prepares everything so you can ex
1452
1526
  return { content: [{ type: 'text', text: `No prompt found with key "${key}"` }] };
1453
1527
  }
1454
1528
 
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\`\`\``;
1529
+ // Add kind/publishable for library prompts
1530
+ prompt.kind = prompt.kind || 'prompt';
1531
+ prompt.publishable = true;
1532
+ prompt.publishing = { status: 'published' };
1533
+
1534
+ 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📌 Kind: ${prompt.kind}\n\n**Content:**\n\`\`\`\n${prompt.content || '(No content)'}\n\`\`\``;
1456
1535
 
1457
1536
  return {
1458
1537
  content: [
@@ -1585,6 +1664,61 @@ Note: This tool does NOT sign transactions. It prepares everything so you can ex
1585
1664
  }
1586
1665
  }
1587
1666
 
1667
+ async getProjectContext(params) {
1668
+ try {
1669
+ const ctx = await this.projectContextService.getProjectContext(params || {});
1670
+ const lines = [];
1671
+ lines.push('✅ Project context');
1672
+ lines.push(`Project root: ${ctx.projectRoot || 'not found'}`);
1673
+ lines.push(`Prompts dir: ${ctx.promptsDir || 'prompts'}`);
1674
+ if (ctx.subdao) {
1675
+ lines.push(`SubDAO: ${ctx.subdao}`);
1676
+ }
1677
+ if (ctx.registry) {
1678
+ lines.push(`Library registry: ${ctx.registry}`);
1679
+ }
1680
+ if (ctx.network) {
1681
+ lines.push(`Network: ${ctx.network}`);
1682
+ }
1683
+ if (ctx.rpcUrl) {
1684
+ lines.push(`RPC URL: ${ctx.rpcUrl}`);
1685
+ }
1686
+ if (ctx.workspace) {
1687
+ lines.push(
1688
+ `Workspace artifacts: prompts=${ctx.workspace.promptCount}, skills=${ctx.workspace.skillCount}, snippets=${ctx.workspace.snippetCount}, total=${ctx.workspace.total}`
1689
+ );
1690
+ }
1691
+ if (ctx.agentSurfaces) {
1692
+ const surfaces = [];
1693
+ if (ctx.agentSurfaces.cursorRulesDir) {
1694
+ surfaces.push(`cursor rules at ${ctx.agentSurfaces.cursorRulesDir}`);
1695
+ }
1696
+ if (ctx.agentSurfaces.claudeManifest) {
1697
+ surfaces.push(`CLAUDE manifest at ${ctx.agentSurfaces.claudeManifest}`);
1698
+ }
1699
+ if (ctx.agentSurfaces.copilotPromptsDir) {
1700
+ surfaces.push(`Copilot prompts at ${ctx.agentSurfaces.copilotPromptsDir}`);
1701
+ }
1702
+ if (Array.isArray(ctx.agentSurfaces.agentsFiles) && ctx.agentSurfaces.agentsFiles.length) {
1703
+ surfaces.push(`AGENTS files: ${ctx.agentSurfaces.agentsFiles.join(', ')}`);
1704
+ }
1705
+ if (surfaces.length) {
1706
+ lines.push('Agent surfaces:');
1707
+ surfaces.forEach((s) => lines.push(`- ${s}`));
1708
+ }
1709
+ }
1710
+
1711
+ return {
1712
+ content: [
1713
+ { type: 'text', text: lines.join('\n') },
1714
+ { type: 'text', text: '```json\n' + JSON.stringify(ctx, null, 2) + '\n```' },
1715
+ ],
1716
+ };
1717
+ } catch (error) {
1718
+ return { content: [{ type: 'text', text: `Error retrieving project context: ${error.message}` }] };
1719
+ }
1720
+ }
1721
+
1588
1722
  async listTemplates(params = {}) {
1589
1723
  try {
1590
1724
  const result = this.templateManager.listTemplates(params || {});
@@ -2453,6 +2587,274 @@ Use \`help(topic="create")\` for more details.
2453
2587
  return this.listLibrariesHandler(options);
2454
2588
  }
2455
2589
 
2590
+ // ───────────────────────── Tool suggestion meta-router ─────────────────────────
2591
+
2592
+ tokenizeForMatching(text) {
2593
+ if (!text) return [];
2594
+ const lower = String(text).toLowerCase();
2595
+ const tokens = lower.split(/[^a-z0-9]+/g).filter(Boolean);
2596
+ // Small stopword list; just enough to ignore generic phrasing.
2597
+ const stop = new Set([
2598
+ 'the', 'a', 'an', 'to', 'for', 'and', 'or', 'of', 'in', 'on', 'with', 'this', 'that',
2599
+ 'i', 'me', 'my', 'we', 'our', 'you', 'your',
2600
+ 'want', 'need', 'help', 'can', 'could', 'would', 'should',
2601
+ 'how', 'do', 'does', 'did', 'what', 'is', 'are', 'it', 'be',
2602
+ ]);
2603
+ return tokens.filter((t) => !stop.has(t));
2604
+ }
2605
+
2606
+ inferStage(goal, explicitStage) {
2607
+ if (explicitStage) return explicitStage;
2608
+ if (!goal) return null;
2609
+
2610
+ const patterns = {
2611
+ prompt_workspace: /\b(prompt|skill|workspace|create|edit|draft|write|local)\b/gi,
2612
+ persona: /\b(persona|metaprompt|interview|agent|character|assistant|system prompt)\b/gi,
2613
+ libraries: /\b(library|manifest|template|collection|import|export)\b/gi,
2614
+ governance: /\b(publish|proposal|vote|subdao|dao|govern|approve|execute)\b/gi,
2615
+ treasury: /\b(treasury|bond|boost|withdraw|sxxx|token|stake)\b/gi,
2616
+ discovery: /\b(search|find|list|browse|explore|trending|discover)\b/gi,
2617
+ };
2618
+
2619
+ const scores = {};
2620
+ for (const [stage, pattern] of Object.entries(patterns)) {
2621
+ const matches = goal.match(pattern) || [];
2622
+ if (matches.length > 0) scores[stage] = matches.length;
2623
+ }
2624
+
2625
+ const entries = Object.entries(scores);
2626
+ if (!entries.length) return null;
2627
+ entries.sort((a, b) => b[1] - a[1]);
2628
+ return entries[0][0];
2629
+ }
2630
+
2631
+ detectWorkflow(goal) {
2632
+ if (!goal) return null;
2633
+ const lower = goal.toLowerCase();
2634
+
2635
+ const workflows = [
2636
+ {
2637
+ // Create & publish a prompt/library
2638
+ pattern: /create.*prompt.*publish|publish.*prompt|create.*library.*publish|publish.*library/i,
2639
+ tools: ['quick_create_prompt', 'improve_prompt', 'publish_manifest_flow'],
2640
+ },
2641
+ {
2642
+ // Design & (optionally) publish a persona
2643
+ pattern: /create.*persona.*publish|design.*persona.*publish|persona.*publish/i,
2644
+ tools: ['list_persona_templates', 'persona_interview_step', 'save_metaprompt', 'publish_manifest_flow'],
2645
+ },
2646
+ {
2647
+ // Design a persona without explicit publishing intent
2648
+ pattern: /design.*persona|create.*persona|persona.*workflow/i,
2649
+ tools: ['list_persona_templates', 'persona_interview_step', 'save_metaprompt'],
2650
+ },
2651
+ {
2652
+ // Find & iterate on prompts
2653
+ pattern: /find.*prompt.*iterate|iterate.*prompt|improve.*prompt/i,
2654
+ tools: ['search_prompts', 'get_prompt', 'quick_iterate_prompt'],
2655
+ },
2656
+ {
2657
+ // Publish to a DAO/SubDAO
2658
+ pattern: /publish.*dao|dao.*publish|publish.*subdao|subdao.*publish/i,
2659
+ tools: ['suggest_subdaos_for_library', 'generate_publishing_commands', 'publish_manifest_flow'],
2660
+ },
2661
+ ];
2662
+
2663
+ for (const wf of workflows) {
2664
+ if (wf.pattern.test(lower)) {
2665
+ return wf.tools;
2666
+ }
2667
+ }
2668
+ return null;
2669
+ }
2670
+
2671
+ scoreToolForGoal(toolMeta, goalTokens, stage) {
2672
+ // BM25-inspired scoring with simple term frequency and length normalization.
2673
+ const baseTerms = [
2674
+ ...(toolMeta.keywords || []),
2675
+ ...this.tokenizeForMatching(toolMeta.description || ''),
2676
+ ...this.tokenizeForMatching(toolMeta.whenToUse || ''),
2677
+ ];
2678
+
2679
+ const docLen = baseTerms.length || 1;
2680
+ const avgLen = 15;
2681
+ const k1 = 1.2;
2682
+ const b = 0.75;
2683
+
2684
+ let score = 0;
2685
+
2686
+ for (const token of goalTokens) {
2687
+ const tf = baseTerms.filter((t) => t === token).length;
2688
+ if (tf > 0) {
2689
+ const tfScore = (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * (docLen / avgLen)));
2690
+ score += tfScore;
2691
+ }
2692
+ }
2693
+
2694
+ // Negative keyword penalty.
2695
+ if (toolMeta.negativeKeywords && toolMeta.negativeKeywords.length) {
2696
+ let penalties = 0;
2697
+ for (const neg of toolMeta.negativeKeywords) {
2698
+ if (goalTokens.includes(neg)) penalties += 1;
2699
+ }
2700
+ if (penalties > 0) {
2701
+ score *= (0.5 ** penalties);
2702
+ }
2703
+ }
2704
+
2705
+ // Stage/category boost.
2706
+ if (stage && toolMeta.category === stage) {
2707
+ score *= 1.5;
2708
+ }
2709
+
2710
+ // Base weight.
2711
+ const weight = typeof toolMeta.weight === 'number' ? toolMeta.weight : 1;
2712
+ score *= weight;
2713
+
2714
+ // Normalize to 0–1 range.
2715
+ return Math.min(score / 5, 1);
2716
+ }
2717
+
2718
+ orderSuggestedTools(scored, limit) {
2719
+ if (!Array.isArray(scored) || !scored.length) return [];
2720
+ const sorted = [...scored].sort((a, b) => b.confidence - a.confidence).slice(0, limit);
2721
+ if (sorted.length <= 3) {
2722
+ return sorted.map((item, idx) => ({ ...item, priority: idx + 1 }));
2723
+ }
2724
+
2725
+ // Simple U-shaped ordering: higher-ranked items gravitate to the ends.
2726
+ const result = [];
2727
+ for (let i = 0; i < sorted.length; i += 1) {
2728
+ if (i % 2 === 0) {
2729
+ // Even indices append to the end
2730
+ result.push(sorted[i]);
2731
+ } else {
2732
+ // Odd indices unshift to the front
2733
+ result.unshift(sorted[i]);
2734
+ }
2735
+ }
2736
+
2737
+ return result.map((item, idx) => ({ ...item, priority: idx + 1 }));
2738
+ }
2739
+
2740
+ async suggestSageTools(params) {
2741
+ // Arguments are already validated by createToolArgsValidator.
2742
+ const { goal, stage, context, limit = 5, includeAlternatives = false } = params || {};
2743
+
2744
+ const effectiveStage = this.inferStage(goal, stage);
2745
+ const goalTokens = this.tokenizeForMatching(goal);
2746
+
2747
+ // Choose candidate tools from registry.
2748
+ let candidates = [];
2749
+ if (effectiveStage === 'discovery') {
2750
+ // Discovery is intentionally cross-cutting; surface the most browse/search oriented tools.
2751
+ const discoveryTools = ['search_prompts', 'list_prompts', 'trending_prompts', 'list_persona_templates', 'list_libraries'];
2752
+ candidates = discoveryTools
2753
+ .map((name) => getToolMeta(name))
2754
+ .filter(Boolean);
2755
+ } else if (effectiveStage) {
2756
+ candidates = getToolsForCategory(effectiveStage);
2757
+ } else {
2758
+ // If no clear stage, fall back to a small set of discovery-friendly tools.
2759
+ const discoveryTools = ['search_prompts', 'list_prompts', 'trending_prompts', 'list_persona_templates', 'list_libraries'];
2760
+ candidates = discoveryTools
2761
+ .map((name) => getToolMeta(name))
2762
+ .filter(Boolean);
2763
+ }
2764
+
2765
+ // Cross-category fallback: if we have a clear stage, allow strongly-matching tools
2766
+ // from other categories to join the candidate set.
2767
+ if (effectiveStage) {
2768
+ const allTools = Object.entries(TOOL_REGISTRY).map(([name, meta]) => ({ name, ...meta }));
2769
+ const existingNames = new Set(candidates.map((c) => c.name));
2770
+ const crossCategory = allTools
2771
+ .filter((t) => t.category !== effectiveStage && !existingNames.has(t.name))
2772
+ .map((t) => ({
2773
+ ...t,
2774
+ confidence: this.scoreToolForGoal(t, goalTokens, null),
2775
+ }))
2776
+ .filter((t) => t.confidence > 0.6);
2777
+
2778
+ if (crossCategory.length) {
2779
+ candidates = candidates.concat(crossCategory);
2780
+ }
2781
+ }
2782
+
2783
+ // Apply simple context-based filtering/hints.
2784
+ if (context && context.hasWorkspace === false) {
2785
+ // If there is no workspace, de-prioritize workspace-only tools.
2786
+ candidates = candidates.map((c) => {
2787
+ if (c.category === 'prompt_workspace') {
2788
+ return { ...c, weight: (c.weight || 1) * 0.7 };
2789
+ }
2790
+ return c;
2791
+ });
2792
+ }
2793
+
2794
+ // Score tools.
2795
+ const scored = candidates.map((meta) => ({
2796
+ ...meta,
2797
+ confidence: this.scoreToolForGoal(meta, goalTokens, effectiveStage),
2798
+ })).filter((item) => item.confidence >= 0.15);
2799
+
2800
+ if (!scored.length) {
2801
+ const fallback = {
2802
+ recommendations: [],
2803
+ alternatives: [],
2804
+ inferredStage: effectiveStage || null,
2805
+ suggestedWorkflow: null,
2806
+ };
2807
+ return {
2808
+ content: [{
2809
+ type: 'text',
2810
+ text: JSON.stringify(fallback, null, 2),
2811
+ }],
2812
+ };
2813
+ }
2814
+
2815
+ const ordered = this.orderSuggestedTools(scored, limit);
2816
+ const workflow = this.detectWorkflow(goal);
2817
+
2818
+ const recommendations = ordered.map((item) => ({
2819
+ toolName: item.name,
2820
+ category: item.category,
2821
+ confidence: item.confidence,
2822
+ priority: item.priority,
2823
+ rationale: item.whenToUse,
2824
+ requiredParams: item.requiredParams || [],
2825
+ }));
2826
+
2827
+ let alternatives = [];
2828
+ if (includeAlternatives) {
2829
+ const remaining = scored
2830
+ .filter((s) => !ordered.find((r) => r.name === s.name))
2831
+ .sort((a, b) => b.confidence - a.confidence)
2832
+ .slice(0, limit);
2833
+ alternatives = remaining.map((item, idx) => ({
2834
+ toolName: item.name,
2835
+ category: item.category,
2836
+ confidence: item.confidence,
2837
+ priority: idx + 1,
2838
+ rationale: item.whenToUse,
2839
+ requiredParams: item.requiredParams || [],
2840
+ }));
2841
+ }
2842
+
2843
+ const result = {
2844
+ recommendations,
2845
+ alternatives,
2846
+ inferredStage: effectiveStage || null,
2847
+ suggestedWorkflow: workflow || null,
2848
+ };
2849
+
2850
+ return {
2851
+ content: [{
2852
+ type: 'text',
2853
+ text: JSON.stringify(result, null, 2),
2854
+ }],
2855
+ };
2856
+ }
2857
+
2456
2858
  listMetaprompts() {
2457
2859
  const rows = metapromptDesigner.listMetaprompts();
2458
2860
  if (!rows.length) {
@@ -2848,6 +3250,207 @@ Use \`help(topic="create")\` for more details.
2848
3250
  }
2849
3251
  }
2850
3252
 
3253
+ // ───────────────────────────── Metaprompt Engine Tools ─────────────────────────────
3254
+
3255
+ async listPersonaTemplates() {
3256
+ try {
3257
+ const templates = require('./services/metaprompt/templates.json');
3258
+ const list = Object.values(templates).map(t => ({
3259
+ key: t.key,
3260
+ name: t.name,
3261
+ description: t.description,
3262
+ recommended_slots: t.recommended_slots?.map(s => s.label) || []
3263
+ }));
3264
+
3265
+ return {
3266
+ content: [
3267
+ {
3268
+ type: 'text',
3269
+ text: JSON.stringify(list, null, 2)
3270
+ }
3271
+ ]
3272
+ };
3273
+ } catch (error) {
3274
+ return {
3275
+ isError: true,
3276
+ content: [{ type: 'text', text: `Failed to list templates: ${error.message}` }]
3277
+ };
3278
+ }
3279
+ }
3280
+
3281
+ async runPersonaInterview(params) {
3282
+ const { template, answers, save, saveKey } = params;
3283
+ try {
3284
+ const config = require('./config');
3285
+ const PromptBuilder = require('./services/metaprompt/prompt-builder');
3286
+ const MetapromptPersistence = require('./services/metaprompt/persistence');
3287
+ const templates = require('./services/metaprompt/templates.json');
3288
+
3289
+ // 1. Validate Template
3290
+ const templateDef = templates[template] || templates['custom'];
3291
+
3292
+ // 2. Build Prompt (One-Shot)
3293
+ // Note: run_persona_interview assumes the *agent* has already done the planning/filling.
3294
+ // It bypasses the SlotPlanner and just builds the artifact.
3295
+ const builder = new PromptBuilder();
3296
+ // We need the full slot definitions to build dynamic sections.
3297
+ // Since we skipped the planner, we'll use the template's recommended slots as the definition.
3298
+ const slots = templateDef.recommended_slots || [];
3299
+
3300
+ const systemPrompt = builder.buildSystemPrompt(template, slots, answers || {});
3301
+
3302
+ let result = {
3303
+ systemPrompt,
3304
+ slug: '',
3305
+ paths: {}
3306
+ };
3307
+
3308
+ // 3. Save (Optional)
3309
+ if (save) {
3310
+ const persistence = new MetapromptPersistence(config);
3311
+ const slug = saveKey || `${template}-${Date.now().toString().slice(-6)}`;
3312
+
3313
+ // Save dummy history so the artifact exists
3314
+ const historyPaths = persistence.saveMetaprompt(slug, {
3315
+ templateKey: template,
3316
+ transcript: [{ role: 'system', content: 'Generated via MCP run_persona_interview (one-shot).' }],
3317
+ answers: answers || {}
3318
+ });
3319
+
3320
+ // Save Skill
3321
+ const skillPath = persistence.saveSkill(slug, systemPrompt);
3322
+
3323
+ result.slug = slug;
3324
+ result.paths = {
3325
+ metaprompt: historyPaths.metaprompt,
3326
+ skill: skillPath
3327
+ };
3328
+ }
3329
+
3330
+ return {
3331
+ content: [
3332
+ {
3333
+ type: 'text',
3334
+ text: JSON.stringify(result, null, 2)
3335
+ }
3336
+ ]
3337
+ };
3338
+
3339
+ } catch (error) {
3340
+ return {
3341
+ isError: true,
3342
+ content: [{ type: 'text', text: `Failed to run persona interview: ${error.message}` }]
3343
+ };
3344
+ }
3345
+ }
3346
+
3347
+ /**
3348
+ * Stateful persona interview that walks through template slots.
3349
+ *
3350
+ * Returns slot metadata (label, description, group) for the host agent to phrase questions.
3351
+ * The agent passes answers back with the stateToken to progress through slots.
3352
+ * When all required slots are filled, returns the generated system prompt.
3353
+ *
3354
+ * @param {object} params
3355
+ * @param {string} params.template - Template key (coding-assistant, governance-helper, etc.)
3356
+ * @param {string} [params.stateToken] - Base64-encoded state from previous call
3357
+ * @param {string} [params.answer] - Answer for the current slot (currentSlotKey from previous response)
3358
+ */
3359
+ async personaInterviewStep(params) {
3360
+ const { template, stateToken, answer } = params;
3361
+ try {
3362
+ const templates = require('./services/metaprompt/templates.json');
3363
+ const PromptBuilder = require('./services/metaprompt/prompt-builder');
3364
+
3365
+ // 1. Restore or initialize state
3366
+ let state;
3367
+ if (stateToken) {
3368
+ try {
3369
+ state = JSON.parse(Buffer.from(stateToken, 'base64').toString('utf8'));
3370
+ } catch {
3371
+ return { isError: true, content: [{ type: 'text', text: 'Invalid stateToken' }] };
3372
+ }
3373
+ } else {
3374
+ // Initialize new interview state
3375
+ const templateKey = template || 'custom';
3376
+ const templateDef = templates[templateKey] || templates.custom;
3377
+ state = {
3378
+ templateKey,
3379
+ slots: templateDef.recommended_slots || [],
3380
+ answers: {},
3381
+ currentSlotKey: null,
3382
+ };
3383
+ }
3384
+
3385
+ // 2. Record answer for the current slot (if provided)
3386
+ if (answer && state.currentSlotKey) {
3387
+ state.answers[state.currentSlotKey] = answer;
3388
+ }
3389
+
3390
+ // 3. Find next unanswered slot (required first, then optional by priority)
3391
+ const unanswered = state.slots.filter(s => !state.answers[s.key]);
3392
+ const requiredUnanswered = unanswered.filter(s => s.required);
3393
+ const nextSlot = requiredUnanswered[0] || unanswered.find(s => s.priority <= 2) || null;
3394
+
3395
+ // 4. Update state with current slot key
3396
+ state.currentSlotKey = nextSlot?.key || null;
3397
+
3398
+ // 5. Serialize state for next call
3399
+ const newStateToken = Buffer.from(JSON.stringify(state)).toString('base64');
3400
+
3401
+ // 6. Check if interview is complete
3402
+ if (!nextSlot) {
3403
+ // All required slots filled - build the system prompt (no LLM needed)
3404
+ const builder = new PromptBuilder();
3405
+ const systemPrompt = builder.buildSystemPrompt(state.templateKey, state.slots, state.answers);
3406
+
3407
+ return {
3408
+ content: [{
3409
+ type: 'text',
3410
+ text: JSON.stringify({
3411
+ done: true,
3412
+ systemPrompt,
3413
+ template: state.templateKey,
3414
+ answers: state.answers,
3415
+ filledSlots: Object.keys(state.answers).length,
3416
+ totalSlots: state.slots.length,
3417
+ stateToken: newStateToken,
3418
+ }, null, 2)
3419
+ }]
3420
+ };
3421
+ }
3422
+
3423
+ // 7. Return next slot info for the host LLM to phrase the question
3424
+ // The host LLM should use slotLabel/slotDescription to ask the user
3425
+ const suggestedQuestion = `Please provide: ${nextSlot.label}. ${nextSlot.description || ''}`.trim();
3426
+
3427
+ return {
3428
+ content: [{
3429
+ type: 'text',
3430
+ text: JSON.stringify({
3431
+ done: false,
3432
+ currentSlotKey: nextSlot.key,
3433
+ slotLabel: nextSlot.label,
3434
+ slotDescription: nextSlot.description || '',
3435
+ slotGroup: nextSlot.group || 'general',
3436
+ slotRequired: nextSlot.required || false,
3437
+ suggestedQuestion,
3438
+ filledSlots: Object.keys(state.answers).length,
3439
+ totalSlots: state.slots.length,
3440
+ remainingRequired: requiredUnanswered.length,
3441
+ stateToken: newStateToken,
3442
+ }, null, 2)
3443
+ }]
3444
+ };
3445
+
3446
+ } catch (error) {
3447
+ return {
3448
+ isError: true,
3449
+ content: [{ type: 'text', text: `Failed to execute interview step: ${error.message}` }]
3450
+ };
3451
+ }
3452
+ }
3453
+
2851
3454
  async getSubDAOList(options = {}) {
2852
3455
  try {
2853
3456
  return await this.subdaoDiscovery.listSubDaos(options);