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.
- package/dist/src/ai-hub/custom-employees.js +148 -0
- package/dist/src/ai-hub/hosts.js +81 -23
- package/dist/src/ai-hub/preferences.js +1 -1
- package/dist/src/ai-hub/server.js +188 -27
- package/dist/src/cli/setup/ide-detector.js +2 -0
- package/dist/src/first-run/types.js +9 -0
- package/package.json +2 -2
- package/public/ai-hub/index.html +65 -0
- package/public/ai-hub/script.js +897 -54
- package/public/ai-hub/styles.css +389 -0
|
@@ -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
|
+
}
|
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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))
|