fraim-hub 2.0.301 → 2.0.302
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/README.md +1 -1
- package/dist/src/ai-hub/conversation-store.js +7 -0
- package/dist/src/ai-hub/hosts.js +34 -9
- package/dist/src/ai-hub/server.js +154 -17
- package/dist/src/cli/doctor/checks/agent-cli-health-checks.js +26 -2
- package/dist/src/cli/setup/ide-detector.js +74 -4
- package/dist/src/cli/utils/managed-agent-paths.js +183 -1
- package/dist/src/config/persona-capability-bundles.js +2 -2
- package/dist/src/first-run/session-service.js +37 -7
- package/package.json +2 -2
- package/public/ai-hub/script.js +130 -5
- package/public/ai-hub/styles.css +17 -0
- package/public/first-run/script.js +19 -51
package/README.md
CHANGED
|
@@ -312,7 +312,7 @@ Use these defaults:
|
|
|
312
312
|
- `feature-specification` when the request is still fuzzy or needs clarified requirements, UX, or acceptance criteria.
|
|
313
313
|
- `technical-design` after the spec is approved and you need the implementation plan, file touchpoints, and risk handling.
|
|
314
314
|
- `feature-implementation` for code changes, bug fixes, and documentation updates that should be executed and validated.
|
|
315
|
-
- `test-
|
|
315
|
+
- `test-authoring` when you need reproduction coverage, missing tests, or stronger regression protection before implementation.
|
|
316
316
|
- `browser-application-validation` or `ui-polish-validation` after user-facing UI changes or when the ask is explicitly browser validation.
|
|
317
317
|
- `implementation-feature-review` when you need to verify the delivered behavior matches the feature spec.
|
|
318
318
|
- `implementation-design-review` when you need to verify the code matches the approved technical design.
|
|
@@ -689,6 +689,13 @@ class AiHubConversationStore {
|
|
|
689
689
|
const { headers } = this.loadIndex(this.bucketDir(key), key);
|
|
690
690
|
return headers.map((h) => ({ ...h }));
|
|
691
691
|
}
|
|
692
|
+
// Issue #1477: the active conversation id, from the same cheap index used by
|
|
693
|
+
// loadProjectHeaders, so a headers-only list response can still report it.
|
|
694
|
+
loadProjectActiveId(projectPath) {
|
|
695
|
+
this.ensureMigrated();
|
|
696
|
+
const key = normalizeConversationKey(projectPath);
|
|
697
|
+
return this.loadIndex(this.bucketDir(key), key).activeId;
|
|
698
|
+
}
|
|
692
699
|
// Lazy body read: one full conversation.
|
|
693
700
|
loadConversation(projectPath, conversationId) {
|
|
694
701
|
this.ensureMigrated();
|
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -16,6 +16,7 @@ exports.__clearEmployeeDetectionMemoryCacheForTests = __clearEmployeeDetectionMe
|
|
|
16
16
|
exports.__setEmployeeDetectionTtlForTests = __setEmployeeDetectionTtlForTests;
|
|
17
17
|
exports.__getEmployeeProbeRoundsForTests = __getEmployeeProbeRoundsForTests;
|
|
18
18
|
exports.__resetEmployeeProbeRoundsForTests = __resetEmployeeProbeRoundsForTests;
|
|
19
|
+
exports.isEmployeeDetectionRefreshing = isEmployeeDetectionRefreshing;
|
|
19
20
|
exports.detectEmployeesAsync = detectEmployeesAsync;
|
|
20
21
|
exports.detectEmployees = detectEmployees;
|
|
21
22
|
exports.prepareCodexBrowserHome = prepareCodexBrowserHome;
|
|
@@ -895,10 +896,11 @@ function resolveHostInvocation(plan) {
|
|
|
895
896
|
args: ['/d', '/s', '/c', [command, ...args].map(escapeWindowsArg).join(' ')],
|
|
896
897
|
};
|
|
897
898
|
}
|
|
899
|
+
// Issue #1256 (slice a, AC-A3): delegates to the shared recovery helper so this probe agrees
|
|
900
|
+
// with server.ts's hubCommandVersion() and fraim doctor's health check on the same PATH,
|
|
901
|
+
// instead of each independently deciding what "the agent's PATH" means.
|
|
898
902
|
function buildAgentVersionProbePath(basePath) {
|
|
899
|
-
|
|
900
|
-
const withoutProjectBins = (0, managed_agent_paths_1.stripProjectLocalNodeBinDirs)(withoutManaged);
|
|
901
|
-
return (0, managed_agent_paths_1.appendBinDirsToPath)(withoutProjectBins, (0, managed_agent_paths_1.getManagedAgentBinDirs)());
|
|
903
|
+
return (0, managed_agent_paths_1.buildRecoveredAgentPath)(basePath);
|
|
902
904
|
}
|
|
903
905
|
// Single source for the probe environment, shared by the sync and async probes so the two
|
|
904
906
|
// cannot drift on how agent bin directories are put on PATH. Project-local npm shims are
|
|
@@ -918,7 +920,10 @@ const availableByVersionProbe = (command) => {
|
|
|
918
920
|
});
|
|
919
921
|
if (result.status !== 0 || result.error)
|
|
920
922
|
return null;
|
|
921
|
-
|
|
923
|
+
// Issue #1256 (AC-D1/D2): only stdout counts as a version — an agent that exits 0 while
|
|
924
|
+
// writing only to stderr (e.g. GitHub Copilot CLI's current failure mode) is broken, not
|
|
925
|
+
// "available with an odd version string".
|
|
926
|
+
return (0, managed_agent_paths_1.versionFromProbeOutput)(result.stdout, result.stderr);
|
|
922
927
|
};
|
|
923
928
|
// Issue #1010: the async counterpart of availableByVersionProbe. Same semantics (exit 0
|
|
924
929
|
// from `<agent> --version` means the CLI is installed AND actually runs), but non-blocking
|
|
@@ -937,7 +942,11 @@ const availableByVersionProbeAsync = (command) => new Promise((resolve) => {
|
|
|
937
942
|
let settled = false;
|
|
938
943
|
let timer;
|
|
939
944
|
let child;
|
|
940
|
-
|
|
945
|
+
// Issue #1256 (AC-D1/D2): kept as separate stdout/stderr buffers rather than one combined
|
|
946
|
+
// buffer, so a broken CLI that exits 0 while writing only to stderr cannot have its error
|
|
947
|
+
// text mistaken for a version string.
|
|
948
|
+
const stdoutChunks = [];
|
|
949
|
+
const stderrChunks = [];
|
|
941
950
|
const finish = (value) => {
|
|
942
951
|
if (settled)
|
|
943
952
|
return;
|
|
@@ -951,8 +960,8 @@ const availableByVersionProbeAsync = (command) => new Promise((resolve) => {
|
|
|
951
960
|
env: versionProbeEnv(),
|
|
952
961
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
953
962
|
});
|
|
954
|
-
child.stdout?.on('data', (chunk) =>
|
|
955
|
-
child.stderr?.on('data', (chunk) =>
|
|
963
|
+
child.stdout?.on('data', (chunk) => stdoutChunks.push(chunk));
|
|
964
|
+
child.stderr?.on('data', (chunk) => stderrChunks.push(chunk));
|
|
956
965
|
timer = setTimeout(() => {
|
|
957
966
|
console.warn(`[ai-hub] agent version probe timed out after ${VERSION_PROBE_TIMEOUT_MS}ms: ${command}`);
|
|
958
967
|
try {
|
|
@@ -965,8 +974,9 @@ const availableByVersionProbeAsync = (command) => new Promise((resolve) => {
|
|
|
965
974
|
timer.unref?.();
|
|
966
975
|
child.on('error', () => finish(null));
|
|
967
976
|
child.on('close', (code) => {
|
|
968
|
-
const
|
|
969
|
-
|
|
977
|
+
const stdout = Buffer.concat(stdoutChunks).toString('utf8');
|
|
978
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf8');
|
|
979
|
+
finish(code === 0 ? (0, managed_agent_paths_1.versionFromProbeOutput)(stdout, stderr) : null);
|
|
970
980
|
});
|
|
971
981
|
}
|
|
972
982
|
catch {
|
|
@@ -1158,6 +1168,16 @@ function buildEmployeeStatus(id, version) {
|
|
|
1158
1168
|
supportsRaw: supportsDirectPath(id),
|
|
1159
1169
|
};
|
|
1160
1170
|
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Issue #1256 (slice b, AC-B1): true when a background re-probe is currently in flight —
|
|
1173
|
+
* i.e. the most recent detectEmployees()/detectEmployeesAsync() answer may have been served
|
|
1174
|
+
* from a stale persisted snapshot rather than a fresh probe. bootstrapResponse reads this
|
|
1175
|
+
* right after detection so the client can be told to poll again shortly instead of latching
|
|
1176
|
+
* a one-shot answer.
|
|
1177
|
+
*/
|
|
1178
|
+
function isEmployeeDetectionRefreshing() {
|
|
1179
|
+
return inFlightDetection !== null;
|
|
1180
|
+
}
|
|
1161
1181
|
/**
|
|
1162
1182
|
* Non-blocking employee detection. Probes every agent CONCURRENTLY, so the cost is the
|
|
1163
1183
|
* slowest single probe rather than the sum of all of them, and the event loop stays free
|
|
@@ -2140,6 +2160,11 @@ class CliHostRuntime {
|
|
|
2140
2160
|
detectEmployeesAsync() {
|
|
2141
2161
|
return detectEmployeesAsync();
|
|
2142
2162
|
}
|
|
2163
|
+
// Issue #1256 (slice b): lets bootstrapResponse tell the client whether the roster it just
|
|
2164
|
+
// served may be stale while a background re-probe lands.
|
|
2165
|
+
isEmployeeDetectionRefreshing() {
|
|
2166
|
+
return isEmployeeDetectionRefreshing();
|
|
2167
|
+
}
|
|
2143
2168
|
startRun(hostId, projectPath, message, handlers, sessionId, launchContext) {
|
|
2144
2169
|
// R11: start/startDirect mint sessions rather than resuming one, so they
|
|
2145
2170
|
// stay outside the continuation queue entirely.
|
|
@@ -41,6 +41,8 @@ exports.selfHealBrokenManagedAgents = selfHealBrokenManagedAgents;
|
|
|
41
41
|
exports.configureFraimForHubAgent = configureFraimForHubAgent;
|
|
42
42
|
exports.installAgentAndRefreshDetection = installAgentAndRefreshDetection;
|
|
43
43
|
exports.hubCommandVersion = hubCommandVersion;
|
|
44
|
+
exports.__setAgentInstallTimeoutMsForTests = __setAgentInstallTimeoutMsForTests;
|
|
45
|
+
exports.hubRunProcess = hubRunProcess;
|
|
44
46
|
exports.buildOpenFileInvocation = buildOpenFileInvocation;
|
|
45
47
|
const express_1 = __importDefault(require("express"));
|
|
46
48
|
const path_1 = __importDefault(require("path"));
|
|
@@ -56,6 +58,9 @@ const brand_store_1 = require("../core/brand-store");
|
|
|
56
58
|
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
57
59
|
const catalog_1 = require("./catalog");
|
|
58
60
|
const job_visualization_1 = require("../core/job-visualization");
|
|
61
|
+
// Issue #1491: reserved sentinel phase a recurring job parks at between
|
|
62
|
+
// scheduled checks (registry/delivery/recurring-checkpoint.md).
|
|
63
|
+
const resolve_phase_edge_1 = require("../core/resolve-phase-edge");
|
|
59
64
|
const custom_employees_1 = require("./custom-employees");
|
|
60
65
|
const agent_token_prices_1 = require("../local-mcp-server/agent-token-prices");
|
|
61
66
|
const hosts_1 = require("./hosts");
|
|
@@ -281,6 +286,12 @@ const MACHINE_LEVEL_JOB_IDS = new Set([
|
|
|
281
286
|
const DEFAULT_CONVERSATION_FLUSH_DELAY_MS = 2000;
|
|
282
287
|
const HUB_RESTART_RECOVERY_LOCK_TIMEOUT_MS = 5000;
|
|
283
288
|
const HUB_RESTART_RECOVERY_LOCK_STALE_MS = 10000;
|
|
289
|
+
// The complete catalog (fraim/ai-employee/jobs + the registry/ source fallback), used
|
|
290
|
+
// everywhere the Hub needs to know about every job it could ever offer or resolve —
|
|
291
|
+
// state.bootstrap.jobs, the firstRun job count, and resolveHubJob. A caller that omits
|
|
292
|
+
// includeRegistry only sees the synced fraim/ layer, which is empty for a project (like
|
|
293
|
+
// this repo) that runs against source registry/ content before it has been synced.
|
|
294
|
+
const FULL_JOB_CATALOG_OPTIONS = { includeRegistry: true };
|
|
284
295
|
function conversationFlushDelayMs() {
|
|
285
296
|
const configured = Number(process.env.FRAIM_HUB_CONVERSATION_FLUSH_MS);
|
|
286
297
|
if (Number.isFinite(configured) && configured >= 0)
|
|
@@ -1674,6 +1685,12 @@ function applySeekMentoringSignal(run, signal) {
|
|
|
1674
1685
|
applyReviewHandoffToRun(run, signal.reviewHandoff);
|
|
1675
1686
|
verifyReviewHandoffReflectsSubmission(run, signal.reviewHandoff, isCrossJob ? callJobName : null);
|
|
1676
1687
|
}
|
|
1688
|
+
else if (!isCrossJob) {
|
|
1689
|
+
// Issue #1501: No reviewHandoff in this same-job call. Clear any prior reviewHandoff so
|
|
1690
|
+
// run.reviewHandoff always reflects the most recent seekMentoring intent,
|
|
1691
|
+
// not a stale flag from an earlier submit phase.
|
|
1692
|
+
run.reviewHandoff = null;
|
|
1693
|
+
}
|
|
1677
1694
|
if (isCrossJob)
|
|
1678
1695
|
return;
|
|
1679
1696
|
// Issue #848: capture the issue number the call carries so a launched follow-on
|
|
@@ -1929,25 +1946,32 @@ function hubCommandVersion(command, extraBinDirs, basePath) {
|
|
|
1929
1946
|
if (process.env.NODE_ENV === 'test' && process.env.FRAIM_TEST_HUB_COMMAND_VERSION_EMPTY === '1') {
|
|
1930
1947
|
return null;
|
|
1931
1948
|
}
|
|
1932
|
-
const
|
|
1949
|
+
const suppliedPath = extraBinDirs && extraBinDirs.length > 0
|
|
1933
1950
|
? (0, managed_agent_paths_1.appendBinDirsToPath)(basePath ?? process.env.PATH, extraBinDirs)
|
|
1934
1951
|
: basePath;
|
|
1952
|
+
// Issue #1256 (AC-A3/AC-C3): route through the same shared recovery helper the version
|
|
1953
|
+
// probe (hosts.ts) and fraim doctor use, so both callers of this function — the install
|
|
1954
|
+
// route's existing-version check and check-agent — find a CLI installed outside FRAIM's
|
|
1955
|
+
// own managed directory (npm-global, ~/.local/bin, a vendor installer) exactly like the
|
|
1956
|
+
// probe does. A strict superset of whatever `suppliedPath`/`process.env.PATH` already had.
|
|
1957
|
+
const recoveredPath = (0, managed_agent_paths_1.buildRecoveredAgentPath)(suppliedPath ?? process.env.PATH);
|
|
1935
1958
|
// Issue #1284/#1285 (Defect D, §0): resolve to a real absolute path — with
|
|
1936
1959
|
// project-local node_modules/.bin stripped — instead of handing the bare
|
|
1937
1960
|
// command name to cmd.exe's own PATH search, which does not exclude those
|
|
1938
1961
|
// shims. Falls back to the bare command (today's behavior, resolves to
|
|
1939
1962
|
// null below) when no system install is found on the constructed PATH.
|
|
1940
|
-
const resolvedCommand = (0, command_resolution_1.getSystemCommandPath)(command,
|
|
1963
|
+
const resolvedCommand = (0, command_resolution_1.getSystemCommandPath)(command, recoveredPath) || command;
|
|
1941
1964
|
const executable = process.platform === 'win32' ? 'cmd.exe' : resolvedCommand;
|
|
1942
1965
|
const args = process.platform === 'win32'
|
|
1943
1966
|
? ['/d', '/s', '/c', `${(0, hosts_1.escapeWindowsArg)(resolvedCommand)} --version`]
|
|
1944
1967
|
: ['--version'];
|
|
1945
|
-
|
|
1946
|
-
const
|
|
1968
|
+
// Issue #1256: set both casings — see the matching comment in resolveNpmGlobalBinDirs().
|
|
1969
|
+
const env = { ...process.env, PATH: recoveredPath, Path: recoveredPath };
|
|
1970
|
+
const result = (0, child_process_1.spawnSync)(executable, args, { encoding: 'utf8', timeout: 5000, env });
|
|
1947
1971
|
if (result.status !== 0 || result.error)
|
|
1948
1972
|
return null;
|
|
1949
|
-
|
|
1950
|
-
return
|
|
1973
|
+
// Issue #1256 (AC-D1/D2): only stdout counts as a version — see versionFromProbeOutput.
|
|
1974
|
+
return (0, managed_agent_paths_1.versionFromProbeOutput)(result.stdout, result.stderr);
|
|
1951
1975
|
}
|
|
1952
1976
|
function hubRunSync(command, args, env) {
|
|
1953
1977
|
const resolvedCommand = (0, command_resolution_1.getSystemCommandPath)(command, process.env.PATH) || command;
|
|
@@ -1974,6 +1998,22 @@ function hubRunSync(command, args, env) {
|
|
|
1974
1998
|
...(result.error ? { error: result.error } : {}),
|
|
1975
1999
|
};
|
|
1976
2000
|
}
|
|
2001
|
+
// Issue #1256 (slice c, AC-C1/AC-C4): the root cause of "Installing…" hanging forever.
|
|
2002
|
+
// hubRunProcess runs the actual `npm install -g <package>` behind the install-agent route,
|
|
2003
|
+
// and previously had NO bound — only `close` resolved the promise. A stalled npm (registry
|
|
2004
|
+
// timeout, an interactive auth prompt with nothing to answer it, a proxy stall) never closed,
|
|
2005
|
+
// so this promise never settled, the route handler's `await` never returned, and the client's
|
|
2006
|
+
// button stayed greyed at "Installing…" with no way back. 90s is generous for a real install
|
|
2007
|
+
// (typically seconds) while still bounding the failure the way AC-C1 requires.
|
|
2008
|
+
const DEFAULT_AGENT_INSTALL_TIMEOUT_MS = 90_000;
|
|
2009
|
+
let agentInstallTimeoutMs = DEFAULT_AGENT_INSTALL_TIMEOUT_MS;
|
|
2010
|
+
/** Test seam only. Mirrors __setEmployeeDetectionTtlForTests in hosts.ts — proves the bound
|
|
2011
|
+
* actually fires without a real 90s hang. Pass null to restore the production value. */
|
|
2012
|
+
function __setAgentInstallTimeoutMsForTests(ms) {
|
|
2013
|
+
agentInstallTimeoutMs = ms ?? DEFAULT_AGENT_INSTALL_TIMEOUT_MS;
|
|
2014
|
+
}
|
|
2015
|
+
// Exported for direct unit testing (issue #1256, AC-C1/AC-C4): asserting a process that never
|
|
2016
|
+
// closes still rejects within the bound, without needing a real 90s-hanging npm install.
|
|
1977
2017
|
function hubRunProcess(command, args, env) {
|
|
1978
2018
|
if (process.env.NODE_ENV === 'test' && command === 'npm' && process.env.FRAIM_TEST_HUB_NPM_ERROR) {
|
|
1979
2019
|
return Promise.reject(new Error(process.env.FRAIM_TEST_HUB_NPM_ERROR));
|
|
@@ -1995,15 +2035,46 @@ function hubRunProcess(command, args, env) {
|
|
|
1995
2035
|
});
|
|
1996
2036
|
let stdout = '';
|
|
1997
2037
|
let stderr = '';
|
|
2038
|
+
let settled = false;
|
|
2039
|
+
const timer = setTimeout(() => {
|
|
2040
|
+
if (settled)
|
|
2041
|
+
return;
|
|
2042
|
+
settled = true;
|
|
2043
|
+
// Issue #1256: tree-kill, not child.kill() — on win32 `child` is the cmd.exe wrapper
|
|
2044
|
+
// (see realCmd/realArgs above); killing only that leaves the real supervised process
|
|
2045
|
+
// (e.g. npm.exe, or a grandchild it spawns) running as an orphan, and — since cmd.exe
|
|
2046
|
+
// typically shares its stdout/stderr pipe handles with what it launches — can keep
|
|
2047
|
+
// those pipes open after the wrapper dies, which is exactly what stopped this promise's
|
|
2048
|
+
// own process from exiting cleanly during this phase's validation run. Same pattern as
|
|
2049
|
+
// HostProcessRegistry.stop() above.
|
|
2050
|
+
if (child.pid != null) {
|
|
2051
|
+
try {
|
|
2052
|
+
(0, tree_kill_1.default)(child.pid, 'SIGTERM');
|
|
2053
|
+
}
|
|
2054
|
+
catch { /* already gone */ }
|
|
2055
|
+
}
|
|
2056
|
+
reject(new Error(`${command} ${args.join(' ')} did not complete within ${agentInstallTimeoutMs / 1000}s.`));
|
|
2057
|
+
}, agentInstallTimeoutMs);
|
|
2058
|
+
timer.unref?.();
|
|
1998
2059
|
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
1999
2060
|
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
2000
2061
|
child.on('close', (code) => {
|
|
2062
|
+
if (settled)
|
|
2063
|
+
return;
|
|
2064
|
+
settled = true;
|
|
2065
|
+
clearTimeout(timer);
|
|
2001
2066
|
if (code === 0)
|
|
2002
2067
|
resolve({ stdout, stderr });
|
|
2003
2068
|
else
|
|
2004
2069
|
reject(new Error(stderr || `Process exited with code ${code}`));
|
|
2005
2070
|
});
|
|
2006
|
-
child.on('error',
|
|
2071
|
+
child.on('error', (error) => {
|
|
2072
|
+
if (settled)
|
|
2073
|
+
return;
|
|
2074
|
+
settled = true;
|
|
2075
|
+
clearTimeout(timer);
|
|
2076
|
+
reject(error);
|
|
2077
|
+
});
|
|
2007
2078
|
});
|
|
2008
2079
|
}
|
|
2009
2080
|
function hubOpenTerminal(command) {
|
|
@@ -2566,6 +2637,15 @@ function classifyExit(run, exitCode) {
|
|
|
2566
2637
|
// real description, not the post-clear fallback.
|
|
2567
2638
|
return { action: 'resume', pauseReason: 'working', recoveryKind: 'background_task_lost', systemNote: describeKilledBackgroundTasks(run) };
|
|
2568
2639
|
}
|
|
2640
|
+
// Issue #1491: a non-final scheduled/webhook check parks at the reserved
|
|
2641
|
+
// recurring-checkpoint sentinel by design (registry/delivery/recurring-checkpoint.md).
|
|
2642
|
+
// This is not a human-decision gate — the scheduler resumes it on its own. Gated on
|
|
2643
|
+
// sourceTrigger so a manager-initiated run that somehow lands on the sentinel phase
|
|
2644
|
+
// (not expected in normal operation) keeps today's conservative awaiting_user default.
|
|
2645
|
+
if (currentPhase === resolve_phase_edge_1.WAITING_FOR_NEXT_INVOCATION
|
|
2646
|
+
&& (run.sourceTrigger === 'scheduled' || run.sourceTrigger === 'webhook')) {
|
|
2647
|
+
return { action: 'park', pauseReason: 'parked_recurring' };
|
|
2648
|
+
}
|
|
2569
2649
|
return { action: 'park', pauseReason: 'awaiting_user' };
|
|
2570
2650
|
}
|
|
2571
2651
|
if (compactingActive) {
|
|
@@ -2932,6 +3012,18 @@ class AiHubServer {
|
|
|
2932
3012
|
}
|
|
2933
3013
|
});
|
|
2934
3014
|
this.registerRoutes();
|
|
3015
|
+
// Issue #1477 R8: global error handler, registered after every route. Express's
|
|
3016
|
+
// default handler for an error that escapes a route (e.g. the RangeError a
|
|
3017
|
+
// JSON.stringify overflow used to throw from the conversations list route) renders
|
|
3018
|
+
// an HTML page with the stack trace. This returns structured JSON instead — the
|
|
3019
|
+
// stack still goes to the server log, never to the response.
|
|
3020
|
+
this.app.use((err, _req, res, next) => {
|
|
3021
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3022
|
+
console.error('[ai-hub] unhandled route error:', err instanceof Error ? err.stack : message);
|
|
3023
|
+
if (res.headersSent)
|
|
3024
|
+
return next(err);
|
|
3025
|
+
res.status(500).json({ error: message });
|
|
3026
|
+
});
|
|
2935
3027
|
}
|
|
2936
3028
|
getApp() {
|
|
2937
3029
|
return this.app;
|
|
@@ -3192,8 +3284,7 @@ class AiHubServer {
|
|
|
3192
3284
|
}
|
|
3193
3285
|
}
|
|
3194
3286
|
const project = (0, catalog_1.summarizeProject)(normalizedProjectPath);
|
|
3195
|
-
const
|
|
3196
|
-
const rawJobs = (0, catalog_1.discoverEmployeeJobs)(normalizedProjectPath, catalogOptions);
|
|
3287
|
+
const rawJobs = (0, catalog_1.discoverEmployeeJobs)(normalizedProjectPath, FULL_JOB_CATALOG_OPTIONS);
|
|
3197
3288
|
// Issue #566 (R7): jobs already carry `personalized` from catalog discovery
|
|
3198
3289
|
// (true for the fraim/personalized-employee layer). The Hub renders a plain
|
|
3199
3290
|
// "Personalized" marking from that flag — no author/attribution is tracked.
|
|
@@ -3213,7 +3304,7 @@ class AiHubServer {
|
|
|
3213
3304
|
...job,
|
|
3214
3305
|
requiredPersonaKey: getProtectedPersonaForHubJob(job.id) ?? GENERIC_WORKER_PERSONA_KEY,
|
|
3215
3306
|
}));
|
|
3216
|
-
const managerTemplates = (0, catalog_1.discoverManagerTemplates)(normalizedProjectPath,
|
|
3307
|
+
const managerTemplates = (0, catalog_1.discoverManagerTemplates)(normalizedProjectPath, FULL_JOB_CATALOG_OPTIONS);
|
|
3217
3308
|
// Issue #750: the apiKey always comes from ~/.fraim/config.json — no header
|
|
3218
3309
|
// override, no ai-hub-state.json copy, no fallback chain.
|
|
3219
3310
|
const resolvedApiKey = resolveApiKey();
|
|
@@ -3261,11 +3352,15 @@ class AiHubServer {
|
|
|
3261
3352
|
// ~/.fraim/config.json, so public/ai-hub/script.js's existing
|
|
3262
3353
|
// tfConnectedApiKey()/tfConnectedSurfaceUrl read path needs no changes.
|
|
3263
3354
|
preferences: { ...preferences, apiKey: resolvedApiKey },
|
|
3264
|
-
categories: (0, catalog_1.getAiHubCategories)(normalizedProjectPath,
|
|
3355
|
+
categories: (0, catalog_1.getAiHubCategories)(normalizedProjectPath, FULL_JOB_CATALOG_OPTIONS),
|
|
3265
3356
|
jobs,
|
|
3266
3357
|
internalJobs,
|
|
3267
3358
|
managerTemplates,
|
|
3268
3359
|
employees,
|
|
3360
|
+
// Issue #1256 (slice b, AC-B1): true when `employees` above may be a stale persisted
|
|
3361
|
+
// snapshot while a background re-probe is in flight. The client uses this to schedule
|
|
3362
|
+
// a bounded follow-up refresh instead of trusting a one-shot bootstrap forever.
|
|
3363
|
+
employeesRefreshing: this.hostRuntime.isEmployeeDetectionRefreshing?.() ?? false,
|
|
3269
3364
|
configuredAgents,
|
|
3270
3365
|
personas,
|
|
3271
3366
|
// Issue #1005: lets the client tell a resolved projection from the first-paint
|
|
@@ -4585,7 +4680,13 @@ class AiHubServer {
|
|
|
4585
4680
|
resolveHubJob(projectPath, jobId) {
|
|
4586
4681
|
if (!jobId || jobId === '__freeform__')
|
|
4587
4682
|
return null;
|
|
4588
|
-
|
|
4683
|
+
// Issue #1477: include the source registry/ fallback, matching FULL_JOB_CATALOG_OPTIONS
|
|
4684
|
+
// used to build state.bootstrap.jobs (the list the UI's job pickers, including the
|
|
4685
|
+
// New Assignment modal, actually offer). Without it, a job that only ships from
|
|
4686
|
+
// registry/ (never synced into fraim/ai-employee/jobs, as on the FRAIM repo itself)
|
|
4687
|
+
// was selectable in the UI but unresolvable here — the exact gap that let
|
|
4688
|
+
// travel-fare-watch's title/persona silently fail to resolve at fire time.
|
|
4689
|
+
const employeeJob = (0, catalog_1.discoverEmployeeJobs)(projectPath, FULL_JOB_CATALOG_OPTIONS).find((job) => job.id === jobId);
|
|
4589
4690
|
if (employeeJob) {
|
|
4590
4691
|
return {
|
|
4591
4692
|
id: employeeJob.id,
|
|
@@ -4594,7 +4695,7 @@ class AiHubServer {
|
|
|
4594
4695
|
personaKey: employeeJob.requiredPersonaKey ?? getCustomPersonaForJob(projectPath, employeeJob.id) ?? getHubPersonaForJob(employeeJob.id),
|
|
4595
4696
|
};
|
|
4596
4697
|
}
|
|
4597
|
-
const managerTemplate = (0, catalog_1.discoverManagerTemplates)(projectPath).find((job) => job.id === jobId);
|
|
4698
|
+
const managerTemplate = (0, catalog_1.discoverManagerTemplates)(projectPath, FULL_JOB_CATALOG_OPTIONS).find((job) => job.id === jobId);
|
|
4598
4699
|
if (managerTemplate) {
|
|
4599
4700
|
return {
|
|
4600
4701
|
id: managerTemplate.id,
|
|
@@ -4605,6 +4706,16 @@ class AiHubServer {
|
|
|
4605
4706
|
}
|
|
4606
4707
|
return null;
|
|
4607
4708
|
}
|
|
4709
|
+
// Issue #1477 R1-R3: shared 400 message for a schedules jobId that doesn't resolve
|
|
4710
|
+
// to a catalog entry. Names the correct shape (catalog slug) and explicitly rules out
|
|
4711
|
+
// the per-run tracking UUID get_fraim_job prints, since that UUID was mistaken for
|
|
4712
|
+
// this field in the reported repro.
|
|
4713
|
+
invalidScheduleJobIdError(jobId) {
|
|
4714
|
+
return {
|
|
4715
|
+
error: `Unknown jobId "${String(jobId)}". jobId must be a catalog job slug (e.g. "travel-fare-watch") ` +
|
|
4716
|
+
'from the Hub job catalog, not the per-run tracking UUID returned by get_fraim_job\'s "Job ID" field.',
|
|
4717
|
+
};
|
|
4718
|
+
}
|
|
4608
4719
|
applySeekMentoringSignalToRun(run, signal) {
|
|
4609
4720
|
// Issue #732: promote using the stable jobName slug, not the per-call UUID
|
|
4610
4721
|
// jobId (resolveHubJob would never match a UUID, leaving a freeform run
|
|
@@ -5031,7 +5142,7 @@ class AiHubServer {
|
|
|
5031
5142
|
// not to the directory the Hub launched from.
|
|
5032
5143
|
const { personas, subscriptionActive, workspaceId, userKey, identityAuthoritative } = await this.computePersonas(apiKey, managerTeamPromise, projectPath);
|
|
5033
5144
|
const managerTeam = await managerTeamPromise;
|
|
5034
|
-
const jobCount = (0, catalog_1.discoverEmployeeJobs)(projectPath,
|
|
5145
|
+
const jobCount = (0, catalog_1.discoverEmployeeJobs)(projectPath, FULL_JOB_CATALOG_OPTIONS)
|
|
5035
5146
|
.filter((job) => !FRAIM_INTERNAL_JOB_IDS.has(job.id))
|
|
5036
5147
|
.length;
|
|
5037
5148
|
res.json({
|
|
@@ -5171,7 +5282,6 @@ class AiHubServer {
|
|
|
5171
5282
|
// Issue #820: lazy read surfaces so the UI never has to parse the whole store.
|
|
5172
5283
|
// ?conversationId=<id> -> one full body (lazy load on open).
|
|
5173
5284
|
// ?headersOnly=1 -> newest-first headers, no bodies (fast list / first paint).
|
|
5174
|
-
// (default) -> full project (backwards compatible).
|
|
5175
5285
|
if (conversationId) {
|
|
5176
5286
|
const conversation = this.conversationStore.loadConversation(key, conversationId);
|
|
5177
5287
|
if (!conversation)
|
|
@@ -5184,12 +5294,28 @@ class AiHubServer {
|
|
|
5184
5294
|
// Issue #1090: ownership is derived on read, never served from the frozen record.
|
|
5185
5295
|
return res.json({ projectPath: key, scope: scope ?? 'project', conversation: withDerivedPersona(withFreshStages, key), source: 'disk' });
|
|
5186
5296
|
}
|
|
5297
|
+
// Issue #1477 R6: a project-scoped list request (no conversationId) always serves
|
|
5298
|
+
// headers only now, never the full project body — the removed full-payload branch
|
|
5299
|
+
// could exceed V8's max string length in JSON.stringify once a project's
|
|
5300
|
+
// conversation store accumulates enough embedded message/event content, which
|
|
5301
|
+
// reached the client as an unhandled RangeError rendered as a raw HTML stack trace.
|
|
5302
|
+
// forcedHeadersOnly tells the caller this response is headers-only regardless of
|
|
5303
|
+
// whether it asked for headersOnly=1. Scope-based (manager/company) buckets are
|
|
5304
|
+
// unaffected and keep the headersOnly/full-body branches below.
|
|
5305
|
+
if (!scope) {
|
|
5306
|
+
const customOwners = buildCustomJobOwnerIndex(key);
|
|
5307
|
+
const headers = this.conversationStore.loadProjectHeaders(key)
|
|
5308
|
+
.map((header) => withDerivedPersona(header, key, customOwners));
|
|
5309
|
+
const activeId = this.conversationStore.loadProjectActiveId(key);
|
|
5310
|
+
return res.json({ projectPath: key, scope: 'project', activeId, headersOnly: true, forcedHeadersOnly: true, conversations: headers, source: 'disk' });
|
|
5311
|
+
}
|
|
5187
5312
|
if (req.query.headersOnly === '1' || req.query.headersOnly === 'true') {
|
|
5188
5313
|
// One disk walk for the whole list, not one per conversation.
|
|
5189
5314
|
const customOwners = buildCustomJobOwnerIndex(key);
|
|
5190
5315
|
const headers = this.conversationStore.loadProjectHeaders(key)
|
|
5191
5316
|
.map((header) => withDerivedPersona(header, key, customOwners));
|
|
5192
|
-
|
|
5317
|
+
const activeId = this.conversationStore.loadProjectActiveId(key);
|
|
5318
|
+
return res.json({ projectPath: key, scope, activeId, headersOnly: true, conversations: headers, source: 'disk' });
|
|
5193
5319
|
}
|
|
5194
5320
|
const loaded = this.conversationStore.loadProject(key);
|
|
5195
5321
|
const fullCustomOwners = buildCustomJobOwnerIndex(key);
|
|
@@ -5197,7 +5323,7 @@ class AiHubServer {
|
|
|
5197
5323
|
...loaded,
|
|
5198
5324
|
conversations: (loaded.conversations || []).map((conversation) => withDerivedPersona(conversation, key, fullCustomOwners)),
|
|
5199
5325
|
};
|
|
5200
|
-
return res.json({ projectPath: key, scope
|
|
5326
|
+
return res.json({ projectPath: key, scope, ...derived, source: 'disk' });
|
|
5201
5327
|
});
|
|
5202
5328
|
this.app.put('/api/ai-hub/conversations', (req, res) => {
|
|
5203
5329
|
try {
|
|
@@ -6987,6 +7113,12 @@ class AiHubServer {
|
|
|
6987
7113
|
catch (err) {
|
|
6988
7114
|
return res.status(400).json({ error: err instanceof Error ? err.message : 'Invalid schedule request.' });
|
|
6989
7115
|
}
|
|
7116
|
+
// Issue #1477 R1/R2: reject a jobId that doesn't resolve to a known catalog entry
|
|
7117
|
+
// (e.g. the per-run tracking UUID from get_fraim_job, mistaken for this field in
|
|
7118
|
+
// the reported repro) before the deployment is ever persisted.
|
|
7119
|
+
if (!this.resolveHubJob(resolvedProjectPath, jobId)) {
|
|
7120
|
+
return res.status(400).json(this.invalidScheduleJobIdError(jobId));
|
|
7121
|
+
}
|
|
6990
7122
|
const normalizedConversationId = typeof conversationId === 'string' && conversationId.trim()
|
|
6991
7123
|
? conversationId.trim()
|
|
6992
7124
|
: undefined;
|
|
@@ -7084,6 +7216,11 @@ class AiHubServer {
|
|
|
7084
7216
|
const existing = this.deploymentStore.load().find((d) => d.id === id && d.type === 'scheduled');
|
|
7085
7217
|
if (!existing)
|
|
7086
7218
|
return res.status(404).json({ error: 'Deployment not found.' });
|
|
7219
|
+
// Issue #1477 R3: same catalog validation as create, applied only when jobId is
|
|
7220
|
+
// actually part of this update.
|
|
7221
|
+
if (jobId !== undefined && !this.resolveHubJob(resolvedProjectPath !== undefined ? resolvedProjectPath : existing.projectPath, jobId)) {
|
|
7222
|
+
return res.status(400).json(this.invalidScheduleJobIdError(jobId));
|
|
7223
|
+
}
|
|
7087
7224
|
const nextHostId = hostId !== undefined && validEmployees.includes(hostId) ? hostId : existing.hostId;
|
|
7088
7225
|
const nextConfiguredAgentId = configuredAgentId !== undefined
|
|
7089
7226
|
? (typeof configuredAgentId === 'string' && configuredAgentId.trim() ? configuredAgentId.trim() : undefined)
|
|
@@ -45,7 +45,8 @@ function probeVersion(commandPath) {
|
|
|
45
45
|
const result = (0, child_process_1.spawnSync)(executable, args, { encoding: 'utf8', timeout: 5000 });
|
|
46
46
|
if (result.status !== 0 || result.error)
|
|
47
47
|
return null;
|
|
48
|
-
|
|
48
|
+
// Issue #1256 (AC-D1/D2): only stdout counts as a version — see versionFromProbeOutput.
|
|
49
|
+
return (0, managed_agent_paths_1.versionFromProbeOutput)(result.stdout, result.stderr);
|
|
49
50
|
}
|
|
50
51
|
catch {
|
|
51
52
|
return null;
|
|
@@ -86,9 +87,32 @@ async function runAgentCliHealthCheck(cli) {
|
|
|
86
87
|
const managedSearchPath = (0, managed_agent_paths_1.buildPathWithManagedAgentBins)(process.env.PATH);
|
|
87
88
|
const managedPath = (0, command_resolution_1.getSystemCommandPath)(cli.command, managedSearchPath);
|
|
88
89
|
if (!ambientPath && !managedPath) {
|
|
90
|
+
// Issue #1256 (AC-A3): before concluding the CLI is absent, try the same shared
|
|
91
|
+
// location-agnostic recovery the Hub's version probe and check-agent route use. This
|
|
92
|
+
// only changes the answer for a CLI this process's raw/managed PATH cannot see at all
|
|
93
|
+
// (e.g. doctor itself invoked from a PATH-truncated context) — it never overrides a
|
|
94
|
+
// drift verdict computed from a real ambient/managed hit above.
|
|
95
|
+
const recoveredSearchPath = (0, managed_agent_paths_1.buildRecoveredAgentPath)(process.env.PATH);
|
|
96
|
+
const recoveredPath = (0, command_resolution_1.getSystemCommandPath)(cli.command, recoveredSearchPath);
|
|
97
|
+
if (!recoveredPath) {
|
|
98
|
+
return {
|
|
99
|
+
status: 'passed',
|
|
100
|
+
message: `${cli.label} is not installed; nothing to check.`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const recoveredVersion = probeVersion(recoveredPath);
|
|
104
|
+
if (!recoveredVersion) {
|
|
105
|
+
return {
|
|
106
|
+
status: 'error',
|
|
107
|
+
message: `${cli.label} is installed at ${recoveredPath} but failed to run (\`${cli.command} --version\` produced no output).`,
|
|
108
|
+
suggestion: `Reinstall ${cli.label}, then run "${cli.command} --version" again to confirm it's fixed.`,
|
|
109
|
+
details: { recoveredPath },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
89
112
|
return {
|
|
90
113
|
status: 'passed',
|
|
91
|
-
message: `${cli.label} is
|
|
114
|
+
message: `${cli.label} is consistent (${recoveredVersion}).`,
|
|
115
|
+
details: { recoveredPath, recoveredVersion },
|
|
92
116
|
};
|
|
93
117
|
}
|
|
94
118
|
const ambientVersion = ambientPath ? probeVersion(ambientPath) : null;
|
|
@@ -3,12 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.expandPath = exports.findIDEByName = exports.getAllSupportedIDEs = exports.detectInstalledIDEs = exports.CLI_PROBE_CONFIGTYPES = exports.IDE_CONFIGS = void 0;
|
|
6
|
+
exports.expandPath = exports.findIDEByName = exports.getAllSupportedIDEs = exports.detectInstalledIDEs = exports.CLI_PROBE_CONFIGTYPES = exports.IDE_CONFIGS = exports.detectCodexDesktop = exports.CODEX_DESKTOP_DOWNLOAD_URL = exports.CODEX_DESKTOP_BUNDLE_ID = void 0;
|
|
7
7
|
exports.getAdapterConfigType = getAdapterConfigType;
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const os_1 = __importDefault(require("os"));
|
|
11
11
|
const child_process_1 = require("child_process");
|
|
12
|
+
const managed_agent_paths_1 = require("../utils/managed-agent-paths");
|
|
12
13
|
/** Returns the configType to use for adapter file filtering. Falls back to configType when adapterConfigType is not set. */
|
|
13
14
|
function getAdapterConfigType(ide) {
|
|
14
15
|
return ide.adapterConfigType ?? ide.configType;
|
|
@@ -78,10 +79,12 @@ const guiAppDetect = (configSurfaceCheck, appName, options = {}) => {
|
|
|
78
79
|
const availableByVersionProbe = (command) => {
|
|
79
80
|
if (process.env.FRAIM_DETECT_DISABLE_CLI_PROBES === '1')
|
|
80
81
|
return false;
|
|
82
|
+
const recoveredPath = (0, managed_agent_paths_1.buildRecoveredAgentPath)(process.env.PATH ?? process.env.Path);
|
|
83
|
+
const env = { ...process.env, PATH: recoveredPath, Path: recoveredPath };
|
|
81
84
|
const result = process.platform === 'win32'
|
|
82
|
-
? (0, child_process_1.spawnSync)('cmd.exe', ['/d', '/s', '/c', `${command} --version`], { encoding: 'utf8', timeout: 1500 })
|
|
83
|
-
: (0, child_process_1.spawnSync)(command, ['--version'], { encoding: 'utf8', timeout: 1500 });
|
|
84
|
-
return result.status === 0;
|
|
85
|
+
? (0, child_process_1.spawnSync)('cmd.exe', ['/d', '/s', '/c', `${command} --version`], { encoding: 'utf8', timeout: 1500, env })
|
|
86
|
+
: (0, child_process_1.spawnSync)(command, ['--version'], { encoding: 'utf8', timeout: 1500, env });
|
|
87
|
+
return result.status === 0 && !result.error && (0, managed_agent_paths_1.versionFromProbeOutput)(result.stdout, result.stderr) !== null;
|
|
85
88
|
};
|
|
86
89
|
const detectClaude = () => {
|
|
87
90
|
const paths = [
|
|
@@ -151,6 +154,73 @@ const detectCodexSurface = () => {
|
|
|
151
154
|
];
|
|
152
155
|
return checkMultiplePaths(paths);
|
|
153
156
|
};
|
|
157
|
+
// Issue #1478: OpenAI folded the standalone Codex desktop app into the unified
|
|
158
|
+
// ChatGPT desktop app (Codex is now a view inside it). That app has no CLI
|
|
159
|
+
// config folder to detect and, unlike Claude Desktop, no local MCP config
|
|
160
|
+
// file for FRAIM to bootstrap — so it is intentionally NOT an IDE_CONFIGS
|
|
161
|
+
// entry. Adding it there would flow into ~13 other detectInstalledIDEs()
|
|
162
|
+
// consumers (auto-mcp-setup, doctor checks, add-ide, sync, init-project) that
|
|
163
|
+
// all assume every catalog entry is MCP-configurable, producing a false
|
|
164
|
+
// "Unsupported config type" failure for a product that was never meant to
|
|
165
|
+
// have one. Detection here is standalone; callers splice its result directly
|
|
166
|
+
// into the surfaces/rows that need it (see session-service.ts).
|
|
167
|
+
//
|
|
168
|
+
// Detection matches a stable package/bundle identity, not a display name or
|
|
169
|
+
// guessed path — openai/codex#32631 documents the Codex CLI's own equivalent
|
|
170
|
+
// bug (searching for the old name `Codex.app`, broken by the rename to
|
|
171
|
+
// `ChatGPT.app`) as the live precedent for why name/path matching fails.
|
|
172
|
+
exports.CODEX_DESKTOP_BUNDLE_ID = 'com.openai.codex';
|
|
173
|
+
// detectCodexDesktop() runs on every platform, so this row renders for
|
|
174
|
+
// macOS/Windows/Linux users alike — a platform-specific doc page (e.g. the
|
|
175
|
+
// Windows-only install guide) would be wrong for everyone else. Use OpenAI's
|
|
176
|
+
// own universal download page, matching every sibling IDE_CONFIGS entry's
|
|
177
|
+
// convention of linking the vendor's own top-level product/download page,
|
|
178
|
+
// not a platform subpage (e.g. `claude.ai/download`, `cursor.com`).
|
|
179
|
+
exports.CODEX_DESKTOP_DOWNLOAD_URL = 'https://chatgpt.com/download/';
|
|
180
|
+
const detectCodexDesktopMac = () => {
|
|
181
|
+
if (process.env.FRAIM_DETECT_DISABLE_PROCESS_CHECK === '1')
|
|
182
|
+
return false;
|
|
183
|
+
const result = (0, child_process_1.spawnSync)('mdfind', [`kMDItemCFBundleIdentifier == '${exports.CODEX_DESKTOP_BUNDLE_ID}'`], { encoding: 'utf8', timeout: 2000 });
|
|
184
|
+
return result.status === 0 && Boolean((result.stdout || '').trim());
|
|
185
|
+
};
|
|
186
|
+
const detectCodexDesktopWindows = () => {
|
|
187
|
+
if (process.env.FRAIM_DETECT_DISABLE_PROCESS_CHECK === '1')
|
|
188
|
+
return false;
|
|
189
|
+
// The exact PackageFamilyName for the ChatGPT desktop app's MSIX package
|
|
190
|
+
// (Store ProductId 9PLM9XGG6VKS) is not yet confirmed against a real install
|
|
191
|
+
// (spec Open Question #2) — Get-AppxPackage's own `Name` property (a fixed
|
|
192
|
+
// publisher-assigned identifier, distinct from the mutable Start-menu
|
|
193
|
+
// display name) is queried by a name pattern as the best-available proxy.
|
|
194
|
+
const psCmd = `(Get-AppxPackage | Where-Object { $_.Name -like '*ChatGPT*' } | Select-Object -First 1).PackageFamilyName`;
|
|
195
|
+
const result = (0, child_process_1.spawnSync)('powershell', ['-NoProfile', '-NonInteractive', '-Command', psCmd], { encoding: 'utf8', timeout: 3000 });
|
|
196
|
+
return result.status === 0 && Boolean((result.stdout || '').trim());
|
|
197
|
+
};
|
|
198
|
+
const detectCodexDesktopOverride = () => {
|
|
199
|
+
const value = process.env.FRAIM_DETECT_CODEX_DESKTOP_OVERRIDE;
|
|
200
|
+
if (value === '1')
|
|
201
|
+
return true;
|
|
202
|
+
if (value === '0')
|
|
203
|
+
return false;
|
|
204
|
+
return null;
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* Issue #1478 R1: detects the Codex view inside OpenAI's ChatGPT desktop app,
|
|
208
|
+
* independent of IDE_CONFIGS (see comment above). Linux is not yet
|
|
209
|
+
* implemented — the app is a public preview (Aug 2026) with no confirmed
|
|
210
|
+
* package identity (spec Open Question #1); this is a deliberate, documented
|
|
211
|
+
* scope boundary, not an oversight.
|
|
212
|
+
*/
|
|
213
|
+
const detectCodexDesktop = () => {
|
|
214
|
+
const override = detectCodexDesktopOverride();
|
|
215
|
+
if (override !== null)
|
|
216
|
+
return override;
|
|
217
|
+
if (process.platform === 'darwin')
|
|
218
|
+
return detectCodexDesktopMac();
|
|
219
|
+
if (process.platform === 'win32')
|
|
220
|
+
return detectCodexDesktopWindows();
|
|
221
|
+
return false;
|
|
222
|
+
};
|
|
223
|
+
exports.detectCodexDesktop = detectCodexDesktop;
|
|
154
224
|
const detectGrokSurface = () => {
|
|
155
225
|
const paths = [
|
|
156
226
|
'~/.grok',
|