@visual-z/xeno 0.2.1 → 0.4.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 +18 -0
- package/README.md +126 -34
- package/docs/app-server.md +84 -0
- package/docs/install.md +51 -13
- package/docs/providers.md +24 -3
- package/package.json +7 -5
- package/src/apps/app-server.ts +28 -0
- package/src/apps/approve.ts +3 -3
- package/src/apps/cli.ts +25 -10
- package/src/apps/commands.ts +45 -8
- package/src/apps/completions.ts +78 -22
- package/src/apps/control-transport.ts +89 -0
- package/src/apps/image-draft.ts +46 -0
- package/src/apps/model-catalog.ts +2 -1
- package/src/apps/project-trust.ts +2 -42
- package/src/apps/provider-connect.ts +346 -0
- package/src/apps/render.ts +22 -47
- package/src/apps/resources.ts +5 -2
- package/src/apps/shell.ts +333 -1542
- package/src/apps/startup-input.ts +45 -9
- package/src/apps/startup.ts +149 -94
- package/src/apps/ui/app.ts +1553 -0
- package/src/apps/ui/control-model.ts +26 -0
- package/src/apps/ui/extension-host.ts +194 -0
- package/src/apps/ui/image-cache.ts +78 -0
- package/src/apps/ui/palette.ts +88 -0
- package/src/apps/ui/project.ts +127 -0
- package/src/apps/ui/standalone.ts +146 -0
- package/src/apps/ui/status-model.ts +68 -0
- package/src/apps/ui/surface.ts +86 -0
- package/src/apps/ui/theme.ts +86 -0
- package/src/core/acceptance.ts +107 -0
- package/src/core/action-defaults.ts +47 -295
- package/src/core/benchmark.ts +2 -2
- package/src/core/branch-receipt.ts +24 -0
- package/src/core/checkpoints.ts +64 -0
- package/src/core/churn.ts +2 -0
- package/src/core/command-artifacts.ts +94 -0
- package/src/core/command-contract.ts +35 -0
- package/src/core/completion-action.ts +20 -0
- package/src/core/completion-review.ts +37 -21
- package/src/core/context-detail.ts +37 -0
- package/src/core/context-policy.ts +74 -1
- package/src/core/continuation.ts +17 -2
- package/src/core/control-protocol.ts +62 -0
- package/src/core/control-view.ts +42 -0
- package/src/core/derive.ts +29 -3
- package/src/core/draft-images.ts +56 -0
- package/src/core/environment.ts +145 -0
- package/src/core/evaluation-coverage.ts +74 -0
- package/src/core/event-validation.ts +81 -6
- package/src/core/events.ts +99 -3
- package/src/core/evidence-links.ts +7 -0
- package/src/core/evidence.ts +13 -3
- package/src/core/execution.ts +8 -2
- package/src/core/experiment.ts +133 -0
- package/src/core/export.ts +9 -0
- package/src/core/failure-recovery.ts +173 -0
- package/src/core/file-precondition.ts +72 -0
- package/src/core/handoff.ts +65 -4
- package/src/core/harness-diagnostics.ts +206 -0
- package/src/core/history-query.ts +38 -2
- package/src/core/history.ts +38 -1
- package/src/core/integration-health.ts +34 -0
- package/src/core/keymap.ts +8 -20
- package/src/core/model-picker.ts +64 -0
- package/src/core/native-eval.ts +25 -0
- package/src/core/note-guidance.ts +34 -0
- package/src/core/oauth-fields.ts +44 -0
- package/src/core/observation-view.ts +49 -8
- package/src/core/permissions.ts +24 -2
- package/src/core/process-receipt.ts +105 -0
- package/src/core/process-state.ts +37 -0
- package/src/core/program-status.ts +31 -0
- package/src/core/project.ts +70 -19
- package/src/core/prompt-policy.ts +37 -0
- package/src/core/query-eval-contract.ts +14 -0
- package/src/core/read-observation.ts +82 -0
- package/src/core/recovery-corpus.ts +17 -0
- package/src/core/recovery-evaluation.ts +60 -0
- package/src/core/recovery-sources.ts +74 -0
- package/src/core/request-progress.ts +87 -0
- package/src/core/request-state.ts +114 -0
- package/src/core/runtime-target.ts +94 -0
- package/src/core/search-observation.ts +97 -0
- package/src/core/session-tree.ts +147 -0
- package/src/core/settings.ts +33 -10
- package/src/core/skills.ts +44 -0
- package/src/core/sql-query.ts +211 -0
- package/src/core/theme.ts +36 -58
- package/src/core/tool-contract.ts +51 -2
- package/src/core/tool-discovery.ts +87 -0
- package/src/core/tool-failure.ts +60 -0
- package/src/core/tool-input.ts +6 -0
- package/src/core/tool-mode-protocol.ts +25 -0
- package/src/core/transcript.ts +19 -4
- package/src/core/trial-lifecycle.ts +14 -0
- package/src/core/tui-mode.ts +29 -0
- package/src/core/update-check.ts +97 -0
- package/src/core/version.ts +55 -0
- package/src/core/work-state.ts +49 -3
- package/src/core/working-set.ts +91 -0
- package/src/runtime/auth.ts +11 -4
- package/src/runtime/branch-jobs.ts +36 -0
- package/src/runtime/catalog.ts +13 -3
- package/src/runtime/clipboard.ts +15 -4
- package/src/runtime/command-artifacts.ts +64 -0
- package/src/runtime/control-connection.ts +104 -0
- package/src/runtime/environment-tools.ts +60 -0
- package/src/runtime/evidence-notes.ts +4 -2
- package/src/runtime/exec-sessions.ts +206 -61
- package/src/runtime/execution-control.ts +1 -0
- package/src/runtime/extension-ui.ts +22 -30
- package/src/runtime/extensions.ts +95 -16
- package/src/runtime/harness-comparison.ts +27 -7
- package/src/runtime/harness-eval.ts +462 -0
- package/src/runtime/history-cache.ts +57 -0
- package/src/runtime/history-tools.ts +62 -12
- package/src/runtime/integrations.ts +210 -0
- package/src/runtime/loop.ts +1088 -282
- package/src/runtime/model-discovery.ts +208 -0
- package/src/runtime/model.ts +9 -0
- package/src/runtime/native-eval-fixtures.ts +33 -0
- package/src/runtime/native-eval.ts +146 -0
- package/src/runtime/oauth.ts +80 -3
- package/src/runtime/pi-provider.ts +169 -12
- package/src/runtime/program-status.ts +38 -0
- package/src/runtime/provider-connect.ts +109 -0
- package/src/runtime/provider-presets.ts +87 -0
- package/src/runtime/provider-request.ts +13 -2
- package/src/runtime/provider.ts +23 -9
- package/src/runtime/query-eval-verifier.ts +28 -0
- package/src/runtime/recovery-coverage.ts +71 -0
- package/src/runtime/recovery-evidence.ts +76 -0
- package/src/runtime/recovery-fixtures.ts +116 -0
- package/src/runtime/runtime-target.ts +149 -0
- package/src/runtime/session-control.ts +198 -0
- package/src/runtime/tool-broker.ts +137 -0
- package/src/runtime/tool-discovery.ts +57 -0
- package/src/runtime/tool-mode-eval.ts +146 -0
- package/src/runtime/tool-queue.ts +77 -0
- package/src/runtime/tools.ts +628 -183
- package/src/runtime/trial-control.ts +45 -0
- package/src/runtime/update-check.ts +47 -0
- package/src/runtime/window-trial.ts +2 -1
- package/src/runtime/working-notes.ts +33 -3
- package/src/runtime/workspace-lifecycle.ts +97 -0
- package/src/store/acceptance.ts +15 -0
- package/src/store/checkpoint-history.ts +43 -0
- package/src/store/command-artifacts.ts +96 -0
- package/src/store/command.ts +65 -37
- package/src/store/file-checkpoints.ts +104 -0
- package/src/store/file-index.ts +13 -1
- package/src/store/git-checkpoints.ts +67 -0
- package/src/store/observations.ts +61 -20
- package/src/store/process-receipts.ts +34 -0
- package/src/store/query-source.ts +13 -8
- package/src/store/recovery.ts +2 -3
- package/src/store/replay.ts +16 -4
- package/src/store/runtime-target.ts +64 -0
- package/src/store/search.ts +171 -132
- package/src/store/session-archive.ts +4 -0
- package/src/store/source-cancellation.ts +6 -0
- package/src/store/update-cache.ts +36 -0
- package/src/store/workspace-observation.ts +113 -0
- package/src/tui/ansi-spans.ts +126 -0
- package/src/tui/style.ts +19 -16
- package/src/tui/ui/atomic-draft.ts +121 -0
- package/src/tui/ui/canvas.ts +96 -0
- package/src/tui/ui/colors.ts +70 -0
- package/src/tui/ui/composer.ts +200 -0
- package/src/tui/ui/control-panel.ts +23 -0
- package/src/tui/ui/dialog.ts +379 -0
- package/src/tui/ui/draft.ts +23 -0
- package/src/tui/ui/edge.ts +186 -0
- package/src/tui/ui/image-size.ts +40 -0
- package/src/tui/ui/ink.ts +210 -0
- package/src/tui/ui/items.ts +40 -0
- package/src/tui/ui/keys.ts +40 -0
- package/src/tui/ui/layout.ts +74 -0
- package/src/tui/ui/list-model.ts +86 -0
- package/src/tui/ui/list-view.ts +172 -0
- package/src/tui/ui/mascot.ts +67 -0
- package/src/tui/ui/messages.ts +518 -0
- package/src/tui/ui/motion.ts +28 -0
- package/src/tui/ui/paths.ts +10 -0
- package/src/tui/ui/pictures.ts +78 -0
- package/src/tui/ui/screen.ts +421 -0
- package/src/tui/ui/status.ts +279 -0
- package/src/tui/ui/styled.ts +85 -0
- package/src/tui/ui/testing.ts +5 -0
- package/src/tui/ui/tool-card.ts +150 -0
- package/src/apps/extension-ui.ts +0 -330
- package/src/apps/resource-browser.ts +0 -75
- package/src/apps/session-browser.ts +0 -113
- package/src/apps/session-picker.ts +0 -55
- package/src/tui/ansi.ts +0 -67
- package/src/tui/autocomplete.ts +0 -29
- package/src/tui/chat-viewport.ts +0 -47
- package/src/tui/complete.ts +0 -87
- package/src/tui/editor.ts +0 -940
- package/src/tui/host.ts +0 -264
- package/src/tui/input.ts +0 -75
- package/src/tui/keys.ts +0 -378
- package/src/tui/markdown.ts +0 -17
- package/src/tui/render.ts +0 -153
- package/src/tui/select.ts +0 -258
- package/src/tui/settings.ts +0 -44
- package/src/tui/statusline.ts +0 -162
- package/src/tui/system-theme.ts +0 -700
- package/src/tui/terminal.ts +0 -30
- package/src/tui/text.ts +0 -52
- package/src/tui/toolview.ts +0 -386
- package/src/tui/transcript.ts +0 -123
- package/src/tui/tree.ts +0 -471
- package/src/vendor/pi-tui/LICENSE.upstream +0 -21
- package/src/vendor/pi-tui/UPSTREAM.json +0 -50
- package/src/vendor/pi-tui/VENDOR.md +0 -25
- package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
- package/src/vendor/pi-tui/autocomplete.ts +0 -860
- package/src/vendor/pi-tui/colors.ts +0 -367
- package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
- package/src/vendor/pi-tui/components/box.ts +0 -166
- package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
- package/src/vendor/pi-tui/components/editor.ts +0 -2472
- package/src/vendor/pi-tui/components/h-stack.ts +0 -44
- package/src/vendor/pi-tui/components/image.ts +0 -127
- package/src/vendor/pi-tui/components/input.ts +0 -494
- package/src/vendor/pi-tui/components/loader.ts +0 -101
- package/src/vendor/pi-tui/components/markdown.ts +0 -1021
- package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
- package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
- package/src/vendor/pi-tui/components/select-list.ts +0 -273
- package/src/vendor/pi-tui/components/settings-list.ts +0 -328
- package/src/vendor/pi-tui/components/spacer.ts +0 -28
- package/src/vendor/pi-tui/components/stack.ts +0 -154
- package/src/vendor/pi-tui/components/text.ts +0 -107
- package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
- package/src/vendor/pi-tui/components/v-stack.ts +0 -33
- package/src/vendor/pi-tui/editor-component.ts +0 -74
- package/src/vendor/pi-tui/fuzzy.ts +0 -138
- package/src/vendor/pi-tui/index.ts +0 -186
- package/src/vendor/pi-tui/keybindings.ts +0 -320
- package/src/vendor/pi-tui/keys.ts +0 -1401
- package/src/vendor/pi-tui/kill-ring.ts +0 -46
- package/src/vendor/pi-tui/latex.ts +0 -1506
- package/src/vendor/pi-tui/layout-node.ts +0 -51
- package/src/vendor/pi-tui/layout.ts +0 -449
- package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
- package/src/vendor/pi-tui/native/clipboard.h +0 -208
- package/src/vendor/pi-tui/native/darwin/README.md +0 -22
- package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
- package/src/vendor/pi-tui/native/linux/README.md +0 -23
- package/src/vendor/pi-tui/native/linux/build.sh +0 -65
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
- package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
- package/src/vendor/pi-tui/native/napi.h +0 -91
- package/src/vendor/pi-tui/native/win32/README.md +0 -31
- package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
- package/src/vendor/pi-tui/native-modifiers.ts +0 -13
- package/src/vendor/pi-tui/native-module-path.ts +0 -31
- package/src/vendor/pi-tui/native-platform.ts +0 -65
- package/src/vendor/pi-tui/oklab.ts +0 -233
- package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
- package/src/vendor/pi-tui/terminal-colors.ts +0 -91
- package/src/vendor/pi-tui/terminal-image.ts +0 -731
- package/src/vendor/pi-tui/terminal.ts +0 -554
- package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
- package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
- package/src/vendor/pi-tui/tui.ts +0 -1493
- package/src/vendor/pi-tui/undo-stack.ts +0 -28
- package/src/vendor/pi-tui/utils.ts +0 -1385
- package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
- package/src/vendor/pi-tui/word-navigation.ts +0 -117
package/src/store/search.ts
CHANGED
|
@@ -18,6 +18,11 @@ import { Worker } from "node:worker_threads";
|
|
|
18
18
|
import { assertNever } from "../core/exhaustive.ts";
|
|
19
19
|
import { buildFileIndex } from "./file-index.ts";
|
|
20
20
|
import { DEFAULT_IGNORE } from "./workspace.ts";
|
|
21
|
+
import { blobAddress } from "./blobs.ts";
|
|
22
|
+
import { previewSearchObservation, type SearchObservation } from "../core/search-observation.ts";
|
|
23
|
+
import { failureDiagnostic, failureFromError, READ_ATTEMPTED, NOT_STARTED } from "../core/tool-failure.ts";
|
|
24
|
+
import type { ToolDiagnostic } from "../core/tool-contract.ts";
|
|
25
|
+
import { COMMAND_TERMINATION_GRACE_MS } from "../core/command-contract.ts";
|
|
21
26
|
|
|
22
27
|
/**
|
|
23
28
|
* Match / entry cap. Assumed, not measured: enough for a model to read in
|
|
@@ -64,12 +69,16 @@ export const MATCH_TIMEOUT_MS = 100;
|
|
|
64
69
|
|
|
65
70
|
/** Assumed: an rg/fd process that runs longer than this is stuck. */
|
|
66
71
|
export const PROCESS_TIMEOUT_MS = 30_000;
|
|
72
|
+
/** Assumed execution budget, independent of display caps; reaching it makes coverage partial. */
|
|
73
|
+
export const MAX_SEARCH_CAPTURE_BYTES = 32 * 1024 * 1024;
|
|
67
74
|
|
|
68
75
|
export type SearchEngine = "auto" | "ripgrep" | "walk" | "fd";
|
|
69
76
|
|
|
70
77
|
export interface SearchResult {
|
|
71
78
|
content: string;
|
|
72
79
|
ok: boolean;
|
|
80
|
+
observation?: SearchObservation;
|
|
81
|
+
diagnostic?: ToolDiagnostic;
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
export interface GrepOpts {
|
|
@@ -81,6 +90,10 @@ export interface GrepOpts {
|
|
|
81
90
|
readonly limit?: number;
|
|
82
91
|
readonly engine?: "auto" | "ripgrep" | "walk";
|
|
83
92
|
readonly timeoutMs?: number;
|
|
93
|
+
readonly signal?: AbortSignal;
|
|
94
|
+
/** Trusted harness budgets, not model arguments. */
|
|
95
|
+
readonly maxCaptureBytes?: number;
|
|
96
|
+
readonly maxIndexEntries?: number;
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
export interface FindOpts {
|
|
@@ -90,12 +103,25 @@ export interface FindOpts {
|
|
|
90
103
|
readonly limit?: number;
|
|
91
104
|
readonly engine?: "auto" | "fd" | "walk";
|
|
92
105
|
readonly timeoutMs?: number;
|
|
106
|
+
readonly signal?: AbortSignal;
|
|
107
|
+
readonly maxCaptureBytes?: number;
|
|
108
|
+
readonly maxIndexEntries?: number;
|
|
93
109
|
}
|
|
94
110
|
|
|
95
111
|
export interface LsOpts {
|
|
96
112
|
readonly root: string;
|
|
97
113
|
readonly path: string;
|
|
98
114
|
readonly limit?: number;
|
|
115
|
+
readonly signal?: AbortSignal;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function observed(observation: SearchObservation, diagnostic?: ToolDiagnostic): SearchResult {
|
|
119
|
+
return { ok: diagnostic === undefined, content: previewSearchObservation(observation, "pending-archive", MAX_OUTPUT_BYTES, MAX_LINE_CHARS).text,
|
|
120
|
+
observation, ...(diagnostic ? { diagnostic } : {}) };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function searchFailure(code: ToolDiagnostic["code"], content: string): SearchResult {
|
|
124
|
+
return { ok: false, content, diagnostic: failureDiagnostic(code, content, NOT_STARTED) };
|
|
99
125
|
}
|
|
100
126
|
|
|
101
127
|
export interface MatchLine {
|
|
@@ -139,7 +165,7 @@ export function findOnPath(name: string): string | null {
|
|
|
139
165
|
|
|
140
166
|
function posixRel(root: string, abs: string): string | null {
|
|
141
167
|
const rel = relative(root, abs);
|
|
142
|
-
if (rel.startsWith(
|
|
168
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return null;
|
|
143
169
|
return rel.split(sep).join("/");
|
|
144
170
|
}
|
|
145
171
|
|
|
@@ -195,71 +221,39 @@ function splitLines(text: string): string[] {
|
|
|
195
221
|
return trimmed.split("\n");
|
|
196
222
|
}
|
|
197
223
|
|
|
198
|
-
function truncateLine(text: string): string {
|
|
199
|
-
const flat = text.replace(/\r/g, "");
|
|
200
|
-
if (flat.length <= MAX_LINE_CHARS) return flat;
|
|
201
|
-
return flat.slice(0, MAX_LINE_CHARS) + "…";
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function formatMatch(m: MatchLine): string {
|
|
205
|
-
return `${m.path}:${m.line}: ${truncateLine(m.text)}`;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Apply the match-count cap and the byte cap. Overflow is counted, never
|
|
210
|
-
* dropped silently: a test that only asserted "at most N" would pass an
|
|
211
|
-
* implementation that threw the rest away.
|
|
212
|
-
*/
|
|
213
|
-
function renderLines(lines: string[], limit: number, extraNotices: string[]): string {
|
|
214
|
-
const omittedFromCap = Math.max(0, lines.length - limit);
|
|
215
|
-
let shown = lines.slice(0, limit);
|
|
216
|
-
let omitted = omittedFromCap;
|
|
217
|
-
|
|
218
|
-
let body = shown.join("\n");
|
|
219
|
-
while (shown.length > 0 && body.length > MAX_OUTPUT_BYTES) {
|
|
220
|
-
shown = shown.slice(0, -1);
|
|
221
|
-
omitted += 1;
|
|
222
|
-
body = shown.join("\n");
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const notices = [...extraNotices];
|
|
226
|
-
if (omitted > 0) notices.push(`${omitted} more matches omitted`);
|
|
227
|
-
if (notices.length === 0) return body;
|
|
228
|
-
const note = `[${notices.join(". ")}]`;
|
|
229
|
-
return body.length > 0 ? `${body}\n${note}` : note;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function noMatches(notices: string[]): SearchResult {
|
|
233
|
-
if (notices.length === 0) return { content: "No matches found", ok: true };
|
|
234
|
-
return { content: `[${notices.join(". ")}]`, ok: true };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
224
|
async function inspectTarget(
|
|
238
225
|
root: string,
|
|
239
226
|
abs: string,
|
|
240
|
-
): Promise<{ kind: "file" | "dir"; rel: string } | { error: string }> {
|
|
227
|
+
): Promise<{ kind: "file" | "dir"; rel: string } | { error: string; diagnostic: ToolDiagnostic }> {
|
|
241
228
|
const rel = posixRel(root, abs);
|
|
242
|
-
|
|
243
|
-
if (
|
|
229
|
+
const refuse = (message: string) => ({ error: message, diagnostic: failureDiagnostic("permission_denied", message, NOT_STARTED) });
|
|
230
|
+
if (rel === null) return refuse(`path escapes the workspace root: ${abs}`);
|
|
231
|
+
if (ignoredRel(rel)) return refuse(`path is ignored: ${rel}`);
|
|
244
232
|
|
|
245
233
|
let st;
|
|
246
234
|
try {
|
|
235
|
+
let current = root;
|
|
236
|
+
for (const part of rel.split("/").slice(0, -1)) {
|
|
237
|
+
current = join(current, part);
|
|
238
|
+
if ((await lstat(current)).isSymbolicLink()) return refuse(`refusing to follow a symlink in path: ${rel || "."}`);
|
|
239
|
+
}
|
|
247
240
|
st = await lstat(abs);
|
|
248
|
-
} catch {
|
|
249
|
-
return { error: `
|
|
241
|
+
} catch (error) {
|
|
242
|
+
return { error: `cannot inspect path: ${rel || "."}`, diagnostic: failureFromError(error, READ_ATTEMPTED) };
|
|
250
243
|
}
|
|
251
244
|
if (st.isSymbolicLink()) {
|
|
252
|
-
return
|
|
245
|
+
return refuse(`refusing to follow a symlink: ${rel || "."}`);
|
|
253
246
|
}
|
|
254
247
|
if (st.isDirectory()) return { kind: "dir", rel };
|
|
255
248
|
if (st.isFile()) return { kind: "file", rel };
|
|
256
|
-
|
|
249
|
+
const message = `not a file or directory: ${rel || "."}`;
|
|
250
|
+
return { error: message, diagnostic: failureDiagnostic("invalid_input", message, READ_ATTEMPTED) };
|
|
257
251
|
}
|
|
258
252
|
|
|
259
|
-
async function listedFiles(abs: string, rel: string, kind: "file" | "dir"
|
|
260
|
-
if (kind === "file") return ignoredRel(rel) ? [] : [rel];
|
|
261
|
-
const index = await buildFileIndex(abs);
|
|
262
|
-
return index.paths.map((p) => (rel.length === 0 ? p : `${rel}/${p}`));
|
|
253
|
+
async function listedFiles(abs: string, rel: string, kind: "file" | "dir", options: Parameters<typeof buildFileIndex>[1] = {}) {
|
|
254
|
+
if (kind === "file") return { paths: ignoredRel(rel) ? [] : [rel], issues: [] };
|
|
255
|
+
const index = await buildFileIndex(abs, options);
|
|
256
|
+
return { paths: index.paths.map((p) => (rel.length === 0 ? p : `${rel}/${p}`)), issues: [...(index.issues ?? [])] };
|
|
263
257
|
}
|
|
264
258
|
|
|
265
259
|
interface WorkerMatch {
|
|
@@ -278,24 +272,29 @@ class LineMatcher {
|
|
|
278
272
|
pattern: string,
|
|
279
273
|
flags: string,
|
|
280
274
|
lines: string[],
|
|
281
|
-
|
|
275
|
+
budgetMs = MATCH_TIMEOUT_MS,
|
|
276
|
+
signal?: AbortSignal,
|
|
277
|
+
): Promise<"timeout" | "invalid" | "cancelled" | MatchLine[]> {
|
|
278
|
+
if (signal?.aborted) return "cancelled";
|
|
282
279
|
if (this.worker === null) this.worker = this.spawn();
|
|
283
280
|
const worker = this.worker;
|
|
284
281
|
return await new Promise((resolve) => {
|
|
285
282
|
let settled = false;
|
|
286
|
-
const finish = (value: "timeout" | "invalid" | MatchLine[]): void => {
|
|
283
|
+
const finish = (value: "timeout" | "invalid" | "cancelled" | MatchLine[]): void => {
|
|
287
284
|
if (settled) return;
|
|
288
285
|
settled = true;
|
|
289
286
|
clearTimeout(timer);
|
|
290
287
|
worker.off("message", onMessage);
|
|
291
288
|
worker.off("error", onError);
|
|
289
|
+
signal?.removeEventListener("abort", abort);
|
|
292
290
|
resolve(value);
|
|
293
291
|
};
|
|
292
|
+
const abort = () => { void worker.terminate(); this.worker = null; finish("cancelled"); };
|
|
294
293
|
const timer = setTimeout(() => {
|
|
295
294
|
void worker.terminate();
|
|
296
295
|
this.worker = null;
|
|
297
296
|
finish("timeout");
|
|
298
|
-
}, MATCH_TIMEOUT_MS);
|
|
297
|
+
}, Math.max(0, Math.min(MATCH_TIMEOUT_MS, budgetMs)));
|
|
299
298
|
const onMessage = (msg: WorkerMatch) => {
|
|
300
299
|
if (!msg.ok) {
|
|
301
300
|
finish("invalid");
|
|
@@ -310,6 +309,7 @@ class LineMatcher {
|
|
|
310
309
|
};
|
|
311
310
|
worker.on("message", onMessage);
|
|
312
311
|
worker.on("error", onError);
|
|
312
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
313
313
|
worker.postMessage({ pattern, flags, lines });
|
|
314
314
|
});
|
|
315
315
|
}
|
|
@@ -326,27 +326,51 @@ function runCommand(
|
|
|
326
326
|
cmd: string,
|
|
327
327
|
args: readonly string[],
|
|
328
328
|
timeoutMs: number,
|
|
329
|
-
|
|
329
|
+
signal?: AbortSignal,
|
|
330
|
+
maxBytes = MAX_SEARCH_CAPTURE_BYTES,
|
|
331
|
+
): Promise<{ stdout: string; raw: Uint8Array; rawErrors?: Uint8Array; stderr: string; exit: number | null; diagnostic?: ToolDiagnostic; stopped?: "timeout" | "cancelled" | "output_limit" }> {
|
|
332
|
+
if (signal?.aborted) return Promise.resolve({ stdout: "", raw: new Uint8Array(), stderr: "", exit: null, stopped: "cancelled" });
|
|
330
333
|
return new Promise((resolve) => {
|
|
331
|
-
const child = spawn(cmd, [...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
332
|
-
|
|
333
|
-
let
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
334
|
+
const child = spawn(cmd, [...args], { stdio: ["ignore", "pipe", "pipe"], detached: true });
|
|
335
|
+
const stdout: Buffer[] = [], stderr: Buffer[] = [];
|
|
336
|
+
let size = 0, errorSize = 0, stopped: "timeout" | "cancelled" | "output_limit" | undefined;
|
|
337
|
+
let escalation: ReturnType<typeof setTimeout> | undefined;
|
|
338
|
+
const kill = (value: "SIGTERM" | "SIGKILL") => { try {
|
|
339
|
+
if (child.pid && process.platform !== "win32") process.kill(-child.pid, value); else child.kill(value);
|
|
340
|
+
} catch { /* the process group already exited */ } };
|
|
341
|
+
const stop = (reason: "timeout" | "cancelled" | "output_limit") => {
|
|
342
|
+
if (stopped) return; stopped = reason; kill("SIGTERM");
|
|
343
|
+
escalation = setTimeout(() => kill("SIGKILL"), COMMAND_TERMINATION_GRACE_MS);
|
|
344
|
+
};
|
|
345
|
+
const abort = () => stop("cancelled");
|
|
346
|
+
child.stdout.on("data", (chunk: Buffer) => {
|
|
347
|
+
const kept = chunk.subarray(0, Math.max(0, maxBytes - size));
|
|
348
|
+
if (kept.length) stdout.push(Buffer.from(kept)); size += kept.length;
|
|
349
|
+
if (kept.length < chunk.length) stop("output_limit");
|
|
338
350
|
});
|
|
339
|
-
child.stderr.on("data", (chunk:
|
|
340
|
-
stderr
|
|
351
|
+
child.stderr.on("data", (chunk: Buffer) => {
|
|
352
|
+
// Errors are bounded too; stderr is explanatory, never a source of success or completeness.
|
|
353
|
+
const kept = chunk.subarray(0, Math.max(0, 65536 - errorSize));
|
|
354
|
+
if (kept.length) stderr.push(Buffer.from(kept)); errorSize += kept.length;
|
|
355
|
+
if (kept.length < chunk.length) stop("output_limit");
|
|
341
356
|
});
|
|
342
|
-
const timer = setTimeout(() =>
|
|
357
|
+
const timer = setTimeout(() => stop("timeout"), timeoutMs);
|
|
358
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
359
|
+
if (signal?.aborted) abort();
|
|
343
360
|
child.on("close", (code) => {
|
|
344
|
-
clearTimeout(timer);
|
|
345
|
-
|
|
361
|
+
clearTimeout(timer); clearTimeout(escalation); signal?.removeEventListener("abort", abort);
|
|
362
|
+
if (stopped) kill("SIGKILL");
|
|
363
|
+
const raw = Buffer.concat(stdout);
|
|
364
|
+
const rawErrors = Buffer.concat(stderr);
|
|
365
|
+
resolve({ stdout: raw.toString("utf8"), raw, rawErrors, stderr: rawErrors.toString("utf8"), exit: code, ...(stopped ? { stopped } : {}) });
|
|
346
366
|
});
|
|
347
367
|
child.on("error", (err) => {
|
|
348
|
-
|
|
349
|
-
|
|
368
|
+
const raw = Buffer.concat(stdout);
|
|
369
|
+
clearTimeout(timer); clearTimeout(escalation); signal?.removeEventListener("abort", abort);
|
|
370
|
+
const facts = child.pid ? READ_ATTEMPTED : NOT_STARTED;
|
|
371
|
+
const os = (err as { code?: string }).code;
|
|
372
|
+
const diagnostic = !child.pid && (os === "ENOENT" || os === "ENOTDIR") ? failureDiagnostic("environment_missing", err.message, facts) : failureFromError(err, facts);
|
|
373
|
+
resolve({ stdout: raw.toString("utf8"), raw, stderr: err.message, exit: null, diagnostic, ...(stopped ? { stopped } : {}) });
|
|
350
374
|
});
|
|
351
375
|
});
|
|
352
376
|
}
|
|
@@ -381,13 +405,13 @@ function parseRgJson(root: string, stdout: string): MatchLine[] {
|
|
|
381
405
|
const pathObj = d["path"];
|
|
382
406
|
const pathText =
|
|
383
407
|
typeof pathObj === "object" && pathObj !== null
|
|
384
|
-
?
|
|
408
|
+
? rgText(pathObj as Record<string, unknown>)
|
|
385
409
|
: "";
|
|
386
410
|
const lineNumber = d["line_number"];
|
|
387
411
|
const linesObj = d["lines"];
|
|
388
412
|
const lineText =
|
|
389
413
|
typeof linesObj === "object" && linesObj !== null
|
|
390
|
-
?
|
|
414
|
+
? rgText(linesObj as Record<string, unknown>)
|
|
391
415
|
: "";
|
|
392
416
|
if (pathText.length === 0 || typeof lineNumber !== "number") continue;
|
|
393
417
|
const rel = posixRel(root, pathText) ?? posixRel(root, join(root, pathText));
|
|
@@ -401,6 +425,10 @@ function parseRgJson(root: string, stdout: string): MatchLine[] {
|
|
|
401
425
|
return hits;
|
|
402
426
|
}
|
|
403
427
|
|
|
428
|
+
function rgText(value: Record<string, unknown>): string {
|
|
429
|
+
return typeof value["text"] === "string" ? value["text"] : typeof value["bytes"] === "string" ? Buffer.from(value["bytes"], "base64").toString("utf8") : "";
|
|
430
|
+
}
|
|
431
|
+
|
|
404
432
|
async function grepRipgrep(opts: GrepOpts, abs: string, limit: number): Promise<SearchResult | null> {
|
|
405
433
|
const rg = findOnPath("rg");
|
|
406
434
|
if (rg === null) return null;
|
|
@@ -416,17 +444,16 @@ async function grepRipgrep(opts: GrepOpts, abs: string, limit: number): Promise<
|
|
|
416
444
|
];
|
|
417
445
|
if (opts.glob !== undefined) args.push("--glob", opts.glob);
|
|
418
446
|
args.push("--", opts.pattern, abs);
|
|
419
|
-
const ran = await runCommand(rg, args, opts.timeoutMs ?? PROCESS_TIMEOUT_MS);
|
|
420
|
-
// 0 = matches, 1 = none; anything else is a real failure (bad pattern, etc.).
|
|
421
|
-
if (ran.exit !== 0 && ran.exit !== 1) {
|
|
422
|
-
const err = ran.stderr.trim() || `ripgrep exited ${ran.exit ?? "null"}`;
|
|
423
|
-
return { content: err, ok: false };
|
|
424
|
-
}
|
|
447
|
+
const ran = await runCommand(rg, args, opts.timeoutMs ?? PROCESS_TIMEOUT_MS, opts.signal, opts.maxCaptureBytes);
|
|
425
448
|
const hits = parseRgJson(opts.root, ran.stdout);
|
|
426
449
|
hits.sort(compareMatch);
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
450
|
+
const failed = ran.stopped !== undefined || ran.exit !== 0 && ran.exit !== 1;
|
|
451
|
+
const reason = ran.stopped ?? "engine_error";
|
|
452
|
+
const observation: SearchObservation = { engine: "ripgrep", raw: ran.raw, ...(ran.rawErrors ? { rawErrors: ran.rawErrors } : {}), rows: hits.map(hit => ({ prefix: `${hit.path}:${hit.line}: `, text: hit.text })),
|
|
453
|
+
sources: [], limit, empty: "No matches found", scan: { status: failed ? "partial" : "unknown", examined_files: null, source_versions: "unknown",
|
|
454
|
+
reasons: [{ code: "engine_does_not_report_all_skipped_files" }, ...(failed ? [{ code: reason }] : [])] } };
|
|
455
|
+
return observed(observation, ran.diagnostic ?? (failed ? failureDiagnostic(reason === "timeout" || reason === "cancelled" ? reason : "execution_failed",
|
|
456
|
+
ran.stderr.trim() || `ripgrep ${reason}; exit ${ran.exit ?? "unknown"}`, READ_ATTEMPTED) : undefined));
|
|
430
457
|
}
|
|
431
458
|
|
|
432
459
|
function compareMatch(a: MatchLine, b: MatchLine): number {
|
|
@@ -438,61 +465,80 @@ async function grepWalk(opts: GrepOpts, abs: string, kind: "file" | "dir", rel:
|
|
|
438
465
|
try {
|
|
439
466
|
new RegExp(opts.pattern);
|
|
440
467
|
} catch (err) {
|
|
441
|
-
return
|
|
468
|
+
return searchFailure("invalid_input", `invalid regex: ${(err as Error).message}`);
|
|
442
469
|
}
|
|
443
470
|
|
|
444
|
-
const
|
|
471
|
+
const deadline = performance.now() + (opts.timeoutMs ?? PROCESS_TIMEOUT_MS);
|
|
472
|
+
const files = await listedFiles(abs, rel, kind, { deadline, ...(opts.signal ? { signal: opts.signal } : {}),
|
|
473
|
+
...(opts.maxIndexEntries === undefined ? {} : { maxEntries: opts.maxIndexEntries }) });
|
|
445
474
|
const glob = opts.glob;
|
|
446
|
-
const filtered = glob === undefined ? files : files.filter((p) => globMatches(glob, p));
|
|
475
|
+
const filtered = glob === undefined ? files.paths : files.paths.filter((p) => globMatches(glob, p));
|
|
447
476
|
const matcher = new LineMatcher();
|
|
448
477
|
const hits: MatchLine[] = [];
|
|
449
|
-
const
|
|
478
|
+
const reasons: SearchObservation["scan"]["reasons"] = [...files.issues];
|
|
479
|
+
const sources: SearchObservation["sources"] = [];
|
|
480
|
+
let examined = 0, capturedBytes = 0;
|
|
481
|
+
const budget = Math.min(opts.maxCaptureBytes ?? MAX_SEARCH_CAPTURE_BYTES, MAX_SEARCH_CAPTURE_BYTES);
|
|
450
482
|
try {
|
|
451
483
|
for (const fileRel of filtered) {
|
|
484
|
+
if (opts.signal?.aborted || performance.now() >= deadline) { reasons.push({ code: opts.signal?.aborted ? "cancelled" : "timeout" }); break; }
|
|
452
485
|
if (ignoredRel(fileRel)) continue;
|
|
453
486
|
const fileAbs = join(opts.root, fileRel);
|
|
454
487
|
let st;
|
|
455
488
|
try {
|
|
456
489
|
st = await lstat(fileAbs);
|
|
457
490
|
} catch {
|
|
491
|
+
reasons.push({ code: "unreadable_file", path: fileRel });
|
|
458
492
|
continue;
|
|
459
493
|
}
|
|
460
|
-
if (st.isSymbolicLink() || !st.isFile()) continue;
|
|
461
|
-
if (st.size > MAX_FILE_BYTES) continue;
|
|
494
|
+
if (st.isSymbolicLink() || !st.isFile()) { reasons.push({ code: "changed_file_type", path: fileRel }); continue; }
|
|
495
|
+
if (st.size > MAX_FILE_BYTES) { reasons.push({ code: "file_size_limit", path: fileRel }); continue; }
|
|
462
496
|
let text: string;
|
|
463
497
|
try {
|
|
464
|
-
|
|
498
|
+
const raw = await readFile(fileAbs);
|
|
499
|
+
if (raw.length > MAX_FILE_BYTES) { reasons.push({ code: "file_size_limit", path: fileRel }); continue; }
|
|
500
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(raw);
|
|
501
|
+
sources.push({ path: fileRel, sha256: blobAddress(raw) }); examined++;
|
|
465
502
|
} catch {
|
|
503
|
+
reasons.push({ code: "unreadable_or_non_utf8_file", path: fileRel });
|
|
466
504
|
continue;
|
|
467
505
|
}
|
|
468
|
-
if (text.includes("\0")) continue;
|
|
506
|
+
if (text.includes("\0")) { reasons.push({ code: "binary_file", path: fileRel }); continue; }
|
|
469
507
|
const lines = splitLines(text);
|
|
470
|
-
|
|
508
|
+
// Reserve the worst-case formatted hits before matching, never discard already observed hits.
|
|
509
|
+
const reservation = Buffer.byteLength(text) + lines.length * (Buffer.byteLength(fileRel) + String(MAX_FILE_BYTES).length + 3);
|
|
510
|
+
if (capturedBytes + reservation > budget) { reasons.push({ code: "capture_budget", path: fileRel }); break; }
|
|
511
|
+
const matched = await matcher.match(opts.pattern, "", lines, deadline - performance.now(), opts.signal);
|
|
512
|
+
if (matched === "cancelled") { reasons.push({ code: "cancelled", path: fileRel }); break; }
|
|
471
513
|
if (matched === "timeout") {
|
|
472
|
-
|
|
514
|
+
reasons.push({ code: "match_timeout", path: fileRel });
|
|
473
515
|
continue;
|
|
474
516
|
}
|
|
475
517
|
if (matched === "invalid") {
|
|
476
|
-
|
|
518
|
+
reasons.push({ code: "matcher_failed", path: fileRel }); break;
|
|
519
|
+
}
|
|
520
|
+
for (const hit of matched) {
|
|
521
|
+
hits.push({ path: fileRel, line: hit.line, text: hit.text });
|
|
522
|
+
capturedBytes += Buffer.byteLength(`${fileRel}:${hit.line}: ${hit.text}\n`);
|
|
477
523
|
}
|
|
478
|
-
for (const hit of matched) hits.push({ path: fileRel, line: hit.line, text: hit.text });
|
|
479
524
|
}
|
|
480
525
|
} finally {
|
|
481
526
|
matcher.close();
|
|
482
527
|
}
|
|
483
528
|
|
|
484
529
|
hits.sort(compareMatch);
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
530
|
+
const stopped = reasons.find(reason => ["timeout", "cancelled"].includes(reason.code));
|
|
531
|
+
return observed({ engine: "walk", rows: hits.map(hit => ({ prefix: `${hit.path}:${hit.line}: `, text: hit.text })), sources, limit, empty: "No matches found",
|
|
532
|
+
scan: { status: reasons.length ? "partial" : "complete", reasons, examined_files: examined, source_versions: "recorded" } },
|
|
533
|
+
stopped ? failureDiagnostic(stopped.code === "cancelled" ? "cancelled" : "timeout", `search ${stopped.code}`, READ_ATTEMPTED) : undefined);
|
|
488
534
|
}
|
|
489
535
|
|
|
490
536
|
export async function grep(opts: GrepOpts): Promise<SearchResult> {
|
|
491
537
|
const pattern = opts.pattern;
|
|
492
|
-
if (pattern.length === 0) return
|
|
538
|
+
if (pattern.length === 0) return searchFailure("invalid_input", "grep needs a pattern");
|
|
493
539
|
|
|
494
540
|
const target = await inspectTarget(opts.root, opts.path);
|
|
495
|
-
if ("error" in target) return { content: target.error, ok: false };
|
|
541
|
+
if ("error" in target) return { content: target.error, ok: false, diagnostic: target.diagnostic };
|
|
496
542
|
|
|
497
543
|
const limit = opts.limit !== undefined && opts.limit > 0 ? Math.floor(opts.limit) : DEFAULT_MATCH_LIMIT;
|
|
498
544
|
const engine = opts.engine ?? "auto";
|
|
@@ -502,7 +548,7 @@ export async function grep(opts: GrepOpts): Promise<SearchResult> {
|
|
|
502
548
|
return grepWalk(opts, opts.path, target.kind, target.rel, limit);
|
|
503
549
|
case "ripgrep": {
|
|
504
550
|
const got = await grepRipgrep(opts, opts.path, limit);
|
|
505
|
-
if (got === null) return
|
|
551
|
+
if (got === null) return searchFailure("environment_missing", "ripgrep is not on PATH");
|
|
506
552
|
return got;
|
|
507
553
|
}
|
|
508
554
|
case "auto": {
|
|
@@ -532,6 +578,7 @@ async function findFd(opts: FindOpts, abs: string, limit: number): Promise<Searc
|
|
|
532
578
|
"--hidden",
|
|
533
579
|
"--no-ignore",
|
|
534
580
|
"--absolute-path",
|
|
581
|
+
"--print0",
|
|
535
582
|
...fdExcludeArgs(),
|
|
536
583
|
];
|
|
537
584
|
let pattern = opts.pattern;
|
|
@@ -540,36 +587,40 @@ async function findFd(opts: FindOpts, abs: string, limit: number): Promise<Searc
|
|
|
540
587
|
if (!pattern.startsWith("**/") && pattern !== "**") pattern = `**/${pattern}`;
|
|
541
588
|
}
|
|
542
589
|
args.push("--", pattern, abs);
|
|
543
|
-
const ran = await runCommand(fd, args, opts.timeoutMs ?? PROCESS_TIMEOUT_MS);
|
|
544
|
-
if (ran.exit !== 0 && ran.stdout.trim().length === 0) {
|
|
545
|
-
const err = ran.stderr.trim() || `fd exited ${ran.exit ?? "null"}`;
|
|
546
|
-
return { content: err, ok: false };
|
|
547
|
-
}
|
|
590
|
+
const ran = await runCommand(fd, args, opts.timeoutMs ?? PROCESS_TIMEOUT_MS, opts.signal, opts.maxCaptureBytes);
|
|
548
591
|
const paths: string[] = [];
|
|
549
|
-
|
|
550
|
-
|
|
592
|
+
// A partial final record is not a path. NUL preserves spaces and newlines in real filenames.
|
|
593
|
+
for (const line of ran.stdout.split("\0").slice(0, -1)) {
|
|
551
594
|
if (line.length === 0) continue;
|
|
552
595
|
const rel = posixRel(opts.root, line);
|
|
553
596
|
if (rel === null || ignoredRel(rel)) continue;
|
|
554
597
|
paths.push(rel);
|
|
555
598
|
}
|
|
556
599
|
paths.sort((a, b) => (a < b ? -1 : 1));
|
|
557
|
-
|
|
558
|
-
|
|
600
|
+
const failed = ran.stopped !== undefined || ran.exit !== 0;
|
|
601
|
+
const reason = ran.stopped ?? "engine_error";
|
|
602
|
+
return observed({ engine: "fd", raw: ran.raw, ...(ran.rawErrors ? { rawErrors: ran.rawErrors } : {}), rows: paths.map(text => ({ prefix: "", text })), limit, empty: "No files found matching pattern", sources: [],
|
|
603
|
+
scan: { status: failed ? "partial" : "unknown", examined_files: null, source_versions: "unknown",
|
|
604
|
+
reasons: [{ code: "engine_does_not_report_all_skipped_files" }, ...(failed ? [{ code: reason }] : [])] } },
|
|
605
|
+
ran.diagnostic ?? (failed ? failureDiagnostic(reason === "timeout" || reason === "cancelled" ? reason : "execution_failed",
|
|
606
|
+
ran.stderr.trim() || `fd ${reason}; exit ${ran.exit ?? "unknown"}`, READ_ATTEMPTED) : undefined));
|
|
559
607
|
}
|
|
560
608
|
|
|
561
609
|
async function findWalk(opts: FindOpts, abs: string, kind: "file" | "dir", rel: string, limit: number): Promise<SearchResult> {
|
|
562
|
-
const files = await listedFiles(abs, rel, kind)
|
|
563
|
-
|
|
610
|
+
const files = await listedFiles(abs, rel, kind, { deadline: performance.now() + (opts.timeoutMs ?? PROCESS_TIMEOUT_MS),
|
|
611
|
+
...(opts.signal ? { signal: opts.signal } : {}), ...(opts.maxIndexEntries === undefined ? {} : { maxEntries: opts.maxIndexEntries }) });
|
|
612
|
+
const paths = files.paths.filter((p) => !ignoredRel(p) && globMatches(opts.pattern, p));
|
|
564
613
|
paths.sort((a, b) => (a < b ? -1 : 1));
|
|
565
|
-
|
|
566
|
-
return {
|
|
614
|
+
const stopped = files.issues.find(reason => ["timeout", "cancelled"].includes(reason.code));
|
|
615
|
+
return observed({ engine: "walk", rows: paths.map(text => ({ prefix: "", text })), sources: [], limit, empty: "No files found matching pattern",
|
|
616
|
+
scan: { status: files.issues.length ? "partial" : "complete", reasons: files.issues, examined_files: files.paths.length, source_versions: "unknown" } },
|
|
617
|
+
stopped ? failureDiagnostic(stopped.code === "cancelled" ? "cancelled" : "timeout", `search ${stopped.code}`, READ_ATTEMPTED) : undefined);
|
|
567
618
|
}
|
|
568
619
|
|
|
569
620
|
export async function find(opts: FindOpts): Promise<SearchResult> {
|
|
570
|
-
if (opts.pattern.length === 0) return
|
|
621
|
+
if (opts.pattern.length === 0) return searchFailure("invalid_input", "find needs a pattern");
|
|
571
622
|
const target = await inspectTarget(opts.root, opts.path);
|
|
572
|
-
if ("error" in target) return { content: target.error, ok: false };
|
|
623
|
+
if ("error" in target) return { content: target.error, ok: false, diagnostic: target.diagnostic };
|
|
573
624
|
const limit = opts.limit !== undefined && opts.limit > 0 ? Math.floor(opts.limit) : DEFAULT_FIND_LIMIT;
|
|
574
625
|
const engine = opts.engine ?? "auto";
|
|
575
626
|
switch (engine) {
|
|
@@ -577,7 +628,7 @@ export async function find(opts: FindOpts): Promise<SearchResult> {
|
|
|
577
628
|
return findWalk(opts, opts.path, target.kind, target.rel, limit);
|
|
578
629
|
case "fd": {
|
|
579
630
|
const got = await findFd(opts, opts.path, limit);
|
|
580
|
-
if (got === null) return
|
|
631
|
+
if (got === null) return searchFailure("environment_missing", "fd is not on PATH");
|
|
581
632
|
return got;
|
|
582
633
|
}
|
|
583
634
|
case "auto": {
|
|
@@ -593,15 +644,16 @@ export async function find(opts: FindOpts): Promise<SearchResult> {
|
|
|
593
644
|
}
|
|
594
645
|
|
|
595
646
|
export async function ls(opts: LsOpts): Promise<SearchResult> {
|
|
647
|
+
if (opts.signal?.aborted) return searchFailure("cancelled", "directory listing cancelled");
|
|
596
648
|
const target = await inspectTarget(opts.root, opts.path);
|
|
597
|
-
if ("error" in target) return { content: target.error, ok: false };
|
|
598
|
-
if (target.kind !== "dir") return
|
|
649
|
+
if ("error" in target) return { content: target.error, ok: false, diagnostic: target.diagnostic };
|
|
650
|
+
if (target.kind !== "dir") return searchFailure("invalid_input", `not a directory: ${target.rel || "."}`);
|
|
599
651
|
|
|
600
652
|
let entries;
|
|
601
653
|
try {
|
|
602
654
|
entries = await readdir(opts.path, { withFileTypes: true });
|
|
603
655
|
} catch (err) {
|
|
604
|
-
return { content: `cannot list ${target.rel || "."}: ${(err as Error).message}`, ok: false };
|
|
656
|
+
return { content: `cannot list ${target.rel || "."}: ${(err as Error).message}`, ok: false, diagnostic: failureFromError(err, READ_ATTEMPTED) };
|
|
605
657
|
}
|
|
606
658
|
|
|
607
659
|
const names: string[] = [];
|
|
@@ -626,19 +678,6 @@ export async function ls(opts: LsOpts): Promise<SearchResult> {
|
|
|
626
678
|
});
|
|
627
679
|
|
|
628
680
|
const limit = opts.limit !== undefined && opts.limit > 0 ? Math.floor(opts.limit) : DEFAULT_LS_LIMIT;
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
const shown = names.slice(0, limit);
|
|
632
|
-
let body = shown.join("\n");
|
|
633
|
-
let extra = omitted;
|
|
634
|
-
while (shown.length > 0 && body.length > MAX_OUTPUT_BYTES) {
|
|
635
|
-
shown.pop();
|
|
636
|
-
extra += 1;
|
|
637
|
-
body = shown.join("\n");
|
|
638
|
-
}
|
|
639
|
-
if (extra > 0) {
|
|
640
|
-
const note = `[${extra} more matches omitted]`;
|
|
641
|
-
body = body.length > 0 ? `${body}\n${note}` : note;
|
|
642
|
-
}
|
|
643
|
-
return { content: body, ok: true };
|
|
681
|
+
return observed({ engine: "directory", rows: names.map(text => ({ prefix: "", text })), sources: [], limit, empty: "(empty directory)",
|
|
682
|
+
scan: { status: "complete", reasons: [], examined_files: names.length, source_versions: "unknown" } });
|
|
644
683
|
}
|
|
@@ -40,6 +40,10 @@ async function referencedBlobs(dir: string, fallback?: string): Promise<Map<stri
|
|
|
40
40
|
const add = (sha: string) => { refs.set(sha, [...new Set([...(refs.get(sha) ?? []), ...roots])]); };
|
|
41
41
|
for (const event of read.events) {
|
|
42
42
|
if (event.t === "snapshot") for (const sha of [...Object.values(event.added), ...Object.values(event.changed)]) add(sha);
|
|
43
|
+
if (event.t === "checkpoint") for (const change of Object.values(event.changes)) {
|
|
44
|
+
if (change.before) add(change.before.sha256);
|
|
45
|
+
if (change.after) add(change.after.sha256);
|
|
46
|
+
}
|
|
43
47
|
if (event.t === "user" || event.t === "tool_result") for (const image of event.images ?? []) { add(image.sha256); if (image.original) add(image.original.sha256); }
|
|
44
48
|
}
|
|
45
49
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** D11: cancelled source capture never becomes a partial successful observation. */
|
|
2
|
+
import { ToolFault, READ_ATTEMPTED } from "../core/tool-failure.ts";
|
|
3
|
+
|
|
4
|
+
export function checkSourceCancellation(signal?: AbortSignal): void {
|
|
5
|
+
if (signal?.aborted) throw new ToolFault("cancelled", "source capture cancelled", READ_ATTEMPTED);
|
|
6
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The last update check, kept in the config directory (docs/tasks/feat-update-check.md).
|
|
3
|
+
*
|
|
4
|
+
* `{ checkedAt, latest? }` and nothing else: no identifiers, no history. A
|
|
5
|
+
* missing or unreadable file means "never checked"; it is rewritten whole.
|
|
6
|
+
*/
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
8
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import type { UpdateCache } from "../core/update-check.ts";
|
|
12
|
+
import { atomicWrite } from "./atomic.ts";
|
|
13
|
+
import { configDir } from "./config.ts";
|
|
14
|
+
|
|
15
|
+
export function updateCachePath(): string { return join(configDir(), "update-check.json"); }
|
|
16
|
+
|
|
17
|
+
export async function readUpdateCache(): Promise<UpdateCache | null> {
|
|
18
|
+
try {
|
|
19
|
+
const value = JSON.parse(await readFile(updateCachePath(), "utf8")) as Record<string, unknown>;
|
|
20
|
+
if (typeof value["checkedAt"] !== "number") return null;
|
|
21
|
+
return { checkedAt: value["checkedAt"], ...(typeof value["latest"] === "string" ? { latest: value["latest"] } : {}) };
|
|
22
|
+
} catch { return null; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function writeUpdateCache(cache: UpdateCache): Promise<void> {
|
|
26
|
+
try { await atomicWrite(updateCachePath(), JSON.stringify(cache) + "\n"); } catch { /* a read-only config dir only means checking again next time */ }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** This copy's package root, resolved through symlinks, and whether it is a git checkout. */
|
|
30
|
+
export function packageLocation(): { root: string; git: boolean } {
|
|
31
|
+
// src/store/update-cache.ts → the package root two directories up.
|
|
32
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
let root = join(here, "..", "..");
|
|
34
|
+
try { root = realpathSync(root); } catch { /* keep the unresolved path */ }
|
|
35
|
+
return { root, git: existsSync(join(root, ".git")) };
|
|
36
|
+
}
|