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.
Files changed (70) hide show
  1. package/README.md +445 -0
  2. package/bin/fraim.js +23 -0
  3. package/dist/src/cli/api/get-provider-client.js +41 -0
  4. package/dist/src/cli/api/provider-client.js +107 -0
  5. package/dist/src/cli/commands/add-ide.js +430 -0
  6. package/dist/src/cli/commands/add-provider.js +233 -0
  7. package/dist/src/cli/commands/doctor.js +149 -0
  8. package/dist/src/cli/commands/init-project.js +301 -0
  9. package/dist/src/cli/commands/list-overridable.js +184 -0
  10. package/dist/src/cli/commands/list.js +57 -0
  11. package/dist/src/cli/commands/login.js +84 -0
  12. package/dist/src/cli/commands/mcp.js +15 -0
  13. package/dist/src/cli/commands/migrate-project-fraim.js +42 -0
  14. package/dist/src/cli/commands/override.js +177 -0
  15. package/dist/src/cli/commands/setup.js +651 -0
  16. package/dist/src/cli/commands/sync.js +162 -0
  17. package/dist/src/cli/commands/test-mcp.js +171 -0
  18. package/dist/src/cli/doctor/check-runner.js +199 -0
  19. package/dist/src/cli/doctor/checks/global-setup-checks.js +220 -0
  20. package/dist/src/cli/doctor/checks/ide-config-checks.js +250 -0
  21. package/dist/src/cli/doctor/checks/mcp-connectivity-checks.js +381 -0
  22. package/dist/src/cli/doctor/checks/project-setup-checks.js +282 -0
  23. package/dist/src/cli/doctor/checks/scripts-checks.js +157 -0
  24. package/dist/src/cli/doctor/checks/workflow-checks.js +251 -0
  25. package/dist/src/cli/doctor/reporters/console-reporter.js +96 -0
  26. package/dist/src/cli/doctor/reporters/json-reporter.js +11 -0
  27. package/dist/src/cli/doctor/types.js +6 -0
  28. package/dist/src/cli/fraim.js +100 -0
  29. package/dist/src/cli/internal/device-flow-service.js +83 -0
  30. package/dist/src/cli/mcp/ide-formats.js +243 -0
  31. package/dist/src/cli/mcp/mcp-server-builder.js +48 -0
  32. package/dist/src/cli/mcp/mcp-server-registry.js +160 -0
  33. package/dist/src/cli/mcp/types.js +3 -0
  34. package/dist/src/cli/providers/local-provider-registry.js +166 -0
  35. package/dist/src/cli/providers/provider-registry.js +230 -0
  36. package/dist/src/cli/setup/auto-mcp-setup.js +331 -0
  37. package/dist/src/cli/setup/codex-local-config.js +37 -0
  38. package/dist/src/cli/setup/first-run.js +242 -0
  39. package/dist/src/cli/setup/ide-detector.js +179 -0
  40. package/dist/src/cli/setup/mcp-config-generator.js +192 -0
  41. package/dist/src/cli/setup/provider-prompts.js +339 -0
  42. package/dist/src/cli/utils/agent-adapters.js +126 -0
  43. package/dist/src/cli/utils/digest-utils.js +47 -0
  44. package/dist/src/cli/utils/fraim-gitignore.js +40 -0
  45. package/dist/src/cli/utils/platform-detection.js +258 -0
  46. package/dist/src/cli/utils/project-bootstrap.js +93 -0
  47. package/dist/src/cli/utils/remote-sync.js +315 -0
  48. package/dist/src/cli/utils/script-sync-utils.js +221 -0
  49. package/dist/src/cli/utils/version-utils.js +32 -0
  50. package/dist/src/core/ai-mentor.js +230 -0
  51. package/dist/src/core/config-loader.js +114 -0
  52. package/dist/src/core/config-writer.js +75 -0
  53. package/dist/src/core/types.js +23 -0
  54. package/dist/src/core/utils/git-utils.js +95 -0
  55. package/dist/src/core/utils/include-resolver.js +92 -0
  56. package/dist/src/core/utils/inheritance-parser.js +288 -0
  57. package/dist/src/core/utils/job-parser.js +176 -0
  58. package/dist/src/core/utils/local-registry-resolver.js +616 -0
  59. package/dist/src/core/utils/object-utils.js +11 -0
  60. package/dist/src/core/utils/project-fraim-migration.js +103 -0
  61. package/dist/src/core/utils/project-fraim-paths.js +38 -0
  62. package/dist/src/core/utils/provider-utils.js +18 -0
  63. package/dist/src/core/utils/server-startup.js +34 -0
  64. package/dist/src/core/utils/stub-generator.js +147 -0
  65. package/dist/src/core/utils/workflow-parser.js +174 -0
  66. package/dist/src/local-mcp-server/learning-context-builder.js +229 -0
  67. package/dist/src/local-mcp-server/stdio-server.js +1698 -0
  68. package/dist/src/local-mcp-server/usage-collector.js +264 -0
  69. package/index.js +85 -0
  70. 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
+ }