glassbox 0.14.0 → 0.15.0-beta.6
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 +1620 -322
- package/dist/client/app.global.js +33 -32
- package/dist/client/styles.css +1 -1
- package/package.json +3 -1
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,
|
|
15373
|
-
if (
|
|
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(
|
|
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
|
|
15730
|
-
import { join as
|
|
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
|
|
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
|
-
|
|
15741
|
-
|
|
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 =
|
|
15745
|
-
if (!
|
|
16194
|
+
const path = join9(blobDir(dataDir), blobName(fileId, side));
|
|
16195
|
+
if (!existsSync8(path)) return null;
|
|
15746
16196
|
try {
|
|
15747
|
-
return
|
|
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
|
|
16303
|
+
import { spawnSync as spawnSync10 } from "child_process";
|
|
15854
16304
|
function git2(args, cwd) {
|
|
15855
|
-
const r =
|
|
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
|
|
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
|
|
16720
|
+
import { join as join18 } from "path";
|
|
15937
16721
|
function difftoolHome() {
|
|
15938
|
-
return
|
|
16722
|
+
return join18(homedir4(), ".glassbox");
|
|
15939
16723
|
}
|
|
15940
16724
|
function discoveryPath(home = difftoolHome()) {
|
|
15941
|
-
return
|
|
16725
|
+
return join18(home, "difftool.lock");
|
|
15942
16726
|
}
|
|
15943
16727
|
function startingLockPath(home = difftoolHome()) {
|
|
15944
|
-
return
|
|
16728
|
+
return join18(home, "difftool-starting.lock");
|
|
15945
16729
|
}
|
|
15946
|
-
function parseDiscovery(
|
|
16730
|
+
function parseDiscovery(raw2) {
|
|
15947
16731
|
let parsed;
|
|
15948
16732
|
try {
|
|
15949
|
-
parsed = JSON.parse(
|
|
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 (!
|
|
16742
|
+
if (!existsSync15(path)) return null;
|
|
15959
16743
|
try {
|
|
15960
|
-
return parseDiscovery(
|
|
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
|
-
|
|
15967
|
-
|
|
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
|
-
|
|
16760
|
+
mkdirSync12(home, { recursive: true });
|
|
15977
16761
|
const path = startingLockPath(home);
|
|
15978
16762
|
try {
|
|
15979
|
-
|
|
16763
|
+
writeFileSync12(path, String(process.pid), { flag: "wx" });
|
|
15980
16764
|
return true;
|
|
15981
16765
|
} catch {
|
|
15982
16766
|
try {
|
|
15983
|
-
const ageMs = Date.now() -
|
|
16767
|
+
const ageMs = Date.now() - statSync4(path).mtimeMs;
|
|
15984
16768
|
if (ageMs > STARTING_LOCK_STALE_MS) {
|
|
15985
16769
|
rmSync5(path, { force: true });
|
|
15986
|
-
|
|
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
|
|
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
|
|
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;
|
|
@@ -16054,13 +16838,18 @@ import { chmodSync, existsSync, mkdirSync as mkdirSync2, readFileSync, writeFile
|
|
|
16054
16838
|
import { homedir } from "os";
|
|
16055
16839
|
import { join as join2 } from "path";
|
|
16056
16840
|
var GlobalConfigSchema = external_exports.record(external_exports.string(), external_exports.unknown());
|
|
16057
|
-
|
|
16841
|
+
function resolveGlobalConfigDir() {
|
|
16842
|
+
const override = process.env.GLASSBOX_CONFIG_DIR;
|
|
16843
|
+
if (override !== void 0 && override.trim() !== "") return override;
|
|
16844
|
+
return join2(homedir(), ".glassbox");
|
|
16845
|
+
}
|
|
16846
|
+
var GLOBAL_CONFIG_DIR = resolveGlobalConfigDir();
|
|
16058
16847
|
var GLOBAL_CONFIG_PATH = join2(GLOBAL_CONFIG_DIR, "config.json");
|
|
16059
16848
|
function readGlobalConfig() {
|
|
16060
16849
|
try {
|
|
16061
16850
|
if (existsSync(GLOBAL_CONFIG_PATH)) {
|
|
16062
|
-
const
|
|
16063
|
-
const parsed = GlobalConfigSchema.safeParse(
|
|
16851
|
+
const raw2 = JSON.parse(readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
|
|
16852
|
+
const parsed = GlobalConfigSchema.safeParse(raw2);
|
|
16064
16853
|
if (parsed.success) return parsed.data;
|
|
16065
16854
|
}
|
|
16066
16855
|
} catch {
|
|
@@ -16187,12 +16976,17 @@ var AIModelSchema = external_exports.object({
|
|
|
16187
16976
|
contextWindow: external_exports.number(),
|
|
16188
16977
|
isDefault: external_exports.boolean()
|
|
16189
16978
|
});
|
|
16190
|
-
var AIPlatformSchema = external_exports.enum(["anthropic", "openai", "google"]);
|
|
16979
|
+
var AIPlatformSchema = external_exports.enum(["anthropic", "openai", "google", "local", "apple"]);
|
|
16980
|
+
var KEYLESS_PLATFORMS = /* @__PURE__ */ new Set(["local", "apple"]);
|
|
16191
16981
|
var PLATFORMS = {
|
|
16192
16982
|
anthropic: "Anthropic",
|
|
16193
16983
|
openai: "OpenAI",
|
|
16194
|
-
google: "Google"
|
|
16984
|
+
google: "Google",
|
|
16985
|
+
local: "Local",
|
|
16986
|
+
apple: "Apple"
|
|
16195
16987
|
};
|
|
16988
|
+
var APPLE_ON_DEVICE_MODEL_ID = "apple-on-device";
|
|
16989
|
+
var APPLE_FM_ANALYSIS_ENABLED = true;
|
|
16196
16990
|
var MODELS = {
|
|
16197
16991
|
anthropic: [
|
|
16198
16992
|
{ id: "claude-opus-4-8", name: "Claude Opus 4.8", contextWindow: 1e6, isDefault: false },
|
|
@@ -16206,12 +17000,28 @@ var MODELS = {
|
|
|
16206
17000
|
google: [
|
|
16207
17001
|
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", contextWindow: 1e6, isDefault: true },
|
|
16208
17002
|
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", contextWindow: 1e6, isDefault: false }
|
|
17003
|
+
],
|
|
17004
|
+
// Local models are server-specific and discovered live from the configured
|
|
17005
|
+
// endpoint; this is only the fallback default when discovery is unavailable.
|
|
17006
|
+
local: [
|
|
17007
|
+
{ id: "llama3.1", name: "Llama 3.1", contextWindow: 8192, isDefault: true }
|
|
17008
|
+
],
|
|
17009
|
+
// On-device Apple Foundation Models — a single fixed entry (no discovery).
|
|
17010
|
+
// The on-device model has a small context window, so batch conservatively.
|
|
17011
|
+
apple: [
|
|
17012
|
+
{ id: APPLE_ON_DEVICE_MODEL_ID, name: "Apple On-Device", contextWindow: 4096, isDefault: true }
|
|
16209
17013
|
]
|
|
16210
17014
|
};
|
|
16211
17015
|
var ENV_KEY_NAMES = {
|
|
16212
17016
|
anthropic: "ANTHROPIC_API_KEY",
|
|
16213
17017
|
openai: "OPENAI_API_KEY",
|
|
16214
|
-
google: "GEMINI_API_KEY"
|
|
17018
|
+
google: "GEMINI_API_KEY",
|
|
17019
|
+
// Optional — most local servers (Ollama) need no key; some do.
|
|
17020
|
+
local: "GLASSBOX_LOCAL_API_KEY",
|
|
17021
|
+
// Apple Foundation Models are keyless (on-device). This name is never read
|
|
17022
|
+
// for auth — `apple` is in KEYLESS_PLATFORMS — but the record requires an
|
|
17023
|
+
// entry per platform.
|
|
17024
|
+
apple: "GLASSBOX_APPLE_API_KEY"
|
|
16215
17025
|
};
|
|
16216
17026
|
function getDefaultModel(platform) {
|
|
16217
17027
|
const models = MODELS[platform];
|
|
@@ -16220,7 +17030,10 @@ function getDefaultModel(platform) {
|
|
|
16220
17030
|
}
|
|
16221
17031
|
function getModelContextWindow(platform, modelId) {
|
|
16222
17032
|
const model = MODELS[platform].find((m) => m.id === modelId);
|
|
16223
|
-
|
|
17033
|
+
if (model) return model.contextWindow;
|
|
17034
|
+
if (platform === "local") return 8192;
|
|
17035
|
+
if (platform === "apple") return 4096;
|
|
17036
|
+
return 128e3;
|
|
16224
17037
|
}
|
|
16225
17038
|
function modelFamily(id) {
|
|
16226
17039
|
const lower = id.toLowerCase();
|
|
@@ -16273,8 +17086,8 @@ function saveAPIKey(platform, key, storage) {
|
|
|
16273
17086
|
if (storage === "keychain") {
|
|
16274
17087
|
saveKeyToKeychain(platform, key);
|
|
16275
17088
|
} else {
|
|
16276
|
-
updateGlobalConfig((
|
|
16277
|
-
const parsed = ConfigFileSchema.safeParse(
|
|
17089
|
+
updateGlobalConfig((raw2) => {
|
|
17090
|
+
const parsed = ConfigFileSchema.safeParse(raw2);
|
|
16278
17091
|
const cfg = parsed.success ? parsed.data : {};
|
|
16279
17092
|
cfg.ai ??= {};
|
|
16280
17093
|
cfg.ai.keys ??= {};
|
|
@@ -16298,8 +17111,8 @@ function deleteAPIKey(platform) {
|
|
|
16298
17111
|
} catch {
|
|
16299
17112
|
}
|
|
16300
17113
|
if (readConfigFile().ai?.keys === void 0) return;
|
|
16301
|
-
updateGlobalConfig((
|
|
16302
|
-
const parsed = ConfigFileSchema.safeParse(
|
|
17114
|
+
updateGlobalConfig((raw2) => {
|
|
17115
|
+
const parsed = ConfigFileSchema.safeParse(raw2);
|
|
16303
17116
|
const cfg = parsed.success ? parsed.data : {};
|
|
16304
17117
|
if (cfg.ai?.keys !== void 0) {
|
|
16305
17118
|
cfg.ai.keys[platform] = "";
|
|
@@ -16320,11 +17133,21 @@ function detectAvailablePlatforms() {
|
|
|
16320
17133
|
}
|
|
16321
17134
|
|
|
16322
17135
|
// src/ai/config.ts
|
|
17136
|
+
var DEFAULT_LOCAL_ENDPOINT = "http://localhost:11434/v1";
|
|
17137
|
+
function resolveLocalEndpoint() {
|
|
17138
|
+
const configured = readConfigFile().ai?.localEndpoint?.trim();
|
|
17139
|
+
const base = configured !== void 0 && configured !== "" ? configured : DEFAULT_LOCAL_ENDPOINT;
|
|
17140
|
+
return base.replace(/\/+$/, "");
|
|
17141
|
+
}
|
|
16323
17142
|
var ConfigFileSchema = external_exports.object({
|
|
16324
17143
|
ai: external_exports.object({
|
|
16325
17144
|
platform: external_exports.string().optional(),
|
|
16326
17145
|
model: external_exports.string().optional(),
|
|
16327
|
-
keys: external_exports.record(external_exports.string(), external_exports.string()).optional()
|
|
17146
|
+
keys: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
17147
|
+
localEndpoint: external_exports.string().optional(),
|
|
17148
|
+
// Secondary model used when the primary (Apple FM) fails a batch.
|
|
17149
|
+
fallbackPlatform: external_exports.string().optional(),
|
|
17150
|
+
fallbackModel: external_exports.string().optional()
|
|
16328
17151
|
}).optional(),
|
|
16329
17152
|
guidedReview: external_exports.object({
|
|
16330
17153
|
enabled: external_exports.boolean().optional(),
|
|
@@ -16332,25 +17155,60 @@ var ConfigFileSchema = external_exports.object({
|
|
|
16332
17155
|
}).optional()
|
|
16333
17156
|
}).loose();
|
|
16334
17157
|
function readConfigFile() {
|
|
16335
|
-
const
|
|
16336
|
-
const parsed = ConfigFileSchema.safeParse(
|
|
17158
|
+
const raw2 = readGlobalConfig();
|
|
17159
|
+
const parsed = ConfigFileSchema.safeParse(raw2);
|
|
16337
17160
|
return parsed.success ? parsed.data : {};
|
|
16338
17161
|
}
|
|
17162
|
+
function resolvePlatformConfig(platform, rawModelOrUndefined) {
|
|
17163
|
+
const rawModel = rawModelOrUndefined ?? getDefaultModel(platform);
|
|
17164
|
+
const model = KEYLESS_PLATFORMS.has(platform) ? rawModel : resolveModelId(platform, rawModel);
|
|
17165
|
+
const { key, source } = resolveAPIKey(platform);
|
|
17166
|
+
const baseUrl = platform === "local" ? resolveLocalEndpoint() : void 0;
|
|
17167
|
+
return { platform, model, apiKey: key, keySource: source, baseUrl };
|
|
17168
|
+
}
|
|
16339
17169
|
function loadAIConfig() {
|
|
16340
17170
|
const config2 = readConfigFile();
|
|
16341
17171
|
const platformRaw = config2.ai?.platform ?? "anthropic";
|
|
16342
|
-
|
|
16343
|
-
|
|
16344
|
-
const
|
|
16345
|
-
|
|
16346
|
-
|
|
16347
|
-
|
|
17172
|
+
let platform = AIPlatformSchema.safeParse(platformRaw).success ? AIPlatformSchema.parse(platformRaw) : "anthropic";
|
|
17173
|
+
if (platform === "apple" && !APPLE_FM_ANALYSIS_ENABLED) platform = "anthropic";
|
|
17174
|
+
const primary = resolvePlatformConfig(platform, config2.ai?.model);
|
|
17175
|
+
if (platform === "apple") {
|
|
17176
|
+
const fallback = resolveFallbackConfig(config2);
|
|
17177
|
+
if (fallback !== null) primary.fallback = fallback;
|
|
17178
|
+
}
|
|
17179
|
+
return primary;
|
|
17180
|
+
}
|
|
17181
|
+
function fallbackSelectionFrom(config2) {
|
|
17182
|
+
const raw2 = config2.ai?.fallbackPlatform;
|
|
17183
|
+
if (raw2 === void 0 || raw2 === "") return null;
|
|
17184
|
+
const parsed = AIPlatformSchema.safeParse(raw2);
|
|
17185
|
+
if (!parsed.success || parsed.data === "apple") return null;
|
|
17186
|
+
return { platform: parsed.data, model: config2.ai?.fallbackModel ?? getDefaultModel(parsed.data) };
|
|
17187
|
+
}
|
|
17188
|
+
function loadFallbackSelection() {
|
|
17189
|
+
return fallbackSelectionFrom(readConfigFile());
|
|
17190
|
+
}
|
|
17191
|
+
function resolveFallbackConfig(config2) {
|
|
17192
|
+
const sel = fallbackSelectionFrom(config2);
|
|
17193
|
+
return sel === null ? null : resolvePlatformConfig(sel.platform, sel.model);
|
|
17194
|
+
}
|
|
17195
|
+
function saveAIConfigPreferences(platform, model, opts = {}) {
|
|
16348
17196
|
updateGlobalConfig((config2) => {
|
|
16349
17197
|
const parsed = ConfigFileSchema.safeParse(config2);
|
|
16350
17198
|
const cfg = parsed.success ? parsed.data : {};
|
|
16351
17199
|
cfg.ai ??= {};
|
|
16352
17200
|
cfg.ai.platform = platform;
|
|
16353
17201
|
cfg.ai.model = model;
|
|
17202
|
+
if (opts.localEndpoint !== void 0) {
|
|
17203
|
+
const trimmed = opts.localEndpoint.trim();
|
|
17204
|
+
cfg.ai.localEndpoint = trimmed === "" ? void 0 : trimmed;
|
|
17205
|
+
}
|
|
17206
|
+
if (opts.fallbackPlatform !== void 0) {
|
|
17207
|
+
const fp = opts.fallbackPlatform.trim();
|
|
17208
|
+
const fm = opts.fallbackModel?.trim();
|
|
17209
|
+
cfg.ai.fallbackPlatform = fp === "" ? void 0 : fp;
|
|
17210
|
+
cfg.ai.fallbackModel = fp === "" || fm === void 0 || fm === "" ? void 0 : fm;
|
|
17211
|
+
}
|
|
16354
17212
|
return cfg;
|
|
16355
17213
|
});
|
|
16356
17214
|
}
|
|
@@ -16495,14 +17353,14 @@ async function getUserPreferences() {
|
|
|
16495
17353
|
last_image_mode: "metadata"
|
|
16496
17354
|
};
|
|
16497
17355
|
if (result.rows.length === 0) return defaults;
|
|
16498
|
-
const
|
|
17356
|
+
const raw2 = result.rows[0];
|
|
16499
17357
|
const merged = {
|
|
16500
|
-
sort_mode: typeof
|
|
16501
|
-
risk_sort_dimension: typeof
|
|
16502
|
-
show_risk_scores: typeof
|
|
16503
|
-
ignore_whitespace: typeof
|
|
16504
|
-
svg_view_mode: typeof
|
|
16505
|
-
last_image_mode: typeof
|
|
17358
|
+
sort_mode: typeof raw2.sort_mode === "string" ? raw2.sort_mode : defaults.sort_mode,
|
|
17359
|
+
risk_sort_dimension: typeof raw2.risk_sort_dimension === "string" ? raw2.risk_sort_dimension : defaults.risk_sort_dimension,
|
|
17360
|
+
show_risk_scores: typeof raw2.show_risk_scores === "boolean" ? raw2.show_risk_scores : defaults.show_risk_scores,
|
|
17361
|
+
ignore_whitespace: typeof raw2.ignore_whitespace === "boolean" ? raw2.ignore_whitespace : defaults.ignore_whitespace,
|
|
17362
|
+
svg_view_mode: typeof raw2.svg_view_mode === "string" ? raw2.svg_view_mode : defaults.svg_view_mode,
|
|
17363
|
+
last_image_mode: typeof raw2.last_image_mode === "string" ? raw2.last_image_mode : defaults.last_image_mode
|
|
16506
17364
|
};
|
|
16507
17365
|
return UserPreferencesSchema.parse(merged);
|
|
16508
17366
|
}
|
|
@@ -16526,6 +17384,17 @@ async function saveUserPreferences(prefs) {
|
|
|
16526
17384
|
|
|
16527
17385
|
// src/demo.ts
|
|
16528
17386
|
init_queries();
|
|
17387
|
+
function demoReviewNotes(filePath) {
|
|
17388
|
+
if (filePath !== "src/auth/session.ts") return [];
|
|
17389
|
+
return [
|
|
17390
|
+
{ 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" },
|
|
17391
|
+
{ 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" }] },
|
|
17392
|
+
{ 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 }] },
|
|
17393
|
+
// Re-anchoring showcase (P3): authored against a 16-byte id, but the code
|
|
17394
|
+
// now uses 32 bytes, so the note no longer matches and renders as stale.
|
|
17395
|
+
{ 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');" }
|
|
17396
|
+
];
|
|
17397
|
+
}
|
|
16529
17398
|
var DEMO_SCENARIOS = [
|
|
16530
17399
|
{ id: 1, label: "Main UI with guided review notes" },
|
|
16531
17400
|
{ id: 2, label: "Risk mode with inline risk notes" },
|
|
@@ -16922,6 +17791,9 @@ var NARRATIVE_ORDER = [
|
|
|
16922
17791
|
{ 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
17792
|
];
|
|
16924
17793
|
var ANNOTATIONS = [
|
|
17794
|
+
// A reviewer reply to the line-31 risk review note (doc 20 threading) —
|
|
17795
|
+
// renders nested beneath that note.
|
|
17796
|
+
{ 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
17797
|
{ 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
17798
|
{ 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
17799
|
{ 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 +17906,7 @@ async function setupAnnotations(fileIdMap) {
|
|
|
17034
17906
|
for (const ann of ANNOTATIONS) {
|
|
17035
17907
|
const fileId = fileIdMap.get(ann.filePath);
|
|
17036
17908
|
if (fileId !== void 0) {
|
|
17037
|
-
await addAnnotation(fileId, ann.line, ann.side, ann.category, ann.content);
|
|
17909
|
+
await addAnnotation(fileId, ann.line, ann.side, ann.category, ann.content, ann.replyToNoteId);
|
|
17038
17910
|
}
|
|
17039
17911
|
}
|
|
17040
17912
|
}
|
|
@@ -17120,11 +17992,11 @@ function emptyFileDiff(filePath = "") {
|
|
|
17120
17992
|
isBinary: false
|
|
17121
17993
|
};
|
|
17122
17994
|
}
|
|
17123
|
-
function parseDiffData(
|
|
17124
|
-
if (
|
|
17995
|
+
function parseDiffData(raw2) {
|
|
17996
|
+
if (raw2 === null || raw2 === void 0 || raw2 === "") return null;
|
|
17125
17997
|
let parsed;
|
|
17126
17998
|
try {
|
|
17127
|
-
parsed = JSON.parse(
|
|
17999
|
+
parsed = JSON.parse(raw2);
|
|
17128
18000
|
} catch {
|
|
17129
18001
|
return null;
|
|
17130
18002
|
}
|
|
@@ -17286,9 +18158,9 @@ function createNewFileDiff(filePath, repoRoot) {
|
|
|
17286
18158
|
isBinary: false
|
|
17287
18159
|
};
|
|
17288
18160
|
}
|
|
17289
|
-
function parseDiff(
|
|
18161
|
+
function parseDiff(raw2) {
|
|
17290
18162
|
const files = [];
|
|
17291
|
-
const fileChunks =
|
|
18163
|
+
const fileChunks = raw2.split(/^diff --git /m).filter(Boolean);
|
|
17292
18164
|
for (const chunk of fileChunks) {
|
|
17293
18165
|
const headerEnd = chunk.indexOf("@@");
|
|
17294
18166
|
const header = headerEnd === -1 ? chunk : chunk.slice(0, headerEnd);
|
|
@@ -17323,12 +18195,12 @@ function parseDiff(raw) {
|
|
|
17323
18195
|
}
|
|
17324
18196
|
return files;
|
|
17325
18197
|
}
|
|
17326
|
-
function parseHunks(
|
|
18198
|
+
function parseHunks(raw2) {
|
|
17327
18199
|
const hunks = [];
|
|
17328
18200
|
const hunkRegex = /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@(.*)/gm;
|
|
17329
18201
|
let match;
|
|
17330
18202
|
const hunkStarts = [];
|
|
17331
|
-
while ((match = hunkRegex.exec(
|
|
18203
|
+
while ((match = hunkRegex.exec(raw2)) !== null) {
|
|
17332
18204
|
const groups = match;
|
|
17333
18205
|
hunkStarts.push({
|
|
17334
18206
|
index: match.index + match[0].length,
|
|
@@ -17340,8 +18212,8 @@ function parseHunks(raw) {
|
|
|
17340
18212
|
}
|
|
17341
18213
|
for (let i = 0; i < hunkStarts.length; i++) {
|
|
17342
18214
|
const start = hunkStarts[i];
|
|
17343
|
-
const end = i + 1 < hunkStarts.length ?
|
|
17344
|
-
const body =
|
|
18215
|
+
const end = i + 1 < hunkStarts.length ? raw2.lastIndexOf("\n@@", hunkStarts[i + 1].index) : raw2.length;
|
|
18216
|
+
const body = raw2.slice(start.index, end);
|
|
17345
18217
|
const lines = [];
|
|
17346
18218
|
let oldNum = start.oldStart;
|
|
17347
18219
|
let newNum = start.newStart;
|
|
@@ -17494,8 +18366,8 @@ function acquireLock(dataDir) {
|
|
|
17494
18366
|
lockPath = join4(dataDir, "glassbox.lock");
|
|
17495
18367
|
if (existsSync3(lockPath)) {
|
|
17496
18368
|
try {
|
|
17497
|
-
const
|
|
17498
|
-
const contents = LockFileSchema.parse(
|
|
18369
|
+
const raw2 = JSON.parse(readFileSync3(lockPath, "utf-8"));
|
|
18370
|
+
const contents = LockFileSchema.parse(raw2);
|
|
17499
18371
|
const pid = contents.pid;
|
|
17500
18372
|
try {
|
|
17501
18373
|
process.kill(pid, 0);
|
|
@@ -17643,9 +18515,9 @@ async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
|
|
|
17643
18515
|
|
|
17644
18516
|
// src/server.ts
|
|
17645
18517
|
import { serve } from "@hono/node-server";
|
|
17646
|
-
import { existsSync as
|
|
17647
|
-
import { Hono as
|
|
17648
|
-
import { dirname as
|
|
18518
|
+
import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
|
|
18519
|
+
import { Hono as Hono19 } from "hono";
|
|
18520
|
+
import { dirname as dirname4, join as join15 } from "path";
|
|
17649
18521
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
17650
18522
|
|
|
17651
18523
|
// src/channel-config.ts
|
|
@@ -17680,8 +18552,8 @@ function registerChannel(dataDir) {
|
|
|
17680
18552
|
let config2 = {};
|
|
17681
18553
|
if (existsSync4(mcpPath)) {
|
|
17682
18554
|
try {
|
|
17683
|
-
const
|
|
17684
|
-
const parsed = McpConfigSchema.safeParse(
|
|
18555
|
+
const raw2 = JSON.parse(readFileSync4(mcpPath, "utf-8"));
|
|
18556
|
+
const parsed = McpConfigSchema.safeParse(raw2);
|
|
17685
18557
|
if (parsed.success) config2 = parsed.data;
|
|
17686
18558
|
} catch {
|
|
17687
18559
|
}
|
|
@@ -17698,8 +18570,8 @@ function unregisterChannel(dataDir) {
|
|
|
17698
18570
|
const mcpPath = join5(root, ".mcp.json");
|
|
17699
18571
|
if (!existsSync4(mcpPath)) return;
|
|
17700
18572
|
try {
|
|
17701
|
-
const
|
|
17702
|
-
const parsed = McpConfigSchema.safeParse(
|
|
18573
|
+
const raw2 = JSON.parse(readFileSync4(mcpPath, "utf-8"));
|
|
18574
|
+
const parsed = McpConfigSchema.safeParse(raw2);
|
|
17703
18575
|
if (!parsed.success) return;
|
|
17704
18576
|
const config2 = parsed.data;
|
|
17705
18577
|
if (config2.mcpServers?.[MCP_SERVER_KEY] !== void 0) {
|
|
@@ -17725,8 +18597,8 @@ async function isChannelAlive(dataDir) {
|
|
|
17725
18597
|
if (port === null) return false;
|
|
17726
18598
|
try {
|
|
17727
18599
|
const res = await fetch(`http://127.0.0.1:${port}/health`);
|
|
17728
|
-
const
|
|
17729
|
-
const parsed = HealthResponseSchema.safeParse(
|
|
18600
|
+
const raw2 = await res.json();
|
|
18601
|
+
const parsed = HealthResponseSchema.safeParse(raw2);
|
|
17730
18602
|
return parsed.success && parsed.data.ok;
|
|
17731
18603
|
} catch {
|
|
17732
18604
|
return false;
|
|
@@ -17759,8 +18631,120 @@ init_zod();
|
|
|
17759
18631
|
// src/ai/shared.ts
|
|
17760
18632
|
init_zod();
|
|
17761
18633
|
|
|
18634
|
+
// src/review-notes/format.ts
|
|
18635
|
+
init_store();
|
|
18636
|
+
init_view();
|
|
18637
|
+
function notesByFile(repoRoot, filePaths) {
|
|
18638
|
+
const out = [];
|
|
18639
|
+
for (const file2 of filePaths) {
|
|
18640
|
+
const notes = loadReviewNotesForFile(repoRoot, file2);
|
|
18641
|
+
if (notes.length > 0) out.push({ file: file2, notes });
|
|
18642
|
+
}
|
|
18643
|
+
return out;
|
|
18644
|
+
}
|
|
18645
|
+
function reviewNotesPromptSection(repoRoot, filePaths) {
|
|
18646
|
+
const grouped = notesByFile(repoRoot, filePaths);
|
|
18647
|
+
if (grouped.length === 0) return "";
|
|
18648
|
+
const lines = [
|
|
18649
|
+
"=== Author review notes ===",
|
|
18650
|
+
"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.",
|
|
18651
|
+
""
|
|
18652
|
+
];
|
|
18653
|
+
for (const { file: file2, notes } of grouped) {
|
|
18654
|
+
lines.push(`${file2}:`);
|
|
18655
|
+
for (const n of notes) {
|
|
18656
|
+
lines.push(`- [${REVIEW_NOTE_LABELS[n.kind] ?? n.kind}, L${String(n.line)}] ${n.body}`);
|
|
18657
|
+
}
|
|
18658
|
+
lines.push("");
|
|
18659
|
+
}
|
|
18660
|
+
return lines.join("\n").trimEnd();
|
|
18661
|
+
}
|
|
18662
|
+
function reviewNotesExportSection(repoRoot, filePaths) {
|
|
18663
|
+
const grouped = notesByFile(repoRoot, filePaths);
|
|
18664
|
+
if (grouped.length === 0) return [];
|
|
18665
|
+
const lines = [
|
|
18666
|
+
"## AI Review Notes",
|
|
18667
|
+
"",
|
|
18668
|
+
"> Line-anchored notes the generating AI left explaining its changes (from `.pr-notes/`). Read these for the rationale and proof behind the code.",
|
|
18669
|
+
""
|
|
18670
|
+
];
|
|
18671
|
+
for (const { file: file2, notes } of grouped) {
|
|
18672
|
+
lines.push(`### ${file2}`);
|
|
18673
|
+
lines.push("");
|
|
18674
|
+
for (const n of notes) {
|
|
18675
|
+
const who = n.producer !== void 0 ? ` _(${n.producer})_` : "";
|
|
18676
|
+
lines.push(`- **Line ${String(n.line)}** [${n.kind}]: ${n.body}${who}`);
|
|
18677
|
+
}
|
|
18678
|
+
lines.push("");
|
|
18679
|
+
}
|
|
18680
|
+
return lines;
|
|
18681
|
+
}
|
|
18682
|
+
|
|
17762
18683
|
// src/ai/client.ts
|
|
17763
18684
|
init_zod();
|
|
18685
|
+
|
|
18686
|
+
// src/ai/apple-foundation.ts
|
|
18687
|
+
init_zod();
|
|
18688
|
+
import { spawn } from "child_process";
|
|
18689
|
+
import { existsSync as existsSync6 } from "fs";
|
|
18690
|
+
import { join as join7 } from "path";
|
|
18691
|
+
var defaultRunner = (bin, args, stdin) => new Promise((resolve11, reject) => {
|
|
18692
|
+
const child = spawn(bin, args, { stdio: ["pipe", "pipe", "ignore"] });
|
|
18693
|
+
let stdout = "";
|
|
18694
|
+
child.stdout.setEncoding("utf-8");
|
|
18695
|
+
child.stdout.on("data", (chunk) => {
|
|
18696
|
+
stdout += chunk;
|
|
18697
|
+
});
|
|
18698
|
+
child.on("error", reject);
|
|
18699
|
+
child.on("close", (code) => {
|
|
18700
|
+
resolve11({ stdout, code: code ?? 0 });
|
|
18701
|
+
});
|
|
18702
|
+
child.stdin.end(stdin);
|
|
18703
|
+
});
|
|
18704
|
+
var runner = defaultRunner;
|
|
18705
|
+
var isDarwin = process.platform === "darwin";
|
|
18706
|
+
var availabilityCache = null;
|
|
18707
|
+
function appleFmBinPath() {
|
|
18708
|
+
const env = process.env.GLASSBOX_APPLE_FM_BIN;
|
|
18709
|
+
if (env !== void 0 && env !== "" && existsSync6(env)) return env;
|
|
18710
|
+
const fallback = join7(process.cwd(), "apple-fm-helper");
|
|
18711
|
+
if (existsSync6(fallback)) return fallback;
|
|
18712
|
+
return null;
|
|
18713
|
+
}
|
|
18714
|
+
async function isAppleFoundationAvailable() {
|
|
18715
|
+
if (availabilityCache !== null) return availabilityCache;
|
|
18716
|
+
availabilityCache = await probeAvailability();
|
|
18717
|
+
return availabilityCache;
|
|
18718
|
+
}
|
|
18719
|
+
async function probeAvailability() {
|
|
18720
|
+
if (!isDarwin) return false;
|
|
18721
|
+
const bin = appleFmBinPath();
|
|
18722
|
+
if (bin === null) return false;
|
|
18723
|
+
try {
|
|
18724
|
+
const { stdout, code } = await runner(bin, ["--probe"], "");
|
|
18725
|
+
return code === 0 && stdout.trim().toLowerCase().startsWith("available");
|
|
18726
|
+
} catch {
|
|
18727
|
+
return false;
|
|
18728
|
+
}
|
|
18729
|
+
}
|
|
18730
|
+
var InferOutputSchema = external_exports.object({ content: external_exports.string() });
|
|
18731
|
+
async function runAppleFoundationInfer(system, messages) {
|
|
18732
|
+
const bin = appleFmBinPath();
|
|
18733
|
+
if (bin === null) throw new Error("Apple Foundation Models helper not found");
|
|
18734
|
+
const { stdout, code } = await runner(bin, ["--infer"], JSON.stringify({ system, messages }));
|
|
18735
|
+
if (code !== 0) throw new Error(`Apple Foundation Models helper exited with code ${String(code)}`);
|
|
18736
|
+
let raw2;
|
|
18737
|
+
try {
|
|
18738
|
+
raw2 = JSON.parse(stdout);
|
|
18739
|
+
} catch {
|
|
18740
|
+
throw new Error("Apple Foundation Models helper returned non-JSON output");
|
|
18741
|
+
}
|
|
18742
|
+
const parsed = InferOutputSchema.safeParse(raw2);
|
|
18743
|
+
if (!parsed.success) throw new Error("Apple Foundation Models helper returned an unexpected payload");
|
|
18744
|
+
return parsed.data.content;
|
|
18745
|
+
}
|
|
18746
|
+
|
|
18747
|
+
// src/ai/client.ts
|
|
17764
18748
|
var AnthropicResponseSchema = external_exports.object({
|
|
17765
18749
|
content: external_exports.array(external_exports.object({ type: external_exports.string(), text: external_exports.string().optional() }).loose()),
|
|
17766
18750
|
usage: external_exports.object({ input_tokens: external_exports.number(), output_tokens: external_exports.number() }).loose()
|
|
@@ -17771,6 +18755,12 @@ var OpenAIResponseSchema = external_exports.object({
|
|
|
17771
18755
|
}).loose()).min(1),
|
|
17772
18756
|
usage: external_exports.object({ prompt_tokens: external_exports.number(), completion_tokens: external_exports.number() }).loose()
|
|
17773
18757
|
}).loose();
|
|
18758
|
+
var LocalResponseSchema = external_exports.object({
|
|
18759
|
+
choices: external_exports.array(external_exports.object({
|
|
18760
|
+
message: external_exports.object({ content: external_exports.string() }).loose()
|
|
18761
|
+
}).loose()).min(1),
|
|
18762
|
+
usage: external_exports.object({ prompt_tokens: external_exports.number(), completion_tokens: external_exports.number() }).loose().optional()
|
|
18763
|
+
}).loose();
|
|
17774
18764
|
var GoogleResponseSchema = external_exports.object({
|
|
17775
18765
|
candidates: external_exports.array(external_exports.object({
|
|
17776
18766
|
content: external_exports.object({
|
|
@@ -17782,23 +18772,35 @@ var GoogleResponseSchema = external_exports.object({
|
|
|
17782
18772
|
candidatesTokenCount: external_exports.number()
|
|
17783
18773
|
}).loose().optional()
|
|
17784
18774
|
}).loose();
|
|
17785
|
-
|
|
18775
|
+
function requireKey(config2) {
|
|
17786
18776
|
if (config2.apiKey === null) {
|
|
17787
18777
|
throw new Error(`No API key configured for ${config2.platform}`);
|
|
17788
18778
|
}
|
|
18779
|
+
return config2.apiKey;
|
|
18780
|
+
}
|
|
18781
|
+
async function sendAIRequest(config2, systemPrompt, messages) {
|
|
18782
|
+
if (config2.apiKey === null && !KEYLESS_PLATFORMS.has(config2.platform)) {
|
|
18783
|
+
throw new Error(`No API key configured for ${config2.platform}`);
|
|
18784
|
+
}
|
|
17789
18785
|
const totalChars = messages.reduce((sum, m) => sum + m.content.length, 0) + systemPrompt.length;
|
|
17790
18786
|
debugLog(`AI request \u2192 ${config2.platform}/${config2.model} | ${String(messages.length)} message(s) | ~${String(Math.ceil(totalChars / 3))} estimated tokens`);
|
|
17791
18787
|
const start = Date.now();
|
|
17792
18788
|
let response;
|
|
17793
18789
|
switch (config2.platform) {
|
|
17794
18790
|
case "anthropic":
|
|
17795
|
-
response = await sendAnthropicRequest(config2
|
|
18791
|
+
response = await sendAnthropicRequest(requireKey(config2), config2.model, systemPrompt, messages);
|
|
17796
18792
|
break;
|
|
17797
18793
|
case "openai":
|
|
17798
|
-
response = await sendOpenAIRequest(config2
|
|
18794
|
+
response = await sendOpenAIRequest(requireKey(config2), config2.model, systemPrompt, messages);
|
|
17799
18795
|
break;
|
|
17800
18796
|
case "google":
|
|
17801
|
-
response = await sendGoogleRequest(config2
|
|
18797
|
+
response = await sendGoogleRequest(requireKey(config2), config2.model, systemPrompt, messages);
|
|
18798
|
+
break;
|
|
18799
|
+
case "local":
|
|
18800
|
+
response = await sendLocalRequest(config2.baseUrl ?? DEFAULT_LOCAL_ENDPOINT, config2.apiKey, config2.model, systemPrompt, messages);
|
|
18801
|
+
break;
|
|
18802
|
+
case "apple":
|
|
18803
|
+
response = await sendAppleRequest(systemPrompt, messages);
|
|
17802
18804
|
break;
|
|
17803
18805
|
}
|
|
17804
18806
|
const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
|
|
@@ -17827,8 +18829,8 @@ async function sendAnthropicRequest(apiKey, model, systemPrompt, messages) {
|
|
|
17827
18829
|
const errorText = await response.text();
|
|
17828
18830
|
throw new Error(`Anthropic API error (${String(response.status)}): ${errorText}`);
|
|
17829
18831
|
}
|
|
17830
|
-
const
|
|
17831
|
-
const data = AnthropicResponseSchema.parse(
|
|
18832
|
+
const raw2 = await response.json();
|
|
18833
|
+
const data = AnthropicResponseSchema.parse(raw2);
|
|
17832
18834
|
const text = data.content.filter((c) => c.type === "text").map((c) => c.text ?? "").join("");
|
|
17833
18835
|
return {
|
|
17834
18836
|
content: text,
|
|
@@ -17857,14 +18859,42 @@ async function sendOpenAIRequest(apiKey, model, systemPrompt, messages) {
|
|
|
17857
18859
|
const errorText = await response.text();
|
|
17858
18860
|
throw new Error(`OpenAI API error (${String(response.status)}): ${errorText}`);
|
|
17859
18861
|
}
|
|
17860
|
-
const
|
|
17861
|
-
const data = OpenAIResponseSchema.parse(
|
|
18862
|
+
const raw2 = await response.json();
|
|
18863
|
+
const data = OpenAIResponseSchema.parse(raw2);
|
|
17862
18864
|
return {
|
|
17863
18865
|
content: data.choices[0].message.content,
|
|
17864
18866
|
inputTokens: data.usage.prompt_tokens,
|
|
17865
18867
|
outputTokens: data.usage.completion_tokens
|
|
17866
18868
|
};
|
|
17867
18869
|
}
|
|
18870
|
+
async function sendLocalRequest(baseUrl, apiKey, model, systemPrompt, messages) {
|
|
18871
|
+
const oaiMessages = [
|
|
18872
|
+
{ role: "system", content: systemPrompt },
|
|
18873
|
+
...messages.map((m) => ({ role: m.role, content: m.content }))
|
|
18874
|
+
];
|
|
18875
|
+
const headers = { "Content-Type": "application/json" };
|
|
18876
|
+
if (apiKey !== null && apiKey !== "") headers.Authorization = `Bearer ${apiKey}`;
|
|
18877
|
+
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
18878
|
+
method: "POST",
|
|
18879
|
+
headers,
|
|
18880
|
+
body: JSON.stringify({ model, messages: oaiMessages, max_tokens: 8192, stream: false })
|
|
18881
|
+
});
|
|
18882
|
+
if (!response.ok) {
|
|
18883
|
+
const errorText = await response.text();
|
|
18884
|
+
throw new Error(`Local model error (${String(response.status)}): ${errorText}`);
|
|
18885
|
+
}
|
|
18886
|
+
const raw2 = await response.json();
|
|
18887
|
+
const data = LocalResponseSchema.parse(raw2);
|
|
18888
|
+
return {
|
|
18889
|
+
content: data.choices[0].message.content,
|
|
18890
|
+
inputTokens: data.usage?.prompt_tokens ?? 0,
|
|
18891
|
+
outputTokens: data.usage?.completion_tokens ?? 0
|
|
18892
|
+
};
|
|
18893
|
+
}
|
|
18894
|
+
async function sendAppleRequest(systemPrompt, messages) {
|
|
18895
|
+
const content = await runAppleFoundationInfer(systemPrompt, messages);
|
|
18896
|
+
return { content, inputTokens: 0, outputTokens: 0 };
|
|
18897
|
+
}
|
|
17868
18898
|
async function sendGoogleRequest(apiKey, model, systemPrompt, messages) {
|
|
17869
18899
|
const contents = messages.map((m) => ({
|
|
17870
18900
|
role: m.role === "assistant" ? "model" : "user",
|
|
@@ -17889,8 +18919,8 @@ async function sendGoogleRequest(apiKey, model, systemPrompt, messages) {
|
|
|
17889
18919
|
const errorText = await response.text();
|
|
17890
18920
|
throw new Error(`Google AI API error (${String(response.status)}): ${errorText}`);
|
|
17891
18921
|
}
|
|
17892
|
-
const
|
|
17893
|
-
const data = GoogleResponseSchema.parse(
|
|
18922
|
+
const raw2 = await response.json();
|
|
18923
|
+
const data = GoogleResponseSchema.parse(raw2);
|
|
17894
18924
|
const text = data.candidates[0].content.parts.map((p) => p.text).join("");
|
|
17895
18925
|
return {
|
|
17896
18926
|
content: text,
|
|
@@ -18014,14 +19044,26 @@ function extractJSON(text) {
|
|
|
18014
19044
|
throw new Error(`Could not extract JSON from AI response: ${text.slice(0, 300)}`);
|
|
18015
19045
|
}
|
|
18016
19046
|
async function runAnalysisBatch(files, config2, repoRoot, options) {
|
|
19047
|
+
try {
|
|
19048
|
+
return await runAnalysisBatchOnce(files, config2, repoRoot, options);
|
|
19049
|
+
} catch (err) {
|
|
19050
|
+
if (config2.fallback === void 0) throw err;
|
|
19051
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
19052
|
+
debugLog(`[${options.analysisName}] primary platform '${config2.platform}' failed for batch (${msg.slice(0, 160)}); retrying with fallback '${config2.fallback.platform}'`);
|
|
19053
|
+
return runAnalysisBatchOnce(files, config2.fallback, repoRoot, options);
|
|
19054
|
+
}
|
|
19055
|
+
}
|
|
19056
|
+
async function runAnalysisBatchOnce(files, config2, repoRoot, options) {
|
|
18017
19057
|
const contextWindow = getModelContextWindow(config2.platform, config2.model);
|
|
18018
19058
|
const charBudget = Math.floor(contextWindow * 0.7 * 3);
|
|
18019
19059
|
const contexts = buildFileContexts(files, charBudget);
|
|
18020
19060
|
const validPaths = new Set(files.map((f) => f.file_path));
|
|
19061
|
+
const notesSection = reviewNotesPromptSection(repoRoot, files.map((f) => f.file_path));
|
|
18021
19062
|
const initialPrompt = [
|
|
18022
19063
|
options.initialPromptHeader(files.length),
|
|
18023
19064
|
"",
|
|
18024
|
-
formatContextsForPrompt(contexts)
|
|
19065
|
+
formatContextsForPrompt(contexts),
|
|
19066
|
+
...notesSection === "" ? [] : ["", notesSection]
|
|
18025
19067
|
].join("\n");
|
|
18026
19068
|
const messages = [{ role: "user", content: initialPrompt }];
|
|
18027
19069
|
for (let round = 0; round < 3; round++) {
|
|
@@ -18045,7 +19087,8 @@ ${formatAdditionalContext(fileContents)}`
|
|
|
18045
19087
|
});
|
|
18046
19088
|
continue;
|
|
18047
19089
|
}
|
|
18048
|
-
const
|
|
19090
|
+
const candidate = Array.isArray(parsed) ? parsed : [parsed];
|
|
19091
|
+
const arrayResult = external_exports.array(options.itemSchema).safeParse(candidate);
|
|
18049
19092
|
if (!arrayResult.success) {
|
|
18050
19093
|
const summary = arrayResult.error.issues.slice(0, 3).map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
18051
19094
|
throw new Error(`Expected an array of ${options.resultLabel} from AI \u2014 ${summary}`);
|
|
@@ -18486,8 +19529,8 @@ function isRetriable(err) {
|
|
|
18486
19529
|
return msg.includes("429") || msg.includes("500") || msg.includes("502") || msg.includes("503") || msg.includes("504") || msg.includes("rate_limit");
|
|
18487
19530
|
}
|
|
18488
19531
|
function sleep(ms) {
|
|
18489
|
-
return new Promise((
|
|
18490
|
-
setTimeout(
|
|
19532
|
+
return new Promise((resolve11) => {
|
|
19533
|
+
setTimeout(resolve11, ms);
|
|
18491
19534
|
});
|
|
18492
19535
|
}
|
|
18493
19536
|
|
|
@@ -18531,8 +19574,8 @@ function randomLines(count) {
|
|
|
18531
19574
|
return lines.sort((a, b) => a.line - b.line);
|
|
18532
19575
|
}
|
|
18533
19576
|
function sleep2(ms) {
|
|
18534
|
-
return new Promise((
|
|
18535
|
-
setTimeout(
|
|
19577
|
+
return new Promise((resolve11) => {
|
|
19578
|
+
setTimeout(resolve11, ms);
|
|
18536
19579
|
});
|
|
18537
19580
|
}
|
|
18538
19581
|
async function mockRiskAnalysisBatch(files) {
|
|
@@ -18699,17 +19742,32 @@ var AIConfigRespSchema = external_exports.object({
|
|
|
18699
19742
|
model: external_exports.string(),
|
|
18700
19743
|
keyConfigured: external_exports.boolean(),
|
|
18701
19744
|
keySource: KeySourceSchema,
|
|
18702
|
-
|
|
19745
|
+
/** Base URL for the `local` (OpenAI-compatible) platform. */
|
|
19746
|
+
localEndpoint: external_exports.string(),
|
|
19747
|
+
guidedReview: GuidedReviewConfigShapeSchema,
|
|
19748
|
+
/** Secondary model used when the primary (Apple FM) fails a batch. `null`
|
|
19749
|
+
* when none is configured. */
|
|
19750
|
+
fallbackPlatform: AIPlatformSchema.nullable(),
|
|
19751
|
+
fallbackModel: external_exports.string().nullable()
|
|
18703
19752
|
});
|
|
18704
19753
|
var SaveAIConfigReqSchema = external_exports.object({
|
|
18705
19754
|
platform: AIPlatformSchema,
|
|
18706
19755
|
model: external_exports.string().min(1),
|
|
18707
|
-
|
|
19756
|
+
localEndpoint: external_exports.string().optional(),
|
|
19757
|
+
guidedReview: GuidedReviewConfigShapeSchema.optional(),
|
|
19758
|
+
/** Apple-FM fallback selection. An empty `fallbackPlatform` clears it. */
|
|
19759
|
+
fallbackPlatform: external_exports.string().optional(),
|
|
19760
|
+
fallbackModel: external_exports.string().optional()
|
|
18708
19761
|
});
|
|
18709
19762
|
var SaveAIConfigRespSchema = OkResponseSchema;
|
|
18710
19763
|
var ListAIModelsRespSchema = external_exports.object({
|
|
18711
19764
|
platforms: external_exports.record(AIPlatformSchema, external_exports.string()),
|
|
18712
|
-
models: external_exports.record(AIPlatformSchema, external_exports.array(AIModelSchema))
|
|
19765
|
+
models: external_exports.record(AIPlatformSchema, external_exports.array(AIModelSchema)),
|
|
19766
|
+
// Whether the on-device Apple Foundation Models helper is available right now
|
|
19767
|
+
// (macOS 26 + Apple Intelligence + bundled helper). The picker uses this to
|
|
19768
|
+
// show/hide the Apple platform; the `platforms`/`models` records always carry
|
|
19769
|
+
// every platform key (the record schema over the platform enum is exhaustive).
|
|
19770
|
+
appleAvailable: external_exports.boolean()
|
|
18713
19771
|
});
|
|
18714
19772
|
var AIKeyStatusEntrySchema = external_exports.object({
|
|
18715
19773
|
configured: external_exports.boolean(),
|
|
@@ -18892,7 +19950,9 @@ var CreateAnnotationReqSchema = external_exports.object({
|
|
|
18892
19950
|
lineNumber: external_exports.number().int().min(1),
|
|
18893
19951
|
side: AnnotationSideSchema,
|
|
18894
19952
|
category: AnnotationCategorySchema,
|
|
18895
|
-
content: external_exports.string().min(1)
|
|
19953
|
+
content: external_exports.string().min(1),
|
|
19954
|
+
/** SARIF guid of the AI review note this annotation replies to (doc 20 threading). */
|
|
19955
|
+
replyToNoteId: external_exports.string().optional()
|
|
18896
19956
|
});
|
|
18897
19957
|
var CreateAnnotationRespSchema = AnnotationSchema;
|
|
18898
19958
|
var UpdateAnnotationReqSchema = external_exports.object({
|
|
@@ -19954,6 +21014,19 @@ var DifftoolPollRespSchema = external_exports.object({
|
|
|
19954
21014
|
});
|
|
19955
21015
|
var DifftoolEndRespSchema = external_exports.object({ ok: external_exports.literal(true) });
|
|
19956
21016
|
|
|
21017
|
+
// src/api/review-notes.ts
|
|
21018
|
+
init_zod();
|
|
21019
|
+
var DiscardReviewNoteReqSchema = external_exports.object({
|
|
21020
|
+
guid: external_exports.string().min(1),
|
|
21021
|
+
/** Repo-relative source file the note is on (scopes the shard search). */
|
|
21022
|
+
file: external_exports.string().min(1)
|
|
21023
|
+
});
|
|
21024
|
+
var DiscardReviewNoteRespSchema = external_exports.object({
|
|
21025
|
+
ok: external_exports.boolean(),
|
|
21026
|
+
/** Whether a note was actually removed (false if it wasn't on disk, e.g. demo). */
|
|
21027
|
+
removed: external_exports.boolean()
|
|
21028
|
+
});
|
|
21029
|
+
|
|
19957
21030
|
// src/api/index.ts
|
|
19958
21031
|
var apis = {
|
|
19959
21032
|
...ai_exports,
|
|
@@ -19977,13 +21050,13 @@ init_schemas3();
|
|
|
19977
21050
|
|
|
19978
21051
|
// src/utils/parseBody.ts
|
|
19979
21052
|
async function parseBody(c, schema) {
|
|
19980
|
-
let
|
|
21053
|
+
let raw2;
|
|
19981
21054
|
try {
|
|
19982
|
-
|
|
21055
|
+
raw2 = await c.req.json();
|
|
19983
21056
|
} catch {
|
|
19984
21057
|
return { ok: false, response: c.json({ error: "Body must be valid JSON" }, 400) };
|
|
19985
21058
|
}
|
|
19986
|
-
const result = schema.safeParse(
|
|
21059
|
+
const result = schema.safeParse(raw2);
|
|
19987
21060
|
if (!result.success) {
|
|
19988
21061
|
const summary = result.error.issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; ");
|
|
19989
21062
|
return { ok: false, response: c.json({ error: summary }, 400) };
|
|
@@ -19991,8 +21064,8 @@ async function parseBody(c, schema) {
|
|
|
19991
21064
|
return { ok: true, data: result.data };
|
|
19992
21065
|
}
|
|
19993
21066
|
function parseQuery(c, schema) {
|
|
19994
|
-
const
|
|
19995
|
-
const result = schema.safeParse(
|
|
21067
|
+
const raw2 = c.req.query();
|
|
21068
|
+
const result = schema.safeParse(raw2);
|
|
19996
21069
|
if (!result.success) {
|
|
19997
21070
|
const summary = result.error.issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; ");
|
|
19998
21071
|
return { ok: false, response: c.json({ error: summary }, 400) };
|
|
@@ -20042,7 +21115,7 @@ aiAnalysisRoutes.post("/analyze", async (c) => {
|
|
|
20042
21115
|
debugLog(`POST /analyze: type=${analysisType}, reviewId=${reviewId}`);
|
|
20043
21116
|
const testMode = isAIServiceTest();
|
|
20044
21117
|
const config2 = loadAIConfig();
|
|
20045
|
-
if (config2.apiKey === null && !testMode) {
|
|
21118
|
+
if (config2.apiKey === null && !testMode && !KEYLESS_PLATFORMS.has(config2.platform)) {
|
|
20046
21119
|
debugLog("POST /analyze: no API key configured");
|
|
20047
21120
|
return c.json({ error: "No API key configured" }, 400);
|
|
20048
21121
|
}
|
|
@@ -20443,11 +21516,11 @@ function anthropicContextWindow(id) {
|
|
|
20443
21516
|
return id.toLowerCase().includes("haiku") ? 2e5 : 1e6;
|
|
20444
21517
|
}
|
|
20445
21518
|
async function fetchAnthropic(apiKey) {
|
|
20446
|
-
const
|
|
21519
|
+
const raw2 = await getJson("https://api.anthropic.com/v1/models?limit=1000", {
|
|
20447
21520
|
"x-api-key": apiKey,
|
|
20448
21521
|
"anthropic-version": "2023-06-01"
|
|
20449
21522
|
});
|
|
20450
|
-
const parsed = AnthropicListSchema.safeParse(
|
|
21523
|
+
const parsed = AnthropicListSchema.safeParse(raw2);
|
|
20451
21524
|
if (!parsed.success) return null;
|
|
20452
21525
|
return parsed.data.data.map((m) => ({
|
|
20453
21526
|
id: m.id,
|
|
@@ -20466,13 +21539,21 @@ function isOpenAIChatModel(id) {
|
|
|
20466
21539
|
return isChat && !isNonChat;
|
|
20467
21540
|
}
|
|
20468
21541
|
async function fetchOpenAI(apiKey) {
|
|
20469
|
-
const
|
|
21542
|
+
const raw2 = await getJson("https://api.openai.com/v1/models", {
|
|
20470
21543
|
Authorization: `Bearer ${apiKey}`
|
|
20471
21544
|
});
|
|
20472
|
-
const parsed = OpenAIListSchema.safeParse(
|
|
21545
|
+
const parsed = OpenAIListSchema.safeParse(raw2);
|
|
20473
21546
|
if (!parsed.success) return null;
|
|
20474
21547
|
return parsed.data.data.filter((m) => isOpenAIChatModel(m.id)).map((m) => ({ id: m.id, name: m.id, contextWindow: 128e3, isDefault: false }));
|
|
20475
21548
|
}
|
|
21549
|
+
async function fetchLocal(baseUrl, apiKey) {
|
|
21550
|
+
const headers = {};
|
|
21551
|
+
if (apiKey !== "") headers.Authorization = `Bearer ${apiKey}`;
|
|
21552
|
+
const raw2 = await getJson(`${baseUrl}/models`, headers);
|
|
21553
|
+
const parsed = OpenAIListSchema.safeParse(raw2);
|
|
21554
|
+
if (!parsed.success) return null;
|
|
21555
|
+
return parsed.data.data.map((m) => ({ id: m.id, name: m.id, contextWindow: 8192, isDefault: false }));
|
|
21556
|
+
}
|
|
20476
21557
|
var GoogleListSchema = external_exports.object({
|
|
20477
21558
|
models: external_exports.array(external_exports.object({
|
|
20478
21559
|
name: external_exports.string(),
|
|
@@ -20482,11 +21563,11 @@ var GoogleListSchema = external_exports.object({
|
|
|
20482
21563
|
}))
|
|
20483
21564
|
});
|
|
20484
21565
|
async function fetchGoogle(apiKey) {
|
|
20485
|
-
const
|
|
21566
|
+
const raw2 = await getJson(
|
|
20486
21567
|
`https://generativelanguage.googleapis.com/v1beta/models?pageSize=1000&key=${encodeURIComponent(apiKey)}`,
|
|
20487
21568
|
{}
|
|
20488
21569
|
);
|
|
20489
|
-
const parsed = GoogleListSchema.safeParse(
|
|
21570
|
+
const parsed = GoogleListSchema.safeParse(raw2);
|
|
20490
21571
|
if (!parsed.success) return null;
|
|
20491
21572
|
return parsed.data.models.filter((m) => (m.supportedGenerationMethods ?? []).includes("generateContent")).map((m) => ({
|
|
20492
21573
|
id: m.name.replace(/^models\//, ""),
|
|
@@ -20495,10 +21576,11 @@ async function fetchGoogle(apiKey) {
|
|
|
20495
21576
|
isDefault: false
|
|
20496
21577
|
}));
|
|
20497
21578
|
}
|
|
20498
|
-
async function fetchAvailableModels(platform, apiKey) {
|
|
21579
|
+
async function fetchAvailableModels(platform, apiKey, opts = {}) {
|
|
20499
21580
|
let models;
|
|
20500
21581
|
if (platform === "anthropic") models = await fetchAnthropic(apiKey);
|
|
20501
21582
|
else if (platform === "openai") models = await fetchOpenAI(apiKey);
|
|
21583
|
+
else if (platform === "local") models = await fetchLocal(opts.baseUrl ?? "", apiKey);
|
|
20502
21584
|
else models = await fetchGoogle(apiKey);
|
|
20503
21585
|
if (models === null || models.length === 0) return null;
|
|
20504
21586
|
const defaultId = getDefaultModel(platform);
|
|
@@ -20511,21 +21593,32 @@ async function fetchAvailableModels(platform, apiKey) {
|
|
|
20511
21593
|
|
|
20512
21594
|
// src/routes/ai-config.ts
|
|
20513
21595
|
var aiConfigRoutes = new Hono2();
|
|
20514
|
-
aiConfigRoutes.get("/config", (c) => {
|
|
21596
|
+
aiConfigRoutes.get("/config", async (c) => {
|
|
20515
21597
|
const config2 = loadAIConfig();
|
|
21598
|
+
const appleReady = APPLE_FM_ANALYSIS_ENABLED && config2.platform === "apple" && await isAppleFoundationAvailable();
|
|
21599
|
+
const fallbackSelection = loadFallbackSelection();
|
|
20516
21600
|
return c.json({
|
|
20517
21601
|
platform: config2.platform,
|
|
20518
21602
|
model: config2.model,
|
|
20519
|
-
keyConfigured: config2.apiKey !== null || isAIServiceTest() || getDemoMode() !== null,
|
|
21603
|
+
keyConfigured: config2.apiKey !== null || config2.platform === "local" || appleReady || isAIServiceTest() || getDemoMode() !== null,
|
|
20520
21604
|
keySource: config2.keySource,
|
|
20521
|
-
|
|
21605
|
+
localEndpoint: resolveLocalEndpoint(),
|
|
21606
|
+
guidedReview: loadGuidedReviewConfig(),
|
|
21607
|
+
// Apple-FM fallback selection as stored, regardless of the current primary
|
|
21608
|
+
// platform, so the settings dialog can show/preserve it; `null` when unset.
|
|
21609
|
+
fallbackPlatform: fallbackSelection?.platform ?? null,
|
|
21610
|
+
fallbackModel: fallbackSelection?.model ?? null
|
|
20522
21611
|
});
|
|
20523
21612
|
});
|
|
20524
21613
|
aiConfigRoutes.post("/config", async (c) => {
|
|
20525
21614
|
const parsed = await parseBody(c, SaveAIConfigReqSchema);
|
|
20526
21615
|
if (!parsed.ok) return parsed.response;
|
|
20527
21616
|
const body = parsed.data;
|
|
20528
|
-
saveAIConfigPreferences(body.platform, body.model
|
|
21617
|
+
saveAIConfigPreferences(body.platform, body.model, {
|
|
21618
|
+
localEndpoint: body.localEndpoint,
|
|
21619
|
+
fallbackPlatform: body.fallbackPlatform,
|
|
21620
|
+
fallbackModel: body.fallbackModel
|
|
21621
|
+
});
|
|
20529
21622
|
if (body.guidedReview !== void 0) {
|
|
20530
21623
|
saveGuidedReviewConfig(body.guidedReview);
|
|
20531
21624
|
}
|
|
@@ -20533,7 +21626,13 @@ aiConfigRoutes.post("/config", async (c) => {
|
|
|
20533
21626
|
});
|
|
20534
21627
|
aiConfigRoutes.get("/models", async (c) => {
|
|
20535
21628
|
const platforms = ["anthropic", "openai", "google"];
|
|
20536
|
-
const models = {
|
|
21629
|
+
const models = {
|
|
21630
|
+
anthropic: MODELS.anthropic,
|
|
21631
|
+
openai: MODELS.openai,
|
|
21632
|
+
google: MODELS.google,
|
|
21633
|
+
local: MODELS.local,
|
|
21634
|
+
apple: MODELS.apple
|
|
21635
|
+
};
|
|
20537
21636
|
if (!isAIServiceTest() && getDemoMode() === null) {
|
|
20538
21637
|
await Promise.all(platforms.map(async (platform) => {
|
|
20539
21638
|
const { key } = resolveAPIKey(platform);
|
|
@@ -20541,11 +21640,15 @@ aiConfigRoutes.get("/models", async (c) => {
|
|
|
20541
21640
|
const live = await fetchAvailableModels(platform, key);
|
|
20542
21641
|
if (live !== null && live.length > 0) models[platform] = live;
|
|
20543
21642
|
}));
|
|
21643
|
+
const { key: localKey } = resolveAPIKey("local");
|
|
21644
|
+
const localLive = await fetchAvailableModels("local", localKey ?? "", { baseUrl: resolveLocalEndpoint() });
|
|
21645
|
+
if (localLive !== null && localLive.length > 0) models.local = localLive;
|
|
20544
21646
|
}
|
|
20545
|
-
|
|
21647
|
+
const appleAvailable = APPLE_FM_ANALYSIS_ENABLED && (isAIServiceTest() || await isAppleFoundationAvailable());
|
|
21648
|
+
return c.json({ platforms: PLATFORMS, models, appleAvailable });
|
|
20546
21649
|
});
|
|
20547
21650
|
aiConfigRoutes.get("/key-status", (c) => {
|
|
20548
|
-
const platforms = ["anthropic", "openai", "google"];
|
|
21651
|
+
const platforms = ["anthropic", "openai", "google", "local", "apple"];
|
|
20549
21652
|
const status = {};
|
|
20550
21653
|
for (const platform of platforms) {
|
|
20551
21654
|
const { source } = resolveAPIKey(platform);
|
|
@@ -20569,7 +21672,7 @@ aiConfigRoutes.delete("/key", (c) => {
|
|
|
20569
21672
|
const platform = c.req.query("platform") ?? "anthropic";
|
|
20570
21673
|
const parsed = AIPlatformSchema.safeParse(platform);
|
|
20571
21674
|
if (!parsed.success) {
|
|
20572
|
-
return errorResponse(c, `platform must be one of:
|
|
21675
|
+
return errorResponse(c, `platform must be one of: ${Object.keys(PLATFORMS).join(", ")}`);
|
|
20573
21676
|
}
|
|
20574
21677
|
deleteAPIKey(parsed.data);
|
|
20575
21678
|
return c.json({ ok: true });
|
|
@@ -20581,7 +21684,7 @@ aiApiRoutes.route("/", aiConfigRoutes);
|
|
|
20581
21684
|
aiApiRoutes.route("/", aiAnalysisRoutes);
|
|
20582
21685
|
|
|
20583
21686
|
// src/routes/api.ts
|
|
20584
|
-
import { Hono as
|
|
21687
|
+
import { Hono as Hono14 } from "hono";
|
|
20585
21688
|
|
|
20586
21689
|
// src/routes/api/annotations.ts
|
|
20587
21690
|
import { Hono as Hono4 } from "hono";
|
|
@@ -20590,28 +21693,28 @@ init_queries();
|
|
|
20590
21693
|
// src/export/generate.ts
|
|
20591
21694
|
init_zod();
|
|
20592
21695
|
init_queries();
|
|
20593
|
-
import { spawnSync as
|
|
20594
|
-
import { appendFileSync, existsSync as
|
|
21696
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
21697
|
+
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
20595
21698
|
import { homedir as homedir2 } from "os";
|
|
20596
|
-
import { join as
|
|
20597
|
-
var DISMISS_FILE =
|
|
21699
|
+
import { join as join8 } from "path";
|
|
21700
|
+
var DISMISS_FILE = join8(homedir2(), ".glassbox", "gitignore-dismissed.json");
|
|
20598
21701
|
var DISMISS_DAYS = 30;
|
|
20599
21702
|
var DismissalsSchema = external_exports.record(external_exports.string(), external_exports.number());
|
|
20600
21703
|
function loadDismissals() {
|
|
20601
21704
|
try {
|
|
20602
|
-
const parsed = DismissalsSchema.safeParse(JSON.parse(
|
|
21705
|
+
const parsed = DismissalsSchema.safeParse(JSON.parse(readFileSync6(DISMISS_FILE, "utf-8")));
|
|
20603
21706
|
return parsed.success ? parsed.data : {};
|
|
20604
21707
|
} catch {
|
|
20605
21708
|
return {};
|
|
20606
21709
|
}
|
|
20607
21710
|
}
|
|
20608
21711
|
function saveDismissals(data) {
|
|
20609
|
-
const dir =
|
|
20610
|
-
|
|
20611
|
-
|
|
21712
|
+
const dir = join8(homedir2(), ".glassbox");
|
|
21713
|
+
mkdirSync5(dir, { recursive: true });
|
|
21714
|
+
writeFileSync6(DISMISS_FILE, JSON.stringify(data), "utf-8");
|
|
20612
21715
|
}
|
|
20613
21716
|
function isGlassboxGitignored(repoRoot) {
|
|
20614
|
-
const result =
|
|
21717
|
+
const result = spawnSync6("git", ["check-ignore", "-q", ".glassbox"], { cwd: repoRoot, stdio: "pipe" });
|
|
20615
21718
|
return result.status === 0;
|
|
20616
21719
|
}
|
|
20617
21720
|
function shouldPromptGitignore(repoRoot) {
|
|
@@ -20626,16 +21729,16 @@ function shouldPromptGitignore(repoRoot) {
|
|
|
20626
21729
|
return true;
|
|
20627
21730
|
}
|
|
20628
21731
|
function addGlassboxToGitignore(repoRoot) {
|
|
20629
|
-
const gitignorePath =
|
|
20630
|
-
if (
|
|
20631
|
-
const content =
|
|
21732
|
+
const gitignorePath = join8(repoRoot, ".gitignore");
|
|
21733
|
+
if (existsSync7(gitignorePath)) {
|
|
21734
|
+
const content = readFileSync6(gitignorePath, "utf-8");
|
|
20632
21735
|
if (!content.endsWith("\n")) {
|
|
20633
21736
|
appendFileSync(gitignorePath, "\n.glassbox/\n", "utf-8");
|
|
20634
21737
|
} else {
|
|
20635
21738
|
appendFileSync(gitignorePath, ".glassbox/\n", "utf-8");
|
|
20636
21739
|
}
|
|
20637
21740
|
} else {
|
|
20638
|
-
|
|
21741
|
+
writeFileSync6(gitignorePath, ".glassbox/\n", "utf-8");
|
|
20639
21742
|
}
|
|
20640
21743
|
}
|
|
20641
21744
|
function dismissGitignorePrompt2(repoRoot) {
|
|
@@ -20644,17 +21747,17 @@ function dismissGitignorePrompt2(repoRoot) {
|
|
|
20644
21747
|
saveDismissals(dismissals);
|
|
20645
21748
|
}
|
|
20646
21749
|
function deleteReviewExport(reviewId, repoRoot) {
|
|
20647
|
-
const exportDir =
|
|
20648
|
-
const archivePath =
|
|
20649
|
-
if (
|
|
21750
|
+
const exportDir = join8(repoRoot, ".glassbox");
|
|
21751
|
+
const archivePath = join8(exportDir, `review-${reviewId}.md`);
|
|
21752
|
+
if (existsSync7(archivePath)) unlinkSync2(archivePath);
|
|
20650
21753
|
}
|
|
20651
21754
|
async function generateReviewExport(reviewId, repoRoot, isCurrent) {
|
|
20652
21755
|
const review = await getReview(reviewId);
|
|
20653
21756
|
if (!review) throw new Error("Review not found");
|
|
20654
21757
|
const files = await getReviewFiles(reviewId);
|
|
20655
21758
|
const annotations = await getAnnotationsForReview(reviewId);
|
|
20656
|
-
const exportDir =
|
|
20657
|
-
|
|
21759
|
+
const exportDir = join8(repoRoot, ".glassbox");
|
|
21760
|
+
mkdirSync5(exportDir, { recursive: true });
|
|
20658
21761
|
const byFile = {};
|
|
20659
21762
|
for (const a of annotations) {
|
|
20660
21763
|
if (!(a.file_path in byFile)) byFile[a.file_path] = [];
|
|
@@ -20705,6 +21808,8 @@ async function generateReviewExport(reviewId, repoRoot, isCurrent) {
|
|
|
20705
21808
|
}
|
|
20706
21809
|
lines.push("");
|
|
20707
21810
|
}
|
|
21811
|
+
const reviewNoteLines = reviewNotesExportSection(repoRoot, files.map((f) => f.file_path));
|
|
21812
|
+
if (reviewNoteLines.length > 0) lines.push(...reviewNoteLines);
|
|
20708
21813
|
lines.push("---");
|
|
20709
21814
|
lines.push("");
|
|
20710
21815
|
lines.push("## Instructions for AI Tools");
|
|
@@ -20719,11 +21824,11 @@ async function generateReviewExport(reviewId, repoRoot, isCurrent) {
|
|
|
20719
21824
|
lines.push("6. **note** annotations are informational context. Consider them but they may not require code changes.");
|
|
20720
21825
|
lines.push("");
|
|
20721
21826
|
const content = lines.join("\n");
|
|
20722
|
-
const archivePath =
|
|
20723
|
-
|
|
21827
|
+
const archivePath = join8(exportDir, `review-${review.id}.md`);
|
|
21828
|
+
writeFileSync6(archivePath, content, "utf-8");
|
|
20724
21829
|
if (isCurrent) {
|
|
20725
|
-
const latestPath =
|
|
20726
|
-
|
|
21830
|
+
const latestPath = join8(exportDir, "latest-review.md");
|
|
21831
|
+
writeFileSync6(latestPath, content, "utf-8");
|
|
20727
21832
|
return latestPath;
|
|
20728
21833
|
}
|
|
20729
21834
|
return archivePath;
|
|
@@ -20754,7 +21859,8 @@ annotationsRoutes.post("/annotations", async (c) => {
|
|
|
20754
21859
|
body.lineNumber,
|
|
20755
21860
|
body.side,
|
|
20756
21861
|
body.category,
|
|
20757
|
-
body.content
|
|
21862
|
+
body.content,
|
|
21863
|
+
body.replyToNoteId
|
|
20758
21864
|
);
|
|
20759
21865
|
autoExport(c);
|
|
20760
21866
|
return c.json(annotation, 201);
|
|
@@ -20841,7 +21947,7 @@ import { resolve as resolve4 } from "path";
|
|
|
20841
21947
|
init_queries();
|
|
20842
21948
|
|
|
20843
21949
|
// src/utils/openOS.ts
|
|
20844
|
-
import { execFileSync, spawn } from "child_process";
|
|
21950
|
+
import { execFileSync, spawn as spawn2 } from "child_process";
|
|
20845
21951
|
import { resolve as resolve3 } from "path";
|
|
20846
21952
|
function openOS(target, mode) {
|
|
20847
21953
|
if (mode === "edit") {
|
|
@@ -20873,7 +21979,7 @@ function openOS(target, mode) {
|
|
|
20873
21979
|
}
|
|
20874
21980
|
}
|
|
20875
21981
|
function launchDetached(command, args) {
|
|
20876
|
-
const child =
|
|
21982
|
+
const child = spawn2(command, args, { detached: true, stdio: "ignore" });
|
|
20877
21983
|
child.on("error", (err) => {
|
|
20878
21984
|
debugLog(`launchDetached(${command}) failed: ${err.message}`);
|
|
20879
21985
|
});
|
|
@@ -20951,9 +22057,9 @@ init_blob_store();
|
|
|
20951
22057
|
import { Hono as Hono7 } from "hono";
|
|
20952
22058
|
|
|
20953
22059
|
// src/git/image.ts
|
|
20954
|
-
import { spawnSync as
|
|
20955
|
-
import { readFileSync as
|
|
20956
|
-
import { join as
|
|
22060
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
22061
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
22062
|
+
import { join as join10, resolve as resolve5 } from "path";
|
|
20957
22063
|
|
|
20958
22064
|
// src/git/image-metadata.ts
|
|
20959
22065
|
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]);
|
|
@@ -21221,20 +22327,20 @@ function getNewRef(mode) {
|
|
|
21221
22327
|
}
|
|
21222
22328
|
function gitShowFile(ref, filePath, repoRoot) {
|
|
21223
22329
|
const spec = ref === ":" ? `:${filePath}` : `${ref}:${filePath}`;
|
|
21224
|
-
const result =
|
|
22330
|
+
const result = spawnSync7("git", ["show", spec], { cwd: repoRoot, maxBuffer: 50 * 1024 * 1024, env: scrubbedGitEnv() });
|
|
21225
22331
|
if (result.status !== 0 || result.stdout.length === 0) return null;
|
|
21226
22332
|
return result.stdout;
|
|
21227
22333
|
}
|
|
21228
22334
|
function readWorkingFile(filePath, repoRoot) {
|
|
21229
22335
|
try {
|
|
21230
|
-
return
|
|
22336
|
+
return readFileSync8(resolve5(repoRoot, filePath));
|
|
21231
22337
|
} catch {
|
|
21232
22338
|
return null;
|
|
21233
22339
|
}
|
|
21234
22340
|
}
|
|
21235
22341
|
function readDiskImage(absPath) {
|
|
21236
22342
|
try {
|
|
21237
|
-
const data =
|
|
22343
|
+
const data = readFileSync8(absPath);
|
|
21238
22344
|
return { data, size: data.length };
|
|
21239
22345
|
} catch {
|
|
21240
22346
|
return null;
|
|
@@ -21242,7 +22348,7 @@ function readDiskImage(absPath) {
|
|
|
21242
22348
|
}
|
|
21243
22349
|
function getOldImage(mode, filePath, oldPath, repoRoot) {
|
|
21244
22350
|
if (mode.type === "diff") {
|
|
21245
|
-
return readDiskImage(
|
|
22351
|
+
return readDiskImage(join10(directComparisonRoots(mode).rootA, oldPath ?? filePath));
|
|
21246
22352
|
}
|
|
21247
22353
|
const ref = getOldRef(mode);
|
|
21248
22354
|
const path = oldPath ?? filePath;
|
|
@@ -21258,7 +22364,7 @@ function getOldImage(mode, filePath, oldPath, repoRoot) {
|
|
|
21258
22364
|
}
|
|
21259
22365
|
function getNewImage(mode, filePath, repoRoot) {
|
|
21260
22366
|
if (mode.type === "diff") {
|
|
21261
|
-
return readDiskImage(
|
|
22367
|
+
return readDiskImage(join10(directComparisonRoots(mode).rootB, filePath));
|
|
21262
22368
|
}
|
|
21263
22369
|
const ref = getNewRef(mode);
|
|
21264
22370
|
if (ref === null) {
|
|
@@ -21280,9 +22386,9 @@ function getNewImage(mode, filePath, repoRoot) {
|
|
|
21280
22386
|
import { Worker } from "worker_threads";
|
|
21281
22387
|
|
|
21282
22388
|
// src/git/svg-rasterize-render.ts
|
|
21283
|
-
import { existsSync as
|
|
22389
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
|
|
21284
22390
|
import { createRequire } from "module";
|
|
21285
|
-
import { join as
|
|
22391
|
+
import { join as join11 } from "path";
|
|
21286
22392
|
var initialized = false;
|
|
21287
22393
|
var ResvgClass;
|
|
21288
22394
|
var fontBuffers = [];
|
|
@@ -21291,7 +22397,7 @@ async function ensureRenderInit() {
|
|
|
21291
22397
|
const require2 = createRequire(import.meta.url);
|
|
21292
22398
|
const resvgPath = require2.resolve("@resvg/resvg-wasm");
|
|
21293
22399
|
const wasmPath = resvgPath.replace(/index\.(js|mjs)$/, "index_bg.wasm");
|
|
21294
|
-
const wasmBuffer =
|
|
22400
|
+
const wasmBuffer = readFileSync9(wasmPath);
|
|
21295
22401
|
const mod = await import("@resvg/resvg-wasm");
|
|
21296
22402
|
await mod.initWasm(wasmBuffer);
|
|
21297
22403
|
ResvgClass = mod.Resvg;
|
|
@@ -21302,9 +22408,9 @@ function loadSystemFonts() {
|
|
|
21302
22408
|
const buffers = [];
|
|
21303
22409
|
const candidates = getFontCandidates();
|
|
21304
22410
|
for (const path of candidates) {
|
|
21305
|
-
if (!
|
|
22411
|
+
if (!existsSync9(path)) continue;
|
|
21306
22412
|
try {
|
|
21307
|
-
buffers.push(
|
|
22413
|
+
buffers.push(readFileSync9(path));
|
|
21308
22414
|
} catch {
|
|
21309
22415
|
}
|
|
21310
22416
|
}
|
|
@@ -21317,24 +22423,24 @@ function getFontCandidates() {
|
|
|
21317
22423
|
const sup = "/System/Library/Fonts/Supplemental";
|
|
21318
22424
|
return [
|
|
21319
22425
|
// Core system fonts (serif, sans-serif, monospace)
|
|
21320
|
-
|
|
21321
|
-
|
|
21322
|
-
|
|
21323
|
-
|
|
21324
|
-
|
|
21325
|
-
|
|
21326
|
-
|
|
22426
|
+
join11(sys, "Helvetica.ttc"),
|
|
22427
|
+
join11(sys, "Times.ttc"),
|
|
22428
|
+
join11(sys, "Courier.ttc"),
|
|
22429
|
+
join11(sys, "Menlo.ttc"),
|
|
22430
|
+
join11(sys, "SFPro.ttf"),
|
|
22431
|
+
join11(sys, "SFNS.ttf"),
|
|
22432
|
+
join11(sys, "SFNSMono.ttf"),
|
|
21327
22433
|
// Supplemental (common named fonts in SVGs)
|
|
21328
|
-
|
|
21329
|
-
|
|
21330
|
-
|
|
21331
|
-
|
|
21332
|
-
|
|
21333
|
-
|
|
21334
|
-
|
|
21335
|
-
|
|
21336
|
-
|
|
21337
|
-
|
|
22434
|
+
join11(sup, "Arial.ttf"),
|
|
22435
|
+
join11(sup, "Arial Bold.ttf"),
|
|
22436
|
+
join11(sup, "Georgia.ttf"),
|
|
22437
|
+
join11(sup, "Verdana.ttf"),
|
|
22438
|
+
join11(sup, "Tahoma.ttf"),
|
|
22439
|
+
join11(sup, "Trebuchet MS.ttf"),
|
|
22440
|
+
join11(sup, "Impact.ttf"),
|
|
22441
|
+
join11(sup, "Comic Sans MS.ttf"),
|
|
22442
|
+
join11(sup, "Courier New.ttf"),
|
|
22443
|
+
join11(sup, "Times New Roman.ttf")
|
|
21338
22444
|
];
|
|
21339
22445
|
}
|
|
21340
22446
|
if (os === "linux") {
|
|
@@ -21353,17 +22459,17 @@ function getFontCandidates() {
|
|
|
21353
22459
|
];
|
|
21354
22460
|
}
|
|
21355
22461
|
if (os === "win32") {
|
|
21356
|
-
const winFonts =
|
|
22462
|
+
const winFonts = join11(process.env.WINDIR ?? "C:\\Windows", "Fonts");
|
|
21357
22463
|
return [
|
|
21358
|
-
|
|
21359
|
-
|
|
21360
|
-
|
|
21361
|
-
|
|
21362
|
-
|
|
21363
|
-
|
|
21364
|
-
|
|
21365
|
-
|
|
21366
|
-
|
|
22464
|
+
join11(winFonts, "arial.ttf"),
|
|
22465
|
+
join11(winFonts, "arialbd.ttf"),
|
|
22466
|
+
join11(winFonts, "times.ttf"),
|
|
22467
|
+
join11(winFonts, "cour.ttf"),
|
|
22468
|
+
join11(winFonts, "verdana.ttf"),
|
|
22469
|
+
join11(winFonts, "tahoma.ttf"),
|
|
22470
|
+
join11(winFonts, "georgia.ttf"),
|
|
22471
|
+
join11(winFonts, "consola.ttf"),
|
|
22472
|
+
join11(winFonts, "segoeui.ttf")
|
|
21367
22473
|
];
|
|
21368
22474
|
}
|
|
21369
22475
|
return [];
|
|
@@ -21512,8 +22618,8 @@ function submit(job) {
|
|
|
21512
22618
|
}
|
|
21513
22619
|
async function rasterizeSvg(svgData) {
|
|
21514
22620
|
const svg = svgData.toString("utf-8");
|
|
21515
|
-
return new Promise((
|
|
21516
|
-
submit({ svg, resolve:
|
|
22621
|
+
return new Promise((resolve11, reject) => {
|
|
22622
|
+
submit({ svg, resolve: resolve11, reject });
|
|
21517
22623
|
});
|
|
21518
22624
|
}
|
|
21519
22625
|
|
|
@@ -21581,8 +22687,8 @@ imageRoutes.get("/image/:fileId/:side", async (c) => {
|
|
|
21581
22687
|
|
|
21582
22688
|
// src/routes/api/outline.ts
|
|
21583
22689
|
init_queries();
|
|
21584
|
-
import { spawnSync as
|
|
21585
|
-
import { readFileSync as
|
|
22690
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
22691
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
21586
22692
|
import { Hono as Hono8 } from "hono";
|
|
21587
22693
|
import { resolve as resolve6 } from "path";
|
|
21588
22694
|
|
|
@@ -21948,7 +23054,7 @@ outlineRoutes.get("/symbol-definition", async (c) => {
|
|
|
21948
23054
|
}
|
|
21949
23055
|
if (definitions.length === 0) {
|
|
21950
23056
|
try {
|
|
21951
|
-
const allFiles =
|
|
23057
|
+
const allFiles = spawnSync8("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).stdout.trim().split("\n").filter(Boolean);
|
|
21952
23058
|
let scanned = 0;
|
|
21953
23059
|
for (const filePath of allFiles) {
|
|
21954
23060
|
if (searchedPaths.has(filePath)) continue;
|
|
@@ -21960,7 +23066,7 @@ outlineRoutes.get("/symbol-definition", async (c) => {
|
|
|
21960
23066
|
}
|
|
21961
23067
|
let content = "";
|
|
21962
23068
|
try {
|
|
21963
|
-
content =
|
|
23069
|
+
content = readFileSync10(resolve6(repoRoot, filePath), "utf-8");
|
|
21964
23070
|
} catch {
|
|
21965
23071
|
continue;
|
|
21966
23072
|
}
|
|
@@ -21996,16 +23102,16 @@ function collectDefinitions(symbols, targetName, fileId, filePath, out) {
|
|
|
21996
23102
|
}
|
|
21997
23103
|
|
|
21998
23104
|
// src/routes/api/project-settings.ts
|
|
21999
|
-
import { existsSync as
|
|
23105
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
|
|
22000
23106
|
import { Hono as Hono9 } from "hono";
|
|
22001
|
-
import { join as
|
|
23107
|
+
import { join as join12 } from "path";
|
|
22002
23108
|
var projectSettingsRoutes = new Hono9();
|
|
22003
23109
|
function readProjectSettings(repoRoot) {
|
|
22004
|
-
const settingsPath =
|
|
23110
|
+
const settingsPath = join12(repoRoot, ".glassbox", "settings.json");
|
|
22005
23111
|
try {
|
|
22006
|
-
if (
|
|
22007
|
-
const
|
|
22008
|
-
const parsed = ProjectSettingsSchema.safeParse(
|
|
23112
|
+
if (existsSync10(settingsPath)) {
|
|
23113
|
+
const raw2 = JSON.parse(readFileSync11(settingsPath, "utf-8"));
|
|
23114
|
+
const parsed = ProjectSettingsSchema.safeParse(raw2);
|
|
22009
23115
|
if (parsed.success) return parsed.data;
|
|
22010
23116
|
}
|
|
22011
23117
|
} catch {
|
|
@@ -22013,9 +23119,9 @@ function readProjectSettings(repoRoot) {
|
|
|
22013
23119
|
return {};
|
|
22014
23120
|
}
|
|
22015
23121
|
function writeProjectSettings(repoRoot, settings) {
|
|
22016
|
-
const dir =
|
|
22017
|
-
|
|
22018
|
-
|
|
23122
|
+
const dir = join12(repoRoot, ".glassbox");
|
|
23123
|
+
mkdirSync7(dir, { recursive: true });
|
|
23124
|
+
writeFileSync8(join12(dir, "settings.json"), JSON.stringify(settings, null, 2), "utf-8");
|
|
22019
23125
|
}
|
|
22020
23126
|
projectSettingsRoutes.get("/project-settings", (c) => {
|
|
22021
23127
|
const repoRoot = c.get("repoRoot");
|
|
@@ -22031,10 +23137,54 @@ projectSettingsRoutes.patch("/project-settings", async (c) => {
|
|
|
22031
23137
|
return c.json(current);
|
|
22032
23138
|
});
|
|
22033
23139
|
|
|
23140
|
+
// src/routes/api/review-notes.ts
|
|
23141
|
+
init_store();
|
|
23142
|
+
import { readFileSync as readFileSync12, statSync as statSync3 } from "fs";
|
|
23143
|
+
import { Hono as Hono10 } from "hono";
|
|
23144
|
+
import { extname, relative, resolve as resolve7 } from "path";
|
|
23145
|
+
var reviewNotesRoutes = new Hono10();
|
|
23146
|
+
var ARTIFACT_SERVE_MAX_BYTES = 1e7;
|
|
23147
|
+
var IMAGE_CONTENT_TYPES = {
|
|
23148
|
+
".png": "image/png",
|
|
23149
|
+
".webp": "image/webp",
|
|
23150
|
+
".avif": "image/avif",
|
|
23151
|
+
".gif": "image/gif",
|
|
23152
|
+
".jpg": "image/jpeg",
|
|
23153
|
+
".jpeg": "image/jpeg",
|
|
23154
|
+
".svg": "image/svg+xml"
|
|
23155
|
+
};
|
|
23156
|
+
reviewNotesRoutes.get("/review-notes/artifact", (c) => {
|
|
23157
|
+
const file2 = c.req.query("file");
|
|
23158
|
+
if (file2 === void 0 || file2 === "") return c.text("Missing file", 400);
|
|
23159
|
+
const repoRoot = c.get("repoRoot");
|
|
23160
|
+
const abs = resolve7(repoRoot, file2);
|
|
23161
|
+
const rel = relative(repoRoot, abs);
|
|
23162
|
+
if (rel === "" || rel.startsWith("..") || rel.startsWith("/")) return c.text("Forbidden", 403);
|
|
23163
|
+
const ext = extname(abs).toLowerCase();
|
|
23164
|
+
if (!(ext in IMAGE_CONTENT_TYPES)) return c.text("Unsupported artifact type", 415);
|
|
23165
|
+
const contentType = IMAGE_CONTENT_TYPES[ext];
|
|
23166
|
+
try {
|
|
23167
|
+
const stat = statSync3(abs);
|
|
23168
|
+
if (!stat.isFile() || stat.size > ARTIFACT_SERVE_MAX_BYTES) return c.text("Not found", 404);
|
|
23169
|
+
const body = readFileSync12(abs);
|
|
23170
|
+
return c.body(body, 200, { "Content-Type": contentType });
|
|
23171
|
+
} catch {
|
|
23172
|
+
return c.text("Not found", 404);
|
|
23173
|
+
}
|
|
23174
|
+
});
|
|
23175
|
+
reviewNotesRoutes.delete("/review-notes/:guid", (c) => {
|
|
23176
|
+
const guid3 = requirePathParam(c, "guid");
|
|
23177
|
+
if (!guid3.ok) return guid3.response;
|
|
23178
|
+
const repoRoot = c.get("repoRoot");
|
|
23179
|
+
const file2 = c.req.query("file");
|
|
23180
|
+
const removed = removeNote(repoRoot, guid3.data, file2);
|
|
23181
|
+
return c.json({ ok: true, removed });
|
|
23182
|
+
});
|
|
23183
|
+
|
|
22034
23184
|
// src/routes/api/reviews.ts
|
|
22035
23185
|
init_queries();
|
|
22036
|
-
import { Hono as
|
|
22037
|
-
var reviewsRoutes = new
|
|
23186
|
+
import { Hono as Hono11 } from "hono";
|
|
23187
|
+
var reviewsRoutes = new Hono11();
|
|
22038
23188
|
reviewsRoutes.get("/reviews", async (c) => {
|
|
22039
23189
|
const repoRoot = c.get("repoRoot");
|
|
22040
23190
|
const reviews = await listReviews(repoRoot);
|
|
@@ -22124,8 +23274,8 @@ reviewsRoutes.post("/reviews/delete-all", async (c) => {
|
|
|
22124
23274
|
|
|
22125
23275
|
// src/routes/api/share-prompt.ts
|
|
22126
23276
|
init_zod();
|
|
22127
|
-
import { Hono as
|
|
22128
|
-
var sharePromptRoutes = new
|
|
23277
|
+
import { Hono as Hono12 } from "hono";
|
|
23278
|
+
var sharePromptRoutes = new Hono12();
|
|
22129
23279
|
var SharePromptShapeSchema = external_exports.object({
|
|
22130
23280
|
dismissedAt: external_exports.number().nullable().optional(),
|
|
22131
23281
|
totalOpenMs: external_exports.number().optional()
|
|
@@ -22164,8 +23314,8 @@ sharePromptRoutes.post("/share-prompt/tick", async (c) => {
|
|
|
22164
23314
|
});
|
|
22165
23315
|
|
|
22166
23316
|
// src/routes/api/system.ts
|
|
22167
|
-
import { Hono as
|
|
22168
|
-
var systemRoutes = new
|
|
23317
|
+
import { Hono as Hono13 } from "hono";
|
|
23318
|
+
var systemRoutes = new Hono13();
|
|
22169
23319
|
systemRoutes.post("/open-external", async (c) => {
|
|
22170
23320
|
const parsed = await parseBody(c, OpenExternalReqSchema);
|
|
22171
23321
|
if (!parsed.ok) return parsed.response;
|
|
@@ -22177,7 +23327,7 @@ systemRoutes.post("/open-external", async (c) => {
|
|
|
22177
23327
|
});
|
|
22178
23328
|
|
|
22179
23329
|
// src/routes/api.ts
|
|
22180
|
-
var apiRoutes = new
|
|
23330
|
+
var apiRoutes = new Hono14();
|
|
22181
23331
|
apiRoutes.route("/", reviewsRoutes);
|
|
22182
23332
|
apiRoutes.route("/", filesRoutes);
|
|
22183
23333
|
apiRoutes.route("/", annotationsRoutes);
|
|
@@ -22185,20 +23335,21 @@ apiRoutes.route("/", outlineRoutes);
|
|
|
22185
23335
|
apiRoutes.route("/", contextRoutes);
|
|
22186
23336
|
apiRoutes.route("/", projectSettingsRoutes);
|
|
22187
23337
|
apiRoutes.route("/", imageRoutes);
|
|
23338
|
+
apiRoutes.route("/", reviewNotesRoutes);
|
|
22188
23339
|
apiRoutes.route("/", sharePromptRoutes);
|
|
22189
23340
|
apiRoutes.route("/", systemRoutes);
|
|
22190
23341
|
|
|
22191
23342
|
// src/routes/channel-api.ts
|
|
22192
|
-
import { spawnSync as
|
|
22193
|
-
import { mkdirSync as
|
|
22194
|
-
import { Hono as
|
|
22195
|
-
import { join as
|
|
22196
|
-
var channelApiRoutes = new
|
|
23343
|
+
import { spawnSync as spawnSync9 } from "child_process";
|
|
23344
|
+
import { mkdirSync as mkdirSync8 } from "fs";
|
|
23345
|
+
import { Hono as Hono15 } from "hono";
|
|
23346
|
+
import { join as join13 } from "path";
|
|
23347
|
+
var channelApiRoutes = new Hono15();
|
|
22197
23348
|
channelApiRoutes.get("/status", async (c) => {
|
|
22198
23349
|
const config2 = readGlobalConfig();
|
|
22199
23350
|
const enabled = config2.channelEnabled === true;
|
|
22200
23351
|
const repoRoot = c.get("repoRoot");
|
|
22201
|
-
const dataDir =
|
|
23352
|
+
const dataDir = join13(repoRoot, ".glassbox");
|
|
22202
23353
|
const connected = enabled ? await isChannelAlive(dataDir) : false;
|
|
22203
23354
|
return c.json({ enabled, connected });
|
|
22204
23355
|
});
|
|
@@ -22207,8 +23358,8 @@ channelApiRoutes.post("/enable", (c) => {
|
|
|
22207
23358
|
config2.channelEnabled = true;
|
|
22208
23359
|
});
|
|
22209
23360
|
const repoRoot = c.get("repoRoot");
|
|
22210
|
-
const dataDir =
|
|
22211
|
-
|
|
23361
|
+
const dataDir = join13(repoRoot, ".glassbox");
|
|
23362
|
+
mkdirSync8(dataDir, { recursive: true });
|
|
22212
23363
|
registerChannel(dataDir);
|
|
22213
23364
|
return c.json({ ok: true });
|
|
22214
23365
|
});
|
|
@@ -22217,7 +23368,7 @@ channelApiRoutes.post("/disable", (c) => {
|
|
|
22217
23368
|
config2.channelEnabled = false;
|
|
22218
23369
|
});
|
|
22219
23370
|
const repoRoot = c.get("repoRoot");
|
|
22220
|
-
const dataDir =
|
|
23371
|
+
const dataDir = join13(repoRoot, ".glassbox");
|
|
22221
23372
|
unregisterChannel(dataDir);
|
|
22222
23373
|
return c.json({ ok: true });
|
|
22223
23374
|
});
|
|
@@ -22225,7 +23376,7 @@ channelApiRoutes.post("/trigger", async (c) => {
|
|
|
22225
23376
|
const parsed = await parseBody(c, TriggerChannelReqSchema);
|
|
22226
23377
|
if (!parsed.ok) return parsed.response;
|
|
22227
23378
|
const repoRoot = c.get("repoRoot");
|
|
22228
|
-
const dataDir =
|
|
23379
|
+
const dataDir = join13(repoRoot, ".glassbox");
|
|
22229
23380
|
const sent = await triggerChannel(dataDir, parsed.data.message);
|
|
22230
23381
|
if (!sent) {
|
|
22231
23382
|
return c.json({ error: "Channel not connected" }, 503);
|
|
@@ -22234,7 +23385,7 @@ channelApiRoutes.post("/trigger", async (c) => {
|
|
|
22234
23385
|
});
|
|
22235
23386
|
channelApiRoutes.get("/claude-check", (c) => {
|
|
22236
23387
|
try {
|
|
22237
|
-
const result =
|
|
23388
|
+
const result = spawnSync9("claude", ["--version"], { encoding: "utf-8", timeout: 5e3 });
|
|
22238
23389
|
if (result.status !== 0) {
|
|
22239
23390
|
return c.json({ installed: false, version: null, meetsMinimum: false });
|
|
22240
23391
|
}
|
|
@@ -22253,13 +23404,13 @@ channelApiRoutes.get("/claude-check", (c) => {
|
|
|
22253
23404
|
});
|
|
22254
23405
|
|
|
22255
23406
|
// src/routes/difftool-api.ts
|
|
22256
|
-
import { Hono as
|
|
23407
|
+
import { Hono as Hono16 } from "hono";
|
|
22257
23408
|
init_connection();
|
|
22258
23409
|
init_queries();
|
|
22259
23410
|
init_blob_store();
|
|
22260
23411
|
init_session();
|
|
22261
23412
|
init_difftool();
|
|
22262
|
-
var difftoolApiRoutes = new
|
|
23413
|
+
var difftoolApiRoutes = new Hono16();
|
|
22263
23414
|
difftoolApiRoutes.get("/status", (c) => {
|
|
22264
23415
|
return c.json(getDifftoolStatus("global"));
|
|
22265
23416
|
});
|
|
@@ -22321,9 +23472,9 @@ difftoolApiRoutes.get("/hold", (c) => {
|
|
|
22321
23472
|
const session2 = getDifftoolSession();
|
|
22322
23473
|
if (session2 === null) return c.json({ ended: true });
|
|
22323
23474
|
noteDifftoolActivity();
|
|
22324
|
-
return new Promise((
|
|
23475
|
+
return new Promise((resolve11) => {
|
|
22325
23476
|
addDifftoolHold(() => {
|
|
22326
|
-
|
|
23477
|
+
resolve11(c.json({ ended: true }));
|
|
22327
23478
|
});
|
|
22328
23479
|
c.req.raw.signal.addEventListener("abort", () => {
|
|
22329
23480
|
endDifftoolSession();
|
|
@@ -22336,9 +23487,12 @@ difftoolApiRoutes.post("/end", (c) => {
|
|
|
22336
23487
|
});
|
|
22337
23488
|
|
|
22338
23489
|
// src/routes/pages.tsx
|
|
22339
|
-
import { readFileSync as
|
|
22340
|
-
import { Hono as
|
|
22341
|
-
import { resolve as
|
|
23490
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
23491
|
+
import { Hono as Hono17 } from "hono";
|
|
23492
|
+
import { resolve as resolve8 } from "path";
|
|
23493
|
+
|
|
23494
|
+
// src/components/diffView.tsx
|
|
23495
|
+
import { raw } from "kerfjs";
|
|
22342
23496
|
|
|
22343
23497
|
// src/icons.tsx
|
|
22344
23498
|
import { jsx, jsxs } from "kerfjs/jsx-runtime";
|
|
@@ -22400,6 +23554,9 @@ function IconActualSize() {
|
|
|
22400
23554
|
] });
|
|
22401
23555
|
}
|
|
22402
23556
|
|
|
23557
|
+
// src/components/diffView.tsx
|
|
23558
|
+
init_view();
|
|
23559
|
+
|
|
22403
23560
|
// src/utils/charDiff.ts
|
|
22404
23561
|
var MAX_LINE_LENGTH = 5e3;
|
|
22405
23562
|
function charDiff(oldStr, newStr) {
|
|
@@ -22471,6 +23628,26 @@ function truncateDiffLine(content) {
|
|
|
22471
23628
|
};
|
|
22472
23629
|
}
|
|
22473
23630
|
|
|
23631
|
+
// src/utils/noteMarkdown.ts
|
|
23632
|
+
function escapeHtml(text) {
|
|
23633
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
23634
|
+
}
|
|
23635
|
+
var SAFE_URL = /^(https?:\/\/|mailto:)/i;
|
|
23636
|
+
function renderInline(escaped) {
|
|
23637
|
+
let out = escaped.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
23638
|
+
out = out.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (match, text, url2) => {
|
|
23639
|
+
if (!SAFE_URL.test(url2)) return match;
|
|
23640
|
+
return `<a href="${url2}" target="_blank" rel="noopener noreferrer">${text}</a>`;
|
|
23641
|
+
});
|
|
23642
|
+
out = out.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
23643
|
+
out = out.replace(/(^|[^*])\*([^*\s][^*]*)\*/g, "$1<em>$2</em>");
|
|
23644
|
+
out = out.replace(/(^|[^_])_([^_\s][^_]*)_/g, "$1<em>$2</em>");
|
|
23645
|
+
return out;
|
|
23646
|
+
}
|
|
23647
|
+
function renderNoteMarkdown(text) {
|
|
23648
|
+
return escapeHtml(text).split("\n").map(renderInline).join("<br>");
|
|
23649
|
+
}
|
|
23650
|
+
|
|
22474
23651
|
// src/components/imageDiff.tsx
|
|
22475
23652
|
import { jsx as jsx2, jsxs as jsxs2 } from "kerfjs/jsx-runtime";
|
|
22476
23653
|
function ImageDiff({ file: file2, diff, fontWarning, baseWidth, baseHeight }) {
|
|
@@ -22521,12 +23698,23 @@ function ImageDiff({ file: file2, diff, fontWarning, baseWidth, baseHeight }) {
|
|
|
22521
23698
|
|
|
22522
23699
|
// src/components/diffView.tsx
|
|
22523
23700
|
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "kerfjs/jsx-runtime";
|
|
22524
|
-
function DiffView({ file: file2, diff, annotations, mode }) {
|
|
23701
|
+
function DiffView({ file: file2, diff, annotations, mode, reviewNotes = [] }) {
|
|
23702
|
+
const loadedNoteGuids = new Set(reviewNotes.map((n) => n.guid).filter((g) => g !== void 0));
|
|
23703
|
+
const repliesByNote = {};
|
|
22525
23704
|
const annotationsByLine = {};
|
|
22526
23705
|
for (const a of annotations) {
|
|
23706
|
+
if (a.reply_to_note_id !== null && loadedNoteGuids.has(a.reply_to_note_id)) {
|
|
23707
|
+
(repliesByNote[a.reply_to_note_id] ??= []).push(a);
|
|
23708
|
+
continue;
|
|
23709
|
+
}
|
|
22527
23710
|
const key = `${a.line_number}:${a.side}`;
|
|
22528
|
-
|
|
22529
|
-
|
|
23711
|
+
(annotationsByLine[key] ??= []).push(a);
|
|
23712
|
+
}
|
|
23713
|
+
const reviewNotesByLine = {};
|
|
23714
|
+
for (const n of reviewNotes) {
|
|
23715
|
+
const key = `${n.line}:${n.side}`;
|
|
23716
|
+
if (!(key in reviewNotesByLine)) reviewNotesByLine[key] = [];
|
|
23717
|
+
reviewNotesByLine[key].push(n);
|
|
22530
23718
|
}
|
|
22531
23719
|
return /* @__PURE__ */ jsxs3(
|
|
22532
23720
|
"div",
|
|
@@ -22543,7 +23731,7 @@ function DiffView({ file: file2, diff, annotations, mode }) {
|
|
|
22543
23731
|
] }),
|
|
22544
23732
|
/* @__PURE__ */ jsx3("div", { className: "diff-header-actions", children: /* @__PURE__ */ jsx3("span", { className: `file-status ${diff.status}`, children: diff.status }) })
|
|
22545
23733
|
] }),
|
|
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 })
|
|
23734
|
+
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
23735
|
]
|
|
22548
23736
|
}
|
|
22549
23737
|
);
|
|
@@ -22553,7 +23741,10 @@ function getAnnotations(pair, annotationsByLine) {
|
|
|
22553
23741
|
const rightAnns = pair.right ? annotationsByLine[`${pair.right.newNum}:new`] ?? [] : [];
|
|
22554
23742
|
return [...leftAnns, ...rightAnns];
|
|
22555
23743
|
}
|
|
22556
|
-
function
|
|
23744
|
+
function getReviewNotes(pair, reviewNotesByLine) {
|
|
23745
|
+
return pair.right ? reviewNotesByLine[`${pair.right.newNum}:new`] ?? [] : [];
|
|
23746
|
+
}
|
|
23747
|
+
function SplitDiff({ hunks, annotationsByLine, reviewNotesByLine, repliesByNote }) {
|
|
22557
23748
|
const lastHunk = hunks[hunks.length - 1];
|
|
22558
23749
|
const tailStart = lastHunk ? lastHunk.newStart + lastHunk.newCount : 1;
|
|
22559
23750
|
const items = [];
|
|
@@ -22565,8 +23756,9 @@ function SplitDiff({ hunks, annotationsByLine }) {
|
|
|
22565
23756
|
items.push({ kind: "separator", hunkIdx, hunk, gapStart, gapEnd });
|
|
22566
23757
|
for (const pair of pairLines(hunk.lines)) {
|
|
22567
23758
|
const anns = getAnnotations(pair, annotationsByLine);
|
|
22568
|
-
|
|
22569
|
-
|
|
23759
|
+
const notes = getReviewNotes(pair, reviewNotesByLine);
|
|
23760
|
+
if (anns.length > 0 || notes.length > 0) {
|
|
23761
|
+
items.push({ kind: "annotated", pair, annotations: anns, reviewNotes: notes });
|
|
22570
23762
|
} else {
|
|
22571
23763
|
items.push({ kind: "pair", pair });
|
|
22572
23764
|
}
|
|
@@ -22581,7 +23773,7 @@ function SplitDiff({ hunks, annotationsByLine }) {
|
|
|
22581
23773
|
groups.push({ type: "columns", items: run });
|
|
22582
23774
|
run = [];
|
|
22583
23775
|
}
|
|
22584
|
-
groups.push({ type: "annotated", pair: item.pair, annotations: item.annotations });
|
|
23776
|
+
groups.push({ type: "annotated", pair: item.pair, annotations: item.annotations, reviewNotes: item.reviewNotes });
|
|
22585
23777
|
} else {
|
|
22586
23778
|
run.push(item);
|
|
22587
23779
|
}
|
|
@@ -22617,7 +23809,8 @@ function SplitDiff({ hunks, annotationsByLine }) {
|
|
|
22617
23809
|
}
|
|
22618
23810
|
)
|
|
22619
23811
|
] }),
|
|
22620
|
-
/* @__PURE__ */ jsx3(AnnotationRows, { annotations: group.annotations })
|
|
23812
|
+
group.annotations.length > 0 ? /* @__PURE__ */ jsx3(AnnotationRows, { annotations: group.annotations }) : null,
|
|
23813
|
+
group.reviewNotes.length > 0 ? /* @__PURE__ */ jsx3(ReviewNoteRows, { notes: group.reviewNotes, repliesByNote }) : null
|
|
22621
23814
|
] });
|
|
22622
23815
|
}
|
|
22623
23816
|
return /* @__PURE__ */ jsxs3("div", { className: "split-columns", children: [
|
|
@@ -22783,7 +23976,7 @@ function buildUnifiedCharDiffs(lines) {
|
|
|
22783
23976
|
}
|
|
22784
23977
|
return result;
|
|
22785
23978
|
}
|
|
22786
|
-
function UnifiedDiff({ hunks, annotationsByLine }) {
|
|
23979
|
+
function UnifiedDiff({ hunks, annotationsByLine, reviewNotesByLine, repliesByNote }) {
|
|
22787
23980
|
const lastHunk = hunks[hunks.length - 1];
|
|
22788
23981
|
const tailStart = lastHunk ? lastHunk.newStart + lastHunk.newCount : 1;
|
|
22789
23982
|
return /* @__PURE__ */ jsxs3("div", { className: "diff-table-unified", children: [
|
|
@@ -22816,6 +24009,7 @@ function UnifiedDiff({ hunks, annotationsByLine }) {
|
|
|
22816
24009
|
const lineNum = line.type === "remove" ? line.oldNum : line.newNum;
|
|
22817
24010
|
const side = line.type === "remove" ? "old" : "new";
|
|
22818
24011
|
const anns = annotationsByLine[`${lineNum}:${side}`] ?? [];
|
|
24012
|
+
const notes = reviewNotesByLine[`${lineNum}:${side}`] ?? [];
|
|
22819
24013
|
const segments = charDiffs.get(line);
|
|
22820
24014
|
return /* @__PURE__ */ jsxs3("div", { children: [
|
|
22821
24015
|
/* @__PURE__ */ jsxs3(
|
|
@@ -22831,7 +24025,8 @@ function UnifiedDiff({ hunks, annotationsByLine }) {
|
|
|
22831
24025
|
]
|
|
22832
24026
|
}
|
|
22833
24027
|
),
|
|
22834
|
-
anns.length > 0 ? /* @__PURE__ */ jsx3(AnnotationRows, { annotations: anns }) : null
|
|
24028
|
+
anns.length > 0 ? /* @__PURE__ */ jsx3(AnnotationRows, { annotations: anns }) : null,
|
|
24029
|
+
notes.length > 0 ? /* @__PURE__ */ jsx3(ReviewNoteRows, { notes, repliesByNote }) : null
|
|
22835
24030
|
] });
|
|
22836
24031
|
})
|
|
22837
24032
|
] });
|
|
@@ -22839,8 +24034,61 @@ function UnifiedDiff({ hunks, annotationsByLine }) {
|
|
|
22839
24034
|
/* @__PURE__ */ jsx3("div", { className: "hunk-separator hunk-expander-tail", "data-start": tailStart, children: "\u2195 Show remaining lines" })
|
|
22840
24035
|
] });
|
|
22841
24036
|
}
|
|
22842
|
-
function
|
|
22843
|
-
return /* @__PURE__ */ jsx3(
|
|
24037
|
+
function ReviewNoteRows({ notes, repliesByNote }) {
|
|
24038
|
+
return /* @__PURE__ */ jsx3(Fragment, { children: notes.map((n) => {
|
|
24039
|
+
const replies = n.guid !== void 0 ? repliesByNote[n.guid] ?? [] : [];
|
|
24040
|
+
return /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
24041
|
+
/* @__PURE__ */ jsxs3(
|
|
24042
|
+
"div",
|
|
24043
|
+
{
|
|
24044
|
+
className: `ai-note-row ai-note-review${n.stale === true ? " ai-note-stale" : ""}`,
|
|
24045
|
+
"data-kind": n.kind,
|
|
24046
|
+
"data-note-id": n.guid,
|
|
24047
|
+
children: [
|
|
24048
|
+
/* @__PURE__ */ jsxs3("div", { className: "ai-note-item", children: [
|
|
24049
|
+
/* @__PURE__ */ jsx3("span", { className: `ai-note-label ai-note-label-${n.kind}`, children: REVIEW_NOTE_LABELS[n.kind] ?? n.kind }),
|
|
24050
|
+
n.stale === true ? /* @__PURE__ */ jsx3("span", { className: "ai-note-stale-tag", title: "The code this note referred to has changed", children: "outdated" }) : null,
|
|
24051
|
+
/* @__PURE__ */ jsx3("span", { className: "ai-note-text", children: raw(renderNoteMarkdown(n.body)) }),
|
|
24052
|
+
n.producer !== void 0 ? /* @__PURE__ */ jsx3("span", { className: "ai-note-producer", children: n.producer }) : null,
|
|
24053
|
+
n.guid !== void 0 ? /* @__PURE__ */ jsx3("button", { className: "ai-note-reply-btn", "data-line": String(n.line), children: "Reply" }) : null,
|
|
24054
|
+
n.stale === true && n.guid !== void 0 ? /* @__PURE__ */ jsxs3("span", { className: "ai-note-stale-actions", children: [
|
|
24055
|
+
/* @__PURE__ */ jsx3("button", { className: "ai-note-keep-btn", title: "Dismiss the outdated flag for now", children: "Keep" }),
|
|
24056
|
+
/* @__PURE__ */ jsx3("button", { className: "ai-note-discard-btn", title: "Remove this note from .pr-notes/", children: "Discard" })
|
|
24057
|
+
] }) : null
|
|
24058
|
+
] }),
|
|
24059
|
+
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: [
|
|
24060
|
+
/* @__PURE__ */ jsxs3("summary", { children: [
|
|
24061
|
+
"\u{1F4CE} ",
|
|
24062
|
+
a.uri
|
|
24063
|
+
] }),
|
|
24064
|
+
/* @__PURE__ */ jsx3("pre", { className: "ai-note-artifact-content", children: /* @__PURE__ */ jsx3("code", { children: a.content }) })
|
|
24065
|
+
] }) : a.isImage === true ? /* @__PURE__ */ jsxs3("details", { className: "ai-note-artifact", children: [
|
|
24066
|
+
/* @__PURE__ */ jsxs3("summary", { children: [
|
|
24067
|
+
"\u{1F4CE} ",
|
|
24068
|
+
a.uri
|
|
24069
|
+
] }),
|
|
24070
|
+
/* @__PURE__ */ jsx3(
|
|
24071
|
+
"img",
|
|
24072
|
+
{
|
|
24073
|
+
className: "ai-note-artifact-img",
|
|
24074
|
+
loading: "lazy",
|
|
24075
|
+
alt: a.uri,
|
|
24076
|
+
src: `/api/review-notes/artifact?file=${encodeURIComponent(a.uri)}`
|
|
24077
|
+
}
|
|
24078
|
+
)
|
|
24079
|
+
] }) : /* @__PURE__ */ jsxs3("div", { className: "ai-note-artifact ai-note-artifact-ref", children: [
|
|
24080
|
+
"\u{1F4CE} ",
|
|
24081
|
+
a.uri
|
|
24082
|
+
] })) }) : null
|
|
24083
|
+
]
|
|
24084
|
+
}
|
|
24085
|
+
),
|
|
24086
|
+
replies.length > 0 ? /* @__PURE__ */ jsx3("div", { className: "annotation-row ai-note-replies", children: replies.map((a) => /* @__PURE__ */ jsx3(AnnotationItem, { annotation: a })) }) : null
|
|
24087
|
+
] });
|
|
24088
|
+
}) });
|
|
24089
|
+
}
|
|
24090
|
+
function AnnotationItem({ annotation: a }) {
|
|
24091
|
+
return /* @__PURE__ */ jsxs3(
|
|
22844
24092
|
"div",
|
|
22845
24093
|
{
|
|
22846
24094
|
className: `annotation-item${a.is_stale ? " annotation-stale" : ""}`,
|
|
@@ -22850,6 +24098,7 @@ function AnnotationRows({ annotations }) {
|
|
|
22850
24098
|
children: [
|
|
22851
24099
|
/* @__PURE__ */ jsx3("span", { className: "annotation-drag-handle", draggable: true, title: "Drag to move", children: "\u283F" }),
|
|
22852
24100
|
/* @__PURE__ */ jsx3("span", { className: `annotation-category category-${a.category}`, "data-action": "reclassify", children: a.category }),
|
|
24101
|
+
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
24102
|
/* @__PURE__ */ jsx3("span", { className: "annotation-text", children: a.content }),
|
|
22854
24103
|
/* @__PURE__ */ jsxs3("div", { className: "annotation-actions", children: [
|
|
22855
24104
|
a.is_stale ? /* @__PURE__ */ jsx3("button", { className: "btn btn-xs btn-keep", "data-action": "keep", children: "Keep" }) : null,
|
|
@@ -22858,13 +24107,16 @@ function AnnotationRows({ annotations }) {
|
|
|
22858
24107
|
] })
|
|
22859
24108
|
]
|
|
22860
24109
|
}
|
|
22861
|
-
)
|
|
24110
|
+
);
|
|
24111
|
+
}
|
|
24112
|
+
function AnnotationRows({ annotations }) {
|
|
24113
|
+
return /* @__PURE__ */ jsx3("div", { className: "annotation-row", children: annotations.map((a) => /* @__PURE__ */ jsx3(AnnotationItem, { annotation: a })) });
|
|
22862
24114
|
}
|
|
22863
24115
|
|
|
22864
24116
|
// src/themes/config.ts
|
|
22865
|
-
import { existsSync as
|
|
22866
|
-
import { join as
|
|
22867
|
-
var THEMES_DIR =
|
|
24117
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync9, readdirSync as readdirSync2, readFileSync as readFileSync13, unlinkSync as unlinkSync3, writeFileSync as writeFileSync9 } from "fs";
|
|
24118
|
+
import { join as join14 } from "path";
|
|
24119
|
+
var THEMES_DIR = join14(GLOBAL_CONFIG_DIR, "themes");
|
|
22868
24120
|
function getActiveThemeId() {
|
|
22869
24121
|
const config2 = readGlobalConfig();
|
|
22870
24122
|
const theme = config2.theme;
|
|
@@ -22878,13 +24130,13 @@ function setActiveThemeId(id) {
|
|
|
22878
24130
|
});
|
|
22879
24131
|
}
|
|
22880
24132
|
function loadCustomThemes() {
|
|
22881
|
-
if (!
|
|
24133
|
+
if (!existsSync11(THEMES_DIR)) return [];
|
|
22882
24134
|
const themes = [];
|
|
22883
24135
|
try {
|
|
22884
|
-
const files =
|
|
24136
|
+
const files = readdirSync2(THEMES_DIR).filter((f) => f.endsWith(".json"));
|
|
22885
24137
|
for (const file2 of files) {
|
|
22886
24138
|
try {
|
|
22887
|
-
const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(
|
|
24139
|
+
const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync13(join14(THEMES_DIR, file2), "utf-8")));
|
|
22888
24140
|
if (!parsed.success) continue;
|
|
22889
24141
|
const d = parsed.data;
|
|
22890
24142
|
themes.push({ id: d.id, name: d.name, colors: d.colors, builtIn: false, baseTheme: d.baseTheme ?? "" });
|
|
@@ -22896,21 +24148,21 @@ function loadCustomThemes() {
|
|
|
22896
24148
|
return themes;
|
|
22897
24149
|
}
|
|
22898
24150
|
function saveCustomTheme(theme) {
|
|
22899
|
-
|
|
22900
|
-
const filePath =
|
|
22901
|
-
|
|
24151
|
+
mkdirSync9(THEMES_DIR, { recursive: true });
|
|
24152
|
+
const filePath = join14(THEMES_DIR, `${theme.id}.json`);
|
|
24153
|
+
writeFileSync9(filePath, JSON.stringify(theme, null, 2), "utf-8");
|
|
22902
24154
|
}
|
|
22903
24155
|
function deleteCustomTheme(id) {
|
|
22904
|
-
const filePath =
|
|
22905
|
-
if (
|
|
22906
|
-
|
|
24156
|
+
const filePath = join14(THEMES_DIR, `${id}.json`);
|
|
24157
|
+
if (existsSync11(filePath)) {
|
|
24158
|
+
unlinkSync3(filePath);
|
|
22907
24159
|
}
|
|
22908
24160
|
}
|
|
22909
24161
|
function getCustomTheme(id) {
|
|
22910
|
-
const filePath =
|
|
22911
|
-
if (!
|
|
24162
|
+
const filePath = join14(THEMES_DIR, `${id}.json`);
|
|
24163
|
+
if (!existsSync11(filePath)) return void 0;
|
|
22912
24164
|
try {
|
|
22913
|
-
const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(
|
|
24165
|
+
const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync13(filePath, "utf-8")));
|
|
22914
24166
|
if (!parsed.success) return void 0;
|
|
22915
24167
|
const d = parsed.data;
|
|
22916
24168
|
return { id: d.id, name: d.name, colors: d.colors, builtIn: false, baseTheme: d.baseTheme ?? "" };
|
|
@@ -22972,8 +24224,8 @@ function formatReviewMode(mode, modeArgs) {
|
|
|
22972
24224
|
return `commit: ${shortenIfSha(argsFor(mode, "commit:", modeArgs))}`;
|
|
22973
24225
|
}
|
|
22974
24226
|
if (mode === "range" || mode.startsWith("range:")) {
|
|
22975
|
-
const
|
|
22976
|
-
const [from = "", to = ""] =
|
|
24227
|
+
const raw2 = argsFor(mode, "range:", modeArgs);
|
|
24228
|
+
const [from = "", to = ""] = raw2.split("..");
|
|
22977
24229
|
return `range: ${shortenIfSha(from)}..${shortenIfSha(to)}`;
|
|
22978
24230
|
}
|
|
22979
24231
|
if (mode === "branch" || mode.startsWith("branch:")) {
|
|
@@ -23209,8 +24461,41 @@ function ReviewShell({ reviewId, review, files, annotationCounts, staleCounts, f
|
|
|
23209
24461
|
|
|
23210
24462
|
// src/routes/pages.tsx
|
|
23211
24463
|
init_queries();
|
|
24464
|
+
|
|
24465
|
+
// src/review-notes/reanchor.ts
|
|
24466
|
+
var MATCH_RADIUS = 50;
|
|
24467
|
+
function reanchorReviewNotes(notes, diff) {
|
|
24468
|
+
const byLine = /* @__PURE__ */ new Map();
|
|
24469
|
+
for (const hunk of diff.hunks) {
|
|
24470
|
+
for (const line of hunk.lines) {
|
|
24471
|
+
if (line.newNum !== null) byLine.set(line.newNum, line.content);
|
|
24472
|
+
}
|
|
24473
|
+
}
|
|
24474
|
+
return notes.map((note) => {
|
|
24475
|
+
if (note.snippet === void 0 || note.snippet === "") return note;
|
|
24476
|
+
const anchor = note.snippet.split("\n")[0];
|
|
24477
|
+
const current = byLine.get(note.line);
|
|
24478
|
+
if (current === anchor) return note;
|
|
24479
|
+
let best = null;
|
|
24480
|
+
let bestDistance = Infinity;
|
|
24481
|
+
for (const [lineNum, content] of byLine) {
|
|
24482
|
+
if (content !== anchor) continue;
|
|
24483
|
+
const distance = Math.abs(lineNum - note.line);
|
|
24484
|
+
if (distance < bestDistance && distance <= MATCH_RADIUS) {
|
|
24485
|
+
bestDistance = distance;
|
|
24486
|
+
best = lineNum;
|
|
24487
|
+
}
|
|
24488
|
+
}
|
|
24489
|
+
if (best !== null) return { ...note, line: best, stale: false };
|
|
24490
|
+
if (current !== void 0) return { ...note, stale: true };
|
|
24491
|
+
return note;
|
|
24492
|
+
});
|
|
24493
|
+
}
|
|
24494
|
+
|
|
24495
|
+
// src/routes/pages.tsx
|
|
24496
|
+
init_store();
|
|
23212
24497
|
import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs8 } from "kerfjs/jsx-runtime";
|
|
23213
|
-
var pageRoutes = new
|
|
24498
|
+
var pageRoutes = new Hono17();
|
|
23214
24499
|
pageRoutes.get("/", async (c) => {
|
|
23215
24500
|
const reviewId = c.get("reviewId");
|
|
23216
24501
|
const review = await getReview(reviewId);
|
|
@@ -23289,7 +24574,9 @@ pageRoutes.get("/file/:fileId", async (c) => {
|
|
|
23289
24574
|
}
|
|
23290
24575
|
}
|
|
23291
24576
|
}
|
|
23292
|
-
const
|
|
24577
|
+
const rawNotes = getDemoMode() !== null ? demoReviewNotes(file2.file_path) : loadReviewNotesForFile(c.get("repoRoot"), file2.file_path);
|
|
24578
|
+
const reviewNotes = reanchorReviewNotes(rawNotes, finalDiff);
|
|
24579
|
+
const html = /* @__PURE__ */ jsx8(DiffView, { file: file2, diff: finalDiff, annotations, mode, reviewNotes });
|
|
23293
24580
|
return c.html(html.toString());
|
|
23294
24581
|
});
|
|
23295
24582
|
pageRoutes.get("/file-raw", (c) => {
|
|
@@ -23298,7 +24585,7 @@ pageRoutes.get("/file-raw", (c) => {
|
|
|
23298
24585
|
const repoRoot = c.get("repoRoot");
|
|
23299
24586
|
let content;
|
|
23300
24587
|
try {
|
|
23301
|
-
content =
|
|
24588
|
+
content = readFileSync14(resolve8(repoRoot, filePath), "utf-8");
|
|
23302
24589
|
} catch {
|
|
23303
24590
|
return c.text("File not found", 404);
|
|
23304
24591
|
}
|
|
@@ -23354,8 +24641,8 @@ pageRoutes.get("/history", async (c) => {
|
|
|
23354
24641
|
});
|
|
23355
24642
|
|
|
23356
24643
|
// src/routes/theme-api.ts
|
|
23357
|
-
import { Hono as
|
|
23358
|
-
var themeApiRoutes = new
|
|
24644
|
+
import { Hono as Hono18 } from "hono";
|
|
24645
|
+
var themeApiRoutes = new Hono18();
|
|
23359
24646
|
themeApiRoutes.get("/", (c) => {
|
|
23360
24647
|
const themes = getAllThemes();
|
|
23361
24648
|
const activeId = getActiveThemeId();
|
|
@@ -23469,10 +24756,10 @@ themeApiRoutes.delete("/:id", (c) => {
|
|
|
23469
24756
|
|
|
23470
24757
|
// src/server.ts
|
|
23471
24758
|
function tryServe(appFetch, port) {
|
|
23472
|
-
return new Promise((
|
|
24759
|
+
return new Promise((resolve11, reject) => {
|
|
23473
24760
|
const server = serve({ fetch: appFetch, port, hostname: "127.0.0.1" });
|
|
23474
24761
|
server.on("listening", () => {
|
|
23475
|
-
|
|
24762
|
+
resolve11({ port, server });
|
|
23476
24763
|
});
|
|
23477
24764
|
server.on("error", (err) => {
|
|
23478
24765
|
reject(err);
|
|
@@ -23480,25 +24767,25 @@ function tryServe(appFetch, port) {
|
|
|
23480
24767
|
});
|
|
23481
24768
|
}
|
|
23482
24769
|
async function startServer(port, reviewId, repoRoot, options) {
|
|
23483
|
-
const app = new
|
|
24770
|
+
const app = new Hono19();
|
|
23484
24771
|
app.use("*", async (c, next) => {
|
|
23485
24772
|
c.set("reviewId", reviewId);
|
|
23486
24773
|
c.set("currentReviewId", reviewId);
|
|
23487
24774
|
c.set("repoRoot", repoRoot);
|
|
23488
24775
|
await next();
|
|
23489
24776
|
});
|
|
23490
|
-
const selfDir =
|
|
23491
|
-
const distDir =
|
|
24777
|
+
const selfDir = dirname4(fileURLToPath2(import.meta.url));
|
|
24778
|
+
const distDir = existsSync12(join15(selfDir, "client", "styles.css")) ? join15(selfDir, "client") : join15(selfDir, "..", "dist", "client");
|
|
23492
24779
|
app.get("/static/styles.css", (c) => {
|
|
23493
|
-
const css =
|
|
24780
|
+
const css = readFileSync15(join15(distDir, "styles.css"), "utf-8");
|
|
23494
24781
|
return c.text(css, 200, { "Content-Type": "text/css", "Cache-Control": "no-cache" });
|
|
23495
24782
|
});
|
|
23496
24783
|
app.get("/static/app.js", (c) => {
|
|
23497
|
-
const js =
|
|
24784
|
+
const js = readFileSync15(join15(distDir, "app.global.js"), "utf-8");
|
|
23498
24785
|
return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
|
|
23499
24786
|
});
|
|
23500
24787
|
app.get("/static/history.js", (c) => {
|
|
23501
|
-
const js =
|
|
24788
|
+
const js = readFileSync15(join15(distDir, "history.global.js"), "utf-8");
|
|
23502
24789
|
return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
|
|
23503
24790
|
});
|
|
23504
24791
|
app.get("/favicon.ico", (c) => c.body(null, 204));
|
|
@@ -23536,7 +24823,7 @@ async function startServer(port, reviewId, repoRoot, options) {
|
|
|
23536
24823
|
try {
|
|
23537
24824
|
const globalConfig2 = readGlobalConfig();
|
|
23538
24825
|
if (globalConfig2.channelEnabled === true) {
|
|
23539
|
-
const dataDir =
|
|
24826
|
+
const dataDir = join15(repoRoot, ".glassbox");
|
|
23540
24827
|
registerChannel(dataDir);
|
|
23541
24828
|
}
|
|
23542
24829
|
} catch {
|
|
@@ -23551,8 +24838,8 @@ async function startServer(port, reviewId, repoRoot, options) {
|
|
|
23551
24838
|
}
|
|
23552
24839
|
|
|
23553
24840
|
// src/skills.ts
|
|
23554
|
-
import { existsSync as
|
|
23555
|
-
import { join as
|
|
24841
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync10 } from "fs";
|
|
24842
|
+
import { join as join16 } from "path";
|
|
23556
24843
|
var SKILL_VERSION = 1;
|
|
23557
24844
|
function versionHeader() {
|
|
23558
24845
|
return `<!-- glassbox-skill-version: ${SKILL_VERSION} -->`;
|
|
@@ -23563,14 +24850,14 @@ function parseVersionHeader(content) {
|
|
|
23563
24850
|
return parseInt(match[1], 10);
|
|
23564
24851
|
}
|
|
23565
24852
|
function updateFile(path, content) {
|
|
23566
|
-
if (
|
|
23567
|
-
const existing =
|
|
24853
|
+
if (existsSync13(path)) {
|
|
24854
|
+
const existing = readFileSync16(path, "utf-8");
|
|
23568
24855
|
const version2 = parseVersionHeader(existing);
|
|
23569
24856
|
if (version2 !== null && version2 >= SKILL_VERSION) {
|
|
23570
24857
|
return false;
|
|
23571
24858
|
}
|
|
23572
24859
|
}
|
|
23573
|
-
|
|
24860
|
+
writeFileSync10(path, content, "utf-8");
|
|
23574
24861
|
return true;
|
|
23575
24862
|
}
|
|
23576
24863
|
function skillBody() {
|
|
@@ -23590,8 +24877,8 @@ function skillBody() {
|
|
|
23590
24877
|
].join("\n");
|
|
23591
24878
|
}
|
|
23592
24879
|
function ensureClaudeSkills(cwd) {
|
|
23593
|
-
const dir =
|
|
23594
|
-
|
|
24880
|
+
const dir = join16(cwd, ".claude", "skills", "glassbox");
|
|
24881
|
+
mkdirSync10(dir, { recursive: true });
|
|
23595
24882
|
const content = [
|
|
23596
24883
|
"---",
|
|
23597
24884
|
"name: glassbox",
|
|
@@ -23603,11 +24890,11 @@ function ensureClaudeSkills(cwd) {
|
|
|
23603
24890
|
skillBody(),
|
|
23604
24891
|
""
|
|
23605
24892
|
].join("\n");
|
|
23606
|
-
return updateFile(
|
|
24893
|
+
return updateFile(join16(dir, "SKILL.md"), content);
|
|
23607
24894
|
}
|
|
23608
24895
|
function ensureCursorRules(cwd) {
|
|
23609
|
-
const rulesDir =
|
|
23610
|
-
|
|
24896
|
+
const rulesDir = join16(cwd, ".cursor", "rules");
|
|
24897
|
+
mkdirSync10(rulesDir, { recursive: true });
|
|
23611
24898
|
const content = [
|
|
23612
24899
|
"---",
|
|
23613
24900
|
"description: Read the latest Glassbox code review and apply all feedback annotations",
|
|
@@ -23618,11 +24905,11 @@ function ensureCursorRules(cwd) {
|
|
|
23618
24905
|
skillBody(),
|
|
23619
24906
|
""
|
|
23620
24907
|
].join("\n");
|
|
23621
|
-
return updateFile(
|
|
24908
|
+
return updateFile(join16(rulesDir, "glassbox.mdc"), content);
|
|
23622
24909
|
}
|
|
23623
24910
|
function ensureCopilotPrompts(cwd) {
|
|
23624
|
-
const promptsDir =
|
|
23625
|
-
|
|
24911
|
+
const promptsDir = join16(cwd, ".github", "prompts");
|
|
24912
|
+
mkdirSync10(promptsDir, { recursive: true });
|
|
23626
24913
|
const content = [
|
|
23627
24914
|
"---",
|
|
23628
24915
|
"description: Read the latest Glassbox code review and apply all feedback annotations",
|
|
@@ -23632,11 +24919,11 @@ function ensureCopilotPrompts(cwd) {
|
|
|
23632
24919
|
skillBody(),
|
|
23633
24920
|
""
|
|
23634
24921
|
].join("\n");
|
|
23635
|
-
return updateFile(
|
|
24922
|
+
return updateFile(join16(promptsDir, "glassbox.prompt.md"), content);
|
|
23636
24923
|
}
|
|
23637
24924
|
function ensureWindsurfRules(cwd) {
|
|
23638
|
-
const rulesDir =
|
|
23639
|
-
|
|
24925
|
+
const rulesDir = join16(cwd, ".windsurf", "rules");
|
|
24926
|
+
mkdirSync10(rulesDir, { recursive: true });
|
|
23640
24927
|
const content = [
|
|
23641
24928
|
"---",
|
|
23642
24929
|
"trigger: manual",
|
|
@@ -23647,21 +24934,21 @@ function ensureWindsurfRules(cwd) {
|
|
|
23647
24934
|
skillBody(),
|
|
23648
24935
|
""
|
|
23649
24936
|
].join("\n");
|
|
23650
|
-
return updateFile(
|
|
24937
|
+
return updateFile(join16(rulesDir, "glassbox.md"), content);
|
|
23651
24938
|
}
|
|
23652
24939
|
function ensureSkills() {
|
|
23653
24940
|
const cwd = process.cwd();
|
|
23654
24941
|
const platforms = [];
|
|
23655
|
-
if (
|
|
24942
|
+
if (existsSync13(join16(cwd, ".claude"))) {
|
|
23656
24943
|
if (ensureClaudeSkills(cwd)) platforms.push("Claude Code");
|
|
23657
24944
|
}
|
|
23658
|
-
if (
|
|
24945
|
+
if (existsSync13(join16(cwd, ".cursor"))) {
|
|
23659
24946
|
if (ensureCursorRules(cwd)) platforms.push("Cursor");
|
|
23660
24947
|
}
|
|
23661
|
-
if (
|
|
24948
|
+
if (existsSync13(join16(cwd, ".github", "prompts")) || existsSync13(join16(cwd, ".github", "copilot-instructions.md"))) {
|
|
23662
24949
|
if (ensureCopilotPrompts(cwd)) platforms.push("GitHub Copilot");
|
|
23663
24950
|
}
|
|
23664
|
-
if (
|
|
24951
|
+
if (existsSync13(join16(cwd, ".windsurf"))) {
|
|
23665
24952
|
if (ensureWindsurfRules(cwd)) platforms.push("Windsurf");
|
|
23666
24953
|
}
|
|
23667
24954
|
return platforms;
|
|
@@ -23669,36 +24956,36 @@ function ensureSkills() {
|
|
|
23669
24956
|
|
|
23670
24957
|
// src/update-check.ts
|
|
23671
24958
|
init_zod();
|
|
23672
|
-
import { existsSync as
|
|
24959
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync11, readFileSync as readFileSync17, writeFileSync as writeFileSync11 } from "fs";
|
|
23673
24960
|
import { get } from "https";
|
|
23674
24961
|
import { homedir as homedir3 } from "os";
|
|
23675
|
-
import { dirname as
|
|
24962
|
+
import { dirname as dirname5, join as join17 } from "path";
|
|
23676
24963
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
23677
|
-
var DATA_DIR =
|
|
23678
|
-
var CHECK_FILE =
|
|
24964
|
+
var DATA_DIR = join17(homedir3(), ".glassbox");
|
|
24965
|
+
var CHECK_FILE = join17(DATA_DIR, "last-update-check");
|
|
23679
24966
|
var PACKAGE_NAME = "glassbox";
|
|
23680
24967
|
var VersionPayloadSchema = external_exports.object({ version: external_exports.string() });
|
|
23681
24968
|
function getCurrentVersion() {
|
|
23682
24969
|
try {
|
|
23683
|
-
const dir =
|
|
23684
|
-
const
|
|
23685
|
-
return VersionPayloadSchema.parse(
|
|
24970
|
+
const dir = dirname5(fileURLToPath3(import.meta.url));
|
|
24971
|
+
const raw2 = JSON.parse(readFileSync17(join17(dir, "..", "package.json"), "utf-8"));
|
|
24972
|
+
return VersionPayloadSchema.parse(raw2).version;
|
|
23686
24973
|
} catch {
|
|
23687
24974
|
return "0.0.0";
|
|
23688
24975
|
}
|
|
23689
24976
|
}
|
|
23690
24977
|
function getLastCheckDate() {
|
|
23691
24978
|
try {
|
|
23692
|
-
if (
|
|
23693
|
-
return
|
|
24979
|
+
if (existsSync14(CHECK_FILE)) {
|
|
24980
|
+
return readFileSync17(CHECK_FILE, "utf-8").trim();
|
|
23694
24981
|
}
|
|
23695
24982
|
} catch {
|
|
23696
24983
|
}
|
|
23697
24984
|
return null;
|
|
23698
24985
|
}
|
|
23699
24986
|
function saveCheckDate() {
|
|
23700
|
-
|
|
23701
|
-
|
|
24987
|
+
mkdirSync11(DATA_DIR, { recursive: true });
|
|
24988
|
+
writeFileSync11(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
|
|
23702
24989
|
}
|
|
23703
24990
|
function isFirstUseToday() {
|
|
23704
24991
|
const last = getLastCheckDate();
|
|
@@ -23707,10 +24994,10 @@ function isFirstUseToday() {
|
|
|
23707
24994
|
return last !== today;
|
|
23708
24995
|
}
|
|
23709
24996
|
function fetchLatestVersion() {
|
|
23710
|
-
return new Promise((
|
|
24997
|
+
return new Promise((resolve11) => {
|
|
23711
24998
|
const req = get(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { timeout: 5e3 }, (res) => {
|
|
23712
24999
|
if (res.statusCode !== 200) {
|
|
23713
|
-
|
|
25000
|
+
resolve11(null);
|
|
23714
25001
|
return;
|
|
23715
25002
|
}
|
|
23716
25003
|
let data = "";
|
|
@@ -23719,19 +25006,19 @@ function fetchLatestVersion() {
|
|
|
23719
25006
|
});
|
|
23720
25007
|
res.on("end", () => {
|
|
23721
25008
|
try {
|
|
23722
|
-
const
|
|
23723
|
-
|
|
25009
|
+
const raw2 = JSON.parse(data);
|
|
25010
|
+
resolve11(VersionPayloadSchema.parse(raw2).version);
|
|
23724
25011
|
} catch {
|
|
23725
|
-
|
|
25012
|
+
resolve11(null);
|
|
23726
25013
|
}
|
|
23727
25014
|
});
|
|
23728
25015
|
});
|
|
23729
25016
|
req.on("error", () => {
|
|
23730
|
-
|
|
25017
|
+
resolve11(null);
|
|
23731
25018
|
});
|
|
23732
25019
|
req.on("timeout", () => {
|
|
23733
25020
|
req.destroy();
|
|
23734
|
-
|
|
25021
|
+
resolve11(null);
|
|
23735
25022
|
});
|
|
23736
25023
|
});
|
|
23737
25024
|
}
|
|
@@ -23881,14 +25168,14 @@ function parseArgs(argv) {
|
|
|
23881
25168
|
console.error("--diff requires two paths: --diff <pathA> <pathB>");
|
|
23882
25169
|
process.exit(1);
|
|
23883
25170
|
}
|
|
23884
|
-
mode = { type: "diff", pathA:
|
|
25171
|
+
mode = { type: "diff", pathA: resolve10(args[++i]), pathB: resolve10(args[++i]) };
|
|
23885
25172
|
break;
|
|
23886
25173
|
}
|
|
23887
25174
|
case "--port":
|
|
23888
25175
|
port = parseInt(args[++i], 10);
|
|
23889
25176
|
break;
|
|
23890
25177
|
case "--data-dir":
|
|
23891
|
-
dataDir =
|
|
25178
|
+
dataDir = resolve10(args[++i]);
|
|
23892
25179
|
break;
|
|
23893
25180
|
case "--resume":
|
|
23894
25181
|
resume = true;
|
|
@@ -23946,6 +25233,17 @@ function parseArgs(argv) {
|
|
|
23946
25233
|
return { mode, port, dataDir, resume, forceUpdateCheck, debug, aiServiceTest, demo, noOpen, strictPort, projectDir, difftoolAction, difftoolLocal, difftoolForce, difftoolServe };
|
|
23947
25234
|
}
|
|
23948
25235
|
async function main() {
|
|
25236
|
+
const rawArgs = process.argv.slice(2);
|
|
25237
|
+
if (rawArgs[0] === "note") {
|
|
25238
|
+
const { runNoteCli: runNoteCli2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
25239
|
+
try {
|
|
25240
|
+
await runNoteCli2(rawArgs.slice(1));
|
|
25241
|
+
process.exit(0);
|
|
25242
|
+
} catch (err) {
|
|
25243
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
25244
|
+
process.exit(1);
|
|
25245
|
+
}
|
|
25246
|
+
}
|
|
23949
25247
|
const parsed = parseArgs(process.argv);
|
|
23950
25248
|
if (!parsed) {
|
|
23951
25249
|
printUsage();
|
|
@@ -23987,19 +25285,19 @@ async function main() {
|
|
|
23987
25285
|
console.log("AI service test mode enabled \u2014 using mock AI responses");
|
|
23988
25286
|
}
|
|
23989
25287
|
if (debug) {
|
|
23990
|
-
console.log(`[debug] Build timestamp: ${"2026-06-
|
|
25288
|
+
console.log(`[debug] Build timestamp: ${"2026-06-19T07:12:20.944Z"}`);
|
|
23991
25289
|
}
|
|
23992
25290
|
if (projectDir !== null) {
|
|
23993
25291
|
process.chdir(projectDir);
|
|
23994
25292
|
}
|
|
23995
25293
|
if (dataDir === null) {
|
|
23996
|
-
dataDir =
|
|
25294
|
+
dataDir = join19(process.cwd(), ".glassbox");
|
|
23997
25295
|
}
|
|
23998
25296
|
if (difftoolServe) {
|
|
23999
25297
|
const { initDifftoolSession: initDifftoolSession2 } = await Promise.resolve().then(() => (init_session(), session_exports));
|
|
24000
25298
|
const { writeDiscovery: writeDiscovery2, clearDiscovery: clearDiscovery2, releaseStartingLock: releaseStartingLock2 } = await Promise.resolve().then(() => (init_difftool_discovery(), difftool_discovery_exports));
|
|
24001
25299
|
const { clearDifftoolBlobs: clearDifftoolBlobs2 } = await Promise.resolve().then(() => (init_blob_store(), blob_store_exports));
|
|
24002
|
-
|
|
25300
|
+
mkdirSync13(dataDir, { recursive: true });
|
|
24003
25301
|
setDataDir(dataDir);
|
|
24004
25302
|
const sessionDataDir = dataDir;
|
|
24005
25303
|
clearDifftoolBlobs2(sessionDataDir);
|
|
@@ -24034,13 +25332,13 @@ async function main() {
|
|
|
24034
25332
|
}
|
|
24035
25333
|
process.exit(1);
|
|
24036
25334
|
}
|
|
24037
|
-
dataDir =
|
|
25335
|
+
dataDir = join19(tmpdir2(), `glassbox-demo-${demo}-${Date.now()}`);
|
|
24038
25336
|
setDemoMode(demo);
|
|
24039
25337
|
console.log(`
|
|
24040
25338
|
DEMO MODE: ${scenario.label}
|
|
24041
25339
|
`);
|
|
24042
25340
|
}
|
|
24043
|
-
|
|
25341
|
+
mkdirSync13(dataDir, { recursive: true });
|
|
24044
25342
|
if (demo === null) {
|
|
24045
25343
|
acquireLock(dataDir);
|
|
24046
25344
|
}
|
|
@@ -24062,12 +25360,12 @@ async function main() {
|
|
|
24062
25360
|
if (mode.type === "diff") {
|
|
24063
25361
|
const { pathA, pathB } = mode;
|
|
24064
25362
|
for (const p of [pathA, pathB]) {
|
|
24065
|
-
if (!
|
|
25363
|
+
if (!existsSync16(p)) {
|
|
24066
25364
|
console.error(`Error: path does not exist: ${p}`);
|
|
24067
25365
|
process.exit(1);
|
|
24068
25366
|
}
|
|
24069
25367
|
}
|
|
24070
|
-
if (
|
|
25368
|
+
if (statSync5(pathA).isDirectory() !== statSync5(pathB).isDirectory()) {
|
|
24071
25369
|
console.error("Error: --diff requires two files or two folders, not a mix of both.");
|
|
24072
25370
|
process.exit(1);
|
|
24073
25371
|
}
|