fraim-framework 2.0.83 → 2.0.85
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 +16 -3
- package/bin/fraim-mcp.js +1 -1
- package/dist/src/cli/commands/add-ide.js +1 -1
- package/dist/src/cli/commands/init-project.js +1 -1
- package/dist/src/cli/commands/list-overridable.js +19 -15
- package/dist/src/cli/commands/override.js +9 -2
- package/dist/src/cli/commands/setup.js +44 -7
- package/dist/src/cli/commands/sync.js +34 -23
- package/dist/src/cli/doctor/checks/workflow-checks.js +12 -4
- package/dist/src/cli/fraim.js +0 -2
- package/dist/src/cli/mcp/mcp-server-registry.js +2 -0
- package/dist/src/cli/setup/auto-mcp-setup.js +12 -1
- package/dist/src/cli/utils/remote-sync.js +82 -21
- package/dist/src/core/utils/local-registry-resolver.js +171 -14
- package/dist/src/core/utils/stub-generator.js +139 -0
- package/dist/src/core/utils/workflow-parser.js +5 -1
- package/dist/src/local-mcp-server/stdio-server.js +174 -63
- package/index.js +1 -1
- package/package.json +6 -3
|
@@ -227,10 +227,11 @@ class FraimLocalMCPServer {
|
|
|
227
227
|
this.pendingFallbackEvents = [];
|
|
228
228
|
this.fallbackSummaryEmitted = false;
|
|
229
229
|
this.remoteUrl = process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
|
|
230
|
-
this.apiKey =
|
|
230
|
+
this.apiKey = this.loadApiKey();
|
|
231
231
|
this.localVersion = this.detectLocalVersion();
|
|
232
232
|
if (!this.apiKey) {
|
|
233
|
-
this.logError('❌
|
|
233
|
+
this.logError('❌ FRAIM API key is required');
|
|
234
|
+
this.logError(' Set FRAIM_API_KEY environment variable or add apiKey to ~/.fraim/config.json');
|
|
234
235
|
process.exit(1);
|
|
235
236
|
}
|
|
236
237
|
this.log('🚀 FRAIM Local MCP Server starting... [DEBUG-PROXY-V3]');
|
|
@@ -239,6 +240,33 @@ class FraimLocalMCPServer {
|
|
|
239
240
|
this.log(`Local MCP version: ${this.localVersion}`);
|
|
240
241
|
this.log(`🔍 DEBUG BUILD: Machine detection v2 active`);
|
|
241
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Load API key from environment variable or user config file
|
|
245
|
+
* Priority: FRAIM_API_KEY env var > ~/.fraim/config.json
|
|
246
|
+
*/
|
|
247
|
+
loadApiKey() {
|
|
248
|
+
// First try environment variable (for IDE MCP configs)
|
|
249
|
+
if (process.env.FRAIM_API_KEY) {
|
|
250
|
+
return process.env.FRAIM_API_KEY;
|
|
251
|
+
}
|
|
252
|
+
// Fallback to user config file
|
|
253
|
+
try {
|
|
254
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
255
|
+
if (!homeDir)
|
|
256
|
+
return '';
|
|
257
|
+
const configPath = (0, path_1.join)(homeDir, '.fraim', 'config.json');
|
|
258
|
+
if ((0, fs_1.existsSync)(configPath)) {
|
|
259
|
+
const config = JSON.parse((0, fs_1.readFileSync)(configPath, 'utf8'));
|
|
260
|
+
if (config.apiKey) {
|
|
261
|
+
return config.apiKey;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
// Ignore errors, will fail with clear message below
|
|
267
|
+
}
|
|
268
|
+
return '';
|
|
269
|
+
}
|
|
242
270
|
log(message) {
|
|
243
271
|
// Log to stderr (stdout is reserved for MCP protocol)
|
|
244
272
|
const key = this.apiKey || 'MISSING_API_KEY';
|
|
@@ -736,37 +764,9 @@ class FraimLocalMCPServer {
|
|
|
736
764
|
if (parsed) {
|
|
737
765
|
engine.setProviderTemplates(provider, parsed);
|
|
738
766
|
this.writeCachedTemplateFile(filename, parsed);
|
|
739
|
-
continue;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
const bundled = this.readBundledTemplateFile(filename);
|
|
743
|
-
if (bundled) {
|
|
744
|
-
engine.setProviderTemplates(provider, bundled);
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
readBundledTemplateFile(filename) {
|
|
749
|
-
const candidates = [
|
|
750
|
-
(0, path_1.join)(__dirname, '..', '..', '..', 'registry', 'providers', filename),
|
|
751
|
-
(0, path_1.join)(__dirname, '..', '..', 'registry', 'providers', filename),
|
|
752
|
-
(0, path_1.join)(process.cwd(), 'registry', 'providers', filename)
|
|
753
|
-
];
|
|
754
|
-
for (const candidate of candidates) {
|
|
755
|
-
try {
|
|
756
|
-
if (!(0, fs_1.existsSync)(candidate))
|
|
757
|
-
continue;
|
|
758
|
-
const content = (0, fs_1.readFileSync)(candidate, 'utf8');
|
|
759
|
-
const parsed = JSON.parse(content);
|
|
760
|
-
if (parsed && typeof parsed === 'object') {
|
|
761
|
-
this.log(`✅ Loaded bundled provider template: ${candidate}`);
|
|
762
|
-
return parsed;
|
|
763
767
|
}
|
|
764
768
|
}
|
|
765
|
-
catch (error) {
|
|
766
|
-
this.log(`⚠️ Failed to load bundled template ${candidate}: ${error.message}`);
|
|
767
|
-
}
|
|
768
769
|
}
|
|
769
|
-
return null;
|
|
770
770
|
}
|
|
771
771
|
async hydrateTemplateCachesForResponse(response, requestSessionId) {
|
|
772
772
|
if (!response.result)
|
|
@@ -790,6 +790,45 @@ class FraimLocalMCPServer {
|
|
|
790
790
|
}
|
|
791
791
|
return this.applyAgentFallbackForUnresolvedProxy(processedResponse);
|
|
792
792
|
}
|
|
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) {
|
|
800
|
+
if (!response.result?.content || !Array.isArray(response.result.content)) {
|
|
801
|
+
return response;
|
|
802
|
+
}
|
|
803
|
+
const resolver = this.getRegistryResolver(requestSessionId);
|
|
804
|
+
const transformedContent = [];
|
|
805
|
+
for (const block of response.result.content) {
|
|
806
|
+
if (block?.type !== 'text' || typeof block.text !== 'string') {
|
|
807
|
+
transformedContent.push(block);
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
const resolvedText = await resolver.resolveIncludes(block.text);
|
|
811
|
+
transformedContent.push({
|
|
812
|
+
...block,
|
|
813
|
+
text: resolvedText
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
return {
|
|
817
|
+
...response,
|
|
818
|
+
result: {
|
|
819
|
+
...response.result,
|
|
820
|
+
content: transformedContent
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
async finalizeToolResponse(request, response, requestSessionId) {
|
|
825
|
+
let finalizedResponse = response;
|
|
826
|
+
const toolName = request.params?.name;
|
|
827
|
+
if (request.method === 'tools/call' && typeof toolName === 'string' && this.shouldResolveIncludes(toolName)) {
|
|
828
|
+
finalizedResponse = await this.resolveIncludesInResponse(finalizedResponse, requestSessionId);
|
|
829
|
+
}
|
|
830
|
+
return this.processResponseWithHydration(finalizedResponse, requestSessionId);
|
|
831
|
+
}
|
|
793
832
|
rewriteProxyTokensInText(text) {
|
|
794
833
|
const tokens = new Set();
|
|
795
834
|
const rewritten = text.replace(/\{\{\s*proxy\.([^}]+?)\s*\}\}/g, (_match, proxyPath) => {
|
|
@@ -861,35 +900,19 @@ class FraimLocalMCPServer {
|
|
|
861
900
|
remoteContentResolver: async (path) => {
|
|
862
901
|
// Fetch parent content from remote for inheritance
|
|
863
902
|
this.log(`🔄 Remote content resolver: fetching ${path}`);
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
params: {
|
|
875
|
-
name: 'get_fraim_workflow',
|
|
876
|
-
arguments: { workflow: workflowName }
|
|
877
|
-
}
|
|
878
|
-
};
|
|
879
|
-
}
|
|
880
|
-
else {
|
|
881
|
-
// For non-workflow files (templates, rules, etc.), use get_fraim_file
|
|
882
|
-
this.log(`🔄 Fetching file: ${path}`);
|
|
883
|
-
request = {
|
|
884
|
-
jsonrpc: '2.0',
|
|
885
|
-
id: (0, crypto_1.randomUUID)(),
|
|
886
|
-
method: 'tools/call',
|
|
887
|
-
params: {
|
|
888
|
-
name: 'get_fraim_file',
|
|
889
|
-
arguments: { path }
|
|
903
|
+
this.log(`🔄 Fetching raw file content: ${path}`);
|
|
904
|
+
const request = {
|
|
905
|
+
jsonrpc: '2.0',
|
|
906
|
+
id: (0, crypto_1.randomUUID)(),
|
|
907
|
+
method: 'tools/call',
|
|
908
|
+
params: {
|
|
909
|
+
name: 'get_fraim_file',
|
|
910
|
+
arguments: {
|
|
911
|
+
path,
|
|
912
|
+
_internalRaw: true
|
|
890
913
|
}
|
|
891
|
-
}
|
|
892
|
-
}
|
|
914
|
+
}
|
|
915
|
+
};
|
|
893
916
|
this.applyRequestSessionId(request, requestSessionId);
|
|
894
917
|
const response = await this.proxyToRemote(request);
|
|
895
918
|
if (response.error) {
|
|
@@ -943,6 +966,38 @@ class FraimLocalMCPServer {
|
|
|
943
966
|
// Default to product-building for unknown workflows
|
|
944
967
|
return 'product-building';
|
|
945
968
|
}
|
|
969
|
+
/**
|
|
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.
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
946
1001
|
/**
|
|
947
1002
|
* Process template substitution in MCP response
|
|
948
1003
|
*/
|
|
@@ -1305,7 +1360,7 @@ class FraimLocalMCPServer {
|
|
|
1305
1360
|
}
|
|
1306
1361
|
// Proxy initialize to remote server first
|
|
1307
1362
|
const response = await this.proxyToRemote(request);
|
|
1308
|
-
const processedResponse = await this.
|
|
1363
|
+
const processedResponse = await this.finalizeToolResponse(request, response, requestSessionId);
|
|
1309
1364
|
// After successful initialization, load config
|
|
1310
1365
|
if (!processedResponse.error) {
|
|
1311
1366
|
// Load config immediately for compatibility, then request roots so
|
|
@@ -1318,9 +1373,10 @@ class FraimLocalMCPServer {
|
|
|
1318
1373
|
this.log(`📤 ${request.method} → ${processedResponse.error ? 'ERROR' : 'OK'}`);
|
|
1319
1374
|
return processedResponse;
|
|
1320
1375
|
}
|
|
1321
|
-
// Intercept get_fraim_workflow and get_fraim_file for override resolution
|
|
1376
|
+
// Intercept get_fraim_workflow, get_fraim_job, and get_fraim_file for override resolution
|
|
1322
1377
|
if (request.method === 'tools/call' &&
|
|
1323
1378
|
(request.params?.name === 'get_fraim_workflow' ||
|
|
1379
|
+
request.params?.name === 'get_fraim_job' ||
|
|
1324
1380
|
request.params?.name === 'get_fraim_file')) {
|
|
1325
1381
|
try {
|
|
1326
1382
|
const toolName = request.params.name;
|
|
@@ -1359,7 +1415,41 @@ class FraimLocalMCPServer {
|
|
|
1359
1415
|
}
|
|
1360
1416
|
};
|
|
1361
1417
|
// Apply template substitution
|
|
1362
|
-
const processedResponse = await this.
|
|
1418
|
+
const processedResponse = await this.finalizeToolResponse(request, response, requestSessionId);
|
|
1419
|
+
this.log(`📤 ${request.method} → OK`);
|
|
1420
|
+
return processedResponse;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
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);
|
|
1363
1453
|
this.log(`📤 ${request.method} → OK`);
|
|
1364
1454
|
return processedResponse;
|
|
1365
1455
|
}
|
|
@@ -1378,6 +1468,27 @@ class FraimLocalMCPServer {
|
|
|
1378
1468
|
else {
|
|
1379
1469
|
this.log(`🔍 Checking for override: ${requestedPath}`);
|
|
1380
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
|
+
}
|
|
1381
1492
|
const hasOverride = resolver.hasLocalOverride(requestedPath);
|
|
1382
1493
|
this.log(`🔍 hasLocalOverride(${requestedPath}) = ${hasOverride}`);
|
|
1383
1494
|
if (hasOverride) {
|
|
@@ -1398,7 +1509,7 @@ class FraimLocalMCPServer {
|
|
|
1398
1509
|
}
|
|
1399
1510
|
};
|
|
1400
1511
|
// Apply template substitution
|
|
1401
|
-
const processedResponse = await this.
|
|
1512
|
+
const processedResponse = await this.finalizeToolResponse(request, response, requestSessionId);
|
|
1402
1513
|
this.log(`📤 ${request.method} → OK`);
|
|
1403
1514
|
return processedResponse;
|
|
1404
1515
|
}
|
|
@@ -1414,7 +1525,7 @@ class FraimLocalMCPServer {
|
|
|
1414
1525
|
}
|
|
1415
1526
|
// Proxy to remote server
|
|
1416
1527
|
const response = await this.proxyToRemote(request);
|
|
1417
|
-
const processedResponse = await this.
|
|
1528
|
+
const processedResponse = await this.finalizeToolResponse(request, response, requestSessionId);
|
|
1418
1529
|
this.log(`📤 ${request.method} → ${processedResponse.error ? 'ERROR' : 'OK'}`);
|
|
1419
1530
|
return processedResponse;
|
|
1420
1531
|
}
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-framework",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.85",
|
|
4
4
|
"description": "FRAIM v2: Framework for Rigor-based AI Management - Transform from solo developer to AI manager orchestrating production-ready code with enterprise-grade discipline",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"test-all": "npm run test && npm run test:isolated && npm run test:ui",
|
|
17
17
|
"test": "node scripts/test-with-server.js",
|
|
18
18
|
"test:isolated": "npx tsx --test --test-reporter=spec tests/isolated/test-*.ts",
|
|
19
|
+
"test:smoke": "node scripts/test-with-server.js tests/test-*.ts --tags=smoke",
|
|
19
20
|
"test:ui": "playwright test",
|
|
20
21
|
"test:ui:headed": "playwright test --headed",
|
|
21
22
|
"start:fraim": "tsx src/fraim-mcp-server.ts",
|
|
@@ -29,14 +30,16 @@
|
|
|
29
30
|
"postinstall": "fraim sync --skip-updates || echo 'FRAIM setup skipped.'",
|
|
30
31
|
"prepublishOnly": "npm run build",
|
|
31
32
|
"release": "npm version patch && npm publish",
|
|
32
|
-
"validate:registry": "tsx scripts/verify-registry-paths.ts && npm run validate:workflows && npm run validate:skills && npm run validate:platform-agnostic && npm run validate:template-namespaces && npm run validate:config-fallbacks && npm run validate:bootstrap-config-coverage && npm run validate:provider-action-mappings",
|
|
33
|
+
"validate:registry": "tsx scripts/verify-registry-paths.ts && npm run validate:workflows && npm run validate:skills && npm run validate:platform-agnostic && npm run validate:template-namespaces && npm run validate:config-fallbacks && npm run validate:bootstrap-config-coverage && npm run validate:provider-action-mappings && npm run validate:fidelity && npm run validate:config-tokens",
|
|
33
34
|
"validate:workflows": "tsx scripts/validate-workflows.ts",
|
|
34
35
|
"validate:platform-agnostic": "tsx scripts/validate-platform-agnostic.ts",
|
|
35
36
|
"validate:skills": "tsx scripts/validate-skills.ts",
|
|
36
37
|
"validate:template-namespaces": "tsx scripts/validate-template-namespaces.ts",
|
|
37
38
|
"validate:config-fallbacks": "tsx scripts/validate-config-fallbacks.ts",
|
|
38
39
|
"validate:bootstrap-config-coverage": "tsx scripts/validate-bootstrap-config-coverage.ts",
|
|
39
|
-
"validate:provider-action-mappings": "tsx scripts/validate-provider-action-mappings.ts"
|
|
40
|
+
"validate:provider-action-mappings": "tsx scripts/validate-provider-action-mappings.ts",
|
|
41
|
+
"validate:fidelity": "tsx scripts/validate-fidelity.ts",
|
|
42
|
+
"validate:config-tokens": "tsx scripts/validate-config-tokens.ts"
|
|
40
43
|
},
|
|
41
44
|
"repository": {
|
|
42
45
|
"type": "git",
|