fraim-hub 2.0.229 → 2.0.232

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.
@@ -0,0 +1,148 @@
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.readCustomEmployees = readCustomEmployees;
7
+ exports.writeCustomEmployee = writeCustomEmployee;
8
+ exports.deleteCustomEmployee = deleteCustomEmployee;
9
+ exports.slugifyDisplayName = slugifyDisplayName;
10
+ exports.resolveEmployee = resolveEmployee;
11
+ exports.buildCustomEmployeePersona = buildCustomEmployeePersona;
12
+ // Issue #945: read/write store for user-authored employees, slug utilities,
13
+ // and the display/attribution resolver union.
14
+ //
15
+ // INVARIANT: resolveEmployee is only for display and run attribution.
16
+ // Entitlement reads (isPersonaHireKey, getProtectedPersonaForJob,
17
+ // evaluatePersonaAccess) must never call resolveEmployee.
18
+ const fs_1 = __importDefault(require("fs"));
19
+ const path_1 = __importDefault(require("path"));
20
+ const persona_hiring_1 = require("../config/persona-hiring");
21
+ const EMPLOYEES_DIR_REL = path_1.default.join('fraim', 'personalized-employee', 'employees');
22
+ function employeesDir(projectDir) {
23
+ return path_1.default.join(projectDir, EMPLOYEES_DIR_REL);
24
+ }
25
+ function slugFor(key) {
26
+ // key is 'custom:<slug>' — file is named '<slug>.json'
27
+ return key.replace(/^custom:/, '');
28
+ }
29
+ function safeSlug(key) {
30
+ // Reject keys whose slug component contains path separators or traversal sequences.
31
+ const slug = slugFor(key);
32
+ if (!slug || /[/\\]|\.\./.test(slug))
33
+ return null;
34
+ return slug;
35
+ }
36
+ function filePath(projectDir, key) {
37
+ const slug = safeSlug(key);
38
+ if (!slug)
39
+ return null;
40
+ return path_1.default.join(employeesDir(projectDir), `${slug}.json`);
41
+ }
42
+ function readCustomEmployees(projectDir) {
43
+ const dir = employeesDir(projectDir);
44
+ if (!fs_1.default.existsSync(dir))
45
+ return [];
46
+ const results = [];
47
+ for (const file of fs_1.default.readdirSync(dir)) {
48
+ if (!file.endsWith('.json'))
49
+ continue;
50
+ try {
51
+ const raw = fs_1.default.readFileSync(path_1.default.join(dir, file), 'utf8');
52
+ const parsed = JSON.parse(raw);
53
+ if (parsed && typeof parsed.key === 'string' && parsed.key.startsWith('custom:')) {
54
+ results.push(parsed);
55
+ }
56
+ }
57
+ catch {
58
+ // silently skip malformed files
59
+ }
60
+ }
61
+ return results;
62
+ }
63
+ function writeCustomEmployee(projectDir, employee) {
64
+ const fp = filePath(projectDir, employee.key);
65
+ if (!fp)
66
+ throw new Error(`Invalid employee key: ${employee.key}`);
67
+ const dir = employeesDir(projectDir);
68
+ fs_1.default.mkdirSync(dir, { recursive: true });
69
+ fs_1.default.writeFileSync(fp, JSON.stringify(employee, null, 2), 'utf8');
70
+ }
71
+ function deleteCustomEmployee(projectDir, key) {
72
+ const fp = filePath(projectDir, key);
73
+ if (!fp || !fs_1.default.existsSync(fp))
74
+ return false;
75
+ fs_1.default.unlinkSync(fp);
76
+ return true;
77
+ }
78
+ function slugifyDisplayName(displayName, existingKeys = []) {
79
+ const base = displayName
80
+ .toLowerCase()
81
+ .replace(/[^a-z0-9]+/g, '-')
82
+ .replace(/^-+|-+$/g, '')
83
+ || 'employee';
84
+ let candidate = `custom:${base}`;
85
+ if (!existingKeys.includes(candidate))
86
+ return candidate;
87
+ let i = 2;
88
+ while (existingKeys.includes(`${candidate}-${i}`))
89
+ i++;
90
+ return `${candidate}-${i}`;
91
+ }
92
+ function resolveEmployee(key, customEmployees) {
93
+ // Custom: key starts with 'custom:'
94
+ if (key.startsWith('custom:')) {
95
+ const emp = customEmployees.find((e) => e.key === key);
96
+ if (!emp)
97
+ return null;
98
+ return {
99
+ key: emp.key,
100
+ displayName: emp.displayName,
101
+ role: emp.role,
102
+ avatarUrl: buildCustomAvatarUrl(emp),
103
+ origin: 'custom',
104
+ };
105
+ }
106
+ // Catalog
107
+ if ((0, persona_hiring_1.isPersonaHireKey)(key)) {
108
+ const entry = persona_hiring_1.PERSONA_HIRE_CATALOG[key];
109
+ return {
110
+ key,
111
+ displayName: entry.displayName,
112
+ role: entry.role,
113
+ avatarUrl: (0, persona_hiring_1.buildPersonaAvatarUrl)(key),
114
+ origin: 'catalog',
115
+ };
116
+ }
117
+ return null;
118
+ }
119
+ // Converts a CustomEmployee to an AiHubPersona for bootstrap / rail rendering.
120
+ // status is always 'hired' (no lock on custom employees); no hireUrl; no seat accounting.
121
+ // jobIds is included so the UI can scope the delegate picker without a second fetch.
122
+ function buildCustomEmployeePersona(emp) {
123
+ return {
124
+ key: emp.key,
125
+ displayName: emp.displayName,
126
+ role: emp.role,
127
+ avatarUrl: buildCustomAvatarUrl(emp),
128
+ pricingLabel: '',
129
+ status: 'hired',
130
+ hireUrl: '',
131
+ seatCount: 0,
132
+ seatsInUse: 0,
133
+ origin: 'custom',
134
+ jobIds: emp.jobIds,
135
+ };
136
+ }
137
+ // Builds an avatar URL for a custom employee.
138
+ // - 'emoji': renders a simple letter-based DiceBear avatar seeded on the display name
139
+ // - 'generated': uses the value as the DiceBear seed directly
140
+ // - 'image': returns an empty string (image upload is deferred; caller falls back to icon)
141
+ function buildCustomAvatarUrl(emp) {
142
+ const { icon, displayName } = emp;
143
+ if (icon.kind === 'image')
144
+ return '';
145
+ const seed = icon.kind === 'generated' ? icon.value : displayName;
146
+ const params = new URLSearchParams({ seed, backgroundColor: 'e0e7ff', radius: '50' });
147
+ return `https://api.dicebear.com/9.x/notionists/svg?${params.toString()}`;
148
+ }
@@ -58,10 +58,8 @@ function parseSeekMentoringSignal(line) {
58
58
  typeof obj.item === 'object' && obj.item !== null) {
59
59
  const item = obj.item;
60
60
  const itemType = item.type;
61
- const tool = typeof item.tool === 'string' ? item.tool : null;
62
- if (itemType === 'mcp_tool_call' &&
63
- (tool === 'seekMentoring' || tool === 'mcp__fraim__seekMentoring')) {
64
- const args = item.arguments;
61
+ if (itemType === 'mcp_tool_call' && isFraimTool(item.tool, 'seekMentoring')) {
62
+ const args = normalizeToolArgs(item.arguments) || undefined;
65
63
  const sig = extractSignalFromArgs(args);
66
64
  if (sig)
67
65
  return sig;
@@ -82,13 +80,9 @@ function parseSeekMentoringSignal(line) {
82
80
  continue;
83
81
  const c = candidate;
84
82
  const isToolUse = c.type === 'tool_use' || c.type === 'function_call';
85
- const nameField = typeof c.name === 'string' ? c.name : (typeof c.tool_name === 'string' ? c.tool_name : '');
86
- const isSeekMentoring = nameField === 'seekMentoring' ||
87
- nameField === 'mcp__fraim__seekMentoring' ||
88
- nameField.endsWith('seekMentoring');
89
- if (!isToolUse || !isSeekMentoring)
83
+ if (!isToolUse || !isFraimTool(readToolName(c), 'seekMentoring'))
90
84
  continue;
91
- const input = (c.input || c.arguments || c.parameters);
85
+ const input = normalizeToolArgs(c.input || c.arguments || c.parameters) || undefined;
92
86
  const sig = extractSignalFromArgs(input);
93
87
  if (sig)
94
88
  return sig;
@@ -116,8 +110,7 @@ function parseFraimJobLoadSignal(line) {
116
110
  if ((obj.type === 'item.started' || obj.type === 'item.completed') &&
117
111
  typeof obj.item === 'object' && obj.item !== null) {
118
112
  const item = obj.item;
119
- const tool = typeof item.tool === 'string' ? item.tool : '';
120
- if (item.type === 'mcp_tool_call' && isGetFraimJobTool(tool)) {
113
+ if (item.type === 'mcp_tool_call' && isFraimTool(item.tool, 'get_fraim_job')) {
121
114
  const sig = readFraimJobFromArgs(item.arguments);
122
115
  if (sig)
123
116
  return sig;
@@ -138,8 +131,7 @@ function parseFraimJobLoadSignal(line) {
138
131
  continue;
139
132
  const c = candidate;
140
133
  const isToolUse = c.type === 'tool_use' || c.type === 'function_call';
141
- const nameField = typeof c.name === 'string' ? c.name : (typeof c.tool_name === 'string' ? c.tool_name : '');
142
- if (!isToolUse || !isGetFraimJobTool(nameField))
134
+ if (!isToolUse || !isFraimTool(readToolName(c), 'get_fraim_job'))
143
135
  continue;
144
136
  const sig = readFraimJobFromArgs(c.input || c.arguments || c.parameters);
145
137
  if (sig)
@@ -276,8 +268,8 @@ function parseAgentIdentitySignal(line) {
276
268
  // Codex shape.
277
269
  if ((obj.type === 'item.started' || obj.type === 'item.completed') && typeof obj.item === 'object' && obj.item !== null) {
278
270
  const item = obj.item;
279
- if (item.type === 'mcp_tool_call' && item.tool === 'fraim_connect') {
280
- return readAgentFromArgs(item.arguments);
271
+ if (item.type === 'mcp_tool_call' && isFraimTool(item.tool, 'fraim_connect')) {
272
+ return readAgentFromArgs(normalizeToolArgs(item.arguments) || undefined);
281
273
  }
282
274
  }
283
275
  // Claude Code shape.
@@ -293,10 +285,9 @@ function parseAgentIdentitySignal(line) {
293
285
  const c = candidate;
294
286
  if (c.type !== 'tool_use' && c.type !== 'function_call')
295
287
  continue;
296
- const name = typeof c.name === 'string' ? c.name : '';
297
- if (!name.endsWith('fraim_connect'))
288
+ if (!isFraimTool(readToolName(c), 'fraim_connect'))
298
289
  continue;
299
- const sig = readAgentFromArgs((c.input || c.arguments));
290
+ const sig = readAgentFromArgs(normalizeToolArgs(c.input || c.arguments || c.parameters) || undefined);
300
291
  if (sig)
301
292
  return sig;
302
293
  }
@@ -312,10 +303,31 @@ function readAgentFromArgs(args) {
312
303
  return null;
313
304
  return { agentName, agentModel };
314
305
  }
315
- function isGetFraimJobTool(toolName) {
316
- return toolName === 'get_fraim_job' ||
317
- toolName === 'mcp__fraim__get_fraim_job' ||
318
- toolName.endsWith('get_fraim_job');
306
+ function readToolName(candidate) {
307
+ if (typeof candidate.name === 'string')
308
+ return candidate.name;
309
+ if (typeof candidate.tool_name === 'string')
310
+ return candidate.tool_name;
311
+ if (typeof candidate.tool === 'string')
312
+ return candidate.tool;
313
+ if (typeof candidate.function === 'object' && candidate.function !== null) {
314
+ const fn = candidate.function;
315
+ if (typeof fn.name === 'string')
316
+ return fn.name;
317
+ }
318
+ return null;
319
+ }
320
+ function canonicalToolName(rawName) {
321
+ if (typeof rawName !== 'string')
322
+ return null;
323
+ const trimmed = rawName.trim();
324
+ if (!trimmed)
325
+ return null;
326
+ const byDoubleUnderscore = trimmed.split('__').filter(Boolean).pop() || trimmed;
327
+ return byDoubleUnderscore.split(/[./:]/).filter(Boolean).pop() || null;
328
+ }
329
+ function isFraimTool(rawName, canonicalName) {
330
+ return canonicalToolName(rawName) === canonicalName;
319
331
  }
320
332
  function readFraimJobFromArgs(rawArgs) {
321
333
  const args = normalizeToolArgs(rawArgs);
@@ -683,12 +695,15 @@ const EMPLOYEE_LABELS = {
683
695
  claude: 'Claude Code',
684
696
  gemini: 'Gemini CLI',
685
697
  copilot: 'GitHub Copilot CLI',
698
+ antigravity: 'Antigravity CLI',
686
699
  };
687
700
  // GitHub Copilot CLI binary name after `npm install -g @github/copilot`.
688
701
  // The @github/copilot package installs a binary named `copilot` on PATH.
689
702
  // Note: the package name is @github/copilot (NOT @github/copilot-cli which
690
703
  // does not exist on npm). The binary is `copilot` (NOT `github-copilot-cli`).
691
704
  const COPILOT_BINARY = 'copilot';
705
+ // Issue #928: agy has no npm package; installed via https://antigravity.google/cli/
706
+ const AGY_BINARY = 'agy';
692
707
  const executableName = (command) => command;
693
708
  function quoteWindowsArg(value) {
694
709
  if (value.length === 0) {
@@ -721,6 +736,8 @@ const availableByVersionProbe = (command) => {
721
736
  function agentBinaryName(id) {
722
737
  if (id === 'copilot')
723
738
  return COPILOT_BINARY;
739
+ if (id === 'antigravity')
740
+ return AGY_BINARY;
724
741
  return executableName(id);
725
742
  }
726
743
  function detectEmployees() {
@@ -989,6 +1006,18 @@ function buildStartPlan(hostId, message, sessionId) {
989
1006
  env: browser.env,
990
1007
  };
991
1008
  }
1009
+ // Issue #928: agy (Antigravity CLI) is TUI-only — no subprocess/headless mode.
1010
+ // supportsSubprocessStream:false is a capability flag for future Branch A
1011
+ // (subprocess mode). Currently unused by spawnHostProcess; TUI output is
1012
+ // handled gracefully by the antigravity branch in parseHostLine.
1013
+ if (hostId === 'antigravity') {
1014
+ return {
1015
+ command: AGY_BINARY,
1016
+ args: ['--dangerously-skip-permissions'],
1017
+ stdin: transformHeadlessFraimMessage(message, 'start'),
1018
+ supportsSubprocessStream: false,
1019
+ };
1020
+ }
992
1021
  const browser = sharedBrowserHostConfig('claude');
993
1022
  return {
994
1023
  command: executableName('claude'),
@@ -1038,6 +1067,15 @@ function buildContinuePlan(hostId, sessionId, message) {
1038
1067
  env: browser.env,
1039
1068
  };
1040
1069
  }
1070
+ // Issue #928: agy resume uses --conversation <sessionId>.
1071
+ if (hostId === 'antigravity') {
1072
+ return {
1073
+ command: AGY_BINARY,
1074
+ args: ['--dangerously-skip-permissions', '--conversation', sessionId],
1075
+ stdin: transformHeadlessFraimMessage(message, 'continue'),
1076
+ supportsSubprocessStream: false,
1077
+ };
1078
+ }
1041
1079
  const browser = sharedBrowserHostConfig('claude');
1042
1080
  return {
1043
1081
  command: executableName('claude'),
@@ -1097,6 +1135,14 @@ function buildDirectStartPlan(hostId, message, sessionId) {
1097
1135
  stdin: DIRECT_PREAMBLE + message,
1098
1136
  };
1099
1137
  }
1138
+ if (hostId === 'antigravity') {
1139
+ return {
1140
+ command: AGY_BINARY,
1141
+ args: ['--dangerously-skip-permissions'],
1142
+ stdin: DIRECT_PREAMBLE + message,
1143
+ supportsSubprocessStream: false,
1144
+ };
1145
+ }
1100
1146
  return {
1101
1147
  command: executableName('claude'),
1102
1148
  args: [
@@ -1204,6 +1250,11 @@ function parseHostLine(hostId, line) {
1204
1250
  return withSignal({ raw: trimmed });
1205
1251
  }
1206
1252
  catch {
1253
+ // Issue #928: detect IneligibleTierError before other notice checks so
1254
+ // the Hub can surface the Antigravity migration prompt.
1255
+ if (trimmed.startsWith('IneligibleTierError')) {
1256
+ return withSignal({ raw: trimmed, geminiDeprecated: true });
1257
+ }
1207
1258
  if (isGeminiCliNotice(trimmed)) {
1208
1259
  return withSignal({ raw: trimmed });
1209
1260
  }
@@ -1211,6 +1262,11 @@ function parseHostLine(hostId, line) {
1211
1262
  return withSignal(message ? { message, raw: trimmed } : { raw: trimmed });
1212
1263
  }
1213
1264
  }
1265
+ // Issue #928: antigravity (agy) is TUI-only. Return raw for any line received;
1266
+ // TUI output is not a structured event stream.
1267
+ if (hostId === 'antigravity') {
1268
+ return withSignal({ raw: trimmed });
1269
+ }
1214
1270
  // GitHub Copilot CLI output: JSON stream where each event carries a `type`
1215
1271
  // field. Known event shapes (from the agentic CLI stream):
1216
1272
  // { "type": "session.started", "session_id": "..." } — session id
@@ -1441,6 +1497,7 @@ class FakeHostRuntime {
1441
1497
  { id: 'claude', label: 'Claude Code', available: true, detail: 'Test double employee.', supportsRaw: true },
1442
1498
  { id: 'gemini', label: 'Gemini CLI', available: true, detail: 'Test double employee.', supportsRaw: true },
1443
1499
  { id: 'copilot', label: 'GitHub Copilot CLI', available: true, detail: 'Test double agent tool.', supportsRaw: true },
1500
+ { id: 'antigravity', label: 'Antigravity CLI', available: true, detail: 'Test double agent tool.', supportsRaw: false },
1444
1501
  ];
1445
1502
  // Remembered across turns like a resumed agent session: the job label from the
1446
1503
  // start turn. Issue #732 — a same-job continue no longer carries a /fraim <job>
@@ -1531,6 +1588,7 @@ class ScriptedHostRuntime {
1531
1588
  { id: 'claude', label: 'Claude Code', available: true, detail: 'Scripted test double.', supportsRaw: true },
1532
1589
  { id: 'gemini', label: 'Gemini CLI', available: true, detail: 'Scripted test double.', supportsRaw: true },
1533
1590
  { id: 'copilot', label: 'GitHub Copilot CLI', available: true, detail: 'Scripted test double.', supportsRaw: true },
1591
+ { id: 'antigravity', label: 'Antigravity CLI', available: true, detail: 'Scripted test double.', supportsRaw: false },
1534
1592
  ];
1535
1593
  // Track each active run so the test can emit signals at it. The Hub
1536
1594
  // passes the run id as the requested start session id in test/demo paths;
@@ -147,7 +147,7 @@ class AiHubPreferencesStore {
147
147
  const removedProjectPaths = normalizeRemovedProjectPaths(raw.removedProjectPaths);
148
148
  return {
149
149
  projectPath: raw.projectPath || projectPath,
150
- employeeId: (raw.employeeId === 'claude' || raw.employeeId === 'codex' || raw.employeeId === 'gemini' || raw.employeeId === 'copilot') ? raw.employeeId : DEFAULT_EMPLOYEE,
150
+ employeeId: (raw.employeeId === 'claude' || raw.employeeId === 'codex' || raw.employeeId === 'gemini' || raw.employeeId === 'copilot' || raw.employeeId === 'antigravity') ? raw.employeeId : DEFAULT_EMPLOYEE,
151
151
  categoryId: typeof raw.categoryId === 'string' && raw.categoryId.length > 0 ? raw.categoryId : DEFAULT_CATEGORY,
152
152
  recentJobIds: Array.isArray(raw.recentJobIds) ? raw.recentJobIds.filter((value) => typeof value === 'string') : [],
153
153
  recentJobInstructions: (typeof raw.recentJobInstructions === 'object' && raw.recentJobInstructions !== null && !Array.isArray(raw.recentJobInstructions))