fraim 2.0.100
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 +445 -0
- package/bin/fraim.js +23 -0
- package/dist/src/cli/api/get-provider-client.js +41 -0
- package/dist/src/cli/api/provider-client.js +107 -0
- package/dist/src/cli/commands/add-ide.js +430 -0
- package/dist/src/cli/commands/add-provider.js +233 -0
- package/dist/src/cli/commands/doctor.js +149 -0
- package/dist/src/cli/commands/init-project.js +301 -0
- package/dist/src/cli/commands/list-overridable.js +184 -0
- package/dist/src/cli/commands/list.js +57 -0
- package/dist/src/cli/commands/login.js +84 -0
- package/dist/src/cli/commands/mcp.js +15 -0
- package/dist/src/cli/commands/migrate-project-fraim.js +42 -0
- package/dist/src/cli/commands/override.js +177 -0
- package/dist/src/cli/commands/setup.js +651 -0
- package/dist/src/cli/commands/sync.js +162 -0
- package/dist/src/cli/commands/test-mcp.js +171 -0
- package/dist/src/cli/doctor/check-runner.js +199 -0
- package/dist/src/cli/doctor/checks/global-setup-checks.js +220 -0
- package/dist/src/cli/doctor/checks/ide-config-checks.js +250 -0
- package/dist/src/cli/doctor/checks/mcp-connectivity-checks.js +381 -0
- package/dist/src/cli/doctor/checks/project-setup-checks.js +282 -0
- package/dist/src/cli/doctor/checks/scripts-checks.js +157 -0
- package/dist/src/cli/doctor/checks/workflow-checks.js +251 -0
- package/dist/src/cli/doctor/reporters/console-reporter.js +96 -0
- package/dist/src/cli/doctor/reporters/json-reporter.js +11 -0
- package/dist/src/cli/doctor/types.js +6 -0
- package/dist/src/cli/fraim.js +100 -0
- package/dist/src/cli/internal/device-flow-service.js +83 -0
- package/dist/src/cli/mcp/ide-formats.js +243 -0
- package/dist/src/cli/mcp/mcp-server-builder.js +48 -0
- package/dist/src/cli/mcp/mcp-server-registry.js +160 -0
- package/dist/src/cli/mcp/types.js +3 -0
- package/dist/src/cli/providers/local-provider-registry.js +166 -0
- package/dist/src/cli/providers/provider-registry.js +230 -0
- package/dist/src/cli/setup/auto-mcp-setup.js +331 -0
- package/dist/src/cli/setup/codex-local-config.js +37 -0
- package/dist/src/cli/setup/first-run.js +242 -0
- package/dist/src/cli/setup/ide-detector.js +179 -0
- package/dist/src/cli/setup/mcp-config-generator.js +192 -0
- package/dist/src/cli/setup/provider-prompts.js +339 -0
- package/dist/src/cli/utils/agent-adapters.js +126 -0
- package/dist/src/cli/utils/digest-utils.js +47 -0
- package/dist/src/cli/utils/fraim-gitignore.js +40 -0
- package/dist/src/cli/utils/platform-detection.js +258 -0
- package/dist/src/cli/utils/project-bootstrap.js +93 -0
- package/dist/src/cli/utils/remote-sync.js +315 -0
- package/dist/src/cli/utils/script-sync-utils.js +221 -0
- package/dist/src/cli/utils/version-utils.js +32 -0
- package/dist/src/core/ai-mentor.js +230 -0
- package/dist/src/core/config-loader.js +114 -0
- package/dist/src/core/config-writer.js +75 -0
- package/dist/src/core/types.js +23 -0
- package/dist/src/core/utils/git-utils.js +95 -0
- package/dist/src/core/utils/include-resolver.js +92 -0
- package/dist/src/core/utils/inheritance-parser.js +288 -0
- package/dist/src/core/utils/job-parser.js +176 -0
- package/dist/src/core/utils/local-registry-resolver.js +616 -0
- package/dist/src/core/utils/object-utils.js +11 -0
- package/dist/src/core/utils/project-fraim-migration.js +103 -0
- package/dist/src/core/utils/project-fraim-paths.js +38 -0
- package/dist/src/core/utils/provider-utils.js +18 -0
- package/dist/src/core/utils/server-startup.js +34 -0
- package/dist/src/core/utils/stub-generator.js +147 -0
- package/dist/src/core/utils/workflow-parser.js +174 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +229 -0
- package/dist/src/local-mcp-server/stdio-server.js +1698 -0
- package/dist/src/local-mcp-server/usage-collector.js +264 -0
- package/index.js +85 -0
- package/package.json +139 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* LocalRegistryResolver
|
|
4
|
+
*
|
|
5
|
+
* Resolves registry file requests by checking for local overrides first,
|
|
6
|
+
* then falling back to remote registry. Handles inheritance via InheritanceParser.
|
|
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
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.LocalRegistryResolver = void 0;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const fs_1 = require("fs");
|
|
45
|
+
const path_1 = require("path");
|
|
46
|
+
const inheritance_parser_1 = require("./inheritance-parser");
|
|
47
|
+
const job_parser_1 = require("./job-parser");
|
|
48
|
+
const project_fraim_paths_1 = require("./project-fraim-paths");
|
|
49
|
+
class LocalRegistryResolver {
|
|
50
|
+
constructor(options) {
|
|
51
|
+
this.workspaceRoot = options.workspaceRoot;
|
|
52
|
+
this.remoteContentResolver = options.remoteContentResolver;
|
|
53
|
+
this.parser = new inheritance_parser_1.InheritanceParser(options.maxDepth);
|
|
54
|
+
this.shouldFilter = options.shouldFilter;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Check if a local override exists for the given path
|
|
58
|
+
*/
|
|
59
|
+
hasLocalOverride(path) {
|
|
60
|
+
return (0, fs_1.existsSync)(this.getOverridePath(path));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Find the actual relative path for a registry item by searching subdirectories.
|
|
64
|
+
* Returns '{type}/{category}/{name}.md' or '{type}/{name}.md'.
|
|
65
|
+
*/
|
|
66
|
+
async findRegistryPath(type, name) {
|
|
67
|
+
const baseName = `${name}.md`;
|
|
68
|
+
const searchDirs = [type];
|
|
69
|
+
for (const dir of searchDirs) {
|
|
70
|
+
// Check literal path first
|
|
71
|
+
const literal = `${dir}/${baseName}`;
|
|
72
|
+
if (this.hasLocalOverride(literal))
|
|
73
|
+
return literal;
|
|
74
|
+
// Deep search
|
|
75
|
+
const fullBaseDir = (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, 'personalized-employee', dir);
|
|
76
|
+
const found = this.searchFileRecursively(fullBaseDir, baseName);
|
|
77
|
+
if (found) {
|
|
78
|
+
// Convert absolute back to relative
|
|
79
|
+
const rel = found.replace(/\\/g, '/');
|
|
80
|
+
const dirMarker = `/${dir}/`;
|
|
81
|
+
if (rel.includes(dirMarker)) {
|
|
82
|
+
return rel.substring(rel.indexOf(dirMarker) + 1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return `${type}/${name}.md`;
|
|
87
|
+
}
|
|
88
|
+
searchFileRecursively(dir, fileName) {
|
|
89
|
+
if (!(0, fs_1.existsSync)(dir))
|
|
90
|
+
return null;
|
|
91
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
const fullPath = (0, path_1.join)(dir, entry.name);
|
|
94
|
+
if (entry.isDirectory()) {
|
|
95
|
+
const found = this.searchFileRecursively(fullPath, fileName);
|
|
96
|
+
if (found)
|
|
97
|
+
return found;
|
|
98
|
+
}
|
|
99
|
+
else if (entry.name === fileName) {
|
|
100
|
+
return fullPath;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Check if a locally synced skill/rule file exists for the given registry path.
|
|
107
|
+
*/
|
|
108
|
+
hasSyncedLocalFile(path) {
|
|
109
|
+
const syncedPath = this.getSyncedFilePath(path);
|
|
110
|
+
return !!syncedPath && (0, fs_1.existsSync)(syncedPath);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Get the full path to a local override file
|
|
114
|
+
*/
|
|
115
|
+
getOverridePath(path) {
|
|
116
|
+
const normalized = path.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
117
|
+
// Personal overrides are in fraim/personalized-employee/
|
|
118
|
+
// We don't need a redundant 'registry/' subfolder here as the path already includes type (e.g. jobs/)
|
|
119
|
+
return (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, 'personalized-employee', normalized);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get the full path to a locally synced FRAIM file when available.
|
|
123
|
+
* Skills and rules are synced under role-based folders:
|
|
124
|
+
* - skills/* -> fraim/ai-employee/skills/*
|
|
125
|
+
* - rules/* -> fraim/ai-employee/rules/*
|
|
126
|
+
*/
|
|
127
|
+
getSyncedFilePath(path) {
|
|
128
|
+
const normalizedPath = path.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
129
|
+
// 1. Workflows: workflows/[role]/path -> fraim/[role]/workflows/path
|
|
130
|
+
if (normalizedPath.startsWith('workflows/')) {
|
|
131
|
+
const parts = normalizedPath.split('/');
|
|
132
|
+
if (parts.length >= 3 && (parts[1] === 'ai-employee' || parts[1] === 'ai-manager')) {
|
|
133
|
+
const role = parts[1];
|
|
134
|
+
const subPath = parts.slice(2).join('/');
|
|
135
|
+
return (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, role, 'workflows', subPath);
|
|
136
|
+
}
|
|
137
|
+
// Fallback: Try ai-employee and ai-manager if no role prefix
|
|
138
|
+
const subPath = normalizedPath.substring('workflows/'.length);
|
|
139
|
+
const employeePath = (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, 'ai-employee', 'workflows', subPath);
|
|
140
|
+
if (fs.existsSync(employeePath))
|
|
141
|
+
return employeePath;
|
|
142
|
+
const managerPath = (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, 'ai-manager', 'workflows', subPath);
|
|
143
|
+
if (fs.existsSync(managerPath))
|
|
144
|
+
return managerPath;
|
|
145
|
+
// Fallback for non-role-prefixed direct workspace paths
|
|
146
|
+
return (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, normalizedPath);
|
|
147
|
+
}
|
|
148
|
+
// 2. Jobs: jobs/[role]/path -> fraim/[role]/jobs/path
|
|
149
|
+
if (normalizedPath.startsWith('jobs/')) {
|
|
150
|
+
const parts = normalizedPath.split('/');
|
|
151
|
+
if (parts.length >= 3 && (parts[1] === 'ai-employee' || parts[1] === 'ai-manager')) {
|
|
152
|
+
const role = parts[1];
|
|
153
|
+
const subPath = parts.slice(2).join('/');
|
|
154
|
+
return (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, role, 'jobs', subPath);
|
|
155
|
+
}
|
|
156
|
+
// Fallback: Try ai-employee and ai-manager if no role prefix
|
|
157
|
+
const subPath = normalizedPath.substring('jobs/'.length);
|
|
158
|
+
const employeePath = (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, 'ai-employee', 'jobs', subPath);
|
|
159
|
+
if (fs.existsSync(employeePath))
|
|
160
|
+
return employeePath;
|
|
161
|
+
const managerPath = (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, 'ai-manager', 'jobs', subPath);
|
|
162
|
+
if (fs.existsSync(managerPath))
|
|
163
|
+
return managerPath;
|
|
164
|
+
// Fallback
|
|
165
|
+
return (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, normalizedPath);
|
|
166
|
+
}
|
|
167
|
+
// 3. Rules: [role]/rules/path -> fraim/[role]/rules/path
|
|
168
|
+
if (normalizedPath.includes('/rules/')) {
|
|
169
|
+
const role = normalizedPath.includes('/ai-manager/') ? 'ai-manager' : 'ai-employee';
|
|
170
|
+
// Extract the part after "rules/"
|
|
171
|
+
const rulesIdx = normalizedPath.indexOf('rules/');
|
|
172
|
+
const subPath = normalizedPath.substring(rulesIdx + 'rules/'.length);
|
|
173
|
+
return (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, role, 'rules', subPath);
|
|
174
|
+
}
|
|
175
|
+
// 4. Skills: skills/path -> fraim/ai-employee/skills/path (default to ai-employee)
|
|
176
|
+
if (normalizedPath.startsWith('skills/')) {
|
|
177
|
+
const subPath = normalizedPath.substring('skills/'.length);
|
|
178
|
+
return (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, 'ai-employee', 'skills', subPath);
|
|
179
|
+
}
|
|
180
|
+
// 5. Rules: rules/path -> fraim/ai-employee/rules/path (default to ai-employee)
|
|
181
|
+
if (normalizedPath.startsWith('rules/')) {
|
|
182
|
+
return (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, 'ai-employee', normalizedPath);
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Read local override file content
|
|
188
|
+
*/
|
|
189
|
+
readLocalOverride(path) {
|
|
190
|
+
try {
|
|
191
|
+
return (0, fs_1.readFileSync)(this.getOverridePath(path), 'utf-8');
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
throw new Error(`Failed to read local override: ${path}. ${error.message}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Read locally synced skill/rule file content.
|
|
199
|
+
*/
|
|
200
|
+
readSyncedLocalFile(path) {
|
|
201
|
+
const syncedPath = this.getSyncedFilePath(path);
|
|
202
|
+
if (!syncedPath || !(0, fs_1.existsSync)(syncedPath)) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const content = (0, fs_1.readFileSync)(syncedPath, 'utf-8');
|
|
207
|
+
if (this.shouldFilter && this.shouldFilter(content)) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
return content;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
normalizeRegistryPath(path) {
|
|
217
|
+
const normalized = path.trim().replace(/\\/g, '/').replace(/^\/+/, '');
|
|
218
|
+
return normalized.endsWith('.md') ? normalized : `${normalized}.md`;
|
|
219
|
+
}
|
|
220
|
+
stripTypePrefix(path) {
|
|
221
|
+
return path.replace(/^(jobs|workflows|skills|rules|templates)\//, '');
|
|
222
|
+
}
|
|
223
|
+
areEquivalentRegistryPaths(left, right) {
|
|
224
|
+
const normalizedLeft = this.normalizeRegistryPath(left);
|
|
225
|
+
const normalizedRight = this.normalizeRegistryPath(right);
|
|
226
|
+
const strippedLeft = this.stripTypePrefix(normalizedLeft);
|
|
227
|
+
const strippedRight = this.stripTypePrefix(normalizedRight);
|
|
228
|
+
return normalizedLeft === normalizedRight ||
|
|
229
|
+
strippedLeft === strippedRight ||
|
|
230
|
+
normalizedLeft.endsWith(`/${strippedRight}`) ||
|
|
231
|
+
normalizedRight.endsWith(`/${strippedLeft}`) ||
|
|
232
|
+
strippedLeft.endsWith(`/${strippedRight}`) ||
|
|
233
|
+
strippedRight.endsWith(`/${strippedLeft}`);
|
|
234
|
+
}
|
|
235
|
+
async fetchRemoteWithFallback(...paths) {
|
|
236
|
+
const candidates = new Set();
|
|
237
|
+
for (const path of paths) {
|
|
238
|
+
if (!path)
|
|
239
|
+
continue;
|
|
240
|
+
const normalized = this.normalizeRegistryPath(path);
|
|
241
|
+
candidates.add(normalized);
|
|
242
|
+
candidates.add(this.stripTypePrefix(normalized));
|
|
243
|
+
}
|
|
244
|
+
let lastError = null;
|
|
245
|
+
for (const candidate of candidates) {
|
|
246
|
+
try {
|
|
247
|
+
return await this.remoteContentResolver(candidate);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
lastError = error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
throw lastError || new Error(`Failed to fetch remote content for ${paths.join(', ')}`);
|
|
254
|
+
}
|
|
255
|
+
stripMcpHeader(content) {
|
|
256
|
+
const trimmed = content.trimStart();
|
|
257
|
+
if (!trimmed.startsWith('#')) {
|
|
258
|
+
return content;
|
|
259
|
+
}
|
|
260
|
+
const separator = '\n---\n';
|
|
261
|
+
const separatorIndex = content.indexOf(separator);
|
|
262
|
+
if (separatorIndex === -1) {
|
|
263
|
+
return content;
|
|
264
|
+
}
|
|
265
|
+
const headerBlock = content.slice(0, separatorIndex);
|
|
266
|
+
if (!headerBlock.includes('** Path:**')) {
|
|
267
|
+
return content;
|
|
268
|
+
}
|
|
269
|
+
return content.slice(separatorIndex + separator.length).trimStart();
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Resolve inheritance in local override content
|
|
273
|
+
*/
|
|
274
|
+
async resolveInheritance(content, currentPath) {
|
|
275
|
+
// Check if content has imports
|
|
276
|
+
const parseResult = this.parser.parse(content);
|
|
277
|
+
if (!parseResult.hasImports) {
|
|
278
|
+
return { content, imports: [] };
|
|
279
|
+
}
|
|
280
|
+
// Resolve inheritance
|
|
281
|
+
try {
|
|
282
|
+
const resolved = await this.parser.resolve(content, currentPath, {
|
|
283
|
+
fetchParent: async (path) => {
|
|
284
|
+
const normalized = this.normalizeRegistryPath(path);
|
|
285
|
+
const normalizedCurrent = this.normalizeRegistryPath(currentPath);
|
|
286
|
+
// If the import points back to the current file, skip local resolution and fetch
|
|
287
|
+
// from the next layer (remote/global) instead.
|
|
288
|
+
if (this.areEquivalentRegistryPaths(normalized, normalizedCurrent)) {
|
|
289
|
+
return this.fetchRemoteWithFallback(normalizedCurrent, normalized);
|
|
290
|
+
}
|
|
291
|
+
// Otherwise, resolve through the normal local-first path.
|
|
292
|
+
try {
|
|
293
|
+
let targetPath = normalized;
|
|
294
|
+
const hasKnownPrefix = targetPath.startsWith('jobs/') || targetPath.startsWith('workflows/') ||
|
|
295
|
+
targetPath.startsWith('skills/') || targetPath.startsWith('rules/') || targetPath.startsWith('templates/');
|
|
296
|
+
if (!hasKnownPrefix) {
|
|
297
|
+
// Try to guess type from currentPath or just try jobs/workflows
|
|
298
|
+
const type = normalizedCurrent.startsWith('jobs/') ? 'jobs' : (normalizedCurrent.startsWith('workflows/') ? 'workflows' : null);
|
|
299
|
+
if (type) {
|
|
300
|
+
try {
|
|
301
|
+
targetPath = await this.findRegistryPath(type, normalized.replace(/\.md$/, ''));
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// Ignore and use original normalized
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const res = await this.resolveFile(targetPath, {
|
|
309
|
+
includeMetadata: false,
|
|
310
|
+
stripMcpHeader: true
|
|
311
|
+
});
|
|
312
|
+
return res.content;
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
maxDepth: 5
|
|
319
|
+
});
|
|
320
|
+
return {
|
|
321
|
+
content: resolved,
|
|
322
|
+
imports: parseResult.imports
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
if (error instanceof inheritance_parser_1.InheritanceError) {
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
throw new Error(`Failed to resolve inheritance for ${currentPath}: ${error.message}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Generate metadata comment for resolved content
|
|
334
|
+
*/
|
|
335
|
+
generateMetadata(result) {
|
|
336
|
+
if (result.source === 'remote') {
|
|
337
|
+
return '';
|
|
338
|
+
}
|
|
339
|
+
if (result.inherited && result.imports && result.imports.length > 0) {
|
|
340
|
+
return `<!-- 📝 Using local override (inherited from: ${result.imports.join(', ')}) -->\n\n`;
|
|
341
|
+
}
|
|
342
|
+
return `<!-- 📝 Using local override -->\n\n`;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Resolve a registry file request
|
|
346
|
+
*
|
|
347
|
+
* Resolution order:
|
|
348
|
+
* 1. Check for local override in fraim/personalized-employee/
|
|
349
|
+
* 2. If found, read and resolve inheritance
|
|
350
|
+
* 3. If not found, fetch from remote
|
|
351
|
+
*
|
|
352
|
+
* @param path - Registry path (e.g., "workflows/product-building/spec.md")
|
|
353
|
+
* @returns Resolved file with metadata
|
|
354
|
+
*/
|
|
355
|
+
async resolveFile(path, options = {}) {
|
|
356
|
+
const includeMetadata = options.includeMetadata ?? true;
|
|
357
|
+
const stripMcpHeader = options.stripMcpHeader ?? false;
|
|
358
|
+
// Check for local override
|
|
359
|
+
if (!this.hasLocalOverride(path)) {
|
|
360
|
+
const syncedLocalContent = this.readSyncedLocalFile(path);
|
|
361
|
+
if (syncedLocalContent !== null) {
|
|
362
|
+
return {
|
|
363
|
+
content: syncedLocalContent,
|
|
364
|
+
source: 'local',
|
|
365
|
+
inherited: false
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
// No useful override or synced content, fetch from remote
|
|
369
|
+
try {
|
|
370
|
+
const rawContent = await this.fetchRemoteWithFallback(path);
|
|
371
|
+
const content = stripMcpHeader ? this.stripMcpHeader(rawContent) : rawContent;
|
|
372
|
+
return {
|
|
373
|
+
content,
|
|
374
|
+
source: 'remote',
|
|
375
|
+
inherited: false
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
throw new Error(`Failed to fetch from remote: ${path}. ${error.message}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// Read local override
|
|
383
|
+
let localContent;
|
|
384
|
+
try {
|
|
385
|
+
localContent = this.readLocalOverride(path);
|
|
386
|
+
if (this.shouldFilter && this.shouldFilter(localContent)) {
|
|
387
|
+
console.warn(`[LocalRegistryResolver] Local override for ${path} filtered (e.g. stub), falling back to remote.`);
|
|
388
|
+
throw new Error('CONTENT_FILTERED');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
// If local read fails, fall back to remote
|
|
393
|
+
console.warn(`Local override read failed, falling back to remote: ${path}`);
|
|
394
|
+
const rawContent = await this.fetchRemoteWithFallback(path);
|
|
395
|
+
const content = stripMcpHeader ? this.stripMcpHeader(rawContent) : rawContent;
|
|
396
|
+
return {
|
|
397
|
+
content,
|
|
398
|
+
source: 'remote',
|
|
399
|
+
inherited: false
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
// Resolve inheritance
|
|
403
|
+
let resolved;
|
|
404
|
+
try {
|
|
405
|
+
resolved = await this.resolveInheritance(localContent, path);
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
const rawContent = await this.fetchRemoteWithFallback(path);
|
|
409
|
+
const content = stripMcpHeader ? this.stripMcpHeader(rawContent) : rawContent;
|
|
410
|
+
return {
|
|
411
|
+
content,
|
|
412
|
+
source: 'remote',
|
|
413
|
+
inherited: false
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
// Build result
|
|
417
|
+
const result = {
|
|
418
|
+
content: resolved.content,
|
|
419
|
+
source: 'local',
|
|
420
|
+
inherited: resolved.imports.length > 0,
|
|
421
|
+
imports: resolved.imports.length > 0 ? resolved.imports : undefined
|
|
422
|
+
};
|
|
423
|
+
// Add metadata comment
|
|
424
|
+
if (includeMetadata) {
|
|
425
|
+
const metadata = this.generateMetadata(result);
|
|
426
|
+
if (metadata) {
|
|
427
|
+
result.metadata = metadata;
|
|
428
|
+
result.content = metadata + result.content;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return result;
|
|
432
|
+
}
|
|
433
|
+
sanitizeIncludePath(path) {
|
|
434
|
+
const trimmed = path.trim().replace(/\\/g, '/');
|
|
435
|
+
if (!trimmed)
|
|
436
|
+
return null;
|
|
437
|
+
if (trimmed.includes('..'))
|
|
438
|
+
return null;
|
|
439
|
+
if (trimmed.startsWith('/'))
|
|
440
|
+
return null;
|
|
441
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed))
|
|
442
|
+
return null;
|
|
443
|
+
return trimmed;
|
|
444
|
+
}
|
|
445
|
+
async resolveIncludesInternal(content, options) {
|
|
446
|
+
if (options.depth >= options.maxDepth) {
|
|
447
|
+
return content;
|
|
448
|
+
}
|
|
449
|
+
const includePattern = /\{\{include:([^}]+)\}\}/g;
|
|
450
|
+
const matches = Array.from(content.matchAll(includePattern));
|
|
451
|
+
if (matches.length === 0) {
|
|
452
|
+
return content;
|
|
453
|
+
}
|
|
454
|
+
const resolvedByPath = new Map();
|
|
455
|
+
for (const match of matches) {
|
|
456
|
+
const includePath = this.sanitizeIncludePath(match[1]);
|
|
457
|
+
if (!includePath)
|
|
458
|
+
continue;
|
|
459
|
+
if (resolvedByPath.has(includePath))
|
|
460
|
+
continue;
|
|
461
|
+
if (options.cache.has(includePath)) {
|
|
462
|
+
resolvedByPath.set(includePath, options.cache.get(includePath));
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (options.stack.has(includePath)) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
try {
|
|
469
|
+
options.stack.add(includePath);
|
|
470
|
+
const resolved = await this.resolveFile(includePath, {
|
|
471
|
+
includeMetadata: false,
|
|
472
|
+
stripMcpHeader: true
|
|
473
|
+
});
|
|
474
|
+
const resolvedContent = await this.resolveIncludesInternal(resolved.content, {
|
|
475
|
+
depth: options.depth + 1,
|
|
476
|
+
maxDepth: options.maxDepth,
|
|
477
|
+
stack: options.stack,
|
|
478
|
+
cache: options.cache
|
|
479
|
+
});
|
|
480
|
+
options.cache.set(includePath, resolvedContent);
|
|
481
|
+
resolvedByPath.set(includePath, resolvedContent);
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
console.warn(`Failed to resolve include ${includePath}: ${error.message}`);
|
|
485
|
+
}
|
|
486
|
+
finally {
|
|
487
|
+
options.stack.delete(includePath);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return content.replace(includePattern, (fullMatch, includeRef) => {
|
|
491
|
+
const includePath = this.sanitizeIncludePath(includeRef);
|
|
492
|
+
if (!includePath)
|
|
493
|
+
return fullMatch;
|
|
494
|
+
return resolvedByPath.get(includePath) ?? fullMatch;
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Resolve {{include:path}} directives using local override precedence:
|
|
499
|
+
* 1. fraim/personalized-employee/
|
|
500
|
+
* 2. remote resolver
|
|
501
|
+
*/
|
|
502
|
+
async resolveIncludes(content, maxDepth = LocalRegistryResolver.MAX_INCLUDE_DEPTH) {
|
|
503
|
+
return this.resolveIncludesInternal(content, {
|
|
504
|
+
depth: 0,
|
|
505
|
+
maxDepth,
|
|
506
|
+
stack: new Set(),
|
|
507
|
+
cache: new Map()
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Implement RegistryResolver: Resolve raw file content
|
|
512
|
+
*/
|
|
513
|
+
async getFile(path) {
|
|
514
|
+
try {
|
|
515
|
+
const result = await this.resolveFile(path, {
|
|
516
|
+
includeMetadata: false,
|
|
517
|
+
stripMcpHeader: true
|
|
518
|
+
});
|
|
519
|
+
return result.content;
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Implement RegistryResolver: Parse and resolve a job
|
|
527
|
+
*/
|
|
528
|
+
async getJob(name) {
|
|
529
|
+
// Priority 1: personalized-employee/jobs/ (user override)
|
|
530
|
+
try {
|
|
531
|
+
const jobPath = await this.findRegistryPath('jobs', name);
|
|
532
|
+
if (this.hasLocalOverride(jobPath)) {
|
|
533
|
+
const resolved = await this.resolveFile(jobPath, {
|
|
534
|
+
includeMetadata: false,
|
|
535
|
+
stripMcpHeader: true
|
|
536
|
+
});
|
|
537
|
+
if (resolved) {
|
|
538
|
+
return job_parser_1.JobParser.parseContent(resolved.content, name, jobPath);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
// not in personalized-employee
|
|
544
|
+
}
|
|
545
|
+
// Priority 2: Remote (via resolveFile)
|
|
546
|
+
try {
|
|
547
|
+
const path = await this.findRegistryPath('jobs', name);
|
|
548
|
+
const resolved = await this.resolveFile(path, {
|
|
549
|
+
includeMetadata: false,
|
|
550
|
+
stripMcpHeader: true
|
|
551
|
+
});
|
|
552
|
+
if (resolved) {
|
|
553
|
+
return job_parser_1.JobParser.parseContent(resolved.content, name, path);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
catch (err) {
|
|
557
|
+
// Not found in jobs
|
|
558
|
+
}
|
|
559
|
+
// Fallback for names that might already include type/category
|
|
560
|
+
try {
|
|
561
|
+
const finalPath = name.endsWith('.md') ? name : `${name}.md`;
|
|
562
|
+
const resolved = await this.resolveFile(finalPath, {
|
|
563
|
+
includeMetadata: false,
|
|
564
|
+
stripMcpHeader: true
|
|
565
|
+
});
|
|
566
|
+
return job_parser_1.JobParser.parseContent(resolved.content, name, finalPath);
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Implement RegistryResolver: List available items
|
|
574
|
+
* Note: For proxy, this is a merged view of local and potentially remote.
|
|
575
|
+
* Remote discovery is handled separately in list_fraim_jobs.
|
|
576
|
+
*/
|
|
577
|
+
async listItems(type) {
|
|
578
|
+
// Current LocalRegistryResolver doesn't maintain a full list of remote items,
|
|
579
|
+
// so it primarily returns local overrides.
|
|
580
|
+
const items = [];
|
|
581
|
+
const dirs = ['jobs'];
|
|
582
|
+
for (const dir of dirs) {
|
|
583
|
+
const localDir = (0, project_fraim_paths_1.getWorkspaceFraimPath)(this.workspaceRoot, 'personalized-employee', dir);
|
|
584
|
+
if (fs.existsSync(localDir)) {
|
|
585
|
+
const relPaths = this.collectLocalMarkdownPaths(localDir);
|
|
586
|
+
for (const rel of relPaths) {
|
|
587
|
+
items.push({
|
|
588
|
+
name: rel.replace(/\.md$/, ''),
|
|
589
|
+
path: `${dir}/${rel}`,
|
|
590
|
+
type: 'job'
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return items;
|
|
596
|
+
}
|
|
597
|
+
collectLocalMarkdownPaths(dir, currentRel = '') {
|
|
598
|
+
const results = [];
|
|
599
|
+
if (!fs.existsSync(dir))
|
|
600
|
+
return results;
|
|
601
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
602
|
+
for (const entry of entries) {
|
|
603
|
+
const rel = currentRel ? `${currentRel}/${entry.name}` : entry.name;
|
|
604
|
+
const full = (0, path_1.join)(dir, entry.name);
|
|
605
|
+
if (entry.isDirectory()) {
|
|
606
|
+
results.push(...this.collectLocalMarkdownPaths(full, rel));
|
|
607
|
+
}
|
|
608
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
609
|
+
results.push(rel);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return results;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
exports.LocalRegistryResolver = LocalRegistryResolver;
|
|
616
|
+
LocalRegistryResolver.MAX_INCLUDE_DEPTH = 10;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getNestedValue = getNestedValue;
|
|
4
|
+
/**
|
|
5
|
+
* Safely get a nested value from an object using a dot-path
|
|
6
|
+
*/
|
|
7
|
+
function getNestedValue(obj, path) {
|
|
8
|
+
if (!obj)
|
|
9
|
+
return undefined;
|
|
10
|
+
return path.split('.').reduce((current, key) => current && current[key] !== undefined ? current[key] : undefined, obj);
|
|
11
|
+
}
|