wave-agent-sdk 1.1.5 → 1.3.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.
Files changed (210) hide show
  1. package/dist/agent.d.ts +128 -28
  2. package/dist/agent.js +201 -49
  3. package/dist/builtin/index.js +2 -0
  4. package/dist/builtin/plugins.js +11 -20
  5. package/dist/builtin/skills/settings.js +7 -20
  6. package/dist/builtin/skills/wave-daemon.d.ts +1 -0
  7. package/dist/builtin/skills/wave-daemon.js +194 -0
  8. package/dist/constants/images.d.ts +26 -0
  9. package/dist/constants/images.js +26 -0
  10. package/dist/constants/index.d.ts +16 -0
  11. package/dist/constants/index.js +16 -0
  12. package/dist/constants/memory.d.ts +26 -0
  13. package/dist/constants/memory.js +34 -0
  14. package/dist/constants/messages.d.ts +11 -0
  15. package/dist/constants/messages.js +11 -0
  16. package/dist/constants/plugins.d.ts +8 -0
  17. package/dist/constants/plugins.js +8 -0
  18. package/dist/constants/tools.d.ts +1 -0
  19. package/dist/constants/tools.js +1 -0
  20. package/dist/core/plugin.d.ts +54 -10
  21. package/dist/core/plugin.js +137 -23
  22. package/dist/core/session.d.ts +1 -1
  23. package/dist/core/session.js +1 -1
  24. package/dist/exec/catalog.d.ts +140 -0
  25. package/dist/exec/catalog.js +470 -0
  26. package/dist/exec/catalogAnnouncement.d.ts +89 -0
  27. package/dist/exec/catalogAnnouncement.js +293 -0
  28. package/dist/exec/constants.d.ts +51 -0
  29. package/dist/exec/constants.js +51 -0
  30. package/dist/exec/execRuntime.d.ts +55 -0
  31. package/dist/exec/execRuntime.js +217 -0
  32. package/dist/exec/workerSource.d.ts +28 -0
  33. package/dist/exec/workerSource.js +299 -0
  34. package/dist/host/index.d.ts +23 -0
  35. package/dist/host/index.js +23 -0
  36. package/dist/index.d.ts +7 -1
  37. package/dist/index.js +8 -1
  38. package/dist/managers/MemoryRuleManager.d.ts +6 -0
  39. package/dist/managers/MemoryRuleManager.js +12 -0
  40. package/dist/managers/aiManager.d.ts +35 -25
  41. package/dist/managers/aiManager.js +204 -202
  42. package/dist/managers/backgroundTaskManager.js +14 -0
  43. package/dist/managers/bashModeManager.d.ts +33 -0
  44. package/dist/managers/bashModeManager.js +110 -0
  45. package/dist/managers/hookManager.d.ts +18 -0
  46. package/dist/managers/hookManager.js +37 -3
  47. package/dist/managers/liveConfigManager.d.ts +33 -0
  48. package/dist/managers/liveConfigManager.js +106 -11
  49. package/dist/managers/lspManager.d.ts +9 -0
  50. package/dist/managers/lspManager.js +47 -18
  51. package/dist/managers/mcpManager.d.ts +68 -10
  52. package/dist/managers/mcpManager.js +265 -15
  53. package/dist/managers/messageManager.d.ts +60 -18
  54. package/dist/managers/messageManager.js +170 -81
  55. package/dist/managers/permissionManager.d.ts +69 -0
  56. package/dist/managers/permissionManager.js +221 -78
  57. package/dist/managers/planManager.d.ts +9 -0
  58. package/dist/managers/planManager.js +19 -1
  59. package/dist/managers/pluginManager.d.ts +46 -2
  60. package/dist/managers/pluginManager.js +117 -11
  61. package/dist/managers/pluginScopeManager.d.ts +15 -2
  62. package/dist/managers/pluginScopeManager.js +20 -1
  63. package/dist/managers/skillManager.d.ts +50 -0
  64. package/dist/managers/skillManager.js +166 -12
  65. package/dist/managers/slashCommandManager.d.ts +10 -0
  66. package/dist/managers/slashCommandManager.js +44 -31
  67. package/dist/managers/subagentManager.d.ts +15 -0
  68. package/dist/managers/subagentManager.js +81 -9
  69. package/dist/managers/toolManager.d.ts +29 -3
  70. package/dist/managers/toolManager.js +87 -13
  71. package/dist/managers/workflowManager.js +6 -0
  72. package/dist/prompts/autoMemory.d.ts +9 -0
  73. package/dist/prompts/autoMemory.js +30 -31
  74. package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
  75. package/dist/prompts/autoMemoryExtraction.js +8 -111
  76. package/dist/prompts/index.d.ts +0 -1
  77. package/dist/prompts/index.js +0 -4
  78. package/dist/prompts/memoryTypes.d.ts +63 -0
  79. package/dist/prompts/memoryTypes.js +191 -0
  80. package/dist/services/GitService.d.ts +7 -0
  81. package/dist/services/GitService.js +23 -0
  82. package/dist/services/MarketplaceService.d.ts +101 -17
  83. package/dist/services/MarketplaceService.js +323 -102
  84. package/dist/services/artifactContent.d.ts +84 -0
  85. package/dist/services/artifactContent.js +204 -0
  86. package/dist/services/artifactSession.d.ts +6 -0
  87. package/dist/services/artifactSession.js +17 -0
  88. package/dist/services/autoMemoryService.js +5 -13
  89. package/dist/services/configurationService.d.ts +92 -9
  90. package/dist/services/configurationService.js +246 -64
  91. package/dist/services/contentSummarizer.d.ts +15 -0
  92. package/dist/services/contentSummarizer.js +45 -0
  93. package/dist/services/execAvailability.d.ts +9 -0
  94. package/dist/services/execAvailability.js +32 -0
  95. package/dist/services/fileWatcher.js +61 -6
  96. package/dist/services/initializationService.js +21 -17
  97. package/dist/services/interactionService.d.ts +9 -1
  98. package/dist/services/interactionService.js +28 -8
  99. package/dist/services/jsonlHandler.d.ts +98 -0
  100. package/dist/services/jsonlHandler.js +250 -12
  101. package/dist/services/memory.d.ts +17 -1
  102. package/dist/services/memory.js +44 -7
  103. package/dist/services/officialMarketplaceMirror.d.ts +85 -0
  104. package/dist/services/officialMarketplaceMirror.js +290 -0
  105. package/dist/services/pluginLoader.d.ts +12 -4
  106. package/dist/services/pluginLoader.js +38 -7
  107. package/dist/services/remoteSettingsService.js +20 -6
  108. package/dist/services/session.d.ts +74 -0
  109. package/dist/services/session.js +174 -16
  110. package/dist/services/sessionEntries.d.ts +2 -0
  111. package/dist/services/sessionEntries.js +20 -0
  112. package/dist/services/worktreeHooks.js +6 -1
  113. package/dist/stdio/index.d.ts +12 -0
  114. package/dist/stdio/index.js +12 -0
  115. package/dist/stdio/notificationRouter.d.ts +38 -0
  116. package/dist/stdio/notificationRouter.js +97 -0
  117. package/dist/stdio/rpcClient.d.ts +18 -0
  118. package/dist/stdio/rpcClient.js +10 -0
  119. package/dist/stdio/stdioAgent.d.ts +229 -0
  120. package/dist/stdio/stdioAgent.js +360 -0
  121. package/dist/tools/artifactTool.js +406 -273
  122. package/dist/tools/bashTool.js +10 -6
  123. package/dist/tools/editTool.js +6 -3
  124. package/dist/tools/execTool.d.ts +2 -0
  125. package/dist/tools/execTool.js +165 -0
  126. package/dist/tools/exitPlanMode.js +10 -2
  127. package/dist/tools/grepTool.js +7 -1
  128. package/dist/tools/readTool.js +30 -2
  129. package/dist/tools/types.d.ts +34 -8
  130. package/dist/tools/webFetchTool.js +15 -166
  131. package/dist/tools/workflowTool.js +40 -8
  132. package/dist/tools/writeTool.js +6 -3
  133. package/dist/types/agent.d.ts +24 -1
  134. package/dist/types/commands.d.ts +7 -0
  135. package/dist/types/configuration.d.ts +45 -2
  136. package/dist/types/hooks.d.ts +1 -0
  137. package/dist/types/hooks.js +19 -0
  138. package/dist/types/marketplace.d.ts +40 -2
  139. package/dist/types/mcp.d.ts +42 -0
  140. package/dist/types/messaging.d.ts +1 -8
  141. package/dist/types/permissions.d.ts +22 -0
  142. package/dist/types/permissions.js +17 -0
  143. package/dist/types/plugins.d.ts +26 -2
  144. package/dist/types/skills.d.ts +26 -0
  145. package/dist/utils/bashParser.d.ts +17 -0
  146. package/dist/utils/bashParser.js +72 -0
  147. package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
  148. package/dist/utils/bashStructure/bashLexer.js +676 -0
  149. package/dist/utils/bashStructure/bashParser.d.ts +144 -0
  150. package/dist/utils/bashStructure/bashParser.js +606 -0
  151. package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
  152. package/dist/utils/bashStructure/bashSemantics.js +477 -0
  153. package/dist/utils/bashStructure/index.d.ts +26 -0
  154. package/dist/utils/bashStructure/index.js +27 -0
  155. package/dist/utils/bashStructure/types.d.ts +62 -0
  156. package/dist/utils/bashStructure/types.js +47 -0
  157. package/dist/utils/constants.d.ts +10 -0
  158. package/dist/utils/constants.js +10 -0
  159. package/dist/utils/containerSetup.js +48 -6
  160. package/dist/utils/convertMessagesForAPI.d.ts +7 -1
  161. package/dist/utils/convertMessagesForAPI.js +64 -14
  162. package/dist/utils/fileChangeReminder.d.ts +20 -0
  163. package/dist/utils/fileChangeReminder.js +153 -0
  164. package/dist/utils/fileSearch.js +4 -3
  165. package/dist/utils/fileUtils.d.ts +44 -0
  166. package/dist/utils/fileUtils.js +118 -0
  167. package/dist/utils/frontmatterYaml.d.ts +33 -0
  168. package/dist/utils/frontmatterYaml.js +192 -0
  169. package/dist/utils/imageBudget.d.ts +85 -0
  170. package/dist/utils/imageBudget.js +109 -0
  171. package/dist/utils/imageDimensions.d.ts +83 -0
  172. package/dist/utils/imageDimensions.js +232 -0
  173. package/dist/utils/imageProcessor.d.ts +66 -0
  174. package/dist/utils/imageProcessor.js +84 -0
  175. package/dist/utils/imageRewrite.d.ts +29 -0
  176. package/dist/utils/imageRewrite.js +251 -0
  177. package/dist/utils/markdownParser.d.ts +5 -1
  178. package/dist/utils/markdownParser.js +9 -51
  179. package/dist/utils/mcpInstructions.d.ts +61 -0
  180. package/dist/utils/mcpInstructions.js +126 -0
  181. package/dist/utils/mcpUtils.d.ts +7 -0
  182. package/dist/utils/mcpUtils.js +11 -2
  183. package/dist/utils/memoryAge.d.ts +32 -0
  184. package/dist/utils/memoryAge.js +47 -0
  185. package/dist/utils/memoryEntrypoint.d.ts +20 -0
  186. package/dist/utils/memoryEntrypoint.js +49 -0
  187. package/dist/utils/memoryIndex.d.ts +30 -0
  188. package/dist/utils/memoryIndex.js +76 -0
  189. package/dist/utils/messageOperations.d.ts +6 -20
  190. package/dist/utils/messageOperations.js +40 -91
  191. package/dist/utils/nestedMemory.d.ts +22 -0
  192. package/dist/utils/nestedMemory.js +61 -0
  193. package/dist/utils/npmTarball.d.ts +19 -0
  194. package/dist/utils/npmTarball.js +92 -0
  195. package/dist/utils/pluginSource.d.ts +37 -0
  196. package/dist/utils/pluginSource.js +73 -0
  197. package/dist/utils/ripgrep.d.ts +18 -4
  198. package/dist/utils/ripgrep.js +56 -4
  199. package/dist/utils/runtimeDeps.d.ts +35 -0
  200. package/dist/utils/runtimeDeps.js +426 -0
  201. package/dist/utils/skillParser.js +22 -52
  202. package/dist/utils/subagentParser.js +48 -45
  203. package/dist/utils/tokenCalculation.js +0 -8
  204. package/dist/utils/userSettings.d.ts +90 -0
  205. package/dist/utils/userSettings.js +291 -0
  206. package/dist/utils/worktreeUtils.d.ts +2 -1
  207. package/dist/utils/worktreeUtils.js +64 -34
  208. package/package.json +12 -4
  209. package/dist/managers/bangManager.d.ts +0 -26
  210. package/dist/managers/bangManager.js +0 -78
@@ -11,7 +11,7 @@ import { isValidHookEvent } from "../types/hooks.js";
11
11
  import { logger } from "../utils/globalLogger.js";
12
12
  import { getAllConfigPaths, getExistingConfigPaths, getUserConfigPaths, getProjectConfigPaths, } from "../utils/configPaths.js";
13
13
  import { isValidEnvironmentVars, } from "../types/environment.js";
14
- import { DEFAULT_WAVE_MAX_INPUT_TOKENS, DEFAULT_WAVE_MAX_OUTPUT_TOKENS, DEFAULT_SERVER_URL, } from "../utils/constants.js";
14
+ import { DEFAULT_WAVE_MAX_INPUT_TOKENS, DEFAULT_WAVE_MAX_OUTPUT_TOKENS, DEFAULT_SERVER_URL, DEFAULT_LANGUAGE, } from "../utils/constants.js";
15
15
  import { parseCustomHeaders } from "../utils/stringUtils.js";
16
16
  import { getRemoteSettingsSync, mergeRemoteSettings, } from "./remoteSettingsService.js";
17
17
  import { createAuthAwareFetch } from "./authService.js";
@@ -119,8 +119,8 @@ export function validateConfigurationObject(config) {
119
119
  result.errors.push("All permission deny rules must be strings");
120
120
  }
121
121
  }
122
- // Validate permissionMode if present
123
- if (config.permissions.permissionMode !== undefined) {
122
+ // Validate defaultMode if present
123
+ if (config.permissions.defaultMode !== undefined) {
124
124
  const validModes = [
125
125
  "default",
126
126
  "bypassPermissions",
@@ -128,9 +128,9 @@ export function validateConfigurationObject(config) {
128
128
  "plan",
129
129
  "dontAsk",
130
130
  ];
131
- if (!validModes.includes(config.permissions.permissionMode)) {
131
+ if (!validModes.includes(config.permissions.defaultMode)) {
132
132
  result.isValid = false;
133
- result.errors.push(`Invalid permissionMode: "${config.permissions.permissionMode}". Must be one of: ${validModes.join(", ")}`);
133
+ result.errors.push(`Invalid defaultMode: "${config.permissions.defaultMode}". Must be one of: ${validModes.join(", ")}`);
134
134
  }
135
135
  }
136
136
  }
@@ -199,22 +199,38 @@ export class ConfigurationService {
199
199
  this._configuredEnvKeys = new Set();
200
200
  // Per-session environment snapshot: settings.json `env` is stored here (NOT
201
201
  // written to process.env) so multiple sessions in one `wave --stdio` process
202
- // don't cross-pollute. Resolve methods read `this.envSnapshot ?? process.env`.
202
+ // don't cross-pollute. Resolve methods read `this.liveEnvSnapshot ?? process.env`.
203
203
  this.envSnapshot = {};
204
204
  }
205
+ /**
206
+ * Register the turn-snapshot source (see {@link TurnConfigurationSnapshot}).
207
+ */
208
+ setTurnSnapshotSource(source) {
209
+ this.turnSnapshotSource = source;
210
+ }
205
211
  /**
206
212
  * Set agent options for configuration resolution
207
213
  */
208
214
  setOptions(options) {
209
215
  this.options = options;
210
216
  }
217
+ /** Configuration the current turn reads from (live configuration outside a turn). */
218
+ get liveConfiguration() {
219
+ const snapshot = this.turnSnapshotSource?.();
220
+ return snapshot ? snapshot.configuration : this.currentConfiguration;
221
+ }
222
+ /** Environment snapshot the current turn reads from. */
223
+ get liveEnvSnapshot() {
224
+ const snapshot = this.turnSnapshotSource?.();
225
+ return snapshot ? snapshot.env : this.envSnapshot;
226
+ }
211
227
  /**
212
228
  * Returns a copy of the per-session environment snapshot (settings.json `env`).
213
229
  * Priority over OS env; does NOT include OS env. For subprocess spawning use
214
230
  * {@link getMergedEnv} instead.
215
231
  */
216
232
  getEnvSnapshot() {
217
- return { ...this.envSnapshot };
233
+ return { ...this.liveEnvSnapshot };
218
234
  }
219
235
  /**
220
236
  * Returns OS env merged with the session snapshot (snapshot wins). Use this
@@ -222,7 +238,7 @@ export class ConfigurationService {
222
238
  * so they inherit both OS env and the session's settings env.
223
239
  */
224
240
  getMergedEnv() {
225
- return Object.fromEntries(Object.entries({ ...process.env, ...this.envSnapshot }).filter(([, v]) => v !== undefined));
241
+ return Object.fromEntries(Object.entries({ ...process.env, ...this.liveEnvSnapshot }).filter(([, v]) => v !== undefined));
226
242
  }
227
243
  // Core loading operations
228
244
  /**
@@ -420,7 +436,7 @@ export class ConfigurationService {
420
436
  }
421
437
  else {
422
438
  resolvedApiKey =
423
- this.envSnapshot.WAVE_API_KEY ?? process.env.WAVE_API_KEY;
439
+ this.liveEnvSnapshot.WAVE_API_KEY ?? process.env.WAVE_API_KEY;
424
440
  }
425
441
  // Resolve base URL: override > options > env (settings.json) > process.env
426
442
  // Note: Explicitly provided empty strings should be treated as invalid, not fall back to env
@@ -433,23 +449,23 @@ export class ConfigurationService {
433
449
  }
434
450
  else {
435
451
  resolvedBaseURL =
436
- this.envSnapshot.WAVE_BASE_URL ?? process.env.WAVE_BASE_URL;
452
+ this.liveEnvSnapshot.WAVE_BASE_URL ?? process.env.WAVE_BASE_URL;
437
453
  }
438
454
  // Fallback to process.env if still not resolved (for dynamic updates in tests)
439
455
  if (resolvedApiKey === undefined) {
440
456
  resolvedApiKey =
441
- this.envSnapshot.WAVE_API_KEY ?? process.env.WAVE_API_KEY;
457
+ this.liveEnvSnapshot.WAVE_API_KEY ?? process.env.WAVE_API_KEY;
442
458
  }
443
459
  if (!resolvedBaseURL) {
444
460
  resolvedBaseURL =
445
- this.envSnapshot.WAVE_BASE_URL ?? process.env.WAVE_BASE_URL;
461
+ this.liveEnvSnapshot.WAVE_BASE_URL ?? process.env.WAVE_BASE_URL;
446
462
  }
447
463
  // Treat empty string as not provided
448
464
  if (resolvedBaseURL?.trim() === "") {
449
465
  resolvedBaseURL = undefined;
450
466
  }
451
467
  // Resolve custom headers from environment: env (settings.json) > process.env
452
- const envCustomHeaders = this.envSnapshot.WAVE_CUSTOM_HEADERS ??
468
+ const envCustomHeaders = this.liveEnvSnapshot.WAVE_CUSTOM_HEADERS ??
453
469
  process.env.WAVE_CUSTOM_HEADERS ??
454
470
  "";
455
471
  const parsedEnvHeaders = parseCustomHeaders(envCustomHeaders);
@@ -482,16 +498,16 @@ export class ConfigurationService {
482
498
  // If admin wants hard enforcement, they set the `model` scalar field (overwrites local in mergeRemoteSettings).
483
499
  const resolvedAgentModel = model ||
484
500
  this.options.model ||
485
- this.currentConfiguration?.model ||
486
- (this.envSnapshot.WAVE_MODEL ?? process.env.WAVE_MODEL);
501
+ this.liveConfiguration?.model ||
502
+ (this.liveEnvSnapshot.WAVE_MODEL ?? process.env.WAVE_MODEL);
487
503
  // Resolve fast model: override > options > process.env (includes settings.json env)
488
504
  const resolvedFastModel = fastModel ||
489
505
  this.options.fastModel ||
490
- (this.envSnapshot.WAVE_FAST_MODEL ?? process.env.WAVE_FAST_MODEL);
506
+ (this.liveEnvSnapshot.WAVE_FAST_MODEL ?? process.env.WAVE_FAST_MODEL);
491
507
  // Resolve vision model: override > options > process.env (includes settings.json env)
492
508
  const resolvedVisionModel = visionModel ||
493
509
  this.options.visionModel ||
494
- (this.envSnapshot.WAVE_VISION_MODEL ?? process.env.WAVE_VISION_MODEL);
510
+ (this.liveEnvSnapshot.WAVE_VISION_MODEL ?? process.env.WAVE_VISION_MODEL);
495
511
  const baseConfig = {
496
512
  model: resolvedAgentModel,
497
513
  fastModel: resolvedFastModel,
@@ -500,8 +516,7 @@ export class ConfigurationService {
500
516
  };
501
517
  // Resolve fast model generation params from models[fastModel].options.
502
518
  // Set on baseConfig before the modelSpecificConfig spread so it isn't overwritten.
503
- const fastModelSource = resolvedFastModel &&
504
- this.currentConfiguration?.models?.[resolvedFastModel];
519
+ const fastModelSource = resolvedFastModel && this.liveConfiguration?.models?.[resolvedFastModel];
505
520
  if (fastModelSource && fastModelSource.options) {
506
521
  baseConfig.fastModelOptions = fastModelSource.options;
507
522
  }
@@ -514,7 +529,7 @@ export class ConfigurationService {
514
529
  }
515
530
  // Merge model-specific settings from configuration
516
531
  const modelSpecificConfig = resolvedAgentModel &&
517
- this.currentConfiguration?.models?.[resolvedAgentModel];
532
+ this.liveConfiguration?.models?.[resolvedAgentModel];
518
533
  if (modelSpecificConfig) {
519
534
  const resolved = {
520
535
  ...baseConfig,
@@ -549,14 +564,14 @@ export class ConfigurationService {
549
564
  // same chain as resolveModelConfig: caller > options > currentConfiguration > env.
550
565
  const resolvedModel = model ||
551
566
  this.options.model ||
552
- this.currentConfiguration?.model ||
553
- (this.envSnapshot.WAVE_MODEL ?? process.env.WAVE_MODEL);
554
- const modelConfig = resolvedModel && this.currentConfiguration?.models?.[resolvedModel];
567
+ this.liveConfiguration?.model ||
568
+ (this.liveEnvSnapshot.WAVE_MODEL ?? process.env.WAVE_MODEL);
569
+ const modelConfig = resolvedModel && this.liveConfiguration?.models?.[resolvedModel];
555
570
  if (modelConfig && modelConfig.maxInputTokens !== undefined) {
556
571
  return modelConfig.maxInputTokens;
557
572
  }
558
573
  // Try env (settings.json snapshot) first, then process.env
559
- const envMaxInputTokens = this.envSnapshot.WAVE_MAX_INPUT_TOKENS ??
574
+ const envMaxInputTokens = this.liveEnvSnapshot.WAVE_MAX_INPUT_TOKENS ??
560
575
  process.env.WAVE_MAX_INPUT_TOKENS;
561
576
  if (envMaxInputTokens) {
562
577
  const parsed = parseInt(envMaxInputTokens, 10);
@@ -569,9 +584,14 @@ export class ConfigurationService {
569
584
  }
570
585
  /**
571
586
  * Resolves preferred language with fallbacks
572
- * Resolution priority: override > options > settings.json > undefined
587
+ * Resolution priority: override > options > settings.json > default (`zh-CN`)
588
+ *
589
+ * 末尾的默认值保证「未设置」时也有明确生效值,且与设置页下拉的默认项一致
590
+ * (spec agent-config 边界说明「语言默认值」)。调用方不得再假定
591
+ * 「undefined = 不注入语言指令」。
592
+ *
573
593
  * @param constructorLanguage - Language override (optional)
574
- * @returns Resolved language or undefined
594
+ * @returns Resolved language (never empty)
575
595
  */
576
596
  resolveLanguage(constructorLanguage) {
577
597
  // 1. Override (highest priority)
@@ -582,29 +602,46 @@ export class ConfigurationService {
582
602
  if (this.options.language !== undefined) {
583
603
  return this.options.language;
584
604
  }
585
- // 2. settings.json (merged)
586
- if (this.currentConfiguration?.language) {
587
- return this.currentConfiguration.language;
605
+ // 3. settings.json (merged)
606
+ if (this.liveConfiguration?.language) {
607
+ return this.liveConfiguration.language;
588
608
  }
589
- return undefined;
609
+ // 4. Default
610
+ return DEFAULT_LANGUAGE;
590
611
  }
591
612
  /**
592
613
  * Resolves auto-memory enabled state with fallbacks
593
- * Resolution priority: settings.json > WAVE_DISABLE_AUTO_MEMORY > default (true)
614
+ * Resolution priority: session options (session-level override) > settings.json > WAVE_DISABLE_AUTO_MEMORY > default (true)
594
615
  * @returns Resolved auto-memory enabled state
595
616
  */
596
617
  resolveAutoMemoryEnabled() {
597
- // 1. settings.json (merged)
598
- if (this.currentConfiguration?.autoMemoryEnabled !== undefined) {
599
- return this.currentConfiguration.autoMemoryEnabled;
618
+ return this.resolveAutoMemoryEnabledFrom(this.liveConfiguration, this.liveEnvSnapshot);
619
+ }
620
+ /**
621
+ * Same resolution chain but read from the live merged configuration, ignoring
622
+ * the turn snapshot. The permission safe zone is a live mechanism (like
623
+ * permission rules, applied by LiveConfigManager on reload), so its sync must
624
+ * not be pinned to the turn-start value.
625
+ */
626
+ resolveAutoMemoryEnabledNow() {
627
+ return this.resolveAutoMemoryEnabledFrom(this.currentConfiguration, this.envSnapshot);
628
+ }
629
+ resolveAutoMemoryEnabledFrom(config, env) {
630
+ // 1. Per-session options override (session-level override semantics; user
631
+ // preferences are no longer passed this way — see core/agent-config.md)
632
+ if (this.options.autoMemoryEnabled !== undefined) {
633
+ return this.options.autoMemoryEnabled;
634
+ }
635
+ // 2. settings.json (merged)
636
+ if (config?.autoMemoryEnabled !== undefined) {
637
+ return config.autoMemoryEnabled;
600
638
  }
601
- // 2. WAVE_DISABLE_AUTO_MEMORY environment variable (settings snapshot > OS env)
602
- const disableAutoMemory = this.envSnapshot.WAVE_DISABLE_AUTO_MEMORY ??
603
- process.env.WAVE_DISABLE_AUTO_MEMORY;
639
+ // 3. WAVE_DISABLE_AUTO_MEMORY environment variable (settings snapshot > OS env)
640
+ const disableAutoMemory = env.WAVE_DISABLE_AUTO_MEMORY ?? process.env.WAVE_DISABLE_AUTO_MEMORY;
604
641
  if (disableAutoMemory === "1" || disableAutoMemory === "true") {
605
642
  return false;
606
643
  }
607
- // 3. Default (true)
644
+ // 4. Default (true)
608
645
  return true;
609
646
  }
610
647
  /**
@@ -613,7 +650,7 @@ export class ConfigurationService {
613
650
  * @returns Resolved worktree base ref
614
651
  */
615
652
  resolveWorktreeBaseRef() {
616
- const baseRef = this.currentConfiguration?.worktree?.baseRef;
653
+ const baseRef = this.liveConfiguration?.worktree?.baseRef;
617
654
  if (baseRef === "head") {
618
655
  return "head";
619
656
  }
@@ -621,16 +658,25 @@ export class ConfigurationService {
621
658
  }
622
659
  /**
623
660
  * Resolves auto-memory extraction frequency with fallbacks
624
- * Resolution priority: settings.json > WAVE_AUTO_MEMORY_FREQUENCY > default (1)
661
+ * Resolution priority: session options (程序化会话级覆盖) > settings.json > WAVE_AUTO_MEMORY_FREQUENCY > default (1)
625
662
  * @returns Resolved auto-memory extraction frequency (turns)
626
663
  */
627
664
  resolveAutoMemoryFrequency() {
628
- // 1. settings.json (merged)
629
- if (this.currentConfiguration?.autoMemoryFrequency !== undefined) {
630
- return this.currentConfiguration.autoMemoryFrequency;
665
+ // 1. Per-session options override (Agent.create → setOptions 的程序化会话级
666
+ // 覆盖;三端设置页的值不走这里——它们落用户级 settings.json,由实时重载
667
+ // 在下一轮生效,见 spec core/agent-config.md「设置实时重载」)。
668
+ // Only positive values are honored so a malformed 0/negative never
669
+ // degenerates into per-turn extraction.
670
+ if (this.options.autoMemoryFrequency !== undefined &&
671
+ this.options.autoMemoryFrequency > 0) {
672
+ return this.options.autoMemoryFrequency;
673
+ }
674
+ // 2. settings.json (merged)
675
+ if (this.liveConfiguration?.autoMemoryFrequency !== undefined) {
676
+ return this.liveConfiguration.autoMemoryFrequency;
631
677
  }
632
- // 2. WAVE_AUTO_MEMORY_FREQUENCY environment variable (settings snapshot > OS env)
633
- const envFrequency = this.envSnapshot.WAVE_AUTO_MEMORY_FREQUENCY ??
678
+ // 3. WAVE_AUTO_MEMORY_FREQUENCY environment variable (settings snapshot > OS env)
679
+ const envFrequency = this.liveEnvSnapshot.WAVE_AUTO_MEMORY_FREQUENCY ??
634
680
  process.env.WAVE_AUTO_MEMORY_FREQUENCY;
635
681
  if (envFrequency) {
636
682
  const parsed = parseInt(envFrequency, 10);
@@ -638,7 +684,7 @@ export class ConfigurationService {
638
684
  return parsed;
639
685
  }
640
686
  }
641
- // 3. Default (1)
687
+ // 4. Default (1)
642
688
  return 1;
643
689
  }
644
690
  /**
@@ -657,7 +703,7 @@ export class ConfigurationService {
657
703
  return this.options.maxTokens;
658
704
  }
659
705
  // Try env (settings.json snapshot) first, then process.env
660
- const envMaxOutputTokens = this.envSnapshot.WAVE_MAX_OUTPUT_TOKENS ??
706
+ const envMaxOutputTokens = this.liveEnvSnapshot.WAVE_MAX_OUTPUT_TOKENS ??
661
707
  process.env.WAVE_MAX_OUTPUT_TOKENS;
662
708
  if (envMaxOutputTokens) {
663
709
  const parsed = parseInt(envMaxOutputTokens, 10);
@@ -669,11 +715,20 @@ export class ConfigurationService {
669
715
  return DEFAULT_WAVE_MAX_OUTPUT_TOKENS;
670
716
  }
671
717
  /**
672
- * Set the active model in the session
718
+ * Set the active model in the session.
719
+ *
720
+ * Persisting to `~/.wave/settings.json` is best-effort: the in-memory model
721
+ * applies immediately, and a failed write (e.g. a locked file / rename EPERM
722
+ * on Windows) is logged instead of escaping as an unhandled rejection.
673
723
  */
674
- setModel(model) {
724
+ async setModel(model) {
675
725
  this.options.model = model;
676
- this.persistModelToSettings(model);
726
+ try {
727
+ await this.persistModelToSettings(model);
728
+ }
729
+ catch (error) {
730
+ logger.warn(`Failed to persist model to settings: ${error}`);
731
+ }
677
732
  }
678
733
  async persistModelToSettings(model) {
679
734
  const configPath = getUserConfigPaths()[0]; // ~/.wave/settings.json
@@ -701,17 +756,17 @@ export class ConfigurationService {
701
756
  const models = new Set();
702
757
  // Add current model from options or environment (settings snapshot > OS env)
703
758
  const currentModel = this.options.model ||
704
- (this.envSnapshot.WAVE_MODEL ?? process.env.WAVE_MODEL);
759
+ (this.liveEnvSnapshot.WAVE_MODEL ?? process.env.WAVE_MODEL);
705
760
  if (currentModel) {
706
761
  models.add(currentModel);
707
762
  }
708
763
  // Persisted model from settings (includes remote-merged)
709
- if (this.currentConfiguration?.model) {
710
- models.add(this.currentConfiguration.model);
764
+ if (this.liveConfiguration?.model) {
765
+ models.add(this.liveConfiguration.model);
711
766
  }
712
767
  // Add models from merged configuration
713
- if (this.currentConfiguration?.models) {
714
- Object.keys(this.currentConfiguration.models).forEach((model) => {
768
+ if (this.liveConfiguration?.models) {
769
+ Object.keys(this.liveConfiguration.models).forEach((model) => {
715
770
  models.add(model);
716
771
  });
717
772
  }
@@ -721,7 +776,7 @@ export class ConfigurationService {
721
776
  * Get the telemetry config from the loaded settings.
722
777
  */
723
778
  resolveTelemetryConfig() {
724
- return this.currentConfiguration?.monitoring?.telemetry;
779
+ return this.liveConfiguration?.monitoring?.telemetry;
725
780
  }
726
781
  /**
727
782
  * Resolve all configuration file paths
@@ -841,11 +896,103 @@ export class ConfigurationService {
841
896
  await atomicWriteFile(configPath, JSON.stringify(config, null, 2));
842
897
  }
843
898
  /**
844
- * Get merged marketplaces from all scopes
899
+ * Resolve the settings.json path for a hook scope.
900
+ * user → ~/.wave/settings.json; project → <workdir>/.wave/settings.json
901
+ */
902
+ resolveHookConfigPath(workdir, scope) {
903
+ const configPath = scope === "user"
904
+ ? getUserConfigPaths()[0]
905
+ : getProjectConfigPaths(workdir)[1];
906
+ return { configPath, configDir: path.dirname(configPath) };
907
+ }
908
+ /**
909
+ * Absolute path of the settings.json that holds hooks for `scope`
910
+ * (user → ~/.wave/settings.json, project → <workdir>/.wave/settings.json).
911
+ * Hosts surface it to the GUI so editors / file panels can open the file —
912
+ * they cannot expand a `~` prefix, so the path must already be absolute.
913
+ */
914
+ getHookConfigPath(workdir, scope) {
915
+ return this.resolveHookConfigPath(workdir, scope).configPath;
916
+ }
917
+ /**
918
+ * Get hook configurations at a specific scope (user or project settings.json)
919
+ */
920
+ async getHooksByScope(workdir, scope) {
921
+ if (scope !== "user" && !existsSync(workdir)) {
922
+ return {};
923
+ }
924
+ const { configPath } = this.resolveHookConfigPath(workdir, scope);
925
+ if (!existsSync(configPath)) {
926
+ return {};
927
+ }
928
+ try {
929
+ const content = await fs.readFile(configPath, "utf-8");
930
+ const config = JSON.parse(content);
931
+ return config.hooks || {};
932
+ }
933
+ catch {
934
+ return {};
935
+ }
936
+ }
937
+ /**
938
+ * Delete a hook at a specific scope, identified by hookName of the form
939
+ * `Event:Matcher`. Removes the matching entry from the scope's settings.json
940
+ * hooks block.
941
+ */
942
+ async deleteHook(workdir, scope, hookName) {
943
+ if (scope !== "user" && !existsSync(workdir)) {
944
+ throw new Error(`Working directory does not exist: ${workdir}`);
945
+ }
946
+ const { configPath, configDir } = this.resolveHookConfigPath(workdir, scope);
947
+ if (!existsSync(configDir)) {
948
+ await fs.mkdir(configDir, { recursive: true });
949
+ }
950
+ let config = {};
951
+ if (existsSync(configPath)) {
952
+ try {
953
+ const content = await fs.readFile(configPath, "utf-8");
954
+ config = JSON.parse(content);
955
+ }
956
+ catch {
957
+ // Start with empty config if file is corrupted
958
+ }
959
+ }
960
+ const hooks = config.hooks || {};
961
+ const { event, matcher } = parseHookName(hookName);
962
+ if (!isValidHookEvent(event)) {
963
+ throw new Error(`Invalid hook event: ${event}`);
964
+ }
965
+ const eventConfigs = hooks[event];
966
+ if (eventConfigs) {
967
+ const remaining = eventConfigs.filter((cfg) => (cfg.matcher || "") !== (matcher || ""));
968
+ if (remaining.length > 0) {
969
+ hooks[event] = remaining;
970
+ }
971
+ else {
972
+ delete hooks[event];
973
+ }
974
+ config.hooks = hooks;
975
+ await atomicWriteFile(configPath, JSON.stringify(config, null, 2));
976
+ }
977
+ }
978
+ /**
979
+ * Get merged marketplaces from all scopes, with admin-managed (remote) entries
980
+ * taking precedence per key (spec enterprise server-managed-config「托管配置
981
+ * 下发插件市场与启用列表」场景 5).
845
982
  */
846
983
  getMergedMarketplaces(workdir) {
847
984
  const mergedConfig = loadMergedWaveConfig(workdir);
848
- return mergedConfig?.marketplaces || {};
985
+ return {
986
+ ...(mergedConfig?.marketplaces || {}),
987
+ ...(this.getManagedMarketplaces() || {}),
988
+ };
989
+ }
990
+ /**
991
+ * Marketplaces declared by the remote (managed) settings. `null` means the
992
+ * managed layer declares none, leaving the local configuration chain in charge.
993
+ */
994
+ getManagedMarketplaces() {
995
+ return getRemoteSettingsSync()?.marketplaces ?? null;
849
996
  }
850
997
  /**
851
998
  * Get marketplaces at a specific scope
@@ -966,11 +1113,25 @@ export class ConfigurationService {
966
1113
  }
967
1114
  }
968
1115
  /**
969
- * Get merged enabled plugins from all scopes
1116
+ * Get merged enabled plugins from all scopes, with admin-managed (remote)
1117
+ * entries taking precedence per key (spec enterprise server-managed-config
1118
+ * 「托管配置下发插件市场与启用列表」场景 5). A managed `false` therefore
1119
+ * force-disables a plugin no matter what the local files say.
970
1120
  */
971
1121
  getMergedEnabledPlugins(workdir) {
972
1122
  const mergedConfig = loadMergedWaveConfig(workdir);
973
- return mergedConfig?.enabledPlugins || {};
1123
+ return {
1124
+ ...(mergedConfig?.enabledPlugins || {}),
1125
+ ...(this.getManagedEnabledPlugins() || {}),
1126
+ };
1127
+ }
1128
+ /**
1129
+ * Enabled-plugin entries declared by the remote (managed) settings. `null`
1130
+ * means the managed layer declares none. Doubles as the authoritative
1131
+ * "is this plugin managed by the organization" lookup (spec plugin A-024).
1132
+ */
1133
+ getManagedEnabledPlugins() {
1134
+ return getRemoteSettingsSync()?.enabledPlugins ?? null;
974
1135
  }
975
1136
  /**
976
1137
  * Load Wave configuration from a JSON file
@@ -1100,6 +1261,7 @@ export function loadWaveConfigFromFile(filePath) {
1100
1261
  marketplaces: config.marketplaces || undefined,
1101
1262
  worktree: config.worktree || undefined,
1102
1263
  enableArtifact: config.enableArtifact !== undefined ? config.enableArtifact : undefined,
1264
+ enableExec: config.enableExec !== undefined ? config.enableExec : undefined,
1103
1265
  };
1104
1266
  }
1105
1267
  catch (error) {
@@ -1195,10 +1357,9 @@ export function loadMergedWaveConfig(workdir) {
1195
1357
  ]),
1196
1358
  ];
1197
1359
  }
1198
- // Merge permissionMode (last one wins)
1199
- if (config.permissions.permissionMode !== undefined) {
1200
- mergedConfig.permissions.permissionMode =
1201
- config.permissions.permissionMode;
1360
+ // Merge defaultMode (last one wins)
1361
+ if (config.permissions.defaultMode !== undefined) {
1362
+ mergedConfig.permissions.defaultMode = config.permissions.defaultMode;
1202
1363
  }
1203
1364
  // Merge additionalDirectories
1204
1365
  if (config.permissions.additionalDirectories) {
@@ -1252,6 +1413,10 @@ export function loadMergedWaveConfig(workdir) {
1252
1413
  if (config.enableArtifact !== undefined) {
1253
1414
  mergedConfig.enableArtifact = config.enableArtifact;
1254
1415
  }
1416
+ // Merge enableExec (last one wins)
1417
+ if (config.enableExec !== undefined) {
1418
+ mergedConfig.enableExec = config.enableExec;
1419
+ }
1255
1420
  // Merge models
1256
1421
  if (config.models) {
1257
1422
  if (!mergedConfig.models)
@@ -1282,6 +1447,7 @@ export function loadMergedWaveConfig(workdir) {
1282
1447
  language: mergedConfig.language,
1283
1448
  model: mergedConfig.model,
1284
1449
  autoMemoryEnabled: mergedConfig.autoMemoryEnabled,
1450
+ autoMemoryFrequency: mergedConfig.autoMemoryFrequency,
1285
1451
  cleanupPeriodDays: mergedConfig.cleanupPeriodDays,
1286
1452
  marketplaces: mergedConfig.marketplaces &&
1287
1453
  Object.keys(mergedConfig.marketplaces).length > 0
@@ -1292,5 +1458,21 @@ export function loadMergedWaveConfig(workdir) {
1292
1458
  : undefined,
1293
1459
  worktree: mergedConfig.worktree,
1294
1460
  enableArtifact: mergedConfig.enableArtifact,
1461
+ enableExec: mergedConfig.enableExec,
1462
+ };
1463
+ }
1464
+ /**
1465
+ * Parse a hookName of the form `Event:Matcher` (e.g. `PreToolUse:Write`)
1466
+ * into its event and matcher parts. Hooks without a matcher use the event
1467
+ * name alone (e.g. `SessionStart`).
1468
+ */
1469
+ export function parseHookName(hookName) {
1470
+ const separator = hookName.indexOf(":");
1471
+ if (separator === -1) {
1472
+ return { event: hookName, matcher: "" };
1473
+ }
1474
+ return {
1475
+ event: hookName.slice(0, separator),
1476
+ matcher: hookName.slice(separator + 1),
1295
1477
  };
1296
1478
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Shared fast-model content processing.
3
+ *
4
+ * WebFetch runs every fetched page through a small, fast model, and the
5
+ * reader-view artifact read summarizes someone else's page the same way.
6
+ * Both go through this single implementation (prompt → answer).
7
+ */
8
+ import type { ToolContext, ToolResult } from "../tools/types.js";
9
+ /** Human-readable byte size (e.g. "512B", "2.0KB", "1.5MB"). */
10
+ export declare function formatSize(bytes: number): string;
11
+ /**
12
+ * Run the fast model over `content` with `prompt` and return its answer as the
13
+ * tool result. The main model only ever sees this answer.
14
+ */
15
+ export declare function processContentWithAI(url: string, prompt: string, content: string, statusCode: number, statusText: string, context: ToolContext, contentSize?: number): Promise<ToolResult>;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Shared fast-model content processing.
3
+ *
4
+ * WebFetch runs every fetched page through a small, fast model, and the
5
+ * reader-view artifact read summarizes someone else's page the same way.
6
+ * Both go through this single implementation (prompt → answer).
7
+ */
8
+ /** Human-readable byte size (e.g. "512B", "2.0KB", "1.5MB"). */
9
+ export function formatSize(bytes) {
10
+ if (bytes < 1024)
11
+ return `${bytes}B`;
12
+ if (bytes < 1024 * 1024)
13
+ return `${(bytes / 1024).toFixed(1)}KB`;
14
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
15
+ }
16
+ /**
17
+ * Run the fast model over `content` with `prompt` and return its answer as the
18
+ * tool result. The main model only ever sees this answer.
19
+ */
20
+ export async function processContentWithAI(url, prompt, content, statusCode, statusText, context, contentSize) {
21
+ if (!context.aiManager || !context.aiService) {
22
+ return {
23
+ success: false,
24
+ content: content,
25
+ error: "AI Manager or AI Service not available for processing content",
26
+ };
27
+ }
28
+ const modelConfig = context.aiManager.getModelConfig();
29
+ const fastModel = modelConfig.fastModel;
30
+ const aiResponse = await context.aiService.processWebContent({
31
+ gatewayConfig: context.aiManager.getGatewayConfig(),
32
+ modelConfig: modelConfig,
33
+ content: content,
34
+ prompt: prompt,
35
+ model: fastModel,
36
+ abortSignal: context.abortSignal,
37
+ });
38
+ const sizeStr = contentSize !== undefined ? formatSize(contentSize) : "unknown size";
39
+ const statusStr = `${statusCode} ${statusText}`.trim();
40
+ return {
41
+ success: true,
42
+ content: aiResponse.content || "",
43
+ shortResult: `Received ${sizeStr} (${statusStr}) from ${url}`,
44
+ };
45
+ }
@@ -0,0 +1,9 @@
1
+ /** Code default for Exec availability. */
2
+ export declare const EXEC_DEFAULT_ENABLED = true;
3
+ /**
4
+ * Whether the Exec tool may be registered for the given workdir.
5
+ * Resolution order: remote managed settings (`enableExec` from
6
+ * `GET /api/wave/settings`, admin override) → explicit `enableExec` in local
7
+ * merged settings → code default.
8
+ */
9
+ export declare function isExecEnabled(workdir?: string): boolean;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Exec feature availability.
3
+ *
4
+ * Exec collapses the MCP tool pool into a single scriptable tool, which changes
5
+ * what the model sees in `tools[]`. It is on by default (the collapse applies
6
+ * whenever the pool is non-empty — there is no minimum tool count);
7
+ * `enableExec: false` is the escape hatch for a session that wants the flat MCP
8
+ * declarations back without downgrading.
9
+ */
10
+ import { loadMergedWaveConfig } from "./configurationService.js";
11
+ import { getRemoteSettingsSync } from "./remoteSettingsService.js";
12
+ /** Code default for Exec availability. */
13
+ export const EXEC_DEFAULT_ENABLED = true;
14
+ /**
15
+ * Whether the Exec tool may be registered for the given workdir.
16
+ * Resolution order: remote managed settings (`enableExec` from
17
+ * `GET /api/wave/settings`, admin override) → explicit `enableExec` in local
18
+ * merged settings → code default.
19
+ */
20
+ export function isExecEnabled(workdir) {
21
+ const remote = getRemoteSettingsSync();
22
+ if (remote?.enableExec !== undefined) {
23
+ return remote.enableExec;
24
+ }
25
+ if (workdir) {
26
+ const config = loadMergedWaveConfig(workdir);
27
+ if (config?.enableExec !== undefined) {
28
+ return config.enableExec;
29
+ }
30
+ }
31
+ return EXEC_DEFAULT_ENABLED;
32
+ }