glassbox 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2566,6 +2566,11 @@ async function mockGuidedAnalysisBatch(files) {
2566
2566
  // src/routes/ai-analysis.ts
2567
2567
  init_queries();
2568
2568
  var aiAnalysisRoutes = new Hono();
2569
+ var VALID_SORT_MODES = ["folder", "risk", "narrative", "guided"];
2570
+ var VALID_RISK_DIMENSIONS = ["aggregate", "security", "correctness", "error-handling", "maintainability", "architecture", "performance"];
2571
+ var VALID_SVG_VIEW_MODES = ["code", "rendered"];
2572
+ var VALID_IMAGE_MODES = ["metadata", "side-by-side", "difference", "slice"];
2573
+ var VALID_ANALYSIS_TYPES = ["risk", "narrative", "guided"];
2569
2574
  var cancelledAnalyses = /* @__PURE__ */ new Set();
2570
2575
  aiAnalysisRoutes.post("/analyze", async (c) => {
2571
2576
  const reviewId = c.req.query("reviewId") ?? "";
@@ -2574,8 +2579,8 @@ aiAnalysisRoutes.post("/analyze", async (c) => {
2574
2579
  const analysisType = body.type;
2575
2580
  const invalidateCache = body.invalidateCache === true;
2576
2581
  debugLog(`POST /analyze: type=${analysisType}, reviewId=${reviewId}`);
2577
- if (analysisType !== "risk" && analysisType !== "narrative" && analysisType !== "guided") {
2578
- return c.json({ error: "Invalid analysis type" }, 400);
2582
+ if (!VALID_ANALYSIS_TYPES.includes(analysisType)) {
2583
+ return c.json({ error: `type must be one of: ${VALID_ANALYSIS_TYPES.join(", ")}` }, 400);
2579
2584
  }
2580
2585
  const testMode = isAIServiceTest();
2581
2586
  const config = loadAIConfig();
@@ -2822,6 +2827,9 @@ async function runBatchedGuidedAnalysis(analysisId, batches, allFiles, config, r
2822
2827
  aiAnalysisRoutes.get("/analysis/:type", async (c) => {
2823
2828
  const reviewId = c.req.query("reviewId") ?? "";
2824
2829
  const analysisType = c.req.param("type");
2830
+ if (!VALID_ANALYSIS_TYPES.includes(analysisType)) {
2831
+ return c.json({ error: `type must be one of: ${VALID_ANALYSIS_TYPES.join(", ")}` }, 400);
2832
+ }
2825
2833
  const analysis = await getLatestAnalysis(reviewId, analysisType);
2826
2834
  if (analysis === void 0) {
2827
2835
  debugLog(`GET /analysis/${analysisType}: no analysis found`);
@@ -2854,6 +2862,9 @@ aiAnalysisRoutes.get("/analysis/:type", async (c) => {
2854
2862
  aiAnalysisRoutes.get("/analysis/:type/status", async (c) => {
2855
2863
  const reviewId = c.req.query("reviewId") ?? "";
2856
2864
  const analysisType = c.req.param("type");
2865
+ if (!VALID_ANALYSIS_TYPES.includes(analysisType)) {
2866
+ return c.json({ error: `type must be one of: ${VALID_ANALYSIS_TYPES.join(", ")}` }, 400);
2867
+ }
2857
2868
  const analysis = await getLatestAnalysis(reviewId, analysisType);
2858
2869
  if (analysis === void 0) {
2859
2870
  debugLog(`GET /analysis/${analysisType}/status: no analysis found`);
@@ -2881,6 +2892,9 @@ aiAnalysisRoutes.get("/debug-status", (c) => {
2881
2892
  aiAnalysisRoutes.post("/debug-log", async (c) => {
2882
2893
  if (!isDebug()) return c.json({ ok: true });
2883
2894
  const body = await c.req.json();
2895
+ if (typeof body.message !== "string") {
2896
+ return c.json({ error: "message must be a string" }, 400);
2897
+ }
2884
2898
  debugLog(`[client] ${body.message}`);
2885
2899
  return c.json({ ok: true });
2886
2900
  });
@@ -2890,6 +2904,24 @@ aiAnalysisRoutes.get("/preferences", async (c) => {
2890
2904
  });
2891
2905
  aiAnalysisRoutes.post("/preferences", async (c) => {
2892
2906
  const body = await c.req.json();
2907
+ if (body.sort_mode !== void 0 && !VALID_SORT_MODES.includes(body.sort_mode)) {
2908
+ return c.json({ error: `sort_mode must be one of: ${VALID_SORT_MODES.join(", ")}` }, 400);
2909
+ }
2910
+ if (body.risk_sort_dimension !== void 0 && !VALID_RISK_DIMENSIONS.includes(body.risk_sort_dimension)) {
2911
+ return c.json({ error: `risk_sort_dimension must be one of: ${VALID_RISK_DIMENSIONS.join(", ")}` }, 400);
2912
+ }
2913
+ if (body.show_risk_scores !== void 0 && typeof body.show_risk_scores !== "boolean") {
2914
+ return c.json({ error: "show_risk_scores must be a boolean" }, 400);
2915
+ }
2916
+ if (body.ignore_whitespace !== void 0 && typeof body.ignore_whitespace !== "boolean") {
2917
+ return c.json({ error: "ignore_whitespace must be a boolean" }, 400);
2918
+ }
2919
+ if (body.svg_view_mode !== void 0 && !VALID_SVG_VIEW_MODES.includes(body.svg_view_mode)) {
2920
+ return c.json({ error: `svg_view_mode must be one of: ${VALID_SVG_VIEW_MODES.join(", ")}` }, 400);
2921
+ }
2922
+ if (body.last_image_mode !== void 0 && !VALID_IMAGE_MODES.includes(body.last_image_mode)) {
2923
+ return c.json({ error: `last_image_mode must be one of: ${VALID_IMAGE_MODES.join(", ")}` }, 400);
2924
+ }
2893
2925
  await saveUserPreferences(body);
2894
2926
  return c.json({ ok: true });
2895
2927
  });
@@ -2897,6 +2929,8 @@ aiAnalysisRoutes.post("/preferences", async (c) => {
2897
2929
  // src/routes/ai-config.ts
2898
2930
  import { Hono as Hono2 } from "hono";
2899
2931
  var aiConfigRoutes = new Hono2();
2932
+ var VALID_PLATFORMS = ["anthropic", "openai", "google"];
2933
+ var VALID_KEY_STORAGES = ["keychain", "config"];
2900
2934
  aiConfigRoutes.get("/config", (c) => {
2901
2935
  const config = loadAIConfig();
2902
2936
  return c.json({
@@ -2909,6 +2943,25 @@ aiConfigRoutes.get("/config", (c) => {
2909
2943
  });
2910
2944
  aiConfigRoutes.post("/config", async (c) => {
2911
2945
  const body = await c.req.json();
2946
+ if (!VALID_PLATFORMS.includes(body.platform)) {
2947
+ return c.json({ error: `platform must be one of: ${VALID_PLATFORMS.join(", ")}` }, 400);
2948
+ }
2949
+ if (typeof body.model !== "string" || body.model.trim() === "") {
2950
+ return c.json({ error: "model must be a non-empty string" }, 400);
2951
+ }
2952
+ if (body.guidedReview !== void 0) {
2953
+ const gr = body.guidedReview;
2954
+ if (typeof gr !== "object" || gr === null || Array.isArray(gr)) {
2955
+ return c.json({ error: "guidedReview must be an object" }, 400);
2956
+ }
2957
+ const grObj = gr;
2958
+ if (typeof grObj.enabled !== "boolean") {
2959
+ return c.json({ error: "guidedReview.enabled must be a boolean" }, 400);
2960
+ }
2961
+ if (!Array.isArray(grObj.topics) || !grObj.topics.every((t) => typeof t === "string")) {
2962
+ return c.json({ error: "guidedReview.topics must be an array of strings" }, 400);
2963
+ }
2964
+ }
2912
2965
  saveAIConfigPreferences(body.platform, body.model);
2913
2966
  if (body.guidedReview !== void 0) {
2914
2967
  saveGuidedReviewConfig(body.guidedReview);
@@ -2937,6 +2990,15 @@ aiConfigRoutes.get("/key-status", (c) => {
2937
2990
  });
2938
2991
  aiConfigRoutes.post("/key", async (c) => {
2939
2992
  const body = await c.req.json();
2993
+ if (!VALID_PLATFORMS.includes(body.platform)) {
2994
+ return c.json({ error: `platform must be one of: ${VALID_PLATFORMS.join(", ")}` }, 400);
2995
+ }
2996
+ if (typeof body.key !== "string" || body.key.trim() === "") {
2997
+ return c.json({ error: "key must be a non-empty string" }, 400);
2998
+ }
2999
+ if (!VALID_KEY_STORAGES.includes(body.storage)) {
3000
+ return c.json({ error: `storage must be one of: ${VALID_KEY_STORAGES.join(", ")}` }, 400);
3001
+ }
2940
3002
  saveAPIKey(
2941
3003
  body.platform,
2942
3004
  body.key,
@@ -2946,6 +3008,9 @@ aiConfigRoutes.post("/key", async (c) => {
2946
3008
  });
2947
3009
  aiConfigRoutes.delete("/key", (c) => {
2948
3010
  const platform = c.req.query("platform") ?? "anthropic";
3011
+ if (!VALID_PLATFORMS.includes(platform)) {
3012
+ return c.json({ error: `platform must be one of: ${VALID_PLATFORMS.join(", ")}` }, 400);
3013
+ }
2949
3014
  deleteAPIKey(platform);
2950
3015
  return c.json({ ok: true });
2951
3016
  });
@@ -3871,6 +3936,9 @@ function pushIndentSymbol(root, stack, sym, indent, lines, lineIdx) {
3871
3936
 
3872
3937
  // src/routes/api.ts
3873
3938
  var apiRoutes = new Hono4();
3939
+ var VALID_CATEGORIES = ["bug", "fix", "style", "pattern-follow", "pattern-avoid", "note", "remember"];
3940
+ var VALID_SIDES = ["old", "new"];
3941
+ var VALID_FILE_STATUSES = ["pending", "reviewed"];
3874
3942
  function resolveReviewId(c) {
3875
3943
  return c.req.query("reviewId") ?? c.get("reviewId");
3876
3944
  }
@@ -3977,6 +4045,9 @@ apiRoutes.get("/files/:fileId", async (c) => {
3977
4045
  });
3978
4046
  apiRoutes.patch("/files/:fileId/status", async (c) => {
3979
4047
  const { status } = await c.req.json();
4048
+ if (!VALID_FILE_STATUSES.includes(status)) {
4049
+ return c.json({ error: `status must be one of: ${VALID_FILE_STATUSES.join(", ")}` }, 400);
4050
+ }
3980
4051
  await updateFileStatus(c.req.param("fileId"), status);
3981
4052
  return c.json({ ok: true });
3982
4053
  });
@@ -4002,6 +4073,21 @@ function autoExport(c) {
4002
4073
  }
4003
4074
  apiRoutes.post("/annotations", async (c) => {
4004
4075
  const body = await c.req.json();
4076
+ if (typeof body.reviewFileId !== "string" || body.reviewFileId === "") {
4077
+ return c.json({ error: "reviewFileId must be a non-empty string" }, 400);
4078
+ }
4079
+ if (typeof body.lineNumber !== "number" || !Number.isInteger(body.lineNumber) || body.lineNumber < 1) {
4080
+ return c.json({ error: "lineNumber must be a positive integer" }, 400);
4081
+ }
4082
+ if (!VALID_SIDES.includes(body.side)) {
4083
+ return c.json({ error: `side must be one of: ${VALID_SIDES.join(", ")}` }, 400);
4084
+ }
4085
+ if (!VALID_CATEGORIES.includes(body.category)) {
4086
+ return c.json({ error: `category must be one of: ${VALID_CATEGORIES.join(", ")}` }, 400);
4087
+ }
4088
+ if (typeof body.content !== "string" || body.content.trim() === "") {
4089
+ return c.json({ error: "content must be a non-empty string" }, 400);
4090
+ }
4005
4091
  const annotation = await addAnnotation(
4006
4092
  body.reviewFileId,
4007
4093
  body.lineNumber,
@@ -4014,6 +4100,12 @@ apiRoutes.post("/annotations", async (c) => {
4014
4100
  });
4015
4101
  apiRoutes.patch("/annotations/:id", async (c) => {
4016
4102
  const { content, category } = await c.req.json();
4103
+ if (typeof content !== "string" || content.trim() === "") {
4104
+ return c.json({ error: "content must be a non-empty string" }, 400);
4105
+ }
4106
+ if (!VALID_CATEGORIES.includes(category)) {
4107
+ return c.json({ error: `category must be one of: ${VALID_CATEGORIES.join(", ")}` }, 400);
4108
+ }
4017
4109
  await updateAnnotation(c.req.param("id"), content, category);
4018
4110
  autoExport(c);
4019
4111
  return c.json({ ok: true });
@@ -4025,6 +4117,12 @@ apiRoutes.delete("/annotations/:id", async (c) => {
4025
4117
  });
4026
4118
  apiRoutes.patch("/annotations/:id/move", async (c) => {
4027
4119
  const { lineNumber, side } = await c.req.json();
4120
+ if (typeof lineNumber !== "number" || !Number.isInteger(lineNumber) || lineNumber < 1) {
4121
+ return c.json({ error: "lineNumber must be a positive integer" }, 400);
4122
+ }
4123
+ if (!VALID_SIDES.includes(side)) {
4124
+ return c.json({ error: `side must be one of: ${VALID_SIDES.join(", ")}` }, 400);
4125
+ }
4028
4126
  await moveAnnotation(c.req.param("id"), lineNumber, side);
4029
4127
  autoExport(c);
4030
4128
  return c.json({ ok: true });
@@ -4173,6 +4271,9 @@ apiRoutes.get("/project-settings", (c) => {
4173
4271
  apiRoutes.patch("/project-settings", async (c) => {
4174
4272
  const repoRoot = c.get("repoRoot");
4175
4273
  const body = await c.req.json();
4274
+ if (body.appName !== void 0 && typeof body.appName !== "string") {
4275
+ return c.json({ error: "appName must be a string" }, 400);
4276
+ }
4176
4277
  const current = readProjectSettings(repoRoot);
4177
4278
  if (body.appName !== void 0) current.appName = body.appName || void 0;
4178
4279
  writeProjectSettings(repoRoot, current);
@@ -4215,7 +4316,7 @@ apiRoutes.get("/image/:fileId/:side", async (c) => {
4215
4316
  if (isSvgFile(file.file_path)) {
4216
4317
  try {
4217
4318
  const png = await rasterizeSvg(image.data);
4218
- return new Response(png, {
4319
+ return new Response(new Uint8Array(png), {
4219
4320
  headers: { "Content-Type": "image/png", "Cache-Control": "no-cache" }
4220
4321
  });
4221
4322
  } catch {
@@ -4223,7 +4324,7 @@ apiRoutes.get("/image/:fileId/:side", async (c) => {
4223
4324
  }
4224
4325
  }
4225
4326
  const contentType = getContentType(file.file_path);
4226
- return new Response(image.data, {
4327
+ return new Response(new Uint8Array(image.data), {
4227
4328
  headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
4228
4329
  });
4229
4330
  });
@@ -5938,6 +6039,21 @@ pageRoutes.get("/history", async (c) => {
5938
6039
  // src/routes/theme-api.ts
5939
6040
  import { Hono as Hono6 } from "hono";
5940
6041
  var themeApiRoutes = new Hono6();
6042
+ function validateColors(colors) {
6043
+ if (typeof colors !== "object" || colors === null || Array.isArray(colors)) {
6044
+ return "colors must be an object";
6045
+ }
6046
+ const validKeys = new Set(THEME_VARIABLES);
6047
+ for (const [key, value] of Object.entries(colors)) {
6048
+ if (!validKeys.has(key)) {
6049
+ return `colors contains unknown key: ${key}`;
6050
+ }
6051
+ if (typeof value !== "string") {
6052
+ return `colors.${key} must be a string`;
6053
+ }
6054
+ }
6055
+ return null;
6056
+ }
5941
6057
  themeApiRoutes.get("/", (c) => {
5942
6058
  const themes = getAllThemes();
5943
6059
  const activeId = getActiveThemeId();
@@ -5958,7 +6074,7 @@ themeApiRoutes.get("/active", (c) => {
5958
6074
  });
5959
6075
  themeApiRoutes.post("/active", async (c) => {
5960
6076
  const body = await c.req.json();
5961
- if (!body.id) return c.json({ error: "Missing theme id" }, 400);
6077
+ if (typeof body.id !== "string" || body.id === "") return c.json({ error: "id must be a non-empty string" }, 400);
5962
6078
  const theme = resolveTheme(body.id);
5963
6079
  if (!theme) return c.json({ error: "Theme not found" }, 404);
5964
6080
  setActiveThemeId(body.id);
@@ -5966,7 +6082,10 @@ themeApiRoutes.post("/active", async (c) => {
5966
6082
  });
5967
6083
  themeApiRoutes.post("/", async (c) => {
5968
6084
  const body = await c.req.json();
5969
- if (!body.sourceId) return c.json({ error: "Missing sourceId" }, 400);
6085
+ if (typeof body.sourceId !== "string" || body.sourceId === "") return c.json({ error: "sourceId must be a non-empty string" }, 400);
6086
+ if (body.name !== void 0 && (typeof body.name !== "string" || body.name.trim() === "")) {
6087
+ return c.json({ error: "name must be a non-empty string when provided" }, 400);
6088
+ }
5970
6089
  const source = resolveTheme(body.sourceId);
5971
6090
  if (!source) return c.json({ error: "Source theme not found" }, 404);
5972
6091
  const baseTheme = source.builtIn ? source.id : source.baseTheme;
@@ -5984,6 +6103,13 @@ themeApiRoutes.post("/", async (c) => {
5984
6103
  themeApiRoutes.post("/:id/edit", async (c) => {
5985
6104
  const id = c.req.param("id");
5986
6105
  const body = await c.req.json();
6106
+ if (body.name !== void 0 && (typeof body.name !== "string" || body.name.trim() === "")) {
6107
+ return c.json({ error: "name must be a non-empty string when provided" }, 400);
6108
+ }
6109
+ if (body.colors !== void 0) {
6110
+ const colorsError = validateColors(body.colors);
6111
+ if (colorsError !== null) return c.json({ error: colorsError }, 400);
6112
+ }
5987
6113
  const source = resolveTheme(id);
5988
6114
  if (!source) return c.json({ error: "Theme not found" }, 404);
5989
6115
  if (source.builtIn) {
@@ -6017,6 +6143,13 @@ themeApiRoutes.patch("/:id", async (c) => {
6017
6143
  return c.json({ error: "Theme not found" }, 404);
6018
6144
  }
6019
6145
  const body = await c.req.json();
6146
+ if (body.name !== void 0 && (typeof body.name !== "string" || body.name.trim() === "")) {
6147
+ return c.json({ error: "name must be a non-empty string when provided" }, 400);
6148
+ }
6149
+ if (body.colors !== void 0) {
6150
+ const colorsError = validateColors(body.colors);
6151
+ if (colorsError !== null) return c.json({ error: colorsError }, 400);
6152
+ }
6020
6153
  const updated = {
6021
6154
  ...existing,
6022
6155
  name: body.name ?? existing.name,
@@ -6038,9 +6171,9 @@ themeApiRoutes.delete("/:id", (c) => {
6038
6171
  });
6039
6172
 
6040
6173
  // src/server.ts
6041
- function tryServe(fetch2, port) {
6174
+ function tryServe(appFetch, port) {
6042
6175
  return new Promise((resolve6, reject) => {
6043
- const server = serve({ fetch: fetch2, port, hostname: "127.0.0.1" });
6176
+ const server = serve({ fetch: appFetch, port, hostname: "127.0.0.1" });
6044
6177
  server.on("listening", () => {
6045
6178
  resolve6(port);
6046
6179
  });
@@ -6337,6 +6470,7 @@ async function checkForUpdates(force) {
6337
6470
  }
6338
6471
 
6339
6472
  // src/cli.ts
6473
+ import { realpathSync } from "fs";
6340
6474
  function printUsage() {
6341
6475
  console.log(`
6342
6476
  glassbox - Review AI-generated code with annotations
@@ -6480,7 +6614,7 @@ async function main() {
6480
6614
  console.log("AI service test mode enabled \u2014 using mock AI responses");
6481
6615
  }
6482
6616
  if (debug) {
6483
- console.log(`[debug] Build timestamp: ${"2026-04-02T08:06:45.351Z"}`);
6617
+ console.log(`[debug] Build timestamp: ${"2026-04-02T11:34:35.783Z"}`);
6484
6618
  }
6485
6619
  if (projectDir !== null) {
6486
6620
  process.chdir(projectDir);
@@ -6561,7 +6695,8 @@ async function main() {
6561
6695
  console.log(`Review ${review.id} created.`);
6562
6696
  await startServer(port, review.id, repoRoot, { noOpen, strictPort });
6563
6697
  }
6564
- var isDirectRun = process.argv[1]?.endsWith("cli.js") || process.argv[1]?.endsWith("cli.ts");
6698
+ var resolvedArg = process.argv[1] !== void 0 ? realpathSync(process.argv[1]) : "";
6699
+ var isDirectRun = resolvedArg.endsWith("cli.js") || resolvedArg.endsWith("cli.ts");
6565
6700
  if (isDirectRun) {
6566
6701
  main().catch((err) => {
6567
6702
  console.error(err);