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.
@@ -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 = process.env.FRAIM_API_KEY || '';
230
+ this.apiKey = this.loadApiKey();
231
231
  this.localVersion = this.detectLocalVersion();
232
232
  if (!this.apiKey) {
233
- this.logError('❌ FRAIM_API_KEY environment variable is required');
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
- let request;
865
- if (path.startsWith('workflows/')) {
866
- // Extract workflow name from path: workflows/category/name.md -> name
867
- const pathParts = path.replace('workflows/', '').replace('.md', '').split('/');
868
- const workflowName = pathParts[pathParts.length - 1]; // Get last part (name)
869
- this.log(`🔄 Fetching workflow: ${workflowName}`);
870
- request = {
871
- jsonrpc: '2.0',
872
- id: (0, crypto_1.randomUUID)(),
873
- method: 'tools/call',
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.processResponseWithHydration(response, requestSessionId);
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.processResponseWithHydration(response, requestSessionId);
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.processResponseWithHydration(response, requestSessionId);
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.processResponseWithHydration(response, requestSessionId);
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
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * FRAIM Framework - Smart Entry Point
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-framework",
3
- "version": "2.0.83",
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",