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