@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.
Files changed (350) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +31 -746
  3. package/bin/xeno.exe +26 -0
  4. package/install.cjs +88 -0
  5. package/package.json +18 -48
  6. package/docs/app-server.md +0 -115
  7. package/docs/foundations.md +0 -88
  8. package/docs/install.md +0 -244
  9. package/docs/providers.md +0 -172
  10. package/docs/research/foundation-clients-2026-10-09.md +0 -27
  11. package/docs/research/foundation-editing-2026-10-09.md +0 -56
  12. package/docs/research/foundation-network-2026-10-09.md +0 -37
  13. package/docs/research/foundation-workflow-2026-10-09.md +0 -37
  14. package/src/apps/acp-server.ts +0 -41
  15. package/src/apps/agent-profiles.ts +0 -19
  16. package/src/apps/app-server.ts +0 -30
  17. package/src/apps/approve.ts +0 -49
  18. package/src/apps/cli.ts +0 -99
  19. package/src/apps/commands.ts +0 -319
  20. package/src/apps/completions.ts +0 -87
  21. package/src/apps/control-transport.ts +0 -90
  22. package/src/apps/export-cli.ts +0 -24
  23. package/src/apps/goal-flow.ts +0 -70
  24. package/src/apps/image-draft.ts +0 -46
  25. package/src/apps/input.ts +0 -312
  26. package/src/apps/model-catalog.ts +0 -69
  27. package/src/apps/preferences.ts +0 -20
  28. package/src/apps/project-trust.ts +0 -9
  29. package/src/apps/provider-connect.ts +0 -346
  30. package/src/apps/question-ui.ts +0 -27
  31. package/src/apps/render.ts +0 -181
  32. package/src/apps/resources.ts +0 -66
  33. package/src/apps/review.ts +0 -109
  34. package/src/apps/session-hub.ts +0 -96
  35. package/src/apps/session-lifecycle.ts +0 -64
  36. package/src/apps/shell.ts +0 -2071
  37. package/src/apps/startup-input.ts +0 -135
  38. package/src/apps/startup.ts +0 -1677
  39. package/src/apps/ui/app.ts +0 -1818
  40. package/src/apps/ui/control-model.ts +0 -43
  41. package/src/apps/ui/extension-host.ts +0 -194
  42. package/src/apps/ui/image-cache.ts +0 -121
  43. package/src/apps/ui/palette.ts +0 -88
  44. package/src/apps/ui/project.ts +0 -127
  45. package/src/apps/ui/standalone.ts +0 -148
  46. package/src/apps/ui/status-model.ts +0 -68
  47. package/src/apps/ui/surface.ts +0 -91
  48. package/src/apps/ui/theme.ts +0 -86
  49. package/src/apps/update-check.ts +0 -112
  50. package/src/apps/workflow.ts +0 -58
  51. package/src/core/acceptance.ts +0 -139
  52. package/src/core/action-defaults.ts +0 -58
  53. package/src/core/activity-status.ts +0 -52
  54. package/src/core/agent-profile.ts +0 -130
  55. package/src/core/attachment-input.ts +0 -17
  56. package/src/core/blind-checks.ts +0 -72
  57. package/src/core/boundary.ts +0 -174
  58. package/src/core/branch-receipt.ts +0 -26
  59. package/src/core/catalog-merge.ts +0 -19
  60. package/src/core/checkpoints.ts +0 -85
  61. package/src/core/churn.ts +0 -61
  62. package/src/core/command-artifacts.ts +0 -94
  63. package/src/core/command-contract.ts +0 -151
  64. package/src/core/completion-action.ts +0 -20
  65. package/src/core/completion-review.ts +0 -98
  66. package/src/core/context-detail.ts +0 -37
  67. package/src/core/context-policy.ts +0 -133
  68. package/src/core/context-window.ts +0 -54
  69. package/src/core/continuation.ts +0 -130
  70. package/src/core/control-protocol.ts +0 -69
  71. package/src/core/control-view.ts +0 -39
  72. package/src/core/cycle.ts +0 -17
  73. package/src/core/data-query.ts +0 -201
  74. package/src/core/deadline.ts +0 -40
  75. package/src/core/derive.ts +0 -516
  76. package/src/core/draft-images.ts +0 -56
  77. package/src/core/edit-plan.ts +0 -200
  78. package/src/core/environment.ts +0 -181
  79. package/src/core/event-validation.ts +0 -220
  80. package/src/core/events.ts +0 -670
  81. package/src/core/evidence-ladder.ts +0 -62
  82. package/src/core/evidence-links.ts +0 -68
  83. package/src/core/evidence.ts +0 -240
  84. package/src/core/execution.ts +0 -18
  85. package/src/core/exhaustive.ts +0 -18
  86. package/src/core/export.ts +0 -179
  87. package/src/core/extension-flags.ts +0 -14
  88. package/src/core/external-effects.ts +0 -488
  89. package/src/core/failure-recovery.ts +0 -173
  90. package/src/core/file-precondition.ts +0 -78
  91. package/src/core/file-references.ts +0 -35
  92. package/src/core/frontmatter.ts +0 -213
  93. package/src/core/git-status.ts +0 -29
  94. package/src/core/goal-view.ts +0 -64
  95. package/src/core/goal.ts +0 -200
  96. package/src/core/handoff.ts +0 -479
  97. package/src/core/history-query.ts +0 -139
  98. package/src/core/history.ts +0 -237
  99. package/src/core/image-limits.ts +0 -72
  100. package/src/core/images.ts +0 -46
  101. package/src/core/integration-health.ts +0 -34
  102. package/src/core/json-event.ts +0 -131
  103. package/src/core/json-pointer.ts +0 -46
  104. package/src/core/keymap.ts +0 -153
  105. package/src/core/lsp-framing.ts +0 -34
  106. package/src/core/mcp-config.ts +0 -68
  107. package/src/core/model-picker.ts +0 -64
  108. package/src/core/model-preferences.ts +0 -28
  109. package/src/core/model-replay.ts +0 -22
  110. package/src/core/model-scope.ts +0 -53
  111. package/src/core/note-guidance.ts +0 -34
  112. package/src/core/oauth-fields.ts +0 -44
  113. package/src/core/observation-view.ts +0 -152
  114. package/src/core/outcome.ts +0 -44
  115. package/src/core/overflow.ts +0 -59
  116. package/src/core/packages.ts +0 -47
  117. package/src/core/patch.ts +0 -130
  118. package/src/core/path-suggestions.ts +0 -59
  119. package/src/core/permissions.ts +0 -410
  120. package/src/core/process-receipt.ts +0 -105
  121. package/src/core/process-state.ts +0 -37
  122. package/src/core/program-status.ts +0 -40
  123. package/src/core/progress.ts +0 -150
  124. package/src/core/project.ts +0 -431
  125. package/src/core/prompt-policy.ts +0 -46
  126. package/src/core/prompts.ts +0 -111
  127. package/src/core/question.ts +0 -94
  128. package/src/core/queue.ts +0 -78
  129. package/src/core/read-bounds.ts +0 -24
  130. package/src/core/read-observation.ts +0 -106
  131. package/src/core/recovery-sources.ts +0 -74
  132. package/src/core/regex-dialect.ts +0 -25
  133. package/src/core/request-budget.ts +0 -23
  134. package/src/core/request-progress.ts +0 -87
  135. package/src/core/request-state.ts +0 -273
  136. package/src/core/resource-selection.ts +0 -36
  137. package/src/core/response-recovery.ts +0 -59
  138. package/src/core/resume.ts +0 -59
  139. package/src/core/review.ts +0 -68
  140. package/src/core/ripgrep-release.ts +0 -64
  141. package/src/core/runtime-target.ts +0 -94
  142. package/src/core/scheduling.ts +0 -8
  143. package/src/core/search-observation.ts +0 -97
  144. package/src/core/secrets.ts +0 -188
  145. package/src/core/session-archive.ts +0 -4
  146. package/src/core/session-browser.ts +0 -82
  147. package/src/core/session-entry.ts +0 -61
  148. package/src/core/session-exit.ts +0 -42
  149. package/src/core/session-format.ts +0 -169
  150. package/src/core/session-position.ts +0 -26
  151. package/src/core/session-tree.ts +0 -147
  152. package/src/core/settings.ts +0 -192
  153. package/src/core/skills.ts +0 -165
  154. package/src/core/sql-query.ts +0 -211
  155. package/src/core/stall.ts +0 -128
  156. package/src/core/stop-report.ts +0 -68
  157. package/src/core/text-normalize.ts +0 -81
  158. package/src/core/theme.ts +0 -166
  159. package/src/core/thinking.ts +0 -26
  160. package/src/core/tokens.ts +0 -151
  161. package/src/core/tool-contract.ts +0 -153
  162. package/src/core/tool-discovery.ts +0 -87
  163. package/src/core/tool-failure.ts +0 -81
  164. package/src/core/tool-input.ts +0 -44
  165. package/src/core/transcript.ts +0 -108
  166. package/src/core/tui-mode.ts +0 -29
  167. package/src/core/update-check.ts +0 -157
  168. package/src/core/version.ts +0 -55
  169. package/src/core/web-content.ts +0 -80
  170. package/src/core/work-state.ts +0 -154
  171. package/src/core/workflow-mode.ts +0 -34
  172. package/src/core/working-set.ts +0 -119
  173. package/src/core/yaml.ts +0 -282
  174. package/src/runtime/acp-agent.ts +0 -289
  175. package/src/runtime/agent-profiles.ts +0 -21
  176. package/src/runtime/auth.ts +0 -367
  177. package/src/runtime/blind-check.ts +0 -36
  178. package/src/runtime/branch-jobs.ts +0 -36
  179. package/src/runtime/catalog.ts +0 -285
  180. package/src/runtime/clipboard.ts +0 -125
  181. package/src/runtime/command-artifacts.ts +0 -64
  182. package/src/runtime/connection-reuse.ts +0 -40
  183. package/src/runtime/control-client.ts +0 -149
  184. package/src/runtime/control-connection.ts +0 -139
  185. package/src/runtime/counter-check.ts +0 -74
  186. package/src/runtime/environment-tools.ts +0 -60
  187. package/src/runtime/evidence-notes.ts +0 -35
  188. package/src/runtime/exec-sessions.ts +0 -456
  189. package/src/runtime/execution-control.ts +0 -40
  190. package/src/runtime/extension-loader.ts +0 -37
  191. package/src/runtime/extension-ui.ts +0 -47
  192. package/src/runtime/extensions.ts +0 -370
  193. package/src/runtime/formatter-tool.ts +0 -38
  194. package/src/runtime/formatter.ts +0 -50
  195. package/src/runtime/foundation.ts +0 -116
  196. package/src/runtime/history-cache.ts +0 -57
  197. package/src/runtime/history-tools.ts +0 -168
  198. package/src/runtime/images.ts +0 -80
  199. package/src/runtime/integrations.ts +0 -216
  200. package/src/runtime/loop.ts +0 -3108
  201. package/src/runtime/lsp-client.ts +0 -201
  202. package/src/runtime/mcp-client.ts +0 -264
  203. package/src/runtime/mcp-oauth.ts +0 -168
  204. package/src/runtime/mcp-schema.ts +0 -51
  205. package/src/runtime/model-config.ts +0 -232
  206. package/src/runtime/model-data.LICENSE +0 -27
  207. package/src/runtime/model-data.ts +0 -21
  208. package/src/runtime/model-discovery.ts +0 -208
  209. package/src/runtime/model-replay.ts +0 -10
  210. package/src/runtime/model-scope.ts +0 -20
  211. package/src/runtime/model-sources.ts +0 -151
  212. package/src/runtime/model.ts +0 -686
  213. package/src/runtime/models-fetch.ts +0 -57
  214. package/src/runtime/oauth-device.ts +0 -86
  215. package/src/runtime/oauth.ts +0 -853
  216. package/src/runtime/open-image.ts +0 -42
  217. package/src/runtime/openai-responses.ts +0 -304
  218. package/src/runtime/openai.ts +0 -697
  219. package/src/runtime/output-schema.ts +0 -26
  220. package/src/runtime/package-manager.ts +0 -125
  221. package/src/runtime/patch-tool.ts +0 -38
  222. package/src/runtime/permission-resources.ts +0 -18
  223. package/src/runtime/pi-provider.ts +0 -423
  224. package/src/runtime/program-status.ts +0 -38
  225. package/src/runtime/progress.ts +0 -26
  226. package/src/runtime/provider-access.ts +0 -21
  227. package/src/runtime/provider-connect.ts +0 -109
  228. package/src/runtime/provider-presets.ts +0 -98
  229. package/src/runtime/provider-protocols.ts +0 -29
  230. package/src/runtime/provider-request.ts +0 -87
  231. package/src/runtime/provider-tool-policy.ts +0 -33
  232. package/src/runtime/provider.ts +0 -580
  233. package/src/runtime/question-tool.ts +0 -62
  234. package/src/runtime/retry.ts +0 -447
  235. package/src/runtime/review-tool.ts +0 -48
  236. package/src/runtime/rich-text.ts +0 -36
  237. package/src/runtime/runtime-target.ts +0 -149
  238. package/src/runtime/secrets.ts +0 -79
  239. package/src/runtime/self-update.ts +0 -74
  240. package/src/runtime/session-control.ts +0 -265
  241. package/src/runtime/session-hub.ts +0 -85
  242. package/src/runtime/stream-idle.ts +0 -80
  243. package/src/runtime/tool-broker.ts +0 -137
  244. package/src/runtime/tool-discovery.ts +0 -58
  245. package/src/runtime/tool-queue.ts +0 -77
  246. package/src/runtime/tools.ts +0 -1820
  247. package/src/runtime/turn-effects.ts +0 -31
  248. package/src/runtime/update-check.ts +0 -48
  249. package/src/runtime/usage.ts +0 -253
  250. package/src/runtime/version.ts +0 -12
  251. package/src/runtime/web-http.ts +0 -56
  252. package/src/runtime/web-tools.ts +0 -88
  253. package/src/runtime/working-notes.ts +0 -60
  254. package/src/runtime/workspace-lifecycle.ts +0 -141
  255. package/src/store/acceptance.ts +0 -15
  256. package/src/store/agent-profile-files.ts +0 -13
  257. package/src/store/atomic.ts +0 -31
  258. package/src/store/auth-store.ts +0 -50
  259. package/src/store/blind-checks.ts +0 -32
  260. package/src/store/blobs.ts +0 -143
  261. package/src/store/checkpoint-history.ts +0 -32
  262. package/src/store/command-artifacts.ts +0 -96
  263. package/src/store/command.ts +0 -111
  264. package/src/store/config.ts +0 -69
  265. package/src/store/context-files.ts +0 -436
  266. package/src/store/file-checkpoints.ts +0 -108
  267. package/src/store/file-index.ts +0 -174
  268. package/src/store/foundation-config.ts +0 -24
  269. package/src/store/git-checkpoints.ts +0 -67
  270. package/src/store/git-status.ts +0 -15
  271. package/src/store/history-search.ts +0 -19
  272. package/src/store/log.ts +0 -725
  273. package/src/store/lsp-document.ts +0 -30
  274. package/src/store/mcp-credentials.ts +0 -85
  275. package/src/store/models.ts +0 -60
  276. package/src/store/observations.ts +0 -224
  277. package/src/store/packages.ts +0 -98
  278. package/src/store/patch.ts +0 -92
  279. package/src/store/permission-paths.ts +0 -34
  280. package/src/store/process-receipts.ts +0 -34
  281. package/src/store/query-source.ts +0 -49
  282. package/src/store/recovery.ts +0 -44
  283. package/src/store/replay.ts +0 -76
  284. package/src/store/resource-index.ts +0 -170
  285. package/src/store/resources.ts +0 -285
  286. package/src/store/restore.ts +0 -110
  287. package/src/store/review.ts +0 -109
  288. package/src/store/runtime-target.ts +0 -64
  289. package/src/store/search.ts +0 -728
  290. package/src/store/secrets.ts +0 -44
  291. package/src/store/serial.ts +0 -13
  292. package/src/store/session-archive.ts +0 -165
  293. package/src/store/session-browser.ts +0 -68
  294. package/src/store/session-position.ts +0 -27
  295. package/src/store/sessions.ts +0 -525
  296. package/src/store/source-cancellation.ts +0 -6
  297. package/src/store/tool-binaries.ts +0 -90
  298. package/src/store/trust.ts +0 -82
  299. package/src/store/update-cache.ts +0 -36
  300. package/src/store/user-settings.ts +0 -30
  301. package/src/store/workspace-clone.ts +0 -42
  302. package/src/store/workspace-observation.ts +0 -113
  303. package/src/store/workspace.ts +0 -115
  304. package/src/store/worktree.ts +0 -278
  305. package/src/tui/ansi-spans.ts +0 -126
  306. package/src/tui/diff.ts +0 -160
  307. package/src/tui/exhaustive.ts +0 -14
  308. package/src/tui/fuzzy.ts +0 -75
  309. package/src/tui/style.ts +0 -208
  310. package/src/tui/ui/atomic-draft.ts +0 -121
  311. package/src/tui/ui/branch-panel.ts +0 -47
  312. package/src/tui/ui/canvas.ts +0 -96
  313. package/src/tui/ui/colors.ts +0 -70
  314. package/src/tui/ui/composer.ts +0 -199
  315. package/src/tui/ui/control-panel.ts +0 -25
  316. package/src/tui/ui/control-sidebar.ts +0 -49
  317. package/src/tui/ui/dialog.ts +0 -398
  318. package/src/tui/ui/draft.ts +0 -23
  319. package/src/tui/ui/duration.ts +0 -16
  320. package/src/tui/ui/edge.ts +0 -186
  321. package/src/tui/ui/image-size.ts +0 -44
  322. package/src/tui/ui/ink.ts +0 -210
  323. package/src/tui/ui/items.ts +0 -42
  324. package/src/tui/ui/keys.ts +0 -40
  325. package/src/tui/ui/layout.ts +0 -74
  326. package/src/tui/ui/list-model.ts +0 -86
  327. package/src/tui/ui/list-view.ts +0 -172
  328. package/src/tui/ui/mascot.ts +0 -67
  329. package/src/tui/ui/messages.ts +0 -518
  330. package/src/tui/ui/motion.ts +0 -28
  331. package/src/tui/ui/paths.ts +0 -10
  332. package/src/tui/ui/pictures.ts +0 -108
  333. package/src/tui/ui/prompt-history.ts +0 -60
  334. package/src/tui/ui/screen.ts +0 -579
  335. package/src/tui/ui/status.ts +0 -403
  336. package/src/tui/ui/styled.ts +0 -85
  337. package/src/tui/ui/tool-card.ts +0 -150
  338. package/src/tui/width.ts +0 -567
  339. package/src/vendor/pi-rich/LICENSE.upstream +0 -21
  340. package/src/vendor/pi-rich/UPSTREAM.json +0 -17
  341. package/src/vendor/pi-rich/clipboard-command.ts +0 -44
  342. package/src/vendor/pi-rich/exif-orientation.ts +0 -182
  343. package/src/vendor/pi-rich/html.ts +0 -51
  344. package/src/vendor/pi-rich/image-convert.ts +0 -49
  345. package/src/vendor/pi-rich/image-resize-core.ts +0 -164
  346. package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
  347. package/src/vendor/pi-rich/image-resize.ts +0 -123
  348. package/src/vendor/pi-rich/mime.ts +0 -116
  349. package/src/vendor/pi-rich/photon.ts +0 -139
  350. package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
@@ -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
- }