wave-agent-sdk 0.19.9 → 1.0.0
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/builtin/plugins/sdd/scripts/session-start.js +1 -1
- package/builtin/plugins/sdd/skills/specify/SKILL.md +3 -4
- package/builtin/skills/settings/ENV.md +15 -9
- package/builtin/skills/settings/HOOKS.md +27 -2
- package/dist/agent.js +5 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/managers/aiManager.d.ts +8 -0
- package/dist/managers/aiManager.js +20 -5
- package/dist/managers/backgroundTaskManager.d.ts +6 -0
- package/dist/managers/backgroundTaskManager.js +11 -0
- package/dist/managers/bangManager.d.ts +6 -0
- package/dist/managers/bangManager.js +11 -0
- package/dist/managers/hookManager.d.ts +8 -2
- package/dist/managers/hookManager.js +14 -4
- package/dist/managers/mcpManager.d.ts +18 -4
- package/dist/managers/mcpManager.js +40 -18
- package/dist/managers/toolManager.js +5 -0
- package/dist/services/configurationService.d.ts +21 -2
- package/dist/services/configurationService.js +72 -23
- package/dist/services/initializationService.js +14 -4
- package/dist/services/interactionService.js +35 -7
- package/dist/services/remoteSettingsService.d.ts +12 -0
- package/dist/services/remoteSettingsService.js +15 -1
- package/dist/services/taskManager.js +7 -1
- package/dist/tools/bashTool.js +1 -0
- package/dist/tools/enterWorktreeTool.js +14 -3
- package/dist/tools/exitWorktreeTool.js +11 -10
- package/dist/tools/types.d.ts +7 -0
- package/dist/types/config.d.ts +2 -0
- package/dist/types/hooks.d.ts +2 -2
- package/dist/utils/containerSetup.js +1 -1
- package/dist/utils/openaiClient.js +2 -1
- package/dist/utils/pathEncoder.js +7 -2
- package/dist/utils/worktreeUtils.d.ts +17 -0
- package/dist/utils/worktreeUtils.js +339 -1
- package/package.json +1 -1
- package/src/agent.ts +7 -2
- package/src/index.ts +1 -0
- package/src/managers/aiManager.ts +23 -5
- package/src/managers/backgroundTaskManager.ts +15 -0
- package/src/managers/bangManager.ts +15 -0
- package/src/managers/hookManager.ts +20 -5
- package/src/managers/mcpManager.ts +60 -18
- package/src/managers/toolManager.ts +7 -0
- package/src/services/configurationService.ts +84 -23
- package/src/services/initializationService.ts +17 -4
- package/src/services/interactionService.ts +49 -6
- package/src/services/remoteSettingsService.ts +16 -1
- package/src/services/taskManager.ts +10 -1
- package/src/tools/bashTool.ts +1 -0
- package/src/tools/enterWorktreeTool.ts +19 -2
- package/src/tools/exitWorktreeTool.ts +15 -12
- package/src/tools/types.ts +7 -0
- package/src/types/config.ts +2 -0
- package/src/types/hooks.ts +2 -2
- package/src/utils/containerSetup.ts +3 -1
- package/src/utils/openaiClient.ts +2 -0
- package/src/utils/pathEncoder.ts +7 -2
- package/src/utils/worktreeUtils.ts +401 -1
|
@@ -14,7 +14,7 @@ const guidance = [
|
|
|
14
14
|
"Spec-First Workflow(规格优先工作流):",
|
|
15
15
|
"- 需求增加或变更时,优先更新 spec:先更新对应规格说明(新增用户故事、验收场景),待用户确认 spec 后再实现代码。spec 是功能设计的权威来源,不是 changelog。",
|
|
16
16
|
"- 边界模糊时也先写 spec 草稿请用户确认,不要直接改代码。",
|
|
17
|
-
"-
|
|
17
|
+
"- 规格编写技能(specify)由 AI 自动触发:对话中涉及新需求或需求变更时主动创建或更新规格文件,不需要用户手动调用(不出现在斜杠命令列表中)。",
|
|
18
18
|
`- 新增或修改 spec 后运行校验:${specCount}(自动检测 docs/specs/,否则 specs/,否则退出)。`,
|
|
19
19
|
].join("\n");
|
|
20
20
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: specify
|
|
3
3
|
description: 根据自然语言描述创建或更新功能规格说明,生成包含用户故事与验收场景的规格文件。
|
|
4
|
+
user-invocable: false
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
## 用户输入
|
|
@@ -9,13 +10,11 @@ description: 根据自然语言描述创建或更新功能规格说明,生成
|
|
|
9
10
|
$ARGUMENTS
|
|
10
11
|
```
|
|
11
12
|
|
|
12
|
-
你**必须**在继续之前考虑用户输入(如果不为空)。
|
|
13
|
-
|
|
14
13
|
## 流程
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
本技能由 AI 在会话中自动触发(不占用手动斜杠命令)。触发时机:用户提出新的需求、修改需求或涉及功能边界时,若对应规格尚未创建或已过期,则主动创建或更新规格说明。$ARGUMENTS 通常为空——需求描述直接来自对话上下文,不要让用户重复。
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
根据对话中的功能描述,执行以下步骤:
|
|
19
18
|
|
|
20
19
|
1. **确定规格文件路径**:
|
|
21
20
|
- **确定规格根目录**:优先复用项目中已有的规格目录——若 `docs/specs/` 存在则用之,否则若 `specs/` 存在则用之,否则默认 `specs/`(并在完成报告中说明所选目录,便于用户纠正)。
|
|
@@ -17,13 +17,13 @@ Environment variables are configured in the `env` field of `settings.json`. It i
|
|
|
17
17
|
|
|
18
18
|
## Supported `WAVE_*` Environment Variables
|
|
19
19
|
|
|
20
|
-
Wave uses several environment variables to control its core functionality.
|
|
20
|
+
Wave uses several environment variables to control its core functionality. Variables marked **OS env only** are read from the OS environment (or constructor / stdio `initialize` params) and are **NOT** read from settings.json `env` — set them in your shell, not in the `env` field.
|
|
21
21
|
|
|
22
22
|
| Variable | Description | Default |
|
|
23
23
|
| :--- | :--- | :--- |
|
|
24
24
|
| `WAVE_API_KEY` | API key for the AI gateway. | - |
|
|
25
25
|
| `WAVE_BASE_URL` | Base URL for the AI gateway. | - |
|
|
26
|
-
| `WAVE_SERVER_URL` | Server URL for SSO authentication. | `https://codechat.codewave.163.com` |
|
|
26
|
+
| `WAVE_SERVER_URL` | Server URL for SSO authentication. **OS env only** — set via OS env or `options.serverUrl`; not read from settings.json `env` (avoids a startup 401 race). | `https://codechat.codewave.163.com` |
|
|
27
27
|
| `WAVE_CUSTOM_HEADERS` | Custom HTTP headers for the AI gateway. Newline-separated `Key: Value` pairs (e.g., `"X-Foo: bar\nAuthorization: Bearer xxx"`). | - |
|
|
28
28
|
| `WAVE_MODEL` | The primary AI model to use for the agent. | `gemini-3-flash` |
|
|
29
29
|
| `WAVE_FAST_MODEL` | The fast AI model to use for quick tasks. | `gemini-2.5-flash` |
|
|
@@ -32,23 +32,29 @@ Wave uses several environment variables to control its core functionality.
|
|
|
32
32
|
| `WAVE_DISABLE_AUTO_MEMORY` | Set to `1` or `true` to disable the auto-memory feature. | `false` |
|
|
33
33
|
| `WAVE_AUTO_MEMORY_FREQUENCY` | Auto memory update frequency. `1` = every turn, `2` = every 2 turns, etc. | `1` |
|
|
34
34
|
| `WAVE_TASK_LIST_ID` | Explicitly set the task list ID for the session. | (Session ID) |
|
|
35
|
-
| `WAVE_PLUGIN_GIT_TIMEOUT_MS` | Timeout in milliseconds for git operations when installing plugins. | `300000` |
|
|
35
|
+
| `WAVE_PLUGIN_GIT_TIMEOUT_MS` | Timeout in milliseconds for git operations when installing plugins. **OS env only** (infrastructure). | `300000` |
|
|
36
36
|
|
|
37
37
|
## Configuration Scopes
|
|
38
38
|
|
|
39
|
-
Environment variables can be set in different scopes
|
|
39
|
+
Environment variables can be set in different scopes. Wave merges scopes from lowest to highest priority and stores the result in the agent's **per-session environment snapshot**. The snapshot takes priority over OS environment variables but is **NOT written to `process.env`** — this keeps multiple sessions in one `wave --stdio` process from polluting each other.
|
|
40
|
+
|
|
41
|
+
Precedence (highest to lowest):
|
|
40
42
|
|
|
41
43
|
1. **Local Scope**: `.wave/settings.local.json` (Local overrides, ignored by git)
|
|
42
44
|
2. **Project Scope**: `.wave/settings.json` (Project-specific settings, shared via git)
|
|
43
45
|
3. **User Scope**: `~/.wave/settings.json` (Global settings for all projects)
|
|
44
|
-
4. **System Environment**: Variables set in your shell (e.g., `export WAVE_API_KEY=...`)
|
|
46
|
+
4. **System Environment**: Variables set in your shell (e.g., `export WAVE_API_KEY=...`). Used as a fallback when a key is absent from the settings snapshot.
|
|
47
|
+
|
|
48
|
+
> Settings `env` shadows (does not mutate) OS env: a key set in both settings.json `env` and the OS environment resolves to the settings value for that session, while the OS value remains untouched and visible to unrelated processes.
|
|
45
49
|
|
|
46
50
|
## Custom Environment Variables
|
|
47
51
|
|
|
48
|
-
You can also define custom environment variables in the `env` field. These variables will be available to:
|
|
52
|
+
You can also define custom environment variables in the `env` field. These variables are stored in the session's environment snapshot and will be available to:
|
|
53
|
+
|
|
54
|
+
- **Hooks**: Any shell command executed as a hook will have these variables in its environment (merged on top of OS env).
|
|
55
|
+
- **Tools**: Tools like `Bash` will have access to these variables (merged on top of OS env).
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
- **Tools**: Tools like `Bash` will have access to these variables.
|
|
57
|
+
In `wave --stdio` mode one process hosts multiple sessions; each session keeps its own snapshot, so sessions with different `env` do not pollute each other (no "last session wins").
|
|
52
58
|
|
|
53
59
|
Example:
|
|
54
60
|
```json
|
|
@@ -62,7 +68,7 @@ Example:
|
|
|
62
68
|
|
|
63
69
|
## Live Reload
|
|
64
70
|
|
|
65
|
-
Environment variables configured in `settings.json` support **live reload**. When you modify the `env` field in any `settings.json` file (user, project, or local scope), the changes take effect immediately without requiring a Wave session restart.
|
|
71
|
+
Environment variables configured in `settings.json` support **live reload**. When you modify the `env` field in any `settings.json` file (user, project, or local scope), the changes take effect immediately without requiring a Wave session restart — the session's environment snapshot is refreshed and subsequent resolve calls / subprocess spawns use the new values.
|
|
66
72
|
|
|
67
73
|
## Best Practices
|
|
68
74
|
|
|
@@ -13,7 +13,7 @@ Wave supports the following hook events:
|
|
|
13
13
|
- `Stop`: Triggered when Wave finishes its response cycle (no more tool calls).
|
|
14
14
|
- `SubagentStop`: Triggered when a subagent finishes its response cycle.
|
|
15
15
|
- `WorktreeCreate`: Triggered when a new worktree is created.
|
|
16
|
-
- `WorktreeRemove`: Triggered
|
|
16
|
+
- `WorktreeRemove`: Triggered before a worktree is removed (e.g., via ExitWorktree with `action: "remove"`). Non-blocking. Fires **before** the worktree directory is deleted so hooks can still read files inside it. The hook receives `worktree_path` in the JSON input. Useful for cleanup tasks (e.g., `docker compose -p $(basename "$worktree_path") down`).
|
|
17
17
|
- `CwdChanged`: Triggered when the working directory changes (e.g., entering/exiting a worktree). Non-blocking.
|
|
18
18
|
- `SessionStart`: Triggered during session initialization. Hooks can inject `additionalContext` and `initialUserMessage` via stdout.
|
|
19
19
|
- `SessionEnd`: Triggered during agent destruction (fire-and-forget, non-blocking). Useful for cleanup, resource teardown, and analytics.
|
|
@@ -79,7 +79,7 @@ Wave provides detailed context to hook processes via `stdin` as a JSON object. T
|
|
|
79
79
|
- `user_prompt`: (UserPromptSubmit) The text submitted by the user.
|
|
80
80
|
- `subagent_type`: (If executed by a subagent) The type of the subagent.
|
|
81
81
|
- `name`: (WorktreeCreate) The name of the new worktree.
|
|
82
|
-
- `worktree_path`: (WorktreeRemove) The absolute path
|
|
82
|
+
- `worktree_path`: (WorktreeRemove) The absolute path of the worktree about to be removed. Derive the worktree name with `basename "$worktree_path"`.
|
|
83
83
|
- `old_cwd`: (CwdChanged) The previous working directory.
|
|
84
84
|
- `new_cwd`: (CwdChanged) The new working directory.
|
|
85
85
|
- `compact_instructions`: (PreCompact) Custom instructions for the compaction, if any.
|
|
@@ -165,6 +165,31 @@ SessionEnd hooks receive `end_source` in the JSON input indicating how the sessi
|
|
|
165
165
|
}
|
|
166
166
|
```
|
|
167
167
|
|
|
168
|
+
## WorktreeRemove Hooks
|
|
169
|
+
|
|
170
|
+
`WorktreeRemove` hooks fire **before** the worktree directory is deleted, so they can still read files inside it. They are non-blocking (Notification type): the hook never replaces `git worktree remove` itself. Useful for cleaning up external resources that were provisioned for the worktree (databases, containers, etc.).
|
|
171
|
+
|
|
172
|
+
### Input
|
|
173
|
+
WorktreeRemove hooks receive `worktree_path` in the JSON input (alongside the common fields `session_id`, `transcript_path`, `cwd`, `hook_event_name`). The worktree name can be derived via `basename "$worktree_path"`.
|
|
174
|
+
|
|
175
|
+
### Example Configuration
|
|
176
|
+
```json
|
|
177
|
+
{
|
|
178
|
+
"hooks": {
|
|
179
|
+
"WorktreeRemove": [
|
|
180
|
+
{
|
|
181
|
+
"hooks": [
|
|
182
|
+
{
|
|
183
|
+
"command": "worktree_path=$(jq -r '.worktree_path') && docker compose -p \"$(basename \"$worktree_path\")\" down || true",
|
|
184
|
+
"description": "Tear down the worktree's docker compose project before removal"
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
168
193
|
## Live Reload
|
|
169
194
|
|
|
170
195
|
Hook configurations support **live reload**. When you modify hooks in `settings.json`, the changes take effect immediately without restarting Wave.
|
package/dist/agent.js
CHANGED
|
@@ -14,7 +14,10 @@ import { remoteSettingsService } from "./services/remoteSettingsService.js";
|
|
|
14
14
|
export class Agent {
|
|
15
15
|
// Dynamic configuration getter methods
|
|
16
16
|
getGatewayConfig() {
|
|
17
|
-
return
|
|
17
|
+
return {
|
|
18
|
+
...this.configurationService.resolveGatewayConfig(),
|
|
19
|
+
sessionId: this.messageManager.getSessionId(),
|
|
20
|
+
};
|
|
18
21
|
}
|
|
19
22
|
getModelConfig() {
|
|
20
23
|
return this.configurationService.resolveModelConfig(undefined, undefined, undefined, this.getPermissionMode());
|
|
@@ -702,7 +705,7 @@ export class Agent {
|
|
|
702
705
|
transcriptPath,
|
|
703
706
|
cwd: this.workdir,
|
|
704
707
|
worktreePath,
|
|
705
|
-
env: Object.fromEntries(Object.entries(
|
|
708
|
+
env: Object.fromEntries(Object.entries(this.configurationService.getMergedEnv()).filter((e) => e[1] !== undefined)),
|
|
706
709
|
});
|
|
707
710
|
// Process results via messageManager (may not be visible during shutdown)
|
|
708
711
|
this.hookManager.processHookResults("WorktreeRemove", hookResults, this.messageManager);
|
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export * from "./utils/tokenCalculation.js";
|
|
|
22
22
|
export * from "./utils/gitUtils.js";
|
|
23
23
|
export * from "./utils/nameGenerator.js";
|
|
24
24
|
export * from "./utils/worktreeSession.js";
|
|
25
|
+
export * from "./utils/worktreeUtils.js";
|
|
25
26
|
export { loadMergedWaveConfig } from "./services/configurationService.js";
|
|
26
27
|
export * from "./types/index.js";
|
|
27
28
|
export * from "./tools/buildTool.js";
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ export * from "./utils/tokenCalculation.js";
|
|
|
26
26
|
export * from "./utils/gitUtils.js";
|
|
27
27
|
export * from "./utils/nameGenerator.js";
|
|
28
28
|
export * from "./utils/worktreeSession.js";
|
|
29
|
+
export * from "./utils/worktreeUtils.js";
|
|
29
30
|
export { loadMergedWaveConfig } from "./services/configurationService.js";
|
|
30
31
|
export * from "./types/index.js";
|
|
31
32
|
// Export tool building utilities
|
|
@@ -53,6 +53,14 @@ export declare class AIManager {
|
|
|
53
53
|
private get permissionManager();
|
|
54
54
|
private get planManager();
|
|
55
55
|
private get configurationService();
|
|
56
|
+
/**
|
|
57
|
+
* OS env merged with the per-session env snapshot. Falls back to process.env
|
|
58
|
+
* when ConfigurationService is absent or its getMergedEnv is missing (e.g. in
|
|
59
|
+
* unit tests with partial mocks), so hook-context env construction never
|
|
60
|
+
* throws. Use this (not the non-null `configurationService` getter) when
|
|
61
|
+
* building hook context env.
|
|
62
|
+
*/
|
|
63
|
+
private get mergedEnv();
|
|
56
64
|
getGatewayConfig(): GatewayConfig;
|
|
57
65
|
getModelConfig(): ModelConfig;
|
|
58
66
|
getMaxInputTokens(): number;
|
|
@@ -99,9 +99,24 @@ export class AIManager {
|
|
|
99
99
|
get configurationService() {
|
|
100
100
|
return this.container.get("ConfigurationService");
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* OS env merged with the per-session env snapshot. Falls back to process.env
|
|
104
|
+
* when ConfigurationService is absent or its getMergedEnv is missing (e.g. in
|
|
105
|
+
* unit tests with partial mocks), so hook-context env construction never
|
|
106
|
+
* throws. Use this (not the non-null `configurationService` getter) when
|
|
107
|
+
* building hook context env.
|
|
108
|
+
*/
|
|
109
|
+
get mergedEnv() {
|
|
110
|
+
return (this.container
|
|
111
|
+
.get("ConfigurationService")
|
|
112
|
+
?.getMergedEnv?.() ?? process.env);
|
|
113
|
+
}
|
|
102
114
|
// Getter methods for accessing dynamic configuration
|
|
103
115
|
getGatewayConfig() {
|
|
104
|
-
return
|
|
116
|
+
return {
|
|
117
|
+
...this.configurationService.resolveGatewayConfig(),
|
|
118
|
+
sessionId: this.messageManager.getSessionId(),
|
|
119
|
+
};
|
|
105
120
|
}
|
|
106
121
|
getModelConfig() {
|
|
107
122
|
const permissionMode = this.container.has("PermissionMode")
|
|
@@ -1262,7 +1277,7 @@ export class AIManager {
|
|
|
1262
1277
|
sessionCrons, // Stop-only: session cron jobs snapshot
|
|
1263
1278
|
lastAssistantMessage: lastAssistantText, // Stop/SubagentStop: last assistant message text
|
|
1264
1279
|
// Stop hooks don't need toolName, toolInput, toolResponse, or userPrompt
|
|
1265
|
-
env: Object.fromEntries(Object.entries(
|
|
1280
|
+
env: Object.fromEntries(Object.entries(this.mergedEnv).filter((e) => e[1] !== undefined)), // Include environment variables
|
|
1266
1281
|
};
|
|
1267
1282
|
const results = await this.hookManager.executeHooks(hookName, context);
|
|
1268
1283
|
// Process hook results to handle exit codes and appropriate responses
|
|
@@ -1417,7 +1432,7 @@ export class AIManager {
|
|
|
1417
1432
|
if (this.hookManager) {
|
|
1418
1433
|
const sessionId = this.messageManager.getSessionId();
|
|
1419
1434
|
const transcriptPath = this.messageManager.getTranscriptPath();
|
|
1420
|
-
const env = Object.fromEntries(Object.entries(
|
|
1435
|
+
const env = Object.fromEntries(Object.entries(this.mergedEnv).filter((e) => e[1] !== undefined));
|
|
1421
1436
|
await this.hookManager.executeCwdChangedHooks(oldCwd, newCwd, sessionId, transcriptPath, env);
|
|
1422
1437
|
}
|
|
1423
1438
|
},
|
|
@@ -1484,7 +1499,7 @@ export class AIManager {
|
|
|
1484
1499
|
cwd: this.getWorkdir(),
|
|
1485
1500
|
toolInput,
|
|
1486
1501
|
subagentType: this.subagentType, // Include subagent type in hook context
|
|
1487
|
-
env: Object.fromEntries(Object.entries(
|
|
1502
|
+
env: Object.fromEntries(Object.entries(this.mergedEnv).filter((e) => e[1] !== undefined)), // Include environment variables
|
|
1488
1503
|
};
|
|
1489
1504
|
const results = await this.hookManager.executeHooks("PreToolUse", context);
|
|
1490
1505
|
// Process hook results to handle exit codes and determine if tool should be blocked
|
|
@@ -1537,7 +1552,7 @@ export class AIManager {
|
|
|
1537
1552
|
toolResponse,
|
|
1538
1553
|
subagentType: this.subagentType, // Include subagent type in hook context
|
|
1539
1554
|
planFilePath: this.permissionManager?.getPlanFilePath(),
|
|
1540
|
-
env: Object.fromEntries(Object.entries(
|
|
1555
|
+
env: Object.fromEntries(Object.entries(this.mergedEnv).filter((e) => e[1] !== undefined)), // Include environment variables
|
|
1541
1556
|
};
|
|
1542
1557
|
const results = await this.hookManager.executeHooks("PostToolUse", context);
|
|
1543
1558
|
// Process hook results to handle exit codes and update tool results
|
|
@@ -15,6 +15,12 @@ export declare class BackgroundTaskManager {
|
|
|
15
15
|
private callbacks;
|
|
16
16
|
private workdir;
|
|
17
17
|
constructor(container: Container, options: BackgroundTaskManagerOptions);
|
|
18
|
+
/**
|
|
19
|
+
* Merged env (OS env overlaid with this session's settings snapshot) for
|
|
20
|
+
* background-task subprocesses, so settings `env` vars reach them without
|
|
21
|
+
* polluting other sessions in one `wave --stdio` process.
|
|
22
|
+
*/
|
|
23
|
+
private get sessionEnv();
|
|
18
24
|
/**
|
|
19
25
|
* Fire the onBackgroundTasksChange callback so UI consumers refresh.
|
|
20
26
|
* Public so other managers (e.g. WorkflowManager) can trigger a refresh
|
|
@@ -13,6 +13,16 @@ export class BackgroundTaskManager {
|
|
|
13
13
|
this.callbacks = options.callbacks || {};
|
|
14
14
|
this.workdir = options.workdir;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Merged env (OS env overlaid with this session's settings snapshot) for
|
|
18
|
+
* background-task subprocesses, so settings `env` vars reach them without
|
|
19
|
+
* polluting other sessions in one `wave --stdio` process.
|
|
20
|
+
*/
|
|
21
|
+
get sessionEnv() {
|
|
22
|
+
return (this.container
|
|
23
|
+
.get("ConfigurationService")
|
|
24
|
+
?.getMergedEnv?.() ?? process.env);
|
|
25
|
+
}
|
|
16
26
|
/**
|
|
17
27
|
* Fire the onBackgroundTasksChange callback so UI consumers refresh.
|
|
18
28
|
* Public so other managers (e.g. WorkflowManager) can trigger a refresh
|
|
@@ -44,6 +54,7 @@ export class BackgroundTaskManager {
|
|
|
44
54
|
cwd: cwd ?? this.workdir,
|
|
45
55
|
env: {
|
|
46
56
|
...process.env,
|
|
57
|
+
...this.sessionEnv,
|
|
47
58
|
},
|
|
48
59
|
});
|
|
49
60
|
// Create log file
|
|
@@ -14,6 +14,12 @@ export declare class BangManager {
|
|
|
14
14
|
onCommandRunningChange?: (running: boolean) => void;
|
|
15
15
|
constructor(container: Container, options: BangManagerOptions);
|
|
16
16
|
private get messageManager();
|
|
17
|
+
/**
|
|
18
|
+
* Merged env (OS env overlaid with this session's settings snapshot) for
|
|
19
|
+
* bang-command subprocesses, so settings `env` vars reach them without
|
|
20
|
+
* polluting other sessions in one `wave --stdio` process.
|
|
21
|
+
*/
|
|
22
|
+
private get sessionEnv();
|
|
17
23
|
private setCommandRunning;
|
|
18
24
|
executeCommand(command: string): Promise<number>;
|
|
19
25
|
abortCommand(): void;
|
|
@@ -10,6 +10,16 @@ export class BangManager {
|
|
|
10
10
|
get messageManager() {
|
|
11
11
|
return this.container.get("MessageManager");
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Merged env (OS env overlaid with this session's settings snapshot) for
|
|
15
|
+
* bang-command subprocesses, so settings `env` vars reach them without
|
|
16
|
+
* polluting other sessions in one `wave --stdio` process.
|
|
17
|
+
*/
|
|
18
|
+
get sessionEnv() {
|
|
19
|
+
return (this.container
|
|
20
|
+
.get("ConfigurationService")
|
|
21
|
+
?.getMergedEnv?.() ?? process.env);
|
|
22
|
+
}
|
|
13
23
|
setCommandRunning(isRunning) {
|
|
14
24
|
this.isCommandRunning = isRunning;
|
|
15
25
|
this.onCommandRunningChange?.(isRunning);
|
|
@@ -28,6 +38,7 @@ export class BangManager {
|
|
|
28
38
|
cwd: this.workdir,
|
|
29
39
|
env: {
|
|
30
40
|
...process.env,
|
|
41
|
+
...this.sessionEnv,
|
|
31
42
|
},
|
|
32
43
|
});
|
|
33
44
|
this.currentProcess = child;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Central orchestrator for the hooks system. Handles configuration loading,
|
|
5
5
|
* validation, and hook execution across all supported events.
|
|
6
6
|
*/
|
|
7
|
-
import { type HookEvent, type HookExecutionContext, type ExtendedHookExecutionContext, type HookExecutionResult, type HookValidationResult, type SessionEndSource } from "../types/hooks.js";
|
|
7
|
+
import { type HookEvent, type HookExecutionContext, type ExtendedHookExecutionContext, type HookExecutionResult, type HookValidationResult, type SessionEndSource, type SessionStartSource } from "../types/hooks.js";
|
|
8
8
|
import type { WaveConfiguration, PartialHookConfiguration } from "../types/configuration.js";
|
|
9
9
|
import { HookMatcher } from "../utils/hookMatcher.js";
|
|
10
10
|
import type { MessageManager } from "./messageManager.js";
|
|
@@ -18,6 +18,12 @@ export declare class HookManager {
|
|
|
18
18
|
private readonly matcher;
|
|
19
19
|
private readonly workdir;
|
|
20
20
|
constructor(container: Container, workdir: string, matcher?: HookMatcher);
|
|
21
|
+
/**
|
|
22
|
+
* Merged env for this session (OS env overlaid with the per-session settings
|
|
23
|
+
* snapshot). Hook subprocesses spawn with this env so settings.json `env`
|
|
24
|
+
* vars reach hooks without polluting other sessions in one stdio process.
|
|
25
|
+
*/
|
|
26
|
+
private get sessionEnv();
|
|
21
27
|
/**
|
|
22
28
|
* Load hook configuration from programmatic source (AgentOptions.hooks)
|
|
23
29
|
*/
|
|
@@ -118,7 +124,7 @@ export declare class HookManager {
|
|
|
118
124
|
* Execute SessionStart hooks during initialization.
|
|
119
125
|
* Collects additionalContext and initialUserMessage from hook stdout.
|
|
120
126
|
*/
|
|
121
|
-
executeSessionStartHooks(source:
|
|
127
|
+
executeSessionStartHooks(source: SessionStartSource, sessionId: string, transcriptPath: string, agentType?: string): Promise<{
|
|
122
128
|
results: HookExecutionResult[];
|
|
123
129
|
additionalContext?: string;
|
|
124
130
|
initialUserMessage?: string;
|
|
@@ -18,6 +18,16 @@ export class HookManager {
|
|
|
18
18
|
this.workdir = workdir;
|
|
19
19
|
this.matcher = matcher;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Merged env for this session (OS env overlaid with the per-session settings
|
|
23
|
+
* snapshot). Hook subprocesses spawn with this env so settings.json `env`
|
|
24
|
+
* vars reach hooks without polluting other sessions in one stdio process.
|
|
25
|
+
*/
|
|
26
|
+
get sessionEnv() {
|
|
27
|
+
return (this.container
|
|
28
|
+
.get("ConfigurationService")
|
|
29
|
+
?.getMergedEnv?.() ?? process.env);
|
|
30
|
+
}
|
|
21
31
|
/**
|
|
22
32
|
* Load hook configuration from programmatic source (AgentOptions.hooks)
|
|
23
33
|
*/
|
|
@@ -674,7 +684,7 @@ export class HookManager {
|
|
|
674
684
|
cwd: this.workdir,
|
|
675
685
|
source,
|
|
676
686
|
agentType,
|
|
677
|
-
env: Object.fromEntries(Object.entries(
|
|
687
|
+
env: Object.fromEntries(Object.entries(this.sessionEnv).filter((e) => e[1] !== undefined)),
|
|
678
688
|
};
|
|
679
689
|
const results = await this.executeHooks("SessionStart", context);
|
|
680
690
|
let additionalContext;
|
|
@@ -718,7 +728,7 @@ export class HookManager {
|
|
|
718
728
|
transcriptPath,
|
|
719
729
|
cwd: this.workdir,
|
|
720
730
|
endSource: source,
|
|
721
|
-
env: Object.fromEntries(Object.entries(
|
|
731
|
+
env: Object.fromEntries(Object.entries(this.sessionEnv).filter((e) => e[1] !== undefined)),
|
|
722
732
|
};
|
|
723
733
|
const results = await this.executeHooks("SessionEnd", context);
|
|
724
734
|
// Process results but never block shutdown
|
|
@@ -740,7 +750,7 @@ export class HookManager {
|
|
|
740
750
|
transcriptPath,
|
|
741
751
|
cwd: this.workdir,
|
|
742
752
|
compactInstructions: customInstructions,
|
|
743
|
-
env: Object.fromEntries(Object.entries(
|
|
753
|
+
env: Object.fromEntries(Object.entries(this.sessionEnv).filter((e) => e[1] !== undefined)),
|
|
744
754
|
};
|
|
745
755
|
const results = await this.executeHooks("PreCompact", context);
|
|
746
756
|
let additionalInstructions;
|
|
@@ -767,7 +777,7 @@ export class HookManager {
|
|
|
767
777
|
transcriptPath,
|
|
768
778
|
cwd: this.workdir,
|
|
769
779
|
compactSummary,
|
|
770
|
-
env: Object.fromEntries(Object.entries(
|
|
780
|
+
env: Object.fromEntries(Object.entries(this.sessionEnv).filter((e) => e[1] !== undefined)),
|
|
771
781
|
};
|
|
772
782
|
const results = await this.executeHooks("PostCompact", context);
|
|
773
783
|
if (results.length > 0) {
|
|
@@ -11,13 +11,21 @@ export interface McpManagerOptions {
|
|
|
11
11
|
/** Pre-configured MCP servers passed from constructor options */
|
|
12
12
|
mcpServers?: Record<string, McpServerConfig>;
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Expand environment variables in a string value.
|
|
16
|
+
* Supports ${VAR} and ${VAR:-default} patterns.
|
|
17
|
+
*
|
|
18
|
+
* @param env - Merged env to resolve against (session snapshot over OS env).
|
|
19
|
+
* Defaults to `process.env` for backward compatibility with tests
|
|
20
|
+
* and standalone callers.
|
|
21
|
+
*/
|
|
22
|
+
export declare function expandEnvVars(value: string, env?: Record<string, string | undefined>): string;
|
|
15
23
|
/**
|
|
16
24
|
* Walk an MCP config and resolve environment variables in all string fields.
|
|
17
|
-
*
|
|
18
|
-
* handled at spawn time
|
|
25
|
+
* Expands ${VAR} against the session env snapshot (over OS env); falls back to
|
|
26
|
+
* process.env. WAVE_PLUGIN_ROOT is skipped — handled at spawn time.
|
|
19
27
|
*/
|
|
20
|
-
export declare function resolveMcpConfig(config: McpConfig): McpConfig;
|
|
28
|
+
export declare function resolveMcpConfig(config: McpConfig, env?: Record<string, string | undefined>): McpConfig;
|
|
21
29
|
export declare class McpManager {
|
|
22
30
|
private container;
|
|
23
31
|
private config;
|
|
@@ -30,6 +38,12 @@ export declare class McpManager {
|
|
|
30
38
|
private reconnectTimers;
|
|
31
39
|
private reconnectAttempts;
|
|
32
40
|
constructor(container: Container, options?: McpManagerOptions);
|
|
41
|
+
/**
|
|
42
|
+
* Merged env for this session: OS env overlaid with the per-session settings
|
|
43
|
+
* snapshot. MCP env-var expansion (${VAR}) resolves against this so multiple
|
|
44
|
+
* sessions in one `wave --stdio` process don't read each other's settings env.
|
|
45
|
+
*/
|
|
46
|
+
private get envSnapshot();
|
|
33
47
|
/**
|
|
34
48
|
* Initialize MCP manager with working directory and optionally auto-connect
|
|
35
49
|
*/
|
|
@@ -11,7 +11,15 @@ import { logger } from "../utils/globalLogger.js";
|
|
|
11
11
|
* Supports ${VAR} and ${VAR:-default} patterns.
|
|
12
12
|
*/
|
|
13
13
|
const WAVE_TEMPLATE_VARS = ["WAVE_PLUGIN_ROOT", "CLAUDE_PLUGIN_ROOT"];
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Expand environment variables in a string value.
|
|
16
|
+
* Supports ${VAR} and ${VAR:-default} patterns.
|
|
17
|
+
*
|
|
18
|
+
* @param env - Merged env to resolve against (session snapshot over OS env).
|
|
19
|
+
* Defaults to `process.env` for backward compatibility with tests
|
|
20
|
+
* and standalone callers.
|
|
21
|
+
*/
|
|
22
|
+
export function expandEnvVars(value, env = process.env) {
|
|
15
23
|
return value.replace(/\$\{([^}]+)\}/g, (_match, expr) => {
|
|
16
24
|
const [varName, ...rest] = expr.split(":-");
|
|
17
25
|
const defaultValue = rest.join(":-");
|
|
@@ -19,38 +27,38 @@ export function expandEnvVars(value) {
|
|
|
19
27
|
if (WAVE_TEMPLATE_VARS.includes(varName)) {
|
|
20
28
|
return _match; // return original ${...} string untouched
|
|
21
29
|
}
|
|
22
|
-
return process.env[varName] ?? defaultValue;
|
|
30
|
+
return env[varName] ?? process.env[varName] ?? defaultValue;
|
|
23
31
|
});
|
|
24
32
|
}
|
|
25
33
|
/**
|
|
26
34
|
* Walk an MCP config and resolve environment variables in all string fields.
|
|
27
|
-
*
|
|
28
|
-
* handled at spawn time
|
|
35
|
+
* Expands ${VAR} against the session env snapshot (over OS env); falls back to
|
|
36
|
+
* process.env. WAVE_PLUGIN_ROOT is skipped — handled at spawn time.
|
|
29
37
|
*/
|
|
30
|
-
export function resolveMcpConfig(config) {
|
|
38
|
+
export function resolveMcpConfig(config, env = process.env) {
|
|
31
39
|
const resolved = { mcpServers: {} };
|
|
32
40
|
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
|
33
41
|
const resolvedServer = { ...serverConfig };
|
|
34
42
|
if (resolvedServer.command) {
|
|
35
|
-
resolvedServer.command = expandEnvVars(resolvedServer.command);
|
|
43
|
+
resolvedServer.command = expandEnvVars(resolvedServer.command, env);
|
|
36
44
|
}
|
|
37
45
|
if (resolvedServer.args) {
|
|
38
|
-
resolvedServer.args = resolvedServer.args.map(expandEnvVars);
|
|
46
|
+
resolvedServer.args = resolvedServer.args.map((a) => expandEnvVars(a, env));
|
|
39
47
|
}
|
|
40
48
|
if (resolvedServer.env) {
|
|
41
49
|
const resolvedEnv = {};
|
|
42
50
|
for (const [key, val] of Object.entries(resolvedServer.env)) {
|
|
43
|
-
resolvedEnv[key] = expandEnvVars(val);
|
|
51
|
+
resolvedEnv[key] = expandEnvVars(val, env);
|
|
44
52
|
}
|
|
45
53
|
resolvedServer.env = resolvedEnv;
|
|
46
54
|
}
|
|
47
55
|
if (resolvedServer.url) {
|
|
48
|
-
resolvedServer.url = expandEnvVars(resolvedServer.url);
|
|
56
|
+
resolvedServer.url = expandEnvVars(resolvedServer.url, env);
|
|
49
57
|
}
|
|
50
58
|
if (resolvedServer.headers) {
|
|
51
59
|
const resolvedHeaders = {};
|
|
52
60
|
for (const [key, val] of Object.entries(resolvedServer.headers)) {
|
|
53
|
-
resolvedHeaders[key] = expandEnvVars(val);
|
|
61
|
+
resolvedHeaders[key] = expandEnvVars(val, env);
|
|
54
62
|
}
|
|
55
63
|
resolvedServer.headers = resolvedHeaders;
|
|
56
64
|
}
|
|
@@ -71,6 +79,16 @@ export class McpManager {
|
|
|
71
79
|
this.callbacks = options.callbacks || {};
|
|
72
80
|
this.mcpServers = options.mcpServers;
|
|
73
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Merged env for this session: OS env overlaid with the per-session settings
|
|
84
|
+
* snapshot. MCP env-var expansion (${VAR}) resolves against this so multiple
|
|
85
|
+
* sessions in one `wave --stdio` process don't read each other's settings env.
|
|
86
|
+
*/
|
|
87
|
+
get envSnapshot() {
|
|
88
|
+
return (this.container
|
|
89
|
+
.get("ConfigurationService")
|
|
90
|
+
?.getMergedEnv?.() ?? process.env);
|
|
91
|
+
}
|
|
74
92
|
/**
|
|
75
93
|
* Initialize MCP manager with working directory and optionally auto-connect
|
|
76
94
|
*/
|
|
@@ -125,7 +143,7 @@ export class McpManager {
|
|
|
125
143
|
try {
|
|
126
144
|
const configContent = await fs.readFile(this.configPath, "utf-8");
|
|
127
145
|
const rawConfig = JSON.parse(configContent);
|
|
128
|
-
const workspaceConfig = resolveMcpConfig(rawConfig);
|
|
146
|
+
const workspaceConfig = resolveMcpConfig(rawConfig, this.envSnapshot);
|
|
129
147
|
// Extract original (pre-resolution) URLs for safe display
|
|
130
148
|
const originalUrls = {};
|
|
131
149
|
for (const [name, serverConfig] of Object.entries(rawConfig.mcpServers)) {
|
|
@@ -210,28 +228,29 @@ export class McpManager {
|
|
|
210
228
|
}
|
|
211
229
|
// Capture original URL before any resolution for safe display
|
|
212
230
|
const originalUrl = config.url;
|
|
213
|
-
// Expand env vars
|
|
231
|
+
// Expand env vars against the session snapshot (over OS env, e.g. ${TAVILY_API_KEY})
|
|
232
|
+
const env = this.envSnapshot;
|
|
214
233
|
const resolvedConfig = { ...config };
|
|
215
234
|
if (resolvedConfig.command) {
|
|
216
|
-
resolvedConfig.command = expandEnvVars(resolvedConfig.command);
|
|
235
|
+
resolvedConfig.command = expandEnvVars(resolvedConfig.command, env);
|
|
217
236
|
}
|
|
218
237
|
if (resolvedConfig.args) {
|
|
219
|
-
resolvedConfig.args = resolvedConfig.args.map(expandEnvVars);
|
|
238
|
+
resolvedConfig.args = resolvedConfig.args.map((a) => expandEnvVars(a, env));
|
|
220
239
|
}
|
|
221
240
|
if (resolvedConfig.env) {
|
|
222
241
|
const resolvedEnv = {};
|
|
223
242
|
for (const [key, val] of Object.entries(resolvedConfig.env)) {
|
|
224
|
-
resolvedEnv[key] = expandEnvVars(val);
|
|
243
|
+
resolvedEnv[key] = expandEnvVars(val, env);
|
|
225
244
|
}
|
|
226
245
|
resolvedConfig.env = resolvedEnv;
|
|
227
246
|
}
|
|
228
247
|
if (resolvedConfig.url) {
|
|
229
|
-
resolvedConfig.url = expandEnvVars(resolvedConfig.url);
|
|
248
|
+
resolvedConfig.url = expandEnvVars(resolvedConfig.url, env);
|
|
230
249
|
}
|
|
231
250
|
if (resolvedConfig.headers) {
|
|
232
251
|
const resolvedHeaders = {};
|
|
233
252
|
for (const [key, val] of Object.entries(resolvedConfig.headers)) {
|
|
234
|
-
resolvedHeaders[key] = expandEnvVars(val);
|
|
253
|
+
resolvedHeaders[key] = expandEnvVars(val, env);
|
|
235
254
|
}
|
|
236
255
|
resolvedConfig.headers = resolvedHeaders;
|
|
237
256
|
}
|
|
@@ -332,8 +351,11 @@ export class McpManager {
|
|
|
332
351
|
if (!server.config.command) {
|
|
333
352
|
throw new Error(`MCP server ${name} with type "stdio" requires a 'command'`);
|
|
334
353
|
}
|
|
354
|
+
// Base env = OS env overlaid with this session's settings snapshot, so
|
|
355
|
+
// custom env vars from settings.json reach the MCP server subprocess
|
|
356
|
+
// without polluting other sessions sharing one `wave --stdio` process.
|
|
335
357
|
const env = {
|
|
336
|
-
...
|
|
358
|
+
...this.envSnapshot,
|
|
337
359
|
...(server.config.env || {}),
|
|
338
360
|
};
|
|
339
361
|
// For plugin servers, substitute ${WAVE_PLUGIN_ROOT} and ${CLAUDE_PLUGIN_ROOT}
|
|
@@ -179,6 +179,11 @@ class ToolManager {
|
|
|
179
179
|
workflowManager: this.container.has("WorkflowManager")
|
|
180
180
|
? this.container.get("WorkflowManager")
|
|
181
181
|
: undefined,
|
|
182
|
+
sessionEnv: this.container.has("ConfigurationService")
|
|
183
|
+
? this.container
|
|
184
|
+
.get("ConfigurationService")
|
|
185
|
+
?.getMergedEnv?.()
|
|
186
|
+
: undefined,
|
|
182
187
|
sessionId: context.sessionId,
|
|
183
188
|
toolCallId: context.toolCallId,
|
|
184
189
|
};
|