@stonerzju/opencode 1.2.16-offline.1

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 (496) hide show
  1. package/AGENTS.md +10 -0
  2. package/BUN_SHELL_MIGRATION_PLAN.md +136 -0
  3. package/Dockerfile +18 -0
  4. package/README.md +15 -0
  5. package/bin/opencode +179 -0
  6. package/bunfig.toml +7 -0
  7. package/drizzle.config.ts +10 -0
  8. package/migration/20260127222353_familiar_lady_ursula/migration.sql +90 -0
  9. package/migration/20260127222353_familiar_lady_ursula/snapshot.json +796 -0
  10. package/migration/20260211171708_add_project_commands/migration.sql +1 -0
  11. package/migration/20260211171708_add_project_commands/snapshot.json +806 -0
  12. package/migration/20260213144116_wakeful_the_professor/migration.sql +11 -0
  13. package/migration/20260213144116_wakeful_the_professor/snapshot.json +897 -0
  14. package/migration/20260225215848_workspace/migration.sql +7 -0
  15. package/migration/20260225215848_workspace/snapshot.json +959 -0
  16. package/package.json +140 -0
  17. package/package.json.bak +140 -0
  18. package/parsers-config.ts +254 -0
  19. package/script/build.ts +224 -0
  20. package/script/check-migrations.ts +16 -0
  21. package/script/postinstall.mjs +131 -0
  22. package/script/publish.ts +181 -0
  23. package/script/schema.ts +63 -0
  24. package/script/seed-e2e.ts +50 -0
  25. package/src/acp/README.md +174 -0
  26. package/src/acp/agent.ts +1741 -0
  27. package/src/acp/session.ts +116 -0
  28. package/src/acp/types.ts +23 -0
  29. package/src/agent/agent.ts +339 -0
  30. package/src/agent/generate.txt +75 -0
  31. package/src/agent/prompt/compaction.txt +14 -0
  32. package/src/agent/prompt/explore.txt +18 -0
  33. package/src/agent/prompt/summary.txt +11 -0
  34. package/src/agent/prompt/title.txt +44 -0
  35. package/src/auth/index.ts +68 -0
  36. package/src/bun/index.ts +131 -0
  37. package/src/bun/registry.ts +50 -0
  38. package/src/bus/bus-event.ts +43 -0
  39. package/src/bus/global.ts +10 -0
  40. package/src/bus/index.ts +105 -0
  41. package/src/cli/bootstrap.ts +17 -0
  42. package/src/cli/cmd/acp.ts +70 -0
  43. package/src/cli/cmd/agent.ts +257 -0
  44. package/src/cli/cmd/auth.ts +449 -0
  45. package/src/cli/cmd/cmd.ts +7 -0
  46. package/src/cli/cmd/db.ts +118 -0
  47. package/src/cli/cmd/debug/agent.ts +167 -0
  48. package/src/cli/cmd/debug/config.ts +16 -0
  49. package/src/cli/cmd/debug/file.ts +97 -0
  50. package/src/cli/cmd/debug/index.ts +48 -0
  51. package/src/cli/cmd/debug/lsp.ts +52 -0
  52. package/src/cli/cmd/debug/ripgrep.ts +87 -0
  53. package/src/cli/cmd/debug/scrap.ts +16 -0
  54. package/src/cli/cmd/debug/skill.ts +16 -0
  55. package/src/cli/cmd/debug/snapshot.ts +52 -0
  56. package/src/cli/cmd/export.ts +88 -0
  57. package/src/cli/cmd/generate.ts +38 -0
  58. package/src/cli/cmd/github.ts +1631 -0
  59. package/src/cli/cmd/import.ts +170 -0
  60. package/src/cli/cmd/mcp.ts +754 -0
  61. package/src/cli/cmd/models.ts +77 -0
  62. package/src/cli/cmd/pr.ts +112 -0
  63. package/src/cli/cmd/run.ts +625 -0
  64. package/src/cli/cmd/serve.ts +31 -0
  65. package/src/cli/cmd/session.ts +156 -0
  66. package/src/cli/cmd/stats.ts +410 -0
  67. package/src/cli/cmd/tui/app.tsx +845 -0
  68. package/src/cli/cmd/tui/attach.ts +88 -0
  69. package/src/cli/cmd/tui/component/border.tsx +21 -0
  70. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  71. package/src/cli/cmd/tui/component/dialog-command.tsx +147 -0
  72. package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
  73. package/src/cli/cmd/tui/component/dialog-model.tsx +165 -0
  74. package/src/cli/cmd/tui/component/dialog-provider.tsx +259 -0
  75. package/src/cli/cmd/tui/component/dialog-session-list.tsx +108 -0
  76. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  77. package/src/cli/cmd/tui/component/dialog-skill.tsx +36 -0
  78. package/src/cli/cmd/tui/component/dialog-stash.tsx +87 -0
  79. package/src/cli/cmd/tui/component/dialog-status.tsx +167 -0
  80. package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
  81. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
  82. package/src/cli/cmd/tui/component/logo.tsx +85 -0
  83. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +667 -0
  84. package/src/cli/cmd/tui/component/prompt/frecency.tsx +90 -0
  85. package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
  86. package/src/cli/cmd/tui/component/prompt/index.tsx +1155 -0
  87. package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
  88. package/src/cli/cmd/tui/component/spinner.tsx +24 -0
  89. package/src/cli/cmd/tui/component/textarea-keybindings.ts +73 -0
  90. package/src/cli/cmd/tui/component/tips.tsx +152 -0
  91. package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
  92. package/src/cli/cmd/tui/context/args.tsx +15 -0
  93. package/src/cli/cmd/tui/context/directory.ts +13 -0
  94. package/src/cli/cmd/tui/context/exit.tsx +53 -0
  95. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  96. package/src/cli/cmd/tui/context/keybind.tsx +102 -0
  97. package/src/cli/cmd/tui/context/kv.tsx +52 -0
  98. package/src/cli/cmd/tui/context/local.tsx +406 -0
  99. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  100. package/src/cli/cmd/tui/context/route.tsx +46 -0
  101. package/src/cli/cmd/tui/context/sdk.tsx +101 -0
  102. package/src/cli/cmd/tui/context/sync.tsx +488 -0
  103. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  104. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  105. package/src/cli/cmd/tui/context/theme/carbonfox.json +248 -0
  106. package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
  107. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
  108. package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
  109. package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
  110. package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
  111. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  112. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  113. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  114. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  115. package/src/cli/cmd/tui/context/theme/gruvbox.json +242 -0
  116. package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
  117. package/src/cli/cmd/tui/context/theme/lucent-orng.json +237 -0
  118. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  119. package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
  120. package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
  121. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  122. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  123. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  124. package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
  125. package/src/cli/cmd/tui/context/theme/orng.json +249 -0
  126. package/src/cli/cmd/tui/context/theme/osaka-jade.json +93 -0
  127. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  128. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  129. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  130. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  131. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  132. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  133. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  134. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  135. package/src/cli/cmd/tui/context/theme.tsx +1152 -0
  136. package/src/cli/cmd/tui/context/tui-config.tsx +9 -0
  137. package/src/cli/cmd/tui/event.ts +48 -0
  138. package/src/cli/cmd/tui/routes/home.tsx +145 -0
  139. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +64 -0
  140. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +109 -0
  141. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -0
  142. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
  143. package/src/cli/cmd/tui/routes/session/footer.tsx +91 -0
  144. package/src/cli/cmd/tui/routes/session/header.tsx +135 -0
  145. package/src/cli/cmd/tui/routes/session/index.tsx +2219 -0
  146. package/src/cli/cmd/tui/routes/session/permission.tsx +685 -0
  147. package/src/cli/cmd/tui/routes/session/question.tsx +466 -0
  148. package/src/cli/cmd/tui/routes/session/sidebar.tsx +321 -0
  149. package/src/cli/cmd/tui/thread.ts +199 -0
  150. package/src/cli/cmd/tui/ui/dialog-alert.tsx +59 -0
  151. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +85 -0
  152. package/src/cli/cmd/tui/ui/dialog-export-options.tsx +207 -0
  153. package/src/cli/cmd/tui/ui/dialog-help.tsx +40 -0
  154. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +80 -0
  155. package/src/cli/cmd/tui/ui/dialog-select.tsx +401 -0
  156. package/src/cli/cmd/tui/ui/dialog.tsx +182 -0
  157. package/src/cli/cmd/tui/ui/link.tsx +28 -0
  158. package/src/cli/cmd/tui/ui/spinner.ts +368 -0
  159. package/src/cli/cmd/tui/ui/toast.tsx +100 -0
  160. package/src/cli/cmd/tui/util/clipboard.ts +164 -0
  161. package/src/cli/cmd/tui/util/editor.ts +33 -0
  162. package/src/cli/cmd/tui/util/selection.ts +25 -0
  163. package/src/cli/cmd/tui/util/signal.ts +7 -0
  164. package/src/cli/cmd/tui/util/terminal.ts +114 -0
  165. package/src/cli/cmd/tui/util/transcript.ts +98 -0
  166. package/src/cli/cmd/tui/win32.ts +129 -0
  167. package/src/cli/cmd/tui/worker.ts +157 -0
  168. package/src/cli/cmd/uninstall.ts +356 -0
  169. package/src/cli/cmd/upgrade.ts +73 -0
  170. package/src/cli/cmd/web.ts +81 -0
  171. package/src/cli/cmd/workspace-serve.ts +16 -0
  172. package/src/cli/error.ts +57 -0
  173. package/src/cli/logo.ts +6 -0
  174. package/src/cli/network.ts +60 -0
  175. package/src/cli/ui.ts +116 -0
  176. package/src/cli/upgrade.ts +25 -0
  177. package/src/command/index.ts +150 -0
  178. package/src/command/template/initialize.txt +10 -0
  179. package/src/command/template/review.txt +101 -0
  180. package/src/config/config.ts +1408 -0
  181. package/src/config/markdown.ts +99 -0
  182. package/src/config/migrate-tui-config.ts +155 -0
  183. package/src/config/paths.ts +174 -0
  184. package/src/config/tui-schema.ts +34 -0
  185. package/src/config/tui.ts +118 -0
  186. package/src/control/control.sql.ts +22 -0
  187. package/src/control/index.ts +67 -0
  188. package/src/control-plane/adaptors/index.ts +10 -0
  189. package/src/control-plane/adaptors/types.ts +7 -0
  190. package/src/control-plane/adaptors/worktree.ts +26 -0
  191. package/src/control-plane/config.ts +10 -0
  192. package/src/control-plane/session-proxy-middleware.ts +46 -0
  193. package/src/control-plane/sse.ts +66 -0
  194. package/src/control-plane/workspace-server/routes.ts +33 -0
  195. package/src/control-plane/workspace-server/server.ts +24 -0
  196. package/src/control-plane/workspace.sql.ts +12 -0
  197. package/src/control-plane/workspace.ts +160 -0
  198. package/src/env/index.ts +28 -0
  199. package/src/file/ignore.ts +82 -0
  200. package/src/file/index.ts +646 -0
  201. package/src/file/ripgrep.ts +372 -0
  202. package/src/file/time.ts +71 -0
  203. package/src/file/watcher.ts +128 -0
  204. package/src/flag/flag.ts +109 -0
  205. package/src/format/formatter.ts +395 -0
  206. package/src/format/index.ts +140 -0
  207. package/src/global/index.ts +54 -0
  208. package/src/id/id.ts +84 -0
  209. package/src/ide/index.ts +76 -0
  210. package/src/index.ts +210 -0
  211. package/src/installation/index.ts +266 -0
  212. package/src/lsp/client.ts +251 -0
  213. package/src/lsp/index.ts +485 -0
  214. package/src/lsp/language.ts +120 -0
  215. package/src/lsp/server.ts +2142 -0
  216. package/src/mcp/auth.ts +130 -0
  217. package/src/mcp/index.ts +937 -0
  218. package/src/mcp/oauth-callback.ts +200 -0
  219. package/src/mcp/oauth-provider.ts +176 -0
  220. package/src/patch/index.ts +680 -0
  221. package/src/permission/arity.ts +163 -0
  222. package/src/permission/index.ts +210 -0
  223. package/src/permission/next.ts +286 -0
  224. package/src/plugin/codex.ts +624 -0
  225. package/src/plugin/copilot.ts +327 -0
  226. package/src/plugin/index.ts +143 -0
  227. package/src/project/bootstrap.ts +33 -0
  228. package/src/project/instance.ts +114 -0
  229. package/src/project/project.sql.ts +15 -0
  230. package/src/project/project.ts +441 -0
  231. package/src/project/state.ts +70 -0
  232. package/src/project/vcs.ts +76 -0
  233. package/src/provider/auth.ts +147 -0
  234. package/src/provider/error.ts +189 -0
  235. package/src/provider/models.ts +146 -0
  236. package/src/provider/provider.ts +1338 -0
  237. package/src/provider/sdk/copilot/README.md +5 -0
  238. package/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts +164 -0
  239. package/src/provider/sdk/copilot/chat/get-response-metadata.ts +15 -0
  240. package/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts +17 -0
  241. package/src/provider/sdk/copilot/chat/openai-compatible-api-types.ts +64 -0
  242. package/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts +780 -0
  243. package/src/provider/sdk/copilot/chat/openai-compatible-chat-options.ts +28 -0
  244. package/src/provider/sdk/copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
  245. package/src/provider/sdk/copilot/chat/openai-compatible-prepare-tools.ts +87 -0
  246. package/src/provider/sdk/copilot/copilot-provider.ts +100 -0
  247. package/src/provider/sdk/copilot/index.ts +2 -0
  248. package/src/provider/sdk/copilot/openai-compatible-error.ts +27 -0
  249. package/src/provider/sdk/copilot/responses/convert-to-openai-responses-input.ts +303 -0
  250. package/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts +22 -0
  251. package/src/provider/sdk/copilot/responses/openai-config.ts +18 -0
  252. package/src/provider/sdk/copilot/responses/openai-error.ts +22 -0
  253. package/src/provider/sdk/copilot/responses/openai-responses-api-types.ts +207 -0
  254. package/src/provider/sdk/copilot/responses/openai-responses-language-model.ts +1732 -0
  255. package/src/provider/sdk/copilot/responses/openai-responses-prepare-tools.ts +177 -0
  256. package/src/provider/sdk/copilot/responses/openai-responses-settings.ts +1 -0
  257. package/src/provider/sdk/copilot/responses/tool/code-interpreter.ts +88 -0
  258. package/src/provider/sdk/copilot/responses/tool/file-search.ts +128 -0
  259. package/src/provider/sdk/copilot/responses/tool/image-generation.ts +115 -0
  260. package/src/provider/sdk/copilot/responses/tool/local-shell.ts +65 -0
  261. package/src/provider/sdk/copilot/responses/tool/web-search-preview.ts +104 -0
  262. package/src/provider/sdk/copilot/responses/tool/web-search.ts +103 -0
  263. package/src/provider/transform.ts +955 -0
  264. package/src/pty/index.ts +324 -0
  265. package/src/question/index.ts +171 -0
  266. package/src/scheduler/index.ts +61 -0
  267. package/src/server/error.ts +36 -0
  268. package/src/server/event.ts +7 -0
  269. package/src/server/mdns.ts +60 -0
  270. package/src/server/routes/config.ts +92 -0
  271. package/src/server/routes/experimental.ts +270 -0
  272. package/src/server/routes/file.ts +197 -0
  273. package/src/server/routes/global.ts +185 -0
  274. package/src/server/routes/mcp.ts +225 -0
  275. package/src/server/routes/permission.ts +68 -0
  276. package/src/server/routes/project.ts +82 -0
  277. package/src/server/routes/provider.ts +165 -0
  278. package/src/server/routes/pty.ts +200 -0
  279. package/src/server/routes/question.ts +98 -0
  280. package/src/server/routes/session.ts +974 -0
  281. package/src/server/routes/tui.ts +379 -0
  282. package/src/server/routes/workspace.ts +104 -0
  283. package/src/server/server.ts +623 -0
  284. package/src/session/compaction.ts +261 -0
  285. package/src/session/index.ts +877 -0
  286. package/src/session/instruction.ts +192 -0
  287. package/src/session/llm.ts +279 -0
  288. package/src/session/message-v2.ts +899 -0
  289. package/src/session/message.ts +189 -0
  290. package/src/session/processor.ts +421 -0
  291. package/src/session/prompt/anthropic-20250930.txt +166 -0
  292. package/src/session/prompt/anthropic.txt +105 -0
  293. package/src/session/prompt/beast.txt +147 -0
  294. package/src/session/prompt/build-switch.txt +5 -0
  295. package/src/session/prompt/codex_header.txt +79 -0
  296. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  297. package/src/session/prompt/gemini.txt +155 -0
  298. package/src/session/prompt/max-steps.txt +16 -0
  299. package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
  300. package/src/session/prompt/plan.txt +26 -0
  301. package/src/session/prompt/qwen.txt +109 -0
  302. package/src/session/prompt/trinity.txt +97 -0
  303. package/src/session/prompt.ts +1959 -0
  304. package/src/session/retry.ts +101 -0
  305. package/src/session/revert.ts +138 -0
  306. package/src/session/session.sql.ts +88 -0
  307. package/src/session/status.ts +76 -0
  308. package/src/session/summary.ts +161 -0
  309. package/src/session/system.ts +54 -0
  310. package/src/session/todo.ts +56 -0
  311. package/src/share/share-next.ts +210 -0
  312. package/src/share/share.sql.ts +13 -0
  313. package/src/shell/shell.ts +68 -0
  314. package/src/skill/discovery.ts +98 -0
  315. package/src/skill/index.ts +1 -0
  316. package/src/skill/skill.ts +189 -0
  317. package/src/snapshot/index.ts +297 -0
  318. package/src/sql.d.ts +4 -0
  319. package/src/storage/db.ts +155 -0
  320. package/src/storage/json-migration.ts +425 -0
  321. package/src/storage/schema.sql.ts +10 -0
  322. package/src/storage/schema.ts +5 -0
  323. package/src/storage/storage.ts +220 -0
  324. package/src/tool/apply_patch.ts +281 -0
  325. package/src/tool/apply_patch.txt +33 -0
  326. package/src/tool/bash.ts +274 -0
  327. package/src/tool/bash.txt +115 -0
  328. package/src/tool/batch.ts +181 -0
  329. package/src/tool/batch.txt +24 -0
  330. package/src/tool/codesearch.ts +132 -0
  331. package/src/tool/codesearch.txt +12 -0
  332. package/src/tool/edit.ts +654 -0
  333. package/src/tool/edit.txt +10 -0
  334. package/src/tool/external-directory.ts +32 -0
  335. package/src/tool/glob.ts +78 -0
  336. package/src/tool/glob.txt +6 -0
  337. package/src/tool/grep.ts +156 -0
  338. package/src/tool/grep.txt +8 -0
  339. package/src/tool/invalid.ts +17 -0
  340. package/src/tool/ls.ts +121 -0
  341. package/src/tool/ls.txt +1 -0
  342. package/src/tool/lsp.ts +97 -0
  343. package/src/tool/lsp.txt +19 -0
  344. package/src/tool/multiedit.ts +46 -0
  345. package/src/tool/multiedit.txt +41 -0
  346. package/src/tool/plan-enter.txt +14 -0
  347. package/src/tool/plan-exit.txt +13 -0
  348. package/src/tool/plan.ts +131 -0
  349. package/src/tool/question.ts +33 -0
  350. package/src/tool/question.txt +10 -0
  351. package/src/tool/read.ts +293 -0
  352. package/src/tool/read.txt +14 -0
  353. package/src/tool/registry.ts +173 -0
  354. package/src/tool/skill.ts +123 -0
  355. package/src/tool/task.ts +165 -0
  356. package/src/tool/task.txt +60 -0
  357. package/src/tool/todo.ts +53 -0
  358. package/src/tool/todoread.txt +14 -0
  359. package/src/tool/todowrite.txt +167 -0
  360. package/src/tool/tool.ts +89 -0
  361. package/src/tool/truncation.ts +107 -0
  362. package/src/tool/webfetch.ts +206 -0
  363. package/src/tool/webfetch.txt +13 -0
  364. package/src/tool/websearch.ts +150 -0
  365. package/src/tool/websearch.txt +14 -0
  366. package/src/tool/write.ts +84 -0
  367. package/src/tool/write.txt +8 -0
  368. package/src/util/abort.ts +35 -0
  369. package/src/util/archive.ts +16 -0
  370. package/src/util/color.ts +19 -0
  371. package/src/util/context.ts +25 -0
  372. package/src/util/defer.ts +12 -0
  373. package/src/util/eventloop.ts +20 -0
  374. package/src/util/filesystem.ts +189 -0
  375. package/src/util/fn.ts +11 -0
  376. package/src/util/format.ts +20 -0
  377. package/src/util/git.ts +35 -0
  378. package/src/util/glob.ts +34 -0
  379. package/src/util/iife.ts +3 -0
  380. package/src/util/keybind.ts +103 -0
  381. package/src/util/lazy.ts +23 -0
  382. package/src/util/locale.ts +81 -0
  383. package/src/util/lock.ts +98 -0
  384. package/src/util/log.ts +182 -0
  385. package/src/util/process.ts +126 -0
  386. package/src/util/proxied.ts +3 -0
  387. package/src/util/queue.ts +32 -0
  388. package/src/util/rpc.ts +66 -0
  389. package/src/util/scrap.ts +10 -0
  390. package/src/util/signal.ts +12 -0
  391. package/src/util/timeout.ts +14 -0
  392. package/src/util/token.ts +7 -0
  393. package/src/util/wildcard.ts +59 -0
  394. package/src/worktree/index.ts +643 -0
  395. package/sst-env.d.ts +10 -0
  396. package/test/AGENTS.md +81 -0
  397. package/test/acp/agent-interface.test.ts +51 -0
  398. package/test/acp/event-subscription.test.ts +683 -0
  399. package/test/agent/agent.test.ts +689 -0
  400. package/test/bun.test.ts +53 -0
  401. package/test/cli/github-action.test.ts +197 -0
  402. package/test/cli/github-remote.test.ts +80 -0
  403. package/test/cli/import.test.ts +38 -0
  404. package/test/cli/plugin-auth-picker.test.ts +120 -0
  405. package/test/cli/tui/transcript.test.ts +322 -0
  406. package/test/config/agent-color.test.ts +71 -0
  407. package/test/config/config.test.ts +1886 -0
  408. package/test/config/fixtures/empty-frontmatter.md +4 -0
  409. package/test/config/fixtures/frontmatter.md +28 -0
  410. package/test/config/fixtures/markdown-header.md +11 -0
  411. package/test/config/fixtures/no-frontmatter.md +1 -0
  412. package/test/config/fixtures/weird-model-id.md +13 -0
  413. package/test/config/markdown.test.ts +228 -0
  414. package/test/config/tui.test.ts +510 -0
  415. package/test/control-plane/session-proxy-middleware.test.ts +147 -0
  416. package/test/control-plane/sse.test.ts +56 -0
  417. package/test/control-plane/workspace-server-sse.test.ts +65 -0
  418. package/test/control-plane/workspace-sync.test.ts +97 -0
  419. package/test/file/ignore.test.ts +10 -0
  420. package/test/file/index.test.ts +394 -0
  421. package/test/file/path-traversal.test.ts +198 -0
  422. package/test/file/ripgrep.test.ts +39 -0
  423. package/test/file/time.test.ts +361 -0
  424. package/test/fixture/db.ts +11 -0
  425. package/test/fixture/fixture.ts +45 -0
  426. package/test/fixture/lsp/fake-lsp-server.js +77 -0
  427. package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
  428. package/test/fixture/skills/agents-sdk/references/callable.md +92 -0
  429. package/test/fixture/skills/cloudflare/SKILL.md +211 -0
  430. package/test/fixture/skills/index.json +6 -0
  431. package/test/ide/ide.test.ts +82 -0
  432. package/test/keybind.test.ts +421 -0
  433. package/test/lsp/client.test.ts +95 -0
  434. package/test/mcp/headers.test.ts +153 -0
  435. package/test/mcp/oauth-browser.test.ts +249 -0
  436. package/test/memory/abort-leak.test.ts +136 -0
  437. package/test/patch/patch.test.ts +348 -0
  438. package/test/permission/arity.test.ts +33 -0
  439. package/test/permission/next.test.ts +689 -0
  440. package/test/permission-task.test.ts +319 -0
  441. package/test/plugin/auth-override.test.ts +44 -0
  442. package/test/plugin/codex.test.ts +123 -0
  443. package/test/preload.ts +80 -0
  444. package/test/project/project.test.ts +348 -0
  445. package/test/project/worktree-remove.test.ts +65 -0
  446. package/test/provider/amazon-bedrock.test.ts +446 -0
  447. package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
  448. package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
  449. package/test/provider/gitlab-duo.test.ts +262 -0
  450. package/test/provider/provider.test.ts +2220 -0
  451. package/test/provider/transform.test.ts +2353 -0
  452. package/test/pty/pty-output-isolation.test.ts +140 -0
  453. package/test/question/question.test.ts +300 -0
  454. package/test/scheduler.test.ts +73 -0
  455. package/test/server/global-session-list.test.ts +89 -0
  456. package/test/server/session-list.test.ts +90 -0
  457. package/test/server/session-select.test.ts +78 -0
  458. package/test/session/compaction.test.ts +423 -0
  459. package/test/session/instruction.test.ts +170 -0
  460. package/test/session/llm.test.ts +667 -0
  461. package/test/session/message-v2.test.ts +924 -0
  462. package/test/session/prompt.test.ts +211 -0
  463. package/test/session/retry.test.ts +188 -0
  464. package/test/session/revert-compact.test.ts +285 -0
  465. package/test/session/session.test.ts +71 -0
  466. package/test/session/structured-output-integration.test.ts +233 -0
  467. package/test/session/structured-output.test.ts +385 -0
  468. package/test/skill/discovery.test.ts +110 -0
  469. package/test/skill/skill.test.ts +388 -0
  470. package/test/snapshot/snapshot.test.ts +1180 -0
  471. package/test/storage/json-migration.test.ts +846 -0
  472. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  473. package/test/tool/apply_patch.test.ts +566 -0
  474. package/test/tool/bash.test.ts +402 -0
  475. package/test/tool/edit.test.ts +496 -0
  476. package/test/tool/external-directory.test.ts +127 -0
  477. package/test/tool/fixtures/large-image.png +0 -0
  478. package/test/tool/fixtures/models-api.json +38413 -0
  479. package/test/tool/grep.test.ts +110 -0
  480. package/test/tool/question.test.ts +107 -0
  481. package/test/tool/read.test.ts +504 -0
  482. package/test/tool/registry.test.ts +122 -0
  483. package/test/tool/skill.test.ts +112 -0
  484. package/test/tool/truncation.test.ts +160 -0
  485. package/test/tool/webfetch.test.ts +100 -0
  486. package/test/tool/write.test.ts +348 -0
  487. package/test/util/filesystem.test.ts +443 -0
  488. package/test/util/format.test.ts +59 -0
  489. package/test/util/glob.test.ts +164 -0
  490. package/test/util/iife.test.ts +36 -0
  491. package/test/util/lazy.test.ts +50 -0
  492. package/test/util/lock.test.ts +72 -0
  493. package/test/util/process.test.ts +59 -0
  494. package/test/util/timeout.test.ts +21 -0
  495. package/test/util/wildcard.test.ts +90 -0
  496. package/tsconfig.json +16 -0
@@ -0,0 +1,496 @@
1
+ import { describe, test, expect } from "bun:test"
2
+ import path from "path"
3
+ import fs from "fs/promises"
4
+ import { EditTool } from "../../src/tool/edit"
5
+ import { Instance } from "../../src/project/instance"
6
+ import { tmpdir } from "../fixture/fixture"
7
+ import { FileTime } from "../../src/file/time"
8
+
9
+ const ctx = {
10
+ sessionID: "test-edit-session",
11
+ messageID: "",
12
+ callID: "",
13
+ agent: "build",
14
+ abort: AbortSignal.any([]),
15
+ messages: [],
16
+ metadata: () => {},
17
+ ask: async () => {},
18
+ }
19
+
20
+ describe("tool.edit", () => {
21
+ describe("creating new files", () => {
22
+ test("creates new file when oldString is empty", async () => {
23
+ await using tmp = await tmpdir()
24
+ const filepath = path.join(tmp.path, "newfile.txt")
25
+
26
+ await Instance.provide({
27
+ directory: tmp.path,
28
+ fn: async () => {
29
+ const edit = await EditTool.init()
30
+ const result = await edit.execute(
31
+ {
32
+ filePath: filepath,
33
+ oldString: "",
34
+ newString: "new content",
35
+ },
36
+ ctx,
37
+ )
38
+
39
+ expect(result.metadata.diff).toContain("new content")
40
+
41
+ const content = await fs.readFile(filepath, "utf-8")
42
+ expect(content).toBe("new content")
43
+ },
44
+ })
45
+ })
46
+
47
+ test("creates new file with nested directories", async () => {
48
+ await using tmp = await tmpdir()
49
+ const filepath = path.join(tmp.path, "nested", "dir", "file.txt")
50
+
51
+ await Instance.provide({
52
+ directory: tmp.path,
53
+ fn: async () => {
54
+ const edit = await EditTool.init()
55
+ await edit.execute(
56
+ {
57
+ filePath: filepath,
58
+ oldString: "",
59
+ newString: "nested file",
60
+ },
61
+ ctx,
62
+ )
63
+
64
+ const content = await fs.readFile(filepath, "utf-8")
65
+ expect(content).toBe("nested file")
66
+ },
67
+ })
68
+ })
69
+
70
+ test("emits add event for new files", async () => {
71
+ await using tmp = await tmpdir()
72
+ const filepath = path.join(tmp.path, "new.txt")
73
+
74
+ await Instance.provide({
75
+ directory: tmp.path,
76
+ fn: async () => {
77
+ const { Bus } = await import("../../src/bus")
78
+ const { File } = await import("../../src/file")
79
+ const { FileWatcher } = await import("../../src/file/watcher")
80
+
81
+ const events: string[] = []
82
+ const unsubEdited = Bus.subscribe(File.Event.Edited, () => events.push("edited"))
83
+ const unsubUpdated = Bus.subscribe(FileWatcher.Event.Updated, () => events.push("updated"))
84
+
85
+ const edit = await EditTool.init()
86
+ await edit.execute(
87
+ {
88
+ filePath: filepath,
89
+ oldString: "",
90
+ newString: "content",
91
+ },
92
+ ctx,
93
+ )
94
+
95
+ expect(events).toContain("edited")
96
+ expect(events).toContain("updated")
97
+ unsubEdited()
98
+ unsubUpdated()
99
+ },
100
+ })
101
+ })
102
+ })
103
+
104
+ describe("editing existing files", () => {
105
+ test("replaces text in existing file", async () => {
106
+ await using tmp = await tmpdir()
107
+ const filepath = path.join(tmp.path, "existing.txt")
108
+ await fs.writeFile(filepath, "old content here", "utf-8")
109
+
110
+ await Instance.provide({
111
+ directory: tmp.path,
112
+ fn: async () => {
113
+ FileTime.read(ctx.sessionID, filepath)
114
+
115
+ const edit = await EditTool.init()
116
+ const result = await edit.execute(
117
+ {
118
+ filePath: filepath,
119
+ oldString: "old content",
120
+ newString: "new content",
121
+ },
122
+ ctx,
123
+ )
124
+
125
+ expect(result.output).toContain("Edit applied successfully")
126
+
127
+ const content = await fs.readFile(filepath, "utf-8")
128
+ expect(content).toBe("new content here")
129
+ },
130
+ })
131
+ })
132
+
133
+ test("throws error when file does not exist", async () => {
134
+ await using tmp = await tmpdir()
135
+ const filepath = path.join(tmp.path, "nonexistent.txt")
136
+
137
+ await Instance.provide({
138
+ directory: tmp.path,
139
+ fn: async () => {
140
+ FileTime.read(ctx.sessionID, filepath)
141
+
142
+ const edit = await EditTool.init()
143
+ await expect(
144
+ edit.execute(
145
+ {
146
+ filePath: filepath,
147
+ oldString: "old",
148
+ newString: "new",
149
+ },
150
+ ctx,
151
+ ),
152
+ ).rejects.toThrow("not found")
153
+ },
154
+ })
155
+ })
156
+
157
+ test("throws error when oldString equals newString", async () => {
158
+ await using tmp = await tmpdir()
159
+ const filepath = path.join(tmp.path, "file.txt")
160
+ await fs.writeFile(filepath, "content", "utf-8")
161
+
162
+ await Instance.provide({
163
+ directory: tmp.path,
164
+ fn: async () => {
165
+ const edit = await EditTool.init()
166
+ await expect(
167
+ edit.execute(
168
+ {
169
+ filePath: filepath,
170
+ oldString: "same",
171
+ newString: "same",
172
+ },
173
+ ctx,
174
+ ),
175
+ ).rejects.toThrow("identical")
176
+ },
177
+ })
178
+ })
179
+
180
+ test("throws error when oldString not found in file", async () => {
181
+ await using tmp = await tmpdir()
182
+ const filepath = path.join(tmp.path, "file.txt")
183
+ await fs.writeFile(filepath, "actual content", "utf-8")
184
+
185
+ await Instance.provide({
186
+ directory: tmp.path,
187
+ fn: async () => {
188
+ FileTime.read(ctx.sessionID, filepath)
189
+
190
+ const edit = await EditTool.init()
191
+ await expect(
192
+ edit.execute(
193
+ {
194
+ filePath: filepath,
195
+ oldString: "not in file",
196
+ newString: "replacement",
197
+ },
198
+ ctx,
199
+ ),
200
+ ).rejects.toThrow()
201
+ },
202
+ })
203
+ })
204
+
205
+ test("throws error when file was not read first (FileTime)", async () => {
206
+ await using tmp = await tmpdir()
207
+ const filepath = path.join(tmp.path, "file.txt")
208
+ await fs.writeFile(filepath, "content", "utf-8")
209
+
210
+ await Instance.provide({
211
+ directory: tmp.path,
212
+ fn: async () => {
213
+ const edit = await EditTool.init()
214
+ await expect(
215
+ edit.execute(
216
+ {
217
+ filePath: filepath,
218
+ oldString: "content",
219
+ newString: "modified",
220
+ },
221
+ ctx,
222
+ ),
223
+ ).rejects.toThrow("You must read file")
224
+ },
225
+ })
226
+ })
227
+
228
+ test("throws error when file has been modified since read", async () => {
229
+ await using tmp = await tmpdir()
230
+ const filepath = path.join(tmp.path, "file.txt")
231
+ await fs.writeFile(filepath, "original content", "utf-8")
232
+
233
+ await Instance.provide({
234
+ directory: tmp.path,
235
+ fn: async () => {
236
+ // Read first
237
+ FileTime.read(ctx.sessionID, filepath)
238
+
239
+ // Wait a bit to ensure different timestamps
240
+ await new Promise((resolve) => setTimeout(resolve, 100))
241
+
242
+ // Simulate external modification
243
+ await fs.writeFile(filepath, "modified externally", "utf-8")
244
+
245
+ // Try to edit with the new content
246
+ const edit = await EditTool.init()
247
+ await expect(
248
+ edit.execute(
249
+ {
250
+ filePath: filepath,
251
+ oldString: "modified externally",
252
+ newString: "edited",
253
+ },
254
+ ctx,
255
+ ),
256
+ ).rejects.toThrow("modified since it was last read")
257
+ },
258
+ })
259
+ })
260
+
261
+ test("replaces all occurrences with replaceAll option", async () => {
262
+ await using tmp = await tmpdir()
263
+ const filepath = path.join(tmp.path, "file.txt")
264
+ await fs.writeFile(filepath, "foo bar foo baz foo", "utf-8")
265
+
266
+ await Instance.provide({
267
+ directory: tmp.path,
268
+ fn: async () => {
269
+ FileTime.read(ctx.sessionID, filepath)
270
+
271
+ const edit = await EditTool.init()
272
+ await edit.execute(
273
+ {
274
+ filePath: filepath,
275
+ oldString: "foo",
276
+ newString: "qux",
277
+ replaceAll: true,
278
+ },
279
+ ctx,
280
+ )
281
+
282
+ const content = await fs.readFile(filepath, "utf-8")
283
+ expect(content).toBe("qux bar qux baz qux")
284
+ },
285
+ })
286
+ })
287
+
288
+ test("emits change event for existing files", async () => {
289
+ await using tmp = await tmpdir()
290
+ const filepath = path.join(tmp.path, "file.txt")
291
+ await fs.writeFile(filepath, "original", "utf-8")
292
+
293
+ await Instance.provide({
294
+ directory: tmp.path,
295
+ fn: async () => {
296
+ FileTime.read(ctx.sessionID, filepath)
297
+
298
+ const { Bus } = await import("../../src/bus")
299
+ const { File } = await import("../../src/file")
300
+ const { FileWatcher } = await import("../../src/file/watcher")
301
+
302
+ const events: string[] = []
303
+ const unsubEdited = Bus.subscribe(File.Event.Edited, () => events.push("edited"))
304
+ const unsubUpdated = Bus.subscribe(FileWatcher.Event.Updated, () => events.push("updated"))
305
+
306
+ const edit = await EditTool.init()
307
+ await edit.execute(
308
+ {
309
+ filePath: filepath,
310
+ oldString: "original",
311
+ newString: "modified",
312
+ },
313
+ ctx,
314
+ )
315
+
316
+ expect(events).toContain("edited")
317
+ expect(events).toContain("updated")
318
+ unsubEdited()
319
+ unsubUpdated()
320
+ },
321
+ })
322
+ })
323
+ })
324
+
325
+ describe("edge cases", () => {
326
+ test("handles multiline replacements", async () => {
327
+ await using tmp = await tmpdir()
328
+ const filepath = path.join(tmp.path, "file.txt")
329
+ await fs.writeFile(filepath, "line1\nline2\nline3", "utf-8")
330
+
331
+ await Instance.provide({
332
+ directory: tmp.path,
333
+ fn: async () => {
334
+ FileTime.read(ctx.sessionID, filepath)
335
+
336
+ const edit = await EditTool.init()
337
+ await edit.execute(
338
+ {
339
+ filePath: filepath,
340
+ oldString: "line2",
341
+ newString: "new line 2\nextra line",
342
+ },
343
+ ctx,
344
+ )
345
+
346
+ const content = await fs.readFile(filepath, "utf-8")
347
+ expect(content).toBe("line1\nnew line 2\nextra line\nline3")
348
+ },
349
+ })
350
+ })
351
+
352
+ test("handles CRLF line endings", async () => {
353
+ await using tmp = await tmpdir()
354
+ const filepath = path.join(tmp.path, "file.txt")
355
+ await fs.writeFile(filepath, "line1\r\nold\r\nline3", "utf-8")
356
+
357
+ await Instance.provide({
358
+ directory: tmp.path,
359
+ fn: async () => {
360
+ FileTime.read(ctx.sessionID, filepath)
361
+
362
+ const edit = await EditTool.init()
363
+ await edit.execute(
364
+ {
365
+ filePath: filepath,
366
+ oldString: "old",
367
+ newString: "new",
368
+ },
369
+ ctx,
370
+ )
371
+
372
+ const content = await fs.readFile(filepath, "utf-8")
373
+ expect(content).toBe("line1\r\nnew\r\nline3")
374
+ },
375
+ })
376
+ })
377
+
378
+ test("throws error when oldString equals newString", async () => {
379
+ await using tmp = await tmpdir()
380
+ const filepath = path.join(tmp.path, "file.txt")
381
+ await fs.writeFile(filepath, "content", "utf-8")
382
+
383
+ await Instance.provide({
384
+ directory: tmp.path,
385
+ fn: async () => {
386
+ const edit = await EditTool.init()
387
+ await expect(
388
+ edit.execute(
389
+ {
390
+ filePath: filepath,
391
+ oldString: "",
392
+ newString: "",
393
+ },
394
+ ctx,
395
+ ),
396
+ ).rejects.toThrow("identical")
397
+ },
398
+ })
399
+ })
400
+
401
+ test("throws error when path is directory", async () => {
402
+ await using tmp = await tmpdir()
403
+ const dirpath = path.join(tmp.path, "adir")
404
+ await fs.mkdir(dirpath)
405
+
406
+ await Instance.provide({
407
+ directory: tmp.path,
408
+ fn: async () => {
409
+ FileTime.read(ctx.sessionID, dirpath)
410
+
411
+ const edit = await EditTool.init()
412
+ await expect(
413
+ edit.execute(
414
+ {
415
+ filePath: dirpath,
416
+ oldString: "old",
417
+ newString: "new",
418
+ },
419
+ ctx,
420
+ ),
421
+ ).rejects.toThrow("directory")
422
+ },
423
+ })
424
+ })
425
+
426
+ test("tracks file diff statistics", async () => {
427
+ await using tmp = await tmpdir()
428
+ const filepath = path.join(tmp.path, "file.txt")
429
+ await fs.writeFile(filepath, "line1\nline2\nline3", "utf-8")
430
+
431
+ await Instance.provide({
432
+ directory: tmp.path,
433
+ fn: async () => {
434
+ FileTime.read(ctx.sessionID, filepath)
435
+
436
+ const edit = await EditTool.init()
437
+ const result = await edit.execute(
438
+ {
439
+ filePath: filepath,
440
+ oldString: "line2",
441
+ newString: "new line a\nnew line b",
442
+ },
443
+ ctx,
444
+ )
445
+
446
+ expect(result.metadata.filediff).toBeDefined()
447
+ expect(result.metadata.filediff.file).toBe(filepath)
448
+ expect(result.metadata.filediff.additions).toBeGreaterThan(0)
449
+ },
450
+ })
451
+ })
452
+ })
453
+
454
+ describe("concurrent editing", () => {
455
+ test("serializes concurrent edits to same file", async () => {
456
+ await using tmp = await tmpdir()
457
+ const filepath = path.join(tmp.path, "file.txt")
458
+ await fs.writeFile(filepath, "0", "utf-8")
459
+
460
+ await Instance.provide({
461
+ directory: tmp.path,
462
+ fn: async () => {
463
+ FileTime.read(ctx.sessionID, filepath)
464
+
465
+ const edit = await EditTool.init()
466
+
467
+ // Two concurrent edits
468
+ const promise1 = edit.execute(
469
+ {
470
+ filePath: filepath,
471
+ oldString: "0",
472
+ newString: "1",
473
+ },
474
+ ctx,
475
+ )
476
+
477
+ // Need to read again since FileTime tracks per-session
478
+ FileTime.read(ctx.sessionID, filepath)
479
+
480
+ const promise2 = edit.execute(
481
+ {
482
+ filePath: filepath,
483
+ oldString: "0",
484
+ newString: "2",
485
+ },
486
+ ctx,
487
+ )
488
+
489
+ // Both should complete without error (though one might fail due to content mismatch)
490
+ const results = await Promise.allSettled([promise1, promise2])
491
+ expect(results.some((r) => r.status === "fulfilled")).toBe(true)
492
+ },
493
+ })
494
+ })
495
+ })
496
+ })
@@ -0,0 +1,127 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import path from "path"
3
+ import type { Tool } from "../../src/tool/tool"
4
+ import { Instance } from "../../src/project/instance"
5
+ import { assertExternalDirectory } from "../../src/tool/external-directory"
6
+ import type { PermissionNext } from "../../src/permission/next"
7
+
8
+ const baseCtx: Omit<Tool.Context, "ask"> = {
9
+ sessionID: "test",
10
+ messageID: "",
11
+ callID: "",
12
+ agent: "build",
13
+ abort: AbortSignal.any([]),
14
+ messages: [],
15
+ metadata: () => {},
16
+ }
17
+
18
+ describe("tool.assertExternalDirectory", () => {
19
+ test("no-ops for empty target", async () => {
20
+ const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
21
+ const ctx: Tool.Context = {
22
+ ...baseCtx,
23
+ ask: async (req) => {
24
+ requests.push(req)
25
+ },
26
+ }
27
+
28
+ await Instance.provide({
29
+ directory: "/tmp",
30
+ fn: async () => {
31
+ await assertExternalDirectory(ctx)
32
+ },
33
+ })
34
+
35
+ expect(requests.length).toBe(0)
36
+ })
37
+
38
+ test("no-ops for paths inside Instance.directory", async () => {
39
+ const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
40
+ const ctx: Tool.Context = {
41
+ ...baseCtx,
42
+ ask: async (req) => {
43
+ requests.push(req)
44
+ },
45
+ }
46
+
47
+ await Instance.provide({
48
+ directory: "/tmp/project",
49
+ fn: async () => {
50
+ await assertExternalDirectory(ctx, path.join("/tmp/project", "file.txt"))
51
+ },
52
+ })
53
+
54
+ expect(requests.length).toBe(0)
55
+ })
56
+
57
+ test("asks with a single canonical glob", async () => {
58
+ const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
59
+ const ctx: Tool.Context = {
60
+ ...baseCtx,
61
+ ask: async (req) => {
62
+ requests.push(req)
63
+ },
64
+ }
65
+
66
+ const directory = "/tmp/project"
67
+ const target = "/tmp/outside/file.txt"
68
+ const expected = path.join(path.dirname(target), "*").replaceAll("\\", "/")
69
+
70
+ await Instance.provide({
71
+ directory,
72
+ fn: async () => {
73
+ await assertExternalDirectory(ctx, target)
74
+ },
75
+ })
76
+
77
+ const req = requests.find((r) => r.permission === "external_directory")
78
+ expect(req).toBeDefined()
79
+ expect(req!.patterns).toEqual([expected])
80
+ expect(req!.always).toEqual([expected])
81
+ })
82
+
83
+ test("uses target directory when kind=directory", async () => {
84
+ const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
85
+ const ctx: Tool.Context = {
86
+ ...baseCtx,
87
+ ask: async (req) => {
88
+ requests.push(req)
89
+ },
90
+ }
91
+
92
+ const directory = "/tmp/project"
93
+ const target = "/tmp/outside"
94
+ const expected = path.join(target, "*").replaceAll("\\", "/")
95
+
96
+ await Instance.provide({
97
+ directory,
98
+ fn: async () => {
99
+ await assertExternalDirectory(ctx, target, { kind: "directory" })
100
+ },
101
+ })
102
+
103
+ const req = requests.find((r) => r.permission === "external_directory")
104
+ expect(req).toBeDefined()
105
+ expect(req!.patterns).toEqual([expected])
106
+ expect(req!.always).toEqual([expected])
107
+ })
108
+
109
+ test("skips prompting when bypass=true", async () => {
110
+ const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
111
+ const ctx: Tool.Context = {
112
+ ...baseCtx,
113
+ ask: async (req) => {
114
+ requests.push(req)
115
+ },
116
+ }
117
+
118
+ await Instance.provide({
119
+ directory: "/tmp/project",
120
+ fn: async () => {
121
+ await assertExternalDirectory(ctx, "/tmp/outside/file.txt", { bypass: true })
122
+ },
123
+ })
124
+
125
+ expect(requests.length).toBe(0)
126
+ })
127
+ })