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.
@@ -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
- dbService: tryCreateDbService(),
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 () => {
@@ -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
- // Issue #347: scan every line for the three signals the Hub UI cares
991
- // about seekMentoring (drives the tracker), turn-level usage
992
- // (drives the totals tokens/cost), and the agent identity from
993
- // fraim_connect (used to look up cost when the host doesn't emit it).
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 { ...event, ...(seekMentoring ? { seekMentoring } : {}), ...(usage ? { usage } : {}), ...(agentIdentity ? { agentIdentity } : {}) };
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. Key is the
1343
- // sessionId we hand back on startRun; mapping sessionId handlers
1344
- // lets emitPhase() reach the right run's onEvent callback.
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 server keeps its own sessionId map keyed off run.sessionId
1468
- // but at the time the test calls emitPhase(runId), we may only have
1469
- // one handler registered (the latest start). Resolve by sessionId
1470
- // first, fall back to the most recent registered handler.
1471
- resolveSession(_runId) {
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: lastMessage(run.messages, 'manager')?.text || null,
222
+ latestManagerMessage,
208
223
  latestEmployeeMessage: lastMessage(run.messages, 'employee')?.text || null,
209
- transcript: run.messages.map((message) => ({
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;