gencode-ai 0.1.1 → 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 (263) hide show
  1. package/.gencode/settings.local.json +7 -0
  2. package/README.md +11 -11
  3. package/dist/agent/agent.d.ts +42 -1
  4. package/dist/agent/agent.d.ts.map +1 -1
  5. package/dist/agent/agent.js +82 -15
  6. package/dist/agent/agent.js.map +1 -1
  7. package/dist/cli/components/App.d.ts +8 -1
  8. package/dist/cli/components/App.d.ts.map +1 -1
  9. package/dist/cli/components/App.js +231 -29
  10. package/dist/cli/components/App.js.map +1 -1
  11. package/dist/cli/components/CommandSuggestions.d.ts.map +1 -1
  12. package/dist/cli/components/CommandSuggestions.js +2 -0
  13. package/dist/cli/components/CommandSuggestions.js.map +1 -1
  14. package/dist/cli/components/Header.d.ts +1 -1
  15. package/dist/cli/components/Header.d.ts.map +1 -1
  16. package/dist/cli/components/Header.js +4 -6
  17. package/dist/cli/components/Header.js.map +1 -1
  18. package/dist/cli/components/Logo.d.ts +1 -0
  19. package/dist/cli/components/Logo.d.ts.map +1 -1
  20. package/dist/cli/components/Logo.js +16 -3
  21. package/dist/cli/components/Logo.js.map +1 -1
  22. package/dist/cli/components/Messages.d.ts +4 -4
  23. package/dist/cli/components/Messages.d.ts.map +1 -1
  24. package/dist/cli/components/Messages.js +51 -25
  25. package/dist/cli/components/Messages.js.map +1 -1
  26. package/dist/cli/components/PermissionPrompt.d.ts +60 -0
  27. package/dist/cli/components/PermissionPrompt.d.ts.map +1 -0
  28. package/dist/cli/components/PermissionPrompt.js +192 -0
  29. package/dist/cli/components/PermissionPrompt.js.map +1 -0
  30. package/dist/cli/components/ProviderManager.js +3 -3
  31. package/dist/cli/components/ProviderManager.js.map +1 -1
  32. package/dist/cli/components/Spinner.d.ts +7 -2
  33. package/dist/cli/components/Spinner.d.ts.map +1 -1
  34. package/dist/cli/components/Spinner.js +116 -25
  35. package/dist/cli/components/Spinner.js.map +1 -1
  36. package/dist/cli/components/TodoList.d.ts +7 -0
  37. package/dist/cli/components/TodoList.d.ts.map +1 -0
  38. package/dist/cli/components/TodoList.js +34 -0
  39. package/dist/cli/components/TodoList.js.map +1 -0
  40. package/dist/cli/components/index.d.ts +1 -0
  41. package/dist/cli/components/index.d.ts.map +1 -1
  42. package/dist/cli/components/index.js +1 -0
  43. package/dist/cli/components/index.js.map +1 -1
  44. package/dist/cli/index.js +47 -7
  45. package/dist/cli/index.js.map +1 -1
  46. package/dist/config/index.d.ts +13 -4
  47. package/dist/config/index.d.ts.map +1 -1
  48. package/dist/config/index.js +18 -3
  49. package/dist/config/index.js.map +1 -1
  50. package/dist/config/levels.d.ts +49 -0
  51. package/dist/config/levels.d.ts.map +1 -0
  52. package/dist/config/levels.js +222 -0
  53. package/dist/config/levels.js.map +1 -0
  54. package/dist/config/loader.d.ts +46 -0
  55. package/dist/config/loader.d.ts.map +1 -0
  56. package/dist/config/loader.js +153 -0
  57. package/dist/config/loader.js.map +1 -0
  58. package/dist/config/manager.d.ts +115 -15
  59. package/dist/config/manager.d.ts.map +1 -1
  60. package/dist/config/manager.js +260 -34
  61. package/dist/config/manager.js.map +1 -1
  62. package/dist/config/manager.test.d.ts +5 -0
  63. package/dist/config/manager.test.d.ts.map +1 -0
  64. package/dist/config/manager.test.js +192 -0
  65. package/dist/config/manager.test.js.map +1 -0
  66. package/dist/config/merger.d.ts +56 -0
  67. package/dist/config/merger.d.ts.map +1 -0
  68. package/dist/config/merger.js +177 -0
  69. package/dist/config/merger.js.map +1 -0
  70. package/dist/config/test-utils.d.ts +24 -0
  71. package/dist/config/test-utils.d.ts.map +1 -0
  72. package/dist/config/test-utils.js +55 -0
  73. package/dist/config/test-utils.js.map +1 -0
  74. package/dist/config/types.d.ts +78 -9
  75. package/dist/config/types.d.ts.map +1 -1
  76. package/dist/config/types.js +52 -2
  77. package/dist/config/types.js.map +1 -1
  78. package/dist/memory/import-resolver.d.ts +46 -0
  79. package/dist/memory/import-resolver.d.ts.map +1 -0
  80. package/dist/memory/import-resolver.js +117 -0
  81. package/dist/memory/import-resolver.js.map +1 -0
  82. package/dist/memory/index.d.ts +7 -6
  83. package/dist/memory/index.d.ts.map +1 -1
  84. package/dist/memory/index.js +7 -5
  85. package/dist/memory/index.js.map +1 -1
  86. package/dist/memory/init-prompt.d.ts +22 -0
  87. package/dist/memory/init-prompt.d.ts.map +1 -0
  88. package/dist/memory/init-prompt.js +103 -0
  89. package/dist/memory/init-prompt.js.map +1 -0
  90. package/dist/memory/memory-manager.d.ts +119 -0
  91. package/dist/memory/memory-manager.d.ts.map +1 -0
  92. package/dist/memory/memory-manager.js +587 -0
  93. package/dist/memory/memory-manager.js.map +1 -0
  94. package/dist/memory/rules-parser.d.ts +38 -0
  95. package/dist/memory/rules-parser.d.ts.map +1 -0
  96. package/dist/memory/rules-parser.js +69 -0
  97. package/dist/memory/rules-parser.js.map +1 -0
  98. package/dist/memory/test-utils.d.ts +20 -0
  99. package/dist/memory/test-utils.d.ts.map +1 -0
  100. package/dist/memory/test-utils.js +44 -0
  101. package/dist/memory/test-utils.js.map +1 -0
  102. package/dist/memory/types.d.ts +70 -63
  103. package/dist/memory/types.d.ts.map +1 -1
  104. package/dist/memory/types.js +42 -2
  105. package/dist/memory/types.js.map +1 -1
  106. package/dist/permissions/audit.d.ts +82 -0
  107. package/dist/permissions/audit.d.ts.map +1 -0
  108. package/dist/permissions/audit.js +229 -0
  109. package/dist/permissions/audit.js.map +1 -0
  110. package/dist/permissions/index.d.ts +11 -1
  111. package/dist/permissions/index.d.ts.map +1 -1
  112. package/dist/permissions/index.js +15 -0
  113. package/dist/permissions/index.js.map +1 -1
  114. package/dist/permissions/manager.d.ts +149 -13
  115. package/dist/permissions/manager.d.ts.map +1 -1
  116. package/dist/permissions/manager.js +480 -35
  117. package/dist/permissions/manager.js.map +1 -1
  118. package/dist/permissions/manager.test.d.ts +5 -0
  119. package/dist/permissions/manager.test.d.ts.map +1 -0
  120. package/dist/permissions/manager.test.js +213 -0
  121. package/dist/permissions/manager.test.js.map +1 -0
  122. package/dist/permissions/persistence.d.ts +74 -0
  123. package/dist/permissions/persistence.d.ts.map +1 -0
  124. package/dist/permissions/persistence.js +248 -0
  125. package/dist/permissions/persistence.js.map +1 -0
  126. package/dist/permissions/persistence.test.d.ts +5 -0
  127. package/dist/permissions/persistence.test.d.ts.map +1 -0
  128. package/dist/permissions/persistence.test.js +171 -0
  129. package/dist/permissions/persistence.test.js.map +1 -0
  130. package/dist/permissions/prompt-matcher.d.ts +64 -0
  131. package/dist/permissions/prompt-matcher.d.ts.map +1 -0
  132. package/dist/permissions/prompt-matcher.js +415 -0
  133. package/dist/permissions/prompt-matcher.js.map +1 -0
  134. package/dist/permissions/prompt-matcher.test.d.ts +5 -0
  135. package/dist/permissions/prompt-matcher.test.d.ts.map +1 -0
  136. package/dist/permissions/prompt-matcher.test.js +107 -0
  137. package/dist/permissions/prompt-matcher.test.js.map +1 -0
  138. package/dist/permissions/types.d.ts +157 -0
  139. package/dist/permissions/types.d.ts.map +1 -1
  140. package/dist/permissions/types.js +43 -8
  141. package/dist/permissions/types.js.map +1 -1
  142. package/dist/prompts/index.d.ts +92 -0
  143. package/dist/prompts/index.d.ts.map +1 -0
  144. package/dist/prompts/index.js +241 -0
  145. package/dist/prompts/index.js.map +1 -0
  146. package/dist/tools/builtin/bash.d.ts.map +1 -1
  147. package/dist/tools/builtin/bash.js +2 -1
  148. package/dist/tools/builtin/bash.js.map +1 -1
  149. package/dist/tools/builtin/edit.d.ts.map +1 -1
  150. package/dist/tools/builtin/edit.js +2 -1
  151. package/dist/tools/builtin/edit.js.map +1 -1
  152. package/dist/tools/builtin/glob.d.ts.map +1 -1
  153. package/dist/tools/builtin/glob.js +2 -1
  154. package/dist/tools/builtin/glob.js.map +1 -1
  155. package/dist/tools/builtin/grep.d.ts.map +1 -1
  156. package/dist/tools/builtin/grep.js +2 -1
  157. package/dist/tools/builtin/grep.js.map +1 -1
  158. package/dist/tools/builtin/read.d.ts.map +1 -1
  159. package/dist/tools/builtin/read.js +2 -1
  160. package/dist/tools/builtin/read.js.map +1 -1
  161. package/dist/tools/builtin/todowrite.d.ts +15 -0
  162. package/dist/tools/builtin/todowrite.d.ts.map +1 -0
  163. package/dist/tools/builtin/todowrite.js +88 -0
  164. package/dist/tools/builtin/todowrite.js.map +1 -0
  165. package/dist/tools/builtin/webfetch.d.ts.map +1 -1
  166. package/dist/tools/builtin/webfetch.js +2 -5
  167. package/dist/tools/builtin/webfetch.js.map +1 -1
  168. package/dist/tools/builtin/websearch.d.ts.map +1 -1
  169. package/dist/tools/builtin/websearch.js +2 -16
  170. package/dist/tools/builtin/websearch.js.map +1 -1
  171. package/dist/tools/builtin/write.d.ts.map +1 -1
  172. package/dist/tools/builtin/write.js +2 -1
  173. package/dist/tools/builtin/write.js.map +1 -1
  174. package/dist/tools/index.d.ts +7 -0
  175. package/dist/tools/index.d.ts.map +1 -1
  176. package/dist/tools/index.js +4 -0
  177. package/dist/tools/index.js.map +1 -1
  178. package/dist/tools/types.d.ts +22 -0
  179. package/dist/tools/types.d.ts.map +1 -1
  180. package/dist/tools/types.js +8 -0
  181. package/dist/tools/types.js.map +1 -1
  182. package/docs/config-system-comparison.md +707 -0
  183. package/docs/memory-system.md +238 -0
  184. package/docs/permissions.md +368 -0
  185. package/docs/proposals/0005-todo-system.md +350 -85
  186. package/docs/proposals/0006-memory-system.md +11 -10
  187. package/docs/proposals/0012-ask-user-question.md +941 -206
  188. package/docs/proposals/0023-permission-enhancements.md +61 -2
  189. package/docs/proposals/0041-configuration-system.md +33 -2
  190. package/docs/proposals/0042-prompt-optimization.md +866 -0
  191. package/docs/proposals/README.md +6 -5
  192. package/jest.config.js +26 -0
  193. package/package.json +8 -2
  194. package/src/agent/agent.ts +111 -16
  195. package/src/cli/components/App.tsx +309 -36
  196. package/src/cli/components/CommandSuggestions.tsx +2 -0
  197. package/src/cli/components/Header.tsx +11 -17
  198. package/src/cli/components/Logo.tsx +76 -9
  199. package/src/cli/components/Messages.tsx +73 -53
  200. package/src/cli/components/PermissionPrompt.tsx +388 -0
  201. package/src/cli/components/ProviderManager.tsx +5 -5
  202. package/src/cli/components/Spinner.tsx +138 -25
  203. package/src/cli/components/TodoList.tsx +54 -0
  204. package/src/cli/components/index.ts +6 -0
  205. package/src/cli/index.tsx +54 -6
  206. package/src/config/index.ts +78 -4
  207. package/src/config/levels.test.ts +163 -0
  208. package/src/config/levels.ts +285 -0
  209. package/src/config/loader.test.ts +120 -0
  210. package/src/config/loader.ts +178 -0
  211. package/src/config/manager.test.ts +215 -0
  212. package/src/config/manager.ts +328 -40
  213. package/src/config/merger.test.ts +360 -0
  214. package/src/config/merger.ts +221 -0
  215. package/src/config/test-utils.ts +79 -0
  216. package/src/config/types.ts +152 -9
  217. package/src/memory/import-resolver.test.ts +117 -0
  218. package/src/memory/import-resolver.ts +149 -0
  219. package/src/memory/index.ts +11 -0
  220. package/src/memory/init-prompt.ts +113 -0
  221. package/src/memory/memory-manager.test.ts +198 -0
  222. package/src/memory/memory-manager.ts +716 -0
  223. package/src/memory/rules-parser.test.ts +182 -0
  224. package/src/memory/rules-parser.ts +82 -0
  225. package/src/memory/test-utils.ts +60 -0
  226. package/src/memory/types.ts +119 -0
  227. package/src/permissions/audit.ts +284 -0
  228. package/src/permissions/index.ts +20 -1
  229. package/src/permissions/manager.test.ts +260 -0
  230. package/src/permissions/manager.ts +592 -40
  231. package/src/permissions/persistence.test.ts +220 -0
  232. package/src/permissions/persistence.ts +301 -0
  233. package/src/permissions/prompt-matcher.test.ts +213 -0
  234. package/src/permissions/prompt-matcher.ts +472 -0
  235. package/src/permissions/types.ts +236 -8
  236. package/src/prompts/index.test.ts +279 -0
  237. package/src/prompts/index.ts +306 -0
  238. package/src/prompts/system/anthropic.txt +29 -0
  239. package/src/prompts/system/base.txt +124 -0
  240. package/src/prompts/system/gemini.txt +35 -0
  241. package/src/prompts/system/generic.txt +128 -0
  242. package/src/prompts/system/openai.txt +29 -0
  243. package/src/prompts/tools/bash.txt +60 -0
  244. package/src/prompts/tools/edit.txt +29 -0
  245. package/src/prompts/tools/glob.txt +35 -0
  246. package/src/prompts/tools/grep.txt +43 -0
  247. package/src/prompts/tools/read.txt +22 -0
  248. package/src/prompts/tools/todowrite.txt +71 -0
  249. package/src/prompts/tools/webfetch.txt +34 -0
  250. package/src/prompts/tools/websearch.txt +41 -0
  251. package/src/prompts/tools/write.txt +23 -0
  252. package/src/tools/builtin/bash.ts +2 -1
  253. package/src/tools/builtin/edit.ts +2 -1
  254. package/src/tools/builtin/glob.ts +2 -1
  255. package/src/tools/builtin/grep.ts +2 -1
  256. package/src/tools/builtin/read.ts +2 -1
  257. package/src/tools/builtin/todowrite.ts +102 -0
  258. package/src/tools/builtin/webfetch.ts +2 -5
  259. package/src/tools/builtin/websearch.ts +2 -16
  260. package/src/tools/builtin/write.ts +2 -1
  261. package/src/tools/index.ts +4 -0
  262. package/src/tools/types.ts +12 -0
  263. package/tsconfig.json +1 -1
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Configuration Levels - Path resolution for multi-level config
3
+ *
4
+ * Defines the configuration hierarchy and resolves paths for each level.
5
+ * At each level, both .gencode and .claude directories are loaded and merged,
6
+ * with .gencode taking higher priority.
7
+ */
8
+
9
+ import * as path from 'path';
10
+ import * as os from 'os';
11
+ import * as fs from 'fs/promises';
12
+ import {
13
+ ConfigLevelType,
14
+ GENCODE_DIR,
15
+ CLAUDE_DIR,
16
+ SETTINGS_FILE_NAME,
17
+ SETTINGS_LOCAL_FILE_NAME,
18
+ MANAGED_SETTINGS_FILE_NAME,
19
+ GENCODE_CONFIG_DIRS_ENV,
20
+ getManagedPaths,
21
+ } from './types.js';
22
+
23
+ /**
24
+ * Configuration path info for a specific namespace
25
+ */
26
+ export interface ConfigPathInfo {
27
+ settingsPath: string;
28
+ localSettingsPath?: string;
29
+ dir: string;
30
+ namespace: 'gencode' | 'claude' | 'extra';
31
+ exists: boolean;
32
+ }
33
+
34
+ /**
35
+ * Configuration level with resolved paths
36
+ */
37
+ export interface ResolvedLevel {
38
+ type: ConfigLevelType;
39
+ priority: number;
40
+ paths: ConfigPathInfo[];
41
+ description: string;
42
+ }
43
+
44
+ /**
45
+ * Find the project root by walking up the directory tree
46
+ */
47
+ export async function findProjectRoot(cwd: string): Promise<string> {
48
+ let current = path.resolve(cwd);
49
+ const root = path.parse(current).root;
50
+
51
+ while (current !== root) {
52
+ // Check for git directory
53
+ try {
54
+ await fs.access(path.join(current, '.git'));
55
+ return current;
56
+ } catch {
57
+ // Not a git root, continue
58
+ }
59
+
60
+ // Check for .gencode or .claude directories
61
+ try {
62
+ await fs.access(path.join(current, GENCODE_DIR));
63
+ return current;
64
+ } catch {
65
+ // Continue
66
+ }
67
+
68
+ try {
69
+ await fs.access(path.join(current, CLAUDE_DIR));
70
+ return current;
71
+ } catch {
72
+ // Continue
73
+ }
74
+
75
+ const parent = path.dirname(current);
76
+ if (parent === current) break;
77
+ current = parent;
78
+ }
79
+
80
+ return cwd;
81
+ }
82
+
83
+ /**
84
+ * Check if a path exists
85
+ */
86
+ async function pathExists(filePath: string): Promise<boolean> {
87
+ try {
88
+ await fs.access(filePath);
89
+ return true;
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Parse GENCODE_CONFIG_DIRS environment variable
97
+ */
98
+ export function parseExtraConfigDirs(): string[] {
99
+ const value = process.env[GENCODE_CONFIG_DIRS_ENV];
100
+ if (!value) return [];
101
+
102
+ return value
103
+ .split(':')
104
+ .map((dir) => dir.trim())
105
+ .filter((dir) => dir.length > 0)
106
+ .map((dir) => dir.replace(/^~/, os.homedir()));
107
+ }
108
+
109
+ /**
110
+ * Get all configuration levels with resolved paths
111
+ */
112
+ export async function getConfigLevels(cwd: string): Promise<ResolvedLevel[]> {
113
+ const projectRoot = await findProjectRoot(cwd);
114
+ const home = os.homedir();
115
+ const managedPaths = getManagedPaths();
116
+ const extraDirs = parseExtraConfigDirs();
117
+
118
+ const levels: ResolvedLevel[] = [];
119
+
120
+ // Level 1: User (lowest priority for settings, loaded first)
121
+ const userPaths: ConfigPathInfo[] = [];
122
+
123
+ // Claude first (lower priority within level)
124
+ const userClaudeDir = path.join(home, CLAUDE_DIR);
125
+ userPaths.push({
126
+ settingsPath: path.join(userClaudeDir, SETTINGS_FILE_NAME),
127
+ dir: userClaudeDir,
128
+ namespace: 'claude',
129
+ exists: await pathExists(path.join(userClaudeDir, SETTINGS_FILE_NAME)),
130
+ });
131
+
132
+ // GenCode second (higher priority within level)
133
+ const userGencodeDir = path.join(home, GENCODE_DIR);
134
+ userPaths.push({
135
+ settingsPath: path.join(userGencodeDir, SETTINGS_FILE_NAME),
136
+ dir: userGencodeDir,
137
+ namespace: 'gencode',
138
+ exists: await pathExists(path.join(userGencodeDir, SETTINGS_FILE_NAME)),
139
+ });
140
+
141
+ levels.push({
142
+ type: 'user',
143
+ priority: 10,
144
+ paths: userPaths,
145
+ description: 'User global settings',
146
+ });
147
+
148
+ // Level 2: Extra config dirs from environment variable
149
+ if (extraDirs.length > 0) {
150
+ for (let i = 0; i < extraDirs.length; i++) {
151
+ const dir = extraDirs[i];
152
+ const extraPaths: ConfigPathInfo[] = [
153
+ {
154
+ settingsPath: path.join(dir, SETTINGS_FILE_NAME),
155
+ dir,
156
+ namespace: 'extra',
157
+ exists: await pathExists(path.join(dir, SETTINGS_FILE_NAME)),
158
+ },
159
+ ];
160
+
161
+ levels.push({
162
+ type: 'extra',
163
+ priority: 20 + i, // Each extra dir has slightly higher priority
164
+ paths: extraPaths,
165
+ description: `Extra config from ${dir}`,
166
+ });
167
+ }
168
+ }
169
+
170
+ // Level 3: Project (shared, committed to git)
171
+ const projectPaths: ConfigPathInfo[] = [];
172
+
173
+ // Claude first (lower priority within level)
174
+ const projectClaudeDir = path.join(projectRoot, CLAUDE_DIR);
175
+ projectPaths.push({
176
+ settingsPath: path.join(projectClaudeDir, SETTINGS_FILE_NAME),
177
+ dir: projectClaudeDir,
178
+ namespace: 'claude',
179
+ exists: await pathExists(path.join(projectClaudeDir, SETTINGS_FILE_NAME)),
180
+ });
181
+
182
+ // GenCode second (higher priority within level)
183
+ const projectGencodeDir = path.join(projectRoot, GENCODE_DIR);
184
+ projectPaths.push({
185
+ settingsPath: path.join(projectGencodeDir, SETTINGS_FILE_NAME),
186
+ dir: projectGencodeDir,
187
+ namespace: 'gencode',
188
+ exists: await pathExists(path.join(projectGencodeDir, SETTINGS_FILE_NAME)),
189
+ });
190
+
191
+ levels.push({
192
+ type: 'project',
193
+ priority: 30,
194
+ paths: projectPaths,
195
+ description: 'Project shared settings',
196
+ });
197
+
198
+ // Level 4: Local (personal, gitignored)
199
+ const localPaths: ConfigPathInfo[] = [];
200
+
201
+ // Claude first (lower priority within level)
202
+ localPaths.push({
203
+ settingsPath: path.join(projectClaudeDir, SETTINGS_LOCAL_FILE_NAME),
204
+ dir: projectClaudeDir,
205
+ namespace: 'claude',
206
+ exists: await pathExists(path.join(projectClaudeDir, SETTINGS_LOCAL_FILE_NAME)),
207
+ });
208
+
209
+ // GenCode second (higher priority within level)
210
+ localPaths.push({
211
+ settingsPath: path.join(projectGencodeDir, SETTINGS_LOCAL_FILE_NAME),
212
+ dir: projectGencodeDir,
213
+ namespace: 'gencode',
214
+ exists: await pathExists(path.join(projectGencodeDir, SETTINGS_LOCAL_FILE_NAME)),
215
+ });
216
+
217
+ levels.push({
218
+ type: 'local',
219
+ priority: 40,
220
+ paths: localPaths,
221
+ description: 'Local personal settings (gitignored)',
222
+ });
223
+
224
+ // Level 5: Managed (highest priority, enforced)
225
+ const managedPathsList: ConfigPathInfo[] = [];
226
+
227
+ // Claude first (lower priority within level)
228
+ managedPathsList.push({
229
+ settingsPath: path.join(managedPaths.claude, MANAGED_SETTINGS_FILE_NAME),
230
+ dir: managedPaths.claude,
231
+ namespace: 'claude',
232
+ exists: await pathExists(path.join(managedPaths.claude, MANAGED_SETTINGS_FILE_NAME)),
233
+ });
234
+
235
+ // GenCode second (higher priority within level)
236
+ managedPathsList.push({
237
+ settingsPath: path.join(managedPaths.gencode, MANAGED_SETTINGS_FILE_NAME),
238
+ dir: managedPaths.gencode,
239
+ namespace: 'gencode',
240
+ exists: await pathExists(path.join(managedPaths.gencode, MANAGED_SETTINGS_FILE_NAME)),
241
+ });
242
+
243
+ levels.push({
244
+ type: 'managed',
245
+ priority: 100, // Highest priority
246
+ paths: managedPathsList,
247
+ description: 'System-wide managed settings (enforced)',
248
+ });
249
+
250
+ // Sort by priority (ascending, so we merge from low to high)
251
+ return levels.sort((a, b) => a.priority - b.priority);
252
+ }
253
+
254
+ /**
255
+ * Get the primary settings directory for saving
256
+ * Prefers .gencode if it exists, otherwise creates it
257
+ */
258
+ export function getPrimarySettingsDir(
259
+ level: 'user' | 'project' | 'local',
260
+ projectRoot: string
261
+ ): string {
262
+ const home = os.homedir();
263
+
264
+ switch (level) {
265
+ case 'user':
266
+ return path.join(home, GENCODE_DIR);
267
+ case 'project':
268
+ case 'local':
269
+ return path.join(projectRoot, GENCODE_DIR);
270
+ default:
271
+ return path.join(home, GENCODE_DIR);
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Get the settings file path for a specific level
277
+ */
278
+ export function getSettingsFilePath(
279
+ level: 'user' | 'project' | 'local',
280
+ projectRoot: string
281
+ ): string {
282
+ const dir = getPrimarySettingsDir(level, projectRoot);
283
+ const fileName = level === 'local' ? SETTINGS_LOCAL_FILE_NAME : SETTINGS_FILE_NAME;
284
+ return path.join(dir, fileName);
285
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Config Loader Tests
3
+ */
4
+
5
+ import * as fs from 'fs/promises';
6
+ import * as path from 'path';
7
+ import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
8
+ import {
9
+ loadAllSources,
10
+ loadSourcesByLevel,
11
+ loadProjectSettings,
12
+ getExistingConfigFiles,
13
+ } from './loader.js';
14
+ import { createTestProject, writeSettings, type TestProject } from './test-utils.js';
15
+
16
+ describe('Config Loader', () => {
17
+ let test: TestProject;
18
+
19
+ beforeEach(async () => {
20
+ test = await createTestProject('gencode-loader-');
21
+ });
22
+
23
+ afterEach(() => test.cleanup());
24
+
25
+ describe('loadAllSources', () => {
26
+ it('should load settings from project .claude directory', async () => {
27
+ await writeSettings(test.projectDir, 'claude', { provider: 'openai' });
28
+ const sources = await loadAllSources(test.projectDir);
29
+
30
+ const source = sources.find((s) => s.level === 'project' && s.namespace === 'claude');
31
+ expect(source?.settings.provider).toBe('openai');
32
+ });
33
+
34
+ it('should load settings from project .gencode directory', async () => {
35
+ await writeSettings(test.projectDir, 'gencode', { provider: 'anthropic' });
36
+ const sources = await loadAllSources(test.projectDir);
37
+
38
+ const source = sources.find((s) => s.level === 'project' && s.namespace === 'gencode');
39
+ expect(source?.settings.provider).toBe('anthropic');
40
+ });
41
+
42
+ it('should load both .claude and .gencode settings at same level', async () => {
43
+ await writeSettings(test.projectDir, 'claude', { provider: 'openai', model: 'gpt-4' });
44
+ await writeSettings(test.projectDir, 'gencode', { provider: 'anthropic' });
45
+
46
+ const sources = await loadAllSources(test.projectDir);
47
+ const claude = sources.find((s) => s.level === 'project' && s.namespace === 'claude');
48
+ const gencode = sources.find((s) => s.level === 'project' && s.namespace === 'gencode');
49
+
50
+ expect(claude?.settings.provider).toBe('openai');
51
+ expect(gencode?.settings.provider).toBe('anthropic');
52
+ });
53
+
54
+ it('should load local settings', async () => {
55
+ await writeSettings(test.projectDir, 'gencode', { alwaysThinkingEnabled: true }, true);
56
+ const sources = await loadAllSources(test.projectDir);
57
+
58
+ const local = sources.find((s) => s.level === 'local');
59
+ expect(local?.settings.alwaysThinkingEnabled).toBe(true);
60
+ });
61
+
62
+ it('should load from extra config dirs', async () => {
63
+ const extraDir = path.join(test.tempDir, 'extra-config');
64
+ await fs.mkdir(extraDir, { recursive: true });
65
+ await fs.writeFile(path.join(extraDir, 'settings.json'), JSON.stringify({ theme: 'dark' }));
66
+ process.env.GENCODE_CONFIG_DIRS = extraDir;
67
+
68
+ const sources = await loadAllSources(test.projectDir);
69
+ const extra = sources.find((s) => s.level === 'extra');
70
+ expect(extra?.settings.theme).toBe('dark');
71
+ });
72
+ });
73
+
74
+ describe('loadSourcesByLevel', () => {
75
+ it('should group sources by level', async () => {
76
+ await writeSettings(test.projectDir, 'claude', { model: 'gpt-4' });
77
+ await writeSettings(test.projectDir, 'gencode', { model: 'claude-sonnet' });
78
+
79
+ const sourcesByLevel = await loadSourcesByLevel(test.projectDir);
80
+
81
+ expect(sourcesByLevel.has('project')).toBe(true);
82
+ expect(sourcesByLevel.get('project')?.length).toBe(2);
83
+ });
84
+ });
85
+
86
+ describe('loadProjectSettings', () => {
87
+ it('should only load project-level settings', async () => {
88
+ await writeSettings(test.projectDir, 'gencode', { model: 'claude-sonnet' });
89
+ await writeSettings(test.projectDir, 'gencode', { theme: 'dark' }, true);
90
+
91
+ const sources = await loadProjectSettings(test.projectDir);
92
+
93
+ expect(sources.length).toBe(1);
94
+ expect(sources[0].level).toBe('project');
95
+ expect(sources[0].settings.model).toBe('claude-sonnet');
96
+ });
97
+ });
98
+
99
+ describe('getExistingConfigFiles', () => {
100
+ it('should list all existing config files', async () => {
101
+ await writeSettings(test.projectDir, 'claude', {});
102
+ await writeSettings(test.projectDir, 'gencode', {});
103
+ await writeSettings(test.projectDir, 'gencode', {}, true);
104
+
105
+ const files = await getExistingConfigFiles(test.projectDir);
106
+
107
+ expect(files.length).toBeGreaterThanOrEqual(3);
108
+ expect(files.some((f) => f.includes('.claude/settings.json'))).toBe(true);
109
+ expect(files.some((f) => f.includes('.gencode/settings.json'))).toBe(true);
110
+ expect(files.some((f) => f.includes('settings.local.json'))).toBe(true);
111
+ });
112
+
113
+ it('should return array when no config files exist', async () => {
114
+ const emptyDir = path.join(test.tempDir, 'empty');
115
+ await fs.mkdir(emptyDir, { recursive: true });
116
+
117
+ expect(Array.isArray(await getExistingConfigFiles(emptyDir))).toBe(true);
118
+ });
119
+ });
120
+ });
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Configuration Loader - Load settings from various sources
3
+ *
4
+ * Loads configuration files from all levels and namespaces.
5
+ * Each level loads both .gencode and .claude directories.
6
+ */
7
+
8
+ import * as fs from 'fs/promises';
9
+ import type { Settings, ConfigSource, ConfigLevelType } from './types.js';
10
+ import { getConfigLevels, type ResolvedLevel, type ConfigPathInfo } from './levels.js';
11
+
12
+ /**
13
+ * Load a single JSON settings file
14
+ */
15
+ async function loadJsonFile(filePath: string): Promise<Settings | null> {
16
+ try {
17
+ const content = await fs.readFile(filePath, 'utf-8');
18
+ return JSON.parse(content);
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Load settings from a specific path info
26
+ */
27
+ async function loadFromPathInfo(
28
+ pathInfo: ConfigPathInfo,
29
+ levelType: ConfigLevelType
30
+ ): Promise<ConfigSource | null> {
31
+ const settings = await loadJsonFile(pathInfo.settingsPath);
32
+ if (!settings) return null;
33
+
34
+ return {
35
+ level: levelType,
36
+ path: pathInfo.settingsPath,
37
+ namespace: pathInfo.namespace,
38
+ settings,
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Load all configuration sources in order
44
+ *
45
+ * Returns an array of ConfigSource in priority order (lowest first).
46
+ * Within each level:
47
+ * - .claude is loaded first (lower priority)
48
+ * - .gencode is loaded second (higher priority)
49
+ *
50
+ * This allows proper merging where:
51
+ * - Later sources override earlier ones
52
+ * - GenCode settings override Claude settings at the same level
53
+ */
54
+ export async function loadAllSources(cwd: string): Promise<ConfigSource[]> {
55
+ const levels = await getConfigLevels(cwd);
56
+ const sources: ConfigSource[] = [];
57
+
58
+ for (const level of levels) {
59
+ // Load from each path in the level (claude first, then gencode)
60
+ for (const pathInfo of level.paths) {
61
+ const source = await loadFromPathInfo(pathInfo, level.type);
62
+ if (source) {
63
+ sources.push(source);
64
+ }
65
+ }
66
+ }
67
+
68
+ return sources;
69
+ }
70
+
71
+ /**
72
+ * Load configuration sources grouped by level
73
+ */
74
+ export async function loadSourcesByLevel(
75
+ cwd: string
76
+ ): Promise<Map<ConfigLevelType, ConfigSource[]>> {
77
+ const levels = await getConfigLevels(cwd);
78
+ const sourcesByLevel = new Map<ConfigLevelType, ConfigSource[]>();
79
+
80
+ for (const level of levels) {
81
+ const levelSources: ConfigSource[] = [];
82
+
83
+ for (const pathInfo of level.paths) {
84
+ const source = await loadFromPathInfo(pathInfo, level.type);
85
+ if (source) {
86
+ levelSources.push(source);
87
+ }
88
+ }
89
+
90
+ if (levelSources.length > 0) {
91
+ sourcesByLevel.set(level.type, levelSources);
92
+ }
93
+ }
94
+
95
+ return sourcesByLevel;
96
+ }
97
+
98
+ /**
99
+ * Load only user-level settings
100
+ */
101
+ export async function loadUserSettings(cwd: string): Promise<ConfigSource[]> {
102
+ const levels = await getConfigLevels(cwd);
103
+ const userLevel = levels.find((l) => l.type === 'user');
104
+ if (!userLevel) return [];
105
+
106
+ const sources: ConfigSource[] = [];
107
+ for (const pathInfo of userLevel.paths) {
108
+ const source = await loadFromPathInfo(pathInfo, 'user');
109
+ if (source) {
110
+ sources.push(source);
111
+ }
112
+ }
113
+
114
+ return sources;
115
+ }
116
+
117
+ /**
118
+ * Load only project-level settings
119
+ */
120
+ export async function loadProjectSettings(cwd: string): Promise<ConfigSource[]> {
121
+ const levels = await getConfigLevels(cwd);
122
+ const projectLevel = levels.find((l) => l.type === 'project');
123
+ if (!projectLevel) return [];
124
+
125
+ const sources: ConfigSource[] = [];
126
+ for (const pathInfo of projectLevel.paths) {
127
+ const source = await loadFromPathInfo(pathInfo, 'project');
128
+ if (source) {
129
+ sources.push(source);
130
+ }
131
+ }
132
+
133
+ return sources;
134
+ }
135
+
136
+ /**
137
+ * Load only managed settings
138
+ */
139
+ export async function loadManagedSettings(cwd: string): Promise<ConfigSource[]> {
140
+ const levels = await getConfigLevels(cwd);
141
+ const managedLevel = levels.find((l) => l.type === 'managed');
142
+ if (!managedLevel) return [];
143
+
144
+ const sources: ConfigSource[] = [];
145
+ for (const pathInfo of managedLevel.paths) {
146
+ const source = await loadFromPathInfo(pathInfo, 'managed');
147
+ if (source) {
148
+ sources.push(source);
149
+ }
150
+ }
151
+
152
+ return sources;
153
+ }
154
+
155
+ /**
156
+ * Get information about all configuration levels
157
+ */
158
+ export async function getConfigInfo(cwd: string): Promise<ResolvedLevel[]> {
159
+ return getConfigLevels(cwd);
160
+ }
161
+
162
+ /**
163
+ * Check which config files exist
164
+ */
165
+ export async function getExistingConfigFiles(cwd: string): Promise<string[]> {
166
+ const levels = await getConfigLevels(cwd);
167
+ const existingFiles: string[] = [];
168
+
169
+ for (const level of levels) {
170
+ for (const pathInfo of level.paths) {
171
+ if (pathInfo.exists) {
172
+ existingFiles.push(pathInfo.settingsPath);
173
+ }
174
+ }
175
+ }
176
+
177
+ return existingFiles;
178
+ }