olakai-cli 0.2.0 → 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 CHANGED
@@ -2183,8 +2183,8 @@ 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
@@ -2236,6 +2236,7 @@ function parseTranscript(raw) {
2236
2236
  const lines = raw.split("\n");
2237
2237
  let lastUserText = "";
2238
2238
  let lastUserTimestamp = NaN;
2239
+ let lastUserTimestampRaw;
2239
2240
  let lastAssistantText = "";
2240
2241
  let lastAssistantTimestamp = NaN;
2241
2242
  let lastAssistantModel = null;
@@ -2259,6 +2260,7 @@ function parseTranscript(raw) {
2259
2260
  lastUserText = text;
2260
2261
  lastUserTimestamp = parseTimestamp(parsed.timestamp);
2261
2262
  currentTurnUserTimestamp = lastUserTimestamp;
2263
+ lastUserTimestampRaw = typeof parsed.timestamp === "string" && parsed.timestamp ? parsed.timestamp : void 0;
2262
2264
  } else if (parsed.type === "assistant" && parsed.message) {
2263
2265
  const text = extractTextContent(parsed.message.content);
2264
2266
  numTurns += 1;
@@ -2297,9 +2299,80 @@ function parseTranscript(raw) {
2297
2299
  if (skill) {
2298
2300
  result.skill = skill;
2299
2301
  }
2302
+ if (lastUserTimestampRaw) {
2303
+ result.userTurnTimestamp = lastUserTimestampRaw;
2304
+ }
2300
2305
  return result;
2301
2306
  }
2302
2307
 
2308
+ // src/commands/monitor-state.ts
2309
+ import * as fs2 from "fs";
2310
+ import * as os2 from "os";
2311
+ import * as path2 from "path";
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
+
2303
2376
  // src/commands/monitor.ts
2304
2377
  var CLAUDE_DIR = ".claude";
2305
2378
  var SETTINGS_FILE = "settings.json";
@@ -2361,39 +2434,55 @@ function prompt(question) {
2361
2434
  function findProjectRoot(startDir) {
2362
2435
  let dir = startDir ?? process.cwd();
2363
2436
  while (true) {
2364
- if (fs2.existsSync(path2.join(dir, CLAUDE_DIR))) {
2437
+ if (fs3.existsSync(path3.join(dir, CLAUDE_DIR))) {
2365
2438
  return dir;
2366
2439
  }
2367
- const parent = path2.dirname(dir);
2440
+ const parent = path3.dirname(dir);
2368
2441
  if (parent === dir) break;
2369
2442
  dir = parent;
2370
2443
  }
2371
2444
  return startDir ?? process.cwd();
2372
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
+ }
2373
2462
  function getClaudeDir(projectRoot) {
2374
- return path2.join(projectRoot, CLAUDE_DIR);
2463
+ return path3.join(projectRoot, CLAUDE_DIR);
2375
2464
  }
2376
2465
  function getSettingsPath(projectRoot) {
2377
- return path2.join(getClaudeDir(projectRoot), SETTINGS_FILE);
2466
+ return path3.join(getClaudeDir(projectRoot), SETTINGS_FILE);
2378
2467
  }
2379
2468
  function getMonitorConfigPath(projectRoot) {
2380
- return path2.join(getClaudeDir(projectRoot), MONITOR_CONFIG_FILE);
2469
+ return path3.join(getClaudeDir(projectRoot), MONITOR_CONFIG_FILE);
2381
2470
  }
2382
2471
  function readJsonFile(filePath) {
2383
2472
  try {
2384
- if (!fs2.existsSync(filePath)) return null;
2385
- const content = fs2.readFileSync(filePath, "utf-8");
2473
+ if (!fs3.existsSync(filePath)) return null;
2474
+ const content = fs3.readFileSync(filePath, "utf-8");
2386
2475
  return JSON.parse(content);
2387
2476
  } catch {
2388
2477
  return null;
2389
2478
  }
2390
2479
  }
2391
2480
  function writeJsonFile(filePath, data) {
2392
- const dir = path2.dirname(filePath);
2393
- if (!fs2.existsSync(dir)) {
2394
- fs2.mkdirSync(dir, { recursive: true });
2481
+ const dir = path3.dirname(filePath);
2482
+ if (!fs3.existsSync(dir)) {
2483
+ fs3.mkdirSync(dir, { recursive: true });
2395
2484
  }
2396
- fs2.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
2485
+ fs3.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
2397
2486
  }
2398
2487
  function loadMonitorConfig(projectRoot) {
2399
2488
  const root = projectRoot ?? findProjectRoot();
@@ -2433,7 +2522,7 @@ async function initCommand() {
2433
2522
  }
2434
2523
  console.log("Setting up Claude Code monitoring for this workspace...\n");
2435
2524
  const projectRoot = process.cwd();
2436
- const dirName = path2.basename(projectRoot);
2525
+ const dirName = path3.basename(projectRoot);
2437
2526
  let agent;
2438
2527
  const choice = await prompt(
2439
2528
  "Create a new agent or use an existing one? (new/existing) [new]: "
@@ -2484,8 +2573,8 @@ Select agent (1-${agents.length}): `);
2484
2573
  }
2485
2574
  }
2486
2575
  const claudeDir = getClaudeDir(projectRoot);
2487
- if (!fs2.existsSync(claudeDir)) {
2488
- fs2.mkdirSync(claudeDir, { recursive: true });
2576
+ if (!fs3.existsSync(claudeDir)) {
2577
+ fs3.mkdirSync(claudeDir, { recursive: true });
2489
2578
  }
2490
2579
  const settingsPath = getSettingsPath(projectRoot);
2491
2580
  const existingSettings = readJsonFile(settingsPath) ?? {};
@@ -2506,7 +2595,7 @@ Select agent (1-${agents.length}): `);
2506
2595
  };
2507
2596
  const monitorConfigPath = getMonitorConfigPath(projectRoot);
2508
2597
  writeJsonFile(monitorConfigPath, monitorConfig);
2509
- fs2.chmodSync(monitorConfigPath, 384);
2598
+ fs3.chmodSync(monitorConfigPath, 384);
2510
2599
  console.log("");
2511
2600
  console.log(`\u2713 Agent "${agent.name}" configured (ID: ${agent.id})`);
2512
2601
  if (agent.apiKey?.key) {
@@ -2550,7 +2639,7 @@ function debugLog(label, data) {
2550
2639
  const logPath = `/tmp/olakai-monitor-debug-${process.pid}.log`;
2551
2640
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${label}: ${typeof data === "string" ? data : JSON.stringify(data, null, 2)}
2552
2641
  `;
2553
- fs2.appendFileSync(logPath, line, "utf-8");
2642
+ fs3.appendFileSync(logPath, line, "utf-8");
2554
2643
  } catch {
2555
2644
  }
2556
2645
  }
@@ -2567,7 +2656,7 @@ function extractFromTranscript(transcriptPath) {
2567
2656
  if (!transcriptPath) return empty;
2568
2657
  let raw;
2569
2658
  try {
2570
- raw = fs2.readFileSync(transcriptPath, "utf-8");
2659
+ raw = fs3.readFileSync(transcriptPath, "utf-8");
2571
2660
  } catch (err) {
2572
2661
  debugLog("transcript-read-failed", {
2573
2662
  transcriptPath,
@@ -2592,11 +2681,8 @@ function extractSubagentName(event) {
2592
2681
  return void 0;
2593
2682
  }
2594
2683
  async function hookCommand(event) {
2684
+ setDebugLogger(debugLog);
2595
2685
  try {
2596
- const config = loadMonitorConfig();
2597
- if (!config) {
2598
- return;
2599
- }
2600
2686
  const stdinData = await readStdin(3e3);
2601
2687
  debugLog("stdin-raw", stdinData);
2602
2688
  let eventData = {};
@@ -2605,15 +2691,42 @@ async function hookCommand(event) {
2605
2691
  eventData = JSON.parse(stdinData);
2606
2692
  } catch {
2607
2693
  debugLog("stdin-parse-failed", stdinData);
2608
- return;
2609
2694
  }
2610
2695
  }
2611
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
+ }
2612
2712
  const payload = buildPayload(event, eventData, config);
2613
2713
  if (!payload) {
2614
2714
  return;
2615
2715
  }
2616
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
+ }
2617
2730
  const controller = new AbortController();
2618
2731
  const timeoutId = setTimeout(() => controller.abort(), 5e3);
2619
2732
  try {
@@ -2633,6 +2746,18 @@ async function hookCommand(event) {
2633
2746
  } finally {
2634
2747
  clearTimeout(timeoutId);
2635
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
+ }
2636
2761
  } catch (err) {
2637
2762
  debugLog("hook-exception", err.message);
2638
2763
  }
@@ -2665,9 +2790,11 @@ function buildPayload(event, eventData, config) {
2665
2790
  } else if (extracted.skill) {
2666
2791
  customData.skill = extracted.skill;
2667
2792
  }
2793
+ const payloadAssistant = eventData.last_assistant_message;
2794
+ const response = typeof payloadAssistant === "string" && payloadAssistant.trim() ? payloadAssistant : extracted.response;
2668
2795
  return {
2669
2796
  prompt: extracted.prompt,
2670
- response: extracted.response,
2797
+ response,
2671
2798
  chatId: sessionId,
2672
2799
  // Top-level source so the backend ExternalPromptRequestSchema
2673
2800
  // picks it up (matches regex ^[a-z0-9_-]+$).
@@ -2774,8 +2901,8 @@ async function disableCommand(options) {
2774
2901
  }
2775
2902
  if (!options.keepConfig) {
2776
2903
  const configPath = getMonitorConfigPath(projectRoot);
2777
- if (fs2.existsSync(configPath)) {
2778
- fs2.unlinkSync(configPath);
2904
+ if (fs3.existsSync(configPath)) {
2905
+ fs3.unlinkSync(configPath);
2779
2906
  console.log(`\u2713 Monitor config removed (${CLAUDE_DIR}/${MONITOR_CONFIG_FILE})`);
2780
2907
  }
2781
2908
  } else {