fraim 2.0.301 → 2.0.303
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/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 +3 -3
- package/dist/src/core/ai-mentor.js +3 -1
- package/dist/src/core/quality-evidence.js +10 -0
- package/dist/src/first-run/session-service.js +37 -7
- package/dist/src/local-mcp-server/stdio-server.js +12 -0
- package/dist/src/middleware/auth.js +10 -1
- package/dist/src/services/demo-seed-service.js +1 -0
- package/package.json +1 -1
- 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.
|
|
@@ -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',
|
|
@@ -14,6 +14,11 @@ exports.getNpmGlobalBinDirsFromPrefix = getNpmGlobalBinDirsFromPrefix;
|
|
|
14
14
|
exports.resolveNpmGlobalBinDirs = resolveNpmGlobalBinDirs;
|
|
15
15
|
exports.buildPathWithManagedAgentBins = buildPathWithManagedAgentBins;
|
|
16
16
|
exports.appendManagedAgentBinDirsToProcessPath = appendManagedAgentBinDirsToProcessPath;
|
|
17
|
+
exports.__setLoginShellPathResolverForTests = __setLoginShellPathResolverForTests;
|
|
18
|
+
exports.resolveLoginShellPathCached = resolveLoginShellPathCached;
|
|
19
|
+
exports.__clearLoginShellPathCacheForTests = __clearLoginShellPathCacheForTests;
|
|
20
|
+
exports.buildRecoveredAgentPath = buildRecoveredAgentPath;
|
|
21
|
+
exports.versionFromProbeOutput = versionFromProbeOutput;
|
|
17
22
|
const fs_1 = __importDefault(require("fs"));
|
|
18
23
|
const path_1 = __importDefault(require("path"));
|
|
19
24
|
const child_process_1 = require("child_process");
|
|
@@ -151,9 +156,15 @@ function buildChildEnv(env) {
|
|
|
151
156
|
return childEnv;
|
|
152
157
|
}
|
|
153
158
|
function resolveNpmGlobalBinDirs(basePath, env) {
|
|
159
|
+
// Issue #1256: set both PATH and Path when overriding. Node's process.env is a
|
|
160
|
+
// case-insensitive view on win32, but a plain object built from `{...process.env}` can
|
|
161
|
+
// carry both casings as distinct own keys — overriding only `PATH` risks leaving a
|
|
162
|
+
// stale `Path` (with the pre-override value) also present in the child's environment
|
|
163
|
+
// block, which environment-lookup on Windows may prefer over the intended override.
|
|
164
|
+
// Mirrors the same defensive pattern hosts.ts's versionProbeEnv() already uses.
|
|
154
165
|
const childEnv = buildChildEnv({
|
|
155
166
|
...env,
|
|
156
|
-
...(basePath === undefined ? {} : { PATH: basePath }),
|
|
167
|
+
...(basePath === undefined ? {} : { PATH: basePath, Path: basePath }),
|
|
157
168
|
});
|
|
158
169
|
const executable = process.platform === 'win32' ? 'cmd.exe' : 'npm';
|
|
159
170
|
const args = process.platform === 'win32'
|
|
@@ -176,3 +187,174 @@ function buildPathWithManagedAgentBins(basePath) {
|
|
|
176
187
|
function appendManagedAgentBinDirsToProcessPath() {
|
|
177
188
|
process.env.PATH = buildPathWithManagedAgentBins(process.env.PATH);
|
|
178
189
|
}
|
|
190
|
+
// ─── Issue #1256 (slice a): location-agnostic PATH recovery ─────────────────
|
|
191
|
+
//
|
|
192
|
+
// A Hub launched from Launchpad/Finder/a Windows shortcut inherits the OS's minimal
|
|
193
|
+
// GUI-launch PATH, not the user's shell PATH. `resolveNpmGlobalBinDirs()` above already
|
|
194
|
+
// recovers the npm-global case, but it is structurally blind to anything installed outside
|
|
195
|
+
// npm — Claude Code's native macOS installer (`~/.local/bin/claude`), Homebrew, or a vendor
|
|
196
|
+
// installer like Antigravity's `agy` (no npm package at all, `src/first-run/types.ts`
|
|
197
|
+
// `installPackage: ''`). Per the 2026-08-27 product decision, recovery must be
|
|
198
|
+
// location-agnostic: it finds whatever the user's own login shell would find, not a list of
|
|
199
|
+
// known install roots (which fails the next vendor's convention by construction).
|
|
200
|
+
//
|
|
201
|
+
// Mechanism: spawn the user's login shell non-interactively (`-lc`, explicitly sourcing the
|
|
202
|
+
// common rc files itself rather than asking the shell to behave interactively — see
|
|
203
|
+
// defaultResolveLoginShellPath) and read back `$PATH` between two markers, so any prompt/
|
|
204
|
+
// motd/rc-file noise on stdout cannot be mistaken for the PATH itself. VS Code's own
|
|
205
|
+
// `resolveShellEnv` uses `-ilc`; this repo's compliance requirement (SOC 2 CC6.1 / ISO 27001
|
|
206
|
+
// A.8.19) is stricter and asks for non-interactive, so this achieves the same practical
|
|
207
|
+
// coverage (PATH exports in `~/.bashrc`/`~/.zshrc`) without it. POSIX only — Windows GUI
|
|
208
|
+
// launches already inherit the registry-composed PATH, so this returns null immediately on
|
|
209
|
+
// win32 rather than attempting a login-shell equivalent that doesn't exist.
|
|
210
|
+
const LOGIN_SHELL_PATH_TIMEOUT_MS = 3000;
|
|
211
|
+
function defaultResolveLoginShellPath() {
|
|
212
|
+
if (process.platform === 'win32')
|
|
213
|
+
return null;
|
|
214
|
+
const shell = process.env.SHELL || '/bin/bash';
|
|
215
|
+
const marker = `__fraim_1256_shell_path_${process.pid}__`;
|
|
216
|
+
// `-lc`, not `-ilc`: login + command, deliberately NOT interactive (SOC 2 CC6.1 / ISO 27001
|
|
217
|
+
// A.8.19 — the spec's compliance section requires a non-interactive invocation). A plain
|
|
218
|
+
// `-lc` alone would still miss PATH exports that live in `~/.bashrc`/`~/.zshrc` — bash/zsh
|
|
219
|
+
// source those only for an interactive shell by design, which is what `-i` exists for. This
|
|
220
|
+
// command explicitly sources both common rc files itself (a no-op if a file doesn't exist or
|
|
221
|
+
// its syntax doesn't apply to the running shell — errors are swallowed, never abort the
|
|
222
|
+
// script) instead of asking the shell to behave interactively to get the same effect. That
|
|
223
|
+
// keeps the shell itself non-interactive while still recovering the common real-world case.
|
|
224
|
+
const script = [
|
|
225
|
+
'for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do [ -f "$rc" ] && . "$rc" >/dev/null 2>&1; done',
|
|
226
|
+
`echo "${marker}"`,
|
|
227
|
+
'printf \'%s\' "$PATH"',
|
|
228
|
+
'echo',
|
|
229
|
+
`echo "${marker}"`,
|
|
230
|
+
].join('; ');
|
|
231
|
+
try {
|
|
232
|
+
const result = (0, child_process_1.spawnSync)(shell, ['-lc', script], {
|
|
233
|
+
encoding: 'utf8',
|
|
234
|
+
timeout: LOGIN_SHELL_PATH_TIMEOUT_MS,
|
|
235
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
236
|
+
});
|
|
237
|
+
// Bounded and best-effort (AC-A5): any failure, non-zero exit, or timeout degrades to
|
|
238
|
+
// "no recovery" rather than throwing or blocking the caller's own PATH resolution.
|
|
239
|
+
if (result.status !== 0 || result.error)
|
|
240
|
+
return null;
|
|
241
|
+
const output = result.stdout || '';
|
|
242
|
+
const firstIdx = output.indexOf(marker);
|
|
243
|
+
if (firstIdx === -1)
|
|
244
|
+
return null;
|
|
245
|
+
const afterFirst = output.slice(firstIdx + marker.length);
|
|
246
|
+
const secondIdx = afterFirst.indexOf(marker);
|
|
247
|
+
const section = (secondIdx === -1 ? afterFirst : afterFirst.slice(0, secondIdx)).trim();
|
|
248
|
+
return section || null;
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
let loginShellPathResolverOverride = null;
|
|
255
|
+
/**
|
|
256
|
+
* Test seam only. Injects a fake login-shell PATH resolver so AC-A2/AC-A6 are provable
|
|
257
|
+
* without a real POSIX shell or real rc files — including on Windows CI, where the real
|
|
258
|
+
* mechanism is a no-op. Pass null to restore the real resolver. Also clears the memoized
|
|
259
|
+
* result so the next call recomputes under the injected resolver.
|
|
260
|
+
*/
|
|
261
|
+
function __setLoginShellPathResolverForTests(resolver) {
|
|
262
|
+
loginShellPathResolverOverride = resolver;
|
|
263
|
+
cachedLoginShellPath = undefined;
|
|
264
|
+
cachedNpmGlobalBinDirsForRecovery = undefined;
|
|
265
|
+
}
|
|
266
|
+
let cachedLoginShellPath;
|
|
267
|
+
/**
|
|
268
|
+
* Resolves and memoizes the PATH the user's own login shell would produce. Computed at most
|
|
269
|
+
* once per process (AC-A5: bounded, fixed cost) — this feeds every agent-resolution call, so
|
|
270
|
+
* recomputing per call would multiply one shell spawn by the number of agents probed.
|
|
271
|
+
*/
|
|
272
|
+
function resolveLoginShellPathCached() {
|
|
273
|
+
if (cachedLoginShellPath !== undefined)
|
|
274
|
+
return cachedLoginShellPath;
|
|
275
|
+
const resolver = loginShellPathResolverOverride ?? defaultResolveLoginShellPath;
|
|
276
|
+
// AC-A5: a resolver failing must degrade to "no recovery", never propagate. The default
|
|
277
|
+
// resolver already guards its own spawnSync internally; this also protects against an
|
|
278
|
+
// injected test resolver (or any future resolver) that throws instead of returning null.
|
|
279
|
+
try {
|
|
280
|
+
cachedLoginShellPath = resolver();
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
cachedLoginShellPath = null;
|
|
284
|
+
}
|
|
285
|
+
return cachedLoginShellPath;
|
|
286
|
+
}
|
|
287
|
+
/** Test seam only. Clears the memoized login-shell PATH so the next call recomputes. */
|
|
288
|
+
function __clearLoginShellPathCacheForTests() {
|
|
289
|
+
cachedLoginShellPath = undefined;
|
|
290
|
+
}
|
|
291
|
+
let cachedNpmGlobalBinDirsForRecovery;
|
|
292
|
+
/**
|
|
293
|
+
* Memoized wrapper around resolveNpmGlobalBinDirs(), scoped to buildRecoveredAgentPath()'s
|
|
294
|
+
* own use only — NOT a change to resolveNpmGlobalBinDirs() itself, which fraim doctor and the
|
|
295
|
+
* install route still call directly and must keep getting a fresh, uncached answer (doctor's
|
|
296
|
+
* whole purpose is detecting drift after a `codex update`; a stale cached answer would defeat
|
|
297
|
+
* that).
|
|
298
|
+
*
|
|
299
|
+
* Bug this fixes (issue #1256, AC-A5): buildRecoveredAgentPath() is called once PER AGENT
|
|
300
|
+
* inside detectEmployeesAsync()'s concurrent probe round (hosts.ts). Without this cache,
|
|
301
|
+
* resolveNpmGlobalBinDirs()'s spawnSync (bounded at 5s) re-ran synchronously for every agent,
|
|
302
|
+
* and because each call sits inside a Promise executor that runs synchronously up to its own
|
|
303
|
+
* spawnSync, the "concurrent" probes actually serialized behind it — up to 5 agents × 5s of
|
|
304
|
+
* blocking spawnSync is exactly the event-loop-freezing regression issue #1010 eliminated,
|
|
305
|
+
* reintroduced one layer deeper. Memoizing once per process (like resolveLoginShellPathCached
|
|
306
|
+
* above) restores the bound this repo's own #1010 postmortem requires.
|
|
307
|
+
*/
|
|
308
|
+
function resolveNpmGlobalBinDirsCached(basePath) {
|
|
309
|
+
if (cachedNpmGlobalBinDirsForRecovery !== undefined)
|
|
310
|
+
return cachedNpmGlobalBinDirsForRecovery;
|
|
311
|
+
try {
|
|
312
|
+
cachedNpmGlobalBinDirsForRecovery = resolveNpmGlobalBinDirs(basePath);
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
cachedNpmGlobalBinDirsForRecovery = [];
|
|
316
|
+
}
|
|
317
|
+
return cachedNpmGlobalBinDirsForRecovery;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Issue #1256 (AC-A3): the single shared PATH-recovery helper every agent-resolution call
|
|
321
|
+
* site routes through — the version probe (`hosts.ts`), the install route's existing-version
|
|
322
|
+
* check and `check-agent` (`server.ts`), and `fraim doctor`'s health check
|
|
323
|
+
* (`agent-cli-health-checks.ts`). One implementation means a CLI resolved by any one of these
|
|
324
|
+
* is resolved by all of them; four independent reimplementations is exactly how #1375 shipped
|
|
325
|
+
* a Windows-only PATH bug that stayed invisible on POSIX.
|
|
326
|
+
*
|
|
327
|
+
* Order: strip FRAIM's managed dirs and any project-local `node_modules/.bin` (AC-A4) →
|
|
328
|
+
* layer in the recovered login-shell PATH (AC-A2/AC-A6, location-agnostic by construction) →
|
|
329
|
+
* strip project-local bins again (a hostile/unusual shell rc could reintroduce one) → layer
|
|
330
|
+
* in npm-global bin dirs resolved against that recovered PATH (covers the npm case even where
|
|
331
|
+
* a login shell is unavailable) → append FRAIM's managed agent bins last, unconditionally.
|
|
332
|
+
*/
|
|
333
|
+
function buildRecoveredAgentPath(basePath) {
|
|
334
|
+
const withoutManaged = stripManagedAgentBinDirsFromPath(basePath);
|
|
335
|
+
const withoutProjectBins = stripProjectLocalNodeBinDirs(withoutManaged);
|
|
336
|
+
const loginShellPath = resolveLoginShellPathCached();
|
|
337
|
+
const loginShellEntries = loginShellPath ? loginShellPath.split(path_1.default.delimiter).filter(Boolean) : [];
|
|
338
|
+
const withLoginShell = loginShellEntries.length > 0
|
|
339
|
+
? appendBinDirsToPath(withoutProjectBins, loginShellEntries)
|
|
340
|
+
: withoutProjectBins;
|
|
341
|
+
const withLoginShellClean = stripProjectLocalNodeBinDirs(withLoginShell);
|
|
342
|
+
const npmGlobalBinDirs = resolveNpmGlobalBinDirsCached(withLoginShellClean);
|
|
343
|
+
const withNpmGlobal = appendBinDirsToPath(withLoginShellClean, npmGlobalBinDirs);
|
|
344
|
+
return appendBinDirsToPath(withNpmGlobal, getManagedAgentBinDirs());
|
|
345
|
+
}
|
|
346
|
+
// ─── Issue #1256 (slice d): trust stdout, not stderr, for a version answer ───
|
|
347
|
+
//
|
|
348
|
+
// GitHub Copilot CLI's current failure mode is `--version` exiting 0 while printing an error
|
|
349
|
+
// to stderr. `(stdout || stderr || '').trim()` — the pattern this replaces, previously
|
|
350
|
+
// duplicated in hosts.ts, server.ts, and agent-cli-health-checks.ts — read that stderr text
|
|
351
|
+
// as a version string and reported the agent ready. Only stdout counts as a version. This is
|
|
352
|
+
// the mirror image of #1355's `isResumableBackgroundTaskType`, which fails OPEN on an ABSENT
|
|
353
|
+
// field (silence is a reporting gap, not evidence). stderr output on exit 0 is not silence —
|
|
354
|
+
// it is positive evidence the CLI is broken — so this fails CLOSED. The two rules are
|
|
355
|
+
// consistent: infer nothing from an absent signal, believe an error when you get one.
|
|
356
|
+
function versionFromProbeOutput(stdout, stderr) {
|
|
357
|
+
void stderr; // never trusted as a version string — kept as a parameter so call sites read as before, not silently dropping a value
|
|
358
|
+
const version = (stdout || '').trim();
|
|
359
|
+
return version || null;
|
|
360
|
+
}
|
|
@@ -71,8 +71,8 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
|
|
|
71
71
|
qasm: {
|
|
72
72
|
personaKey: 'qasm',
|
|
73
73
|
bundleId: 'persona-qasm-core',
|
|
74
|
-
catalogMetadata: buildCatalogMetadata('qasm', ['test-
|
|
75
|
-
protectedJobs: ['test-
|
|
74
|
+
catalogMetadata: buildCatalogMetadata('qasm', ['test-authoring', 'browser-application-validation', 'ui-polish-validation']),
|
|
75
|
+
protectedJobs: ['test-authoring', 'browser-application-validation', 'ui-polish-validation', 'code-quality-assessment', 'test-quality-assessment', 'user-testing-and-bug-bash', 'broken-windows-detection-and-remediation', 'iterative-quality-improvement', 'accessibility-audit', 'api-testing', 'performance-benchmarking'],
|
|
76
76
|
protectedAliases: ['qa', 'quality-assurance'],
|
|
77
77
|
defaultHireMode: 'job',
|
|
78
78
|
lockCopy: 'Hire QAsm to unlock QA validation for this request.'
|
|
@@ -81,7 +81,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
|
|
|
81
81
|
personaKey: 'huxley',
|
|
82
82
|
bundleId: 'persona-huxley-core',
|
|
83
83
|
catalogMetadata: buildCatalogMetadata('huxley', ['design-system-creation', 'user-facing-prototyping', 'ux-research-synthesis']),
|
|
84
|
-
protectedJobs: ['design-system-creation', 'user-facing-prototyping', 'website-creation', 'brand-creation', 'branding-quality-audit', 'ux-research-synthesis', 'user-journey-mapping'],
|
|
84
|
+
protectedJobs: ['design-system-creation', 'user-facing-prototyping', 'website-creation', 'brand-creation', 'branding-quality-audit', 'ux-research-synthesis', 'user-journey-mapping', 'ui-quality-assessment'],
|
|
85
85
|
protectedAliases: ['ux-design', 'brand-design'],
|
|
86
86
|
defaultHireMode: 'job',
|
|
87
87
|
lockCopy: 'Hire hUXley to unlock design-system, UX research, and brand design work for this request.'
|
|
@@ -177,7 +177,9 @@ class AIMentor {
|
|
|
177
177
|
message += [
|
|
178
178
|
'### Waiting For Next Invocation',
|
|
179
179
|
'',
|
|
180
|
-
'Send a concise status update that this recurring assignment is waiting for its next scheduled check, then stop.'
|
|
180
|
+
'Send a concise status update that this recurring assignment is waiting for its next scheduled check, then stop.',
|
|
181
|
+
'',
|
|
182
|
+
'<!-- FRAIM_RECURRING_PARK -->',
|
|
181
183
|
].join('\n');
|
|
182
184
|
}
|
|
183
185
|
else if (nextPhaseId) {
|
|
@@ -39,6 +39,8 @@ exports.QUALITY_REGISTRY = {
|
|
|
39
39
|
'branding-quality-audit': { stage: 'branding', enforced: true, telemetryKind: 'score' },
|
|
40
40
|
// Product Quality
|
|
41
41
|
'code-quality-assessment': { stage: 'product-quality', enforced: true, telemetryKind: 'score' },
|
|
42
|
+
// UI/UX Quality
|
|
43
|
+
'ui-quality-assessment': { stage: 'ui-ux-quality', enforced: true, telemetryKind: 'score' },
|
|
42
44
|
// Test Quality
|
|
43
45
|
'test-quality-assessment': { stage: 'test-quality', enforced: true, telemetryKind: 'score' },
|
|
44
46
|
// Security
|
|
@@ -76,6 +78,7 @@ exports.STAGE_DISPLAY_NAMES = {
|
|
|
76
78
|
'business-strategy': 'Business Strategy',
|
|
77
79
|
'branding': 'Branding',
|
|
78
80
|
'product-quality': 'Product Quality',
|
|
81
|
+
'ui-ux-quality': 'UI/UX Quality',
|
|
79
82
|
'test-quality': 'Test Quality',
|
|
80
83
|
'security': 'Security',
|
|
81
84
|
'production-readiness': 'Production Readiness',
|
|
@@ -90,6 +93,7 @@ exports.ALL_STAGE_CATEGORIES = [
|
|
|
90
93
|
'business-strategy',
|
|
91
94
|
'branding',
|
|
92
95
|
'product-quality',
|
|
96
|
+
'ui-ux-quality',
|
|
93
97
|
'test-quality',
|
|
94
98
|
'security',
|
|
95
99
|
'production-readiness',
|
|
@@ -153,6 +157,12 @@ const QUALITY_SCORE_DIMENSIONS = {
|
|
|
153
157
|
'architecture',
|
|
154
158
|
'maintainability'
|
|
155
159
|
],
|
|
160
|
+
'ui-quality-assessment': [
|
|
161
|
+
'lookAndFeel',
|
|
162
|
+
'usabilityHeuristics',
|
|
163
|
+
'interactionConsistency',
|
|
164
|
+
'accessibility'
|
|
165
|
+
],
|
|
156
166
|
'test-quality-assessment': [
|
|
157
167
|
'coverage',
|
|
158
168
|
'testIntegrity',
|
|
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.FIRST_RUN_ROW_IDS = exports.FirstRunSessionService = void 0;
|
|
40
|
+
exports.commandVersion = commandVersion;
|
|
40
41
|
exports.buildPersistShellPathWindowsCommand = buildPersistShellPathWindowsCommand;
|
|
41
42
|
exports.buildFirstRunHubLaunchArgs = buildFirstRunHubLaunchArgs;
|
|
42
43
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -71,18 +72,18 @@ function commandVersion(command, extraBinDirs, basePath) {
|
|
|
71
72
|
? ['/d', '/s', '/c', `${command} --version`]
|
|
72
73
|
: ['--version'];
|
|
73
74
|
const pathValue = extraBinDirs && extraBinDirs.length > 0
|
|
74
|
-
? (0, managed_agent_paths_1.appendBinDirsToPath)(basePath ?? process.env.PATH, extraBinDirs)
|
|
75
|
+
? (0, managed_agent_paths_1.appendBinDirsToPath)(basePath ?? process.env.PATH ?? process.env.Path, extraBinDirs)
|
|
75
76
|
: basePath;
|
|
76
|
-
const
|
|
77
|
+
const recoveredPath = (0, managed_agent_paths_1.buildRecoveredAgentPath)(pathValue ?? process.env.PATH ?? process.env.Path);
|
|
78
|
+
const env = { ...process.env, PATH: recoveredPath, Path: recoveredPath };
|
|
77
79
|
const result = (0, child_process_1.spawnSync)(executable, args, {
|
|
78
80
|
encoding: 'utf8',
|
|
79
81
|
timeout: 5000,
|
|
80
|
-
|
|
82
|
+
env,
|
|
81
83
|
});
|
|
82
|
-
if (result.status !== 0)
|
|
84
|
+
if (result.status !== 0 || result.error)
|
|
83
85
|
return null;
|
|
84
|
-
|
|
85
|
-
return text;
|
|
86
|
+
return (0, managed_agent_paths_1.versionFromProbeOutput)(result.stdout, result.stderr);
|
|
86
87
|
}
|
|
87
88
|
function ensureOutputDirs() {
|
|
88
89
|
fs_1.default.mkdirSync((0, script_sync_utils_1.getUserFraimDir)(), { recursive: true });
|
|
@@ -259,12 +260,25 @@ function buildRunnableAgentSurfaces() {
|
|
|
259
260
|
const cliRunnableTypes = new Set((0, ide_detector_1.detectInstalledIDEs)('cli-runnable').map((ide) => ide.configType));
|
|
260
261
|
const ides = configSurface.filter((ide) => !ide_detector_1.CLI_PROBE_CONFIGTYPES.has(ide.configType) || cliRunnableTypes.has(ide.configType));
|
|
261
262
|
const hints = (0, ide_global_integration_1.describeOnboardingInvocationSurfaces)(ides);
|
|
262
|
-
|
|
263
|
+
const surfaces = ides.map((ide, index) => ({
|
|
263
264
|
id: ide.configType,
|
|
264
265
|
name: ide.name,
|
|
265
266
|
invocationHint: hints[index] || '/fraim onboard this project',
|
|
266
267
|
status: 'configured',
|
|
267
268
|
}));
|
|
269
|
+
// Issue #1478 R1/R1.2: Codex Desktop is detected standalone (not an
|
|
270
|
+
// IDE_CONFIGS entry — see ide-detector.ts) and spliced in here so it flows
|
|
271
|
+
// straight to detectedAgentCount() without ever touching the
|
|
272
|
+
// CLI_PROBE_CONFIGTYPES gate above.
|
|
273
|
+
if ((0, ide_detector_1.detectCodexDesktop)()) {
|
|
274
|
+
surfaces.push({
|
|
275
|
+
id: 'codex-desktop',
|
|
276
|
+
name: 'Codex Desktop',
|
|
277
|
+
invocationHint: 'Codex Desktop: onboard this project',
|
|
278
|
+
status: 'configured',
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return surfaces;
|
|
268
282
|
}
|
|
269
283
|
function surfaceForAgent(option) {
|
|
270
284
|
return {
|
|
@@ -523,12 +537,28 @@ class FirstRunSessionService {
|
|
|
523
537
|
this.detectRowsOnLoad();
|
|
524
538
|
this.persist();
|
|
525
539
|
const detectedNames = new Set((this.state.setupResult?.configuredSurfaces ?? []).map((s) => s.name));
|
|
540
|
+
// Issue #1478 R2: one unified list. `agentOptionId` is set when this
|
|
541
|
+
// product also has a working Set up flow (matched by exact label against
|
|
542
|
+
// FIRST_RUN_AGENT_OPTIONS) — the client renders "Set up" instead of
|
|
543
|
+
// "Download" for those rows, which is what lets the old two-section split
|
|
544
|
+
// ("Supported AI tools and IDEs" + "Hub-ready CLI agents") retire into one.
|
|
526
545
|
const supportedAgents = ide_detector_1.IDE_CONFIGS
|
|
527
546
|
.map((ide) => ({
|
|
528
547
|
name: ide.name,
|
|
529
548
|
detected: detectedNames.has(ide.name),
|
|
530
549
|
downloadUrl: ide.downloadUrl ?? null,
|
|
550
|
+
agentOptionId: types_1.FIRST_RUN_AGENT_OPTIONS.find((option) => option.label === ide.name)?.id ?? null,
|
|
531
551
|
}));
|
|
552
|
+
// Codex Desktop is not an IDE_CONFIGS entry (see ide-detector.ts) but must
|
|
553
|
+
// still appear as a peer row; insert it next to "Codex" per the approved
|
|
554
|
+
// mock's row order.
|
|
555
|
+
const codexIndex = supportedAgents.findIndex((agent) => agent.name === 'Codex');
|
|
556
|
+
supportedAgents.splice(codexIndex === -1 ? supportedAgents.length : codexIndex + 1, 0, {
|
|
557
|
+
name: 'Codex Desktop',
|
|
558
|
+
detected: detectedNames.has('Codex Desktop'),
|
|
559
|
+
downloadUrl: ide_detector_1.CODEX_DESKTOP_DOWNLOAD_URL,
|
|
560
|
+
agentOptionId: null,
|
|
561
|
+
});
|
|
532
562
|
return {
|
|
533
563
|
state: this.state,
|
|
534
564
|
rows: this.state.rows,
|
|
@@ -1527,6 +1527,18 @@ class FraimLocalMCPServer {
|
|
|
1527
1527
|
if (typeof jobId !== 'string' || !jobId.trim())
|
|
1528
1528
|
continue;
|
|
1529
1529
|
const normalizedJobId = jobId.trim();
|
|
1530
|
+
// Issue #1504: a category-qualified jobId (e.g. "ai-employee/product-building/x")
|
|
1531
|
+
// can resolve through getJobOverview -> LocalRegistryResolver.getJob's lenient
|
|
1532
|
+
// local-override/remote fallback, even though every real catalog job id is a bare
|
|
1533
|
+
// filename slug (src/ai-hub/catalog.ts: `id: fileName`) and the Hub UI's
|
|
1534
|
+
// resolveNextJobFromCatalog() only ever matches that bare slug with strict equality
|
|
1535
|
+
// (public/ai-hub/script.js). Reject the path-qualified shape directly, before
|
|
1536
|
+
// trusting the resolver, so this class of malformed jobId cannot pass here and then
|
|
1537
|
+
// silently render as a disabled chip in the Hub.
|
|
1538
|
+
if (normalizedJobId.includes('/') || normalizedJobId.includes('\\')) {
|
|
1539
|
+
errors.push(`evidence.nextJobRecommendations[${i}].jobId must be the bare job slug from the catalog (e.g. "feature-implementation"), not a category-qualified path (got "${normalizedJobId}")`);
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1530
1542
|
const job = await mentor.getJobOverview(normalizedJobId);
|
|
1531
1543
|
if (!job) {
|
|
1532
1544
|
errors.push(`evidence.nextJobRecommendations[${i}].jobId must reference a known FRAIM job (got "${normalizedJobId}")`);
|
|
@@ -198,7 +198,16 @@ class AuthMiddleware {
|
|
|
198
198
|
const strictTestAuth = typeof authMode === 'string' && authMode.toLowerCase() === 'strict';
|
|
199
199
|
const isTestBypass = process.env.NODE_ENV === 'test' && !this.skipTestBypass && !strictTestAuth && apiKey !== 'invalid-key';
|
|
200
200
|
const isLocalDev = apiKey === 'local-dev' && process.env.NODE_ENV !== 'production';
|
|
201
|
-
|
|
201
|
+
let testSeededApiKeyData = null;
|
|
202
|
+
if (isTestBypass) {
|
|
203
|
+
try {
|
|
204
|
+
testSeededApiKeyData = await this.dbService.verifyApiKey(apiKey);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
testSeededApiKeyData = null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (isLocalDev || (isTestBypass && !testSeededApiKeyData)) {
|
|
202
211
|
req.apiKeyData = {
|
|
203
212
|
key: apiKey,
|
|
204
213
|
userId: isLocalDev ? 'local-dev-user' : 'test-user',
|
|
@@ -104,6 +104,7 @@ async function seedDemoDataForUser(dbService, userId) {
|
|
|
104
104
|
{ category: 'business-strategy', jobName: 'review-business-strategy' },
|
|
105
105
|
{ category: 'branding', jobName: 'branding-quality-audit' },
|
|
106
106
|
{ category: 'product-quality', jobName: 'code-quality-assessment' },
|
|
107
|
+
{ category: 'ui-ux-quality', jobName: 'ui-quality-assessment' },
|
|
107
108
|
{ category: 'test-quality', jobName: 'test-quality-assessment' },
|
|
108
109
|
{ category: 'security', jobName: 'security-review' },
|
|
109
110
|
{ category: 'production-readiness', jobName: 'production-readiness-review' },
|
package/package.json
CHANGED
|
@@ -92,12 +92,8 @@
|
|
|
92
92
|
return String(name || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
? state.session.state.agentInstalls[opt.id]
|
|
98
|
-
: null;
|
|
99
|
-
if (installState && installState.status === 'ready') return true;
|
|
100
|
-
return configuredSurfaces().some((surface) => surface && (surface.id === opt.id || surface.name === opt.label));
|
|
95
|
+
function findAgentOptionById(agentOptionId) {
|
|
96
|
+
return (state.session && state.session.agentOptions || []).find((opt) => opt.id === agentOptionId) || null;
|
|
101
97
|
}
|
|
102
98
|
|
|
103
99
|
function detectedAgentCount() {
|
|
@@ -244,7 +240,7 @@
|
|
|
244
240
|
|
|
245
241
|
const catalogCopy = document.createElement('p');
|
|
246
242
|
catalogCopy.className = 'pane-copy';
|
|
247
|
-
catalogCopy.textContent = 'FRAIM can configure every supported local agent or IDE it detects.
|
|
243
|
+
catalogCopy.textContent = 'FRAIM can configure every supported local agent or IDE it detects. "Set up" installs it for you; "Download" opens the developer\'s own install page.';
|
|
248
244
|
pane.appendChild(catalogCopy);
|
|
249
245
|
|
|
250
246
|
const list = document.createElement('ul');
|
|
@@ -271,6 +267,15 @@
|
|
|
271
267
|
badge.setAttribute('data-testid', `supported-agent-installed-${slug}`);
|
|
272
268
|
badge.textContent = 'Installed';
|
|
273
269
|
actionSpan.appendChild(badge);
|
|
270
|
+
} else if (agent.agentOptionId) {
|
|
271
|
+
// Issue #1478 R2: this product also has a working install/sign-in/check
|
|
272
|
+
// flow — route to it instead of a static Download link so there is no
|
|
273
|
+
// row left that could dead-end.
|
|
274
|
+
const opt = findAgentOptionById(agent.agentOptionId);
|
|
275
|
+
const setupBtn = button('Set up', 'secondary');
|
|
276
|
+
setupBtn.setAttribute('data-testid', `supported-agent-setup-${agent.agentOptionId}`);
|
|
277
|
+
setupBtn.addEventListener('click', () => { if (opt) openAgentModal(opt); });
|
|
278
|
+
actionSpan.appendChild(setupBtn);
|
|
274
279
|
} else if (agent.downloadUrl) {
|
|
275
280
|
const link = document.createElement('a');
|
|
276
281
|
link.className = 'agent-download-link';
|
|
@@ -303,52 +308,15 @@
|
|
|
303
308
|
});
|
|
304
309
|
pane.appendChild(rescanBtn);
|
|
305
310
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const hubCopy = document.createElement('p');
|
|
312
|
-
hubCopy.className = 'pane-copy';
|
|
313
|
-
hubCopy.textContent = 'FRAIM Hub runs jobs through CLI agents. Set up at least one of these if you plan to launch jobs in Hub.';
|
|
314
|
-
pane.appendChild(hubCopy);
|
|
315
|
-
|
|
316
|
-
const grid = document.createElement('div');
|
|
317
|
-
grid.className = 'agent-grid';
|
|
318
|
-
const agentOptions = state.session.agentOptions || [];
|
|
319
|
-
for (const opt of agentOptions) {
|
|
320
|
-
const card = document.createElement('div');
|
|
321
|
-
card.className = 'user-type-card recruit-card';
|
|
322
|
-
card.setAttribute('data-agent-id', opt.id);
|
|
323
|
-
card.setAttribute('data-testid', `agent-card-${opt.id}`);
|
|
324
|
-
const agentReady = isAgentReady(opt);
|
|
325
|
-
|
|
326
|
-
const title = document.createElement('strong');
|
|
327
|
-
title.className = 'card-title';
|
|
328
|
-
title.textContent = opt.label;
|
|
329
|
-
|
|
330
|
-
const desc = document.createElement('p');
|
|
331
|
-
desc.className = 'card-desc';
|
|
332
|
-
desc.textContent = agentReady
|
|
333
|
-
? 'Already installed and ready for FRAIM.'
|
|
334
|
-
: `Available to set up: install, sign in, and verify ${opt.label}.`;
|
|
335
|
-
|
|
336
|
-
const action = button(agentReady ? 'Ready' : 'Set up', 'secondary');
|
|
337
|
-
action.disabled = agentReady;
|
|
338
|
-
action.setAttribute('data-testid', `install-${opt.id}`);
|
|
339
|
-
action.addEventListener('click', () => openAgentModal(opt));
|
|
340
|
-
|
|
341
|
-
card.appendChild(title);
|
|
342
|
-
card.appendChild(desc);
|
|
343
|
-
card.appendChild(action);
|
|
344
|
-
grid.appendChild(card);
|
|
345
|
-
}
|
|
346
|
-
|
|
311
|
+
// Issue #1478 R2: the old "Hub-ready CLI agents" heading/copy/card-grid is
|
|
312
|
+
// retired — every product it used to list (Claude Code, Codex, Gemini CLI,
|
|
313
|
+
// GitHub Copilot CLI) now renders as a "Set up" row in the unified list
|
|
314
|
+
// above via agent.agentOptionId, with the identical install/sign-in/check
|
|
315
|
+
// modal. Bring Your Own Agent still renders, now standalone.
|
|
347
316
|
const byoa = document.createElement('div');
|
|
348
317
|
byoa.className = 'user-type-card recruit-card byoa-card';
|
|
349
|
-
byoa.innerHTML = '<strong class="card-title">Bring Your Own Agent</strong><p class="card-desc">Use
|
|
350
|
-
|
|
351
|
-
pane.appendChild(grid);
|
|
318
|
+
byoa.innerHTML = '<strong class="card-title">Bring Your Own Agent</strong><p class="card-desc">Use Windsurf, Kiro, or another supported local AI tool not listed above. If you install a new agent later and want FRAIM to use it, run this command from your project.</p><div class="cmd-block">npx fraim add-ide</div>';
|
|
319
|
+
pane.appendChild(byoa);
|
|
352
320
|
|
|
353
321
|
const done = button('Next', 'primary');
|
|
354
322
|
done.setAttribute('data-testid', 'done-installing-agents');
|