jonsoc 1.1.43

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 (420) hide show
  1. package/AGENTS.md +27 -0
  2. package/Dockerfile +18 -0
  3. package/PUBLISHING_GUIDE.md +151 -0
  4. package/README.md +58 -0
  5. package/bin/jonsoc +279 -0
  6. package/bunfig.toml +7 -0
  7. package/package.json +147 -0
  8. package/package.json.placeholder +11 -0
  9. package/parsers-config.ts +253 -0
  10. package/script/build.ts +115 -0
  11. package/script/publish-registries.ts +197 -0
  12. package/script/publish.ts +110 -0
  13. package/script/schema.ts +47 -0
  14. package/script/seed-e2e.ts +50 -0
  15. package/src/acp/README.md +164 -0
  16. package/src/acp/agent.ts +1437 -0
  17. package/src/acp/session.ts +105 -0
  18. package/src/acp/types.ts +22 -0
  19. package/src/agent/agent.ts +347 -0
  20. package/src/agent/generate.txt +75 -0
  21. package/src/agent/prompt/compaction.txt +12 -0
  22. package/src/agent/prompt/explore.txt +18 -0
  23. package/src/agent/prompt/summary.txt +11 -0
  24. package/src/agent/prompt/title.txt +44 -0
  25. package/src/auth/index.ts +73 -0
  26. package/src/brand/index.ts +73 -0
  27. package/src/bun/index.ts +139 -0
  28. package/src/bus/bus-event.ts +43 -0
  29. package/src/bus/global.ts +10 -0
  30. package/src/bus/index.ts +105 -0
  31. package/src/cli/bootstrap.ts +17 -0
  32. package/src/cli/cmd/acp.ts +69 -0
  33. package/src/cli/cmd/agent.ts +257 -0
  34. package/src/cli/cmd/auth.ts +405 -0
  35. package/src/cli/cmd/cmd.ts +7 -0
  36. package/src/cli/cmd/debug/agent.ts +166 -0
  37. package/src/cli/cmd/debug/config.ts +16 -0
  38. package/src/cli/cmd/debug/file.ts +97 -0
  39. package/src/cli/cmd/debug/index.ts +48 -0
  40. package/src/cli/cmd/debug/lsp.ts +52 -0
  41. package/src/cli/cmd/debug/ripgrep.ts +87 -0
  42. package/src/cli/cmd/debug/scrap.ts +16 -0
  43. package/src/cli/cmd/debug/skill.ts +16 -0
  44. package/src/cli/cmd/debug/snapshot.ts +52 -0
  45. package/src/cli/cmd/export.ts +88 -0
  46. package/src/cli/cmd/generate.ts +38 -0
  47. package/src/cli/cmd/github.ts +1548 -0
  48. package/src/cli/cmd/import.ts +99 -0
  49. package/src/cli/cmd/mcp.ts +765 -0
  50. package/src/cli/cmd/models.ts +77 -0
  51. package/src/cli/cmd/pr.ts +112 -0
  52. package/src/cli/cmd/run.ts +395 -0
  53. package/src/cli/cmd/serve.ts +20 -0
  54. package/src/cli/cmd/session.ts +135 -0
  55. package/src/cli/cmd/stats.ts +402 -0
  56. package/src/cli/cmd/tui/app.tsx +923 -0
  57. package/src/cli/cmd/tui/attach.ts +39 -0
  58. package/src/cli/cmd/tui/component/border.tsx +21 -0
  59. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  60. package/src/cli/cmd/tui/component/dialog-command.tsx +162 -0
  61. package/src/cli/cmd/tui/component/dialog-error-log.tsx +155 -0
  62. package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
  63. package/src/cli/cmd/tui/component/dialog-model.tsx +234 -0
  64. package/src/cli/cmd/tui/component/dialog-provider.tsx +256 -0
  65. package/src/cli/cmd/tui/component/dialog-session-list.tsx +114 -0
  66. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  67. package/src/cli/cmd/tui/component/dialog-stash.tsx +87 -0
  68. package/src/cli/cmd/tui/component/dialog-status.tsx +164 -0
  69. package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
  70. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
  71. package/src/cli/cmd/tui/component/dynamic-layout.tsx +86 -0
  72. package/src/cli/cmd/tui/component/inspector-overlay.tsx +247 -0
  73. package/src/cli/cmd/tui/component/logo.tsx +88 -0
  74. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +653 -0
  75. package/src/cli/cmd/tui/component/prompt/frecency.tsx +89 -0
  76. package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
  77. package/src/cli/cmd/tui/component/prompt/index.tsx +1347 -0
  78. package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
  79. package/src/cli/cmd/tui/component/textarea-keybindings.ts +73 -0
  80. package/src/cli/cmd/tui/component/tips.tsx +153 -0
  81. package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
  82. package/src/cli/cmd/tui/context/args.tsx +14 -0
  83. package/src/cli/cmd/tui/context/directory.ts +13 -0
  84. package/src/cli/cmd/tui/context/error-log.tsx +56 -0
  85. package/src/cli/cmd/tui/context/exit.tsx +26 -0
  86. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  87. package/src/cli/cmd/tui/context/inspector.tsx +57 -0
  88. package/src/cli/cmd/tui/context/keybind.tsx +108 -0
  89. package/src/cli/cmd/tui/context/kv.tsx +53 -0
  90. package/src/cli/cmd/tui/context/layout.tsx +240 -0
  91. package/src/cli/cmd/tui/context/local.tsx +402 -0
  92. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  93. package/src/cli/cmd/tui/context/route.tsx +51 -0
  94. package/src/cli/cmd/tui/context/sdk.tsx +94 -0
  95. package/src/cli/cmd/tui/context/sync.tsx +449 -0
  96. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  97. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  98. package/src/cli/cmd/tui/context/theme/carbonfox.json +248 -0
  99. package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
  100. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
  101. package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
  102. package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
  103. package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
  104. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  105. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  106. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  107. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  108. package/src/cli/cmd/tui/context/theme/gruvbox.json +242 -0
  109. package/src/cli/cmd/tui/context/theme/jonsoc.json +245 -0
  110. package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
  111. package/src/cli/cmd/tui/context/theme/lucent-orng.json +237 -0
  112. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  113. package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
  114. package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
  115. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  116. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  117. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  118. package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
  119. package/src/cli/cmd/tui/context/theme/orng.json +249 -0
  120. package/src/cli/cmd/tui/context/theme/osaka-jade.json +93 -0
  121. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  122. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  123. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  124. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  125. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  126. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  127. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  128. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  129. package/src/cli/cmd/tui/context/theme.tsx +1153 -0
  130. package/src/cli/cmd/tui/event.ts +48 -0
  131. package/src/cli/cmd/tui/hooks/use-command-registry.tsx +184 -0
  132. package/src/cli/cmd/tui/routes/home.tsx +198 -0
  133. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +64 -0
  134. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +109 -0
  135. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -0
  136. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
  137. package/src/cli/cmd/tui/routes/session/footer.tsx +91 -0
  138. package/src/cli/cmd/tui/routes/session/git-commit.tsx +59 -0
  139. package/src/cli/cmd/tui/routes/session/git-history.tsx +122 -0
  140. package/src/cli/cmd/tui/routes/session/header.tsx +185 -0
  141. package/src/cli/cmd/tui/routes/session/index.tsx +2363 -0
  142. package/src/cli/cmd/tui/routes/session/navigator-ui.tsx +214 -0
  143. package/src/cli/cmd/tui/routes/session/navigator.tsx +1124 -0
  144. package/src/cli/cmd/tui/routes/session/panel-explorer.tsx +553 -0
  145. package/src/cli/cmd/tui/routes/session/panel-viewer.tsx +386 -0
  146. package/src/cli/cmd/tui/routes/session/permission.tsx +501 -0
  147. package/src/cli/cmd/tui/routes/session/question.tsx +507 -0
  148. package/src/cli/cmd/tui/routes/session/sidebar.tsx +365 -0
  149. package/src/cli/cmd/tui/routes/session/vcs-diff-viewer.tsx +37 -0
  150. package/src/cli/cmd/tui/routes/ui-settings.tsx +449 -0
  151. package/src/cli/cmd/tui/thread.ts +172 -0
  152. package/src/cli/cmd/tui/ui/dialog-alert.tsx +90 -0
  153. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +83 -0
  154. package/src/cli/cmd/tui/ui/dialog-export-options.tsx +204 -0
  155. package/src/cli/cmd/tui/ui/dialog-help.tsx +38 -0
  156. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +77 -0
  157. package/src/cli/cmd/tui/ui/dialog-select.tsx +384 -0
  158. package/src/cli/cmd/tui/ui/dialog.tsx +170 -0
  159. package/src/cli/cmd/tui/ui/link.tsx +28 -0
  160. package/src/cli/cmd/tui/ui/spinner.ts +375 -0
  161. package/src/cli/cmd/tui/ui/toast.tsx +100 -0
  162. package/src/cli/cmd/tui/util/clipboard.ts +255 -0
  163. package/src/cli/cmd/tui/util/editor.ts +32 -0
  164. package/src/cli/cmd/tui/util/signal.ts +7 -0
  165. package/src/cli/cmd/tui/util/terminal.ts +114 -0
  166. package/src/cli/cmd/tui/util/transcript.ts +98 -0
  167. package/src/cli/cmd/tui/worker.ts +152 -0
  168. package/src/cli/cmd/uninstall.ts +362 -0
  169. package/src/cli/cmd/upgrade.ts +73 -0
  170. package/src/cli/cmd/web.ts +81 -0
  171. package/src/cli/error.ts +57 -0
  172. package/src/cli/network.ts +53 -0
  173. package/src/cli/ui.ts +119 -0
  174. package/src/cli/upgrade.ts +25 -0
  175. package/src/command/index.ts +131 -0
  176. package/src/command/template/initialize.txt +10 -0
  177. package/src/command/template/review.txt +99 -0
  178. package/src/config/config.ts +1404 -0
  179. package/src/config/markdown.ts +93 -0
  180. package/src/env/index.ts +26 -0
  181. package/src/file/ignore.ts +83 -0
  182. package/src/file/index.ts +432 -0
  183. package/src/file/ripgrep.ts +407 -0
  184. package/src/file/time.ts +69 -0
  185. package/src/file/watcher.ts +127 -0
  186. package/src/flag/flag.ts +80 -0
  187. package/src/format/formatter.ts +357 -0
  188. package/src/format/index.ts +137 -0
  189. package/src/global/index.ts +58 -0
  190. package/src/id/id.ts +83 -0
  191. package/src/ide/index.ts +76 -0
  192. package/src/index.ts +208 -0
  193. package/src/installation/index.ts +258 -0
  194. package/src/lsp/client.ts +252 -0
  195. package/src/lsp/index.ts +485 -0
  196. package/src/lsp/language.ts +119 -0
  197. package/src/lsp/server.ts +2046 -0
  198. package/src/mcp/auth.ts +135 -0
  199. package/src/mcp/index.ts +934 -0
  200. package/src/mcp/oauth-callback.ts +200 -0
  201. package/src/mcp/oauth-provider.ts +155 -0
  202. package/src/patch/index.ts +680 -0
  203. package/src/permission/arity.ts +163 -0
  204. package/src/permission/index.ts +210 -0
  205. package/src/permission/next.ts +280 -0
  206. package/src/plugin/codex.ts +500 -0
  207. package/src/plugin/copilot.ts +283 -0
  208. package/src/plugin/index.ts +135 -0
  209. package/src/project/bootstrap.ts +35 -0
  210. package/src/project/instance.ts +91 -0
  211. package/src/project/project.ts +371 -0
  212. package/src/project/state.ts +66 -0
  213. package/src/project/vcs.ts +151 -0
  214. package/src/provider/auth.ts +147 -0
  215. package/src/provider/models-macro.ts +14 -0
  216. package/src/provider/models.ts +114 -0
  217. package/src/provider/provider.ts +1220 -0
  218. package/src/provider/sdk/openai-compatible/src/README.md +5 -0
  219. package/src/provider/sdk/openai-compatible/src/index.ts +2 -0
  220. package/src/provider/sdk/openai-compatible/src/openai-compatible-provider.ts +100 -0
  221. package/src/provider/sdk/openai-compatible/src/responses/convert-to-openai-responses-input.ts +303 -0
  222. package/src/provider/sdk/openai-compatible/src/responses/map-openai-responses-finish-reason.ts +22 -0
  223. package/src/provider/sdk/openai-compatible/src/responses/openai-config.ts +18 -0
  224. package/src/provider/sdk/openai-compatible/src/responses/openai-error.ts +22 -0
  225. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-api-types.ts +207 -0
  226. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts +1732 -0
  227. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-prepare-tools.ts +177 -0
  228. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-settings.ts +1 -0
  229. package/src/provider/sdk/openai-compatible/src/responses/tool/code-interpreter.ts +88 -0
  230. package/src/provider/sdk/openai-compatible/src/responses/tool/file-search.ts +128 -0
  231. package/src/provider/sdk/openai-compatible/src/responses/tool/image-generation.ts +115 -0
  232. package/src/provider/sdk/openai-compatible/src/responses/tool/local-shell.ts +65 -0
  233. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search-preview.ts +104 -0
  234. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search.ts +103 -0
  235. package/src/provider/transform.ts +742 -0
  236. package/src/pty/index.ts +241 -0
  237. package/src/question/index.ts +176 -0
  238. package/src/scheduler/index.ts +61 -0
  239. package/src/server/error.ts +36 -0
  240. package/src/server/event.ts +7 -0
  241. package/src/server/mdns.ts +59 -0
  242. package/src/server/routes/config.ts +92 -0
  243. package/src/server/routes/experimental.ts +208 -0
  244. package/src/server/routes/file.ts +227 -0
  245. package/src/server/routes/global.ts +135 -0
  246. package/src/server/routes/mcp.ts +225 -0
  247. package/src/server/routes/permission.ts +68 -0
  248. package/src/server/routes/project.ts +82 -0
  249. package/src/server/routes/provider.ts +165 -0
  250. package/src/server/routes/pty.ts +169 -0
  251. package/src/server/routes/question.ts +98 -0
  252. package/src/server/routes/session.ts +939 -0
  253. package/src/server/routes/tui.ts +379 -0
  254. package/src/server/server.ts +663 -0
  255. package/src/session/compaction.ts +225 -0
  256. package/src/session/index.ts +498 -0
  257. package/src/session/llm.ts +288 -0
  258. package/src/session/message-v2.ts +740 -0
  259. package/src/session/message.ts +189 -0
  260. package/src/session/processor.ts +406 -0
  261. package/src/session/prompt/anthropic-20250930.txt +168 -0
  262. package/src/session/prompt/anthropic.txt +172 -0
  263. package/src/session/prompt/anthropic_spoof.txt +1 -0
  264. package/src/session/prompt/beast.txt +149 -0
  265. package/src/session/prompt/build-switch.txt +5 -0
  266. package/src/session/prompt/codex_header.txt +81 -0
  267. package/src/session/prompt/copilot-gpt-5.txt +145 -0
  268. package/src/session/prompt/gemini.txt +157 -0
  269. package/src/session/prompt/max-steps.txt +16 -0
  270. package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
  271. package/src/session/prompt/plan.txt +26 -0
  272. package/src/session/prompt/qwen.txt +111 -0
  273. package/src/session/prompt.ts +1815 -0
  274. package/src/session/retry.ts +90 -0
  275. package/src/session/revert.ts +121 -0
  276. package/src/session/status.ts +76 -0
  277. package/src/session/summary.ts +150 -0
  278. package/src/session/system.ts +156 -0
  279. package/src/session/todo.ts +37 -0
  280. package/src/share/share-next.ts +204 -0
  281. package/src/share/share.ts +95 -0
  282. package/src/shell/shell.ts +67 -0
  283. package/src/skill/index.ts +1 -0
  284. package/src/skill/skill.ts +135 -0
  285. package/src/snapshot/index.ts +236 -0
  286. package/src/storage/storage.ts +227 -0
  287. package/src/tool/apply_patch.ts +279 -0
  288. package/src/tool/apply_patch.txt +33 -0
  289. package/src/tool/bash.ts +258 -0
  290. package/src/tool/bash.txt +115 -0
  291. package/src/tool/batch.ts +175 -0
  292. package/src/tool/batch.txt +24 -0
  293. package/src/tool/codesearch.ts +132 -0
  294. package/src/tool/codesearch.txt +12 -0
  295. package/src/tool/edit.ts +645 -0
  296. package/src/tool/edit.txt +10 -0
  297. package/src/tool/external-directory.ts +32 -0
  298. package/src/tool/glob.ts +77 -0
  299. package/src/tool/glob.txt +6 -0
  300. package/src/tool/grep.ts +154 -0
  301. package/src/tool/grep.txt +8 -0
  302. package/src/tool/invalid.ts +17 -0
  303. package/src/tool/ls.ts +121 -0
  304. package/src/tool/ls.txt +1 -0
  305. package/src/tool/lsp.ts +96 -0
  306. package/src/tool/lsp.txt +19 -0
  307. package/src/tool/multiedit.ts +46 -0
  308. package/src/tool/multiedit.txt +41 -0
  309. package/src/tool/plan-enter.txt +14 -0
  310. package/src/tool/plan-exit.txt +13 -0
  311. package/src/tool/plan.ts +130 -0
  312. package/src/tool/question.ts +33 -0
  313. package/src/tool/question.txt +10 -0
  314. package/src/tool/read.ts +202 -0
  315. package/src/tool/read.txt +12 -0
  316. package/src/tool/registry.ts +162 -0
  317. package/src/tool/skill.ts +82 -0
  318. package/src/tool/task.ts +188 -0
  319. package/src/tool/task.txt +60 -0
  320. package/src/tool/todo.ts +53 -0
  321. package/src/tool/todoread.txt +14 -0
  322. package/src/tool/todowrite.txt +167 -0
  323. package/src/tool/tool.ts +88 -0
  324. package/src/tool/truncation.ts +106 -0
  325. package/src/tool/webfetch.ts +182 -0
  326. package/src/tool/webfetch.txt +13 -0
  327. package/src/tool/websearch.ts +150 -0
  328. package/src/tool/websearch.txt +14 -0
  329. package/src/tool/write.ts +80 -0
  330. package/src/tool/write.txt +8 -0
  331. package/src/util/archive.ts +16 -0
  332. package/src/util/color.ts +19 -0
  333. package/src/util/context.ts +25 -0
  334. package/src/util/defer.ts +12 -0
  335. package/src/util/eventloop.ts +20 -0
  336. package/src/util/filesystem.ts +93 -0
  337. package/src/util/fn.ts +11 -0
  338. package/src/util/format.ts +20 -0
  339. package/src/util/iife.ts +3 -0
  340. package/src/util/keybind.ts +103 -0
  341. package/src/util/lazy.ts +18 -0
  342. package/src/util/locale.ts +81 -0
  343. package/src/util/lock.ts +98 -0
  344. package/src/util/log.ts +180 -0
  345. package/src/util/queue.ts +32 -0
  346. package/src/util/rpc.ts +66 -0
  347. package/src/util/scrap.ts +10 -0
  348. package/src/util/signal.ts +12 -0
  349. package/src/util/timeout.ts +14 -0
  350. package/src/util/token.ts +7 -0
  351. package/src/util/wildcard.ts +56 -0
  352. package/src/worktree/index.ts +524 -0
  353. package/sst-env.d.ts +9 -0
  354. package/test/acp/agent-interface.test.ts +51 -0
  355. package/test/acp/event-subscription.test.ts +436 -0
  356. package/test/agent/agent.test.ts +638 -0
  357. package/test/bun.test.ts +53 -0
  358. package/test/cli/cmd/tui/fileref.test.ts +30 -0
  359. package/test/cli/github-action.test.ts +129 -0
  360. package/test/cli/github-remote.test.ts +80 -0
  361. package/test/cli/tui/navigator_logic.test.ts +99 -0
  362. package/test/cli/tui/transcript.test.ts +297 -0
  363. package/test/cli/ui.test.ts +80 -0
  364. package/test/config/agent-color.test.ts +66 -0
  365. package/test/config/config.test.ts +1613 -0
  366. package/test/config/fixtures/empty-frontmatter.md +4 -0
  367. package/test/config/fixtures/frontmatter.md +28 -0
  368. package/test/config/fixtures/no-frontmatter.md +1 -0
  369. package/test/config/markdown.test.ts +192 -0
  370. package/test/file/ignore.test.ts +10 -0
  371. package/test/file/path-traversal.test.ts +198 -0
  372. package/test/fixture/fixture.ts +45 -0
  373. package/test/fixture/lsp/fake-lsp-server.js +77 -0
  374. package/test/ide/ide.test.ts +82 -0
  375. package/test/keybind.test.ts +421 -0
  376. package/test/lsp/client.test.ts +95 -0
  377. package/test/mcp/headers.test.ts +153 -0
  378. package/test/mcp/oauth-browser.test.ts +261 -0
  379. package/test/patch/patch.test.ts +348 -0
  380. package/test/permission/arity.test.ts +33 -0
  381. package/test/permission/next.test.ts +690 -0
  382. package/test/permission-task.test.ts +319 -0
  383. package/test/plugin/codex.test.ts +123 -0
  384. package/test/preload.ts +67 -0
  385. package/test/project/project.test.ts +120 -0
  386. package/test/provider/amazon-bedrock.test.ts +268 -0
  387. package/test/provider/gitlab-duo.test.ts +286 -0
  388. package/test/provider/provider.test.ts +2149 -0
  389. package/test/provider/transform.test.ts +1631 -0
  390. package/test/question/question.test.ts +300 -0
  391. package/test/scheduler.test.ts +73 -0
  392. package/test/server/session-list.test.ts +39 -0
  393. package/test/server/session-select.test.ts +78 -0
  394. package/test/session/compaction.test.ts +293 -0
  395. package/test/session/llm.test.ts +90 -0
  396. package/test/session/message-v2.test.ts +786 -0
  397. package/test/session/retry.test.ts +131 -0
  398. package/test/session/revert-compact.test.ts +285 -0
  399. package/test/session/session.test.ts +71 -0
  400. package/test/skill/skill.test.ts +185 -0
  401. package/test/snapshot/snapshot.test.ts +939 -0
  402. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  403. package/test/tool/apply_patch.test.ts +499 -0
  404. package/test/tool/bash.test.ts +320 -0
  405. package/test/tool/external-directory.test.ts +126 -0
  406. package/test/tool/fixtures/large-image.png +0 -0
  407. package/test/tool/fixtures/models-api.json +33453 -0
  408. package/test/tool/grep.test.ts +109 -0
  409. package/test/tool/question.test.ts +105 -0
  410. package/test/tool/read.test.ts +332 -0
  411. package/test/tool/registry.test.ts +76 -0
  412. package/test/tool/truncation.test.ts +159 -0
  413. package/test/util/filesystem.test.ts +39 -0
  414. package/test/util/format.test.ts +59 -0
  415. package/test/util/iife.test.ts +36 -0
  416. package/test/util/lazy.test.ts +50 -0
  417. package/test/util/lock.test.ts +72 -0
  418. package/test/util/timeout.test.ts +21 -0
  419. package/test/util/wildcard.test.ts +75 -0
  420. package/tsconfig.json +16 -0
@@ -0,0 +1,690 @@
1
+ import { test, expect } from "bun:test"
2
+ import os from "os"
3
+ import { PermissionNext } from "../../src/permission/next"
4
+ import { Instance } from "../../src/project/instance"
5
+ import { Storage } from "../../src/storage/storage"
6
+ import { tmpdir } from "../fixture/fixture"
7
+
8
+ // fromConfig tests
9
+
10
+ test("fromConfig - string value becomes wildcard rule", () => {
11
+ const result = PermissionNext.fromConfig({ bash: "allow" })
12
+ expect(result).toEqual([{ permission: "bash", pattern: "*", action: "allow" }])
13
+ })
14
+
15
+ test("fromConfig - object value converts to rules array", () => {
16
+ const result = PermissionNext.fromConfig({ bash: { "*": "allow", rm: "deny" } })
17
+ expect(result).toEqual([
18
+ { permission: "bash", pattern: "*", action: "allow" },
19
+ { permission: "bash", pattern: "rm", action: "deny" },
20
+ ])
21
+ })
22
+
23
+ test("fromConfig - mixed string and object values", () => {
24
+ const result = PermissionNext.fromConfig({
25
+ bash: { "*": "allow", rm: "deny" },
26
+ edit: "allow",
27
+ webfetch: "ask",
28
+ })
29
+ expect(result).toEqual([
30
+ { permission: "bash", pattern: "*", action: "allow" },
31
+ { permission: "bash", pattern: "rm", action: "deny" },
32
+ { permission: "edit", pattern: "*", action: "allow" },
33
+ { permission: "webfetch", pattern: "*", action: "ask" },
34
+ ])
35
+ })
36
+
37
+ test("fromConfig - empty object", () => {
38
+ const result = PermissionNext.fromConfig({})
39
+ expect(result).toEqual([])
40
+ })
41
+
42
+ test("fromConfig - expands tilde to home directory", () => {
43
+ const result = PermissionNext.fromConfig({ external_directory: { "~/projects/*": "allow" } })
44
+ expect(result).toEqual([{ permission: "external_directory", pattern: `${os.homedir()}/projects/*`, action: "allow" }])
45
+ })
46
+
47
+ test("fromConfig - expands $HOME to home directory", () => {
48
+ const result = PermissionNext.fromConfig({ external_directory: { "$HOME/projects/*": "allow" } })
49
+ expect(result).toEqual([{ permission: "external_directory", pattern: `${os.homedir()}/projects/*`, action: "allow" }])
50
+ })
51
+
52
+ test("fromConfig - expands $HOME without trailing slash", () => {
53
+ const result = PermissionNext.fromConfig({ external_directory: { $HOME: "allow" } })
54
+ expect(result).toEqual([{ permission: "external_directory", pattern: os.homedir(), action: "allow" }])
55
+ })
56
+
57
+ test("fromConfig - does not expand tilde in middle of path", () => {
58
+ const result = PermissionNext.fromConfig({ external_directory: { "/some/~/path": "allow" } })
59
+ expect(result).toEqual([{ permission: "external_directory", pattern: "/some/~/path", action: "allow" }])
60
+ })
61
+
62
+ test("fromConfig - expands exact tilde to home directory", () => {
63
+ const result = PermissionNext.fromConfig({ external_directory: { "~": "allow" } })
64
+ expect(result).toEqual([{ permission: "external_directory", pattern: os.homedir(), action: "allow" }])
65
+ })
66
+
67
+ test("evaluate - matches expanded tilde pattern", () => {
68
+ const ruleset = PermissionNext.fromConfig({ external_directory: { "~/projects/*": "allow" } })
69
+ const result = PermissionNext.evaluate("external_directory", `${os.homedir()}/projects/file.txt`, ruleset)
70
+ expect(result.action).toBe("allow")
71
+ })
72
+
73
+ test("evaluate - matches expanded $HOME pattern", () => {
74
+ const ruleset = PermissionNext.fromConfig({ external_directory: { "$HOME/projects/*": "allow" } })
75
+ const result = PermissionNext.evaluate("external_directory", `${os.homedir()}/projects/file.txt`, ruleset)
76
+ expect(result.action).toBe("allow")
77
+ })
78
+
79
+ // merge tests
80
+
81
+ test("merge - simple concatenation", () => {
82
+ const result = PermissionNext.merge(
83
+ [{ permission: "bash", pattern: "*", action: "allow" }],
84
+ [{ permission: "bash", pattern: "*", action: "deny" }],
85
+ )
86
+ expect(result).toEqual([
87
+ { permission: "bash", pattern: "*", action: "allow" },
88
+ { permission: "bash", pattern: "*", action: "deny" },
89
+ ])
90
+ })
91
+
92
+ test("merge - adds new permission", () => {
93
+ const result = PermissionNext.merge(
94
+ [{ permission: "bash", pattern: "*", action: "allow" }],
95
+ [{ permission: "edit", pattern: "*", action: "deny" }],
96
+ )
97
+ expect(result).toEqual([
98
+ { permission: "bash", pattern: "*", action: "allow" },
99
+ { permission: "edit", pattern: "*", action: "deny" },
100
+ ])
101
+ })
102
+
103
+ test("merge - concatenates rules for same permission", () => {
104
+ const result = PermissionNext.merge(
105
+ [{ permission: "bash", pattern: "foo", action: "ask" }],
106
+ [{ permission: "bash", pattern: "*", action: "deny" }],
107
+ )
108
+ expect(result).toEqual([
109
+ { permission: "bash", pattern: "foo", action: "ask" },
110
+ { permission: "bash", pattern: "*", action: "deny" },
111
+ ])
112
+ })
113
+
114
+ test("merge - multiple rulesets", () => {
115
+ const result = PermissionNext.merge(
116
+ [{ permission: "bash", pattern: "*", action: "allow" }],
117
+ [{ permission: "bash", pattern: "rm", action: "ask" }],
118
+ [{ permission: "edit", pattern: "*", action: "allow" }],
119
+ )
120
+ expect(result).toEqual([
121
+ { permission: "bash", pattern: "*", action: "allow" },
122
+ { permission: "bash", pattern: "rm", action: "ask" },
123
+ { permission: "edit", pattern: "*", action: "allow" },
124
+ ])
125
+ })
126
+
127
+ test("merge - empty ruleset does nothing", () => {
128
+ const result = PermissionNext.merge([{ permission: "bash", pattern: "*", action: "allow" }], [])
129
+ expect(result).toEqual([{ permission: "bash", pattern: "*", action: "allow" }])
130
+ })
131
+
132
+ test("merge - preserves rule order", () => {
133
+ const result = PermissionNext.merge(
134
+ [
135
+ { permission: "edit", pattern: "src/*", action: "allow" },
136
+ { permission: "edit", pattern: "src/secret/*", action: "deny" },
137
+ ],
138
+ [{ permission: "edit", pattern: "src/secret/ok.ts", action: "allow" }],
139
+ )
140
+ expect(result).toEqual([
141
+ { permission: "edit", pattern: "src/*", action: "allow" },
142
+ { permission: "edit", pattern: "src/secret/*", action: "deny" },
143
+ { permission: "edit", pattern: "src/secret/ok.ts", action: "allow" },
144
+ ])
145
+ })
146
+
147
+ test("merge - config permission overrides default ask", () => {
148
+ // Simulates: defaults have "*": "ask", config sets bash: "allow"
149
+ const defaults: PermissionNext.Ruleset = [{ permission: "*", pattern: "*", action: "ask" }]
150
+ const config: PermissionNext.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
151
+ const merged = PermissionNext.merge(defaults, config)
152
+
153
+ // Config's bash allow should override default ask
154
+ expect(PermissionNext.evaluate("bash", "ls", merged).action).toBe("allow")
155
+ // Other permissions should still be ask (from defaults)
156
+ expect(PermissionNext.evaluate("edit", "foo.ts", merged).action).toBe("ask")
157
+ })
158
+
159
+ test("merge - config ask overrides default allow", () => {
160
+ // Simulates: defaults have bash: "allow", config sets bash: "ask"
161
+ const defaults: PermissionNext.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
162
+ const config: PermissionNext.Ruleset = [{ permission: "bash", pattern: "*", action: "ask" }]
163
+ const merged = PermissionNext.merge(defaults, config)
164
+
165
+ // Config's ask should override default allow
166
+ expect(PermissionNext.evaluate("bash", "ls", merged).action).toBe("ask")
167
+ })
168
+
169
+ // evaluate tests
170
+
171
+ test("evaluate - exact pattern match", () => {
172
+ const result = PermissionNext.evaluate("bash", "rm", [{ permission: "bash", pattern: "rm", action: "deny" }])
173
+ expect(result.action).toBe("deny")
174
+ })
175
+
176
+ test("evaluate - wildcard pattern match", () => {
177
+ const result = PermissionNext.evaluate("bash", "rm", [{ permission: "bash", pattern: "*", action: "allow" }])
178
+ expect(result.action).toBe("allow")
179
+ })
180
+
181
+ test("evaluate - last matching rule wins", () => {
182
+ const result = PermissionNext.evaluate("bash", "rm", [
183
+ { permission: "bash", pattern: "*", action: "allow" },
184
+ { permission: "bash", pattern: "rm", action: "deny" },
185
+ ])
186
+ expect(result.action).toBe("deny")
187
+ })
188
+
189
+ test("evaluate - last matching rule wins (wildcard after specific)", () => {
190
+ const result = PermissionNext.evaluate("bash", "rm", [
191
+ { permission: "bash", pattern: "rm", action: "deny" },
192
+ { permission: "bash", pattern: "*", action: "allow" },
193
+ ])
194
+ expect(result.action).toBe("allow")
195
+ })
196
+
197
+ test("evaluate - glob pattern match", () => {
198
+ const result = PermissionNext.evaluate("edit", "src/foo.ts", [
199
+ { permission: "edit", pattern: "src/*", action: "allow" },
200
+ ])
201
+ expect(result.action).toBe("allow")
202
+ })
203
+
204
+ test("evaluate - last matching glob wins", () => {
205
+ const result = PermissionNext.evaluate("edit", "src/components/Button.tsx", [
206
+ { permission: "edit", pattern: "src/*", action: "deny" },
207
+ { permission: "edit", pattern: "src/components/*", action: "allow" },
208
+ ])
209
+ expect(result.action).toBe("allow")
210
+ })
211
+
212
+ test("evaluate - order matters for specificity", () => {
213
+ // If more specific rule comes first, later wildcard overrides it
214
+ const result = PermissionNext.evaluate("edit", "src/components/Button.tsx", [
215
+ { permission: "edit", pattern: "src/components/*", action: "allow" },
216
+ { permission: "edit", pattern: "src/*", action: "deny" },
217
+ ])
218
+ expect(result.action).toBe("deny")
219
+ })
220
+
221
+ test("evaluate - unknown permission returns ask", () => {
222
+ const result = PermissionNext.evaluate("unknown_tool", "anything", [
223
+ { permission: "bash", pattern: "*", action: "allow" },
224
+ ])
225
+ expect(result.action).toBe("ask")
226
+ })
227
+
228
+ test("evaluate - empty ruleset returns ask", () => {
229
+ const result = PermissionNext.evaluate("bash", "rm", [])
230
+ expect(result.action).toBe("ask")
231
+ })
232
+
233
+ test("evaluate - no matching pattern returns ask", () => {
234
+ const result = PermissionNext.evaluate("edit", "etc/passwd", [
235
+ { permission: "edit", pattern: "src/*", action: "allow" },
236
+ ])
237
+ expect(result.action).toBe("ask")
238
+ })
239
+
240
+ test("evaluate - empty rules array returns ask", () => {
241
+ const result = PermissionNext.evaluate("bash", "rm", [])
242
+ expect(result.action).toBe("ask")
243
+ })
244
+
245
+ test("evaluate - multiple matching patterns, last wins", () => {
246
+ const result = PermissionNext.evaluate("edit", "src/secret.ts", [
247
+ { permission: "edit", pattern: "*", action: "ask" },
248
+ { permission: "edit", pattern: "src/*", action: "allow" },
249
+ { permission: "edit", pattern: "src/secret.ts", action: "deny" },
250
+ ])
251
+ expect(result.action).toBe("deny")
252
+ })
253
+
254
+ test("evaluate - non-matching patterns are skipped", () => {
255
+ const result = PermissionNext.evaluate("edit", "src/foo.ts", [
256
+ { permission: "edit", pattern: "*", action: "ask" },
257
+ { permission: "edit", pattern: "test/*", action: "deny" },
258
+ { permission: "edit", pattern: "src/*", action: "allow" },
259
+ ])
260
+ expect(result.action).toBe("allow")
261
+ })
262
+
263
+ test("evaluate - exact match at end wins over earlier wildcard", () => {
264
+ const result = PermissionNext.evaluate("bash", "/bin/rm", [
265
+ { permission: "bash", pattern: "*", action: "allow" },
266
+ { permission: "bash", pattern: "/bin/rm", action: "deny" },
267
+ ])
268
+ expect(result.action).toBe("deny")
269
+ })
270
+
271
+ test("evaluate - wildcard at end overrides earlier exact match", () => {
272
+ const result = PermissionNext.evaluate("bash", "/bin/rm", [
273
+ { permission: "bash", pattern: "/bin/rm", action: "deny" },
274
+ { permission: "bash", pattern: "*", action: "allow" },
275
+ ])
276
+ expect(result.action).toBe("allow")
277
+ })
278
+
279
+ // wildcard permission tests
280
+
281
+ test("evaluate - wildcard permission matches any permission", () => {
282
+ const result = PermissionNext.evaluate("bash", "rm", [{ permission: "*", pattern: "*", action: "deny" }])
283
+ expect(result.action).toBe("deny")
284
+ })
285
+
286
+ test("evaluate - wildcard permission with specific pattern", () => {
287
+ const result = PermissionNext.evaluate("bash", "rm", [{ permission: "*", pattern: "rm", action: "deny" }])
288
+ expect(result.action).toBe("deny")
289
+ })
290
+
291
+ test("evaluate - glob permission pattern", () => {
292
+ const result = PermissionNext.evaluate("mcp_server_tool", "anything", [
293
+ { permission: "mcp_*", pattern: "*", action: "allow" },
294
+ ])
295
+ expect(result.action).toBe("allow")
296
+ })
297
+
298
+ test("evaluate - specific permission and wildcard permission combined", () => {
299
+ const result = PermissionNext.evaluate("bash", "rm", [
300
+ { permission: "*", pattern: "*", action: "deny" },
301
+ { permission: "bash", pattern: "*", action: "allow" },
302
+ ])
303
+ expect(result.action).toBe("allow")
304
+ })
305
+
306
+ test("evaluate - wildcard permission does not match when specific exists", () => {
307
+ const result = PermissionNext.evaluate("edit", "src/foo.ts", [
308
+ { permission: "*", pattern: "*", action: "deny" },
309
+ { permission: "edit", pattern: "src/*", action: "allow" },
310
+ ])
311
+ expect(result.action).toBe("allow")
312
+ })
313
+
314
+ test("evaluate - multiple matching permission patterns combine rules", () => {
315
+ const result = PermissionNext.evaluate("mcp_dangerous", "anything", [
316
+ { permission: "*", pattern: "*", action: "ask" },
317
+ { permission: "mcp_*", pattern: "*", action: "allow" },
318
+ { permission: "mcp_dangerous", pattern: "*", action: "deny" },
319
+ ])
320
+ expect(result.action).toBe("deny")
321
+ })
322
+
323
+ test("evaluate - wildcard permission fallback for unknown tool", () => {
324
+ const result = PermissionNext.evaluate("unknown_tool", "anything", [
325
+ { permission: "*", pattern: "*", action: "ask" },
326
+ { permission: "bash", pattern: "*", action: "allow" },
327
+ ])
328
+ expect(result.action).toBe("ask")
329
+ })
330
+
331
+ test("evaluate - permission patterns sorted by length regardless of object order", () => {
332
+ // specific permission listed before wildcard, but specific should still win
333
+ const result = PermissionNext.evaluate("bash", "rm", [
334
+ { permission: "bash", pattern: "*", action: "allow" },
335
+ { permission: "*", pattern: "*", action: "deny" },
336
+ ])
337
+ // With flat list, last matching rule wins - so "*" matches bash and wins
338
+ expect(result.action).toBe("deny")
339
+ })
340
+
341
+ test("evaluate - merges multiple rulesets", () => {
342
+ const config: PermissionNext.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
343
+ const approved: PermissionNext.Ruleset = [{ permission: "bash", pattern: "rm", action: "deny" }]
344
+ // approved comes after config, so rm should be denied
345
+ const result = PermissionNext.evaluate("bash", "rm", config, approved)
346
+ expect(result.action).toBe("deny")
347
+ })
348
+
349
+ // disabled tests
350
+
351
+ test("disabled - returns empty set when all tools allowed", () => {
352
+ const result = PermissionNext.disabled(["bash", "edit", "read"], [{ permission: "*", pattern: "*", action: "allow" }])
353
+ expect(result.size).toBe(0)
354
+ })
355
+
356
+ test("disabled - disables tool when denied", () => {
357
+ const result = PermissionNext.disabled(
358
+ ["bash", "edit", "read"],
359
+ [
360
+ { permission: "*", pattern: "*", action: "allow" },
361
+ { permission: "bash", pattern: "*", action: "deny" },
362
+ ],
363
+ )
364
+ expect(result.has("bash")).toBe(true)
365
+ expect(result.has("edit")).toBe(false)
366
+ expect(result.has("read")).toBe(false)
367
+ })
368
+
369
+ test("disabled - disables edit/write/patch/multiedit when edit denied", () => {
370
+ const result = PermissionNext.disabled(
371
+ ["edit", "write", "patch", "multiedit", "bash"],
372
+ [
373
+ { permission: "*", pattern: "*", action: "allow" },
374
+ { permission: "edit", pattern: "*", action: "deny" },
375
+ ],
376
+ )
377
+ expect(result.has("edit")).toBe(true)
378
+ expect(result.has("write")).toBe(true)
379
+ expect(result.has("patch")).toBe(true)
380
+ expect(result.has("multiedit")).toBe(true)
381
+ expect(result.has("bash")).toBe(false)
382
+ })
383
+
384
+ test("disabled - does not disable when partially denied", () => {
385
+ const result = PermissionNext.disabled(
386
+ ["bash"],
387
+ [
388
+ { permission: "bash", pattern: "*", action: "allow" },
389
+ { permission: "bash", pattern: "rm *", action: "deny" },
390
+ ],
391
+ )
392
+ expect(result.has("bash")).toBe(false)
393
+ })
394
+
395
+ test("disabled - does not disable when action is ask", () => {
396
+ const result = PermissionNext.disabled(["bash", "edit"], [{ permission: "*", pattern: "*", action: "ask" }])
397
+ expect(result.size).toBe(0)
398
+ })
399
+
400
+ test("disabled - does not disable when specific allow after wildcard deny", () => {
401
+ // Tool is NOT disabled because a specific allow after wildcard deny means
402
+ // there's at least some usage allowed
403
+ const result = PermissionNext.disabled(
404
+ ["bash"],
405
+ [
406
+ { permission: "bash", pattern: "*", action: "deny" },
407
+ { permission: "bash", pattern: "echo *", action: "allow" },
408
+ ],
409
+ )
410
+ expect(result.has("bash")).toBe(false)
411
+ })
412
+
413
+ test("disabled - does not disable when wildcard allow after deny", () => {
414
+ const result = PermissionNext.disabled(
415
+ ["bash"],
416
+ [
417
+ { permission: "bash", pattern: "rm *", action: "deny" },
418
+ { permission: "bash", pattern: "*", action: "allow" },
419
+ ],
420
+ )
421
+ expect(result.has("bash")).toBe(false)
422
+ })
423
+
424
+ test("disabled - disables multiple tools", () => {
425
+ const result = PermissionNext.disabled(
426
+ ["bash", "edit", "webfetch"],
427
+ [
428
+ { permission: "bash", pattern: "*", action: "deny" },
429
+ { permission: "edit", pattern: "*", action: "deny" },
430
+ { permission: "webfetch", pattern: "*", action: "deny" },
431
+ ],
432
+ )
433
+ expect(result.has("bash")).toBe(true)
434
+ expect(result.has("edit")).toBe(true)
435
+ expect(result.has("webfetch")).toBe(true)
436
+ })
437
+
438
+ test("disabled - wildcard permission denies all tools", () => {
439
+ const result = PermissionNext.disabled(["bash", "edit", "read"], [{ permission: "*", pattern: "*", action: "deny" }])
440
+ expect(result.has("bash")).toBe(true)
441
+ expect(result.has("edit")).toBe(true)
442
+ expect(result.has("read")).toBe(true)
443
+ })
444
+
445
+ test("disabled - specific allow overrides wildcard deny", () => {
446
+ const result = PermissionNext.disabled(
447
+ ["bash", "edit", "read"],
448
+ [
449
+ { permission: "*", pattern: "*", action: "deny" },
450
+ { permission: "bash", pattern: "*", action: "allow" },
451
+ ],
452
+ )
453
+ expect(result.has("bash")).toBe(false)
454
+ expect(result.has("edit")).toBe(true)
455
+ expect(result.has("read")).toBe(true)
456
+ })
457
+
458
+ // ask tests
459
+
460
+ test("ask - resolves immediately when action is allow", async () => {
461
+ await using tmp = await tmpdir({ git: true })
462
+ await Instance.provide({
463
+ directory: tmp.path,
464
+ fn: async () => {
465
+ const result = await PermissionNext.ask({
466
+ sessionID: "session_test",
467
+ permission: "bash",
468
+ patterns: ["ls"],
469
+ metadata: {},
470
+ always: [],
471
+ ruleset: [{ permission: "bash", pattern: "*", action: "allow" }],
472
+ })
473
+ expect(result).toBeUndefined()
474
+ },
475
+ })
476
+ })
477
+
478
+ test("ask - throws RejectedError when action is deny", async () => {
479
+ await using tmp = await tmpdir({ git: true })
480
+ await Instance.provide({
481
+ directory: tmp.path,
482
+ fn: async () => {
483
+ await expect(
484
+ PermissionNext.ask({
485
+ sessionID: "session_test",
486
+ permission: "bash",
487
+ patterns: ["rm -rf /"],
488
+ metadata: {},
489
+ always: [],
490
+ ruleset: [{ permission: "bash", pattern: "*", action: "deny" }],
491
+ }),
492
+ ).rejects.toBeInstanceOf(PermissionNext.DeniedError)
493
+ },
494
+ })
495
+ })
496
+
497
+ test("ask - returns pending promise when action is ask", async () => {
498
+ await using tmp = await tmpdir({ git: true })
499
+ await Instance.provide({
500
+ directory: tmp.path,
501
+ fn: async () => {
502
+ const promise = PermissionNext.ask({
503
+ sessionID: "session_test",
504
+ permission: "bash",
505
+ patterns: ["ls"],
506
+ metadata: {},
507
+ always: [],
508
+ ruleset: [{ permission: "bash", pattern: "*", action: "ask" }],
509
+ })
510
+ // Promise should be pending, not resolved
511
+ expect(promise).toBeInstanceOf(Promise)
512
+ // Don't await - just verify it returns a promise
513
+ },
514
+ })
515
+ })
516
+
517
+ // reply tests
518
+
519
+ test("reply - once resolves the pending ask", async () => {
520
+ await using tmp = await tmpdir({ git: true })
521
+ await Instance.provide({
522
+ directory: tmp.path,
523
+ fn: async () => {
524
+ const askPromise = PermissionNext.ask({
525
+ id: "permission_test1",
526
+ sessionID: "session_test",
527
+ permission: "bash",
528
+ patterns: ["ls"],
529
+ metadata: {},
530
+ always: [],
531
+ ruleset: [],
532
+ })
533
+
534
+ await PermissionNext.reply({
535
+ requestID: "permission_test1",
536
+ reply: "once",
537
+ })
538
+
539
+ await expect(askPromise).resolves.toBeUndefined()
540
+ },
541
+ })
542
+ })
543
+
544
+ test("reply - reject throws RejectedError", async () => {
545
+ await using tmp = await tmpdir({ git: true })
546
+ await Instance.provide({
547
+ directory: tmp.path,
548
+ fn: async () => {
549
+ const askPromise = PermissionNext.ask({
550
+ id: "permission_test2",
551
+ sessionID: "session_test",
552
+ permission: "bash",
553
+ patterns: ["ls"],
554
+ metadata: {},
555
+ always: [],
556
+ ruleset: [],
557
+ })
558
+
559
+ await PermissionNext.reply({
560
+ requestID: "permission_test2",
561
+ reply: "reject",
562
+ })
563
+
564
+ await expect(askPromise).rejects.toBeInstanceOf(PermissionNext.RejectedError)
565
+ },
566
+ })
567
+ })
568
+
569
+ test("reply - always persists approval and resolves", async () => {
570
+ await using tmp = await tmpdir({ git: true })
571
+ await Instance.provide({
572
+ directory: tmp.path,
573
+ fn: async () => {
574
+ const askPromise = PermissionNext.ask({
575
+ id: "permission_test3",
576
+ sessionID: "session_test",
577
+ permission: "bash",
578
+ patterns: ["ls"],
579
+ metadata: {},
580
+ always: ["ls"],
581
+ ruleset: [],
582
+ })
583
+
584
+ await PermissionNext.reply({
585
+ requestID: "permission_test3",
586
+ reply: "always",
587
+ })
588
+
589
+ await expect(askPromise).resolves.toBeUndefined()
590
+ },
591
+ })
592
+ // Re-provide to reload state with stored permissions
593
+ await Instance.provide({
594
+ directory: tmp.path,
595
+ fn: async () => {
596
+ // Stored approval should allow without asking
597
+ const result = await PermissionNext.ask({
598
+ sessionID: "session_test2",
599
+ permission: "bash",
600
+ patterns: ["ls"],
601
+ metadata: {},
602
+ always: [],
603
+ ruleset: [],
604
+ })
605
+ expect(result).toBeUndefined()
606
+ },
607
+ })
608
+ })
609
+
610
+ test("reply - reject cancels all pending for same session", async () => {
611
+ await using tmp = await tmpdir({ git: true })
612
+ await Instance.provide({
613
+ directory: tmp.path,
614
+ fn: async () => {
615
+ const askPromise1 = PermissionNext.ask({
616
+ id: "permission_test4a",
617
+ sessionID: "session_same",
618
+ permission: "bash",
619
+ patterns: ["ls"],
620
+ metadata: {},
621
+ always: [],
622
+ ruleset: [],
623
+ })
624
+
625
+ const askPromise2 = PermissionNext.ask({
626
+ id: "permission_test4b",
627
+ sessionID: "session_same",
628
+ permission: "edit",
629
+ patterns: ["foo.ts"],
630
+ metadata: {},
631
+ always: [],
632
+ ruleset: [],
633
+ })
634
+
635
+ // Catch rejections before they become unhandled
636
+ const result1 = askPromise1.catch((e) => e)
637
+ const result2 = askPromise2.catch((e) => e)
638
+
639
+ // Reject the first one
640
+ await PermissionNext.reply({
641
+ requestID: "permission_test4a",
642
+ reply: "reject",
643
+ })
644
+
645
+ // Both should be rejected
646
+ expect(await result1).toBeInstanceOf(PermissionNext.RejectedError)
647
+ expect(await result2).toBeInstanceOf(PermissionNext.RejectedError)
648
+ },
649
+ })
650
+ })
651
+
652
+ test("ask - checks all patterns and stops on first deny", async () => {
653
+ await using tmp = await tmpdir({ git: true })
654
+ await Instance.provide({
655
+ directory: tmp.path,
656
+ fn: async () => {
657
+ await expect(
658
+ PermissionNext.ask({
659
+ sessionID: "session_test",
660
+ permission: "bash",
661
+ patterns: ["echo hello", "rm -rf /"],
662
+ metadata: {},
663
+ always: [],
664
+ ruleset: [
665
+ { permission: "bash", pattern: "*", action: "allow" },
666
+ { permission: "bash", pattern: "rm *", action: "deny" },
667
+ ],
668
+ }),
669
+ ).rejects.toBeInstanceOf(PermissionNext.DeniedError)
670
+ },
671
+ })
672
+ })
673
+
674
+ test("ask - allows all patterns when all match allow rules", async () => {
675
+ await using tmp = await tmpdir({ git: true })
676
+ await Instance.provide({
677
+ directory: tmp.path,
678
+ fn: async () => {
679
+ const result = await PermissionNext.ask({
680
+ sessionID: "session_test",
681
+ permission: "bash",
682
+ patterns: ["echo hello", "ls -la", "pwd"],
683
+ metadata: {},
684
+ always: [],
685
+ ruleset: [{ permission: "bash", pattern: "*", action: "allow" }],
686
+ })
687
+ expect(result).toBeUndefined()
688
+ },
689
+ })
690
+ })