fraim-framework 2.0.146 → 2.0.148
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/hosts.js +36 -18
- package/dist/src/ai-hub/server.js +50 -16
- package/index.js +1 -1
- package/package.json +1 -1
- package/public/ai-hub/index.html +31 -8
- package/public/ai-hub/script.js +256 -19
- package/public/ai-hub/styles.css +389 -92
- package/public/first-run/index.html +35 -34
- package/public/first-run/script.js +667 -655
- package/public/first-run/styles.css +46 -22
- package/dist/src/cli/commands/test-mcp.js +0 -171
- package/dist/src/cli/setup/first-run.js +0 -242
- package/dist/src/core/config-writer.js +0 -75
- package/dist/src/core/utils/job-aliases.js +0 -47
- package/dist/src/core/utils/workflow-parser.js +0 -174
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) {
|
|
@@ -13,8 +13,6 @@ 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
16
|
const PERSONA_AVATAR_SEEDS = {
|
|
19
17
|
maestro: { seed: 'MAESTRO-founder-mode', bg: 'fde68a' },
|
|
20
18
|
beza: { seed: 'BEZA-strategist', bg: 'c7d2fe' },
|
|
@@ -39,11 +37,51 @@ function buildPersonaAvatarUrl(personaKey) {
|
|
|
39
37
|
const params = new URLSearchParams({ seed: data.seed, backgroundColor: data.bg, radius: '50' });
|
|
40
38
|
return `https://api.dicebear.com/9.x/notionists/svg?${params.toString()}`;
|
|
41
39
|
}
|
|
42
|
-
const db_service_1 = require("../fraim/db-service");
|
|
43
40
|
const catalog_1 = require("./catalog");
|
|
44
41
|
const agent_token_prices_1 = require("../local-mcp-server/agent-token-prices");
|
|
45
42
|
const hosts_1 = require("./hosts");
|
|
46
43
|
const preferences_1 = require("./preferences");
|
|
44
|
+
function loadPersonaCapabilityModule() {
|
|
45
|
+
try {
|
|
46
|
+
// Server deployments include the persona catalog. The npm client package
|
|
47
|
+
// intentionally does not, so Hub setup must degrade without loading it.
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
49
|
+
return require('../config/persona-capability-bundles');
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function getProtectedPersonaForHubJob(jobName) {
|
|
56
|
+
return loadPersonaCapabilityModule()?.getProtectedPersonaForJob(jobName) ?? null;
|
|
57
|
+
}
|
|
58
|
+
function listHubPersonaBundles() {
|
|
59
|
+
return loadPersonaCapabilityModule()?.listPersonaCapabilityBundles() ?? [];
|
|
60
|
+
}
|
|
61
|
+
function buildHubPersonaHireUrl(personaKey, hireMode = 'job') {
|
|
62
|
+
const params = new URLSearchParams({ persona: personaKey, mode: hireMode });
|
|
63
|
+
return `/pricing?${params.toString()}`;
|
|
64
|
+
}
|
|
65
|
+
async function getHubWorkspacePersonaState(dbService, userId, apiKey) {
|
|
66
|
+
try {
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
68
|
+
const service = require('../services/persona-entitlement-service');
|
|
69
|
+
return await service.getWorkspacePersonaState(dbService, userId, apiKey);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function createDefaultDbService() {
|
|
76
|
+
try {
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
78
|
+
const { FraimDbService } = require('../fraim/db-service');
|
|
79
|
+
return new FraimDbService();
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
47
85
|
class AiHubRunRegistry {
|
|
48
86
|
constructor() {
|
|
49
87
|
this.runs = new Map();
|
|
@@ -358,13 +396,7 @@ class AiHubServer {
|
|
|
358
396
|
this.dbService = options.dbService;
|
|
359
397
|
}
|
|
360
398
|
else {
|
|
361
|
-
|
|
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
|
-
}
|
|
399
|
+
this.dbService = createDefaultDbService();
|
|
368
400
|
}
|
|
369
401
|
this.app.use(express_1.default.json());
|
|
370
402
|
this.app.use('/ai-hub', express_1.default.static(resolveAiHubPublicDir()));
|
|
@@ -424,7 +456,7 @@ class AiHubServer {
|
|
|
424
456
|
const rawJobs = (0, catalog_1.discoverEmployeeJobs)(normalizedProjectPath);
|
|
425
457
|
const jobs = rawJobs.map((job) => ({
|
|
426
458
|
...job,
|
|
427
|
-
requiredPersonaKey: (
|
|
459
|
+
requiredPersonaKey: getProtectedPersonaForHubJob(job.id),
|
|
428
460
|
}));
|
|
429
461
|
const managerTemplates = (0, catalog_1.discoverManagerTemplates)(normalizedProjectPath);
|
|
430
462
|
const { personas, subscriptionActive } = await this.computePersonas(apiKey || preferences.apiKey);
|
|
@@ -447,7 +479,7 @@ class AiHubServer {
|
|
|
447
479
|
};
|
|
448
480
|
}
|
|
449
481
|
async computePersonas(apiKey) {
|
|
450
|
-
const allBundles = (
|
|
482
|
+
const allBundles = listHubPersonaBundles();
|
|
451
483
|
const fallbackPersonas = allBundles.map((bundle) => ({
|
|
452
484
|
key: bundle.personaKey,
|
|
453
485
|
displayName: bundle.catalogMetadata.displayName,
|
|
@@ -455,12 +487,14 @@ class AiHubServer {
|
|
|
455
487
|
avatarUrl: buildPersonaAvatarUrl(bundle.personaKey),
|
|
456
488
|
pricingLabel: bundle.catalogMetadata.pricingLabel,
|
|
457
489
|
status: 'locked',
|
|
458
|
-
hireUrl: (
|
|
490
|
+
hireUrl: buildHubPersonaHireUrl(bundle.personaKey, bundle.defaultHireMode),
|
|
459
491
|
}));
|
|
460
492
|
if (!apiKey || !this.dbService)
|
|
461
493
|
return { personas: fallbackPersonas, subscriptionActive: false };
|
|
462
494
|
try {
|
|
463
|
-
const state = await (
|
|
495
|
+
const state = await getHubWorkspacePersonaState(this.dbService, 'anonymous', apiKey);
|
|
496
|
+
if (!state)
|
|
497
|
+
return { personas: fallbackPersonas, subscriptionActive: false };
|
|
464
498
|
const hiredKeys = new Set(state.entitlements
|
|
465
499
|
.filter((e) => e.status === 'active')
|
|
466
500
|
.map((e) => e.personaKey));
|
|
@@ -471,7 +505,7 @@ class AiHubServer {
|
|
|
471
505
|
avatarUrl: buildPersonaAvatarUrl(bundle.personaKey),
|
|
472
506
|
pricingLabel: hiredKeys.has(bundle.personaKey) ? '' : bundle.catalogMetadata.pricingLabel,
|
|
473
507
|
status: (hiredKeys.has(bundle.personaKey) ? 'hired' : 'locked'),
|
|
474
|
-
hireUrl: (
|
|
508
|
+
hireUrl: buildHubPersonaHireUrl(bundle.personaKey, bundle.defaultHireMode),
|
|
475
509
|
}));
|
|
476
510
|
return { personas, subscriptionActive: state.subscriptionActive };
|
|
477
511
|
}
|
|
@@ -613,7 +647,7 @@ class AiHubServer {
|
|
|
613
647
|
phaseHistory: [],
|
|
614
648
|
totals: emptyTotals(),
|
|
615
649
|
lastStatusChangeAt: startTimestamp,
|
|
616
|
-
personaKey: (
|
|
650
|
+
personaKey: getProtectedPersonaForHubJob(jobId),
|
|
617
651
|
// Gemini CLI does not emit a session ID in its output stream;
|
|
618
652
|
// pre-seed one so the Send button is enabled and the continue
|
|
619
653
|
// endpoint can proceed. continueRun for Gemini ignores it.
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-framework",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.148",
|
|
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
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
<div class="page">
|
|
13
13
|
|
|
14
14
|
<header class="header">
|
|
15
|
-
<
|
|
15
|
+
<div class="header-copy">
|
|
16
|
+
<span class="header-eyebrow">FRAIM Hub</span>
|
|
17
|
+
<h1>AI Hub</h1>
|
|
18
|
+
</div>
|
|
16
19
|
<button class="project-button" type="button" id="project-button">
|
|
17
20
|
<span class="folder">Project</span>
|
|
18
21
|
<strong id="project-name">Choose a folder</strong>
|
|
@@ -65,10 +68,17 @@
|
|
|
65
68
|
<div class="layout">
|
|
66
69
|
<aside class="rail">
|
|
67
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>
|
|
68
72
|
<!-- R2.4: team roster — horizontal row of avatar chips per hired persona -->
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
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>
|
|
72
82
|
</aside>
|
|
73
83
|
|
|
74
84
|
<main class="conversation" id="conversation">
|
|
@@ -78,12 +88,21 @@
|
|
|
78
88
|
</div>
|
|
79
89
|
|
|
80
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
|
+
|
|
81
96
|
<div class="conv-header">
|
|
82
|
-
<
|
|
83
|
-
|
|
97
|
+
<div class="title-block">
|
|
98
|
+
<h2 id="active-title"></h2>
|
|
99
|
+
<div class="conv-job" id="active-job"></div>
|
|
100
|
+
</div>
|
|
84
101
|
<div id="artifact-slot"></div>
|
|
85
102
|
</div>
|
|
86
103
|
|
|
104
|
+
<div class="summary-strip" id="summary-strip"></div>
|
|
105
|
+
|
|
87
106
|
<!-- Issue #347 R1: pizza tracker. Hidden when the active job
|
|
88
107
|
declares no phases. Populated by renderTracker() in script.js. -->
|
|
89
108
|
<div class="tracker" id="tracker" aria-label="Job progress" hidden>
|
|
@@ -96,13 +115,16 @@
|
|
|
96
115
|
<span class="latest" id="latest"></span>
|
|
97
116
|
</div>
|
|
98
117
|
|
|
99
|
-
<
|
|
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>
|
|
100
122
|
|
|
101
123
|
<div class="coach">
|
|
102
124
|
<div class="coach-title-row">
|
|
103
125
|
<span class="section-title">Coach the employee</span>
|
|
104
126
|
<span class="active-employee-row">
|
|
105
|
-
<label for="active-employee-select" class="active-employee-label">
|
|
127
|
+
<label for="active-employee-select" class="active-employee-label">tool</label>
|
|
106
128
|
<select id="active-employee-select" class="employee-select inline"></select>
|
|
107
129
|
</span>
|
|
108
130
|
</div>
|
|
@@ -114,6 +136,7 @@
|
|
|
114
136
|
<button class="send-button" type="button" id="send" disabled>Send</button>
|
|
115
137
|
<div class="template-popover" id="template-popover" role="menu" hidden></div>
|
|
116
138
|
</div>
|
|
139
|
+
<div class="coach-note" id="coach-note"></div>
|
|
117
140
|
<!-- Issue #347 R4: run-level totals. Discoverable, not dominating.
|
|
118
141
|
Populated by renderTotals() each poll tick. -->
|
|
119
142
|
<div class="totals" id="totals" aria-label="Run totals" hidden></div>
|