opencode-reviewer 0.4.1 → 0.4.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/README.md +76 -185
- package/dist/tui.js +879 -438
- package/package.json +10 -2
package/dist/tui.js
CHANGED
|
@@ -554,7 +554,7 @@ import { CliRenderEvents, CodeRenderable, RGBA, SyntaxStyle } from "@opentui/cor
|
|
|
554
554
|
init_define_REVIEW_PROMPTS();
|
|
555
555
|
import path from "node:path";
|
|
556
556
|
function parseConfig(options = {}) {
|
|
557
|
-
const keys = /* @__PURE__ */ new Set(["baseURL", "model", "apiKey", "apiKeyEnv", "instructions", "stream", "reviewBash", "reviewEdits", "reviewMcp", "reviewCustomTools", "reviewExternalDirectories", "autoApprove", "autoApproveDelaySeconds", "formatRetries", "timeoutMs", "maxFiles", "maxEvidenceBytes"]);
|
|
557
|
+
const keys = /* @__PURE__ */ new Set(["baseURL", "model", "apiKey", "apiKeyEnv", "instructions", "stream", "reviewBash", "reviewEdits", "reviewMcp", "reviewCustomTools", "reviewExternalDirectories", "autoApprove", "extraCareful", "autoApproveDelaySeconds", "formatRetries", "timeoutMs", "maxFiles", "maxEvidenceBytes"]);
|
|
558
558
|
for (const key of Object.keys(options)) if (!keys.has(key)) throw new Error(`Unknown opencode-reviewer setting: ${key}`);
|
|
559
559
|
const text = (name, optional = false) => {
|
|
560
560
|
const value = options[name];
|
|
@@ -602,6 +602,7 @@ function parseConfig(options = {}) {
|
|
|
602
602
|
reviewCustomTools: boolean("reviewCustomTools", false),
|
|
603
603
|
reviewExternalDirectories: boolean("reviewExternalDirectories", false),
|
|
604
604
|
autoApprove: boolean("autoApprove", false),
|
|
605
|
+
extraCareful: boolean("extraCareful"),
|
|
605
606
|
autoApproveDelaySeconds: number("autoApproveDelaySeconds", 15, 0, 3600),
|
|
606
607
|
formatRetries: number("formatRetries", 1, 0, 100),
|
|
607
608
|
timeoutMs: number("timeoutMs", 3e4, 1, 36e5),
|
|
@@ -768,10 +769,10 @@ var FileScope = class {
|
|
|
768
769
|
var fileAccess = new FileAccess();
|
|
769
770
|
|
|
770
771
|
// src/context.ts
|
|
771
|
-
async function loadInvocation(request,
|
|
772
|
+
async function loadInvocation(request, reader2, signal) {
|
|
772
773
|
signal.throwIfAborted();
|
|
773
774
|
if (!request.tool) throw new Error("Pending tool linkage unavailable");
|
|
774
|
-
const message = await
|
|
775
|
+
const message = await reader2.message(request.sessionID, request.tool.messageID, signal);
|
|
775
776
|
signal.throwIfAborted();
|
|
776
777
|
if (!message || message.info.role !== "assistant" || message.info.sessionID !== request.sessionID || message.info.id !== request.tool.messageID) {
|
|
777
778
|
throw new Error("Pending tool message unavailable or mismatched");
|
|
@@ -787,6 +788,7 @@ async function loadInvocation(request, reader, signal) {
|
|
|
787
788
|
info: message.info,
|
|
788
789
|
tool: part.tool,
|
|
789
790
|
input: part.state.input,
|
|
791
|
+
linkage: { sessionID: request.sessionID, messageID: request.tool.messageID, callID: request.tool.callID },
|
|
790
792
|
location: {
|
|
791
793
|
instanceDirectory: typeof invocation?.cwd === "string" && path2.isAbsolute(invocation.cwd) ? invocation.cwd : null,
|
|
792
794
|
instanceWorktree: typeof invocation?.root === "string" && path2.isAbsolute(invocation.root) ? invocation.root : null
|
|
@@ -824,27 +826,33 @@ function findUserPrompt(messages, sessionID) {
|
|
|
824
826
|
function latestUserPrompt(messages, sessionID) {
|
|
825
827
|
return findUserPrompt(messages, sessionID)?.text ?? null;
|
|
826
828
|
}
|
|
827
|
-
async function
|
|
829
|
+
async function nativeInvocation(request, reader2, signal, kind) {
|
|
830
|
+
try {
|
|
831
|
+
return await loadInvocation(request, reader2, signal);
|
|
832
|
+
} catch (error) {
|
|
833
|
+
signal.throwIfAborted();
|
|
834
|
+
throw new Error(`Pending native ${kind} message unavailable or arguments mismatched`, { cause: error });
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
function invocationFor(request, verified) {
|
|
838
|
+
if (verified.linkage.sessionID !== request.sessionID || verified.linkage.messageID !== request.tool?.messageID || verified.linkage.callID !== request.tool?.callID) throw new Error("Verified invocation linkage mismatched");
|
|
839
|
+
return verified;
|
|
840
|
+
}
|
|
841
|
+
async function loadContext(request, reader2, signal, scope = fileAccess.scope(signal), verified) {
|
|
842
|
+
signal.throwIfAborted();
|
|
828
843
|
if (request.permission !== "bash" && request.permission !== "external_directory") return null;
|
|
829
844
|
if (!request.tool) {
|
|
830
845
|
if (request.permission === "external_directory") return null;
|
|
831
846
|
throw new Error("Native shell tool context unavailable");
|
|
832
847
|
}
|
|
833
|
-
const
|
|
834
|
-
if (
|
|
835
|
-
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
if (part?.type === "tool" && part.tool !== "bash" && request.permission === "external_directory") return null;
|
|
839
|
-
if (!part || part.type !== "tool" || part.sessionID !== request.sessionID || part.messageID !== request.tool.messageID || part.tool !== "bash" || part.state.status !== "running") {
|
|
840
|
-
throw new Error("Pending native shell arguments unavailable");
|
|
841
|
-
}
|
|
842
|
-
const { command, workdir } = part.state.input;
|
|
848
|
+
const invocation = verified ? invocationFor(request, verified) : await nativeInvocation(request, reader2, signal, "shell");
|
|
849
|
+
if (invocation.tool !== "bash" && request.permission === "external_directory") return null;
|
|
850
|
+
if (invocation.tool !== "bash") throw new Error("Pending native shell arguments unavailable");
|
|
851
|
+
const { command, workdir } = invocation.input;
|
|
852
|
+
if (typeof command === "string" && command.length > 16 * 1024 * 1024) throw new Error("Shell command exceeds the 16 MiB preprocessing limit");
|
|
843
853
|
if (typeof command !== "string" || !command.trim()) throw new Error("Shell command unavailable");
|
|
844
854
|
const limitations = [];
|
|
845
|
-
const
|
|
846
|
-
const instanceDirectory = typeof invocation?.cwd === "string" && path2.isAbsolute(invocation.cwd) ? invocation.cwd : null;
|
|
847
|
-
const instanceWorktree = typeof invocation?.root === "string" && path2.isAbsolute(invocation.root) ? invocation.root : null;
|
|
855
|
+
const { instanceDirectory, instanceWorktree } = invocation.location;
|
|
848
856
|
const invalidWorkdir = workdir !== void 0 && typeof workdir !== "string";
|
|
849
857
|
const requestedWorkdir = typeof workdir === "string" && workdir ? workdir : null;
|
|
850
858
|
const cwd = invalidWorkdir ? null : requestedWorkdir && path2.isAbsolute(requestedWorkdir) ? requestedWorkdir : instanceDirectory ? path2.resolve(instanceDirectory, requestedWorkdir ?? ".") : null;
|
|
@@ -859,7 +867,7 @@ async function loadContext(request, reader, signal, scope = fileAccess.scope(sig
|
|
|
859
867
|
cwdSource: invalidWorkdir ? "unavailable" : requestedWorkdir && path2.isAbsolute(requestedWorkdir) ? "absolute tool.workdir" : instanceDirectory ? requestedWorkdir ? "tool.workdir relative to assistant.path.cwd" : "assistant.path.cwd" : "unavailable",
|
|
860
868
|
canonicalCwd: null
|
|
861
869
|
};
|
|
862
|
-
const conversation = await loadConversationContext(request,
|
|
870
|
+
const conversation = await loadConversationContext(request, reader2, signal);
|
|
863
871
|
if (cwd) {
|
|
864
872
|
const canonical = await scope.canonical(cwd);
|
|
865
873
|
execution.canonicalCwd = canonical.path;
|
|
@@ -885,28 +893,20 @@ function permissionContext(request, clone = true) {
|
|
|
885
893
|
tool: request.tool ? { ...request.tool } : null
|
|
886
894
|
};
|
|
887
895
|
}
|
|
888
|
-
async function loadEditContext(request,
|
|
896
|
+
async function loadEditContext(request, reader2, signal, verified) {
|
|
889
897
|
signal.throwIfAborted();
|
|
890
898
|
if (request.permission !== "edit" || !request.tool) throw new Error("Native edit tool context unavailable");
|
|
891
|
-
const
|
|
892
|
-
if (
|
|
893
|
-
throw new Error("Pending native edit message unavailable");
|
|
894
|
-
}
|
|
895
|
-
const part = message.parts.find((p) => p.type === "tool" && p.callID === request.tool.callID);
|
|
896
|
-
if (!part || part.type !== "tool" || part.sessionID !== request.sessionID || part.messageID !== request.tool.messageID || part.state.status !== "running" || part.tool !== "edit" && part.tool !== "write" && part.tool !== "apply_patch") {
|
|
899
|
+
const invocation = verified ? invocationFor(request, verified) : await nativeInvocation(request, reader2, signal, "edit");
|
|
900
|
+
if (invocation.tool !== "edit" && invocation.tool !== "write" && invocation.tool !== "apply_patch") {
|
|
897
901
|
throw new Error("Pending native edit arguments unavailable or unsupported tool");
|
|
898
902
|
}
|
|
899
|
-
const
|
|
900
|
-
const
|
|
901
|
-
instanceDirectory: typeof invocation?.cwd === "string" && path2.isAbsolute(invocation.cwd) ? invocation.cwd : null,
|
|
902
|
-
instanceWorktree: typeof invocation?.root === "string" && path2.isAbsolute(invocation.root) ? invocation.root : null
|
|
903
|
-
};
|
|
904
|
-
const conversation = await loadConversationContext(request, reader, signal);
|
|
903
|
+
const location = invocation.location;
|
|
904
|
+
const conversation = await loadConversationContext(request, reader2, signal);
|
|
905
905
|
if (!location.instanceDirectory) conversation.limitations.push("Edit invocation directory unavailable; session origin is not substituted.");
|
|
906
906
|
if (!location.instanceWorktree) conversation.limitations.push("Edit invocation worktree unavailable.");
|
|
907
|
-
return { ...conversation, kind: "edit", tool:
|
|
907
|
+
return { ...conversation, kind: "edit", tool: invocation.tool, location, permission: permissionContext(request, false) };
|
|
908
908
|
}
|
|
909
|
-
async function loadConversationContext(request,
|
|
909
|
+
async function loadConversationContext(request, reader2, signal) {
|
|
910
910
|
signal.throwIfAborted();
|
|
911
911
|
const limitations = [];
|
|
912
912
|
let current;
|
|
@@ -915,7 +915,7 @@ async function loadConversationContext(request, reader, signal) {
|
|
|
915
915
|
const contextEnd = performance.now() + Math.min(5e3, remainingTime(signal) / 3);
|
|
916
916
|
const contextMs = () => Math.max(0, contextEnd - performance.now());
|
|
917
917
|
const ancestry = async (contextSignal) => {
|
|
918
|
-
const loaded = await
|
|
918
|
+
const loaded = await reader2.session(request.sessionID, contextSignal);
|
|
919
919
|
contextSignal.throwIfAborted();
|
|
920
920
|
if (loaded?.id !== request.sessionID) throw new Error("Session mismatch");
|
|
921
921
|
current = loaded;
|
|
@@ -926,13 +926,13 @@ async function loadConversationContext(request, reader, signal) {
|
|
|
926
926
|
if (seen.has(ancestor.id) || seen.size >= 16) throw new Error("Parent chain incomplete");
|
|
927
927
|
seen.add(ancestor.id);
|
|
928
928
|
const expected = ancestor.parentID;
|
|
929
|
-
const parent = await
|
|
929
|
+
const parent = await reader2.session(expected, contextSignal);
|
|
930
930
|
contextSignal.throwIfAborted();
|
|
931
931
|
if (parent?.id !== expected) throw new Error("Parent session unavailable");
|
|
932
932
|
ancestor = parent;
|
|
933
933
|
}
|
|
934
934
|
root = ancestor;
|
|
935
|
-
const messages = await
|
|
935
|
+
const messages = await reader2.messages(root.id, contextSignal);
|
|
936
936
|
contextSignal.throwIfAborted();
|
|
937
937
|
prompt2 = latestUserPrompt(messages, root.id);
|
|
938
938
|
};
|
|
@@ -945,7 +945,7 @@ async function loadConversationContext(request, reader, signal) {
|
|
|
945
945
|
let projects = [];
|
|
946
946
|
if (current || root) {
|
|
947
947
|
try {
|
|
948
|
-
projects = await withDeadline(signal, contextMs(), (contextSignal) =>
|
|
948
|
+
projects = await withDeadline(signal, contextMs(), (contextSignal) => reader2.projects(contextSignal), "Project context lookup");
|
|
949
949
|
} catch (error) {
|
|
950
950
|
signal.throwIfAborted();
|
|
951
951
|
limitations.push(error instanceof DeadlineError ? error.message : "OpenCode project metadata unavailable.");
|
|
@@ -1122,7 +1122,9 @@ function estimatedUsage(body, requestedModel, pricing) {
|
|
|
1122
1122
|
const result = { input: usage.prompt_tokens, output: usage.completion_tokens };
|
|
1123
1123
|
const prices = pricing?.(typeof body.model === "string" ? body.model : requestedModel);
|
|
1124
1124
|
if (!prices) return result;
|
|
1125
|
-
const
|
|
1125
|
+
const rawDetails = usage.prompt_tokens_details;
|
|
1126
|
+
if (rawDetails !== void 0 && (!rawDetails || typeof rawDetails !== "object" || Array.isArray(rawDetails))) return result;
|
|
1127
|
+
const details = record(rawDetails);
|
|
1126
1128
|
const cached = details.cached_tokens === void 0 ? 0 : details.cached_tokens;
|
|
1127
1129
|
const written = details.cache_write_tokens === void 0 ? 0 : details.cache_write_tokens;
|
|
1128
1130
|
if (!count(cached) || !count(written) || cached + written > result.input || usage.cache_creation_input_tokens || usage.cache_read_input_tokens) return result;
|
|
@@ -1149,7 +1151,8 @@ function usageAttempt(baseURL, requestedModel, pricing, onUsage) {
|
|
|
1149
1151
|
finalized = true;
|
|
1150
1152
|
if (usage) {
|
|
1151
1153
|
try {
|
|
1152
|
-
onUsage?.({ ...usage })
|
|
1154
|
+
void Promise.resolve(onUsage?.({ ...usage })).catch(() => {
|
|
1155
|
+
});
|
|
1153
1156
|
} catch {
|
|
1154
1157
|
}
|
|
1155
1158
|
}
|
|
@@ -1174,7 +1177,7 @@ function sumUsage(previous, next) {
|
|
|
1174
1177
|
}
|
|
1175
1178
|
function usageText(usage) {
|
|
1176
1179
|
return [
|
|
1177
|
-
...count(usage.input) && count(usage.output) ? [`
|
|
1180
|
+
...count(usage.input) && count(usage.output) ? [`token: ${usage.input} in ${usage.output} out`] : [],
|
|
1178
1181
|
...usage.cost === void 0 ? [] : [`cost: $${usage.cost.toFixed(4)}`]
|
|
1179
1182
|
].join("\n");
|
|
1180
1183
|
}
|
|
@@ -1523,13 +1526,13 @@ function cancelBody(response) {
|
|
|
1523
1526
|
}
|
|
1524
1527
|
async function readBody(response, signal, consume) {
|
|
1525
1528
|
if (!response.body) throw new Error("Reviewer returned an empty HTTP response");
|
|
1526
|
-
const
|
|
1529
|
+
const reader2 = response.body.getReader();
|
|
1527
1530
|
try {
|
|
1528
1531
|
while (true) {
|
|
1529
1532
|
reviewStage(signal, "Reviewer response body");
|
|
1530
1533
|
let part;
|
|
1531
1534
|
try {
|
|
1532
|
-
part = await abortable(
|
|
1535
|
+
part = await abortable(reader2.read(), signal);
|
|
1533
1536
|
} catch {
|
|
1534
1537
|
signal.throwIfAborted();
|
|
1535
1538
|
throw new Error("Reviewer response read failed");
|
|
@@ -1540,10 +1543,10 @@ async function readBody(response, signal, consume) {
|
|
|
1540
1543
|
}
|
|
1541
1544
|
} finally {
|
|
1542
1545
|
try {
|
|
1543
|
-
void
|
|
1546
|
+
void reader2.cancel().catch(() => {
|
|
1544
1547
|
});
|
|
1545
1548
|
} finally {
|
|
1546
|
-
|
|
1549
|
+
reader2.releaseLock();
|
|
1547
1550
|
}
|
|
1548
1551
|
}
|
|
1549
1552
|
}
|
|
@@ -1635,7 +1638,7 @@ async function review(evidence, config, signal, fetcher = fetch, environment = p
|
|
|
1635
1638
|
if (config.apiKeyEnv && !key) throw new Error(`API key environment variable ${config.apiKeyEnv} is unset or empty`);
|
|
1636
1639
|
const prompt2 = prompts[evidence.kind];
|
|
1637
1640
|
const messages = [
|
|
1638
|
-
{ role: "system", content: [prompt2.instructions, ...config.autoApprove ? [prompts.extraCareful] : [], CONTRACT].join("\n\n") },
|
|
1641
|
+
{ role: "system", content: [prompt2.instructions, ...config.autoApprove && config.extraCareful ? [prompts.extraCareful] : [], CONTRACT].join("\n\n") },
|
|
1639
1642
|
{ role: "user", content: JSON.stringify(evidence) }
|
|
1640
1643
|
];
|
|
1641
1644
|
let usage;
|
|
@@ -1644,7 +1647,8 @@ async function review(evidence, config, signal, fetcher = fetch, environment = p
|
|
|
1644
1647
|
const progress = (phase, preview) => {
|
|
1645
1648
|
if (signal.aborted) return;
|
|
1646
1649
|
try {
|
|
1647
|
-
onProgress?.({ attempt, phase, ...preview ? { preview: { ...preview } } : {} })
|
|
1650
|
+
void Promise.resolve(onProgress?.({ attempt, phase, ...preview ? { preview: { ...preview } } : {} })).catch(() => {
|
|
1651
|
+
});
|
|
1648
1652
|
} catch {
|
|
1649
1653
|
}
|
|
1650
1654
|
};
|
|
@@ -1697,7 +1701,7 @@ async function review(evidence, config, signal, fetcher = fetch, environment = p
|
|
|
1697
1701
|
const choices = object(envelope) ? envelope.choices : void 0;
|
|
1698
1702
|
if (!Array.isArray(choices) || choices.length !== 1) throw new Error("Reviewer API must return one completion");
|
|
1699
1703
|
const choice = choices[0], message = choice?.message;
|
|
1700
|
-
if (!object(message) || typeof message.content !== "string" || !textMetadata(message) || object(envelope) && envelope.error != null || choice.index !== void 0 && choice.index !== 0 || choice.finish_reason !== void 0 && choice.finish_reason !== "stop") {
|
|
1704
|
+
if (!object(choice) || !object(message) || typeof message.content !== "string" || !textMetadata(message) || object(envelope) && envelope.error != null || choice.error != null || choice.index !== void 0 && choice.index !== 0 || choice.finish_reason !== void 0 && choice.finish_reason !== "stop") {
|
|
1701
1705
|
throw new Error("Reviewer API did not return a text assessment");
|
|
1702
1706
|
}
|
|
1703
1707
|
if (message.content) diagnose?.("first-content");
|
|
@@ -1896,7 +1900,7 @@ var Controller = class {
|
|
|
1896
1900
|
for (const entry of this.entries.values()) if (entry.root === root) this.suspend(entry);
|
|
1897
1901
|
this.publish();
|
|
1898
1902
|
}
|
|
1899
|
-
suspend(entry) {
|
|
1903
|
+
suspend(entry, reason = "mode") {
|
|
1900
1904
|
const id = entry.view.request.id;
|
|
1901
1905
|
const state = entry.view.autoApproval?.status;
|
|
1902
1906
|
if (state) this.manual.set(id, state === "failed" || state === "allowing" ? "failed" : "cancelled");
|
|
@@ -1904,6 +1908,7 @@ var Controller = class {
|
|
|
1904
1908
|
this.clearProgress(entry);
|
|
1905
1909
|
entry.approvalAbort?.abort();
|
|
1906
1910
|
entry.abort.abort();
|
|
1911
|
+
entry.suspension = reason;
|
|
1907
1912
|
entry.view = { request: entry.view.request, status: "suspended" };
|
|
1908
1913
|
}
|
|
1909
1914
|
/** Called only after the assessment has actually been rendered, or to hide it. */
|
|
@@ -2106,7 +2111,7 @@ var Controller = class {
|
|
|
2106
2111
|
}
|
|
2107
2112
|
} catch {
|
|
2108
2113
|
if (active()) {
|
|
2109
|
-
this.suspend(entry);
|
|
2114
|
+
this.suspend(entry, "unavailable");
|
|
2110
2115
|
this.publish();
|
|
2111
2116
|
}
|
|
2112
2117
|
return null;
|
|
@@ -2157,7 +2162,7 @@ var Controller = class {
|
|
|
2157
2162
|
for (const request of requests) {
|
|
2158
2163
|
const entry = this.entries.get(request.id);
|
|
2159
2164
|
if (entry && !entry.root) entry.gateRevision = this.version;
|
|
2160
|
-
if (entry?.view.status === "suspended" && (!entry.root || this.modes?.enabled(entry.root))) {
|
|
2165
|
+
if (entry?.view.status === "suspended" && (entry.suspension === "unavailable" || !entry.root || this.modes?.enabled(entry.root))) {
|
|
2161
2166
|
this.entries.delete(request.id);
|
|
2162
2167
|
}
|
|
2163
2168
|
this.asked(request);
|
|
@@ -2199,8 +2204,7 @@ init_define_REVIEW_PROMPTS();
|
|
|
2199
2204
|
|
|
2200
2205
|
// src/evidence.ts
|
|
2201
2206
|
init_define_REVIEW_PROMPTS();
|
|
2202
|
-
|
|
2203
|
-
import path5 from "node:path";
|
|
2207
|
+
import path6 from "node:path";
|
|
2204
2208
|
import { constants as constants2 } from "node:fs";
|
|
2205
2209
|
|
|
2206
2210
|
// src/files.ts
|
|
@@ -2244,47 +2248,199 @@ var FileLimit = class {
|
|
|
2244
2248
|
function omittedFile(filename) {
|
|
2245
2249
|
return `[!] File ${JSON.stringify(filename)} not included in context.`;
|
|
2246
2250
|
}
|
|
2247
|
-
|
|
2251
|
+
var DiffBudget = class {
|
|
2252
|
+
constructor(limits = { bytes: 16 * 1024 * 1024, lines: 65536, ms: 250 }, now = () => performance.now()) {
|
|
2253
|
+
this.now = now;
|
|
2254
|
+
this.bytes = limits.bytes;
|
|
2255
|
+
this.lines = limits.lines;
|
|
2256
|
+
this.ms = limits.ms;
|
|
2257
|
+
}
|
|
2258
|
+
now;
|
|
2259
|
+
bytes;
|
|
2260
|
+
lines;
|
|
2261
|
+
ms;
|
|
2262
|
+
exhausted = false;
|
|
2263
|
+
check(signal, end) {
|
|
2264
|
+
reviewStage(signal, "Evidence collection");
|
|
2265
|
+
if (this.now() >= end) {
|
|
2266
|
+
this.exhausted = true;
|
|
2267
|
+
return false;
|
|
2268
|
+
}
|
|
2269
|
+
return true;
|
|
2270
|
+
}
|
|
2271
|
+
};
|
|
2272
|
+
function diffDelta(diff, signal, budget = new DiffBudget()) {
|
|
2273
|
+
reviewStage(signal, "Evidence collection");
|
|
2248
2274
|
if (typeof diff !== "string") return;
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
const
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
if (
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
if (
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2275
|
+
if (diff.length > budget.bytes || budget.ms <= 0) {
|
|
2276
|
+
budget.exhausted = true;
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
const startTime = budget.now(), endTime = startTime + budget.ms;
|
|
2280
|
+
try {
|
|
2281
|
+
const bytes = Buffer.byteLength(diff);
|
|
2282
|
+
const available = budget.bytes;
|
|
2283
|
+
budget.bytes = Math.max(0, available - bytes);
|
|
2284
|
+
if (bytes > available) {
|
|
2285
|
+
budget.exhausted = true;
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
let old = 0, next = 0, added = 0, removed = 0, hunks = 0;
|
|
2289
|
+
for (let start = 0; start < diff.length; ) {
|
|
2290
|
+
if (!budget.check(signal, endTime)) return;
|
|
2291
|
+
if (budget.lines <= 0) {
|
|
2292
|
+
budget.exhausted = true;
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
budget.lines--;
|
|
2296
|
+
const end = diff.indexOf("\n", start);
|
|
2297
|
+
const line = diff.slice(start, end < 0 ? diff.length : end).replace(/\r$/, "");
|
|
2298
|
+
start = end < 0 ? diff.length : end + 1;
|
|
2299
|
+
const header = /^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@(?:.*)$/.exec(line);
|
|
2300
|
+
if (header) {
|
|
2301
|
+
if (old || next) return;
|
|
2302
|
+
old = Number(header[1] ?? 1);
|
|
2303
|
+
next = Number(header[2] ?? 1);
|
|
2304
|
+
if (!Number.isSafeInteger(old) || !Number.isSafeInteger(next)) return;
|
|
2305
|
+
hunks++;
|
|
2306
|
+
} else if (line === "\") continue;
|
|
2307
|
+
else if (old || next) {
|
|
2308
|
+
if (line.startsWith("+")) {
|
|
2309
|
+
next--;
|
|
2310
|
+
added++;
|
|
2311
|
+
} else if (line.startsWith("-")) {
|
|
2312
|
+
old--;
|
|
2313
|
+
removed++;
|
|
2314
|
+
} else if (line.startsWith(" ")) {
|
|
2315
|
+
old--;
|
|
2316
|
+
next--;
|
|
2317
|
+
} else return;
|
|
2318
|
+
if (old < 0 || next < 0) return;
|
|
2319
|
+
} else if (hunks && line) return;
|
|
2320
|
+
}
|
|
2321
|
+
if (budget.check(signal, endTime) && hunks && !old && !next) return { added, removed };
|
|
2322
|
+
} finally {
|
|
2323
|
+
budget.ms = Math.max(0, budget.ms - (budget.now() - startTime));
|
|
2324
|
+
}
|
|
2278
2325
|
}
|
|
2279
2326
|
|
|
2280
|
-
// src/
|
|
2327
|
+
// src/shell-discovery.ts
|
|
2328
|
+
init_define_REVIEW_PROMPTS();
|
|
2329
|
+
var import_shell_quote = __toESM(require_shell_quote(), 1);
|
|
2330
|
+
import path5 from "node:path";
|
|
2281
2331
|
var VARIABLE = "\0UNRESOLVED_VARIABLE\0";
|
|
2282
2332
|
var python = /^python(?:[23](?:\.\d+)*)?$/;
|
|
2283
2333
|
var shell = /^(?:ba|da|k|z)?sh$/;
|
|
2284
2334
|
var assignment = /^[A-Za-z_][A-Za-z_0-9]*=/;
|
|
2285
|
-
function
|
|
2335
|
+
function literal(word) {
|
|
2336
|
+
return !word.includes(VARIABLE) && !word.startsWith("~");
|
|
2337
|
+
}
|
|
2338
|
+
function unsupportedWhitespace(char) {
|
|
2339
|
+
return /\s/.test(char) && char !== " " && char !== " ";
|
|
2340
|
+
}
|
|
2341
|
+
var DiscoveryLimit = class extends Error {
|
|
2342
|
+
};
|
|
2343
|
+
var DiscoveryBudget = class {
|
|
2344
|
+
constructor(signal) {
|
|
2345
|
+
this.signal = signal;
|
|
2346
|
+
}
|
|
2347
|
+
signal;
|
|
2348
|
+
bytes = 16 * 1024 * 1024;
|
|
2349
|
+
tokens = 65536;
|
|
2350
|
+
references = 16384;
|
|
2351
|
+
expansions = 16384;
|
|
2352
|
+
steps = 262144;
|
|
2353
|
+
notices = /* @__PURE__ */ new Set();
|
|
2354
|
+
end = performance.now() + 500;
|
|
2355
|
+
step() {
|
|
2356
|
+
if (this.signal) reviewStage(this.signal, "Evidence collection");
|
|
2357
|
+
if (--this.steps < 0 || performance.now() >= this.end) throw new DiscoveryLimit();
|
|
2358
|
+
}
|
|
2359
|
+
admit(command) {
|
|
2360
|
+
this.step();
|
|
2361
|
+
if (command.length > this.bytes) throw new DiscoveryLimit();
|
|
2362
|
+
this.bytes -= Buffer.byteLength(command);
|
|
2363
|
+
if (this.bytes < 0) throw new DiscoveryLimit();
|
|
2364
|
+
let quote = "", word = false, units = 0;
|
|
2365
|
+
for (let i = 0; i < command.length; i++) {
|
|
2366
|
+
if (!(i % 4096)) this.step();
|
|
2367
|
+
const char = command[i];
|
|
2368
|
+
if (char === "\\" && quote !== "'") {
|
|
2369
|
+
i++;
|
|
2370
|
+
if (!word) {
|
|
2371
|
+
word = true;
|
|
2372
|
+
units++;
|
|
2373
|
+
}
|
|
2374
|
+
} else if (quote) {
|
|
2375
|
+
if (char === quote) quote = "";
|
|
2376
|
+
else if (quote === '"' && char === "$" && --this.expansions < 0) throw new DiscoveryLimit();
|
|
2377
|
+
} else if (char === "'" || char === '"') {
|
|
2378
|
+
quote = char;
|
|
2379
|
+
if (!word) {
|
|
2380
|
+
word = true;
|
|
2381
|
+
units++;
|
|
2382
|
+
}
|
|
2383
|
+
} else if (char === "#" && !word) break;
|
|
2384
|
+
else if (/\s/.test(char)) word = false;
|
|
2385
|
+
else if (/[;&|()<>]/.test(char)) {
|
|
2386
|
+
word = false;
|
|
2387
|
+
units++;
|
|
2388
|
+
} else {
|
|
2389
|
+
if (char === "$" && --this.expansions < 0) throw new DiscoveryLimit();
|
|
2390
|
+
if (!word) {
|
|
2391
|
+
word = true;
|
|
2392
|
+
units++;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
if (units > this.tokens) throw new DiscoveryLimit();
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
parsed(count3) {
|
|
2399
|
+
this.step();
|
|
2400
|
+
this.tokens -= count3;
|
|
2401
|
+
if (this.tokens < 0) throw new DiscoveryLimit();
|
|
2402
|
+
}
|
|
2403
|
+
reference() {
|
|
2404
|
+
this.step();
|
|
2405
|
+
if (--this.references < 0) throw new DiscoveryLimit();
|
|
2406
|
+
}
|
|
2407
|
+
notice(text) {
|
|
2408
|
+
if (this.notices.has(text)) return;
|
|
2409
|
+
if (this.notices.size >= 256) throw new DiscoveryLimit();
|
|
2410
|
+
this.notices.add(text);
|
|
2411
|
+
}
|
|
2412
|
+
};
|
|
2413
|
+
var Words = class {
|
|
2414
|
+
constructor(words, budget) {
|
|
2415
|
+
this.words = words;
|
|
2416
|
+
this.budget = budget;
|
|
2417
|
+
}
|
|
2418
|
+
words;
|
|
2419
|
+
budget;
|
|
2420
|
+
index = 0;
|
|
2421
|
+
get length() {
|
|
2422
|
+
return this.words.length - this.index;
|
|
2423
|
+
}
|
|
2424
|
+
peek() {
|
|
2425
|
+
this.budget.step();
|
|
2426
|
+
return this.words[this.index];
|
|
2427
|
+
}
|
|
2428
|
+
take() {
|
|
2429
|
+
const word = this.peek();
|
|
2430
|
+
if (this.length) this.index++;
|
|
2431
|
+
return word;
|
|
2432
|
+
}
|
|
2433
|
+
*values() {
|
|
2434
|
+
for (let i = this.index; i < this.words.length; i++) {
|
|
2435
|
+
this.budget.step();
|
|
2436
|
+
yield this.words[i];
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
};
|
|
2440
|
+
function balancedQuotes(command, budget) {
|
|
2286
2441
|
let quote = "";
|
|
2287
2442
|
for (let i = 0; i < command.length; i++) {
|
|
2443
|
+
if (!(i % 4096)) budget.step();
|
|
2288
2444
|
const char = command[i];
|
|
2289
2445
|
if (char === "\\" && quote !== "'") {
|
|
2290
2446
|
if (++i === command.length) return false;
|
|
@@ -2293,12 +2449,15 @@ function balancedQuotes(command) {
|
|
|
2293
2449
|
}
|
|
2294
2450
|
return !quote;
|
|
2295
2451
|
}
|
|
2296
|
-
function unsupportedShellSyntax(command) {
|
|
2452
|
+
function unsupportedShellSyntax(command, budget) {
|
|
2297
2453
|
let quote = "";
|
|
2298
2454
|
let inWord = false;
|
|
2455
|
+
const whitespace2 = "Unsupported unquoted shell whitespace is outside reliable tokenization; script targets were not resolved.";
|
|
2299
2456
|
for (let i = 0; i < command.length; i++) {
|
|
2457
|
+
if (!(i % 4096)) budget.step();
|
|
2300
2458
|
const char = command[i];
|
|
2301
2459
|
if (char === "\\" && quote !== "'") {
|
|
2460
|
+
if (!quote && unsupportedWhitespace(command[i + 1] ?? "")) return whitespace2;
|
|
2302
2461
|
i++;
|
|
2303
2462
|
inWord = true;
|
|
2304
2463
|
} else if (quote) {
|
|
@@ -2306,276 +2465,356 @@ function unsupportedShellSyntax(command) {
|
|
|
2306
2465
|
} else if (char === "'" || char === '"') {
|
|
2307
2466
|
quote = char;
|
|
2308
2467
|
inWord = true;
|
|
2309
|
-
} else if (char
|
|
2468
|
+
} else if (unsupportedWhitespace(char)) return whitespace2;
|
|
2469
|
+
else if (char === "#") {
|
|
2310
2470
|
if (!inWord) return;
|
|
2311
2471
|
return "Unquoted # within a shell word is outside reliable tokenization; script targets were not resolved.";
|
|
2312
2472
|
} else if (char === "[" || char === "{" || char === "}") {
|
|
2313
2473
|
return "Unquoted bracket glob or brace syntax is outside literal discovery; script targets were not resolved.";
|
|
2314
|
-
} else inWord = !/[\
|
|
2474
|
+
} else inWord = !/[ \t;&|()<>]/.test(char);
|
|
2315
2475
|
}
|
|
2316
2476
|
}
|
|
2317
|
-
function
|
|
2318
|
-
const
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2477
|
+
function programIdentity(executable) {
|
|
2478
|
+
const name = /^(?:\/(?:usr\/)?bin\/)?([^/]+)$/.exec(executable)?.[1];
|
|
2479
|
+
if (!name) return;
|
|
2480
|
+
if (python.test(name)) return "python";
|
|
2481
|
+
if (shell.test(name)) return "shell";
|
|
2482
|
+
if (name === "cat" || name === "head" || name === "env") return name;
|
|
2483
|
+
}
|
|
2484
|
+
var DiscoveryState = class {
|
|
2485
|
+
constructor(cwd, depth, budget) {
|
|
2486
|
+
this.cwd = cwd;
|
|
2487
|
+
this.depth = depth;
|
|
2488
|
+
this.budget = budget;
|
|
2489
|
+
}
|
|
2490
|
+
cwd;
|
|
2491
|
+
depth;
|
|
2492
|
+
budget;
|
|
2493
|
+
references = [];
|
|
2494
|
+
limitations = [];
|
|
2495
|
+
uncertainWord = false;
|
|
2496
|
+
preceded = false;
|
|
2497
|
+
previousOperator;
|
|
2498
|
+
conditionalCwd = false;
|
|
2499
|
+
explicitCdpath = false;
|
|
2500
|
+
note(text) {
|
|
2501
|
+
this.budget.notice(text);
|
|
2502
|
+
if (!this.limitations.includes(text)) this.limitations.push(text);
|
|
2503
|
+
}
|
|
2504
|
+
invalidateCwd(text) {
|
|
2505
|
+
this.cwd = null;
|
|
2506
|
+
this.note(text);
|
|
2507
|
+
}
|
|
2508
|
+
observeAssignment(word) {
|
|
2509
|
+
if (!word.startsWith("CDPATH=")) return;
|
|
2510
|
+
this.explicitCdpath = true;
|
|
2511
|
+
this.note("Explicit CDPATH assignment is not evaluated; directory-search targets may be unresolved.");
|
|
2326
2512
|
}
|
|
2327
|
-
|
|
2328
|
-
note("
|
|
2329
|
-
return { references, limitations };
|
|
2513
|
+
qualifySnapshot() {
|
|
2514
|
+
if (this.preceded) this.note("Files are current filesystem snapshots; preceding command statements may change them before execution.");
|
|
2330
2515
|
}
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2516
|
+
add(filename, executable = false) {
|
|
2517
|
+
if (!literal(filename) || this.uncertainWord) {
|
|
2518
|
+
this.note(`Script filename unresolved: ${filename.replaceAll(VARIABLE, "<variable>")}`);
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
this.budget.reference();
|
|
2522
|
+
this.qualifySnapshot();
|
|
2523
|
+
this.references.push({ filename, cwd: this.cwd, executable });
|
|
2524
|
+
}
|
|
2525
|
+
nested(command) {
|
|
2526
|
+
const nested = scan(command, this.explicitCdpath ? null : this.cwd, this.depth + 1, this.budget);
|
|
2527
|
+
if (nested.references.length) this.qualifySnapshot();
|
|
2528
|
+
this.references.push(...nested.references);
|
|
2529
|
+
nested.limitations.forEach((text) => this.note(text));
|
|
2530
|
+
}
|
|
2531
|
+
afterOperator(operator) {
|
|
2532
|
+
this.previousOperator = operator;
|
|
2533
|
+
this.uncertainWord = false;
|
|
2534
|
+
this.preceded = true;
|
|
2535
|
+
if (operator === "||") {
|
|
2536
|
+
this.invalidateCwd("Conditional fallback makes the working directory uncertain for later statements.");
|
|
2537
|
+
} else if (operator === ";" && this.conditionalCwd) {
|
|
2538
|
+
this.conditionalCwd = false;
|
|
2539
|
+
this.invalidateCwd("Earlier cd may fail or be skipped before this statement; the working directory is unresolved at the branch join.");
|
|
2540
|
+
}
|
|
2335
2541
|
}
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
let words = [];
|
|
2349
|
-
let uncertainWord = false;
|
|
2350
|
-
let preceded = false;
|
|
2351
|
-
let previousOperator;
|
|
2352
|
-
let conditionalCwd = false;
|
|
2353
|
-
let explicitCdpath = false;
|
|
2354
|
-
const observeAssignment = (word) => {
|
|
2355
|
-
if (!word.startsWith("CDPATH=")) return;
|
|
2356
|
-
explicitCdpath = true;
|
|
2357
|
-
note("Explicit CDPATH assignment is not evaluated; directory-search targets may be unresolved.");
|
|
2358
|
-
};
|
|
2359
|
-
const literal = (s) => !s.includes(VARIABLE) && !s.startsWith("~");
|
|
2360
|
-
const qualifySnapshot = () => {
|
|
2361
|
-
if (preceded) note("Files are current filesystem snapshots; preceding command statements may change them before execution.");
|
|
2362
|
-
};
|
|
2363
|
-
const add2 = (filename, executable = false) => {
|
|
2364
|
-
if (!literal(filename) || uncertainWord) {
|
|
2365
|
-
note(`Script filename unresolved: ${filename.replaceAll(VARIABLE, "<variable>")}`);
|
|
2542
|
+
};
|
|
2543
|
+
function unwrap(args, state) {
|
|
2544
|
+
let builtins = true, builtinOnly = false, wrappers = 0;
|
|
2545
|
+
while (args.peek() && assignment.test(args.peek())) state.observeAssignment(args.take());
|
|
2546
|
+
while (args.peek()) {
|
|
2547
|
+
const name = args.peek();
|
|
2548
|
+
const env = programIdentity(name) === "env";
|
|
2549
|
+
if (!env && !["command", "exec", "builtin"].includes(name)) break;
|
|
2550
|
+
if (wrappers++ >= 8) {
|
|
2551
|
+
const text = "Wrapper nesting exceeds literal discovery's eight-wrapper limit; source was not resolved.";
|
|
2552
|
+
if (builtins) state.invalidateCwd(`${text} The working directory is unresolved.`);
|
|
2553
|
+
else state.note(text);
|
|
2366
2554
|
return;
|
|
2367
2555
|
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
const args = words;
|
|
2373
|
-
words = [];
|
|
2374
|
-
let builtins = true;
|
|
2375
|
-
while (args[0] && assignment.test(args[0])) observeAssignment(args.shift());
|
|
2376
|
-
let wrappers = 0;
|
|
2377
|
-
while (["env", "/usr/bin/env", "command", "exec"].includes(args[0] ?? "")) {
|
|
2378
|
-
if (wrappers++ >= 8) {
|
|
2379
|
-
note("Wrapper nesting exceeds literal discovery's eight-wrapper limit; source was not resolved.");
|
|
2556
|
+
const wrapper = args.take();
|
|
2557
|
+
if (env) {
|
|
2558
|
+
if (builtinOnly) {
|
|
2559
|
+
state.note(`Unsupported wrapper composition: ${wrapper} is not assumed to be a shell builtin.`);
|
|
2380
2560
|
return;
|
|
2381
2561
|
}
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
if (
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
note("Missing or unresolved env option argument; invoked source was not resolved.");
|
|
2392
|
-
return;
|
|
2393
|
-
}
|
|
2394
|
-
args.shift();
|
|
2395
|
-
continue;
|
|
2562
|
+
builtins = false;
|
|
2563
|
+
while (args.peek()?.startsWith("-")) {
|
|
2564
|
+
const option = args.take();
|
|
2565
|
+
if (option === "--") break;
|
|
2566
|
+
if (option === "-i" || option === "--ignore-environment") continue;
|
|
2567
|
+
if (option === "-u" || option === "--unset") {
|
|
2568
|
+
if (!args.peek() || !literal(args.peek()) || state.uncertainWord) {
|
|
2569
|
+
state.note("Missing or unresolved env option argument; invoked source was not resolved.");
|
|
2570
|
+
return;
|
|
2396
2571
|
}
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
}
|
|
2400
|
-
while (args[0] && assignment.test(args[0])) observeAssignment(args.shift());
|
|
2401
|
-
} else {
|
|
2402
|
-
if (!builtins) {
|
|
2403
|
-
note(`Unsupported wrapper composition: ${wrapper} after an external launcher is not assumed to be a shell builtin.`);
|
|
2404
|
-
return;
|
|
2405
|
-
}
|
|
2406
|
-
if (wrapper === "exec") builtins = false;
|
|
2407
|
-
if (args[0] === "--") args.shift();
|
|
2408
|
-
else if (args[0]?.startsWith("-")) {
|
|
2409
|
-
note(`Unsupported ${wrapper} option; invoked script source was not resolved.`);
|
|
2410
|
-
return;
|
|
2572
|
+
args.take();
|
|
2573
|
+
continue;
|
|
2411
2574
|
}
|
|
2575
|
+
state.note("Unsupported env option; invoked script source could not be resolved.");
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2578
|
+
while (args.peek() && assignment.test(args.peek())) state.observeAssignment(args.take());
|
|
2579
|
+
} else {
|
|
2580
|
+
if (!builtins) {
|
|
2581
|
+
state.note(`Unsupported wrapper composition: ${wrapper} after an external launcher is not assumed to be a shell builtin.`);
|
|
2582
|
+
return;
|
|
2412
2583
|
}
|
|
2413
|
-
|
|
2414
|
-
|
|
2584
|
+
builtinOnly = wrapper === "builtin";
|
|
2585
|
+
if (wrapper === "exec") builtins = false;
|
|
2586
|
+
if (args.peek() === "--") args.take();
|
|
2587
|
+
else if (args.peek()?.startsWith("-")) {
|
|
2588
|
+
const text = `Unsupported ${wrapper} option; invoked script source was not resolved.`;
|
|
2589
|
+
if (builtins && !(wrapper === "command" && ["-v", "-V"].includes(args.peek()))) {
|
|
2590
|
+
state.invalidateCwd(`${text} The working directory is unresolved.`);
|
|
2591
|
+
} else state.note(text);
|
|
2415
2592
|
return;
|
|
2416
2593
|
}
|
|
2417
2594
|
}
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
if (executable.startsWith("-")) {
|
|
2421
|
-
note("Wrapper options prevent script discovery.");
|
|
2595
|
+
if (!args.peek()) {
|
|
2596
|
+
state.note(`No command operand after ${wrapper}; invoked script source was not resolved.`);
|
|
2422
2597
|
return;
|
|
2423
2598
|
}
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2599
|
+
}
|
|
2600
|
+
const executable = args.take();
|
|
2601
|
+
if (!executable) return;
|
|
2602
|
+
if (executable.startsWith("-")) {
|
|
2603
|
+
state.note("Wrapper options prevent script discovery.");
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
return { executable, builtins, builtinOnly };
|
|
2607
|
+
}
|
|
2608
|
+
function builtin(executable, args, state, next) {
|
|
2609
|
+
if (["export", "readonly", "declare", "typeset"].includes(executable)) {
|
|
2610
|
+
let unsupported = state.uncertainWord;
|
|
2611
|
+
for (const word of args.values()) {
|
|
2612
|
+
state.observeAssignment(word);
|
|
2613
|
+
unsupported ||= !literal(word) || word.startsWith("-") && word !== "--" || word.startsWith("+");
|
|
2430
2614
|
}
|
|
2431
|
-
if (
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2615
|
+
if (unsupported) {
|
|
2616
|
+
state.invalidateCwd(`Unsupported ${executable} operands or options may change shell state; the working directory is unresolved.`);
|
|
2617
|
+
}
|
|
2618
|
+
return true;
|
|
2619
|
+
}
|
|
2620
|
+
if (["pushd", "popd"].includes(executable)) {
|
|
2621
|
+
state.invalidateCwd("Directory-stack operations are outside literal discovery; the working directory is unresolved.");
|
|
2622
|
+
return true;
|
|
2623
|
+
}
|
|
2624
|
+
if (executable === "cd") {
|
|
2625
|
+
if (args.peek() === "--") args.take();
|
|
2626
|
+
const target = args.peek();
|
|
2627
|
+
const searchesCdpath = target && !path5.isAbsolute(target) && !/^\.\.?(?:\/|$)/.test(target);
|
|
2628
|
+
if (state.previousOperator === "|" || next === "|") {
|
|
2629
|
+
state.invalidateCwd("Pipeline directory changes do not establish a reliable cwd for later script targets.");
|
|
2630
|
+
} else if (args.length === 1 && target && !target.startsWith("-") && literal(target) && !state.uncertainWord && !(state.explicitCdpath && searchesCdpath) && next === "&&" && state.cwd) {
|
|
2631
|
+
state.cwd = path5.resolve(state.cwd, target);
|
|
2632
|
+
state.conditionalCwd = true;
|
|
2633
|
+
} else {
|
|
2634
|
+
state.invalidateCwd("Working directory after cd is unresolved; relative script targets will not be guessed.");
|
|
2635
|
+
}
|
|
2636
|
+
return true;
|
|
2637
|
+
}
|
|
2638
|
+
if (executable === "source" || executable === ".") {
|
|
2639
|
+
if (args.peek() === "--") args.take();
|
|
2640
|
+
const target = args.peek();
|
|
2641
|
+
if (target?.startsWith("-")) state.note("Unsupported sourcing option; source target was not resolved.");
|
|
2642
|
+
else if (target && !target.includes("/")) state.note("Bare source operand uses PATH lookup; its file was not resolved.");
|
|
2643
|
+
else if (target) state.add(target);
|
|
2644
|
+
state.invalidateCwd("Sourced code may change the shell environment or working directory.");
|
|
2645
|
+
return true;
|
|
2646
|
+
}
|
|
2647
|
+
if (["eval", "read", "readarray", "mapfile", "getopts", "let", "set", "shopt", "alias", "unalias", "enable", "unset"].includes(executable)) {
|
|
2648
|
+
state.invalidateCwd(`In-process shell state change through ${executable} is outside literal discovery; the working directory is unresolved.`);
|
|
2649
|
+
return true;
|
|
2650
|
+
}
|
|
2651
|
+
if (["if", "then", "else", "fi", "for", "while", "until", "do", "done", "case", "function", "{"].includes(executable)) {
|
|
2652
|
+
state.invalidateCwd("Shell control flow is outside literal discovery; script coverage is incomplete.");
|
|
2653
|
+
return true;
|
|
2654
|
+
}
|
|
2655
|
+
return false;
|
|
2656
|
+
}
|
|
2657
|
+
function reader(program, args, state) {
|
|
2658
|
+
const operands = [];
|
|
2659
|
+
let options = true;
|
|
2660
|
+
while (args.length) {
|
|
2661
|
+
const arg = args.take();
|
|
2662
|
+
if (options && arg === "--") {
|
|
2663
|
+
options = false;
|
|
2664
|
+
continue;
|
|
2665
|
+
}
|
|
2666
|
+
if (options && arg.startsWith("-") && arg !== "-") {
|
|
2667
|
+
if (program === "cat" && (/^-[AbEenstTuv]+$/.test(arg) || ["--show-all", "--number-nonblank", "--show-ends", "--number", "--squeeze-blank", "--show-tabs", "--show-nonprinting"].includes(arg))) continue;
|
|
2668
|
+
if (program === "head") {
|
|
2669
|
+
if (/^-[qvz]+$/.test(arg) || ["--quiet", "--silent", "--verbose", "--zero-terminated"].includes(arg)) continue;
|
|
2670
|
+
if (["-n", "-c", "--lines", "--bytes"].includes(arg) && /^-?\d+[kKMGTPEZY]?(?:B|iB)?$/.test(args.peek() ?? "")) {
|
|
2671
|
+
args.take();
|
|
2672
|
+
continue;
|
|
2673
|
+
}
|
|
2674
|
+
if (/^(?:-[nc]?|--(?:lines|bytes)=)-?\d+[kKMGTPEZY]?(?:B|iB)?$/.test(arg)) continue;
|
|
2444
2675
|
}
|
|
2676
|
+
state.note(`Unsupported ${program} option; file operands were not resolved.`);
|
|
2445
2677
|
return;
|
|
2446
2678
|
}
|
|
2447
|
-
if (
|
|
2448
|
-
|
|
2449
|
-
|
|
2679
|
+
if (arg === "-") state.note(`${program} reads stdin; no file was resolved for that operand.`);
|
|
2680
|
+
else operands.push(arg);
|
|
2681
|
+
}
|
|
2682
|
+
operands.forEach((operand) => state.add(operand));
|
|
2683
|
+
}
|
|
2684
|
+
function interpreter(isPython, args, state) {
|
|
2685
|
+
while (args.length) {
|
|
2686
|
+
const arg = args.take();
|
|
2687
|
+
if (arg === "--") {
|
|
2688
|
+
const operand = args.peek();
|
|
2689
|
+
if (!operand || isPython && operand === "-") state.note("Interpreter reads stdin; no separate script file was resolved.");
|
|
2690
|
+
else if (!isPython && operand === "-") state.note("Shell '-' operand after -- is outside supported script discovery.");
|
|
2691
|
+
else state.add(operand);
|
|
2450
2692
|
return;
|
|
2451
2693
|
}
|
|
2452
|
-
if (!
|
|
2453
|
-
note("
|
|
2694
|
+
if (!isPython && arg === "-") {
|
|
2695
|
+
state.note("Shell '-' invocation is outside supported script discovery.");
|
|
2454
2696
|
return;
|
|
2455
2697
|
}
|
|
2456
|
-
if (
|
|
2457
|
-
|
|
2458
|
-
let options = true;
|
|
2459
|
-
for (let i = 0; i < args.length; i++) {
|
|
2460
|
-
const arg = args[i];
|
|
2461
|
-
if (options && arg === "--") {
|
|
2462
|
-
options = false;
|
|
2463
|
-
continue;
|
|
2464
|
-
}
|
|
2465
|
-
if (options && arg.startsWith("-") && arg !== "-") {
|
|
2466
|
-
if (base === "cat" && (/^-[AbEenstTuv]+$/.test(arg) || ["--show-all", "--number-nonblank", "--show-ends", "--number", "--squeeze-blank", "--show-tabs", "--show-nonprinting"].includes(arg))) continue;
|
|
2467
|
-
if (base === "head") {
|
|
2468
|
-
if (/^-[qvz]+$/.test(arg) || ["--quiet", "--silent", "--verbose", "--zero-terminated"].includes(arg)) continue;
|
|
2469
|
-
if (["-n", "-c", "--lines", "--bytes"].includes(arg) && /^-?\d+[kKMGTPEZY]?(?:B|iB)?$/.test(args[i + 1] ?? "")) {
|
|
2470
|
-
i++;
|
|
2471
|
-
continue;
|
|
2472
|
-
}
|
|
2473
|
-
if (/^(?:-[nc]?|--(?:lines|bytes)=)-?\d+[kKMGTPEZY]?(?:B|iB)?$/.test(arg)) continue;
|
|
2474
|
-
}
|
|
2475
|
-
note(`Unsupported ${base} option; file operands were not resolved.`);
|
|
2476
|
-
return;
|
|
2477
|
-
}
|
|
2478
|
-
if (arg === "-") note(`${base} reads stdin; no file was resolved for that operand.`);
|
|
2479
|
-
else operands.push(arg);
|
|
2480
|
-
}
|
|
2481
|
-
operands.forEach((operand) => add2(operand));
|
|
2698
|
+
if (arg === "-" || !isPython && /^-[abefhiklmnptuvxBCEHPTs]*s[abefhiklmnptuvxBCEHPTs]*$/.test(arg)) {
|
|
2699
|
+
state.note("Interpreter reads stdin; no separate script file was resolved.");
|
|
2482
2700
|
return;
|
|
2483
2701
|
}
|
|
2484
|
-
if (
|
|
2485
|
-
const
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
return;
|
|
2498
|
-
}
|
|
2499
|
-
if (arg === "-" || !isPython && /^-[abefhiklmnptuvxBCEHPTs]*s[abefhiklmnptuvxBCEHPTs]*$/.test(arg)) {
|
|
2500
|
-
note("Interpreter reads stdin; no separate script file was resolved.");
|
|
2501
|
-
return;
|
|
2502
|
-
}
|
|
2503
|
-
if (isPython && arg === "-c" || !isPython && /^-[abefhiklmnptuvxBCEHPT]*c$/.test(arg)) {
|
|
2504
|
-
if (!args[i + 1] || !literal(args[i + 1]) || uncertainWord) note("Inline interpreter code contains unresolved expansion or is missing; its runtime contents are unavailable.");
|
|
2505
|
-
if (!isPython && args[i + 1] && literal(args[i + 1]) && !uncertainWord) {
|
|
2506
|
-
const nested = discover(args[i + 1], explicitCdpath ? null : currentCwd, depth + 1);
|
|
2507
|
-
references.push(...nested.references);
|
|
2508
|
-
nested.limitations.forEach(note);
|
|
2509
|
-
if (nested.references.length) qualifySnapshot();
|
|
2510
|
-
}
|
|
2511
|
-
return;
|
|
2512
|
-
}
|
|
2513
|
-
if (isPython && (arg === "-m" || arg.startsWith("-m") || arg.startsWith("-c"))) {
|
|
2514
|
-
if (!arg.startsWith("-c")) note("Python module execution: module source is not resolved by direct-script discovery.");
|
|
2515
|
-
return;
|
|
2516
|
-
}
|
|
2517
|
-
if (isPython && (arg === "-W" || arg === "-X") || !isPython && (arg === "-o" || arg === "+o")) {
|
|
2518
|
-
if (!args[i + 1] || !literal(args[i + 1]) || uncertainWord) {
|
|
2519
|
-
note(`Interpreter option ${arg} has a missing or unresolved argument; script source was not resolved.`);
|
|
2520
|
-
return;
|
|
2521
|
-
}
|
|
2522
|
-
i++;
|
|
2523
|
-
continue;
|
|
2524
|
-
}
|
|
2525
|
-
if (arg.startsWith("-") || !isPython && arg.startsWith("+")) {
|
|
2526
|
-
const known = isPython ? /^-[bBdEIOPqRsSuv]+$/.test(arg) || /^-(W|X).+/.test(arg) : /^[-+][abefhiklmnptuvxBCEHPT]+$/.test(arg) || ["--noprofile", "--norc"].includes(arg);
|
|
2527
|
-
if (known) continue;
|
|
2528
|
-
note(`Interpreter option ${arg} is outside supported script discovery.`);
|
|
2529
|
-
return;
|
|
2530
|
-
}
|
|
2531
|
-
add2(arg);
|
|
2702
|
+
if (isPython && arg === "-c" || !isPython && /^-[abefhiklmnptuvxBCEHPT]*c$/.test(arg)) {
|
|
2703
|
+
const code = args.peek();
|
|
2704
|
+
if (!code || !literal(code) || state.uncertainWord) state.note("Inline interpreter code contains unresolved expansion or is missing; its runtime contents are unavailable.");
|
|
2705
|
+
if (!isPython && code && literal(code) && !state.uncertainWord) state.nested(code);
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
if (isPython && (arg === "-m" || arg.startsWith("-m") || arg.startsWith("-c"))) {
|
|
2709
|
+
if (!arg.startsWith("-c")) state.note("Python module execution: module source is not resolved by direct-script discovery.");
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
if (isPython && (arg === "-W" || arg === "-X") || !isPython && (arg === "-o" || arg === "+o")) {
|
|
2713
|
+
if (!args.peek() || !literal(args.peek()) || state.uncertainWord) {
|
|
2714
|
+
state.note(`Interpreter option ${arg} has a missing or unresolved argument; script source was not resolved.`);
|
|
2532
2715
|
return;
|
|
2533
2716
|
}
|
|
2534
|
-
|
|
2717
|
+
args.take();
|
|
2718
|
+
continue;
|
|
2535
2719
|
}
|
|
2536
|
-
if (
|
|
2537
|
-
|
|
2538
|
-
if (
|
|
2539
|
-
|
|
2540
|
-
else if (args[0]) add2(args[0]);
|
|
2541
|
-
currentCwd = null;
|
|
2542
|
-
note("Sourced code may change the shell environment or working directory.");
|
|
2720
|
+
if (arg.startsWith("-") || !isPython && arg.startsWith("+")) {
|
|
2721
|
+
const known = isPython ? /^-[bBdEIOPqRsSuv]+$/.test(arg) || /^-(W|X).+/.test(arg) : /^[-+][abefhiklmnptuvxBCEHPT]+$/.test(arg) || ["--noprofile", "--norc"].includes(arg);
|
|
2722
|
+
if (known) continue;
|
|
2723
|
+
state.note(`Interpreter option ${arg} is outside supported script discovery.`);
|
|
2543
2724
|
return;
|
|
2544
2725
|
}
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2726
|
+
state.add(arg);
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
function visit(args, state, next) {
|
|
2731
|
+
const words = new Words(args, state.budget);
|
|
2732
|
+
const invocation = unwrap(words, state);
|
|
2733
|
+
if (!invocation) return;
|
|
2734
|
+
const { executable, builtins, builtinOnly } = invocation;
|
|
2735
|
+
if (!literal(executable)) {
|
|
2736
|
+
const text = "Executable name contains unresolved expansion; script coverage is incomplete.";
|
|
2737
|
+
if (builtins) state.invalidateCwd(`${text} The working directory is unresolved.`);
|
|
2738
|
+
else state.note(text);
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
if (builtins && builtin(executable, words, state, next)) return;
|
|
2742
|
+
if (builtinOnly) {
|
|
2743
|
+
if ([":", "true", "false", "pwd", "echo", "printf", "test"].includes(executable)) return;
|
|
2744
|
+
state.invalidateCwd(`Unsupported builtin operand ${executable}; source and subsequent working directory were not resolved.`);
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
const program = programIdentity(executable);
|
|
2748
|
+
if (program === "cat" || program === "head") return reader(program, words, state);
|
|
2749
|
+
if (program === "python" || program === "shell") return interpreter(program === "python", words, state);
|
|
2750
|
+
if (executable.includes("/")) state.add(executable, true);
|
|
2751
|
+
else if (/\.(py|sh|bash|zsh)$/.test(executable)) state.note(`Executable ${executable} uses PATH lookup; its file was not resolved.`);
|
|
2752
|
+
else if (["sudo", "su", "ssh", "docker", "podman", "eval", "xargs", "find", "npm", "npx", "make", "uv", "poetry"].includes(executable)) {
|
|
2753
|
+
state.note(`Indirect execution through ${executable}: invoked source is not automatically discovered.`);
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
function discover(command, cwd, depth = 0, signal) {
|
|
2757
|
+
return scan(command, cwd, depth, new DiscoveryBudget(signal));
|
|
2758
|
+
}
|
|
2759
|
+
function scan(command, cwd, depth, budget) {
|
|
2760
|
+
const state = new DiscoveryState(cwd, depth, budget);
|
|
2761
|
+
const result = { references: state.references, limitations: state.limitations };
|
|
2762
|
+
try {
|
|
2763
|
+
budget.admit(command);
|
|
2764
|
+
if (depth > 4 || command.includes("\0") || /[\r\n`]/.test(command)) {
|
|
2765
|
+
state.note("Script discovery unavailable for multiline commands, backticks, NULs, or deeply nested shell strings; inspect command text directly.");
|
|
2766
|
+
return result;
|
|
2767
|
+
}
|
|
2768
|
+
if (!balancedQuotes(command, budget)) {
|
|
2769
|
+
state.note("Shell quoting could not be resolved reliably; script contents were not collected.");
|
|
2770
|
+
return result;
|
|
2549
2771
|
}
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2772
|
+
const unsupported = unsupportedShellSyntax(command, budget);
|
|
2773
|
+
if (unsupported) {
|
|
2774
|
+
state.note(unsupported);
|
|
2775
|
+
return result;
|
|
2776
|
+
}
|
|
2777
|
+
let tokens;
|
|
2778
|
+
try {
|
|
2779
|
+
tokens = (0, import_shell_quote.parse)(command, () => VARIABLE);
|
|
2780
|
+
} catch {
|
|
2781
|
+
state.note("Shell tokenization failed; script targets could not be resolved.");
|
|
2782
|
+
return result;
|
|
2783
|
+
}
|
|
2784
|
+
budget.parsed(tokens.length);
|
|
2785
|
+
if (tokens.some((t) => typeof t !== "string" && "op" in t && !["&&", "||", ";", "|", "glob"].includes(t.op))) {
|
|
2786
|
+
state.note("Shell grouping, substitution, background execution or redirection prevents reliable script discovery; inspect command text directly.");
|
|
2787
|
+
return result;
|
|
2788
|
+
}
|
|
2789
|
+
let words = [];
|
|
2790
|
+
for (const token of tokens) {
|
|
2791
|
+
budget.step();
|
|
2792
|
+
if (typeof token === "string") words.push(token);
|
|
2793
|
+
else if ("comment" in token) break;
|
|
2794
|
+
else if ("op" in token && token.op === "glob" && "pattern" in token) {
|
|
2795
|
+
words.push(String(token.pattern));
|
|
2796
|
+
state.uncertainWord = true;
|
|
2797
|
+
} else if ("op" in token && typeof token.op === "string") {
|
|
2798
|
+
visit(words, state, token.op);
|
|
2799
|
+
words = [];
|
|
2800
|
+
state.afterOperator(token.op);
|
|
2569
2801
|
}
|
|
2570
2802
|
}
|
|
2803
|
+
visit(words, state);
|
|
2804
|
+
return result;
|
|
2805
|
+
} catch (error) {
|
|
2806
|
+
if (!(error instanceof DiscoveryLimit)) throw error;
|
|
2807
|
+
const text = "Shell discovery exceeded its shared 16 MiB/65,536-token/16,384-reference/16,384-expansion/262,144-step/256-notice/500 ms work limits; remaining source targets were not resolved.";
|
|
2808
|
+
if (!state.limitations.includes(text)) state.limitations.push(text);
|
|
2809
|
+
return result;
|
|
2571
2810
|
}
|
|
2572
|
-
visit();
|
|
2573
|
-
return { references, limitations };
|
|
2574
2811
|
}
|
|
2812
|
+
|
|
2813
|
+
// src/evidence.ts
|
|
2575
2814
|
async function capture(reference, budget, signal, scope) {
|
|
2576
2815
|
const result = { filename: reference.filename, status: "unavailable" };
|
|
2577
|
-
if (!reference.cwd && !
|
|
2578
|
-
const filename =
|
|
2816
|
+
if (!reference.cwd && !path6.isAbsolute(reference.filename)) return { ...result, status: "working directory unresolved; contents not provided" };
|
|
2817
|
+
const filename = path6.isAbsolute(reference.filename) ? reference.filename : `${reference.cwd}/${reference.filename}`;
|
|
2579
2818
|
result.path = filename;
|
|
2580
2819
|
signal.throwIfAborted();
|
|
2581
2820
|
try {
|
|
@@ -2632,9 +2871,10 @@ async function capture(reference, budget, signal, scope) {
|
|
|
2632
2871
|
}
|
|
2633
2872
|
async function collectEvidence(input, limits, signal, scope = fileAccess.scope(signal)) {
|
|
2634
2873
|
signal.throwIfAborted();
|
|
2874
|
+
if (input.command.length > limits.maxEvidenceBytes) throw new Error("Command exceeds configured evidence budget");
|
|
2635
2875
|
const commandBytes = Buffer.byteLength(input.command);
|
|
2636
2876
|
if (commandBytes > limits.maxEvidenceBytes) throw new Error("Command exceeds configured evidence budget");
|
|
2637
|
-
const discovery = discover(input.command, input.cwd);
|
|
2877
|
+
const discovery = discover(input.command, input.cwd, 0, signal);
|
|
2638
2878
|
const evidence = { ...input, kind: "shell", files: [], limitations: [
|
|
2639
2879
|
...input.limitations ?? [],
|
|
2640
2880
|
"Only literal Python/shell source and supported cat/head file operands are collected. Files are full review-time snapshots, not command output. Imports, dependencies, other runtimes and calls inside source files are not recursively inspected.",
|
|
@@ -2643,72 +2883,143 @@ async function collectEvidence(input, limits, signal, scope = fileAccess.scope(s
|
|
|
2643
2883
|
if (!input.userPrompt) evidence.limitations.push("User prompt unavailable.");
|
|
2644
2884
|
let remaining = limits.maxEvidenceBytes - commandBytes;
|
|
2645
2885
|
const limit = new FileLimit(limits.maxFiles, scope);
|
|
2646
|
-
const
|
|
2886
|
+
const declared = /* @__PURE__ */ new Map();
|
|
2647
2887
|
for (const reference of discovery.references) {
|
|
2648
2888
|
signal.throwIfAborted();
|
|
2649
|
-
const filename =
|
|
2889
|
+
const filename = path6.isAbsolute(reference.filename) ? reference.filename : reference.cwd ? `${reference.cwd}/${reference.filename}` : null;
|
|
2890
|
+
const key = filename ?? /* @__PURE__ */ Symbol();
|
|
2891
|
+
const previous = declared.get(key);
|
|
2892
|
+
if (previous) previous.executable &&= reference.executable;
|
|
2893
|
+
else declared.set(key, { ...reference });
|
|
2894
|
+
}
|
|
2895
|
+
const selected = /* @__PURE__ */ new Map();
|
|
2896
|
+
for (const reference of declared.values()) {
|
|
2897
|
+
signal.throwIfAborted();
|
|
2898
|
+
const filename = path6.isAbsolute(reference.filename) ? reference.filename : reference.cwd ? `${reference.cwd}/${reference.filename}` : null;
|
|
2650
2899
|
const { key, withinLimit } = await limit.consider(filename, signal);
|
|
2651
2900
|
const previous = selected.get(key);
|
|
2652
2901
|
if (previous) {
|
|
2902
|
+
const promote = previous.reference.executable && !reference.executable;
|
|
2653
2903
|
previous.reference.executable &&= reference.executable;
|
|
2654
|
-
if (filename && filename !== previous.filename
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2904
|
+
if (filename && filename !== previous.filename) previous.aliases.add(filename);
|
|
2905
|
+
if (promote && previous.withinLimit && previous.file.status === "direct executable is not identifiable as Python/shell source; contents not provided") {
|
|
2906
|
+
const file2 = await capture(previous.reference, remaining, signal, scope);
|
|
2907
|
+
Object.assign(previous.file, file2);
|
|
2908
|
+
if (file2.contents !== void 0) {
|
|
2909
|
+
delete previous.file.warning;
|
|
2910
|
+
remaining -= Buffer.byteLength(file2.contents);
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2661
2913
|
continue;
|
|
2662
2914
|
}
|
|
2663
|
-
const file = await capture(reference, remaining, signal, scope);
|
|
2664
|
-
if (aliases.length) file.aliases = aliases;
|
|
2915
|
+
const file = withinLimit ? await capture(reference, remaining, signal, scope) : { filename: reference.filename, ...filename ? { path: filename } : {}, status: "file-count limit reached" };
|
|
2665
2916
|
if (file.contents === void 0) file.warning = omittedFile(file.path ?? file.filename);
|
|
2917
|
+
selected.set(key, { reference, filename, withinLimit, aliases: /* @__PURE__ */ new Set(), file });
|
|
2666
2918
|
evidence.files.push(file);
|
|
2667
2919
|
remaining -= Buffer.byteLength(file.contents ?? "");
|
|
2668
2920
|
}
|
|
2921
|
+
for (const { file, aliases } of selected.values()) if (aliases.size) file.aliases = [...aliases];
|
|
2669
2922
|
signal.throwIfAborted();
|
|
2670
2923
|
evidence.limitations.push(...limit.limitations);
|
|
2671
2924
|
return evidence;
|
|
2672
2925
|
}
|
|
2673
2926
|
async function collectEditEvidence(input, limits, signal, scope = fileAccess.scope(signal)) {
|
|
2674
|
-
signal
|
|
2675
|
-
const
|
|
2927
|
+
reviewStage(signal, "Evidence collection");
|
|
2928
|
+
const metadata = input.permission.metadata;
|
|
2676
2929
|
const limitations = [
|
|
2677
2930
|
...input.limitations,
|
|
2678
2931
|
"Proposed diffs come from the pending host permission, not from applying edits. Full files, dependencies and post-approval formatter changes are not inspected; host diffs may normalize whitespace or omit BOMs."
|
|
2679
2932
|
];
|
|
2680
2933
|
const record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2934
|
+
const field = (value, key) => {
|
|
2935
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
2936
|
+
if (!descriptor) return void 0;
|
|
2937
|
+
if (!Object.hasOwn(descriptor, "value")) throw new Error("Native edit metadata contains an accessor");
|
|
2938
|
+
return descriptor.value;
|
|
2939
|
+
};
|
|
2940
|
+
let headerBytes = 16 * 1024 * 1024;
|
|
2941
|
+
const text = (value) => {
|
|
2942
|
+
if (typeof value !== "string") return;
|
|
2943
|
+
reviewStage(signal, "Evidence collection");
|
|
2944
|
+
if (value.length > headerBytes) throw new Error("Native edit scope exceeds the 16 MiB normalization limit");
|
|
2945
|
+
headerBytes -= Buffer.byteLength(value);
|
|
2946
|
+
if (headerBytes < 0) throw new Error("Native edit scope exceeds the 16 MiB normalization limit");
|
|
2947
|
+
return value;
|
|
2948
|
+
};
|
|
2949
|
+
const patterns = (values) => {
|
|
2950
|
+
if (values.length > 16384) throw new Error("Native edit scope exceeds the 16,384-entry normalization limit");
|
|
2951
|
+
const result = [];
|
|
2952
|
+
for (let index = 0; index < values.length; index++) {
|
|
2953
|
+
const value = text(field(values, String(index)));
|
|
2954
|
+
if (value === void 0) throw new Error("Native edit scope contains an invalid pattern");
|
|
2955
|
+
result.push(value);
|
|
2956
|
+
}
|
|
2957
|
+
return result;
|
|
2958
|
+
};
|
|
2959
|
+
const scopeText = (value) => {
|
|
2960
|
+
const result = text(value);
|
|
2961
|
+
if (result === void 0) throw new Error("Native edit scope contains invalid identity text");
|
|
2962
|
+
return result;
|
|
2963
|
+
};
|
|
2964
|
+
const permissionScope = {
|
|
2965
|
+
id: scopeText(input.permission.id),
|
|
2966
|
+
type: scopeText(input.permission.type),
|
|
2967
|
+
patterns: patterns(input.permission.patterns),
|
|
2968
|
+
always: patterns(input.permission.always),
|
|
2969
|
+
tool: input.permission.tool ? { messageID: scopeText(input.permission.tool.messageID), callID: scopeText(input.permission.tool.callID) } : null
|
|
2970
|
+
};
|
|
2681
2971
|
const patch = input.tool === "apply_patch";
|
|
2682
|
-
const
|
|
2972
|
+
const hostFiles = patch ? field(record2(metadata), "files") : void 0;
|
|
2973
|
+
const files = patch ? Array.isArray(hostFiles) ? hostFiles : [] : [metadata];
|
|
2974
|
+
if (files.length > 16384) throw new Error("Native edit changes exceed the 16,384-entry normalization limit");
|
|
2683
2975
|
if (!files.length) limitations.push("Per-file patch metadata unavailable; affected changes could not be enumerated.");
|
|
2684
2976
|
let remaining = limits.maxEvidenceBytes;
|
|
2977
|
+
let measurementUnits = 16 * 1024 * 1024;
|
|
2685
2978
|
const changes = [];
|
|
2686
2979
|
const limit = new FileLimit(limits.maxFiles, scope);
|
|
2980
|
+
const countsBudget = new DiffBudget();
|
|
2687
2981
|
for (let index = 0; index < files.length; index++) {
|
|
2688
|
-
signal
|
|
2689
|
-
const file = record2(files
|
|
2690
|
-
const
|
|
2982
|
+
reviewStage(signal, "Evidence collection");
|
|
2983
|
+
const file = record2(patch ? field(files, String(index)) : metadata);
|
|
2984
|
+
const type = patch ? field(file, "type") : input.tool;
|
|
2985
|
+
const operation = typeof type === "string" && ["add", "update", "delete", "move", "edit", "write"].includes(type) ? type : "unknown";
|
|
2986
|
+
const filePath = text(field(file, patch ? "filePath" : "filepath"));
|
|
2987
|
+
const movePath = patch ? text(field(file, "movePath")) : void 0;
|
|
2988
|
+
const diff = field(file, patch ? "patch" : "diff");
|
|
2691
2989
|
const change = {
|
|
2692
|
-
path:
|
|
2990
|
+
path: filePath?.trim() ? filePath : null,
|
|
2693
2991
|
operation,
|
|
2694
2992
|
status: "omitted",
|
|
2695
|
-
...
|
|
2993
|
+
...movePath !== void 0 ? { movePath } : {}
|
|
2696
2994
|
};
|
|
2697
|
-
const { withinLimit } = await limit.consider(change.path &&
|
|
2995
|
+
const { withinLimit } = await limit.consider(change.path && path6.isAbsolute(change.path) ? change.path : null, signal);
|
|
2698
2996
|
if (!withinLimit) change.reason = "file-count limit reached";
|
|
2699
|
-
else if (!change.path || !
|
|
2997
|
+
else if (!change.path || !path6.isAbsolute(change.path)) change.reason = "absolute target path unavailable";
|
|
2700
2998
|
else if (operation === "unknown" || patch && (operation === "edit" || operation === "write")) change.reason = "file operation unavailable or unsupported";
|
|
2701
|
-
else if (operation === "move" && (!change.movePath || !
|
|
2702
|
-
else if (typeof
|
|
2703
|
-
else if (
|
|
2999
|
+
else if (operation === "move" && (!change.movePath || !path6.isAbsolute(change.movePath))) change.reason = "absolute move destination unavailable";
|
|
3000
|
+
else if (typeof diff !== "string" || !diff.length) change.reason = "proposed diff unavailable";
|
|
3001
|
+
else if (diff.length > remaining) change.reason = "complete diff exceeds remaining evidence byte budget";
|
|
3002
|
+
else if (diff.length > measurementUnits) change.reason = "shared diff UTF-8 measurement work limit reached";
|
|
2704
3003
|
else {
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
remaining
|
|
3004
|
+
measurementUnits -= diff.length;
|
|
3005
|
+
const bytes = Buffer.byteLength(diff);
|
|
3006
|
+
if (bytes > remaining) change.reason = "complete diff exceeds remaining evidence byte budget";
|
|
3007
|
+
else if (!diff.trim()) change.reason = "proposed diff unavailable";
|
|
3008
|
+
else {
|
|
3009
|
+
change.status = "included";
|
|
3010
|
+
change.diff = diff;
|
|
3011
|
+
remaining -= bytes;
|
|
3012
|
+
}
|
|
2708
3013
|
}
|
|
2709
3014
|
if (change.status === "omitted") {
|
|
2710
3015
|
change.warning = omittedFile(change.path);
|
|
2711
|
-
|
|
3016
|
+
let counts = diffDelta(diff, signal, countsBudget);
|
|
3017
|
+
if (!counts) {
|
|
3018
|
+
const added = field(file, "additions"), removed = field(file, "deletions");
|
|
3019
|
+
if (Number.isSafeInteger(added) && Number(added) >= 0 && Number.isSafeInteger(removed) && Number(removed) >= 0) {
|
|
3020
|
+
counts = { added: Number(added), removed: Number(removed) };
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
2712
3023
|
if (counts) change.delta = `[\u0394] ${JSON.stringify(change.path)}: +${counts.added} \u2212${counts.removed} lines`;
|
|
2713
3024
|
}
|
|
2714
3025
|
changes.push(change);
|
|
@@ -2717,7 +3028,8 @@ async function collectEditEvidence(input, limits, signal, scope = fileAccess.sco
|
|
|
2717
3028
|
if (partial) limitations.push("Edit evidence is incomplete. Omitted changes are not assessed by the supplied diffs; do not assume the whole proposal is safe.");
|
|
2718
3029
|
if (!input.userPrompt) limitations.push("User prompt unavailable.");
|
|
2719
3030
|
limitations.push(...limit.limitations);
|
|
2720
|
-
|
|
3031
|
+
if (countsBudget.exhausted) limitations.push("Some omitted-diff line counts exceeded the shared 16 MiB/65,536-line/250 ms counting allowance; valid host counts were used when available.");
|
|
3032
|
+
reviewStage(signal, "Evidence collection");
|
|
2721
3033
|
return {
|
|
2722
3034
|
kind: "edit",
|
|
2723
3035
|
tool: input.tool,
|
|
@@ -2775,10 +3087,10 @@ function patchOperations(input, signal) {
|
|
|
2775
3087
|
update = void 0;
|
|
2776
3088
|
const kind = line.startsWith("*** Add File:") ? "add" : line.startsWith("*** Delete File:") ? "delete" : line.startsWith("*** Update File:") ? "update" : void 0;
|
|
2777
3089
|
if (!kind) continue;
|
|
2778
|
-
const
|
|
2779
|
-
if (!
|
|
3090
|
+
const path11 = line.slice(line.indexOf(":") + 1).trim();
|
|
3091
|
+
if (!path11) throw new Error("Patch operation summary contains an empty target path");
|
|
2780
3092
|
if (operations.length >= 16384) throw new Error("Patch operation summary exceeds bounded JSON structure limits");
|
|
2781
|
-
const operation = { operation: kind, path:
|
|
3093
|
+
const operation = { operation: kind, path: path11 };
|
|
2782
3094
|
operations.push(operation);
|
|
2783
3095
|
if (kind === "update") update = operation;
|
|
2784
3096
|
}
|
|
@@ -2809,7 +3121,7 @@ function boundedCopy(value, budget, valueBudget = 16384) {
|
|
|
2809
3121
|
} else add2(code >= 55296 && code <= 57343 ? 6 : 3);
|
|
2810
3122
|
}
|
|
2811
3123
|
};
|
|
2812
|
-
const
|
|
3124
|
+
const visit2 = (value2, depth) => {
|
|
2813
3125
|
if (++nodes > valueBudget || depth > 32) throw new Error("Operation evidence exceeds bounded JSON structure limits");
|
|
2814
3126
|
if (value2 === null) {
|
|
2815
3127
|
add2(4);
|
|
@@ -2835,7 +3147,7 @@ function boundedCopy(value, budget, valueBudget = 16384) {
|
|
|
2835
3147
|
const descriptor = Object.getOwnPropertyDescriptor(value2, String(index));
|
|
2836
3148
|
if (!descriptor || !Object.hasOwn(descriptor, "value")) throw new Error("Operation evidence contains an array hole or accessor");
|
|
2837
3149
|
if (count3++) add2(1);
|
|
2838
|
-
entries.push(
|
|
3150
|
+
entries.push(visit2(descriptor.value, depth + 1));
|
|
2839
3151
|
}
|
|
2840
3152
|
} else {
|
|
2841
3153
|
for (const key in value2) {
|
|
@@ -2845,13 +3157,13 @@ function boundedCopy(value, budget, valueBudget = 16384) {
|
|
|
2845
3157
|
if (count3++) add2(1);
|
|
2846
3158
|
text(key);
|
|
2847
3159
|
add2(1);
|
|
2848
|
-
Object.defineProperty(result, key, { value:
|
|
3160
|
+
Object.defineProperty(result, key, { value: visit2(descriptor.value, depth + 1), enumerable: true, writable: true, configurable: true });
|
|
2849
3161
|
}
|
|
2850
3162
|
}
|
|
2851
3163
|
ancestors.delete(value2);
|
|
2852
3164
|
return result;
|
|
2853
3165
|
};
|
|
2854
|
-
return { value:
|
|
3166
|
+
return { value: visit2(value, 0), bytes, values: nodes };
|
|
2855
3167
|
}
|
|
2856
3168
|
function collectToolEvidence(context, limits, signal) {
|
|
2857
3169
|
signal.throwIfAborted();
|
|
@@ -2911,39 +3223,38 @@ function collectDirectoryEvidence(context, limits, signal) {
|
|
|
2911
3223
|
}
|
|
2912
3224
|
|
|
2913
3225
|
// src/evaluate.ts
|
|
2914
|
-
async function evaluateEvidence(request,
|
|
3226
|
+
async function evaluateEvidence(request, reader2, options, config, configError, signal, onIdentified, files) {
|
|
2915
3227
|
if (!candidateEnabled(request, options)) return null;
|
|
2916
3228
|
reviewStage(signal, "Permission context lookup");
|
|
2917
|
-
const invocation = await loadInvocation(request,
|
|
3229
|
+
const invocation = await loadInvocation(request, reader2, signal);
|
|
2918
3230
|
let ids;
|
|
2919
3231
|
try {
|
|
2920
3232
|
ids = await withDeadline(signal, Math.min(1500, remainingTime(signal) / 4), async (s) => {
|
|
2921
|
-
const value = await
|
|
3233
|
+
const value = await reader2.toolIDs?.(s);
|
|
2922
3234
|
if (!value || value.length > 16384 || !value.every((id) => typeof id === "string")) throw new Error("Tool registry unavailable");
|
|
2923
3235
|
return value;
|
|
2924
3236
|
}, "Tool registry lookup");
|
|
2925
3237
|
} catch {
|
|
2926
3238
|
signal.throwIfAborted();
|
|
2927
3239
|
}
|
|
2928
|
-
const classified = classify(request, invocation, ids,
|
|
3240
|
+
const classified = classify(request, invocation, ids, reader2.mcpServers?.() ?? []);
|
|
2929
3241
|
if (!classified) return null;
|
|
2930
3242
|
if (!enabledKind(classified.kind, options)) return null;
|
|
2931
3243
|
onIdentified();
|
|
2932
3244
|
if (!config) throw new Error(configError || "Invalid configuration");
|
|
2933
3245
|
const scope = files.scope(signal);
|
|
2934
|
-
const cachedReader = { ...reader, message: async () => invocation.message };
|
|
2935
3246
|
if (classified.kind === "shell") {
|
|
2936
|
-
const context = await loadContext(request,
|
|
3247
|
+
const context = await loadContext(request, reader2, signal, scope, invocation);
|
|
2937
3248
|
if (!context) throw new Error("Native shell context unavailable");
|
|
2938
3249
|
reviewStage(signal, "Evidence collection");
|
|
2939
3250
|
return collectEvidence(context, config, signal, scope);
|
|
2940
3251
|
}
|
|
2941
3252
|
if (classified.kind === "edit") {
|
|
2942
|
-
const context = await loadEditContext(request,
|
|
3253
|
+
const context = await loadEditContext(request, reader2, signal, invocation);
|
|
2943
3254
|
reviewStage(signal, "Evidence collection");
|
|
2944
3255
|
return collectEditEvidence(context, config, signal, scope);
|
|
2945
3256
|
}
|
|
2946
|
-
const conversation = await loadConversationContext(request,
|
|
3257
|
+
const conversation = await loadConversationContext(request, reader2, signal);
|
|
2947
3258
|
if (!invocation.location.instanceDirectory) conversation.limitations.push("Invocation directory unavailable; session origin is not substituted.");
|
|
2948
3259
|
if (!invocation.location.instanceWorktree) conversation.limitations.push("Invocation worktree unavailable.");
|
|
2949
3260
|
const common = { ...conversation, tool: invocation.tool, input: invocation.input, permission: permissionContext(request, false), location: invocation.location };
|
|
@@ -2957,7 +3268,7 @@ async function evaluateEvidence(request, reader, options, config, configError, s
|
|
|
2957
3268
|
definition = await withDeadline(
|
|
2958
3269
|
signal,
|
|
2959
3270
|
Math.min(1e3, remainingTime(signal) / 4),
|
|
2960
|
-
(s) =>
|
|
3271
|
+
(s) => reader2.definition?.(invocation.info, invocation.tool, s) ?? Promise.resolve(void 0),
|
|
2961
3272
|
"Tool definition lookup"
|
|
2962
3273
|
);
|
|
2963
3274
|
if (definition?.id !== invocation.tool) definition = void 0;
|
|
@@ -2989,11 +3300,58 @@ function approvalTransport(client, directory) {
|
|
|
2989
3300
|
};
|
|
2990
3301
|
}
|
|
2991
3302
|
|
|
3303
|
+
// src/pending-refresh.ts
|
|
3304
|
+
init_define_REVIEW_PROMPTS();
|
|
3305
|
+
var PendingRefresh = class {
|
|
3306
|
+
constructor(pending, read, parent) {
|
|
3307
|
+
this.pending = pending;
|
|
3308
|
+
this.read = read;
|
|
3309
|
+
this.signal = AbortSignal.any([parent, this.abort.signal]);
|
|
3310
|
+
}
|
|
3311
|
+
pending;
|
|
3312
|
+
read;
|
|
3313
|
+
syncing = false;
|
|
3314
|
+
stopped = false;
|
|
3315
|
+
again = false;
|
|
3316
|
+
generation = 0;
|
|
3317
|
+
abort = new AbortController();
|
|
3318
|
+
signal;
|
|
3319
|
+
async refresh() {
|
|
3320
|
+
if (this.stopped || this.signal.aborted) return;
|
|
3321
|
+
if (this.syncing) {
|
|
3322
|
+
this.again = true;
|
|
3323
|
+
return;
|
|
3324
|
+
}
|
|
3325
|
+
this.syncing = true;
|
|
3326
|
+
const generation = ++this.generation;
|
|
3327
|
+
const revision = this.pending.revision;
|
|
3328
|
+
try {
|
|
3329
|
+
const requests = await withDeadline(this.signal, 5e3, this.read, "Pending permission refresh");
|
|
3330
|
+
if (this.stopped || this.signal.aborted || this.generation !== generation || this.pending.revision !== revision) return;
|
|
3331
|
+
this.pending.reconcile(requests, revision);
|
|
3332
|
+
} catch {
|
|
3333
|
+
} finally {
|
|
3334
|
+
if (this.generation === generation) {
|
|
3335
|
+
this.syncing = false;
|
|
3336
|
+
const again = this.again;
|
|
3337
|
+
this.again = false;
|
|
3338
|
+
if (again) void this.refresh();
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
dispose() {
|
|
3343
|
+
this.stopped = true;
|
|
3344
|
+
this.again = false;
|
|
3345
|
+
this.generation++;
|
|
3346
|
+
this.abort.abort();
|
|
3347
|
+
}
|
|
3348
|
+
};
|
|
3349
|
+
|
|
2992
3350
|
// src/lifetime-view.tsx
|
|
2993
3351
|
init_define_REVIEW_PROMPTS();
|
|
2994
3352
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
2995
3353
|
import { memo as _$memo } from "@opentui/solid";
|
|
2996
|
-
import
|
|
3354
|
+
import path8 from "node:path";
|
|
2997
3355
|
import { createSignal } from "solid-js";
|
|
2998
3356
|
|
|
2999
3357
|
// src/lifetime.ts
|
|
@@ -3001,7 +3359,7 @@ init_define_REVIEW_PROMPTS();
|
|
|
3001
3359
|
import { constants as constants3 } from "node:fs";
|
|
3002
3360
|
import { mkdir, open as open3, readdir as readdir2, rename, unlink } from "node:fs/promises";
|
|
3003
3361
|
import { randomUUID } from "node:crypto";
|
|
3004
|
-
import
|
|
3362
|
+
import path7 from "node:path";
|
|
3005
3363
|
var empty = () => ({ requests: 0, tokenRequests: 0, input: 0, output: 0, priced: 0, cost: 0, since: null });
|
|
3006
3364
|
var count2 = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
3007
3365
|
var amount = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
@@ -3025,7 +3383,7 @@ var LifetimeUsage = class {
|
|
|
3025
3383
|
constructor(directory, legacyDirectory) {
|
|
3026
3384
|
this.directory = directory;
|
|
3027
3385
|
this.legacyDirectory = legacyDirectory;
|
|
3028
|
-
if (!
|
|
3386
|
+
if (!path7.isAbsolute(directory) || legacyDirectory !== void 0 && !path7.isAbsolute(legacyDirectory)) {
|
|
3029
3387
|
throw new Error("Lifetime usage directory must be absolute");
|
|
3030
3388
|
}
|
|
3031
3389
|
}
|
|
@@ -3052,7 +3410,7 @@ var LifetimeUsage = class {
|
|
|
3052
3410
|
}).then(async () => {
|
|
3053
3411
|
this.local = add(this.local, increment);
|
|
3054
3412
|
await mkdir(this.directory, { recursive: true, mode: 448 });
|
|
3055
|
-
const temporary =
|
|
3413
|
+
const temporary = path7.join(this.directory, `${this.id}.${randomUUID()}.tmp`);
|
|
3056
3414
|
try {
|
|
3057
3415
|
const file = await open3(temporary, "wx", 384);
|
|
3058
3416
|
try {
|
|
@@ -3061,7 +3419,7 @@ var LifetimeUsage = class {
|
|
|
3061
3419
|
} finally {
|
|
3062
3420
|
await file.close();
|
|
3063
3421
|
}
|
|
3064
|
-
await rename(temporary,
|
|
3422
|
+
await rename(temporary, path7.join(this.directory, `${this.id}.json`));
|
|
3065
3423
|
const directory = await open3(this.directory, constants3.O_RDONLY | constants3.O_DIRECTORY);
|
|
3066
3424
|
try {
|
|
3067
3425
|
await directory.sync();
|
|
@@ -3082,8 +3440,9 @@ var LifetimeUsage = class {
|
|
|
3082
3440
|
async totals(signal) {
|
|
3083
3441
|
signal?.throwIfAborted();
|
|
3084
3442
|
await this.flush();
|
|
3443
|
+
signal?.throwIfAborted();
|
|
3085
3444
|
let total = empty();
|
|
3086
|
-
const directories = new Set([this.directory, ...this.legacyDirectory ? [this.legacyDirectory] : []].map((directory) =>
|
|
3445
|
+
const directories = new Set([this.directory, ...this.legacyDirectory ? [this.legacyDirectory] : []].map((directory) => path7.resolve(directory)));
|
|
3087
3446
|
for (const directory of directories) total = add(total, await this.readDirectory(directory, signal));
|
|
3088
3447
|
signal?.throwIfAborted();
|
|
3089
3448
|
return total;
|
|
@@ -3094,17 +3453,22 @@ var LifetimeUsage = class {
|
|
|
3094
3453
|
if (error.code === "ENOENT") return [];
|
|
3095
3454
|
throw error;
|
|
3096
3455
|
});
|
|
3456
|
+
signal?.throwIfAborted();
|
|
3097
3457
|
let total = empty();
|
|
3098
3458
|
for (const name of names.filter((name2) => snapshotName.test(name2))) {
|
|
3099
3459
|
signal?.throwIfAborted();
|
|
3100
|
-
const file = await open3(
|
|
3460
|
+
const file = await open3(path7.join(directory, name), constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK);
|
|
3101
3461
|
try {
|
|
3462
|
+
signal?.throwIfAborted();
|
|
3102
3463
|
const stat = await file.stat();
|
|
3464
|
+
signal?.throwIfAborted();
|
|
3103
3465
|
if (!stat.isFile() || stat.size > 1024) throw new Error("Invalid lifetime usage snapshot");
|
|
3104
3466
|
const bytes = Buffer.alloc(1025);
|
|
3105
3467
|
let size = 0;
|
|
3106
3468
|
while (size < bytes.length) {
|
|
3469
|
+
signal?.throwIfAborted();
|
|
3107
3470
|
const read = await file.read(bytes, size, bytes.length - size, size);
|
|
3471
|
+
signal?.throwIfAborted();
|
|
3108
3472
|
if (!read.bytesRead) break;
|
|
3109
3473
|
size += read.bytesRead;
|
|
3110
3474
|
}
|
|
@@ -3123,6 +3487,7 @@ var LifetimeUsage = class {
|
|
|
3123
3487
|
} finally {
|
|
3124
3488
|
await file.close();
|
|
3125
3489
|
}
|
|
3490
|
+
signal?.throwIfAborted();
|
|
3126
3491
|
}
|
|
3127
3492
|
signal?.throwIfAborted();
|
|
3128
3493
|
return total;
|
|
@@ -3137,7 +3502,7 @@ function lifetimeReport(totals) {
|
|
|
3137
3502
|
return [
|
|
3138
3503
|
lifetimeCost(totals),
|
|
3139
3504
|
`${totals.requests} requests with recorded usage`,
|
|
3140
|
-
totals.tokenRequests ? `
|
|
3505
|
+
totals.tokenRequests ? `token: ${totals.input} in ${totals.output} out${totals.tokenRequests < totals.requests ? " (partial coverage)" : ""}` : "tokens unavailable",
|
|
3141
3506
|
`Token counts available: ${totals.tokenRequests}/${totals.requests} requests`,
|
|
3142
3507
|
`Pricing available: ${totals.priced}/${totals.requests} requests`,
|
|
3143
3508
|
...totals.since === null ? [] : [`Recorded since: ${new Date(totals.since).toISOString().slice(0, 10)}`],
|
|
@@ -3146,25 +3511,96 @@ function lifetimeReport(totals) {
|
|
|
3146
3511
|
].join("\n");
|
|
3147
3512
|
}
|
|
3148
3513
|
|
|
3514
|
+
// src/lifetime-refresh.ts
|
|
3515
|
+
init_define_REVIEW_PROMPTS();
|
|
3516
|
+
var LifetimeRefresh = class {
|
|
3517
|
+
constructor(read, publish, parent) {
|
|
3518
|
+
this.read = read;
|
|
3519
|
+
this.publish = publish;
|
|
3520
|
+
this.signal = AbortSignal.any([parent, this.abort.signal]);
|
|
3521
|
+
}
|
|
3522
|
+
read;
|
|
3523
|
+
publish;
|
|
3524
|
+
revision = 0;
|
|
3525
|
+
followUp = false;
|
|
3526
|
+
stopped = false;
|
|
3527
|
+
active;
|
|
3528
|
+
abort = new AbortController();
|
|
3529
|
+
signal;
|
|
3530
|
+
refresh() {
|
|
3531
|
+
if (this.stopped || this.signal.aborted) return;
|
|
3532
|
+
this.revision++;
|
|
3533
|
+
if (this.active) {
|
|
3534
|
+
this.followUp = true;
|
|
3535
|
+
return;
|
|
3536
|
+
}
|
|
3537
|
+
this.start();
|
|
3538
|
+
}
|
|
3539
|
+
/** A failed write invalidates active results and requests queued before it. */
|
|
3540
|
+
failed() {
|
|
3541
|
+
if (this.stopped || this.signal.aborted) return;
|
|
3542
|
+
this.revision++;
|
|
3543
|
+
this.followUp = false;
|
|
3544
|
+
this.publish(void 0);
|
|
3545
|
+
}
|
|
3546
|
+
start() {
|
|
3547
|
+
if (this.stopped || this.signal.aborted) return;
|
|
3548
|
+
this.followUp = false;
|
|
3549
|
+
const transaction = { revision: this.revision, settled: true, finished: false };
|
|
3550
|
+
this.active = transaction;
|
|
3551
|
+
void this.run(transaction);
|
|
3552
|
+
}
|
|
3553
|
+
live(transaction) {
|
|
3554
|
+
return !this.stopped && !this.signal.aborted && this.active === transaction;
|
|
3555
|
+
}
|
|
3556
|
+
async run(transaction) {
|
|
3557
|
+
try {
|
|
3558
|
+
const totals = await withDeadline(this.signal, 5e3, (signal) => {
|
|
3559
|
+
transaction.settled = false;
|
|
3560
|
+
const worker = Promise.resolve().then(() => {
|
|
3561
|
+
signal.throwIfAborted();
|
|
3562
|
+
return this.read(signal);
|
|
3563
|
+
});
|
|
3564
|
+
const settled = () => {
|
|
3565
|
+
transaction.settled = true;
|
|
3566
|
+
this.release(transaction);
|
|
3567
|
+
};
|
|
3568
|
+
void worker.then(settled, settled);
|
|
3569
|
+
return worker;
|
|
3570
|
+
}, "Lifetime usage loading");
|
|
3571
|
+
if (this.live(transaction) && this.revision === transaction.revision) this.publish(totals);
|
|
3572
|
+
} catch {
|
|
3573
|
+
if (this.live(transaction)) this.publish(void 0);
|
|
3574
|
+
} finally {
|
|
3575
|
+
transaction.finished = true;
|
|
3576
|
+
this.release(transaction);
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
release(transaction) {
|
|
3580
|
+
if (this.active !== transaction || !transaction.finished || !transaction.settled) return;
|
|
3581
|
+
this.active = void 0;
|
|
3582
|
+
if (this.followUp) this.start();
|
|
3583
|
+
}
|
|
3584
|
+
dispose() {
|
|
3585
|
+
this.stopped = true;
|
|
3586
|
+
this.followUp = false;
|
|
3587
|
+
this.revision++;
|
|
3588
|
+
this.abort.abort();
|
|
3589
|
+
}
|
|
3590
|
+
};
|
|
3591
|
+
|
|
3149
3592
|
// src/lifetime-view.tsx
|
|
3150
3593
|
function lifetimeTracker(api) {
|
|
3151
|
-
const directory =
|
|
3152
|
-
const store = new LifetimeUsage(
|
|
3594
|
+
const directory = path8.join(api.state.path.state, "opencode-reviewer");
|
|
3595
|
+
const store = new LifetimeUsage(path8.join(directory, "usage-v2"), path8.join(directory, "usage-v1"));
|
|
3153
3596
|
const [totals, setTotals] = createSignal();
|
|
3154
3597
|
const [unavailable, setUnavailable] = createSignal(false);
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
setTotals(value);
|
|
3162
|
-
setUnavailable(false);
|
|
3163
|
-
}
|
|
3164
|
-
} catch {
|
|
3165
|
-
if (alive && revision === current) setUnavailable(true);
|
|
3166
|
-
}
|
|
3167
|
-
};
|
|
3598
|
+
const refresh = new LifetimeRefresh((signal) => store.totals(signal), (value) => {
|
|
3599
|
+
if (value) {
|
|
3600
|
+
setTotals(value);
|
|
3601
|
+
setUnavailable(false);
|
|
3602
|
+
} else setUnavailable(true);
|
|
3603
|
+
}, api.lifecycle.signal);
|
|
3168
3604
|
const unregister = api.keymap.registerLayer({
|
|
3169
3605
|
commands: [{
|
|
3170
3606
|
name: "opencode-reviewer.lifetime",
|
|
@@ -3172,7 +3608,7 @@ function lifetimeTracker(api) {
|
|
|
3172
3608
|
title: "Reviewer: Lifetime usage",
|
|
3173
3609
|
category: "Reviewer",
|
|
3174
3610
|
run: () => {
|
|
3175
|
-
|
|
3611
|
+
refresh.refresh();
|
|
3176
3612
|
api.ui.dialog.replace(() => _$createComponent(api.ui.DialogAlert, {
|
|
3177
3613
|
title: "Reviewer lifetime usage",
|
|
3178
3614
|
get message() {
|
|
@@ -3183,47 +3619,43 @@ function lifetimeTracker(api) {
|
|
|
3183
3619
|
}]
|
|
3184
3620
|
});
|
|
3185
3621
|
api.lifecycle.onDispose(() => {
|
|
3186
|
-
|
|
3622
|
+
refresh.dispose();
|
|
3187
3623
|
unregister();
|
|
3188
3624
|
});
|
|
3189
|
-
|
|
3625
|
+
refresh.refresh();
|
|
3190
3626
|
return {
|
|
3191
3627
|
// The controller must settle aborted review workers before this final queue drain.
|
|
3192
3628
|
flush: () => store.flush().catch(() => {
|
|
3193
3629
|
}),
|
|
3194
3630
|
text: () => unavailable() ? "lifetime: usage unavailable" : totals()?.requests ? lifetimeCost(totals()) : void 0,
|
|
3195
3631
|
record: (usage) => {
|
|
3196
|
-
void store.record(usage).then(refresh, () =>
|
|
3197
|
-
if (alive) {
|
|
3198
|
-
revision++;
|
|
3199
|
-
setUnavailable(true);
|
|
3200
|
-
}
|
|
3201
|
-
});
|
|
3632
|
+
void store.record(usage).then(() => refresh.refresh(), () => refresh.failed());
|
|
3202
3633
|
}
|
|
3203
3634
|
};
|
|
3204
3635
|
}
|
|
3205
3636
|
|
|
3206
3637
|
// src/tui.tsx
|
|
3207
|
-
import
|
|
3638
|
+
import path10 from "node:path";
|
|
3208
3639
|
|
|
3209
3640
|
// src/session-mode.ts
|
|
3210
3641
|
init_define_REVIEW_PROMPTS();
|
|
3211
3642
|
import { constants as constants4 } from "node:fs";
|
|
3212
3643
|
import { mkdir as mkdir2, open as open4, rename as rename2, unlink as unlink2 } from "node:fs/promises";
|
|
3213
3644
|
import { createHash, randomUUID as randomUUID2 } from "node:crypto";
|
|
3214
|
-
import
|
|
3645
|
+
import path9 from "node:path";
|
|
3646
|
+
var MAX_MODE_READS = 2;
|
|
3215
3647
|
var SessionModeStore = class {
|
|
3216
3648
|
constructor(directory, host) {
|
|
3217
3649
|
this.directory = directory;
|
|
3218
3650
|
this.host = host;
|
|
3219
|
-
if (!
|
|
3651
|
+
if (!path9.isAbsolute(directory)) throw new Error("Session mode directory must be absolute");
|
|
3220
3652
|
}
|
|
3221
3653
|
directory;
|
|
3222
3654
|
host;
|
|
3223
3655
|
pending = Promise.resolve();
|
|
3224
3656
|
file(root) {
|
|
3225
3657
|
const key = createHash("sha256").update(JSON.stringify([this.host, root])).digest("hex");
|
|
3226
|
-
return
|
|
3658
|
+
return path9.join(this.directory, `${key}.json`);
|
|
3227
3659
|
}
|
|
3228
3660
|
async read(root, signal) {
|
|
3229
3661
|
signal.throwIfAborted();
|
|
@@ -3238,12 +3670,14 @@ var SessionModeStore = class {
|
|
|
3238
3670
|
try {
|
|
3239
3671
|
signal.throwIfAborted();
|
|
3240
3672
|
const stat = await file.stat();
|
|
3673
|
+
signal.throwIfAborted();
|
|
3241
3674
|
if (!stat.isFile() || stat.size > 1024) throw new Error("Invalid session mode record");
|
|
3242
3675
|
const buffer = Buffer.alloc(1025);
|
|
3243
3676
|
let size = 0;
|
|
3244
3677
|
while (size < buffer.length) {
|
|
3245
3678
|
signal.throwIfAborted();
|
|
3246
3679
|
const result = await file.read(buffer, size, buffer.length - size, size);
|
|
3680
|
+
signal.throwIfAborted();
|
|
3247
3681
|
if (!result.bytesRead) break;
|
|
3248
3682
|
size += result.bytesRead;
|
|
3249
3683
|
}
|
|
@@ -3300,6 +3734,12 @@ var SessionModes = class {
|
|
|
3300
3734
|
roots = /* @__PURE__ */ new Map();
|
|
3301
3735
|
values = /* @__PURE__ */ new Map();
|
|
3302
3736
|
changes = /* @__PURE__ */ new Map();
|
|
3737
|
+
reads = /* @__PURE__ */ new Map();
|
|
3738
|
+
remember(visited, root, edges) {
|
|
3739
|
+
if (this.roots.size + visited.size > 4096) this.roots.clear();
|
|
3740
|
+
for (const child of visited) this.roots.set(child, { root, edges: edges-- });
|
|
3741
|
+
return root;
|
|
3742
|
+
}
|
|
3303
3743
|
root(sessionID, parent) {
|
|
3304
3744
|
return withDeadline(parent, 5e3, async (signal) => {
|
|
3305
3745
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -3308,29 +3748,57 @@ var SessionModes = class {
|
|
|
3308
3748
|
signal.throwIfAborted();
|
|
3309
3749
|
if (!id || visited.has(id)) throw new Error("Session ancestry unavailable");
|
|
3310
3750
|
const cached = this.roots.get(id);
|
|
3311
|
-
if (cached)
|
|
3751
|
+
if (cached) {
|
|
3752
|
+
if (depth + cached.edges > 16) throw new Error("Session ancestry limit reached");
|
|
3753
|
+
return this.remember(visited, cached.root, depth + cached.edges);
|
|
3754
|
+
}
|
|
3312
3755
|
visited.add(id);
|
|
3313
3756
|
const value = await this.session(id, signal);
|
|
3314
3757
|
signal.throwIfAborted();
|
|
3315
3758
|
if (value?.id !== id || value.parentID !== void 0 && (typeof value.parentID !== "string" || !value.parentID)) {
|
|
3316
3759
|
throw new Error("Session ancestry unavailable");
|
|
3317
3760
|
}
|
|
3318
|
-
if (!value.parentID)
|
|
3319
|
-
if (this.roots.size + visited.size > 4096) this.roots.clear();
|
|
3320
|
-
for (const child of visited) this.roots.set(child, id);
|
|
3321
|
-
return id;
|
|
3322
|
-
}
|
|
3761
|
+
if (!value.parentID) return this.remember(visited, id, depth);
|
|
3323
3762
|
id = value.parentID;
|
|
3324
3763
|
}
|
|
3325
3764
|
throw new Error("Session ancestry limit reached");
|
|
3326
3765
|
}, "Session mode ancestry");
|
|
3327
3766
|
}
|
|
3328
3767
|
async load(root, parent) {
|
|
3768
|
+
parent.throwIfAborted();
|
|
3329
3769
|
if (this.values.has(root)) return;
|
|
3770
|
+
const pending = this.reads.get(root);
|
|
3771
|
+
if (pending) return withDeadline(parent, 5e3, () => pending.wait, "Session mode loading");
|
|
3772
|
+
if (this.reads.size >= MAX_MODE_READS) throw new Error("Session mode read capacity unavailable");
|
|
3773
|
+
const read = { settled: true, finished: false };
|
|
3774
|
+
this.reads.set(root, read);
|
|
3775
|
+
return read.wait = this.loadMode(root, parent, read);
|
|
3776
|
+
}
|
|
3777
|
+
async loadMode(root, parent, read) {
|
|
3330
3778
|
const revision = this.changes.get(root);
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3779
|
+
try {
|
|
3780
|
+
const value = await withDeadline(parent, 5e3, (signal) => {
|
|
3781
|
+
read.settled = false;
|
|
3782
|
+
const worker = Promise.resolve().then(() => {
|
|
3783
|
+
signal.throwIfAborted();
|
|
3784
|
+
return this.store.read(root, signal);
|
|
3785
|
+
});
|
|
3786
|
+
const settled = () => {
|
|
3787
|
+
read.settled = true;
|
|
3788
|
+
this.releaseRead(root, read);
|
|
3789
|
+
};
|
|
3790
|
+
void worker.then(settled, settled);
|
|
3791
|
+
return worker;
|
|
3792
|
+
}, "Session mode loading");
|
|
3793
|
+
parent.throwIfAborted();
|
|
3794
|
+
if (this.changes.get(root) === revision) this.values.set(root, value);
|
|
3795
|
+
} finally {
|
|
3796
|
+
read.finished = true;
|
|
3797
|
+
this.releaseRead(root, read);
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3800
|
+
releaseRead(root, read) {
|
|
3801
|
+
if (read.finished && read.settled && this.reads.get(root) === read) this.reads.delete(root);
|
|
3334
3802
|
}
|
|
3335
3803
|
enabled(root) {
|
|
3336
3804
|
return this.values.get(root) === true;
|
|
@@ -3341,7 +3809,7 @@ var SessionModes = class {
|
|
|
3341
3809
|
return this.store.write(root, enabled);
|
|
3342
3810
|
}
|
|
3343
3811
|
deleted(id) {
|
|
3344
|
-
for (const [child,
|
|
3812
|
+
for (const [child, value] of this.roots) if (child === id || value.root === id) this.roots.delete(child);
|
|
3345
3813
|
}
|
|
3346
3814
|
flush() {
|
|
3347
3815
|
return this.store.flush();
|
|
@@ -3697,15 +4165,15 @@ async function reviewTui(api, options, fileIO, observer) {
|
|
|
3697
4165
|
const hostTrace = observer ? new DiagnosticTrace(observer, 0) : void 0;
|
|
3698
4166
|
const traces = observer ? /* @__PURE__ */ new WeakMap() : void 0;
|
|
3699
4167
|
let reviewSequence = 0;
|
|
3700
|
-
const
|
|
3701
|
-
const modes = new SessionModes(new SessionModeStore(
|
|
4168
|
+
const reader2 = contextReader(api, hostTrace);
|
|
4169
|
+
const modes = new SessionModes(new SessionModeStore(path10.join(api.state.path.state, "opencode-reviewer", "session-mode-v1"), api.state.path.directory), async (id, signal) => api.state.session.get(id) ?? reader2.session(id, signal));
|
|
3702
4170
|
const files = new FileAccess(fileIO);
|
|
3703
4171
|
const approval = approvalTransport(api.client, api.state.path.directory);
|
|
3704
4172
|
let visibleApproval = () => void 0;
|
|
3705
4173
|
const controller = new Controller(async (request, parent, onIdentified, onProgress) => {
|
|
3706
4174
|
const trace = observer ? new DiagnosticTrace(observer, ++reviewSequence) : void 0;
|
|
3707
4175
|
if (trace) traces.set(request, trace);
|
|
3708
|
-
const reviewReader = trace ? contextReader(api, trace) :
|
|
4176
|
+
const reviewReader = trace ? contextReader(api, trace) : reader2;
|
|
3709
4177
|
let worker;
|
|
3710
4178
|
try {
|
|
3711
4179
|
return await withDeadline(parent, config?.timeoutMs ?? 3e4, async (signal) => {
|
|
@@ -3740,35 +4208,8 @@ async function reviewTui(api, options, fileIO, observer) {
|
|
|
3740
4208
|
modes.deleted(event.properties.info.id);
|
|
3741
4209
|
controller.deleted(event.properties.info.id);
|
|
3742
4210
|
});
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
let refreshAgain = false;
|
|
3746
|
-
const refresh = async () => {
|
|
3747
|
-
if (stopped) return;
|
|
3748
|
-
if (syncing) {
|
|
3749
|
-
refreshAgain = true;
|
|
3750
|
-
return;
|
|
3751
|
-
}
|
|
3752
|
-
syncing = true;
|
|
3753
|
-
const revision = controller.revision;
|
|
3754
|
-
try {
|
|
3755
|
-
await withDeadline(api.lifecycle.signal, 5e3, async (signal) => {
|
|
3756
|
-
const result = await measured(hostTrace, "pending-refresh", () => api.client.permission.list({
|
|
3757
|
-
directory: api.state.path.directory
|
|
3758
|
-
}, {
|
|
3759
|
-
signal
|
|
3760
|
-
}));
|
|
3761
|
-
if (result.data) controller.reconcile(result.data, revision);
|
|
3762
|
-
});
|
|
3763
|
-
} catch {
|
|
3764
|
-
} finally {
|
|
3765
|
-
syncing = false;
|
|
3766
|
-
if (refreshAgain) {
|
|
3767
|
-
refreshAgain = false;
|
|
3768
|
-
void refresh();
|
|
3769
|
-
}
|
|
3770
|
-
}
|
|
3771
|
-
};
|
|
4211
|
+
const pendingRefresh = new PendingRefresh(controller, (signal) => measured(hostTrace, "pending-refresh", () => approval.list(signal)), api.lifecycle.signal);
|
|
4212
|
+
const refresh = () => pendingRefresh.refresh();
|
|
3772
4213
|
const unregisterMode = sessionModeCommands(api, modes, (root) => {
|
|
3773
4214
|
controller.modeChanged(root);
|
|
3774
4215
|
void refresh();
|
|
@@ -3778,7 +4219,7 @@ async function reviewTui(api, options, fileIO, observer) {
|
|
|
3778
4219
|
api.event.on("session.idle", () => void refresh());
|
|
3779
4220
|
api.event.on("session.error", () => void refresh());
|
|
3780
4221
|
api.lifecycle.onDispose(async () => {
|
|
3781
|
-
|
|
4222
|
+
pendingRefresh.dispose();
|
|
3782
4223
|
clearInterval(interval);
|
|
3783
4224
|
setSidebar(void 0);
|
|
3784
4225
|
unregisterMode();
|
|
@@ -3912,7 +4353,7 @@ async function reviewTui(api, options, fileIO, observer) {
|
|
|
3912
4353
|
}), null);
|
|
3913
4354
|
_$insert(_el$14, _$createComponent2(Show, {
|
|
3914
4355
|
get when() {
|
|
3915
|
-
return view().status === "analyzing";
|
|
4356
|
+
return _$memo2(() => view().status === "analyzing")() && rating() === void 0;
|
|
3916
4357
|
},
|
|
3917
4358
|
get children() {
|
|
3918
4359
|
return _$createComponent2(ReviewLoading, {
|