@sema-agent/core 5.41.0 → 5.43.0
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/CHANGELOG.md +95 -0
- package/dist/core/checkpoint-store.d.ts +15 -0
- package/dist/core/checkpoint-store.js +5 -1
- package/dist/core/memory-engine/file-backend.js +2 -2
- package/dist/core/memory-engine/layout.d.ts +5 -0
- package/dist/core/memory-engine/layout.js +6 -3
- package/dist/core/park-selfcheck.js +3 -1
- package/dist/core/runner/assemble-result.d.ts +3 -0
- package/dist/core/runner/assemble-result.js +4 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +192 -0
- package/dist/core/runner/prepare-hands-readface.js +283 -0
- package/dist/core/runner/prepare-task.d.ts +3 -5
- package/dist/core/runner/prepare-task.js +22 -233
- package/dist/core/runner/runtask.js +6 -7
- package/dist/core/types.d.ts +23 -6
- package/dist/core/types.js +20 -0
- package/dist/engine/execution-env/node-execution-env.d.ts +8 -0
- package/dist/engine/execution-env/node-execution-env.js +41 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/tools/fs/fs-search-tools.d.ts +8 -0
- package/dist/tools/fs/fs-search-tools.js +70 -58
- package/dist/tools/fs/search.d.ts +42 -9
- package/dist/tools/fs/search.js +125 -77
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +2 -1
|
@@ -2,6 +2,75 @@ import { Type } from "typebox";
|
|
|
2
2
|
import { defineTool, errorResult } from "../../core/tools.js";
|
|
3
3
|
import { resolveKey, violationText, violationDetails } from "./safety.js";
|
|
4
4
|
import { runGrepDetailed, runGlobDetailed, splitAbsoluteGlobPattern, invalidGlobTokens } from "./search.js";
|
|
5
|
+
export function grepDetailFields(text, mode, offset, structuredRows) {
|
|
6
|
+
const structuredIdentityComplete = structuredRows !== undefined && structuredRows.every((r) => r.path !== undefined || r.text === "--");
|
|
7
|
+
const structuredPaths = structuredIdentityComplete && structuredRows !== undefined ? [...new Set(structuredRows.filter((r) => r.path !== undefined).map((r) => r.path))] : undefined;
|
|
8
|
+
const rows = text.startsWith("No matches.")
|
|
9
|
+
? []
|
|
10
|
+
: text
|
|
11
|
+
.split("\n")
|
|
12
|
+
.filter((l) => l.length > 0 &&
|
|
13
|
+
l !== "No matches." &&
|
|
14
|
+
!l.startsWith("…[") &&
|
|
15
|
+
!l.startsWith("[offset") &&
|
|
16
|
+
!l.startsWith("[note") &&
|
|
17
|
+
!l.startsWith("[!]") &&
|
|
18
|
+
!l.startsWith("<<<UNTRUSTED ") &&
|
|
19
|
+
!l.startsWith("<<<END UNTRUSTED "));
|
|
20
|
+
const contentPathOf = (l) => {
|
|
21
|
+
const m = /^(.*?):(\d+):/.exec(l);
|
|
22
|
+
return m ? m[1] : l;
|
|
23
|
+
};
|
|
24
|
+
const byteTruncated = text.includes("…[output truncated at ");
|
|
25
|
+
const truncatedFlag = byteTruncated ? { truncated: true } : {};
|
|
26
|
+
const capMarker = /^…\[capped at (\d+) of (\d+)\]$/m.exec(text);
|
|
27
|
+
const cappedAt = capMarker ? Number(capMarker[1]) : undefined;
|
|
28
|
+
const capTotal = capMarker ? Number(capMarker[2]) : undefined;
|
|
29
|
+
const appliedOffset = typeof offset === "number" && offset > 0 ? { appliedOffset: offset } : {};
|
|
30
|
+
const appliedLimit = cappedAt !== undefined ? { appliedLimit: cappedAt } : {};
|
|
31
|
+
let detailFields;
|
|
32
|
+
if (mode === "files_with_matches") {
|
|
33
|
+
const fileNames = structuredPaths ?? rows;
|
|
34
|
+
detailFields = { filenames: fileNames, numFiles: fileNames.length, totalFiles: capTotal ?? fileNames.length, ...appliedLimit, ...appliedOffset };
|
|
35
|
+
}
|
|
36
|
+
else if (mode === "count") {
|
|
37
|
+
const filenames = structuredPaths ?? [...new Set(rows.map((l) => (l.includes(":") ? l.slice(0, l.lastIndexOf(":")) : l)))];
|
|
38
|
+
let numMatches = 0;
|
|
39
|
+
let malformed = false;
|
|
40
|
+
for (const l of rows) {
|
|
41
|
+
const m = /:(\d+)$/.exec(l);
|
|
42
|
+
if (m)
|
|
43
|
+
numMatches += Number(m[1]);
|
|
44
|
+
else
|
|
45
|
+
malformed = true;
|
|
46
|
+
}
|
|
47
|
+
detailFields = {
|
|
48
|
+
filenames,
|
|
49
|
+
numFiles: filenames.length,
|
|
50
|
+
content: rows.join("\n"),
|
|
51
|
+
...(malformed || byteTruncated ? {} : { numMatches }),
|
|
52
|
+
...truncatedFlag,
|
|
53
|
+
...appliedLimit,
|
|
54
|
+
...appliedOffset,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
const filenames = structuredPaths ?? [...new Set(rows.map(contentPathOf))];
|
|
59
|
+
const joined = rows.join("\n");
|
|
60
|
+
const GREP_CONTENT_PREVIEW_CHARS = 16_000;
|
|
61
|
+
detailFields = {
|
|
62
|
+
filenames,
|
|
63
|
+
numFiles: filenames.length,
|
|
64
|
+
content: joined.length > GREP_CONTENT_PREVIEW_CHARS ? `${joined.slice(0, GREP_CONTENT_PREVIEW_CHARS)}\n…[truncated — full text in the tool output]` : joined,
|
|
65
|
+
numLines: rows.length,
|
|
66
|
+
...(byteTruncated ? {} : { totalLines: capTotal ?? rows.length }),
|
|
67
|
+
...truncatedFlag,
|
|
68
|
+
...appliedLimit,
|
|
69
|
+
...appliedOffset,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return detailFields;
|
|
73
|
+
}
|
|
5
74
|
export function createGrepTool(env, rootCanonical, additionalRoots, readDeny, readFace) {
|
|
6
75
|
return defineTool({
|
|
7
76
|
name: "Grep",
|
|
@@ -95,64 +164,7 @@ export function createGrepTool(env, rootCanonical, additionalRoots, readDeny, re
|
|
|
95
164
|
if (text.startsWith("Error (grep)") || text.startsWith("Error (Grep)"))
|
|
96
165
|
return errorResult(text);
|
|
97
166
|
const mode = a.output_mode ?? "files_with_matches";
|
|
98
|
-
const
|
|
99
|
-
? []
|
|
100
|
-
: text
|
|
101
|
-
.split("\n")
|
|
102
|
-
.filter((l) => l.length > 0 &&
|
|
103
|
-
!l.startsWith("…[") &&
|
|
104
|
-
!l.startsWith("[offset") &&
|
|
105
|
-
!l.startsWith("[note") &&
|
|
106
|
-
!l.startsWith("[!]") &&
|
|
107
|
-
!l.startsWith("<<<UNTRUSTED ") &&
|
|
108
|
-
!l.startsWith("<<<END UNTRUSTED "));
|
|
109
|
-
const contentPathOf = (l) => {
|
|
110
|
-
const m = /^(.*?):(\d+):/.exec(l);
|
|
111
|
-
return m ? m[1] : l;
|
|
112
|
-
};
|
|
113
|
-
const capMarker = /^…\[capped at (\d+) of (\d+)\]$/m.exec(text);
|
|
114
|
-
const cappedAt = capMarker ? Number(capMarker[1]) : undefined;
|
|
115
|
-
const capTotal = capMarker ? Number(capMarker[2]) : undefined;
|
|
116
|
-
const appliedOffset = typeof a.offset === "number" && a.offset > 0 ? { appliedOffset: a.offset } : {};
|
|
117
|
-
const appliedLimit = cappedAt !== undefined ? { appliedLimit: cappedAt } : {};
|
|
118
|
-
let detailFields;
|
|
119
|
-
if (mode === "files_with_matches") {
|
|
120
|
-
detailFields = { filenames: rows, numFiles: rows.length, totalFiles: capTotal ?? rows.length, ...appliedLimit, ...appliedOffset };
|
|
121
|
-
}
|
|
122
|
-
else if (mode === "count") {
|
|
123
|
-
const filenames = [...new Set(rows.map((l) => (l.includes(":") ? l.slice(0, l.lastIndexOf(":")) : l)))];
|
|
124
|
-
let numMatches = 0;
|
|
125
|
-
let malformed = false;
|
|
126
|
-
for (const l of rows) {
|
|
127
|
-
const m = /:(\d+)$/.exec(l);
|
|
128
|
-
if (m)
|
|
129
|
-
numMatches += Number(m[1]);
|
|
130
|
-
else
|
|
131
|
-
malformed = true;
|
|
132
|
-
}
|
|
133
|
-
detailFields = {
|
|
134
|
-
filenames,
|
|
135
|
-
numFiles: filenames.length,
|
|
136
|
-
content: rows.join("\n"),
|
|
137
|
-
...(malformed ? {} : { numMatches }),
|
|
138
|
-
...appliedLimit,
|
|
139
|
-
...appliedOffset,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
const filenames = [...new Set(rows.map(contentPathOf))];
|
|
144
|
-
const joined = rows.join("\n");
|
|
145
|
-
const GREP_CONTENT_PREVIEW_CHARS = 16_000;
|
|
146
|
-
detailFields = {
|
|
147
|
-
filenames,
|
|
148
|
-
numFiles: filenames.length,
|
|
149
|
-
content: joined.length > GREP_CONTENT_PREVIEW_CHARS ? `${joined.slice(0, GREP_CONTENT_PREVIEW_CHARS)}\n…[truncated — full text in the tool output]` : joined,
|
|
150
|
-
numLines: rows.length,
|
|
151
|
-
totalLines: capTotal ?? rows.length,
|
|
152
|
-
...appliedLimit,
|
|
153
|
-
...appliedOffset,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
167
|
+
const detailFields = grepDetailFields(text, mode, a.offset, grepRun.rows);
|
|
156
168
|
return {
|
|
157
169
|
content: text,
|
|
158
170
|
details: { type: "grep", mode, ...detailFields, ...(grepRun.degraded ?? {}), ...(grepRun.withheld !== undefined ? { withheld: grepRun.withheld } : {}) },
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
2
|
+
/** backlog #310 — the JS scanner's own output ceiling, in bytes of emitted content rows. Pinned to the
|
|
3
|
+
* ripgrep leg's figure ({@link MAX_EXEC_OUTPUT_BYTES}, the exec pipe's rolling-tail bound) so the two
|
|
4
|
+
* legs cannot disagree about how much one search may return; see the collection site in {@link jsGrep}
|
|
5
|
+
* for why this leg bounds the HEAD where the pipe bounds the tail. */
|
|
6
|
+
export declare const JS_GREP_OUTPUT_MAX_BYTES: number;
|
|
2
7
|
/** Directories never worth crawling (design/64 §10.3) — dependency/build/cache trees, PLUS the VCS
|
|
3
8
|
* metadata directories CC names explicitly (RB-200 F1, 220 @368402: `Ok_ = [".git",".svn",".hg",
|
|
4
9
|
* ".bzr",".jj",".sl"]` — git/svn/mercurial/bazaar/jujutsu/sapling). `.bzr`/`.jj`/`.sl` are additive here
|
|
@@ -153,6 +158,10 @@ export interface JsGrepGuards {
|
|
|
153
158
|
budgetMs?: number;
|
|
154
159
|
/** Longest line/content (chars) a gray-zone pattern may be matched against. */
|
|
155
160
|
longLineLimit?: number;
|
|
161
|
+
/** backlog #310 — bytes of content rows this scan may accumulate before it stops collecting and
|
|
162
|
+
* says so (default {@link JS_GREP_OUTPUT_MAX_BYTES}). A seam for the same reason the two above
|
|
163
|
+
* are: the boundary is pinnable at a testable size instead of only at an 8MB harness. */
|
|
164
|
+
outputMaxBytes?: number;
|
|
156
165
|
}
|
|
157
166
|
/** design/199 件B — what a traversal withheld under the sensitive-path read deny list, structured
|
|
158
167
|
* (the prose note is the model-facing twin). Three shapes (§3.1): `pruned_count` = the JS walker
|
|
@@ -201,13 +210,26 @@ export type GrepDegradation = {
|
|
|
201
210
|
};
|
|
202
211
|
/** Structured grep result: the model-facing text plus the degradation facts, so the tool layer can
|
|
203
212
|
* ship them on the structured frame instead of leaving them prose-only. */
|
|
213
|
+
/** #313 (first stage, rg legs) — one SERVED result row with its path read from ripgrep's own
|
|
214
|
+
* field ({@link parseRgRecords}), exactly the window `text` shows (post cap/offset). The tool
|
|
215
|
+
* layer prefers these over re-parsing `text` (whose `path:line:text` split mis-cuts a path that
|
|
216
|
+
* itself contains `:digits:`); absent = a leg that has no structured rows yet (the JS scanner —
|
|
217
|
+
* its rows stage is the ticket's remainder) and the text parse is the honest fallback. */
|
|
218
|
+
export interface GrepRow {
|
|
219
|
+
path?: string;
|
|
220
|
+
text: string;
|
|
221
|
+
}
|
|
204
222
|
export interface GrepRunResult {
|
|
205
223
|
text: string;
|
|
224
|
+
/** Served rows (#313): present on the ripgrep legs, absent on the JS-scanner legs. */
|
|
225
|
+
rows?: readonly GrepRow[];
|
|
206
226
|
degraded?: GrepDegradation;
|
|
207
227
|
/** design/199 件B — deny-list withholding facts (see {@link ReadDenyWithheld}); absent = nothing
|
|
208
228
|
* withheld / no deny judge in play. */
|
|
209
229
|
withheld?: ReadDenyWithheld;
|
|
210
230
|
}
|
|
231
|
+
/** The output modes the rg leg and {@link jsGrep} share. */
|
|
232
|
+
type GrepOutputMode = "content" | "files_with_matches" | "count";
|
|
211
233
|
/**
|
|
212
234
|
* backlog #303 (rescan-hardened form) — the rg leg's deny TRIPWIRE. rg's exclusion globs are spelled
|
|
213
235
|
* from the pattern text and cannot express the win32 component-alias family (`.aws.` / `.aws ` —
|
|
@@ -220,21 +242,31 @@ export interface GrepRunResult {
|
|
|
220
242
|
* ripgrep prints filenames verbatim, so a path component containing a NEWLINE splits one record
|
|
221
243
|
* across physical lines whose fragments carry no `:line:` boundary — the filter kept them and the
|
|
222
244
|
* deny-listed content passed (a protection hole, not a precision residual); and pruning every line
|
|
223
|
-
* of a partial run fabricated a `No matches.` row inside the structured card.
|
|
224
|
-
*
|
|
225
|
-
* longer edits anything: it JUDGES. Any deny hit — or any line the format cannot account for —
|
|
245
|
+
* of a partial run fabricated a `No matches.` row inside the structured card. So this function no
|
|
246
|
+
* longer edits anything: it JUDGES. Any deny hit — or any record the format cannot account for —
|
|
226
247
|
* trips, and the caller abandons the rg run for the JS scanner, whose walk prunes with the
|
|
227
248
|
* authoritative judge and needs no path parsing at all. rg stays the fast path for the common case
|
|
228
249
|
* (no guarded entries in the result); the moment a guarded spelling is involved, the engine that
|
|
229
250
|
* cannot mis-parse it owns the answer.
|
|
230
251
|
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
252
|
+
* backlog #306 — what it judges is now ripgrep's OWN path field ({@link parseRgRecords}, `--null`),
|
|
253
|
+
* not a prefix cut at a guessed separator. Trip conditions, all three modes alike: a record whose
|
|
254
|
+
* path the deny judge names trips as `deny-hit`; a record with no path field at all (rg's binary-file
|
|
255
|
+
* notice, a no-filename row, an unterminated tail, output from an env that ignored `--null`) is
|
|
256
|
+
* unaccountable and trips as `ambiguous-record` — same abandonment, same reason code as before.
|
|
257
|
+
* `dropIncompleteTail` mirrors {@link parseRgRecords}: on the partial legs the cut tail is dropped
|
|
258
|
+
* BEFORE judging, because judging a record the caller will never receive can only cost an
|
|
259
|
+
* unnecessary rescan — every record that does reach the output is still judged, which is what the
|
|
260
|
+
* rule is for. (A killed stream ending mid-record is an EXPECTED shape with a rule of its own; the
|
|
261
|
+
* unaccountable arm is for records that survive that rule.)
|
|
262
|
+
* `--` group separators and empty lines carry no path by construction and pass. The `:digits:` /
|
|
263
|
+
* `-digits-` boundary family this used to reason about is gone with the guessing: both of #303's
|
|
264
|
+
* declared residuals (over-tripping on a cluster inside match TEXT, and the dedup key folding two
|
|
265
|
+
* denied files whose own names carry a cluster) were artifacts of that decision, not of the judge.
|
|
236
266
|
*/
|
|
237
|
-
export declare function rgOutputDenyTripwire(stdout: string, mode:
|
|
267
|
+
export declare function rgOutputDenyTripwire(stdout: string, mode: GrepOutputMode, judge: Pick<ReadDenyJudge, "matchPath">, opts?: {
|
|
268
|
+
dropIncompleteTail?: boolean;
|
|
269
|
+
}): {
|
|
238
270
|
trip: false;
|
|
239
271
|
} | {
|
|
240
272
|
trip: true;
|
|
@@ -285,3 +317,4 @@ export declare function runGlobDetailed(env: ExecutionEnv, root: string, pattern
|
|
|
285
317
|
error?: string;
|
|
286
318
|
withheld?: ReadDenyWithheld;
|
|
287
319
|
}>;
|
|
320
|
+
export {};
|
package/dist/tools/fs/search.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { delimitUntrusted } from "../../core/untrusted-text.js";
|
|
2
|
+
import { MAX_EXEC_OUTPUT_BYTES } from "../../core/exec-output-tail.js";
|
|
2
3
|
import { hasBinaryExtension, isAbsolutePathForm } from "./safety.js";
|
|
3
4
|
const WALK_MAX_FILES = 5000;
|
|
4
5
|
const WALK_MAX_DEPTH = 32;
|
|
5
6
|
const GREP_DEFAULT_CAP = 250;
|
|
6
7
|
const FILE_MAX_BYTES = 5 * 1024 * 1024;
|
|
8
|
+
export const JS_GREP_OUTPUT_MAX_BYTES = MAX_EXEC_OUTPUT_BYTES;
|
|
7
9
|
export const DEFAULT_IGNORE_DIRS = new Set([
|
|
8
10
|
"node_modules", ".git", ".hg", ".svn", ".bzr", ".jj", ".sl", "build", "dist", "out", "target",
|
|
9
11
|
".dart_tool", ".pub-cache", ".next", ".nuxt", ".gradle", ".idea", ".vscode", "Pods", ".venv", "venv",
|
|
@@ -840,6 +842,27 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
|
|
|
840
842
|
const off = Math.min(100_000, Math.max(0, Math.floor(p.offset ?? 0)));
|
|
841
843
|
const collectCap = cap + off;
|
|
842
844
|
const out = [];
|
|
845
|
+
const outputMaxBytes = guards?.outputMaxBytes ?? JS_GREP_OUTPUT_MAX_BYTES;
|
|
846
|
+
let outBytes = 0;
|
|
847
|
+
let outputTruncated = false;
|
|
848
|
+
let rowsBeforeWindow = 0;
|
|
849
|
+
const pushRow = (row) => {
|
|
850
|
+
if (outputTruncated)
|
|
851
|
+
return;
|
|
852
|
+
if (rowsBeforeWindow < off) {
|
|
853
|
+
rowsBeforeWindow++;
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
if (out.length >= cap)
|
|
857
|
+
return;
|
|
858
|
+
const size = Buffer.byteLength(row, "utf8") + 1;
|
|
859
|
+
if (outBytes + size > outputMaxBytes) {
|
|
860
|
+
outputTruncated = true;
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
outBytes += size;
|
|
864
|
+
out.push(row);
|
|
865
|
+
};
|
|
843
866
|
const fileMatches = [];
|
|
844
867
|
const counts = [];
|
|
845
868
|
let totalContent = 0;
|
|
@@ -888,14 +911,12 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
|
|
|
888
911
|
if (p.only_matching && text !== undefined) {
|
|
889
912
|
const parts = text.split("\n");
|
|
890
913
|
for (let k = 0; k < parts.length; k++) {
|
|
891
|
-
|
|
892
|
-
out.push(`${relOut(f)}:${s + k}:${clipLine(parts[k])}`);
|
|
914
|
+
pushRow(`${relOut(f)}:${s + k}:${clipLine(parts[k])}`);
|
|
893
915
|
}
|
|
894
916
|
continue;
|
|
895
917
|
}
|
|
896
918
|
for (let j = Math.max(0, s - 1 - ctxB); j <= Math.min(lines.length - 1, eL - 1 + ctxA); j++) {
|
|
897
|
-
|
|
898
|
-
out.push(`${relOut(f)}:${j + 1}:${clipLine(lines[j])}`);
|
|
919
|
+
pushRow(`${relOut(f)}:${j + 1}:${clipLine(lines[j])}`);
|
|
899
920
|
}
|
|
900
921
|
}
|
|
901
922
|
}
|
|
@@ -924,18 +945,16 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
|
|
|
924
945
|
if (mode === "content") {
|
|
925
946
|
if (p.only_matching) {
|
|
926
947
|
for (const part of matchedParts(lines[i], p)) {
|
|
927
|
-
|
|
928
|
-
out.push(`${relOut(f)}:${i + 1}:${clipLine(part)}`);
|
|
948
|
+
pushRow(`${relOut(f)}:${i + 1}:${clipLine(part)}`);
|
|
929
949
|
}
|
|
930
950
|
}
|
|
931
951
|
else if (ctx > 0) {
|
|
932
952
|
for (let j = Math.max(0, i - ctxB); j <= Math.min(lines.length - 1, i + ctxA); j++) {
|
|
933
|
-
|
|
934
|
-
out.push(`${relOut(f)}:${j + 1}:${clipLine(lines[j])}`);
|
|
953
|
+
pushRow(`${relOut(f)}:${j + 1}:${clipLine(lines[j])}`);
|
|
935
954
|
}
|
|
936
955
|
}
|
|
937
|
-
else
|
|
938
|
-
|
|
956
|
+
else {
|
|
957
|
+
pushRow(`${relOut(f)}:${i + 1}:${clipLine(lines[i])}`);
|
|
939
958
|
}
|
|
940
959
|
}
|
|
941
960
|
}
|
|
@@ -990,15 +1009,16 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
|
|
|
990
1009
|
? NO_MATCHES + offNote + typeNote + caveat
|
|
991
1010
|
: body.join("\n") + (totalFiles > off + cap ? `\n…[capped at ${cap} of ${totalFiles}]` : "") + offNote + typeNote + caveat;
|
|
992
1011
|
}
|
|
993
|
-
const body =
|
|
1012
|
+
const body = out;
|
|
1013
|
+
const byteNote = outputTruncated ? `\n…[output truncated at ${outputMaxBytes} bytes — narrow the pattern or set a smaller head_limit]` : "";
|
|
994
1014
|
if (body.length === 0)
|
|
995
|
-
return NO_MATCHES + offNote + typeNote + caveat;
|
|
996
|
-
if (out.length <
|
|
997
|
-
return body.join("\n") + offNote + typeNote + caveat;
|
|
1015
|
+
return NO_MATCHES + byteNote + offNote + typeNote + caveat;
|
|
1016
|
+
if (out.length < cap)
|
|
1017
|
+
return body.join("\n") + byteNote + offNote + typeNote + caveat;
|
|
998
1018
|
const marker = ctx > 0 || p.multiline || p.only_matching
|
|
999
1019
|
? `\n…[capped at ${cap}; ${totalContent}+ match(es) found, more output omitted]`
|
|
1000
1020
|
: `\n…[capped at ${cap} of ${totalContent}]`;
|
|
1001
|
-
return body.join("\n") + marker + offNote + typeNote + caveat;
|
|
1021
|
+
return body.join("\n") + marker + byteNote + offNote + typeNote + caveat;
|
|
1002
1022
|
}
|
|
1003
1023
|
const rgCache = new WeakMap();
|
|
1004
1024
|
export function detectRipgrep(env) {
|
|
@@ -1012,79 +1032,93 @@ export function detectRipgrep(env) {
|
|
|
1012
1032
|
}
|
|
1013
1033
|
return cached;
|
|
1014
1034
|
}
|
|
1015
|
-
async function sortRgFilesByMtime(env, root,
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
return stdout;
|
|
1035
|
+
async function sortRgFilesByMtime(env, root, records, signal) {
|
|
1036
|
+
if (records.length === 0)
|
|
1037
|
+
return [...records];
|
|
1019
1038
|
const rootPrefix = root.replace(/[\\/]+$/, "") + (root.includes("\\") ? "\\" : "/");
|
|
1020
1039
|
const toAbs = (p) => {
|
|
1021
1040
|
const stripped = p.startsWith("./") ? p.slice(2) : p;
|
|
1022
1041
|
return isAbsolutePathForm(stripped) ? stripped : `${rootPrefix}${stripped}`;
|
|
1023
1042
|
};
|
|
1043
|
+
const paths = records.map((r) => r.path ?? r.text);
|
|
1024
1044
|
const infos = await Promise.all(paths.map((p) => env.fileInfo(toAbs(p), signal)));
|
|
1025
|
-
const withMtime =
|
|
1026
|
-
const
|
|
1027
|
-
return { p, m:
|
|
1045
|
+
const withMtime = records.map((r, i) => {
|
|
1046
|
+
const info = infos[i];
|
|
1047
|
+
return { r, p: paths[i], m: info.ok && typeof info.value.mtimeMs === "number" ? info.value.mtimeMs : undefined };
|
|
1028
1048
|
});
|
|
1029
1049
|
const sorted = withMtime.every((x) => x.m !== undefined)
|
|
1030
1050
|
? [...withMtime].sort((a, b) => b.m - a.m || (a.p < b.p ? -1 : a.p > b.p ? 1 : 0))
|
|
1031
1051
|
: [...withMtime].sort((a, b) => (a.p < b.p ? -1 : a.p > b.p ? 1 : 0));
|
|
1032
|
-
return sorted.map((x) => x.
|
|
1052
|
+
return sorted.map((x) => x.r);
|
|
1053
|
+
}
|
|
1054
|
+
function rgRecordSeparator(mode, rest) {
|
|
1055
|
+
if (mode === "count")
|
|
1056
|
+
return /^\d+$/.test(rest) ? ":" : null;
|
|
1057
|
+
const m = /^\d+([:\-])/.exec(rest);
|
|
1058
|
+
return m === null ? null : m[1];
|
|
1033
1059
|
}
|
|
1034
|
-
function
|
|
1060
|
+
function parseRgRecords(stdout, mode, dropIncompleteTail = false) {
|
|
1061
|
+
if (stdout.length === 0)
|
|
1062
|
+
return [];
|
|
1063
|
+
if (mode === "files_with_matches") {
|
|
1064
|
+
if (!stdout.includes("\0")) {
|
|
1065
|
+
if (dropIncompleteTail)
|
|
1066
|
+
return [];
|
|
1067
|
+
return stdout.split("\n").filter((l) => l.length > 0).map((l) => ({ path: l, text: l }));
|
|
1068
|
+
}
|
|
1069
|
+
const parts = stdout.split("\0");
|
|
1070
|
+
const tail = parts.pop() ?? "";
|
|
1071
|
+
const records = parts.filter((p) => p.length > 0).map((p) => ({ path: p, text: p }));
|
|
1072
|
+
if (tail.length > 0 && !dropIncompleteTail)
|
|
1073
|
+
records.push({ text: tail });
|
|
1074
|
+
return records;
|
|
1075
|
+
}
|
|
1076
|
+
const lines = stdout.split("\n");
|
|
1077
|
+
if (!stdout.endsWith("\n") && dropIncompleteTail)
|
|
1078
|
+
lines.pop();
|
|
1079
|
+
const records = [];
|
|
1080
|
+
for (const line of lines) {
|
|
1081
|
+
if (line.length === 0)
|
|
1082
|
+
continue;
|
|
1083
|
+
const nul = line.indexOf("\0");
|
|
1084
|
+
const rest = nul < 0 ? "" : line.slice(nul + 1);
|
|
1085
|
+
const sep = nul < 0 ? null : rgRecordSeparator(mode, rest);
|
|
1086
|
+
if (nul < 0 || sep === null) {
|
|
1087
|
+
records.push({ text: line });
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
const path = line.slice(0, nul);
|
|
1091
|
+
records.push({ path, text: `${path}${sep}${rest}` });
|
|
1092
|
+
}
|
|
1093
|
+
return records;
|
|
1094
|
+
}
|
|
1095
|
+
function formatRgRecords(records, p, caveat = "") {
|
|
1035
1096
|
const cap = p.head_limit === 0 ? Infinity : Math.max(1, Math.floor(p.head_limit ?? GREP_DEFAULT_CAP));
|
|
1036
1097
|
const off = Math.max(0, Math.floor(p.offset ?? 0));
|
|
1037
|
-
const
|
|
1038
|
-
const
|
|
1098
|
+
const capped = records.slice(off, off + cap);
|
|
1099
|
+
const served = capped.map((r) => (r.path !== undefined ? { path: r.path, text: r.text } : { text: r.text }));
|
|
1039
1100
|
if (capped.length === 0)
|
|
1040
|
-
return NO_MATCHES + (off > 0 ? `\n[offset ${off}]` : "") + caveat;
|
|
1041
|
-
return
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1101
|
+
return { text: NO_MATCHES + (off > 0 ? `\n[offset ${off}]` : "") + caveat, served };
|
|
1102
|
+
return {
|
|
1103
|
+
text: capped.map((r) => r.text).join("\n") +
|
|
1104
|
+
(records.length > off + cap ? `\n…[capped at ${cap} of ${records.length}]` : "") +
|
|
1105
|
+
(off > 0 ? `\n[offset ${off}]` : "") +
|
|
1106
|
+
caveat,
|
|
1107
|
+
served,
|
|
1108
|
+
};
|
|
1045
1109
|
}
|
|
1046
|
-
export function rgOutputDenyTripwire(stdout, mode, judge) {
|
|
1110
|
+
export function rgOutputDenyTripwire(stdout, mode, judge, opts = {}) {
|
|
1047
1111
|
if (stdout.length === 0)
|
|
1048
1112
|
return { trip: false };
|
|
1049
|
-
const
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
sawBoundary = true;
|
|
1053
|
-
const prefix = line.slice(0, m.index);
|
|
1054
|
-
if (prefix.length === 0)
|
|
1113
|
+
for (const record of parseRgRecords(stdout, mode, opts.dropIncompleteTail === true)) {
|
|
1114
|
+
if (record.path === undefined) {
|
|
1115
|
+
if (record.text === "--")
|
|
1055
1116
|
continue;
|
|
1056
|
-
|
|
1057
|
-
if (hit !== null)
|
|
1058
|
-
return hit.pattern;
|
|
1059
|
-
}
|
|
1060
|
-
return sawBoundary ? null : "";
|
|
1061
|
-
};
|
|
1062
|
-
for (const line of stdout.split("\n")) {
|
|
1063
|
-
if (line.length === 0 || line === "--")
|
|
1064
|
-
continue;
|
|
1065
|
-
if (mode === "files_with_matches") {
|
|
1066
|
-
const h = judge.matchPath(line);
|
|
1067
|
-
if (h !== null)
|
|
1068
|
-
return { trip: true, reason: "deny-hit", pattern: h.pattern };
|
|
1069
|
-
}
|
|
1070
|
-
else if (mode === "count") {
|
|
1071
|
-
const m = /^(.*):\d+$/.exec(line);
|
|
1072
|
-
if (m === null)
|
|
1073
|
-
return { trip: true, reason: "ambiguous-record" };
|
|
1074
|
-
const h = judge.matchPath(m[1]);
|
|
1075
|
-
if (h !== null)
|
|
1076
|
-
return { trip: true, reason: "deny-hit", pattern: h.pattern };
|
|
1077
|
-
}
|
|
1078
|
-
else {
|
|
1079
|
-
const colon = judgeBoundaries(line, /:\d+:/g);
|
|
1080
|
-
if (colon !== null && colon !== "")
|
|
1081
|
-
return { trip: true, reason: "deny-hit", pattern: colon };
|
|
1082
|
-
const dash = judgeBoundaries(line, /-\d+-/g);
|
|
1083
|
-
if (dash !== null && dash !== "")
|
|
1084
|
-
return { trip: true, reason: "deny-hit", pattern: dash };
|
|
1085
|
-
if (colon === "" && dash === "")
|
|
1086
|
-
return { trip: true, reason: "ambiguous-record" };
|
|
1117
|
+
return { trip: true, reason: "ambiguous-record" };
|
|
1087
1118
|
}
|
|
1119
|
+
const hit = judge.matchPath(record.path);
|
|
1120
|
+
if (hit !== null)
|
|
1121
|
+
return { trip: true, reason: "deny-hit", pattern: hit.pattern };
|
|
1088
1122
|
}
|
|
1089
1123
|
return { trip: false };
|
|
1090
1124
|
}
|
|
@@ -1113,7 +1147,7 @@ async function jsGrepFallback(env, root, p, signal, reason, deny) {
|
|
|
1113
1147
|
}
|
|
1114
1148
|
export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
1115
1149
|
const mode = p.output_mode ?? "files_with_matches";
|
|
1116
|
-
const flags = ["--no-messages", "--no-require-git", "--hidden"];
|
|
1150
|
+
const flags = ["--null", "-H", "--no-messages", "--no-require-git", "--hidden"];
|
|
1117
1151
|
for (const d of VCS_DIRS)
|
|
1118
1152
|
flags.push("--glob", `!${d}`);
|
|
1119
1153
|
flags.push("--max-columns", "500", "--max-columns-preview");
|
|
@@ -1155,12 +1189,15 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
|
1155
1189
|
const partial = err.partialStdout ?? "";
|
|
1156
1190
|
if (partial.trim().length > 0) {
|
|
1157
1191
|
if (deny !== undefined) {
|
|
1158
|
-
const trip = rgOutputDenyTripwire(partial, mode, deny);
|
|
1192
|
+
const trip = rgOutputDenyTripwire(partial, mode, deny, { dropIncompleteTail: true });
|
|
1159
1193
|
if (trip.trip)
|
|
1160
1194
|
return jsGrepDenyTripFallback(env, root, p, signal, trip, deny);
|
|
1161
1195
|
}
|
|
1196
|
+
const records = parseRgRecords(partial, mode, true);
|
|
1197
|
+
if (records.length === 0)
|
|
1198
|
+
return jsGrepFallback(env, root, p, signal, "timed out before completing a result", deny);
|
|
1162
1199
|
return {
|
|
1163
|
-
text: `${delimitUntrusted("ripgrep partial output",
|
|
1200
|
+
...(() => { const f = formatRgRecords(records, p); return { text: `${delimitUntrusted("ripgrep partial output", f.text)}\n…[ripgrep timed out after producing partial output — results may be incomplete]`, rows: f.served }; })(),
|
|
1164
1201
|
degraded: { partial: true, reason: "ripgrep timed out" },
|
|
1165
1202
|
};
|
|
1166
1203
|
}
|
|
@@ -1172,6 +1209,12 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
|
1172
1209
|
const denyDisclosure = async () => {
|
|
1173
1210
|
if (deny === undefined || deny.rgExclusionGlobs.length === 0)
|
|
1174
1211
|
return { note: "" };
|
|
1212
|
+
if (p.path) {
|
|
1213
|
+
const startPath = isAbsolutePathForm(p.path) ? p.path : `${root.replace(/[\\/]+$/, "")}${root.includes("\\") ? "\\" : "/"}${p.path}`;
|
|
1214
|
+
const info = await env.fileInfo(startPath, signal);
|
|
1215
|
+
if (info.ok && info.value.kind === "file")
|
|
1216
|
+
return { note: "" };
|
|
1217
|
+
}
|
|
1175
1218
|
const withheld = await rgDenyExistenceProbe(env, root, deny, target, signal);
|
|
1176
1219
|
if (withheld === undefined)
|
|
1177
1220
|
return { note: "" };
|
|
@@ -1182,17 +1225,20 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
|
1182
1225
|
};
|
|
1183
1226
|
if (exitCode === 1) {
|
|
1184
1227
|
const d = await denyDisclosure();
|
|
1185
|
-
return { text: NO_MATCHES + d.note, ...(d.withheld !== undefined ? { withheld: d.withheld } : {}) };
|
|
1228
|
+
return { text: NO_MATCHES + d.note, rows: [], ...(d.withheld !== undefined ? { withheld: d.withheld } : {}) };
|
|
1186
1229
|
}
|
|
1187
1230
|
if (exitCode >= 2) {
|
|
1188
1231
|
if (stdout.trim().length > 0) {
|
|
1189
1232
|
if (deny !== undefined) {
|
|
1190
|
-
const trip = rgOutputDenyTripwire(stdout, mode, deny);
|
|
1233
|
+
const trip = rgOutputDenyTripwire(stdout, mode, deny, { dropIncompleteTail: true });
|
|
1191
1234
|
if (trip.trip)
|
|
1192
1235
|
return jsGrepDenyTripFallback(env, root, p, signal, trip, deny);
|
|
1193
1236
|
}
|
|
1237
|
+
const records = parseRgRecords(stdout, mode, true);
|
|
1238
|
+
if (records.length === 0)
|
|
1239
|
+
return jsGrepFallback(env, root, p, signal, `exited with code ${exitCode} and produced no complete result`, deny);
|
|
1194
1240
|
return {
|
|
1195
|
-
text: `${delimitUntrusted("ripgrep partial output",
|
|
1241
|
+
...(() => { const f = formatRgRecords(records, p); return { text: `${delimitUntrusted("ripgrep partial output", f.text)}\n…[ripgrep exited with an error after producing partial output — results may be incomplete]`, rows: f.served }; })(),
|
|
1196
1242
|
degraded: { partial: true, reason: `ripgrep exited with code ${exitCode}` },
|
|
1197
1243
|
};
|
|
1198
1244
|
}
|
|
@@ -1203,9 +1249,11 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
|
1203
1249
|
if (trip.trip)
|
|
1204
1250
|
return jsGrepDenyTripFallback(env, root, p, signal, trip, deny);
|
|
1205
1251
|
}
|
|
1206
|
-
const
|
|
1252
|
+
const parsed = parseRgRecords(stdout, mode);
|
|
1253
|
+
const ordered = mode === "files_with_matches" ? await sortRgFilesByMtime(env, root, parsed, signal) : parsed;
|
|
1207
1254
|
const d = await denyDisclosure();
|
|
1208
|
-
|
|
1255
|
+
const formatted = formatRgRecords(ordered, p);
|
|
1256
|
+
return { text: formatted.text + d.note, rows: formatted.served, ...(d.withheld !== undefined ? { withheld: d.withheld } : {}) };
|
|
1209
1257
|
}
|
|
1210
1258
|
async function rgDenyExistenceProbe(env, root, deny, target, signal) {
|
|
1211
1259
|
const probeFlags = ["--files", "--hidden", "--no-require-git", "--no-messages"];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "design/87 L3 — frozen public export surface of src/index.ts (name -> kind). DO NOT edit by hand to silence a red test. A removed/changed entry = a SemVer-BREAKING change; bump MAJOR and update this fixture in the SAME commit (design/87 §4.2 / §5.2). Regenerate via REGEN in test/export-surface.test.ts.",
|
|
3
|
-
"count":
|
|
3
|
+
"count": 1606,
|
|
4
4
|
"exports": {
|
|
5
5
|
"A2ATaskState": "type",
|
|
6
6
|
"A2ATaskStateReversal": "type",
|
|
@@ -1390,6 +1390,7 @@
|
|
|
1390
1390
|
"mergeRecallHits": "function",
|
|
1391
1391
|
"mergeWorkflowArgs": "function",
|
|
1392
1392
|
"migrateScope": "function",
|
|
1393
|
+
"mintCheckpointId": "function",
|
|
1393
1394
|
"mintCheckpointToken": "function",
|
|
1394
1395
|
"mintRuleTicket": "function",
|
|
1395
1396
|
"missingRestoreSurface": "function",
|