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.
@@ -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, `Started ${hostId}: ${message}`, handlers);
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, `Resumed ${hostId} session ${sessionId}: ${message}`, handlers);
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)(), raw: 'fake-session-start' }, 'system');
506
- handlers.onEvent({ message: text, raw: text }, 'stdout');
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 result = (0, child_process_1.spawnSync)(executable, args, { encoding: 'utf8', timeout: 5000 });
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 (!this.httpServer)
435
- return;
436
- await new Promise((resolve, reject) => {
437
- this.httpServer.close((error) => {
438
- if (error) {
439
- reject(error);
440
- return;
441
- }
442
- resolve();
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
- this.httpServer = undefined;
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 = path_1.default.join(getUserHubDir(), 'node');
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: detectGeminiCli,
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: () => availableByVersionProbe('codex'),
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
- let _cachedIDEs = null;
203
- let _cacheTimestamp = 0;
204
- let _cacheHomeDir = '';
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 detectInstalledIDEs = () => {
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
- if (_cachedIDEs !== null && _cacheHomeDir === currentHome && (now - _cacheTimestamp) < DETECT_CACHE_TTL_MS) {
210
- return _cachedIDEs;
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
- _cachedIDEs = exports.IDE_CONFIGS.filter(ide => ide.detectMethod()).map(ide => ({
239
+ const detected = exports.IDE_CONFIGS.filter((ide) => isDetectedForMode(ide, mode)).map(ide => ({
213
240
  ...ide,
214
241
  configPath: findBestConfigPath(ide)
215
242
  }));
216
- _cacheTimestamp = now;
217
- _cacheHomeDir = currentHome;
218
- return _cachedIDEs;
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, extraBinDir) {
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 = extraBinDir
68
- ? { ...process.env, PATH: `${extraBinDir}${path_1.default.delimiter}${process.env.PATH || ''}` }
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
- const fraimNodeBin = getFraimNodeBinPath();
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 escapedBin = fraimNodeBin.replace(/'/g, "''");
124
- // Guard pattern matches both the versioned subdirectory and the root fallback.
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
- `$bin = '${escapedBin}'`,
103
+ ...assignments,
127
104
  `$cur = [Environment]::GetEnvironmentVariable('PATH', 'User')`,
128
- `if ($cur -notlike "*$bin*") { [Environment]::SetEnvironmentVariable('PATH', "$bin;$cur", 'User') }`,
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 executable = process.platform === 'win32' && command === 'npm' ? 'npm.cmd' : command;
169
- const child = (0, child_process_1.spawn)(executable, args, {
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
- // Require at least one Hub-compatible CLI to be installed (binary on PATH)
745
- // OR fully IDE-configured. Accept freshly-installed binaries even before
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 hubCompatibleIds = new Set(['claude-code', 'codex']);
749
- const surfaces = buildConfiguredSurfaces();
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.152",
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": "./bin/fraim.js",
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",