olakai-cli 0.2.1 → 0.4.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.
package/dist/index.js CHANGED
@@ -1,4 +1,33 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ CLAUDE_DIR,
4
+ CLIENT_ID,
5
+ OLAKAI_DIR,
6
+ OLAKAI_HOOK_MARKER,
7
+ SETTINGS_FILE,
8
+ clearToken,
9
+ deleteClaudeCodeConfig,
10
+ findConfiguredWorkspace,
11
+ getBaseUrl,
12
+ getClaudeCodeConfigPath,
13
+ getClaudeCodeStatus,
14
+ getClaudeDir,
15
+ getEnvironment,
16
+ getLegacyClaudeMonitorConfigPath,
17
+ getMonitorConfigPath,
18
+ getSettingsPath,
19
+ getValidEnvironments,
20
+ getValidToken,
21
+ isTokenValid,
22
+ isValidEnvironment,
23
+ loadClaudeCodeConfig,
24
+ mergeHooksSettings,
25
+ readJsonFile,
26
+ saveToken,
27
+ setEnvironment,
28
+ writeClaudeCodeConfig,
29
+ writeJsonFile
30
+ } from "./chunk-HI5R5CP2.js";
2
31
 
3
32
  // src/index.ts
4
33
  import { createRequire } from "module";
@@ -7,102 +36,6 @@ import { Command } from "commander";
7
36
  // src/commands/login.ts
8
37
  import open from "open";
9
38
 
10
- // src/lib/config.ts
11
- var HOSTS = {
12
- production: "https://app.olakai.ai",
13
- staging: "https://staging.app.olakai.ai",
14
- local: "http://localhost:3000"
15
- };
16
- var CLIENT_ID = "olakai-cli";
17
- var currentEnvironment = "production";
18
- function setEnvironment(env) {
19
- currentEnvironment = env;
20
- }
21
- function getEnvironment() {
22
- const envVar = process.env.OLAKAI_ENV;
23
- if (envVar && isValidEnvironment(envVar)) {
24
- return envVar;
25
- }
26
- return currentEnvironment;
27
- }
28
- function getBaseUrl() {
29
- return HOSTS[getEnvironment()];
30
- }
31
- function isValidEnvironment(env) {
32
- return env === "production" || env === "staging" || env === "local";
33
- }
34
- function getValidEnvironments() {
35
- return ["production", "staging", "local"];
36
- }
37
-
38
- // src/lib/auth.ts
39
- import * as fs from "fs";
40
- import * as path from "path";
41
- import * as os from "os";
42
- function getCredentialsPath() {
43
- const configDir = path.join(os.homedir(), ".config", "olakai");
44
- return path.join(configDir, "credentials.json");
45
- }
46
- function ensureConfigDir() {
47
- const configDir = path.dirname(getCredentialsPath());
48
- if (!fs.existsSync(configDir)) {
49
- fs.mkdirSync(configDir, { recursive: true, mode: 448 });
50
- }
51
- }
52
- function saveToken(token, expiresIn) {
53
- ensureConfigDir();
54
- const credentials = {
55
- token,
56
- expiresAt: Math.floor(Date.now() / 1e3) + expiresIn,
57
- environment: getEnvironment()
58
- };
59
- const credentialsPath = getCredentialsPath();
60
- fs.writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2), {
61
- mode: 384
62
- // Read/write for owner only
63
- });
64
- }
65
- function loadToken() {
66
- const credentialsPath = getCredentialsPath();
67
- if (!fs.existsSync(credentialsPath)) {
68
- return null;
69
- }
70
- try {
71
- const content = fs.readFileSync(credentialsPath, "utf-8");
72
- const credentials = JSON.parse(content);
73
- return credentials;
74
- } catch {
75
- return null;
76
- }
77
- }
78
- function clearToken() {
79
- const credentialsPath = getCredentialsPath();
80
- if (fs.existsSync(credentialsPath)) {
81
- fs.unlinkSync(credentialsPath);
82
- }
83
- }
84
- function isTokenValid() {
85
- const credentials = loadToken();
86
- if (!credentials) {
87
- return false;
88
- }
89
- const now = Math.floor(Date.now() / 1e3);
90
- if (credentials.expiresAt <= now + 60) {
91
- return false;
92
- }
93
- if (credentials.environment !== getEnvironment()) {
94
- return false;
95
- }
96
- return true;
97
- }
98
- function getValidToken() {
99
- if (!isTokenValid()) {
100
- return null;
101
- }
102
- const credentials = loadToken();
103
- return credentials?.token ?? null;
104
- }
105
-
106
39
  // src/lib/api.ts
107
40
  async function requestDeviceCode() {
108
41
  const response = await fetch(`${getBaseUrl()}/api/auth/device/code`, {
@@ -584,6 +517,33 @@ async function validateKpiFormula(formula, agentId, scope) {
584
517
  }
585
518
  return await response.json();
586
519
  }
520
+ async function validateMonitoringApiKey(monitoringEndpoint, apiKey) {
521
+ const normalizedEndpoint = monitoringEndpoint.replace(/\/+$/, "");
522
+ const url = `${normalizedEndpoint}/me`;
523
+ const controller = new AbortController();
524
+ const timeout = setTimeout(() => controller.abort(), 5e3);
525
+ try {
526
+ const response = await fetch(url, {
527
+ method: "GET",
528
+ headers: {
529
+ "x-api-key": apiKey
530
+ },
531
+ signal: controller.signal
532
+ });
533
+ if (!response.ok) {
534
+ return null;
535
+ }
536
+ const data = await response.json();
537
+ if (typeof data?.accountId !== "string" || typeof data?.apiKeyId !== "string") {
538
+ return null;
539
+ }
540
+ return { accountId: data.accountId, apiKeyId: data.apiKeyId };
541
+ } catch {
542
+ return null;
543
+ } finally {
544
+ clearTimeout(timeout);
545
+ }
546
+ }
587
547
  async function listActivity(options = {}) {
588
548
  const token = getValidToken();
589
549
  if (!token) {
@@ -1928,15 +1888,15 @@ function formatActivityTable(data) {
1928
1888
  return;
1929
1889
  }
1930
1890
  const headers = ["ID", "AGENT", "APP", "MODEL", "TOKENS", "TIME(ms)", "RISK", "STATUS"];
1931
- const rows = data.prompts.map((prompt2) => [
1932
- prompt2.id.slice(0, 12) + "...",
1933
- prompt2.agentName || (prompt2.agentId ? prompt2.agentId.slice(0, 12) + "..." : "-"),
1934
- prompt2.app.slice(0, 12),
1935
- prompt2.modelId?.slice(0, 15) || "-",
1936
- String(prompt2.tokens),
1937
- String(prompt2.requestTime),
1938
- prompt2.isHighRisk ? "Yes" : "No",
1939
- prompt2.decorationStatus.slice(0, 10)
1891
+ const rows = data.prompts.map((prompt) => [
1892
+ prompt.id.slice(0, 12) + "...",
1893
+ prompt.agentName || (prompt.agentId ? prompt.agentId.slice(0, 12) + "..." : "-"),
1894
+ prompt.app.slice(0, 12),
1895
+ prompt.modelId?.slice(0, 15) || "-",
1896
+ String(prompt.tokens),
1897
+ String(prompt.requestTime),
1898
+ prompt.isHighRisk ? "Yes" : "No",
1899
+ prompt.decorationStatus.slice(0, 10)
1940
1900
  ]);
1941
1901
  const widths = headers.map(
1942
1902
  (h, i) => Math.max(h.length, ...rows.map((r) => r[i].length))
@@ -1952,51 +1912,51 @@ function formatActivityTable(data) {
1952
1912
  console.log(`Use --offset ${data.offset + data.limit} to see more results`);
1953
1913
  }
1954
1914
  }
1955
- function formatActivityDetail(prompt2) {
1956
- console.log(`ID: ${prompt2.id}`);
1957
- console.log(`Created: ${prompt2.createdAt}`);
1958
- console.log(`Agent: ${prompt2.agentName || "-"} (${prompt2.agentId || "-"})`);
1959
- console.log(`Workflow: ${prompt2.workflowName || "-"} (${prompt2.workflowId || "-"})`);
1960
- if (prompt2.taskExecutionId) {
1961
- console.log(`Task Exec ID: ${prompt2.taskExecutionId}`);
1962
- }
1963
- console.log(`App: ${prompt2.app}`);
1964
- console.log(`Model: ${prompt2.modelId || "-"} (${prompt2.modelType || "-"})`);
1965
- console.log(`Tokens: ${prompt2.tokens}`);
1966
- console.log(`Request Time: ${prompt2.requestTime}ms`);
1967
- console.log(`High Risk: ${prompt2.isHighRisk ? "Yes" : "No"}`);
1968
- console.log(`Blocked: ${prompt2.blocked ? "Yes" : "No"}`);
1969
- console.log(`Status: ${prompt2.decorationStatus}`);
1970
- if (prompt2.sensitivity && prompt2.sensitivity.length > 0) {
1971
- console.log(`Sensitivity: ${prompt2.sensitivity.join(", ")}`);
1972
- }
1973
- if (prompt2.analytics) {
1915
+ function formatActivityDetail(prompt) {
1916
+ console.log(`ID: ${prompt.id}`);
1917
+ console.log(`Created: ${prompt.createdAt}`);
1918
+ console.log(`Agent: ${prompt.agentName || "-"} (${prompt.agentId || "-"})`);
1919
+ console.log(`Workflow: ${prompt.workflowName || "-"} (${prompt.workflowId || "-"})`);
1920
+ if (prompt.taskExecutionId) {
1921
+ console.log(`Task Exec ID: ${prompt.taskExecutionId}`);
1922
+ }
1923
+ console.log(`App: ${prompt.app}`);
1924
+ console.log(`Model: ${prompt.modelId || "-"} (${prompt.modelType || "-"})`);
1925
+ console.log(`Tokens: ${prompt.tokens}`);
1926
+ console.log(`Request Time: ${prompt.requestTime}ms`);
1927
+ console.log(`High Risk: ${prompt.isHighRisk ? "Yes" : "No"}`);
1928
+ console.log(`Blocked: ${prompt.blocked ? "Yes" : "No"}`);
1929
+ console.log(`Status: ${prompt.decorationStatus}`);
1930
+ if (prompt.sensitivity && prompt.sensitivity.length > 0) {
1931
+ console.log(`Sensitivity: ${prompt.sensitivity.join(", ")}`);
1932
+ }
1933
+ if (prompt.analytics) {
1974
1934
  console.log("");
1975
1935
  console.log("Analytics:");
1976
- console.log(` Task: ${prompt2.analytics.task || "-"}`);
1977
- console.log(` Subtask: ${prompt2.analytics.subtask || "-"}`);
1978
- console.log(` Time Saved: ${prompt2.analytics.timesaved_minutes ?? "-"} minutes`);
1979
- console.log(` Risk Score: ${prompt2.analytics.riskassessment_dangerousity ?? "-"}`);
1936
+ console.log(` Task: ${prompt.analytics.task || "-"}`);
1937
+ console.log(` Subtask: ${prompt.analytics.subtask || "-"}`);
1938
+ console.log(` Time Saved: ${prompt.analytics.timesaved_minutes ?? "-"} minutes`);
1939
+ console.log(` Risk Score: ${prompt.analytics.riskassessment_dangerousity ?? "-"}`);
1980
1940
  }
1981
- if (prompt2.kpiData && Object.keys(prompt2.kpiData).length > 0) {
1941
+ if (prompt.kpiData && Object.keys(prompt.kpiData).length > 0) {
1982
1942
  console.log("");
1983
1943
  console.log("KPIs:");
1984
- for (const [key, value] of Object.entries(prompt2.kpiData)) {
1944
+ for (const [key, value] of Object.entries(prompt.kpiData)) {
1985
1945
  console.log(` ${key}: ${value}`);
1986
1946
  }
1987
1947
  }
1988
- if (prompt2.prompt !== void 0) {
1948
+ if (prompt.prompt !== void 0) {
1989
1949
  console.log("");
1990
1950
  console.log("Prompt:");
1991
1951
  console.log("---");
1992
- console.log(prompt2.prompt.slice(0, 1e3) + (prompt2.prompt.length > 1e3 ? "..." : ""));
1952
+ console.log(prompt.prompt.slice(0, 1e3) + (prompt.prompt.length > 1e3 ? "..." : ""));
1993
1953
  console.log("---");
1994
1954
  }
1995
- if (prompt2.response !== void 0) {
1955
+ if (prompt.response !== void 0) {
1996
1956
  console.log("");
1997
1957
  console.log("Response:");
1998
1958
  console.log("---");
1999
- console.log(prompt2.response.slice(0, 1e3) + (prompt2.response.length > 1e3 ? "..." : ""));
1959
+ console.log(prompt.response.slice(0, 1e3) + (prompt.response.length > 1e3 ? "..." : ""));
2000
1960
  console.log("---");
2001
1961
  }
2002
1962
  }
@@ -2072,11 +2032,11 @@ async function listCommand5(options) {
2072
2032
  }
2073
2033
  async function getCommand5(id, options) {
2074
2034
  try {
2075
- const prompt2 = await getActivity(id, options.includeContent);
2035
+ const prompt = await getActivity(id, options.includeContent);
2076
2036
  if (options.json) {
2077
- console.log(JSON.stringify(prompt2, null, 2));
2037
+ console.log(JSON.stringify(prompt, null, 2));
2078
2038
  } else {
2079
- formatActivityDetail(prompt2);
2039
+ formatActivityDetail(prompt);
2080
2040
  }
2081
2041
  } catch (error) {
2082
2042
  console.error(
@@ -2183,138 +2143,52 @@ function registerActivityCommand(program2) {
2183
2143
  }
2184
2144
 
2185
2145
  // src/commands/monitor.ts
2186
- import * as fs3 from "fs";
2187
- import * as path3 from "path";
2188
- import * as readline from "readline";
2146
+ import * as fs15 from "fs";
2189
2147
 
2190
- // src/commands/monitor-transcript.ts
2191
- var SKILL_REGEX = /^\/([\w-]+)(?:\s|$)/;
2192
- function detectSkill(userMessage) {
2193
- if (typeof userMessage !== "string") return void 0;
2194
- const trimmed = userMessage.trimStart();
2195
- if (!trimmed) return void 0;
2196
- const match = SKILL_REGEX.exec(trimmed);
2197
- if (!match) return void 0;
2198
- return match[1];
2199
- }
2200
- function extractTextContent(content) {
2201
- if (typeof content === "string") return content;
2202
- if (!Array.isArray(content)) return "";
2203
- const parts = [];
2204
- for (const block of content) {
2205
- if (block?.type === "text" && typeof block.text === "string") {
2206
- parts.push(block.text);
2207
- }
2208
- }
2209
- return parts.join("\n").trim();
2210
- }
2211
- function isMetaUserMessage(line, text) {
2212
- if (line.isMeta === true) return true;
2213
- if (!text) return true;
2214
- return text.includes("<command-name>") || text.includes("<local-command-caveat>") || text.includes("<command-message>");
2215
- }
2216
- function parseTimestamp(ts) {
2217
- if (typeof ts !== "string" || !ts) return NaN;
2218
- return Date.parse(ts);
2219
- }
2220
- function isCompactionEntry(line) {
2221
- if (line.type !== "system") return false;
2222
- const subtype = line.subtype ?? "";
2223
- return subtype === "compact_boundary" || subtype === "pre_compact" || subtype === "post_compact" || /compact/i.test(subtype);
2224
- }
2225
- function parseTranscript(raw) {
2226
- const empty = {
2227
- prompt: "",
2228
- response: "",
2229
- tokens: 0,
2230
- inputTokens: 0,
2231
- outputTokens: 0,
2232
- modelName: null,
2233
- numTurns: 0
2234
- };
2235
- if (!raw) return empty;
2236
- const lines = raw.split("\n");
2237
- let lastUserText = "";
2238
- let lastUserTimestamp = NaN;
2239
- let lastUserTimestampRaw;
2240
- let lastAssistantText = "";
2241
- let lastAssistantTimestamp = NaN;
2242
- let lastAssistantModel = null;
2243
- let lastAssistantInputTokens = 0;
2244
- let lastAssistantOutputTokens = 0;
2245
- let numTurns = 0;
2246
- let currentTurnUserTimestamp = NaN;
2247
- for (const rawLine of lines) {
2248
- if (!rawLine) continue;
2249
- let parsed;
2250
- try {
2251
- parsed = JSON.parse(rawLine);
2252
- } catch {
2253
- continue;
2254
- }
2255
- if (isCompactionEntry(parsed)) continue;
2256
- if (parsed.isSidechain === true) continue;
2257
- if (parsed.type === "user" && parsed.message) {
2258
- const text = extractTextContent(parsed.message.content);
2259
- if (isMetaUserMessage(parsed, text)) continue;
2260
- lastUserText = text;
2261
- lastUserTimestamp = parseTimestamp(parsed.timestamp);
2262
- currentTurnUserTimestamp = lastUserTimestamp;
2263
- lastUserTimestampRaw = typeof parsed.timestamp === "string" && parsed.timestamp ? parsed.timestamp : void 0;
2264
- } else if (parsed.type === "assistant" && parsed.message) {
2265
- const text = extractTextContent(parsed.message.content);
2266
- numTurns += 1;
2267
- if (text) lastAssistantText = text;
2268
- if (typeof parsed.message.model === "string") {
2269
- lastAssistantModel = parsed.message.model;
2270
- }
2271
- const usage = parsed.message.usage;
2272
- if (usage) {
2273
- const input = (usage.input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0);
2274
- const output = usage.output_tokens ?? 0;
2275
- lastAssistantInputTokens = input;
2276
- lastAssistantOutputTokens = output;
2277
- }
2278
- const ts = parseTimestamp(parsed.timestamp);
2279
- if (!Number.isNaN(ts)) {
2280
- lastAssistantTimestamp = ts;
2281
- }
2282
- }
2283
- }
2284
- const result = {
2285
- prompt: lastUserText,
2286
- response: lastAssistantText,
2287
- tokens: lastAssistantInputTokens + lastAssistantOutputTokens,
2288
- inputTokens: lastAssistantInputTokens,
2289
- outputTokens: lastAssistantOutputTokens,
2290
- modelName: lastAssistantModel,
2291
- numTurns
2292
- };
2293
- if (!Number.isNaN(currentTurnUserTimestamp) && !Number.isNaN(lastAssistantTimestamp) && lastAssistantTimestamp >= currentTurnUserTimestamp) {
2294
- result.latencyMs = Math.round(
2295
- lastAssistantTimestamp - currentTurnUserTimestamp
2148
+ // src/monitor/plugins/claude-code/index.ts
2149
+ import * as fs4 from "fs";
2150
+
2151
+ // src/monitor/plugin.ts
2152
+ var TOOL_IDS = [
2153
+ "claude-code",
2154
+ "codex",
2155
+ "cursor"
2156
+ ];
2157
+ var registry = /* @__PURE__ */ new Map();
2158
+ function registerPlugin(plugin) {
2159
+ registry.set(plugin.id, plugin);
2160
+ }
2161
+ function getPlugin(id) {
2162
+ if (!isToolId(id)) {
2163
+ throw new Error(
2164
+ `Unknown tool: "${id}". Supported tools: ${TOOL_IDS.join(", ")}`
2296
2165
  );
2297
2166
  }
2298
- const skill = detectSkill(lastUserText);
2299
- if (skill) {
2300
- result.skill = skill;
2301
- }
2302
- if (lastUserTimestampRaw) {
2303
- result.userTurnTimestamp = lastUserTimestampRaw;
2167
+ const plugin = registry.get(id);
2168
+ if (!plugin) {
2169
+ throw new Error(
2170
+ `Tool "${id}" is not registered. This is a CLI bug \u2014 please report it.`
2171
+ );
2304
2172
  }
2305
- return result;
2173
+ return plugin;
2174
+ }
2175
+ function listPlugins() {
2176
+ return Array.from(registry.values());
2177
+ }
2178
+ function isToolId(value) {
2179
+ return TOOL_IDS.includes(value);
2306
2180
  }
2307
2181
 
2308
2182
  // src/commands/monitor-state.ts
2309
- import * as fs2 from "fs";
2310
- import * as os2 from "os";
2311
- import * as path2 from "path";
2183
+ import * as fs from "fs";
2184
+ import * as os from "os";
2185
+ import * as path from "path";
2312
2186
  var STATE_DIR_SEGMENTS = [".olakai", "monitor-state"];
2313
2187
  function getStateDir(homeDir) {
2314
- return path2.join(homeDir, ...STATE_DIR_SEGMENTS);
2188
+ return path.join(homeDir, ...STATE_DIR_SEGMENTS);
2315
2189
  }
2316
2190
  function getStateFile(sessionId, homeDir) {
2317
- return path2.join(getStateDir(homeDir), `${sessionId}.json`);
2191
+ return path.join(getStateDir(homeDir), `${sessionId}.json`);
2318
2192
  }
2319
2193
  var debugLogger = null;
2320
2194
  function setDebugLogger(logger) {
@@ -2328,12 +2202,12 @@ function log(label, data) {
2328
2202
  }
2329
2203
  }
2330
2204
  }
2331
- function loadSessionState(sessionId, homeDir = os2.homedir()) {
2205
+ function loadSessionState(sessionId, homeDir = os.homedir()) {
2332
2206
  if (!sessionId) return null;
2333
2207
  const filePath = getStateFile(sessionId, homeDir);
2334
2208
  try {
2335
- if (!fs2.existsSync(filePath)) return null;
2336
- const raw = fs2.readFileSync(filePath, "utf-8");
2209
+ if (!fs.existsSync(filePath)) return null;
2210
+ const raw = fs.readFileSync(filePath, "utf-8");
2337
2211
  const parsed = JSON.parse(raw);
2338
2212
  if (typeof parsed?.lastUserTimestamp !== "string" || typeof parsed?.lastReportedAt !== "string" || typeof parsed?.numTurnsAtLastReport !== "number") {
2339
2213
  return null;
@@ -2351,15 +2225,15 @@ function loadSessionState(sessionId, homeDir = os2.homedir()) {
2351
2225
  return null;
2352
2226
  }
2353
2227
  }
2354
- function saveSessionState(sessionId, state, homeDir = os2.homedir()) {
2228
+ function saveSessionState(sessionId, state, homeDir = os.homedir()) {
2355
2229
  if (!sessionId) return;
2356
2230
  const dir = getStateDir(homeDir);
2357
2231
  const filePath = getStateFile(sessionId, homeDir);
2358
2232
  try {
2359
- if (!fs2.existsSync(dir)) {
2360
- fs2.mkdirSync(dir, { recursive: true });
2233
+ if (!fs.existsSync(dir)) {
2234
+ fs.mkdirSync(dir, { recursive: true });
2361
2235
  }
2362
- fs2.writeFileSync(filePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
2236
+ fs.writeFileSync(filePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
2363
2237
  } catch (err) {
2364
2238
  log("state-save-failed", {
2365
2239
  sessionId,
@@ -2373,53 +2247,12 @@ function shouldReportTurn(existing, currentUserTimestamp) {
2373
2247
  return existing.lastUserTimestamp !== currentUserTimestamp;
2374
2248
  }
2375
2249
 
2376
- // src/commands/monitor.ts
2377
- var CLAUDE_DIR = ".claude";
2378
- var SETTINGS_FILE = "settings.json";
2379
- var MONITOR_CONFIG_FILE = "olakai-monitor.json";
2380
- var OLAKAI_HOOK_MARKER = "olakai monitor hook";
2381
- var HOOK_DEFINITIONS = {
2382
- Stop: [
2383
- {
2384
- matcher: "",
2385
- hooks: [
2386
- {
2387
- type: "command",
2388
- command: "olakai monitor hook stop"
2389
- }
2390
- ]
2391
- }
2392
- ],
2393
- SubagentStop: [
2394
- {
2395
- matcher: "",
2396
- hooks: [
2397
- {
2398
- type: "command",
2399
- command: "olakai monitor hook subagent-stop"
2400
- }
2401
- ]
2402
- }
2403
- ]
2404
- };
2405
- function mergeHooksSettings(existing, definitions = HOOK_DEFINITIONS) {
2406
- const merged = {
2407
- ...existing ?? {}
2408
- };
2409
- for (const [event, defaultEntries] of Object.entries(definitions)) {
2410
- const existingEntries = merged[event] ?? [];
2411
- const hasOlakaiHook = existingEntries.some(
2412
- (e) => e.hooks.some((h) => h.command.includes(OLAKAI_HOOK_MARKER))
2413
- );
2414
- if (hasOlakaiHook) {
2415
- merged[event] = existingEntries;
2416
- } else {
2417
- merged[event] = [...existingEntries, ...defaultEntries];
2418
- }
2419
- }
2420
- return merged;
2421
- }
2422
- function prompt(question) {
2250
+ // src/monitor/plugins/claude-code/install.ts
2251
+ import * as fs2 from "fs";
2252
+
2253
+ // src/monitor/prompt.ts
2254
+ import * as readline from "readline";
2255
+ function promptUser(question) {
2423
2256
  const rl = readline.createInterface({
2424
2257
  input: process.stdin,
2425
2258
  output: process.stdout
@@ -2431,100 +2264,62 @@ function prompt(question) {
2431
2264
  });
2432
2265
  });
2433
2266
  }
2434
- function findProjectRoot(startDir) {
2435
- let dir = startDir ?? process.cwd();
2436
- while (true) {
2437
- if (fs3.existsSync(path3.join(dir, CLAUDE_DIR))) {
2438
- return dir;
2439
- }
2440
- const parent = path3.dirname(dir);
2441
- if (parent === dir) break;
2442
- dir = parent;
2443
- }
2444
- return startDir ?? process.cwd();
2267
+ function isInteractive() {
2268
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
2445
2269
  }
2446
- function findConfiguredProjectRoot(startDir) {
2447
- let dir = startDir ?? process.cwd();
2448
- while (true) {
2449
- if (fs3.existsSync(path3.join(dir, CLAUDE_DIR, MONITOR_CONFIG_FILE))) {
2450
- return dir;
2451
- }
2452
- const parent = path3.dirname(dir);
2453
- if (parent === dir) break;
2454
- dir = parent;
2270
+
2271
+ // src/monitor/validate-pasted-key.ts
2272
+ async function validatePastedApiKey(opts) {
2273
+ const validation = await validateMonitoringApiKey(
2274
+ opts.monitoringEndpoint,
2275
+ opts.apiKey
2276
+ );
2277
+ if (validation === null) {
2278
+ console.log(
2279
+ "\u26A0 Could not verify the pasted API key (network or endpoint unreachable). Continuing."
2280
+ );
2281
+ return;
2455
2282
  }
2456
- return null;
2457
- }
2458
- function resolveProjectRootFromPayload(eventData, fallbackCwd) {
2459
- const payloadCwd = typeof eventData.cwd === "string" && eventData.cwd.trim() ? eventData.cwd : fallbackCwd;
2460
- return findConfiguredProjectRoot(payloadCwd);
2461
- }
2462
- function getClaudeDir(projectRoot) {
2463
- return path3.join(projectRoot, CLAUDE_DIR);
2464
- }
2465
- function getSettingsPath(projectRoot) {
2466
- return path3.join(getClaudeDir(projectRoot), SETTINGS_FILE);
2467
- }
2468
- function getMonitorConfigPath(projectRoot) {
2469
- return path3.join(getClaudeDir(projectRoot), MONITOR_CONFIG_FILE);
2470
- }
2471
- function readJsonFile(filePath) {
2472
- try {
2473
- if (!fs3.existsSync(filePath)) return null;
2474
- const content = fs3.readFileSync(filePath, "utf-8");
2475
- return JSON.parse(content);
2476
- } catch {
2477
- return null;
2283
+ if (opts.expectedApiKeyId === null) {
2284
+ return;
2478
2285
  }
2479
- }
2480
- function writeJsonFile(filePath, data) {
2481
- const dir = path3.dirname(filePath);
2482
- if (!fs3.existsSync(dir)) {
2483
- fs3.mkdirSync(dir, { recursive: true });
2286
+ if (validation.apiKeyId === opts.expectedApiKeyId) {
2287
+ return;
2288
+ }
2289
+ console.log("");
2290
+ console.log(
2291
+ `\u26A0 The pasted API key does not belong to "${opts.expectedAgentName}" (${opts.expectedAgentId}).`
2292
+ );
2293
+ console.log(
2294
+ ` It resolves to a different agent on this account, so monitoring events would be`
2295
+ );
2296
+ console.log(
2297
+ ` attributed to that other agent \u2014 not "${opts.expectedAgentName}".`
2298
+ );
2299
+ console.log("");
2300
+ const proceed = await promptUser("Use the pasted key anyway? (y/n) [n]: ");
2301
+ if (proceed.trim().toLowerCase() !== "y") {
2302
+ console.log("Aborted. Re-run init with the correct key for this agent.");
2303
+ process.exit(1);
2484
2304
  }
2485
- fs3.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
2486
- }
2487
- function loadMonitorConfig(projectRoot) {
2488
- const root = projectRoot ?? findProjectRoot();
2489
- return readJsonFile(getMonitorConfigPath(root));
2490
- }
2491
- function readStdin(timeoutMs = 3e3) {
2492
- return new Promise((resolve) => {
2493
- if (process.stdin.isTTY) {
2494
- resolve("");
2495
- return;
2496
- }
2497
- let data = "";
2498
- const timer = setTimeout(() => {
2499
- process.stdin.removeAllListeners();
2500
- process.stdin.destroy();
2501
- resolve(data);
2502
- }, timeoutMs);
2503
- process.stdin.setEncoding("utf-8");
2504
- process.stdin.on("data", (chunk) => {
2505
- data += chunk;
2506
- });
2507
- process.stdin.on("end", () => {
2508
- clearTimeout(timer);
2509
- resolve(data);
2510
- });
2511
- process.stdin.on("error", () => {
2512
- clearTimeout(timer);
2513
- resolve(data);
2514
- });
2515
- });
2516
2305
  }
2517
- async function initCommand() {
2306
+
2307
+ // src/monitor/plugins/claude-code/install.ts
2308
+ import path2 from "path";
2309
+ var CLAUDE_CODE_SOURCE = "claude-code";
2310
+ var CLAUDE_CODE_AGENT_SOURCE = "CLAUDE_CODE";
2311
+ var CLAUDE_CODE_AGENT_CATEGORY = "CODING";
2312
+ async function installClaudeCode(opts) {
2313
+ const projectRoot = opts.projectRoot ?? process.cwd();
2518
2314
  const token = getValidToken();
2519
2315
  if (!token) {
2520
2316
  console.error("Not logged in. Run 'olakai login' first.");
2521
2317
  process.exit(1);
2522
2318
  }
2523
2319
  console.log("Setting up Claude Code monitoring for this workspace...\n");
2524
- const projectRoot = process.cwd();
2525
- const dirName = path3.basename(projectRoot);
2320
+ const dirName = path2.basename(projectRoot);
2526
2321
  let agent;
2527
- const choice = await prompt(
2322
+ const choice = await promptUser(
2528
2323
  "Create a new agent or use an existing one? (new/existing) [new]: "
2529
2324
  );
2530
2325
  if (choice.toLowerCase() === "existing" || choice.toLowerCase() === "e") {
@@ -2535,9 +2330,11 @@ async function initCommand() {
2535
2330
  } else {
2536
2331
  console.log("\nAvailable agents:");
2537
2332
  for (let i = 0; i < agents.length; i++) {
2538
- console.log(` ${i + 1}. ${agents[i].name} (${agents[i].id.slice(0, 12)}...)`);
2333
+ console.log(
2334
+ ` ${i + 1}. ${agents[i].name} (${agents[i].id.slice(0, 12)}...)`
2335
+ );
2539
2336
  }
2540
- const selection = await prompt(`
2337
+ const selection = await promptUser(`
2541
2338
  Select agent (1-${agents.length}): `);
2542
2339
  const idx = parseInt(selection, 10) - 1;
2543
2340
  if (isNaN(idx) || idx < 0 || idx >= agents.length) {
@@ -2550,7 +2347,7 @@ Select agent (1-${agents.length}): `);
2550
2347
  "\nThis agent has no active API key. Please create a new agent instead,"
2551
2348
  );
2552
2349
  console.log("or generate an API key via the Olakai dashboard.\n");
2553
- const createNew = await prompt("Create a new agent? (y/n) [y]: ");
2350
+ const createNew = await promptUser("Create a new agent? (y/n) [y]: ");
2554
2351
  if (createNew.toLowerCase() !== "n") {
2555
2352
  agent = await createNewAgent(dirName);
2556
2353
  } else {
@@ -2561,20 +2358,28 @@ Select agent (1-${agents.length}): `);
2561
2358
  } else {
2562
2359
  agent = await createNewAgent(dirName);
2563
2360
  }
2361
+ const monitoringEndpoint = `${getBaseUrl()}/api/monitoring/prompt`;
2564
2362
  let apiKey = agent.apiKey?.key;
2565
2363
  if (!apiKey) {
2566
2364
  console.log(
2567
2365
  "\nThe API key for this agent is not available (it is only shown once at creation)."
2568
2366
  );
2569
- apiKey = await prompt("Paste the API key for this agent: ");
2367
+ apiKey = await promptUser("Paste the API key for this agent: ");
2570
2368
  if (!apiKey) {
2571
2369
  console.error("API key is required for monitoring.");
2572
2370
  process.exit(1);
2573
2371
  }
2372
+ await validatePastedApiKey({
2373
+ monitoringEndpoint,
2374
+ apiKey,
2375
+ expectedAgentId: agent.id,
2376
+ expectedAgentName: agent.name,
2377
+ expectedApiKeyId: agent.apiKey?.id ?? null
2378
+ });
2574
2379
  }
2575
2380
  const claudeDir = getClaudeDir(projectRoot);
2576
- if (!fs3.existsSync(claudeDir)) {
2577
- fs3.mkdirSync(claudeDir, { recursive: true });
2381
+ if (!fs2.existsSync(claudeDir)) {
2382
+ fs2.mkdirSync(claudeDir, { recursive: true });
2578
2383
  }
2579
2384
  const settingsPath = getSettingsPath(projectRoot);
2580
2385
  const existingSettings = readJsonFile(settingsPath) ?? {};
@@ -2584,66 +2389,145 @@ Select agent (1-${agents.length}): `);
2584
2389
  hooks: mergedHooks
2585
2390
  };
2586
2391
  writeJsonFile(settingsPath, updatedSettings);
2587
- const monitoringEndpoint = `${getBaseUrl()}/api/monitoring/prompt`;
2588
2392
  const monitorConfig = {
2589
2393
  agentId: agent.id,
2590
2394
  apiKey,
2591
2395
  agentName: agent.name,
2592
- source: "claude-code",
2396
+ source: CLAUDE_CODE_SOURCE,
2593
2397
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2594
2398
  monitoringEndpoint
2595
2399
  };
2596
- const monitorConfigPath = getMonitorConfigPath(projectRoot);
2597
- writeJsonFile(monitorConfigPath, monitorConfig);
2598
- fs3.chmodSync(monitorConfigPath, 384);
2400
+ writeClaudeCodeConfig(projectRoot, monitorConfig);
2401
+ const configPath = getClaudeCodeConfigPath(projectRoot);
2402
+ const configRel = path2.relative(projectRoot, configPath);
2599
2403
  console.log("");
2600
2404
  console.log(`\u2713 Agent "${agent.name}" configured (ID: ${agent.id})`);
2601
2405
  if (agent.apiKey?.key) {
2602
2406
  console.log("\u2713 API key generated");
2603
2407
  }
2604
- console.log(`\u2713 Claude Code hooks configured in ${CLAUDE_DIR}/${SETTINGS_FILE}`);
2605
- console.log(`\u2713 Monitor config saved to ${CLAUDE_DIR}/${MONITOR_CONFIG_FILE}`);
2606
- console.log("");
2607
2408
  console.log(
2608
- "Monitoring is now active. Claude Code will report activity to Olakai"
2409
+ `\u2713 Claude Code hooks configured in ${CLAUDE_DIR}/${SETTINGS_FILE}`
2609
2410
  );
2411
+ console.log(`\u2713 Monitor config saved to ${configRel}`);
2412
+ console.log("");
2610
2413
  console.log(
2611
- `on each turn. View activity at: ${getBaseUrl()}/dashboard`
2414
+ "Monitoring is now active. Claude Code will report activity to Olakai"
2612
2415
  );
2416
+ console.log(`on each turn. View activity at: ${getBaseUrl()}/dashboard`);
2613
2417
  console.log("");
2614
2418
  console.log(
2615
- `\u26A0 Ensure ${CLAUDE_DIR}/${MONITOR_CONFIG_FILE} is in your .gitignore (it contains your API key)`
2419
+ `\u26A0 Ensure ${OLAKAI_DIR}/ is in your .gitignore (it contains your API key)`
2616
2420
  );
2617
2421
  console.log("");
2618
- console.log("To check status: olakai monitor status");
2619
- console.log("To disable: olakai monitor disable");
2422
+ console.log("To check status: olakai monitor status --tool claude-code");
2423
+ console.log("To disable: olakai monitor disable --tool claude-code");
2424
+ return {
2425
+ agentId: agent.id,
2426
+ agentName: agent.name,
2427
+ source: CLAUDE_CODE_SOURCE,
2428
+ monitoringEndpoint
2429
+ };
2620
2430
  }
2621
2431
  async function createNewAgent(defaultName) {
2622
- const nameInput = await prompt(
2623
- `Agent name [${defaultName}]: `
2624
- );
2432
+ const nameInput = await promptUser(`Agent name [${defaultName}]: `);
2625
2433
  const agentName = nameInput || defaultName;
2626
- const agent = await createAgent({
2434
+ return createAgent({
2627
2435
  name: agentName,
2628
2436
  description: `Claude Code local agent for ${agentName}`,
2629
2437
  role: "WORKER",
2630
2438
  createApiKey: true,
2631
- category: "CODING",
2632
- source: "CLAUDE_CODE"
2439
+ category: CLAUDE_CODE_AGENT_CATEGORY,
2440
+ source: CLAUDE_CODE_AGENT_SOURCE
2633
2441
  });
2634
- return agent;
2635
2442
  }
2636
- function debugLog(label, data) {
2637
- if (process.env.OLAKAI_MONITOR_DEBUG !== "1") return;
2638
- try {
2639
- const logPath = `/tmp/olakai-monitor-debug-${process.pid}.log`;
2640
- const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${label}: ${typeof data === "string" ? data : JSON.stringify(data, null, 2)}
2641
- `;
2642
- fs3.appendFileSync(logPath, line, "utf-8");
2643
- } catch {
2443
+ async function uninstallClaudeCode(opts) {
2444
+ const projectRoot = opts.projectRoot ?? process.cwd();
2445
+ const settingsPath = getSettingsPath(projectRoot);
2446
+ const settings = readJsonFile(settingsPath);
2447
+ if (settings?.hooks) {
2448
+ const cleanedHooks = {};
2449
+ for (const [event, entries] of Object.entries(settings.hooks)) {
2450
+ const filtered = entries.filter(
2451
+ (e) => !e.hooks.some((h) => h.command.includes(OLAKAI_HOOK_MARKER))
2452
+ );
2453
+ if (filtered.length > 0) {
2454
+ cleanedHooks[event] = filtered;
2455
+ }
2456
+ }
2457
+ if (Object.keys(cleanedHooks).length > 0) {
2458
+ settings.hooks = cleanedHooks;
2459
+ } else {
2460
+ delete settings.hooks;
2461
+ }
2462
+ writeJsonFile(settingsPath, settings);
2463
+ console.log(
2464
+ `\u2713 Olakai hooks removed from ${CLAUDE_DIR}/${SETTINGS_FILE}`
2465
+ );
2466
+ } else {
2467
+ console.log("No hooks found in settings.json.");
2468
+ }
2469
+ if (!opts.keepConfig) {
2470
+ const configPath = getClaudeCodeConfigPath(projectRoot);
2471
+ const configRel = path2.relative(projectRoot, configPath);
2472
+ if (deleteClaudeCodeConfig(projectRoot)) {
2473
+ console.log(`\u2713 Monitor config removed (${configRel})`);
2474
+ }
2475
+ } else {
2476
+ const configPath = getClaudeCodeConfigPath(projectRoot);
2477
+ const configRel = path2.relative(projectRoot, configPath);
2478
+ console.log(`Monitor config retained at ${configRel}`);
2479
+ }
2480
+ console.log("");
2481
+ console.log(
2482
+ "Monitoring disabled. Run 'olakai monitor init --tool claude-code' to re-enable."
2483
+ );
2484
+ }
2485
+
2486
+ // src/monitor/plugins/claude-code/hook.ts
2487
+ import * as fs3 from "fs";
2488
+
2489
+ // src/commands/monitor-transcript.ts
2490
+ var FILE_EDITING_TOOL_NAMES = /* @__PURE__ */ new Set([
2491
+ "Edit",
2492
+ "Write",
2493
+ "MultiEdit"
2494
+ ]);
2495
+ var BASH_TOOL_NAME = "Bash";
2496
+ var SKILL_REGEX = /^\/([\w-]+)(?:\s|$)/;
2497
+ function detectSkill(userMessage) {
2498
+ if (typeof userMessage !== "string") return void 0;
2499
+ const trimmed = userMessage.trimStart();
2500
+ if (!trimmed) return void 0;
2501
+ const match = SKILL_REGEX.exec(trimmed);
2502
+ if (!match) return void 0;
2503
+ return match[1];
2504
+ }
2505
+ function extractTextContent(content) {
2506
+ if (typeof content === "string") return content;
2507
+ if (!Array.isArray(content)) return "";
2508
+ const parts = [];
2509
+ for (const block of content) {
2510
+ if (block?.type === "text" && typeof block.text === "string") {
2511
+ parts.push(block.text);
2512
+ }
2644
2513
  }
2514
+ return parts.join("\n").trim();
2515
+ }
2516
+ function isMetaUserMessage(line, text) {
2517
+ if (line.isMeta === true) return true;
2518
+ if (!text) return true;
2519
+ return text.includes("<command-name>") || text.includes("<local-command-caveat>") || text.includes("<command-message>");
2520
+ }
2521
+ function parseTimestamp(ts) {
2522
+ if (typeof ts !== "string" || !ts) return NaN;
2523
+ return Date.parse(ts);
2524
+ }
2525
+ function isCompactionEntry(line) {
2526
+ if (line.type !== "system") return false;
2527
+ const subtype = line.subtype ?? "";
2528
+ return subtype === "compact_boundary" || subtype === "pre_compact" || subtype === "post_compact" || /compact/i.test(subtype);
2645
2529
  }
2646
- function extractFromTranscript(transcriptPath) {
2530
+ function parseTranscript(raw) {
2647
2531
  const empty = {
2648
2532
  prompt: "",
2649
2533
  response: "",
@@ -2651,14 +2535,139 @@ function extractFromTranscript(transcriptPath) {
2651
2535
  inputTokens: 0,
2652
2536
  outputTokens: 0,
2653
2537
  modelName: null,
2654
- numTurns: 0
2538
+ numTurns: 0,
2539
+ toolCallCount: 0,
2540
+ filesEditedCount: 0,
2541
+ bashCommandCount: 0
2542
+ };
2543
+ if (!raw) return empty;
2544
+ const lines = raw.split("\n");
2545
+ let lastUserText = "";
2546
+ let lastUserTimestamp = NaN;
2547
+ let lastUserTimestampRaw;
2548
+ let lastAssistantText = "";
2549
+ let lastAssistantTimestamp = NaN;
2550
+ let lastAssistantModel = null;
2551
+ let lastAssistantInputTokens = 0;
2552
+ let lastAssistantOutputTokens = 0;
2553
+ let numTurns = 0;
2554
+ let currentTurnUserTimestamp = NaN;
2555
+ let toolCallCount = 0;
2556
+ let bashCommandCount = 0;
2557
+ let editedFilePaths = /* @__PURE__ */ new Set();
2558
+ for (const rawLine of lines) {
2559
+ if (!rawLine) continue;
2560
+ let parsed;
2561
+ try {
2562
+ parsed = JSON.parse(rawLine);
2563
+ } catch {
2564
+ continue;
2565
+ }
2566
+ if (isCompactionEntry(parsed)) continue;
2567
+ if (parsed.isSidechain === true) continue;
2568
+ if (parsed.type === "user" && parsed.message) {
2569
+ const text = extractTextContent(parsed.message.content);
2570
+ if (isMetaUserMessage(parsed, text)) continue;
2571
+ lastUserText = text;
2572
+ lastUserTimestamp = parseTimestamp(parsed.timestamp);
2573
+ currentTurnUserTimestamp = lastUserTimestamp;
2574
+ toolCallCount = 0;
2575
+ bashCommandCount = 0;
2576
+ editedFilePaths = /* @__PURE__ */ new Set();
2577
+ lastUserTimestampRaw = typeof parsed.timestamp === "string" && parsed.timestamp ? parsed.timestamp : void 0;
2578
+ } else if (parsed.type === "assistant" && parsed.message) {
2579
+ const text = extractTextContent(parsed.message.content);
2580
+ numTurns += 1;
2581
+ if (text) lastAssistantText = text;
2582
+ if (typeof parsed.message.model === "string") {
2583
+ lastAssistantModel = parsed.message.model;
2584
+ }
2585
+ const content = parsed.message.content;
2586
+ if (Array.isArray(content)) {
2587
+ for (const block of content) {
2588
+ try {
2589
+ if (!block || block.type !== "tool_use") continue;
2590
+ toolCallCount += 1;
2591
+ const name = typeof block.name === "string" ? block.name : "";
2592
+ if (name === BASH_TOOL_NAME) {
2593
+ bashCommandCount += 1;
2594
+ continue;
2595
+ }
2596
+ if (FILE_EDITING_TOOL_NAMES.has(name)) {
2597
+ const input = block.input;
2598
+ if (input !== null && typeof input === "object" && "file_path" in input) {
2599
+ const filePath = input.file_path;
2600
+ if (typeof filePath === "string" && filePath) {
2601
+ editedFilePaths.add(filePath);
2602
+ }
2603
+ }
2604
+ }
2605
+ } catch {
2606
+ }
2607
+ }
2608
+ }
2609
+ const usage = parsed.message.usage;
2610
+ if (usage) {
2611
+ const input = (usage.input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0);
2612
+ const output = usage.output_tokens ?? 0;
2613
+ lastAssistantInputTokens = input;
2614
+ lastAssistantOutputTokens = output;
2615
+ }
2616
+ const ts = parseTimestamp(parsed.timestamp);
2617
+ if (!Number.isNaN(ts)) {
2618
+ lastAssistantTimestamp = ts;
2619
+ }
2620
+ }
2621
+ }
2622
+ const result = {
2623
+ prompt: lastUserText,
2624
+ response: lastAssistantText,
2625
+ tokens: lastAssistantInputTokens + lastAssistantOutputTokens,
2626
+ inputTokens: lastAssistantInputTokens,
2627
+ outputTokens: lastAssistantOutputTokens,
2628
+ modelName: lastAssistantModel,
2629
+ numTurns,
2630
+ toolCallCount,
2631
+ filesEditedCount: editedFilePaths.size,
2632
+ bashCommandCount
2633
+ };
2634
+ if (!Number.isNaN(currentTurnUserTimestamp) && !Number.isNaN(lastAssistantTimestamp) && lastAssistantTimestamp >= currentTurnUserTimestamp) {
2635
+ result.latencyMs = Math.round(
2636
+ lastAssistantTimestamp - currentTurnUserTimestamp
2637
+ );
2638
+ }
2639
+ const skill = detectSkill(lastUserText);
2640
+ if (skill) {
2641
+ result.skill = skill;
2642
+ }
2643
+ if (lastUserTimestampRaw) {
2644
+ result.userTurnTimestamp = lastUserTimestampRaw;
2645
+ }
2646
+ return result;
2647
+ }
2648
+
2649
+ // src/monitor/plugins/claude-code/hook.ts
2650
+ var noopDebug = () => {
2651
+ };
2652
+ function extractFromTranscript(transcriptPath, debugLog4 = noopDebug) {
2653
+ const empty = {
2654
+ prompt: "",
2655
+ response: "",
2656
+ tokens: 0,
2657
+ inputTokens: 0,
2658
+ outputTokens: 0,
2659
+ modelName: null,
2660
+ numTurns: 0,
2661
+ toolCallCount: 0,
2662
+ filesEditedCount: 0,
2663
+ bashCommandCount: 0
2655
2664
  };
2656
2665
  if (!transcriptPath) return empty;
2657
2666
  let raw;
2658
2667
  try {
2659
2668
  raw = fs3.readFileSync(transcriptPath, "utf-8");
2660
2669
  } catch (err) {
2661
- debugLog("transcript-read-failed", {
2670
+ debugLog4("transcript-read-failed", {
2662
2671
  transcriptPath,
2663
2672
  error: err.message
2664
2673
  });
@@ -2680,89 +2689,7 @@ function extractSubagentName(event) {
2680
2689
  }
2681
2690
  return void 0;
2682
2691
  }
2683
- async function hookCommand(event) {
2684
- setDebugLogger(debugLog);
2685
- try {
2686
- const stdinData = await readStdin(3e3);
2687
- debugLog("stdin-raw", stdinData);
2688
- let eventData = {};
2689
- if (stdinData) {
2690
- try {
2691
- eventData = JSON.parse(stdinData);
2692
- } catch {
2693
- debugLog("stdin-parse-failed", stdinData);
2694
- }
2695
- }
2696
- debugLog("event-parsed", { event, eventData });
2697
- const projectRoot = resolveProjectRootFromPayload(
2698
- eventData,
2699
- process.cwd()
2700
- );
2701
- if (!projectRoot) {
2702
- debugLog("config-not-found", {
2703
- startDir: typeof eventData.cwd === "string" && eventData.cwd.trim() ? eventData.cwd : process.cwd()
2704
- });
2705
- return;
2706
- }
2707
- const config = loadMonitorConfig(projectRoot);
2708
- if (!config) {
2709
- debugLog("config-load-failed", { projectRoot });
2710
- return;
2711
- }
2712
- const payload = buildPayload(event, eventData, config);
2713
- if (!payload) {
2714
- return;
2715
- }
2716
- debugLog("payload-built", payload);
2717
- const sessionId = typeof eventData.session_id === "string" && eventData.session_id || void 0;
2718
- const extracted = extractFromTranscript(eventData.transcript_path);
2719
- const userTurnTimestamp = extracted.userTurnTimestamp;
2720
- if (sessionId) {
2721
- const existingState = loadSessionState(sessionId);
2722
- if (!shouldReportTurn(existingState, userTurnTimestamp)) {
2723
- debugLog("turn-dedup-skip", {
2724
- sessionId,
2725
- userTurnTimestamp
2726
- });
2727
- return;
2728
- }
2729
- }
2730
- const controller = new AbortController();
2731
- const timeoutId = setTimeout(() => controller.abort(), 5e3);
2732
- try {
2733
- const res = await fetch(config.monitoringEndpoint, {
2734
- method: "POST",
2735
- headers: {
2736
- "x-api-key": config.apiKey,
2737
- "Content-Type": "application/json"
2738
- },
2739
- body: JSON.stringify([payload]),
2740
- signal: controller.signal
2741
- });
2742
- debugLog("response-status", {
2743
- status: res.status,
2744
- ok: res.ok
2745
- });
2746
- } finally {
2747
- clearTimeout(timeoutId);
2748
- }
2749
- if (sessionId && userTurnTimestamp) {
2750
- saveSessionState(sessionId, {
2751
- lastUserTimestamp: userTurnTimestamp,
2752
- lastReportedAt: (/* @__PURE__ */ new Date()).toISOString(),
2753
- numTurnsAtLastReport: extracted.numTurns
2754
- });
2755
- debugLog("state-saved", {
2756
- sessionId,
2757
- userTurnTimestamp,
2758
- numTurns: extracted.numTurns
2759
- });
2760
- }
2761
- } catch (err) {
2762
- debugLog("hook-exception", err.message);
2763
- }
2764
- }
2765
- function buildPayload(event, eventData, config) {
2692
+ function buildClaudeCodePayload(event, eventData, config) {
2766
2693
  const sessionId = eventData.session_id ?? `claude-code-${Date.now()}`;
2767
2694
  switch (event) {
2768
2695
  case "stop":
@@ -2777,7 +2704,13 @@ function buildPayload(event, eventData, config) {
2777
2704
  stopHookActive: eventData.stop_hook_active ?? false,
2778
2705
  inputTokens: extracted.inputTokens,
2779
2706
  outputTokens: extracted.outputTokens,
2780
- numTurns: extracted.numTurns
2707
+ numTurns: extracted.numTurns,
2708
+ // Per-turn work signals for the Claude Code classifier (D-027).
2709
+ // Always emitted as JSON numbers — the backend classifier and
2710
+ // KPI formulas must see numeric 0, not missing keys or strings.
2711
+ toolCallCount: extracted.toolCallCount,
2712
+ filesEditedCount: extracted.filesEditedCount,
2713
+ bashCommandCount: extracted.bashCommandCount
2781
2714
  };
2782
2715
  if (typeof extracted.latencyMs === "number") {
2783
2716
  customData.latencyMs = extracted.latencyMs;
@@ -2796,8 +2729,6 @@ function buildPayload(event, eventData, config) {
2796
2729
  prompt: extracted.prompt,
2797
2730
  response,
2798
2731
  chatId: sessionId,
2799
- // Top-level source so the backend ExternalPromptRequestSchema
2800
- // picks it up (matches regex ^[a-z0-9_-]+$).
2801
2732
  source: config.source,
2802
2733
  modelName: extracted.modelName ?? void 0,
2803
2734
  tokens: extracted.tokens,
@@ -2808,122 +2739,2002 @@ function buildPayload(event, eventData, config) {
2808
2739
  return null;
2809
2740
  }
2810
2741
  }
2811
- async function statusCommand(options) {
2812
- const projectRoot = findProjectRoot();
2813
- const config = loadMonitorConfig(projectRoot);
2814
- if (!config) {
2815
- console.log("Monitoring is not configured for this workspace.");
2816
- console.log("Run 'olakai monitor init' to set up monitoring.");
2817
- process.exit(1);
2818
- }
2819
- const settings = readJsonFile(getSettingsPath(projectRoot));
2820
- const hooksPresent = settings?.hooks ? Object.values(settings.hooks).some(
2821
- (entries) => entries.some(
2822
- (e) => e.hooks.some((h) => h.command.includes(OLAKAI_HOOK_MARKER))
2823
- )
2824
- ) : false;
2825
- if (options.json) {
2826
- console.log(
2827
- JSON.stringify(
2828
- {
2829
- ...config,
2830
- apiKey: config.apiKey.slice(0, 12) + "...",
2831
- hooksConfigured: hooksPresent
2832
- },
2833
- null,
2834
- 2
2835
- )
2836
- );
2837
- return;
2838
- }
2839
- console.log("Olakai Monitor Status");
2840
- console.log("=====================");
2841
- console.log(`Agent: ${config.agentName}`);
2842
- console.log(`Agent ID: ${config.agentId}`);
2843
- console.log(`API Key: ${config.apiKey.slice(0, 12)}...`);
2844
- console.log(`Endpoint: ${config.monitoringEndpoint}`);
2845
- console.log(`Source: ${config.source}`);
2846
- console.log(`Configured: ${config.createdAt}`);
2847
- console.log(`Hooks: ${hooksPresent ? "Active" : "Missing (run 'olakai monitor init' to restore)"}`);
2742
+
2743
+ // src/monitor/plugins/claude-code/index.ts
2744
+ var TOOL_ID = "claude-code";
2745
+ var SUPPORTED_HOOK_EVENTS = /* @__PURE__ */ new Set(["stop", "subagent-stop"]);
2746
+ function debugLog(label, data) {
2747
+ if (process.env.OLAKAI_MONITOR_DEBUG !== "1") return;
2848
2748
  try {
2849
- const token = getValidToken();
2850
- if (token) {
2851
- const params = new URLSearchParams({
2852
- agentId: config.agentId,
2853
- limit: "5"
2854
- });
2855
- const response = await fetch(
2856
- `${getBaseUrl()}/api/activity/prompts?${params}`,
2857
- {
2858
- headers: { Authorization: `Bearer ${token}` }
2859
- }
2860
- );
2861
- if (response.ok) {
2862
- const data = await response.json();
2863
- if (data.prompts && data.prompts.length > 0) {
2864
- console.log("");
2865
- console.log("Recent Activity:");
2866
- for (const p of data.prompts) {
2867
- console.log(` ${p.createdAt} ${p.id.slice(0, 12)}...`);
2868
- }
2869
- } else {
2870
- console.log("");
2871
- console.log("No activity recorded yet.");
2872
- }
2873
- }
2749
+ const logPath = `/tmp/olakai-monitor-debug-${process.pid}.log`;
2750
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${label}: ${typeof data === "string" ? data : JSON.stringify(data, null, 2)}
2751
+ `;
2752
+ fs4.appendFileSync(logPath, line, "utf-8");
2753
+ } catch {
2754
+ }
2755
+ }
2756
+ function resolveProjectRootFromPayload(eventData, fallbackCwd) {
2757
+ const payloadCwd = typeof eventData.cwd === "string" && eventData.cwd.trim() ? eventData.cwd : fallbackCwd;
2758
+ return findConfiguredWorkspace(payloadCwd, [TOOL_ID]);
2759
+ }
2760
+ var claudeCodePlugin = {
2761
+ id: TOOL_ID,
2762
+ displayName: "Claude Code",
2763
+ install(opts) {
2764
+ return installClaudeCode(opts);
2765
+ },
2766
+ uninstall(opts) {
2767
+ return uninstallClaudeCode(opts);
2768
+ },
2769
+ status(opts) {
2770
+ return getClaudeCodeStatus(opts);
2771
+ },
2772
+ async handleHook(eventName, payloadJson) {
2773
+ setDebugLogger(debugLog);
2774
+ const event = eventName.trim();
2775
+ if (!SUPPORTED_HOOK_EVENTS.has(event)) {
2776
+ debugLog("hook-unknown-event", event);
2777
+ return null;
2778
+ }
2779
+ const eventData = payloadJson ?? {};
2780
+ debugLog("event-parsed", { event, eventData });
2781
+ const projectRoot = resolveProjectRootFromPayload(
2782
+ eventData,
2783
+ process.cwd()
2784
+ );
2785
+ if (!projectRoot) {
2786
+ debugLog("config-not-found", {
2787
+ startDir: typeof eventData.cwd === "string" && eventData.cwd.trim() ? eventData.cwd : process.cwd()
2788
+ });
2789
+ return null;
2790
+ }
2791
+ const config = loadClaudeCodeConfig(projectRoot, () => {
2792
+ });
2793
+ if (!config) {
2794
+ debugLog("config-load-failed", { projectRoot });
2795
+ return null;
2796
+ }
2797
+ const payload = buildClaudeCodePayload(event, eventData, config);
2798
+ if (!payload) return null;
2799
+ debugLog("payload-built", payload);
2800
+ const sessionId = typeof eventData.session_id === "string" && eventData.session_id || void 0;
2801
+ const extracted = extractFromTranscript(
2802
+ eventData.transcript_path,
2803
+ debugLog
2804
+ );
2805
+ const userTurnTimestamp = extracted.userTurnTimestamp;
2806
+ if (extracted.prompt.trim() === "" && extracted.response.trim() === "" && extracted.numTurns === 0) {
2807
+ debugLog("empty-parse-skip", {
2808
+ event,
2809
+ sessionId,
2810
+ transcriptPath: eventData.transcript_path
2811
+ });
2812
+ return null;
2813
+ }
2814
+ if (sessionId) {
2815
+ const existingState = loadSessionState(sessionId);
2816
+ if (!shouldReportTurn(existingState, userTurnTimestamp)) {
2817
+ debugLog("turn-dedup-skip", { sessionId, userTurnTimestamp });
2818
+ return null;
2819
+ }
2820
+ }
2821
+ if (sessionId && userTurnTimestamp) {
2822
+ saveSessionState(sessionId, {
2823
+ lastUserTimestamp: userTurnTimestamp,
2824
+ lastReportedAt: (/* @__PURE__ */ new Date()).toISOString(),
2825
+ numTurnsAtLastReport: extracted.numTurns
2826
+ });
2827
+ debugLog("state-saved", {
2828
+ sessionId,
2829
+ userTurnTimestamp,
2830
+ numTurns: extracted.numTurns
2831
+ });
2832
+ }
2833
+ return {
2834
+ payload,
2835
+ transport: {
2836
+ endpoint: config.monitoringEndpoint,
2837
+ apiKey: config.apiKey,
2838
+ projectRoot
2839
+ }
2840
+ };
2841
+ },
2842
+ async detectInstalled(opts) {
2843
+ const projectRoot = opts?.projectRoot ?? process.cwd();
2844
+ try {
2845
+ if (fs4.existsSync(getMonitorConfigPath(projectRoot, TOOL_ID))) {
2846
+ return true;
2847
+ }
2848
+ if (fs4.existsSync(getLegacyClaudeMonitorConfigPath(projectRoot))) {
2849
+ return true;
2850
+ }
2851
+ return false;
2852
+ } catch {
2853
+ return false;
2854
+ }
2855
+ }
2856
+ };
2857
+ registerPlugin(claudeCodePlugin);
2858
+
2859
+ // src/monitor/plugins/codex/index.ts
2860
+ import * as fs9 from "fs";
2861
+ import { spawnSync } from "child_process";
2862
+
2863
+ // src/monitor/plugins/codex/install.ts
2864
+ import * as fs6 from "fs";
2865
+ import * as path5 from "path";
2866
+ import * as TOML from "@iarna/toml";
2867
+
2868
+ // src/monitor/plugins/codex/paths.ts
2869
+ import * as os2 from "os";
2870
+ import * as path3 from "path";
2871
+ var CODEX_HOME_DIRNAME = ".codex";
2872
+ var CODEX_CONFIG_FILENAME = "config.toml";
2873
+ var CODEX_SESSIONS_DIRNAME = "sessions";
2874
+ function getCodexHomeDir() {
2875
+ return path3.join(os2.homedir(), CODEX_HOME_DIRNAME);
2876
+ }
2877
+ function getCodexConfigPath() {
2878
+ return path3.join(getCodexHomeDir(), CODEX_CONFIG_FILENAME);
2879
+ }
2880
+ function getCodexSessionsDir() {
2881
+ return path3.join(getCodexHomeDir(), CODEX_SESSIONS_DIRNAME);
2882
+ }
2883
+
2884
+ // src/monitor/plugins/codex/hooks.ts
2885
+ var OLAKAI_HOOK_MARKER2 = "olakai monitor hook";
2886
+ var CODEX_HOOK_TIMEOUT_SECONDS = 5;
2887
+ var SUPPORTED_HOOK_EVENT_NAMES = ["Stop"];
2888
+ function buildOlakaiHookGroup(event) {
2889
+ return {
2890
+ hooks: [
2891
+ {
2892
+ type: "command",
2893
+ command: `olakai monitor hook --tool codex ${event}`,
2894
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS
2895
+ }
2896
+ ]
2897
+ };
2898
+ }
2899
+ function isOlakaiHandler(handler) {
2900
+ return typeof handler.command === "string" && handler.command.includes(OLAKAI_HOOK_MARKER2);
2901
+ }
2902
+ function groupContainsOlakaiHandler(group) {
2903
+ if (!Array.isArray(group.hooks)) return false;
2904
+ return group.hooks.some(isOlakaiHandler);
2905
+ }
2906
+ function mergeCodexHooks(existing, events = SUPPORTED_HOOK_EVENT_NAMES) {
2907
+ const merged = { ...existing ?? {} };
2908
+ for (const event of events) {
2909
+ const existingGroups = merged[event] ?? [];
2910
+ const hasOlakaiHook = existingGroups.some(groupContainsOlakaiHandler);
2911
+ if (hasOlakaiHook) {
2912
+ merged[event] = existingGroups;
2913
+ } else {
2914
+ merged[event] = [...existingGroups, buildOlakaiHookGroup(event)];
2915
+ }
2916
+ }
2917
+ return merged;
2918
+ }
2919
+ function stripOlakaiHooks(existing) {
2920
+ if (!existing) return void 0;
2921
+ const cleaned = {};
2922
+ for (const [event, groups] of Object.entries(existing)) {
2923
+ if (!Array.isArray(groups)) continue;
2924
+ const filteredGroups = [];
2925
+ for (const group of groups) {
2926
+ const handlers = Array.isArray(group.hooks) ? group.hooks : [];
2927
+ const remaining = handlers.filter((h) => !isOlakaiHandler(h));
2928
+ if (remaining.length === 0 && handlers.length > 0) {
2929
+ continue;
2930
+ }
2931
+ if (remaining.length === handlers.length) {
2932
+ filteredGroups.push(group);
2933
+ } else {
2934
+ filteredGroups.push({ ...group, hooks: remaining });
2935
+ }
2936
+ }
2937
+ if (filteredGroups.length > 0) {
2938
+ cleaned[event] = filteredGroups;
2939
+ }
2940
+ }
2941
+ return Object.keys(cleaned).length > 0 ? cleaned : void 0;
2942
+ }
2943
+ function hasOlakaiHooksInstalled(parsed) {
2944
+ const hooks = parsed.hooks;
2945
+ if (!hooks) return false;
2946
+ for (const groups of Object.values(hooks)) {
2947
+ if (!Array.isArray(groups)) continue;
2948
+ for (const group of groups) {
2949
+ if (groupContainsOlakaiHandler(group)) return true;
2950
+ }
2951
+ }
2952
+ return false;
2953
+ }
2954
+
2955
+ // src/monitor/plugins/codex/config.ts
2956
+ import * as fs5 from "fs";
2957
+ import * as path4 from "path";
2958
+ function getCodexConfigPath2(projectRoot) {
2959
+ return getMonitorConfigPath(projectRoot, "codex");
2960
+ }
2961
+ function loadCodexConfig(projectRoot) {
2962
+ const filePath = getCodexConfigPath2(projectRoot);
2963
+ try {
2964
+ if (!fs5.existsSync(filePath)) return null;
2965
+ const raw = fs5.readFileSync(filePath, "utf-8");
2966
+ return JSON.parse(raw);
2967
+ } catch {
2968
+ return null;
2969
+ }
2970
+ }
2971
+ function writeCodexConfig(projectRoot, config) {
2972
+ const filePath = getCodexConfigPath2(projectRoot);
2973
+ const dir = path4.dirname(filePath);
2974
+ if (!fs5.existsSync(dir)) {
2975
+ fs5.mkdirSync(dir, { recursive: true });
2976
+ }
2977
+ fs5.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2978
+ try {
2979
+ fs5.chmodSync(filePath, 384);
2980
+ } catch {
2981
+ }
2982
+ }
2983
+ function deleteCodexConfig(projectRoot) {
2984
+ const filePath = getCodexConfigPath2(projectRoot);
2985
+ if (!fs5.existsSync(filePath)) return false;
2986
+ try {
2987
+ fs5.unlinkSync(filePath);
2988
+ return true;
2989
+ } catch {
2990
+ return false;
2991
+ }
2992
+ }
2993
+
2994
+ // src/monitor/plugins/codex/install.ts
2995
+ var CODEX_SOURCE = "codex";
2996
+ var CODEX_AGENT_SOURCE = "CODEX";
2997
+ var CODEX_AGENT_CATEGORY = "CODING";
2998
+ async function installCodex(opts) {
2999
+ const projectRoot = opts.projectRoot ?? process.cwd();
3000
+ const token = getValidToken();
3001
+ if (!token) {
3002
+ console.error("Not logged in. Run 'olakai login' first.");
3003
+ process.exit(1);
3004
+ }
3005
+ console.log("Setting up Codex CLI monitoring for this workspace...\n");
3006
+ const dirName = path5.basename(projectRoot);
3007
+ let agent;
3008
+ const choice = await promptUser(
3009
+ "Create a new agent or use an existing one? (new/existing) [new]: "
3010
+ );
3011
+ if (choice.toLowerCase() === "existing" || choice.toLowerCase() === "e") {
3012
+ const agents = await listAgents();
3013
+ if (agents.length === 0) {
3014
+ console.log("No agents found. Creating a new one instead.\n");
3015
+ agent = await createNewAgent2(dirName);
3016
+ } else {
3017
+ console.log("\nAvailable agents:");
3018
+ for (let i = 0; i < agents.length; i++) {
3019
+ console.log(
3020
+ ` ${i + 1}. ${agents[i].name} (${agents[i].id.slice(0, 12)}...)`
3021
+ );
3022
+ }
3023
+ const selection = await promptUser(`
3024
+ Select agent (1-${agents.length}): `);
3025
+ const idx = parseInt(selection, 10) - 1;
3026
+ if (isNaN(idx) || idx < 0 || idx >= agents.length) {
3027
+ console.error("Invalid selection.");
3028
+ process.exit(1);
3029
+ }
3030
+ agent = await getAgent(agents[idx].id);
3031
+ if (!agent.apiKey?.key && !agent.apiKey?.isActive) {
3032
+ console.log(
3033
+ "\nThis agent has no active API key. Please create a new agent instead,"
3034
+ );
3035
+ console.log("or generate an API key via the Olakai dashboard.\n");
3036
+ const createNew = await promptUser("Create a new agent? (y/n) [y]: ");
3037
+ if (createNew.toLowerCase() !== "n") {
3038
+ agent = await createNewAgent2(dirName);
3039
+ } else {
3040
+ process.exit(1);
3041
+ }
3042
+ }
3043
+ }
3044
+ } else {
3045
+ agent = await createNewAgent2(dirName);
3046
+ }
3047
+ const monitoringEndpoint = `${getBaseUrl()}/api/monitoring/prompt`;
3048
+ let apiKey = agent.apiKey?.key;
3049
+ if (!apiKey) {
3050
+ console.log(
3051
+ "\nThe API key for this agent is not available (it is only shown once at creation)."
3052
+ );
3053
+ apiKey = await promptUser("Paste the API key for this agent: ");
3054
+ if (!apiKey) {
3055
+ console.error("API key is required for monitoring.");
3056
+ process.exit(1);
3057
+ }
3058
+ await validatePastedApiKey({
3059
+ monitoringEndpoint,
3060
+ apiKey,
3061
+ expectedAgentId: agent.id,
3062
+ expectedAgentName: agent.name,
3063
+ expectedApiKeyId: agent.apiKey?.id ?? null
3064
+ });
3065
+ }
3066
+ const { configExisted: codexConfigExisted } = installCodexHooksConfig();
3067
+ const monitorConfig = {
3068
+ agentId: agent.id,
3069
+ apiKey,
3070
+ agentName: agent.name,
3071
+ source: CODEX_SOURCE,
3072
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3073
+ monitoringEndpoint
3074
+ };
3075
+ writeCodexConfig(projectRoot, monitorConfig);
3076
+ const configPath = getCodexConfigPath2(projectRoot);
3077
+ const configRel = path5.relative(projectRoot, configPath);
3078
+ console.log("");
3079
+ console.log(`\u2713 Agent "${agent.name}" configured (ID: ${agent.id})`);
3080
+ if (agent.apiKey?.key) {
3081
+ console.log("\u2713 API key generated");
3082
+ }
3083
+ console.log(
3084
+ `\u2713 Codex hooks configured in ~/${CODEX_HOME_DIRNAME}/${CODEX_CONFIG_FILENAME}`
3085
+ );
3086
+ console.log(`\u2713 Monitor config saved to ${configRel}`);
3087
+ console.log("");
3088
+ console.log(
3089
+ "Monitoring is now active. Codex CLI will report activity to Olakai"
3090
+ );
3091
+ console.log(`on each turn. View activity at: ${getBaseUrl()}/dashboard`);
3092
+ console.log("");
3093
+ console.log(
3094
+ `\u26A0 Ensure ${OLAKAI_DIR}/ is in your .gitignore (it contains your API key).`
3095
+ );
3096
+ console.log(
3097
+ `\u26A0 Codex hooks require codex >= 0.124.0. Earlier versions silently skip them.`
3098
+ );
3099
+ if (codexConfigExisted) {
3100
+ console.log(
3101
+ `\u26A0 Existing comments in ~/${CODEX_HOME_DIRNAME}/${CODEX_CONFIG_FILENAME} were not preserved (TOML serializer limitation).`
3102
+ );
3103
+ }
3104
+ console.log("");
3105
+ console.log("To check status: olakai monitor status --tool codex");
3106
+ console.log("To disable: olakai monitor disable --tool codex");
3107
+ return {
3108
+ agentId: agent.id,
3109
+ agentName: agent.name,
3110
+ source: CODEX_SOURCE,
3111
+ monitoringEndpoint
3112
+ };
3113
+ }
3114
+ async function createNewAgent2(defaultName) {
3115
+ const nameInput = await promptUser(`Agent name [${defaultName}]: `);
3116
+ const agentName = nameInput || defaultName;
3117
+ return createAgent({
3118
+ name: agentName,
3119
+ description: `Codex CLI local agent for ${agentName}`,
3120
+ role: "WORKER",
3121
+ createApiKey: true,
3122
+ category: CODEX_AGENT_CATEGORY,
3123
+ source: CODEX_AGENT_SOURCE
3124
+ });
3125
+ }
3126
+ function installCodexHooksConfig() {
3127
+ const homeDir = getCodexHomeDir();
3128
+ const configPath = getCodexConfigPath();
3129
+ if (!fs6.existsSync(homeDir)) {
3130
+ fs6.mkdirSync(homeDir, { recursive: true });
3131
+ }
3132
+ const configExisted = fs6.existsSync(configPath);
3133
+ const parsed = readCodexConfigToml(configPath);
3134
+ const merged = {
3135
+ ...parsed,
3136
+ hooks: mergeCodexHooks(parsed.hooks, SUPPORTED_HOOK_EVENT_NAMES)
3137
+ };
3138
+ writeCodexConfigToml(configPath, merged);
3139
+ return { configExisted };
3140
+ }
3141
+ async function uninstallCodex(opts) {
3142
+ const projectRoot = opts.projectRoot ?? process.cwd();
3143
+ const removedHooks = stripCodexHooksConfig();
3144
+ if (removedHooks) {
3145
+ console.log(
3146
+ `\u2713 Olakai hooks removed from ~/${CODEX_HOME_DIRNAME}/${CODEX_CONFIG_FILENAME}`
3147
+ );
3148
+ } else {
3149
+ console.log("No Olakai hooks found in Codex config.");
3150
+ }
3151
+ if (!opts.keepConfig) {
3152
+ const configPath = getCodexConfigPath2(projectRoot);
3153
+ const configRel = path5.relative(projectRoot, configPath);
3154
+ if (deleteCodexConfig(projectRoot)) {
3155
+ console.log(`\u2713 Monitor config removed (${configRel})`);
3156
+ }
3157
+ } else {
3158
+ const configPath = getCodexConfigPath2(projectRoot);
3159
+ const configRel = path5.relative(projectRoot, configPath);
3160
+ console.log(`Monitor config retained at ${configRel}`);
3161
+ }
3162
+ console.log("");
3163
+ console.log(
3164
+ "Monitoring disabled. Run 'olakai monitor init --tool codex' to re-enable."
3165
+ );
3166
+ }
3167
+ function applyOlakaiStripToConfig(parsed) {
3168
+ const cleaned = stripOlakaiHooks(parsed.hooks);
3169
+ const next = { ...parsed };
3170
+ if (cleaned === void 0) {
3171
+ delete next.hooks;
3172
+ } else {
3173
+ next.hooks = cleaned;
3174
+ }
3175
+ return next;
3176
+ }
3177
+ function stripCodexHooksConfig() {
3178
+ const configPath = getCodexConfigPath();
3179
+ if (!fs6.existsSync(configPath)) return false;
3180
+ const parsed = readCodexConfigToml(configPath);
3181
+ if (!hasOlakaiHooksInstalled(parsed)) return false;
3182
+ const next = applyOlakaiStripToConfig(parsed);
3183
+ writeCodexConfigToml(configPath, next);
3184
+ return true;
3185
+ }
3186
+ function readCodexConfigToml(configPath) {
3187
+ try {
3188
+ if (!fs6.existsSync(configPath)) return {};
3189
+ const raw = fs6.readFileSync(configPath, "utf-8");
3190
+ if (!raw.trim()) return {};
3191
+ return TOML.parse(raw);
3192
+ } catch {
3193
+ return {};
3194
+ }
3195
+ }
3196
+ function writeCodexConfigToml(configPath, data) {
3197
+ const dir = path5.dirname(configPath);
3198
+ if (!fs6.existsSync(dir)) {
3199
+ fs6.mkdirSync(dir, { recursive: true });
3200
+ }
3201
+ const serialized = TOML.stringify(data);
3202
+ fs6.writeFileSync(configPath, serialized, "utf-8");
3203
+ }
3204
+
3205
+ // src/monitor/plugins/codex/status.ts
3206
+ import path6 from "path";
3207
+ import * as fs7 from "fs";
3208
+ async function getCodexStatus(opts) {
3209
+ const projectRoot = opts?.projectRoot ?? process.cwd();
3210
+ const configPath = getCodexConfigPath2(projectRoot);
3211
+ const config = loadCodexConfig(projectRoot);
3212
+ const hooksConfigured = isHooksBlockInstalled();
3213
+ if (!config) {
3214
+ return {
3215
+ toolId: "codex",
3216
+ configured: false,
3217
+ hooksConfigured,
3218
+ configPath,
3219
+ notes: hooksConfigured ? [
3220
+ `Codex hooks present in ~/${CODEX_HOME_DIRNAME}/${CODEX_CONFIG_FILENAME} but no monitor config in this workspace \u2014 re-run init.`
3221
+ ] : []
3222
+ };
3223
+ }
3224
+ return {
3225
+ toolId: "codex",
3226
+ configured: true,
3227
+ hooksConfigured,
3228
+ agentId: config.agentId,
3229
+ agentName: config.agentName,
3230
+ source: config.source,
3231
+ apiKeyMasked: config.apiKey.slice(0, 12) + "...",
3232
+ monitoringEndpoint: config.monitoringEndpoint,
3233
+ configuredAt: config.createdAt,
3234
+ configPath
3235
+ };
3236
+ }
3237
+ function isHooksBlockInstalled() {
3238
+ const homeConfig = getCodexConfigPath();
3239
+ if (!fs7.existsSync(homeConfig)) return false;
3240
+ const parsed = readCodexConfigToml(homeConfig);
3241
+ return hasOlakaiHooksInstalled(parsed);
3242
+ }
3243
+
3244
+ // src/monitor/plugins/codex/transcript.ts
3245
+ import * as fs8 from "fs";
3246
+ import * as path7 from "path";
3247
+ function emptyRollout() {
3248
+ return {
3249
+ prompt: "",
3250
+ response: "",
3251
+ modelName: null,
3252
+ inputTokens: 0,
3253
+ outputTokens: 0,
3254
+ cachedInputTokens: 0,
3255
+ reasoningOutputTokens: 0,
3256
+ tokens: 0,
3257
+ numTurns: 0
3258
+ };
3259
+ }
3260
+ var noopDebug2 = () => {
3261
+ };
3262
+ var DEFAULT_LIMITS = {
3263
+ maxDirs: 200,
3264
+ maxFiles: 5e3
3265
+ };
3266
+ function findRolloutPathForSession(sessionId, sessionsDir = getCodexSessionsDir(), limits = {}) {
3267
+ const merged = { ...DEFAULT_LIMITS, ...limits };
3268
+ if (!sessionId) return null;
3269
+ if (!fs8.existsSync(sessionsDir)) return null;
3270
+ const suffix = `-${sessionId}.jsonl`;
3271
+ const matches = [];
3272
+ let dirsScanned = 0;
3273
+ let filesScanned = 0;
3274
+ const queue = [sessionsDir];
3275
+ while (queue.length > 0) {
3276
+ const dir = queue.shift();
3277
+ if (dirsScanned++ > merged.maxDirs) break;
3278
+ let entries;
3279
+ try {
3280
+ entries = fs8.readdirSync(dir, { withFileTypes: true });
3281
+ } catch {
3282
+ continue;
3283
+ }
3284
+ for (const entry of entries) {
3285
+ if (filesScanned++ > merged.maxFiles) break;
3286
+ const full = path7.join(dir, entry.name);
3287
+ if (entry.isDirectory()) {
3288
+ queue.push(full);
3289
+ continue;
3290
+ }
3291
+ if (!entry.isFile()) continue;
3292
+ if (!entry.name.endsWith(suffix)) continue;
3293
+ if (!entry.name.startsWith("rollout-")) continue;
3294
+ try {
3295
+ const stat = fs8.statSync(full);
3296
+ matches.push({ path: full, mtimeMs: stat.mtimeMs });
3297
+ } catch {
3298
+ }
3299
+ }
3300
+ }
3301
+ if (matches.length === 0) return null;
3302
+ matches.sort((a, b) => b.mtimeMs - a.mtimeMs);
3303
+ return matches[0].path;
3304
+ }
3305
+ function parseRolloutContent(raw, debugLog4 = noopDebug2) {
3306
+ const result = emptyRollout();
3307
+ if (!raw) return result;
3308
+ const lines = raw.split("\n");
3309
+ let lastUserMessage = "";
3310
+ let lastAssistantMessage = "";
3311
+ let lastTokenUsage = null;
3312
+ let totalTokenUsage = null;
3313
+ let modelName = null;
3314
+ let userTurnCount = 0;
3315
+ for (const line of lines) {
3316
+ const trimmed = line.trim();
3317
+ if (!trimmed) continue;
3318
+ let parsed;
3319
+ try {
3320
+ parsed = JSON.parse(trimmed);
3321
+ } catch (err) {
3322
+ debugLog4("rollout-line-parse-failed", {
3323
+ line: trimmed.slice(0, 120),
3324
+ error: err.message
3325
+ });
3326
+ continue;
3327
+ }
3328
+ const type = parsed.type;
3329
+ const payload = parsed.payload;
3330
+ if (!type || !payload) continue;
3331
+ if (type === "session_meta") {
3332
+ continue;
3333
+ }
3334
+ if (type === "event_msg") {
3335
+ const ev = payload;
3336
+ const evType = ev.type;
3337
+ if (evType === "user_message" && typeof ev.message === "string") {
3338
+ lastUserMessage = ev.message;
3339
+ userTurnCount += 1;
3340
+ } else if (evType === "agent_message" && typeof ev.message === "string") {
3341
+ lastAssistantMessage = ev.message;
3342
+ } else if (evType === "token_count" && ev.info) {
3343
+ if (ev.info.last_token_usage) {
3344
+ lastTokenUsage = ev.info.last_token_usage;
3345
+ }
3346
+ if (ev.info.total_token_usage) {
3347
+ totalTokenUsage = ev.info.total_token_usage;
3348
+ }
3349
+ } else if (evType === "session_configured" && typeof ev.model === "string" && ev.model) {
3350
+ modelName = ev.model;
3351
+ }
3352
+ continue;
3353
+ }
3354
+ if (type === "response_item") {
3355
+ const ri = payload;
3356
+ if (ri.type === "message" && Array.isArray(ri.content)) {
3357
+ const text = extractTextFromContent(ri.content);
3358
+ if (!text) continue;
3359
+ if (ri.role === "user") {
3360
+ lastUserMessage = text;
3361
+ userTurnCount += 1;
3362
+ } else if (ri.role === "assistant") {
3363
+ lastAssistantMessage = text;
3364
+ }
3365
+ }
3366
+ continue;
3367
+ }
3368
+ }
3369
+ result.prompt = lastUserMessage;
3370
+ result.response = lastAssistantMessage;
3371
+ result.modelName = modelName;
3372
+ result.numTurns = userTurnCount;
3373
+ const tokenSource = lastTokenUsage ?? totalTokenUsage;
3374
+ if (tokenSource) {
3375
+ result.inputTokens = numberOrZero(tokenSource.input_tokens);
3376
+ result.outputTokens = numberOrZero(tokenSource.output_tokens);
3377
+ result.cachedInputTokens = numberOrZero(tokenSource.cached_input_tokens);
3378
+ result.reasoningOutputTokens = numberOrZero(
3379
+ tokenSource.reasoning_output_tokens
3380
+ );
3381
+ result.tokens = numberOrZero(tokenSource.total_tokens) || result.inputTokens + result.outputTokens;
3382
+ }
3383
+ return result;
3384
+ }
3385
+ function extractTextFromContent(content) {
3386
+ const parts = [];
3387
+ for (const block of content) {
3388
+ if (typeof block.text !== "string") continue;
3389
+ if (block.type === "output_text" || block.type === "input_text" || block.type === "text") {
3390
+ parts.push(block.text);
3391
+ }
3392
+ }
3393
+ return parts.join("\n").trim();
3394
+ }
3395
+ function numberOrZero(value) {
3396
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
3397
+ }
3398
+ function loadRolloutForSession(sessionId, options = {}) {
3399
+ const debugLog4 = options.debugLog ?? noopDebug2;
3400
+ const sessionsDir = options.sessionsDir ?? getCodexSessionsDir();
3401
+ const result = emptyRollout();
3402
+ const rolloutPath = findRolloutPathForSession(sessionId, sessionsDir);
3403
+ if (!rolloutPath) {
3404
+ debugLog4("rollout-not-found", {
3405
+ sessionId,
3406
+ sessionsDir
3407
+ });
3408
+ return result;
3409
+ }
3410
+ let raw;
3411
+ try {
3412
+ raw = fs8.readFileSync(rolloutPath, "utf-8");
3413
+ } catch (err) {
3414
+ debugLog4("rollout-read-failed", {
3415
+ rolloutPath,
3416
+ error: err.message
3417
+ });
3418
+ return result;
3419
+ }
3420
+ const parsed = parseRolloutContent(raw, debugLog4);
3421
+ parsed.rolloutPath = rolloutPath;
3422
+ return parsed;
3423
+ }
3424
+
3425
+ // src/monitor/plugins/codex/hook.ts
3426
+ var noopDebug3 = () => {
3427
+ };
3428
+ var SUPPORTED_EVENTS = /* @__PURE__ */ new Set(["Stop", "UserPromptSubmit"]);
3429
+ function isSupportedCodexEvent(eventName) {
3430
+ return SUPPORTED_EVENTS.has(eventName);
3431
+ }
3432
+ function buildCodexPayload(eventName, eventData, config, rollout = emptyRollout()) {
3433
+ if (!isSupportedCodexEvent(eventName)) return null;
3434
+ if (eventName === "UserPromptSubmit") return null;
3435
+ const sessionId = typeof eventData.session_id === "string" && eventData.session_id ? eventData.session_id : `codex-${Date.now()}`;
3436
+ const cwd = typeof eventData.cwd === "string" ? eventData.cwd : "";
3437
+ const turnId = typeof eventData.turn_id === "string" ? eventData.turn_id : "";
3438
+ const permissionMode = typeof eventData.permission_mode === "string" ? eventData.permission_mode : "";
3439
+ const transcriptPath = typeof eventData.transcript_path === "string" ? eventData.transcript_path : "";
3440
+ const modelName = (typeof eventData.model === "string" && eventData.model.trim() ? eventData.model : null) ?? rollout.modelName;
3441
+ const customData = {
3442
+ hookEvent: eventData.hook_event_name ?? eventName,
3443
+ sessionId,
3444
+ cwd,
3445
+ turnId,
3446
+ permissionMode,
3447
+ transcriptPath,
3448
+ inputTokens: rollout.inputTokens,
3449
+ outputTokens: rollout.outputTokens,
3450
+ cachedInputTokens: rollout.cachedInputTokens,
3451
+ reasoningOutputTokens: rollout.reasoningOutputTokens,
3452
+ numTurns: rollout.numTurns
3453
+ };
3454
+ if (rollout.rolloutPath) {
3455
+ customData.rolloutPath = rollout.rolloutPath;
3456
+ }
3457
+ if (typeof eventData.stop_hook_active === "boolean") {
3458
+ customData.stopHookActive = eventData.stop_hook_active;
3459
+ }
3460
+ const inlineAssistant = typeof eventData.last_assistant_message === "string" && eventData.last_assistant_message.trim() ? eventData.last_assistant_message : "";
3461
+ const response = inlineAssistant || rollout.response;
3462
+ return {
3463
+ prompt: rollout.prompt,
3464
+ response,
3465
+ chatId: sessionId,
3466
+ source: config.source,
3467
+ modelName: modelName ?? void 0,
3468
+ tokens: rollout.tokens,
3469
+ customData
3470
+ };
3471
+ }
3472
+ function handleCodexHook(eventName, payloadJson, config, options = {}) {
3473
+ const debugLog4 = options.debugLog ?? noopDebug3;
3474
+ if (!isSupportedCodexEvent(eventName)) {
3475
+ debugLog4("hook-unknown-event", eventName);
3476
+ return null;
3477
+ }
3478
+ if (eventName === "UserPromptSubmit") {
3479
+ debugLog4("user-prompt-submit-dropped", { eventName });
3480
+ return null;
3481
+ }
3482
+ const eventData = payloadJson ?? {};
3483
+ debugLog4("event-parsed", { eventName, eventData });
3484
+ const sessionId = typeof eventData.session_id === "string" ? eventData.session_id : "";
3485
+ let rollout = emptyRollout();
3486
+ if (eventName === "Stop" && sessionId) {
3487
+ rollout = loadRolloutForSession(sessionId, {
3488
+ debugLog: debugLog4,
3489
+ sessionsDir: options.sessionsDir
3490
+ });
3491
+ }
3492
+ const payload = buildCodexPayload(eventName, eventData, config, rollout);
3493
+ if (!payload) return null;
3494
+ if (!payload.prompt && !payload.response) {
3495
+ debugLog4("payload-empty", { eventName, sessionId });
3496
+ return null;
3497
+ }
3498
+ debugLog4("payload-built", payload);
3499
+ return payload;
3500
+ }
3501
+
3502
+ // src/monitor/plugins/codex/index.ts
3503
+ var TOOL_ID2 = "codex";
3504
+ function debugLog2(label, data) {
3505
+ if (process.env.OLAKAI_MONITOR_DEBUG !== "1") return;
3506
+ try {
3507
+ const logPath = `/tmp/olakai-monitor-debug-${process.pid}.log`;
3508
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] codex/${label}: ${typeof data === "string" ? data : JSON.stringify(data, null, 2)}
3509
+ `;
3510
+ fs9.appendFileSync(logPath, line, "utf-8");
3511
+ } catch {
3512
+ }
3513
+ }
3514
+ function resolveCodexProjectRoot(eventData, fallbackCwd) {
3515
+ const payloadCwd = typeof eventData.cwd === "string" && eventData.cwd.trim() ? eventData.cwd : fallbackCwd;
3516
+ return findConfiguredWorkspace(payloadCwd, [TOOL_ID2]);
3517
+ }
3518
+ var codexPlugin = {
3519
+ id: TOOL_ID2,
3520
+ displayName: "OpenAI Codex CLI",
3521
+ install(opts) {
3522
+ return installCodex(opts);
3523
+ },
3524
+ uninstall(opts) {
3525
+ return uninstallCodex(opts);
3526
+ },
3527
+ status(opts) {
3528
+ return getCodexStatus(opts);
3529
+ },
3530
+ async handleHook(eventName, payloadJson) {
3531
+ const eventData = payloadJson ?? {};
3532
+ debugLog2("hook-fired", { eventName, hasPayload: payloadJson != null });
3533
+ const projectRoot = resolveCodexProjectRoot(eventData, process.cwd());
3534
+ if (!projectRoot) {
3535
+ debugLog2("config-not-found", {
3536
+ startDir: typeof eventData.cwd === "string" && eventData.cwd.trim() ? eventData.cwd : process.cwd()
3537
+ });
3538
+ return null;
3539
+ }
3540
+ const config = loadCodexConfig(projectRoot);
3541
+ if (!config) {
3542
+ debugLog2("monitor-config-missing", { projectRoot });
3543
+ return null;
3544
+ }
3545
+ const payload = handleCodexHook(eventName, eventData, config, {
3546
+ debugLog: debugLog2
3547
+ });
3548
+ if (!payload) return null;
3549
+ return {
3550
+ payload,
3551
+ transport: {
3552
+ endpoint: config.monitoringEndpoint,
3553
+ apiKey: config.apiKey,
3554
+ projectRoot
3555
+ }
3556
+ };
3557
+ },
3558
+ async detectInstalled(opts) {
3559
+ const projectRoot = opts?.projectRoot ?? process.cwd();
3560
+ try {
3561
+ if (fs9.existsSync(getCodexConfigPath2(projectRoot))) {
3562
+ return true;
3563
+ }
3564
+ } catch {
3565
+ }
3566
+ try {
3567
+ if (fs9.existsSync(getCodexConfigPath())) {
3568
+ return true;
3569
+ }
3570
+ if (fs9.existsSync(getCodexHomeDir())) {
3571
+ return true;
3572
+ }
3573
+ } catch {
3574
+ }
3575
+ return detectCodexBinaryOnPath();
3576
+ }
3577
+ };
3578
+ function detectCodexBinaryOnPath() {
3579
+ try {
3580
+ const probe = spawnSync(
3581
+ process.platform === "win32" ? "where" : "which",
3582
+ ["codex"],
3583
+ {
3584
+ stdio: ["ignore", "pipe", "ignore"],
3585
+ timeout: 1e3
3586
+ }
3587
+ );
3588
+ if (probe.status === 0 && probe.stdout && probe.stdout.toString().trim()) {
3589
+ return true;
3590
+ }
3591
+ } catch {
3592
+ }
3593
+ return false;
3594
+ }
3595
+ registerPlugin(codexPlugin);
3596
+
3597
+ // src/monitor/plugins/cursor/index.ts
3598
+ import * as fs14 from "fs";
3599
+ import * as os7 from "os";
3600
+
3601
+ // src/monitor/plugins/cursor/install.ts
3602
+ import * as fs11 from "fs";
3603
+ import * as os4 from "os";
3604
+ import * as path10 from "path";
3605
+
3606
+ // src/monitor/plugins/cursor/config.ts
3607
+ import * as fs10 from "fs";
3608
+ import * as path8 from "path";
3609
+ function getCursorConfigPath(projectRoot) {
3610
+ return getMonitorConfigPath(projectRoot, "cursor");
3611
+ }
3612
+ function loadCursorConfig(projectRoot) {
3613
+ const filePath = getCursorConfigPath(projectRoot);
3614
+ try {
3615
+ if (!fs10.existsSync(filePath)) return null;
3616
+ const raw = fs10.readFileSync(filePath, "utf-8");
3617
+ return JSON.parse(raw);
3618
+ } catch {
3619
+ return null;
3620
+ }
3621
+ }
3622
+ function writeCursorConfig(projectRoot, config) {
3623
+ const filePath = getCursorConfigPath(projectRoot);
3624
+ const dir = path8.dirname(filePath);
3625
+ if (!fs10.existsSync(dir)) {
3626
+ fs10.mkdirSync(dir, { recursive: true });
3627
+ }
3628
+ fs10.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
3629
+ try {
3630
+ fs10.chmodSync(filePath, 384);
3631
+ } catch {
3632
+ }
3633
+ }
3634
+ function deleteCursorConfig(projectRoot) {
3635
+ const filePath = getCursorConfigPath(projectRoot);
3636
+ if (!fs10.existsSync(filePath)) return false;
3637
+ try {
3638
+ fs10.unlinkSync(filePath);
3639
+ return true;
3640
+ } catch {
3641
+ return false;
3642
+ }
3643
+ }
3644
+
3645
+ // src/monitor/plugins/cursor/paths.ts
3646
+ import * as os3 from "os";
3647
+ import * as path9 from "path";
3648
+ var CURSOR_DIR_NAME = ".cursor";
3649
+ var CURSOR_HOOKS_FILE = "hooks.json";
3650
+ function getCursorUserDir(homeDir = os3.homedir()) {
3651
+ return path9.join(homeDir, CURSOR_DIR_NAME);
3652
+ }
3653
+ function getCursorHooksPath(homeDir = os3.homedir()) {
3654
+ return path9.join(getCursorUserDir(homeDir), CURSOR_HOOKS_FILE);
3655
+ }
3656
+
3657
+ // src/monitor/plugins/cursor/hooks-config.ts
3658
+ var OLAKAI_HOOK_MARKER3 = "olakai monitor hook --tool cursor";
3659
+ var CURSOR_HOOK_DEFINITIONS = {
3660
+ beforeSubmitPrompt: [
3661
+ {
3662
+ command: "olakai monitor hook --tool cursor beforeSubmitPrompt",
3663
+ timeout: 5
3664
+ }
3665
+ ],
3666
+ afterAgentResponse: [
3667
+ {
3668
+ command: "olakai monitor hook --tool cursor afterAgentResponse",
3669
+ timeout: 5
3670
+ }
3671
+ ],
3672
+ sessionEnd: [
3673
+ {
3674
+ command: "olakai monitor hook --tool cursor sessionEnd",
3675
+ timeout: 5
3676
+ }
3677
+ ],
3678
+ stop: [
3679
+ {
3680
+ command: "olakai monitor hook --tool cursor stop",
3681
+ timeout: 5
3682
+ }
3683
+ ]
3684
+ };
3685
+ var CURSOR_HOOKS_VERSION = 1;
3686
+ function mergeCursorHooks(existing, definitions = CURSOR_HOOK_DEFINITIONS) {
3687
+ const base = existing ? { ...existing } : {};
3688
+ if (typeof base.version !== "number") {
3689
+ base.version = CURSOR_HOOKS_VERSION;
3690
+ }
3691
+ const mergedHooks = {
3692
+ ...base.hooks ?? {}
3693
+ };
3694
+ for (const [event, defaultEntries] of Object.entries(definitions)) {
3695
+ const existingEntries = mergedHooks[event] ?? [];
3696
+ const hasOlakaiEntry = existingEntries.some(
3697
+ (e) => typeof e?.command === "string" && e.command.includes(OLAKAI_HOOK_MARKER3)
3698
+ );
3699
+ if (hasOlakaiEntry) {
3700
+ mergedHooks[event] = existingEntries;
3701
+ } else {
3702
+ mergedHooks[event] = [...existingEntries, ...defaultEntries];
3703
+ }
3704
+ }
3705
+ return {
3706
+ ...base,
3707
+ hooks: mergedHooks
3708
+ };
3709
+ }
3710
+ function removeOlakaiCursorHooks(existing) {
3711
+ const base = existing ? { ...existing } : {};
3712
+ const sourceHooks = base.hooks ?? {};
3713
+ const cleaned = {};
3714
+ for (const [event, entries] of Object.entries(sourceHooks)) {
3715
+ const filtered = entries.filter(
3716
+ (e) => typeof e?.command !== "string" || !e.command.includes(OLAKAI_HOOK_MARKER3)
3717
+ );
3718
+ if (filtered.length > 0) {
3719
+ cleaned[event] = filtered;
3720
+ }
3721
+ }
3722
+ if (Object.keys(cleaned).length > 0) {
3723
+ base.hooks = cleaned;
3724
+ } else {
3725
+ delete base.hooks;
3726
+ }
3727
+ return base;
3728
+ }
3729
+ function hasOlakaiCursorHooks(config) {
3730
+ if (!config?.hooks) return false;
3731
+ return Object.values(config.hooks).some(
3732
+ (entries) => entries.some(
3733
+ (e) => typeof e?.command === "string" && e.command.includes(OLAKAI_HOOK_MARKER3)
3734
+ )
3735
+ );
3736
+ }
3737
+
3738
+ // src/monitor/plugins/cursor/install.ts
3739
+ var CURSOR_SOURCE = "cursor";
3740
+ var CURSOR_AGENT_SOURCE = "CURSOR";
3741
+ var CURSOR_AGENT_CATEGORY = "CODING";
3742
+ function readJsonFileTolerant(filePath) {
3743
+ try {
3744
+ if (!fs11.existsSync(filePath)) return null;
3745
+ const raw = fs11.readFileSync(filePath, "utf-8");
3746
+ return JSON.parse(raw);
3747
+ } catch {
3748
+ return null;
3749
+ }
3750
+ }
3751
+ function writeJsonFileWithDir(filePath, data) {
3752
+ const dir = path10.dirname(filePath);
3753
+ if (!fs11.existsSync(dir)) {
3754
+ fs11.mkdirSync(dir, { recursive: true });
3755
+ }
3756
+ fs11.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
3757
+ }
3758
+ async function installCursor(opts) {
3759
+ const projectRoot = opts.projectRoot ?? process.cwd();
3760
+ const homeDir = opts.homeDir ?? os4.homedir();
3761
+ const token = getValidToken();
3762
+ if (!token) {
3763
+ console.error("Not logged in. Run 'olakai login' first.");
3764
+ process.exit(1);
3765
+ }
3766
+ console.log("Setting up Cursor monitoring for this workspace...\n");
3767
+ console.log(
3768
+ "Note: Cursor hooks are installed globally per user (~/.cursor/hooks.json)."
3769
+ );
3770
+ console.log(
3771
+ `Activity from this workspace (${projectRoot}) will be associated with`
3772
+ );
3773
+ console.log("the agent you select below.\n");
3774
+ const dirName = path10.basename(projectRoot);
3775
+ const agent = await chooseOrCreateAgent(dirName);
3776
+ const monitoringEndpoint = `${getBaseUrl()}/api/monitoring/prompt`;
3777
+ let apiKey = agent.apiKey?.key;
3778
+ if (!apiKey) {
3779
+ console.log(
3780
+ "\nThe API key for this agent is not available (it is only shown once at creation)."
3781
+ );
3782
+ apiKey = await promptUser("Paste the API key for this agent: ");
3783
+ if (!apiKey) {
3784
+ console.error("API key is required for monitoring.");
3785
+ process.exit(1);
3786
+ }
3787
+ await validatePastedApiKey({
3788
+ monitoringEndpoint,
3789
+ apiKey,
3790
+ expectedAgentId: agent.id,
3791
+ expectedAgentName: agent.name,
3792
+ expectedApiKeyId: agent.apiKey?.id ?? null
3793
+ });
3794
+ }
3795
+ const cursorDir = getCursorUserDir(homeDir);
3796
+ if (!fs11.existsSync(cursorDir)) {
3797
+ fs11.mkdirSync(cursorDir, { recursive: true });
3798
+ }
3799
+ const hooksPath = getCursorHooksPath(homeDir);
3800
+ const existingHooks = readJsonFileTolerant(hooksPath);
3801
+ const mergedHooks = mergeCursorHooks(existingHooks);
3802
+ writeJsonFileWithDir(hooksPath, mergedHooks);
3803
+ const monitorConfig = {
3804
+ agentId: agent.id,
3805
+ apiKey,
3806
+ agentName: agent.name,
3807
+ source: CURSOR_SOURCE,
3808
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3809
+ monitoringEndpoint
3810
+ };
3811
+ writeCursorConfig(projectRoot, monitorConfig);
3812
+ const configPath = getCursorConfigPath(projectRoot);
3813
+ const configRel = path10.relative(projectRoot, configPath);
3814
+ const hooksDisplay = `~/${path10.join(CURSOR_DIR_NAME, CURSOR_HOOKS_FILE)}`;
3815
+ console.log("");
3816
+ console.log(`\u2713 Agent "${agent.name}" configured (ID: ${agent.id})`);
3817
+ if (agent.apiKey?.key) {
3818
+ console.log("\u2713 API key generated");
3819
+ }
3820
+ console.log(`\u2713 Cursor hooks installed at ${hooksDisplay}`);
3821
+ console.log(`\u2713 Monitor config saved to ${configRel}`);
3822
+ console.log("");
3823
+ console.log(
3824
+ "Monitoring is now active. Restart Cursor for the new hooks to take effect."
3825
+ );
3826
+ console.log(`View activity at: ${getBaseUrl()}/dashboard`);
3827
+ console.log("");
3828
+ console.log(
3829
+ `\u26A0 Ensure ${OLAKAI_DIR}/ is in your .gitignore (it contains your API key)`
3830
+ );
3831
+ console.log("");
3832
+ console.log("To check status: olakai monitor status --tool cursor");
3833
+ console.log("To disable: olakai monitor disable --tool cursor");
3834
+ return {
3835
+ agentId: agent.id,
3836
+ agentName: agent.name,
3837
+ source: CURSOR_SOURCE,
3838
+ monitoringEndpoint
3839
+ };
3840
+ }
3841
+ async function chooseOrCreateAgent(defaultName) {
3842
+ const choice = await promptUser(
3843
+ "Create a new agent or use an existing one? (new/existing) [new]: "
3844
+ );
3845
+ if (choice.toLowerCase() === "existing" || choice.toLowerCase() === "e") {
3846
+ const agents = await listAgents();
3847
+ if (agents.length === 0) {
3848
+ console.log("No agents found. Creating a new one instead.\n");
3849
+ return createNewAgent3(defaultName);
3850
+ }
3851
+ console.log("\nAvailable agents:");
3852
+ for (let i = 0; i < agents.length; i++) {
3853
+ console.log(
3854
+ ` ${i + 1}. ${agents[i].name} (${agents[i].id.slice(0, 12)}...)`
3855
+ );
3856
+ }
3857
+ const selection = await promptUser(`
3858
+ Select agent (1-${agents.length}): `);
3859
+ const idx = parseInt(selection, 10) - 1;
3860
+ if (isNaN(idx) || idx < 0 || idx >= agents.length) {
3861
+ console.error("Invalid selection.");
3862
+ process.exit(1);
3863
+ }
3864
+ const fetched = await getAgent(agents[idx].id);
3865
+ if (!fetched.apiKey?.key && !fetched.apiKey?.isActive) {
3866
+ console.log(
3867
+ "\nThis agent has no active API key. Please create a new agent instead,"
3868
+ );
3869
+ console.log("or generate an API key via the Olakai dashboard.\n");
3870
+ const createNew = await promptUser("Create a new agent? (y/n) [y]: ");
3871
+ if (createNew.toLowerCase() !== "n") {
3872
+ return createNewAgent3(defaultName);
3873
+ }
3874
+ process.exit(1);
3875
+ }
3876
+ return fetched;
3877
+ }
3878
+ return createNewAgent3(defaultName);
3879
+ }
3880
+ async function createNewAgent3(defaultName) {
3881
+ const nameInput = await promptUser(`Agent name [${defaultName}]: `);
3882
+ const agentName = nameInput || defaultName;
3883
+ return createAgent({
3884
+ name: agentName,
3885
+ description: `Cursor local agent for ${agentName}`,
3886
+ role: "WORKER",
3887
+ createApiKey: true,
3888
+ category: CURSOR_AGENT_CATEGORY,
3889
+ source: CURSOR_AGENT_SOURCE
3890
+ });
3891
+ }
3892
+ async function uninstallCursor(opts) {
3893
+ const projectRoot = opts.projectRoot ?? process.cwd();
3894
+ const homeDir = opts.homeDir ?? os4.homedir();
3895
+ const hooksPath = getCursorHooksPath(homeDir);
3896
+ const existingHooks = readJsonFileTolerant(hooksPath);
3897
+ if (existingHooks) {
3898
+ const cleaned = removeOlakaiCursorHooks(existingHooks);
3899
+ if (!cleaned.hooks || Object.keys(cleaned.hooks).length === 0) {
3900
+ const otherKeys = Object.keys(cleaned).filter(
3901
+ (k) => k !== "hooks" && k !== "version"
3902
+ );
3903
+ const onlyDefaults = otherKeys.length === 0 && (cleaned.version === 1 || cleaned.version === void 0);
3904
+ if (onlyDefaults) {
3905
+ try {
3906
+ fs11.unlinkSync(hooksPath);
3907
+ } catch {
3908
+ }
3909
+ } else {
3910
+ writeJsonFileWithDir(hooksPath, cleaned);
3911
+ }
3912
+ } else {
3913
+ writeJsonFileWithDir(hooksPath, cleaned);
3914
+ }
3915
+ console.log(
3916
+ `\u2713 Olakai hooks removed from ~/${path10.join(CURSOR_DIR_NAME, CURSOR_HOOKS_FILE)}`
3917
+ );
3918
+ } else {
3919
+ console.log("No Cursor hooks file found.");
3920
+ }
3921
+ if (!opts.keepConfig) {
3922
+ const configPath = getCursorConfigPath(projectRoot);
3923
+ const configRel = path10.relative(projectRoot, configPath);
3924
+ if (deleteCursorConfig(projectRoot)) {
3925
+ console.log(`\u2713 Monitor config removed (${configRel})`);
3926
+ }
3927
+ } else {
3928
+ const configPath = getCursorConfigPath(projectRoot);
3929
+ const configRel = path10.relative(projectRoot, configPath);
3930
+ console.log(`Monitor config retained at ${configRel}`);
3931
+ }
3932
+ console.log("");
3933
+ console.log(
3934
+ "Monitoring disabled. Run 'olakai monitor init --tool cursor' to re-enable."
3935
+ );
3936
+ console.log("Restart Cursor for the change to take effect.");
3937
+ }
3938
+
3939
+ // src/monitor/plugins/cursor/status.ts
3940
+ import * as fs12 from "fs";
3941
+ import * as os5 from "os";
3942
+ import * as path11 from "path";
3943
+ async function getCursorStatus(opts) {
3944
+ const projectRoot = opts?.projectRoot ?? process.cwd();
3945
+ const homeDir = opts?.homeDir ?? os5.homedir();
3946
+ const configPath = getCursorConfigPath(projectRoot);
3947
+ const config = loadCursorConfig(projectRoot);
3948
+ let hooksConfigured = false;
3949
+ const hooksPath = getCursorHooksPath(homeDir);
3950
+ if (fs12.existsSync(hooksPath)) {
3951
+ try {
3952
+ const raw = fs12.readFileSync(hooksPath, "utf-8");
3953
+ const parsed = JSON.parse(raw);
3954
+ hooksConfigured = hasOlakaiCursorHooks(parsed);
3955
+ } catch {
2874
3956
  }
3957
+ }
3958
+ if (!config) {
3959
+ return {
3960
+ toolId: "cursor",
3961
+ configured: false,
3962
+ hooksConfigured,
3963
+ configPath,
3964
+ notes: hooksConfigured ? [
3965
+ "Cursor hooks installed but no monitor config in this workspace \u2014 run 'olakai monitor init --tool cursor' to associate it with an agent."
3966
+ ] : []
3967
+ };
3968
+ }
3969
+ return {
3970
+ toolId: "cursor",
3971
+ configured: true,
3972
+ hooksConfigured,
3973
+ agentId: config.agentId,
3974
+ agentName: config.agentName,
3975
+ source: config.source,
3976
+ apiKeyMasked: config.apiKey.slice(0, 12) + "...",
3977
+ monitoringEndpoint: config.monitoringEndpoint,
3978
+ configuredAt: config.createdAt,
3979
+ configPath
3980
+ };
3981
+ }
3982
+
3983
+ // src/monitor/plugins/cursor/hook.ts
3984
+ var SUPPORTED_CURSOR_EVENTS = /* @__PURE__ */ new Set([
3985
+ "beforeSubmitPrompt",
3986
+ "afterAgentResponse",
3987
+ "sessionEnd",
3988
+ "stop"
3989
+ ]);
3990
+ function asString(value) {
3991
+ return typeof value === "string" && value.trim() ? value : void 0;
3992
+ }
3993
+ function asNumber(value) {
3994
+ if (typeof value !== "number") return 0;
3995
+ if (!Number.isFinite(value)) return 0;
3996
+ return value;
3997
+ }
3998
+ function extractCursorTokens(payload) {
3999
+ return {
4000
+ inputTokens: asNumber(payload.input_tokens),
4001
+ outputTokens: asNumber(payload.output_tokens),
4002
+ cacheReadTokens: asNumber(payload.cache_read_tokens),
4003
+ cacheWriteTokens: asNumber(payload.cache_write_tokens)
4004
+ };
4005
+ }
4006
+ function asStringArray(value) {
4007
+ if (!Array.isArray(value)) return void 0;
4008
+ const out = [];
4009
+ for (const item of value) {
4010
+ if (typeof item === "string" && item.trim()) out.push(item);
4011
+ }
4012
+ return out.length > 0 ? out : void 0;
4013
+ }
4014
+ function extractPromptText(payload) {
4015
+ if (typeof payload.prompt === "string") return payload.prompt;
4016
+ if (payload.prompt && typeof payload.prompt === "object" && typeof payload.prompt.text === "string") {
4017
+ return payload.prompt.text;
4018
+ }
4019
+ return "";
4020
+ }
4021
+ function extractResponseText(payload) {
4022
+ if (typeof payload.response === "string") return payload.response;
4023
+ if (payload.response && typeof payload.response === "object" && typeof payload.response.text === "string") {
4024
+ return payload.response.text;
4025
+ }
4026
+ if (typeof payload.text === "string") return payload.text;
4027
+ return "";
4028
+ }
4029
+ function extractAttachments(payload) {
4030
+ if (Array.isArray(payload.attachments)) return payload.attachments;
4031
+ if (payload.prompt && typeof payload.prompt === "object" && Array.isArray(payload.prompt.attachments)) {
4032
+ return payload.prompt.attachments;
4033
+ }
4034
+ return void 0;
4035
+ }
4036
+ function buildPairedPayload(inputs, config) {
4037
+ const customData = {
4038
+ hookEvent: "afterAgentResponse",
4039
+ conversationId: inputs.conversationId
4040
+ };
4041
+ if (inputs.generationId) customData.generationId = inputs.generationId;
4042
+ if (inputs.cursorVersion) customData.cursorVersion = inputs.cursorVersion;
4043
+ if (inputs.workspaceRoots) customData.workspaceRoots = inputs.workspaceRoots;
4044
+ if (inputs.transcriptPath) customData.transcriptPath = inputs.transcriptPath;
4045
+ if (inputs.attachments && inputs.attachments.length > 0) {
4046
+ customData.attachmentCount = inputs.attachments.length;
4047
+ }
4048
+ if (inputs.userEmail) customData.userEmail = inputs.userEmail;
4049
+ const inputTokens = asNumber(inputs.inputTokens);
4050
+ const outputTokens = asNumber(inputs.outputTokens);
4051
+ const cacheReadTokens = asNumber(inputs.cacheReadTokens);
4052
+ const cacheWriteTokens = asNumber(inputs.cacheWriteTokens);
4053
+ customData.inputTokens = inputTokens;
4054
+ customData.outputTokens = outputTokens;
4055
+ customData.cacheReadTokens = cacheReadTokens;
4056
+ customData.cacheWriteTokens = cacheWriteTokens;
4057
+ if (inputs.unknownFields && Object.keys(inputs.unknownFields).length > 0) {
4058
+ customData.unknownPayloadFields = inputs.unknownFields;
4059
+ }
4060
+ return {
4061
+ prompt: inputs.prompt,
4062
+ response: inputs.response,
4063
+ chatId: inputs.conversationId,
4064
+ source: config.source,
4065
+ modelName: inputs.model,
4066
+ tokens: inputTokens + outputTokens,
4067
+ customData
4068
+ };
4069
+ }
4070
+ function buildOrphanPayload(inputs, config, reason = "orphan-prompt") {
4071
+ const base = buildPairedPayload({ ...inputs, response: "" }, config);
4072
+ return {
4073
+ ...base,
4074
+ customData: {
4075
+ ...base.customData,
4076
+ hookEvent: "sessionEnd",
4077
+ partial: true,
4078
+ partialReason: reason
4079
+ }
4080
+ };
4081
+ }
4082
+ var KNOWN_PAYLOAD_KEYS = /* @__PURE__ */ new Set([
4083
+ "event",
4084
+ "conversation_id",
4085
+ "generation_id",
4086
+ "model",
4087
+ "cursor_version",
4088
+ "workspace_roots",
4089
+ "user_email",
4090
+ "transcript_path",
4091
+ "prompt",
4092
+ "response",
4093
+ "text",
4094
+ "attachments",
4095
+ "input_tokens",
4096
+ "output_tokens",
4097
+ "cache_read_tokens",
4098
+ "cache_write_tokens"
4099
+ ]);
4100
+ function collectUnknownFields(payload) {
4101
+ const out = {};
4102
+ for (const key of Object.keys(payload)) {
4103
+ if (!KNOWN_PAYLOAD_KEYS.has(key)) {
4104
+ out[key] = payload[key];
4105
+ }
4106
+ }
4107
+ return Object.keys(out).length > 0 ? out : void 0;
4108
+ }
4109
+ function normalizeEventName(event) {
4110
+ const lower = event.trim();
4111
+ if (!lower) return null;
4112
+ const exact = lower;
4113
+ if (SUPPORTED_CURSOR_EVENTS.has(exact)) return exact;
4114
+ for (const known of SUPPORTED_CURSOR_EVENTS) {
4115
+ if (known.toLowerCase() === lower.toLowerCase()) return known;
4116
+ }
4117
+ return null;
4118
+ }
4119
+ function extractCursorMeta(payload) {
4120
+ return {
4121
+ conversationId: asString(payload.conversation_id),
4122
+ generationId: asString(payload.generation_id),
4123
+ model: asString(payload.model),
4124
+ cursorVersion: asString(payload.cursor_version),
4125
+ workspaceRoots: asStringArray(payload.workspace_roots),
4126
+ userEmail: asString(payload.user_email),
4127
+ transcriptPath: asString(payload.transcript_path)
4128
+ };
4129
+ }
4130
+
4131
+ // src/monitor/plugins/cursor/pairing-state.ts
4132
+ import * as fs13 from "fs";
4133
+ import * as os6 from "os";
4134
+ import * as path12 from "path";
4135
+ var STATE_DIR_SEGMENTS2 = [".olakai", "cursor-pairings"];
4136
+ function getPairingsDir(homeDir) {
4137
+ return path12.join(homeDir, ...STATE_DIR_SEGMENTS2);
4138
+ }
4139
+ function sanitizeKeyFragment(value) {
4140
+ return value.replace(/[^A-Za-z0-9._-]/g, "_");
4141
+ }
4142
+ function getPairingKey(conversationId, generationId) {
4143
+ const conv = sanitizeKeyFragment(conversationId);
4144
+ if (!generationId) return conv;
4145
+ return `${conv}__${sanitizeKeyFragment(generationId)}`;
4146
+ }
4147
+ function getPairingFile(conversationId, generationId, homeDir) {
4148
+ return path12.join(
4149
+ getPairingsDir(homeDir),
4150
+ `${getPairingKey(conversationId, generationId)}.json`
4151
+ );
4152
+ }
4153
+ function stashPendingPrompt(pending, homeDir = os6.homedir()) {
4154
+ if (!pending.conversationId) return;
4155
+ const dir = getPairingsDir(homeDir);
4156
+ try {
4157
+ if (!fs13.existsSync(dir)) {
4158
+ fs13.mkdirSync(dir, { recursive: true });
4159
+ }
4160
+ const filePath = getPairingFile(
4161
+ pending.conversationId,
4162
+ pending.generationId,
4163
+ homeDir
4164
+ );
4165
+ fs13.writeFileSync(
4166
+ filePath,
4167
+ JSON.stringify(pending, null, 2) + "\n",
4168
+ "utf-8"
4169
+ );
2875
4170
  } catch {
2876
4171
  }
2877
4172
  }
2878
- async function disableCommand(options) {
2879
- const projectRoot = findProjectRoot();
2880
- const settingsPath = getSettingsPath(projectRoot);
2881
- const settings = readJsonFile(settingsPath);
2882
- if (settings?.hooks) {
2883
- const cleanedHooks = {};
2884
- for (const [event, entries] of Object.entries(settings.hooks)) {
2885
- const filtered = entries.filter(
2886
- (e) => !e.hooks.some((h) => h.command.includes(OLAKAI_HOOK_MARKER))
4173
+ function takePendingPrompt(conversationId, generationId, homeDir = os6.homedir()) {
4174
+ if (!conversationId) return null;
4175
+ const candidates = [];
4176
+ if (generationId) {
4177
+ candidates.push(getPairingFile(conversationId, generationId, homeDir));
4178
+ }
4179
+ candidates.push(getPairingFile(conversationId, void 0, homeDir));
4180
+ for (const filePath of candidates) {
4181
+ let raw;
4182
+ try {
4183
+ if (!fs13.existsSync(filePath)) continue;
4184
+ raw = fs13.readFileSync(filePath, "utf-8");
4185
+ } catch {
4186
+ continue;
4187
+ }
4188
+ try {
4189
+ fs13.unlinkSync(filePath);
4190
+ } catch {
4191
+ }
4192
+ try {
4193
+ const parsed = JSON.parse(raw);
4194
+ if (parsed && typeof parsed.conversationId === "string") {
4195
+ return parsed;
4196
+ }
4197
+ } catch {
4198
+ }
4199
+ }
4200
+ return null;
4201
+ }
4202
+ function listPendingPrompts(homeDir = os6.homedir()) {
4203
+ const dir = getPairingsDir(homeDir);
4204
+ if (!fs13.existsSync(dir)) return [];
4205
+ let files;
4206
+ try {
4207
+ files = fs13.readdirSync(dir);
4208
+ } catch {
4209
+ return [];
4210
+ }
4211
+ const result = [];
4212
+ for (const name of files) {
4213
+ if (!name.endsWith(".json")) continue;
4214
+ const filePath = path12.join(dir, name);
4215
+ try {
4216
+ const raw = fs13.readFileSync(filePath, "utf-8");
4217
+ const parsed = JSON.parse(raw);
4218
+ if (parsed && typeof parsed.conversationId === "string") {
4219
+ result.push(parsed);
4220
+ }
4221
+ } catch {
4222
+ }
4223
+ }
4224
+ return result;
4225
+ }
4226
+ function clearPendingPrompt(conversationId, generationId, homeDir = os6.homedir()) {
4227
+ if (!conversationId) return;
4228
+ const candidates = [];
4229
+ if (generationId) {
4230
+ candidates.push(getPairingFile(conversationId, generationId, homeDir));
4231
+ }
4232
+ candidates.push(getPairingFile(conversationId, void 0, homeDir));
4233
+ for (const filePath of candidates) {
4234
+ try {
4235
+ if (fs13.existsSync(filePath)) {
4236
+ fs13.unlinkSync(filePath);
4237
+ }
4238
+ } catch {
4239
+ }
4240
+ }
4241
+ }
4242
+
4243
+ // src/monitor/plugins/cursor/index.ts
4244
+ var TOOL_ID3 = "cursor";
4245
+ function debugLog3(label, data) {
4246
+ if (process.env.OLAKAI_MONITOR_DEBUG !== "1") return;
4247
+ try {
4248
+ const logPath = `/tmp/olakai-monitor-debug-${process.pid}.log`;
4249
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] cursor:${label}: ${typeof data === "string" ? data : JSON.stringify(data, null, 2)}
4250
+ `;
4251
+ fs14.appendFileSync(logPath, line, "utf-8");
4252
+ } catch {
4253
+ }
4254
+ }
4255
+ function resolveCursorProjectRoot(payload, fallbackCwd = process.cwd()) {
4256
+ const roots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots.filter(
4257
+ (r) => typeof r === "string" && r.trim().length > 0
4258
+ ) : [];
4259
+ const candidates = roots.length > 0 ? roots : [fallbackCwd];
4260
+ for (const candidate of candidates) {
4261
+ const found = findConfiguredWorkspace(candidate, [TOOL_ID3]);
4262
+ if (found) return found;
4263
+ }
4264
+ return null;
4265
+ }
4266
+ async function handleCursorHook(eventName, payloadJson, opts = {}) {
4267
+ const event = normalizeEventName(eventName);
4268
+ if (!event) {
4269
+ debugLog3("hook-unknown-event", eventName);
4270
+ return null;
4271
+ }
4272
+ const payload = payloadJson && typeof payloadJson === "object" ? payloadJson : {};
4273
+ debugLog3("event-parsed", { event, payload });
4274
+ const meta = extractCursorMeta(payload);
4275
+ const homeDir = opts.homeDir ?? os7.homedir();
4276
+ switch (event) {
4277
+ case "beforeSubmitPrompt": {
4278
+ if (!meta.conversationId) {
4279
+ debugLog3("missing-conversation-id", { event });
4280
+ return null;
4281
+ }
4282
+ stashPendingPrompt(
4283
+ {
4284
+ prompt: extractPromptText(payload),
4285
+ userEmail: meta.userEmail,
4286
+ model: meta.model,
4287
+ cursorVersion: meta.cursorVersion,
4288
+ conversationId: meta.conversationId,
4289
+ generationId: meta.generationId,
4290
+ workspaceRoots: meta.workspaceRoots,
4291
+ transcriptPath: meta.transcriptPath,
4292
+ attachments: extractAttachments(payload),
4293
+ stashedAt: (/* @__PURE__ */ new Date()).toISOString(),
4294
+ extra: collectUnknownFields(payload)
4295
+ },
4296
+ homeDir
2887
4297
  );
2888
- if (filtered.length > 0) {
2889
- cleanedHooks[event] = filtered;
4298
+ return null;
4299
+ }
4300
+ case "afterAgentResponse": {
4301
+ if (!meta.conversationId) {
4302
+ debugLog3("missing-conversation-id", { event });
4303
+ return null;
4304
+ }
4305
+ const projectRoot = resolveCursorProjectRoot(
4306
+ payload,
4307
+ opts.projectRoot ?? process.cwd()
4308
+ );
4309
+ if (!projectRoot) {
4310
+ debugLog3("config-not-found", {
4311
+ workspaceRoots: meta.workspaceRoots,
4312
+ fallbackCwd: opts.projectRoot ?? process.cwd()
4313
+ });
4314
+ takePendingPrompt(meta.conversationId, meta.generationId, homeDir);
4315
+ return null;
4316
+ }
4317
+ const config = loadCursorConfig(projectRoot);
4318
+ if (!config) {
4319
+ debugLog3("config-load-failed", { projectRoot });
4320
+ takePendingPrompt(meta.conversationId, meta.generationId, homeDir);
4321
+ return null;
2890
4322
  }
4323
+ const stashed = takePendingPrompt(
4324
+ meta.conversationId,
4325
+ meta.generationId,
4326
+ homeDir
4327
+ );
4328
+ const responseText = extractResponseText(payload);
4329
+ const promptText = stashed?.prompt ?? extractPromptText(payload);
4330
+ const userEmail = meta.userEmail ?? stashed?.userEmail;
4331
+ const model = meta.model ?? stashed?.model;
4332
+ const cursorVersion = meta.cursorVersion ?? stashed?.cursorVersion;
4333
+ const workspaceRoots = meta.workspaceRoots ?? stashed?.workspaceRoots;
4334
+ const transcriptPath = meta.transcriptPath ?? stashed?.transcriptPath;
4335
+ const attachments = extractAttachments(payload) ?? stashed?.attachments;
4336
+ const tokens = extractCursorTokens(payload);
4337
+ const unknownFields = mergeUnknownFields(
4338
+ collectUnknownFields(payload),
4339
+ stashed?.extra
4340
+ );
4341
+ const built = buildPairedPayload(
4342
+ {
4343
+ conversationId: meta.conversationId,
4344
+ generationId: meta.generationId ?? stashed?.generationId,
4345
+ prompt: promptText,
4346
+ response: responseText,
4347
+ userEmail,
4348
+ model,
4349
+ cursorVersion,
4350
+ workspaceRoots,
4351
+ transcriptPath,
4352
+ attachments,
4353
+ inputTokens: tokens.inputTokens,
4354
+ outputTokens: tokens.outputTokens,
4355
+ cacheReadTokens: tokens.cacheReadTokens,
4356
+ cacheWriteTokens: tokens.cacheWriteTokens,
4357
+ unknownFields
4358
+ },
4359
+ config
4360
+ );
4361
+ const finalPayload = userEmail ? { ...built, email: userEmail } : built;
4362
+ debugLog3("payload-built", finalPayload);
4363
+ return {
4364
+ payload: finalPayload,
4365
+ transport: {
4366
+ endpoint: config.monitoringEndpoint,
4367
+ apiKey: config.apiKey,
4368
+ projectRoot
4369
+ }
4370
+ };
2891
4371
  }
2892
- if (Object.keys(cleanedHooks).length > 0) {
2893
- settings.hooks = cleanedHooks;
2894
- } else {
2895
- delete settings.hooks;
4372
+ case "sessionEnd":
4373
+ case "stop": {
4374
+ const orphan = pickOrphanForFlush(meta.conversationId, homeDir);
4375
+ if (!orphan) return null;
4376
+ const projectRoot = resolveCursorProjectRoot(
4377
+ // Synthesize a payload-like for resolution — orphan carries
4378
+ // the workspace_roots from the stashed beforeSubmitPrompt.
4379
+ {
4380
+ workspace_roots: orphan.workspaceRoots
4381
+ },
4382
+ opts.projectRoot ?? process.cwd()
4383
+ );
4384
+ if (!projectRoot) {
4385
+ debugLog3("orphan-config-not-found", {
4386
+ conversationId: orphan.conversationId
4387
+ });
4388
+ clearPendingPrompt(
4389
+ orphan.conversationId,
4390
+ orphan.generationId,
4391
+ homeDir
4392
+ );
4393
+ return null;
4394
+ }
4395
+ const config = loadCursorConfig(projectRoot);
4396
+ if (!config) {
4397
+ debugLog3("orphan-config-load-failed", { projectRoot });
4398
+ clearPendingPrompt(
4399
+ orphan.conversationId,
4400
+ orphan.generationId,
4401
+ homeDir
4402
+ );
4403
+ return null;
4404
+ }
4405
+ clearPendingPrompt(
4406
+ orphan.conversationId,
4407
+ orphan.generationId,
4408
+ homeDir
4409
+ );
4410
+ const built = buildOrphanPayload(
4411
+ {
4412
+ conversationId: orphan.conversationId,
4413
+ generationId: orphan.generationId,
4414
+ prompt: orphan.prompt,
4415
+ response: "",
4416
+ userEmail: orphan.userEmail,
4417
+ model: orphan.model,
4418
+ cursorVersion: orphan.cursorVersion,
4419
+ workspaceRoots: orphan.workspaceRoots,
4420
+ transcriptPath: orphan.transcriptPath,
4421
+ attachments: orphan.attachments,
4422
+ unknownFields: orphan.extra
4423
+ },
4424
+ config,
4425
+ event === "sessionEnd" ? "session-end" : "orphan-prompt"
4426
+ );
4427
+ const finalPayload = orphan.userEmail ? { ...built, email: orphan.userEmail } : built;
4428
+ return {
4429
+ payload: finalPayload,
4430
+ transport: {
4431
+ endpoint: config.monitoringEndpoint,
4432
+ apiKey: config.apiKey,
4433
+ projectRoot
4434
+ }
4435
+ };
2896
4436
  }
2897
- writeJsonFile(settingsPath, settings);
2898
- console.log(`\u2713 Olakai hooks removed from ${CLAUDE_DIR}/${SETTINGS_FILE}`);
2899
- } else {
2900
- console.log("No hooks found in settings.json.");
2901
4437
  }
2902
- if (!options.keepConfig) {
2903
- const configPath = getMonitorConfigPath(projectRoot);
2904
- if (fs3.existsSync(configPath)) {
2905
- fs3.unlinkSync(configPath);
2906
- console.log(`\u2713 Monitor config removed (${CLAUDE_DIR}/${MONITOR_CONFIG_FILE})`);
4438
+ }
4439
+ function mergeUnknownFields(primary, fallback) {
4440
+ if (!primary && !fallback) return void 0;
4441
+ return { ...fallback ?? {}, ...primary ?? {} };
4442
+ }
4443
+ function pickOrphanForFlush(conversationId, homeDir) {
4444
+ const pending = listPendingPrompts(homeDir);
4445
+ if (pending.length === 0) return null;
4446
+ if (conversationId) {
4447
+ const match = pending.find((p) => p.conversationId === conversationId);
4448
+ if (match) return match;
4449
+ }
4450
+ return pending.sort(
4451
+ (a, b) => (a.stashedAt || "").localeCompare(b.stashedAt || "")
4452
+ )[0];
4453
+ }
4454
+ var cursorPlugin = {
4455
+ id: TOOL_ID3,
4456
+ displayName: "Cursor",
4457
+ install(opts) {
4458
+ return installCursor(opts);
4459
+ },
4460
+ uninstall(opts) {
4461
+ return uninstallCursor(opts);
4462
+ },
4463
+ status(opts) {
4464
+ return getCursorStatus(opts);
4465
+ },
4466
+ handleHook(eventName, payloadJson, opts) {
4467
+ return handleCursorHook(eventName, payloadJson, opts);
4468
+ },
4469
+ async detectInstalled() {
4470
+ try {
4471
+ if (fs14.existsSync(getCursorUserDir())) return true;
4472
+ return whichCursorOnPath();
4473
+ } catch {
4474
+ return false;
2907
4475
  }
2908
- } else {
2909
- console.log(
2910
- `Monitor config retained at ${CLAUDE_DIR}/${MONITOR_CONFIG_FILE}`
4476
+ }
4477
+ };
4478
+ function whichCursorOnPath() {
4479
+ const pathEnv = process.env.PATH;
4480
+ if (!pathEnv) return false;
4481
+ const sep = process.platform === "win32" ? ";" : ":";
4482
+ const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
4483
+ for (const dir of pathEnv.split(sep)) {
4484
+ for (const ext of exts) {
4485
+ const candidate = `${dir}/cursor${ext.toLowerCase()}`;
4486
+ try {
4487
+ if (fs14.existsSync(candidate)) return true;
4488
+ } catch {
4489
+ }
4490
+ }
4491
+ }
4492
+ return false;
4493
+ }
4494
+ registerPlugin(cursorPlugin);
4495
+
4496
+ // src/commands/monitor.ts
4497
+ function readStdin(timeoutMs = 3e3) {
4498
+ return new Promise((resolve) => {
4499
+ if (process.stdin.isTTY) {
4500
+ resolve("");
4501
+ return;
4502
+ }
4503
+ let data = "";
4504
+ const timer = setTimeout(() => {
4505
+ process.stdin.removeAllListeners();
4506
+ process.stdin.destroy();
4507
+ resolve(data);
4508
+ }, timeoutMs);
4509
+ process.stdin.setEncoding("utf-8");
4510
+ process.stdin.on("data", (chunk) => {
4511
+ data += chunk;
4512
+ });
4513
+ process.stdin.on("end", () => {
4514
+ clearTimeout(timer);
4515
+ resolve(data);
4516
+ });
4517
+ process.stdin.on("error", () => {
4518
+ clearTimeout(timer);
4519
+ resolve(data);
4520
+ });
4521
+ });
4522
+ }
4523
+ var DEFAULT_NON_INTERACTIVE_TOOL = "claude-code";
4524
+ async function resolveToolFromOptions(flag, context) {
4525
+ if (typeof flag === "string" && flag.length > 0) {
4526
+ if (!isToolId(flag)) {
4527
+ console.error(
4528
+ `Unknown --tool "${flag}". Supported: ${TOOL_IDS.join(", ")}`
4529
+ );
4530
+ process.exit(1);
4531
+ }
4532
+ return flag;
4533
+ }
4534
+ if (!isInteractive()) {
4535
+ console.error(
4536
+ `[olakai] --tool not provided; defaulting to "${DEFAULT_NON_INTERACTIVE_TOOL}". This default will be removed in a future version \u2014 please pass --tool <tool> explicitly.`
2911
4537
  );
4538
+ return DEFAULT_NON_INTERACTIVE_TOOL;
4539
+ }
4540
+ return promptForTool(context);
4541
+ }
4542
+ async function promptForTool(context) {
4543
+ const plugins = listPlugins();
4544
+ const detected = [];
4545
+ for (const p of plugins) {
4546
+ try {
4547
+ if (await p.detectInstalled()) detected.push(p.id);
4548
+ } catch {
4549
+ }
4550
+ }
4551
+ console.log("Which coding tool do you want to monitor?");
4552
+ for (let i = 0; i < plugins.length; i++) {
4553
+ const p = plugins[i];
4554
+ const flag = detected.includes(p.id) ? " (detected)" : "";
4555
+ console.log(` ${i + 1}. ${p.displayName} [${p.id}]${flag}`);
4556
+ }
4557
+ const defaultIdx = plugins.findIndex((p) => detected.includes(p.id));
4558
+ const defaultLabel = defaultIdx >= 0 ? `${defaultIdx + 1}` : "1";
4559
+ const answer = await promptUser(
4560
+ `Select a tool to ${context} (1-${plugins.length}) [${defaultLabel}]: `
4561
+ );
4562
+ const choice = answer.trim() === "" ? defaultLabel : answer.trim();
4563
+ const idx = parseInt(choice, 10) - 1;
4564
+ if (isNaN(idx) || idx < 0 || idx >= plugins.length) {
4565
+ console.error("Invalid selection.");
4566
+ process.exit(1);
4567
+ }
4568
+ return plugins[idx].id;
4569
+ }
4570
+ async function initCommand(options) {
4571
+ const toolId = await resolveToolFromOptions(options.tool, "init");
4572
+ const plugin = getPlugin(toolId);
4573
+ await runPluginAction(
4574
+ plugin,
4575
+ () => plugin.install({ projectRoot: process.cwd(), interactive: true })
4576
+ );
4577
+ }
4578
+ async function statusCommand(options) {
4579
+ const toolId = await resolveToolFromOptions(options.tool, "status");
4580
+ const plugin = getPlugin(toolId);
4581
+ if (options.json) {
4582
+ const report2 = await plugin.status({ projectRoot: process.cwd() });
4583
+ console.log(JSON.stringify(report2, null, 2));
4584
+ return;
4585
+ }
4586
+ if (plugin.id === "claude-code") {
4587
+ const { printClaudeCodeStatus } = await import("./status-PPSSB7FV.js");
4588
+ await printClaudeCodeStatus({ projectRoot: process.cwd() });
4589
+ return;
4590
+ }
4591
+ const report = await plugin.status({ projectRoot: process.cwd() });
4592
+ if (report.notes && report.notes.length > 0) {
4593
+ for (const note of report.notes) {
4594
+ console.log(note);
4595
+ }
4596
+ return;
4597
+ }
4598
+ console.log(JSON.stringify(report, null, 2));
4599
+ }
4600
+ async function disableCommand(options) {
4601
+ const toolId = await resolveToolFromOptions(options.tool, "disable");
4602
+ const plugin = getPlugin(toolId);
4603
+ await runPluginAction(
4604
+ plugin,
4605
+ () => plugin.uninstall({
4606
+ projectRoot: process.cwd(),
4607
+ keepConfig: options.keepConfig
4608
+ })
4609
+ );
4610
+ }
4611
+ async function runPluginAction(plugin, fn) {
4612
+ try {
4613
+ return await fn();
4614
+ } catch (err) {
4615
+ const e = err;
4616
+ if (e.name === "NotImplementedError") {
4617
+ console.error(`${plugin.displayName}: ${e.message}`);
4618
+ process.exit(1);
4619
+ }
4620
+ throw err;
4621
+ }
4622
+ }
4623
+ async function hookCommand(event, options, command) {
4624
+ try {
4625
+ const flagPresent = command.getOptionValueSource("tool") !== void 0;
4626
+ let toolId;
4627
+ if (flagPresent) {
4628
+ if (!options.tool || !isToolId(options.tool)) {
4629
+ debugInvalidToolFlag(options.tool, event);
4630
+ return;
4631
+ }
4632
+ toolId = options.tool;
4633
+ } else {
4634
+ toolId = "claude-code";
4635
+ }
4636
+ const plugin = getPlugin(toolId);
4637
+ const stdinData = await readStdin(3e3);
4638
+ let payloadJson = {};
4639
+ if (stdinData) {
4640
+ try {
4641
+ payloadJson = JSON.parse(stdinData);
4642
+ } catch {
4643
+ }
4644
+ }
4645
+ const result = await plugin.handleHook(event, payloadJson);
4646
+ if (!result) return;
4647
+ await postMonitoringPayload(result);
4648
+ } catch {
4649
+ }
4650
+ }
4651
+ function debugInvalidToolFlag(value, event) {
4652
+ if (process.env.OLAKAI_MONITOR_DEBUG !== "1") return;
4653
+ try {
4654
+ const logPath = `/tmp/olakai-monitor-debug-${process.pid}.log`;
4655
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] hook-invalid-tool-flag: ${JSON.stringify(
4656
+ { event, tool: value ?? null }
4657
+ )}
4658
+ `;
4659
+ fs15.appendFileSync(logPath, line, "utf-8");
4660
+ } catch {
4661
+ }
4662
+ }
4663
+ async function postMonitoringPayload(result) {
4664
+ const controller = new AbortController();
4665
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
4666
+ const debug = process.env.OLAKAI_MONITOR_DEBUG === "1";
4667
+ const logPath = `/tmp/olakai-monitor-debug-${process.pid}.log`;
4668
+ const log2 = (event, data) => {
4669
+ if (!debug) return;
4670
+ try {
4671
+ fs15.appendFileSync(
4672
+ logPath,
4673
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] dispatcher/${event}: ${JSON.stringify(data)}
4674
+ `,
4675
+ "utf-8"
4676
+ );
4677
+ } catch {
4678
+ }
4679
+ };
4680
+ try {
4681
+ log2("posting", {
4682
+ endpoint: result.transport.endpoint,
4683
+ apiKeyPresent: Boolean(result.transport.apiKey),
4684
+ apiKeyPrefix: result.transport.apiKey ? result.transport.apiKey.slice(0, 8) + "..." : null,
4685
+ bodyBytes: JSON.stringify([result.payload]).length
4686
+ });
4687
+ const response = await fetch(result.transport.endpoint, {
4688
+ method: "POST",
4689
+ headers: {
4690
+ "x-api-key": result.transport.apiKey,
4691
+ "Content-Type": "application/json"
4692
+ },
4693
+ body: JSON.stringify([result.payload]),
4694
+ signal: controller.signal
4695
+ });
4696
+ let bodyPreview = null;
4697
+ try {
4698
+ bodyPreview = (await response.text()).slice(0, 500);
4699
+ } catch {
4700
+ }
4701
+ log2("posted", { status: response.status, bodyPreview });
4702
+ } catch (err) {
4703
+ log2("post-error", {
4704
+ name: err instanceof Error ? err.name : "unknown",
4705
+ message: err instanceof Error ? err.message : String(err)
4706
+ });
4707
+ throw err;
4708
+ } finally {
4709
+ clearTimeout(timeoutId);
2912
4710
  }
2913
- console.log("");
2914
- console.log("Monitoring disabled. Run 'olakai monitor init' to re-enable.");
2915
4711
  }
2916
4712
  function registerMonitorCommand(program2) {
2917
- const monitor = program2.command("monitor").description("Monitor Claude Code sessions with Olakai");
2918
- monitor.command("init").description(
2919
- "Set up Olakai monitoring for this Claude Code workspace"
4713
+ const monitor = program2.command("monitor").description(
4714
+ "Monitor local coding agents (Claude Code, Codex, Cursor) with Olakai"
4715
+ );
4716
+ monitor.command("init").description("Set up Olakai monitoring for this workspace").option(
4717
+ "--tool <tool>",
4718
+ `Tool to configure (${TOOL_IDS.join("|")}). Prompts when omitted in interactive mode.`
2920
4719
  ).action(initCommand);
2921
- monitor.command("hook <event>").description("Hook handler called by Claude Code (internal use)").action(hookCommand);
2922
- monitor.command("status").description("Show monitoring status for this workspace").option("--json", "Output as JSON").action(statusCommand);
4720
+ monitor.command("hook <event>").description("Hook handler invoked by the tool's hook system (internal)").option(
4721
+ "--tool <tool>",
4722
+ `Tool whose hook is firing (defaults to claude-code for back-compat)`
4723
+ ).action(hookCommand);
4724
+ monitor.command("status").description("Show monitoring status for this workspace").option(
4725
+ "--tool <tool>",
4726
+ `Tool to inspect (${TOOL_IDS.join("|")}). Prompts when omitted in interactive mode.`
4727
+ ).option("--json", "Output as JSON").action(statusCommand);
2923
4728
  monitor.command("disable").description("Remove Olakai monitoring from this workspace").option(
4729
+ "--tool <tool>",
4730
+ `Tool to disable (${TOOL_IDS.join("|")}). Prompts when omitted in interactive mode.`
4731
+ ).option(
2924
4732
  "--keep-config",
2925
4733
  "Keep the monitor config file (only remove hooks)"
2926
4734
  ).action(disableCommand);
4735
+ for (const plugin of listPlugins()) {
4736
+ plugin.registerCommands?.(monitor);
4737
+ }
2927
4738
  }
2928
4739
 
2929
4740
  // src/index.ts