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 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: eventData.prompt ?? "",
2456
- response: eventData.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: eventData.model,
2460
- tokens: eventData.total_tokens_in !== void 0 && eventData.total_tokens_out !== void 0 ? eventData.total_tokens_in + eventData.total_tokens_out : void 0,
2461
- requestTime: eventData.duration_api_ms,
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: