olakai-cli 0.6.7 → 0.8.0

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,3953 @@
1
+ import {
2
+ createAgent,
3
+ getCurrentUser,
4
+ listMyAgents,
5
+ regenerateAgentApiKey
6
+ } from "./chunk-KNGRF4XU.js";
7
+ import {
8
+ CLAUDE_DIR,
9
+ OLAKAI_DIR,
10
+ OLAKAI_HOOK_MARKER,
11
+ SETTINGS_FILE,
12
+ deleteClaudeCodeConfig,
13
+ findConfiguredWorkspace,
14
+ getClaudeCodeConfigPath,
15
+ getClaudeCodeStatus,
16
+ getClaudeDir,
17
+ getLegacyClaudeMonitorConfigPath,
18
+ getMonitorConfigPath,
19
+ getSettingsPath,
20
+ loadClaudeCodeConfig,
21
+ mergeHooksSettings,
22
+ readJsonFile,
23
+ writeClaudeCodeConfig,
24
+ writeJsonFile
25
+ } from "./chunk-KY6OHQZW.js";
26
+ import {
27
+ getBaseUrl,
28
+ getValidToken
29
+ } from "./chunk-AVB4N2UN.js";
30
+
31
+ // src/monitor/plugin.ts
32
+ var TOOL_IDS = [
33
+ "claude-code",
34
+ "codex",
35
+ "cursor",
36
+ "gemini-cli",
37
+ "antigravity"
38
+ ];
39
+ var registry = /* @__PURE__ */ new Map();
40
+ function registerPlugin(plugin) {
41
+ registry.set(plugin.id, plugin);
42
+ }
43
+ function getPlugin(id) {
44
+ if (!isToolId(id)) {
45
+ throw new Error(
46
+ `Unknown tool: "${id}". Supported tools: ${TOOL_IDS.join(", ")}`
47
+ );
48
+ }
49
+ const plugin = registry.get(id);
50
+ if (!plugin) {
51
+ throw new Error(
52
+ `Tool "${id}" is not registered. This is a CLI bug \u2014 please report it.`
53
+ );
54
+ }
55
+ return plugin;
56
+ }
57
+ function listPlugins() {
58
+ return Array.from(registry.values());
59
+ }
60
+ function isToolId(value) {
61
+ return TOOL_IDS.includes(value);
62
+ }
63
+
64
+ // src/monitor/registry.ts
65
+ import * as fs from "fs";
66
+ import * as os from "os";
67
+ import * as path from "path";
68
+ var REGISTRY_VERSION = 1;
69
+ var OLAKAI_HOME_DIRNAME = ".olakai";
70
+ var REGISTRY_FILENAME = "registry.json";
71
+ var TOOL_DESCRIPTORS = {
72
+ "claude-code": {
73
+ scope: "workspace",
74
+ hooksPath: (projectRoot) => path.join(projectRoot, ".claude", "settings.json"),
75
+ source: "CLAUDE_CODE"
76
+ },
77
+ codex: {
78
+ scope: "global",
79
+ hooksPath: (_projectRoot, homeDir) => path.join(homeDir, ".codex", "config.toml"),
80
+ source: "CODEX"
81
+ },
82
+ cursor: {
83
+ scope: "global",
84
+ hooksPath: (_projectRoot, homeDir) => path.join(homeDir, ".cursor", "hooks.json"),
85
+ source: "CURSOR"
86
+ },
87
+ "gemini-cli": {
88
+ scope: "global",
89
+ hooksPath: (_projectRoot, homeDir) => path.join(homeDir, ".gemini", "settings.json"),
90
+ source: "GEMINI_CLI"
91
+ },
92
+ antigravity: {
93
+ scope: "global",
94
+ hooksPath: (_projectRoot, homeDir) => path.join(homeDir, ".gemini", "config", "hooks.json"),
95
+ source: "ANTIGRAVITY"
96
+ }
97
+ };
98
+ function getToolScope(toolId) {
99
+ return TOOL_DESCRIPTORS[toolId].scope;
100
+ }
101
+ function getToolHooksPath(toolId, projectRoot, homeDir = os.homedir()) {
102
+ return TOOL_DESCRIPTORS[toolId].hooksPath(projectRoot, homeDir);
103
+ }
104
+ function getToolSource(toolId) {
105
+ return TOOL_DESCRIPTORS[toolId].source;
106
+ }
107
+ function getOlakaiHomeDir(homeDir) {
108
+ return path.join(homeDir, OLAKAI_HOME_DIRNAME);
109
+ }
110
+ function getRegistryPath(homeDir = os.homedir()) {
111
+ return path.join(getOlakaiHomeDir(homeDir), REGISTRY_FILENAME);
112
+ }
113
+ function emptyRegistry() {
114
+ return { version: REGISTRY_VERSION, workspaces: [] };
115
+ }
116
+ function isRegistryEntry(value) {
117
+ if (typeof value !== "object" || value === null) return false;
118
+ const e = value;
119
+ return typeof e.path === "string" && typeof e.tool === "string" && TOOL_IDS.includes(e.tool) && (e.scope === "workspace" || e.scope === "global") && typeof e.agentId === "string" && typeof e.agentName === "string" && typeof e.source === "string" && typeof e.configPath === "string" && typeof e.hooksPath === "string" && typeof e.monitoringEndpoint === "string" && typeof e.createdAt === "string";
120
+ }
121
+ function readRegistry(homeDir = os.homedir()) {
122
+ const filePath = getRegistryPath(homeDir);
123
+ try {
124
+ if (!fs.existsSync(filePath)) return emptyRegistry();
125
+ const raw = fs.readFileSync(filePath, "utf-8");
126
+ const parsed = JSON.parse(raw);
127
+ if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.workspaces)) {
128
+ return emptyRegistry();
129
+ }
130
+ const workspaces = parsed.workspaces.filter(
131
+ isRegistryEntry
132
+ );
133
+ return { version: REGISTRY_VERSION, workspaces };
134
+ } catch {
135
+ return emptyRegistry();
136
+ }
137
+ }
138
+ function writeRegistry(registry2, homeDir = os.homedir()) {
139
+ const dir = getOlakaiHomeDir(homeDir);
140
+ const filePath = getRegistryPath(homeDir);
141
+ if (!fs.existsSync(dir)) {
142
+ fs.mkdirSync(dir, { recursive: true });
143
+ }
144
+ const body = JSON.stringify(registry2, null, 2) + "\n";
145
+ const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
146
+ try {
147
+ fs.writeFileSync(tmpPath, body, { encoding: "utf-8", mode: 384 });
148
+ fs.renameSync(tmpPath, filePath);
149
+ } catch (err) {
150
+ try {
151
+ if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
152
+ } catch {
153
+ }
154
+ throw err;
155
+ }
156
+ try {
157
+ fs.chmodSync(filePath, 384);
158
+ } catch {
159
+ }
160
+ }
161
+ function sameKey(a, path_, tool) {
162
+ return a.path === path_ && a.tool === tool;
163
+ }
164
+ function upsertEntry(entry, homeDir = os.homedir()) {
165
+ const registry2 = readRegistry(homeDir);
166
+ const idx = registry2.workspaces.findIndex(
167
+ (e) => sameKey(e, entry.path, entry.tool)
168
+ );
169
+ if (idx >= 0) {
170
+ registry2.workspaces[idx] = entry;
171
+ } else {
172
+ registry2.workspaces.push(entry);
173
+ }
174
+ writeRegistry(registry2, homeDir);
175
+ return registry2;
176
+ }
177
+ function removeEntry(entryPath, tool, homeDir = os.homedir()) {
178
+ const registry2 = readRegistry(homeDir);
179
+ const next = registry2.workspaces.filter(
180
+ (e) => !sameKey(e, entryPath, tool)
181
+ );
182
+ if (next.length !== registry2.workspaces.length) {
183
+ registry2.workspaces = next;
184
+ writeRegistry(registry2, homeDir);
185
+ }
186
+ return registry2;
187
+ }
188
+ function readOnDiskConfig(filePath) {
189
+ try {
190
+ if (!fs.existsSync(filePath)) return null;
191
+ const raw = fs.readFileSync(filePath, "utf-8");
192
+ const parsed = JSON.parse(raw);
193
+ if (typeof parsed?.agentId !== "string" || typeof parsed?.agentName !== "string" || typeof parsed?.source !== "string" || typeof parsed?.monitoringEndpoint !== "string") {
194
+ return null;
195
+ }
196
+ return {
197
+ agentId: parsed.agentId,
198
+ agentName: parsed.agentName,
199
+ source: parsed.source,
200
+ monitoringEndpoint: parsed.monitoringEndpoint,
201
+ createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : void 0
202
+ };
203
+ } catch {
204
+ return null;
205
+ }
206
+ }
207
+ function reconcileCurrentWorkspace(cwd, homeDir = os.homedir()) {
208
+ const root = findConfiguredWorkspace(cwd, TOOL_IDS);
209
+ if (!root) return [];
210
+ let registry2 = readRegistry(homeDir);
211
+ let mutated = false;
212
+ for (const toolId of TOOL_IDS) {
213
+ const configPath = getMonitorConfigPath(root, toolId);
214
+ const alreadyRegistered = registry2.workspaces.some(
215
+ (e) => sameKey(e, root, toolId)
216
+ );
217
+ if (alreadyRegistered) continue;
218
+ const config = readOnDiskConfig(configPath);
219
+ if (!config) continue;
220
+ const descriptor = TOOL_DESCRIPTORS[toolId];
221
+ const entry = {
222
+ path: root,
223
+ tool: toolId,
224
+ scope: descriptor.scope,
225
+ agentId: config.agentId,
226
+ agentName: config.agentName,
227
+ source: config.source,
228
+ configPath,
229
+ hooksPath: descriptor.hooksPath(root, homeDir),
230
+ monitoringEndpoint: config.monitoringEndpoint,
231
+ createdAt: config.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
232
+ };
233
+ registry2.workspaces.push(entry);
234
+ mutated = true;
235
+ }
236
+ const legacyClaudePath = getLegacyClaudeMonitorConfigPath(root);
237
+ const claudeRegistered = registry2.workspaces.some(
238
+ (e) => sameKey(e, root, "claude-code")
239
+ );
240
+ if (!claudeRegistered) {
241
+ const legacyConfig = readOnDiskConfig(legacyClaudePath);
242
+ if (legacyConfig) {
243
+ const descriptor = TOOL_DESCRIPTORS["claude-code"];
244
+ registry2.workspaces.push({
245
+ path: root,
246
+ tool: "claude-code",
247
+ scope: descriptor.scope,
248
+ agentId: legacyConfig.agentId,
249
+ agentName: legacyConfig.agentName,
250
+ source: legacyConfig.source,
251
+ configPath: getMonitorConfigPath(root, "claude-code"),
252
+ hooksPath: descriptor.hooksPath(root, homeDir),
253
+ monitoringEndpoint: legacyConfig.monitoringEndpoint,
254
+ createdAt: legacyConfig.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
255
+ });
256
+ mutated = true;
257
+ }
258
+ }
259
+ let persisted = true;
260
+ if (mutated) {
261
+ try {
262
+ writeRegistry(registry2, homeDir);
263
+ } catch {
264
+ persisted = false;
265
+ }
266
+ }
267
+ if (persisted) {
268
+ registry2 = readRegistry(homeDir);
269
+ }
270
+ return registry2.workspaces.filter((e) => e.path === root);
271
+ }
272
+ function formatRegistryTable(registry2, configExists = (p) => fs.existsSync(p)) {
273
+ if (registry2.workspaces.length === 0) {
274
+ return "No monitored workspaces recorded on this machine.\nRun 'olakai monitor init --tool <tool>' to start monitoring.";
275
+ }
276
+ const byTool = /* @__PURE__ */ new Map();
277
+ for (const entry of registry2.workspaces) {
278
+ const list = byTool.get(entry.tool) ?? [];
279
+ list.push(entry);
280
+ byTool.set(entry.tool, list);
281
+ }
282
+ const lines = [];
283
+ for (const toolId of TOOL_IDS) {
284
+ const entries = byTool.get(toolId);
285
+ if (!entries || entries.length === 0) continue;
286
+ lines.push(`${toolId} (${entries.length}):`);
287
+ for (const entry of entries) {
288
+ const missing = configExists(entry.configPath) ? "" : " (config missing)";
289
+ const verified = entry.lastVerifiedAt ? ` \xB7 verified ${entry.lastVerifiedAt}` : "";
290
+ lines.push(
291
+ ` \u2022 ${entry.agentName} [${entry.scope}] \u2014 ${entry.path}${verified}${missing}`
292
+ );
293
+ }
294
+ lines.push("");
295
+ }
296
+ return lines.join("\n").trimEnd();
297
+ }
298
+
299
+ // src/monitor/plugins/claude-code/index.ts
300
+ import * as fs5 from "fs";
301
+ import { spawnSync as spawnSync2 } from "child_process";
302
+
303
+ // src/commands/monitor-state.ts
304
+ import * as fs2 from "fs";
305
+ import * as os2 from "os";
306
+ import * as path2 from "path";
307
+ var STATE_DIR_SEGMENTS = [".olakai", "monitor-state"];
308
+ function getStateDir(homeDir) {
309
+ return path2.join(homeDir, ...STATE_DIR_SEGMENTS);
310
+ }
311
+ function getStateFile(sessionId, homeDir) {
312
+ return path2.join(getStateDir(homeDir), `${sessionId}.json`);
313
+ }
314
+ var debugLogger = null;
315
+ function setDebugLogger(logger) {
316
+ debugLogger = logger;
317
+ }
318
+ function log(label, data) {
319
+ if (debugLogger) {
320
+ try {
321
+ debugLogger(label, data);
322
+ } catch {
323
+ }
324
+ }
325
+ }
326
+ function loadSessionState(sessionId, homeDir = os2.homedir()) {
327
+ if (!sessionId) return null;
328
+ const filePath = getStateFile(sessionId, homeDir);
329
+ try {
330
+ if (!fs2.existsSync(filePath)) return null;
331
+ const raw = fs2.readFileSync(filePath, "utf-8");
332
+ const parsed = JSON.parse(raw);
333
+ if (typeof parsed?.lastUserTimestamp !== "string" || typeof parsed?.lastReportedAt !== "string" || typeof parsed?.numTurnsAtLastReport !== "number") {
334
+ return null;
335
+ }
336
+ return {
337
+ lastUserTimestamp: parsed.lastUserTimestamp,
338
+ lastReportedAt: parsed.lastReportedAt,
339
+ numTurnsAtLastReport: parsed.numTurnsAtLastReport
340
+ };
341
+ } catch (err) {
342
+ log("state-load-failed", {
343
+ sessionId,
344
+ error: err.message
345
+ });
346
+ return null;
347
+ }
348
+ }
349
+ function saveSessionState(sessionId, state, homeDir = os2.homedir()) {
350
+ if (!sessionId) return;
351
+ const dir = getStateDir(homeDir);
352
+ const filePath = getStateFile(sessionId, homeDir);
353
+ try {
354
+ if (!fs2.existsSync(dir)) {
355
+ fs2.mkdirSync(dir, { recursive: true });
356
+ }
357
+ fs2.writeFileSync(filePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
358
+ } catch (err) {
359
+ log("state-save-failed", {
360
+ sessionId,
361
+ error: err.message
362
+ });
363
+ }
364
+ }
365
+ function shouldReportTurn(existing, currentUserTimestamp) {
366
+ if (!currentUserTimestamp) return true;
367
+ if (!existing) return true;
368
+ return existing.lastUserTimestamp !== currentUserTimestamp;
369
+ }
370
+
371
+ // src/monitor/plugins/claude-code/install.ts
372
+ import * as fs3 from "fs";
373
+
374
+ // src/monitor/self-monitor-provision.ts
375
+ import path3 from "path";
376
+
377
+ // src/lib/git.ts
378
+ import { spawnSync } from "child_process";
379
+ function getGitConfigEmail() {
380
+ let result;
381
+ try {
382
+ result = spawnSync("git", ["config", "--global", "user.email"], {
383
+ encoding: "utf8",
384
+ // Cap stdout. A normal `user.email` value is well under 320 bytes
385
+ // (RFC 5321 max email length); 64KB leaves headroom for git config
386
+ // wrappers that print a banner without ever truncating real
387
+ // values. spawnSync sets `result.error` (ENOBUFS) on overflow
388
+ // rather than truncating silently — we check it below.
389
+ maxBuffer: 65536,
390
+ // Don't pipe stderr to our process; we treat any error as "no email".
391
+ stdio: ["ignore", "pipe", "ignore"]
392
+ });
393
+ } catch {
394
+ return null;
395
+ }
396
+ if (result.error) return null;
397
+ if (result.status !== 0) return null;
398
+ const raw = (result.stdout || "").toString().trim();
399
+ if (!raw) return null;
400
+ const EMAIL_RE = /^[^\s@.]+(?:\.[^\s@.]+)*@[^\s@.]+(?:\.[^\s@.]+)*\.[a-zA-Z]{2,}$/;
401
+ if (!EMAIL_RE.test(raw)) return null;
402
+ if (raw.length > 320) return null;
403
+ return raw;
404
+ }
405
+
406
+ // src/monitor/prompt.ts
407
+ import * as readline from "readline";
408
+ function promptUser(question) {
409
+ const rl = readline.createInterface({
410
+ input: process.stdin,
411
+ output: process.stdout
412
+ });
413
+ return new Promise((resolve) => {
414
+ rl.question(question, (answer) => {
415
+ rl.close();
416
+ resolve(answer.trim());
417
+ });
418
+ });
419
+ }
420
+ function isInteractive() {
421
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
422
+ }
423
+
424
+ // src/monitor/self-monitor-provision.ts
425
+ async function provisionSelfMonitorAgent(opts) {
426
+ const me = await getCurrentUser();
427
+ const localPart = (me.email.split("@")[0] ?? "user").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
428
+ const workspaceName = path3.basename(opts.projectRoot).toLowerCase();
429
+ const defaultName = `${workspaceName}-${localPart}`;
430
+ let existing = [];
431
+ try {
432
+ existing = await listMyAgents({
433
+ source: opts.source,
434
+ name: defaultName
435
+ });
436
+ } catch (err) {
437
+ const message = err instanceof Error ? err.message : String(err);
438
+ if (!message.includes("Failed to list your agents")) {
439
+ }
440
+ }
441
+ if (existing.length > 0) {
442
+ const found = existing[0];
443
+ console.log(
444
+ `
445
+ Found your existing ${opts.displayName} agent "${found.name}".`
446
+ );
447
+ console.log(
448
+ "Re-using it requires rotating its API key. Any other workspace currently using this agent will start failing on the next monitor request until it re-runs 'olakai monitor init'."
449
+ );
450
+ const ack = await promptUser("Rotate the API key and reuse? [y/N]: ");
451
+ if (ack.trim().toLowerCase() !== "y") {
452
+ console.error(
453
+ "Cancelled. To use this workspace without affecting the other workspace, run 'olakai monitor init' again and pick a different agent name when prompted."
454
+ );
455
+ process.exit(1);
456
+ }
457
+ const rotated = await regenerateAgentApiKey(found.id);
458
+ return {
459
+ id: found.id,
460
+ name: found.name,
461
+ description: found.description,
462
+ role: found.role,
463
+ // `source` on the listMyAgents response is the AgentSource enum
464
+ // string; the Agent type narrows it more loosely. Cast through
465
+ // the shared union.
466
+ source: found.source,
467
+ apiKey: {
468
+ id: rotated.id,
469
+ key: rotated.key,
470
+ keyMasked: rotated.keyMasked,
471
+ isActive: rotated.isActive
472
+ },
473
+ workflowId: null,
474
+ category: opts.category
475
+ };
476
+ }
477
+ const nameInput = await promptUser(
478
+ `Enter a descriptive name for this agent or accept the recommended name [${defaultName}]: `
479
+ );
480
+ const agentName = nameInput.trim() || defaultName;
481
+ const vcsEmailHint = getGitConfigEmail() ?? void 0;
482
+ try {
483
+ return await createAgent({
484
+ name: agentName,
485
+ description: `${opts.displayName} local agent for ${agentName}`,
486
+ role: "WORKER",
487
+ createApiKey: true,
488
+ source: opts.source,
489
+ ...vcsEmailHint ? { vcsEmailHint } : {}
490
+ });
491
+ } catch (err) {
492
+ const message = err instanceof Error ? err.message : String(err);
493
+ if (message.toLowerCase().includes("already exists") || message.toLowerCase().includes("conflict")) {
494
+ console.log(
495
+ `
496
+ An agent named "${agentName}" already exists on this account (created by someone else or an admin).`
497
+ );
498
+ const retry = await promptUser("Try a different name: ");
499
+ if (!retry.trim()) {
500
+ console.error("No name provided. Aborting.");
501
+ process.exit(1);
502
+ }
503
+ return createAgent({
504
+ name: retry.trim(),
505
+ description: `${opts.displayName} local agent for ${retry.trim()}`,
506
+ role: "WORKER",
507
+ createApiKey: true,
508
+ source: opts.source,
509
+ ...vcsEmailHint ? { vcsEmailHint } : {}
510
+ });
511
+ }
512
+ throw err;
513
+ }
514
+ }
515
+
516
+ // src/monitor/plugins/claude-code/install.ts
517
+ import path4 from "path";
518
+ var CLAUDE_CODE_SOURCE = "claude-code";
519
+ var CLAUDE_CODE_AGENT_SOURCE = "CLAUDE_CODE";
520
+ var CLAUDE_CODE_AGENT_CATEGORY = "CODING";
521
+ async function installClaudeCode(opts) {
522
+ const projectRoot = opts.projectRoot ?? process.cwd();
523
+ const token = getValidToken();
524
+ if (!token) {
525
+ console.error("Not logged in. Run 'olakai login' first.");
526
+ process.exit(1);
527
+ }
528
+ console.log("Setting up Claude Code monitoring for this workspace...\n");
529
+ const agent = await provisionSelfMonitorAgent({
530
+ projectRoot,
531
+ source: CLAUDE_CODE_AGENT_SOURCE,
532
+ displayName: "Claude Code",
533
+ category: CLAUDE_CODE_AGENT_CATEGORY
534
+ });
535
+ const monitoringEndpoint = `${getBaseUrl()}/api/monitoring/prompt`;
536
+ const apiKey = agent.apiKey?.key;
537
+ if (!apiKey) {
538
+ console.error(
539
+ "Internal error: agent provisioned but no API key returned. Please re-run 'olakai monitor init'."
540
+ );
541
+ process.exit(1);
542
+ }
543
+ const claudeDir = getClaudeDir(projectRoot);
544
+ if (!fs3.existsSync(claudeDir)) {
545
+ fs3.mkdirSync(claudeDir, { recursive: true });
546
+ }
547
+ const settingsPath = getSettingsPath(projectRoot);
548
+ const existingSettings = readJsonFile(settingsPath) ?? {};
549
+ const mergedHooks = mergeHooksSettings(existingSettings.hooks);
550
+ const updatedSettings = {
551
+ ...existingSettings,
552
+ hooks: mergedHooks
553
+ };
554
+ writeJsonFile(settingsPath, updatedSettings);
555
+ const monitorConfig = {
556
+ agentId: agent.id,
557
+ apiKey,
558
+ agentName: agent.name,
559
+ source: CLAUDE_CODE_SOURCE,
560
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
561
+ monitoringEndpoint
562
+ };
563
+ writeClaudeCodeConfig(projectRoot, monitorConfig);
564
+ const configPath = getClaudeCodeConfigPath(projectRoot);
565
+ const configRel = path4.relative(projectRoot, configPath);
566
+ console.log("");
567
+ console.log(`\u2713 Agent "${agent.name}" configured (ID: ${agent.id})`);
568
+ if (agent.apiKey?.key) {
569
+ console.log("\u2713 API key generated");
570
+ }
571
+ console.log(
572
+ `\u2713 Claude Code hooks configured in ${CLAUDE_DIR}/${SETTINGS_FILE}`
573
+ );
574
+ console.log(`\u2713 Monitor config saved to ${configRel}`);
575
+ console.log("");
576
+ console.log(
577
+ "Congrats! Monitoring is now active. Claude Code will report activity to Olakai"
578
+ );
579
+ console.log(`on each turn.`);
580
+ console.log("");
581
+ console.log(
582
+ `\u26A0 Ensure ${OLAKAI_DIR}/ is in your .gitignore (it contains your API key)`
583
+ );
584
+ console.log("");
585
+ console.log("To check status: olakai monitor status --tool claude-code");
586
+ console.log("To disable: olakai monitor disable --tool claude-code");
587
+ return {
588
+ agentId: agent.id,
589
+ agentName: agent.name,
590
+ source: CLAUDE_CODE_SOURCE,
591
+ monitoringEndpoint
592
+ };
593
+ }
594
+ async function uninstallClaudeCode(opts) {
595
+ const projectRoot = opts.projectRoot ?? process.cwd();
596
+ const settingsPath = getSettingsPath(projectRoot);
597
+ const settings = readJsonFile(settingsPath);
598
+ if (settings?.hooks) {
599
+ const cleanedHooks = {};
600
+ for (const [event, entries] of Object.entries(settings.hooks)) {
601
+ const filtered = entries.filter(
602
+ (e) => !e.hooks.some((h) => h.command.includes(OLAKAI_HOOK_MARKER))
603
+ );
604
+ if (filtered.length > 0) {
605
+ cleanedHooks[event] = filtered;
606
+ }
607
+ }
608
+ if (Object.keys(cleanedHooks).length > 0) {
609
+ settings.hooks = cleanedHooks;
610
+ } else {
611
+ delete settings.hooks;
612
+ }
613
+ writeJsonFile(settingsPath, settings);
614
+ console.log(`\u2713 Olakai hooks removed from ${CLAUDE_DIR}/${SETTINGS_FILE}`);
615
+ } else {
616
+ console.log("No hooks found in settings.json.");
617
+ }
618
+ if (!opts.keepConfig) {
619
+ const configPath = getClaudeCodeConfigPath(projectRoot);
620
+ const configRel = path4.relative(projectRoot, configPath);
621
+ if (deleteClaudeCodeConfig(projectRoot)) {
622
+ console.log(`\u2713 Monitor config removed (${configRel})`);
623
+ }
624
+ } else {
625
+ const configPath = getClaudeCodeConfigPath(projectRoot);
626
+ const configRel = path4.relative(projectRoot, configPath);
627
+ console.log(`Monitor config retained at ${configRel}`);
628
+ }
629
+ console.log("");
630
+ console.log(
631
+ "Monitoring disabled. Run 'olakai monitor init --tool claude-code' to re-enable."
632
+ );
633
+ }
634
+
635
+ // src/monitor/plugins/claude-code/hook.ts
636
+ import * as fs4 from "fs";
637
+
638
+ // src/commands/monitor-transcript.ts
639
+ var FILE_EDITING_TOOL_NAMES = /* @__PURE__ */ new Set([
640
+ "Edit",
641
+ "Write",
642
+ "MultiEdit"
643
+ ]);
644
+ var BASH_TOOL_NAME = "Bash";
645
+ var SKILL_REGEX = /^\/([\w-]+)(?:\s|$)/;
646
+ function detectSkill(userMessage) {
647
+ if (typeof userMessage !== "string") return void 0;
648
+ const trimmed = userMessage.trimStart();
649
+ if (!trimmed) return void 0;
650
+ const match = SKILL_REGEX.exec(trimmed);
651
+ if (!match) return void 0;
652
+ return match[1];
653
+ }
654
+ function extractTextContent(content) {
655
+ if (typeof content === "string") return content;
656
+ if (!Array.isArray(content)) return "";
657
+ const parts = [];
658
+ for (const block of content) {
659
+ if (block?.type === "text" && typeof block.text === "string") {
660
+ parts.push(block.text);
661
+ }
662
+ }
663
+ return parts.join("\n").trim();
664
+ }
665
+ function isMetaUserMessage(line, text) {
666
+ if (line.isMeta === true) return true;
667
+ if (!text) return true;
668
+ return text.includes("<command-name>") || text.includes("<local-command-caveat>") || text.includes("<command-message>");
669
+ }
670
+ function parseTimestamp(ts) {
671
+ if (typeof ts !== "string" || !ts) return NaN;
672
+ return Date.parse(ts);
673
+ }
674
+ function isCompactionEntry(line) {
675
+ if (line.type !== "system") return false;
676
+ const subtype = line.subtype ?? "";
677
+ return subtype === "compact_boundary" || subtype === "pre_compact" || subtype === "post_compact" || /compact/i.test(subtype);
678
+ }
679
+ function parseTranscript(raw) {
680
+ const empty = {
681
+ prompt: "",
682
+ response: "",
683
+ tokens: 0,
684
+ inputTokens: 0,
685
+ outputTokens: 0,
686
+ modelName: null,
687
+ numTurns: 0,
688
+ toolCallCount: 0,
689
+ filesEditedCount: 0,
690
+ bashCommandCount: 0
691
+ };
692
+ if (!raw) return empty;
693
+ const lines = raw.split("\n");
694
+ let lastUserText = "";
695
+ let lastUserTimestamp = NaN;
696
+ let lastUserTimestampRaw;
697
+ let lastAssistantText = "";
698
+ let lastAssistantTimestamp = NaN;
699
+ let lastAssistantModel = null;
700
+ let lastAssistantInputTokens = 0;
701
+ let lastAssistantOutputTokens = 0;
702
+ let numTurns = 0;
703
+ let currentTurnUserTimestamp = NaN;
704
+ let toolCallCount = 0;
705
+ let bashCommandCount = 0;
706
+ let editedFilePaths = /* @__PURE__ */ new Set();
707
+ for (const rawLine of lines) {
708
+ if (!rawLine) continue;
709
+ let parsed;
710
+ try {
711
+ parsed = JSON.parse(rawLine);
712
+ } catch {
713
+ continue;
714
+ }
715
+ if (isCompactionEntry(parsed)) continue;
716
+ if (parsed.isSidechain === true) continue;
717
+ if (parsed.type === "user" && parsed.message) {
718
+ const text = extractTextContent(parsed.message.content);
719
+ if (isMetaUserMessage(parsed, text)) continue;
720
+ lastUserText = text;
721
+ lastUserTimestamp = parseTimestamp(parsed.timestamp);
722
+ currentTurnUserTimestamp = lastUserTimestamp;
723
+ toolCallCount = 0;
724
+ bashCommandCount = 0;
725
+ editedFilePaths = /* @__PURE__ */ new Set();
726
+ lastUserTimestampRaw = typeof parsed.timestamp === "string" && parsed.timestamp ? parsed.timestamp : void 0;
727
+ } else if (parsed.type === "assistant" && parsed.message) {
728
+ const text = extractTextContent(parsed.message.content);
729
+ numTurns += 1;
730
+ if (text) lastAssistantText = text;
731
+ if (typeof parsed.message.model === "string") {
732
+ lastAssistantModel = parsed.message.model;
733
+ }
734
+ const content = parsed.message.content;
735
+ if (Array.isArray(content)) {
736
+ for (const block of content) {
737
+ try {
738
+ if (!block || block.type !== "tool_use") continue;
739
+ toolCallCount += 1;
740
+ const name = typeof block.name === "string" ? block.name : "";
741
+ if (name === BASH_TOOL_NAME) {
742
+ bashCommandCount += 1;
743
+ continue;
744
+ }
745
+ if (FILE_EDITING_TOOL_NAMES.has(name)) {
746
+ const input = block.input;
747
+ if (input !== null && typeof input === "object" && "file_path" in input) {
748
+ const filePath = input.file_path;
749
+ if (typeof filePath === "string" && filePath) {
750
+ editedFilePaths.add(filePath);
751
+ }
752
+ }
753
+ }
754
+ } catch {
755
+ }
756
+ }
757
+ }
758
+ const usage = parsed.message.usage;
759
+ if (usage) {
760
+ const input = (usage.input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0);
761
+ const output = usage.output_tokens ?? 0;
762
+ lastAssistantInputTokens = input;
763
+ lastAssistantOutputTokens = output;
764
+ }
765
+ const ts = parseTimestamp(parsed.timestamp);
766
+ if (!Number.isNaN(ts)) {
767
+ lastAssistantTimestamp = ts;
768
+ }
769
+ }
770
+ }
771
+ const result = {
772
+ prompt: lastUserText,
773
+ response: lastAssistantText,
774
+ tokens: lastAssistantInputTokens + lastAssistantOutputTokens,
775
+ inputTokens: lastAssistantInputTokens,
776
+ outputTokens: lastAssistantOutputTokens,
777
+ modelName: lastAssistantModel,
778
+ numTurns,
779
+ toolCallCount,
780
+ filesEditedCount: editedFilePaths.size,
781
+ bashCommandCount
782
+ };
783
+ if (!Number.isNaN(currentTurnUserTimestamp) && !Number.isNaN(lastAssistantTimestamp) && lastAssistantTimestamp >= currentTurnUserTimestamp) {
784
+ result.latencyMs = Math.round(
785
+ lastAssistantTimestamp - currentTurnUserTimestamp
786
+ );
787
+ }
788
+ const skill = detectSkill(lastUserText);
789
+ if (skill) {
790
+ result.skill = skill;
791
+ }
792
+ if (lastUserTimestampRaw) {
793
+ result.userTurnTimestamp = lastUserTimestampRaw;
794
+ }
795
+ return result;
796
+ }
797
+
798
+ // src/monitor/plugins/claude-code/hook.ts
799
+ var noopDebug = () => {
800
+ };
801
+ function extractFromTranscript(transcriptPath, debugLog6 = noopDebug) {
802
+ const empty = {
803
+ prompt: "",
804
+ response: "",
805
+ tokens: 0,
806
+ inputTokens: 0,
807
+ outputTokens: 0,
808
+ modelName: null,
809
+ numTurns: 0,
810
+ toolCallCount: 0,
811
+ filesEditedCount: 0,
812
+ bashCommandCount: 0
813
+ };
814
+ if (!transcriptPath) return empty;
815
+ let raw;
816
+ try {
817
+ raw = fs4.readFileSync(transcriptPath, "utf-8");
818
+ } catch (err) {
819
+ debugLog6("transcript-read-failed", {
820
+ transcriptPath,
821
+ error: err.message
822
+ });
823
+ return empty;
824
+ }
825
+ return parseTranscript(raw);
826
+ }
827
+ function extractSubagentName(event) {
828
+ const candidates = [
829
+ event.agent_name,
830
+ event.subagent_type,
831
+ event.agent_type,
832
+ event.tool_input?.subagent_type
833
+ ];
834
+ for (const value of candidates) {
835
+ if (typeof value === "string" && value.trim()) {
836
+ return value.trim();
837
+ }
838
+ }
839
+ return void 0;
840
+ }
841
+ function buildClaudeCodePayload(event, eventData, config) {
842
+ const sessionId = eventData.session_id ?? `claude-code-${Date.now()}`;
843
+ switch (event) {
844
+ case "stop":
845
+ case "subagent-stop": {
846
+ const extracted = extractFromTranscript(eventData.transcript_path);
847
+ const isSubagent = event === "subagent-stop";
848
+ const customData = {
849
+ hookEvent: eventData.hook_event_name ?? (isSubagent ? "SubagentStop" : "Stop"),
850
+ sessionId,
851
+ transcriptPath: eventData.transcript_path ?? "",
852
+ cwd: eventData.cwd ?? "",
853
+ stopHookActive: eventData.stop_hook_active ?? false,
854
+ inputTokens: extracted.inputTokens,
855
+ outputTokens: extracted.outputTokens,
856
+ numTurns: extracted.numTurns,
857
+ // Per-turn work signals for the Claude Code classifier (D-027).
858
+ // Always emitted as JSON numbers — the backend classifier and
859
+ // KPI formulas must see numeric 0, not missing keys or strings.
860
+ toolCallCount: extracted.toolCallCount,
861
+ filesEditedCount: extracted.filesEditedCount,
862
+ bashCommandCount: extracted.bashCommandCount
863
+ };
864
+ if (typeof extracted.latencyMs === "number") {
865
+ customData.latencyMs = extracted.latencyMs;
866
+ }
867
+ if (isSubagent) {
868
+ const subagent = extractSubagentName(eventData);
869
+ if (subagent) {
870
+ customData.subagent = subagent;
871
+ }
872
+ } else if (extracted.skill) {
873
+ customData.skill = extracted.skill;
874
+ }
875
+ const payloadAssistant = eventData.last_assistant_message;
876
+ const response = typeof payloadAssistant === "string" && payloadAssistant.trim() ? payloadAssistant : extracted.response;
877
+ return {
878
+ prompt: extracted.prompt,
879
+ response,
880
+ chatId: sessionId,
881
+ source: config.source,
882
+ modelName: extracted.modelName ?? void 0,
883
+ tokens: extracted.tokens,
884
+ customData
885
+ };
886
+ }
887
+ default:
888
+ return null;
889
+ }
890
+ }
891
+
892
+ // src/monitor/plugins/claude-code/index.ts
893
+ var TOOL_ID = "claude-code";
894
+ var SUPPORTED_HOOK_EVENTS = /* @__PURE__ */ new Set(["stop", "subagent-stop"]);
895
+ function debugLog(label, data) {
896
+ if (process.env.OLAKAI_MONITOR_DEBUG !== "1") return;
897
+ try {
898
+ const logPath = `/tmp/olakai-monitor-debug-${process.pid}.log`;
899
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${label}: ${typeof data === "string" ? data : JSON.stringify(data, null, 2)}
900
+ `;
901
+ fs5.appendFileSync(logPath, line, "utf-8");
902
+ } catch {
903
+ }
904
+ }
905
+ function resolveProjectRootFromPayload(eventData, fallbackCwd) {
906
+ const payloadCwd = typeof eventData.cwd === "string" && eventData.cwd.trim() ? eventData.cwd : fallbackCwd;
907
+ return findConfiguredWorkspace(payloadCwd, [TOOL_ID]);
908
+ }
909
+ var claudeCodePlugin = {
910
+ id: TOOL_ID,
911
+ displayName: "Claude Code",
912
+ install(opts) {
913
+ return installClaudeCode(opts);
914
+ },
915
+ uninstall(opts) {
916
+ return uninstallClaudeCode(opts);
917
+ },
918
+ status(opts) {
919
+ return getClaudeCodeStatus(opts);
920
+ },
921
+ async handleHook(eventName, payloadJson) {
922
+ setDebugLogger(debugLog);
923
+ const event = eventName.trim();
924
+ if (!SUPPORTED_HOOK_EVENTS.has(event)) {
925
+ debugLog("hook-unknown-event", event);
926
+ return null;
927
+ }
928
+ const eventData = payloadJson ?? {};
929
+ debugLog("event-parsed", { event, eventData });
930
+ const projectRoot = resolveProjectRootFromPayload(
931
+ eventData,
932
+ process.cwd()
933
+ );
934
+ if (!projectRoot) {
935
+ debugLog("config-not-found", {
936
+ startDir: typeof eventData.cwd === "string" && eventData.cwd.trim() ? eventData.cwd : process.cwd()
937
+ });
938
+ return null;
939
+ }
940
+ const config = loadClaudeCodeConfig(projectRoot, () => {
941
+ });
942
+ if (!config) {
943
+ debugLog("config-load-failed", { projectRoot });
944
+ return null;
945
+ }
946
+ const payload = buildClaudeCodePayload(event, eventData, config);
947
+ if (!payload) return null;
948
+ debugLog("payload-built", payload);
949
+ const sessionId = typeof eventData.session_id === "string" && eventData.session_id || void 0;
950
+ const extracted = extractFromTranscript(
951
+ eventData.transcript_path,
952
+ debugLog
953
+ );
954
+ const userTurnTimestamp = extracted.userTurnTimestamp;
955
+ if (extracted.prompt.trim() === "" && extracted.response.trim() === "" && extracted.numTurns === 0) {
956
+ debugLog("empty-parse-skip", {
957
+ event,
958
+ sessionId,
959
+ transcriptPath: eventData.transcript_path
960
+ });
961
+ return null;
962
+ }
963
+ if (sessionId) {
964
+ const existingState = loadSessionState(sessionId);
965
+ if (!shouldReportTurn(existingState, userTurnTimestamp)) {
966
+ debugLog("turn-dedup-skip", { sessionId, userTurnTimestamp });
967
+ return null;
968
+ }
969
+ }
970
+ if (sessionId && userTurnTimestamp) {
971
+ saveSessionState(sessionId, {
972
+ lastUserTimestamp: userTurnTimestamp,
973
+ lastReportedAt: (/* @__PURE__ */ new Date()).toISOString(),
974
+ numTurnsAtLastReport: extracted.numTurns
975
+ });
976
+ debugLog("state-saved", {
977
+ sessionId,
978
+ userTurnTimestamp,
979
+ numTurns: extracted.numTurns
980
+ });
981
+ }
982
+ return {
983
+ payload,
984
+ transport: {
985
+ endpoint: config.monitoringEndpoint,
986
+ apiKey: config.apiKey,
987
+ projectRoot
988
+ }
989
+ };
990
+ },
991
+ async detectInstalled(opts) {
992
+ const projectRoot = opts?.projectRoot ?? process.cwd();
993
+ try {
994
+ if (fs5.existsSync(getMonitorConfigPath(projectRoot, TOOL_ID))) {
995
+ return true;
996
+ }
997
+ if (fs5.existsSync(getLegacyClaudeMonitorConfigPath(projectRoot))) {
998
+ return true;
999
+ }
1000
+ } catch {
1001
+ }
1002
+ return detectClaudeBinaryOnPath();
1003
+ }
1004
+ };
1005
+ function detectClaudeBinaryOnPath() {
1006
+ try {
1007
+ const probe = spawnSync2(
1008
+ process.platform === "win32" ? "where" : "which",
1009
+ ["claude"],
1010
+ {
1011
+ stdio: ["ignore", "pipe", "ignore"],
1012
+ timeout: 1e3
1013
+ }
1014
+ );
1015
+ if (probe.status === 0 && probe.stdout && probe.stdout.toString().trim()) {
1016
+ return true;
1017
+ }
1018
+ } catch {
1019
+ }
1020
+ return false;
1021
+ }
1022
+ registerPlugin(claudeCodePlugin);
1023
+
1024
+ // src/monitor/plugins/codex/index.ts
1025
+ import * as fs10 from "fs";
1026
+ import { spawnSync as spawnSync3 } from "child_process";
1027
+
1028
+ // src/monitor/plugins/codex/install.ts
1029
+ import * as fs7 from "fs";
1030
+ import * as path7 from "path";
1031
+ import * as TOML from "@iarna/toml";
1032
+
1033
+ // src/monitor/plugins/codex/paths.ts
1034
+ import * as os3 from "os";
1035
+ import * as path5 from "path";
1036
+ var CODEX_HOME_DIRNAME = ".codex";
1037
+ var CODEX_CONFIG_FILENAME = "config.toml";
1038
+ var CODEX_SESSIONS_DIRNAME = "sessions";
1039
+ function getCodexHomeDir() {
1040
+ return path5.join(os3.homedir(), CODEX_HOME_DIRNAME);
1041
+ }
1042
+ function getCodexConfigPath() {
1043
+ return path5.join(getCodexHomeDir(), CODEX_CONFIG_FILENAME);
1044
+ }
1045
+ function getCodexSessionsDir() {
1046
+ return path5.join(getCodexHomeDir(), CODEX_SESSIONS_DIRNAME);
1047
+ }
1048
+
1049
+ // src/monitor/plugins/codex/hooks.ts
1050
+ var OLAKAI_HOOK_MARKER2 = "olakai monitor hook";
1051
+ var CODEX_HOOK_TIMEOUT_SECONDS = 5;
1052
+ var SUPPORTED_HOOK_EVENT_NAMES = ["Stop"];
1053
+ function buildOlakaiHookGroup(event) {
1054
+ return {
1055
+ hooks: [
1056
+ {
1057
+ type: "command",
1058
+ command: `olakai monitor hook --tool codex ${event}`,
1059
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS
1060
+ }
1061
+ ]
1062
+ };
1063
+ }
1064
+ function isOlakaiHandler(handler) {
1065
+ return typeof handler.command === "string" && handler.command.includes(OLAKAI_HOOK_MARKER2);
1066
+ }
1067
+ function groupContainsOlakaiHandler(group) {
1068
+ if (!Array.isArray(group.hooks)) return false;
1069
+ return group.hooks.some(isOlakaiHandler);
1070
+ }
1071
+ function mergeCodexHooks(existing, events = SUPPORTED_HOOK_EVENT_NAMES) {
1072
+ const merged = { ...existing ?? {} };
1073
+ for (const event of events) {
1074
+ const existingGroups = merged[event] ?? [];
1075
+ const hasOlakaiHook2 = existingGroups.some(groupContainsOlakaiHandler);
1076
+ if (hasOlakaiHook2) {
1077
+ merged[event] = existingGroups;
1078
+ } else {
1079
+ merged[event] = [...existingGroups, buildOlakaiHookGroup(event)];
1080
+ }
1081
+ }
1082
+ return merged;
1083
+ }
1084
+ function stripOlakaiHooks(existing) {
1085
+ if (!existing) return void 0;
1086
+ const cleaned = {};
1087
+ for (const [event, groups] of Object.entries(existing)) {
1088
+ if (!Array.isArray(groups)) continue;
1089
+ const filteredGroups = [];
1090
+ for (const group of groups) {
1091
+ const handlers = Array.isArray(group.hooks) ? group.hooks : [];
1092
+ const remaining = handlers.filter((h) => !isOlakaiHandler(h));
1093
+ if (remaining.length === 0 && handlers.length > 0) {
1094
+ continue;
1095
+ }
1096
+ if (remaining.length === handlers.length) {
1097
+ filteredGroups.push(group);
1098
+ } else {
1099
+ filteredGroups.push({ ...group, hooks: remaining });
1100
+ }
1101
+ }
1102
+ if (filteredGroups.length > 0) {
1103
+ cleaned[event] = filteredGroups;
1104
+ }
1105
+ }
1106
+ return Object.keys(cleaned).length > 0 ? cleaned : void 0;
1107
+ }
1108
+ function hasOlakaiHooksInstalled(parsed) {
1109
+ const hooks = parsed.hooks;
1110
+ if (!hooks) return false;
1111
+ for (const groups of Object.values(hooks)) {
1112
+ if (!Array.isArray(groups)) continue;
1113
+ for (const group of groups) {
1114
+ if (groupContainsOlakaiHandler(group)) return true;
1115
+ }
1116
+ }
1117
+ return false;
1118
+ }
1119
+
1120
+ // src/monitor/plugins/codex/config.ts
1121
+ import * as fs6 from "fs";
1122
+ import * as path6 from "path";
1123
+ function getCodexConfigPath2(projectRoot) {
1124
+ return getMonitorConfigPath(projectRoot, "codex");
1125
+ }
1126
+ function loadCodexConfig(projectRoot) {
1127
+ const filePath = getCodexConfigPath2(projectRoot);
1128
+ try {
1129
+ if (!fs6.existsSync(filePath)) return null;
1130
+ const raw = fs6.readFileSync(filePath, "utf-8");
1131
+ return JSON.parse(raw);
1132
+ } catch {
1133
+ return null;
1134
+ }
1135
+ }
1136
+ function writeCodexConfig(projectRoot, config) {
1137
+ const filePath = getCodexConfigPath2(projectRoot);
1138
+ const dir = path6.dirname(filePath);
1139
+ if (!fs6.existsSync(dir)) {
1140
+ fs6.mkdirSync(dir, { recursive: true });
1141
+ }
1142
+ fs6.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1143
+ try {
1144
+ fs6.chmodSync(filePath, 384);
1145
+ } catch {
1146
+ }
1147
+ }
1148
+ function deleteCodexConfig(projectRoot) {
1149
+ const filePath = getCodexConfigPath2(projectRoot);
1150
+ if (!fs6.existsSync(filePath)) return false;
1151
+ try {
1152
+ fs6.unlinkSync(filePath);
1153
+ return true;
1154
+ } catch {
1155
+ return false;
1156
+ }
1157
+ }
1158
+
1159
+ // src/monitor/plugins/codex/install.ts
1160
+ var CODEX_SOURCE = "codex";
1161
+ var CODEX_AGENT_SOURCE = "CODEX";
1162
+ var CODEX_AGENT_CATEGORY = "CODING";
1163
+ async function installCodex(opts) {
1164
+ const projectRoot = opts.projectRoot ?? process.cwd();
1165
+ const token = getValidToken();
1166
+ if (!token) {
1167
+ console.error("Not logged in. Run 'olakai login' first.");
1168
+ process.exit(1);
1169
+ }
1170
+ console.log("Setting up Codex CLI monitoring for this workspace...\n");
1171
+ const agent = await provisionSelfMonitorAgent({
1172
+ projectRoot,
1173
+ source: CODEX_AGENT_SOURCE,
1174
+ displayName: "Codex CLI",
1175
+ category: CODEX_AGENT_CATEGORY
1176
+ });
1177
+ const monitoringEndpoint = `${getBaseUrl()}/api/monitoring/prompt`;
1178
+ const apiKey = agent.apiKey?.key;
1179
+ if (!apiKey) {
1180
+ console.error(
1181
+ "Internal error: agent provisioned but no API key returned. Please re-run 'olakai monitor init'."
1182
+ );
1183
+ process.exit(1);
1184
+ }
1185
+ const { configExisted: codexConfigExisted } = installCodexHooksConfig();
1186
+ const monitorConfig = {
1187
+ agentId: agent.id,
1188
+ apiKey,
1189
+ agentName: agent.name,
1190
+ source: CODEX_SOURCE,
1191
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1192
+ monitoringEndpoint
1193
+ };
1194
+ writeCodexConfig(projectRoot, monitorConfig);
1195
+ const configPath = getCodexConfigPath2(projectRoot);
1196
+ const configRel = path7.relative(projectRoot, configPath);
1197
+ console.log("");
1198
+ console.log(`\u2713 Agent "${agent.name}" configured (ID: ${agent.id})`);
1199
+ if (agent.apiKey?.key) {
1200
+ console.log("\u2713 API key generated");
1201
+ }
1202
+ console.log(
1203
+ `\u2713 Codex hooks configured in ~/${CODEX_HOME_DIRNAME}/${CODEX_CONFIG_FILENAME}`
1204
+ );
1205
+ console.log(`\u2713 Monitor config saved to ${configRel}`);
1206
+ console.log("");
1207
+ console.log(
1208
+ "Congrats! Monitoring is now active. Codex CLI will report activity to Olakai"
1209
+ );
1210
+ console.log(`on each turn.`);
1211
+ console.log("");
1212
+ console.log(
1213
+ `\u26A0 Ensure ${OLAKAI_DIR}/ is in your .gitignore (it contains your API key).`
1214
+ );
1215
+ console.log(
1216
+ `\u26A0 Codex hooks require codex >= 0.124.0. Earlier versions silently skip them.`
1217
+ );
1218
+ if (codexConfigExisted) {
1219
+ console.log(
1220
+ `\u26A0 Existing comments in ~/${CODEX_HOME_DIRNAME}/${CODEX_CONFIG_FILENAME} were not preserved (TOML serializer limitation).`
1221
+ );
1222
+ }
1223
+ console.log("");
1224
+ console.log("To check status: olakai monitor status --tool codex");
1225
+ console.log("To disable: olakai monitor disable --tool codex");
1226
+ return {
1227
+ agentId: agent.id,
1228
+ agentName: agent.name,
1229
+ source: CODEX_SOURCE,
1230
+ monitoringEndpoint
1231
+ };
1232
+ }
1233
+ function installCodexHooksConfig() {
1234
+ const homeDir = getCodexHomeDir();
1235
+ const configPath = getCodexConfigPath();
1236
+ if (!fs7.existsSync(homeDir)) {
1237
+ fs7.mkdirSync(homeDir, { recursive: true });
1238
+ }
1239
+ const configExisted = fs7.existsSync(configPath);
1240
+ const parsed = readCodexConfigToml(configPath);
1241
+ const merged = {
1242
+ ...parsed,
1243
+ hooks: mergeCodexHooks(parsed.hooks, SUPPORTED_HOOK_EVENT_NAMES)
1244
+ };
1245
+ writeCodexConfigToml(configPath, merged);
1246
+ return { configExisted };
1247
+ }
1248
+ async function uninstallCodex(opts) {
1249
+ const projectRoot = opts.projectRoot ?? process.cwd();
1250
+ const removedHooks = stripCodexHooksConfig();
1251
+ if (removedHooks) {
1252
+ console.log(
1253
+ `\u2713 Olakai hooks removed from ~/${CODEX_HOME_DIRNAME}/${CODEX_CONFIG_FILENAME}`
1254
+ );
1255
+ } else {
1256
+ console.log("No Olakai hooks found in Codex config.");
1257
+ }
1258
+ if (!opts.keepConfig) {
1259
+ const configPath = getCodexConfigPath2(projectRoot);
1260
+ const configRel = path7.relative(projectRoot, configPath);
1261
+ if (deleteCodexConfig(projectRoot)) {
1262
+ console.log(`\u2713 Monitor config removed (${configRel})`);
1263
+ }
1264
+ } else {
1265
+ const configPath = getCodexConfigPath2(projectRoot);
1266
+ const configRel = path7.relative(projectRoot, configPath);
1267
+ console.log(`Monitor config retained at ${configRel}`);
1268
+ }
1269
+ console.log("");
1270
+ console.log(
1271
+ "Monitoring disabled. Run 'olakai monitor init --tool codex' to re-enable."
1272
+ );
1273
+ }
1274
+ function applyOlakaiStripToConfig(parsed) {
1275
+ const cleaned = stripOlakaiHooks(parsed.hooks);
1276
+ const next = { ...parsed };
1277
+ if (cleaned === void 0) {
1278
+ delete next.hooks;
1279
+ } else {
1280
+ next.hooks = cleaned;
1281
+ }
1282
+ return next;
1283
+ }
1284
+ function stripCodexHooksConfig() {
1285
+ const configPath = getCodexConfigPath();
1286
+ if (!fs7.existsSync(configPath)) return false;
1287
+ const parsed = readCodexConfigToml(configPath);
1288
+ if (!hasOlakaiHooksInstalled(parsed)) return false;
1289
+ const next = applyOlakaiStripToConfig(parsed);
1290
+ writeCodexConfigToml(configPath, next);
1291
+ return true;
1292
+ }
1293
+ function readCodexConfigToml(configPath) {
1294
+ try {
1295
+ if (!fs7.existsSync(configPath)) return {};
1296
+ const raw = fs7.readFileSync(configPath, "utf-8");
1297
+ if (!raw.trim()) return {};
1298
+ return TOML.parse(raw);
1299
+ } catch {
1300
+ return {};
1301
+ }
1302
+ }
1303
+ function writeCodexConfigToml(configPath, data) {
1304
+ const dir = path7.dirname(configPath);
1305
+ if (!fs7.existsSync(dir)) {
1306
+ fs7.mkdirSync(dir, { recursive: true });
1307
+ }
1308
+ const serialized = TOML.stringify(data);
1309
+ fs7.writeFileSync(configPath, serialized, "utf-8");
1310
+ }
1311
+
1312
+ // src/monitor/plugins/codex/status.ts
1313
+ import path8 from "path";
1314
+ import * as fs8 from "fs";
1315
+ async function getCodexStatus(opts) {
1316
+ const projectRoot = opts?.projectRoot ?? process.cwd();
1317
+ const configPath = getCodexConfigPath2(projectRoot);
1318
+ const config = loadCodexConfig(projectRoot);
1319
+ const hooksConfigured = isHooksBlockInstalled();
1320
+ if (!config) {
1321
+ return {
1322
+ toolId: "codex",
1323
+ configured: false,
1324
+ hooksConfigured,
1325
+ configPath,
1326
+ notes: hooksConfigured ? [
1327
+ `Codex hooks present in ~/${CODEX_HOME_DIRNAME}/${CODEX_CONFIG_FILENAME} but no monitor config in this workspace \u2014 re-run init.`
1328
+ ] : []
1329
+ };
1330
+ }
1331
+ return {
1332
+ toolId: "codex",
1333
+ configured: true,
1334
+ hooksConfigured,
1335
+ agentId: config.agentId,
1336
+ agentName: config.agentName,
1337
+ source: config.source,
1338
+ apiKeyMasked: config.apiKey.slice(0, 12) + "...",
1339
+ monitoringEndpoint: config.monitoringEndpoint,
1340
+ configuredAt: config.createdAt,
1341
+ configPath
1342
+ };
1343
+ }
1344
+ function isHooksBlockInstalled() {
1345
+ const homeConfig = getCodexConfigPath();
1346
+ if (!fs8.existsSync(homeConfig)) return false;
1347
+ const parsed = readCodexConfigToml(homeConfig);
1348
+ return hasOlakaiHooksInstalled(parsed);
1349
+ }
1350
+
1351
+ // src/monitor/plugins/codex/transcript.ts
1352
+ import * as fs9 from "fs";
1353
+ import * as path9 from "path";
1354
+ function emptyRollout() {
1355
+ return {
1356
+ prompt: "",
1357
+ response: "",
1358
+ modelName: null,
1359
+ inputTokens: 0,
1360
+ outputTokens: 0,
1361
+ cachedInputTokens: 0,
1362
+ reasoningOutputTokens: 0,
1363
+ tokens: 0,
1364
+ numTurns: 0
1365
+ };
1366
+ }
1367
+ var noopDebug2 = () => {
1368
+ };
1369
+ var DEFAULT_LIMITS = {
1370
+ maxDirs: 200,
1371
+ maxFiles: 5e3
1372
+ };
1373
+ function findRolloutPathForSession(sessionId, sessionsDir = getCodexSessionsDir(), limits = {}) {
1374
+ const merged = { ...DEFAULT_LIMITS, ...limits };
1375
+ if (!sessionId) return null;
1376
+ if (!fs9.existsSync(sessionsDir)) return null;
1377
+ const suffix = `-${sessionId}.jsonl`;
1378
+ const matches = [];
1379
+ let dirsScanned = 0;
1380
+ let filesScanned = 0;
1381
+ const queue = [sessionsDir];
1382
+ while (queue.length > 0) {
1383
+ const dir = queue.shift();
1384
+ if (dirsScanned++ > merged.maxDirs) break;
1385
+ let entries;
1386
+ try {
1387
+ entries = fs9.readdirSync(dir, { withFileTypes: true });
1388
+ } catch {
1389
+ continue;
1390
+ }
1391
+ for (const entry of entries) {
1392
+ if (filesScanned++ > merged.maxFiles) break;
1393
+ const full = path9.join(dir, entry.name);
1394
+ if (entry.isDirectory()) {
1395
+ queue.push(full);
1396
+ continue;
1397
+ }
1398
+ if (!entry.isFile()) continue;
1399
+ if (!entry.name.endsWith(suffix)) continue;
1400
+ if (!entry.name.startsWith("rollout-")) continue;
1401
+ try {
1402
+ const stat = fs9.statSync(full);
1403
+ matches.push({ path: full, mtimeMs: stat.mtimeMs });
1404
+ } catch {
1405
+ }
1406
+ }
1407
+ }
1408
+ if (matches.length === 0) return null;
1409
+ matches.sort((a, b) => b.mtimeMs - a.mtimeMs);
1410
+ return matches[0].path;
1411
+ }
1412
+ function parseRolloutContent(raw, debugLog6 = noopDebug2) {
1413
+ const result = emptyRollout();
1414
+ if (!raw) return result;
1415
+ const lines = raw.split("\n");
1416
+ let lastUserMessage = "";
1417
+ let lastAssistantMessage = "";
1418
+ let lastTokenUsage = null;
1419
+ let totalTokenUsage = null;
1420
+ let modelName = null;
1421
+ let userTurnCount = 0;
1422
+ for (const line of lines) {
1423
+ const trimmed = line.trim();
1424
+ if (!trimmed) continue;
1425
+ let parsed;
1426
+ try {
1427
+ parsed = JSON.parse(trimmed);
1428
+ } catch (err) {
1429
+ debugLog6("rollout-line-parse-failed", {
1430
+ line: trimmed.slice(0, 120),
1431
+ error: err.message
1432
+ });
1433
+ continue;
1434
+ }
1435
+ const type = parsed.type;
1436
+ const payload = parsed.payload;
1437
+ if (!type || !payload) continue;
1438
+ if (type === "session_meta") {
1439
+ continue;
1440
+ }
1441
+ if (type === "event_msg") {
1442
+ const ev = payload;
1443
+ const evType = ev.type;
1444
+ if (evType === "user_message" && typeof ev.message === "string") {
1445
+ lastUserMessage = ev.message;
1446
+ userTurnCount += 1;
1447
+ } else if (evType === "agent_message" && typeof ev.message === "string") {
1448
+ lastAssistantMessage = ev.message;
1449
+ } else if (evType === "token_count" && ev.info) {
1450
+ if (ev.info.last_token_usage) {
1451
+ lastTokenUsage = ev.info.last_token_usage;
1452
+ }
1453
+ if (ev.info.total_token_usage) {
1454
+ totalTokenUsage = ev.info.total_token_usage;
1455
+ }
1456
+ } else if (evType === "session_configured" && typeof ev.model === "string" && ev.model) {
1457
+ modelName = ev.model;
1458
+ }
1459
+ continue;
1460
+ }
1461
+ if (type === "response_item") {
1462
+ const ri = payload;
1463
+ if (ri.type === "message" && Array.isArray(ri.content)) {
1464
+ const text = extractTextFromContent(ri.content);
1465
+ if (!text) continue;
1466
+ if (ri.role === "user") {
1467
+ lastUserMessage = text;
1468
+ userTurnCount += 1;
1469
+ } else if (ri.role === "assistant") {
1470
+ lastAssistantMessage = text;
1471
+ }
1472
+ }
1473
+ continue;
1474
+ }
1475
+ }
1476
+ result.prompt = lastUserMessage;
1477
+ result.response = lastAssistantMessage;
1478
+ result.modelName = modelName;
1479
+ result.numTurns = userTurnCount;
1480
+ const tokenSource = lastTokenUsage ?? totalTokenUsage;
1481
+ if (tokenSource) {
1482
+ result.inputTokens = numberOrZero(tokenSource.input_tokens);
1483
+ result.outputTokens = numberOrZero(tokenSource.output_tokens);
1484
+ result.cachedInputTokens = numberOrZero(tokenSource.cached_input_tokens);
1485
+ result.reasoningOutputTokens = numberOrZero(
1486
+ tokenSource.reasoning_output_tokens
1487
+ );
1488
+ result.tokens = numberOrZero(tokenSource.total_tokens) || result.inputTokens + result.outputTokens;
1489
+ }
1490
+ return result;
1491
+ }
1492
+ function extractTextFromContent(content) {
1493
+ const parts = [];
1494
+ for (const block of content) {
1495
+ if (typeof block.text !== "string") continue;
1496
+ if (block.type === "output_text" || block.type === "input_text" || block.type === "text") {
1497
+ parts.push(block.text);
1498
+ }
1499
+ }
1500
+ return parts.join("\n").trim();
1501
+ }
1502
+ function numberOrZero(value) {
1503
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
1504
+ }
1505
+ function loadRolloutForSession(sessionId, options = {}) {
1506
+ const debugLog6 = options.debugLog ?? noopDebug2;
1507
+ const sessionsDir = options.sessionsDir ?? getCodexSessionsDir();
1508
+ const result = emptyRollout();
1509
+ const rolloutPath = findRolloutPathForSession(sessionId, sessionsDir);
1510
+ if (!rolloutPath) {
1511
+ debugLog6("rollout-not-found", {
1512
+ sessionId,
1513
+ sessionsDir
1514
+ });
1515
+ return result;
1516
+ }
1517
+ let raw;
1518
+ try {
1519
+ raw = fs9.readFileSync(rolloutPath, "utf-8");
1520
+ } catch (err) {
1521
+ debugLog6("rollout-read-failed", {
1522
+ rolloutPath,
1523
+ error: err.message
1524
+ });
1525
+ return result;
1526
+ }
1527
+ const parsed = parseRolloutContent(raw, debugLog6);
1528
+ parsed.rolloutPath = rolloutPath;
1529
+ return parsed;
1530
+ }
1531
+
1532
+ // src/monitor/plugins/codex/hook.ts
1533
+ var noopDebug3 = () => {
1534
+ };
1535
+ var SUPPORTED_EVENTS = /* @__PURE__ */ new Set(["Stop", "UserPromptSubmit"]);
1536
+ function isSupportedCodexEvent(eventName) {
1537
+ return SUPPORTED_EVENTS.has(eventName);
1538
+ }
1539
+ function buildCodexPayload(eventName, eventData, config, rollout = emptyRollout()) {
1540
+ if (!isSupportedCodexEvent(eventName)) return null;
1541
+ if (eventName === "UserPromptSubmit") return null;
1542
+ const sessionId = typeof eventData.session_id === "string" && eventData.session_id ? eventData.session_id : `codex-${Date.now()}`;
1543
+ const cwd = typeof eventData.cwd === "string" ? eventData.cwd : "";
1544
+ const turnId = typeof eventData.turn_id === "string" ? eventData.turn_id : "";
1545
+ const permissionMode = typeof eventData.permission_mode === "string" ? eventData.permission_mode : "";
1546
+ const transcriptPath = typeof eventData.transcript_path === "string" ? eventData.transcript_path : "";
1547
+ const modelName = (typeof eventData.model === "string" && eventData.model.trim() ? eventData.model : null) ?? rollout.modelName;
1548
+ const customData = {
1549
+ hookEvent: eventData.hook_event_name ?? eventName,
1550
+ sessionId,
1551
+ cwd,
1552
+ turnId,
1553
+ permissionMode,
1554
+ transcriptPath,
1555
+ inputTokens: rollout.inputTokens,
1556
+ outputTokens: rollout.outputTokens,
1557
+ cachedInputTokens: rollout.cachedInputTokens,
1558
+ reasoningOutputTokens: rollout.reasoningOutputTokens,
1559
+ numTurns: rollout.numTurns
1560
+ };
1561
+ if (rollout.rolloutPath) {
1562
+ customData.rolloutPath = rollout.rolloutPath;
1563
+ }
1564
+ if (typeof eventData.stop_hook_active === "boolean") {
1565
+ customData.stopHookActive = eventData.stop_hook_active;
1566
+ }
1567
+ const inlineAssistant = typeof eventData.last_assistant_message === "string" && eventData.last_assistant_message.trim() ? eventData.last_assistant_message : "";
1568
+ const response = inlineAssistant || rollout.response;
1569
+ return {
1570
+ prompt: rollout.prompt,
1571
+ response,
1572
+ chatId: sessionId,
1573
+ source: config.source,
1574
+ modelName: modelName ?? void 0,
1575
+ tokens: rollout.tokens,
1576
+ customData
1577
+ };
1578
+ }
1579
+ function handleCodexHook(eventName, payloadJson, config, options = {}) {
1580
+ const debugLog6 = options.debugLog ?? noopDebug3;
1581
+ if (!isSupportedCodexEvent(eventName)) {
1582
+ debugLog6("hook-unknown-event", eventName);
1583
+ return null;
1584
+ }
1585
+ if (eventName === "UserPromptSubmit") {
1586
+ debugLog6("user-prompt-submit-dropped", { eventName });
1587
+ return null;
1588
+ }
1589
+ const eventData = payloadJson ?? {};
1590
+ debugLog6("event-parsed", { eventName, eventData });
1591
+ const sessionId = typeof eventData.session_id === "string" ? eventData.session_id : "";
1592
+ let rollout = emptyRollout();
1593
+ if (eventName === "Stop" && sessionId) {
1594
+ rollout = loadRolloutForSession(sessionId, {
1595
+ debugLog: debugLog6,
1596
+ sessionsDir: options.sessionsDir
1597
+ });
1598
+ }
1599
+ const payload = buildCodexPayload(eventName, eventData, config, rollout);
1600
+ if (!payload) return null;
1601
+ if (!payload.prompt && !payload.response) {
1602
+ debugLog6("payload-empty", { eventName, sessionId });
1603
+ return null;
1604
+ }
1605
+ debugLog6("payload-built", payload);
1606
+ return payload;
1607
+ }
1608
+
1609
+ // src/monitor/plugins/codex/index.ts
1610
+ var TOOL_ID2 = "codex";
1611
+ function debugLog2(label, data) {
1612
+ if (process.env.OLAKAI_MONITOR_DEBUG !== "1") return;
1613
+ try {
1614
+ const logPath = `/tmp/olakai-monitor-debug-${process.pid}.log`;
1615
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] codex/${label}: ${typeof data === "string" ? data : JSON.stringify(data, null, 2)}
1616
+ `;
1617
+ fs10.appendFileSync(logPath, line, "utf-8");
1618
+ } catch {
1619
+ }
1620
+ }
1621
+ function resolveCodexProjectRoot(eventData, fallbackCwd) {
1622
+ const payloadCwd = typeof eventData.cwd === "string" && eventData.cwd.trim() ? eventData.cwd : fallbackCwd;
1623
+ return findConfiguredWorkspace(payloadCwd, [TOOL_ID2]);
1624
+ }
1625
+ var codexPlugin = {
1626
+ id: TOOL_ID2,
1627
+ displayName: "OpenAI Codex CLI",
1628
+ install(opts) {
1629
+ return installCodex(opts);
1630
+ },
1631
+ uninstall(opts) {
1632
+ return uninstallCodex(opts);
1633
+ },
1634
+ status(opts) {
1635
+ return getCodexStatus(opts);
1636
+ },
1637
+ async handleHook(eventName, payloadJson) {
1638
+ const eventData = payloadJson ?? {};
1639
+ debugLog2("hook-fired", { eventName, hasPayload: payloadJson != null });
1640
+ const projectRoot = resolveCodexProjectRoot(eventData, process.cwd());
1641
+ if (!projectRoot) {
1642
+ debugLog2("config-not-found", {
1643
+ startDir: typeof eventData.cwd === "string" && eventData.cwd.trim() ? eventData.cwd : process.cwd()
1644
+ });
1645
+ return null;
1646
+ }
1647
+ const config = loadCodexConfig(projectRoot);
1648
+ if (!config) {
1649
+ debugLog2("monitor-config-missing", { projectRoot });
1650
+ return null;
1651
+ }
1652
+ const payload = handleCodexHook(eventName, eventData, config, {
1653
+ debugLog: debugLog2
1654
+ });
1655
+ if (!payload) return null;
1656
+ return {
1657
+ payload,
1658
+ transport: {
1659
+ endpoint: config.monitoringEndpoint,
1660
+ apiKey: config.apiKey,
1661
+ projectRoot
1662
+ }
1663
+ };
1664
+ },
1665
+ async detectInstalled(opts) {
1666
+ const projectRoot = opts?.projectRoot ?? process.cwd();
1667
+ try {
1668
+ if (fs10.existsSync(getCodexConfigPath2(projectRoot))) {
1669
+ return true;
1670
+ }
1671
+ } catch {
1672
+ }
1673
+ try {
1674
+ if (fs10.existsSync(getCodexConfigPath())) {
1675
+ return true;
1676
+ }
1677
+ if (fs10.existsSync(getCodexHomeDir())) {
1678
+ return true;
1679
+ }
1680
+ } catch {
1681
+ }
1682
+ return detectCodexBinaryOnPath();
1683
+ }
1684
+ };
1685
+ function detectCodexBinaryOnPath() {
1686
+ try {
1687
+ const probe = spawnSync3(
1688
+ process.platform === "win32" ? "where" : "which",
1689
+ ["codex"],
1690
+ {
1691
+ stdio: ["ignore", "pipe", "ignore"],
1692
+ timeout: 1e3
1693
+ }
1694
+ );
1695
+ if (probe.status === 0 && probe.stdout && probe.stdout.toString().trim()) {
1696
+ return true;
1697
+ }
1698
+ } catch {
1699
+ }
1700
+ return false;
1701
+ }
1702
+ registerPlugin(codexPlugin);
1703
+
1704
+ // src/monitor/plugins/cursor/index.ts
1705
+ import * as fs15 from "fs";
1706
+ import * as os8 from "os";
1707
+
1708
+ // src/monitor/plugins/cursor/install.ts
1709
+ import * as fs12 from "fs";
1710
+ import * as os5 from "os";
1711
+ import * as path12 from "path";
1712
+
1713
+ // src/monitor/plugins/cursor/config.ts
1714
+ import * as fs11 from "fs";
1715
+ import * as path10 from "path";
1716
+ function getCursorConfigPath(projectRoot) {
1717
+ return getMonitorConfigPath(projectRoot, "cursor");
1718
+ }
1719
+ function loadCursorConfig(projectRoot) {
1720
+ const filePath = getCursorConfigPath(projectRoot);
1721
+ try {
1722
+ if (!fs11.existsSync(filePath)) return null;
1723
+ const raw = fs11.readFileSync(filePath, "utf-8");
1724
+ return JSON.parse(raw);
1725
+ } catch {
1726
+ return null;
1727
+ }
1728
+ }
1729
+ function writeCursorConfig(projectRoot, config) {
1730
+ const filePath = getCursorConfigPath(projectRoot);
1731
+ const dir = path10.dirname(filePath);
1732
+ if (!fs11.existsSync(dir)) {
1733
+ fs11.mkdirSync(dir, { recursive: true });
1734
+ }
1735
+ fs11.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1736
+ try {
1737
+ fs11.chmodSync(filePath, 384);
1738
+ } catch {
1739
+ }
1740
+ }
1741
+ function deleteCursorConfig(projectRoot) {
1742
+ const filePath = getCursorConfigPath(projectRoot);
1743
+ if (!fs11.existsSync(filePath)) return false;
1744
+ try {
1745
+ fs11.unlinkSync(filePath);
1746
+ return true;
1747
+ } catch {
1748
+ return false;
1749
+ }
1750
+ }
1751
+
1752
+ // src/monitor/plugins/cursor/paths.ts
1753
+ import * as os4 from "os";
1754
+ import * as path11 from "path";
1755
+ var CURSOR_DIR_NAME = ".cursor";
1756
+ var CURSOR_HOOKS_FILE = "hooks.json";
1757
+ function getCursorUserDir(homeDir = os4.homedir()) {
1758
+ return path11.join(homeDir, CURSOR_DIR_NAME);
1759
+ }
1760
+ function getCursorHooksPath(homeDir = os4.homedir()) {
1761
+ return path11.join(getCursorUserDir(homeDir), CURSOR_HOOKS_FILE);
1762
+ }
1763
+
1764
+ // src/monitor/plugins/cursor/hooks-config.ts
1765
+ var OLAKAI_HOOK_MARKER3 = "olakai monitor hook --tool cursor";
1766
+ var CURSOR_HOOK_DEFINITIONS = {
1767
+ beforeSubmitPrompt: [
1768
+ {
1769
+ command: "olakai monitor hook --tool cursor beforeSubmitPrompt",
1770
+ timeout: 5
1771
+ }
1772
+ ],
1773
+ afterAgentResponse: [
1774
+ {
1775
+ command: "olakai monitor hook --tool cursor afterAgentResponse",
1776
+ timeout: 5
1777
+ }
1778
+ ],
1779
+ sessionEnd: [
1780
+ {
1781
+ command: "olakai monitor hook --tool cursor sessionEnd",
1782
+ timeout: 5
1783
+ }
1784
+ ],
1785
+ stop: [
1786
+ {
1787
+ command: "olakai monitor hook --tool cursor stop",
1788
+ timeout: 5
1789
+ }
1790
+ ]
1791
+ };
1792
+ var CURSOR_HOOKS_VERSION = 1;
1793
+ function mergeCursorHooks(existing, definitions = CURSOR_HOOK_DEFINITIONS) {
1794
+ const base = existing ? { ...existing } : {};
1795
+ if (typeof base.version !== "number") {
1796
+ base.version = CURSOR_HOOKS_VERSION;
1797
+ }
1798
+ const mergedHooks = {
1799
+ ...base.hooks ?? {}
1800
+ };
1801
+ for (const [event, defaultEntries] of Object.entries(definitions)) {
1802
+ const existingEntries = mergedHooks[event] ?? [];
1803
+ const hasOlakaiEntry = existingEntries.some(
1804
+ (e) => typeof e?.command === "string" && e.command.includes(OLAKAI_HOOK_MARKER3)
1805
+ );
1806
+ if (hasOlakaiEntry) {
1807
+ mergedHooks[event] = existingEntries;
1808
+ } else {
1809
+ mergedHooks[event] = [...existingEntries, ...defaultEntries];
1810
+ }
1811
+ }
1812
+ return {
1813
+ ...base,
1814
+ hooks: mergedHooks
1815
+ };
1816
+ }
1817
+ function removeOlakaiCursorHooks(existing) {
1818
+ const base = existing ? { ...existing } : {};
1819
+ const sourceHooks = base.hooks ?? {};
1820
+ const cleaned = {};
1821
+ for (const [event, entries] of Object.entries(sourceHooks)) {
1822
+ const filtered = entries.filter(
1823
+ (e) => typeof e?.command !== "string" || !e.command.includes(OLAKAI_HOOK_MARKER3)
1824
+ );
1825
+ if (filtered.length > 0) {
1826
+ cleaned[event] = filtered;
1827
+ }
1828
+ }
1829
+ if (Object.keys(cleaned).length > 0) {
1830
+ base.hooks = cleaned;
1831
+ } else {
1832
+ delete base.hooks;
1833
+ }
1834
+ return base;
1835
+ }
1836
+ function hasOlakaiCursorHooks(config) {
1837
+ if (!config?.hooks) return false;
1838
+ return Object.values(config.hooks).some(
1839
+ (entries) => entries.some(
1840
+ (e) => typeof e?.command === "string" && e.command.includes(OLAKAI_HOOK_MARKER3)
1841
+ )
1842
+ );
1843
+ }
1844
+
1845
+ // src/monitor/plugins/cursor/install.ts
1846
+ var CURSOR_SOURCE = "cursor";
1847
+ var CURSOR_AGENT_SOURCE = "CURSOR";
1848
+ var CURSOR_AGENT_CATEGORY = "CODING";
1849
+ function readJsonFileTolerant(filePath) {
1850
+ try {
1851
+ if (!fs12.existsSync(filePath)) return null;
1852
+ const raw = fs12.readFileSync(filePath, "utf-8");
1853
+ return JSON.parse(raw);
1854
+ } catch {
1855
+ return null;
1856
+ }
1857
+ }
1858
+ function writeJsonFileWithDir(filePath, data) {
1859
+ const dir = path12.dirname(filePath);
1860
+ if (!fs12.existsSync(dir)) {
1861
+ fs12.mkdirSync(dir, { recursive: true });
1862
+ }
1863
+ fs12.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
1864
+ }
1865
+ async function installCursor(opts) {
1866
+ const projectRoot = opts.projectRoot ?? process.cwd();
1867
+ const homeDir = opts.homeDir ?? os5.homedir();
1868
+ const token = getValidToken();
1869
+ if (!token) {
1870
+ console.error("Not logged in. Run 'olakai login' first.");
1871
+ process.exit(1);
1872
+ }
1873
+ console.log("Setting up Cursor monitoring for this workspace...\n");
1874
+ console.log(
1875
+ "Note: Cursor hooks are installed globally per user (~/.cursor/hooks.json)."
1876
+ );
1877
+ console.log(
1878
+ `Activity from this workspace (${projectRoot}) will be associated with`
1879
+ );
1880
+ console.log("the agent you select below.\n");
1881
+ const agent = await provisionSelfMonitorAgent({
1882
+ projectRoot,
1883
+ source: CURSOR_AGENT_SOURCE,
1884
+ displayName: "Cursor",
1885
+ category: CURSOR_AGENT_CATEGORY
1886
+ });
1887
+ const monitoringEndpoint = `${getBaseUrl()}/api/monitoring/prompt`;
1888
+ const apiKey = agent.apiKey?.key;
1889
+ if (!apiKey) {
1890
+ console.error(
1891
+ "Internal error: agent provisioned but no API key returned. Please re-run 'olakai monitor init'."
1892
+ );
1893
+ process.exit(1);
1894
+ }
1895
+ const cursorDir = getCursorUserDir(homeDir);
1896
+ if (!fs12.existsSync(cursorDir)) {
1897
+ fs12.mkdirSync(cursorDir, { recursive: true });
1898
+ }
1899
+ const hooksPath = getCursorHooksPath(homeDir);
1900
+ const existingHooks = readJsonFileTolerant(hooksPath);
1901
+ const mergedHooks = mergeCursorHooks(existingHooks);
1902
+ writeJsonFileWithDir(hooksPath, mergedHooks);
1903
+ const monitorConfig = {
1904
+ agentId: agent.id,
1905
+ apiKey,
1906
+ agentName: agent.name,
1907
+ source: CURSOR_SOURCE,
1908
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1909
+ monitoringEndpoint
1910
+ };
1911
+ writeCursorConfig(projectRoot, monitorConfig);
1912
+ const configPath = getCursorConfigPath(projectRoot);
1913
+ const configRel = path12.relative(projectRoot, configPath);
1914
+ const hooksDisplay = `~/${path12.join(CURSOR_DIR_NAME, CURSOR_HOOKS_FILE)}`;
1915
+ console.log("");
1916
+ console.log(`\u2713 Agent "${agent.name}" configured (ID: ${agent.id})`);
1917
+ if (agent.apiKey?.key) {
1918
+ console.log("\u2713 API key generated");
1919
+ }
1920
+ console.log(`\u2713 Cursor hooks installed at ${hooksDisplay}`);
1921
+ console.log(`\u2713 Monitor config saved to ${configRel}`);
1922
+ console.log("");
1923
+ console.log(
1924
+ "Congrats! Monitoring is now active. Restart Cursor for the new hooks to take effect."
1925
+ );
1926
+ console.log("");
1927
+ console.log(
1928
+ `\u26A0 Ensure ${OLAKAI_DIR}/ is in your .gitignore (it contains your API key)`
1929
+ );
1930
+ console.log("");
1931
+ console.log("To check status: olakai monitor status --tool cursor");
1932
+ console.log("To disable: olakai monitor disable --tool cursor");
1933
+ return {
1934
+ agentId: agent.id,
1935
+ agentName: agent.name,
1936
+ source: CURSOR_SOURCE,
1937
+ monitoringEndpoint
1938
+ };
1939
+ }
1940
+ async function uninstallCursor(opts) {
1941
+ const projectRoot = opts.projectRoot ?? process.cwd();
1942
+ const homeDir = opts.homeDir ?? os5.homedir();
1943
+ const hooksPath = getCursorHooksPath(homeDir);
1944
+ const existingHooks = readJsonFileTolerant(hooksPath);
1945
+ if (existingHooks) {
1946
+ const cleaned = removeOlakaiCursorHooks(existingHooks);
1947
+ if (!cleaned.hooks || Object.keys(cleaned.hooks).length === 0) {
1948
+ const otherKeys = Object.keys(cleaned).filter(
1949
+ (k) => k !== "hooks" && k !== "version"
1950
+ );
1951
+ const onlyDefaults = otherKeys.length === 0 && (cleaned.version === 1 || cleaned.version === void 0);
1952
+ if (onlyDefaults) {
1953
+ try {
1954
+ fs12.unlinkSync(hooksPath);
1955
+ } catch {
1956
+ }
1957
+ } else {
1958
+ writeJsonFileWithDir(hooksPath, cleaned);
1959
+ }
1960
+ } else {
1961
+ writeJsonFileWithDir(hooksPath, cleaned);
1962
+ }
1963
+ console.log(
1964
+ `\u2713 Olakai hooks removed from ~/${path12.join(CURSOR_DIR_NAME, CURSOR_HOOKS_FILE)}`
1965
+ );
1966
+ } else {
1967
+ console.log("No Cursor hooks file found.");
1968
+ }
1969
+ if (!opts.keepConfig) {
1970
+ const configPath = getCursorConfigPath(projectRoot);
1971
+ const configRel = path12.relative(projectRoot, configPath);
1972
+ if (deleteCursorConfig(projectRoot)) {
1973
+ console.log(`\u2713 Monitor config removed (${configRel})`);
1974
+ }
1975
+ } else {
1976
+ const configPath = getCursorConfigPath(projectRoot);
1977
+ const configRel = path12.relative(projectRoot, configPath);
1978
+ console.log(`Monitor config retained at ${configRel}`);
1979
+ }
1980
+ console.log("");
1981
+ console.log(
1982
+ "Monitoring disabled. Run 'olakai monitor init --tool cursor' to re-enable."
1983
+ );
1984
+ console.log("Restart Cursor for the change to take effect.");
1985
+ }
1986
+
1987
+ // src/monitor/plugins/cursor/status.ts
1988
+ import * as fs13 from "fs";
1989
+ import * as os6 from "os";
1990
+ import * as path13 from "path";
1991
+ async function getCursorStatus(opts) {
1992
+ const projectRoot = opts?.projectRoot ?? process.cwd();
1993
+ const homeDir = opts?.homeDir ?? os6.homedir();
1994
+ const configPath = getCursorConfigPath(projectRoot);
1995
+ const config = loadCursorConfig(projectRoot);
1996
+ let hooksConfigured = false;
1997
+ const hooksPath = getCursorHooksPath(homeDir);
1998
+ if (fs13.existsSync(hooksPath)) {
1999
+ try {
2000
+ const raw = fs13.readFileSync(hooksPath, "utf-8");
2001
+ const parsed = JSON.parse(raw);
2002
+ hooksConfigured = hasOlakaiCursorHooks(parsed);
2003
+ } catch {
2004
+ }
2005
+ }
2006
+ if (!config) {
2007
+ return {
2008
+ toolId: "cursor",
2009
+ configured: false,
2010
+ hooksConfigured,
2011
+ configPath,
2012
+ notes: hooksConfigured ? [
2013
+ "Cursor hooks installed but no monitor config in this workspace \u2014 run 'olakai monitor init --tool cursor' to associate it with an agent."
2014
+ ] : []
2015
+ };
2016
+ }
2017
+ return {
2018
+ toolId: "cursor",
2019
+ configured: true,
2020
+ hooksConfigured,
2021
+ agentId: config.agentId,
2022
+ agentName: config.agentName,
2023
+ source: config.source,
2024
+ apiKeyMasked: config.apiKey.slice(0, 12) + "...",
2025
+ monitoringEndpoint: config.monitoringEndpoint,
2026
+ configuredAt: config.createdAt,
2027
+ configPath
2028
+ };
2029
+ }
2030
+
2031
+ // src/monitor/plugins/cursor/hook.ts
2032
+ var SUPPORTED_CURSOR_EVENTS = /* @__PURE__ */ new Set([
2033
+ "beforeSubmitPrompt",
2034
+ "afterAgentResponse",
2035
+ "sessionEnd",
2036
+ "stop"
2037
+ ]);
2038
+ function asString(value) {
2039
+ return typeof value === "string" && value.trim() ? value : void 0;
2040
+ }
2041
+ function asNumber(value) {
2042
+ if (typeof value !== "number") return 0;
2043
+ if (!Number.isFinite(value)) return 0;
2044
+ return value;
2045
+ }
2046
+ function extractCursorTokens(payload) {
2047
+ return {
2048
+ inputTokens: asNumber(payload.input_tokens),
2049
+ outputTokens: asNumber(payload.output_tokens),
2050
+ cacheReadTokens: asNumber(payload.cache_read_tokens),
2051
+ cacheWriteTokens: asNumber(payload.cache_write_tokens)
2052
+ };
2053
+ }
2054
+ function asStringArray(value) {
2055
+ if (!Array.isArray(value)) return void 0;
2056
+ const out = [];
2057
+ for (const item of value) {
2058
+ if (typeof item === "string" && item.trim()) out.push(item);
2059
+ }
2060
+ return out.length > 0 ? out : void 0;
2061
+ }
2062
+ function extractPromptText(payload) {
2063
+ if (typeof payload.prompt === "string") return payload.prompt;
2064
+ if (payload.prompt && typeof payload.prompt === "object" && typeof payload.prompt.text === "string") {
2065
+ return payload.prompt.text;
2066
+ }
2067
+ return "";
2068
+ }
2069
+ function extractResponseText(payload) {
2070
+ if (typeof payload.response === "string") return payload.response;
2071
+ if (payload.response && typeof payload.response === "object" && typeof payload.response.text === "string") {
2072
+ return payload.response.text;
2073
+ }
2074
+ if (typeof payload.text === "string") return payload.text;
2075
+ return "";
2076
+ }
2077
+ function extractAttachments(payload) {
2078
+ if (Array.isArray(payload.attachments)) return payload.attachments;
2079
+ if (payload.prompt && typeof payload.prompt === "object" && Array.isArray(payload.prompt.attachments)) {
2080
+ return payload.prompt.attachments;
2081
+ }
2082
+ return void 0;
2083
+ }
2084
+ function buildPairedPayload(inputs, config) {
2085
+ const customData = {
2086
+ hookEvent: "afterAgentResponse",
2087
+ conversationId: inputs.conversationId
2088
+ };
2089
+ if (inputs.generationId) customData.generationId = inputs.generationId;
2090
+ if (inputs.cursorVersion) customData.cursorVersion = inputs.cursorVersion;
2091
+ if (inputs.workspaceRoots) customData.workspaceRoots = inputs.workspaceRoots;
2092
+ if (inputs.transcriptPath) customData.transcriptPath = inputs.transcriptPath;
2093
+ if (inputs.attachments && inputs.attachments.length > 0) {
2094
+ customData.attachmentCount = inputs.attachments.length;
2095
+ }
2096
+ if (inputs.userEmail) customData.userEmail = inputs.userEmail;
2097
+ const inputTokens = asNumber(inputs.inputTokens);
2098
+ const outputTokens = asNumber(inputs.outputTokens);
2099
+ const cacheReadTokens = asNumber(inputs.cacheReadTokens);
2100
+ const cacheWriteTokens = asNumber(inputs.cacheWriteTokens);
2101
+ customData.inputTokens = inputTokens;
2102
+ customData.outputTokens = outputTokens;
2103
+ customData.cacheReadTokens = cacheReadTokens;
2104
+ customData.cacheWriteTokens = cacheWriteTokens;
2105
+ if (inputs.unknownFields && Object.keys(inputs.unknownFields).length > 0) {
2106
+ customData.unknownPayloadFields = inputs.unknownFields;
2107
+ }
2108
+ return {
2109
+ prompt: inputs.prompt,
2110
+ response: inputs.response,
2111
+ chatId: inputs.conversationId,
2112
+ source: config.source,
2113
+ modelName: inputs.model,
2114
+ tokens: inputTokens + outputTokens,
2115
+ customData
2116
+ };
2117
+ }
2118
+ function buildOrphanPayload(inputs, config, reason = "orphan-prompt") {
2119
+ const base = buildPairedPayload({ ...inputs, response: "" }, config);
2120
+ return {
2121
+ ...base,
2122
+ customData: {
2123
+ ...base.customData,
2124
+ hookEvent: "sessionEnd",
2125
+ partial: true,
2126
+ partialReason: reason
2127
+ }
2128
+ };
2129
+ }
2130
+ var KNOWN_PAYLOAD_KEYS = /* @__PURE__ */ new Set([
2131
+ "event",
2132
+ "conversation_id",
2133
+ "generation_id",
2134
+ "model",
2135
+ "cursor_version",
2136
+ "workspace_roots",
2137
+ "user_email",
2138
+ "transcript_path",
2139
+ "prompt",
2140
+ "response",
2141
+ "text",
2142
+ "attachments",
2143
+ "input_tokens",
2144
+ "output_tokens",
2145
+ "cache_read_tokens",
2146
+ "cache_write_tokens"
2147
+ ]);
2148
+ function collectUnknownFields(payload) {
2149
+ const out = {};
2150
+ for (const key of Object.keys(payload)) {
2151
+ if (!KNOWN_PAYLOAD_KEYS.has(key)) {
2152
+ out[key] = payload[key];
2153
+ }
2154
+ }
2155
+ return Object.keys(out).length > 0 ? out : void 0;
2156
+ }
2157
+ function normalizeEventName(event) {
2158
+ const lower = event.trim();
2159
+ if (!lower) return null;
2160
+ const exact = lower;
2161
+ if (SUPPORTED_CURSOR_EVENTS.has(exact)) return exact;
2162
+ for (const known of SUPPORTED_CURSOR_EVENTS) {
2163
+ if (known.toLowerCase() === lower.toLowerCase()) return known;
2164
+ }
2165
+ return null;
2166
+ }
2167
+ function extractCursorMeta(payload) {
2168
+ return {
2169
+ conversationId: asString(payload.conversation_id),
2170
+ generationId: asString(payload.generation_id),
2171
+ model: asString(payload.model),
2172
+ cursorVersion: asString(payload.cursor_version),
2173
+ workspaceRoots: asStringArray(payload.workspace_roots),
2174
+ userEmail: asString(payload.user_email),
2175
+ transcriptPath: asString(payload.transcript_path)
2176
+ };
2177
+ }
2178
+
2179
+ // src/monitor/plugins/cursor/pairing-state.ts
2180
+ import * as fs14 from "fs";
2181
+ import * as os7 from "os";
2182
+ import * as path14 from "path";
2183
+ var STATE_DIR_SEGMENTS2 = [".olakai", "cursor-pairings"];
2184
+ function getPairingsDir(homeDir) {
2185
+ return path14.join(homeDir, ...STATE_DIR_SEGMENTS2);
2186
+ }
2187
+ function sanitizeKeyFragment(value) {
2188
+ return value.replace(/[^A-Za-z0-9._-]/g, "_");
2189
+ }
2190
+ function getPairingKey(conversationId, generationId) {
2191
+ const conv = sanitizeKeyFragment(conversationId);
2192
+ if (!generationId) return conv;
2193
+ return `${conv}__${sanitizeKeyFragment(generationId)}`;
2194
+ }
2195
+ function getPairingFile(conversationId, generationId, homeDir) {
2196
+ return path14.join(
2197
+ getPairingsDir(homeDir),
2198
+ `${getPairingKey(conversationId, generationId)}.json`
2199
+ );
2200
+ }
2201
+ function stashPendingPrompt(pending, homeDir = os7.homedir()) {
2202
+ if (!pending.conversationId) return;
2203
+ const dir = getPairingsDir(homeDir);
2204
+ try {
2205
+ if (!fs14.existsSync(dir)) {
2206
+ fs14.mkdirSync(dir, { recursive: true });
2207
+ }
2208
+ const filePath = getPairingFile(
2209
+ pending.conversationId,
2210
+ pending.generationId,
2211
+ homeDir
2212
+ );
2213
+ fs14.writeFileSync(
2214
+ filePath,
2215
+ JSON.stringify(pending, null, 2) + "\n",
2216
+ "utf-8"
2217
+ );
2218
+ } catch {
2219
+ }
2220
+ }
2221
+ function takePendingPrompt(conversationId, generationId, homeDir = os7.homedir()) {
2222
+ if (!conversationId) return null;
2223
+ const candidates = [];
2224
+ if (generationId) {
2225
+ candidates.push(getPairingFile(conversationId, generationId, homeDir));
2226
+ }
2227
+ candidates.push(getPairingFile(conversationId, void 0, homeDir));
2228
+ for (const filePath of candidates) {
2229
+ let raw;
2230
+ try {
2231
+ if (!fs14.existsSync(filePath)) continue;
2232
+ raw = fs14.readFileSync(filePath, "utf-8");
2233
+ } catch {
2234
+ continue;
2235
+ }
2236
+ try {
2237
+ fs14.unlinkSync(filePath);
2238
+ } catch {
2239
+ }
2240
+ try {
2241
+ const parsed = JSON.parse(raw);
2242
+ if (parsed && typeof parsed.conversationId === "string") {
2243
+ return parsed;
2244
+ }
2245
+ } catch {
2246
+ }
2247
+ }
2248
+ return null;
2249
+ }
2250
+ function listPendingPrompts(homeDir = os7.homedir()) {
2251
+ const dir = getPairingsDir(homeDir);
2252
+ if (!fs14.existsSync(dir)) return [];
2253
+ let files;
2254
+ try {
2255
+ files = fs14.readdirSync(dir);
2256
+ } catch {
2257
+ return [];
2258
+ }
2259
+ const result = [];
2260
+ for (const name of files) {
2261
+ if (!name.endsWith(".json")) continue;
2262
+ const filePath = path14.join(dir, name);
2263
+ try {
2264
+ const raw = fs14.readFileSync(filePath, "utf-8");
2265
+ const parsed = JSON.parse(raw);
2266
+ if (parsed && typeof parsed.conversationId === "string") {
2267
+ result.push(parsed);
2268
+ }
2269
+ } catch {
2270
+ }
2271
+ }
2272
+ return result;
2273
+ }
2274
+ function clearPendingPrompt(conversationId, generationId, homeDir = os7.homedir()) {
2275
+ if (!conversationId) return;
2276
+ const candidates = [];
2277
+ if (generationId) {
2278
+ candidates.push(getPairingFile(conversationId, generationId, homeDir));
2279
+ }
2280
+ candidates.push(getPairingFile(conversationId, void 0, homeDir));
2281
+ for (const filePath of candidates) {
2282
+ try {
2283
+ if (fs14.existsSync(filePath)) {
2284
+ fs14.unlinkSync(filePath);
2285
+ }
2286
+ } catch {
2287
+ }
2288
+ }
2289
+ }
2290
+
2291
+ // src/monitor/plugins/cursor/index.ts
2292
+ var TOOL_ID3 = "cursor";
2293
+ function debugLog3(label, data) {
2294
+ if (process.env.OLAKAI_MONITOR_DEBUG !== "1") return;
2295
+ try {
2296
+ const logPath = `/tmp/olakai-monitor-debug-${process.pid}.log`;
2297
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] cursor:${label}: ${typeof data === "string" ? data : JSON.stringify(data, null, 2)}
2298
+ `;
2299
+ fs15.appendFileSync(logPath, line, "utf-8");
2300
+ } catch {
2301
+ }
2302
+ }
2303
+ function resolveCursorProjectRoot(payload, fallbackCwd = process.cwd()) {
2304
+ const roots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots.filter(
2305
+ (r) => typeof r === "string" && r.trim().length > 0
2306
+ ) : [];
2307
+ const candidates = roots.length > 0 ? roots : [fallbackCwd];
2308
+ for (const candidate of candidates) {
2309
+ const found = findConfiguredWorkspace(candidate, [TOOL_ID3]);
2310
+ if (found) return found;
2311
+ }
2312
+ return null;
2313
+ }
2314
+ async function handleCursorHook(eventName, payloadJson, opts = {}) {
2315
+ const event = normalizeEventName(eventName);
2316
+ if (!event) {
2317
+ debugLog3("hook-unknown-event", eventName);
2318
+ return null;
2319
+ }
2320
+ const payload = payloadJson && typeof payloadJson === "object" ? payloadJson : {};
2321
+ debugLog3("event-parsed", { event, payload });
2322
+ const meta = extractCursorMeta(payload);
2323
+ const homeDir = opts.homeDir ?? os8.homedir();
2324
+ switch (event) {
2325
+ case "beforeSubmitPrompt": {
2326
+ if (!meta.conversationId) {
2327
+ debugLog3("missing-conversation-id", { event });
2328
+ return null;
2329
+ }
2330
+ stashPendingPrompt(
2331
+ {
2332
+ prompt: extractPromptText(payload),
2333
+ userEmail: meta.userEmail,
2334
+ model: meta.model,
2335
+ cursorVersion: meta.cursorVersion,
2336
+ conversationId: meta.conversationId,
2337
+ generationId: meta.generationId,
2338
+ workspaceRoots: meta.workspaceRoots,
2339
+ transcriptPath: meta.transcriptPath,
2340
+ attachments: extractAttachments(payload),
2341
+ stashedAt: (/* @__PURE__ */ new Date()).toISOString(),
2342
+ extra: collectUnknownFields(payload)
2343
+ },
2344
+ homeDir
2345
+ );
2346
+ return null;
2347
+ }
2348
+ case "afterAgentResponse": {
2349
+ if (!meta.conversationId) {
2350
+ debugLog3("missing-conversation-id", { event });
2351
+ return null;
2352
+ }
2353
+ const projectRoot = resolveCursorProjectRoot(
2354
+ payload,
2355
+ opts.projectRoot ?? process.cwd()
2356
+ );
2357
+ if (!projectRoot) {
2358
+ debugLog3("config-not-found", {
2359
+ workspaceRoots: meta.workspaceRoots,
2360
+ fallbackCwd: opts.projectRoot ?? process.cwd()
2361
+ });
2362
+ takePendingPrompt(meta.conversationId, meta.generationId, homeDir);
2363
+ return null;
2364
+ }
2365
+ const config = loadCursorConfig(projectRoot);
2366
+ if (!config) {
2367
+ debugLog3("config-load-failed", { projectRoot });
2368
+ takePendingPrompt(meta.conversationId, meta.generationId, homeDir);
2369
+ return null;
2370
+ }
2371
+ const stashed = takePendingPrompt(
2372
+ meta.conversationId,
2373
+ meta.generationId,
2374
+ homeDir
2375
+ );
2376
+ const responseText = extractResponseText(payload);
2377
+ const promptText = stashed?.prompt ?? extractPromptText(payload);
2378
+ const userEmail = meta.userEmail ?? stashed?.userEmail;
2379
+ const model = meta.model ?? stashed?.model;
2380
+ const cursorVersion = meta.cursorVersion ?? stashed?.cursorVersion;
2381
+ const workspaceRoots = meta.workspaceRoots ?? stashed?.workspaceRoots;
2382
+ const transcriptPath = meta.transcriptPath ?? stashed?.transcriptPath;
2383
+ const attachments = extractAttachments(payload) ?? stashed?.attachments;
2384
+ const tokens = extractCursorTokens(payload);
2385
+ const unknownFields = mergeUnknownFields(
2386
+ collectUnknownFields(payload),
2387
+ stashed?.extra
2388
+ );
2389
+ const built = buildPairedPayload(
2390
+ {
2391
+ conversationId: meta.conversationId,
2392
+ generationId: meta.generationId ?? stashed?.generationId,
2393
+ prompt: promptText,
2394
+ response: responseText,
2395
+ userEmail,
2396
+ model,
2397
+ cursorVersion,
2398
+ workspaceRoots,
2399
+ transcriptPath,
2400
+ attachments,
2401
+ inputTokens: tokens.inputTokens,
2402
+ outputTokens: tokens.outputTokens,
2403
+ cacheReadTokens: tokens.cacheReadTokens,
2404
+ cacheWriteTokens: tokens.cacheWriteTokens,
2405
+ unknownFields
2406
+ },
2407
+ config
2408
+ );
2409
+ const finalPayload = userEmail ? { ...built, email: userEmail } : built;
2410
+ debugLog3("payload-built", finalPayload);
2411
+ return {
2412
+ payload: finalPayload,
2413
+ transport: {
2414
+ endpoint: config.monitoringEndpoint,
2415
+ apiKey: config.apiKey,
2416
+ projectRoot
2417
+ }
2418
+ };
2419
+ }
2420
+ case "sessionEnd":
2421
+ case "stop": {
2422
+ const orphan = pickOrphanForFlush(meta.conversationId, homeDir);
2423
+ if (!orphan) return null;
2424
+ const projectRoot = resolveCursorProjectRoot(
2425
+ // Synthesize a payload-like for resolution — orphan carries
2426
+ // the workspace_roots from the stashed beforeSubmitPrompt.
2427
+ {
2428
+ workspace_roots: orphan.workspaceRoots
2429
+ },
2430
+ opts.projectRoot ?? process.cwd()
2431
+ );
2432
+ if (!projectRoot) {
2433
+ debugLog3("orphan-config-not-found", {
2434
+ conversationId: orphan.conversationId
2435
+ });
2436
+ clearPendingPrompt(
2437
+ orphan.conversationId,
2438
+ orphan.generationId,
2439
+ homeDir
2440
+ );
2441
+ return null;
2442
+ }
2443
+ const config = loadCursorConfig(projectRoot);
2444
+ if (!config) {
2445
+ debugLog3("orphan-config-load-failed", { projectRoot });
2446
+ clearPendingPrompt(
2447
+ orphan.conversationId,
2448
+ orphan.generationId,
2449
+ homeDir
2450
+ );
2451
+ return null;
2452
+ }
2453
+ clearPendingPrompt(
2454
+ orphan.conversationId,
2455
+ orphan.generationId,
2456
+ homeDir
2457
+ );
2458
+ const built = buildOrphanPayload(
2459
+ {
2460
+ conversationId: orphan.conversationId,
2461
+ generationId: orphan.generationId,
2462
+ prompt: orphan.prompt,
2463
+ response: "",
2464
+ userEmail: orphan.userEmail,
2465
+ model: orphan.model,
2466
+ cursorVersion: orphan.cursorVersion,
2467
+ workspaceRoots: orphan.workspaceRoots,
2468
+ transcriptPath: orphan.transcriptPath,
2469
+ attachments: orphan.attachments,
2470
+ unknownFields: orphan.extra
2471
+ },
2472
+ config,
2473
+ event === "sessionEnd" ? "session-end" : "orphan-prompt"
2474
+ );
2475
+ const finalPayload = orphan.userEmail ? { ...built, email: orphan.userEmail } : built;
2476
+ return {
2477
+ payload: finalPayload,
2478
+ transport: {
2479
+ endpoint: config.monitoringEndpoint,
2480
+ apiKey: config.apiKey,
2481
+ projectRoot
2482
+ }
2483
+ };
2484
+ }
2485
+ }
2486
+ }
2487
+ function mergeUnknownFields(primary, fallback) {
2488
+ if (!primary && !fallback) return void 0;
2489
+ return { ...fallback ?? {}, ...primary ?? {} };
2490
+ }
2491
+ function pickOrphanForFlush(conversationId, homeDir) {
2492
+ const pending = listPendingPrompts(homeDir);
2493
+ if (pending.length === 0) return null;
2494
+ if (conversationId) {
2495
+ const match = pending.find((p) => p.conversationId === conversationId);
2496
+ if (match) return match;
2497
+ }
2498
+ return pending.sort(
2499
+ (a, b) => (a.stashedAt || "").localeCompare(b.stashedAt || "")
2500
+ )[0];
2501
+ }
2502
+ var cursorPlugin = {
2503
+ id: TOOL_ID3,
2504
+ displayName: "Cursor",
2505
+ install(opts) {
2506
+ return installCursor(opts);
2507
+ },
2508
+ uninstall(opts) {
2509
+ return uninstallCursor(opts);
2510
+ },
2511
+ status(opts) {
2512
+ return getCursorStatus(opts);
2513
+ },
2514
+ handleHook(eventName, payloadJson, opts) {
2515
+ return handleCursorHook(eventName, payloadJson, opts);
2516
+ },
2517
+ async detectInstalled() {
2518
+ try {
2519
+ if (fs15.existsSync(getCursorUserDir())) return true;
2520
+ return whichCursorOnPath();
2521
+ } catch {
2522
+ return false;
2523
+ }
2524
+ }
2525
+ };
2526
+ function whichCursorOnPath() {
2527
+ const pathEnv = process.env.PATH;
2528
+ if (!pathEnv) return false;
2529
+ const sep = process.platform === "win32" ? ";" : ":";
2530
+ const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
2531
+ for (const dir of pathEnv.split(sep)) {
2532
+ for (const ext of exts) {
2533
+ const candidate = `${dir}/cursor${ext.toLowerCase()}`;
2534
+ try {
2535
+ if (fs15.existsSync(candidate)) return true;
2536
+ } catch {
2537
+ }
2538
+ }
2539
+ }
2540
+ return false;
2541
+ }
2542
+ registerPlugin(cursorPlugin);
2543
+
2544
+ // src/monitor/plugins/gemini-cli/index.ts
2545
+ import * as fs20 from "fs";
2546
+ import { spawnSync as spawnSync4 } from "child_process";
2547
+
2548
+ // src/monitor/plugins/gemini-cli/install.ts
2549
+ import * as fs18 from "fs";
2550
+ import * as os10 from "os";
2551
+ import * as path18 from "path";
2552
+
2553
+ // src/monitor/plugins/gemini-cli/config.ts
2554
+ import * as fs16 from "fs";
2555
+ import * as path15 from "path";
2556
+ function getGeminiCliConfigPath(projectRoot) {
2557
+ return getMonitorConfigPath(projectRoot, "gemini-cli");
2558
+ }
2559
+ function loadGeminiCliConfig(projectRoot) {
2560
+ const filePath = getGeminiCliConfigPath(projectRoot);
2561
+ try {
2562
+ if (!fs16.existsSync(filePath)) return null;
2563
+ const raw = fs16.readFileSync(filePath, "utf-8");
2564
+ return JSON.parse(raw);
2565
+ } catch {
2566
+ return null;
2567
+ }
2568
+ }
2569
+ function writeGeminiCliConfig(projectRoot, config) {
2570
+ const filePath = getGeminiCliConfigPath(projectRoot);
2571
+ const dir = path15.dirname(filePath);
2572
+ if (!fs16.existsSync(dir)) {
2573
+ fs16.mkdirSync(dir, { recursive: true });
2574
+ }
2575
+ fs16.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2576
+ try {
2577
+ fs16.chmodSync(filePath, 384);
2578
+ } catch {
2579
+ }
2580
+ }
2581
+ function deleteGeminiCliConfig(projectRoot) {
2582
+ const filePath = getGeminiCliConfigPath(projectRoot);
2583
+ if (!fs16.existsSync(filePath)) return false;
2584
+ try {
2585
+ fs16.unlinkSync(filePath);
2586
+ return true;
2587
+ } catch {
2588
+ return false;
2589
+ }
2590
+ }
2591
+
2592
+ // src/monitor/plugins/gemini-cli/paths.ts
2593
+ import * as os9 from "os";
2594
+ import * as path16 from "path";
2595
+ var GEMINI_HOME_DIRNAME = ".gemini";
2596
+ var GEMINI_SETTINGS_FILENAME = "settings.json";
2597
+ function getGeminiHomeDir(homeDir = os9.homedir()) {
2598
+ return path16.join(homeDir, GEMINI_HOME_DIRNAME);
2599
+ }
2600
+ function getGeminiSettingsPath(homeDir = os9.homedir()) {
2601
+ return path16.join(getGeminiHomeDir(homeDir), GEMINI_SETTINGS_FILENAME);
2602
+ }
2603
+
2604
+ // src/monitor/plugins/gemini-cli/settings.ts
2605
+ import * as fs17 from "fs";
2606
+ import * as path17 from "path";
2607
+ var OLAKAI_HOOK_MARKER4 = "olakai monitor hook --tool gemini-cli";
2608
+ var GEMINI_HOOK_TIMEOUT_MS = 5e3;
2609
+ var SUPPORTED_GEMINI_HOOK_EVENTS = [
2610
+ "SessionStart",
2611
+ "SessionEnd",
2612
+ "BeforeModel",
2613
+ "AfterModel",
2614
+ "BeforeTool",
2615
+ "AfterTool"
2616
+ ];
2617
+ function buildOlakaiHookEntry(event) {
2618
+ return {
2619
+ matcher: "",
2620
+ hooks: [
2621
+ {
2622
+ type: "command",
2623
+ command: `olakai monitor hook --tool gemini-cli ${event}`,
2624
+ timeout: GEMINI_HOOK_TIMEOUT_MS
2625
+ }
2626
+ ]
2627
+ };
2628
+ }
2629
+ var HOOK_DEFINITIONS = Object.fromEntries(
2630
+ SUPPORTED_GEMINI_HOOK_EVENTS.map((event) => [event, [buildOlakaiHookEntry(event)]])
2631
+ );
2632
+ function entryContainsOlakaiHook(entry) {
2633
+ if (!Array.isArray(entry.hooks)) return false;
2634
+ return entry.hooks.some(
2635
+ (h) => typeof h?.command === "string" && h.command.includes(OLAKAI_HOOK_MARKER4)
2636
+ );
2637
+ }
2638
+ function mergeHooksSettings2(existing, definitions = HOOK_DEFINITIONS) {
2639
+ const merged = {
2640
+ ...existing ?? {}
2641
+ };
2642
+ for (const [event, defaultEntries] of Object.entries(definitions)) {
2643
+ const existingEntries = merged[event] ?? [];
2644
+ const hasOlakaiHook2 = existingEntries.some(entryContainsOlakaiHook);
2645
+ if (hasOlakaiHook2) {
2646
+ merged[event] = existingEntries;
2647
+ } else {
2648
+ merged[event] = [...existingEntries, ...defaultEntries];
2649
+ }
2650
+ }
2651
+ return merged;
2652
+ }
2653
+ function removeOlakaiHooks(existing) {
2654
+ if (!existing) return void 0;
2655
+ const cleaned = {};
2656
+ for (const [event, entries] of Object.entries(existing)) {
2657
+ if (!Array.isArray(entries)) continue;
2658
+ const filteredEntries = [];
2659
+ for (const entry of entries) {
2660
+ const handlers = Array.isArray(entry.hooks) ? entry.hooks : [];
2661
+ const remaining = handlers.filter(
2662
+ (h) => typeof h?.command !== "string" || !h.command.includes(OLAKAI_HOOK_MARKER4)
2663
+ );
2664
+ if (remaining.length === 0 && handlers.length > 0) {
2665
+ continue;
2666
+ }
2667
+ if (remaining.length === handlers.length) {
2668
+ filteredEntries.push(entry);
2669
+ } else {
2670
+ filteredEntries.push({ ...entry, hooks: remaining });
2671
+ }
2672
+ }
2673
+ if (filteredEntries.length > 0) {
2674
+ cleaned[event] = filteredEntries;
2675
+ }
2676
+ }
2677
+ return Object.keys(cleaned).length > 0 ? cleaned : void 0;
2678
+ }
2679
+ function hasOlakaiHooksInstalled2(settings) {
2680
+ const hooks = settings.hooks;
2681
+ if (!hooks) return false;
2682
+ for (const entries of Object.values(hooks)) {
2683
+ if (!Array.isArray(entries)) continue;
2684
+ if (entries.some(entryContainsOlakaiHook)) return true;
2685
+ }
2686
+ return false;
2687
+ }
2688
+ function readJsonFile2(filePath) {
2689
+ try {
2690
+ if (!fs17.existsSync(filePath)) return null;
2691
+ const content = fs17.readFileSync(filePath, "utf-8");
2692
+ if (!content.trim()) return null;
2693
+ return JSON.parse(content);
2694
+ } catch {
2695
+ return null;
2696
+ }
2697
+ }
2698
+ function writeJsonFile2(filePath, data) {
2699
+ const dir = path17.dirname(filePath);
2700
+ if (!fs17.existsSync(dir)) {
2701
+ fs17.mkdirSync(dir, { recursive: true });
2702
+ }
2703
+ fs17.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
2704
+ }
2705
+
2706
+ // src/monitor/plugins/gemini-cli/install.ts
2707
+ var GEMINI_CLI_SOURCE = "gemini-cli";
2708
+ var GEMINI_CLI_AGENT_SOURCE = "GEMINI_CLI";
2709
+ var GEMINI_CLI_AGENT_CATEGORY = "CODING";
2710
+ async function installGeminiCli(opts) {
2711
+ const projectRoot = opts.projectRoot ?? process.cwd();
2712
+ const homeDir = opts.homeDir ?? os10.homedir();
2713
+ const token = getValidToken();
2714
+ if (!token) {
2715
+ console.error("Not logged in. Run 'olakai login' first.");
2716
+ process.exit(1);
2717
+ }
2718
+ console.log("Setting up Gemini CLI monitoring for this workspace...\n");
2719
+ console.log(
2720
+ `Note: Gemini CLI hooks are installed globally per user (~/${GEMINI_HOME_DIRNAME}/${GEMINI_SETTINGS_FILENAME}).`
2721
+ );
2722
+ console.log(
2723
+ `Activity from this workspace (${projectRoot}) will be associated with`
2724
+ );
2725
+ console.log("the agent you select below.\n");
2726
+ const agent = await provisionSelfMonitorAgent({
2727
+ projectRoot,
2728
+ source: GEMINI_CLI_AGENT_SOURCE,
2729
+ displayName: "Gemini CLI",
2730
+ category: GEMINI_CLI_AGENT_CATEGORY
2731
+ });
2732
+ const monitoringEndpoint = `${getBaseUrl()}/api/monitoring/prompt`;
2733
+ const apiKey = agent.apiKey?.key;
2734
+ if (!apiKey) {
2735
+ console.error(
2736
+ "Internal error: agent provisioned but no API key returned. Please re-run 'olakai monitor init'."
2737
+ );
2738
+ process.exit(1);
2739
+ }
2740
+ const geminiDir = getGeminiHomeDir(homeDir);
2741
+ if (!fs18.existsSync(geminiDir)) {
2742
+ fs18.mkdirSync(geminiDir, { recursive: true });
2743
+ }
2744
+ const settingsPath = getGeminiSettingsPath(homeDir);
2745
+ const existingSettings = readJsonFile2(settingsPath) ?? {};
2746
+ const mergedHooks = mergeHooksSettings2(existingSettings.hooks);
2747
+ const updatedSettings = {
2748
+ ...existingSettings,
2749
+ hooks: mergedHooks
2750
+ };
2751
+ writeJsonFile2(settingsPath, updatedSettings);
2752
+ const monitorConfig = {
2753
+ agentId: agent.id,
2754
+ apiKey,
2755
+ agentName: agent.name,
2756
+ source: GEMINI_CLI_SOURCE,
2757
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2758
+ monitoringEndpoint
2759
+ };
2760
+ writeGeminiCliConfig(projectRoot, monitorConfig);
2761
+ const configPath = getGeminiCliConfigPath(projectRoot);
2762
+ const configRel = path18.relative(projectRoot, configPath);
2763
+ const settingsDisplay = `~/${path18.join(GEMINI_HOME_DIRNAME, GEMINI_SETTINGS_FILENAME)}`;
2764
+ console.log("");
2765
+ console.log(`\u2713 Agent "${agent.name}" configured (ID: ${agent.id})`);
2766
+ if (agent.apiKey?.key) {
2767
+ console.log("\u2713 API key generated");
2768
+ }
2769
+ console.log(`\u2713 Gemini CLI hooks configured in ${settingsDisplay}`);
2770
+ console.log(`\u2713 Monitor config saved to ${configRel}`);
2771
+ console.log("");
2772
+ console.log(
2773
+ "Congrats! Monitoring is now active. Gemini CLI will report activity to Olakai"
2774
+ );
2775
+ console.log(`on each model turn.`);
2776
+ console.log("");
2777
+ console.log(
2778
+ `\u26A0 Ensure ${OLAKAI_DIR}/ is in your .gitignore (it contains your API key)`
2779
+ );
2780
+ console.log(
2781
+ `\u26A0 Gemini CLI hooks require gemini >= 0.26.0. Earlier versions silently skip them.`
2782
+ );
2783
+ console.log("");
2784
+ console.log("To check status: olakai monitor status --tool gemini-cli");
2785
+ console.log("To disable: olakai monitor disable --tool gemini-cli");
2786
+ return {
2787
+ agentId: agent.id,
2788
+ agentName: agent.name,
2789
+ source: GEMINI_CLI_SOURCE,
2790
+ monitoringEndpoint
2791
+ };
2792
+ }
2793
+ async function uninstallGeminiCli(opts) {
2794
+ const projectRoot = opts.projectRoot ?? process.cwd();
2795
+ const homeDir = opts.homeDir ?? os10.homedir();
2796
+ const settingsPath = getGeminiSettingsPath(homeDir);
2797
+ const settings = readJsonFile2(settingsPath);
2798
+ if (settings && hasOlakaiHooksInstalled2(settings)) {
2799
+ const cleanedHooks = removeOlakaiHooks(settings.hooks);
2800
+ const next = { ...settings };
2801
+ if (cleanedHooks === void 0) {
2802
+ delete next.hooks;
2803
+ } else {
2804
+ next.hooks = cleanedHooks;
2805
+ }
2806
+ const remainingKeys = Object.keys(next).filter((k) => k !== "hooks");
2807
+ const cleanedHasNoHooks = !next.hooks || Object.keys(next.hooks).length === 0;
2808
+ if (cleanedHasNoHooks && remainingKeys.length === 0) {
2809
+ try {
2810
+ fs18.unlinkSync(settingsPath);
2811
+ } catch {
2812
+ }
2813
+ } else {
2814
+ writeJsonFile2(settingsPath, next);
2815
+ }
2816
+ console.log(
2817
+ `\u2713 Olakai hooks removed from ~/${path18.join(GEMINI_HOME_DIRNAME, GEMINI_SETTINGS_FILENAME)}`
2818
+ );
2819
+ } else {
2820
+ console.log("No Olakai hooks found in Gemini CLI settings.");
2821
+ }
2822
+ if (!opts.keepConfig) {
2823
+ const configPath = getGeminiCliConfigPath(projectRoot);
2824
+ const configRel = path18.relative(projectRoot, configPath);
2825
+ if (deleteGeminiCliConfig(projectRoot)) {
2826
+ console.log(`\u2713 Monitor config removed (${configRel})`);
2827
+ }
2828
+ } else {
2829
+ const configPath = getGeminiCliConfigPath(projectRoot);
2830
+ const configRel = path18.relative(projectRoot, configPath);
2831
+ console.log(`Monitor config retained at ${configRel}`);
2832
+ }
2833
+ console.log("");
2834
+ console.log(
2835
+ "Monitoring disabled. Run 'olakai monitor init --tool gemini-cli' to re-enable."
2836
+ );
2837
+ }
2838
+
2839
+ // src/monitor/plugins/gemini-cli/status.ts
2840
+ import * as os11 from "os";
2841
+ import * as path19 from "path";
2842
+ async function getGeminiCliStatus(opts) {
2843
+ const projectRoot = opts?.projectRoot ?? process.cwd();
2844
+ const homeDir = opts?.homeDir ?? os11.homedir();
2845
+ const configPath = getGeminiCliConfigPath(projectRoot);
2846
+ const config = loadGeminiCliConfig(projectRoot);
2847
+ const settings = readJsonFile2(getGeminiSettingsPath(homeDir));
2848
+ const hooksConfigured = settings ? hasOlakaiHooksInstalled2(settings) : false;
2849
+ if (!config) {
2850
+ return {
2851
+ toolId: "gemini-cli",
2852
+ configured: false,
2853
+ hooksConfigured,
2854
+ configPath,
2855
+ notes: hooksConfigured ? [
2856
+ `Gemini CLI hooks present in ~/${GEMINI_HOME_DIRNAME}/${GEMINI_SETTINGS_FILENAME} but no monitor config in this workspace \u2014 re-run init.`
2857
+ ] : []
2858
+ };
2859
+ }
2860
+ return {
2861
+ toolId: "gemini-cli",
2862
+ configured: true,
2863
+ hooksConfigured,
2864
+ agentId: config.agentId,
2865
+ agentName: config.agentName,
2866
+ source: config.source,
2867
+ apiKeyMasked: config.apiKey.slice(0, 12) + "...",
2868
+ monitoringEndpoint: config.monitoringEndpoint,
2869
+ configuredAt: config.createdAt,
2870
+ configPath
2871
+ };
2872
+ }
2873
+
2874
+ // src/monitor/plugins/gemini-cli/pairing-state.ts
2875
+ import * as fs19 from "fs";
2876
+ import * as os12 from "os";
2877
+ import * as path20 from "path";
2878
+ var STATE_DIR_SEGMENTS3 = [".olakai", "gemini-pairings"];
2879
+ function getPairingsDir2(homeDir) {
2880
+ return path20.join(homeDir, ...STATE_DIR_SEGMENTS3);
2881
+ }
2882
+ function sanitizeKeyFragment2(value) {
2883
+ return value.replace(/[^A-Za-z0-9._-]/g, "_");
2884
+ }
2885
+ function getPairingFile2(sessionId, homeDir) {
2886
+ return path20.join(
2887
+ getPairingsDir2(homeDir),
2888
+ `${sanitizeKeyFragment2(sessionId)}.json`
2889
+ );
2890
+ }
2891
+ function ensurePairingsDir(homeDir) {
2892
+ const dir = getPairingsDir2(homeDir);
2893
+ try {
2894
+ if (!fs19.existsSync(dir)) {
2895
+ fs19.mkdirSync(dir, { recursive: true, mode: 448 });
2896
+ }
2897
+ } catch {
2898
+ }
2899
+ }
2900
+ function readPendingTurn(sessionId, homeDir = os12.homedir()) {
2901
+ if (!sessionId) return null;
2902
+ const file = getPairingFile2(sessionId, homeDir);
2903
+ try {
2904
+ if (!fs19.existsSync(file)) return null;
2905
+ const raw = fs19.readFileSync(file, "utf-8");
2906
+ return JSON.parse(raw);
2907
+ } catch {
2908
+ return null;
2909
+ }
2910
+ }
2911
+ function writePendingTurn(sessionId, pending, homeDir = os12.homedir()) {
2912
+ if (!sessionId) return;
2913
+ ensurePairingsDir(homeDir);
2914
+ const file = getPairingFile2(sessionId, homeDir);
2915
+ try {
2916
+ fs19.writeFileSync(file, JSON.stringify(pending, null, 2) + "\n", "utf-8");
2917
+ try {
2918
+ fs19.chmodSync(file, 384);
2919
+ } catch {
2920
+ }
2921
+ } catch {
2922
+ }
2923
+ }
2924
+ function updatePendingTurn(sessionId, mutator, homeDir = os12.homedir()) {
2925
+ if (!sessionId) return null;
2926
+ const current = readPendingTurn(sessionId, homeDir) ?? {
2927
+ prompt: "",
2928
+ stashedAt: (/* @__PURE__ */ new Date()).toISOString(),
2929
+ toolCallCount: 0,
2930
+ toolNames: []
2931
+ };
2932
+ const next = mutator(current);
2933
+ writePendingTurn(sessionId, next, homeDir);
2934
+ return next;
2935
+ }
2936
+ function clearPendingTurn(sessionId, homeDir = os12.homedir()) {
2937
+ if (!sessionId) return;
2938
+ const file = getPairingFile2(sessionId, homeDir);
2939
+ try {
2940
+ if (fs19.existsSync(file)) fs19.unlinkSync(file);
2941
+ } catch {
2942
+ }
2943
+ }
2944
+
2945
+ // src/monitor/plugins/gemini-cli/hook.ts
2946
+ var noopDebug4 = () => {
2947
+ };
2948
+ var SUPPORTED_EVENTS2 = /* @__PURE__ */ new Set([
2949
+ "SessionStart",
2950
+ "SessionEnd",
2951
+ "BeforeModel",
2952
+ "AfterModel",
2953
+ "BeforeTool",
2954
+ "AfterTool"
2955
+ ]);
2956
+ function isSupportedGeminiEvent(eventName) {
2957
+ return SUPPORTED_EVENTS2.has(eventName);
2958
+ }
2959
+ function asString2(value) {
2960
+ return typeof value === "string" && value.trim() ? value : void 0;
2961
+ }
2962
+ function asNumber2(value) {
2963
+ if (typeof value !== "number" || !Number.isFinite(value)) return 0;
2964
+ return value;
2965
+ }
2966
+ function isRecord(value) {
2967
+ return typeof value === "object" && value !== null;
2968
+ }
2969
+ function messageContentToText(content) {
2970
+ if (typeof content === "string") return content;
2971
+ if (Array.isArray(content)) {
2972
+ return content.map((p) => typeof p === "string" ? p : asString2(p?.text) ?? "").join("");
2973
+ }
2974
+ if (isRecord(content) && Array.isArray(content.parts)) {
2975
+ return messageContentToText(content.parts);
2976
+ }
2977
+ return "";
2978
+ }
2979
+ function extractStructuredPrompt(llmRequest) {
2980
+ if (!isRecord(llmRequest) || !Array.isArray(llmRequest.messages)) {
2981
+ return void 0;
2982
+ }
2983
+ for (let i = llmRequest.messages.length - 1; i >= 0; i--) {
2984
+ const msg = llmRequest.messages[i];
2985
+ if (msg && msg.role === "user") {
2986
+ const text = messageContentToText(msg.content).trim();
2987
+ if (text) return text;
2988
+ }
2989
+ }
2990
+ return void 0;
2991
+ }
2992
+ function extractStructuredModel(llmRequest) {
2993
+ return isRecord(llmRequest) ? asString2(llmRequest.model) : void 0;
2994
+ }
2995
+ function extractStructuredResponseDelta(llmResponse) {
2996
+ if (!isRecord(llmResponse)) return "";
2997
+ if (typeof llmResponse.text === "string" && llmResponse.text) {
2998
+ return llmResponse.text;
2999
+ }
3000
+ const parts = llmResponse.candidates?.[0]?.content?.parts;
3001
+ if (Array.isArray(parts)) {
3002
+ return parts.map((p) => typeof p === "string" ? p : asString2(p?.text) ?? "").join("");
3003
+ }
3004
+ return "";
3005
+ }
3006
+ function isStructuredStreamComplete(llmResponse) {
3007
+ if (!isRecord(llmResponse) || !Array.isArray(llmResponse.candidates)) {
3008
+ return false;
3009
+ }
3010
+ return llmResponse.candidates.some((c) => asString2(c?.finishReason));
3011
+ }
3012
+ function extractGeminiTokens(payload) {
3013
+ const meta = payload.llm_response?.usageMetadata;
3014
+ if (isRecord(meta)) {
3015
+ const inputTokens2 = asNumber2(meta.promptTokenCount);
3016
+ const outputTokens2 = asNumber2(meta.candidatesTokenCount);
3017
+ const explicitTotal2 = asNumber2(meta.totalTokenCount);
3018
+ const totalTokens2 = explicitTotal2 > 0 ? explicitTotal2 : inputTokens2 + outputTokens2;
3019
+ return { inputTokens: inputTokens2, outputTokens: outputTokens2, totalTokens: totalTokens2 };
3020
+ }
3021
+ const usage = payload.usage ?? {};
3022
+ const inputTokens = asNumber2(usage.input_tokens);
3023
+ const outputTokens = asNumber2(usage.output_tokens);
3024
+ const explicitTotal = asNumber2(usage.total_tokens);
3025
+ const totalTokens = explicitTotal > 0 ? explicitTotal : inputTokens + outputTokens;
3026
+ return { inputTokens, outputTokens, totalTokens };
3027
+ }
3028
+ function usesStructuredSchema(event) {
3029
+ return isRecord(event.llm_request) || isRecord(event.llm_response);
3030
+ }
3031
+ function buildPayload(args) {
3032
+ if (!args.prompt && !args.response) return null;
3033
+ const customData = {
3034
+ hookEvent: args.hookEvent ?? "AfterModel",
3035
+ sessionId: args.sessionId,
3036
+ cwd: args.cwd ?? "",
3037
+ inputTokens: args.inputTokens,
3038
+ outputTokens: args.outputTokens,
3039
+ toolCallCount: args.toolCallCount,
3040
+ toolNames: args.toolNames
3041
+ };
3042
+ if (args.sessionStartedAt) {
3043
+ customData.sessionStartedAt = args.sessionStartedAt;
3044
+ }
3045
+ if (args.extra) {
3046
+ customData.stashedExtra = args.extra;
3047
+ }
3048
+ return {
3049
+ prompt: args.prompt,
3050
+ response: args.response,
3051
+ chatId: args.sessionId,
3052
+ source: args.config.source,
3053
+ modelName: args.modelName,
3054
+ tokens: args.totalTokens,
3055
+ customData
3056
+ };
3057
+ }
3058
+ function buildAfterModelPayload(event, stashed, config) {
3059
+ const sessionId = asString2(event.session_id) ?? `gemini-cli-${Date.now()}`;
3060
+ const prompt = stashed?.prompt ?? asString2(event.prompt) ?? "";
3061
+ const response = asString2(event.response) ?? "";
3062
+ if (!prompt && !response) return null;
3063
+ const { inputTokens, outputTokens, totalTokens } = extractGeminiTokens(event);
3064
+ const modelName = asString2(event.model) ?? stashed?.modelName ?? void 0;
3065
+ return buildPayload({
3066
+ sessionId,
3067
+ prompt,
3068
+ response,
3069
+ modelName,
3070
+ inputTokens,
3071
+ outputTokens,
3072
+ totalTokens,
3073
+ cwd: asString2(event.cwd) ?? stashed?.cwd,
3074
+ toolCallCount: stashed?.toolCallCount ?? 0,
3075
+ toolNames: stashed?.toolNames ?? [],
3076
+ sessionStartedAt: stashed?.sessionStartedAt,
3077
+ extra: stashed?.extra,
3078
+ hookEvent: event.hook_event_name ?? "AfterModel",
3079
+ config
3080
+ });
3081
+ }
3082
+ function handleGeminiHook(eventName, payloadJson, config, options = {}) {
3083
+ const debugLog6 = options.debugLog ?? noopDebug4;
3084
+ if (!isSupportedGeminiEvent(eventName)) {
3085
+ debugLog6("hook-unknown-event", eventName);
3086
+ return null;
3087
+ }
3088
+ const event = payloadJson ?? {};
3089
+ debugLog6("event-parsed", { eventName, event });
3090
+ const structured = usesStructuredSchema(event);
3091
+ const sessionId = asString2(event.session_id) ?? "";
3092
+ if (!sessionId) {
3093
+ if (eventName !== "AfterModel") {
3094
+ debugLog6("missing-session-id", { eventName });
3095
+ return null;
3096
+ }
3097
+ if (structured) {
3098
+ const prompt = extractStructuredPrompt(event.llm_request) ?? "";
3099
+ const response = extractStructuredResponseDelta(event.llm_response);
3100
+ const { inputTokens, outputTokens, totalTokens } = extractGeminiTokens(event);
3101
+ const payload2 = buildPayload({
3102
+ sessionId: `gemini-cli-${Date.now()}`,
3103
+ prompt,
3104
+ response,
3105
+ modelName: extractStructuredModel(event.llm_request),
3106
+ inputTokens,
3107
+ outputTokens,
3108
+ totalTokens,
3109
+ cwd: asString2(event.cwd),
3110
+ toolCallCount: 0,
3111
+ toolNames: [],
3112
+ config
3113
+ });
3114
+ if (!payload2) debugLog6("after-model-empty-no-session", { eventName });
3115
+ return payload2;
3116
+ }
3117
+ const payload = buildAfterModelPayload(event, null, config);
3118
+ if (!payload) {
3119
+ debugLog6("after-model-empty-no-session", { eventName });
3120
+ return null;
3121
+ }
3122
+ return payload;
3123
+ }
3124
+ switch (eventName) {
3125
+ case "SessionStart": {
3126
+ writePendingTurn(
3127
+ sessionId,
3128
+ {
3129
+ prompt: "",
3130
+ accumulatedResponse: "",
3131
+ cwd: asString2(event.cwd),
3132
+ sessionStartedAt: (/* @__PURE__ */ new Date()).toISOString(),
3133
+ stashedAt: (/* @__PURE__ */ new Date()).toISOString(),
3134
+ toolCallCount: 0,
3135
+ toolNames: []
3136
+ },
3137
+ options.homeDir
3138
+ );
3139
+ return null;
3140
+ }
3141
+ case "BeforeModel": {
3142
+ const structuredPrompt = extractStructuredPrompt(event.llm_request);
3143
+ const structuredModel = extractStructuredModel(event.llm_request);
3144
+ updatePendingTurn(
3145
+ sessionId,
3146
+ (current) => ({
3147
+ ...current,
3148
+ prompt: structuredPrompt ?? asString2(event.prompt) ?? "",
3149
+ modelName: structuredModel ?? asString2(event.model) ?? current.modelName,
3150
+ cwd: asString2(event.cwd) ?? current.cwd,
3151
+ stashedAt: (/* @__PURE__ */ new Date()).toISOString(),
3152
+ // New model call — reset the streamed-response accumulator and
3153
+ // the per-turn tool counters so they only reflect THIS call.
3154
+ accumulatedResponse: "",
3155
+ toolCallCount: 0,
3156
+ toolNames: []
3157
+ }),
3158
+ options.homeDir
3159
+ );
3160
+ return null;
3161
+ }
3162
+ case "BeforeTool": {
3163
+ const toolName = asString2(event.tool_name);
3164
+ updatePendingTurn(
3165
+ sessionId,
3166
+ (current) => {
3167
+ const nextToolNames = toolName ? current.toolNames.includes(toolName) ? current.toolNames : [...current.toolNames, toolName] : current.toolNames;
3168
+ return {
3169
+ ...current,
3170
+ toolCallCount: current.toolCallCount + 1,
3171
+ toolNames: nextToolNames
3172
+ };
3173
+ },
3174
+ options.homeDir
3175
+ );
3176
+ return null;
3177
+ }
3178
+ case "AfterTool": {
3179
+ return null;
3180
+ }
3181
+ case "AfterModel": {
3182
+ if (!structured) {
3183
+ const stashed = readPendingTurn(sessionId, options.homeDir);
3184
+ const payload2 = buildAfterModelPayload(event, stashed, config);
3185
+ clearPendingTurn(sessionId, options.homeDir);
3186
+ if (!payload2) debugLog6("after-model-empty", { sessionId });
3187
+ return payload2;
3188
+ }
3189
+ const delta = extractStructuredResponseDelta(event.llm_response);
3190
+ const complete = isStructuredStreamComplete(event.llm_response);
3191
+ const structuredPrompt = extractStructuredPrompt(event.llm_request);
3192
+ const structuredModel = extractStructuredModel(event.llm_request);
3193
+ const updated = updatePendingTurn(
3194
+ sessionId,
3195
+ (current) => ({
3196
+ ...current,
3197
+ prompt: structuredPrompt ?? current.prompt,
3198
+ modelName: structuredModel ?? current.modelName,
3199
+ cwd: asString2(event.cwd) ?? current.cwd,
3200
+ accumulatedResponse: (current.accumulatedResponse ?? "") + delta
3201
+ }),
3202
+ options.homeDir
3203
+ );
3204
+ if (!complete) {
3205
+ debugLog6("after-model-streaming", {
3206
+ sessionId,
3207
+ deltaLen: delta.length
3208
+ });
3209
+ return null;
3210
+ }
3211
+ const { inputTokens, outputTokens, totalTokens } = extractGeminiTokens(event);
3212
+ const response = updated?.accumulatedResponse ?? "";
3213
+ const payload = buildPayload({
3214
+ sessionId,
3215
+ prompt: structuredPrompt ?? updated?.prompt ?? "",
3216
+ response,
3217
+ modelName: structuredModel ?? updated?.modelName,
3218
+ inputTokens,
3219
+ outputTokens,
3220
+ totalTokens,
3221
+ cwd: asString2(event.cwd) ?? updated?.cwd,
3222
+ toolCallCount: updated?.toolCallCount ?? 0,
3223
+ toolNames: updated?.toolNames ?? [],
3224
+ sessionStartedAt: updated?.sessionStartedAt,
3225
+ extra: updated?.extra,
3226
+ hookEvent: event.hook_event_name ?? "AfterModel",
3227
+ config
3228
+ });
3229
+ updatePendingTurn(
3230
+ sessionId,
3231
+ (current) => ({
3232
+ ...current,
3233
+ accumulatedResponse: "",
3234
+ toolCallCount: 0,
3235
+ toolNames: []
3236
+ }),
3237
+ options.homeDir
3238
+ );
3239
+ if (!payload) {
3240
+ debugLog6("after-model-empty", { sessionId });
3241
+ } else {
3242
+ debugLog6("payload-built", {
3243
+ sessionId,
3244
+ responseLen: response.length,
3245
+ totalTokens
3246
+ });
3247
+ }
3248
+ return payload;
3249
+ }
3250
+ case "SessionEnd": {
3251
+ clearPendingTurn(sessionId, options.homeDir);
3252
+ return null;
3253
+ }
3254
+ }
3255
+ return null;
3256
+ }
3257
+
3258
+ // src/monitor/plugins/gemini-cli/index.ts
3259
+ var TOOL_ID4 = "gemini-cli";
3260
+ function debugLog4(label, data) {
3261
+ if (process.env.OLAKAI_MONITOR_DEBUG !== "1") return;
3262
+ try {
3263
+ const logPath = `/tmp/olakai-monitor-debug-${process.pid}.log`;
3264
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] gemini-cli/${label}: ${typeof data === "string" ? data : JSON.stringify(data, null, 2)}
3265
+ `;
3266
+ fs20.appendFileSync(logPath, line, "utf-8");
3267
+ } catch {
3268
+ }
3269
+ }
3270
+ function resolveGeminiCliProjectRoot(eventData, fallbackCwd) {
3271
+ const payloadCwd = typeof eventData.cwd === "string" && eventData.cwd.trim() ? eventData.cwd : fallbackCwd;
3272
+ return findConfiguredWorkspace(payloadCwd, [TOOL_ID4]);
3273
+ }
3274
+ var geminiCliPlugin = {
3275
+ id: TOOL_ID4,
3276
+ displayName: "Gemini CLI",
3277
+ install(opts) {
3278
+ return installGeminiCli(opts);
3279
+ },
3280
+ uninstall(opts) {
3281
+ return uninstallGeminiCli(opts);
3282
+ },
3283
+ status(opts) {
3284
+ return getGeminiCliStatus(opts);
3285
+ },
3286
+ async handleHook(eventName, payloadJson) {
3287
+ const eventData = payloadJson ?? {};
3288
+ debugLog4("hook-fired", { eventName, hasPayload: payloadJson != null });
3289
+ const projectRoot = resolveGeminiCliProjectRoot(
3290
+ eventData,
3291
+ process.cwd()
3292
+ );
3293
+ if (!projectRoot) {
3294
+ debugLog4("config-not-found", {
3295
+ startDir: typeof eventData.cwd === "string" && eventData.cwd.trim() ? eventData.cwd : process.cwd()
3296
+ });
3297
+ return null;
3298
+ }
3299
+ const config = loadGeminiCliConfig(projectRoot);
3300
+ if (!config) {
3301
+ debugLog4("monitor-config-missing", { projectRoot });
3302
+ return null;
3303
+ }
3304
+ const payload = handleGeminiHook(eventName, eventData, config, {
3305
+ debugLog: debugLog4
3306
+ });
3307
+ if (!payload) return null;
3308
+ return {
3309
+ payload,
3310
+ transport: {
3311
+ endpoint: config.monitoringEndpoint,
3312
+ apiKey: config.apiKey,
3313
+ projectRoot
3314
+ }
3315
+ };
3316
+ },
3317
+ async detectInstalled(opts) {
3318
+ const projectRoot = opts?.projectRoot ?? process.cwd();
3319
+ try {
3320
+ if (fs20.existsSync(getGeminiCliConfigPath(projectRoot))) {
3321
+ return true;
3322
+ }
3323
+ } catch {
3324
+ }
3325
+ try {
3326
+ if (fs20.existsSync(getGeminiSettingsPath())) {
3327
+ return true;
3328
+ }
3329
+ if (fs20.existsSync(getGeminiHomeDir())) {
3330
+ return true;
3331
+ }
3332
+ } catch {
3333
+ }
3334
+ return detectGeminiBinaryOnPath();
3335
+ }
3336
+ };
3337
+ function detectGeminiBinaryOnPath() {
3338
+ try {
3339
+ const probe = spawnSync4(
3340
+ process.platform === "win32" ? "where" : "which",
3341
+ ["gemini"],
3342
+ {
3343
+ stdio: ["ignore", "pipe", "ignore"],
3344
+ timeout: 1e3
3345
+ }
3346
+ );
3347
+ if (probe.status === 0 && probe.stdout && probe.stdout.toString().trim()) {
3348
+ return true;
3349
+ }
3350
+ } catch {
3351
+ }
3352
+ return false;
3353
+ }
3354
+ registerPlugin(geminiCliPlugin);
3355
+
3356
+ // src/monitor/plugins/antigravity/index.ts
3357
+ import * as fs25 from "fs";
3358
+ import { spawnSync as spawnSync5 } from "child_process";
3359
+
3360
+ // src/monitor/plugins/antigravity/install.ts
3361
+ import * as fs23 from "fs";
3362
+ import * as os14 from "os";
3363
+ import * as path24 from "path";
3364
+
3365
+ // src/monitor/plugins/antigravity/config.ts
3366
+ import * as fs21 from "fs";
3367
+ import * as path21 from "path";
3368
+ function getAntigravityConfigPath(projectRoot) {
3369
+ return getMonitorConfigPath(projectRoot, "antigravity");
3370
+ }
3371
+ function loadAntigravityConfig(projectRoot) {
3372
+ const filePath = getAntigravityConfigPath(projectRoot);
3373
+ try {
3374
+ if (!fs21.existsSync(filePath)) return null;
3375
+ const raw = fs21.readFileSync(filePath, "utf-8");
3376
+ return JSON.parse(raw);
3377
+ } catch {
3378
+ return null;
3379
+ }
3380
+ }
3381
+ function writeAntigravityConfig(projectRoot, config) {
3382
+ const filePath = getAntigravityConfigPath(projectRoot);
3383
+ const dir = path21.dirname(filePath);
3384
+ if (!fs21.existsSync(dir)) {
3385
+ fs21.mkdirSync(dir, { recursive: true });
3386
+ }
3387
+ fs21.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
3388
+ try {
3389
+ fs21.chmodSync(filePath, 384);
3390
+ } catch {
3391
+ }
3392
+ }
3393
+ function deleteAntigravityConfig(projectRoot) {
3394
+ const filePath = getAntigravityConfigPath(projectRoot);
3395
+ if (!fs21.existsSync(filePath)) return false;
3396
+ try {
3397
+ fs21.unlinkSync(filePath);
3398
+ return true;
3399
+ } catch {
3400
+ return false;
3401
+ }
3402
+ }
3403
+
3404
+ // src/monitor/plugins/antigravity/paths.ts
3405
+ import * as os13 from "os";
3406
+ import * as path22 from "path";
3407
+ var GEMINI_HOME_DIRNAME2 = ".gemini";
3408
+ var ANTIGRAVITY_CONFIG_DIRNAME = "config";
3409
+ var ANTIGRAVITY_HOOKS_FILENAME = "hooks.json";
3410
+ var ANTIGRAVITY_CLI_DIRNAME = "antigravity-cli";
3411
+ function getGeminiHomeDir2(homeDir = os13.homedir()) {
3412
+ return path22.join(homeDir, GEMINI_HOME_DIRNAME2);
3413
+ }
3414
+ function getGeminiConfigDir(homeDir = os13.homedir()) {
3415
+ return path22.join(getGeminiHomeDir2(homeDir), ANTIGRAVITY_CONFIG_DIRNAME);
3416
+ }
3417
+ function getGeminiConfigHooksPath(homeDir = os13.homedir()) {
3418
+ return path22.join(getGeminiConfigDir(homeDir), ANTIGRAVITY_HOOKS_FILENAME);
3419
+ }
3420
+ function getAntigravityCliDir(homeDir = os13.homedir()) {
3421
+ return path22.join(getGeminiHomeDir2(homeDir), ANTIGRAVITY_CLI_DIRNAME);
3422
+ }
3423
+
3424
+ // src/monitor/plugins/antigravity/settings.ts
3425
+ import * as fs22 from "fs";
3426
+ import * as path23 from "path";
3427
+ var OLAKAI_HOOK_NAME = "olakai-monitor";
3428
+ var OLAKAI_HOOK_MARKER5 = "olakai monitor hook --tool antigravity";
3429
+ var ANTIGRAVITY_HOOK_TIMEOUT_SECONDS = 30;
3430
+ var SUPPORTED_ANTIGRAVITY_HOOK_EVENTS = ["Stop"];
3431
+ function buildOlakaiHookCommand(event) {
3432
+ return {
3433
+ type: "command",
3434
+ command: `${OLAKAI_HOOK_MARKER5} ${event}`,
3435
+ timeout: ANTIGRAVITY_HOOK_TIMEOUT_SECONDS
3436
+ };
3437
+ }
3438
+ function buildOlakaiHookEntry2() {
3439
+ const entry = {};
3440
+ for (const event of SUPPORTED_ANTIGRAVITY_HOOK_EVENTS) {
3441
+ entry[event] = [buildOlakaiHookCommand(event)];
3442
+ }
3443
+ return entry;
3444
+ }
3445
+ function mergeHooksFile(existing) {
3446
+ const merged = { ...existing ?? {} };
3447
+ merged[OLAKAI_HOOK_NAME] = buildOlakaiHookEntry2();
3448
+ return merged;
3449
+ }
3450
+ function entryContainsOlakaiHook2(entry) {
3451
+ for (const commands of Object.values(entry)) {
3452
+ if (!Array.isArray(commands)) continue;
3453
+ if (commands.some(
3454
+ (c) => typeof c?.command === "string" && c.command.includes(OLAKAI_HOOK_MARKER5)
3455
+ )) {
3456
+ return true;
3457
+ }
3458
+ }
3459
+ return false;
3460
+ }
3461
+ function removeOlakaiHooks2(existing) {
3462
+ if (!existing) return void 0;
3463
+ const cleaned = {};
3464
+ for (const [name, entry] of Object.entries(existing)) {
3465
+ if (name === OLAKAI_HOOK_NAME) continue;
3466
+ if (!entry || typeof entry !== "object") continue;
3467
+ const cleanedEntry = {};
3468
+ for (const [event, commands] of Object.entries(entry)) {
3469
+ if (!Array.isArray(commands)) {
3470
+ cleanedEntry[event] = commands;
3471
+ continue;
3472
+ }
3473
+ const remaining = commands.filter(
3474
+ (c) => typeof c?.command !== "string" || !c.command.includes(OLAKAI_HOOK_MARKER5)
3475
+ );
3476
+ if (remaining.length > 0) {
3477
+ cleanedEntry[event] = remaining;
3478
+ }
3479
+ }
3480
+ if (Object.keys(cleanedEntry).length > 0) {
3481
+ cleaned[name] = cleanedEntry;
3482
+ }
3483
+ }
3484
+ return Object.keys(cleaned).length > 0 ? cleaned : void 0;
3485
+ }
3486
+ function hasOlakaiHook(hooksFile) {
3487
+ if (!hooksFile) return false;
3488
+ for (const entry of Object.values(hooksFile)) {
3489
+ if (entry && entryContainsOlakaiHook2(entry)) return true;
3490
+ }
3491
+ return false;
3492
+ }
3493
+ function readJsonFile3(filePath) {
3494
+ try {
3495
+ if (!fs22.existsSync(filePath)) return null;
3496
+ const content = fs22.readFileSync(filePath, "utf-8");
3497
+ if (!content.trim()) return null;
3498
+ return JSON.parse(content);
3499
+ } catch {
3500
+ return null;
3501
+ }
3502
+ }
3503
+ function writeJsonFile3(filePath, data) {
3504
+ const dir = path23.dirname(filePath);
3505
+ if (!fs22.existsSync(dir)) {
3506
+ fs22.mkdirSync(dir, { recursive: true });
3507
+ }
3508
+ fs22.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
3509
+ }
3510
+
3511
+ // src/monitor/plugins/antigravity/install.ts
3512
+ var ANTIGRAVITY_SOURCE = "antigravity";
3513
+ var ANTIGRAVITY_AGENT_SOURCE = "ANTIGRAVITY";
3514
+ var ANTIGRAVITY_AGENT_CATEGORY = "CODING";
3515
+ var HOOKS_DISPLAY = "~/.gemini/config/hooks.json";
3516
+ async function installAntigravity(opts) {
3517
+ const projectRoot = opts.projectRoot ?? process.cwd();
3518
+ const homeDir = opts.homeDir ?? os14.homedir();
3519
+ const token = getValidToken();
3520
+ if (!token) {
3521
+ console.error("Not logged in. Run 'olakai login' first.");
3522
+ process.exit(1);
3523
+ }
3524
+ console.log("Setting up Antigravity monitoring for this workspace...\n");
3525
+ console.log(
3526
+ `Note: Antigravity hooks are installed globally per user (${HOOKS_DISPLAY}).`
3527
+ );
3528
+ console.log(
3529
+ `Activity from this workspace (${projectRoot}) will be associated with`
3530
+ );
3531
+ console.log("the agent you select below.\n");
3532
+ const agent = await provisionSelfMonitorAgent({
3533
+ projectRoot,
3534
+ source: ANTIGRAVITY_AGENT_SOURCE,
3535
+ displayName: "Antigravity",
3536
+ category: ANTIGRAVITY_AGENT_CATEGORY
3537
+ });
3538
+ const monitoringEndpoint = `${getBaseUrl()}/api/monitoring/prompt`;
3539
+ const apiKey = agent.apiKey?.key;
3540
+ if (!apiKey) {
3541
+ console.error(
3542
+ "Internal error: agent provisioned but no API key returned. Please re-run 'olakai monitor init'."
3543
+ );
3544
+ process.exit(1);
3545
+ }
3546
+ const configDir = getGeminiConfigDir(homeDir);
3547
+ if (!fs23.existsSync(configDir)) {
3548
+ fs23.mkdirSync(configDir, { recursive: true });
3549
+ }
3550
+ const hooksPath = getGeminiConfigHooksPath(homeDir);
3551
+ const existingHooks = readJsonFile3(hooksPath) ?? {};
3552
+ const mergedHooks = mergeHooksFile(existingHooks);
3553
+ writeJsonFile3(hooksPath, mergedHooks);
3554
+ const monitorConfig = {
3555
+ agentId: agent.id,
3556
+ apiKey,
3557
+ agentName: agent.name,
3558
+ source: ANTIGRAVITY_SOURCE,
3559
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3560
+ monitoringEndpoint
3561
+ };
3562
+ writeAntigravityConfig(projectRoot, monitorConfig);
3563
+ const configPath = getAntigravityConfigPath(projectRoot);
3564
+ const configRel = path24.relative(projectRoot, configPath);
3565
+ console.log("");
3566
+ console.log(`\u2713 Agent "${agent.name}" configured (ID: ${agent.id})`);
3567
+ if (agent.apiKey?.key) {
3568
+ console.log("\u2713 API key generated");
3569
+ }
3570
+ console.log(`\u2713 Antigravity Stop hook configured in ${HOOKS_DISPLAY}`);
3571
+ console.log(`\u2713 Monitor config saved to ${configRel}`);
3572
+ console.log("");
3573
+ console.log(
3574
+ "Congrats! Monitoring is now active. Antigravity will report activity to Olakai"
3575
+ );
3576
+ console.log(`when the agent finishes a turn (Stop hook).`);
3577
+ console.log("");
3578
+ console.log(
3579
+ `\u26A0 Ensure ${OLAKAI_DIR}/ is in your .gitignore (it contains your API key)`
3580
+ );
3581
+ console.log(
3582
+ `\u26A0 Antigravity hooks are global (${HOOKS_DISPLAY}) and require an agy build with hooks support.`
3583
+ );
3584
+ console.log(
3585
+ `\u26A0 Turn capture is interactive-only \u2014 headless 'agy -p' runs skip hooks.`
3586
+ );
3587
+ console.log("");
3588
+ console.log("To check status: olakai monitor status --tool antigravity");
3589
+ console.log("To disable: olakai monitor disable --tool antigravity");
3590
+ return {
3591
+ agentId: agent.id,
3592
+ agentName: agent.name,
3593
+ source: ANTIGRAVITY_SOURCE,
3594
+ monitoringEndpoint
3595
+ };
3596
+ }
3597
+ async function uninstallAntigravity(opts) {
3598
+ const projectRoot = opts.projectRoot ?? process.cwd();
3599
+ const homeDir = opts.homeDir ?? os14.homedir();
3600
+ const hooksPath = getGeminiConfigHooksPath(homeDir);
3601
+ const hooksFile = readJsonFile3(hooksPath);
3602
+ if (hooksFile && hasOlakaiHook(hooksFile)) {
3603
+ const cleaned = removeOlakaiHooks2(hooksFile);
3604
+ if (cleaned === void 0) {
3605
+ try {
3606
+ fs23.unlinkSync(hooksPath);
3607
+ } catch {
3608
+ }
3609
+ } else {
3610
+ writeJsonFile3(hooksPath, cleaned);
3611
+ }
3612
+ console.log(`\u2713 Olakai hooks removed from ${HOOKS_DISPLAY}`);
3613
+ } else {
3614
+ console.log("No Olakai hooks found in Antigravity hooks.json.");
3615
+ }
3616
+ if (!opts.keepConfig) {
3617
+ const configPath = getAntigravityConfigPath(projectRoot);
3618
+ const configRel = path24.relative(projectRoot, configPath);
3619
+ if (deleteAntigravityConfig(projectRoot)) {
3620
+ console.log(`\u2713 Monitor config removed (${configRel})`);
3621
+ }
3622
+ } else {
3623
+ const configPath = getAntigravityConfigPath(projectRoot);
3624
+ const configRel = path24.relative(projectRoot, configPath);
3625
+ console.log(`Monitor config retained at ${configRel}`);
3626
+ }
3627
+ console.log("");
3628
+ console.log(
3629
+ "Monitoring disabled. Run 'olakai monitor init --tool antigravity' to re-enable."
3630
+ );
3631
+ }
3632
+
3633
+ // src/monitor/plugins/antigravity/status.ts
3634
+ import * as os15 from "os";
3635
+ import * as path25 from "path";
3636
+ var HOOKS_DISPLAY2 = "~/.gemini/config/hooks.json";
3637
+ async function getAntigravityStatus(opts) {
3638
+ const projectRoot = opts?.projectRoot ?? process.cwd();
3639
+ const homeDir = opts?.homeDir ?? os15.homedir();
3640
+ const configPath = getAntigravityConfigPath(projectRoot);
3641
+ const config = loadAntigravityConfig(projectRoot);
3642
+ const hooksFile = readJsonFile3(
3643
+ getGeminiConfigHooksPath(homeDir)
3644
+ );
3645
+ const hooksConfigured = hasOlakaiHook(hooksFile);
3646
+ if (!config) {
3647
+ return {
3648
+ toolId: "antigravity",
3649
+ configured: false,
3650
+ hooksConfigured,
3651
+ configPath,
3652
+ notes: hooksConfigured ? [
3653
+ `Antigravity hooks present in ${HOOKS_DISPLAY2} but no monitor config in this workspace \u2014 re-run init.`
3654
+ ] : []
3655
+ };
3656
+ }
3657
+ return {
3658
+ toolId: "antigravity",
3659
+ configured: true,
3660
+ hooksConfigured,
3661
+ agentId: config.agentId,
3662
+ agentName: config.agentName,
3663
+ source: config.source,
3664
+ apiKeyMasked: config.apiKey.slice(0, 12) + "...",
3665
+ monitoringEndpoint: config.monitoringEndpoint,
3666
+ configuredAt: config.createdAt,
3667
+ configPath
3668
+ };
3669
+ }
3670
+
3671
+ // src/monitor/plugins/antigravity/hook.ts
3672
+ import * as fs24 from "fs";
3673
+
3674
+ // src/monitor/plugins/antigravity/transcript.ts
3675
+ var USER_REQUEST_RE = /<USER_REQUEST>([\s\S]*?)<\/USER_REQUEST>/;
3676
+ function parseTranscriptLines(raw) {
3677
+ const lines = [];
3678
+ for (const line of raw.split("\n")) {
3679
+ const trimmed = line.trim();
3680
+ if (!trimmed) continue;
3681
+ try {
3682
+ const parsed = JSON.parse(trimmed);
3683
+ if (parsed && typeof parsed === "object") {
3684
+ lines.push(parsed);
3685
+ }
3686
+ } catch {
3687
+ }
3688
+ }
3689
+ return lines;
3690
+ }
3691
+ function asContentString(value) {
3692
+ return typeof value === "string" ? value : "";
3693
+ }
3694
+ function extractPrompt(lines) {
3695
+ let lastContent = null;
3696
+ for (const line of lines) {
3697
+ if (line?.type === "USER_INPUT") {
3698
+ lastContent = asContentString(line.content);
3699
+ }
3700
+ }
3701
+ if (lastContent === null) return "";
3702
+ const match = USER_REQUEST_RE.exec(lastContent);
3703
+ if (match) {
3704
+ return match[1].trim();
3705
+ }
3706
+ return lastContent.trim();
3707
+ }
3708
+ function extractResponse(lines) {
3709
+ let lastContent = null;
3710
+ for (const line of lines) {
3711
+ if (line?.type === "PLANNER_RESPONSE") {
3712
+ lastContent = asContentString(line.content);
3713
+ }
3714
+ }
3715
+ if (lastContent === null) return "";
3716
+ return lastContent.trim();
3717
+ }
3718
+
3719
+ // src/monitor/plugins/antigravity/hook.ts
3720
+ var noopDebug5 = () => {
3721
+ };
3722
+ var SUPPORTED_EVENTS3 = /* @__PURE__ */ new Set(["Stop"]);
3723
+ function isSupportedAntigravityEvent(eventName) {
3724
+ return SUPPORTED_EVENTS3.has(eventName);
3725
+ }
3726
+ function asString3(value) {
3727
+ return typeof value === "string" && value.trim() ? value : void 0;
3728
+ }
3729
+ function getWorkspacePath(payload) {
3730
+ const paths = payload.workspacePaths;
3731
+ if (Array.isArray(paths)) {
3732
+ const first = paths[0];
3733
+ if (typeof first === "string" && first.trim()) return first;
3734
+ }
3735
+ return void 0;
3736
+ }
3737
+ function extractFromTranscript2(transcriptPath, debugLog6 = noopDebug5) {
3738
+ if (!transcriptPath) return { prompt: "", response: "" };
3739
+ let raw;
3740
+ try {
3741
+ raw = fs24.readFileSync(transcriptPath, "utf-8");
3742
+ } catch (err) {
3743
+ debugLog6("transcript-read-failed", {
3744
+ transcriptPath,
3745
+ error: err.message
3746
+ });
3747
+ return { prompt: "", response: "" };
3748
+ }
3749
+ const lines = parseTranscriptLines(raw);
3750
+ return {
3751
+ prompt: extractPrompt(lines),
3752
+ response: extractResponse(lines)
3753
+ };
3754
+ }
3755
+ function buildStopPayload(payload, extracted, config) {
3756
+ const prompt = extracted.prompt ?? "";
3757
+ const response = extracted.response ?? "";
3758
+ if (!prompt.trim() && !response.trim()) return null;
3759
+ const conversationId = asString3(payload.conversationId) ?? `antigravity-${Date.now()}`;
3760
+ const cwd = getWorkspacePath(payload) ?? "";
3761
+ const customData = {
3762
+ hookEvent: "Stop",
3763
+ conversationId,
3764
+ cwd,
3765
+ terminationReason: asString3(payload.terminationReason) ?? ""
3766
+ };
3767
+ return {
3768
+ prompt,
3769
+ response,
3770
+ chatId: conversationId,
3771
+ source: config.source,
3772
+ tokens: 0,
3773
+ customData
3774
+ };
3775
+ }
3776
+ function handleAntigravityHook(eventName, payloadJson, config, options = {}) {
3777
+ const debugLog6 = options.debugLog ?? noopDebug5;
3778
+ if (!isSupportedAntigravityEvent(eventName)) {
3779
+ debugLog6("hook-unknown-event", eventName);
3780
+ return null;
3781
+ }
3782
+ const payload = payloadJson ?? {};
3783
+ debugLog6("event-parsed", { eventName, payload });
3784
+ const extracted = extractFromTranscript2(payload.transcriptPath, debugLog6);
3785
+ const result = buildStopPayload(payload, extracted, config);
3786
+ if (!result) {
3787
+ debugLog6("stop-empty", { conversationId: payload.conversationId });
3788
+ return null;
3789
+ }
3790
+ debugLog6("payload-built", result);
3791
+ return result;
3792
+ }
3793
+
3794
+ // src/monitor/plugins/antigravity/index.ts
3795
+ var TOOL_ID5 = "antigravity";
3796
+ function debugLog5(label, data) {
3797
+ if (process.env.OLAKAI_MONITOR_DEBUG !== "1") return;
3798
+ try {
3799
+ const logPath = `/tmp/olakai-monitor-debug-${process.pid}.log`;
3800
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] antigravity/${label}: ${typeof data === "string" ? data : JSON.stringify(data, null, 2)}
3801
+ `;
3802
+ fs25.appendFileSync(logPath, line, "utf-8");
3803
+ } catch {
3804
+ }
3805
+ }
3806
+ function resolveAntigravityProjectRoot(payload, fallbackCwd) {
3807
+ const startDir = getWorkspacePath(payload) ?? fallbackCwd;
3808
+ return findConfiguredWorkspace(startDir, [TOOL_ID5]);
3809
+ }
3810
+ var antigravityPlugin = {
3811
+ id: TOOL_ID5,
3812
+ displayName: "Antigravity",
3813
+ install(opts) {
3814
+ return installAntigravity(opts);
3815
+ },
3816
+ uninstall(opts) {
3817
+ return uninstallAntigravity(opts);
3818
+ },
3819
+ status(opts) {
3820
+ return getAntigravityStatus(opts);
3821
+ },
3822
+ async handleHook(eventName, payloadJson) {
3823
+ const payload = payloadJson ?? {};
3824
+ debugLog5("hook-fired", { eventName, hasPayload: payloadJson != null });
3825
+ const projectRoot = resolveAntigravityProjectRoot(payload, process.cwd());
3826
+ if (!projectRoot) {
3827
+ debugLog5("config-not-found", {
3828
+ startDir: getWorkspacePath(payload) ?? process.cwd()
3829
+ });
3830
+ return null;
3831
+ }
3832
+ const config = loadAntigravityConfig(projectRoot);
3833
+ if (!config) {
3834
+ debugLog5("monitor-config-missing", { projectRoot });
3835
+ return null;
3836
+ }
3837
+ const result = handleAntigravityHook(eventName, payload, config, {
3838
+ debugLog: debugLog5
3839
+ });
3840
+ if (!result) return null;
3841
+ return {
3842
+ payload: result,
3843
+ transport: {
3844
+ endpoint: config.monitoringEndpoint,
3845
+ apiKey: config.apiKey,
3846
+ projectRoot
3847
+ }
3848
+ };
3849
+ },
3850
+ async detectInstalled(opts) {
3851
+ const projectRoot = opts?.projectRoot ?? process.cwd();
3852
+ try {
3853
+ if (fs25.existsSync(getAntigravityConfigPath(projectRoot))) {
3854
+ return true;
3855
+ }
3856
+ } catch {
3857
+ }
3858
+ try {
3859
+ if (fs25.existsSync(getAntigravityCliDir())) {
3860
+ return true;
3861
+ }
3862
+ } catch {
3863
+ }
3864
+ return detectAgyBinaryOnPath();
3865
+ }
3866
+ };
3867
+ function detectAgyBinaryOnPath() {
3868
+ try {
3869
+ const probe = spawnSync5(
3870
+ process.platform === "win32" ? "where" : "which",
3871
+ ["agy"],
3872
+ {
3873
+ stdio: ["ignore", "pipe", "ignore"],
3874
+ timeout: 1e3
3875
+ }
3876
+ );
3877
+ if (probe.status === 0 && probe.stdout && probe.stdout.toString().trim()) {
3878
+ return true;
3879
+ }
3880
+ } catch {
3881
+ }
3882
+ return false;
3883
+ }
3884
+ registerPlugin(antigravityPlugin);
3885
+
3886
+ // src/monitor/install.ts
3887
+ async function runMonitorInstall(toolId, opts = {}) {
3888
+ const plugin = getPlugin(toolId);
3889
+ const projectRoot = opts.projectRoot ?? process.cwd();
3890
+ const result = await plugin.install({
3891
+ projectRoot,
3892
+ interactive: opts.interactive ?? true
3893
+ });
3894
+ recordInstall(toolId, projectRoot, result);
3895
+ return result;
3896
+ }
3897
+ function recordInstall(toolId, projectRoot, result) {
3898
+ try {
3899
+ const entry = {
3900
+ path: projectRoot,
3901
+ tool: toolId,
3902
+ scope: getToolScope(toolId),
3903
+ agentId: result.agentId,
3904
+ agentName: result.agentName,
3905
+ source: result.source,
3906
+ configPath: getMonitorConfigPath(projectRoot, toolId),
3907
+ hooksPath: getToolHooksPath(toolId, projectRoot),
3908
+ monitoringEndpoint: result.monitoringEndpoint,
3909
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
3910
+ };
3911
+ upsertEntry(entry);
3912
+ } catch {
3913
+ }
3914
+ }
3915
+
3916
+ export {
3917
+ promptUser,
3918
+ isInteractive,
3919
+ getCodexHomeDir,
3920
+ getCodexConfigPath,
3921
+ TOOL_IDS,
3922
+ getPlugin,
3923
+ listPlugins,
3924
+ isToolId,
3925
+ loadCodexConfig,
3926
+ writeCodexConfig,
3927
+ installCodexHooksConfig,
3928
+ loadCursorConfig,
3929
+ writeCursorConfig,
3930
+ getCursorHooksPath,
3931
+ mergeCursorHooks,
3932
+ loadGeminiCliConfig,
3933
+ writeGeminiCliConfig,
3934
+ getGeminiSettingsPath,
3935
+ mergeHooksSettings2 as mergeHooksSettings,
3936
+ readJsonFile2 as readJsonFile,
3937
+ writeJsonFile2 as writeJsonFile,
3938
+ loadAntigravityConfig,
3939
+ writeAntigravityConfig,
3940
+ getGeminiConfigHooksPath,
3941
+ mergeHooksFile,
3942
+ readJsonFile3 as readJsonFile2,
3943
+ writeJsonFile3 as writeJsonFile2,
3944
+ getToolScope,
3945
+ getToolSource,
3946
+ readRegistry,
3947
+ upsertEntry,
3948
+ removeEntry,
3949
+ reconcileCurrentWorkspace,
3950
+ formatRegistryTable,
3951
+ runMonitorInstall
3952
+ };
3953
+ //# sourceMappingURL=chunk-E33XD5CO.js.map