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