@sema-agent/core 5.42.0 → 5.44.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 +97 -0
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/memory-engine/engine.d.ts +94 -2
- package/dist/core/memory-engine/engine.js +46 -4
- package/dist/core/memory-engine/layout.d.ts +11 -4
- package/dist/core/memory-engine/layout.js +3 -3
- package/dist/core/memory-engine/tools.d.ts +12 -0
- package/dist/core/memory-engine/tools.js +30 -11
- package/dist/core/runner/assemble-result.d.ts +3 -0
- package/dist/core/runner/assemble-result.js +4 -1
- package/dist/core/runner/prepare-memory.js +24 -3
- package/dist/core/runner/prepare-task.d.ts +2 -0
- package/dist/core/runner/prepare-task.js +6 -4
- package/dist/core/runner/runtask.js +2 -0
- package/dist/core/types.d.ts +32 -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/tools/fs/fs-search-tools.d.ts +5 -4
- package/dist/tools/fs/fs-search-tools.js +8 -5
- package/dist/tools/fs/search.d.ts +48 -1
- package/dist/tools/fs/search.js +96 -41
- package/package.json +3 -2
package/dist/core/types.d.ts
CHANGED
|
@@ -2881,6 +2881,10 @@ export interface TaskResult {
|
|
|
2881
2881
|
* the resume capability, §6). Undefined for every other status.
|
|
2882
2882
|
*/
|
|
2883
2883
|
checkpointToken?: import("./checkpoint-store.js").CheckpointToken;
|
|
2884
|
+
/** The suspend's NON-SECRET stable identity ({@link import("./checkpoint-store.js").Checkpoint.checkpointId}),
|
|
2885
|
+
* set beside `checkpointToken` on the suspended/needs_review terminals — the display/correlation key a
|
|
2886
|
+
* consumer may log or render where the token must not travel. Absent on pre-identity checkpoints. */
|
|
2887
|
+
checkpointId?: string;
|
|
2884
2888
|
/** Set with `checkpointToken`: who/what must resume (e.g. `{kind:"human", reason, toolName}`), so the
|
|
2885
2889
|
* caller knows what decision the suspension is awaiting. */
|
|
2886
2890
|
checkpointGate?: import("./checkpoint-store.js").CheckpointGate;
|
|
@@ -4679,6 +4683,34 @@ export interface EngineNotice {
|
|
|
4679
4683
|
* `detail: { steer, taskId? }` / `{ followUp, taskId? }`. Per-run, at most once per family
|
|
4680
4684
|
* (the terminal sweep is a single site).
|
|
4681
4685
|
*
|
|
4686
|
+
* - `"memory.session_polluted"` (design/178 §3, #324a) — this session's memory crossed into the
|
|
4687
|
+
* one-way POLLUTED state (a tool classified as an external content source was invoked, directly
|
|
4688
|
+
* or through a delegated child): its memory writes are no longer eligible for the long-term
|
|
4689
|
+
* library. The notice states that VERDICT plus what a harvest does when it collects — it does
|
|
4690
|
+
* not promise quarantine, because a `writeScope`-null layering and a declared-unavailable
|
|
4691
|
+
* session both reach the zero-admission harvest arm (nothing collected ⇒ nothing captured), and
|
|
4692
|
+
* a mark landing after the pre-commit pollution read leaves already-committed rows to the
|
|
4693
|
+
* challenge sweep; what a harvest actually contained is the other code below. The challenge
|
|
4694
|
+
* clause is a policy statement, not a receipt — a sweep that cannot run announces
|
|
4695
|
+
* `"memory.challenge_sweep_failed"` on the engine's incident seat. Announced ONCE PER
|
|
4696
|
+
* SESSION, at the mark seat: a repeat mark inside the process and a resumed session whose
|
|
4697
|
+
* durable marker already exists stay quiet (the state is one-way, so a second line would carry
|
|
4698
|
+
* no new fact); a pollution state that cannot be read announces rather than assuming it was
|
|
4699
|
+
* already said. `detail: { reason, sessionId? }` — `reason` names the invoked tool and is
|
|
4700
|
+
* neutralized/length-bounded (the name comes from the host/protocol roster).
|
|
4701
|
+
* - `"memory.harvest_quarantined"` (design/178 §3, #324a) — a polluted session's harvest ran its
|
|
4702
|
+
* containment: `count` ENTRY files written or changed in the session were withheld from the
|
|
4703
|
+
* library, `moved` of them were physically moved into the control-plane quarantine directory,
|
|
4704
|
+
* and `escalated` of them carry a `HarvestReport.quarantineFailures` row (a failed capture, a
|
|
4705
|
+
* failed removal, or a file tombstoned in place — which counts as moved AND escalated, so the
|
|
4706
|
+
* two numbers are read off the report's rows, never subtracted from each other). One notice PER
|
|
4707
|
+
* HARVEST that withheld at least one entry file (a checkpoint harvest and the terminal harvest
|
|
4708
|
+
* are distinct facts), never minted for a clean session; `detail: { count, moved, escalated,
|
|
4709
|
+
* reason?, sessionId? }` (`reason` absent ⇔ the pollution marker could not be re-read at report
|
|
4710
|
+
* time — the withheld count stays true either way). Registered gap: the derived index (`MEMORY.md`) is
|
|
4711
|
+
* contained on a path that mints no rejection row, so an index-ONLY containment produces no
|
|
4712
|
+
* notice and is disclosed by the harvest report's warnings alone.
|
|
4713
|
+
*
|
|
4682
4714
|
* Deliberately NOT a notice family: brain retry/reconnect liveness (a rate limit, a 5xx, a
|
|
4683
4715
|
* transient network failure being retried). Those are per-attempt liveness frames with their own
|
|
4684
4716
|
* frequency semantics and ride the wire `status` channel ({@link BrainStatus}), whose sink the
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ExecResult, type ExecutionEnv, ExecutionError, type ExecutionEnvExecOptions, FileError, type FileInfo, type Result } from "../harness/types.js";
|
|
2
|
+
import { type SecretEnvFinding } from "../../core/secret-env.js";
|
|
2
3
|
import type { BackgroundShellCapability, BackgroundShellId, BackgroundPoll, BackgroundSpawnOptions } from "../../core/background-shell.js";
|
|
3
4
|
import { BackgroundShellError } from "../../core/background-shell.js";
|
|
4
5
|
import type { SchedulerCapability, ScheduledIntent, SchedulerContext, ScheduledTaskId, ScheduledTaskSummary } from "../../core/scheduler.js";
|
|
@@ -18,6 +19,8 @@ export declare function getShellConfig(customShellPath?: string): Promise<Result
|
|
|
18
19
|
shell: string;
|
|
19
20
|
args: string[];
|
|
20
21
|
}, ExecutionError>>;
|
|
22
|
+
/** test seam (mirrors the notice-seat announce reset): the once-per-process fallback latches. */
|
|
23
|
+
export declare function __resetSecretScrubAnnouncement(): void;
|
|
21
24
|
/** RB-235 ([1937] BB2): the spool-reclaim decision, extracted PURE so every rule is unit-pinnable.
|
|
22
25
|
* Both reclaim sites (foreground `reclaimSpool`, background `syncSpool`) route through this one
|
|
23
26
|
* function so they cannot drift apart.
|
|
@@ -83,6 +86,7 @@ export declare class NodeExecutionEnv implements ExecutionEnv, BackgroundShellCa
|
|
|
83
86
|
private readonly bgShells;
|
|
84
87
|
private bgCounter;
|
|
85
88
|
private readonly retainBackgroundProcesses;
|
|
89
|
+
private readonly onSecretEnvScrub?;
|
|
86
90
|
/** design/128 T1-1: lazily-created env-owned spool dir (mkdtemp under os tmp — unpredictable, never the cwd). */
|
|
87
91
|
private bgSpoolDir?;
|
|
88
92
|
readonly backgroundCapabilities: BackgroundShellCapability["backgroundCapabilities"];
|
|
@@ -108,6 +112,10 @@ export declare class NodeExecutionEnv implements ExecutionEnv, BackgroundShellCa
|
|
|
108
112
|
* deployments (their pre-suspend dispose becomes a no-op too — do not combine).
|
|
109
113
|
*/
|
|
110
114
|
retainBackgroundProcesses?: boolean;
|
|
115
|
+
/** The D1 scrub decision's observation seat: called with every NON-EMPTY finding set the default
|
|
116
|
+
* "scrub" leg withholds from a shell child. Absent ⇒ a once-per-process console summary (loud
|
|
117
|
+
* enough to notice, quiet enough not to spam). Never called on the "all"/allowlist legs. */
|
|
118
|
+
onSecretEnvScrub?: (findings: readonly SecretEnvFinding[]) => void;
|
|
111
119
|
});
|
|
112
120
|
get schedulerCapabilities(): SchedulerCapability["schedulerCapabilities"];
|
|
113
121
|
schedule(intent: ScheduledIntent, ctx: SchedulerContext): Promise<Result<{
|
|
@@ -188,7 +188,13 @@ export async function getShellConfig(customShellPath) {
|
|
|
188
188
|
return ok({ shell: "sh", args: ["-c"] });
|
|
189
189
|
}
|
|
190
190
|
const EXEC_FORCE_SETTLE_GRACE_MS = 3000;
|
|
191
|
-
|
|
191
|
+
let secretScrubConsoleAnnounced = false;
|
|
192
|
+
let secretScrubSeatDefectAnnounced = false;
|
|
193
|
+
export function __resetSecretScrubAnnouncement() {
|
|
194
|
+
secretScrubConsoleAnnounced = false;
|
|
195
|
+
secretScrubSeatDefectAnnounced = false;
|
|
196
|
+
}
|
|
197
|
+
function getShellEnv(inheritEnv, baseEnv, extraEnv, onScrub) {
|
|
192
198
|
let inherited;
|
|
193
199
|
if (inheritEnv === "all") {
|
|
194
200
|
inherited = process.env;
|
|
@@ -200,7 +206,36 @@ function getShellEnv(inheritEnv, baseEnv, extraEnv) {
|
|
|
200
206
|
inherited[k] = process.env[k];
|
|
201
207
|
}
|
|
202
208
|
else {
|
|
203
|
-
|
|
209
|
+
const scrubbed = [];
|
|
210
|
+
inherited = scrubSecretEnv(process.env, scrubbed);
|
|
211
|
+
const findings = scrubbed.filter((f) => baseEnv?.[f.key] === undefined && extraEnv?.[f.key] === undefined);
|
|
212
|
+
if (findings.length > 0) {
|
|
213
|
+
if (typeof onScrub === "function") {
|
|
214
|
+
try {
|
|
215
|
+
const r = onScrub(findings);
|
|
216
|
+
if (typeof r?.then === "function") {
|
|
217
|
+
r.then(undefined, () => {
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
if (onScrub !== undefined && !secretScrubSeatDefectAnnounced) {
|
|
226
|
+
secretScrubSeatDefectAnnounced = true;
|
|
227
|
+
try {
|
|
228
|
+
console.warn(`onSecretEnvScrub seat holds ${onScrub === null ? "null" : typeof onScrub} — not a function; scrub findings fall back to the console summary (once per process)`);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (!secretScrubConsoleAnnounced) {
|
|
234
|
+
secretScrubConsoleAnnounced = true;
|
|
235
|
+
console.warn(`secret_env_scrubbed: ${findings.length} env key(s) withheld from the shell child (${findings.map((f) => f.key).join(", ")}) — once per process; wire onSecretEnvScrub for per-exec telemetry`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
204
239
|
}
|
|
205
240
|
return {
|
|
206
241
|
...inherited,
|
|
@@ -254,6 +289,7 @@ export class NodeExecutionEnv {
|
|
|
254
289
|
bgShells = new Map();
|
|
255
290
|
bgCounter = 0;
|
|
256
291
|
retainBackgroundProcesses;
|
|
292
|
+
onSecretEnvScrub;
|
|
257
293
|
bgSpoolDir;
|
|
258
294
|
backgroundCapabilities;
|
|
259
295
|
constructor(options) {
|
|
@@ -263,6 +299,7 @@ export class NodeExecutionEnv {
|
|
|
263
299
|
this.inheritEnv = options.inheritEnv ?? "scrub";
|
|
264
300
|
this.scheduler = options.scheduler;
|
|
265
301
|
this.retainBackgroundProcesses = options.retainBackgroundProcesses ?? false;
|
|
302
|
+
this.onSecretEnvScrub = options.onSecretEnvScrub;
|
|
266
303
|
this.backgroundCapabilities = {
|
|
267
304
|
supported: true,
|
|
268
305
|
maxConcurrent: 8,
|
|
@@ -420,7 +457,7 @@ export class NodeExecutionEnv {
|
|
|
420
457
|
child = spawn(shellConfig.value.shell, [...shellConfig.value.args, command], {
|
|
421
458
|
cwd,
|
|
422
459
|
detached: process.platform !== "win32",
|
|
423
|
-
env: getShellEnv(this.inheritEnv, this.shellEnv, options?.env),
|
|
460
|
+
env: getShellEnv(this.inheritEnv, this.shellEnv, options?.env, this.onSecretEnvScrub),
|
|
424
461
|
stdio: execSpoolFds ? ["ignore", execSpoolFds[0], execSpoolFds[1]] : ["ignore", "pipe", "pipe"],
|
|
425
462
|
windowsHide: true,
|
|
426
463
|
});
|
|
@@ -1114,7 +1151,7 @@ export class NodeExecutionEnv {
|
|
|
1114
1151
|
child = spawn(shellConfig.value.shell, [...shellConfig.value.args, command], {
|
|
1115
1152
|
cwd: options?.cwd ? resolvePath(this.cwd, options.cwd) : this.cwd,
|
|
1116
1153
|
detached: process.platform !== "win32",
|
|
1117
|
-
env: getShellEnv(this.inheritEnv, this.shellEnv, options?.env),
|
|
1154
|
+
env: getShellEnv(this.inheritEnv, this.shellEnv, options?.env, this.onSecretEnvScrub),
|
|
1118
1155
|
stdio: spoolFds ? ["ignore", spoolFds[0], spoolFds[1]] : ["ignore", "pipe", "pipe"],
|
|
1119
1156
|
windowsHide: true,
|
|
1120
1157
|
});
|
|
@@ -5,11 +5,12 @@ import type { ReadFace } from "./read-face.js";
|
|
|
5
5
|
/**
|
|
6
6
|
* The Grep card's details assembly, PURE over the engine text ({mode, offset} from the request) —
|
|
7
7
|
* exported so the text→structured mapping is pinnable with synthetic texts (the byte-truncation and
|
|
8
|
-
* fenced-partial shapes are impractical to construct through a live tool call).
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* fenced-partial shapes are impractical to construct through a live tool call). #313 moved PATH
|
|
9
|
+
* IDENTITY off the text and onto the engines' served rows (both legs supply them now); the counts
|
|
10
|
+
* and totals below still read the engine's own honesty markers out of the text, which is what keeps
|
|
11
|
+
* this seam the honesty boundary.
|
|
11
12
|
*/
|
|
12
|
-
export declare function grepDetailFields(text: string, mode: "files_with_matches" | "content" | "count", offset?: number): Record<string, unknown>;
|
|
13
|
+
export declare function grepDetailFields(text: string, mode: "files_with_matches" | "content" | "count", offset?: number, structuredRows?: readonly import("./search.js").GrepRow[]): Record<string, unknown>;
|
|
13
14
|
export declare function createGrepTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[], readDeny?: ReadDenyMatcher, readFace?: ReadFace): AgentTool;
|
|
14
15
|
export declare function createGlobTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[], readDeny?: ReadDenyMatcher, readFace?: ReadFace): AgentTool;
|
|
15
16
|
/** Static side-effect class of every hand tool, by name (design/44 §3). Used by prepare-task to (a) feed
|
|
@@ -2,7 +2,9 @@ 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) {
|
|
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;
|
|
6
8
|
const rows = text.startsWith("No matches.")
|
|
7
9
|
? []
|
|
8
10
|
: text
|
|
@@ -28,10 +30,11 @@ export function grepDetailFields(text, mode, offset) {
|
|
|
28
30
|
const appliedLimit = cappedAt !== undefined ? { appliedLimit: cappedAt } : {};
|
|
29
31
|
let detailFields;
|
|
30
32
|
if (mode === "files_with_matches") {
|
|
31
|
-
|
|
33
|
+
const fileNames = structuredPaths ?? rows;
|
|
34
|
+
detailFields = { filenames: fileNames, numFiles: fileNames.length, totalFiles: capTotal ?? fileNames.length, ...appliedLimit, ...appliedOffset };
|
|
32
35
|
}
|
|
33
36
|
else if (mode === "count") {
|
|
34
|
-
const filenames = [...new Set(rows.map((l) => (l.includes(":") ? l.slice(0, l.lastIndexOf(":")) : l)))];
|
|
37
|
+
const filenames = structuredPaths ?? [...new Set(rows.map((l) => (l.includes(":") ? l.slice(0, l.lastIndexOf(":")) : l)))];
|
|
35
38
|
let numMatches = 0;
|
|
36
39
|
let malformed = false;
|
|
37
40
|
for (const l of rows) {
|
|
@@ -52,7 +55,7 @@ export function grepDetailFields(text, mode, offset) {
|
|
|
52
55
|
};
|
|
53
56
|
}
|
|
54
57
|
else {
|
|
55
|
-
const filenames = [...new Set(rows.map(contentPathOf))];
|
|
58
|
+
const filenames = structuredPaths ?? [...new Set(rows.map(contentPathOf))];
|
|
56
59
|
const joined = rows.join("\n");
|
|
57
60
|
const GREP_CONTENT_PREVIEW_CHARS = 16_000;
|
|
58
61
|
detailFields = {
|
|
@@ -161,7 +164,7 @@ export function createGrepTool(env, rootCanonical, additionalRoots, readDeny, re
|
|
|
161
164
|
if (text.startsWith("Error (grep)") || text.startsWith("Error (Grep)"))
|
|
162
165
|
return errorResult(text);
|
|
163
166
|
const mode = a.output_mode ?? "files_with_matches";
|
|
164
|
-
const detailFields = grepDetailFields(text, mode, a.offset);
|
|
167
|
+
const detailFields = grepDetailFields(text, mode, a.offset, grepRun.rows);
|
|
165
168
|
return {
|
|
166
169
|
content: text,
|
|
167
170
|
details: { type: "grep", mode, ...detailFields, ...(grepRun.degraded ?? {}), ...(grepRun.withheld !== undefined ? { withheld: grepRun.withheld } : {}) },
|
|
@@ -190,7 +190,27 @@ export interface ReadDenyJudge {
|
|
|
190
190
|
/** JS-fallback grep: ignore-aware walk + per-line scan, honoring output_mode / context / head_limit.
|
|
191
191
|
* design/199 件B: `deny` prunes the walk (note appended to every output shape); `denyOut` is the
|
|
192
192
|
* structured twin — a sink because this function's dozen error returns predate the facts (the sink
|
|
193
|
-
* is written the moment the walk lands, whatever the scan then returns).
|
|
193
|
+
* is written the moment the walk lands, whatever the scan then returns).
|
|
194
|
+
*
|
|
195
|
+
* backlog #313 — the SERVED rows ride back next to the text. This scanner knows every result's
|
|
196
|
+
* identity natively (the walk hands it whole paths), so a row's `path` is that walked identity,
|
|
197
|
+
* never a cut of the rendered line — the mis-split family the text re-parse carries cannot exist
|
|
198
|
+
* here. The rows ARE the window `text` shows: post offset, post row cap, post byte ceiling, in the
|
|
199
|
+
* same order. The unit is the RECORD, which is the unit the cap has always counted: one record is
|
|
200
|
+
* one rendered line EXCEPT where the file's own name carries a newline, and then the one record
|
|
201
|
+
* spans two physical lines — the same relation {@link formatRgRecords} states for the rg leg, and
|
|
202
|
+
* precisely the case where a row's identity beats anything the text can be split into.
|
|
203
|
+
* Every row carries a path (this leg emits no group-separator row — the one pathless shape
|
|
204
|
+
* ripgrep's output can carry), and a no-match run's row set is `[]`, not absent. A typed
|
|
205
|
+
* `Error (grep): …` return carries no rows at all: a refusal is not a row set. */
|
|
206
|
+
export declare function jsGrepDetailed(env: ExecutionEnv, root: string, p: GrepParams, signal?: AbortSignal, guards?: JsGrepGuards, deny?: ReadDenyJudge, denyOut?: {
|
|
207
|
+
withheld?: ReadDenyWithheld;
|
|
208
|
+
}): Promise<{
|
|
209
|
+
text: string;
|
|
210
|
+
rows?: readonly GrepRow[];
|
|
211
|
+
}>;
|
|
212
|
+
/** Text-only wrapper of {@link jsGrepDetailed} (the shape callers that never look at the served rows
|
|
213
|
+
* want — the same relation {@link runGrep} has to {@link runGrepDetailed}). */
|
|
194
214
|
export declare function jsGrep(env: ExecutionEnv, root: string, p: GrepParams, signal?: AbortSignal, guards?: JsGrepGuards, deny?: ReadDenyJudge, denyOut?: {
|
|
195
215
|
withheld?: ReadDenyWithheld;
|
|
196
216
|
}): Promise<string>;
|
|
@@ -210,8 +230,35 @@ export type GrepDegradation = {
|
|
|
210
230
|
};
|
|
211
231
|
/** Structured grep result: the model-facing text plus the degradation facts, so the tool layer can
|
|
212
232
|
* ship them on the structured frame instead of leaving them prose-only. */
|
|
233
|
+
/** #313 — one SERVED result RECORD with its path taken from the engine's own knowledge, exactly the
|
|
234
|
+
* window `text` shows (post cap/offset): ripgrep's `--null` path field ({@link parseRgRecords}) on
|
|
235
|
+
* the rg legs, the walked file identity ({@link jsGrepDetailed}) on the JS-scanner legs. A record is
|
|
236
|
+
* one rendered line except where a path carries a newline (see {@link formatRgRecords}), which is
|
|
237
|
+
* also the case the text can no longer be split into records at all. The tool layer prefers these
|
|
238
|
+
* over re-parsing `text` (whose `path:line:text` split mis-cuts a path that
|
|
239
|
+
* itself contains `:digits:`).
|
|
240
|
+
*
|
|
241
|
+
* KNOWN EXCEPTION to "exactly the window `text` shows", stated because it is measurable: on the two
|
|
242
|
+
* ripgrep PARTIAL legs (timeout / error-exit with output) the delivered text is fenced by
|
|
243
|
+
* `delimitUntrusted`, which NEUTRALIZES fence markers inside the body, while the rows carry the
|
|
244
|
+
* record bytes as ripgrep emitted them. A row's `text` there is therefore the un-neutralized form —
|
|
245
|
+
* identity (`path`) is unaffected. Consume `text` for anything rendered; the rows' contract is
|
|
246
|
+
* identity. Reconciling the two (defuse per record, or withhold rows where the text is fenced) is a
|
|
247
|
+
* contract choice recorded for the backlog, not settled here.
|
|
248
|
+
*
|
|
249
|
+
* `path` is absent only where the record genuinely carries none —
|
|
250
|
+
* ripgrep's `--` group separator and the unaccountable shapes listed on {@link RgRecord} — and a
|
|
251
|
+
* row set carrying such a row sends the tool layer back to the text parse for identity. */
|
|
252
|
+
export interface GrepRow {
|
|
253
|
+
path?: string;
|
|
254
|
+
text: string;
|
|
255
|
+
}
|
|
213
256
|
export interface GrepRunResult {
|
|
214
257
|
text: string;
|
|
258
|
+
/** Served rows (#313): present on BOTH engine legs — ripgrep's and the JS scanner's, including the
|
|
259
|
+
* rescan legs. Absent only when the JS scanner refused the search outright (a typed
|
|
260
|
+
* `Error (grep): …` text): a refusal has no row set, truthfully, rather than an empty one. */
|
|
261
|
+
rows?: readonly GrepRow[];
|
|
215
262
|
degraded?: GrepDegradation;
|
|
216
263
|
/** design/199 件B — deny-list withholding facts (see {@link ReadDenyWithheld}); absent = nothing
|
|
217
264
|
* withheld / no deny judge in play. */
|
package/dist/tools/fs/search.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { delimitUntrusted } from "../../core/untrusted-text.js";
|
|
2
2
|
import { MAX_EXEC_OUTPUT_BYTES } from "../../core/exec-output-tail.js";
|
|
3
|
-
import { hasBinaryExtension, isAbsolutePathForm } from "./safety.js";
|
|
3
|
+
import { hasBinaryExtension, isAbsolutePathForm, isWinFormPath } from "./safety.js";
|
|
4
4
|
const WALK_MAX_FILES = 5000;
|
|
5
5
|
const WALK_MAX_DEPTH = 32;
|
|
6
6
|
const GREP_DEFAULT_CAP = 250;
|
|
@@ -35,6 +35,25 @@ const TYPE_GLOBS = {
|
|
|
35
35
|
css: "*.{css,scss,less}",
|
|
36
36
|
};
|
|
37
37
|
const basename = (p) => p.slice(Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\")) + 1);
|
|
38
|
+
function collapseDotSegments(p) {
|
|
39
|
+
const win = isWinFormPath(p);
|
|
40
|
+
const lead = (win ? /^[\\/]*/ : /^\/*/).exec(p)?.[0] ?? "";
|
|
41
|
+
const sep = win ? "\\" : "/";
|
|
42
|
+
const parts = p.slice(lead.length).split(win ? /[\\/]+/ : /\/+/);
|
|
43
|
+
const floor = !win ? 0 : lead.length >= 2 ? 2 : /^[A-Za-z]:$/.test(parts[0] ?? "") ? 1 : 0;
|
|
44
|
+
const kept = [];
|
|
45
|
+
for (const part of parts) {
|
|
46
|
+
if (part === "" || part === ".")
|
|
47
|
+
continue;
|
|
48
|
+
if (part === "..") {
|
|
49
|
+
if (kept.length > floor)
|
|
50
|
+
kept.pop();
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
kept.push(part);
|
|
54
|
+
}
|
|
55
|
+
return kept.length === 0 ? "" : lead + kept.join(sep);
|
|
56
|
+
}
|
|
38
57
|
const NO_MATCHES = "No matches.";
|
|
39
58
|
const GREP_LINE_CLIP = 500;
|
|
40
59
|
function clipLine(line) {
|
|
@@ -759,10 +778,10 @@ function multilineSpans(content, p, captureText = false, deadline = Infinity) {
|
|
|
759
778
|
}
|
|
760
779
|
return spans;
|
|
761
780
|
}
|
|
762
|
-
export async function
|
|
781
|
+
export async function jsGrepDetailed(env, root, p, signal, guards, deny, denyOut) {
|
|
763
782
|
const analysis = analyzeRedos(p.pattern, p.ignore_case === true);
|
|
764
783
|
if (analysis.risk)
|
|
765
|
-
return redosRejection(analysis.risk);
|
|
784
|
+
return { text: redosRejection(analysis.risk) };
|
|
766
785
|
const budgetMs = guards?.budgetMs ?? GREP_FILE_BUDGET_MS;
|
|
767
786
|
const longLineLimit = guards?.longLineLimit ?? GREP_GRAY_LINE_MAX;
|
|
768
787
|
const grayPattern = analysis.quantifiedGroup || analysis.unboundedCount >= 2;
|
|
@@ -776,7 +795,7 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
|
|
|
776
795
|
test = compilePattern(p);
|
|
777
796
|
}
|
|
778
797
|
catch (e) {
|
|
779
|
-
return `Error (grep): invalid regex: ${e instanceof Error ? e.message : String(e)}
|
|
798
|
+
return { text: `Error (grep): invalid regex: ${e instanceof Error ? e.message : String(e)}` };
|
|
780
799
|
}
|
|
781
800
|
const ignore = await buildIgnore(env, root, signal);
|
|
782
801
|
const rootPrefix = root.replace(/[\\/]+$/, "") + (root.includes("\\") ? "\\" : "/");
|
|
@@ -816,9 +835,20 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
|
|
|
816
835
|
const cap = p.head_limit === 0 ? Infinity : Math.max(1, Math.floor(p.head_limit ?? GREP_DEFAULT_CAP));
|
|
817
836
|
const mode = p.output_mode ?? "files_with_matches";
|
|
818
837
|
const rel = (abs) => (abs.startsWith(rootPrefix) ? abs.slice(rootPrefix.length) : abs);
|
|
838
|
+
const startWin = isWinFormPath(start);
|
|
839
|
+
const startNorm = collapseDotSegments(start);
|
|
840
|
+
const targetSpell = p.path ? p.path.replace(startWin ? /[\\/]+$/ : /\/+$/, "") : "";
|
|
841
|
+
const isBoundary = (c) => c === "/" || (startWin && c === "\\");
|
|
819
842
|
const relOut = (abs) => {
|
|
820
|
-
|
|
821
|
-
|
|
843
|
+
if (!p.path) {
|
|
844
|
+
const r = rel(abs);
|
|
845
|
+
return r === abs ? r : `./${r}`;
|
|
846
|
+
}
|
|
847
|
+
if (abs === startNorm || abs === start)
|
|
848
|
+
return targetSpell;
|
|
849
|
+
if (abs.startsWith(startNorm) && isBoundary(abs[startNorm.length]))
|
|
850
|
+
return targetSpell + abs.slice(startNorm.length);
|
|
851
|
+
return rel(abs);
|
|
822
852
|
};
|
|
823
853
|
if (p.glob) {
|
|
824
854
|
const globRes = splitGlobParam(p.glob).map((g) => globTokenToRegExp(normalizeGlobToken(g), globIsAnchored(g), true));
|
|
@@ -846,7 +876,7 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
|
|
|
846
876
|
let outBytes = 0;
|
|
847
877
|
let outputTruncated = false;
|
|
848
878
|
let rowsBeforeWindow = 0;
|
|
849
|
-
const pushRow = (row) => {
|
|
879
|
+
const pushRow = (row, path) => {
|
|
850
880
|
if (outputTruncated)
|
|
851
881
|
return;
|
|
852
882
|
if (rowsBeforeWindow < off) {
|
|
@@ -861,7 +891,7 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
|
|
|
861
891
|
return;
|
|
862
892
|
}
|
|
863
893
|
outBytes += size;
|
|
864
|
-
out.push(row);
|
|
894
|
+
out.push({ path, text: row });
|
|
865
895
|
};
|
|
866
896
|
const fileMatches = [];
|
|
867
897
|
const counts = [];
|
|
@@ -889,6 +919,7 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
|
|
|
889
919
|
const lines = read.value.split("\n");
|
|
890
920
|
if (lines.length > 1 && lines.at(-1) === "")
|
|
891
921
|
lines.pop();
|
|
922
|
+
const shownPath = relOut(f);
|
|
892
923
|
let fileCount = 0;
|
|
893
924
|
const fileMatchStart = monotonicNow();
|
|
894
925
|
if (p.multiline) {
|
|
@@ -902,8 +933,8 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
|
|
|
902
933
|
}
|
|
903
934
|
catch (e) {
|
|
904
935
|
if (e instanceof GrepBudgetExceeded)
|
|
905
|
-
return budgetError(
|
|
906
|
-
return `Error (grep): invalid regex: ${e instanceof Error ? e.message : String(e)}
|
|
936
|
+
return { text: budgetError(shownPath, monotonicNow() - fileMatchStart) };
|
|
937
|
+
return { text: `Error (grep): invalid regex: ${e instanceof Error ? e.message : String(e)}` };
|
|
907
938
|
}
|
|
908
939
|
fileCount = spans.length;
|
|
909
940
|
if (mode === "content") {
|
|
@@ -911,12 +942,12 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
|
|
|
911
942
|
if (p.only_matching && text !== undefined) {
|
|
912
943
|
const parts = text.split("\n");
|
|
913
944
|
for (let k = 0; k < parts.length; k++) {
|
|
914
|
-
pushRow(`${
|
|
945
|
+
pushRow(`${shownPath}:${s + k}:${clipLine(parts[k])}`, shownPath);
|
|
915
946
|
}
|
|
916
947
|
continue;
|
|
917
948
|
}
|
|
918
949
|
for (let j = Math.max(0, s - 1 - ctxB); j <= Math.min(lines.length - 1, eL - 1 + ctxA); j++) {
|
|
919
|
-
pushRow(`${
|
|
950
|
+
pushRow(`${shownPath}:${j + 1}:${clipLine(lines[j])}`, shownPath);
|
|
920
951
|
}
|
|
921
952
|
}
|
|
922
953
|
}
|
|
@@ -926,7 +957,7 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
|
|
|
926
957
|
if (grayPattern || (i > 0 && i % GREP_LINE_CHECK_EVERY === 0)) {
|
|
927
958
|
const elapsed = monotonicNow() - fileMatchStart;
|
|
928
959
|
if (elapsed >= budgetMs)
|
|
929
|
-
return budgetError(
|
|
960
|
+
return { text: budgetError(shownPath, elapsed) };
|
|
930
961
|
}
|
|
931
962
|
if (i > 0 && i % GREP_LINE_CHECK_EVERY === 0) {
|
|
932
963
|
await yieldEventLoop();
|
|
@@ -945,16 +976,16 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
|
|
|
945
976
|
if (mode === "content") {
|
|
946
977
|
if (p.only_matching) {
|
|
947
978
|
for (const part of matchedParts(lines[i], p)) {
|
|
948
|
-
pushRow(`${
|
|
979
|
+
pushRow(`${shownPath}:${i + 1}:${clipLine(part)}`, shownPath);
|
|
949
980
|
}
|
|
950
981
|
}
|
|
951
982
|
else if (ctx > 0) {
|
|
952
983
|
for (let j = Math.max(0, i - ctxB); j <= Math.min(lines.length - 1, i + ctxA); j++) {
|
|
953
|
-
pushRow(`${
|
|
984
|
+
pushRow(`${shownPath}:${j + 1}:${clipLine(lines[j])}`, shownPath);
|
|
954
985
|
}
|
|
955
986
|
}
|
|
956
987
|
else {
|
|
957
|
-
pushRow(`${
|
|
988
|
+
pushRow(`${shownPath}:${i + 1}:${clipLine(lines[i])}`, shownPath);
|
|
958
989
|
}
|
|
959
990
|
}
|
|
960
991
|
}
|
|
@@ -968,7 +999,7 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
|
|
|
968
999
|
if (mode === "files_with_matches")
|
|
969
1000
|
fileMatches.push(f);
|
|
970
1001
|
else if (mode === "count" && counts.length < collectCap)
|
|
971
|
-
counts.push(`${
|
|
1002
|
+
counts.push({ path: shownPath, text: `${shownPath}:${fileCount}` });
|
|
972
1003
|
}
|
|
973
1004
|
}
|
|
974
1005
|
if (scanAborted || skippedUnreadableFiles > 0 || skippedLongLines > 0 || multilineSkippedFiles > 0) {
|
|
@@ -988,7 +1019,7 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
|
|
|
988
1019
|
}
|
|
989
1020
|
}
|
|
990
1021
|
const caveat = honestyCaveat + denyNote;
|
|
991
|
-
const paged = (arr) => (off > 0 ? arr.slice(off, off + cap) : arr);
|
|
1022
|
+
const paged = (arr) => (off > 0 ? arr.slice(off, off + cap) : [...arr]);
|
|
992
1023
|
const offNote = off > 0 ? `\n[offset ${off}]` : "";
|
|
993
1024
|
if (mode === "files_with_matches") {
|
|
994
1025
|
const haveMtimes = fileMatches.length > 0 && fileMatches.every((f) => walked.mtimes.has(f));
|
|
@@ -999,26 +1030,37 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
|
|
|
999
1030
|
.map((x) => x.r)
|
|
1000
1031
|
: fileMatches.map(relOut).sort();
|
|
1001
1032
|
const body = sortedFileMatches.slice(off, off + cap);
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
: body.
|
|
1033
|
+
const rows = body.map((r) => ({ path: r, text: r }));
|
|
1034
|
+
return {
|
|
1035
|
+
text: body.length === 0
|
|
1036
|
+
? NO_MATCHES + offNote + typeNote + caveat
|
|
1037
|
+
: body.join("\n") + (totalFiles > off + cap ? `\n…[capped at ${cap} of ${totalFiles}]` : "") + offNote + typeNote + caveat,
|
|
1038
|
+
rows,
|
|
1039
|
+
};
|
|
1005
1040
|
}
|
|
1006
1041
|
if (mode === "count") {
|
|
1007
1042
|
const body = paged(counts);
|
|
1008
|
-
return
|
|
1009
|
-
|
|
1010
|
-
|
|
1043
|
+
return {
|
|
1044
|
+
text: body.length === 0
|
|
1045
|
+
? NO_MATCHES + offNote + typeNote + caveat
|
|
1046
|
+
: body.map((r) => r.text).join("\n") + (totalFiles > off + cap ? `\n…[capped at ${cap} of ${totalFiles}]` : "") + offNote + typeNote + caveat,
|
|
1047
|
+
rows: body,
|
|
1048
|
+
};
|
|
1011
1049
|
}
|
|
1012
1050
|
const body = out;
|
|
1013
1051
|
const byteNote = outputTruncated ? `\n…[output truncated at ${outputMaxBytes} bytes — narrow the pattern or set a smaller head_limit]` : "";
|
|
1014
1052
|
if (body.length === 0)
|
|
1015
|
-
return NO_MATCHES + byteNote + offNote + typeNote + caveat;
|
|
1053
|
+
return { text: NO_MATCHES + byteNote + offNote + typeNote + caveat, rows: [] };
|
|
1054
|
+
const bodyText = body.map((r) => r.text).join("\n");
|
|
1016
1055
|
if (out.length < cap)
|
|
1017
|
-
return
|
|
1056
|
+
return { text: bodyText + byteNote + offNote + typeNote + caveat, rows: body };
|
|
1018
1057
|
const marker = ctx > 0 || p.multiline || p.only_matching
|
|
1019
1058
|
? `\n…[capped at ${cap}; ${totalContent}+ match(es) found, more output omitted]`
|
|
1020
1059
|
: `\n…[capped at ${cap} of ${totalContent}]`;
|
|
1021
|
-
return
|
|
1060
|
+
return { text: bodyText + marker + byteNote + offNote + typeNote + caveat, rows: body };
|
|
1061
|
+
}
|
|
1062
|
+
export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
|
|
1063
|
+
return (await jsGrepDetailed(env, root, p, signal, guards, deny, denyOut)).text;
|
|
1022
1064
|
}
|
|
1023
1065
|
const rgCache = new WeakMap();
|
|
1024
1066
|
export function detectRipgrep(env) {
|
|
@@ -1096,12 +1138,16 @@ function formatRgRecords(records, p, caveat = "") {
|
|
|
1096
1138
|
const cap = p.head_limit === 0 ? Infinity : Math.max(1, Math.floor(p.head_limit ?? GREP_DEFAULT_CAP));
|
|
1097
1139
|
const off = Math.max(0, Math.floor(p.offset ?? 0));
|
|
1098
1140
|
const capped = records.slice(off, off + cap);
|
|
1141
|
+
const served = capped.map((r) => (r.path !== undefined ? { path: r.path, text: r.text } : { text: r.text }));
|
|
1099
1142
|
if (capped.length === 0)
|
|
1100
|
-
return NO_MATCHES + (off > 0 ? `\n[offset ${off}]` : "") + caveat;
|
|
1101
|
-
return
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1143
|
+
return { text: NO_MATCHES + (off > 0 ? `\n[offset ${off}]` : "") + caveat, served };
|
|
1144
|
+
return {
|
|
1145
|
+
text: capped.map((r) => r.text).join("\n") +
|
|
1146
|
+
(records.length > off + cap ? `\n…[capped at ${cap} of ${records.length}]` : "") +
|
|
1147
|
+
(off > 0 ? `\n[offset ${off}]` : "") +
|
|
1148
|
+
caveat,
|
|
1149
|
+
served,
|
|
1150
|
+
};
|
|
1105
1151
|
}
|
|
1106
1152
|
export function rgOutputDenyTripwire(stdout, mode, judge, opts = {}) {
|
|
1107
1153
|
if (stdout.length === 0)
|
|
@@ -1120,30 +1166,32 @@ export function rgOutputDenyTripwire(stdout, mode, judge, opts = {}) {
|
|
|
1120
1166
|
}
|
|
1121
1167
|
async function jsGrepDenyTripFallback(env, root, p, signal, trip, deny) {
|
|
1122
1168
|
const denyOut = {};
|
|
1123
|
-
const text = await
|
|
1169
|
+
const { text, rows } = await jsGrepDetailed(env, root, p, signal, undefined, deny, denyOut);
|
|
1124
1170
|
const why = trip.reason === "deny-hit" ? "its output involved sensitive-path deny-listed entries the exclusion globs cannot express" : "its output contained a record the line format cannot account for";
|
|
1125
1171
|
return {
|
|
1126
1172
|
text: text.startsWith("Error (grep)")
|
|
1127
1173
|
? text
|
|
1128
1174
|
: `${text}\n[note: the ripgrep pass was abandoned (${why}); results are from the fallback scanner, which prunes with the authoritative deny judge]`,
|
|
1175
|
+
...(rows !== undefined ? { rows } : {}),
|
|
1129
1176
|
degraded: { fallback: "js-scan", reason: `ripgrep output tripped the deny tripwire (${trip.reason})` },
|
|
1130
1177
|
...(denyOut.withheld !== undefined ? { withheld: denyOut.withheld } : {}),
|
|
1131
1178
|
};
|
|
1132
1179
|
}
|
|
1133
1180
|
async function jsGrepFallback(env, root, p, signal, reason, deny) {
|
|
1134
1181
|
const denyOut = {};
|
|
1135
|
-
const text = await
|
|
1182
|
+
const { text, rows } = await jsGrepDetailed(env, root, p, signal, undefined, deny, denyOut);
|
|
1136
1183
|
return {
|
|
1137
1184
|
text: text.startsWith("Error (grep)")
|
|
1138
1185
|
? text
|
|
1139
1186
|
: `${text}\n[note: ripgrep failed (${reason}); results are from a fallback scanner]`,
|
|
1187
|
+
...(rows !== undefined ? { rows } : {}),
|
|
1140
1188
|
degraded: { fallback: "js-scan", reason: `ripgrep failed (${reason})` },
|
|
1141
1189
|
...(denyOut.withheld !== undefined ? { withheld: denyOut.withheld } : {}),
|
|
1142
1190
|
};
|
|
1143
1191
|
}
|
|
1144
1192
|
export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
1145
1193
|
const mode = p.output_mode ?? "files_with_matches";
|
|
1146
|
-
const flags = ["--null", "--no-messages", "--no-require-git", "--hidden"];
|
|
1194
|
+
const flags = ["--null", "-H", "--no-messages", "--no-require-git", "--hidden"];
|
|
1147
1195
|
for (const d of VCS_DIRS)
|
|
1148
1196
|
flags.push("--glob", `!${d}`);
|
|
1149
1197
|
flags.push("--max-columns", "500", "--max-columns-preview");
|
|
@@ -1193,7 +1241,7 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
|
1193
1241
|
if (records.length === 0)
|
|
1194
1242
|
return jsGrepFallback(env, root, p, signal, "timed out before completing a result", deny);
|
|
1195
1243
|
return {
|
|
1196
|
-
text: `${delimitUntrusted("ripgrep partial output",
|
|
1244
|
+
...(() => { 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 }; })(),
|
|
1197
1245
|
degraded: { partial: true, reason: "ripgrep timed out" },
|
|
1198
1246
|
};
|
|
1199
1247
|
}
|
|
@@ -1205,6 +1253,12 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
|
1205
1253
|
const denyDisclosure = async () => {
|
|
1206
1254
|
if (deny === undefined || deny.rgExclusionGlobs.length === 0)
|
|
1207
1255
|
return { note: "" };
|
|
1256
|
+
if (p.path) {
|
|
1257
|
+
const startPath = isAbsolutePathForm(p.path) ? p.path : `${root.replace(/[\\/]+$/, "")}${root.includes("\\") ? "\\" : "/"}${p.path}`;
|
|
1258
|
+
const info = await env.fileInfo(startPath, signal);
|
|
1259
|
+
if (info.ok && info.value.kind === "file")
|
|
1260
|
+
return { note: "" };
|
|
1261
|
+
}
|
|
1208
1262
|
const withheld = await rgDenyExistenceProbe(env, root, deny, target, signal);
|
|
1209
1263
|
if (withheld === undefined)
|
|
1210
1264
|
return { note: "" };
|
|
@@ -1215,7 +1269,7 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
|
1215
1269
|
};
|
|
1216
1270
|
if (exitCode === 1) {
|
|
1217
1271
|
const d = await denyDisclosure();
|
|
1218
|
-
return { text: NO_MATCHES + d.note, ...(d.withheld !== undefined ? { withheld: d.withheld } : {}) };
|
|
1272
|
+
return { text: NO_MATCHES + d.note, rows: [], ...(d.withheld !== undefined ? { withheld: d.withheld } : {}) };
|
|
1219
1273
|
}
|
|
1220
1274
|
if (exitCode >= 2) {
|
|
1221
1275
|
if (stdout.trim().length > 0) {
|
|
@@ -1228,7 +1282,7 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
|
1228
1282
|
if (records.length === 0)
|
|
1229
1283
|
return jsGrepFallback(env, root, p, signal, `exited with code ${exitCode} and produced no complete result`, deny);
|
|
1230
1284
|
return {
|
|
1231
|
-
text: `${delimitUntrusted("ripgrep partial output",
|
|
1285
|
+
...(() => { 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 }; })(),
|
|
1232
1286
|
degraded: { partial: true, reason: `ripgrep exited with code ${exitCode}` },
|
|
1233
1287
|
};
|
|
1234
1288
|
}
|
|
@@ -1242,7 +1296,8 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
|
1242
1296
|
const parsed = parseRgRecords(stdout, mode);
|
|
1243
1297
|
const ordered = mode === "files_with_matches" ? await sortRgFilesByMtime(env, root, parsed, signal) : parsed;
|
|
1244
1298
|
const d = await denyDisclosure();
|
|
1245
|
-
|
|
1299
|
+
const formatted = formatRgRecords(ordered, p);
|
|
1300
|
+
return { text: formatted.text + d.note, rows: formatted.served, ...(d.withheld !== undefined ? { withheld: d.withheld } : {}) };
|
|
1246
1301
|
}
|
|
1247
1302
|
async function rgDenyExistenceProbe(env, root, deny, target, signal) {
|
|
1248
1303
|
const probeFlags = ["--files", "--hidden", "--no-require-git", "--no-messages"];
|
|
@@ -1277,8 +1332,8 @@ export async function runGrepDetailed(env, root, p, signal, deny) {
|
|
|
1277
1332
|
if (await detectRipgrep(env))
|
|
1278
1333
|
return rgGrepDetailed(env, root, p, signal, deny);
|
|
1279
1334
|
const denyOut = {};
|
|
1280
|
-
const text = await
|
|
1281
|
-
return { text, ...(denyOut.withheld !== undefined ? { withheld: denyOut.withheld } : {}) };
|
|
1335
|
+
const { text, rows } = await jsGrepDetailed(env, root, p, signal, undefined, deny, denyOut);
|
|
1336
|
+
return { text, ...(rows !== undefined ? { rows } : {}), ...(denyOut.withheld !== undefined ? { withheld: denyOut.withheld } : {}) };
|
|
1282
1337
|
}
|
|
1283
1338
|
export async function runGrep(env, root, p, signal) {
|
|
1284
1339
|
return (await runGrepDetailed(env, root, p, signal)).text;
|