olakai-cli 0.1.14 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +234 -27
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -2186,6 +2186,121 @@ function registerActivityCommand(program2) {
|
|
|
2186
2186
|
import * as fs2 from "fs";
|
|
2187
2187
|
import * as path2 from "path";
|
|
2188
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 lastAssistantText = "";
|
|
2240
|
+
let lastAssistantTimestamp = NaN;
|
|
2241
|
+
let lastAssistantModel = null;
|
|
2242
|
+
let lastAssistantInputTokens = 0;
|
|
2243
|
+
let lastAssistantOutputTokens = 0;
|
|
2244
|
+
let numTurns = 0;
|
|
2245
|
+
let currentTurnUserTimestamp = NaN;
|
|
2246
|
+
for (const rawLine of lines) {
|
|
2247
|
+
if (!rawLine) continue;
|
|
2248
|
+
let parsed;
|
|
2249
|
+
try {
|
|
2250
|
+
parsed = JSON.parse(rawLine);
|
|
2251
|
+
} catch {
|
|
2252
|
+
continue;
|
|
2253
|
+
}
|
|
2254
|
+
if (isCompactionEntry(parsed)) continue;
|
|
2255
|
+
if (parsed.isSidechain === true) continue;
|
|
2256
|
+
if (parsed.type === "user" && parsed.message) {
|
|
2257
|
+
const text = extractTextContent(parsed.message.content);
|
|
2258
|
+
if (isMetaUserMessage(parsed, text)) continue;
|
|
2259
|
+
lastUserText = text;
|
|
2260
|
+
lastUserTimestamp = parseTimestamp(parsed.timestamp);
|
|
2261
|
+
currentTurnUserTimestamp = lastUserTimestamp;
|
|
2262
|
+
} else if (parsed.type === "assistant" && parsed.message) {
|
|
2263
|
+
const text = extractTextContent(parsed.message.content);
|
|
2264
|
+
numTurns += 1;
|
|
2265
|
+
if (text) lastAssistantText = text;
|
|
2266
|
+
if (typeof parsed.message.model === "string") {
|
|
2267
|
+
lastAssistantModel = parsed.message.model;
|
|
2268
|
+
}
|
|
2269
|
+
const usage = parsed.message.usage;
|
|
2270
|
+
if (usage) {
|
|
2271
|
+
const input = (usage.input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0);
|
|
2272
|
+
const output = usage.output_tokens ?? 0;
|
|
2273
|
+
lastAssistantInputTokens = input;
|
|
2274
|
+
lastAssistantOutputTokens = output;
|
|
2275
|
+
}
|
|
2276
|
+
const ts = parseTimestamp(parsed.timestamp);
|
|
2277
|
+
if (!Number.isNaN(ts)) {
|
|
2278
|
+
lastAssistantTimestamp = ts;
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
const result = {
|
|
2283
|
+
prompt: lastUserText,
|
|
2284
|
+
response: lastAssistantText,
|
|
2285
|
+
tokens: lastAssistantInputTokens + lastAssistantOutputTokens,
|
|
2286
|
+
inputTokens: lastAssistantInputTokens,
|
|
2287
|
+
outputTokens: lastAssistantOutputTokens,
|
|
2288
|
+
modelName: lastAssistantModel,
|
|
2289
|
+
numTurns
|
|
2290
|
+
};
|
|
2291
|
+
if (!Number.isNaN(currentTurnUserTimestamp) && !Number.isNaN(lastAssistantTimestamp) && lastAssistantTimestamp >= currentTurnUserTimestamp) {
|
|
2292
|
+
result.latencyMs = Math.round(
|
|
2293
|
+
lastAssistantTimestamp - currentTurnUserTimestamp
|
|
2294
|
+
);
|
|
2295
|
+
}
|
|
2296
|
+
const skill = detectSkill(lastUserText);
|
|
2297
|
+
if (skill) {
|
|
2298
|
+
result.skill = skill;
|
|
2299
|
+
}
|
|
2300
|
+
return result;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
// src/commands/monitor.ts
|
|
2189
2304
|
var CLAUDE_DIR = ".claude";
|
|
2190
2305
|
var SETTINGS_FILE = "settings.json";
|
|
2191
2306
|
var MONITOR_CONFIG_FILE = "olakai-monitor.json";
|
|
@@ -2201,8 +2316,36 @@ var HOOK_DEFINITIONS = {
|
|
|
2201
2316
|
}
|
|
2202
2317
|
]
|
|
2203
2318
|
}
|
|
2319
|
+
],
|
|
2320
|
+
SubagentStop: [
|
|
2321
|
+
{
|
|
2322
|
+
matcher: "",
|
|
2323
|
+
hooks: [
|
|
2324
|
+
{
|
|
2325
|
+
type: "command",
|
|
2326
|
+
command: "olakai monitor hook subagent-stop"
|
|
2327
|
+
}
|
|
2328
|
+
]
|
|
2329
|
+
}
|
|
2204
2330
|
]
|
|
2205
2331
|
};
|
|
2332
|
+
function mergeHooksSettings(existing, definitions = HOOK_DEFINITIONS) {
|
|
2333
|
+
const merged = {
|
|
2334
|
+
...existing ?? {}
|
|
2335
|
+
};
|
|
2336
|
+
for (const [event, defaultEntries] of Object.entries(definitions)) {
|
|
2337
|
+
const existingEntries = merged[event] ?? [];
|
|
2338
|
+
const hasOlakaiHook = existingEntries.some(
|
|
2339
|
+
(e) => e.hooks.some((h) => h.command.includes(OLAKAI_HOOK_MARKER))
|
|
2340
|
+
);
|
|
2341
|
+
if (hasOlakaiHook) {
|
|
2342
|
+
merged[event] = existingEntries;
|
|
2343
|
+
} else {
|
|
2344
|
+
merged[event] = [...existingEntries, ...defaultEntries];
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
return merged;
|
|
2348
|
+
}
|
|
2206
2349
|
function prompt(question) {
|
|
2207
2350
|
const rl = readline.createInterface({
|
|
2208
2351
|
input: process.stdin,
|
|
@@ -2346,16 +2489,7 @@ Select agent (1-${agents.length}): `);
|
|
|
2346
2489
|
}
|
|
2347
2490
|
const settingsPath = getSettingsPath(projectRoot);
|
|
2348
2491
|
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
|
-
}
|
|
2492
|
+
const mergedHooks = mergeHooksSettings(existingSettings.hooks);
|
|
2359
2493
|
const updatedSettings = {
|
|
2360
2494
|
...existingSettings,
|
|
2361
2495
|
hooks: mergedHooks
|
|
@@ -2410,6 +2544,53 @@ async function createNewAgent(defaultName) {
|
|
|
2410
2544
|
});
|
|
2411
2545
|
return agent;
|
|
2412
2546
|
}
|
|
2547
|
+
function debugLog(label, data) {
|
|
2548
|
+
if (process.env.OLAKAI_MONITOR_DEBUG !== "1") return;
|
|
2549
|
+
try {
|
|
2550
|
+
const logPath = `/tmp/olakai-monitor-debug-${process.pid}.log`;
|
|
2551
|
+
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${label}: ${typeof data === "string" ? data : JSON.stringify(data, null, 2)}
|
|
2552
|
+
`;
|
|
2553
|
+
fs2.appendFileSync(logPath, line, "utf-8");
|
|
2554
|
+
} catch {
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
function extractFromTranscript(transcriptPath) {
|
|
2558
|
+
const empty = {
|
|
2559
|
+
prompt: "",
|
|
2560
|
+
response: "",
|
|
2561
|
+
tokens: 0,
|
|
2562
|
+
inputTokens: 0,
|
|
2563
|
+
outputTokens: 0,
|
|
2564
|
+
modelName: null,
|
|
2565
|
+
numTurns: 0
|
|
2566
|
+
};
|
|
2567
|
+
if (!transcriptPath) return empty;
|
|
2568
|
+
let raw;
|
|
2569
|
+
try {
|
|
2570
|
+
raw = fs2.readFileSync(transcriptPath, "utf-8");
|
|
2571
|
+
} catch (err) {
|
|
2572
|
+
debugLog("transcript-read-failed", {
|
|
2573
|
+
transcriptPath,
|
|
2574
|
+
error: err.message
|
|
2575
|
+
});
|
|
2576
|
+
return empty;
|
|
2577
|
+
}
|
|
2578
|
+
return parseTranscript(raw);
|
|
2579
|
+
}
|
|
2580
|
+
function extractSubagentName(event) {
|
|
2581
|
+
const candidates = [
|
|
2582
|
+
event.agent_name,
|
|
2583
|
+
event.subagent_type,
|
|
2584
|
+
event.agent_type,
|
|
2585
|
+
event.tool_input?.subagent_type
|
|
2586
|
+
];
|
|
2587
|
+
for (const value of candidates) {
|
|
2588
|
+
if (typeof value === "string" && value.trim()) {
|
|
2589
|
+
return value.trim();
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
return void 0;
|
|
2593
|
+
}
|
|
2413
2594
|
async function hookCommand(event) {
|
|
2414
2595
|
try {
|
|
2415
2596
|
const config = loadMonitorConfig();
|
|
@@ -2417,22 +2598,26 @@ async function hookCommand(event) {
|
|
|
2417
2598
|
return;
|
|
2418
2599
|
}
|
|
2419
2600
|
const stdinData = await readStdin(3e3);
|
|
2601
|
+
debugLog("stdin-raw", stdinData);
|
|
2420
2602
|
let eventData = {};
|
|
2421
2603
|
if (stdinData) {
|
|
2422
2604
|
try {
|
|
2423
2605
|
eventData = JSON.parse(stdinData);
|
|
2424
2606
|
} catch {
|
|
2607
|
+
debugLog("stdin-parse-failed", stdinData);
|
|
2425
2608
|
return;
|
|
2426
2609
|
}
|
|
2427
2610
|
}
|
|
2611
|
+
debugLog("event-parsed", { event, eventData });
|
|
2428
2612
|
const payload = buildPayload(event, eventData, config);
|
|
2429
2613
|
if (!payload) {
|
|
2430
2614
|
return;
|
|
2431
2615
|
}
|
|
2616
|
+
debugLog("payload-built", payload);
|
|
2432
2617
|
const controller = new AbortController();
|
|
2433
2618
|
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
2434
2619
|
try {
|
|
2435
|
-
await fetch(config.monitoringEndpoint, {
|
|
2620
|
+
const res = await fetch(config.monitoringEndpoint, {
|
|
2436
2621
|
method: "POST",
|
|
2437
2622
|
headers: {
|
|
2438
2623
|
"x-api-key": config.apiKey,
|
|
@@ -2441,33 +2626,55 @@ async function hookCommand(event) {
|
|
|
2441
2626
|
body: JSON.stringify([payload]),
|
|
2442
2627
|
signal: controller.signal
|
|
2443
2628
|
});
|
|
2629
|
+
debugLog("response-status", {
|
|
2630
|
+
status: res.status,
|
|
2631
|
+
ok: res.ok
|
|
2632
|
+
});
|
|
2444
2633
|
} finally {
|
|
2445
2634
|
clearTimeout(timeoutId);
|
|
2446
2635
|
}
|
|
2447
|
-
} catch {
|
|
2636
|
+
} catch (err) {
|
|
2637
|
+
debugLog("hook-exception", err.message);
|
|
2448
2638
|
}
|
|
2449
2639
|
}
|
|
2450
2640
|
function buildPayload(event, eventData, config) {
|
|
2451
2641
|
const sessionId = eventData.session_id ?? `claude-code-${Date.now()}`;
|
|
2452
2642
|
switch (event) {
|
|
2453
|
-
case "stop":
|
|
2643
|
+
case "stop":
|
|
2644
|
+
case "subagent-stop": {
|
|
2645
|
+
const extracted = extractFromTranscript(eventData.transcript_path);
|
|
2646
|
+
const isSubagent = event === "subagent-stop";
|
|
2647
|
+
const customData = {
|
|
2648
|
+
hookEvent: eventData.hook_event_name ?? (isSubagent ? "SubagentStop" : "Stop"),
|
|
2649
|
+
sessionId,
|
|
2650
|
+
transcriptPath: eventData.transcript_path ?? "",
|
|
2651
|
+
cwd: eventData.cwd ?? "",
|
|
2652
|
+
stopHookActive: eventData.stop_hook_active ?? false,
|
|
2653
|
+
inputTokens: extracted.inputTokens,
|
|
2654
|
+
outputTokens: extracted.outputTokens,
|
|
2655
|
+
numTurns: extracted.numTurns
|
|
2656
|
+
};
|
|
2657
|
+
if (typeof extracted.latencyMs === "number") {
|
|
2658
|
+
customData.latencyMs = extracted.latencyMs;
|
|
2659
|
+
}
|
|
2660
|
+
if (isSubagent) {
|
|
2661
|
+
const subagent = extractSubagentName(eventData);
|
|
2662
|
+
if (subagent) {
|
|
2663
|
+
customData.subagent = subagent;
|
|
2664
|
+
}
|
|
2665
|
+
} else if (extracted.skill) {
|
|
2666
|
+
customData.skill = extracted.skill;
|
|
2667
|
+
}
|
|
2454
2668
|
return {
|
|
2455
|
-
prompt:
|
|
2456
|
-
response:
|
|
2669
|
+
prompt: extracted.prompt,
|
|
2670
|
+
response: extracted.response,
|
|
2457
2671
|
chatId: sessionId,
|
|
2672
|
+
// Top-level source so the backend ExternalPromptRequestSchema
|
|
2673
|
+
// picks it up (matches regex ^[a-z0-9_-]+$).
|
|
2458
2674
|
source: config.source,
|
|
2459
|
-
modelName:
|
|
2460
|
-
tokens:
|
|
2461
|
-
|
|
2462
|
-
customData: {
|
|
2463
|
-
hookEvent: "Stop",
|
|
2464
|
-
stopReason: eventData.stop_reason ?? "",
|
|
2465
|
-
totalTokensIn: eventData.total_tokens_in ?? 0,
|
|
2466
|
-
totalTokensOut: eventData.total_tokens_out ?? 0,
|
|
2467
|
-
totalCost: eventData.total_cost ?? 0,
|
|
2468
|
-
durationMs: eventData.duration_ms ?? 0,
|
|
2469
|
-
numTurns: eventData.num_turns ?? 0
|
|
2470
|
-
}
|
|
2675
|
+
modelName: extracted.modelName ?? void 0,
|
|
2676
|
+
tokens: extracted.tokens,
|
|
2677
|
+
customData
|
|
2471
2678
|
};
|
|
2472
2679
|
}
|
|
2473
2680
|
default:
|