glassbox 0.13.3 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -15851,7 +15851,7 @@ __export(difftool_exports, {
15851
15851
  unregisterDifftool: () => unregisterDifftool
15852
15852
  });
15853
15853
  import { spawnSync as spawnSync9 } from "child_process";
15854
- function git3(args, cwd) {
15854
+ function git2(args, cwd) {
15855
15855
  const r = spawnSync9("git", args, { encoding: "utf-8", cwd });
15856
15856
  if (r.error !== void 0) {
15857
15857
  const code = r.error.code ?? "UNKNOWN";
@@ -15860,7 +15860,7 @@ function git3(args, cwd) {
15860
15860
  return { status: r.status ?? -1, stdout: (r.stdout || "").trim(), stderr: (r.stderr || "").trim() };
15861
15861
  }
15862
15862
  function readConfig(key, scope, cwd) {
15863
- const r = git3(["config", `--${scope}`, "--get", key], cwd);
15863
+ const r = git2(["config", `--${scope}`, "--get", key], cwd);
15864
15864
  return r.status === 0 ? r.stdout : null;
15865
15865
  }
15866
15866
  function getDifftoolStatus(scope = "global", cwd) {
@@ -15884,7 +15884,7 @@ function registerDifftool(opts = {}) {
15884
15884
  ["difftool.prompt", "false"]
15885
15885
  ];
15886
15886
  for (const [key, value] of sets) {
15887
- const r = git3(["config", `--${scope}`, key, value], opts.cwd);
15887
+ const r = git2(["config", `--${scope}`, key, value], opts.cwd);
15888
15888
  if (r.status !== 0) {
15889
15889
  const detail = r.stderr || `(no stderr; exit ${String(r.status)})`;
15890
15890
  return { ok: false, reason: "git-failed", message: `\`git config --${scope} ${key}\` failed: ${detail}` };
@@ -15899,7 +15899,7 @@ function unregisterDifftool(opts = {}) {
15899
15899
  return { ok: true, removed: false };
15900
15900
  }
15901
15901
  const tryUnset = (key) => {
15902
- const r = git3(["config", `--${scope}`, "--unset", key], opts.cwd);
15902
+ const r = git2(["config", `--${scope}`, "--unset", key], opts.cwd);
15903
15903
  if (r.status !== 0 && r.status !== 5) {
15904
15904
  console.error(`git config --unset ${key} failed: ${r.stderr}`);
15905
15905
  }
@@ -16195,8 +16195,9 @@ var PLATFORMS = {
16195
16195
  };
16196
16196
  var MODELS = {
16197
16197
  anthropic: [
16198
- { id: "claude-sonnet-4-20250514", name: "Claude Sonnet 4", contextWindow: 2e5, isDefault: true },
16199
- { id: "claude-haiku-4-20250514", name: "Claude Haiku 4", contextWindow: 2e5, isDefault: false }
16198
+ { id: "claude-opus-4-8", name: "Claude Opus 4.8", contextWindow: 1e6, isDefault: false },
16199
+ { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", contextWindow: 1e6, isDefault: true },
16200
+ { id: "claude-haiku-4-5", name: "Claude Haiku 4.5", contextWindow: 2e5, isDefault: false }
16200
16201
  ],
16201
16202
  openai: [
16202
16203
  { id: "gpt-4o", name: "GPT-4o", contextWindow: 128e3, isDefault: true },
@@ -16221,6 +16222,28 @@ function getModelContextWindow(platform, modelId) {
16221
16222
  const model = MODELS[platform].find((m) => m.id === modelId);
16222
16223
  return model ? model.contextWindow : 128e3;
16223
16224
  }
16225
+ function modelFamily(id) {
16226
+ const lower = id.toLowerCase();
16227
+ for (const tier of ["opus", "sonnet", "haiku"]) {
16228
+ if (lower.includes(tier)) return `anthropic:${tier}`;
16229
+ }
16230
+ if (lower.includes("gemini")) {
16231
+ if (lower.includes("flash")) return "google:flash";
16232
+ if (lower.includes("pro")) return "google:pro";
16233
+ return "google:gemini";
16234
+ }
16235
+ if (lower.includes("gpt") || lower.startsWith("o1") || lower.startsWith("o3")) {
16236
+ return lower.includes("mini") ? "openai:mini" : "openai:gpt";
16237
+ }
16238
+ return lower;
16239
+ }
16240
+ function resolveModelId(platform, modelId) {
16241
+ const models = MODELS[platform];
16242
+ if (models.some((m) => m.id === modelId)) return modelId;
16243
+ const family = modelFamily(modelId);
16244
+ const familyMatch = models.find((m) => modelFamily(m.id) === family);
16245
+ return familyMatch ? familyMatch.id : getDefaultModel(platform);
16246
+ }
16224
16247
 
16225
16248
  // src/ai/api-keys.ts
16226
16249
  function getKeyFromEnv(platform) {
@@ -16317,7 +16340,7 @@ function loadAIConfig() {
16317
16340
  const config2 = readConfigFile();
16318
16341
  const platformRaw = config2.ai?.platform ?? "anthropic";
16319
16342
  const platform = AIPlatformSchema.safeParse(platformRaw).success ? AIPlatformSchema.parse(platformRaw) : "anthropic";
16320
- const model = config2.ai?.model ?? getDefaultModel(platform);
16343
+ const model = resolveModelId(platform, config2.ai?.model ?? getDefaultModel(platform));
16321
16344
  const { key, source } = resolveAPIKey(platform);
16322
16345
  return { platform, model, apiKey: key, keySource: source };
16323
16346
  }
@@ -17017,12 +17040,14 @@ async function setupAnnotations(fileIdMap) {
17017
17040
  }
17018
17041
 
17019
17042
  // src/git/diff.ts
17020
- import { spawnSync as spawnSync4 } from "child_process";
17021
17043
  import { existsSync as existsSync2, mkdirSync as mkdirSync3, mkdtempSync, readFileSync as readFileSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
17022
17044
  import { tmpdir } from "os";
17023
17045
  import { basename, dirname, join as join3, resolve } from "path";
17024
17046
 
17025
17047
  // src/git/repo.ts
17048
+ import { spawnSync as spawnSync4 } from "child_process";
17049
+
17050
+ // src/git/spawn.ts
17026
17051
  import { spawnSync as spawnSync3 } from "child_process";
17027
17052
  function scrubbedGitEnv() {
17028
17053
  const env = { ...process.env };
@@ -17041,6 +17066,8 @@ function git(args, cwd) {
17041
17066
  err.status = result.status;
17042
17067
  throw err;
17043
17068
  }
17069
+
17070
+ // src/git/repo.ts
17044
17071
  function getRepoRoot(cwd) {
17045
17072
  return git(["rev-parse", "--show-toplevel"], cwd).trim();
17046
17073
  }
@@ -17057,7 +17084,7 @@ function isGitRepo(cwd) {
17057
17084
  }
17058
17085
  }
17059
17086
  function getHeadCommit(cwd) {
17060
- return spawnSync3("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", env: scrubbedGitEnv() }).stdout.trim();
17087
+ return spawnSync4("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", env: scrubbedGitEnv() }).stdout.trim();
17061
17088
  }
17062
17089
 
17063
17090
  // src/git/types.ts
@@ -17107,15 +17134,13 @@ function parseDiffData(raw) {
17107
17134
 
17108
17135
  // src/git/diff.ts
17109
17136
  var toGitArg = (p) => process.platform === "win32" ? p.replace(/\\/g, "/") : p;
17110
- function git2(args, cwd) {
17111
- const result = spawnSync4("git", args, { cwd, encoding: "utf-8", maxBuffer: 50 * 1024 * 1024, env: scrubbedGitEnv() });
17112
- if (result.status === 0) return result.stdout;
17113
- if (result.stdout !== "") return result.stdout;
17114
- const err = new Error(result.stderr);
17115
- err.stdout = result.stdout;
17116
- err.stderr = result.stderr;
17117
- err.status = result.status;
17118
- throw err;
17137
+ function gitOrEmpty(args, cwd) {
17138
+ try {
17139
+ return git(args, cwd);
17140
+ } catch (err) {
17141
+ debugLog(`git ${args.join(" ")} failed (treating as empty diff): ${err instanceof Error ? err.message : String(err)}`);
17142
+ return "";
17143
+ }
17119
17144
  }
17120
17145
  function getDiffArgs(mode) {
17121
17146
  switch (mode.type) {
@@ -17166,12 +17191,7 @@ function normalizeDiffPaths(diffs, rootA, rootB) {
17166
17191
  }
17167
17192
  function getDirectComparisonFiles(mode, cwd) {
17168
17193
  const { rootA, rootB } = directComparisonRoots(mode);
17169
- let rawDiff;
17170
- try {
17171
- rawDiff = git2(["diff", "--no-index", "-U3", toGitArg(mode.pathA), toGitArg(mode.pathB)], cwd);
17172
- } catch {
17173
- rawDiff = "";
17174
- }
17194
+ const rawDiff = gitOrEmpty(["diff", "--no-index", "-U3", toGitArg(mode.pathA), toGitArg(mode.pathB)], cwd);
17175
17195
  return normalizeDiffPaths(parseDiff(rawDiff), rootA, rootB);
17176
17196
  }
17177
17197
  function diffRawContent(displayPath, oldContent, newContent) {
@@ -17187,12 +17207,7 @@ function diffRawContent(displayPath, oldContent, newContent) {
17187
17207
  mkdirSync3(dirname(newAbs), { recursive: true });
17188
17208
  writeFileSync2(oldAbs, oldContent);
17189
17209
  writeFileSync2(newAbs, newContent);
17190
- let rawDiff;
17191
- try {
17192
- rawDiff = git2(["diff", "--no-index", "-U3", toGitArg(oldAbs), toGitArg(newAbs)], work);
17193
- } catch {
17194
- rawDiff = "";
17195
- }
17210
+ const rawDiff = gitOrEmpty(["diff", "--no-index", "-U3", toGitArg(oldAbs), toGitArg(newAbs)], work);
17196
17211
  const diffs = normalizeDiffPaths(parseDiff(rawDiff), rootA, rootB);
17197
17212
  if (diffs.length === 0) {
17198
17213
  return { filePath: safeRel, oldPath: null, status: "modified", hunks: [], isBinary: false };
@@ -17218,15 +17233,10 @@ function getFileDiffs(mode, cwd) {
17218
17233
  return getAllFiles(repoRoot);
17219
17234
  }
17220
17235
  const diffArgs = getDiffArgs(mode);
17221
- let rawDiff;
17222
- try {
17223
- rawDiff = git2([...diffArgs, "-U3"], repoRoot);
17224
- } catch {
17225
- rawDiff = "";
17226
- }
17236
+ const rawDiff = gitOrEmpty([...diffArgs, "-U3"], repoRoot);
17227
17237
  const diffs = parseDiff(rawDiff);
17228
17238
  if (mode.type === "uncommitted") {
17229
- const untracked = git2(["ls-files", "--others", "--exclude-standard"], repoRoot).trim();
17239
+ const untracked = git(["ls-files", "--others", "--exclude-standard"], repoRoot).trim();
17230
17240
  if (untracked) {
17231
17241
  for (const file2 of untracked.split("\n").filter(Boolean)) {
17232
17242
  if (!diffs.some((d) => d.filePath === file2)) {
@@ -17238,7 +17248,7 @@ function getFileDiffs(mode, cwd) {
17238
17248
  return diffs;
17239
17249
  }
17240
17250
  function getAllFiles(repoRoot) {
17241
- const files = git2(["ls-files"], repoRoot).trim().split("\n").filter(Boolean);
17251
+ const files = git(["ls-files"], repoRoot).trim().split("\n").filter(Boolean);
17242
17252
  return files.map((file2) => createNewFileDiff(file2, repoRoot));
17243
17253
  }
17244
17254
  function createNewFileDiff(filePath, repoRoot) {
@@ -17366,7 +17376,7 @@ function getFileContent(filePath, ref, cwd) {
17366
17376
  if (ref === "working") {
17367
17377
  return readFileSync2(resolve(repoRoot, filePath), "utf-8");
17368
17378
  }
17369
- return git2(["show", `${ref}:${filePath}`], repoRoot);
17379
+ return git(["show", `${ref}:${filePath}`], repoRoot);
17370
17380
  } catch {
17371
17381
  return "";
17372
17382
  }
@@ -17418,12 +17428,7 @@ function getSingleFileDiff(mode, filePath, repoRoot, extraFlags = "") {
17418
17428
  const args2 = ["diff", "--no-index", "-U3"];
17419
17429
  if (extraFlags) args2.push(...extraFlags.split(" ").filter(Boolean));
17420
17430
  args2.push(toGitArg(oldAbs), toGitArg(newAbs));
17421
- let rawDiff2;
17422
- try {
17423
- rawDiff2 = git2(args2, repoRoot);
17424
- } catch {
17425
- rawDiff2 = "";
17426
- }
17431
+ const rawDiff2 = gitOrEmpty(args2, repoRoot);
17427
17432
  const diffs2 = normalizeDiffPaths(parseDiff(rawDiff2), rootA, rootB);
17428
17433
  return diffs2[0] ?? null;
17429
17434
  }
@@ -17431,12 +17436,7 @@ function getSingleFileDiff(mode, filePath, repoRoot, extraFlags = "") {
17431
17436
  const args = [...diffArgs, "-U3"];
17432
17437
  if (extraFlags) args.push(...extraFlags.split(" ").filter(Boolean));
17433
17438
  args.push("--", filePath);
17434
- let rawDiff;
17435
- try {
17436
- rawDiff = git2(args, repoRoot);
17437
- } catch {
17438
- rawDiff = "";
17439
- }
17439
+ const rawDiff = gitOrEmpty(args, repoRoot);
17440
17440
  const diffs = parseDiff(rawDiff);
17441
17441
  return diffs[0] ?? null;
17442
17442
  }
@@ -19032,16 +19032,22 @@ async function getContextLines(req) {
19032
19032
  var files_exports = {};
19033
19033
  __export(files_exports, {
19034
19034
  FileStatusSchema: () => FileStatusSchema,
19035
+ GetFilePathReqSchema: () => GetFilePathReqSchema,
19036
+ GetFilePathRespSchema: () => GetFilePathRespSchema,
19035
19037
  GetFileReqSchema: () => GetFileReqSchema,
19036
19038
  GetFileRespSchema: () => GetFileRespSchema,
19037
19039
  ListFilesRespSchema: () => ListFilesRespSchema,
19040
+ OpenFileReqSchema: () => OpenFileReqSchema,
19041
+ OpenFileRespSchema: () => OpenFileRespSchema,
19038
19042
  RevealFileReqSchema: () => RevealFileReqSchema,
19039
19043
  RevealFileRespSchema: () => RevealFileRespSchema,
19040
19044
  SetFileStatusBodySchema: () => SetFileStatusBodySchema,
19041
19045
  SetFileStatusReqSchema: () => SetFileStatusReqSchema,
19042
19046
  SetFileStatusRespSchema: () => SetFileStatusRespSchema,
19043
19047
  getFile: () => getFile,
19048
+ getFilePath: () => getFilePath,
19044
19049
  listFiles: () => listFiles,
19050
+ openFileInEditor: () => openFileInEditor,
19045
19051
  revealFile: () => revealFile,
19046
19052
  setFileStatus: () => setFileStatus
19047
19053
  });
@@ -19066,6 +19072,13 @@ var SetFileStatusBodySchema = SetFileStatusReqSchema.omit({ fileId: true });
19066
19072
  var SetFileStatusRespSchema = OkResponseSchema;
19067
19073
  var RevealFileReqSchema = external_exports.object({ fileId: external_exports.string() });
19068
19074
  var RevealFileRespSchema = OkResponseSchema;
19075
+ var GetFilePathReqSchema = external_exports.object({ fileId: external_exports.string() });
19076
+ var GetFilePathRespSchema = external_exports.object({
19077
+ relativePath: external_exports.string(),
19078
+ absolutePath: external_exports.string()
19079
+ });
19080
+ var OpenFileReqSchema = external_exports.object({ fileId: external_exports.string() });
19081
+ var OpenFileRespSchema = OkResponseSchema;
19069
19082
  async function listFiles() {
19070
19083
  return apiCall(ListFilesRespSchema, "/files");
19071
19084
  }
@@ -19081,6 +19094,12 @@ async function setFileStatus(req) {
19081
19094
  async function revealFile(req) {
19082
19095
  return apiCall(RevealFileRespSchema, `/files/${req.fileId}/reveal`, { method: "POST" });
19083
19096
  }
19097
+ async function getFilePath(req) {
19098
+ return apiCall(GetFilePathRespSchema, `/files/${req.fileId}/path`);
19099
+ }
19100
+ async function openFileInEditor(req) {
19101
+ return apiCall(OpenFileRespSchema, `/files/${req.fileId}/open`, { method: "POST" });
19102
+ }
19084
19103
 
19085
19104
  // src/api/image.ts
19086
19105
  var image_exports = {};
@@ -19998,6 +20017,7 @@ function resolveReviewId(c) {
19998
20017
 
19999
20018
  // src/routes/ai-analysis.ts
20000
20019
  var aiAnalysisRoutes = new Hono();
20020
+ var ANALYSIS_REUSE_TIMEOUT_MS = 15 * 60 * 1e3;
20001
20021
  function parseAnalysisTimestamp(updatedAt) {
20002
20022
  if (updatedAt.endsWith("Z") || /[+-]\d{2}:?\d{2}$/.test(updatedAt)) {
20003
20023
  return new Date(updatedAt);
@@ -20058,7 +20078,7 @@ aiAnalysisRoutes.post("/analyze", async (c) => {
20058
20078
  if (existing !== void 0 && existing.status === "running") {
20059
20079
  const ageMs = Date.now() - parseAnalysisTimestamp(existing.updated_at).getTime();
20060
20080
  debugLog(`POST /analyze: existing analysis age=${String(Math.round(ageMs / 1e3))}s`);
20061
- if (ageMs < 15 * 60 * 1e3) {
20081
+ if (ageMs < ANALYSIS_REUSE_TIMEOUT_MS) {
20062
20082
  debugLog("POST /analyze: reusing existing running analysis");
20063
20083
  return c.json({ analysisId: existing.id, status: "running" });
20064
20084
  }
@@ -20078,6 +20098,8 @@ aiAnalysisRoutes.post("/analyze", async (c) => {
20078
20098
  repoRoot,
20079
20099
  guidedReview,
20080
20100
  invalidateCache
20101
+ }).catch((err) => {
20102
+ debugLog(`executeAnalysis dispatch rejected for ${analysis.id}: ${err instanceof Error ? err.message : String(err)}`);
20081
20103
  });
20082
20104
  return c.json({ analysisId: analysis.id, status: "running" });
20083
20105
  });
@@ -20395,6 +20417,99 @@ aiAnalysisRoutes.post("/preferences", async (c) => {
20395
20417
 
20396
20418
  // src/routes/ai-config.ts
20397
20419
  import { Hono as Hono2 } from "hono";
20420
+
20421
+ // src/ai/list-models.ts
20422
+ init_zod();
20423
+ var FETCH_TIMEOUT_MS = 8e3;
20424
+ async function getJson(url2, headers) {
20425
+ const controller = new AbortController();
20426
+ const timer = setTimeout(() => {
20427
+ controller.abort();
20428
+ }, FETCH_TIMEOUT_MS);
20429
+ try {
20430
+ const res = await fetch(url2, { headers, signal: controller.signal });
20431
+ if (!res.ok) return null;
20432
+ return await res.json();
20433
+ } catch {
20434
+ return null;
20435
+ } finally {
20436
+ clearTimeout(timer);
20437
+ }
20438
+ }
20439
+ var AnthropicListSchema = external_exports.object({
20440
+ data: external_exports.array(external_exports.object({ id: external_exports.string(), display_name: external_exports.string().optional() }))
20441
+ });
20442
+ function anthropicContextWindow(id) {
20443
+ return id.toLowerCase().includes("haiku") ? 2e5 : 1e6;
20444
+ }
20445
+ async function fetchAnthropic(apiKey) {
20446
+ const raw = await getJson("https://api.anthropic.com/v1/models?limit=1000", {
20447
+ "x-api-key": apiKey,
20448
+ "anthropic-version": "2023-06-01"
20449
+ });
20450
+ const parsed = AnthropicListSchema.safeParse(raw);
20451
+ if (!parsed.success) return null;
20452
+ return parsed.data.data.map((m) => ({
20453
+ id: m.id,
20454
+ name: m.display_name ?? m.id,
20455
+ contextWindow: anthropicContextWindow(m.id),
20456
+ isDefault: false
20457
+ }));
20458
+ }
20459
+ var OpenAIListSchema = external_exports.object({
20460
+ data: external_exports.array(external_exports.object({ id: external_exports.string() }))
20461
+ });
20462
+ function isOpenAIChatModel(id) {
20463
+ const l = id.toLowerCase();
20464
+ const isChat = /^(gpt-|chatgpt-|o\d)/.test(l);
20465
+ const isNonChat = /(embedding|whisper|tts|audio|realtime|transcribe|moderation|dall-e|image|search|babbage|davinci|instruct)/.test(l);
20466
+ return isChat && !isNonChat;
20467
+ }
20468
+ async function fetchOpenAI(apiKey) {
20469
+ const raw = await getJson("https://api.openai.com/v1/models", {
20470
+ Authorization: `Bearer ${apiKey}`
20471
+ });
20472
+ const parsed = OpenAIListSchema.safeParse(raw);
20473
+ if (!parsed.success) return null;
20474
+ return parsed.data.data.filter((m) => isOpenAIChatModel(m.id)).map((m) => ({ id: m.id, name: m.id, contextWindow: 128e3, isDefault: false }));
20475
+ }
20476
+ var GoogleListSchema = external_exports.object({
20477
+ models: external_exports.array(external_exports.object({
20478
+ name: external_exports.string(),
20479
+ displayName: external_exports.string().optional(),
20480
+ inputTokenLimit: external_exports.number().optional(),
20481
+ supportedGenerationMethods: external_exports.array(external_exports.string()).optional()
20482
+ }))
20483
+ });
20484
+ async function fetchGoogle(apiKey) {
20485
+ const raw = await getJson(
20486
+ `https://generativelanguage.googleapis.com/v1beta/models?pageSize=1000&key=${encodeURIComponent(apiKey)}`,
20487
+ {}
20488
+ );
20489
+ const parsed = GoogleListSchema.safeParse(raw);
20490
+ if (!parsed.success) return null;
20491
+ return parsed.data.models.filter((m) => (m.supportedGenerationMethods ?? []).includes("generateContent")).map((m) => ({
20492
+ id: m.name.replace(/^models\//, ""),
20493
+ name: m.displayName ?? m.name.replace(/^models\//, ""),
20494
+ contextWindow: m.inputTokenLimit !== void 0 && m.inputTokenLimit > 0 ? m.inputTokenLimit : 1e6,
20495
+ isDefault: false
20496
+ }));
20497
+ }
20498
+ async function fetchAvailableModels(platform, apiKey) {
20499
+ let models;
20500
+ if (platform === "anthropic") models = await fetchAnthropic(apiKey);
20501
+ else if (platform === "openai") models = await fetchOpenAI(apiKey);
20502
+ else models = await fetchGoogle(apiKey);
20503
+ if (models === null || models.length === 0) return null;
20504
+ const defaultId = getDefaultModel(platform);
20505
+ const hasDefault = models.some((m) => m.id === defaultId);
20506
+ return models.map((m, i) => ({
20507
+ ...m,
20508
+ isDefault: hasDefault ? m.id === defaultId : i === 0
20509
+ }));
20510
+ }
20511
+
20512
+ // src/routes/ai-config.ts
20398
20513
  var aiConfigRoutes = new Hono2();
20399
20514
  aiConfigRoutes.get("/config", (c) => {
20400
20515
  const config2 = loadAIConfig();
@@ -20416,8 +20531,18 @@ aiConfigRoutes.post("/config", async (c) => {
20416
20531
  }
20417
20532
  return c.json({ ok: true });
20418
20533
  });
20419
- aiConfigRoutes.get("/models", (c) => {
20420
- return c.json({ platforms: PLATFORMS, models: MODELS });
20534
+ aiConfigRoutes.get("/models", async (c) => {
20535
+ const platforms = ["anthropic", "openai", "google"];
20536
+ const models = { anthropic: MODELS.anthropic, openai: MODELS.openai, google: MODELS.google };
20537
+ if (!isAIServiceTest() && getDemoMode() === null) {
20538
+ await Promise.all(platforms.map(async (platform) => {
20539
+ const { key } = resolveAPIKey(platform);
20540
+ if (key === null) return;
20541
+ const live = await fetchAvailableModels(platform, key);
20542
+ if (live !== null && live.length > 0) models[platform] = live;
20543
+ }));
20544
+ }
20545
+ return c.json({ platforms: PLATFORMS, models });
20421
20546
  });
20422
20547
  aiConfigRoutes.get("/key-status", (c) => {
20423
20548
  const platforms = ["anthropic", "openai", "google"];
@@ -20719,6 +20844,16 @@ init_queries();
20719
20844
  import { execFileSync, spawn } from "child_process";
20720
20845
  import { resolve as resolve3 } from "path";
20721
20846
  function openOS(target, mode) {
20847
+ if (mode === "edit") {
20848
+ if (process.platform === "darwin") {
20849
+ launchDetached("open", [target]);
20850
+ } else if (process.platform === "win32") {
20851
+ launchDetached("cmd", ["/c", "start", "", target]);
20852
+ } else {
20853
+ launchDetached("xdg-open", [target]);
20854
+ }
20855
+ return;
20856
+ }
20722
20857
  if (mode === "reveal") {
20723
20858
  if (process.platform === "darwin") {
20724
20859
  launchDetached("open", ["-R", target]);
@@ -20739,7 +20874,8 @@ function openOS(target, mode) {
20739
20874
  }
20740
20875
  function launchDetached(command, args) {
20741
20876
  const child = spawn(command, args, { detached: true, stdio: "ignore" });
20742
- child.on("error", () => {
20877
+ child.on("error", (err) => {
20878
+ debugLog(`launchDetached(${command}) failed: ${err.message}`);
20743
20879
  });
20744
20880
  child.unref();
20745
20881
  }
@@ -20780,7 +20916,30 @@ filesRoutes.post("/files/:fileId/reveal", async (c) => {
20780
20916
  const fullPath = resolve4(repoRoot, file2.file_path);
20781
20917
  try {
20782
20918
  openOS(fullPath, "reveal");
20783
- } catch {
20919
+ } catch (err) {
20920
+ debugLog(`reveal failed for ${fullPath}: ${err instanceof Error ? err.message : String(err)}`);
20921
+ }
20922
+ return c.json({ ok: true });
20923
+ });
20924
+ filesRoutes.get("/files/:fileId/path", async (c) => {
20925
+ const fileId = requirePathParam(c, "fileId");
20926
+ if (!fileId.ok) return fileId.response;
20927
+ const file2 = await getReviewFile(fileId.data);
20928
+ if (!file2) return c.json({ error: "Not found" }, 404);
20929
+ const repoRoot = c.get("repoRoot");
20930
+ return c.json({ relativePath: file2.file_path, absolutePath: resolve4(repoRoot, file2.file_path) });
20931
+ });
20932
+ filesRoutes.post("/files/:fileId/open", async (c) => {
20933
+ const fileId = requirePathParam(c, "fileId");
20934
+ if (!fileId.ok) return fileId.response;
20935
+ const file2 = await getReviewFile(fileId.data);
20936
+ if (!file2) return c.json({ error: "Not found" }, 404);
20937
+ const repoRoot = c.get("repoRoot");
20938
+ const fullPath = resolve4(repoRoot, file2.file_path);
20939
+ try {
20940
+ openOS(fullPath, "edit");
20941
+ } catch (err) {
20942
+ debugLog(`open-in-editor failed for ${fullPath}: ${err instanceof Error ? err.message : String(err)}`);
20784
20943
  }
20785
20944
  return c.json({ ok: true });
20786
20945
  });
@@ -21746,6 +21905,7 @@ function pushIndentSymbol(root, stack, sym, indent, lines, lineIdx) {
21746
21905
 
21747
21906
  // src/routes/api/outline.ts
21748
21907
  var outlineRoutes = new Hono8();
21908
+ var MAX_REPO_SCAN_FILES = 2e3;
21749
21909
  outlineRoutes.get("/outline/:fileId", async (c) => {
21750
21910
  const repoRoot = c.get("repoRoot");
21751
21911
  const fileId = requirePathParam(c, "fileId");
@@ -21789,16 +21949,22 @@ outlineRoutes.get("/symbol-definition", async (c) => {
21789
21949
  if (definitions.length === 0) {
21790
21950
  try {
21791
21951
  const allFiles = spawnSync7("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).stdout.trim().split("\n").filter(Boolean);
21952
+ let scanned = 0;
21792
21953
  for (const filePath of allFiles) {
21793
21954
  if (searchedPaths.has(filePath)) continue;
21794
21955
  const ext = filePath.slice(filePath.lastIndexOf("."));
21795
21956
  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;
21957
+ if (scanned >= MAX_REPO_SCAN_FILES) {
21958
+ debugLog(`outline: repo scan hit ${String(MAX_REPO_SCAN_FILES)}-file budget for "${name}" without a match, stopping early`);
21959
+ break;
21960
+ }
21796
21961
  let content = "";
21797
21962
  try {
21798
21963
  content = readFileSync9(resolve6(repoRoot, filePath), "utf-8");
21799
21964
  } catch {
21800
21965
  continue;
21801
21966
  }
21967
+ scanned++;
21802
21968
  if (!content) continue;
21803
21969
  const symbols = parseOutline(content, filePath);
21804
21970
  collectDefinitions(symbols, name, null, filePath, definitions);
@@ -23821,7 +23987,7 @@ async function main() {
23821
23987
  console.log("AI service test mode enabled \u2014 using mock AI responses");
23822
23988
  }
23823
23989
  if (debug) {
23824
- console.log(`[debug] Build timestamp: ${"2026-06-17T07:20:59.383Z"}`);
23990
+ console.log(`[debug] Build timestamp: ${"2026-06-17T13:45:47.895Z"}`);
23825
23991
  }
23826
23992
  if (projectDir !== null) {
23827
23993
  process.chdir(projectDir);