glassbox 0.14.0 → 0.15.0-beta.3

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
@@ -52,6 +52,7 @@ var init_schema = __esm({
52
52
  content TEXT NOT NULL,
53
53
  is_stale BOOLEAN NOT NULL DEFAULT FALSE,
54
54
  original_content TEXT,
55
+ reply_to_note_id TEXT,
55
56
  created_at TIMESTAMP NOT NULL DEFAULT NOW(),
56
57
  updated_at TIMESTAMP NOT NULL DEFAULT NOW()
57
58
  );
@@ -146,6 +147,7 @@ async function initSchema(db2) {
146
147
  await addColumnIfMissing(db2, "reviews", "head_commit", "TEXT");
147
148
  await addColumnIfMissing(db2, "annotations", "is_stale", "BOOLEAN NOT NULL DEFAULT FALSE");
148
149
  await addColumnIfMissing(db2, "annotations", "original_content", "TEXT");
150
+ await addColumnIfMissing(db2, "annotations", "reply_to_note_id", "TEXT");
149
151
  await addColumnIfMissing(db2, "ai_file_scores", "notes", "TEXT");
150
152
  await addColumnIfMissing(db2, "ai_analyses", "progress_completed", "INTEGER NOT NULL DEFAULT 0");
151
153
  await addColumnIfMissing(db2, "ai_analyses", "progress_total", "INTEGER NOT NULL DEFAULT 0");
@@ -15369,11 +15371,11 @@ function parseRow(schema, row) {
15369
15371
  }
15370
15372
  return result.data;
15371
15373
  }
15372
- function parseJsonColumn(schema, raw) {
15373
- if (raw === null || raw === void 0) return null;
15374
+ function parseJsonColumn(schema, raw2) {
15375
+ if (raw2 === null || raw2 === void 0) return null;
15374
15376
  let parsed;
15375
15377
  try {
15376
- parsed = JSON.parse(raw);
15378
+ parsed = JSON.parse(raw2);
15377
15379
  } catch {
15378
15380
  return null;
15379
15381
  }
@@ -15421,6 +15423,10 @@ var init_schemas3 = __esm({
15421
15423
  content: external_exports.string(),
15422
15424
  is_stale: external_exports.boolean(),
15423
15425
  original_content: external_exports.string().nullable(),
15426
+ // The SARIF guid of the AI review note this annotation replies to (doc 20
15427
+ // threading), or null for a normal annotation. `.default(null)` tolerates
15428
+ // rows written before the column existed.
15429
+ reply_to_note_id: external_exports.string().nullable().default(null),
15424
15430
  created_at: TimestampSchema,
15425
15431
  updated_at: TimestampSchema
15426
15432
  });
@@ -15592,13 +15598,13 @@ async function deleteReviewFile(id) {
15592
15598
  await db2.query("DELETE FROM annotations WHERE review_file_id = $1", [id]);
15593
15599
  await db2.query("DELETE FROM review_files WHERE id = $1", [id]);
15594
15600
  }
15595
- async function addAnnotation(reviewFileId, lineNumber, side, category, content) {
15601
+ async function addAnnotation(reviewFileId, lineNumber, side, category, content, replyToNoteId) {
15596
15602
  const db2 = await getDb();
15597
15603
  const id = generateId();
15598
15604
  const result = await db2.query(
15599
- `INSERT INTO annotations (id, review_file_id, line_number, side, category, content)
15600
- VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`,
15601
- [id, reviewFileId, lineNumber, side, category, content]
15605
+ `INSERT INTO annotations (id, review_file_id, line_number, side, category, content, reply_to_note_id)
15606
+ VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
15607
+ [id, reviewFileId, lineNumber, side, category, content, replyToNoteId ?? null]
15602
15608
  );
15603
15609
  const annotation = parseRow(AnnotationSchema, result.rows[0]);
15604
15610
  if (annotation === void 0) throw new Error("addAnnotation: INSERT did not return a row");
@@ -15719,6 +15725,450 @@ var init_queries = __esm({
15719
15725
  }
15720
15726
  });
15721
15727
 
15728
+ // src/review-notes/types.ts
15729
+ function isNoteKind(value) {
15730
+ return NOTE_KINDS.includes(value);
15731
+ }
15732
+ var NOTE_KINDS, CONFIDENCE_PROPERTY_KEY, ANCHOR_FINGERPRINT_KEY, DEFAULT_PRODUCER, DEFAULT_SHARD_CAP;
15733
+ var init_types = __esm({
15734
+ "src/review-notes/types.ts"() {
15735
+ "use strict";
15736
+ NOTE_KINDS = [
15737
+ "rationale",
15738
+ "proof",
15739
+ "assumption",
15740
+ "alternative-considered",
15741
+ "risk",
15742
+ "test-evidence"
15743
+ ];
15744
+ CONFIDENCE_PROPERTY_KEY = "ext-ai-tool-confidence";
15745
+ ANCHOR_FINGERPRINT_KEY = "prNoteAnchor/v1";
15746
+ DEFAULT_PRODUCER = "unknown-ai-tool";
15747
+ DEFAULT_SHARD_CAP = 1e4;
15748
+ }
15749
+ });
15750
+
15751
+ // src/review-notes/sarif.ts
15752
+ function emptyLog(producer, opts = {}) {
15753
+ return { $schema: SARIF_SCHEMA_URL, version: "2.1.0", runs: [newRun(producer, opts)] };
15754
+ }
15755
+ function newRun(producer, opts = {}) {
15756
+ const run = {
15757
+ tool: {
15758
+ driver: {
15759
+ name: producer,
15760
+ ...opts.producerVersion !== void 0 ? { version: opts.producerVersion } : {},
15761
+ rules: [{
15762
+ id: REVIEW_NOTE_RULE_ID,
15763
+ name: "ReviewNote",
15764
+ shortDescription: { text: "AI-authored, line-anchored review note." }
15765
+ }]
15766
+ }
15767
+ },
15768
+ results: []
15769
+ };
15770
+ if (opts.revisionId !== void 0 || opts.branch !== void 0 || opts.repositoryUri !== void 0) {
15771
+ const vcs = {};
15772
+ if (opts.repositoryUri !== void 0) vcs.repositoryUri = opts.repositoryUri;
15773
+ if (opts.revisionId !== void 0) vcs.revisionId = opts.revisionId;
15774
+ if (opts.branch !== void 0) vcs.branch = opts.branch;
15775
+ run.versionControlProvenance = [vcs];
15776
+ }
15777
+ return run;
15778
+ }
15779
+ function buildResult(input, meta3) {
15780
+ const region = { startLine: input.startLine, endLine: input.endLine };
15781
+ if (meta3.snippet !== void 0) region.snippet = { text: meta3.snippet };
15782
+ const properties = { tags: [input.kind] };
15783
+ if (input.confidence !== void 0) properties[CONFIDENCE_PROPERTY_KEY] = input.confidence;
15784
+ const result = {
15785
+ ruleId: REVIEW_NOTE_RULE_ID,
15786
+ ruleIndex: 0,
15787
+ kind: "informational",
15788
+ level: input.kind === "risk" ? "warning" : "none",
15789
+ guid: meta3.guid,
15790
+ message: { text: input.body, markdown: input.body },
15791
+ locations: [{
15792
+ physicalLocation: {
15793
+ artifactLocation: { uri: input.file },
15794
+ region
15795
+ }
15796
+ }],
15797
+ properties
15798
+ };
15799
+ if (input.rank !== void 0) result.rank = input.rank;
15800
+ if (input.ticket !== void 0 && input.ticket !== "") result.workItemUris = [input.ticket];
15801
+ if (meta3.fingerprint !== void 0) result.partialFingerprints = { [ANCHOR_FINGERPRINT_KEY]: meta3.fingerprint };
15802
+ if (input.artifacts !== void 0 && input.artifacts.length > 0) {
15803
+ result.attachments = input.artifacts.map((uri) => {
15804
+ const artifactLocation = { uri };
15805
+ const hash2 = meta3.artifactHashes?.[uri];
15806
+ if (hash2 !== void 0) artifactLocation.properties = { "ext-sha256": hash2 };
15807
+ return { artifactLocation };
15808
+ });
15809
+ }
15810
+ return result;
15811
+ }
15812
+ var SARIF_SCHEMA_URL, REVIEW_NOTE_RULE_ID, SarifLogShapeSchema;
15813
+ var init_sarif = __esm({
15814
+ "src/review-notes/sarif.ts"() {
15815
+ "use strict";
15816
+ init_zod();
15817
+ init_types();
15818
+ SARIF_SCHEMA_URL = "https://json.schemastore.org/sarif-2.1.0.json";
15819
+ REVIEW_NOTE_RULE_ID = "review-note";
15820
+ SarifLogShapeSchema = external_exports.object({
15821
+ version: external_exports.string(),
15822
+ runs: external_exports.array(external_exports.object({
15823
+ tool: external_exports.object({ driver: external_exports.object({ name: external_exports.string() }).loose() }).loose(),
15824
+ versionControlProvenance: external_exports.array(external_exports.object({ revisionId: external_exports.string().optional() }).loose()).optional(),
15825
+ results: external_exports.array(external_exports.unknown())
15826
+ }).loose())
15827
+ }).loose();
15828
+ }
15829
+ });
15830
+
15831
+ // src/review-notes/view.ts
15832
+ var REVIEW_NOTE_LABELS, IMAGE_ARTIFACT_RE;
15833
+ var init_view = __esm({
15834
+ "src/review-notes/view.ts"() {
15835
+ "use strict";
15836
+ REVIEW_NOTE_LABELS = {
15837
+ rationale: "Rationale",
15838
+ proof: "Proof",
15839
+ assumption: "Assumption",
15840
+ "alternative-considered": "Alternative",
15841
+ risk: "Risk",
15842
+ "test-evidence": "Test"
15843
+ };
15844
+ IMAGE_ARTIFACT_RE = /\.(png|webp|avif|gif|jpe?g|svg)$/i;
15845
+ }
15846
+ });
15847
+
15848
+ // src/review-notes/store.ts
15849
+ import { spawnSync as spawnSync5 } from "child_process";
15850
+ import { createHash } from "crypto";
15851
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync5, statSync as statSync2, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
15852
+ import { dirname as dirname3, join as join6 } from "path";
15853
+ function sanitizeRel(file2) {
15854
+ const rel = file2.replace(/\\/g, "/").replace(/^\/+/, "").replace(/(^|\/)\.\.(?=\/|$)/g, "$1_");
15855
+ return rel === "" ? "file" : rel;
15856
+ }
15857
+ function shardPath(repoRoot, safeRel, index) {
15858
+ return join6(repoRoot, NOTES_SUBDIR, `${safeRel}.${String(index).padStart(6, "0")}.sarif`);
15859
+ }
15860
+ function listShardIndices(repoRoot, safeRel) {
15861
+ const dir = join6(repoRoot, NOTES_SUBDIR, dirname3(safeRel));
15862
+ if (!existsSync5(dir)) return [];
15863
+ const base = safeRel.split("/").pop() ?? safeRel;
15864
+ const indices = [];
15865
+ for (const entry of readdirSync(dir)) {
15866
+ if (!entry.startsWith(base + ".")) continue;
15867
+ const m = SHARD_RE.exec(entry);
15868
+ if (m !== null && entry === `${base}.${m[1]}.sarif`) indices.push(parseInt(m[1], 10));
15869
+ }
15870
+ return indices.sort((a, b) => a - b);
15871
+ }
15872
+ function totalResults(log) {
15873
+ return log.runs.reduce((sum, run) => sum + run.results.length, 0);
15874
+ }
15875
+ function readLog(path) {
15876
+ const raw2 = JSON.parse(readFileSync5(path, "utf-8"));
15877
+ const parsed = SarifLogShapeSchema.safeParse(raw2);
15878
+ if (!parsed.success) {
15879
+ throw new Error(`existing notes shard is not a SARIF log we recognize, refusing to overwrite: ${path}`);
15880
+ }
15881
+ return raw2;
15882
+ }
15883
+ function findOrAddRun(log, producer, producerVersion, vcs) {
15884
+ const existing = log.runs.find((run2) => run2.tool.driver.name === producer && run2.tool.driver.version === producerVersion && (run2.versionControlProvenance?.[0]?.revisionId ?? void 0) === vcs.revisionId);
15885
+ if (existing !== void 0) return existing;
15886
+ const run = newRun(producer, { producerVersion, ...vcs });
15887
+ log.runs.push(run);
15888
+ return run;
15889
+ }
15890
+ function gitValue(repoRoot, args) {
15891
+ try {
15892
+ const res = spawnSync5("git", args, { cwd: repoRoot, encoding: "utf-8" });
15893
+ if (res.status !== 0) return void 0;
15894
+ const out = res.stdout.trim();
15895
+ return out === "" ? void 0 : out;
15896
+ } catch {
15897
+ return void 0;
15898
+ }
15899
+ }
15900
+ function anchorSnippet(repoRoot, safeRel, startLine, endLine) {
15901
+ try {
15902
+ const lines = readFileSync5(join6(repoRoot, safeRel), "utf-8").split("\n");
15903
+ const slice = lines.slice(Math.max(0, startLine - 1), Math.max(startLine, endLine));
15904
+ if (slice.length === 0) return {};
15905
+ const snippet = slice.join("\n");
15906
+ const normalized = slice.map((l) => l.trim().replace(/\s+/g, " ")).join("\n");
15907
+ const fingerprint = createHash("sha256").update(normalized).digest("hex").slice(0, 32);
15908
+ return { snippet, fingerprint };
15909
+ } catch {
15910
+ return {};
15911
+ }
15912
+ }
15913
+ function warnIfPrNotesIgnored(repoRoot) {
15914
+ try {
15915
+ const res = spawnSync5("git", ["check-ignore", ".pr-notes"], { cwd: repoRoot, encoding: "utf-8" });
15916
+ if (res.status === 0) {
15917
+ console.error("Warning: .pr-notes/ appears to be gitignored. Review notes must be committed to be useful \u2014 remove it from .gitignore.");
15918
+ }
15919
+ } catch {
15920
+ }
15921
+ }
15922
+ function writeReviewNote(repoRoot, input, opts = {}) {
15923
+ const cap = opts.cap ?? DEFAULT_SHARD_CAP;
15924
+ const safeRel = sanitizeRel(input.file);
15925
+ const producer = input.producer ?? DEFAULT_PRODUCER;
15926
+ const vcs = {
15927
+ revisionId: gitValue(repoRoot, ["rev-parse", "HEAD"]),
15928
+ branch: gitValue(repoRoot, ["rev-parse", "--abbrev-ref", "HEAD"]),
15929
+ repositoryUri: gitValue(repoRoot, ["config", "--get", "remote.origin.url"])
15930
+ };
15931
+ const { snippet, fingerprint } = anchorSnippet(repoRoot, safeRel, input.startLine, input.endLine);
15932
+ const guid3 = generateId();
15933
+ const artifactHashes = hashArtifacts(repoRoot, input.artifacts ?? []);
15934
+ const result = buildResult({ ...input, file: safeRel }, { guid: guid3, snippet, fingerprint, artifactHashes });
15935
+ if ((input.artifacts ?? []).some((a) => IMAGE_ARTIFACT_RE.test(a))) ensureArtifactLfsFilter(repoRoot);
15936
+ const indices = listShardIndices(repoRoot, safeRel);
15937
+ let index = indices.length > 0 ? indices[indices.length - 1] : 0;
15938
+ let path = shardPath(repoRoot, safeRel, index);
15939
+ let log;
15940
+ if (existsSync5(path)) {
15941
+ log = readLog(path);
15942
+ if (totalResults(log) >= cap) {
15943
+ index += 1;
15944
+ path = shardPath(repoRoot, safeRel, index);
15945
+ log = emptyLog(producer, { producerVersion: input.producerVersion, ...vcs });
15946
+ }
15947
+ } else {
15948
+ log = emptyLog(producer, { producerVersion: input.producerVersion, ...vcs });
15949
+ }
15950
+ findOrAddRun(log, producer, input.producerVersion, vcs).results.push(result);
15951
+ mkdirSync4(dirname3(path), { recursive: true });
15952
+ writeFileSync5(path, JSON.stringify(log, null, 2) + "\n", "utf-8");
15953
+ return { path, guid: guid3 };
15954
+ }
15955
+ function writeLog(path, log) {
15956
+ writeFileSync5(path, JSON.stringify(log, null, 2) + "\n", "utf-8");
15957
+ }
15958
+ function hashArtifacts(repoRoot, artifacts) {
15959
+ const out = {};
15960
+ for (const uri of artifacts) {
15961
+ const safe = uri.replace(/\\/g, "/").replace(/^\/+/, "");
15962
+ if (/(^|\/)\.\.(\/|$)/.test(safe)) continue;
15963
+ try {
15964
+ const abs = join6(repoRoot, safe);
15965
+ const stat = statSync2(abs);
15966
+ if (!stat.isFile() || stat.size > ARTIFACT_HASH_MAX_BYTES) continue;
15967
+ out[uri] = createHash("sha256").update(readFileSync5(abs)).digest("hex");
15968
+ } catch {
15969
+ }
15970
+ }
15971
+ return out;
15972
+ }
15973
+ function ensureArtifactLfsFilter(repoRoot) {
15974
+ const path = join6(repoRoot, ".gitattributes");
15975
+ try {
15976
+ const existing = existsSync5(path) ? readFileSync5(path, "utf-8") : "";
15977
+ if (existing.includes(".pr-notes/artifacts/**")) return;
15978
+ const prefix = existing === "" || existing.endsWith("\n") ? "" : "\n";
15979
+ writeFileSync5(path, `${existing}${prefix}${LFS_FILTER_LINE}
15980
+ `, "utf-8");
15981
+ } catch {
15982
+ }
15983
+ }
15984
+ function listShardPaths(repoRoot, safeRel) {
15985
+ return listShardIndices(repoRoot, safeRel).map((i) => shardPath(repoRoot, safeRel, i));
15986
+ }
15987
+ function allShardPaths(repoRoot) {
15988
+ const root = join6(repoRoot, NOTES_SUBDIR);
15989
+ if (!existsSync5(root)) return [];
15990
+ const entries = readdirSync(root, { recursive: true });
15991
+ return entries.filter((e) => SHARD_RE.test(e)).map((e) => join6(root, e));
15992
+ }
15993
+ function persistOrDelete(path, log) {
15994
+ log.runs = log.runs.filter((run) => run.results.length > 0);
15995
+ if (log.runs.length === 0) {
15996
+ if (existsSync5(path)) unlinkSync(path);
15997
+ } else {
15998
+ writeLog(path, log);
15999
+ }
16000
+ }
16001
+ function removeNote(repoRoot, guid3, file2) {
16002
+ const paths = file2 !== void 0 ? listShardPaths(repoRoot, sanitizeRel(file2)) : allShardPaths(repoRoot);
16003
+ for (const path of paths) {
16004
+ const log = readLog(path);
16005
+ for (const run of log.runs) {
16006
+ const idx = run.results.findIndex((r) => r.guid === guid3);
16007
+ if (idx !== -1) {
16008
+ run.results.splice(idx, 1);
16009
+ persistOrDelete(path, log);
16010
+ return true;
16011
+ }
16012
+ }
16013
+ }
16014
+ return false;
16015
+ }
16016
+ function updateNote(repoRoot, guid3, patch, file2) {
16017
+ const paths = file2 !== void 0 ? listShardPaths(repoRoot, sanitizeRel(file2)) : allShardPaths(repoRoot);
16018
+ for (const path of paths) {
16019
+ const log = readLog(path);
16020
+ for (const run of log.runs) {
16021
+ const result = run.results.find((r) => r.guid === guid3);
16022
+ if (result !== void 0) {
16023
+ if (patch.body !== void 0) result.message = { text: patch.body, markdown: patch.body };
16024
+ if (patch.kind !== void 0) {
16025
+ result.properties = { ...result.properties, tags: [patch.kind] };
16026
+ result.level = patch.kind === "risk" ? "warning" : "none";
16027
+ }
16028
+ if (patch.confidence !== void 0) {
16029
+ result.properties = { ...result.properties, [CONFIDENCE_PROPERTY_KEY]: patch.confidence };
16030
+ }
16031
+ if (patch.rank !== void 0) result.rank = patch.rank;
16032
+ if (patch.ticket !== void 0) result.workItemUris = patch.ticket === "" ? void 0 : [patch.ticket];
16033
+ writeLog(path, log);
16034
+ return true;
16035
+ }
16036
+ }
16037
+ }
16038
+ return false;
16039
+ }
16040
+ function noteKey(r) {
16041
+ const loc = r.locations?.[0]?.physicalLocation;
16042
+ return JSON.stringify([
16043
+ loc?.artifactLocation?.uri,
16044
+ loc?.region?.startLine,
16045
+ loc?.region?.endLine,
16046
+ r.properties?.tags,
16047
+ r.message?.text
16048
+ ]);
16049
+ }
16050
+ function coalesceFile(repoRoot, file2) {
16051
+ const paths = listShardPaths(repoRoot, sanitizeRel(file2));
16052
+ const logs = paths.map((path) => ({ path, log: readLog(path) }));
16053
+ const refs = [];
16054
+ logs.forEach((entry, logIdx) => {
16055
+ entry.log.runs.forEach((run, runIdx) => {
16056
+ run.results.forEach((r, resultIdx) => {
16057
+ refs.push({ logIdx, runIdx, resultIdx, key: noteKey(r) });
16058
+ });
16059
+ });
16060
+ });
16061
+ const lastIndexForKey = /* @__PURE__ */ new Map();
16062
+ refs.forEach((ref, i) => lastIndexForKey.set(ref.key, i));
16063
+ const toRemove = refs.filter((ref, i) => lastIndexForKey.get(ref.key) !== i);
16064
+ if (toRemove.length === 0) return 0;
16065
+ const touched = /* @__PURE__ */ new Set();
16066
+ const byRun = /* @__PURE__ */ new Map();
16067
+ for (const ref of toRemove) {
16068
+ const k = `${String(ref.logIdx)}:${String(ref.runIdx)}`;
16069
+ const arr = byRun.get(k) ?? [];
16070
+ arr.push(ref.resultIdx);
16071
+ byRun.set(k, arr);
16072
+ touched.add(ref.logIdx);
16073
+ }
16074
+ for (const [k, idxs] of byRun) {
16075
+ const [logIdx, runIdx] = k.split(":").map(Number);
16076
+ const results = logs[logIdx].log.runs[runIdx].results;
16077
+ for (const idx of idxs.sort((a, b) => b - a)) results.splice(idx, 1);
16078
+ }
16079
+ for (const logIdx of touched) persistOrDelete(logs[logIdx].path, logs[logIdx].log);
16080
+ return toRemove.length;
16081
+ }
16082
+ function coalesceAll(repoRoot) {
16083
+ const root = join6(repoRoot, NOTES_SUBDIR);
16084
+ if (!existsSync5(root)) return 0;
16085
+ const sources = /* @__PURE__ */ new Set();
16086
+ for (const entry of readdirSync(root, { recursive: true })) {
16087
+ const m = SHARD_RE.exec(entry);
16088
+ if (m !== null) sources.add(entry.replace(SHARD_RE, "").replace(/\\/g, "/"));
16089
+ }
16090
+ let total = 0;
16091
+ for (const src of sources) total += coalesceFile(repoRoot, src);
16092
+ return total;
16093
+ }
16094
+ function readArtifactText(repoRoot, uri) {
16095
+ const safe = uri.replace(/\\/g, "/").replace(/^\/+/, "");
16096
+ if (/(^|\/)\.\.(\/|$)/.test(safe)) return void 0;
16097
+ try {
16098
+ const abs = join6(repoRoot, safe);
16099
+ const stat = statSync2(abs);
16100
+ if (!stat.isFile() || stat.size > ARTIFACT_MAX_BYTES) return void 0;
16101
+ const buf = readFileSync5(abs);
16102
+ if (buf.includes(0)) return void 0;
16103
+ return buf.toString("utf-8");
16104
+ } catch {
16105
+ return void 0;
16106
+ }
16107
+ }
16108
+ function readArtifacts(repoRoot, result) {
16109
+ const out = [];
16110
+ for (const att of result.attachments ?? []) {
16111
+ const uri = att.artifactLocation?.uri;
16112
+ if (typeof uri !== "string" || uri === "") continue;
16113
+ if (IMAGE_ARTIFACT_RE.test(uri)) {
16114
+ out.push({ uri, isImage: true });
16115
+ } else {
16116
+ out.push({ uri, content: readArtifactText(repoRoot, uri) });
16117
+ }
16118
+ }
16119
+ return out.length > 0 ? out : void 0;
16120
+ }
16121
+ function loadReviewNotesForFile(repoRoot, file2) {
16122
+ const safeRel = sanitizeRel(file2);
16123
+ const out = [];
16124
+ for (const path of listShardPaths(repoRoot, safeRel)) {
16125
+ let log;
16126
+ try {
16127
+ log = readLog(path);
16128
+ } catch {
16129
+ continue;
16130
+ }
16131
+ for (const run of log.runs) {
16132
+ const producer = run.tool.driver.name;
16133
+ for (const raw2 of run.results) {
16134
+ const r = raw2;
16135
+ const region = r.locations?.[0]?.physicalLocation?.region;
16136
+ const startLine = region?.startLine;
16137
+ const kind = r.properties?.tags?.[0];
16138
+ if (startLine === void 0 || kind === void 0 || !isNoteKind(kind)) continue;
16139
+ const confidence = r.properties?.[CONFIDENCE_PROPERTY_KEY];
16140
+ out.push({
16141
+ guid: r.guid,
16142
+ line: startLine,
16143
+ side: "new",
16144
+ kind,
16145
+ body: r.message?.text ?? "",
16146
+ confidence: typeof confidence === "number" ? confidence : void 0,
16147
+ producer: producer === "" ? void 0 : producer,
16148
+ snippet: region?.snippet?.text,
16149
+ artifacts: readArtifacts(repoRoot, r)
16150
+ });
16151
+ }
16152
+ }
16153
+ }
16154
+ return out;
16155
+ }
16156
+ var NOTES_SUBDIR, SHARD_RE, ARTIFACT_HASH_MAX_BYTES, LFS_FILTER_LINE, ARTIFACT_MAX_BYTES;
16157
+ var init_store = __esm({
16158
+ "src/review-notes/store.ts"() {
16159
+ "use strict";
16160
+ init_ids();
16161
+ init_sarif();
16162
+ init_types();
16163
+ init_view();
16164
+ NOTES_SUBDIR = join6(".pr-notes", "notes");
16165
+ SHARD_RE = /\.(\d{6})\.sarif$/;
16166
+ ARTIFACT_HASH_MAX_BYTES = 5e7;
16167
+ LFS_FILTER_LINE = ".pr-notes/artifacts/** filter=lfs diff=lfs merge=lfs -text";
16168
+ ARTIFACT_MAX_BYTES = 2e4;
16169
+ }
16170
+ });
16171
+
15722
16172
  // src/difftool/blob-store.ts
15723
16173
  var blob_store_exports = {};
15724
16174
  __export(blob_store_exports, {
@@ -15726,10 +16176,10 @@ __export(blob_store_exports, {
15726
16176
  readDifftoolBlob: () => readDifftoolBlob,
15727
16177
  writeDifftoolBlob: () => writeDifftoolBlob
15728
16178
  });
15729
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
15730
- import { join as join7 } from "path";
16179
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync7, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "fs";
16180
+ import { join as join9 } from "path";
15731
16181
  function blobDir(dataDir) {
15732
- return join7(dataDir, "difftool-blobs");
16182
+ return join9(dataDir, "difftool-blobs");
15733
16183
  }
15734
16184
  function blobName(fileId, side) {
15735
16185
  return `${fileId.replace(/[^a-z0-9]/gi, "")}-${side}`;
@@ -15737,14 +16187,14 @@ function blobName(fileId, side) {
15737
16187
  function writeDifftoolBlob(dataDir, fileId, side, bytes) {
15738
16188
  if (bytes.length === 0) return;
15739
16189
  const dir = blobDir(dataDir);
15740
- mkdirSync5(dir, { recursive: true });
15741
- writeFileSync6(join7(dir, blobName(fileId, side)), bytes);
16190
+ mkdirSync6(dir, { recursive: true });
16191
+ writeFileSync7(join9(dir, blobName(fileId, side)), bytes);
15742
16192
  }
15743
16193
  function readDifftoolBlob(dataDir, fileId, side) {
15744
- const path = join7(blobDir(dataDir), blobName(fileId, side));
15745
- if (!existsSync6(path)) return null;
16194
+ const path = join9(blobDir(dataDir), blobName(fileId, side));
16195
+ if (!existsSync8(path)) return null;
15746
16196
  try {
15747
- return readFileSync6(path);
16197
+ return readFileSync7(path);
15748
16198
  } catch {
15749
16199
  return null;
15750
16200
  }
@@ -15850,9 +16300,9 @@ __export(difftool_exports, {
15850
16300
  registerDifftool: () => registerDifftool,
15851
16301
  unregisterDifftool: () => unregisterDifftool
15852
16302
  });
15853
- import { spawnSync as spawnSync9 } from "child_process";
16303
+ import { spawnSync as spawnSync10 } from "child_process";
15854
16304
  function git2(args, cwd) {
15855
- const r = spawnSync9("git", args, { encoding: "utf-8", cwd });
16305
+ const r = spawnSync10("git", args, { encoding: "utf-8", cwd });
15856
16306
  if (r.error !== void 0) {
15857
16307
  const code = r.error.code ?? "UNKNOWN";
15858
16308
  return { status: -1, stdout: "", stderr: `spawn git failed (${code}): ${r.error.message}` };
@@ -15918,6 +16368,340 @@ var init_difftool = __esm({
15918
16368
  }
15919
16369
  });
15920
16370
 
16371
+ // src/review-notes/instructions.ts
16372
+ function reviewNoteInstructions() {
16373
+ return [
16374
+ "# Emitting AI review notes (`.pr-notes/`)",
16375
+ "",
16376
+ "As you write or change code, leave **line-anchored review notes** that explain",
16377
+ "your reasoning and prove your work \u2014 the rationale a careful reviewer (human or",
16378
+ "the next AI session) would want, anchored to the exact lines it concerns. Notes",
16379
+ "are committed to the repo as tool-neutral SARIF under `.pr-notes/` and shown",
16380
+ "review-comment-style in Glassbox. Write them **while editing**, at the moment of",
16381
+ "richest context \u2014 not reconstructed afterward.",
16382
+ "",
16383
+ "## When to emit a note",
16384
+ "",
16385
+ 'Emit one when a future reader would otherwise have to ask "why?". Good triggers:',
16386
+ "",
16387
+ "- A **non-obvious decision** \u2014 why this approach over the obvious one.",
16388
+ "- **Proof** a change is correct \u2014 the reasoning, test output, or invariant that backs it.",
16389
+ "- An **assumption** you relied on that could later be wrong.",
16390
+ "- An **alternative you considered and rejected**, and why.",
16391
+ "- A **risk** or sharp edge a reviewer should scrutinize.",
16392
+ "- **Test evidence** demonstrating the change works.",
16393
+ "",
16394
+ "Do **not** narrate the obvious or restate what the diff already says. One note per",
16395
+ "genuine decision or claim \u2014 not per line.",
16396
+ "",
16397
+ "## How to emit a note",
16398
+ "",
16399
+ "Shell out to the `glassbox note` CLI (it owns the on-disk format, fingerprint,",
16400
+ "and commit provenance \u2014 you never write the SARIF by hand):",
16401
+ "",
16402
+ "```",
16403
+ "glassbox note add \\",
16404
+ " --file <repo-relative path> \\",
16405
+ " --lines <A[-B]> \\",
16406
+ ` --kind <${NOTE_KINDS.join("|")}> \\`,
16407
+ ' --body - \\ # body read from stdin (use --body "text" for short notes)',
16408
+ " [--confidence 0..1] [--rank 0..100] [--ticket <id|url>] \\",
16409
+ ' [--producer "<your tool/agent name>"] [--producer-version <v>]',
16410
+ "```",
16411
+ "",
16412
+ "Always set `--producer` so the note records who wrote it, and `--ticket` when the",
16413
+ "work traces to a tracked task. Keep the body focused: the claim and its support.",
16414
+ "",
16415
+ "## Revise and consolidate",
16416
+ "",
16417
+ "Notes should reflect the **final** state of your change, not an obsolete",
16418
+ "intermediate step:",
16419
+ "",
16420
+ "- `glassbox note add` prints a stable note id (guid).",
16421
+ "- `glassbox note update --id <guid> [--body -|--kind \u2026|--confidence \u2026|--rank \u2026|--ticket \u2026]`",
16422
+ " to correct a note as the work evolves.",
16423
+ "- `glassbox note remove --id <guid>` to drop one that no longer applies.",
16424
+ "- When the task is done, run `glassbox note coalesce` to drop redundant notes",
16425
+ " (identical anchor + kind + body, keeping the most recent).",
16426
+ "",
16427
+ "## Final consolidation pass",
16428
+ "",
16429
+ "After the mechanical `coalesce`, do one **cross-cutting pass over all the notes",
16430
+ "you wrote this session** \u2014 the relationships that only become visible once the",
16431
+ "whole change is in front of you, not while editing a single file. `coalesce`",
16432
+ "only catches byte-identical duplicates; this pass is the judgment it cannot make:",
16433
+ "",
16434
+ "- **Merge near-duplicates.** Two notes that make the *same* point in different",
16435
+ " words (e.g. the same rationale restated on two files) aren't caught by",
16436
+ " `coalesce`. Keep the clearest one \u2014 `update` it to the best wording, widen its",
16437
+ " anchor or `--ticket` if helpful \u2014 and `remove` the rest.",
16438
+ "- **Link related notes across files.** When several notes are facets of one",
16439
+ " decision spanning multiple files, make that explicit: name the related file(s)",
16440
+ ' and the connecting idea in each note body (a short "see also `path/to/file`"',
16441
+ " line), and give them a shared `--ticket` so a reader can pivot between them.",
16442
+ " Bodies render as Markdown, so an inline `` `path` `` reference reads cleanly.",
16443
+ "- **Prune what the finished change made obvious.** A note that justified an",
16444
+ " intermediate step the final diff no longer shows is noise \u2014 `remove` it.",
16445
+ "",
16446
+ "The goal is the smallest set of notes that still proves the work: no restated",
16447
+ "point twice, and every cross-file relationship spelled out where a reviewer will",
16448
+ "see it.",
16449
+ "",
16450
+ "If your tool can't shell out, write the SARIF directly under `.pr-notes/` per the",
16451
+ "format in `docs/20-ai-review-notes.md` \u2014 but prefer the CLI."
16452
+ ].join("\n");
16453
+ }
16454
+ var init_instructions = __esm({
16455
+ "src/review-notes/instructions.ts"() {
16456
+ "use strict";
16457
+ init_types();
16458
+ }
16459
+ });
16460
+
16461
+ // src/review-notes/cli.ts
16462
+ var cli_exports = {};
16463
+ __export(cli_exports, {
16464
+ parseNoteAdd: () => parseNoteAdd,
16465
+ runNoteCli: () => runNoteCli
16466
+ });
16467
+ import { spawnSync as spawnSync11 } from "child_process";
16468
+ import { relative as relative2, resolve as resolve9 } from "path";
16469
+ function parseFlags(args) {
16470
+ const flags = /* @__PURE__ */ new Map();
16471
+ for (let i = 0; i < args.length; i++) {
16472
+ const a = args[i];
16473
+ if (!a.startsWith("--")) throw new Error(`unexpected argument: ${a}`);
16474
+ const key = a.slice(2);
16475
+ const next = args.at(i + 1);
16476
+ if (next === void 0 || next.startsWith("--")) throw new Error(`missing value for --${key}`);
16477
+ flags.set(key, next);
16478
+ i++;
16479
+ }
16480
+ return flags;
16481
+ }
16482
+ function collectRepeatable(args, name) {
16483
+ const out = [];
16484
+ for (let i = 0; i < args.length; i++) {
16485
+ if (args[i] === `--${name}`) {
16486
+ const v = args.at(i + 1);
16487
+ if (v !== void 0 && !v.startsWith("--")) out.push(v);
16488
+ }
16489
+ }
16490
+ return out;
16491
+ }
16492
+ function noteUsage() {
16493
+ return `glassbox note \u2014 AI-authored, line-anchored review notes (docs/20)
16494
+
16495
+ Usage:
16496
+ glassbox note add --file <path> --lines <A[-B]> --kind <kind> --body <text|-> [options]
16497
+ glassbox note update --id <guid> [--file <path>] [--body <text|->] [--kind <kind>] [--confidence <0..1>] [--rank <0..100>] [--ticket <id>]
16498
+ glassbox note remove --id <guid> [--file <path>]
16499
+ glassbox note coalesce [--file <path>]
16500
+ glassbox note instructions Print the inbound AI-instructions contract (for orchestrators to inject)
16501
+
16502
+ add \u2014 required:
16503
+ --file <path> Source file the note anchors to (relative to cwd or absolute)
16504
+ --lines <A[-B]> 1-based line or line range (e.g. 42 or 42-50)
16505
+ --kind <kind> One of: ${NOTE_KINDS.join(", ")}
16506
+ --body <text|-> Markdown body; pass - to read the body from stdin
16507
+
16508
+ add \u2014 options:
16509
+ --confidence <0..1> Author confidence
16510
+ --rank <0..100> Importance
16511
+ --ticket <id|url> Linked ticket
16512
+ --producer <name> Producing tool/agent (e.g. "Claude Code", "Hot Sheet")
16513
+ --producer-version <v>
16514
+ --artifact <path> Attach a committed proof artifact (test output, log,
16515
+ diagram source); repeatable, repo-relative path
16516
+
16517
+ update/remove use the guid returned by 'add' (--file scopes the search; omit to search all notes).
16518
+ coalesce drops redundant notes (identical anchor + kind + body), keeping the most recent.
16519
+ `;
16520
+ }
16521
+ function parseInteger(label, value) {
16522
+ const n = Number(value);
16523
+ if (!Number.isInteger(n)) throw new Error(`${label} must be an integer, got "${value}"`);
16524
+ return n;
16525
+ }
16526
+ function parseNoteAdd(args) {
16527
+ const flags = parseFlags(args);
16528
+ const file2 = flags.get("file");
16529
+ const lines = flags.get("lines");
16530
+ const kindRaw = flags.get("kind");
16531
+ if (file2 === void 0) throw new Error("--file is required");
16532
+ if (lines === void 0) throw new Error("--lines is required");
16533
+ if (kindRaw === void 0) throw new Error("--kind is required");
16534
+ if (!isNoteKind(kindRaw)) throw new Error(`--kind must be one of: ${NOTE_KINDS.join(", ")}`);
16535
+ const lineMatch = /^(\d+)(?:-(\d+))?$/.exec(lines);
16536
+ if (lineMatch === null) throw new Error(`--lines must be A or A-B (e.g. 42 or 42-50), got "${lines}"`);
16537
+ const startLine = parseInteger("--lines start", lineMatch[1]);
16538
+ const endRaw = lineMatch.at(2);
16539
+ const endLine = endRaw !== void 0 ? parseInteger("--lines end", endRaw) : startLine;
16540
+ if (startLine < 1 || endLine < startLine) throw new Error("--lines must be 1-based with start <= end");
16541
+ const parsed = { file: file2, startLine, endLine, kind: kindRaw, artifacts: collectRepeatable(args, "artifact"), bodyStdin: false };
16542
+ const confidence = flags.get("confidence");
16543
+ if (confidence !== void 0) {
16544
+ const c = Number(confidence);
16545
+ if (Number.isNaN(c) || c < 0 || c > 1) throw new Error("--confidence must be between 0 and 1");
16546
+ parsed.confidence = c;
16547
+ }
16548
+ const rank = flags.get("rank");
16549
+ if (rank !== void 0) {
16550
+ const r = Number(rank);
16551
+ if (!Number.isInteger(r) || r < 0 || r > 100) throw new Error("--rank must be an integer 0..100");
16552
+ parsed.rank = r;
16553
+ }
16554
+ if (flags.has("ticket")) parsed.ticket = flags.get("ticket");
16555
+ if (flags.has("producer")) parsed.producer = flags.get("producer");
16556
+ if (flags.has("producer-version")) parsed.producerVersion = flags.get("producer-version");
16557
+ const body = flags.get("body");
16558
+ if (body === void 0) throw new Error("--body is required (text, or - to read stdin)");
16559
+ if (body === "-") parsed.bodyStdin = true;
16560
+ else parsed.body = body;
16561
+ return parsed;
16562
+ }
16563
+ function readStdin() {
16564
+ return new Promise((res, rej) => {
16565
+ let data = "";
16566
+ process.stdin.setEncoding("utf-8");
16567
+ process.stdin.on("data", (chunk) => {
16568
+ data += String(chunk);
16569
+ });
16570
+ process.stdin.on("end", () => {
16571
+ res(data);
16572
+ });
16573
+ process.stdin.on("error", rej);
16574
+ });
16575
+ }
16576
+ function findRepoRoot(cwd) {
16577
+ try {
16578
+ const res = spawnSync11("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf-8" });
16579
+ if (res.status === 0) {
16580
+ const top = res.stdout.trim();
16581
+ if (top !== "") return top;
16582
+ }
16583
+ } catch {
16584
+ }
16585
+ return cwd;
16586
+ }
16587
+ function toRepoRelative(repoRoot, cwd, file2) {
16588
+ const abs = resolve9(cwd, file2);
16589
+ const rel = relative2(repoRoot, abs).replace(/\\/g, "/");
16590
+ if (rel === "" || rel.startsWith("../")) {
16591
+ throw new Error(`--file must be inside the repository: ${file2}`);
16592
+ }
16593
+ return rel;
16594
+ }
16595
+ async function runAdd(args, cwd) {
16596
+ const parsed = parseNoteAdd(args);
16597
+ const body = parsed.bodyStdin ? await readStdin() : parsed.body;
16598
+ if (body === void 0 || body.trim() === "") throw new Error("note body is empty");
16599
+ const repoRoot = findRepoRoot(cwd);
16600
+ const input = {
16601
+ file: toRepoRelative(repoRoot, cwd, parsed.file),
16602
+ startLine: parsed.startLine,
16603
+ endLine: parsed.endLine,
16604
+ body,
16605
+ kind: parsed.kind,
16606
+ confidence: parsed.confidence,
16607
+ rank: parsed.rank,
16608
+ ticket: parsed.ticket,
16609
+ producer: parsed.producer,
16610
+ producerVersion: parsed.producerVersion,
16611
+ artifacts: parsed.artifacts.length > 0 ? parsed.artifacts : void 0
16612
+ };
16613
+ warnIfPrNotesIgnored(repoRoot);
16614
+ const { path, guid: guid3 } = writeReviewNote(repoRoot, input);
16615
+ console.log(`Wrote review note ${guid3} -> ${relative2(repoRoot, path)}`);
16616
+ }
16617
+ function scopedFile(flags, repoRoot, cwd) {
16618
+ const file2 = flags.get("file");
16619
+ return file2 === void 0 ? void 0 : toRepoRelative(repoRoot, cwd, file2);
16620
+ }
16621
+ function runRemove(args, cwd) {
16622
+ const flags = parseFlags(args);
16623
+ const id = flags.get("id");
16624
+ if (id === void 0) throw new Error("--id <guid> is required");
16625
+ const repoRoot = findRepoRoot(cwd);
16626
+ const removed = removeNote(repoRoot, id, scopedFile(flags, repoRoot, cwd));
16627
+ if (!removed) throw new Error(`no review note found with id ${id}`);
16628
+ console.log(`Removed review note ${id}`);
16629
+ }
16630
+ async function runUpdate(args, cwd) {
16631
+ const flags = parseFlags(args);
16632
+ const id = flags.get("id");
16633
+ if (id === void 0) throw new Error("--id <guid> is required");
16634
+ const patch = {};
16635
+ const bodyFlag = flags.get("body");
16636
+ if (bodyFlag !== void 0) patch.body = bodyFlag === "-" ? await readStdin() : bodyFlag;
16637
+ const kind = flags.get("kind");
16638
+ if (kind !== void 0) {
16639
+ if (!isNoteKind(kind)) throw new Error(`--kind must be one of: ${NOTE_KINDS.join(", ")}`);
16640
+ patch.kind = kind;
16641
+ }
16642
+ const confidence = flags.get("confidence");
16643
+ if (confidence !== void 0) {
16644
+ const c = Number(confidence);
16645
+ if (Number.isNaN(c) || c < 0 || c > 1) throw new Error("--confidence must be between 0 and 1");
16646
+ patch.confidence = c;
16647
+ }
16648
+ const rank = flags.get("rank");
16649
+ if (rank !== void 0) {
16650
+ const r = Number(rank);
16651
+ if (!Number.isInteger(r) || r < 0 || r > 100) throw new Error("--rank must be an integer 0..100");
16652
+ patch.rank = r;
16653
+ }
16654
+ if (flags.has("ticket")) patch.ticket = flags.get("ticket");
16655
+ if (Object.keys(patch).length === 0) throw new Error("nothing to update \u2014 pass at least one of --body/--kind/--confidence/--rank/--ticket");
16656
+ const repoRoot = findRepoRoot(cwd);
16657
+ const updated = updateNote(repoRoot, id, patch, scopedFile(flags, repoRoot, cwd));
16658
+ if (!updated) throw new Error(`no review note found with id ${id}`);
16659
+ console.log(`Updated review note ${id}`);
16660
+ }
16661
+ function runCoalesce(args, cwd) {
16662
+ const flags = parseFlags(args);
16663
+ const repoRoot = findRepoRoot(cwd);
16664
+ const file2 = flags.get("file");
16665
+ const removed = file2 !== void 0 ? coalesceFile(repoRoot, toRepoRelative(repoRoot, cwd, file2)) : coalesceAll(repoRoot);
16666
+ console.log(`Coalesced review notes \u2014 removed ${String(removed)} redundant note(s)`);
16667
+ }
16668
+ async function runNoteCli(args, ctx = {}) {
16669
+ const cwd = ctx.cwd ?? process.cwd();
16670
+ const sub = args.at(0);
16671
+ if (sub === void 0 || sub === "help" || sub === "--help" || sub === "-h") {
16672
+ console.log(noteUsage());
16673
+ return;
16674
+ }
16675
+ const rest = args.slice(1);
16676
+ switch (sub) {
16677
+ case "add":
16678
+ await runAdd(rest, cwd);
16679
+ return;
16680
+ case "remove":
16681
+ runRemove(rest, cwd);
16682
+ return;
16683
+ case "update":
16684
+ await runUpdate(rest, cwd);
16685
+ return;
16686
+ case "coalesce":
16687
+ runCoalesce(rest, cwd);
16688
+ return;
16689
+ case "instructions":
16690
+ console.log(reviewNoteInstructions());
16691
+ return;
16692
+ default:
16693
+ throw new Error(`unknown 'note' subcommand: ${sub} (expected add/update/remove/coalesce/instructions)`);
16694
+ }
16695
+ }
16696
+ var init_cli = __esm({
16697
+ "src/review-notes/cli.ts"() {
16698
+ "use strict";
16699
+ init_instructions();
16700
+ init_store();
16701
+ init_types();
16702
+ }
16703
+ });
16704
+
15921
16705
  // src/git/difftool-discovery.ts
15922
16706
  var difftool_discovery_exports = {};
15923
16707
  __export(difftool_discovery_exports, {
@@ -15931,22 +16715,22 @@ __export(difftool_discovery_exports, {
15931
16715
  tryAcquireStartingLock: () => tryAcquireStartingLock,
15932
16716
  writeDiscovery: () => writeDiscovery
15933
16717
  });
15934
- import { existsSync as existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync16, rmSync as rmSync5, statSync as statSync2, writeFileSync as writeFileSync11 } from "fs";
16718
+ import { existsSync as existsSync15, mkdirSync as mkdirSync12, readFileSync as readFileSync18, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync12 } from "fs";
15935
16719
  import { homedir as homedir4 } from "os";
15936
- import { join as join16 } from "path";
16720
+ import { join as join18 } from "path";
15937
16721
  function difftoolHome() {
15938
- return join16(homedir4(), ".glassbox");
16722
+ return join18(homedir4(), ".glassbox");
15939
16723
  }
15940
16724
  function discoveryPath(home = difftoolHome()) {
15941
- return join16(home, "difftool.lock");
16725
+ return join18(home, "difftool.lock");
15942
16726
  }
15943
16727
  function startingLockPath(home = difftoolHome()) {
15944
- return join16(home, "difftool-starting.lock");
16728
+ return join18(home, "difftool-starting.lock");
15945
16729
  }
15946
- function parseDiscovery(raw) {
16730
+ function parseDiscovery(raw2) {
15947
16731
  let parsed;
15948
16732
  try {
15949
- parsed = JSON.parse(raw);
16733
+ parsed = JSON.parse(raw2);
15950
16734
  } catch {
15951
16735
  return null;
15952
16736
  }
@@ -15955,16 +16739,16 @@ function parseDiscovery(raw) {
15955
16739
  }
15956
16740
  function readDiscovery(home = difftoolHome()) {
15957
16741
  const path = discoveryPath(home);
15958
- if (!existsSync13(path)) return null;
16742
+ if (!existsSync15(path)) return null;
15959
16743
  try {
15960
- return parseDiscovery(readFileSync16(path, "utf-8"));
16744
+ return parseDiscovery(readFileSync18(path, "utf-8"));
15961
16745
  } catch {
15962
16746
  return null;
15963
16747
  }
15964
16748
  }
15965
16749
  function writeDiscovery(port, home = difftoolHome()) {
15966
- mkdirSync11(home, { recursive: true });
15967
- writeFileSync11(discoveryPath(home), JSON.stringify({ port, pid: process.pid }));
16750
+ mkdirSync12(home, { recursive: true });
16751
+ writeFileSync12(discoveryPath(home), JSON.stringify({ port, pid: process.pid }));
15968
16752
  }
15969
16753
  function clearDiscovery(home = difftoolHome()) {
15970
16754
  try {
@@ -15973,17 +16757,17 @@ function clearDiscovery(home = difftoolHome()) {
15973
16757
  }
15974
16758
  }
15975
16759
  function tryAcquireStartingLock(home = difftoolHome()) {
15976
- mkdirSync11(home, { recursive: true });
16760
+ mkdirSync12(home, { recursive: true });
15977
16761
  const path = startingLockPath(home);
15978
16762
  try {
15979
- writeFileSync11(path, String(process.pid), { flag: "wx" });
16763
+ writeFileSync12(path, String(process.pid), { flag: "wx" });
15980
16764
  return true;
15981
16765
  } catch {
15982
16766
  try {
15983
- const ageMs = Date.now() - statSync2(path).mtimeMs;
16767
+ const ageMs = Date.now() - statSync4(path).mtimeMs;
15984
16768
  if (ageMs > STARTING_LOCK_STALE_MS) {
15985
16769
  rmSync5(path, { force: true });
15986
- writeFileSync11(path, String(process.pid), { flag: "wx" });
16770
+ writeFileSync12(path, String(process.pid), { flag: "wx" });
15987
16771
  return true;
15988
16772
  }
15989
16773
  } catch {
@@ -16013,9 +16797,9 @@ var init_difftool_discovery = __esm({
16013
16797
  // src/cli.ts
16014
16798
  init_connection();
16015
16799
  init_queries();
16016
- import { existsSync as existsSync14, mkdirSync as mkdirSync12, realpathSync, statSync as statSync3 } from "fs";
16800
+ import { existsSync as existsSync16, mkdirSync as mkdirSync13, realpathSync, statSync as statSync5 } from "fs";
16017
16801
  import { tmpdir as tmpdir2 } from "os";
16018
- import { basename as basename2, join as join17, resolve as resolve8 } from "path";
16802
+ import { basename as basename2, join as join19, resolve as resolve10 } from "path";
16019
16803
 
16020
16804
  // src/debug.ts
16021
16805
  var debugEnabled = false;
@@ -16059,8 +16843,8 @@ var GLOBAL_CONFIG_PATH = join2(GLOBAL_CONFIG_DIR, "config.json");
16059
16843
  function readGlobalConfig() {
16060
16844
  try {
16061
16845
  if (existsSync(GLOBAL_CONFIG_PATH)) {
16062
- const raw = JSON.parse(readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
16063
- const parsed = GlobalConfigSchema.safeParse(raw);
16846
+ const raw2 = JSON.parse(readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
16847
+ const parsed = GlobalConfigSchema.safeParse(raw2);
16064
16848
  if (parsed.success) return parsed.data;
16065
16849
  }
16066
16850
  } catch {
@@ -16187,12 +16971,16 @@ var AIModelSchema = external_exports.object({
16187
16971
  contextWindow: external_exports.number(),
16188
16972
  isDefault: external_exports.boolean()
16189
16973
  });
16190
- var AIPlatformSchema = external_exports.enum(["anthropic", "openai", "google"]);
16974
+ var AIPlatformSchema = external_exports.enum(["anthropic", "openai", "google", "local", "apple"]);
16975
+ var KEYLESS_PLATFORMS = /* @__PURE__ */ new Set(["local", "apple"]);
16191
16976
  var PLATFORMS = {
16192
16977
  anthropic: "Anthropic",
16193
16978
  openai: "OpenAI",
16194
- google: "Google"
16979
+ google: "Google",
16980
+ local: "Local",
16981
+ apple: "Apple"
16195
16982
  };
16983
+ var APPLE_ON_DEVICE_MODEL_ID = "apple-on-device";
16196
16984
  var MODELS = {
16197
16985
  anthropic: [
16198
16986
  { id: "claude-opus-4-8", name: "Claude Opus 4.8", contextWindow: 1e6, isDefault: false },
@@ -16206,12 +16994,28 @@ var MODELS = {
16206
16994
  google: [
16207
16995
  { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", contextWindow: 1e6, isDefault: true },
16208
16996
  { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", contextWindow: 1e6, isDefault: false }
16997
+ ],
16998
+ // Local models are server-specific and discovered live from the configured
16999
+ // endpoint; this is only the fallback default when discovery is unavailable.
17000
+ local: [
17001
+ { id: "llama3.1", name: "Llama 3.1", contextWindow: 8192, isDefault: true }
17002
+ ],
17003
+ // On-device Apple Foundation Models — a single fixed entry (no discovery).
17004
+ // The on-device model has a small context window, so batch conservatively.
17005
+ apple: [
17006
+ { id: APPLE_ON_DEVICE_MODEL_ID, name: "Apple On-Device", contextWindow: 4096, isDefault: true }
16209
17007
  ]
16210
17008
  };
16211
17009
  var ENV_KEY_NAMES = {
16212
17010
  anthropic: "ANTHROPIC_API_KEY",
16213
17011
  openai: "OPENAI_API_KEY",
16214
- google: "GEMINI_API_KEY"
17012
+ google: "GEMINI_API_KEY",
17013
+ // Optional — most local servers (Ollama) need no key; some do.
17014
+ local: "GLASSBOX_LOCAL_API_KEY",
17015
+ // Apple Foundation Models are keyless (on-device). This name is never read
17016
+ // for auth — `apple` is in KEYLESS_PLATFORMS — but the record requires an
17017
+ // entry per platform.
17018
+ apple: "GLASSBOX_APPLE_API_KEY"
16215
17019
  };
16216
17020
  function getDefaultModel(platform) {
16217
17021
  const models = MODELS[platform];
@@ -16220,7 +17024,10 @@ function getDefaultModel(platform) {
16220
17024
  }
16221
17025
  function getModelContextWindow(platform, modelId) {
16222
17026
  const model = MODELS[platform].find((m) => m.id === modelId);
16223
- return model ? model.contextWindow : 128e3;
17027
+ if (model) return model.contextWindow;
17028
+ if (platform === "local") return 8192;
17029
+ if (platform === "apple") return 4096;
17030
+ return 128e3;
16224
17031
  }
16225
17032
  function modelFamily(id) {
16226
17033
  const lower = id.toLowerCase();
@@ -16273,8 +17080,8 @@ function saveAPIKey(platform, key, storage) {
16273
17080
  if (storage === "keychain") {
16274
17081
  saveKeyToKeychain(platform, key);
16275
17082
  } else {
16276
- updateGlobalConfig((raw) => {
16277
- const parsed = ConfigFileSchema.safeParse(raw);
17083
+ updateGlobalConfig((raw2) => {
17084
+ const parsed = ConfigFileSchema.safeParse(raw2);
16278
17085
  const cfg = parsed.success ? parsed.data : {};
16279
17086
  cfg.ai ??= {};
16280
17087
  cfg.ai.keys ??= {};
@@ -16298,8 +17105,8 @@ function deleteAPIKey(platform) {
16298
17105
  } catch {
16299
17106
  }
16300
17107
  if (readConfigFile().ai?.keys === void 0) return;
16301
- updateGlobalConfig((raw) => {
16302
- const parsed = ConfigFileSchema.safeParse(raw);
17108
+ updateGlobalConfig((raw2) => {
17109
+ const parsed = ConfigFileSchema.safeParse(raw2);
16303
17110
  const cfg = parsed.success ? parsed.data : {};
16304
17111
  if (cfg.ai?.keys !== void 0) {
16305
17112
  cfg.ai.keys[platform] = "";
@@ -16320,11 +17127,18 @@ function detectAvailablePlatforms() {
16320
17127
  }
16321
17128
 
16322
17129
  // src/ai/config.ts
17130
+ var DEFAULT_LOCAL_ENDPOINT = "http://localhost:11434/v1";
17131
+ function resolveLocalEndpoint() {
17132
+ const configured = readConfigFile().ai?.localEndpoint?.trim();
17133
+ const base = configured !== void 0 && configured !== "" ? configured : DEFAULT_LOCAL_ENDPOINT;
17134
+ return base.replace(/\/+$/, "");
17135
+ }
16323
17136
  var ConfigFileSchema = external_exports.object({
16324
17137
  ai: external_exports.object({
16325
17138
  platform: external_exports.string().optional(),
16326
17139
  model: external_exports.string().optional(),
16327
- keys: external_exports.record(external_exports.string(), external_exports.string()).optional()
17140
+ keys: external_exports.record(external_exports.string(), external_exports.string()).optional(),
17141
+ localEndpoint: external_exports.string().optional()
16328
17142
  }).optional(),
16329
17143
  guidedReview: external_exports.object({
16330
17144
  enabled: external_exports.boolean().optional(),
@@ -16332,25 +17146,31 @@ var ConfigFileSchema = external_exports.object({
16332
17146
  }).optional()
16333
17147
  }).loose();
16334
17148
  function readConfigFile() {
16335
- const raw = readGlobalConfig();
16336
- const parsed = ConfigFileSchema.safeParse(raw);
17149
+ const raw2 = readGlobalConfig();
17150
+ const parsed = ConfigFileSchema.safeParse(raw2);
16337
17151
  return parsed.success ? parsed.data : {};
16338
17152
  }
16339
17153
  function loadAIConfig() {
16340
17154
  const config2 = readConfigFile();
16341
17155
  const platformRaw = config2.ai?.platform ?? "anthropic";
16342
17156
  const platform = AIPlatformSchema.safeParse(platformRaw).success ? AIPlatformSchema.parse(platformRaw) : "anthropic";
16343
- const model = resolveModelId(platform, config2.ai?.model ?? getDefaultModel(platform));
17157
+ const rawModel = config2.ai?.model ?? getDefaultModel(platform);
17158
+ const model = KEYLESS_PLATFORMS.has(platform) ? rawModel : resolveModelId(platform, rawModel);
16344
17159
  const { key, source } = resolveAPIKey(platform);
16345
- return { platform, model, apiKey: key, keySource: source };
17160
+ const baseUrl = platform === "local" ? resolveLocalEndpoint() : void 0;
17161
+ return { platform, model, apiKey: key, keySource: source, baseUrl };
16346
17162
  }
16347
- function saveAIConfigPreferences(platform, model) {
17163
+ function saveAIConfigPreferences(platform, model, opts = {}) {
16348
17164
  updateGlobalConfig((config2) => {
16349
17165
  const parsed = ConfigFileSchema.safeParse(config2);
16350
17166
  const cfg = parsed.success ? parsed.data : {};
16351
17167
  cfg.ai ??= {};
16352
17168
  cfg.ai.platform = platform;
16353
17169
  cfg.ai.model = model;
17170
+ if (opts.localEndpoint !== void 0) {
17171
+ const trimmed = opts.localEndpoint.trim();
17172
+ cfg.ai.localEndpoint = trimmed === "" ? void 0 : trimmed;
17173
+ }
16354
17174
  return cfg;
16355
17175
  });
16356
17176
  }
@@ -16495,14 +17315,14 @@ async function getUserPreferences() {
16495
17315
  last_image_mode: "metadata"
16496
17316
  };
16497
17317
  if (result.rows.length === 0) return defaults;
16498
- const raw = result.rows[0];
17318
+ const raw2 = result.rows[0];
16499
17319
  const merged = {
16500
- sort_mode: typeof raw.sort_mode === "string" ? raw.sort_mode : defaults.sort_mode,
16501
- risk_sort_dimension: typeof raw.risk_sort_dimension === "string" ? raw.risk_sort_dimension : defaults.risk_sort_dimension,
16502
- show_risk_scores: typeof raw.show_risk_scores === "boolean" ? raw.show_risk_scores : defaults.show_risk_scores,
16503
- ignore_whitespace: typeof raw.ignore_whitespace === "boolean" ? raw.ignore_whitespace : defaults.ignore_whitespace,
16504
- svg_view_mode: typeof raw.svg_view_mode === "string" ? raw.svg_view_mode : defaults.svg_view_mode,
16505
- last_image_mode: typeof raw.last_image_mode === "string" ? raw.last_image_mode : defaults.last_image_mode
17320
+ sort_mode: typeof raw2.sort_mode === "string" ? raw2.sort_mode : defaults.sort_mode,
17321
+ risk_sort_dimension: typeof raw2.risk_sort_dimension === "string" ? raw2.risk_sort_dimension : defaults.risk_sort_dimension,
17322
+ show_risk_scores: typeof raw2.show_risk_scores === "boolean" ? raw2.show_risk_scores : defaults.show_risk_scores,
17323
+ ignore_whitespace: typeof raw2.ignore_whitespace === "boolean" ? raw2.ignore_whitespace : defaults.ignore_whitespace,
17324
+ svg_view_mode: typeof raw2.svg_view_mode === "string" ? raw2.svg_view_mode : defaults.svg_view_mode,
17325
+ last_image_mode: typeof raw2.last_image_mode === "string" ? raw2.last_image_mode : defaults.last_image_mode
16506
17326
  };
16507
17327
  return UserPreferencesSchema.parse(merged);
16508
17328
  }
@@ -16526,6 +17346,17 @@ async function saveUserPreferences(prefs) {
16526
17346
 
16527
17347
  // src/demo.ts
16528
17348
  init_queries();
17349
+ function demoReviewNotes(filePath) {
17350
+ if (filePath !== "src/auth/session.ts") return [];
17351
+ return [
17352
+ { guid: "demo-note-rationale", line: 14, side: "new", kind: "rationale", body: "`createSession` is **async** now because session state moved from an in-process `Map` to Redis; callers must `await` it.", confidence: 0.9, producer: "Claude Code" },
17353
+ { guid: "demo-note-proof", line: 23, side: "new", kind: "proof", body: "The TTL is written atomically with the value via the EX option, so a session can never be stored without an expiry.", producer: "Claude Code", artifacts: [{ uri: ".pr-notes/artifacts/session-ttl.test.txt", content: "PASS session.test.ts\n \u2713 createSession writes value and TTL atomically (4 ms)\n \u2713 a session always has an expiry (2 ms)\n\nTests: 2 passed, 2 total" }] },
17354
+ { guid: "demo-note-risk", line: 31, side: "new", kind: "risk", body: "expiresAt round-trips through JSON as a string and is re-wrapped in Date() \u2014 verify the comparison holds in your runtime.", confidence: 0.6, producer: "Claude Code", artifacts: [{ uri: "assets/demo-annotations.png", isImage: true }] },
17355
+ // Re-anchoring showcase (P3): authored against a 16-byte id, but the code
17356
+ // now uses 32 bytes, so the note no longer matches and renders as stale.
17357
+ { guid: "demo-note-stale", line: 15, side: "new", kind: "assumption", body: "Assumed a 16-byte token id here \u2014 the implementation has since changed, so this note is out of date.", producer: "Claude Code", snippet: " const id = randomBytes(16).toString('hex');" }
17358
+ ];
17359
+ }
16529
17360
  var DEMO_SCENARIOS = [
16530
17361
  { id: 1, label: "Main UI with guided review notes" },
16531
17362
  { id: 2, label: "Risk mode with inline risk notes" },
@@ -16922,6 +17753,9 @@ var NARRATIVE_ORDER = [
16922
17753
  { path: "tests/auth.test.ts", position: 7, rationale: "Tests \u2014 read last to verify the changes work correctly.", notes: { overview: "Tests for the session management changes. Read these last to confirm the new async API works as expected.", lines: [] } }
16923
17754
  ];
16924
17755
  var ANNOTATIONS = [
17756
+ // A reviewer reply to the line-31 risk review note (doc 20 threading) —
17757
+ // renders nested beneath that note.
17758
+ { filePath: "src/auth/session.ts", line: 31, side: "new", category: "note", content: "Confirmed \u2014 the JSON value is an ISO string, so new Date() parses it and the comparison holds.", replyToNoteId: "demo-note-risk" },
16925
17759
  { filePath: "src/auth/session.ts", line: 23, side: "new", category: "bug", content: "Redis key should be sanitized \u2014 if a session ID contains a colon, it will conflict with the key namespace." },
16926
17760
  { filePath: "src/auth/session.ts", line: 30, side: "new", category: "fix", content: "Wrap JSON.parse in try/catch to handle corrupted Redis data gracefully instead of crashing." },
16927
17761
  { filePath: "src/auth/session.ts", line: 12, side: "new", category: "pattern-follow", content: "Good use of a named constant instead of a magic number. This makes the TTL self-documenting." },
@@ -17034,7 +17868,7 @@ async function setupAnnotations(fileIdMap) {
17034
17868
  for (const ann of ANNOTATIONS) {
17035
17869
  const fileId = fileIdMap.get(ann.filePath);
17036
17870
  if (fileId !== void 0) {
17037
- await addAnnotation(fileId, ann.line, ann.side, ann.category, ann.content);
17871
+ await addAnnotation(fileId, ann.line, ann.side, ann.category, ann.content, ann.replyToNoteId);
17038
17872
  }
17039
17873
  }
17040
17874
  }
@@ -17120,11 +17954,11 @@ function emptyFileDiff(filePath = "") {
17120
17954
  isBinary: false
17121
17955
  };
17122
17956
  }
17123
- function parseDiffData(raw) {
17124
- if (raw === null || raw === void 0 || raw === "") return null;
17957
+ function parseDiffData(raw2) {
17958
+ if (raw2 === null || raw2 === void 0 || raw2 === "") return null;
17125
17959
  let parsed;
17126
17960
  try {
17127
- parsed = JSON.parse(raw);
17961
+ parsed = JSON.parse(raw2);
17128
17962
  } catch {
17129
17963
  return null;
17130
17964
  }
@@ -17286,9 +18120,9 @@ function createNewFileDiff(filePath, repoRoot) {
17286
18120
  isBinary: false
17287
18121
  };
17288
18122
  }
17289
- function parseDiff(raw) {
18123
+ function parseDiff(raw2) {
17290
18124
  const files = [];
17291
- const fileChunks = raw.split(/^diff --git /m).filter(Boolean);
18125
+ const fileChunks = raw2.split(/^diff --git /m).filter(Boolean);
17292
18126
  for (const chunk of fileChunks) {
17293
18127
  const headerEnd = chunk.indexOf("@@");
17294
18128
  const header = headerEnd === -1 ? chunk : chunk.slice(0, headerEnd);
@@ -17323,12 +18157,12 @@ function parseDiff(raw) {
17323
18157
  }
17324
18158
  return files;
17325
18159
  }
17326
- function parseHunks(raw) {
18160
+ function parseHunks(raw2) {
17327
18161
  const hunks = [];
17328
18162
  const hunkRegex = /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@(.*)/gm;
17329
18163
  let match;
17330
18164
  const hunkStarts = [];
17331
- while ((match = hunkRegex.exec(raw)) !== null) {
18165
+ while ((match = hunkRegex.exec(raw2)) !== null) {
17332
18166
  const groups = match;
17333
18167
  hunkStarts.push({
17334
18168
  index: match.index + match[0].length,
@@ -17340,8 +18174,8 @@ function parseHunks(raw) {
17340
18174
  }
17341
18175
  for (let i = 0; i < hunkStarts.length; i++) {
17342
18176
  const start = hunkStarts[i];
17343
- const end = i + 1 < hunkStarts.length ? raw.lastIndexOf("\n@@", hunkStarts[i + 1].index) : raw.length;
17344
- const body = raw.slice(start.index, end);
18177
+ const end = i + 1 < hunkStarts.length ? raw2.lastIndexOf("\n@@", hunkStarts[i + 1].index) : raw2.length;
18178
+ const body = raw2.slice(start.index, end);
17345
18179
  const lines = [];
17346
18180
  let oldNum = start.oldStart;
17347
18181
  let newNum = start.newStart;
@@ -17494,8 +18328,8 @@ function acquireLock(dataDir) {
17494
18328
  lockPath = join4(dataDir, "glassbox.lock");
17495
18329
  if (existsSync3(lockPath)) {
17496
18330
  try {
17497
- const raw = JSON.parse(readFileSync3(lockPath, "utf-8"));
17498
- const contents = LockFileSchema.parse(raw);
18331
+ const raw2 = JSON.parse(readFileSync3(lockPath, "utf-8"));
18332
+ const contents = LockFileSchema.parse(raw2);
17499
18333
  const pid = contents.pid;
17500
18334
  try {
17501
18335
  process.kill(pid, 0);
@@ -17643,9 +18477,9 @@ async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
17643
18477
 
17644
18478
  // src/server.ts
17645
18479
  import { serve } from "@hono/node-server";
17646
- import { existsSync as existsSync10, readFileSync as readFileSync13 } from "fs";
17647
- import { Hono as Hono18 } from "hono";
17648
- import { dirname as dirname3, join as join13 } from "path";
18480
+ import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
18481
+ import { Hono as Hono19 } from "hono";
18482
+ import { dirname as dirname4, join as join15 } from "path";
17649
18483
  import { fileURLToPath as fileURLToPath2 } from "url";
17650
18484
 
17651
18485
  // src/channel-config.ts
@@ -17680,8 +18514,8 @@ function registerChannel(dataDir) {
17680
18514
  let config2 = {};
17681
18515
  if (existsSync4(mcpPath)) {
17682
18516
  try {
17683
- const raw = JSON.parse(readFileSync4(mcpPath, "utf-8"));
17684
- const parsed = McpConfigSchema.safeParse(raw);
18517
+ const raw2 = JSON.parse(readFileSync4(mcpPath, "utf-8"));
18518
+ const parsed = McpConfigSchema.safeParse(raw2);
17685
18519
  if (parsed.success) config2 = parsed.data;
17686
18520
  } catch {
17687
18521
  }
@@ -17698,8 +18532,8 @@ function unregisterChannel(dataDir) {
17698
18532
  const mcpPath = join5(root, ".mcp.json");
17699
18533
  if (!existsSync4(mcpPath)) return;
17700
18534
  try {
17701
- const raw = JSON.parse(readFileSync4(mcpPath, "utf-8"));
17702
- const parsed = McpConfigSchema.safeParse(raw);
18535
+ const raw2 = JSON.parse(readFileSync4(mcpPath, "utf-8"));
18536
+ const parsed = McpConfigSchema.safeParse(raw2);
17703
18537
  if (!parsed.success) return;
17704
18538
  const config2 = parsed.data;
17705
18539
  if (config2.mcpServers?.[MCP_SERVER_KEY] !== void 0) {
@@ -17725,8 +18559,8 @@ async function isChannelAlive(dataDir) {
17725
18559
  if (port === null) return false;
17726
18560
  try {
17727
18561
  const res = await fetch(`http://127.0.0.1:${port}/health`);
17728
- const raw = await res.json();
17729
- const parsed = HealthResponseSchema.safeParse(raw);
18562
+ const raw2 = await res.json();
18563
+ const parsed = HealthResponseSchema.safeParse(raw2);
17730
18564
  return parsed.success && parsed.data.ok;
17731
18565
  } catch {
17732
18566
  return false;
@@ -17759,8 +18593,120 @@ init_zod();
17759
18593
  // src/ai/shared.ts
17760
18594
  init_zod();
17761
18595
 
18596
+ // src/review-notes/format.ts
18597
+ init_store();
18598
+ init_view();
18599
+ function notesByFile(repoRoot, filePaths) {
18600
+ const out = [];
18601
+ for (const file2 of filePaths) {
18602
+ const notes = loadReviewNotesForFile(repoRoot, file2);
18603
+ if (notes.length > 0) out.push({ file: file2, notes });
18604
+ }
18605
+ return out;
18606
+ }
18607
+ function reviewNotesPromptSection(repoRoot, filePaths) {
18608
+ const grouped = notesByFile(repoRoot, filePaths);
18609
+ if (grouped.length === 0) return "";
18610
+ const lines = [
18611
+ "=== Author review notes ===",
18612
+ "The author (an AI tool) left line-anchored notes explaining these changes. Use them to inform your analysis \u2014 weight their stated risks and assumptions.",
18613
+ ""
18614
+ ];
18615
+ for (const { file: file2, notes } of grouped) {
18616
+ lines.push(`${file2}:`);
18617
+ for (const n of notes) {
18618
+ lines.push(`- [${REVIEW_NOTE_LABELS[n.kind] ?? n.kind}, L${String(n.line)}] ${n.body}`);
18619
+ }
18620
+ lines.push("");
18621
+ }
18622
+ return lines.join("\n").trimEnd();
18623
+ }
18624
+ function reviewNotesExportSection(repoRoot, filePaths) {
18625
+ const grouped = notesByFile(repoRoot, filePaths);
18626
+ if (grouped.length === 0) return [];
18627
+ const lines = [
18628
+ "## AI Review Notes",
18629
+ "",
18630
+ "> Line-anchored notes the generating AI left explaining its changes (from `.pr-notes/`). Read these for the rationale and proof behind the code.",
18631
+ ""
18632
+ ];
18633
+ for (const { file: file2, notes } of grouped) {
18634
+ lines.push(`### ${file2}`);
18635
+ lines.push("");
18636
+ for (const n of notes) {
18637
+ const who = n.producer !== void 0 ? ` _(${n.producer})_` : "";
18638
+ lines.push(`- **Line ${String(n.line)}** [${n.kind}]: ${n.body}${who}`);
18639
+ }
18640
+ lines.push("");
18641
+ }
18642
+ return lines;
18643
+ }
18644
+
17762
18645
  // src/ai/client.ts
17763
18646
  init_zod();
18647
+
18648
+ // src/ai/apple-foundation.ts
18649
+ init_zod();
18650
+ import { spawn } from "child_process";
18651
+ import { existsSync as existsSync6 } from "fs";
18652
+ import { join as join7 } from "path";
18653
+ var defaultRunner = (bin, args, stdin) => new Promise((resolve11, reject) => {
18654
+ const child = spawn(bin, args, { stdio: ["pipe", "pipe", "ignore"] });
18655
+ let stdout = "";
18656
+ child.stdout.setEncoding("utf-8");
18657
+ child.stdout.on("data", (chunk) => {
18658
+ stdout += chunk;
18659
+ });
18660
+ child.on("error", reject);
18661
+ child.on("close", (code) => {
18662
+ resolve11({ stdout, code: code ?? 0 });
18663
+ });
18664
+ child.stdin.end(stdin);
18665
+ });
18666
+ var runner = defaultRunner;
18667
+ var isDarwin = process.platform === "darwin";
18668
+ var availabilityCache = null;
18669
+ function appleFmBinPath() {
18670
+ const env = process.env.GLASSBOX_APPLE_FM_BIN;
18671
+ if (env !== void 0 && env !== "" && existsSync6(env)) return env;
18672
+ const fallback = join7(process.cwd(), "apple-fm-helper");
18673
+ if (existsSync6(fallback)) return fallback;
18674
+ return null;
18675
+ }
18676
+ async function isAppleFoundationAvailable() {
18677
+ if (availabilityCache !== null) return availabilityCache;
18678
+ availabilityCache = await probeAvailability();
18679
+ return availabilityCache;
18680
+ }
18681
+ async function probeAvailability() {
18682
+ if (!isDarwin) return false;
18683
+ const bin = appleFmBinPath();
18684
+ if (bin === null) return false;
18685
+ try {
18686
+ const { stdout, code } = await runner(bin, ["--probe"], "");
18687
+ return code === 0 && stdout.trim().toLowerCase().startsWith("available");
18688
+ } catch {
18689
+ return false;
18690
+ }
18691
+ }
18692
+ var InferOutputSchema = external_exports.object({ content: external_exports.string() });
18693
+ async function runAppleFoundationInfer(system, messages) {
18694
+ const bin = appleFmBinPath();
18695
+ if (bin === null) throw new Error("Apple Foundation Models helper not found");
18696
+ const { stdout, code } = await runner(bin, ["--infer"], JSON.stringify({ system, messages }));
18697
+ if (code !== 0) throw new Error(`Apple Foundation Models helper exited with code ${String(code)}`);
18698
+ let raw2;
18699
+ try {
18700
+ raw2 = JSON.parse(stdout);
18701
+ } catch {
18702
+ throw new Error("Apple Foundation Models helper returned non-JSON output");
18703
+ }
18704
+ const parsed = InferOutputSchema.safeParse(raw2);
18705
+ if (!parsed.success) throw new Error("Apple Foundation Models helper returned an unexpected payload");
18706
+ return parsed.data.content;
18707
+ }
18708
+
18709
+ // src/ai/client.ts
17764
18710
  var AnthropicResponseSchema = external_exports.object({
17765
18711
  content: external_exports.array(external_exports.object({ type: external_exports.string(), text: external_exports.string().optional() }).loose()),
17766
18712
  usage: external_exports.object({ input_tokens: external_exports.number(), output_tokens: external_exports.number() }).loose()
@@ -17771,6 +18717,12 @@ var OpenAIResponseSchema = external_exports.object({
17771
18717
  }).loose()).min(1),
17772
18718
  usage: external_exports.object({ prompt_tokens: external_exports.number(), completion_tokens: external_exports.number() }).loose()
17773
18719
  }).loose();
18720
+ var LocalResponseSchema = external_exports.object({
18721
+ choices: external_exports.array(external_exports.object({
18722
+ message: external_exports.object({ content: external_exports.string() }).loose()
18723
+ }).loose()).min(1),
18724
+ usage: external_exports.object({ prompt_tokens: external_exports.number(), completion_tokens: external_exports.number() }).loose().optional()
18725
+ }).loose();
17774
18726
  var GoogleResponseSchema = external_exports.object({
17775
18727
  candidates: external_exports.array(external_exports.object({
17776
18728
  content: external_exports.object({
@@ -17782,23 +18734,35 @@ var GoogleResponseSchema = external_exports.object({
17782
18734
  candidatesTokenCount: external_exports.number()
17783
18735
  }).loose().optional()
17784
18736
  }).loose();
17785
- async function sendAIRequest(config2, systemPrompt, messages) {
18737
+ function requireKey(config2) {
17786
18738
  if (config2.apiKey === null) {
17787
18739
  throw new Error(`No API key configured for ${config2.platform}`);
17788
18740
  }
18741
+ return config2.apiKey;
18742
+ }
18743
+ async function sendAIRequest(config2, systemPrompt, messages) {
18744
+ if (config2.apiKey === null && !KEYLESS_PLATFORMS.has(config2.platform)) {
18745
+ throw new Error(`No API key configured for ${config2.platform}`);
18746
+ }
17789
18747
  const totalChars = messages.reduce((sum, m) => sum + m.content.length, 0) + systemPrompt.length;
17790
18748
  debugLog(`AI request \u2192 ${config2.platform}/${config2.model} | ${String(messages.length)} message(s) | ~${String(Math.ceil(totalChars / 3))} estimated tokens`);
17791
18749
  const start = Date.now();
17792
18750
  let response;
17793
18751
  switch (config2.platform) {
17794
18752
  case "anthropic":
17795
- response = await sendAnthropicRequest(config2.apiKey, config2.model, systemPrompt, messages);
18753
+ response = await sendAnthropicRequest(requireKey(config2), config2.model, systemPrompt, messages);
17796
18754
  break;
17797
18755
  case "openai":
17798
- response = await sendOpenAIRequest(config2.apiKey, config2.model, systemPrompt, messages);
18756
+ response = await sendOpenAIRequest(requireKey(config2), config2.model, systemPrompt, messages);
17799
18757
  break;
17800
18758
  case "google":
17801
- response = await sendGoogleRequest(config2.apiKey, config2.model, systemPrompt, messages);
18759
+ response = await sendGoogleRequest(requireKey(config2), config2.model, systemPrompt, messages);
18760
+ break;
18761
+ case "local":
18762
+ response = await sendLocalRequest(config2.baseUrl ?? DEFAULT_LOCAL_ENDPOINT, config2.apiKey, config2.model, systemPrompt, messages);
18763
+ break;
18764
+ case "apple":
18765
+ response = await sendAppleRequest(systemPrompt, messages);
17802
18766
  break;
17803
18767
  }
17804
18768
  const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
@@ -17827,8 +18791,8 @@ async function sendAnthropicRequest(apiKey, model, systemPrompt, messages) {
17827
18791
  const errorText = await response.text();
17828
18792
  throw new Error(`Anthropic API error (${String(response.status)}): ${errorText}`);
17829
18793
  }
17830
- const raw = await response.json();
17831
- const data = AnthropicResponseSchema.parse(raw);
18794
+ const raw2 = await response.json();
18795
+ const data = AnthropicResponseSchema.parse(raw2);
17832
18796
  const text = data.content.filter((c) => c.type === "text").map((c) => c.text ?? "").join("");
17833
18797
  return {
17834
18798
  content: text,
@@ -17857,14 +18821,42 @@ async function sendOpenAIRequest(apiKey, model, systemPrompt, messages) {
17857
18821
  const errorText = await response.text();
17858
18822
  throw new Error(`OpenAI API error (${String(response.status)}): ${errorText}`);
17859
18823
  }
17860
- const raw = await response.json();
17861
- const data = OpenAIResponseSchema.parse(raw);
18824
+ const raw2 = await response.json();
18825
+ const data = OpenAIResponseSchema.parse(raw2);
17862
18826
  return {
17863
18827
  content: data.choices[0].message.content,
17864
18828
  inputTokens: data.usage.prompt_tokens,
17865
18829
  outputTokens: data.usage.completion_tokens
17866
18830
  };
17867
18831
  }
18832
+ async function sendLocalRequest(baseUrl, apiKey, model, systemPrompt, messages) {
18833
+ const oaiMessages = [
18834
+ { role: "system", content: systemPrompt },
18835
+ ...messages.map((m) => ({ role: m.role, content: m.content }))
18836
+ ];
18837
+ const headers = { "Content-Type": "application/json" };
18838
+ if (apiKey !== null && apiKey !== "") headers.Authorization = `Bearer ${apiKey}`;
18839
+ const response = await fetch(`${baseUrl}/chat/completions`, {
18840
+ method: "POST",
18841
+ headers,
18842
+ body: JSON.stringify({ model, messages: oaiMessages, max_tokens: 8192, stream: false })
18843
+ });
18844
+ if (!response.ok) {
18845
+ const errorText = await response.text();
18846
+ throw new Error(`Local model error (${String(response.status)}): ${errorText}`);
18847
+ }
18848
+ const raw2 = await response.json();
18849
+ const data = LocalResponseSchema.parse(raw2);
18850
+ return {
18851
+ content: data.choices[0].message.content,
18852
+ inputTokens: data.usage?.prompt_tokens ?? 0,
18853
+ outputTokens: data.usage?.completion_tokens ?? 0
18854
+ };
18855
+ }
18856
+ async function sendAppleRequest(systemPrompt, messages) {
18857
+ const content = await runAppleFoundationInfer(systemPrompt, messages);
18858
+ return { content, inputTokens: 0, outputTokens: 0 };
18859
+ }
17868
18860
  async function sendGoogleRequest(apiKey, model, systemPrompt, messages) {
17869
18861
  const contents = messages.map((m) => ({
17870
18862
  role: m.role === "assistant" ? "model" : "user",
@@ -17889,8 +18881,8 @@ async function sendGoogleRequest(apiKey, model, systemPrompt, messages) {
17889
18881
  const errorText = await response.text();
17890
18882
  throw new Error(`Google AI API error (${String(response.status)}): ${errorText}`);
17891
18883
  }
17892
- const raw = await response.json();
17893
- const data = GoogleResponseSchema.parse(raw);
18884
+ const raw2 = await response.json();
18885
+ const data = GoogleResponseSchema.parse(raw2);
17894
18886
  const text = data.candidates[0].content.parts.map((p) => p.text).join("");
17895
18887
  return {
17896
18888
  content: text,
@@ -18018,10 +19010,12 @@ async function runAnalysisBatch(files, config2, repoRoot, options) {
18018
19010
  const charBudget = Math.floor(contextWindow * 0.7 * 3);
18019
19011
  const contexts = buildFileContexts(files, charBudget);
18020
19012
  const validPaths = new Set(files.map((f) => f.file_path));
19013
+ const notesSection = reviewNotesPromptSection(repoRoot, files.map((f) => f.file_path));
18021
19014
  const initialPrompt = [
18022
19015
  options.initialPromptHeader(files.length),
18023
19016
  "",
18024
- formatContextsForPrompt(contexts)
19017
+ formatContextsForPrompt(contexts),
19018
+ ...notesSection === "" ? [] : ["", notesSection]
18025
19019
  ].join("\n");
18026
19020
  const messages = [{ role: "user", content: initialPrompt }];
18027
19021
  for (let round = 0; round < 3; round++) {
@@ -18045,7 +19039,8 @@ ${formatAdditionalContext(fileContents)}`
18045
19039
  });
18046
19040
  continue;
18047
19041
  }
18048
- const arrayResult = external_exports.array(options.itemSchema).safeParse(parsed);
19042
+ const candidate = Array.isArray(parsed) ? parsed : [parsed];
19043
+ const arrayResult = external_exports.array(options.itemSchema).safeParse(candidate);
18049
19044
  if (!arrayResult.success) {
18050
19045
  const summary = arrayResult.error.issues.slice(0, 3).map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
18051
19046
  throw new Error(`Expected an array of ${options.resultLabel} from AI \u2014 ${summary}`);
@@ -18486,8 +19481,8 @@ function isRetriable(err) {
18486
19481
  return msg.includes("429") || msg.includes("500") || msg.includes("502") || msg.includes("503") || msg.includes("504") || msg.includes("rate_limit");
18487
19482
  }
18488
19483
  function sleep(ms) {
18489
- return new Promise((resolve9) => {
18490
- setTimeout(resolve9, ms);
19484
+ return new Promise((resolve11) => {
19485
+ setTimeout(resolve11, ms);
18491
19486
  });
18492
19487
  }
18493
19488
 
@@ -18531,8 +19526,8 @@ function randomLines(count) {
18531
19526
  return lines.sort((a, b) => a.line - b.line);
18532
19527
  }
18533
19528
  function sleep2(ms) {
18534
- return new Promise((resolve9) => {
18535
- setTimeout(resolve9, ms);
19529
+ return new Promise((resolve11) => {
19530
+ setTimeout(resolve11, ms);
18536
19531
  });
18537
19532
  }
18538
19533
  async function mockRiskAnalysisBatch(files) {
@@ -18699,17 +19694,25 @@ var AIConfigRespSchema = external_exports.object({
18699
19694
  model: external_exports.string(),
18700
19695
  keyConfigured: external_exports.boolean(),
18701
19696
  keySource: KeySourceSchema,
19697
+ /** Base URL for the `local` (OpenAI-compatible) platform. */
19698
+ localEndpoint: external_exports.string(),
18702
19699
  guidedReview: GuidedReviewConfigShapeSchema
18703
19700
  });
18704
19701
  var SaveAIConfigReqSchema = external_exports.object({
18705
19702
  platform: AIPlatformSchema,
18706
19703
  model: external_exports.string().min(1),
19704
+ localEndpoint: external_exports.string().optional(),
18707
19705
  guidedReview: GuidedReviewConfigShapeSchema.optional()
18708
19706
  });
18709
19707
  var SaveAIConfigRespSchema = OkResponseSchema;
18710
19708
  var ListAIModelsRespSchema = external_exports.object({
18711
19709
  platforms: external_exports.record(AIPlatformSchema, external_exports.string()),
18712
- models: external_exports.record(AIPlatformSchema, external_exports.array(AIModelSchema))
19710
+ models: external_exports.record(AIPlatformSchema, external_exports.array(AIModelSchema)),
19711
+ // Whether the on-device Apple Foundation Models helper is available right now
19712
+ // (macOS 26 + Apple Intelligence + bundled helper). The picker uses this to
19713
+ // show/hide the Apple platform; the `platforms`/`models` records always carry
19714
+ // every platform key (the record schema over the platform enum is exhaustive).
19715
+ appleAvailable: external_exports.boolean()
18713
19716
  });
18714
19717
  var AIKeyStatusEntrySchema = external_exports.object({
18715
19718
  configured: external_exports.boolean(),
@@ -18892,7 +19895,9 @@ var CreateAnnotationReqSchema = external_exports.object({
18892
19895
  lineNumber: external_exports.number().int().min(1),
18893
19896
  side: AnnotationSideSchema,
18894
19897
  category: AnnotationCategorySchema,
18895
- content: external_exports.string().min(1)
19898
+ content: external_exports.string().min(1),
19899
+ /** SARIF guid of the AI review note this annotation replies to (doc 20 threading). */
19900
+ replyToNoteId: external_exports.string().optional()
18896
19901
  });
18897
19902
  var CreateAnnotationRespSchema = AnnotationSchema;
18898
19903
  var UpdateAnnotationReqSchema = external_exports.object({
@@ -19954,6 +20959,19 @@ var DifftoolPollRespSchema = external_exports.object({
19954
20959
  });
19955
20960
  var DifftoolEndRespSchema = external_exports.object({ ok: external_exports.literal(true) });
19956
20961
 
20962
+ // src/api/review-notes.ts
20963
+ init_zod();
20964
+ var DiscardReviewNoteReqSchema = external_exports.object({
20965
+ guid: external_exports.string().min(1),
20966
+ /** Repo-relative source file the note is on (scopes the shard search). */
20967
+ file: external_exports.string().min(1)
20968
+ });
20969
+ var DiscardReviewNoteRespSchema = external_exports.object({
20970
+ ok: external_exports.boolean(),
20971
+ /** Whether a note was actually removed (false if it wasn't on disk, e.g. demo). */
20972
+ removed: external_exports.boolean()
20973
+ });
20974
+
19957
20975
  // src/api/index.ts
19958
20976
  var apis = {
19959
20977
  ...ai_exports,
@@ -19977,13 +20995,13 @@ init_schemas3();
19977
20995
 
19978
20996
  // src/utils/parseBody.ts
19979
20997
  async function parseBody(c, schema) {
19980
- let raw;
20998
+ let raw2;
19981
20999
  try {
19982
- raw = await c.req.json();
21000
+ raw2 = await c.req.json();
19983
21001
  } catch {
19984
21002
  return { ok: false, response: c.json({ error: "Body must be valid JSON" }, 400) };
19985
21003
  }
19986
- const result = schema.safeParse(raw);
21004
+ const result = schema.safeParse(raw2);
19987
21005
  if (!result.success) {
19988
21006
  const summary = result.error.issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; ");
19989
21007
  return { ok: false, response: c.json({ error: summary }, 400) };
@@ -19991,8 +21009,8 @@ async function parseBody(c, schema) {
19991
21009
  return { ok: true, data: result.data };
19992
21010
  }
19993
21011
  function parseQuery(c, schema) {
19994
- const raw = c.req.query();
19995
- const result = schema.safeParse(raw);
21012
+ const raw2 = c.req.query();
21013
+ const result = schema.safeParse(raw2);
19996
21014
  if (!result.success) {
19997
21015
  const summary = result.error.issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; ");
19998
21016
  return { ok: false, response: c.json({ error: summary }, 400) };
@@ -20443,11 +21461,11 @@ function anthropicContextWindow(id) {
20443
21461
  return id.toLowerCase().includes("haiku") ? 2e5 : 1e6;
20444
21462
  }
20445
21463
  async function fetchAnthropic(apiKey) {
20446
- const raw = await getJson("https://api.anthropic.com/v1/models?limit=1000", {
21464
+ const raw2 = await getJson("https://api.anthropic.com/v1/models?limit=1000", {
20447
21465
  "x-api-key": apiKey,
20448
21466
  "anthropic-version": "2023-06-01"
20449
21467
  });
20450
- const parsed = AnthropicListSchema.safeParse(raw);
21468
+ const parsed = AnthropicListSchema.safeParse(raw2);
20451
21469
  if (!parsed.success) return null;
20452
21470
  return parsed.data.data.map((m) => ({
20453
21471
  id: m.id,
@@ -20466,13 +21484,21 @@ function isOpenAIChatModel(id) {
20466
21484
  return isChat && !isNonChat;
20467
21485
  }
20468
21486
  async function fetchOpenAI(apiKey) {
20469
- const raw = await getJson("https://api.openai.com/v1/models", {
21487
+ const raw2 = await getJson("https://api.openai.com/v1/models", {
20470
21488
  Authorization: `Bearer ${apiKey}`
20471
21489
  });
20472
- const parsed = OpenAIListSchema.safeParse(raw);
21490
+ const parsed = OpenAIListSchema.safeParse(raw2);
20473
21491
  if (!parsed.success) return null;
20474
21492
  return parsed.data.data.filter((m) => isOpenAIChatModel(m.id)).map((m) => ({ id: m.id, name: m.id, contextWindow: 128e3, isDefault: false }));
20475
21493
  }
21494
+ async function fetchLocal(baseUrl, apiKey) {
21495
+ const headers = {};
21496
+ if (apiKey !== "") headers.Authorization = `Bearer ${apiKey}`;
21497
+ const raw2 = await getJson(`${baseUrl}/models`, headers);
21498
+ const parsed = OpenAIListSchema.safeParse(raw2);
21499
+ if (!parsed.success) return null;
21500
+ return parsed.data.data.map((m) => ({ id: m.id, name: m.id, contextWindow: 8192, isDefault: false }));
21501
+ }
20476
21502
  var GoogleListSchema = external_exports.object({
20477
21503
  models: external_exports.array(external_exports.object({
20478
21504
  name: external_exports.string(),
@@ -20482,11 +21508,11 @@ var GoogleListSchema = external_exports.object({
20482
21508
  }))
20483
21509
  });
20484
21510
  async function fetchGoogle(apiKey) {
20485
- const raw = await getJson(
21511
+ const raw2 = await getJson(
20486
21512
  `https://generativelanguage.googleapis.com/v1beta/models?pageSize=1000&key=${encodeURIComponent(apiKey)}`,
20487
21513
  {}
20488
21514
  );
20489
- const parsed = GoogleListSchema.safeParse(raw);
21515
+ const parsed = GoogleListSchema.safeParse(raw2);
20490
21516
  if (!parsed.success) return null;
20491
21517
  return parsed.data.models.filter((m) => (m.supportedGenerationMethods ?? []).includes("generateContent")).map((m) => ({
20492
21518
  id: m.name.replace(/^models\//, ""),
@@ -20495,10 +21521,11 @@ async function fetchGoogle(apiKey) {
20495
21521
  isDefault: false
20496
21522
  }));
20497
21523
  }
20498
- async function fetchAvailableModels(platform, apiKey) {
21524
+ async function fetchAvailableModels(platform, apiKey, opts = {}) {
20499
21525
  let models;
20500
21526
  if (platform === "anthropic") models = await fetchAnthropic(apiKey);
20501
21527
  else if (platform === "openai") models = await fetchOpenAI(apiKey);
21528
+ else if (platform === "local") models = await fetchLocal(opts.baseUrl ?? "", apiKey);
20502
21529
  else models = await fetchGoogle(apiKey);
20503
21530
  if (models === null || models.length === 0) return null;
20504
21531
  const defaultId = getDefaultModel(platform);
@@ -20511,13 +21538,15 @@ async function fetchAvailableModels(platform, apiKey) {
20511
21538
 
20512
21539
  // src/routes/ai-config.ts
20513
21540
  var aiConfigRoutes = new Hono2();
20514
- aiConfigRoutes.get("/config", (c) => {
21541
+ aiConfigRoutes.get("/config", async (c) => {
20515
21542
  const config2 = loadAIConfig();
21543
+ const appleReady = config2.platform === "apple" && await isAppleFoundationAvailable();
20516
21544
  return c.json({
20517
21545
  platform: config2.platform,
20518
21546
  model: config2.model,
20519
- keyConfigured: config2.apiKey !== null || isAIServiceTest() || getDemoMode() !== null,
21547
+ keyConfigured: config2.apiKey !== null || config2.platform === "local" || appleReady || isAIServiceTest() || getDemoMode() !== null,
20520
21548
  keySource: config2.keySource,
21549
+ localEndpoint: resolveLocalEndpoint(),
20521
21550
  guidedReview: loadGuidedReviewConfig()
20522
21551
  });
20523
21552
  });
@@ -20525,7 +21554,7 @@ aiConfigRoutes.post("/config", async (c) => {
20525
21554
  const parsed = await parseBody(c, SaveAIConfigReqSchema);
20526
21555
  if (!parsed.ok) return parsed.response;
20527
21556
  const body = parsed.data;
20528
- saveAIConfigPreferences(body.platform, body.model);
21557
+ saveAIConfigPreferences(body.platform, body.model, { localEndpoint: body.localEndpoint });
20529
21558
  if (body.guidedReview !== void 0) {
20530
21559
  saveGuidedReviewConfig(body.guidedReview);
20531
21560
  }
@@ -20533,7 +21562,13 @@ aiConfigRoutes.post("/config", async (c) => {
20533
21562
  });
20534
21563
  aiConfigRoutes.get("/models", async (c) => {
20535
21564
  const platforms = ["anthropic", "openai", "google"];
20536
- const models = { anthropic: MODELS.anthropic, openai: MODELS.openai, google: MODELS.google };
21565
+ const models = {
21566
+ anthropic: MODELS.anthropic,
21567
+ openai: MODELS.openai,
21568
+ google: MODELS.google,
21569
+ local: MODELS.local,
21570
+ apple: MODELS.apple
21571
+ };
20537
21572
  if (!isAIServiceTest() && getDemoMode() === null) {
20538
21573
  await Promise.all(platforms.map(async (platform) => {
20539
21574
  const { key } = resolveAPIKey(platform);
@@ -20541,11 +21576,15 @@ aiConfigRoutes.get("/models", async (c) => {
20541
21576
  const live = await fetchAvailableModels(platform, key);
20542
21577
  if (live !== null && live.length > 0) models[platform] = live;
20543
21578
  }));
21579
+ const { key: localKey } = resolveAPIKey("local");
21580
+ const localLive = await fetchAvailableModels("local", localKey ?? "", { baseUrl: resolveLocalEndpoint() });
21581
+ if (localLive !== null && localLive.length > 0) models.local = localLive;
20544
21582
  }
20545
- return c.json({ platforms: PLATFORMS, models });
21583
+ const appleAvailable = await isAppleFoundationAvailable();
21584
+ return c.json({ platforms: PLATFORMS, models, appleAvailable });
20546
21585
  });
20547
21586
  aiConfigRoutes.get("/key-status", (c) => {
20548
- const platforms = ["anthropic", "openai", "google"];
21587
+ const platforms = ["anthropic", "openai", "google", "local", "apple"];
20549
21588
  const status = {};
20550
21589
  for (const platform of platforms) {
20551
21590
  const { source } = resolveAPIKey(platform);
@@ -20569,7 +21608,7 @@ aiConfigRoutes.delete("/key", (c) => {
20569
21608
  const platform = c.req.query("platform") ?? "anthropic";
20570
21609
  const parsed = AIPlatformSchema.safeParse(platform);
20571
21610
  if (!parsed.success) {
20572
- return errorResponse(c, `platform must be one of: anthropic, openai, google`);
21611
+ return errorResponse(c, `platform must be one of: ${Object.keys(PLATFORMS).join(", ")}`);
20573
21612
  }
20574
21613
  deleteAPIKey(parsed.data);
20575
21614
  return c.json({ ok: true });
@@ -20581,7 +21620,7 @@ aiApiRoutes.route("/", aiConfigRoutes);
20581
21620
  aiApiRoutes.route("/", aiAnalysisRoutes);
20582
21621
 
20583
21622
  // src/routes/api.ts
20584
- import { Hono as Hono13 } from "hono";
21623
+ import { Hono as Hono14 } from "hono";
20585
21624
 
20586
21625
  // src/routes/api/annotations.ts
20587
21626
  import { Hono as Hono4 } from "hono";
@@ -20590,28 +21629,28 @@ init_queries();
20590
21629
  // src/export/generate.ts
20591
21630
  init_zod();
20592
21631
  init_queries();
20593
- import { spawnSync as spawnSync5 } from "child_process";
20594
- import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
21632
+ import { spawnSync as spawnSync6 } from "child_process";
21633
+ import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
20595
21634
  import { homedir as homedir2 } from "os";
20596
- import { join as join6 } from "path";
20597
- var DISMISS_FILE = join6(homedir2(), ".glassbox", "gitignore-dismissed.json");
21635
+ import { join as join8 } from "path";
21636
+ var DISMISS_FILE = join8(homedir2(), ".glassbox", "gitignore-dismissed.json");
20598
21637
  var DISMISS_DAYS = 30;
20599
21638
  var DismissalsSchema = external_exports.record(external_exports.string(), external_exports.number());
20600
21639
  function loadDismissals() {
20601
21640
  try {
20602
- const parsed = DismissalsSchema.safeParse(JSON.parse(readFileSync5(DISMISS_FILE, "utf-8")));
21641
+ const parsed = DismissalsSchema.safeParse(JSON.parse(readFileSync6(DISMISS_FILE, "utf-8")));
20603
21642
  return parsed.success ? parsed.data : {};
20604
21643
  } catch {
20605
21644
  return {};
20606
21645
  }
20607
21646
  }
20608
21647
  function saveDismissals(data) {
20609
- const dir = join6(homedir2(), ".glassbox");
20610
- mkdirSync4(dir, { recursive: true });
20611
- writeFileSync5(DISMISS_FILE, JSON.stringify(data), "utf-8");
21648
+ const dir = join8(homedir2(), ".glassbox");
21649
+ mkdirSync5(dir, { recursive: true });
21650
+ writeFileSync6(DISMISS_FILE, JSON.stringify(data), "utf-8");
20612
21651
  }
20613
21652
  function isGlassboxGitignored(repoRoot) {
20614
- const result = spawnSync5("git", ["check-ignore", "-q", ".glassbox"], { cwd: repoRoot, stdio: "pipe" });
21653
+ const result = spawnSync6("git", ["check-ignore", "-q", ".glassbox"], { cwd: repoRoot, stdio: "pipe" });
20615
21654
  return result.status === 0;
20616
21655
  }
20617
21656
  function shouldPromptGitignore(repoRoot) {
@@ -20626,16 +21665,16 @@ function shouldPromptGitignore(repoRoot) {
20626
21665
  return true;
20627
21666
  }
20628
21667
  function addGlassboxToGitignore(repoRoot) {
20629
- const gitignorePath = join6(repoRoot, ".gitignore");
20630
- if (existsSync5(gitignorePath)) {
20631
- const content = readFileSync5(gitignorePath, "utf-8");
21668
+ const gitignorePath = join8(repoRoot, ".gitignore");
21669
+ if (existsSync7(gitignorePath)) {
21670
+ const content = readFileSync6(gitignorePath, "utf-8");
20632
21671
  if (!content.endsWith("\n")) {
20633
21672
  appendFileSync(gitignorePath, "\n.glassbox/\n", "utf-8");
20634
21673
  } else {
20635
21674
  appendFileSync(gitignorePath, ".glassbox/\n", "utf-8");
20636
21675
  }
20637
21676
  } else {
20638
- writeFileSync5(gitignorePath, ".glassbox/\n", "utf-8");
21677
+ writeFileSync6(gitignorePath, ".glassbox/\n", "utf-8");
20639
21678
  }
20640
21679
  }
20641
21680
  function dismissGitignorePrompt2(repoRoot) {
@@ -20644,17 +21683,17 @@ function dismissGitignorePrompt2(repoRoot) {
20644
21683
  saveDismissals(dismissals);
20645
21684
  }
20646
21685
  function deleteReviewExport(reviewId, repoRoot) {
20647
- const exportDir = join6(repoRoot, ".glassbox");
20648
- const archivePath = join6(exportDir, `review-${reviewId}.md`);
20649
- if (existsSync5(archivePath)) unlinkSync(archivePath);
21686
+ const exportDir = join8(repoRoot, ".glassbox");
21687
+ const archivePath = join8(exportDir, `review-${reviewId}.md`);
21688
+ if (existsSync7(archivePath)) unlinkSync2(archivePath);
20650
21689
  }
20651
21690
  async function generateReviewExport(reviewId, repoRoot, isCurrent) {
20652
21691
  const review = await getReview(reviewId);
20653
21692
  if (!review) throw new Error("Review not found");
20654
21693
  const files = await getReviewFiles(reviewId);
20655
21694
  const annotations = await getAnnotationsForReview(reviewId);
20656
- const exportDir = join6(repoRoot, ".glassbox");
20657
- mkdirSync4(exportDir, { recursive: true });
21695
+ const exportDir = join8(repoRoot, ".glassbox");
21696
+ mkdirSync5(exportDir, { recursive: true });
20658
21697
  const byFile = {};
20659
21698
  for (const a of annotations) {
20660
21699
  if (!(a.file_path in byFile)) byFile[a.file_path] = [];
@@ -20705,6 +21744,8 @@ async function generateReviewExport(reviewId, repoRoot, isCurrent) {
20705
21744
  }
20706
21745
  lines.push("");
20707
21746
  }
21747
+ const reviewNoteLines = reviewNotesExportSection(repoRoot, files.map((f) => f.file_path));
21748
+ if (reviewNoteLines.length > 0) lines.push(...reviewNoteLines);
20708
21749
  lines.push("---");
20709
21750
  lines.push("");
20710
21751
  lines.push("## Instructions for AI Tools");
@@ -20719,11 +21760,11 @@ async function generateReviewExport(reviewId, repoRoot, isCurrent) {
20719
21760
  lines.push("6. **note** annotations are informational context. Consider them but they may not require code changes.");
20720
21761
  lines.push("");
20721
21762
  const content = lines.join("\n");
20722
- const archivePath = join6(exportDir, `review-${review.id}.md`);
20723
- writeFileSync5(archivePath, content, "utf-8");
21763
+ const archivePath = join8(exportDir, `review-${review.id}.md`);
21764
+ writeFileSync6(archivePath, content, "utf-8");
20724
21765
  if (isCurrent) {
20725
- const latestPath = join6(exportDir, "latest-review.md");
20726
- writeFileSync5(latestPath, content, "utf-8");
21766
+ const latestPath = join8(exportDir, "latest-review.md");
21767
+ writeFileSync6(latestPath, content, "utf-8");
20727
21768
  return latestPath;
20728
21769
  }
20729
21770
  return archivePath;
@@ -20754,7 +21795,8 @@ annotationsRoutes.post("/annotations", async (c) => {
20754
21795
  body.lineNumber,
20755
21796
  body.side,
20756
21797
  body.category,
20757
- body.content
21798
+ body.content,
21799
+ body.replyToNoteId
20758
21800
  );
20759
21801
  autoExport(c);
20760
21802
  return c.json(annotation, 201);
@@ -20841,7 +21883,7 @@ import { resolve as resolve4 } from "path";
20841
21883
  init_queries();
20842
21884
 
20843
21885
  // src/utils/openOS.ts
20844
- import { execFileSync, spawn } from "child_process";
21886
+ import { execFileSync, spawn as spawn2 } from "child_process";
20845
21887
  import { resolve as resolve3 } from "path";
20846
21888
  function openOS(target, mode) {
20847
21889
  if (mode === "edit") {
@@ -20873,7 +21915,7 @@ function openOS(target, mode) {
20873
21915
  }
20874
21916
  }
20875
21917
  function launchDetached(command, args) {
20876
- const child = spawn(command, args, { detached: true, stdio: "ignore" });
21918
+ const child = spawn2(command, args, { detached: true, stdio: "ignore" });
20877
21919
  child.on("error", (err) => {
20878
21920
  debugLog(`launchDetached(${command}) failed: ${err.message}`);
20879
21921
  });
@@ -20951,9 +21993,9 @@ init_blob_store();
20951
21993
  import { Hono as Hono7 } from "hono";
20952
21994
 
20953
21995
  // src/git/image.ts
20954
- import { spawnSync as spawnSync6 } from "child_process";
20955
- import { readFileSync as readFileSync7 } from "fs";
20956
- import { join as join8, resolve as resolve5 } from "path";
21996
+ import { spawnSync as spawnSync7 } from "child_process";
21997
+ import { readFileSync as readFileSync8 } from "fs";
21998
+ import { join as join10, resolve as resolve5 } from "path";
20957
21999
 
20958
22000
  // src/git/image-metadata.ts
20959
22001
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]);
@@ -21221,20 +22263,20 @@ function getNewRef(mode) {
21221
22263
  }
21222
22264
  function gitShowFile(ref, filePath, repoRoot) {
21223
22265
  const spec = ref === ":" ? `:${filePath}` : `${ref}:${filePath}`;
21224
- const result = spawnSync6("git", ["show", spec], { cwd: repoRoot, maxBuffer: 50 * 1024 * 1024, env: scrubbedGitEnv() });
22266
+ const result = spawnSync7("git", ["show", spec], { cwd: repoRoot, maxBuffer: 50 * 1024 * 1024, env: scrubbedGitEnv() });
21225
22267
  if (result.status !== 0 || result.stdout.length === 0) return null;
21226
22268
  return result.stdout;
21227
22269
  }
21228
22270
  function readWorkingFile(filePath, repoRoot) {
21229
22271
  try {
21230
- return readFileSync7(resolve5(repoRoot, filePath));
22272
+ return readFileSync8(resolve5(repoRoot, filePath));
21231
22273
  } catch {
21232
22274
  return null;
21233
22275
  }
21234
22276
  }
21235
22277
  function readDiskImage(absPath) {
21236
22278
  try {
21237
- const data = readFileSync7(absPath);
22279
+ const data = readFileSync8(absPath);
21238
22280
  return { data, size: data.length };
21239
22281
  } catch {
21240
22282
  return null;
@@ -21242,7 +22284,7 @@ function readDiskImage(absPath) {
21242
22284
  }
21243
22285
  function getOldImage(mode, filePath, oldPath, repoRoot) {
21244
22286
  if (mode.type === "diff") {
21245
- return readDiskImage(join8(directComparisonRoots(mode).rootA, oldPath ?? filePath));
22287
+ return readDiskImage(join10(directComparisonRoots(mode).rootA, oldPath ?? filePath));
21246
22288
  }
21247
22289
  const ref = getOldRef(mode);
21248
22290
  const path = oldPath ?? filePath;
@@ -21258,7 +22300,7 @@ function getOldImage(mode, filePath, oldPath, repoRoot) {
21258
22300
  }
21259
22301
  function getNewImage(mode, filePath, repoRoot) {
21260
22302
  if (mode.type === "diff") {
21261
- return readDiskImage(join8(directComparisonRoots(mode).rootB, filePath));
22303
+ return readDiskImage(join10(directComparisonRoots(mode).rootB, filePath));
21262
22304
  }
21263
22305
  const ref = getNewRef(mode);
21264
22306
  if (ref === null) {
@@ -21280,9 +22322,9 @@ function getNewImage(mode, filePath, repoRoot) {
21280
22322
  import { Worker } from "worker_threads";
21281
22323
 
21282
22324
  // src/git/svg-rasterize-render.ts
21283
- import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
22325
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
21284
22326
  import { createRequire } from "module";
21285
- import { join as join9 } from "path";
22327
+ import { join as join11 } from "path";
21286
22328
  var initialized = false;
21287
22329
  var ResvgClass;
21288
22330
  var fontBuffers = [];
@@ -21291,7 +22333,7 @@ async function ensureRenderInit() {
21291
22333
  const require2 = createRequire(import.meta.url);
21292
22334
  const resvgPath = require2.resolve("@resvg/resvg-wasm");
21293
22335
  const wasmPath = resvgPath.replace(/index\.(js|mjs)$/, "index_bg.wasm");
21294
- const wasmBuffer = readFileSync8(wasmPath);
22336
+ const wasmBuffer = readFileSync9(wasmPath);
21295
22337
  const mod = await import("@resvg/resvg-wasm");
21296
22338
  await mod.initWasm(wasmBuffer);
21297
22339
  ResvgClass = mod.Resvg;
@@ -21302,9 +22344,9 @@ function loadSystemFonts() {
21302
22344
  const buffers = [];
21303
22345
  const candidates = getFontCandidates();
21304
22346
  for (const path of candidates) {
21305
- if (!existsSync7(path)) continue;
22347
+ if (!existsSync9(path)) continue;
21306
22348
  try {
21307
- buffers.push(readFileSync8(path));
22349
+ buffers.push(readFileSync9(path));
21308
22350
  } catch {
21309
22351
  }
21310
22352
  }
@@ -21317,24 +22359,24 @@ function getFontCandidates() {
21317
22359
  const sup = "/System/Library/Fonts/Supplemental";
21318
22360
  return [
21319
22361
  // Core system fonts (serif, sans-serif, monospace)
21320
- join9(sys, "Helvetica.ttc"),
21321
- join9(sys, "Times.ttc"),
21322
- join9(sys, "Courier.ttc"),
21323
- join9(sys, "Menlo.ttc"),
21324
- join9(sys, "SFPro.ttf"),
21325
- join9(sys, "SFNS.ttf"),
21326
- join9(sys, "SFNSMono.ttf"),
22362
+ join11(sys, "Helvetica.ttc"),
22363
+ join11(sys, "Times.ttc"),
22364
+ join11(sys, "Courier.ttc"),
22365
+ join11(sys, "Menlo.ttc"),
22366
+ join11(sys, "SFPro.ttf"),
22367
+ join11(sys, "SFNS.ttf"),
22368
+ join11(sys, "SFNSMono.ttf"),
21327
22369
  // Supplemental (common named fonts in SVGs)
21328
- join9(sup, "Arial.ttf"),
21329
- join9(sup, "Arial Bold.ttf"),
21330
- join9(sup, "Georgia.ttf"),
21331
- join9(sup, "Verdana.ttf"),
21332
- join9(sup, "Tahoma.ttf"),
21333
- join9(sup, "Trebuchet MS.ttf"),
21334
- join9(sup, "Impact.ttf"),
21335
- join9(sup, "Comic Sans MS.ttf"),
21336
- join9(sup, "Courier New.ttf"),
21337
- join9(sup, "Times New Roman.ttf")
22370
+ join11(sup, "Arial.ttf"),
22371
+ join11(sup, "Arial Bold.ttf"),
22372
+ join11(sup, "Georgia.ttf"),
22373
+ join11(sup, "Verdana.ttf"),
22374
+ join11(sup, "Tahoma.ttf"),
22375
+ join11(sup, "Trebuchet MS.ttf"),
22376
+ join11(sup, "Impact.ttf"),
22377
+ join11(sup, "Comic Sans MS.ttf"),
22378
+ join11(sup, "Courier New.ttf"),
22379
+ join11(sup, "Times New Roman.ttf")
21338
22380
  ];
21339
22381
  }
21340
22382
  if (os === "linux") {
@@ -21353,17 +22395,17 @@ function getFontCandidates() {
21353
22395
  ];
21354
22396
  }
21355
22397
  if (os === "win32") {
21356
- const winFonts = join9(process.env.WINDIR ?? "C:\\Windows", "Fonts");
22398
+ const winFonts = join11(process.env.WINDIR ?? "C:\\Windows", "Fonts");
21357
22399
  return [
21358
- join9(winFonts, "arial.ttf"),
21359
- join9(winFonts, "arialbd.ttf"),
21360
- join9(winFonts, "times.ttf"),
21361
- join9(winFonts, "cour.ttf"),
21362
- join9(winFonts, "verdana.ttf"),
21363
- join9(winFonts, "tahoma.ttf"),
21364
- join9(winFonts, "georgia.ttf"),
21365
- join9(winFonts, "consola.ttf"),
21366
- join9(winFonts, "segoeui.ttf")
22400
+ join11(winFonts, "arial.ttf"),
22401
+ join11(winFonts, "arialbd.ttf"),
22402
+ join11(winFonts, "times.ttf"),
22403
+ join11(winFonts, "cour.ttf"),
22404
+ join11(winFonts, "verdana.ttf"),
22405
+ join11(winFonts, "tahoma.ttf"),
22406
+ join11(winFonts, "georgia.ttf"),
22407
+ join11(winFonts, "consola.ttf"),
22408
+ join11(winFonts, "segoeui.ttf")
21367
22409
  ];
21368
22410
  }
21369
22411
  return [];
@@ -21512,8 +22554,8 @@ function submit(job) {
21512
22554
  }
21513
22555
  async function rasterizeSvg(svgData) {
21514
22556
  const svg = svgData.toString("utf-8");
21515
- return new Promise((resolve9, reject) => {
21516
- submit({ svg, resolve: resolve9, reject });
22557
+ return new Promise((resolve11, reject) => {
22558
+ submit({ svg, resolve: resolve11, reject });
21517
22559
  });
21518
22560
  }
21519
22561
 
@@ -21581,8 +22623,8 @@ imageRoutes.get("/image/:fileId/:side", async (c) => {
21581
22623
 
21582
22624
  // src/routes/api/outline.ts
21583
22625
  init_queries();
21584
- import { spawnSync as spawnSync7 } from "child_process";
21585
- import { readFileSync as readFileSync9 } from "fs";
22626
+ import { spawnSync as spawnSync8 } from "child_process";
22627
+ import { readFileSync as readFileSync10 } from "fs";
21586
22628
  import { Hono as Hono8 } from "hono";
21587
22629
  import { resolve as resolve6 } from "path";
21588
22630
 
@@ -21948,7 +22990,7 @@ outlineRoutes.get("/symbol-definition", async (c) => {
21948
22990
  }
21949
22991
  if (definitions.length === 0) {
21950
22992
  try {
21951
- const allFiles = spawnSync7("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).stdout.trim().split("\n").filter(Boolean);
22993
+ const allFiles = spawnSync8("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).stdout.trim().split("\n").filter(Boolean);
21952
22994
  let scanned = 0;
21953
22995
  for (const filePath of allFiles) {
21954
22996
  if (searchedPaths.has(filePath)) continue;
@@ -21960,7 +23002,7 @@ outlineRoutes.get("/symbol-definition", async (c) => {
21960
23002
  }
21961
23003
  let content = "";
21962
23004
  try {
21963
- content = readFileSync9(resolve6(repoRoot, filePath), "utf-8");
23005
+ content = readFileSync10(resolve6(repoRoot, filePath), "utf-8");
21964
23006
  } catch {
21965
23007
  continue;
21966
23008
  }
@@ -21996,16 +23038,16 @@ function collectDefinitions(symbols, targetName, fileId, filePath, out) {
21996
23038
  }
21997
23039
 
21998
23040
  // src/routes/api/project-settings.ts
21999
- import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
23041
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
22000
23042
  import { Hono as Hono9 } from "hono";
22001
- import { join as join10 } from "path";
23043
+ import { join as join12 } from "path";
22002
23044
  var projectSettingsRoutes = new Hono9();
22003
23045
  function readProjectSettings(repoRoot) {
22004
- const settingsPath = join10(repoRoot, ".glassbox", "settings.json");
23046
+ const settingsPath = join12(repoRoot, ".glassbox", "settings.json");
22005
23047
  try {
22006
- if (existsSync8(settingsPath)) {
22007
- const raw = JSON.parse(readFileSync10(settingsPath, "utf-8"));
22008
- const parsed = ProjectSettingsSchema.safeParse(raw);
23048
+ if (existsSync10(settingsPath)) {
23049
+ const raw2 = JSON.parse(readFileSync11(settingsPath, "utf-8"));
23050
+ const parsed = ProjectSettingsSchema.safeParse(raw2);
22009
23051
  if (parsed.success) return parsed.data;
22010
23052
  }
22011
23053
  } catch {
@@ -22013,9 +23055,9 @@ function readProjectSettings(repoRoot) {
22013
23055
  return {};
22014
23056
  }
22015
23057
  function writeProjectSettings(repoRoot, settings) {
22016
- const dir = join10(repoRoot, ".glassbox");
22017
- mkdirSync6(dir, { recursive: true });
22018
- writeFileSync7(join10(dir, "settings.json"), JSON.stringify(settings, null, 2), "utf-8");
23058
+ const dir = join12(repoRoot, ".glassbox");
23059
+ mkdirSync7(dir, { recursive: true });
23060
+ writeFileSync8(join12(dir, "settings.json"), JSON.stringify(settings, null, 2), "utf-8");
22019
23061
  }
22020
23062
  projectSettingsRoutes.get("/project-settings", (c) => {
22021
23063
  const repoRoot = c.get("repoRoot");
@@ -22031,10 +23073,54 @@ projectSettingsRoutes.patch("/project-settings", async (c) => {
22031
23073
  return c.json(current);
22032
23074
  });
22033
23075
 
23076
+ // src/routes/api/review-notes.ts
23077
+ init_store();
23078
+ import { readFileSync as readFileSync12, statSync as statSync3 } from "fs";
23079
+ import { Hono as Hono10 } from "hono";
23080
+ import { extname, relative, resolve as resolve7 } from "path";
23081
+ var reviewNotesRoutes = new Hono10();
23082
+ var ARTIFACT_SERVE_MAX_BYTES = 1e7;
23083
+ var IMAGE_CONTENT_TYPES = {
23084
+ ".png": "image/png",
23085
+ ".webp": "image/webp",
23086
+ ".avif": "image/avif",
23087
+ ".gif": "image/gif",
23088
+ ".jpg": "image/jpeg",
23089
+ ".jpeg": "image/jpeg",
23090
+ ".svg": "image/svg+xml"
23091
+ };
23092
+ reviewNotesRoutes.get("/review-notes/artifact", (c) => {
23093
+ const file2 = c.req.query("file");
23094
+ if (file2 === void 0 || file2 === "") return c.text("Missing file", 400);
23095
+ const repoRoot = c.get("repoRoot");
23096
+ const abs = resolve7(repoRoot, file2);
23097
+ const rel = relative(repoRoot, abs);
23098
+ if (rel === "" || rel.startsWith("..") || rel.startsWith("/")) return c.text("Forbidden", 403);
23099
+ const ext = extname(abs).toLowerCase();
23100
+ if (!(ext in IMAGE_CONTENT_TYPES)) return c.text("Unsupported artifact type", 415);
23101
+ const contentType = IMAGE_CONTENT_TYPES[ext];
23102
+ try {
23103
+ const stat = statSync3(abs);
23104
+ if (!stat.isFile() || stat.size > ARTIFACT_SERVE_MAX_BYTES) return c.text("Not found", 404);
23105
+ const body = readFileSync12(abs);
23106
+ return c.body(body, 200, { "Content-Type": contentType });
23107
+ } catch {
23108
+ return c.text("Not found", 404);
23109
+ }
23110
+ });
23111
+ reviewNotesRoutes.delete("/review-notes/:guid", (c) => {
23112
+ const guid3 = requirePathParam(c, "guid");
23113
+ if (!guid3.ok) return guid3.response;
23114
+ const repoRoot = c.get("repoRoot");
23115
+ const file2 = c.req.query("file");
23116
+ const removed = removeNote(repoRoot, guid3.data, file2);
23117
+ return c.json({ ok: true, removed });
23118
+ });
23119
+
22034
23120
  // src/routes/api/reviews.ts
22035
23121
  init_queries();
22036
- import { Hono as Hono10 } from "hono";
22037
- var reviewsRoutes = new Hono10();
23122
+ import { Hono as Hono11 } from "hono";
23123
+ var reviewsRoutes = new Hono11();
22038
23124
  reviewsRoutes.get("/reviews", async (c) => {
22039
23125
  const repoRoot = c.get("repoRoot");
22040
23126
  const reviews = await listReviews(repoRoot);
@@ -22124,8 +23210,8 @@ reviewsRoutes.post("/reviews/delete-all", async (c) => {
22124
23210
 
22125
23211
  // src/routes/api/share-prompt.ts
22126
23212
  init_zod();
22127
- import { Hono as Hono11 } from "hono";
22128
- var sharePromptRoutes = new Hono11();
23213
+ import { Hono as Hono12 } from "hono";
23214
+ var sharePromptRoutes = new Hono12();
22129
23215
  var SharePromptShapeSchema = external_exports.object({
22130
23216
  dismissedAt: external_exports.number().nullable().optional(),
22131
23217
  totalOpenMs: external_exports.number().optional()
@@ -22164,8 +23250,8 @@ sharePromptRoutes.post("/share-prompt/tick", async (c) => {
22164
23250
  });
22165
23251
 
22166
23252
  // src/routes/api/system.ts
22167
- import { Hono as Hono12 } from "hono";
22168
- var systemRoutes = new Hono12();
23253
+ import { Hono as Hono13 } from "hono";
23254
+ var systemRoutes = new Hono13();
22169
23255
  systemRoutes.post("/open-external", async (c) => {
22170
23256
  const parsed = await parseBody(c, OpenExternalReqSchema);
22171
23257
  if (!parsed.ok) return parsed.response;
@@ -22177,7 +23263,7 @@ systemRoutes.post("/open-external", async (c) => {
22177
23263
  });
22178
23264
 
22179
23265
  // src/routes/api.ts
22180
- var apiRoutes = new Hono13();
23266
+ var apiRoutes = new Hono14();
22181
23267
  apiRoutes.route("/", reviewsRoutes);
22182
23268
  apiRoutes.route("/", filesRoutes);
22183
23269
  apiRoutes.route("/", annotationsRoutes);
@@ -22185,20 +23271,21 @@ apiRoutes.route("/", outlineRoutes);
22185
23271
  apiRoutes.route("/", contextRoutes);
22186
23272
  apiRoutes.route("/", projectSettingsRoutes);
22187
23273
  apiRoutes.route("/", imageRoutes);
23274
+ apiRoutes.route("/", reviewNotesRoutes);
22188
23275
  apiRoutes.route("/", sharePromptRoutes);
22189
23276
  apiRoutes.route("/", systemRoutes);
22190
23277
 
22191
23278
  // src/routes/channel-api.ts
22192
- import { spawnSync as spawnSync8 } from "child_process";
22193
- import { mkdirSync as mkdirSync7 } from "fs";
22194
- import { Hono as Hono14 } from "hono";
22195
- import { join as join11 } from "path";
22196
- var channelApiRoutes = new Hono14();
23279
+ import { spawnSync as spawnSync9 } from "child_process";
23280
+ import { mkdirSync as mkdirSync8 } from "fs";
23281
+ import { Hono as Hono15 } from "hono";
23282
+ import { join as join13 } from "path";
23283
+ var channelApiRoutes = new Hono15();
22197
23284
  channelApiRoutes.get("/status", async (c) => {
22198
23285
  const config2 = readGlobalConfig();
22199
23286
  const enabled = config2.channelEnabled === true;
22200
23287
  const repoRoot = c.get("repoRoot");
22201
- const dataDir = join11(repoRoot, ".glassbox");
23288
+ const dataDir = join13(repoRoot, ".glassbox");
22202
23289
  const connected = enabled ? await isChannelAlive(dataDir) : false;
22203
23290
  return c.json({ enabled, connected });
22204
23291
  });
@@ -22207,8 +23294,8 @@ channelApiRoutes.post("/enable", (c) => {
22207
23294
  config2.channelEnabled = true;
22208
23295
  });
22209
23296
  const repoRoot = c.get("repoRoot");
22210
- const dataDir = join11(repoRoot, ".glassbox");
22211
- mkdirSync7(dataDir, { recursive: true });
23297
+ const dataDir = join13(repoRoot, ".glassbox");
23298
+ mkdirSync8(dataDir, { recursive: true });
22212
23299
  registerChannel(dataDir);
22213
23300
  return c.json({ ok: true });
22214
23301
  });
@@ -22217,7 +23304,7 @@ channelApiRoutes.post("/disable", (c) => {
22217
23304
  config2.channelEnabled = false;
22218
23305
  });
22219
23306
  const repoRoot = c.get("repoRoot");
22220
- const dataDir = join11(repoRoot, ".glassbox");
23307
+ const dataDir = join13(repoRoot, ".glassbox");
22221
23308
  unregisterChannel(dataDir);
22222
23309
  return c.json({ ok: true });
22223
23310
  });
@@ -22225,7 +23312,7 @@ channelApiRoutes.post("/trigger", async (c) => {
22225
23312
  const parsed = await parseBody(c, TriggerChannelReqSchema);
22226
23313
  if (!parsed.ok) return parsed.response;
22227
23314
  const repoRoot = c.get("repoRoot");
22228
- const dataDir = join11(repoRoot, ".glassbox");
23315
+ const dataDir = join13(repoRoot, ".glassbox");
22229
23316
  const sent = await triggerChannel(dataDir, parsed.data.message);
22230
23317
  if (!sent) {
22231
23318
  return c.json({ error: "Channel not connected" }, 503);
@@ -22234,7 +23321,7 @@ channelApiRoutes.post("/trigger", async (c) => {
22234
23321
  });
22235
23322
  channelApiRoutes.get("/claude-check", (c) => {
22236
23323
  try {
22237
- const result = spawnSync8("claude", ["--version"], { encoding: "utf-8", timeout: 5e3 });
23324
+ const result = spawnSync9("claude", ["--version"], { encoding: "utf-8", timeout: 5e3 });
22238
23325
  if (result.status !== 0) {
22239
23326
  return c.json({ installed: false, version: null, meetsMinimum: false });
22240
23327
  }
@@ -22253,13 +23340,13 @@ channelApiRoutes.get("/claude-check", (c) => {
22253
23340
  });
22254
23341
 
22255
23342
  // src/routes/difftool-api.ts
22256
- import { Hono as Hono15 } from "hono";
23343
+ import { Hono as Hono16 } from "hono";
22257
23344
  init_connection();
22258
23345
  init_queries();
22259
23346
  init_blob_store();
22260
23347
  init_session();
22261
23348
  init_difftool();
22262
- var difftoolApiRoutes = new Hono15();
23349
+ var difftoolApiRoutes = new Hono16();
22263
23350
  difftoolApiRoutes.get("/status", (c) => {
22264
23351
  return c.json(getDifftoolStatus("global"));
22265
23352
  });
@@ -22321,9 +23408,9 @@ difftoolApiRoutes.get("/hold", (c) => {
22321
23408
  const session2 = getDifftoolSession();
22322
23409
  if (session2 === null) return c.json({ ended: true });
22323
23410
  noteDifftoolActivity();
22324
- return new Promise((resolve9) => {
23411
+ return new Promise((resolve11) => {
22325
23412
  addDifftoolHold(() => {
22326
- resolve9(c.json({ ended: true }));
23413
+ resolve11(c.json({ ended: true }));
22327
23414
  });
22328
23415
  c.req.raw.signal.addEventListener("abort", () => {
22329
23416
  endDifftoolSession();
@@ -22336,9 +23423,12 @@ difftoolApiRoutes.post("/end", (c) => {
22336
23423
  });
22337
23424
 
22338
23425
  // src/routes/pages.tsx
22339
- import { readFileSync as readFileSync12 } from "fs";
22340
- import { Hono as Hono16 } from "hono";
22341
- import { resolve as resolve7 } from "path";
23426
+ import { readFileSync as readFileSync14 } from "fs";
23427
+ import { Hono as Hono17 } from "hono";
23428
+ import { resolve as resolve8 } from "path";
23429
+
23430
+ // src/components/diffView.tsx
23431
+ import { raw } from "kerfjs";
22342
23432
 
22343
23433
  // src/icons.tsx
22344
23434
  import { jsx, jsxs } from "kerfjs/jsx-runtime";
@@ -22400,6 +23490,9 @@ function IconActualSize() {
22400
23490
  ] });
22401
23491
  }
22402
23492
 
23493
+ // src/components/diffView.tsx
23494
+ init_view();
23495
+
22403
23496
  // src/utils/charDiff.ts
22404
23497
  var MAX_LINE_LENGTH = 5e3;
22405
23498
  function charDiff(oldStr, newStr) {
@@ -22471,6 +23564,26 @@ function truncateDiffLine(content) {
22471
23564
  };
22472
23565
  }
22473
23566
 
23567
+ // src/utils/noteMarkdown.ts
23568
+ function escapeHtml(text) {
23569
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
23570
+ }
23571
+ var SAFE_URL = /^(https?:\/\/|mailto:)/i;
23572
+ function renderInline(escaped) {
23573
+ let out = escaped.replace(/`([^`]+)`/g, "<code>$1</code>");
23574
+ out = out.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (match, text, url2) => {
23575
+ if (!SAFE_URL.test(url2)) return match;
23576
+ return `<a href="${url2}" target="_blank" rel="noopener noreferrer">${text}</a>`;
23577
+ });
23578
+ out = out.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
23579
+ out = out.replace(/(^|[^*])\*([^*\s][^*]*)\*/g, "$1<em>$2</em>");
23580
+ out = out.replace(/(^|[^_])_([^_\s][^_]*)_/g, "$1<em>$2</em>");
23581
+ return out;
23582
+ }
23583
+ function renderNoteMarkdown(text) {
23584
+ return escapeHtml(text).split("\n").map(renderInline).join("<br>");
23585
+ }
23586
+
22474
23587
  // src/components/imageDiff.tsx
22475
23588
  import { jsx as jsx2, jsxs as jsxs2 } from "kerfjs/jsx-runtime";
22476
23589
  function ImageDiff({ file: file2, diff, fontWarning, baseWidth, baseHeight }) {
@@ -22521,12 +23634,23 @@ function ImageDiff({ file: file2, diff, fontWarning, baseWidth, baseHeight }) {
22521
23634
 
22522
23635
  // src/components/diffView.tsx
22523
23636
  import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "kerfjs/jsx-runtime";
22524
- function DiffView({ file: file2, diff, annotations, mode }) {
23637
+ function DiffView({ file: file2, diff, annotations, mode, reviewNotes = [] }) {
23638
+ const loadedNoteGuids = new Set(reviewNotes.map((n) => n.guid).filter((g) => g !== void 0));
23639
+ const repliesByNote = {};
22525
23640
  const annotationsByLine = {};
22526
23641
  for (const a of annotations) {
23642
+ if (a.reply_to_note_id !== null && loadedNoteGuids.has(a.reply_to_note_id)) {
23643
+ (repliesByNote[a.reply_to_note_id] ??= []).push(a);
23644
+ continue;
23645
+ }
22527
23646
  const key = `${a.line_number}:${a.side}`;
22528
- if (!(key in annotationsByLine)) annotationsByLine[key] = [];
22529
- annotationsByLine[key].push(a);
23647
+ (annotationsByLine[key] ??= []).push(a);
23648
+ }
23649
+ const reviewNotesByLine = {};
23650
+ for (const n of reviewNotes) {
23651
+ const key = `${n.line}:${n.side}`;
23652
+ if (!(key in reviewNotesByLine)) reviewNotesByLine[key] = [];
23653
+ reviewNotesByLine[key].push(n);
22530
23654
  }
22531
23655
  return /* @__PURE__ */ jsxs3(
22532
23656
  "div",
@@ -22543,7 +23667,7 @@ function DiffView({ file: file2, diff, annotations, mode }) {
22543
23667
  ] }),
22544
23668
  /* @__PURE__ */ jsx3("div", { className: "diff-header-actions", children: /* @__PURE__ */ jsx3("span", { className: `file-status ${diff.status}`, children: diff.status }) })
22545
23669
  ] }),
22546
- diff.isBinary && isImageFile(diff.filePath) ? /* @__PURE__ */ jsx3(ImageDiff, { file: file2, diff }) : diff.isBinary ? /* @__PURE__ */ jsx3("div", { className: "hunk-separator", children: "Binary file" }) : diff.status === "added" || diff.status === "deleted" || mode === "unified" ? /* @__PURE__ */ jsx3(UnifiedDiff, { hunks: diff.hunks, annotationsByLine }) : /* @__PURE__ */ jsx3(SplitDiff, { hunks: diff.hunks, annotationsByLine })
23670
+ diff.isBinary && isImageFile(diff.filePath) ? /* @__PURE__ */ jsx3(ImageDiff, { file: file2, diff }) : diff.isBinary ? /* @__PURE__ */ jsx3("div", { className: "hunk-separator", children: "Binary file" }) : diff.status === "added" || diff.status === "deleted" || mode === "unified" ? /* @__PURE__ */ jsx3(UnifiedDiff, { hunks: diff.hunks, annotationsByLine, reviewNotesByLine, repliesByNote }) : /* @__PURE__ */ jsx3(SplitDiff, { hunks: diff.hunks, annotationsByLine, reviewNotesByLine, repliesByNote })
22547
23671
  ]
22548
23672
  }
22549
23673
  );
@@ -22553,7 +23677,10 @@ function getAnnotations(pair, annotationsByLine) {
22553
23677
  const rightAnns = pair.right ? annotationsByLine[`${pair.right.newNum}:new`] ?? [] : [];
22554
23678
  return [...leftAnns, ...rightAnns];
22555
23679
  }
22556
- function SplitDiff({ hunks, annotationsByLine }) {
23680
+ function getReviewNotes(pair, reviewNotesByLine) {
23681
+ return pair.right ? reviewNotesByLine[`${pair.right.newNum}:new`] ?? [] : [];
23682
+ }
23683
+ function SplitDiff({ hunks, annotationsByLine, reviewNotesByLine, repliesByNote }) {
22557
23684
  const lastHunk = hunks[hunks.length - 1];
22558
23685
  const tailStart = lastHunk ? lastHunk.newStart + lastHunk.newCount : 1;
22559
23686
  const items = [];
@@ -22565,8 +23692,9 @@ function SplitDiff({ hunks, annotationsByLine }) {
22565
23692
  items.push({ kind: "separator", hunkIdx, hunk, gapStart, gapEnd });
22566
23693
  for (const pair of pairLines(hunk.lines)) {
22567
23694
  const anns = getAnnotations(pair, annotationsByLine);
22568
- if (anns.length > 0) {
22569
- items.push({ kind: "annotated", pair, annotations: anns });
23695
+ const notes = getReviewNotes(pair, reviewNotesByLine);
23696
+ if (anns.length > 0 || notes.length > 0) {
23697
+ items.push({ kind: "annotated", pair, annotations: anns, reviewNotes: notes });
22570
23698
  } else {
22571
23699
  items.push({ kind: "pair", pair });
22572
23700
  }
@@ -22581,7 +23709,7 @@ function SplitDiff({ hunks, annotationsByLine }) {
22581
23709
  groups.push({ type: "columns", items: run });
22582
23710
  run = [];
22583
23711
  }
22584
- groups.push({ type: "annotated", pair: item.pair, annotations: item.annotations });
23712
+ groups.push({ type: "annotated", pair: item.pair, annotations: item.annotations, reviewNotes: item.reviewNotes });
22585
23713
  } else {
22586
23714
  run.push(item);
22587
23715
  }
@@ -22617,7 +23745,8 @@ function SplitDiff({ hunks, annotationsByLine }) {
22617
23745
  }
22618
23746
  )
22619
23747
  ] }),
22620
- /* @__PURE__ */ jsx3(AnnotationRows, { annotations: group.annotations })
23748
+ group.annotations.length > 0 ? /* @__PURE__ */ jsx3(AnnotationRows, { annotations: group.annotations }) : null,
23749
+ group.reviewNotes.length > 0 ? /* @__PURE__ */ jsx3(ReviewNoteRows, { notes: group.reviewNotes, repliesByNote }) : null
22621
23750
  ] });
22622
23751
  }
22623
23752
  return /* @__PURE__ */ jsxs3("div", { className: "split-columns", children: [
@@ -22783,7 +23912,7 @@ function buildUnifiedCharDiffs(lines) {
22783
23912
  }
22784
23913
  return result;
22785
23914
  }
22786
- function UnifiedDiff({ hunks, annotationsByLine }) {
23915
+ function UnifiedDiff({ hunks, annotationsByLine, reviewNotesByLine, repliesByNote }) {
22787
23916
  const lastHunk = hunks[hunks.length - 1];
22788
23917
  const tailStart = lastHunk ? lastHunk.newStart + lastHunk.newCount : 1;
22789
23918
  return /* @__PURE__ */ jsxs3("div", { className: "diff-table-unified", children: [
@@ -22816,6 +23945,7 @@ function UnifiedDiff({ hunks, annotationsByLine }) {
22816
23945
  const lineNum = line.type === "remove" ? line.oldNum : line.newNum;
22817
23946
  const side = line.type === "remove" ? "old" : "new";
22818
23947
  const anns = annotationsByLine[`${lineNum}:${side}`] ?? [];
23948
+ const notes = reviewNotesByLine[`${lineNum}:${side}`] ?? [];
22819
23949
  const segments = charDiffs.get(line);
22820
23950
  return /* @__PURE__ */ jsxs3("div", { children: [
22821
23951
  /* @__PURE__ */ jsxs3(
@@ -22831,7 +23961,8 @@ function UnifiedDiff({ hunks, annotationsByLine }) {
22831
23961
  ]
22832
23962
  }
22833
23963
  ),
22834
- anns.length > 0 ? /* @__PURE__ */ jsx3(AnnotationRows, { annotations: anns }) : null
23964
+ anns.length > 0 ? /* @__PURE__ */ jsx3(AnnotationRows, { annotations: anns }) : null,
23965
+ notes.length > 0 ? /* @__PURE__ */ jsx3(ReviewNoteRows, { notes, repliesByNote }) : null
22835
23966
  ] });
22836
23967
  })
22837
23968
  ] });
@@ -22839,8 +23970,61 @@ function UnifiedDiff({ hunks, annotationsByLine }) {
22839
23970
  /* @__PURE__ */ jsx3("div", { className: "hunk-separator hunk-expander-tail", "data-start": tailStart, children: "\u2195 Show remaining lines" })
22840
23971
  ] });
22841
23972
  }
22842
- function AnnotationRows({ annotations }) {
22843
- return /* @__PURE__ */ jsx3("div", { className: "annotation-row", children: annotations.map((a) => /* @__PURE__ */ jsxs3(
23973
+ function ReviewNoteRows({ notes, repliesByNote }) {
23974
+ return /* @__PURE__ */ jsx3(Fragment, { children: notes.map((n) => {
23975
+ const replies = n.guid !== void 0 ? repliesByNote[n.guid] ?? [] : [];
23976
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
23977
+ /* @__PURE__ */ jsxs3(
23978
+ "div",
23979
+ {
23980
+ className: `ai-note-row ai-note-review${n.stale === true ? " ai-note-stale" : ""}`,
23981
+ "data-kind": n.kind,
23982
+ "data-note-id": n.guid,
23983
+ children: [
23984
+ /* @__PURE__ */ jsxs3("div", { className: "ai-note-item", children: [
23985
+ /* @__PURE__ */ jsx3("span", { className: `ai-note-label ai-note-label-${n.kind}`, children: REVIEW_NOTE_LABELS[n.kind] ?? n.kind }),
23986
+ n.stale === true ? /* @__PURE__ */ jsx3("span", { className: "ai-note-stale-tag", title: "The code this note referred to has changed", children: "outdated" }) : null,
23987
+ /* @__PURE__ */ jsx3("span", { className: "ai-note-text", children: raw(renderNoteMarkdown(n.body)) }),
23988
+ n.producer !== void 0 ? /* @__PURE__ */ jsx3("span", { className: "ai-note-producer", children: n.producer }) : null,
23989
+ n.guid !== void 0 ? /* @__PURE__ */ jsx3("button", { className: "ai-note-reply-btn", "data-line": String(n.line), children: "Reply" }) : null,
23990
+ n.stale === true && n.guid !== void 0 ? /* @__PURE__ */ jsxs3("span", { className: "ai-note-stale-actions", children: [
23991
+ /* @__PURE__ */ jsx3("button", { className: "ai-note-keep-btn", title: "Dismiss the outdated flag for now", children: "Keep" }),
23992
+ /* @__PURE__ */ jsx3("button", { className: "ai-note-discard-btn", title: "Remove this note from .pr-notes/", children: "Discard" })
23993
+ ] }) : null
23994
+ ] }),
23995
+ n.artifacts !== void 0 && n.artifacts.length > 0 ? /* @__PURE__ */ jsx3("div", { className: "ai-note-artifacts", children: n.artifacts.map((a) => a.content !== void 0 ? /* @__PURE__ */ jsxs3("details", { className: "ai-note-artifact", children: [
23996
+ /* @__PURE__ */ jsxs3("summary", { children: [
23997
+ "\u{1F4CE} ",
23998
+ a.uri
23999
+ ] }),
24000
+ /* @__PURE__ */ jsx3("pre", { className: "ai-note-artifact-content", children: /* @__PURE__ */ jsx3("code", { children: a.content }) })
24001
+ ] }) : a.isImage === true ? /* @__PURE__ */ jsxs3("details", { className: "ai-note-artifact", children: [
24002
+ /* @__PURE__ */ jsxs3("summary", { children: [
24003
+ "\u{1F4CE} ",
24004
+ a.uri
24005
+ ] }),
24006
+ /* @__PURE__ */ jsx3(
24007
+ "img",
24008
+ {
24009
+ className: "ai-note-artifact-img",
24010
+ loading: "lazy",
24011
+ alt: a.uri,
24012
+ src: `/api/review-notes/artifact?file=${encodeURIComponent(a.uri)}`
24013
+ }
24014
+ )
24015
+ ] }) : /* @__PURE__ */ jsxs3("div", { className: "ai-note-artifact ai-note-artifact-ref", children: [
24016
+ "\u{1F4CE} ",
24017
+ a.uri
24018
+ ] })) }) : null
24019
+ ]
24020
+ }
24021
+ ),
24022
+ replies.length > 0 ? /* @__PURE__ */ jsx3("div", { className: "annotation-row ai-note-replies", children: replies.map((a) => /* @__PURE__ */ jsx3(AnnotationItem, { annotation: a })) }) : null
24023
+ ] });
24024
+ }) });
24025
+ }
24026
+ function AnnotationItem({ annotation: a }) {
24027
+ return /* @__PURE__ */ jsxs3(
22844
24028
  "div",
22845
24029
  {
22846
24030
  className: `annotation-item${a.is_stale ? " annotation-stale" : ""}`,
@@ -22850,6 +24034,7 @@ function AnnotationRows({ annotations }) {
22850
24034
  children: [
22851
24035
  /* @__PURE__ */ jsx3("span", { className: "annotation-drag-handle", draggable: true, title: "Drag to move", children: "\u283F" }),
22852
24036
  /* @__PURE__ */ jsx3("span", { className: `annotation-category category-${a.category}`, "data-action": "reclassify", children: a.category }),
24037
+ a.reply_to_note_id !== null ? /* @__PURE__ */ jsx3("span", { className: "annotation-reply-tag", title: "Reply to an AI review note", children: "\u21B3 reply" }) : null,
22853
24038
  /* @__PURE__ */ jsx3("span", { className: "annotation-text", children: a.content }),
22854
24039
  /* @__PURE__ */ jsxs3("div", { className: "annotation-actions", children: [
22855
24040
  a.is_stale ? /* @__PURE__ */ jsx3("button", { className: "btn btn-xs btn-keep", "data-action": "keep", children: "Keep" }) : null,
@@ -22858,13 +24043,16 @@ function AnnotationRows({ annotations }) {
22858
24043
  ] })
22859
24044
  ]
22860
24045
  }
22861
- )) });
24046
+ );
24047
+ }
24048
+ function AnnotationRows({ annotations }) {
24049
+ return /* @__PURE__ */ jsx3("div", { className: "annotation-row", children: annotations.map((a) => /* @__PURE__ */ jsx3(AnnotationItem, { annotation: a })) });
22862
24050
  }
22863
24051
 
22864
24052
  // src/themes/config.ts
22865
- import { existsSync as existsSync9, mkdirSync as mkdirSync8, readdirSync, readFileSync as readFileSync11, unlinkSync as unlinkSync2, writeFileSync as writeFileSync8 } from "fs";
22866
- import { join as join12 } from "path";
22867
- var THEMES_DIR = join12(GLOBAL_CONFIG_DIR, "themes");
24053
+ import { existsSync as existsSync11, mkdirSync as mkdirSync9, readdirSync as readdirSync2, readFileSync as readFileSync13, unlinkSync as unlinkSync3, writeFileSync as writeFileSync9 } from "fs";
24054
+ import { join as join14 } from "path";
24055
+ var THEMES_DIR = join14(GLOBAL_CONFIG_DIR, "themes");
22868
24056
  function getActiveThemeId() {
22869
24057
  const config2 = readGlobalConfig();
22870
24058
  const theme = config2.theme;
@@ -22878,13 +24066,13 @@ function setActiveThemeId(id) {
22878
24066
  });
22879
24067
  }
22880
24068
  function loadCustomThemes() {
22881
- if (!existsSync9(THEMES_DIR)) return [];
24069
+ if (!existsSync11(THEMES_DIR)) return [];
22882
24070
  const themes = [];
22883
24071
  try {
22884
- const files = readdirSync(THEMES_DIR).filter((f) => f.endsWith(".json"));
24072
+ const files = readdirSync2(THEMES_DIR).filter((f) => f.endsWith(".json"));
22885
24073
  for (const file2 of files) {
22886
24074
  try {
22887
- const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync11(join12(THEMES_DIR, file2), "utf-8")));
24075
+ const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync13(join14(THEMES_DIR, file2), "utf-8")));
22888
24076
  if (!parsed.success) continue;
22889
24077
  const d = parsed.data;
22890
24078
  themes.push({ id: d.id, name: d.name, colors: d.colors, builtIn: false, baseTheme: d.baseTheme ?? "" });
@@ -22896,21 +24084,21 @@ function loadCustomThemes() {
22896
24084
  return themes;
22897
24085
  }
22898
24086
  function saveCustomTheme(theme) {
22899
- mkdirSync8(THEMES_DIR, { recursive: true });
22900
- const filePath = join12(THEMES_DIR, `${theme.id}.json`);
22901
- writeFileSync8(filePath, JSON.stringify(theme, null, 2), "utf-8");
24087
+ mkdirSync9(THEMES_DIR, { recursive: true });
24088
+ const filePath = join14(THEMES_DIR, `${theme.id}.json`);
24089
+ writeFileSync9(filePath, JSON.stringify(theme, null, 2), "utf-8");
22902
24090
  }
22903
24091
  function deleteCustomTheme(id) {
22904
- const filePath = join12(THEMES_DIR, `${id}.json`);
22905
- if (existsSync9(filePath)) {
22906
- unlinkSync2(filePath);
24092
+ const filePath = join14(THEMES_DIR, `${id}.json`);
24093
+ if (existsSync11(filePath)) {
24094
+ unlinkSync3(filePath);
22907
24095
  }
22908
24096
  }
22909
24097
  function getCustomTheme(id) {
22910
- const filePath = join12(THEMES_DIR, `${id}.json`);
22911
- if (!existsSync9(filePath)) return void 0;
24098
+ const filePath = join14(THEMES_DIR, `${id}.json`);
24099
+ if (!existsSync11(filePath)) return void 0;
22912
24100
  try {
22913
- const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync11(filePath, "utf-8")));
24101
+ const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync13(filePath, "utf-8")));
22914
24102
  if (!parsed.success) return void 0;
22915
24103
  const d = parsed.data;
22916
24104
  return { id: d.id, name: d.name, colors: d.colors, builtIn: false, baseTheme: d.baseTheme ?? "" };
@@ -22972,8 +24160,8 @@ function formatReviewMode(mode, modeArgs) {
22972
24160
  return `commit: ${shortenIfSha(argsFor(mode, "commit:", modeArgs))}`;
22973
24161
  }
22974
24162
  if (mode === "range" || mode.startsWith("range:")) {
22975
- const raw = argsFor(mode, "range:", modeArgs);
22976
- const [from = "", to = ""] = raw.split("..");
24163
+ const raw2 = argsFor(mode, "range:", modeArgs);
24164
+ const [from = "", to = ""] = raw2.split("..");
22977
24165
  return `range: ${shortenIfSha(from)}..${shortenIfSha(to)}`;
22978
24166
  }
22979
24167
  if (mode === "branch" || mode.startsWith("branch:")) {
@@ -23209,8 +24397,41 @@ function ReviewShell({ reviewId, review, files, annotationCounts, staleCounts, f
23209
24397
 
23210
24398
  // src/routes/pages.tsx
23211
24399
  init_queries();
24400
+
24401
+ // src/review-notes/reanchor.ts
24402
+ var MATCH_RADIUS = 50;
24403
+ function reanchorReviewNotes(notes, diff) {
24404
+ const byLine = /* @__PURE__ */ new Map();
24405
+ for (const hunk of diff.hunks) {
24406
+ for (const line of hunk.lines) {
24407
+ if (line.newNum !== null) byLine.set(line.newNum, line.content);
24408
+ }
24409
+ }
24410
+ return notes.map((note) => {
24411
+ if (note.snippet === void 0 || note.snippet === "") return note;
24412
+ const anchor = note.snippet.split("\n")[0];
24413
+ const current = byLine.get(note.line);
24414
+ if (current === anchor) return note;
24415
+ let best = null;
24416
+ let bestDistance = Infinity;
24417
+ for (const [lineNum, content] of byLine) {
24418
+ if (content !== anchor) continue;
24419
+ const distance = Math.abs(lineNum - note.line);
24420
+ if (distance < bestDistance && distance <= MATCH_RADIUS) {
24421
+ bestDistance = distance;
24422
+ best = lineNum;
24423
+ }
24424
+ }
24425
+ if (best !== null) return { ...note, line: best, stale: false };
24426
+ if (current !== void 0) return { ...note, stale: true };
24427
+ return note;
24428
+ });
24429
+ }
24430
+
24431
+ // src/routes/pages.tsx
24432
+ init_store();
23212
24433
  import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs8 } from "kerfjs/jsx-runtime";
23213
- var pageRoutes = new Hono16();
24434
+ var pageRoutes = new Hono17();
23214
24435
  pageRoutes.get("/", async (c) => {
23215
24436
  const reviewId = c.get("reviewId");
23216
24437
  const review = await getReview(reviewId);
@@ -23289,7 +24510,9 @@ pageRoutes.get("/file/:fileId", async (c) => {
23289
24510
  }
23290
24511
  }
23291
24512
  }
23292
- const html = /* @__PURE__ */ jsx8(DiffView, { file: file2, diff: finalDiff, annotations, mode });
24513
+ const rawNotes = getDemoMode() !== null ? demoReviewNotes(file2.file_path) : loadReviewNotesForFile(c.get("repoRoot"), file2.file_path);
24514
+ const reviewNotes = reanchorReviewNotes(rawNotes, finalDiff);
24515
+ const html = /* @__PURE__ */ jsx8(DiffView, { file: file2, diff: finalDiff, annotations, mode, reviewNotes });
23293
24516
  return c.html(html.toString());
23294
24517
  });
23295
24518
  pageRoutes.get("/file-raw", (c) => {
@@ -23298,7 +24521,7 @@ pageRoutes.get("/file-raw", (c) => {
23298
24521
  const repoRoot = c.get("repoRoot");
23299
24522
  let content;
23300
24523
  try {
23301
- content = readFileSync12(resolve7(repoRoot, filePath), "utf-8");
24524
+ content = readFileSync14(resolve8(repoRoot, filePath), "utf-8");
23302
24525
  } catch {
23303
24526
  return c.text("File not found", 404);
23304
24527
  }
@@ -23354,8 +24577,8 @@ pageRoutes.get("/history", async (c) => {
23354
24577
  });
23355
24578
 
23356
24579
  // src/routes/theme-api.ts
23357
- import { Hono as Hono17 } from "hono";
23358
- var themeApiRoutes = new Hono17();
24580
+ import { Hono as Hono18 } from "hono";
24581
+ var themeApiRoutes = new Hono18();
23359
24582
  themeApiRoutes.get("/", (c) => {
23360
24583
  const themes = getAllThemes();
23361
24584
  const activeId = getActiveThemeId();
@@ -23469,10 +24692,10 @@ themeApiRoutes.delete("/:id", (c) => {
23469
24692
 
23470
24693
  // src/server.ts
23471
24694
  function tryServe(appFetch, port) {
23472
- return new Promise((resolve9, reject) => {
24695
+ return new Promise((resolve11, reject) => {
23473
24696
  const server = serve({ fetch: appFetch, port, hostname: "127.0.0.1" });
23474
24697
  server.on("listening", () => {
23475
- resolve9({ port, server });
24698
+ resolve11({ port, server });
23476
24699
  });
23477
24700
  server.on("error", (err) => {
23478
24701
  reject(err);
@@ -23480,25 +24703,25 @@ function tryServe(appFetch, port) {
23480
24703
  });
23481
24704
  }
23482
24705
  async function startServer(port, reviewId, repoRoot, options) {
23483
- const app = new Hono18();
24706
+ const app = new Hono19();
23484
24707
  app.use("*", async (c, next) => {
23485
24708
  c.set("reviewId", reviewId);
23486
24709
  c.set("currentReviewId", reviewId);
23487
24710
  c.set("repoRoot", repoRoot);
23488
24711
  await next();
23489
24712
  });
23490
- const selfDir = dirname3(fileURLToPath2(import.meta.url));
23491
- const distDir = existsSync10(join13(selfDir, "client", "styles.css")) ? join13(selfDir, "client") : join13(selfDir, "..", "dist", "client");
24713
+ const selfDir = dirname4(fileURLToPath2(import.meta.url));
24714
+ const distDir = existsSync12(join15(selfDir, "client", "styles.css")) ? join15(selfDir, "client") : join15(selfDir, "..", "dist", "client");
23492
24715
  app.get("/static/styles.css", (c) => {
23493
- const css = readFileSync13(join13(distDir, "styles.css"), "utf-8");
24716
+ const css = readFileSync15(join15(distDir, "styles.css"), "utf-8");
23494
24717
  return c.text(css, 200, { "Content-Type": "text/css", "Cache-Control": "no-cache" });
23495
24718
  });
23496
24719
  app.get("/static/app.js", (c) => {
23497
- const js = readFileSync13(join13(distDir, "app.global.js"), "utf-8");
24720
+ const js = readFileSync15(join15(distDir, "app.global.js"), "utf-8");
23498
24721
  return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
23499
24722
  });
23500
24723
  app.get("/static/history.js", (c) => {
23501
- const js = readFileSync13(join13(distDir, "history.global.js"), "utf-8");
24724
+ const js = readFileSync15(join15(distDir, "history.global.js"), "utf-8");
23502
24725
  return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
23503
24726
  });
23504
24727
  app.get("/favicon.ico", (c) => c.body(null, 204));
@@ -23536,7 +24759,7 @@ async function startServer(port, reviewId, repoRoot, options) {
23536
24759
  try {
23537
24760
  const globalConfig2 = readGlobalConfig();
23538
24761
  if (globalConfig2.channelEnabled === true) {
23539
- const dataDir = join13(repoRoot, ".glassbox");
24762
+ const dataDir = join15(repoRoot, ".glassbox");
23540
24763
  registerChannel(dataDir);
23541
24764
  }
23542
24765
  } catch {
@@ -23551,8 +24774,8 @@ async function startServer(port, reviewId, repoRoot, options) {
23551
24774
  }
23552
24775
 
23553
24776
  // src/skills.ts
23554
- import { existsSync as existsSync11, mkdirSync as mkdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
23555
- import { join as join14 } from "path";
24777
+ import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync10 } from "fs";
24778
+ import { join as join16 } from "path";
23556
24779
  var SKILL_VERSION = 1;
23557
24780
  function versionHeader() {
23558
24781
  return `<!-- glassbox-skill-version: ${SKILL_VERSION} -->`;
@@ -23563,14 +24786,14 @@ function parseVersionHeader(content) {
23563
24786
  return parseInt(match[1], 10);
23564
24787
  }
23565
24788
  function updateFile(path, content) {
23566
- if (existsSync11(path)) {
23567
- const existing = readFileSync14(path, "utf-8");
24789
+ if (existsSync13(path)) {
24790
+ const existing = readFileSync16(path, "utf-8");
23568
24791
  const version2 = parseVersionHeader(existing);
23569
24792
  if (version2 !== null && version2 >= SKILL_VERSION) {
23570
24793
  return false;
23571
24794
  }
23572
24795
  }
23573
- writeFileSync9(path, content, "utf-8");
24796
+ writeFileSync10(path, content, "utf-8");
23574
24797
  return true;
23575
24798
  }
23576
24799
  function skillBody() {
@@ -23590,8 +24813,8 @@ function skillBody() {
23590
24813
  ].join("\n");
23591
24814
  }
23592
24815
  function ensureClaudeSkills(cwd) {
23593
- const dir = join14(cwd, ".claude", "skills", "glassbox");
23594
- mkdirSync9(dir, { recursive: true });
24816
+ const dir = join16(cwd, ".claude", "skills", "glassbox");
24817
+ mkdirSync10(dir, { recursive: true });
23595
24818
  const content = [
23596
24819
  "---",
23597
24820
  "name: glassbox",
@@ -23603,11 +24826,11 @@ function ensureClaudeSkills(cwd) {
23603
24826
  skillBody(),
23604
24827
  ""
23605
24828
  ].join("\n");
23606
- return updateFile(join14(dir, "SKILL.md"), content);
24829
+ return updateFile(join16(dir, "SKILL.md"), content);
23607
24830
  }
23608
24831
  function ensureCursorRules(cwd) {
23609
- const rulesDir = join14(cwd, ".cursor", "rules");
23610
- mkdirSync9(rulesDir, { recursive: true });
24832
+ const rulesDir = join16(cwd, ".cursor", "rules");
24833
+ mkdirSync10(rulesDir, { recursive: true });
23611
24834
  const content = [
23612
24835
  "---",
23613
24836
  "description: Read the latest Glassbox code review and apply all feedback annotations",
@@ -23618,11 +24841,11 @@ function ensureCursorRules(cwd) {
23618
24841
  skillBody(),
23619
24842
  ""
23620
24843
  ].join("\n");
23621
- return updateFile(join14(rulesDir, "glassbox.mdc"), content);
24844
+ return updateFile(join16(rulesDir, "glassbox.mdc"), content);
23622
24845
  }
23623
24846
  function ensureCopilotPrompts(cwd) {
23624
- const promptsDir = join14(cwd, ".github", "prompts");
23625
- mkdirSync9(promptsDir, { recursive: true });
24847
+ const promptsDir = join16(cwd, ".github", "prompts");
24848
+ mkdirSync10(promptsDir, { recursive: true });
23626
24849
  const content = [
23627
24850
  "---",
23628
24851
  "description: Read the latest Glassbox code review and apply all feedback annotations",
@@ -23632,11 +24855,11 @@ function ensureCopilotPrompts(cwd) {
23632
24855
  skillBody(),
23633
24856
  ""
23634
24857
  ].join("\n");
23635
- return updateFile(join14(promptsDir, "glassbox.prompt.md"), content);
24858
+ return updateFile(join16(promptsDir, "glassbox.prompt.md"), content);
23636
24859
  }
23637
24860
  function ensureWindsurfRules(cwd) {
23638
- const rulesDir = join14(cwd, ".windsurf", "rules");
23639
- mkdirSync9(rulesDir, { recursive: true });
24861
+ const rulesDir = join16(cwd, ".windsurf", "rules");
24862
+ mkdirSync10(rulesDir, { recursive: true });
23640
24863
  const content = [
23641
24864
  "---",
23642
24865
  "trigger: manual",
@@ -23647,21 +24870,21 @@ function ensureWindsurfRules(cwd) {
23647
24870
  skillBody(),
23648
24871
  ""
23649
24872
  ].join("\n");
23650
- return updateFile(join14(rulesDir, "glassbox.md"), content);
24873
+ return updateFile(join16(rulesDir, "glassbox.md"), content);
23651
24874
  }
23652
24875
  function ensureSkills() {
23653
24876
  const cwd = process.cwd();
23654
24877
  const platforms = [];
23655
- if (existsSync11(join14(cwd, ".claude"))) {
24878
+ if (existsSync13(join16(cwd, ".claude"))) {
23656
24879
  if (ensureClaudeSkills(cwd)) platforms.push("Claude Code");
23657
24880
  }
23658
- if (existsSync11(join14(cwd, ".cursor"))) {
24881
+ if (existsSync13(join16(cwd, ".cursor"))) {
23659
24882
  if (ensureCursorRules(cwd)) platforms.push("Cursor");
23660
24883
  }
23661
- if (existsSync11(join14(cwd, ".github", "prompts")) || existsSync11(join14(cwd, ".github", "copilot-instructions.md"))) {
24884
+ if (existsSync13(join16(cwd, ".github", "prompts")) || existsSync13(join16(cwd, ".github", "copilot-instructions.md"))) {
23662
24885
  if (ensureCopilotPrompts(cwd)) platforms.push("GitHub Copilot");
23663
24886
  }
23664
- if (existsSync11(join14(cwd, ".windsurf"))) {
24887
+ if (existsSync13(join16(cwd, ".windsurf"))) {
23665
24888
  if (ensureWindsurfRules(cwd)) platforms.push("Windsurf");
23666
24889
  }
23667
24890
  return platforms;
@@ -23669,36 +24892,36 @@ function ensureSkills() {
23669
24892
 
23670
24893
  // src/update-check.ts
23671
24894
  init_zod();
23672
- import { existsSync as existsSync12, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
24895
+ import { existsSync as existsSync14, mkdirSync as mkdirSync11, readFileSync as readFileSync17, writeFileSync as writeFileSync11 } from "fs";
23673
24896
  import { get } from "https";
23674
24897
  import { homedir as homedir3 } from "os";
23675
- import { dirname as dirname4, join as join15 } from "path";
24898
+ import { dirname as dirname5, join as join17 } from "path";
23676
24899
  import { fileURLToPath as fileURLToPath3 } from "url";
23677
- var DATA_DIR = join15(homedir3(), ".glassbox");
23678
- var CHECK_FILE = join15(DATA_DIR, "last-update-check");
24900
+ var DATA_DIR = join17(homedir3(), ".glassbox");
24901
+ var CHECK_FILE = join17(DATA_DIR, "last-update-check");
23679
24902
  var PACKAGE_NAME = "glassbox";
23680
24903
  var VersionPayloadSchema = external_exports.object({ version: external_exports.string() });
23681
24904
  function getCurrentVersion() {
23682
24905
  try {
23683
- const dir = dirname4(fileURLToPath3(import.meta.url));
23684
- const raw = JSON.parse(readFileSync15(join15(dir, "..", "package.json"), "utf-8"));
23685
- return VersionPayloadSchema.parse(raw).version;
24906
+ const dir = dirname5(fileURLToPath3(import.meta.url));
24907
+ const raw2 = JSON.parse(readFileSync17(join17(dir, "..", "package.json"), "utf-8"));
24908
+ return VersionPayloadSchema.parse(raw2).version;
23686
24909
  } catch {
23687
24910
  return "0.0.0";
23688
24911
  }
23689
24912
  }
23690
24913
  function getLastCheckDate() {
23691
24914
  try {
23692
- if (existsSync12(CHECK_FILE)) {
23693
- return readFileSync15(CHECK_FILE, "utf-8").trim();
24915
+ if (existsSync14(CHECK_FILE)) {
24916
+ return readFileSync17(CHECK_FILE, "utf-8").trim();
23694
24917
  }
23695
24918
  } catch {
23696
24919
  }
23697
24920
  return null;
23698
24921
  }
23699
24922
  function saveCheckDate() {
23700
- mkdirSync10(DATA_DIR, { recursive: true });
23701
- writeFileSync10(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
24923
+ mkdirSync11(DATA_DIR, { recursive: true });
24924
+ writeFileSync11(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
23702
24925
  }
23703
24926
  function isFirstUseToday() {
23704
24927
  const last = getLastCheckDate();
@@ -23707,10 +24930,10 @@ function isFirstUseToday() {
23707
24930
  return last !== today;
23708
24931
  }
23709
24932
  function fetchLatestVersion() {
23710
- return new Promise((resolve9) => {
24933
+ return new Promise((resolve11) => {
23711
24934
  const req = get(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { timeout: 5e3 }, (res) => {
23712
24935
  if (res.statusCode !== 200) {
23713
- resolve9(null);
24936
+ resolve11(null);
23714
24937
  return;
23715
24938
  }
23716
24939
  let data = "";
@@ -23719,19 +24942,19 @@ function fetchLatestVersion() {
23719
24942
  });
23720
24943
  res.on("end", () => {
23721
24944
  try {
23722
- const raw = JSON.parse(data);
23723
- resolve9(VersionPayloadSchema.parse(raw).version);
24945
+ const raw2 = JSON.parse(data);
24946
+ resolve11(VersionPayloadSchema.parse(raw2).version);
23724
24947
  } catch {
23725
- resolve9(null);
24948
+ resolve11(null);
23726
24949
  }
23727
24950
  });
23728
24951
  });
23729
24952
  req.on("error", () => {
23730
- resolve9(null);
24953
+ resolve11(null);
23731
24954
  });
23732
24955
  req.on("timeout", () => {
23733
24956
  req.destroy();
23734
- resolve9(null);
24957
+ resolve11(null);
23735
24958
  });
23736
24959
  });
23737
24960
  }
@@ -23881,14 +25104,14 @@ function parseArgs(argv) {
23881
25104
  console.error("--diff requires two paths: --diff <pathA> <pathB>");
23882
25105
  process.exit(1);
23883
25106
  }
23884
- mode = { type: "diff", pathA: resolve8(args[++i]), pathB: resolve8(args[++i]) };
25107
+ mode = { type: "diff", pathA: resolve10(args[++i]), pathB: resolve10(args[++i]) };
23885
25108
  break;
23886
25109
  }
23887
25110
  case "--port":
23888
25111
  port = parseInt(args[++i], 10);
23889
25112
  break;
23890
25113
  case "--data-dir":
23891
- dataDir = resolve8(args[++i]);
25114
+ dataDir = resolve10(args[++i]);
23892
25115
  break;
23893
25116
  case "--resume":
23894
25117
  resume = true;
@@ -23946,6 +25169,17 @@ function parseArgs(argv) {
23946
25169
  return { mode, port, dataDir, resume, forceUpdateCheck, debug, aiServiceTest, demo, noOpen, strictPort, projectDir, difftoolAction, difftoolLocal, difftoolForce, difftoolServe };
23947
25170
  }
23948
25171
  async function main() {
25172
+ const rawArgs = process.argv.slice(2);
25173
+ if (rawArgs[0] === "note") {
25174
+ const { runNoteCli: runNoteCli2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
25175
+ try {
25176
+ await runNoteCli2(rawArgs.slice(1));
25177
+ process.exit(0);
25178
+ } catch (err) {
25179
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
25180
+ process.exit(1);
25181
+ }
25182
+ }
23949
25183
  const parsed = parseArgs(process.argv);
23950
25184
  if (!parsed) {
23951
25185
  printUsage();
@@ -23987,19 +25221,19 @@ async function main() {
23987
25221
  console.log("AI service test mode enabled \u2014 using mock AI responses");
23988
25222
  }
23989
25223
  if (debug) {
23990
- console.log(`[debug] Build timestamp: ${"2026-06-17T13:45:47.895Z"}`);
25224
+ console.log(`[debug] Build timestamp: ${"2026-06-19T00:30:57.390Z"}`);
23991
25225
  }
23992
25226
  if (projectDir !== null) {
23993
25227
  process.chdir(projectDir);
23994
25228
  }
23995
25229
  if (dataDir === null) {
23996
- dataDir = join17(process.cwd(), ".glassbox");
25230
+ dataDir = join19(process.cwd(), ".glassbox");
23997
25231
  }
23998
25232
  if (difftoolServe) {
23999
25233
  const { initDifftoolSession: initDifftoolSession2 } = await Promise.resolve().then(() => (init_session(), session_exports));
24000
25234
  const { writeDiscovery: writeDiscovery2, clearDiscovery: clearDiscovery2, releaseStartingLock: releaseStartingLock2 } = await Promise.resolve().then(() => (init_difftool_discovery(), difftool_discovery_exports));
24001
25235
  const { clearDifftoolBlobs: clearDifftoolBlobs2 } = await Promise.resolve().then(() => (init_blob_store(), blob_store_exports));
24002
- mkdirSync12(dataDir, { recursive: true });
25236
+ mkdirSync13(dataDir, { recursive: true });
24003
25237
  setDataDir(dataDir);
24004
25238
  const sessionDataDir = dataDir;
24005
25239
  clearDifftoolBlobs2(sessionDataDir);
@@ -24034,13 +25268,13 @@ async function main() {
24034
25268
  }
24035
25269
  process.exit(1);
24036
25270
  }
24037
- dataDir = join17(tmpdir2(), `glassbox-demo-${demo}-${Date.now()}`);
25271
+ dataDir = join19(tmpdir2(), `glassbox-demo-${demo}-${Date.now()}`);
24038
25272
  setDemoMode(demo);
24039
25273
  console.log(`
24040
25274
  DEMO MODE: ${scenario.label}
24041
25275
  `);
24042
25276
  }
24043
- mkdirSync12(dataDir, { recursive: true });
25277
+ mkdirSync13(dataDir, { recursive: true });
24044
25278
  if (demo === null) {
24045
25279
  acquireLock(dataDir);
24046
25280
  }
@@ -24062,12 +25296,12 @@ async function main() {
24062
25296
  if (mode.type === "diff") {
24063
25297
  const { pathA, pathB } = mode;
24064
25298
  for (const p of [pathA, pathB]) {
24065
- if (!existsSync14(p)) {
25299
+ if (!existsSync16(p)) {
24066
25300
  console.error(`Error: path does not exist: ${p}`);
24067
25301
  process.exit(1);
24068
25302
  }
24069
25303
  }
24070
- if (statSync3(pathA).isDirectory() !== statSync3(pathB).isDirectory()) {
25304
+ if (statSync5(pathA).isDirectory() !== statSync5(pathB).isDirectory()) {
24071
25305
  console.error("Error: --diff requires two files or two folders, not a mix of both.");
24072
25306
  process.exit(1);
24073
25307
  }