fraim-framework 2.0.184 → 2.0.186
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 +2 -10
- package/dist/src/ai-hub/hosts.js +127 -15
- package/dist/src/ai-hub/openclaw-bridge.js +17 -6
- package/dist/src/ai-hub/remote-hub-gateway.js +88 -0
- package/dist/src/ai-hub/server.js +189 -191
- package/dist/src/api/ai-hub/manager-team.js +93 -0
- package/package.json +1 -1
- package/public/ai-hub/index.html +77 -90
- package/public/ai-hub/review.css +4 -2
- package/public/ai-hub/script.js +733 -553
- package/public/ai-hub/styles.css +162 -35
|
@@ -21,15 +21,6 @@ let isQuitting = false; // distinguishes window-close (→ tray) from app-quit
|
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
22
22
|
// Helpers
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
|
-
function tryCreateDbService() {
|
|
25
|
-
try {
|
|
26
|
-
const loaded = require('../fraim/db-service');
|
|
27
|
-
return new loaded.FraimDbService();
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return undefined;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
24
|
function preferredWindowSize() {
|
|
34
25
|
const { workAreaSize } = electron_1.screen.getPrimaryDisplay();
|
|
35
26
|
return {
|
|
@@ -255,7 +246,8 @@ async function launchDesktopShell(options) {
|
|
|
255
246
|
const certBundle = await (0, cert_store_1.loadOrCreateCert)();
|
|
256
247
|
server = new server_1.AiHubServer({
|
|
257
248
|
projectPath: options.projectPath,
|
|
258
|
-
|
|
249
|
+
// Issue #701: no local DB. Persona/manager-team state resolves through the hosted
|
|
250
|
+
// server via the Hub's default remote gateway (authenticated by the user's API key).
|
|
259
251
|
httpsPort,
|
|
260
252
|
certBundle,
|
|
261
253
|
folderPicker: async () => {
|
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.createHubEvent = exports.createHubMessage = exports.ScriptedHostRuntime = exports.FakeHostRuntime = exports.CliHostRuntime = void 0;
|
|
7
7
|
exports.parseSeekMentoringSignal = parseSeekMentoringSignal;
|
|
8
|
+
exports.parseFraimJobLoadSignal = parseFraimJobLoadSignal;
|
|
8
9
|
exports.parseUsageSignal = parseUsageSignal;
|
|
9
10
|
exports.parseAgentIdentitySignal = parseAgentIdentitySignal;
|
|
10
11
|
exports.detectEmployees = detectEmployees;
|
|
@@ -91,6 +92,58 @@ function parseSeekMentoringSignal(line) {
|
|
|
91
92
|
}
|
|
92
93
|
return null;
|
|
93
94
|
}
|
|
95
|
+
// Issue #710: extract the job name from a get_fraim_job tool call. This
|
|
96
|
+
// is structured known-job evidence; the server still validates the job id
|
|
97
|
+
// against the catalog before promoting any run.
|
|
98
|
+
function parseFraimJobLoadSignal(line) {
|
|
99
|
+
if (!line.includes('get_fraim_job'))
|
|
100
|
+
return null;
|
|
101
|
+
let parsed;
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(line);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
109
|
+
return null;
|
|
110
|
+
const obj = parsed;
|
|
111
|
+
// Codex shape: { type: 'item.started' | 'item.completed', item:
|
|
112
|
+
// { type: 'mcp_tool_call', tool: 'get_fraim_job', arguments: { job } } }.
|
|
113
|
+
if ((obj.type === 'item.started' || obj.type === 'item.completed') &&
|
|
114
|
+
typeof obj.item === 'object' && obj.item !== null) {
|
|
115
|
+
const item = obj.item;
|
|
116
|
+
const tool = typeof item.tool === 'string' ? item.tool : '';
|
|
117
|
+
if (item.type === 'mcp_tool_call' && isGetFraimJobTool(tool)) {
|
|
118
|
+
const sig = readFraimJobFromArgs(item.arguments);
|
|
119
|
+
if (sig)
|
|
120
|
+
return sig;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Claude Code shape: tool_use blocks live inside message.content with
|
|
124
|
+
// an MCP-prefixed name such as mcp__fraim__get_fraim_job.
|
|
125
|
+
const candidates = [obj];
|
|
126
|
+
if (Array.isArray(obj.content))
|
|
127
|
+
candidates.push(...obj.content);
|
|
128
|
+
if (typeof obj.message === 'object' && obj.message !== null) {
|
|
129
|
+
const msg = obj.message;
|
|
130
|
+
if (Array.isArray(msg.content))
|
|
131
|
+
candidates.push(...msg.content);
|
|
132
|
+
}
|
|
133
|
+
for (const candidate of candidates) {
|
|
134
|
+
if (typeof candidate !== 'object' || candidate === null)
|
|
135
|
+
continue;
|
|
136
|
+
const c = candidate;
|
|
137
|
+
const isToolUse = c.type === 'tool_use' || c.type === 'function_call';
|
|
138
|
+
const nameField = typeof c.name === 'string' ? c.name : (typeof c.tool_name === 'string' ? c.tool_name : '');
|
|
139
|
+
if (!isToolUse || !isGetFraimJobTool(nameField))
|
|
140
|
+
continue;
|
|
141
|
+
const sig = readFraimJobFromArgs(c.input || c.arguments || c.parameters);
|
|
142
|
+
if (sig)
|
|
143
|
+
return sig;
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
94
147
|
// Issue #347 — extract per-turn usage from the host's JSON stream.
|
|
95
148
|
// Codex: `{"type":"turn.completed","usage":{input_tokens, cached_input_tokens, output_tokens, reasoning_output_tokens}}`.
|
|
96
149
|
// Claude Code: `{"type":"result", ..., "usage":{input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens}, "total_cost_usd": ...}`.
|
|
@@ -256,6 +309,41 @@ function readAgentFromArgs(args) {
|
|
|
256
309
|
return null;
|
|
257
310
|
return { agentName, agentModel };
|
|
258
311
|
}
|
|
312
|
+
function isGetFraimJobTool(toolName) {
|
|
313
|
+
return toolName === 'get_fraim_job' ||
|
|
314
|
+
toolName === 'mcp__fraim__get_fraim_job' ||
|
|
315
|
+
toolName.endsWith('get_fraim_job');
|
|
316
|
+
}
|
|
317
|
+
function readFraimJobFromArgs(rawArgs) {
|
|
318
|
+
const args = normalizeToolArgs(rawArgs);
|
|
319
|
+
if (!args)
|
|
320
|
+
return null;
|
|
321
|
+
const rawJob = stringValue(args.job) ||
|
|
322
|
+
stringValue(args.jobName) ||
|
|
323
|
+
stringValue(args.job_id) ||
|
|
324
|
+
stringValue(args.name);
|
|
325
|
+
const jobId = rawJob.trim().toLowerCase();
|
|
326
|
+
if (!jobId)
|
|
327
|
+
return null;
|
|
328
|
+
return { jobId, source: 'get_fraim_job' };
|
|
329
|
+
}
|
|
330
|
+
function normalizeToolArgs(rawArgs) {
|
|
331
|
+
if (rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)) {
|
|
332
|
+
return rawArgs;
|
|
333
|
+
}
|
|
334
|
+
if (typeof rawArgs !== 'string' || rawArgs.trim().length === 0)
|
|
335
|
+
return null;
|
|
336
|
+
try {
|
|
337
|
+
const parsed = JSON.parse(rawArgs);
|
|
338
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
339
|
+
return parsed;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
259
347
|
function numberOrNull(v) {
|
|
260
348
|
return typeof v === 'number' && Number.isFinite(v) ? v : null;
|
|
261
349
|
}
|
|
@@ -987,17 +1075,24 @@ function parseHostLine(hostId, line) {
|
|
|
987
1075
|
const trimmed = line.trim();
|
|
988
1076
|
if (!trimmed)
|
|
989
1077
|
return {};
|
|
990
|
-
//
|
|
991
|
-
//
|
|
992
|
-
//
|
|
993
|
-
//
|
|
1078
|
+
// Scan every line for structured signals the Hub UI cares about:
|
|
1079
|
+
// seekMentoring (tracker), get_fraim_job (job identity promotion),
|
|
1080
|
+
// turn-level usage (totals), and fraim_connect agent identity
|
|
1081
|
+
// (cost lookup when the host doesn't emit it).
|
|
994
1082
|
const seekMentoring = parseSeekMentoringSignal(trimmed);
|
|
1083
|
+
const fraimJob = parseFraimJobLoadSignal(trimmed);
|
|
995
1084
|
const usage = parseUsageSignal(trimmed);
|
|
996
1085
|
const agentIdentity = parseAgentIdentitySignal(trimmed);
|
|
997
1086
|
const withSignal = (event) => {
|
|
998
|
-
if (!seekMentoring && !usage && !agentIdentity)
|
|
1087
|
+
if (!seekMentoring && !fraimJob && !usage && !agentIdentity)
|
|
999
1088
|
return event;
|
|
1000
|
-
return {
|
|
1089
|
+
return {
|
|
1090
|
+
...event,
|
|
1091
|
+
...(seekMentoring ? { seekMentoring } : {}),
|
|
1092
|
+
...(fraimJob ? { fraimJob } : {}),
|
|
1093
|
+
...(usage ? { usage } : {}),
|
|
1094
|
+
...(agentIdentity ? { agentIdentity } : {}),
|
|
1095
|
+
};
|
|
1001
1096
|
};
|
|
1002
1097
|
if (hostId === 'codex') {
|
|
1003
1098
|
try {
|
|
@@ -1339,9 +1434,10 @@ class ScriptedHostRuntime {
|
|
|
1339
1434
|
{ id: 'gemini', label: 'Gemini CLI', available: true, detail: 'Scripted test double.', supportsRaw: true },
|
|
1340
1435
|
{ id: 'copilot', label: 'GitHub Copilot CLI', available: true, detail: 'Scripted test double.', supportsRaw: true },
|
|
1341
1436
|
];
|
|
1342
|
-
// Track each active run so the test can emit signals at it.
|
|
1343
|
-
//
|
|
1344
|
-
//
|
|
1437
|
+
// Track each active run so the test can emit signals at it. The Hub
|
|
1438
|
+
// passes the run id as the requested start session id in test/demo paths;
|
|
1439
|
+
// real resume flows still use the host session id. Resolving by the
|
|
1440
|
+
// supplied id first lets concurrent scripted runs receive targeted events.
|
|
1345
1441
|
this.handlersBySession = new Map();
|
|
1346
1442
|
// Tracks runDiscriminant per sessionId so consecutive emitPhase calls
|
|
1347
1443
|
// can resolve onSuccess routing without each test having to repeat it.
|
|
@@ -1352,8 +1448,8 @@ class ScriptedHostRuntime {
|
|
|
1352
1448
|
}
|
|
1353
1449
|
startRun(_hostId, _projectPath, _message, handlers, requestedSessionId) {
|
|
1354
1450
|
const sessionId = requestedSessionId || (0, crypto_1.randomUUID)();
|
|
1355
|
-
handlers.onEvent({ sessionId, raw: 'scripted-session-start' }, 'system');
|
|
1356
1451
|
this.handlersBySession.set(sessionId, handlers);
|
|
1452
|
+
handlers.onEvent({ sessionId, raw: 'scripted-session-start' }, 'system');
|
|
1357
1453
|
return this.spawnDouble();
|
|
1358
1454
|
}
|
|
1359
1455
|
continueRun(_hostId, _projectPath, sessionId, _message, handlers) {
|
|
@@ -1363,6 +1459,7 @@ class ScriptedHostRuntime {
|
|
|
1363
1459
|
}
|
|
1364
1460
|
startDirectRun(_hostId, _message, _projectPath, handlers, requestedSessionId) {
|
|
1365
1461
|
const sessionId = requestedSessionId || (0, crypto_1.randomUUID)();
|
|
1462
|
+
this.handlersBySession.set(sessionId, handlers);
|
|
1366
1463
|
handlers.onEvent({ sessionId, raw: 'scripted-direct-session-start' }, 'system');
|
|
1367
1464
|
return this.spawnDouble();
|
|
1368
1465
|
}
|
|
@@ -1420,6 +1517,18 @@ class ScriptedHostRuntime {
|
|
|
1420
1517
|
},
|
|
1421
1518
|
}, 'stdout');
|
|
1422
1519
|
}
|
|
1520
|
+
// Test API — emit the structured job-load signal that get_fraim_job
|
|
1521
|
+
// produces before the first seekMentoring phase call.
|
|
1522
|
+
emitFraimJobLoad(runId, jobId) {
|
|
1523
|
+
const target = this.resolveSession(runId);
|
|
1524
|
+
if (!target)
|
|
1525
|
+
return;
|
|
1526
|
+
target.handlers.onEvent({
|
|
1527
|
+
sessionId: target.sessionId,
|
|
1528
|
+
raw: `scripted-get_fraim_job:${jobId}`,
|
|
1529
|
+
fraimJob: { jobId, source: 'get_fraim_job' },
|
|
1530
|
+
}, 'stdout');
|
|
1531
|
+
}
|
|
1423
1532
|
// Test API — emit a per-turn usage signal in the normalized shape
|
|
1424
1533
|
// the server expects. Mirrors what parseUsageSignal would produce
|
|
1425
1534
|
// from a real host's stream.
|
|
@@ -1464,13 +1573,16 @@ class ScriptedHostRuntime {
|
|
|
1464
1573
|
this.handlersBySession.clear();
|
|
1465
1574
|
this.discriminantBySession.clear();
|
|
1466
1575
|
}
|
|
1467
|
-
// The Hub
|
|
1468
|
-
//
|
|
1469
|
-
//
|
|
1470
|
-
//
|
|
1471
|
-
resolveSession(
|
|
1576
|
+
// The Hub passes run.id as the requested session id for scripted starts,
|
|
1577
|
+
// so tests/demo controls can target a specific run even when multiple runs
|
|
1578
|
+
// are active. Fall back to the latest handler for older callers that do not
|
|
1579
|
+
// provide a known id.
|
|
1580
|
+
resolveSession(runId) {
|
|
1472
1581
|
if (this.handlersBySession.size === 0)
|
|
1473
1582
|
return null;
|
|
1583
|
+
const direct = this.handlersBySession.get(runId);
|
|
1584
|
+
if (direct)
|
|
1585
|
+
return { sessionId: runId, handlers: direct };
|
|
1474
1586
|
const entries = [...this.handlersBySession.entries()];
|
|
1475
1587
|
const [sessionId, handlers] = entries[entries.length - 1];
|
|
1476
1588
|
return { sessionId, handlers };
|
|
@@ -134,6 +134,7 @@ class OpenClawAiHubBridge {
|
|
|
134
134
|
jobId: run.jobId,
|
|
135
135
|
projectPath,
|
|
136
136
|
lastSeenAt: new Date().toISOString(),
|
|
137
|
+
managerTexts: [inbound.text],
|
|
137
138
|
};
|
|
138
139
|
this.threads.set(key, state);
|
|
139
140
|
this.persistThreadState();
|
|
@@ -147,6 +148,7 @@ class OpenClawAiHubBridge {
|
|
|
147
148
|
}
|
|
148
149
|
run = continued.data;
|
|
149
150
|
state.lastSeenAt = new Date().toISOString();
|
|
151
|
+
state.managerTexts = [...(state.managerTexts ?? []), inbound.text];
|
|
150
152
|
this.persistThreadState();
|
|
151
153
|
}
|
|
152
154
|
const settled = await this.waitForSettlement(run.id);
|
|
@@ -193,6 +195,19 @@ class OpenClawAiHubBridge {
|
|
|
193
195
|
return this.fetchRun(runId);
|
|
194
196
|
}
|
|
195
197
|
presentThread(state, run) {
|
|
198
|
+
// The Hub records the invocation it sent as each manager bubble (#696). For a channel
|
|
199
|
+
// transcript we substitute the user's own words (state.managerTexts, in turn order)
|
|
200
|
+
// for manager-role messages, so the thread reflects what the user actually typed.
|
|
201
|
+
const managerTexts = state.managerTexts ?? [];
|
|
202
|
+
let managerIdx = 0;
|
|
203
|
+
const transcript = run.messages.map((message) => ({
|
|
204
|
+
role: message.role,
|
|
205
|
+
text: message.role === 'manager' && managerIdx < managerTexts.length
|
|
206
|
+
? managerTexts[managerIdx++]
|
|
207
|
+
: message.text,
|
|
208
|
+
createdAt: message.createdAt,
|
|
209
|
+
}));
|
|
210
|
+
const latestManagerMessage = [...transcript].reverse().find((m) => m.role === 'manager')?.text || null;
|
|
196
211
|
return {
|
|
197
212
|
channelId: state.channelId,
|
|
198
213
|
threadId: state.threadId,
|
|
@@ -204,13 +219,9 @@ class OpenClawAiHubBridge {
|
|
|
204
219
|
currentPhase: run.currentPhase || null,
|
|
205
220
|
stages: run.stages || [],
|
|
206
221
|
totals: run.totals || null,
|
|
207
|
-
latestManagerMessage
|
|
222
|
+
latestManagerMessage,
|
|
208
223
|
latestEmployeeMessage: lastMessage(run.messages, 'employee')?.text || null,
|
|
209
|
-
transcript
|
|
210
|
-
role: message.role,
|
|
211
|
-
text: message.text,
|
|
212
|
-
createdAt: message.createdAt,
|
|
213
|
-
})),
|
|
224
|
+
transcript,
|
|
214
225
|
};
|
|
215
226
|
}
|
|
216
227
|
threadKey(channelId, threadId) {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.HttpHubRemoteGateway = void 0;
|
|
7
|
+
exports.resolveFraimRemoteUrl = resolveFraimRemoteUrl;
|
|
8
|
+
const axios_1 = __importDefault(require("axios"));
|
|
9
|
+
function resolveFraimRemoteUrl(explicit) {
|
|
10
|
+
return (explicit || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me').replace(/\/+$/, '');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Default HTTP-backed gateway. Mirrors the ProviderClient axios pattern:
|
|
14
|
+
* base URL from FRAIM_REMOTE_URL, `x-api-key` auth header, 10s timeout.
|
|
15
|
+
*/
|
|
16
|
+
class HttpHubRemoteGateway {
|
|
17
|
+
constructor(serverUrl) {
|
|
18
|
+
this.baseURL = resolveFraimRemoteUrl(serverUrl);
|
|
19
|
+
}
|
|
20
|
+
client(apiKey) {
|
|
21
|
+
return axios_1.default.create({
|
|
22
|
+
baseURL: this.baseURL,
|
|
23
|
+
headers: { 'x-api-key': apiKey, 'Content-Type': 'application/json' },
|
|
24
|
+
timeout: 10000,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async getPersonaState(apiKey) {
|
|
28
|
+
if (!apiKey)
|
|
29
|
+
return null;
|
|
30
|
+
try {
|
|
31
|
+
const res = await this.client(apiKey).get('/api/personas/me');
|
|
32
|
+
return res.data;
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
// 401 (no/expired key) or 404 (feature disabled) => treat as "no state" so the Hub
|
|
36
|
+
// renders a clean locked/not-signed-in view instead of crashing.
|
|
37
|
+
const status = err?.response?.status;
|
|
38
|
+
if (status === 401 || status === 404)
|
|
39
|
+
return null;
|
|
40
|
+
console.warn('[ai-hub] getPersonaState failed:', err?.message || err);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async listManagerTeam(apiKey) {
|
|
45
|
+
if (!apiKey)
|
|
46
|
+
return [];
|
|
47
|
+
try {
|
|
48
|
+
const res = await this.client(apiKey).get('/api/ai-hub/manager-team');
|
|
49
|
+
const team = (res.data?.team ?? []);
|
|
50
|
+
return Array.isArray(team) ? team : [];
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
const status = err?.response?.status;
|
|
54
|
+
if (status === 401 || status === 404)
|
|
55
|
+
return [];
|
|
56
|
+
console.warn('[ai-hub] listManagerTeam failed:', err?.message || err);
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async assignManagerTeam(apiKey, personaKey) {
|
|
61
|
+
if (!apiKey)
|
|
62
|
+
return { status: 401, body: { error: 'authentication_required' } };
|
|
63
|
+
try {
|
|
64
|
+
const res = await this.client(apiKey).post('/api/ai-hub/manager-team/assign', { personaKey });
|
|
65
|
+
return { status: res.status, body: res.data };
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
if (err?.response)
|
|
69
|
+
return { status: err.response.status, body: err.response.data };
|
|
70
|
+
console.warn('[ai-hub] assignManagerTeam failed:', err?.message || err);
|
|
71
|
+
return { status: 500, body: { error: 'internal_error' } };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async removeManagerTeam(apiKey, personaKey) {
|
|
75
|
+
if (!apiKey)
|
|
76
|
+
return;
|
|
77
|
+
try {
|
|
78
|
+
await this.client(apiKey).delete(`/api/ai-hub/manager-team/assign/${encodeURIComponent(personaKey)}`);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
const status = err?.response?.status;
|
|
82
|
+
if (status === 401 || status === 404)
|
|
83
|
+
return;
|
|
84
|
+
console.warn('[ai-hub] removeManagerTeam failed:', err?.message || err);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
exports.HttpHubRemoteGateway = HttpHubRemoteGateway;
|