wave-agent-sdk 0.15.0 → 0.15.2

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 (144) hide show
  1. package/builtin/skills/loop/SKILL.md +29 -3
  2. package/dist/agent.d.ts +11 -2
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +44 -11
  5. package/dist/constants/tools.d.ts +3 -0
  6. package/dist/constants/tools.d.ts.map +1 -1
  7. package/dist/constants/tools.js +3 -0
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -0
  11. package/dist/managers/aiManager.d.ts +13 -1
  12. package/dist/managers/aiManager.d.ts.map +1 -1
  13. package/dist/managers/aiManager.js +69 -17
  14. package/dist/managers/hookManager.d.ts.map +1 -1
  15. package/dist/managers/hookManager.js +9 -0
  16. package/dist/managers/mcpManager.d.ts +4 -1
  17. package/dist/managers/mcpManager.d.ts.map +1 -1
  18. package/dist/managers/mcpManager.js +25 -5
  19. package/dist/managers/messageManager.d.ts.map +1 -1
  20. package/dist/managers/messageManager.js +7 -6
  21. package/dist/managers/permissionManager.d.ts +0 -2
  22. package/dist/managers/permissionManager.d.ts.map +1 -1
  23. package/dist/managers/permissionManager.js +0 -30
  24. package/dist/managers/slashCommandManager.d.ts +1 -0
  25. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  26. package/dist/managers/slashCommandManager.js +20 -4
  27. package/dist/managers/subagentManager.d.ts +6 -1
  28. package/dist/managers/subagentManager.d.ts.map +1 -1
  29. package/dist/managers/subagentManager.js +17 -18
  30. package/dist/managers/toolManager.d.ts +6 -0
  31. package/dist/managers/toolManager.d.ts.map +1 -1
  32. package/dist/managers/toolManager.js +41 -1
  33. package/dist/prompts/index.d.ts +1 -2
  34. package/dist/prompts/index.d.ts.map +1 -1
  35. package/dist/prompts/index.js +14 -6
  36. package/dist/services/initializationService.d.ts +0 -2
  37. package/dist/services/initializationService.d.ts.map +1 -1
  38. package/dist/services/initializationService.js +3 -35
  39. package/dist/services/jsonlHandler.d.ts +4 -4
  40. package/dist/services/jsonlHandler.d.ts.map +1 -1
  41. package/dist/services/jsonlHandler.js +4 -13
  42. package/dist/services/memory.d.ts +6 -0
  43. package/dist/services/memory.d.ts.map +1 -1
  44. package/dist/services/memory.js +27 -14
  45. package/dist/services/session.d.ts.map +1 -1
  46. package/dist/services/session.js +3 -12
  47. package/dist/tools/agentTool.d.ts.map +1 -1
  48. package/dist/tools/agentTool.js +16 -4
  49. package/dist/tools/bashTool.d.ts.map +1 -1
  50. package/dist/tools/bashTool.js +2 -5
  51. package/dist/tools/cronCreateTool.d.ts.map +1 -1
  52. package/dist/tools/cronCreateTool.js +71 -6
  53. package/dist/tools/cronDeleteTool.d.ts.map +1 -1
  54. package/dist/tools/cronDeleteTool.js +5 -1
  55. package/dist/tools/cronListTool.d.ts.map +1 -1
  56. package/dist/tools/cronListTool.js +5 -1
  57. package/dist/tools/enterWorktreeTool.d.ts +8 -0
  58. package/dist/tools/enterWorktreeTool.d.ts.map +1 -0
  59. package/dist/tools/enterWorktreeTool.js +144 -0
  60. package/dist/tools/exitWorktreeTool.d.ts +8 -0
  61. package/dist/tools/exitWorktreeTool.d.ts.map +1 -0
  62. package/dist/tools/exitWorktreeTool.js +184 -0
  63. package/dist/tools/skillTool.d.ts.map +1 -1
  64. package/dist/tools/skillTool.js +16 -4
  65. package/dist/tools/taskManagementTools.d.ts.map +1 -1
  66. package/dist/tools/taskManagementTools.js +4 -0
  67. package/dist/tools/toolSearchTool.d.ts +15 -0
  68. package/dist/tools/toolSearchTool.d.ts.map +1 -0
  69. package/dist/tools/toolSearchTool.js +185 -0
  70. package/dist/tools/types.d.ts +19 -0
  71. package/dist/tools/types.d.ts.map +1 -1
  72. package/dist/tools/webFetchTool.d.ts.map +1 -1
  73. package/dist/tools/webFetchTool.js +1 -0
  74. package/dist/types/agent.d.ts +6 -1
  75. package/dist/types/agent.d.ts.map +1 -1
  76. package/dist/types/hooks.d.ts +3 -1
  77. package/dist/types/hooks.d.ts.map +1 -1
  78. package/dist/types/hooks.js +1 -0
  79. package/dist/types/messaging.d.ts +1 -0
  80. package/dist/types/messaging.d.ts.map +1 -1
  81. package/dist/types/session.d.ts +0 -4
  82. package/dist/types/session.d.ts.map +1 -1
  83. package/dist/utils/containerSetup.d.ts.map +1 -1
  84. package/dist/utils/containerSetup.js +4 -6
  85. package/dist/utils/cronToHuman.d.ts +6 -0
  86. package/dist/utils/cronToHuman.d.ts.map +1 -0
  87. package/dist/utils/cronToHuman.js +79 -0
  88. package/dist/utils/isDeferredTool.d.ts +19 -0
  89. package/dist/utils/isDeferredTool.d.ts.map +1 -0
  90. package/dist/utils/isDeferredTool.js +31 -0
  91. package/dist/utils/mcpUtils.d.ts.map +1 -1
  92. package/dist/utils/mcpUtils.js +1 -0
  93. package/dist/utils/messageOperations.d.ts.map +1 -1
  94. package/dist/utils/messageOperations.js +5 -0
  95. package/dist/utils/parseCronExpression.d.ts +6 -0
  96. package/dist/utils/parseCronExpression.d.ts.map +1 -0
  97. package/dist/utils/parseCronExpression.js +74 -0
  98. package/dist/utils/worktreeSession.d.ts +26 -0
  99. package/dist/utils/worktreeSession.d.ts.map +1 -0
  100. package/dist/utils/worktreeSession.js +14 -0
  101. package/dist/utils/worktreeUtils.d.ts +42 -0
  102. package/dist/utils/worktreeUtils.d.ts.map +1 -0
  103. package/dist/utils/worktreeUtils.js +236 -0
  104. package/package.json +1 -1
  105. package/src/agent.ts +61 -12
  106. package/src/constants/tools.ts +3 -0
  107. package/src/index.ts +1 -0
  108. package/src/managers/aiManager.ts +73 -18
  109. package/src/managers/hookManager.ts +10 -0
  110. package/src/managers/mcpManager.ts +32 -6
  111. package/src/managers/messageManager.ts +7 -8
  112. package/src/managers/permissionManager.ts +0 -42
  113. package/src/managers/slashCommandManager.ts +30 -5
  114. package/src/managers/subagentManager.ts +28 -23
  115. package/src/managers/toolManager.ts +47 -1
  116. package/src/prompts/index.ts +17 -6
  117. package/src/services/initializationService.ts +2 -41
  118. package/src/services/jsonlHandler.ts +12 -24
  119. package/src/services/memory.ts +30 -17
  120. package/src/services/session.ts +3 -14
  121. package/src/tools/agentTool.ts +24 -5
  122. package/src/tools/bashTool.ts +2 -5
  123. package/src/tools/cronCreateTool.ts +81 -8
  124. package/src/tools/cronDeleteTool.ts +7 -2
  125. package/src/tools/cronListTool.ts +7 -2
  126. package/src/tools/enterWorktreeTool.ts +183 -0
  127. package/src/tools/exitWorktreeTool.ts +242 -0
  128. package/src/tools/skillTool.ts +24 -4
  129. package/src/tools/taskManagementTools.ts +4 -0
  130. package/src/tools/toolSearchTool.ts +228 -0
  131. package/src/tools/types.ts +19 -0
  132. package/src/tools/webFetchTool.ts +1 -0
  133. package/src/types/agent.ts +6 -0
  134. package/src/types/hooks.ts +4 -0
  135. package/src/types/messaging.ts +1 -0
  136. package/src/types/session.ts +0 -8
  137. package/src/utils/containerSetup.ts +7 -8
  138. package/src/utils/cronToHuman.ts +99 -0
  139. package/src/utils/isDeferredTool.ts +36 -0
  140. package/src/utils/mcpUtils.ts +1 -0
  141. package/src/utils/messageOperations.ts +5 -0
  142. package/src/utils/parseCronExpression.ts +78 -0
  143. package/src/utils/worktreeSession.ts +36 -0
  144. package/src/utils/worktreeUtils.ts +288 -0
@@ -0,0 +1,144 @@
1
+ /**
2
+ * EnterWorktree tool - creates an isolated git worktree and switches the session into it.
3
+ * Mirrors Claude Code's EnterWorktree tool behavior and prompt.
4
+ */
5
+ import { getCurrentWorktreeSession, setCurrentWorktreeSession, } from "../utils/worktreeSession.js";
6
+ import { createWorktree, validateWorktreeName, generateWorktreeName, } from "../utils/worktreeUtils.js";
7
+ import { getGitMainRepoRoot } from "../utils/gitUtils.js";
8
+ import { ENTER_WORKTREE_TOOL_NAME } from "../constants/tools.js";
9
+ import { logger } from "../utils/globalLogger.js";
10
+ export const ENTER_WORKTREE_TOOL_PROMPT = `Use this tool ONLY when the user explicitly asks to work in a worktree. This tool creates an isolated git worktree and switches the current session into it.
11
+
12
+ ## When to Use
13
+
14
+ - The user explicitly says "worktree" (e.g., "start a worktree", "work in a worktree", "create a worktree", "use a worktree")
15
+
16
+ ## When NOT to Use
17
+
18
+ - The user asks to create a branch, switch branches, or work on a different branch — use git commands instead
19
+ - The user asks to fix a bug or work on a feature — use normal git workflow unless they specifically mention worktrees
20
+ - Never use this tool unless the user explicitly mentions "worktree"
21
+
22
+ ## Requirements
23
+
24
+ - Must be in a git repository
25
+ - Must not already be in a worktree
26
+
27
+ ## Behavior
28
+
29
+ - Creates a new git worktree inside \`.wave/worktrees/\` with a new branch based on HEAD
30
+ - Switches the session's working directory to the new worktree
31
+ - Use ExitWorktree to leave the worktree mid-session (keep or remove). On session exit, if still in the worktree, the user will be prompted to keep or remove it
32
+
33
+ ## Parameters
34
+
35
+ - \`name\` (optional): A name for the worktree. Each "/"-separated segment may contain only letters, digits, dots, underscores, and dashes; max 64 chars total. A random name is generated if not provided.
36
+ `;
37
+ export const enterWorktreeTool = {
38
+ name: ENTER_WORKTREE_TOOL_NAME,
39
+ shouldDefer: true,
40
+ config: {
41
+ type: "function",
42
+ function: {
43
+ name: ENTER_WORKTREE_TOOL_NAME,
44
+ description: ENTER_WORKTREE_TOOL_PROMPT,
45
+ parameters: {
46
+ type: "object",
47
+ properties: {
48
+ name: {
49
+ type: "string",
50
+ description: 'Optional name for the worktree. Each "/"-separated segment may contain only letters, digits, dots, underscores, and dashes; max 64 chars total. A random name is generated if not provided.',
51
+ },
52
+ },
53
+ },
54
+ },
55
+ },
56
+ prompt: () => ENTER_WORKTREE_TOOL_PROMPT,
57
+ async execute(args, context) {
58
+ // Validate not already in a worktree created by this session
59
+ if (getCurrentWorktreeSession()) {
60
+ return {
61
+ success: false,
62
+ content: "Already in a worktree session. Use ExitWorktree to leave before creating a new one.",
63
+ error: "Already in a worktree session",
64
+ };
65
+ }
66
+ const name = args.name || generateWorktreeName();
67
+ // Validate the worktree name
68
+ try {
69
+ validateWorktreeName(name);
70
+ }
71
+ catch (e) {
72
+ return {
73
+ success: false,
74
+ content: `Invalid worktree name: ${e.message}`,
75
+ error: `Invalid worktree name: ${e.message}`,
76
+ };
77
+ }
78
+ // Resolve to main repo root so worktree creation works from within a subdirectory
79
+ const mainRepoRoot = getGitMainRepoRoot(context.workdir);
80
+ if (!mainRepoRoot) {
81
+ return {
82
+ success: false,
83
+ content: "Cannot create a worktree: not in a git repository. Configure WorktreeCreate and WorktreeRemove hooks in settings.json to use worktree isolation with other VCS systems.",
84
+ error: "Not in a git repository",
85
+ };
86
+ }
87
+ // Create the worktree (captures originalHeadCommit internally)
88
+ const worktreeInfo = createWorktree(name, mainRepoRoot);
89
+ // Build session state
90
+ const session = {
91
+ originalCwd: context.workdir,
92
+ worktreePath: worktreeInfo.path,
93
+ worktreeBranch: worktreeInfo.branch,
94
+ worktreeName: worktreeInfo.name,
95
+ isNew: worktreeInfo.isNew,
96
+ repoRoot: worktreeInfo.repoRoot,
97
+ originalHeadCommit: worktreeInfo.originalHeadCommit,
98
+ };
99
+ // Set module-level session state
100
+ setCurrentWorktreeSession(session);
101
+ // Update CWD via AIManager
102
+ const aiManager = context.aiManager;
103
+ if (aiManager) {
104
+ aiManager.setWorkdir(worktreeInfo.path);
105
+ }
106
+ // Also update the container's Workdir entry
107
+ // (Container is not directly accessible from ToolContext, but AIManager.setWorkdir
108
+ // handles both its internal field and process.chdir)
109
+ // Trigger WorktreeCreate hook if worktree is new
110
+ let hookTriggered = false;
111
+ if (session.isNew && context.hookManager) {
112
+ try {
113
+ const hookResults = await context.hookManager.executeHooks("WorktreeCreate", {
114
+ event: "WorktreeCreate",
115
+ projectDir: worktreeInfo.path,
116
+ timestamp: new Date(),
117
+ sessionId: context.sessionId ?? "",
118
+ transcriptPath: context.messageManager?.getTranscriptPath() ?? "",
119
+ cwd: worktreeInfo.path,
120
+ worktreeName: worktreeInfo.name,
121
+ env: Object.fromEntries(Object.entries(process.env).filter((e) => e[1] !== undefined)),
122
+ });
123
+ if (context.messageManager) {
124
+ context.hookManager.processHookResults("WorktreeCreate", hookResults, context.messageManager);
125
+ }
126
+ hookTriggered = true;
127
+ }
128
+ catch (error) {
129
+ // Non-blocking: log but don't fail the tool
130
+ logger?.warn("WorktreeCreate hooks execution failed:", error);
131
+ }
132
+ }
133
+ const branchInfo = worktreeInfo.branch
134
+ ? ` on branch ${worktreeInfo.branch}`
135
+ : "";
136
+ const hookInfo = hookTriggered
137
+ ? " WorktreeCreate hooks were executed."
138
+ : "";
139
+ return {
140
+ success: true,
141
+ content: `Created worktree at ${worktreeInfo.path}${branchInfo}. The session is now working in the worktree. Use ExitWorktree to leave mid-session, or exit the session to be prompted.${hookInfo}`,
142
+ };
143
+ },
144
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * ExitWorktree tool - exits a worktree session and returns to the original directory.
3
+ * Mirrors Claude Code's ExitWorktree tool behavior and prompt.
4
+ */
5
+ import type { ToolPlugin } from "./types.js";
6
+ export declare const EXIT_WORKTREE_TOOL_PROMPT = "Exit a worktree session created by EnterWorktree and return the session to the original working directory.\n\n## Scope\n\nThis tool ONLY operates on worktrees created by EnterWorktree in this session. It will NOT touch:\n- Worktrees you created manually with `git worktree add`\n- Worktrees from a previous session (even if created by EnterWorktree then)\n- The directory you're in if EnterWorktree was never called\n\nIf called outside an EnterWorktree session, the tool is a **no-op**: it reports that no worktree session is active and takes no action. Filesystem state is unchanged.\n\n## When to Use\n\n- The user explicitly asks to \"exit the worktree\", \"leave the worktree\", \"go back\", or otherwise end the worktree session\n- Do NOT call this proactively \u2014 only when the user asks\n\n## Parameters\n\n- `action` (required): `\"keep\"` or `\"remove\"`\n - `\"keep\"` \u2014 leave the worktree directory and branch intact on disk. Use this if the user wants to come back to the work later, or if there are changes to preserve.\n - `\"remove\"` \u2014 delete the worktree directory and its branch. Use this for a clean exit when the work is done or abandoned.\n- `discard_changes` (optional, default false): only meaningful with `action: \"remove\"`. If the worktree has uncommitted files or commits not on the original branch, the tool will REFUSE to remove it unless this is set to `true`. If the tool returns an error listing changes, confirm with the user before re-invoking with `discard_changes: true`.\n\n## Behavior\n\n- Restores the session's working directory to where it was before EnterWorktree\n- If action is \"remove\": deletes the worktree directory and branch\n- Once exited, EnterWorktree can be called again to create a fresh worktree\n";
7
+ export declare const exitWorktreeTool: ToolPlugin;
8
+ //# sourceMappingURL=exitWorktreeTool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exitWorktreeTool.d.ts","sourceRoot":"","sources":["../../src/tools/exitWorktreeTool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAA2B,MAAM,YAAY,CAAC;AAYtE,eAAO,MAAM,yBAAyB,gvDA4BrC,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,UAkM9B,CAAC"}
@@ -0,0 +1,184 @@
1
+ /**
2
+ * ExitWorktree tool - exits a worktree session and returns to the original directory.
3
+ * Mirrors Claude Code's ExitWorktree tool behavior and prompt.
4
+ */
5
+ import { getCurrentWorktreeSession, setCurrentWorktreeSession, } from "../utils/worktreeSession.js";
6
+ import { removeWorktree, countWorktreeChanges, } from "../utils/worktreeUtils.js";
7
+ import { EXIT_WORKTREE_TOOL_NAME } from "../constants/tools.js";
8
+ import { logger } from "../utils/globalLogger.js";
9
+ export const EXIT_WORKTREE_TOOL_PROMPT = `Exit a worktree session created by EnterWorktree and return the session to the original working directory.
10
+
11
+ ## Scope
12
+
13
+ This tool ONLY operates on worktrees created by EnterWorktree in this session. It will NOT touch:
14
+ - Worktrees you created manually with \`git worktree add\`
15
+ - Worktrees from a previous session (even if created by EnterWorktree then)
16
+ - The directory you're in if EnterWorktree was never called
17
+
18
+ If called outside an EnterWorktree session, the tool is a **no-op**: it reports that no worktree session is active and takes no action. Filesystem state is unchanged.
19
+
20
+ ## When to Use
21
+
22
+ - The user explicitly asks to "exit the worktree", "leave the worktree", "go back", or otherwise end the worktree session
23
+ - Do NOT call this proactively — only when the user asks
24
+
25
+ ## Parameters
26
+
27
+ - \`action\` (required): \`"keep"\` or \`"remove"\`
28
+ - \`"keep"\` — leave the worktree directory and branch intact on disk. Use this if the user wants to come back to the work later, or if there are changes to preserve.
29
+ - \`"remove"\` — delete the worktree directory and its branch. Use this for a clean exit when the work is done or abandoned.
30
+ - \`discard_changes\` (optional, default false): only meaningful with \`action: "remove"\`. If the worktree has uncommitted files or commits not on the original branch, the tool will REFUSE to remove it unless this is set to \`true\`. If the tool returns an error listing changes, confirm with the user before re-invoking with \`discard_changes: true\`.
31
+
32
+ ## Behavior
33
+
34
+ - Restores the session's working directory to where it was before EnterWorktree
35
+ - If action is "remove": deletes the worktree directory and branch
36
+ - Once exited, EnterWorktree can be called again to create a fresh worktree
37
+ `;
38
+ export const exitWorktreeTool = {
39
+ name: EXIT_WORKTREE_TOOL_NAME,
40
+ shouldDefer: true,
41
+ config: {
42
+ type: "function",
43
+ function: {
44
+ name: EXIT_WORKTREE_TOOL_NAME,
45
+ description: EXIT_WORKTREE_TOOL_PROMPT,
46
+ parameters: {
47
+ type: "object",
48
+ properties: {
49
+ action: {
50
+ type: "string",
51
+ enum: ["keep", "remove"],
52
+ description: '"keep" leaves the worktree and branch on disk; "remove" deletes both.',
53
+ },
54
+ discard_changes: {
55
+ type: "boolean",
56
+ description: 'Required true when action is "remove" and the worktree has uncommitted files or unmerged commits. The tool will refuse and list them otherwise.',
57
+ },
58
+ },
59
+ required: ["action"],
60
+ },
61
+ },
62
+ },
63
+ prompt: () => EXIT_WORKTREE_TOOL_PROMPT,
64
+ async execute(args, context) {
65
+ const action = args.action;
66
+ const discardChanges = args.discard_changes ?? false;
67
+ if (!action) {
68
+ return {
69
+ success: false,
70
+ content: 'Missing required parameter: "action" must be "keep" or "remove".',
71
+ error: 'Missing required parameter: "action"',
72
+ };
73
+ }
74
+ // Validate: must be in an active worktree session
75
+ const session = getCurrentWorktreeSession();
76
+ if (!session) {
77
+ return {
78
+ success: false,
79
+ content: "No-op: there is no active EnterWorktree session to exit. This tool only operates on worktrees created by EnterWorktree in the current session — it will not touch worktrees created manually or in a previous session. No filesystem changes were made.",
80
+ error: "No active worktree session",
81
+ };
82
+ }
83
+ // Safety check for removal with changes
84
+ if (action === "remove" && !discardChanges) {
85
+ const summary = countWorktreeChanges(session.worktreePath, session.originalHeadCommit);
86
+ if (summary === null) {
87
+ return {
88
+ success: false,
89
+ content: `Could not verify worktree state at ${session.worktreePath}. Refusing to remove without explicit confirmation. Re-invoke with discard_changes: true to proceed — or use action: "keep" to preserve the worktree.`,
90
+ error: "Could not verify worktree state",
91
+ };
92
+ }
93
+ const { changedFiles, commits } = summary;
94
+ if (changedFiles > 0 || commits > 0) {
95
+ const parts = [];
96
+ if (changedFiles > 0) {
97
+ parts.push(`${changedFiles} uncommitted ${changedFiles === 1 ? "file" : "files"}`);
98
+ }
99
+ if (commits > 0) {
100
+ parts.push(`${commits} ${commits === 1 ? "commit" : "commits"} on ${session.worktreeBranch ?? "the worktree branch"}`);
101
+ }
102
+ return {
103
+ success: false,
104
+ content: `Worktree has ${parts.join(" and ")}. Removing will discard this work permanently. Confirm with the user, then re-invoke with discard_changes: true — or use action: "keep" to preserve the worktree.`,
105
+ error: "Worktree has uncommitted changes",
106
+ };
107
+ }
108
+ }
109
+ // Capture info before clearing session
110
+ const { originalCwd, worktreePath, worktreeBranch } = session;
111
+ if (action === "keep") {
112
+ // Clear session state
113
+ setCurrentWorktreeSession(null);
114
+ // Restore CWD
115
+ const aiManager = context.aiManager;
116
+ if (aiManager) {
117
+ aiManager.setWorkdir(originalCwd);
118
+ }
119
+ return {
120
+ success: true,
121
+ content: `Exited worktree. Your work is preserved at ${worktreePath}${worktreeBranch ? ` on branch ${worktreeBranch}` : ""}. Session is now back in ${originalCwd}.`,
122
+ };
123
+ }
124
+ // action === "remove"
125
+ const worktreeInfo = {
126
+ name: session.worktreeName,
127
+ path: worktreePath,
128
+ branch: worktreeBranch,
129
+ repoRoot: session.repoRoot,
130
+ isNew: session.isNew,
131
+ };
132
+ // Count changes BEFORE removing the worktree (directory will be gone after)
133
+ const summary = countWorktreeChanges(worktreePath, session.originalHeadCommit) ?? { changedFiles: 0, commits: 0 };
134
+ removeWorktree(worktreeInfo);
135
+ // Clear session state
136
+ setCurrentWorktreeSession(null);
137
+ // Restore CWD
138
+ const aiManager = context.aiManager;
139
+ if (aiManager) {
140
+ aiManager.setWorkdir(originalCwd);
141
+ }
142
+ // Trigger WorktreeRemove hook (non-blocking)
143
+ let hookTriggered = false;
144
+ if (context.hookManager) {
145
+ try {
146
+ const hookResults = await context.hookManager.executeHooks("WorktreeRemove", {
147
+ event: "WorktreeRemove",
148
+ projectDir: originalCwd,
149
+ timestamp: new Date(),
150
+ sessionId: context.sessionId ?? "",
151
+ transcriptPath: context.messageManager?.getTranscriptPath() ?? "",
152
+ cwd: originalCwd,
153
+ worktreePath,
154
+ env: Object.fromEntries(Object.entries(process.env).filter((e) => e[1] !== undefined)),
155
+ });
156
+ if (context.messageManager) {
157
+ context.hookManager.processHookResults("WorktreeRemove", hookResults, context.messageManager);
158
+ }
159
+ hookTriggered = true;
160
+ }
161
+ catch (error) {
162
+ // Non-blocking: log but don't fail the tool
163
+ logger?.warn("WorktreeRemove hooks execution failed:", error);
164
+ }
165
+ }
166
+ const discardParts = [];
167
+ if (summary.commits > 0) {
168
+ discardParts.push(`${summary.commits} ${summary.commits === 1 ? "commit" : "commits"}`);
169
+ }
170
+ if (summary.changedFiles > 0) {
171
+ discardParts.push(`${summary.changedFiles} uncommitted ${summary.changedFiles === 1 ? "file" : "files"}`);
172
+ }
173
+ const discardNote = discardParts.length > 0
174
+ ? ` Discarded ${discardParts.join(" and ")}.`
175
+ : "";
176
+ const hookNote = hookTriggered
177
+ ? " WorktreeRemove hooks were executed."
178
+ : "";
179
+ return {
180
+ success: true,
181
+ content: `Exited and removed worktree at ${worktreePath}.${discardNote} Session is now back in ${originalCwd}.${hookNote}`,
182
+ };
183
+ },
184
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"skillTool.d.ts","sourceRoot":"","sources":["../../src/tools/skillTool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAA2B,MAAM,YAAY,CAAC;AAgBtE,eAAO,MAAM,SAAS,EAAE,UAmNvB,CAAC"}
1
+ {"version":3,"file":"skillTool.d.ts","sourceRoot":"","sources":["../../src/tools/skillTool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAA2B,MAAM,YAAY,CAAC;AAgBtE,eAAO,MAAM,SAAS,EAAE,UAuOvB,CAAC"}
@@ -109,17 +109,29 @@ export const skillTool = {
109
109
  // Update shortResult
110
110
  const messages = instance.messageManager.getMessages();
111
111
  const tokens = instance.messageManager.getLatestTotalTokens();
112
- const lastTools = instance.lastTools;
112
+ const usedTools = instance.usedTools;
113
113
  const toolCount = countToolBlocks(messages);
114
114
  const summary = formatToolTokenSummary(toolCount, tokens);
115
+ const getDisplayParam = (t) => {
116
+ if ((t.stage === "end" || t.stage === "running") &&
117
+ t.compactParams) {
118
+ return t.compactParams;
119
+ }
120
+ const flat = t.parameters.replace(/\n/g, "\\n");
121
+ return flat.length > 30 ? `…${flat.slice(-30)}` : flat;
122
+ };
115
123
  let shortResult = "";
116
124
  if (toolCount > 2) {
117
125
  shortResult += "... ";
118
126
  }
119
- if (lastTools.length > 0) {
120
- shortResult += `${lastTools.join(", ")} `;
121
- }
122
127
  shortResult += summary;
128
+ if (usedTools.length > 0) {
129
+ shortResult +=
130
+ "\n" +
131
+ usedTools
132
+ .map((t) => `${t.name} ${getDisplayParam(t)}`)
133
+ .join("\n");
134
+ }
123
135
  context.onShortResultUpdate?.(shortResult);
124
136
  });
125
137
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"taskManagementTools.d.ts","sourceRoot":"","sources":["../../src/tools/taskManagementTools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAA2B,MAAM,YAAY,CAAC;AASjE,eAAO,MAAM,cAAc,EAAE,UAoI5B,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,UAyDzB,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,UAgW5B,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,UAkE1B,CAAC"}
1
+ {"version":3,"file":"taskManagementTools.d.ts","sourceRoot":"","sources":["../../src/tools/taskManagementTools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAA2B,MAAM,YAAY,CAAC;AASjE,eAAO,MAAM,cAAc,EAAE,UAqI5B,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,UA0DzB,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,UAiW5B,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,UAmE1B,CAAC"}
@@ -1,6 +1,7 @@
1
1
  import { TASK_CREATE_TOOL_NAME, TASK_GET_TOOL_NAME, TASK_UPDATE_TOOL_NAME, TASK_LIST_TOOL_NAME, } from "../constants/tools.js";
2
2
  export const taskCreateTool = {
3
3
  name: TASK_CREATE_TOOL_NAME,
4
+ shouldDefer: true,
4
5
  config: {
5
6
  type: "function",
6
7
  function: {
@@ -123,6 +124,7 @@ NOTE that you should not use this tool if there is only one trivial task to do.
123
124
  };
124
125
  export const taskGetTool = {
125
126
  name: TASK_GET_TOOL_NAME,
127
+ shouldDefer: true,
126
128
  config: {
127
129
  type: "function",
128
130
  function: {
@@ -179,6 +181,7 @@ Returns full task details:
179
181
  };
180
182
  export const taskUpdateTool = {
181
183
  name: TASK_UPDATE_TOOL_NAME,
184
+ shouldDefer: true,
182
185
  config: {
183
186
  type: "function",
184
187
  function: {
@@ -478,6 +481,7 @@ Set up task dependencies:
478
481
  };
479
482
  export const taskListTool = {
480
483
  name: TASK_LIST_TOOL_NAME,
484
+ shouldDefer: true,
481
485
  config: {
482
486
  type: "function",
483
487
  function: {
@@ -0,0 +1,15 @@
1
+ /**
2
+ * ToolSearchTool - Discovers deferred tool schemas on demand.
3
+ *
4
+ * When tool deferral is enabled, deferred tools are not sent to the API.
5
+ * The model must call this tool to discover a deferred tool's full schema
6
+ * before it can invoke it.
7
+ *
8
+ * Query formats:
9
+ * - "select:ToolName" — direct selection by name (comma-separated for multiple)
10
+ * - "notebook jupyter" — keyword search, up to max_results best matches
11
+ * - "+slack send" — require "slack" in the name, rank by remaining terms
12
+ */
13
+ import { ToolPlugin } from "./types.js";
14
+ export declare const toolSearchTool: ToolPlugin;
15
+ //# sourceMappingURL=toolSearchTool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toolSearchTool.d.ts","sourceRoot":"","sources":["../../src/tools/toolSearchTool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,UAAU,EAA2B,MAAM,YAAY,CAAC;AA4GjE,eAAO,MAAM,cAAc,EAAE,UAsG5B,CAAC"}
@@ -0,0 +1,185 @@
1
+ /**
2
+ * ToolSearchTool - Discovers deferred tool schemas on demand.
3
+ *
4
+ * When tool deferral is enabled, deferred tools are not sent to the API.
5
+ * The model must call this tool to discover a deferred tool's full schema
6
+ * before it can invoke it.
7
+ *
8
+ * Query formats:
9
+ * - "select:ToolName" — direct selection by name (comma-separated for multiple)
10
+ * - "notebook jupyter" — keyword search, up to max_results best matches
11
+ * - "+slack send" — require "slack" in the name, rank by remaining terms
12
+ */
13
+ import { isDeferredTool, TOOL_SEARCH_TOOL_NAME, } from "../utils/isDeferredTool.js";
14
+ function formatSchema(tool) {
15
+ const desc = tool.config.function.description || "";
16
+ const params = JSON.stringify(tool.config.function.parameters || {}, null, 2);
17
+ return `${tool.name}: ${desc}\nParameters: ${params}`;
18
+ }
19
+ /**
20
+ * Parse tool name into searchable parts (handles CamelCase and underscores).
21
+ */
22
+ function parseToolName(name) {
23
+ return name
24
+ .replace(/([a-z])([A-Z])/g, "$1 $2") // CamelCase to spaces
25
+ .replace(/_/g, " ")
26
+ .toLowerCase()
27
+ .split(/\s+/)
28
+ .filter(Boolean);
29
+ }
30
+ /**
31
+ * Keyword search over deferred tools by name and description.
32
+ * Matches Claude Code's scoring: required terms (+prefix) must all match,
33
+ * optional terms contribute to ranking.
34
+ */
35
+ function keywordSearch(query, deferredTools, maxResults) {
36
+ const queryLower = query.toLowerCase().trim();
37
+ const queryTerms = queryLower.split(/\s+/).filter(Boolean);
38
+ // Exact match fast path
39
+ const exact = deferredTools.find((t) => t.name.toLowerCase() === queryLower);
40
+ if (exact)
41
+ return [exact];
42
+ // Partition into required (+prefixed) and optional terms
43
+ const requiredTerms = [];
44
+ const optionalTerms = [];
45
+ for (const term of queryTerms) {
46
+ if (term.startsWith("+") && term.length > 1) {
47
+ requiredTerms.push(term.slice(1));
48
+ }
49
+ else {
50
+ optionalTerms.push(term);
51
+ }
52
+ }
53
+ const allScoringTerms = requiredTerms.length > 0
54
+ ? [...requiredTerms, ...optionalTerms]
55
+ : queryTerms;
56
+ // Pre-filter to tools matching ALL required terms
57
+ let candidateTools = deferredTools;
58
+ if (requiredTerms.length > 0) {
59
+ candidateTools = deferredTools.filter((tool) => {
60
+ const parts = parseToolName(tool.name);
61
+ const desc = (tool.config.function.description || "").toLowerCase();
62
+ return requiredTerms.every((term) => parts.includes(term) ||
63
+ parts.some((p) => p.includes(term)) ||
64
+ desc.includes(term));
65
+ });
66
+ }
67
+ // Score each tool
68
+ const scored = candidateTools
69
+ .map((tool) => {
70
+ const parts = parseToolName(tool.name);
71
+ const desc = (tool.config.function.description || "").toLowerCase();
72
+ let score = 0;
73
+ for (const term of allScoringTerms) {
74
+ // Exact part match (high weight)
75
+ if (parts.includes(term)) {
76
+ score += tool.isMcp ? 12 : 10;
77
+ }
78
+ else if (parts.some((p) => p.includes(term))) {
79
+ score += tool.isMcp ? 6 : 5;
80
+ }
81
+ // Full name fallback
82
+ if (tool.name.toLowerCase().includes(term) && score === 0) {
83
+ score += 3;
84
+ }
85
+ // Description match
86
+ if (desc.includes(term)) {
87
+ score += 2;
88
+ }
89
+ }
90
+ return { tool, score };
91
+ })
92
+ .filter((s) => s.score > 0)
93
+ .sort((a, b) => b.score - a.score)
94
+ .slice(0, maxResults)
95
+ .map((s) => s.tool);
96
+ return scored;
97
+ }
98
+ export const toolSearchTool = {
99
+ name: TOOL_SEARCH_TOOL_NAME,
100
+ config: {
101
+ type: "function",
102
+ function: {
103
+ name: TOOL_SEARCH_TOOL_NAME,
104
+ description: `Fetches full schema definitions for deferred tools so they can be called.
105
+
106
+ Deferred tools appear by name in <available-deferred-tools> messages. Until fetched, only the name is known — there is no parameter schema, so the tool cannot be invoked. This tool takes a query, matches it against the deferred tool list, and returns the matched tools' complete JSONSchema definitions inside a <functions> block. Once a tool's schema appears in that result, it is callable exactly like any tool defined at the top of the prompt.
107
+
108
+ Result format: each matched tool appears as one <function>{"description": "...", "name": "...", "parameters": {...}}`,
109
+ },
110
+ },
111
+ shouldDefer: false, // Always available
112
+ execute: async (args, context) => {
113
+ const { query, max_results = 5 } = args;
114
+ if (!query) {
115
+ return {
116
+ success: false,
117
+ content: "",
118
+ error: "Missing required 'query' parameter",
119
+ };
120
+ }
121
+ if (!context.toolManager) {
122
+ return {
123
+ success: false,
124
+ content: "",
125
+ error: "ToolManager not available in context",
126
+ };
127
+ }
128
+ const allTools = context.toolManager.list();
129
+ const deferredTools = allTools.filter(isDeferredTool);
130
+ // Handle select: prefix
131
+ const selectMatch = query.match(/^select:(.+)$/i);
132
+ if (selectMatch) {
133
+ const requested = selectMatch[1]
134
+ .split(",")
135
+ .map((s) => s.trim())
136
+ .filter(Boolean);
137
+ const found = [];
138
+ const missing = [];
139
+ for (const toolName of requested) {
140
+ const tool = deferredTools.find((t) => t.name === toolName) ??
141
+ allTools.find((t) => t.name === toolName);
142
+ if (tool) {
143
+ if (!found.some((f) => f.name === tool.name))
144
+ found.push(tool);
145
+ }
146
+ else {
147
+ missing.push(toolName);
148
+ }
149
+ }
150
+ if (found.length === 0) {
151
+ return {
152
+ success: false,
153
+ content: "",
154
+ error: `No matching deferred tools found for: ${missing.join(", ")}`,
155
+ };
156
+ }
157
+ const result = found.map(formatSchema).join("\n\n---\n\n");
158
+ const shortResult = `Discovered tools: ${found.map((t) => t.name).join(", ")}`;
159
+ return {
160
+ success: true,
161
+ content: result,
162
+ shortResult,
163
+ };
164
+ }
165
+ // Keyword search
166
+ const matches = keywordSearch(query, deferredTools, max_results);
167
+ if (matches.length === 0) {
168
+ return {
169
+ success: false,
170
+ content: "",
171
+ error: `No matching deferred tools found for query: "${query}". Available deferred tools: ${getDeferredToolNamesList(deferredTools)}`,
172
+ };
173
+ }
174
+ const result = matches.map(formatSchema).join("\n\n---\n\n");
175
+ const shortResult = `Found ${matches.length} tools: ${matches.map((t) => t.name).join(", ")}`;
176
+ return {
177
+ success: true,
178
+ content: result,
179
+ shortResult,
180
+ };
181
+ },
182
+ };
183
+ function getDeferredToolNamesList(tools) {
184
+ return tools.map((t) => t.name).join(", ");
185
+ }
@@ -19,6 +19,21 @@ export interface ToolPlugin {
19
19
  workdir?: string;
20
20
  isSubagent?: boolean;
21
21
  }) => string;
22
+ /**
23
+ * When true, this tool is deferred — it's not sent to the API until the model
24
+ * discovers it via ToolSearch. MCP tools are always deferred.
25
+ */
26
+ shouldDefer?: boolean;
27
+ /**
28
+ * When true, this tool is never deferred — its full schema always appears in
29
+ * the initial prompt even when tool search is enabled.
30
+ */
31
+ alwaysLoad?: boolean;
32
+ /**
33
+ * When true, this is an MCP tool (auto-set by McpManager). MCP tools are
34
+ * always deferred unless they have alwaysLoad: true.
35
+ */
36
+ isMcp?: boolean;
22
37
  }
23
38
  export interface ToolResult {
24
39
  success: boolean;
@@ -38,6 +53,8 @@ export interface ToolContext {
38
53
  abortSignal?: AbortSignal;
39
54
  backgroundTaskManager?: import("../managers/backgroundTaskManager.js").BackgroundTaskManager;
40
55
  workdir: string;
56
+ /** Tool manager instance for tool discovery (used by ToolSearchTool) */
57
+ toolManager?: import("../managers/toolManager.js").ToolManager;
41
58
  /** Permission mode for this tool execution */
42
59
  permissionMode?: PermissionMode;
43
60
  /** Custom permission callback */
@@ -86,5 +103,7 @@ export interface ToolContext {
86
103
  mtime: number;
87
104
  hash: string;
88
105
  }>;
106
+ /** Hook manager instance for executing hooks */
107
+ hookManager?: import("../managers/hookManager.js").HookManager;
89
108
  }
90
109
  //# sourceMappingURL=types.d.ts.map