wave-agent-sdk 1.2.0 → 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 (175) hide show
  1. package/dist/agent.d.ts +58 -4
  2. package/dist/agent.js +91 -19
  3. package/dist/builtin/index.js +2 -0
  4. package/dist/builtin/skills/settings.js +1 -12
  5. package/dist/builtin/skills/wave-daemon.d.ts +1 -0
  6. package/dist/builtin/skills/wave-daemon.js +194 -0
  7. package/dist/constants/images.d.ts +26 -0
  8. package/dist/constants/images.js +26 -0
  9. package/dist/constants/index.d.ts +16 -0
  10. package/dist/constants/index.js +16 -0
  11. package/dist/constants/memory.d.ts +26 -0
  12. package/dist/constants/memory.js +34 -0
  13. package/dist/constants/messages.d.ts +11 -0
  14. package/dist/constants/messages.js +11 -0
  15. package/dist/constants/plugins.d.ts +8 -0
  16. package/dist/constants/plugins.js +8 -0
  17. package/dist/constants/tools.d.ts +1 -0
  18. package/dist/constants/tools.js +1 -0
  19. package/dist/core/plugin.d.ts +53 -13
  20. package/dist/core/plugin.js +134 -26
  21. package/dist/core/session.d.ts +1 -1
  22. package/dist/core/session.js +1 -1
  23. package/dist/exec/catalog.d.ts +140 -0
  24. package/dist/exec/catalog.js +470 -0
  25. package/dist/exec/catalogAnnouncement.d.ts +89 -0
  26. package/dist/exec/catalogAnnouncement.js +293 -0
  27. package/dist/exec/constants.d.ts +51 -0
  28. package/dist/exec/constants.js +51 -0
  29. package/dist/exec/execRuntime.d.ts +55 -0
  30. package/dist/exec/execRuntime.js +217 -0
  31. package/dist/exec/workerSource.d.ts +28 -0
  32. package/dist/exec/workerSource.js +299 -0
  33. package/dist/host/index.d.ts +23 -0
  34. package/dist/host/index.js +23 -0
  35. package/dist/index.d.ts +5 -0
  36. package/dist/index.js +6 -0
  37. package/dist/managers/MemoryRuleManager.d.ts +6 -0
  38. package/dist/managers/MemoryRuleManager.js +12 -0
  39. package/dist/managers/aiManager.d.ts +35 -1
  40. package/dist/managers/aiManager.js +190 -21
  41. package/dist/managers/backgroundTaskManager.js +14 -0
  42. package/dist/managers/hookManager.d.ts +13 -0
  43. package/dist/managers/hookManager.js +31 -4
  44. package/dist/managers/liveConfigManager.d.ts +33 -0
  45. package/dist/managers/liveConfigManager.js +103 -8
  46. package/dist/managers/lspManager.d.ts +9 -0
  47. package/dist/managers/lspManager.js +47 -18
  48. package/dist/managers/mcpManager.d.ts +45 -10
  49. package/dist/managers/mcpManager.js +103 -1
  50. package/dist/managers/messageManager.d.ts +48 -5
  51. package/dist/managers/messageManager.js +107 -21
  52. package/dist/managers/permissionManager.d.ts +40 -0
  53. package/dist/managers/permissionManager.js +63 -8
  54. package/dist/managers/pluginManager.d.ts +46 -2
  55. package/dist/managers/pluginManager.js +117 -11
  56. package/dist/managers/pluginScopeManager.d.ts +15 -2
  57. package/dist/managers/pluginScopeManager.js +20 -1
  58. package/dist/managers/skillManager.d.ts +19 -0
  59. package/dist/managers/skillManager.js +44 -0
  60. package/dist/managers/slashCommandManager.d.ts +10 -0
  61. package/dist/managers/slashCommandManager.js +35 -3
  62. package/dist/managers/subagentManager.d.ts +8 -0
  63. package/dist/managers/subagentManager.js +20 -0
  64. package/dist/managers/toolManager.d.ts +29 -3
  65. package/dist/managers/toolManager.js +87 -13
  66. package/dist/prompts/autoMemory.d.ts +9 -0
  67. package/dist/prompts/autoMemory.js +30 -31
  68. package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
  69. package/dist/prompts/autoMemoryExtraction.js +8 -111
  70. package/dist/prompts/memoryTypes.d.ts +63 -0
  71. package/dist/prompts/memoryTypes.js +191 -0
  72. package/dist/services/GitService.d.ts +7 -0
  73. package/dist/services/GitService.js +23 -0
  74. package/dist/services/MarketplaceService.d.ts +101 -17
  75. package/dist/services/MarketplaceService.js +318 -119
  76. package/dist/services/artifactContent.d.ts +84 -0
  77. package/dist/services/artifactContent.js +204 -0
  78. package/dist/services/artifactSession.d.ts +6 -0
  79. package/dist/services/artifactSession.js +17 -0
  80. package/dist/services/autoMemoryService.js +5 -13
  81. package/dist/services/configurationService.d.ts +60 -9
  82. package/dist/services/configurationService.js +129 -54
  83. package/dist/services/contentSummarizer.d.ts +15 -0
  84. package/dist/services/contentSummarizer.js +45 -0
  85. package/dist/services/execAvailability.d.ts +9 -0
  86. package/dist/services/execAvailability.js +32 -0
  87. package/dist/services/fileWatcher.js +61 -6
  88. package/dist/services/initializationService.js +19 -15
  89. package/dist/services/interactionService.d.ts +9 -1
  90. package/dist/services/interactionService.js +28 -8
  91. package/dist/services/jsonlHandler.d.ts +84 -0
  92. package/dist/services/jsonlHandler.js +209 -14
  93. package/dist/services/memory.d.ts +3 -1
  94. package/dist/services/memory.js +13 -9
  95. package/dist/services/officialMarketplaceMirror.js +3 -2
  96. package/dist/services/pluginLoader.d.ts +12 -4
  97. package/dist/services/pluginLoader.js +38 -7
  98. package/dist/services/remoteSettingsService.js +16 -2
  99. package/dist/services/session.d.ts +74 -0
  100. package/dist/services/session.js +144 -3
  101. package/dist/services/sessionEntries.d.ts +2 -0
  102. package/dist/services/sessionEntries.js +20 -0
  103. package/dist/stdio/index.d.ts +3 -1
  104. package/dist/stdio/index.js +3 -1
  105. package/dist/stdio/notificationRouter.js +1 -0
  106. package/dist/stdio/stdioAgent.d.ts +14 -7
  107. package/dist/stdio/stdioAgent.js +19 -0
  108. package/dist/tools/artifactTool.js +406 -273
  109. package/dist/tools/bashTool.js +8 -6
  110. package/dist/tools/editTool.js +6 -3
  111. package/dist/tools/execTool.d.ts +2 -0
  112. package/dist/tools/execTool.js +165 -0
  113. package/dist/tools/grepTool.js +7 -1
  114. package/dist/tools/readTool.js +30 -2
  115. package/dist/tools/types.d.ts +34 -8
  116. package/dist/tools/webFetchTool.js +15 -166
  117. package/dist/tools/workflowTool.js +40 -8
  118. package/dist/tools/writeTool.js +6 -3
  119. package/dist/types/agent.d.ts +20 -5
  120. package/dist/types/configuration.d.ts +39 -1
  121. package/dist/types/marketplace.d.ts +40 -2
  122. package/dist/types/mcp.d.ts +39 -0
  123. package/dist/types/permissions.d.ts +22 -0
  124. package/dist/types/permissions.js +17 -0
  125. package/dist/types/plugins.d.ts +26 -2
  126. package/dist/types/skills.d.ts +15 -0
  127. package/dist/utils/constants.d.ts +10 -0
  128. package/dist/utils/constants.js +10 -0
  129. package/dist/utils/containerSetup.js +43 -0
  130. package/dist/utils/convertMessagesForAPI.d.ts +7 -1
  131. package/dist/utils/convertMessagesForAPI.js +64 -14
  132. package/dist/utils/fileChangeReminder.d.ts +20 -0
  133. package/dist/utils/fileChangeReminder.js +153 -0
  134. package/dist/utils/fileSearch.js +4 -3
  135. package/dist/utils/fileUtils.d.ts +33 -0
  136. package/dist/utils/fileUtils.js +81 -0
  137. package/dist/utils/frontmatterYaml.d.ts +33 -0
  138. package/dist/utils/frontmatterYaml.js +192 -0
  139. package/dist/utils/imageBudget.d.ts +85 -0
  140. package/dist/utils/imageBudget.js +109 -0
  141. package/dist/utils/imageDimensions.d.ts +83 -0
  142. package/dist/utils/imageDimensions.js +232 -0
  143. package/dist/utils/imageProcessor.d.ts +66 -0
  144. package/dist/utils/imageProcessor.js +84 -0
  145. package/dist/utils/imageRewrite.d.ts +29 -0
  146. package/dist/utils/imageRewrite.js +251 -0
  147. package/dist/utils/markdownParser.d.ts +5 -1
  148. package/dist/utils/markdownParser.js +9 -51
  149. package/dist/utils/mcpInstructions.d.ts +61 -0
  150. package/dist/utils/mcpInstructions.js +126 -0
  151. package/dist/utils/mcpUtils.d.ts +7 -0
  152. package/dist/utils/mcpUtils.js +11 -2
  153. package/dist/utils/memoryAge.d.ts +32 -0
  154. package/dist/utils/memoryAge.js +47 -0
  155. package/dist/utils/memoryEntrypoint.d.ts +20 -0
  156. package/dist/utils/memoryEntrypoint.js +49 -0
  157. package/dist/utils/memoryIndex.d.ts +30 -0
  158. package/dist/utils/memoryIndex.js +76 -0
  159. package/dist/utils/messageOperations.d.ts +6 -2
  160. package/dist/utils/messageOperations.js +40 -29
  161. package/dist/utils/nestedMemory.d.ts +22 -0
  162. package/dist/utils/nestedMemory.js +61 -0
  163. package/dist/utils/npmTarball.d.ts +19 -0
  164. package/dist/utils/npmTarball.js +92 -0
  165. package/dist/utils/pluginSource.d.ts +37 -0
  166. package/dist/utils/pluginSource.js +73 -0
  167. package/dist/utils/ripgrep.d.ts +18 -4
  168. package/dist/utils/ripgrep.js +56 -4
  169. package/dist/utils/runtimeDeps.d.ts +35 -0
  170. package/dist/utils/runtimeDeps.js +426 -0
  171. package/dist/utils/skillParser.js +22 -52
  172. package/dist/utils/subagentParser.js +39 -43
  173. package/dist/utils/userSettings.d.ts +90 -0
  174. package/dist/utils/userSettings.js +291 -0
  175. package/package.json +10 -7
@@ -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";
@@ -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,30 +602,42 @@ 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: session options (host settings-page value) > 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. Per-session options override (hosts pass the settings-page toggle over
598
- // stdio initialize/updateConfig; Agent.create → setOptions stores it)
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)
599
632
  if (this.options.autoMemoryEnabled !== undefined) {
600
633
  return this.options.autoMemoryEnabled;
601
634
  }
602
635
  // 2. settings.json (merged)
603
- if (this.currentConfiguration?.autoMemoryEnabled !== undefined) {
604
- return this.currentConfiguration.autoMemoryEnabled;
636
+ if (config?.autoMemoryEnabled !== undefined) {
637
+ return config.autoMemoryEnabled;
605
638
  }
606
639
  // 3. WAVE_DISABLE_AUTO_MEMORY environment variable (settings snapshot > OS env)
607
- const disableAutoMemory = this.envSnapshot.WAVE_DISABLE_AUTO_MEMORY ??
608
- process.env.WAVE_DISABLE_AUTO_MEMORY;
640
+ const disableAutoMemory = env.WAVE_DISABLE_AUTO_MEMORY ?? process.env.WAVE_DISABLE_AUTO_MEMORY;
609
641
  if (disableAutoMemory === "1" || disableAutoMemory === "true") {
610
642
  return false;
611
643
  }
@@ -618,7 +650,7 @@ export class ConfigurationService {
618
650
  * @returns Resolved worktree base ref
619
651
  */
620
652
  resolveWorktreeBaseRef() {
621
- const baseRef = this.currentConfiguration?.worktree?.baseRef;
653
+ const baseRef = this.liveConfiguration?.worktree?.baseRef;
622
654
  if (baseRef === "head") {
623
655
  return "head";
624
656
  }
@@ -626,12 +658,13 @@ export class ConfigurationService {
626
658
  }
627
659
  /**
628
660
  * Resolves auto-memory extraction frequency with fallbacks
629
- * Resolution priority: session options (host settings-page value) > settings.json > WAVE_AUTO_MEMORY_FREQUENCY > default (1)
661
+ * Resolution priority: session options (程序化会话级覆盖) > settings.json > WAVE_AUTO_MEMORY_FREQUENCY > default (1)
630
662
  * @returns Resolved auto-memory extraction frequency (turns)
631
663
  */
632
664
  resolveAutoMemoryFrequency() {
633
- // 1. Per-session options override (hosts pass the settings-page value over
634
- // stdio initialize/updateConfig; Agent.create → setOptions stores it).
665
+ // 1. Per-session options override (Agent.create → setOptions 的程序化会话级
666
+ // 覆盖;三端设置页的值不走这里——它们落用户级 settings.json,由实时重载
667
+ // 在下一轮生效,见 spec core/agent-config.md「设置实时重载」)。
635
668
  // Only positive values are honored so a malformed 0/negative never
636
669
  // degenerates into per-turn extraction.
637
670
  if (this.options.autoMemoryFrequency !== undefined &&
@@ -639,11 +672,11 @@ export class ConfigurationService {
639
672
  return this.options.autoMemoryFrequency;
640
673
  }
641
674
  // 2. settings.json (merged)
642
- if (this.currentConfiguration?.autoMemoryFrequency !== undefined) {
643
- return this.currentConfiguration.autoMemoryFrequency;
675
+ if (this.liveConfiguration?.autoMemoryFrequency !== undefined) {
676
+ return this.liveConfiguration.autoMemoryFrequency;
644
677
  }
645
678
  // 3. WAVE_AUTO_MEMORY_FREQUENCY environment variable (settings snapshot > OS env)
646
- const envFrequency = this.envSnapshot.WAVE_AUTO_MEMORY_FREQUENCY ??
679
+ const envFrequency = this.liveEnvSnapshot.WAVE_AUTO_MEMORY_FREQUENCY ??
647
680
  process.env.WAVE_AUTO_MEMORY_FREQUENCY;
648
681
  if (envFrequency) {
649
682
  const parsed = parseInt(envFrequency, 10);
@@ -670,7 +703,7 @@ export class ConfigurationService {
670
703
  return this.options.maxTokens;
671
704
  }
672
705
  // Try env (settings.json snapshot) first, then process.env
673
- const envMaxOutputTokens = this.envSnapshot.WAVE_MAX_OUTPUT_TOKENS ??
706
+ const envMaxOutputTokens = this.liveEnvSnapshot.WAVE_MAX_OUTPUT_TOKENS ??
674
707
  process.env.WAVE_MAX_OUTPUT_TOKENS;
675
708
  if (envMaxOutputTokens) {
676
709
  const parsed = parseInt(envMaxOutputTokens, 10);
@@ -682,11 +715,20 @@ export class ConfigurationService {
682
715
  return DEFAULT_WAVE_MAX_OUTPUT_TOKENS;
683
716
  }
684
717
  /**
685
- * 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.
686
723
  */
687
- setModel(model) {
724
+ async setModel(model) {
688
725
  this.options.model = model;
689
- 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
+ }
690
732
  }
691
733
  async persistModelToSettings(model) {
692
734
  const configPath = getUserConfigPaths()[0]; // ~/.wave/settings.json
@@ -714,17 +756,17 @@ export class ConfigurationService {
714
756
  const models = new Set();
715
757
  // Add current model from options or environment (settings snapshot > OS env)
716
758
  const currentModel = this.options.model ||
717
- (this.envSnapshot.WAVE_MODEL ?? process.env.WAVE_MODEL);
759
+ (this.liveEnvSnapshot.WAVE_MODEL ?? process.env.WAVE_MODEL);
718
760
  if (currentModel) {
719
761
  models.add(currentModel);
720
762
  }
721
763
  // Persisted model from settings (includes remote-merged)
722
- if (this.currentConfiguration?.model) {
723
- models.add(this.currentConfiguration.model);
764
+ if (this.liveConfiguration?.model) {
765
+ models.add(this.liveConfiguration.model);
724
766
  }
725
767
  // Add models from merged configuration
726
- if (this.currentConfiguration?.models) {
727
- Object.keys(this.currentConfiguration.models).forEach((model) => {
768
+ if (this.liveConfiguration?.models) {
769
+ Object.keys(this.liveConfiguration.models).forEach((model) => {
728
770
  models.add(model);
729
771
  });
730
772
  }
@@ -734,7 +776,7 @@ export class ConfigurationService {
734
776
  * Get the telemetry config from the loaded settings.
735
777
  */
736
778
  resolveTelemetryConfig() {
737
- return this.currentConfiguration?.monitoring?.telemetry;
779
+ return this.liveConfiguration?.monitoring?.telemetry;
738
780
  }
739
781
  /**
740
782
  * Resolve all configuration file paths
@@ -934,11 +976,23 @@ export class ConfigurationService {
934
976
  }
935
977
  }
936
978
  /**
937
- * Get merged marketplaces from all scopes
979
+ * Get merged marketplaces from all scopes, with admin-managed (remote) entries
980
+ * taking precedence per key (spec enterprise server-managed-config「托管配置
981
+ * 下发插件市场与启用列表」场景 5).
938
982
  */
939
983
  getMergedMarketplaces(workdir) {
940
984
  const mergedConfig = loadMergedWaveConfig(workdir);
941
- 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;
942
996
  }
943
997
  /**
944
998
  * Get marketplaces at a specific scope
@@ -1059,11 +1113,25 @@ export class ConfigurationService {
1059
1113
  }
1060
1114
  }
1061
1115
  /**
1062
- * 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.
1063
1120
  */
1064
1121
  getMergedEnabledPlugins(workdir) {
1065
1122
  const mergedConfig = loadMergedWaveConfig(workdir);
1066
- 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;
1067
1135
  }
1068
1136
  /**
1069
1137
  * Load Wave configuration from a JSON file
@@ -1193,6 +1261,7 @@ export function loadWaveConfigFromFile(filePath) {
1193
1261
  marketplaces: config.marketplaces || undefined,
1194
1262
  worktree: config.worktree || undefined,
1195
1263
  enableArtifact: config.enableArtifact !== undefined ? config.enableArtifact : undefined,
1264
+ enableExec: config.enableExec !== undefined ? config.enableExec : undefined,
1196
1265
  };
1197
1266
  }
1198
1267
  catch (error) {
@@ -1344,6 +1413,10 @@ export function loadMergedWaveConfig(workdir) {
1344
1413
  if (config.enableArtifact !== undefined) {
1345
1414
  mergedConfig.enableArtifact = config.enableArtifact;
1346
1415
  }
1416
+ // Merge enableExec (last one wins)
1417
+ if (config.enableExec !== undefined) {
1418
+ mergedConfig.enableExec = config.enableExec;
1419
+ }
1347
1420
  // Merge models
1348
1421
  if (config.models) {
1349
1422
  if (!mergedConfig.models)
@@ -1374,6 +1447,7 @@ export function loadMergedWaveConfig(workdir) {
1374
1447
  language: mergedConfig.language,
1375
1448
  model: mergedConfig.model,
1376
1449
  autoMemoryEnabled: mergedConfig.autoMemoryEnabled,
1450
+ autoMemoryFrequency: mergedConfig.autoMemoryFrequency,
1377
1451
  cleanupPeriodDays: mergedConfig.cleanupPeriodDays,
1378
1452
  marketplaces: mergedConfig.marketplaces &&
1379
1453
  Object.keys(mergedConfig.marketplaces).length > 0
@@ -1384,6 +1458,7 @@ export function loadMergedWaveConfig(workdir) {
1384
1458
  : undefined,
1385
1459
  worktree: mergedConfig.worktree,
1386
1460
  enableArtifact: mergedConfig.enableArtifact,
1461
+ enableExec: mergedConfig.enableExec,
1387
1462
  };
1388
1463
  }
1389
1464
  /**
@@ -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
+ }
@@ -5,7 +5,59 @@
5
5
  * Handles file watching with debouncing, error recovery, and graceful fallbacks.
6
6
  */
7
7
  import * as chokidar from "chokidar";
8
+ import * as fs from "fs";
9
+ import * as path from "path";
8
10
  import { EventEmitter } from "events";
11
+ /**
12
+ * Expand a watch path to its canonical long form (Windows only).
13
+ *
14
+ * libuv's fs-event backend resolves each event path with `GetLongPathNameW()`
15
+ * and then asserts the result is prefixed by the watched directory string
16
+ * (`uv__relative_path()`, src\win\fs-event.c). libuv <= 1.51 expanded the
17
+ * watched directory itself; 1.52 dropped that step (it ships with Node 24.16+,
18
+ * 26.x, and Electron 43 — which bundles Node 24.18), so `handle->dirw`
19
+ * keeps whatever the caller passed, and watching an 8.3 short path
20
+ * (`C:\Users\LIUYIQ~1\...` — what %TEMP% yields when it is configured with a
21
+ * short name) aborts the whole process on the first event. The abort is not a
22
+ * catchable error, so the root must be canonicalized before it reaches
23
+ * chokidar. `fs.realpathSync()` alone is not enough: only the `.native()`
24
+ * variant expands short names.
25
+ *
26
+ * Paths that don't exist yet (`~/.wave/settings.json` on a fresh install) are
27
+ * handled by expanding the nearest existing ancestor and appending the
28
+ * remaining segments back; if nothing can be resolved the input is returned
29
+ * unchanged.
30
+ */
31
+ function toLongFormPath(target) {
32
+ if (process.platform !== "win32")
33
+ return target;
34
+ // Only drive-qualified (`C:\...`) and UNC (`\\server\share`) paths are real
35
+ // Windows paths. POSIX-style input (what tests and other platforms use) is
36
+ // passed through untouched rather than re-rooted onto the current drive.
37
+ if (!/^[a-zA-Z]:[\\/]/.test(target) && !target.startsWith("\\\\")) {
38
+ return target;
39
+ }
40
+ let candidate = target;
41
+ const missing = [];
42
+ for (;;) {
43
+ try {
44
+ const resolved = fs.realpathSync.native(candidate);
45
+ return missing.length > 0 ? path.join(resolved, ...missing) : resolved;
46
+ }
47
+ catch {
48
+ const parent = path.dirname(candidate);
49
+ if (parent === candidate)
50
+ return target;
51
+ missing.unshift(path.basename(candidate));
52
+ candidate = parent;
53
+ }
54
+ }
55
+ }
56
+ /** Whether `filePath` (slash-normalized) is the watched root or inside it. */
57
+ function isWithin(filePath, root) {
58
+ const normalizedRoot = root.replace(/\\/g, "/");
59
+ return (filePath === normalizedRoot || filePath.startsWith(normalizedRoot + "/"));
60
+ }
9
61
  export class FileWatcherService extends EventEmitter {
10
62
  constructor(logger, config) {
11
63
  super();
@@ -38,6 +90,7 @@ export class FileWatcherService extends EventEmitter {
38
90
  // Create new watcher entry
39
91
  const entry = {
40
92
  path,
93
+ watchPath: toLongFormPath(path),
41
94
  watcher: null,
42
95
  isActive: false,
43
96
  lastEvent: Date.now(),
@@ -64,7 +117,7 @@ export class FileWatcherService extends EventEmitter {
64
117
  return;
65
118
  try {
66
119
  if (entry.watcher) {
67
- entry.watcher.unwatch(path);
120
+ entry.watcher.unwatch(entry.watchPath);
68
121
  }
69
122
  this.watchers.delete(path);
70
123
  }
@@ -148,7 +201,7 @@ export class FileWatcherService extends EventEmitter {
148
201
  this.setupGlobalWatcherEvents();
149
202
  }
150
203
  // Add path to global watcher
151
- this.globalWatcher.add(entry.path);
204
+ this.globalWatcher.add(entry.watchPath);
152
205
  entry.watcher = this.globalWatcher;
153
206
  entry.isActive = true;
154
207
  entry.errorCount = 0;
@@ -207,12 +260,14 @@ export class FileWatcherService extends EventEmitter {
207
260
  timestamp: Date.now(),
208
261
  size: stats?.size,
209
262
  };
210
- // Notify all watchers that match the path or are parents of the path
263
+ // Notify all watchers that match the path or are parents of the path.
264
+ // Events arrive under whichever form chokidar was given (the long form on
265
+ // Windows), while entries are keyed by the caller's original path, so both
266
+ // have to be considered.
211
267
  for (const [watchedPath, entry] of this.watchers.entries()) {
212
268
  const normalizedFilePath = filePath.replace(/\\/g, "/");
213
- const normalizedWatchedPath = watchedPath.replace(/\\/g, "/");
214
- if (normalizedFilePath === normalizedWatchedPath ||
215
- normalizedFilePath.startsWith(normalizedWatchedPath + "/")) {
269
+ if (isWithin(normalizedFilePath, watchedPath) ||
270
+ isWithin(normalizedFilePath, entry.watchPath)) {
216
271
  entry.lastEvent = event.timestamp;
217
272
  // Notify all callbacks for this watcher
218
273
  for (const callback of entry.callbacks) {