fraim-framework 2.0.171 → 2.0.173
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/hosts.js +227 -6
- package/dist/src/ai-hub/server.js +1014 -35
- package/dist/src/cli/commands/add-ide.js +2 -0
- package/dist/src/cli/commands/cleanup-artifacts.js +38 -0
- package/dist/src/cli/commands/init-project.js +12 -5
- package/dist/src/cli/commands/sync.js +74 -7
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/setup/ide-detector.js +6 -0
- package/dist/src/cli/utils/agent-adapters.js +40 -18
- package/dist/src/cli/utils/fraim-gitignore.js +13 -0
- package/dist/src/cli/utils/remote-sync.js +129 -53
- package/dist/src/cli/utils/user-config.js +12 -0
- package/dist/src/config/ai-manager-hiring.js +121 -0
- package/dist/src/config/compat.js +16 -0
- package/dist/src/config/feature-flags.js +25 -0
- package/dist/src/config/persona-capability-bundles.js +273 -0
- package/dist/src/config/persona-hiring.js +270 -0
- package/dist/src/config/portfolio-slug-overrides.js +17 -0
- package/dist/src/config/pricing.js +37 -0
- package/dist/src/config/stripe.js +43 -0
- package/dist/src/core/fraim-config-schema.generated.js +8 -2
- package/dist/src/core/utils/local-registry-resolver.js +26 -0
- package/dist/src/core/utils/project-fraim-paths.js +89 -2
- package/dist/src/first-run/session-service.js +9 -0
- package/dist/src/local-mcp-server/artifact-retention-cleanup.js +255 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +41 -81
- package/dist/src/local-mcp-server/stdio-server.js +42 -7
- package/package.json +5 -1
- package/public/ai-hub/index.html +205 -89
- package/public/ai-hub/review.css +12 -0
- package/public/ai-hub/script.js +1720 -240
- package/public/ai-hub/styles.css +473 -6
|
@@ -11,6 +11,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
11
11
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
12
12
|
};
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.SYNCED_CONTENT_BANNER_MARKER = void 0;
|
|
14
15
|
exports.syncFromRemote = syncFromRemote;
|
|
15
16
|
const axios_1 = __importDefault(require("axios"));
|
|
16
17
|
const fs_1 = require("fs");
|
|
@@ -20,7 +21,41 @@ const script_sync_utils_1 = require("./script-sync-utils");
|
|
|
20
21
|
const fraim_gitignore_1 = require("./fraim-gitignore");
|
|
21
22
|
const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
|
|
22
23
|
const LOCK_SYNCED_CONTENT_ENV = 'FRAIM_LOCK_SYNCED_CONTENT';
|
|
23
|
-
|
|
24
|
+
exports.SYNCED_CONTENT_BANNER_MARKER = '<!-- FRAIM_SYNC_MANAGED_CONTENT -->';
|
|
25
|
+
function isTransientNetworkError(error) {
|
|
26
|
+
if (error?.response) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
const code = String(error?.code || '');
|
|
30
|
+
const message = String(error?.message || '');
|
|
31
|
+
return ['ECONNRESET', 'ETIMEDOUT', 'ECONNABORTED', 'EPIPE', 'socket hang up'].some((needle) => code.includes(needle) || message.includes(needle));
|
|
32
|
+
}
|
|
33
|
+
function wait(ms) {
|
|
34
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
35
|
+
}
|
|
36
|
+
async function fetchRegistrySync(remoteUrl, apiKey) {
|
|
37
|
+
let lastError;
|
|
38
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
39
|
+
try {
|
|
40
|
+
return await axios_1.default.get(`${remoteUrl}/api/registry/sync`, {
|
|
41
|
+
headers: {
|
|
42
|
+
'x-api-key': apiKey,
|
|
43
|
+
Connection: 'close'
|
|
44
|
+
},
|
|
45
|
+
timeout: 30000
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
lastError = error;
|
|
50
|
+
if (!isTransientNetworkError(error) || attempt === 3) {
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
console.warn(chalk_1.default.yellow(`Registry sync request reset; retrying (${attempt + 1}/3)...`));
|
|
54
|
+
await wait(250 * attempt);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
throw lastError;
|
|
58
|
+
}
|
|
24
59
|
function shouldLockSyncedContent() {
|
|
25
60
|
const raw = process.env[LOCK_SYNCED_CONTENT_ENV];
|
|
26
61
|
if (!raw) {
|
|
@@ -35,15 +70,64 @@ function getSyncedContentLockTargets(projectRoot) {
|
|
|
35
70
|
.map((entry) => entry.replace(/[\\/]+$/, ''))
|
|
36
71
|
.map((entry) => (0, path_1.join)(projectRoot, entry));
|
|
37
72
|
}
|
|
38
|
-
function
|
|
73
|
+
function isPathInside(parentPath, childPath) {
|
|
74
|
+
const relativePath = (0, path_1.relative)(parentPath, childPath);
|
|
75
|
+
return relativePath.length > 0 && !relativePath.startsWith('..') && !(0, path_1.isAbsolute)(relativePath);
|
|
76
|
+
}
|
|
77
|
+
function assertPathInsideDirectory(baseDir, targetPath, label) {
|
|
78
|
+
const resolvedBase = (0, path_1.resolve)(baseDir);
|
|
79
|
+
const resolvedTarget = (0, path_1.resolve)(targetPath);
|
|
80
|
+
if (!isPathInside(resolvedBase, resolvedTarget)) {
|
|
81
|
+
throw new Error(`Refusing to sync ${label}: "${resolvedTarget}" is outside "${resolvedBase}".`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function normalizeRegistryRelativePath(rawPath, label) {
|
|
85
|
+
const normalized = rawPath.replace(/\\/g, '/');
|
|
86
|
+
const parts = normalized.split('/');
|
|
87
|
+
if (normalized.length === 0 ||
|
|
88
|
+
normalized.includes('\0') ||
|
|
89
|
+
(0, path_1.isAbsolute)(normalized) ||
|
|
90
|
+
/^[a-zA-Z]:\//.test(normalized) ||
|
|
91
|
+
parts.some((part) => part.length === 0 || part === '.' || part === '..')) {
|
|
92
|
+
throw new Error(`Refusing to sync ${label}: unsafe registry path "${rawPath}".`);
|
|
93
|
+
}
|
|
94
|
+
return normalized;
|
|
95
|
+
}
|
|
96
|
+
function prepareWorkspaceDirectory(projectRoot, dirPath, _label, ...expectedParts) {
|
|
97
|
+
(0, project_fraim_paths_1.assertExactWorkspaceFraimPath)(projectRoot, dirPath, ...expectedParts);
|
|
98
|
+
if (!(0, fs_1.existsSync)(dirPath)) {
|
|
99
|
+
(0, fs_1.mkdirSync)(dirPath, { recursive: true });
|
|
100
|
+
}
|
|
101
|
+
(0, project_fraim_paths_1.assertExactWorkspaceFraimPath)(projectRoot, dirPath, ...expectedParts);
|
|
102
|
+
}
|
|
103
|
+
function cleanWorkspaceDirectory(projectRoot, dirPath, label, assertWorkspacePath, ...expectedParts) {
|
|
104
|
+
prepareWorkspaceDirectory(projectRoot, dirPath, label, ...expectedParts);
|
|
105
|
+
cleanDirectory(dirPath, assertWorkspacePath);
|
|
106
|
+
}
|
|
107
|
+
function resolveWorkspaceRegistryFile(projectRoot, baseDir, registryPath, label, assertWorkspacePath) {
|
|
108
|
+
const relativePath = normalizeRegistryRelativePath(registryPath, label);
|
|
109
|
+
const filePath = (0, path_1.join)(baseDir, relativePath);
|
|
110
|
+
assertPathInsideDirectory(baseDir, filePath, label);
|
|
111
|
+
assertWorkspacePath(filePath);
|
|
112
|
+
return { filePath, relativePath };
|
|
113
|
+
}
|
|
114
|
+
function resolveUserRegistryFile(baseDir, registryPath, label) {
|
|
115
|
+
const relativePath = normalizeRegistryRelativePath(registryPath, label);
|
|
116
|
+
const filePath = (0, path_1.join)(baseDir, relativePath);
|
|
117
|
+
assertPathInsideDirectory(baseDir, filePath, label);
|
|
118
|
+
return { filePath, relativePath };
|
|
119
|
+
}
|
|
120
|
+
function setFileWriteLockRecursively(dirPath, readOnly, assertSafePath = () => undefined) {
|
|
39
121
|
if (!(0, fs_1.existsSync)(dirPath)) {
|
|
40
122
|
return;
|
|
41
123
|
}
|
|
124
|
+
assertSafePath(dirPath);
|
|
42
125
|
const entries = (0, fs_1.readdirSync)(dirPath, { withFileTypes: true });
|
|
43
126
|
for (const entry of entries) {
|
|
44
127
|
const fullPath = (0, path_1.join)(dirPath, entry.name);
|
|
128
|
+
assertSafePath(fullPath);
|
|
45
129
|
if (entry.isDirectory()) {
|
|
46
|
-
setFileWriteLockRecursively(fullPath, readOnly);
|
|
130
|
+
setFileWriteLockRecursively(fullPath, readOnly, assertSafePath);
|
|
47
131
|
continue;
|
|
48
132
|
}
|
|
49
133
|
try {
|
|
@@ -80,7 +164,7 @@ function insertAfterFrontmatter(content, banner) {
|
|
|
80
164
|
return `${frontmatter}${banner}${body}`;
|
|
81
165
|
}
|
|
82
166
|
function buildSyncedContentBanner(typeLabel) {
|
|
83
|
-
return `${SYNCED_CONTENT_BANNER_MARKER}\r\n> [!IMPORTANT]\r\n> This ${typeLabel} is synced from FRAIM and will be overwritten on the next \`fraim sync\`.\r\n> Do not edit this file.\r\n`;
|
|
167
|
+
return `${exports.SYNCED_CONTENT_BANNER_MARKER}\r\n> [!IMPORTANT]\r\n> This ${typeLabel} is synced from FRAIM and will be overwritten on the next \`fraim sync\`.\r\n> Do not edit this file.\r\n`;
|
|
84
168
|
}
|
|
85
169
|
function applySyncedContentBanner(file) {
|
|
86
170
|
const registryPath = getBannerRegistryPath(file);
|
|
@@ -112,15 +196,12 @@ async function syncFromRemote(options) {
|
|
|
112
196
|
};
|
|
113
197
|
}
|
|
114
198
|
try {
|
|
199
|
+
(0, project_fraim_paths_1.assertWorkspaceFraimRoot)(options.projectRoot);
|
|
200
|
+
const assertWorkspacePath = (0, project_fraim_paths_1.createWorkspaceFraimPathAsserter)(options.projectRoot);
|
|
115
201
|
console.log(chalk_1.default.blue('🔄 Syncing from remote FRAIM server...'));
|
|
116
202
|
console.log(chalk_1.default.gray(` Remote: ${remoteUrl}`));
|
|
117
203
|
// Fetch registry files from remote server
|
|
118
|
-
const response = await
|
|
119
|
-
headers: {
|
|
120
|
-
'x-api-key': apiKey
|
|
121
|
-
},
|
|
122
|
-
timeout: 30000
|
|
123
|
-
});
|
|
204
|
+
const response = await fetchRegistrySync(remoteUrl, apiKey);
|
|
124
205
|
const files = response.data.files || [];
|
|
125
206
|
if (!files || files.length === 0) {
|
|
126
207
|
console.log(chalk_1.default.yellow('⚠️ No files received from remote server'));
|
|
@@ -136,94 +217,85 @@ async function syncFromRemote(options) {
|
|
|
136
217
|
};
|
|
137
218
|
}
|
|
138
219
|
const lockTargets = getSyncedContentLockTargets(options.projectRoot);
|
|
220
|
+
for (const target of lockTargets) {
|
|
221
|
+
assertWorkspacePath(target);
|
|
222
|
+
}
|
|
139
223
|
if (shouldLockSyncedContent()) {
|
|
140
224
|
// If previous sync locked these paths read-only, temporarily unlock before cleanup/write.
|
|
141
225
|
for (const target of lockTargets) {
|
|
142
|
-
setFileWriteLockRecursively(target, false);
|
|
226
|
+
setFileWriteLockRecursively(target, false, assertWorkspacePath);
|
|
143
227
|
}
|
|
144
228
|
}
|
|
145
229
|
// Sync job stubs to role-specific folders under fraim/
|
|
146
230
|
const allJobFiles = files.filter(f => f.type === 'job');
|
|
147
|
-
const managerJobFiles = allJobFiles.filter(f => f.path.startsWith('ai-manager/'));
|
|
148
|
-
const jobFiles = allJobFiles.filter(f => !f.path.startsWith('ai-manager/'));
|
|
231
|
+
const managerJobFiles = allJobFiles.filter(f => f.path.replace(/\\/g, '/').startsWith('ai-manager/'));
|
|
232
|
+
const jobFiles = allJobFiles.filter(f => !f.path.replace(/\\/g, '/').startsWith('ai-manager/'));
|
|
149
233
|
const employeeJobsDir = (0, project_fraim_paths_1.getWorkspaceFraimPath)(options.projectRoot, 'ai-employee', 'jobs');
|
|
150
|
-
|
|
151
|
-
(0, fs_1.mkdirSync)(employeeJobsDir, { recursive: true });
|
|
152
|
-
}
|
|
153
|
-
cleanDirectory(employeeJobsDir);
|
|
234
|
+
cleanWorkspaceDirectory(options.projectRoot, employeeJobsDir, 'employee job directory', assertWorkspacePath, 'ai-employee', 'jobs');
|
|
154
235
|
for (const file of jobFiles) {
|
|
155
236
|
// Strip "jobs/" prefix and "ai-employee/" role prefix
|
|
156
|
-
let relPath = file.path;
|
|
237
|
+
let relPath = file.path.replace(/\\/g, '/');
|
|
157
238
|
if (relPath.startsWith('jobs/'))
|
|
158
239
|
relPath = relPath.substring('jobs/'.length);
|
|
159
240
|
relPath = relPath.replace(/^ai-employee\//, '');
|
|
160
|
-
const filePath = (
|
|
241
|
+
const { filePath, relativePath } = resolveWorkspaceRegistryFile(options.projectRoot, employeeJobsDir, relPath, 'employee job file', assertWorkspacePath);
|
|
161
242
|
const fileDir = (0, path_1.dirname)(filePath);
|
|
162
243
|
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
163
244
|
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
164
245
|
}
|
|
165
246
|
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
166
|
-
console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`ai-employee/jobs/${
|
|
247
|
+
console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`ai-employee/jobs/${relativePath}`)}`));
|
|
167
248
|
}
|
|
168
249
|
// Sync ai-manager job stubs to fraim/ai-manager/jobs/
|
|
169
250
|
const managerJobsDir = (0, project_fraim_paths_1.getWorkspaceFraimPath)(options.projectRoot, 'ai-manager', 'jobs');
|
|
170
|
-
|
|
171
|
-
(0, fs_1.mkdirSync)(managerJobsDir, { recursive: true });
|
|
172
|
-
}
|
|
173
|
-
cleanDirectory(managerJobsDir);
|
|
251
|
+
cleanWorkspaceDirectory(options.projectRoot, managerJobsDir, 'manager job directory', assertWorkspacePath, 'ai-manager', 'jobs');
|
|
174
252
|
for (const file of managerJobFiles) {
|
|
175
253
|
// Strip "jobs/" prefix and "ai-manager/" role prefix
|
|
176
|
-
let relPath = file.path;
|
|
254
|
+
let relPath = file.path.replace(/\\/g, '/');
|
|
177
255
|
if (relPath.startsWith('jobs/'))
|
|
178
256
|
relPath = relPath.substring('jobs/'.length);
|
|
179
257
|
relPath = relPath.replace(/^ai-manager\//, '');
|
|
180
|
-
const filePath = (
|
|
258
|
+
const { filePath, relativePath } = resolveWorkspaceRegistryFile(options.projectRoot, managerJobsDir, relPath, 'manager job file', assertWorkspacePath);
|
|
181
259
|
const fileDir = (0, path_1.dirname)(filePath);
|
|
182
260
|
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
183
261
|
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
184
262
|
}
|
|
185
263
|
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
186
|
-
console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`ai-manager/jobs/${
|
|
264
|
+
console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`ai-manager/jobs/${relativePath}`)}`));
|
|
187
265
|
}
|
|
188
266
|
// Sync skill STUBS to fraim/ai-employee/skills/
|
|
189
267
|
const skillFiles = files.filter(f => f.type === 'skill');
|
|
190
268
|
const skillsDir = (0, project_fraim_paths_1.getWorkspaceFraimPath)(options.projectRoot, 'ai-employee', 'skills');
|
|
191
|
-
|
|
192
|
-
(0, fs_1.mkdirSync)(skillsDir, { recursive: true });
|
|
193
|
-
}
|
|
194
|
-
cleanDirectory(skillsDir);
|
|
269
|
+
cleanWorkspaceDirectory(options.projectRoot, skillsDir, 'skill directory', assertWorkspacePath, 'ai-employee', 'skills');
|
|
195
270
|
for (const file of skillFiles) {
|
|
196
271
|
// Strip "skills/" prefix to avoid redundant nesting in fraim/ai-employee/skills/
|
|
197
|
-
let relPath = file.path;
|
|
272
|
+
let relPath = file.path.replace(/\\/g, '/');
|
|
198
273
|
if (relPath.startsWith('skills/'))
|
|
199
274
|
relPath = relPath.substring('skills/'.length);
|
|
200
|
-
const filePath = (
|
|
275
|
+
const { filePath, relativePath } = resolveWorkspaceRegistryFile(options.projectRoot, skillsDir, relPath, 'skill file', assertWorkspacePath);
|
|
201
276
|
const fileDir = (0, path_1.dirname)(filePath);
|
|
202
277
|
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
203
278
|
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
204
279
|
}
|
|
205
280
|
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
206
|
-
console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`ai-employee/skills/${
|
|
281
|
+
console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`ai-employee/skills/${relativePath}`)} (stub)`));
|
|
207
282
|
}
|
|
208
283
|
// Sync rule STUBS to fraim/ai-employee/rules/
|
|
209
284
|
const ruleFiles = files.filter(f => f.type === 'rule');
|
|
210
285
|
const rulesDir = (0, project_fraim_paths_1.getWorkspaceFraimPath)(options.projectRoot, 'ai-employee', 'rules');
|
|
211
|
-
|
|
212
|
-
(0, fs_1.mkdirSync)(rulesDir, { recursive: true });
|
|
213
|
-
}
|
|
214
|
-
cleanDirectory(rulesDir);
|
|
286
|
+
cleanWorkspaceDirectory(options.projectRoot, rulesDir, 'rule directory', assertWorkspacePath, 'ai-employee', 'rules');
|
|
215
287
|
for (const file of ruleFiles) {
|
|
216
288
|
// Strip "rules/" prefix to avoid redundant nesting in fraim/ai-employee/rules/
|
|
217
|
-
let relPath = file.path;
|
|
289
|
+
let relPath = file.path.replace(/\\/g, '/');
|
|
218
290
|
if (relPath.startsWith('rules/'))
|
|
219
291
|
relPath = relPath.substring('rules/'.length);
|
|
220
|
-
const filePath = (
|
|
292
|
+
const { filePath, relativePath } = resolveWorkspaceRegistryFile(options.projectRoot, rulesDir, relPath, 'rule file', assertWorkspacePath);
|
|
221
293
|
const fileDir = (0, path_1.dirname)(filePath);
|
|
222
294
|
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
223
295
|
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
224
296
|
}
|
|
225
297
|
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
226
|
-
console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`ai-employee/rules/${
|
|
298
|
+
console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`ai-employee/rules/${relativePath}`)} (stub)`));
|
|
227
299
|
}
|
|
228
300
|
// Sync scripts to user directory
|
|
229
301
|
const scriptFiles = files.filter(f => f.type === 'script');
|
|
@@ -233,36 +305,37 @@ async function syncFromRemote(options) {
|
|
|
233
305
|
(0, fs_1.mkdirSync)(scriptsDir, { recursive: true });
|
|
234
306
|
}
|
|
235
307
|
// Clean existing scripts
|
|
236
|
-
cleanDirectory(scriptsDir)
|
|
308
|
+
cleanDirectory(scriptsDir, (candidatePath) => {
|
|
309
|
+
if ((0, path_1.resolve)(candidatePath) !== (0, path_1.resolve)(scriptsDir)) {
|
|
310
|
+
assertPathInsideDirectory(scriptsDir, candidatePath, 'script directory');
|
|
311
|
+
}
|
|
312
|
+
});
|
|
237
313
|
// Write script files
|
|
238
314
|
for (const file of scriptFiles) {
|
|
239
|
-
const filePath = (
|
|
315
|
+
const { filePath, relativePath } = resolveUserRegistryFile(scriptsDir, file.path, 'script file');
|
|
240
316
|
const fileDir = (0, path_1.dirname)(filePath);
|
|
241
317
|
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
242
318
|
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
243
319
|
}
|
|
244
320
|
(0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
|
|
245
|
-
console.log(chalk_1.default.gray(` + ${
|
|
321
|
+
console.log(chalk_1.default.gray(` + ${relativePath}`));
|
|
246
322
|
}
|
|
247
323
|
// Sync docs to fraim/docs/
|
|
248
324
|
const docsFiles = files.filter(f => f.type === 'docs');
|
|
249
325
|
const docsDir = (0, project_fraim_paths_1.getWorkspaceFraimPath)(options.projectRoot, 'docs');
|
|
250
|
-
|
|
251
|
-
(0, fs_1.mkdirSync)(docsDir, { recursive: true });
|
|
252
|
-
}
|
|
253
|
-
cleanDirectory(docsDir);
|
|
326
|
+
cleanWorkspaceDirectory(options.projectRoot, docsDir, 'docs directory', assertWorkspacePath, 'docs');
|
|
254
327
|
for (const file of docsFiles) {
|
|
255
|
-
const filePath = (
|
|
328
|
+
const { filePath, relativePath } = resolveWorkspaceRegistryFile(options.projectRoot, docsDir, file.path, 'docs file', assertWorkspacePath);
|
|
256
329
|
const fileDir = (0, path_1.dirname)(filePath);
|
|
257
330
|
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
258
331
|
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
259
332
|
}
|
|
260
333
|
(0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
|
|
261
|
-
console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`docs/${
|
|
334
|
+
console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`docs/${relativePath}`)}`));
|
|
262
335
|
}
|
|
263
336
|
if (shouldLockSyncedContent()) {
|
|
264
337
|
for (const target of lockTargets) {
|
|
265
|
-
setFileWriteLockRecursively(target, true);
|
|
338
|
+
setFileWriteLockRecursively(target, true, assertWorkspacePath);
|
|
266
339
|
}
|
|
267
340
|
console.log(chalk_1.default.gray(` 🔒 Synced FRAIM content locked as read-only (set ${LOCK_SYNCED_CONTENT_ENV}=false to disable)`));
|
|
268
341
|
}
|
|
@@ -293,15 +366,18 @@ async function syncFromRemote(options) {
|
|
|
293
366
|
/**
|
|
294
367
|
* Clean directory contents (but keep the directory itself)
|
|
295
368
|
*/
|
|
296
|
-
function cleanDirectory(dirPath) {
|
|
369
|
+
function cleanDirectory(dirPath, assertSafePath = () => undefined) {
|
|
297
370
|
if (!(0, fs_1.existsSync)(dirPath))
|
|
298
371
|
return;
|
|
372
|
+
assertSafePath(dirPath);
|
|
299
373
|
const entries = (0, fs_1.readdirSync)(dirPath, { withFileTypes: true });
|
|
300
374
|
for (const entry of entries) {
|
|
301
375
|
const fullPath = (0, path_1.join)(dirPath, entry.name);
|
|
376
|
+
assertSafePath(fullPath);
|
|
302
377
|
if (entry.isDirectory()) {
|
|
303
|
-
cleanDirectory(fullPath);
|
|
378
|
+
cleanDirectory(fullPath, assertSafePath);
|
|
304
379
|
try {
|
|
380
|
+
assertSafePath(fullPath);
|
|
305
381
|
(0, fs_1.rmdirSync)(fullPath);
|
|
306
382
|
}
|
|
307
383
|
catch (e) {
|
|
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.readUserFraimConfig = readUserFraimConfig;
|
|
7
7
|
exports.writeUserFraimConfig = writeUserFraimConfig;
|
|
8
|
+
exports.getInstalledIdes = getInstalledIdes;
|
|
9
|
+
exports.addInstalledIde = addInstalledIde;
|
|
8
10
|
exports.getOrganizationConfig = getOrganizationConfig;
|
|
9
11
|
exports.getManagerStorageConfig = getManagerStorageConfig;
|
|
10
12
|
/**
|
|
@@ -48,6 +50,16 @@ function writeUserFraimConfig(patch) {
|
|
|
48
50
|
fs_1.default.writeFileSync(getUserConfigPath(), JSON.stringify(merged, null, 2));
|
|
49
51
|
return merged;
|
|
50
52
|
}
|
|
53
|
+
function getInstalledIdes() {
|
|
54
|
+
const raw = readUserFraimConfig().installedIdes;
|
|
55
|
+
return Array.isArray(raw) ? raw : null;
|
|
56
|
+
}
|
|
57
|
+
function addInstalledIde(configType) {
|
|
58
|
+
const current = getInstalledIdes() ?? [];
|
|
59
|
+
if (!current.includes(configType)) {
|
|
60
|
+
writeUserFraimConfig({ installedIdes: [...current, configType] });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
51
63
|
/**
|
|
52
64
|
* Resolve the validated organization configuration, or null when no valid
|
|
53
65
|
* organization is configured on this machine (R1.1).
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HUMAN_MANAGER_PROFILES = exports.AI_MANAGER_QUALITIES = exports.HIRING_SERVICES = void 0;
|
|
4
|
+
exports.getHumanManagerProfile = getHumanManagerProfile;
|
|
5
|
+
exports.buildSearchQuery = buildSearchQuery;
|
|
6
|
+
exports.buildJobDescription = buildJobDescription;
|
|
7
|
+
exports.buildServiceUrl = buildServiceUrl;
|
|
8
|
+
exports.buildManagerHiringCatalog = buildManagerHiringCatalog;
|
|
9
|
+
/**
|
|
10
|
+
* Issue #538 — human-manager hiring engine (app side).
|
|
11
|
+
*
|
|
12
|
+
* Self-contained on purpose: the app (AI Hub UI modal + bootstrap) must resolve this
|
|
13
|
+
* at runtime in every packaging context (dev tsx, compiled dist, packed CLI), so it
|
|
14
|
+
* must NOT import across the app→registry boundary. The `hire-the-right-human-ai-manager`
|
|
15
|
+
* job uses the mirror engine `registry/scripts/ai-manager-hiring.ts` (self-contained +
|
|
16
|
+
* CLI). The two are kept identical by a parity test (tests/isolated/test-538-ai-manager-hiring.ts);
|
|
17
|
+
* that is the DRY contract — one behavior, enforced, with no fragile cross-boundary import.
|
|
18
|
+
*/
|
|
19
|
+
const persona_hiring_1 = require("./persona-hiring");
|
|
20
|
+
exports.HIRING_SERVICES = ['linkedin', 'indeed', 'seekout'];
|
|
21
|
+
/** The qualities of a strong AI manager — embedded in both the query and the JD. */
|
|
22
|
+
exports.AI_MANAGER_QUALITIES = [
|
|
23
|
+
{ title: 'Clear specification & written communication', detail: 'Can brief an AI agent so it executes correctly the first time.' },
|
|
24
|
+
{ title: 'Trust-but-verify judgment', detail: 'Knows when to delegate to the agent and when to review deeply.' },
|
|
25
|
+
{ title: 'Coaching mindset', detail: 'Gives feedback that compounds into reusable rules and learnings.' },
|
|
26
|
+
{ title: 'Outcome ownership & AI fluency', detail: 'Owns results; comfortable directing autonomous agents.' },
|
|
27
|
+
];
|
|
28
|
+
const GENERALIST_PROFILE = {
|
|
29
|
+
humanTitle: 'Manager',
|
|
30
|
+
keywords: ['"Manager"', '"Director"', '"Team Lead"', '"Chief of Staff"'],
|
|
31
|
+
};
|
|
32
|
+
/** Role key -> the human manager best suited to manage that AI employee. Mirror of registry/scripts/ai-manager-hiring.ts. */
|
|
33
|
+
exports.HUMAN_MANAGER_PROFILES = {
|
|
34
|
+
maestro: { humanTitle: 'Co-Founder / General Manager', keywords: ['"Co-Founder"', '"General Manager"', '"Chief of Staff"', '"Founder"'] },
|
|
35
|
+
beza: { humanTitle: 'Head of Strategy', keywords: ['"Head of Strategy"', '"Strategy Director"', '"Chief of Staff"'] },
|
|
36
|
+
pam: { humanTitle: 'Head of Product', keywords: ['"Head of Product"', '"Group Product Manager"', '"Director of Product"'] },
|
|
37
|
+
swen: { humanTitle: 'Engineering Manager', keywords: ['"Engineering Manager"', '"Tech Lead"', '"Staff Software Engineer"'] },
|
|
38
|
+
qasm: { humanTitle: 'QA Manager', keywords: ['"QA Manager"', '"QA Lead"', '"Head of Quality"'] },
|
|
39
|
+
huxley: { humanTitle: 'Design Manager', keywords: ['"Design Manager"', '"Head of Design"', '"Design Lead"'] },
|
|
40
|
+
gautam: { humanTitle: 'Marketing Director', keywords: ['"Head of Growth"', '"Marketing Director"', '"Demand Generation Lead"'] },
|
|
41
|
+
cela: { humanTitle: 'General Counsel', keywords: ['"General Counsel"', '"Head of Legal"', '"Legal Director"'] },
|
|
42
|
+
sekhar: { humanTitle: 'Security Manager', keywords: ['"Security Manager"', '"Head of Security"', '"CISO"'] },
|
|
43
|
+
ashley: { humanTitle: 'Chief of Staff', keywords: ['"Chief of Staff"', '"Executive Operations Manager"', '"Office Manager"'] },
|
|
44
|
+
mandy: { humanTitle: 'Head of Operations', keywords: ['"Head of Operations"', '"Director of Operations"', '"Program Manager"'] },
|
|
45
|
+
ricardo: { humanTitle: 'Recruiting Manager', keywords: ['"Recruiting Manager"', '"Head of Talent"', '"Talent Acquisition Lead"'] },
|
|
46
|
+
hari: { humanTitle: 'HR Manager', keywords: ['"HR Manager"', '"Head of People"', '"HR Business Partner"'] },
|
|
47
|
+
careena: { humanTitle: 'Head of Talent Development', keywords: ['"Head of Talent Development"', '"L&D Manager"', '"Career Coaching Lead"'] },
|
|
48
|
+
sade: { humanTitle: 'Salesforce Manager', keywords: ['"Salesforce Manager"', '"CRM Lead"', '"Salesforce Architect"'] },
|
|
49
|
+
sam: { humanTitle: 'Sales Manager', keywords: ['"Sales Manager"', '"Head of Sales"', '"Director of Sales"'] },
|
|
50
|
+
casey: { humanTitle: 'Customer Success Manager', keywords: ['"Customer Success Manager"', '"Head of Customer Success"', '"Support Manager"'] },
|
|
51
|
+
deidre: { humanTitle: 'Head of DEI', keywords: ['"Head of DEI"', '"Director of Inclusion"', '"DEI Program Manager"'] },
|
|
52
|
+
mona: { humanTitle: 'Finance Manager', keywords: ['"Finance Manager"', '"Head of Finance"', '"Controller"'] },
|
|
53
|
+
sreya: { humanTitle: 'SRE Manager', keywords: ['"SRE Manager"', '"Head of Reliability"', '"Platform Engineering Lead"'] },
|
|
54
|
+
procella: { humanTitle: 'Procurement Manager', keywords: ['"Procurement Manager"', '"Head of Procurement"', '"Sourcing Lead"'] },
|
|
55
|
+
banke: { humanTitle: 'KYC Operations Manager', keywords: ['"KYC Operations Manager"', '"AML Compliance Manager"', '"Banking Compliance Lead"'] },
|
|
56
|
+
auditya: { humanTitle: 'Audit Manager', keywords: ['"Audit Manager"', '"Internal Audit Lead"', '"Compliance Audit Manager"'] },
|
|
57
|
+
};
|
|
58
|
+
function getHumanManagerProfile(roleKey) {
|
|
59
|
+
return exports.HUMAN_MANAGER_PROFILES[roleKey] ?? GENERALIST_PROFILE;
|
|
60
|
+
}
|
|
61
|
+
const AI_MANAGER_QUERY_TERMS = '("AI" OR "LLM" OR "agentic") AND ("coaching" OR "mentoring")';
|
|
62
|
+
function buildSearchQuery(roleKey) {
|
|
63
|
+
const profile = getHumanManagerProfile(roleKey);
|
|
64
|
+
return `${profile.keywords.join(' OR ')} AND ${AI_MANAGER_QUERY_TERMS}`;
|
|
65
|
+
}
|
|
66
|
+
function buildJobDescription(roleKey) {
|
|
67
|
+
const profile = getHumanManagerProfile(roleKey);
|
|
68
|
+
const role = persona_hiring_1.PERSONA_HIRE_CATALOG[roleKey]?.role ?? 'AI Employee';
|
|
69
|
+
return [
|
|
70
|
+
`${profile.humanTitle} — manager for an ${role}`,
|
|
71
|
+
'',
|
|
72
|
+
`You will manage an ${role} (an autonomous AI agent) and the humans around it, owning the outcomes it ships.`,
|
|
73
|
+
'',
|
|
74
|
+
"What you'll do:",
|
|
75
|
+
`- Write crisp specifications and acceptance criteria the ${role} can execute against.`,
|
|
76
|
+
`- Review the agent's output with judgment: know when to trust it and when to dig in.`,
|
|
77
|
+
'- Coach the agent and team so feedback compounds into reusable rules.',
|
|
78
|
+
'- Own delivery outcomes, not activity; prioritize ruthlessly.',
|
|
79
|
+
'',
|
|
80
|
+
'What makes a strong AI manager:',
|
|
81
|
+
...exports.AI_MANAGER_QUALITIES.map((q) => `- ${q.title}: ${q.detail}`),
|
|
82
|
+
].join('\n');
|
|
83
|
+
}
|
|
84
|
+
function buildServiceUrl(service, query, location) {
|
|
85
|
+
const q = encodeURIComponent(query);
|
|
86
|
+
const loc = location && location.trim().length > 0 ? location.trim() : '';
|
|
87
|
+
switch (service) {
|
|
88
|
+
case 'linkedin':
|
|
89
|
+
return `https://www.linkedin.com/search/results/people/?keywords=${q}`;
|
|
90
|
+
case 'indeed':
|
|
91
|
+
return `https://www.indeed.com/jobs?q=${q}${loc ? `&l=${encodeURIComponent(loc)}` : ''}`;
|
|
92
|
+
case 'seekout':
|
|
93
|
+
return 'https://app.seekout.com/';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Catalog projection for the AI Hub bootstrap so the client renders off the server's
|
|
98
|
+
* source of truth. Defaults to exactly the hireable personas in `PERSONA_HIRE_CATALOG`.
|
|
99
|
+
*/
|
|
100
|
+
function buildManagerHiringCatalog(roleKeys = Object.keys(persona_hiring_1.PERSONA_HIRE_CATALOG)) {
|
|
101
|
+
const roles = {};
|
|
102
|
+
for (const key of roleKeys) {
|
|
103
|
+
const profile = getHumanManagerProfile(key);
|
|
104
|
+
const query = buildSearchQuery(key);
|
|
105
|
+
// managedRole is derived from PERSONA_HIRE_CATALOG so the bootstrap response
|
|
106
|
+
// shape is unchanged even after removing the field from HumanManagerProfile.
|
|
107
|
+
const managedRole = persona_hiring_1.PERSONA_HIRE_CATALOG[key]?.role ?? 'AI Employee';
|
|
108
|
+
roles[key] = {
|
|
109
|
+
...profile,
|
|
110
|
+
managedRole,
|
|
111
|
+
query,
|
|
112
|
+
jobDescription: buildJobDescription(key),
|
|
113
|
+
serviceUrls: {
|
|
114
|
+
linkedin: buildServiceUrl('linkedin', query),
|
|
115
|
+
indeed: buildServiceUrl('indeed', query),
|
|
116
|
+
seekout: buildServiceUrl('seekout', query),
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return { qualities: exports.AI_MANAGER_QUALITIES, services: exports.HIRING_SERVICES, roles };
|
|
121
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Compatibility configuration for FRAIM.
|
|
4
|
+
* This file defines breaking changes and minimum required versions for clients.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.getBreakingChangesNotice = exports.MINIMUM_CLIENT_VERSION = void 0;
|
|
8
|
+
exports.MINIMUM_CLIENT_VERSION = '2.0.94';
|
|
9
|
+
/**
|
|
10
|
+
* Notice template for clients running outdated versions.
|
|
11
|
+
* This is appended to tool responses to guide users toward the standardized setup.
|
|
12
|
+
*/
|
|
13
|
+
const getBreakingChangesNotice = (currentVersion) => {
|
|
14
|
+
return `\n\n---\n\n> [!WARNING] migration_notice\n> **Action Required**: You are using an outdated FRAIM configuration (v${currentVersion}). FRAIM requires at least v${exports.MINIMUM_CLIENT_VERSION}.\n> \n> To fix this permanently so you never have to update again, please run this terminal command **once**:\n> \`\`\`bash\n> npx fraim@latest setup\n> \`\`\`\n> Then restart your IDE.`;
|
|
15
|
+
};
|
|
16
|
+
exports.getBreakingChangesNotice = getBreakingChangesNotice;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getFeatureFlags = getFeatureFlags;
|
|
4
|
+
exports.isPersonaEntitlementsEnabled = isPersonaEntitlementsEnabled;
|
|
5
|
+
exports.getPublicFeatureFlags = getPublicFeatureFlags;
|
|
6
|
+
function parseBooleanFlag(value) {
|
|
7
|
+
if (!value)
|
|
8
|
+
return false;
|
|
9
|
+
return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase());
|
|
10
|
+
}
|
|
11
|
+
function getFeatureFlags() {
|
|
12
|
+
return {
|
|
13
|
+
personaEntitlements: {
|
|
14
|
+
enabled: parseBooleanFlag(process.env.FRAIM_FF_PERSONA_ENTITLEMENTS)
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function isPersonaEntitlementsEnabled() {
|
|
19
|
+
return getFeatureFlags().personaEntitlements.enabled;
|
|
20
|
+
}
|
|
21
|
+
function getPublicFeatureFlags() {
|
|
22
|
+
return {
|
|
23
|
+
personaEntitlementsEnabled: isPersonaEntitlementsEnabled()
|
|
24
|
+
};
|
|
25
|
+
}
|