wave-agent-sdk 1.0.4 → 1.0.6
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 +23 -1
- package/dist/agent.js +34 -4
- package/dist/managers/aiManager.d.ts +36 -0
- package/dist/managers/aiManager.js +240 -8
- package/dist/managers/messageManager.d.ts +1 -3
- package/dist/managers/messageManager.js +19 -9
- package/dist/managers/slashCommandManager.js +28 -0
- package/dist/managers/subagentManager.d.ts +0 -2
- package/dist/services/session.js +2 -9
- package/dist/telemetry/instrumentation.d.ts +1 -1
- package/dist/telemetry/instrumentation.js +57 -28
- package/dist/tools/bashTool.js +16 -6
- package/dist/tools/grepTool.js +11 -3
- package/dist/types/index.d.ts +3 -0
- package/dist/types/messaging.d.ts +2 -0
- package/dist/utils/containerSetup.js +0 -3
- package/dist/utils/messageOperations.d.ts +10 -1
- package/dist/utils/messageOperations.js +19 -1
- package/dist/utils/notificationXml.js +5 -0
- package/dist/utils/ripgrep.d.ts +4 -4
- package/dist/utils/ripgrep.js +4 -27
- package/package.json +20 -8
- package/bin/rg +0 -79
- package/dist/managers/forkedAgentManager.d.ts +0 -50
- package/dist/managers/forkedAgentManager.js +0 -130
- package/scripts/install_ripgrep.js +0 -111
- package/scripts/postinstall.js +0 -38
- package/src/agent.ts +0 -1271
- package/src/constants/subagents.ts +0 -4
- package/src/constants/toolLimits.ts +0 -15
- package/src/constants/tools.ts +0 -24
- package/src/core/plugin.ts +0 -237
- package/src/core/session.ts +0 -9
- package/src/index.ts +0 -38
- package/src/managers/MemoryRuleManager.ts +0 -198
- package/src/managers/aiManager.ts +0 -2429
- package/src/managers/backgroundTaskManager.ts +0 -480
- package/src/managers/bangManager.ts +0 -119
- package/src/managers/cronManager.ts +0 -401
- package/src/managers/foregroundTaskManager.ts +0 -30
- package/src/managers/forkedAgentManager.ts +0 -216
- package/src/managers/hookManager.ts +0 -1089
- package/src/managers/liveConfigManager.ts +0 -445
- package/src/managers/lspManager.ts +0 -470
- package/src/managers/mcpManager.ts +0 -1010
- package/src/managers/messageManager.ts +0 -1180
- package/src/managers/messageQueue.ts +0 -157
- package/src/managers/permissionManager.ts +0 -1140
- package/src/managers/planManager.ts +0 -112
- package/src/managers/pluginManager.ts +0 -330
- package/src/managers/pluginScopeManager.ts +0 -124
- package/src/managers/reversionManager.ts +0 -178
- package/src/managers/skillManager.ts +0 -578
- package/src/managers/slashCommandManager.ts +0 -564
- package/src/managers/subagentManager.ts +0 -915
- package/src/managers/toolManager.ts +0 -489
- package/src/managers/workflowManager.ts +0 -493
- package/src/prompts/autoMemory.ts +0 -33
- package/src/prompts/autoMemoryExtraction.ts +0 -146
- package/src/prompts/index.ts +0 -506
- package/src/prompts/planModeReminders.ts +0 -126
- package/src/services/GitService.ts +0 -131
- package/src/services/MarketplaceService.ts +0 -1052
- package/src/services/MemoryRuleService.ts +0 -71
- package/src/services/aiService.ts +0 -919
- package/src/services/authService.ts +0 -565
- package/src/services/autoMemoryService.ts +0 -280
- package/src/services/configurationService.ts +0 -1520
- package/src/services/fileWatcher.ts +0 -319
- package/src/services/hook.ts +0 -348
- package/src/services/initializationService.ts +0 -357
- package/src/services/interactionService.ts +0 -229
- package/src/services/jsonlHandler.ts +0 -295
- package/src/services/memory.ts +0 -234
- package/src/services/pluginLoader.ts +0 -252
- package/src/services/remoteSettingsService.ts +0 -366
- package/src/services/reversionService.ts +0 -122
- package/src/services/session.ts +0 -922
- package/src/services/taskManager.ts +0 -321
- package/src/telemetry/events.ts +0 -65
- package/src/telemetry/instrumentation.ts +0 -499
- package/src/telemetry/sessionTracing.ts +0 -348
- package/src/tools/agentTool.ts +0 -322
- package/src/tools/askUserQuestion.ts +0 -160
- package/src/tools/bashTool.ts +0 -621
- package/src/tools/buildTool.ts +0 -61
- package/src/tools/cronCreateTool.ts +0 -161
- package/src/tools/cronDeleteTool.ts +0 -51
- package/src/tools/cronListTool.ts +0 -47
- package/src/tools/editTool.ts +0 -313
- package/src/tools/enterPlanMode.ts +0 -124
- package/src/tools/enterWorktreeTool.ts +0 -190
- package/src/tools/exitPlanMode.ts +0 -139
- package/src/tools/exitWorktreeTool.ts +0 -236
- package/src/tools/globTool.ts +0 -167
- package/src/tools/grepTool.ts +0 -399
- package/src/tools/lspTool.ts +0 -883
- package/src/tools/readTool.ts +0 -426
- package/src/tools/skillTool.ts +0 -248
- package/src/tools/taskManagementTools.ts +0 -574
- package/src/tools/taskStopTool.ts +0 -78
- package/src/tools/types.ts +0 -137
- package/src/tools/webFetchTool.ts +0 -373
- package/src/tools/workflowTool.ts +0 -205
- package/src/tools/writeTool.ts +0 -267
- package/src/types/agent.ts +0 -128
- package/src/types/auth.ts +0 -19
- package/src/types/commands.ts +0 -30
- package/src/types/config.ts +0 -43
- package/src/types/configuration.ts +0 -168
- package/src/types/core.ts +0 -105
- package/src/types/cron.ts +0 -13
- package/src/types/environment.ts +0 -106
- package/src/types/fileSearch.ts +0 -4
- package/src/types/history.ts +0 -7
- package/src/types/hooks.ts +0 -256
- package/src/types/index.ts +0 -41
- package/src/types/lsp.ts +0 -98
- package/src/types/marketplace.ts +0 -71
- package/src/types/mcp.ts +0 -43
- package/src/types/memoryRule.ts +0 -31
- package/src/types/messaging.ts +0 -114
- package/src/types/permissions.ts +0 -80
- package/src/types/plugins.ts +0 -39
- package/src/types/processes.ts +0 -99
- package/src/types/reversion.ts +0 -29
- package/src/types/session.ts +0 -12
- package/src/types/skills.ts +0 -108
- package/src/types/tasks.ts +0 -13
- package/src/types/telemetry.ts +0 -98
- package/src/types/tools.ts +0 -44
- package/src/types/workflow.ts +0 -6
- package/src/utils/abortUtils.ts +0 -118
- package/src/utils/atomicWrite.ts +0 -61
- package/src/utils/bashParser.ts +0 -899
- package/src/utils/cacheControlUtils.ts +0 -426
- package/src/utils/commandArgumentParser.ts +0 -161
- package/src/utils/commandPathResolver.ts +0 -88
- package/src/utils/configPaths.ts +0 -220
- package/src/utils/configValidator.ts +0 -100
- package/src/utils/constants.ts +0 -39
- package/src/utils/container.ts +0 -92
- package/src/utils/containerSetup.ts +0 -371
- package/src/utils/convertMessagesForAPI.ts +0 -326
- package/src/utils/cronTasks.ts +0 -128
- package/src/utils/cronTasksLock.ts +0 -182
- package/src/utils/cronToHuman.ts +0 -99
- package/src/utils/customCommands.ts +0 -95
- package/src/utils/editUtils.ts +0 -20
- package/src/utils/fileFormat.ts +0 -40
- package/src/utils/fileSearch.ts +0 -151
- package/src/utils/fileUtils.ts +0 -223
- package/src/utils/gitUtils.ts +0 -315
- package/src/utils/globalLogger.ts +0 -128
- package/src/utils/groupMessagesByApiRound.ts +0 -121
- package/src/utils/hookMatcher.ts +0 -176
- package/src/utils/markdownParser.ts +0 -317
- package/src/utils/mcpUtils.ts +0 -138
- package/src/utils/messageOperations.ts +0 -641
- package/src/utils/modelCapabilities.ts +0 -30
- package/src/utils/nameGenerator.ts +0 -99
- package/src/utils/notificationXml.ts +0 -57
- package/src/utils/openaiClient.ts +0 -251
- package/src/utils/parseCronExpression.ts +0 -78
- package/src/utils/path.ts +0 -72
- package/src/utils/pathEncoder.ts +0 -400
- package/src/utils/pathSafety.ts +0 -40
- package/src/utils/promptHistory.ts +0 -168
- package/src/utils/ripgrep.ts +0 -35
- package/src/utils/shellResolver.ts +0 -190
- package/src/utils/skillParser.ts +0 -246
- package/src/utils/stringUtils.ts +0 -156
- package/src/utils/subagentParser.ts +0 -279
- package/src/utils/taskReminder.ts +0 -96
- package/src/utils/tokenCalculation.ts +0 -43
- package/src/utils/tokenEstimate.ts +0 -34
- package/src/utils/toolResultStorage.ts +0 -117
- package/src/utils/worktreeSession.ts +0 -26
- package/src/utils/worktreeUtils.ts +0 -786
- package/src/workflow/budgetTracker.ts +0 -34
- package/src/workflow/concurrencyLimiter.ts +0 -47
- package/src/workflow/journal.ts +0 -95
- package/src/workflow/progressReporter.ts +0 -141
- package/src/workflow/runState.ts +0 -65
- package/src/workflow/scriptRuntime.ts +0 -274
- package/src/workflow/structuredOutput.ts +0 -123
- package/src/workflow/types.ts +0 -101
- package/src/workflow/workflowApis.ts +0 -410
- package/vendor/ripgrep/linux-aarch64/rg +0 -0
- package/vendor/ripgrep/linux-x86_64/rg +0 -0
- package/vendor/ripgrep/macos-aarch64/rg +0 -0
- package/vendor/ripgrep/macos-x86_64/rg +0 -0
- package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
- package/vendor/ripgrep/windows-x86_64/rg.exe +0 -0
|
@@ -1,1520 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Configuration Service
|
|
3
|
-
*
|
|
4
|
-
* Centralized service for loading, validating, and managing Wave configuration files.
|
|
5
|
-
* Replaces distributed configuration logic previously embedded in hook.ts.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { readFileSync, existsSync, promises as fs } from "fs";
|
|
9
|
-
import * as path from "path";
|
|
10
|
-
import * as os from "os";
|
|
11
|
-
import { isValidHookEvent } from "../types/hooks.js";
|
|
12
|
-
import { logger } from "../utils/globalLogger.js";
|
|
13
|
-
import type {
|
|
14
|
-
ConfigurationLoadResult,
|
|
15
|
-
ValidationResult,
|
|
16
|
-
ConfigurationPaths,
|
|
17
|
-
WaveConfiguration,
|
|
18
|
-
Scope,
|
|
19
|
-
MarketplaceConfig,
|
|
20
|
-
} from "../types/configuration.js";
|
|
21
|
-
import {
|
|
22
|
-
getAllConfigPaths,
|
|
23
|
-
getExistingConfigPaths,
|
|
24
|
-
getUserConfigPaths,
|
|
25
|
-
getProjectConfigPaths,
|
|
26
|
-
} from "../utils/configPaths.js";
|
|
27
|
-
import {
|
|
28
|
-
type EnvironmentValidationResult,
|
|
29
|
-
type MergedEnvironmentContext,
|
|
30
|
-
type EnvironmentMergeOptions,
|
|
31
|
-
isValidEnvironmentVars,
|
|
32
|
-
} from "../types/environment.js";
|
|
33
|
-
import {
|
|
34
|
-
GatewayConfig,
|
|
35
|
-
ModelConfig,
|
|
36
|
-
PermissionMode,
|
|
37
|
-
AgentOptions,
|
|
38
|
-
} from "../types/index.js";
|
|
39
|
-
import {
|
|
40
|
-
DEFAULT_WAVE_MAX_INPUT_TOKENS,
|
|
41
|
-
DEFAULT_WAVE_MAX_OUTPUT_TOKENS,
|
|
42
|
-
DEFAULT_SERVER_URL,
|
|
43
|
-
} from "../utils/constants.js";
|
|
44
|
-
import { ClientOptions } from "openai";
|
|
45
|
-
import { parseCustomHeaders } from "../utils/stringUtils.js";
|
|
46
|
-
import {
|
|
47
|
-
getRemoteSettingsSync,
|
|
48
|
-
mergeRemoteSettings,
|
|
49
|
-
} from "./remoteSettingsService.js";
|
|
50
|
-
import { createAuthAwareFetch } from "./authService.js";
|
|
51
|
-
import { ensureWaveRuntimeFilesExcluded } from "../utils/gitUtils.js";
|
|
52
|
-
import { atomicWriteFile } from "../utils/atomicWrite.js";
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Default ConfigurationService implementation
|
|
56
|
-
*
|
|
57
|
-
* Provides centralized configuration loading, validation, and management.
|
|
58
|
-
* Extracted from distributed logic in hook.ts with improved error handling.
|
|
59
|
-
*/
|
|
60
|
-
export class ConfigurationService {
|
|
61
|
-
private currentConfiguration: WaveConfiguration | null = null;
|
|
62
|
-
private options: AgentOptions = {};
|
|
63
|
-
private _configuredEnvKeys = new Set<string>();
|
|
64
|
-
// Per-session environment snapshot: settings.json `env` is stored here (NOT
|
|
65
|
-
// written to process.env) so multiple sessions in one `wave --stdio` process
|
|
66
|
-
// don't cross-pollute. Resolve methods read `this.envSnapshot ?? process.env`.
|
|
67
|
-
private envSnapshot: Record<string, string> = {};
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Set agent options for configuration resolution
|
|
71
|
-
*/
|
|
72
|
-
setOptions(options: AgentOptions): void {
|
|
73
|
-
this.options = options;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Returns a copy of the per-session environment snapshot (settings.json `env`).
|
|
78
|
-
* Priority over OS env; does NOT include OS env. For subprocess spawning use
|
|
79
|
-
* {@link getMergedEnv} instead.
|
|
80
|
-
*/
|
|
81
|
-
getEnvSnapshot(): Record<string, string> {
|
|
82
|
-
return { ...this.envSnapshot };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Returns OS env merged with the session snapshot (snapshot wins). Use this
|
|
87
|
-
* when spawning user-facing subprocesses (bash, hooks, bang, background, MCP)
|
|
88
|
-
* so they inherit both OS env and the session's settings env.
|
|
89
|
-
*/
|
|
90
|
-
getMergedEnv(): Record<string, string> {
|
|
91
|
-
return Object.fromEntries(
|
|
92
|
-
Object.entries({ ...process.env, ...this.envSnapshot }).filter(
|
|
93
|
-
([, v]) => v !== undefined,
|
|
94
|
-
),
|
|
95
|
-
) as Record<string, string>;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Core loading operations
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Load and merge configuration with comprehensive validation
|
|
102
|
-
*/
|
|
103
|
-
async loadMergedConfiguration(
|
|
104
|
-
workdir: string,
|
|
105
|
-
): Promise<ConfigurationLoadResult> {
|
|
106
|
-
try {
|
|
107
|
-
// Use the merged configuration function (this loads user and project configs internally)
|
|
108
|
-
const mergedConfig = loadMergedWaveConfig(workdir);
|
|
109
|
-
|
|
110
|
-
if (!mergedConfig) {
|
|
111
|
-
// Still set system environment variables even if no config is found
|
|
112
|
-
const env = { WAVE_PROJECT_DIR: workdir };
|
|
113
|
-
this.setEnvironmentVars(env);
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
configuration: { env },
|
|
117
|
-
success: true, // No config is valid
|
|
118
|
-
warnings: [
|
|
119
|
-
"No configuration files found in user or project directories",
|
|
120
|
-
],
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Comprehensive validation
|
|
125
|
-
const validation = this.validateConfiguration(mergedConfig);
|
|
126
|
-
|
|
127
|
-
if (!validation.isValid) {
|
|
128
|
-
return {
|
|
129
|
-
configuration: null,
|
|
130
|
-
success: false,
|
|
131
|
-
error: `Merged configuration validation failed: ${validation.errors.join(", ")}`,
|
|
132
|
-
warnings: validation.warnings,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Merge remote settings (highest priority: Remote > Local > Project > User)
|
|
137
|
-
const remoteSettings = getRemoteSettingsSync();
|
|
138
|
-
const finalConfig = remoteSettings
|
|
139
|
-
? mergeRemoteSettings(mergedConfig, remoteSettings)
|
|
140
|
-
: mergedConfig;
|
|
141
|
-
|
|
142
|
-
// Success case
|
|
143
|
-
this.currentConfiguration = finalConfig;
|
|
144
|
-
|
|
145
|
-
// Set environment variables from merged config and inject system variables
|
|
146
|
-
const env = {
|
|
147
|
-
...(finalConfig.env || {}),
|
|
148
|
-
WAVE_PROJECT_DIR: workdir,
|
|
149
|
-
};
|
|
150
|
-
this.setEnvironmentVars(env);
|
|
151
|
-
finalConfig.env = env;
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
configuration: finalConfig,
|
|
155
|
-
success: true,
|
|
156
|
-
sourcePath: "merged configuration",
|
|
157
|
-
warnings: validation.warnings,
|
|
158
|
-
};
|
|
159
|
-
} catch (error) {
|
|
160
|
-
return {
|
|
161
|
-
configuration: null,
|
|
162
|
-
success: false,
|
|
163
|
-
error: `Failed to load merged configuration from ${workdir}: ${(error as Error).message}`,
|
|
164
|
-
warnings: [],
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Validation operations
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Validate configuration object structure and values
|
|
173
|
-
*/
|
|
174
|
-
validateConfiguration(config: WaveConfiguration): ValidationResult {
|
|
175
|
-
const result: ValidationResult = {
|
|
176
|
-
isValid: true,
|
|
177
|
-
errors: [],
|
|
178
|
-
warnings: [],
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
// Validate basic structure
|
|
182
|
-
if (!config || typeof config !== "object") {
|
|
183
|
-
result.isValid = false;
|
|
184
|
-
result.errors.push("Configuration must be a valid object");
|
|
185
|
-
return result;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Validate hooks if present
|
|
189
|
-
if (config.hooks !== undefined) {
|
|
190
|
-
if (typeof config.hooks !== "object" || config.hooks === null) {
|
|
191
|
-
result.isValid = false;
|
|
192
|
-
result.errors.push("Hooks configuration must be an object");
|
|
193
|
-
} else {
|
|
194
|
-
for (const [event, eventConfigs] of Object.entries(config.hooks)) {
|
|
195
|
-
if (!isValidHookEvent(event)) {
|
|
196
|
-
result.warnings.push(`Unknown hook event: ${event}`);
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (!Array.isArray(eventConfigs)) {
|
|
201
|
-
result.isValid = false;
|
|
202
|
-
result.errors.push(`Hook event '${event}' must be an array`);
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Validate individual hook configurations
|
|
207
|
-
for (let i = 0; i < eventConfigs.length; i++) {
|
|
208
|
-
const hookConfig = eventConfigs[i];
|
|
209
|
-
if (!hookConfig || typeof hookConfig !== "object") {
|
|
210
|
-
result.isValid = false;
|
|
211
|
-
result.errors.push(
|
|
212
|
-
`Hook configuration ${i} for event '${event}' must be an object`,
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Validate enabledPlugins if present
|
|
221
|
-
if (config.enabledPlugins !== undefined) {
|
|
222
|
-
if (
|
|
223
|
-
typeof config.enabledPlugins !== "object" ||
|
|
224
|
-
config.enabledPlugins === null
|
|
225
|
-
) {
|
|
226
|
-
result.isValid = false;
|
|
227
|
-
result.errors.push("enabledPlugins configuration must be an object");
|
|
228
|
-
} else {
|
|
229
|
-
for (const [pluginId, enabled] of Object.entries(
|
|
230
|
-
config.enabledPlugins,
|
|
231
|
-
)) {
|
|
232
|
-
if (typeof enabled !== "boolean") {
|
|
233
|
-
result.isValid = false;
|
|
234
|
-
result.errors.push(
|
|
235
|
-
`Value for plugin '${pluginId}' in enabledPlugins must be a boolean`,
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
if (!pluginId.includes("@")) {
|
|
239
|
-
result.warnings.push(
|
|
240
|
-
`Plugin ID '${pluginId}' in enabledPlugins should follow 'name@marketplace' format`,
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Validate environment variables if present
|
|
248
|
-
if (config.env !== undefined) {
|
|
249
|
-
const envValidation = validateEnvironmentConfig(config.env);
|
|
250
|
-
if (!envValidation.isValid) {
|
|
251
|
-
result.isValid = false;
|
|
252
|
-
result.errors.push(...envValidation.errors);
|
|
253
|
-
}
|
|
254
|
-
result.warnings.push(...envValidation.warnings);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Validate permissions if present
|
|
258
|
-
if (config.permissions !== undefined) {
|
|
259
|
-
if (
|
|
260
|
-
typeof config.permissions !== "object" ||
|
|
261
|
-
config.permissions === null
|
|
262
|
-
) {
|
|
263
|
-
result.isValid = false;
|
|
264
|
-
result.errors.push("Permissions configuration must be an object");
|
|
265
|
-
} else {
|
|
266
|
-
// Validate allow if present
|
|
267
|
-
if (config.permissions.allow !== undefined) {
|
|
268
|
-
if (!Array.isArray(config.permissions.allow)) {
|
|
269
|
-
result.isValid = false;
|
|
270
|
-
result.errors.push("Permissions allow must be an array of strings");
|
|
271
|
-
} else if (
|
|
272
|
-
!config.permissions.allow.every((rule) => typeof rule === "string")
|
|
273
|
-
) {
|
|
274
|
-
result.isValid = false;
|
|
275
|
-
result.errors.push("All permission allow rules must be strings");
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Validate deny if present
|
|
280
|
-
if (config.permissions.deny !== undefined) {
|
|
281
|
-
if (!Array.isArray(config.permissions.deny)) {
|
|
282
|
-
result.isValid = false;
|
|
283
|
-
result.errors.push("Permissions deny must be an array of strings");
|
|
284
|
-
} else if (
|
|
285
|
-
!config.permissions.deny.every((rule) => typeof rule === "string")
|
|
286
|
-
) {
|
|
287
|
-
result.isValid = false;
|
|
288
|
-
result.errors.push("All permission deny rules must be strings");
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Validate permissionMode if present
|
|
293
|
-
if (config.permissions.permissionMode !== undefined) {
|
|
294
|
-
const validModes: PermissionMode[] = [
|
|
295
|
-
"default",
|
|
296
|
-
"bypassPermissions",
|
|
297
|
-
"acceptEdits",
|
|
298
|
-
"plan",
|
|
299
|
-
"dontAsk",
|
|
300
|
-
];
|
|
301
|
-
if (!validModes.includes(config.permissions.permissionMode)) {
|
|
302
|
-
result.isValid = false;
|
|
303
|
-
result.errors.push(
|
|
304
|
-
`Invalid permissionMode: "${config.permissions.permissionMode}". Must be one of: ${validModes.join(", ")}`,
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Validate autoMemoryEnabled if present
|
|
312
|
-
if (
|
|
313
|
-
config.autoMemoryEnabled !== undefined &&
|
|
314
|
-
typeof config.autoMemoryEnabled !== "boolean"
|
|
315
|
-
) {
|
|
316
|
-
result.isValid = false;
|
|
317
|
-
result.errors.push("autoMemoryEnabled configuration must be a boolean");
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Validate autoMemoryFrequency if present
|
|
321
|
-
if (
|
|
322
|
-
config.autoMemoryFrequency !== undefined &&
|
|
323
|
-
(typeof config.autoMemoryFrequency !== "number" ||
|
|
324
|
-
config.autoMemoryFrequency <= 0)
|
|
325
|
-
) {
|
|
326
|
-
result.isValid = false;
|
|
327
|
-
result.errors.push(
|
|
328
|
-
"autoMemoryFrequency configuration must be a positive number",
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Validate models if present
|
|
333
|
-
if (config.models !== undefined) {
|
|
334
|
-
if (typeof config.models !== "object" || config.models === null) {
|
|
335
|
-
result.isValid = false;
|
|
336
|
-
result.errors.push("models configuration must be an object");
|
|
337
|
-
} else {
|
|
338
|
-
for (const [modelName, modelConfig] of Object.entries(config.models)) {
|
|
339
|
-
if (typeof modelConfig !== "object" || modelConfig === null) {
|
|
340
|
-
result.isValid = false;
|
|
341
|
-
result.errors.push(
|
|
342
|
-
`Configuration for model '${modelName}' must be an object`,
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Validate worktree if present
|
|
350
|
-
if (config.worktree !== undefined) {
|
|
351
|
-
if (typeof config.worktree !== "object" || config.worktree === null) {
|
|
352
|
-
result.isValid = false;
|
|
353
|
-
result.errors.push("worktree configuration must be an object");
|
|
354
|
-
} else if (
|
|
355
|
-
config.worktree.baseRef !== undefined &&
|
|
356
|
-
config.worktree.baseRef !== "fresh" &&
|
|
357
|
-
config.worktree.baseRef !== "head"
|
|
358
|
-
) {
|
|
359
|
-
result.isValid = false;
|
|
360
|
-
result.errors.push(
|
|
361
|
-
`Invalid worktree.baseRef: "${config.worktree.baseRef}". Must be "fresh" or "head".`,
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return result;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Validate configuration file without loading
|
|
371
|
-
*/
|
|
372
|
-
validateConfigurationFile(filePath: string): ValidationResult {
|
|
373
|
-
const result: ValidationResult = {
|
|
374
|
-
isValid: true,
|
|
375
|
-
errors: [],
|
|
376
|
-
warnings: [],
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
if (!existsSync(filePath)) {
|
|
380
|
-
result.isValid = false;
|
|
381
|
-
result.errors.push(`Configuration file not found: ${filePath}`);
|
|
382
|
-
return result;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
try {
|
|
386
|
-
const content = readFileSync(filePath, "utf-8");
|
|
387
|
-
const config = JSON.parse(content) as WaveConfiguration;
|
|
388
|
-
|
|
389
|
-
// Use the main validation method
|
|
390
|
-
const configValidation = this.validateConfiguration(config);
|
|
391
|
-
result.isValid = configValidation.isValid;
|
|
392
|
-
result.errors = configValidation.errors;
|
|
393
|
-
result.warnings = configValidation.warnings;
|
|
394
|
-
} catch (error) {
|
|
395
|
-
result.isValid = false;
|
|
396
|
-
if (error instanceof SyntaxError) {
|
|
397
|
-
result.errors.push(
|
|
398
|
-
`Invalid JSON syntax in ${filePath}: ${error.message}`,
|
|
399
|
-
);
|
|
400
|
-
} else {
|
|
401
|
-
result.errors.push(
|
|
402
|
-
`Error reading configuration file ${filePath}: ${(error as Error).message}`,
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
return result;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Utility operations
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Store environment variables from configuration into the per-session
|
|
414
|
-
* snapshot (NOT process.env). Settings `env` shadows OS env for this session
|
|
415
|
-
* only — multiple sessions in one stdio process stay isolated.
|
|
416
|
-
*
|
|
417
|
-
* Exception: `WAVE_SERVER_URL` is also mirrored to `process.env` because the
|
|
418
|
-
* process-level singletons (AuthService, remoteSettingsService background
|
|
419
|
-
* fetch) need to read it and don't hold a per-session snapshot. Same value
|
|
420
|
-
* across sessions ⇒ no cross-pollution. See docs/specs/core/agent-config.md.
|
|
421
|
-
*/
|
|
422
|
-
setEnvironmentVars(env: Record<string, string>): void {
|
|
423
|
-
for (const [key, value] of Object.entries(env)) {
|
|
424
|
-
if (
|
|
425
|
-
process.env[key] !== undefined &&
|
|
426
|
-
!this._configuredEnvKeys.has(key) &&
|
|
427
|
-
process.env[key] !== value
|
|
428
|
-
) {
|
|
429
|
-
logger.warn(`Overriding environment variable: ${key}`);
|
|
430
|
-
}
|
|
431
|
-
this.envSnapshot[key] = value;
|
|
432
|
-
this._configuredEnvKeys.add(key);
|
|
433
|
-
// WAVE_SERVER_URL is consumed by process-level singletons (AuthService,
|
|
434
|
-
// remoteSettingsService) that can't see the per-session snapshot — mirror
|
|
435
|
-
// it to process.env so they read the settings value. Same value across
|
|
436
|
-
// sessions ⇒ no last-session-wins cross-pollution.
|
|
437
|
-
if (key === "WAVE_SERVER_URL") {
|
|
438
|
-
process.env[key] = value;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// =============================================================================
|
|
444
|
-
// Configuration Resolution Methods (merged from configResolver.ts)
|
|
445
|
-
// =============================================================================
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Read SSO token from ~/.wave/auth.json
|
|
449
|
-
*/
|
|
450
|
-
private readSSOToken(): string | undefined {
|
|
451
|
-
const homeDir = os.homedir();
|
|
452
|
-
const authPath = path.join(homeDir, ".wave", "auth.json");
|
|
453
|
-
if (!existsSync(authPath)) {
|
|
454
|
-
return undefined;
|
|
455
|
-
}
|
|
456
|
-
try {
|
|
457
|
-
const content = readFileSync(authPath, "utf-8");
|
|
458
|
-
const authConfig = JSON.parse(content) as { SSO_TOKEN?: string };
|
|
459
|
-
return authConfig.SSO_TOKEN;
|
|
460
|
-
} catch {
|
|
461
|
-
return undefined;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* Resolves gateway configuration from constructor args and environment
|
|
467
|
-
* Resolution priority: SSO_TOKEN > options > env (from settings.json) > process.env > error
|
|
468
|
-
* @param apiKey - API key override (optional)
|
|
469
|
-
* @param baseURL - Base URL override (optional)
|
|
470
|
-
* @param defaultHeaders - HTTP headers override (optional)
|
|
471
|
-
* @param fetchOptions - Fetch options override (optional)
|
|
472
|
-
* @param fetch - Custom fetch implementation override (optional)
|
|
473
|
-
* @returns Resolved gateway configuration
|
|
474
|
-
* @returns Resolved model configuration (model/fastModel may be undefined if not yet configured)
|
|
475
|
-
*/
|
|
476
|
-
resolveGatewayConfig(
|
|
477
|
-
apiKey?: string,
|
|
478
|
-
baseURL?: string,
|
|
479
|
-
defaultHeaders?: Record<string, string>,
|
|
480
|
-
fetchOptions?: ClientOptions["fetchOptions"],
|
|
481
|
-
fetch?: ClientOptions["fetch"],
|
|
482
|
-
): GatewayConfig {
|
|
483
|
-
// Check for SSO token first - if present and server URL is available, use SSO mode.
|
|
484
|
-
// Server URL resolution: options.serverUrl > process.env.WAVE_SERVER_URL > default.
|
|
485
|
-
// settings.json `env` WAVE_SERVER_URL is mirrored to process.env by
|
|
486
|
-
// setEnvironmentVars (so process-level singletons AuthService / remoteSettings
|
|
487
|
-
// can read it), then read here. See docs/specs/core/agent-config.md.
|
|
488
|
-
const ssoToken = this.readSSOToken();
|
|
489
|
-
const serverUrl =
|
|
490
|
-
this.options.serverUrl ||
|
|
491
|
-
process.env.WAVE_SERVER_URL ||
|
|
492
|
-
DEFAULT_SERVER_URL;
|
|
493
|
-
if (ssoToken && serverUrl) {
|
|
494
|
-
const baseFetch = fetch ?? this.options.fetch ?? globalThis.fetch;
|
|
495
|
-
const authAwareFetch = createAuthAwareFetch(
|
|
496
|
-
baseFetch as typeof globalThis.fetch,
|
|
497
|
-
) as ClientOptions["fetch"];
|
|
498
|
-
return {
|
|
499
|
-
apiKey: ssoToken,
|
|
500
|
-
baseURL: `${serverUrl}/api/v1`,
|
|
501
|
-
defaultHeaders:
|
|
502
|
-
Object.keys(defaultHeaders || {}).length > 0
|
|
503
|
-
? defaultHeaders
|
|
504
|
-
: undefined,
|
|
505
|
-
fetchOptions: fetchOptions ?? this.options.fetchOptions,
|
|
506
|
-
fetch: authAwareFetch,
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Resolve API key: override > options > env (settings.json) > process.env
|
|
511
|
-
// Note: Explicitly provided empty strings should be treated as invalid, not fall back to env
|
|
512
|
-
let resolvedApiKey: string | undefined;
|
|
513
|
-
if (apiKey !== undefined) {
|
|
514
|
-
resolvedApiKey = apiKey;
|
|
515
|
-
} else if (this.options.apiKey !== undefined) {
|
|
516
|
-
resolvedApiKey = this.options.apiKey;
|
|
517
|
-
} else {
|
|
518
|
-
resolvedApiKey =
|
|
519
|
-
this.envSnapshot.WAVE_API_KEY ?? process.env.WAVE_API_KEY;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// Resolve base URL: override > options > env (settings.json) > process.env
|
|
523
|
-
// Note: Explicitly provided empty strings should be treated as invalid, not fall back to env
|
|
524
|
-
let resolvedBaseURL: string | undefined;
|
|
525
|
-
if (baseURL !== undefined) {
|
|
526
|
-
resolvedBaseURL = baseURL;
|
|
527
|
-
} else if (this.options.baseURL !== undefined) {
|
|
528
|
-
resolvedBaseURL = this.options.baseURL;
|
|
529
|
-
} else {
|
|
530
|
-
resolvedBaseURL =
|
|
531
|
-
this.envSnapshot.WAVE_BASE_URL ?? process.env.WAVE_BASE_URL;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// Fallback to process.env if still not resolved (for dynamic updates in tests)
|
|
535
|
-
if (resolvedApiKey === undefined) {
|
|
536
|
-
resolvedApiKey =
|
|
537
|
-
this.envSnapshot.WAVE_API_KEY ?? process.env.WAVE_API_KEY;
|
|
538
|
-
}
|
|
539
|
-
if (!resolvedBaseURL) {
|
|
540
|
-
resolvedBaseURL =
|
|
541
|
-
this.envSnapshot.WAVE_BASE_URL ?? process.env.WAVE_BASE_URL;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// Treat empty string as not provided
|
|
545
|
-
if (resolvedBaseURL?.trim() === "") {
|
|
546
|
-
resolvedBaseURL = undefined;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// Resolve custom headers from environment: env (settings.json) > process.env
|
|
550
|
-
const envCustomHeaders =
|
|
551
|
-
this.envSnapshot.WAVE_CUSTOM_HEADERS ??
|
|
552
|
-
process.env.WAVE_CUSTOM_HEADERS ??
|
|
553
|
-
"";
|
|
554
|
-
const parsedEnvHeaders = parseCustomHeaders(envCustomHeaders);
|
|
555
|
-
|
|
556
|
-
// Merge headers: env headers < options < override
|
|
557
|
-
const resolvedHeaders = {
|
|
558
|
-
...parsedEnvHeaders,
|
|
559
|
-
...this.options.defaultHeaders,
|
|
560
|
-
...defaultHeaders,
|
|
561
|
-
};
|
|
562
|
-
|
|
563
|
-
return {
|
|
564
|
-
apiKey: resolvedApiKey,
|
|
565
|
-
baseURL: resolvedBaseURL,
|
|
566
|
-
defaultHeaders:
|
|
567
|
-
Object.keys(resolvedHeaders).length > 0 ? resolvedHeaders : undefined,
|
|
568
|
-
fetchOptions: fetchOptions ?? this.options.fetchOptions,
|
|
569
|
-
fetch: fetch ?? this.options.fetch,
|
|
570
|
-
};
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
* Resolves model configuration with fallbacks
|
|
575
|
-
* Resolution priority: override > options > env (from settings.json) > process.env > default
|
|
576
|
-
* @param model - Agent model override (optional)
|
|
577
|
-
* @param fastModel - Fast model override (optional)
|
|
578
|
-
* @param maxTokens - Max output tokens override (optional)
|
|
579
|
-
* @param permissionMode - Permission mode override (optional)
|
|
580
|
-
* @returns Resolved model configuration with defaults
|
|
581
|
-
*/
|
|
582
|
-
resolveModelConfig(
|
|
583
|
-
model?: string,
|
|
584
|
-
fastModel?: string,
|
|
585
|
-
maxTokens?: number,
|
|
586
|
-
permissionMode?: PermissionMode,
|
|
587
|
-
): ModelConfig {
|
|
588
|
-
// Resolve agent model: override > options > currentConfiguration (settings.json model, possibly remote-merged) > process.env
|
|
589
|
-
// Priority: user's explicit model field > admin's env.WAVE_MODEL default.
|
|
590
|
-
// If admin wants hard enforcement, they set the `model` scalar field (overwrites local in mergeRemoteSettings).
|
|
591
|
-
const resolvedAgentModel =
|
|
592
|
-
model ||
|
|
593
|
-
this.options.model ||
|
|
594
|
-
this.currentConfiguration?.model ||
|
|
595
|
-
(this.envSnapshot.WAVE_MODEL ?? process.env.WAVE_MODEL);
|
|
596
|
-
|
|
597
|
-
// Resolve fast model: override > options > process.env (includes settings.json env)
|
|
598
|
-
const resolvedFastModel =
|
|
599
|
-
fastModel ||
|
|
600
|
-
this.options.fastModel ||
|
|
601
|
-
(this.envSnapshot.WAVE_FAST_MODEL ?? process.env.WAVE_FAST_MODEL);
|
|
602
|
-
|
|
603
|
-
// Resolve max output tokens
|
|
604
|
-
const resolvedMaxTokens = this.resolveMaxOutputTokens(maxTokens);
|
|
605
|
-
|
|
606
|
-
const baseConfig: ModelConfig = {
|
|
607
|
-
model: resolvedAgentModel,
|
|
608
|
-
fastModel: resolvedFastModel,
|
|
609
|
-
maxTokens: resolvedMaxTokens,
|
|
610
|
-
permissionMode: permissionMode ?? this.options.permissionMode,
|
|
611
|
-
};
|
|
612
|
-
|
|
613
|
-
// Resolve fast model generation params from models[fastModel].options.
|
|
614
|
-
// Set on baseConfig before the modelSpecificConfig spread so it isn't overwritten.
|
|
615
|
-
const fastModelSource =
|
|
616
|
-
resolvedFastModel &&
|
|
617
|
-
this.currentConfiguration?.models?.[resolvedFastModel];
|
|
618
|
-
if (fastModelSource && fastModelSource.options) {
|
|
619
|
-
baseConfig.fastModelOptions = fastModelSource.options;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// Resolve fast-model disable-thinking params from models[fastModel].disableThinkingOptions
|
|
623
|
-
const fastModelDisableThinking =
|
|
624
|
-
fastModelSource && fastModelSource.disableThinkingOptions
|
|
625
|
-
? fastModelSource.disableThinkingOptions
|
|
626
|
-
: undefined;
|
|
627
|
-
if (fastModelDisableThinking) {
|
|
628
|
-
baseConfig.disableThinkingOptions = fastModelDisableThinking;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// Merge model-specific settings from configuration
|
|
632
|
-
const modelSpecificConfig =
|
|
633
|
-
resolvedAgentModel &&
|
|
634
|
-
this.currentConfiguration?.models?.[resolvedAgentModel];
|
|
635
|
-
|
|
636
|
-
if (modelSpecificConfig) {
|
|
637
|
-
const resolved: ModelConfig = {
|
|
638
|
-
...baseConfig,
|
|
639
|
-
...modelSpecificConfig,
|
|
640
|
-
};
|
|
641
|
-
// Re-apply after the spread so the agent model's own
|
|
642
|
-
// disableThinkingOptions cannot clobber the fast-model value.
|
|
643
|
-
if (fastModelDisableThinking) {
|
|
644
|
-
resolved.disableThinkingOptions = fastModelDisableThinking;
|
|
645
|
-
}
|
|
646
|
-
return resolved;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
return baseConfig;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/**
|
|
653
|
-
* Resolves token limit with fallbacks
|
|
654
|
-
* Resolution priority: override > options > env (from settings.json) > process.env > default
|
|
655
|
-
* @param constructorLimit - Token limit override (optional)
|
|
656
|
-
* @returns Resolved token limit
|
|
657
|
-
*/
|
|
658
|
-
resolveMaxInputTokens(constructorLimit?: number): number {
|
|
659
|
-
// If override value provided, use it
|
|
660
|
-
if (constructorLimit !== undefined) {
|
|
661
|
-
return constructorLimit;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// If options value provided, use it
|
|
665
|
-
if (this.options.maxInputTokens !== undefined) {
|
|
666
|
-
return this.options.maxInputTokens;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
// Try env (settings.json snapshot) first, then process.env
|
|
670
|
-
const envMaxInputTokens =
|
|
671
|
-
this.envSnapshot.WAVE_MAX_INPUT_TOKENS ??
|
|
672
|
-
process.env.WAVE_MAX_INPUT_TOKENS;
|
|
673
|
-
if (envMaxInputTokens) {
|
|
674
|
-
const parsed = parseInt(envMaxInputTokens, 10);
|
|
675
|
-
if (!isNaN(parsed)) {
|
|
676
|
-
return parsed;
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// Use default
|
|
681
|
-
return DEFAULT_WAVE_MAX_INPUT_TOKENS;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
/**
|
|
685
|
-
* Resolves preferred language with fallbacks
|
|
686
|
-
* Resolution priority: override > options > settings.json > undefined
|
|
687
|
-
* @param constructorLanguage - Language override (optional)
|
|
688
|
-
* @returns Resolved language or undefined
|
|
689
|
-
*/
|
|
690
|
-
resolveLanguage(constructorLanguage?: string): string | undefined {
|
|
691
|
-
// 1. Override (highest priority)
|
|
692
|
-
if (constructorLanguage !== undefined) {
|
|
693
|
-
return constructorLanguage;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
// 2. Agent options
|
|
697
|
-
if (this.options.language !== undefined) {
|
|
698
|
-
return this.options.language;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// 2. settings.json (merged)
|
|
702
|
-
if (this.currentConfiguration?.language) {
|
|
703
|
-
return this.currentConfiguration.language;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
return undefined;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
/**
|
|
710
|
-
* Resolves auto-memory enabled state with fallbacks
|
|
711
|
-
* Resolution priority: settings.json > WAVE_DISABLE_AUTO_MEMORY > default (true)
|
|
712
|
-
* @returns Resolved auto-memory enabled state
|
|
713
|
-
*/
|
|
714
|
-
resolveAutoMemoryEnabled(): boolean {
|
|
715
|
-
// 1. settings.json (merged)
|
|
716
|
-
if (this.currentConfiguration?.autoMemoryEnabled !== undefined) {
|
|
717
|
-
return this.currentConfiguration.autoMemoryEnabled;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
// 2. WAVE_DISABLE_AUTO_MEMORY environment variable (settings snapshot > OS env)
|
|
721
|
-
const disableAutoMemory =
|
|
722
|
-
this.envSnapshot.WAVE_DISABLE_AUTO_MEMORY ??
|
|
723
|
-
process.env.WAVE_DISABLE_AUTO_MEMORY;
|
|
724
|
-
if (disableAutoMemory === "1" || disableAutoMemory === "true") {
|
|
725
|
-
return false;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
// 3. Default (true)
|
|
729
|
-
return true;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
/**
|
|
733
|
-
* Resolves worktree base ref with fallbacks
|
|
734
|
-
* Resolution priority: settings.json > default ("fresh")
|
|
735
|
-
* @returns Resolved worktree base ref
|
|
736
|
-
*/
|
|
737
|
-
resolveWorktreeBaseRef(): "fresh" | "head" {
|
|
738
|
-
const baseRef = this.currentConfiguration?.worktree?.baseRef;
|
|
739
|
-
if (baseRef === "head") {
|
|
740
|
-
return "head";
|
|
741
|
-
}
|
|
742
|
-
return "fresh";
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
/**
|
|
746
|
-
* Resolves auto-memory extraction frequency with fallbacks
|
|
747
|
-
* Resolution priority: settings.json > WAVE_AUTO_MEMORY_FREQUENCY > default (1)
|
|
748
|
-
* @returns Resolved auto-memory extraction frequency (turns)
|
|
749
|
-
*/
|
|
750
|
-
resolveAutoMemoryFrequency(): number {
|
|
751
|
-
// 1. settings.json (merged)
|
|
752
|
-
if (this.currentConfiguration?.autoMemoryFrequency !== undefined) {
|
|
753
|
-
return this.currentConfiguration.autoMemoryFrequency;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// 2. WAVE_AUTO_MEMORY_FREQUENCY environment variable (settings snapshot > OS env)
|
|
757
|
-
const envFrequency =
|
|
758
|
-
this.envSnapshot.WAVE_AUTO_MEMORY_FREQUENCY ??
|
|
759
|
-
process.env.WAVE_AUTO_MEMORY_FREQUENCY;
|
|
760
|
-
if (envFrequency) {
|
|
761
|
-
const parsed = parseInt(envFrequency, 10);
|
|
762
|
-
if (!isNaN(parsed) && parsed > 0) {
|
|
763
|
-
return parsed;
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
// 3. Default (1)
|
|
768
|
-
return 1;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
/**
|
|
772
|
-
* Resolves max output tokens with fallbacks
|
|
773
|
-
* Resolution priority: override > options > env (from settings.json) > process.env > default
|
|
774
|
-
* @param constructorLimit - Max output tokens override (optional)
|
|
775
|
-
* @returns Resolved max output tokens
|
|
776
|
-
*/
|
|
777
|
-
resolveMaxOutputTokens(constructorLimit?: number): number {
|
|
778
|
-
// If override value provided, use it
|
|
779
|
-
if (constructorLimit !== undefined) {
|
|
780
|
-
return constructorLimit;
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
// If options value provided, use it
|
|
784
|
-
if (this.options.maxTokens !== undefined) {
|
|
785
|
-
return this.options.maxTokens;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
// Try env (settings.json snapshot) first, then process.env
|
|
789
|
-
const envMaxOutputTokens =
|
|
790
|
-
this.envSnapshot.WAVE_MAX_OUTPUT_TOKENS ??
|
|
791
|
-
process.env.WAVE_MAX_OUTPUT_TOKENS;
|
|
792
|
-
if (envMaxOutputTokens) {
|
|
793
|
-
const parsed = parseInt(envMaxOutputTokens, 10);
|
|
794
|
-
if (!isNaN(parsed) && parsed > 0) {
|
|
795
|
-
return parsed;
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
// Use default
|
|
800
|
-
return DEFAULT_WAVE_MAX_OUTPUT_TOKENS;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
/**
|
|
804
|
-
* Set the active model in the session
|
|
805
|
-
*/
|
|
806
|
-
setModel(model: string): void {
|
|
807
|
-
this.options.model = model;
|
|
808
|
-
this.persistModelToSettings(model);
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
private async persistModelToSettings(model: string): Promise<void> {
|
|
812
|
-
const configPath = getUserConfigPaths()[0]; // ~/.wave/settings.json
|
|
813
|
-
const configDir = path.dirname(configPath);
|
|
814
|
-
if (!existsSync(configDir)) {
|
|
815
|
-
await fs.mkdir(configDir, { recursive: true });
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
let config: WaveConfiguration = {};
|
|
819
|
-
if (existsSync(configPath)) {
|
|
820
|
-
try {
|
|
821
|
-
const content = await fs.readFile(configPath, "utf-8");
|
|
822
|
-
config = JSON.parse(content);
|
|
823
|
-
} catch {
|
|
824
|
-
// Start fresh if corrupted
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
config.model = model;
|
|
829
|
-
await atomicWriteFile(configPath, JSON.stringify(config, null, 2));
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
/**
|
|
833
|
-
* Get all configured models from settings.json and environment
|
|
834
|
-
*/
|
|
835
|
-
getConfiguredModels(): string[] {
|
|
836
|
-
const models = new Set<string>();
|
|
837
|
-
|
|
838
|
-
// Add current model from options or environment (settings snapshot > OS env)
|
|
839
|
-
const currentModel =
|
|
840
|
-
this.options.model ||
|
|
841
|
-
(this.envSnapshot.WAVE_MODEL ?? process.env.WAVE_MODEL);
|
|
842
|
-
if (currentModel) {
|
|
843
|
-
models.add(currentModel);
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
// Persisted model from settings (includes remote-merged)
|
|
847
|
-
if (this.currentConfiguration?.model) {
|
|
848
|
-
models.add(this.currentConfiguration.model);
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
// Add models from merged configuration
|
|
852
|
-
if (this.currentConfiguration?.models) {
|
|
853
|
-
Object.keys(this.currentConfiguration.models).forEach((model) => {
|
|
854
|
-
models.add(model);
|
|
855
|
-
});
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
return Array.from(models);
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
/**
|
|
862
|
-
* Get the telemetry config from the loaded settings.
|
|
863
|
-
*/
|
|
864
|
-
resolveTelemetryConfig():
|
|
865
|
-
| Partial<import("../types/telemetry.js").TelemetryConfig>
|
|
866
|
-
| undefined {
|
|
867
|
-
return this.currentConfiguration?.monitoring?.telemetry;
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
/**
|
|
871
|
-
* Resolve all configuration file paths
|
|
872
|
-
*/
|
|
873
|
-
getConfigurationPaths(workdir: string): ConfigurationPaths {
|
|
874
|
-
const allPaths = getAllConfigPaths(workdir);
|
|
875
|
-
const existingPaths = getExistingConfigPaths(workdir);
|
|
876
|
-
|
|
877
|
-
return {
|
|
878
|
-
userPaths: allPaths.userPaths,
|
|
879
|
-
projectPaths: allPaths.projectPaths,
|
|
880
|
-
builtinPaths: allPaths.builtinPaths,
|
|
881
|
-
allPaths: allPaths.allPaths,
|
|
882
|
-
existingPaths: existingPaths.existingPaths,
|
|
883
|
-
};
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
/**
|
|
887
|
-
* Add a permission rule to the local settings.local.json
|
|
888
|
-
*/
|
|
889
|
-
async addAllowedRule(workdir: string, rule: string): Promise<void> {
|
|
890
|
-
const localConfigPath = path.join(workdir, ".wave", "settings.local.json");
|
|
891
|
-
|
|
892
|
-
// Ensure .wave directory exists
|
|
893
|
-
const waveDir = path.join(workdir, ".wave");
|
|
894
|
-
ensureWaveRuntimeFilesExcluded(workdir);
|
|
895
|
-
if (!existsSync(waveDir)) {
|
|
896
|
-
await fs.mkdir(waveDir, { recursive: true });
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
let config: WaveConfiguration = {};
|
|
900
|
-
if (existsSync(localConfigPath)) {
|
|
901
|
-
try {
|
|
902
|
-
const content = await fs.readFile(localConfigPath, "utf-8");
|
|
903
|
-
config = JSON.parse(content);
|
|
904
|
-
} catch {
|
|
905
|
-
// If file is corrupted, start with empty config
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
if (!config.permissions) {
|
|
910
|
-
config.permissions = {};
|
|
911
|
-
}
|
|
912
|
-
if (!config.permissions.allow) {
|
|
913
|
-
config.permissions.allow = [];
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
if (!config.permissions.allow.includes(rule)) {
|
|
917
|
-
config.permissions.allow.push(rule);
|
|
918
|
-
await atomicWriteFile(localConfigPath, JSON.stringify(config, null, 2));
|
|
919
|
-
}
|
|
920
|
-
}
|
|
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
|
-
|
|
962
|
-
/**
|
|
963
|
-
* Update the enabled state of a plugin in the specified scope
|
|
964
|
-
*/
|
|
965
|
-
async updateEnabledPlugin(
|
|
966
|
-
workdir: string,
|
|
967
|
-
scope: Scope,
|
|
968
|
-
pluginId: string,
|
|
969
|
-
enabled: boolean,
|
|
970
|
-
): Promise<void> {
|
|
971
|
-
if (scope !== "user" && !existsSync(workdir)) {
|
|
972
|
-
throw new Error(`Working directory does not exist: ${workdir}`);
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
let configPath: string;
|
|
976
|
-
if (scope === "user") {
|
|
977
|
-
configPath = getUserConfigPaths()[0]; // settings.json
|
|
978
|
-
} else if (scope === "project") {
|
|
979
|
-
configPath = getProjectConfigPaths(workdir)[1]; // settings.json
|
|
980
|
-
} else {
|
|
981
|
-
configPath = getProjectConfigPaths(workdir)[0]; // local settings.local.json
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
// Ensure directory exists
|
|
985
|
-
const configDir = path.dirname(configPath);
|
|
986
|
-
if (!existsSync(configDir)) {
|
|
987
|
-
await fs.mkdir(configDir, { recursive: true });
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
let config: WaveConfiguration = {};
|
|
991
|
-
if (existsSync(configPath)) {
|
|
992
|
-
try {
|
|
993
|
-
const content = await fs.readFile(configPath, "utf-8");
|
|
994
|
-
config = JSON.parse(content);
|
|
995
|
-
} catch {
|
|
996
|
-
// Start with empty config if file is corrupted
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
if (!config.enabledPlugins) {
|
|
1001
|
-
config.enabledPlugins = {};
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
config.enabledPlugins[pluginId] = enabled;
|
|
1005
|
-
|
|
1006
|
-
await atomicWriteFile(configPath, JSON.stringify(config, null, 2));
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
/**
|
|
1010
|
-
* Get merged marketplaces from all scopes
|
|
1011
|
-
*/
|
|
1012
|
-
getMergedMarketplaces(workdir: string): Record<string, MarketplaceConfig> {
|
|
1013
|
-
const mergedConfig = loadMergedWaveConfig(workdir);
|
|
1014
|
-
return mergedConfig?.marketplaces || {};
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
/**
|
|
1018
|
-
* Get marketplaces at a specific scope
|
|
1019
|
-
*/
|
|
1020
|
-
getScopedMarketplaces(
|
|
1021
|
-
workdir: string,
|
|
1022
|
-
scope: Scope,
|
|
1023
|
-
): Record<string, MarketplaceConfig> {
|
|
1024
|
-
let configPath: string;
|
|
1025
|
-
if (scope === "user") {
|
|
1026
|
-
configPath = getUserConfigPaths()[0];
|
|
1027
|
-
} else if (scope === "project") {
|
|
1028
|
-
configPath = getProjectConfigPaths(workdir)[1];
|
|
1029
|
-
} else {
|
|
1030
|
-
configPath = getProjectConfigPaths(workdir)[0];
|
|
1031
|
-
}
|
|
1032
|
-
const config = loadWaveConfigFromFile(configPath);
|
|
1033
|
-
return config?.marketplaces || {};
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
/**
|
|
1037
|
-
* Add a marketplace to the specified scope
|
|
1038
|
-
*/
|
|
1039
|
-
async addMarketplaceToScope(
|
|
1040
|
-
workdir: string,
|
|
1041
|
-
scope: Scope,
|
|
1042
|
-
name: string,
|
|
1043
|
-
config: MarketplaceConfig,
|
|
1044
|
-
): Promise<void> {
|
|
1045
|
-
if (scope !== "user" && !existsSync(workdir)) {
|
|
1046
|
-
throw new Error(`Working directory does not exist: ${workdir}`);
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
let configPath: string;
|
|
1050
|
-
if (scope === "user") {
|
|
1051
|
-
configPath = getUserConfigPaths()[0];
|
|
1052
|
-
} else if (scope === "project") {
|
|
1053
|
-
configPath = getProjectConfigPaths(workdir)[1];
|
|
1054
|
-
} else {
|
|
1055
|
-
configPath = getProjectConfigPaths(workdir)[0];
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
const configDir = path.dirname(configPath);
|
|
1059
|
-
if (!existsSync(configDir)) {
|
|
1060
|
-
await fs.mkdir(configDir, { recursive: true });
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
let fileConfig: WaveConfiguration = {};
|
|
1064
|
-
if (existsSync(configPath)) {
|
|
1065
|
-
try {
|
|
1066
|
-
const content = await fs.readFile(configPath, "utf-8");
|
|
1067
|
-
fileConfig = JSON.parse(content);
|
|
1068
|
-
} catch {
|
|
1069
|
-
// Start with empty config if file is corrupted
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
if (!fileConfig.marketplaces) {
|
|
1074
|
-
fileConfig.marketplaces = {};
|
|
1075
|
-
}
|
|
1076
|
-
fileConfig.marketplaces[name] = config;
|
|
1077
|
-
|
|
1078
|
-
await atomicWriteFile(configPath, JSON.stringify(fileConfig, null, 2));
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
/**
|
|
1082
|
-
* Remove a marketplace from the specified scope
|
|
1083
|
-
*/
|
|
1084
|
-
async removeMarketplaceFromScope(
|
|
1085
|
-
workdir: string,
|
|
1086
|
-
scope: Scope,
|
|
1087
|
-
name: string,
|
|
1088
|
-
): Promise<void> {
|
|
1089
|
-
if (scope !== "user" && !existsSync(workdir)) {
|
|
1090
|
-
throw new Error(`Working directory does not exist: ${workdir}`);
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
let configPath: string;
|
|
1094
|
-
if (scope === "user") {
|
|
1095
|
-
configPath = getUserConfigPaths()[0];
|
|
1096
|
-
} else if (scope === "project") {
|
|
1097
|
-
configPath = getProjectConfigPaths(workdir)[1];
|
|
1098
|
-
} else {
|
|
1099
|
-
configPath = getProjectConfigPaths(workdir)[0];
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
if (!existsSync(configPath)) {
|
|
1103
|
-
return;
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
try {
|
|
1107
|
-
const content = await fs.readFile(configPath, "utf-8");
|
|
1108
|
-
const fileConfig: WaveConfiguration = JSON.parse(content);
|
|
1109
|
-
|
|
1110
|
-
if (fileConfig.marketplaces && name in fileConfig.marketplaces) {
|
|
1111
|
-
delete fileConfig.marketplaces[name];
|
|
1112
|
-
await atomicWriteFile(configPath, JSON.stringify(fileConfig, null, 2));
|
|
1113
|
-
}
|
|
1114
|
-
} catch {
|
|
1115
|
-
// Ignore errors for corrupted or non-existent files
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
/**
|
|
1120
|
-
* Remove a plugin from the enabled plugins in the specified scope
|
|
1121
|
-
*/
|
|
1122
|
-
async removeEnabledPlugin(
|
|
1123
|
-
workdir: string,
|
|
1124
|
-
scope: Scope,
|
|
1125
|
-
pluginId: string,
|
|
1126
|
-
): Promise<void> {
|
|
1127
|
-
if (scope !== "user" && !existsSync(workdir)) {
|
|
1128
|
-
throw new Error(`Working directory does not exist: ${workdir}`);
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
let configPath: string;
|
|
1132
|
-
if (scope === "user") {
|
|
1133
|
-
configPath = getUserConfigPaths()[0]; // settings.json
|
|
1134
|
-
} else if (scope === "project") {
|
|
1135
|
-
configPath = getProjectConfigPaths(workdir)[1]; // settings.json
|
|
1136
|
-
} else {
|
|
1137
|
-
configPath = getProjectConfigPaths(workdir)[0]; // local settings.local.json
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
if (!existsSync(configPath)) {
|
|
1141
|
-
return; // Nothing to remove
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
try {
|
|
1145
|
-
const content = await fs.readFile(configPath, "utf-8");
|
|
1146
|
-
const config: WaveConfiguration = JSON.parse(content);
|
|
1147
|
-
|
|
1148
|
-
if (config.enabledPlugins && pluginId in config.enabledPlugins) {
|
|
1149
|
-
delete config.enabledPlugins[pluginId];
|
|
1150
|
-
await atomicWriteFile(configPath, JSON.stringify(config, null, 2));
|
|
1151
|
-
}
|
|
1152
|
-
} catch {
|
|
1153
|
-
// Ignore errors for corrupted or non-existent files
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
/**
|
|
1158
|
-
* Get merged enabled plugins from all scopes
|
|
1159
|
-
*/
|
|
1160
|
-
getMergedEnabledPlugins(workdir: string): Record<string, boolean> {
|
|
1161
|
-
const mergedConfig = loadMergedWaveConfig(workdir);
|
|
1162
|
-
return mergedConfig?.enabledPlugins || {};
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
/**
|
|
1166
|
-
* Load Wave configuration from a JSON file
|
|
1167
|
-
* Supports both hooks and environment variables with proper validation
|
|
1168
|
-
*/
|
|
1169
|
-
loadWaveConfigFromFile(filePath: string): WaveConfiguration | null {
|
|
1170
|
-
return loadWaveConfigFromFile(filePath);
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
// =============================================================================
|
|
1174
|
-
// Extracted Configuration Functions
|
|
1175
|
-
// =============================================================================
|
|
1176
|
-
|
|
1177
|
-
/**
|
|
1178
|
-
* Validate environment variable configuration
|
|
1179
|
-
*/
|
|
1180
|
-
export function validateEnvironmentConfig(
|
|
1181
|
-
env: unknown,
|
|
1182
|
-
configPath?: string,
|
|
1183
|
-
): EnvironmentValidationResult {
|
|
1184
|
-
const result: EnvironmentValidationResult = {
|
|
1185
|
-
isValid: true,
|
|
1186
|
-
errors: [],
|
|
1187
|
-
warnings: [],
|
|
1188
|
-
};
|
|
1189
|
-
|
|
1190
|
-
// Check if env is defined
|
|
1191
|
-
if (env === undefined || env === null) {
|
|
1192
|
-
return result; // undefined/null env is valid (means no env vars)
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
// Validate that env is a Record<string, string>
|
|
1196
|
-
if (!isValidEnvironmentVars(env)) {
|
|
1197
|
-
result.isValid = false;
|
|
1198
|
-
result.errors.push(
|
|
1199
|
-
`Invalid env field format${configPath ? ` in ${configPath}` : ""}. Environment variables must be a Record<string, string>.`,
|
|
1200
|
-
);
|
|
1201
|
-
return result;
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
// Additional validation for environment variable names
|
|
1205
|
-
const envVars = env as Record<string, string>;
|
|
1206
|
-
for (const [key, value] of Object.entries(envVars)) {
|
|
1207
|
-
// Check for valid environment variable naming convention
|
|
1208
|
-
if (!/^[A-Z_][A-Z0-9_]*$/i.test(key)) {
|
|
1209
|
-
result.warnings.push(
|
|
1210
|
-
`Environment variable '${key}' does not follow standard naming convention (alphanumeric and underscores only).`,
|
|
1211
|
-
);
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
// Check for empty values
|
|
1215
|
-
if (value === "") {
|
|
1216
|
-
result.warnings.push(`Environment variable '${key}' has an empty value.`);
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
// Check for reserved variable names that might cause conflicts
|
|
1220
|
-
const reservedNames = [
|
|
1221
|
-
"PATH",
|
|
1222
|
-
"HOME",
|
|
1223
|
-
"USER",
|
|
1224
|
-
"PWD",
|
|
1225
|
-
"SHELL",
|
|
1226
|
-
"TERM",
|
|
1227
|
-
"NODE_ENV",
|
|
1228
|
-
];
|
|
1229
|
-
if (reservedNames.includes(key.toUpperCase())) {
|
|
1230
|
-
result.warnings.push(
|
|
1231
|
-
`Environment variable '${key}' overrides a system variable, which may cause unexpected behavior.`,
|
|
1232
|
-
);
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
return result;
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
/**
|
|
1240
|
-
* Merge environment configurations with project taking precedence over user
|
|
1241
|
-
*/
|
|
1242
|
-
export function mergeEnvironmentConfig(
|
|
1243
|
-
userEnv: Record<string, string> | undefined,
|
|
1244
|
-
projectEnv: Record<string, string> | undefined,
|
|
1245
|
-
options: EnvironmentMergeOptions = {},
|
|
1246
|
-
): MergedEnvironmentContext {
|
|
1247
|
-
const userVars = userEnv || {};
|
|
1248
|
-
const projectVars = projectEnv || {};
|
|
1249
|
-
const mergedVars: Record<string, string> = {};
|
|
1250
|
-
const conflicts: MergedEnvironmentContext["conflicts"] = [];
|
|
1251
|
-
|
|
1252
|
-
// Start with user environment variables
|
|
1253
|
-
Object.assign(mergedVars, userVars);
|
|
1254
|
-
|
|
1255
|
-
// Override with project environment variables and track conflicts
|
|
1256
|
-
for (const [key, projectValue] of Object.entries(projectVars)) {
|
|
1257
|
-
const userValue = userVars[key];
|
|
1258
|
-
|
|
1259
|
-
if (
|
|
1260
|
-
userValue !== undefined &&
|
|
1261
|
-
userValue !== projectValue &&
|
|
1262
|
-
options.includeConflictWarnings !== false
|
|
1263
|
-
) {
|
|
1264
|
-
// Conflict detected - project value takes precedence
|
|
1265
|
-
conflicts.push({
|
|
1266
|
-
key,
|
|
1267
|
-
userValue,
|
|
1268
|
-
projectValue,
|
|
1269
|
-
resolvedValue: projectValue,
|
|
1270
|
-
});
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
mergedVars[key] = projectValue;
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
return {
|
|
1277
|
-
userVars,
|
|
1278
|
-
projectVars,
|
|
1279
|
-
mergedVars,
|
|
1280
|
-
conflicts,
|
|
1281
|
-
};
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
/**
|
|
1285
|
-
* Load Wave configuration from a JSON file
|
|
1286
|
-
* Supports both hooks and environment variables with proper validation
|
|
1287
|
-
*/
|
|
1288
|
-
export function loadWaveConfigFromFile(
|
|
1289
|
-
filePath: string,
|
|
1290
|
-
): WaveConfiguration | null {
|
|
1291
|
-
if (!existsSync(filePath)) {
|
|
1292
|
-
return null;
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
try {
|
|
1296
|
-
const content = readFileSync(filePath, "utf-8");
|
|
1297
|
-
|
|
1298
|
-
// Tolerate empty/whitespace-only files: a concurrent writer may have the
|
|
1299
|
-
// file truncated mid-write. Returning null lets callers skip this source
|
|
1300
|
-
// instead of crashing (consistent with the "file not found" path).
|
|
1301
|
-
if (content.trim() === "") {
|
|
1302
|
-
logger.warn(`Empty configuration file (likely mid-write): ${filePath}`);
|
|
1303
|
-
return null;
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
const config = JSON.parse(content) as WaveConfiguration;
|
|
1307
|
-
|
|
1308
|
-
return {
|
|
1309
|
-
hooks: config.hooks || undefined,
|
|
1310
|
-
env: config.env || undefined,
|
|
1311
|
-
permissions: config.permissions || undefined,
|
|
1312
|
-
enabledPlugins: config.enabledPlugins || undefined,
|
|
1313
|
-
language: config.language || undefined,
|
|
1314
|
-
model: config.model || undefined,
|
|
1315
|
-
autoMemoryEnabled:
|
|
1316
|
-
config.autoMemoryEnabled !== undefined
|
|
1317
|
-
? config.autoMemoryEnabled
|
|
1318
|
-
: undefined,
|
|
1319
|
-
models: config.models || undefined,
|
|
1320
|
-
marketplaces: config.marketplaces || undefined,
|
|
1321
|
-
worktree: config.worktree || undefined,
|
|
1322
|
-
};
|
|
1323
|
-
} catch (error) {
|
|
1324
|
-
if (error instanceof SyntaxError) {
|
|
1325
|
-
// Tolerate corrupt JSON: a concurrent writer may leave the file in a
|
|
1326
|
-
// partially-written state. Warn and return null so callers skip this
|
|
1327
|
-
// source instead of crashing.
|
|
1328
|
-
logger.warn(`Invalid JSON syntax in ${filePath}: ${error.message}`);
|
|
1329
|
-
return null;
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
// Re-throw other errors as-is
|
|
1333
|
-
throw error;
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
/**
|
|
1338
|
-
* Load and merge Wave configuration from both user and project sources
|
|
1339
|
-
* Project configuration takes precedence over user configuration
|
|
1340
|
-
* Checks .local.json files first, then falls back to .json files
|
|
1341
|
-
*/
|
|
1342
|
-
export function loadMergedWaveConfig(
|
|
1343
|
-
workdir: string,
|
|
1344
|
-
): WaveConfiguration | null {
|
|
1345
|
-
const userPaths = getUserConfigPaths(); // [json]
|
|
1346
|
-
const projectPaths = getProjectConfigPaths(workdir); // [local, json]
|
|
1347
|
-
|
|
1348
|
-
// Priority order (lowest to highest):
|
|
1349
|
-
// user settings.json -> project settings.json -> local settings.local.json
|
|
1350
|
-
const pathsToLoad = [
|
|
1351
|
-
userPaths[0], // user settings.json
|
|
1352
|
-
projectPaths[1], // project settings.json
|
|
1353
|
-
projectPaths[0], // local settings.local.json
|
|
1354
|
-
];
|
|
1355
|
-
|
|
1356
|
-
const configs: WaveConfiguration[] = [];
|
|
1357
|
-
for (const path of pathsToLoad) {
|
|
1358
|
-
const config = loadWaveConfigFromFile(path);
|
|
1359
|
-
if (config) {
|
|
1360
|
-
configs.push(config);
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
if (configs.length === 0) {
|
|
1365
|
-
return null;
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
// Merge all configurations in order
|
|
1369
|
-
const mergedConfig: WaveConfiguration = {};
|
|
1370
|
-
|
|
1371
|
-
for (const config of configs) {
|
|
1372
|
-
// Merge hooks
|
|
1373
|
-
if (config.hooks) {
|
|
1374
|
-
if (!mergedConfig.hooks) mergedConfig.hooks = {};
|
|
1375
|
-
for (const [event, eventConfigs] of Object.entries(config.hooks)) {
|
|
1376
|
-
if (!isValidHookEvent(event)) continue;
|
|
1377
|
-
if (!mergedConfig.hooks[event]) mergedConfig.hooks[event] = [];
|
|
1378
|
-
mergedConfig.hooks[event]!.push(...eventConfigs);
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
// Merge env
|
|
1383
|
-
if (config.env) {
|
|
1384
|
-
const environmentContext = mergeEnvironmentConfig(
|
|
1385
|
-
mergedConfig.env,
|
|
1386
|
-
config.env,
|
|
1387
|
-
{ includeConflictWarnings: true },
|
|
1388
|
-
);
|
|
1389
|
-
mergedConfig.env = environmentContext.mergedVars;
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
// Merge permissions
|
|
1393
|
-
if (config.permissions) {
|
|
1394
|
-
if (!mergedConfig.permissions) mergedConfig.permissions = {};
|
|
1395
|
-
|
|
1396
|
-
// Merge allow rules
|
|
1397
|
-
if (config.permissions.allow) {
|
|
1398
|
-
if (!mergedConfig.permissions.allow)
|
|
1399
|
-
mergedConfig.permissions.allow = [];
|
|
1400
|
-
mergedConfig.permissions.allow = [
|
|
1401
|
-
...new Set([
|
|
1402
|
-
...mergedConfig.permissions.allow,
|
|
1403
|
-
...config.permissions.allow,
|
|
1404
|
-
]),
|
|
1405
|
-
];
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
// Merge deny rules
|
|
1409
|
-
if (config.permissions.deny) {
|
|
1410
|
-
if (!mergedConfig.permissions.deny) mergedConfig.permissions.deny = [];
|
|
1411
|
-
mergedConfig.permissions.deny = [
|
|
1412
|
-
...new Set([
|
|
1413
|
-
...mergedConfig.permissions.deny,
|
|
1414
|
-
...config.permissions.deny,
|
|
1415
|
-
]),
|
|
1416
|
-
];
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
// Merge permissionMode (last one wins)
|
|
1420
|
-
if (config.permissions.permissionMode !== undefined) {
|
|
1421
|
-
mergedConfig.permissions.permissionMode =
|
|
1422
|
-
config.permissions.permissionMode;
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
// Merge additionalDirectories
|
|
1426
|
-
if (config.permissions.additionalDirectories) {
|
|
1427
|
-
if (!mergedConfig.permissions.additionalDirectories)
|
|
1428
|
-
mergedConfig.permissions.additionalDirectories = [];
|
|
1429
|
-
mergedConfig.permissions.additionalDirectories = [
|
|
1430
|
-
...new Set([
|
|
1431
|
-
...mergedConfig.permissions.additionalDirectories,
|
|
1432
|
-
...config.permissions.additionalDirectories,
|
|
1433
|
-
]),
|
|
1434
|
-
];
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
// Merge enabledPlugins
|
|
1439
|
-
if (config.enabledPlugins) {
|
|
1440
|
-
if (!mergedConfig.enabledPlugins) mergedConfig.enabledPlugins = {};
|
|
1441
|
-
Object.assign(mergedConfig.enabledPlugins, config.enabledPlugins);
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
// Merge language (last one wins)
|
|
1445
|
-
if (config.language !== undefined) {
|
|
1446
|
-
mergedConfig.language = config.language;
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
// Merge model (last one wins)
|
|
1450
|
-
if (config.model !== undefined) {
|
|
1451
|
-
mergedConfig.model = config.model;
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
// Merge autoMemoryEnabled (last one wins)
|
|
1455
|
-
if (config.autoMemoryEnabled !== undefined) {
|
|
1456
|
-
mergedConfig.autoMemoryEnabled = config.autoMemoryEnabled;
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
// Merge autoMemoryFrequency (last one wins)
|
|
1460
|
-
if (config.autoMemoryFrequency !== undefined) {
|
|
1461
|
-
mergedConfig.autoMemoryFrequency = config.autoMemoryFrequency;
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
// Merge marketplaces (last one wins for same key)
|
|
1465
|
-
if (config.marketplaces) {
|
|
1466
|
-
if (!mergedConfig.marketplaces) mergedConfig.marketplaces = {};
|
|
1467
|
-
Object.assign(mergedConfig.marketplaces, config.marketplaces);
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
// Merge worktree (last one wins)
|
|
1471
|
-
if (config.worktree !== undefined) {
|
|
1472
|
-
mergedConfig.worktree = config.worktree;
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
// Merge models
|
|
1476
|
-
if (config.models) {
|
|
1477
|
-
if (!mergedConfig.models) mergedConfig.models = {};
|
|
1478
|
-
for (const [modelName, modelConfig] of Object.entries(config.models)) {
|
|
1479
|
-
if (!mergedConfig.models[modelName]) {
|
|
1480
|
-
mergedConfig.models[modelName] = {};
|
|
1481
|
-
}
|
|
1482
|
-
Object.assign(mergedConfig.models[modelName], modelConfig);
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
return {
|
|
1488
|
-
hooks:
|
|
1489
|
-
mergedConfig.hooks && Object.keys(mergedConfig.hooks).length > 0
|
|
1490
|
-
? mergedConfig.hooks
|
|
1491
|
-
: undefined,
|
|
1492
|
-
env:
|
|
1493
|
-
mergedConfig.env && Object.keys(mergedConfig.env).length > 0
|
|
1494
|
-
? mergedConfig.env
|
|
1495
|
-
: undefined,
|
|
1496
|
-
permissions:
|
|
1497
|
-
mergedConfig.permissions &&
|
|
1498
|
-
Object.keys(mergedConfig.permissions).length > 0
|
|
1499
|
-
? mergedConfig.permissions
|
|
1500
|
-
: undefined,
|
|
1501
|
-
enabledPlugins:
|
|
1502
|
-
mergedConfig.enabledPlugins &&
|
|
1503
|
-
Object.keys(mergedConfig.enabledPlugins).length > 0
|
|
1504
|
-
? mergedConfig.enabledPlugins
|
|
1505
|
-
: undefined,
|
|
1506
|
-
language: mergedConfig.language,
|
|
1507
|
-
model: mergedConfig.model,
|
|
1508
|
-
autoMemoryEnabled: mergedConfig.autoMemoryEnabled,
|
|
1509
|
-
marketplaces:
|
|
1510
|
-
mergedConfig.marketplaces &&
|
|
1511
|
-
Object.keys(mergedConfig.marketplaces).length > 0
|
|
1512
|
-
? mergedConfig.marketplaces
|
|
1513
|
-
: undefined,
|
|
1514
|
-
models:
|
|
1515
|
-
mergedConfig.models && Object.keys(mergedConfig.models).length > 0
|
|
1516
|
-
? mergedConfig.models
|
|
1517
|
-
: undefined,
|
|
1518
|
-
worktree: mergedConfig.worktree,
|
|
1519
|
-
};
|
|
1520
|
-
}
|