@oh-my-pi/pi-coding-agent 3.30.0 → 3.31.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 (155) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/package.json +5 -5
  3. package/src/cli/args.ts +4 -0
  4. package/src/core/agent-session.ts +29 -2
  5. package/src/core/bash-executor.ts +2 -1
  6. package/src/core/custom-commands/bundled/review/index.ts +369 -14
  7. package/src/core/custom-commands/bundled/wt/index.ts +1 -1
  8. package/src/core/session-manager.ts +158 -246
  9. package/src/core/session-storage.ts +379 -0
  10. package/src/core/settings-manager.ts +155 -4
  11. package/src/core/system-prompt.ts +62 -64
  12. package/src/core/tools/ask.ts +5 -4
  13. package/src/core/tools/bash-interceptor.ts +26 -61
  14. package/src/core/tools/bash.ts +13 -8
  15. package/src/core/tools/edit-diff.ts +11 -4
  16. package/src/core/tools/edit.ts +7 -13
  17. package/src/core/tools/find.ts +111 -50
  18. package/src/core/tools/gemini-image.ts +128 -147
  19. package/src/core/tools/grep.ts +397 -415
  20. package/src/core/tools/index.test.ts +5 -1
  21. package/src/core/tools/index.ts +6 -8
  22. package/src/core/tools/ls.ts +12 -10
  23. package/src/core/tools/lsp/client.ts +58 -9
  24. package/src/core/tools/lsp/config.ts +205 -656
  25. package/src/core/tools/lsp/defaults.json +465 -0
  26. package/src/core/tools/lsp/index.ts +55 -32
  27. package/src/core/tools/lsp/rust-analyzer.ts +49 -10
  28. package/src/core/tools/lsp/types.ts +1 -0
  29. package/src/core/tools/lsp/utils.ts +1 -1
  30. package/src/core/tools/read.ts +150 -74
  31. package/src/core/tools/render-utils.ts +70 -10
  32. package/src/core/tools/review.ts +38 -126
  33. package/src/core/tools/task/artifacts.ts +5 -4
  34. package/src/core/tools/task/executor.ts +94 -83
  35. package/src/core/tools/task/index.ts +129 -92
  36. package/src/core/tools/task/parallel.ts +30 -3
  37. package/src/core/tools/task/render.ts +85 -39
  38. package/src/core/tools/task/types.ts +15 -6
  39. package/src/core/tools/task/worker.ts +124 -89
  40. package/src/core/tools/web-fetch.ts +112 -377
  41. package/src/core/tools/{web-fetch-handlers → web-scrapers}/artifacthub.ts +6 -1
  42. package/src/core/tools/{web-fetch-handlers → web-scrapers}/arxiv.ts +8 -4
  43. package/src/core/tools/{web-fetch-handlers → web-scrapers}/aur.ts +6 -2
  44. package/src/core/tools/{web-fetch-handlers → web-scrapers}/biorxiv.ts +6 -1
  45. package/src/core/tools/{web-fetch-handlers → web-scrapers}/bluesky.ts +10 -3
  46. package/src/core/tools/{web-fetch-handlers → web-scrapers}/brew.ts +6 -2
  47. package/src/core/tools/{web-fetch-handlers → web-scrapers}/cheatsh.ts +6 -1
  48. package/src/core/tools/{web-fetch-handlers → web-scrapers}/chocolatey.ts +6 -1
  49. package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
  50. package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
  51. package/src/core/tools/web-scrapers/clojars.ts +180 -0
  52. package/src/core/tools/{web-fetch-handlers → web-scrapers}/coingecko.ts +6 -1
  53. package/src/core/tools/{web-fetch-handlers → web-scrapers}/crates-io.ts +7 -2
  54. package/src/core/tools/web-scrapers/crossref.ts +149 -0
  55. package/src/core/tools/{web-fetch-handlers → web-scrapers}/devto.ts +8 -4
  56. package/src/core/tools/{web-fetch-handlers → web-scrapers}/discogs.ts +6 -1
  57. package/src/core/tools/web-scrapers/discourse.ts +221 -0
  58. package/src/core/tools/{web-fetch-handlers → web-scrapers}/dockerhub.ts +7 -3
  59. package/src/core/tools/web-scrapers/fdroid.ts +158 -0
  60. package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
  61. package/src/core/tools/web-scrapers/flathub.ts +239 -0
  62. package/src/core/tools/{web-fetch-handlers → web-scrapers}/github-gist.ts +6 -2
  63. package/src/core/tools/{web-fetch-handlers → web-scrapers}/github.ts +63 -32
  64. package/src/core/tools/{web-fetch-handlers → web-scrapers}/gitlab.ts +31 -19
  65. package/src/core/tools/{web-fetch-handlers → web-scrapers}/go-pkg.ts +8 -4
  66. package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackage.ts +6 -1
  67. package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackernews.ts +18 -18
  68. package/src/core/tools/{web-fetch-handlers → web-scrapers}/hex.ts +3 -3
  69. package/src/core/tools/{web-fetch-handlers → web-scrapers}/huggingface.ts +10 -10
  70. package/src/core/tools/{web-fetch-handlers → web-scrapers}/iacr.ts +8 -4
  71. package/src/core/tools/web-scrapers/index.ts +250 -0
  72. package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
  73. package/src/core/tools/web-scrapers/lemmy.ts +220 -0
  74. package/src/core/tools/{web-fetch-handlers → web-scrapers}/lobsters.ts +3 -3
  75. package/src/core/tools/{web-fetch-handlers → web-scrapers}/mastodon.ts +11 -3
  76. package/src/core/tools/{web-fetch-handlers → web-scrapers}/maven.ts +6 -1
  77. package/src/core/tools/{web-fetch-handlers → web-scrapers}/mdn.ts +2 -2
  78. package/src/core/tools/{web-fetch-handlers → web-scrapers}/metacpan.ts +13 -7
  79. package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
  80. package/src/core/tools/{web-fetch-handlers → web-scrapers}/npm.ts +12 -5
  81. package/src/core/tools/{web-fetch-handlers → web-scrapers}/nuget.ts +9 -5
  82. package/src/core/tools/{web-fetch-handlers → web-scrapers}/nvd.ts +6 -1
  83. package/src/core/tools/web-scrapers/ollama.ts +267 -0
  84. package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
  85. package/src/core/tools/{web-fetch-handlers → web-scrapers}/opencorporates.ts +2 -0
  86. package/src/core/tools/{web-fetch-handlers → web-scrapers}/openlibrary.ts +18 -12
  87. package/src/core/tools/web-scrapers/orcid.ts +299 -0
  88. package/src/core/tools/{web-fetch-handlers → web-scrapers}/osv.ts +6 -1
  89. package/src/core/tools/{web-fetch-handlers → web-scrapers}/packagist.ts +6 -2
  90. package/src/core/tools/{web-fetch-handlers → web-scrapers}/pub-dev.ts +3 -3
  91. package/src/core/tools/{web-fetch-handlers → web-scrapers}/pubmed.ts +8 -4
  92. package/src/core/tools/{web-fetch-handlers → web-scrapers}/pypi.ts +7 -3
  93. package/src/core/tools/web-scrapers/rawg.ts +124 -0
  94. package/src/core/tools/{web-fetch-handlers → web-scrapers}/readthedocs.ts +7 -3
  95. package/src/core/tools/{web-fetch-handlers → web-scrapers}/reddit.ts +6 -2
  96. package/src/core/tools/{web-fetch-handlers → web-scrapers}/repology.ts +6 -1
  97. package/src/core/tools/{web-fetch-handlers → web-scrapers}/rfc.ts +7 -3
  98. package/src/core/tools/{web-fetch-handlers → web-scrapers}/rubygems.ts +6 -1
  99. package/src/core/tools/web-scrapers/searchcode.ts +217 -0
  100. package/src/core/tools/{web-fetch-handlers → web-scrapers}/sec-edgar.ts +6 -1
  101. package/src/core/tools/{web-fetch-handlers → web-scrapers}/semantic-scholar.ts +2 -2
  102. package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
  103. package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
  104. package/src/core/tools/web-scrapers/spdx.ts +121 -0
  105. package/src/core/tools/{web-fetch-handlers → web-scrapers}/spotify.ts +3 -3
  106. package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackoverflow.ts +3 -2
  107. package/src/core/tools/{web-fetch-handlers → web-scrapers}/terraform.ts +11 -3
  108. package/src/core/tools/{web-fetch-handlers → web-scrapers}/tldr.ts +6 -2
  109. package/src/core/tools/{web-fetch-handlers → web-scrapers}/twitter.ts +15 -3
  110. package/src/core/tools/{web-fetch-handlers → web-scrapers}/types.ts +98 -27
  111. package/src/core/tools/web-scrapers/utils.ts +162 -0
  112. package/src/core/tools/{web-fetch-handlers → web-scrapers}/vimeo.ts +3 -3
  113. package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
  114. package/src/core/tools/web-scrapers/w3c.ts +163 -0
  115. package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikidata.ts +13 -5
  116. package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.ts +7 -3
  117. package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.ts +72 -20
  118. package/src/core/tools/write.ts +21 -18
  119. package/src/core/voice.ts +3 -2
  120. package/src/lib/worktree/collapse.ts +2 -1
  121. package/src/lib/worktree/git.ts +2 -18
  122. package/src/main.ts +59 -3
  123. package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
  124. package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
  125. package/src/modes/interactive/components/hook-editor.ts +2 -1
  126. package/src/modes/interactive/components/model-selector.ts +19 -4
  127. package/src/modes/interactive/interactive-mode.ts +41 -38
  128. package/src/modes/interactive/theme/theme.ts +58 -58
  129. package/src/modes/rpc/rpc-mode.ts +10 -9
  130. package/src/prompts/review-request.md +27 -0
  131. package/src/prompts/reviewer.md +64 -68
  132. package/src/prompts/tools/output.md +22 -3
  133. package/src/prompts/tools/task.md +32 -33
  134. package/src/utils/clipboard.ts +2 -1
  135. package/examples/extensions/subagent/agents/reviewer.md +0 -35
  136. package/src/core/tools/web-fetch-handlers/index.ts +0 -69
  137. package/src/core/tools/web-fetch-handlers/utils.ts +0 -91
  138. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/academic.test.ts +0 -0
  139. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/business.test.ts +0 -0
  140. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/dev-platforms.test.ts +0 -0
  141. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/documentation.test.ts +0 -0
  142. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/finance-media.test.ts +0 -0
  143. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/git-hosting.test.ts +0 -0
  144. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/media.test.ts +0 -0
  145. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers-2.test.ts +0 -0
  146. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers.test.ts +0 -0
  147. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-registries.test.ts +0 -0
  148. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/research.test.ts +0 -0
  149. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/security.test.ts +0 -0
  150. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social-extended.test.ts +0 -0
  151. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social.test.ts +0 -0
  152. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackexchange.test.ts +0 -0
  153. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/standards.test.ts +0 -0
  154. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.test.ts +0 -0
  155. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.test.ts +0 -0
@@ -83,9 +83,49 @@ const toolDescriptions: Record<ToolName, string> = {
83
83
  web_fetch: "Fetch and render URLs into clean text for LLM consumption",
84
84
  web_search: "Search the web for information",
85
85
  report_finding: "Report a finding during code review",
86
- submit_review: "Submit the final code review with all findings",
87
86
  };
88
87
 
88
+ function applyTemplate(template: string, values: Record<string, string>): string {
89
+ let output = template;
90
+ for (const [key, value] of Object.entries(values)) {
91
+ output = output.replaceAll(`{{${key}}}`, value);
92
+ }
93
+ return output;
94
+ }
95
+
96
+ function appendBlock(prompt: string, block: string | null | undefined, separator = "\n\n"): string {
97
+ if (!block) return prompt;
98
+ if (block.startsWith("\n")) {
99
+ return `${prompt}${block}`;
100
+ }
101
+ return `${prompt}${separator}${block}`;
102
+ }
103
+
104
+ function appendSection(prompt: string, title: string, content: string | null | undefined): string {
105
+ if (!content) return prompt;
106
+ return `${prompt}\n\n# ${title}\n\n${content}`;
107
+ }
108
+
109
+ function formatProjectContext(contextFiles: Array<{ path: string; content: string; depth?: number }>): string | null {
110
+ if (contextFiles.length === 0) return null;
111
+ const parts: string[] = ["The following project context files have been loaded:", ""];
112
+ for (const { path: filePath, content } of contextFiles) {
113
+ parts.push(`## ${filePath}`, "", content, "");
114
+ }
115
+ return parts.join("\n").trimEnd();
116
+ }
117
+
118
+ function formatToolDescriptions(tools: Map<string, { description: string; label: string }> | undefined): string | null {
119
+ if (!tools || tools.size === 0) return null;
120
+ return Array.from(tools.entries())
121
+ .map(([name, { description }]) => `- ${name}: ${description}`)
122
+ .join("\n");
123
+ }
124
+
125
+ function buildPromptFooter(dateTime: string, cwd: string): string {
126
+ return `Current date and time: ${dateTime}\nCurrent working directory: ${cwd}`;
127
+ }
128
+
89
129
  /**
90
130
  * Generate anti-bash rules section if the agent has both bash and specialized tools.
91
131
  * Only include rules for tools that are actually available.
@@ -306,8 +346,6 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
306
346
  timeZoneName: "short",
307
347
  });
308
348
 
309
- const appendSection = resolvedAppendPrompt ? `\n\n${resolvedAppendPrompt}` : "";
310
-
311
349
  // Resolve context files: use provided or discover
312
350
  const contextFiles = providedContextFiles ?? loadProjectContextFiles({ cwd: resolvedCwd });
313
351
 
@@ -324,46 +362,22 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
324
362
  ? `${systemPromptCustomization}\n\n${resolvedCustomPrompt}`
325
363
  : resolvedCustomPrompt;
326
364
 
327
- if (appendSection) {
328
- prompt += appendSection;
329
- }
330
-
331
- // Append project context files
332
- if (contextFiles.length > 0) {
333
- prompt += "\n\n# Project Context\n\n";
334
- prompt += "The following project context files have been loaded:\n\n";
335
- for (const { path: filePath, content } of contextFiles) {
336
- prompt += `## ${filePath}\n\n${content}\n\n`;
337
- }
338
- }
339
-
340
- // Append custom tool descriptions if provided
341
- if (tools && tools.size > 0) {
342
- prompt += "\n\n# Tools\n\n";
343
- prompt += Array.from(tools.entries())
344
- .map(([name, { description }]) => `- ${name}: ${description}`)
345
- .join("\n");
346
- }
365
+ prompt = appendBlock(prompt, resolvedAppendPrompt);
366
+ prompt = appendSection(prompt, "Project Context", formatProjectContext(contextFiles));
367
+ prompt = appendSection(prompt, "Tools", formatToolDescriptions(tools));
347
368
 
348
- // Append git context if in a git repo
349
369
  const gitContext = loadGitContext(resolvedCwd);
350
- if (gitContext) {
351
- prompt += `\n\n# Git Status\n\n${gitContext}`;
352
- }
370
+ prompt = appendSection(prompt, "Git Status", gitContext);
353
371
 
354
- // Append skills section (only if read tool is available)
355
372
  if (tools?.has("read") && skills.length > 0) {
356
- prompt += formatSkillsForPrompt(skills);
373
+ prompt = appendBlock(prompt, formatSkillsForPrompt(skills));
357
374
  }
358
375
 
359
- // Append rules section (always enabled when rules exist)
360
376
  if (rulebookRules && rulebookRules.length > 0) {
361
- prompt += formatRulesForPrompt(rulebookRules);
377
+ prompt = appendBlock(prompt, formatRulesForPrompt(rulebookRules));
362
378
  }
363
379
 
364
- // Add date/time and working directory last
365
- prompt += `\nCurrent date and time: ${dateTime}`;
366
- prompt += `\nCurrent working directory: ${resolvedCwd}`;
380
+ prompt = appendBlock(prompt, buildPromptFooter(dateTime, resolvedCwd), "\n");
367
381
 
368
382
  return prompt;
369
383
  }
@@ -428,46 +442,30 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
428
442
 
429
443
  // Build the prompt with anti-bash rules prominently placed
430
444
  const antiBashBlock = antiBashSection ? `\n${antiBashSection}\n` : "";
431
- let prompt = systemPromptTemplate
432
- .replaceAll("{{toolsList}}", toolsList)
433
- .replaceAll("{{antiBashSection}}", antiBashBlock)
434
- .replaceAll("{{guidelines}}", guidelines)
435
- .replaceAll("{{readmePath}}", readmePath)
436
- .replaceAll("{{docsPath}}", docsPath)
437
- .replaceAll("{{examplesPath}}", examplesPath);
438
-
439
- if (appendSection) {
440
- prompt += appendSection;
441
- }
445
+ let prompt = applyTemplate(systemPromptTemplate, {
446
+ toolsList,
447
+ antiBashSection: antiBashBlock,
448
+ guidelines,
449
+ readmePath,
450
+ docsPath,
451
+ examplesPath,
452
+ });
442
453
 
443
- // Append project context files
444
- if (contextFiles.length > 0) {
445
- prompt += "\n\n# Project Context\n\n";
446
- prompt += "The following project context files have been loaded:\n\n";
447
- for (const { path: filePath, content } of contextFiles) {
448
- prompt += `## ${filePath}\n\n${content}\n\n`;
449
- }
450
- }
454
+ prompt = appendBlock(prompt, resolvedAppendPrompt);
455
+ prompt = appendSection(prompt, "Project Context", formatProjectContext(contextFiles));
451
456
 
452
- // Append git context if in a git repo
453
457
  const gitContext = loadGitContext(resolvedCwd);
454
- if (gitContext) {
455
- prompt += `\n\n# Git Status\n\n${gitContext}`;
456
- }
458
+ prompt = appendSection(prompt, "Git Status", gitContext);
457
459
 
458
- // Append skills section (only if read tool is available)
459
460
  if (hasRead && skills.length > 0) {
460
- prompt += formatSkillsForPrompt(skills);
461
+ prompt = appendBlock(prompt, formatSkillsForPrompt(skills));
461
462
  }
462
463
 
463
- // Append rules section (always enabled when rules exist)
464
464
  if (rulebookRules && rulebookRules.length > 0) {
465
- prompt += formatRulesForPrompt(rulebookRules);
465
+ prompt = appendBlock(prompt, formatRulesForPrompt(rulebookRules));
466
466
  }
467
467
 
468
- // Add date/time and working directory last
469
- prompt += `\nCurrent date and time: ${dateTime}`;
470
- prompt += `\nCurrent working directory: ${resolvedCwd}`;
468
+ prompt = appendBlock(prompt, buildPromptFooter(dateTime, resolvedCwd), "\n");
471
469
 
472
470
  // Prepend SYSTEM.md customization if present
473
471
  if (systemPromptCustomization) {
@@ -23,7 +23,7 @@ import { type Theme, theme } from "../../modes/interactive/theme/theme";
23
23
  import askDescription from "../../prompts/tools/ask.md" with { type: "text" };
24
24
  import type { RenderResultOptions } from "../custom-tools/types";
25
25
  import type { ToolSession } from "./index";
26
- import { formatErrorMessage, formatMeta } from "./render-utils";
26
+ import { createToolUIKit } from "./render-utils";
27
27
 
28
28
  // =============================================================================
29
29
  // Types
@@ -218,17 +218,18 @@ interface AskRenderArgs {
218
218
 
219
219
  export const askToolRenderer = {
220
220
  renderCall(args: AskRenderArgs, uiTheme: Theme): Component {
221
+ const ui = createToolUIKit(uiTheme);
221
222
  if (!args.question) {
222
- return new Text(formatErrorMessage("No question provided", uiTheme), 0, 0);
223
+ return new Text(ui.errorMessage("No question provided"), 0, 0);
223
224
  }
224
225
 
225
- const label = uiTheme.fg("toolTitle", uiTheme.bold("Ask"));
226
+ const label = ui.title("Ask");
226
227
  let text = `${label} ${uiTheme.fg("accent", args.question)}`;
227
228
 
228
229
  const meta: string[] = [];
229
230
  if (args.multi) meta.push("multi");
230
231
  if (args.options?.length) meta.push(`options:${args.options.length}`);
231
- text += formatMeta(meta, uiTheme);
232
+ text += ui.meta(meta);
232
233
 
233
234
  if (args.options?.length) {
234
235
  for (let i = 0; i < args.options.length; i++) {
@@ -6,6 +6,8 @@
6
6
  * the specialized tools instead.
7
7
  */
8
8
 
9
+ import { type BashInterceptorRule, DEFAULT_BASH_INTERCEPTOR_RULES } from "../settings-manager";
10
+
9
11
  export interface InterceptionResult {
10
12
  /** If true, the bash command should be blocked */
11
13
  block: boolean;
@@ -16,62 +18,20 @@ export interface InterceptionResult {
16
18
  }
17
19
 
18
20
  /**
19
- * Patterns that should NEVER use bash when specialized tools exist.
20
- * Each pattern maps to a helpful error message.
21
+ * Compile bash interceptor rules into regexes, skipping invalid patterns.
21
22
  */
22
- const forbiddenPatterns: Array<{
23
- pattern: RegExp;
24
- tool: string;
25
- message: string;
26
- }> = [
27
- // File reading
28
- {
29
- pattern: /^\s*(cat|head|tail|less|more)\s+/,
30
- tool: "read",
31
- message: "Use the `read` tool instead of cat/head/tail. It provides better context and handles binary files.",
32
- },
33
- // Content search (grep variants)
34
- {
35
- pattern: /^\s*(grep|rg|ripgrep|ag|ack)\s+/,
36
- tool: "grep",
37
- message: "Use the `grep` tool instead of grep/rg. It respects .gitignore and provides structured output.",
38
- },
39
- // Git operations
40
- {
41
- pattern: /^\s*git(\s+|$)/,
42
- tool: "git",
43
- message:
44
- "Use the `git` tool instead of running git in bash. It provides structured output and safety confirmations.",
45
- },
46
- // File finding
47
- {
48
- pattern: /^\s*(find|fd|locate)\s+.*(-name|-iname|-type|--type|-glob)/,
49
- tool: "find",
50
- message: "Use the `find` tool instead of find/fd. It respects .gitignore and is faster for glob patterns.",
51
- },
52
- // In-place file editing
53
- {
54
- pattern: /^\s*sed\s+(-i|--in-place)/,
55
- tool: "edit",
56
- message: "Use the `edit` tool instead of sed -i. It provides diff preview and fuzzy matching.",
57
- },
58
- {
59
- pattern: /^\s*perl\s+.*-[pn]?i/,
60
- tool: "edit",
61
- message: "Use the `edit` tool instead of perl -i. It provides diff preview and fuzzy matching.",
62
- },
63
- {
64
- pattern: /^\s*awk\s+.*-i\s+inplace/,
65
- tool: "edit",
66
- message: "Use the `edit` tool instead of awk -i inplace. It provides diff preview and fuzzy matching.",
67
- },
68
- // File creation via redirection (but allow legitimate uses like piping)
69
- {
70
- pattern: /^\s*(echo|printf|cat\s*<<)\s+.*[^|]>\s*\S/,
71
- tool: "write",
72
- message: "Use the `write` tool instead of echo/cat redirection. It handles encoding and provides confirmation.",
73
- },
74
- ];
23
+ function compileRules(rules: BashInterceptorRule[]): Array<{ rule: BashInterceptorRule; regex: RegExp }> {
24
+ const compiled: Array<{ rule: BashInterceptorRule; regex: RegExp }> = [];
25
+ for (const rule of rules) {
26
+ const flags = rule.flags ?? "";
27
+ try {
28
+ compiled.push({ rule, regex: new RegExp(rule.pattern, flags) });
29
+ } catch {
30
+ // Skip invalid regex patterns
31
+ }
32
+ }
33
+ return compiled;
34
+ }
75
35
 
76
36
  /**
77
37
  * Check if a bash command should be intercepted.
@@ -80,21 +40,26 @@ const forbiddenPatterns: Array<{
80
40
  * @param availableTools Set of tool names that are available
81
41
  * @returns InterceptionResult indicating if the command should be blocked
82
42
  */
83
- export function checkBashInterception(command: string, availableTools: string[]): InterceptionResult {
43
+ export function checkBashInterception(
44
+ command: string,
45
+ availableTools: string[],
46
+ rules: BashInterceptorRule[] = DEFAULT_BASH_INTERCEPTOR_RULES,
47
+ ): InterceptionResult {
84
48
  // Normalize command for pattern matching
85
49
  const normalizedCommand = command.trim();
50
+ const compiled = compileRules(rules);
86
51
 
87
- for (const { pattern, tool, message } of forbiddenPatterns) {
52
+ for (const { rule, regex } of compiled) {
88
53
  // Only block if the suggested tool is actually available
89
- if (!availableTools.includes(tool)) {
54
+ if (!availableTools.includes(rule.tool)) {
90
55
  continue;
91
56
  }
92
57
 
93
- if (pattern.test(normalizedCommand)) {
58
+ if (regex.test(normalizedCommand)) {
94
59
  return {
95
60
  block: true,
96
- message: `❌ Blocked: ${message}\n\nOriginal command: ${command}`,
97
- suggestedTool: tool,
61
+ message: `Blocked: ${rule.message}\n\nOriginal command: ${command}`,
62
+ suggestedTool: rule.tool,
98
63
  };
99
64
  }
100
65
  }
@@ -8,7 +8,7 @@ import { executeBash } from "../bash-executor";
8
8
  import type { RenderResultOptions } from "../custom-tools/types";
9
9
  import { checkBashInterception, checkSimpleLsInterception } from "./bash-interceptor";
10
10
  import type { ToolSession } from "./index";
11
- import { formatBytes, wrapBrackets } from "./render-utils";
11
+ import { createToolUIKit } from "./render-utils";
12
12
  import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateTail } from "./truncate";
13
13
 
14
14
  const bashSchema = Type.Object({
@@ -36,13 +36,16 @@ export function createBashTool(session: ToolSession): AgentTool<typeof bashSchem
36
36
  ) => {
37
37
  // Check interception if enabled and available tools are known
38
38
  if (session.settings?.getBashInterceptorEnabled()) {
39
- const interception = checkBashInterception(command, ctx?.toolNames ?? []);
39
+ const rules = session.settings?.getBashInterceptorRules?.();
40
+ const interception = checkBashInterception(command, ctx?.toolNames ?? [], rules);
40
41
  if (interception.block) {
41
42
  throw new Error(interception.message);
42
43
  }
43
- const lsInterception = checkSimpleLsInterception(command, ctx?.toolNames ?? []);
44
- if (lsInterception.block) {
45
- throw new Error(lsInterception.message);
44
+ if (session.settings?.getBashInterceptorSimpleLsEnabled?.() !== false) {
45
+ const lsInterception = checkSimpleLsInterception(command, ctx?.toolNames ?? []);
46
+ if (lsInterception.block) {
47
+ throw new Error(lsInterception.message);
48
+ }
46
49
  }
47
50
  }
48
51
 
@@ -127,8 +130,9 @@ interface BashRenderContext {
127
130
 
128
131
  export const bashToolRenderer = {
129
132
  renderCall(args: BashRenderArgs, uiTheme: Theme): Component {
133
+ const ui = createToolUIKit(uiTheme);
130
134
  const command = args.command || uiTheme.format.ellipsis;
131
- const text = uiTheme.fg("toolTitle", uiTheme.bold(`$ ${command}`));
135
+ const text = ui.title(`$ ${command}`);
132
136
  return new Text(text, 0, 0);
133
137
  },
134
138
 
@@ -140,6 +144,7 @@ export const bashToolRenderer = {
140
144
  options: RenderResultOptions & { renderContext?: BashRenderContext },
141
145
  uiTheme: Theme,
142
146
  ): Component {
147
+ const ui = createToolUIKit(uiTheme);
143
148
  const { expanded, renderContext } = options;
144
149
  const details = result.details;
145
150
  const lines: string[] = [];
@@ -195,11 +200,11 @@ export const bashToolRenderer = {
195
200
  warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
196
201
  } else {
197
202
  warnings.push(
198
- `Truncated: ${truncation.outputLines} lines shown (${formatBytes(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`,
203
+ `Truncated: ${truncation.outputLines} lines shown (${ui.formatBytes(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`,
199
204
  );
200
205
  }
201
206
  }
202
- lines.push(uiTheme.fg("warning", wrapBrackets(warnings.join(". "), uiTheme)));
207
+ lines.push(uiTheme.fg("warning", ui.wrapBrackets(warnings.join(". "))));
203
208
  }
204
209
 
205
210
  return new Text(lines.join("\n"), 0, 0);
@@ -3,8 +3,6 @@
3
3
  * Used by both edit.ts (for execution) and tool-execution.ts (for preview rendering).
4
4
  */
5
5
 
6
- import { constants } from "node:fs";
7
- import { access, readFile } from "node:fs/promises";
8
6
  import * as Diff from "diff";
9
7
  import { resolveToCwd } from "./path-utils";
10
8
 
@@ -428,14 +426,23 @@ export async function computeEditDiff(
428
426
 
429
427
  try {
430
428
  // Check if file exists and is readable
429
+ const file = Bun.file(absolutePath);
431
430
  try {
432
- await access(absolutePath, constants.R_OK);
431
+ if (!(await file.exists())) {
432
+ return { error: `File not found: ${path}` };
433
+ }
433
434
  } catch {
434
435
  return { error: `File not found: ${path}` };
435
436
  }
436
437
 
437
438
  // Read the file
438
- const rawContent = await readFile(absolutePath, "utf-8");
439
+ let rawContent: string;
440
+ try {
441
+ rawContent = await file.text();
442
+ } catch (error) {
443
+ const message = error instanceof Error ? error.message : String(error);
444
+ return { error: message || `Unable to read ${path}` };
445
+ }
439
446
 
440
447
  // Strip BOM before matching (LLM won't include invisible BOM in oldText)
441
448
  const { text: content } = stripBom(rawContent);
@@ -20,14 +20,7 @@ import {
20
20
  import type { ToolSession } from "./index";
21
21
  import { createLspWritethrough, type FileDiagnosticsResult } from "./lsp/index";
22
22
  import { resolveToCwd } from "./path-utils";
23
- import {
24
- formatDiagnostics,
25
- formatDiffStats,
26
- getDiffStats,
27
- shortenPath,
28
- truncateDiffByHunk,
29
- wrapBrackets,
30
- } from "./render-utils";
23
+ import { createToolUIKit, getDiffStats, shortenPath, truncateDiffByHunk } from "./render-utils";
31
24
 
32
25
  const editSchema = Type.Object({
33
26
  path: Type.String({ description: "Path to the file to edit (relative or absolute)" }),
@@ -237,13 +230,14 @@ function formatMetadataLine(lineCount: number | null, language: string | undefin
237
230
 
238
231
  export const editToolRenderer = {
239
232
  renderCall(args: EditRenderArgs, uiTheme: Theme): Component {
233
+ const ui = createToolUIKit(uiTheme);
240
234
  const rawPath = args.file_path || args.path || "";
241
235
  const filePath = shortenPath(rawPath);
242
236
  const editLanguage = getLanguageFromPath(rawPath) ?? "text";
243
237
  const editIcon = uiTheme.fg("muted", uiTheme.getLangIcon(editLanguage));
244
238
  const pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", uiTheme.format.ellipsis);
245
239
 
246
- const text = `${uiTheme.fg("toolTitle", uiTheme.bold("Edit"))} ${editIcon} ${pathDisplay}`;
240
+ const text = `${ui.title("Edit")} ${editIcon} ${pathDisplay}`;
247
241
  return new Text(text, 0, 0);
248
242
  },
249
243
 
@@ -253,6 +247,7 @@ export const editToolRenderer = {
253
247
  uiTheme: Theme,
254
248
  args?: EditRenderArgs,
255
249
  ): Component {
250
+ const ui = createToolUIKit(uiTheme);
256
251
  const { expanded, renderContext } = options;
257
252
  const rawPath = args?.file_path || args?.path || "";
258
253
  const filePath = shortenPath(rawPath);
@@ -287,11 +282,10 @@ export const editToolRenderer = {
287
282
  text += `\n\n${uiTheme.fg("error", editDiffPreview.error)}`;
288
283
  } else if (editDiffPreview.diff) {
289
284
  const diffStats = getDiffStats(editDiffPreview.diff);
290
- text += `\n${uiTheme.fg("dim", uiTheme.format.bracketLeft)}${formatDiffStats(
285
+ text += `\n${uiTheme.fg("dim", uiTheme.format.bracketLeft)}${ui.formatDiffStats(
291
286
  diffStats.added,
292
287
  diffStats.removed,
293
288
  diffStats.hunks,
294
- uiTheme,
295
289
  )}${uiTheme.fg("dim", uiTheme.format.bracketRight)}`;
296
290
 
297
291
  const {
@@ -309,7 +303,7 @@ export const editToolRenderer = {
309
303
  if (hiddenLines > 0) remainder.push(`${hiddenLines} more lines`);
310
304
  text += uiTheme.fg(
311
305
  "toolOutput",
312
- `\n${uiTheme.format.ellipsis} (${remainder.join(", ")}) ${wrapBrackets("Ctrl+O to expand", uiTheme)}`,
306
+ `\n${uiTheme.format.ellipsis} (${remainder.join(", ")}) ${ui.wrapBrackets("Ctrl+O to expand")}`,
313
307
  );
314
308
  }
315
309
  }
@@ -317,7 +311,7 @@ export const editToolRenderer = {
317
311
 
318
312
  // Show LSP diagnostics if available
319
313
  if (result.details?.diagnostics) {
320
- text += formatDiagnostics(result.details.diagnostics, expanded, uiTheme, (fp) =>
314
+ text += ui.formatDiagnostics(result.details.diagnostics, expanded, (fp: string) =>
321
315
  uiTheme.getLangIcon(getLanguageFromPath(fp)),
322
316
  );
323
317
  }