wave-agent-sdk 0.16.9 → 0.16.12

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 (85) hide show
  1. package/dist/agent.d.ts +5 -0
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +18 -0
  4. package/dist/constants/toolLimits.d.ts +2 -0
  5. package/dist/constants/toolLimits.d.ts.map +1 -1
  6. package/dist/constants/toolLimits.js +2 -0
  7. package/dist/managers/aiManager.d.ts +5 -0
  8. package/dist/managers/aiManager.d.ts.map +1 -1
  9. package/dist/managers/aiManager.js +21 -0
  10. package/dist/managers/hookManager.d.ts +6 -3
  11. package/dist/managers/hookManager.d.ts.map +1 -1
  12. package/dist/managers/hookManager.js +36 -13
  13. package/dist/managers/mcpManager.d.ts +4 -28
  14. package/dist/managers/mcpManager.d.ts.map +1 -1
  15. package/dist/managers/mcpManager.js +10 -127
  16. package/dist/services/authService.d.ts +33 -1
  17. package/dist/services/authService.d.ts.map +1 -1
  18. package/dist/services/authService.js +212 -11
  19. package/dist/services/configurationService.d.ts +1 -0
  20. package/dist/services/configurationService.d.ts.map +1 -1
  21. package/dist/services/configurationService.js +48 -6
  22. package/dist/services/hook.d.ts +4 -0
  23. package/dist/services/hook.d.ts.map +1 -1
  24. package/dist/services/hook.js +10 -0
  25. package/dist/services/initializationService.d.ts.map +1 -1
  26. package/dist/services/initializationService.js +11 -0
  27. package/dist/services/interactionService.d.ts.map +1 -1
  28. package/dist/services/interactionService.js +0 -12
  29. package/dist/services/remoteSettingsService.d.ts +21 -0
  30. package/dist/services/remoteSettingsService.d.ts.map +1 -0
  31. package/dist/services/remoteSettingsService.js +280 -0
  32. package/dist/tools/bashTool.d.ts.map +1 -1
  33. package/dist/tools/bashTool.js +58 -32
  34. package/dist/tools/types.d.ts +4 -0
  35. package/dist/tools/types.d.ts.map +1 -1
  36. package/dist/types/agent.d.ts +7 -0
  37. package/dist/types/agent.d.ts.map +1 -1
  38. package/dist/types/auth.d.ts +12 -0
  39. package/dist/types/auth.d.ts.map +1 -1
  40. package/dist/types/configuration.d.ts +20 -0
  41. package/dist/types/configuration.d.ts.map +1 -1
  42. package/dist/types/hooks.d.ts +5 -1
  43. package/dist/types/hooks.d.ts.map +1 -1
  44. package/dist/types/hooks.js +1 -0
  45. package/dist/types/mcp.d.ts +1 -1
  46. package/dist/types/mcp.d.ts.map +1 -1
  47. package/dist/utils/containerSetup.d.ts.map +1 -1
  48. package/dist/utils/containerSetup.js +9 -8
  49. package/dist/utils/gitUtils.d.ts +18 -1
  50. package/dist/utils/gitUtils.d.ts.map +1 -1
  51. package/dist/utils/gitUtils.js +120 -49
  52. package/dist/utils/mcpUtils.d.ts.map +1 -1
  53. package/dist/utils/mcpUtils.js +6 -1
  54. package/dist/utils/openaiClient.d.ts.map +1 -1
  55. package/dist/utils/openaiClient.js +4 -2
  56. package/dist/utils/toolResultStorage.d.ts +46 -0
  57. package/dist/utils/toolResultStorage.d.ts.map +1 -0
  58. package/dist/utils/toolResultStorage.js +90 -0
  59. package/dist/utils/worktreeUtils.d.ts.map +1 -1
  60. package/dist/utils/worktreeUtils.js +58 -0
  61. package/package.json +3 -3
  62. package/src/agent.ts +20 -0
  63. package/src/constants/toolLimits.ts +3 -0
  64. package/src/managers/aiManager.ts +37 -0
  65. package/src/managers/hookManager.ts +42 -17
  66. package/src/managers/mcpManager.ts +10 -178
  67. package/src/services/authService.ts +243 -16
  68. package/src/services/configurationService.ts +58 -6
  69. package/src/services/hook.ts +15 -0
  70. package/src/services/initializationService.ts +13 -0
  71. package/src/services/interactionService.ts +0 -18
  72. package/src/services/remoteSettingsService.ts +315 -0
  73. package/src/tools/bashTool.ts +70 -38
  74. package/src/tools/types.ts +4 -0
  75. package/src/types/agent.ts +7 -0
  76. package/src/types/auth.ts +10 -0
  77. package/src/types/configuration.ts +23 -0
  78. package/src/types/hooks.ts +7 -1
  79. package/src/types/mcp.ts +1 -1
  80. package/src/utils/containerSetup.ts +8 -8
  81. package/src/utils/gitUtils.ts +123 -48
  82. package/src/utils/mcpUtils.ts +12 -1
  83. package/src/utils/openaiClient.ts +5 -2
  84. package/src/utils/toolResultStorage.ts +117 -0
  85. package/src/utils/worktreeUtils.ts +63 -0
@@ -40,6 +40,7 @@ import { logOTelEvent } from "../telemetry/events.js";
40
40
  export interface AIManagerCallbacks {
41
41
  onCompactionStateChange?: (isCompacting: boolean) => void;
42
42
  onUsageAdded?: (usage: Usage) => void;
43
+ onCwdChange?: (newCwd: string) => void;
43
44
  }
44
45
 
45
46
  export interface AIManagerOptions {
@@ -64,6 +65,8 @@ export class AIManager {
64
65
  private subagentType?: string; // Store subagent type for hook context
65
66
  private stream: boolean; // Streaming mode flag
66
67
  private modelOverride?: string;
68
+ private _onCwdChange?: (newCwd: string) => void; // Store callback for CWD changes
69
+ private originalWorkdir: string;
67
70
  private consecutiveCompactionFailures: number = 0;
68
71
  private readonly maxTurns?: number;
69
72
 
@@ -77,6 +80,8 @@ export class AIManager {
77
80
  this.stream = options.stream ?? true; // Default to true if not specified
78
81
  this.callbacks = options.callbacks ?? {};
79
82
  this.modelOverride = options.modelOverride;
83
+ this._onCwdChange = options.callbacks?.onCwdChange; // Initialize onCwdChange
84
+ this.originalWorkdir = options.workdir;
80
85
  this.maxTurns = options.maxTurns;
81
86
  }
82
87
 
@@ -174,6 +179,10 @@ export class AIManager {
174
179
  return this.container.get<string>("Workdir") ?? process.cwd();
175
180
  }
176
181
 
182
+ public getOriginalWorkdir(): string {
183
+ return this.originalWorkdir;
184
+ }
185
+
177
186
  /**
178
187
  * Update the working directory mid-session (e.g., when entering/exiting a worktree).
179
188
  * Also updates process.chdir() so bash commands use the new directory.
@@ -183,6 +192,10 @@ export class AIManager {
183
192
  process.chdir(newWorkdir);
184
193
  }
185
194
 
195
+ public setOnCwdChange(callback: (newCwd: string) => void): void {
196
+ this._onCwdChange = callback;
197
+ }
198
+
186
199
  private isCompacting: boolean = false;
187
200
  private callbacks: AIManagerCallbacks;
188
201
 
@@ -248,6 +261,7 @@ export class AIManager {
248
261
  if (toolPlugin?.formatCompactParams) {
249
262
  const context: ToolContext = {
250
263
  workdir: this.getWorkdir(),
264
+ originalWorkdir: this.originalWorkdir,
251
265
  taskManager: this.taskManager,
252
266
  };
253
267
  return toolPlugin.formatCompactParams(toolArgs, context);
@@ -998,6 +1012,7 @@ export class AIManager {
998
1012
  abortSignal: toolAbortController.signal,
999
1013
  backgroundTaskManager: this.backgroundTaskManager,
1000
1014
  workdir: this.getWorkdir(),
1015
+ originalWorkdir: this.originalWorkdir,
1001
1016
  messageId: this.messageManager.getMessages().slice(-1)[0]?.id,
1002
1017
  sessionId: this.messageManager.getSessionId(),
1003
1018
  toolCallId: toolId,
@@ -1016,6 +1031,28 @@ export class AIManager {
1016
1031
  stage: "running", // Keep it in running stage while updating result
1017
1032
  });
1018
1033
  },
1034
+ onCwdChange: async (newCwd: string) => {
1035
+ const oldCwd = this.getWorkdir();
1036
+ this.container.register("Workdir", newCwd);
1037
+ this._onCwdChange?.(newCwd);
1038
+ if (this.hookManager) {
1039
+ const sessionId = this.messageManager.getSessionId();
1040
+ const transcriptPath =
1041
+ this.messageManager.getTranscriptPath();
1042
+ const env = Object.fromEntries(
1043
+ Object.entries(process.env).filter(
1044
+ (e) => e[1] !== undefined,
1045
+ ),
1046
+ ) as Record<string, string>;
1047
+ await this.hookManager.executeCwdChangedHooks(
1048
+ oldCwd,
1049
+ newCwd,
1050
+ sessionId,
1051
+ transcriptPath,
1052
+ env,
1053
+ );
1054
+ }
1055
+ },
1019
1056
  };
1020
1057
 
1021
1058
  // Execute tool
@@ -44,23 +44,13 @@ export class HookManager {
44
44
  }
45
45
 
46
46
  /**
47
- * Load and merge hook configurations from user and project settings
48
- * Project settings take precedence over user settings
47
+ * Load hook configuration from programmatic source (AgentOptions.hooks)
49
48
  */
50
- loadConfiguration(
51
- userHooks?: PartialHookConfiguration,
52
- projectHooks?: PartialHookConfiguration,
53
- ): void {
49
+ loadConfiguration(hooks?: PartialHookConfiguration): void {
54
50
  const merged: PartialHookConfiguration = {};
55
51
 
56
- // Start with user hooks
57
- if (userHooks) {
58
- this.mergeHooksConfiguration(merged, userHooks);
59
- }
60
-
61
- // Override with project hooks (project settings take precedence)
62
- if (projectHooks) {
63
- this.mergeHooksConfiguration(merged, projectHooks);
52
+ if (hooks) {
53
+ this.mergeHooksConfiguration(merged, hooks);
64
54
  }
65
55
 
66
56
  // Validate merged configuration
@@ -654,9 +644,12 @@ export class HookManager {
654
644
  ): void {
655
645
  for (const [event, configs] of Object.entries(source)) {
656
646
  if (isValidHookEvent(event)) {
657
- // For now, completely replace event configs rather than merging
658
- // This ensures project settings completely override user settings for each event
659
- target[event] = [...configs];
647
+ // Concatenate hook configs so multiple sources (programmatic, file-based, plugins) coexist
648
+ if (!target[event]) {
649
+ target[event] = [...configs];
650
+ } else {
651
+ target[event] = [...target[event], ...configs];
652
+ }
660
653
  }
661
654
  }
662
655
  }
@@ -676,6 +669,7 @@ export class HookManager {
676
669
  event === "SubagentStop" ||
677
670
  event === "WorktreeCreate" ||
678
671
  event === "WorktreeRemove" ||
672
+ event === "CwdChanged" ||
679
673
  event === "SessionStart" ||
680
674
  event === "SessionEnd"
681
675
  ) {
@@ -781,6 +775,7 @@ export class HookManager {
781
775
  PermissionRequest: 0,
782
776
  WorktreeCreate: 0,
783
777
  WorktreeRemove: 0,
778
+ CwdChanged: 0,
784
779
  SessionStart: 0,
785
780
  SessionEnd: 0,
786
781
  },
@@ -796,6 +791,7 @@ export class HookManager {
796
791
  PermissionRequest: 0,
797
792
  WorktreeCreate: 0,
798
793
  WorktreeRemove: 0,
794
+ CwdChanged: 0,
799
795
  SessionStart: 0,
800
796
  SessionEnd: 0,
801
797
  };
@@ -822,6 +818,35 @@ export class HookManager {
822
818
  };
823
819
  }
824
820
 
821
+ /**
822
+ * Execute CwdChanged hooks.
823
+ */
824
+ async executeCwdChangedHooks(
825
+ oldCwd: string,
826
+ newCwd: string,
827
+ sessionId: string,
828
+ transcriptPath: string,
829
+ env: Record<string, string>,
830
+ ): Promise<HookExecutionResult[]> {
831
+ const context: ExtendedHookExecutionContext = {
832
+ event: "CwdChanged",
833
+ projectDir: this.workdir,
834
+ timestamp: new Date(),
835
+ sessionId,
836
+ transcriptPath,
837
+ cwd: newCwd,
838
+ oldCwd,
839
+ newCwd,
840
+ env,
841
+ };
842
+ const results = await this.executeHooks("CwdChanged", context);
843
+ if (results.length > 0) {
844
+ // For CwdChanged hooks, we don't block, just log errors
845
+ this.processHookResults("CwdChanged", results);
846
+ }
847
+ return results;
848
+ }
849
+
825
850
  /**
826
851
  * Register hooks provided by a plugin
827
852
  */
@@ -34,27 +34,19 @@ export interface McpManagerOptions {
34
34
  logger?: Logger;
35
35
  /** Pre-configured MCP servers passed from constructor options */
36
36
  mcpServers?: Record<string, McpServerConfig>;
37
- /** Wave server URL for resolving ${WAVE_SERVER_URL} templates in MCP configs */
38
- serverUrl?: string;
39
- /** SSO token for resolving ${WAVE_SSO_TOKEN} templates in MCP configs */
40
- ssoToken?: string;
41
37
  }
42
38
 
43
39
  /**
44
40
  * Expand environment variables in a string value.
45
41
  * Supports ${VAR} and ${VAR:-default} patterns.
46
42
  */
47
- const WAVE_TEMPLATE_VARS = [
48
- "WAVE_SERVER_URL",
49
- "WAVE_SSO_TOKEN",
50
- "WAVE_PLUGIN_ROOT",
51
- ];
43
+ const WAVE_TEMPLATE_VARS = ["WAVE_PLUGIN_ROOT"];
52
44
 
53
45
  export function expandEnvVars(value: string): string {
54
46
  return value.replace(/\$\{([^}]+)\}/g, (_match, expr: string) => {
55
47
  const [varName, ...rest] = expr.split(":-");
56
48
  const defaultValue = rest.join(":-");
57
- // Skip Wave-specific template variables — they are handled by resolveMcpTemplates
49
+ // Skip Wave-specific template variables — they are handled at spawn time
58
50
  if (WAVE_TEMPLATE_VARS.includes(varName)) {
59
51
  return _match; // return original ${...} string untouched
60
52
  }
@@ -63,81 +55,15 @@ export function expandEnvVars(value: string): string {
63
55
  }
64
56
 
65
57
  /**
66
- * Context for resolving Wave-specific MCP template variables.
58
+ * Walk an MCP config and resolve environment variables in all string fields.
59
+ * Only expands ${VAR} from process.env (skipping WAVE_PLUGIN_ROOT which is
60
+ * handled at spawn time).
67
61
  */
68
- export interface McpResolverContext {
69
- serverUrl?: string; // resolves ${WAVE_SERVER_URL}
70
- ssoToken?: string; // resolves ${WAVE_SSO_TOKEN}
71
- }
72
-
73
- /**
74
- * Walk a single McpServerConfig and replace Wave template variables.
75
- * Only replaces ${WAVE_SERVER_URL} and ${WAVE_SSO_TOKEN} — does not touch
76
- * arbitrary env vars (that is what expandEnvVars handles).
77
- */
78
- export function resolveMcpTemplates(
79
- config: McpServerConfig,
80
- ctx: McpResolverContext,
81
- ): McpServerConfig {
82
- const resolved: McpServerConfig = { ...config };
83
-
84
- const replace = (value: string): string => {
85
- let result = value;
86
- if (ctx.serverUrl !== undefined) {
87
- result = result.replace(/\$\{WAVE_SERVER_URL\}/g, ctx.serverUrl);
88
- }
89
- if (ctx.ssoToken !== undefined) {
90
- result = result.replace(/\$\{WAVE_SSO_TOKEN\}/g, ctx.ssoToken);
91
- }
92
- return result;
93
- };
94
-
95
- if (resolved.command) {
96
- resolved.command = replace(resolved.command);
97
- }
98
-
99
- if (resolved.args) {
100
- resolved.args = resolved.args.map(replace);
101
- }
102
-
103
- if (resolved.env) {
104
- const resolvedEnv: Record<string, string> = {};
105
- for (const [key, val] of Object.entries(resolved.env)) {
106
- resolvedEnv[key] = replace(val);
107
- }
108
- resolved.env = resolvedEnv;
109
- }
110
-
111
- if (resolved.url) {
112
- resolved.url = replace(resolved.url);
113
- }
114
-
115
- if (resolved.headers) {
116
- const resolvedHeaders: Record<string, string> = {};
117
- for (const [key, val] of Object.entries(resolved.headers)) {
118
- resolvedHeaders[key] = replace(val);
119
- }
120
- resolved.headers = resolvedHeaders;
121
- }
122
-
123
- return resolved;
124
- }
125
-
126
- /**
127
- * Walk an MCP config and resolve variables in all string fields.
128
- * Applies two steps in order:
129
- * 1. expandEnvVars — resolves ${VAR} from process.env
130
- * 2. resolveMcpTemplates — resolves ${WAVE_SERVER_URL}, ${WAVE_SSO_TOKEN} from context
131
- */
132
- export function resolveMcpConfig(
133
- config: McpConfig,
134
- ctx?: McpResolverContext,
135
- ): McpConfig {
62
+ export function resolveMcpConfig(config: McpConfig): McpConfig {
136
63
  const resolved: McpConfig = { mcpServers: {} };
137
64
 
138
65
  for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
139
- // Step 1: expand env vars from process.env
140
- let resolvedServer: McpServerConfig = { ...serverConfig };
66
+ const resolvedServer: McpServerConfig = { ...serverConfig };
141
67
 
142
68
  if (resolvedServer.command) {
143
69
  resolvedServer.command = expandEnvVars(resolvedServer.command);
@@ -167,11 +93,6 @@ export function resolveMcpConfig(
167
93
  resolvedServer.headers = resolvedHeaders;
168
94
  }
169
95
 
170
- // Step 2: resolve Wave template variables from context
171
- if (ctx) {
172
- resolvedServer = resolveMcpTemplates(resolvedServer, ctx);
173
- }
174
-
175
96
  resolved.mcpServers[name] = resolvedServer;
176
97
  }
177
98
 
@@ -187,7 +108,6 @@ export class McpManager {
187
108
  private callbacks: McpManagerCallbacks;
188
109
  private mcpServers: Record<string, McpServerConfig> | undefined;
189
110
 
190
- private resolverCtx: McpResolverContext | undefined;
191
111
  private reconnectTimers: Map<string, NodeJS.Timeout> = new Map();
192
112
  private reconnectAttempts: Map<string, number> = new Map();
193
113
 
@@ -197,10 +117,6 @@ export class McpManager {
197
117
  ) {
198
118
  this.callbacks = options.callbacks || {};
199
119
  this.mcpServers = options.mcpServers;
200
- this.resolverCtx = {
201
- serverUrl: options.serverUrl,
202
- ssoToken: options.ssoToken,
203
- };
204
120
  }
205
121
 
206
122
  /**
@@ -273,7 +189,7 @@ export class McpManager {
273
189
  try {
274
190
  const configContent = await fs.readFile(this.configPath, "utf-8");
275
191
  const rawConfig: McpConfig = JSON.parse(configContent);
276
- const workspaceConfig = resolveMcpConfig(rawConfig, this.resolverCtx);
192
+ const workspaceConfig = resolveMcpConfig(rawConfig);
277
193
 
278
194
  // Extract original (pre-resolution) URLs for safe display
279
195
  const originalUrls: Record<string, string | undefined> = {};
@@ -368,8 +284,8 @@ export class McpManager {
368
284
  // Capture original URL before any resolution for safe display
369
285
  const originalUrl = config.url;
370
286
 
371
- // Step 1: expand env vars from process.env (e.g. ${TAVILY_API_KEY})
372
- let resolvedConfig: McpServerConfig = { ...config };
287
+ // Expand env vars from process.env (e.g. ${TAVILY_API_KEY})
288
+ const resolvedConfig: McpServerConfig = { ...config };
373
289
  if (resolvedConfig.command) {
374
290
  resolvedConfig.command = expandEnvVars(resolvedConfig.command);
375
291
  }
@@ -394,12 +310,6 @@ export class McpManager {
394
310
  resolvedConfig.headers = resolvedHeaders;
395
311
  }
396
312
 
397
- // Step 2: resolve Wave template variables (e.g. ${WAVE_SERVER_URL}, ${WAVE_SSO_TOKEN})
398
- resolvedConfig = resolveMcpTemplates(
399
- resolvedConfig,
400
- this.resolverCtx ?? { serverUrl: undefined, ssoToken: undefined },
401
- );
402
-
403
313
  const newServer: McpServerStatus = {
404
314
  name,
405
315
  config: resolvedConfig,
@@ -921,84 +831,6 @@ export class McpManager {
921
831
  await Promise.all(disconnectPromises);
922
832
  }
923
833
 
924
- /**
925
- * Update credentials and reconnect MCP servers that use template variables.
926
- * Called after SSO login to refresh ${WAVE_SSO_TOKEN} and ${WAVE_SERVER_URL}.
927
- */
928
- async refreshCredentials(
929
- serverUrl?: string,
930
- ssoToken?: string,
931
- ): Promise<void> {
932
- // Update resolver context
933
- this.resolverCtx = {
934
- serverUrl: serverUrl ?? this.resolverCtx?.serverUrl,
935
- ssoToken: ssoToken ?? this.resolverCtx?.ssoToken,
936
- };
937
-
938
- logger?.info(
939
- `MCP refreshCredentials: serverUrl=${serverUrl}, hasToken=${!!ssoToken}`,
940
- );
941
-
942
- // Collect servers that need reconnection
943
- const serversToReconnect: string[] = [];
944
-
945
- for (const [name, server] of this.servers) {
946
- // Re-resolve config with new credentials
947
- const originalConfig = server.config;
948
- const resolvedConfig = resolveMcpTemplates(
949
- originalConfig,
950
- this.resolverCtx,
951
- );
952
-
953
- // Update the stored config, preserving originalUrl
954
- this.servers.set(name, {
955
- ...server,
956
- config: resolvedConfig,
957
- });
958
-
959
- if (this.config && this.config.mcpServers[name]) {
960
- this.config.mcpServers[name] = resolvedConfig;
961
- }
962
-
963
- // Determine if reconnection is needed
964
- const wasConnected = this.connections.has(name);
965
- const wasDisconnected =
966
- server.status === "disconnected" ||
967
- server.status === "error" ||
968
- server.status === "reconnecting";
969
-
970
- if (wasConnected) {
971
- // Disconnect first, then reconnect with new resolved config
972
- await this.disconnectServer(name);
973
- serversToReconnect.push(name);
974
- } else if (wasDisconnected) {
975
- // Was disconnected or errored — try to reconnect now that we have credentials
976
- serversToReconnect.push(name);
977
- }
978
- }
979
-
980
- // Reconnect servers
981
- for (const name of serversToReconnect) {
982
- logger?.debug(
983
- `Reconnecting MCP server after credential refresh: ${name}`,
984
- );
985
- this.connectServer(name)
986
- .then((success) => {
987
- if (success) {
988
- logger?.info(`Successfully reconnected MCP server: ${name}`);
989
- } else {
990
- logger?.warn(`Failed to reconnect MCP server: ${name}`);
991
- }
992
- })
993
- .catch((error) => {
994
- logger?.error(`Reconnection to MCP server ${name} failed:`, error);
995
- });
996
- }
997
-
998
- // Trigger state change callback
999
- this.callbacks.onMcpServersChange?.(this.getAllServers());
1000
- }
1001
-
1002
834
  // ========== Tools Registry Methods ==========
1003
835
 
1004
836
  /**