wave-agent-sdk 0.0.8 → 0.0.11

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 (236) hide show
  1. package/dist/agent.d.ts +92 -23
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +351 -137
  4. package/dist/index.d.ts +3 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +3 -0
  7. package/dist/managers/aiManager.d.ts +14 -36
  8. package/dist/managers/aiManager.d.ts.map +1 -1
  9. package/dist/managers/aiManager.js +74 -77
  10. package/dist/managers/backgroundBashManager.d.ts.map +1 -1
  11. package/dist/managers/backgroundBashManager.js +4 -3
  12. package/dist/managers/hookManager.d.ts +3 -8
  13. package/dist/managers/hookManager.d.ts.map +1 -1
  14. package/dist/managers/hookManager.js +39 -29
  15. package/dist/managers/liveConfigManager.d.ts +55 -18
  16. package/dist/managers/liveConfigManager.d.ts.map +1 -1
  17. package/dist/managers/liveConfigManager.js +372 -90
  18. package/dist/managers/lspManager.d.ts +43 -0
  19. package/dist/managers/lspManager.d.ts.map +1 -0
  20. package/dist/managers/lspManager.js +326 -0
  21. package/dist/managers/messageManager.d.ts +8 -16
  22. package/dist/managers/messageManager.d.ts.map +1 -1
  23. package/dist/managers/messageManager.js +52 -74
  24. package/dist/managers/permissionManager.d.ts +75 -0
  25. package/dist/managers/permissionManager.d.ts.map +1 -0
  26. package/dist/managers/permissionManager.js +368 -0
  27. package/dist/managers/skillManager.d.ts +1 -0
  28. package/dist/managers/skillManager.d.ts.map +1 -1
  29. package/dist/managers/skillManager.js +2 -1
  30. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  31. package/dist/managers/slashCommandManager.js +0 -1
  32. package/dist/managers/subagentManager.d.ts +8 -23
  33. package/dist/managers/subagentManager.d.ts.map +1 -1
  34. package/dist/managers/subagentManager.js +97 -117
  35. package/dist/managers/toolManager.d.ts +38 -1
  36. package/dist/managers/toolManager.d.ts.map +1 -1
  37. package/dist/managers/toolManager.js +66 -2
  38. package/dist/services/aiService.d.ts +3 -1
  39. package/dist/services/aiService.d.ts.map +1 -1
  40. package/dist/services/aiService.js +123 -30
  41. package/dist/services/configurationService.d.ts +116 -0
  42. package/dist/services/configurationService.d.ts.map +1 -0
  43. package/dist/services/configurationService.js +585 -0
  44. package/dist/services/fileWatcher.d.ts.map +1 -1
  45. package/dist/services/fileWatcher.js +5 -6
  46. package/dist/services/hook.d.ts +7 -124
  47. package/dist/services/hook.d.ts.map +1 -1
  48. package/dist/services/hook.js +46 -458
  49. package/dist/services/jsonlHandler.d.ts +24 -15
  50. package/dist/services/jsonlHandler.d.ts.map +1 -1
  51. package/dist/services/jsonlHandler.js +67 -88
  52. package/dist/services/memory.d.ts +0 -9
  53. package/dist/services/memory.d.ts.map +1 -1
  54. package/dist/services/memory.js +2 -49
  55. package/dist/services/session.d.ts +82 -33
  56. package/dist/services/session.d.ts.map +1 -1
  57. package/dist/services/session.js +275 -181
  58. package/dist/tools/bashTool.d.ts.map +1 -1
  59. package/dist/tools/bashTool.js +109 -11
  60. package/dist/tools/deleteFileTool.d.ts.map +1 -1
  61. package/dist/tools/deleteFileTool.js +25 -0
  62. package/dist/tools/editTool.d.ts.map +1 -1
  63. package/dist/tools/editTool.js +30 -6
  64. package/dist/tools/lspTool.d.ts +6 -0
  65. package/dist/tools/lspTool.d.ts.map +1 -0
  66. package/dist/tools/lspTool.js +589 -0
  67. package/dist/tools/multiEditTool.d.ts.map +1 -1
  68. package/dist/tools/multiEditTool.js +26 -7
  69. package/dist/tools/readTool.d.ts.map +1 -1
  70. package/dist/tools/readTool.js +111 -2
  71. package/dist/tools/skillTool.js +2 -2
  72. package/dist/tools/todoWriteTool.d.ts.map +1 -1
  73. package/dist/tools/todoWriteTool.js +23 -0
  74. package/dist/tools/types.d.ts +11 -8
  75. package/dist/tools/types.d.ts.map +1 -1
  76. package/dist/tools/writeTool.d.ts.map +1 -1
  77. package/dist/tools/writeTool.js +25 -9
  78. package/dist/types/commands.d.ts +0 -1
  79. package/dist/types/commands.d.ts.map +1 -1
  80. package/dist/types/config.d.ts +4 -0
  81. package/dist/types/config.d.ts.map +1 -1
  82. package/dist/types/configuration.d.ts +69 -0
  83. package/dist/types/configuration.d.ts.map +1 -0
  84. package/dist/types/configuration.js +8 -0
  85. package/dist/types/core.d.ts +10 -0
  86. package/dist/types/core.d.ts.map +1 -1
  87. package/dist/types/environment.d.ts +41 -0
  88. package/dist/types/environment.d.ts.map +1 -1
  89. package/dist/types/fileSearch.d.ts +5 -0
  90. package/dist/types/fileSearch.d.ts.map +1 -0
  91. package/dist/types/fileSearch.js +1 -0
  92. package/dist/types/hooks.d.ts +11 -2
  93. package/dist/types/hooks.d.ts.map +1 -1
  94. package/dist/types/hooks.js +1 -7
  95. package/dist/types/index.d.ts +5 -0
  96. package/dist/types/index.d.ts.map +1 -1
  97. package/dist/types/index.js +5 -0
  98. package/dist/types/lsp.d.ts +90 -0
  99. package/dist/types/lsp.d.ts.map +1 -0
  100. package/dist/types/lsp.js +4 -0
  101. package/dist/types/messaging.d.ts +6 -11
  102. package/dist/types/messaging.d.ts.map +1 -1
  103. package/dist/types/permissions.d.ts +39 -0
  104. package/dist/types/permissions.d.ts.map +1 -0
  105. package/dist/types/permissions.js +12 -0
  106. package/dist/types/session.d.ts +1 -6
  107. package/dist/types/session.d.ts.map +1 -1
  108. package/dist/types/skills.d.ts +1 -0
  109. package/dist/types/skills.d.ts.map +1 -1
  110. package/dist/types/tools.d.ts +35 -0
  111. package/dist/types/tools.d.ts.map +1 -0
  112. package/dist/types/tools.js +4 -0
  113. package/dist/utils/abortUtils.d.ts +34 -0
  114. package/dist/utils/abortUtils.d.ts.map +1 -0
  115. package/dist/utils/abortUtils.js +92 -0
  116. package/dist/utils/bashHistory.d.ts +4 -0
  117. package/dist/utils/bashHistory.d.ts.map +1 -1
  118. package/dist/utils/bashHistory.js +21 -4
  119. package/dist/utils/bashParser.d.ts +24 -0
  120. package/dist/utils/bashParser.d.ts.map +1 -0
  121. package/dist/utils/bashParser.js +413 -0
  122. package/dist/utils/builtinSubagents.d.ts +7 -0
  123. package/dist/utils/builtinSubagents.d.ts.map +1 -0
  124. package/dist/utils/builtinSubagents.js +65 -0
  125. package/dist/utils/cacheControlUtils.d.ts +8 -33
  126. package/dist/utils/cacheControlUtils.d.ts.map +1 -1
  127. package/dist/utils/cacheControlUtils.js +83 -126
  128. package/dist/utils/constants.d.ts +0 -12
  129. package/dist/utils/constants.d.ts.map +1 -1
  130. package/dist/utils/constants.js +1 -13
  131. package/dist/utils/convertMessagesForAPI.d.ts +2 -1
  132. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  133. package/dist/utils/convertMessagesForAPI.js +33 -14
  134. package/dist/utils/fileSearch.d.ts +14 -0
  135. package/dist/utils/fileSearch.d.ts.map +1 -0
  136. package/dist/utils/fileSearch.js +88 -0
  137. package/dist/utils/fileUtils.d.ts +14 -2
  138. package/dist/utils/fileUtils.d.ts.map +1 -1
  139. package/dist/utils/fileUtils.js +101 -17
  140. package/dist/utils/globalLogger.d.ts +0 -14
  141. package/dist/utils/globalLogger.d.ts.map +1 -1
  142. package/dist/utils/globalLogger.js +0 -16
  143. package/dist/utils/markdownParser.d.ts.map +1 -1
  144. package/dist/utils/markdownParser.js +1 -17
  145. package/dist/utils/messageOperations.d.ts +1 -11
  146. package/dist/utils/messageOperations.d.ts.map +1 -1
  147. package/dist/utils/messageOperations.js +7 -24
  148. package/dist/utils/pathEncoder.d.ts +4 -0
  149. package/dist/utils/pathEncoder.d.ts.map +1 -1
  150. package/dist/utils/pathEncoder.js +16 -9
  151. package/dist/utils/pathSafety.d.ts +10 -0
  152. package/dist/utils/pathSafety.d.ts.map +1 -0
  153. package/dist/utils/pathSafety.js +23 -0
  154. package/dist/utils/subagentParser.d.ts +2 -2
  155. package/dist/utils/subagentParser.d.ts.map +1 -1
  156. package/dist/utils/subagentParser.js +10 -7
  157. package/package.json +9 -9
  158. package/src/agent.ts +475 -216
  159. package/src/index.ts +3 -0
  160. package/src/managers/aiManager.ts +107 -111
  161. package/src/managers/backgroundBashManager.ts +4 -3
  162. package/src/managers/hookManager.ts +44 -39
  163. package/src/managers/liveConfigManager.ts +524 -138
  164. package/src/managers/lspManager.ts +434 -0
  165. package/src/managers/messageManager.ts +73 -103
  166. package/src/managers/permissionManager.ts +480 -0
  167. package/src/managers/skillManager.ts +3 -1
  168. package/src/managers/slashCommandManager.ts +1 -2
  169. package/src/managers/subagentManager.ts +116 -159
  170. package/src/managers/toolManager.ts +95 -3
  171. package/src/services/aiService.ts +207 -26
  172. package/src/services/configurationService.ts +762 -0
  173. package/src/services/fileWatcher.ts +5 -6
  174. package/src/services/hook.ts +50 -631
  175. package/src/services/jsonlHandler.ts +84 -100
  176. package/src/services/memory.ts +2 -59
  177. package/src/services/session.ts +338 -213
  178. package/src/tools/bashTool.ts +126 -13
  179. package/src/tools/deleteFileTool.ts +36 -0
  180. package/src/tools/editTool.ts +41 -7
  181. package/src/tools/lspTool.ts +760 -0
  182. package/src/tools/multiEditTool.ts +37 -8
  183. package/src/tools/readTool.ts +125 -2
  184. package/src/tools/skillTool.ts +2 -2
  185. package/src/tools/todoWriteTool.ts +33 -1
  186. package/src/tools/types.ts +15 -9
  187. package/src/tools/writeTool.ts +36 -10
  188. package/src/types/commands.ts +0 -1
  189. package/src/types/config.ts +5 -0
  190. package/src/types/configuration.ts +73 -0
  191. package/src/types/core.ts +11 -0
  192. package/src/types/environment.ts +44 -0
  193. package/src/types/fileSearch.ts +4 -0
  194. package/src/types/hooks.ts +14 -11
  195. package/src/types/index.ts +5 -0
  196. package/src/types/lsp.ts +96 -0
  197. package/src/types/messaging.ts +8 -13
  198. package/src/types/permissions.ts +52 -0
  199. package/src/types/session.ts +3 -8
  200. package/src/types/skills.ts +1 -0
  201. package/src/types/tools.ts +38 -0
  202. package/src/utils/abortUtils.ts +118 -0
  203. package/src/utils/bashHistory.ts +28 -4
  204. package/src/utils/bashParser.ts +444 -0
  205. package/src/utils/builtinSubagents.ts +71 -0
  206. package/src/utils/cacheControlUtils.ts +106 -171
  207. package/src/utils/constants.ts +1 -16
  208. package/src/utils/convertMessagesForAPI.ts +38 -14
  209. package/src/utils/fileSearch.ts +107 -0
  210. package/src/utils/fileUtils.ts +114 -19
  211. package/src/utils/globalLogger.ts +0 -17
  212. package/src/utils/markdownParser.ts +1 -19
  213. package/src/utils/messageOperations.ts +7 -35
  214. package/src/utils/pathEncoder.ts +24 -9
  215. package/src/utils/pathSafety.ts +26 -0
  216. package/src/utils/subagentParser.ts +11 -8
  217. package/dist/constants/events.d.ts +0 -28
  218. package/dist/constants/events.d.ts.map +0 -1
  219. package/dist/constants/events.js +0 -27
  220. package/dist/services/configurationWatcher.d.ts +0 -120
  221. package/dist/services/configurationWatcher.d.ts.map +0 -1
  222. package/dist/services/configurationWatcher.js +0 -439
  223. package/dist/services/memoryStore.d.ts +0 -81
  224. package/dist/services/memoryStore.d.ts.map +0 -1
  225. package/dist/services/memoryStore.js +0 -200
  226. package/dist/types/memoryStore.d.ts +0 -82
  227. package/dist/types/memoryStore.d.ts.map +0 -1
  228. package/dist/types/memoryStore.js +0 -7
  229. package/dist/utils/configResolver.d.ts +0 -65
  230. package/dist/utils/configResolver.d.ts.map +0 -1
  231. package/dist/utils/configResolver.js +0 -210
  232. package/src/constants/events.ts +0 -38
  233. package/src/services/configurationWatcher.ts +0 -622
  234. package/src/services/memoryStore.ts +0 -279
  235. package/src/types/memoryStore.ts +0 -94
  236. package/src/utils/configResolver.ts +0 -302
@@ -0,0 +1,762 @@
1
+ /**
2
+ * Configuration Service
3
+ *
4
+ * Centralized service for loading, validating, and managing Wave configuration files.
5
+ * Replaces distributed configuration logic previously embedded in hook.ts.
6
+ */
7
+
8
+ import { readFileSync, existsSync, promises as fs } from "fs";
9
+ import * as path from "path";
10
+ import type {
11
+ WaveConfiguration,
12
+ PartialHookConfiguration,
13
+ } from "../types/hooks.js";
14
+ import { isValidHookEvent } from "../types/hooks.js";
15
+ import type {
16
+ ConfigurationLoadResult,
17
+ ValidationResult,
18
+ ConfigurationPaths,
19
+ } from "../types/configuration.js";
20
+ import {
21
+ getAllConfigPaths,
22
+ getExistingConfigPaths,
23
+ getUserConfigPaths,
24
+ getProjectConfigPaths,
25
+ } from "../utils/configPaths.js";
26
+ import {
27
+ type EnvironmentValidationResult,
28
+ type MergedEnvironmentContext,
29
+ type EnvironmentMergeOptions,
30
+ isValidEnvironmentVars,
31
+ } from "../types/environment.js";
32
+ import {
33
+ GatewayConfig,
34
+ ModelConfig,
35
+ ConfigurationError,
36
+ CONFIG_ERRORS,
37
+ } from "../types/index.js";
38
+ import { DEFAULT_TOKEN_LIMIT } from "../utils/constants.js";
39
+ import { ClientOptions } from "openai";
40
+
41
+ /**
42
+ * Default ConfigurationService implementation
43
+ *
44
+ * Provides centralized configuration loading, validation, and management.
45
+ * Extracted from distributed logic in hook.ts with improved error handling.
46
+ */
47
+ export class ConfigurationService {
48
+ private currentConfiguration: WaveConfiguration | null = null;
49
+ private env: Record<string, string> = {};
50
+
51
+ // Core loading operations
52
+
53
+ /**
54
+ * Load and merge configuration with comprehensive validation
55
+ */
56
+ async loadMergedConfiguration(
57
+ workdir: string,
58
+ ): Promise<ConfigurationLoadResult> {
59
+ try {
60
+ const userConfigPaths = getUserConfigPaths();
61
+ const projectConfigPaths = getProjectConfigPaths(workdir);
62
+
63
+ // Use the merged configuration function (this loads user and project configs internally)
64
+ const mergedConfig = loadMergedWaveConfig(workdir);
65
+
66
+ // Track loading context for better error messages by checking which files exist
67
+ const loadingContext: string[] = [];
68
+ const userPath = userConfigPaths.find((path) => existsSync(path));
69
+ if (userPath) {
70
+ loadingContext.push(`user config from ${userPath}`);
71
+ }
72
+
73
+ const projectPath = projectConfigPaths.find((path) => existsSync(path));
74
+ if (projectPath) {
75
+ loadingContext.push(`project config from ${projectPath}`);
76
+ }
77
+
78
+ if (!mergedConfig) {
79
+ const message =
80
+ loadingContext.length > 0
81
+ ? `No valid configuration found despite attempting to load: ${loadingContext.join(", ")}`
82
+ : "No configuration files found in user or project directories";
83
+
84
+ return {
85
+ configuration: null,
86
+ success: true, // No config is valid
87
+ warnings: [message],
88
+ };
89
+ }
90
+
91
+ // Comprehensive validation
92
+ const validation = this.validateConfiguration(mergedConfig);
93
+
94
+ if (!validation.isValid) {
95
+ const sourcePaths = loadingContext.join(" and ");
96
+ return {
97
+ configuration: null,
98
+ success: false,
99
+ error: `Merged configuration validation failed (sources: ${sourcePaths}): ${validation.errors.join(", ")}`,
100
+ warnings: validation.warnings,
101
+ };
102
+ }
103
+
104
+ // Success case
105
+ this.currentConfiguration = mergedConfig;
106
+
107
+ // Set environment variables if present in the merged config
108
+ if (mergedConfig.env) {
109
+ this.setEnvironmentVars(mergedConfig.env);
110
+ }
111
+
112
+ const sourcePaths = loadingContext.join(" and ");
113
+
114
+ return {
115
+ configuration: mergedConfig,
116
+ success: true,
117
+ sourcePath: sourcePaths || "merged configuration",
118
+ warnings: validation.warnings,
119
+ };
120
+ } catch (error) {
121
+ return {
122
+ configuration: null,
123
+ success: false,
124
+ error: `Failed to load merged configuration from ${workdir}: ${(error as Error).message}`,
125
+ warnings: [],
126
+ };
127
+ }
128
+ }
129
+
130
+ // Validation operations
131
+
132
+ /**
133
+ * Validate configuration object structure and values
134
+ */
135
+ validateConfiguration(config: WaveConfiguration): ValidationResult {
136
+ const result: ValidationResult = {
137
+ isValid: true,
138
+ errors: [],
139
+ warnings: [],
140
+ };
141
+
142
+ // Validate basic structure
143
+ if (!config || typeof config !== "object") {
144
+ result.isValid = false;
145
+ result.errors.push("Configuration must be a valid object");
146
+ return result;
147
+ }
148
+
149
+ // Validate hooks if present
150
+ if (config.hooks !== undefined) {
151
+ if (typeof config.hooks !== "object" || config.hooks === null) {
152
+ result.isValid = false;
153
+ result.errors.push("Hooks configuration must be an object");
154
+ } else {
155
+ for (const [event, eventConfigs] of Object.entries(config.hooks)) {
156
+ if (!isValidHookEvent(event)) {
157
+ result.warnings.push(`Unknown hook event: ${event}`);
158
+ continue;
159
+ }
160
+
161
+ if (!Array.isArray(eventConfigs)) {
162
+ result.isValid = false;
163
+ result.errors.push(`Hook event '${event}' must be an array`);
164
+ continue;
165
+ }
166
+
167
+ // Validate individual hook configurations
168
+ for (let i = 0; i < eventConfigs.length; i++) {
169
+ const hookConfig = eventConfigs[i];
170
+ if (!hookConfig || typeof hookConfig !== "object") {
171
+ result.isValid = false;
172
+ result.errors.push(
173
+ `Hook configuration ${i} for event '${event}' must be an object`,
174
+ );
175
+ }
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ // Validate environment variables if present
182
+ if (config.env !== undefined) {
183
+ const envValidation = validateEnvironmentConfig(config.env);
184
+ if (!envValidation.isValid) {
185
+ result.isValid = false;
186
+ result.errors.push(...envValidation.errors);
187
+ }
188
+ result.warnings.push(...envValidation.warnings);
189
+ }
190
+
191
+ // Validate defaultMode if present
192
+ if (config.defaultMode !== undefined) {
193
+ if (
194
+ config.defaultMode !== "default" &&
195
+ config.defaultMode !== "bypassPermissions" &&
196
+ config.defaultMode !== "acceptEdits"
197
+ ) {
198
+ result.isValid = false;
199
+ result.errors.push(
200
+ `Invalid defaultMode: "${config.defaultMode}". Must be "default", "bypassPermissions" or "acceptEdits"`,
201
+ );
202
+ }
203
+ }
204
+
205
+ // Validate permissions if present
206
+ if (config.permissions !== undefined) {
207
+ if (
208
+ typeof config.permissions !== "object" ||
209
+ config.permissions === null
210
+ ) {
211
+ result.isValid = false;
212
+ result.errors.push("Permissions configuration must be an object");
213
+ } else if (config.permissions.allow !== undefined) {
214
+ if (!Array.isArray(config.permissions.allow)) {
215
+ result.isValid = false;
216
+ result.errors.push("Permissions allow must be an array of strings");
217
+ } else if (
218
+ !config.permissions.allow.every((rule) => typeof rule === "string")
219
+ ) {
220
+ result.isValid = false;
221
+ result.errors.push("All permission rules must be strings");
222
+ }
223
+ }
224
+ }
225
+
226
+ return result;
227
+ }
228
+
229
+ /**
230
+ * Validate configuration file without loading
231
+ */
232
+ validateConfigurationFile(filePath: string): ValidationResult {
233
+ const result: ValidationResult = {
234
+ isValid: true,
235
+ errors: [],
236
+ warnings: [],
237
+ };
238
+
239
+ if (!existsSync(filePath)) {
240
+ result.isValid = false;
241
+ result.errors.push(`Configuration file not found: ${filePath}`);
242
+ return result;
243
+ }
244
+
245
+ try {
246
+ const content = readFileSync(filePath, "utf-8");
247
+ const config = JSON.parse(content) as WaveConfiguration;
248
+
249
+ // Use the main validation method
250
+ const configValidation = this.validateConfiguration(config);
251
+ result.isValid = configValidation.isValid;
252
+ result.errors = configValidation.errors;
253
+ result.warnings = configValidation.warnings;
254
+ } catch (error) {
255
+ result.isValid = false;
256
+ if (error instanceof SyntaxError) {
257
+ result.errors.push(
258
+ `Invalid JSON syntax in ${filePath}: ${error.message}`,
259
+ );
260
+ } else {
261
+ result.errors.push(
262
+ `Error reading configuration file ${filePath}: ${(error as Error).message}`,
263
+ );
264
+ }
265
+ }
266
+
267
+ return result;
268
+ }
269
+
270
+ // Utility operations
271
+
272
+ /**
273
+ * Get currently loaded configuration
274
+ */
275
+ getCurrentConfiguration(): WaveConfiguration | null {
276
+ return this.currentConfiguration;
277
+ }
278
+
279
+ /**
280
+ * Set environment variables from configuration
281
+ * This replaces direct process.env modification
282
+ */
283
+ setEnvironmentVars(env: Record<string, string>): void {
284
+ this.env = { ...env };
285
+ }
286
+
287
+ /**
288
+ * Get current environment variables
289
+ */
290
+ getEnvironmentVars(): Record<string, string> {
291
+ return { ...this.env };
292
+ }
293
+
294
+ // =============================================================================
295
+ // Configuration Resolution Methods (merged from configResolver.ts)
296
+ // =============================================================================
297
+
298
+ /**
299
+ * Resolves gateway configuration from constructor args and environment
300
+ * Resolution priority: options > env (from settings.json) > process.env > error
301
+ * @param apiKey - API key from constructor (optional)
302
+ * @param baseURL - Base URL from constructor (optional)
303
+ * @param defaultHeaders - HTTP headers from constructor (optional)
304
+ * @param fetchOptions - Fetch options from constructor (optional)
305
+ * @param fetch - Custom fetch implementation from constructor (optional)
306
+ * @returns Resolved gateway configuration
307
+ * @throws ConfigurationError if required configuration is missing after fallbacks
308
+ */
309
+ resolveGatewayConfig(
310
+ apiKey?: string,
311
+ baseURL?: string,
312
+ defaultHeaders?: Record<string, string>,
313
+ fetchOptions?: ClientOptions["fetchOptions"],
314
+ fetch?: ClientOptions["fetch"],
315
+ ): GatewayConfig {
316
+ // Resolve API key: constructor > env (settings.json) > process.env
317
+ // Note: Explicitly provided empty strings should be treated as invalid, not fall back to env
318
+ let resolvedApiKey: string;
319
+ if (apiKey !== undefined) {
320
+ resolvedApiKey = apiKey;
321
+ } else {
322
+ resolvedApiKey = this.env.AIGW_TOKEN || process.env.AIGW_TOKEN || "";
323
+ }
324
+
325
+ if (!resolvedApiKey && apiKey === undefined) {
326
+ throw new ConfigurationError(CONFIG_ERRORS.MISSING_API_KEY, "apiKey", {
327
+ constructor: apiKey,
328
+ environment: process.env.AIGW_TOKEN,
329
+ settings: this.env.AIGW_TOKEN,
330
+ });
331
+ }
332
+
333
+ if (resolvedApiKey.trim() === "") {
334
+ throw new ConfigurationError(
335
+ CONFIG_ERRORS.EMPTY_API_KEY,
336
+ "apiKey",
337
+ resolvedApiKey,
338
+ );
339
+ }
340
+
341
+ // Resolve base URL: constructor > env (settings.json) > process.env
342
+ // Note: Explicitly provided empty strings should be treated as invalid, not fall back to env
343
+ let resolvedBaseURL: string;
344
+ if (baseURL !== undefined) {
345
+ resolvedBaseURL = baseURL;
346
+ } else {
347
+ resolvedBaseURL = this.env.AIGW_URL || process.env.AIGW_URL || "";
348
+ }
349
+
350
+ if (!resolvedBaseURL && baseURL === undefined) {
351
+ throw new ConfigurationError(CONFIG_ERRORS.MISSING_BASE_URL, "baseURL", {
352
+ constructor: baseURL,
353
+ environment: process.env.AIGW_URL,
354
+ settings: this.env.AIGW_URL,
355
+ });
356
+ }
357
+
358
+ if (resolvedBaseURL.trim() === "") {
359
+ throw new ConfigurationError(
360
+ CONFIG_ERRORS.EMPTY_BASE_URL,
361
+ "baseURL",
362
+ resolvedBaseURL,
363
+ );
364
+ }
365
+
366
+ return {
367
+ apiKey: resolvedApiKey,
368
+ baseURL: resolvedBaseURL,
369
+ defaultHeaders,
370
+ fetchOptions,
371
+ fetch,
372
+ };
373
+ }
374
+
375
+ /**
376
+ * Resolves model configuration with fallbacks
377
+ * Resolution priority: options > env (from settings.json) > process.env > default
378
+ * @param agentModel - Agent model from constructor (optional)
379
+ * @param fastModel - Fast model from constructor (optional)
380
+ * @returns Resolved model configuration with defaults
381
+ */
382
+ resolveModelConfig(agentModel?: string, fastModel?: string): ModelConfig {
383
+ // Default values as per data-model.md
384
+ const DEFAULT_AGENT_MODEL = "claude-sonnet-4-20250514";
385
+ const DEFAULT_FAST_MODEL = "gemini-2.5-flash";
386
+
387
+ // Resolve agent model: constructor > env (settings.json) > process.env > default
388
+ const resolvedAgentModel =
389
+ agentModel ||
390
+ this.env.AIGW_MODEL ||
391
+ process.env.AIGW_MODEL ||
392
+ DEFAULT_AGENT_MODEL;
393
+
394
+ // Resolve fast model: constructor > env (settings.json) > process.env > default
395
+ const resolvedFastModel =
396
+ fastModel ||
397
+ this.env.AIGW_FAST_MODEL ||
398
+ process.env.AIGW_FAST_MODEL ||
399
+ DEFAULT_FAST_MODEL;
400
+
401
+ return {
402
+ agentModel: resolvedAgentModel,
403
+ fastModel: resolvedFastModel,
404
+ };
405
+ }
406
+
407
+ /**
408
+ * Resolves token limit with fallbacks
409
+ * Resolution priority: options > env (from settings.json) > process.env > default
410
+ * @param constructorLimit - Token limit from constructor (optional)
411
+ * @returns Resolved token limit
412
+ */
413
+ resolveTokenLimit(constructorLimit?: number): number {
414
+ // If constructor value provided, use it
415
+ if (constructorLimit !== undefined) {
416
+ return constructorLimit;
417
+ }
418
+
419
+ // Try env (settings.json) first, then process.env
420
+ const envTokenLimit = this.env.TOKEN_LIMIT || process.env.TOKEN_LIMIT;
421
+ if (envTokenLimit) {
422
+ const parsed = parseInt(envTokenLimit, 10);
423
+ if (!isNaN(parsed)) {
424
+ return parsed;
425
+ }
426
+ }
427
+
428
+ // Use default
429
+ return DEFAULT_TOKEN_LIMIT;
430
+ }
431
+
432
+ /**
433
+ * Resolve all configuration file paths
434
+ */
435
+ getConfigurationPaths(workdir: string): ConfigurationPaths {
436
+ const allPaths = getAllConfigPaths(workdir);
437
+ const existingPaths = getExistingConfigPaths(workdir);
438
+
439
+ return {
440
+ userPaths: allPaths.userPaths,
441
+ projectPaths: allPaths.projectPaths,
442
+ allPaths: allPaths.allPaths,
443
+ existingPaths: existingPaths.existingPaths,
444
+ };
445
+ }
446
+
447
+ /**
448
+ * Add a permission rule to the project's settings.local.json
449
+ */
450
+ async addAllowedRule(workdir: string, rule: string): Promise<void> {
451
+ const localConfigPath = path.join(workdir, ".wave", "settings.local.json");
452
+
453
+ // Ensure .wave directory exists
454
+ const waveDir = path.join(workdir, ".wave");
455
+ if (!existsSync(waveDir)) {
456
+ await fs.mkdir(waveDir, { recursive: true });
457
+ }
458
+
459
+ let config: WaveConfiguration = {};
460
+ if (existsSync(localConfigPath)) {
461
+ try {
462
+ const content = await fs.readFile(localConfigPath, "utf-8");
463
+ config = JSON.parse(content);
464
+ } catch {
465
+ // If file is corrupted, start with empty config
466
+ }
467
+ }
468
+
469
+ if (!config.permissions) {
470
+ config.permissions = {};
471
+ }
472
+ if (!config.permissions.allow) {
473
+ config.permissions.allow = [];
474
+ }
475
+
476
+ if (!config.permissions.allow.includes(rule)) {
477
+ config.permissions.allow.push(rule);
478
+ await fs.writeFile(
479
+ localConfigPath,
480
+ JSON.stringify(config, null, 2),
481
+ "utf-8",
482
+ );
483
+ }
484
+ }
485
+ }
486
+ // =============================================================================
487
+ // Extracted Configuration Functions
488
+ // =============================================================================
489
+
490
+ /**
491
+ * Validate environment variable configuration
492
+ */
493
+ export function validateEnvironmentConfig(
494
+ env: unknown,
495
+ configPath?: string,
496
+ ): EnvironmentValidationResult {
497
+ const result: EnvironmentValidationResult = {
498
+ isValid: true,
499
+ errors: [],
500
+ warnings: [],
501
+ };
502
+
503
+ // Check if env is defined
504
+ if (env === undefined || env === null) {
505
+ return result; // undefined/null env is valid (means no env vars)
506
+ }
507
+
508
+ // Validate that env is a Record<string, string>
509
+ if (!isValidEnvironmentVars(env)) {
510
+ result.isValid = false;
511
+ result.errors.push(
512
+ `Invalid env field format${configPath ? ` in ${configPath}` : ""}. Environment variables must be a Record<string, string>.`,
513
+ );
514
+ return result;
515
+ }
516
+
517
+ // Additional validation for environment variable names
518
+ const envVars = env as Record<string, string>;
519
+ for (const [key, value] of Object.entries(envVars)) {
520
+ // Check for valid environment variable naming convention
521
+ if (!/^[A-Z_][A-Z0-9_]*$/i.test(key)) {
522
+ result.warnings.push(
523
+ `Environment variable '${key}' does not follow standard naming convention (alphanumeric and underscores only).`,
524
+ );
525
+ }
526
+
527
+ // Check for empty values
528
+ if (value === "") {
529
+ result.warnings.push(`Environment variable '${key}' has an empty value.`);
530
+ }
531
+
532
+ // Check for reserved variable names that might cause conflicts
533
+ const reservedNames = [
534
+ "PATH",
535
+ "HOME",
536
+ "USER",
537
+ "PWD",
538
+ "SHELL",
539
+ "TERM",
540
+ "NODE_ENV",
541
+ ];
542
+ if (reservedNames.includes(key.toUpperCase())) {
543
+ result.warnings.push(
544
+ `Environment variable '${key}' overrides a system variable, which may cause unexpected behavior.`,
545
+ );
546
+ }
547
+ }
548
+
549
+ return result;
550
+ }
551
+
552
+ /**
553
+ * Merge environment configurations with project taking precedence over user
554
+ */
555
+ export function mergeEnvironmentConfig(
556
+ userEnv: Record<string, string> | undefined,
557
+ projectEnv: Record<string, string> | undefined,
558
+ options: EnvironmentMergeOptions = {},
559
+ ): MergedEnvironmentContext {
560
+ const userVars = userEnv || {};
561
+ const projectVars = projectEnv || {};
562
+ const mergedVars: Record<string, string> = {};
563
+ const conflicts: MergedEnvironmentContext["conflicts"] = [];
564
+
565
+ // Start with user environment variables
566
+ Object.assign(mergedVars, userVars);
567
+
568
+ // Override with project environment variables and track conflicts
569
+ for (const [key, projectValue] of Object.entries(projectVars)) {
570
+ const userValue = userVars[key];
571
+
572
+ if (
573
+ userValue !== undefined &&
574
+ userValue !== projectValue &&
575
+ options.includeConflictWarnings !== false
576
+ ) {
577
+ // Conflict detected - project value takes precedence
578
+ conflicts.push({
579
+ key,
580
+ userValue,
581
+ projectValue,
582
+ resolvedValue: projectValue,
583
+ });
584
+ }
585
+
586
+ mergedVars[key] = projectValue;
587
+ }
588
+
589
+ return {
590
+ userVars,
591
+ projectVars,
592
+ mergedVars,
593
+ conflicts,
594
+ };
595
+ }
596
+
597
+ /**
598
+ * Load Wave configuration from a JSON file
599
+ * Supports both hooks and environment variables with proper validation
600
+ */
601
+ export function loadWaveConfigFromFile(
602
+ filePath: string,
603
+ ): WaveConfiguration | null {
604
+ if (!existsSync(filePath)) {
605
+ return null;
606
+ }
607
+
608
+ try {
609
+ const content = readFileSync(filePath, "utf-8");
610
+ const config = JSON.parse(content) as WaveConfiguration;
611
+
612
+ // Validate basic structure
613
+ if (!config || typeof config !== "object") {
614
+ throw new Error(`Invalid configuration structure in ${filePath}`);
615
+ }
616
+
617
+ // Validate environment variables if present
618
+ if (config.env !== undefined) {
619
+ const envValidation = validateEnvironmentConfig(config.env, filePath);
620
+
621
+ if (!envValidation.isValid) {
622
+ throw new Error(
623
+ `Environment variable validation failed in ${filePath}: ${envValidation.errors.join(", ")}`,
624
+ );
625
+ }
626
+
627
+ // Log warnings if any
628
+ if (envValidation.warnings.length > 0) {
629
+ console.warn(
630
+ `Environment variable warnings in ${filePath}:\n- ${envValidation.warnings.join("\n- ")}`,
631
+ );
632
+ }
633
+ }
634
+
635
+ return {
636
+ hooks: config.hooks || undefined,
637
+ env: config.env || undefined,
638
+ defaultMode: config.defaultMode,
639
+ permissions: config.permissions || undefined,
640
+ };
641
+ } catch (error) {
642
+ if (error instanceof SyntaxError) {
643
+ throw new Error(`Invalid JSON syntax in ${filePath}: ${error.message}`);
644
+ }
645
+
646
+ // Re-throw validation errors and other errors as-is
647
+ throw error;
648
+ }
649
+ }
650
+
651
+ /**
652
+ * Load Wave configuration from multiple file paths in priority order
653
+ * Returns the first valid configuration found, or null if none exist
654
+ */
655
+ export function loadWaveConfigFromFiles(
656
+ filePaths: string[],
657
+ ): WaveConfiguration | null {
658
+ for (const filePath of filePaths) {
659
+ const config = loadWaveConfigFromFile(filePath);
660
+ if (config !== null) {
661
+ return config;
662
+ }
663
+ }
664
+ return null;
665
+ }
666
+
667
+ /**
668
+ * Load user-specific Wave configuration
669
+ * Checks .local.json first, then falls back to .json
670
+ */
671
+ export function loadUserWaveConfig(): WaveConfiguration | null {
672
+ return loadWaveConfigFromFiles(getUserConfigPaths());
673
+ }
674
+
675
+ /**
676
+ * Load project-specific Wave configuration
677
+ * Checks .local.json first, then falls back to .json
678
+ */
679
+ export function loadProjectWaveConfig(
680
+ workdir: string,
681
+ ): WaveConfiguration | null {
682
+ return loadWaveConfigFromFiles(getProjectConfigPaths(workdir));
683
+ }
684
+
685
+ /**
686
+ * Load and merge Wave configuration from both user and project sources
687
+ * Project configuration takes precedence over user configuration
688
+ * Checks .local.json files first, then falls back to .json files
689
+ */
690
+ export function loadMergedWaveConfig(
691
+ workdir: string,
692
+ ): WaveConfiguration | null {
693
+ const userConfig = loadUserWaveConfig();
694
+ const projectConfig = loadProjectWaveConfig(workdir);
695
+
696
+ // No configuration found
697
+ if (!userConfig && !projectConfig) {
698
+ return null;
699
+ }
700
+
701
+ // Only one configuration found
702
+ if (!userConfig) return projectConfig;
703
+ if (!projectConfig) return userConfig;
704
+
705
+ // Merge configurations (project overrides user)
706
+ const mergedHooks: PartialHookConfiguration = {};
707
+
708
+ // Merge environment variables using the new mergeEnvironmentConfig function
709
+ const environmentContext = mergeEnvironmentConfig(
710
+ userConfig.env,
711
+ projectConfig.env,
712
+ { includeConflictWarnings: true },
713
+ );
714
+
715
+ // Log environment variable conflicts if any
716
+ if (environmentContext.conflicts.length > 0) {
717
+ console.warn(
718
+ `Environment variable conflicts detected (project values take precedence):\n${environmentContext.conflicts
719
+ .map(
720
+ (conflict) =>
721
+ `- ${conflict.key}: "${conflict.userValue}" → "${conflict.projectValue}"`,
722
+ )
723
+ .join("\n")}`,
724
+ );
725
+ }
726
+
727
+ // Merge hooks (combine arrays, project configs come after user configs)
728
+ const allEvents = new Set([
729
+ ...Object.keys(userConfig.hooks || {}),
730
+ ...Object.keys(projectConfig.hooks || {}),
731
+ ]);
732
+
733
+ for (const event of allEvents) {
734
+ if (!isValidHookEvent(event)) continue;
735
+
736
+ const userEventConfigs = userConfig.hooks?.[event] || [];
737
+ const projectEventConfigs = projectConfig.hooks?.[event] || [];
738
+
739
+ // Project configurations take precedence
740
+ mergedHooks[event] = [...userEventConfigs, ...projectEventConfigs];
741
+ }
742
+
743
+ // Merge permissions (combine allow arrays)
744
+ const mergedPermissions: { allow?: string[] } = {};
745
+ const userAllow = userConfig.permissions?.allow || [];
746
+ const projectAllow = projectConfig.permissions?.allow || [];
747
+ if (userAllow.length > 0 || projectAllow.length > 0) {
748
+ mergedPermissions.allow = [...new Set([...userAllow, ...projectAllow])];
749
+ }
750
+
751
+ return {
752
+ hooks: Object.keys(mergedHooks).length > 0 ? mergedHooks : undefined,
753
+ env:
754
+ Object.keys(environmentContext.mergedVars).length > 0
755
+ ? environmentContext.mergedVars
756
+ : undefined,
757
+ // Project defaultMode takes precedence over user defaultMode
758
+ defaultMode: projectConfig.defaultMode ?? userConfig.defaultMode,
759
+ permissions:
760
+ Object.keys(mergedPermissions).length > 0 ? mergedPermissions : undefined,
761
+ };
762
+ }