fraim-framework 2.0.153 → 2.0.159
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/dist/src/ai-hub/hosts.js +135 -8
- package/dist/src/ai-hub/server.js +201 -1
- package/dist/src/cli/commands/init-project.js +50 -36
- package/dist/src/cli/commands/sync.js +22 -1
- package/dist/src/cli/setup/ide-invocation-surfaces.js +2 -2
- package/dist/src/cli/utils/github-workflow-sync.js +231 -0
- package/dist/src/cli/utils/managed-agent-paths.js +1 -1
- package/dist/src/cli/utils/project-bootstrap.js +6 -3
- package/dist/src/core/ai-mentor.js +46 -37
- package/dist/src/core/config-loader.js +68 -0
- package/dist/src/core/fraim-config-schema.generated.js +267 -1
- package/dist/src/core/utils/fraim-labels.js +182 -0
- package/dist/src/core/utils/git-utils.js +22 -1
- package/dist/src/core/utils/project-fraim-paths.js +58 -0
- package/dist/src/first-run/types.js +1 -1
- package/dist/src/local-mcp-server/learning-context-builder.js +77 -52
- package/dist/src/local-mcp-server/stdio-server.js +212 -13
- package/package.json +8 -3
- package/public/ai-hub/index.html +271 -229
- package/public/ai-hub/script.js +879 -527
- package/public/ai-hub/styles.css +877 -694
- package/public/first-run/index.html +35 -35
- package/public/first-run/script.js +667 -667
|
@@ -11,14 +11,37 @@ exports.buildLearningContextSection = buildLearningContextSection;
|
|
|
11
11
|
const fs_1 = require("fs");
|
|
12
12
|
const path_1 = require("path");
|
|
13
13
|
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
14
|
-
const
|
|
14
|
+
const REPO_LEARNINGS_REL = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('personalized-employee/learnings').replace(/\/$/, '');
|
|
15
15
|
const DEFAULT_THRESHOLD = 3.0;
|
|
16
16
|
const AGING_HORIZON_DAYS = 7;
|
|
17
17
|
const MAX_ENTRIES_SCANNED = 200;
|
|
18
18
|
const BACKLOG_MIN = 5;
|
|
19
19
|
const OLDEST_AGE_DAYS_TRIGGER = 3;
|
|
20
|
-
function
|
|
21
|
-
return
|
|
20
|
+
function getLearningRoots(workspaceRoot) {
|
|
21
|
+
return {
|
|
22
|
+
globalPersonalBase: (0, project_fraim_paths_1.getConfiguredPortableLearningsDir)(workspaceRoot),
|
|
23
|
+
globalPersonalDisplayBase: (0, project_fraim_paths_1.getConfiguredPortableLearningsDisplayPath)(workspaceRoot),
|
|
24
|
+
repoLearningsBase: (0, project_fraim_paths_1.getWorkspaceLearningsDir)(workspaceRoot)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function resolvePersonalLearningFile(repoBase, globalBase, globalDisplayBase, fileName) {
|
|
28
|
+
const repoPath = (0, path_1.join)(repoBase, fileName);
|
|
29
|
+
if ((0, fs_1.existsSync)(repoPath)) {
|
|
30
|
+
return {
|
|
31
|
+
present: true,
|
|
32
|
+
path: repoPath,
|
|
33
|
+
displayPath: `${REPO_LEARNINGS_REL}/${fileName}`
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const globalPath = (0, path_1.join)(globalBase, fileName);
|
|
37
|
+
if ((0, fs_1.existsSync)(globalPath)) {
|
|
38
|
+
return {
|
|
39
|
+
present: true,
|
|
40
|
+
path: globalPath,
|
|
41
|
+
displayPath: `${globalDisplayBase.replace(/\/$/, '')}/${fileName}`
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return { present: false, path: globalPath, displayPath: `${globalDisplayBase.replace(/\/$/, '')}/${fileName}` };
|
|
22
45
|
}
|
|
23
46
|
function buildUserIdCandidates(userId) {
|
|
24
47
|
const candidates = new Set();
|
|
@@ -40,7 +63,7 @@ function countMatchingFilesByPrefix(dirPath, matcher) {
|
|
|
40
63
|
return 0;
|
|
41
64
|
}
|
|
42
65
|
}
|
|
43
|
-
function collectAvailableUserPrefixes(workspaceRoot,
|
|
66
|
+
function collectAvailableUserPrefixes(workspaceRoot, personalRoots) {
|
|
44
67
|
const prefixes = new Set();
|
|
45
68
|
const collect = (dirPath, extractor) => {
|
|
46
69
|
if (!(0, fs_1.existsSync)(dirPath))
|
|
@@ -56,13 +79,15 @@ function collectAvailableUserPrefixes(workspaceRoot, learningsBase) {
|
|
|
56
79
|
// Ignore unreadable directories.
|
|
57
80
|
}
|
|
58
81
|
};
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
82
|
+
for (const personalRoot of personalRoots) {
|
|
83
|
+
collect(personalRoot, (fileName) => {
|
|
84
|
+
if (!fileName.endsWith('.md') || fileName.startsWith('org-'))
|
|
85
|
+
return null;
|
|
86
|
+
const match = fileName.match(/^(.*?)-(preferences|manager-coaching|mistake-patterns|validated-patterns)\.md$/);
|
|
87
|
+
return match ? match[1] : null;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
collect((0, path_1.join)(personalRoots[0], 'raw'), (fileName) => {
|
|
66
91
|
const match = fileName.match(/^(.*?)-\d{4}-\d{2}-\d{2}-.*\.md$/);
|
|
67
92
|
return match ? match[1] : null;
|
|
68
93
|
});
|
|
@@ -72,16 +97,20 @@ function collectAvailableUserPrefixes(workspaceRoot, learningsBase) {
|
|
|
72
97
|
});
|
|
73
98
|
return prefixes;
|
|
74
99
|
}
|
|
75
|
-
function resolveLearningUserId(workspaceRoot, userId) {
|
|
76
|
-
const learningsBase = getLearningsBase(workspaceRoot);
|
|
100
|
+
function resolveLearningUserId(workspaceRoot, userId, roots) {
|
|
77
101
|
const candidates = buildUserIdCandidates(userId);
|
|
78
102
|
let bestCandidate = candidates[0] || userId;
|
|
79
103
|
let bestScore = -1;
|
|
80
104
|
for (const candidate of candidates) {
|
|
81
|
-
const score = ((0, fs_1.existsSync)((0, path_1.join)(
|
|
82
|
-
((0, fs_1.existsSync)((0, path_1.join)(
|
|
83
|
-
((0, fs_1.existsSync)((0, path_1.join)(
|
|
84
|
-
|
|
105
|
+
const score = ((0, fs_1.existsSync)((0, path_1.join)(roots.globalPersonalBase, `${candidate}-preferences.md`)) ? 1 : 0) +
|
|
106
|
+
((0, fs_1.existsSync)((0, path_1.join)(roots.globalPersonalBase, `${candidate}-manager-coaching.md`)) ? 1 : 0) +
|
|
107
|
+
((0, fs_1.existsSync)((0, path_1.join)(roots.globalPersonalBase, `${candidate}-mistake-patterns.md`)) ? 1 : 0) +
|
|
108
|
+
((0, fs_1.existsSync)((0, path_1.join)(roots.globalPersonalBase, `${candidate}-validated-patterns.md`)) ? 1 : 0) +
|
|
109
|
+
((0, fs_1.existsSync)((0, path_1.join)(roots.repoLearningsBase, `${candidate}-preferences.md`)) ? 1 : 0) +
|
|
110
|
+
((0, fs_1.existsSync)((0, path_1.join)(roots.repoLearningsBase, `${candidate}-manager-coaching.md`)) ? 1 : 0) +
|
|
111
|
+
((0, fs_1.existsSync)((0, path_1.join)(roots.repoLearningsBase, `${candidate}-mistake-patterns.md`)) ? 1 : 0) +
|
|
112
|
+
((0, fs_1.existsSync)((0, path_1.join)(roots.repoLearningsBase, `${candidate}-validated-patterns.md`)) ? 1 : 0) +
|
|
113
|
+
countMatchingFilesByPrefix((0, path_1.join)(roots.repoLearningsBase, 'raw'), (fileName) => fileName.startsWith(`${candidate}-`)) +
|
|
85
114
|
countMatchingFilesByPrefix((0, path_1.join)(workspaceRoot, 'docs', 'retrospectives'), (fileName) => fileName.startsWith(`${candidate}-`) && fileName.endsWith('.md'));
|
|
86
115
|
if (score > bestScore) {
|
|
87
116
|
bestCandidate = candidate;
|
|
@@ -90,7 +119,7 @@ function resolveLearningUserId(workspaceRoot, userId) {
|
|
|
90
119
|
}
|
|
91
120
|
if (bestScore > 0)
|
|
92
121
|
return bestCandidate;
|
|
93
|
-
const availablePrefixes = collectAvailableUserPrefixes(workspaceRoot,
|
|
122
|
+
const availablePrefixes = collectAvailableUserPrefixes(workspaceRoot, [roots.repoLearningsBase, roots.globalPersonalBase]);
|
|
94
123
|
if (availablePrefixes.size === 1) {
|
|
95
124
|
return Array.from(availablePrefixes)[0];
|
|
96
125
|
}
|
|
@@ -242,7 +271,7 @@ function isUnsynthesizedRetrospective(filePath) {
|
|
|
242
271
|
}
|
|
243
272
|
/** Oldest mtime-age in days across this user's L0 signals. 0 if none. */
|
|
244
273
|
function computeOldestL0AgeDays(workspaceRoot, userId) {
|
|
245
|
-
const learningsBase =
|
|
274
|
+
const learningsBase = (0, project_fraim_paths_1.getWorkspaceLearningsDir)(workspaceRoot);
|
|
246
275
|
const now = Date.now();
|
|
247
276
|
let oldest = 0;
|
|
248
277
|
const consider = (filePath) => {
|
|
@@ -287,31 +316,27 @@ function computeOldestL0AgeDays(workspaceRoot, userId) {
|
|
|
287
316
|
return oldest;
|
|
288
317
|
}
|
|
289
318
|
function buildLearningContextSection(workspaceRoot, userId, forJob) {
|
|
290
|
-
const
|
|
291
|
-
const resolvedUserId = resolveLearningUserId(workspaceRoot, userId);
|
|
319
|
+
const roots = getLearningRoots(workspaceRoot);
|
|
320
|
+
const resolvedUserId = resolveLearningUserId(workspaceRoot, userId, roots);
|
|
292
321
|
const threshold = getScoreThreshold(workspaceRoot);
|
|
293
|
-
const l2MistakePath = (0, path_1.join)(
|
|
294
|
-
const l2PrefPath = (0, path_1.join)(
|
|
295
|
-
const l2CoachPath = (0, path_1.join)(
|
|
296
|
-
const l2ValidatedPath = (0, path_1.join)(
|
|
322
|
+
const l2MistakePath = (0, path_1.join)(roots.repoLearningsBase, 'org-mistake-patterns.md');
|
|
323
|
+
const l2PrefPath = (0, path_1.join)(roots.repoLearningsBase, 'org-preferences.md');
|
|
324
|
+
const l2CoachPath = (0, path_1.join)(roots.repoLearningsBase, 'org-manager-coaching.md');
|
|
325
|
+
const l2ValidatedPath = (0, path_1.join)(roots.repoLearningsBase, 'org-validated-patterns.md');
|
|
297
326
|
const l2MistakePresent = (0, fs_1.existsSync)(l2MistakePath);
|
|
298
327
|
const l2PrefPresent = (0, fs_1.existsSync)(l2PrefPath);
|
|
299
328
|
const l2CoachPresent = (0, fs_1.existsSync)(l2CoachPath);
|
|
300
329
|
const l2ValidatedPresent = (0, fs_1.existsSync)(l2ValidatedPath);
|
|
301
330
|
const l2MistakeStats = l2MistakePresent ? scanMistakePatternFile(l2MistakePath, threshold, 'mistake-patterns') : null;
|
|
302
331
|
const l2ValidatedStats = l2ValidatedPresent ? scanMistakePatternFile(l2ValidatedPath, threshold, 'validated-patterns') : null;
|
|
303
|
-
const
|
|
304
|
-
const
|
|
305
|
-
const
|
|
306
|
-
const
|
|
307
|
-
const
|
|
308
|
-
const
|
|
309
|
-
const l1CoachPresent = (0, fs_1.existsSync)(l1CoachPath);
|
|
310
|
-
const l1ValidatedPresent = (0, fs_1.existsSync)(l1ValidatedPath);
|
|
311
|
-
const l1MistakeStats = l1MistakePresent ? scanMistakePatternFile(l1MistakePath, threshold, 'mistake-patterns') : null;
|
|
312
|
-
const l1ValidatedStats = l1ValidatedPresent ? scanMistakePatternFile(l1ValidatedPath, threshold, 'validated-patterns') : null;
|
|
332
|
+
const l1Mistake = resolvePersonalLearningFile(roots.repoLearningsBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-mistake-patterns.md`);
|
|
333
|
+
const l1Pref = resolvePersonalLearningFile(roots.repoLearningsBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-preferences.md`);
|
|
334
|
+
const l1Coach = resolvePersonalLearningFile(roots.repoLearningsBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-manager-coaching.md`);
|
|
335
|
+
const l1Validated = resolvePersonalLearningFile(roots.repoLearningsBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-validated-patterns.md`);
|
|
336
|
+
const l1MistakeStats = l1Mistake.present ? scanMistakePatternFile(l1Mistake.path, threshold, 'mistake-patterns') : null;
|
|
337
|
+
const l1ValidatedStats = l1Validated.present ? scanMistakePatternFile(l1Validated.path, threshold, 'validated-patterns') : null;
|
|
313
338
|
let l0CoachingCount = 0;
|
|
314
|
-
const rawPath = (0, path_1.join)(
|
|
339
|
+
const rawPath = (0, path_1.join)(roots.repoLearningsBase, 'raw');
|
|
315
340
|
if ((0, fs_1.existsSync)(rawPath)) {
|
|
316
341
|
try {
|
|
317
342
|
l0CoachingCount = (0, fs_1.readdirSync)(rawPath).filter(f => f.startsWith(`${resolvedUserId}-`)).length;
|
|
@@ -333,7 +358,7 @@ function buildLearningContextSection(workspaceRoot, userId, forJob) {
|
|
|
333
358
|
}
|
|
334
359
|
}
|
|
335
360
|
const hasL2 = l2MistakePresent || l2PrefPresent || l2CoachPresent || l2ValidatedPresent;
|
|
336
|
-
const hasL1 =
|
|
361
|
+
const hasL1 = l1Mistake.present || l1Pref.present || l1Coach.present || l1Validated.present;
|
|
337
362
|
const hasContent = hasL2 || hasL1 || l0CoachingCount > 0 || l0RetroCount > 0;
|
|
338
363
|
if (!hasContent)
|
|
339
364
|
return '';
|
|
@@ -343,13 +368,13 @@ function buildLearningContextSection(workspaceRoot, userId, forJob) {
|
|
|
343
368
|
if (hasL2) {
|
|
344
369
|
section += '### L2 - Org patterns\n';
|
|
345
370
|
if (l2MistakePresent)
|
|
346
|
-
section += `\`${
|
|
371
|
+
section += `\`${REPO_LEARNINGS_REL}/org-mistake-patterns.md\` (entries above score threshold)\n`;
|
|
347
372
|
if (l2PrefPresent)
|
|
348
|
-
section += `\`${
|
|
373
|
+
section += `\`${REPO_LEARNINGS_REL}/org-preferences.md\` (all entries)\n`;
|
|
349
374
|
if (l2CoachPresent)
|
|
350
|
-
section += `\`${
|
|
375
|
+
section += `\`${REPO_LEARNINGS_REL}/org-manager-coaching.md\` (manager-facing; all entries)\n`;
|
|
351
376
|
if (l2ValidatedPresent)
|
|
352
|
-
section += `\`${
|
|
377
|
+
section += `\`${REPO_LEARNINGS_REL}/org-validated-patterns.md\` (entries above score threshold)\n`;
|
|
353
378
|
const l2DormantTotal = (l2MistakeStats?.dormant || 0) + (l2ValidatedStats?.dormant || 0);
|
|
354
379
|
if (l2DormantTotal > 0) {
|
|
355
380
|
section += `Dormant: ${l2DormantTotal} org pattern${l2DormantTotal !== 1 ? 's' : ''} below threshold\n`;
|
|
@@ -358,14 +383,14 @@ function buildLearningContextSection(workspaceRoot, userId, forJob) {
|
|
|
358
383
|
}
|
|
359
384
|
if (hasL1) {
|
|
360
385
|
section += '### L1 - Your patterns\n';
|
|
361
|
-
if (
|
|
362
|
-
section += `\`${
|
|
363
|
-
if (
|
|
364
|
-
section += `\`${
|
|
365
|
-
if (
|
|
366
|
-
section += `\`${
|
|
367
|
-
if (
|
|
368
|
-
section += `\`${
|
|
386
|
+
if (l1Pref.present)
|
|
387
|
+
section += `\`${l1Pref.displayPath}\` (all entries)\n`;
|
|
388
|
+
if (l1Coach.present)
|
|
389
|
+
section += `\`${l1Coach.displayPath}\` (manager-facing; all entries)\n`;
|
|
390
|
+
if (l1Mistake.present)
|
|
391
|
+
section += `\`${l1Mistake.displayPath}\` (entries above score threshold)\n`;
|
|
392
|
+
if (l1Validated.present)
|
|
393
|
+
section += `\`${l1Validated.displayPath}\` (entries above score threshold)\n`;
|
|
369
394
|
const l1DormantTotal = (l1MistakeStats?.dormant || 0) + (l1ValidatedStats?.dormant || 0);
|
|
370
395
|
if (l1DormantTotal > 0) {
|
|
371
396
|
section += `Dormant: ${l1DormantTotal} personal pattern${l1DormantTotal !== 1 ? 's' : ''} below threshold\n`;
|
|
@@ -375,7 +400,7 @@ function buildLearningContextSection(workspaceRoot, userId, forJob) {
|
|
|
375
400
|
if (l0CoachingCount > 0 || l0RetroCount > 0) {
|
|
376
401
|
section += '### L0 - Your unprocessed signals\n';
|
|
377
402
|
if (l0CoachingCount > 0) {
|
|
378
|
-
section += `${l0CoachingCount} coaching moment${l0CoachingCount !== 1 ? 's' : ''} in \`${
|
|
403
|
+
section += `${l0CoachingCount} coaching moment${l0CoachingCount !== 1 ? 's' : ''} in \`${REPO_LEARNINGS_REL}/raw/${resolvedUserId}-*\`\n`;
|
|
379
404
|
}
|
|
380
405
|
if (l0RetroCount > 0) {
|
|
381
406
|
section += `${l0RetroCount} retrospective${l0RetroCount !== 1 ? 's' : ''} in \`docs/retrospectives/${resolvedUserId}-*\` with unsynthesized or missing \`synthesized\` frontmatter\n`;
|
|
@@ -389,7 +414,7 @@ function buildLearningContextSection(workspaceRoot, userId, forJob) {
|
|
|
389
414
|
if (forJob) {
|
|
390
415
|
if (hasL2 || hasL1) {
|
|
391
416
|
section += 'Use the relevant patterns and preferences in this job.\n';
|
|
392
|
-
if (
|
|
417
|
+
if (l1Coach.present || l2CoachPresent) {
|
|
393
418
|
section += 'Treat manager-coaching as feedback for how the manager should continue or improve managing AI, not as agent instruction.\n';
|
|
394
419
|
}
|
|
395
420
|
}
|
|
@@ -401,7 +426,7 @@ function buildLearningContextSection(workspaceRoot, userId, forJob) {
|
|
|
401
426
|
}
|
|
402
427
|
else {
|
|
403
428
|
section += 'Use this synthesized learning context throughout the session.\n';
|
|
404
|
-
if (
|
|
429
|
+
if (l1Coach.present || l2CoachPresent) {
|
|
405
430
|
section += 'Manager-coaching entries are manager-facing feedback, not instructions for the AI to follow.\n';
|
|
406
431
|
}
|
|
407
432
|
if (backlogTriggered) {
|
|
@@ -14,6 +14,39 @@
|
|
|
14
14
|
* (PR or Conversation from ~/.fraim/config.json). Delivery phases live
|
|
15
15
|
* server-side in the workflow; the proxy just fills in mode-specific content.
|
|
16
16
|
*/
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
+
var ownKeys = function(o) {
|
|
35
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
+
var ar = [];
|
|
37
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
+
return ar;
|
|
39
|
+
};
|
|
40
|
+
return ownKeys(o);
|
|
41
|
+
};
|
|
42
|
+
return function (mod) {
|
|
43
|
+
if (mod && mod.__esModule) return mod;
|
|
44
|
+
var result = {};
|
|
45
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
+
__setModuleDefault(result, mod);
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
})();
|
|
17
50
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
51
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
52
|
};
|
|
@@ -380,6 +413,8 @@ class FraimLocalMCPServer {
|
|
|
380
413
|
this.otlpServer = null;
|
|
381
414
|
this.isShutdown = false;
|
|
382
415
|
this.mentoringResponseCache = null;
|
|
416
|
+
this.connectSyncInFlight = null;
|
|
417
|
+
this.latestConnectSyncWarning = null;
|
|
383
418
|
this.writer = writer || process.stdout.write.bind(process.stdout);
|
|
384
419
|
this.remoteUrl = process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
|
|
385
420
|
this.apiKey = this.loadApiKey();
|
|
@@ -554,6 +589,125 @@ class FraimLocalMCPServer {
|
|
|
554
589
|
}
|
|
555
590
|
}
|
|
556
591
|
}
|
|
592
|
+
getConnectSyncMaxAgeMs() {
|
|
593
|
+
const raw = process.env.FRAIM_SYNC_ON_CONNECT_MAX_AGE_MS;
|
|
594
|
+
if (!raw)
|
|
595
|
+
return FraimLocalMCPServer.DEFAULT_CONNECT_SYNC_MAX_AGE_MS;
|
|
596
|
+
const parsed = Number(raw);
|
|
597
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
598
|
+
return FraimLocalMCPServer.DEFAULT_CONNECT_SYNC_MAX_AGE_MS;
|
|
599
|
+
}
|
|
600
|
+
return Math.floor(parsed);
|
|
601
|
+
}
|
|
602
|
+
shouldUseLocalSyncTarget() {
|
|
603
|
+
if (process.env.FRAIM_LOCAL_SYNC === '1') {
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
try {
|
|
607
|
+
const parsed = new URL(this.remoteUrl);
|
|
608
|
+
return parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1' || parsed.hostname === '::1';
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
getLocalCatalogMetadataPath(projectRoot) {
|
|
615
|
+
return (0, path_1.join)((0, project_fraim_paths_1.getWorkspaceFraimDir)(projectRoot), FraimLocalMCPServer.CONNECT_SYNC_METADATA_PATH);
|
|
616
|
+
}
|
|
617
|
+
readLocalCatalogMetadata(projectRoot) {
|
|
618
|
+
const metadataPath = this.getLocalCatalogMetadataPath(projectRoot);
|
|
619
|
+
if (!(0, fs_1.existsSync)(metadataPath)) {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
try {
|
|
623
|
+
return JSON.parse((0, fs_1.readFileSync)(metadataPath, 'utf8'));
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
writeLocalCatalogMetadata(projectRoot) {
|
|
630
|
+
const metadataPath = this.getLocalCatalogMetadataPath(projectRoot);
|
|
631
|
+
const metadata = {
|
|
632
|
+
localVersion: this.localVersion,
|
|
633
|
+
mode: this.shouldUseLocalSyncTarget() ? 'local' : 'remote',
|
|
634
|
+
remoteUrl: this.remoteUrl,
|
|
635
|
+
syncedAt: new Date().toISOString()
|
|
636
|
+
};
|
|
637
|
+
(0, fs_1.writeFileSync)(metadataPath, JSON.stringify(metadata, null, 2), 'utf8');
|
|
638
|
+
}
|
|
639
|
+
getLocalCatalogSyncReason(projectRoot) {
|
|
640
|
+
const catalogRoots = [
|
|
641
|
+
(0, path_1.join)(projectRoot, 'fraim', 'ai-employee', 'jobs'),
|
|
642
|
+
(0, path_1.join)(projectRoot, 'fraim', 'ai-manager', 'jobs')
|
|
643
|
+
];
|
|
644
|
+
if (catalogRoots.some((catalogRoot) => !(0, fs_1.existsSync)(catalogRoot))) {
|
|
645
|
+
return 'local job catalog is missing';
|
|
646
|
+
}
|
|
647
|
+
const metadata = this.readLocalCatalogMetadata(projectRoot);
|
|
648
|
+
if (!metadata) {
|
|
649
|
+
return 'local catalog has no freshness metadata';
|
|
650
|
+
}
|
|
651
|
+
if (metadata.localVersion !== this.localVersion) {
|
|
652
|
+
return `local catalog was synced by FRAIM ${metadata.localVersion}`;
|
|
653
|
+
}
|
|
654
|
+
const syncedAtMs = Date.parse(metadata.syncedAt);
|
|
655
|
+
if (!Number.isFinite(syncedAtMs)) {
|
|
656
|
+
return 'local catalog has invalid freshness metadata';
|
|
657
|
+
}
|
|
658
|
+
const maxAgeMs = this.getConnectSyncMaxAgeMs();
|
|
659
|
+
if (Date.now() - syncedAtMs > maxAgeMs) {
|
|
660
|
+
return `local catalog is older than ${maxAgeMs}ms`;
|
|
661
|
+
}
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
async performLocalCatalogSync(projectRoot) {
|
|
665
|
+
const { runSync } = await Promise.resolve().then(() => __importStar(require('../cli/commands/sync')));
|
|
666
|
+
await runSync({
|
|
667
|
+
projectRoot,
|
|
668
|
+
skipUpdates: true,
|
|
669
|
+
local: this.shouldUseLocalSyncTarget(),
|
|
670
|
+
failHard: 'throw'
|
|
671
|
+
});
|
|
672
|
+
this.writeLocalCatalogMetadata(projectRoot);
|
|
673
|
+
}
|
|
674
|
+
async ensureFreshLocalCatalogOnConnect(requestId) {
|
|
675
|
+
if (process.env.FRAIM_DISABLE_SYNC_ON_CONNECT === '1') {
|
|
676
|
+
this.latestConnectSyncWarning = null;
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (this.connectSyncInFlight) {
|
|
680
|
+
await this.connectSyncInFlight;
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
this.connectSyncInFlight = (async () => {
|
|
684
|
+
const projectRoot = this.findProjectRoot();
|
|
685
|
+
this.latestConnectSyncWarning = null;
|
|
686
|
+
if (!projectRoot || !(0, project_fraim_paths_1.workspaceFraimExists)(projectRoot)) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const reason = this.getLocalCatalogSyncReason(projectRoot);
|
|
690
|
+
if (!reason) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
this.log(`[req:${requestId}] Refreshing local FRAIM catalog before fraim_connect because ${reason}`);
|
|
694
|
+
try {
|
|
695
|
+
await this.performLocalCatalogSync(projectRoot);
|
|
696
|
+
this.log(`[req:${requestId}] Local FRAIM catalog refresh complete`);
|
|
697
|
+
}
|
|
698
|
+
catch (error) {
|
|
699
|
+
const message = error?.message || String(error);
|
|
700
|
+
this.latestConnectSyncWarning = `Local FRAIM catalog refresh failed: ${message}. Discovery will fall back to remote FRAIM tools until sync succeeds.`;
|
|
701
|
+
this.logError(`[req:${requestId}] ${this.latestConnectSyncWarning}`);
|
|
702
|
+
}
|
|
703
|
+
})();
|
|
704
|
+
try {
|
|
705
|
+
await this.connectSyncInFlight;
|
|
706
|
+
}
|
|
707
|
+
finally {
|
|
708
|
+
this.connectSyncInFlight = null;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
557
711
|
/**
|
|
558
712
|
* Automatically detect machine information
|
|
559
713
|
*/
|
|
@@ -1025,6 +1179,7 @@ class FraimLocalMCPServer {
|
|
|
1025
1179
|
if (toolName === 'fraim_connect' && !finalizedResponse.error) {
|
|
1026
1180
|
const text = finalizedResponse.result?.content?.[0]?.text;
|
|
1027
1181
|
if (typeof text === 'string') {
|
|
1182
|
+
let responseText = text;
|
|
1028
1183
|
const emailMatch = text.match(/Your identity for this session: \*\*([^*]+)\*\*/);
|
|
1029
1184
|
if (emailMatch) {
|
|
1030
1185
|
const userEmail = emailMatch[1].trim();
|
|
@@ -1034,10 +1189,14 @@ class FraimLocalMCPServer {
|
|
|
1034
1189
|
const workspaceRoot = this.findProjectRoot() || process.cwd();
|
|
1035
1190
|
const learningSection = (0, learning_context_builder_js_1.buildLearningContextSection)(workspaceRoot, userEmail, false);
|
|
1036
1191
|
if (learningSection) {
|
|
1037
|
-
|
|
1192
|
+
responseText += learningSection;
|
|
1038
1193
|
this.log(`[req:${requestId}] Injected learning context for ${userEmail} from ${workspaceRoot}`);
|
|
1039
1194
|
}
|
|
1040
1195
|
}
|
|
1196
|
+
if (this.latestConnectSyncWarning) {
|
|
1197
|
+
responseText += `\n\n## Local Catalog\n${this.latestConnectSyncWarning}`;
|
|
1198
|
+
}
|
|
1199
|
+
finalizedResponse.result.content[0].text = responseText;
|
|
1041
1200
|
}
|
|
1042
1201
|
}
|
|
1043
1202
|
// 4. After get_fraim_job succeeds, inject learning context with job-focus frame.
|
|
@@ -1461,6 +1620,7 @@ class FraimLocalMCPServer {
|
|
|
1461
1620
|
// Special handling for fraim_connect - automatically inject machine and repo info
|
|
1462
1621
|
if (request.method === 'tools/call' && request.params?.name === 'fraim_connect') {
|
|
1463
1622
|
this.log(`[req:${requestId}] Intercepting fraim_connect to inject machine/repo info`);
|
|
1623
|
+
await this.ensureFreshLocalCatalogOnConnect(requestId);
|
|
1464
1624
|
const args = request.params.arguments || {};
|
|
1465
1625
|
// REQUIRED: Auto-detect and inject machine info
|
|
1466
1626
|
const detectedMachine = this.detectMachineInfo();
|
|
@@ -1755,9 +1915,7 @@ class FraimLocalMCPServer {
|
|
|
1755
1915
|
this.log(`✅ Local override found for get_fraim_job: ${name}`);
|
|
1756
1916
|
let responseText = overview.overview;
|
|
1757
1917
|
if (!overview.isSimple) {
|
|
1758
|
-
|
|
1759
|
-
if (phaseAuthority)
|
|
1760
|
-
responseText = `${phaseAuthority}\n\n---\n\n${responseText}`;
|
|
1918
|
+
responseText = `${mentor.getCompactPhaseAuthority()}\n\n${responseText}`;
|
|
1761
1919
|
responseText += `\n\n---\n\n**This job has phases.** Use \`seekMentoring\` to get phase-specific instructions.`;
|
|
1762
1920
|
}
|
|
1763
1921
|
// Inject local learning context for job requests (RFC 177).
|
|
@@ -1778,25 +1936,64 @@ class FraimLocalMCPServer {
|
|
|
1778
1936
|
}
|
|
1779
1937
|
}
|
|
1780
1938
|
}
|
|
1781
|
-
// DISCOVERY AGGREGATION: Merge local and remote jobs
|
|
1782
1939
|
if (toolName === 'list_fraim_jobs') {
|
|
1783
1940
|
const response = await this._doProxyToRemote(request, requestId);
|
|
1784
1941
|
if (!response.error && response.result?.content?.[0]?.text) {
|
|
1785
1942
|
try {
|
|
1786
|
-
const
|
|
1787
|
-
const
|
|
1788
|
-
if (
|
|
1789
|
-
|
|
1943
|
+
const projectRoot = this.findProjectRoot();
|
|
1944
|
+
const uniqueLocalItems = new Map();
|
|
1945
|
+
if (projectRoot) {
|
|
1946
|
+
const personalizedJobsDir = (0, path_1.join)(projectRoot, 'fraim', 'personalized-employee', 'jobs');
|
|
1947
|
+
if ((0, fs_1.existsSync)(personalizedJobsDir)) {
|
|
1948
|
+
const collectLocalJobPaths = (dir, currentRel = '') => {
|
|
1949
|
+
const results = [];
|
|
1950
|
+
const entries = (0, fs_1.readdirSync)(dir, { withFileTypes: true });
|
|
1951
|
+
for (const entry of entries) {
|
|
1952
|
+
const rel = currentRel ? `${currentRel}/${entry.name}` : entry.name;
|
|
1953
|
+
const full = (0, path_1.join)(dir, entry.name);
|
|
1954
|
+
if (entry.isDirectory()) {
|
|
1955
|
+
results.push(...collectLocalJobPaths(full, rel));
|
|
1956
|
+
}
|
|
1957
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
1958
|
+
results.push(rel);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
return results;
|
|
1962
|
+
};
|
|
1963
|
+
const localJobPaths = collectLocalJobPaths(personalizedJobsDir);
|
|
1964
|
+
for (const relPath of localJobPaths) {
|
|
1965
|
+
const normalizedName = relPath.split('/').pop()?.replace(/\.md$/, '').trim() || '';
|
|
1966
|
+
if (!normalizedName || uniqueLocalItems.has(normalizedName)) {
|
|
1967
|
+
continue;
|
|
1968
|
+
}
|
|
1969
|
+
const category = relPath.includes('/')
|
|
1970
|
+
? relPath.split('/').slice(0, -1).join('/') || 'personalized-employee'
|
|
1971
|
+
: 'personalized-employee';
|
|
1972
|
+
uniqueLocalItems.set(normalizedName, {
|
|
1973
|
+
name: normalizedName,
|
|
1974
|
+
path: relPath,
|
|
1975
|
+
category
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
if (uniqueLocalItems.size > 0) {
|
|
1981
|
+
const sortedLocalItems = Array.from(uniqueLocalItems.values())
|
|
1982
|
+
.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
|
|
1790
1983
|
let combinedText = response.result.content[0].text;
|
|
1791
|
-
combinedText += `\n\n## Local
|
|
1792
|
-
|
|
1793
|
-
|
|
1984
|
+
combinedText += `\n\n## Local Personalized Jobs (${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('personalized-employee/jobs/')})\n`;
|
|
1985
|
+
combinedText += 'These local jobs override catalog jobs of the same name when you call `get_fraim_job`.\n';
|
|
1986
|
+
for (const item of sortedLocalItems) {
|
|
1987
|
+
const categorySuffix = item.category && item.category !== 'personalized-employee'
|
|
1988
|
+
? ` (${item.category})`
|
|
1989
|
+
: '';
|
|
1990
|
+
combinedText += `- **${item.name}**${categorySuffix}\n`;
|
|
1794
1991
|
}
|
|
1795
1992
|
response.result.content[0].text = combinedText;
|
|
1796
1993
|
}
|
|
1797
1994
|
}
|
|
1798
1995
|
catch (error) {
|
|
1799
|
-
this.log(`⚠️
|
|
1996
|
+
this.log(`⚠️ Local personalized job listing failed: ${error.message}`);
|
|
1800
1997
|
}
|
|
1801
1998
|
}
|
|
1802
1999
|
return response;
|
|
@@ -2128,6 +2325,8 @@ FraimLocalMCPServer.AGENT_RESOLUTION_NOTICE = [
|
|
|
2128
2325
|
].join('\n');
|
|
2129
2326
|
FraimLocalMCPServer.FALLBACK_ALERT_MARKER = 'PROXY_FALLBACK_ALERT';
|
|
2130
2327
|
FraimLocalMCPServer.SESSION_HEADER = 'x-fraim-session-id';
|
|
2328
|
+
FraimLocalMCPServer.CONNECT_SYNC_METADATA_PATH = '.sync-metadata.json';
|
|
2329
|
+
FraimLocalMCPServer.DEFAULT_CONNECT_SYNC_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
2131
2330
|
// Start server if run directly
|
|
2132
2331
|
if (require.main === module) {
|
|
2133
2332
|
const server = new FraimLocalMCPServer();
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-framework",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.159",
|
|
4
4
|
"description": "FRAIM: AI Workforce Infrastructure — the organizational capability that turns AI agents into an accountable workforce, their operators into capable AI managers, and executives into leaders with clear optics on AI proficiency.",
|
|
5
5
|
"main": "index.js",
|
|
6
|
-
"bin":
|
|
6
|
+
"bin": {
|
|
7
|
+
"fraim-framework": "bin/fraim.js"
|
|
8
|
+
},
|
|
7
9
|
"scripts": {
|
|
8
10
|
"dev": "tsx --watch src/fraim-mcp-server.ts > server.log 2>&1",
|
|
9
11
|
"dev:prod": "npm run build && node dist/src/fraim-mcp-server.js > server.log 2>&1",
|
|
10
|
-
"build": "tsx scripts/build-fraim-config-schema-template.ts && npm run typecheck:scripts && tsc && npm run build:stubs && npm run build:fraim-brain && node scripts/copy-registry.js && npm run validate:registry && npm run validate:fraim-pro-assets && tsx scripts/validate-purity.ts",
|
|
12
|
+
"build": "tsx scripts/build-fraim-config-schema-template.ts && npm run typecheck:scripts && tsc && npm run build:stubs && npm run build:fraim-brain && node scripts/copy-registry.js && npm run validate:registry && npm run validate:fraim-pro-assets && npm run validate:employee-catalog && tsx scripts/validate-purity.ts",
|
|
11
13
|
"build:stubs": "tsx scripts/build-stub-registry.ts",
|
|
12
14
|
"build:fraim-brain": "node scripts/generate-fraim-brain.js",
|
|
13
15
|
"test-all": "npm run test && npm run test:isolated && npm run test:ui",
|
|
@@ -21,6 +23,8 @@
|
|
|
21
23
|
"test:ui": "playwright test --workers=1",
|
|
22
24
|
"test:ui:headed": "playwright test --headed --workers=1",
|
|
23
25
|
"hub:desktop": "npm run build && electron dist/src/ai-hub/desktop-main.js",
|
|
26
|
+
"hub:dev": "tsx scripts/start-hub-dev.ts",
|
|
27
|
+
"firstrun:dev": "tsx scripts/start-firstrun-dev.ts",
|
|
24
28
|
"start:fraim": "tsx src/fraim-mcp-server.ts",
|
|
25
29
|
"dev:fraim": "tsx --watch src/fraim-mcp-server.ts",
|
|
26
30
|
"serve:website": "node fraim-pro/serve.js",
|
|
@@ -45,6 +49,7 @@
|
|
|
45
49
|
"validate:registry-references": "tsx scripts/validate-registry-references.ts",
|
|
46
50
|
"validate:brain-mapping": "tsx scripts/validate-brain-mapping.ts",
|
|
47
51
|
"validate:fraim-pro-assets": "tsx scripts/validate-fraim-pro-assets.ts",
|
|
52
|
+
"validate:employee-catalog": "tsx scripts/validate-employee-catalog.ts",
|
|
48
53
|
"validate:jobs": "tsx scripts/validate-jobs.ts",
|
|
49
54
|
"validate:platform-agnostic": "tsx scripts/validate-platform-agnostic.ts",
|
|
50
55
|
"validate:skills": "tsx scripts/validate-skills.ts",
|