whale-code 6.4.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 (319) hide show
  1. package/README.md +95 -0
  2. package/bin/swag-agent.js +9 -0
  3. package/bin/swagmanager-mcp.js +321 -0
  4. package/dist/cli/app.d.ts +26 -0
  5. package/dist/cli/app.js +64 -0
  6. package/dist/cli/chat/AgentSelector.d.ts +14 -0
  7. package/dist/cli/chat/AgentSelector.js +14 -0
  8. package/dist/cli/chat/ChatApp.d.ts +9 -0
  9. package/dist/cli/chat/ChatApp.js +267 -0
  10. package/dist/cli/chat/ChatInput.d.ts +39 -0
  11. package/dist/cli/chat/ChatInput.js +509 -0
  12. package/dist/cli/chat/MarkdownText.d.ts +10 -0
  13. package/dist/cli/chat/MarkdownText.js +20 -0
  14. package/dist/cli/chat/MessageList.d.ts +37 -0
  15. package/dist/cli/chat/MessageList.js +80 -0
  16. package/dist/cli/chat/ModelSelector.d.ts +20 -0
  17. package/dist/cli/chat/ModelSelector.js +73 -0
  18. package/dist/cli/chat/RewindViewer.d.ts +26 -0
  19. package/dist/cli/chat/RewindViewer.js +185 -0
  20. package/dist/cli/chat/StoreSelector.d.ts +14 -0
  21. package/dist/cli/chat/StoreSelector.js +24 -0
  22. package/dist/cli/chat/StreamingText.d.ts +12 -0
  23. package/dist/cli/chat/StreamingText.js +12 -0
  24. package/dist/cli/chat/SubagentPanel.d.ts +45 -0
  25. package/dist/cli/chat/SubagentPanel.js +110 -0
  26. package/dist/cli/chat/TeamPanel.d.ts +21 -0
  27. package/dist/cli/chat/TeamPanel.js +42 -0
  28. package/dist/cli/chat/ToolIndicator.d.ts +25 -0
  29. package/dist/cli/chat/ToolIndicator.js +436 -0
  30. package/dist/cli/chat/hooks/useAgentLoop.d.ts +39 -0
  31. package/dist/cli/chat/hooks/useAgentLoop.js +382 -0
  32. package/dist/cli/chat/hooks/useSlashCommands.d.ts +37 -0
  33. package/dist/cli/chat/hooks/useSlashCommands.js +387 -0
  34. package/dist/cli/commands/config-cmd.d.ts +10 -0
  35. package/dist/cli/commands/config-cmd.js +99 -0
  36. package/dist/cli/commands/doctor.d.ts +14 -0
  37. package/dist/cli/commands/doctor.js +172 -0
  38. package/dist/cli/commands/init.d.ts +16 -0
  39. package/dist/cli/commands/init.js +278 -0
  40. package/dist/cli/commands/mcp.d.ts +12 -0
  41. package/dist/cli/commands/mcp.js +162 -0
  42. package/dist/cli/login/LoginApp.d.ts +7 -0
  43. package/dist/cli/login/LoginApp.js +157 -0
  44. package/dist/cli/print-mode.d.ts +31 -0
  45. package/dist/cli/print-mode.js +202 -0
  46. package/dist/cli/serve-mode.d.ts +37 -0
  47. package/dist/cli/serve-mode.js +636 -0
  48. package/dist/cli/services/agent-definitions.d.ts +25 -0
  49. package/dist/cli/services/agent-definitions.js +91 -0
  50. package/dist/cli/services/agent-events.d.ts +178 -0
  51. package/dist/cli/services/agent-events.js +175 -0
  52. package/dist/cli/services/agent-loop.d.ts +90 -0
  53. package/dist/cli/services/agent-loop.js +762 -0
  54. package/dist/cli/services/agent-worker-base.d.ts +97 -0
  55. package/dist/cli/services/agent-worker-base.js +220 -0
  56. package/dist/cli/services/auth-service.d.ts +30 -0
  57. package/dist/cli/services/auth-service.js +160 -0
  58. package/dist/cli/services/background-processes.d.ts +126 -0
  59. package/dist/cli/services/background-processes.js +318 -0
  60. package/dist/cli/services/browser-auth.d.ts +24 -0
  61. package/dist/cli/services/browser-auth.js +180 -0
  62. package/dist/cli/services/claude-md-loader.d.ts +16 -0
  63. package/dist/cli/services/claude-md-loader.js +58 -0
  64. package/dist/cli/services/config-store.d.ts +47 -0
  65. package/dist/cli/services/config-store.js +79 -0
  66. package/dist/cli/services/debug-log.d.ts +10 -0
  67. package/dist/cli/services/debug-log.js +52 -0
  68. package/dist/cli/services/error-logger.d.ts +58 -0
  69. package/dist/cli/services/error-logger.js +269 -0
  70. package/dist/cli/services/file-history.d.ts +21 -0
  71. package/dist/cli/services/file-history.js +83 -0
  72. package/dist/cli/services/format-server-response.d.ts +16 -0
  73. package/dist/cli/services/format-server-response.js +440 -0
  74. package/dist/cli/services/git-context.d.ts +11 -0
  75. package/dist/cli/services/git-context.js +66 -0
  76. package/dist/cli/services/hooks.d.ts +85 -0
  77. package/dist/cli/services/hooks.js +258 -0
  78. package/dist/cli/services/interactive-tools.d.ts +125 -0
  79. package/dist/cli/services/interactive-tools.js +260 -0
  80. package/dist/cli/services/keybinding-manager.d.ts +52 -0
  81. package/dist/cli/services/keybinding-manager.js +115 -0
  82. package/dist/cli/services/local-tools.d.ts +22 -0
  83. package/dist/cli/services/local-tools.js +697 -0
  84. package/dist/cli/services/lsp-manager.d.ts +18 -0
  85. package/dist/cli/services/lsp-manager.js +717 -0
  86. package/dist/cli/services/mcp-client.d.ts +48 -0
  87. package/dist/cli/services/mcp-client.js +157 -0
  88. package/dist/cli/services/memory-manager.d.ts +16 -0
  89. package/dist/cli/services/memory-manager.js +57 -0
  90. package/dist/cli/services/model-manager.d.ts +18 -0
  91. package/dist/cli/services/model-manager.js +71 -0
  92. package/dist/cli/services/model-router.d.ts +26 -0
  93. package/dist/cli/services/model-router.js +149 -0
  94. package/dist/cli/services/permission-modes.d.ts +13 -0
  95. package/dist/cli/services/permission-modes.js +43 -0
  96. package/dist/cli/services/rewind.d.ts +84 -0
  97. package/dist/cli/services/rewind.js +194 -0
  98. package/dist/cli/services/ripgrep.d.ts +28 -0
  99. package/dist/cli/services/ripgrep.js +138 -0
  100. package/dist/cli/services/sandbox.d.ts +29 -0
  101. package/dist/cli/services/sandbox.js +97 -0
  102. package/dist/cli/services/server-tools.d.ts +61 -0
  103. package/dist/cli/services/server-tools.js +543 -0
  104. package/dist/cli/services/session-persistence.d.ts +23 -0
  105. package/dist/cli/services/session-persistence.js +99 -0
  106. package/dist/cli/services/subagent-worker.d.ts +19 -0
  107. package/dist/cli/services/subagent-worker.js +41 -0
  108. package/dist/cli/services/subagent.d.ts +47 -0
  109. package/dist/cli/services/subagent.js +647 -0
  110. package/dist/cli/services/system-prompt.d.ts +7 -0
  111. package/dist/cli/services/system-prompt.js +198 -0
  112. package/dist/cli/services/team-lead.d.ts +73 -0
  113. package/dist/cli/services/team-lead.js +512 -0
  114. package/dist/cli/services/team-state.d.ts +77 -0
  115. package/dist/cli/services/team-state.js +398 -0
  116. package/dist/cli/services/teammate.d.ts +31 -0
  117. package/dist/cli/services/teammate.js +689 -0
  118. package/dist/cli/services/telemetry.d.ts +61 -0
  119. package/dist/cli/services/telemetry.js +209 -0
  120. package/dist/cli/services/tools/agent-tools.d.ts +14 -0
  121. package/dist/cli/services/tools/agent-tools.js +347 -0
  122. package/dist/cli/services/tools/file-ops.d.ts +15 -0
  123. package/dist/cli/services/tools/file-ops.js +487 -0
  124. package/dist/cli/services/tools/search-tools.d.ts +8 -0
  125. package/dist/cli/services/tools/search-tools.js +186 -0
  126. package/dist/cli/services/tools/shell-exec.d.ts +10 -0
  127. package/dist/cli/services/tools/shell-exec.js +168 -0
  128. package/dist/cli/services/tools/task-manager.d.ts +28 -0
  129. package/dist/cli/services/tools/task-manager.js +209 -0
  130. package/dist/cli/services/tools/web-tools.d.ts +11 -0
  131. package/dist/cli/services/tools/web-tools.js +395 -0
  132. package/dist/cli/setup/SetupApp.d.ts +9 -0
  133. package/dist/cli/setup/SetupApp.js +191 -0
  134. package/dist/cli/shared/MatrixIntro.d.ts +4 -0
  135. package/dist/cli/shared/MatrixIntro.js +83 -0
  136. package/dist/cli/shared/Theme.d.ts +74 -0
  137. package/dist/cli/shared/Theme.js +127 -0
  138. package/dist/cli/shared/WhaleBanner.d.ts +10 -0
  139. package/dist/cli/shared/WhaleBanner.js +12 -0
  140. package/dist/cli/shared/markdown.d.ts +21 -0
  141. package/dist/cli/shared/markdown.js +756 -0
  142. package/dist/cli/status/StatusApp.d.ts +4 -0
  143. package/dist/cli/status/StatusApp.js +105 -0
  144. package/dist/cli/stores/StoreApp.d.ts +7 -0
  145. package/dist/cli/stores/StoreApp.js +81 -0
  146. package/dist/index.d.ts +15 -0
  147. package/dist/index.js +538 -0
  148. package/dist/local-agent/connection.d.ts +48 -0
  149. package/dist/local-agent/connection.js +332 -0
  150. package/dist/local-agent/discovery.d.ts +18 -0
  151. package/dist/local-agent/discovery.js +146 -0
  152. package/dist/local-agent/executor.d.ts +34 -0
  153. package/dist/local-agent/executor.js +241 -0
  154. package/dist/local-agent/index.d.ts +14 -0
  155. package/dist/local-agent/index.js +198 -0
  156. package/dist/node/adapters/base.d.ts +35 -0
  157. package/dist/node/adapters/base.js +10 -0
  158. package/dist/node/adapters/discord.d.ts +29 -0
  159. package/dist/node/adapters/discord.js +299 -0
  160. package/dist/node/adapters/email.d.ts +23 -0
  161. package/dist/node/adapters/email.js +218 -0
  162. package/dist/node/adapters/imessage.d.ts +17 -0
  163. package/dist/node/adapters/imessage.js +118 -0
  164. package/dist/node/adapters/slack.d.ts +26 -0
  165. package/dist/node/adapters/slack.js +259 -0
  166. package/dist/node/adapters/sms.d.ts +23 -0
  167. package/dist/node/adapters/sms.js +161 -0
  168. package/dist/node/adapters/telegram.d.ts +17 -0
  169. package/dist/node/adapters/telegram.js +101 -0
  170. package/dist/node/adapters/webchat.d.ts +27 -0
  171. package/dist/node/adapters/webchat.js +160 -0
  172. package/dist/node/adapters/whatsapp.d.ts +28 -0
  173. package/dist/node/adapters/whatsapp.js +230 -0
  174. package/dist/node/cli.d.ts +2 -0
  175. package/dist/node/cli.js +325 -0
  176. package/dist/node/config.d.ts +17 -0
  177. package/dist/node/config.js +31 -0
  178. package/dist/node/runtime.d.ts +50 -0
  179. package/dist/node/runtime.js +351 -0
  180. package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +11 -0
  181. package/dist/server/handlers/__test-utils__/mock-supabase.js +393 -0
  182. package/dist/server/handlers/analytics.d.ts +17 -0
  183. package/dist/server/handlers/analytics.js +266 -0
  184. package/dist/server/handlers/api-keys.d.ts +6 -0
  185. package/dist/server/handlers/api-keys.js +221 -0
  186. package/dist/server/handlers/billing.d.ts +33 -0
  187. package/dist/server/handlers/billing.js +272 -0
  188. package/dist/server/handlers/browser.d.ts +10 -0
  189. package/dist/server/handlers/browser.js +517 -0
  190. package/dist/server/handlers/catalog.d.ts +99 -0
  191. package/dist/server/handlers/catalog.js +976 -0
  192. package/dist/server/handlers/comms.d.ts +254 -0
  193. package/dist/server/handlers/comms.js +588 -0
  194. package/dist/server/handlers/creations.d.ts +6 -0
  195. package/dist/server/handlers/creations.js +479 -0
  196. package/dist/server/handlers/crm.d.ts +89 -0
  197. package/dist/server/handlers/crm.js +538 -0
  198. package/dist/server/handlers/discovery.d.ts +6 -0
  199. package/dist/server/handlers/discovery.js +288 -0
  200. package/dist/server/handlers/embeddings.d.ts +92 -0
  201. package/dist/server/handlers/embeddings.js +197 -0
  202. package/dist/server/handlers/enrichment.d.ts +8 -0
  203. package/dist/server/handlers/enrichment.js +768 -0
  204. package/dist/server/handlers/image-gen.d.ts +6 -0
  205. package/dist/server/handlers/image-gen.js +409 -0
  206. package/dist/server/handlers/inventory.d.ts +319 -0
  207. package/dist/server/handlers/inventory.js +447 -0
  208. package/dist/server/handlers/kali.d.ts +10 -0
  209. package/dist/server/handlers/kali.js +210 -0
  210. package/dist/server/handlers/llm-providers.d.ts +6 -0
  211. package/dist/server/handlers/llm-providers.js +673 -0
  212. package/dist/server/handlers/local-agent.d.ts +6 -0
  213. package/dist/server/handlers/local-agent.js +118 -0
  214. package/dist/server/handlers/meta-ads.d.ts +111 -0
  215. package/dist/server/handlers/meta-ads.js +2279 -0
  216. package/dist/server/handlers/nodes.d.ts +33 -0
  217. package/dist/server/handlers/nodes.js +699 -0
  218. package/dist/server/handlers/operations.d.ts +138 -0
  219. package/dist/server/handlers/operations.js +131 -0
  220. package/dist/server/handlers/platform.d.ts +23 -0
  221. package/dist/server/handlers/platform.js +227 -0
  222. package/dist/server/handlers/supply-chain.d.ts +19 -0
  223. package/dist/server/handlers/supply-chain.js +327 -0
  224. package/dist/server/handlers/transcription.d.ts +17 -0
  225. package/dist/server/handlers/transcription.js +121 -0
  226. package/dist/server/handlers/video-gen.d.ts +6 -0
  227. package/dist/server/handlers/video-gen.js +466 -0
  228. package/dist/server/handlers/voice.d.ts +8 -0
  229. package/dist/server/handlers/voice.js +1146 -0
  230. package/dist/server/handlers/workflow-steps.d.ts +86 -0
  231. package/dist/server/handlers/workflow-steps.js +2349 -0
  232. package/dist/server/handlers/workflows.d.ts +7 -0
  233. package/dist/server/handlers/workflows.js +989 -0
  234. package/dist/server/index.d.ts +1 -0
  235. package/dist/server/index.js +2427 -0
  236. package/dist/server/lib/batch-client.d.ts +80 -0
  237. package/dist/server/lib/batch-client.js +467 -0
  238. package/dist/server/lib/code-worker-pool.d.ts +31 -0
  239. package/dist/server/lib/code-worker-pool.js +224 -0
  240. package/dist/server/lib/code-worker.d.ts +1 -0
  241. package/dist/server/lib/code-worker.js +188 -0
  242. package/dist/server/lib/compaction-service.d.ts +32 -0
  243. package/dist/server/lib/compaction-service.js +162 -0
  244. package/dist/server/lib/logger.d.ts +19 -0
  245. package/dist/server/lib/logger.js +46 -0
  246. package/dist/server/lib/otel.d.ts +38 -0
  247. package/dist/server/lib/otel.js +126 -0
  248. package/dist/server/lib/pg-rate-limiter.d.ts +21 -0
  249. package/dist/server/lib/pg-rate-limiter.js +86 -0
  250. package/dist/server/lib/prompt-sanitizer.d.ts +37 -0
  251. package/dist/server/lib/prompt-sanitizer.js +177 -0
  252. package/dist/server/lib/provider-capabilities.d.ts +85 -0
  253. package/dist/server/lib/provider-capabilities.js +190 -0
  254. package/dist/server/lib/provider-failover.d.ts +74 -0
  255. package/dist/server/lib/provider-failover.js +210 -0
  256. package/dist/server/lib/rate-limiter.d.ts +39 -0
  257. package/dist/server/lib/rate-limiter.js +147 -0
  258. package/dist/server/lib/server-agent-loop.d.ts +107 -0
  259. package/dist/server/lib/server-agent-loop.js +667 -0
  260. package/dist/server/lib/server-subagent.d.ts +78 -0
  261. package/dist/server/lib/server-subagent.js +203 -0
  262. package/dist/server/lib/session-checkpoint.d.ts +51 -0
  263. package/dist/server/lib/session-checkpoint.js +145 -0
  264. package/dist/server/lib/ssrf-guard.d.ts +13 -0
  265. package/dist/server/lib/ssrf-guard.js +240 -0
  266. package/dist/server/lib/supabase-client.d.ts +7 -0
  267. package/dist/server/lib/supabase-client.js +78 -0
  268. package/dist/server/lib/template-resolver.d.ts +31 -0
  269. package/dist/server/lib/template-resolver.js +215 -0
  270. package/dist/server/lib/utils.d.ts +16 -0
  271. package/dist/server/lib/utils.js +147 -0
  272. package/dist/server/local-agent-gateway.d.ts +82 -0
  273. package/dist/server/local-agent-gateway.js +426 -0
  274. package/dist/server/providers/anthropic.d.ts +20 -0
  275. package/dist/server/providers/anthropic.js +199 -0
  276. package/dist/server/providers/bedrock.d.ts +20 -0
  277. package/dist/server/providers/bedrock.js +194 -0
  278. package/dist/server/providers/gemini.d.ts +24 -0
  279. package/dist/server/providers/gemini.js +486 -0
  280. package/dist/server/providers/openai.d.ts +24 -0
  281. package/dist/server/providers/openai.js +522 -0
  282. package/dist/server/providers/registry.d.ts +32 -0
  283. package/dist/server/providers/registry.js +58 -0
  284. package/dist/server/providers/shared.d.ts +32 -0
  285. package/dist/server/providers/shared.js +124 -0
  286. package/dist/server/providers/types.d.ts +92 -0
  287. package/dist/server/providers/types.js +12 -0
  288. package/dist/server/proxy-handlers.d.ts +6 -0
  289. package/dist/server/proxy-handlers.js +89 -0
  290. package/dist/server/tool-router.d.ts +149 -0
  291. package/dist/server/tool-router.js +803 -0
  292. package/dist/server/validation.d.ts +24 -0
  293. package/dist/server/validation.js +301 -0
  294. package/dist/server/worker.d.ts +19 -0
  295. package/dist/server/worker.js +201 -0
  296. package/dist/setup.d.ts +8 -0
  297. package/dist/setup.js +181 -0
  298. package/dist/shared/agent-core.d.ts +157 -0
  299. package/dist/shared/agent-core.js +534 -0
  300. package/dist/shared/anthropic-types.d.ts +105 -0
  301. package/dist/shared/anthropic-types.js +7 -0
  302. package/dist/shared/api-client.d.ts +90 -0
  303. package/dist/shared/api-client.js +379 -0
  304. package/dist/shared/constants.d.ts +33 -0
  305. package/dist/shared/constants.js +80 -0
  306. package/dist/shared/sse-parser.d.ts +26 -0
  307. package/dist/shared/sse-parser.js +259 -0
  308. package/dist/shared/tool-dispatch.d.ts +52 -0
  309. package/dist/shared/tool-dispatch.js +191 -0
  310. package/dist/shared/types.d.ts +72 -0
  311. package/dist/shared/types.js +7 -0
  312. package/dist/updater.d.ts +25 -0
  313. package/dist/updater.js +140 -0
  314. package/dist/webchat/widget.d.ts +0 -0
  315. package/dist/webchat/widget.js +397 -0
  316. package/package.json +95 -0
  317. package/src/cli/services/builtin-skills/commit.md +19 -0
  318. package/src/cli/services/builtin-skills/review-pr.md +21 -0
  319. package/src/cli/services/builtin-skills/review.md +18 -0
@@ -0,0 +1,487 @@
1
+ /**
2
+ * File Operations — read, write, edit, multi-edit, notebook, list, search
3
+ *
4
+ * Extracted from local-tools.ts for single-responsibility.
5
+ */
6
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
7
+ import { dirname, join } from "path";
8
+ import { execSync } from "child_process";
9
+ import { homedir } from "os";
10
+ import { backupFile } from "../file-history.js";
11
+ import { notifyFileChanged } from "../lsp-manager.js";
12
+ import { debugLog } from "../debug-log.js";
13
+ export function resolvePath(p) {
14
+ if (p.startsWith("~/"))
15
+ return join(homedir(), p.slice(2));
16
+ return p;
17
+ }
18
+ // ============================================================================
19
+ // READ FILE
20
+ // ============================================================================
21
+ const IMAGE_EXTENSIONS = new Set(["png", "jpg", "jpeg", "gif", "webp"]);
22
+ const IMAGE_MEDIA_TYPES = {
23
+ png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg",
24
+ gif: "image/gif", webp: "image/webp",
25
+ };
26
+ const AUDIO_EXTENSIONS = new Set(["mp3", "wav", "aiff", "aac", "ogg", "flac", "m4a"]);
27
+ const AUDIO_MEDIA_TYPES = {
28
+ mp3: "audio/mpeg", wav: "audio/wav", aiff: "audio/aiff",
29
+ aac: "audio/aac", ogg: "audio/ogg", flac: "audio/flac",
30
+ m4a: "audio/mp4",
31
+ };
32
+ const AUDIO_MAX_SIZE = 25 * 1024 * 1024; // 25MB
33
+ export async function readFile(input) {
34
+ const path = resolvePath(input.path);
35
+ if (!existsSync(path))
36
+ return { success: false, output: `File not found: ${path}` };
37
+ const ext = path.split(".").pop()?.toLowerCase() || "";
38
+ // Image files -> base64 with marker for agent-loop to convert to image content block
39
+ if (IMAGE_EXTENSIONS.has(ext)) {
40
+ try {
41
+ const buffer = readFileSync(path);
42
+ const base64 = buffer.toString("base64");
43
+ const mediaType = IMAGE_MEDIA_TYPES[ext] || "image/png";
44
+ return { success: true, output: `__IMAGE__${mediaType}__${base64}` };
45
+ }
46
+ catch (err) {
47
+ return { success: false, output: `Failed to read image: ${err}` };
48
+ }
49
+ }
50
+ // Audio files -> base64 with marker for agent-loop to convert to audio content block
51
+ if (AUDIO_EXTENSIONS.has(ext)) {
52
+ try {
53
+ const stat = statSync(path);
54
+ if (stat.size > AUDIO_MAX_SIZE) {
55
+ return { success: false, output: `Audio file too large: ${(stat.size / 1024 / 1024).toFixed(1)}MB (max 25MB)` };
56
+ }
57
+ const buffer = readFileSync(path);
58
+ const base64 = buffer.toString("base64");
59
+ const mediaType = AUDIO_MEDIA_TYPES[ext] || "audio/mpeg";
60
+ return { success: true, output: `__AUDIO__${mediaType}__${base64}` };
61
+ }
62
+ catch (err) {
63
+ return { success: false, output: `Failed to read audio file: ${err}` };
64
+ }
65
+ }
66
+ // PDF files -> extract text
67
+ if (ext === "pdf") {
68
+ try {
69
+ const pdfParse = (await import("pdf-parse")).default;
70
+ const buffer = readFileSync(path);
71
+ const data = await pdfParse(buffer);
72
+ let text = data.text || "";
73
+ const totalPages = data.numpages || 0;
74
+ const pagesParam = input.pages;
75
+ // Apply page range filter if specified
76
+ if (pagesParam && text) {
77
+ const pageTexts = text.split(/\f/); // Form feed splits pages in most PDFs
78
+ const { start, end } = parsePageRange(pagesParam, pageTexts.length);
79
+ text = pageTexts.slice(start, end).join("\n\n---\n\n");
80
+ }
81
+ if (text.length > 500_000) {
82
+ text = text.slice(0, 500_000) + `\n\n... (safety truncated — ${text.length.toLocaleString()} chars total)`;
83
+ }
84
+ return {
85
+ success: true,
86
+ output: `PDF: ${path} (${totalPages} pages)\n\n${text}`,
87
+ };
88
+ }
89
+ catch (err) {
90
+ return { success: false, output: `Failed to parse PDF: ${err}` };
91
+ }
92
+ }
93
+ // Text files — existing behavior
94
+ const content = readFileSync(path, "utf-8");
95
+ const lines = content.split("\n");
96
+ const offset = input.offset || 1; // 1-based
97
+ const limit = input.limit;
98
+ if (offset > 1 || limit) {
99
+ const startIdx = Math.max(0, offset - 1);
100
+ const endIdx = limit ? startIdx + limit : lines.length;
101
+ const slice = lines.slice(startIdx, endIdx);
102
+ const numbered = slice.map((line, i) => {
103
+ const lineNum = startIdx + i + 1;
104
+ return `${String(lineNum).padStart(6)} ${line}`;
105
+ });
106
+ let output = numbered.join("\n");
107
+ if (endIdx < lines.length) {
108
+ output += `\n\n... (showing lines ${startIdx + 1}-${Math.min(endIdx, lines.length)} of ${lines.length})`;
109
+ }
110
+ return { success: true, output };
111
+ }
112
+ if (content.length > 500_000) {
113
+ return { success: true, output: content.slice(0, 500_000) + `\n\n... (safety truncated, ${content.length.toLocaleString()} total chars)` };
114
+ }
115
+ return { success: true, output: content };
116
+ }
117
+ function parsePageRange(range, totalPages) {
118
+ const parts = range.split("-");
119
+ const start = Math.max(0, parseInt(parts[0], 10) - 1);
120
+ const end = parts.length > 1 ? Math.min(totalPages, parseInt(parts[1], 10)) : start + 1;
121
+ return { start, end };
122
+ }
123
+ // ============================================================================
124
+ // WRITE FILE
125
+ // ============================================================================
126
+ /** Compute a unified diff between old and new file lines using prefix/suffix matching */
127
+ function computeWriteDiff(oldLines, newLines) {
128
+ const CTX = 3;
129
+ const MAX_PER_SIDE = 60;
130
+ // Find common prefix
131
+ let prefixLen = 0;
132
+ while (prefixLen < oldLines.length && prefixLen < newLines.length &&
133
+ oldLines[prefixLen] === newLines[prefixLen]) {
134
+ prefixLen++;
135
+ }
136
+ // Find common suffix (not overlapping prefix)
137
+ let suffixLen = 0;
138
+ while (suffixLen < (oldLines.length - prefixLen) &&
139
+ suffixLen < (newLines.length - prefixLen) &&
140
+ oldLines[oldLines.length - 1 - suffixLen] === newLines[newLines.length - 1 - suffixLen]) {
141
+ suffixLen++;
142
+ }
143
+ // If identical
144
+ if (prefixLen + suffixLen >= oldLines.length && prefixLen + suffixLen >= newLines.length) {
145
+ return []; // no changes
146
+ }
147
+ const oldMiddle = oldLines.slice(prefixLen, oldLines.length - suffixLen);
148
+ const newMiddle = newLines.slice(prefixLen, newLines.length - suffixLen);
149
+ // If most of the file changed, show a compact summary
150
+ if (oldMiddle.length > MAX_PER_SIDE * 2 && newMiddle.length > MAX_PER_SIDE * 2) {
151
+ const showOld = oldMiddle.slice(0, MAX_PER_SIDE);
152
+ const showNew = newMiddle.slice(0, MAX_PER_SIDE);
153
+ const ctxStart = Math.max(0, prefixLen - CTX);
154
+ const ctxBefore = oldLines.slice(ctxStart, prefixLen);
155
+ const parts = [`@@ -${ctxStart + 1},${ctxBefore.length + showOld.length} +${ctxStart + 1},${ctxBefore.length + showNew.length} @@`];
156
+ for (const l of ctxBefore)
157
+ parts.push(` ${l}`);
158
+ for (const l of showOld)
159
+ parts.push(`-${l}`);
160
+ parts.push(`-... (${oldMiddle.length - MAX_PER_SIDE} more lines removed)`);
161
+ for (const l of showNew)
162
+ parts.push(`+${l}`);
163
+ parts.push(`+... (${newMiddle.length - MAX_PER_SIDE} more lines added)`);
164
+ return parts;
165
+ }
166
+ // Build single hunk with context
167
+ const ctxStart = Math.max(0, prefixLen - CTX);
168
+ const ctxBefore = oldLines.slice(ctxStart, prefixLen);
169
+ const newSuffixStart = newLines.length - suffixLen;
170
+ const ctxAfter = newLines.slice(newSuffixStart, Math.min(newSuffixStart + CTX, newLines.length));
171
+ const hunkOldLen = ctxBefore.length + oldMiddle.length + ctxAfter.length;
172
+ const hunkNewLen = ctxBefore.length + newMiddle.length + ctxAfter.length;
173
+ const parts = [`@@ -${ctxStart + 1},${hunkOldLen} +${ctxStart + 1},${hunkNewLen} @@`];
174
+ for (const l of ctxBefore)
175
+ parts.push(` ${l}`);
176
+ for (const l of oldMiddle.slice(0, MAX_PER_SIDE))
177
+ parts.push(`-${l}`);
178
+ if (oldMiddle.length > MAX_PER_SIDE)
179
+ parts.push(`-... (${oldMiddle.length - MAX_PER_SIDE} more lines removed)`);
180
+ for (const l of newMiddle.slice(0, MAX_PER_SIDE))
181
+ parts.push(`+${l}`);
182
+ if (newMiddle.length > MAX_PER_SIDE)
183
+ parts.push(`+... (${newMiddle.length - MAX_PER_SIDE} more lines added)`);
184
+ for (const l of ctxAfter)
185
+ parts.push(` ${l}`);
186
+ return parts;
187
+ }
188
+ export function writeFile(input) {
189
+ const path = resolvePath(input.path);
190
+ const content = input.content;
191
+ const existed = existsSync(path);
192
+ const oldContent = existed ? readFileSync(path, "utf-8") : null;
193
+ backupFile(path); // Save backup before modification
194
+ const dir = dirname(path);
195
+ if (!existsSync(dir))
196
+ mkdirSync(dir, { recursive: true });
197
+ writeFileSync(path, content, "utf-8");
198
+ debugLog("tools", `write_file: ${path} (${content.length} chars)`);
199
+ notifyFileChanged(path);
200
+ const newLines = content.split("\n");
201
+ if (!existed || !oldContent) {
202
+ // New file — show as all-added unified diff
203
+ const previewMax = 30;
204
+ const preview = newLines.slice(0, previewMax).map(l => `+${l}`);
205
+ if (newLines.length > previewMax)
206
+ preview.push(`+... (+${newLines.length - previewMax} more lines)`);
207
+ return {
208
+ success: true,
209
+ output: `Created: ${path} (${newLines.length} lines, ${content.length} chars)\n@@ -0,0 +1,${Math.min(newLines.length, previewMax)} @@\n${preview.join("\n")}`,
210
+ };
211
+ }
212
+ // Overwrite — compute diff between old and new content
213
+ const oldLines = oldContent.split("\n");
214
+ const diff = computeWriteDiff(oldLines, newLines);
215
+ // Count changes
216
+ let added = 0, removed = 0;
217
+ for (const line of diff) {
218
+ if (line.startsWith("+"))
219
+ added++;
220
+ else if (line.startsWith("-"))
221
+ removed++;
222
+ }
223
+ const summary = `Added ${added} lines, removed ${removed} lines`;
224
+ return {
225
+ success: true,
226
+ output: `Updated: ${path} (${summary})\n${diff.join("\n")}`,
227
+ };
228
+ }
229
+ // ============================================================================
230
+ // EDIT FILE
231
+ // ============================================================================
232
+ export function editFile(input) {
233
+ const path = resolvePath(input.path);
234
+ const oldString = input.old_string;
235
+ const newString = input.new_string;
236
+ const replaceAll = input.replace_all ?? false;
237
+ if (!existsSync(path))
238
+ return { success: false, output: `File not found: ${path}` };
239
+ backupFile(path); // Save backup before modification
240
+ let content = readFileSync(path, "utf-8");
241
+ if (!content.includes(oldString))
242
+ return { success: false, output: "old_string not found in file" };
243
+ if (replaceAll) {
244
+ let count = 0;
245
+ if (newString.includes(oldString)) {
246
+ // Avoid infinite loop: use split-join for safe replacement
247
+ const parts = content.split(oldString);
248
+ count = parts.length - 1;
249
+ content = parts.join(newString);
250
+ }
251
+ else {
252
+ while (content.includes(oldString)) {
253
+ content = content.replace(oldString, newString);
254
+ count++;
255
+ if (count > 10000)
256
+ break; // safety
257
+ }
258
+ }
259
+ writeFileSync(path, content, "utf-8");
260
+ notifyFileChanged(path);
261
+ return { success: true, output: `File edited: ${path} (${count} replacements)` };
262
+ }
263
+ // Single replacement (original behavior)
264
+ const idx = content.indexOf(oldString);
265
+ const newContent = content.slice(0, idx) + newString + content.slice(idx + oldString.length);
266
+ writeFileSync(path, newContent, "utf-8");
267
+ notifyFileChanged(path);
268
+ // Generate unified diff with context and line numbers
269
+ const allOldLines = content.split("\n");
270
+ const allNewLines = newContent.split("\n");
271
+ const beforeEdit = content.slice(0, idx);
272
+ const startLine = beforeEdit.split("\n").length; // 1-based
273
+ const oldLines = oldString.split("\n");
274
+ const newLines = newString.split("\n");
275
+ const CTX = 3;
276
+ const MAX_LINES = 20;
277
+ const ctxStart = Math.max(1, startLine - CTX);
278
+ const ctxBeforeLines = allOldLines.slice(ctxStart - 1, startLine - 1);
279
+ const newEndLine = startLine + newLines.length - 1;
280
+ const ctxAfterLines = allNewLines.slice(newEndLine, Math.min(newEndLine + CTX, allNewLines.length));
281
+ const showOld = oldLines.slice(0, MAX_LINES);
282
+ const showNew = newLines.slice(0, MAX_LINES);
283
+ const hunkOldLen = ctxBeforeLines.length + showOld.length + ctxAfterLines.length;
284
+ const hunkNewLen = ctxBeforeLines.length + showNew.length + ctxAfterLines.length;
285
+ const diffParts = [];
286
+ diffParts.push(`@@ -${ctxStart},${hunkOldLen} +${ctxStart},${hunkNewLen} @@`);
287
+ for (const l of ctxBeforeLines)
288
+ diffParts.push(` ${l}`);
289
+ for (const l of showOld)
290
+ diffParts.push(`-${l}`);
291
+ if (oldLines.length > MAX_LINES)
292
+ diffParts.push(`-... (${oldLines.length - MAX_LINES} more lines)`);
293
+ for (const l of showNew)
294
+ diffParts.push(`+${l}`);
295
+ if (newLines.length > MAX_LINES)
296
+ diffParts.push(`+... (${newLines.length - MAX_LINES} more lines)`);
297
+ for (const l of ctxAfterLines)
298
+ diffParts.push(` ${l}`);
299
+ return { success: true, output: `File edited: ${path}\n${diffParts.join("\n")}` };
300
+ }
301
+ // ============================================================================
302
+ // MULTI EDIT
303
+ // ============================================================================
304
+ export function multiEdit(input) {
305
+ const path = resolvePath(input.file_path);
306
+ const edits = input.edits;
307
+ if (!existsSync(path))
308
+ return { success: false, output: `File not found: ${path}` };
309
+ if (!Array.isArray(edits) || edits.length === 0)
310
+ return { success: false, output: "edits array is required and must not be empty" };
311
+ backupFile(path); // Save backup before modification
312
+ let content = readFileSync(path, "utf-8");
313
+ const diffParts = [];
314
+ const CTX = 2;
315
+ const MAX_LINES = 10;
316
+ for (let i = 0; i < edits.length; i++) {
317
+ const { old_string, new_string } = edits[i];
318
+ const idx = content.indexOf(old_string);
319
+ if (idx === -1) {
320
+ return {
321
+ success: false,
322
+ output: `Edit ${i + 1}/${edits.length} failed: old_string not found (${i} edits applied successfully before failure)`,
323
+ };
324
+ }
325
+ // Compute line numbers before applying edit
326
+ const allOldLines = content.split("\n");
327
+ const beforeEdit = content.slice(0, idx);
328
+ const startLine = beforeEdit.split("\n").length;
329
+ const oldLines = old_string.split("\n");
330
+ const newLines = new_string.split("\n");
331
+ const newContent = content.slice(0, idx) + new_string + content.slice(idx + old_string.length);
332
+ const allNewLines = newContent.split("\n");
333
+ const ctxStart = Math.max(1, startLine - CTX);
334
+ const ctxBeforeLines = allOldLines.slice(ctxStart - 1, startLine - 1);
335
+ const newEndLine = startLine + newLines.length - 1;
336
+ const ctxAfterLines = allNewLines.slice(newEndLine, Math.min(newEndLine + CTX, allNewLines.length));
337
+ const showOld = oldLines.slice(0, MAX_LINES);
338
+ const showNew = newLines.slice(0, MAX_LINES);
339
+ const hunkOldLen = ctxBeforeLines.length + showOld.length + ctxAfterLines.length;
340
+ const hunkNewLen = ctxBeforeLines.length + showNew.length + ctxAfterLines.length;
341
+ diffParts.push(`@@ -${ctxStart},${hunkOldLen} +${ctxStart},${hunkNewLen} @@`);
342
+ for (const l of ctxBeforeLines)
343
+ diffParts.push(` ${l}`);
344
+ for (const l of showOld)
345
+ diffParts.push(`-${l}`);
346
+ if (oldLines.length > MAX_LINES)
347
+ diffParts.push(`-... (${oldLines.length - MAX_LINES} more)`);
348
+ for (const l of showNew)
349
+ diffParts.push(`+${l}`);
350
+ if (newLines.length > MAX_LINES)
351
+ diffParts.push(`+... (${newLines.length - MAX_LINES} more)`);
352
+ for (const l of ctxAfterLines)
353
+ diffParts.push(` ${l}`);
354
+ content = newContent;
355
+ }
356
+ writeFileSync(path, content, "utf-8");
357
+ notifyFileChanged(path);
358
+ return {
359
+ success: true,
360
+ output: `Applied ${edits.length} edits to ${path}\n${diffParts.join("\n")}`,
361
+ };
362
+ }
363
+ // ============================================================================
364
+ // NOTEBOOK EDIT
365
+ // ============================================================================
366
+ export function notebookEdit(input) {
367
+ const path = resolvePath(input.notebook_path);
368
+ const newSource = input.new_source || "";
369
+ const cellType = input.cell_type || "code";
370
+ const editMode = input.edit_mode || "replace";
371
+ const cellId = input.cell_id;
372
+ if (!existsSync(path))
373
+ return { success: false, output: `Notebook not found: ${path}` };
374
+ let notebook;
375
+ try {
376
+ notebook = JSON.parse(readFileSync(path, "utf-8"));
377
+ }
378
+ catch (err) {
379
+ return { success: false, output: `Failed to parse notebook: ${err}` };
380
+ }
381
+ const cells = notebook.cells || [];
382
+ // Find cell by ID or numeric index
383
+ let cellIndex = -1;
384
+ if (cellId !== undefined) {
385
+ cellIndex = cells.findIndex((c) => c.id === cellId);
386
+ if (cellIndex === -1) {
387
+ const idx = parseInt(cellId, 10);
388
+ if (!isNaN(idx) && idx >= 0 && idx < cells.length)
389
+ cellIndex = idx;
390
+ }
391
+ }
392
+ // Split source into notebook-format lines (each line ends with \n except last)
393
+ const sourceLines = newSource.split("\n").map((line, i, arr) => i < arr.length - 1 ? line + "\n" : line);
394
+ switch (editMode) {
395
+ case "replace": {
396
+ if (cellIndex < 0)
397
+ return { success: false, output: `Cell not found: ${cellId}` };
398
+ cells[cellIndex].source = sourceLines;
399
+ if (cellType)
400
+ cells[cellIndex].cell_type = cellType;
401
+ break;
402
+ }
403
+ case "insert": {
404
+ const newCell = {
405
+ cell_type: cellType,
406
+ source: sourceLines,
407
+ metadata: {},
408
+ };
409
+ if (cellType === "code") {
410
+ newCell.execution_count = null;
411
+ newCell.outputs = [];
412
+ }
413
+ if (cellIndex >= 0) {
414
+ cells.splice(cellIndex + 1, 0, newCell);
415
+ }
416
+ else {
417
+ cells.push(newCell);
418
+ }
419
+ break;
420
+ }
421
+ case "delete": {
422
+ if (cellIndex < 0)
423
+ return { success: false, output: `Cell not found: ${cellId}` };
424
+ cells.splice(cellIndex, 1);
425
+ break;
426
+ }
427
+ default:
428
+ return { success: false, output: `Unknown edit_mode: ${editMode}` };
429
+ }
430
+ notebook.cells = cells;
431
+ writeFileSync(path, JSON.stringify(notebook, null, 1), "utf-8");
432
+ return { success: true, output: `Notebook ${editMode}d cell in ${path} (${cells.length} cells total)` };
433
+ }
434
+ // ============================================================================
435
+ // LIST DIRECTORY
436
+ // ============================================================================
437
+ export function listDirectory(input) {
438
+ const path = resolvePath(input.path);
439
+ const recursive = input.recursive ?? false;
440
+ if (!existsSync(path))
441
+ return { success: false, output: `Directory not found: ${path}` };
442
+ if (recursive) {
443
+ try {
444
+ const escapedPath = path.replace(/'/g, "'\\''");
445
+ const output = execSync(`find '${escapedPath}' -maxdepth 4 -not -path '*/.*' 2>/dev/null | head -200`, {
446
+ encoding: "utf-8", timeout: 5000,
447
+ });
448
+ return { success: true, output: output.trim() || "(empty)" };
449
+ }
450
+ catch {
451
+ return { success: false, output: "Failed to list directory recursively" };
452
+ }
453
+ }
454
+ const entries = readdirSync(path, { withFileTypes: true });
455
+ const lines = entries.map((e) => `${e.isDirectory() ? "[dir] " : " "}${e.name}`);
456
+ return { success: true, output: lines.join("\n") || "(empty directory)" };
457
+ }
458
+ // ============================================================================
459
+ // SEARCH FILES / SEARCH CONTENT (legacy)
460
+ // ============================================================================
461
+ export function searchFiles(input) {
462
+ const pattern = input.pattern.replace(/'/g, "'\\''");
463
+ const path = resolvePath(input.path).replace(/'/g, "'\\''");
464
+ try {
465
+ const output = execSync(`find '${path}' -name '${pattern}' -type f -not -path '*/.*' 2>/dev/null | head -100`, { encoding: "utf-8", timeout: 10000 });
466
+ return { success: true, output: output.trim() || "No files found" };
467
+ }
468
+ catch {
469
+ return { success: false, output: "Search failed" };
470
+ }
471
+ }
472
+ export function searchContent(input) {
473
+ const query = input.query;
474
+ const path = resolvePath(input.path);
475
+ const filePattern = input.file_pattern;
476
+ let cmd = `grep -rn '${query.replace(/'/g, "'\\''")}' '${path.replace(/'/g, "'\\''")}'`;
477
+ if (filePattern)
478
+ cmd += ` --include='${filePattern.replace(/'/g, "'\\''")}'`;
479
+ cmd += " 2>/dev/null | head -50";
480
+ try {
481
+ const output = execSync(cmd, { encoding: "utf-8", timeout: 10000 });
482
+ return { success: true, output: output.trim() || "No matches found" };
483
+ }
484
+ catch {
485
+ return { success: true, output: "No matches found" };
486
+ }
487
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Search Tools — glob and grep implementations
3
+ *
4
+ * Extracted from local-tools.ts for single-responsibility.
5
+ */
6
+ import { ToolResult } from "../../../shared/types.js";
7
+ export declare function globSearch(input: Record<string, unknown>): ToolResult;
8
+ export declare function grepSearch(input: Record<string, unknown>): ToolResult;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Search Tools — glob and grep implementations
3
+ *
4
+ * Extracted from local-tools.ts for single-responsibility.
5
+ */
6
+ import { existsSync } from "fs";
7
+ import { join } from "path";
8
+ import { execSync } from "child_process";
9
+ import { homedir } from "os";
10
+ import { isRgAvailable, rgGrep, rgGlob } from "../ripgrep.js";
11
+ function resolvePath(p) {
12
+ if (p.startsWith("~/"))
13
+ return join(homedir(), p.slice(2));
14
+ return p;
15
+ }
16
+ // --- GLOB -------------------------------------------------------------------
17
+ export function globSearch(input) {
18
+ const pattern = input.pattern;
19
+ const basePath = resolvePath(input.path || process.cwd());
20
+ if (!existsSync(basePath))
21
+ return { success: false, output: `Directory not found: ${basePath}` };
22
+ // Try ripgrep first for speed
23
+ if (isRgAvailable()) {
24
+ try {
25
+ const result = rgGlob({ pattern, path: basePath, headLimit: 200 });
26
+ if (result === null)
27
+ return { success: true, output: "No files found" };
28
+ const files = result.split("\n").filter(Boolean);
29
+ return { success: true, output: `${files.length} files:\n${result}` };
30
+ }
31
+ catch {
32
+ // Fall through to find
33
+ }
34
+ }
35
+ // Fallback: system find
36
+ let searchDir = basePath;
37
+ let namePattern = pattern;
38
+ const lastSlash = pattern.lastIndexOf("/");
39
+ if (lastSlash >= 0) {
40
+ const dirPart = pattern.slice(0, lastSlash).replace(/\*\*\/?/g, "").replace(/\/+$/, "");
41
+ namePattern = pattern.slice(lastSlash + 1);
42
+ if (dirPart && !dirPart.includes("*")) {
43
+ searchDir = join(basePath, dirPart);
44
+ }
45
+ }
46
+ let findCmd;
47
+ const braceMatch = namePattern.match(/\{([^}]+)\}/);
48
+ if (braceMatch) {
49
+ const extensions = braceMatch[1].split(",").map((e) => e.trim());
50
+ const conditions = extensions
51
+ .map((ext) => {
52
+ const expanded = namePattern.replace(`{${braceMatch[1]}}`, ext);
53
+ // Escape single quotes to prevent shell injection
54
+ const escaped = expanded.replace(/'/g, "'\\''");
55
+ return `-name '${escaped}'`;
56
+ })
57
+ .join(" -o ");
58
+ const escapedDir = searchDir.replace(/'/g, "'\\''");
59
+ findCmd = `find '${escapedDir}' \\( ${conditions} \\) -type f -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' 2>/dev/null | sort | head -200`;
60
+ }
61
+ else {
62
+ const escapedDir = searchDir.replace(/'/g, "'\\''");
63
+ const escapedPattern = namePattern.replace(/'/g, "'\\''");
64
+ findCmd = `find '${escapedDir}' -name '${escapedPattern}' -type f -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' 2>/dev/null | sort | head -200`;
65
+ }
66
+ try {
67
+ const output = execSync(findCmd, { encoding: "utf-8", timeout: 10000 });
68
+ const files = output.trim().split("\n").filter(Boolean);
69
+ if (files.length === 0)
70
+ return { success: true, output: "No files found" };
71
+ return { success: true, output: `${files.length} files:\n${files.join("\n")}` };
72
+ }
73
+ catch {
74
+ return { success: true, output: "No files found" };
75
+ }
76
+ }
77
+ // --- GREP -------------------------------------------------------------------
78
+ export function grepSearch(input) {
79
+ const pattern = input.pattern;
80
+ const path = resolvePath(input.path || process.cwd());
81
+ const globFilter = input.glob;
82
+ const outputMode = input.output_mode || "files_with_matches";
83
+ const contextLines = input.context;
84
+ const beforeLines = input.before;
85
+ const afterLines = input.after;
86
+ const caseInsensitive = input.case_insensitive;
87
+ const fileType = input.type;
88
+ const headLimit = input.head_limit || 200;
89
+ const offset = input.offset || 0;
90
+ const multiline = input.multiline;
91
+ // Try ripgrep first
92
+ if (isRgAvailable()) {
93
+ try {
94
+ const result = rgGrep({
95
+ pattern,
96
+ path,
97
+ outputMode: outputMode,
98
+ glob: globFilter,
99
+ type: fileType,
100
+ caseInsensitive: caseInsensitive || false,
101
+ multiline: multiline || false,
102
+ context: contextLines,
103
+ before: beforeLines,
104
+ after: afterLines,
105
+ headLimit,
106
+ });
107
+ if (result === null)
108
+ return { success: true, output: "No matches found" };
109
+ // For count mode, filter zero-count files
110
+ if (outputMode === "count") {
111
+ const lines = result.split("\n").filter((l) => !l.endsWith(":0"));
112
+ const sliced = offset > 0 ? lines.slice(offset) : lines;
113
+ return { success: true, output: sliced.join("\n") || "No matches found" };
114
+ }
115
+ // Apply offset (skip first N entries)
116
+ if (offset > 0) {
117
+ const lines = result.split("\n");
118
+ const sliced = lines.slice(offset);
119
+ return { success: true, output: sliced.join("\n") || "No matches found" };
120
+ }
121
+ return { success: true, output: result };
122
+ }
123
+ catch {
124
+ // Fall through to system grep
125
+ }
126
+ }
127
+ // Multiline requires rg — can't do with system grep
128
+ if (multiline) {
129
+ return { success: false, output: "Multiline search requires ripgrep (rg). Install: brew install ripgrep" };
130
+ }
131
+ // Fallback: system grep
132
+ const parts = ["grep", "-r"];
133
+ if (caseInsensitive)
134
+ parts.push("-i");
135
+ switch (outputMode) {
136
+ case "files_with_matches":
137
+ parts.push("-l");
138
+ break;
139
+ case "count":
140
+ parts.push("-c");
141
+ break;
142
+ case "content":
143
+ parts.push("-n");
144
+ break;
145
+ }
146
+ if (outputMode === "content") {
147
+ if (contextLines)
148
+ parts.push(`-C ${contextLines}`);
149
+ if (beforeLines)
150
+ parts.push(`-B ${beforeLines}`);
151
+ if (afterLines)
152
+ parts.push(`-A ${afterLines}`);
153
+ }
154
+ const typeMap = {
155
+ js: "*.js", ts: "*.ts", tsx: "*.tsx", jsx: "*.jsx",
156
+ py: "*.py", rust: "*.rs", go: "*.go", java: "*.java",
157
+ rb: "*.rb", php: "*.php", css: "*.css", html: "*.html",
158
+ json: "*.json", yaml: "*.yaml", yml: "*.yml", md: "*.md",
159
+ swift: "*.swift", kt: "*.kt", cpp: "*.cpp", c: "*.c", h: "*.h",
160
+ sh: "*.sh", sql: "*.sql", xml: "*.xml", toml: "*.toml",
161
+ };
162
+ if (globFilter) {
163
+ parts.push(`--include='${globFilter.replace(/'/g, "'\\''")}'`);
164
+ }
165
+ else if (fileType && typeMap[fileType]) {
166
+ parts.push(`--include='${typeMap[fileType]}'`);
167
+ }
168
+ parts.push("--exclude-dir='node_modules'", "--exclude-dir='.git'", "--exclude-dir='dist'");
169
+ const escaped = pattern.replace(/'/g, "'\\''");
170
+ parts.push(`'${escaped}'`, `'${path.replace(/'/g, "'\\''")}'`);
171
+ const cmd = `${parts.join(" ")} 2>/dev/null | head -${headLimit}`;
172
+ try {
173
+ const output = execSync(cmd, { encoding: "utf-8", timeout: 15000 });
174
+ const result = output.trim();
175
+ if (!result)
176
+ return { success: true, output: "No matches found" };
177
+ if (outputMode === "count") {
178
+ const lines = result.split("\n").filter((l) => !l.endsWith(":0"));
179
+ return { success: true, output: lines.join("\n") || "No matches found" };
180
+ }
181
+ return { success: true, output: result };
182
+ }
183
+ catch {
184
+ return { success: true, output: "No matches found" };
185
+ }
186
+ }