glassbox 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -395,7 +395,7 @@ init_connection();
395
395
  init_queries();
396
396
  import { mkdirSync as mkdirSync9, realpathSync } from "fs";
397
397
  import { tmpdir } from "os";
398
- import { join as join13, resolve as resolve6 } from "path";
398
+ import { join as join13, resolve as resolve7 } from "path";
399
399
 
400
400
  // src/debug.ts
401
401
  var debugEnabled = false;
@@ -425,10 +425,37 @@ function debugLog(...args) {
425
425
  }
426
426
  }
427
427
 
428
- // src/ai/config.ts
428
+ // src/global-config.ts
429
429
  import { chmodSync, existsSync, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
430
430
  import { homedir } from "os";
431
431
  import { join as join2 } from "path";
432
+ var GLOBAL_CONFIG_DIR = join2(homedir(), ".glassbox");
433
+ var GLOBAL_CONFIG_PATH = join2(GLOBAL_CONFIG_DIR, "config.json");
434
+ function readGlobalConfig() {
435
+ try {
436
+ if (existsSync(GLOBAL_CONFIG_PATH)) {
437
+ const parsed = JSON.parse(readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
438
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
439
+ return parsed;
440
+ }
441
+ }
442
+ } catch {
443
+ }
444
+ return {};
445
+ }
446
+ function writeGlobalConfig(config) {
447
+ mkdirSync2(GLOBAL_CONFIG_DIR, { recursive: true });
448
+ writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
449
+ try {
450
+ chmodSync(GLOBAL_CONFIG_PATH, 384);
451
+ } catch {
452
+ }
453
+ }
454
+ function updateGlobalConfig(mutator) {
455
+ const cfg = readGlobalConfig();
456
+ const result = mutator(cfg);
457
+ writeGlobalConfig(result === void 0 ? cfg : result);
458
+ }
432
459
 
433
460
  // src/ai/api-keys.ts
434
461
  import { spawnSync as spawnSync2 } from "child_process";
@@ -589,11 +616,12 @@ function saveAPIKey(platform, key, storage) {
589
616
  if (storage === "keychain") {
590
617
  saveKeyToKeychain(platform, key);
591
618
  } else {
592
- const config = readConfigFile();
593
- if (config.ai === void 0) config.ai = {};
594
- if (config.ai.keys === void 0) config.ai.keys = {};
595
- config.ai.keys[platform] = Buffer.from(key).toString("base64");
596
- writeConfigFile(config);
619
+ updateGlobalConfig((raw) => {
620
+ const cfg = raw;
621
+ if (cfg.ai === void 0) cfg.ai = {};
622
+ if (cfg.ai.keys === void 0) cfg.ai.keys = {};
623
+ cfg.ai.keys[platform] = Buffer.from(key).toString("base64");
624
+ });
597
625
  }
598
626
  }
599
627
  function deleteAPIKey(platform) {
@@ -610,11 +638,13 @@ function deleteAPIKey(platform) {
610
638
  }
611
639
  } catch {
612
640
  }
613
- const config = readConfigFile();
614
- if (config.ai?.keys !== void 0) {
615
- config.ai.keys[platform] = "";
616
- writeConfigFile(config);
617
- }
641
+ if (readConfigFile().ai?.keys === void 0) return;
642
+ updateGlobalConfig((raw) => {
643
+ const cfg = raw;
644
+ if (cfg.ai?.keys !== void 0) {
645
+ cfg.ai.keys[platform] = "";
646
+ }
647
+ });
618
648
  }
619
649
  function detectAvailablePlatforms() {
620
650
  const results = [];
@@ -628,24 +658,8 @@ function detectAvailablePlatforms() {
628
658
  }
629
659
 
630
660
  // src/ai/config.ts
631
- var CONFIG_DIR = join2(homedir(), ".glassbox");
632
- var CONFIG_PATH = join2(CONFIG_DIR, "config.json");
633
661
  function readConfigFile() {
634
- try {
635
- if (existsSync(CONFIG_PATH)) {
636
- return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
637
- }
638
- } catch {
639
- }
640
- return {};
641
- }
642
- function writeConfigFile(config) {
643
- mkdirSync2(CONFIG_DIR, { recursive: true });
644
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
645
- try {
646
- chmodSync(CONFIG_PATH, 384);
647
- } catch {
648
- }
662
+ return readGlobalConfig();
649
663
  }
650
664
  function loadAIConfig() {
651
665
  const config = readConfigFile();
@@ -655,11 +669,12 @@ function loadAIConfig() {
655
669
  return { platform, model, apiKey: key, keySource: source };
656
670
  }
657
671
  function saveAIConfigPreferences(platform, model) {
658
- const config = readConfigFile();
659
- if (config.ai === void 0) config.ai = {};
660
- config.ai.platform = platform;
661
- config.ai.model = model;
662
- writeConfigFile(config);
672
+ updateGlobalConfig((config) => {
673
+ const cfg = config;
674
+ if (cfg.ai === void 0) cfg.ai = {};
675
+ cfg.ai.platform = platform;
676
+ cfg.ai.model = model;
677
+ });
663
678
  }
664
679
  function loadGuidedReviewConfig() {
665
680
  const config = readConfigFile();
@@ -669,9 +684,9 @@ function loadGuidedReviewConfig() {
669
684
  };
670
685
  }
671
686
  function saveGuidedReviewConfig(settings) {
672
- const config = readConfigFile();
673
- config.guidedReview = { enabled: settings.enabled, topics: settings.topics };
674
- writeConfigFile(config);
687
+ updateGlobalConfig((config) => {
688
+ config.guidedReview = { enabled: settings.enabled, topics: settings.topics };
689
+ });
675
690
  }
676
691
 
677
692
  // src/db/ai-queries.ts
@@ -1308,6 +1323,18 @@ function getHeadCommit(cwd) {
1308
1323
  return spawnSync3("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8" }).stdout.trim();
1309
1324
  }
1310
1325
 
1326
+ // src/git/parseDiffData.ts
1327
+ function parseDiffData(raw) {
1328
+ if (raw === null || raw === void 0 || raw === "") return null;
1329
+ try {
1330
+ const parsed = JSON.parse(raw);
1331
+ if (typeof parsed !== "object" || parsed === null) return null;
1332
+ return parsed;
1333
+ } catch {
1334
+ return null;
1335
+ }
1336
+ }
1337
+
1311
1338
  // src/git/diff.ts
1312
1339
  function git2(args, cwd) {
1313
1340
  const result = spawnSync4("git", args, { cwd, encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
@@ -1689,7 +1716,7 @@ async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
1689
1716
  for (const [path, existingFile] of existingByPath) {
1690
1717
  const newDiff = newDiffsByPath.get(path);
1691
1718
  if (newDiff) {
1692
- const oldDiff = JSON.parse(existingFile.diff_data ?? "{}");
1719
+ const oldDiff = parseDiffData(existingFile.diff_data) ?? {};
1693
1720
  const annotations = await getAnnotationsForFile(existingFile.id);
1694
1721
  if (annotations.length > 0) {
1695
1722
  stale += await migrateAnnotations(annotations, oldDiff, newDiff);
@@ -1701,7 +1728,7 @@ async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
1701
1728
  if (annotations.length === 0) {
1702
1729
  await deleteReviewFile(existingFile.id);
1703
1730
  } else {
1704
- const oldDiff = JSON.parse(existingFile.diff_data ?? "{}");
1731
+ const oldDiff = parseDiffData(existingFile.diff_data) ?? {};
1705
1732
  for (const a of annotations) {
1706
1733
  if (!a.is_stale) {
1707
1734
  const content = findLineContent(oldDiff, a.line_number, a.side);
@@ -1725,9 +1752,8 @@ async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
1725
1752
  // src/server.ts
1726
1753
  import { serve } from "@hono/node-server";
1727
1754
  import { exec } from "child_process";
1728
- import { existsSync as existsSync9, readFileSync as readFileSync12 } from "fs";
1729
- import { Hono as Hono8 } from "hono";
1730
- import { homedir as homedir6 } from "os";
1755
+ import { existsSync as existsSync8, readFileSync as readFileSync12 } from "fs";
1756
+ import { Hono as Hono16 } from "hono";
1731
1757
  import { dirname as dirname2, join as join10 } from "path";
1732
1758
  import { fileURLToPath as fileURLToPath2 } from "url";
1733
1759
 
@@ -1985,7 +2011,7 @@ function buildFileContexts(files, charBudget) {
1985
2011
  const contexts = [];
1986
2012
  const perFileBudget = Math.floor(charBudget / Math.max(files.length, 1));
1987
2013
  for (const file of files) {
1988
- const diff = JSON.parse(file.diff_data !== null && file.diff_data !== "" ? file.diff_data : "{}");
2014
+ const diff = parseDiffData(file.diff_data) ?? {};
1989
2015
  let added = 0;
1990
2016
  let removed = 0;
1991
2017
  const hunks = diff.hunks;
@@ -2048,6 +2074,45 @@ function extractJSON(text) {
2048
2074
  }
2049
2075
  throw new Error(`Could not extract JSON from AI response: ${text.slice(0, 300)}`);
2050
2076
  }
2077
+ async function runAnalysisBatch(files, config, repoRoot, options) {
2078
+ const contextWindow = getModelContextWindow(config.platform, config.model);
2079
+ const charBudget = Math.floor(contextWindow * 0.7 * 3);
2080
+ const contexts = buildFileContexts(files, charBudget);
2081
+ const validPaths = new Set(files.map((f) => f.file_path));
2082
+ const initialPrompt = [
2083
+ options.initialPromptHeader(files.length),
2084
+ "",
2085
+ formatContextsForPrompt(contexts)
2086
+ ].join("\n");
2087
+ const messages = [{ role: "user", content: initialPrompt }];
2088
+ for (let round = 0; round < 3; round++) {
2089
+ const response = await sendAIRequest(config, options.systemPrompt, messages);
2090
+ const parsed = extractJSON(response.content);
2091
+ if (isNeedContext(parsed)) {
2092
+ const safePaths = parsed.needContext.filter((p) => validPaths.has(p));
2093
+ if (safePaths.length === 0) {
2094
+ throw new Error("AI requested context for files not in the review");
2095
+ }
2096
+ const fileContents = safePaths.map((path) => ({
2097
+ path,
2098
+ content: getFileContent(path, "working", repoRoot)
2099
+ }));
2100
+ messages.push({ role: "assistant", content: response.content });
2101
+ messages.push({
2102
+ role: "user",
2103
+ content: `Here is the full content of the requested files:
2104
+
2105
+ ${formatAdditionalContext(fileContents)}`
2106
+ });
2107
+ continue;
2108
+ }
2109
+ if (!Array.isArray(parsed)) {
2110
+ throw new Error(`Expected an array of ${options.resultLabel} from AI`);
2111
+ }
2112
+ return parsed;
2113
+ }
2114
+ throw new Error(`${options.analysisName} did not converge after 3 context rounds`);
2115
+ }
2051
2116
 
2052
2117
  // src/ai/analyze-guided.ts
2053
2118
  var TOPIC_DISPLAY = {
@@ -2132,45 +2197,13 @@ If you need full file content to explain accurately, output ONLY: {"needContext"
2132
2197
 
2133
2198
  CRITICAL: Your entire response must be parseable by JSON.parse(). No prose, no markdown, no explanation.`;
2134
2199
  }
2135
- async function runGuidedAnalysisBatch(files, config, repoRoot, guidedReview) {
2136
- const contextWindow = getModelContextWindow(config.platform, config.model);
2137
- const charBudget = Math.floor(contextWindow * 0.7 * 3);
2138
- const systemPrompt = buildSystemPrompt(guidedReview);
2139
- const contexts = buildFileContexts(files, charBudget);
2140
- const validPaths = new Set(files.map((f) => f.file_path));
2141
- const initialPrompt = [
2142
- `Provide educational walkthrough notes for these ${String(files.length)} changed files:`,
2143
- "",
2144
- formatContextsForPrompt(contexts)
2145
- ].join("\n");
2146
- const messages = [{ role: "user", content: initialPrompt }];
2147
- for (let round = 0; round < 3; round++) {
2148
- const response = await sendAIRequest(config, systemPrompt, messages);
2149
- const parsed = extractJSON(response.content);
2150
- if (isNeedContext(parsed)) {
2151
- const safePaths = parsed.needContext.filter((p) => validPaths.has(p));
2152
- if (safePaths.length === 0) {
2153
- throw new Error("AI requested context for files not in the review");
2154
- }
2155
- const fileContents = safePaths.map((path) => ({
2156
- path,
2157
- content: getFileContent(path, "working", repoRoot)
2158
- }));
2159
- messages.push({ role: "assistant", content: response.content });
2160
- messages.push({
2161
- role: "user",
2162
- content: `Here is the full content of the requested files:
2163
-
2164
- ${formatAdditionalContext(fileContents)}`
2165
- });
2166
- continue;
2167
- }
2168
- if (!Array.isArray(parsed)) {
2169
- throw new Error("Expected an array of guided review notes from AI");
2170
- }
2171
- return parsed;
2172
- }
2173
- throw new Error("Guided analysis did not converge after 3 context rounds");
2200
+ function runGuidedAnalysisBatch(files, config, repoRoot, guidedReview) {
2201
+ return runAnalysisBatch(files, config, repoRoot, {
2202
+ systemPrompt: buildSystemPrompt(guidedReview),
2203
+ initialPromptHeader: (n) => `Provide educational walkthrough notes for these ${String(n)} changed files:`,
2204
+ resultLabel: "guided review notes",
2205
+ analysisName: "Guided analysis"
2206
+ });
2174
2207
  }
2175
2208
 
2176
2209
  // src/ai/guided-review.ts
@@ -2260,45 +2293,14 @@ Every file in the diff must appear exactly once.
2260
2293
  If you need full file content to determine dependencies, output ONLY: {"needContext":["path/to/file.ts"]}
2261
2294
 
2262
2295
  CRITICAL: Your entire response must be parseable by JSON.parse(). No prose, no markdown, no explanation.`;
2263
- async function runNarrativeAnalysisBatch(files, config, repoRoot, guidedReview) {
2264
- const contextWindow = getModelContextWindow(config.platform, config.model);
2265
- const charBudget = Math.floor(contextWindow * 0.7 * 3);
2296
+ function runNarrativeAnalysisBatch(files, config, repoRoot, guidedReview) {
2266
2297
  const systemPrompt = SYSTEM_PROMPT + (guidedReview !== void 0 ? buildGuidedReviewSuffix(guidedReview, "narrative") : "");
2267
- const contexts = buildFileContexts(files, charBudget);
2268
- const validPaths = new Set(files.map((f) => f.file_path));
2269
- const initialPrompt = [
2270
- `Determine the best reading order for reviewing these ${String(files.length)} changed files:`,
2271
- "",
2272
- formatContextsForPrompt(contexts)
2273
- ].join("\n");
2274
- const messages = [{ role: "user", content: initialPrompt }];
2275
- for (let round = 0; round < 3; round++) {
2276
- const response = await sendAIRequest(config, systemPrompt, messages);
2277
- const parsed = extractJSON(response.content);
2278
- if (isNeedContext(parsed)) {
2279
- const safePaths = parsed.needContext.filter((p) => validPaths.has(p));
2280
- if (safePaths.length === 0) {
2281
- throw new Error("AI requested context for files not in the review");
2282
- }
2283
- const fileContents = safePaths.map((path) => ({
2284
- path,
2285
- content: getFileContent(path, "working", repoRoot)
2286
- }));
2287
- messages.push({ role: "assistant", content: response.content });
2288
- messages.push({
2289
- role: "user",
2290
- content: `Here is the full content of the requested files:
2291
-
2292
- ${formatAdditionalContext(fileContents)}`
2293
- });
2294
- continue;
2295
- }
2296
- if (!Array.isArray(parsed)) {
2297
- throw new Error("Expected an array of narrative ordering from AI");
2298
- }
2299
- return parsed;
2300
- }
2301
- throw new Error("Narrative analysis did not converge after 3 context rounds");
2298
+ return runAnalysisBatch(files, config, repoRoot, {
2299
+ systemPrompt,
2300
+ initialPromptHeader: (n) => `Determine the best reading order for reviewing these ${String(n)} changed files:`,
2301
+ resultLabel: "narrative ordering",
2302
+ analysisName: "Narrative analysis"
2303
+ });
2302
2304
  }
2303
2305
  function mergeNarrativeOrders(batchResults, batchCount) {
2304
2306
  if (batchCount <= 1) {
@@ -2359,45 +2361,14 @@ The aggregate should be the MAX of all individual dimension scores (if a file ha
2359
2361
  If you need full file content to assess accurately, output ONLY: {"needContext":["path/to/file.ts"]}
2360
2362
 
2361
2363
  CRITICAL: Your entire response must be parseable by JSON.parse(). No prose, no markdown, no explanation.`;
2362
- async function runRiskAnalysisBatch(files, config, repoRoot, guidedReview) {
2363
- const contextWindow = getModelContextWindow(config.platform, config.model);
2364
- const charBudget = Math.floor(contextWindow * 0.7 * 3);
2364
+ function runRiskAnalysisBatch(files, config, repoRoot, guidedReview) {
2365
2365
  const systemPrompt = SYSTEM_PROMPT2 + (guidedReview !== void 0 ? buildGuidedReviewSuffix(guidedReview, "risk") : "");
2366
- const contexts = buildFileContexts(files, charBudget);
2367
- const validPaths = new Set(files.map((f) => f.file_path));
2368
- const initialPrompt = [
2369
- `Analyze the following ${String(files.length)} file diffs for risk:`,
2370
- "",
2371
- formatContextsForPrompt(contexts)
2372
- ].join("\n");
2373
- const messages = [{ role: "user", content: initialPrompt }];
2374
- for (let round = 0; round < 3; round++) {
2375
- const response = await sendAIRequest(config, systemPrompt, messages);
2376
- const parsed = extractJSON(response.content);
2377
- if (isNeedContext(parsed)) {
2378
- const safePaths = parsed.needContext.filter((p) => validPaths.has(p));
2379
- if (safePaths.length === 0) {
2380
- throw new Error("AI requested context for files not in the review");
2381
- }
2382
- const fileContents = safePaths.map((path) => ({
2383
- path,
2384
- content: getFileContent(path, "working", repoRoot)
2385
- }));
2386
- messages.push({ role: "assistant", content: response.content });
2387
- messages.push({
2388
- role: "user",
2389
- content: `Here is the full content of the requested files:
2390
-
2391
- ${formatAdditionalContext(fileContents)}`
2392
- });
2393
- continue;
2394
- }
2395
- if (!Array.isArray(parsed)) {
2396
- throw new Error("Expected an array of risk assessments from AI");
2397
- }
2398
- return parsed;
2399
- }
2400
- throw new Error("Risk analysis did not converge after 3 context rounds");
2366
+ return runAnalysisBatch(files, config, repoRoot, {
2367
+ systemPrompt,
2368
+ initialPromptHeader: (n) => `Analyze the following ${String(n)} file diffs for risk:`,
2369
+ resultLabel: "risk assessments",
2370
+ analysisName: "Risk analysis"
2371
+ });
2401
2372
  }
2402
2373
 
2403
2374
  // src/ai/batch-planner.ts
@@ -2538,8 +2509,8 @@ function isRetriable(err) {
2538
2509
  return msg.includes("429") || msg.includes("500") || msg.includes("502") || msg.includes("503") || msg.includes("504") || msg.includes("rate_limit");
2539
2510
  }
2540
2511
  function sleep(ms) {
2541
- return new Promise((resolve7) => {
2542
- setTimeout(resolve7, ms);
2512
+ return new Promise((resolve8) => {
2513
+ setTimeout(resolve8, ms);
2543
2514
  });
2544
2515
  }
2545
2516
 
@@ -2583,8 +2554,8 @@ function randomLines(count) {
2583
2554
  return lines.sort((a, b) => a.line - b.line);
2584
2555
  }
2585
2556
  function sleep2(ms) {
2586
- return new Promise((resolve7) => {
2587
- setTimeout(resolve7, ms);
2557
+ return new Promise((resolve8) => {
2558
+ setTimeout(resolve8, ms);
2588
2559
  });
2589
2560
  }
2590
2561
  async function mockRiskAnalysisBatch(files) {
@@ -2653,6 +2624,24 @@ async function mockGuidedAnalysisBatch(files) {
2653
2624
 
2654
2625
  // src/routes/ai-analysis.ts
2655
2626
  init_queries();
2627
+
2628
+ // src/utils/resolveReviewId.ts
2629
+ function resolveReviewId(c) {
2630
+ return c.req.query("reviewId") ?? c.get("reviewId");
2631
+ }
2632
+
2633
+ // src/utils/validate.ts
2634
+ function checkEnum(value, name, allowed) {
2635
+ if (typeof value !== "string" || !allowed.includes(value)) {
2636
+ return { error: `${name} must be one of: ${allowed.join(", ")}` };
2637
+ }
2638
+ return { ok: value };
2639
+ }
2640
+ function isNonEmptyString(value) {
2641
+ return typeof value === "string" && value.trim() !== "";
2642
+ }
2643
+
2644
+ // src/routes/ai-analysis.ts
2656
2645
  var aiAnalysisRoutes = new Hono();
2657
2646
  var VALID_SORT_MODES = ["folder", "risk", "narrative", "guided"];
2658
2647
  var VALID_RISK_DIMENSIONS = ["aggregate", "security", "correctness", "error-handling", "maintainability", "architecture", "performance"];
@@ -2661,15 +2650,14 @@ var VALID_IMAGE_MODES = ["metadata", "side-by-side", "difference", "slice"];
2661
2650
  var VALID_ANALYSIS_TYPES = ["risk", "narrative", "guided"];
2662
2651
  var cancelledAnalyses = /* @__PURE__ */ new Set();
2663
2652
  aiAnalysisRoutes.post("/analyze", async (c) => {
2664
- const reviewId = c.req.query("reviewId") ?? "";
2653
+ const reviewId = resolveReviewId(c);
2665
2654
  const repoRoot = c.get("repoRoot");
2666
2655
  const body = await c.req.json();
2667
2656
  const analysisType = body.type;
2668
2657
  const invalidateCache = body.invalidateCache === true;
2669
2658
  debugLog(`POST /analyze: type=${analysisType}, reviewId=${reviewId}`);
2670
- if (!VALID_ANALYSIS_TYPES.includes(analysisType)) {
2671
- return c.json({ error: `type must be one of: ${VALID_ANALYSIS_TYPES.join(", ")}` }, 400);
2672
- }
2659
+ const typeCheck = checkEnum(analysisType, "type", VALID_ANALYSIS_TYPES);
2660
+ if ("error" in typeCheck) return c.json({ error: typeCheck.error }, 400);
2673
2661
  const testMode = isAIServiceTest();
2674
2662
  const config = loadAIConfig();
2675
2663
  if (config.apiKey === null && !testMode) {
@@ -2719,113 +2707,115 @@ aiAnalysisRoutes.post("/analyze", async (c) => {
2719
2707
  const analysis = await createAnalysis(reviewId, analysisType);
2720
2708
  debugLog(`POST /analyze: created new analysis id=${analysis.id}`);
2721
2709
  const guidedReview = loadGuidedReviewConfig();
2722
- void (async () => {
2723
- try {
2724
- debugLog("Background analysis starting...");
2725
- const contextWindow = getModelContextWindow(config.platform, config.model);
2726
- debugLog(`Context window: ${String(contextWindow)} tokens`);
2727
- const { batches, binaryFiles } = planBatches(files, contextWindow);
2728
- const fileIdMap = new Map(files.map((f) => [f.file_path, f.id]));
2729
- const totalAnalyzable = batches.reduce((sum, b) => sum + b.files.length, 0);
2730
- debugLog(`Analysis plan: ${String(totalAnalyzable)} analyzable + ${String(binaryFiles.length)} binary = ${String(totalAnalyzable + binaryFiles.length)} total files in ${String(batches.length)} batch(es)`);
2731
- const prevScores = invalidateCache ? [] : await getPreviousScores(reviewId, analysisType, analysis.id);
2732
- const binaryPathSet = new Set(binaryFiles.map((f) => f.file_path));
2733
- const unchangedPaths = /* @__PURE__ */ new Set();
2734
- const cachedScores = prevScores.filter((s) => {
2735
- if (fileIdMap.has(s.file_path) && !binaryPathSet.has(s.file_path)) {
2736
- unchangedPaths.add(s.file_path);
2737
- return true;
2738
- }
2739
- return false;
2740
- });
2741
- debugLog(`Cache: ${String(cachedScores.length)} scores from previous analysis, ${String(totalAnalyzable - cachedScores.length)} files need processing`);
2742
- if (cachedScores.length > 0) {
2743
- const cachedForInsert = cachedScores.map((s) => ({
2744
- reviewFileId: fileIdMap.get(s.file_path) ?? s.review_file_id,
2745
- filePath: s.file_path,
2746
- sortOrder: s.sort_order,
2747
- aggregateScore: s.aggregate_score,
2748
- rationale: s.rationale,
2749
- dimensionScores: s.dimension_scores !== null ? JSON.parse(s.dimension_scores) : null,
2750
- notes: s.notes !== null ? JSON.parse(s.notes) : null
2751
- }));
2752
- await appendFileScores(analysis.id, cachedForInsert);
2753
- }
2754
- const filteredBatches = batches.map((batch) => {
2755
- const remaining = batch.files.filter((f) => !unchangedPaths.has(f.file_path));
2756
- return { files: remaining, estimatedTokens: batch.estimatedTokens };
2757
- }).filter((batch) => batch.files.length > 0);
2758
- const filteredAnalyzable = filteredBatches.reduce((sum, b) => sum + b.files.length, 0);
2759
- const totalForProgress = filteredAnalyzable + binaryFiles.length + cachedScores.length;
2760
- debugLog(`After cache: ${String(filteredAnalyzable)} files to analyze in ${String(filteredBatches.length)} batch(es)`);
2761
- await updateAnalysisProgress(analysis.id, cachedScores.length, totalForProgress);
2762
- if (binaryFiles.length > 0) {
2763
- debugLog(`Saving ${String(binaryFiles.length)} binary files with score 0`);
2764
- const binaryScoreEntries = binaryFiles.map((f, idx) => ({
2765
- reviewFileId: fileIdMap.get(f.file_path) ?? "",
2766
- filePath: f.file_path,
2767
- sortOrder: 99999 + idx,
2768
- // Will be re-sorted later
2769
- aggregateScore: analysisType === "risk" ? 0 : null,
2770
- rationale: "Binary file \u2014 not analyzed",
2771
- dimensionScores: analysisType === "risk" ? { security: 0, correctness: 0, "error-handling": 0, maintainability: 0, architecture: 0, performance: 0 } : null,
2772
- notes: null
2773
- }));
2774
- await appendFileScores(analysis.id, binaryScoreEntries);
2775
- await updateAnalysisProgress(analysis.id, cachedScores.length + binaryFiles.length, totalForProgress);
2776
- }
2777
- if (filteredBatches.length === 0) {
2778
- debugLog("No batches to process (all files cached or binary), marking completed");
2779
- await updateAnalysisStatus(analysis.id, "completed");
2780
- return;
2781
- }
2782
- const shouldCancel = () => cancelledAnalyses.has(analysis.id);
2783
- const progressOffset = cachedScores.length + binaryFiles.length;
2784
- if (analysisType === "risk") {
2785
- await runBatchedRiskAnalysis(analysis.id, filteredBatches, files, config, repoRoot, fileIdMap, totalForProgress, progressOffset, shouldCancel, guidedReview);
2786
- } else if (analysisType === "narrative") {
2787
- await runBatchedNarrativeAnalysis(analysis.id, filteredBatches, files, config, repoRoot, fileIdMap, totalForProgress, progressOffset, shouldCancel, guidedReview);
2788
- } else {
2789
- await runBatchedGuidedAnalysis(analysis.id, filteredBatches, files, config, repoRoot, fileIdMap, totalForProgress, progressOffset, shouldCancel, guidedReview);
2790
- }
2791
- if (cancelledAnalyses.has(analysis.id)) {
2792
- cancelledAnalyses.delete(analysis.id);
2793
- debugLog(`Analysis ${analysis.id} was cancelled (user switched modes)`);
2794
- await updateAnalysisStatus(analysis.id, "failed", "Cancelled");
2795
- return;
2796
- }
2797
- cancelledAnalyses.delete(analysis.id);
2798
- debugLog(`Analysis ${analysis.id} completed successfully`);
2799
- await updateAnalysisStatus(analysis.id, "completed");
2800
- } catch (err) {
2801
- const message = err instanceof Error ? err.message : "Unknown error";
2802
- console.error(`Analysis failed: ${message}`);
2803
- debugLog(`Analysis ${analysis.id} failed: ${message}`);
2804
- await updateAnalysisStatus(analysis.id, "failed", message);
2805
- }
2806
- })();
2710
+ void executeAnalysis({
2711
+ analysisId: analysis.id,
2712
+ analysisType: typeCheck.ok,
2713
+ reviewId,
2714
+ files,
2715
+ config,
2716
+ repoRoot,
2717
+ guidedReview,
2718
+ invalidateCache
2719
+ });
2807
2720
  return c.json({ analysisId: analysis.id, status: "running" });
2808
2721
  });
2809
- async function runBatchedRiskAnalysis(analysisId, batches, allFiles, config, repoRoot, fileIdMap, progressTotal, progressOffset, shouldCancel, guidedReview) {
2722
+ async function executeAnalysis(input) {
2723
+ const { analysisId, analysisType, reviewId, files, config, repoRoot, guidedReview, invalidateCache } = input;
2724
+ try {
2725
+ debugLog("Background analysis starting...");
2726
+ const contextWindow = getModelContextWindow(config.platform, config.model);
2727
+ debugLog(`Context window: ${String(contextWindow)} tokens`);
2728
+ const { batches, binaryFiles } = planBatches(files, contextWindow);
2729
+ const fileIdMap = new Map(files.map((f) => [f.file_path, f.id]));
2730
+ const totalAnalyzable = batches.reduce((sum, b) => sum + b.files.length, 0);
2731
+ debugLog(`Analysis plan: ${String(totalAnalyzable)} analyzable + ${String(binaryFiles.length)} binary = ${String(totalAnalyzable + binaryFiles.length)} total files in ${String(batches.length)} batch(es)`);
2732
+ const prevScores = invalidateCache ? [] : await getPreviousScores(reviewId, analysisType, analysisId);
2733
+ const binaryPathSet = new Set(binaryFiles.map((f) => f.file_path));
2734
+ const unchangedPaths = /* @__PURE__ */ new Set();
2735
+ const cachedScores = prevScores.filter((s) => {
2736
+ if (fileIdMap.has(s.file_path) && !binaryPathSet.has(s.file_path)) {
2737
+ unchangedPaths.add(s.file_path);
2738
+ return true;
2739
+ }
2740
+ return false;
2741
+ });
2742
+ debugLog(`Cache: ${String(cachedScores.length)} scores from previous analysis, ${String(totalAnalyzable - cachedScores.length)} files need processing`);
2743
+ if (cachedScores.length > 0) {
2744
+ const cachedForInsert = cachedScores.map((s) => ({
2745
+ reviewFileId: fileIdMap.get(s.file_path) ?? s.review_file_id,
2746
+ filePath: s.file_path,
2747
+ sortOrder: s.sort_order,
2748
+ aggregateScore: s.aggregate_score,
2749
+ rationale: s.rationale,
2750
+ dimensionScores: s.dimension_scores !== null ? JSON.parse(s.dimension_scores) : null,
2751
+ notes: s.notes !== null ? JSON.parse(s.notes) : null
2752
+ }));
2753
+ await appendFileScores(analysisId, cachedForInsert);
2754
+ }
2755
+ const filteredBatches = batches.map((batch) => {
2756
+ const remaining = batch.files.filter((f) => !unchangedPaths.has(f.file_path));
2757
+ return { files: remaining, estimatedTokens: batch.estimatedTokens };
2758
+ }).filter((batch) => batch.files.length > 0);
2759
+ const filteredAnalyzable = filteredBatches.reduce((sum, b) => sum + b.files.length, 0);
2760
+ const totalForProgress = filteredAnalyzable + binaryFiles.length + cachedScores.length;
2761
+ debugLog(`After cache: ${String(filteredAnalyzable)} files to analyze in ${String(filteredBatches.length)} batch(es)`);
2762
+ await updateAnalysisProgress(analysisId, cachedScores.length, totalForProgress);
2763
+ if (binaryFiles.length > 0) {
2764
+ debugLog(`Saving ${String(binaryFiles.length)} binary files with score 0`);
2765
+ const binaryScoreEntries = binaryFiles.map((f, idx) => ({
2766
+ reviewFileId: fileIdMap.get(f.file_path) ?? "",
2767
+ filePath: f.file_path,
2768
+ sortOrder: 99999 + idx,
2769
+ // Will be re-sorted later
2770
+ aggregateScore: analysisType === "risk" ? 0 : null,
2771
+ rationale: "Binary file \u2014 not analyzed",
2772
+ dimensionScores: analysisType === "risk" ? { security: 0, correctness: 0, "error-handling": 0, maintainability: 0, architecture: 0, performance: 0 } : null,
2773
+ notes: null
2774
+ }));
2775
+ await appendFileScores(analysisId, binaryScoreEntries);
2776
+ await updateAnalysisProgress(analysisId, cachedScores.length + binaryFiles.length, totalForProgress);
2777
+ }
2778
+ if (filteredBatches.length === 0) {
2779
+ debugLog("No batches to process (all files cached or binary), marking completed");
2780
+ await updateAnalysisStatus(analysisId, "completed");
2781
+ return;
2782
+ }
2783
+ const shouldCancel = () => cancelledAnalyses.has(analysisId);
2784
+ const progressOffset = cachedScores.length + binaryFiles.length;
2785
+ const runArgs = [analysisId, filteredBatches, files.length, totalForProgress, progressOffset];
2786
+ if (analysisType === "risk") {
2787
+ await runBatchedAnalysis(...runArgs, riskAnalysisConfig(config, repoRoot, fileIdMap, guidedReview, analysisId), shouldCancel);
2788
+ } else if (analysisType === "narrative") {
2789
+ await runBatchedAnalysis(...runArgs, narrativeAnalysisConfig(config, repoRoot, fileIdMap, guidedReview, analysisId), shouldCancel);
2790
+ } else {
2791
+ await runBatchedAnalysis(...runArgs, guidedAnalysisConfig(config, repoRoot, fileIdMap, guidedReview), shouldCancel);
2792
+ }
2793
+ if (cancelledAnalyses.has(analysisId)) {
2794
+ cancelledAnalyses.delete(analysisId);
2795
+ debugLog(`Analysis ${analysisId} was cancelled (user switched modes)`);
2796
+ await updateAnalysisStatus(analysisId, "failed", "Cancelled");
2797
+ return;
2798
+ }
2799
+ cancelledAnalyses.delete(analysisId);
2800
+ debugLog(`Analysis ${analysisId} completed successfully`);
2801
+ await updateAnalysisStatus(analysisId, "completed");
2802
+ } catch (err) {
2803
+ const message = err instanceof Error ? err.message : "Unknown error";
2804
+ console.error(`Analysis failed: ${message}`);
2805
+ debugLog(`Analysis ${analysisId} failed: ${message}`);
2806
+ await updateAnalysisStatus(analysisId, "failed", message);
2807
+ }
2808
+ }
2809
+ async function runBatchedAnalysis(analysisId, batches, totalFiles, progressTotal, progressOffset, cfg, shouldCancel) {
2810
2810
  const allResults = await runBatches(
2811
2811
  batches,
2812
- allFiles.length,
2813
- async (batch) => isAIServiceTest() ? mockRiskAnalysisBatch(batch.files) : runRiskAnalysisBatch(batch.files, config, repoRoot, guidedReview),
2812
+ totalFiles,
2813
+ async (batch) => cfg.runBatch(batch.files),
2814
2814
  async (_batchIndex, results) => {
2815
- for (const r of results) {
2816
- const maxDimension = Math.max(...Object.values(r.scores));
2817
- r.aggregate = Math.max(r.aggregate, maxDimension);
2815
+ if (cfg.postProcessResult) {
2816
+ for (const r of results) cfg.postProcessResult(r);
2818
2817
  }
2819
- const scores = results.map((r) => ({
2820
- reviewFileId: fileIdMap.get(r.filePath) ?? "",
2821
- filePath: r.filePath,
2822
- sortOrder: 0,
2823
- // Placeholder — final sort happens after all batches
2824
- aggregateScore: r.aggregate,
2825
- rationale: r.rationale,
2826
- dimensionScores: r.scores,
2827
- notes: r.notes ?? null
2828
- }));
2818
+ const scores = results.map((r, idx) => cfg.mapResult(r, idx));
2829
2819
  await appendFileScores(analysisId, scores);
2830
2820
  },
2831
2821
  async (progress) => {
@@ -2833,91 +2823,98 @@ async function runBatchedRiskAnalysis(analysisId, batches, allFiles, config, rep
2833
2823
  },
2834
2824
  1,
2835
2825
  shouldCancel,
2836
- "risk"
2826
+ cfg.analysisType
2837
2827
  );
2838
- const sorted = allResults.slice().sort((a, b) => b.aggregate - a.aggregate);
2839
- const sortMap = new Map(sorted.map((r, idx) => [r.filePath, idx]));
2828
+ if (cfg.finalize) await cfg.finalize(allResults, batches.length);
2829
+ }
2830
+ async function updateSortOrders(analysisId, entries) {
2840
2831
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_connection(), connection_exports));
2841
2832
  const db2 = await getDb2();
2842
- for (const [filePath, sortOrder] of sortMap) {
2833
+ for (const [filePath, sortOrder] of entries) {
2843
2834
  await db2.query(
2844
2835
  "UPDATE ai_file_scores SET sort_order = $1 WHERE analysis_id = $2 AND file_path = $3",
2845
2836
  [sortOrder, analysisId, filePath]
2846
2837
  );
2847
2838
  }
2848
2839
  }
2849
- async function runBatchedNarrativeAnalysis(analysisId, batches, allFiles, config, repoRoot, fileIdMap, progressTotal, progressOffset, shouldCancel, guidedReview) {
2850
- const allResults = await runBatches(
2851
- batches,
2852
- allFiles.length,
2853
- async (batch) => isAIServiceTest() ? mockNarrativeAnalysisBatch(batch.files) : runNarrativeAnalysisBatch(batch.files, config, repoRoot, guidedReview),
2854
- async (_batchIndex, results) => {
2855
- const scores = results.map((r) => ({
2856
- reviewFileId: fileIdMap.get(r.filePath) ?? "",
2857
- filePath: r.filePath,
2858
- sortOrder: r.position,
2859
- // Batch-local position — will be re-sorted after merge
2860
- aggregateScore: null,
2861
- rationale: r.rationale,
2862
- dimensionScores: null,
2863
- notes: r.notes ?? null
2864
- }));
2865
- await appendFileScores(analysisId, scores);
2866
- },
2867
- async (progress) => {
2868
- await updateAnalysisProgress(analysisId, progressOffset + progress.completedFiles, progressTotal);
2840
+ function pickRunner(real, mock) {
2841
+ return isAIServiceTest() ? mock() : real();
2842
+ }
2843
+ function riskAnalysisConfig(config, repoRoot, fileIdMap, guidedReview, analysisId) {
2844
+ return {
2845
+ analysisType: "risk",
2846
+ runBatch: (files) => pickRunner(
2847
+ () => runRiskAnalysisBatch(files, config, repoRoot, guidedReview),
2848
+ () => mockRiskAnalysisBatch(files)
2849
+ ),
2850
+ postProcessResult: (r) => {
2851
+ const maxDimension = Math.max(...Object.values(r.scores));
2852
+ r.aggregate = Math.max(r.aggregate, maxDimension);
2869
2853
  },
2870
- 1,
2871
- shouldCancel,
2872
- "narrative"
2873
- );
2874
- if (allResults.length > 0) {
2875
- const mergedPositions = mergeNarrativeOrders(allResults, batches.length);
2876
- const { getDb: getDb2 } = await Promise.resolve().then(() => (init_connection(), connection_exports));
2877
- const db2 = await getDb2();
2878
- for (const [filePath, position] of mergedPositions) {
2879
- await db2.query(
2880
- "UPDATE ai_file_scores SET sort_order = $1 WHERE analysis_id = $2 AND file_path = $3",
2881
- [position, analysisId, filePath]
2882
- );
2854
+ mapResult: (r) => ({
2855
+ reviewFileId: fileIdMap.get(r.filePath) ?? "",
2856
+ filePath: r.filePath,
2857
+ sortOrder: 0,
2858
+ // Placeholder final sort happens after all batches
2859
+ aggregateScore: r.aggregate,
2860
+ rationale: r.rationale,
2861
+ dimensionScores: r.scores,
2862
+ notes: r.notes ?? null
2863
+ }),
2864
+ finalize: async (allResults) => {
2865
+ const sorted = allResults.slice().sort((a, b) => b.aggregate - a.aggregate);
2866
+ await updateSortOrders(analysisId, sorted.map((r, idx) => [r.filePath, idx]));
2883
2867
  }
2884
- }
2868
+ };
2885
2869
  }
2886
- async function runBatchedGuidedAnalysis(analysisId, batches, allFiles, config, repoRoot, fileIdMap, progressTotal, progressOffset, shouldCancel, guidedReview) {
2887
- await runBatches(
2888
- batches,
2889
- allFiles.length,
2890
- async (batch) => {
2891
- if (isAIServiceTest()) return mockGuidedAnalysisBatch(batch.files);
2870
+ function narrativeAnalysisConfig(config, repoRoot, fileIdMap, guidedReview, analysisId) {
2871
+ return {
2872
+ analysisType: "narrative",
2873
+ runBatch: (files) => pickRunner(
2874
+ () => runNarrativeAnalysisBatch(files, config, repoRoot, guidedReview),
2875
+ () => mockNarrativeAnalysisBatch(files)
2876
+ ),
2877
+ mapResult: (r) => ({
2878
+ reviewFileId: fileIdMap.get(r.filePath) ?? "",
2879
+ filePath: r.filePath,
2880
+ sortOrder: r.position,
2881
+ // Batch-local position — will be re-sorted after merge
2882
+ aggregateScore: null,
2883
+ rationale: r.rationale,
2884
+ dimensionScores: null,
2885
+ notes: r.notes ?? null
2886
+ }),
2887
+ finalize: async (allResults, batchCount) => {
2888
+ if (allResults.length === 0) return;
2889
+ const merged = mergeNarrativeOrders(allResults, batchCount);
2890
+ await updateSortOrders(analysisId, merged);
2891
+ }
2892
+ };
2893
+ }
2894
+ function guidedAnalysisConfig(config, repoRoot, fileIdMap, guidedReview) {
2895
+ return {
2896
+ analysisType: "guided",
2897
+ runBatch: (files) => {
2898
+ if (isAIServiceTest()) return mockGuidedAnalysisBatch(files);
2892
2899
  if (guidedReview === void 0) throw new Error("Guided review config required");
2893
- return runGuidedAnalysisBatch(batch.files, config, repoRoot, guidedReview);
2894
- },
2895
- async (_batchIndex, results) => {
2896
- const scores = results.map((r, idx) => ({
2897
- reviewFileId: fileIdMap.get(r.filePath) ?? "",
2898
- filePath: r.filePath,
2899
- sortOrder: idx,
2900
- aggregateScore: null,
2901
- rationale: null,
2902
- dimensionScores: null,
2903
- notes: r.notes
2904
- }));
2905
- await appendFileScores(analysisId, scores);
2906
- },
2907
- async (progress) => {
2908
- await updateAnalysisProgress(analysisId, progressOffset + progress.completedFiles, progressTotal);
2900
+ return runGuidedAnalysisBatch(files, config, repoRoot, guidedReview);
2909
2901
  },
2910
- 1,
2911
- shouldCancel,
2912
- "guided"
2913
- );
2914
- }
2902
+ mapResult: (r, idx) => ({
2903
+ reviewFileId: fileIdMap.get(r.filePath) ?? "",
2904
+ filePath: r.filePath,
2905
+ sortOrder: idx,
2906
+ aggregateScore: null,
2907
+ rationale: null,
2908
+ dimensionScores: null,
2909
+ notes: r.notes
2910
+ })
2911
+ };
2912
+ }
2915
2913
  aiAnalysisRoutes.get("/analysis/:type", async (c) => {
2916
- const reviewId = c.req.query("reviewId") ?? "";
2914
+ const reviewId = resolveReviewId(c);
2917
2915
  const analysisType = c.req.param("type");
2918
- if (!VALID_ANALYSIS_TYPES.includes(analysisType)) {
2919
- return c.json({ error: `type must be one of: ${VALID_ANALYSIS_TYPES.join(", ")}` }, 400);
2920
- }
2916
+ const typeCheck = checkEnum(analysisType, "type", VALID_ANALYSIS_TYPES);
2917
+ if ("error" in typeCheck) return c.json({ error: typeCheck.error }, 400);
2921
2918
  const analysis = await getLatestAnalysis(reviewId, analysisType);
2922
2919
  if (analysis === void 0) {
2923
2920
  debugLog(`GET /analysis/${analysisType}: no analysis found`);
@@ -2948,11 +2945,10 @@ aiAnalysisRoutes.get("/analysis/:type", async (c) => {
2948
2945
  });
2949
2946
  });
2950
2947
  aiAnalysisRoutes.get("/analysis/:type/status", async (c) => {
2951
- const reviewId = c.req.query("reviewId") ?? "";
2948
+ const reviewId = resolveReviewId(c);
2952
2949
  const analysisType = c.req.param("type");
2953
- if (!VALID_ANALYSIS_TYPES.includes(analysisType)) {
2954
- return c.json({ error: `type must be one of: ${VALID_ANALYSIS_TYPES.join(", ")}` }, 400);
2955
- }
2950
+ const typeCheck = checkEnum(analysisType, "type", VALID_ANALYSIS_TYPES);
2951
+ if ("error" in typeCheck) return c.json({ error: typeCheck.error }, 400);
2956
2952
  const analysis = await getLatestAnalysis(reviewId, analysisType);
2957
2953
  if (analysis === void 0) {
2958
2954
  debugLog(`GET /analysis/${analysisType}/status: no analysis found`);
@@ -2992,11 +2988,13 @@ aiAnalysisRoutes.get("/preferences", async (c) => {
2992
2988
  });
2993
2989
  aiAnalysisRoutes.post("/preferences", async (c) => {
2994
2990
  const body = await c.req.json();
2995
- if (body.sort_mode !== void 0 && !VALID_SORT_MODES.includes(body.sort_mode)) {
2996
- return c.json({ error: `sort_mode must be one of: ${VALID_SORT_MODES.join(", ")}` }, 400);
2991
+ if (body.sort_mode !== void 0) {
2992
+ const v = checkEnum(body.sort_mode, "sort_mode", VALID_SORT_MODES);
2993
+ if ("error" in v) return c.json({ error: v.error }, 400);
2997
2994
  }
2998
- if (body.risk_sort_dimension !== void 0 && !VALID_RISK_DIMENSIONS.includes(body.risk_sort_dimension)) {
2999
- return c.json({ error: `risk_sort_dimension must be one of: ${VALID_RISK_DIMENSIONS.join(", ")}` }, 400);
2995
+ if (body.risk_sort_dimension !== void 0) {
2996
+ const v = checkEnum(body.risk_sort_dimension, "risk_sort_dimension", VALID_RISK_DIMENSIONS);
2997
+ if ("error" in v) return c.json({ error: v.error }, 400);
3000
2998
  }
3001
2999
  if (body.show_risk_scores !== void 0 && typeof body.show_risk_scores !== "boolean") {
3002
3000
  return c.json({ error: "show_risk_scores must be a boolean" }, 400);
@@ -3004,11 +3002,13 @@ aiAnalysisRoutes.post("/preferences", async (c) => {
3004
3002
  if (body.ignore_whitespace !== void 0 && typeof body.ignore_whitespace !== "boolean") {
3005
3003
  return c.json({ error: "ignore_whitespace must be a boolean" }, 400);
3006
3004
  }
3007
- if (body.svg_view_mode !== void 0 && !VALID_SVG_VIEW_MODES.includes(body.svg_view_mode)) {
3008
- return c.json({ error: `svg_view_mode must be one of: ${VALID_SVG_VIEW_MODES.join(", ")}` }, 400);
3005
+ if (body.svg_view_mode !== void 0) {
3006
+ const v = checkEnum(body.svg_view_mode, "svg_view_mode", VALID_SVG_VIEW_MODES);
3007
+ if ("error" in v) return c.json({ error: v.error }, 400);
3009
3008
  }
3010
- if (body.last_image_mode !== void 0 && !VALID_IMAGE_MODES.includes(body.last_image_mode)) {
3011
- return c.json({ error: `last_image_mode must be one of: ${VALID_IMAGE_MODES.join(", ")}` }, 400);
3009
+ if (body.last_image_mode !== void 0) {
3010
+ const v = checkEnum(body.last_image_mode, "last_image_mode", VALID_IMAGE_MODES);
3011
+ if ("error" in v) return c.json({ error: v.error }, 400);
3012
3012
  }
3013
3013
  await saveUserPreferences(body);
3014
3014
  return c.json({ ok: true });
@@ -3031,10 +3031,9 @@ aiConfigRoutes.get("/config", (c) => {
3031
3031
  });
3032
3032
  aiConfigRoutes.post("/config", async (c) => {
3033
3033
  const body = await c.req.json();
3034
- if (!VALID_PLATFORMS.includes(body.platform)) {
3035
- return c.json({ error: `platform must be one of: ${VALID_PLATFORMS.join(", ")}` }, 400);
3036
- }
3037
- if (typeof body.model !== "string" || body.model.trim() === "") {
3034
+ const platformCheck = checkEnum(body.platform, "platform", VALID_PLATFORMS);
3035
+ if ("error" in platformCheck) return c.json({ error: platformCheck.error }, 400);
3036
+ if (!isNonEmptyString(body.model)) {
3038
3037
  return c.json({ error: "model must be a non-empty string" }, 400);
3039
3038
  }
3040
3039
  if (body.guidedReview !== void 0) {
@@ -3050,7 +3049,7 @@ aiConfigRoutes.post("/config", async (c) => {
3050
3049
  return c.json({ error: "guidedReview.topics must be an array of strings" }, 400);
3051
3050
  }
3052
3051
  }
3053
- saveAIConfigPreferences(body.platform, body.model);
3052
+ saveAIConfigPreferences(platformCheck.ok, body.model);
3054
3053
  if (body.guidedReview !== void 0) {
3055
3054
  saveGuidedReviewConfig(body.guidedReview);
3056
3055
  }
@@ -3078,28 +3077,25 @@ aiConfigRoutes.get("/key-status", (c) => {
3078
3077
  });
3079
3078
  aiConfigRoutes.post("/key", async (c) => {
3080
3079
  const body = await c.req.json();
3081
- if (!VALID_PLATFORMS.includes(body.platform)) {
3082
- return c.json({ error: `platform must be one of: ${VALID_PLATFORMS.join(", ")}` }, 400);
3083
- }
3084
- if (typeof body.key !== "string" || body.key.trim() === "") {
3080
+ const platformCheck = checkEnum(body.platform, "platform", VALID_PLATFORMS);
3081
+ if ("error" in platformCheck) return c.json({ error: platformCheck.error }, 400);
3082
+ if (!isNonEmptyString(body.key)) {
3085
3083
  return c.json({ error: "key must be a non-empty string" }, 400);
3086
3084
  }
3087
- if (!VALID_KEY_STORAGES.includes(body.storage)) {
3088
- return c.json({ error: `storage must be one of: ${VALID_KEY_STORAGES.join(", ")}` }, 400);
3089
- }
3085
+ const storageCheck = checkEnum(body.storage, "storage", VALID_KEY_STORAGES);
3086
+ if ("error" in storageCheck) return c.json({ error: storageCheck.error }, 400);
3090
3087
  saveAPIKey(
3091
- body.platform,
3088
+ platformCheck.ok,
3092
3089
  body.key,
3093
- body.storage
3090
+ storageCheck.ok
3094
3091
  );
3095
3092
  return c.json({ ok: true });
3096
3093
  });
3097
3094
  aiConfigRoutes.delete("/key", (c) => {
3098
3095
  const platform = c.req.query("platform") ?? "anthropic";
3099
- if (!VALID_PLATFORMS.includes(platform)) {
3100
- return c.json({ error: `platform must be one of: ${VALID_PLATFORMS.join(", ")}` }, 400);
3101
- }
3102
- deleteAPIKey(platform);
3096
+ const platformCheck = checkEnum(platform, "platform", VALID_PLATFORMS);
3097
+ if ("error" in platformCheck) return c.json({ error: platformCheck.error }, 400);
3098
+ deleteAPIKey(platformCheck.ok);
3103
3099
  return c.json({ ok: true });
3104
3100
  });
3105
3101
 
@@ -3109,12 +3105,11 @@ aiApiRoutes.route("/", aiConfigRoutes);
3109
3105
  aiApiRoutes.route("/", aiAnalysisRoutes);
3110
3106
 
3111
3107
  // src/routes/api.ts
3108
+ import { Hono as Hono12 } from "hono";
3109
+
3110
+ // src/routes/api/annotations.ts
3112
3111
  init_queries();
3113
- import { execFileSync, spawnSync as spawnSync7 } from "child_process";
3114
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
3115
3112
  import { Hono as Hono4 } from "hono";
3116
- import { homedir as homedir3 } from "os";
3117
- import { join as join7, resolve as resolve4 } from "path";
3118
3113
 
3119
3114
  // src/export/generate.ts
3120
3115
  init_queries();
@@ -3265,10 +3260,166 @@ function scheduleAutoExport(reviewId, repoRoot) {
3265
3260
  }, DEBOUNCE_MS);
3266
3261
  }
3267
3262
 
3263
+ // src/routes/api/annotations.ts
3264
+ var annotationsRoutes = new Hono4();
3265
+ var VALID_CATEGORIES = ["bug", "fix", "style", "pattern-follow", "pattern-avoid", "note", "remember"];
3266
+ var VALID_SIDES = ["old", "new"];
3267
+ function autoExport(c) {
3268
+ scheduleAutoExport(c.get("reviewId"), c.get("repoRoot"));
3269
+ }
3270
+ annotationsRoutes.post("/annotations", async (c) => {
3271
+ const body = await c.req.json();
3272
+ if (!isNonEmptyString(body.reviewFileId)) {
3273
+ return c.json({ error: "reviewFileId must be a non-empty string" }, 400);
3274
+ }
3275
+ if (typeof body.lineNumber !== "number" || !Number.isInteger(body.lineNumber) || body.lineNumber < 1) {
3276
+ return c.json({ error: "lineNumber must be a positive integer" }, 400);
3277
+ }
3278
+ const sideCheck = checkEnum(body.side, "side", VALID_SIDES);
3279
+ if ("error" in sideCheck) return c.json({ error: sideCheck.error }, 400);
3280
+ const categoryCheck = checkEnum(body.category, "category", VALID_CATEGORIES);
3281
+ if ("error" in categoryCheck) return c.json({ error: categoryCheck.error }, 400);
3282
+ if (!isNonEmptyString(body.content)) {
3283
+ return c.json({ error: "content must be a non-empty string" }, 400);
3284
+ }
3285
+ const annotation = await addAnnotation(
3286
+ body.reviewFileId,
3287
+ body.lineNumber,
3288
+ sideCheck.ok,
3289
+ categoryCheck.ok,
3290
+ body.content
3291
+ );
3292
+ autoExport(c);
3293
+ return c.json(annotation, 201);
3294
+ });
3295
+ annotationsRoutes.patch("/annotations/:id", async (c) => {
3296
+ const { content, category } = await c.req.json();
3297
+ if (!isNonEmptyString(content)) {
3298
+ return c.json({ error: "content must be a non-empty string" }, 400);
3299
+ }
3300
+ const categoryCheck = checkEnum(category, "category", VALID_CATEGORIES);
3301
+ if ("error" in categoryCheck) return c.json({ error: categoryCheck.error }, 400);
3302
+ await updateAnnotation(c.req.param("id"), content, categoryCheck.ok);
3303
+ autoExport(c);
3304
+ return c.json({ ok: true });
3305
+ });
3306
+ annotationsRoutes.delete("/annotations/:id", async (c) => {
3307
+ await deleteAnnotation(c.req.param("id"));
3308
+ autoExport(c);
3309
+ return c.json({ ok: true });
3310
+ });
3311
+ annotationsRoutes.patch("/annotations/:id/move", async (c) => {
3312
+ const { lineNumber, side } = await c.req.json();
3313
+ if (typeof lineNumber !== "number" || !Number.isInteger(lineNumber) || lineNumber < 1) {
3314
+ return c.json({ error: "lineNumber must be a positive integer" }, 400);
3315
+ }
3316
+ const sideCheck = checkEnum(side, "side", VALID_SIDES);
3317
+ if ("error" in sideCheck) return c.json({ error: sideCheck.error }, 400);
3318
+ await moveAnnotation(c.req.param("id"), lineNumber, sideCheck.ok);
3319
+ autoExport(c);
3320
+ return c.json({ ok: true });
3321
+ });
3322
+ annotationsRoutes.post("/annotations/:id/keep", async (c) => {
3323
+ await markAnnotationCurrent(c.req.param("id"));
3324
+ autoExport(c);
3325
+ return c.json({ ok: true });
3326
+ });
3327
+ annotationsRoutes.post("/annotations/stale/delete-all", async (c) => {
3328
+ const reviewId = resolveReviewId(c);
3329
+ await deleteStaleAnnotations(reviewId);
3330
+ autoExport(c);
3331
+ return c.json({ ok: true });
3332
+ });
3333
+ annotationsRoutes.post("/annotations/stale/keep-all", async (c) => {
3334
+ const reviewId = resolveReviewId(c);
3335
+ await keepAllStaleAnnotations(reviewId);
3336
+ autoExport(c);
3337
+ return c.json({ ok: true });
3338
+ });
3339
+ annotationsRoutes.get("/annotations/all", async (c) => {
3340
+ const reviewId = resolveReviewId(c);
3341
+ const annotations = await getAnnotationsForReview(reviewId);
3342
+ return c.json(annotations);
3343
+ });
3344
+
3345
+ // src/routes/api/context.ts
3346
+ init_queries();
3347
+ import { Hono as Hono5 } from "hono";
3348
+ var contextRoutes = new Hono5();
3349
+ contextRoutes.get("/context/:fileId", async (c) => {
3350
+ const repoRoot = c.get("repoRoot");
3351
+ const file = await getReviewFile(c.req.param("fileId"));
3352
+ if (!file) return c.json({ error: "Not found" }, 404);
3353
+ const startLine = parseInt(c.req.query("start") ?? "1", 10);
3354
+ const endLine = parseInt(c.req.query("end") ?? "20", 10);
3355
+ const content = getFileContent(file.file_path, "working", repoRoot);
3356
+ const allLines = content.split("\n");
3357
+ const clampedStart = Math.max(1, startLine);
3358
+ const clampedEnd = Math.min(allLines.length, endLine);
3359
+ const lines = [];
3360
+ for (let i = clampedStart; i <= clampedEnd; i++) {
3361
+ lines.push({ num: i, content: allLines[i - 1] || "" });
3362
+ }
3363
+ return c.json({ lines });
3364
+ });
3365
+
3366
+ // src/routes/api/files.ts
3367
+ init_queries();
3368
+ import { execFileSync } from "child_process";
3369
+ import { Hono as Hono6 } from "hono";
3370
+ import { resolve as resolve3 } from "path";
3371
+ var filesRoutes = new Hono6();
3372
+ var VALID_FILE_STATUSES = ["pending", "reviewed"];
3373
+ filesRoutes.get("/files", async (c) => {
3374
+ const reviewId = resolveReviewId(c);
3375
+ const files = await getReviewFiles(reviewId);
3376
+ const annotationCounts = {};
3377
+ for (const file of files) {
3378
+ const annotations = await getAnnotationsForFile(file.id);
3379
+ annotationCounts[file.id] = annotations.length;
3380
+ }
3381
+ const staleCounts = await getStaleCountsForReview(reviewId);
3382
+ return c.json({ files, annotationCounts, staleCounts });
3383
+ });
3384
+ filesRoutes.get("/files/:fileId", async (c) => {
3385
+ const file = await getReviewFile(c.req.param("fileId"));
3386
+ if (!file) return c.json({ error: "Not found" }, 404);
3387
+ const annotations = await getAnnotationsForFile(file.id);
3388
+ return c.json({ file, annotations });
3389
+ });
3390
+ filesRoutes.patch("/files/:fileId/status", async (c) => {
3391
+ const { status } = await c.req.json();
3392
+ const v = checkEnum(status, "status", VALID_FILE_STATUSES);
3393
+ if ("error" in v) return c.json({ error: v.error }, 400);
3394
+ await updateFileStatus(c.req.param("fileId"), v.ok);
3395
+ return c.json({ ok: true });
3396
+ });
3397
+ filesRoutes.post("/files/:fileId/reveal", async (c) => {
3398
+ const file = await getReviewFile(c.req.param("fileId"));
3399
+ if (!file) return c.json({ error: "Not found" }, 404);
3400
+ const repoRoot = c.get("repoRoot");
3401
+ const fullPath = resolve3(repoRoot, file.file_path);
3402
+ try {
3403
+ if (process.platform === "darwin") {
3404
+ execFileSync("open", ["-R", fullPath]);
3405
+ } else if (process.platform === "win32") {
3406
+ execFileSync("explorer", ["/select," + fullPath]);
3407
+ } else {
3408
+ execFileSync("xdg-open", [resolve3(fullPath, "..")]);
3409
+ }
3410
+ } catch {
3411
+ }
3412
+ return c.json({ ok: true });
3413
+ });
3414
+
3415
+ // src/routes/api/image.ts
3416
+ init_queries();
3417
+ import { Hono as Hono7 } from "hono";
3418
+
3268
3419
  // src/git/image.ts
3269
3420
  import { spawnSync as spawnSync6 } from "child_process";
3270
3421
  import { readFileSync as readFileSync6 } from "fs";
3271
- import { resolve as resolve3 } from "path";
3422
+ import { resolve as resolve4 } from "path";
3272
3423
 
3273
3424
  // src/git/image-metadata.ts
3274
3425
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]);
@@ -3538,7 +3689,7 @@ function gitShowFile(ref, filePath, repoRoot) {
3538
3689
  }
3539
3690
  function readWorkingFile(filePath, repoRoot) {
3540
3691
  try {
3541
- return readFileSync6(resolve3(repoRoot, filePath));
3692
+ return readFileSync6(resolve4(repoRoot, filePath));
3542
3693
  } catch {
3543
3694
  return null;
3544
3695
  }
@@ -3705,6 +3856,65 @@ async function rasterizeSvg(svgData) {
3705
3856
  return png;
3706
3857
  }
3707
3858
 
3859
+ // src/routes/api/image.ts
3860
+ var imageRoutes = new Hono7();
3861
+ imageRoutes.get("/image/:fileId/metadata", async (c) => {
3862
+ const fileId = c.req.param("fileId");
3863
+ const file = await getReviewFile(fileId);
3864
+ if (!file) return c.json({ error: "Not found" }, 404);
3865
+ const repoRoot = c.get("repoRoot");
3866
+ const review = await getReview(file.review_id);
3867
+ if (!review) return c.json({ error: "Review not found" }, 404);
3868
+ const mode = parseModeString(review.mode);
3869
+ const diff = parseDiffData(file.diff_data);
3870
+ const oldPath = diff?.oldPath ?? null;
3871
+ const status = diff?.status ?? "modified";
3872
+ const oldImage = status !== "added" ? getOldImage(mode, file.file_path, oldPath, repoRoot) : null;
3873
+ const newImage = status !== "deleted" ? getNewImage(mode, file.file_path, repoRoot) : null;
3874
+ const oldMeta = oldImage !== null ? extractMetadata(oldImage.data, oldPath ?? file.file_path) : null;
3875
+ const newMeta = newImage !== null ? extractMetadata(newImage.data, file.file_path) : null;
3876
+ return c.json({
3877
+ old: oldMeta ? formatMetadataLines(oldMeta) : null,
3878
+ new: newMeta ? formatMetadataLines(newMeta) : null
3879
+ });
3880
+ });
3881
+ imageRoutes.get("/image/:fileId/:side", async (c) => {
3882
+ const fileId = c.req.param("fileId");
3883
+ const side = c.req.param("side");
3884
+ if (side !== "old" && side !== "new") return c.text("Invalid side", 400);
3885
+ const file = await getReviewFile(fileId);
3886
+ if (!file) return c.text("Not found", 404);
3887
+ const repoRoot = c.get("repoRoot");
3888
+ const review = await getReview(file.review_id);
3889
+ if (!review) return c.text("Review not found", 404);
3890
+ const mode = parseModeString(review.mode);
3891
+ const diff = parseDiffData(file.diff_data);
3892
+ const oldPath = diff?.oldPath ?? null;
3893
+ const image = side === "old" ? getOldImage(mode, file.file_path, oldPath, repoRoot) : getNewImage(mode, file.file_path, repoRoot);
3894
+ if (!image) return c.text("Image not available", 404);
3895
+ if (isSvgFile(file.file_path)) {
3896
+ try {
3897
+ const png = await rasterizeSvg(image.data);
3898
+ return new Response(new Uint8Array(png), {
3899
+ headers: { "Content-Type": "image/png", "Cache-Control": "no-cache" }
3900
+ });
3901
+ } catch {
3902
+ return c.text("SVG rasterization failed", 500);
3903
+ }
3904
+ }
3905
+ const contentType = getContentType(file.file_path);
3906
+ return new Response(new Uint8Array(image.data), {
3907
+ headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
3908
+ });
3909
+ });
3910
+
3911
+ // src/routes/api/outline.ts
3912
+ init_queries();
3913
+ import { spawnSync as spawnSync7 } from "child_process";
3914
+ import { readFileSync as readFileSync8 } from "fs";
3915
+ import { Hono as Hono8 } from "hono";
3916
+ import { resolve as resolve5 } from "path";
3917
+
3708
3918
  // src/outline/parser.ts
3709
3919
  var BRACE_LANGS = /* @__PURE__ */ new Set([
3710
3920
  "javascript",
@@ -4023,262 +4233,45 @@ function pushIndentSymbol(root, stack, sym, indent, lines, lineIdx) {
4023
4233
  stack.push({ symbol: sym, indent });
4024
4234
  }
4025
4235
 
4026
- // src/routes/api.ts
4027
- var apiRoutes = new Hono4();
4028
- var VALID_CATEGORIES = ["bug", "fix", "style", "pattern-follow", "pattern-avoid", "note", "remember"];
4029
- var VALID_SIDES = ["old", "new"];
4030
- var VALID_FILE_STATUSES = ["pending", "reviewed"];
4031
- function resolveReviewId(c) {
4032
- return c.req.query("reviewId") ?? c.get("reviewId");
4033
- }
4034
- apiRoutes.get("/reviews", async (c) => {
4035
- const repoRoot = c.get("repoRoot");
4036
- const reviews = await listReviews(repoRoot);
4037
- return c.json(reviews);
4038
- });
4039
- apiRoutes.get("/review", async (c) => {
4040
- const reviewId = resolveReviewId(c);
4041
- const review = await getReview(reviewId);
4042
- return c.json(review);
4043
- });
4044
- apiRoutes.post("/review/complete", async (c) => {
4045
- const reviewId = resolveReviewId(c);
4046
- const currentReviewId = c.get("currentReviewId");
4047
- const repoRoot = c.get("repoRoot");
4048
- await updateReviewStatus(reviewId, "completed");
4049
- const isCurrent = reviewId === currentReviewId;
4050
- const exportPath = await generateReviewExport(reviewId, repoRoot, isCurrent);
4051
- const gitignorePrompt = shouldPromptGitignore(repoRoot);
4052
- return c.json({ status: "completed", exportPath, isCurrent, reviewId, gitignorePrompt });
4053
- });
4054
- apiRoutes.post("/gitignore/add", (c) => {
4236
+ // src/routes/api/outline.ts
4237
+ var outlineRoutes = new Hono8();
4238
+ outlineRoutes.get("/outline/:fileId", async (c) => {
4055
4239
  const repoRoot = c.get("repoRoot");
4056
- addGlassboxToGitignore(repoRoot);
4057
- return c.json({ ok: true });
4058
- });
4059
- apiRoutes.post("/gitignore/dismiss", (c) => {
4060
- const repoRoot = c.get("repoRoot");
4061
- dismissGitignorePrompt(repoRoot);
4062
- return c.json({ ok: true });
4063
- });
4064
- apiRoutes.post("/review/reopen", async (c) => {
4065
- const reviewId = resolveReviewId(c);
4066
- await updateReviewStatus(reviewId, "in_progress");
4067
- return c.json({ status: "in_progress" });
4240
+ const file = await getReviewFile(c.req.param("fileId"));
4241
+ if (!file) return c.json({ error: "Not found" }, 404);
4242
+ const diff = parseDiffData(file.diff_data);
4243
+ const isDeleted = diff?.status === "deleted";
4244
+ let content = "";
4245
+ try {
4246
+ content = isDeleted ? getFileContent(file.file_path, "HEAD", repoRoot) : getFileContent(file.file_path, "working", repoRoot);
4247
+ } catch {
4248
+ }
4249
+ if (!content) return c.json({ symbols: [] });
4250
+ const symbols = parseOutline(content, file.file_path);
4251
+ return c.json({ symbols });
4068
4252
  });
4069
- apiRoutes.post("/review/refresh", async (c) => {
4253
+ outlineRoutes.get("/symbol-definition", async (c) => {
4254
+ const name = c.req.query("name");
4255
+ const currentFileId = c.req.query("currentFileId");
4256
+ if (name === void 0 || name === "") return c.json({ definitions: [] });
4070
4257
  const reviewId = resolveReviewId(c);
4071
4258
  const repoRoot = c.get("repoRoot");
4072
- const review = await getReview(reviewId);
4073
- if (!review) return c.json({ error: "Review not found" }, 404);
4074
- const mode = parseModeString(review.mode);
4075
- const headCommit = getHeadCommit(repoRoot);
4076
- const diffs = getFileDiffs(mode, repoRoot);
4077
- const result = await updateReviewDiffs(reviewId, diffs, headCommit);
4078
- return c.json({
4079
- updated: result.updated,
4080
- added: result.added,
4081
- stale: result.stale,
4082
- fileCount: diffs.length
4083
- });
4084
- });
4085
- apiRoutes.delete("/review/:id", async (c) => {
4086
- const reviewId = c.req.param("id");
4087
- const currentReviewId = c.get("currentReviewId");
4088
- if (reviewId === currentReviewId) {
4089
- return c.json({ error: "Cannot delete the current review" }, 400);
4090
- }
4091
- const repoRoot = c.get("repoRoot");
4092
- deleteReviewExport(reviewId, repoRoot);
4093
- await deleteReview(reviewId);
4094
- return c.json({ ok: true });
4095
- });
4096
- apiRoutes.post("/reviews/delete-completed", async (c) => {
4097
- const currentReviewId = c.get("currentReviewId");
4098
- const repoRoot = c.get("repoRoot");
4099
- const reviews = await listReviews(repoRoot);
4100
- const toDelete = reviews.filter((r) => r.status === "completed" && r.id !== currentReviewId);
4101
- for (const r of toDelete) {
4102
- deleteReviewExport(r.id, repoRoot);
4103
- await deleteReview(r.id);
4104
- }
4105
- return c.json({ deleted: toDelete.length });
4106
- });
4107
- apiRoutes.post("/reviews/delete-all", async (c) => {
4108
- const currentReviewId = c.get("currentReviewId");
4109
- const repoRoot = c.get("repoRoot");
4110
- const reviews = await listReviews(repoRoot);
4111
- const toDelete = reviews.filter((r) => r.id !== currentReviewId);
4112
- for (const r of toDelete) {
4113
- deleteReviewExport(r.id, repoRoot);
4114
- await deleteReview(r.id);
4115
- }
4116
- return c.json({ deleted: toDelete.length });
4117
- });
4118
- apiRoutes.get("/files", async (c) => {
4119
- const reviewId = resolveReviewId(c);
4120
- const files = await getReviewFiles(reviewId);
4121
- const annotationCounts = {};
4122
- for (const file of files) {
4123
- const annotations = await getAnnotationsForFile(file.id);
4124
- annotationCounts[file.id] = annotations.length;
4125
- }
4126
- const staleCounts = await getStaleCountsForReview(reviewId);
4127
- return c.json({ files, annotationCounts, staleCounts });
4128
- });
4129
- apiRoutes.get("/files/:fileId", async (c) => {
4130
- const file = await getReviewFile(c.req.param("fileId"));
4131
- if (!file) return c.json({ error: "Not found" }, 404);
4132
- const annotations = await getAnnotationsForFile(file.id);
4133
- return c.json({ file, annotations });
4134
- });
4135
- apiRoutes.patch("/files/:fileId/status", async (c) => {
4136
- const { status } = await c.req.json();
4137
- if (!VALID_FILE_STATUSES.includes(status)) {
4138
- return c.json({ error: `status must be one of: ${VALID_FILE_STATUSES.join(", ")}` }, 400);
4139
- }
4140
- await updateFileStatus(c.req.param("fileId"), status);
4141
- return c.json({ ok: true });
4142
- });
4143
- apiRoutes.post("/files/:fileId/reveal", async (c) => {
4144
- const file = await getReviewFile(c.req.param("fileId"));
4145
- if (!file) return c.json({ error: "Not found" }, 404);
4146
- const repoRoot = c.get("repoRoot");
4147
- const fullPath = resolve4(repoRoot, file.file_path);
4148
- try {
4149
- if (process.platform === "darwin") {
4150
- execFileSync("open", ["-R", fullPath]);
4151
- } else if (process.platform === "win32") {
4152
- execFileSync("explorer", ["/select," + fullPath]);
4153
- } else {
4154
- execFileSync("xdg-open", [resolve4(fullPath, "..")]);
4155
- }
4156
- } catch {
4157
- }
4158
- return c.json({ ok: true });
4159
- });
4160
- function autoExport(c) {
4161
- scheduleAutoExport(c.get("reviewId"), c.get("repoRoot"));
4162
- }
4163
- apiRoutes.post("/annotations", async (c) => {
4164
- const body = await c.req.json();
4165
- if (typeof body.reviewFileId !== "string" || body.reviewFileId === "") {
4166
- return c.json({ error: "reviewFileId must be a non-empty string" }, 400);
4167
- }
4168
- if (typeof body.lineNumber !== "number" || !Number.isInteger(body.lineNumber) || body.lineNumber < 1) {
4169
- return c.json({ error: "lineNumber must be a positive integer" }, 400);
4170
- }
4171
- if (!VALID_SIDES.includes(body.side)) {
4172
- return c.json({ error: `side must be one of: ${VALID_SIDES.join(", ")}` }, 400);
4173
- }
4174
- if (!VALID_CATEGORIES.includes(body.category)) {
4175
- return c.json({ error: `category must be one of: ${VALID_CATEGORIES.join(", ")}` }, 400);
4176
- }
4177
- if (typeof body.content !== "string" || body.content.trim() === "") {
4178
- return c.json({ error: "content must be a non-empty string" }, 400);
4179
- }
4180
- const annotation = await addAnnotation(
4181
- body.reviewFileId,
4182
- body.lineNumber,
4183
- body.side,
4184
- body.category,
4185
- body.content
4186
- );
4187
- autoExport(c);
4188
- return c.json(annotation, 201);
4189
- });
4190
- apiRoutes.patch("/annotations/:id", async (c) => {
4191
- const { content, category } = await c.req.json();
4192
- if (typeof content !== "string" || content.trim() === "") {
4193
- return c.json({ error: "content must be a non-empty string" }, 400);
4194
- }
4195
- if (!VALID_CATEGORIES.includes(category)) {
4196
- return c.json({ error: `category must be one of: ${VALID_CATEGORIES.join(", ")}` }, 400);
4197
- }
4198
- await updateAnnotation(c.req.param("id"), content, category);
4199
- autoExport(c);
4200
- return c.json({ ok: true });
4201
- });
4202
- apiRoutes.delete("/annotations/:id", async (c) => {
4203
- await deleteAnnotation(c.req.param("id"));
4204
- autoExport(c);
4205
- return c.json({ ok: true });
4206
- });
4207
- apiRoutes.patch("/annotations/:id/move", async (c) => {
4208
- const { lineNumber, side } = await c.req.json();
4209
- if (typeof lineNumber !== "number" || !Number.isInteger(lineNumber) || lineNumber < 1) {
4210
- return c.json({ error: "lineNumber must be a positive integer" }, 400);
4211
- }
4212
- if (!VALID_SIDES.includes(side)) {
4213
- return c.json({ error: `side must be one of: ${VALID_SIDES.join(", ")}` }, 400);
4214
- }
4215
- await moveAnnotation(c.req.param("id"), lineNumber, side);
4216
- autoExport(c);
4217
- return c.json({ ok: true });
4218
- });
4219
- apiRoutes.post("/annotations/:id/keep", async (c) => {
4220
- await markAnnotationCurrent(c.req.param("id"));
4221
- autoExport(c);
4222
- return c.json({ ok: true });
4223
- });
4224
- apiRoutes.post("/annotations/stale/delete-all", async (c) => {
4225
- const reviewId = resolveReviewId(c);
4226
- await deleteStaleAnnotations(reviewId);
4227
- autoExport(c);
4228
- return c.json({ ok: true });
4229
- });
4230
- apiRoutes.post("/annotations/stale/keep-all", async (c) => {
4231
- const reviewId = resolveReviewId(c);
4232
- await keepAllStaleAnnotations(reviewId);
4233
- autoExport(c);
4234
- return c.json({ ok: true });
4235
- });
4236
- apiRoutes.get("/annotations/all", async (c) => {
4237
- const reviewId = resolveReviewId(c);
4238
- const annotations = await getAnnotationsForReview(reviewId);
4239
- return c.json(annotations);
4240
- });
4241
- apiRoutes.get("/outline/:fileId", async (c) => {
4242
- const repoRoot = c.get("repoRoot");
4243
- const file = await getReviewFile(c.req.param("fileId"));
4244
- if (!file) return c.json({ error: "Not found" }, 404);
4245
- const diff = JSON.parse(file.diff_data ?? "{}");
4246
- const isDeleted = diff.status === "deleted";
4247
- let content = "";
4248
- try {
4249
- if (isDeleted) {
4250
- content = getFileContent(file.file_path, "HEAD", repoRoot);
4251
- } else {
4252
- content = getFileContent(file.file_path, "working", repoRoot);
4253
- }
4254
- } catch {
4255
- }
4256
- if (!content) return c.json({ symbols: [] });
4257
- const symbols = parseOutline(content, file.file_path);
4258
- return c.json({ symbols });
4259
- });
4260
- apiRoutes.get("/symbol-definition", async (c) => {
4261
- const name = c.req.query("name");
4262
- const currentFileId = c.req.query("currentFileId");
4263
- if (name === void 0 || name === "") return c.json({ definitions: [] });
4264
- const reviewId = resolveReviewId(c);
4265
- const repoRoot = c.get("repoRoot");
4266
- const definitions = [];
4267
- const searchedPaths = /* @__PURE__ */ new Set();
4268
- const reviewFiles = await getReviewFiles(reviewId);
4269
- for (const file of reviewFiles) {
4270
- searchedPaths.add(file.file_path);
4271
- const diff = JSON.parse(file.diff_data ?? "{}");
4272
- const isDeleted = diff.status === "deleted";
4273
- let content = "";
4274
- try {
4275
- content = isDeleted ? getFileContent(file.file_path, "HEAD", repoRoot) : getFileContent(file.file_path, "working", repoRoot);
4276
- } catch {
4277
- continue;
4278
- }
4279
- if (!content) continue;
4280
- const symbols = parseOutline(content, file.file_path);
4281
- collectDefinitions(symbols, name, file.id, file.file_path, definitions);
4259
+ const definitions = [];
4260
+ const searchedPaths = /* @__PURE__ */ new Set();
4261
+ const reviewFiles = await getReviewFiles(reviewId);
4262
+ for (const file of reviewFiles) {
4263
+ searchedPaths.add(file.file_path);
4264
+ const diff = parseDiffData(file.diff_data);
4265
+ const isDeleted = diff?.status === "deleted";
4266
+ let content = "";
4267
+ try {
4268
+ content = isDeleted ? getFileContent(file.file_path, "HEAD", repoRoot) : getFileContent(file.file_path, "working", repoRoot);
4269
+ } catch {
4270
+ continue;
4271
+ }
4272
+ if (!content) continue;
4273
+ const symbols = parseOutline(content, file.file_path);
4274
+ collectDefinitions(symbols, name, file.id, file.file_path, definitions);
4282
4275
  }
4283
4276
  if (definitions.length === 0) {
4284
4277
  try {
@@ -4289,7 +4282,7 @@ apiRoutes.get("/symbol-definition", async (c) => {
4289
4282
  if (!/\.(js|mjs|cjs|jsx|ts|tsx|mts|cts|java|go|rs|c|h|cpp|cc|cxx|hpp|cs|swift|php|kt|kts|scala|dart|groovy|py|rb)$/i.test(ext)) continue;
4290
4283
  let content = "";
4291
4284
  try {
4292
- content = readFileSync8(resolve4(repoRoot, filePath), "utf-8");
4285
+ content = readFileSync8(resolve5(repoRoot, filePath), "utf-8");
4293
4286
  } catch {
4294
4287
  continue;
4295
4288
  }
@@ -4322,27 +4315,17 @@ function collectDefinitions(symbols, targetName, fileId, filePath, out) {
4322
4315
  }
4323
4316
  }
4324
4317
  }
4325
- apiRoutes.get("/context/:fileId", async (c) => {
4326
- const repoRoot = c.get("repoRoot");
4327
- const file = await getReviewFile(c.req.param("fileId"));
4328
- if (!file) return c.json({ error: "Not found" }, 404);
4329
- const startLine = parseInt(c.req.query("start") ?? "1", 10);
4330
- const endLine = parseInt(c.req.query("end") ?? "20", 10);
4331
- const content = getFileContent(file.file_path, "working", repoRoot);
4332
- const allLines = content.split("\n");
4333
- const clampedStart = Math.max(1, startLine);
4334
- const clampedEnd = Math.min(allLines.length, endLine);
4335
- const lines = [];
4336
- for (let i = clampedStart; i <= clampedEnd; i++) {
4337
- lines.push({ num: i, content: allLines[i - 1] || "" });
4338
- }
4339
- return c.json({ lines });
4340
- });
4318
+
4319
+ // src/routes/api/project-settings.ts
4320
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
4321
+ import { Hono as Hono9 } from "hono";
4322
+ import { join as join7 } from "path";
4323
+ var projectSettingsRoutes = new Hono9();
4341
4324
  function readProjectSettings(repoRoot) {
4342
4325
  const settingsPath = join7(repoRoot, ".glassbox", "settings.json");
4343
4326
  try {
4344
4327
  if (existsSync6(settingsPath)) {
4345
- return JSON.parse(readFileSync8(settingsPath, "utf-8"));
4328
+ return JSON.parse(readFileSync9(settingsPath, "utf-8"));
4346
4329
  }
4347
4330
  } catch {
4348
4331
  }
@@ -4353,11 +4336,11 @@ function writeProjectSettings(repoRoot, settings) {
4353
4336
  mkdirSync4(dir, { recursive: true });
4354
4337
  writeFileSync5(join7(dir, "settings.json"), JSON.stringify(settings, null, 2), "utf-8");
4355
4338
  }
4356
- apiRoutes.get("/project-settings", (c) => {
4339
+ projectSettingsRoutes.get("/project-settings", (c) => {
4357
4340
  const repoRoot = c.get("repoRoot");
4358
4341
  return c.json(readProjectSettings(repoRoot));
4359
4342
  });
4360
- apiRoutes.patch("/project-settings", async (c) => {
4343
+ projectSettingsRoutes.patch("/project-settings", async (c) => {
4361
4344
  const repoRoot = c.get("repoRoot");
4362
4345
  const body = await c.req.json();
4363
4346
  if (body.appName !== void 0 && typeof body.appName !== "string") {
@@ -4368,120 +4351,146 @@ apiRoutes.patch("/project-settings", async (c) => {
4368
4351
  writeProjectSettings(repoRoot, current);
4369
4352
  return c.json(current);
4370
4353
  });
4371
- apiRoutes.get("/image/:fileId/metadata", async (c) => {
4372
- const fileId = c.req.param("fileId");
4373
- const file = await getReviewFile(fileId);
4374
- if (!file) return c.json({ error: "Not found" }, 404);
4354
+
4355
+ // src/routes/api/reviews.ts
4356
+ init_queries();
4357
+ import { Hono as Hono10 } from "hono";
4358
+ var reviewsRoutes = new Hono10();
4359
+ reviewsRoutes.get("/reviews", async (c) => {
4375
4360
  const repoRoot = c.get("repoRoot");
4376
- const review = await getReview(file.review_id);
4361
+ const reviews = await listReviews(repoRoot);
4362
+ return c.json(reviews);
4363
+ });
4364
+ reviewsRoutes.get("/review", async (c) => {
4365
+ const reviewId = resolveReviewId(c);
4366
+ const review = await getReview(reviewId);
4367
+ return c.json(review);
4368
+ });
4369
+ reviewsRoutes.post("/review/complete", async (c) => {
4370
+ const reviewId = resolveReviewId(c);
4371
+ const currentReviewId = c.get("currentReviewId");
4372
+ const repoRoot = c.get("repoRoot");
4373
+ await updateReviewStatus(reviewId, "completed");
4374
+ const isCurrent = reviewId === currentReviewId;
4375
+ const exportPath = await generateReviewExport(reviewId, repoRoot, isCurrent);
4376
+ const gitignorePrompt = shouldPromptGitignore(repoRoot);
4377
+ return c.json({ status: "completed", exportPath, isCurrent, reviewId, gitignorePrompt });
4378
+ });
4379
+ reviewsRoutes.post("/gitignore/add", (c) => {
4380
+ const repoRoot = c.get("repoRoot");
4381
+ addGlassboxToGitignore(repoRoot);
4382
+ return c.json({ ok: true });
4383
+ });
4384
+ reviewsRoutes.post("/gitignore/dismiss", (c) => {
4385
+ const repoRoot = c.get("repoRoot");
4386
+ dismissGitignorePrompt(repoRoot);
4387
+ return c.json({ ok: true });
4388
+ });
4389
+ reviewsRoutes.post("/review/reopen", async (c) => {
4390
+ const reviewId = resolveReviewId(c);
4391
+ await updateReviewStatus(reviewId, "in_progress");
4392
+ return c.json({ status: "in_progress" });
4393
+ });
4394
+ reviewsRoutes.post("/review/refresh", async (c) => {
4395
+ const reviewId = resolveReviewId(c);
4396
+ const repoRoot = c.get("repoRoot");
4397
+ const review = await getReview(reviewId);
4377
4398
  if (!review) return c.json({ error: "Review not found" }, 404);
4378
4399
  const mode = parseModeString(review.mode);
4379
- const diff = JSON.parse(file.diff_data ?? "{}");
4380
- const oldPath = diff.oldPath ?? null;
4381
- const status = diff.status ?? "modified";
4382
- const oldImage = status !== "added" ? getOldImage(mode, file.file_path, oldPath, repoRoot) : null;
4383
- const newImage = status !== "deleted" ? getNewImage(mode, file.file_path, repoRoot) : null;
4384
- const oldMeta = oldImage !== null ? extractMetadata(oldImage.data, oldPath ?? file.file_path) : null;
4385
- const newMeta = newImage !== null ? extractMetadata(newImage.data, file.file_path) : null;
4400
+ const headCommit = getHeadCommit(repoRoot);
4401
+ const diffs = getFileDiffs(mode, repoRoot);
4402
+ const result = await updateReviewDiffs(reviewId, diffs, headCommit);
4386
4403
  return c.json({
4387
- old: oldMeta ? formatMetadataLines(oldMeta) : null,
4388
- new: newMeta ? formatMetadataLines(newMeta) : null
4404
+ updated: result.updated,
4405
+ added: result.added,
4406
+ stale: result.stale,
4407
+ fileCount: diffs.length
4389
4408
  });
4390
4409
  });
4391
- apiRoutes.get("/image/:fileId/:side", async (c) => {
4392
- const fileId = c.req.param("fileId");
4393
- const side = c.req.param("side");
4394
- if (side !== "old" && side !== "new") return c.text("Invalid side", 400);
4395
- const file = await getReviewFile(fileId);
4396
- if (!file) return c.text("Not found", 404);
4410
+ reviewsRoutes.delete("/review/:id", async (c) => {
4411
+ const reviewId = c.req.param("id");
4412
+ const currentReviewId = c.get("currentReviewId");
4413
+ if (reviewId === currentReviewId) {
4414
+ return c.json({ error: "Cannot delete the current review" }, 400);
4415
+ }
4397
4416
  const repoRoot = c.get("repoRoot");
4398
- const review = await getReview(file.review_id);
4399
- if (!review) return c.text("Review not found", 404);
4400
- const mode = parseModeString(review.mode);
4401
- const diff = JSON.parse(file.diff_data ?? "{}");
4402
- const oldPath = diff.oldPath ?? null;
4403
- const image = side === "old" ? getOldImage(mode, file.file_path, oldPath, repoRoot) : getNewImage(mode, file.file_path, repoRoot);
4404
- if (!image) return c.text("Image not available", 404);
4405
- if (isSvgFile(file.file_path)) {
4406
- try {
4407
- const png = await rasterizeSvg(image.data);
4408
- return new Response(new Uint8Array(png), {
4409
- headers: { "Content-Type": "image/png", "Cache-Control": "no-cache" }
4410
- });
4411
- } catch {
4412
- return c.text("SVG rasterization failed", 500);
4413
- }
4417
+ deleteReviewExport(reviewId, repoRoot);
4418
+ await deleteReview(reviewId);
4419
+ return c.json({ ok: true });
4420
+ });
4421
+ reviewsRoutes.post("/reviews/delete-completed", async (c) => {
4422
+ const currentReviewId = c.get("currentReviewId");
4423
+ const repoRoot = c.get("repoRoot");
4424
+ const reviews = await listReviews(repoRoot);
4425
+ const toDelete = reviews.filter((r) => r.status === "completed" && r.id !== currentReviewId);
4426
+ for (const r of toDelete) {
4427
+ deleteReviewExport(r.id, repoRoot);
4428
+ await deleteReview(r.id);
4414
4429
  }
4415
- const contentType = getContentType(file.file_path);
4416
- return new Response(new Uint8Array(image.data), {
4417
- headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
4418
- });
4430
+ return c.json({ deleted: toDelete.length });
4419
4431
  });
4420
- function readGlobalConfig() {
4421
- const configPath = join7(homedir3(), ".glassbox", "config.json");
4422
- try {
4423
- if (existsSync6(configPath)) {
4424
- return JSON.parse(readFileSync8(configPath, "utf-8"));
4425
- }
4426
- } catch {
4432
+ reviewsRoutes.post("/reviews/delete-all", async (c) => {
4433
+ const currentReviewId = c.get("currentReviewId");
4434
+ const repoRoot = c.get("repoRoot");
4435
+ const reviews = await listReviews(repoRoot);
4436
+ const toDelete = reviews.filter((r) => r.id !== currentReviewId);
4437
+ for (const r of toDelete) {
4438
+ deleteReviewExport(r.id, repoRoot);
4439
+ await deleteReview(r.id);
4427
4440
  }
4428
- return {};
4429
- }
4430
- function writeGlobalConfig(config) {
4431
- const configDir = join7(homedir3(), ".glassbox");
4432
- mkdirSync4(configDir, { recursive: true });
4433
- writeFileSync5(join7(configDir, "config.json"), JSON.stringify(config, null, 2), "utf-8");
4434
- }
4435
- apiRoutes.get("/share-prompt/state", (c) => {
4441
+ return c.json({ deleted: toDelete.length });
4442
+ });
4443
+
4444
+ // src/routes/api/share-prompt.ts
4445
+ import { Hono as Hono11 } from "hono";
4446
+ var sharePromptRoutes = new Hono11();
4447
+ sharePromptRoutes.get("/share-prompt/state", (c) => {
4436
4448
  const config = readGlobalConfig();
4437
4449
  const sp = config.sharePrompt;
4438
4450
  const dismissedAt = sp !== void 0 && typeof sp.dismissedAt === "number" ? sp.dismissedAt : null;
4439
4451
  const totalOpenMs = sp !== void 0 && typeof sp.totalOpenMs === "number" ? sp.totalOpenMs : 0;
4440
4452
  return c.json({ dismissedAt, totalOpenMs });
4441
4453
  });
4442
- apiRoutes.post("/share-prompt/dismiss", (c) => {
4443
- const config = readGlobalConfig();
4444
- if (config.sharePrompt === void 0) config.sharePrompt = {};
4445
- const sp = config.sharePrompt;
4446
- sp.dismissedAt = Date.now();
4447
- writeGlobalConfig(config);
4454
+ sharePromptRoutes.post("/share-prompt/dismiss", (c) => {
4455
+ updateGlobalConfig((config) => {
4456
+ if (config.sharePrompt === void 0) config.sharePrompt = {};
4457
+ config.sharePrompt.dismissedAt = Date.now();
4458
+ });
4448
4459
  return c.json({ ok: true });
4449
4460
  });
4450
- apiRoutes.post("/share-prompt/tick", async (c) => {
4461
+ sharePromptRoutes.post("/share-prompt/tick", async (c) => {
4451
4462
  const body = await c.req.json();
4452
- const config = readGlobalConfig();
4453
- if (config.sharePrompt === void 0) config.sharePrompt = {};
4454
- const sp = config.sharePrompt;
4455
- const current = typeof sp.totalOpenMs === "number" ? sp.totalOpenMs : 0;
4456
- sp.totalOpenMs = current + (body.sessionMs > 0 ? body.sessionMs : 0);
4457
- writeGlobalConfig(config);
4458
- return c.json({ totalOpenMs: sp.totalOpenMs });
4463
+ let totalOpenMs = 0;
4464
+ updateGlobalConfig((config) => {
4465
+ if (config.sharePrompt === void 0) config.sharePrompt = {};
4466
+ const sp = config.sharePrompt;
4467
+ const current = typeof sp.totalOpenMs === "number" ? sp.totalOpenMs : 0;
4468
+ const next = current + (body.sessionMs > 0 ? body.sessionMs : 0);
4469
+ sp.totalOpenMs = next;
4470
+ totalOpenMs = next;
4471
+ });
4472
+ return c.json({ totalOpenMs });
4459
4473
  });
4460
4474
 
4475
+ // src/routes/api.ts
4476
+ var apiRoutes = new Hono12();
4477
+ apiRoutes.route("/", reviewsRoutes);
4478
+ apiRoutes.route("/", filesRoutes);
4479
+ apiRoutes.route("/", annotationsRoutes);
4480
+ apiRoutes.route("/", outlineRoutes);
4481
+ apiRoutes.route("/", contextRoutes);
4482
+ apiRoutes.route("/", projectSettingsRoutes);
4483
+ apiRoutes.route("/", imageRoutes);
4484
+ apiRoutes.route("/", sharePromptRoutes);
4485
+
4461
4486
  // src/routes/channel-api.ts
4462
4487
  import { spawnSync as spawnSync8 } from "child_process";
4463
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
4464
- import { Hono as Hono5 } from "hono";
4465
- import { homedir as homedir4 } from "os";
4488
+ import { mkdirSync as mkdirSync5 } from "fs";
4489
+ import { Hono as Hono13 } from "hono";
4466
4490
  import { join as join8 } from "path";
4467
- var channelApiRoutes = new Hono5();
4468
- var CONFIG_DIR2 = join8(homedir4(), ".glassbox");
4469
- var CONFIG_PATH2 = join8(CONFIG_DIR2, "config.json");
4470
- function readGlobalConfig2() {
4471
- try {
4472
- if (existsSync7(CONFIG_PATH2)) {
4473
- return JSON.parse(readFileSync9(CONFIG_PATH2, "utf-8"));
4474
- }
4475
- } catch {
4476
- }
4477
- return {};
4478
- }
4479
- function writeGlobalConfig2(config) {
4480
- mkdirSync5(CONFIG_DIR2, { recursive: true });
4481
- writeFileSync6(CONFIG_PATH2, JSON.stringify(config, null, 2), "utf-8");
4482
- }
4491
+ var channelApiRoutes = new Hono13();
4483
4492
  channelApiRoutes.get("/status", async (c) => {
4484
- const config = readGlobalConfig2();
4493
+ const config = readGlobalConfig();
4485
4494
  const enabled = config.channelEnabled === true;
4486
4495
  const repoRoot = c.get("repoRoot");
4487
4496
  const dataDir = join8(repoRoot, ".glassbox");
@@ -4489,9 +4498,9 @@ channelApiRoutes.get("/status", async (c) => {
4489
4498
  return c.json({ enabled, connected });
4490
4499
  });
4491
4500
  channelApiRoutes.post("/enable", (c) => {
4492
- const config = readGlobalConfig2();
4493
- config.channelEnabled = true;
4494
- writeGlobalConfig2(config);
4501
+ updateGlobalConfig((config) => {
4502
+ config.channelEnabled = true;
4503
+ });
4495
4504
  const repoRoot = c.get("repoRoot");
4496
4505
  const dataDir = join8(repoRoot, ".glassbox");
4497
4506
  mkdirSync5(dataDir, { recursive: true });
@@ -4499,9 +4508,9 @@ channelApiRoutes.post("/enable", (c) => {
4499
4508
  return c.json({ ok: true });
4500
4509
  });
4501
4510
  channelApiRoutes.post("/disable", (c) => {
4502
- const config = readGlobalConfig2();
4503
- config.channelEnabled = false;
4504
- writeGlobalConfig2(config);
4511
+ updateGlobalConfig((config) => {
4512
+ config.channelEnabled = false;
4513
+ });
4505
4514
  const repoRoot = c.get("repoRoot");
4506
4515
  const dataDir = join8(repoRoot, ".glassbox");
4507
4516
  unregisterChannel(dataDir);
@@ -4509,6 +4518,9 @@ channelApiRoutes.post("/disable", (c) => {
4509
4518
  });
4510
4519
  channelApiRoutes.post("/trigger", async (c) => {
4511
4520
  const body = await c.req.json();
4521
+ if (!isNonEmptyString(body.message)) {
4522
+ return c.json({ error: "message must be a non-empty string" }, 400);
4523
+ }
4512
4524
  const repoRoot = c.get("repoRoot");
4513
4525
  const dataDir = join8(repoRoot, ".glassbox");
4514
4526
  const sent = await triggerChannel(dataDir, body.message);
@@ -4539,8 +4551,8 @@ channelApiRoutes.get("/claude-check", (c) => {
4539
4551
 
4540
4552
  // src/routes/pages.tsx
4541
4553
  import { readFileSync as readFileSync11 } from "fs";
4542
- import { Hono as Hono6 } from "hono";
4543
- import { resolve as resolve5 } from "path";
4554
+ import { Hono as Hono14 } from "hono";
4555
+ import { resolve as resolve6 } from "path";
4544
4556
 
4545
4557
  // src/utils/escapeHtml.ts
4546
4558
  function escapeHtml(str) {
@@ -5173,147 +5185,64 @@ function UnifiedDiff({ hunks, annotationsByLine }) {
5173
5185
  "data-new-start": hunk.newStart,
5174
5186
  "data-new-count": hunk.newCount,
5175
5187
  children: [
5176
- "@@ -",
5177
- hunk.oldStart,
5178
- ",",
5179
- hunk.oldCount,
5180
- " +",
5181
- hunk.newStart,
5182
- ",",
5183
- hunk.newCount,
5184
- " @@"
5185
- ]
5186
- }
5187
- ),
5188
- hunk.lines.map((line) => {
5189
- const lineNum = line.type === "remove" ? line.oldNum : line.newNum;
5190
- const side = line.type === "remove" ? "old" : "new";
5191
- const anns = annotationsByLine[`${lineNum}:${side}`] ?? [];
5192
- const segments = charDiffs.get(line);
5193
- return /* @__PURE__ */ jsx("div", { children: [
5194
- /* @__PURE__ */ jsx(
5195
- "div",
5196
- {
5197
- className: `diff-line ${line.type}${anns.length ? " has-annotation" : ""}`,
5198
- "data-line": lineNum,
5199
- "data-side": side,
5200
- children: [
5201
- /* @__PURE__ */ jsx("span", { className: "gutter-old", "data-line-number": line.oldNum ?? "" }),
5202
- /* @__PURE__ */ jsx("span", { className: "gutter-new", "data-line-number": line.newNum ?? "" }),
5203
- /* @__PURE__ */ jsx("span", { className: "code", children: segments ? renderSegments(segments) : line.content })
5204
- ]
5205
- }
5206
- ),
5207
- anns.length > 0 ? /* @__PURE__ */ jsx(AnnotationRows, { annotations: anns }) : null
5208
- ] });
5209
- })
5210
- ] });
5211
- }),
5212
- /* @__PURE__ */ jsx("div", { className: "hunk-separator hunk-expander-tail", "data-start": tailStart, children: "\u2195 Show remaining lines" })
5213
- ] });
5214
- }
5215
- function AnnotationRows({ annotations }) {
5216
- return /* @__PURE__ */ jsx("div", { className: "annotation-row", children: annotations.map((a) => /* @__PURE__ */ jsx(
5217
- "div",
5218
- {
5219
- className: `annotation-item${a.is_stale ? " annotation-stale" : ""}`,
5220
- "data-annotation-id": a.id,
5221
- "data-is-stale": a.is_stale ? "true" : void 0,
5222
- children: [
5223
- /* @__PURE__ */ jsx("span", { className: "annotation-drag-handle", draggable: "true", title: "Drag to move", children: "\u283F" }),
5224
- /* @__PURE__ */ jsx("span", { className: `annotation-category category-${a.category}`, "data-action": "reclassify", children: a.category }),
5225
- /* @__PURE__ */ jsx("span", { className: "annotation-text", children: a.content }),
5226
- /* @__PURE__ */ jsx("div", { className: "annotation-actions", children: [
5227
- a.is_stale ? /* @__PURE__ */ jsx("button", { className: "btn btn-xs btn-keep", "data-action": "keep", children: "Keep" }) : null,
5228
- /* @__PURE__ */ jsx("button", { className: "btn btn-xs btn-icon", "data-action": "edit", title: "Edit", children: /* @__PURE__ */ jsx(IconEdit, {}) }),
5229
- /* @__PURE__ */ jsx("button", { className: "btn btn-xs btn-icon btn-danger", "data-action": "delete", title: "Delete", children: /* @__PURE__ */ jsx(IconTrash, {}) })
5230
- ] })
5231
- ]
5232
- }
5233
- )) });
5234
- }
5235
-
5236
- // src/components/fileList.tsx
5237
- function buildFileTree(files) {
5238
- const root = { name: "", children: [], files: [] };
5239
- for (const f of files) {
5240
- const parts = f.file_path.split("/");
5241
- let node = root;
5242
- for (let i = 0; i < parts.length - 1; i++) {
5243
- let child = node.children.find((c) => c.name === parts[i]);
5244
- if (!child) {
5245
- child = { name: parts[i], children: [], files: [] };
5246
- node.children.push(child);
5247
- }
5248
- node = child;
5249
- }
5250
- node.files.push(f);
5251
- }
5252
- compressTree(root);
5253
- return root;
5254
- }
5255
- function compressTree(node) {
5256
- for (let i = 0; i < node.children.length; i++) {
5257
- let child = node.children[i];
5258
- while (child.children.length === 1 && child.files.length === 0) {
5259
- const grandchild = child.children[0];
5260
- child = { name: child.name + "/" + grandchild.name, children: grandchild.children, files: grandchild.files };
5261
- node.children[i] = child;
5262
- }
5263
- compressTree(child);
5264
- }
5265
- }
5266
- function countFiles(node) {
5267
- let count = node.files.length;
5268
- for (const child of node.children) count += countFiles(child);
5269
- return count;
5270
- }
5271
- function hasStale(node, staleCounts) {
5272
- for (const f of node.files) {
5273
- if (staleCounts[f.id]) return true;
5274
- }
5275
- for (const child of node.children) {
5276
- if (hasStale(child, staleCounts)) return true;
5277
- }
5278
- return false;
5279
- }
5280
- function TreeView({ node, depth, annotationCounts, staleCounts }) {
5281
- const sortedChildren = [...node.children].sort((a, b) => a.name.localeCompare(b.name));
5282
- return /* @__PURE__ */ jsx("div", { children: [
5283
- sortedChildren.map((child) => {
5284
- const total = countFiles(child);
5285
- const isCollapsible = total > 1;
5286
- const stale = hasStale(child, staleCounts);
5287
- return /* @__PURE__ */ jsx("div", { className: "folder-group", children: [
5288
- /* @__PURE__ */ jsx("div", { className: `folder-header${isCollapsible ? " collapsible" : ""}`, style: `padding-left:${16 + depth * 12}px`, children: [
5289
- isCollapsible ? /* @__PURE__ */ jsx("span", { className: "folder-arrow", children: "\u25BE" }) : /* @__PURE__ */ jsx("span", { className: "folder-arrow-spacer" }),
5290
- /* @__PURE__ */ jsx("span", { className: "folder-name", children: [
5291
- child.name,
5292
- "/"
5293
- ] }),
5294
- stale ? /* @__PURE__ */ jsx("span", { className: "stale-dot" }) : null
5295
- ] }),
5296
- /* @__PURE__ */ jsx("div", { className: "folder-content", children: /* @__PURE__ */ jsx(TreeView, { node: child, depth: depth + 1, annotationCounts, staleCounts }) })
5188
+ "@@ -",
5189
+ hunk.oldStart,
5190
+ ",",
5191
+ hunk.oldCount,
5192
+ " +",
5193
+ hunk.newStart,
5194
+ ",",
5195
+ hunk.newCount,
5196
+ " @@"
5197
+ ]
5198
+ }
5199
+ ),
5200
+ hunk.lines.map((line) => {
5201
+ const lineNum = line.type === "remove" ? line.oldNum : line.newNum;
5202
+ const side = line.type === "remove" ? "old" : "new";
5203
+ const anns = annotationsByLine[`${lineNum}:${side}`] ?? [];
5204
+ const segments = charDiffs.get(line);
5205
+ return /* @__PURE__ */ jsx("div", { children: [
5206
+ /* @__PURE__ */ jsx(
5207
+ "div",
5208
+ {
5209
+ className: `diff-line ${line.type}${anns.length ? " has-annotation" : ""}`,
5210
+ "data-line": lineNum,
5211
+ "data-side": side,
5212
+ children: [
5213
+ /* @__PURE__ */ jsx("span", { className: "gutter-old", "data-line-number": line.oldNum ?? "" }),
5214
+ /* @__PURE__ */ jsx("span", { className: "gutter-new", "data-line-number": line.newNum ?? "" }),
5215
+ /* @__PURE__ */ jsx("span", { className: "code", children: segments ? renderSegments(segments) : line.content })
5216
+ ]
5217
+ }
5218
+ ),
5219
+ anns.length > 0 ? /* @__PURE__ */ jsx(AnnotationRows, { annotations: anns }) : null
5220
+ ] });
5221
+ })
5297
5222
  ] });
5298
5223
  }),
5299
- node.files.map((f) => {
5300
- const diff = JSON.parse(f.diff_data ?? "{}");
5301
- const count = annotationCounts[f.id] || 0;
5302
- const stale = staleCounts[f.id] || 0;
5303
- const fileName = f.file_path.split("/").pop() ?? "";
5304
- return /* @__PURE__ */ jsx("div", { className: "file-item", "data-file-id": f.id, style: `padding-left:${16 + depth * 12}px`, children: [
5305
- /* @__PURE__ */ jsx("span", { className: `status-dot ${f.status}` }),
5306
- /* @__PURE__ */ jsx("span", { className: "file-name", title: f.file_path, children: fileName }),
5307
- /* @__PURE__ */ jsx("span", { className: `file-status ${diff.status ?? ""}`, children: diff.status ?? "" }),
5308
- stale > 0 ? /* @__PURE__ */ jsx("span", { className: "stale-dot" }) : null,
5309
- count > 0 ? /* @__PURE__ */ jsx("span", { className: "annotation-count", children: count }) : null
5310
- ] });
5311
- })
5224
+ /* @__PURE__ */ jsx("div", { className: "hunk-separator hunk-expander-tail", "data-start": tailStart, children: "\u2195 Show remaining lines" })
5312
5225
  ] });
5313
5226
  }
5314
- function FileList({ files, annotationCounts, staleCounts }) {
5315
- const tree = buildFileTree(files);
5316
- return /* @__PURE__ */ jsx("div", { className: "file-list", children: /* @__PURE__ */ jsx("div", { className: "file-list-items", children: /* @__PURE__ */ jsx(TreeView, { node: tree, depth: 0, annotationCounts, staleCounts }) }) });
5227
+ function AnnotationRows({ annotations }) {
5228
+ return /* @__PURE__ */ jsx("div", { className: "annotation-row", children: annotations.map((a) => /* @__PURE__ */ jsx(
5229
+ "div",
5230
+ {
5231
+ className: `annotation-item${a.is_stale ? " annotation-stale" : ""}`,
5232
+ "data-annotation-id": a.id,
5233
+ "data-is-stale": a.is_stale ? "true" : void 0,
5234
+ children: [
5235
+ /* @__PURE__ */ jsx("span", { className: "annotation-drag-handle", draggable: "true", title: "Drag to move", children: "\u283F" }),
5236
+ /* @__PURE__ */ jsx("span", { className: `annotation-category category-${a.category}`, "data-action": "reclassify", children: a.category }),
5237
+ /* @__PURE__ */ jsx("span", { className: "annotation-text", children: a.content }),
5238
+ /* @__PURE__ */ jsx("div", { className: "annotation-actions", children: [
5239
+ a.is_stale ? /* @__PURE__ */ jsx("button", { className: "btn btn-xs btn-keep", "data-action": "keep", children: "Keep" }) : null,
5240
+ /* @__PURE__ */ jsx("button", { className: "btn btn-xs btn-icon", "data-action": "edit", title: "Edit", children: /* @__PURE__ */ jsx(IconEdit, {}) }),
5241
+ /* @__PURE__ */ jsx("button", { className: "btn btn-xs btn-icon btn-danger", "data-action": "delete", title: "Delete", children: /* @__PURE__ */ jsx(IconTrash, {}) })
5242
+ ] })
5243
+ ]
5244
+ }
5245
+ )) });
5317
5246
  }
5318
5247
 
5319
5248
  // src/themes/built-in.ts
@@ -5786,39 +5715,23 @@ function themeToInlineStyle(colors) {
5786
5715
  }
5787
5716
 
5788
5717
  // src/themes/config.ts
5789
- import { existsSync as existsSync8, mkdirSync as mkdirSync6, readdirSync, readFileSync as readFileSync10, unlinkSync as unlinkSync2, writeFileSync as writeFileSync7 } from "fs";
5790
- import { homedir as homedir5 } from "os";
5718
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6, readdirSync, readFileSync as readFileSync10, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
5791
5719
  import { join as join9 } from "path";
5792
- var CONFIG_DIR3 = join9(homedir5(), ".glassbox");
5793
- var CONFIG_PATH3 = join9(CONFIG_DIR3, "config.json");
5794
- var THEMES_DIR = join9(CONFIG_DIR3, "themes");
5795
- function readConfigFile2() {
5796
- try {
5797
- if (existsSync8(CONFIG_PATH3)) {
5798
- return JSON.parse(readFileSync10(CONFIG_PATH3, "utf-8"));
5799
- }
5800
- } catch {
5801
- }
5802
- return {};
5803
- }
5804
- function writeConfigFile2(config) {
5805
- mkdirSync6(CONFIG_DIR3, { recursive: true });
5806
- writeFileSync7(CONFIG_PATH3, JSON.stringify(config, null, 2), "utf-8");
5807
- }
5720
+ var THEMES_DIR = join9(GLOBAL_CONFIG_DIR, "themes");
5808
5721
  function getActiveThemeId() {
5809
- const config = readConfigFile2();
5722
+ const config = readGlobalConfig();
5810
5723
  const theme = config.theme;
5811
5724
  const active = theme?.active;
5812
5725
  return active ?? DEFAULT_THEME_ID;
5813
5726
  }
5814
5727
  function setActiveThemeId(id) {
5815
- const config = readConfigFile2();
5816
- if (config.theme === void 0) config.theme = {};
5817
- config.theme.active = id;
5818
- writeConfigFile2(config);
5728
+ updateGlobalConfig((config) => {
5729
+ if (config.theme === void 0) config.theme = {};
5730
+ config.theme.active = id;
5731
+ });
5819
5732
  }
5820
5733
  function loadCustomThemes() {
5821
- if (!existsSync8(THEMES_DIR)) return [];
5734
+ if (!existsSync7(THEMES_DIR)) return [];
5822
5735
  const themes = [];
5823
5736
  try {
5824
5737
  const files = readdirSync(THEMES_DIR).filter((f) => f.endsWith(".json"));
@@ -5838,17 +5751,17 @@ function loadCustomThemes() {
5838
5751
  function saveCustomTheme(theme) {
5839
5752
  mkdirSync6(THEMES_DIR, { recursive: true });
5840
5753
  const filePath = join9(THEMES_DIR, `${theme.id}.json`);
5841
- writeFileSync7(filePath, JSON.stringify(theme, null, 2), "utf-8");
5754
+ writeFileSync6(filePath, JSON.stringify(theme, null, 2), "utf-8");
5842
5755
  }
5843
5756
  function deleteCustomTheme(id) {
5844
5757
  const filePath = join9(THEMES_DIR, `${id}.json`);
5845
- if (existsSync8(filePath)) {
5758
+ if (existsSync7(filePath)) {
5846
5759
  unlinkSync2(filePath);
5847
5760
  }
5848
5761
  }
5849
5762
  function getCustomTheme(id) {
5850
5763
  const filePath = join9(THEMES_DIR, `${id}.json`);
5851
- if (!existsSync8(filePath)) return void 0;
5764
+ if (!existsSync7(filePath)) return void 0;
5852
5765
  try {
5853
5766
  const data = JSON.parse(readFileSync10(filePath, "utf-8"));
5854
5767
  return { ...data, builtIn: false };
@@ -5958,20 +5871,92 @@ function ReviewHistory({ reviews, currentReviewId }) {
5958
5871
  ] });
5959
5872
  }
5960
5873
 
5961
- // src/routes/pages.tsx
5962
- init_queries();
5963
- var pageRoutes = new Hono6();
5964
- pageRoutes.get("/", async (c) => {
5965
- const reviewId = c.get("reviewId");
5966
- const review = await getReview(reviewId);
5967
- if (!review) return c.text("Review not found", 404);
5968
- const files = await getReviewFiles(reviewId);
5969
- const annotationCounts = {};
5874
+ // src/components/fileList.tsx
5875
+ function buildFileTree(files) {
5876
+ const root = { name: "", children: [], files: [] };
5970
5877
  for (const f of files) {
5971
- const anns = await getAnnotationsForFile(f.id);
5972
- annotationCounts[f.id] = anns.length;
5878
+ const parts = f.file_path.split("/");
5879
+ let node = root;
5880
+ for (let i = 0; i < parts.length - 1; i++) {
5881
+ let child = node.children.find((c) => c.name === parts[i]);
5882
+ if (!child) {
5883
+ child = { name: parts[i], children: [], files: [] };
5884
+ node.children.push(child);
5885
+ }
5886
+ node = child;
5887
+ }
5888
+ node.files.push(f);
5889
+ }
5890
+ compressTree(root);
5891
+ return root;
5892
+ }
5893
+ function compressTree(node) {
5894
+ for (let i = 0; i < node.children.length; i++) {
5895
+ let child = node.children[i];
5896
+ while (child.children.length === 1 && child.files.length === 0) {
5897
+ const grandchild = child.children[0];
5898
+ child = { name: child.name + "/" + grandchild.name, children: grandchild.children, files: grandchild.files };
5899
+ node.children[i] = child;
5900
+ }
5901
+ compressTree(child);
5902
+ }
5903
+ }
5904
+ function countFiles(node) {
5905
+ let count = node.files.length;
5906
+ for (const child of node.children) count += countFiles(child);
5907
+ return count;
5908
+ }
5909
+ function hasStale(node, staleCounts) {
5910
+ for (const f of node.files) {
5911
+ if (staleCounts[f.id]) return true;
5973
5912
  }
5974
- const html = /* @__PURE__ */ jsx(Layout, { title: `Glassbox - ${review.repo_name}`, reviewId, children: /* @__PURE__ */ jsx("div", { className: "review-app", "data-review-id": reviewId, children: [
5913
+ for (const child of node.children) {
5914
+ if (hasStale(child, staleCounts)) return true;
5915
+ }
5916
+ return false;
5917
+ }
5918
+ function TreeView({ node, depth, annotationCounts, staleCounts }) {
5919
+ const sortedChildren = [...node.children].sort((a, b) => a.name.localeCompare(b.name));
5920
+ return /* @__PURE__ */ jsx("div", { children: [
5921
+ sortedChildren.map((child) => {
5922
+ const total = countFiles(child);
5923
+ const isCollapsible = total > 1;
5924
+ const stale = hasStale(child, staleCounts);
5925
+ return /* @__PURE__ */ jsx("div", { className: "folder-group", children: [
5926
+ /* @__PURE__ */ jsx("div", { className: `folder-header${isCollapsible ? " collapsible" : ""}`, style: `padding-left:${16 + depth * 12}px`, children: [
5927
+ isCollapsible ? /* @__PURE__ */ jsx("span", { className: "folder-arrow", children: "\u25BE" }) : /* @__PURE__ */ jsx("span", { className: "folder-arrow-spacer" }),
5928
+ /* @__PURE__ */ jsx("span", { className: "folder-name", children: [
5929
+ child.name,
5930
+ "/"
5931
+ ] }),
5932
+ stale ? /* @__PURE__ */ jsx("span", { className: "stale-dot" }) : null
5933
+ ] }),
5934
+ /* @__PURE__ */ jsx("div", { className: "folder-content", children: /* @__PURE__ */ jsx(TreeView, { node: child, depth: depth + 1, annotationCounts, staleCounts }) })
5935
+ ] });
5936
+ }),
5937
+ node.files.map((f) => {
5938
+ const diff = parseDiffData(f.diff_data);
5939
+ const count = annotationCounts[f.id] || 0;
5940
+ const stale = staleCounts[f.id] || 0;
5941
+ const fileName = f.file_path.split("/").pop() ?? "";
5942
+ return /* @__PURE__ */ jsx("div", { className: "file-item", "data-file-id": f.id, style: `padding-left:${16 + depth * 12}px`, children: [
5943
+ /* @__PURE__ */ jsx("span", { className: `status-dot ${f.status}` }),
5944
+ /* @__PURE__ */ jsx("span", { className: "file-name", title: f.file_path, children: fileName }),
5945
+ /* @__PURE__ */ jsx("span", { className: `file-status ${diff?.status ?? ""}`, children: diff?.status ?? "" }),
5946
+ stale > 0 ? /* @__PURE__ */ jsx("span", { className: "stale-dot" }) : null,
5947
+ count > 0 ? /* @__PURE__ */ jsx("span", { className: "annotation-count", children: count }) : null
5948
+ ] });
5949
+ })
5950
+ ] });
5951
+ }
5952
+ function FileList({ files, annotationCounts, staleCounts }) {
5953
+ const tree = buildFileTree(files);
5954
+ return /* @__PURE__ */ jsx("div", { className: "file-list", children: /* @__PURE__ */ jsx("div", { className: "file-list-items", children: /* @__PURE__ */ jsx(TreeView, { node: tree, depth: 0, annotationCounts, staleCounts }) }) });
5955
+ }
5956
+
5957
+ // src/components/reviewShell.tsx
5958
+ function ReviewShell({ reviewId, review, files, annotationCounts, staleCounts, footer }) {
5959
+ return /* @__PURE__ */ jsx("div", { className: "review-app", "data-review-id": reviewId, children: [
5975
5960
  /* @__PURE__ */ jsx("div", { id: "update-banner", className: "update-banner", style: "display:none", children: [
5976
5961
  /* @__PURE__ */ jsx("span", { id: "update-banner-label", children: "Update available" }),
5977
5962
  /* @__PURE__ */ jsx("div", { className: "update-banner-actions", children: [
@@ -5989,12 +5974,9 @@ pageRoutes.get("/", async (c) => {
5989
5974
  ] })
5990
5975
  ] }),
5991
5976
  /* @__PURE__ */ jsx("div", { className: "file-filter", children: /* @__PURE__ */ jsx("input", { type: "text", className: "file-filter-input", id: "file-filter", placeholder: "Filter files..." }) }),
5992
- /* @__PURE__ */ jsx(FileList, { files, annotationCounts, staleCounts: {} }),
5977
+ /* @__PURE__ */ jsx(FileList, { files, annotationCounts, staleCounts }),
5993
5978
  /* @__PURE__ */ jsx("div", { className: "sidebar-share", id: "sidebar-share" }),
5994
- /* @__PURE__ */ jsx("div", { className: "sidebar-footer", children: [
5995
- /* @__PURE__ */ jsx("button", { className: "btn btn-primary btn-complete", id: "complete-review", children: "Complete Review" }),
5996
- /* @__PURE__ */ jsx("a", { href: "/history", className: "btn btn-sm btn-link", children: "Review History" })
5997
- ] })
5979
+ /* @__PURE__ */ jsx("div", { className: "sidebar-footer", children: footer })
5998
5980
  ] }),
5999
5981
  /* @__PURE__ */ jsx("div", { className: "sidebar-resize", id: "sidebar-resize" }),
6000
5982
  /* @__PURE__ */ jsx("main", { className: "main-content", children: [
@@ -6045,7 +6027,27 @@ pageRoutes.get("/", async (c) => {
6045
6027
  ] })
6046
6028
  ] })
6047
6029
  ] })
6048
- ] }) });
6030
+ ] });
6031
+ }
6032
+
6033
+ // src/routes/pages.tsx
6034
+ init_queries();
6035
+ var pageRoutes = new Hono14();
6036
+ pageRoutes.get("/", async (c) => {
6037
+ const reviewId = c.get("reviewId");
6038
+ const review = await getReview(reviewId);
6039
+ if (!review) return c.text("Review not found", 404);
6040
+ const files = await getReviewFiles(reviewId);
6041
+ const annotationCounts = {};
6042
+ for (const f of files) {
6043
+ const anns = await getAnnotationsForFile(f.id);
6044
+ annotationCounts[f.id] = anns.length;
6045
+ }
6046
+ const footer = /* @__PURE__ */ jsx(Fragment, { children: [
6047
+ /* @__PURE__ */ jsx("button", { className: "btn btn-primary btn-complete", id: "complete-review", children: "Complete Review" }),
6048
+ /* @__PURE__ */ jsx("a", { href: "/history", className: "btn btn-sm btn-link", children: "Review History" })
6049
+ ] });
6050
+ const html = /* @__PURE__ */ jsx(Layout, { title: `Glassbox - ${review.repo_name}`, reviewId, children: /* @__PURE__ */ jsx(ReviewShell, { reviewId, review, files, annotationCounts, staleCounts: {}, footer }) });
6049
6051
  return c.html(html.toString());
6050
6052
  });
6051
6053
  pageRoutes.get("/file/:fileId", async (c) => {
@@ -6055,7 +6057,7 @@ pageRoutes.get("/file/:fileId", async (c) => {
6055
6057
  const view = c.req.query("view");
6056
6058
  const file = await getReviewFile(fileId);
6057
6059
  if (!file) return c.text("File not found", 404);
6058
- const diff = JSON.parse(file.diff_data ?? "{}");
6060
+ const diff = parseDiffData(file.diff_data) ?? {};
6059
6061
  if (view === "rendered" && isSvgFile(file.file_path)) {
6060
6062
  const repoRoot = c.get("repoRoot");
6061
6063
  const review = await getReview(file.review_id);
@@ -6119,7 +6121,7 @@ pageRoutes.get("/file-raw", (c) => {
6119
6121
  const repoRoot = c.get("repoRoot");
6120
6122
  let content;
6121
6123
  try {
6122
- content = readFileSync11(resolve5(repoRoot, filePath), "utf-8");
6124
+ content = readFileSync11(resolve6(repoRoot, filePath), "utf-8");
6123
6125
  } catch {
6124
6126
  return c.text("File not found", 404);
6125
6127
  }
@@ -6160,82 +6162,12 @@ pageRoutes.get("/review/:reviewId", async (c) => {
6160
6162
  const anns = await getAnnotationsForFile(f.id);
6161
6163
  annotationCounts[f.id] = anns.length;
6162
6164
  }
6163
- const html = /* @__PURE__ */ jsx(Layout, { title: `Glassbox - ${review.repo_name}`, reviewId, children: /* @__PURE__ */ jsx("div", { className: "review-app", "data-review-id": reviewId, children: [
6164
- /* @__PURE__ */ jsx("div", { id: "update-banner", className: "update-banner", style: "display:none", children: [
6165
- /* @__PURE__ */ jsx("span", { id: "update-banner-label", children: "Update available" }),
6166
- /* @__PURE__ */ jsx("div", { className: "update-banner-actions", children: [
6167
- /* @__PURE__ */ jsx("button", { id: "update-install-btn", className: "btn btn-sm btn-accent", children: "Install Update" }),
6168
- /* @__PURE__ */ jsx("button", { id: "update-banner-dismiss", className: "btn btn-sm", children: "Later" })
6169
- ] })
6170
- ] }),
6171
- /* @__PURE__ */ jsx("div", { className: "review-body", children: [
6172
- /* @__PURE__ */ jsx("aside", { className: "sidebar", children: [
6173
- /* @__PURE__ */ jsx("div", { className: "sidebar-header", children: [
6174
- /* @__PURE__ */ jsx("h2", { children: review.repo_name }),
6175
- /* @__PURE__ */ jsx("span", { className: "review-mode", children: [
6176
- review.mode,
6177
- review.mode_args !== null && review.mode_args !== "" ? `: ${review.mode_args}` : ""
6178
- ] })
6179
- ] }),
6180
- /* @__PURE__ */ jsx("div", { className: "file-filter", children: /* @__PURE__ */ jsx("input", { type: "text", className: "file-filter-input", id: "file-filter", placeholder: "Filter files..." }) }),
6181
- /* @__PURE__ */ jsx(FileList, { files, annotationCounts, staleCounts: {} }),
6182
- /* @__PURE__ */ jsx("div", { className: "sidebar-share", id: "sidebar-share" }),
6183
- /* @__PURE__ */ jsx("div", { className: "sidebar-footer", children: [
6184
- review.status === "completed" ? /* @__PURE__ */ jsx("button", { className: "btn btn-primary", id: "reopen-review", children: "Reopen Review" }) : /* @__PURE__ */ jsx("button", { className: "btn btn-primary btn-complete", id: "complete-review", children: "Complete Review" }),
6185
- /* @__PURE__ */ jsx("a", { href: "/history", className: "btn btn-sm btn-link", children: "Review History" }),
6186
- /* @__PURE__ */ jsx("a", { href: "/", className: "btn btn-sm btn-link", children: "Back to current review" })
6187
- ] })
6188
- ] }),
6189
- /* @__PURE__ */ jsx("div", { className: "sidebar-resize", id: "sidebar-resize" }),
6190
- /* @__PURE__ */ jsx("main", { className: "main-content", children: [
6191
- /* @__PURE__ */ jsx("div", { className: "welcome-message", children: [
6192
- /* @__PURE__ */ jsx("h3", { children: "Select a file to begin reviewing" }),
6193
- /* @__PURE__ */ jsx("p", { children: [
6194
- files.length,
6195
- " file(s) to review"
6196
- ] }),
6197
- /* @__PURE__ */ jsx("p", { className: "progress-summary", id: "progress-summary" })
6198
- ] }),
6199
- /* @__PURE__ */ jsx("div", { className: "diff-nav-bar", id: "diff-nav-bar", style: "display:none", children: [
6200
- /* @__PURE__ */ jsx("button", { className: "nav-btn disabled", id: "nav-back-btn", disabled: true, title: "Back", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "m15 18-6-6 6-6" }) }) }),
6201
- /* @__PURE__ */ jsx("button", { className: "nav-btn disabled", id: "nav-forward-btn", disabled: true, title: "Forward", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" }) }) }),
6202
- /* @__PURE__ */ jsx("span", { className: "nav-file-path", id: "nav-file-path" })
6203
- ] }),
6204
- /* @__PURE__ */ jsx("div", { className: "diff-container", id: "diff-container", style: "display:none" }),
6205
- /* @__PURE__ */ jsx("div", { className: "diff-toolbar", id: "diff-toolbar", style: "display:none", children: [
6206
- /* @__PURE__ */ jsx("div", { className: "diff-toolbar-svg-toggle", style: "display:none", children: /* @__PURE__ */ jsx("div", { className: "segmented-control", children: [
6207
- /* @__PURE__ */ jsx("button", { className: "segment active", "data-svg-mode": "code", children: "Code" }),
6208
- /* @__PURE__ */ jsx("button", { className: "segment", "data-svg-mode": "rendered", children: "Rendered" })
6209
- ] }) }),
6210
- /* @__PURE__ */ jsx("div", { className: "diff-toolbar-text", children: [
6211
- /* @__PURE__ */ jsx("div", { className: "diff-toolbar-left", children: [
6212
- /* @__PURE__ */ jsx("div", { className: "segmented-control", children: [
6213
- /* @__PURE__ */ jsx("button", { className: "segment active", "data-diff-mode": "split", children: "Split" }),
6214
- /* @__PURE__ */ jsx("button", { className: "segment", "data-diff-mode": "unified", children: "Unified" })
6215
- ] }),
6216
- /* @__PURE__ */ jsx("button", { className: "toolbar-btn", id: "wrap-toggle", children: "Wrap" }),
6217
- /* @__PURE__ */ jsx("button", { className: "toolbar-btn", id: "whitespace-toggle", children: "Ignore Whitespace" })
6218
- ] }),
6219
- /* @__PURE__ */ jsx("div", { className: "diff-toolbar-right", children: /* @__PURE__ */ jsx("button", { className: "toolbar-btn", id: "language-btn", children: "Plain Text" }) })
6220
- ] }),
6221
- /* @__PURE__ */ jsx("div", { className: "diff-toolbar-image", style: "display:none", children: [
6222
- /* @__PURE__ */ jsx("div", { className: "diff-toolbar-left", children: /* @__PURE__ */ jsx("div", { className: "segmented-control", children: [
6223
- /* @__PURE__ */ jsx("button", { className: "segment active", "data-image-mode": "metadata", children: "Metadata" }),
6224
- /* @__PURE__ */ jsx("button", { className: "segment", "data-image-mode": "difference", children: "Difference" }),
6225
- /* @__PURE__ */ jsx("button", { className: "segment", "data-image-mode": "slice", children: "Slice" }),
6226
- /* @__PURE__ */ jsx("button", { className: "segment", "data-image-mode": "image", style: "display:none", children: "Image" })
6227
- ] }) }),
6228
- /* @__PURE__ */ jsx("div", { className: "diff-toolbar-right", children: [
6229
- /* @__PURE__ */ jsx("button", { className: "image-zoom-btn", "data-zoom-action": "out", title: "Zoom out", children: /* @__PURE__ */ jsx(IconZoomOut, {}) }),
6230
- /* @__PURE__ */ jsx("button", { className: "image-zoom-btn", "data-zoom-action": "fit", title: "Fit to view", children: /* @__PURE__ */ jsx(IconFit, {}) }),
6231
- /* @__PURE__ */ jsx("button", { className: "image-zoom-btn", "data-zoom-action": "actual", title: "Actual size (1:1)", children: /* @__PURE__ */ jsx(IconActualSize, {}) }),
6232
- /* @__PURE__ */ jsx("button", { className: "image-zoom-btn", "data-zoom-action": "in", title: "Zoom in", children: /* @__PURE__ */ jsx(IconZoomIn, {}) })
6233
- ] })
6234
- ] })
6235
- ] })
6236
- ] })
6237
- ] })
6238
- ] }) });
6165
+ const footer = /* @__PURE__ */ jsx(Fragment, { children: [
6166
+ review.status === "completed" ? /* @__PURE__ */ jsx("button", { className: "btn btn-primary", id: "reopen-review", children: "Reopen Review" }) : /* @__PURE__ */ jsx("button", { className: "btn btn-primary btn-complete", id: "complete-review", children: "Complete Review" }),
6167
+ /* @__PURE__ */ jsx("a", { href: "/history", className: "btn btn-sm btn-link", children: "Review History" }),
6168
+ /* @__PURE__ */ jsx("a", { href: "/", className: "btn btn-sm btn-link", children: "Back to current review" })
6169
+ ] });
6170
+ const html = /* @__PURE__ */ jsx(Layout, { title: `Glassbox - ${review.repo_name}`, reviewId, children: /* @__PURE__ */ jsx(ReviewShell, { reviewId, review, files, annotationCounts, staleCounts: {}, footer }) });
6239
6171
  return c.html(html.toString());
6240
6172
  });
6241
6173
  pageRoutes.get("/history", async (c) => {
@@ -6247,8 +6179,8 @@ pageRoutes.get("/history", async (c) => {
6247
6179
  });
6248
6180
 
6249
6181
  // src/routes/theme-api.ts
6250
- import { Hono as Hono7 } from "hono";
6251
- var themeApiRoutes = new Hono7();
6182
+ import { Hono as Hono15 } from "hono";
6183
+ var themeApiRoutes = new Hono15();
6252
6184
  function validateColors(colors) {
6253
6185
  if (typeof colors !== "object" || colors === null || Array.isArray(colors)) {
6254
6186
  return "colors must be an object";
@@ -6284,7 +6216,7 @@ themeApiRoutes.get("/active", (c) => {
6284
6216
  });
6285
6217
  themeApiRoutes.post("/active", async (c) => {
6286
6218
  const body = await c.req.json();
6287
- if (typeof body.id !== "string" || body.id === "") return c.json({ error: "id must be a non-empty string" }, 400);
6219
+ if (!isNonEmptyString(body.id)) return c.json({ error: "id must be a non-empty string" }, 400);
6288
6220
  const theme = resolveTheme(body.id);
6289
6221
  if (!theme) return c.json({ error: "Theme not found" }, 404);
6290
6222
  setActiveThemeId(body.id);
@@ -6292,8 +6224,8 @@ themeApiRoutes.post("/active", async (c) => {
6292
6224
  });
6293
6225
  themeApiRoutes.post("/", async (c) => {
6294
6226
  const body = await c.req.json();
6295
- if (typeof body.sourceId !== "string" || body.sourceId === "") return c.json({ error: "sourceId must be a non-empty string" }, 400);
6296
- if (body.name !== void 0 && (typeof body.name !== "string" || body.name.trim() === "")) {
6227
+ if (!isNonEmptyString(body.sourceId)) return c.json({ error: "sourceId must be a non-empty string" }, 400);
6228
+ if (body.name !== void 0 && !isNonEmptyString(body.name)) {
6297
6229
  return c.json({ error: "name must be a non-empty string when provided" }, 400);
6298
6230
  }
6299
6231
  const source = resolveTheme(body.sourceId);
@@ -6313,7 +6245,7 @@ themeApiRoutes.post("/", async (c) => {
6313
6245
  themeApiRoutes.post("/:id/edit", async (c) => {
6314
6246
  const id = c.req.param("id");
6315
6247
  const body = await c.req.json();
6316
- if (body.name !== void 0 && (typeof body.name !== "string" || body.name.trim() === "")) {
6248
+ if (body.name !== void 0 && !isNonEmptyString(body.name)) {
6317
6249
  return c.json({ error: "name must be a non-empty string when provided" }, 400);
6318
6250
  }
6319
6251
  if (body.colors !== void 0) {
@@ -6353,7 +6285,7 @@ themeApiRoutes.patch("/:id", async (c) => {
6353
6285
  return c.json({ error: "Theme not found" }, 404);
6354
6286
  }
6355
6287
  const body = await c.req.json();
6356
- if (body.name !== void 0 && (typeof body.name !== "string" || body.name.trim() === "")) {
6288
+ if (body.name !== void 0 && !isNonEmptyString(body.name)) {
6357
6289
  return c.json({ error: "name must be a non-empty string when provided" }, 400);
6358
6290
  }
6359
6291
  if (body.colors !== void 0) {
@@ -6382,10 +6314,10 @@ themeApiRoutes.delete("/:id", (c) => {
6382
6314
 
6383
6315
  // src/server.ts
6384
6316
  function tryServe(appFetch, port) {
6385
- return new Promise((resolve7, reject) => {
6317
+ return new Promise((resolve8, reject) => {
6386
6318
  const server = serve({ fetch: appFetch, port, hostname: "127.0.0.1" });
6387
6319
  server.on("listening", () => {
6388
- resolve7(port);
6320
+ resolve8(port);
6389
6321
  });
6390
6322
  server.on("error", (err) => {
6391
6323
  if (err.code === "EADDRINUSE") {
@@ -6397,7 +6329,7 @@ function tryServe(appFetch, port) {
6397
6329
  });
6398
6330
  }
6399
6331
  async function startServer(port, reviewId, repoRoot, options) {
6400
- const app = new Hono8();
6332
+ const app = new Hono16();
6401
6333
  app.use("*", async (c, next) => {
6402
6334
  c.set("reviewId", reviewId);
6403
6335
  c.set("currentReviewId", reviewId);
@@ -6405,7 +6337,7 @@ async function startServer(port, reviewId, repoRoot, options) {
6405
6337
  await next();
6406
6338
  });
6407
6339
  const selfDir = dirname2(fileURLToPath2(import.meta.url));
6408
- const distDir = existsSync9(join10(selfDir, "client", "styles.css")) ? join10(selfDir, "client") : join10(selfDir, "..", "dist", "client");
6340
+ const distDir = existsSync8(join10(selfDir, "client", "styles.css")) ? join10(selfDir, "client") : join10(selfDir, "..", "dist", "client");
6409
6341
  app.get("/static/styles.css", (c) => {
6410
6342
  const css = readFileSync12(join10(distDir, "styles.css"), "utf-8");
6411
6343
  return c.text(css, 200, { "Content-Type": "text/css", "Cache-Control": "no-cache" });
@@ -6447,13 +6379,10 @@ async function startServer(port, reviewId, repoRoot, options) {
6447
6379
  Glassbox running at ${url}
6448
6380
  `);
6449
6381
  try {
6450
- const homePath = join10(homedir6(), ".glassbox", "config.json");
6451
- if (existsSync9(homePath)) {
6452
- const globalConfig = JSON.parse(readFileSync12(homePath, "utf-8"));
6453
- if (globalConfig.channelEnabled === true) {
6454
- const dataDir = join10(repoRoot, ".glassbox");
6455
- registerChannel(dataDir);
6456
- }
6382
+ const globalConfig = readGlobalConfig();
6383
+ if (globalConfig.channelEnabled === true) {
6384
+ const dataDir = join10(repoRoot, ".glassbox");
6385
+ registerChannel(dataDir);
6457
6386
  }
6458
6387
  } catch {
6459
6388
  }
@@ -6464,7 +6393,7 @@ async function startServer(port, reviewId, repoRoot, options) {
6464
6393
  }
6465
6394
 
6466
6395
  // src/skills.ts
6467
- import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
6396
+ import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "fs";
6468
6397
  import { join as join11 } from "path";
6469
6398
  var SKILL_VERSION = 1;
6470
6399
  function versionHeader() {
@@ -6476,14 +6405,14 @@ function parseVersionHeader(content) {
6476
6405
  return parseInt(match[1], 10);
6477
6406
  }
6478
6407
  function updateFile(path, content) {
6479
- if (existsSync10(path)) {
6408
+ if (existsSync9(path)) {
6480
6409
  const existing = readFileSync13(path, "utf-8");
6481
6410
  const version = parseVersionHeader(existing);
6482
6411
  if (version !== null && version >= SKILL_VERSION) {
6483
6412
  return false;
6484
6413
  }
6485
6414
  }
6486
- writeFileSync8(path, content, "utf-8");
6415
+ writeFileSync7(path, content, "utf-8");
6487
6416
  return true;
6488
6417
  }
6489
6418
  function skillBody() {
@@ -6565,28 +6494,28 @@ function ensureWindsurfRules(cwd) {
6565
6494
  function ensureSkills() {
6566
6495
  const cwd = process.cwd();
6567
6496
  const platforms = [];
6568
- if (existsSync10(join11(cwd, ".claude"))) {
6497
+ if (existsSync9(join11(cwd, ".claude"))) {
6569
6498
  if (ensureClaudeSkills(cwd)) platforms.push("Claude Code");
6570
6499
  }
6571
- if (existsSync10(join11(cwd, ".cursor"))) {
6500
+ if (existsSync9(join11(cwd, ".cursor"))) {
6572
6501
  if (ensureCursorRules(cwd)) platforms.push("Cursor");
6573
6502
  }
6574
- if (existsSync10(join11(cwd, ".github", "prompts")) || existsSync10(join11(cwd, ".github", "copilot-instructions.md"))) {
6503
+ if (existsSync9(join11(cwd, ".github", "prompts")) || existsSync9(join11(cwd, ".github", "copilot-instructions.md"))) {
6575
6504
  if (ensureCopilotPrompts(cwd)) platforms.push("GitHub Copilot");
6576
6505
  }
6577
- if (existsSync10(join11(cwd, ".windsurf"))) {
6506
+ if (existsSync9(join11(cwd, ".windsurf"))) {
6578
6507
  if (ensureWindsurfRules(cwd)) platforms.push("Windsurf");
6579
6508
  }
6580
6509
  return platforms;
6581
6510
  }
6582
6511
 
6583
6512
  // src/update-check.ts
6584
- import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
6513
+ import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "fs";
6585
6514
  import { get } from "https";
6586
- import { homedir as homedir7 } from "os";
6515
+ import { homedir as homedir3 } from "os";
6587
6516
  import { dirname as dirname3, join as join12 } from "path";
6588
6517
  import { fileURLToPath as fileURLToPath3 } from "url";
6589
- var DATA_DIR = join12(homedir7(), ".glassbox");
6518
+ var DATA_DIR = join12(homedir3(), ".glassbox");
6590
6519
  var CHECK_FILE = join12(DATA_DIR, "last-update-check");
6591
6520
  var PACKAGE_NAME = "glassbox";
6592
6521
  function getCurrentVersion() {
@@ -6600,7 +6529,7 @@ function getCurrentVersion() {
6600
6529
  }
6601
6530
  function getLastCheckDate() {
6602
6531
  try {
6603
- if (existsSync11(CHECK_FILE)) {
6532
+ if (existsSync10(CHECK_FILE)) {
6604
6533
  return readFileSync14(CHECK_FILE, "utf-8").trim();
6605
6534
  }
6606
6535
  } catch {
@@ -6609,7 +6538,7 @@ function getLastCheckDate() {
6609
6538
  }
6610
6539
  function saveCheckDate() {
6611
6540
  mkdirSync8(DATA_DIR, { recursive: true });
6612
- writeFileSync9(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
6541
+ writeFileSync8(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
6613
6542
  }
6614
6543
  function isFirstUseToday() {
6615
6544
  const last = getLastCheckDate();
@@ -6618,10 +6547,10 @@ function isFirstUseToday() {
6618
6547
  return last !== today;
6619
6548
  }
6620
6549
  function fetchLatestVersion() {
6621
- return new Promise((resolve7) => {
6550
+ return new Promise((resolve8) => {
6622
6551
  const req = get(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { timeout: 5e3 }, (res) => {
6623
6552
  if (res.statusCode !== 200) {
6624
- resolve7(null);
6553
+ resolve8(null);
6625
6554
  return;
6626
6555
  }
6627
6556
  let data = "";
@@ -6630,18 +6559,18 @@ function fetchLatestVersion() {
6630
6559
  });
6631
6560
  res.on("end", () => {
6632
6561
  try {
6633
- resolve7(JSON.parse(data).version);
6562
+ resolve8(JSON.parse(data).version);
6634
6563
  } catch {
6635
- resolve7(null);
6564
+ resolve8(null);
6636
6565
  }
6637
6566
  });
6638
6567
  });
6639
6568
  req.on("error", () => {
6640
- resolve7(null);
6569
+ resolve8(null);
6641
6570
  });
6642
6571
  req.on("timeout", () => {
6643
6572
  req.destroy();
6644
- resolve7(null);
6573
+ resolve8(null);
6645
6574
  });
6646
6575
  });
6647
6576
  }
@@ -6779,7 +6708,7 @@ function parseArgs(argv) {
6779
6708
  port = parseInt(args[++i], 10);
6780
6709
  break;
6781
6710
  case "--data-dir":
6782
- dataDir = resolve6(args[++i]);
6711
+ dataDir = resolve7(args[++i]);
6783
6712
  break;
6784
6713
  case "--resume":
6785
6714
  resume = true;
@@ -6835,7 +6764,7 @@ async function main() {
6835
6764
  console.log("AI service test mode enabled \u2014 using mock AI responses");
6836
6765
  }
6837
6766
  if (debug) {
6838
- console.log(`[debug] Build timestamp: ${"2026-04-08T04:46:44.646Z"}`);
6767
+ console.log(`[debug] Build timestamp: ${"2026-05-03T02:47:39.743Z"}`);
6839
6768
  }
6840
6769
  if (projectDir !== null) {
6841
6770
  process.chdir(projectDir);