fraim 2.0.177 → 2.0.180
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/dist/src/ai-hub/desktop-main.js +2 -2
- package/dist/src/ai-hub/server.js +50 -1
- package/dist/src/api/admin/payments.js +33 -0
- package/dist/src/api/admin/sales-leads.js +21 -0
- package/dist/src/api/payment/create-session.js +338 -0
- package/dist/src/api/payment/dashboard-link.js +149 -0
- package/dist/src/api/payment/session-details.js +31 -0
- package/dist/src/api/payment/webhook.js +587 -0
- package/dist/src/api/personas/me.js +29 -0
- package/dist/src/api/pricing/get-config.js +25 -0
- package/dist/src/api/sales/contact.js +44 -0
- package/dist/src/cli/commands/add-provider.js +74 -61
- package/dist/src/cli/commands/add-surface.js +128 -0
- package/dist/src/cli/commands/login.js +5 -69
- package/dist/src/cli/commands/setup.js +27 -347
- package/dist/src/cli/distribution/marketplace-bundles.js +580 -0
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/mcp/ide-formats.js +5 -3
- package/dist/src/cli/mcp/mcp-server-registry.js +10 -3
- package/dist/src/cli/providers/local-provider-registry.js +2 -3
- package/dist/src/cli/setup/auto-mcp-setup.js +9 -32
- package/dist/src/cli/setup/ide-detector.js +34 -14
- package/dist/src/config/persona-capability-bundles.js +17 -13
- package/dist/src/db/payment-repository.js +61 -0
- package/dist/src/first-run/session-service.js +2 -2
- package/dist/src/fraim/config-loader.js +11 -0
- package/dist/src/fraim/db-service.js +2387 -0
- package/dist/src/fraim/issues.js +152 -0
- package/dist/src/fraim/template-processor.js +184 -0
- package/dist/src/fraim/utils/request-utils.js +23 -0
- package/dist/src/local-mcp-server/stdio-server.js +28 -4
- package/dist/src/local-mcp-server/usage-collector.js +24 -0
- package/dist/src/middleware/auth.js +266 -0
- package/dist/src/middleware/cors-config.js +111 -0
- package/dist/src/middleware/logger.js +116 -0
- package/dist/src/middleware/rate-limit.js +110 -0
- package/dist/src/middleware/reject-query-api-key.js +45 -0
- package/dist/src/middleware/security-headers.js +41 -0
- package/dist/src/middleware/telemetry.js +134 -0
- package/dist/src/models/payment.js +2 -0
- package/dist/src/routes/analytics.js +1447 -0
- package/dist/src/routes/app-routes.js +32 -0
- package/dist/src/routes/auth-routes.js +505 -0
- package/dist/src/routes/oauth-routes.js +325 -0
- package/dist/src/routes/payment-routes.js +186 -0
- package/dist/src/routes/persona-catalog-routes.js +84 -0
- package/dist/src/services/admin-service.js +229 -0
- package/dist/src/services/audit-log-persistence.js +60 -0
- package/dist/src/services/audit-log.js +69 -0
- package/dist/src/services/cookie-service.js +129 -0
- package/dist/src/services/dashboard-access.js +27 -0
- package/dist/src/services/demo-seed-service.js +139 -0
- package/dist/src/services/email-code.js +23 -0
- package/dist/src/services/email-service-clean.js +782 -0
- package/dist/src/services/email-service.js +951 -0
- package/dist/src/services/installer-service.js +131 -0
- package/dist/src/services/mcp-oauth-store.js +33 -0
- package/dist/src/services/mcp-service.js +823 -0
- package/dist/src/services/oauth-helpers.js +127 -0
- package/dist/src/services/org-service.js +89 -0
- package/dist/src/services/persona-entitlement-service.js +288 -0
- package/dist/src/services/provider-service.js +215 -0
- package/dist/src/services/registry-service.js +628 -0
- package/dist/src/services/session-service.js +86 -0
- package/dist/src/services/trial-reminder-service.js +120 -0
- package/dist/src/services/usage-analytics-service.js +419 -0
- package/dist/src/services/workspace-identity.js +21 -0
- package/dist/src/types/analytics.js +2 -0
- package/dist/src/utils/payment-calculator.js +52 -0
- package/extensions/office-word/favicon.ico +0 -0
- package/extensions/office-word/icon-64.png +0 -0
- package/extensions/office-word/manifest.xml +33 -0
- package/extensions/office-word/taskpane.html +242 -0
- package/package.json +14 -3
- package/public/ai-hub/index.html +14 -2
- package/public/ai-hub/script.js +340 -66
- package/public/ai-hub/styles.css +83 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RegistryService = void 0;
|
|
7
|
+
const fs_1 = require("fs");
|
|
8
|
+
const path_1 = require("path");
|
|
9
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
10
|
+
const stub_generator_1 = require("../core/utils/stub-generator");
|
|
11
|
+
const job_parser_1 = require("../core/utils/job-parser");
|
|
12
|
+
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
13
|
+
class RegistryService {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.fileIndex = new Map();
|
|
16
|
+
this.registryPath = this.findRegistryPath();
|
|
17
|
+
try {
|
|
18
|
+
this.buildFileIndex();
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
console.error('❌ RegistryService.buildFileIndex failed:', err?.message ?? err);
|
|
22
|
+
if (err?.stack)
|
|
23
|
+
console.error(err.stack);
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Find registry directory
|
|
29
|
+
* Checks multiple locations (dist/registry, registry/) and ensures robustness.
|
|
30
|
+
* Prioritizes source directory in development and dist/ in production.
|
|
31
|
+
*/
|
|
32
|
+
findRegistryPath() {
|
|
33
|
+
const isProd = process.env.NODE_ENV === 'production';
|
|
34
|
+
// Define candidate locations
|
|
35
|
+
const locations = [
|
|
36
|
+
// Preference 1: Explicitly from process.cwd() / registry (Development preference)
|
|
37
|
+
(0, path_1.join)(process.cwd(), 'registry'),
|
|
38
|
+
// Preference 2: Under dist/ (Production preference)
|
|
39
|
+
(0, path_1.join)(process.cwd(), 'dist', 'registry'),
|
|
40
|
+
// Preference 3: Relative to this file's directory (Fallback)
|
|
41
|
+
// from src/services/ or dist/src/services/
|
|
42
|
+
(0, path_1.join)(__dirname, '..', '..', 'registry'),
|
|
43
|
+
(0, path_1.join)(__dirname, '..', '..', '..', 'registry')
|
|
44
|
+
];
|
|
45
|
+
// If we're in production, try dist/ first
|
|
46
|
+
if (isProd) {
|
|
47
|
+
locations.unshift((0, path_1.join)(process.cwd(), 'dist', 'registry'));
|
|
48
|
+
}
|
|
49
|
+
for (const loc of locations) {
|
|
50
|
+
if ((0, fs_1.existsSync)(loc)) {
|
|
51
|
+
// Validation: A valid registry should have some expected subfolders
|
|
52
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(loc, 'jobs')) || (0, fs_1.existsSync)((0, path_1.join)(loc, 'rules')) || (0, fs_1.existsSync)((0, path_1.join)(loc, 'stubs'))) {
|
|
53
|
+
return loc;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Recursive search: Walk up from __dirname to find a valid 'registry'
|
|
58
|
+
let currentDir = __dirname;
|
|
59
|
+
while (currentDir !== (0, path_1.resolve)(currentDir, '..')) {
|
|
60
|
+
const candidate = (0, path_1.join)(currentDir, 'registry');
|
|
61
|
+
if ((0, fs_1.existsSync)(candidate) && ((0, fs_1.existsSync)((0, path_1.join)(candidate, 'jobs')) || (0, fs_1.existsSync)((0, path_1.join)(candidate, 'rules')))) {
|
|
62
|
+
return candidate;
|
|
63
|
+
}
|
|
64
|
+
const distCandidate = (0, path_1.join)(currentDir, 'dist', 'registry');
|
|
65
|
+
if ((0, fs_1.existsSync)(distCandidate) && (0, fs_1.existsSync)((0, path_1.join)(distCandidate, 'jobs'))) {
|
|
66
|
+
return distCandidate;
|
|
67
|
+
}
|
|
68
|
+
currentDir = (0, path_1.resolve)(currentDir, '..');
|
|
69
|
+
}
|
|
70
|
+
console.warn(`⚠️ RegistryService: registry directory not found in any standard location. Checked: ${locations.join(', ')}`);
|
|
71
|
+
// Final fallback: try to find something in cwd
|
|
72
|
+
return (0, path_1.join)(process.cwd(), 'registry');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Build an index of all files in registry directory
|
|
76
|
+
*/
|
|
77
|
+
buildFileIndex() {
|
|
78
|
+
this.fileIndex.clear();
|
|
79
|
+
// 1. Index the main registry
|
|
80
|
+
if ((0, fs_1.existsSync)(this.registryPath)) {
|
|
81
|
+
console.log(`🔍 RegistryService: Indexing ${this.registryPath}...`);
|
|
82
|
+
this.indexDirectory(this.registryPath);
|
|
83
|
+
console.log(`✅ RegistryService: Indexed ${this.fileIndex.size} files.`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
console.warn(`⚠️ RegistryService: registryPath does not exist: ${this.registryPath}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Index files in a directory
|
|
91
|
+
*/
|
|
92
|
+
indexDirectory(dir, basePath = '') {
|
|
93
|
+
const indexFile = (dirPath, relativeBase = '') => {
|
|
94
|
+
let entries;
|
|
95
|
+
try {
|
|
96
|
+
entries = (0, fs_1.readdirSync)(dirPath, { withFileTypes: true });
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
console.error(`❌ readdirSync failed for ${dirPath}:`, err?.message ?? err);
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
const fullPath = (0, path_1.join)(dirPath, entry.name);
|
|
104
|
+
// Normalize path separators to forward slashes for internal index
|
|
105
|
+
const relativePath = relativeBase ? (0, path_1.join)(relativeBase, entry.name) : entry.name;
|
|
106
|
+
const normalizedPath = relativePath.split(/[\\/]/).join('/');
|
|
107
|
+
let isFileEntry = entry.isFile();
|
|
108
|
+
const isDirectoryEntry = entry.isDirectory();
|
|
109
|
+
// On some environments (e.g., OneDrive/workspace virtualization),
|
|
110
|
+
// real files are surfaced as symlinks. Treat symlinked files as files.
|
|
111
|
+
if (!isFileEntry && !isDirectoryEntry && entry.isSymbolicLink()) {
|
|
112
|
+
try {
|
|
113
|
+
isFileEntry = (0, fs_1.statSync)(fullPath).isFile();
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Ignore broken symlinks
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (isDirectoryEntry) {
|
|
121
|
+
// Skip internal/junk directories
|
|
122
|
+
const isJunkDir = entry.name.includes('Issue ') ||
|
|
123
|
+
entry.name.includes('Master') ||
|
|
124
|
+
entry.name === 'dist' ||
|
|
125
|
+
entry.name === 'node_modules' ||
|
|
126
|
+
entry.name === '.git' ||
|
|
127
|
+
entry.name === '.github' ||
|
|
128
|
+
entry.name === 'github' ||
|
|
129
|
+
entry.name.startsWith('stubs.tmp-') ||
|
|
130
|
+
entry.name.endsWith('-phases');
|
|
131
|
+
if (isJunkDir) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
indexFile(fullPath, normalizedPath);
|
|
135
|
+
}
|
|
136
|
+
else if (isFileEntry) {
|
|
137
|
+
const extension = (0, path_1.extname)(entry.name).toLowerCase();
|
|
138
|
+
let type = 'other';
|
|
139
|
+
let category;
|
|
140
|
+
// Determine type and category based on path and extension
|
|
141
|
+
if (normalizedPath.includes('.github/')) {
|
|
142
|
+
// Skip github workflows
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const isJob = normalizedPath.includes('jobs/');
|
|
146
|
+
const isSkill = normalizedPath.includes('skills/');
|
|
147
|
+
if (isJob) {
|
|
148
|
+
type = 'job';
|
|
149
|
+
const parts = normalizedPath.split('/');
|
|
150
|
+
const jobIdx = parts.lastIndexOf('jobs');
|
|
151
|
+
if (jobIdx !== -1 && parts[jobIdx + 1]) {
|
|
152
|
+
category = parts[jobIdx + 1];
|
|
153
|
+
if ((category === 'ai-employee' || category === 'ai-manager') && parts[jobIdx + 2]) {
|
|
154
|
+
category = parts[jobIdx + 2];
|
|
155
|
+
}
|
|
156
|
+
if (category.endsWith('.md')) {
|
|
157
|
+
category = undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else if (isSkill) {
|
|
162
|
+
type = 'skill';
|
|
163
|
+
}
|
|
164
|
+
else if (normalizedPath.includes('rules/')) {
|
|
165
|
+
type = 'rule';
|
|
166
|
+
}
|
|
167
|
+
else if (normalizedPath.includes('templates/')) {
|
|
168
|
+
type = 'template';
|
|
169
|
+
}
|
|
170
|
+
else if (normalizedPath.includes('scripts/')) {
|
|
171
|
+
type = 'script';
|
|
172
|
+
}
|
|
173
|
+
else if (normalizedPath.includes('ai-manager-rules/') || normalizedPath.endsWith('-phases')) {
|
|
174
|
+
type = 'phase';
|
|
175
|
+
}
|
|
176
|
+
// Stubs are identified by path or content
|
|
177
|
+
const metadata = {
|
|
178
|
+
path: normalizedPath,
|
|
179
|
+
name: (0, path_1.basename)(entry.name, extension),
|
|
180
|
+
type,
|
|
181
|
+
category,
|
|
182
|
+
keywords: this.extractKeywords(entry.name, normalizedPath),
|
|
183
|
+
fullPath,
|
|
184
|
+
isStub: normalizedPath.includes('stubs/')
|
|
185
|
+
};
|
|
186
|
+
this.fileIndex.set(normalizedPath, metadata);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
indexFile(dir, basePath);
|
|
191
|
+
}
|
|
192
|
+
extractKeywords(filename, path) {
|
|
193
|
+
const tokens = [...filename.split(/[-_.\s]/), ...path.split('/')];
|
|
194
|
+
return Array.from(new Set(tokens))
|
|
195
|
+
.map(t => t.toLowerCase())
|
|
196
|
+
.filter(t => t.length > 2 && !['fraim', 'rule', 'template', 'md', 'ts', 'js'].includes(t));
|
|
197
|
+
}
|
|
198
|
+
collectMarkdownRelativePaths(rootDir, currentRelativeDir = '') {
|
|
199
|
+
if (!(0, fs_1.existsSync)(rootDir)) {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
const results = [];
|
|
203
|
+
const entries = (0, fs_1.readdirSync)(rootDir, { withFileTypes: true });
|
|
204
|
+
for (const entry of entries) {
|
|
205
|
+
const relativePath = currentRelativeDir
|
|
206
|
+
? (0, path_1.join)(currentRelativeDir, entry.name)
|
|
207
|
+
: entry.name;
|
|
208
|
+
const normalizedRelativePath = relativePath.split(/[\\/]/).join('/');
|
|
209
|
+
const fullPath = (0, path_1.join)(rootDir, entry.name);
|
|
210
|
+
if (entry.isDirectory()) {
|
|
211
|
+
results.push(...this.collectMarkdownRelativePaths(fullPath, normalizedRelativePath));
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
215
|
+
results.push(normalizedRelativePath);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return results;
|
|
219
|
+
}
|
|
220
|
+
getAvailableJobs() {
|
|
221
|
+
return Array.from(this.fileIndex.values())
|
|
222
|
+
.filter(m => m.type === 'job' && !m.path.includes('stubs/'))
|
|
223
|
+
.map(m => m.name);
|
|
224
|
+
}
|
|
225
|
+
getRegistryPath() {
|
|
226
|
+
return this.registryPath;
|
|
227
|
+
}
|
|
228
|
+
getFileIndex() {
|
|
229
|
+
return this.fileIndex;
|
|
230
|
+
}
|
|
231
|
+
findFileByPath(path) {
|
|
232
|
+
// console.log(`[RegistryService] findFileByPath: ${path}`);
|
|
233
|
+
return this.findFileByPathInternal(path, new Set());
|
|
234
|
+
}
|
|
235
|
+
choosePreferredMatch(matches) {
|
|
236
|
+
if (matches.length === 0) {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
return matches.find((match) => !match.isStub) || matches[0];
|
|
240
|
+
}
|
|
241
|
+
findFileByPathInternal(path, visited) {
|
|
242
|
+
const normalized = path.replace(/^\/+/, '').split(/[\\/]/).join('/');
|
|
243
|
+
if (visited.has(normalized))
|
|
244
|
+
return undefined;
|
|
245
|
+
visited.add(normalized);
|
|
246
|
+
const literal = this.fileIndex.get(normalized);
|
|
247
|
+
if (literal)
|
|
248
|
+
return literal;
|
|
249
|
+
if (!normalized.endsWith('.md')) {
|
|
250
|
+
const withMd = this.findFileByPathInternal(`${normalized}.md`, visited);
|
|
251
|
+
if (withMd)
|
|
252
|
+
return withMd;
|
|
253
|
+
}
|
|
254
|
+
// Role-based fallbacks for global registry
|
|
255
|
+
if (normalized.startsWith('skills/')) {
|
|
256
|
+
const sub = normalized.substring('skills/'.length);
|
|
257
|
+
if (!sub.startsWith('ai-employee/') && !sub.startsWith('ai-manager/')) {
|
|
258
|
+
return this.findFileByPathInternal(`skills/ai-employee/${sub}`, visited);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (normalized.startsWith('rules/')) {
|
|
262
|
+
const sub = normalized.substring('rules/'.length);
|
|
263
|
+
if (!sub.startsWith('ai-employee/') && !sub.startsWith('ai-manager/')) {
|
|
264
|
+
return this.findFileByPathInternal(`rules/ai-employee/${sub}`, visited);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (normalized.startsWith('jobs/')) {
|
|
268
|
+
const sub = normalized.substring('jobs/'.length);
|
|
269
|
+
if (!sub.startsWith('ai-employee/') && !sub.startsWith('ai-manager/')) {
|
|
270
|
+
return this.findFileByPathInternal(`jobs/ai-employee/${sub}`, visited) ||
|
|
271
|
+
this.findFileByPathInternal(`jobs/ai-manager/${sub}`, visited) ||
|
|
272
|
+
this.findFileByPathInternal(sub, visited);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (normalized.startsWith('ai-employee/') || normalized.startsWith('ai-manager/')) {
|
|
276
|
+
const isTypePrefixed = normalized.includes('/jobs/') ||
|
|
277
|
+
normalized.includes('/skills/') || normalized.includes('/rules/');
|
|
278
|
+
if (!isTypePrefixed) {
|
|
279
|
+
return this.findFileByPathInternal(`jobs/${normalized}`, visited);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Final fallback: suffix match for deeply nested leaf filenames
|
|
283
|
+
const suffixMatches = [];
|
|
284
|
+
for (const [indexPath, entry] of this.fileIndex.entries()) {
|
|
285
|
+
if (indexPath.endsWith(`/${normalized}`) || indexPath === normalized) {
|
|
286
|
+
suffixMatches.push(entry);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const preferredSuffixMatch = this.choosePreferredMatch(suffixMatches);
|
|
290
|
+
if (preferredSuffixMatch) {
|
|
291
|
+
return preferredSuffixMatch;
|
|
292
|
+
}
|
|
293
|
+
// Very desperate fallback: if it's just a filename, try matching it anywhere
|
|
294
|
+
if (!normalized.includes('/')) {
|
|
295
|
+
const filenameMatches = [];
|
|
296
|
+
for (const [indexPath, entry] of this.fileIndex.entries()) {
|
|
297
|
+
if (indexPath.endsWith(`/${normalized}`)) {
|
|
298
|
+
filenameMatches.push(entry);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return this.choosePreferredMatch(filenameMatches);
|
|
302
|
+
}
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
getMetadata(path) {
|
|
306
|
+
return this.fileIndex.get(path);
|
|
307
|
+
}
|
|
308
|
+
getSyncBundle() {
|
|
309
|
+
const files = [];
|
|
310
|
+
const allFiles = Array.from(this.fileIndex.values());
|
|
311
|
+
// 1. Add job STUBS (not full jobs) from registry/stubs/jobs
|
|
312
|
+
const jobStubs = allFiles.filter(f => f.type === 'job' && f.path.includes('stubs/jobs/'));
|
|
313
|
+
let jobStubCount = 0;
|
|
314
|
+
for (const stub of jobStubs) {
|
|
315
|
+
try {
|
|
316
|
+
const content = (0, fs_1.readFileSync)(stub.fullPath, 'utf8');
|
|
317
|
+
const relativePath = stub.path.replace('stubs/jobs/', '');
|
|
318
|
+
files.push({
|
|
319
|
+
path: relativePath,
|
|
320
|
+
content,
|
|
321
|
+
type: 'job'
|
|
322
|
+
});
|
|
323
|
+
jobStubCount++;
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
console.error(`⚠️ Failed to read job stub ${stub.path}:`, error);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// 2b. Fallback: synthesize job stubs
|
|
330
|
+
const existingJobPaths = new Set(files.filter(f => f.type === 'job').map(f => f.path));
|
|
331
|
+
const jobs = allFiles.filter(f => f.type === 'job' && !f.path.includes('stubs/') && f.path.startsWith('jobs/') && f.path.endsWith('.md'));
|
|
332
|
+
if (jobStubCount < jobs.length) {
|
|
333
|
+
for (const job of jobs) {
|
|
334
|
+
try {
|
|
335
|
+
const relativePath = job.path.replace('jobs/', '');
|
|
336
|
+
if (existingJobPaths.has(relativePath)) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const content = (0, fs_1.readFileSync)(job.fullPath, 'utf8');
|
|
340
|
+
const { intent, outcome, steps } = (0, stub_generator_1.parseRegistryJob)(content);
|
|
341
|
+
const stub = (0, stub_generator_1.generateJobStub)(job.name, relativePath, intent, outcome, steps);
|
|
342
|
+
files.push({
|
|
343
|
+
path: relativePath,
|
|
344
|
+
content: stub,
|
|
345
|
+
type: 'job'
|
|
346
|
+
});
|
|
347
|
+
existingJobPaths.add(relativePath);
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
console.error(`⚠️ Failed to synthesize job stub ${job.path}:`, error);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// 2c. Add skill STUBS from registry/stubs/skills
|
|
355
|
+
const skillStubs = allFiles.filter(f => f.type === 'skill' && f.path.includes('stubs/skills/'));
|
|
356
|
+
const existingSkillPaths = new Set(files.filter(f => f.type === 'skill').map(f => f.path));
|
|
357
|
+
let skillStubCount = 0;
|
|
358
|
+
for (const stub of skillStubs) {
|
|
359
|
+
try {
|
|
360
|
+
const content = (0, fs_1.readFileSync)(stub.fullPath, 'utf8');
|
|
361
|
+
const relativePath = stub.path.replace('stubs/skills/', '');
|
|
362
|
+
if (existingSkillPaths.has(relativePath)) {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
files.push({
|
|
366
|
+
path: relativePath,
|
|
367
|
+
content,
|
|
368
|
+
type: 'skill'
|
|
369
|
+
});
|
|
370
|
+
existingSkillPaths.add(relativePath);
|
|
371
|
+
skillStubCount++;
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
console.error(`⚠️ Failed to read skill stub ${stub.path}:`, error);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// 2d. Fallback: synthesize skill stubs
|
|
378
|
+
const skills = allFiles.filter(f => f.type === 'skill' && !f.path.includes('stubs/') && f.path.startsWith('skills/') && f.path.endsWith('.md'));
|
|
379
|
+
if (skillStubCount < skills.length) {
|
|
380
|
+
for (const skill of skills) {
|
|
381
|
+
try {
|
|
382
|
+
const relativePath = skill.path.replace('skills/', '');
|
|
383
|
+
if (existingSkillPaths.has(relativePath)) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
const content = (0, fs_1.readFileSync)(skill.fullPath, 'utf8');
|
|
387
|
+
const { skillInput, skillOutput } = (0, stub_generator_1.parseRegistrySkill)(content);
|
|
388
|
+
const stub = (0, stub_generator_1.generateSkillStub)(skill.name, relativePath, skillInput, skillOutput);
|
|
389
|
+
files.push({
|
|
390
|
+
path: relativePath,
|
|
391
|
+
content: stub,
|
|
392
|
+
type: 'skill'
|
|
393
|
+
});
|
|
394
|
+
existingSkillPaths.add(relativePath);
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
console.error(`⚠️ Failed to synthesize skill stub ${skill.path}:`, error);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// 2e. Add rule STUBS from registry/stubs/rules
|
|
402
|
+
const ruleStubs = allFiles.filter(f => f.type === 'rule' && f.path.includes('stubs/rules/'));
|
|
403
|
+
const existingRulePaths = new Set(files.filter(f => f.type === 'rule').map(f => f.path));
|
|
404
|
+
let ruleStubCount = 0;
|
|
405
|
+
for (const stub of ruleStubs) {
|
|
406
|
+
try {
|
|
407
|
+
const content = (0, fs_1.readFileSync)(stub.fullPath, 'utf8');
|
|
408
|
+
const relativePath = stub.path.replace('stubs/rules/', '');
|
|
409
|
+
if (existingRulePaths.has(relativePath)) {
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
files.push({
|
|
413
|
+
path: relativePath,
|
|
414
|
+
content,
|
|
415
|
+
type: 'rule'
|
|
416
|
+
});
|
|
417
|
+
existingRulePaths.add(relativePath);
|
|
418
|
+
ruleStubCount++;
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
console.error(`⚠️ Failed to read rule stub ${stub.path}:`, error);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// 2f. Fallback: synthesize rule stubs
|
|
425
|
+
const rules = allFiles.filter(f => f.type === 'rule' && !f.path.includes('stubs/') && f.path.startsWith('rules/') && f.path.endsWith('.md'));
|
|
426
|
+
if (ruleStubCount < rules.length) {
|
|
427
|
+
for (const rule of rules) {
|
|
428
|
+
try {
|
|
429
|
+
const relativePath = rule.path.replace('rules/', '');
|
|
430
|
+
if (existingRulePaths.has(relativePath)) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
const content = (0, fs_1.readFileSync)(rule.fullPath, 'utf8');
|
|
434
|
+
const { intent } = (0, stub_generator_1.parseRegistryRule)(content);
|
|
435
|
+
const stub = (0, stub_generator_1.generateRuleStub)(rule.name, relativePath, intent);
|
|
436
|
+
files.push({
|
|
437
|
+
path: relativePath,
|
|
438
|
+
content: stub,
|
|
439
|
+
type: 'rule'
|
|
440
|
+
});
|
|
441
|
+
existingRulePaths.add(relativePath);
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
console.error(`⚠️ Failed to synthesize rule stub ${rule.path}:`, error);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// 3. Add scripts
|
|
449
|
+
const scripts = allFiles.filter(f => f.type === 'script');
|
|
450
|
+
for (const script of scripts) {
|
|
451
|
+
const ext = (0, path_1.extname)(script.path);
|
|
452
|
+
// Only skip .js/.js.map files if a corresponding .ts file exists
|
|
453
|
+
// Allow all other file types (.css, .sh, .py, etc.) to pass through
|
|
454
|
+
if (ext === '.js' || ext === '.js.map') {
|
|
455
|
+
const tsPath = script.path.replace(/\.js(\.map)?$/, '.ts');
|
|
456
|
+
if (Array.from(this.fileIndex.values()).some(f => f.path === tsPath)) {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
const content = (0, fs_1.readFileSync)(script.fullPath, 'utf8');
|
|
462
|
+
const scriptRelativePath = script.path.startsWith('scripts/')
|
|
463
|
+
? script.path.substring('scripts/'.length)
|
|
464
|
+
: script.name;
|
|
465
|
+
files.push({
|
|
466
|
+
path: scriptRelativePath,
|
|
467
|
+
content,
|
|
468
|
+
type: 'script'
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
console.error(`⚠️ Failed to read script ${script.path}:`, error);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// 5. Add docs
|
|
476
|
+
const docsDir = (0, path_1.join)(this.registryPath, 'docs');
|
|
477
|
+
if ((0, fs_1.existsSync)(docsDir)) {
|
|
478
|
+
try {
|
|
479
|
+
const docsEntries = (0, fs_1.readdirSync)(docsDir, { withFileTypes: true });
|
|
480
|
+
for (const entry of docsEntries) {
|
|
481
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
482
|
+
const fullPath = (0, path_1.join)(docsDir, entry.name);
|
|
483
|
+
try {
|
|
484
|
+
const content = (0, fs_1.readFileSync)(fullPath, 'utf8');
|
|
485
|
+
files.push({
|
|
486
|
+
path: entry.name,
|
|
487
|
+
content,
|
|
488
|
+
type: 'docs'
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
console.error(`⚠️ Failed to read docs file ${entry.name}:`, error);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
catch (err) {
|
|
498
|
+
console.warn(`⚠️ Failed to read docs directory: ${docsDir}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return files;
|
|
502
|
+
}
|
|
503
|
+
getGitHubWorkflowBundle() {
|
|
504
|
+
const workflowsDir = (0, path_1.join)(this.registryPath, 'github', 'workflows');
|
|
505
|
+
if (!(0, fs_1.existsSync)(workflowsDir)) {
|
|
506
|
+
return [];
|
|
507
|
+
}
|
|
508
|
+
const files = [];
|
|
509
|
+
const entries = (0, fs_1.readdirSync)(workflowsDir, { withFileTypes: true });
|
|
510
|
+
for (const entry of entries) {
|
|
511
|
+
if (!entry.isFile() || !entry.name.endsWith('.yml')) {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
const fullPath = (0, path_1.join)(workflowsDir, entry.name);
|
|
515
|
+
const content = (0, fs_1.readFileSync)(fullPath, 'utf8');
|
|
516
|
+
files.push({
|
|
517
|
+
path: entry.name,
|
|
518
|
+
content,
|
|
519
|
+
type: 'github-workflow',
|
|
520
|
+
digest: `sha256:${crypto_1.default.createHash('sha256').update(content).digest('hex')}`
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
524
|
+
}
|
|
525
|
+
getPersonalizedTopology() {
|
|
526
|
+
const personalizedRoot = (0, project_fraim_paths_1.getWorkspaceFraimPath)(process.cwd(), 'personalized-employee');
|
|
527
|
+
if (!(0, fs_1.existsSync)(personalizedRoot)) {
|
|
528
|
+
return { nodes: [], edges: [] };
|
|
529
|
+
}
|
|
530
|
+
const nodes = [];
|
|
531
|
+
const edges = [];
|
|
532
|
+
const seenNodeIds = new Set();
|
|
533
|
+
const normalize = (value) => value.split(/[\\/]/).join('/');
|
|
534
|
+
const addNode = (node) => {
|
|
535
|
+
if (seenNodeIds.has(node.id))
|
|
536
|
+
return;
|
|
537
|
+
seenNodeIds.add(node.id);
|
|
538
|
+
nodes.push(node);
|
|
539
|
+
};
|
|
540
|
+
const skillsRoot = (0, path_1.join)(personalizedRoot, 'skills');
|
|
541
|
+
if ((0, fs_1.existsSync)(skillsRoot)) {
|
|
542
|
+
for (const relativePath of this.collectMarkdownRelativePaths(skillsRoot)) {
|
|
543
|
+
const normalizedRelativePath = normalize(relativePath);
|
|
544
|
+
const skillId = normalizedRelativePath.replace(/\.md$/, '');
|
|
545
|
+
const category = skillId.split('/')[0] || 'uncategorized';
|
|
546
|
+
const baselinePath = (0, path_1.join)(this.registryPath, 'skills', normalizedRelativePath);
|
|
547
|
+
addNode({
|
|
548
|
+
id: skillId,
|
|
549
|
+
label: (0, path_1.basename)(skillId),
|
|
550
|
+
type: 'skill',
|
|
551
|
+
category,
|
|
552
|
+
group: skillId.includes('/') ? skillId.slice(0, skillId.lastIndexOf('/')) : skillId,
|
|
553
|
+
role: null,
|
|
554
|
+
path: (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`personalized-employee/skills/${normalizedRelativePath}`),
|
|
555
|
+
shadowing: (0, fs_1.existsSync)(baselinePath)
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const jobsRoot = (0, path_1.join)(personalizedRoot, 'jobs');
|
|
560
|
+
if ((0, fs_1.existsSync)(jobsRoot)) {
|
|
561
|
+
for (const relativePath of this.collectMarkdownRelativePaths(jobsRoot)) {
|
|
562
|
+
const normalizedRelativePath = normalize(relativePath);
|
|
563
|
+
const jobPathWithoutExt = normalizedRelativePath.replace(/\.md$/, '');
|
|
564
|
+
const parts = jobPathWithoutExt.split('/');
|
|
565
|
+
const category = parts[0] || 'uncategorized';
|
|
566
|
+
const jobId = `job:${jobPathWithoutExt}`;
|
|
567
|
+
const baselinePath = (0, path_1.join)(this.registryPath, 'jobs', normalizedRelativePath);
|
|
568
|
+
const fullPath = (0, path_1.join)(jobsRoot, normalizedRelativePath);
|
|
569
|
+
const content = (0, fs_1.readFileSync)(fullPath, 'utf8');
|
|
570
|
+
addNode({
|
|
571
|
+
id: jobId,
|
|
572
|
+
label: (0, path_1.basename)(jobPathWithoutExt),
|
|
573
|
+
type: 'job',
|
|
574
|
+
category,
|
|
575
|
+
group: parts.slice(0, -1).join('/') || category,
|
|
576
|
+
role: category === 'ai-manager' ? 'ai-manager' : 'ai-employee',
|
|
577
|
+
path: (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`personalized-employee/jobs/${normalizedRelativePath}`),
|
|
578
|
+
shadowing: (0, fs_1.existsSync)(baselinePath)
|
|
579
|
+
});
|
|
580
|
+
const includes = [...content.matchAll(/\{\{include:skills\/([^}]+)\.md\}\}/g)];
|
|
581
|
+
const seenSkills = new Set();
|
|
582
|
+
for (const match of includes) {
|
|
583
|
+
const skillId = match[1];
|
|
584
|
+
if (!skillId || seenSkills.has(skillId)) {
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
seenSkills.add(skillId);
|
|
588
|
+
edges.push({ source: jobId, target: skillId });
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return { nodes, edges };
|
|
593
|
+
}
|
|
594
|
+
async getFile(path) {
|
|
595
|
+
const metadata = this.findFileByPath(path);
|
|
596
|
+
if (!metadata)
|
|
597
|
+
return null;
|
|
598
|
+
try {
|
|
599
|
+
return (0, fs_1.readFileSync)(metadata.fullPath, 'utf8');
|
|
600
|
+
}
|
|
601
|
+
catch (e) {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async getJob(name) {
|
|
606
|
+
const allMatches = Array.from(this.fileIndex.values()).filter(f => f.name === name && f.type === 'job');
|
|
607
|
+
if (allMatches.length === 0)
|
|
608
|
+
return null;
|
|
609
|
+
const metadata = allMatches.find(f => !f.isStub) || allMatches[0];
|
|
610
|
+
const def = job_parser_1.JobParser.parse(metadata.fullPath);
|
|
611
|
+
if (def && !def.metadata.type) {
|
|
612
|
+
def.metadata.type = 'job';
|
|
613
|
+
}
|
|
614
|
+
return def;
|
|
615
|
+
}
|
|
616
|
+
async listItems(type) {
|
|
617
|
+
return Array.from(this.fileIndex.values())
|
|
618
|
+
.filter(f => !f.isStub && f.type === 'job')
|
|
619
|
+
.map(f => ({
|
|
620
|
+
name: f.name,
|
|
621
|
+
path: f.path,
|
|
622
|
+
type: f.type,
|
|
623
|
+
category: f.category,
|
|
624
|
+
description: job_parser_1.JobParser.extractDescription(f.fullPath)
|
|
625
|
+
}));
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
exports.RegistryService = RegistryService;
|