rovecode 0.4.0-beta.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (432) hide show
  1. package/README.md +57 -72
  2. package/THIRD_PARTY_NOTICES.md +0 -44
  3. package/bin/rovecode.ts +21 -0
  4. package/package.json +16 -38
  5. package/src/account/keys.ts +97 -0
  6. package/src/account/login.ts +158 -0
  7. package/src/account/provision.ts +47 -0
  8. package/src/account/store.ts +63 -0
  9. package/src/acp/server.ts +373 -0
  10. package/src/cli/account-cmd.ts +116 -0
  11. package/src/cli/connect.ts +244 -0
  12. package/src/cli/context-cmd.ts +199 -0
  13. package/src/cli/dispatch.ts +109 -0
  14. package/src/cli/doctor.ts +324 -0
  15. package/src/cli/export.ts +278 -0
  16. package/src/cli/help.ts +240 -0
  17. package/src/cli/is-tui-invocation.ts +8 -0
  18. package/src/cli/main.ts +599 -0
  19. package/src/cli/market-cmd.ts +658 -0
  20. package/src/cli/mcp-market-cmd.ts +299 -0
  21. package/src/cli/output.ts +382 -0
  22. package/src/cli/repl.ts +172 -0
  23. package/src/cli/resume.ts +32 -0
  24. package/src/cli/run-limits.ts +78 -0
  25. package/src/cli/runtime.ts +792 -0
  26. package/src/cli/setup.ts +187 -0
  27. package/src/cli/update-cmd.ts +78 -0
  28. package/src/cli/workflow-cmd.ts +100 -0
  29. package/src/coding/checkpoints.ts +270 -0
  30. package/src/coding/diff.ts +136 -0
  31. package/src/coding/files.ts +339 -0
  32. package/src/coding/hashline.ts +319 -0
  33. package/src/coding/lsp.ts +406 -0
  34. package/src/coding/repomap-cache.ts +99 -0
  35. package/src/coding/repomap-files.ts +110 -0
  36. package/src/coding/repomap.ts +392 -0
  37. package/src/core/compaction.ts +399 -0
  38. package/src/core/config.ts +289 -0
  39. package/src/core/context-report.ts +228 -0
  40. package/src/core/context.ts +60 -0
  41. package/src/core/count-remote.ts +107 -0
  42. package/src/core/execpolicy-rules.ts +196 -0
  43. package/src/core/execpolicy.ts +385 -0
  44. package/src/core/executor.ts +397 -0
  45. package/src/core/guardrails.ts +400 -0
  46. package/src/core/hooks.ts +398 -0
  47. package/src/core/images.ts +230 -0
  48. package/src/core/intro.ts +236 -0
  49. package/src/core/loop.ts +621 -0
  50. package/src/core/modes.ts +372 -0
  51. package/src/core/orchestrator.ts +207 -0
  52. package/src/core/reflection.ts +165 -0
  53. package/src/core/sandbox-config.ts +167 -0
  54. package/src/core/session-images.ts +73 -0
  55. package/src/core/session.ts +398 -0
  56. package/src/core/settings.ts +98 -0
  57. package/src/core/stuck-detector.ts +273 -0
  58. package/src/core/tasks.ts +374 -0
  59. package/src/core/token-scale.ts +108 -0
  60. package/src/core/tool-output-budget.ts +166 -0
  61. package/src/core/tools.ts +288 -0
  62. package/src/core/types.ts +330 -0
  63. package/src/core/update-check.ts +171 -0
  64. package/src/core/update.ts +158 -0
  65. package/src/core/usage.ts +204 -0
  66. package/src/core/validate.ts +121 -0
  67. package/src/core/verify-gate.ts +159 -0
  68. package/src/core/verify.ts +237 -0
  69. package/src/core/voice.ts +158 -0
  70. package/src/core/win-job.ts +183 -0
  71. package/src/design/audit.ts +797 -0
  72. package/src/design/direction.ts +190 -0
  73. package/src/design/rules.ts +157 -0
  74. package/src/eval/bench.ts +150 -0
  75. package/src/eval/gauntlet-runner.ts +218 -0
  76. package/src/eval/gauntlet.ts +226 -0
  77. package/src/eval/grader.ts +186 -0
  78. package/src/eval/record.ts +202 -0
  79. package/src/eval/redact.ts +141 -0
  80. package/src/eval/replay.ts +147 -0
  81. package/src/eval/trajectory.ts +373 -0
  82. package/src/index.ts +17 -0
  83. package/src/market/catalogs/mcp-docs.json +111 -0
  84. package/src/market/catalogs/plugins.json +111 -0
  85. package/src/market/catalogs/skills.json +478 -0
  86. package/src/market/clone.ts +72 -0
  87. package/src/market/context-cost.ts +121 -0
  88. package/src/market/digest.ts +106 -0
  89. package/src/market/index.ts +22 -0
  90. package/src/market/install.ts +578 -0
  91. package/src/market/manifest.ts +187 -0
  92. package/src/market/prereq.ts +145 -0
  93. package/src/market/registry.ts +363 -0
  94. package/src/market/resolve.ts +111 -0
  95. package/src/market/types.ts +236 -0
  96. package/src/market/validate.ts +227 -0
  97. package/src/mcp/client.ts +431 -0
  98. package/src/mcp/config.ts +239 -0
  99. package/src/mcp/local-package.ts +211 -0
  100. package/src/mcp/market-catalog.ts +84 -0
  101. package/src/mcp/market-install.ts +289 -0
  102. package/src/mcp/market.ts +0 -0
  103. package/src/mcp/tools.ts +131 -0
  104. package/src/mcp/trust.ts +49 -0
  105. package/src/memory/blocks.ts +175 -0
  106. package/src/memory/recall.ts +355 -0
  107. package/src/memory/store.ts +105 -0
  108. package/src/memory/tools.ts +99 -0
  109. package/src/plugins/cli.ts +123 -0
  110. package/src/plugins/discover.ts +108 -0
  111. package/src/plugins/index.ts +50 -0
  112. package/src/plugins/init.ts +140 -0
  113. package/src/plugins/install.ts +184 -0
  114. package/src/plugins/load.ts +149 -0
  115. package/src/plugins/manifest.ts +106 -0
  116. package/src/plugins/state.ts +83 -0
  117. package/src/providers/auth.ts +293 -0
  118. package/src/providers/cache.ts +223 -0
  119. package/src/providers/catalog-local.ts +160 -0
  120. package/src/providers/catalog.ts +408 -0
  121. package/src/providers/middleware-context.ts +86 -0
  122. package/src/providers/middleware.ts +373 -0
  123. package/src/providers/profile-glm53.ts +111 -0
  124. package/src/providers/profile-sonnet5-persona.ts +65 -0
  125. package/src/providers/profile-sonnet5-voice.ts +23 -0
  126. package/src/providers/profiles.ts +156 -0
  127. package/src/providers/provider-config.ts +311 -0
  128. package/src/providers/registry.ts +302 -0
  129. package/src/providers/response-validation.ts +80 -0
  130. package/src/providers/retry.ts +234 -0
  131. package/src/providers/router.ts +294 -0
  132. package/src/providers/sse.ts +26 -0
  133. package/src/providers/stream-errors.ts +117 -0
  134. package/src/providers/stream.ts +569 -0
  135. package/src/providers/thinking.ts +189 -0
  136. package/src/providers/wire-messages.ts +129 -0
  137. package/src/sdk/client.ts +225 -0
  138. package/src/sdk/index.ts +3 -0
  139. package/src/server/dashboard.ts +144 -0
  140. package/src/server/http.ts +343 -0
  141. package/src/server/openapi.ts +246 -0
  142. package/src/sextant/card-hits.ts +102 -0
  143. package/src/sextant/card-keys.ts +55 -0
  144. package/src/sextant/context-source.ts +157 -0
  145. package/src/sextant/draw-agents.ts +273 -0
  146. package/src/sextant/draw-code.ts +388 -0
  147. package/src/sextant/draw-context.ts +222 -0
  148. package/src/sextant/draw-frame.ts +164 -0
  149. package/src/sextant/draw-market.ts +573 -0
  150. package/src/sextant/draw-messages.ts +386 -0
  151. package/src/sextant/draw-pet.ts +230 -0
  152. package/src/sextant/draw-plan.ts +159 -0
  153. package/src/sextant/draw-tabs.ts +85 -0
  154. package/src/sextant/draw-util.ts +65 -0
  155. package/src/sextant/engine.ts +230 -0
  156. package/src/sextant/frame-hits.ts +25 -0
  157. package/src/sextant/frame.ts +101 -0
  158. package/src/sextant/git-status.ts +197 -0
  159. package/src/sextant/grid.ts +59 -0
  160. package/src/sextant/input.ts +119 -0
  161. package/src/sextant/keys.ts +488 -0
  162. package/src/sextant/layout.ts +86 -0
  163. package/src/sextant/local-commands.ts +156 -0
  164. package/src/sextant/market-source.ts +287 -0
  165. package/src/sextant/mentions.ts +141 -0
  166. package/src/sextant/message-hits.ts +26 -0
  167. package/src/sextant/model.ts +387 -0
  168. package/src/sextant/overlays.ts +451 -0
  169. package/src/sextant/panel-hits.ts +38 -0
  170. package/src/sextant/pet.ts +399 -0
  171. package/src/sextant/screen.ts +324 -0
  172. package/src/sextant/scroll-hits.ts +66 -0
  173. package/src/sextant/scrollbar.ts +82 -0
  174. package/src/sextant/selection.ts +123 -0
  175. package/src/sextant/sextant-bridge.ts +174 -0
  176. package/src/sextant/sextant-cards.ts +142 -0
  177. package/src/sextant/sextant-diff-base.ts +63 -0
  178. package/src/sextant/sextant-files.ts +154 -0
  179. package/src/sextant/sextant-frame-loop.ts +314 -0
  180. package/src/sextant/sextant-renderer.ts +478 -0
  181. package/src/sextant/sextant-repo.ts +131 -0
  182. package/src/sextant/theme.ts +66 -0
  183. package/src/sextant/tool-rows.ts +189 -0
  184. package/src/sextant/types.ts +473 -0
  185. package/src/skills/index.ts +306 -0
  186. package/src/skills/tools.ts +69 -0
  187. package/src/skills/versioned.ts +227 -0
  188. package/src/telemetry/otel.ts +353 -0
  189. package/src/telemetry/otlp.ts +68 -0
  190. package/src/tools/ask-user.ts +156 -0
  191. package/src/tools/design.ts +151 -0
  192. package/src/tools/evalcell.ts +338 -0
  193. package/src/tools/html-text.ts +139 -0
  194. package/src/tools/provider.ts +149 -0
  195. package/src/tools/task.ts +216 -0
  196. package/src/tools/todo.ts +320 -0
  197. package/src/tools/webfetch.ts +331 -0
  198. package/src/tui/app.ts +608 -0
  199. package/src/tui/attach.ts +127 -0
  200. package/src/tui/checkpoints-cmd.ts +70 -0
  201. package/src/tui/clipboard-image.ts +81 -0
  202. package/src/tui/commands.ts +277 -0
  203. package/src/tui/cost.ts +108 -0
  204. package/src/tui/info-cmd.ts +144 -0
  205. package/src/tui/mcp-cmd.ts +128 -0
  206. package/src/tui/modes-cmd.ts +45 -0
  207. package/src/tui/overlays.ts +97 -0
  208. package/src/tui/pi-renderer.ts +424 -0
  209. package/src/tui/providers-cmd.ts +366 -0
  210. package/src/tui/renderer.ts +101 -0
  211. package/src/tui/replay-marker.ts +29 -0
  212. package/src/tui/session-cmd.ts +146 -0
  213. package/src/tui/sextant-attach.ts +68 -0
  214. package/src/tui/sextant-io.ts +184 -0
  215. package/src/tui/sextant-smoke.ts +110 -0
  216. package/src/tui/smoke.ts +72 -0
  217. package/src/tui/theme.ts +59 -0
  218. package/src/tui/todo-label.ts +7 -0
  219. package/src/workflow/engine.ts +266 -0
  220. package/tsconfig.json +30 -0
  221. package/vendor/pi-tui/LICENSE +21 -0
  222. package/vendor/pi-tui/PATCHES.md +12 -0
  223. package/vendor/pi-tui/PROVENANCE.md +12 -0
  224. package/vendor/pi-tui/README.upstream.md +854 -0
  225. package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  226. package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  227. package/vendor/pi-tui/src/alt-screen-search.ts +158 -0
  228. package/vendor/pi-tui/src/autocomplete.ts +827 -0
  229. package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
  230. package/vendor/pi-tui/src/components/box.ts +138 -0
  231. package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
  232. package/vendor/pi-tui/src/components/editor.ts +2364 -0
  233. package/vendor/pi-tui/src/components/h-stack.ts +45 -0
  234. package/vendor/pi-tui/src/components/image.ts +128 -0
  235. package/vendor/pi-tui/src/components/input.ts +448 -0
  236. package/vendor/pi-tui/src/components/loader.ts +93 -0
  237. package/vendor/pi-tui/src/components/markdown.ts +1016 -0
  238. package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
  239. package/vendor/pi-tui/src/components/select-list.ts +230 -0
  240. package/vendor/pi-tui/src/components/settings-list.ts +277 -0
  241. package/vendor/pi-tui/src/components/spacer.ts +29 -0
  242. package/vendor/pi-tui/src/components/stack.ts +155 -0
  243. package/vendor/pi-tui/src/components/text.ts +108 -0
  244. package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
  245. package/vendor/pi-tui/src/components/v-stack.ts +34 -0
  246. package/vendor/pi-tui/src/editor-component.ts +75 -0
  247. package/vendor/pi-tui/src/fuzzy.ts +138 -0
  248. package/vendor/pi-tui/src/index.ts +149 -0
  249. package/vendor/pi-tui/src/keybindings.ts +321 -0
  250. package/vendor/pi-tui/src/keys.ts +1402 -0
  251. package/vendor/pi-tui/src/kill-ring.ts +47 -0
  252. package/vendor/pi-tui/src/latex.ts +1381 -0
  253. package/vendor/pi-tui/src/layout-node.ts +52 -0
  254. package/vendor/pi-tui/src/layout.ts +411 -0
  255. package/vendor/pi-tui/src/native-modifiers.ts +60 -0
  256. package/vendor/pi-tui/src/native-module-path.ts +32 -0
  257. package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
  258. package/vendor/pi-tui/src/terminal-colors.ts +74 -0
  259. package/vendor/pi-tui/src/terminal-image.ts +701 -0
  260. package/vendor/pi-tui/src/terminal.ts +554 -0
  261. package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
  262. package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
  263. package/vendor/pi-tui/src/tui.ts +1264 -0
  264. package/vendor/pi-tui/src/undo-stack.ts +29 -0
  265. package/vendor/pi-tui/src/utils.ts +1327 -0
  266. package/vendor/pi-tui/src/word-navigation.ts +118 -0
  267. package/vendor/pi-tui/test/test-themes.ts +39 -0
  268. package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
  269. package/CHANGELOG.md +0 -527
  270. package/bin/rovecode.js +0 -24
  271. package/dist/cli/app-j6gn14w3.js +0 -2
  272. package/dist/cli/ask-user-cwstt8fz.js +0 -2
  273. package/dist/cli/auth-login-9bbp9915.js +0 -2
  274. package/dist/cli/auth-m8p9grty.js +0 -2
  275. package/dist/cli/bench-16zqdms5.js +0 -9
  276. package/dist/cli/catalog-1xchffa4.js +0 -2
  277. package/dist/cli/cli-1n1zb64f.js +0 -2
  278. package/dist/cli/client-2t9gjkck.js +0 -2
  279. package/dist/cli/commands-exafvm2b.js +0 -2
  280. package/dist/cli/connect-6zde0kn3.js +0 -2
  281. package/dist/cli/context-cmd-5t43wgqt.js +0 -2
  282. package/dist/cli/context-report-kt01pw8y.js +0 -2
  283. package/dist/cli/count-remote-ap7x3vh6.js +0 -2
  284. package/dist/cli/design-ne5zszyh.js +0 -2
  285. package/dist/cli/dispatch-2r5myxye.js +0 -2
  286. package/dist/cli/doctor-ws4fh4tn.js +0 -3
  287. package/dist/cli/executor-bdrjn634.js +0 -2
  288. package/dist/cli/export-1mxb9g5p.js +0 -2
  289. package/dist/cli/files-g104xghh.js +0 -2
  290. package/dist/cli/gauntlet-07xrjpj7.js +0 -2
  291. package/dist/cli/gauntlet-runner-xvy64436.js +0 -10
  292. package/dist/cli/gauntlet-wave3-jm91yt5w.js +0 -5
  293. package/dist/cli/gauntlet-wave4-r13py7p1.js +0 -14
  294. package/dist/cli/hashline-znvrat11.js +0 -2
  295. package/dist/cli/http-xafw6fsh.js +0 -143
  296. package/dist/cli/index-1sgjm25y.js +0 -2
  297. package/dist/cli/init-g2m0tn4m.js +0 -51
  298. package/dist/cli/install-avaqjjqq.js +0 -2
  299. package/dist/cli/loop-mmpfft01.js +0 -2
  300. package/dist/cli/main-0904f6ps.js +0 -5
  301. package/dist/cli/main-0ab9fc26.js +0 -9
  302. package/dist/cli/main-0jys2ccn.js +0 -3
  303. package/dist/cli/main-0mtcdbs7.js +0 -3
  304. package/dist/cli/main-0z1w2zsg.js +0 -3
  305. package/dist/cli/main-1dchs7xv.js +0 -18
  306. package/dist/cli/main-1ereejm1.js +0 -3
  307. package/dist/cli/main-1k1kw6b5.js +0 -3
  308. package/dist/cli/main-27y4sm2k.js +0 -38
  309. package/dist/cli/main-2wwjex5j.js +0 -58
  310. package/dist/cli/main-2yeveeve.js +0 -6
  311. package/dist/cli/main-2yfck9b5.js +0 -3
  312. package/dist/cli/main-2zmzgkwh.js +0 -3
  313. package/dist/cli/main-351pz3z7.js +0 -7
  314. package/dist/cli/main-3gjqfh7a.js +0 -6
  315. package/dist/cli/main-3nf3kgve.js +0 -3
  316. package/dist/cli/main-3pjrb2hd.js +0 -3
  317. package/dist/cli/main-3rxcvgna.js +0 -19
  318. package/dist/cli/main-4b3jgy66.js +0 -19
  319. package/dist/cli/main-4wndhjdc.js +0 -7
  320. package/dist/cli/main-4xcmvxnk.js +0 -3
  321. package/dist/cli/main-5tbz0wbz.js +0 -4
  322. package/dist/cli/main-5ywnwthm.js +0 -3
  323. package/dist/cli/main-6b62vkz0.js +0 -14
  324. package/dist/cli/main-6dnk69vp.js +0 -3
  325. package/dist/cli/main-6genrmhs.js +0 -136
  326. package/dist/cli/main-73g7eff4.js +0 -15
  327. package/dist/cli/main-7c5thhjd.js +0 -5
  328. package/dist/cli/main-7rn6bqje.js +0 -3
  329. package/dist/cli/main-80haw7qk.js +0 -4
  330. package/dist/cli/main-875s60s2.js +0 -4
  331. package/dist/cli/main-8kjxbpw4.js +0 -8
  332. package/dist/cli/main-90ds1z4e.js +0 -10
  333. package/dist/cli/main-9etavkew.js +0 -3
  334. package/dist/cli/main-a9njrkk1.js +0 -3
  335. package/dist/cli/main-aecrjq2d.js +0 -12
  336. package/dist/cli/main-ck9asesq.js +0 -9
  337. package/dist/cli/main-cta9racd.js +0 -4
  338. package/dist/cli/main-ddv7j2ag.js +0 -3
  339. package/dist/cli/main-dfreez27.js +0 -10
  340. package/dist/cli/main-f7rw7des.js +0 -3
  341. package/dist/cli/main-ggcn7rd7.js +0 -5
  342. package/dist/cli/main-gzkmycnv.js +0 -3
  343. package/dist/cli/main-hq51jg8v.js +0 -18
  344. package/dist/cli/main-jft389w9.js +0 -8
  345. package/dist/cli/main-k1eqkg83.js +0 -3
  346. package/dist/cli/main-k2y8a2aw.js +0 -9
  347. package/dist/cli/main-kcpbykxz.js +0 -4
  348. package/dist/cli/main-kd488vje.js +0 -22
  349. package/dist/cli/main-kh32yvgk.js +0 -5
  350. package/dist/cli/main-kqxnqjnv.js +0 -25
  351. package/dist/cli/main-kyn0xnsg.js +0 -3
  352. package/dist/cli/main-m1kk6fp5.js +0 -21
  353. package/dist/cli/main-mv40pcr2.js +0 -4
  354. package/dist/cli/main-n0t3973w.js +0 -3
  355. package/dist/cli/main-nqveez48.js +0 -4
  356. package/dist/cli/main-pknhvrmj.js +0 -3
  357. package/dist/cli/main-pn1w7a7j.js +0 -3
  358. package/dist/cli/main-prxxs70n.js +0 -4
  359. package/dist/cli/main-q3vsesf9.js +0 -3
  360. package/dist/cli/main-qsevpgsv.js +0 -3
  361. package/dist/cli/main-rdgdw24b.js +0 -25
  362. package/dist/cli/main-rfth4tbm.js +0 -16
  363. package/dist/cli/main-rg0wn0xf.js +0 -5
  364. package/dist/cli/main-sdmxhtv8.js +0 -4
  365. package/dist/cli/main-skbp13js.js +0 -18
  366. package/dist/cli/main-t4xnd213.js +0 -7
  367. package/dist/cli/main-vqak588n.js +0 -4
  368. package/dist/cli/main-w2n1303f.js +0 -9
  369. package/dist/cli/main-wbrdspr2.js +0 -5
  370. package/dist/cli/main-wsrg79c1.js +0 -7
  371. package/dist/cli/main-x4r0fne4.js +0 -5
  372. package/dist/cli/main-xea2f3tn.js +0 -6
  373. package/dist/cli/main-xg704a3c.js +0 -3
  374. package/dist/cli/main-xvnrabfp.js +0 -16
  375. package/dist/cli/main-xy53xf0r.js +0 -4
  376. package/dist/cli/main-y1fqy60y.js +0 -3
  377. package/dist/cli/main-yn8cd281.js +0 -34
  378. package/dist/cli/main-yr0ksc0h.js +0 -4
  379. package/dist/cli/main-z2ex2vyf.js +0 -4
  380. package/dist/cli/main-z3aayzvq.js +0 -3
  381. package/dist/cli/main-zaqh35jg.js +0 -3
  382. package/dist/cli/main-zc2e8e46.js +0 -4
  383. package/dist/cli/main-zzrfw6cf.js +0 -13
  384. package/dist/cli/main.js +0 -280
  385. package/dist/cli/market-cmd-e14kmx9n.js +0 -5
  386. package/dist/cli/mcp-login-wq7ktdek.js +0 -2
  387. package/dist/cli/mcp-market-cmd-9mg3jecy.js +0 -2
  388. package/dist/cli/notify-b7qc0cjb.js +0 -2
  389. package/dist/cli/oauth-z8whcgfx.js +0 -2
  390. package/dist/cli/output-b3ewj3ps.js +0 -16
  391. package/dist/cli/profiles-6mr5he5e.js +0 -2
  392. package/dist/cli/provider-config-g7j42q8x.js +0 -2
  393. package/dist/cli/provider-jr1y8vvm.js +0 -2
  394. package/dist/cli/registry-s8yk86g0.js +0 -2
  395. package/dist/cli/registry-t6p8d4mn.js +0 -2
  396. package/dist/cli/repl-bajwe1mh.js +0 -11
  397. package/dist/cli/resume-rwn9nz7y.js +0 -2
  398. package/dist/cli/run-flags-nah7ndpt.js +0 -2
  399. package/dist/cli/runtime-n7gafzhb.js +0 -2
  400. package/dist/cli/sandbox-config-emdy18x4.js +0 -2
  401. package/dist/cli/server-b0nvs2bn.js +0 -5
  402. package/dist/cli/session-arg-y75wd4kj.js +0 -2
  403. package/dist/cli/session-j62evmjq.js +0 -2
  404. package/dist/cli/sessions-cmd-tsnwz0ns.js +0 -7
  405. package/dist/cli/settings-df10wfez.js +0 -2
  406. package/dist/cli/setup-jzvv72fg.js +0 -2
  407. package/dist/cli/sextant-smoke-37m81ke6.js +0 -5
  408. package/dist/cli/skills-cmd-gjxnxnhx.js +0 -2
  409. package/dist/cli/smoke-p7748apt.js +0 -8
  410. package/dist/cli/start-chat-s4st3mm0.js +0 -12
  411. package/dist/cli/stream-gmeyewds.js +0 -2
  412. package/dist/cli/task-gh0kkp3n.js +0 -2
  413. package/dist/cli/tasks-z1kfpe8e.js +0 -2
  414. package/dist/cli/thinking-0eqkrz6t.js +0 -2
  415. package/dist/cli/todo-5brcrt9m.js +0 -2
  416. package/dist/cli/tools-7pzm0vj9.js +0 -2
  417. package/dist/cli/tools-s635p6s8.js +0 -2
  418. package/dist/cli/trust-cmd-cjav8zgm.js +0 -2
  419. package/dist/cli/update-check-pt31bm2f.js +0 -2
  420. package/dist/cli/update-cmd-tk131s9t.js +0 -2
  421. package/dist/cli/voice-56nabd8d.js +0 -2
  422. package/dist/cli/webfetch-xd8q596m.js +0 -2
  423. package/dist/cli/websearch-5hkf98k1.js +0 -2
  424. package/dist/cli/workflow-cmd-cy3cvzjp.js +0 -4
  425. package/dist/cli/workspace-q10g5z3e.js +0 -2
  426. package/dist/lib/index.js +0 -62
  427. package/dist/lib/models-index.json +0 -1
  428. package/dist/lib/plugins.js +0 -55
  429. package/dist/lib/providers.js +0 -17
  430. package/dist/lib/public-api.js +0 -20
  431. package/dist/lib/sdk.js +0 -360
  432. /package/{dist/cli → src/providers}/models-index.json +0 -0
@@ -0,0 +1,136 @@
1
+ /** Port #24 — bounded unified-diff preview for edit/write approvals.
2
+ *
3
+ * The post-change content is computed IN MEMORY (edit: hashline's pure apply over the
4
+ * current file; write: the proposed content), diffed against the file with jsdiff
5
+ * (`diff`, BSD-3-Clause — see THIRD_PARTY_NOTICES.md), and rendered git-style, clipped
6
+ * to maxLines with an exact "… +N more lines" marker. Never throws: stale anchors,
7
+ * binary content, malformed args and I/O errors all come back as kind "unavailable"
8
+ * carrying the reason, so the approval overlay degrades instead of blocking the ask.
9
+ * Shape follows gemini-cli's confirmation diffs — structuredPatch with a small context
10
+ * (packages/core/src/tools/diffOptions.ts:10-18) and CR-tolerant line comparison
11
+ * (diff-utils.ts:22 splits on /\r?\n/; here jsdiff's stripTrailingCr). No code copied. */
12
+
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import { isAbsolute, join, relative } from "node:path";
15
+ import { structuredPatch } from "diff";
16
+ import { applyEditsToContent, type EditFailure, type EditOp } from "./hashline.ts";
17
+
18
+ export type DiffKind = "modify" | "create" | "unchanged" | "unavailable";
19
+ export interface DiffPreview { text: string; truncated: boolean; kind: DiffKind }
20
+ export interface DiffPreviewOptions { maxLines?: number; context?: number }
21
+
22
+ /** default cap on rendered lines (headers + hunks); the TUI clips further to its rows */
23
+ export const DIFF_MAX_LINES = 40;
24
+ const DIFF_CONTEXT = 3;
25
+ /** git's buffer_is_binary heuristic: a NUL byte within the first 8000 bytes */
26
+ const BINARY_SNIFF = 8000;
27
+ /** Myers budget: a rewrite beyond this many edited lines is reported, not diffed (bounded CPU);
28
+ * the wall-clock cap is a safety net for huge files, the edit-length cap is the deterministic one */
29
+ const MAX_EDIT_LENGTH = 2000;
30
+ const MAX_DIFF_MS = 1000;
31
+ const MAX_CHARS = 4_000_000;
32
+
33
+ /** `… +N more lines` — the exact marker appended when the output is clipped. */
34
+ export function moreMarker(n: number): string { return `… +${n} more line${n === 1 ? "" : "s"}`; }
35
+
36
+ /** Keep the first `max` lines; the marker accounts for every hidden line. */
37
+ export function clipLines(lines: string[], max: number): string[] {
38
+ return lines.length > max ? [...lines.slice(0, max), moreMarker(lines.length - max)] : lines;
39
+ }
40
+
41
+ function resolvePath(cwd: string, p: string): string { return isAbsolute(p) ? p : join(cwd, p); }
42
+
43
+ function hasNul(buf: Uint8Array): boolean {
44
+ const n = Math.min(buf.length, BINARY_SNIFF);
45
+ for (let i = 0; i < n; i++) if (buf[i] === 0) return true;
46
+ return false;
47
+ }
48
+
49
+ const unavailable = (reason: string): DiffPreview => ({ text: `diff unavailable: ${reason}`, truncated: false, kind: "unavailable" });
50
+
51
+ function failureReason(f: EditFailure): string {
52
+ switch (f.kind) {
53
+ case "tag-mismatch": return `stale read — file TAG is now ${f.actual}, the edit expects ${f.expected}`;
54
+ case "hash-mismatch": return `stale anchor — line ${f.line} hash is ${f.actual}, the edit expects ${f.expected}; ${f.nearest}`;
55
+ case "out-of-range": return `line ${f.line} out of range (file has ${f.lineCount} lines)`;
56
+ }
57
+ }
58
+
59
+ /** The edit tool's args, validated (a malformed call yields "unavailable", never a throw). */
60
+ function editOps(args: Record<string, unknown>, path: string): EditOp[] | null {
61
+ if (!Array.isArray(args.edits)) return null;
62
+ const ops: EditOp[] = [];
63
+ for (const e of args.edits as unknown[]) {
64
+ const o = (typeof e === "object" && e !== null ? e : {}) as Record<string, unknown>;
65
+ if (typeof o.tag !== "string" || typeof o.anchorHash !== "string" || typeof o.anchorLine !== "number") return null;
66
+ if (!Array.isArray(o.newLines) || !o.newLines.every((l) => typeof l === "string")) return null;
67
+ ops.push({ path, tag: o.tag, anchorLine: o.anchorLine, anchorHash: o.anchorHash, newLines: o.newLines as string[] });
68
+ }
69
+ return ops;
70
+ }
71
+
72
+ /** Content → signed display lines: CRLF-tolerant, trailing newline dropped, missing EOF newline noted. */
73
+ function signedLines(content: string, sign: "+" | "-"): string[] {
74
+ const lines = content.replace(/\r\n/g, "\n").split("\n");
75
+ const eofNewline = lines[lines.length - 1] === "";
76
+ if (eofNewline) lines.pop();
77
+ const out = lines.map((l) => sign + l);
78
+ if (!eofNewline && out.length > 0) out.push("\");
79
+ return out;
80
+ }
81
+
82
+ function renderUnified(rel: string, before: string | null, after: string, opts: DiffPreviewOptions): DiffPreview {
83
+ const maxLines = opts.maxLines ?? DIFF_MAX_LINES;
84
+ if (before === after) return { text: "", truncated: false, kind: "unchanged" };
85
+ const body: string[] = [];
86
+ let kind: DiffKind = "modify";
87
+ if (before === null) {
88
+ // new file: every line is an add — no Myers pass needed (nor its cost on big files)
89
+ kind = "create";
90
+ const adds = signedLines(after, "+");
91
+ if (adds.length > 0) body.push(`@@ -0,0 +1,${adds.filter((l) => l[0] === "+").length} @@`, ...adds);
92
+ } else {
93
+ if (before.length + after.length > MAX_CHARS) return unavailable("file too large to preview");
94
+ // stripTrailingCr: a CRLF file diffs by content, not by line ending (gemini-cli diff-utils.ts:22)
95
+ const patch = structuredPatch(rel, rel, before, after, undefined, undefined,
96
+ { context: opts.context ?? DIFF_CONTEXT, stripTrailingCr: true, maxEditLength: MAX_EDIT_LENGTH, timeout: MAX_DIFF_MS });
97
+ if (patch === undefined) return unavailable(`change too large to preview (over ${MAX_EDIT_LENGTH} edited lines or ${MAX_DIFF_MS}ms)`);
98
+ for (const h of patch.hunks) body.push(`@@ -${h.oldStart},${h.oldLines} +${h.newStart},${h.newLines} @@`, ...h.lines);
99
+ if (body.length === 0) return { text: "no content changes (line endings only)", truncated: false, kind: "unchanged" };
100
+ }
101
+ const lines = [`--- ${before === null ? "/dev/null" : `a/${rel}`}`, `+++ b/${rel}`, ...body];
102
+ return { text: clipLines(lines, maxLines).join("\n"), truncated: lines.length > maxLines, kind };
103
+ }
104
+
105
+ /** Preview what an edit/write tool call would do to its file, as a bounded unified diff.
106
+ * `args` are the (revised) tool args exactly as the tool would receive them. */
107
+ export function previewDiff(tool: "edit" | "write", args: unknown, cwd: string, opts: DiffPreviewOptions = {}): DiffPreview {
108
+ try {
109
+ const a = (typeof args === "object" && args !== null ? args : {}) as Record<string, unknown>;
110
+ if (typeof a.path !== "string" || a.path === "") return unavailable("missing path");
111
+ const abs = resolvePath(cwd, a.path);
112
+ const rel = relative(cwd, abs).replace(/\\/g, "/") || a.path;
113
+ let before: string | null = null;
114
+ if (existsSync(abs)) {
115
+ const buf = readFileSync(abs);
116
+ if (hasNul(buf)) return unavailable("binary file");
117
+ before = buf.toString("utf8");
118
+ }
119
+ let after: string;
120
+ if (tool === "write") {
121
+ if (typeof a.content !== "string") return unavailable("missing content");
122
+ after = a.content;
123
+ } else {
124
+ if (before === null) return unavailable(`file not found: ${abs}`);
125
+ const ops = editOps(a, abs);
126
+ if (ops === null) return unavailable("malformed edits");
127
+ const r = applyEditsToContent(before, ops, abs);
128
+ if (!r.ok) return unavailable(failureReason(r.failure));
129
+ after = r.content;
130
+ }
131
+ if (after.includes("\0")) return unavailable("binary content");
132
+ return renderUnified(rel, before, after, opts);
133
+ } catch (e) {
134
+ return unavailable(e instanceof Error ? e.message : String(e));
135
+ }
136
+ }
@@ -0,0 +1,339 @@
1
+ /** First-class glob/grep/ls tools (PORT #22). Ported from opencode (MIT,
2
+ * snapshot ebece6e): glob/grep tool contracts, limits, and truncation markers
3
+ * (packages/opencode/src/tool/glob.ts:49-62, grep.ts:80-102) and the per-line
4
+ * 2000-char cap w/ surrogate guard (packages/core/src/ripgrep.ts:267-270).
5
+ * Bounded-output + ignore semantics follow gemini-cli (Apache-2.0, snapshot
6
+ * 0bd1d43): glob recency-then-alpha sort (packages/core/src/tools/glob.ts:47-70)
7
+ * and the ls output shape — dirs-first sort, [DIR] markers, "(N ignored)" note
8
+ * (packages/core/src/tools/ls.ts:191-271). Upstream shells out to ripgrep;
9
+ * rg is NOT guaranteed on PATH here, so matching is pure Bun/TS over a
10
+ * gitignore-aware enumeration: `git ls-files` in repos (repomap-files.ts
11
+ * seam, wave-2), a bounded walk skipping dotfiles/node_modules outside git. */
12
+
13
+ import { readdirSync, statSync, lstatSync, existsSync } from "node:fs";
14
+ import { readFile, stat } from "node:fs/promises";
15
+ import { isAbsolute, join, basename } from "node:path";
16
+ import { spawnSync } from "node:child_process";
17
+ import type { Tool, ToolOutput } from "../core/types.ts";
18
+ import { gitListFiles } from "./repomap-files.ts";
19
+
20
+ // ---------- bounds (advertised in schemas; args are clamped, never trusted) ----------
21
+
22
+ export const GLOB_LIMIT_DEFAULT = 100; // opencode glob.ts:49
23
+ export const GREP_LIMIT_DEFAULT = 100; // opencode grep.ts:67, gemini-cli DEFAULT_TOTAL_MAX_MATCHES
24
+ export const LS_LIMIT_DEFAULT = 200;
25
+ export const LIMIT_CAP = 1000; // hard ceiling for maxFiles/maxMatches/maxEntries args
26
+ export const SCAN_CAP = 10_000; // candidate files enumerated per call
27
+ export const GREP_LINE_CAP = 2000; // chars per matching line (opencode ripgrep.ts:268)
28
+ export const GREP_FILE_BYTES_CAP = 1024 * 1024; // grep skips larger files
29
+ const BINARY_SNIFF_BYTES = 8192; // NUL within the first 8k → binary, skip (rg default)
30
+
31
+ /** Exported so the cap CONTRACT is pinnable: ceiling (LIMIT_CAP), floor
32
+ * (absent / NaN / non-positive → the tool default), integer truncation. */
33
+ export function clampLimit(v: number | undefined, dflt: number): number {
34
+ return Number.isFinite(v) && v! > 0 ? Math.min(Math.floor(v!), LIMIT_CAP) : dflt;
35
+ }
36
+ function resolvePath(cwd: string, p: string | undefined): string {
37
+ if (!p) return cwd;
38
+ return isAbsolute(p) ? p : join(cwd, p);
39
+ }
40
+
41
+ // ---------- gitignore-aware enumeration ----------
42
+
43
+ export interface FileListing { rel: string[]; viaGit: boolean; capped: boolean }
44
+
45
+ /** Relative file paths (forward slashes) under root. Git path: `git ls-files`
46
+ * (tracked + untracked-unignored → .gitignore honored). Fallback: bounded
47
+ * per-dir-sorted DFS skipping dot-entries and node_modules (repomap-files.ts
48
+ * walk rules, generic file filter), lstat-based so symlinks/junctions are
49
+ * never followed — a directory link back into an ancestor is a cycle, not a
50
+ * subtree. Both deterministic, both ≤ cap; `cap` is a test seam only. */
51
+ export function listFiles(root: string, cap = SCAN_CAP): FileListing {
52
+ const fromGit = gitListFiles(root);
53
+ if (fromGit) {
54
+ const rel = fromGit.slice(0, cap + 1);
55
+ const capped = rel.length > cap;
56
+ if (capped) rel.pop();
57
+ return { rel, viaGit: true, capped };
58
+ }
59
+ const rel: string[] = [];
60
+ let capped = false;
61
+ const walk = (dir: string, prefix: string): void => {
62
+ let names: string[];
63
+ try { names = readdirSync(dir); } catch { return; }
64
+ for (const name of names.sort()) {
65
+ if (capped) return;
66
+ if (name.startsWith(".") || name === "node_modules") continue;
67
+ const full = join(dir, name);
68
+ let st;
69
+ try { st = lstatSync(full); } catch { continue; }
70
+ if (st.isSymbolicLink()) continue; // junction/symlink: never descend (cycle hazard)
71
+ if (st.isDirectory()) walk(full, `${prefix}${name}/`);
72
+ else if (st.isFile()) {
73
+ if (rel.length >= cap) { capped = true; return; }
74
+ rel.push(prefix + name);
75
+ }
76
+ }
77
+ };
78
+ walk(root, "");
79
+ return { rel, viaGit: false, capped };
80
+ }
81
+
82
+ /** rg --glob semantics (opencode ripgrep.ts:165): a pattern without "/" matches
83
+ * file NAMES at any depth; a pattern with "/" matches the root-relative path. */
84
+ function makeMatcher(pattern: string): (rel: string) => boolean {
85
+ const glob = new Bun.Glob(pattern);
86
+ if (!pattern.includes("/")) return (rel) => glob.match(basename(rel));
87
+ return (rel) => glob.match(rel);
88
+ }
89
+
90
+ /** Enumeration-cap marker shared by the glob/grep listings and the ls scan
91
+ * cap; exported so the exact string is pinnable without a 10k-file fixture. */
92
+ export const scanCapLine = (cap: number): string =>
93
+ `(File enumeration capped at ${cap} files; results may be incomplete.)`;
94
+ const scanCapNote = (capped: boolean, cap: number): string[] => (capped ? ["", scanCapLine(cap)] : []);
95
+
96
+ // ---------- grep helpers ----------
97
+
98
+ function isBinary(buf: Buffer): boolean {
99
+ return buf.subarray(0, BINARY_SNIFF_BYTES).includes(0);
100
+ }
101
+
102
+ /** opencode ripgrep.ts:267-270: cap line text, guard a split surrogate pair. */
103
+ function capLine(text: string): string {
104
+ return text.length > GREP_LINE_CAP
105
+ ? text.slice(0, GREP_LINE_CAP).replace(/[\uD800-\uDBFF]$/, "") + "..."
106
+ : text;
107
+ }
108
+
109
+ // ---------- ls helpers ----------
110
+
111
+ /** Names inside root that git ignores (git check-ignore --stdin; bounded).
112
+ * null = no git / not a repo → caller applies the non-git default excludes. */
113
+ function gitIgnoredNames(root: string, names: string[]): Set<string> | null {
114
+ try {
115
+ const res = spawnSync("git", ["-C", root, "check-ignore", "--stdin", "-z"],
116
+ { input: names.join("\0"), timeout: 5000, maxBuffer: 16 * 1024 * 1024, encoding: "utf8", windowsHide: true });
117
+ if (res.error || typeof res.stdout !== "string") return null;
118
+ if (res.status !== 0 && res.status !== 1) return null; // 0 = some ignored, 1 = none; 128 = not a repo
119
+ return new Set(res.stdout.split("\0").filter((s) => s.length > 0));
120
+ } catch {
121
+ return null;
122
+ }
123
+ }
124
+
125
+ /** Builds the three tools over ONE enumeration cap. `scanCap` is a test seam
126
+ * only (repomap.ts `opts.maxFiles` idiom) so the cap note and the bounded ls
127
+ * stat sweep are pinnable on 3-file fixtures; every production registration
128
+ * uses the module-level globTool/grepTool/lsTool built with SCAN_CAP. */
129
+ export function fileTools(scanCap = SCAN_CAP): { globTool: Tool; grepTool: Tool; lsTool: Tool } {
130
+ // ---------- glob ----------
131
+
132
+ const globTool: Tool = {
133
+ schema: {
134
+ name: "glob",
135
+ description: "Find files by glob pattern (e.g. \"**/*.ts\", \"src/**/*.test.ts\"; a pattern without \"/\" matches file names at any depth). Honors .gitignore in git repos; skips dotfiles and node_modules elsewhere. Returns absolute paths, most recently modified first, at most maxFiles (default 100, cap 1000) with a truncation note when more matched.",
136
+ args: {
137
+ type: "object",
138
+ properties: {
139
+ pattern: { type: "string", description: "glob pattern to match files against" },
140
+ path: { type: "string", description: "directory to search (default: session cwd)" },
141
+ maxFiles: { type: "integer", description: `max paths returned (default ${GLOB_LIMIT_DEFAULT}, cap ${LIMIT_CAP})` },
142
+ },
143
+ required: ["pattern"],
144
+ },
145
+ },
146
+ kind: "read",
147
+ sequential: false,
148
+ execute(args, ctx): Promise<ToolOutput> {
149
+ const a = args as { pattern: string; path?: string; maxFiles?: number };
150
+ const root = resolvePath(ctx.cwd, a.path);
151
+ if (!existsSync(root)) return Promise.resolve({ ok: false, output: `directory not found: ${root}` });
152
+ if (!statSync(root).isDirectory()) return Promise.resolve({ ok: false, output: `glob path must be a directory: ${root}` });
153
+ const limit = clampLimit(a.maxFiles, GLOB_LIMIT_DEFAULT);
154
+ let match: (rel: string) => boolean;
155
+ try { match = makeMatcher(a.pattern); } catch (e) {
156
+ return Promise.resolve({ ok: false, output: `invalid glob pattern: ${e instanceof Error ? e.message : String(e)}` });
157
+ }
158
+ const listing = listFiles(root, scanCap);
159
+ // stat for the recency sort; stat failure = deleted-but-tracked etc. → drop
160
+ const hits: { abs: string; mtimeMs: number }[] = [];
161
+ for (const rel of listing.rel) {
162
+ if (!match(rel)) continue;
163
+ const abs = join(root, rel);
164
+ try { hits.push({ abs, mtimeMs: statSync(abs).mtimeMs }); } catch { /* skip */ }
165
+ }
166
+ // gemini-cli glob.ts:47-70: files touched within 24h first (newest→oldest), rest alphabetical
167
+ const now = Date.now();
168
+ const oneDayMs = 24 * 60 * 60 * 1000;
169
+ hits.sort((x, y) => {
170
+ const xr = now - x.mtimeMs < oneDayMs, yr = now - y.mtimeMs < oneDayMs;
171
+ if (xr && yr) return y.mtimeMs - x.mtimeMs;
172
+ if (xr) return -1;
173
+ if (yr) return 1;
174
+ return x.abs.localeCompare(y.abs);
175
+ });
176
+ const truncated = hits.length > limit;
177
+ const shown = truncated ? hits.slice(0, limit) : hits;
178
+ const output: string[] = [];
179
+ if (shown.length === 0) output.push("No files found");
180
+ else {
181
+ output.push(...shown.map((h) => h.abs));
182
+ if (truncated) {
183
+ output.push("", `(Results are truncated: showing first ${limit} results. Consider using a more specific path or pattern.)`);
184
+ }
185
+ }
186
+ output.push(...scanCapNote(listing.capped, scanCap));
187
+ return Promise.resolve({ ok: true, output: output.join("\n"), data: { count: shown.length, truncated } });
188
+ },
189
+ };
190
+
191
+ // ---------- grep ----------
192
+
193
+ const grepTool: Tool = {
194
+ schema: {
195
+ name: "grep",
196
+ description: "Search file contents with a JS regular expression (e.g. \"log.*Error\", \"function\\s+\\w+\"). Optional glob filters files (\"*.ts\", \"src/**/*.tsx\"). Honors .gitignore in git repos; skips dotfiles/node_modules elsewhere, plus binary files and files over 1MB. Output is `path:line: text` matches, at most maxMatches (default 100, cap 1000), each line capped at 2000 chars, with truncation notes.",
197
+ args: {
198
+ type: "object",
199
+ properties: {
200
+ pattern: { type: "string", description: "regex to search for in file contents" },
201
+ path: { type: "string", description: "directory (or single file) to search (default: session cwd)" },
202
+ glob: { type: "string", description: "file filter glob, e.g. \"*.ts\" (names at any depth) or \"src/**/*.ts\"" },
203
+ maxMatches: { type: "integer", description: `max matches returned (default ${GREP_LIMIT_DEFAULT}, cap ${LIMIT_CAP})` },
204
+ },
205
+ required: ["pattern"],
206
+ },
207
+ },
208
+ kind: "read",
209
+ sequential: false,
210
+ async execute(args, ctx): Promise<ToolOutput> {
211
+ const a = args as { pattern: string; path?: string; glob?: string; maxMatches?: number };
212
+ let rx: RegExp;
213
+ try { rx = new RegExp(a.pattern); } catch (e) {
214
+ return { ok: false, output: `invalid regex: ${e instanceof Error ? e.message : String(e)}` };
215
+ }
216
+ const root = resolvePath(ctx.cwd, a.path);
217
+ if (!existsSync(root)) return { ok: false, output: `path not found: ${root}` };
218
+ const limit = clampLimit(a.maxMatches, GREP_LIMIT_DEFAULT);
219
+ let filter: ((rel: string) => boolean) | null = null;
220
+ if (a.glob) {
221
+ try { filter = makeMatcher(a.glob); } catch (e) {
222
+ return { ok: false, output: `invalid glob filter: ${e instanceof Error ? e.message : String(e)}` };
223
+ }
224
+ }
225
+ if (ctx.signal.aborted) return { ok: false, output: "grep aborted" }; // pre-aborted: enumerate nothing
226
+ // single-file target scans just that file (opencode grep.ts:62)
227
+ const singleFile = statSync(root).isFile();
228
+ const listing: FileListing = singleFile
229
+ ? { rel: [basename(root)], viaGit: false, capped: false }
230
+ : listFiles(root, scanCap);
231
+ const base = singleFile ? join(root, "..") : root;
232
+ const candidates = listing.rel.filter((rel) => (filter ? filter(rel) : true)).sort();
233
+
234
+ const matches: { abs: string; line: number; text: string }[] = [];
235
+ let truncated = false;
236
+ outer: for (const rel of candidates) {
237
+ // reads are awaited, so the loop yields per file and an abort that lands
238
+ // MID-scan is observed here — a synchronous scan could never see one
239
+ if (ctx.signal.aborted) return { ok: false, output: "grep aborted" };
240
+ const abs = singleFile ? root : join(base, rel);
241
+ let buf: Buffer;
242
+ try {
243
+ if ((await stat(abs)).size > GREP_FILE_BYTES_CAP) continue;
244
+ buf = await readFile(abs);
245
+ } catch { continue; }
246
+ if (isBinary(buf)) continue;
247
+ const lines = buf.toString("utf8").split(/\r?\n/);
248
+ for (let i = 0; i < lines.length; i++) {
249
+ if (!rx.test(lines[i]!)) continue;
250
+ if (matches.length >= limit) { truncated = true; break outer; }
251
+ matches.push({ abs, line: i + 1, text: capLine(lines[i]!) });
252
+ }
253
+ }
254
+ if (matches.length === 0) {
255
+ return { ok: true, output: ["No matches found", ...scanCapNote(listing.capped, scanCap)].join("\n"), data: { matches: 0, truncated: false } };
256
+ }
257
+ const output = [`Found ${matches.length} matches${truncated ? " (more matches available)" : ""}`];
258
+ output.push(...matches.map((m) => `${m.abs}:${m.line}: ${m.text}`));
259
+ if (truncated) output.push("", "(Results truncated. Consider using a more specific path or pattern.)");
260
+ output.push(...scanCapNote(listing.capped, scanCap));
261
+ return { ok: true, output: output.join("\n"), data: { matches: matches.length, truncated } };
262
+ },
263
+ };
264
+
265
+ // ---------- ls ----------
266
+
267
+ const lsTool: Tool = {
268
+ schema: {
269
+ name: "ls",
270
+ description: "List a directory (non-recursive). Directories first as `[DIR] name`, files as `name (N bytes)`, alphabetical. Gitignored entries are hidden and counted in git repos; dotfiles and node_modules are hidden elsewhere. At most maxEntries rows (default 200, cap 1000) with a truncation note.",
271
+ args: {
272
+ type: "object",
273
+ properties: {
274
+ path: { type: "string", description: "directory to list (default: session cwd)" },
275
+ maxEntries: { type: "integer", description: `max entries returned (default ${LS_LIMIT_DEFAULT}, cap ${LIMIT_CAP})` },
276
+ },
277
+ },
278
+ },
279
+ kind: "read",
280
+ sequential: false,
281
+ execute(args, ctx): Promise<ToolOutput> {
282
+ const a = args as { path?: string; maxEntries?: number };
283
+ const root = resolvePath(ctx.cwd, a.path);
284
+ if (!existsSync(root)) return Promise.resolve({ ok: false, output: `directory not found: ${root}` });
285
+ if (!statSync(root).isDirectory()) return Promise.resolve({ ok: false, output: `Path is not a directory: ${root}` });
286
+ const limit = clampLimit(a.maxEntries, LS_LIMIT_DEFAULT);
287
+
288
+ const names = readdirSync(root).filter((n) => n !== ".git");
289
+ const ignored = gitIgnoredNames(root, names);
290
+ let kept: string[];
291
+ let ignoredCount: number;
292
+ if (ignored) {
293
+ kept = names.filter((n) => !ignored.has(n));
294
+ ignoredCount = names.length - kept.length;
295
+ } else {
296
+ // outside git: dotfile/node_modules default excludes (gemini-cli filters via
297
+ // its FileDiscoveryService; we approximate with the walk rules)
298
+ kept = names.filter((n) => !n.startsWith(".") && n !== "node_modules");
299
+ ignoredCount = names.length - kept.length;
300
+ }
301
+ if (kept.length === 0) {
302
+ const note = ignoredCount > 0 ? `\n\n(${ignoredCount} ignored)` : "";
303
+ return Promise.resolve({ ok: true, output: `Directory ${root} is empty.${note}`, data: { entries: 0, truncated: false } });
304
+ }
305
+
306
+ // the per-entry stat sweep is bounded by the scan cap: alphabetical slice
307
+ // FIRST, then stat only the survivors (a 30k-entry dir with maxEntries 5
308
+ // used to stat all 30k before truncating). Dirs-first ordering therefore
309
+ // holds within the scanned slice; the cap note flags what was left out.
310
+ const scanCapped = kept.length > scanCap;
311
+ const scanned = scanCapped ? kept.sort((x, y) => x.localeCompare(y)).slice(0, scanCap) : kept;
312
+ const entries: { name: string; isDir: boolean; size: number }[] = [];
313
+ for (const name of scanned) {
314
+ try {
315
+ const st = statSync(join(root, name));
316
+ entries.push({ name, isDir: st.isDirectory(), size: st.isDirectory() ? 0 : st.size });
317
+ } catch { /* unreadable entry → drop */ }
318
+ }
319
+ // gemini-cli ls.ts:253-257: directories first, then alphabetical
320
+ entries.sort((x, y) => {
321
+ if (x.isDir && !y.isDir) return -1;
322
+ if (!x.isDir && y.isDir) return 1;
323
+ return x.name.localeCompare(y.name);
324
+ });
325
+ const truncated = entries.length > limit;
326
+ const shown = truncated ? entries.slice(0, limit) : entries;
327
+ const body = shown.map((e) => (e.isDir ? `[DIR] ${e.name}` : `${e.name} (${e.size} bytes)`)).join("\n");
328
+ let output = `Directory listing for ${root}:\n${body}`;
329
+ if (truncated) output += `\n\n(Results truncated: showing first ${limit} of ${entries.length} entries.)`;
330
+ if (ignoredCount > 0) output += `\n\n(${ignoredCount} ignored)`;
331
+ if (scanCapped) output += `\n\n${scanCapLine(scanCap)}`;
332
+ return Promise.resolve({ ok: true, output, data: { entries: shown.length, truncated } });
333
+ },
334
+ };
335
+
336
+ return { globTool, grepTool, lsTool };
337
+ }
338
+
339
+ export const { globTool, grepTool, lsTool } = fileTools();