wave-agent-sdk 1.0.3 → 1.0.4

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.
package/dist/agent.d.ts CHANGED
@@ -299,6 +299,25 @@ export declare class Agent {
299
299
  path: string;
300
300
  mimeType: string;
301
301
  }>): Promise<void>;
302
+ /**
303
+ * Add an additional directory to the Safe Zone for this session.
304
+ *
305
+ * The directory immediately becomes part of the Safe Zone (permission
306
+ * checks and the system prompt), and with `remember: true` it is also
307
+ * persisted to `.wave/settings.local.json` `permissions.additionalDirectories`
308
+ * so future sessions pick it up automatically.
309
+ *
310
+ * @param directory Path of the directory to add (relative paths resolve against the workdir).
311
+ * @param options.remember Whether to persist the directory across sessions.
312
+ */
313
+ addAdditionalDirectory(directory: string, options?: {
314
+ remember?: boolean;
315
+ }): Promise<void>;
316
+ /**
317
+ * Get the effective additional directories for this session (config +
318
+ * session-level union), matching what is listed in the system prompt.
319
+ */
320
+ getAdditionalDirectories(): string[];
302
321
  /** Get all MCP server status */
303
322
  getMcpServers(): McpServerStatus[];
304
323
  /** Connect MCP server */
package/dist/agent.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { LspManager } from "./managers/lspManager.js";
2
2
  import { configValidator } from "./utils/configValidator.js";
3
+ import path from "node:path";
3
4
  import { parseTaskNotificationXml } from "./utils/notificationXml.js";
4
5
  import { InitializationService } from "./services/initializationService.js";
5
6
  import { InteractionService } from "./services/interactionService.js";
@@ -790,6 +791,34 @@ export class Agent {
790
791
  abortMessage: () => this.abortMessage(),
791
792
  }, content, images);
792
793
  }
794
+ // ========== Additional Directories ==========
795
+ /**
796
+ * Add an additional directory to the Safe Zone for this session.
797
+ *
798
+ * The directory immediately becomes part of the Safe Zone (permission
799
+ * checks and the system prompt), and with `remember: true` it is also
800
+ * persisted to `.wave/settings.local.json` `permissions.additionalDirectories`
801
+ * so future sessions pick it up automatically.
802
+ *
803
+ * @param directory Path of the directory to add (relative paths resolve against the workdir).
804
+ * @param options.remember Whether to persist the directory across sessions.
805
+ */
806
+ async addAdditionalDirectory(directory, options) {
807
+ const resolvedPath = path.isAbsolute(directory)
808
+ ? directory
809
+ : path.resolve(this.workdir, directory);
810
+ this.permissionManager.addInstanceAdditionalDirectory(resolvedPath);
811
+ if (options?.remember) {
812
+ await this.configurationService.addAdditionalDirectory(this.workdir, resolvedPath);
813
+ }
814
+ }
815
+ /**
816
+ * Get the effective additional directories for this session (config +
817
+ * session-level union), matching what is listed in the system prompt.
818
+ */
819
+ getAdditionalDirectories() {
820
+ return this.permissionManager.getEffectiveAdditionalDirectories();
821
+ }
793
822
  // ========== MCP Management Methods ==========
794
823
  /** Get all MCP server status */
795
824
  getMcpServers() {
@@ -482,6 +482,7 @@ export class AIManager {
482
482
  language: this.getLanguage(),
483
483
  isSubagent: !!this.subagentType,
484
484
  worktreeSession: this.getWorktreeSession(),
485
+ additionalWorkingDirectories: this.permissionManager?.getEffectiveAdditionalDirectories?.() ?? [],
485
486
  autoMemory: autoMemoryOptions,
486
487
  });
487
488
  }
@@ -21,6 +21,8 @@ export interface PermissionManagerOptions {
21
21
  instanceDeniedRules?: string[];
22
22
  /** Additional directories considered part of the Safe Zone */
23
23
  additionalDirectories?: string[];
24
+ /** Instance-specific additional directories (from AgentOptions, session-level) */
25
+ instanceAdditionalDirectories?: string[];
24
26
  /** System additional directories (persistent across reloads) */
25
27
  systemAdditionalDirectories?: string[];
26
28
  /** Path to the current plan file */
@@ -37,6 +39,7 @@ export declare class PermissionManager {
37
39
  private instanceDeniedRules;
38
40
  private temporaryRules;
39
41
  private additionalDirectories;
42
+ private instanceAdditionalDirectories;
40
43
  private systemAdditionalDirectories;
41
44
  private planFilePath?;
42
45
  private hasExitedPlanMode;
@@ -83,6 +86,15 @@ export declare class PermissionManager {
83
86
  * Get all additional directories
84
87
  */
85
88
  getAdditionalDirectories(): string[];
89
+ /**
90
+ * Get all instance-specific additional directories (session-level, from AgentOptions or /add-dir)
91
+ */
92
+ getInstanceAdditionalDirectories(): string[];
93
+ /**
94
+ * Get the effective union of additional directories (config + instance, deduplicated).
95
+ * This is the data source for system prompt injection and Safe Zone checks.
96
+ */
97
+ getEffectiveAdditionalDirectories(): string[];
86
98
  /**
87
99
  * Get all system additional directories
88
100
  */
@@ -111,6 +123,10 @@ export declare class PermissionManager {
111
123
  * Update the additional directories (e.g., when configuration reloads)
112
124
  */
113
125
  updateAdditionalDirectories(directories: string[]): void;
126
+ /**
127
+ * Add an instance-level additional directory (session-level, e.g. /add-dir command)
128
+ */
129
+ addInstanceAdditionalDirectory(directory: string): void;
114
130
  /**
115
131
  * Add a system-level additional directory that is persistent across configuration reloads
116
132
  */
@@ -63,6 +63,7 @@ export class PermissionManager {
63
63
  this.instanceDeniedRules = [];
64
64
  this.temporaryRules = [];
65
65
  this.additionalDirectories = [];
66
+ this.instanceAdditionalDirectories = [];
66
67
  this.systemAdditionalDirectories = [];
67
68
  this.hasExitedPlanMode = false;
68
69
  this.needsPlanModeExitAttachment = false;
@@ -73,13 +74,17 @@ export class PermissionManager {
73
74
  this.instanceDeniedRules = options.instanceDeniedRules || [];
74
75
  this.planFilePath = options.planFilePath;
75
76
  this._logger = options.logger;
77
+ // Read workdir first so relative additional directories resolve against it.
78
+ this.workdir = this.container.get("Workdir");
79
+ this.worktreeName = this.container.get("WorktreeName");
80
+ this.mainRepoRoot = this.container.get("MainRepoRoot");
76
81
  this.updateAdditionalDirectories(options.additionalDirectories || []);
82
+ for (const dir of options.instanceAdditionalDirectories || []) {
83
+ this.addInstanceAdditionalDirectory(dir);
84
+ }
77
85
  for (const dir of options.systemAdditionalDirectories || []) {
78
86
  this.addSystemAdditionalDirectory(dir);
79
87
  }
80
- this.workdir = this.container.get("Workdir");
81
- this.worktreeName = this.container.get("WorktreeName");
82
- this.mainRepoRoot = this.container.get("MainRepoRoot");
83
88
  }
84
89
  /**
85
90
  * Resolve the working directory from the DI container
@@ -141,6 +146,30 @@ export class PermissionManager {
141
146
  getAdditionalDirectories() {
142
147
  return [...this.additionalDirectories];
143
148
  }
149
+ /**
150
+ * Get all instance-specific additional directories (session-level, from AgentOptions or /add-dir)
151
+ */
152
+ getInstanceAdditionalDirectories() {
153
+ return [...this.instanceAdditionalDirectories];
154
+ }
155
+ /**
156
+ * Get the effective union of additional directories (config + instance, deduplicated).
157
+ * This is the data source for system prompt injection and Safe Zone checks.
158
+ */
159
+ getEffectiveAdditionalDirectories() {
160
+ const seen = new Set();
161
+ const result = [];
162
+ for (const dir of [
163
+ ...this.additionalDirectories,
164
+ ...this.instanceAdditionalDirectories,
165
+ ]) {
166
+ if (!seen.has(dir)) {
167
+ seen.add(dir);
168
+ result.push(dir);
169
+ }
170
+ }
171
+ return result;
172
+ }
144
173
  /**
145
174
  * Get all system additional directories
146
175
  */
@@ -189,6 +218,18 @@ export class PermissionManager {
189
218
  return path.resolve(dir);
190
219
  });
191
220
  }
221
+ /**
222
+ * Add an instance-level additional directory (session-level, e.g. /add-dir command)
223
+ */
224
+ addInstanceAdditionalDirectory(directory) {
225
+ const workdir = this.workdir;
226
+ const resolvedPath = workdir && !path.isAbsolute(directory)
227
+ ? path.resolve(workdir, directory)
228
+ : path.resolve(directory);
229
+ if (!this.instanceAdditionalDirectories.includes(resolvedPath)) {
230
+ this.instanceAdditionalDirectories.push(resolvedPath);
231
+ }
232
+ }
192
233
  /**
193
234
  * Add a system-level additional directory that is persistent across configuration reloads
194
235
  */
@@ -250,6 +291,12 @@ export class PermissionManager {
250
291
  return { isInside: true, resolvedPath: absolutePath };
251
292
  }
252
293
  }
294
+ // Check instance additional directories
295
+ for (const dir of this.instanceAdditionalDirectories) {
296
+ if (isPathInside(absolutePath, dir)) {
297
+ return { isInside: true, resolvedPath: absolutePath };
298
+ }
299
+ }
253
300
  // Check system additional directories
254
301
  for (const dir of this.systemAdditionalDirectories) {
255
302
  if (isPathInside(absolutePath, dir)) {
@@ -178,6 +178,7 @@ export class SubagentManager {
178
178
  TASK_LIST_TOOL_NAME,
179
179
  ],
180
180
  additionalDirectories: parentPermissionManager?.getAdditionalDirectories(),
181
+ instanceAdditionalDirectories: parentPermissionManager?.getInstanceAdditionalDirectories?.(),
181
182
  systemAdditionalDirectories: parentPermissionManager?.getSystemAdditionalDirectories(),
182
183
  planFilePath: parentPermissionManager?.getPlanFilePath(),
183
184
  });
@@ -40,6 +40,7 @@ export declare function buildSystemPrompt(basePrompt: string | undefined, tools:
40
40
  language?: string;
41
41
  isSubagent?: boolean;
42
42
  worktreeSession?: WorktreeSession | null;
43
+ additionalWorkingDirectories?: string[];
43
44
  autoMemory?: {
44
45
  directory: string;
45
46
  content: string;
@@ -346,10 +346,14 @@ export function buildSystemPrompt(basePrompt, tools, options = {}) {
346
346
  const osVersion = getUnameSR();
347
347
  const primaryWorkdir = options.originalWorkdir ?? options.workdir;
348
348
  const worktreeSession = options.worktreeSession;
349
+ const additionalWorkingDirectories = options.additionalWorkingDirectories || [];
349
350
  if (options.isSubagent) {
350
351
  // Subagent env section, aligned with Claude Code's computeEnvInfo() +
351
352
  // enhanceSystemPromptWithEnvDetails() (without the model description and
352
353
  // knowledge cutoff lines, which Wave does not use).
354
+ const additionalDirsInfo = additionalWorkingDirectories.length > 0
355
+ ? `Additional working directories: ${additionalWorkingDirectories.join(", ")}\n`
356
+ : "";
353
357
  dynamicText += `
354
358
 
355
359
  ${SUBAGENT_ENV_NOTES}
@@ -358,7 +362,7 @@ Here is useful information about the environment you are running in:
358
362
  <env>
359
363
  Working directory: ${primaryWorkdir}
360
364
  Is directory a git repo: ${isGitRepo}
361
- Platform: ${platform}
365
+ ${additionalDirsInfo}Platform: ${platform}
362
366
  ${shellInfo}
363
367
  OS Version: ${osVersion}
364
368
  </env>
@@ -374,6 +378,12 @@ OS Version: ${osVersion}
374
378
  ? `This is a git worktree — an isolated copy of the repository. Run all commands from this directory. Do NOT \`cd\` to the original repository root.`
375
379
  : null,
376
380
  `Is a git repository: ${isGitRepo}`,
381
+ additionalWorkingDirectories.length > 0
382
+ ? `Additional working directories:`
383
+ : null,
384
+ additionalWorkingDirectories.length > 0
385
+ ? additionalWorkingDirectories
386
+ : null,
377
387
  `Platform: ${platform}`,
378
388
  shellInfo,
379
389
  `OS Version: ${osVersion}`,
@@ -381,7 +391,9 @@ OS Version: ${osVersion}
381
391
  const envBlock = [
382
392
  `# Environment`,
383
393
  `You have been invoked in the following environment: `,
384
- ...envItems.map((item) => ` - ${item}`),
394
+ ...envItems.flatMap((item) => Array.isArray(item)
395
+ ? item.map((subItem) => ` - ${subItem}`)
396
+ : [` - ${item}`]),
385
397
  ].join("\n");
386
398
  dynamicText += `\n\n${envBlock}`;
387
399
  }
@@ -144,6 +144,11 @@ export declare class ConfigurationService {
144
144
  * Add a permission rule to the local settings.local.json
145
145
  */
146
146
  addAllowedRule(workdir: string, rule: string): Promise<void>;
147
+ /**
148
+ * Add an additional directory to the local settings.local.json
149
+ * `permissions.additionalDirectories`, so it persists across sessions.
150
+ */
151
+ addAdditionalDirectory(workdir: string, directory: string): Promise<void>;
147
152
  /**
148
153
  * Update the enabled state of a plugin in the specified scope
149
154
  */
@@ -741,6 +741,39 @@ export class ConfigurationService {
741
741
  await atomicWriteFile(localConfigPath, JSON.stringify(config, null, 2));
742
742
  }
743
743
  }
744
+ /**
745
+ * Add an additional directory to the local settings.local.json
746
+ * `permissions.additionalDirectories`, so it persists across sessions.
747
+ */
748
+ async addAdditionalDirectory(workdir, directory) {
749
+ const localConfigPath = path.join(workdir, ".wave", "settings.local.json");
750
+ // Ensure .wave directory exists
751
+ const waveDir = path.join(workdir, ".wave");
752
+ ensureWaveRuntimeFilesExcluded(workdir);
753
+ if (!existsSync(waveDir)) {
754
+ await fs.mkdir(waveDir, { recursive: true });
755
+ }
756
+ let config = {};
757
+ if (existsSync(localConfigPath)) {
758
+ try {
759
+ const content = await fs.readFile(localConfigPath, "utf-8");
760
+ config = JSON.parse(content);
761
+ }
762
+ catch {
763
+ // If file is corrupted, start with empty config
764
+ }
765
+ }
766
+ if (!config.permissions) {
767
+ config.permissions = {};
768
+ }
769
+ if (!config.permissions.additionalDirectories) {
770
+ config.permissions.additionalDirectories = [];
771
+ }
772
+ if (!config.permissions.additionalDirectories.includes(directory)) {
773
+ config.permissions.additionalDirectories.push(directory);
774
+ await atomicWriteFile(localConfigPath, JSON.stringify(config, null, 2));
775
+ }
776
+ }
744
777
  /**
745
778
  * Update the enabled state of a plugin in the specified scope
746
779
  */
@@ -72,6 +72,12 @@ export interface AgentOptions {
72
72
  * These rules follow the standard permission rule syntax: `ToolName` or `ToolName(pattern)`.
73
73
  */
74
74
  disallowedTools?: string[];
75
+ /**
76
+ * Optional list of additional directories to include in the Safe Zone
77
+ * for this agent session. These directories are listed in the system
78
+ * prompt and treated as safe for file operations (e.g. in acceptEdits mode).
79
+ */
80
+ additionalDirectories?: string[];
75
81
  /**
76
82
  * Optional MCP server configs to connect at startup.
77
83
  * Overrides .mcp.json for specified server names.
@@ -115,6 +115,7 @@ export function setupAgentContainer(setupOptions) {
115
115
  const permissionManager = new PermissionManager(container, {
116
116
  instanceAllowedRules: options.allowedTools,
117
117
  instanceDeniedRules: options.disallowedTools,
118
+ instanceAdditionalDirectories: options.additionalDirectories,
118
119
  });
119
120
  if (configurationService.resolveAutoMemoryEnabled()) {
120
121
  const autoMemoryDir = memoryService.getAutoMemoryDirectory(workdir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-agent-sdk",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "SDK for building AI-powered development tools and agents",
5
5
  "keywords": [
6
6
  "ai",
package/src/agent.ts CHANGED
@@ -38,6 +38,7 @@ import { configValidator } from "./utils/configValidator.js";
38
38
  import { SkillManager } from "./managers/skillManager.js";
39
39
  import { TaskManager } from "./services/taskManager.js";
40
40
  import type { WorktreeSession } from "./utils/worktreeSession.js";
41
+ import path from "node:path";
41
42
  import { parseTaskNotificationXml } from "./utils/notificationXml.js";
42
43
  import { InitializationService } from "./services/initializationService.js";
43
44
  import { InteractionService } from "./services/interactionService.js";
@@ -1045,6 +1046,43 @@ export class Agent {
1045
1046
  );
1046
1047
  }
1047
1048
 
1049
+ // ========== Additional Directories ==========
1050
+
1051
+ /**
1052
+ * Add an additional directory to the Safe Zone for this session.
1053
+ *
1054
+ * The directory immediately becomes part of the Safe Zone (permission
1055
+ * checks and the system prompt), and with `remember: true` it is also
1056
+ * persisted to `.wave/settings.local.json` `permissions.additionalDirectories`
1057
+ * so future sessions pick it up automatically.
1058
+ *
1059
+ * @param directory Path of the directory to add (relative paths resolve against the workdir).
1060
+ * @param options.remember Whether to persist the directory across sessions.
1061
+ */
1062
+ public async addAdditionalDirectory(
1063
+ directory: string,
1064
+ options?: { remember?: boolean },
1065
+ ): Promise<void> {
1066
+ const resolvedPath = path.isAbsolute(directory)
1067
+ ? directory
1068
+ : path.resolve(this.workdir, directory);
1069
+ this.permissionManager.addInstanceAdditionalDirectory(resolvedPath);
1070
+ if (options?.remember) {
1071
+ await this.configurationService.addAdditionalDirectory(
1072
+ this.workdir,
1073
+ resolvedPath,
1074
+ );
1075
+ }
1076
+ }
1077
+
1078
+ /**
1079
+ * Get the effective additional directories for this session (config +
1080
+ * session-level union), matching what is listed in the system prompt.
1081
+ */
1082
+ public getAdditionalDirectories(): string[] {
1083
+ return this.permissionManager.getEffectiveAdditionalDirectories();
1084
+ }
1085
+
1048
1086
  // ========== MCP Management Methods ==========
1049
1087
 
1050
1088
  /** Get all MCP server status */
@@ -746,6 +746,8 @@ export class AIManager {
746
746
  language: this.getLanguage(),
747
747
  isSubagent: !!this.subagentType,
748
748
  worktreeSession: this.getWorktreeSession(),
749
+ additionalWorkingDirectories:
750
+ this.permissionManager?.getEffectiveAdditionalDirectories?.() ?? [],
749
751
  autoMemory: autoMemoryOptions,
750
752
  });
751
753
  }
@@ -99,6 +99,8 @@ export interface PermissionManagerOptions {
99
99
  instanceDeniedRules?: string[];
100
100
  /** Additional directories considered part of the Safe Zone */
101
101
  additionalDirectories?: string[];
102
+ /** Instance-specific additional directories (from AgentOptions, session-level) */
103
+ instanceAdditionalDirectories?: string[];
102
104
  /** System additional directories (persistent across reloads) */
103
105
  systemAdditionalDirectories?: string[];
104
106
  /** Path to the current plan file */
@@ -115,6 +117,7 @@ export class PermissionManager {
115
117
  private instanceDeniedRules: string[] = [];
116
118
  private temporaryRules: string[] = [];
117
119
  private additionalDirectories: string[] = [];
120
+ private instanceAdditionalDirectories: string[] = [];
118
121
  private systemAdditionalDirectories: string[] = [];
119
122
  private planFilePath?: string;
120
123
  private hasExitedPlanMode: boolean = false;
@@ -136,14 +139,17 @@ export class PermissionManager {
136
139
  this.instanceDeniedRules = options.instanceDeniedRules || [];
137
140
  this.planFilePath = options.planFilePath;
138
141
  this._logger = options.logger;
142
+ // Read workdir first so relative additional directories resolve against it.
143
+ this.workdir = this.container.get<string>("Workdir");
144
+ this.worktreeName = this.container.get<string | undefined>("WorktreeName");
145
+ this.mainRepoRoot = this.container.get<string | undefined>("MainRepoRoot");
139
146
  this.updateAdditionalDirectories(options.additionalDirectories || []);
147
+ for (const dir of options.instanceAdditionalDirectories || []) {
148
+ this.addInstanceAdditionalDirectory(dir);
149
+ }
140
150
  for (const dir of options.systemAdditionalDirectories || []) {
141
151
  this.addSystemAdditionalDirectory(dir);
142
152
  }
143
-
144
- this.workdir = this.container.get<string>("Workdir");
145
- this.worktreeName = this.container.get<string | undefined>("WorktreeName");
146
- this.mainRepoRoot = this.container.get<string | undefined>("MainRepoRoot");
147
153
  }
148
154
 
149
155
  /**
@@ -221,6 +227,32 @@ export class PermissionManager {
221
227
  return [...this.additionalDirectories];
222
228
  }
223
229
 
230
+ /**
231
+ * Get all instance-specific additional directories (session-level, from AgentOptions or /add-dir)
232
+ */
233
+ public getInstanceAdditionalDirectories(): string[] {
234
+ return [...this.instanceAdditionalDirectories];
235
+ }
236
+
237
+ /**
238
+ * Get the effective union of additional directories (config + instance, deduplicated).
239
+ * This is the data source for system prompt injection and Safe Zone checks.
240
+ */
241
+ public getEffectiveAdditionalDirectories(): string[] {
242
+ const seen = new Set<string>();
243
+ const result: string[] = [];
244
+ for (const dir of [
245
+ ...this.additionalDirectories,
246
+ ...this.instanceAdditionalDirectories,
247
+ ]) {
248
+ if (!seen.has(dir)) {
249
+ seen.add(dir);
250
+ result.push(dir);
251
+ }
252
+ }
253
+ return result;
254
+ }
255
+
224
256
  /**
225
257
  * Get all system additional directories
226
258
  */
@@ -276,6 +308,21 @@ export class PermissionManager {
276
308
  });
277
309
  }
278
310
 
311
+ /**
312
+ * Add an instance-level additional directory (session-level, e.g. /add-dir command)
313
+ */
314
+ public addInstanceAdditionalDirectory(directory: string): void {
315
+ const workdir = this.workdir;
316
+ const resolvedPath =
317
+ workdir && !path.isAbsolute(directory)
318
+ ? path.resolve(workdir, directory)
319
+ : path.resolve(directory);
320
+
321
+ if (!this.instanceAdditionalDirectories.includes(resolvedPath)) {
322
+ this.instanceAdditionalDirectories.push(resolvedPath);
323
+ }
324
+ }
325
+
279
326
  /**
280
327
  * Add a system-level additional directory that is persistent across configuration reloads
281
328
  */
@@ -355,6 +402,13 @@ export class PermissionManager {
355
402
  }
356
403
  }
357
404
 
405
+ // Check instance additional directories
406
+ for (const dir of this.instanceAdditionalDirectories) {
407
+ if (isPathInside(absolutePath, dir)) {
408
+ return { isInside: true, resolvedPath: absolutePath };
409
+ }
410
+ }
411
+
358
412
  // Check system additional directories
359
413
  for (const dir of this.systemAdditionalDirectories) {
360
414
  if (isPathInside(absolutePath, dir)) {
@@ -341,6 +341,8 @@ export class SubagentManager {
341
341
  ],
342
342
  additionalDirectories:
343
343
  parentPermissionManager?.getAdditionalDirectories(),
344
+ instanceAdditionalDirectories:
345
+ parentPermissionManager?.getInstanceAdditionalDirectories?.(),
344
346
  systemAdditionalDirectories:
345
347
  parentPermissionManager?.getSystemAdditionalDirectories(),
346
348
  planFilePath: parentPermissionManager?.getPlanFilePath(),
@@ -396,6 +396,7 @@ export function buildSystemPrompt(
396
396
  language?: string;
397
397
  isSubagent?: boolean;
398
398
  worktreeSession?: WorktreeSession | null;
399
+ additionalWorkingDirectories?: string[];
399
400
  autoMemory?: {
400
401
  directory: string;
401
402
  content: string;
@@ -431,11 +432,17 @@ export function buildSystemPrompt(
431
432
  const osVersion = getUnameSR();
432
433
  const primaryWorkdir = options.originalWorkdir ?? options.workdir;
433
434
  const worktreeSession = options.worktreeSession;
435
+ const additionalWorkingDirectories =
436
+ options.additionalWorkingDirectories || [];
434
437
 
435
438
  if (options.isSubagent) {
436
439
  // Subagent env section, aligned with Claude Code's computeEnvInfo() +
437
440
  // enhanceSystemPromptWithEnvDetails() (without the model description and
438
441
  // knowledge cutoff lines, which Wave does not use).
442
+ const additionalDirsInfo =
443
+ additionalWorkingDirectories.length > 0
444
+ ? `Additional working directories: ${additionalWorkingDirectories.join(", ")}\n`
445
+ : "";
439
446
  dynamicText += `
440
447
 
441
448
  ${SUBAGENT_ENV_NOTES}
@@ -444,7 +451,7 @@ Here is useful information about the environment you are running in:
444
451
  <env>
445
452
  Working directory: ${primaryWorkdir}
446
453
  Is directory a git repo: ${isGitRepo}
447
- Platform: ${platform}
454
+ ${additionalDirsInfo}Platform: ${platform}
448
455
  ${shellInfo}
449
456
  OS Version: ${osVersion}
450
457
  </env>
@@ -453,21 +460,31 @@ OS Version: ${osVersion}
453
460
  // Main agent env section, aligned with Claude Code's
454
461
  // computeSimpleEnvInfo() (without the model description, knowledge
455
462
  // cutoff, and marketing lines, which Wave does not use).
456
- const envItems = [
463
+ const envItems: Array<string | string[]> = [
457
464
  `Primary working directory: ${primaryWorkdir}`,
458
465
  worktreeSession
459
466
  ? `This is a git worktree — an isolated copy of the repository. Run all commands from this directory. Do NOT \`cd\` to the original repository root.`
460
467
  : null,
461
468
  `Is a git repository: ${isGitRepo}`,
469
+ additionalWorkingDirectories.length > 0
470
+ ? `Additional working directories:`
471
+ : null,
472
+ additionalWorkingDirectories.length > 0
473
+ ? additionalWorkingDirectories
474
+ : null,
462
475
  `Platform: ${platform}`,
463
476
  shellInfo,
464
477
  `OS Version: ${osVersion}`,
465
- ].filter((item): item is string => item !== null);
478
+ ].filter((item): item is string | string[] => item !== null);
466
479
 
467
480
  const envBlock = [
468
481
  `# Environment`,
469
482
  `You have been invoked in the following environment: `,
470
- ...envItems.map((item) => ` - ${item}`),
483
+ ...envItems.flatMap((item) =>
484
+ Array.isArray(item)
485
+ ? item.map((subItem) => ` - ${subItem}`)
486
+ : [` - ${item}`],
487
+ ),
471
488
  ].join("\n");
472
489
 
473
490
  dynamicText += `\n\n${envBlock}`;
@@ -919,6 +919,46 @@ export class ConfigurationService {
919
919
  }
920
920
  }
921
921
 
922
+ /**
923
+ * Add an additional directory to the local settings.local.json
924
+ * `permissions.additionalDirectories`, so it persists across sessions.
925
+ */
926
+ async addAdditionalDirectory(
927
+ workdir: string,
928
+ directory: string,
929
+ ): Promise<void> {
930
+ const localConfigPath = path.join(workdir, ".wave", "settings.local.json");
931
+
932
+ // Ensure .wave directory exists
933
+ const waveDir = path.join(workdir, ".wave");
934
+ ensureWaveRuntimeFilesExcluded(workdir);
935
+ if (!existsSync(waveDir)) {
936
+ await fs.mkdir(waveDir, { recursive: true });
937
+ }
938
+
939
+ let config: WaveConfiguration = {};
940
+ if (existsSync(localConfigPath)) {
941
+ try {
942
+ const content = await fs.readFile(localConfigPath, "utf-8");
943
+ config = JSON.parse(content);
944
+ } catch {
945
+ // If file is corrupted, start with empty config
946
+ }
947
+ }
948
+
949
+ if (!config.permissions) {
950
+ config.permissions = {};
951
+ }
952
+ if (!config.permissions.additionalDirectories) {
953
+ config.permissions.additionalDirectories = [];
954
+ }
955
+
956
+ if (!config.permissions.additionalDirectories.includes(directory)) {
957
+ config.permissions.additionalDirectories.push(directory);
958
+ await atomicWriteFile(localConfigPath, JSON.stringify(config, null, 2));
959
+ }
960
+ }
961
+
922
962
  /**
923
963
  * Update the enabled state of a plugin in the specified scope
924
964
  */
@@ -85,6 +85,12 @@ export interface AgentOptions {
85
85
  * These rules follow the standard permission rule syntax: `ToolName` or `ToolName(pattern)`.
86
86
  */
87
87
  disallowedTools?: string[];
88
+ /**
89
+ * Optional list of additional directories to include in the Safe Zone
90
+ * for this agent session. These directories are listed in the system
91
+ * prompt and treated as safe for file operations (e.g. in acceptEdits mode).
92
+ */
93
+ additionalDirectories?: string[];
88
94
  /**
89
95
  * Optional MCP server configs to connect at startup.
90
96
  * Overrides .mcp.json for specified server names.
@@ -176,6 +176,7 @@ export function setupAgentContainer(
176
176
  const permissionManager = new PermissionManager(container, {
177
177
  instanceAllowedRules: options.allowedTools,
178
178
  instanceDeniedRules: options.disallowedTools,
179
+ instanceAdditionalDirectories: options.additionalDirectories,
179
180
  });
180
181
  if (configurationService.resolveAutoMemoryEnabled()) {
181
182
  const autoMemoryDir = memoryService.getAutoMemoryDirectory(workdir);