fraim-framework 2.0.152 → 2.0.154
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 +20 -4
- package/dist/src/ai-hub/server.js +40 -16
- package/dist/src/cli/commands/init-project.js +4 -2
- package/dist/src/cli/setup/ide-detector.js +46 -18
- package/dist/src/cli/utils/managed-agent-paths.js +48 -0
- package/dist/src/first-run/session-service.js +35 -48
- package/package.json +4 -2
- package/public/ai-hub/index.html +187 -179
- package/public/ai-hub/script.js +100 -45
- package/public/ai-hub/styles.css +112 -51
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -16,6 +16,7 @@ const child_process_1 = require("child_process");
|
|
|
16
16
|
const fs_1 = __importDefault(require("fs"));
|
|
17
17
|
const os_1 = __importDefault(require("os"));
|
|
18
18
|
const path_1 = __importDefault(require("path"));
|
|
19
|
+
const managed_agent_paths_1 = require("../cli/utils/managed-agent-paths");
|
|
19
20
|
// Parse a single line of host stdout looking for a seekMentoring tool-use
|
|
20
21
|
// signal. Returns null if the line does not contain one. Supports both
|
|
21
22
|
// hosts FRAIM ships against today:
|
|
@@ -239,6 +240,7 @@ const availableByVersionProbe = (command) => {
|
|
|
239
240
|
const invocation = resolveHostInvocation({ command, args: ['--version'] });
|
|
240
241
|
const result = (0, child_process_1.spawnSync)(invocation.command, invocation.args, {
|
|
241
242
|
encoding: 'utf8',
|
|
243
|
+
env: { ...process.env, PATH: (0, managed_agent_paths_1.buildPathWithManagedAgentBins)(process.env.PATH) },
|
|
242
244
|
});
|
|
243
245
|
return result.status === 0;
|
|
244
246
|
};
|
|
@@ -496,14 +498,14 @@ class FakeHostRuntime {
|
|
|
496
498
|
return this.employees;
|
|
497
499
|
}
|
|
498
500
|
startRun(hostId, _projectPath, message, handlers) {
|
|
499
|
-
return this.fakeProcess(hostId,
|
|
501
|
+
return this.fakeProcess(hostId, this.fakeEmployeeReply('start', message), handlers);
|
|
500
502
|
}
|
|
501
503
|
continueRun(hostId, _projectPath, sessionId, message, handlers) {
|
|
502
|
-
return this.fakeProcess(hostId,
|
|
504
|
+
return this.fakeProcess(hostId, this.fakeEmployeeReply('continue', message), handlers);
|
|
503
505
|
}
|
|
504
506
|
fakeProcess(_hostId, text, handlers) {
|
|
505
|
-
handlers.onEvent({ sessionId: (0, crypto_1.randomUUID)()
|
|
506
|
-
handlers.onEvent({ message: text
|
|
507
|
+
handlers.onEvent({ sessionId: (0, crypto_1.randomUUID)() }, 'system');
|
|
508
|
+
handlers.onEvent({ message: text }, 'stdout');
|
|
507
509
|
setTimeout(() => handlers.onExit(0), 25);
|
|
508
510
|
return {
|
|
509
511
|
stdout: process.stdout,
|
|
@@ -537,6 +539,20 @@ class FakeHostRuntime {
|
|
|
537
539
|
ref: () => undefined,
|
|
538
540
|
};
|
|
539
541
|
}
|
|
542
|
+
fakeEmployeeReply(kind, message) {
|
|
543
|
+
const parsed = parseFraimInvocation(message);
|
|
544
|
+
const label = parsed?.jobId
|
|
545
|
+
? parsed.jobId.split('-').map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' ')
|
|
546
|
+
: null;
|
|
547
|
+
if (kind === 'continue') {
|
|
548
|
+
return label
|
|
549
|
+
? `Understood. I'm taking another pass with ${label}.`
|
|
550
|
+
: 'Understood. I am incorporating your coaching now.';
|
|
551
|
+
}
|
|
552
|
+
return label
|
|
553
|
+
? `Understood. I'm starting ${label} now.`
|
|
554
|
+
: 'Understood. I am working on that now.';
|
|
555
|
+
}
|
|
540
556
|
}
|
|
541
557
|
exports.FakeHostRuntime = FakeHostRuntime;
|
|
542
558
|
// Issue #347 — test-only host that lets a test inject seekMentoring
|
|
@@ -41,6 +41,7 @@ const catalog_1 = require("./catalog");
|
|
|
41
41
|
const agent_token_prices_1 = require("../local-mcp-server/agent-token-prices");
|
|
42
42
|
const hosts_1 = require("./hosts");
|
|
43
43
|
const preferences_1 = require("./preferences");
|
|
44
|
+
const managed_agent_paths_1 = require("../cli/utils/managed-agent-paths");
|
|
44
45
|
function loadPersonaCapabilityModule() {
|
|
45
46
|
try {
|
|
46
47
|
// Server deployments include the persona catalog. The npm client package
|
|
@@ -319,12 +320,18 @@ function hubAgentOption(hubId) {
|
|
|
319
320
|
const frId = HUB_TO_FIRST_RUN_ID[hubId];
|
|
320
321
|
return frId ? types_1.FIRST_RUN_AGENT_OPTIONS.find((o) => o.id === frId) : undefined;
|
|
321
322
|
}
|
|
322
|
-
function hubCommandVersion(command) {
|
|
323
|
+
function hubCommandVersion(command, extraBinDirs) {
|
|
323
324
|
const executable = process.platform === 'win32' ? 'cmd.exe' : command;
|
|
324
325
|
const args = process.platform === 'win32'
|
|
325
326
|
? ['/d', '/s', '/c', `${command} --version`]
|
|
326
327
|
: ['--version'];
|
|
327
|
-
const
|
|
328
|
+
const env = extraBinDirs && extraBinDirs.length > 0
|
|
329
|
+
? {
|
|
330
|
+
...process.env,
|
|
331
|
+
PATH: [...new Set([...extraBinDirs, ...(process.env.PATH || '').split(path_1.default.delimiter).filter(Boolean)])].join(path_1.default.delimiter),
|
|
332
|
+
}
|
|
333
|
+
: undefined;
|
|
334
|
+
const result = (0, child_process_1.spawnSync)(executable, args, { encoding: 'utf8', timeout: 5000, ...(env ? { env } : {}) });
|
|
328
335
|
if (result.status !== 0 || result.error)
|
|
329
336
|
return null;
|
|
330
337
|
const raw = (result.stdout || result.stderr || '').trim();
|
|
@@ -376,6 +383,13 @@ function hubOpenTerminal(command) {
|
|
|
376
383
|
}
|
|
377
384
|
(0, child_process_1.spawn)('bash', ['-c', command], { detached: true, stdio: 'ignore' }).unref();
|
|
378
385
|
}
|
|
386
|
+
function buildManagedLoginCommand(command) {
|
|
387
|
+
const managedPath = (0, managed_agent_paths_1.buildPathWithManagedAgentBins)(process.env.PATH);
|
|
388
|
+
if (process.platform === 'win32') {
|
|
389
|
+
return `set "PATH=${managedPath}" && ${command}`;
|
|
390
|
+
}
|
|
391
|
+
return `export PATH="${managedPath}"; ${command}`;
|
|
392
|
+
}
|
|
379
393
|
function getUserHubDir() {
|
|
380
394
|
return path_1.default.join(os_1.default.homedir(), '.fraim');
|
|
381
395
|
}
|
|
@@ -400,9 +414,11 @@ class AiHubServer {
|
|
|
400
414
|
this.hostRuntime = options.hostRuntime || (process.env.FRAIM_AI_HUB_FAKE_HOST === '1' ? new hosts_1.FakeHostRuntime() : new hosts_1.CliHostRuntime());
|
|
401
415
|
if (options.dbService !== undefined) {
|
|
402
416
|
this.dbService = options.dbService;
|
|
417
|
+
this.ownsDbService = false;
|
|
403
418
|
}
|
|
404
419
|
else {
|
|
405
420
|
this.dbService = createDefaultDbService();
|
|
421
|
+
this.ownsDbService = this.dbService !== undefined;
|
|
406
422
|
}
|
|
407
423
|
this.app.use(express_1.default.json());
|
|
408
424
|
this.app.use('/ai-hub', express_1.default.static(resolveAiHubPublicDir()));
|
|
@@ -431,18 +447,22 @@ class AiHubServer {
|
|
|
431
447
|
});
|
|
432
448
|
}
|
|
433
449
|
async stop() {
|
|
434
|
-
if (
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
450
|
+
if (this.httpServer) {
|
|
451
|
+
await new Promise((resolve, reject) => {
|
|
452
|
+
this.httpServer.close((error) => {
|
|
453
|
+
if (error) {
|
|
454
|
+
reject(error);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
resolve();
|
|
458
|
+
});
|
|
443
459
|
});
|
|
444
|
-
|
|
445
|
-
|
|
460
|
+
this.httpServer = undefined;
|
|
461
|
+
}
|
|
462
|
+
if (this.ownsDbService && this.dbService) {
|
|
463
|
+
await this.dbService.close();
|
|
464
|
+
this.dbService = undefined;
|
|
465
|
+
}
|
|
446
466
|
}
|
|
447
467
|
async bootstrapResponse(projectPath, apiKey) {
|
|
448
468
|
const normalizedProjectPath = path_1.default.resolve(projectPath || this.projectPath);
|
|
@@ -563,9 +583,13 @@ class AiHubServer {
|
|
|
563
583
|
if (!option)
|
|
564
584
|
return res.status(400).json({ error: `Unknown agent: ${hubId}` });
|
|
565
585
|
try {
|
|
566
|
-
const prefix =
|
|
586
|
+
const prefix = (0, managed_agent_paths_1.getManagedNodeRoot)();
|
|
567
587
|
fs_1.default.mkdirSync(prefix, { recursive: true });
|
|
568
588
|
await hubRunProcess('npm', ['install', '-g', option.installPackage], { npm_config_prefix: prefix });
|
|
589
|
+
const ver = hubCommandVersion(option.launchCommand, (0, managed_agent_paths_1.getManagedAgentBinDirs)());
|
|
590
|
+
if (!ver) {
|
|
591
|
+
throw new Error(`${option.label} install completed, but the CLI is not runnable from FRAIM's managed PATH.`);
|
|
592
|
+
}
|
|
569
593
|
return res.json({
|
|
570
594
|
ok: true,
|
|
571
595
|
message: `${option.label} installed successfully.`,
|
|
@@ -587,7 +611,7 @@ class AiHubServer {
|
|
|
587
611
|
if (!option)
|
|
588
612
|
return res.status(400).json({ error: `Unknown agent: ${hubId}` });
|
|
589
613
|
try {
|
|
590
|
-
hubOpenTerminal(option.loginCommand);
|
|
614
|
+
hubOpenTerminal(buildManagedLoginCommand(option.loginCommand));
|
|
591
615
|
return res.json({
|
|
592
616
|
ok: true,
|
|
593
617
|
message: `A terminal window opened with the ${option.label} sign-in command. Complete sign-in there, then return here.`,
|
|
@@ -608,7 +632,7 @@ class AiHubServer {
|
|
|
608
632
|
const option = hubAgentOption(hubId);
|
|
609
633
|
if (!option)
|
|
610
634
|
return res.status(400).json({ error: `Unknown agent: ${hubId}` });
|
|
611
|
-
const ver = hubCommandVersion(option.launchCommand);
|
|
635
|
+
const ver = hubCommandVersion(option.launchCommand, (0, managed_agent_paths_1.getManagedAgentBinDirs)());
|
|
612
636
|
if (ver) {
|
|
613
637
|
return res.json({ ok: true, ready: true, message: `${option.label} is ready.` });
|
|
614
638
|
}
|
|
@@ -256,14 +256,16 @@ const runInitProject = async (options = {}) => {
|
|
|
256
256
|
else {
|
|
257
257
|
result.warnings.push('Sync was skipped for this run.');
|
|
258
258
|
}
|
|
259
|
-
const codexAvailable = (0, ide_detector_1.detectInstalledIDEs)().some((ide) => ide.configType === 'codex')
|
|
259
|
+
const codexAvailable = (0, ide_detector_1.detectInstalledIDEs)().some((ide) => ide.configType === 'codex') ||
|
|
260
|
+
(0, ide_detector_1.detectInstalledIDEs)('cli-runnable').some((ide) => ide.configType === 'codex');
|
|
260
261
|
if (codexAvailable) {
|
|
261
262
|
const codexLocalResult = (0, codex_local_config_1.ensureCodexLocalConfig)(projectRoot);
|
|
262
263
|
const status = codexLocalResult.created ? 'Created' : codexLocalResult.updated ? 'Updated' : 'Verified';
|
|
263
264
|
console.log(chalk_1.default.green(`${status} project Codex config at ${codexLocalResult.path}`));
|
|
264
265
|
}
|
|
265
266
|
// Enable token telemetry for Claude Code (user-level, applies to all projects)
|
|
266
|
-
const claudeCodeAvailable = (0, ide_detector_1.detectInstalledIDEs)().some((ide) => ide.configType === 'claude-code')
|
|
267
|
+
const claudeCodeAvailable = (0, ide_detector_1.detectInstalledIDEs)().some((ide) => ide.configType === 'claude-code') ||
|
|
268
|
+
(0, ide_detector_1.detectInstalledIDEs)('cli-runnable').some((ide) => ide.configType === 'claude-code');
|
|
267
269
|
if (claudeCodeAvailable) {
|
|
268
270
|
(0, claude_code_telemetry_1.ensureClaudeCodeTelemetryEnv)();
|
|
269
271
|
}
|
|
@@ -62,11 +62,23 @@ const detectWindsurf = () => {
|
|
|
62
62
|
return checkMultiplePaths(paths);
|
|
63
63
|
};
|
|
64
64
|
const detectGeminiCli = () => {
|
|
65
|
-
// Require the binary to be in PATH. The ~/.gemini directory alone is not
|
|
66
|
-
// sufficient — it exists when only Antigravity or other Gemini-adjacent
|
|
67
|
-
// tools are installed, which would produce a false positive.
|
|
68
65
|
return availableByVersionProbe('gemini');
|
|
69
66
|
};
|
|
67
|
+
const detectGeminiSurface = () => {
|
|
68
|
+
const paths = [
|
|
69
|
+
'~/.gemini',
|
|
70
|
+
'~/AppData/Roaming/gemini',
|
|
71
|
+
'~/.config/gemini'
|
|
72
|
+
];
|
|
73
|
+
return checkMultiplePaths(paths);
|
|
74
|
+
};
|
|
75
|
+
const detectCodexSurface = () => {
|
|
76
|
+
const paths = [
|
|
77
|
+
'~/.codex',
|
|
78
|
+
'~/.codex/config.toml'
|
|
79
|
+
];
|
|
80
|
+
return checkMultiplePaths(paths);
|
|
81
|
+
};
|
|
70
82
|
exports.IDE_CONFIGS = [
|
|
71
83
|
{
|
|
72
84
|
name: 'Claude Code',
|
|
@@ -109,7 +121,7 @@ exports.IDE_CONFIGS = [
|
|
|
109
121
|
configFormat: 'json',
|
|
110
122
|
configType: 'gemini-cli',
|
|
111
123
|
invocationProfile: 'gemini-command',
|
|
112
|
-
detectMethod:
|
|
124
|
+
detectMethod: detectGeminiSurface,
|
|
113
125
|
supportsConfigBootstrap: true,
|
|
114
126
|
aliases: ['gemini', 'gemini-cli', 'gemini cli'],
|
|
115
127
|
alternativePaths: [
|
|
@@ -165,7 +177,7 @@ exports.IDE_CONFIGS = [
|
|
|
165
177
|
configFormat: 'toml',
|
|
166
178
|
configType: 'codex',
|
|
167
179
|
invocationProfile: 'codex-skill',
|
|
168
|
-
detectMethod:
|
|
180
|
+
detectMethod: detectCodexSurface,
|
|
169
181
|
description: 'Codex AI development environment'
|
|
170
182
|
},
|
|
171
183
|
{
|
|
@@ -184,11 +196,9 @@ exports.IDE_CONFIGS = [
|
|
|
184
196
|
}
|
|
185
197
|
];
|
|
186
198
|
const findBestConfigPath = (ide) => {
|
|
187
|
-
// First try the default path
|
|
188
199
|
if (fs_1.default.existsSync(expandPath(ide.configPath))) {
|
|
189
200
|
return ide.configPath;
|
|
190
201
|
}
|
|
191
|
-
// Then try alternative paths
|
|
192
202
|
if (ide.alternativePaths) {
|
|
193
203
|
for (const altPath of ide.alternativePaths) {
|
|
194
204
|
if (fs_1.default.existsSync(expandPath(altPath))) {
|
|
@@ -196,26 +206,44 @@ const findBestConfigPath = (ide) => {
|
|
|
196
206
|
}
|
|
197
207
|
}
|
|
198
208
|
}
|
|
199
|
-
// Return default path if nothing found (will be created)
|
|
200
209
|
return ide.configPath;
|
|
201
210
|
};
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
211
|
+
const _cachedIDEs = new Map();
|
|
212
|
+
const _cacheTimestamps = new Map();
|
|
213
|
+
const _cacheHomeDirs = new Map();
|
|
205
214
|
const DETECT_CACHE_TTL_MS = 5000;
|
|
206
|
-
const
|
|
215
|
+
const isDetectedForMode = (ide, mode) => {
|
|
216
|
+
if (mode === 'cli-runnable') {
|
|
217
|
+
switch (ide.configType) {
|
|
218
|
+
case 'claude-code':
|
|
219
|
+
return availableByVersionProbe('claude');
|
|
220
|
+
case 'codex':
|
|
221
|
+
return availableByVersionProbe('codex');
|
|
222
|
+
case 'gemini-cli':
|
|
223
|
+
return detectGeminiCli();
|
|
224
|
+
default:
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return ide.detectMethod();
|
|
229
|
+
};
|
|
230
|
+
const detectInstalledIDEs = (mode = 'config-surface') => {
|
|
207
231
|
const now = Date.now();
|
|
208
232
|
const currentHome = os_1.default.homedir();
|
|
209
|
-
|
|
210
|
-
|
|
233
|
+
const cached = _cachedIDEs.get(mode);
|
|
234
|
+
const cacheTimestamp = _cacheTimestamps.get(mode) || 0;
|
|
235
|
+
const cacheHomeDir = _cacheHomeDirs.get(mode) || '';
|
|
236
|
+
if (cached !== undefined && cacheHomeDir === currentHome && (now - cacheTimestamp) < DETECT_CACHE_TTL_MS) {
|
|
237
|
+
return cached;
|
|
211
238
|
}
|
|
212
|
-
|
|
239
|
+
const detected = exports.IDE_CONFIGS.filter((ide) => isDetectedForMode(ide, mode)).map(ide => ({
|
|
213
240
|
...ide,
|
|
214
241
|
configPath: findBestConfigPath(ide)
|
|
215
242
|
}));
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
243
|
+
_cachedIDEs.set(mode, detected);
|
|
244
|
+
_cacheTimestamps.set(mode, now);
|
|
245
|
+
_cacheHomeDirs.set(mode, currentHome);
|
|
246
|
+
return detected;
|
|
219
247
|
};
|
|
220
248
|
exports.detectInstalledIDEs = detectInstalledIDEs;
|
|
221
249
|
const getAllSupportedIDEs = () => {
|
|
@@ -0,0 +1,48 @@
|
|
|
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.getManagedNodeRoot = getManagedNodeRoot;
|
|
7
|
+
exports.getPortableNodeBinPath = getPortableNodeBinPath;
|
|
8
|
+
exports.getManagedAgentBinDirs = getManagedAgentBinDirs;
|
|
9
|
+
exports.buildPathWithManagedAgentBins = buildPathWithManagedAgentBins;
|
|
10
|
+
exports.prependManagedAgentBinDirsToProcessPath = prependManagedAgentBinDirsToProcessPath;
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const script_sync_utils_1 = require("./script-sync-utils");
|
|
14
|
+
function getManagedNodeRoot() {
|
|
15
|
+
return path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'node');
|
|
16
|
+
}
|
|
17
|
+
function getPortableNodeBinPath() {
|
|
18
|
+
const nodeRoot = getManagedNodeRoot();
|
|
19
|
+
if (process.platform === 'win32') {
|
|
20
|
+
if (fs_1.default.existsSync(nodeRoot)) {
|
|
21
|
+
const extractedDir = fs_1.default.readdirSync(nodeRoot, { withFileTypes: true })
|
|
22
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith('node-v'))
|
|
23
|
+
.sort((a, b) => b.name.localeCompare(a.name))[0];
|
|
24
|
+
if (extractedDir) {
|
|
25
|
+
return path_1.default.join(nodeRoot, extractedDir.name);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return nodeRoot;
|
|
29
|
+
}
|
|
30
|
+
return path_1.default.join(nodeRoot, 'bin');
|
|
31
|
+
}
|
|
32
|
+
function getManagedAgentBinDirs() {
|
|
33
|
+
const nodeRoot = getManagedNodeRoot();
|
|
34
|
+
const portableNodeBin = getPortableNodeBinPath();
|
|
35
|
+
const candidates = process.platform === 'win32'
|
|
36
|
+
? [nodeRoot, portableNodeBin]
|
|
37
|
+
: [path_1.default.join(nodeRoot, 'bin'), portableNodeBin];
|
|
38
|
+
return [...new Set(candidates.filter(Boolean))];
|
|
39
|
+
}
|
|
40
|
+
function buildPathWithManagedAgentBins(basePath) {
|
|
41
|
+
const current = basePath ?? process.env.PATH ?? '';
|
|
42
|
+
const existing = current.split(path_1.default.delimiter).filter(Boolean);
|
|
43
|
+
const merged = [...getManagedAgentBinDirs(), ...existing];
|
|
44
|
+
return [...new Set(merged)].join(path_1.default.delimiter);
|
|
45
|
+
}
|
|
46
|
+
function prependManagedAgentBinDirsToProcessPath() {
|
|
47
|
+
process.env.PATH = buildPathWithManagedAgentBins(process.env.PATH);
|
|
48
|
+
}
|
|
@@ -47,6 +47,7 @@ const ide_global_integration_1 = require("../cli/setup/ide-global-integration");
|
|
|
47
47
|
const auto_mcp_setup_1 = require("../cli/setup/auto-mcp-setup");
|
|
48
48
|
const setup_1 = require("../cli/commands/setup");
|
|
49
49
|
const script_sync_utils_1 = require("../cli/utils/script-sync-utils");
|
|
50
|
+
const managed_agent_paths_1 = require("../cli/utils/managed-agent-paths");
|
|
50
51
|
const types_1 = require("./types");
|
|
51
52
|
Object.defineProperty(exports, "FIRST_RUN_ROW_IDS", { enumerable: true, get: function () { return types_1.FIRST_RUN_ROW_IDS; } });
|
|
52
53
|
const install_state_1 = require("./install-state");
|
|
@@ -59,13 +60,16 @@ function getFakeStateMode() {
|
|
|
59
60
|
}
|
|
60
61
|
return 'default';
|
|
61
62
|
}
|
|
62
|
-
function commandVersion(command,
|
|
63
|
+
function commandVersion(command, extraBinDirs) {
|
|
63
64
|
const executable = process.platform === 'win32' ? 'cmd.exe' : command;
|
|
64
65
|
const args = process.platform === 'win32'
|
|
65
66
|
? ['/d', '/s', '/c', `${command} --version`]
|
|
66
67
|
: ['--version'];
|
|
67
|
-
const env =
|
|
68
|
-
? {
|
|
68
|
+
const env = extraBinDirs && extraBinDirs.length > 0
|
|
69
|
+
? {
|
|
70
|
+
...process.env,
|
|
71
|
+
PATH: [...new Set([...extraBinDirs, ...(process.env.PATH || '').split(path_1.default.delimiter).filter(Boolean)])].join(path_1.default.delimiter),
|
|
72
|
+
}
|
|
69
73
|
: undefined;
|
|
70
74
|
const result = (0, child_process_1.spawnSync)(executable, args, {
|
|
71
75
|
encoding: 'utf8',
|
|
@@ -81,51 +85,25 @@ function ensureOutputDirs() {
|
|
|
81
85
|
fs_1.default.mkdirSync((0, script_sync_utils_1.getUserFraimDir)(), { recursive: true });
|
|
82
86
|
fs_1.default.mkdirSync(path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'last-install'), { recursive: true });
|
|
83
87
|
}
|
|
84
|
-
/**
|
|
85
|
-
* Returns the directory that contains node/npm/npx executables for FRAIM's
|
|
86
|
-
* portable Node installation.
|
|
87
|
-
*
|
|
88
|
-
* - Mac/Linux: ~/.fraim/node/bin (standard Unix layout)
|
|
89
|
-
* - Windows: ~/.fraim/node/node-v<version>-win-x64/ if extracted, else
|
|
90
|
-
* ~/.fraim/node/ as fallback (executables live at the root on Windows)
|
|
91
|
-
*/
|
|
92
|
-
function getFraimNodeBinPath() {
|
|
93
|
-
const nodeRoot = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'node');
|
|
94
|
-
if (process.platform === 'win32') {
|
|
95
|
-
if (fs_1.default.existsSync(nodeRoot)) {
|
|
96
|
-
const extractedDir = fs_1.default.readdirSync(nodeRoot, { withFileTypes: true })
|
|
97
|
-
.filter((entry) => entry.isDirectory() && entry.name.startsWith('node-v'))
|
|
98
|
-
.sort((a, b) => b.name.localeCompare(a.name))[0];
|
|
99
|
-
if (extractedDir) {
|
|
100
|
-
return path_1.default.join(nodeRoot, extractedDir.name);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return nodeRoot;
|
|
104
|
-
}
|
|
105
|
-
return path_1.default.join(nodeRoot, 'bin');
|
|
106
|
-
}
|
|
107
88
|
// Prepend the portable Node bin dir to process PATH once at module load so
|
|
108
89
|
// every spawnSync call (detection, login probe, change-agent) finds binaries
|
|
109
90
|
// installed there without needing per-call path overrides.
|
|
110
91
|
(function bootstrapFraimNodeBin() {
|
|
111
|
-
|
|
112
|
-
const current = process.env.PATH || '';
|
|
113
|
-
if (!current.split(path_1.default.delimiter).includes(fraimNodeBin)) {
|
|
114
|
-
process.env.PATH = `${fraimNodeBin}${path_1.default.delimiter}${current}`;
|
|
115
|
-
}
|
|
92
|
+
(0, managed_agent_paths_1.prependManagedAgentBinDirsToProcessPath)();
|
|
116
93
|
})();
|
|
117
94
|
function persistShellPath() {
|
|
118
|
-
const fraimNodeBin = getFraimNodeBinPath();
|
|
119
95
|
const marker = '# FRAIM managed binaries';
|
|
120
96
|
const exportLine = 'export PATH="$HOME/.fraim/node/bin:$PATH"';
|
|
121
97
|
const stanza = `\n${marker}\n${exportLine}\n`;
|
|
122
98
|
if (process.platform === 'win32') {
|
|
123
|
-
const
|
|
124
|
-
|
|
99
|
+
const bins = (0, managed_agent_paths_1.getManagedAgentBinDirs)();
|
|
100
|
+
const assignments = bins.map((entry, index) => `$bin${index} = '${entry.replace(/'/g, "''")}'`);
|
|
101
|
+
const updates = bins.map((_, index) => `if ($cur -notlike "*$bin${index}*") { $cur = "$bin${index};$cur" }`);
|
|
125
102
|
const psCmd = [
|
|
126
|
-
|
|
103
|
+
...assignments,
|
|
127
104
|
`$cur = [Environment]::GetEnvironmentVariable('PATH', 'User')`,
|
|
128
|
-
|
|
105
|
+
...updates,
|
|
106
|
+
`[Environment]::SetEnvironmentVariable('PATH', $cur, 'User')`,
|
|
129
107
|
].join('; ');
|
|
130
108
|
(0, child_process_1.spawnSync)('powershell', ['-NoProfile', '-NonInteractive', '-Command', psCmd], { encoding: 'utf8' });
|
|
131
109
|
return;
|
|
@@ -165,8 +143,10 @@ function appendInstallLog(line) {
|
|
|
165
143
|
}
|
|
166
144
|
function runProcess(command, args, env) {
|
|
167
145
|
return new Promise((resolve, reject) => {
|
|
168
|
-
const
|
|
169
|
-
|
|
146
|
+
const [realCmd, realArgs] = process.platform === 'win32'
|
|
147
|
+
? ['cmd.exe', ['/d', '/s', '/c', command, ...args]]
|
|
148
|
+
: [command, args];
|
|
149
|
+
const child = (0, child_process_1.spawn)(realCmd, realArgs, {
|
|
170
150
|
env: { ...process.env, ...(env || {}) },
|
|
171
151
|
shell: false,
|
|
172
152
|
});
|
|
@@ -650,6 +630,10 @@ class FirstRunSessionService {
|
|
|
650
630
|
const prefix = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'node');
|
|
651
631
|
fs_1.default.mkdirSync(prefix, { recursive: true });
|
|
652
632
|
await runProcess('npm', ['install', '-g', option.installPackage], { npm_config_prefix: prefix });
|
|
633
|
+
const ver = commandVersion(option.launchCommand, (0, managed_agent_paths_1.getManagedAgentBinDirs)());
|
|
634
|
+
if (!ver) {
|
|
635
|
+
throw new Error(`${option.label} install completed, but the CLI is not runnable from FRAIM's managed PATH.`);
|
|
636
|
+
}
|
|
653
637
|
const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
|
|
654
638
|
if (detectedIDEs.length > 0) {
|
|
655
639
|
await (0, auto_mcp_setup_1.autoConfigureMCP)(this.key, {}, detectedIDEs.map((ide) => ide.name), {});
|
|
@@ -678,7 +662,7 @@ class FirstRunSessionService {
|
|
|
678
662
|
return { ok: true, message: `${option.label} login triggered (fake-mode).` };
|
|
679
663
|
}
|
|
680
664
|
try {
|
|
681
|
-
this.openTerminalWithCommand(option.loginCommand);
|
|
665
|
+
this.openTerminalWithCommand(this.buildManagedLoginCommand(option.loginCommand));
|
|
682
666
|
appendInstallLog(`agent-login-triggered ${agentId}`);
|
|
683
667
|
return {
|
|
684
668
|
ok: true,
|
|
@@ -701,7 +685,7 @@ class FirstRunSessionService {
|
|
|
701
685
|
if (this.fakeMode) {
|
|
702
686
|
return { ok: true, ready: true, message: `${option.label} is ready (fake-mode).` };
|
|
703
687
|
}
|
|
704
|
-
const ver = commandVersion(option.launchCommand);
|
|
688
|
+
const ver = commandVersion(option.launchCommand, (0, managed_agent_paths_1.getManagedAgentBinDirs)());
|
|
705
689
|
if (ver) {
|
|
706
690
|
this.updateAgentSummaryRow();
|
|
707
691
|
this.persist();
|
|
@@ -736,20 +720,23 @@ class FirstRunSessionService {
|
|
|
736
720
|
}
|
|
737
721
|
}
|
|
738
722
|
}
|
|
723
|
+
buildManagedLoginCommand(command) {
|
|
724
|
+
const managedPath = (0, managed_agent_paths_1.buildPathWithManagedAgentBins)(process.env.PATH);
|
|
725
|
+
if (process.platform === 'win32') {
|
|
726
|
+
return `set "PATH=${managedPath}" && ${command}`;
|
|
727
|
+
}
|
|
728
|
+
return `export PATH="${managedPath}"; ${command}`;
|
|
729
|
+
}
|
|
739
730
|
async openHub() {
|
|
740
731
|
if (this.fakeMode) {
|
|
741
732
|
// Tests don't actually want a Hub server running — just confirm intent.
|
|
742
733
|
return { ok: true, message: 'Fake-mode Hub open requested.', hubUrl: 'http://127.0.0.1:0/ai-hub/?firstRun=true' };
|
|
743
734
|
}
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
// their IDE config files exist (created on first run / login).
|
|
735
|
+
// Hub launches a real CLI process, so folder-only config surfaces are not
|
|
736
|
+
// enough here. Require a runnable command in the managed or ambient PATH.
|
|
747
737
|
const hubCompatibleBinaries = ['claude', 'codex'];
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
const hasConfiguredCli = surfaces.some((s) => hubCompatibleIds.has(s.id));
|
|
751
|
-
const hasInstalledCli = hubCompatibleBinaries.some((cmd) => commandVersion(cmd) !== null);
|
|
752
|
-
if (!hasConfiguredCli && !hasInstalledCli) {
|
|
738
|
+
const hasInstalledCli = hubCompatibleBinaries.some((cmd) => commandVersion(cmd, (0, managed_agent_paths_1.getManagedAgentBinDirs)()) !== null);
|
|
739
|
+
if (!hasInstalledCli) {
|
|
753
740
|
return {
|
|
754
741
|
ok: false,
|
|
755
742
|
needsAgentSetup: true,
|
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-framework",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.154",
|
|
4
4
|
"description": "FRAIM: AI Workforce Infrastructure — the organizational capability that turns AI agents into an accountable workforce, their operators into capable AI managers, and executives into leaders with clear optics on AI proficiency.",
|
|
5
5
|
"main": "index.js",
|
|
6
|
-
"bin":
|
|
6
|
+
"bin": {
|
|
7
|
+
"fraim-framework": "bin/fraim.js"
|
|
8
|
+
},
|
|
7
9
|
"scripts": {
|
|
8
10
|
"dev": "tsx --watch src/fraim-mcp-server.ts > server.log 2>&1",
|
|
9
11
|
"dev:prod": "npm run build && node dist/src/fraim-mcp-server.js > server.log 2>&1",
|