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
@@ -0,0 +1,220 @@
1
+ /**
2
+ * PermissionPersistence Tests
3
+ */
4
+
5
+ import * as fs from 'fs/promises';
6
+ import * as path from 'path';
7
+ import * as os from 'os';
8
+ import { PermissionPersistence } from './persistence.js';
9
+
10
+ describe('PermissionPersistence', () => {
11
+ let tempDir: string;
12
+ let projectDir: string;
13
+
14
+ beforeEach(async () => {
15
+ // Create temp directories for testing
16
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gencode-perm-test-'));
17
+ projectDir = path.join(tempDir, 'project');
18
+
19
+ await fs.mkdir(projectDir, { recursive: true });
20
+ });
21
+
22
+ afterEach(async () => {
23
+ // Cleanup temp directories
24
+ await fs.rm(tempDir, { recursive: true, force: true });
25
+ });
26
+
27
+ describe('addRule', () => {
28
+ it('should add a rule to project scope', async () => {
29
+ const projectPermDir = path.join(projectDir, '.claude');
30
+ await fs.mkdir(projectPermDir, { recursive: true });
31
+
32
+ const persistence = new PermissionPersistence(projectDir);
33
+
34
+ const rule = await persistence.addRule('WebSearch', 'auto', {
35
+ scope: 'project',
36
+ });
37
+
38
+ expect(rule.id).toBeDefined();
39
+ expect(rule.tool).toBe('WebSearch');
40
+ expect(rule.mode).toBe('auto');
41
+ expect(rule.scope).toBe('project');
42
+ });
43
+
44
+ it('should add a rule with pattern', async () => {
45
+ const projectPermDir = path.join(projectDir, '.claude');
46
+ await fs.mkdir(projectPermDir, { recursive: true });
47
+
48
+ const persistence = new PermissionPersistence(projectDir);
49
+
50
+ const rule = await persistence.addRule('Bash', 'auto', {
51
+ pattern: 'git add:*',
52
+ scope: 'project',
53
+ description: 'Test rule',
54
+ });
55
+
56
+ expect(rule.pattern).toBe('git add:*');
57
+ expect(rule.description).toBe('Test rule');
58
+ });
59
+ });
60
+
61
+ describe('getRules', () => {
62
+ it('should return empty array for non-existent project file', async () => {
63
+ const projectPermDir = path.join(projectDir, '.claude');
64
+ await fs.mkdir(projectPermDir, { recursive: true });
65
+
66
+ const persistence = new PermissionPersistence(projectDir);
67
+
68
+ const rules = await persistence.getRules('project');
69
+ expect(rules).toEqual([]);
70
+ });
71
+
72
+ it('should return saved project rules', async () => {
73
+ const projectPermDir = path.join(projectDir, '.claude');
74
+ await fs.mkdir(projectPermDir, { recursive: true });
75
+
76
+ const persistence = new PermissionPersistence(projectDir);
77
+
78
+ await persistence.addRule('Bash', 'auto', {
79
+ pattern: 'npm test:*',
80
+ scope: 'project',
81
+ });
82
+
83
+ const rules = await persistence.getRules('project');
84
+ expect(rules.length).toBe(1);
85
+ expect(rules[0].tool).toBe('Bash');
86
+ });
87
+ });
88
+
89
+ describe('removeRule', () => {
90
+ it('should remove a rule by ID', async () => {
91
+ const projectPermDir = path.join(projectDir, '.claude');
92
+ await fs.mkdir(projectPermDir, { recursive: true });
93
+
94
+ const persistence = new PermissionPersistence(projectDir);
95
+
96
+ const rule = await persistence.addRule('Bash', 'auto', {
97
+ pattern: 'npm:*',
98
+ scope: 'project',
99
+ });
100
+
101
+ const removed = await persistence.removeRule(rule.id, 'project');
102
+ expect(removed).toBe(true);
103
+
104
+ const rules = await persistence.getRules('project');
105
+ expect(rules.length).toBe(0);
106
+ });
107
+
108
+ it('should return false for non-existent rule', async () => {
109
+ const projectPermDir = path.join(projectDir, '.claude');
110
+ await fs.mkdir(projectPermDir, { recursive: true });
111
+
112
+ const persistence = new PermissionPersistence(projectDir);
113
+
114
+ const removed = await persistence.removeRule('non-existent-id', 'project');
115
+ expect(removed).toBe(false);
116
+ });
117
+ });
118
+
119
+ describe('parseSettingsPermissions', () => {
120
+ it('should parse allow rules', () => {
121
+ const persistence = new PermissionPersistence(projectDir);
122
+
123
+ const rules = persistence.parseSettingsPermissions({
124
+ allow: ['Bash(git add:*)', 'WebSearch'],
125
+ });
126
+
127
+ expect(rules.length).toBe(2);
128
+ expect(rules[0].tool).toBe('Bash');
129
+ expect(rules[0].mode).toBe('auto');
130
+ expect(rules[0].pattern).toBe('git add:*');
131
+ expect(rules[1].tool).toBe('WebSearch');
132
+ expect(rules[1].mode).toBe('auto');
133
+ });
134
+
135
+ it('should parse ask rules', () => {
136
+ const persistence = new PermissionPersistence(projectDir);
137
+
138
+ const rules = persistence.parseSettingsPermissions({
139
+ ask: ['Bash(npm run:*)'],
140
+ });
141
+
142
+ expect(rules.length).toBe(1);
143
+ expect(rules[0].tool).toBe('Bash');
144
+ expect(rules[0].mode).toBe('confirm');
145
+ expect(rules[0].pattern).toBe('npm run:*');
146
+ });
147
+
148
+ it('should parse deny rules', () => {
149
+ const persistence = new PermissionPersistence(projectDir);
150
+
151
+ const rules = persistence.parseSettingsPermissions({
152
+ deny: ['Bash(rm -rf:*)'],
153
+ });
154
+
155
+ expect(rules.length).toBe(1);
156
+ expect(rules[0].tool).toBe('Bash');
157
+ expect(rules[0].mode).toBe('deny');
158
+ expect(rules[0].pattern).toBe('rm -rf:*');
159
+ });
160
+
161
+ it('should parse mixed rules', () => {
162
+ const persistence = new PermissionPersistence(projectDir);
163
+
164
+ const rules = persistence.parseSettingsPermissions({
165
+ allow: ['Bash(git:*)'],
166
+ ask: ['Bash(npm run:*)'],
167
+ deny: ['Bash(rm -rf:*)'],
168
+ });
169
+
170
+ expect(rules.length).toBe(3);
171
+
172
+ const allowRule = rules.find(r => r.pattern === 'git:*');
173
+ const askRule = rules.find(r => r.pattern === 'npm run:*');
174
+ const denyRule = rules.find(r => r.pattern === 'rm -rf:*');
175
+
176
+ expect(allowRule?.mode).toBe('auto');
177
+ expect(askRule?.mode).toBe('confirm');
178
+ expect(denyRule?.mode).toBe('deny');
179
+ });
180
+ });
181
+
182
+ describe('persistedToRuntime', () => {
183
+ it('should convert persisted rules to runtime format', () => {
184
+ const persistence = new PermissionPersistence(projectDir);
185
+
186
+ const persisted = [
187
+ {
188
+ id: 'test-id',
189
+ tool: 'Bash',
190
+ pattern: 'git:*',
191
+ mode: 'auto' as const,
192
+ scope: 'global' as const,
193
+ createdAt: new Date().toISOString(),
194
+ description: 'Test',
195
+ },
196
+ ];
197
+
198
+ const runtime = persistence.persistedToRuntime(persisted);
199
+
200
+ expect(runtime.length).toBe(1);
201
+ expect(runtime[0].tool).toBe('Bash');
202
+ expect(runtime[0].mode).toBe('auto');
203
+ expect(runtime[0].pattern).toBe('git:*');
204
+ });
205
+ });
206
+
207
+ describe('clearRules', () => {
208
+ it('should clear all rules for a scope', async () => {
209
+ const persistence = new PermissionPersistence(projectDir);
210
+
211
+ await persistence.addRule('Bash', 'auto', { scope: 'global' });
212
+ await persistence.addRule('WebSearch', 'auto', { scope: 'global' });
213
+
214
+ await persistence.clearRules('global');
215
+
216
+ const rules = await persistence.getRules('global');
217
+ expect(rules.length).toBe(0);
218
+ });
219
+ });
220
+ });
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Permission Persistence - Store and load permission rules
3
+ *
4
+ * Handles persistent storage of permission rules at:
5
+ * - Global: ~/.gencode/permissions.json
6
+ * - Project: .gencode/permissions.json
7
+ */
8
+
9
+ import * as fs from 'fs/promises';
10
+ import * as path from 'path';
11
+ import * as os from 'os';
12
+ import type {
13
+ PersistedPermissions,
14
+ PersistedRule,
15
+ PermissionRule,
16
+ PermissionScope,
17
+ PermissionMode,
18
+ PermissionSettings,
19
+ } from './types.js';
20
+ import { parsePatternString } from './prompt-matcher.js';
21
+
22
+ const PERMISSIONS_VERSION = 1;
23
+ const PERMISSIONS_FILE = 'permissions.json';
24
+ const GLOBAL_DIR = path.join(os.homedir(), '.gencode');
25
+
26
+ /**
27
+ * Permission Persistence Manager
28
+ */
29
+ export class PermissionPersistence {
30
+ private globalDir: string;
31
+ private projectDir: string | null;
32
+
33
+ constructor(projectPath?: string) {
34
+ this.globalDir = GLOBAL_DIR;
35
+ this.projectDir = projectPath ? path.join(projectPath, '.gencode') : null;
36
+ }
37
+
38
+ /**
39
+ * Generate unique rule ID
40
+ */
41
+ private generateId(): string {
42
+ return `rule_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
43
+ }
44
+
45
+ /**
46
+ * Ensure directory exists
47
+ */
48
+ private async ensureDir(dir: string): Promise<void> {
49
+ try {
50
+ await fs.mkdir(dir, { recursive: true });
51
+ } catch {
52
+ // Directory may already exist
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Get path for scope
58
+ */
59
+ private getPath(scope: PermissionScope): string {
60
+ switch (scope) {
61
+ case 'global':
62
+ return path.join(this.globalDir, PERMISSIONS_FILE);
63
+ case 'project':
64
+ if (!this.projectDir) {
65
+ throw new Error('Project path not set for project-scoped permissions');
66
+ }
67
+ return path.join(this.projectDir, PERMISSIONS_FILE);
68
+ default:
69
+ // Session scope is not persisted
70
+ throw new Error('Session-scoped permissions are not persisted');
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Load permissions from file
76
+ */
77
+ async load(scope: PermissionScope = 'global'): Promise<PersistedPermissions> {
78
+ try {
79
+ const filePath = this.getPath(scope);
80
+ const content = await fs.readFile(filePath, 'utf-8');
81
+ const data = JSON.parse(content) as PersistedPermissions;
82
+
83
+ // Migration: add version if missing
84
+ if (!data.version) {
85
+ data.version = PERMISSIONS_VERSION;
86
+ }
87
+
88
+ return data;
89
+ } catch {
90
+ // Return empty permissions
91
+ return { version: PERMISSIONS_VERSION, rules: [] };
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Save permissions to file
97
+ */
98
+ async save(permissions: PersistedPermissions, scope: PermissionScope = 'global'): Promise<void> {
99
+ const dir = scope === 'global' ? this.globalDir : this.projectDir;
100
+ if (!dir) {
101
+ throw new Error('Project path not set for project-scoped permissions');
102
+ }
103
+
104
+ await this.ensureDir(dir);
105
+ const filePath = this.getPath(scope);
106
+
107
+ await fs.writeFile(
108
+ filePath,
109
+ JSON.stringify(permissions, null, 2),
110
+ 'utf-8'
111
+ );
112
+ }
113
+
114
+ /**
115
+ * Add a new permission rule
116
+ */
117
+ async addRule(
118
+ tool: string,
119
+ mode: PermissionMode,
120
+ options: {
121
+ pattern?: string;
122
+ scope?: PermissionScope;
123
+ description?: string;
124
+ } = {}
125
+ ): Promise<PersistedRule> {
126
+ const scope = options.scope ?? 'global';
127
+
128
+ // Session rules are not persisted
129
+ if (scope === 'session') {
130
+ throw new Error('Session-scoped rules cannot be persisted');
131
+ }
132
+
133
+ const permissions = await this.load(scope);
134
+
135
+ const rule: PersistedRule = {
136
+ id: this.generateId(),
137
+ tool,
138
+ pattern: options.pattern,
139
+ mode,
140
+ scope,
141
+ createdAt: new Date().toISOString(),
142
+ description: options.description,
143
+ };
144
+
145
+ permissions.rules.push(rule);
146
+ await this.save(permissions, scope);
147
+
148
+ return rule;
149
+ }
150
+
151
+ /**
152
+ * Remove a permission rule by ID
153
+ */
154
+ async removeRule(ruleId: string, scope: PermissionScope = 'global'): Promise<boolean> {
155
+ if (scope === 'session') {
156
+ return false;
157
+ }
158
+
159
+ const permissions = await this.load(scope);
160
+ const initialLength = permissions.rules.length;
161
+ permissions.rules = permissions.rules.filter((r) => r.id !== ruleId);
162
+
163
+ if (permissions.rules.length < initialLength) {
164
+ await this.save(permissions, scope);
165
+ return true;
166
+ }
167
+
168
+ return false;
169
+ }
170
+
171
+ /**
172
+ * Get all persisted rules for a scope
173
+ */
174
+ async getRules(scope: PermissionScope = 'global'): Promise<PersistedRule[]> {
175
+ if (scope === 'session') {
176
+ return [];
177
+ }
178
+
179
+ const permissions = await this.load(scope);
180
+ return permissions.rules;
181
+ }
182
+
183
+ /**
184
+ * Get all rules (global + project)
185
+ */
186
+ async getAllRules(): Promise<PersistedRule[]> {
187
+ const globalRules = await this.getRules('global');
188
+ let projectRules: PersistedRule[] = [];
189
+
190
+ if (this.projectDir) {
191
+ try {
192
+ projectRules = await this.getRules('project');
193
+ } catch {
194
+ // Project permissions file may not exist
195
+ }
196
+ }
197
+
198
+ return [...projectRules, ...globalRules];
199
+ }
200
+
201
+ /**
202
+ * Convert persisted rules to runtime permission rules
203
+ */
204
+ persistedToRuntime(persisted: PersistedRule[]): PermissionRule[] {
205
+ return persisted.map((p) => ({
206
+ tool: p.tool,
207
+ mode: p.mode,
208
+ pattern: p.pattern,
209
+ scope: p.scope,
210
+ description: p.description,
211
+ }));
212
+ }
213
+
214
+ /**
215
+ * Parse settings-style permissions (from settings.json)
216
+ * Claude Code format: { allow: ["Bash(git add:*)"], ask: ["Bash(npm run:*)"], deny: ["Bash(rm -rf:*)"] }
217
+ */
218
+ parseSettingsPermissions(settings: PermissionSettings): PermissionRule[] {
219
+ const rules: PermissionRule[] = [];
220
+
221
+ // Parse allow rules (auto-approve)
222
+ if (settings.allow) {
223
+ for (const pattern of settings.allow) {
224
+ const parsed = parsePatternString(pattern);
225
+ if (parsed) {
226
+ rules.push({
227
+ tool: parsed.tool,
228
+ mode: 'auto',
229
+ pattern: parsed.pattern,
230
+ scope: 'global',
231
+ description: `Settings: ${pattern}`,
232
+ });
233
+ }
234
+ }
235
+ }
236
+
237
+ // Parse ask rules (require confirmation)
238
+ if (settings.ask) {
239
+ for (const pattern of settings.ask) {
240
+ const parsed = parsePatternString(pattern);
241
+ if (parsed) {
242
+ rules.push({
243
+ tool: parsed.tool,
244
+ mode: 'confirm',
245
+ pattern: parsed.pattern,
246
+ scope: 'global',
247
+ description: `Settings: ${pattern}`,
248
+ });
249
+ }
250
+ }
251
+ }
252
+
253
+ // Parse deny rules (block)
254
+ if (settings.deny) {
255
+ for (const pattern of settings.deny) {
256
+ const parsed = parsePatternString(pattern);
257
+ if (parsed) {
258
+ rules.push({
259
+ tool: parsed.tool,
260
+ mode: 'deny',
261
+ pattern: parsed.pattern,
262
+ scope: 'global',
263
+ description: `Settings: ${pattern}`,
264
+ });
265
+ }
266
+ }
267
+ }
268
+
269
+ return rules;
270
+ }
271
+
272
+ /**
273
+ * Clear all rules for a scope
274
+ */
275
+ async clearRules(scope: PermissionScope = 'global'): Promise<void> {
276
+ if (scope === 'session') {
277
+ return;
278
+ }
279
+
280
+ const permissions = await this.load(scope);
281
+ permissions.rules = [];
282
+ await this.save(permissions, scope);
283
+ }
284
+
285
+ /**
286
+ * Check if permissions file exists
287
+ */
288
+ async exists(scope: PermissionScope = 'global'): Promise<boolean> {
289
+ if (scope === 'session') {
290
+ return false;
291
+ }
292
+
293
+ try {
294
+ const filePath = this.getPath(scope);
295
+ await fs.access(filePath);
296
+ return true;
297
+ } catch {
298
+ return false;
299
+ }
300
+ }
301
+ }