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.
- package/README.md +30 -0
- package/bin/fraim.js +1 -1
- package/dist/src/cli/commands/add-provider.js +16 -6
- package/dist/src/cli/commands/init-project.js +103 -1
- package/dist/src/cli/commands/login.js +84 -0
- package/dist/src/cli/commands/setup.js +135 -13
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/internal/device-flow-service.js +83 -0
- package/dist/src/cli/mcp/mcp-server-registry.js +11 -10
- package/dist/src/cli/providers/local-provider-registry.js +22 -1
- package/dist/src/cli/setup/provider-prompts.js +39 -0
- package/dist/src/cli/utils/remote-sync.js +72 -32
- package/dist/src/core/ai-mentor.js +248 -0
- package/dist/src/core/utils/git-utils.js +6 -6
- package/dist/src/core/utils/include-resolver.js +45 -0
- package/dist/src/core/utils/inheritance-parser.js +154 -16
- package/dist/src/core/utils/local-registry-resolver.js +326 -22
- package/dist/src/core/utils/server-startup.js +34 -0
- package/dist/src/core/utils/stub-generator.js +62 -55
- package/dist/src/core/utils/workflow-parser.js +103 -46
- package/dist/src/local-mcp-server/stdio-server.js +240 -284
- package/index.js +27 -6
- package/package.json +14 -5
|
@@ -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
|
-
//
|
|
452
|
-
|
|
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: {
|
|
660
|
+
arguments: {
|
|
661
|
+
path,
|
|
662
|
+
raw: true
|
|
663
|
+
}
|
|
667
664
|
}
|
|
668
665
|
};
|
|
669
666
|
this.applyRequestSessionId(request, requestSessionId);
|
|
670
|
-
const response = await this.
|
|
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
|
-
|
|
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
|
-
|
|
828
|
-
|
|
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
|
-
|
|
961
|
+
raw: true
|
|
913
962
|
}
|
|
914
963
|
}
|
|
915
964
|
};
|
|
916
965
|
this.applyRequestSessionId(request, requestSessionId);
|
|
917
|
-
const response = await this.
|
|
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
|
-
*
|
|
987
|
+
* Check if content is a "stub" indicating local version should be ignored.
|
|
939
988
|
*/
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
'
|
|
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
|
-
|
|
971
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1143
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
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
|
-
//
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
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
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
const
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
this.log(
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
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
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
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
|
-
|
|
1527
|
-
const
|
|
1528
|
-
|
|
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 (
|
|
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(`⚠️
|
|
1519
|
+
this.log(`⚠️ Client returned invalid root URI: ${firstRoot.uri}`);
|
|
1564
1520
|
this.loadConfig();
|
|
1565
1521
|
}
|
|
1566
1522
|
}
|
|
1567
1523
|
else {
|
|
1568
|
-
this.log(
|
|
1524
|
+
this.log(`⚠️ Client returned empty or invalid roots array`);
|
|
1569
1525
|
this.loadConfig();
|
|
1570
1526
|
}
|
|
1571
1527
|
}
|