wave-agent-sdk 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/dist/agent.d.ts +23 -1
  2. package/dist/agent.js +34 -4
  3. package/dist/managers/aiManager.d.ts +36 -0
  4. package/dist/managers/aiManager.js +240 -8
  5. package/dist/managers/messageManager.d.ts +1 -3
  6. package/dist/managers/messageManager.js +19 -9
  7. package/dist/managers/slashCommandManager.js +28 -0
  8. package/dist/managers/subagentManager.d.ts +0 -2
  9. package/dist/services/session.js +2 -9
  10. package/dist/telemetry/instrumentation.d.ts +1 -1
  11. package/dist/telemetry/instrumentation.js +57 -28
  12. package/dist/tools/bashTool.js +16 -6
  13. package/dist/tools/grepTool.js +11 -3
  14. package/dist/types/index.d.ts +3 -0
  15. package/dist/types/messaging.d.ts +2 -0
  16. package/dist/utils/containerSetup.js +0 -3
  17. package/dist/utils/messageOperations.d.ts +10 -1
  18. package/dist/utils/messageOperations.js +19 -1
  19. package/dist/utils/notificationXml.js +5 -0
  20. package/dist/utils/ripgrep.d.ts +4 -4
  21. package/dist/utils/ripgrep.js +4 -27
  22. package/package.json +20 -8
  23. package/bin/rg +0 -79
  24. package/dist/managers/forkedAgentManager.d.ts +0 -50
  25. package/dist/managers/forkedAgentManager.js +0 -130
  26. package/scripts/install_ripgrep.js +0 -111
  27. package/scripts/postinstall.js +0 -38
  28. package/src/agent.ts +0 -1271
  29. package/src/constants/subagents.ts +0 -4
  30. package/src/constants/toolLimits.ts +0 -15
  31. package/src/constants/tools.ts +0 -24
  32. package/src/core/plugin.ts +0 -237
  33. package/src/core/session.ts +0 -9
  34. package/src/index.ts +0 -38
  35. package/src/managers/MemoryRuleManager.ts +0 -198
  36. package/src/managers/aiManager.ts +0 -2429
  37. package/src/managers/backgroundTaskManager.ts +0 -480
  38. package/src/managers/bangManager.ts +0 -119
  39. package/src/managers/cronManager.ts +0 -401
  40. package/src/managers/foregroundTaskManager.ts +0 -30
  41. package/src/managers/forkedAgentManager.ts +0 -216
  42. package/src/managers/hookManager.ts +0 -1089
  43. package/src/managers/liveConfigManager.ts +0 -445
  44. package/src/managers/lspManager.ts +0 -470
  45. package/src/managers/mcpManager.ts +0 -1010
  46. package/src/managers/messageManager.ts +0 -1180
  47. package/src/managers/messageQueue.ts +0 -157
  48. package/src/managers/permissionManager.ts +0 -1140
  49. package/src/managers/planManager.ts +0 -112
  50. package/src/managers/pluginManager.ts +0 -330
  51. package/src/managers/pluginScopeManager.ts +0 -124
  52. package/src/managers/reversionManager.ts +0 -178
  53. package/src/managers/skillManager.ts +0 -578
  54. package/src/managers/slashCommandManager.ts +0 -564
  55. package/src/managers/subagentManager.ts +0 -915
  56. package/src/managers/toolManager.ts +0 -489
  57. package/src/managers/workflowManager.ts +0 -493
  58. package/src/prompts/autoMemory.ts +0 -33
  59. package/src/prompts/autoMemoryExtraction.ts +0 -146
  60. package/src/prompts/index.ts +0 -506
  61. package/src/prompts/planModeReminders.ts +0 -126
  62. package/src/services/GitService.ts +0 -131
  63. package/src/services/MarketplaceService.ts +0 -1052
  64. package/src/services/MemoryRuleService.ts +0 -71
  65. package/src/services/aiService.ts +0 -919
  66. package/src/services/authService.ts +0 -565
  67. package/src/services/autoMemoryService.ts +0 -280
  68. package/src/services/configurationService.ts +0 -1520
  69. package/src/services/fileWatcher.ts +0 -319
  70. package/src/services/hook.ts +0 -348
  71. package/src/services/initializationService.ts +0 -357
  72. package/src/services/interactionService.ts +0 -229
  73. package/src/services/jsonlHandler.ts +0 -295
  74. package/src/services/memory.ts +0 -234
  75. package/src/services/pluginLoader.ts +0 -252
  76. package/src/services/remoteSettingsService.ts +0 -366
  77. package/src/services/reversionService.ts +0 -122
  78. package/src/services/session.ts +0 -922
  79. package/src/services/taskManager.ts +0 -321
  80. package/src/telemetry/events.ts +0 -65
  81. package/src/telemetry/instrumentation.ts +0 -499
  82. package/src/telemetry/sessionTracing.ts +0 -348
  83. package/src/tools/agentTool.ts +0 -322
  84. package/src/tools/askUserQuestion.ts +0 -160
  85. package/src/tools/bashTool.ts +0 -621
  86. package/src/tools/buildTool.ts +0 -61
  87. package/src/tools/cronCreateTool.ts +0 -161
  88. package/src/tools/cronDeleteTool.ts +0 -51
  89. package/src/tools/cronListTool.ts +0 -47
  90. package/src/tools/editTool.ts +0 -313
  91. package/src/tools/enterPlanMode.ts +0 -124
  92. package/src/tools/enterWorktreeTool.ts +0 -190
  93. package/src/tools/exitPlanMode.ts +0 -139
  94. package/src/tools/exitWorktreeTool.ts +0 -236
  95. package/src/tools/globTool.ts +0 -167
  96. package/src/tools/grepTool.ts +0 -399
  97. package/src/tools/lspTool.ts +0 -883
  98. package/src/tools/readTool.ts +0 -426
  99. package/src/tools/skillTool.ts +0 -248
  100. package/src/tools/taskManagementTools.ts +0 -574
  101. package/src/tools/taskStopTool.ts +0 -78
  102. package/src/tools/types.ts +0 -137
  103. package/src/tools/webFetchTool.ts +0 -373
  104. package/src/tools/workflowTool.ts +0 -205
  105. package/src/tools/writeTool.ts +0 -267
  106. package/src/types/agent.ts +0 -128
  107. package/src/types/auth.ts +0 -19
  108. package/src/types/commands.ts +0 -30
  109. package/src/types/config.ts +0 -43
  110. package/src/types/configuration.ts +0 -168
  111. package/src/types/core.ts +0 -105
  112. package/src/types/cron.ts +0 -13
  113. package/src/types/environment.ts +0 -106
  114. package/src/types/fileSearch.ts +0 -4
  115. package/src/types/history.ts +0 -7
  116. package/src/types/hooks.ts +0 -256
  117. package/src/types/index.ts +0 -41
  118. package/src/types/lsp.ts +0 -98
  119. package/src/types/marketplace.ts +0 -71
  120. package/src/types/mcp.ts +0 -43
  121. package/src/types/memoryRule.ts +0 -31
  122. package/src/types/messaging.ts +0 -114
  123. package/src/types/permissions.ts +0 -80
  124. package/src/types/plugins.ts +0 -39
  125. package/src/types/processes.ts +0 -99
  126. package/src/types/reversion.ts +0 -29
  127. package/src/types/session.ts +0 -12
  128. package/src/types/skills.ts +0 -108
  129. package/src/types/tasks.ts +0 -13
  130. package/src/types/telemetry.ts +0 -98
  131. package/src/types/tools.ts +0 -44
  132. package/src/types/workflow.ts +0 -6
  133. package/src/utils/abortUtils.ts +0 -118
  134. package/src/utils/atomicWrite.ts +0 -61
  135. package/src/utils/bashParser.ts +0 -899
  136. package/src/utils/cacheControlUtils.ts +0 -426
  137. package/src/utils/commandArgumentParser.ts +0 -161
  138. package/src/utils/commandPathResolver.ts +0 -88
  139. package/src/utils/configPaths.ts +0 -220
  140. package/src/utils/configValidator.ts +0 -100
  141. package/src/utils/constants.ts +0 -39
  142. package/src/utils/container.ts +0 -92
  143. package/src/utils/containerSetup.ts +0 -371
  144. package/src/utils/convertMessagesForAPI.ts +0 -326
  145. package/src/utils/cronTasks.ts +0 -128
  146. package/src/utils/cronTasksLock.ts +0 -182
  147. package/src/utils/cronToHuman.ts +0 -99
  148. package/src/utils/customCommands.ts +0 -95
  149. package/src/utils/editUtils.ts +0 -20
  150. package/src/utils/fileFormat.ts +0 -40
  151. package/src/utils/fileSearch.ts +0 -151
  152. package/src/utils/fileUtils.ts +0 -223
  153. package/src/utils/gitUtils.ts +0 -315
  154. package/src/utils/globalLogger.ts +0 -128
  155. package/src/utils/groupMessagesByApiRound.ts +0 -121
  156. package/src/utils/hookMatcher.ts +0 -176
  157. package/src/utils/markdownParser.ts +0 -317
  158. package/src/utils/mcpUtils.ts +0 -138
  159. package/src/utils/messageOperations.ts +0 -641
  160. package/src/utils/modelCapabilities.ts +0 -30
  161. package/src/utils/nameGenerator.ts +0 -99
  162. package/src/utils/notificationXml.ts +0 -57
  163. package/src/utils/openaiClient.ts +0 -251
  164. package/src/utils/parseCronExpression.ts +0 -78
  165. package/src/utils/path.ts +0 -72
  166. package/src/utils/pathEncoder.ts +0 -400
  167. package/src/utils/pathSafety.ts +0 -40
  168. package/src/utils/promptHistory.ts +0 -168
  169. package/src/utils/ripgrep.ts +0 -35
  170. package/src/utils/shellResolver.ts +0 -190
  171. package/src/utils/skillParser.ts +0 -246
  172. package/src/utils/stringUtils.ts +0 -156
  173. package/src/utils/subagentParser.ts +0 -279
  174. package/src/utils/taskReminder.ts +0 -96
  175. package/src/utils/tokenCalculation.ts +0 -43
  176. package/src/utils/tokenEstimate.ts +0 -34
  177. package/src/utils/toolResultStorage.ts +0 -117
  178. package/src/utils/worktreeSession.ts +0 -26
  179. package/src/utils/worktreeUtils.ts +0 -786
  180. package/src/workflow/budgetTracker.ts +0 -34
  181. package/src/workflow/concurrencyLimiter.ts +0 -47
  182. package/src/workflow/journal.ts +0 -95
  183. package/src/workflow/progressReporter.ts +0 -141
  184. package/src/workflow/runState.ts +0 -65
  185. package/src/workflow/scriptRuntime.ts +0 -274
  186. package/src/workflow/structuredOutput.ts +0 -123
  187. package/src/workflow/types.ts +0 -101
  188. package/src/workflow/workflowApis.ts +0 -410
  189. package/vendor/ripgrep/linux-aarch64/rg +0 -0
  190. package/vendor/ripgrep/linux-x86_64/rg +0 -0
  191. package/vendor/ripgrep/macos-aarch64/rg +0 -0
  192. package/vendor/ripgrep/macos-x86_64/rg +0 -0
  193. package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
  194. package/vendor/ripgrep/windows-x86_64/rg.exe +0 -0
@@ -1,161 +0,0 @@
1
- import { ToolPlugin, ToolResult, ToolContext } from "./types.js";
2
- import { CRON_CREATE_TOOL_NAME } from "../constants/tools.js";
3
- import { cronToHuman } from "../utils/cronToHuman.js";
4
- import { parseCronExpression } from "../utils/parseCronExpression.js";
5
-
6
- const DEFAULT_MAX_AGE_DAYS = 7;
7
- const MAX_JOBS = 50;
8
-
9
- const CRON_CREATE_DESCRIPTION = `Schedule a prompt to run at a future time within this Wave session — either recurring on a cron schedule, or once at a specific time.`;
10
-
11
- const CRON_CREATE_PROMPT = `Schedule a prompt to be enqueued at a future time. Use for both recurring schedules and one-shot reminders.
12
-
13
- Uses standard 5-field cron in the user's local timezone: minute hour day-of-month month day-of-week. "0 9 * * *" means 9am local — no timezone conversion needed.
14
-
15
- ## One-shot tasks (recurring: false)
16
-
17
- For "remind me at X" or "at <time>, do Y" requests — fire once then auto-delete.
18
- Pin minute/hour/day-of-month/month to specific values:
19
- "remind me at 2:30pm today to check the deploy" → cron: "30 14 <today_dom> <today_month> *", recurring: false
20
- "tomorrow morning, run the smoke test" → cron: "57 8 <tomorrow_dom> <tomorrow_month> *", recurring: false
21
-
22
- ## Recurring jobs (recurring: true, the default)
23
-
24
- For "every N minutes" / "every hour" / "weekdays at 9am" requests:
25
- "*/5 * * * *" (every 5 min), "0 * * * *" (hourly), "0 9 * * 1-5" (weekdays at 9am local)
26
-
27
- ## Avoid the :00 and :30 minute marks when the task allows it
28
-
29
- Every user who asks for "9am" gets \`0 9\`, and every user who asks for "hourly" gets \`0 *\` — which means requests from across the planet land on the API at the same instant. When the user's request is approximate, pick a minute that is NOT 0 or 30:
30
- "every morning around 9" → "57 8 * * *" or "3 9 * * *" (not "0 9 * * *")
31
- "hourly" → "7 * * * *" (not "0 * * * *")
32
- "in an hour or so, remind me to..." → pick whatever minute you land on, don't round
33
-
34
- Only use minute 0 or 30 when the user names that exact time and clearly means it ("at 9:00 sharp", "at half past", coordinating with a meeting). When in doubt, nudge a few minutes early or late — the user will not notice, and the fleet will.
35
-
36
- ## Session-only vs durable
37
-
38
- By default, jobs live only in this Wave session — nothing is written to disk, and the job is gone when Wave exits.
39
- Set \`durable: true\` to persist the job to \`.wave/scheduled_tasks.json\` so it survives Wave restarts (requires the session to hold the scheduler lock).
40
-
41
- ## Runtime behavior
42
-
43
- Jobs only fire while the REPL is idle (not mid-query). The scheduler adds a small deterministic jitter on top of whatever you pick: recurring tasks fire up to 10% of their period late (max 15 min); one-shot tasks landing on :00 or :30 fire up to 90s early. Picking an off-minute is still the bigger lever.
44
-
45
- Recurring tasks auto-expire after ${DEFAULT_MAX_AGE_DAYS} days — they fire one final time, then are deleted. This bounds session lifetime. Tell the user about the ${DEFAULT_MAX_AGE_DAYS}-day limit when scheduling recurring jobs.
46
-
47
- Returns a job ID you can pass to CronDelete.`;
48
-
49
- export const cronCreateTool: ToolPlugin = {
50
- name: CRON_CREATE_TOOL_NAME,
51
- config: {
52
- type: "function",
53
- function: {
54
- name: CRON_CREATE_TOOL_NAME,
55
- description: CRON_CREATE_DESCRIPTION,
56
- parameters: {
57
- type: "object",
58
- properties: {
59
- cron: {
60
- type: "string",
61
- description:
62
- 'Standard 5-field cron expression in local time: "M H DoM Mon DoW" (e.g. "*/5 * * * *" = every 5 minutes, "30 14 28 2 *" = Feb 28 at 2:30pm local once).',
63
- },
64
- prompt: {
65
- type: "string",
66
- description: "The prompt to enqueue at each fire time.",
67
- },
68
- recurring: {
69
- type: "boolean",
70
- description: `true (default) = fire on every cron match until deleted or auto-expired after ${DEFAULT_MAX_AGE_DAYS} days. false = fire once at the next match, then auto-delete. Use false for "remind me at X" one-shot requests with pinned minute/hour/dom/month.`,
71
- default: true,
72
- },
73
- durable: {
74
- type: "boolean",
75
- description:
76
- "true = persist job to .wave/scheduled_tasks.json so it survives Wave restarts. false (default) = session-only, removed when Wave exits.",
77
- default: false,
78
- },
79
- },
80
- required: ["cron", "prompt"],
81
- },
82
- },
83
- },
84
- prompt: () => CRON_CREATE_PROMPT,
85
- execute: async (
86
- args: Record<string, unknown>,
87
- context: ToolContext,
88
- ): Promise<ToolResult> => {
89
- const {
90
- cron,
91
- prompt,
92
- recurring = true,
93
- durable = false,
94
- } = args as {
95
- cron: string;
96
- prompt: string;
97
- recurring?: boolean;
98
- durable?: boolean;
99
- };
100
-
101
- if (!context.cronManager) {
102
- return {
103
- success: false,
104
- content: "",
105
- error: "CronManager not available",
106
- };
107
- }
108
-
109
- // Validate cron expression
110
- if (!parseCronExpression(cron)) {
111
- return {
112
- success: false,
113
- content: "",
114
- error: `Invalid cron expression '${cron}'. Expected 5 fields: M H DoM Mon DoW.`,
115
- };
116
- }
117
-
118
- // Check max jobs limit
119
- const existingJobs = context.cronManager.listJobs();
120
- if (existingJobs.length >= MAX_JOBS) {
121
- return {
122
- success: false,
123
- content: "",
124
- error: `Too many scheduled jobs (max ${MAX_JOBS}). Cancel one first.`,
125
- };
126
- }
127
-
128
- try {
129
- const job = context.cronManager.createJob({
130
- cron,
131
- prompt,
132
- recurring,
133
- durable,
134
- });
135
-
136
- const humanSchedule = cronToHuman(cron);
137
- const where = durable
138
- ? "Persisted to .wave/scheduled_tasks.json"
139
- : "Session-only (not written to disk, dies when Wave exits)";
140
- const resultMessage = recurring
141
- ? `Scheduled recurring job ${job.id} (${humanSchedule}). ${where}. Auto-expires after ${DEFAULT_MAX_AGE_DAYS} days. Use CronDelete to cancel sooner.`
142
- : `Scheduled one-shot task ${job.id} (${humanSchedule}). ${where}. It will fire once then auto-delete.`;
143
-
144
- return {
145
- success: true,
146
- content: JSON.stringify(
147
- { id: job.id, humanSchedule, recurring, durable },
148
- null,
149
- 2,
150
- ),
151
- shortResult: resultMessage,
152
- };
153
- } catch (error) {
154
- return {
155
- success: false,
156
- content: "",
157
- error: error instanceof Error ? error.message : String(error),
158
- };
159
- }
160
- },
161
- };
@@ -1,51 +0,0 @@
1
- import { ToolPlugin, ToolResult, ToolContext } from "./types.js";
2
- import { CRON_DELETE_TOOL_NAME } from "../constants/tools.js";
3
-
4
- const CRON_DELETE_DESCRIPTION = "Cancel a scheduled cron job by ID";
5
-
6
- const CRON_DELETE_PROMPT = `Cancel a cron job previously scheduled with CronCreate. Removes it from both the in-memory session store and durable file storage (if applicable).`;
7
-
8
- export const cronDeleteTool: ToolPlugin = {
9
- name: CRON_DELETE_TOOL_NAME,
10
- config: {
11
- type: "function",
12
- function: {
13
- name: CRON_DELETE_TOOL_NAME,
14
- description: CRON_DELETE_DESCRIPTION,
15
- parameters: {
16
- type: "object",
17
- properties: {
18
- id: {
19
- type: "string",
20
- description: "Job ID returned by CronCreate",
21
- },
22
- },
23
- required: ["id"],
24
- },
25
- },
26
- },
27
- prompt: () => CRON_DELETE_PROMPT,
28
- execute: async (
29
- args: Record<string, unknown>,
30
- context: ToolContext,
31
- ): Promise<ToolResult> => {
32
- const { id } = args as { id: string };
33
-
34
- if (!context.cronManager) {
35
- return {
36
- success: false,
37
- content: "",
38
- error: "CronManager not available",
39
- };
40
- }
41
-
42
- const success = context.cronManager.deleteJob(id);
43
-
44
- return {
45
- success,
46
- content: JSON.stringify({ success }, null, 2),
47
- shortResult: success ? `Deleted job ${id}` : `Job ${id} not found`,
48
- error: success ? undefined : `Job ${id} not found`,
49
- };
50
- },
51
- };
@@ -1,47 +0,0 @@
1
- import { ToolPlugin, ToolResult, ToolContext } from "./types.js";
2
- import { CRON_LIST_TOOL_NAME } from "../constants/tools.js";
3
-
4
- const CRON_LIST_DESCRIPTION = "List scheduled cron jobs";
5
-
6
- const CRON_LIST_PROMPT = `List all cron jobs scheduled via CronCreate in this session.`;
7
-
8
- export const cronListTool: ToolPlugin = {
9
- name: CRON_LIST_TOOL_NAME,
10
- config: {
11
- type: "function",
12
- function: {
13
- name: CRON_LIST_TOOL_NAME,
14
- description: CRON_LIST_DESCRIPTION,
15
- parameters: {
16
- type: "object",
17
- properties: {},
18
- },
19
- },
20
- },
21
- prompt: () => CRON_LIST_PROMPT,
22
- execute: async (
23
- _args: Record<string, unknown>,
24
- context: ToolContext,
25
- ): Promise<ToolResult> => {
26
- if (!context.cronManager) {
27
- return {
28
- success: false,
29
- content: "",
30
- error: "CronManager not available",
31
- };
32
- }
33
-
34
- const jobs = context.cronManager.listJobs();
35
-
36
- const annotatedJobs = jobs.map((job) => ({
37
- ...job,
38
- scope: job.durable ? "durable" : "session-only",
39
- }));
40
-
41
- return {
42
- success: true,
43
- content: JSON.stringify({ jobs: annotatedJobs }, null, 2),
44
- shortResult: `Found ${jobs.length} jobs`,
45
- };
46
- },
47
- };
@@ -1,313 +0,0 @@
1
- import { readFile, writeFile, stat } from "fs/promises";
2
- import { createHash } from "crypto";
3
- import { logger } from "../utils/globalLogger.js";
4
- import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
5
- import { resolvePath, getDisplayPath } from "../utils/path.js";
6
- import { escapeRegExp, analyzeEditMismatch } from "../utils/editUtils.js";
7
- import { EDIT_TOOL_NAME, READ_TOOL_NAME } from "../constants/tools.js";
8
-
9
- /**
10
- * Format compact parameter display
11
- */
12
- function formatCompactParams(
13
- args: Record<string, unknown>,
14
- context: ToolContext,
15
- ): string {
16
- const filePath = args.file_path as string;
17
- return getDisplayPath(filePath || "", context.workdir);
18
- }
19
-
20
- /**
21
- * Single file edit tool plugin
22
- */
23
- export const editTool: ToolPlugin = {
24
- name: EDIT_TOOL_NAME,
25
- isConcurrencySafe: false,
26
- formatCompactParams,
27
- prompt: () =>
28
- `Performs exact string replacements in files.
29
-
30
- Usage:
31
- - You must use your \`${READ_TOOL_NAME}\` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
32
- - When editing text from ${READ_TOOL_NAME} tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.
33
- - ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
34
- - Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
35
- - Use the smallest \`old_string\` that's clearly unique — usually 2-4 adjacent lines is sufficient. Avoid including 10+ lines of context when less uniquely identifies the target. Shorter matches are less likely to contain reproduction errors.
36
- - The edit will FAIL if \`old_string\` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use \`replace_all\` to change every instance of \`old_string\`.
37
- - Use \`replace_all\` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.`,
38
- config: {
39
- type: "function",
40
- function: {
41
- name: EDIT_TOOL_NAME,
42
- description: "A tool for editing files",
43
- parameters: {
44
- type: "object",
45
- properties: {
46
- file_path: {
47
- type: "string",
48
- description: "The absolute path to the file to modify",
49
- },
50
- old_string: {
51
- type: "string",
52
- description: "The text to replace",
53
- },
54
- new_string: {
55
- type: "string",
56
- description:
57
- "The text to replace it with (must be different from old_string)",
58
- },
59
- replace_all: {
60
- type: "boolean",
61
- default: false,
62
- description: "Replace all occurences of old_string (default false)",
63
- },
64
- },
65
- required: ["file_path", "old_string", "new_string"],
66
- additionalProperties: false,
67
- },
68
- },
69
- },
70
- execute: async (
71
- args: Record<string, unknown>,
72
- context: ToolContext,
73
- ): Promise<ToolResult> => {
74
- const filePath = args.file_path as string;
75
- const oldString = args.old_string as string;
76
- const newString = args.new_string as string;
77
- const replaceAll = (args.replace_all as boolean) || false;
78
-
79
- // Validate required parameters
80
- if (!filePath || typeof filePath !== "string") {
81
- return {
82
- success: false,
83
- content: "",
84
- error: "file_path parameter is required and must be a string",
85
- };
86
- }
87
-
88
- if (typeof oldString !== "string") {
89
- return {
90
- success: false,
91
- content: "",
92
- error: "old_string parameter is required and must be a string",
93
- };
94
- }
95
-
96
- if (typeof newString !== "string") {
97
- return {
98
- success: false,
99
- content: "",
100
- error: "new_string parameter is required and must be a string",
101
- };
102
- }
103
-
104
- if (oldString === newString) {
105
- return {
106
- success: false,
107
- content: "",
108
- error: "old_string and new_string must be different",
109
- };
110
- }
111
-
112
- // Trigger conditional rule loading for this file
113
- context.messageManager?.triggerFileRead(filePath);
114
-
115
- // Enforce read-before-edit: the file must have been read or written first.
116
- // readFileState is populated by Read, Write, and Edit tools — single source
117
- // of truth, aligned with Claude Code's readFileState approach. Skipped in
118
- // plan mode: permissionManager enforces a plan-file-only gate whose denial
119
- // message must surface instead of being masked by a read-state rejection.
120
- const resolvedPath = resolvePath(filePath, context.workdir);
121
- if (
122
- context.permissionMode !== "plan" &&
123
- !context.readFileState?.has(resolvedPath)
124
- ) {
125
- return {
126
- success: false,
127
- content: "",
128
- error: `You must read the file with the ${READ_TOOL_NAME} tool before editing it. Use ${READ_TOOL_NAME} on ${filePath} first.`,
129
- };
130
- }
131
-
132
- try {
133
- // Read file content
134
- let originalContent: string;
135
- try {
136
- originalContent = await readFile(resolvedPath, "utf-8");
137
- } catch (readError) {
138
- return {
139
- success: false,
140
- content: "",
141
- error: `Failed to read file: ${readError instanceof Error ? readError.message : String(readError)}`,
142
- };
143
- }
144
-
145
- // Staleness check (aligned with Claude Code): only flag when the file got
146
- // newer since last read. For full reads, a content-hash fallback avoids
147
- // false positives when mtime changed but content didn't (git checkout,
148
- // editor round-trip save, cloud sync, antivirus). Partial reads get no
149
- // fallback since only a slice was cached. Skipped in plan mode (see
150
- // read-before-edit note above) so the plan-file-only denial wins.
151
- if (context.permissionMode !== "plan" && context.readFileState) {
152
- const state = context.readFileState.get(resolvedPath);
153
- if (state) {
154
- const currentStats = await stat(resolvedPath);
155
- if (currentStats.mtime.getTime() > state.mtime) {
156
- const isFullRead =
157
- state.offset === undefined && state.limit === undefined;
158
- const contentUnchanged =
159
- isFullRead &&
160
- createHash("sha256").update(originalContent).digest("hex") ===
161
- state.hash;
162
- if (!contentUnchanged) {
163
- return {
164
- success: false,
165
- content: "",
166
- error:
167
- "File has been unexpectedly modified since last read. Read it again before editing it.",
168
- };
169
- }
170
- }
171
- }
172
- }
173
-
174
- // Normalize line endings for matching
175
- const normalizedContent = originalContent.replace(/\r\n/g, "\n");
176
- const normalizedOldString = oldString.replace(/\r\n/g, "\n");
177
-
178
- // Check if old_string exists
179
- const index = normalizedContent.indexOf(normalizedOldString);
180
- const matchedOldString = index !== -1 ? normalizedOldString : null;
181
- const startLineNumber =
182
- index !== -1
183
- ? normalizedContent.substring(0, index).split("\n").length
184
- : undefined;
185
-
186
- if (!matchedOldString) {
187
- return {
188
- success: false,
189
- content: "",
190
- error: analyzeEditMismatch(normalizedOldString),
191
- };
192
- }
193
-
194
- let newContent: string;
195
- let replacementCount: number;
196
-
197
- if (replaceAll) {
198
- // Replace all matches
199
- const regex = new RegExp(escapeRegExp(matchedOldString), "g");
200
- newContent = normalizedContent.replace(regex, newString);
201
- replacementCount = (normalizedContent.match(regex) || []).length;
202
- } else {
203
- // Replace only the first match, but first check if it's unique
204
- const matches = normalizedContent.split(matchedOldString).length - 1;
205
- if (matches > 1) {
206
- return {
207
- success: false,
208
- content: "",
209
- error: `old_string appears ${matches} times in the file. Either provide a larger string with more surrounding context to make it unique or use replace_all=true to change every instance.`,
210
- };
211
- }
212
-
213
- newContent = normalizedContent.replace(matchedOldString, newString);
214
- replacementCount = 1;
215
- }
216
-
217
- // Permission check after validation but before real operation
218
- if (context.permissionManager) {
219
- try {
220
- const permissionContext = context.permissionManager.createContext(
221
- EDIT_TOOL_NAME,
222
- context.permissionMode || "default",
223
- context.canUseToolCallback,
224
- {
225
- file_path: filePath,
226
- old_string: oldString,
227
- new_string: newString,
228
- replace_all: replaceAll,
229
- startLineNumber,
230
- },
231
- context.toolCallId,
232
- );
233
- const permissionResult =
234
- await context.permissionManager.checkPermission(permissionContext);
235
-
236
- if (permissionResult.behavior === "deny") {
237
- return {
238
- success: false,
239
- content: "",
240
- error: `${EDIT_TOOL_NAME} operation denied by user, reason: ${permissionResult.message || "No reason provided"}`,
241
- };
242
- }
243
- } catch {
244
- return {
245
- success: false,
246
- content: "",
247
- error: "Permission check failed",
248
- };
249
- }
250
- }
251
-
252
- // Record snapshot for reversion
253
- let snapshotId: string | undefined;
254
- if (context.reversionManager && context.messageId) {
255
- snapshotId = await context.reversionManager.recordSnapshot(
256
- context.messageId,
257
- resolvedPath,
258
- "modify",
259
- );
260
- }
261
-
262
- // Write file
263
- try {
264
- await writeFile(resolvedPath, newContent, "utf-8");
265
- // Commit snapshot on success
266
- if (context.reversionManager && snapshotId) {
267
- await context.reversionManager.commitSnapshot(snapshotId);
268
- }
269
- } catch (writeError) {
270
- return {
271
- success: false,
272
- content: "",
273
- error: `Failed to write file: ${writeError instanceof Error ? writeError.message : String(writeError)}`,
274
- };
275
- }
276
-
277
- // Update readFileState so subsequent serialized edits see fresh state
278
- if (context.readFileState) {
279
- const newStats = await stat(resolvedPath);
280
- const hash = createHash("sha256").update(newContent).digest("hex");
281
- context.readFileState.set(resolvedPath, {
282
- mtime: newStats.mtime.getTime(),
283
- hash,
284
- offset: undefined,
285
- limit: undefined,
286
- });
287
- }
288
-
289
- const shortResult = replaceAll
290
- ? `Replaced ${replacementCount} instances`
291
- : "Text replaced successfully";
292
-
293
- logger.debug(`Edit tool: ${shortResult}`);
294
-
295
- return {
296
- success: true,
297
- content: shortResult,
298
- shortResult,
299
- filePath: resolvedPath,
300
- startLineNumber,
301
- };
302
- } catch (error) {
303
- const errorMessage =
304
- error instanceof Error ? error.message : String(error);
305
- logger.error(`Edit tool error: ${errorMessage}`);
306
- return {
307
- success: false,
308
- content: "",
309
- error: errorMessage,
310
- };
311
- }
312
- },
313
- };
@@ -1,124 +0,0 @@
1
- import { logger } from "../utils/globalLogger.js";
2
- import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
3
- import { ENTER_PLAN_MODE_TOOL_NAME } from "../constants/tools.js";
4
- import { OPERATION_CANCELLED_BY_USER } from "../types/permissions.js";
5
-
6
- /**
7
- * Enter Plan Mode Tool Plugin
8
- */
9
- export const enterPlanModeTool: ToolPlugin = {
10
- name: ENTER_PLAN_MODE_TOOL_NAME,
11
- config: {
12
- type: "function",
13
- function: {
14
- name: ENTER_PLAN_MODE_TOOL_NAME,
15
- description:
16
- "Request to enter plan mode for complex tasks requiring user approval before coding",
17
- parameters: {
18
- type: "object",
19
- properties: {},
20
- required: [],
21
- additionalProperties: false,
22
- },
23
- },
24
- },
25
- prompt:
26
- () => `Use this tool to proactively request entering plan mode when a task is non-trivial and benefits from planning before implementation.
27
-
28
- ## When to Use This Tool
29
- - Multi-file changes or refactoring that requires architectural decisions
30
- - New features with multiple implementation steps or design trade-offs
31
- - Tasks where you want user approval on approach before writing code
32
- - Complex bug fixes that span multiple components
33
-
34
- ## When NOT to Use This Tool
35
- - Simple fixes (typos, small logic changes in a single file)
36
- - Research tasks (searching files, reading code, gathering information)
37
- - Tasks where the implementation approach is straightforward and unambiguous
38
- - When already in plan mode
39
-
40
- ## Examples
41
-
42
- 1. Task: "Refactor the authentication system to use OAuth2" - Use EnterPlanMode to propose an approach before implementation.
43
- 2. Task: "Add input validation to the login form" - Do NOT use EnterPlanMode, just implement it.
44
- 3. Task: "Migrate the database schema to support multi-tenancy" - Use EnterPlanMode to plan the migration steps.
45
- 4. Task: "Fix the off-by-one error in the pagination logic" - Do NOT use EnterPlanMode, just fix the bug.
46
- `,
47
- execute: async (
48
- _args: Record<string, unknown>,
49
- context: ToolContext,
50
- ): Promise<ToolResult> => {
51
- try {
52
- // Runtime guard: tool is always visible but only works outside plan mode
53
- if (context.permissionMode === "plan") {
54
- return {
55
- success: false,
56
- content: "Already in plan mode. Use ExitPlanMode to exit.",
57
- error: "Already in plan mode",
58
- };
59
- }
60
-
61
- if (!context.permissionManager) {
62
- return {
63
- success: false,
64
- content: "",
65
- error: "Permission manager is not available",
66
- };
67
- }
68
-
69
- const permissionContext = context.permissionManager.createContext(
70
- ENTER_PLAN_MODE_TOOL_NAME,
71
- context.permissionMode || "default",
72
- context.canUseToolCallback,
73
- {},
74
- context.toolCallId,
75
- );
76
-
77
- // No "allow always" option for plan mode transitions (matching Claude Code behavior)
78
- permissionContext.hidePersistentOption = true;
79
-
80
- const permissionResult =
81
- await context.permissionManager.checkPermission(permissionContext);
82
-
83
- if (permissionResult.behavior === "deny") {
84
- if (permissionResult.message === OPERATION_CANCELLED_BY_USER) {
85
- return {
86
- success: false,
87
- content: OPERATION_CANCELLED_BY_USER,
88
- };
89
- }
90
- return {
91
- success: false,
92
- content: `User declined to enter plan mode. Proceed in current mode.`,
93
- error: permissionResult.message
94
- ? undefined
95
- : "Operation declined by user",
96
- };
97
- }
98
-
99
- // Ensure the full plan mode transition happens (planManager + UI callback).
100
- // In bypass mode the canUseTool callback is not invoked, so the transition
101
- // never occurs. In normal mode, the callback already handled it
102
- // (permissionResult.newPermissionMode will be set).
103
- if (!permissionResult.newPermissionMode && context.toolManager) {
104
- context.toolManager.requestPermissionModeChange("plan");
105
- }
106
-
107
- return {
108
- success: true,
109
- content:
110
- "Plan mode entered successfully. Please write your plan to the plan file and use ExitPlanMode when ready for approval.",
111
- shortResult: "Plan mode entered",
112
- };
113
- } catch (error) {
114
- const errorMessage =
115
- error instanceof Error ? error.message : String(error);
116
- logger.error(`EnterPlanMode tool error: ${errorMessage}`);
117
- return {
118
- success: false,
119
- content: "",
120
- error: errorMessage,
121
- };
122
- }
123
- },
124
- };