glassbox 0.14.0 → 0.15.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1548 -314
- package/dist/client/app.global.js +19 -18
- 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;
|
|
@@ -16059,8 +16843,8 @@ var GLOBAL_CONFIG_PATH = join2(GLOBAL_CONFIG_DIR, "config.json");
|
|
|
16059
16843
|
function readGlobalConfig() {
|
|
16060
16844
|
try {
|
|
16061
16845
|
if (existsSync(GLOBAL_CONFIG_PATH)) {
|
|
16062
|
-
const
|
|
16063
|
-
const parsed = GlobalConfigSchema.safeParse(
|
|
16846
|
+
const raw2 = JSON.parse(readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
|
|
16847
|
+
const parsed = GlobalConfigSchema.safeParse(raw2);
|
|
16064
16848
|
if (parsed.success) return parsed.data;
|
|
16065
16849
|
}
|
|
16066
16850
|
} catch {
|
|
@@ -16187,12 +16971,16 @@ var AIModelSchema = external_exports.object({
|
|
|
16187
16971
|
contextWindow: external_exports.number(),
|
|
16188
16972
|
isDefault: external_exports.boolean()
|
|
16189
16973
|
});
|
|
16190
|
-
var AIPlatformSchema = external_exports.enum(["anthropic", "openai", "google"]);
|
|
16974
|
+
var AIPlatformSchema = external_exports.enum(["anthropic", "openai", "google", "local", "apple"]);
|
|
16975
|
+
var KEYLESS_PLATFORMS = /* @__PURE__ */ new Set(["local", "apple"]);
|
|
16191
16976
|
var PLATFORMS = {
|
|
16192
16977
|
anthropic: "Anthropic",
|
|
16193
16978
|
openai: "OpenAI",
|
|
16194
|
-
google: "Google"
|
|
16979
|
+
google: "Google",
|
|
16980
|
+
local: "Local",
|
|
16981
|
+
apple: "Apple"
|
|
16195
16982
|
};
|
|
16983
|
+
var APPLE_ON_DEVICE_MODEL_ID = "apple-on-device";
|
|
16196
16984
|
var MODELS = {
|
|
16197
16985
|
anthropic: [
|
|
16198
16986
|
{ id: "claude-opus-4-8", name: "Claude Opus 4.8", contextWindow: 1e6, isDefault: false },
|
|
@@ -16206,12 +16994,28 @@ var MODELS = {
|
|
|
16206
16994
|
google: [
|
|
16207
16995
|
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", contextWindow: 1e6, isDefault: true },
|
|
16208
16996
|
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", contextWindow: 1e6, isDefault: false }
|
|
16997
|
+
],
|
|
16998
|
+
// Local models are server-specific and discovered live from the configured
|
|
16999
|
+
// endpoint; this is only the fallback default when discovery is unavailable.
|
|
17000
|
+
local: [
|
|
17001
|
+
{ id: "llama3.1", name: "Llama 3.1", contextWindow: 8192, isDefault: true }
|
|
17002
|
+
],
|
|
17003
|
+
// On-device Apple Foundation Models — a single fixed entry (no discovery).
|
|
17004
|
+
// The on-device model has a small context window, so batch conservatively.
|
|
17005
|
+
apple: [
|
|
17006
|
+
{ id: APPLE_ON_DEVICE_MODEL_ID, name: "Apple On-Device", contextWindow: 4096, isDefault: true }
|
|
16209
17007
|
]
|
|
16210
17008
|
};
|
|
16211
17009
|
var ENV_KEY_NAMES = {
|
|
16212
17010
|
anthropic: "ANTHROPIC_API_KEY",
|
|
16213
17011
|
openai: "OPENAI_API_KEY",
|
|
16214
|
-
google: "GEMINI_API_KEY"
|
|
17012
|
+
google: "GEMINI_API_KEY",
|
|
17013
|
+
// Optional — most local servers (Ollama) need no key; some do.
|
|
17014
|
+
local: "GLASSBOX_LOCAL_API_KEY",
|
|
17015
|
+
// Apple Foundation Models are keyless (on-device). This name is never read
|
|
17016
|
+
// for auth — `apple` is in KEYLESS_PLATFORMS — but the record requires an
|
|
17017
|
+
// entry per platform.
|
|
17018
|
+
apple: "GLASSBOX_APPLE_API_KEY"
|
|
16215
17019
|
};
|
|
16216
17020
|
function getDefaultModel(platform) {
|
|
16217
17021
|
const models = MODELS[platform];
|
|
@@ -16220,7 +17024,10 @@ function getDefaultModel(platform) {
|
|
|
16220
17024
|
}
|
|
16221
17025
|
function getModelContextWindow(platform, modelId) {
|
|
16222
17026
|
const model = MODELS[platform].find((m) => m.id === modelId);
|
|
16223
|
-
|
|
17027
|
+
if (model) return model.contextWindow;
|
|
17028
|
+
if (platform === "local") return 8192;
|
|
17029
|
+
if (platform === "apple") return 4096;
|
|
17030
|
+
return 128e3;
|
|
16224
17031
|
}
|
|
16225
17032
|
function modelFamily(id) {
|
|
16226
17033
|
const lower = id.toLowerCase();
|
|
@@ -16273,8 +17080,8 @@ function saveAPIKey(platform, key, storage) {
|
|
|
16273
17080
|
if (storage === "keychain") {
|
|
16274
17081
|
saveKeyToKeychain(platform, key);
|
|
16275
17082
|
} else {
|
|
16276
|
-
updateGlobalConfig((
|
|
16277
|
-
const parsed = ConfigFileSchema.safeParse(
|
|
17083
|
+
updateGlobalConfig((raw2) => {
|
|
17084
|
+
const parsed = ConfigFileSchema.safeParse(raw2);
|
|
16278
17085
|
const cfg = parsed.success ? parsed.data : {};
|
|
16279
17086
|
cfg.ai ??= {};
|
|
16280
17087
|
cfg.ai.keys ??= {};
|
|
@@ -16298,8 +17105,8 @@ function deleteAPIKey(platform) {
|
|
|
16298
17105
|
} catch {
|
|
16299
17106
|
}
|
|
16300
17107
|
if (readConfigFile().ai?.keys === void 0) return;
|
|
16301
|
-
updateGlobalConfig((
|
|
16302
|
-
const parsed = ConfigFileSchema.safeParse(
|
|
17108
|
+
updateGlobalConfig((raw2) => {
|
|
17109
|
+
const parsed = ConfigFileSchema.safeParse(raw2);
|
|
16303
17110
|
const cfg = parsed.success ? parsed.data : {};
|
|
16304
17111
|
if (cfg.ai?.keys !== void 0) {
|
|
16305
17112
|
cfg.ai.keys[platform] = "";
|
|
@@ -16320,11 +17127,18 @@ function detectAvailablePlatforms() {
|
|
|
16320
17127
|
}
|
|
16321
17128
|
|
|
16322
17129
|
// src/ai/config.ts
|
|
17130
|
+
var DEFAULT_LOCAL_ENDPOINT = "http://localhost:11434/v1";
|
|
17131
|
+
function resolveLocalEndpoint() {
|
|
17132
|
+
const configured = readConfigFile().ai?.localEndpoint?.trim();
|
|
17133
|
+
const base = configured !== void 0 && configured !== "" ? configured : DEFAULT_LOCAL_ENDPOINT;
|
|
17134
|
+
return base.replace(/\/+$/, "");
|
|
17135
|
+
}
|
|
16323
17136
|
var ConfigFileSchema = external_exports.object({
|
|
16324
17137
|
ai: external_exports.object({
|
|
16325
17138
|
platform: external_exports.string().optional(),
|
|
16326
17139
|
model: external_exports.string().optional(),
|
|
16327
|
-
keys: external_exports.record(external_exports.string(), external_exports.string()).optional()
|
|
17140
|
+
keys: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
17141
|
+
localEndpoint: external_exports.string().optional()
|
|
16328
17142
|
}).optional(),
|
|
16329
17143
|
guidedReview: external_exports.object({
|
|
16330
17144
|
enabled: external_exports.boolean().optional(),
|
|
@@ -16332,25 +17146,31 @@ var ConfigFileSchema = external_exports.object({
|
|
|
16332
17146
|
}).optional()
|
|
16333
17147
|
}).loose();
|
|
16334
17148
|
function readConfigFile() {
|
|
16335
|
-
const
|
|
16336
|
-
const parsed = ConfigFileSchema.safeParse(
|
|
17149
|
+
const raw2 = readGlobalConfig();
|
|
17150
|
+
const parsed = ConfigFileSchema.safeParse(raw2);
|
|
16337
17151
|
return parsed.success ? parsed.data : {};
|
|
16338
17152
|
}
|
|
16339
17153
|
function loadAIConfig() {
|
|
16340
17154
|
const config2 = readConfigFile();
|
|
16341
17155
|
const platformRaw = config2.ai?.platform ?? "anthropic";
|
|
16342
17156
|
const platform = AIPlatformSchema.safeParse(platformRaw).success ? AIPlatformSchema.parse(platformRaw) : "anthropic";
|
|
16343
|
-
const
|
|
17157
|
+
const rawModel = config2.ai?.model ?? getDefaultModel(platform);
|
|
17158
|
+
const model = KEYLESS_PLATFORMS.has(platform) ? rawModel : resolveModelId(platform, rawModel);
|
|
16344
17159
|
const { key, source } = resolveAPIKey(platform);
|
|
16345
|
-
|
|
17160
|
+
const baseUrl = platform === "local" ? resolveLocalEndpoint() : void 0;
|
|
17161
|
+
return { platform, model, apiKey: key, keySource: source, baseUrl };
|
|
16346
17162
|
}
|
|
16347
|
-
function saveAIConfigPreferences(platform, model) {
|
|
17163
|
+
function saveAIConfigPreferences(platform, model, opts = {}) {
|
|
16348
17164
|
updateGlobalConfig((config2) => {
|
|
16349
17165
|
const parsed = ConfigFileSchema.safeParse(config2);
|
|
16350
17166
|
const cfg = parsed.success ? parsed.data : {};
|
|
16351
17167
|
cfg.ai ??= {};
|
|
16352
17168
|
cfg.ai.platform = platform;
|
|
16353
17169
|
cfg.ai.model = model;
|
|
17170
|
+
if (opts.localEndpoint !== void 0) {
|
|
17171
|
+
const trimmed = opts.localEndpoint.trim();
|
|
17172
|
+
cfg.ai.localEndpoint = trimmed === "" ? void 0 : trimmed;
|
|
17173
|
+
}
|
|
16354
17174
|
return cfg;
|
|
16355
17175
|
});
|
|
16356
17176
|
}
|
|
@@ -16495,14 +17315,14 @@ async function getUserPreferences() {
|
|
|
16495
17315
|
last_image_mode: "metadata"
|
|
16496
17316
|
};
|
|
16497
17317
|
if (result.rows.length === 0) return defaults;
|
|
16498
|
-
const
|
|
17318
|
+
const raw2 = result.rows[0];
|
|
16499
17319
|
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
|
|
17320
|
+
sort_mode: typeof raw2.sort_mode === "string" ? raw2.sort_mode : defaults.sort_mode,
|
|
17321
|
+
risk_sort_dimension: typeof raw2.risk_sort_dimension === "string" ? raw2.risk_sort_dimension : defaults.risk_sort_dimension,
|
|
17322
|
+
show_risk_scores: typeof raw2.show_risk_scores === "boolean" ? raw2.show_risk_scores : defaults.show_risk_scores,
|
|
17323
|
+
ignore_whitespace: typeof raw2.ignore_whitespace === "boolean" ? raw2.ignore_whitespace : defaults.ignore_whitespace,
|
|
17324
|
+
svg_view_mode: typeof raw2.svg_view_mode === "string" ? raw2.svg_view_mode : defaults.svg_view_mode,
|
|
17325
|
+
last_image_mode: typeof raw2.last_image_mode === "string" ? raw2.last_image_mode : defaults.last_image_mode
|
|
16506
17326
|
};
|
|
16507
17327
|
return UserPreferencesSchema.parse(merged);
|
|
16508
17328
|
}
|
|
@@ -16526,6 +17346,17 @@ async function saveUserPreferences(prefs) {
|
|
|
16526
17346
|
|
|
16527
17347
|
// src/demo.ts
|
|
16528
17348
|
init_queries();
|
|
17349
|
+
function demoReviewNotes(filePath) {
|
|
17350
|
+
if (filePath !== "src/auth/session.ts") return [];
|
|
17351
|
+
return [
|
|
17352
|
+
{ guid: "demo-note-rationale", line: 14, side: "new", kind: "rationale", body: "`createSession` is **async** now because session state moved from an in-process `Map` to Redis; callers must `await` it.", confidence: 0.9, producer: "Claude Code" },
|
|
17353
|
+
{ guid: "demo-note-proof", line: 23, side: "new", kind: "proof", body: "The TTL is written atomically with the value via the EX option, so a session can never be stored without an expiry.", producer: "Claude Code", artifacts: [{ uri: ".pr-notes/artifacts/session-ttl.test.txt", content: "PASS session.test.ts\n \u2713 createSession writes value and TTL atomically (4 ms)\n \u2713 a session always has an expiry (2 ms)\n\nTests: 2 passed, 2 total" }] },
|
|
17354
|
+
{ guid: "demo-note-risk", line: 31, side: "new", kind: "risk", body: "expiresAt round-trips through JSON as a string and is re-wrapped in Date() \u2014 verify the comparison holds in your runtime.", confidence: 0.6, producer: "Claude Code", artifacts: [{ uri: "assets/demo-annotations.png", isImage: true }] },
|
|
17355
|
+
// Re-anchoring showcase (P3): authored against a 16-byte id, but the code
|
|
17356
|
+
// now uses 32 bytes, so the note no longer matches and renders as stale.
|
|
17357
|
+
{ guid: "demo-note-stale", line: 15, side: "new", kind: "assumption", body: "Assumed a 16-byte token id here \u2014 the implementation has since changed, so this note is out of date.", producer: "Claude Code", snippet: " const id = randomBytes(16).toString('hex');" }
|
|
17358
|
+
];
|
|
17359
|
+
}
|
|
16529
17360
|
var DEMO_SCENARIOS = [
|
|
16530
17361
|
{ id: 1, label: "Main UI with guided review notes" },
|
|
16531
17362
|
{ id: 2, label: "Risk mode with inline risk notes" },
|
|
@@ -16922,6 +17753,9 @@ var NARRATIVE_ORDER = [
|
|
|
16922
17753
|
{ path: "tests/auth.test.ts", position: 7, rationale: "Tests \u2014 read last to verify the changes work correctly.", notes: { overview: "Tests for the session management changes. Read these last to confirm the new async API works as expected.", lines: [] } }
|
|
16923
17754
|
];
|
|
16924
17755
|
var ANNOTATIONS = [
|
|
17756
|
+
// A reviewer reply to the line-31 risk review note (doc 20 threading) —
|
|
17757
|
+
// renders nested beneath that note.
|
|
17758
|
+
{ filePath: "src/auth/session.ts", line: 31, side: "new", category: "note", content: "Confirmed \u2014 the JSON value is an ISO string, so new Date() parses it and the comparison holds.", replyToNoteId: "demo-note-risk" },
|
|
16925
17759
|
{ filePath: "src/auth/session.ts", line: 23, side: "new", category: "bug", content: "Redis key should be sanitized \u2014 if a session ID contains a colon, it will conflict with the key namespace." },
|
|
16926
17760
|
{ filePath: "src/auth/session.ts", line: 30, side: "new", category: "fix", content: "Wrap JSON.parse in try/catch to handle corrupted Redis data gracefully instead of crashing." },
|
|
16927
17761
|
{ filePath: "src/auth/session.ts", line: 12, side: "new", category: "pattern-follow", content: "Good use of a named constant instead of a magic number. This makes the TTL self-documenting." },
|
|
@@ -17034,7 +17868,7 @@ async function setupAnnotations(fileIdMap) {
|
|
|
17034
17868
|
for (const ann of ANNOTATIONS) {
|
|
17035
17869
|
const fileId = fileIdMap.get(ann.filePath);
|
|
17036
17870
|
if (fileId !== void 0) {
|
|
17037
|
-
await addAnnotation(fileId, ann.line, ann.side, ann.category, ann.content);
|
|
17871
|
+
await addAnnotation(fileId, ann.line, ann.side, ann.category, ann.content, ann.replyToNoteId);
|
|
17038
17872
|
}
|
|
17039
17873
|
}
|
|
17040
17874
|
}
|
|
@@ -17120,11 +17954,11 @@ function emptyFileDiff(filePath = "") {
|
|
|
17120
17954
|
isBinary: false
|
|
17121
17955
|
};
|
|
17122
17956
|
}
|
|
17123
|
-
function parseDiffData(
|
|
17124
|
-
if (
|
|
17957
|
+
function parseDiffData(raw2) {
|
|
17958
|
+
if (raw2 === null || raw2 === void 0 || raw2 === "") return null;
|
|
17125
17959
|
let parsed;
|
|
17126
17960
|
try {
|
|
17127
|
-
parsed = JSON.parse(
|
|
17961
|
+
parsed = JSON.parse(raw2);
|
|
17128
17962
|
} catch {
|
|
17129
17963
|
return null;
|
|
17130
17964
|
}
|
|
@@ -17286,9 +18120,9 @@ function createNewFileDiff(filePath, repoRoot) {
|
|
|
17286
18120
|
isBinary: false
|
|
17287
18121
|
};
|
|
17288
18122
|
}
|
|
17289
|
-
function parseDiff(
|
|
18123
|
+
function parseDiff(raw2) {
|
|
17290
18124
|
const files = [];
|
|
17291
|
-
const fileChunks =
|
|
18125
|
+
const fileChunks = raw2.split(/^diff --git /m).filter(Boolean);
|
|
17292
18126
|
for (const chunk of fileChunks) {
|
|
17293
18127
|
const headerEnd = chunk.indexOf("@@");
|
|
17294
18128
|
const header = headerEnd === -1 ? chunk : chunk.slice(0, headerEnd);
|
|
@@ -17323,12 +18157,12 @@ function parseDiff(raw) {
|
|
|
17323
18157
|
}
|
|
17324
18158
|
return files;
|
|
17325
18159
|
}
|
|
17326
|
-
function parseHunks(
|
|
18160
|
+
function parseHunks(raw2) {
|
|
17327
18161
|
const hunks = [];
|
|
17328
18162
|
const hunkRegex = /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@(.*)/gm;
|
|
17329
18163
|
let match;
|
|
17330
18164
|
const hunkStarts = [];
|
|
17331
|
-
while ((match = hunkRegex.exec(
|
|
18165
|
+
while ((match = hunkRegex.exec(raw2)) !== null) {
|
|
17332
18166
|
const groups = match;
|
|
17333
18167
|
hunkStarts.push({
|
|
17334
18168
|
index: match.index + match[0].length,
|
|
@@ -17340,8 +18174,8 @@ function parseHunks(raw) {
|
|
|
17340
18174
|
}
|
|
17341
18175
|
for (let i = 0; i < hunkStarts.length; i++) {
|
|
17342
18176
|
const start = hunkStarts[i];
|
|
17343
|
-
const end = i + 1 < hunkStarts.length ?
|
|
17344
|
-
const body =
|
|
18177
|
+
const end = i + 1 < hunkStarts.length ? raw2.lastIndexOf("\n@@", hunkStarts[i + 1].index) : raw2.length;
|
|
18178
|
+
const body = raw2.slice(start.index, end);
|
|
17345
18179
|
const lines = [];
|
|
17346
18180
|
let oldNum = start.oldStart;
|
|
17347
18181
|
let newNum = start.newStart;
|
|
@@ -17494,8 +18328,8 @@ function acquireLock(dataDir) {
|
|
|
17494
18328
|
lockPath = join4(dataDir, "glassbox.lock");
|
|
17495
18329
|
if (existsSync3(lockPath)) {
|
|
17496
18330
|
try {
|
|
17497
|
-
const
|
|
17498
|
-
const contents = LockFileSchema.parse(
|
|
18331
|
+
const raw2 = JSON.parse(readFileSync3(lockPath, "utf-8"));
|
|
18332
|
+
const contents = LockFileSchema.parse(raw2);
|
|
17499
18333
|
const pid = contents.pid;
|
|
17500
18334
|
try {
|
|
17501
18335
|
process.kill(pid, 0);
|
|
@@ -17643,9 +18477,9 @@ async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
|
|
|
17643
18477
|
|
|
17644
18478
|
// src/server.ts
|
|
17645
18479
|
import { serve } from "@hono/node-server";
|
|
17646
|
-
import { existsSync as
|
|
17647
|
-
import { Hono as
|
|
17648
|
-
import { dirname as
|
|
18480
|
+
import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
|
|
18481
|
+
import { Hono as Hono19 } from "hono";
|
|
18482
|
+
import { dirname as dirname4, join as join15 } from "path";
|
|
17649
18483
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
17650
18484
|
|
|
17651
18485
|
// src/channel-config.ts
|
|
@@ -17680,8 +18514,8 @@ function registerChannel(dataDir) {
|
|
|
17680
18514
|
let config2 = {};
|
|
17681
18515
|
if (existsSync4(mcpPath)) {
|
|
17682
18516
|
try {
|
|
17683
|
-
const
|
|
17684
|
-
const parsed = McpConfigSchema.safeParse(
|
|
18517
|
+
const raw2 = JSON.parse(readFileSync4(mcpPath, "utf-8"));
|
|
18518
|
+
const parsed = McpConfigSchema.safeParse(raw2);
|
|
17685
18519
|
if (parsed.success) config2 = parsed.data;
|
|
17686
18520
|
} catch {
|
|
17687
18521
|
}
|
|
@@ -17698,8 +18532,8 @@ function unregisterChannel(dataDir) {
|
|
|
17698
18532
|
const mcpPath = join5(root, ".mcp.json");
|
|
17699
18533
|
if (!existsSync4(mcpPath)) return;
|
|
17700
18534
|
try {
|
|
17701
|
-
const
|
|
17702
|
-
const parsed = McpConfigSchema.safeParse(
|
|
18535
|
+
const raw2 = JSON.parse(readFileSync4(mcpPath, "utf-8"));
|
|
18536
|
+
const parsed = McpConfigSchema.safeParse(raw2);
|
|
17703
18537
|
if (!parsed.success) return;
|
|
17704
18538
|
const config2 = parsed.data;
|
|
17705
18539
|
if (config2.mcpServers?.[MCP_SERVER_KEY] !== void 0) {
|
|
@@ -17725,8 +18559,8 @@ async function isChannelAlive(dataDir) {
|
|
|
17725
18559
|
if (port === null) return false;
|
|
17726
18560
|
try {
|
|
17727
18561
|
const res = await fetch(`http://127.0.0.1:${port}/health`);
|
|
17728
|
-
const
|
|
17729
|
-
const parsed = HealthResponseSchema.safeParse(
|
|
18562
|
+
const raw2 = await res.json();
|
|
18563
|
+
const parsed = HealthResponseSchema.safeParse(raw2);
|
|
17730
18564
|
return parsed.success && parsed.data.ok;
|
|
17731
18565
|
} catch {
|
|
17732
18566
|
return false;
|
|
@@ -17759,8 +18593,120 @@ init_zod();
|
|
|
17759
18593
|
// src/ai/shared.ts
|
|
17760
18594
|
init_zod();
|
|
17761
18595
|
|
|
18596
|
+
// src/review-notes/format.ts
|
|
18597
|
+
init_store();
|
|
18598
|
+
init_view();
|
|
18599
|
+
function notesByFile(repoRoot, filePaths) {
|
|
18600
|
+
const out = [];
|
|
18601
|
+
for (const file2 of filePaths) {
|
|
18602
|
+
const notes = loadReviewNotesForFile(repoRoot, file2);
|
|
18603
|
+
if (notes.length > 0) out.push({ file: file2, notes });
|
|
18604
|
+
}
|
|
18605
|
+
return out;
|
|
18606
|
+
}
|
|
18607
|
+
function reviewNotesPromptSection(repoRoot, filePaths) {
|
|
18608
|
+
const grouped = notesByFile(repoRoot, filePaths);
|
|
18609
|
+
if (grouped.length === 0) return "";
|
|
18610
|
+
const lines = [
|
|
18611
|
+
"=== Author review notes ===",
|
|
18612
|
+
"The author (an AI tool) left line-anchored notes explaining these changes. Use them to inform your analysis \u2014 weight their stated risks and assumptions.",
|
|
18613
|
+
""
|
|
18614
|
+
];
|
|
18615
|
+
for (const { file: file2, notes } of grouped) {
|
|
18616
|
+
lines.push(`${file2}:`);
|
|
18617
|
+
for (const n of notes) {
|
|
18618
|
+
lines.push(`- [${REVIEW_NOTE_LABELS[n.kind] ?? n.kind}, L${String(n.line)}] ${n.body}`);
|
|
18619
|
+
}
|
|
18620
|
+
lines.push("");
|
|
18621
|
+
}
|
|
18622
|
+
return lines.join("\n").trimEnd();
|
|
18623
|
+
}
|
|
18624
|
+
function reviewNotesExportSection(repoRoot, filePaths) {
|
|
18625
|
+
const grouped = notesByFile(repoRoot, filePaths);
|
|
18626
|
+
if (grouped.length === 0) return [];
|
|
18627
|
+
const lines = [
|
|
18628
|
+
"## AI Review Notes",
|
|
18629
|
+
"",
|
|
18630
|
+
"> Line-anchored notes the generating AI left explaining its changes (from `.pr-notes/`). Read these for the rationale and proof behind the code.",
|
|
18631
|
+
""
|
|
18632
|
+
];
|
|
18633
|
+
for (const { file: file2, notes } of grouped) {
|
|
18634
|
+
lines.push(`### ${file2}`);
|
|
18635
|
+
lines.push("");
|
|
18636
|
+
for (const n of notes) {
|
|
18637
|
+
const who = n.producer !== void 0 ? ` _(${n.producer})_` : "";
|
|
18638
|
+
lines.push(`- **Line ${String(n.line)}** [${n.kind}]: ${n.body}${who}`);
|
|
18639
|
+
}
|
|
18640
|
+
lines.push("");
|
|
18641
|
+
}
|
|
18642
|
+
return lines;
|
|
18643
|
+
}
|
|
18644
|
+
|
|
17762
18645
|
// src/ai/client.ts
|
|
17763
18646
|
init_zod();
|
|
18647
|
+
|
|
18648
|
+
// src/ai/apple-foundation.ts
|
|
18649
|
+
init_zod();
|
|
18650
|
+
import { spawn } from "child_process";
|
|
18651
|
+
import { existsSync as existsSync6 } from "fs";
|
|
18652
|
+
import { join as join7 } from "path";
|
|
18653
|
+
var defaultRunner = (bin, args, stdin) => new Promise((resolve11, reject) => {
|
|
18654
|
+
const child = spawn(bin, args, { stdio: ["pipe", "pipe", "ignore"] });
|
|
18655
|
+
let stdout = "";
|
|
18656
|
+
child.stdout.setEncoding("utf-8");
|
|
18657
|
+
child.stdout.on("data", (chunk) => {
|
|
18658
|
+
stdout += chunk;
|
|
18659
|
+
});
|
|
18660
|
+
child.on("error", reject);
|
|
18661
|
+
child.on("close", (code) => {
|
|
18662
|
+
resolve11({ stdout, code: code ?? 0 });
|
|
18663
|
+
});
|
|
18664
|
+
child.stdin.end(stdin);
|
|
18665
|
+
});
|
|
18666
|
+
var runner = defaultRunner;
|
|
18667
|
+
var isDarwin = process.platform === "darwin";
|
|
18668
|
+
var availabilityCache = null;
|
|
18669
|
+
function appleFmBinPath() {
|
|
18670
|
+
const env = process.env.GLASSBOX_APPLE_FM_BIN;
|
|
18671
|
+
if (env !== void 0 && env !== "" && existsSync6(env)) return env;
|
|
18672
|
+
const fallback = join7(process.cwd(), "apple-fm-helper");
|
|
18673
|
+
if (existsSync6(fallback)) return fallback;
|
|
18674
|
+
return null;
|
|
18675
|
+
}
|
|
18676
|
+
async function isAppleFoundationAvailable() {
|
|
18677
|
+
if (availabilityCache !== null) return availabilityCache;
|
|
18678
|
+
availabilityCache = await probeAvailability();
|
|
18679
|
+
return availabilityCache;
|
|
18680
|
+
}
|
|
18681
|
+
async function probeAvailability() {
|
|
18682
|
+
if (!isDarwin) return false;
|
|
18683
|
+
const bin = appleFmBinPath();
|
|
18684
|
+
if (bin === null) return false;
|
|
18685
|
+
try {
|
|
18686
|
+
const { stdout, code } = await runner(bin, ["--probe"], "");
|
|
18687
|
+
return code === 0 && stdout.trim().toLowerCase().startsWith("available");
|
|
18688
|
+
} catch {
|
|
18689
|
+
return false;
|
|
18690
|
+
}
|
|
18691
|
+
}
|
|
18692
|
+
var InferOutputSchema = external_exports.object({ content: external_exports.string() });
|
|
18693
|
+
async function runAppleFoundationInfer(system, messages) {
|
|
18694
|
+
const bin = appleFmBinPath();
|
|
18695
|
+
if (bin === null) throw new Error("Apple Foundation Models helper not found");
|
|
18696
|
+
const { stdout, code } = await runner(bin, ["--infer"], JSON.stringify({ system, messages }));
|
|
18697
|
+
if (code !== 0) throw new Error(`Apple Foundation Models helper exited with code ${String(code)}`);
|
|
18698
|
+
let raw2;
|
|
18699
|
+
try {
|
|
18700
|
+
raw2 = JSON.parse(stdout);
|
|
18701
|
+
} catch {
|
|
18702
|
+
throw new Error("Apple Foundation Models helper returned non-JSON output");
|
|
18703
|
+
}
|
|
18704
|
+
const parsed = InferOutputSchema.safeParse(raw2);
|
|
18705
|
+
if (!parsed.success) throw new Error("Apple Foundation Models helper returned an unexpected payload");
|
|
18706
|
+
return parsed.data.content;
|
|
18707
|
+
}
|
|
18708
|
+
|
|
18709
|
+
// src/ai/client.ts
|
|
17764
18710
|
var AnthropicResponseSchema = external_exports.object({
|
|
17765
18711
|
content: external_exports.array(external_exports.object({ type: external_exports.string(), text: external_exports.string().optional() }).loose()),
|
|
17766
18712
|
usage: external_exports.object({ input_tokens: external_exports.number(), output_tokens: external_exports.number() }).loose()
|
|
@@ -17771,6 +18717,12 @@ var OpenAIResponseSchema = external_exports.object({
|
|
|
17771
18717
|
}).loose()).min(1),
|
|
17772
18718
|
usage: external_exports.object({ prompt_tokens: external_exports.number(), completion_tokens: external_exports.number() }).loose()
|
|
17773
18719
|
}).loose();
|
|
18720
|
+
var LocalResponseSchema = external_exports.object({
|
|
18721
|
+
choices: external_exports.array(external_exports.object({
|
|
18722
|
+
message: external_exports.object({ content: external_exports.string() }).loose()
|
|
18723
|
+
}).loose()).min(1),
|
|
18724
|
+
usage: external_exports.object({ prompt_tokens: external_exports.number(), completion_tokens: external_exports.number() }).loose().optional()
|
|
18725
|
+
}).loose();
|
|
17774
18726
|
var GoogleResponseSchema = external_exports.object({
|
|
17775
18727
|
candidates: external_exports.array(external_exports.object({
|
|
17776
18728
|
content: external_exports.object({
|
|
@@ -17782,23 +18734,35 @@ var GoogleResponseSchema = external_exports.object({
|
|
|
17782
18734
|
candidatesTokenCount: external_exports.number()
|
|
17783
18735
|
}).loose().optional()
|
|
17784
18736
|
}).loose();
|
|
17785
|
-
|
|
18737
|
+
function requireKey(config2) {
|
|
17786
18738
|
if (config2.apiKey === null) {
|
|
17787
18739
|
throw new Error(`No API key configured for ${config2.platform}`);
|
|
17788
18740
|
}
|
|
18741
|
+
return config2.apiKey;
|
|
18742
|
+
}
|
|
18743
|
+
async function sendAIRequest(config2, systemPrompt, messages) {
|
|
18744
|
+
if (config2.apiKey === null && !KEYLESS_PLATFORMS.has(config2.platform)) {
|
|
18745
|
+
throw new Error(`No API key configured for ${config2.platform}`);
|
|
18746
|
+
}
|
|
17789
18747
|
const totalChars = messages.reduce((sum, m) => sum + m.content.length, 0) + systemPrompt.length;
|
|
17790
18748
|
debugLog(`AI request \u2192 ${config2.platform}/${config2.model} | ${String(messages.length)} message(s) | ~${String(Math.ceil(totalChars / 3))} estimated tokens`);
|
|
17791
18749
|
const start = Date.now();
|
|
17792
18750
|
let response;
|
|
17793
18751
|
switch (config2.platform) {
|
|
17794
18752
|
case "anthropic":
|
|
17795
|
-
response = await sendAnthropicRequest(config2
|
|
18753
|
+
response = await sendAnthropicRequest(requireKey(config2), config2.model, systemPrompt, messages);
|
|
17796
18754
|
break;
|
|
17797
18755
|
case "openai":
|
|
17798
|
-
response = await sendOpenAIRequest(config2
|
|
18756
|
+
response = await sendOpenAIRequest(requireKey(config2), config2.model, systemPrompt, messages);
|
|
17799
18757
|
break;
|
|
17800
18758
|
case "google":
|
|
17801
|
-
response = await sendGoogleRequest(config2
|
|
18759
|
+
response = await sendGoogleRequest(requireKey(config2), config2.model, systemPrompt, messages);
|
|
18760
|
+
break;
|
|
18761
|
+
case "local":
|
|
18762
|
+
response = await sendLocalRequest(config2.baseUrl ?? DEFAULT_LOCAL_ENDPOINT, config2.apiKey, config2.model, systemPrompt, messages);
|
|
18763
|
+
break;
|
|
18764
|
+
case "apple":
|
|
18765
|
+
response = await sendAppleRequest(systemPrompt, messages);
|
|
17802
18766
|
break;
|
|
17803
18767
|
}
|
|
17804
18768
|
const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
|
|
@@ -17827,8 +18791,8 @@ async function sendAnthropicRequest(apiKey, model, systemPrompt, messages) {
|
|
|
17827
18791
|
const errorText = await response.text();
|
|
17828
18792
|
throw new Error(`Anthropic API error (${String(response.status)}): ${errorText}`);
|
|
17829
18793
|
}
|
|
17830
|
-
const
|
|
17831
|
-
const data = AnthropicResponseSchema.parse(
|
|
18794
|
+
const raw2 = await response.json();
|
|
18795
|
+
const data = AnthropicResponseSchema.parse(raw2);
|
|
17832
18796
|
const text = data.content.filter((c) => c.type === "text").map((c) => c.text ?? "").join("");
|
|
17833
18797
|
return {
|
|
17834
18798
|
content: text,
|
|
@@ -17857,14 +18821,42 @@ async function sendOpenAIRequest(apiKey, model, systemPrompt, messages) {
|
|
|
17857
18821
|
const errorText = await response.text();
|
|
17858
18822
|
throw new Error(`OpenAI API error (${String(response.status)}): ${errorText}`);
|
|
17859
18823
|
}
|
|
17860
|
-
const
|
|
17861
|
-
const data = OpenAIResponseSchema.parse(
|
|
18824
|
+
const raw2 = await response.json();
|
|
18825
|
+
const data = OpenAIResponseSchema.parse(raw2);
|
|
17862
18826
|
return {
|
|
17863
18827
|
content: data.choices[0].message.content,
|
|
17864
18828
|
inputTokens: data.usage.prompt_tokens,
|
|
17865
18829
|
outputTokens: data.usage.completion_tokens
|
|
17866
18830
|
};
|
|
17867
18831
|
}
|
|
18832
|
+
async function sendLocalRequest(baseUrl, apiKey, model, systemPrompt, messages) {
|
|
18833
|
+
const oaiMessages = [
|
|
18834
|
+
{ role: "system", content: systemPrompt },
|
|
18835
|
+
...messages.map((m) => ({ role: m.role, content: m.content }))
|
|
18836
|
+
];
|
|
18837
|
+
const headers = { "Content-Type": "application/json" };
|
|
18838
|
+
if (apiKey !== null && apiKey !== "") headers.Authorization = `Bearer ${apiKey}`;
|
|
18839
|
+
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
18840
|
+
method: "POST",
|
|
18841
|
+
headers,
|
|
18842
|
+
body: JSON.stringify({ model, messages: oaiMessages, max_tokens: 8192, stream: false })
|
|
18843
|
+
});
|
|
18844
|
+
if (!response.ok) {
|
|
18845
|
+
const errorText = await response.text();
|
|
18846
|
+
throw new Error(`Local model error (${String(response.status)}): ${errorText}`);
|
|
18847
|
+
}
|
|
18848
|
+
const raw2 = await response.json();
|
|
18849
|
+
const data = LocalResponseSchema.parse(raw2);
|
|
18850
|
+
return {
|
|
18851
|
+
content: data.choices[0].message.content,
|
|
18852
|
+
inputTokens: data.usage?.prompt_tokens ?? 0,
|
|
18853
|
+
outputTokens: data.usage?.completion_tokens ?? 0
|
|
18854
|
+
};
|
|
18855
|
+
}
|
|
18856
|
+
async function sendAppleRequest(systemPrompt, messages) {
|
|
18857
|
+
const content = await runAppleFoundationInfer(systemPrompt, messages);
|
|
18858
|
+
return { content, inputTokens: 0, outputTokens: 0 };
|
|
18859
|
+
}
|
|
17868
18860
|
async function sendGoogleRequest(apiKey, model, systemPrompt, messages) {
|
|
17869
18861
|
const contents = messages.map((m) => ({
|
|
17870
18862
|
role: m.role === "assistant" ? "model" : "user",
|
|
@@ -17889,8 +18881,8 @@ async function sendGoogleRequest(apiKey, model, systemPrompt, messages) {
|
|
|
17889
18881
|
const errorText = await response.text();
|
|
17890
18882
|
throw new Error(`Google AI API error (${String(response.status)}): ${errorText}`);
|
|
17891
18883
|
}
|
|
17892
|
-
const
|
|
17893
|
-
const data = GoogleResponseSchema.parse(
|
|
18884
|
+
const raw2 = await response.json();
|
|
18885
|
+
const data = GoogleResponseSchema.parse(raw2);
|
|
17894
18886
|
const text = data.candidates[0].content.parts.map((p) => p.text).join("");
|
|
17895
18887
|
return {
|
|
17896
18888
|
content: text,
|
|
@@ -18018,10 +19010,12 @@ async function runAnalysisBatch(files, config2, repoRoot, options) {
|
|
|
18018
19010
|
const charBudget = Math.floor(contextWindow * 0.7 * 3);
|
|
18019
19011
|
const contexts = buildFileContexts(files, charBudget);
|
|
18020
19012
|
const validPaths = new Set(files.map((f) => f.file_path));
|
|
19013
|
+
const notesSection = reviewNotesPromptSection(repoRoot, files.map((f) => f.file_path));
|
|
18021
19014
|
const initialPrompt = [
|
|
18022
19015
|
options.initialPromptHeader(files.length),
|
|
18023
19016
|
"",
|
|
18024
|
-
formatContextsForPrompt(contexts)
|
|
19017
|
+
formatContextsForPrompt(contexts),
|
|
19018
|
+
...notesSection === "" ? [] : ["", notesSection]
|
|
18025
19019
|
].join("\n");
|
|
18026
19020
|
const messages = [{ role: "user", content: initialPrompt }];
|
|
18027
19021
|
for (let round = 0; round < 3; round++) {
|
|
@@ -18045,7 +19039,8 @@ ${formatAdditionalContext(fileContents)}`
|
|
|
18045
19039
|
});
|
|
18046
19040
|
continue;
|
|
18047
19041
|
}
|
|
18048
|
-
const
|
|
19042
|
+
const candidate = Array.isArray(parsed) ? parsed : [parsed];
|
|
19043
|
+
const arrayResult = external_exports.array(options.itemSchema).safeParse(candidate);
|
|
18049
19044
|
if (!arrayResult.success) {
|
|
18050
19045
|
const summary = arrayResult.error.issues.slice(0, 3).map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
18051
19046
|
throw new Error(`Expected an array of ${options.resultLabel} from AI \u2014 ${summary}`);
|
|
@@ -18486,8 +19481,8 @@ function isRetriable(err) {
|
|
|
18486
19481
|
return msg.includes("429") || msg.includes("500") || msg.includes("502") || msg.includes("503") || msg.includes("504") || msg.includes("rate_limit");
|
|
18487
19482
|
}
|
|
18488
19483
|
function sleep(ms) {
|
|
18489
|
-
return new Promise((
|
|
18490
|
-
setTimeout(
|
|
19484
|
+
return new Promise((resolve11) => {
|
|
19485
|
+
setTimeout(resolve11, ms);
|
|
18491
19486
|
});
|
|
18492
19487
|
}
|
|
18493
19488
|
|
|
@@ -18531,8 +19526,8 @@ function randomLines(count) {
|
|
|
18531
19526
|
return lines.sort((a, b) => a.line - b.line);
|
|
18532
19527
|
}
|
|
18533
19528
|
function sleep2(ms) {
|
|
18534
|
-
return new Promise((
|
|
18535
|
-
setTimeout(
|
|
19529
|
+
return new Promise((resolve11) => {
|
|
19530
|
+
setTimeout(resolve11, ms);
|
|
18536
19531
|
});
|
|
18537
19532
|
}
|
|
18538
19533
|
async function mockRiskAnalysisBatch(files) {
|
|
@@ -18699,17 +19694,25 @@ var AIConfigRespSchema = external_exports.object({
|
|
|
18699
19694
|
model: external_exports.string(),
|
|
18700
19695
|
keyConfigured: external_exports.boolean(),
|
|
18701
19696
|
keySource: KeySourceSchema,
|
|
19697
|
+
/** Base URL for the `local` (OpenAI-compatible) platform. */
|
|
19698
|
+
localEndpoint: external_exports.string(),
|
|
18702
19699
|
guidedReview: GuidedReviewConfigShapeSchema
|
|
18703
19700
|
});
|
|
18704
19701
|
var SaveAIConfigReqSchema = external_exports.object({
|
|
18705
19702
|
platform: AIPlatformSchema,
|
|
18706
19703
|
model: external_exports.string().min(1),
|
|
19704
|
+
localEndpoint: external_exports.string().optional(),
|
|
18707
19705
|
guidedReview: GuidedReviewConfigShapeSchema.optional()
|
|
18708
19706
|
});
|
|
18709
19707
|
var SaveAIConfigRespSchema = OkResponseSchema;
|
|
18710
19708
|
var ListAIModelsRespSchema = external_exports.object({
|
|
18711
19709
|
platforms: external_exports.record(AIPlatformSchema, external_exports.string()),
|
|
18712
|
-
models: external_exports.record(AIPlatformSchema, external_exports.array(AIModelSchema))
|
|
19710
|
+
models: external_exports.record(AIPlatformSchema, external_exports.array(AIModelSchema)),
|
|
19711
|
+
// Whether the on-device Apple Foundation Models helper is available right now
|
|
19712
|
+
// (macOS 26 + Apple Intelligence + bundled helper). The picker uses this to
|
|
19713
|
+
// show/hide the Apple platform; the `platforms`/`models` records always carry
|
|
19714
|
+
// every platform key (the record schema over the platform enum is exhaustive).
|
|
19715
|
+
appleAvailable: external_exports.boolean()
|
|
18713
19716
|
});
|
|
18714
19717
|
var AIKeyStatusEntrySchema = external_exports.object({
|
|
18715
19718
|
configured: external_exports.boolean(),
|
|
@@ -18892,7 +19895,9 @@ var CreateAnnotationReqSchema = external_exports.object({
|
|
|
18892
19895
|
lineNumber: external_exports.number().int().min(1),
|
|
18893
19896
|
side: AnnotationSideSchema,
|
|
18894
19897
|
category: AnnotationCategorySchema,
|
|
18895
|
-
content: external_exports.string().min(1)
|
|
19898
|
+
content: external_exports.string().min(1),
|
|
19899
|
+
/** SARIF guid of the AI review note this annotation replies to (doc 20 threading). */
|
|
19900
|
+
replyToNoteId: external_exports.string().optional()
|
|
18896
19901
|
});
|
|
18897
19902
|
var CreateAnnotationRespSchema = AnnotationSchema;
|
|
18898
19903
|
var UpdateAnnotationReqSchema = external_exports.object({
|
|
@@ -19954,6 +20959,19 @@ var DifftoolPollRespSchema = external_exports.object({
|
|
|
19954
20959
|
});
|
|
19955
20960
|
var DifftoolEndRespSchema = external_exports.object({ ok: external_exports.literal(true) });
|
|
19956
20961
|
|
|
20962
|
+
// src/api/review-notes.ts
|
|
20963
|
+
init_zod();
|
|
20964
|
+
var DiscardReviewNoteReqSchema = external_exports.object({
|
|
20965
|
+
guid: external_exports.string().min(1),
|
|
20966
|
+
/** Repo-relative source file the note is on (scopes the shard search). */
|
|
20967
|
+
file: external_exports.string().min(1)
|
|
20968
|
+
});
|
|
20969
|
+
var DiscardReviewNoteRespSchema = external_exports.object({
|
|
20970
|
+
ok: external_exports.boolean(),
|
|
20971
|
+
/** Whether a note was actually removed (false if it wasn't on disk, e.g. demo). */
|
|
20972
|
+
removed: external_exports.boolean()
|
|
20973
|
+
});
|
|
20974
|
+
|
|
19957
20975
|
// src/api/index.ts
|
|
19958
20976
|
var apis = {
|
|
19959
20977
|
...ai_exports,
|
|
@@ -19977,13 +20995,13 @@ init_schemas3();
|
|
|
19977
20995
|
|
|
19978
20996
|
// src/utils/parseBody.ts
|
|
19979
20997
|
async function parseBody(c, schema) {
|
|
19980
|
-
let
|
|
20998
|
+
let raw2;
|
|
19981
20999
|
try {
|
|
19982
|
-
|
|
21000
|
+
raw2 = await c.req.json();
|
|
19983
21001
|
} catch {
|
|
19984
21002
|
return { ok: false, response: c.json({ error: "Body must be valid JSON" }, 400) };
|
|
19985
21003
|
}
|
|
19986
|
-
const result = schema.safeParse(
|
|
21004
|
+
const result = schema.safeParse(raw2);
|
|
19987
21005
|
if (!result.success) {
|
|
19988
21006
|
const summary = result.error.issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; ");
|
|
19989
21007
|
return { ok: false, response: c.json({ error: summary }, 400) };
|
|
@@ -19991,8 +21009,8 @@ async function parseBody(c, schema) {
|
|
|
19991
21009
|
return { ok: true, data: result.data };
|
|
19992
21010
|
}
|
|
19993
21011
|
function parseQuery(c, schema) {
|
|
19994
|
-
const
|
|
19995
|
-
const result = schema.safeParse(
|
|
21012
|
+
const raw2 = c.req.query();
|
|
21013
|
+
const result = schema.safeParse(raw2);
|
|
19996
21014
|
if (!result.success) {
|
|
19997
21015
|
const summary = result.error.issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; ");
|
|
19998
21016
|
return { ok: false, response: c.json({ error: summary }, 400) };
|
|
@@ -20443,11 +21461,11 @@ function anthropicContextWindow(id) {
|
|
|
20443
21461
|
return id.toLowerCase().includes("haiku") ? 2e5 : 1e6;
|
|
20444
21462
|
}
|
|
20445
21463
|
async function fetchAnthropic(apiKey) {
|
|
20446
|
-
const
|
|
21464
|
+
const raw2 = await getJson("https://api.anthropic.com/v1/models?limit=1000", {
|
|
20447
21465
|
"x-api-key": apiKey,
|
|
20448
21466
|
"anthropic-version": "2023-06-01"
|
|
20449
21467
|
});
|
|
20450
|
-
const parsed = AnthropicListSchema.safeParse(
|
|
21468
|
+
const parsed = AnthropicListSchema.safeParse(raw2);
|
|
20451
21469
|
if (!parsed.success) return null;
|
|
20452
21470
|
return parsed.data.data.map((m) => ({
|
|
20453
21471
|
id: m.id,
|
|
@@ -20466,13 +21484,21 @@ function isOpenAIChatModel(id) {
|
|
|
20466
21484
|
return isChat && !isNonChat;
|
|
20467
21485
|
}
|
|
20468
21486
|
async function fetchOpenAI(apiKey) {
|
|
20469
|
-
const
|
|
21487
|
+
const raw2 = await getJson("https://api.openai.com/v1/models", {
|
|
20470
21488
|
Authorization: `Bearer ${apiKey}`
|
|
20471
21489
|
});
|
|
20472
|
-
const parsed = OpenAIListSchema.safeParse(
|
|
21490
|
+
const parsed = OpenAIListSchema.safeParse(raw2);
|
|
20473
21491
|
if (!parsed.success) return null;
|
|
20474
21492
|
return parsed.data.data.filter((m) => isOpenAIChatModel(m.id)).map((m) => ({ id: m.id, name: m.id, contextWindow: 128e3, isDefault: false }));
|
|
20475
21493
|
}
|
|
21494
|
+
async function fetchLocal(baseUrl, apiKey) {
|
|
21495
|
+
const headers = {};
|
|
21496
|
+
if (apiKey !== "") headers.Authorization = `Bearer ${apiKey}`;
|
|
21497
|
+
const raw2 = await getJson(`${baseUrl}/models`, headers);
|
|
21498
|
+
const parsed = OpenAIListSchema.safeParse(raw2);
|
|
21499
|
+
if (!parsed.success) return null;
|
|
21500
|
+
return parsed.data.data.map((m) => ({ id: m.id, name: m.id, contextWindow: 8192, isDefault: false }));
|
|
21501
|
+
}
|
|
20476
21502
|
var GoogleListSchema = external_exports.object({
|
|
20477
21503
|
models: external_exports.array(external_exports.object({
|
|
20478
21504
|
name: external_exports.string(),
|
|
@@ -20482,11 +21508,11 @@ var GoogleListSchema = external_exports.object({
|
|
|
20482
21508
|
}))
|
|
20483
21509
|
});
|
|
20484
21510
|
async function fetchGoogle(apiKey) {
|
|
20485
|
-
const
|
|
21511
|
+
const raw2 = await getJson(
|
|
20486
21512
|
`https://generativelanguage.googleapis.com/v1beta/models?pageSize=1000&key=${encodeURIComponent(apiKey)}`,
|
|
20487
21513
|
{}
|
|
20488
21514
|
);
|
|
20489
|
-
const parsed = GoogleListSchema.safeParse(
|
|
21515
|
+
const parsed = GoogleListSchema.safeParse(raw2);
|
|
20490
21516
|
if (!parsed.success) return null;
|
|
20491
21517
|
return parsed.data.models.filter((m) => (m.supportedGenerationMethods ?? []).includes("generateContent")).map((m) => ({
|
|
20492
21518
|
id: m.name.replace(/^models\//, ""),
|
|
@@ -20495,10 +21521,11 @@ async function fetchGoogle(apiKey) {
|
|
|
20495
21521
|
isDefault: false
|
|
20496
21522
|
}));
|
|
20497
21523
|
}
|
|
20498
|
-
async function fetchAvailableModels(platform, apiKey) {
|
|
21524
|
+
async function fetchAvailableModels(platform, apiKey, opts = {}) {
|
|
20499
21525
|
let models;
|
|
20500
21526
|
if (platform === "anthropic") models = await fetchAnthropic(apiKey);
|
|
20501
21527
|
else if (platform === "openai") models = await fetchOpenAI(apiKey);
|
|
21528
|
+
else if (platform === "local") models = await fetchLocal(opts.baseUrl ?? "", apiKey);
|
|
20502
21529
|
else models = await fetchGoogle(apiKey);
|
|
20503
21530
|
if (models === null || models.length === 0) return null;
|
|
20504
21531
|
const defaultId = getDefaultModel(platform);
|
|
@@ -20511,13 +21538,15 @@ async function fetchAvailableModels(platform, apiKey) {
|
|
|
20511
21538
|
|
|
20512
21539
|
// src/routes/ai-config.ts
|
|
20513
21540
|
var aiConfigRoutes = new Hono2();
|
|
20514
|
-
aiConfigRoutes.get("/config", (c) => {
|
|
21541
|
+
aiConfigRoutes.get("/config", async (c) => {
|
|
20515
21542
|
const config2 = loadAIConfig();
|
|
21543
|
+
const appleReady = config2.platform === "apple" && await isAppleFoundationAvailable();
|
|
20516
21544
|
return c.json({
|
|
20517
21545
|
platform: config2.platform,
|
|
20518
21546
|
model: config2.model,
|
|
20519
|
-
keyConfigured: config2.apiKey !== null || isAIServiceTest() || getDemoMode() !== null,
|
|
21547
|
+
keyConfigured: config2.apiKey !== null || config2.platform === "local" || appleReady || isAIServiceTest() || getDemoMode() !== null,
|
|
20520
21548
|
keySource: config2.keySource,
|
|
21549
|
+
localEndpoint: resolveLocalEndpoint(),
|
|
20521
21550
|
guidedReview: loadGuidedReviewConfig()
|
|
20522
21551
|
});
|
|
20523
21552
|
});
|
|
@@ -20525,7 +21554,7 @@ aiConfigRoutes.post("/config", async (c) => {
|
|
|
20525
21554
|
const parsed = await parseBody(c, SaveAIConfigReqSchema);
|
|
20526
21555
|
if (!parsed.ok) return parsed.response;
|
|
20527
21556
|
const body = parsed.data;
|
|
20528
|
-
saveAIConfigPreferences(body.platform, body.model);
|
|
21557
|
+
saveAIConfigPreferences(body.platform, body.model, { localEndpoint: body.localEndpoint });
|
|
20529
21558
|
if (body.guidedReview !== void 0) {
|
|
20530
21559
|
saveGuidedReviewConfig(body.guidedReview);
|
|
20531
21560
|
}
|
|
@@ -20533,7 +21562,13 @@ aiConfigRoutes.post("/config", async (c) => {
|
|
|
20533
21562
|
});
|
|
20534
21563
|
aiConfigRoutes.get("/models", async (c) => {
|
|
20535
21564
|
const platforms = ["anthropic", "openai", "google"];
|
|
20536
|
-
const models = {
|
|
21565
|
+
const models = {
|
|
21566
|
+
anthropic: MODELS.anthropic,
|
|
21567
|
+
openai: MODELS.openai,
|
|
21568
|
+
google: MODELS.google,
|
|
21569
|
+
local: MODELS.local,
|
|
21570
|
+
apple: MODELS.apple
|
|
21571
|
+
};
|
|
20537
21572
|
if (!isAIServiceTest() && getDemoMode() === null) {
|
|
20538
21573
|
await Promise.all(platforms.map(async (platform) => {
|
|
20539
21574
|
const { key } = resolveAPIKey(platform);
|
|
@@ -20541,11 +21576,15 @@ aiConfigRoutes.get("/models", async (c) => {
|
|
|
20541
21576
|
const live = await fetchAvailableModels(platform, key);
|
|
20542
21577
|
if (live !== null && live.length > 0) models[platform] = live;
|
|
20543
21578
|
}));
|
|
21579
|
+
const { key: localKey } = resolveAPIKey("local");
|
|
21580
|
+
const localLive = await fetchAvailableModels("local", localKey ?? "", { baseUrl: resolveLocalEndpoint() });
|
|
21581
|
+
if (localLive !== null && localLive.length > 0) models.local = localLive;
|
|
20544
21582
|
}
|
|
20545
|
-
|
|
21583
|
+
const appleAvailable = await isAppleFoundationAvailable();
|
|
21584
|
+
return c.json({ platforms: PLATFORMS, models, appleAvailable });
|
|
20546
21585
|
});
|
|
20547
21586
|
aiConfigRoutes.get("/key-status", (c) => {
|
|
20548
|
-
const platforms = ["anthropic", "openai", "google"];
|
|
21587
|
+
const platforms = ["anthropic", "openai", "google", "local", "apple"];
|
|
20549
21588
|
const status = {};
|
|
20550
21589
|
for (const platform of platforms) {
|
|
20551
21590
|
const { source } = resolveAPIKey(platform);
|
|
@@ -20569,7 +21608,7 @@ aiConfigRoutes.delete("/key", (c) => {
|
|
|
20569
21608
|
const platform = c.req.query("platform") ?? "anthropic";
|
|
20570
21609
|
const parsed = AIPlatformSchema.safeParse(platform);
|
|
20571
21610
|
if (!parsed.success) {
|
|
20572
|
-
return errorResponse(c, `platform must be one of:
|
|
21611
|
+
return errorResponse(c, `platform must be one of: ${Object.keys(PLATFORMS).join(", ")}`);
|
|
20573
21612
|
}
|
|
20574
21613
|
deleteAPIKey(parsed.data);
|
|
20575
21614
|
return c.json({ ok: true });
|
|
@@ -20581,7 +21620,7 @@ aiApiRoutes.route("/", aiConfigRoutes);
|
|
|
20581
21620
|
aiApiRoutes.route("/", aiAnalysisRoutes);
|
|
20582
21621
|
|
|
20583
21622
|
// src/routes/api.ts
|
|
20584
|
-
import { Hono as
|
|
21623
|
+
import { Hono as Hono14 } from "hono";
|
|
20585
21624
|
|
|
20586
21625
|
// src/routes/api/annotations.ts
|
|
20587
21626
|
import { Hono as Hono4 } from "hono";
|
|
@@ -20590,28 +21629,28 @@ init_queries();
|
|
|
20590
21629
|
// src/export/generate.ts
|
|
20591
21630
|
init_zod();
|
|
20592
21631
|
init_queries();
|
|
20593
|
-
import { spawnSync as
|
|
20594
|
-
import { appendFileSync, existsSync as
|
|
21632
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
21633
|
+
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
20595
21634
|
import { homedir as homedir2 } from "os";
|
|
20596
|
-
import { join as
|
|
20597
|
-
var DISMISS_FILE =
|
|
21635
|
+
import { join as join8 } from "path";
|
|
21636
|
+
var DISMISS_FILE = join8(homedir2(), ".glassbox", "gitignore-dismissed.json");
|
|
20598
21637
|
var DISMISS_DAYS = 30;
|
|
20599
21638
|
var DismissalsSchema = external_exports.record(external_exports.string(), external_exports.number());
|
|
20600
21639
|
function loadDismissals() {
|
|
20601
21640
|
try {
|
|
20602
|
-
const parsed = DismissalsSchema.safeParse(JSON.parse(
|
|
21641
|
+
const parsed = DismissalsSchema.safeParse(JSON.parse(readFileSync6(DISMISS_FILE, "utf-8")));
|
|
20603
21642
|
return parsed.success ? parsed.data : {};
|
|
20604
21643
|
} catch {
|
|
20605
21644
|
return {};
|
|
20606
21645
|
}
|
|
20607
21646
|
}
|
|
20608
21647
|
function saveDismissals(data) {
|
|
20609
|
-
const dir =
|
|
20610
|
-
|
|
20611
|
-
|
|
21648
|
+
const dir = join8(homedir2(), ".glassbox");
|
|
21649
|
+
mkdirSync5(dir, { recursive: true });
|
|
21650
|
+
writeFileSync6(DISMISS_FILE, JSON.stringify(data), "utf-8");
|
|
20612
21651
|
}
|
|
20613
21652
|
function isGlassboxGitignored(repoRoot) {
|
|
20614
|
-
const result =
|
|
21653
|
+
const result = spawnSync6("git", ["check-ignore", "-q", ".glassbox"], { cwd: repoRoot, stdio: "pipe" });
|
|
20615
21654
|
return result.status === 0;
|
|
20616
21655
|
}
|
|
20617
21656
|
function shouldPromptGitignore(repoRoot) {
|
|
@@ -20626,16 +21665,16 @@ function shouldPromptGitignore(repoRoot) {
|
|
|
20626
21665
|
return true;
|
|
20627
21666
|
}
|
|
20628
21667
|
function addGlassboxToGitignore(repoRoot) {
|
|
20629
|
-
const gitignorePath =
|
|
20630
|
-
if (
|
|
20631
|
-
const content =
|
|
21668
|
+
const gitignorePath = join8(repoRoot, ".gitignore");
|
|
21669
|
+
if (existsSync7(gitignorePath)) {
|
|
21670
|
+
const content = readFileSync6(gitignorePath, "utf-8");
|
|
20632
21671
|
if (!content.endsWith("\n")) {
|
|
20633
21672
|
appendFileSync(gitignorePath, "\n.glassbox/\n", "utf-8");
|
|
20634
21673
|
} else {
|
|
20635
21674
|
appendFileSync(gitignorePath, ".glassbox/\n", "utf-8");
|
|
20636
21675
|
}
|
|
20637
21676
|
} else {
|
|
20638
|
-
|
|
21677
|
+
writeFileSync6(gitignorePath, ".glassbox/\n", "utf-8");
|
|
20639
21678
|
}
|
|
20640
21679
|
}
|
|
20641
21680
|
function dismissGitignorePrompt2(repoRoot) {
|
|
@@ -20644,17 +21683,17 @@ function dismissGitignorePrompt2(repoRoot) {
|
|
|
20644
21683
|
saveDismissals(dismissals);
|
|
20645
21684
|
}
|
|
20646
21685
|
function deleteReviewExport(reviewId, repoRoot) {
|
|
20647
|
-
const exportDir =
|
|
20648
|
-
const archivePath =
|
|
20649
|
-
if (
|
|
21686
|
+
const exportDir = join8(repoRoot, ".glassbox");
|
|
21687
|
+
const archivePath = join8(exportDir, `review-${reviewId}.md`);
|
|
21688
|
+
if (existsSync7(archivePath)) unlinkSync2(archivePath);
|
|
20650
21689
|
}
|
|
20651
21690
|
async function generateReviewExport(reviewId, repoRoot, isCurrent) {
|
|
20652
21691
|
const review = await getReview(reviewId);
|
|
20653
21692
|
if (!review) throw new Error("Review not found");
|
|
20654
21693
|
const files = await getReviewFiles(reviewId);
|
|
20655
21694
|
const annotations = await getAnnotationsForReview(reviewId);
|
|
20656
|
-
const exportDir =
|
|
20657
|
-
|
|
21695
|
+
const exportDir = join8(repoRoot, ".glassbox");
|
|
21696
|
+
mkdirSync5(exportDir, { recursive: true });
|
|
20658
21697
|
const byFile = {};
|
|
20659
21698
|
for (const a of annotations) {
|
|
20660
21699
|
if (!(a.file_path in byFile)) byFile[a.file_path] = [];
|
|
@@ -20705,6 +21744,8 @@ async function generateReviewExport(reviewId, repoRoot, isCurrent) {
|
|
|
20705
21744
|
}
|
|
20706
21745
|
lines.push("");
|
|
20707
21746
|
}
|
|
21747
|
+
const reviewNoteLines = reviewNotesExportSection(repoRoot, files.map((f) => f.file_path));
|
|
21748
|
+
if (reviewNoteLines.length > 0) lines.push(...reviewNoteLines);
|
|
20708
21749
|
lines.push("---");
|
|
20709
21750
|
lines.push("");
|
|
20710
21751
|
lines.push("## Instructions for AI Tools");
|
|
@@ -20719,11 +21760,11 @@ async function generateReviewExport(reviewId, repoRoot, isCurrent) {
|
|
|
20719
21760
|
lines.push("6. **note** annotations are informational context. Consider them but they may not require code changes.");
|
|
20720
21761
|
lines.push("");
|
|
20721
21762
|
const content = lines.join("\n");
|
|
20722
|
-
const archivePath =
|
|
20723
|
-
|
|
21763
|
+
const archivePath = join8(exportDir, `review-${review.id}.md`);
|
|
21764
|
+
writeFileSync6(archivePath, content, "utf-8");
|
|
20724
21765
|
if (isCurrent) {
|
|
20725
|
-
const latestPath =
|
|
20726
|
-
|
|
21766
|
+
const latestPath = join8(exportDir, "latest-review.md");
|
|
21767
|
+
writeFileSync6(latestPath, content, "utf-8");
|
|
20727
21768
|
return latestPath;
|
|
20728
21769
|
}
|
|
20729
21770
|
return archivePath;
|
|
@@ -20754,7 +21795,8 @@ annotationsRoutes.post("/annotations", async (c) => {
|
|
|
20754
21795
|
body.lineNumber,
|
|
20755
21796
|
body.side,
|
|
20756
21797
|
body.category,
|
|
20757
|
-
body.content
|
|
21798
|
+
body.content,
|
|
21799
|
+
body.replyToNoteId
|
|
20758
21800
|
);
|
|
20759
21801
|
autoExport(c);
|
|
20760
21802
|
return c.json(annotation, 201);
|
|
@@ -20841,7 +21883,7 @@ import { resolve as resolve4 } from "path";
|
|
|
20841
21883
|
init_queries();
|
|
20842
21884
|
|
|
20843
21885
|
// src/utils/openOS.ts
|
|
20844
|
-
import { execFileSync, spawn } from "child_process";
|
|
21886
|
+
import { execFileSync, spawn as spawn2 } from "child_process";
|
|
20845
21887
|
import { resolve as resolve3 } from "path";
|
|
20846
21888
|
function openOS(target, mode) {
|
|
20847
21889
|
if (mode === "edit") {
|
|
@@ -20873,7 +21915,7 @@ function openOS(target, mode) {
|
|
|
20873
21915
|
}
|
|
20874
21916
|
}
|
|
20875
21917
|
function launchDetached(command, args) {
|
|
20876
|
-
const child =
|
|
21918
|
+
const child = spawn2(command, args, { detached: true, stdio: "ignore" });
|
|
20877
21919
|
child.on("error", (err) => {
|
|
20878
21920
|
debugLog(`launchDetached(${command}) failed: ${err.message}`);
|
|
20879
21921
|
});
|
|
@@ -20951,9 +21993,9 @@ init_blob_store();
|
|
|
20951
21993
|
import { Hono as Hono7 } from "hono";
|
|
20952
21994
|
|
|
20953
21995
|
// src/git/image.ts
|
|
20954
|
-
import { spawnSync as
|
|
20955
|
-
import { readFileSync as
|
|
20956
|
-
import { join as
|
|
21996
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
21997
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
21998
|
+
import { join as join10, resolve as resolve5 } from "path";
|
|
20957
21999
|
|
|
20958
22000
|
// src/git/image-metadata.ts
|
|
20959
22001
|
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]);
|
|
@@ -21221,20 +22263,20 @@ function getNewRef(mode) {
|
|
|
21221
22263
|
}
|
|
21222
22264
|
function gitShowFile(ref, filePath, repoRoot) {
|
|
21223
22265
|
const spec = ref === ":" ? `:${filePath}` : `${ref}:${filePath}`;
|
|
21224
|
-
const result =
|
|
22266
|
+
const result = spawnSync7("git", ["show", spec], { cwd: repoRoot, maxBuffer: 50 * 1024 * 1024, env: scrubbedGitEnv() });
|
|
21225
22267
|
if (result.status !== 0 || result.stdout.length === 0) return null;
|
|
21226
22268
|
return result.stdout;
|
|
21227
22269
|
}
|
|
21228
22270
|
function readWorkingFile(filePath, repoRoot) {
|
|
21229
22271
|
try {
|
|
21230
|
-
return
|
|
22272
|
+
return readFileSync8(resolve5(repoRoot, filePath));
|
|
21231
22273
|
} catch {
|
|
21232
22274
|
return null;
|
|
21233
22275
|
}
|
|
21234
22276
|
}
|
|
21235
22277
|
function readDiskImage(absPath) {
|
|
21236
22278
|
try {
|
|
21237
|
-
const data =
|
|
22279
|
+
const data = readFileSync8(absPath);
|
|
21238
22280
|
return { data, size: data.length };
|
|
21239
22281
|
} catch {
|
|
21240
22282
|
return null;
|
|
@@ -21242,7 +22284,7 @@ function readDiskImage(absPath) {
|
|
|
21242
22284
|
}
|
|
21243
22285
|
function getOldImage(mode, filePath, oldPath, repoRoot) {
|
|
21244
22286
|
if (mode.type === "diff") {
|
|
21245
|
-
return readDiskImage(
|
|
22287
|
+
return readDiskImage(join10(directComparisonRoots(mode).rootA, oldPath ?? filePath));
|
|
21246
22288
|
}
|
|
21247
22289
|
const ref = getOldRef(mode);
|
|
21248
22290
|
const path = oldPath ?? filePath;
|
|
@@ -21258,7 +22300,7 @@ function getOldImage(mode, filePath, oldPath, repoRoot) {
|
|
|
21258
22300
|
}
|
|
21259
22301
|
function getNewImage(mode, filePath, repoRoot) {
|
|
21260
22302
|
if (mode.type === "diff") {
|
|
21261
|
-
return readDiskImage(
|
|
22303
|
+
return readDiskImage(join10(directComparisonRoots(mode).rootB, filePath));
|
|
21262
22304
|
}
|
|
21263
22305
|
const ref = getNewRef(mode);
|
|
21264
22306
|
if (ref === null) {
|
|
@@ -21280,9 +22322,9 @@ function getNewImage(mode, filePath, repoRoot) {
|
|
|
21280
22322
|
import { Worker } from "worker_threads";
|
|
21281
22323
|
|
|
21282
22324
|
// src/git/svg-rasterize-render.ts
|
|
21283
|
-
import { existsSync as
|
|
22325
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
|
|
21284
22326
|
import { createRequire } from "module";
|
|
21285
|
-
import { join as
|
|
22327
|
+
import { join as join11 } from "path";
|
|
21286
22328
|
var initialized = false;
|
|
21287
22329
|
var ResvgClass;
|
|
21288
22330
|
var fontBuffers = [];
|
|
@@ -21291,7 +22333,7 @@ async function ensureRenderInit() {
|
|
|
21291
22333
|
const require2 = createRequire(import.meta.url);
|
|
21292
22334
|
const resvgPath = require2.resolve("@resvg/resvg-wasm");
|
|
21293
22335
|
const wasmPath = resvgPath.replace(/index\.(js|mjs)$/, "index_bg.wasm");
|
|
21294
|
-
const wasmBuffer =
|
|
22336
|
+
const wasmBuffer = readFileSync9(wasmPath);
|
|
21295
22337
|
const mod = await import("@resvg/resvg-wasm");
|
|
21296
22338
|
await mod.initWasm(wasmBuffer);
|
|
21297
22339
|
ResvgClass = mod.Resvg;
|
|
@@ -21302,9 +22344,9 @@ function loadSystemFonts() {
|
|
|
21302
22344
|
const buffers = [];
|
|
21303
22345
|
const candidates = getFontCandidates();
|
|
21304
22346
|
for (const path of candidates) {
|
|
21305
|
-
if (!
|
|
22347
|
+
if (!existsSync9(path)) continue;
|
|
21306
22348
|
try {
|
|
21307
|
-
buffers.push(
|
|
22349
|
+
buffers.push(readFileSync9(path));
|
|
21308
22350
|
} catch {
|
|
21309
22351
|
}
|
|
21310
22352
|
}
|
|
@@ -21317,24 +22359,24 @@ function getFontCandidates() {
|
|
|
21317
22359
|
const sup = "/System/Library/Fonts/Supplemental";
|
|
21318
22360
|
return [
|
|
21319
22361
|
// Core system fonts (serif, sans-serif, monospace)
|
|
21320
|
-
|
|
21321
|
-
|
|
21322
|
-
|
|
21323
|
-
|
|
21324
|
-
|
|
21325
|
-
|
|
21326
|
-
|
|
22362
|
+
join11(sys, "Helvetica.ttc"),
|
|
22363
|
+
join11(sys, "Times.ttc"),
|
|
22364
|
+
join11(sys, "Courier.ttc"),
|
|
22365
|
+
join11(sys, "Menlo.ttc"),
|
|
22366
|
+
join11(sys, "SFPro.ttf"),
|
|
22367
|
+
join11(sys, "SFNS.ttf"),
|
|
22368
|
+
join11(sys, "SFNSMono.ttf"),
|
|
21327
22369
|
// Supplemental (common named fonts in SVGs)
|
|
21328
|
-
|
|
21329
|
-
|
|
21330
|
-
|
|
21331
|
-
|
|
21332
|
-
|
|
21333
|
-
|
|
21334
|
-
|
|
21335
|
-
|
|
21336
|
-
|
|
21337
|
-
|
|
22370
|
+
join11(sup, "Arial.ttf"),
|
|
22371
|
+
join11(sup, "Arial Bold.ttf"),
|
|
22372
|
+
join11(sup, "Georgia.ttf"),
|
|
22373
|
+
join11(sup, "Verdana.ttf"),
|
|
22374
|
+
join11(sup, "Tahoma.ttf"),
|
|
22375
|
+
join11(sup, "Trebuchet MS.ttf"),
|
|
22376
|
+
join11(sup, "Impact.ttf"),
|
|
22377
|
+
join11(sup, "Comic Sans MS.ttf"),
|
|
22378
|
+
join11(sup, "Courier New.ttf"),
|
|
22379
|
+
join11(sup, "Times New Roman.ttf")
|
|
21338
22380
|
];
|
|
21339
22381
|
}
|
|
21340
22382
|
if (os === "linux") {
|
|
@@ -21353,17 +22395,17 @@ function getFontCandidates() {
|
|
|
21353
22395
|
];
|
|
21354
22396
|
}
|
|
21355
22397
|
if (os === "win32") {
|
|
21356
|
-
const winFonts =
|
|
22398
|
+
const winFonts = join11(process.env.WINDIR ?? "C:\\Windows", "Fonts");
|
|
21357
22399
|
return [
|
|
21358
|
-
|
|
21359
|
-
|
|
21360
|
-
|
|
21361
|
-
|
|
21362
|
-
|
|
21363
|
-
|
|
21364
|
-
|
|
21365
|
-
|
|
21366
|
-
|
|
22400
|
+
join11(winFonts, "arial.ttf"),
|
|
22401
|
+
join11(winFonts, "arialbd.ttf"),
|
|
22402
|
+
join11(winFonts, "times.ttf"),
|
|
22403
|
+
join11(winFonts, "cour.ttf"),
|
|
22404
|
+
join11(winFonts, "verdana.ttf"),
|
|
22405
|
+
join11(winFonts, "tahoma.ttf"),
|
|
22406
|
+
join11(winFonts, "georgia.ttf"),
|
|
22407
|
+
join11(winFonts, "consola.ttf"),
|
|
22408
|
+
join11(winFonts, "segoeui.ttf")
|
|
21367
22409
|
];
|
|
21368
22410
|
}
|
|
21369
22411
|
return [];
|
|
@@ -21512,8 +22554,8 @@ function submit(job) {
|
|
|
21512
22554
|
}
|
|
21513
22555
|
async function rasterizeSvg(svgData) {
|
|
21514
22556
|
const svg = svgData.toString("utf-8");
|
|
21515
|
-
return new Promise((
|
|
21516
|
-
submit({ svg, resolve:
|
|
22557
|
+
return new Promise((resolve11, reject) => {
|
|
22558
|
+
submit({ svg, resolve: resolve11, reject });
|
|
21517
22559
|
});
|
|
21518
22560
|
}
|
|
21519
22561
|
|
|
@@ -21581,8 +22623,8 @@ imageRoutes.get("/image/:fileId/:side", async (c) => {
|
|
|
21581
22623
|
|
|
21582
22624
|
// src/routes/api/outline.ts
|
|
21583
22625
|
init_queries();
|
|
21584
|
-
import { spawnSync as
|
|
21585
|
-
import { readFileSync as
|
|
22626
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
22627
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
21586
22628
|
import { Hono as Hono8 } from "hono";
|
|
21587
22629
|
import { resolve as resolve6 } from "path";
|
|
21588
22630
|
|
|
@@ -21948,7 +22990,7 @@ outlineRoutes.get("/symbol-definition", async (c) => {
|
|
|
21948
22990
|
}
|
|
21949
22991
|
if (definitions.length === 0) {
|
|
21950
22992
|
try {
|
|
21951
|
-
const allFiles =
|
|
22993
|
+
const allFiles = spawnSync8("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).stdout.trim().split("\n").filter(Boolean);
|
|
21952
22994
|
let scanned = 0;
|
|
21953
22995
|
for (const filePath of allFiles) {
|
|
21954
22996
|
if (searchedPaths.has(filePath)) continue;
|
|
@@ -21960,7 +23002,7 @@ outlineRoutes.get("/symbol-definition", async (c) => {
|
|
|
21960
23002
|
}
|
|
21961
23003
|
let content = "";
|
|
21962
23004
|
try {
|
|
21963
|
-
content =
|
|
23005
|
+
content = readFileSync10(resolve6(repoRoot, filePath), "utf-8");
|
|
21964
23006
|
} catch {
|
|
21965
23007
|
continue;
|
|
21966
23008
|
}
|
|
@@ -21996,16 +23038,16 @@ function collectDefinitions(symbols, targetName, fileId, filePath, out) {
|
|
|
21996
23038
|
}
|
|
21997
23039
|
|
|
21998
23040
|
// src/routes/api/project-settings.ts
|
|
21999
|
-
import { existsSync as
|
|
23041
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
|
|
22000
23042
|
import { Hono as Hono9 } from "hono";
|
|
22001
|
-
import { join as
|
|
23043
|
+
import { join as join12 } from "path";
|
|
22002
23044
|
var projectSettingsRoutes = new Hono9();
|
|
22003
23045
|
function readProjectSettings(repoRoot) {
|
|
22004
|
-
const settingsPath =
|
|
23046
|
+
const settingsPath = join12(repoRoot, ".glassbox", "settings.json");
|
|
22005
23047
|
try {
|
|
22006
|
-
if (
|
|
22007
|
-
const
|
|
22008
|
-
const parsed = ProjectSettingsSchema.safeParse(
|
|
23048
|
+
if (existsSync10(settingsPath)) {
|
|
23049
|
+
const raw2 = JSON.parse(readFileSync11(settingsPath, "utf-8"));
|
|
23050
|
+
const parsed = ProjectSettingsSchema.safeParse(raw2);
|
|
22009
23051
|
if (parsed.success) return parsed.data;
|
|
22010
23052
|
}
|
|
22011
23053
|
} catch {
|
|
@@ -22013,9 +23055,9 @@ function readProjectSettings(repoRoot) {
|
|
|
22013
23055
|
return {};
|
|
22014
23056
|
}
|
|
22015
23057
|
function writeProjectSettings(repoRoot, settings) {
|
|
22016
|
-
const dir =
|
|
22017
|
-
|
|
22018
|
-
|
|
23058
|
+
const dir = join12(repoRoot, ".glassbox");
|
|
23059
|
+
mkdirSync7(dir, { recursive: true });
|
|
23060
|
+
writeFileSync8(join12(dir, "settings.json"), JSON.stringify(settings, null, 2), "utf-8");
|
|
22019
23061
|
}
|
|
22020
23062
|
projectSettingsRoutes.get("/project-settings", (c) => {
|
|
22021
23063
|
const repoRoot = c.get("repoRoot");
|
|
@@ -22031,10 +23073,54 @@ projectSettingsRoutes.patch("/project-settings", async (c) => {
|
|
|
22031
23073
|
return c.json(current);
|
|
22032
23074
|
});
|
|
22033
23075
|
|
|
23076
|
+
// src/routes/api/review-notes.ts
|
|
23077
|
+
init_store();
|
|
23078
|
+
import { readFileSync as readFileSync12, statSync as statSync3 } from "fs";
|
|
23079
|
+
import { Hono as Hono10 } from "hono";
|
|
23080
|
+
import { extname, relative, resolve as resolve7 } from "path";
|
|
23081
|
+
var reviewNotesRoutes = new Hono10();
|
|
23082
|
+
var ARTIFACT_SERVE_MAX_BYTES = 1e7;
|
|
23083
|
+
var IMAGE_CONTENT_TYPES = {
|
|
23084
|
+
".png": "image/png",
|
|
23085
|
+
".webp": "image/webp",
|
|
23086
|
+
".avif": "image/avif",
|
|
23087
|
+
".gif": "image/gif",
|
|
23088
|
+
".jpg": "image/jpeg",
|
|
23089
|
+
".jpeg": "image/jpeg",
|
|
23090
|
+
".svg": "image/svg+xml"
|
|
23091
|
+
};
|
|
23092
|
+
reviewNotesRoutes.get("/review-notes/artifact", (c) => {
|
|
23093
|
+
const file2 = c.req.query("file");
|
|
23094
|
+
if (file2 === void 0 || file2 === "") return c.text("Missing file", 400);
|
|
23095
|
+
const repoRoot = c.get("repoRoot");
|
|
23096
|
+
const abs = resolve7(repoRoot, file2);
|
|
23097
|
+
const rel = relative(repoRoot, abs);
|
|
23098
|
+
if (rel === "" || rel.startsWith("..") || rel.startsWith("/")) return c.text("Forbidden", 403);
|
|
23099
|
+
const ext = extname(abs).toLowerCase();
|
|
23100
|
+
if (!(ext in IMAGE_CONTENT_TYPES)) return c.text("Unsupported artifact type", 415);
|
|
23101
|
+
const contentType = IMAGE_CONTENT_TYPES[ext];
|
|
23102
|
+
try {
|
|
23103
|
+
const stat = statSync3(abs);
|
|
23104
|
+
if (!stat.isFile() || stat.size > ARTIFACT_SERVE_MAX_BYTES) return c.text("Not found", 404);
|
|
23105
|
+
const body = readFileSync12(abs);
|
|
23106
|
+
return c.body(body, 200, { "Content-Type": contentType });
|
|
23107
|
+
} catch {
|
|
23108
|
+
return c.text("Not found", 404);
|
|
23109
|
+
}
|
|
23110
|
+
});
|
|
23111
|
+
reviewNotesRoutes.delete("/review-notes/:guid", (c) => {
|
|
23112
|
+
const guid3 = requirePathParam(c, "guid");
|
|
23113
|
+
if (!guid3.ok) return guid3.response;
|
|
23114
|
+
const repoRoot = c.get("repoRoot");
|
|
23115
|
+
const file2 = c.req.query("file");
|
|
23116
|
+
const removed = removeNote(repoRoot, guid3.data, file2);
|
|
23117
|
+
return c.json({ ok: true, removed });
|
|
23118
|
+
});
|
|
23119
|
+
|
|
22034
23120
|
// src/routes/api/reviews.ts
|
|
22035
23121
|
init_queries();
|
|
22036
|
-
import { Hono as
|
|
22037
|
-
var reviewsRoutes = new
|
|
23122
|
+
import { Hono as Hono11 } from "hono";
|
|
23123
|
+
var reviewsRoutes = new Hono11();
|
|
22038
23124
|
reviewsRoutes.get("/reviews", async (c) => {
|
|
22039
23125
|
const repoRoot = c.get("repoRoot");
|
|
22040
23126
|
const reviews = await listReviews(repoRoot);
|
|
@@ -22124,8 +23210,8 @@ reviewsRoutes.post("/reviews/delete-all", async (c) => {
|
|
|
22124
23210
|
|
|
22125
23211
|
// src/routes/api/share-prompt.ts
|
|
22126
23212
|
init_zod();
|
|
22127
|
-
import { Hono as
|
|
22128
|
-
var sharePromptRoutes = new
|
|
23213
|
+
import { Hono as Hono12 } from "hono";
|
|
23214
|
+
var sharePromptRoutes = new Hono12();
|
|
22129
23215
|
var SharePromptShapeSchema = external_exports.object({
|
|
22130
23216
|
dismissedAt: external_exports.number().nullable().optional(),
|
|
22131
23217
|
totalOpenMs: external_exports.number().optional()
|
|
@@ -22164,8 +23250,8 @@ sharePromptRoutes.post("/share-prompt/tick", async (c) => {
|
|
|
22164
23250
|
});
|
|
22165
23251
|
|
|
22166
23252
|
// src/routes/api/system.ts
|
|
22167
|
-
import { Hono as
|
|
22168
|
-
var systemRoutes = new
|
|
23253
|
+
import { Hono as Hono13 } from "hono";
|
|
23254
|
+
var systemRoutes = new Hono13();
|
|
22169
23255
|
systemRoutes.post("/open-external", async (c) => {
|
|
22170
23256
|
const parsed = await parseBody(c, OpenExternalReqSchema);
|
|
22171
23257
|
if (!parsed.ok) return parsed.response;
|
|
@@ -22177,7 +23263,7 @@ systemRoutes.post("/open-external", async (c) => {
|
|
|
22177
23263
|
});
|
|
22178
23264
|
|
|
22179
23265
|
// src/routes/api.ts
|
|
22180
|
-
var apiRoutes = new
|
|
23266
|
+
var apiRoutes = new Hono14();
|
|
22181
23267
|
apiRoutes.route("/", reviewsRoutes);
|
|
22182
23268
|
apiRoutes.route("/", filesRoutes);
|
|
22183
23269
|
apiRoutes.route("/", annotationsRoutes);
|
|
@@ -22185,20 +23271,21 @@ apiRoutes.route("/", outlineRoutes);
|
|
|
22185
23271
|
apiRoutes.route("/", contextRoutes);
|
|
22186
23272
|
apiRoutes.route("/", projectSettingsRoutes);
|
|
22187
23273
|
apiRoutes.route("/", imageRoutes);
|
|
23274
|
+
apiRoutes.route("/", reviewNotesRoutes);
|
|
22188
23275
|
apiRoutes.route("/", sharePromptRoutes);
|
|
22189
23276
|
apiRoutes.route("/", systemRoutes);
|
|
22190
23277
|
|
|
22191
23278
|
// src/routes/channel-api.ts
|
|
22192
|
-
import { spawnSync as
|
|
22193
|
-
import { mkdirSync as
|
|
22194
|
-
import { Hono as
|
|
22195
|
-
import { join as
|
|
22196
|
-
var channelApiRoutes = new
|
|
23279
|
+
import { spawnSync as spawnSync9 } from "child_process";
|
|
23280
|
+
import { mkdirSync as mkdirSync8 } from "fs";
|
|
23281
|
+
import { Hono as Hono15 } from "hono";
|
|
23282
|
+
import { join as join13 } from "path";
|
|
23283
|
+
var channelApiRoutes = new Hono15();
|
|
22197
23284
|
channelApiRoutes.get("/status", async (c) => {
|
|
22198
23285
|
const config2 = readGlobalConfig();
|
|
22199
23286
|
const enabled = config2.channelEnabled === true;
|
|
22200
23287
|
const repoRoot = c.get("repoRoot");
|
|
22201
|
-
const dataDir =
|
|
23288
|
+
const dataDir = join13(repoRoot, ".glassbox");
|
|
22202
23289
|
const connected = enabled ? await isChannelAlive(dataDir) : false;
|
|
22203
23290
|
return c.json({ enabled, connected });
|
|
22204
23291
|
});
|
|
@@ -22207,8 +23294,8 @@ channelApiRoutes.post("/enable", (c) => {
|
|
|
22207
23294
|
config2.channelEnabled = true;
|
|
22208
23295
|
});
|
|
22209
23296
|
const repoRoot = c.get("repoRoot");
|
|
22210
|
-
const dataDir =
|
|
22211
|
-
|
|
23297
|
+
const dataDir = join13(repoRoot, ".glassbox");
|
|
23298
|
+
mkdirSync8(dataDir, { recursive: true });
|
|
22212
23299
|
registerChannel(dataDir);
|
|
22213
23300
|
return c.json({ ok: true });
|
|
22214
23301
|
});
|
|
@@ -22217,7 +23304,7 @@ channelApiRoutes.post("/disable", (c) => {
|
|
|
22217
23304
|
config2.channelEnabled = false;
|
|
22218
23305
|
});
|
|
22219
23306
|
const repoRoot = c.get("repoRoot");
|
|
22220
|
-
const dataDir =
|
|
23307
|
+
const dataDir = join13(repoRoot, ".glassbox");
|
|
22221
23308
|
unregisterChannel(dataDir);
|
|
22222
23309
|
return c.json({ ok: true });
|
|
22223
23310
|
});
|
|
@@ -22225,7 +23312,7 @@ channelApiRoutes.post("/trigger", async (c) => {
|
|
|
22225
23312
|
const parsed = await parseBody(c, TriggerChannelReqSchema);
|
|
22226
23313
|
if (!parsed.ok) return parsed.response;
|
|
22227
23314
|
const repoRoot = c.get("repoRoot");
|
|
22228
|
-
const dataDir =
|
|
23315
|
+
const dataDir = join13(repoRoot, ".glassbox");
|
|
22229
23316
|
const sent = await triggerChannel(dataDir, parsed.data.message);
|
|
22230
23317
|
if (!sent) {
|
|
22231
23318
|
return c.json({ error: "Channel not connected" }, 503);
|
|
@@ -22234,7 +23321,7 @@ channelApiRoutes.post("/trigger", async (c) => {
|
|
|
22234
23321
|
});
|
|
22235
23322
|
channelApiRoutes.get("/claude-check", (c) => {
|
|
22236
23323
|
try {
|
|
22237
|
-
const result =
|
|
23324
|
+
const result = spawnSync9("claude", ["--version"], { encoding: "utf-8", timeout: 5e3 });
|
|
22238
23325
|
if (result.status !== 0) {
|
|
22239
23326
|
return c.json({ installed: false, version: null, meetsMinimum: false });
|
|
22240
23327
|
}
|
|
@@ -22253,13 +23340,13 @@ channelApiRoutes.get("/claude-check", (c) => {
|
|
|
22253
23340
|
});
|
|
22254
23341
|
|
|
22255
23342
|
// src/routes/difftool-api.ts
|
|
22256
|
-
import { Hono as
|
|
23343
|
+
import { Hono as Hono16 } from "hono";
|
|
22257
23344
|
init_connection();
|
|
22258
23345
|
init_queries();
|
|
22259
23346
|
init_blob_store();
|
|
22260
23347
|
init_session();
|
|
22261
23348
|
init_difftool();
|
|
22262
|
-
var difftoolApiRoutes = new
|
|
23349
|
+
var difftoolApiRoutes = new Hono16();
|
|
22263
23350
|
difftoolApiRoutes.get("/status", (c) => {
|
|
22264
23351
|
return c.json(getDifftoolStatus("global"));
|
|
22265
23352
|
});
|
|
@@ -22321,9 +23408,9 @@ difftoolApiRoutes.get("/hold", (c) => {
|
|
|
22321
23408
|
const session2 = getDifftoolSession();
|
|
22322
23409
|
if (session2 === null) return c.json({ ended: true });
|
|
22323
23410
|
noteDifftoolActivity();
|
|
22324
|
-
return new Promise((
|
|
23411
|
+
return new Promise((resolve11) => {
|
|
22325
23412
|
addDifftoolHold(() => {
|
|
22326
|
-
|
|
23413
|
+
resolve11(c.json({ ended: true }));
|
|
22327
23414
|
});
|
|
22328
23415
|
c.req.raw.signal.addEventListener("abort", () => {
|
|
22329
23416
|
endDifftoolSession();
|
|
@@ -22336,9 +23423,12 @@ difftoolApiRoutes.post("/end", (c) => {
|
|
|
22336
23423
|
});
|
|
22337
23424
|
|
|
22338
23425
|
// src/routes/pages.tsx
|
|
22339
|
-
import { readFileSync as
|
|
22340
|
-
import { Hono as
|
|
22341
|
-
import { resolve as
|
|
23426
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
23427
|
+
import { Hono as Hono17 } from "hono";
|
|
23428
|
+
import { resolve as resolve8 } from "path";
|
|
23429
|
+
|
|
23430
|
+
// src/components/diffView.tsx
|
|
23431
|
+
import { raw } from "kerfjs";
|
|
22342
23432
|
|
|
22343
23433
|
// src/icons.tsx
|
|
22344
23434
|
import { jsx, jsxs } from "kerfjs/jsx-runtime";
|
|
@@ -22400,6 +23490,9 @@ function IconActualSize() {
|
|
|
22400
23490
|
] });
|
|
22401
23491
|
}
|
|
22402
23492
|
|
|
23493
|
+
// src/components/diffView.tsx
|
|
23494
|
+
init_view();
|
|
23495
|
+
|
|
22403
23496
|
// src/utils/charDiff.ts
|
|
22404
23497
|
var MAX_LINE_LENGTH = 5e3;
|
|
22405
23498
|
function charDiff(oldStr, newStr) {
|
|
@@ -22471,6 +23564,26 @@ function truncateDiffLine(content) {
|
|
|
22471
23564
|
};
|
|
22472
23565
|
}
|
|
22473
23566
|
|
|
23567
|
+
// src/utils/noteMarkdown.ts
|
|
23568
|
+
function escapeHtml(text) {
|
|
23569
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
23570
|
+
}
|
|
23571
|
+
var SAFE_URL = /^(https?:\/\/|mailto:)/i;
|
|
23572
|
+
function renderInline(escaped) {
|
|
23573
|
+
let out = escaped.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
23574
|
+
out = out.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (match, text, url2) => {
|
|
23575
|
+
if (!SAFE_URL.test(url2)) return match;
|
|
23576
|
+
return `<a href="${url2}" target="_blank" rel="noopener noreferrer">${text}</a>`;
|
|
23577
|
+
});
|
|
23578
|
+
out = out.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
23579
|
+
out = out.replace(/(^|[^*])\*([^*\s][^*]*)\*/g, "$1<em>$2</em>");
|
|
23580
|
+
out = out.replace(/(^|[^_])_([^_\s][^_]*)_/g, "$1<em>$2</em>");
|
|
23581
|
+
return out;
|
|
23582
|
+
}
|
|
23583
|
+
function renderNoteMarkdown(text) {
|
|
23584
|
+
return escapeHtml(text).split("\n").map(renderInline).join("<br>");
|
|
23585
|
+
}
|
|
23586
|
+
|
|
22474
23587
|
// src/components/imageDiff.tsx
|
|
22475
23588
|
import { jsx as jsx2, jsxs as jsxs2 } from "kerfjs/jsx-runtime";
|
|
22476
23589
|
function ImageDiff({ file: file2, diff, fontWarning, baseWidth, baseHeight }) {
|
|
@@ -22521,12 +23634,23 @@ function ImageDiff({ file: file2, diff, fontWarning, baseWidth, baseHeight }) {
|
|
|
22521
23634
|
|
|
22522
23635
|
// src/components/diffView.tsx
|
|
22523
23636
|
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "kerfjs/jsx-runtime";
|
|
22524
|
-
function DiffView({ file: file2, diff, annotations, mode }) {
|
|
23637
|
+
function DiffView({ file: file2, diff, annotations, mode, reviewNotes = [] }) {
|
|
23638
|
+
const loadedNoteGuids = new Set(reviewNotes.map((n) => n.guid).filter((g) => g !== void 0));
|
|
23639
|
+
const repliesByNote = {};
|
|
22525
23640
|
const annotationsByLine = {};
|
|
22526
23641
|
for (const a of annotations) {
|
|
23642
|
+
if (a.reply_to_note_id !== null && loadedNoteGuids.has(a.reply_to_note_id)) {
|
|
23643
|
+
(repliesByNote[a.reply_to_note_id] ??= []).push(a);
|
|
23644
|
+
continue;
|
|
23645
|
+
}
|
|
22527
23646
|
const key = `${a.line_number}:${a.side}`;
|
|
22528
|
-
|
|
22529
|
-
|
|
23647
|
+
(annotationsByLine[key] ??= []).push(a);
|
|
23648
|
+
}
|
|
23649
|
+
const reviewNotesByLine = {};
|
|
23650
|
+
for (const n of reviewNotes) {
|
|
23651
|
+
const key = `${n.line}:${n.side}`;
|
|
23652
|
+
if (!(key in reviewNotesByLine)) reviewNotesByLine[key] = [];
|
|
23653
|
+
reviewNotesByLine[key].push(n);
|
|
22530
23654
|
}
|
|
22531
23655
|
return /* @__PURE__ */ jsxs3(
|
|
22532
23656
|
"div",
|
|
@@ -22543,7 +23667,7 @@ function DiffView({ file: file2, diff, annotations, mode }) {
|
|
|
22543
23667
|
] }),
|
|
22544
23668
|
/* @__PURE__ */ jsx3("div", { className: "diff-header-actions", children: /* @__PURE__ */ jsx3("span", { className: `file-status ${diff.status}`, children: diff.status }) })
|
|
22545
23669
|
] }),
|
|
22546
|
-
diff.isBinary && isImageFile(diff.filePath) ? /* @__PURE__ */ jsx3(ImageDiff, { file: file2, diff }) : diff.isBinary ? /* @__PURE__ */ jsx3("div", { className: "hunk-separator", children: "Binary file" }) : diff.status === "added" || diff.status === "deleted" || mode === "unified" ? /* @__PURE__ */ jsx3(UnifiedDiff, { hunks: diff.hunks, annotationsByLine }) : /* @__PURE__ */ jsx3(SplitDiff, { hunks: diff.hunks, annotationsByLine })
|
|
23670
|
+
diff.isBinary && isImageFile(diff.filePath) ? /* @__PURE__ */ jsx3(ImageDiff, { file: file2, diff }) : diff.isBinary ? /* @__PURE__ */ jsx3("div", { className: "hunk-separator", children: "Binary file" }) : diff.status === "added" || diff.status === "deleted" || mode === "unified" ? /* @__PURE__ */ jsx3(UnifiedDiff, { hunks: diff.hunks, annotationsByLine, reviewNotesByLine, repliesByNote }) : /* @__PURE__ */ jsx3(SplitDiff, { hunks: diff.hunks, annotationsByLine, reviewNotesByLine, repliesByNote })
|
|
22547
23671
|
]
|
|
22548
23672
|
}
|
|
22549
23673
|
);
|
|
@@ -22553,7 +23677,10 @@ function getAnnotations(pair, annotationsByLine) {
|
|
|
22553
23677
|
const rightAnns = pair.right ? annotationsByLine[`${pair.right.newNum}:new`] ?? [] : [];
|
|
22554
23678
|
return [...leftAnns, ...rightAnns];
|
|
22555
23679
|
}
|
|
22556
|
-
function
|
|
23680
|
+
function getReviewNotes(pair, reviewNotesByLine) {
|
|
23681
|
+
return pair.right ? reviewNotesByLine[`${pair.right.newNum}:new`] ?? [] : [];
|
|
23682
|
+
}
|
|
23683
|
+
function SplitDiff({ hunks, annotationsByLine, reviewNotesByLine, repliesByNote }) {
|
|
22557
23684
|
const lastHunk = hunks[hunks.length - 1];
|
|
22558
23685
|
const tailStart = lastHunk ? lastHunk.newStart + lastHunk.newCount : 1;
|
|
22559
23686
|
const items = [];
|
|
@@ -22565,8 +23692,9 @@ function SplitDiff({ hunks, annotationsByLine }) {
|
|
|
22565
23692
|
items.push({ kind: "separator", hunkIdx, hunk, gapStart, gapEnd });
|
|
22566
23693
|
for (const pair of pairLines(hunk.lines)) {
|
|
22567
23694
|
const anns = getAnnotations(pair, annotationsByLine);
|
|
22568
|
-
|
|
22569
|
-
|
|
23695
|
+
const notes = getReviewNotes(pair, reviewNotesByLine);
|
|
23696
|
+
if (anns.length > 0 || notes.length > 0) {
|
|
23697
|
+
items.push({ kind: "annotated", pair, annotations: anns, reviewNotes: notes });
|
|
22570
23698
|
} else {
|
|
22571
23699
|
items.push({ kind: "pair", pair });
|
|
22572
23700
|
}
|
|
@@ -22581,7 +23709,7 @@ function SplitDiff({ hunks, annotationsByLine }) {
|
|
|
22581
23709
|
groups.push({ type: "columns", items: run });
|
|
22582
23710
|
run = [];
|
|
22583
23711
|
}
|
|
22584
|
-
groups.push({ type: "annotated", pair: item.pair, annotations: item.annotations });
|
|
23712
|
+
groups.push({ type: "annotated", pair: item.pair, annotations: item.annotations, reviewNotes: item.reviewNotes });
|
|
22585
23713
|
} else {
|
|
22586
23714
|
run.push(item);
|
|
22587
23715
|
}
|
|
@@ -22617,7 +23745,8 @@ function SplitDiff({ hunks, annotationsByLine }) {
|
|
|
22617
23745
|
}
|
|
22618
23746
|
)
|
|
22619
23747
|
] }),
|
|
22620
|
-
/* @__PURE__ */ jsx3(AnnotationRows, { annotations: group.annotations })
|
|
23748
|
+
group.annotations.length > 0 ? /* @__PURE__ */ jsx3(AnnotationRows, { annotations: group.annotations }) : null,
|
|
23749
|
+
group.reviewNotes.length > 0 ? /* @__PURE__ */ jsx3(ReviewNoteRows, { notes: group.reviewNotes, repliesByNote }) : null
|
|
22621
23750
|
] });
|
|
22622
23751
|
}
|
|
22623
23752
|
return /* @__PURE__ */ jsxs3("div", { className: "split-columns", children: [
|
|
@@ -22783,7 +23912,7 @@ function buildUnifiedCharDiffs(lines) {
|
|
|
22783
23912
|
}
|
|
22784
23913
|
return result;
|
|
22785
23914
|
}
|
|
22786
|
-
function UnifiedDiff({ hunks, annotationsByLine }) {
|
|
23915
|
+
function UnifiedDiff({ hunks, annotationsByLine, reviewNotesByLine, repliesByNote }) {
|
|
22787
23916
|
const lastHunk = hunks[hunks.length - 1];
|
|
22788
23917
|
const tailStart = lastHunk ? lastHunk.newStart + lastHunk.newCount : 1;
|
|
22789
23918
|
return /* @__PURE__ */ jsxs3("div", { className: "diff-table-unified", children: [
|
|
@@ -22816,6 +23945,7 @@ function UnifiedDiff({ hunks, annotationsByLine }) {
|
|
|
22816
23945
|
const lineNum = line.type === "remove" ? line.oldNum : line.newNum;
|
|
22817
23946
|
const side = line.type === "remove" ? "old" : "new";
|
|
22818
23947
|
const anns = annotationsByLine[`${lineNum}:${side}`] ?? [];
|
|
23948
|
+
const notes = reviewNotesByLine[`${lineNum}:${side}`] ?? [];
|
|
22819
23949
|
const segments = charDiffs.get(line);
|
|
22820
23950
|
return /* @__PURE__ */ jsxs3("div", { children: [
|
|
22821
23951
|
/* @__PURE__ */ jsxs3(
|
|
@@ -22831,7 +23961,8 @@ function UnifiedDiff({ hunks, annotationsByLine }) {
|
|
|
22831
23961
|
]
|
|
22832
23962
|
}
|
|
22833
23963
|
),
|
|
22834
|
-
anns.length > 0 ? /* @__PURE__ */ jsx3(AnnotationRows, { annotations: anns }) : null
|
|
23964
|
+
anns.length > 0 ? /* @__PURE__ */ jsx3(AnnotationRows, { annotations: anns }) : null,
|
|
23965
|
+
notes.length > 0 ? /* @__PURE__ */ jsx3(ReviewNoteRows, { notes, repliesByNote }) : null
|
|
22835
23966
|
] });
|
|
22836
23967
|
})
|
|
22837
23968
|
] });
|
|
@@ -22839,8 +23970,61 @@ function UnifiedDiff({ hunks, annotationsByLine }) {
|
|
|
22839
23970
|
/* @__PURE__ */ jsx3("div", { className: "hunk-separator hunk-expander-tail", "data-start": tailStart, children: "\u2195 Show remaining lines" })
|
|
22840
23971
|
] });
|
|
22841
23972
|
}
|
|
22842
|
-
function
|
|
22843
|
-
return /* @__PURE__ */ jsx3(
|
|
23973
|
+
function ReviewNoteRows({ notes, repliesByNote }) {
|
|
23974
|
+
return /* @__PURE__ */ jsx3(Fragment, { children: notes.map((n) => {
|
|
23975
|
+
const replies = n.guid !== void 0 ? repliesByNote[n.guid] ?? [] : [];
|
|
23976
|
+
return /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
23977
|
+
/* @__PURE__ */ jsxs3(
|
|
23978
|
+
"div",
|
|
23979
|
+
{
|
|
23980
|
+
className: `ai-note-row ai-note-review${n.stale === true ? " ai-note-stale" : ""}`,
|
|
23981
|
+
"data-kind": n.kind,
|
|
23982
|
+
"data-note-id": n.guid,
|
|
23983
|
+
children: [
|
|
23984
|
+
/* @__PURE__ */ jsxs3("div", { className: "ai-note-item", children: [
|
|
23985
|
+
/* @__PURE__ */ jsx3("span", { className: `ai-note-label ai-note-label-${n.kind}`, children: REVIEW_NOTE_LABELS[n.kind] ?? n.kind }),
|
|
23986
|
+
n.stale === true ? /* @__PURE__ */ jsx3("span", { className: "ai-note-stale-tag", title: "The code this note referred to has changed", children: "outdated" }) : null,
|
|
23987
|
+
/* @__PURE__ */ jsx3("span", { className: "ai-note-text", children: raw(renderNoteMarkdown(n.body)) }),
|
|
23988
|
+
n.producer !== void 0 ? /* @__PURE__ */ jsx3("span", { className: "ai-note-producer", children: n.producer }) : null,
|
|
23989
|
+
n.guid !== void 0 ? /* @__PURE__ */ jsx3("button", { className: "ai-note-reply-btn", "data-line": String(n.line), children: "Reply" }) : null,
|
|
23990
|
+
n.stale === true && n.guid !== void 0 ? /* @__PURE__ */ jsxs3("span", { className: "ai-note-stale-actions", children: [
|
|
23991
|
+
/* @__PURE__ */ jsx3("button", { className: "ai-note-keep-btn", title: "Dismiss the outdated flag for now", children: "Keep" }),
|
|
23992
|
+
/* @__PURE__ */ jsx3("button", { className: "ai-note-discard-btn", title: "Remove this note from .pr-notes/", children: "Discard" })
|
|
23993
|
+
] }) : null
|
|
23994
|
+
] }),
|
|
23995
|
+
n.artifacts !== void 0 && n.artifacts.length > 0 ? /* @__PURE__ */ jsx3("div", { className: "ai-note-artifacts", children: n.artifacts.map((a) => a.content !== void 0 ? /* @__PURE__ */ jsxs3("details", { className: "ai-note-artifact", children: [
|
|
23996
|
+
/* @__PURE__ */ jsxs3("summary", { children: [
|
|
23997
|
+
"\u{1F4CE} ",
|
|
23998
|
+
a.uri
|
|
23999
|
+
] }),
|
|
24000
|
+
/* @__PURE__ */ jsx3("pre", { className: "ai-note-artifact-content", children: /* @__PURE__ */ jsx3("code", { children: a.content }) })
|
|
24001
|
+
] }) : a.isImage === true ? /* @__PURE__ */ jsxs3("details", { className: "ai-note-artifact", children: [
|
|
24002
|
+
/* @__PURE__ */ jsxs3("summary", { children: [
|
|
24003
|
+
"\u{1F4CE} ",
|
|
24004
|
+
a.uri
|
|
24005
|
+
] }),
|
|
24006
|
+
/* @__PURE__ */ jsx3(
|
|
24007
|
+
"img",
|
|
24008
|
+
{
|
|
24009
|
+
className: "ai-note-artifact-img",
|
|
24010
|
+
loading: "lazy",
|
|
24011
|
+
alt: a.uri,
|
|
24012
|
+
src: `/api/review-notes/artifact?file=${encodeURIComponent(a.uri)}`
|
|
24013
|
+
}
|
|
24014
|
+
)
|
|
24015
|
+
] }) : /* @__PURE__ */ jsxs3("div", { className: "ai-note-artifact ai-note-artifact-ref", children: [
|
|
24016
|
+
"\u{1F4CE} ",
|
|
24017
|
+
a.uri
|
|
24018
|
+
] })) }) : null
|
|
24019
|
+
]
|
|
24020
|
+
}
|
|
24021
|
+
),
|
|
24022
|
+
replies.length > 0 ? /* @__PURE__ */ jsx3("div", { className: "annotation-row ai-note-replies", children: replies.map((a) => /* @__PURE__ */ jsx3(AnnotationItem, { annotation: a })) }) : null
|
|
24023
|
+
] });
|
|
24024
|
+
}) });
|
|
24025
|
+
}
|
|
24026
|
+
function AnnotationItem({ annotation: a }) {
|
|
24027
|
+
return /* @__PURE__ */ jsxs3(
|
|
22844
24028
|
"div",
|
|
22845
24029
|
{
|
|
22846
24030
|
className: `annotation-item${a.is_stale ? " annotation-stale" : ""}`,
|
|
@@ -22850,6 +24034,7 @@ function AnnotationRows({ annotations }) {
|
|
|
22850
24034
|
children: [
|
|
22851
24035
|
/* @__PURE__ */ jsx3("span", { className: "annotation-drag-handle", draggable: true, title: "Drag to move", children: "\u283F" }),
|
|
22852
24036
|
/* @__PURE__ */ jsx3("span", { className: `annotation-category category-${a.category}`, "data-action": "reclassify", children: a.category }),
|
|
24037
|
+
a.reply_to_note_id !== null ? /* @__PURE__ */ jsx3("span", { className: "annotation-reply-tag", title: "Reply to an AI review note", children: "\u21B3 reply" }) : null,
|
|
22853
24038
|
/* @__PURE__ */ jsx3("span", { className: "annotation-text", children: a.content }),
|
|
22854
24039
|
/* @__PURE__ */ jsxs3("div", { className: "annotation-actions", children: [
|
|
22855
24040
|
a.is_stale ? /* @__PURE__ */ jsx3("button", { className: "btn btn-xs btn-keep", "data-action": "keep", children: "Keep" }) : null,
|
|
@@ -22858,13 +24043,16 @@ function AnnotationRows({ annotations }) {
|
|
|
22858
24043
|
] })
|
|
22859
24044
|
]
|
|
22860
24045
|
}
|
|
22861
|
-
)
|
|
24046
|
+
);
|
|
24047
|
+
}
|
|
24048
|
+
function AnnotationRows({ annotations }) {
|
|
24049
|
+
return /* @__PURE__ */ jsx3("div", { className: "annotation-row", children: annotations.map((a) => /* @__PURE__ */ jsx3(AnnotationItem, { annotation: a })) });
|
|
22862
24050
|
}
|
|
22863
24051
|
|
|
22864
24052
|
// src/themes/config.ts
|
|
22865
|
-
import { existsSync as
|
|
22866
|
-
import { join as
|
|
22867
|
-
var THEMES_DIR =
|
|
24053
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync9, readdirSync as readdirSync2, readFileSync as readFileSync13, unlinkSync as unlinkSync3, writeFileSync as writeFileSync9 } from "fs";
|
|
24054
|
+
import { join as join14 } from "path";
|
|
24055
|
+
var THEMES_DIR = join14(GLOBAL_CONFIG_DIR, "themes");
|
|
22868
24056
|
function getActiveThemeId() {
|
|
22869
24057
|
const config2 = readGlobalConfig();
|
|
22870
24058
|
const theme = config2.theme;
|
|
@@ -22878,13 +24066,13 @@ function setActiveThemeId(id) {
|
|
|
22878
24066
|
});
|
|
22879
24067
|
}
|
|
22880
24068
|
function loadCustomThemes() {
|
|
22881
|
-
if (!
|
|
24069
|
+
if (!existsSync11(THEMES_DIR)) return [];
|
|
22882
24070
|
const themes = [];
|
|
22883
24071
|
try {
|
|
22884
|
-
const files =
|
|
24072
|
+
const files = readdirSync2(THEMES_DIR).filter((f) => f.endsWith(".json"));
|
|
22885
24073
|
for (const file2 of files) {
|
|
22886
24074
|
try {
|
|
22887
|
-
const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(
|
|
24075
|
+
const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync13(join14(THEMES_DIR, file2), "utf-8")));
|
|
22888
24076
|
if (!parsed.success) continue;
|
|
22889
24077
|
const d = parsed.data;
|
|
22890
24078
|
themes.push({ id: d.id, name: d.name, colors: d.colors, builtIn: false, baseTheme: d.baseTheme ?? "" });
|
|
@@ -22896,21 +24084,21 @@ function loadCustomThemes() {
|
|
|
22896
24084
|
return themes;
|
|
22897
24085
|
}
|
|
22898
24086
|
function saveCustomTheme(theme) {
|
|
22899
|
-
|
|
22900
|
-
const filePath =
|
|
22901
|
-
|
|
24087
|
+
mkdirSync9(THEMES_DIR, { recursive: true });
|
|
24088
|
+
const filePath = join14(THEMES_DIR, `${theme.id}.json`);
|
|
24089
|
+
writeFileSync9(filePath, JSON.stringify(theme, null, 2), "utf-8");
|
|
22902
24090
|
}
|
|
22903
24091
|
function deleteCustomTheme(id) {
|
|
22904
|
-
const filePath =
|
|
22905
|
-
if (
|
|
22906
|
-
|
|
24092
|
+
const filePath = join14(THEMES_DIR, `${id}.json`);
|
|
24093
|
+
if (existsSync11(filePath)) {
|
|
24094
|
+
unlinkSync3(filePath);
|
|
22907
24095
|
}
|
|
22908
24096
|
}
|
|
22909
24097
|
function getCustomTheme(id) {
|
|
22910
|
-
const filePath =
|
|
22911
|
-
if (!
|
|
24098
|
+
const filePath = join14(THEMES_DIR, `${id}.json`);
|
|
24099
|
+
if (!existsSync11(filePath)) return void 0;
|
|
22912
24100
|
try {
|
|
22913
|
-
const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(
|
|
24101
|
+
const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync13(filePath, "utf-8")));
|
|
22914
24102
|
if (!parsed.success) return void 0;
|
|
22915
24103
|
const d = parsed.data;
|
|
22916
24104
|
return { id: d.id, name: d.name, colors: d.colors, builtIn: false, baseTheme: d.baseTheme ?? "" };
|
|
@@ -22972,8 +24160,8 @@ function formatReviewMode(mode, modeArgs) {
|
|
|
22972
24160
|
return `commit: ${shortenIfSha(argsFor(mode, "commit:", modeArgs))}`;
|
|
22973
24161
|
}
|
|
22974
24162
|
if (mode === "range" || mode.startsWith("range:")) {
|
|
22975
|
-
const
|
|
22976
|
-
const [from = "", to = ""] =
|
|
24163
|
+
const raw2 = argsFor(mode, "range:", modeArgs);
|
|
24164
|
+
const [from = "", to = ""] = raw2.split("..");
|
|
22977
24165
|
return `range: ${shortenIfSha(from)}..${shortenIfSha(to)}`;
|
|
22978
24166
|
}
|
|
22979
24167
|
if (mode === "branch" || mode.startsWith("branch:")) {
|
|
@@ -23209,8 +24397,41 @@ function ReviewShell({ reviewId, review, files, annotationCounts, staleCounts, f
|
|
|
23209
24397
|
|
|
23210
24398
|
// src/routes/pages.tsx
|
|
23211
24399
|
init_queries();
|
|
24400
|
+
|
|
24401
|
+
// src/review-notes/reanchor.ts
|
|
24402
|
+
var MATCH_RADIUS = 50;
|
|
24403
|
+
function reanchorReviewNotes(notes, diff) {
|
|
24404
|
+
const byLine = /* @__PURE__ */ new Map();
|
|
24405
|
+
for (const hunk of diff.hunks) {
|
|
24406
|
+
for (const line of hunk.lines) {
|
|
24407
|
+
if (line.newNum !== null) byLine.set(line.newNum, line.content);
|
|
24408
|
+
}
|
|
24409
|
+
}
|
|
24410
|
+
return notes.map((note) => {
|
|
24411
|
+
if (note.snippet === void 0 || note.snippet === "") return note;
|
|
24412
|
+
const anchor = note.snippet.split("\n")[0];
|
|
24413
|
+
const current = byLine.get(note.line);
|
|
24414
|
+
if (current === anchor) return note;
|
|
24415
|
+
let best = null;
|
|
24416
|
+
let bestDistance = Infinity;
|
|
24417
|
+
for (const [lineNum, content] of byLine) {
|
|
24418
|
+
if (content !== anchor) continue;
|
|
24419
|
+
const distance = Math.abs(lineNum - note.line);
|
|
24420
|
+
if (distance < bestDistance && distance <= MATCH_RADIUS) {
|
|
24421
|
+
bestDistance = distance;
|
|
24422
|
+
best = lineNum;
|
|
24423
|
+
}
|
|
24424
|
+
}
|
|
24425
|
+
if (best !== null) return { ...note, line: best, stale: false };
|
|
24426
|
+
if (current !== void 0) return { ...note, stale: true };
|
|
24427
|
+
return note;
|
|
24428
|
+
});
|
|
24429
|
+
}
|
|
24430
|
+
|
|
24431
|
+
// src/routes/pages.tsx
|
|
24432
|
+
init_store();
|
|
23212
24433
|
import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs8 } from "kerfjs/jsx-runtime";
|
|
23213
|
-
var pageRoutes = new
|
|
24434
|
+
var pageRoutes = new Hono17();
|
|
23214
24435
|
pageRoutes.get("/", async (c) => {
|
|
23215
24436
|
const reviewId = c.get("reviewId");
|
|
23216
24437
|
const review = await getReview(reviewId);
|
|
@@ -23289,7 +24510,9 @@ pageRoutes.get("/file/:fileId", async (c) => {
|
|
|
23289
24510
|
}
|
|
23290
24511
|
}
|
|
23291
24512
|
}
|
|
23292
|
-
const
|
|
24513
|
+
const rawNotes = getDemoMode() !== null ? demoReviewNotes(file2.file_path) : loadReviewNotesForFile(c.get("repoRoot"), file2.file_path);
|
|
24514
|
+
const reviewNotes = reanchorReviewNotes(rawNotes, finalDiff);
|
|
24515
|
+
const html = /* @__PURE__ */ jsx8(DiffView, { file: file2, diff: finalDiff, annotations, mode, reviewNotes });
|
|
23293
24516
|
return c.html(html.toString());
|
|
23294
24517
|
});
|
|
23295
24518
|
pageRoutes.get("/file-raw", (c) => {
|
|
@@ -23298,7 +24521,7 @@ pageRoutes.get("/file-raw", (c) => {
|
|
|
23298
24521
|
const repoRoot = c.get("repoRoot");
|
|
23299
24522
|
let content;
|
|
23300
24523
|
try {
|
|
23301
|
-
content =
|
|
24524
|
+
content = readFileSync14(resolve8(repoRoot, filePath), "utf-8");
|
|
23302
24525
|
} catch {
|
|
23303
24526
|
return c.text("File not found", 404);
|
|
23304
24527
|
}
|
|
@@ -23354,8 +24577,8 @@ pageRoutes.get("/history", async (c) => {
|
|
|
23354
24577
|
});
|
|
23355
24578
|
|
|
23356
24579
|
// src/routes/theme-api.ts
|
|
23357
|
-
import { Hono as
|
|
23358
|
-
var themeApiRoutes = new
|
|
24580
|
+
import { Hono as Hono18 } from "hono";
|
|
24581
|
+
var themeApiRoutes = new Hono18();
|
|
23359
24582
|
themeApiRoutes.get("/", (c) => {
|
|
23360
24583
|
const themes = getAllThemes();
|
|
23361
24584
|
const activeId = getActiveThemeId();
|
|
@@ -23469,10 +24692,10 @@ themeApiRoutes.delete("/:id", (c) => {
|
|
|
23469
24692
|
|
|
23470
24693
|
// src/server.ts
|
|
23471
24694
|
function tryServe(appFetch, port) {
|
|
23472
|
-
return new Promise((
|
|
24695
|
+
return new Promise((resolve11, reject) => {
|
|
23473
24696
|
const server = serve({ fetch: appFetch, port, hostname: "127.0.0.1" });
|
|
23474
24697
|
server.on("listening", () => {
|
|
23475
|
-
|
|
24698
|
+
resolve11({ port, server });
|
|
23476
24699
|
});
|
|
23477
24700
|
server.on("error", (err) => {
|
|
23478
24701
|
reject(err);
|
|
@@ -23480,25 +24703,25 @@ function tryServe(appFetch, port) {
|
|
|
23480
24703
|
});
|
|
23481
24704
|
}
|
|
23482
24705
|
async function startServer(port, reviewId, repoRoot, options) {
|
|
23483
|
-
const app = new
|
|
24706
|
+
const app = new Hono19();
|
|
23484
24707
|
app.use("*", async (c, next) => {
|
|
23485
24708
|
c.set("reviewId", reviewId);
|
|
23486
24709
|
c.set("currentReviewId", reviewId);
|
|
23487
24710
|
c.set("repoRoot", repoRoot);
|
|
23488
24711
|
await next();
|
|
23489
24712
|
});
|
|
23490
|
-
const selfDir =
|
|
23491
|
-
const distDir =
|
|
24713
|
+
const selfDir = dirname4(fileURLToPath2(import.meta.url));
|
|
24714
|
+
const distDir = existsSync12(join15(selfDir, "client", "styles.css")) ? join15(selfDir, "client") : join15(selfDir, "..", "dist", "client");
|
|
23492
24715
|
app.get("/static/styles.css", (c) => {
|
|
23493
|
-
const css =
|
|
24716
|
+
const css = readFileSync15(join15(distDir, "styles.css"), "utf-8");
|
|
23494
24717
|
return c.text(css, 200, { "Content-Type": "text/css", "Cache-Control": "no-cache" });
|
|
23495
24718
|
});
|
|
23496
24719
|
app.get("/static/app.js", (c) => {
|
|
23497
|
-
const js =
|
|
24720
|
+
const js = readFileSync15(join15(distDir, "app.global.js"), "utf-8");
|
|
23498
24721
|
return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
|
|
23499
24722
|
});
|
|
23500
24723
|
app.get("/static/history.js", (c) => {
|
|
23501
|
-
const js =
|
|
24724
|
+
const js = readFileSync15(join15(distDir, "history.global.js"), "utf-8");
|
|
23502
24725
|
return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
|
|
23503
24726
|
});
|
|
23504
24727
|
app.get("/favicon.ico", (c) => c.body(null, 204));
|
|
@@ -23536,7 +24759,7 @@ async function startServer(port, reviewId, repoRoot, options) {
|
|
|
23536
24759
|
try {
|
|
23537
24760
|
const globalConfig2 = readGlobalConfig();
|
|
23538
24761
|
if (globalConfig2.channelEnabled === true) {
|
|
23539
|
-
const dataDir =
|
|
24762
|
+
const dataDir = join15(repoRoot, ".glassbox");
|
|
23540
24763
|
registerChannel(dataDir);
|
|
23541
24764
|
}
|
|
23542
24765
|
} catch {
|
|
@@ -23551,8 +24774,8 @@ async function startServer(port, reviewId, repoRoot, options) {
|
|
|
23551
24774
|
}
|
|
23552
24775
|
|
|
23553
24776
|
// src/skills.ts
|
|
23554
|
-
import { existsSync as
|
|
23555
|
-
import { join as
|
|
24777
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync10 } from "fs";
|
|
24778
|
+
import { join as join16 } from "path";
|
|
23556
24779
|
var SKILL_VERSION = 1;
|
|
23557
24780
|
function versionHeader() {
|
|
23558
24781
|
return `<!-- glassbox-skill-version: ${SKILL_VERSION} -->`;
|
|
@@ -23563,14 +24786,14 @@ function parseVersionHeader(content) {
|
|
|
23563
24786
|
return parseInt(match[1], 10);
|
|
23564
24787
|
}
|
|
23565
24788
|
function updateFile(path, content) {
|
|
23566
|
-
if (
|
|
23567
|
-
const existing =
|
|
24789
|
+
if (existsSync13(path)) {
|
|
24790
|
+
const existing = readFileSync16(path, "utf-8");
|
|
23568
24791
|
const version2 = parseVersionHeader(existing);
|
|
23569
24792
|
if (version2 !== null && version2 >= SKILL_VERSION) {
|
|
23570
24793
|
return false;
|
|
23571
24794
|
}
|
|
23572
24795
|
}
|
|
23573
|
-
|
|
24796
|
+
writeFileSync10(path, content, "utf-8");
|
|
23574
24797
|
return true;
|
|
23575
24798
|
}
|
|
23576
24799
|
function skillBody() {
|
|
@@ -23590,8 +24813,8 @@ function skillBody() {
|
|
|
23590
24813
|
].join("\n");
|
|
23591
24814
|
}
|
|
23592
24815
|
function ensureClaudeSkills(cwd) {
|
|
23593
|
-
const dir =
|
|
23594
|
-
|
|
24816
|
+
const dir = join16(cwd, ".claude", "skills", "glassbox");
|
|
24817
|
+
mkdirSync10(dir, { recursive: true });
|
|
23595
24818
|
const content = [
|
|
23596
24819
|
"---",
|
|
23597
24820
|
"name: glassbox",
|
|
@@ -23603,11 +24826,11 @@ function ensureClaudeSkills(cwd) {
|
|
|
23603
24826
|
skillBody(),
|
|
23604
24827
|
""
|
|
23605
24828
|
].join("\n");
|
|
23606
|
-
return updateFile(
|
|
24829
|
+
return updateFile(join16(dir, "SKILL.md"), content);
|
|
23607
24830
|
}
|
|
23608
24831
|
function ensureCursorRules(cwd) {
|
|
23609
|
-
const rulesDir =
|
|
23610
|
-
|
|
24832
|
+
const rulesDir = join16(cwd, ".cursor", "rules");
|
|
24833
|
+
mkdirSync10(rulesDir, { recursive: true });
|
|
23611
24834
|
const content = [
|
|
23612
24835
|
"---",
|
|
23613
24836
|
"description: Read the latest Glassbox code review and apply all feedback annotations",
|
|
@@ -23618,11 +24841,11 @@ function ensureCursorRules(cwd) {
|
|
|
23618
24841
|
skillBody(),
|
|
23619
24842
|
""
|
|
23620
24843
|
].join("\n");
|
|
23621
|
-
return updateFile(
|
|
24844
|
+
return updateFile(join16(rulesDir, "glassbox.mdc"), content);
|
|
23622
24845
|
}
|
|
23623
24846
|
function ensureCopilotPrompts(cwd) {
|
|
23624
|
-
const promptsDir =
|
|
23625
|
-
|
|
24847
|
+
const promptsDir = join16(cwd, ".github", "prompts");
|
|
24848
|
+
mkdirSync10(promptsDir, { recursive: true });
|
|
23626
24849
|
const content = [
|
|
23627
24850
|
"---",
|
|
23628
24851
|
"description: Read the latest Glassbox code review and apply all feedback annotations",
|
|
@@ -23632,11 +24855,11 @@ function ensureCopilotPrompts(cwd) {
|
|
|
23632
24855
|
skillBody(),
|
|
23633
24856
|
""
|
|
23634
24857
|
].join("\n");
|
|
23635
|
-
return updateFile(
|
|
24858
|
+
return updateFile(join16(promptsDir, "glassbox.prompt.md"), content);
|
|
23636
24859
|
}
|
|
23637
24860
|
function ensureWindsurfRules(cwd) {
|
|
23638
|
-
const rulesDir =
|
|
23639
|
-
|
|
24861
|
+
const rulesDir = join16(cwd, ".windsurf", "rules");
|
|
24862
|
+
mkdirSync10(rulesDir, { recursive: true });
|
|
23640
24863
|
const content = [
|
|
23641
24864
|
"---",
|
|
23642
24865
|
"trigger: manual",
|
|
@@ -23647,21 +24870,21 @@ function ensureWindsurfRules(cwd) {
|
|
|
23647
24870
|
skillBody(),
|
|
23648
24871
|
""
|
|
23649
24872
|
].join("\n");
|
|
23650
|
-
return updateFile(
|
|
24873
|
+
return updateFile(join16(rulesDir, "glassbox.md"), content);
|
|
23651
24874
|
}
|
|
23652
24875
|
function ensureSkills() {
|
|
23653
24876
|
const cwd = process.cwd();
|
|
23654
24877
|
const platforms = [];
|
|
23655
|
-
if (
|
|
24878
|
+
if (existsSync13(join16(cwd, ".claude"))) {
|
|
23656
24879
|
if (ensureClaudeSkills(cwd)) platforms.push("Claude Code");
|
|
23657
24880
|
}
|
|
23658
|
-
if (
|
|
24881
|
+
if (existsSync13(join16(cwd, ".cursor"))) {
|
|
23659
24882
|
if (ensureCursorRules(cwd)) platforms.push("Cursor");
|
|
23660
24883
|
}
|
|
23661
|
-
if (
|
|
24884
|
+
if (existsSync13(join16(cwd, ".github", "prompts")) || existsSync13(join16(cwd, ".github", "copilot-instructions.md"))) {
|
|
23662
24885
|
if (ensureCopilotPrompts(cwd)) platforms.push("GitHub Copilot");
|
|
23663
24886
|
}
|
|
23664
|
-
if (
|
|
24887
|
+
if (existsSync13(join16(cwd, ".windsurf"))) {
|
|
23665
24888
|
if (ensureWindsurfRules(cwd)) platforms.push("Windsurf");
|
|
23666
24889
|
}
|
|
23667
24890
|
return platforms;
|
|
@@ -23669,36 +24892,36 @@ function ensureSkills() {
|
|
|
23669
24892
|
|
|
23670
24893
|
// src/update-check.ts
|
|
23671
24894
|
init_zod();
|
|
23672
|
-
import { existsSync as
|
|
24895
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync11, readFileSync as readFileSync17, writeFileSync as writeFileSync11 } from "fs";
|
|
23673
24896
|
import { get } from "https";
|
|
23674
24897
|
import { homedir as homedir3 } from "os";
|
|
23675
|
-
import { dirname as
|
|
24898
|
+
import { dirname as dirname5, join as join17 } from "path";
|
|
23676
24899
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
23677
|
-
var DATA_DIR =
|
|
23678
|
-
var CHECK_FILE =
|
|
24900
|
+
var DATA_DIR = join17(homedir3(), ".glassbox");
|
|
24901
|
+
var CHECK_FILE = join17(DATA_DIR, "last-update-check");
|
|
23679
24902
|
var PACKAGE_NAME = "glassbox";
|
|
23680
24903
|
var VersionPayloadSchema = external_exports.object({ version: external_exports.string() });
|
|
23681
24904
|
function getCurrentVersion() {
|
|
23682
24905
|
try {
|
|
23683
|
-
const dir =
|
|
23684
|
-
const
|
|
23685
|
-
return VersionPayloadSchema.parse(
|
|
24906
|
+
const dir = dirname5(fileURLToPath3(import.meta.url));
|
|
24907
|
+
const raw2 = JSON.parse(readFileSync17(join17(dir, "..", "package.json"), "utf-8"));
|
|
24908
|
+
return VersionPayloadSchema.parse(raw2).version;
|
|
23686
24909
|
} catch {
|
|
23687
24910
|
return "0.0.0";
|
|
23688
24911
|
}
|
|
23689
24912
|
}
|
|
23690
24913
|
function getLastCheckDate() {
|
|
23691
24914
|
try {
|
|
23692
|
-
if (
|
|
23693
|
-
return
|
|
24915
|
+
if (existsSync14(CHECK_FILE)) {
|
|
24916
|
+
return readFileSync17(CHECK_FILE, "utf-8").trim();
|
|
23694
24917
|
}
|
|
23695
24918
|
} catch {
|
|
23696
24919
|
}
|
|
23697
24920
|
return null;
|
|
23698
24921
|
}
|
|
23699
24922
|
function saveCheckDate() {
|
|
23700
|
-
|
|
23701
|
-
|
|
24923
|
+
mkdirSync11(DATA_DIR, { recursive: true });
|
|
24924
|
+
writeFileSync11(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
|
|
23702
24925
|
}
|
|
23703
24926
|
function isFirstUseToday() {
|
|
23704
24927
|
const last = getLastCheckDate();
|
|
@@ -23707,10 +24930,10 @@ function isFirstUseToday() {
|
|
|
23707
24930
|
return last !== today;
|
|
23708
24931
|
}
|
|
23709
24932
|
function fetchLatestVersion() {
|
|
23710
|
-
return new Promise((
|
|
24933
|
+
return new Promise((resolve11) => {
|
|
23711
24934
|
const req = get(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { timeout: 5e3 }, (res) => {
|
|
23712
24935
|
if (res.statusCode !== 200) {
|
|
23713
|
-
|
|
24936
|
+
resolve11(null);
|
|
23714
24937
|
return;
|
|
23715
24938
|
}
|
|
23716
24939
|
let data = "";
|
|
@@ -23719,19 +24942,19 @@ function fetchLatestVersion() {
|
|
|
23719
24942
|
});
|
|
23720
24943
|
res.on("end", () => {
|
|
23721
24944
|
try {
|
|
23722
|
-
const
|
|
23723
|
-
|
|
24945
|
+
const raw2 = JSON.parse(data);
|
|
24946
|
+
resolve11(VersionPayloadSchema.parse(raw2).version);
|
|
23724
24947
|
} catch {
|
|
23725
|
-
|
|
24948
|
+
resolve11(null);
|
|
23726
24949
|
}
|
|
23727
24950
|
});
|
|
23728
24951
|
});
|
|
23729
24952
|
req.on("error", () => {
|
|
23730
|
-
|
|
24953
|
+
resolve11(null);
|
|
23731
24954
|
});
|
|
23732
24955
|
req.on("timeout", () => {
|
|
23733
24956
|
req.destroy();
|
|
23734
|
-
|
|
24957
|
+
resolve11(null);
|
|
23735
24958
|
});
|
|
23736
24959
|
});
|
|
23737
24960
|
}
|
|
@@ -23881,14 +25104,14 @@ function parseArgs(argv) {
|
|
|
23881
25104
|
console.error("--diff requires two paths: --diff <pathA> <pathB>");
|
|
23882
25105
|
process.exit(1);
|
|
23883
25106
|
}
|
|
23884
|
-
mode = { type: "diff", pathA:
|
|
25107
|
+
mode = { type: "diff", pathA: resolve10(args[++i]), pathB: resolve10(args[++i]) };
|
|
23885
25108
|
break;
|
|
23886
25109
|
}
|
|
23887
25110
|
case "--port":
|
|
23888
25111
|
port = parseInt(args[++i], 10);
|
|
23889
25112
|
break;
|
|
23890
25113
|
case "--data-dir":
|
|
23891
|
-
dataDir =
|
|
25114
|
+
dataDir = resolve10(args[++i]);
|
|
23892
25115
|
break;
|
|
23893
25116
|
case "--resume":
|
|
23894
25117
|
resume = true;
|
|
@@ -23946,6 +25169,17 @@ function parseArgs(argv) {
|
|
|
23946
25169
|
return { mode, port, dataDir, resume, forceUpdateCheck, debug, aiServiceTest, demo, noOpen, strictPort, projectDir, difftoolAction, difftoolLocal, difftoolForce, difftoolServe };
|
|
23947
25170
|
}
|
|
23948
25171
|
async function main() {
|
|
25172
|
+
const rawArgs = process.argv.slice(2);
|
|
25173
|
+
if (rawArgs[0] === "note") {
|
|
25174
|
+
const { runNoteCli: runNoteCli2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
25175
|
+
try {
|
|
25176
|
+
await runNoteCli2(rawArgs.slice(1));
|
|
25177
|
+
process.exit(0);
|
|
25178
|
+
} catch (err) {
|
|
25179
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
25180
|
+
process.exit(1);
|
|
25181
|
+
}
|
|
25182
|
+
}
|
|
23949
25183
|
const parsed = parseArgs(process.argv);
|
|
23950
25184
|
if (!parsed) {
|
|
23951
25185
|
printUsage();
|
|
@@ -23987,19 +25221,19 @@ async function main() {
|
|
|
23987
25221
|
console.log("AI service test mode enabled \u2014 using mock AI responses");
|
|
23988
25222
|
}
|
|
23989
25223
|
if (debug) {
|
|
23990
|
-
console.log(`[debug] Build timestamp: ${"2026-06-
|
|
25224
|
+
console.log(`[debug] Build timestamp: ${"2026-06-19T00:30:57.390Z"}`);
|
|
23991
25225
|
}
|
|
23992
25226
|
if (projectDir !== null) {
|
|
23993
25227
|
process.chdir(projectDir);
|
|
23994
25228
|
}
|
|
23995
25229
|
if (dataDir === null) {
|
|
23996
|
-
dataDir =
|
|
25230
|
+
dataDir = join19(process.cwd(), ".glassbox");
|
|
23997
25231
|
}
|
|
23998
25232
|
if (difftoolServe) {
|
|
23999
25233
|
const { initDifftoolSession: initDifftoolSession2 } = await Promise.resolve().then(() => (init_session(), session_exports));
|
|
24000
25234
|
const { writeDiscovery: writeDiscovery2, clearDiscovery: clearDiscovery2, releaseStartingLock: releaseStartingLock2 } = await Promise.resolve().then(() => (init_difftool_discovery(), difftool_discovery_exports));
|
|
24001
25235
|
const { clearDifftoolBlobs: clearDifftoolBlobs2 } = await Promise.resolve().then(() => (init_blob_store(), blob_store_exports));
|
|
24002
|
-
|
|
25236
|
+
mkdirSync13(dataDir, { recursive: true });
|
|
24003
25237
|
setDataDir(dataDir);
|
|
24004
25238
|
const sessionDataDir = dataDir;
|
|
24005
25239
|
clearDifftoolBlobs2(sessionDataDir);
|
|
@@ -24034,13 +25268,13 @@ async function main() {
|
|
|
24034
25268
|
}
|
|
24035
25269
|
process.exit(1);
|
|
24036
25270
|
}
|
|
24037
|
-
dataDir =
|
|
25271
|
+
dataDir = join19(tmpdir2(), `glassbox-demo-${demo}-${Date.now()}`);
|
|
24038
25272
|
setDemoMode(demo);
|
|
24039
25273
|
console.log(`
|
|
24040
25274
|
DEMO MODE: ${scenario.label}
|
|
24041
25275
|
`);
|
|
24042
25276
|
}
|
|
24043
|
-
|
|
25277
|
+
mkdirSync13(dataDir, { recursive: true });
|
|
24044
25278
|
if (demo === null) {
|
|
24045
25279
|
acquireLock(dataDir);
|
|
24046
25280
|
}
|
|
@@ -24062,12 +25296,12 @@ async function main() {
|
|
|
24062
25296
|
if (mode.type === "diff") {
|
|
24063
25297
|
const { pathA, pathB } = mode;
|
|
24064
25298
|
for (const p of [pathA, pathB]) {
|
|
24065
|
-
if (!
|
|
25299
|
+
if (!existsSync16(p)) {
|
|
24066
25300
|
console.error(`Error: path does not exist: ${p}`);
|
|
24067
25301
|
process.exit(1);
|
|
24068
25302
|
}
|
|
24069
25303
|
}
|
|
24070
|
-
if (
|
|
25304
|
+
if (statSync5(pathA).isDirectory() !== statSync5(pathB).isDirectory()) {
|
|
24071
25305
|
console.error("Error: --diff requires two files or two folders, not a mix of both.");
|
|
24072
25306
|
process.exit(1);
|
|
24073
25307
|
}
|