fraim-framework 2.0.87 → 2.0.89

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.
@@ -28,6 +28,7 @@ const axios_1 = __importDefault(require("axios"));
28
28
  const provider_utils_1 = require("../core/utils/provider-utils");
29
29
  const object_utils_1 = require("../core/utils/object-utils");
30
30
  const local_registry_resolver_1 = require("../core/utils/local-registry-resolver");
31
+ const ai_mentor_1 = require("../core/ai-mentor");
31
32
  /**
32
33
  * Handle template substitution logic separately for better testability
33
34
  */
@@ -60,11 +61,33 @@ class FraimTemplateEngine {
60
61
  setRepoInfo(repoInfo) {
61
62
  this.repoInfo = repoInfo;
62
63
  }
64
+ setAgentInfo(info) {
65
+ this.agentInfo = info;
66
+ }
67
+ setMachineInfo(info) {
68
+ this.machineInfo = info;
69
+ }
63
70
  setConfig(config) {
64
71
  this.config = config;
65
72
  }
66
73
  substituteTemplates(content) {
67
74
  let result = content;
75
+ // 0. Substitute runtime context tokens: {{agent.*}}, {{machine.*}}, {{repository.*}}
76
+ // These come from the fraim_connect payload captured during handshake.
77
+ const contexts = {
78
+ agent: this.agentInfo,
79
+ machine: this.machineInfo,
80
+ repository: this.repoInfo
81
+ };
82
+ result = result.replace(/\{\{(agent|machine|repository)\.([^}]+)\}\}/g, (match, ns, path) => {
83
+ const source = contexts[ns];
84
+ if (source) {
85
+ const value = (0, object_utils_1.getNestedValue)(source, path.trim());
86
+ if (value !== undefined)
87
+ return String(value);
88
+ }
89
+ return match;
90
+ });
68
91
  // First, substitute config variables with fallback support.
69
92
  // Fallbacks must work even when local config is unavailable.
70
93
  result = result.replace(/\{\{proxy\.config\.([^}|]+)(?:\s*\|\s*"([^"]+)")?\}\}/g, (match, path, fallback) => {
@@ -210,11 +233,12 @@ FraimTemplateEngine.ISSUE_ACTIONS = new Set([
210
233
  'list_issues'
211
234
  ]);
212
235
  class FraimLocalMCPServer {
213
- constructor() {
236
+ constructor(writer) {
214
237
  this.config = null;
215
238
  this.clientSupportsRoots = false;
216
239
  this.workspaceRoot = null;
217
240
  this.pendingRootsRequest = false;
241
+ this.agentInfo = null;
218
242
  this.machineInfo = null;
219
243
  this.repoInfo = null;
220
244
  this.engine = null;
@@ -226,6 +250,7 @@ class FraimLocalMCPServer {
226
250
  this.pendingFallbackTokenCounts = new Map();
227
251
  this.pendingFallbackEvents = [];
228
252
  this.fallbackSummaryEmitted = false;
253
+ this.writer = writer || process.stdout.write.bind(process.stdout);
229
254
  this.remoteUrl = process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
230
255
  this.apiKey = this.loadApiKey();
231
256
  this.localVersion = this.detectLocalVersion();
@@ -311,21 +336,6 @@ class FraimLocalMCPServer {
311
336
  this.log(`✅ Using workspace root from MCP roots: ${this.workspaceRoot}`);
312
337
  return this.workspaceRoot;
313
338
  }
314
- // Log all potentially useful environment variables for debugging
315
- this.log(`🔍 Environment variables:`);
316
- this.log(` WORKSPACE_FOLDER_PATHS: ${process.env.WORKSPACE_FOLDER_PATHS || '(not set)'}`);
317
- this.log(` WORKSPACE_FOLDER: ${process.env.WORKSPACE_FOLDER || '(not set)'}`);
318
- this.log(` PROJECT_ROOT: ${process.env.PROJECT_ROOT || '(not set)'}`);
319
- this.log(` VSCODE_CWD: ${process.env.VSCODE_CWD || '(not set)'}`);
320
- this.log(` INIT_CWD: ${process.env.INIT_CWD || '(not set)'}`);
321
- this.log(` HOME: ${process.env.HOME || '(not set)'}`);
322
- this.log(` USERPROFILE: ${process.env.USERPROFILE || '(not set)'}`);
323
- this.log(` PWD: ${process.env.PWD || '(not set)'}`);
324
- this.log(` OLDPWD: ${process.env.OLDPWD || '(not set)'}`);
325
- // Log ALL env vars that might contain workspace info
326
- Object.keys(process.env).filter(k => k.toLowerCase().includes('workspace') || k.toLowerCase().includes('project') || k.toLowerCase().includes('kiro')).forEach(k => {
327
- this.log(` ${k}: ${process.env[k]}`);
328
- });
329
339
  // Priority 1: Check for IDE-provided workspace environment variables
330
340
  const workspaceHints = [
331
341
  process.env.WORKSPACE_FOLDER_PATHS?.split(':')[0], // Cursor provides this (colon-separated for multi-root)
@@ -448,33 +458,8 @@ class FraimLocalMCPServer {
448
458
  }).trim();
449
459
  }
450
460
  catch (error) {
451
- // If git command fails, construct URL from config if available.
452
- const repositoryConfig = this.config?.repository;
453
- if (repositoryConfig?.url) {
454
- repoUrl = repositoryConfig.url;
455
- this.log(`Constructed repo URL from config: ${repoUrl}`);
456
- }
457
- else if (repositoryConfig?.provider === 'github' && repositoryConfig.owner && repositoryConfig.name) {
458
- repoUrl = `https://github.com/${repositoryConfig.owner}/${repositoryConfig.name}.git`;
459
- this.log(`Constructed repo URL from config: ${repoUrl}`);
460
- }
461
- else if (repositoryConfig?.provider === 'ado' &&
462
- repositoryConfig.organization &&
463
- repositoryConfig.project &&
464
- repositoryConfig.name) {
465
- repoUrl = `https://dev.azure.com/${repositoryConfig.organization}/${repositoryConfig.project}/_git/${repositoryConfig.name}`;
466
- this.log(`Constructed repo URL from config: ${repoUrl}`);
467
- }
468
- else if (repositoryConfig?.provider === 'gitlab') {
469
- const projectPath = repositoryConfig.projectPath
470
- || (repositoryConfig.namespace && repositoryConfig.name
471
- ? `${repositoryConfig.namespace}/${repositoryConfig.name}`
472
- : null);
473
- if (projectPath) {
474
- repoUrl = `https://gitlab.com/${projectPath}.git`;
475
- this.log(`Constructed repo URL from config: ${repoUrl}`);
476
- }
477
- }
461
+ // Fall back to config if git fails
462
+ repoUrl = this.config?.repository?.url || '';
478
463
  }
479
464
  if (!repoUrl) {
480
465
  this.log('No git repository found and no config available');
@@ -620,12 +605,21 @@ class FraimLocalMCPServer {
620
605
  projectRoot: this.findProjectRoot(),
621
606
  logFn: (msg) => this.log(msg)
622
607
  });
608
+ // Apply stored runtime context if available.
609
+ if (this.agentInfo)
610
+ this.engine.setAgentInfo(this.agentInfo);
611
+ if (this.machineInfo)
612
+ this.engine.setMachineInfo(this.machineInfo);
623
613
  }
624
614
  else {
625
615
  this.engine.setConfig(this.config);
626
616
  if (this.repoInfo) {
627
617
  this.engine.setRepoInfo(this.repoInfo);
628
618
  }
619
+ if (this.agentInfo)
620
+ this.engine.setAgentInfo(this.agentInfo);
621
+ if (this.machineInfo)
622
+ this.engine.setMachineInfo(this.machineInfo);
629
623
  }
630
624
  return this.engine;
631
625
  }
@@ -663,11 +657,14 @@ class FraimLocalMCPServer {
663
657
  method: 'tools/call',
664
658
  params: {
665
659
  name: 'get_fraim_file',
666
- arguments: { path }
660
+ arguments: {
661
+ path,
662
+ raw: true
663
+ }
667
664
  }
668
665
  };
669
666
  this.applyRequestSessionId(request, requestSessionId);
670
- const response = await this.proxyToRemote(request);
667
+ const response = await this._doProxyToRemote(request);
671
668
  if (response.error) {
672
669
  this.log(`⚠️ Failed to fetch registry file ${path}: ${response.error.message}`);
673
670
  return null;
@@ -790,13 +787,7 @@ class FraimLocalMCPServer {
790
787
  }
791
788
  return this.applyAgentFallbackForUnresolvedProxy(processedResponse);
792
789
  }
793
- shouldResolveIncludes(toolName) {
794
- return toolName === 'get_fraim_workflow' ||
795
- toolName === 'get_fraim_job' ||
796
- toolName === 'get_fraim_file' ||
797
- toolName === 'seekMentoring';
798
- }
799
- async resolveIncludesInResponse(response, requestSessionId) {
790
+ async resolveIncludesInResponse(response, requestSessionId, requestId) {
800
791
  if (!response.result?.content || !Array.isArray(response.result.content)) {
801
792
  return response;
802
793
  }
@@ -807,6 +798,8 @@ class FraimLocalMCPServer {
807
798
  transformedContent.push(block);
808
799
  continue;
809
800
  }
801
+ // resolver.resolveIncludes handles recursion internally via the remoteContentResolver
802
+ // configured in getRegistryResolver, which correctly uses _doProxyToRemote.
810
803
  const resolvedText = await resolver.resolveIncludes(block.text);
811
804
  transformedContent.push({
812
805
  ...block,
@@ -821,14 +814,68 @@ class FraimLocalMCPServer {
821
814
  }
822
815
  };
823
816
  }
824
- async finalizeToolResponse(request, response, requestSessionId) {
825
- let finalizedResponse = response;
817
+ async finalizeToolResponse(request, response, requestSessionId, requestId) {
826
818
  const toolName = request.params?.name;
827
- if (request.method === 'tools/call' && typeof toolName === 'string' && this.shouldResolveIncludes(toolName)) {
828
- finalizedResponse = await this.resolveIncludesInResponse(finalizedResponse, requestSessionId);
819
+ const args = request.params?.arguments || {};
820
+ if (request.method !== 'tools/call' || typeof toolName !== 'string') {
821
+ return this.processResponseWithHydration(response, requestSessionId);
822
+ }
823
+ const projectRoot = this.findProjectRoot();
824
+ const resolver = projectRoot ? this.getRegistryResolver(requestSessionId) : null;
825
+ let finalizedResponse = response;
826
+ // 1. Resolve top-level override if this tool maps to a specific file.
827
+ if (resolver) {
828
+ let registryPath = null;
829
+ if (toolName === 'get_fraim_file')
830
+ registryPath = args.path;
831
+ else if (toolName === 'get_fraim_skill')
832
+ registryPath = await resolver.findRegistryPath('skills', args.skill);
833
+ else if (toolName === 'get_fraim_job')
834
+ registryPath = await resolver.findRegistryPath('jobs', args.job);
835
+ else if (toolName === 'get_fraim_workflow')
836
+ registryPath = await resolver.findRegistryPath('workflows', args.workflow);
837
+ if (registryPath) {
838
+ const localOverride = resolver.hasLocalOverride(registryPath);
839
+ const remoteFailed = response.error && (response.error.code === -32001 ||
840
+ response.error.data?.remoteStatus === 404);
841
+ if (localOverride || remoteFailed) {
842
+ try {
843
+ const resolved = await resolver.resolveFile(registryPath);
844
+ if (resolved.content) {
845
+ const substitutedContent = this.substituteTemplates(resolved.content);
846
+ const newResponse = {
847
+ ...response,
848
+ result: {
849
+ ...response.result,
850
+ content: [{ type: 'text', text: substitutedContent }]
851
+ }
852
+ };
853
+ delete newResponse.error;
854
+ finalizedResponse = newResponse;
855
+ // Continue to Step 2 to resolve includes within the local override
856
+ }
857
+ }
858
+ catch (error) {
859
+ this.log(`[req:${requestId}] Top-level local resolution failed for ${registryPath}: ${error.message}`);
860
+ }
861
+ }
862
+ }
829
863
  }
864
+ // 2. Resolve includes within the content (for all registry tools)
865
+ finalizedResponse = await this.resolveIncludesInResponse(finalizedResponse, requestSessionId, requestId);
830
866
  return this.processResponseWithHydration(finalizedResponse, requestSessionId);
831
867
  }
868
+ async finalizeLocalToolTextResponse(request, requestSessionId, requestId, text) {
869
+ const response = {
870
+ jsonrpc: '2.0',
871
+ id: request.id,
872
+ result: {
873
+ content: [{ type: 'text', text }]
874
+ }
875
+ };
876
+ const withResolvedIncludes = await this.resolveIncludesInResponse(response, requestSessionId, requestId);
877
+ return this.processResponseWithHydration(withResolvedIncludes, requestSessionId);
878
+ }
832
879
  rewriteProxyTokensInText(text) {
833
880
  const tokens = new Set();
834
881
  const rewritten = text.replace(/\{\{\s*proxy\.([^}]+?)\s*\}\}/g, (_match, proxyPath) => {
@@ -891,12 +938,14 @@ class FraimLocalMCPServer {
891
938
  workspaceRoot: process.cwd(),
892
939
  remoteContentResolver: async (_path) => {
893
940
  throw new Error('No project root available');
894
- }
941
+ },
942
+ shouldFilter: (content) => this.isStub(content)
895
943
  });
896
944
  }
897
945
  else {
898
946
  return new local_registry_resolver_1.LocalRegistryResolver({
899
947
  workspaceRoot: projectRoot,
948
+ shouldFilter: (content) => this.isStub(content),
900
949
  remoteContentResolver: async (path) => {
901
950
  // Fetch parent content from remote for inheritance
902
951
  this.log(`🔄 Remote content resolver: fetching ${path}`);
@@ -909,12 +958,12 @@ class FraimLocalMCPServer {
909
958
  name: 'get_fraim_file',
910
959
  arguments: {
911
960
  path,
912
- _internalRaw: true
961
+ raw: true
913
962
  }
914
963
  }
915
964
  };
916
965
  this.applyRequestSessionId(request, requestSessionId);
917
- const response = await this.proxyToRemote(request);
966
+ const response = await this._doProxyToRemote(request);
918
967
  if (response.error) {
919
968
  this.logError(`❌ Remote content resolver failed: ${response.error.message}`);
920
969
  throw new Error(`Failed to fetch parent: ${response.error.message}`);
@@ -935,69 +984,28 @@ class FraimLocalMCPServer {
935
984
  }
936
985
  }
937
986
  /**
938
- * Determine workflow category from workflow name
987
+ * Check if content is a "stub" indicating local version should be ignored.
939
988
  */
940
- getWorkflowCategory(workflowName) {
941
- // Product development workflows
942
- const productWorkflows = [
943
- 'prep-issue', 'spec', 'design', 'implement', 'test', 'resolve',
944
- 'prototype', 'refactor', 'iterate-on-pr-comments', 'retrospect'
945
- ];
946
- // Customer development workflows
947
- const customerWorkflows = [
948
- 'linkedin-outreach', 'customer-interview', 'insight-analysis',
949
- 'insight-triage', 'interview-preparation', 'strategic-brainstorming',
950
- 'thank-customers', 'user-survey-dispatch', 'users-to-target', 'weekly-newsletter'
951
- ];
952
- // Business development workflows
953
- const businessWorkflows = [
954
- 'partnership-outreach', 'investor-pitch', 'create-business-plan',
955
- 'ideate-business-opportunity', 'price-product'
956
- ];
957
- if (productWorkflows.includes(workflowName)) {
958
- return 'product-building';
959
- }
960
- else if (customerWorkflows.includes(workflowName)) {
961
- return 'customer-development';
962
- }
963
- else if (businessWorkflows.includes(workflowName)) {
964
- return 'business-development';
965
- }
966
- // Default to product-building for unknown workflows
967
- return 'product-building';
989
+ isStub(content) {
990
+ return content.includes('<!-- FRAIM_DISCOVERY_STUB -->') ||
991
+ content.includes('STUB:') ||
992
+ content.includes('<!-- STUB -->');
968
993
  }
969
994
  /**
970
- * Find local override path for a job name by scanning override roots.
971
- * Returns a registry-style path like jobs/<category>/<name>.md when found.
972
- */
973
- findJobOverridePath(jobName) {
974
- const projectRoot = this.findProjectRoot();
975
- if (!projectRoot)
976
- return null;
977
- const candidates = [
978
- (0, path_1.join)(projectRoot, '.fraim', 'personalized-employee', 'jobs'),
979
- (0, path_1.join)(projectRoot, '.fraim', 'overrides', 'jobs')
980
- ];
981
- for (const jobsRoot of candidates) {
982
- if (!(0, fs_1.existsSync)(jobsRoot))
983
- continue;
984
- try {
985
- const categories = (0, fs_1.readdirSync)(jobsRoot, { withFileTypes: true })
986
- .filter(entry => entry.isDirectory())
987
- .map(entry => entry.name);
988
- for (const category of categories) {
989
- const jobFilePath = (0, path_1.join)(jobsRoot, category, `${jobName}.md`);
990
- if ((0, fs_1.existsSync)(jobFilePath)) {
991
- return `jobs/${category}/${jobName}.md`;
992
- }
993
- }
994
- }
995
- catch {
996
- // Best effort scan only.
995
+ if (existsSync(jobFilePath)) {
996
+ return `jobs/${category}/${jobName}.md`;
997
997
  }
998
+ }
999
+ } catch {
1000
+ // Best effort scan only.
998
1001
  }
999
- return null;
1002
+ }
1003
+
1004
+ return null;
1000
1005
  }
1006
+
1007
+
1008
+
1001
1009
  /**
1002
1010
  * Process template substitution in MCP response
1003
1011
  */
@@ -1135,13 +1143,19 @@ class FraimLocalMCPServer {
1135
1143
  request.params.arguments.sessionId = requestSessionId;
1136
1144
  }
1137
1145
  }
1146
+ /**
1147
+ * Get or create a local AI mentor instance for the current session.
1148
+ */
1149
+ getMentor(requestSessionId) {
1150
+ const resolver = this.getRegistryResolver(requestSessionId);
1151
+ return new ai_mentor_1.AIMentor(resolver);
1152
+ }
1138
1153
  normalizeRepoContext(repo) {
1139
1154
  if (!repo || typeof repo !== 'object')
1140
1155
  return repo;
1141
1156
  const normalized = { ...repo };
1142
- const detectedProvider = (0, provider_utils_1.detectProvider)(normalized.url);
1143
- if (typeof detectedProvider === 'string' && detectedProvider.length > 0) {
1144
- normalized.provider = detectedProvider;
1157
+ if (!normalized.provider && normalized.url) {
1158
+ normalized.provider = (0, provider_utils_1.detectProvider)(normalized.url);
1145
1159
  }
1146
1160
  if (normalized.provider !== 'gitlab')
1147
1161
  return normalized;
@@ -1174,10 +1188,11 @@ class FraimLocalMCPServer {
1174
1188
  };
1175
1189
  }
1176
1190
  /**
1177
- * Proxy request to remote FRAIM server
1191
+ * Internal method to perform the actual proxy request to the remote server.
1192
+ * This method does NOT inject raw: true, as it is used for both top-level
1193
+ * tool calls and recursive inclusion resolution.
1178
1194
  */
1179
- async proxyToRemote(request) {
1180
- const requestId = (0, crypto_1.randomUUID)();
1195
+ async _doProxyToRemote(request, requestId = (0, crypto_1.randomUUID)()) {
1181
1196
  let sentFallbackTelemetry = null;
1182
1197
  try {
1183
1198
  // Special handling for fraim_connect - automatically inject machine and repo info
@@ -1191,9 +1206,6 @@ class FraimLocalMCPServer {
1191
1206
  ...detectedMachine // Detected values override (always win)
1192
1207
  };
1193
1208
  this.log(`[req:${requestId}] Auto-detected and injected machine info: ${JSON.stringify(args.machine)}`);
1194
- if (!args.machine.memory || !args.machine.cpus) {
1195
- this.logError(`[req:${requestId}] WARNING: Machine info missing memory or cpus! Detected: ${JSON.stringify(detectedMachine)}, Final: ${JSON.stringify(args.machine)}`);
1196
- }
1197
1209
  // REQUIRED: Auto-detect and inject repo info
1198
1210
  const detectedRepo = this.detectRepoInfo();
1199
1211
  if (detectedRepo) {
@@ -1239,10 +1251,14 @@ class FraimLocalMCPServer {
1239
1251
  ...args.repo,
1240
1252
  ...(args.issueTracking ? { issueTracking: args.issueTracking } : {})
1241
1253
  };
1242
- // Keep proxy runtime repository context in sync with connect payload.
1254
+ // Keep proxy runtime context in sync with connect payload.
1255
+ this.agentInfo = args.agent;
1256
+ this.machineInfo = args.machine;
1243
1257
  this.repoInfo = runtimeRepoContext;
1244
1258
  if (this.engine) {
1245
- this.engine.setRepoInfo(runtimeRepoContext);
1259
+ this.engine.setAgentInfo(this.agentInfo);
1260
+ this.engine.setMachineInfo(this.machineInfo);
1261
+ this.engine.setRepoInfo(this.repoInfo);
1246
1262
  }
1247
1263
  // Update the request with injected info
1248
1264
  request.params.arguments = args;
@@ -1264,7 +1280,12 @@ class FraimLocalMCPServer {
1264
1280
  .toString('base64');
1265
1281
  }
1266
1282
  this.log(`[req:${requestId}] Proxying ${request.method} to ${this.remoteUrl}/mcp`);
1267
- const response = await axios_1.default.post(`${this.remoteUrl}/mcp`, request, {
1283
+ // Resolve templates in the outgoing request so the remote server
1284
+ // only ever sees finalized values.
1285
+ const stringifiedRequest = JSON.stringify(request);
1286
+ const resolvedRequestStr = this.substituteTemplates(stringifiedRequest);
1287
+ const finalRequest = JSON.parse(resolvedRequestStr);
1288
+ const response = await axios_1.default.post(`${this.remoteUrl}/mcp`, finalRequest, {
1268
1289
  headers,
1269
1290
  timeout: 30000
1270
1291
  });
@@ -1335,7 +1356,7 @@ class FraimLocalMCPServer {
1335
1356
  params: {}
1336
1357
  };
1337
1358
  this.log(`📤 Sending roots/list request: ${JSON.stringify(rootsRequest)}`);
1338
- process.stdout.write(JSON.stringify(rootsRequest) + '\n');
1359
+ this.writer(JSON.stringify(rootsRequest) + '\n');
1339
1360
  }
1340
1361
  catch (error) {
1341
1362
  this.log(`⚠️ Failed to request roots: ${error.message}`);
@@ -1350,6 +1371,7 @@ class FraimLocalMCPServer {
1350
1371
  async handleRequest(request) {
1351
1372
  this.log(`📥 ${request.method}`);
1352
1373
  const requestSessionId = this.extractSessionIdFromRequest(request);
1374
+ const requestId = (0, crypto_1.randomUUID)();
1353
1375
  // Special handling for initialize request
1354
1376
  if (request.method === 'initialize') {
1355
1377
  // Check if client supports roots
@@ -1358,9 +1380,13 @@ class FraimLocalMCPServer {
1358
1380
  this.clientSupportsRoots = true;
1359
1381
  this.log(`✅ Client supports roots capability (listChanged: ${clientCapabilities.roots.listChanged})`);
1360
1382
  }
1361
- // Proxy initialize to remote server first
1362
- const response = await this.proxyToRemote(request);
1363
- const processedResponse = await this.finalizeToolResponse(request, response, requestSessionId);
1383
+ else {
1384
+ this.clientSupportsRoots = false;
1385
+ this.log(`❌ Client does NOT support roots capability`);
1386
+ }
1387
+ // Proxy initialize to remote server first using the raw proxy method
1388
+ const response = await this._doProxyToRemote(request, requestId);
1389
+ const processedResponse = await this.finalizeToolResponse(request, response, requestSessionId, requestId);
1364
1390
  // After successful initialization, load config
1365
1391
  if (!processedResponse.error) {
1366
1392
  // Load config immediately for compatibility, then request roots so
@@ -1373,160 +1399,90 @@ class FraimLocalMCPServer {
1373
1399
  this.log(`📤 ${request.method} → ${processedResponse.error ? 'ERROR' : 'OK'}`);
1374
1400
  return processedResponse;
1375
1401
  }
1376
- // Intercept get_fraim_workflow, get_fraim_job, and get_fraim_file for override resolution
1377
- if (request.method === 'tools/call' &&
1378
- (request.params?.name === 'get_fraim_workflow' ||
1379
- request.params?.name === 'get_fraim_job' ||
1380
- request.params?.name === 'get_fraim_file')) {
1381
- try {
1382
- const toolName = request.params.name;
1383
- const args = request.params.arguments || {};
1384
- // Extract the requested path
1385
- let requestedPath;
1386
- if (toolName === 'get_fraim_workflow') {
1387
- // Convert workflow name to path (e.g., "spec" -> "workflows/product-building/spec.md")
1388
- const workflowName = args.workflow;
1389
- if (!workflowName) {
1390
- this.log('⚠️ No workflow name provided in get_fraim_workflow');
1391
- }
1392
- else {
1393
- // Determine workflow category from name
1394
- const category = this.getWorkflowCategory(workflowName);
1395
- requestedPath = `workflows/${category}/${workflowName}.md`;
1396
- this.log(`🔍 Checking for override: ${requestedPath}`);
1397
- const resolver = this.getRegistryResolver(requestSessionId);
1398
- const hasOverride = resolver.hasLocalOverride(requestedPath);
1399
- this.log(`🔍 hasLocalOverride(${requestedPath}) = ${hasOverride}`);
1400
- if (hasOverride) {
1401
- this.log(`✅ Local override found: ${requestedPath}`);
1402
- const resolved = await resolver.resolveFile(requestedPath);
1403
- this.log(`📝 Override resolved (source: ${resolved.source}, inherited: ${resolved.inherited})`);
1404
- // Build MCP response with resolved content
1405
- const response = {
1406
- jsonrpc: '2.0',
1407
- id: request.id,
1408
- result: {
1409
- content: [
1410
- {
1411
- type: 'text',
1412
- text: resolved.content
1413
- }
1414
- ]
1415
- }
1416
- };
1417
- // Apply template substitution
1418
- const processedResponse = await this.finalizeToolResponse(request, response, requestSessionId);
1419
- this.log(`📤 ${request.method} → OK`);
1420
- return processedResponse;
1421
- }
1422
- }
1402
+ // Force ALL tools/call requests to return raw definitions so the proxy
1403
+ // can resolve templates and replace includes locally
1404
+ const toolName = request.params?.name;
1405
+ let injectedRequest = request;
1406
+ if (request.method === 'tools/call' && typeof toolName === 'string') {
1407
+ const args = request.params.arguments || {};
1408
+ // 🔍 SMART DISPATCHER: Intercept mentoring and job/workflow tools for local overrides
1409
+ if (toolName === 'seekMentoring') {
1410
+ try {
1411
+ const mentor = this.getMentor(requestSessionId);
1412
+ const tutoringResponse = await mentor.handleMentoringRequest(args);
1413
+ this.log(`✅ Local seekMentoring succeeded for ${args.workflowType}:${args.currentPhase}`);
1414
+ return await this.finalizeLocalToolTextResponse(request, requestSessionId, requestId, tutoringResponse.message);
1423
1415
  }
1424
- else if (toolName === 'get_fraim_job') {
1425
- const jobName = args.job;
1426
- if (!jobName) {
1427
- this.log('⚠️ No job name provided in get_fraim_job');
1428
- }
1429
- else {
1430
- // Determine job path by scanning local override roots.
1431
- requestedPath = this.findJobOverridePath(jobName) || `jobs/product-building/${jobName}.md`;
1432
- this.log(`🔍 Checking for override: ${requestedPath}`);
1433
- const resolver = this.getRegistryResolver(requestSessionId);
1434
- const hasOverride = resolver.hasLocalOverride(requestedPath);
1435
- this.log(`🔍 hasLocalOverride(${requestedPath}) = ${hasOverride}`);
1436
- if (hasOverride) {
1437
- this.log(`✅ Local override found: ${requestedPath}`);
1438
- const resolved = await resolver.resolveFile(requestedPath);
1439
- this.log(`📝 Override resolved (source: ${resolved.source}, inherited: ${resolved.inherited})`);
1440
- const response = {
1441
- jsonrpc: '2.0',
1442
- id: request.id,
1443
- result: {
1444
- content: [
1445
- {
1446
- type: 'text',
1447
- text: resolved.content
1448
- }
1449
- ]
1450
- }
1451
- };
1452
- const processedResponse = await this.finalizeToolResponse(request, response, requestSessionId);
1453
- this.log(`📤 ${request.method} → OK`);
1454
- return processedResponse;
1416
+ catch (error) {
1417
+ this.log(`⚠️ Local seekMentoring failed: ${error.message}. Falling back to remote.`);
1418
+ // If local fails, we continue to proxy to the remote server
1419
+ }
1420
+ }
1421
+ if (toolName === 'get_fraim_job' || toolName === 'get_fraim_workflow') {
1422
+ const isJob = toolName === 'get_fraim_job';
1423
+ const name = isJob ? args.job : args.workflow;
1424
+ if (name) {
1425
+ try {
1426
+ const mentor = this.getMentor(requestSessionId);
1427
+ const overview = isJob
1428
+ ? await mentor.getJobOverview(name)
1429
+ : await mentor.getWorkflowOverview(name);
1430
+ if (overview) {
1431
+ this.log(`✅ Local override found for ${toolName}: ${name}`);
1432
+ let responseText = overview.overview;
1433
+ if (!overview.isSimple) {
1434
+ const phaseAuthority = await mentor.getPhaseAuthorityContent();
1435
+ if (phaseAuthority)
1436
+ responseText = `${phaseAuthority}\n\n---\n\n${responseText}`;
1437
+ responseText += `\n\n---\n\n**This ${isJob ? 'job' : 'workflow'} has phases.** Use \`seekMentoring\` to get phase-specific instructions.`;
1438
+ }
1439
+ return await this.finalizeLocalToolTextResponse(request, requestSessionId, requestId, responseText);
1455
1440
  }
1456
1441
  }
1457
- }
1458
- else if (toolName === 'get_fraim_file') {
1459
- requestedPath = args.path;
1460
- if (!requestedPath) {
1461
- this.log('⚠️ No path provided in get_fraim_file');
1442
+ catch (error) {
1443
+ this.log(`⚠️ Local ${toolName} failed for ${name}: ${error.message}. Falling back to remote.`);
1462
1444
  }
1463
- else {
1464
- if (requestedPath.startsWith('providers/')) {
1465
- // Server-authoritative templates: never resolve provider files via local overrides.
1466
- this.log(`🔒 Skipping local override for provider template file: ${requestedPath}`);
1467
- }
1468
- else {
1469
- this.log(`🔍 Checking for override: ${requestedPath}`);
1470
- const resolver = this.getRegistryResolver(requestSessionId);
1471
- const isLocalFirstSyncedPath = requestedPath.startsWith('skills/') || requestedPath.startsWith('rules/');
1472
- if (isLocalFirstSyncedPath && resolver.hasSyncedLocalFile(requestedPath)) {
1473
- this.log(`✅ Synced local file found: ${requestedPath}`);
1474
- const resolved = await resolver.resolveFile(requestedPath, { includeMetadata: false });
1475
- this.log(`📝 Synced local file resolved (source: ${resolved.source}, inherited: ${resolved.inherited})`);
1476
- const response = {
1477
- jsonrpc: '2.0',
1478
- id: request.id,
1479
- result: {
1480
- content: [
1481
- {
1482
- type: 'text',
1483
- text: resolved.content
1484
- }
1485
- ]
1486
- }
1487
- };
1488
- const processedResponse = await this.finalizeToolResponse(request, response, requestSessionId);
1489
- this.log(`📤 ${request.method} → OK`);
1490
- return processedResponse;
1491
- }
1492
- const hasOverride = resolver.hasLocalOverride(requestedPath);
1493
- this.log(`🔍 hasLocalOverride(${requestedPath}) = ${hasOverride}`);
1494
- if (hasOverride) {
1495
- this.log(`✅ Local override found: ${requestedPath}`);
1496
- const resolved = await resolver.resolveFile(requestedPath);
1497
- this.log(`📝 Override resolved (source: ${resolved.source}, inherited: ${resolved.inherited})`);
1498
- // Build MCP response with resolved content
1499
- const response = {
1500
- jsonrpc: '2.0',
1501
- id: request.id,
1502
- result: {
1503
- content: [
1504
- {
1505
- type: 'text',
1506
- text: resolved.content
1507
- }
1508
- ]
1509
- }
1510
- };
1511
- // Apply template substitution
1512
- const processedResponse = await this.finalizeToolResponse(request, response, requestSessionId);
1513
- this.log(`📤 ${request.method} → OK`);
1514
- return processedResponse;
1445
+ }
1446
+ }
1447
+ // DISCOVERY AGGREGATION: Merge local and remote jobs/workflows
1448
+ if (toolName === 'list_fraim_jobs' || toolName === 'list_fraim_workflows') {
1449
+ const isJob = toolName === 'list_fraim_jobs';
1450
+ const response = await this._doProxyToRemote(request, requestId);
1451
+ if (!response.error && response.result?.content?.[0]?.text) {
1452
+ try {
1453
+ const resolver = this.getRegistryResolver(requestSessionId);
1454
+ const localItems = await resolver.listItems(isJob ? 'job' : 'workflow');
1455
+ if (localItems.length > 0) {
1456
+ this.log(`📦 Aggregating ${localItems.length} local ${isJob ? 'jobs' : 'workflows'} into remote response`);
1457
+ let combinedText = response.result.content[0].text;
1458
+ combinedText += `\n\n## Local & Personalized ${isJob ? 'Jobs' : 'Workflows'} (.fraim/)\n\n`;
1459
+ for (const item of localItems) {
1460
+ combinedText += `- **${item.name}**: ${item.description || '(No description available)'}\n`;
1515
1461
  }
1462
+ response.result.content[0].text = combinedText;
1516
1463
  }
1517
1464
  }
1465
+ catch (error) {
1466
+ this.log(`⚠️ Discovery aggregation failed: ${error.message}`);
1467
+ }
1518
1468
  }
1469
+ return response;
1519
1470
  }
1520
- catch (error) {
1521
- this.logError(`Override resolution failed: ${error.message}`);
1522
- this.log('⚠️ Falling back to remote');
1523
- // Fall through to proxy to remote
1524
- }
1471
+ // Normal path for other tools: inject raw:true
1472
+ injectedRequest = {
1473
+ ...request,
1474
+ params: {
1475
+ ...request.params,
1476
+ arguments: {
1477
+ ...args,
1478
+ raw: true
1479
+ }
1480
+ }
1481
+ };
1525
1482
  }
1526
- // Proxy to remote server
1527
- const response = await this.proxyToRemote(request);
1528
- const processedResponse = await this.finalizeToolResponse(request, response, requestSessionId);
1529
- this.log(`📤 ${request.method} → ${processedResponse.error ? 'ERROR' : 'OK'}`);
1483
+ const response = await this._doProxyToRemote(injectedRequest, requestId);
1484
+ const processedResponse = await this.finalizeToolResponse(injectedRequest, response, requestSessionId, requestId);
1485
+ this.log(`📤 ${injectedRequest.method} → ${processedResponse.error ? 'ERROR' : 'OK'}`);
1530
1486
  return processedResponse;
1531
1487
  }
1532
1488
  /**
@@ -1549,23 +1505,23 @@ class FraimLocalMCPServer {
1549
1505
  const firstRoot = roots[0];
1550
1506
  if (firstRoot.uri && firstRoot.uri.startsWith('file://')) {
1551
1507
  const rootPath = this.fileUriToLocalPath(firstRoot.uri);
1552
- if (!rootPath) {
1508
+ if (rootPath) {
1509
+ this.workspaceRoot = rootPath;
1510
+ this.log(`✅ Got workspace root from client: ${this.workspaceRoot} (${firstRoot.name || 'unknown'})`);
1511
+ this.loadConfig(); // Reload config with the correct workspace root
1512
+ }
1513
+ else {
1553
1514
  this.log(`⚠️ Could not parse root URI: ${firstRoot.uri}`);
1554
1515
  this.loadConfig();
1555
- return;
1556
1516
  }
1557
- this.log(`✅ Got workspace root from client: ${rootPath} (${firstRoot.name || 'unnamed'})`);
1558
- this.workspaceRoot = rootPath;
1559
- // Now load config with the correct workspace root
1560
- this.loadConfig();
1561
1517
  }
1562
1518
  else {
1563
- this.log(`⚠️ Root URI is not a file:// URI: ${firstRoot.uri}`);
1519
+ this.log(`⚠️ Client returned invalid root URI: ${firstRoot.uri}`);
1564
1520
  this.loadConfig();
1565
1521
  }
1566
1522
  }
1567
1523
  else {
1568
- this.log('⚠️ No roots provided by client');
1524
+ this.log(`⚠️ Client returned empty or invalid roots array`);
1569
1525
  this.loadConfig();
1570
1526
  }
1571
1527
  }