@sage-protocol/cli 0.3.10 → 0.4.1

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 (36) 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 +234 -89
  5. package/dist/cli/commands/stake-status.js +0 -2
  6. package/dist/cli/config.js +28 -8
  7. package/dist/cli/governance-manager.js +28 -19
  8. package/dist/cli/index.js +32 -8
  9. package/dist/cli/library-manager.js +16 -6
  10. package/dist/cli/mcp-server-stdio.js +549 -0
  11. package/dist/cli/mcp-server.js +4 -30
  12. package/dist/cli/mcp-setup.md +35 -34
  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/doctor/fixers.js +1 -1
  17. package/dist/cli/services/mcp/env-loader.js +2 -0
  18. package/dist/cli/services/mcp/quick-start.js +14 -15
  19. package/dist/cli/services/mcp/sage-tool-registry.js +330 -0
  20. package/dist/cli/services/mcp/tool-args-validator.js +31 -0
  21. package/dist/cli/services/metaprompt/anthropic-client.js +87 -0
  22. package/dist/cli/services/metaprompt/interview-driver.js +161 -0
  23. package/dist/cli/services/metaprompt/model-client.js +49 -0
  24. package/dist/cli/services/metaprompt/openai-client.js +67 -0
  25. package/dist/cli/services/metaprompt/persistence.js +86 -0
  26. package/dist/cli/services/metaprompt/prompt-builder.js +186 -0
  27. package/dist/cli/services/metaprompt/session.js +18 -80
  28. package/dist/cli/services/metaprompt/slot-planner.js +115 -0
  29. package/dist/cli/services/metaprompt/templates.json +130 -0
  30. package/dist/cli/subdao.js +0 -3
  31. package/dist/cli/sxxx-manager.js +0 -1
  32. package/dist/cli/utils/tx-wait.js +0 -3
  33. package/dist/cli/wallet-manager.js +18 -19
  34. package/dist/cli/walletconnect-integration.js +0 -1
  35. package/dist/cli/wizard-manager.js +0 -1
  36. package/package.json +3 -1
@@ -53,6 +53,11 @@ 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');
57
62
  const { createProposalLister } = require('./services/mcp/proposal-lister');
58
63
  const fs = require('fs');
@@ -79,6 +84,41 @@ class SageMCPServer {
79
84
  };
80
85
 
81
86
  this.tools = [
87
+ {
88
+ name: 'suggest_sage_tools',
89
+ description: `Suggest the most relevant Sage MCP tools for a given goal.
90
+ When to use:
91
+ - You have a vague goal and want help choosing the right Sage tools
92
+ - You want a ranked list of 3–5 tools with rationale and required parameters
93
+
94
+ This tool does not execute any actions; it only recommends which tools to call next.`,
95
+ inputSchema: {
96
+ type: 'object',
97
+ properties: {
98
+ goal: { type: 'string', description: 'Free-text description of what you are trying to do' },
99
+ stage: {
100
+ type: 'string',
101
+ enum: ['prompt_workspace', 'persona', 'libraries', 'governance', 'treasury', 'discovery'],
102
+ description: 'Optional hint about which area you are working in',
103
+ },
104
+ context: {
105
+ type: 'object',
106
+ description: 'Optional execution context (workspace, wallet, etc.)',
107
+ properties: {
108
+ hasWorkspace: { type: 'boolean', description: 'Whether a Sage prompt workspace is present' },
109
+ hasWallet: { type: 'boolean', description: 'Whether a wallet is configured/available' },
110
+ currentTask: { type: 'string', description: 'Short description of what you just did' },
111
+ },
112
+ },
113
+ limit: { type: 'number', description: 'Maximum number of primary recommendations (default: 5)' },
114
+ includeAlternatives: {
115
+ type: 'boolean',
116
+ description: 'Whether to include a secondary list of alternative tools (default: false)',
117
+ },
118
+ },
119
+ required: ['goal'],
120
+ },
121
+ },
82
122
  {
83
123
  name: 'quick_create_prompt',
84
124
  description: `Quickly create a new prompt. Handles library creation automatically.
@@ -374,6 +414,42 @@ Next steps:
374
414
  required: []
375
415
  }
376
416
  },
417
+ {
418
+ name: 'list_persona_templates',
419
+ description: 'List available persona templates for the Metaprompt Engine',
420
+ inputSchema: {
421
+ type: 'object',
422
+ properties: {},
423
+ required: []
424
+ }
425
+ },
426
+ {
427
+ name: 'run_persona_interview',
428
+ description: 'Generate a persona system prompt from structured answers (One-Shot, no human loop)',
429
+ inputSchema: {
430
+ type: 'object',
431
+ properties: {
432
+ template: { type: 'string', description: 'Template key (e.g., coding-assistant, governance-helper)' },
433
+ answers: { type: 'object', description: 'Key-value map of slot answers' },
434
+ save: { type: 'boolean', description: 'Whether to save the artifact to disk (default: false)' },
435
+ saveKey: { type: 'string', description: 'Optional filename for saving' }
436
+ },
437
+ required: ['template', 'answers']
438
+ }
439
+ },
440
+ {
441
+ name: 'persona_interview_step',
442
+ description: 'Stateful persona interview. Returns slot metadata for the host agent to phrase questions. Pass answers back with the stateToken to progress through slots.',
443
+ inputSchema: {
444
+ type: 'object',
445
+ properties: {
446
+ template: { type: 'string', description: 'Template key (coding-assistant, governance-helper, research-analyst, custom). Required on first call.' },
447
+ stateToken: { type: 'string', description: 'Base64-encoded state from previous response. Omit on first call.' },
448
+ answer: { type: 'string', description: 'Answer for the slot identified by currentSlotKey in the previous response.' }
449
+ },
450
+ required: []
451
+ }
452
+ },
377
453
  {
378
454
  name: 'save_metaprompt',
379
455
  description: 'Persist a metaprompt to the local Sage workspace and optionally append to agents.md',
@@ -1013,6 +1089,9 @@ Note: This tool does NOT sign transactions. It prepares everything so you can ex
1013
1089
  'tool:get_prompts_from_manifest': (params) => this.getPromptsFromManifest(params),
1014
1090
  'tool:list_metaprompts': () => this.listMetaprompts(),
1015
1091
  'tool:start_metaprompt_interview': (params) => this.startMetapromptInterview(params),
1092
+ 'tool:list_persona_templates': () => this.listPersonaTemplates(),
1093
+ 'tool:run_persona_interview': (params) => this.runPersonaInterview(params),
1094
+ 'tool:persona_interview_step': (params) => this.personaInterviewStep(params),
1016
1095
  'tool:save_metaprompt': (params) => this.saveMetaprompt(params),
1017
1096
  'tool:get_metaprompt': (params) => this.getMetaprompt(params),
1018
1097
  'tool:generate_metaprompt_link': (params) => this.generateMetapromptLink(params),
@@ -1035,6 +1114,7 @@ Note: This tool does NOT sign transactions. It prepares everything so you can ex
1035
1114
  'tool:analyze_dependencies': (params) => this.analyzeDependencies(params),
1036
1115
  'tool:suggest_subdaos_for_library': (params) => this.suggestSubdaosForLibrary(params),
1037
1116
  'tool:generate_publishing_commands': (params) => this.generatePublishingCommands(params),
1117
+ 'tool:suggest_sage_tools': (params) => this.suggestSageTools(params),
1038
1118
  });
1039
1119
 
1040
1120
  const toolHandlers = {
@@ -2453,6 +2533,274 @@ Use \`help(topic="create")\` for more details.
2453
2533
  return this.listLibrariesHandler(options);
2454
2534
  }
2455
2535
 
2536
+ // ───────────────────────── Tool suggestion meta-router ─────────────────────────
2537
+
2538
+ tokenizeForMatching(text) {
2539
+ if (!text) return [];
2540
+ const lower = String(text).toLowerCase();
2541
+ const tokens = lower.split(/[^a-z0-9]+/g).filter(Boolean);
2542
+ // Small stopword list; just enough to ignore generic phrasing.
2543
+ const stop = new Set([
2544
+ 'the', 'a', 'an', 'to', 'for', 'and', 'or', 'of', 'in', 'on', 'with', 'this', 'that',
2545
+ 'i', 'me', 'my', 'we', 'our', 'you', 'your',
2546
+ 'want', 'need', 'help', 'can', 'could', 'would', 'should',
2547
+ 'how', 'do', 'does', 'did', 'what', 'is', 'are', 'it', 'be',
2548
+ ]);
2549
+ return tokens.filter((t) => !stop.has(t));
2550
+ }
2551
+
2552
+ inferStage(goal, explicitStage) {
2553
+ if (explicitStage) return explicitStage;
2554
+ if (!goal) return null;
2555
+
2556
+ const patterns = {
2557
+ prompt_workspace: /\b(prompt|skill|workspace|create|edit|draft|write|local)\b/gi,
2558
+ persona: /\b(persona|metaprompt|interview|agent|character|assistant|system prompt)\b/gi,
2559
+ libraries: /\b(library|manifest|template|collection|import|export)\b/gi,
2560
+ governance: /\b(publish|proposal|vote|subdao|dao|govern|approve|execute)\b/gi,
2561
+ treasury: /\b(treasury|bond|boost|withdraw|sxxx|token|stake)\b/gi,
2562
+ discovery: /\b(search|find|list|browse|explore|trending|discover)\b/gi,
2563
+ };
2564
+
2565
+ const scores = {};
2566
+ for (const [stage, pattern] of Object.entries(patterns)) {
2567
+ const matches = goal.match(pattern) || [];
2568
+ if (matches.length > 0) scores[stage] = matches.length;
2569
+ }
2570
+
2571
+ const entries = Object.entries(scores);
2572
+ if (!entries.length) return null;
2573
+ entries.sort((a, b) => b[1] - a[1]);
2574
+ return entries[0][0];
2575
+ }
2576
+
2577
+ detectWorkflow(goal) {
2578
+ if (!goal) return null;
2579
+ const lower = goal.toLowerCase();
2580
+
2581
+ const workflows = [
2582
+ {
2583
+ // Create & publish a prompt/library
2584
+ pattern: /create.*prompt.*publish|publish.*prompt|create.*library.*publish|publish.*library/i,
2585
+ tools: ['quick_create_prompt', 'improve_prompt', 'publish_manifest_flow'],
2586
+ },
2587
+ {
2588
+ // Design & (optionally) publish a persona
2589
+ pattern: /create.*persona.*publish|design.*persona.*publish|persona.*publish/i,
2590
+ tools: ['list_persona_templates', 'persona_interview_step', 'save_metaprompt', 'publish_manifest_flow'],
2591
+ },
2592
+ {
2593
+ // Design a persona without explicit publishing intent
2594
+ pattern: /design.*persona|create.*persona|persona.*workflow/i,
2595
+ tools: ['list_persona_templates', 'persona_interview_step', 'save_metaprompt'],
2596
+ },
2597
+ {
2598
+ // Find & iterate on prompts
2599
+ pattern: /find.*prompt.*iterate|iterate.*prompt|improve.*prompt/i,
2600
+ tools: ['search_prompts', 'get_prompt', 'quick_iterate_prompt'],
2601
+ },
2602
+ {
2603
+ // Publish to a DAO/SubDAO
2604
+ pattern: /publish.*dao|dao.*publish|publish.*subdao|subdao.*publish/i,
2605
+ tools: ['suggest_subdaos_for_library', 'generate_publishing_commands', 'publish_manifest_flow'],
2606
+ },
2607
+ ];
2608
+
2609
+ for (const wf of workflows) {
2610
+ if (wf.pattern.test(lower)) {
2611
+ return wf.tools;
2612
+ }
2613
+ }
2614
+ return null;
2615
+ }
2616
+
2617
+ scoreToolForGoal(toolMeta, goalTokens, stage) {
2618
+ // BM25-inspired scoring with simple term frequency and length normalization.
2619
+ const baseTerms = [
2620
+ ...(toolMeta.keywords || []),
2621
+ ...this.tokenizeForMatching(toolMeta.description || ''),
2622
+ ...this.tokenizeForMatching(toolMeta.whenToUse || ''),
2623
+ ];
2624
+
2625
+ const docLen = baseTerms.length || 1;
2626
+ const avgLen = 15;
2627
+ const k1 = 1.2;
2628
+ const b = 0.75;
2629
+
2630
+ let score = 0;
2631
+
2632
+ for (const token of goalTokens) {
2633
+ const tf = baseTerms.filter((t) => t === token).length;
2634
+ if (tf > 0) {
2635
+ const tfScore = (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * (docLen / avgLen)));
2636
+ score += tfScore;
2637
+ }
2638
+ }
2639
+
2640
+ // Negative keyword penalty.
2641
+ if (toolMeta.negativeKeywords && toolMeta.negativeKeywords.length) {
2642
+ let penalties = 0;
2643
+ for (const neg of toolMeta.negativeKeywords) {
2644
+ if (goalTokens.includes(neg)) penalties += 1;
2645
+ }
2646
+ if (penalties > 0) {
2647
+ score *= (0.5 ** penalties);
2648
+ }
2649
+ }
2650
+
2651
+ // Stage/category boost.
2652
+ if (stage && toolMeta.category === stage) {
2653
+ score *= 1.5;
2654
+ }
2655
+
2656
+ // Base weight.
2657
+ const weight = typeof toolMeta.weight === 'number' ? toolMeta.weight : 1;
2658
+ score *= weight;
2659
+
2660
+ // Normalize to 0–1 range.
2661
+ return Math.min(score / 5, 1);
2662
+ }
2663
+
2664
+ orderSuggestedTools(scored, limit) {
2665
+ if (!Array.isArray(scored) || !scored.length) return [];
2666
+ const sorted = [...scored].sort((a, b) => b.confidence - a.confidence).slice(0, limit);
2667
+ if (sorted.length <= 3) {
2668
+ return sorted.map((item, idx) => ({ ...item, priority: idx + 1 }));
2669
+ }
2670
+
2671
+ // Simple U-shaped ordering: higher-ranked items gravitate to the ends.
2672
+ const result = [];
2673
+ for (let i = 0; i < sorted.length; i += 1) {
2674
+ if (i % 2 === 0) {
2675
+ // Even indices append to the end
2676
+ result.push(sorted[i]);
2677
+ } else {
2678
+ // Odd indices unshift to the front
2679
+ result.unshift(sorted[i]);
2680
+ }
2681
+ }
2682
+
2683
+ return result.map((item, idx) => ({ ...item, priority: idx + 1 }));
2684
+ }
2685
+
2686
+ async suggestSageTools(params) {
2687
+ // Arguments are already validated by createToolArgsValidator.
2688
+ const { goal, stage, context, limit = 5, includeAlternatives = false } = params || {};
2689
+
2690
+ const effectiveStage = this.inferStage(goal, stage);
2691
+ const goalTokens = this.tokenizeForMatching(goal);
2692
+
2693
+ // Choose candidate tools from registry.
2694
+ let candidates = [];
2695
+ if (effectiveStage === 'discovery') {
2696
+ // Discovery is intentionally cross-cutting; surface the most browse/search oriented tools.
2697
+ const discoveryTools = ['search_prompts', 'list_prompts', 'trending_prompts', 'list_persona_templates', 'list_libraries'];
2698
+ candidates = discoveryTools
2699
+ .map((name) => getToolMeta(name))
2700
+ .filter(Boolean);
2701
+ } else if (effectiveStage) {
2702
+ candidates = getToolsForCategory(effectiveStage);
2703
+ } else {
2704
+ // If no clear stage, fall back to a small set of discovery-friendly tools.
2705
+ const discoveryTools = ['search_prompts', 'list_prompts', 'trending_prompts', 'list_persona_templates', 'list_libraries'];
2706
+ candidates = discoveryTools
2707
+ .map((name) => getToolMeta(name))
2708
+ .filter(Boolean);
2709
+ }
2710
+
2711
+ // Cross-category fallback: if we have a clear stage, allow strongly-matching tools
2712
+ // from other categories to join the candidate set.
2713
+ if (effectiveStage) {
2714
+ const allTools = Object.entries(TOOL_REGISTRY).map(([name, meta]) => ({ name, ...meta }));
2715
+ const existingNames = new Set(candidates.map((c) => c.name));
2716
+ const crossCategory = allTools
2717
+ .filter((t) => t.category !== effectiveStage && !existingNames.has(t.name))
2718
+ .map((t) => ({
2719
+ ...t,
2720
+ confidence: this.scoreToolForGoal(t, goalTokens, null),
2721
+ }))
2722
+ .filter((t) => t.confidence > 0.6);
2723
+
2724
+ if (crossCategory.length) {
2725
+ candidates = candidates.concat(crossCategory);
2726
+ }
2727
+ }
2728
+
2729
+ // Apply simple context-based filtering/hints.
2730
+ if (context && context.hasWorkspace === false) {
2731
+ // If there is no workspace, de-prioritize workspace-only tools.
2732
+ candidates = candidates.map((c) => {
2733
+ if (c.category === 'prompt_workspace') {
2734
+ return { ...c, weight: (c.weight || 1) * 0.7 };
2735
+ }
2736
+ return c;
2737
+ });
2738
+ }
2739
+
2740
+ // Score tools.
2741
+ const scored = candidates.map((meta) => ({
2742
+ ...meta,
2743
+ confidence: this.scoreToolForGoal(meta, goalTokens, effectiveStage),
2744
+ })).filter((item) => item.confidence >= 0.15);
2745
+
2746
+ if (!scored.length) {
2747
+ const fallback = {
2748
+ recommendations: [],
2749
+ alternatives: [],
2750
+ inferredStage: effectiveStage || null,
2751
+ suggestedWorkflow: null,
2752
+ };
2753
+ return {
2754
+ content: [{
2755
+ type: 'text',
2756
+ text: JSON.stringify(fallback, null, 2),
2757
+ }],
2758
+ };
2759
+ }
2760
+
2761
+ const ordered = this.orderSuggestedTools(scored, limit);
2762
+ const workflow = this.detectWorkflow(goal);
2763
+
2764
+ const recommendations = ordered.map((item) => ({
2765
+ toolName: item.name,
2766
+ category: item.category,
2767
+ confidence: item.confidence,
2768
+ priority: item.priority,
2769
+ rationale: item.whenToUse,
2770
+ requiredParams: item.requiredParams || [],
2771
+ }));
2772
+
2773
+ let alternatives = [];
2774
+ if (includeAlternatives) {
2775
+ const remaining = scored
2776
+ .filter((s) => !ordered.find((r) => r.name === s.name))
2777
+ .sort((a, b) => b.confidence - a.confidence)
2778
+ .slice(0, limit);
2779
+ alternatives = remaining.map((item, idx) => ({
2780
+ toolName: item.name,
2781
+ category: item.category,
2782
+ confidence: item.confidence,
2783
+ priority: idx + 1,
2784
+ rationale: item.whenToUse,
2785
+ requiredParams: item.requiredParams || [],
2786
+ }));
2787
+ }
2788
+
2789
+ const result = {
2790
+ recommendations,
2791
+ alternatives,
2792
+ inferredStage: effectiveStage || null,
2793
+ suggestedWorkflow: workflow || null,
2794
+ };
2795
+
2796
+ return {
2797
+ content: [{
2798
+ type: 'text',
2799
+ text: JSON.stringify(result, null, 2),
2800
+ }],
2801
+ };
2802
+ }
2803
+
2456
2804
  listMetaprompts() {
2457
2805
  const rows = metapromptDesigner.listMetaprompts();
2458
2806
  if (!rows.length) {
@@ -2848,6 +3196,207 @@ Use \`help(topic="create")\` for more details.
2848
3196
  }
2849
3197
  }
2850
3198
 
3199
+ // ───────────────────────────── Metaprompt Engine Tools ─────────────────────────────
3200
+
3201
+ async listPersonaTemplates() {
3202
+ try {
3203
+ const templates = require('./services/metaprompt/templates.json');
3204
+ const list = Object.values(templates).map(t => ({
3205
+ key: t.key,
3206
+ name: t.name,
3207
+ description: t.description,
3208
+ recommended_slots: t.recommended_slots?.map(s => s.label) || []
3209
+ }));
3210
+
3211
+ return {
3212
+ content: [
3213
+ {
3214
+ type: 'text',
3215
+ text: JSON.stringify(list, null, 2)
3216
+ }
3217
+ ]
3218
+ };
3219
+ } catch (error) {
3220
+ return {
3221
+ isError: true,
3222
+ content: [{ type: 'text', text: `Failed to list templates: ${error.message}` }]
3223
+ };
3224
+ }
3225
+ }
3226
+
3227
+ async runPersonaInterview(params) {
3228
+ const { template, answers, save, saveKey } = params;
3229
+ try {
3230
+ const config = require('./config');
3231
+ const PromptBuilder = require('./services/metaprompt/prompt-builder');
3232
+ const MetapromptPersistence = require('./services/metaprompt/persistence');
3233
+ const templates = require('./services/metaprompt/templates.json');
3234
+
3235
+ // 1. Validate Template
3236
+ const templateDef = templates[template] || templates['custom'];
3237
+
3238
+ // 2. Build Prompt (One-Shot)
3239
+ // Note: run_persona_interview assumes the *agent* has already done the planning/filling.
3240
+ // It bypasses the SlotPlanner and just builds the artifact.
3241
+ const builder = new PromptBuilder();
3242
+ // We need the full slot definitions to build dynamic sections.
3243
+ // Since we skipped the planner, we'll use the template's recommended slots as the definition.
3244
+ const slots = templateDef.recommended_slots || [];
3245
+
3246
+ const systemPrompt = builder.buildSystemPrompt(template, slots, answers || {});
3247
+
3248
+ let result = {
3249
+ systemPrompt,
3250
+ slug: '',
3251
+ paths: {}
3252
+ };
3253
+
3254
+ // 3. Save (Optional)
3255
+ if (save) {
3256
+ const persistence = new MetapromptPersistence(config);
3257
+ const slug = saveKey || `${template}-${Date.now().toString().slice(-6)}`;
3258
+
3259
+ // Save dummy history so the artifact exists
3260
+ const historyPaths = persistence.saveMetaprompt(slug, {
3261
+ templateKey: template,
3262
+ transcript: [{ role: 'system', content: 'Generated via MCP run_persona_interview (one-shot).' }],
3263
+ answers: answers || {}
3264
+ });
3265
+
3266
+ // Save Skill
3267
+ const skillPath = persistence.saveSkill(slug, systemPrompt);
3268
+
3269
+ result.slug = slug;
3270
+ result.paths = {
3271
+ metaprompt: historyPaths.metaprompt,
3272
+ skill: skillPath
3273
+ };
3274
+ }
3275
+
3276
+ return {
3277
+ content: [
3278
+ {
3279
+ type: 'text',
3280
+ text: JSON.stringify(result, null, 2)
3281
+ }
3282
+ ]
3283
+ };
3284
+
3285
+ } catch (error) {
3286
+ return {
3287
+ isError: true,
3288
+ content: [{ type: 'text', text: `Failed to run persona interview: ${error.message}` }]
3289
+ };
3290
+ }
3291
+ }
3292
+
3293
+ /**
3294
+ * Stateful persona interview that walks through template slots.
3295
+ *
3296
+ * Returns slot metadata (label, description, group) for the host agent to phrase questions.
3297
+ * The agent passes answers back with the stateToken to progress through slots.
3298
+ * When all required slots are filled, returns the generated system prompt.
3299
+ *
3300
+ * @param {object} params
3301
+ * @param {string} params.template - Template key (coding-assistant, governance-helper, etc.)
3302
+ * @param {string} [params.stateToken] - Base64-encoded state from previous call
3303
+ * @param {string} [params.answer] - Answer for the current slot (currentSlotKey from previous response)
3304
+ */
3305
+ async personaInterviewStep(params) {
3306
+ const { template, stateToken, answer } = params;
3307
+ try {
3308
+ const templates = require('./services/metaprompt/templates.json');
3309
+ const PromptBuilder = require('./services/metaprompt/prompt-builder');
3310
+
3311
+ // 1. Restore or initialize state
3312
+ let state;
3313
+ if (stateToken) {
3314
+ try {
3315
+ state = JSON.parse(Buffer.from(stateToken, 'base64').toString('utf8'));
3316
+ } catch {
3317
+ return { isError: true, content: [{ type: 'text', text: 'Invalid stateToken' }] };
3318
+ }
3319
+ } else {
3320
+ // Initialize new interview state
3321
+ const templateKey = template || 'custom';
3322
+ const templateDef = templates[templateKey] || templates.custom;
3323
+ state = {
3324
+ templateKey,
3325
+ slots: templateDef.recommended_slots || [],
3326
+ answers: {},
3327
+ currentSlotKey: null,
3328
+ };
3329
+ }
3330
+
3331
+ // 2. Record answer for the current slot (if provided)
3332
+ if (answer && state.currentSlotKey) {
3333
+ state.answers[state.currentSlotKey] = answer;
3334
+ }
3335
+
3336
+ // 3. Find next unanswered slot (required first, then optional by priority)
3337
+ const unanswered = state.slots.filter(s => !state.answers[s.key]);
3338
+ const requiredUnanswered = unanswered.filter(s => s.required);
3339
+ const nextSlot = requiredUnanswered[0] || unanswered.find(s => s.priority <= 2) || null;
3340
+
3341
+ // 4. Update state with current slot key
3342
+ state.currentSlotKey = nextSlot?.key || null;
3343
+
3344
+ // 5. Serialize state for next call
3345
+ const newStateToken = Buffer.from(JSON.stringify(state)).toString('base64');
3346
+
3347
+ // 6. Check if interview is complete
3348
+ if (!nextSlot) {
3349
+ // All required slots filled - build the system prompt (no LLM needed)
3350
+ const builder = new PromptBuilder();
3351
+ const systemPrompt = builder.buildSystemPrompt(state.templateKey, state.slots, state.answers);
3352
+
3353
+ return {
3354
+ content: [{
3355
+ type: 'text',
3356
+ text: JSON.stringify({
3357
+ done: true,
3358
+ systemPrompt,
3359
+ template: state.templateKey,
3360
+ answers: state.answers,
3361
+ filledSlots: Object.keys(state.answers).length,
3362
+ totalSlots: state.slots.length,
3363
+ stateToken: newStateToken,
3364
+ }, null, 2)
3365
+ }]
3366
+ };
3367
+ }
3368
+
3369
+ // 7. Return next slot info for the host LLM to phrase the question
3370
+ // The host LLM should use slotLabel/slotDescription to ask the user
3371
+ const suggestedQuestion = `Please provide: ${nextSlot.label}. ${nextSlot.description || ''}`.trim();
3372
+
3373
+ return {
3374
+ content: [{
3375
+ type: 'text',
3376
+ text: JSON.stringify({
3377
+ done: false,
3378
+ currentSlotKey: nextSlot.key,
3379
+ slotLabel: nextSlot.label,
3380
+ slotDescription: nextSlot.description || '',
3381
+ slotGroup: nextSlot.group || 'general',
3382
+ slotRequired: nextSlot.required || false,
3383
+ suggestedQuestion,
3384
+ filledSlots: Object.keys(state.answers).length,
3385
+ totalSlots: state.slots.length,
3386
+ remainingRequired: requiredUnanswered.length,
3387
+ stateToken: newStateToken,
3388
+ }, null, 2)
3389
+ }]
3390
+ };
3391
+
3392
+ } catch (error) {
3393
+ return {
3394
+ isError: true,
3395
+ content: [{ type: 'text', text: `Failed to execute interview step: ${error.message}` }]
3396
+ };
3397
+ }
3398
+ }
3399
+
2851
3400
  async getSubDAOList(options = {}) {
2852
3401
  try {
2853
3402
  return await this.subdaoDiscovery.listSubDaos(options);
@@ -11,6 +11,7 @@ const { ethers } = require('ethers');
11
11
  const { execSync } = require('child_process');
12
12
  const axios = require('axios');
13
13
  const fss = require('fs');
14
+ const { hydrateEnvFromSageConfig } = require('./services/mcp/env-loader');
14
15
  const { createLibraryManifestLister } = require('./services/mcp/library-manifests');
15
16
  const { createManifestFetcher } = require('./services/mcp/manifest-fetcher');
16
17
  const { createManifestWorkflows } = require('./services/mcp/manifest-workflows');
@@ -57,37 +58,10 @@ class MCPServer extends EventEmitter {
57
58
  }
58
59
 
59
60
  async initialize() {
60
- // Hydrate env from .sage/config.json if missing
61
+ // Hydrate env from .sage/config.json if missing (factory, registry, token, subgraph)
61
62
  try {
62
- let dir = process.cwd();
63
- let cfgPath = null;
64
- for (let i = 0; i < 6; i++) {
65
- const cand = path.join(dir, '.sage', 'config.json');
66
- if (fss.existsSync(cand)) { cfgPath = cand; break; }
67
- const next = path.dirname(dir);
68
- if (next === dir) break;
69
- dir = next;
70
- }
71
- if (!cfgPath) {
72
- const alt = path.join(__dirname, '..', '.sage', 'config.json');
73
- if (fss.existsSync(alt)) cfgPath = alt;
74
- }
75
- if (cfgPath) {
76
- const raw = fss.readFileSync(cfgPath, 'utf8');
77
- const j = JSON.parse(raw);
78
- const active = j.activeProfile || Object.keys(j.profiles || {})[0];
79
- const a = (j.profiles && j.profiles[active] && j.profiles[active].addresses) || {};
80
- if (!process.env.SUBDAO_FACTORY_ADDRESS && (a.SUBDAO_FACTORY_ADDRESS || a.SUBDAO_FACTORY)) {
81
- process.env.SUBDAO_FACTORY_ADDRESS = a.SUBDAO_FACTORY_ADDRESS || a.SUBDAO_FACTORY;
82
- }
83
- if (!process.env.LIBRARY_REGISTRY_ADDRESS && (a.LIBRARY_REGISTRY_ADDRESS || a.LIBRARY_REGISTRY)) {
84
- process.env.LIBRARY_REGISTRY_ADDRESS = a.LIBRARY_REGISTRY_ADDRESS || a.LIBRARY_REGISTRY;
85
- }
86
- if (!process.env.SXXX_TOKEN_ADDRESS && (a.SXXX_TOKEN_ADDRESS || a.SXXX)) {
87
- process.env.SXXX_TOKEN_ADDRESS = a.SXXX_TOKEN_ADDRESS || a.SXXX;
88
- }
89
- }
90
- } catch (_) { /* ignore */ }
63
+ hydrateEnvFromSageConfig({ cwd: process.cwd(), fallbackDir: path.join(__dirname, '..') });
64
+ } catch (_) { /* non-fatal */ }
91
65
  // Ensure directories exist
92
66
  await this.ensureDirectories();
93
67