olakai-cli 0.1.15 → 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 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
@@ -2420,22 +2554,6 @@ function debugLog(label, data) {
2420
2554
  } catch {
2421
2555
  }
2422
2556
  }
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
2557
  function extractFromTranscript(transcriptPath) {
2440
2558
  const empty = {
2441
2559
  prompt: "",
@@ -2457,51 +2575,21 @@ function extractFromTranscript(transcriptPath) {
2457
2575
  });
2458
2576
  return empty;
2459
2577
  }
2460
- const lines = raw.split("\n");
2461
- let lastUserText = "";
2462
- let lastAssistantText = "";
2463
- let lastAssistantModel = null;
2464
- let lastAssistantInputTokens = 0;
2465
- let lastAssistantOutputTokens = 0;
2466
- let numTurns = 0;
2467
- for (const rawLine of lines) {
2468
- if (!rawLine) continue;
2469
- let parsed;
2470
- try {
2471
- parsed = JSON.parse(rawLine);
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
- }
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();
2494
2590
  }
2495
2591
  }
2496
- return {
2497
- prompt: lastUserText,
2498
- response: lastAssistantText,
2499
- tokens: lastAssistantInputTokens + lastAssistantOutputTokens,
2500
- inputTokens: lastAssistantInputTokens,
2501
- outputTokens: lastAssistantOutputTokens,
2502
- modelName: lastAssistantModel,
2503
- numTurns
2504
- };
2592
+ return void 0;
2505
2593
  }
2506
2594
  async function hookCommand(event) {
2507
2595
  try {
@@ -2552,8 +2640,31 @@ async function hookCommand(event) {
2552
2640
  function buildPayload(event, eventData, config) {
2553
2641
  const sessionId = eventData.session_id ?? `claude-code-${Date.now()}`;
2554
2642
  switch (event) {
2555
- case "stop": {
2643
+ case "stop":
2644
+ case "subagent-stop": {
2556
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
+ }
2557
2668
  return {
2558
2669
  prompt: extracted.prompt,
2559
2670
  response: extracted.response,
@@ -2563,16 +2674,7 @@ function buildPayload(event, eventData, config) {
2563
2674
  source: config.source,
2564
2675
  modelName: extracted.modelName ?? void 0,
2565
2676
  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
- }
2677
+ customData
2576
2678
  };
2577
2679
  }
2578
2680
  default: