wave-agent-sdk 0.0.6 → 0.0.8

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 (180) hide show
  1. package/dist/agent.d.ts +32 -20
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +209 -24
  4. package/dist/constants/events.d.ts +28 -0
  5. package/dist/constants/events.d.ts.map +1 -0
  6. package/dist/constants/events.js +27 -0
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +2 -0
  10. package/dist/managers/aiManager.d.ts +34 -1
  11. package/dist/managers/aiManager.d.ts.map +1 -1
  12. package/dist/managers/aiManager.js +248 -132
  13. package/dist/managers/backgroundBashManager.d.ts.map +1 -1
  14. package/dist/managers/backgroundBashManager.js +7 -6
  15. package/dist/managers/hookManager.d.ts +13 -16
  16. package/dist/managers/hookManager.d.ts.map +1 -1
  17. package/dist/managers/hookManager.js +81 -44
  18. package/dist/managers/liveConfigManager.d.ts +58 -0
  19. package/dist/managers/liveConfigManager.d.ts.map +1 -0
  20. package/dist/managers/liveConfigManager.js +160 -0
  21. package/dist/managers/messageManager.d.ts +41 -24
  22. package/dist/managers/messageManager.d.ts.map +1 -1
  23. package/dist/managers/messageManager.js +168 -49
  24. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  25. package/dist/managers/slashCommandManager.js +9 -3
  26. package/dist/managers/subagentManager.d.ts +51 -0
  27. package/dist/managers/subagentManager.d.ts.map +1 -1
  28. package/dist/managers/subagentManager.js +190 -19
  29. package/dist/services/aiService.d.ts +13 -5
  30. package/dist/services/aiService.d.ts.map +1 -1
  31. package/dist/services/aiService.js +350 -74
  32. package/dist/services/configurationWatcher.d.ts +120 -0
  33. package/dist/services/configurationWatcher.d.ts.map +1 -0
  34. package/dist/services/configurationWatcher.js +439 -0
  35. package/dist/services/fileWatcher.d.ts +69 -0
  36. package/dist/services/fileWatcher.d.ts.map +1 -0
  37. package/dist/services/fileWatcher.js +213 -0
  38. package/dist/services/hook.d.ts +91 -9
  39. package/dist/services/hook.d.ts.map +1 -1
  40. package/dist/services/hook.js +393 -43
  41. package/dist/services/jsonlHandler.d.ts +62 -0
  42. package/dist/services/jsonlHandler.d.ts.map +1 -0
  43. package/dist/services/jsonlHandler.js +257 -0
  44. package/dist/services/memory.d.ts +9 -0
  45. package/dist/services/memory.d.ts.map +1 -1
  46. package/dist/services/memory.js +81 -12
  47. package/dist/services/memoryStore.d.ts +81 -0
  48. package/dist/services/memoryStore.d.ts.map +1 -0
  49. package/dist/services/memoryStore.js +200 -0
  50. package/dist/services/session.d.ts +64 -49
  51. package/dist/services/session.d.ts.map +1 -1
  52. package/dist/services/session.js +310 -132
  53. package/dist/tools/bashTool.d.ts.map +1 -1
  54. package/dist/tools/bashTool.js +5 -4
  55. package/dist/tools/deleteFileTool.d.ts.map +1 -1
  56. package/dist/tools/deleteFileTool.js +2 -1
  57. package/dist/tools/editTool.d.ts.map +1 -1
  58. package/dist/tools/editTool.js +3 -2
  59. package/dist/tools/multiEditTool.d.ts.map +1 -1
  60. package/dist/tools/multiEditTool.js +4 -3
  61. package/dist/tools/readTool.d.ts.map +1 -1
  62. package/dist/tools/readTool.js +2 -1
  63. package/dist/tools/todoWriteTool.d.ts.map +1 -1
  64. package/dist/tools/todoWriteTool.js +3 -10
  65. package/dist/tools/writeTool.d.ts.map +1 -1
  66. package/dist/tools/writeTool.js +5 -6
  67. package/dist/types/commands.d.ts +4 -0
  68. package/dist/types/commands.d.ts.map +1 -1
  69. package/dist/types/core.d.ts +35 -0
  70. package/dist/types/core.d.ts.map +1 -1
  71. package/dist/types/environment.d.ts +42 -0
  72. package/dist/types/environment.d.ts.map +1 -0
  73. package/dist/types/environment.js +21 -0
  74. package/dist/types/hooks.d.ts +8 -2
  75. package/dist/types/hooks.d.ts.map +1 -1
  76. package/dist/types/hooks.js +8 -2
  77. package/dist/types/index.d.ts +2 -0
  78. package/dist/types/index.d.ts.map +1 -1
  79. package/dist/types/index.js +2 -0
  80. package/dist/types/memoryStore.d.ts +82 -0
  81. package/dist/types/memoryStore.d.ts.map +1 -0
  82. package/dist/types/memoryStore.js +7 -0
  83. package/dist/types/messaging.d.ts +21 -9
  84. package/dist/types/messaging.d.ts.map +1 -1
  85. package/dist/types/messaging.js +5 -1
  86. package/dist/types/session.d.ts +20 -0
  87. package/dist/types/session.d.ts.map +1 -0
  88. package/dist/types/session.js +7 -0
  89. package/dist/utils/bashHistory.d.ts.map +1 -1
  90. package/dist/utils/bashHistory.js +27 -26
  91. package/dist/utils/cacheControlUtils.d.ts +121 -0
  92. package/dist/utils/cacheControlUtils.d.ts.map +1 -0
  93. package/dist/utils/cacheControlUtils.js +367 -0
  94. package/dist/utils/commandPathResolver.d.ts +52 -0
  95. package/dist/utils/commandPathResolver.d.ts.map +1 -0
  96. package/dist/utils/commandPathResolver.js +145 -0
  97. package/dist/utils/configPaths.d.ts +85 -0
  98. package/dist/utils/configPaths.d.ts.map +1 -0
  99. package/dist/utils/configPaths.js +121 -0
  100. package/dist/utils/configResolver.d.ts +37 -10
  101. package/dist/utils/configResolver.d.ts.map +1 -1
  102. package/dist/utils/configResolver.js +127 -23
  103. package/dist/utils/constants.d.ts +1 -1
  104. package/dist/utils/constants.js +1 -1
  105. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  106. package/dist/utils/convertMessagesForAPI.js +8 -13
  107. package/dist/utils/customCommands.d.ts.map +1 -1
  108. package/dist/utils/customCommands.js +66 -21
  109. package/dist/utils/fileUtils.d.ts +15 -0
  110. package/dist/utils/fileUtils.d.ts.map +1 -0
  111. package/dist/utils/fileUtils.js +61 -0
  112. package/dist/utils/globalLogger.d.ts +102 -0
  113. package/dist/utils/globalLogger.d.ts.map +1 -0
  114. package/dist/utils/globalLogger.js +136 -0
  115. package/dist/utils/hookMatcher.d.ts +1 -6
  116. package/dist/utils/hookMatcher.d.ts.map +1 -1
  117. package/dist/utils/mcpUtils.d.ts.map +1 -1
  118. package/dist/utils/mcpUtils.js +25 -3
  119. package/dist/utils/messageOperations.d.ts +27 -27
  120. package/dist/utils/messageOperations.d.ts.map +1 -1
  121. package/dist/utils/messageOperations.js +46 -36
  122. package/dist/utils/pathEncoder.d.ts +104 -0
  123. package/dist/utils/pathEncoder.d.ts.map +1 -0
  124. package/dist/utils/pathEncoder.js +272 -0
  125. package/dist/utils/subagentParser.d.ts.map +1 -1
  126. package/dist/utils/subagentParser.js +2 -1
  127. package/dist/utils/tokenCalculation.d.ts +26 -0
  128. package/dist/utils/tokenCalculation.d.ts.map +1 -0
  129. package/dist/utils/tokenCalculation.js +36 -0
  130. package/package.json +6 -3
  131. package/src/agent.ts +301 -37
  132. package/src/constants/events.ts +38 -0
  133. package/src/index.ts +2 -0
  134. package/src/managers/aiManager.ts +325 -173
  135. package/src/managers/backgroundBashManager.ts +7 -6
  136. package/src/managers/hookManager.ts +106 -84
  137. package/src/managers/liveConfigManager.ts +248 -0
  138. package/src/managers/messageManager.ts +237 -100
  139. package/src/managers/slashCommandManager.ts +9 -7
  140. package/src/managers/subagentManager.ts +284 -22
  141. package/src/services/aiService.ts +474 -83
  142. package/src/services/configurationWatcher.ts +622 -0
  143. package/src/services/fileWatcher.ts +301 -0
  144. package/src/services/hook.ts +538 -47
  145. package/src/services/jsonlHandler.ts +319 -0
  146. package/src/services/memory.ts +92 -12
  147. package/src/services/memoryStore.ts +279 -0
  148. package/src/services/session.ts +381 -157
  149. package/src/tools/bashTool.ts +5 -4
  150. package/src/tools/deleteFileTool.ts +2 -1
  151. package/src/tools/editTool.ts +3 -2
  152. package/src/tools/multiEditTool.ts +4 -3
  153. package/src/tools/readTool.ts +2 -1
  154. package/src/tools/todoWriteTool.ts +3 -11
  155. package/src/tools/writeTool.ts +7 -6
  156. package/src/types/commands.ts +6 -0
  157. package/src/types/core.ts +44 -0
  158. package/src/types/environment.ts +60 -0
  159. package/src/types/hooks.ts +21 -8
  160. package/src/types/index.ts +2 -0
  161. package/src/types/memoryStore.ts +94 -0
  162. package/src/types/messaging.ts +21 -10
  163. package/src/types/session.ts +25 -0
  164. package/src/utils/bashHistory.ts +27 -27
  165. package/src/utils/cacheControlUtils.ts +540 -0
  166. package/src/utils/commandPathResolver.ts +189 -0
  167. package/src/utils/configPaths.ts +163 -0
  168. package/src/utils/configResolver.ts +182 -22
  169. package/src/utils/constants.ts +1 -1
  170. package/src/utils/convertMessagesForAPI.ts +8 -14
  171. package/src/utils/customCommands.ts +90 -22
  172. package/src/utils/fileUtils.ts +65 -0
  173. package/src/utils/globalLogger.ts +145 -0
  174. package/src/utils/hookMatcher.ts +1 -12
  175. package/src/utils/mcpUtils.ts +34 -3
  176. package/src/utils/messageOperations.ts +77 -60
  177. package/src/utils/pathEncoder.ts +379 -0
  178. package/src/utils/subagentParser.ts +2 -1
  179. package/src/utils/tokenCalculation.ts +43 -0
  180. package/src/types/index.ts.backup +0 -357
@@ -7,19 +7,31 @@
7
7
 
8
8
  import { spawn, type ChildProcess } from "child_process";
9
9
  import { existsSync, readFileSync } from "fs";
10
- import { join } from "path";
11
- import { homedir } from "os";
10
+ import {
11
+ getUserConfigPath,
12
+ getProjectConfigPath,
13
+ getUserConfigPaths,
14
+ getProjectConfigPaths,
15
+ hasAnyConfig,
16
+ getConfigurationInfo,
17
+ } from "../utils/configPaths.js";
12
18
  import {
13
19
  type HookExecutionContext,
14
20
  type HookExecutionResult,
15
21
  type HookExecutionOptions,
16
22
  type ExtendedHookExecutionContext,
17
23
  type HookJsonInput,
18
- type HookConfiguration,
24
+ type WaveConfiguration,
19
25
  type PartialHookConfiguration,
20
26
  getSessionFilePath,
21
27
  isValidHookEvent,
22
28
  } from "../types/hooks.js";
29
+ import {
30
+ type EnvironmentValidationResult,
31
+ type MergedEnvironmentContext,
32
+ type EnvironmentMergeOptions,
33
+ isValidEnvironmentVars,
34
+ } from "../types/environment.js";
23
35
 
24
36
  // =============================================================================
25
37
  // Hook Execution Functions
@@ -61,6 +73,11 @@ function buildHookJsonInput(
61
73
  jsonInput.user_prompt = context.userPrompt;
62
74
  }
63
75
 
76
+ // Add subagent_type if present
77
+ if (context.subagentType !== undefined) {
78
+ jsonInput.subagent_type = context.subagentType;
79
+ }
80
+
64
81
  return jsonInput;
65
82
  }
66
83
 
@@ -71,6 +88,7 @@ export async function executeCommand(
71
88
  command: string,
72
89
  context: HookExecutionContext | ExtendedHookExecutionContext,
73
90
  options?: HookExecutionOptions,
91
+ additionalEnvVars?: Record<string, string>,
74
92
  ): Promise<HookExecutionResult> {
75
93
  const defaultTimeout = 10000; // 10 seconds
76
94
  const maxTimeout = 300000; // 5 minutes
@@ -108,6 +126,7 @@ export async function executeCommand(
108
126
  cwd: context.projectDir,
109
127
  env: {
110
128
  ...process.env,
129
+ ...additionalEnvVars, // Merge additional environment variables from Wave configuration
111
130
  HOOK_EVENT: context.event,
112
131
  HOOK_TOOL_NAME: context.toolName || "",
113
132
  HOOK_PROJECT_DIR: context.projectDir,
@@ -193,11 +212,17 @@ export async function executeCommands(
193
212
  commands: string[],
194
213
  context: HookExecutionContext | ExtendedHookExecutionContext,
195
214
  options?: HookExecutionOptions,
215
+ additionalEnvVars?: Record<string, string>,
196
216
  ): Promise<HookExecutionResult[]> {
197
217
  const results: HookExecutionResult[] = [];
198
218
 
199
219
  for (const command of commands) {
200
- const result = await executeCommand(command, context, options);
220
+ const result = await executeCommand(
221
+ command,
222
+ context,
223
+ options,
224
+ additionalEnvVars,
225
+ );
201
226
  results.push(result);
202
227
 
203
228
  // Stop on first failure unless continueOnFailure is set
@@ -237,68 +262,502 @@ export function isCommandSafe(command: string): boolean {
237
262
  }
238
263
 
239
264
  // =============================================================================
240
- // Hook Settings Functions
265
+ // Environment Variable Functions
241
266
  // =============================================================================
242
267
 
243
268
  /**
244
- * Get the user-specific hooks configuration file path
269
+ * Validate environment variable configuration
270
+ */
271
+ export function validateEnvironmentConfig(
272
+ env: unknown,
273
+ configPath?: string,
274
+ ): EnvironmentValidationResult {
275
+ const result: EnvironmentValidationResult = {
276
+ isValid: true,
277
+ errors: [],
278
+ warnings: [],
279
+ };
280
+
281
+ // Check if env is defined
282
+ if (env === undefined || env === null) {
283
+ return result; // undefined/null env is valid (means no env vars)
284
+ }
285
+
286
+ // Validate that env is a Record<string, string>
287
+ if (!isValidEnvironmentVars(env)) {
288
+ result.isValid = false;
289
+ result.errors.push(
290
+ `Invalid env field format${configPath ? ` in ${configPath}` : ""}. Environment variables must be a Record<string, string>.`,
291
+ );
292
+ return result;
293
+ }
294
+
295
+ // Additional validation for environment variable names
296
+ const envVars = env as Record<string, string>;
297
+ for (const [key, value] of Object.entries(envVars)) {
298
+ // Check for valid environment variable naming convention
299
+ if (!/^[A-Z_][A-Z0-9_]*$/i.test(key)) {
300
+ result.warnings.push(
301
+ `Environment variable '${key}' does not follow standard naming convention (alphanumeric and underscores only).`,
302
+ );
303
+ }
304
+
305
+ // Check for empty values
306
+ if (value === "") {
307
+ result.warnings.push(`Environment variable '${key}' has an empty value.`);
308
+ }
309
+
310
+ // Check for reserved variable names that might cause conflicts
311
+ const reservedNames = [
312
+ "PATH",
313
+ "HOME",
314
+ "USER",
315
+ "PWD",
316
+ "SHELL",
317
+ "TERM",
318
+ "NODE_ENV",
319
+ ];
320
+ if (reservedNames.includes(key.toUpperCase())) {
321
+ result.warnings.push(
322
+ `Environment variable '${key}' overrides a system variable, which may cause unexpected behavior.`,
323
+ );
324
+ }
325
+ }
326
+
327
+ return result;
328
+ }
329
+
330
+ /**
331
+ * Merge environment configurations with project taking precedence over user
332
+ */
333
+ export function mergeEnvironmentConfig(
334
+ userEnv: Record<string, string> | undefined,
335
+ projectEnv: Record<string, string> | undefined,
336
+ options: EnvironmentMergeOptions = {},
337
+ ): MergedEnvironmentContext {
338
+ const userVars = userEnv || {};
339
+ const projectVars = projectEnv || {};
340
+ const mergedVars: Record<string, string> = {};
341
+ const conflicts: MergedEnvironmentContext["conflicts"] = [];
342
+
343
+ // Start with user environment variables
344
+ Object.assign(mergedVars, userVars);
345
+
346
+ // Override with project environment variables and track conflicts
347
+ for (const [key, projectValue] of Object.entries(projectVars)) {
348
+ const userValue = userVars[key];
349
+
350
+ if (
351
+ userValue !== undefined &&
352
+ userValue !== projectValue &&
353
+ options.includeConflictWarnings !== false
354
+ ) {
355
+ // Conflict detected - project value takes precedence
356
+ conflicts.push({
357
+ key,
358
+ userValue,
359
+ projectValue,
360
+ resolvedValue: projectValue,
361
+ });
362
+ }
363
+
364
+ mergedVars[key] = projectValue;
365
+ }
366
+
367
+ return {
368
+ userVars,
369
+ projectVars,
370
+ mergedVars,
371
+ conflicts,
372
+ };
373
+ }
374
+
375
+ // =============================================================================
376
+ // Hook Settings Functions (using centralized config path utilities)
377
+ // =============================================================================
378
+
379
+ /**
380
+ * Get the user-specific hooks configuration file path (legacy function)
381
+ * @deprecated Use getUserConfigPaths() from configPaths.ts for better priority support
245
382
  */
246
383
  export function getUserHooksConfigPath(): string {
247
- return join(homedir(), ".wave", "settings.json");
384
+ return getUserConfigPath();
248
385
  }
249
386
 
250
387
  /**
251
- * Get the project-specific hooks configuration file path
388
+ * Get the project-specific hooks configuration file path (legacy function)
389
+ * @deprecated Use getProjectConfigPaths() from configPaths.ts for better priority support
252
390
  */
253
391
  export function getProjectHooksConfigPath(workdir: string): string {
254
- return join(workdir, ".wave", "settings.json");
392
+ return getProjectConfigPath(workdir);
255
393
  }
256
394
 
257
395
  /**
258
- * Load hooks configuration from a JSON file
396
+ * Get the user-specific hooks configuration file paths in priority order
397
+ * @deprecated Use getUserConfigPaths() from configPaths.ts directly
259
398
  */
260
- export function loadHooksConfigFromFile(
399
+ export function getUserHooksConfigPaths(): string[] {
400
+ return getUserConfigPaths();
401
+ }
402
+
403
+ /**
404
+ * Get the project-specific hooks configuration file paths in priority order
405
+ * @deprecated Use getProjectConfigPaths() from configPaths.ts directly
406
+ */
407
+ export function getProjectHooksConfigPaths(workdir: string): string[] {
408
+ return getProjectConfigPaths(workdir);
409
+ }
410
+
411
+ /**
412
+ * Load Wave configuration from a JSON file with graceful fallback
413
+ * This version is optimized for live reload scenarios where invalid config should not crash the system
414
+ */
415
+ export function loadWaveConfigFromFileWithFallback(
261
416
  filePath: string,
262
- ): PartialHookConfiguration | null {
417
+ previousValidConfig?: WaveConfiguration | null,
418
+ ): { config: WaveConfiguration | null; error?: string; usedFallback: boolean } {
419
+ if (!existsSync(filePath)) {
420
+ return { config: null, usedFallback: false };
421
+ }
422
+
423
+ try {
424
+ const content = readFileSync(filePath, "utf-8");
425
+ const config = JSON.parse(content) as WaveConfiguration;
426
+
427
+ // Validate basic structure
428
+ if (!config || typeof config !== "object") {
429
+ const error = `Invalid configuration structure in ${filePath}`;
430
+ return {
431
+ config: previousValidConfig || null,
432
+ error,
433
+ usedFallback: !!previousValidConfig,
434
+ };
435
+ }
436
+
437
+ // Validate environment variables if present
438
+ if (config.env !== undefined) {
439
+ const envValidation = validateEnvironmentConfig(config.env, filePath);
440
+
441
+ if (!envValidation.isValid) {
442
+ const error = `Environment variable validation failed in ${filePath}: ${envValidation.errors.join(", ")}`;
443
+ return {
444
+ config: previousValidConfig || null,
445
+ error,
446
+ usedFallback: !!previousValidConfig,
447
+ };
448
+ }
449
+
450
+ // Log warnings if any
451
+ if (envValidation.warnings.length > 0) {
452
+ console.warn(
453
+ `Environment variable warnings in ${filePath}:\n- ${envValidation.warnings.join("\n- ")}`,
454
+ );
455
+ }
456
+ }
457
+
458
+ // Return valid configuration
459
+ return {
460
+ config: {
461
+ hooks: config.hooks || undefined,
462
+ env: config.env || undefined,
463
+ },
464
+ usedFallback: false,
465
+ };
466
+ } catch (error) {
467
+ let errorMessage: string;
468
+
469
+ if (error instanceof SyntaxError) {
470
+ errorMessage = `Invalid JSON syntax in ${filePath}: ${error.message}`;
471
+ } else {
472
+ errorMessage = `Error loading configuration from ${filePath}: ${(error as Error).message}`;
473
+ }
474
+
475
+ return {
476
+ config: previousValidConfig || null,
477
+ error: errorMessage,
478
+ usedFallback: !!previousValidConfig,
479
+ };
480
+ }
481
+ }
482
+
483
+ /**
484
+ * Load Wave configuration from multiple file paths in priority order
485
+ * Returns the first valid configuration found, or null if none exist
486
+ */
487
+ export function loadWaveConfigFromFiles(
488
+ filePaths: string[],
489
+ ): WaveConfiguration | null {
490
+ for (const filePath of filePaths) {
491
+ const config = loadWaveConfigFromFile(filePath);
492
+ if (config !== null) {
493
+ return config;
494
+ }
495
+ }
496
+ return null;
497
+ }
498
+
499
+ /**
500
+ * Load Wave configuration from multiple file paths with graceful fallback
501
+ * Returns the first valid configuration found with fallback support
502
+ */
503
+ export function loadWaveConfigFromFilesWithFallback(
504
+ filePaths: string[],
505
+ previousValidConfig?: WaveConfiguration | null,
506
+ ): {
507
+ config: WaveConfiguration | null;
508
+ error?: string;
509
+ usedFallback: boolean;
510
+ usedPath?: string;
511
+ } {
512
+ let lastError: string | undefined;
513
+
514
+ for (const filePath of filePaths) {
515
+ const result = loadWaveConfigFromFileWithFallback(
516
+ filePath,
517
+ previousValidConfig,
518
+ );
519
+
520
+ if (result.config !== null && !result.usedFallback) {
521
+ // Found a valid config at this path
522
+ return {
523
+ config: result.config,
524
+ error: result.error,
525
+ usedFallback: result.usedFallback,
526
+ usedPath: filePath,
527
+ };
528
+ }
529
+
530
+ if (result.error) {
531
+ lastError = result.error;
532
+ }
533
+ }
534
+
535
+ // No valid config found in any path
536
+ return {
537
+ config: previousValidConfig || null,
538
+ error: lastError,
539
+ usedFallback: !!previousValidConfig,
540
+ };
541
+ }
542
+
543
+ /**
544
+ * Load and merge Wave configuration with graceful fallback for live reload
545
+ * Provides error recovery by falling back to previous valid configuration
546
+ */
547
+ export function loadMergedWaveConfigWithFallback(
548
+ workdir: string,
549
+ previousValidConfig?: WaveConfiguration | null,
550
+ ): {
551
+ config: WaveConfiguration | null;
552
+ errors: string[];
553
+ usedFallback: boolean;
554
+ } {
555
+ const errors: string[] = [];
556
+ let usedFallback = false;
557
+
558
+ // Load user config with fallback (check .local.json first, then .json)
559
+ const userResult = loadWaveConfigFromFilesWithFallback(
560
+ getUserHooksConfigPaths(),
561
+ previousValidConfig,
562
+ );
563
+ if (userResult.error) {
564
+ errors.push(`User config: ${userResult.error}`);
565
+ }
566
+ if (userResult.usedFallback) {
567
+ usedFallback = true;
568
+ }
569
+
570
+ // Load project config with fallback (check .local.json first, then .json)
571
+ const projectResult = loadWaveConfigFromFilesWithFallback(
572
+ getProjectHooksConfigPaths(workdir),
573
+ previousValidConfig,
574
+ );
575
+ if (projectResult.error) {
576
+ errors.push(`Project config: ${projectResult.error}`);
577
+ }
578
+ if (projectResult.usedFallback) {
579
+ usedFallback = true;
580
+ }
581
+
582
+ const userConfig = userResult.config;
583
+ const projectConfig = projectResult.config;
584
+
585
+ // If both configs failed and no fallback available
586
+ if (!userConfig && !projectConfig && errors.length > 0) {
587
+ return {
588
+ config: previousValidConfig || null,
589
+ errors,
590
+ usedFallback: !!previousValidConfig,
591
+ };
592
+ }
593
+
594
+ // No configuration found at all
595
+ if (!userConfig && !projectConfig) {
596
+ return { config: null, errors, usedFallback };
597
+ }
598
+
599
+ // Only one configuration found
600
+ if (!userConfig) return { config: projectConfig, errors, usedFallback };
601
+ if (!projectConfig) return { config: userConfig, errors, usedFallback };
602
+
603
+ // Merge configurations (project overrides user)
604
+ try {
605
+ const mergedHooks: PartialHookConfiguration = {};
606
+
607
+ // Merge environment variables using the new mergeEnvironmentConfig function
608
+ const environmentContext = mergeEnvironmentConfig(
609
+ userConfig.env,
610
+ projectConfig.env,
611
+ { includeConflictWarnings: true },
612
+ );
613
+
614
+ // Merge hooks (combine arrays, project configs come after user configs)
615
+ const allEvents = new Set([
616
+ ...Object.keys(userConfig.hooks || {}),
617
+ ...Object.keys(projectConfig.hooks || {}),
618
+ ]);
619
+
620
+ for (const event of allEvents) {
621
+ if (!isValidHookEvent(event)) continue;
622
+
623
+ const userEventConfigs = userConfig.hooks?.[event] || [];
624
+ const projectEventConfigs = projectConfig.hooks?.[event] || [];
625
+
626
+ // Project configurations take precedence
627
+ mergedHooks[event] = [...userEventConfigs, ...projectEventConfigs];
628
+ }
629
+
630
+ const mergedConfig = {
631
+ hooks: Object.keys(mergedHooks).length > 0 ? mergedHooks : undefined,
632
+ env:
633
+ Object.keys(environmentContext.mergedVars).length > 0
634
+ ? environmentContext.mergedVars
635
+ : undefined,
636
+ };
637
+
638
+ return { config: mergedConfig, errors, usedFallback };
639
+ } catch (error) {
640
+ errors.push(`Merge error: ${(error as Error).message}`);
641
+ return {
642
+ config: previousValidConfig || null,
643
+ errors,
644
+ usedFallback: !!previousValidConfig,
645
+ };
646
+ }
647
+ }
648
+
649
+ /**
650
+ * Load Wave configuration from a JSON file
651
+ * Supports both hooks and environment variables with proper validation
652
+ */
653
+ export function loadWaveConfigFromFile(
654
+ filePath: string,
655
+ ): WaveConfiguration | null {
263
656
  if (!existsSync(filePath)) {
264
657
  return null;
265
658
  }
266
659
 
267
- const content = readFileSync(filePath, "utf-8");
268
- const config = JSON.parse(content) as HookConfiguration;
660
+ try {
661
+ const content = readFileSync(filePath, "utf-8");
662
+ const config = JSON.parse(content) as WaveConfiguration;
663
+
664
+ // Validate basic structure
665
+ if (!config || typeof config !== "object") {
666
+ throw new Error(`Invalid configuration structure in ${filePath}`);
667
+ }
668
+
669
+ // Validate environment variables if present
670
+ if (config.env !== undefined) {
671
+ const envValidation = validateEnvironmentConfig(config.env, filePath);
672
+
673
+ if (!envValidation.isValid) {
674
+ throw new Error(
675
+ `Environment variable validation failed in ${filePath}: ${envValidation.errors.join(", ")}`,
676
+ );
677
+ }
678
+
679
+ // Log warnings if any
680
+ if (envValidation.warnings.length > 0) {
681
+ console.warn(
682
+ `Environment variable warnings in ${filePath}:\n- ${envValidation.warnings.join("\n- ")}`,
683
+ );
684
+ }
685
+ }
686
+
687
+ return {
688
+ hooks: config.hooks || undefined,
689
+ env: config.env || undefined,
690
+ };
691
+ } catch (error) {
692
+ if (error instanceof SyntaxError) {
693
+ throw new Error(`Invalid JSON syntax in ${filePath}: ${error.message}`);
694
+ }
269
695
 
270
- // Validate basic structure
271
- if (!config || typeof config !== "object" || !config.hooks) {
272
- throw new Error(`Invalid hooks configuration structure in ${filePath}`);
696
+ // Re-throw validation errors and other errors as-is
697
+ throw error;
273
698
  }
699
+ }
274
700
 
275
- return config.hooks;
701
+ /**
702
+ * Load hooks configuration from a JSON file (legacy function)
703
+ */
704
+ export function loadHooksConfigFromFile(
705
+ filePath: string,
706
+ ): PartialHookConfiguration | null {
707
+ const waveConfig = loadWaveConfigFromFile(filePath);
708
+ if (!waveConfig) {
709
+ return null;
710
+ }
711
+
712
+ return waveConfig.hooks || null;
713
+ }
714
+
715
+ /**
716
+ * Load user-specific Wave configuration
717
+ * Checks .local.json first, then falls back to .json
718
+ */
719
+ export function loadUserWaveConfig(): WaveConfiguration | null {
720
+ return loadWaveConfigFromFiles(getUserHooksConfigPaths());
721
+ }
722
+
723
+ /**
724
+ * Load project-specific Wave configuration
725
+ * Checks .local.json first, then falls back to .json
726
+ */
727
+ export function loadProjectWaveConfig(
728
+ workdir: string,
729
+ ): WaveConfiguration | null {
730
+ return loadWaveConfigFromFiles(getProjectHooksConfigPaths(workdir));
276
731
  }
277
732
 
278
733
  /**
279
- * Load user-specific hooks configuration
734
+ * Load user-specific hooks configuration (legacy function)
280
735
  */
281
736
  export function loadUserHooksConfig(): PartialHookConfiguration | null {
282
- return loadHooksConfigFromFile(getUserHooksConfigPath());
737
+ const waveConfig = loadUserWaveConfig();
738
+ return waveConfig?.hooks || null;
283
739
  }
284
740
 
285
741
  /**
286
- * Load project-specific hooks configuration
742
+ * Load project-specific hooks configuration (legacy function)
287
743
  */
288
744
  export function loadProjectHooksConfig(
289
745
  workdir: string,
290
746
  ): PartialHookConfiguration | null {
291
- return loadHooksConfigFromFile(getProjectHooksConfigPath(workdir));
747
+ const waveConfig = loadProjectWaveConfig(workdir);
748
+ return waveConfig?.hooks || null;
292
749
  }
293
750
 
294
751
  /**
295
- * Load and merge hooks configuration from both user and project sources
752
+ * Load and merge Wave configuration from both user and project sources
753
+ * Project configuration takes precedence over user configuration
754
+ * Checks .local.json files first, then falls back to .json files
296
755
  */
297
- export function loadMergedHooksConfig(
756
+ export function loadMergedWaveConfig(
298
757
  workdir: string,
299
- ): PartialHookConfiguration | null {
300
- const userConfig = loadUserHooksConfig();
301
- const projectConfig = loadProjectHooksConfig(workdir);
758
+ ): WaveConfiguration | null {
759
+ const userConfig = loadUserWaveConfig();
760
+ const projectConfig = loadProjectWaveConfig(workdir);
302
761
 
303
762
  // No configuration found
304
763
  if (!userConfig && !projectConfig) {
@@ -310,51 +769,83 @@ export function loadMergedHooksConfig(
310
769
  if (!projectConfig) return userConfig;
311
770
 
312
771
  // Merge configurations (project overrides user)
313
- const merged: PartialHookConfiguration = {};
772
+ const mergedHooks: PartialHookConfiguration = {};
773
+
774
+ // Merge environment variables using the new mergeEnvironmentConfig function
775
+ const environmentContext = mergeEnvironmentConfig(
776
+ userConfig.env,
777
+ projectConfig.env,
778
+ { includeConflictWarnings: true },
779
+ );
314
780
 
315
- // Combine all hook events
781
+ // Log environment variable conflicts if any
782
+ if (environmentContext.conflicts.length > 0) {
783
+ console.warn(
784
+ `Environment variable conflicts detected (project values take precedence):\n${environmentContext.conflicts
785
+ .map(
786
+ (conflict) =>
787
+ `- ${conflict.key}: "${conflict.userValue}" → "${conflict.projectValue}"`,
788
+ )
789
+ .join("\n")}`,
790
+ );
791
+ }
792
+
793
+ // Merge hooks (combine arrays, project configs come after user configs)
316
794
  const allEvents = new Set([
317
- ...Object.keys(userConfig),
318
- ...Object.keys(projectConfig),
795
+ ...Object.keys(userConfig.hooks || {}),
796
+ ...Object.keys(projectConfig.hooks || {}),
319
797
  ]);
320
798
 
321
799
  for (const event of allEvents) {
322
800
  if (!isValidHookEvent(event)) continue;
323
801
 
324
- const userEventConfigs = userConfig[event] || [];
325
- const projectEventConfigs = projectConfig[event] || [];
802
+ const userEventConfigs = userConfig.hooks?.[event] || [];
803
+ const projectEventConfigs = projectConfig.hooks?.[event] || [];
326
804
 
327
805
  // Project configurations take precedence
328
- merged[event] = [...userEventConfigs, ...projectEventConfigs];
806
+ mergedHooks[event] = [...userEventConfigs, ...projectEventConfigs];
329
807
  }
330
808
 
331
- return merged;
809
+ return {
810
+ hooks: Object.keys(mergedHooks).length > 0 ? mergedHooks : undefined,
811
+ env:
812
+ Object.keys(environmentContext.mergedVars).length > 0
813
+ ? environmentContext.mergedVars
814
+ : undefined,
815
+ };
816
+ }
817
+
818
+ /**
819
+ * Load and merge hooks configuration from both user and project sources (legacy function)
820
+ */
821
+ export function loadMergedHooksConfig(
822
+ workdir: string,
823
+ ): PartialHookConfiguration | null {
824
+ const waveConfig = loadMergedWaveConfig(workdir);
825
+ return waveConfig?.hooks || null;
332
826
  }
333
827
 
334
828
  /**
335
829
  * Check if hooks configuration exists (user or project)
830
+ * Checks both .local.json and .json variants
831
+ * @deprecated Use hasAnyConfig() from configPaths.ts for better functionality
336
832
  */
337
833
  export function hasHooksConfiguration(workdir: string): boolean {
338
- return (
339
- existsSync(getUserHooksConfigPath()) ||
340
- existsSync(getProjectHooksConfigPath(workdir))
341
- );
834
+ return hasAnyConfig(workdir);
342
835
  }
343
836
 
344
837
  /**
345
838
  * Get hooks configuration information for debugging
839
+ * Includes both .local.json and .json variants
840
+ * @deprecated Use getConfigurationInfo() from configPaths.ts for better functionality
346
841
  */
347
842
  export function getHooksConfigurationInfo(workdir: string): {
348
843
  hasUser: boolean;
349
844
  hasProject: boolean;
350
845
  paths: string[];
846
+ userPaths: string[];
847
+ projectPaths: string[];
848
+ existingPaths: string[];
351
849
  } {
352
- const userPath = getUserHooksConfigPath();
353
- const projectPath = getProjectHooksConfigPath(workdir);
354
-
355
- return {
356
- hasUser: existsSync(userPath),
357
- hasProject: existsSync(projectPath),
358
- paths: [userPath, projectPath],
359
- };
850
+ return getConfigurationInfo(workdir);
360
851
  }