gencode-ai 0.1.0 → 0.1.2

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 (356) hide show
  1. package/.gencode/settings.local.json +7 -0
  2. package/README.md +20 -102
  3. package/dist/agent/agent.d.ts +43 -2
  4. package/dist/agent/agent.d.ts.map +1 -1
  5. package/dist/agent/agent.js +90 -17
  6. package/dist/agent/agent.js.map +1 -1
  7. package/dist/agent/types.d.ts +9 -1
  8. package/dist/agent/types.d.ts.map +1 -1
  9. package/dist/cli/components/AllModelsSelector.d.ts +11 -0
  10. package/dist/cli/components/AllModelsSelector.d.ts.map +1 -0
  11. package/dist/cli/components/AllModelsSelector.js +153 -0
  12. package/dist/cli/components/AllModelsSelector.js.map +1 -0
  13. package/dist/cli/components/App.d.ts +8 -1
  14. package/dist/cli/components/App.d.ts.map +1 -1
  15. package/dist/cli/components/App.js +276 -40
  16. package/dist/cli/components/App.js.map +1 -1
  17. package/dist/cli/components/CommandSuggestions.d.ts.map +1 -1
  18. package/dist/cli/components/CommandSuggestions.js +3 -0
  19. package/dist/cli/components/CommandSuggestions.js.map +1 -1
  20. package/dist/cli/components/Header.d.ts +1 -1
  21. package/dist/cli/components/Header.d.ts.map +1 -1
  22. package/dist/cli/components/Header.js +4 -6
  23. package/dist/cli/components/Header.js.map +1 -1
  24. package/dist/cli/components/Logo.d.ts +1 -0
  25. package/dist/cli/components/Logo.d.ts.map +1 -1
  26. package/dist/cli/components/Logo.js +16 -3
  27. package/dist/cli/components/Logo.js.map +1 -1
  28. package/dist/cli/components/Messages.d.ts +17 -3
  29. package/dist/cli/components/Messages.d.ts.map +1 -1
  30. package/dist/cli/components/Messages.js +70 -18
  31. package/dist/cli/components/Messages.js.map +1 -1
  32. package/dist/cli/components/ModelSelector.d.ts +7 -7
  33. package/dist/cli/components/ModelSelector.d.ts.map +1 -1
  34. package/dist/cli/components/ModelSelector.js +116 -33
  35. package/dist/cli/components/ModelSelector.js.map +1 -1
  36. package/dist/cli/components/PermissionPrompt.d.ts +60 -0
  37. package/dist/cli/components/PermissionPrompt.d.ts.map +1 -0
  38. package/dist/cli/components/PermissionPrompt.js +192 -0
  39. package/dist/cli/components/PermissionPrompt.js.map +1 -0
  40. package/dist/cli/components/ProviderManager.d.ts +8 -0
  41. package/dist/cli/components/ProviderManager.d.ts.map +1 -0
  42. package/dist/cli/components/ProviderManager.js +280 -0
  43. package/dist/cli/components/ProviderManager.js.map +1 -0
  44. package/dist/cli/components/Spinner.d.ts +7 -2
  45. package/dist/cli/components/Spinner.d.ts.map +1 -1
  46. package/dist/cli/components/Spinner.js +116 -25
  47. package/dist/cli/components/Spinner.js.map +1 -1
  48. package/dist/cli/components/TodoList.d.ts +7 -0
  49. package/dist/cli/components/TodoList.d.ts.map +1 -0
  50. package/dist/cli/components/TodoList.js +34 -0
  51. package/dist/cli/components/TodoList.js.map +1 -0
  52. package/dist/cli/components/index.d.ts +1 -0
  53. package/dist/cli/components/index.d.ts.map +1 -1
  54. package/dist/cli/components/index.js +1 -0
  55. package/dist/cli/components/index.js.map +1 -1
  56. package/dist/cli/components/markdown.d.ts +9 -0
  57. package/dist/cli/components/markdown.d.ts.map +1 -0
  58. package/dist/cli/components/markdown.js +129 -0
  59. package/dist/cli/components/markdown.js.map +1 -0
  60. package/dist/cli/components/theme.d.ts +5 -0
  61. package/dist/cli/components/theme.d.ts.map +1 -1
  62. package/dist/cli/components/theme.js +7 -0
  63. package/dist/cli/components/theme.js.map +1 -1
  64. package/dist/cli/index.js +66 -12
  65. package/dist/cli/index.js.map +1 -1
  66. package/dist/config/index.d.ts +14 -4
  67. package/dist/config/index.d.ts.map +1 -1
  68. package/dist/config/index.js +19 -3
  69. package/dist/config/index.js.map +1 -1
  70. package/dist/config/levels.d.ts +49 -0
  71. package/dist/config/levels.d.ts.map +1 -0
  72. package/dist/config/levels.js +222 -0
  73. package/dist/config/levels.js.map +1 -0
  74. package/dist/config/loader.d.ts +46 -0
  75. package/dist/config/loader.d.ts.map +1 -0
  76. package/dist/config/loader.js +153 -0
  77. package/dist/config/loader.js.map +1 -0
  78. package/dist/config/manager.d.ts +115 -15
  79. package/dist/config/manager.d.ts.map +1 -1
  80. package/dist/config/manager.js +260 -34
  81. package/dist/config/manager.js.map +1 -1
  82. package/dist/config/manager.test.d.ts +5 -0
  83. package/dist/config/manager.test.d.ts.map +1 -0
  84. package/dist/config/manager.test.js +192 -0
  85. package/dist/config/manager.test.js.map +1 -0
  86. package/dist/config/merger.d.ts +56 -0
  87. package/dist/config/merger.d.ts.map +1 -0
  88. package/dist/config/merger.js +177 -0
  89. package/dist/config/merger.js.map +1 -0
  90. package/dist/config/providers-config.d.ts +28 -0
  91. package/dist/config/providers-config.d.ts.map +1 -0
  92. package/dist/config/providers-config.js +79 -0
  93. package/dist/config/providers-config.js.map +1 -0
  94. package/dist/config/test-utils.d.ts +24 -0
  95. package/dist/config/test-utils.d.ts.map +1 -0
  96. package/dist/config/test-utils.js +55 -0
  97. package/dist/config/test-utils.js.map +1 -0
  98. package/dist/config/types.d.ts +108 -9
  99. package/dist/config/types.d.ts.map +1 -1
  100. package/dist/config/types.js +53 -2
  101. package/dist/config/types.js.map +1 -1
  102. package/dist/memory/import-resolver.d.ts +46 -0
  103. package/dist/memory/import-resolver.d.ts.map +1 -0
  104. package/dist/memory/import-resolver.js +117 -0
  105. package/dist/memory/import-resolver.js.map +1 -0
  106. package/dist/memory/index.d.ts +7 -6
  107. package/dist/memory/index.d.ts.map +1 -1
  108. package/dist/memory/index.js +7 -5
  109. package/dist/memory/index.js.map +1 -1
  110. package/dist/memory/init-prompt.d.ts +22 -0
  111. package/dist/memory/init-prompt.d.ts.map +1 -0
  112. package/dist/memory/init-prompt.js +103 -0
  113. package/dist/memory/init-prompt.js.map +1 -0
  114. package/dist/memory/memory-manager.d.ts +119 -0
  115. package/dist/memory/memory-manager.d.ts.map +1 -0
  116. package/dist/memory/memory-manager.js +587 -0
  117. package/dist/memory/memory-manager.js.map +1 -0
  118. package/dist/memory/rules-parser.d.ts +38 -0
  119. package/dist/memory/rules-parser.d.ts.map +1 -0
  120. package/dist/memory/rules-parser.js +69 -0
  121. package/dist/memory/rules-parser.js.map +1 -0
  122. package/dist/memory/test-utils.d.ts +20 -0
  123. package/dist/memory/test-utils.d.ts.map +1 -0
  124. package/dist/memory/test-utils.js +44 -0
  125. package/dist/memory/test-utils.js.map +1 -0
  126. package/dist/memory/types.d.ts +70 -63
  127. package/dist/memory/types.d.ts.map +1 -1
  128. package/dist/memory/types.js +42 -2
  129. package/dist/memory/types.js.map +1 -1
  130. package/dist/permissions/audit.d.ts +82 -0
  131. package/dist/permissions/audit.d.ts.map +1 -0
  132. package/dist/permissions/audit.js +229 -0
  133. package/dist/permissions/audit.js.map +1 -0
  134. package/dist/permissions/index.d.ts +11 -1
  135. package/dist/permissions/index.d.ts.map +1 -1
  136. package/dist/permissions/index.js +15 -0
  137. package/dist/permissions/index.js.map +1 -1
  138. package/dist/permissions/manager.d.ts +149 -13
  139. package/dist/permissions/manager.d.ts.map +1 -1
  140. package/dist/permissions/manager.js +480 -35
  141. package/dist/permissions/manager.js.map +1 -1
  142. package/dist/permissions/manager.test.d.ts +5 -0
  143. package/dist/permissions/manager.test.d.ts.map +1 -0
  144. package/dist/permissions/manager.test.js +213 -0
  145. package/dist/permissions/manager.test.js.map +1 -0
  146. package/dist/permissions/persistence.d.ts +74 -0
  147. package/dist/permissions/persistence.d.ts.map +1 -0
  148. package/dist/permissions/persistence.js +248 -0
  149. package/dist/permissions/persistence.js.map +1 -0
  150. package/dist/permissions/persistence.test.d.ts +5 -0
  151. package/dist/permissions/persistence.test.d.ts.map +1 -0
  152. package/dist/permissions/persistence.test.js +171 -0
  153. package/dist/permissions/persistence.test.js.map +1 -0
  154. package/dist/permissions/prompt-matcher.d.ts +64 -0
  155. package/dist/permissions/prompt-matcher.d.ts.map +1 -0
  156. package/dist/permissions/prompt-matcher.js +415 -0
  157. package/dist/permissions/prompt-matcher.js.map +1 -0
  158. package/dist/permissions/prompt-matcher.test.d.ts +5 -0
  159. package/dist/permissions/prompt-matcher.test.d.ts.map +1 -0
  160. package/dist/permissions/prompt-matcher.test.js +107 -0
  161. package/dist/permissions/prompt-matcher.test.js.map +1 -0
  162. package/dist/permissions/types.d.ts +157 -0
  163. package/dist/permissions/types.d.ts.map +1 -1
  164. package/dist/permissions/types.js +43 -8
  165. package/dist/permissions/types.js.map +1 -1
  166. package/dist/prompts/index.d.ts +92 -0
  167. package/dist/prompts/index.d.ts.map +1 -0
  168. package/dist/prompts/index.js +241 -0
  169. package/dist/prompts/index.js.map +1 -0
  170. package/dist/providers/gemini.d.ts.map +1 -1
  171. package/dist/providers/gemini.js +14 -3
  172. package/dist/providers/gemini.js.map +1 -1
  173. package/dist/providers/index.d.ts +5 -3
  174. package/dist/providers/index.d.ts.map +1 -1
  175. package/dist/providers/index.js +13 -1
  176. package/dist/providers/index.js.map +1 -1
  177. package/dist/providers/registry.d.ts +66 -0
  178. package/dist/providers/registry.d.ts.map +1 -0
  179. package/dist/providers/registry.js +158 -0
  180. package/dist/providers/registry.js.map +1 -0
  181. package/dist/providers/search/brave.d.ts +14 -0
  182. package/dist/providers/search/brave.d.ts.map +1 -0
  183. package/dist/providers/search/brave.js +87 -0
  184. package/dist/providers/search/brave.js.map +1 -0
  185. package/dist/providers/search/exa.d.ts +12 -0
  186. package/dist/providers/search/exa.d.ts.map +1 -0
  187. package/dist/providers/search/exa.js +158 -0
  188. package/dist/providers/search/exa.js.map +1 -0
  189. package/dist/providers/search/index.d.ts +31 -0
  190. package/dist/providers/search/index.d.ts.map +1 -0
  191. package/dist/providers/search/index.js +75 -0
  192. package/dist/providers/search/index.js.map +1 -0
  193. package/dist/providers/search/serper.d.ts +14 -0
  194. package/dist/providers/search/serper.d.ts.map +1 -0
  195. package/dist/providers/search/serper.js +87 -0
  196. package/dist/providers/search/serper.js.map +1 -0
  197. package/dist/providers/search/types.d.ts +21 -0
  198. package/dist/providers/search/types.d.ts.map +1 -0
  199. package/dist/providers/search/types.js +5 -0
  200. package/dist/providers/search/types.js.map +1 -0
  201. package/dist/providers/store.d.ts +104 -0
  202. package/dist/providers/store.d.ts.map +1 -0
  203. package/dist/providers/store.js +171 -0
  204. package/dist/providers/store.js.map +1 -0
  205. package/dist/providers/types.d.ts +7 -1
  206. package/dist/providers/types.d.ts.map +1 -1
  207. package/dist/providers/vertex-ai.d.ts +33 -0
  208. package/dist/providers/vertex-ai.d.ts.map +1 -0
  209. package/dist/providers/vertex-ai.js +407 -0
  210. package/dist/providers/vertex-ai.js.map +1 -0
  211. package/dist/tools/builtin/bash.d.ts.map +1 -1
  212. package/dist/tools/builtin/bash.js +2 -1
  213. package/dist/tools/builtin/bash.js.map +1 -1
  214. package/dist/tools/builtin/edit.d.ts.map +1 -1
  215. package/dist/tools/builtin/edit.js +2 -1
  216. package/dist/tools/builtin/edit.js.map +1 -1
  217. package/dist/tools/builtin/glob.d.ts.map +1 -1
  218. package/dist/tools/builtin/glob.js +2 -1
  219. package/dist/tools/builtin/glob.js.map +1 -1
  220. package/dist/tools/builtin/grep.d.ts.map +1 -1
  221. package/dist/tools/builtin/grep.js +2 -1
  222. package/dist/tools/builtin/grep.js.map +1 -1
  223. package/dist/tools/builtin/read.d.ts.map +1 -1
  224. package/dist/tools/builtin/read.js +2 -1
  225. package/dist/tools/builtin/read.js.map +1 -1
  226. package/dist/tools/builtin/todowrite.d.ts +15 -0
  227. package/dist/tools/builtin/todowrite.d.ts.map +1 -0
  228. package/dist/tools/builtin/todowrite.js +88 -0
  229. package/dist/tools/builtin/todowrite.js.map +1 -0
  230. package/dist/tools/builtin/webfetch.d.ts +20 -0
  231. package/dist/tools/builtin/webfetch.d.ts.map +1 -0
  232. package/dist/tools/builtin/webfetch.js +228 -0
  233. package/dist/tools/builtin/webfetch.js.map +1 -0
  234. package/dist/tools/builtin/websearch.d.ts +17 -0
  235. package/dist/tools/builtin/websearch.d.ts.map +1 -0
  236. package/dist/tools/builtin/websearch.js +87 -0
  237. package/dist/tools/builtin/websearch.js.map +1 -0
  238. package/dist/tools/builtin/write.d.ts.map +1 -1
  239. package/dist/tools/builtin/write.js +2 -1
  240. package/dist/tools/builtin/write.js.map +1 -1
  241. package/dist/tools/index.d.ts +18 -0
  242. package/dist/tools/index.d.ts.map +1 -1
  243. package/dist/tools/index.js +28 -2
  244. package/dist/tools/index.js.map +1 -1
  245. package/dist/tools/types.d.ts +41 -0
  246. package/dist/tools/types.d.ts.map +1 -1
  247. package/dist/tools/types.js +16 -0
  248. package/dist/tools/types.js.map +1 -1
  249. package/dist/tools/utils/ssrf.d.ts +18 -0
  250. package/dist/tools/utils/ssrf.d.ts.map +1 -0
  251. package/dist/tools/utils/ssrf.js +70 -0
  252. package/dist/tools/utils/ssrf.js.map +1 -0
  253. package/docs/README.md +5 -4
  254. package/docs/config-system-comparison.md +707 -0
  255. package/docs/memory-system.md +238 -0
  256. package/docs/permissions.md +368 -0
  257. package/docs/proposals/0001-web-fetch-tool.md +32 -2
  258. package/docs/proposals/0002-web-search-tool.md +59 -2
  259. package/docs/proposals/0005-todo-system.md +350 -85
  260. package/docs/proposals/0006-memory-system.md +11 -10
  261. package/docs/proposals/0012-ask-user-question.md +941 -206
  262. package/docs/proposals/0023-permission-enhancements.md +61 -2
  263. package/docs/proposals/0041-configuration-system.md +587 -0
  264. package/docs/proposals/0042-prompt-optimization.md +866 -0
  265. package/docs/proposals/README.md +8 -6
  266. package/docs/providers.md +220 -0
  267. package/jest.config.js +26 -0
  268. package/package.json +14 -3
  269. package/src/agent/agent.ts +120 -18
  270. package/src/agent/types.ts +9 -1
  271. package/src/cli/components/App.tsx +369 -47
  272. package/src/cli/components/CommandSuggestions.tsx +3 -0
  273. package/src/cli/components/Header.tsx +11 -17
  274. package/src/cli/components/Logo.tsx +76 -9
  275. package/src/cli/components/Messages.tsx +146 -38
  276. package/src/cli/components/ModelSelector.tsx +169 -52
  277. package/src/cli/components/PermissionPrompt.tsx +388 -0
  278. package/src/cli/components/ProviderManager.tsx +534 -0
  279. package/src/cli/components/Spinner.tsx +138 -25
  280. package/src/cli/components/TodoList.tsx +54 -0
  281. package/src/cli/components/index.ts +6 -0
  282. package/src/cli/components/markdown.ts +157 -0
  283. package/src/cli/components/theme.ts +7 -0
  284. package/src/cli/index.tsx +76 -13
  285. package/src/config/index.ts +79 -4
  286. package/src/config/levels.test.ts +163 -0
  287. package/src/config/levels.ts +285 -0
  288. package/src/config/loader.test.ts +120 -0
  289. package/src/config/loader.ts +178 -0
  290. package/src/config/manager.test.ts +215 -0
  291. package/src/config/manager.ts +328 -40
  292. package/src/config/merger.test.ts +360 -0
  293. package/src/config/merger.ts +221 -0
  294. package/src/config/providers-config.ts +85 -0
  295. package/src/config/test-utils.ts +79 -0
  296. package/src/config/types.ts +186 -9
  297. package/src/memory/import-resolver.test.ts +117 -0
  298. package/src/memory/import-resolver.ts +149 -0
  299. package/src/memory/index.ts +11 -0
  300. package/src/memory/init-prompt.ts +113 -0
  301. package/src/memory/memory-manager.test.ts +198 -0
  302. package/src/memory/memory-manager.ts +716 -0
  303. package/src/memory/rules-parser.test.ts +182 -0
  304. package/src/memory/rules-parser.ts +82 -0
  305. package/src/memory/test-utils.ts +60 -0
  306. package/src/memory/types.ts +119 -0
  307. package/src/permissions/audit.ts +284 -0
  308. package/src/permissions/index.ts +20 -1
  309. package/src/permissions/manager.test.ts +260 -0
  310. package/src/permissions/manager.ts +592 -40
  311. package/src/permissions/persistence.test.ts +220 -0
  312. package/src/permissions/persistence.ts +301 -0
  313. package/src/permissions/prompt-matcher.test.ts +213 -0
  314. package/src/permissions/prompt-matcher.ts +472 -0
  315. package/src/permissions/types.ts +236 -8
  316. package/src/prompts/index.test.ts +279 -0
  317. package/src/prompts/index.ts +306 -0
  318. package/src/prompts/system/anthropic.txt +29 -0
  319. package/src/prompts/system/base.txt +124 -0
  320. package/src/prompts/system/gemini.txt +35 -0
  321. package/src/prompts/system/generic.txt +128 -0
  322. package/src/prompts/system/openai.txt +29 -0
  323. package/src/prompts/tools/bash.txt +60 -0
  324. package/src/prompts/tools/edit.txt +29 -0
  325. package/src/prompts/tools/glob.txt +35 -0
  326. package/src/prompts/tools/grep.txt +43 -0
  327. package/src/prompts/tools/read.txt +22 -0
  328. package/src/prompts/tools/todowrite.txt +71 -0
  329. package/src/prompts/tools/webfetch.txt +34 -0
  330. package/src/prompts/tools/websearch.txt +41 -0
  331. package/src/prompts/tools/write.txt +23 -0
  332. package/src/providers/gemini.ts +20 -4
  333. package/src/providers/index.ts +18 -3
  334. package/src/providers/registry.ts +198 -0
  335. package/src/providers/search/brave.ts +132 -0
  336. package/src/providers/search/exa.ts +217 -0
  337. package/src/providers/search/index.ts +79 -0
  338. package/src/providers/search/serper.ts +133 -0
  339. package/src/providers/search/types.ts +24 -0
  340. package/src/providers/store.ts +216 -0
  341. package/src/providers/types.ts +9 -1
  342. package/src/providers/vertex-ai.ts +594 -0
  343. package/src/tools/builtin/bash.ts +2 -1
  344. package/src/tools/builtin/edit.ts +2 -1
  345. package/src/tools/builtin/glob.ts +2 -1
  346. package/src/tools/builtin/grep.ts +2 -1
  347. package/src/tools/builtin/read.ts +2 -1
  348. package/src/tools/builtin/todowrite.ts +102 -0
  349. package/src/tools/builtin/webfetch.ts +261 -0
  350. package/src/tools/builtin/websearch.ts +103 -0
  351. package/src/tools/builtin/write.ts +2 -1
  352. package/src/tools/index.ts +28 -2
  353. package/src/tools/types.ts +32 -0
  354. package/src/tools/utils/ssrf.ts +79 -0
  355. package/tsconfig.json +1 -1
  356. package/CLAUDE.md +0 -70
@@ -1,44 +1,311 @@
1
1
  /**
2
- * Permission Manager - Controls tool execution permissions
2
+ * Permission Manager - Enhanced permission control with pattern matching,
3
+ * prompt-based approvals, persistence, and audit logging.
4
+ *
5
+ * Claude Code compatible design with:
6
+ * - Pattern-based rules (e.g., "Bash(git add:*)")
7
+ * - Prompt-based permissions (e.g., { tool: "Bash", prompt: "run tests" })
8
+ * - Session/project/global scopes
9
+ * - Persistent allowlists
10
+ * - Audit trail
3
11
  */
4
12
 
5
- import type { PermissionConfig, PermissionMode } from './types.js';
6
- import { DEFAULT_PERMISSION_CONFIG } from './types.js';
13
+ import type {
14
+ PermissionConfig,
15
+ PermissionContext,
16
+ PermissionDecision,
17
+ PermissionMode,
18
+ PermissionRule,
19
+ PromptPermission,
20
+ ApprovalAction,
21
+ ApprovalSuggestion,
22
+ ConfirmCallback,
23
+ SimpleConfirmCallback,
24
+ PermissionSettings,
25
+ AuditDecision,
26
+ } from './types.js';
27
+ import {
28
+ DEFAULT_PERMISSION_CONFIG,
29
+ DEFAULT_SUGGESTIONS,
30
+ } from './types.js';
31
+ import { PromptMatcher, matchesPatternString } from './prompt-matcher.js';
32
+ import { PermissionPersistence } from './persistence.js';
33
+ import { PermissionAudit } from './audit.js';
7
34
 
8
- export type ConfirmCallback = (
9
- tool: string,
10
- input: unknown
11
- ) => Promise<boolean>;
35
+ /**
36
+ * Session approval cache entry
37
+ */
38
+ interface SessionApproval {
39
+ tool: string;
40
+ pattern?: string;
41
+ approvedAt: Date;
42
+ }
12
43
 
44
+ /**
45
+ * Enhanced Permission Manager
46
+ */
13
47
  export class PermissionManager {
14
48
  private config: PermissionConfig;
49
+ private promptMatcher: PromptMatcher;
50
+ private persistence: PermissionPersistence;
51
+ private audit: PermissionAudit;
52
+
53
+ // Session-scoped caches
54
+ private sessionApprovals: Map<string, SessionApproval> = new Map();
55
+ private sessionRejections: Set<string> = new Set();
56
+
57
+ // Callbacks
15
58
  private confirmCallback?: ConfirmCallback;
16
- private approvedTools: Set<string> = new Set();
59
+ private simpleConfirmCallback?: SimpleConfirmCallback;
60
+ private saveRuleCallback?: (tool: string, pattern?: string) => Promise<void>;
61
+
62
+ // Current context
63
+ private sessionId?: string;
64
+ private projectPath?: string;
17
65
 
18
- constructor(config?: Partial<PermissionConfig>) {
66
+ constructor(options: {
67
+ config?: Partial<PermissionConfig>;
68
+ projectPath?: string;
69
+ enableAudit?: boolean;
70
+ } = {}) {
19
71
  this.config = {
20
72
  ...DEFAULT_PERMISSION_CONFIG,
21
- ...config,
73
+ ...options.config,
74
+ rules: [
75
+ ...DEFAULT_PERMISSION_CONFIG.rules,
76
+ ...(options.config?.rules ?? []),
77
+ ],
22
78
  };
79
+
80
+ this.projectPath = options.projectPath;
81
+ this.promptMatcher = new PromptMatcher();
82
+ this.persistence = new PermissionPersistence(options.projectPath);
83
+ this.audit = new PermissionAudit({
84
+ persistToFile: options.enableAudit ?? false,
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Initialize - load persisted rules and settings
90
+ */
91
+ async initialize(settings?: PermissionSettings): Promise<void> {
92
+ // Load persisted rules
93
+ const persistedRules = await this.persistence.getAllRules();
94
+ const runtimeRules = this.persistence.persistedToRuntime(persistedRules);
95
+ this.config.rules.push(...runtimeRules);
96
+
97
+ // Parse settings permissions
98
+ if (settings) {
99
+ const settingsRules = this.persistence.parseSettingsPermissions(settings);
100
+ this.config.rules.push(...settingsRules);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Set session ID for tracking
106
+ */
107
+ setSessionId(sessionId: string): void {
108
+ this.sessionId = sessionId;
23
109
  }
24
110
 
25
111
  /**
26
- * Set the confirmation callback
112
+ * Set enhanced confirmation callback
27
113
  */
28
114
  setConfirmCallback(callback: ConfirmCallback): void {
29
115
  this.confirmCallback = callback;
30
116
  }
31
117
 
32
118
  /**
33
- * Get the permission mode for a tool
119
+ * Set simple yes/no confirmation callback (backward compatible)
120
+ */
121
+ setSimpleConfirmCallback(callback: SimpleConfirmCallback): void {
122
+ this.simpleConfirmCallback = callback;
123
+ }
124
+
125
+ /**
126
+ * Set callback to save permission rules to settings
127
+ * This allows integration with SettingsManager for settings.local.json persistence
128
+ */
129
+ setSaveRuleCallback(callback: (tool: string, pattern?: string) => Promise<void>): void {
130
+ this.saveRuleCallback = callback;
131
+ }
132
+
133
+ /**
134
+ * Add prompt-based permissions (Claude Code ExitPlanMode style)
135
+ */
136
+ addAllowedPrompts(prompts: PromptPermission[]): void {
137
+ this.config.allowedPrompts.push(...prompts);
138
+ }
139
+
140
+ /**
141
+ * Clear prompt-based permissions
142
+ */
143
+ clearAllowedPrompts(): void {
144
+ this.config.allowedPrompts = [];
145
+ }
146
+
147
+ /**
148
+ * Get current allowed prompts
149
+ */
150
+ getAllowedPrompts(): PromptPermission[] {
151
+ return [...this.config.allowedPrompts];
152
+ }
153
+
154
+ /**
155
+ * Get all rules
156
+ */
157
+ getRules(): PermissionRule[] {
158
+ return [...this.config.rules];
159
+ }
160
+
161
+ /**
162
+ * Check permission (without prompting user)
163
+ *
164
+ * Flow matches Claude Code official design:
165
+ * 1. DENY rules → block immediately
166
+ * 2. ALLOW rules → auto-approve (includes prompt-based & session cache)
167
+ * 3. ASK rules → force prompt
168
+ * 4. Default behavior (read-only → auto, write → prompt)
169
+ */
170
+ async checkPermission(context: PermissionContext): Promise<PermissionDecision> {
171
+ const { tool, input } = context;
172
+
173
+ // ========================================================================
174
+ // Step 1: Check DENY rules (highest priority - block immediately)
175
+ // ========================================================================
176
+ const denyRule = this.findMatchingRule(context, 'deny');
177
+ if (denyRule) {
178
+ await this.logAudit(context, 'denied', `Denied by rule: ${this.describeRule(denyRule)}`);
179
+ return {
180
+ allowed: false,
181
+ reason: `Blocked: ${denyRule.description ?? denyRule.pattern ?? tool}`,
182
+ matchedRule: denyRule,
183
+ requiresConfirmation: false,
184
+ };
185
+ }
186
+
187
+ // ========================================================================
188
+ // Step 2: Check ALLOW rules (auto-approve if matched)
189
+ // Includes: settings.allow[], prompt-based permissions, session cache
190
+ // ========================================================================
191
+
192
+ // 2a. Check settings.allow rules
193
+ const autoRule = this.findMatchingRule(context, 'auto');
194
+ if (autoRule) {
195
+ await this.logAudit(context, 'allowed', `Auto-approved: ${this.describeRule(autoRule)}`);
196
+ return {
197
+ allowed: true,
198
+ reason: 'Auto-approved',
199
+ matchedRule: autoRule,
200
+ requiresConfirmation: false,
201
+ };
202
+ }
203
+
204
+ // 2b. Check prompt-based permissions (Plan mode allowedPrompts)
205
+ const promptMatch = this.matchPrompt(tool, input);
206
+ if (promptMatch) {
207
+ await this.logAudit(context, 'allowed', `Prompt match: ${promptMatch.prompt}`);
208
+ return {
209
+ allowed: true,
210
+ reason: `Approved: ${promptMatch.prompt}`,
211
+ matchedRule: promptMatch,
212
+ requiresConfirmation: false,
213
+ };
214
+ }
215
+
216
+ // 2c. Check session approval cache
217
+ const cacheKey = this.getCacheKey(tool, input);
218
+ if (this.sessionApprovals.has(cacheKey)) {
219
+ await this.logAudit(context, 'allowed', 'Session cache hit');
220
+ return {
221
+ allowed: true,
222
+ reason: 'Previously approved',
223
+ requiresConfirmation: false,
224
+ };
225
+ }
226
+
227
+ // ========================================================================
228
+ // Step 3: Check ASK rules (force confirmation)
229
+ // ========================================================================
230
+ const askRule = this.findMatchingRule(context, 'confirm');
231
+ if (askRule) {
232
+ await this.logAudit(context, 'confirmed', `ASK rule matched: ${this.describeRule(askRule)}`);
233
+ return {
234
+ allowed: false,
235
+ reason: `Requires confirmation: ${askRule.description ?? askRule.pattern ?? tool}`,
236
+ matchedRule: askRule,
237
+ requiresConfirmation: true,
238
+ suggestions: this.getSuggestions(context),
239
+ };
240
+ }
241
+
242
+ // ========================================================================
243
+ // Step 4: Default behavior (read-only → auto, write → prompt)
244
+ // ========================================================================
245
+
246
+ // Check session rejection cache
247
+ if (this.sessionRejections.has(cacheKey)) {
248
+ await this.logAudit(context, 'denied', 'Session rejection cache');
249
+ return {
250
+ allowed: false,
251
+ reason: 'Previously denied',
252
+ requiresConfirmation: false,
253
+ };
254
+ }
255
+
256
+ // Default: requires confirmation for write operations
257
+ return {
258
+ allowed: false,
259
+ reason: 'Requires approval',
260
+ requiresConfirmation: true,
261
+ suggestions: this.getSuggestions(context),
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Request permission (prompts user if needed)
267
+ */
268
+ async requestPermission(tool: string, input: unknown): Promise<boolean> {
269
+ const context: PermissionContext = {
270
+ tool,
271
+ input,
272
+ sessionId: this.sessionId,
273
+ projectPath: this.projectPath,
274
+ };
275
+
276
+ const decision = await this.checkPermission(context);
277
+
278
+ if (decision.allowed) {
279
+ return true;
280
+ }
281
+
282
+ if (!decision.requiresConfirmation) {
283
+ return false;
284
+ }
285
+
286
+ // Prompt user for confirmation
287
+ const action = await this.promptUser(tool, input, decision.suggestions);
288
+
289
+ return this.handleApprovalAction(action, context);
290
+ }
291
+
292
+ /**
293
+ * Backward-compatible check method
294
+ */
295
+ async check(tool: string, input: unknown): Promise<boolean> {
296
+ return this.requestPermission(tool, input);
297
+ }
298
+
299
+ /**
300
+ * Get permission mode for a tool (for simple queries)
34
301
  */
35
302
  getModeForTool(tool: string): PermissionMode {
36
303
  for (const rule of this.config.rules) {
37
304
  if (typeof rule.tool === 'string') {
38
- if (rule.tool === tool) {
305
+ if (rule.tool === tool && !rule.pattern) {
39
306
  return rule.mode;
40
307
  }
41
- } else if (rule.tool.test(tool)) {
308
+ } else if (rule.tool.test(tool) && !rule.pattern) {
42
309
  return rule.mode;
43
310
  }
44
311
  }
@@ -46,35 +313,288 @@ export class PermissionManager {
46
313
  }
47
314
 
48
315
  /**
49
- * Check if a tool can be executed
316
+ * Approve a tool for this session
50
317
  */
51
- async checkPermission(tool: string, input: unknown): Promise<boolean> {
52
- const mode = this.getModeForTool(tool);
318
+ approveForSession(tool: string, pattern?: string): void {
319
+ const key = pattern ? `${tool}:${pattern}` : tool;
320
+ this.sessionApprovals.set(key, {
321
+ tool,
322
+ pattern,
323
+ approvedAt: new Date(),
324
+ });
325
+ }
53
326
 
54
- switch (mode) {
55
- case 'auto':
56
- return true;
327
+ /**
328
+ * Add a persistent allow rule
329
+ */
330
+ async addAllowRule(
331
+ tool: string,
332
+ pattern?: string,
333
+ scope: 'project' | 'global' = 'global'
334
+ ): Promise<void> {
335
+ await this.persistence.addRule(tool, 'auto', {
336
+ pattern,
337
+ scope,
338
+ description: `User approved: ${tool}${pattern ? `(${pattern})` : ''}`,
339
+ });
57
340
 
58
- case 'deny':
59
- return false;
341
+ // Also add to runtime config
342
+ this.config.rules.push({
343
+ tool,
344
+ mode: 'auto',
345
+ pattern,
346
+ scope,
347
+ });
348
+ }
60
349
 
61
- case 'confirm':
62
- // Check if already approved this session
63
- const key = `${tool}:${JSON.stringify(input)}`;
64
- if (this.approvedTools.has(key)) {
65
- return true;
350
+ /**
351
+ * Add a persistent deny rule
352
+ */
353
+ async addDenyRule(
354
+ tool: string,
355
+ pattern?: string,
356
+ scope: 'project' | 'global' = 'global'
357
+ ): Promise<void> {
358
+ await this.persistence.addRule(tool, 'deny', {
359
+ pattern,
360
+ scope,
361
+ description: `User blocked: ${tool}${pattern ? `(${pattern})` : ''}`,
362
+ });
363
+
364
+ // Also add to runtime config
365
+ this.config.rules.push({
366
+ tool,
367
+ mode: 'deny',
368
+ pattern,
369
+ scope,
370
+ });
371
+ }
372
+
373
+ /**
374
+ * Clear session approvals
375
+ */
376
+ clearSessionApprovals(): void {
377
+ this.sessionApprovals.clear();
378
+ this.sessionRejections.clear();
379
+ }
380
+
381
+ /**
382
+ * Get audit log
383
+ */
384
+ getAuditLog(count?: number): ReturnType<PermissionAudit['getRecent']> {
385
+ return this.audit.getRecent(count);
386
+ }
387
+
388
+ /**
389
+ * Get audit statistics
390
+ */
391
+ getAuditStats(): ReturnType<PermissionAudit['getStats']> {
392
+ return this.audit.getStats();
393
+ }
394
+
395
+ /**
396
+ * Get persistence manager (for direct access)
397
+ */
398
+ getPersistence(): PermissionPersistence {
399
+ return this.persistence;
400
+ }
401
+
402
+ // ============================================================================
403
+ // Private Methods
404
+ // ============================================================================
405
+
406
+ /**
407
+ * Find a matching rule for the given context and mode
408
+ */
409
+ private findMatchingRule(
410
+ context: PermissionContext,
411
+ mode: PermissionMode
412
+ ): PermissionRule | undefined {
413
+ for (const rule of this.config.rules) {
414
+ if (rule.mode !== mode) continue;
415
+ if (!this.matchesTool(rule.tool, context.tool)) continue;
416
+
417
+ // If rule has pattern, check it (pass tool name for path matching)
418
+ if (rule.pattern) {
419
+ if (!this.matchesPattern(rule.pattern, context.input, context.tool)) {
420
+ continue;
66
421
  }
422
+ }
423
+
424
+ // Check expiration
425
+ if (rule.expiresAt && new Date() > rule.expiresAt) {
426
+ continue;
427
+ }
428
+
429
+ return rule;
430
+ }
67
431
 
68
- if (!this.confirmCallback) {
69
- // No callback, auto-approve in non-interactive mode
70
- return true;
432
+ return undefined;
433
+ }
434
+
435
+ /**
436
+ * Check if tool matches rule
437
+ */
438
+ private matchesTool(rulePattern: string | RegExp, tool: string): boolean {
439
+ if (typeof rulePattern === 'string') {
440
+ return rulePattern === tool || rulePattern === '*';
441
+ }
442
+ return rulePattern.test(tool);
443
+ }
444
+
445
+ /**
446
+ * Check if input matches pattern
447
+ */
448
+ private matchesPattern(pattern: string | RegExp, input: unknown, tool?: string): boolean {
449
+ if (typeof pattern === 'string') {
450
+ return matchesPatternString(pattern, input, tool);
451
+ }
452
+
453
+ const inputStr = typeof input === 'string'
454
+ ? input
455
+ : JSON.stringify(input);
456
+
457
+ return pattern.test(inputStr);
458
+ }
459
+
460
+ /**
461
+ * Match against prompt-based permissions
462
+ */
463
+ private matchPrompt(tool: string, input: unknown): PromptPermission | null {
464
+ for (const permission of this.config.allowedPrompts) {
465
+ if (permission.tool !== tool) continue;
466
+
467
+ if (this.promptMatcher.matches(permission.prompt, input)) {
468
+ return permission;
469
+ }
470
+ }
471
+ return null;
472
+ }
473
+
474
+ /**
475
+ * Generate cache key for session approvals
476
+ */
477
+ private getCacheKey(tool: string, input: unknown): string {
478
+ // For Bash, use the command as the key
479
+ if (tool === 'Bash' && input && typeof input === 'object') {
480
+ const cmd = (input as { command?: string }).command;
481
+ if (cmd) {
482
+ // Use first few tokens as pattern
483
+ const tokens = cmd.split(/\s+/).slice(0, 3).join(' ');
484
+ return `${tool}:${tokens}`;
485
+ }
486
+ }
487
+
488
+ // For file operations, use the path
489
+ if (['Read', 'Write', 'Edit', 'Glob'].includes(tool)) {
490
+ if (input && typeof input === 'object') {
491
+ const path = (input as { file_path?: string; path?: string }).file_path
492
+ ?? (input as { file_path?: string; path?: string }).path;
493
+ if (path) {
494
+ return `${tool}:${path}`;
71
495
  }
496
+ }
497
+ }
498
+
499
+ // Default: use full input JSON
500
+ return `${tool}:${JSON.stringify(input)}`;
501
+ }
502
+
503
+ /**
504
+ * Get approval suggestions for a context
505
+ */
506
+ private getSuggestions(context: PermissionContext): ApprovalSuggestion[] {
507
+ // Start with default suggestions
508
+ const suggestions = [...DEFAULT_SUGGESTIONS];
509
+
510
+ // Customize "Always allow" label based on tool/input
511
+ const alwaysIndex = suggestions.findIndex((s) => s.action === 'allow_always');
512
+ if (alwaysIndex >= 0 && context.tool === 'Bash') {
513
+ const input = context.input as { command?: string };
514
+ if (input?.command) {
515
+ const prefix = input.command.split(/\s+/).slice(0, 2).join(' ');
516
+ suggestions[alwaysIndex] = {
517
+ ...suggestions[alwaysIndex],
518
+ label: `Always allow "${prefix}*"`,
519
+ };
520
+ }
521
+ }
522
+
523
+ return suggestions;
524
+ }
525
+
526
+ /**
527
+ * Prompt user for approval
528
+ */
529
+ private async promptUser(
530
+ tool: string,
531
+ input: unknown,
532
+ suggestions?: ApprovalSuggestion[]
533
+ ): Promise<ApprovalAction> {
534
+ const effectiveSuggestions = suggestions ?? DEFAULT_SUGGESTIONS;
535
+
536
+ // Use enhanced callback if available
537
+ if (this.confirmCallback) {
538
+ return this.confirmCallback(tool, input, effectiveSuggestions);
539
+ }
540
+
541
+ // Fall back to simple callback
542
+ if (this.simpleConfirmCallback) {
543
+ const confirmed = await this.simpleConfirmCallback(tool, input);
544
+ return confirmed ? 'allow_session' : 'deny';
545
+ }
546
+
547
+ // No callback - auto-approve (non-interactive mode)
548
+ return 'allow_once';
549
+ }
550
+
551
+ /**
552
+ * Handle the user's approval action
553
+ */
554
+ private async handleApprovalAction(
555
+ action: ApprovalAction,
556
+ context: PermissionContext
557
+ ): Promise<boolean> {
558
+ const { tool, input } = context;
559
+ const cacheKey = this.getCacheKey(tool, input);
560
+
561
+ switch (action) {
562
+ case 'allow_once':
563
+ await this.logAudit(context, 'confirmed', 'User approved once');
564
+ return true;
72
565
 
73
- const approved = await this.confirmCallback(tool, input);
74
- if (approved) {
75
- this.approvedTools.add(key);
566
+ case 'allow_session':
567
+ this.sessionApprovals.set(cacheKey, {
568
+ tool,
569
+ approvedAt: new Date(),
570
+ });
571
+ await this.logAudit(context, 'confirmed', 'User approved for session');
572
+ return true;
573
+
574
+ case 'allow_always':
575
+ // Extract pattern for persistent rule
576
+ const pattern = this.extractPattern(tool, input);
577
+ // Use saveRuleCallback if available (saves to settings.local.json)
578
+ // Otherwise fallback to addAllowRule (saves to permissions.json)
579
+ if (this.saveRuleCallback) {
580
+ await this.saveRuleCallback(tool, pattern);
581
+ } else {
582
+ await this.addAllowRule(tool, pattern, 'project');
76
583
  }
77
- return approved;
584
+ // Also add to runtime config for immediate effect
585
+ this.config.rules.push({
586
+ tool,
587
+ mode: 'auto',
588
+ pattern,
589
+ scope: 'project',
590
+ });
591
+ await this.logAudit(context, 'confirmed', 'User approved for project');
592
+ return true;
593
+
594
+ case 'deny':
595
+ this.sessionRejections.add(cacheKey);
596
+ await this.logAudit(context, 'rejected', 'User denied');
597
+ return false;
78
598
 
79
599
  default:
80
600
  return false;
@@ -82,16 +602,48 @@ export class PermissionManager {
82
602
  }
83
603
 
84
604
  /**
85
- * Approve a tool for this session
605
+ * Extract a pattern from tool input for persistent rules
86
606
  */
87
- approveToolForSession(tool: string): void {
88
- this.approvedTools.add(tool);
607
+ private extractPattern(tool: string, input: unknown): string | undefined {
608
+ if (tool === 'Bash' && input && typeof input === 'object') {
609
+ const cmd = (input as { command?: string }).command;
610
+ if (cmd) {
611
+ // Use first two tokens + wildcard
612
+ const tokens = cmd.split(/\s+/).slice(0, 2);
613
+ return tokens.join(' ') + ':*';
614
+ }
615
+ }
616
+
617
+ return undefined;
89
618
  }
90
619
 
91
620
  /**
92
- * Clear all session approvals
621
+ * Describe a rule for logging
93
622
  */
94
- clearApprovals(): void {
95
- this.approvedTools.clear();
623
+ private describeRule(rule: PermissionRule): string {
624
+ const parts = [rule.tool.toString()];
625
+ if (rule.pattern) {
626
+ parts.push(`(${rule.pattern})`);
627
+ }
628
+ if (rule.description) {
629
+ parts.push(`- ${rule.description}`);
630
+ }
631
+ return parts.join('');
632
+ }
633
+
634
+ /**
635
+ * Log audit entry
636
+ */
637
+ private async logAudit(
638
+ context: PermissionContext,
639
+ decision: AuditDecision,
640
+ reason: string
641
+ ): Promise<void> {
642
+ await this.audit.log(context.tool, context.input, decision, reason, {
643
+ sessionId: context.sessionId,
644
+ });
96
645
  }
97
646
  }
647
+
648
+ // Re-export types and utilities
649
+ export type { ConfirmCallback, SimpleConfirmCallback };