olakai-cli 0.2.0 → 0.3.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
@@ -2183,11 +2183,17 @@ function registerActivityCommand(program2) {
2183
2183
  }
2184
2184
 
2185
2185
  // src/commands/monitor.ts
2186
- import * as fs2 from "fs";
2187
- import * as path2 from "path";
2186
+ import * as fs3 from "fs";
2187
+ import * as path3 from "path";
2188
2188
  import * as readline from "readline";
2189
2189
 
2190
2190
  // src/commands/monitor-transcript.ts
2191
+ var FILE_EDITING_TOOL_NAMES = /* @__PURE__ */ new Set([
2192
+ "Edit",
2193
+ "Write",
2194
+ "MultiEdit"
2195
+ ]);
2196
+ var BASH_TOOL_NAME = "Bash";
2191
2197
  var SKILL_REGEX = /^\/([\w-]+)(?:\s|$)/;
2192
2198
  function detectSkill(userMessage) {
2193
2199
  if (typeof userMessage !== "string") return void 0;
@@ -2230,12 +2236,16 @@ function parseTranscript(raw) {
2230
2236
  inputTokens: 0,
2231
2237
  outputTokens: 0,
2232
2238
  modelName: null,
2233
- numTurns: 0
2239
+ numTurns: 0,
2240
+ toolCallCount: 0,
2241
+ filesEditedCount: 0,
2242
+ bashCommandCount: 0
2234
2243
  };
2235
2244
  if (!raw) return empty;
2236
2245
  const lines = raw.split("\n");
2237
2246
  let lastUserText = "";
2238
2247
  let lastUserTimestamp = NaN;
2248
+ let lastUserTimestampRaw;
2239
2249
  let lastAssistantText = "";
2240
2250
  let lastAssistantTimestamp = NaN;
2241
2251
  let lastAssistantModel = null;
@@ -2243,6 +2253,9 @@ function parseTranscript(raw) {
2243
2253
  let lastAssistantOutputTokens = 0;
2244
2254
  let numTurns = 0;
2245
2255
  let currentTurnUserTimestamp = NaN;
2256
+ let toolCallCount = 0;
2257
+ let bashCommandCount = 0;
2258
+ let editedFilePaths = /* @__PURE__ */ new Set();
2246
2259
  for (const rawLine of lines) {
2247
2260
  if (!rawLine) continue;
2248
2261
  let parsed;
@@ -2259,6 +2272,10 @@ function parseTranscript(raw) {
2259
2272
  lastUserText = text;
2260
2273
  lastUserTimestamp = parseTimestamp(parsed.timestamp);
2261
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;
2262
2279
  } else if (parsed.type === "assistant" && parsed.message) {
2263
2280
  const text = extractTextContent(parsed.message.content);
2264
2281
  numTurns += 1;
@@ -2266,6 +2283,30 @@ function parseTranscript(raw) {
2266
2283
  if (typeof parsed.message.model === "string") {
2267
2284
  lastAssistantModel = parsed.message.model;
2268
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
+ }
2269
2310
  const usage = parsed.message.usage;
2270
2311
  if (usage) {
2271
2312
  const input = (usage.input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0);
@@ -2286,7 +2327,10 @@ function parseTranscript(raw) {
2286
2327
  inputTokens: lastAssistantInputTokens,
2287
2328
  outputTokens: lastAssistantOutputTokens,
2288
2329
  modelName: lastAssistantModel,
2289
- numTurns
2330
+ numTurns,
2331
+ toolCallCount,
2332
+ filesEditedCount: editedFilePaths.size,
2333
+ bashCommandCount
2290
2334
  };
2291
2335
  if (!Number.isNaN(currentTurnUserTimestamp) && !Number.isNaN(lastAssistantTimestamp) && lastAssistantTimestamp >= currentTurnUserTimestamp) {
2292
2336
  result.latencyMs = Math.round(
@@ -2297,9 +2341,80 @@ function parseTranscript(raw) {
2297
2341
  if (skill) {
2298
2342
  result.skill = skill;
2299
2343
  }
2344
+ if (lastUserTimestampRaw) {
2345
+ result.userTurnTimestamp = lastUserTimestampRaw;
2346
+ }
2300
2347
  return result;
2301
2348
  }
2302
2349
 
2350
+ // src/commands/monitor-state.ts
2351
+ import * as fs2 from "fs";
2352
+ import * as os2 from "os";
2353
+ import * as path2 from "path";
2354
+ var STATE_DIR_SEGMENTS = [".olakai", "monitor-state"];
2355
+ function getStateDir(homeDir) {
2356
+ return path2.join(homeDir, ...STATE_DIR_SEGMENTS);
2357
+ }
2358
+ function getStateFile(sessionId, homeDir) {
2359
+ return path2.join(getStateDir(homeDir), `${sessionId}.json`);
2360
+ }
2361
+ var debugLogger = null;
2362
+ function setDebugLogger(logger) {
2363
+ debugLogger = logger;
2364
+ }
2365
+ function log(label, data) {
2366
+ if (debugLogger) {
2367
+ try {
2368
+ debugLogger(label, data);
2369
+ } catch {
2370
+ }
2371
+ }
2372
+ }
2373
+ function loadSessionState(sessionId, homeDir = os2.homedir()) {
2374
+ if (!sessionId) return null;
2375
+ const filePath = getStateFile(sessionId, homeDir);
2376
+ try {
2377
+ if (!fs2.existsSync(filePath)) return null;
2378
+ const raw = fs2.readFileSync(filePath, "utf-8");
2379
+ const parsed = JSON.parse(raw);
2380
+ if (typeof parsed?.lastUserTimestamp !== "string" || typeof parsed?.lastReportedAt !== "string" || typeof parsed?.numTurnsAtLastReport !== "number") {
2381
+ return null;
2382
+ }
2383
+ return {
2384
+ lastUserTimestamp: parsed.lastUserTimestamp,
2385
+ lastReportedAt: parsed.lastReportedAt,
2386
+ numTurnsAtLastReport: parsed.numTurnsAtLastReport
2387
+ };
2388
+ } catch (err) {
2389
+ log("state-load-failed", {
2390
+ sessionId,
2391
+ error: err.message
2392
+ });
2393
+ return null;
2394
+ }
2395
+ }
2396
+ function saveSessionState(sessionId, state, homeDir = os2.homedir()) {
2397
+ if (!sessionId) return;
2398
+ const dir = getStateDir(homeDir);
2399
+ const filePath = getStateFile(sessionId, homeDir);
2400
+ try {
2401
+ if (!fs2.existsSync(dir)) {
2402
+ fs2.mkdirSync(dir, { recursive: true });
2403
+ }
2404
+ fs2.writeFileSync(filePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
2405
+ } catch (err) {
2406
+ log("state-save-failed", {
2407
+ sessionId,
2408
+ error: err.message
2409
+ });
2410
+ }
2411
+ }
2412
+ function shouldReportTurn(existing, currentUserTimestamp) {
2413
+ if (!currentUserTimestamp) return true;
2414
+ if (!existing) return true;
2415
+ return existing.lastUserTimestamp !== currentUserTimestamp;
2416
+ }
2417
+
2303
2418
  // src/commands/monitor.ts
2304
2419
  var CLAUDE_DIR = ".claude";
2305
2420
  var SETTINGS_FILE = "settings.json";
@@ -2361,39 +2476,55 @@ function prompt(question) {
2361
2476
  function findProjectRoot(startDir) {
2362
2477
  let dir = startDir ?? process.cwd();
2363
2478
  while (true) {
2364
- if (fs2.existsSync(path2.join(dir, CLAUDE_DIR))) {
2479
+ if (fs3.existsSync(path3.join(dir, CLAUDE_DIR))) {
2365
2480
  return dir;
2366
2481
  }
2367
- const parent = path2.dirname(dir);
2482
+ const parent = path3.dirname(dir);
2368
2483
  if (parent === dir) break;
2369
2484
  dir = parent;
2370
2485
  }
2371
2486
  return startDir ?? process.cwd();
2372
2487
  }
2488
+ function findConfiguredProjectRoot(startDir) {
2489
+ let dir = startDir ?? process.cwd();
2490
+ while (true) {
2491
+ if (fs3.existsSync(path3.join(dir, CLAUDE_DIR, MONITOR_CONFIG_FILE))) {
2492
+ return dir;
2493
+ }
2494
+ const parent = path3.dirname(dir);
2495
+ if (parent === dir) break;
2496
+ dir = parent;
2497
+ }
2498
+ return null;
2499
+ }
2500
+ function resolveProjectRootFromPayload(eventData, fallbackCwd) {
2501
+ const payloadCwd = typeof eventData.cwd === "string" && eventData.cwd.trim() ? eventData.cwd : fallbackCwd;
2502
+ return findConfiguredProjectRoot(payloadCwd);
2503
+ }
2373
2504
  function getClaudeDir(projectRoot) {
2374
- return path2.join(projectRoot, CLAUDE_DIR);
2505
+ return path3.join(projectRoot, CLAUDE_DIR);
2375
2506
  }
2376
2507
  function getSettingsPath(projectRoot) {
2377
- return path2.join(getClaudeDir(projectRoot), SETTINGS_FILE);
2508
+ return path3.join(getClaudeDir(projectRoot), SETTINGS_FILE);
2378
2509
  }
2379
2510
  function getMonitorConfigPath(projectRoot) {
2380
- return path2.join(getClaudeDir(projectRoot), MONITOR_CONFIG_FILE);
2511
+ return path3.join(getClaudeDir(projectRoot), MONITOR_CONFIG_FILE);
2381
2512
  }
2382
2513
  function readJsonFile(filePath) {
2383
2514
  try {
2384
- if (!fs2.existsSync(filePath)) return null;
2385
- const content = fs2.readFileSync(filePath, "utf-8");
2515
+ if (!fs3.existsSync(filePath)) return null;
2516
+ const content = fs3.readFileSync(filePath, "utf-8");
2386
2517
  return JSON.parse(content);
2387
2518
  } catch {
2388
2519
  return null;
2389
2520
  }
2390
2521
  }
2391
2522
  function writeJsonFile(filePath, data) {
2392
- const dir = path2.dirname(filePath);
2393
- if (!fs2.existsSync(dir)) {
2394
- fs2.mkdirSync(dir, { recursive: true });
2523
+ const dir = path3.dirname(filePath);
2524
+ if (!fs3.existsSync(dir)) {
2525
+ fs3.mkdirSync(dir, { recursive: true });
2395
2526
  }
2396
- fs2.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
2527
+ fs3.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
2397
2528
  }
2398
2529
  function loadMonitorConfig(projectRoot) {
2399
2530
  const root = projectRoot ?? findProjectRoot();
@@ -2433,7 +2564,7 @@ async function initCommand() {
2433
2564
  }
2434
2565
  console.log("Setting up Claude Code monitoring for this workspace...\n");
2435
2566
  const projectRoot = process.cwd();
2436
- const dirName = path2.basename(projectRoot);
2567
+ const dirName = path3.basename(projectRoot);
2437
2568
  let agent;
2438
2569
  const choice = await prompt(
2439
2570
  "Create a new agent or use an existing one? (new/existing) [new]: "
@@ -2484,8 +2615,8 @@ Select agent (1-${agents.length}): `);
2484
2615
  }
2485
2616
  }
2486
2617
  const claudeDir = getClaudeDir(projectRoot);
2487
- if (!fs2.existsSync(claudeDir)) {
2488
- fs2.mkdirSync(claudeDir, { recursive: true });
2618
+ if (!fs3.existsSync(claudeDir)) {
2619
+ fs3.mkdirSync(claudeDir, { recursive: true });
2489
2620
  }
2490
2621
  const settingsPath = getSettingsPath(projectRoot);
2491
2622
  const existingSettings = readJsonFile(settingsPath) ?? {};
@@ -2506,7 +2637,7 @@ Select agent (1-${agents.length}): `);
2506
2637
  };
2507
2638
  const monitorConfigPath = getMonitorConfigPath(projectRoot);
2508
2639
  writeJsonFile(monitorConfigPath, monitorConfig);
2509
- fs2.chmodSync(monitorConfigPath, 384);
2640
+ fs3.chmodSync(monitorConfigPath, 384);
2510
2641
  console.log("");
2511
2642
  console.log(`\u2713 Agent "${agent.name}" configured (ID: ${agent.id})`);
2512
2643
  if (agent.apiKey?.key) {
@@ -2550,7 +2681,7 @@ function debugLog(label, data) {
2550
2681
  const logPath = `/tmp/olakai-monitor-debug-${process.pid}.log`;
2551
2682
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${label}: ${typeof data === "string" ? data : JSON.stringify(data, null, 2)}
2552
2683
  `;
2553
- fs2.appendFileSync(logPath, line, "utf-8");
2684
+ fs3.appendFileSync(logPath, line, "utf-8");
2554
2685
  } catch {
2555
2686
  }
2556
2687
  }
@@ -2562,12 +2693,15 @@ function extractFromTranscript(transcriptPath) {
2562
2693
  inputTokens: 0,
2563
2694
  outputTokens: 0,
2564
2695
  modelName: null,
2565
- numTurns: 0
2696
+ numTurns: 0,
2697
+ toolCallCount: 0,
2698
+ filesEditedCount: 0,
2699
+ bashCommandCount: 0
2566
2700
  };
2567
2701
  if (!transcriptPath) return empty;
2568
2702
  let raw;
2569
2703
  try {
2570
- raw = fs2.readFileSync(transcriptPath, "utf-8");
2704
+ raw = fs3.readFileSync(transcriptPath, "utf-8");
2571
2705
  } catch (err) {
2572
2706
  debugLog("transcript-read-failed", {
2573
2707
  transcriptPath,
@@ -2592,11 +2726,8 @@ function extractSubagentName(event) {
2592
2726
  return void 0;
2593
2727
  }
2594
2728
  async function hookCommand(event) {
2729
+ setDebugLogger(debugLog);
2595
2730
  try {
2596
- const config = loadMonitorConfig();
2597
- if (!config) {
2598
- return;
2599
- }
2600
2731
  const stdinData = await readStdin(3e3);
2601
2732
  debugLog("stdin-raw", stdinData);
2602
2733
  let eventData = {};
@@ -2605,15 +2736,42 @@ async function hookCommand(event) {
2605
2736
  eventData = JSON.parse(stdinData);
2606
2737
  } catch {
2607
2738
  debugLog("stdin-parse-failed", stdinData);
2608
- return;
2609
2739
  }
2610
2740
  }
2611
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
+ }
2612
2757
  const payload = buildPayload(event, eventData, config);
2613
2758
  if (!payload) {
2614
2759
  return;
2615
2760
  }
2616
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
+ }
2617
2775
  const controller = new AbortController();
2618
2776
  const timeoutId = setTimeout(() => controller.abort(), 5e3);
2619
2777
  try {
@@ -2633,6 +2791,18 @@ async function hookCommand(event) {
2633
2791
  } finally {
2634
2792
  clearTimeout(timeoutId);
2635
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
+ }
2636
2806
  } catch (err) {
2637
2807
  debugLog("hook-exception", err.message);
2638
2808
  }
@@ -2652,7 +2822,15 @@ function buildPayload(event, eventData, config) {
2652
2822
  stopHookActive: eventData.stop_hook_active ?? false,
2653
2823
  inputTokens: extracted.inputTokens,
2654
2824
  outputTokens: extracted.outputTokens,
2655
- numTurns: extracted.numTurns
2825
+ numTurns: extracted.numTurns,
2826
+ // Per-turn work signals for the Claude Code classifier (D-027).
2827
+ // Always emitted as JSON numbers — the backend classifier and
2828
+ // future KPI formulas must see numeric 0, not missing keys or
2829
+ // strings. File paths and bash command strings are deliberately
2830
+ // NOT emitted; only the cardinality is privacy-safe to ship.
2831
+ toolCallCount: extracted.toolCallCount,
2832
+ filesEditedCount: extracted.filesEditedCount,
2833
+ bashCommandCount: extracted.bashCommandCount
2656
2834
  };
2657
2835
  if (typeof extracted.latencyMs === "number") {
2658
2836
  customData.latencyMs = extracted.latencyMs;
@@ -2665,9 +2843,11 @@ function buildPayload(event, eventData, config) {
2665
2843
  } else if (extracted.skill) {
2666
2844
  customData.skill = extracted.skill;
2667
2845
  }
2846
+ const payloadAssistant = eventData.last_assistant_message;
2847
+ const response = typeof payloadAssistant === "string" && payloadAssistant.trim() ? payloadAssistant : extracted.response;
2668
2848
  return {
2669
2849
  prompt: extracted.prompt,
2670
- response: extracted.response,
2850
+ response,
2671
2851
  chatId: sessionId,
2672
2852
  // Top-level source so the backend ExternalPromptRequestSchema
2673
2853
  // picks it up (matches regex ^[a-z0-9_-]+$).
@@ -2774,8 +2954,8 @@ async function disableCommand(options) {
2774
2954
  }
2775
2955
  if (!options.keepConfig) {
2776
2956
  const configPath = getMonitorConfigPath(projectRoot);
2777
- if (fs2.existsSync(configPath)) {
2778
- fs2.unlinkSync(configPath);
2957
+ if (fs3.existsSync(configPath)) {
2958
+ fs3.unlinkSync(configPath);
2779
2959
  console.log(`\u2713 Monitor config removed (${CLAUDE_DIR}/${MONITOR_CONFIG_FILE})`);
2780
2960
  }
2781
2961
  } else {