olakai-cli 0.1.15 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +335 -106
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -2183,9 +2183,197 @@ function registerActivityCommand(program2) {
|
|
|
2183
2183
|
}
|
|
2184
2184
|
|
|
2185
2185
|
// src/commands/monitor.ts
|
|
2186
|
+
import * as fs3 from "fs";
|
|
2187
|
+
import * as path3 from "path";
|
|
2188
|
+
import * as readline from "readline";
|
|
2189
|
+
|
|
2190
|
+
// src/commands/monitor-transcript.ts
|
|
2191
|
+
var SKILL_REGEX = /^\/([\w-]+)(?:\s|$)/;
|
|
2192
|
+
function detectSkill(userMessage) {
|
|
2193
|
+
if (typeof userMessage !== "string") return void 0;
|
|
2194
|
+
const trimmed = userMessage.trimStart();
|
|
2195
|
+
if (!trimmed) return void 0;
|
|
2196
|
+
const match = SKILL_REGEX.exec(trimmed);
|
|
2197
|
+
if (!match) return void 0;
|
|
2198
|
+
return match[1];
|
|
2199
|
+
}
|
|
2200
|
+
function extractTextContent(content) {
|
|
2201
|
+
if (typeof content === "string") return content;
|
|
2202
|
+
if (!Array.isArray(content)) return "";
|
|
2203
|
+
const parts = [];
|
|
2204
|
+
for (const block of content) {
|
|
2205
|
+
if (block?.type === "text" && typeof block.text === "string") {
|
|
2206
|
+
parts.push(block.text);
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
return parts.join("\n").trim();
|
|
2210
|
+
}
|
|
2211
|
+
function isMetaUserMessage(line, text) {
|
|
2212
|
+
if (line.isMeta === true) return true;
|
|
2213
|
+
if (!text) return true;
|
|
2214
|
+
return text.includes("<command-name>") || text.includes("<local-command-caveat>") || text.includes("<command-message>");
|
|
2215
|
+
}
|
|
2216
|
+
function parseTimestamp(ts) {
|
|
2217
|
+
if (typeof ts !== "string" || !ts) return NaN;
|
|
2218
|
+
return Date.parse(ts);
|
|
2219
|
+
}
|
|
2220
|
+
function isCompactionEntry(line) {
|
|
2221
|
+
if (line.type !== "system") return false;
|
|
2222
|
+
const subtype = line.subtype ?? "";
|
|
2223
|
+
return subtype === "compact_boundary" || subtype === "pre_compact" || subtype === "post_compact" || /compact/i.test(subtype);
|
|
2224
|
+
}
|
|
2225
|
+
function parseTranscript(raw) {
|
|
2226
|
+
const empty = {
|
|
2227
|
+
prompt: "",
|
|
2228
|
+
response: "",
|
|
2229
|
+
tokens: 0,
|
|
2230
|
+
inputTokens: 0,
|
|
2231
|
+
outputTokens: 0,
|
|
2232
|
+
modelName: null,
|
|
2233
|
+
numTurns: 0
|
|
2234
|
+
};
|
|
2235
|
+
if (!raw) return empty;
|
|
2236
|
+
const lines = raw.split("\n");
|
|
2237
|
+
let lastUserText = "";
|
|
2238
|
+
let lastUserTimestamp = NaN;
|
|
2239
|
+
let lastUserTimestampRaw;
|
|
2240
|
+
let lastAssistantText = "";
|
|
2241
|
+
let lastAssistantTimestamp = NaN;
|
|
2242
|
+
let lastAssistantModel = null;
|
|
2243
|
+
let lastAssistantInputTokens = 0;
|
|
2244
|
+
let lastAssistantOutputTokens = 0;
|
|
2245
|
+
let numTurns = 0;
|
|
2246
|
+
let currentTurnUserTimestamp = NaN;
|
|
2247
|
+
for (const rawLine of lines) {
|
|
2248
|
+
if (!rawLine) continue;
|
|
2249
|
+
let parsed;
|
|
2250
|
+
try {
|
|
2251
|
+
parsed = JSON.parse(rawLine);
|
|
2252
|
+
} catch {
|
|
2253
|
+
continue;
|
|
2254
|
+
}
|
|
2255
|
+
if (isCompactionEntry(parsed)) continue;
|
|
2256
|
+
if (parsed.isSidechain === true) continue;
|
|
2257
|
+
if (parsed.type === "user" && parsed.message) {
|
|
2258
|
+
const text = extractTextContent(parsed.message.content);
|
|
2259
|
+
if (isMetaUserMessage(parsed, text)) continue;
|
|
2260
|
+
lastUserText = text;
|
|
2261
|
+
lastUserTimestamp = parseTimestamp(parsed.timestamp);
|
|
2262
|
+
currentTurnUserTimestamp = lastUserTimestamp;
|
|
2263
|
+
lastUserTimestampRaw = typeof parsed.timestamp === "string" && parsed.timestamp ? parsed.timestamp : void 0;
|
|
2264
|
+
} else if (parsed.type === "assistant" && parsed.message) {
|
|
2265
|
+
const text = extractTextContent(parsed.message.content);
|
|
2266
|
+
numTurns += 1;
|
|
2267
|
+
if (text) lastAssistantText = text;
|
|
2268
|
+
if (typeof parsed.message.model === "string") {
|
|
2269
|
+
lastAssistantModel = parsed.message.model;
|
|
2270
|
+
}
|
|
2271
|
+
const usage = parsed.message.usage;
|
|
2272
|
+
if (usage) {
|
|
2273
|
+
const input = (usage.input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0);
|
|
2274
|
+
const output = usage.output_tokens ?? 0;
|
|
2275
|
+
lastAssistantInputTokens = input;
|
|
2276
|
+
lastAssistantOutputTokens = output;
|
|
2277
|
+
}
|
|
2278
|
+
const ts = parseTimestamp(parsed.timestamp);
|
|
2279
|
+
if (!Number.isNaN(ts)) {
|
|
2280
|
+
lastAssistantTimestamp = ts;
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
const result = {
|
|
2285
|
+
prompt: lastUserText,
|
|
2286
|
+
response: lastAssistantText,
|
|
2287
|
+
tokens: lastAssistantInputTokens + lastAssistantOutputTokens,
|
|
2288
|
+
inputTokens: lastAssistantInputTokens,
|
|
2289
|
+
outputTokens: lastAssistantOutputTokens,
|
|
2290
|
+
modelName: lastAssistantModel,
|
|
2291
|
+
numTurns
|
|
2292
|
+
};
|
|
2293
|
+
if (!Number.isNaN(currentTurnUserTimestamp) && !Number.isNaN(lastAssistantTimestamp) && lastAssistantTimestamp >= currentTurnUserTimestamp) {
|
|
2294
|
+
result.latencyMs = Math.round(
|
|
2295
|
+
lastAssistantTimestamp - currentTurnUserTimestamp
|
|
2296
|
+
);
|
|
2297
|
+
}
|
|
2298
|
+
const skill = detectSkill(lastUserText);
|
|
2299
|
+
if (skill) {
|
|
2300
|
+
result.skill = skill;
|
|
2301
|
+
}
|
|
2302
|
+
if (lastUserTimestampRaw) {
|
|
2303
|
+
result.userTurnTimestamp = lastUserTimestampRaw;
|
|
2304
|
+
}
|
|
2305
|
+
return result;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
// src/commands/monitor-state.ts
|
|
2186
2309
|
import * as fs2 from "fs";
|
|
2310
|
+
import * as os2 from "os";
|
|
2187
2311
|
import * as path2 from "path";
|
|
2188
|
-
|
|
2312
|
+
var STATE_DIR_SEGMENTS = [".olakai", "monitor-state"];
|
|
2313
|
+
function getStateDir(homeDir) {
|
|
2314
|
+
return path2.join(homeDir, ...STATE_DIR_SEGMENTS);
|
|
2315
|
+
}
|
|
2316
|
+
function getStateFile(sessionId, homeDir) {
|
|
2317
|
+
return path2.join(getStateDir(homeDir), `${sessionId}.json`);
|
|
2318
|
+
}
|
|
2319
|
+
var debugLogger = null;
|
|
2320
|
+
function setDebugLogger(logger) {
|
|
2321
|
+
debugLogger = logger;
|
|
2322
|
+
}
|
|
2323
|
+
function log(label, data) {
|
|
2324
|
+
if (debugLogger) {
|
|
2325
|
+
try {
|
|
2326
|
+
debugLogger(label, data);
|
|
2327
|
+
} catch {
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
function loadSessionState(sessionId, homeDir = os2.homedir()) {
|
|
2332
|
+
if (!sessionId) return null;
|
|
2333
|
+
const filePath = getStateFile(sessionId, homeDir);
|
|
2334
|
+
try {
|
|
2335
|
+
if (!fs2.existsSync(filePath)) return null;
|
|
2336
|
+
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
2337
|
+
const parsed = JSON.parse(raw);
|
|
2338
|
+
if (typeof parsed?.lastUserTimestamp !== "string" || typeof parsed?.lastReportedAt !== "string" || typeof parsed?.numTurnsAtLastReport !== "number") {
|
|
2339
|
+
return null;
|
|
2340
|
+
}
|
|
2341
|
+
return {
|
|
2342
|
+
lastUserTimestamp: parsed.lastUserTimestamp,
|
|
2343
|
+
lastReportedAt: parsed.lastReportedAt,
|
|
2344
|
+
numTurnsAtLastReport: parsed.numTurnsAtLastReport
|
|
2345
|
+
};
|
|
2346
|
+
} catch (err) {
|
|
2347
|
+
log("state-load-failed", {
|
|
2348
|
+
sessionId,
|
|
2349
|
+
error: err.message
|
|
2350
|
+
});
|
|
2351
|
+
return null;
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
function saveSessionState(sessionId, state, homeDir = os2.homedir()) {
|
|
2355
|
+
if (!sessionId) return;
|
|
2356
|
+
const dir = getStateDir(homeDir);
|
|
2357
|
+
const filePath = getStateFile(sessionId, homeDir);
|
|
2358
|
+
try {
|
|
2359
|
+
if (!fs2.existsSync(dir)) {
|
|
2360
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
2361
|
+
}
|
|
2362
|
+
fs2.writeFileSync(filePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
2363
|
+
} catch (err) {
|
|
2364
|
+
log("state-save-failed", {
|
|
2365
|
+
sessionId,
|
|
2366
|
+
error: err.message
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
function shouldReportTurn(existing, currentUserTimestamp) {
|
|
2371
|
+
if (!currentUserTimestamp) return true;
|
|
2372
|
+
if (!existing) return true;
|
|
2373
|
+
return existing.lastUserTimestamp !== currentUserTimestamp;
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
// src/commands/monitor.ts
|
|
2189
2377
|
var CLAUDE_DIR = ".claude";
|
|
2190
2378
|
var SETTINGS_FILE = "settings.json";
|
|
2191
2379
|
var MONITOR_CONFIG_FILE = "olakai-monitor.json";
|
|
@@ -2201,8 +2389,36 @@ var HOOK_DEFINITIONS = {
|
|
|
2201
2389
|
}
|
|
2202
2390
|
]
|
|
2203
2391
|
}
|
|
2392
|
+
],
|
|
2393
|
+
SubagentStop: [
|
|
2394
|
+
{
|
|
2395
|
+
matcher: "",
|
|
2396
|
+
hooks: [
|
|
2397
|
+
{
|
|
2398
|
+
type: "command",
|
|
2399
|
+
command: "olakai monitor hook subagent-stop"
|
|
2400
|
+
}
|
|
2401
|
+
]
|
|
2402
|
+
}
|
|
2204
2403
|
]
|
|
2205
2404
|
};
|
|
2405
|
+
function mergeHooksSettings(existing, definitions = HOOK_DEFINITIONS) {
|
|
2406
|
+
const merged = {
|
|
2407
|
+
...existing ?? {}
|
|
2408
|
+
};
|
|
2409
|
+
for (const [event, defaultEntries] of Object.entries(definitions)) {
|
|
2410
|
+
const existingEntries = merged[event] ?? [];
|
|
2411
|
+
const hasOlakaiHook = existingEntries.some(
|
|
2412
|
+
(e) => e.hooks.some((h) => h.command.includes(OLAKAI_HOOK_MARKER))
|
|
2413
|
+
);
|
|
2414
|
+
if (hasOlakaiHook) {
|
|
2415
|
+
merged[event] = existingEntries;
|
|
2416
|
+
} else {
|
|
2417
|
+
merged[event] = [...existingEntries, ...defaultEntries];
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
return merged;
|
|
2421
|
+
}
|
|
2206
2422
|
function prompt(question) {
|
|
2207
2423
|
const rl = readline.createInterface({
|
|
2208
2424
|
input: process.stdin,
|
|
@@ -2218,39 +2434,55 @@ function prompt(question) {
|
|
|
2218
2434
|
function findProjectRoot(startDir) {
|
|
2219
2435
|
let dir = startDir ?? process.cwd();
|
|
2220
2436
|
while (true) {
|
|
2221
|
-
if (
|
|
2437
|
+
if (fs3.existsSync(path3.join(dir, CLAUDE_DIR))) {
|
|
2222
2438
|
return dir;
|
|
2223
2439
|
}
|
|
2224
|
-
const parent =
|
|
2440
|
+
const parent = path3.dirname(dir);
|
|
2225
2441
|
if (parent === dir) break;
|
|
2226
2442
|
dir = parent;
|
|
2227
2443
|
}
|
|
2228
2444
|
return startDir ?? process.cwd();
|
|
2229
2445
|
}
|
|
2446
|
+
function findConfiguredProjectRoot(startDir) {
|
|
2447
|
+
let dir = startDir ?? process.cwd();
|
|
2448
|
+
while (true) {
|
|
2449
|
+
if (fs3.existsSync(path3.join(dir, CLAUDE_DIR, MONITOR_CONFIG_FILE))) {
|
|
2450
|
+
return dir;
|
|
2451
|
+
}
|
|
2452
|
+
const parent = path3.dirname(dir);
|
|
2453
|
+
if (parent === dir) break;
|
|
2454
|
+
dir = parent;
|
|
2455
|
+
}
|
|
2456
|
+
return null;
|
|
2457
|
+
}
|
|
2458
|
+
function resolveProjectRootFromPayload(eventData, fallbackCwd) {
|
|
2459
|
+
const payloadCwd = typeof eventData.cwd === "string" && eventData.cwd.trim() ? eventData.cwd : fallbackCwd;
|
|
2460
|
+
return findConfiguredProjectRoot(payloadCwd);
|
|
2461
|
+
}
|
|
2230
2462
|
function getClaudeDir(projectRoot) {
|
|
2231
|
-
return
|
|
2463
|
+
return path3.join(projectRoot, CLAUDE_DIR);
|
|
2232
2464
|
}
|
|
2233
2465
|
function getSettingsPath(projectRoot) {
|
|
2234
|
-
return
|
|
2466
|
+
return path3.join(getClaudeDir(projectRoot), SETTINGS_FILE);
|
|
2235
2467
|
}
|
|
2236
2468
|
function getMonitorConfigPath(projectRoot) {
|
|
2237
|
-
return
|
|
2469
|
+
return path3.join(getClaudeDir(projectRoot), MONITOR_CONFIG_FILE);
|
|
2238
2470
|
}
|
|
2239
2471
|
function readJsonFile(filePath) {
|
|
2240
2472
|
try {
|
|
2241
|
-
if (!
|
|
2242
|
-
const content =
|
|
2473
|
+
if (!fs3.existsSync(filePath)) return null;
|
|
2474
|
+
const content = fs3.readFileSync(filePath, "utf-8");
|
|
2243
2475
|
return JSON.parse(content);
|
|
2244
2476
|
} catch {
|
|
2245
2477
|
return null;
|
|
2246
2478
|
}
|
|
2247
2479
|
}
|
|
2248
2480
|
function writeJsonFile(filePath, data) {
|
|
2249
|
-
const dir =
|
|
2250
|
-
if (!
|
|
2251
|
-
|
|
2481
|
+
const dir = path3.dirname(filePath);
|
|
2482
|
+
if (!fs3.existsSync(dir)) {
|
|
2483
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
2252
2484
|
}
|
|
2253
|
-
|
|
2485
|
+
fs3.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
2254
2486
|
}
|
|
2255
2487
|
function loadMonitorConfig(projectRoot) {
|
|
2256
2488
|
const root = projectRoot ?? findProjectRoot();
|
|
@@ -2290,7 +2522,7 @@ async function initCommand() {
|
|
|
2290
2522
|
}
|
|
2291
2523
|
console.log("Setting up Claude Code monitoring for this workspace...\n");
|
|
2292
2524
|
const projectRoot = process.cwd();
|
|
2293
|
-
const dirName =
|
|
2525
|
+
const dirName = path3.basename(projectRoot);
|
|
2294
2526
|
let agent;
|
|
2295
2527
|
const choice = await prompt(
|
|
2296
2528
|
"Create a new agent or use an existing one? (new/existing) [new]: "
|
|
@@ -2341,21 +2573,12 @@ Select agent (1-${agents.length}): `);
|
|
|
2341
2573
|
}
|
|
2342
2574
|
}
|
|
2343
2575
|
const claudeDir = getClaudeDir(projectRoot);
|
|
2344
|
-
if (!
|
|
2345
|
-
|
|
2576
|
+
if (!fs3.existsSync(claudeDir)) {
|
|
2577
|
+
fs3.mkdirSync(claudeDir, { recursive: true });
|
|
2346
2578
|
}
|
|
2347
2579
|
const settingsPath = getSettingsPath(projectRoot);
|
|
2348
2580
|
const existingSettings = readJsonFile(settingsPath) ?? {};
|
|
2349
|
-
const mergedHooks =
|
|
2350
|
-
...existingSettings.hooks ?? {}
|
|
2351
|
-
};
|
|
2352
|
-
for (const [event, entries] of Object.entries(HOOK_DEFINITIONS)) {
|
|
2353
|
-
const existing = mergedHooks[event] ?? [];
|
|
2354
|
-
const filtered = existing.filter(
|
|
2355
|
-
(e) => !e.hooks.some((h) => h.command.includes(OLAKAI_HOOK_MARKER))
|
|
2356
|
-
);
|
|
2357
|
-
mergedHooks[event] = [...filtered, ...entries];
|
|
2358
|
-
}
|
|
2581
|
+
const mergedHooks = mergeHooksSettings(existingSettings.hooks);
|
|
2359
2582
|
const updatedSettings = {
|
|
2360
2583
|
...existingSettings,
|
|
2361
2584
|
hooks: mergedHooks
|
|
@@ -2372,7 +2595,7 @@ Select agent (1-${agents.length}): `);
|
|
|
2372
2595
|
};
|
|
2373
2596
|
const monitorConfigPath = getMonitorConfigPath(projectRoot);
|
|
2374
2597
|
writeJsonFile(monitorConfigPath, monitorConfig);
|
|
2375
|
-
|
|
2598
|
+
fs3.chmodSync(monitorConfigPath, 384);
|
|
2376
2599
|
console.log("");
|
|
2377
2600
|
console.log(`\u2713 Agent "${agent.name}" configured (ID: ${agent.id})`);
|
|
2378
2601
|
if (agent.apiKey?.key) {
|
|
@@ -2416,26 +2639,10 @@ function debugLog(label, data) {
|
|
|
2416
2639
|
const logPath = `/tmp/olakai-monitor-debug-${process.pid}.log`;
|
|
2417
2640
|
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${label}: ${typeof data === "string" ? data : JSON.stringify(data, null, 2)}
|
|
2418
2641
|
`;
|
|
2419
|
-
|
|
2642
|
+
fs3.appendFileSync(logPath, line, "utf-8");
|
|
2420
2643
|
} catch {
|
|
2421
2644
|
}
|
|
2422
2645
|
}
|
|
2423
|
-
function extractTextContent(content) {
|
|
2424
|
-
if (typeof content === "string") return content;
|
|
2425
|
-
if (!Array.isArray(content)) return "";
|
|
2426
|
-
const parts = [];
|
|
2427
|
-
for (const block of content) {
|
|
2428
|
-
if (block?.type === "text" && typeof block.text === "string") {
|
|
2429
|
-
parts.push(block.text);
|
|
2430
|
-
}
|
|
2431
|
-
}
|
|
2432
|
-
return parts.join("\n").trim();
|
|
2433
|
-
}
|
|
2434
|
-
function isMetaUserMessage(line, text) {
|
|
2435
|
-
if (line.isMeta === true) return true;
|
|
2436
|
-
if (!text) return true;
|
|
2437
|
-
return text.includes("<command-name>") || text.includes("<local-command-caveat>") || text.includes("<command-message>");
|
|
2438
|
-
}
|
|
2439
2646
|
function extractFromTranscript(transcriptPath) {
|
|
2440
2647
|
const empty = {
|
|
2441
2648
|
prompt: "",
|
|
@@ -2449,7 +2656,7 @@ function extractFromTranscript(transcriptPath) {
|
|
|
2449
2656
|
if (!transcriptPath) return empty;
|
|
2450
2657
|
let raw;
|
|
2451
2658
|
try {
|
|
2452
|
-
raw =
|
|
2659
|
+
raw = fs3.readFileSync(transcriptPath, "utf-8");
|
|
2453
2660
|
} catch (err) {
|
|
2454
2661
|
debugLog("transcript-read-failed", {
|
|
2455
2662
|
transcriptPath,
|
|
@@ -2457,58 +2664,25 @@ function extractFromTranscript(transcriptPath) {
|
|
|
2457
2664
|
});
|
|
2458
2665
|
return empty;
|
|
2459
2666
|
}
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
} catch {
|
|
2473
|
-
continue;
|
|
2474
|
-
}
|
|
2475
|
-
if (parsed.isSidechain === true) continue;
|
|
2476
|
-
if (parsed.type === "user" && parsed.message) {
|
|
2477
|
-
const text = extractTextContent(parsed.message.content);
|
|
2478
|
-
if (isMetaUserMessage(parsed, text)) continue;
|
|
2479
|
-
lastUserText = text;
|
|
2480
|
-
} else if (parsed.type === "assistant" && parsed.message) {
|
|
2481
|
-
const text = extractTextContent(parsed.message.content);
|
|
2482
|
-
numTurns += 1;
|
|
2483
|
-
if (text) lastAssistantText = text;
|
|
2484
|
-
if (typeof parsed.message.model === "string") {
|
|
2485
|
-
lastAssistantModel = parsed.message.model;
|
|
2486
|
-
}
|
|
2487
|
-
const usage = parsed.message.usage;
|
|
2488
|
-
if (usage) {
|
|
2489
|
-
const input = (usage.input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0);
|
|
2490
|
-
const output = usage.output_tokens ?? 0;
|
|
2491
|
-
lastAssistantInputTokens = input;
|
|
2492
|
-
lastAssistantOutputTokens = output;
|
|
2493
|
-
}
|
|
2667
|
+
return parseTranscript(raw);
|
|
2668
|
+
}
|
|
2669
|
+
function extractSubagentName(event) {
|
|
2670
|
+
const candidates = [
|
|
2671
|
+
event.agent_name,
|
|
2672
|
+
event.subagent_type,
|
|
2673
|
+
event.agent_type,
|
|
2674
|
+
event.tool_input?.subagent_type
|
|
2675
|
+
];
|
|
2676
|
+
for (const value of candidates) {
|
|
2677
|
+
if (typeof value === "string" && value.trim()) {
|
|
2678
|
+
return value.trim();
|
|
2494
2679
|
}
|
|
2495
2680
|
}
|
|
2496
|
-
return
|
|
2497
|
-
prompt: lastUserText,
|
|
2498
|
-
response: lastAssistantText,
|
|
2499
|
-
tokens: lastAssistantInputTokens + lastAssistantOutputTokens,
|
|
2500
|
-
inputTokens: lastAssistantInputTokens,
|
|
2501
|
-
outputTokens: lastAssistantOutputTokens,
|
|
2502
|
-
modelName: lastAssistantModel,
|
|
2503
|
-
numTurns
|
|
2504
|
-
};
|
|
2681
|
+
return void 0;
|
|
2505
2682
|
}
|
|
2506
2683
|
async function hookCommand(event) {
|
|
2684
|
+
setDebugLogger(debugLog);
|
|
2507
2685
|
try {
|
|
2508
|
-
const config = loadMonitorConfig();
|
|
2509
|
-
if (!config) {
|
|
2510
|
-
return;
|
|
2511
|
-
}
|
|
2512
2686
|
const stdinData = await readStdin(3e3);
|
|
2513
2687
|
debugLog("stdin-raw", stdinData);
|
|
2514
2688
|
let eventData = {};
|
|
@@ -2517,15 +2691,42 @@ async function hookCommand(event) {
|
|
|
2517
2691
|
eventData = JSON.parse(stdinData);
|
|
2518
2692
|
} catch {
|
|
2519
2693
|
debugLog("stdin-parse-failed", stdinData);
|
|
2520
|
-
return;
|
|
2521
2694
|
}
|
|
2522
2695
|
}
|
|
2523
2696
|
debugLog("event-parsed", { event, eventData });
|
|
2697
|
+
const projectRoot = resolveProjectRootFromPayload(
|
|
2698
|
+
eventData,
|
|
2699
|
+
process.cwd()
|
|
2700
|
+
);
|
|
2701
|
+
if (!projectRoot) {
|
|
2702
|
+
debugLog("config-not-found", {
|
|
2703
|
+
startDir: typeof eventData.cwd === "string" && eventData.cwd.trim() ? eventData.cwd : process.cwd()
|
|
2704
|
+
});
|
|
2705
|
+
return;
|
|
2706
|
+
}
|
|
2707
|
+
const config = loadMonitorConfig(projectRoot);
|
|
2708
|
+
if (!config) {
|
|
2709
|
+
debugLog("config-load-failed", { projectRoot });
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2524
2712
|
const payload = buildPayload(event, eventData, config);
|
|
2525
2713
|
if (!payload) {
|
|
2526
2714
|
return;
|
|
2527
2715
|
}
|
|
2528
2716
|
debugLog("payload-built", payload);
|
|
2717
|
+
const sessionId = typeof eventData.session_id === "string" && eventData.session_id || void 0;
|
|
2718
|
+
const extracted = extractFromTranscript(eventData.transcript_path);
|
|
2719
|
+
const userTurnTimestamp = extracted.userTurnTimestamp;
|
|
2720
|
+
if (sessionId) {
|
|
2721
|
+
const existingState = loadSessionState(sessionId);
|
|
2722
|
+
if (!shouldReportTurn(existingState, userTurnTimestamp)) {
|
|
2723
|
+
debugLog("turn-dedup-skip", {
|
|
2724
|
+
sessionId,
|
|
2725
|
+
userTurnTimestamp
|
|
2726
|
+
});
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2529
2730
|
const controller = new AbortController();
|
|
2530
2731
|
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
2531
2732
|
try {
|
|
@@ -2545,6 +2746,18 @@ async function hookCommand(event) {
|
|
|
2545
2746
|
} finally {
|
|
2546
2747
|
clearTimeout(timeoutId);
|
|
2547
2748
|
}
|
|
2749
|
+
if (sessionId && userTurnTimestamp) {
|
|
2750
|
+
saveSessionState(sessionId, {
|
|
2751
|
+
lastUserTimestamp: userTurnTimestamp,
|
|
2752
|
+
lastReportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2753
|
+
numTurnsAtLastReport: extracted.numTurns
|
|
2754
|
+
});
|
|
2755
|
+
debugLog("state-saved", {
|
|
2756
|
+
sessionId,
|
|
2757
|
+
userTurnTimestamp,
|
|
2758
|
+
numTurns: extracted.numTurns
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2548
2761
|
} catch (err) {
|
|
2549
2762
|
debugLog("hook-exception", err.message);
|
|
2550
2763
|
}
|
|
@@ -2552,27 +2765,43 @@ async function hookCommand(event) {
|
|
|
2552
2765
|
function buildPayload(event, eventData, config) {
|
|
2553
2766
|
const sessionId = eventData.session_id ?? `claude-code-${Date.now()}`;
|
|
2554
2767
|
switch (event) {
|
|
2555
|
-
case "stop":
|
|
2768
|
+
case "stop":
|
|
2769
|
+
case "subagent-stop": {
|
|
2556
2770
|
const extracted = extractFromTranscript(eventData.transcript_path);
|
|
2771
|
+
const isSubagent = event === "subagent-stop";
|
|
2772
|
+
const customData = {
|
|
2773
|
+
hookEvent: eventData.hook_event_name ?? (isSubagent ? "SubagentStop" : "Stop"),
|
|
2774
|
+
sessionId,
|
|
2775
|
+
transcriptPath: eventData.transcript_path ?? "",
|
|
2776
|
+
cwd: eventData.cwd ?? "",
|
|
2777
|
+
stopHookActive: eventData.stop_hook_active ?? false,
|
|
2778
|
+
inputTokens: extracted.inputTokens,
|
|
2779
|
+
outputTokens: extracted.outputTokens,
|
|
2780
|
+
numTurns: extracted.numTurns
|
|
2781
|
+
};
|
|
2782
|
+
if (typeof extracted.latencyMs === "number") {
|
|
2783
|
+
customData.latencyMs = extracted.latencyMs;
|
|
2784
|
+
}
|
|
2785
|
+
if (isSubagent) {
|
|
2786
|
+
const subagent = extractSubagentName(eventData);
|
|
2787
|
+
if (subagent) {
|
|
2788
|
+
customData.subagent = subagent;
|
|
2789
|
+
}
|
|
2790
|
+
} else if (extracted.skill) {
|
|
2791
|
+
customData.skill = extracted.skill;
|
|
2792
|
+
}
|
|
2793
|
+
const payloadAssistant = eventData.last_assistant_message;
|
|
2794
|
+
const response = typeof payloadAssistant === "string" && payloadAssistant.trim() ? payloadAssistant : extracted.response;
|
|
2557
2795
|
return {
|
|
2558
2796
|
prompt: extracted.prompt,
|
|
2559
|
-
response
|
|
2797
|
+
response,
|
|
2560
2798
|
chatId: sessionId,
|
|
2561
2799
|
// Top-level source so the backend ExternalPromptRequestSchema
|
|
2562
2800
|
// picks it up (matches regex ^[a-z0-9_-]+$).
|
|
2563
2801
|
source: config.source,
|
|
2564
2802
|
modelName: extracted.modelName ?? void 0,
|
|
2565
2803
|
tokens: extracted.tokens,
|
|
2566
|
-
customData
|
|
2567
|
-
hookEvent: eventData.hook_event_name ?? "Stop",
|
|
2568
|
-
sessionId,
|
|
2569
|
-
transcriptPath: eventData.transcript_path ?? "",
|
|
2570
|
-
cwd: eventData.cwd ?? "",
|
|
2571
|
-
stopHookActive: eventData.stop_hook_active ?? false,
|
|
2572
|
-
inputTokens: extracted.inputTokens,
|
|
2573
|
-
outputTokens: extracted.outputTokens,
|
|
2574
|
-
numTurns: extracted.numTurns
|
|
2575
|
-
}
|
|
2804
|
+
customData
|
|
2576
2805
|
};
|
|
2577
2806
|
}
|
|
2578
2807
|
default:
|
|
@@ -2672,8 +2901,8 @@ async function disableCommand(options) {
|
|
|
2672
2901
|
}
|
|
2673
2902
|
if (!options.keepConfig) {
|
|
2674
2903
|
const configPath = getMonitorConfigPath(projectRoot);
|
|
2675
|
-
if (
|
|
2676
|
-
|
|
2904
|
+
if (fs3.existsSync(configPath)) {
|
|
2905
|
+
fs3.unlinkSync(configPath);
|
|
2677
2906
|
console.log(`\u2713 Monitor config removed (${CLAUDE_DIR}/${MONITOR_CONFIG_FILE})`);
|
|
2678
2907
|
}
|
|
2679
2908
|
} else {
|