fraim-framework 2.0.83 → 2.0.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -3
- package/bin/fraim-mcp.js +1 -1
- package/dist/src/cli/commands/add-ide.js +1 -1
- package/dist/src/cli/commands/init-project.js +1 -1
- package/dist/src/cli/commands/list-overridable.js +19 -15
- package/dist/src/cli/commands/override.js +9 -2
- package/dist/src/cli/commands/setup.js +44 -7
- package/dist/src/cli/commands/sync.js +34 -23
- package/dist/src/cli/doctor/checks/workflow-checks.js +12 -4
- package/dist/src/cli/fraim.js +0 -2
- package/dist/src/cli/mcp/mcp-server-registry.js +2 -0
- package/dist/src/cli/setup/auto-mcp-setup.js +12 -1
- package/dist/src/cli/utils/remote-sync.js +82 -21
- package/dist/src/core/utils/local-registry-resolver.js +171 -14
- package/dist/src/core/utils/stub-generator.js +139 -0
- package/dist/src/core/utils/workflow-parser.js +5 -1
- package/dist/src/local-mcp-server/stdio-server.js +174 -63
- package/index.js +1 -1
- package/package.json +6 -3
|
@@ -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
|
+
}
|
|
@@ -13,7 +13,11 @@ class WorkflowParser {
|
|
|
13
13
|
static parse(filePath) {
|
|
14
14
|
if (!(0, fs_1.existsSync)(filePath))
|
|
15
15
|
return null;
|
|
16
|
-
|
|
16
|
+
let content = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
17
|
+
// Strip optional UTF-8 BOM so frontmatter regex remains stable across editors.
|
|
18
|
+
if (content.charCodeAt(0) === 0xfeff) {
|
|
19
|
+
content = content.slice(1);
|
|
20
|
+
}
|
|
17
21
|
// Try to extract JSON Metadata (frontmatter)
|
|
18
22
|
const metadataMatch = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
|
|
19
23
|
if (metadataMatch) {
|