@visual-z/xeno 0.8.4 → 0.9.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 +8 -0
- package/README.md +31 -746
- package/bin/xeno.exe +26 -0
- package/install.cjs +88 -0
- package/package.json +18 -48
- package/docs/app-server.md +0 -115
- package/docs/foundations.md +0 -88
- package/docs/install.md +0 -244
- package/docs/providers.md +0 -172
- package/docs/research/foundation-clients-2026-10-09.md +0 -27
- package/docs/research/foundation-editing-2026-10-09.md +0 -56
- package/docs/research/foundation-network-2026-10-09.md +0 -37
- package/docs/research/foundation-workflow-2026-10-09.md +0 -37
- package/src/apps/acp-server.ts +0 -41
- package/src/apps/agent-profiles.ts +0 -19
- package/src/apps/app-server.ts +0 -30
- package/src/apps/approve.ts +0 -49
- package/src/apps/cli.ts +0 -99
- package/src/apps/commands.ts +0 -319
- package/src/apps/completions.ts +0 -87
- package/src/apps/control-transport.ts +0 -90
- package/src/apps/export-cli.ts +0 -24
- package/src/apps/goal-flow.ts +0 -70
- package/src/apps/image-draft.ts +0 -46
- package/src/apps/input.ts +0 -312
- package/src/apps/model-catalog.ts +0 -69
- package/src/apps/preferences.ts +0 -20
- package/src/apps/project-trust.ts +0 -9
- package/src/apps/provider-connect.ts +0 -346
- package/src/apps/question-ui.ts +0 -27
- package/src/apps/render.ts +0 -181
- package/src/apps/resources.ts +0 -66
- package/src/apps/review.ts +0 -109
- package/src/apps/session-hub.ts +0 -96
- package/src/apps/session-lifecycle.ts +0 -64
- package/src/apps/shell.ts +0 -2071
- package/src/apps/startup-input.ts +0 -135
- package/src/apps/startup.ts +0 -1677
- package/src/apps/ui/app.ts +0 -1818
- package/src/apps/ui/control-model.ts +0 -43
- package/src/apps/ui/extension-host.ts +0 -194
- package/src/apps/ui/image-cache.ts +0 -121
- package/src/apps/ui/palette.ts +0 -88
- package/src/apps/ui/project.ts +0 -127
- package/src/apps/ui/standalone.ts +0 -148
- package/src/apps/ui/status-model.ts +0 -68
- package/src/apps/ui/surface.ts +0 -91
- package/src/apps/ui/theme.ts +0 -86
- package/src/apps/update-check.ts +0 -112
- package/src/apps/workflow.ts +0 -58
- package/src/core/acceptance.ts +0 -139
- package/src/core/action-defaults.ts +0 -58
- package/src/core/activity-status.ts +0 -52
- package/src/core/agent-profile.ts +0 -130
- package/src/core/attachment-input.ts +0 -17
- package/src/core/blind-checks.ts +0 -72
- package/src/core/boundary.ts +0 -174
- package/src/core/branch-receipt.ts +0 -26
- package/src/core/catalog-merge.ts +0 -19
- package/src/core/checkpoints.ts +0 -85
- package/src/core/churn.ts +0 -61
- package/src/core/command-artifacts.ts +0 -94
- package/src/core/command-contract.ts +0 -151
- package/src/core/completion-action.ts +0 -20
- package/src/core/completion-review.ts +0 -98
- package/src/core/context-detail.ts +0 -37
- package/src/core/context-policy.ts +0 -133
- package/src/core/context-window.ts +0 -54
- package/src/core/continuation.ts +0 -130
- package/src/core/control-protocol.ts +0 -69
- package/src/core/control-view.ts +0 -39
- package/src/core/cycle.ts +0 -17
- package/src/core/data-query.ts +0 -201
- package/src/core/deadline.ts +0 -40
- package/src/core/derive.ts +0 -516
- package/src/core/draft-images.ts +0 -56
- package/src/core/edit-plan.ts +0 -200
- package/src/core/environment.ts +0 -181
- package/src/core/event-validation.ts +0 -220
- package/src/core/events.ts +0 -670
- package/src/core/evidence-ladder.ts +0 -62
- package/src/core/evidence-links.ts +0 -68
- package/src/core/evidence.ts +0 -240
- package/src/core/execution.ts +0 -18
- package/src/core/exhaustive.ts +0 -18
- package/src/core/export.ts +0 -179
- package/src/core/extension-flags.ts +0 -14
- package/src/core/external-effects.ts +0 -488
- package/src/core/failure-recovery.ts +0 -173
- package/src/core/file-precondition.ts +0 -78
- package/src/core/file-references.ts +0 -35
- package/src/core/frontmatter.ts +0 -213
- package/src/core/git-status.ts +0 -29
- package/src/core/goal-view.ts +0 -64
- package/src/core/goal.ts +0 -200
- package/src/core/handoff.ts +0 -479
- package/src/core/history-query.ts +0 -139
- package/src/core/history.ts +0 -237
- package/src/core/image-limits.ts +0 -72
- package/src/core/images.ts +0 -46
- package/src/core/integration-health.ts +0 -34
- package/src/core/json-event.ts +0 -131
- package/src/core/json-pointer.ts +0 -46
- package/src/core/keymap.ts +0 -153
- package/src/core/lsp-framing.ts +0 -34
- package/src/core/mcp-config.ts +0 -68
- package/src/core/model-picker.ts +0 -64
- package/src/core/model-preferences.ts +0 -28
- package/src/core/model-replay.ts +0 -22
- package/src/core/model-scope.ts +0 -53
- package/src/core/note-guidance.ts +0 -34
- package/src/core/oauth-fields.ts +0 -44
- package/src/core/observation-view.ts +0 -152
- package/src/core/outcome.ts +0 -44
- package/src/core/overflow.ts +0 -59
- package/src/core/packages.ts +0 -47
- package/src/core/patch.ts +0 -130
- package/src/core/path-suggestions.ts +0 -59
- package/src/core/permissions.ts +0 -410
- package/src/core/process-receipt.ts +0 -105
- package/src/core/process-state.ts +0 -37
- package/src/core/program-status.ts +0 -40
- package/src/core/progress.ts +0 -150
- package/src/core/project.ts +0 -431
- package/src/core/prompt-policy.ts +0 -46
- package/src/core/prompts.ts +0 -111
- package/src/core/question.ts +0 -94
- package/src/core/queue.ts +0 -78
- package/src/core/read-bounds.ts +0 -24
- package/src/core/read-observation.ts +0 -106
- package/src/core/recovery-sources.ts +0 -74
- package/src/core/regex-dialect.ts +0 -25
- package/src/core/request-budget.ts +0 -23
- package/src/core/request-progress.ts +0 -87
- package/src/core/request-state.ts +0 -273
- package/src/core/resource-selection.ts +0 -36
- package/src/core/response-recovery.ts +0 -59
- package/src/core/resume.ts +0 -59
- package/src/core/review.ts +0 -68
- package/src/core/ripgrep-release.ts +0 -64
- package/src/core/runtime-target.ts +0 -94
- package/src/core/scheduling.ts +0 -8
- package/src/core/search-observation.ts +0 -97
- package/src/core/secrets.ts +0 -188
- package/src/core/session-archive.ts +0 -4
- package/src/core/session-browser.ts +0 -82
- package/src/core/session-entry.ts +0 -61
- package/src/core/session-exit.ts +0 -42
- package/src/core/session-format.ts +0 -169
- package/src/core/session-position.ts +0 -26
- package/src/core/session-tree.ts +0 -147
- package/src/core/settings.ts +0 -192
- package/src/core/skills.ts +0 -165
- package/src/core/sql-query.ts +0 -211
- package/src/core/stall.ts +0 -128
- package/src/core/stop-report.ts +0 -68
- package/src/core/text-normalize.ts +0 -81
- package/src/core/theme.ts +0 -166
- package/src/core/thinking.ts +0 -26
- package/src/core/tokens.ts +0 -151
- package/src/core/tool-contract.ts +0 -153
- package/src/core/tool-discovery.ts +0 -87
- package/src/core/tool-failure.ts +0 -81
- package/src/core/tool-input.ts +0 -44
- package/src/core/transcript.ts +0 -108
- package/src/core/tui-mode.ts +0 -29
- package/src/core/update-check.ts +0 -157
- package/src/core/version.ts +0 -55
- package/src/core/web-content.ts +0 -80
- package/src/core/work-state.ts +0 -154
- package/src/core/workflow-mode.ts +0 -34
- package/src/core/working-set.ts +0 -119
- package/src/core/yaml.ts +0 -282
- package/src/runtime/acp-agent.ts +0 -289
- package/src/runtime/agent-profiles.ts +0 -21
- package/src/runtime/auth.ts +0 -367
- package/src/runtime/blind-check.ts +0 -36
- package/src/runtime/branch-jobs.ts +0 -36
- package/src/runtime/catalog.ts +0 -285
- package/src/runtime/clipboard.ts +0 -125
- package/src/runtime/command-artifacts.ts +0 -64
- package/src/runtime/connection-reuse.ts +0 -40
- package/src/runtime/control-client.ts +0 -149
- package/src/runtime/control-connection.ts +0 -139
- package/src/runtime/counter-check.ts +0 -74
- package/src/runtime/environment-tools.ts +0 -60
- package/src/runtime/evidence-notes.ts +0 -35
- package/src/runtime/exec-sessions.ts +0 -456
- package/src/runtime/execution-control.ts +0 -40
- package/src/runtime/extension-loader.ts +0 -37
- package/src/runtime/extension-ui.ts +0 -47
- package/src/runtime/extensions.ts +0 -370
- package/src/runtime/formatter-tool.ts +0 -38
- package/src/runtime/formatter.ts +0 -50
- package/src/runtime/foundation.ts +0 -116
- package/src/runtime/history-cache.ts +0 -57
- package/src/runtime/history-tools.ts +0 -168
- package/src/runtime/images.ts +0 -80
- package/src/runtime/integrations.ts +0 -216
- package/src/runtime/loop.ts +0 -3108
- package/src/runtime/lsp-client.ts +0 -201
- package/src/runtime/mcp-client.ts +0 -264
- package/src/runtime/mcp-oauth.ts +0 -168
- package/src/runtime/mcp-schema.ts +0 -51
- package/src/runtime/model-config.ts +0 -232
- package/src/runtime/model-data.LICENSE +0 -27
- package/src/runtime/model-data.ts +0 -21
- package/src/runtime/model-discovery.ts +0 -208
- package/src/runtime/model-replay.ts +0 -10
- package/src/runtime/model-scope.ts +0 -20
- package/src/runtime/model-sources.ts +0 -151
- package/src/runtime/model.ts +0 -686
- package/src/runtime/models-fetch.ts +0 -57
- package/src/runtime/oauth-device.ts +0 -86
- package/src/runtime/oauth.ts +0 -853
- package/src/runtime/open-image.ts +0 -42
- package/src/runtime/openai-responses.ts +0 -304
- package/src/runtime/openai.ts +0 -697
- package/src/runtime/output-schema.ts +0 -26
- package/src/runtime/package-manager.ts +0 -125
- package/src/runtime/patch-tool.ts +0 -38
- package/src/runtime/permission-resources.ts +0 -18
- package/src/runtime/pi-provider.ts +0 -423
- package/src/runtime/program-status.ts +0 -38
- package/src/runtime/progress.ts +0 -26
- package/src/runtime/provider-access.ts +0 -21
- package/src/runtime/provider-connect.ts +0 -109
- package/src/runtime/provider-presets.ts +0 -98
- package/src/runtime/provider-protocols.ts +0 -29
- package/src/runtime/provider-request.ts +0 -87
- package/src/runtime/provider-tool-policy.ts +0 -33
- package/src/runtime/provider.ts +0 -580
- package/src/runtime/question-tool.ts +0 -62
- package/src/runtime/retry.ts +0 -447
- package/src/runtime/review-tool.ts +0 -48
- package/src/runtime/rich-text.ts +0 -36
- package/src/runtime/runtime-target.ts +0 -149
- package/src/runtime/secrets.ts +0 -79
- package/src/runtime/self-update.ts +0 -74
- package/src/runtime/session-control.ts +0 -265
- package/src/runtime/session-hub.ts +0 -85
- package/src/runtime/stream-idle.ts +0 -80
- package/src/runtime/tool-broker.ts +0 -137
- package/src/runtime/tool-discovery.ts +0 -58
- package/src/runtime/tool-queue.ts +0 -77
- package/src/runtime/tools.ts +0 -1820
- package/src/runtime/turn-effects.ts +0 -31
- package/src/runtime/update-check.ts +0 -48
- package/src/runtime/usage.ts +0 -253
- package/src/runtime/version.ts +0 -12
- package/src/runtime/web-http.ts +0 -56
- package/src/runtime/web-tools.ts +0 -88
- package/src/runtime/working-notes.ts +0 -60
- package/src/runtime/workspace-lifecycle.ts +0 -141
- package/src/store/acceptance.ts +0 -15
- package/src/store/agent-profile-files.ts +0 -13
- package/src/store/atomic.ts +0 -31
- package/src/store/auth-store.ts +0 -50
- package/src/store/blind-checks.ts +0 -32
- package/src/store/blobs.ts +0 -143
- package/src/store/checkpoint-history.ts +0 -32
- package/src/store/command-artifacts.ts +0 -96
- package/src/store/command.ts +0 -111
- package/src/store/config.ts +0 -69
- package/src/store/context-files.ts +0 -436
- package/src/store/file-checkpoints.ts +0 -108
- package/src/store/file-index.ts +0 -174
- package/src/store/foundation-config.ts +0 -24
- package/src/store/git-checkpoints.ts +0 -67
- package/src/store/git-status.ts +0 -15
- package/src/store/history-search.ts +0 -19
- package/src/store/log.ts +0 -725
- package/src/store/lsp-document.ts +0 -30
- package/src/store/mcp-credentials.ts +0 -85
- package/src/store/models.ts +0 -60
- package/src/store/observations.ts +0 -224
- package/src/store/packages.ts +0 -98
- package/src/store/patch.ts +0 -92
- package/src/store/permission-paths.ts +0 -34
- package/src/store/process-receipts.ts +0 -34
- package/src/store/query-source.ts +0 -49
- package/src/store/recovery.ts +0 -44
- package/src/store/replay.ts +0 -76
- package/src/store/resource-index.ts +0 -170
- package/src/store/resources.ts +0 -285
- package/src/store/restore.ts +0 -110
- package/src/store/review.ts +0 -109
- package/src/store/runtime-target.ts +0 -64
- package/src/store/search.ts +0 -728
- package/src/store/secrets.ts +0 -44
- package/src/store/serial.ts +0 -13
- package/src/store/session-archive.ts +0 -165
- package/src/store/session-browser.ts +0 -68
- package/src/store/session-position.ts +0 -27
- package/src/store/sessions.ts +0 -525
- package/src/store/source-cancellation.ts +0 -6
- package/src/store/tool-binaries.ts +0 -90
- package/src/store/trust.ts +0 -82
- package/src/store/update-cache.ts +0 -36
- package/src/store/user-settings.ts +0 -30
- package/src/store/workspace-clone.ts +0 -42
- package/src/store/workspace-observation.ts +0 -113
- package/src/store/workspace.ts +0 -115
- package/src/store/worktree.ts +0 -278
- package/src/tui/ansi-spans.ts +0 -126
- package/src/tui/diff.ts +0 -160
- package/src/tui/exhaustive.ts +0 -14
- package/src/tui/fuzzy.ts +0 -75
- package/src/tui/style.ts +0 -208
- package/src/tui/ui/atomic-draft.ts +0 -121
- package/src/tui/ui/branch-panel.ts +0 -47
- package/src/tui/ui/canvas.ts +0 -96
- package/src/tui/ui/colors.ts +0 -70
- package/src/tui/ui/composer.ts +0 -199
- package/src/tui/ui/control-panel.ts +0 -25
- package/src/tui/ui/control-sidebar.ts +0 -49
- package/src/tui/ui/dialog.ts +0 -398
- package/src/tui/ui/draft.ts +0 -23
- package/src/tui/ui/duration.ts +0 -16
- package/src/tui/ui/edge.ts +0 -186
- package/src/tui/ui/image-size.ts +0 -44
- package/src/tui/ui/ink.ts +0 -210
- package/src/tui/ui/items.ts +0 -42
- package/src/tui/ui/keys.ts +0 -40
- package/src/tui/ui/layout.ts +0 -74
- package/src/tui/ui/list-model.ts +0 -86
- package/src/tui/ui/list-view.ts +0 -172
- package/src/tui/ui/mascot.ts +0 -67
- package/src/tui/ui/messages.ts +0 -518
- package/src/tui/ui/motion.ts +0 -28
- package/src/tui/ui/paths.ts +0 -10
- package/src/tui/ui/pictures.ts +0 -108
- package/src/tui/ui/prompt-history.ts +0 -60
- package/src/tui/ui/screen.ts +0 -579
- package/src/tui/ui/status.ts +0 -403
- package/src/tui/ui/styled.ts +0 -85
- package/src/tui/ui/tool-card.ts +0 -150
- package/src/tui/width.ts +0 -567
- package/src/vendor/pi-rich/LICENSE.upstream +0 -21
- package/src/vendor/pi-rich/UPSTREAM.json +0 -17
- package/src/vendor/pi-rich/clipboard-command.ts +0 -44
- package/src/vendor/pi-rich/exif-orientation.ts +0 -182
- package/src/vendor/pi-rich/html.ts +0 -51
- package/src/vendor/pi-rich/image-convert.ts +0 -49
- package/src/vendor/pi-rich/image-resize-core.ts +0 -164
- package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
- package/src/vendor/pi-rich/image-resize.ts +0 -123
- package/src/vendor/pi-rich/mime.ts +0 -116
- package/src/vendor/pi-rich/photon.ts +0 -139
- package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
package/src/store/search.ts
DELETED
|
@@ -1,728 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workspace search: grep, find, ls.
|
|
3
|
-
*
|
|
4
|
-
* The walk reuses buildFileIndex so ignore rules (.gitignore inside a Git
|
|
5
|
-
* work tree, S.6 item 4), the walk cap, and the no-symlink rule stay one
|
|
6
|
-
* implementation. Ripgrep / fd follow the same .gitignore rule, and search an
|
|
7
|
-
* ignored target named explicitly, like the walk. They are used only when
|
|
8
|
-
* they are already on PATH -- never downloaded -- because fetching a binary
|
|
9
|
-
* at runtime is a supply-chain path the Isolation section of README.md
|
|
10
|
-
* told the user they would not be taking.
|
|
11
|
-
*
|
|
12
|
-
* Both engines format the same way so two machines do not silently disagree.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { spawn } from "node:child_process";
|
|
16
|
-
import { existsSync } from "node:fs";
|
|
17
|
-
import { lstat, readdir, readFile } from "node:fs/promises";
|
|
18
|
-
import { isAbsolute, join, relative, sep } from "node:path";
|
|
19
|
-
import { Worker } from "node:worker_threads";
|
|
20
|
-
import { assertNever } from "../core/exhaustive.ts";
|
|
21
|
-
import { jsPatternFromRipgrep } from "../core/regex-dialect.ts";
|
|
22
|
-
import { buildFileIndex, gitIgnored } from "./file-index.ts";
|
|
23
|
-
import { DEFAULT_IGNORE } from "./workspace.ts";
|
|
24
|
-
import { blobAddress } from "./blobs.ts";
|
|
25
|
-
import { configDir } from "./config.ts";
|
|
26
|
-
import { findRipgrep } from "./tool-binaries.ts";
|
|
27
|
-
import { previewSearchObservation, type SearchObservation } from "../core/search-observation.ts";
|
|
28
|
-
import { failureDiagnostic, failureFromError, READ_ATTEMPTED, NOT_STARTED } from "../core/tool-failure.ts";
|
|
29
|
-
import type { ToolDiagnostic } from "../core/tool-contract.ts";
|
|
30
|
-
import { COMMAND_TERMINATION_GRACE_MS } from "../core/command-contract.ts";
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Match / entry cap. Assumed, not measured: enough for a model to read in
|
|
34
|
-
* one result; past this it should refine the query. A silent cut would hide
|
|
35
|
-
* how much evidence was dropped.
|
|
36
|
-
*/
|
|
37
|
-
export const DEFAULT_MATCH_LIMIT = 100;
|
|
38
|
-
|
|
39
|
-
/** Assumed, not measured: same order of magnitude as pi's find default. */
|
|
40
|
-
export const DEFAULT_FIND_LIMIT = 1_000;
|
|
41
|
-
|
|
42
|
-
/** Assumed, not measured: a directory listing larger than this needs a filter. */
|
|
43
|
-
export const DEFAULT_LS_LIMIT = 500;
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Assumed, not measured: a longer line belongs in `read`, not in a match
|
|
47
|
-
* list. Cutting it without a cap would let one minified file blow the budget.
|
|
48
|
-
*/
|
|
49
|
-
export const MAX_LINE_CHARS = 500;
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Assumed, not measured: same ballpark as the tool-output cap in tools.ts.
|
|
53
|
-
* Hitting it drops trailing matches and says how many.
|
|
54
|
-
*/
|
|
55
|
-
export const MAX_OUTPUT_BYTES = 30_000;
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Assumed, not measured: a 1 MB file is already past what a match snippet
|
|
59
|
-
* can usefully show; ripgrep is told the same ceiling so the two engines
|
|
60
|
-
* skip the same files.
|
|
61
|
-
*/
|
|
62
|
-
export const MAX_FILE_BYTES = 1_000_000;
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Per-file match ceiling. A linear scan of MAX_FILE_BYTES with a simple
|
|
66
|
-
* pattern was measured at ~0.07ms/MB on bun 1.3; 100ms is assumed slack on
|
|
67
|
-
* top of that. `(a+)+$` against 40 a's was measured at ~220ms here -- JSC
|
|
68
|
-
* aborts the backtrack around there rather than hanging forever -- so a
|
|
69
|
-
* ceiling above that would never fire on the card's catastrophic case, and
|
|
70
|
-
* the agent would see "no matches" instead of a cutoff. The pattern comes
|
|
71
|
-
* from the model, not a person: a hung tool_call has no result in the log.
|
|
72
|
-
*/
|
|
73
|
-
export const MATCH_TIMEOUT_MS = 100;
|
|
74
|
-
|
|
75
|
-
/** Assumed: an rg/fd process that runs longer than this is stuck. */
|
|
76
|
-
export const PROCESS_TIMEOUT_MS = 30_000;
|
|
77
|
-
/** Assumed execution budget, independent of display caps; reaching it makes coverage partial. */
|
|
78
|
-
export const MAX_SEARCH_CAPTURE_BYTES = 32 * 1024 * 1024;
|
|
79
|
-
|
|
80
|
-
export type SearchEngine = "auto" | "ripgrep" | "walk" | "fd";
|
|
81
|
-
|
|
82
|
-
export interface SearchResult {
|
|
83
|
-
content: string;
|
|
84
|
-
ok: boolean;
|
|
85
|
-
observation?: SearchObservation;
|
|
86
|
-
diagnostic?: ToolDiagnostic;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export interface GrepOpts {
|
|
90
|
-
readonly root: string;
|
|
91
|
-
/** Absolute path, already confined to root. */
|
|
92
|
-
readonly path: string;
|
|
93
|
-
readonly pattern: string;
|
|
94
|
-
readonly glob?: string;
|
|
95
|
-
readonly limit?: number;
|
|
96
|
-
readonly engine?: "auto" | "ripgrep" | "walk";
|
|
97
|
-
readonly timeoutMs?: number;
|
|
98
|
-
readonly signal?: AbortSignal;
|
|
99
|
-
/** Trusted harness budgets, not model arguments. */
|
|
100
|
-
readonly maxCaptureBytes?: number;
|
|
101
|
-
readonly maxIndexEntries?: number;
|
|
102
|
-
/** Trusted admission for each POSIX workspace-relative resource, before content is read. */
|
|
103
|
-
readonly canRead?: (path: string) => Promise<boolean>;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export interface FindOpts {
|
|
107
|
-
readonly root: string;
|
|
108
|
-
readonly path: string;
|
|
109
|
-
readonly pattern: string;
|
|
110
|
-
readonly limit?: number;
|
|
111
|
-
readonly engine?: "auto" | "fd" | "walk";
|
|
112
|
-
readonly timeoutMs?: number;
|
|
113
|
-
readonly signal?: AbortSignal;
|
|
114
|
-
readonly maxCaptureBytes?: number;
|
|
115
|
-
readonly maxIndexEntries?: number;
|
|
116
|
-
readonly canRead?: (path: string) => Promise<boolean>;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export interface LsOpts {
|
|
120
|
-
readonly root: string;
|
|
121
|
-
readonly path: string;
|
|
122
|
-
readonly limit?: number;
|
|
123
|
-
readonly signal?: AbortSignal;
|
|
124
|
-
readonly canRead?: (path: string) => Promise<boolean>;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function observed(observation: SearchObservation, diagnostic?: ToolDiagnostic): SearchResult {
|
|
128
|
-
return { ok: diagnostic === undefined, content: previewSearchObservation(observation, "pending-archive", MAX_OUTPUT_BYTES, MAX_LINE_CHARS).text,
|
|
129
|
-
observation, ...(diagnostic ? { diagnostic } : {}) };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function searchFailure(code: ToolDiagnostic["code"], content: string): SearchResult {
|
|
133
|
-
return { ok: false, content, diagnostic: failureDiagnostic(code, content, NOT_STARTED) };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export interface MatchLine {
|
|
137
|
-
readonly path: string;
|
|
138
|
-
readonly line: number;
|
|
139
|
-
readonly text: string;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const MATCH_WORKER_SOURCE = `
|
|
143
|
-
const { parentPort } = require("node:worker_threads");
|
|
144
|
-
parentPort.on("message", (msg) => {
|
|
145
|
-
const { pattern, flags, lines } = msg;
|
|
146
|
-
try {
|
|
147
|
-
const re = new RegExp(pattern, flags);
|
|
148
|
-
const hits = [];
|
|
149
|
-
for (let i = 0; i < lines.length; i++) {
|
|
150
|
-
const line = lines[i];
|
|
151
|
-
re.lastIndex = 0;
|
|
152
|
-
if (re.test(line)) hits.push({ line: i + 1, text: line });
|
|
153
|
-
}
|
|
154
|
-
parentPort.postMessage({ ok: true, hits });
|
|
155
|
-
} catch (err) {
|
|
156
|
-
parentPort.postMessage({
|
|
157
|
-
ok: false,
|
|
158
|
-
error: err instanceof Error ? err.message : String(err),
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
`;
|
|
163
|
-
|
|
164
|
-
export function findOnPath(name: string): string | null {
|
|
165
|
-
const pathEnv = process.env["PATH"] ?? "";
|
|
166
|
-
const delim = process.platform === "win32" ? ";" : ":";
|
|
167
|
-
for (const dir of pathEnv.split(delim)) {
|
|
168
|
-
if (dir.length === 0) continue;
|
|
169
|
-
const candidate = join(dir, name);
|
|
170
|
-
if (existsSync(candidate)) return candidate;
|
|
171
|
-
}
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function posixRel(root: string, abs: string): string | null {
|
|
176
|
-
const rel = relative(root, abs);
|
|
177
|
-
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return null;
|
|
178
|
-
return rel.split(sep).join("/");
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function ignoredRel(rel: string): boolean {
|
|
182
|
-
if (rel.length === 0) return false;
|
|
183
|
-
const parts = rel.split("/");
|
|
184
|
-
return DEFAULT_IGNORE.some((name) => parts.includes(name));
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Convert a glob to a fully-anchored regexp. Assumed to cover the shapes
|
|
189
|
-
* models actually type (basename wildcards and double-star path globs), not
|
|
190
|
-
* the full gitignore grammar -- a missed edge is a missed file, not a hang.
|
|
191
|
-
*/
|
|
192
|
-
export function globToRegExp(glob: string): RegExp {
|
|
193
|
-
let i = 0;
|
|
194
|
-
let out = "";
|
|
195
|
-
while (i < glob.length) {
|
|
196
|
-
if (glob.startsWith("**/", i)) {
|
|
197
|
-
out += "(?:.*/)?";
|
|
198
|
-
i += 3;
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
if (glob[i] === "*" && glob[i + 1] === "*") {
|
|
202
|
-
out += ".*";
|
|
203
|
-
i += 2;
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
const c = glob[i]!;
|
|
207
|
-
if (c === "*") out += "[^/]*";
|
|
208
|
-
else if (c === "?") out += "[^/]";
|
|
209
|
-
else if (".+^${}()|[]\\".includes(c)) out += `\\${c}`;
|
|
210
|
-
else out += c;
|
|
211
|
-
i += 1;
|
|
212
|
-
}
|
|
213
|
-
return new RegExp(`^${out}$`);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export function globMatches(glob: string, relPath: string): boolean {
|
|
217
|
-
const posix = relPath.split(sep).join("/");
|
|
218
|
-
const slash = posix.lastIndexOf("/");
|
|
219
|
-
const base = slash === -1 ? posix : posix.slice(slash + 1);
|
|
220
|
-
const re = globToRegExp(glob);
|
|
221
|
-
if (glob.includes("/")) return re.test(posix);
|
|
222
|
-
return re.test(base);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function splitLines(text: string): string[] {
|
|
226
|
-
const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
227
|
-
if (normalized.length === 0) return [];
|
|
228
|
-
const trimmed = normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized;
|
|
229
|
-
if (trimmed.length === 0) return [];
|
|
230
|
-
return trimmed.split("\n");
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
async function inspectTarget(
|
|
234
|
-
root: string,
|
|
235
|
-
abs: string,
|
|
236
|
-
): Promise<{ kind: "file" | "dir"; rel: string } | { error: string; diagnostic: ToolDiagnostic }> {
|
|
237
|
-
const rel = posixRel(root, abs);
|
|
238
|
-
const refuse = (message: string) => ({ error: message, diagnostic: failureDiagnostic("permission_denied", message, NOT_STARTED) });
|
|
239
|
-
if (rel === null) return refuse(`path escapes the workspace root: ${abs}`);
|
|
240
|
-
if (ignoredRel(rel)) return refuse(`path is ignored: ${rel}`);
|
|
241
|
-
|
|
242
|
-
let st;
|
|
243
|
-
try {
|
|
244
|
-
let current = root;
|
|
245
|
-
for (const part of rel.split("/").slice(0, -1)) {
|
|
246
|
-
current = join(current, part);
|
|
247
|
-
if ((await lstat(current)).isSymbolicLink()) return refuse(`refusing to follow a symlink in path: ${rel || "."}`);
|
|
248
|
-
}
|
|
249
|
-
st = await lstat(abs);
|
|
250
|
-
} catch (error) {
|
|
251
|
-
return { error: `cannot inspect path: ${rel || "."}`, diagnostic: failureFromError(error, READ_ATTEMPTED) };
|
|
252
|
-
}
|
|
253
|
-
if (st.isSymbolicLink()) {
|
|
254
|
-
return refuse(`refusing to follow a symlink: ${rel || "."}`);
|
|
255
|
-
}
|
|
256
|
-
if (st.isDirectory()) return { kind: "dir", rel };
|
|
257
|
-
if (st.isFile()) return { kind: "file", rel };
|
|
258
|
-
const message = `not a file or directory: ${rel || "."}`;
|
|
259
|
-
return { error: message, diagnostic: failureDiagnostic("invalid_input", message, READ_ATTEMPTED) };
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
async function listedFiles(abs: string, rel: string, kind: "file" | "dir", options: Parameters<typeof buildFileIndex>[1] = {}) {
|
|
263
|
-
if (kind === "file") return { paths: ignoredRel(rel) ? [] : [rel], issues: [] };
|
|
264
|
-
const index = await buildFileIndex(abs, options);
|
|
265
|
-
return { paths: index.paths.map((p) => (rel.length === 0 ? p : `${rel}/${p}`)), issues: [...(index.issues ?? [])] };
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/** Denied filenames cannot reappear in scan diagnostics or archived observations. */
|
|
269
|
-
function policyIssues(issues: SearchObservation["scan"]["reasons"], canRead: GrepOpts["canRead"]): SearchObservation["scan"]["reasons"] {
|
|
270
|
-
return canRead ? issues.map(issue => ({ code: issue.code })) : [...issues];
|
|
271
|
-
}
|
|
272
|
-
async function resourceAdmitted(path: string, canRead: GrepOpts["canRead"], reasons: SearchObservation["scan"]["reasons"],
|
|
273
|
-
signal?: AbortSignal, deadline?: number): Promise<boolean> {
|
|
274
|
-
if (!canRead) return true;
|
|
275
|
-
let allowed: boolean | undefined = false, abort = () => {};
|
|
276
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
277
|
-
const stopped = new Promise<undefined>(resolve => {
|
|
278
|
-
abort = () => resolve(undefined); signal?.addEventListener("abort", abort, { once: true });
|
|
279
|
-
if (deadline !== undefined) timer = setTimeout(abort, Math.max(0, deadline - performance.now()));
|
|
280
|
-
if (signal?.aborted) abort();
|
|
281
|
-
});
|
|
282
|
-
try { allowed = await Promise.race([Promise.resolve().then(() => canRead(path)).catch(() => false), stopped]); }
|
|
283
|
-
finally { clearTimeout(timer); signal?.removeEventListener("abort", abort); }
|
|
284
|
-
if (allowed === undefined || signal?.aborted || deadline !== undefined && performance.now() >= deadline) {
|
|
285
|
-
reasons.push({ code: signal?.aborted ? "cancelled" : "timeout" }); return false;
|
|
286
|
-
}
|
|
287
|
-
if (!allowed && !reasons.some(reason => reason.code === "permission_omitted")) reasons.push({ code: "permission_omitted" });
|
|
288
|
-
return allowed;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
interface WorkerMatch {
|
|
292
|
-
readonly ok: boolean;
|
|
293
|
-
readonly hits?: ReadonlyArray<{ readonly line: number; readonly text: string }>;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
class LineMatcher {
|
|
297
|
-
private worker: Worker | null = null;
|
|
298
|
-
|
|
299
|
-
private spawn(): Worker {
|
|
300
|
-
return new Worker(MATCH_WORKER_SOURCE, { eval: true });
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
async match(
|
|
304
|
-
pattern: string,
|
|
305
|
-
flags: string,
|
|
306
|
-
lines: string[],
|
|
307
|
-
budgetMs = MATCH_TIMEOUT_MS,
|
|
308
|
-
signal?: AbortSignal,
|
|
309
|
-
): Promise<"timeout" | "invalid" | "cancelled" | MatchLine[]> {
|
|
310
|
-
if (signal?.aborted) return "cancelled";
|
|
311
|
-
if (this.worker === null) this.worker = this.spawn();
|
|
312
|
-
const worker = this.worker;
|
|
313
|
-
return await new Promise((resolve) => {
|
|
314
|
-
let settled = false;
|
|
315
|
-
const finish = (value: "timeout" | "invalid" | "cancelled" | MatchLine[]): void => {
|
|
316
|
-
if (settled) return;
|
|
317
|
-
settled = true;
|
|
318
|
-
clearTimeout(timer);
|
|
319
|
-
worker.off("message", onMessage);
|
|
320
|
-
worker.off("error", onError);
|
|
321
|
-
signal?.removeEventListener("abort", abort);
|
|
322
|
-
resolve(value);
|
|
323
|
-
};
|
|
324
|
-
const abort = () => { void worker.terminate(); this.worker = null; finish("cancelled"); };
|
|
325
|
-
const timer = setTimeout(() => {
|
|
326
|
-
void worker.terminate();
|
|
327
|
-
this.worker = null;
|
|
328
|
-
finish("timeout");
|
|
329
|
-
}, Math.max(0, Math.min(MATCH_TIMEOUT_MS, budgetMs)));
|
|
330
|
-
const onMessage = (msg: WorkerMatch) => {
|
|
331
|
-
if (!msg.ok) {
|
|
332
|
-
finish("invalid");
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
finish((msg.hits ?? []).map((h) => ({ path: "", line: h.line, text: h.text })));
|
|
336
|
-
};
|
|
337
|
-
const onError = () => {
|
|
338
|
-
void worker.terminate();
|
|
339
|
-
this.worker = null;
|
|
340
|
-
finish("invalid");
|
|
341
|
-
};
|
|
342
|
-
worker.on("message", onMessage);
|
|
343
|
-
worker.on("error", onError);
|
|
344
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
345
|
-
worker.postMessage({ pattern, flags, lines });
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
close(): void {
|
|
350
|
-
if (this.worker !== null) {
|
|
351
|
-
void this.worker.terminate();
|
|
352
|
-
this.worker = null;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function runCommand(
|
|
358
|
-
cmd: string,
|
|
359
|
-
args: readonly string[],
|
|
360
|
-
timeoutMs: number,
|
|
361
|
-
signal?: AbortSignal,
|
|
362
|
-
maxBytes = MAX_SEARCH_CAPTURE_BYTES,
|
|
363
|
-
): Promise<{ stdout: string; raw: Uint8Array; rawErrors?: Uint8Array; stderr: string; exit: number | null; diagnostic?: ToolDiagnostic; stopped?: "timeout" | "cancelled" | "output_limit" }> {
|
|
364
|
-
if (signal?.aborted) return Promise.resolve({ stdout: "", raw: new Uint8Array(), stderr: "", exit: null, stopped: "cancelled" });
|
|
365
|
-
return new Promise((resolve) => {
|
|
366
|
-
const child = spawn(cmd, [...args], { stdio: ["ignore", "pipe", "pipe"], detached: true });
|
|
367
|
-
const stdout: Buffer[] = [], stderr: Buffer[] = [];
|
|
368
|
-
let size = 0, errorSize = 0, stopped: "timeout" | "cancelled" | "output_limit" | undefined;
|
|
369
|
-
let escalation: ReturnType<typeof setTimeout> | undefined;
|
|
370
|
-
const kill = (value: "SIGTERM" | "SIGKILL") => { try {
|
|
371
|
-
if (child.pid && process.platform !== "win32") process.kill(-child.pid, value); else child.kill(value);
|
|
372
|
-
} catch { /* the process group already exited */ } };
|
|
373
|
-
const stop = (reason: "timeout" | "cancelled" | "output_limit") => {
|
|
374
|
-
if (stopped) return; stopped = reason; kill("SIGTERM");
|
|
375
|
-
escalation = setTimeout(() => kill("SIGKILL"), COMMAND_TERMINATION_GRACE_MS);
|
|
376
|
-
};
|
|
377
|
-
const abort = () => stop("cancelled");
|
|
378
|
-
child.stdout.on("data", (chunk: Buffer) => {
|
|
379
|
-
const kept = chunk.subarray(0, Math.max(0, maxBytes - size));
|
|
380
|
-
if (kept.length) stdout.push(Buffer.from(kept)); size += kept.length;
|
|
381
|
-
if (kept.length < chunk.length) stop("output_limit");
|
|
382
|
-
});
|
|
383
|
-
child.stderr.on("data", (chunk: Buffer) => {
|
|
384
|
-
// Errors are bounded too; stderr is explanatory, never a source of success or completeness.
|
|
385
|
-
const kept = chunk.subarray(0, Math.max(0, 65536 - errorSize));
|
|
386
|
-
if (kept.length) stderr.push(Buffer.from(kept)); errorSize += kept.length;
|
|
387
|
-
if (kept.length < chunk.length) stop("output_limit");
|
|
388
|
-
});
|
|
389
|
-
const timer = setTimeout(() => stop("timeout"), timeoutMs);
|
|
390
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
391
|
-
if (signal?.aborted) abort();
|
|
392
|
-
child.on("close", (code) => {
|
|
393
|
-
clearTimeout(timer); clearTimeout(escalation); signal?.removeEventListener("abort", abort);
|
|
394
|
-
if (stopped) kill("SIGKILL");
|
|
395
|
-
const raw = Buffer.concat(stdout);
|
|
396
|
-
const rawErrors = Buffer.concat(stderr);
|
|
397
|
-
resolve({ stdout: raw.toString("utf8"), raw, rawErrors, stderr: rawErrors.toString("utf8"), exit: code, ...(stopped ? { stopped } : {}) });
|
|
398
|
-
});
|
|
399
|
-
child.on("error", (err) => {
|
|
400
|
-
const raw = Buffer.concat(stdout);
|
|
401
|
-
clearTimeout(timer); clearTimeout(escalation); signal?.removeEventListener("abort", abort);
|
|
402
|
-
const facts = child.pid ? READ_ATTEMPTED : NOT_STARTED;
|
|
403
|
-
const os = (err as { code?: string }).code;
|
|
404
|
-
const diagnostic = !child.pid && (os === "ENOENT" || os === "ENOTDIR") ? failureDiagnostic("environment_missing", err.message, facts) : failureFromError(err, facts);
|
|
405
|
-
resolve({ stdout: raw.toString("utf8"), raw, stderr: err.message, exit: null, diagnostic, ...(stopped ? { stopped } : {}) });
|
|
406
|
-
});
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function rgIgnoreArgs(): string[] {
|
|
411
|
-
const args: string[] = [];
|
|
412
|
-
for (const name of DEFAULT_IGNORE) {
|
|
413
|
-
args.push("--glob", `!${name}`);
|
|
414
|
-
args.push("--glob", `!${name}/**`);
|
|
415
|
-
args.push("--glob", `!**/${name}`);
|
|
416
|
-
args.push("--glob", `!**/${name}/**`);
|
|
417
|
-
}
|
|
418
|
-
return args;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function parseRgJson(root: string, stdout: string): MatchLine[] {
|
|
422
|
-
const hits: MatchLine[] = [];
|
|
423
|
-
for (const raw of stdout.split("\n")) {
|
|
424
|
-
if (raw.trim().length === 0) continue;
|
|
425
|
-
let parsed: unknown;
|
|
426
|
-
try {
|
|
427
|
-
parsed = JSON.parse(raw);
|
|
428
|
-
} catch {
|
|
429
|
-
continue;
|
|
430
|
-
}
|
|
431
|
-
if (typeof parsed !== "object" || parsed === null) continue;
|
|
432
|
-
const rec = parsed as Record<string, unknown>;
|
|
433
|
-
if (rec["type"] !== "match") continue;
|
|
434
|
-
const data = rec["data"];
|
|
435
|
-
if (typeof data !== "object" || data === null) continue;
|
|
436
|
-
const d = data as Record<string, unknown>;
|
|
437
|
-
const pathObj = d["path"];
|
|
438
|
-
const pathText =
|
|
439
|
-
typeof pathObj === "object" && pathObj !== null
|
|
440
|
-
? rgText(pathObj as Record<string, unknown>)
|
|
441
|
-
: "";
|
|
442
|
-
const lineNumber = d["line_number"];
|
|
443
|
-
const linesObj = d["lines"];
|
|
444
|
-
const lineText =
|
|
445
|
-
typeof linesObj === "object" && linesObj !== null
|
|
446
|
-
? rgText(linesObj as Record<string, unknown>)
|
|
447
|
-
: "";
|
|
448
|
-
if (pathText.length === 0 || typeof lineNumber !== "number") continue;
|
|
449
|
-
const rel = posixRel(root, pathText) ?? posixRel(root, join(root, pathText));
|
|
450
|
-
if (rel === null || ignoredRel(rel)) continue;
|
|
451
|
-
hits.push({
|
|
452
|
-
path: rel,
|
|
453
|
-
line: lineNumber,
|
|
454
|
-
text: lineText.replace(/\n$/, "").replace(/\r/g, ""),
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
return hits;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function rgText(value: Record<string, unknown>): string {
|
|
461
|
-
return typeof value["text"] === "string" ? value["text"] : typeof value["bytes"] === "string" ? Buffer.from(value["bytes"], "base64").toString("utf8") : "";
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
async function grepRipgrep(opts: GrepOpts, abs: string, limit: number): Promise<SearchResult | null> {
|
|
465
|
-
const rg = findRipgrep(configDir());
|
|
466
|
-
if (rg === null) return null;
|
|
467
|
-
const args = [
|
|
468
|
-
"--json",
|
|
469
|
-
"--color=never",
|
|
470
|
-
"--hidden",
|
|
471
|
-
"--no-follow",
|
|
472
|
-
...(await gitIgnored(abs, opts.signal) ? ["--no-ignore"] : []),
|
|
473
|
-
"--max-filesize",
|
|
474
|
-
String(MAX_FILE_BYTES),
|
|
475
|
-
...rgIgnoreArgs(),
|
|
476
|
-
];
|
|
477
|
-
if (opts.glob !== undefined) args.push("--glob", opts.glob);
|
|
478
|
-
args.push("--", opts.pattern, abs);
|
|
479
|
-
const ran = await runCommand(rg, args, opts.timeoutMs ?? PROCESS_TIMEOUT_MS, opts.signal, opts.maxCaptureBytes);
|
|
480
|
-
const hits = parseRgJson(opts.root, ran.stdout);
|
|
481
|
-
hits.sort(compareMatch);
|
|
482
|
-
const failed = ran.stopped !== undefined || ran.exit !== 0 && ran.exit !== 1;
|
|
483
|
-
const reason = ran.stopped ?? "engine_error";
|
|
484
|
-
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 })),
|
|
485
|
-
sources: [], limit, empty: "No matches found", scan: { status: failed ? "partial" : "unknown", examined_files: null, source_versions: "unknown",
|
|
486
|
-
reasons: [{ code: "engine_does_not_report_all_skipped_files" }, ...(failed ? [{ code: reason }] : [])] } };
|
|
487
|
-
return observed(observation, ran.diagnostic ?? (failed ? failureDiagnostic(reason === "timeout" || reason === "cancelled" ? reason : "execution_failed",
|
|
488
|
-
ran.stderr.trim() || `ripgrep ${reason}; exit ${ran.exit ?? "unknown"}`, READ_ATTEMPTED) : undefined));
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function compareMatch(a: MatchLine, b: MatchLine): number {
|
|
492
|
-
if (a.path !== b.path) return a.path < b.path ? -1 : 1;
|
|
493
|
-
return a.line - b.line;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
async function grepWalk(opts: GrepOpts, abs: string, kind: "file" | "dir", rel: string, limit: number): Promise<SearchResult> {
|
|
497
|
-
const pattern = jsPatternFromRipgrep(opts.pattern);
|
|
498
|
-
try {
|
|
499
|
-
new RegExp(pattern);
|
|
500
|
-
} catch (err) {
|
|
501
|
-
return searchFailure("invalid_input", `invalid regex: ${(err as Error).message}`);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
const deadline = performance.now() + (opts.timeoutMs ?? PROCESS_TIMEOUT_MS);
|
|
505
|
-
const files = await listedFiles(abs, rel, kind, { deadline, ...(opts.signal ? { signal: opts.signal } : {}),
|
|
506
|
-
...(opts.maxIndexEntries === undefined ? {} : { maxEntries: opts.maxIndexEntries }) });
|
|
507
|
-
const glob = opts.glob;
|
|
508
|
-
const filtered = glob === undefined ? files.paths : files.paths.filter((p) => globMatches(glob, p));
|
|
509
|
-
const matcher = new LineMatcher();
|
|
510
|
-
const hits: MatchLine[] = [];
|
|
511
|
-
const reasons: SearchObservation["scan"]["reasons"] = policyIssues(files.issues, opts.canRead);
|
|
512
|
-
const sources: SearchObservation["sources"] = [];
|
|
513
|
-
let examined = 0, capturedBytes = 0;
|
|
514
|
-
const budget = Math.min(opts.maxCaptureBytes ?? MAX_SEARCH_CAPTURE_BYTES, MAX_SEARCH_CAPTURE_BYTES);
|
|
515
|
-
try {
|
|
516
|
-
for (const fileRel of filtered) {
|
|
517
|
-
if (opts.signal?.aborted || performance.now() >= deadline) { reasons.push({ code: opts.signal?.aborted ? "cancelled" : "timeout" }); break; }
|
|
518
|
-
if (ignoredRel(fileRel)) continue;
|
|
519
|
-
if (!await resourceAdmitted(fileRel, opts.canRead, reasons, opts.signal, deadline)) continue;
|
|
520
|
-
const fileAbs = join(opts.root, fileRel);
|
|
521
|
-
let st;
|
|
522
|
-
try {
|
|
523
|
-
st = await lstat(fileAbs);
|
|
524
|
-
} catch {
|
|
525
|
-
reasons.push({ code: "unreadable_file", path: fileRel });
|
|
526
|
-
continue;
|
|
527
|
-
}
|
|
528
|
-
if (st.isSymbolicLink() || !st.isFile()) { reasons.push({ code: "changed_file_type", path: fileRel }); continue; }
|
|
529
|
-
if (st.size > MAX_FILE_BYTES) { reasons.push({ code: "file_size_limit", path: fileRel }); continue; }
|
|
530
|
-
let text: string;
|
|
531
|
-
try {
|
|
532
|
-
const raw = await readFile(fileAbs);
|
|
533
|
-
if (raw.length > MAX_FILE_BYTES) { reasons.push({ code: "file_size_limit", path: fileRel }); continue; }
|
|
534
|
-
text = new TextDecoder("utf-8", { fatal: true }).decode(raw);
|
|
535
|
-
sources.push({ path: fileRel, sha256: blobAddress(raw) }); examined++;
|
|
536
|
-
} catch {
|
|
537
|
-
reasons.push({ code: "unreadable_or_non_utf8_file", path: fileRel });
|
|
538
|
-
continue;
|
|
539
|
-
}
|
|
540
|
-
if (text.includes("\0")) { reasons.push({ code: "binary_file", path: fileRel }); continue; }
|
|
541
|
-
const lines = splitLines(text);
|
|
542
|
-
// Reserve the worst-case formatted hits before matching, never discard already observed hits.
|
|
543
|
-
const reservation = Buffer.byteLength(text) + lines.length * (Buffer.byteLength(fileRel) + String(MAX_FILE_BYTES).length + 3);
|
|
544
|
-
if (capturedBytes + reservation > budget) { reasons.push({ code: "capture_budget", path: fileRel }); break; }
|
|
545
|
-
const matched = await matcher.match(pattern, "", lines, deadline - performance.now(), opts.signal);
|
|
546
|
-
if (matched === "cancelled") { reasons.push({ code: "cancelled", path: fileRel }); break; }
|
|
547
|
-
if (matched === "timeout") {
|
|
548
|
-
reasons.push({ code: "match_timeout", path: fileRel });
|
|
549
|
-
continue;
|
|
550
|
-
}
|
|
551
|
-
if (matched === "invalid") {
|
|
552
|
-
reasons.push({ code: "matcher_failed", path: fileRel }); break;
|
|
553
|
-
}
|
|
554
|
-
for (const hit of matched) {
|
|
555
|
-
hits.push({ path: fileRel, line: hit.line, text: hit.text });
|
|
556
|
-
capturedBytes += Buffer.byteLength(`${fileRel}:${hit.line}: ${hit.text}\n`);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
} finally {
|
|
560
|
-
matcher.close();
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
hits.sort(compareMatch);
|
|
564
|
-
const stopped = reasons.find(reason => ["timeout", "cancelled"].includes(reason.code));
|
|
565
|
-
return observed({ engine: "walk", rows: hits.map(hit => ({ prefix: `${hit.path}:${hit.line}: `, text: hit.text })), sources, limit, empty: "No matches found",
|
|
566
|
-
scan: { status: reasons.length ? "partial" : "complete", reasons, examined_files: examined, source_versions: "recorded" } },
|
|
567
|
-
stopped ? failureDiagnostic(stopped.code === "cancelled" ? "cancelled" : "timeout", `search ${stopped.code}`, READ_ATTEMPTED) : undefined);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
export async function grep(opts: GrepOpts): Promise<SearchResult> {
|
|
571
|
-
const pattern = opts.pattern;
|
|
572
|
-
if (pattern.length === 0) return searchFailure("invalid_input", "grep needs a pattern");
|
|
573
|
-
|
|
574
|
-
const target = await inspectTarget(opts.root, opts.path);
|
|
575
|
-
if ("error" in target) return { content: target.error, ok: false, diagnostic: target.diagnostic };
|
|
576
|
-
|
|
577
|
-
const limit = opts.limit !== undefined && opts.limit > 0 ? Math.floor(opts.limit) : DEFAULT_MATCH_LIMIT;
|
|
578
|
-
// External search captures bytes before the runtime can inspect each
|
|
579
|
-
// resource. A policy callback therefore always uses the admitted walk.
|
|
580
|
-
const engine = opts.canRead ? "walk" : opts.engine ?? "auto";
|
|
581
|
-
|
|
582
|
-
switch (engine) {
|
|
583
|
-
case "walk":
|
|
584
|
-
return grepWalk(opts, opts.path, target.kind, target.rel, limit);
|
|
585
|
-
case "ripgrep": {
|
|
586
|
-
const got = await grepRipgrep(opts, opts.path, limit);
|
|
587
|
-
if (got === null) return searchFailure("environment_missing", "ripgrep is not on PATH or in xeno's bin directory");
|
|
588
|
-
return got;
|
|
589
|
-
}
|
|
590
|
-
case "auto": {
|
|
591
|
-
if (findRipgrep(configDir()) !== null) {
|
|
592
|
-
const got = await grepRipgrep(opts, opts.path, limit);
|
|
593
|
-
if (got !== null) return got;
|
|
594
|
-
}
|
|
595
|
-
return grepWalk(opts, opts.path, target.kind, target.rel, limit);
|
|
596
|
-
}
|
|
597
|
-
default:
|
|
598
|
-
return assertNever(engine, "grep");
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
function fdExcludeArgs(): string[] {
|
|
603
|
-
const args: string[] = [];
|
|
604
|
-
for (const name of DEFAULT_IGNORE) args.push("--exclude", name);
|
|
605
|
-
return args;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
async function findFd(opts: FindOpts, abs: string, limit: number): Promise<SearchResult | null> {
|
|
609
|
-
const fd = findOnPath("fd");
|
|
610
|
-
if (fd === null) return null;
|
|
611
|
-
const args = [
|
|
612
|
-
"--glob",
|
|
613
|
-
"--color=never",
|
|
614
|
-
"--hidden",
|
|
615
|
-
...(await gitIgnored(abs, opts.signal) ? ["--no-ignore"] : []),
|
|
616
|
-
"--absolute-path",
|
|
617
|
-
"--print0",
|
|
618
|
-
...fdExcludeArgs(),
|
|
619
|
-
];
|
|
620
|
-
let pattern = opts.pattern;
|
|
621
|
-
if (pattern.includes("/")) {
|
|
622
|
-
args.push("--full-path");
|
|
623
|
-
if (!pattern.startsWith("**/") && pattern !== "**") pattern = `**/${pattern}`;
|
|
624
|
-
}
|
|
625
|
-
args.push("--", pattern, abs);
|
|
626
|
-
const ran = await runCommand(fd, args, opts.timeoutMs ?? PROCESS_TIMEOUT_MS, opts.signal, opts.maxCaptureBytes);
|
|
627
|
-
const paths: string[] = [];
|
|
628
|
-
// A partial final record is not a path. NUL preserves spaces and newlines in real filenames.
|
|
629
|
-
for (const line of ran.stdout.split("\0").slice(0, -1)) {
|
|
630
|
-
if (line.length === 0) continue;
|
|
631
|
-
const rel = posixRel(opts.root, line);
|
|
632
|
-
if (rel === null || ignoredRel(rel)) continue;
|
|
633
|
-
paths.push(rel);
|
|
634
|
-
}
|
|
635
|
-
paths.sort((a, b) => (a < b ? -1 : 1));
|
|
636
|
-
const failed = ran.stopped !== undefined || ran.exit !== 0;
|
|
637
|
-
const reason = ran.stopped ?? "engine_error";
|
|
638
|
-
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: [],
|
|
639
|
-
scan: { status: failed ? "partial" : "unknown", examined_files: null, source_versions: "unknown",
|
|
640
|
-
reasons: [{ code: "engine_does_not_report_all_skipped_files" }, ...(failed ? [{ code: reason }] : [])] } },
|
|
641
|
-
ran.diagnostic ?? (failed ? failureDiagnostic(reason === "timeout" || reason === "cancelled" ? reason : "execution_failed",
|
|
642
|
-
ran.stderr.trim() || `fd ${reason}; exit ${ran.exit ?? "unknown"}`, READ_ATTEMPTED) : undefined));
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
async function findWalk(opts: FindOpts, abs: string, kind: "file" | "dir", rel: string, limit: number): Promise<SearchResult> {
|
|
646
|
-
const deadline = performance.now() + (opts.timeoutMs ?? PROCESS_TIMEOUT_MS);
|
|
647
|
-
const files = await listedFiles(abs, rel, kind, { deadline,
|
|
648
|
-
...(opts.signal ? { signal: opts.signal } : {}), ...(opts.maxIndexEntries === undefined ? {} : { maxEntries: opts.maxIndexEntries }) });
|
|
649
|
-
const reasons = policyIssues(files.issues, opts.canRead), paths: string[] = [];
|
|
650
|
-
for (const path of files.paths) {
|
|
651
|
-
if (opts.signal?.aborted || performance.now() >= deadline) { reasons.push({ code: opts.signal?.aborted ? "cancelled" : "timeout" }); break; }
|
|
652
|
-
if (!ignoredRel(path) && globMatches(opts.pattern, path) && await resourceAdmitted(path, opts.canRead, reasons, opts.signal, deadline)) paths.push(path);
|
|
653
|
-
}
|
|
654
|
-
paths.sort((a, b) => (a < b ? -1 : 1));
|
|
655
|
-
const stopped = reasons.find(reason => ["timeout", "cancelled"].includes(reason.code));
|
|
656
|
-
return observed({ engine: "walk", rows: paths.map(text => ({ prefix: "", text })), sources: [], limit, empty: "No files found matching pattern",
|
|
657
|
-
scan: { status: reasons.length ? "partial" : "complete", reasons, examined_files: files.paths.length, source_versions: "unknown" } },
|
|
658
|
-
stopped ? failureDiagnostic(stopped.code === "cancelled" ? "cancelled" : "timeout", `search ${stopped.code}`, READ_ATTEMPTED) : undefined);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
export async function find(opts: FindOpts): Promise<SearchResult> {
|
|
662
|
-
if (opts.pattern.length === 0) return searchFailure("invalid_input", "find needs a pattern");
|
|
663
|
-
const target = await inspectTarget(opts.root, opts.path);
|
|
664
|
-
if ("error" in target) return { content: target.error, ok: false, diagnostic: target.diagnostic };
|
|
665
|
-
const limit = opts.limit !== undefined && opts.limit > 0 ? Math.floor(opts.limit) : DEFAULT_FIND_LIMIT;
|
|
666
|
-
const engine = opts.canRead ? "walk" : opts.engine ?? "auto";
|
|
667
|
-
switch (engine) {
|
|
668
|
-
case "walk":
|
|
669
|
-
return findWalk(opts, opts.path, target.kind, target.rel, limit);
|
|
670
|
-
case "fd": {
|
|
671
|
-
const got = await findFd(opts, opts.path, limit);
|
|
672
|
-
if (got === null) return searchFailure("environment_missing", "fd is not on PATH");
|
|
673
|
-
return got;
|
|
674
|
-
}
|
|
675
|
-
case "auto": {
|
|
676
|
-
if (findOnPath("fd") !== null) {
|
|
677
|
-
const got = await findFd(opts, opts.path, limit);
|
|
678
|
-
if (got !== null) return got;
|
|
679
|
-
}
|
|
680
|
-
return findWalk(opts, opts.path, target.kind, target.rel, limit);
|
|
681
|
-
}
|
|
682
|
-
default:
|
|
683
|
-
return assertNever(engine, "find");
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
export async function ls(opts: LsOpts): Promise<SearchResult> {
|
|
688
|
-
if (opts.signal?.aborted) return searchFailure("cancelled", "directory listing cancelled");
|
|
689
|
-
const target = await inspectTarget(opts.root, opts.path);
|
|
690
|
-
if ("error" in target) return { content: target.error, ok: false, diagnostic: target.diagnostic };
|
|
691
|
-
if (target.kind !== "dir") return searchFailure("invalid_input", `not a directory: ${target.rel || "."}`);
|
|
692
|
-
|
|
693
|
-
let entries;
|
|
694
|
-
try {
|
|
695
|
-
entries = await readdir(opts.path, { withFileTypes: true });
|
|
696
|
-
} catch (err) {
|
|
697
|
-
return { content: `cannot list ${target.rel || "."}: ${(err as Error).message}`, ok: false, diagnostic: failureFromError(err, READ_ATTEMPTED) };
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
const names: string[] = [], reasons: SearchObservation["scan"]["reasons"] = [];
|
|
701
|
-
const deadline = performance.now() + PROCESS_TIMEOUT_MS;
|
|
702
|
-
for (const entry of entries) {
|
|
703
|
-
if (DEFAULT_IGNORE.includes(entry.name)) continue;
|
|
704
|
-
const childRel = target.rel.length === 0 ? entry.name : `${target.rel}/${entry.name}`;
|
|
705
|
-
if (ignoredRel(childRel)) continue;
|
|
706
|
-
if (opts.signal?.aborted || performance.now() >= deadline) { reasons.push({ code: opts.signal?.aborted ? "cancelled" : "timeout" }); break; }
|
|
707
|
-
if (!await resourceAdmitted(childRel, opts.canRead, reasons, opts.signal, deadline)) continue;
|
|
708
|
-
// Do not follow: a symlink is listed as a name, never as the directory
|
|
709
|
-
// it points at, so a link to / cannot walk the disk.
|
|
710
|
-
if (entry.isSymbolicLink()) {
|
|
711
|
-
names.push(entry.name);
|
|
712
|
-
continue;
|
|
713
|
-
}
|
|
714
|
-
if (entry.isDirectory()) names.push(`${entry.name}/`);
|
|
715
|
-
else if (entry.isFile()) names.push(entry.name);
|
|
716
|
-
}
|
|
717
|
-
names.sort((a, b) => {
|
|
718
|
-
const al = a.toLowerCase();
|
|
719
|
-
const bl = b.toLowerCase();
|
|
720
|
-
if (al === bl) return a < b ? -1 : 1;
|
|
721
|
-
return al < bl ? -1 : 1;
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
const limit = opts.limit !== undefined && opts.limit > 0 ? Math.floor(opts.limit) : DEFAULT_LS_LIMIT;
|
|
725
|
-
return observed({ engine: "directory", rows: names.map(text => ({ prefix: "", text })), sources: [], limit, empty: "(empty directory)",
|
|
726
|
-
scan: { status: reasons.length ? "partial" : "complete", reasons, examined_files: names.length, source_versions: "unknown" } },
|
|
727
|
-
reasons.some(reason => reason.code === "cancelled" || reason.code === "timeout") ? failureDiagnostic(opts.signal?.aborted ? "cancelled" : "timeout", "directory listing stopped", READ_ATTEMPTED) : undefined);
|
|
728
|
-
}
|