fraim-framework 2.0.86 → 2.0.88
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/services/device-flow-service.js +83 -0
- package/dist/src/cli/setup/provider-prompts.js +39 -0
- package/dist/src/cli/utils/remote-sync.js +159 -28
- 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 +34 -27
- package/dist/src/core/utils/workflow-parser.js +32 -2
- package/dist/src/local-mcp-server/stdio-server.js +240 -284
- package/index.js +26 -5
- package/package.json +15 -5
|
@@ -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;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPortConflictError = createPortConflictError;
|
|
4
|
+
exports.startListening = startListening;
|
|
5
|
+
exports.logStartupError = logStartupError;
|
|
6
|
+
function createPortConflictError(port) {
|
|
7
|
+
const error = new Error(`Port ${port} is already in use. Another process is already listening on http://localhost:${port}. Stop that process or set FRAIM_MCP_PORT/PORT to a different port.`);
|
|
8
|
+
error.code = 'EADDRINUSE';
|
|
9
|
+
return error;
|
|
10
|
+
}
|
|
11
|
+
async function startListening(app, port, onListening) {
|
|
12
|
+
await new Promise((resolve, reject) => {
|
|
13
|
+
const server = app.listen(port);
|
|
14
|
+
server.once('listening', () => {
|
|
15
|
+
onListening();
|
|
16
|
+
resolve();
|
|
17
|
+
});
|
|
18
|
+
server.once('error', (error) => {
|
|
19
|
+
if (error?.code === 'EADDRINUSE') {
|
|
20
|
+
reject(createPortConflictError(port));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
reject(error);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function logStartupError(error) {
|
|
28
|
+
const maybePortError = error;
|
|
29
|
+
if (maybePortError?.code === 'EADDRINUSE') {
|
|
30
|
+
console.error(`[FRAIM PORT] ${maybePortError.message}`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
console.error('❌ Failed to start FRAIM MCP Server:', error);
|
|
34
|
+
}
|
|
@@ -8,6 +8,7 @@ exports.generateRuleStub = generateRuleStub;
|
|
|
8
8
|
exports.parseRegistryJob = parseRegistryJob;
|
|
9
9
|
exports.parseRegistrySkill = parseRegistrySkill;
|
|
10
10
|
exports.parseRegistryRule = parseRegistryRule;
|
|
11
|
+
const STUB_MARKER = '<!-- FRAIM_DISCOVERY_STUB -->';
|
|
11
12
|
function extractSection(content, headingPatterns) {
|
|
12
13
|
for (const heading of headingPatterns) {
|
|
13
14
|
const pattern = new RegExp(`(?:^|\\n)#{2,3}\\s*${heading}\\s*\\n([\\s\\S]*?)(?=\\n#{2,3}\\s|$)`, 'i');
|
|
@@ -49,7 +50,8 @@ function extractLeadParagraph(content) {
|
|
|
49
50
|
* These stubs are committed to the user's repo for discoverability.
|
|
50
51
|
*/
|
|
51
52
|
function generateWorkflowStub(workflowName, workflowPath, intent, principles) {
|
|
52
|
-
return
|
|
53
|
+
return `${STUB_MARKER}
|
|
54
|
+
# FRAIM Workflow: ${workflowName}
|
|
53
55
|
|
|
54
56
|
> [!IMPORTANT]
|
|
55
57
|
> This is a **FRAIM-managed workflow stub**.
|
|
@@ -78,8 +80,9 @@ function parseRegistryWorkflow(content) {
|
|
|
78
80
|
/**
|
|
79
81
|
* Coaching stubs are discoverability artifacts and should be resolved with get_fraim_file.
|
|
80
82
|
*/
|
|
81
|
-
function generateJobStub(jobName, _jobPath, intent, outcome) {
|
|
82
|
-
return
|
|
83
|
+
function generateJobStub(jobName, _jobPath, intent, outcome, steps) {
|
|
84
|
+
return `${STUB_MARKER}
|
|
85
|
+
# FRAIM Job: ${jobName}
|
|
83
86
|
|
|
84
87
|
## Intent
|
|
85
88
|
${intent}
|
|
@@ -87,6 +90,9 @@ ${intent}
|
|
|
87
90
|
## Outcome
|
|
88
91
|
${outcome}
|
|
89
92
|
|
|
93
|
+
## Steps
|
|
94
|
+
${steps}
|
|
95
|
+
|
|
90
96
|
---
|
|
91
97
|
|
|
92
98
|
> [!IMPORTANT]
|
|
@@ -103,19 +109,21 @@ ${outcome}
|
|
|
103
109
|
/**
|
|
104
110
|
* Generates a lightweight markdown stub for a skill.
|
|
105
111
|
*/
|
|
106
|
-
function generateSkillStub(skillName, skillPath,
|
|
107
|
-
return
|
|
112
|
+
function generateSkillStub(skillName, skillPath, skillInput, skillOutput) {
|
|
113
|
+
return `${STUB_MARKER}
|
|
114
|
+
# FRAIM Skill: ${skillName}
|
|
108
115
|
|
|
109
|
-
##
|
|
110
|
-
${
|
|
116
|
+
## Skill Input
|
|
117
|
+
${skillInput}
|
|
111
118
|
|
|
112
|
-
##
|
|
113
|
-
${
|
|
119
|
+
## Skill Output
|
|
120
|
+
${skillOutput}
|
|
114
121
|
|
|
115
122
|
---
|
|
116
123
|
|
|
117
124
|
> [!IMPORTANT]
|
|
118
|
-
> **For AI Agents:** This is a
|
|
125
|
+
> **For AI Agents:** This is a discoverability stub for the skill.
|
|
126
|
+
> All execution details must be fetched from MCP before use.
|
|
119
127
|
> To retrieve the complete skill instructions, call:
|
|
120
128
|
> \`get_fraim_file({ path: "skills/${skillPath}" })\`
|
|
121
129
|
`;
|
|
@@ -123,52 +131,51 @@ ${outcome}
|
|
|
123
131
|
/**
|
|
124
132
|
* Generates a lightweight markdown stub for a rule.
|
|
125
133
|
*/
|
|
126
|
-
function generateRuleStub(ruleName, rulePath, intent
|
|
127
|
-
return
|
|
134
|
+
function generateRuleStub(ruleName, rulePath, intent) {
|
|
135
|
+
return `${STUB_MARKER}
|
|
136
|
+
# FRAIM Rule: ${ruleName}
|
|
128
137
|
|
|
129
138
|
## Intent
|
|
130
139
|
${intent}
|
|
131
140
|
|
|
132
|
-
## Outcome
|
|
133
|
-
${outcome}
|
|
134
|
-
|
|
135
141
|
---
|
|
136
142
|
|
|
137
143
|
> [!IMPORTANT]
|
|
138
|
-
> **For AI Agents:** This is a
|
|
144
|
+
> **For AI Agents:** This is a discoverability stub for the rule.
|
|
145
|
+
> All rule details must be fetched from MCP before use.
|
|
139
146
|
> To retrieve the complete rule instructions, call:
|
|
140
147
|
> \`get_fraim_file({ path: "rules/${rulePath}" })\`
|
|
141
148
|
`;
|
|
142
149
|
}
|
|
143
150
|
/**
|
|
144
|
-
* Parses a job file from the registry to extract its intent and
|
|
151
|
+
* Parses a job file from the registry to extract its intent, outcome, and steps for the stub.
|
|
145
152
|
*/
|
|
146
153
|
function parseRegistryJob(content) {
|
|
147
154
|
const intentMatch = content.match(/##\s*intent\s+([\s\S]*?)(?=\n##|$)/i);
|
|
148
155
|
const outcomeMatch = content.match(/##\s*outcome\s+([\s\S]*?)(?=\n##|$)/i);
|
|
156
|
+
const stepsMatch = content.match(/##\s*steps\s+([\s\S]*?)(?=\n##|$)/i);
|
|
149
157
|
const intent = intentMatch ? intentMatch[1].trim() : 'No intent defined.';
|
|
150
158
|
const outcome = outcomeMatch ? outcomeMatch[1].trim() : 'No outcome defined.';
|
|
151
|
-
|
|
159
|
+
const steps = stepsMatch ? stepsMatch[1].trim() : 'Use get_fraim_job to retrieve the full phase-by-phase execution steps.';
|
|
160
|
+
return { intent, outcome, steps };
|
|
152
161
|
}
|
|
153
162
|
/**
|
|
154
163
|
* Parses a skill file from the registry to extract intent and expected outcome for stubs.
|
|
155
164
|
*/
|
|
156
165
|
function parseRegistrySkill(content) {
|
|
157
|
-
const
|
|
166
|
+
const skillInput = extractSection(content, ['skill input', 'input', 'intent', 'skill intent']) ||
|
|
158
167
|
extractLeadParagraph(content) ||
|
|
159
|
-
'
|
|
160
|
-
const
|
|
161
|
-
'Produce the
|
|
162
|
-
return {
|
|
168
|
+
'Use this skill only when the task matches the skill trigger, context, and required inputs.';
|
|
169
|
+
const skillOutput = extractSection(content, ['skill output', 'output', 'outcome', 'expected behavior']) ||
|
|
170
|
+
'Produce the concrete deliverable or decision this skill is designed to return.';
|
|
171
|
+
return { skillInput, skillOutput };
|
|
163
172
|
}
|
|
164
173
|
/**
|
|
165
|
-
* Parses a rule file from the registry to extract intent
|
|
174
|
+
* Parses a rule file from the registry to extract intent for stubs.
|
|
166
175
|
*/
|
|
167
176
|
function parseRegistryRule(content) {
|
|
168
177
|
const intent = extractSection(content, ['intent']) ||
|
|
169
178
|
extractLeadParagraph(content) ||
|
|
170
179
|
'Follow this rule when executing related FRAIM workflows and jobs.';
|
|
171
|
-
|
|
172
|
-
'Consistently apply this rule throughout execution.';
|
|
173
|
-
return { intent, outcome };
|
|
180
|
+
return { intent };
|
|
174
181
|
}
|