fraim-framework 2.0.84 → 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/bin/fraim-mcp.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/sync.js +34 -23
- package/dist/src/cli/doctor/checks/workflow-checks.js +12 -4
- 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/local-mcp-server/stdio-server.js +144 -33
- package/index.js +1 -1
- package/package.json +2 -1
package/bin/fraim-mcp.js
CHANGED
|
@@ -159,7 +159,7 @@ const runInitProject = async () => {
|
|
|
159
159
|
fs_1.default.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
160
160
|
console.log(chalk_1.default.green('Created .fraim/config.json'));
|
|
161
161
|
}
|
|
162
|
-
['workflows'].forEach((dir) => {
|
|
162
|
+
['workflows', 'ai-employee/jobs', 'ai-employee/skills', 'ai-manager/jobs', 'personalized-employee'].forEach((dir) => {
|
|
163
163
|
const dirPath = path_1.default.join(fraimDir, dir);
|
|
164
164
|
if (!fs_1.default.existsSync(dirPath)) {
|
|
165
165
|
fs_1.default.mkdirSync(dirPath, { recursive: true });
|
|
@@ -16,7 +16,8 @@ exports.listOverridableCommand = new commander_1.Command('list-overridable')
|
|
|
16
16
|
const projectRoot = process.cwd();
|
|
17
17
|
const fraimDir = path_1.default.join(projectRoot, '.fraim');
|
|
18
18
|
const configPath = path_1.default.join(fraimDir, 'config.json');
|
|
19
|
-
const
|
|
19
|
+
const personalizedDir = path_1.default.join(fraimDir, 'personalized-employee');
|
|
20
|
+
const legacyOverridesDir = path_1.default.join(fraimDir, 'overrides');
|
|
20
21
|
// Validate .fraim directory exists
|
|
21
22
|
if (!fs_1.default.existsSync(fraimDir)) {
|
|
22
23
|
console.log(chalk_1.default.red('❌ .fraim/ directory not found. Run "fraim setup" or "fraim init-project" first.'));
|
|
@@ -42,20 +43,23 @@ exports.listOverridableCommand = new commander_1.Command('list-overridable')
|
|
|
42
43
|
console.log(chalk_1.default.blue('📋 Overridable FRAIM Registry Paths:\n'));
|
|
43
44
|
// Get list of existing overrides
|
|
44
45
|
const existingOverrides = new Set();
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
scanDir(path_1.default.join(dir, entry.name), relativePath);
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
existingOverrides.add(relativePath.replace(/\\/g, '/'));
|
|
55
|
-
}
|
|
46
|
+
const scanDir = (dir, base = '') => {
|
|
47
|
+
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
const relativePath = path_1.default.join(base, entry.name);
|
|
50
|
+
if (entry.isDirectory()) {
|
|
51
|
+
scanDir(path_1.default.join(dir, entry.name), relativePath);
|
|
56
52
|
}
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
else {
|
|
54
|
+
existingOverrides.add(relativePath.replace(/\\/g, '/'));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
if (fs_1.default.existsSync(personalizedDir)) {
|
|
59
|
+
scanDir(personalizedDir);
|
|
60
|
+
}
|
|
61
|
+
if (fs_1.default.existsSync(legacyOverridesDir)) {
|
|
62
|
+
scanDir(legacyOverridesDir);
|
|
59
63
|
}
|
|
60
64
|
// Handle --rules flag
|
|
61
65
|
if (options.rules) {
|
|
@@ -197,6 +201,6 @@ exports.listOverridableCommand = new commander_1.Command('list-overridable')
|
|
|
197
201
|
console.log(chalk_1.default.gray(' • Use "fraim override <path> --copy" to copy current content'));
|
|
198
202
|
console.log(chalk_1.default.gray(' • Use --job-category <category> to see category-specific items'));
|
|
199
203
|
console.log(chalk_1.default.gray(' • Use --rules to see all overridable rules'));
|
|
200
|
-
console.log(chalk_1.default.gray(' • Overrides are stored in .fraim/
|
|
204
|
+
console.log(chalk_1.default.gray(' • Overrides are stored in .fraim/personalized-employee/'));
|
|
201
205
|
}
|
|
202
206
|
});
|
|
@@ -39,8 +39,8 @@ exports.overrideCommand = new commander_1.Command('override')
|
|
|
39
39
|
console.log(chalk_1.default.red('❌ Must specify either --inherit or --copy.'));
|
|
40
40
|
process.exit(1);
|
|
41
41
|
}
|
|
42
|
-
// Create
|
|
43
|
-
const overridePath = path_1.default.join(fraimDir, '
|
|
42
|
+
// Create personalized override directory structure
|
|
43
|
+
const overridePath = path_1.default.join(fraimDir, 'personalized-employee', registryPath);
|
|
44
44
|
const overrideDir = path_1.default.dirname(overridePath);
|
|
45
45
|
if (!fs_1.default.existsSync(overrideDir)) {
|
|
46
46
|
fs_1.default.mkdirSync(overrideDir, { recursive: true });
|
|
@@ -124,6 +124,13 @@ exports.overrideCommand = new commander_1.Command('override')
|
|
|
124
124
|
toolName = 'get_fraim_workflow';
|
|
125
125
|
toolArgs = { workflow: workflowName };
|
|
126
126
|
}
|
|
127
|
+
else if (registryPath.startsWith('jobs/')) {
|
|
128
|
+
// e.g., "jobs/product-building/feature-specification.md" -> "feature-specification"
|
|
129
|
+
const parts = registryPath.split('/');
|
|
130
|
+
const jobName = parts[parts.length - 1].replace('.md', '');
|
|
131
|
+
toolName = 'get_fraim_job';
|
|
132
|
+
toolArgs = { job: jobName };
|
|
133
|
+
}
|
|
127
134
|
else {
|
|
128
135
|
toolName = 'get_fraim_file';
|
|
129
136
|
toolArgs = { path: registryPath };
|
|
@@ -63,6 +63,24 @@ function loadUserApiKey() {
|
|
|
63
63
|
return undefined;
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
function updateVersionInConfig(fraimDir) {
|
|
67
|
+
const configPath = path_1.default.join(fraimDir, 'config.json');
|
|
68
|
+
if (!fs_1.default.existsSync(configPath)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const currentConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
|
|
73
|
+
const newVersion = (0, version_utils_1.getFraimVersion)();
|
|
74
|
+
if (currentConfig.version !== newVersion) {
|
|
75
|
+
currentConfig.version = newVersion;
|
|
76
|
+
fs_1.default.writeFileSync(configPath, JSON.stringify(currentConfig, null, 2));
|
|
77
|
+
console.log(chalk_1.default.green(`✅ Updated FRAIM version to ${newVersion} in config.`));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
console.warn(chalk_1.default.yellow('⚠️ Could not update version in config.json.'));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
66
84
|
const runSync = async (options) => {
|
|
67
85
|
const projectRoot = process.cwd();
|
|
68
86
|
const config = (0, config_loader_1.loadFraimConfig)();
|
|
@@ -88,7 +106,8 @@ const runSync = async (options) => {
|
|
|
88
106
|
skipUpdates: true
|
|
89
107
|
});
|
|
90
108
|
if (result.success) {
|
|
91
|
-
console.log(chalk_1.default.green(`✅ Successfully synced ${result.workflowsSynced} workflows, ${result.
|
|
109
|
+
console.log(chalk_1.default.green(`✅ Successfully synced ${result.workflowsSynced} workflows, ${result.employeeJobsSynced} ai-employee jobs, ${result.managerJobsSynced} ai-manager jobs, ${result.skillsSynced} skills, ${result.rulesSynced} rules, ${result.scriptsSynced} scripts, and ${result.docsSynced} docs from local server`));
|
|
110
|
+
updateVersionInConfig(fraimDir);
|
|
92
111
|
return;
|
|
93
112
|
}
|
|
94
113
|
console.error(chalk_1.default.red(`❌ Local sync failed: ${result.error}`));
|
|
@@ -96,12 +115,18 @@ const runSync = async (options) => {
|
|
|
96
115
|
process.exit(1);
|
|
97
116
|
}
|
|
98
117
|
// Path 2: Remote sync with API key
|
|
99
|
-
|
|
118
|
+
let apiKey = loadUserApiKey() || config.apiKey || process.env.FRAIM_API_KEY;
|
|
100
119
|
if (!apiKey) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
120
|
+
if (process.env.TEST_MODE === 'true') {
|
|
121
|
+
console.log(chalk_1.default.yellow('⚠️ TEST_MODE: No API key configured. Using test placeholder key.'));
|
|
122
|
+
apiKey = 'test-mode-key';
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
console.error(chalk_1.default.red('❌ No API key configured. Cannot sync.'));
|
|
126
|
+
console.error(chalk_1.default.yellow('💡 Set FRAIM_API_KEY in your environment, or add apiKey to ~/.fraim/config.json or .fraim/config.json'));
|
|
127
|
+
console.error(chalk_1.default.yellow('💡 Or use --local to sync from a locally running FRAIM server.'));
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
105
130
|
}
|
|
106
131
|
console.log(chalk_1.default.blue('🔄 Syncing FRAIM workflows from remote server...'));
|
|
107
132
|
const result = await syncFromRemote({
|
|
@@ -115,27 +140,13 @@ const runSync = async (options) => {
|
|
|
115
140
|
console.error(chalk_1.default.yellow('💡 Check your API key and network connection.'));
|
|
116
141
|
if (process.env.TEST_MODE === 'true') {
|
|
117
142
|
console.log(chalk_1.default.yellow('⚠️ TEST_MODE: Continuing without remote sync (server may be unavailable).'));
|
|
143
|
+
updateVersionInConfig(fraimDir);
|
|
118
144
|
return;
|
|
119
145
|
}
|
|
120
146
|
process.exit(1);
|
|
121
147
|
}
|
|
122
|
-
console.log(chalk_1.default.green(`✅ Successfully synced ${result.workflowsSynced} workflows, ${result.
|
|
123
|
-
|
|
124
|
-
const configPath = path_1.default.join(fraimDir, 'config.json');
|
|
125
|
-
if (fs_1.default.existsSync(configPath)) {
|
|
126
|
-
try {
|
|
127
|
-
const currentConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
|
|
128
|
-
const newVersion = (0, version_utils_1.getFraimVersion)();
|
|
129
|
-
if (currentConfig.version !== newVersion) {
|
|
130
|
-
currentConfig.version = newVersion;
|
|
131
|
-
fs_1.default.writeFileSync(configPath, JSON.stringify(currentConfig, null, 2));
|
|
132
|
-
console.log(chalk_1.default.green(`✅ Updated FRAIM version to ${newVersion} in config.`));
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
catch (e) {
|
|
136
|
-
console.warn(chalk_1.default.yellow('⚠️ Could not update version in config.json.'));
|
|
137
|
-
}
|
|
138
|
-
}
|
|
148
|
+
console.log(chalk_1.default.green(`✅ Successfully synced ${result.workflowsSynced} workflows, ${result.employeeJobsSynced} ai-employee jobs, ${result.managerJobsSynced} ai-manager jobs, ${result.skillsSynced} skills, ${result.rulesSynced} rules, ${result.scriptsSynced} scripts, and ${result.docsSynced} docs from remote`));
|
|
149
|
+
updateVersionInConfig(fraimDir);
|
|
139
150
|
};
|
|
140
151
|
exports.runSync = runSync;
|
|
141
152
|
async function checkAndUpdateCLI() {
|
|
@@ -166,8 +166,9 @@ function checkOverrideSyntaxValid() {
|
|
|
166
166
|
category: 'workflows',
|
|
167
167
|
critical: false,
|
|
168
168
|
run: async () => {
|
|
169
|
-
const
|
|
170
|
-
|
|
169
|
+
const personalizedDir = path_1.default.join(process.cwd(), '.fraim', 'personalized-employee');
|
|
170
|
+
const legacyOverridesDir = path_1.default.join(process.cwd(), '.fraim', 'overrides');
|
|
171
|
+
if (!fs_1.default.existsSync(personalizedDir) && !fs_1.default.existsSync(legacyOverridesDir)) {
|
|
171
172
|
return {
|
|
172
173
|
status: 'passed',
|
|
173
174
|
message: 'No overrides (not required)',
|
|
@@ -192,10 +193,17 @@ function checkOverrideSyntaxValid() {
|
|
|
192
193
|
}
|
|
193
194
|
}
|
|
194
195
|
};
|
|
195
|
-
|
|
196
|
+
if (fs_1.default.existsSync(personalizedDir)) {
|
|
197
|
+
scanDir(personalizedDir);
|
|
198
|
+
}
|
|
199
|
+
if (fs_1.default.existsSync(legacyOverridesDir)) {
|
|
200
|
+
scanDir(legacyOverridesDir);
|
|
201
|
+
}
|
|
196
202
|
// Validate each override
|
|
197
203
|
for (const override of overrides) {
|
|
198
|
-
const
|
|
204
|
+
const primaryPath = path_1.default.join(personalizedDir, override);
|
|
205
|
+
const legacyPath = path_1.default.join(legacyOverridesDir, override);
|
|
206
|
+
const overridePath = fs_1.default.existsSync(primaryPath) ? primaryPath : legacyPath;
|
|
199
207
|
const content = fs_1.default.readFileSync(overridePath, 'utf-8');
|
|
200
208
|
const parseResult = parser.parse(content);
|
|
201
209
|
if (parseResult.hasImports) {
|
|
@@ -27,8 +27,11 @@ async function syncFromRemote(options) {
|
|
|
27
27
|
return {
|
|
28
28
|
success: false,
|
|
29
29
|
workflowsSynced: 0,
|
|
30
|
+
employeeJobsSynced: 0,
|
|
31
|
+
managerJobsSynced: 0,
|
|
32
|
+
skillsSynced: 0,
|
|
33
|
+
rulesSynced: 0,
|
|
30
34
|
scriptsSynced: 0,
|
|
31
|
-
coachingSynced: 0,
|
|
32
35
|
docsSynced: 0,
|
|
33
36
|
error: 'FRAIM_API_KEY not set'
|
|
34
37
|
};
|
|
@@ -49,8 +52,11 @@ async function syncFromRemote(options) {
|
|
|
49
52
|
return {
|
|
50
53
|
success: false,
|
|
51
54
|
workflowsSynced: 0,
|
|
55
|
+
employeeJobsSynced: 0,
|
|
56
|
+
managerJobsSynced: 0,
|
|
57
|
+
skillsSynced: 0,
|
|
58
|
+
rulesSynced: 0,
|
|
52
59
|
scriptsSynced: 0,
|
|
53
|
-
coachingSynced: 0,
|
|
54
60
|
docsSynced: 0,
|
|
55
61
|
error: 'No files received'
|
|
56
62
|
};
|
|
@@ -73,6 +79,72 @@ async function syncFromRemote(options) {
|
|
|
73
79
|
(0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
|
|
74
80
|
console.log(chalk_1.default.gray(` + ${file.path}`));
|
|
75
81
|
}
|
|
82
|
+
// Sync job stubs to role-specific folders under .fraim
|
|
83
|
+
const allJobFiles = files.filter(f => f.type === 'job');
|
|
84
|
+
const managerJobFiles = allJobFiles.filter(f => f.path.startsWith('ai-manager/'));
|
|
85
|
+
const jobFiles = allJobFiles.filter(f => !f.path.startsWith('ai-manager/'));
|
|
86
|
+
const employeeJobsDir = (0, path_1.join)(options.projectRoot, '.fraim', 'ai-employee', 'jobs');
|
|
87
|
+
if (!(0, fs_1.existsSync)(employeeJobsDir)) {
|
|
88
|
+
(0, fs_1.mkdirSync)(employeeJobsDir, { recursive: true });
|
|
89
|
+
}
|
|
90
|
+
cleanDirectory(employeeJobsDir);
|
|
91
|
+
for (const file of jobFiles) {
|
|
92
|
+
const filePath = (0, path_1.join)(employeeJobsDir, file.path);
|
|
93
|
+
const fileDir = (0, path_1.dirname)(filePath);
|
|
94
|
+
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
95
|
+
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
(0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
|
|
98
|
+
console.log(chalk_1.default.gray(` + ai-employee/jobs/${file.path}`));
|
|
99
|
+
}
|
|
100
|
+
// Sync ai-manager job stubs to .fraim/ai-manager/jobs/
|
|
101
|
+
const managerJobsDir = (0, path_1.join)(options.projectRoot, '.fraim', 'ai-manager', 'jobs');
|
|
102
|
+
if (!(0, fs_1.existsSync)(managerJobsDir)) {
|
|
103
|
+
(0, fs_1.mkdirSync)(managerJobsDir, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
cleanDirectory(managerJobsDir);
|
|
106
|
+
for (const file of managerJobFiles) {
|
|
107
|
+
const managerRelativePath = file.path.replace(/^ai-manager\//, '');
|
|
108
|
+
const filePath = (0, path_1.join)(managerJobsDir, managerRelativePath);
|
|
109
|
+
const fileDir = (0, path_1.dirname)(filePath);
|
|
110
|
+
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
111
|
+
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
112
|
+
}
|
|
113
|
+
(0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
|
|
114
|
+
console.log(chalk_1.default.gray(` + ai-manager/jobs/${managerRelativePath}`));
|
|
115
|
+
}
|
|
116
|
+
// Sync full skill files to .fraim/ai-employee/skills/
|
|
117
|
+
const skillFiles = files.filter(f => f.type === 'skill');
|
|
118
|
+
const skillsDir = (0, path_1.join)(options.projectRoot, '.fraim', 'ai-employee', 'skills');
|
|
119
|
+
if (!(0, fs_1.existsSync)(skillsDir)) {
|
|
120
|
+
(0, fs_1.mkdirSync)(skillsDir, { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
cleanDirectory(skillsDir);
|
|
123
|
+
for (const file of skillFiles) {
|
|
124
|
+
const filePath = (0, path_1.join)(skillsDir, file.path);
|
|
125
|
+
const fileDir = (0, path_1.dirname)(filePath);
|
|
126
|
+
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
127
|
+
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
128
|
+
}
|
|
129
|
+
(0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
|
|
130
|
+
console.log(chalk_1.default.gray(` + ai-employee/skills/${file.path}`));
|
|
131
|
+
}
|
|
132
|
+
// Sync full rule files to .fraim/ai-employee/rules/
|
|
133
|
+
const ruleFiles = files.filter(f => f.type === 'rule');
|
|
134
|
+
const rulesDir = (0, path_1.join)(options.projectRoot, '.fraim', 'ai-employee', 'rules');
|
|
135
|
+
if (!(0, fs_1.existsSync)(rulesDir)) {
|
|
136
|
+
(0, fs_1.mkdirSync)(rulesDir, { recursive: true });
|
|
137
|
+
}
|
|
138
|
+
cleanDirectory(rulesDir);
|
|
139
|
+
for (const file of ruleFiles) {
|
|
140
|
+
const filePath = (0, path_1.join)(rulesDir, file.path);
|
|
141
|
+
const fileDir = (0, path_1.dirname)(filePath);
|
|
142
|
+
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
143
|
+
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
144
|
+
}
|
|
145
|
+
(0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
|
|
146
|
+
console.log(chalk_1.default.gray(` + ai-employee/rules/${file.path}`));
|
|
147
|
+
}
|
|
76
148
|
// Sync scripts to user directory
|
|
77
149
|
const scriptFiles = files.filter(f => f.type === 'script');
|
|
78
150
|
const userDir = (0, script_sync_utils_1.getUserFraimDir)();
|
|
@@ -92,22 +164,6 @@ async function syncFromRemote(options) {
|
|
|
92
164
|
(0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
|
|
93
165
|
console.log(chalk_1.default.gray(` + ${file.path}`));
|
|
94
166
|
}
|
|
95
|
-
// Sync coaching files to .fraim/coaching-moments/
|
|
96
|
-
const coachingFiles = files.filter(f => f.type === 'coaching');
|
|
97
|
-
const coachingDir = (0, path_1.join)(options.projectRoot, '.fraim', 'coaching-moments');
|
|
98
|
-
if (!(0, fs_1.existsSync)(coachingDir)) {
|
|
99
|
-
(0, fs_1.mkdirSync)(coachingDir, { recursive: true });
|
|
100
|
-
}
|
|
101
|
-
cleanDirectory(coachingDir);
|
|
102
|
-
for (const file of coachingFiles) {
|
|
103
|
-
const filePath = (0, path_1.join)(coachingDir, file.path);
|
|
104
|
-
const fileDir = (0, path_1.dirname)(filePath);
|
|
105
|
-
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
106
|
-
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
107
|
-
}
|
|
108
|
-
(0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
|
|
109
|
-
console.log(chalk_1.default.gray(` + coaching-moments/${file.path}`));
|
|
110
|
-
}
|
|
111
167
|
// Sync docs to .fraim/docs/
|
|
112
168
|
const docsFiles = files.filter(f => f.type === 'docs');
|
|
113
169
|
const docsDir = (0, path_1.join)(options.projectRoot, '.fraim', 'docs');
|
|
@@ -124,12 +180,14 @@ async function syncFromRemote(options) {
|
|
|
124
180
|
(0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
|
|
125
181
|
console.log(chalk_1.default.gray(` + docs/${file.path}`));
|
|
126
182
|
}
|
|
127
|
-
console.log(chalk_1.default.green(`\n✅ Synced ${workflowFiles.length} workflows, ${scriptFiles.length} scripts, ${coachingFiles.length} coaching files, and ${docsFiles.length} docs from remote`));
|
|
128
183
|
return {
|
|
129
184
|
success: true,
|
|
130
185
|
workflowsSynced: workflowFiles.length,
|
|
186
|
+
employeeJobsSynced: jobFiles.length,
|
|
187
|
+
managerJobsSynced: managerJobFiles.length,
|
|
188
|
+
skillsSynced: skillFiles.length,
|
|
189
|
+
rulesSynced: ruleFiles.length,
|
|
131
190
|
scriptsSynced: scriptFiles.length,
|
|
132
|
-
coachingSynced: coachingFiles.length,
|
|
133
191
|
docsSynced: docsFiles.length
|
|
134
192
|
};
|
|
135
193
|
}
|
|
@@ -138,8 +196,11 @@ async function syncFromRemote(options) {
|
|
|
138
196
|
return {
|
|
139
197
|
success: false,
|
|
140
198
|
workflowsSynced: 0,
|
|
199
|
+
employeeJobsSynced: 0,
|
|
200
|
+
managerJobsSynced: 0,
|
|
201
|
+
skillsSynced: 0,
|
|
202
|
+
rulesSynced: 0,
|
|
141
203
|
scriptsSynced: 0,
|
|
142
|
-
coachingSynced: 0,
|
|
143
204
|
docsSynced: 0,
|
|
144
205
|
error: error.message
|
|
145
206
|
};
|
|
@@ -20,22 +20,54 @@ class LocalRegistryResolver {
|
|
|
20
20
|
* Check if a local override exists for the given path
|
|
21
21
|
*/
|
|
22
22
|
hasLocalOverride(path) {
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
23
|
+
const primaryPath = this.getOverridePath(path);
|
|
24
|
+
const legacyPath = this.getLegacyOverridePath(path);
|
|
25
|
+
const exists = (0, fs_1.existsSync)(primaryPath) || (0, fs_1.existsSync)(legacyPath);
|
|
26
|
+
console.error(`[LocalRegistryResolver] hasLocalOverride(${path}) -> primary: ${primaryPath}, legacy: ${legacyPath}, exists: ${exists}`);
|
|
26
27
|
return exists;
|
|
27
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Check if a locally synced skill/rule file exists for the given registry path.
|
|
31
|
+
*/
|
|
32
|
+
hasSyncedLocalFile(path) {
|
|
33
|
+
const syncedPath = this.getSyncedFilePath(path);
|
|
34
|
+
return !!syncedPath && (0, fs_1.existsSync)(syncedPath);
|
|
35
|
+
}
|
|
28
36
|
/**
|
|
29
37
|
* Get the full path to a local override file
|
|
30
38
|
*/
|
|
31
39
|
getOverridePath(path) {
|
|
40
|
+
return (0, path_1.join)(this.workspaceRoot, '.fraim/personalized-employee', path);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get the full path to a legacy local override file.
|
|
44
|
+
* Kept for backward compatibility while migrating to personalized-employee.
|
|
45
|
+
*/
|
|
46
|
+
getLegacyOverridePath(path) {
|
|
32
47
|
return (0, path_1.join)(this.workspaceRoot, '.fraim/overrides', path);
|
|
33
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Get the full path to a locally synced FRAIM file when available.
|
|
51
|
+
* Skills and rules are synced under role-based folders:
|
|
52
|
+
* - skills/* -> .fraim/ai-employee/skills/*
|
|
53
|
+
* - rules/* -> .fraim/ai-employee/rules/*
|
|
54
|
+
*/
|
|
55
|
+
getSyncedFilePath(path) {
|
|
56
|
+
const normalizedPath = path.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
57
|
+
if (normalizedPath.startsWith('skills/')) {
|
|
58
|
+
return (0, path_1.join)(this.workspaceRoot, '.fraim/ai-employee', normalizedPath);
|
|
59
|
+
}
|
|
60
|
+
if (normalizedPath.startsWith('rules/')) {
|
|
61
|
+
return (0, path_1.join)(this.workspaceRoot, '.fraim/ai-employee', normalizedPath);
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
34
65
|
/**
|
|
35
66
|
* Read local override file content
|
|
36
67
|
*/
|
|
37
68
|
readLocalOverride(path) {
|
|
38
|
-
const
|
|
69
|
+
const primaryPath = this.getOverridePath(path);
|
|
70
|
+
const overridePath = (0, fs_1.existsSync)(primaryPath) ? primaryPath : this.getLegacyOverridePath(path);
|
|
39
71
|
try {
|
|
40
72
|
return (0, fs_1.readFileSync)(overridePath, 'utf-8');
|
|
41
73
|
}
|
|
@@ -43,6 +75,37 @@ class LocalRegistryResolver {
|
|
|
43
75
|
throw new Error(`Failed to read local override: ${path}. ${error.message}`);
|
|
44
76
|
}
|
|
45
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Read locally synced skill/rule file content.
|
|
80
|
+
*/
|
|
81
|
+
readSyncedLocalFile(path) {
|
|
82
|
+
const syncedPath = this.getSyncedFilePath(path);
|
|
83
|
+
if (!syncedPath || !(0, fs_1.existsSync)(syncedPath)) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
return (0, fs_1.readFileSync)(syncedPath, 'utf-8');
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
stripMcpHeader(content) {
|
|
94
|
+
const trimmed = content.trimStart();
|
|
95
|
+
if (!trimmed.startsWith('#')) {
|
|
96
|
+
return content;
|
|
97
|
+
}
|
|
98
|
+
const separator = '\n---\n';
|
|
99
|
+
const separatorIndex = content.indexOf(separator);
|
|
100
|
+
if (separatorIndex === -1) {
|
|
101
|
+
return content;
|
|
102
|
+
}
|
|
103
|
+
const headerBlock = content.slice(0, separatorIndex);
|
|
104
|
+
if (!headerBlock.includes('** Path:**')) {
|
|
105
|
+
return content;
|
|
106
|
+
}
|
|
107
|
+
return content.slice(separatorIndex + separator.length).trimStart();
|
|
108
|
+
}
|
|
46
109
|
/**
|
|
47
110
|
* Resolve inheritance in local override content
|
|
48
111
|
*/
|
|
@@ -86,20 +149,32 @@ class LocalRegistryResolver {
|
|
|
86
149
|
* Resolve a registry file request
|
|
87
150
|
*
|
|
88
151
|
* Resolution order:
|
|
89
|
-
* 1. Check for local override in .fraim/
|
|
90
|
-
* 2.
|
|
91
|
-
* 3. If
|
|
152
|
+
* 1. Check for local override in .fraim/personalized-employee/
|
|
153
|
+
* 2. Fallback to .fraim/overrides/ (legacy)
|
|
154
|
+
* 3. If found, read and resolve inheritance
|
|
155
|
+
* 4. If not found, fetch from remote
|
|
92
156
|
*
|
|
93
157
|
* @param path - Registry path (e.g., "workflows/product-building/spec.md")
|
|
94
158
|
* @returns Resolved file with metadata
|
|
95
159
|
*/
|
|
96
|
-
async resolveFile(path) {
|
|
160
|
+
async resolveFile(path, options = {}) {
|
|
97
161
|
console.error(`[LocalRegistryResolver] ===== resolveFile called for: ${path} =====`);
|
|
162
|
+
const includeMetadata = options.includeMetadata ?? true;
|
|
163
|
+
const stripMcpHeader = options.stripMcpHeader ?? false;
|
|
98
164
|
// Check for local override
|
|
99
165
|
if (!this.hasLocalOverride(path)) {
|
|
166
|
+
const syncedLocalContent = this.readSyncedLocalFile(path);
|
|
167
|
+
if (syncedLocalContent !== null) {
|
|
168
|
+
return {
|
|
169
|
+
content: syncedLocalContent,
|
|
170
|
+
source: 'local',
|
|
171
|
+
inherited: false
|
|
172
|
+
};
|
|
173
|
+
}
|
|
100
174
|
// No override, fetch from remote
|
|
101
175
|
try {
|
|
102
|
-
const
|
|
176
|
+
const rawContent = await this.remoteContentResolver(path);
|
|
177
|
+
const content = stripMcpHeader ? this.stripMcpHeader(rawContent) : rawContent;
|
|
103
178
|
return {
|
|
104
179
|
content,
|
|
105
180
|
source: 'remote',
|
|
@@ -118,7 +193,8 @@ class LocalRegistryResolver {
|
|
|
118
193
|
catch (error) {
|
|
119
194
|
// If local read fails, fall back to remote
|
|
120
195
|
console.warn(`Local override read failed, falling back to remote: ${path}`);
|
|
121
|
-
const
|
|
196
|
+
const rawContent = await this.remoteContentResolver(path);
|
|
197
|
+
const content = stripMcpHeader ? this.stripMcpHeader(rawContent) : rawContent;
|
|
122
198
|
return {
|
|
123
199
|
content,
|
|
124
200
|
source: 'remote',
|
|
@@ -154,12 +230,93 @@ class LocalRegistryResolver {
|
|
|
154
230
|
imports: resolved.imports.length > 0 ? resolved.imports : undefined
|
|
155
231
|
};
|
|
156
232
|
// Add metadata comment
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
233
|
+
if (includeMetadata) {
|
|
234
|
+
const metadata = this.generateMetadata(result);
|
|
235
|
+
if (metadata) {
|
|
236
|
+
result.metadata = metadata;
|
|
237
|
+
result.content = metadata + result.content;
|
|
238
|
+
}
|
|
161
239
|
}
|
|
162
240
|
return result;
|
|
163
241
|
}
|
|
242
|
+
sanitizeIncludePath(path) {
|
|
243
|
+
const trimmed = path.trim().replace(/\\/g, '/');
|
|
244
|
+
if (!trimmed)
|
|
245
|
+
return null;
|
|
246
|
+
if (trimmed.includes('..'))
|
|
247
|
+
return null;
|
|
248
|
+
if (trimmed.startsWith('/'))
|
|
249
|
+
return null;
|
|
250
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed))
|
|
251
|
+
return null;
|
|
252
|
+
return trimmed;
|
|
253
|
+
}
|
|
254
|
+
async resolveIncludesInternal(content, options) {
|
|
255
|
+
if (options.depth >= options.maxDepth) {
|
|
256
|
+
return content;
|
|
257
|
+
}
|
|
258
|
+
const includePattern = /\{\{include:([^}]+)\}\}/g;
|
|
259
|
+
const matches = Array.from(content.matchAll(includePattern));
|
|
260
|
+
if (matches.length === 0) {
|
|
261
|
+
return content;
|
|
262
|
+
}
|
|
263
|
+
const resolvedByPath = new Map();
|
|
264
|
+
for (const match of matches) {
|
|
265
|
+
const includePath = this.sanitizeIncludePath(match[1]);
|
|
266
|
+
if (!includePath)
|
|
267
|
+
continue;
|
|
268
|
+
if (resolvedByPath.has(includePath))
|
|
269
|
+
continue;
|
|
270
|
+
if (options.cache.has(includePath)) {
|
|
271
|
+
resolvedByPath.set(includePath, options.cache.get(includePath));
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (options.stack.has(includePath)) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
options.stack.add(includePath);
|
|
279
|
+
const resolved = await this.resolveFile(includePath, {
|
|
280
|
+
includeMetadata: false,
|
|
281
|
+
stripMcpHeader: true
|
|
282
|
+
});
|
|
283
|
+
const resolvedContent = await this.resolveIncludesInternal(resolved.content, {
|
|
284
|
+
depth: options.depth + 1,
|
|
285
|
+
maxDepth: options.maxDepth,
|
|
286
|
+
stack: options.stack,
|
|
287
|
+
cache: options.cache
|
|
288
|
+
});
|
|
289
|
+
options.cache.set(includePath, resolvedContent);
|
|
290
|
+
resolvedByPath.set(includePath, resolvedContent);
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
console.warn(`Failed to resolve include ${includePath}: ${error.message}`);
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
options.stack.delete(includePath);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return content.replace(includePattern, (fullMatch, includeRef) => {
|
|
300
|
+
const includePath = this.sanitizeIncludePath(includeRef);
|
|
301
|
+
if (!includePath)
|
|
302
|
+
return fullMatch;
|
|
303
|
+
return resolvedByPath.get(includePath) ?? fullMatch;
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Resolve {{include:path}} directives using local override precedence:
|
|
308
|
+
* 1. .fraim/personalized-employee/
|
|
309
|
+
* 2. .fraim/overrides/ (legacy)
|
|
310
|
+
* 3. remote resolver
|
|
311
|
+
*/
|
|
312
|
+
async resolveIncludes(content, maxDepth = LocalRegistryResolver.MAX_INCLUDE_DEPTH) {
|
|
313
|
+
return this.resolveIncludesInternal(content, {
|
|
314
|
+
depth: 0,
|
|
315
|
+
maxDepth,
|
|
316
|
+
stack: new Set(),
|
|
317
|
+
cache: new Map()
|
|
318
|
+
});
|
|
319
|
+
}
|
|
164
320
|
}
|
|
165
321
|
exports.LocalRegistryResolver = LocalRegistryResolver;
|
|
322
|
+
LocalRegistryResolver.MAX_INCLUDE_DEPTH = 10;
|
|
@@ -2,6 +2,48 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generateWorkflowStub = generateWorkflowStub;
|
|
4
4
|
exports.parseRegistryWorkflow = parseRegistryWorkflow;
|
|
5
|
+
exports.generateJobStub = generateJobStub;
|
|
6
|
+
exports.generateSkillStub = generateSkillStub;
|
|
7
|
+
exports.generateRuleStub = generateRuleStub;
|
|
8
|
+
exports.parseRegistryJob = parseRegistryJob;
|
|
9
|
+
exports.parseRegistrySkill = parseRegistrySkill;
|
|
10
|
+
exports.parseRegistryRule = parseRegistryRule;
|
|
11
|
+
function extractSection(content, headingPatterns) {
|
|
12
|
+
for (const heading of headingPatterns) {
|
|
13
|
+
const pattern = new RegExp(`(?:^|\\n)#{2,3}\\s*${heading}\\s*\\n([\\s\\S]*?)(?=\\n#{2,3}\\s|$)`, 'i');
|
|
14
|
+
const match = content.match(pattern);
|
|
15
|
+
if (match?.[1]) {
|
|
16
|
+
const section = match[1].trim();
|
|
17
|
+
if (section.length > 0)
|
|
18
|
+
return section;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
function extractLeadParagraph(content) {
|
|
24
|
+
const withoutFrontmatter = content.replace(/^---[\s\S]*?---\s*/m, '');
|
|
25
|
+
const lines = withoutFrontmatter.split(/\r?\n/);
|
|
26
|
+
const paragraphLines = [];
|
|
27
|
+
let started = false;
|
|
28
|
+
for (const rawLine of lines) {
|
|
29
|
+
const line = rawLine.trim();
|
|
30
|
+
if (!started) {
|
|
31
|
+
if (!line || line.startsWith('#'))
|
|
32
|
+
continue;
|
|
33
|
+
if (line.startsWith('-') || /^\d+\./.test(line))
|
|
34
|
+
continue;
|
|
35
|
+
started = true;
|
|
36
|
+
}
|
|
37
|
+
if (!line)
|
|
38
|
+
break;
|
|
39
|
+
if (line.startsWith('#'))
|
|
40
|
+
break;
|
|
41
|
+
paragraphLines.push(line);
|
|
42
|
+
}
|
|
43
|
+
if (paragraphLines.length === 0)
|
|
44
|
+
return null;
|
|
45
|
+
return paragraphLines.join(' ').trim();
|
|
46
|
+
}
|
|
5
47
|
/**
|
|
6
48
|
* Generates a lightweight markdown stub for a workflow.
|
|
7
49
|
* These stubs are committed to the user's repo for discoverability.
|
|
@@ -33,3 +75,100 @@ function parseRegistryWorkflow(content) {
|
|
|
33
75
|
: [];
|
|
34
76
|
return { intent, principles };
|
|
35
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Coaching stubs are discoverability artifacts and should be resolved with get_fraim_file.
|
|
80
|
+
*/
|
|
81
|
+
function generateJobStub(jobName, _jobPath, intent, outcome) {
|
|
82
|
+
return `# FRAIM Job: ${jobName}
|
|
83
|
+
|
|
84
|
+
## Intent
|
|
85
|
+
${intent}
|
|
86
|
+
|
|
87
|
+
## Outcome
|
|
88
|
+
${outcome}
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
> [!IMPORTANT]
|
|
93
|
+
> **For AI Agents:** Do NOT attempt to execute this job based on the Intent/Outcome above.
|
|
94
|
+
> This stub is for discoverability only. The actual job has multiple phases with
|
|
95
|
+
> detailed steps, validation criteria, and required skills.
|
|
96
|
+
>
|
|
97
|
+
> To get full phase-by-phase instructions, call:
|
|
98
|
+
> \`get_fraim_job({ job: "${jobName}" })\`
|
|
99
|
+
>
|
|
100
|
+
> Then follow all phases using \`seekMentoring\` at each phase transition.
|
|
101
|
+
`;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Generates a lightweight markdown stub for a skill.
|
|
105
|
+
*/
|
|
106
|
+
function generateSkillStub(skillName, skillPath, intent, outcome) {
|
|
107
|
+
return `# FRAIM Skill: ${skillName}
|
|
108
|
+
|
|
109
|
+
## Intent
|
|
110
|
+
${intent}
|
|
111
|
+
|
|
112
|
+
## Outcome
|
|
113
|
+
${outcome}
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
> [!IMPORTANT]
|
|
118
|
+
> **For AI Agents:** This is a skill stub for discoverability.
|
|
119
|
+
> To retrieve the complete skill instructions, call:
|
|
120
|
+
> \`get_fraim_file({ path: "skills/${skillPath}" })\`
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Generates a lightweight markdown stub for a rule.
|
|
125
|
+
*/
|
|
126
|
+
function generateRuleStub(ruleName, rulePath, intent, outcome) {
|
|
127
|
+
return `# FRAIM Rule: ${ruleName}
|
|
128
|
+
|
|
129
|
+
## Intent
|
|
130
|
+
${intent}
|
|
131
|
+
|
|
132
|
+
## Outcome
|
|
133
|
+
${outcome}
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
> [!IMPORTANT]
|
|
138
|
+
> **For AI Agents:** This is a rule stub for discoverability.
|
|
139
|
+
> To retrieve the complete rule instructions, call:
|
|
140
|
+
> \`get_fraim_file({ path: "rules/${rulePath}" })\`
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Parses a job file from the registry to extract its intent and outcome for the stub.
|
|
145
|
+
*/
|
|
146
|
+
function parseRegistryJob(content) {
|
|
147
|
+
const intentMatch = content.match(/##\s*intent\s+([\s\S]*?)(?=\n##|$)/i);
|
|
148
|
+
const outcomeMatch = content.match(/##\s*outcome\s+([\s\S]*?)(?=\n##|$)/i);
|
|
149
|
+
const intent = intentMatch ? intentMatch[1].trim() : 'No intent defined.';
|
|
150
|
+
const outcome = outcomeMatch ? outcomeMatch[1].trim() : 'No outcome defined.';
|
|
151
|
+
return { intent, outcome };
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Parses a skill file from the registry to extract intent and expected outcome for stubs.
|
|
155
|
+
*/
|
|
156
|
+
function parseRegistrySkill(content) {
|
|
157
|
+
const intent = extractSection(content, ['intent', 'skill intent']) ||
|
|
158
|
+
extractLeadParagraph(content) ||
|
|
159
|
+
'Apply the skill correctly using the provided inputs and constraints.';
|
|
160
|
+
const outcome = extractSection(content, ['outcome', 'expected behavior', 'skill output']) ||
|
|
161
|
+
'Produce the expected skill output while following skill guardrails.';
|
|
162
|
+
return { intent, outcome };
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Parses a rule file from the registry to extract intent and expected behavior for stubs.
|
|
166
|
+
*/
|
|
167
|
+
function parseRegistryRule(content) {
|
|
168
|
+
const intent = extractSection(content, ['intent']) ||
|
|
169
|
+
extractLeadParagraph(content) ||
|
|
170
|
+
'Follow this rule when executing related FRAIM workflows and jobs.';
|
|
171
|
+
const outcome = extractSection(content, ['outcome', 'expected behavior', 'principles']) ||
|
|
172
|
+
'Consistently apply this rule throughout execution.';
|
|
173
|
+
return { intent, outcome };
|
|
174
|
+
}
|
|
@@ -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",
|