wave-agent-sdk 1.1.4 → 1.2.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 (89) hide show
  1. package/dist/agent.d.ts +72 -24
  2. package/dist/agent.js +123 -31
  3. package/dist/builtin/plugins.js +17 -2
  4. package/dist/builtin/skills/settings.js +6 -8
  5. package/dist/core/plugin.d.ts +4 -0
  6. package/dist/core/plugin.js +7 -1
  7. package/dist/index.d.ts +2 -1
  8. package/dist/index.js +2 -1
  9. package/dist/managers/aiManager.d.ts +0 -24
  10. package/dist/managers/aiManager.js +23 -181
  11. package/dist/managers/bashModeManager.d.ts +33 -0
  12. package/dist/managers/bashModeManager.js +110 -0
  13. package/dist/managers/hookManager.d.ts +5 -0
  14. package/dist/managers/hookManager.js +7 -0
  15. package/dist/managers/liveConfigManager.js +3 -3
  16. package/dist/managers/mcpManager.d.ts +23 -0
  17. package/dist/managers/mcpManager.js +162 -14
  18. package/dist/managers/messageManager.d.ts +12 -13
  19. package/dist/managers/messageManager.js +63 -60
  20. package/dist/managers/permissionManager.d.ts +29 -0
  21. package/dist/managers/permissionManager.js +158 -70
  22. package/dist/managers/planManager.d.ts +9 -0
  23. package/dist/managers/planManager.js +19 -1
  24. package/dist/managers/skillManager.d.ts +31 -0
  25. package/dist/managers/skillManager.js +122 -12
  26. package/dist/managers/slashCommandManager.js +9 -28
  27. package/dist/managers/subagentManager.d.ts +7 -0
  28. package/dist/managers/subagentManager.js +61 -9
  29. package/dist/managers/workflowManager.js +6 -0
  30. package/dist/prompts/index.d.ts +0 -1
  31. package/dist/prompts/index.js +0 -4
  32. package/dist/services/MarketplaceService.js +36 -14
  33. package/dist/services/configurationService.d.ts +34 -2
  34. package/dist/services/configurationService.js +123 -16
  35. package/dist/services/initializationService.js +2 -2
  36. package/dist/services/jsonlHandler.d.ts +14 -0
  37. package/dist/services/jsonlHandler.js +44 -1
  38. package/dist/services/memory.d.ts +14 -0
  39. package/dist/services/memory.js +33 -0
  40. package/dist/services/officialMarketplaceMirror.d.ts +85 -0
  41. package/dist/services/officialMarketplaceMirror.js +289 -0
  42. package/dist/services/remoteSettingsService.js +4 -4
  43. package/dist/services/session.js +30 -13
  44. package/dist/services/worktreeHooks.js +6 -1
  45. package/dist/stdio/index.d.ts +10 -0
  46. package/dist/stdio/index.js +10 -0
  47. package/dist/stdio/notificationRouter.d.ts +38 -0
  48. package/dist/stdio/notificationRouter.js +96 -0
  49. package/dist/stdio/rpcClient.d.ts +18 -0
  50. package/dist/stdio/rpcClient.js +10 -0
  51. package/dist/stdio/stdioAgent.d.ts +222 -0
  52. package/dist/stdio/stdioAgent.js +341 -0
  53. package/dist/tools/bashTool.js +2 -0
  54. package/dist/tools/exitPlanMode.js +10 -2
  55. package/dist/types/agent.d.ts +8 -0
  56. package/dist/types/commands.d.ts +7 -0
  57. package/dist/types/configuration.d.ts +6 -1
  58. package/dist/types/hooks.d.ts +1 -0
  59. package/dist/types/hooks.js +19 -0
  60. package/dist/types/mcp.d.ts +3 -0
  61. package/dist/types/messaging.d.ts +1 -8
  62. package/dist/types/skills.d.ts +11 -0
  63. package/dist/utils/bashParser.d.ts +17 -0
  64. package/dist/utils/bashParser.js +72 -0
  65. package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
  66. package/dist/utils/bashStructure/bashLexer.js +676 -0
  67. package/dist/utils/bashStructure/bashParser.d.ts +144 -0
  68. package/dist/utils/bashStructure/bashParser.js +606 -0
  69. package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
  70. package/dist/utils/bashStructure/bashSemantics.js +477 -0
  71. package/dist/utils/bashStructure/index.d.ts +26 -0
  72. package/dist/utils/bashStructure/index.js +27 -0
  73. package/dist/utils/bashStructure/types.d.ts +62 -0
  74. package/dist/utils/bashStructure/types.js +47 -0
  75. package/dist/utils/container.d.ts +6 -0
  76. package/dist/utils/container.js +9 -0
  77. package/dist/utils/containerSetup.d.ts +11 -1
  78. package/dist/utils/containerSetup.js +26 -11
  79. package/dist/utils/fileUtils.d.ts +11 -0
  80. package/dist/utils/fileUtils.js +37 -0
  81. package/dist/utils/messageOperations.d.ts +0 -18
  82. package/dist/utils/messageOperations.js +0 -62
  83. package/dist/utils/subagentParser.js +9 -2
  84. package/dist/utils/tokenCalculation.js +0 -8
  85. package/dist/utils/worktreeUtils.d.ts +2 -1
  86. package/dist/utils/worktreeUtils.js +64 -34
  87. package/package.json +6 -1
  88. package/dist/managers/bangManager.d.ts +0 -26
  89. package/dist/managers/bangManager.js +0 -78
@@ -14,34 +14,6 @@ export class SlashCommandManager {
14
14
  }
15
15
  initialize() {
16
16
  this.loadCustomCommands();
17
- // Builtin /subtask: manual fork subagent (aligned with Claude Code's
18
- // /subtask — inherits the parent's full context, runs in the background,
19
- // result returns to the main conversation via a task notification).
20
- this.registerCommand({
21
- id: "subtask",
22
- name: "subtask",
23
- description: "Start a fork subagent with the current conversation context in the background",
24
- handler: async (args, signal) => {
25
- const taskDescription = args?.trim();
26
- if (!taskDescription) {
27
- this.messageManager.addErrorBlock("Usage: /subtask <task description>");
28
- return;
29
- }
30
- try {
31
- // The fork snapshots its context synchronously on entry, so the
32
- // transcript echo below never duplicates the prompt inside the fork.
33
- await this.aiManager.runForkSubagent(taskDescription, { description: taskDescription }, signal);
34
- this.messageManager.addUserMessage({
35
- content: `/subtask ${taskDescription}`,
36
- });
37
- }
38
- catch (error) {
39
- this.aiManager.setIsLoading(false);
40
- logger?.error("Failed to execute /subtask:", error);
41
- this.messageManager.addErrorBlock(`Failed to execute /subtask: ${error instanceof Error ? error.message : String(error)}`);
42
- }
43
- },
44
- });
45
17
  // Listen for skill refreshes and update skill commands
46
18
  const skillManager = this.container.get("SkillManager");
47
19
  if (skillManager) {
@@ -103,6 +75,13 @@ export class SlashCommandManager {
103
75
  this.unregisterCommand(commandId);
104
76
  }
105
77
  this.skillCommandIds.clear();
78
+ const skillSourceFor = (skill) => {
79
+ if (skill.pluginName)
80
+ return "plugin";
81
+ if (skill.type === "personal")
82
+ return "user";
83
+ return skill.type; // "project" | "builtin"
84
+ };
106
85
  for (const skill of skills) {
107
86
  if (skill.userInvocable === false) {
108
87
  continue;
@@ -113,6 +92,8 @@ export class SlashCommandManager {
113
92
  id: commandId,
114
93
  name: skill.name,
115
94
  description: `Skill: ${skill.description}`,
95
+ // UI source tag: plugin skills carry pluginName; personal → 用户
96
+ skillSource: skillSourceFor(skill),
116
97
  handler: async (args, signal) => {
117
98
  try {
118
99
  // 1. Prepare skill content immediately
@@ -100,6 +100,13 @@ export declare class SubagentManager {
100
100
  * for conditional subagent registration (e.g. WAVE_VISION_MODEL).
101
101
  */
102
102
  private getMergedEnv;
103
+ /**
104
+ * Delete a user-level or project-level subagent by removing its markdown
105
+ * file. Builtin and plugin agents are read-only and cannot be deleted.
106
+ * @param name - The subagent name as shown in configurations
107
+ * @returns true if the subagent was deleted
108
+ */
109
+ deleteSubagent(name: string): Promise<boolean>;
103
110
  /**
104
111
  * Get cached configurations synchronously (must call loadConfigurations first)
105
112
  */
@@ -107,6 +107,31 @@ export class SubagentManager {
107
107
  return (this.configurationService?.getMergedEnv?.() ??
108
108
  process.env);
109
109
  }
110
+ /**
111
+ * Delete a user-level or project-level subagent by removing its markdown
112
+ * file. Builtin and plugin agents are read-only and cannot be deleted.
113
+ * @param name - The subagent name as shown in configurations
114
+ * @returns true if the subagent was deleted
115
+ */
116
+ async deleteSubagent(name) {
117
+ const configs = this.cachedConfigurations ?? [];
118
+ const config = configs.find((c) => c.name === name);
119
+ if (!config) {
120
+ return false;
121
+ }
122
+ // Builtin and plugin agents are read-only
123
+ if (config.scope !== "user" && config.scope !== "project") {
124
+ return false;
125
+ }
126
+ const { unlink } = await import("fs/promises");
127
+ await unlink(config.filePath);
128
+ // Drop the deleted agent from the cached configurations immediately.
129
+ // A later refreshConfigurations() re-scans the filesystem and reflects
130
+ // the deletion persistently.
131
+ this.cachedConfigurations = configs.filter((c) => c.name !== name);
132
+ logger?.debug(`Deleted subagent '${name}' at ${config.filePath}`);
133
+ return true;
134
+ }
110
135
  /**
111
136
  * Get cached configurations synchronously (must call loadConfigurations first)
112
137
  */
@@ -339,8 +364,13 @@ export class SubagentManager {
339
364
  const result = await this.internalExecute(instance, prompt, abortSignal);
340
365
  const task = backgroundTaskManager?.getTask(taskId);
341
366
  if (task) {
342
- task.status = "completed";
343
- task.stdout = result;
367
+ // internalExecute already recorded the terminal state; only fill
368
+ // in a "completed" status when the task was not concurrently
369
+ // killed (mirrors the catch branch below).
370
+ if (task.status !== "killed") {
371
+ task.status = "completed";
372
+ task.stdout = result;
373
+ }
344
374
  task.endTime = Date.now();
345
375
  task.runtime = task.endTime - startTime;
346
376
  }
@@ -348,9 +378,13 @@ export class SubagentManager {
348
378
  catch (error) {
349
379
  const task = backgroundTaskManager?.getTask(taskId);
350
380
  if (task) {
351
- task.status = "failed";
352
- task.stderr =
353
- error instanceof Error ? error.message : String(error);
381
+ // Preserve a "killed" terminal state (internalExecute already
382
+ // notified it) instead of flipping the in-map task to failed.
383
+ if (task.status !== "killed") {
384
+ task.status = "failed";
385
+ task.stderr =
386
+ error instanceof Error ? error.message : String(error);
387
+ }
354
388
  task.endTime = Date.now();
355
389
  task.runtime = task.endTime - startTime;
356
390
  }
@@ -480,8 +514,13 @@ export class SubagentManager {
480
514
  const task = backgroundTaskManager.getTask(instance.backgroundTaskId);
481
515
  if (task) {
482
516
  const wasAlreadyKilled = task.status === "killed";
483
- task.status = "completed";
484
- task.stdout = response || "Agent completed with no text response";
517
+ // Preserve a "killed" terminal state (stopTask already notified it)
518
+ // instead of overwriting it with "completed" mirrors shell exit
519
+ // semantics in BackgroundTaskManager.onExit.
520
+ if (!wasAlreadyKilled) {
521
+ task.status = "completed";
522
+ task.stdout = response || "Agent completed with no text response";
523
+ }
485
524
  task.endTime = Date.now();
486
525
  if (task.startTime) {
487
526
  task.runtime = task.endTime - task.startTime;
@@ -496,6 +535,11 @@ export class SubagentManager {
496
535
  messageQueue.enqueueNotification(`<task-notification>\n<task-id>${instance.backgroundTaskId}</task-id>\n<task-type>agent</task-type>\n<status>completed</status>\n<summary>${summary}</summary>\n</task-notification>`);
497
536
  }
498
537
  }
538
+ // Push a terminal snapshot: SubagentManager mutates the task object in
539
+ // place, so without an explicit notify the UI/hosts keep the last
540
+ // snapshot — the task created with status "running" — and the IDE
541
+ // background-task gate (new chat / history restore) stays locked.
542
+ backgroundTaskManager.notifyTasksChange();
499
543
  }
500
544
  }
501
545
  return response || "Agent completed with no text response";
@@ -512,12 +556,20 @@ export class SubagentManager {
512
556
  const task = backgroundTaskManager.getTask(instance.backgroundTaskId);
513
557
  if (task) {
514
558
  const wasAlreadyKilled = task.status === "killed";
515
- task.status = "failed";
516
- task.stderr = error instanceof Error ? error.message : String(error);
559
+ // Preserve a user/agent-initiated "killed" terminal state instead of
560
+ // overwriting it with "failed" (mirrors shell exit semantics) — the
561
+ // abort that stopped the task surfaces here as a thrown error.
562
+ if (!wasAlreadyKilled) {
563
+ task.status = "failed";
564
+ task.stderr =
565
+ error instanceof Error ? error.message : String(error);
566
+ }
517
567
  task.endTime = Date.now();
518
568
  if (task.startTime) {
519
569
  task.runtime = task.endTime - task.startTime;
520
570
  }
571
+ // Push a terminal snapshot (see the completion branch above).
572
+ backgroundTaskManager.notifyTasksChange();
521
573
  // Skip notification if task was already stopped (e.g. by main agent shutdown)
522
574
  if (!wasAlreadyKilled) {
523
575
  const messageQueue = this.container.has("MessageQueue")
@@ -207,6 +207,10 @@ export class WorkflowManager {
207
207
  task.status = "completed";
208
208
  task.endTime = Date.now();
209
209
  }
210
+ // Push the terminal snapshot: without it the last snapshot holds the
211
+ // "running" registration and UI consumers (background-task gate) never
212
+ // see the workflow finish.
213
+ this.backgroundTaskManager.notifyTasksChange();
210
214
  // Enqueue completion notification
211
215
  const journalPath = journal.filePath;
212
216
  this.messageQueue.enqueueNotification(taskNotificationToXml({
@@ -244,6 +248,8 @@ export class WorkflowManager {
244
248
  task.stderr = run.error || "";
245
249
  task.endTime = Date.now();
246
250
  }
251
+ // Push the terminal snapshot (see the completion branch above).
252
+ this.backgroundTaskManager.notifyTasksChange();
247
253
  this.messageQueue.enqueueNotification(taskNotificationToXml({
248
254
  type: "task_notification",
249
255
  taskId: runId,
@@ -1,7 +1,6 @@
1
1
  import { ToolPlugin } from "../tools/types.js";
2
2
  import type { WorktreeSession } from "../utils/worktreeSession.js";
3
3
  export declare const BASE_SYSTEM_PROMPT = "You are a coding agent developed by NetEase that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.";
4
- export declare const MODEL_IDENTITY_PROMPT = "# Model identity\n\nIf asked about your model, model version, underlying architecture, or product name, identify yourself simply as a coding agent developed by NetEase. Do not reveal the specific model ID, model family, model provider, or product name. Do not mention or hint at any of these details.";
5
4
  export declare const DOING_TASKS_PROMPT = "# Doing tasks\n- The user will primarily request you to perform software engineering tasks. These may include solving bugs, adding new functionality, refactoring code, explaining code, and more. When given an unclear or generic instruction, consider it in the context of these software engineering tasks and the current working directory. For example, if the user asks you to change \"methodName\" to snake case, do not reply with just \"method_name\", instead find the method in the code and modify the code.\n- You are highly capable and often allow users to complete ambitious tasks that would otherwise be too complex or take too long. You should defer to user judgement about whether a task is too large to attempt.\n- If you notice the user's request is based on a misconception, or spot a bug adjacent to what they asked about, say so. You're a collaborator, not just an executor\u2014users benefit from your judgment, not just your compliance.\n- In general, do not propose changes to code you haven't read. If a user asks about or wants you to modify a file, read it first. Understand existing code before suggesting modifications.\n- Do not create files unless they're absolutely necessary for achieving your goal. Generally prefer editing an existing file to creating a new one, as this prevents file bloat and builds on existing work more effectively.\n- If an approach fails, diagnose why before switching tactics\u2014read the error, check your assumptions, try a focused fix. Don't retry the identical action blindly, but don't abandon a viable approach after a single failure either. Escalate to the user with AskUserQuestion only when you're genuinely stuck after investigation, not as a first response to friction.\n- Be careful not to introduce security vulnerabilities such as command injection, XSS, SQL injection, and other OWASP top 10 vulnerabilities. If you notice that you wrote insecure code, immediately fix it. Prioritize writing safe, secure, and correct code.\n- Avoid over-engineering. Only make changes that are directly requested or clearly necessary. Keep solutions simple and focused.\n - Don't add features, refactor code, or make \"improvements\" beyond what was asked. A bug fix doesn't need surrounding code cleaned up. A simple feature doesn't need extra configurability. Don't add docstrings, comments, or type annotations to code you didn't change. Only add comments where the logic isn't self-evident.\n - Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees. Only validate at system boundaries (user input, external APIs). Don't use feature flags or backwards-compatibility shims when you can just change the code.\n - Don't create helpers, utilities, or abstractions for one-time operations. Don't design for hypothetical future requirements. The right amount of complexity is what the task actually requires\u2014no speculative abstractions, but no half-finished implementations either. Three similar lines of code is better than a premature abstraction.\n- Avoid backwards-compatibility hacks like renaming unused _vars, re-exporting types, adding // removed comments for removed code, etc. If you are certain that something is unused, you can delete it completely.\n- Report outcomes faithfully: if tests fail, say so with the relevant output; if you did not run a verification step, say that rather than implying it succeeded. Never claim \"all tests pass\" when output shows failures, never suppress or simplify failing checks (tests, lints, type errors) to manufacture a green result, and never characterize incomplete or broken work as done. Equally, when a check did pass or a task is complete, state it plainly \u2014 do not hedge confirmed results with unnecessary disclaimers, downgrade finished work to \"partial,\" or re-verify things you already checked. The goal is an accurate report, not a defensive one.\n- Before reporting a task complete, verify it actually works: run the test, execute the script, check the output. Minimum complexity means no gold-plating, not skipping the finish line. If you can't verify (no test exists, can't run the code), say so explicitly rather than claiming success.";
6
5
  export declare const EXECUTING_ACTIONS_PROMPT = "# Executing actions with care\n\nCarefully consider the reversibility and blast radius of actions. Generally you can freely take local, reversible actions like editing files or running tests. But for actions that are hard to reverse, affect shared systems beyond your local environment, or could otherwise be risky or destructive, check with the user before proceeding. The cost of pausing to confirm is low, while the cost of an unwanted action (lost work, unintended messages sent, deleted branches) can be very high. For actions like these, consider the context, the action, and user instructions, and by default transparently communicate the action and ask for confirmation before proceeding.\n\nExamples of the kind of risky actions that warrant user confirmation:\n- Destructive operations: deleting files/branches, dropping database tables, killing processes, rm -rf, overwriting uncommitted changes\n- Hard-to-reverse operations: force-pushing (can also overwrite upstream), git reset --hard, amending published commits, removing or downgrading packages/dependencies, modifying CI/CD pipelines\n- Actions visible to others or that affect shared state: pushing code, creating/closing/commenting on PRs or issues, sending messages (Slack, email, GitHub), posting to external services\n\nWhen you encounter an obstacle, do not use destructive actions as a shortcut to simply make it go away. For instance, try to identify root causes and fix underlying issues rather than bypassing safety checks (e.g. --no-verify). If you discover unexpected state like unfamiliar files, branches, or configuration, investigate before deleting or overwriting, as it may represent the user's in-progress work. For example, typically resolve merge conflicts rather than discarding changes. In short: only take risky actions carefully, and when in doubt, ask before acting. Follow both the spirit and letter of these instructions - measure twice, cut once.";
7
6
  export declare const TOOL_POLICY = "# Using your tools\n\n- Do NOT use the Bash to run commands when a relevant dedicated tool is provided. Using dedicated tools allows the user to better understand and review your work. This is CRITICAL to assisting the user:\n - To read files use Read instead of cat, head, tail, or sed\n - To edit files use Edit instead of sed or awk\n - To create files use Write instead of cat or echo redirection\n - To search for files use Glob instead of find or ls\n - To search the content of files, use Grep instead of grep or rg\n - Reserve using the Bash exclusively for system commands and terminal operations that require shell execution. If you are unsure and there is a relevant dedicated tool, default to using the dedicated tool and only fallback on using the Bash tool for these if it is absolutely necessary.\n- You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel. Maximize use of parallel tool calls where possible to increase efficiency.\n- However, if some tool calls depend on previous calls to inform dependent values, do NOT call these tools in parallel and instead call them sequentially. For instance, if one operation must complete before another starts, run these operations sequentially instead. Never use placeholders or guess missing parameters in tool calls.\n- If the user specifies that they want you to run tools \"in parallel\", you MUST send a single message with multiple tool use content blocks.";
@@ -4,9 +4,6 @@ import { buildAutoMemoryPrompt } from "./autoMemory.js";
4
4
  import { EXPLORE_SUBAGENT_TYPE, PLAN_SUBAGENT_TYPE, } from "../constants/subagents.js";
5
5
  import { ASK_USER_QUESTION_TOOL_NAME, EDIT_TOOL_NAME, WRITE_TOOL_NAME, EXIT_PLAN_MODE_TOOL_NAME, AGENT_TOOL_NAME, BASH_TOOL_NAME, READ_TOOL_NAME, GLOB_TOOL_NAME, GREP_TOOL_NAME, } from "../constants/tools.js";
6
6
  export const BASE_SYSTEM_PROMPT = `You are a coding agent developed by NetEase that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.`;
7
- export const MODEL_IDENTITY_PROMPT = `# Model identity
8
-
9
- If asked about your model, model version, underlying architecture, or product name, identify yourself simply as a coding agent developed by NetEase. Do not reveal the specific model ID, model family, model provider, or product name. Do not mention or hint at any of these details.`;
10
7
  export const DOING_TASKS_PROMPT = `# Doing tasks
11
8
  - The user will primarily request you to perform software engineering tasks. These may include solving bugs, adding new functionality, refactoring code, explaining code, and more. When given an unclear or generic instruction, consider it in the context of these software engineering tasks and the current working directory. For example, if the user asks you to change "methodName" to snake case, do not reply with just "method_name", instead find the method in the code and modify the code.
12
9
  - You are highly capable and often allow users to complete ambitious tasks that would otherwise be too complex or take too long. You should defer to user judgement about whether a task is too large to attempt.
@@ -332,7 +329,6 @@ export function buildSystemPrompt(basePrompt, tools, options = {}) {
332
329
  }
333
330
  staticText += `\n\n${OUTPUT_EFFICIENCY_PROMPT}`;
334
331
  staticText += `\n\n${TONE_AND_STYLE_PROMPT}`;
335
- staticText += `\n\n${MODEL_IDENTITY_PROMPT}`;
336
332
  const blocks = [{ text: staticText, cacheable: true }];
337
333
  // --- Dynamic block (not cacheable) ---
338
334
  let dynamicText = "";
@@ -3,6 +3,7 @@ import * as path from "path";
3
3
  import * as crypto from "crypto";
4
4
  import { getPluginsDir } from "../utils/configPaths.js";
5
5
  import { GitService } from "./GitService.js";
6
+ import { ALLOW_OFFICIAL_MARKET_GIT_FALLBACK, fetchOfficialMarketplaceFromMirror, } from "./officialMarketplaceMirror.js";
6
7
  import { ConfigurationService } from "./configurationService.js";
7
8
  import { logger, logError, logWarn } from "../utils/globalLogger.js";
8
9
  /**
@@ -510,25 +511,46 @@ export class MarketplaceService {
510
511
  const errors = [];
511
512
  for (const marketplace of toUpdate) {
512
513
  try {
513
- if (marketplace.source.source === "github" ||
514
- marketplace.source.source === "git") {
515
- if (!isGitAvailable) {
516
- logWarn(`Skipping update for Git/GitHub marketplace "${marketplace.name}" because Git is not installed.`);
517
- continue;
518
- }
514
+ // Builtin official marketplace: prefer the zip-snapshot mirror
515
+ // (content-addressed zip over a plain HTTP base, no git/GitHub
516
+ // needed). Only the builtin is special-cased by name — its
517
+ // `source` stays "github" in settings/cache, so there is zero
518
+ // data migration. On mirror failure (e.g. prod URL not yet live)
519
+ // fall back to the git path unless the kill switch forbids it.
520
+ const isOfficialBuiltin = marketplace.name === MarketplaceService.BUILTIN_MARKETPLACE.name;
521
+ let mirrorUpdated = false;
522
+ if (isOfficialBuiltin) {
519
523
  const targetPath = this.getMarketplacePath(marketplace.source);
520
- if (existsSync(targetPath)) {
521
- await this.gitService.pull(targetPath);
524
+ const mirrorSha = await fetchOfficialMarketplaceFromMirror(targetPath, this.marketplacesDir);
525
+ if (mirrorSha !== null) {
526
+ mirrorUpdated = true;
522
527
  }
523
- else {
524
- let url;
525
- if (marketplace.source.source === "github") {
526
- url = marketplace.source.repo;
528
+ else if (!ALLOW_OFFICIAL_MARKET_GIT_FALLBACK) {
529
+ logWarn(`Skipping update for official marketplace "${marketplace.name}": mirror fetch failed and git fallback is disabled.`);
530
+ continue;
531
+ }
532
+ }
533
+ if (marketplace.source.source === "github" ||
534
+ marketplace.source.source === "git") {
535
+ if (!mirrorUpdated) {
536
+ if (!isGitAvailable) {
537
+ logWarn(`Skipping update for Git/GitHub marketplace "${marketplace.name}" because Git is not installed.`);
538
+ continue;
539
+ }
540
+ const targetPath = this.getMarketplacePath(marketplace.source);
541
+ if (existsSync(targetPath)) {
542
+ await this.gitService.pull(targetPath);
527
543
  }
528
544
  else {
529
- url = marketplace.source.url;
545
+ let url;
546
+ if (marketplace.source.source === "github") {
547
+ url = marketplace.source.repo;
548
+ }
549
+ else {
550
+ url = marketplace.source.url;
551
+ }
552
+ await this.gitService.clone(url, targetPath, marketplace.source.ref);
530
553
  }
531
- await this.gitService.clone(url, targetPath, marketplace.source.ref);
532
554
  }
533
555
  }
534
556
  const manifest = await this.loadMarketplaceManifest(this.getMarketplacePath(marketplace.source));
@@ -4,6 +4,7 @@
4
4
  * Centralized service for loading, validating, and managing Wave configuration files.
5
5
  * Replaces distributed configuration logic previously embedded in hook.ts.
6
6
  */
7
+ import type { HookEvent, HookEventConfig } from "../types/hooks.js";
7
8
  import type { ConfigurationLoadResult, ValidationResult, ConfigurationPaths, WaveConfiguration, Scope, MarketplaceConfig } from "../types/configuration.js";
8
9
  import { type EnvironmentValidationResult, type MergedEnvironmentContext, type EnvironmentMergeOptions } from "../types/environment.js";
9
10
  import { GatewayConfig, ModelConfig, PermissionMode, AgentOptions } from "../types/index.js";
@@ -107,7 +108,7 @@ export declare class ConfigurationService {
107
108
  resolveLanguage(constructorLanguage?: string): string | undefined;
108
109
  /**
109
110
  * Resolves auto-memory enabled state with fallbacks
110
- * Resolution priority: settings.json > WAVE_DISABLE_AUTO_MEMORY > default (true)
111
+ * Resolution priority: session options (host settings-page value) > settings.json > WAVE_DISABLE_AUTO_MEMORY > default (true)
111
112
  * @returns Resolved auto-memory enabled state
112
113
  */
113
114
  resolveAutoMemoryEnabled(): boolean;
@@ -119,7 +120,7 @@ export declare class ConfigurationService {
119
120
  resolveWorktreeBaseRef(): "fresh" | "head";
120
121
  /**
121
122
  * Resolves auto-memory extraction frequency with fallbacks
122
- * Resolution priority: settings.json > WAVE_AUTO_MEMORY_FREQUENCY > default (1)
123
+ * Resolution priority: session options (host settings-page value) > settings.json > WAVE_AUTO_MEMORY_FREQUENCY > default (1)
123
124
  * @returns Resolved auto-memory extraction frequency (turns)
124
125
  */
125
126
  resolveAutoMemoryFrequency(): number;
@@ -160,6 +161,28 @@ export declare class ConfigurationService {
160
161
  * Update the enabled state of a plugin in the specified scope
161
162
  */
162
163
  updateEnabledPlugin(workdir: string, scope: Scope, pluginId: string, enabled: boolean): Promise<void>;
164
+ /**
165
+ * Resolve the settings.json path for a hook scope.
166
+ * user → ~/.wave/settings.json; project → <workdir>/.wave/settings.json
167
+ */
168
+ private resolveHookConfigPath;
169
+ /**
170
+ * Absolute path of the settings.json that holds hooks for `scope`
171
+ * (user → ~/.wave/settings.json, project → <workdir>/.wave/settings.json).
172
+ * Hosts surface it to the GUI so editors / file panels can open the file —
173
+ * they cannot expand a `~` prefix, so the path must already be absolute.
174
+ */
175
+ getHookConfigPath(workdir: string, scope: "user" | "project"): string;
176
+ /**
177
+ * Get hook configurations at a specific scope (user or project settings.json)
178
+ */
179
+ getHooksByScope(workdir: string, scope: Scope): Promise<Partial<Record<HookEvent, HookEventConfig[]>>>;
180
+ /**
181
+ * Delete a hook at a specific scope, identified by hookName of the form
182
+ * `Event:Matcher`. Removes the matching entry from the scope's settings.json
183
+ * hooks block.
184
+ */
185
+ deleteHook(workdir: string, scope: Scope, hookName: string): Promise<void>;
163
186
  /**
164
187
  * Get merged marketplaces from all scopes
165
188
  */
@@ -216,3 +239,12 @@ export declare function loadUserConfigEnv(): Record<string, string>;
216
239
  * Checks .local.json files first, then falls back to .json files
217
240
  */
218
241
  export declare function loadMergedWaveConfig(workdir: string): WaveConfiguration | null;
242
+ /**
243
+ * Parse a hookName of the form `Event:Matcher` (e.g. `PreToolUse:Write`)
244
+ * into its event and matcher parts. Hooks without a matcher use the event
245
+ * name alone (e.g. `SessionStart`).
246
+ */
247
+ export declare function parseHookName(hookName: string): {
248
+ event: HookEvent;
249
+ matcher: string;
250
+ };
@@ -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
  }
@@ -590,21 +590,26 @@ export class ConfigurationService {
590
590
  }
591
591
  /**
592
592
  * Resolves auto-memory enabled state with fallbacks
593
- * Resolution priority: settings.json > WAVE_DISABLE_AUTO_MEMORY > default (true)
593
+ * Resolution priority: session options (host settings-page value) > settings.json > WAVE_DISABLE_AUTO_MEMORY > default (true)
594
594
  * @returns Resolved auto-memory enabled state
595
595
  */
596
596
  resolveAutoMemoryEnabled() {
597
- // 1. settings.json (merged)
597
+ // 1. Per-session options override (hosts pass the settings-page toggle over
598
+ // stdio initialize/updateConfig; Agent.create → setOptions stores it)
599
+ if (this.options.autoMemoryEnabled !== undefined) {
600
+ return this.options.autoMemoryEnabled;
601
+ }
602
+ // 2. settings.json (merged)
598
603
  if (this.currentConfiguration?.autoMemoryEnabled !== undefined) {
599
604
  return this.currentConfiguration.autoMemoryEnabled;
600
605
  }
601
- // 2. WAVE_DISABLE_AUTO_MEMORY environment variable (settings snapshot > OS env)
606
+ // 3. WAVE_DISABLE_AUTO_MEMORY environment variable (settings snapshot > OS env)
602
607
  const disableAutoMemory = this.envSnapshot.WAVE_DISABLE_AUTO_MEMORY ??
603
608
  process.env.WAVE_DISABLE_AUTO_MEMORY;
604
609
  if (disableAutoMemory === "1" || disableAutoMemory === "true") {
605
610
  return false;
606
611
  }
607
- // 3. Default (true)
612
+ // 4. Default (true)
608
613
  return true;
609
614
  }
610
615
  /**
@@ -621,15 +626,23 @@ export class ConfigurationService {
621
626
  }
622
627
  /**
623
628
  * Resolves auto-memory extraction frequency with fallbacks
624
- * Resolution priority: settings.json > WAVE_AUTO_MEMORY_FREQUENCY > default (1)
629
+ * Resolution priority: session options (host settings-page value) > settings.json > WAVE_AUTO_MEMORY_FREQUENCY > default (1)
625
630
  * @returns Resolved auto-memory extraction frequency (turns)
626
631
  */
627
632
  resolveAutoMemoryFrequency() {
628
- // 1. settings.json (merged)
633
+ // 1. Per-session options override (hosts pass the settings-page value over
634
+ // stdio initialize/updateConfig; Agent.create → setOptions stores it).
635
+ // Only positive values are honored so a malformed 0/negative never
636
+ // degenerates into per-turn extraction.
637
+ if (this.options.autoMemoryFrequency !== undefined &&
638
+ this.options.autoMemoryFrequency > 0) {
639
+ return this.options.autoMemoryFrequency;
640
+ }
641
+ // 2. settings.json (merged)
629
642
  if (this.currentConfiguration?.autoMemoryFrequency !== undefined) {
630
643
  return this.currentConfiguration.autoMemoryFrequency;
631
644
  }
632
- // 2. WAVE_AUTO_MEMORY_FREQUENCY environment variable (settings snapshot > OS env)
645
+ // 3. WAVE_AUTO_MEMORY_FREQUENCY environment variable (settings snapshot > OS env)
633
646
  const envFrequency = this.envSnapshot.WAVE_AUTO_MEMORY_FREQUENCY ??
634
647
  process.env.WAVE_AUTO_MEMORY_FREQUENCY;
635
648
  if (envFrequency) {
@@ -638,7 +651,7 @@ export class ConfigurationService {
638
651
  return parsed;
639
652
  }
640
653
  }
641
- // 3. Default (1)
654
+ // 4. Default (1)
642
655
  return 1;
643
656
  }
644
657
  /**
@@ -840,6 +853,86 @@ export class ConfigurationService {
840
853
  config.enabledPlugins[pluginId] = enabled;
841
854
  await atomicWriteFile(configPath, JSON.stringify(config, null, 2));
842
855
  }
856
+ /**
857
+ * Resolve the settings.json path for a hook scope.
858
+ * user → ~/.wave/settings.json; project → <workdir>/.wave/settings.json
859
+ */
860
+ resolveHookConfigPath(workdir, scope) {
861
+ const configPath = scope === "user"
862
+ ? getUserConfigPaths()[0]
863
+ : getProjectConfigPaths(workdir)[1];
864
+ return { configPath, configDir: path.dirname(configPath) };
865
+ }
866
+ /**
867
+ * Absolute path of the settings.json that holds hooks for `scope`
868
+ * (user → ~/.wave/settings.json, project → <workdir>/.wave/settings.json).
869
+ * Hosts surface it to the GUI so editors / file panels can open the file —
870
+ * they cannot expand a `~` prefix, so the path must already be absolute.
871
+ */
872
+ getHookConfigPath(workdir, scope) {
873
+ return this.resolveHookConfigPath(workdir, scope).configPath;
874
+ }
875
+ /**
876
+ * Get hook configurations at a specific scope (user or project settings.json)
877
+ */
878
+ async getHooksByScope(workdir, scope) {
879
+ if (scope !== "user" && !existsSync(workdir)) {
880
+ return {};
881
+ }
882
+ const { configPath } = this.resolveHookConfigPath(workdir, scope);
883
+ if (!existsSync(configPath)) {
884
+ return {};
885
+ }
886
+ try {
887
+ const content = await fs.readFile(configPath, "utf-8");
888
+ const config = JSON.parse(content);
889
+ return config.hooks || {};
890
+ }
891
+ catch {
892
+ return {};
893
+ }
894
+ }
895
+ /**
896
+ * Delete a hook at a specific scope, identified by hookName of the form
897
+ * `Event:Matcher`. Removes the matching entry from the scope's settings.json
898
+ * hooks block.
899
+ */
900
+ async deleteHook(workdir, scope, hookName) {
901
+ if (scope !== "user" && !existsSync(workdir)) {
902
+ throw new Error(`Working directory does not exist: ${workdir}`);
903
+ }
904
+ const { configPath, configDir } = this.resolveHookConfigPath(workdir, scope);
905
+ if (!existsSync(configDir)) {
906
+ await fs.mkdir(configDir, { recursive: true });
907
+ }
908
+ let config = {};
909
+ if (existsSync(configPath)) {
910
+ try {
911
+ const content = await fs.readFile(configPath, "utf-8");
912
+ config = JSON.parse(content);
913
+ }
914
+ catch {
915
+ // Start with empty config if file is corrupted
916
+ }
917
+ }
918
+ const hooks = config.hooks || {};
919
+ const { event, matcher } = parseHookName(hookName);
920
+ if (!isValidHookEvent(event)) {
921
+ throw new Error(`Invalid hook event: ${event}`);
922
+ }
923
+ const eventConfigs = hooks[event];
924
+ if (eventConfigs) {
925
+ const remaining = eventConfigs.filter((cfg) => (cfg.matcher || "") !== (matcher || ""));
926
+ if (remaining.length > 0) {
927
+ hooks[event] = remaining;
928
+ }
929
+ else {
930
+ delete hooks[event];
931
+ }
932
+ config.hooks = hooks;
933
+ await atomicWriteFile(configPath, JSON.stringify(config, null, 2));
934
+ }
935
+ }
843
936
  /**
844
937
  * Get merged marketplaces from all scopes
845
938
  */
@@ -1195,10 +1288,9 @@ export function loadMergedWaveConfig(workdir) {
1195
1288
  ]),
1196
1289
  ];
1197
1290
  }
1198
- // Merge permissionMode (last one wins)
1199
- if (config.permissions.permissionMode !== undefined) {
1200
- mergedConfig.permissions.permissionMode =
1201
- config.permissions.permissionMode;
1291
+ // Merge defaultMode (last one wins)
1292
+ if (config.permissions.defaultMode !== undefined) {
1293
+ mergedConfig.permissions.defaultMode = config.permissions.defaultMode;
1202
1294
  }
1203
1295
  // Merge additionalDirectories
1204
1296
  if (config.permissions.additionalDirectories) {
@@ -1294,3 +1386,18 @@ export function loadMergedWaveConfig(workdir) {
1294
1386
  enableArtifact: mergedConfig.enableArtifact,
1295
1387
  };
1296
1388
  }
1389
+ /**
1390
+ * Parse a hookName of the form `Event:Matcher` (e.g. `PreToolUse:Write`)
1391
+ * into its event and matcher parts. Hooks without a matcher use the event
1392
+ * name alone (e.g. `SessionStart`).
1393
+ */
1394
+ export function parseHookName(hookName) {
1395
+ const separator = hookName.indexOf(":");
1396
+ if (separator === -1) {
1397
+ return { event: hookName, matcher: "" };
1398
+ }
1399
+ return {
1400
+ event: hookName.slice(0, separator),
1401
+ matcher: hookName.slice(separator + 1),
1402
+ };
1403
+ }
@@ -84,8 +84,8 @@ export class InitializationService {
84
84
  if (configResult.configuration.permissions.deny) {
85
85
  permissionManager.updateDeniedRules(configResult.configuration.permissions.deny);
86
86
  }
87
- if (configResult.configuration.permissions.permissionMode) {
88
- permissionManager.updateConfiguredPermissionMode(configResult.configuration.permissions.permissionMode);
87
+ if (configResult.configuration.permissions.defaultMode) {
88
+ permissionManager.updateConfiguredPermissionMode(configResult.configuration.permissions.defaultMode);
89
89
  }
90
90
  if (configResult.configuration.permissions.additionalDirectories) {
91
91
  permissionManager.updateAdditionalDirectories(configResult.configuration.permissions.additionalDirectories);
@@ -65,6 +65,20 @@ export declare class JsonlHandler {
65
65
  * Get the last message from JSONL file using efficient file reading (simplified)
66
66
  */
67
67
  getLastMessage(filePath: string): Promise<Message | null>;
68
+ /**
69
+ * Latest context-usage total for a session file.
70
+ *
71
+ * Sessions accumulate usage-less messages at the end — SessionStart hook
72
+ * meta messages are appended (and persisted) on every resume — so the last
73
+ * line alone cannot answer "how much context did this conversation use".
74
+ * Scan backwards over the file's tail window and take the newest message
75
+ * that carries usage.
76
+ *
77
+ * @param filePath - Path to the session JSONL file
78
+ * @returns total_tokens of the newest usage-bearing message, or 0 when the
79
+ * tail holds none (empty/unreadable file, no completed request yet)
80
+ */
81
+ getLatestTotalTokens(filePath: string): Promise<number>;
68
82
  /**
69
83
  * Read the creation-time metadata from the session file's header line.
70
84
  *