fraim 2.0.134 → 2.0.136
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/ai-hub/catalog.js +7 -1
- package/dist/src/ai-hub/hosts.js +32 -3
- package/dist/src/ai-hub/preferences.js +3 -3
- package/dist/src/ai-hub/server.js +165 -8
- package/dist/src/cli/setup/ide-detector.js +1 -1
- package/dist/src/cli/setup/ide-global-integration.js +4 -0
- package/dist/src/cli/setup/ide-invocation-surfaces.js +6 -0
- package/dist/src/core/fraim-config-schema.generated.js +38 -1
- package/dist/src/first-run/server.js +38 -0
- package/dist/src/first-run/session-service.js +142 -14
- package/dist/src/first-run/types.js +7 -5
- package/index.js +1 -1
- package/package.json +1 -1
- package/public/ai-hub/index.html +1 -0
- package/public/ai-hub/script.js +156 -1
- package/public/ai-hub/styles.css +21 -0
- package/public/first-run/script.js +209 -19
|
@@ -27,11 +27,13 @@ const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
|
27
27
|
// (per-project override layer; CLAUDE.md says it
|
|
28
28
|
// "takes precedence over synced baseline content")
|
|
29
29
|
const EMPLOYEE_JOB_LAYERS = [
|
|
30
|
+
{ base: 'registry', segments: ['jobs', 'ai-employee'] },
|
|
30
31
|
{ base: 'fraim', segments: ['ai-employee', 'jobs'] },
|
|
31
32
|
{ base: 'fraim', segments: ['personalized-employee', 'jobs'] },
|
|
32
33
|
];
|
|
33
34
|
// Manager templates use the matching two-layer model.
|
|
34
35
|
const MANAGER_JOB_LAYERS = [
|
|
36
|
+
{ base: 'registry', segments: ['jobs', 'ai-manager'] },
|
|
35
37
|
{ base: 'fraim', segments: ['ai-manager', 'jobs'] },
|
|
36
38
|
{ base: 'fraim', segments: ['personalized-employee', 'manager-jobs'] },
|
|
37
39
|
];
|
|
@@ -137,7 +139,8 @@ function summarizeProject(projectPath) {
|
|
|
137
139
|
};
|
|
138
140
|
}
|
|
139
141
|
const fraimDir = (0, project_fraim_paths_1.getWorkspaceFraimDir)(projectPath);
|
|
140
|
-
const
|
|
142
|
+
const registryJobsDir = path_1.default.join(projectPath, 'registry', 'jobs');
|
|
143
|
+
const hasFraim = fs_1.default.existsSync(fraimDir) || fs_1.default.existsSync(registryJobsDir);
|
|
141
144
|
if (!hasFraim) {
|
|
142
145
|
return {
|
|
143
146
|
path: projectPath,
|
|
@@ -154,6 +157,9 @@ function summarizeProject(projectPath) {
|
|
|
154
157
|
};
|
|
155
158
|
}
|
|
156
159
|
function resolveLayerRoot(projectPath, layer) {
|
|
160
|
+
if (layer.base === 'registry') {
|
|
161
|
+
return path_1.default.join(projectPath, 'registry', ...layer.segments);
|
|
162
|
+
}
|
|
157
163
|
return path_1.default.join((0, project_fraim_paths_1.getWorkspaceFraimDir)(projectPath), ...layer.segments);
|
|
158
164
|
}
|
|
159
165
|
function discoverLayers(projectPath, layers) {
|
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -207,6 +207,7 @@ function extractSignalFromArgs(args) {
|
|
|
207
207
|
const EMPLOYEE_LABELS = {
|
|
208
208
|
codex: 'Codex',
|
|
209
209
|
claude: 'Claude Code',
|
|
210
|
+
gemini: 'Gemini CLI',
|
|
210
211
|
};
|
|
211
212
|
const executableName = (command) => command;
|
|
212
213
|
function quoteWindowsArg(value) {
|
|
@@ -237,9 +238,7 @@ const availableByVersionProbe = (command) => {
|
|
|
237
238
|
};
|
|
238
239
|
function detectEmployees() {
|
|
239
240
|
return Object.keys(EMPLOYEE_LABELS).map((id) => {
|
|
240
|
-
const available = id
|
|
241
|
-
? availableByVersionProbe(executableName('codex'))
|
|
242
|
-
: availableByVersionProbe(executableName('claude'));
|
|
241
|
+
const available = availableByVersionProbe(executableName(id));
|
|
243
242
|
return {
|
|
244
243
|
id,
|
|
245
244
|
label: EMPLOYEE_LABELS[id],
|
|
@@ -256,6 +255,13 @@ function buildStartPlan(hostId, message) {
|
|
|
256
255
|
stdin: message,
|
|
257
256
|
};
|
|
258
257
|
}
|
|
258
|
+
if (hostId === 'gemini') {
|
|
259
|
+
return {
|
|
260
|
+
command: executableName('gemini'),
|
|
261
|
+
args: ['--yolo'],
|
|
262
|
+
stdin: message,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
259
265
|
return {
|
|
260
266
|
command: executableName('claude'),
|
|
261
267
|
args: ['-p', '--verbose', '--output-format', 'stream-json', '--dangerously-skip-permissions'],
|
|
@@ -270,6 +276,15 @@ function buildContinuePlan(hostId, sessionId, message) {
|
|
|
270
276
|
stdin: message,
|
|
271
277
|
};
|
|
272
278
|
}
|
|
279
|
+
if (hostId === 'gemini') {
|
|
280
|
+
// Gemini CLI does not have a native session-resume flag; each message
|
|
281
|
+
// is sent as a fresh invocation. The Hub still tracks state client-side.
|
|
282
|
+
return {
|
|
283
|
+
command: executableName('gemini'),
|
|
284
|
+
args: ['--yolo'],
|
|
285
|
+
stdin: message,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
273
288
|
return {
|
|
274
289
|
command: executableName('claude'),
|
|
275
290
|
args: ['-p', '--verbose', '--output-format', 'stream-json', '--dangerously-skip-permissions', '-r', sessionId],
|
|
@@ -307,6 +322,18 @@ function parseHostLine(hostId, line) {
|
|
|
307
322
|
return withSignal({ raw: trimmed });
|
|
308
323
|
}
|
|
309
324
|
}
|
|
325
|
+
// Gemini CLI output: if the line is valid JSON, apply signal scanning and
|
|
326
|
+
// capture the raw line. Plain-text output (non-JSON) is captured as a raw
|
|
327
|
+
// message so it still surfaces in the Hub timeline.
|
|
328
|
+
if (hostId === 'gemini') {
|
|
329
|
+
try {
|
|
330
|
+
JSON.parse(trimmed); // validate JSON — if this throws, fall through to plain text
|
|
331
|
+
return withSignal({ raw: trimmed });
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
return withSignal({ message: trimmed, raw: trimmed });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
310
337
|
try {
|
|
311
338
|
const parsed = JSON.parse(trimmed);
|
|
312
339
|
if (parsed.type === 'system' && parsed.session_id) {
|
|
@@ -385,6 +412,7 @@ class FakeHostRuntime {
|
|
|
385
412
|
this.employees = [
|
|
386
413
|
{ id: 'codex', label: 'Codex', available: true, detail: 'Test double employee.' },
|
|
387
414
|
{ id: 'claude', label: 'Claude Code', available: true, detail: 'Test double employee.' },
|
|
415
|
+
{ id: 'gemini', label: 'Gemini CLI', available: true, detail: 'Test double employee.' },
|
|
388
416
|
];
|
|
389
417
|
}
|
|
390
418
|
detectEmployees() {
|
|
@@ -444,6 +472,7 @@ class ScriptedHostRuntime {
|
|
|
444
472
|
this.employees = [
|
|
445
473
|
{ id: 'codex', label: 'Codex', available: true, detail: 'Scripted test double.' },
|
|
446
474
|
{ id: 'claude', label: 'Claude Code', available: true, detail: 'Scripted test double.' },
|
|
475
|
+
{ id: 'gemini', label: 'Gemini CLI', available: true, detail: 'Scripted test double.' },
|
|
447
476
|
];
|
|
448
477
|
// Track each active run so the test can emit signals at it. Key is the
|
|
449
478
|
// sessionId we hand back on startRun; mapping sessionId → handlers
|
|
@@ -8,7 +8,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
10
10
|
const DEFAULT_CATEGORY = 'marketing';
|
|
11
|
-
const DEFAULT_EMPLOYEE = '
|
|
11
|
+
const DEFAULT_EMPLOYEE = 'claude';
|
|
12
12
|
const defaultPreferences = (projectPath) => ({
|
|
13
13
|
projectPath,
|
|
14
14
|
employeeId: DEFAULT_EMPLOYEE,
|
|
@@ -27,8 +27,8 @@ class AiHubPreferencesStore {
|
|
|
27
27
|
const raw = JSON.parse(fs_1.default.readFileSync(this.stateFilePath, 'utf8'));
|
|
28
28
|
return {
|
|
29
29
|
projectPath: raw.projectPath || projectPath,
|
|
30
|
-
employeeId: raw.employeeId === 'claude'
|
|
31
|
-
categoryId: raw.categoryId === '
|
|
30
|
+
employeeId: (raw.employeeId === 'claude' || raw.employeeId === 'codex') ? raw.employeeId : DEFAULT_EMPLOYEE,
|
|
31
|
+
categoryId: typeof raw.categoryId === 'string' && raw.categoryId.length > 0 ? raw.categoryId : DEFAULT_CATEGORY,
|
|
32
32
|
recentJobIds: Array.isArray(raw.recentJobIds) ? raw.recentJobIds.filter((value) => typeof value === 'string') : [],
|
|
33
33
|
};
|
|
34
34
|
}
|
|
@@ -9,8 +9,10 @@ const express_1 = __importDefault(require("express"));
|
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const fs_1 = __importDefault(require("fs"));
|
|
11
11
|
const net_1 = __importDefault(require("net"));
|
|
12
|
+
const os_1 = __importDefault(require("os"));
|
|
12
13
|
const crypto_1 = require("crypto");
|
|
13
14
|
const child_process_1 = require("child_process");
|
|
15
|
+
const types_1 = require("../first-run/types");
|
|
14
16
|
const catalog_1 = require("./catalog");
|
|
15
17
|
const agent_token_prices_1 = require("../local-mcp-server/agent-token-prices");
|
|
16
18
|
const hosts_1 = require("./hosts");
|
|
@@ -210,19 +212,100 @@ function deriveStages(run, projectPath) {
|
|
|
210
212
|
const currentIndex = run.currentPhase
|
|
211
213
|
? declaredPath.findIndex((p) => p.id === run.currentPhase)
|
|
212
214
|
: -1;
|
|
215
|
+
const historyMap = new Map((run.phaseHistory || []).map((e) => [e.phaseId, e]));
|
|
213
216
|
return declaredPath.map((phase, index) => {
|
|
214
217
|
let state;
|
|
215
|
-
if (currentIndex < 0)
|
|
218
|
+
if (currentIndex < 0) {
|
|
216
219
|
state = 'upcoming';
|
|
217
|
-
|
|
220
|
+
}
|
|
221
|
+
else if (index < currentIndex) {
|
|
218
222
|
state = 'done';
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
223
|
+
}
|
|
224
|
+
else if (index === currentIndex) {
|
|
225
|
+
// If the agent has already reported this phase as 'complete', advance
|
|
226
|
+
// its visual state to 'done' so the tracker doesn't look frozen while
|
|
227
|
+
// waiting for the agent to start the next phase (e.g. after
|
|
228
|
+
// implement-submission completes but before address-feedback starts).
|
|
229
|
+
const entry = historyMap.get(phase.id);
|
|
230
|
+
state = entry?.latestStatus === 'complete' ? 'done' : 'current';
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
222
233
|
state = 'upcoming';
|
|
234
|
+
}
|
|
223
235
|
return { phaseId: phase.id, label: phase.label, state };
|
|
224
236
|
});
|
|
225
237
|
}
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// Agent install helpers (shared with first-run; duplicated here to avoid
|
|
240
|
+
// the session-key dependency in that module's public API).
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
const HUB_TO_FIRST_RUN_ID = {
|
|
243
|
+
claude: 'claude-code',
|
|
244
|
+
codex: 'codex',
|
|
245
|
+
gemini: 'gemini-cli',
|
|
246
|
+
};
|
|
247
|
+
function hubAgentOption(hubId) {
|
|
248
|
+
const frId = HUB_TO_FIRST_RUN_ID[hubId];
|
|
249
|
+
return frId ? types_1.FIRST_RUN_AGENT_OPTIONS.find((o) => o.id === frId) : undefined;
|
|
250
|
+
}
|
|
251
|
+
function hubCommandVersion(command) {
|
|
252
|
+
const executable = process.platform === 'win32' ? 'cmd.exe' : command;
|
|
253
|
+
const args = process.platform === 'win32'
|
|
254
|
+
? ['/d', '/s', '/c', `${command} --version`]
|
|
255
|
+
: ['--version'];
|
|
256
|
+
const result = (0, child_process_1.spawnSync)(executable, args, { encoding: 'utf8', timeout: 5000 });
|
|
257
|
+
if (result.status !== 0 || result.error)
|
|
258
|
+
return null;
|
|
259
|
+
const raw = (result.stdout || result.stderr || '').trim();
|
|
260
|
+
return raw || null;
|
|
261
|
+
}
|
|
262
|
+
function hubRunProcess(command, args, env) {
|
|
263
|
+
return new Promise((resolve, reject) => {
|
|
264
|
+
const executable = process.platform === 'win32' && command === 'npm' ? 'npm.cmd' : command;
|
|
265
|
+
const child = (0, child_process_1.spawn)(executable, args, {
|
|
266
|
+
env: { ...process.env, ...env },
|
|
267
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
268
|
+
});
|
|
269
|
+
let stdout = '';
|
|
270
|
+
let stderr = '';
|
|
271
|
+
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
272
|
+
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
273
|
+
child.on('close', (code) => {
|
|
274
|
+
if (code === 0)
|
|
275
|
+
resolve({ stdout, stderr });
|
|
276
|
+
else
|
|
277
|
+
reject(new Error(stderr || `Process exited with code ${code}`));
|
|
278
|
+
});
|
|
279
|
+
child.on('error', reject);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
function hubOpenTerminal(command) {
|
|
283
|
+
if (process.platform === 'win32') {
|
|
284
|
+
(0, child_process_1.spawn)('cmd.exe', ['/c', 'start', 'cmd.exe', '/k', command], { detached: true, stdio: 'ignore' }).unref();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (process.platform === 'darwin') {
|
|
288
|
+
const script = `tell application "Terminal" to do script "${command.replace(/"/g, '\\"')}"`;
|
|
289
|
+
(0, child_process_1.spawn)('osascript', ['-e', script], { detached: true, stdio: 'ignore' }).unref();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const linux = [
|
|
293
|
+
['gnome-terminal', ['--', 'bash', '-c', `${command}; exec bash`]],
|
|
294
|
+
['xterm', ['-e', `bash -c '${command}; exec bash'`]],
|
|
295
|
+
['konsole', ['--noclose', '-e', 'bash', '-c', command]],
|
|
296
|
+
['x-terminal-emulator', ['-e', `bash -c '${command}; exec bash'`]],
|
|
297
|
+
];
|
|
298
|
+
for (const [term, args] of linux) {
|
|
299
|
+
if ((0, child_process_1.spawnSync)('which', [term], { encoding: 'utf8' }).status === 0) {
|
|
300
|
+
(0, child_process_1.spawn)(term, args, { detached: true, stdio: 'ignore' }).unref();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
(0, child_process_1.spawn)('bash', ['-c', command], { detached: true, stdio: 'ignore' }).unref();
|
|
305
|
+
}
|
|
306
|
+
function getUserHubDir() {
|
|
307
|
+
return path_1.default.join(os_1.default.homedir(), '.fraim');
|
|
308
|
+
}
|
|
226
309
|
function ensureDirectoryPath(projectPath) {
|
|
227
310
|
const trimmed = (projectPath || '').trim();
|
|
228
311
|
if (!trimmed) {
|
|
@@ -275,7 +358,18 @@ class AiHubServer {
|
|
|
275
358
|
}
|
|
276
359
|
bootstrapResponse(projectPath) {
|
|
277
360
|
const normalizedProjectPath = path_1.default.resolve(projectPath || this.projectPath);
|
|
278
|
-
const
|
|
361
|
+
const employees = this.hostRuntime.detectEmployees();
|
|
362
|
+
let preferences = this.preferencesStore.load(normalizedProjectPath);
|
|
363
|
+
// If the stored employee isn't available on this machine, auto-select the
|
|
364
|
+
// first available one so the Hub never opens showing "(unavailable)".
|
|
365
|
+
const storedAvailable = employees.find((e) => e.id === preferences.employeeId)?.available ?? false;
|
|
366
|
+
if (!storedAvailable) {
|
|
367
|
+
const firstAvailable = employees.find((e) => e.available);
|
|
368
|
+
if (firstAvailable) {
|
|
369
|
+
preferences = { ...preferences, employeeId: firstAvailable.id };
|
|
370
|
+
this.preferencesStore.save(preferences);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
279
373
|
const project = (0, catalog_1.summarizeProject)(normalizedProjectPath);
|
|
280
374
|
const jobs = (0, catalog_1.discoverEmployeeJobs)(normalizedProjectPath);
|
|
281
375
|
const managerTemplates = (0, catalog_1.discoverManagerTemplates)(normalizedProjectPath);
|
|
@@ -291,7 +385,7 @@ class AiHubServer {
|
|
|
291
385
|
categories: (0, catalog_1.getAiHubCategories)(normalizedProjectPath),
|
|
292
386
|
jobs,
|
|
293
387
|
managerTemplates,
|
|
294
|
-
employees
|
|
388
|
+
employees,
|
|
295
389
|
activeRun,
|
|
296
390
|
};
|
|
297
391
|
}
|
|
@@ -314,13 +408,76 @@ class AiHubServer {
|
|
|
314
408
|
return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not open the folder picker.' });
|
|
315
409
|
}
|
|
316
410
|
});
|
|
411
|
+
this.app.post('/api/ai-hub/install-agent', async (req, res) => {
|
|
412
|
+
const { hubId } = req.body;
|
|
413
|
+
if (!hubId)
|
|
414
|
+
return res.status(400).json({ error: 'hubId is required.' });
|
|
415
|
+
const option = hubAgentOption(hubId);
|
|
416
|
+
if (!option)
|
|
417
|
+
return res.status(400).json({ error: `Unknown agent: ${hubId}` });
|
|
418
|
+
try {
|
|
419
|
+
const prefix = path_1.default.join(getUserHubDir(), 'node');
|
|
420
|
+
fs_1.default.mkdirSync(prefix, { recursive: true });
|
|
421
|
+
await hubRunProcess('npm', ['install', '-g', option.installPackage], { npm_config_prefix: prefix });
|
|
422
|
+
return res.json({
|
|
423
|
+
ok: true,
|
|
424
|
+
message: `${option.label} installed successfully.`,
|
|
425
|
+
needsLogin: true,
|
|
426
|
+
loginCommand: option.loginCommand,
|
|
427
|
+
loginHint: `Sign in to ${option.label} to activate it. A terminal window will open — complete sign-in there, then click "Check if Ready".`,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
const detail = error instanceof Error ? error.message : 'Unknown error';
|
|
432
|
+
return res.status(500).json({ ok: false, error: `Failed to install ${option.label}: ${detail}` });
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
this.app.post('/api/ai-hub/trigger-agent-login', (req, res) => {
|
|
436
|
+
const { hubId } = req.body;
|
|
437
|
+
if (!hubId)
|
|
438
|
+
return res.status(400).json({ error: 'hubId is required.' });
|
|
439
|
+
const option = hubAgentOption(hubId);
|
|
440
|
+
if (!option)
|
|
441
|
+
return res.status(400).json({ error: `Unknown agent: ${hubId}` });
|
|
442
|
+
try {
|
|
443
|
+
hubOpenTerminal(option.loginCommand);
|
|
444
|
+
return res.json({
|
|
445
|
+
ok: true,
|
|
446
|
+
message: `A terminal window opened with the ${option.label} sign-in command. Complete sign-in there, then return here.`,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
const detail = error instanceof Error ? error.message : 'Unknown error';
|
|
451
|
+
return res.json({
|
|
452
|
+
ok: false,
|
|
453
|
+
message: `Could not open a terminal automatically: ${detail}. Run \`${option.loginCommand}\` in a terminal to sign in.`,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
this.app.post('/api/ai-hub/check-agent', (req, res) => {
|
|
458
|
+
const { hubId } = req.body;
|
|
459
|
+
if (!hubId)
|
|
460
|
+
return res.status(400).json({ error: 'hubId is required.' });
|
|
461
|
+
const option = hubAgentOption(hubId);
|
|
462
|
+
if (!option)
|
|
463
|
+
return res.status(400).json({ error: `Unknown agent: ${hubId}` });
|
|
464
|
+
const ver = hubCommandVersion(option.launchCommand);
|
|
465
|
+
if (ver) {
|
|
466
|
+
return res.json({ ok: true, ready: true, message: `${option.label} is ready.` });
|
|
467
|
+
}
|
|
468
|
+
return res.json({
|
|
469
|
+
ok: true,
|
|
470
|
+
ready: false,
|
|
471
|
+
message: `${option.label} is not detected yet. Make sure sign-in is complete and try again.`,
|
|
472
|
+
});
|
|
473
|
+
});
|
|
317
474
|
this.app.post('/api/ai-hub/runs', (req, res) => {
|
|
318
475
|
try {
|
|
319
476
|
const projectPath = ensureDirectoryPath(req.body.projectPath || this.projectPath);
|
|
320
477
|
const hostId = req.body.hostId;
|
|
321
478
|
const jobId = req.body.jobId;
|
|
322
479
|
const message = (req.body.message || '').trim();
|
|
323
|
-
if (hostId !== 'codex' && hostId !== 'claude') {
|
|
480
|
+
if (hostId !== 'codex' && hostId !== 'claude' && hostId !== 'gemini') {
|
|
324
481
|
throw new Error('Choose an available employee before starting a job.');
|
|
325
482
|
}
|
|
326
483
|
if (!jobId) {
|
|
@@ -102,7 +102,7 @@ exports.IDE_CONFIGS = [
|
|
|
102
102
|
configPath: '~/.gemini/antigravity/mcp_config.json',
|
|
103
103
|
configFormat: 'json',
|
|
104
104
|
configType: 'standard',
|
|
105
|
-
invocationProfile: '
|
|
105
|
+
invocationProfile: 'cursor-mention',
|
|
106
106
|
detectMethod: () => fs_1.default.existsSync(expandPath('~/.gemini/antigravity')),
|
|
107
107
|
description: 'Google Gemini Antigravity IDE'
|
|
108
108
|
},
|
|
@@ -72,6 +72,10 @@ async function installGlobalRules(homeDir) {
|
|
|
72
72
|
if (fs_1.default.existsSync(geminiDir)) {
|
|
73
73
|
installFileIfMissing(path_1.default.join(geminiDir, 'commands', 'fraim.toml'), (0, ide_invocation_surfaces_1.buildGeminiCommandContent)(), 'Gemini CLI FRAIM command (~/.gemini/commands/fraim.toml)');
|
|
74
74
|
}
|
|
75
|
+
const antigravityDir = path_1.default.join(home, '.gemini', 'antigravity');
|
|
76
|
+
if (fs_1.default.existsSync(antigravityDir)) {
|
|
77
|
+
installFileIfMissing(path_1.default.join(antigravityDir, 'commands', 'fraim.md'), (0, ide_invocation_surfaces_1.buildAntigravityCommandContent)(), 'Antigravity FRAIM command (~/.gemini/antigravity/commands/fraim.md)');
|
|
78
|
+
}
|
|
75
79
|
const windsurfDir = path_1.default.join(home, '.codeium', 'windsurf');
|
|
76
80
|
if (fs_1.default.existsSync(windsurfDir)) {
|
|
77
81
|
installFileIfMissing(path_1.default.join(windsurfDir, 'commands', 'fraim.md'), (0, ide_invocation_surfaces_1.buildWindsurfCommandContent)(), 'Windsurf FRAIM command (~/.codeium/windsurf/commands/fraim.md)');
|
|
@@ -9,6 +9,7 @@ exports.buildCursorMentionRuleContent = buildCursorMentionRuleContent;
|
|
|
9
9
|
exports.buildCodexSkillContent = buildCodexSkillContent;
|
|
10
10
|
exports.buildWindsurfCommandContent = buildWindsurfCommandContent;
|
|
11
11
|
exports.buildKiroCommandContent = buildKiroCommandContent;
|
|
12
|
+
exports.buildAntigravityCommandContent = buildAntigravityCommandContent;
|
|
12
13
|
exports.buildGeminiCommandContent = buildGeminiCommandContent;
|
|
13
14
|
exports.describeInvocationSurface = describeInvocationSurface;
|
|
14
15
|
exports.FRAIM_LAUNCH_PHRASE = 'Use FRAIM for <job or task>';
|
|
@@ -111,6 +112,11 @@ ${buildFraimInvocationBody('generic-tool-discovery')}`;
|
|
|
111
112
|
function escapeTomlMultiline(value) {
|
|
112
113
|
return value.replace(/"""/g, '\\"""');
|
|
113
114
|
}
|
|
115
|
+
function buildAntigravityCommandContent() {
|
|
116
|
+
return `# FRAIM
|
|
117
|
+
|
|
118
|
+
${buildFraimInvocationBody('generic-tool-discovery')}`;
|
|
119
|
+
}
|
|
114
120
|
function buildGeminiCommandContent() {
|
|
115
121
|
return `description = "Discover and execute FRAIM jobs and skills"
|
|
116
122
|
prompt = """
|
|
@@ -231,6 +231,37 @@ exports.FRAIM_CONFIG_SCHEMA = {
|
|
|
231
231
|
"required": true
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
+
},
|
|
235
|
+
"stakeholderUpdate": {
|
|
236
|
+
"kind": "object",
|
|
237
|
+
"properties": {
|
|
238
|
+
"stakeholderListPath": {
|
|
239
|
+
"kind": "string"
|
|
240
|
+
},
|
|
241
|
+
"cadence": {
|
|
242
|
+
"kind": "enum",
|
|
243
|
+
"values": [
|
|
244
|
+
"monthly",
|
|
245
|
+
"quarterly"
|
|
246
|
+
]
|
|
247
|
+
},
|
|
248
|
+
"historyPath": {
|
|
249
|
+
"kind": "string"
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
"operatingReview": {
|
|
254
|
+
"kind": "object",
|
|
255
|
+
"properties": {
|
|
256
|
+
"cadence": {
|
|
257
|
+
"kind": "enum",
|
|
258
|
+
"values": [
|
|
259
|
+
"weekly",
|
|
260
|
+
"bi-weekly",
|
|
261
|
+
"monthly"
|
|
262
|
+
]
|
|
263
|
+
}
|
|
264
|
+
}
|
|
234
265
|
}
|
|
235
266
|
},
|
|
236
267
|
"required": true
|
|
@@ -292,5 +323,11 @@ exports.SUPPORTED_FRAIM_CONFIG_PATHS = [
|
|
|
292
323
|
"customer-communication.senderEmail",
|
|
293
324
|
"customer-communication.senderReplyTo",
|
|
294
325
|
"customer-communication.newsletterAudienceProvider",
|
|
295
|
-
"customer-communication.deliveryProvider"
|
|
326
|
+
"customer-communication.deliveryProvider",
|
|
327
|
+
"stakeholderUpdate",
|
|
328
|
+
"stakeholderUpdate.stakeholderListPath",
|
|
329
|
+
"stakeholderUpdate.cadence",
|
|
330
|
+
"stakeholderUpdate.historyPath",
|
|
331
|
+
"operatingReview",
|
|
332
|
+
"operatingReview.cadence"
|
|
296
333
|
];
|
|
@@ -234,6 +234,44 @@ class FirstRunServer {
|
|
|
234
234
|
(0, setup_preferences_1.writeSetupHandoffChoice)(choice);
|
|
235
235
|
return res.json({ ok: true });
|
|
236
236
|
});
|
|
237
|
+
this.app.post('/api/first-run/install-agent', async (req, res) => {
|
|
238
|
+
try {
|
|
239
|
+
const { agentId } = req.body || {};
|
|
240
|
+
if (!agentId || typeof agentId !== 'string') {
|
|
241
|
+
return res.status(400).json({ error: 'agentId is required.' });
|
|
242
|
+
}
|
|
243
|
+
const result = await this.sessionService.installAgent(agentId);
|
|
244
|
+
return res.json(result);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not install agent.' });
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
this.app.post('/api/first-run/trigger-agent-login', async (req, res) => {
|
|
251
|
+
try {
|
|
252
|
+
const { agentId } = req.body || {};
|
|
253
|
+
if (!agentId || typeof agentId !== 'string') {
|
|
254
|
+
return res.status(400).json({ error: 'agentId is required.' });
|
|
255
|
+
}
|
|
256
|
+
const result = await this.sessionService.triggerAgentLogin(agentId);
|
|
257
|
+
return res.json(result);
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not trigger login.' });
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
this.app.post('/api/first-run/check-agent', (req, res) => {
|
|
264
|
+
try {
|
|
265
|
+
const { agentId } = req.body || {};
|
|
266
|
+
if (!agentId || typeof agentId !== 'string') {
|
|
267
|
+
return res.status(400).json({ error: 'agentId is required.' });
|
|
268
|
+
}
|
|
269
|
+
return res.json(this.sessionService.checkAgentReady(agentId));
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not check agent.' });
|
|
273
|
+
}
|
|
274
|
+
});
|
|
237
275
|
// Hub-launch helper - starts an AiHubServer for the chosen project and
|
|
238
276
|
// opens the user's browser. v2 (#355) replaces the in-process spawn with
|
|
239
277
|
// a durable launcher binary that survives independently.
|
|
@@ -257,8 +257,11 @@ class FirstRunSessionService {
|
|
|
257
257
|
if (/ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|getaddrinfo|network/i.test(detail)) {
|
|
258
258
|
return "Couldn't reach the FRAIM registry. Check your internet connection (or if you're testing locally, that the local FRAIM server is running) and click Retry.";
|
|
259
259
|
}
|
|
260
|
+
if (/EPERM|EACCES|permission denied/i.test(detail)) {
|
|
261
|
+
return 'A security tool (Windows Defender, antivirus, or IT policy) may be locking files in the install folder. Try disabling real-time scanning for the ~/.fraim directory, then click Retry.';
|
|
262
|
+
}
|
|
260
263
|
if (/Global FRAIM setup not found/i.test(detail)) {
|
|
261
|
-
return 'FRAIM\'s global config is missing. This usually means the agent step didn\'t complete cleanly
|
|
264
|
+
return 'FRAIM\'s global config is missing. This usually means the agent step didn\'t complete cleanly — go back and re-run the AI agent row.';
|
|
262
265
|
}
|
|
263
266
|
return undefined;
|
|
264
267
|
}
|
|
@@ -286,7 +289,7 @@ class FirstRunSessionService {
|
|
|
286
289
|
}
|
|
287
290
|
else {
|
|
288
291
|
gitRow.status = 'pending';
|
|
289
|
-
gitRow.verb =
|
|
292
|
+
gitRow.verb = 'optional — only needed for code delivery workflows';
|
|
290
293
|
}
|
|
291
294
|
const fraimRow = this.getRow('fraim');
|
|
292
295
|
if (commandVersion('npx') !== null) {
|
|
@@ -496,22 +499,19 @@ class FirstRunSessionService {
|
|
|
496
499
|
}
|
|
497
500
|
async runGitRow() {
|
|
498
501
|
const row = this.getRow('git');
|
|
499
|
-
const ver = commandVersion('git');
|
|
502
|
+
const ver = commandVersion('git') || (this.fakeMode ? 'git version 2.45' : null);
|
|
500
503
|
if (ver) {
|
|
501
504
|
row.status = 'ok';
|
|
502
505
|
row.verb = `${ver} installed`;
|
|
503
506
|
this.persist();
|
|
504
507
|
return this.respond(`git detected (${ver}).`, true);
|
|
505
508
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
return this.respond('Fake-mode git ok.', true);
|
|
511
|
-
}
|
|
512
|
-
this.setRowError(row, 'Verifying git installation', 'git --version returned a non-zero exit code or no output.\n\nOn macOS, run `xcode-select --install` to install the Command Line Developer Tools, then retry. On Windows, install Git for Windows from https://git-scm.com/download/win.', [{ id: 'retry', label: 'Retry', variant: 'primary' }]);
|
|
509
|
+
// git is optional — non-technical users and non-code workflows don't need it.
|
|
510
|
+
// Mark ok so the wizard is not blocked; the verb makes the situation clear.
|
|
511
|
+
row.status = 'ok';
|
|
512
|
+
row.verb = 'not installed — optional, only needed for code delivery';
|
|
513
513
|
this.persist();
|
|
514
|
-
return this.respond('git not
|
|
514
|
+
return this.respond('git not found — continuing without it. Install git later if you plan to do code delivery work.', true);
|
|
515
515
|
}
|
|
516
516
|
async runFraimRow() {
|
|
517
517
|
const row = this.getRow('fraim');
|
|
@@ -544,10 +544,10 @@ class FirstRunSessionService {
|
|
|
544
544
|
await installSlashCommands();
|
|
545
545
|
await installGlobalRules();
|
|
546
546
|
row.status = 'ok';
|
|
547
|
-
row.verb = 'Ready.';
|
|
547
|
+
row.verb = 'Ready — open a new terminal before running fraim commands.';
|
|
548
548
|
delete row.streamOutput;
|
|
549
549
|
this.persist();
|
|
550
|
-
return this.respond('FRAIM is ready.', true);
|
|
550
|
+
return this.respond('FRAIM is ready. Open a new terminal window so your PATH update takes effect before running fraim commands.', true);
|
|
551
551
|
}
|
|
552
552
|
catch (error) {
|
|
553
553
|
const detail = error instanceof Error ? error.message : 'Unknown error';
|
|
@@ -574,6 +574,15 @@ class FirstRunSessionService {
|
|
|
574
574
|
this.persist();
|
|
575
575
|
return this.respond('AI Employee recruiting deferred.', true);
|
|
576
576
|
}
|
|
577
|
+
if (this.fakeMode === 'agent-install-fails') {
|
|
578
|
+
this.setRowError(row, 'Checking for installed AI Employees and configuring one for this project', this.fakeStderr, [
|
|
579
|
+
{ id: 'retry', label: 'Retry', variant: 'primary' },
|
|
580
|
+
{ id: 'alternative', label: 'Try alternative', variant: 'secondary' },
|
|
581
|
+
{ id: 'skip', label: 'Skip and continue', variant: 'ghost' },
|
|
582
|
+
]);
|
|
583
|
+
this.persist();
|
|
584
|
+
return this.respond('AI Employee recruiting failed.', false);
|
|
585
|
+
}
|
|
577
586
|
this.updateAgentSummaryRow();
|
|
578
587
|
this.persist();
|
|
579
588
|
const count = this.state.setupResult?.detectedSurfaceCount || 0;
|
|
@@ -623,11 +632,130 @@ class FirstRunSessionService {
|
|
|
623
632
|
* leave the user typing a command. The single-file launcher binary in v2
|
|
624
633
|
* (#355) replaces this in-process spawn with a durable tray-icon launcher.
|
|
625
634
|
*/
|
|
635
|
+
async installAgent(agentId) {
|
|
636
|
+
const option = findAgentOption(agentId);
|
|
637
|
+
if (!option) {
|
|
638
|
+
return { ok: false, message: `Unknown agent: ${agentId}` };
|
|
639
|
+
}
|
|
640
|
+
if (this.fakeMode) {
|
|
641
|
+
return {
|
|
642
|
+
ok: true,
|
|
643
|
+
message: `${option.label} installed successfully.`,
|
|
644
|
+
needsLogin: true,
|
|
645
|
+
loginCommand: option.loginCommand,
|
|
646
|
+
loginHint: `Sign in to ${option.label} to activate it.`,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
try {
|
|
650
|
+
const prefix = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'node');
|
|
651
|
+
fs_1.default.mkdirSync(prefix, { recursive: true });
|
|
652
|
+
await runProcess('npm', ['install', '-g', option.installPackage], { npm_config_prefix: prefix });
|
|
653
|
+
const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
|
|
654
|
+
if (detectedIDEs.length > 0) {
|
|
655
|
+
await (0, auto_mcp_setup_1.autoConfigureMCP)(this.key, {}, detectedIDEs.map((ide) => ide.name), {});
|
|
656
|
+
}
|
|
657
|
+
appendInstallLog(`agent-installed ${agentId}`);
|
|
658
|
+
return {
|
|
659
|
+
ok: true,
|
|
660
|
+
message: `${option.label} installed successfully.`,
|
|
661
|
+
needsLogin: true,
|
|
662
|
+
loginCommand: option.loginCommand,
|
|
663
|
+
loginHint: `Sign in to ${option.label} to activate it. A terminal window will open with the sign-in command — complete sign-in there, then return here and click "Check if Ready".`,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
catch (error) {
|
|
667
|
+
const detail = error instanceof Error ? error.message : 'Unknown error';
|
|
668
|
+
appendInstallLog(`agent-install-failed ${agentId} ${detail}`);
|
|
669
|
+
return { ok: false, message: `Failed to install ${option.label}: ${detail}` };
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
async triggerAgentLogin(agentId) {
|
|
673
|
+
const option = findAgentOption(agentId);
|
|
674
|
+
if (!option) {
|
|
675
|
+
return { ok: false, message: `Unknown agent: ${agentId}` };
|
|
676
|
+
}
|
|
677
|
+
if (this.fakeMode) {
|
|
678
|
+
return { ok: true, message: `${option.label} login triggered (fake-mode).` };
|
|
679
|
+
}
|
|
680
|
+
try {
|
|
681
|
+
this.openTerminalWithCommand(option.loginCommand);
|
|
682
|
+
appendInstallLog(`agent-login-triggered ${agentId}`);
|
|
683
|
+
return {
|
|
684
|
+
ok: true,
|
|
685
|
+
message: `A terminal window opened with the ${option.label} sign-in command. Complete sign-in there, then return here.`,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
const detail = error instanceof Error ? error.message : 'Unknown error';
|
|
690
|
+
return {
|
|
691
|
+
ok: false,
|
|
692
|
+
message: `Could not open a terminal automatically: ${detail}. Run \`${option.loginCommand}\` in a terminal to sign in.`,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
checkAgentReady(agentId) {
|
|
697
|
+
const option = findAgentOption(agentId);
|
|
698
|
+
if (!option) {
|
|
699
|
+
return { ok: false, ready: false, message: `Unknown agent: ${agentId}` };
|
|
700
|
+
}
|
|
701
|
+
if (this.fakeMode) {
|
|
702
|
+
return { ok: true, ready: true, message: `${option.label} is ready (fake-mode).` };
|
|
703
|
+
}
|
|
704
|
+
const ver = commandVersion(option.launchCommand);
|
|
705
|
+
if (ver) {
|
|
706
|
+
this.updateAgentSummaryRow();
|
|
707
|
+
this.persist();
|
|
708
|
+
return { ok: true, ready: true, message: `${option.label} is ready.` };
|
|
709
|
+
}
|
|
710
|
+
return {
|
|
711
|
+
ok: true,
|
|
712
|
+
ready: false,
|
|
713
|
+
message: `${option.label} is not detected yet. Make sure sign-in is complete and try again.`,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
openTerminalWithCommand(command) {
|
|
717
|
+
if (process.platform === 'win32') {
|
|
718
|
+
(0, child_process_1.spawn)('cmd.exe', ['/c', 'start', 'cmd.exe', '/k', command], { detached: true, stdio: 'ignore' }).unref();
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
if (process.platform === 'darwin') {
|
|
722
|
+
const script = `tell application "Terminal" to do script "${command.replace(/"/g, '\\"')}"`;
|
|
723
|
+
(0, child_process_1.spawn)('osascript', ['-e', script], { detached: true, stdio: 'ignore' }).unref();
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const linux = [
|
|
727
|
+
['gnome-terminal', ['--', 'bash', '-c', `${command}; exec bash`]],
|
|
728
|
+
['xterm', ['-e', `bash -c '${command}; exec bash'`]],
|
|
729
|
+
['konsole', ['--noclose', '-e', 'bash', '-c', command]],
|
|
730
|
+
['x-terminal-emulator', ['-e', `bash -c '${command}; exec bash'`]],
|
|
731
|
+
];
|
|
732
|
+
for (const [term, args] of linux) {
|
|
733
|
+
if ((0, child_process_1.spawnSync)('which', [term], { encoding: 'utf8' }).status === 0) {
|
|
734
|
+
(0, child_process_1.spawn)(term, args, { detached: true, stdio: 'ignore' }).unref();
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
626
739
|
async openHub() {
|
|
627
740
|
if (this.fakeMode) {
|
|
628
|
-
// Tests don't actually want a Hub server running
|
|
741
|
+
// Tests don't actually want a Hub server running — just confirm intent.
|
|
629
742
|
return { ok: true, message: 'Fake-mode Hub open requested.', hubUrl: 'http://127.0.0.1:0/ai-hub/?firstRun=true' };
|
|
630
743
|
}
|
|
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).
|
|
747
|
+
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) {
|
|
753
|
+
return {
|
|
754
|
+
ok: false,
|
|
755
|
+
needsAgentSetup: true,
|
|
756
|
+
message: "No AI agent (Claude Code or Codex) found on this machine. Let's install one first.",
|
|
757
|
+
};
|
|
758
|
+
}
|
|
631
759
|
try {
|
|
632
760
|
const { AiHubServer, findAvailablePort } = await Promise.resolve().then(() => __importStar(require('../ai-hub/server')));
|
|
633
761
|
const port = await findAvailablePort(43091);
|
|
@@ -58,7 +58,7 @@ exports.FIRST_RUN_AGENT_OPTIONS = [
|
|
|
58
58
|
function createInitialRows() {
|
|
59
59
|
return [
|
|
60
60
|
{ id: 'node', label: 'Node.js', status: 'pending', verb: "we'll install" },
|
|
61
|
-
{ id: 'git', label: 'git', status: 'pending', verb: "we'll install" },
|
|
61
|
+
{ id: 'git', label: 'git', status: 'pending', verb: "we'll install", optional: true },
|
|
62
62
|
{ id: 'fraim', label: 'FRAIM', status: 'pending', verb: "we'll set up FRAIM" },
|
|
63
63
|
{ id: 'agent', label: 'AI Employees', status: 'pending', verb: "we'll check for AI Employees" },
|
|
64
64
|
];
|
|
@@ -73,14 +73,16 @@ function createInitialRows() {
|
|
|
73
73
|
* - "Set up FRAIM": no rows ok yet.
|
|
74
74
|
*/
|
|
75
75
|
function derivePrimaryButtonLabel(rows) {
|
|
76
|
-
|
|
76
|
+
// Optional rows (e.g. git) never block wizard progression.
|
|
77
|
+
const required = rows.filter((row) => !row.optional);
|
|
78
|
+
const allOk = required.every((row) => row.status === 'ok');
|
|
77
79
|
if (allOk)
|
|
78
80
|
return 'Get Started';
|
|
79
|
-
// Skip-path: every row is ok-or-manual-required — nothing left for the wizard.
|
|
80
|
-
if (
|
|
81
|
+
// Skip-path: every required row is ok-or-manual-required — nothing left for the wizard.
|
|
82
|
+
if (required.every((row) => row.status === 'ok' || row.status === 'manual-required')) {
|
|
81
83
|
return 'Get Started';
|
|
82
84
|
}
|
|
83
|
-
if (
|
|
85
|
+
if (required.some((row) => row.status === 'ok'))
|
|
84
86
|
return 'Continue';
|
|
85
87
|
return 'Set up FRAIM';
|
|
86
88
|
}
|
package/index.js
CHANGED
package/package.json
CHANGED
package/public/ai-hub/index.html
CHANGED
|
@@ -159,6 +159,7 @@
|
|
|
159
159
|
<span class="employee-label">Employee:</span>
|
|
160
160
|
<select id="employee-select" class="employee-select"></select>
|
|
161
161
|
</div>
|
|
162
|
+
<div id="agent-install-panel"></div>
|
|
162
163
|
</div>
|
|
163
164
|
<div class="modal-footer">
|
|
164
165
|
<span class="left">You can coach the employee with more detail after they start.</span>
|
package/public/ai-hub/script.js
CHANGED
|
@@ -40,7 +40,7 @@ function gatherElements() {
|
|
|
40
40
|
'cancel1', 'next1', 'back2', 'start',
|
|
41
41
|
'job-search', 'job-catalog', 'job-pick-status',
|
|
42
42
|
'picked-name', 'picked-desc', 'instructions',
|
|
43
|
-
'employee-select',
|
|
43
|
+
'employee-select', 'agent-install-panel',
|
|
44
44
|
// Issue #347 additions: tracker, template picker, totals.
|
|
45
45
|
'tracker', 'tracker-rows', 'tracker-note',
|
|
46
46
|
'template-picker-btn', 'template-popover',
|
|
@@ -891,6 +891,153 @@ function renderEmployeeSelect() {
|
|
|
891
891
|
if (state.selectedEmployeeId === emp.id) opt.selected = true;
|
|
892
892
|
els['employee-select'].appendChild(opt);
|
|
893
893
|
}
|
|
894
|
+
renderAgentInstallPanel();
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// ---------------------------------------------------------------------------
|
|
898
|
+
// Agent install panel — shown in Step 2 for unavailable agents
|
|
899
|
+
// ---------------------------------------------------------------------------
|
|
900
|
+
|
|
901
|
+
// Per-agent install state tracked within the modal session.
|
|
902
|
+
const agentInstallState = {};
|
|
903
|
+
|
|
904
|
+
function renderAgentInstallPanel() {
|
|
905
|
+
const panel = els['agent-install-panel'];
|
|
906
|
+
if (!panel) return;
|
|
907
|
+
panel.innerHTML = '';
|
|
908
|
+
|
|
909
|
+
const employees = state.bootstrap?.employees || [];
|
|
910
|
+
const unavailable = employees.filter((e) => !e.available);
|
|
911
|
+
if (unavailable.length === 0) return;
|
|
912
|
+
|
|
913
|
+
const heading = document.createElement('div');
|
|
914
|
+
heading.className = 'install-panel-heading';
|
|
915
|
+
heading.textContent = 'Install missing agents';
|
|
916
|
+
panel.appendChild(heading);
|
|
917
|
+
|
|
918
|
+
for (const emp of unavailable) {
|
|
919
|
+
const row = document.createElement('div');
|
|
920
|
+
row.className = 'install-row';
|
|
921
|
+
row.id = `install-row-${emp.id}`;
|
|
922
|
+
|
|
923
|
+
const label = document.createElement('span');
|
|
924
|
+
label.className = 'install-label';
|
|
925
|
+
label.textContent = emp.label;
|
|
926
|
+
row.appendChild(label);
|
|
927
|
+
|
|
928
|
+
const status = document.createElement('span');
|
|
929
|
+
status.className = 'install-status';
|
|
930
|
+
status.id = `install-status-${emp.id}`;
|
|
931
|
+
status.textContent = agentInstallState[emp.id]?.statusText || '';
|
|
932
|
+
row.appendChild(status);
|
|
933
|
+
|
|
934
|
+
const btn = document.createElement('button');
|
|
935
|
+
btn.className = 'secondary small';
|
|
936
|
+
btn.id = `install-btn-${emp.id}`;
|
|
937
|
+
btn.dataset.hubId = emp.id;
|
|
938
|
+
|
|
939
|
+
const st = agentInstallState[emp.id] || {};
|
|
940
|
+
if (!st.phase) {
|
|
941
|
+
btn.textContent = `Install ${emp.label}`;
|
|
942
|
+
btn.addEventListener('click', () => startAgentInstall(emp.id));
|
|
943
|
+
} else if (st.phase === 'installing') {
|
|
944
|
+
btn.textContent = 'Installing…';
|
|
945
|
+
btn.disabled = true;
|
|
946
|
+
} else if (st.phase === 'needs-login') {
|
|
947
|
+
btn.textContent = 'Sign In';
|
|
948
|
+
btn.addEventListener('click', () => triggerAgentLogin(emp.id));
|
|
949
|
+
} else if (st.phase === 'login-triggered') {
|
|
950
|
+
const checkBtn = document.createElement('button');
|
|
951
|
+
checkBtn.className = 'secondary small';
|
|
952
|
+
checkBtn.textContent = 'Check if Ready';
|
|
953
|
+
checkBtn.addEventListener('click', () => checkAgentReady(emp.id));
|
|
954
|
+
row.appendChild(checkBtn);
|
|
955
|
+
|
|
956
|
+
const skipBtn = document.createElement('button');
|
|
957
|
+
skipBtn.className = 'ghost small';
|
|
958
|
+
skipBtn.textContent = 'Skip for now';
|
|
959
|
+
skipBtn.style.marginLeft = '6px';
|
|
960
|
+
skipBtn.addEventListener('click', () => {
|
|
961
|
+
delete agentInstallState[emp.id];
|
|
962
|
+
renderAgentInstallPanel();
|
|
963
|
+
});
|
|
964
|
+
row.appendChild(skipBtn);
|
|
965
|
+
panel.appendChild(row);
|
|
966
|
+
continue;
|
|
967
|
+
} else if (st.phase === 'ready') {
|
|
968
|
+
btn.textContent = '✓ Ready';
|
|
969
|
+
btn.disabled = true;
|
|
970
|
+
btn.style.color = 'var(--accent)';
|
|
971
|
+
} else if (st.phase === 'error') {
|
|
972
|
+
btn.textContent = 'Retry';
|
|
973
|
+
btn.addEventListener('click', () => startAgentInstall(emp.id));
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
row.appendChild(btn);
|
|
977
|
+
panel.appendChild(row);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function setInstallState(hubId, phase, statusText) {
|
|
982
|
+
agentInstallState[hubId] = { phase, statusText: statusText || '' };
|
|
983
|
+
renderAgentInstallPanel();
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
async function startAgentInstall(hubId) {
|
|
987
|
+
setInstallState(hubId, 'installing', 'Installing…');
|
|
988
|
+
try {
|
|
989
|
+
const result = await requestJson('/api/ai-hub/install-agent', {
|
|
990
|
+
method: 'POST',
|
|
991
|
+
headers: { 'Content-Type': 'application/json' },
|
|
992
|
+
body: JSON.stringify({ hubId }),
|
|
993
|
+
});
|
|
994
|
+
if (result.ok) {
|
|
995
|
+
setInstallState(hubId, 'needs-login', result.loginHint || 'Installed. Sign in to activate.');
|
|
996
|
+
} else {
|
|
997
|
+
setInstallState(hubId, 'error', result.message || 'Install failed.');
|
|
998
|
+
}
|
|
999
|
+
} catch (err) {
|
|
1000
|
+
setInstallState(hubId, 'error', err.message || 'Install failed.');
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
async function triggerAgentLogin(hubId) {
|
|
1005
|
+
try {
|
|
1006
|
+
const result = await requestJson('/api/ai-hub/trigger-agent-login', {
|
|
1007
|
+
method: 'POST',
|
|
1008
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1009
|
+
body: JSON.stringify({ hubId }),
|
|
1010
|
+
});
|
|
1011
|
+
setInstallState(hubId, 'login-triggered', result.message || 'Sign-in terminal opened.');
|
|
1012
|
+
} catch (err) {
|
|
1013
|
+
setInstallState(hubId, 'error', err.message || 'Could not open terminal.');
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
async function checkAgentReady(hubId) {
|
|
1018
|
+
try {
|
|
1019
|
+
const result = await requestJson('/api/ai-hub/check-agent', {
|
|
1020
|
+
method: 'POST',
|
|
1021
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1022
|
+
body: JSON.stringify({ hubId }),
|
|
1023
|
+
});
|
|
1024
|
+
if (result.ready) {
|
|
1025
|
+
setInstallState(hubId, 'ready', `${result.message}`);
|
|
1026
|
+
await refreshEmployees();
|
|
1027
|
+
} else {
|
|
1028
|
+
setInstallState(hubId, 'login-triggered', result.message || 'Not ready yet — complete sign-in and try again.');
|
|
1029
|
+
}
|
|
1030
|
+
} catch (err) {
|
|
1031
|
+
setInstallState(hubId, 'error', err.message || 'Check failed.');
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
async function refreshEmployees() {
|
|
1036
|
+
try {
|
|
1037
|
+
const bootstrap = await requestJson(`/api/ai-hub/bootstrap?projectPath=${encodeURIComponent(state.projectPath)}`);
|
|
1038
|
+
state.bootstrap = bootstrap;
|
|
1039
|
+
renderEmployeeSelect();
|
|
1040
|
+
} catch { /* best-effort */ }
|
|
894
1041
|
}
|
|
895
1042
|
|
|
896
1043
|
// ---------------------------------------------------------------------------
|
|
@@ -937,6 +1084,7 @@ function deriveTitle(jobTitle, instructions) {
|
|
|
937
1084
|
const FRAIM_INVOCATION_SYMBOL = {
|
|
938
1085
|
codex: '$fraim',
|
|
939
1086
|
claude: '/fraim',
|
|
1087
|
+
gemini: '/fraim',
|
|
940
1088
|
};
|
|
941
1089
|
|
|
942
1090
|
function fraimInvocationFor(employeeId, jobId, kind) {
|
|
@@ -956,6 +1104,13 @@ function buildAgentMessage(employeeId, jobId, kind, instructions, stubPath) {
|
|
|
956
1104
|
const stub = (kind === 'start' && stubPath) ? `\n[Job stub: ${stubPath}]` : '';
|
|
957
1105
|
const trimmed = (instructions || '').trim();
|
|
958
1106
|
if (!trimmed) return `${invocation}${stub}`;
|
|
1107
|
+
// For continue turns, applyTemplateInvocation already writes the full
|
|
1108
|
+
// FRAIM invocation (e.g. "/fraim follow-your-mentor") into the textarea.
|
|
1109
|
+
// If the text already starts with a known FRAIM symbol, don't prepend again.
|
|
1110
|
+
if (kind === 'continue') {
|
|
1111
|
+
const knownSymbols = Object.values(FRAIM_INVOCATION_SYMBOL);
|
|
1112
|
+
if (knownSymbols.some((s) => trimmed.startsWith(s))) return trimmed;
|
|
1113
|
+
}
|
|
959
1114
|
return `${invocation}${stub}\n\n${trimmed}`;
|
|
960
1115
|
}
|
|
961
1116
|
|
package/public/ai-hub/styles.css
CHANGED
|
@@ -662,6 +662,27 @@ button { font: inherit; cursor: pointer; }
|
|
|
662
662
|
}
|
|
663
663
|
.employee-select:focus { outline: none; border-color: var(--accent); }
|
|
664
664
|
|
|
665
|
+
/* Agent install panel */
|
|
666
|
+
#agent-install-panel { margin-top: 10px; }
|
|
667
|
+
.install-panel-heading {
|
|
668
|
+
font-size: 12px;
|
|
669
|
+
font-weight: 600;
|
|
670
|
+
color: var(--muted);
|
|
671
|
+
text-transform: uppercase;
|
|
672
|
+
letter-spacing: .04em;
|
|
673
|
+
margin-bottom: 6px;
|
|
674
|
+
}
|
|
675
|
+
.install-row {
|
|
676
|
+
display: flex;
|
|
677
|
+
align-items: center;
|
|
678
|
+
gap: 8px;
|
|
679
|
+
padding: 6px 0;
|
|
680
|
+
font-size: 13px;
|
|
681
|
+
}
|
|
682
|
+
.install-label { font-weight: 500; min-width: 90px; }
|
|
683
|
+
.install-status { color: var(--muted); flex: 1; font-size: 12px; }
|
|
684
|
+
button.small { padding: 4px 10px; font-size: 12px; }
|
|
685
|
+
|
|
665
686
|
@media (max-width: 820px) {
|
|
666
687
|
/* Single-column reflow — the rigid 100vh layout doesn't make sense at
|
|
667
688
|
mobile width because the rail stacks above the conversation. Let the
|
|
@@ -243,46 +243,72 @@
|
|
|
243
243
|
PRIMARY_BUTTON.style.display = 'none';
|
|
244
244
|
setHeader('Recruit AI Employees', 'Choose how you want to add AI Employees to this machine.');
|
|
245
245
|
|
|
246
|
-
const
|
|
247
|
-
{
|
|
248
|
-
{
|
|
249
|
-
{
|
|
250
|
-
{ title: 'Bring Your Own Agent', desc: 'Install Cursor, Windsurf, Kiro, VS Code, or another supported AI tool.' },
|
|
246
|
+
const agentOptions = (state.session && state.session.agentOptions) ? state.session.agentOptions : [
|
|
247
|
+
{ id: 'claude-code', label: 'Claude Code' },
|
|
248
|
+
{ id: 'codex', label: 'Codex' },
|
|
249
|
+
{ id: 'gemini-cli', label: 'Gemini CLI' },
|
|
251
250
|
];
|
|
252
251
|
|
|
253
|
-
|
|
252
|
+
const AGENT_DESCS = {
|
|
253
|
+
'claude-code': 'Install Claude Code and connect it to FRAIM.',
|
|
254
|
+
'codex': 'Install Codex and connect it to FRAIM.',
|
|
255
|
+
'gemini-cli': 'Install Gemini CLI and connect it to FRAIM.',
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
for (const opt of agentOptions) {
|
|
254
259
|
const li = document.createElement('li');
|
|
255
260
|
const card = document.createElement('div');
|
|
256
261
|
card.className = 'user-type-card recruit-card';
|
|
262
|
+
card.setAttribute('data-agent-id', opt.id);
|
|
263
|
+
|
|
257
264
|
const title = document.createElement('strong');
|
|
258
265
|
title.className = 'card-title';
|
|
259
|
-
title.textContent =
|
|
266
|
+
title.textContent = opt.label;
|
|
260
267
|
const desc = document.createElement('p');
|
|
261
268
|
desc.className = 'card-desc';
|
|
262
|
-
desc.textContent =
|
|
269
|
+
desc.textContent = AGENT_DESCS[opt.id] || ('Install ' + opt.label + ' and connect it to FRAIM.');
|
|
263
270
|
card.appendChild(title);
|
|
264
271
|
card.appendChild(desc);
|
|
265
272
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
cmd.textContent = 'npx fraim add-ide';
|
|
274
|
-
card.appendChild(cmd);
|
|
275
|
-
}
|
|
273
|
+
const installBtn = document.createElement('button');
|
|
274
|
+
installBtn.type = 'button';
|
|
275
|
+
installBtn.className = 'btn btn-secondary btn-block';
|
|
276
|
+
installBtn.textContent = 'Install';
|
|
277
|
+
installBtn.setAttribute('data-testid', 'install-' + opt.id);
|
|
278
|
+
installBtn.addEventListener('click', () => renderAgentInstallFlow(opt));
|
|
279
|
+
card.appendChild(installBtn);
|
|
276
280
|
|
|
277
281
|
li.appendChild(card);
|
|
278
282
|
CHECKLIST_EL.appendChild(li);
|
|
279
283
|
}
|
|
280
284
|
|
|
285
|
+
const byoaLi = document.createElement('li');
|
|
286
|
+
const byoaCard = document.createElement('div');
|
|
287
|
+
byoaCard.className = 'user-type-card recruit-card';
|
|
288
|
+
const byoaTitle = document.createElement('strong');
|
|
289
|
+
byoaTitle.className = 'card-title';
|
|
290
|
+
byoaTitle.textContent = 'Bring Your Own Agent';
|
|
291
|
+
const byoaDesc = document.createElement('p');
|
|
292
|
+
byoaDesc.className = 'card-desc';
|
|
293
|
+
byoaDesc.textContent = 'Install Cursor, Windsurf, Kiro, VS Code, or another supported AI tool.';
|
|
294
|
+
const byoaNote = document.createElement('p');
|
|
295
|
+
byoaNote.className = 'card-desc';
|
|
296
|
+
byoaNote.textContent = 'When you are done installing your agent, run:';
|
|
297
|
+
const byoaCmd = document.createElement('div');
|
|
298
|
+
byoaCmd.className = 'cmd-block';
|
|
299
|
+
byoaCmd.textContent = 'npx fraim add-ide';
|
|
300
|
+
byoaCard.appendChild(byoaTitle);
|
|
301
|
+
byoaCard.appendChild(byoaDesc);
|
|
302
|
+
byoaCard.appendChild(byoaNote);
|
|
303
|
+
byoaCard.appendChild(byoaCmd);
|
|
304
|
+
byoaLi.appendChild(byoaCard);
|
|
305
|
+
CHECKLIST_EL.appendChild(byoaLi);
|
|
306
|
+
|
|
281
307
|
const continueLi = document.createElement('li');
|
|
282
308
|
const continueBtn = document.createElement('button');
|
|
283
309
|
continueBtn.type = 'button';
|
|
284
310
|
continueBtn.className = 'btn btn-primary btn-block';
|
|
285
|
-
continueBtn.textContent = 'Continue';
|
|
311
|
+
continueBtn.textContent = 'Continue without AI Agent';
|
|
286
312
|
continueBtn.addEventListener('click', async () => {
|
|
287
313
|
try {
|
|
288
314
|
const ideData = await api('/api/first-run/ide-commands');
|
|
@@ -295,6 +321,161 @@
|
|
|
295
321
|
CHECKLIST_EL.appendChild(continueLi);
|
|
296
322
|
}
|
|
297
323
|
|
|
324
|
+
function renderAgentInstallFlow(opt) {
|
|
325
|
+
CHECKLIST_EL.className = 'selection-container recruit-container';
|
|
326
|
+
CHECKLIST_EL.innerHTML = '';
|
|
327
|
+
setHeader('Install ' + opt.label, 'Setting up ' + opt.label + ' on this machine...');
|
|
328
|
+
|
|
329
|
+
const statusLi = document.createElement('li');
|
|
330
|
+
const statusDiv = document.createElement('div');
|
|
331
|
+
statusDiv.className = 'install-status';
|
|
332
|
+
statusDiv.setAttribute('data-testid', 'agent-install-status');
|
|
333
|
+
statusDiv.textContent = 'Installing ' + opt.label + '...';
|
|
334
|
+
statusLi.appendChild(statusDiv);
|
|
335
|
+
CHECKLIST_EL.appendChild(statusLi);
|
|
336
|
+
|
|
337
|
+
(async () => {
|
|
338
|
+
try {
|
|
339
|
+
const result = await api('/api/first-run/install-agent', 'POST', { agentId: opt.id });
|
|
340
|
+
if (!result || !result.ok) {
|
|
341
|
+
statusDiv.textContent = (result && result.message) ? result.message : 'Install failed.';
|
|
342
|
+
statusDiv.setAttribute('data-tone', 'error');
|
|
343
|
+
const actions = document.createElement('div');
|
|
344
|
+
actions.className = 'install-actions';
|
|
345
|
+
const retryBtn = document.createElement('button');
|
|
346
|
+
retryBtn.type = 'button';
|
|
347
|
+
retryBtn.className = 'btn btn-primary';
|
|
348
|
+
retryBtn.textContent = 'Retry';
|
|
349
|
+
retryBtn.addEventListener('click', () => renderAgentInstallFlow(opt));
|
|
350
|
+
const backBtn = document.createElement('button');
|
|
351
|
+
backBtn.type = 'button';
|
|
352
|
+
backBtn.className = 'btn btn-ghost';
|
|
353
|
+
backBtn.textContent = 'Choose a different agent';
|
|
354
|
+
backBtn.addEventListener('click', () => renderRecruitAgents());
|
|
355
|
+
actions.appendChild(retryBtn);
|
|
356
|
+
actions.appendChild(backBtn);
|
|
357
|
+
statusLi.appendChild(actions);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
statusDiv.textContent = opt.label + ' installed successfully!';
|
|
361
|
+
renderAgentLoginStep(opt, result.loginCommand, result.loginHint, statusLi);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
statusDiv.textContent = err.message || 'Install failed.';
|
|
364
|
+
statusDiv.setAttribute('data-tone', 'error');
|
|
365
|
+
const backBtn = document.createElement('button');
|
|
366
|
+
backBtn.type = 'button';
|
|
367
|
+
backBtn.className = 'btn btn-ghost btn-block';
|
|
368
|
+
backBtn.textContent = 'Choose a different agent';
|
|
369
|
+
backBtn.addEventListener('click', () => renderRecruitAgents());
|
|
370
|
+
statusLi.appendChild(backBtn);
|
|
371
|
+
}
|
|
372
|
+
})();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function renderAgentLoginStep(opt, loginCommand, loginHint, parentLi) {
|
|
376
|
+
const hintEl = document.createElement('p');
|
|
377
|
+
hintEl.className = 'install-hint';
|
|
378
|
+
hintEl.textContent = loginHint || ('Sign in to ' + opt.label + ' to activate it.');
|
|
379
|
+
parentLi.appendChild(hintEl);
|
|
380
|
+
|
|
381
|
+
const signInBtn = document.createElement('button');
|
|
382
|
+
signInBtn.type = 'button';
|
|
383
|
+
signInBtn.className = 'btn btn-primary btn-block';
|
|
384
|
+
signInBtn.textContent = 'Sign In to ' + opt.label;
|
|
385
|
+
signInBtn.addEventListener('click', async () => {
|
|
386
|
+
signInBtn.disabled = true;
|
|
387
|
+
signInBtn.textContent = 'Opening terminal...';
|
|
388
|
+
try {
|
|
389
|
+
const result = await api('/api/first-run/trigger-agent-login', 'POST', { agentId: opt.id });
|
|
390
|
+
signInBtn.style.display = 'none';
|
|
391
|
+
renderAgentReadyCheck(opt, result && result.message, parentLi);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
signInBtn.disabled = false;
|
|
394
|
+
signInBtn.textContent = 'Sign In to ' + opt.label;
|
|
395
|
+
setStatus(err.message, 'error');
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
parentLi.appendChild(signInBtn);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function renderAgentReadyCheck(opt, loginMessage, parentLi) {
|
|
402
|
+
const msgEl = document.createElement('p');
|
|
403
|
+
msgEl.className = 'install-hint';
|
|
404
|
+
msgEl.textContent = loginMessage || ('Complete sign-in in the terminal, then click Check if Ready.');
|
|
405
|
+
parentLi.appendChild(msgEl);
|
|
406
|
+
|
|
407
|
+
const checkBtn = document.createElement('button');
|
|
408
|
+
checkBtn.type = 'button';
|
|
409
|
+
checkBtn.className = 'btn btn-primary btn-block';
|
|
410
|
+
checkBtn.textContent = 'Check if Ready';
|
|
411
|
+
|
|
412
|
+
// Declare skipEl before the click handler so it can be hidden on success.
|
|
413
|
+
const skipEl = document.createElement('p');
|
|
414
|
+
const skipLink = document.createElement('button');
|
|
415
|
+
skipLink.type = 'button';
|
|
416
|
+
skipLink.className = 'text-button';
|
|
417
|
+
skipLink.textContent = 'Skip for now — I will sign in later';
|
|
418
|
+
skipLink.addEventListener('click', async () => {
|
|
419
|
+
try {
|
|
420
|
+
const ideData = await api('/api/first-run/ide-commands');
|
|
421
|
+
renderStartWorking(ideData ? ideData.commands : []);
|
|
422
|
+
} catch (_err) {
|
|
423
|
+
renderStartWorking([]);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
skipEl.appendChild(skipLink);
|
|
427
|
+
|
|
428
|
+
checkBtn.addEventListener('click', async () => {
|
|
429
|
+
checkBtn.disabled = true;
|
|
430
|
+
checkBtn.textContent = 'Checking...';
|
|
431
|
+
try {
|
|
432
|
+
const result = await api('/api/first-run/check-agent', 'POST', { agentId: opt.id });
|
|
433
|
+
if (result && result.ready) {
|
|
434
|
+
checkBtn.style.display = 'none';
|
|
435
|
+
skipEl.style.display = 'none';
|
|
436
|
+
msgEl.textContent = opt.label + ' is ready!';
|
|
437
|
+
setHeader(opt.label + ' is ready!', 'You can now open the Hub.');
|
|
438
|
+
renderOpenHubButton(parentLi);
|
|
439
|
+
} else {
|
|
440
|
+
checkBtn.disabled = false;
|
|
441
|
+
checkBtn.textContent = 'Check if Ready';
|
|
442
|
+
setStatus((result && result.message) || (opt.label + ' not detected yet. Complete sign-in and try again.'), 'error');
|
|
443
|
+
}
|
|
444
|
+
} catch (err) {
|
|
445
|
+
checkBtn.disabled = false;
|
|
446
|
+
checkBtn.textContent = 'Check if Ready';
|
|
447
|
+
setStatus(err.message, 'error');
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
parentLi.appendChild(checkBtn);
|
|
452
|
+
parentLi.appendChild(skipEl);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function renderOpenHubButton(parentLi) {
|
|
456
|
+
const openHubBtn = document.createElement('button');
|
|
457
|
+
openHubBtn.type = 'button';
|
|
458
|
+
openHubBtn.className = 'btn btn-primary btn-block';
|
|
459
|
+
openHubBtn.textContent = 'Open Hub';
|
|
460
|
+
openHubBtn.addEventListener('click', async () => {
|
|
461
|
+
openHubBtn.disabled = true;
|
|
462
|
+
setStatus('Opening Hub...');
|
|
463
|
+
try {
|
|
464
|
+
await api('/api/first-run/set-preference', 'POST', { choice: 'hub' });
|
|
465
|
+
} catch (_err) { /* non-fatal */ }
|
|
466
|
+
try {
|
|
467
|
+
const openResp = await api('/api/first-run/open-hub', 'POST');
|
|
468
|
+
if (openResp && openResp.message) setStatus(openResp.message);
|
|
469
|
+
if (openResp && openResp.hubUrl) window.location.replace(openResp.hubUrl);
|
|
470
|
+
else openHubBtn.disabled = false;
|
|
471
|
+
} catch (err) {
|
|
472
|
+
openHubBtn.disabled = false;
|
|
473
|
+
setStatus(err.message, 'error');
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
parentLi.appendChild(openHubBtn);
|
|
477
|
+
}
|
|
478
|
+
|
|
298
479
|
function renderStartWorking(ideCommands) {
|
|
299
480
|
CHECKLIST_EL.className = 'selection-container start-container';
|
|
300
481
|
CHECKLIST_EL.innerHTML = '';
|
|
@@ -342,8 +523,13 @@
|
|
|
342
523
|
hubBtn.disabled = true;
|
|
343
524
|
setStatus('Opening Hub...');
|
|
344
525
|
const openResp = await api('/api/first-run/open-hub', 'POST');
|
|
526
|
+
if (openResp && openResp.needsAgentSetup) {
|
|
527
|
+
renderRecruitAgents();
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
345
530
|
if (openResp && openResp.message) setStatus(openResp.message);
|
|
346
531
|
if (openResp && openResp.hubUrl) window.location.replace(openResp.hubUrl);
|
|
532
|
+
else hubBtn.disabled = false;
|
|
347
533
|
} catch (err) {
|
|
348
534
|
hubBtn.disabled = false;
|
|
349
535
|
setStatus(err.message, 'error');
|
|
@@ -431,6 +617,10 @@
|
|
|
431
617
|
try {
|
|
432
618
|
await api('/api/first-run/set-preference', 'POST', { choice: 'hub' });
|
|
433
619
|
const openResp = await api('/api/first-run/open-hub', 'POST');
|
|
620
|
+
if (openResp && openResp.needsAgentSetup) {
|
|
621
|
+
renderRecruitAgents();
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
434
624
|
if (openResp && openResp.hubUrl) window.location.replace(openResp.hubUrl);
|
|
435
625
|
} catch (err) {
|
|
436
626
|
setStatus(err.message, 'error');
|