index.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. /**
  2. * The model-facing filesystem discovery tool suite (`glob`, `grep`) over the
  3. * packaged ripgrep binary (`@vscode/ripgrep`). This single plugin registers
  4. * both tools; the binary ships inside the npm dependency, so no system `rg`
  5. * install and no shell layer is involved.
  6. *
  7. * ## Spawn-backed, not a `ctx.fs` provider method
  8. *
  9. * Local workspace discovery is a process-backed `rg` workflow, so these tools
  10. * execute through `ctx.subprocess.spawn()` with fixed ripgrep argv templates —
  11. * never `ctx.bash`, never `ctx.bash.start()`, never a model-visible background
  12. * task. The tool layer owns schemas, argument validation, argv construction
  13. * ({@link module:@deepseek-ai/dsh-tool-fs-search/glob} /
  14. * {@link module:@deepseek-ai/dsh-tool-fs-search/grep}), result parsing,
  15. * retention, formatted-result spill, and timeout declaration; the subprocess
  16. * seam owns spawn execution, process-tree termination, environment scrubbing,
  17. * and raw output capture. The package injects `tools`, `systemPrompt`, and
  18. * `subprocess` — deliberately NOT `fs`, and `ctx.spillStore` is read
  19. * opportunistically with `ctx.get()` because formatted-result spill is optional.
  20. *
  21. * Returned paths are displayed relative to the resolved workdir and are
  22. * follow-up-readable only in co-located deployments where the workdir and the
  23. * filesystem `read` root are the same workspace — a documented v1 deployment
  24. * requirement, not runtime-validated.
  25. *
  26. * @module @deepseek-ai/dsh-tool-fs-search
  27. */
  28. import type { Context } from 'cordis'
  29. import z from 'schemastery'
  30. import { GLOB_MAX_RESULTS, applyGlobTool } from './glob.ts'
  31. import { GREP_MAX_LINE_BYTES, GREP_MAX_MATCHES, applyGrepTool } from './grep.ts'
  32. import { RAW_OUTPUT_MAX_BYTES, SEARCH_GRACE_MS, SEARCH_META_MAX_BYTES, SEARCH_STDERR_MAX_BYTES, SEARCH_TIMEOUT_MS } from './search-core.ts'
  33. export { GLOB_MAX_RESULTS, GLOB_VCS_EXCLUDES, applyGlobTool, buildGlobCommand, formatGlobOutput, parseGlobArgs, presentGlobCall, presentGlobResult, sampleAcrossTopLevel } from './glob.ts'
  34. export type { GlobInput, GlobSample, GlobToolCaps } from './glob.ts'
  35. export {
  36. GREP_MAX_LINE_BYTES,
  37. GREP_MAX_MATCHES,
  38. applyGrepTool,
  39. buildGrepCommand,
  40. formatGrepMatches,
  41. formatGrepOutput,
  42. parseGrepArgs,
  43. parseGrepMatches,
  44. presentGrepCall,
  45. presentGrepResult,
  46. } from './grep.ts'
  47. export type { GrepInput, GrepToolCaps } from './grep.ts'
  48. export {
  49. RAW_OUTPUT_MAX_BYTES,
  50. SEARCH_GRACE_MS,
  51. SEARCH_META_MAX_BYTES,
  52. SEARCH_STDERR_MAX_BYTES,
  53. SEARCH_TIMEOUT_MS,
  54. SearchError,
  55. previewLine,
  56. resolveRgPath,
  57. runRipgrep,
  58. toWorkdirRelative,
  59. trySaveFormattedResult,
  60. } from './search-core.ts'
  61. export type { GrepMatch, RipgrepRun, SearchErrorCode } from './search-core.ts'
  62. /** Cordis plugin name used by loader diagnostics. */
  63. export const name = 'tool-fs-search'
  64. /** Services required by the search tool suite (`spillStore` is optional, read via `ctx.get()`). */
  65. export const inject = ['tools', 'systemPrompt', 'subprocess']
  66. /** Plugin config; over-cap glob sampling is an explicit deployment choice and the remaining fields have defaults. */
  67. export interface Config {
  68. /** Whether an over-cap `glob` page is sampled across top-level entries instead of taking the modification-time head. */
  69. sampleOverCapGlobResults: boolean
  70. /** Max paths one `glob` call retains inline; later paths go to the formatted spill file. */
  71. globMaxResults?: number
  72. /** Max flat matches one `grep` call retains inline; later matches go to the formatted spill file. */
  73. grepMaxMatches?: number
  74. /** Max bytes retained for one matched-line preview (the cut preserves UTF-8 boundaries). */
  75. grepMaxLineBytes?: number
  76. /** Max bytes of one search's serialized `presentationMeta`; trailing groups/paths drop past it so the persisted card stays bounded. */
  77. searchMetaMaxBytes?: number
  78. /** Max complete raw `rg` stdout bytes a search will parse; larger raw output fails with `SEARCH_RAW_OUTPUT_OVERFLOW`. */
  79. rawOutputMaxBytes?: number
  80. /** Terminate-escalation grace period (ms) for one search process, handed to the subprocess seam. */
  81. graceMs?: number
  82. /** Max bytes retained for one search's stderr tail; the excerpt is embedded in `SEARCH_*` error messages, never shown on success. */
  83. stderrMaxBytes?: number
  84. /** Cooperative tool-call timeout budget (ms) on both tools, enforced by `@deepseek-ai/dsh-timeout-policy` through `exec.signal`. */
  85. timeoutMs?: number
  86. }
  87. export const Config: z<Config> = z.object({
  88. sampleOverCapGlobResults: z.boolean().required(),
  89. globMaxResults: z.number().default(GLOB_MAX_RESULTS),
  90. grepMaxMatches: z.number().default(GREP_MAX_MATCHES),
  91. grepMaxLineBytes: z.number().default(GREP_MAX_LINE_BYTES),
  92. searchMetaMaxBytes: z.number().default(SEARCH_META_MAX_BYTES),
  93. rawOutputMaxBytes: z.number().default(RAW_OUTPUT_MAX_BYTES),
  94. graceMs: z.number().default(SEARCH_GRACE_MS),
  95. stderrMaxBytes: z.number().default(SEARCH_STDERR_MAX_BYTES),
  96. timeoutMs: z.number().default(SEARCH_TIMEOUT_MS),
  97. })
  98. /** The shape after schemastery applied the defaults. */
  99. type ResolvedConfig = Required<Config>
  100. /** Every search cap counts items/bytes/milliseconds — a positive integer, or retention and timeout arithmetic misbehaves silently. */
  101. function assertPositiveInteger(name: string, value: number): void {
  102. if (!Number.isInteger(value) || value < 1) {
  103. throw new Error(`tool-fs-search: ${name} must be a positive integer`)
  104. }
  105. }
  106. /**
  107. * Register the `glob`/`grep` filesystem discovery tool suite. The packaged
  108. * ripgrep binary is always available (an npm dependency), so registration is
  109. * unconditional.
  110. *
  111. * @param ctx - plugin context; registrations are effects scoped to this plugin.
  112. * @param config - resolved plugin configuration from schemastery.
  113. */
  114. // oxlint-disable-next-line typescript/require-await -- async keeps a load-time config rejection a rejection, not a synchronous throw
  115. export async function apply(ctx: Context, config: Config): Promise<void> {
  116. // schemastery (Config) has already filled every defaulted field.
  117. const resolved = config as ResolvedConfig
  118. assertPositiveInteger('globMaxResults', resolved.globMaxResults)
  119. assertPositiveInteger('grepMaxMatches', resolved.grepMaxMatches)
  120. assertPositiveInteger('grepMaxLineBytes', resolved.grepMaxLineBytes)
  121. assertPositiveInteger('searchMetaMaxBytes', resolved.searchMetaMaxBytes)
  122. assertPositiveInteger('rawOutputMaxBytes', resolved.rawOutputMaxBytes)
  123. assertPositiveInteger('graceMs', resolved.graceMs)
  124. assertPositiveInteger('stderrMaxBytes', resolved.stderrMaxBytes)
  125. assertPositiveInteger('timeoutMs', resolved.timeoutMs)
  126. applyGlobTool(ctx, {
  127. sampleOverCapGlobResults: resolved.sampleOverCapGlobResults,
  128. maxResults: resolved.globMaxResults,
  129. maxMetaBytes: resolved.searchMetaMaxBytes,
  130. rawOutputMaxBytes: resolved.rawOutputMaxBytes,
  131. graceMs: resolved.graceMs,
  132. stderrMaxBytes: resolved.stderrMaxBytes,
  133. timeoutMs: resolved.timeoutMs,
  134. })
  135. applyGrepTool(ctx, {
  136. maxMatches: resolved.grepMaxMatches,
  137. maxLineBytes: resolved.grepMaxLineBytes,
  138. maxMetaBytes: resolved.searchMetaMaxBytes,
  139. rawOutputMaxBytes: resolved.rawOutputMaxBytes,
  140. graceMs: resolved.graceMs,
  141. stderrMaxBytes: resolved.stderrMaxBytes,
  142. timeoutMs: resolved.timeoutMs,
  143. })
  144. }