fraim-framework 2.0.87 → 2.0.89
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 +30 -0
- package/bin/fraim.js +1 -1
- package/dist/src/cli/commands/add-provider.js +16 -6
- package/dist/src/cli/commands/init-project.js +103 -1
- package/dist/src/cli/commands/login.js +84 -0
- package/dist/src/cli/commands/setup.js +135 -13
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/internal/device-flow-service.js +83 -0
- package/dist/src/cli/mcp/mcp-server-registry.js +11 -10
- package/dist/src/cli/providers/local-provider-registry.js +22 -1
- package/dist/src/cli/setup/provider-prompts.js +39 -0
- package/dist/src/cli/utils/remote-sync.js +72 -32
- package/dist/src/core/ai-mentor.js +248 -0
- package/dist/src/core/utils/git-utils.js +6 -6
- package/dist/src/core/utils/include-resolver.js +45 -0
- package/dist/src/core/utils/inheritance-parser.js +154 -16
- package/dist/src/core/utils/local-registry-resolver.js +326 -22
- package/dist/src/core/utils/server-startup.js +34 -0
- package/dist/src/core/utils/stub-generator.js +62 -55
- package/dist/src/core/utils/workflow-parser.js +103 -46
- package/dist/src/local-mcp-server/stdio-server.js +240 -284
- package/index.js +27 -6
- package/package.json +14 -5
|
@@ -81,14 +81,14 @@ class InheritanceParser {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
|
-
* Resolve all import directives in content recursively
|
|
84
|
+
* Resolve all import and extends directives in content recursively
|
|
85
85
|
*
|
|
86
|
-
* @param content - Content with {{ import }}
|
|
86
|
+
* @param content - Content with {{ import }} or extends frontmatter
|
|
87
87
|
* @param currentPath - Path of current file (for circular detection)
|
|
88
88
|
* @param options - Resolution options
|
|
89
|
-
* @returns Resolved content with all
|
|
89
|
+
* @returns Resolved content with all inheritance applied
|
|
90
90
|
*
|
|
91
|
-
* @throws {InheritanceError} If circular
|
|
91
|
+
* @throws {InheritanceError} If circular inheritance, path traversal, or max depth exceeded
|
|
92
92
|
*/
|
|
93
93
|
async resolve(content, currentPath, options) {
|
|
94
94
|
const depth = options.currentDepth || 0;
|
|
@@ -96,18 +96,53 @@ class InheritanceParser {
|
|
|
96
96
|
const maxDepth = options.maxDepth || this.maxDepth;
|
|
97
97
|
// Check depth limit
|
|
98
98
|
if (depth > maxDepth) {
|
|
99
|
-
throw new InheritanceError(`Max
|
|
99
|
+
throw new InheritanceError(`Max inheritance depth exceeded (${maxDepth})`, currentPath);
|
|
100
100
|
}
|
|
101
|
-
// Check circular
|
|
101
|
+
// Check circular inheritance (but allow importing/extending the same path as parent)
|
|
102
102
|
this.detectCircularImport(currentPath, visited, false);
|
|
103
103
|
visited.add(currentPath);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
let resolvedContent = content;
|
|
105
|
+
// 1. Handle JSON frontmatter 'extends'
|
|
106
|
+
const metadataMatch = resolvedContent.match(/^---\r?\n([\s\S]+?)\r?\n---/);
|
|
107
|
+
if (metadataMatch) {
|
|
108
|
+
try {
|
|
109
|
+
const metadata = JSON.parse(metadataMatch[1]);
|
|
110
|
+
const extendsPath = metadata.extends;
|
|
111
|
+
if (extendsPath && typeof extendsPath === 'string') {
|
|
112
|
+
// Sanitize path
|
|
113
|
+
const sanitizedExtends = this.sanitizePath(extendsPath);
|
|
114
|
+
const isParentExtends = sanitizedExtends === currentPath;
|
|
115
|
+
// Fetch parent content
|
|
116
|
+
let parentContent;
|
|
117
|
+
try {
|
|
118
|
+
parentContent = await options.fetchParent(sanitizedExtends);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
throw new InheritanceError(`Failed to fetch extended parent content: ${sanitizedExtends}. ${error.message}`, sanitizedExtends);
|
|
122
|
+
}
|
|
123
|
+
// Recursively resolve parent
|
|
124
|
+
const parentVisited = isParentExtends ? new Set() : new Set(visited);
|
|
125
|
+
const resolvedParent = await this.resolve(parentContent, sanitizedExtends, {
|
|
126
|
+
...options,
|
|
127
|
+
currentDepth: depth + 1,
|
|
128
|
+
visited: parentVisited
|
|
129
|
+
});
|
|
130
|
+
// Merge current content with resolved parent
|
|
131
|
+
resolvedContent = this.mergeContent(resolvedContent, resolvedParent);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (error instanceof SyntaxError) {
|
|
136
|
+
// Not JSON or invalid JSON, ignore extends logic but log it
|
|
137
|
+
console.warn(`[InheritanceParser] Failed to parse frontmatter for ${currentPath}: ${error.message}`);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
108
143
|
}
|
|
109
|
-
//
|
|
110
|
-
|
|
144
|
+
// 2. Handle {{ import: path }}
|
|
145
|
+
const imports = this.extractImports(resolvedContent);
|
|
111
146
|
for (const importPath of imports) {
|
|
112
147
|
// Sanitize path
|
|
113
148
|
const sanitized = this.sanitizePath(importPath);
|
|
@@ -122,7 +157,6 @@ class InheritanceParser {
|
|
|
122
157
|
throw new InheritanceError(`Failed to fetch parent content: ${sanitized}. ${error.message}`, sanitized);
|
|
123
158
|
}
|
|
124
159
|
// Recursively resolve parent imports
|
|
125
|
-
// For parent imports, use a fresh visited set to allow the same path
|
|
126
160
|
const parentVisited = isParentImport ? new Set() : new Set(visited);
|
|
127
161
|
const resolvedParent = await this.resolve(parentContent, sanitized, {
|
|
128
162
|
...options,
|
|
@@ -131,20 +165,124 @@ class InheritanceParser {
|
|
|
131
165
|
});
|
|
132
166
|
// Replace import directive with resolved parent content
|
|
133
167
|
const importDirective = `{{ import: ${importPath} }}`;
|
|
134
|
-
|
|
168
|
+
resolvedContent = resolvedContent.replace(importDirective, resolvedParent);
|
|
135
169
|
}
|
|
136
|
-
return
|
|
170
|
+
return resolvedContent;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Merge two registry files (child override + parent base)
|
|
174
|
+
*
|
|
175
|
+
* Merging rules:
|
|
176
|
+
* 1. Metadata: JSON merge (child overrides parent)
|
|
177
|
+
* 2. Overview: Parent overview + child overview (if multi-para)
|
|
178
|
+
* 3. Phases: Phase override (child phase with same ID replaces parent phase)
|
|
179
|
+
*/
|
|
180
|
+
mergeContent(child, parent) {
|
|
181
|
+
const childMatch = child.match(/^---\r?\n([\s\S]+?)\r?\n---/);
|
|
182
|
+
const parentMatch = parent.match(/^---\r?\n([\s\S]+?)\r?\n---/);
|
|
183
|
+
if (!childMatch || !parentMatch)
|
|
184
|
+
return child;
|
|
185
|
+
// 1. Merge Metadata
|
|
186
|
+
const childMeta = JSON.parse(childMatch[1]);
|
|
187
|
+
const parentMeta = JSON.parse(parentMatch[1]);
|
|
188
|
+
const mergedMeta = { ...parentMeta, ...childMeta };
|
|
189
|
+
delete mergedMeta.extends; // Remove extends from final merged content
|
|
190
|
+
// 2. Extract Body (everything after frontmatter)
|
|
191
|
+
const childBody = this.stripRedundantParentImports(child.substring(childMatch[0].length).trim(), typeof childMeta.extends === 'string' ? childMeta.extends : undefined);
|
|
192
|
+
const parentBody = parent.substring(parentMatch[0].length).trim();
|
|
193
|
+
// 3. Parse Phases and Overview
|
|
194
|
+
const parsePhases = (body) => {
|
|
195
|
+
const phases = new Map();
|
|
196
|
+
const sections = body.split(/^##\s+Phase:\s+/m);
|
|
197
|
+
const overview = sections[0]?.trim() || '';
|
|
198
|
+
for (let i = 1; i < sections.length; i++) {
|
|
199
|
+
const section = sections[i];
|
|
200
|
+
if (!section.trim())
|
|
201
|
+
continue;
|
|
202
|
+
// Extract ID from first line: e.g. "implement-scoping (Primary)" -> "implement-scoping"
|
|
203
|
+
const firstLine = section.split(/\r?\n/)[0].trim();
|
|
204
|
+
const id = firstLine.split(/[ (]/)[0].trim().toLowerCase();
|
|
205
|
+
phases.set(id, `## Phase: ${section.trim()}`);
|
|
206
|
+
}
|
|
207
|
+
return { overview, phases };
|
|
208
|
+
};
|
|
209
|
+
const childParts = parsePhases(childBody);
|
|
210
|
+
const parentParts = parsePhases(parentBody);
|
|
211
|
+
// 4. Merge Overview: retain the parent framing, then append local overview additions.
|
|
212
|
+
const mergedOverview = childParts.overview
|
|
213
|
+
? `${parentParts.overview}\n\n${childParts.overview}`.trim()
|
|
214
|
+
: parentParts.overview;
|
|
215
|
+
// 5. Merge Phases
|
|
216
|
+
const mergedPhases = new Map(parentParts.phases);
|
|
217
|
+
for (const [id, content] of childParts.phases.entries()) {
|
|
218
|
+
mergedPhases.set(id, content);
|
|
219
|
+
}
|
|
220
|
+
// 6. Reassemble
|
|
221
|
+
let finalContent = `---\n${JSON.stringify(mergedMeta, null, 2)}\n---\n\n`;
|
|
222
|
+
if (mergedOverview) {
|
|
223
|
+
finalContent += `${mergedOverview}\n\n`;
|
|
224
|
+
}
|
|
225
|
+
const addedPhases = new Set();
|
|
226
|
+
// First, add parent phases in order, using child overrides when present.
|
|
227
|
+
for (const id of parentParts.phases.keys()) {
|
|
228
|
+
if (mergedPhases.has(id)) {
|
|
229
|
+
finalContent += `${mergedPhases.get(id)}\n\n`;
|
|
230
|
+
addedPhases.add(id);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Then append child-only phases in the order the child declared them.
|
|
234
|
+
for (const [id, content] of childParts.phases.entries()) {
|
|
235
|
+
if (!addedPhases.has(id)) {
|
|
236
|
+
finalContent += `${content}\n\n`;
|
|
237
|
+
addedPhases.add(id);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return finalContent.trim();
|
|
137
241
|
}
|
|
138
242
|
/**
|
|
139
243
|
* Parse content and return detailed information about imports
|
|
140
244
|
*/
|
|
141
245
|
parse(content) {
|
|
142
246
|
const imports = this.extractImports(content);
|
|
247
|
+
const hasExtends = /^---\r?\n[\s\S]*?"extends":\s*"[^"]+"[\s\S]*?\r?\n---/m.test(content);
|
|
143
248
|
return {
|
|
144
249
|
content,
|
|
145
250
|
imports,
|
|
146
|
-
hasImports: imports.length > 0
|
|
251
|
+
hasImports: imports.length > 0 || hasExtends
|
|
147
252
|
};
|
|
148
253
|
}
|
|
254
|
+
normalizeImportRef(path) {
|
|
255
|
+
let normalized = path.trim().replace(/\\/g, '/').replace(/^\/+/, '');
|
|
256
|
+
if (normalized.endsWith('.md')) {
|
|
257
|
+
normalized = normalized.slice(0, -3);
|
|
258
|
+
}
|
|
259
|
+
return normalized;
|
|
260
|
+
}
|
|
261
|
+
stripTypePrefix(path) {
|
|
262
|
+
return path.replace(/^(jobs|workflows|skills|rules|templates)\//, '');
|
|
263
|
+
}
|
|
264
|
+
isEquivalentImportRef(left, right) {
|
|
265
|
+
const normalizedLeft = this.normalizeImportRef(left);
|
|
266
|
+
const normalizedRight = this.normalizeImportRef(right);
|
|
267
|
+
const strippedLeft = this.stripTypePrefix(normalizedLeft);
|
|
268
|
+
const strippedRight = this.stripTypePrefix(normalizedRight);
|
|
269
|
+
return normalizedLeft === normalizedRight ||
|
|
270
|
+
strippedLeft === strippedRight ||
|
|
271
|
+
normalizedLeft.endsWith(`/${strippedRight}`) ||
|
|
272
|
+
normalizedRight.endsWith(`/${strippedLeft}`) ||
|
|
273
|
+
strippedLeft.endsWith(`/${strippedRight}`) ||
|
|
274
|
+
strippedRight.endsWith(`/${strippedLeft}`);
|
|
275
|
+
}
|
|
276
|
+
stripRedundantParentImports(body, extendsPath) {
|
|
277
|
+
if (!extendsPath) {
|
|
278
|
+
return body;
|
|
279
|
+
}
|
|
280
|
+
return body
|
|
281
|
+
.replace(/\{\{\s*import:\s*([^\}]+)\s*\}\}\s*\r?\n?/g, (match, importPath) => {
|
|
282
|
+
return this.isEquivalentImportRef(importPath, extendsPath) ? '' : match;
|
|
283
|
+
})
|
|
284
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
285
|
+
.trim();
|
|
286
|
+
}
|
|
149
287
|
}
|
|
150
288
|
exports.InheritanceParser = InheritanceParser;
|
|
@@ -5,16 +5,52 @@
|
|
|
5
5
|
* Resolves registry file requests by checking for local overrides first,
|
|
6
6
|
* then falling back to remote registry. Handles inheritance via InheritanceParser.
|
|
7
7
|
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
8
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
42
|
exports.LocalRegistryResolver = void 0;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
10
44
|
const fs_1 = require("fs");
|
|
11
45
|
const path_1 = require("path");
|
|
12
46
|
const inheritance_parser_1 = require("./inheritance-parser");
|
|
47
|
+
const workflow_parser_1 = require("./workflow-parser");
|
|
13
48
|
class LocalRegistryResolver {
|
|
14
49
|
constructor(options) {
|
|
15
50
|
this.workspaceRoot = options.workspaceRoot;
|
|
16
51
|
this.remoteContentResolver = options.remoteContentResolver;
|
|
17
52
|
this.parser = new inheritance_parser_1.InheritanceParser(options.maxDepth);
|
|
53
|
+
this.shouldFilter = options.shouldFilter;
|
|
18
54
|
}
|
|
19
55
|
/**
|
|
20
56
|
* Check if a local override exists for the given path
|
|
@@ -22,9 +58,52 @@ class LocalRegistryResolver {
|
|
|
22
58
|
hasLocalOverride(path) {
|
|
23
59
|
const primaryPath = this.getOverridePath(path);
|
|
24
60
|
const legacyPath = this.getLegacyOverridePath(path);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
61
|
+
return (0, fs_1.existsSync)(primaryPath) || (0, fs_1.existsSync)(legacyPath);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Find the actual relative path for a registry item by searching subdirectories.
|
|
65
|
+
* Returns '{type}/{category}/{name}.md' or '{type}/{name}.md'.
|
|
66
|
+
*/
|
|
67
|
+
async findRegistryPath(type, name) {
|
|
68
|
+
const baseName = `${name}.md`;
|
|
69
|
+
const searchDirs = [type];
|
|
70
|
+
for (const dir of searchDirs) {
|
|
71
|
+
// Check literal path first
|
|
72
|
+
const literal = `${dir}/${baseName}`;
|
|
73
|
+
if (this.hasLocalOverride(literal))
|
|
74
|
+
return literal;
|
|
75
|
+
// Deep search
|
|
76
|
+
const fullBaseDir = (0, path_1.join)(this.workspaceRoot, '.fraim', 'personalized-employee', dir);
|
|
77
|
+
const legacyBaseDir = (0, path_1.join)(this.workspaceRoot, '.fraim', 'overrides', dir);
|
|
78
|
+
const found = this.searchFileRecursively(fullBaseDir, baseName) ||
|
|
79
|
+
this.searchFileRecursively(legacyBaseDir, baseName);
|
|
80
|
+
if (found) {
|
|
81
|
+
// Convert absolute back to relative
|
|
82
|
+
const rel = found.replace(/\\/g, '/');
|
|
83
|
+
const dirMarker = `/${dir}/`;
|
|
84
|
+
if (rel.includes(dirMarker)) {
|
|
85
|
+
return rel.substring(rel.indexOf(dirMarker) + 1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return `${type}/${name}.md`;
|
|
90
|
+
}
|
|
91
|
+
searchFileRecursively(dir, fileName) {
|
|
92
|
+
if (!(0, fs_1.existsSync)(dir))
|
|
93
|
+
return null;
|
|
94
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
const fullPath = (0, path_1.join)(dir, entry.name);
|
|
97
|
+
if (entry.isDirectory()) {
|
|
98
|
+
const found = this.searchFileRecursively(fullPath, fileName);
|
|
99
|
+
if (found)
|
|
100
|
+
return found;
|
|
101
|
+
}
|
|
102
|
+
else if (entry.name === fileName) {
|
|
103
|
+
return fullPath;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
28
107
|
}
|
|
29
108
|
/**
|
|
30
109
|
* Check if a locally synced skill/rule file exists for the given registry path.
|
|
@@ -37,14 +116,24 @@ class LocalRegistryResolver {
|
|
|
37
116
|
* Get the full path to a local override file
|
|
38
117
|
*/
|
|
39
118
|
getOverridePath(path) {
|
|
40
|
-
|
|
119
|
+
const normalized = path.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
120
|
+
// Personal overrides are in .fraim/personalized-employee/
|
|
121
|
+
// We don't need a redundant 'registry/' subfolder here as the path already includes type (e.g. jobs/)
|
|
122
|
+
return (0, path_1.join)(this.workspaceRoot, '.fraim', 'personalized-employee', normalized);
|
|
41
123
|
}
|
|
42
124
|
/**
|
|
43
125
|
* Get the full path to a legacy local override file.
|
|
44
126
|
* Kept for backward compatibility while migrating to personalized-employee.
|
|
45
127
|
*/
|
|
46
128
|
getLegacyOverridePath(path) {
|
|
47
|
-
|
|
129
|
+
const normalized = path.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
130
|
+
const parts = normalized.split('/');
|
|
131
|
+
let lookupPath = normalized;
|
|
132
|
+
if (parts.length > 1 && (parts[0] === 'jobs' || parts[0] === 'workflows' || parts[0] === 'skills' || parts[0] === 'rules')) {
|
|
133
|
+
lookupPath = parts.slice(1).join('/');
|
|
134
|
+
return (0, path_1.join)(this.workspaceRoot, '.fraim', 'overrides', parts[0], lookupPath);
|
|
135
|
+
}
|
|
136
|
+
return (0, path_1.join)(this.workspaceRoot, '.fraim/overrides', normalized);
|
|
48
137
|
}
|
|
49
138
|
/**
|
|
50
139
|
* Get the full path to a locally synced FRAIM file when available.
|
|
@@ -54,9 +143,58 @@ class LocalRegistryResolver {
|
|
|
54
143
|
*/
|
|
55
144
|
getSyncedFilePath(path) {
|
|
56
145
|
const normalizedPath = path.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
146
|
+
// 1. Workflows: workflows/[role]/path -> .fraim/[role]/workflows/path
|
|
147
|
+
if (normalizedPath.startsWith('workflows/')) {
|
|
148
|
+
const parts = normalizedPath.split('/');
|
|
149
|
+
if (parts.length >= 3 && (parts[1] === 'ai-employee' || parts[1] === 'ai-manager')) {
|
|
150
|
+
const role = parts[1];
|
|
151
|
+
const subPath = parts.slice(2).join('/');
|
|
152
|
+
return (0, path_1.join)(this.workspaceRoot, '.fraim', role, 'workflows', subPath);
|
|
153
|
+
}
|
|
154
|
+
// Fallback: Try ai-employee and ai-manager if no role prefix
|
|
155
|
+
const subPath = normalizedPath.substring('workflows/'.length);
|
|
156
|
+
const employeePath = (0, path_1.join)(this.workspaceRoot, '.fraim', 'ai-employee', 'workflows', subPath);
|
|
157
|
+
if (fs.existsSync(employeePath))
|
|
158
|
+
return employeePath;
|
|
159
|
+
const managerPath = (0, path_1.join)(this.workspaceRoot, '.fraim', 'ai-manager', 'workflows', subPath);
|
|
160
|
+
if (fs.existsSync(managerPath))
|
|
161
|
+
return managerPath;
|
|
162
|
+
// Fallback for non-role-prefixed (legacy or direct)
|
|
163
|
+
return (0, path_1.join)(this.workspaceRoot, '.fraim', normalizedPath);
|
|
164
|
+
}
|
|
165
|
+
// 2. Jobs: jobs/[role]/path -> .fraim/[role]/jobs/path
|
|
166
|
+
if (normalizedPath.startsWith('jobs/')) {
|
|
167
|
+
const parts = normalizedPath.split('/');
|
|
168
|
+
if (parts.length >= 3 && (parts[1] === 'ai-employee' || parts[1] === 'ai-manager')) {
|
|
169
|
+
const role = parts[1];
|
|
170
|
+
const subPath = parts.slice(2).join('/');
|
|
171
|
+
return (0, path_1.join)(this.workspaceRoot, '.fraim', role, 'jobs', subPath);
|
|
172
|
+
}
|
|
173
|
+
// Fallback: Try ai-employee and ai-manager if no role prefix
|
|
174
|
+
const subPath = normalizedPath.substring('jobs/'.length);
|
|
175
|
+
const employeePath = (0, path_1.join)(this.workspaceRoot, '.fraim', 'ai-employee', 'jobs', subPath);
|
|
176
|
+
if (fs.existsSync(employeePath))
|
|
177
|
+
return employeePath;
|
|
178
|
+
const managerPath = (0, path_1.join)(this.workspaceRoot, '.fraim', 'ai-manager', 'jobs', subPath);
|
|
179
|
+
if (fs.existsSync(managerPath))
|
|
180
|
+
return managerPath;
|
|
181
|
+
// Fallback
|
|
182
|
+
return (0, path_1.join)(this.workspaceRoot, '.fraim', normalizedPath);
|
|
183
|
+
}
|
|
184
|
+
// 3. Rules: [role]/rules/path -> .fraim/[role]/rules/path
|
|
185
|
+
if (normalizedPath.includes('/rules/')) {
|
|
186
|
+
const role = normalizedPath.includes('/ai-manager/') ? 'ai-manager' : 'ai-employee';
|
|
187
|
+
// Extract the part after "rules/"
|
|
188
|
+
const rulesIdx = normalizedPath.indexOf('rules/');
|
|
189
|
+
const subPath = normalizedPath.substring(rulesIdx + 'rules/'.length);
|
|
190
|
+
return (0, path_1.join)(this.workspaceRoot, '.fraim', role, 'rules', subPath);
|
|
191
|
+
}
|
|
192
|
+
// 4. Skills: skills/path -> .fraim/ai-employee/skills/path (default to ai-employee)
|
|
57
193
|
if (normalizedPath.startsWith('skills/')) {
|
|
58
|
-
|
|
194
|
+
const subPath = normalizedPath.substring('skills/'.length);
|
|
195
|
+
return (0, path_1.join)(this.workspaceRoot, '.fraim/ai-employee', 'skills', subPath);
|
|
59
196
|
}
|
|
197
|
+
// 5. Rules: rules/path -> .fraim/ai-employee/rules/path (default to ai-employee)
|
|
60
198
|
if (normalizedPath.startsWith('rules/')) {
|
|
61
199
|
return (0, path_1.join)(this.workspaceRoot, '.fraim/ai-employee', normalizedPath);
|
|
62
200
|
}
|
|
@@ -84,12 +222,55 @@ class LocalRegistryResolver {
|
|
|
84
222
|
return null;
|
|
85
223
|
}
|
|
86
224
|
try {
|
|
87
|
-
|
|
225
|
+
const content = (0, fs_1.readFileSync)(syncedPath, 'utf-8');
|
|
226
|
+
if (this.shouldFilter && this.shouldFilter(content)) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
return content;
|
|
88
230
|
}
|
|
89
231
|
catch {
|
|
90
232
|
return null;
|
|
91
233
|
}
|
|
92
234
|
}
|
|
235
|
+
normalizeRegistryPath(path) {
|
|
236
|
+
const normalized = path.trim().replace(/\\/g, '/').replace(/^\/+/, '');
|
|
237
|
+
return normalized.endsWith('.md') ? normalized : `${normalized}.md`;
|
|
238
|
+
}
|
|
239
|
+
stripTypePrefix(path) {
|
|
240
|
+
return path.replace(/^(jobs|workflows|skills|rules|templates)\//, '');
|
|
241
|
+
}
|
|
242
|
+
areEquivalentRegistryPaths(left, right) {
|
|
243
|
+
const normalizedLeft = this.normalizeRegistryPath(left);
|
|
244
|
+
const normalizedRight = this.normalizeRegistryPath(right);
|
|
245
|
+
const strippedLeft = this.stripTypePrefix(normalizedLeft);
|
|
246
|
+
const strippedRight = this.stripTypePrefix(normalizedRight);
|
|
247
|
+
return normalizedLeft === normalizedRight ||
|
|
248
|
+
strippedLeft === strippedRight ||
|
|
249
|
+
normalizedLeft.endsWith(`/${strippedRight}`) ||
|
|
250
|
+
normalizedRight.endsWith(`/${strippedLeft}`) ||
|
|
251
|
+
strippedLeft.endsWith(`/${strippedRight}`) ||
|
|
252
|
+
strippedRight.endsWith(`/${strippedLeft}`);
|
|
253
|
+
}
|
|
254
|
+
async fetchRemoteWithFallback(...paths) {
|
|
255
|
+
const candidates = new Set();
|
|
256
|
+
for (const path of paths) {
|
|
257
|
+
if (!path)
|
|
258
|
+
continue;
|
|
259
|
+
const normalized = this.normalizeRegistryPath(path);
|
|
260
|
+
candidates.add(normalized);
|
|
261
|
+
candidates.add(this.stripTypePrefix(normalized));
|
|
262
|
+
}
|
|
263
|
+
let lastError = null;
|
|
264
|
+
for (const candidate of candidates) {
|
|
265
|
+
try {
|
|
266
|
+
return await this.remoteContentResolver(candidate);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
lastError = error;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
throw lastError || new Error(`Failed to fetch remote content for ${paths.join(', ')}`);
|
|
273
|
+
}
|
|
93
274
|
stripMcpHeader(content) {
|
|
94
275
|
const trimmed = content.trimStart();
|
|
95
276
|
if (!trimmed.startsWith('#')) {
|
|
@@ -115,10 +296,44 @@ class LocalRegistryResolver {
|
|
|
115
296
|
if (!parseResult.hasImports) {
|
|
116
297
|
return { content, imports: [] };
|
|
117
298
|
}
|
|
118
|
-
// Resolve
|
|
299
|
+
// Resolve inheritance
|
|
119
300
|
try {
|
|
120
301
|
const resolved = await this.parser.resolve(content, currentPath, {
|
|
121
|
-
fetchParent:
|
|
302
|
+
fetchParent: async (path) => {
|
|
303
|
+
const normalized = this.normalizeRegistryPath(path);
|
|
304
|
+
const normalizedCurrent = this.normalizeRegistryPath(currentPath);
|
|
305
|
+
// If the import points back to the current file, skip local resolution and fetch
|
|
306
|
+
// from the next layer (remote/global) instead.
|
|
307
|
+
if (this.areEquivalentRegistryPaths(normalized, normalizedCurrent)) {
|
|
308
|
+
return this.fetchRemoteWithFallback(normalizedCurrent, normalized);
|
|
309
|
+
}
|
|
310
|
+
// Otherwise, resolve through the normal local-first path.
|
|
311
|
+
try {
|
|
312
|
+
let targetPath = normalized;
|
|
313
|
+
const hasKnownPrefix = targetPath.startsWith('jobs/') || targetPath.startsWith('workflows/') ||
|
|
314
|
+
targetPath.startsWith('skills/') || targetPath.startsWith('rules/') || targetPath.startsWith('templates/');
|
|
315
|
+
if (!hasKnownPrefix) {
|
|
316
|
+
// Try to guess type from currentPath or just try jobs/workflows
|
|
317
|
+
const type = normalizedCurrent.startsWith('jobs/') ? 'jobs' : (normalizedCurrent.startsWith('workflows/') ? 'workflows' : null);
|
|
318
|
+
if (type) {
|
|
319
|
+
try {
|
|
320
|
+
targetPath = await this.findRegistryPath(type, normalized.replace(/\.md$/, ''));
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
// Ignore and use original normalized
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const res = await this.resolveFile(targetPath, {
|
|
328
|
+
includeMetadata: false,
|
|
329
|
+
stripMcpHeader: true
|
|
330
|
+
});
|
|
331
|
+
return res.content;
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
},
|
|
122
337
|
maxDepth: 5
|
|
123
338
|
});
|
|
124
339
|
return {
|
|
@@ -158,7 +373,6 @@ class LocalRegistryResolver {
|
|
|
158
373
|
* @returns Resolved file with metadata
|
|
159
374
|
*/
|
|
160
375
|
async resolveFile(path, options = {}) {
|
|
161
|
-
console.error(`[LocalRegistryResolver] ===== resolveFile called for: ${path} =====`);
|
|
162
376
|
const includeMetadata = options.includeMetadata ?? true;
|
|
163
377
|
const stripMcpHeader = options.stripMcpHeader ?? false;
|
|
164
378
|
// Check for local override
|
|
@@ -171,9 +385,9 @@ class LocalRegistryResolver {
|
|
|
171
385
|
inherited: false
|
|
172
386
|
};
|
|
173
387
|
}
|
|
174
|
-
// No override, fetch from remote
|
|
388
|
+
// No useful override or synced content, fetch from remote
|
|
175
389
|
try {
|
|
176
|
-
const rawContent = await this.
|
|
390
|
+
const rawContent = await this.fetchRemoteWithFallback(path);
|
|
177
391
|
const content = stripMcpHeader ? this.stripMcpHeader(rawContent) : rawContent;
|
|
178
392
|
return {
|
|
179
393
|
content,
|
|
@@ -189,11 +403,15 @@ class LocalRegistryResolver {
|
|
|
189
403
|
let localContent;
|
|
190
404
|
try {
|
|
191
405
|
localContent = this.readLocalOverride(path);
|
|
406
|
+
if (this.shouldFilter && this.shouldFilter(localContent)) {
|
|
407
|
+
console.warn(`[LocalRegistryResolver] Local override for ${path} filtered (e.g. stub), falling back to remote.`);
|
|
408
|
+
throw new Error('CONTENT_FILTERED');
|
|
409
|
+
}
|
|
192
410
|
}
|
|
193
411
|
catch (error) {
|
|
194
412
|
// If local read fails, fall back to remote
|
|
195
413
|
console.warn(`Local override read failed, falling back to remote: ${path}`);
|
|
196
|
-
const rawContent = await this.
|
|
414
|
+
const rawContent = await this.fetchRemoteWithFallback(path);
|
|
197
415
|
const content = stripMcpHeader ? this.stripMcpHeader(rawContent) : rawContent;
|
|
198
416
|
return {
|
|
199
417
|
content,
|
|
@@ -204,18 +422,11 @@ class LocalRegistryResolver {
|
|
|
204
422
|
// Resolve inheritance
|
|
205
423
|
let resolved;
|
|
206
424
|
try {
|
|
207
|
-
console.error(`[LocalRegistryResolver] Resolving inheritance for ${path}`);
|
|
208
|
-
console.error(`[LocalRegistryResolver] Local content length: ${localContent.length} chars`);
|
|
209
|
-
console.error(`[LocalRegistryResolver] Local content preview: ${localContent.substring(0, 200)}`);
|
|
210
425
|
resolved = await this.resolveInheritance(localContent, path);
|
|
211
|
-
console.error(`[LocalRegistryResolver] Inheritance resolved: ${resolved.imports.length} imports`);
|
|
212
|
-
console.error(`[LocalRegistryResolver] Resolved content length: ${resolved.content.length} chars`);
|
|
213
|
-
console.error(`[LocalRegistryResolver] Resolved content preview: ${resolved.content.substring(0, 200)}`);
|
|
214
426
|
}
|
|
215
427
|
catch (error) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const content = await this.remoteContentResolver(path);
|
|
428
|
+
const rawContent = await this.fetchRemoteWithFallback(path);
|
|
429
|
+
const content = stripMcpHeader ? this.stripMcpHeader(rawContent) : rawContent;
|
|
219
430
|
return {
|
|
220
431
|
content,
|
|
221
432
|
source: 'remote',
|
|
@@ -317,6 +528,99 @@ class LocalRegistryResolver {
|
|
|
317
528
|
cache: new Map()
|
|
318
529
|
});
|
|
319
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* Implement RegistryResolver: Resolve raw file content
|
|
533
|
+
*/
|
|
534
|
+
async getFile(path) {
|
|
535
|
+
try {
|
|
536
|
+
const result = await this.resolveFile(path, {
|
|
537
|
+
includeMetadata: false,
|
|
538
|
+
stripMcpHeader: true
|
|
539
|
+
});
|
|
540
|
+
return result.content;
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Implement RegistryResolver: Parse and resolve a workflow or job
|
|
548
|
+
*/
|
|
549
|
+
async getWorkflow(name, preferredType) {
|
|
550
|
+
const types = preferredType
|
|
551
|
+
? [preferredType === 'job' ? 'jobs' : 'workflows']
|
|
552
|
+
: ['workflows', 'jobs'];
|
|
553
|
+
for (const typeDir of types) {
|
|
554
|
+
try {
|
|
555
|
+
const path = await this.findRegistryPath(typeDir, name);
|
|
556
|
+
const resolved = await this.resolveFile(path, {
|
|
557
|
+
includeMetadata: false,
|
|
558
|
+
stripMcpHeader: true
|
|
559
|
+
});
|
|
560
|
+
if (resolved) {
|
|
561
|
+
return workflow_parser_1.WorkflowParser.parseContent(resolved.content, name, path);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
catch (err) {
|
|
565
|
+
// Continue to next type if not successful
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// Fallback for names that might already include type/category
|
|
570
|
+
try {
|
|
571
|
+
const finalPath = name.endsWith('.md') ? name : `${name}.md`;
|
|
572
|
+
const resolved = await this.resolveFile(finalPath, {
|
|
573
|
+
includeMetadata: false,
|
|
574
|
+
stripMcpHeader: true
|
|
575
|
+
});
|
|
576
|
+
return workflow_parser_1.WorkflowParser.parseContent(resolved.content, name, finalPath);
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Implement RegistryResolver: List available items
|
|
584
|
+
* Note: For proxy, this is a merged view of local and potentially remote.
|
|
585
|
+
* Remote discovery is handled separately in list_fraim_jobs/workflows.
|
|
586
|
+
*/
|
|
587
|
+
async listItems(type) {
|
|
588
|
+
// Current LocalRegistryResolver doesn't maintain a full list of remote items,
|
|
589
|
+
// so it primarily returns local overrides.
|
|
590
|
+
const items = [];
|
|
591
|
+
const dirs = type === 'job' ? ['jobs'] : (type === 'workflow' ? ['workflows'] : ['jobs', 'workflows']);
|
|
592
|
+
for (const dir of dirs) {
|
|
593
|
+
const localDir = (0, path_1.join)(this.workspaceRoot, '.fraim', 'personalized-employee', dir);
|
|
594
|
+
if (fs.existsSync(localDir)) {
|
|
595
|
+
const relPaths = this.collectLocalMarkdownPaths(localDir);
|
|
596
|
+
for (const rel of relPaths) {
|
|
597
|
+
items.push({
|
|
598
|
+
name: rel.replace(/\.md$/, ''),
|
|
599
|
+
path: `${dir}/${rel}`,
|
|
600
|
+
type: dir === 'jobs' ? 'job' : 'workflow'
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return items;
|
|
606
|
+
}
|
|
607
|
+
collectLocalMarkdownPaths(dir, currentRel = '') {
|
|
608
|
+
const results = [];
|
|
609
|
+
if (!fs.existsSync(dir))
|
|
610
|
+
return results;
|
|
611
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
612
|
+
for (const entry of entries) {
|
|
613
|
+
const rel = currentRel ? `${currentRel}/${entry.name}` : entry.name;
|
|
614
|
+
const full = (0, path_1.join)(dir, entry.name);
|
|
615
|
+
if (entry.isDirectory()) {
|
|
616
|
+
results.push(...this.collectLocalMarkdownPaths(full, rel));
|
|
617
|
+
}
|
|
618
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
619
|
+
results.push(rel);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return results;
|
|
623
|
+
}
|
|
320
624
|
}
|
|
321
625
|
exports.LocalRegistryResolver = LocalRegistryResolver;
|
|
322
626
|
LocalRegistryResolver.MAX_INCLUDE_DEPTH = 10;
|