@open-code-review/cli 1.9.0 → 1.10.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.
@@ -18410,10 +18410,10 @@ var require_view = __commonJS({
18410
18410
  var debug = require_src()("express:view");
18411
18411
  var path2 = __require("path");
18412
18412
  var fs6 = __require("fs");
18413
- var dirname11 = path2.dirname;
18413
+ var dirname12 = path2.dirname;
18414
18414
  var basename3 = path2.basename;
18415
18415
  var extname = path2.extname;
18416
- var join14 = path2.join;
18416
+ var join15 = path2.join;
18417
18417
  var resolve3 = path2.resolve;
18418
18418
  module.exports = View;
18419
18419
  function View(name, options) {
@@ -18449,7 +18449,7 @@ var require_view = __commonJS({
18449
18449
  for (var i = 0; i < roots.length && !path3; i++) {
18450
18450
  var root = roots[i];
18451
18451
  var loc = resolve3(root, name);
18452
- var dir = dirname11(loc);
18452
+ var dir = dirname12(loc);
18453
18453
  var file = basename3(loc);
18454
18454
  path3 = this.resolve(dir, file);
18455
18455
  }
@@ -18461,12 +18461,12 @@ var require_view = __commonJS({
18461
18461
  };
18462
18462
  View.prototype.resolve = function resolve4(dir, file) {
18463
18463
  var ext = this.ext;
18464
- var path3 = join14(dir, file);
18464
+ var path3 = join15(dir, file);
18465
18465
  var stat = tryStat(path3);
18466
18466
  if (stat && stat.isFile()) {
18467
18467
  return path3;
18468
18468
  }
18469
- path3 = join14(dir, basename3(file, ext), "index" + ext);
18469
+ path3 = join15(dir, basename3(file, ext), "index" + ext);
18470
18470
  stat = tryStat(path3);
18471
18471
  if (stat && stat.isFile()) {
18472
18472
  return path3;
@@ -19099,7 +19099,7 @@ var require_send = __commonJS({
19099
19099
  var Stream = __require("stream");
19100
19100
  var util = __require("util");
19101
19101
  var extname = path2.extname;
19102
- var join14 = path2.join;
19102
+ var join15 = path2.join;
19103
19103
  var normalize = path2.normalize;
19104
19104
  var resolve3 = path2.resolve;
19105
19105
  var sep = path2.sep;
@@ -19318,7 +19318,7 @@ var require_send = __commonJS({
19318
19318
  return res;
19319
19319
  }
19320
19320
  parts = path3.split(sep);
19321
- path3 = normalize(join14(root, path3));
19321
+ path3 = normalize(join15(root, path3));
19322
19322
  } else {
19323
19323
  if (UP_PATH_REGEXP.test(path3)) {
19324
19324
  debug('malicious path "%s"', path3);
@@ -19453,7 +19453,7 @@ var require_send = __commonJS({
19453
19453
  if (err) return self.onStatError(err);
19454
19454
  return self.error(404);
19455
19455
  }
19456
- var p = join14(path3, self._index[i]);
19456
+ var p = join15(path3, self._index[i]);
19457
19457
  debug('stat "%s"', p);
19458
19458
  fs6.stat(p, function(err2, stat) {
19459
19459
  if (err2) return next(err2);
@@ -23181,8 +23181,8 @@ var init_open = __esm({
23181
23181
  // src/server/index.ts
23182
23182
  var import_express12 = __toESM(require_express2(), 1);
23183
23183
  import { createServer } from "node:http";
23184
- import { existsSync as existsSync9, readFileSync as readFileSync10, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, mkdirSync as mkdirSync3 } from "node:fs";
23185
- import { join as join13, dirname as dirname10, resolve as resolve2 } from "node:path";
23184
+ import { existsSync as existsSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2, mkdirSync as mkdirSync4 } from "node:fs";
23185
+ import { join as join14, dirname as dirname11, resolve as resolve2 } from "node:path";
23186
23186
  import { fileURLToPath as fileURLToPath3 } from "node:url";
23187
23187
  import { randomBytes } from "node:crypto";
23188
23188
  import { Server as SocketIOServer } from "socket.io";
@@ -23208,12 +23208,12 @@ function resolveOcrDir(startDir) {
23208
23208
  }
23209
23209
 
23210
23210
  // src/server/db.ts
23211
- import { existsSync as existsSync2, readFileSync, renameSync, writeFileSync, mkdirSync } from "node:fs";
23212
- import { join as join3, dirname as dirname3 } from "node:path";
23211
+ import { existsSync as existsSync3, readFileSync as readFileSync2, renameSync as renameSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "node:fs";
23212
+ import { join as join4, dirname as dirname4 } from "node:path";
23213
23213
  import initSqlJs2 from "sql.js";
23214
23214
 
23215
23215
  // ../cli/src/lib/db/index.ts
23216
- import { dirname as dirname2, join as join2 } from "node:path";
23216
+ import { dirname as dirname3, join as join3 } from "node:path";
23217
23217
  import { createRequire } from "node:module";
23218
23218
  import initSqlJs from "sql.js";
23219
23219
 
@@ -23464,6 +23464,14 @@ var MIGRATIONS = [
23464
23464
  ALTER TABLE map_runs ADD COLUMN source TEXT DEFAULT NULL;
23465
23465
  ALTER TABLE map_runs ADD COLUMN section_count INTEGER DEFAULT 0;
23466
23466
  `
23467
+ },
23468
+ {
23469
+ version: 9,
23470
+ description: "Add uid column to command_executions for JSONL-backed recovery",
23471
+ sql: `
23472
+ ALTER TABLE command_executions ADD COLUMN uid TEXT;
23473
+ CREATE UNIQUE INDEX idx_command_executions_uid ON command_executions(uid);
23474
+ `
23467
23475
  }
23468
23476
  ];
23469
23477
  function ensureSchemaVersionTable(db) {
@@ -23526,11 +23534,108 @@ function resultToRow(result) {
23526
23534
  return rows[0];
23527
23535
  }
23528
23536
 
23537
+ // ../cli/src/lib/db/command-log.ts
23538
+ import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
23539
+ import { dirname as dirname2, join as join2 } from "node:path";
23540
+ import { randomUUID } from "node:crypto";
23541
+ var CACHE_DIR = ".cache";
23542
+ var FILENAME = "command-history.jsonl";
23543
+ var MAX_LINES = 5e3;
23544
+ var KEEP_LINES = 4e3;
23545
+ var approxLineCount = -1;
23546
+ function generateCommandUid() {
23547
+ return randomUUID();
23548
+ }
23549
+ function cacheDir(ocrDir) {
23550
+ return join2(ocrDir, "data", CACHE_DIR);
23551
+ }
23552
+ function commandLogPath(ocrDir) {
23553
+ return join2(cacheDir(ocrDir), FILENAME);
23554
+ }
23555
+ function appendCommandLog(ocrDir, entry) {
23556
+ try {
23557
+ const filePath = commandLogPath(ocrDir);
23558
+ const dir = dirname2(filePath);
23559
+ if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
23560
+ const line = JSON.stringify(entry) + "\n";
23561
+ appendFileSync(filePath, line, { encoding: "utf-8" });
23562
+ if (approxLineCount >= 0) approxLineCount++;
23563
+ rotateIfNeeded(filePath);
23564
+ } catch {
23565
+ }
23566
+ }
23567
+ function readCommandLog(ocrDir) {
23568
+ const filePath = commandLogPath(ocrDir);
23569
+ if (!existsSync2(filePath)) return [];
23570
+ const content = readFileSync(filePath, "utf-8");
23571
+ const entries = [];
23572
+ for (const line of content.split("\n")) {
23573
+ if (!line.trim()) continue;
23574
+ try {
23575
+ entries.push(JSON.parse(line));
23576
+ } catch {
23577
+ }
23578
+ }
23579
+ return entries;
23580
+ }
23581
+ function replayCommandLog(db, ocrDir) {
23582
+ const entries = readCommandLog(ocrDir);
23583
+ if (entries.length === 0) return 0;
23584
+ const latest = /* @__PURE__ */ new Map();
23585
+ for (const entry of entries) {
23586
+ if (!entry.uid || !entry.command || !entry.started_at) continue;
23587
+ const existing = latest.get(entry.uid);
23588
+ if (!existing || entry.event !== "start") {
23589
+ latest.set(entry.uid, entry);
23590
+ }
23591
+ }
23592
+ let imported = 0;
23593
+ for (const entry of latest.values()) {
23594
+ if (entry.event === "start" && !entry.finished_at) continue;
23595
+ const existing = db.exec(
23596
+ "SELECT COUNT(*) as c FROM command_executions WHERE uid = ?",
23597
+ [entry.uid]
23598
+ );
23599
+ if ((existing[0]?.values[0]?.[0] ?? 0) > 0) continue;
23600
+ db.run(
23601
+ `INSERT INTO command_executions
23602
+ (uid, command, args, exit_code, started_at, finished_at, pid, is_detached)
23603
+ VALUES (?, ?, ?, ?, ?, ?, NULL, ?)`,
23604
+ [
23605
+ entry.uid,
23606
+ entry.command,
23607
+ entry.args,
23608
+ entry.exit_code,
23609
+ entry.started_at,
23610
+ entry.finished_at,
23611
+ entry.is_detached
23612
+ ]
23613
+ );
23614
+ imported++;
23615
+ }
23616
+ return imported;
23617
+ }
23618
+ function rotateIfNeeded(filePath) {
23619
+ try {
23620
+ if (approxLineCount >= 0 && approxLineCount <= MAX_LINES) return;
23621
+ const content = readFileSync(filePath, "utf-8");
23622
+ const lines = content.split("\n").filter((l) => l.trim());
23623
+ approxLineCount = lines.length;
23624
+ if (approxLineCount <= MAX_LINES) return;
23625
+ const kept = lines.slice(lines.length - KEEP_LINES);
23626
+ const tmpPath = `${filePath}.${process.pid}.tmp`;
23627
+ writeFileSync(tmpPath, kept.join("\n") + "\n", { encoding: "utf-8" });
23628
+ renameSync(tmpPath, filePath);
23629
+ approxLineCount = KEEP_LINES;
23630
+ } catch {
23631
+ }
23632
+ }
23633
+
23529
23634
  // ../cli/src/lib/db/index.ts
23530
23635
  function locateWasm() {
23531
23636
  const require2 = createRequire(import.meta.url);
23532
23637
  const sqlJsPath = require2.resolve("sql.js");
23533
- return join2(dirname2(sqlJsPath), "sql-wasm.wasm");
23638
+ return join3(dirname3(sqlJsPath), "sql-wasm.wasm");
23534
23639
  }
23535
23640
  function applyPragmas(db) {
23536
23641
  db.run("PRAGMA foreign_keys = ON;");
@@ -23548,24 +23653,24 @@ function registerSaveHooks(preSave, postSave) {
23548
23653
  postSaveHook = postSave;
23549
23654
  }
23550
23655
  async function openDb(ocrDir) {
23551
- const dbPath = join3(ocrDir, "data", "ocr.db");
23656
+ const dbPath = join4(ocrDir, "data", "ocr.db");
23552
23657
  if (cachedDb && cachedDbPath === dbPath) {
23553
23658
  return cachedDb;
23554
23659
  }
23555
- const wasmBuffer = readFileSync(locateWasm());
23660
+ const wasmBuffer = readFileSync2(locateWasm());
23556
23661
  const wasmBinary = wasmBuffer.buffer.slice(
23557
23662
  wasmBuffer.byteOffset,
23558
23663
  wasmBuffer.byteOffset + wasmBuffer.byteLength
23559
23664
  );
23560
23665
  const SQL = await initSqlJs2({ wasmBinary });
23561
23666
  let db;
23562
- if (existsSync2(dbPath)) {
23563
- const fileBuffer = readFileSync(dbPath);
23667
+ if (existsSync3(dbPath)) {
23668
+ const fileBuffer = readFileSync2(dbPath);
23564
23669
  db = new SQL.Database(fileBuffer);
23565
23670
  } else {
23566
- const dataDir = dirname3(dbPath);
23567
- if (!existsSync2(dataDir)) {
23568
- mkdirSync(dataDir, { recursive: true });
23671
+ const dataDir = dirname4(dbPath);
23672
+ if (!existsSync3(dataDir)) {
23673
+ mkdirSync2(dataDir, { recursive: true });
23569
23674
  }
23570
23675
  db = new SQL.Database();
23571
23676
  }
@@ -23577,15 +23682,15 @@ async function openDb(ocrDir) {
23577
23682
  }
23578
23683
  function saveDb(db, ocrDir) {
23579
23684
  preSaveHook?.();
23580
- const dbPath = join3(ocrDir, "data", "ocr.db");
23685
+ const dbPath = join4(ocrDir, "data", "ocr.db");
23581
23686
  const data = db.export();
23582
- const dir = dirname3(dbPath);
23583
- if (!existsSync2(dir)) {
23584
- mkdirSync(dir, { recursive: true });
23687
+ const dir = dirname4(dbPath);
23688
+ if (!existsSync3(dir)) {
23689
+ mkdirSync2(dir, { recursive: true });
23585
23690
  }
23586
- const tmpPath = dbPath + ".tmp";
23587
- writeFileSync(tmpPath, Buffer.from(data));
23588
- renameSync(tmpPath, dbPath);
23691
+ const tmpPath = `${dbPath}.${process.pid}.tmp`;
23692
+ writeFileSync2(tmpPath, Buffer.from(data));
23693
+ renameSync2(tmpPath, dbPath);
23589
23694
  postSaveHook?.();
23590
23695
  }
23591
23696
  function closeDb() {
@@ -24798,12 +24903,12 @@ var import_express8 = __toESM(require_express2(), 1);
24798
24903
 
24799
24904
  // src/server/socket/command-runner.ts
24800
24905
  import { spawn as spawn3 } from "node:child_process";
24801
- import { readFileSync as readFileSync3 } from "node:fs";
24802
- import { dirname as dirname5, join as join7 } from "node:path";
24906
+ import { readFileSync as readFileSync4 } from "node:fs";
24907
+ import { dirname as dirname6, join as join8 } from "node:path";
24803
24908
 
24804
24909
  // src/server/services/ai-cli/index.ts
24805
- import { readFileSync as readFileSync2 } from "node:fs";
24806
- import { join as join5 } from "node:path";
24910
+ import { readFileSync as readFileSync3 } from "node:fs";
24911
+ import { join as join6 } from "node:path";
24807
24912
 
24808
24913
  // src/server/services/ai-cli/claude-adapter.ts
24809
24914
  import { execFileSync } from "node:child_process";
@@ -24811,7 +24916,7 @@ import { spawn } from "node:child_process";
24811
24916
 
24812
24917
  // src/server/services/ai-cli/helpers.ts
24813
24918
  import { tmpdir } from "node:os";
24814
- import { join as join4 } from "node:path";
24919
+ import { join as join5 } from "node:path";
24815
24920
  function formatToolDetail(tool, input) {
24816
24921
  switch (tool) {
24817
24922
  case "Read":
@@ -24847,7 +24952,7 @@ function extractAssistantText(parsed) {
24847
24952
  }
24848
24953
  return text;
24849
24954
  }
24850
- var TEMP_BASE = join4(tmpdir(), "ocr-ai-prompts");
24955
+ var TEMP_BASE = join5(tmpdir(), "ocr-ai-prompts");
24851
24956
 
24852
24957
  // src/server/socket/env.ts
24853
24958
  var ENV_ALLOWLIST = [
@@ -25081,8 +25186,8 @@ function extractToolInput(part) {
25081
25186
  // src/server/services/ai-cli/index.ts
25082
25187
  function readAiCliPreference(ocrDir) {
25083
25188
  try {
25084
- const configPath = join5(ocrDir, "config.yaml");
25085
- const content = readFileSync2(configPath, "utf-8");
25189
+ const configPath = join6(ocrDir, "config.yaml");
25190
+ const content = readFileSync3(configPath, "utf-8");
25086
25191
  const match = content.match(/^\s*ai_cli:\s*(\S+)/m);
25087
25192
  const value = match?.[1] ?? "auto";
25088
25193
  if (value === "claude" || value === "opencode" || value === "off") return value;
@@ -25163,24 +25268,24 @@ var AiCliService = class {
25163
25268
  };
25164
25269
 
25165
25270
  // src/server/socket/cli-resolver.ts
25166
- import { existsSync as existsSync3 } from "node:fs";
25167
- import { dirname as dirname4, join as join6 } from "node:path";
25271
+ import { existsSync as existsSync4 } from "node:fs";
25272
+ import { dirname as dirname5, join as join7 } from "node:path";
25168
25273
  import { fileURLToPath } from "node:url";
25169
- var __dirname = dirname4(fileURLToPath(import.meta.url));
25274
+ var __dirname = dirname5(fileURLToPath(import.meta.url));
25170
25275
  function resolveLocalCli() {
25171
- const parentDir = join6(__dirname, "..");
25172
- const bundledCli = join6(parentDir, "index.js");
25173
- if (existsSync3(bundledCli) && existsSync3(join6(parentDir, "dashboard", "server.js"))) {
25276
+ const parentDir = join7(__dirname, "..");
25277
+ const bundledCli = join7(parentDir, "index.js");
25278
+ if (existsSync4(bundledCli) && existsSync4(join7(parentDir, "dashboard", "server.js"))) {
25174
25279
  return bundledCli;
25175
25280
  }
25176
25281
  let dir = __dirname;
25177
25282
  for (let i = 0; i < 8; i++) {
25178
- if (existsSync3(join6(dir, "nx.json"))) {
25179
- const candidate = join6(dir, "packages", "cli", "dist", "index.js");
25180
- if (existsSync3(candidate)) return candidate;
25283
+ if (existsSync4(join7(dir, "nx.json"))) {
25284
+ const candidate = join7(dir, "packages", "cli", "dist", "index.js");
25285
+ if (existsSync4(candidate)) return candidate;
25181
25286
  break;
25182
25287
  }
25183
- const parent = dirname4(dir);
25288
+ const parent = dirname5(dir);
25184
25289
  if (parent === dir) break;
25185
25290
  dir = parent;
25186
25291
  }
@@ -25267,17 +25372,34 @@ function registerCommandHandlers(io2, socket, db, ocrDir, aiCliService) {
25267
25372
  return;
25268
25373
  }
25269
25374
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
25375
+ const uid = generateCommandUid();
25376
+ const argsJson = JSON.stringify(subArgs);
25270
25377
  db.run(
25271
- `INSERT INTO command_executions (command, args, started_at)
25272
- VALUES (?, ?, ?)`,
25273
- [command, JSON.stringify(subArgs), startedAt]
25378
+ `INSERT INTO command_executions (uid, command, args, started_at)
25379
+ VALUES (?, ?, ?, ?)`,
25380
+ [uid, command, argsJson, startedAt]
25274
25381
  );
25275
25382
  const idResult = db.exec("SELECT last_insert_rowid() as id");
25276
25383
  const executionId = idResult[0]?.values[0]?.[0] ?? 0;
25384
+ appendCommandLog(ocrDir, {
25385
+ v: 1,
25386
+ uid,
25387
+ db_id: executionId,
25388
+ command,
25389
+ args: argsJson,
25390
+ exit_code: null,
25391
+ started_at: startedAt,
25392
+ finished_at: null,
25393
+ is_detached: AI_COMMANDS.has(baseCommand) ? 1 : 0,
25394
+ event: "start",
25395
+ writer: "dashboard"
25396
+ });
25277
25397
  const isAi = AI_COMMANDS.has(baseCommand);
25278
25398
  const entry = {
25279
25399
  process: null,
25280
25400
  executionId,
25401
+ uid,
25402
+ argsJson,
25281
25403
  outputBuffer: "",
25282
25404
  commandStr: command,
25283
25405
  startedAt,
@@ -25344,7 +25466,7 @@ function registerCommandHandlers(io2, socket, db, ocrDir, aiCliService) {
25344
25466
  }
25345
25467
  function spawnCliCommand(io2, db, ocrDir, executionId, baseCommand, subArgs, entry) {
25346
25468
  const localCli = resolveLocalCli();
25347
- const repoRoot = dirname5(ocrDir);
25469
+ const repoRoot = dirname6(ocrDir);
25348
25470
  const proc = localCli ? spawn3("node", [localCli, baseCommand, ...subArgs], {
25349
25471
  cwd: repoRoot,
25350
25472
  env: cleanEnv()
@@ -25386,10 +25508,10 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
25386
25508
  finishExecution(io2, db, ocrDir, executionId, 1, content);
25387
25509
  return;
25388
25510
  }
25389
- const commandMdPath = join7(ocrDir, "commands", `${baseCommand}.md`);
25511
+ const commandMdPath = join8(ocrDir, "commands", `${baseCommand}.md`);
25390
25512
  let commandContent;
25391
25513
  try {
25392
- commandContent = readFileSync3(commandMdPath, "utf-8");
25514
+ commandContent = readFileSync4(commandMdPath, "utf-8");
25393
25515
  } catch {
25394
25516
  const content = `Error: Could not read command file at ${commandMdPath}
25395
25517
  `;
@@ -25483,7 +25605,7 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
25483
25605
  }
25484
25606
  promptLines.push("", "---", "", commandContent);
25485
25607
  const prompt = promptLines.join("\n");
25486
- const repoRoot = dirname5(ocrDir);
25608
+ const repoRoot = dirname6(ocrDir);
25487
25609
  const { process: proc, detached } = adapter.spawn({ mode: "workflow", prompt, cwd: repoRoot });
25488
25610
  entry.process = proc;
25489
25611
  entry.detached = detached;
@@ -25597,6 +25719,7 @@ ${stderrBuffer}`;
25597
25719
  }
25598
25720
  function finishExecution(io2, db, ocrDir, executionId, code, output) {
25599
25721
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
25722
+ const entry = activeCommands.get(executionId);
25600
25723
  db.run(
25601
25724
  `UPDATE command_executions
25602
25725
  SET exit_code = ?, finished_at = ?, output = ?, pid = NULL
@@ -25604,6 +25727,21 @@ function finishExecution(io2, db, ocrDir, executionId, code, output) {
25604
25727
  [code, finishedAt, output, executionId]
25605
25728
  );
25606
25729
  saveDb(db, ocrDir);
25730
+ if (entry?.uid) {
25731
+ appendCommandLog(ocrDir, {
25732
+ v: 1,
25733
+ uid: entry.uid,
25734
+ db_id: executionId,
25735
+ command: entry.commandStr,
25736
+ args: entry.argsJson ?? null,
25737
+ exit_code: code,
25738
+ started_at: entry.startedAt,
25739
+ finished_at: finishedAt,
25740
+ is_detached: entry.detached ? 1 : 0,
25741
+ event: code === -2 ? "cancel" : "finish",
25742
+ writer: "dashboard"
25743
+ });
25744
+ }
25607
25745
  io2.emit("command:finished", {
25608
25746
  execution_id: executionId,
25609
25747
  exitCode: code,
@@ -25672,9 +25810,9 @@ function createCommandsRouter(db) {
25672
25810
 
25673
25811
  // src/server/routes/config.ts
25674
25812
  var import_express9 = __toESM(require_express2(), 1);
25675
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
25813
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
25676
25814
  import { execFileSync as execFileSync3 } from "node:child_process";
25677
- import { join as join8, dirname as dirname6, basename } from "node:path";
25815
+ import { join as join9, dirname as dirname7, basename } from "node:path";
25678
25816
  var VALID_IDES = ["vscode", "cursor", "windsurf", "jetbrains", "sublime"];
25679
25817
  function detectIde() {
25680
25818
  const termProgram = process.env.TERM_PROGRAM?.toLowerCase() ?? "";
@@ -25691,8 +25829,8 @@ function detectIde() {
25691
25829
  }
25692
25830
  function readIdeFromConfig(ocrDir) {
25693
25831
  try {
25694
- const configPath = join8(ocrDir, "config.yaml");
25695
- const content = readFileSync4(configPath, "utf-8");
25832
+ const configPath = join9(ocrDir, "config.yaml");
25833
+ const content = readFileSync5(configPath, "utf-8");
25696
25834
  const match = content.match(/^\s*ide:\s*(\S+)/m);
25697
25835
  return match?.[1] ?? "auto";
25698
25836
  } catch {
@@ -25719,7 +25857,7 @@ function detectGitBranch(cwd) {
25719
25857
  }
25720
25858
  function createConfigRouter(ocrDir, aiCliService) {
25721
25859
  const router = (0, import_express9.Router)();
25722
- const projectRoot = dirname6(ocrDir);
25860
+ const projectRoot = dirname7(ocrDir);
25723
25861
  const workspaceName = basename(projectRoot);
25724
25862
  const gitBranch = detectGitBranch(projectRoot);
25725
25863
  router.get("/", (_req, res) => {
@@ -25738,8 +25876,8 @@ function createConfigRouter(ocrDir, aiCliService) {
25738
25876
  return;
25739
25877
  }
25740
25878
  try {
25741
- const configPath = join8(ocrDir, "config.yaml");
25742
- let content = readFileSync4(configPath, "utf-8");
25879
+ const configPath = join9(ocrDir, "config.yaml");
25880
+ let content = readFileSync5(configPath, "utf-8");
25743
25881
  if (content.match(/^\s*ide:\s*\S+/m)) {
25744
25882
  content = content.replace(/^(\s*ide:\s*)\S+/m, `$1${ide}`);
25745
25883
  } else if (content.includes("dashboard:")) {
@@ -25751,7 +25889,7 @@ dashboard:
25751
25889
  ide: ${ide}
25752
25890
  `;
25753
25891
  }
25754
- writeFileSync2(configPath, content);
25892
+ writeFileSync3(configPath, content);
25755
25893
  res.json({ ide });
25756
25894
  } catch (err) {
25757
25895
  console.error("Failed to update config:", err);
@@ -25828,15 +25966,15 @@ function createChatRouter(db, ocrDir) {
25828
25966
 
25829
25967
  // src/server/routes/reviewers.ts
25830
25968
  var import_express11 = __toESM(require_express2(), 1);
25831
- import { readFileSync as readFileSync5, existsSync as existsSync4, watch } from "node:fs";
25832
- import { join as join9 } from "node:path";
25969
+ import { readFileSync as readFileSync6, existsSync as existsSync5, watch } from "node:fs";
25970
+ import { join as join10 } from "node:path";
25833
25971
  function readReviewersMeta(ocrDir) {
25834
- const metaPath = join9(ocrDir, "reviewers-meta.json");
25835
- if (!existsSync4(metaPath)) {
25972
+ const metaPath = join10(ocrDir, "reviewers-meta.json");
25973
+ if (!existsSync5(metaPath)) {
25836
25974
  return { reviewers: [], defaults: [] };
25837
25975
  }
25838
25976
  try {
25839
- const raw = readFileSync5(metaPath, "utf-8");
25977
+ const raw = readFileSync6(metaPath, "utf-8");
25840
25978
  const meta = JSON.parse(raw);
25841
25979
  const reviewers = meta.reviewers ?? [];
25842
25980
  const defaults = reviewers.filter((r) => r.is_default).map((r) => r.id);
@@ -25857,13 +25995,13 @@ function createReviewersRouter(ocrDir) {
25857
25995
  res.status(400).json({ error: "Invalid reviewer ID" });
25858
25996
  return;
25859
25997
  }
25860
- const filePath = join9(ocrDir, "skills", "references", "reviewers", `${id}.md`);
25861
- if (!existsSync4(filePath)) {
25998
+ const filePath = join10(ocrDir, "skills", "references", "reviewers", `${id}.md`);
25999
+ if (!existsSync5(filePath)) {
25862
26000
  res.status(404).json({ error: "Reviewer not found", id });
25863
26001
  return;
25864
26002
  }
25865
26003
  try {
25866
- const content = readFileSync5(filePath, "utf-8");
26004
+ const content = readFileSync6(filePath, "utf-8");
25867
26005
  res.json({ id, content });
25868
26006
  } catch {
25869
26007
  res.status(500).json({ error: "Failed to read reviewer file", id });
@@ -25872,7 +26010,7 @@ function createReviewersRouter(ocrDir) {
25872
26010
  return router;
25873
26011
  }
25874
26012
  function watchReviewersMeta(ocrDir, io2) {
25875
- const metaPath = join9(ocrDir, "reviewers-meta.json");
26013
+ const metaPath = join10(ocrDir, "reviewers-meta.json");
25876
26014
  let watcher = null;
25877
26015
  let debounce;
25878
26016
  try {
@@ -25894,8 +26032,8 @@ function watchReviewersMeta(ocrDir, io2) {
25894
26032
  }
25895
26033
 
25896
26034
  // src/server/services/filesystem-sync.ts
25897
- import { readdirSync, readFileSync as readFileSync6, statSync, existsSync as existsSync5 } from "node:fs";
25898
- import { join as join10, basename as basename2, dirname as dirname7, relative } from "node:path";
26035
+ import { readdirSync, readFileSync as readFileSync7, statSync, existsSync as existsSync6 } from "node:fs";
26036
+ import { join as join11, basename as basename2, dirname as dirname8, relative } from "node:path";
25899
26037
  import { watch as watch2 } from "chokidar";
25900
26038
 
25901
26039
  // src/server/services/parsers/reviewer-parser.ts
@@ -26077,67 +26215,67 @@ var FilesystemSync = class {
26077
26215
  onSync;
26078
26216
  // ── 6.1: Full Scan ──
26079
26217
  async fullScan() {
26080
- if (!existsSync5(this.sessionsDir)) return;
26218
+ if (!existsSync6(this.sessionsDir)) return;
26081
26219
  const entries = readdirSync(this.sessionsDir, { withFileTypes: true });
26082
26220
  for (const entry of entries) {
26083
26221
  if (!entry.isDirectory()) continue;
26084
26222
  const sessionId = entry.name;
26085
- const sessionDir = join10(this.sessionsDir, sessionId);
26223
+ const sessionDir = join11(this.sessionsDir, sessionId);
26086
26224
  this.syncSession(sessionId, sessionDir);
26087
26225
  }
26088
26226
  }
26089
26227
  syncSession(sessionId, sessionDir) {
26090
26228
  this.ensureSessionRow(sessionId, sessionDir);
26091
- const roundsDir = join10(sessionDir, "rounds");
26092
- if (existsSync5(roundsDir)) {
26229
+ const roundsDir = join11(sessionDir, "rounds");
26230
+ if (existsSync6(roundsDir)) {
26093
26231
  const rounds = readdirSync(roundsDir, { withFileTypes: true });
26094
26232
  for (const roundEntry of rounds) {
26095
26233
  if (!roundEntry.isDirectory()) continue;
26096
26234
  const roundMatch = roundEntry.name.match(/^round-(\d+)$/);
26097
26235
  if (!roundMatch) continue;
26098
26236
  const roundNumber = parseInt(roundMatch[1] ?? "0", 10);
26099
- const roundDir = join10(roundsDir, roundEntry.name);
26100
- const reviewsDir = join10(roundDir, "reviews");
26101
- if (existsSync5(reviewsDir)) {
26237
+ const roundDir = join11(roundsDir, roundEntry.name);
26238
+ const reviewsDir = join11(roundDir, "reviews");
26239
+ if (existsSync6(reviewsDir)) {
26102
26240
  const reviewFiles = readdirSync(reviewsDir).filter((f) => f.endsWith(".md"));
26103
26241
  for (const reviewFile of reviewFiles) {
26104
- const filePath = join10(reviewsDir, reviewFile);
26242
+ const filePath = join11(reviewsDir, reviewFile);
26105
26243
  this.processReviewerOutput(sessionId, roundNumber, filePath, reviewFile);
26106
26244
  }
26107
26245
  }
26108
- const roundMetaPath = join10(roundDir, "round-meta.json");
26109
- if (existsSync5(roundMetaPath)) {
26246
+ const roundMetaPath = join11(roundDir, "round-meta.json");
26247
+ if (existsSync6(roundMetaPath)) {
26110
26248
  this.processRoundMeta(sessionId, roundNumber, roundMetaPath);
26111
26249
  }
26112
- const finalPath = join10(roundDir, "final.md");
26113
- if (existsSync5(finalPath)) {
26250
+ const finalPath = join11(roundDir, "final.md");
26251
+ if (existsSync6(finalPath)) {
26114
26252
  this.processFinalMd(sessionId, roundNumber, finalPath);
26115
26253
  }
26116
- const finalHumanPath = join10(roundDir, "final-human.md");
26117
- if (existsSync5(finalHumanPath)) {
26254
+ const finalHumanPath = join11(roundDir, "final-human.md");
26255
+ if (existsSync6(finalHumanPath)) {
26118
26256
  this.processGenericArtifact(sessionId, "final-human", finalHumanPath, roundNumber);
26119
26257
  }
26120
- const discoursePath = join10(roundDir, "discourse.md");
26121
- if (existsSync5(discoursePath)) {
26258
+ const discoursePath = join11(roundDir, "discourse.md");
26259
+ if (existsSync6(discoursePath)) {
26122
26260
  this.processGenericArtifact(sessionId, "discourse", discoursePath, roundNumber);
26123
26261
  }
26124
26262
  }
26125
26263
  }
26126
- const mapDir = join10(sessionDir, "map", "runs");
26127
- if (existsSync5(mapDir)) {
26264
+ const mapDir = join11(sessionDir, "map", "runs");
26265
+ if (existsSync6(mapDir)) {
26128
26266
  const runs = readdirSync(mapDir, { withFileTypes: true });
26129
26267
  for (const runEntry of runs) {
26130
26268
  if (!runEntry.isDirectory()) continue;
26131
26269
  const runMatch = runEntry.name.match(/^run-(\d+)$/);
26132
26270
  if (!runMatch) continue;
26133
26271
  const runNumber = parseInt(runMatch[1] ?? "0", 10);
26134
- const runDir = join10(mapDir, runEntry.name);
26135
- const mapMetaPath = join10(runDir, "map-meta.json");
26136
- if (existsSync5(mapMetaPath)) {
26272
+ const runDir = join11(mapDir, runEntry.name);
26273
+ const mapMetaPath = join11(runDir, "map-meta.json");
26274
+ if (existsSync6(mapMetaPath)) {
26137
26275
  this.processMapMeta(sessionId, runNumber, mapMetaPath);
26138
26276
  }
26139
- const mapPath = join10(runDir, "map.md");
26140
- if (existsSync5(mapPath)) {
26277
+ const mapPath = join11(runDir, "map.md");
26278
+ if (existsSync6(mapPath)) {
26141
26279
  this.processMapMd(sessionId, runNumber, mapPath);
26142
26280
  }
26143
26281
  const mapArtifacts = [
@@ -26146,8 +26284,8 @@ var FilesystemSync = class {
26146
26284
  ["requirements-mapping.md", "requirements-mapping"]
26147
26285
  ];
26148
26286
  for (const [fileName, artifactType] of mapArtifacts) {
26149
- const filePath = join10(runDir, fileName);
26150
- if (existsSync5(filePath)) {
26287
+ const filePath = join11(runDir, fileName);
26288
+ if (existsSync6(filePath)) {
26151
26289
  this.processGenericArtifact(sessionId, artifactType, filePath, void 0, runNumber);
26152
26290
  }
26153
26291
  }
@@ -26158,8 +26296,8 @@ var FilesystemSync = class {
26158
26296
  ["discovered-standards.md", "discovered-standards"]
26159
26297
  ];
26160
26298
  for (const [fileName, artifactType] of sessionArtifacts) {
26161
- const filePath = join10(sessionDir, fileName);
26162
- if (existsSync5(filePath)) {
26299
+ const filePath = join11(sessionDir, fileName);
26300
+ if (existsSync6(filePath)) {
26163
26301
  this.processGenericArtifact(sessionId, artifactType, filePath);
26164
26302
  }
26165
26303
  }
@@ -26168,58 +26306,58 @@ var FilesystemSync = class {
26168
26306
  ensureSessionRow(sessionId, sessionDir) {
26169
26307
  const branchMatch = sessionId.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
26170
26308
  const branch = branchMatch?.[1] ?? "unknown";
26171
- const hasRoundsDir = existsSync5(join10(sessionDir, "rounds"));
26172
- const hasMapDir = existsSync5(join10(sessionDir, "map"));
26309
+ const hasRoundsDir = existsSync6(join11(sessionDir, "rounds"));
26310
+ const hasMapDir = existsSync6(join11(sessionDir, "map"));
26173
26311
  const workflowType = hasMapDir && !hasRoundsDir ? "map" : "review";
26174
26312
  let currentRound = 1;
26175
26313
  if (hasRoundsDir) {
26176
- const roundDirs = readdirSync(join10(sessionDir, "rounds")).filter((d) => d.match(/^round-\d+$/));
26314
+ const roundDirs = readdirSync(join11(sessionDir, "rounds")).filter((d) => d.match(/^round-\d+$/));
26177
26315
  currentRound = Math.max(1, roundDirs.length);
26178
26316
  }
26179
26317
  let currentMapRun = 1;
26180
- const mapRunsDir = join10(sessionDir, "map", "runs");
26181
- if (existsSync5(mapRunsDir)) {
26318
+ const mapRunsDir = join11(sessionDir, "map", "runs");
26319
+ if (existsSync6(mapRunsDir)) {
26182
26320
  const runDirs = readdirSync(mapRunsDir).filter((d) => d.match(/^run-\d+$/));
26183
26321
  currentMapRun = Math.max(1, runDirs.length);
26184
26322
  }
26185
26323
  let phase = "context";
26186
26324
  let phaseNumber = 1;
26187
- let status = "active";
26325
+ let status = "closed";
26188
26326
  if (workflowType === "review" && hasRoundsDir) {
26189
- const roundDir = join10(sessionDir, "rounds", `round-${currentRound}`);
26190
- if (existsSync5(join10(roundDir, "final.md"))) {
26327
+ const roundDir = join11(sessionDir, "rounds", `round-${currentRound}`);
26328
+ if (existsSync6(join11(roundDir, "final.md"))) {
26191
26329
  phase = "complete";
26192
26330
  phaseNumber = 8;
26193
26331
  status = "closed";
26194
- } else if (existsSync5(join10(roundDir, "discourse.md"))) {
26332
+ } else if (existsSync6(join11(roundDir, "discourse.md"))) {
26195
26333
  phase = "synthesis";
26196
26334
  phaseNumber = 7;
26197
- } else if (existsSync5(join10(roundDir, "reviews")) && readdirSync(join10(roundDir, "reviews")).filter((f) => f.endsWith(".md")).length > 0) {
26335
+ } else if (existsSync6(join11(roundDir, "reviews")) && readdirSync(join11(roundDir, "reviews")).filter((f) => f.endsWith(".md")).length > 0) {
26198
26336
  phase = "reviews";
26199
26337
  phaseNumber = 4;
26200
- } else if (existsSync5(join10(sessionDir, "context.md"))) {
26338
+ } else if (existsSync6(join11(sessionDir, "context.md"))) {
26201
26339
  phase = "analysis";
26202
26340
  phaseNumber = 3;
26203
- } else if (existsSync5(join10(sessionDir, "discovered-standards.md"))) {
26341
+ } else if (existsSync6(join11(sessionDir, "discovered-standards.md"))) {
26204
26342
  phase = "change-context";
26205
26343
  phaseNumber = 2;
26206
26344
  }
26207
26345
  } else if (workflowType === "map" && hasMapDir) {
26208
- const runDir = join10(mapRunsDir, `run-${currentMapRun}`);
26209
- if (existsSync5(join10(runDir, "map.md"))) {
26346
+ const runDir = join11(mapRunsDir, `run-${currentMapRun}`);
26347
+ if (existsSync6(join11(runDir, "map.md"))) {
26210
26348
  phase = "complete";
26211
26349
  phaseNumber = 6;
26212
26350
  status = "closed";
26213
- } else if (existsSync5(join10(runDir, "requirements-mapping.md"))) {
26351
+ } else if (existsSync6(join11(runDir, "requirements-mapping.md"))) {
26214
26352
  phase = "synthesis";
26215
26353
  phaseNumber = 5;
26216
- } else if (existsSync5(join10(runDir, "flow-analysis.md"))) {
26354
+ } else if (existsSync6(join11(runDir, "flow-analysis.md"))) {
26217
26355
  phase = "requirements-mapping";
26218
26356
  phaseNumber = 4;
26219
- } else if (existsSync5(join10(runDir, "topology.md"))) {
26357
+ } else if (existsSync6(join11(runDir, "topology.md"))) {
26220
26358
  phase = "flow-analysis";
26221
26359
  phaseNumber = 3;
26222
- } else if (existsSync5(join10(sessionDir, "discovered-standards.md"))) {
26360
+ } else if (existsSync6(join11(sessionDir, "discovered-standards.md"))) {
26223
26361
  phase = "topology";
26224
26362
  phaseNumber = 2;
26225
26363
  }
@@ -26232,6 +26370,7 @@ var FilesystemSync = class {
26232
26370
  [currentRound, currentMapRun, sessionId]
26233
26371
  );
26234
26372
  } else {
26373
+ if (!this.hasArtifacts(sessionDir)) return;
26235
26374
  this.db.run(
26236
26375
  `INSERT INTO sessions (id, branch, workflow_type, current_phase, phase_number, current_round, current_map_run, session_dir, status)
26237
26376
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -26241,6 +26380,21 @@ var FilesystemSync = class {
26241
26380
  }
26242
26381
  this.onSync?.();
26243
26382
  }
26383
+ // ── Artifact Check ──
26384
+ /** Returns true if the directory contains at least one .md or .json file (recursively). */
26385
+ hasArtifacts(dir) {
26386
+ try {
26387
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
26388
+ if (entry.isDirectory()) {
26389
+ if (this.hasArtifacts(join11(dir, entry.name))) return true;
26390
+ } else if (/\.(md|json)$/.test(entry.name)) {
26391
+ return true;
26392
+ }
26393
+ }
26394
+ } catch {
26395
+ }
26396
+ return false;
26397
+ }
26244
26398
  // ── Mtime Skip Check ──
26245
26399
  shouldSkip(filePath, existingParsedAt) {
26246
26400
  if (!existingParsedAt) return false;
@@ -26282,12 +26436,12 @@ var FilesystemSync = class {
26282
26436
  );
26283
26437
  if (existingRun && this.shouldSkip(filePath, existingRun["parsed_at"] ?? null)) return;
26284
26438
  if (existingRun?.["source"] === "orchestrator") {
26285
- const content2 = readFileSync6(filePath, "utf-8");
26439
+ const content2 = readFileSync7(filePath, "utf-8");
26286
26440
  const action2 = this.upsertMarkdownArtifact(sessionId, "map", filePath, content2, void 0);
26287
26441
  this.emitArtifactEvent(action2, { sessionId, artifactType: "map", filePath });
26288
26442
  return;
26289
26443
  }
26290
- const content = readFileSync6(filePath, "utf-8");
26444
+ const content = readFileSync7(filePath, "utf-8");
26291
26445
  const parsed = parseMapMd(content);
26292
26446
  this.db.run(
26293
26447
  `INSERT OR REPLACE INTO map_runs (session_id, run_number, file_count, map_md_path, parsed_at, source)
@@ -26407,7 +26561,7 @@ var FilesystemSync = class {
26407
26561
  const roundId = roundRow?.["id"];
26408
26562
  if (!roundId) return;
26409
26563
  if (roundRow?.["source"] === "orchestrator") {
26410
- const content2 = readFileSync6(filePath, "utf-8");
26564
+ const content2 = readFileSync7(filePath, "utf-8");
26411
26565
  const action2 = this.upsertMarkdownArtifact(sessionId, "reviewer-output", filePath, content2, roundNumber);
26412
26566
  this.emitArtifactEvent(action2, {
26413
26567
  sessionId,
@@ -26426,7 +26580,7 @@ var FilesystemSync = class {
26426
26580
  [roundId, reviewerType, instanceNumber]
26427
26581
  );
26428
26582
  if (existingOutput && this.shouldSkip(filePath, existingOutput["parsed_at"] ?? null)) return;
26429
- const content = readFileSync6(filePath, "utf-8");
26583
+ const content = readFileSync7(filePath, "utf-8");
26430
26584
  const parsed = parseReviewerOutput(content);
26431
26585
  this.db.run(
26432
26586
  `INSERT OR REPLACE INTO reviewer_outputs (round_id, reviewer_type, instance_number, file_path, finding_count, parsed_at)
@@ -26514,7 +26668,7 @@ var FilesystemSync = class {
26514
26668
  if (existingRound?.["source"] === "orchestrator" && this.shouldSkip(filePath, existingRound["parsed_at"] ?? null)) return;
26515
26669
  let raw;
26516
26670
  try {
26517
- raw = JSON.parse(readFileSync6(filePath, "utf-8"));
26671
+ raw = JSON.parse(readFileSync7(filePath, "utf-8"));
26518
26672
  } catch {
26519
26673
  console.error(`[FilesystemSync] Failed to parse ${filePath}`);
26520
26674
  return;
@@ -26560,12 +26714,12 @@ var FilesystemSync = class {
26560
26714
  this.db.run("COMMIT");
26561
26715
  return;
26562
26716
  }
26563
- const roundDir = dirname7(filePath);
26717
+ const roundDir = dirname8(filePath);
26564
26718
  for (const reviewer of meta.reviewers) {
26565
26719
  const reviewerType = reviewer.type ?? "unknown";
26566
26720
  const instanceNumber = reviewer.instance ?? 1;
26567
26721
  const findings = reviewer.findings ?? [];
26568
- const reviewerMdPath = join10(roundDir, "reviews", `${reviewerType}-${instanceNumber}.md`);
26722
+ const reviewerMdPath = join11(roundDir, "reviews", `${reviewerType}-${instanceNumber}.md`);
26569
26723
  this.db.run(
26570
26724
  `INSERT OR REPLACE INTO reviewer_outputs (round_id, reviewer_type, instance_number, file_path, finding_count, parsed_at)
26571
26725
  VALUES (?, ?, ?, ?, ?, ?)`,
@@ -26663,7 +26817,7 @@ var FilesystemSync = class {
26663
26817
  if (existingRun?.["source"] === "orchestrator" && this.shouldSkip(filePath, existingRun["parsed_at"] ?? null)) return;
26664
26818
  let raw;
26665
26819
  try {
26666
- raw = JSON.parse(readFileSync6(filePath, "utf-8"));
26820
+ raw = JSON.parse(readFileSync7(filePath, "utf-8"));
26667
26821
  } catch {
26668
26822
  console.error(`[FilesystemSync] Failed to parse ${filePath}`);
26669
26823
  return;
@@ -26791,7 +26945,7 @@ var FilesystemSync = class {
26791
26945
  );
26792
26946
  const isOrchestratorSource = existingRound?.["source"] === "orchestrator";
26793
26947
  if (!isOrchestratorSource && existingRound && this.shouldSkip(filePath, existingRound["parsed_at"] ?? null)) return;
26794
- const content = readFileSync6(filePath, "utf-8");
26948
+ const content = readFileSync7(filePath, "utf-8");
26795
26949
  if (isOrchestratorSource) {
26796
26950
  this.db.run(
26797
26951
  `UPDATE review_rounds SET final_md_path = ?, parsed_at = ?
@@ -26864,7 +27018,7 @@ var FilesystemSync = class {
26864
27018
  [sessionId, artifactType, relPath]
26865
27019
  );
26866
27020
  if (existing && this.shouldSkip(filePath, existing["parsed_at"] ?? null)) return;
26867
- const content = readFileSync6(filePath, "utf-8");
27021
+ const content = readFileSync7(filePath, "utf-8");
26868
27022
  const action = this.upsertMarkdownArtifact(sessionId, artifactType, filePath, content, roundNumber);
26869
27023
  this.emitArtifactEvent(action, {
26870
27024
  sessionId,
@@ -26923,7 +27077,7 @@ var FilesystemSync = class {
26923
27077
  const parts = relFromSessions.split("/");
26924
27078
  const sessionId = parts[0];
26925
27079
  if (!sessionId) return;
26926
- const sessionDir = join10(this.sessionsDir, sessionId);
27080
+ const sessionDir = join11(this.sessionsDir, sessionId);
26927
27081
  this.ensureSessionRow(sessionId, sessionDir);
26928
27082
  const fileName = basename2(filePath);
26929
27083
  const reviewerMatch = relFromSessions.match(/rounds\/round-(\d+)\/reviews\/(.+\.md)$/);
@@ -26995,7 +27149,7 @@ var FilesystemSync = class {
26995
27149
  };
26996
27150
 
26997
27151
  // src/server/services/db-sync-watcher.ts
26998
- import { existsSync as existsSync6, readFileSync as readFileSync7, statSync as statSync2 } from "node:fs";
27152
+ import { existsSync as existsSync7, readFileSync as readFileSync8, statSync as statSync2 } from "node:fs";
26999
27153
  import { watch as watch3 } from "chokidar";
27000
27154
  import initSqlJs3 from "sql.js";
27001
27155
  function col(row, key) {
@@ -27017,7 +27171,7 @@ var DbSyncWatcher = class {
27017
27171
  * Initialize the WASM runtime (called once at startup).
27018
27172
  */
27019
27173
  async init() {
27020
- const wasmBuffer = readFileSync7(locateWasm());
27174
+ const wasmBuffer = readFileSync8(locateWasm());
27021
27175
  this.wasmBinary = wasmBuffer.buffer.slice(
27022
27176
  wasmBuffer.byteOffset,
27023
27177
  wasmBuffer.byteOffset + wasmBuffer.byteLength
@@ -27028,7 +27182,7 @@ var DbSyncWatcher = class {
27028
27182
  * Start watching the DB file for external changes.
27029
27183
  */
27030
27184
  startWatching() {
27031
- if (!existsSync6(this.dbFilePath)) return;
27185
+ if (!existsSync7(this.dbFilePath)) return;
27032
27186
  try {
27033
27187
  this.lastMtime = statSync2(this.dbFilePath).mtimeMs;
27034
27188
  } catch {
@@ -27079,7 +27233,7 @@ var DbSyncWatcher = class {
27079
27233
  * to avoid overwriting CLI changes.
27080
27234
  */
27081
27235
  syncFromDisk() {
27082
- if (!this.SQL || !existsSync6(this.dbFilePath)) return;
27236
+ if (!this.SQL || !existsSync7(this.dbFilePath)) return;
27083
27237
  let currentMtime;
27084
27238
  try {
27085
27239
  currentMtime = statSync2(this.dbFilePath).mtimeMs;
@@ -27090,7 +27244,7 @@ var DbSyncWatcher = class {
27090
27244
  this.lastMtime = currentMtime;
27091
27245
  let diskDb = null;
27092
27246
  try {
27093
- const fileBuffer = readFileSync7(this.dbFilePath);
27247
+ const fileBuffer = readFileSync8(this.dbFilePath);
27094
27248
  diskDb = new this.SQL.Database(fileBuffer);
27095
27249
  this.syncSessions(diskDb);
27096
27250
  this.syncEvents(diskDb);
@@ -27344,20 +27498,20 @@ var DbSyncWatcher = class {
27344
27498
  };
27345
27499
 
27346
27500
  // src/server/socket/chat-handler.ts
27347
- import { dirname as dirname8 } from "node:path";
27501
+ import { dirname as dirname9 } from "node:path";
27348
27502
 
27349
27503
  // src/server/services/chat-context.ts
27350
- import { readFileSync as readFileSync8, readdirSync as readdirSync2, existsSync as existsSync7 } from "node:fs";
27351
- import { join as join11 } from "node:path";
27504
+ import { readFileSync as readFileSync9, readdirSync as readdirSync2, existsSync as existsSync8 } from "node:fs";
27505
+ import { join as join12 } from "node:path";
27352
27506
  function buildChatContext(ocrDir, target) {
27353
- const sessionsDir = join11(ocrDir, "sessions");
27507
+ const sessionsDir = join12(ocrDir, "sessions");
27354
27508
  if (target.type === "map_run") {
27355
27509
  return buildMapRunContext(sessionsDir, target.sessionId, target.runNumber);
27356
27510
  }
27357
27511
  return buildReviewRoundContext(sessionsDir, target.sessionId, target.roundNumber);
27358
27512
  }
27359
27513
  function buildMapRunContext(sessionsDir, sessionId, runNumber) {
27360
- const mapPath = join11(
27514
+ const mapPath = join12(
27361
27515
  sessionsDir,
27362
27516
  sessionId,
27363
27517
  "map",
@@ -27371,8 +27525,8 @@ function buildMapRunContext(sessionsDir, sessionId, runNumber) {
27371
27525
  "",
27372
27526
  `Below is the Code Review Map that organizes the changeset into reviewable sections:`
27373
27527
  ];
27374
- if (existsSync7(mapPath)) {
27375
- const content = readFileSync8(mapPath, "utf-8");
27528
+ if (existsSync8(mapPath)) {
27529
+ const content = readFileSync9(mapPath, "utf-8");
27376
27530
  parts.push("");
27377
27531
  parts.push("<map>");
27378
27532
  parts.push(content);
@@ -27384,26 +27538,26 @@ function buildMapRunContext(sessionsDir, sessionId, runNumber) {
27384
27538
  return parts.join("\n");
27385
27539
  }
27386
27540
  function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
27387
- const roundDir = join11(sessionsDir, sessionId, "rounds", `round-${roundNumber}`);
27388
- const finalPath = join11(roundDir, "final.md");
27389
- const reviewersDir = join11(roundDir, "reviews");
27541
+ const roundDir = join12(sessionsDir, sessionId, "rounds", `round-${roundNumber}`);
27542
+ const finalPath = join12(roundDir, "final.md");
27543
+ const reviewersDir = join12(roundDir, "reviews");
27390
27544
  const parts = [
27391
27545
  `You are an expert code reviewer assisting with a code review session.`,
27392
27546
  `You are looking at review round #${roundNumber} for session "${sessionId}".`,
27393
27547
  "",
27394
27548
  `Below are the review artifacts for this round:`
27395
27549
  ];
27396
- if (existsSync7(finalPath)) {
27397
- const content = readFileSync8(finalPath, "utf-8");
27550
+ if (existsSync8(finalPath)) {
27551
+ const content = readFileSync9(finalPath, "utf-8");
27398
27552
  parts.push("");
27399
27553
  parts.push("<final-synthesis>");
27400
27554
  parts.push(content);
27401
27555
  parts.push("</final-synthesis>");
27402
27556
  }
27403
- if (existsSync7(reviewersDir)) {
27557
+ if (existsSync8(reviewersDir)) {
27404
27558
  const files = readdirSync2(reviewersDir).filter((f) => f.endsWith(".md")).sort();
27405
27559
  for (const file of files) {
27406
- const content = readFileSync8(join11(reviewersDir, file), "utf-8");
27560
+ const content = readFileSync9(join12(reviewersDir, file), "utf-8");
27407
27561
  const reviewerName = file.replace(/\.md$/, "");
27408
27562
  parts.push("");
27409
27563
  parts.push(`<reviewer name="${reviewerName}">`);
@@ -27411,7 +27565,7 @@ function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
27411
27565
  parts.push("</reviewer>");
27412
27566
  }
27413
27567
  }
27414
- if (!existsSync7(finalPath) && !existsSync7(reviewersDir)) {
27568
+ if (!existsSync8(finalPath) && !existsSync8(reviewersDir)) {
27415
27569
  parts.push("");
27416
27570
  parts.push("(No review artifacts found on disk for this round.)");
27417
27571
  }
@@ -27421,13 +27575,26 @@ function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
27421
27575
  // src/server/socket/execution-tracker.ts
27422
27576
  function startTrackedExecution(io2, db, ocrDir, command, args = []) {
27423
27577
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
27578
+ const uid = generateCommandUid();
27579
+ const argsJson = JSON.stringify(args);
27424
27580
  db.run(
27425
- `INSERT INTO command_executions (command, args, started_at)
27426
- VALUES (?, ?, ?)`,
27427
- [command, JSON.stringify(args), startedAt]
27581
+ `INSERT INTO command_executions (uid, command, args, started_at)
27582
+ VALUES (?, ?, ?, ?)`,
27583
+ [uid, command, argsJson, startedAt]
27428
27584
  );
27429
27585
  const idResult = db.exec("SELECT last_insert_rowid() as id");
27430
27586
  const executionId = idResult[0]?.values[0]?.[0] ?? 0;
27587
+ const baseLogEntry = {
27588
+ v: 1,
27589
+ uid,
27590
+ db_id: executionId,
27591
+ command,
27592
+ args: argsJson,
27593
+ started_at: startedAt,
27594
+ is_detached: 0,
27595
+ writer: "dashboard"
27596
+ };
27597
+ appendCommandLog(ocrDir, { ...baseLogEntry, event: "start", exit_code: null, finished_at: null });
27431
27598
  io2.emit("command:started", {
27432
27599
  execution_id: executionId,
27433
27600
  command,
@@ -27435,6 +27602,7 @@ function startTrackedExecution(io2, db, ocrDir, command, args = []) {
27435
27602
  started_at: startedAt
27436
27603
  });
27437
27604
  let outputBuffer = "";
27605
+ let trackedIsDetached = 0;
27438
27606
  return {
27439
27607
  executionId,
27440
27608
  appendOutput(content) {
@@ -27442,9 +27610,10 @@ function startTrackedExecution(io2, db, ocrDir, command, args = []) {
27442
27610
  io2.emit("command:output", { execution_id: executionId, content });
27443
27611
  },
27444
27612
  setPid(pid, isDetached) {
27613
+ trackedIsDetached = isDetached ? 1 : 0;
27445
27614
  db.run(
27446
27615
  "UPDATE command_executions SET pid = ?, is_detached = ? WHERE id = ?",
27447
- [pid, isDetached ? 1 : 0, executionId]
27616
+ [pid, trackedIsDetached, executionId]
27448
27617
  );
27449
27618
  },
27450
27619
  finish(exitCode) {
@@ -27456,6 +27625,13 @@ function startTrackedExecution(io2, db, ocrDir, command, args = []) {
27456
27625
  [exitCode, finishedAt, outputBuffer, executionId]
27457
27626
  );
27458
27627
  saveDb(db, ocrDir);
27628
+ appendCommandLog(ocrDir, {
27629
+ ...baseLogEntry,
27630
+ is_detached: trackedIsDetached,
27631
+ event: exitCode === -2 ? "cancel" : "finish",
27632
+ exit_code: exitCode,
27633
+ finished_at: finishedAt
27634
+ });
27459
27635
  io2.emit("command:finished", {
27460
27636
  execution_id: executionId,
27461
27637
  exitCode,
@@ -27539,7 +27715,7 @@ User: ${message}`;
27539
27715
  });
27540
27716
  return;
27541
27717
  }
27542
- const repoRoot = dirname8(ocrDir);
27718
+ const repoRoot = dirname9(ocrDir);
27543
27719
  const spawnResult = adapter.spawn({
27544
27720
  prompt,
27545
27721
  cwd: repoRoot,
@@ -27716,15 +27892,15 @@ function cleanupAllChats() {
27716
27892
 
27717
27893
  // src/server/socket/post-handler.ts
27718
27894
  import { execFile } from "node:child_process";
27719
- import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync9, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
27895
+ import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync10, unlinkSync, writeFileSync as writeFileSync4 } from "node:fs";
27720
27896
  import { tmpdir as tmpdir2 } from "node:os";
27721
- import { join as join12, dirname as dirname9, isAbsolute } from "node:path";
27722
- import { randomUUID } from "node:crypto";
27897
+ import { join as join13, dirname as dirname10, isAbsolute } from "node:path";
27898
+ import { randomUUID as randomUUID2 } from "node:crypto";
27723
27899
  import { promisify } from "node:util";
27724
27900
  var execFileAsync = promisify(execFile);
27725
27901
  function resolveSessionDir(sessionDir, ocrDir) {
27726
27902
  if (isAbsolute(sessionDir)) return sessionDir;
27727
- return join12(dirname9(ocrDir), sessionDir);
27903
+ return join13(dirname10(ocrDir), sessionDir);
27728
27904
  }
27729
27905
  var BRANCH_PREFIXES = [
27730
27906
  "feat",
@@ -27793,7 +27969,7 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
27793
27969
  return;
27794
27970
  }
27795
27971
  const branch = session.branch;
27796
- const repoRoot = dirname9(ocrDir);
27972
+ const repoRoot = dirname10(ocrDir);
27797
27973
  try {
27798
27974
  await execFileAsync("gh", ["auth", "status"], { env: cleanEnv(), cwd: repoRoot });
27799
27975
  } catch {
@@ -27894,19 +28070,19 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
27894
28070
  socket.emit("post:error", { error: "Session not found" });
27895
28071
  return;
27896
28072
  }
27897
- const sessionDir = session.session_dir ? resolveSessionDir(session.session_dir, ocrDir) : join12(ocrDir, "sessions", sessionId);
27898
- const roundDir = join12(sessionDir, "rounds", `round-${roundNumber}`);
27899
- const finalPath = join12(roundDir, "final.md");
27900
- if (!existsSync8(finalPath)) {
28073
+ const sessionDir = session.session_dir ? resolveSessionDir(session.session_dir, ocrDir) : join13(ocrDir, "sessions", sessionId);
28074
+ const roundDir = join13(sessionDir, "rounds", `round-${roundNumber}`);
28075
+ const finalPath = join13(roundDir, "final.md");
28076
+ if (!existsSync9(finalPath)) {
27901
28077
  socket.emit("post:error", { error: "final.md not found for this round" });
27902
28078
  return;
27903
28079
  }
27904
- const humanReviewPath = join12(roundDir, "final-human.md");
27905
- const repoRoot = dirname9(ocrDir);
27906
- const commandMdPath = join12(ocrDir, "commands", "translate-review-to-single-human.md");
28080
+ const humanReviewPath = join13(roundDir, "final-human.md");
28081
+ const repoRoot = dirname10(ocrDir);
28082
+ const commandMdPath = join13(ocrDir, "commands", "translate-review-to-single-human.md");
27907
28083
  let commandContent;
27908
28084
  try {
27909
- commandContent = readFileSync9(commandMdPath, "utf-8");
28085
+ commandContent = readFileSync10(commandMdPath, "utf-8");
27910
28086
  } catch {
27911
28087
  socket.emit("post:error", {
27912
28088
  error: `Command file not found: ${commandMdPath}. Run \`ocr init\` to set up.`
@@ -27982,9 +28158,9 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
27982
28158
  }
27983
28159
  }
27984
28160
  let generatedContent = "";
27985
- if (existsSync8(humanReviewPath)) {
28161
+ if (existsSync9(humanReviewPath)) {
27986
28162
  try {
27987
- generatedContent = readFileSync9(humanReviewPath, "utf-8").trim();
28163
+ generatedContent = readFileSync10(humanReviewPath, "utf-8").trim();
27988
28164
  } catch {
27989
28165
  }
27990
28166
  }
@@ -28059,11 +28235,11 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
28059
28235
  socket.emit("post:save-result", { success: false, error: "Session not found" });
28060
28236
  return;
28061
28237
  }
28062
- const sessionDir = session.session_dir ? resolveSessionDir(session.session_dir, ocrDir) : join12(ocrDir, "sessions", sessionId);
28063
- const roundDir = join12(sessionDir, "rounds", `round-${roundNumber}`);
28064
- mkdirSync2(roundDir, { recursive: true });
28065
- const filePath = join12(roundDir, "final-human.md");
28066
- writeFileSync3(filePath, content, { mode: 420 });
28238
+ const sessionDir = session.session_dir ? resolveSessionDir(session.session_dir, ocrDir) : join13(ocrDir, "sessions", sessionId);
28239
+ const roundDir = join13(sessionDir, "rounds", `round-${roundNumber}`);
28240
+ mkdirSync3(roundDir, { recursive: true });
28241
+ const filePath = join13(roundDir, "final-human.md");
28242
+ writeFileSync4(filePath, content, { mode: 420 });
28067
28243
  saveDb(db, ocrDir);
28068
28244
  socket.emit("post:save-result", { success: true });
28069
28245
  } catch (err) {
@@ -28090,14 +28266,14 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
28090
28266
  );
28091
28267
  tracker.appendOutput(`\u25B8 Posting review to PR #${prNumber}...
28092
28268
  `);
28093
- const tmpDir = join12(tmpdir2(), "ocr-post-comments");
28269
+ const tmpDir = join13(tmpdir2(), "ocr-post-comments");
28094
28270
  try {
28095
- mkdirSync2(tmpDir, { recursive: true, mode: 448 });
28271
+ mkdirSync3(tmpDir, { recursive: true, mode: 448 });
28096
28272
  } catch {
28097
28273
  }
28098
- const tmpFile = join12(tmpDir, `${randomUUID()}.md`);
28099
- writeFileSync3(tmpFile, content, { mode: 384 });
28100
- const repoRoot = dirname9(ocrDir);
28274
+ const tmpFile = join13(tmpDir, `${randomUUID2()}.md`);
28275
+ writeFileSync4(tmpFile, content, { mode: 384 });
28276
+ const repoRoot = dirname10(ocrDir);
28101
28277
  try {
28102
28278
  const { stdout } = await execFileAsync(
28103
28279
  "gh",
@@ -28142,17 +28318,21 @@ function cleanupAllPostGenerations() {
28142
28318
 
28143
28319
  // src/server/index.ts
28144
28320
  import { homedir } from "node:os";
28145
- var __dirname3 = dirname10(fileURLToPath3(import.meta.url));
28321
+ var __dirname3 = dirname11(fileURLToPath3(import.meta.url));
28146
28322
  function shortenPath(p) {
28147
28323
  const home = homedir();
28148
28324
  return p.startsWith(home) ? "~" + p.slice(home.length) : p;
28149
28325
  }
28326
+ function isLocalhostOrigin(origin) {
28327
+ if (!origin) return false;
28328
+ return /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin);
28329
+ }
28150
28330
  var AUTH_TOKEN = randomBytes(32).toString("hex");
28151
28331
  var app = (0, import_express12.default)();
28152
28332
  var httpServer = createServer(app);
28153
28333
  var io = new SocketIOServer(httpServer, {
28154
28334
  cors: {
28155
- origin: process.env.NODE_ENV !== "production" ? ["http://localhost:5173", "http://localhost:4173"] : false
28335
+ origin: process.env.NODE_ENV !== "production" ? (origin, cb) => cb(null, !origin || isLocalhostOrigin(origin)) : false
28156
28336
  },
28157
28337
  maxHttpBufferSize: 1e6
28158
28338
  // 1 MB — explicit default; review if large payloads are needed
@@ -28161,8 +28341,7 @@ app.use(import_express12.default.json());
28161
28341
  if (process.env.NODE_ENV !== "production") {
28162
28342
  app.use((_req, res, next) => {
28163
28343
  const origin = _req.headers.origin;
28164
- const allowed = ["http://localhost:5173", "http://localhost:4173"];
28165
- if (origin && allowed.includes(origin)) {
28344
+ if (origin && isLocalhostOrigin(origin)) {
28166
28345
  res.header("Access-Control-Allow-Origin", origin);
28167
28346
  }
28168
28347
  res.header("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS");
@@ -28193,8 +28372,7 @@ io.use((socket, next) => {
28193
28372
  if (process.env.NODE_ENV !== "production") {
28194
28373
  app.get("/auth/token", (req, res) => {
28195
28374
  const origin = req.headers.origin;
28196
- const allowed = ["http://localhost:5173", "http://localhost:4173"];
28197
- if (origin && !allowed.includes(origin)) {
28375
+ if (origin && !isLocalhostOrigin(origin)) {
28198
28376
  res.status(403).json({ error: "Forbidden: invalid origin" });
28199
28377
  return;
28200
28378
  }
@@ -28209,12 +28387,12 @@ async function startServer(options = {}) {
28209
28387
  const ocrDir = resolveOcrDir();
28210
28388
  const aiCliService = new AiCliService(ocrDir);
28211
28389
  const db = await openDb(ocrDir);
28212
- const dataDir = join13(ocrDir, "data");
28213
- const pidFilePath = join13(dataDir, "dashboard.pid");
28214
- mkdirSync3(dataDir, { recursive: true });
28215
- if (existsSync9(pidFilePath)) {
28390
+ const dataDir = join14(ocrDir, "data");
28391
+ const pidFilePath = join14(dataDir, "dashboard.pid");
28392
+ mkdirSync4(dataDir, { recursive: true });
28393
+ if (existsSync10(pidFilePath)) {
28216
28394
  try {
28217
- const oldPid = parseInt(readFileSync10(pidFilePath, "utf-8").trim(), 10);
28395
+ const oldPid = parseInt(readFileSync11(pidFilePath, "utf-8").trim(), 10);
28218
28396
  if (!isNaN(oldPid)) {
28219
28397
  try {
28220
28398
  process.kill(oldPid, 0);
@@ -28227,7 +28405,16 @@ async function startServer(options = {}) {
28227
28405
  } catch {
28228
28406
  }
28229
28407
  }
28230
- writeFileSync4(pidFilePath, String(process.pid), { mode: 384 });
28408
+ writeFileSync5(pidFilePath, String(process.pid), { mode: 384 });
28409
+ const cmdCountResult = db.exec("SELECT COUNT(*) as c FROM command_executions");
28410
+ const totalCmds = cmdCountResult[0]?.values[0]?.[0] ?? 0;
28411
+ if (totalCmds === 0) {
28412
+ const recovered = replayCommandLog(db, ocrDir);
28413
+ if (recovered > 0) {
28414
+ saveDb(db, ocrDir);
28415
+ console.log(` Recovered ${recovered} command(s) from JSONL backup`);
28416
+ }
28417
+ }
28231
28418
  const orphanResult = db.exec(
28232
28419
  `SELECT id, pid, is_detached, started_at FROM command_executions
28233
28420
  WHERE pid IS NOT NULL AND finished_at IS NULL`
@@ -28314,11 +28501,11 @@ async function startServer(options = {}) {
28314
28501
  app.use("/api/config", createConfigRouter(ocrDir, aiCliService));
28315
28502
  app.use("/api/sessions", createChatRouter(db, ocrDir));
28316
28503
  app.use("/api/reviewers", createReviewersRouter(ocrDir));
28317
- const clientDir = join13(__dirname3, "client");
28318
- if (process.env.NODE_ENV === "production" && existsSync9(clientDir)) {
28504
+ const clientDir = join14(__dirname3, "client");
28505
+ if (process.env.NODE_ENV === "production" && existsSync10(clientDir)) {
28319
28506
  app.use(import_express12.default.static(clientDir, { index: false }));
28320
- const indexHtmlPath = join13(clientDir, "index.html");
28321
- const rawIndexHtml = existsSync9(indexHtmlPath) ? readFileSync10(indexHtmlPath, "utf-8") : "";
28507
+ const indexHtmlPath = join14(clientDir, "index.html");
28508
+ const rawIndexHtml = existsSync10(indexHtmlPath) ? readFileSync11(indexHtmlPath, "utf-8") : "";
28322
28509
  const tokenScript = `<script>window.__OCR_TOKEN__=${JSON.stringify(AUTH_TOKEN)};</script>`;
28323
28510
  const injectedIndexHtml = rawIndexHtml.replace(
28324
28511
  "</head>",
@@ -28337,7 +28524,7 @@ async function startServer(options = {}) {
28337
28524
  registerChatHandlers(io, socket, db, ocrDir, aiCliService);
28338
28525
  registerPostHandlers(io, socket, db, ocrDir, aiCliService);
28339
28526
  });
28340
- const dbFilePath = join13(ocrDir, "data", "ocr.db");
28527
+ const dbFilePath = join14(ocrDir, "data", "ocr.db");
28341
28528
  const dbSyncWatcher = new DbSyncWatcher(db, dbFilePath, io, () => {
28342
28529
  saveDb(db, ocrDir);
28343
28530
  });
@@ -28348,36 +28535,57 @@ async function startServer(options = {}) {
28348
28535
  () => dbSyncWatcher.syncFromDisk(),
28349
28536
  () => dbSyncWatcher.markOwnWrite()
28350
28537
  );
28351
- const sessionsDir = join13(ocrDir, "sessions");
28538
+ const sessionsDir = join14(ocrDir, "sessions");
28352
28539
  const fsSync = new FilesystemSync(db, sessionsDir, io, () => saveDb(db, ocrDir));
28353
28540
  await fsSync.fullScan();
28354
28541
  saveDb(db, ocrDir);
28355
28542
  fsSync.startWatching();
28356
28543
  console.log(` Watching sessions: ${shortenPath(sessionsDir)}`);
28357
28544
  const stopReviewersWatch = watchReviewersMeta(ocrDir, io);
28358
- await new Promise((resolve3, reject) => {
28359
- httpServer.once("error", (err) => {
28360
- if (err.code === "EADDRINUSE") {
28361
- reject(new Error(
28362
- `Port ${port} is already in use. Either stop the other process (lsof -ti:${port} | xargs kill) or choose a different port (PORT=${port + 1} pnpm dev:server).`
28363
- ));
28545
+ const MAX_PORT_ATTEMPTS = 10;
28546
+ let actualPort = port;
28547
+ for (let attempt = 0; attempt < MAX_PORT_ATTEMPTS; attempt++) {
28548
+ try {
28549
+ await new Promise((resolve3, reject) => {
28550
+ const onError = (err) => reject(err);
28551
+ httpServer.once("error", onError);
28552
+ httpServer.listen(actualPort, "127.0.0.1", () => {
28553
+ httpServer.removeListener("error", onError);
28554
+ resolve3();
28555
+ });
28556
+ });
28557
+ break;
28558
+ } catch (err) {
28559
+ const nodeErr = err;
28560
+ if (nodeErr.code === "EADDRINUSE") {
28561
+ httpServer.close();
28562
+ if (attempt < MAX_PORT_ATTEMPTS - 1) {
28563
+ console.log(` Port ${actualPort} in use, trying ${actualPort + 1}...`);
28564
+ actualPort++;
28565
+ } else {
28566
+ throw new Error(
28567
+ `Could not find an available port (tried ${port}\u2013${actualPort}). Stop other processes or set PORT explicitly.`
28568
+ );
28569
+ }
28364
28570
  } else {
28365
- reject(err);
28571
+ throw err;
28366
28572
  }
28367
- });
28368
- httpServer.listen(port, "127.0.0.1", () => {
28369
- console.log(` Server: http://localhost:${port}`);
28370
- console.log(` OCR directory: ${shortenPath(ocrDir)}`);
28371
- console.log();
28372
- console.log(` Auth token: ${AUTH_TOKEN.slice(0, 8)}...[redacted]`);
28373
- console.log();
28374
- resolve3();
28375
- });
28376
- });
28573
+ }
28574
+ }
28575
+ if (actualPort !== port) {
28576
+ console.log(` Note: using port ${actualPort} (${port} was in use)`);
28577
+ }
28578
+ const portFilePath = join14(dataDir, "server-port");
28579
+ writeFileSync5(portFilePath, String(actualPort), { mode: 384 });
28580
+ console.log(` Server: http://localhost:${actualPort}`);
28581
+ console.log(` OCR directory: ${shortenPath(ocrDir)}`);
28582
+ console.log();
28583
+ console.log(` Auth token: ${AUTH_TOKEN.slice(0, 8)}...[redacted]`);
28584
+ console.log();
28377
28585
  if (options.open) {
28378
28586
  try {
28379
28587
  const { default: openBrowser } = await Promise.resolve().then(() => (init_open(), open_exports));
28380
- await openBrowser(`http://localhost:${port}`);
28588
+ await openBrowser(`http://localhost:${actualPort}`);
28381
28589
  } catch {
28382
28590
  }
28383
28591
  }
@@ -28387,6 +28595,10 @@ async function startServer(options = {}) {
28387
28595
  unlinkSync2(pidFilePath);
28388
28596
  } catch {
28389
28597
  }
28598
+ try {
28599
+ unlinkSync2(portFilePath);
28600
+ } catch {
28601
+ }
28390
28602
  try {
28391
28603
  const activeResult = db.exec(
28392
28604
  "SELECT id, pid, is_detached FROM command_executions WHERE pid IS NOT NULL AND finished_at IS NULL"