@open-code-review/cli 1.9.0 → 1.10.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.
@@ -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,17 +26306,17 @@ 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
  }
@@ -26186,40 +26324,40 @@ var FilesystemSync = class {
26186
26324
  let phaseNumber = 1;
26187
26325
  let status = "active";
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
  }
@@ -26282,12 +26420,12 @@ var FilesystemSync = class {
26282
26420
  );
26283
26421
  if (existingRun && this.shouldSkip(filePath, existingRun["parsed_at"] ?? null)) return;
26284
26422
  if (existingRun?.["source"] === "orchestrator") {
26285
- const content2 = readFileSync6(filePath, "utf-8");
26423
+ const content2 = readFileSync7(filePath, "utf-8");
26286
26424
  const action2 = this.upsertMarkdownArtifact(sessionId, "map", filePath, content2, void 0);
26287
26425
  this.emitArtifactEvent(action2, { sessionId, artifactType: "map", filePath });
26288
26426
  return;
26289
26427
  }
26290
- const content = readFileSync6(filePath, "utf-8");
26428
+ const content = readFileSync7(filePath, "utf-8");
26291
26429
  const parsed = parseMapMd(content);
26292
26430
  this.db.run(
26293
26431
  `INSERT OR REPLACE INTO map_runs (session_id, run_number, file_count, map_md_path, parsed_at, source)
@@ -26407,7 +26545,7 @@ var FilesystemSync = class {
26407
26545
  const roundId = roundRow?.["id"];
26408
26546
  if (!roundId) return;
26409
26547
  if (roundRow?.["source"] === "orchestrator") {
26410
- const content2 = readFileSync6(filePath, "utf-8");
26548
+ const content2 = readFileSync7(filePath, "utf-8");
26411
26549
  const action2 = this.upsertMarkdownArtifact(sessionId, "reviewer-output", filePath, content2, roundNumber);
26412
26550
  this.emitArtifactEvent(action2, {
26413
26551
  sessionId,
@@ -26426,7 +26564,7 @@ var FilesystemSync = class {
26426
26564
  [roundId, reviewerType, instanceNumber]
26427
26565
  );
26428
26566
  if (existingOutput && this.shouldSkip(filePath, existingOutput["parsed_at"] ?? null)) return;
26429
- const content = readFileSync6(filePath, "utf-8");
26567
+ const content = readFileSync7(filePath, "utf-8");
26430
26568
  const parsed = parseReviewerOutput(content);
26431
26569
  this.db.run(
26432
26570
  `INSERT OR REPLACE INTO reviewer_outputs (round_id, reviewer_type, instance_number, file_path, finding_count, parsed_at)
@@ -26514,7 +26652,7 @@ var FilesystemSync = class {
26514
26652
  if (existingRound?.["source"] === "orchestrator" && this.shouldSkip(filePath, existingRound["parsed_at"] ?? null)) return;
26515
26653
  let raw;
26516
26654
  try {
26517
- raw = JSON.parse(readFileSync6(filePath, "utf-8"));
26655
+ raw = JSON.parse(readFileSync7(filePath, "utf-8"));
26518
26656
  } catch {
26519
26657
  console.error(`[FilesystemSync] Failed to parse ${filePath}`);
26520
26658
  return;
@@ -26560,12 +26698,12 @@ var FilesystemSync = class {
26560
26698
  this.db.run("COMMIT");
26561
26699
  return;
26562
26700
  }
26563
- const roundDir = dirname7(filePath);
26701
+ const roundDir = dirname8(filePath);
26564
26702
  for (const reviewer of meta.reviewers) {
26565
26703
  const reviewerType = reviewer.type ?? "unknown";
26566
26704
  const instanceNumber = reviewer.instance ?? 1;
26567
26705
  const findings = reviewer.findings ?? [];
26568
- const reviewerMdPath = join10(roundDir, "reviews", `${reviewerType}-${instanceNumber}.md`);
26706
+ const reviewerMdPath = join11(roundDir, "reviews", `${reviewerType}-${instanceNumber}.md`);
26569
26707
  this.db.run(
26570
26708
  `INSERT OR REPLACE INTO reviewer_outputs (round_id, reviewer_type, instance_number, file_path, finding_count, parsed_at)
26571
26709
  VALUES (?, ?, ?, ?, ?, ?)`,
@@ -26663,7 +26801,7 @@ var FilesystemSync = class {
26663
26801
  if (existingRun?.["source"] === "orchestrator" && this.shouldSkip(filePath, existingRun["parsed_at"] ?? null)) return;
26664
26802
  let raw;
26665
26803
  try {
26666
- raw = JSON.parse(readFileSync6(filePath, "utf-8"));
26804
+ raw = JSON.parse(readFileSync7(filePath, "utf-8"));
26667
26805
  } catch {
26668
26806
  console.error(`[FilesystemSync] Failed to parse ${filePath}`);
26669
26807
  return;
@@ -26791,7 +26929,7 @@ var FilesystemSync = class {
26791
26929
  );
26792
26930
  const isOrchestratorSource = existingRound?.["source"] === "orchestrator";
26793
26931
  if (!isOrchestratorSource && existingRound && this.shouldSkip(filePath, existingRound["parsed_at"] ?? null)) return;
26794
- const content = readFileSync6(filePath, "utf-8");
26932
+ const content = readFileSync7(filePath, "utf-8");
26795
26933
  if (isOrchestratorSource) {
26796
26934
  this.db.run(
26797
26935
  `UPDATE review_rounds SET final_md_path = ?, parsed_at = ?
@@ -26864,7 +27002,7 @@ var FilesystemSync = class {
26864
27002
  [sessionId, artifactType, relPath]
26865
27003
  );
26866
27004
  if (existing && this.shouldSkip(filePath, existing["parsed_at"] ?? null)) return;
26867
- const content = readFileSync6(filePath, "utf-8");
27005
+ const content = readFileSync7(filePath, "utf-8");
26868
27006
  const action = this.upsertMarkdownArtifact(sessionId, artifactType, filePath, content, roundNumber);
26869
27007
  this.emitArtifactEvent(action, {
26870
27008
  sessionId,
@@ -26923,7 +27061,7 @@ var FilesystemSync = class {
26923
27061
  const parts = relFromSessions.split("/");
26924
27062
  const sessionId = parts[0];
26925
27063
  if (!sessionId) return;
26926
- const sessionDir = join10(this.sessionsDir, sessionId);
27064
+ const sessionDir = join11(this.sessionsDir, sessionId);
26927
27065
  this.ensureSessionRow(sessionId, sessionDir);
26928
27066
  const fileName = basename2(filePath);
26929
27067
  const reviewerMatch = relFromSessions.match(/rounds\/round-(\d+)\/reviews\/(.+\.md)$/);
@@ -26995,7 +27133,7 @@ var FilesystemSync = class {
26995
27133
  };
26996
27134
 
26997
27135
  // src/server/services/db-sync-watcher.ts
26998
- import { existsSync as existsSync6, readFileSync as readFileSync7, statSync as statSync2 } from "node:fs";
27136
+ import { existsSync as existsSync7, readFileSync as readFileSync8, statSync as statSync2 } from "node:fs";
26999
27137
  import { watch as watch3 } from "chokidar";
27000
27138
  import initSqlJs3 from "sql.js";
27001
27139
  function col(row, key) {
@@ -27017,7 +27155,7 @@ var DbSyncWatcher = class {
27017
27155
  * Initialize the WASM runtime (called once at startup).
27018
27156
  */
27019
27157
  async init() {
27020
- const wasmBuffer = readFileSync7(locateWasm());
27158
+ const wasmBuffer = readFileSync8(locateWasm());
27021
27159
  this.wasmBinary = wasmBuffer.buffer.slice(
27022
27160
  wasmBuffer.byteOffset,
27023
27161
  wasmBuffer.byteOffset + wasmBuffer.byteLength
@@ -27028,7 +27166,7 @@ var DbSyncWatcher = class {
27028
27166
  * Start watching the DB file for external changes.
27029
27167
  */
27030
27168
  startWatching() {
27031
- if (!existsSync6(this.dbFilePath)) return;
27169
+ if (!existsSync7(this.dbFilePath)) return;
27032
27170
  try {
27033
27171
  this.lastMtime = statSync2(this.dbFilePath).mtimeMs;
27034
27172
  } catch {
@@ -27079,7 +27217,7 @@ var DbSyncWatcher = class {
27079
27217
  * to avoid overwriting CLI changes.
27080
27218
  */
27081
27219
  syncFromDisk() {
27082
- if (!this.SQL || !existsSync6(this.dbFilePath)) return;
27220
+ if (!this.SQL || !existsSync7(this.dbFilePath)) return;
27083
27221
  let currentMtime;
27084
27222
  try {
27085
27223
  currentMtime = statSync2(this.dbFilePath).mtimeMs;
@@ -27090,7 +27228,7 @@ var DbSyncWatcher = class {
27090
27228
  this.lastMtime = currentMtime;
27091
27229
  let diskDb = null;
27092
27230
  try {
27093
- const fileBuffer = readFileSync7(this.dbFilePath);
27231
+ const fileBuffer = readFileSync8(this.dbFilePath);
27094
27232
  diskDb = new this.SQL.Database(fileBuffer);
27095
27233
  this.syncSessions(diskDb);
27096
27234
  this.syncEvents(diskDb);
@@ -27344,20 +27482,20 @@ var DbSyncWatcher = class {
27344
27482
  };
27345
27483
 
27346
27484
  // src/server/socket/chat-handler.ts
27347
- import { dirname as dirname8 } from "node:path";
27485
+ import { dirname as dirname9 } from "node:path";
27348
27486
 
27349
27487
  // 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";
27488
+ import { readFileSync as readFileSync9, readdirSync as readdirSync2, existsSync as existsSync8 } from "node:fs";
27489
+ import { join as join12 } from "node:path";
27352
27490
  function buildChatContext(ocrDir, target) {
27353
- const sessionsDir = join11(ocrDir, "sessions");
27491
+ const sessionsDir = join12(ocrDir, "sessions");
27354
27492
  if (target.type === "map_run") {
27355
27493
  return buildMapRunContext(sessionsDir, target.sessionId, target.runNumber);
27356
27494
  }
27357
27495
  return buildReviewRoundContext(sessionsDir, target.sessionId, target.roundNumber);
27358
27496
  }
27359
27497
  function buildMapRunContext(sessionsDir, sessionId, runNumber) {
27360
- const mapPath = join11(
27498
+ const mapPath = join12(
27361
27499
  sessionsDir,
27362
27500
  sessionId,
27363
27501
  "map",
@@ -27371,8 +27509,8 @@ function buildMapRunContext(sessionsDir, sessionId, runNumber) {
27371
27509
  "",
27372
27510
  `Below is the Code Review Map that organizes the changeset into reviewable sections:`
27373
27511
  ];
27374
- if (existsSync7(mapPath)) {
27375
- const content = readFileSync8(mapPath, "utf-8");
27512
+ if (existsSync8(mapPath)) {
27513
+ const content = readFileSync9(mapPath, "utf-8");
27376
27514
  parts.push("");
27377
27515
  parts.push("<map>");
27378
27516
  parts.push(content);
@@ -27384,26 +27522,26 @@ function buildMapRunContext(sessionsDir, sessionId, runNumber) {
27384
27522
  return parts.join("\n");
27385
27523
  }
27386
27524
  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");
27525
+ const roundDir = join12(sessionsDir, sessionId, "rounds", `round-${roundNumber}`);
27526
+ const finalPath = join12(roundDir, "final.md");
27527
+ const reviewersDir = join12(roundDir, "reviews");
27390
27528
  const parts = [
27391
27529
  `You are an expert code reviewer assisting with a code review session.`,
27392
27530
  `You are looking at review round #${roundNumber} for session "${sessionId}".`,
27393
27531
  "",
27394
27532
  `Below are the review artifacts for this round:`
27395
27533
  ];
27396
- if (existsSync7(finalPath)) {
27397
- const content = readFileSync8(finalPath, "utf-8");
27534
+ if (existsSync8(finalPath)) {
27535
+ const content = readFileSync9(finalPath, "utf-8");
27398
27536
  parts.push("");
27399
27537
  parts.push("<final-synthesis>");
27400
27538
  parts.push(content);
27401
27539
  parts.push("</final-synthesis>");
27402
27540
  }
27403
- if (existsSync7(reviewersDir)) {
27541
+ if (existsSync8(reviewersDir)) {
27404
27542
  const files = readdirSync2(reviewersDir).filter((f) => f.endsWith(".md")).sort();
27405
27543
  for (const file of files) {
27406
- const content = readFileSync8(join11(reviewersDir, file), "utf-8");
27544
+ const content = readFileSync9(join12(reviewersDir, file), "utf-8");
27407
27545
  const reviewerName = file.replace(/\.md$/, "");
27408
27546
  parts.push("");
27409
27547
  parts.push(`<reviewer name="${reviewerName}">`);
@@ -27411,7 +27549,7 @@ function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
27411
27549
  parts.push("</reviewer>");
27412
27550
  }
27413
27551
  }
27414
- if (!existsSync7(finalPath) && !existsSync7(reviewersDir)) {
27552
+ if (!existsSync8(finalPath) && !existsSync8(reviewersDir)) {
27415
27553
  parts.push("");
27416
27554
  parts.push("(No review artifacts found on disk for this round.)");
27417
27555
  }
@@ -27421,13 +27559,26 @@ function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
27421
27559
  // src/server/socket/execution-tracker.ts
27422
27560
  function startTrackedExecution(io2, db, ocrDir, command, args = []) {
27423
27561
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
27562
+ const uid = generateCommandUid();
27563
+ const argsJson = JSON.stringify(args);
27424
27564
  db.run(
27425
- `INSERT INTO command_executions (command, args, started_at)
27426
- VALUES (?, ?, ?)`,
27427
- [command, JSON.stringify(args), startedAt]
27565
+ `INSERT INTO command_executions (uid, command, args, started_at)
27566
+ VALUES (?, ?, ?, ?)`,
27567
+ [uid, command, argsJson, startedAt]
27428
27568
  );
27429
27569
  const idResult = db.exec("SELECT last_insert_rowid() as id");
27430
27570
  const executionId = idResult[0]?.values[0]?.[0] ?? 0;
27571
+ const baseLogEntry = {
27572
+ v: 1,
27573
+ uid,
27574
+ db_id: executionId,
27575
+ command,
27576
+ args: argsJson,
27577
+ started_at: startedAt,
27578
+ is_detached: 0,
27579
+ writer: "dashboard"
27580
+ };
27581
+ appendCommandLog(ocrDir, { ...baseLogEntry, event: "start", exit_code: null, finished_at: null });
27431
27582
  io2.emit("command:started", {
27432
27583
  execution_id: executionId,
27433
27584
  command,
@@ -27435,6 +27586,7 @@ function startTrackedExecution(io2, db, ocrDir, command, args = []) {
27435
27586
  started_at: startedAt
27436
27587
  });
27437
27588
  let outputBuffer = "";
27589
+ let trackedIsDetached = 0;
27438
27590
  return {
27439
27591
  executionId,
27440
27592
  appendOutput(content) {
@@ -27442,9 +27594,10 @@ function startTrackedExecution(io2, db, ocrDir, command, args = []) {
27442
27594
  io2.emit("command:output", { execution_id: executionId, content });
27443
27595
  },
27444
27596
  setPid(pid, isDetached) {
27597
+ trackedIsDetached = isDetached ? 1 : 0;
27445
27598
  db.run(
27446
27599
  "UPDATE command_executions SET pid = ?, is_detached = ? WHERE id = ?",
27447
- [pid, isDetached ? 1 : 0, executionId]
27600
+ [pid, trackedIsDetached, executionId]
27448
27601
  );
27449
27602
  },
27450
27603
  finish(exitCode) {
@@ -27456,6 +27609,13 @@ function startTrackedExecution(io2, db, ocrDir, command, args = []) {
27456
27609
  [exitCode, finishedAt, outputBuffer, executionId]
27457
27610
  );
27458
27611
  saveDb(db, ocrDir);
27612
+ appendCommandLog(ocrDir, {
27613
+ ...baseLogEntry,
27614
+ is_detached: trackedIsDetached,
27615
+ event: exitCode === -2 ? "cancel" : "finish",
27616
+ exit_code: exitCode,
27617
+ finished_at: finishedAt
27618
+ });
27459
27619
  io2.emit("command:finished", {
27460
27620
  execution_id: executionId,
27461
27621
  exitCode,
@@ -27539,7 +27699,7 @@ User: ${message}`;
27539
27699
  });
27540
27700
  return;
27541
27701
  }
27542
- const repoRoot = dirname8(ocrDir);
27702
+ const repoRoot = dirname9(ocrDir);
27543
27703
  const spawnResult = adapter.spawn({
27544
27704
  prompt,
27545
27705
  cwd: repoRoot,
@@ -27716,15 +27876,15 @@ function cleanupAllChats() {
27716
27876
 
27717
27877
  // src/server/socket/post-handler.ts
27718
27878
  import { execFile } from "node:child_process";
27719
- import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync9, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
27879
+ import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync10, unlinkSync, writeFileSync as writeFileSync4 } from "node:fs";
27720
27880
  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";
27881
+ import { join as join13, dirname as dirname10, isAbsolute } from "node:path";
27882
+ import { randomUUID as randomUUID2 } from "node:crypto";
27723
27883
  import { promisify } from "node:util";
27724
27884
  var execFileAsync = promisify(execFile);
27725
27885
  function resolveSessionDir(sessionDir, ocrDir) {
27726
27886
  if (isAbsolute(sessionDir)) return sessionDir;
27727
- return join12(dirname9(ocrDir), sessionDir);
27887
+ return join13(dirname10(ocrDir), sessionDir);
27728
27888
  }
27729
27889
  var BRANCH_PREFIXES = [
27730
27890
  "feat",
@@ -27793,7 +27953,7 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
27793
27953
  return;
27794
27954
  }
27795
27955
  const branch = session.branch;
27796
- const repoRoot = dirname9(ocrDir);
27956
+ const repoRoot = dirname10(ocrDir);
27797
27957
  try {
27798
27958
  await execFileAsync("gh", ["auth", "status"], { env: cleanEnv(), cwd: repoRoot });
27799
27959
  } catch {
@@ -27894,19 +28054,19 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
27894
28054
  socket.emit("post:error", { error: "Session not found" });
27895
28055
  return;
27896
28056
  }
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)) {
28057
+ const sessionDir = session.session_dir ? resolveSessionDir(session.session_dir, ocrDir) : join13(ocrDir, "sessions", sessionId);
28058
+ const roundDir = join13(sessionDir, "rounds", `round-${roundNumber}`);
28059
+ const finalPath = join13(roundDir, "final.md");
28060
+ if (!existsSync9(finalPath)) {
27901
28061
  socket.emit("post:error", { error: "final.md not found for this round" });
27902
28062
  return;
27903
28063
  }
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");
28064
+ const humanReviewPath = join13(roundDir, "final-human.md");
28065
+ const repoRoot = dirname10(ocrDir);
28066
+ const commandMdPath = join13(ocrDir, "commands", "translate-review-to-single-human.md");
27907
28067
  let commandContent;
27908
28068
  try {
27909
- commandContent = readFileSync9(commandMdPath, "utf-8");
28069
+ commandContent = readFileSync10(commandMdPath, "utf-8");
27910
28070
  } catch {
27911
28071
  socket.emit("post:error", {
27912
28072
  error: `Command file not found: ${commandMdPath}. Run \`ocr init\` to set up.`
@@ -27982,9 +28142,9 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
27982
28142
  }
27983
28143
  }
27984
28144
  let generatedContent = "";
27985
- if (existsSync8(humanReviewPath)) {
28145
+ if (existsSync9(humanReviewPath)) {
27986
28146
  try {
27987
- generatedContent = readFileSync9(humanReviewPath, "utf-8").trim();
28147
+ generatedContent = readFileSync10(humanReviewPath, "utf-8").trim();
27988
28148
  } catch {
27989
28149
  }
27990
28150
  }
@@ -28059,11 +28219,11 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
28059
28219
  socket.emit("post:save-result", { success: false, error: "Session not found" });
28060
28220
  return;
28061
28221
  }
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 });
28222
+ const sessionDir = session.session_dir ? resolveSessionDir(session.session_dir, ocrDir) : join13(ocrDir, "sessions", sessionId);
28223
+ const roundDir = join13(sessionDir, "rounds", `round-${roundNumber}`);
28224
+ mkdirSync3(roundDir, { recursive: true });
28225
+ const filePath = join13(roundDir, "final-human.md");
28226
+ writeFileSync4(filePath, content, { mode: 420 });
28067
28227
  saveDb(db, ocrDir);
28068
28228
  socket.emit("post:save-result", { success: true });
28069
28229
  } catch (err) {
@@ -28090,14 +28250,14 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
28090
28250
  );
28091
28251
  tracker.appendOutput(`\u25B8 Posting review to PR #${prNumber}...
28092
28252
  `);
28093
- const tmpDir = join12(tmpdir2(), "ocr-post-comments");
28253
+ const tmpDir = join13(tmpdir2(), "ocr-post-comments");
28094
28254
  try {
28095
- mkdirSync2(tmpDir, { recursive: true, mode: 448 });
28255
+ mkdirSync3(tmpDir, { recursive: true, mode: 448 });
28096
28256
  } catch {
28097
28257
  }
28098
- const tmpFile = join12(tmpDir, `${randomUUID()}.md`);
28099
- writeFileSync3(tmpFile, content, { mode: 384 });
28100
- const repoRoot = dirname9(ocrDir);
28258
+ const tmpFile = join13(tmpDir, `${randomUUID2()}.md`);
28259
+ writeFileSync4(tmpFile, content, { mode: 384 });
28260
+ const repoRoot = dirname10(ocrDir);
28101
28261
  try {
28102
28262
  const { stdout } = await execFileAsync(
28103
28263
  "gh",
@@ -28142,17 +28302,21 @@ function cleanupAllPostGenerations() {
28142
28302
 
28143
28303
  // src/server/index.ts
28144
28304
  import { homedir } from "node:os";
28145
- var __dirname3 = dirname10(fileURLToPath3(import.meta.url));
28305
+ var __dirname3 = dirname11(fileURLToPath3(import.meta.url));
28146
28306
  function shortenPath(p) {
28147
28307
  const home = homedir();
28148
28308
  return p.startsWith(home) ? "~" + p.slice(home.length) : p;
28149
28309
  }
28310
+ function isLocalhostOrigin(origin) {
28311
+ if (!origin) return false;
28312
+ return /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin);
28313
+ }
28150
28314
  var AUTH_TOKEN = randomBytes(32).toString("hex");
28151
28315
  var app = (0, import_express12.default)();
28152
28316
  var httpServer = createServer(app);
28153
28317
  var io = new SocketIOServer(httpServer, {
28154
28318
  cors: {
28155
- origin: process.env.NODE_ENV !== "production" ? ["http://localhost:5173", "http://localhost:4173"] : false
28319
+ origin: process.env.NODE_ENV !== "production" ? (origin, cb) => cb(null, !origin || isLocalhostOrigin(origin)) : false
28156
28320
  },
28157
28321
  maxHttpBufferSize: 1e6
28158
28322
  // 1 MB — explicit default; review if large payloads are needed
@@ -28161,8 +28325,7 @@ app.use(import_express12.default.json());
28161
28325
  if (process.env.NODE_ENV !== "production") {
28162
28326
  app.use((_req, res, next) => {
28163
28327
  const origin = _req.headers.origin;
28164
- const allowed = ["http://localhost:5173", "http://localhost:4173"];
28165
- if (origin && allowed.includes(origin)) {
28328
+ if (origin && isLocalhostOrigin(origin)) {
28166
28329
  res.header("Access-Control-Allow-Origin", origin);
28167
28330
  }
28168
28331
  res.header("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS");
@@ -28193,8 +28356,7 @@ io.use((socket, next) => {
28193
28356
  if (process.env.NODE_ENV !== "production") {
28194
28357
  app.get("/auth/token", (req, res) => {
28195
28358
  const origin = req.headers.origin;
28196
- const allowed = ["http://localhost:5173", "http://localhost:4173"];
28197
- if (origin && !allowed.includes(origin)) {
28359
+ if (origin && !isLocalhostOrigin(origin)) {
28198
28360
  res.status(403).json({ error: "Forbidden: invalid origin" });
28199
28361
  return;
28200
28362
  }
@@ -28209,12 +28371,12 @@ async function startServer(options = {}) {
28209
28371
  const ocrDir = resolveOcrDir();
28210
28372
  const aiCliService = new AiCliService(ocrDir);
28211
28373
  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)) {
28374
+ const dataDir = join14(ocrDir, "data");
28375
+ const pidFilePath = join14(dataDir, "dashboard.pid");
28376
+ mkdirSync4(dataDir, { recursive: true });
28377
+ if (existsSync10(pidFilePath)) {
28216
28378
  try {
28217
- const oldPid = parseInt(readFileSync10(pidFilePath, "utf-8").trim(), 10);
28379
+ const oldPid = parseInt(readFileSync11(pidFilePath, "utf-8").trim(), 10);
28218
28380
  if (!isNaN(oldPid)) {
28219
28381
  try {
28220
28382
  process.kill(oldPid, 0);
@@ -28227,7 +28389,16 @@ async function startServer(options = {}) {
28227
28389
  } catch {
28228
28390
  }
28229
28391
  }
28230
- writeFileSync4(pidFilePath, String(process.pid), { mode: 384 });
28392
+ writeFileSync5(pidFilePath, String(process.pid), { mode: 384 });
28393
+ const cmdCountResult = db.exec("SELECT COUNT(*) as c FROM command_executions");
28394
+ const totalCmds = cmdCountResult[0]?.values[0]?.[0] ?? 0;
28395
+ if (totalCmds === 0) {
28396
+ const recovered = replayCommandLog(db, ocrDir);
28397
+ if (recovered > 0) {
28398
+ saveDb(db, ocrDir);
28399
+ console.log(` Recovered ${recovered} command(s) from JSONL backup`);
28400
+ }
28401
+ }
28231
28402
  const orphanResult = db.exec(
28232
28403
  `SELECT id, pid, is_detached, started_at FROM command_executions
28233
28404
  WHERE pid IS NOT NULL AND finished_at IS NULL`
@@ -28314,11 +28485,11 @@ async function startServer(options = {}) {
28314
28485
  app.use("/api/config", createConfigRouter(ocrDir, aiCliService));
28315
28486
  app.use("/api/sessions", createChatRouter(db, ocrDir));
28316
28487
  app.use("/api/reviewers", createReviewersRouter(ocrDir));
28317
- const clientDir = join13(__dirname3, "client");
28318
- if (process.env.NODE_ENV === "production" && existsSync9(clientDir)) {
28488
+ const clientDir = join14(__dirname3, "client");
28489
+ if (process.env.NODE_ENV === "production" && existsSync10(clientDir)) {
28319
28490
  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") : "";
28491
+ const indexHtmlPath = join14(clientDir, "index.html");
28492
+ const rawIndexHtml = existsSync10(indexHtmlPath) ? readFileSync11(indexHtmlPath, "utf-8") : "";
28322
28493
  const tokenScript = `<script>window.__OCR_TOKEN__=${JSON.stringify(AUTH_TOKEN)};</script>`;
28323
28494
  const injectedIndexHtml = rawIndexHtml.replace(
28324
28495
  "</head>",
@@ -28337,7 +28508,7 @@ async function startServer(options = {}) {
28337
28508
  registerChatHandlers(io, socket, db, ocrDir, aiCliService);
28338
28509
  registerPostHandlers(io, socket, db, ocrDir, aiCliService);
28339
28510
  });
28340
- const dbFilePath = join13(ocrDir, "data", "ocr.db");
28511
+ const dbFilePath = join14(ocrDir, "data", "ocr.db");
28341
28512
  const dbSyncWatcher = new DbSyncWatcher(db, dbFilePath, io, () => {
28342
28513
  saveDb(db, ocrDir);
28343
28514
  });
@@ -28348,36 +28519,57 @@ async function startServer(options = {}) {
28348
28519
  () => dbSyncWatcher.syncFromDisk(),
28349
28520
  () => dbSyncWatcher.markOwnWrite()
28350
28521
  );
28351
- const sessionsDir = join13(ocrDir, "sessions");
28522
+ const sessionsDir = join14(ocrDir, "sessions");
28352
28523
  const fsSync = new FilesystemSync(db, sessionsDir, io, () => saveDb(db, ocrDir));
28353
28524
  await fsSync.fullScan();
28354
28525
  saveDb(db, ocrDir);
28355
28526
  fsSync.startWatching();
28356
28527
  console.log(` Watching sessions: ${shortenPath(sessionsDir)}`);
28357
28528
  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
- ));
28529
+ const MAX_PORT_ATTEMPTS = 10;
28530
+ let actualPort = port;
28531
+ for (let attempt = 0; attempt < MAX_PORT_ATTEMPTS; attempt++) {
28532
+ try {
28533
+ await new Promise((resolve3, reject) => {
28534
+ const onError = (err) => reject(err);
28535
+ httpServer.once("error", onError);
28536
+ httpServer.listen(actualPort, "127.0.0.1", () => {
28537
+ httpServer.removeListener("error", onError);
28538
+ resolve3();
28539
+ });
28540
+ });
28541
+ break;
28542
+ } catch (err) {
28543
+ const nodeErr = err;
28544
+ if (nodeErr.code === "EADDRINUSE") {
28545
+ httpServer.close();
28546
+ if (attempt < MAX_PORT_ATTEMPTS - 1) {
28547
+ console.log(` Port ${actualPort} in use, trying ${actualPort + 1}...`);
28548
+ actualPort++;
28549
+ } else {
28550
+ throw new Error(
28551
+ `Could not find an available port (tried ${port}\u2013${actualPort}). Stop other processes or set PORT explicitly.`
28552
+ );
28553
+ }
28364
28554
  } else {
28365
- reject(err);
28555
+ throw err;
28366
28556
  }
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
- });
28557
+ }
28558
+ }
28559
+ if (actualPort !== port) {
28560
+ console.log(` Note: using port ${actualPort} (${port} was in use)`);
28561
+ }
28562
+ const portFilePath = join14(dataDir, "server-port");
28563
+ writeFileSync5(portFilePath, String(actualPort), { mode: 384 });
28564
+ console.log(` Server: http://localhost:${actualPort}`);
28565
+ console.log(` OCR directory: ${shortenPath(ocrDir)}`);
28566
+ console.log();
28567
+ console.log(` Auth token: ${AUTH_TOKEN.slice(0, 8)}...[redacted]`);
28568
+ console.log();
28377
28569
  if (options.open) {
28378
28570
  try {
28379
28571
  const { default: openBrowser } = await Promise.resolve().then(() => (init_open(), open_exports));
28380
- await openBrowser(`http://localhost:${port}`);
28572
+ await openBrowser(`http://localhost:${actualPort}`);
28381
28573
  } catch {
28382
28574
  }
28383
28575
  }
@@ -28387,6 +28579,10 @@ async function startServer(options = {}) {
28387
28579
  unlinkSync2(pidFilePath);
28388
28580
  } catch {
28389
28581
  }
28582
+ try {
28583
+ unlinkSync2(portFilePath);
28584
+ } catch {
28585
+ }
28390
28586
  try {
28391
28587
  const activeResult = db.exec(
28392
28588
  "SELECT id, pid, is_detached FROM command_executions WHERE pid IS NOT NULL AND finished_at IS NULL"