fraim-framework 2.0.145 → 2.0.147
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/desktop-main.js +10 -1
- package/dist/src/ai-hub/hosts.js +36 -18
- package/dist/src/ai-hub/preferences.js +3 -0
- package/dist/src/ai-hub/server.js +110 -4
- package/dist/src/cli/commands/add-ide.js +22 -21
- package/dist/src/cli/setup/ide-detector.js +13 -1
- package/package.json +1 -1
- package/public/ai-hub/index.html +77 -41
- package/public/ai-hub/script.js +611 -89
- package/public/ai-hub/styles.css +712 -220
- package/public/first-run/index.html +1 -0
- package/public/first-run/script.js +30 -18
- package/public/first-run/styles.css +73 -49
|
@@ -3,6 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.launchDesktopShell = launchDesktopShell;
|
|
4
4
|
const electron_1 = require("electron");
|
|
5
5
|
const server_1 = require("./server");
|
|
6
|
+
const db_service_1 = require("../fraim/db-service");
|
|
7
|
+
function tryCreateDbService() {
|
|
8
|
+
try {
|
|
9
|
+
return new db_service_1.FraimDbService();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
6
15
|
let server = null;
|
|
7
16
|
let mainWindow = null;
|
|
8
17
|
let stopping = null;
|
|
@@ -79,7 +88,7 @@ async function createWindow(url) {
|
|
|
79
88
|
}
|
|
80
89
|
async function launchDesktopShell(options) {
|
|
81
90
|
const port = await (0, server_1.findAvailablePort)(options.preferredPort);
|
|
82
|
-
server = new server_1.AiHubServer({ projectPath: options.projectPath });
|
|
91
|
+
server = new server_1.AiHubServer({ projectPath: options.projectPath, dbService: tryCreateDbService() });
|
|
83
92
|
await server.start(port);
|
|
84
93
|
await createWindow(`http://127.0.0.1:${port}/ai-hub/`);
|
|
85
94
|
}
|
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -253,22 +253,40 @@ function detectEmployees() {
|
|
|
253
253
|
};
|
|
254
254
|
});
|
|
255
255
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
function transformGeminiMessage(message) {
|
|
262
|
-
const match = message.match(/^[/$]fraim\s+(\S+)\n?([\s\S]*)$/);
|
|
256
|
+
function parseFraimInvocation(message) {
|
|
257
|
+
const trimmed = message.trim();
|
|
258
|
+
if (!trimmed)
|
|
259
|
+
return null;
|
|
260
|
+
const match = trimmed.match(/^[/$]fraim(?:\s+(\S+))?\s*([\s\S]*)$/);
|
|
263
261
|
if (!match)
|
|
262
|
+
return null;
|
|
263
|
+
return {
|
|
264
|
+
jobId: match[1] || null,
|
|
265
|
+
remainder: (match[2] || '').trim(),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
// Rewrite UI-facing /fraim or $fraim invocations into direct MCP tool
|
|
269
|
+
// instructions for headless hosts. The Hub still shows the familiar
|
|
270
|
+
// invocation in the manager timeline, but the actual CLI child process
|
|
271
|
+
// receives an explicit instruction that works in non-interactive mode.
|
|
272
|
+
function transformHeadlessFraimMessage(message, kind) {
|
|
273
|
+
const parsed = parseFraimInvocation(message);
|
|
274
|
+
if (!parsed)
|
|
275
|
+
return message;
|
|
276
|
+
if (kind === 'continue') {
|
|
277
|
+
if (parsed.remainder) {
|
|
278
|
+
return `Continue the active FRAIM job with this manager coaching:\n\n${parsed.remainder}`;
|
|
279
|
+
}
|
|
280
|
+
return 'Continue the active FRAIM job using the current session context.';
|
|
281
|
+
}
|
|
282
|
+
if (!parsed.jobId)
|
|
264
283
|
return message;
|
|
265
|
-
const jobId = match[1];
|
|
266
|
-
const instructions = match[2].trim();
|
|
267
284
|
const parts = [
|
|
268
|
-
`Call the get_fraim_job MCP tool with job "${jobId}" to get the full job instructions, then follow them exactly.`,
|
|
285
|
+
`Call the get_fraim_job MCP tool with job "${parsed.jobId}" to get the full job instructions, then follow them exactly.`,
|
|
269
286
|
];
|
|
270
|
-
if (
|
|
271
|
-
parts.push(`\n\nUser instructions: ${
|
|
287
|
+
if (parsed.remainder) {
|
|
288
|
+
parts.push(`\n\nUser instructions: ${parsed.remainder}`);
|
|
289
|
+
}
|
|
272
290
|
return parts.join('');
|
|
273
291
|
}
|
|
274
292
|
// If ~/.gemini/settings.json has a wrong/test FRAIM_API_KEY, patch it with the
|
|
@@ -301,7 +319,7 @@ function buildStartPlan(hostId, message) {
|
|
|
301
319
|
return {
|
|
302
320
|
command: executableName('codex'),
|
|
303
321
|
args: ['exec', '--json', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox'],
|
|
304
|
-
stdin: message,
|
|
322
|
+
stdin: transformHeadlessFraimMessage(message, 'start'),
|
|
305
323
|
};
|
|
306
324
|
}
|
|
307
325
|
if (hostId === 'gemini') {
|
|
@@ -309,13 +327,13 @@ function buildStartPlan(hostId, message) {
|
|
|
309
327
|
return {
|
|
310
328
|
command: executableName('gemini'),
|
|
311
329
|
args: ['--yolo', '--skip-trust'],
|
|
312
|
-
stdin:
|
|
330
|
+
stdin: transformHeadlessFraimMessage(message, 'start'),
|
|
313
331
|
};
|
|
314
332
|
}
|
|
315
333
|
return {
|
|
316
334
|
command: executableName('claude'),
|
|
317
335
|
args: ['-p', '--verbose', '--output-format', 'stream-json', '--dangerously-skip-permissions'],
|
|
318
|
-
stdin: message,
|
|
336
|
+
stdin: transformHeadlessFraimMessage(message, 'start'),
|
|
319
337
|
};
|
|
320
338
|
}
|
|
321
339
|
function buildContinuePlan(hostId, sessionId, message) {
|
|
@@ -323,7 +341,7 @@ function buildContinuePlan(hostId, sessionId, message) {
|
|
|
323
341
|
return {
|
|
324
342
|
command: executableName('codex'),
|
|
325
343
|
args: ['exec', 'resume', '--json', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox', sessionId],
|
|
326
|
-
stdin: message,
|
|
344
|
+
stdin: transformHeadlessFraimMessage(message, 'continue'),
|
|
327
345
|
};
|
|
328
346
|
}
|
|
329
347
|
if (hostId === 'gemini') {
|
|
@@ -332,13 +350,13 @@ function buildContinuePlan(hostId, sessionId, message) {
|
|
|
332
350
|
return {
|
|
333
351
|
command: executableName('gemini'),
|
|
334
352
|
args: ['--yolo', '--skip-trust'],
|
|
335
|
-
stdin: message,
|
|
353
|
+
stdin: transformHeadlessFraimMessage(message, 'continue'),
|
|
336
354
|
};
|
|
337
355
|
}
|
|
338
356
|
return {
|
|
339
357
|
command: executableName('claude'),
|
|
340
358
|
args: ['-p', '--verbose', '--output-format', 'stream-json', '--dangerously-skip-permissions', '-r', sessionId],
|
|
341
|
-
stdin: message,
|
|
359
|
+
stdin: transformHeadlessFraimMessage(message, 'continue'),
|
|
342
360
|
};
|
|
343
361
|
}
|
|
344
362
|
function parseHostLine(hostId, line) {
|
|
@@ -14,6 +14,7 @@ const defaultPreferences = (projectPath) => ({
|
|
|
14
14
|
employeeId: DEFAULT_EMPLOYEE,
|
|
15
15
|
categoryId: DEFAULT_CATEGORY,
|
|
16
16
|
recentJobIds: [],
|
|
17
|
+
personaKey: null,
|
|
17
18
|
});
|
|
18
19
|
class AiHubPreferencesStore {
|
|
19
20
|
constructor(stateFilePath = path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'ai-hub-state.json')) {
|
|
@@ -30,6 +31,8 @@ class AiHubPreferencesStore {
|
|
|
30
31
|
employeeId: (raw.employeeId === 'claude' || raw.employeeId === 'codex') ? raw.employeeId : DEFAULT_EMPLOYEE,
|
|
31
32
|
categoryId: typeof raw.categoryId === 'string' && raw.categoryId.length > 0 ? raw.categoryId : DEFAULT_CATEGORY,
|
|
32
33
|
recentJobIds: Array.isArray(raw.recentJobIds) ? raw.recentJobIds.filter((value) => typeof value === 'string') : [],
|
|
34
|
+
personaKey: typeof raw.personaKey === 'string' ? raw.personaKey : null,
|
|
35
|
+
apiKey: typeof raw.apiKey === 'string' && raw.apiKey.length > 0 ? raw.apiKey : undefined,
|
|
33
36
|
};
|
|
34
37
|
}
|
|
35
38
|
catch {
|
|
@@ -13,6 +13,33 @@ const os_1 = __importDefault(require("os"));
|
|
|
13
13
|
const crypto_1 = require("crypto");
|
|
14
14
|
const child_process_1 = require("child_process");
|
|
15
15
|
const types_1 = require("../first-run/types");
|
|
16
|
+
const persona_entitlement_service_1 = require("../services/persona-entitlement-service");
|
|
17
|
+
const persona_capability_bundles_1 = require("../config/persona-capability-bundles");
|
|
18
|
+
const PERSONA_AVATAR_SEEDS = {
|
|
19
|
+
maestro: { seed: 'MAESTRO-founder-mode', bg: 'fde68a' },
|
|
20
|
+
beza: { seed: 'BEZA-strategist', bg: 'c7d2fe' },
|
|
21
|
+
pam: { seed: 'PAM-product', bg: 'ddd6fe' },
|
|
22
|
+
swen: { seed: 'SWEN-engineer', bg: 'bfdbfe' },
|
|
23
|
+
qasm: { seed: 'QASM-quality', bg: 'a7f3d0' },
|
|
24
|
+
huxley: { seed: 'HUXLEY-design', bg: 'fbcfe8' },
|
|
25
|
+
gautam: { seed: 'Gautam-marketing', bg: 'fed7aa' },
|
|
26
|
+
cela: { seed: 'CELA-legal', bg: 'cbd5e1' },
|
|
27
|
+
sekhar: { seed: 'SEKHAR-security', bg: 'fecaca' },
|
|
28
|
+
ashley: { seed: 'Ashley-assistant', bg: 'fde68a' },
|
|
29
|
+
mandy: { seed: 'MANDY-manager', bg: 'ede9fe' },
|
|
30
|
+
hari: { seed: 'HARI-hr', bg: 'ccfbf1' },
|
|
31
|
+
careena: { seed: 'CAREENA-career-coach', bg: 'e0f2fe' },
|
|
32
|
+
ricardo: { seed: 'RICARDO-recruiter', bg: 'ede9fe' },
|
|
33
|
+
sade: { seed: 'SADE-salesforce-dev', bg: 'bae6fd' },
|
|
34
|
+
sam: { seed: 'SAM-sales-manager', bg: 'bbf7d0' },
|
|
35
|
+
casey: { seed: 'CASEY-customer-cx', bg: 'fecdd3' },
|
|
36
|
+
};
|
|
37
|
+
function buildPersonaAvatarUrl(personaKey) {
|
|
38
|
+
const data = PERSONA_AVATAR_SEEDS[personaKey] ?? { seed: personaKey, bg: 'f1f5f9' };
|
|
39
|
+
const params = new URLSearchParams({ seed: data.seed, backgroundColor: data.bg, radius: '50' });
|
|
40
|
+
return `https://api.dicebear.com/9.x/notionists/svg?${params.toString()}`;
|
|
41
|
+
}
|
|
42
|
+
const db_service_1 = require("../fraim/db-service");
|
|
16
43
|
const catalog_1 = require("./catalog");
|
|
17
44
|
const agent_token_prices_1 = require("../local-mcp-server/agent-token-prices");
|
|
18
45
|
const hosts_1 = require("./hosts");
|
|
@@ -327,6 +354,18 @@ class AiHubServer {
|
|
|
327
354
|
this.projectPath = options.projectPath || process.cwd();
|
|
328
355
|
this.preferencesStore = options.preferencesStore || new preferences_1.AiHubPreferencesStore();
|
|
329
356
|
this.hostRuntime = options.hostRuntime || (process.env.FRAIM_AI_HUB_FAKE_HOST === '1' ? new hosts_1.FakeHostRuntime() : new hosts_1.CliHostRuntime());
|
|
357
|
+
if (options.dbService !== undefined) {
|
|
358
|
+
this.dbService = options.dbService;
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
try {
|
|
362
|
+
this.dbService = new db_service_1.FraimDbService();
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
this.dbService = undefined;
|
|
366
|
+
console.warn('[ai-hub] No dbService — personas will show as locked');
|
|
367
|
+
}
|
|
368
|
+
}
|
|
330
369
|
this.app.use(express_1.default.json());
|
|
331
370
|
this.app.use('/ai-hub', express_1.default.static(resolveAiHubPublicDir()));
|
|
332
371
|
this.app.get('/health', (_req, res) => {
|
|
@@ -338,6 +377,15 @@ class AiHubServer {
|
|
|
338
377
|
return this.app;
|
|
339
378
|
}
|
|
340
379
|
async start(port) {
|
|
380
|
+
if (this.dbService) {
|
|
381
|
+
try {
|
|
382
|
+
await this.dbService.connect();
|
|
383
|
+
}
|
|
384
|
+
catch (err) {
|
|
385
|
+
console.warn('[ai-hub] DB connect failed — personas will show as locked:', err);
|
|
386
|
+
this.dbService = undefined;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
341
389
|
await new Promise((resolve, reject) => {
|
|
342
390
|
this.httpServer = this.app.listen(port, '127.0.0.1');
|
|
343
391
|
this.httpServer.once('listening', () => resolve());
|
|
@@ -358,7 +406,7 @@ class AiHubServer {
|
|
|
358
406
|
});
|
|
359
407
|
this.httpServer = undefined;
|
|
360
408
|
}
|
|
361
|
-
bootstrapResponse(projectPath) {
|
|
409
|
+
async bootstrapResponse(projectPath, apiKey) {
|
|
362
410
|
const normalizedProjectPath = path_1.default.resolve(projectPath || this.projectPath);
|
|
363
411
|
const employees = this.hostRuntime.detectEmployees();
|
|
364
412
|
let preferences = this.preferencesStore.load(normalizedProjectPath);
|
|
@@ -373,8 +421,13 @@ class AiHubServer {
|
|
|
373
421
|
}
|
|
374
422
|
}
|
|
375
423
|
const project = (0, catalog_1.summarizeProject)(normalizedProjectPath);
|
|
376
|
-
const
|
|
424
|
+
const rawJobs = (0, catalog_1.discoverEmployeeJobs)(normalizedProjectPath);
|
|
425
|
+
const jobs = rawJobs.map((job) => ({
|
|
426
|
+
...job,
|
|
427
|
+
requiredPersonaKey: (0, persona_capability_bundles_1.getProtectedPersonaForJob)(job.id),
|
|
428
|
+
}));
|
|
377
429
|
const managerTemplates = (0, catalog_1.discoverManagerTemplates)(normalizedProjectPath);
|
|
430
|
+
const { personas, subscriptionActive } = await this.computePersonas(apiKey || preferences.apiKey);
|
|
378
431
|
// Issue #347: enrich the activeRun the same way GET /runs/:id does
|
|
379
432
|
// so the bootstrap surface (used on first paint) carries stages and
|
|
380
433
|
// live totals — not just the raw run state.
|
|
@@ -388,15 +441,67 @@ class AiHubServer {
|
|
|
388
441
|
jobs,
|
|
389
442
|
managerTemplates,
|
|
390
443
|
employees,
|
|
444
|
+
personas,
|
|
445
|
+
subscriptionActive,
|
|
391
446
|
activeRun,
|
|
392
447
|
};
|
|
393
448
|
}
|
|
449
|
+
async computePersonas(apiKey) {
|
|
450
|
+
const allBundles = (0, persona_capability_bundles_1.listPersonaCapabilityBundles)();
|
|
451
|
+
const fallbackPersonas = allBundles.map((bundle) => ({
|
|
452
|
+
key: bundle.personaKey,
|
|
453
|
+
displayName: bundle.catalogMetadata.displayName,
|
|
454
|
+
role: bundle.catalogMetadata.role,
|
|
455
|
+
avatarUrl: buildPersonaAvatarUrl(bundle.personaKey),
|
|
456
|
+
pricingLabel: bundle.catalogMetadata.pricingLabel,
|
|
457
|
+
status: 'locked',
|
|
458
|
+
hireUrl: (0, persona_entitlement_service_1.buildPersonaHireUrl)(bundle.personaKey, bundle.defaultHireMode),
|
|
459
|
+
}));
|
|
460
|
+
if (!apiKey || !this.dbService)
|
|
461
|
+
return { personas: fallbackPersonas, subscriptionActive: false };
|
|
462
|
+
try {
|
|
463
|
+
const state = await (0, persona_entitlement_service_1.getWorkspacePersonaState)(this.dbService, 'anonymous', apiKey);
|
|
464
|
+
const hiredKeys = new Set(state.entitlements
|
|
465
|
+
.filter((e) => e.status === 'active')
|
|
466
|
+
.map((e) => e.personaKey));
|
|
467
|
+
const personas = allBundles.map((bundle) => ({
|
|
468
|
+
key: bundle.personaKey,
|
|
469
|
+
displayName: bundle.catalogMetadata.displayName,
|
|
470
|
+
role: bundle.catalogMetadata.role,
|
|
471
|
+
avatarUrl: buildPersonaAvatarUrl(bundle.personaKey),
|
|
472
|
+
pricingLabel: hiredKeys.has(bundle.personaKey) ? '' : bundle.catalogMetadata.pricingLabel,
|
|
473
|
+
status: (hiredKeys.has(bundle.personaKey) ? 'hired' : 'locked'),
|
|
474
|
+
hireUrl: (0, persona_entitlement_service_1.buildPersonaHireUrl)(bundle.personaKey, bundle.defaultHireMode),
|
|
475
|
+
}));
|
|
476
|
+
return { personas, subscriptionActive: state.subscriptionActive };
|
|
477
|
+
}
|
|
478
|
+
catch (err) {
|
|
479
|
+
console.error('[ai-hub] persona entitlement lookup failed:', err);
|
|
480
|
+
return { personas: fallbackPersonas, subscriptionActive: false };
|
|
481
|
+
}
|
|
482
|
+
}
|
|
394
483
|
registerRoutes() {
|
|
395
|
-
this.app.get('/api/ai-hub/bootstrap', (req, res) => {
|
|
484
|
+
this.app.get('/api/ai-hub/bootstrap', async (req, res) => {
|
|
396
485
|
const projectPath = typeof req.query.projectPath === 'string' && req.query.projectPath.length > 0
|
|
397
486
|
? req.query.projectPath
|
|
398
487
|
: this.projectPath;
|
|
399
|
-
|
|
488
|
+
// Read API key from header — query-param API keys are prohibited (§3.14)
|
|
489
|
+
const apiKey = typeof req.headers['x-fraim-api-key'] === 'string' ? req.headers['x-fraim-api-key'] : undefined;
|
|
490
|
+
res.json(await this.bootstrapResponse(projectPath, apiKey));
|
|
491
|
+
});
|
|
492
|
+
this.app.post('/api/ai-hub/api-key', (req, res) => {
|
|
493
|
+
const { apiKey } = req.body;
|
|
494
|
+
if (!apiKey || typeof apiKey !== 'string')
|
|
495
|
+
return res.status(400).json({ error: 'apiKey required' });
|
|
496
|
+
const prefs = this.preferencesStore.load(this.projectPath);
|
|
497
|
+
this.preferencesStore.save({ ...prefs, apiKey });
|
|
498
|
+
return res.json({ ok: true });
|
|
499
|
+
});
|
|
500
|
+
this.app.post('/api/ai-hub/preferences', (req, res) => {
|
|
501
|
+
const { personaKey } = req.body;
|
|
502
|
+
const prefs = this.preferencesStore.load(this.projectPath);
|
|
503
|
+
this.preferencesStore.save({ ...prefs, personaKey: personaKey ?? null });
|
|
504
|
+
return res.json({ ok: true });
|
|
400
505
|
});
|
|
401
506
|
this.app.post('/api/ai-hub/project-path/pick', (_req, res) => {
|
|
402
507
|
try {
|
|
@@ -508,6 +613,7 @@ class AiHubServer {
|
|
|
508
613
|
phaseHistory: [],
|
|
509
614
|
totals: emptyTotals(),
|
|
510
615
|
lastStatusChangeAt: startTimestamp,
|
|
616
|
+
personaKey: (0, persona_capability_bundles_1.getProtectedPersonaForJob)(jobId) ?? null,
|
|
511
617
|
// Gemini CLI does not emit a session ID in its output stream;
|
|
512
618
|
// pre-seed one so the Send button is enabled and the continue
|
|
513
619
|
// endpoint can proceed. continueRun for Gemini ignores it.
|
|
@@ -354,38 +354,39 @@ const runAddIDE = async (options) => {
|
|
|
354
354
|
else {
|
|
355
355
|
console.log(chalk_1.default.green('✅ Using existing FRAIM configuration\n'));
|
|
356
356
|
}
|
|
357
|
-
// Detect available IDEs
|
|
358
|
-
const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
|
|
359
|
-
if (detectedIDEs.length === 0) {
|
|
360
|
-
console.log(chalk_1.default.yellow('⚠️ No supported IDEs detected on your system.'));
|
|
361
|
-
console.log(chalk_1.default.gray('Supported IDEs: Claude, Claude Code, Antigravity, Gemini CLI, Kiro, Cursor, VSCode, Codex, Windsurf'));
|
|
362
|
-
console.log(chalk_1.default.blue('\n💡 Install an IDE and run this command again.'));
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
357
|
let idesToConfigure;
|
|
366
358
|
if (options.ide) {
|
|
367
|
-
//
|
|
359
|
+
// Explicit IDE requested: configure it regardless of whether it is
|
|
360
|
+
// currently detected. Use the detected path if available (may differ
|
|
361
|
+
// from the default), otherwise fall back to the default so the config
|
|
362
|
+
// can be pre-staged before the IDE is installed.
|
|
368
363
|
const requestedIDE = (0, ide_detector_1.findIDEByName)(options.ide);
|
|
369
364
|
if (!requestedIDE) {
|
|
370
365
|
console.log(chalk_1.default.red(`❌ IDE "${options.ide}" not supported.`));
|
|
371
366
|
console.log(chalk_1.default.yellow('💡 Use "fraim add-ide --list" to see supported IDEs.'));
|
|
372
367
|
return;
|
|
373
368
|
}
|
|
369
|
+
const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
|
|
374
370
|
const detectedIDE = detectedIDEs.find(ide => ide.name === requestedIDE.name);
|
|
375
|
-
|
|
376
|
-
console.log(chalk_1.default.red(`❌ ${requestedIDE.name} not found on your system.`));
|
|
377
|
-
console.log(chalk_1.default.yellow(`💡 Please install ${requestedIDE.name} and try again.`));
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
idesToConfigure = [detectedIDE];
|
|
381
|
-
}
|
|
382
|
-
else if (options.all) {
|
|
383
|
-
// Configure all detected IDEs
|
|
384
|
-
idesToConfigure = detectedIDEs;
|
|
371
|
+
idesToConfigure = [detectedIDE || requestedIDE];
|
|
385
372
|
}
|
|
386
373
|
else {
|
|
387
|
-
//
|
|
388
|
-
|
|
374
|
+
// Auto-detect or interactive selection.
|
|
375
|
+
const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
|
|
376
|
+
if (detectedIDEs.length === 0) {
|
|
377
|
+
console.log(chalk_1.default.yellow('⚠️ No supported IDEs detected on your system.'));
|
|
378
|
+
console.log(chalk_1.default.gray('Supported IDEs: Claude, Claude Code, Antigravity, Gemini CLI, Kiro, Cursor, VSCode, Codex, Windsurf'));
|
|
379
|
+
console.log(chalk_1.default.blue('\n💡 Install an IDE and run this command again.'));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (options.all) {
|
|
383
|
+
// Configure all detected IDEs
|
|
384
|
+
idesToConfigure = detectedIDEs;
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
// Interactive selection
|
|
388
|
+
idesToConfigure = await promptForIDESelection(detectedIDEs, platformTokens);
|
|
389
|
+
}
|
|
389
390
|
}
|
|
390
391
|
if (idesToConfigure.length === 0) {
|
|
391
392
|
console.log(chalk_1.default.yellow('⚠️ No IDEs selected for configuration.'));
|
|
@@ -199,11 +199,23 @@ const findBestConfigPath = (ide) => {
|
|
|
199
199
|
// Return default path if nothing found (will be created)
|
|
200
200
|
return ide.configPath;
|
|
201
201
|
};
|
|
202
|
+
let _cachedIDEs = null;
|
|
203
|
+
let _cacheTimestamp = 0;
|
|
204
|
+
let _cacheHomeDir = '';
|
|
205
|
+
const DETECT_CACHE_TTL_MS = 5000;
|
|
202
206
|
const detectInstalledIDEs = () => {
|
|
203
|
-
|
|
207
|
+
const now = Date.now();
|
|
208
|
+
const currentHome = os_1.default.homedir();
|
|
209
|
+
if (_cachedIDEs !== null && _cacheHomeDir === currentHome && (now - _cacheTimestamp) < DETECT_CACHE_TTL_MS) {
|
|
210
|
+
return _cachedIDEs;
|
|
211
|
+
}
|
|
212
|
+
_cachedIDEs = exports.IDE_CONFIGS.filter(ide => ide.detectMethod()).map(ide => ({
|
|
204
213
|
...ide,
|
|
205
214
|
configPath: findBestConfigPath(ide)
|
|
206
215
|
}));
|
|
216
|
+
_cacheTimestamp = now;
|
|
217
|
+
_cacheHomeDir = currentHome;
|
|
218
|
+
return _cachedIDEs;
|
|
207
219
|
};
|
|
208
220
|
exports.detectInstalledIDEs = detectInstalledIDEs;
|
|
209
221
|
const getAllSupportedIDEs = () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-framework",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.147",
|
|
4
4
|
"description": "FRAIM: AI Workforce Infrastructure — the organizational capability that turns AI agents into an accountable workforce, their operators into capable AI managers, and executives into leaders with clear optics on AI proficiency.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
package/public/ai-hub/index.html
CHANGED
|
@@ -11,13 +11,16 @@
|
|
|
11
11
|
|
|
12
12
|
<div class="page">
|
|
13
13
|
|
|
14
|
-
<header class="header">
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
<header class="header">
|
|
15
|
+
<div class="header-copy">
|
|
16
|
+
<span class="header-eyebrow">FRAIM Hub</span>
|
|
17
|
+
<h1>AI Hub</h1>
|
|
18
|
+
</div>
|
|
19
|
+
<button class="project-button" type="button" id="project-button">
|
|
20
|
+
<span class="folder">Project</span>
|
|
21
|
+
<strong id="project-name">Choose a folder</strong>
|
|
22
|
+
</button>
|
|
23
|
+
</header>
|
|
21
24
|
|
|
22
25
|
<section class="welcome">
|
|
23
26
|
Hi <strong class="you">there</strong>, remember, you are the
|
|
@@ -63,10 +66,20 @@
|
|
|
63
66
|
</section>
|
|
64
67
|
|
|
65
68
|
<div class="layout">
|
|
66
|
-
<aside class="rail">
|
|
67
|
-
<button class="new-conv" type="button" id="new-conv-btn">+ New job</button>
|
|
68
|
-
<div class="
|
|
69
|
-
|
|
69
|
+
<aside class="rail">
|
|
70
|
+
<button class="new-conv" type="button" id="new-conv-btn">+ New job</button>
|
|
71
|
+
<div class="rail-note">Alpha: browser shell for directing employees across your project.</div>
|
|
72
|
+
<!-- R2.4: team roster — horizontal row of avatar chips per hired persona -->
|
|
73
|
+
<section class="rail-section rail-section--employees">
|
|
74
|
+
<div class="rail-section-label">Hired employees</div>
|
|
75
|
+
<div class="team-roster" id="team-roster" hidden></div>
|
|
76
|
+
</section>
|
|
77
|
+
|
|
78
|
+
<section class="rail-section rail-section--runs">
|
|
79
|
+
<div class="rail-section-label">Runs</div>
|
|
80
|
+
<div class="conv-list" id="conv-list"></div>
|
|
81
|
+
</section>
|
|
82
|
+
</aside>
|
|
70
83
|
|
|
71
84
|
<main class="conversation" id="conversation">
|
|
72
85
|
<div class="empty-state" id="empty">
|
|
@@ -74,16 +87,25 @@
|
|
|
74
87
|
<p>Pick an existing job from the left, or click <strong>+ New job</strong> to give your employee something to work on.</p>
|
|
75
88
|
</div>
|
|
76
89
|
|
|
77
|
-
<div id="active-conv" hidden>
|
|
78
|
-
<div class="conv-
|
|
79
|
-
<
|
|
80
|
-
<div class="
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
<div id="active-conv" hidden>
|
|
91
|
+
<div class="conv-topline">
|
|
92
|
+
<div class="employee-identity" id="active-identity"></div>
|
|
93
|
+
<div class="run-state-pill" id="run-state-pill"></div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div class="conv-header">
|
|
97
|
+
<div class="title-block">
|
|
98
|
+
<h2 id="active-title"></h2>
|
|
99
|
+
<div class="conv-job" id="active-job"></div>
|
|
100
|
+
</div>
|
|
101
|
+
<div id="artifact-slot"></div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div class="summary-strip" id="summary-strip"></div>
|
|
105
|
+
|
|
106
|
+
<!-- Issue #347 R1: pizza tracker. Hidden when the active job
|
|
107
|
+
declares no phases. Populated by renderTracker() in script.js. -->
|
|
108
|
+
<div class="tracker" id="tracker" aria-label="Job progress" hidden>
|
|
87
109
|
<div class="tracker-rows" id="tracker-rows"></div>
|
|
88
110
|
<div class="tracker-note" id="tracker-note" hidden></div>
|
|
89
111
|
</div>
|
|
@@ -93,28 +115,32 @@
|
|
|
93
115
|
<span class="latest" id="latest"></span>
|
|
94
116
|
</div>
|
|
95
117
|
|
|
96
|
-
<
|
|
118
|
+
<section class="thread-surface" aria-label="Manager and employee thread">
|
|
119
|
+
<div class="thread-surface-label">Manager and employee thread</div>
|
|
120
|
+
<div class="messages" id="messages"></div>
|
|
121
|
+
</section>
|
|
97
122
|
|
|
98
123
|
<div class="coach">
|
|
99
|
-
<div class="coach-title-row">
|
|
100
|
-
<span class="section-title">Coach the employee</span>
|
|
101
|
-
<span class="active-employee-row">
|
|
102
|
-
<label for="active-employee-select" class="active-employee-label">
|
|
103
|
-
<select id="active-employee-select" class="employee-select inline"></select>
|
|
104
|
-
</span>
|
|
105
|
-
</div>
|
|
106
|
-
<textarea id="coach-text" placeholder="Tell the employee what to do next…"></textarea>
|
|
107
|
-
<div class="coach-actions">
|
|
108
|
-
<!-- Issue #347 R2: template picker. Hidden when the project
|
|
109
|
-
has no manager-job templates. -->
|
|
110
|
-
<button class="ghost" type="button" id="template-picker-btn" aria-haspopup="menu" aria-expanded="false" hidden>Use a template ▾</button>
|
|
111
|
-
<button class="send-button" type="button" id="send" disabled>Send</button>
|
|
112
|
-
<div class="template-popover" id="template-popover" role="menu" hidden></div>
|
|
113
|
-
</div>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
124
|
+
<div class="coach-title-row">
|
|
125
|
+
<span class="section-title">Coach the employee</span>
|
|
126
|
+
<span class="active-employee-row">
|
|
127
|
+
<label for="active-employee-select" class="active-employee-label">tool</label>
|
|
128
|
+
<select id="active-employee-select" class="employee-select inline"></select>
|
|
129
|
+
</span>
|
|
130
|
+
</div>
|
|
131
|
+
<textarea id="coach-text" placeholder="Tell the employee what to do next…"></textarea>
|
|
132
|
+
<div class="coach-actions">
|
|
133
|
+
<!-- Issue #347 R2: template picker. Hidden when the project
|
|
134
|
+
has no manager-job templates. -->
|
|
135
|
+
<button class="ghost" type="button" id="template-picker-btn" aria-haspopup="menu" aria-expanded="false" hidden>Use a template ▾</button>
|
|
136
|
+
<button class="send-button" type="button" id="send" disabled>Send</button>
|
|
137
|
+
<div class="template-popover" id="template-popover" role="menu" hidden></div>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="coach-note" id="coach-note"></div>
|
|
140
|
+
<!-- Issue #347 R4: run-level totals. Discoverable, not dominating.
|
|
141
|
+
Populated by renderTotals() each poll tick. -->
|
|
142
|
+
<div class="totals" id="totals" aria-label="Run totals" hidden></div>
|
|
143
|
+
</div>
|
|
118
144
|
|
|
119
145
|
<details class="micro" id="micro-manage">
|
|
120
146
|
<summary>Micro-manage — raw host details</summary>
|
|
@@ -138,8 +164,18 @@
|
|
|
138
164
|
</div>
|
|
139
165
|
<div class="modal-body">
|
|
140
166
|
<input class="search" type="text" placeholder="Search jobs…" id="job-search">
|
|
167
|
+
<!-- R3+: persona job filter — only shown when subscription is active -->
|
|
168
|
+
<div id="job-persona-filter" hidden></div>
|
|
141
169
|
<div id="job-catalog"></div>
|
|
142
170
|
</div>
|
|
171
|
+
<!-- R3: hire-required notice — shown when a locked job is clicked -->
|
|
172
|
+
<div id="hire-notice" hidden>
|
|
173
|
+
<p id="hire-notice-text"></p>
|
|
174
|
+
<div class="hire-notice-actions">
|
|
175
|
+
<a id="hire-notice-link" href="#" target="_blank" rel="noopener" class="send-button">Go to Pricing →</a>
|
|
176
|
+
<button id="hire-notice-back" type="button" class="ghost">← Back to all jobs</button>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
143
179
|
<div class="modal-footer">
|
|
144
180
|
<span class="left" id="job-pick-status">Choose a job to continue</span>
|
|
145
181
|
<div class="right">
|