@oh-my-pi/pi-coding-agent 3.30.0 → 3.32.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.
- package/CHANGELOG.md +85 -0
- package/package.json +5 -5
- package/src/cli/args.ts +4 -0
- package/src/core/agent-session.ts +29 -2
- package/src/core/bash-executor.ts +2 -1
- package/src/core/custom-commands/bundled/review/index.ts +367 -14
- package/src/core/custom-commands/bundled/wt/index.ts +1 -1
- package/src/core/sdk.ts +10 -2
- package/src/core/session-manager.ts +158 -246
- package/src/core/session-storage.ts +379 -0
- package/src/core/settings-manager.ts +155 -4
- package/src/core/slash-commands.ts +39 -13
- package/src/core/system-prompt.ts +62 -64
- package/src/core/tools/ask.ts +5 -4
- package/src/core/tools/bash-interceptor.ts +26 -61
- package/src/core/tools/bash.ts +13 -8
- package/src/core/tools/edit-diff.ts +11 -4
- package/src/core/tools/edit.ts +7 -13
- package/src/core/tools/find.ts +111 -50
- package/src/core/tools/gemini-image.ts +128 -147
- package/src/core/tools/grep.ts +397 -415
- package/src/core/tools/index.test.ts +5 -1
- package/src/core/tools/index.ts +8 -4
- package/src/core/tools/ls.ts +12 -10
- package/src/core/tools/lsp/client.ts +84 -19
- package/src/core/tools/lsp/config.ts +205 -656
- package/src/core/tools/lsp/defaults.json +465 -0
- package/src/core/tools/lsp/index.ts +72 -35
- package/src/core/tools/lsp/rust-analyzer.ts +49 -10
- package/src/core/tools/lsp/types.ts +1 -0
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/read.ts +150 -74
- package/src/core/tools/render-utils.ts +70 -10
- package/src/core/tools/review.ts +38 -126
- package/src/core/tools/task/artifacts.ts +5 -4
- package/src/core/tools/task/commands.ts +4 -0
- package/src/core/tools/task/executor.ts +94 -83
- package/src/core/tools/task/index.ts +130 -92
- package/src/core/tools/task/parallel.ts +30 -3
- package/src/core/tools/task/render.ts +85 -39
- package/src/core/tools/task/types.ts +15 -6
- package/src/core/tools/task/worker.ts +124 -89
- package/src/core/tools/web-fetch.ts +112 -377
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/artifacthub.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/arxiv.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/aur.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/biorxiv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/bluesky.ts +10 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/brew.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/cheatsh.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/chocolatey.ts +6 -1
- package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
- package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
- package/src/core/tools/web-scrapers/clojars.ts +180 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/coingecko.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/crates-io.ts +7 -2
- package/src/core/tools/web-scrapers/crossref.ts +149 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/devto.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/discogs.ts +6 -1
- package/src/core/tools/web-scrapers/discourse.ts +221 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/dockerhub.ts +7 -3
- package/src/core/tools/web-scrapers/fdroid.ts +158 -0
- package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
- package/src/core/tools/web-scrapers/flathub.ts +239 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/github-gist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/github.ts +63 -32
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/gitlab.ts +31 -19
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/go-pkg.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackage.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackernews.ts +18 -18
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hex.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/huggingface.ts +10 -10
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/iacr.ts +8 -4
- package/src/core/tools/web-scrapers/index.ts +250 -0
- package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
- package/src/core/tools/web-scrapers/lemmy.ts +220 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/lobsters.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mastodon.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/maven.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mdn.ts +2 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/metacpan.ts +13 -7
- package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/npm.ts +12 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nuget.ts +9 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nvd.ts +6 -1
- package/src/core/tools/web-scrapers/ollama.ts +267 -0
- package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/opencorporates.ts +2 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/openlibrary.ts +18 -12
- package/src/core/tools/web-scrapers/orcid.ts +299 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/osv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/packagist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pub-dev.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pubmed.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pypi.ts +7 -3
- package/src/core/tools/web-scrapers/rawg.ts +124 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/readthedocs.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/reddit.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/repology.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rfc.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rubygems.ts +6 -1
- package/src/core/tools/web-scrapers/searchcode.ts +217 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/sec-edgar.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/semantic-scholar.ts +2 -2
- package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
- package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
- package/src/core/tools/web-scrapers/spdx.ts +121 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/spotify.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackoverflow.ts +3 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/terraform.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/tldr.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/twitter.ts +15 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/types.ts +98 -27
- package/src/core/tools/web-scrapers/utils.ts +162 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/vimeo.ts +3 -3
- package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
- package/src/core/tools/web-scrapers/w3c.ts +163 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikidata.ts +13 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.ts +72 -20
- package/src/core/tools/write.ts +21 -18
- package/src/core/voice.ts +3 -2
- package/src/lib/worktree/collapse.ts +2 -1
- package/src/lib/worktree/git.ts +2 -18
- package/src/main.ts +59 -3
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
- package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
- package/src/modes/interactive/components/hook-editor.ts +2 -1
- package/src/modes/interactive/components/model-selector.ts +19 -4
- package/src/modes/interactive/interactive-mode.ts +41 -63
- package/src/modes/interactive/theme/theme.ts +58 -58
- package/src/modes/rpc/rpc-mode.ts +10 -9
- package/src/prompts/review-request.md +27 -0
- package/src/prompts/reviewer.md +64 -68
- package/src/prompts/tools/output.md +22 -3
- package/src/prompts/tools/task.md +32 -33
- package/src/utils/clipboard.ts +2 -1
- package/examples/extensions/subagent/agents/reviewer.md +0 -35
- package/src/core/tools/web-fetch-handlers/index.ts +0 -69
- package/src/core/tools/web-fetch-handlers/utils.ts +0 -91
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/academic.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/business.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/dev-platforms.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/documentation.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/finance-media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/git-hosting.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers-2.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-registries.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/research.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/security.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social-extended.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackexchange.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/standards.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.test.ts +0 -0
- /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
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
377
|
+
prompt = appendBlock(prompt, formatRulesForPrompt(rulebookRules));
|
|
362
378
|
}
|
|
363
379
|
|
|
364
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
444
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
465
|
+
prompt = appendBlock(prompt, formatRulesForPrompt(rulebookRules));
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
-
|
|
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) {
|
package/src/core/tools/ask.ts
CHANGED
|
@@ -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 {
|
|
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(
|
|
223
|
+
return new Text(ui.errorMessage("No question provided"), 0, 0);
|
|
223
224
|
}
|
|
224
225
|
|
|
225
|
-
const label =
|
|
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 +=
|
|
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
|
-
*
|
|
20
|
-
* Each pattern maps to a helpful error message.
|
|
21
|
+
* Compile bash interceptor rules into regexes, skipping invalid patterns.
|
|
21
22
|
*/
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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(
|
|
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 {
|
|
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 (
|
|
58
|
+
if (regex.test(normalizedCommand)) {
|
|
94
59
|
return {
|
|
95
60
|
block: true,
|
|
96
|
-
message:
|
|
97
|
-
suggestedTool: tool,
|
|
61
|
+
message: `Blocked: ${rule.message}\n\nOriginal command: ${command}`,
|
|
62
|
+
suggestedTool: rule.tool,
|
|
98
63
|
};
|
|
99
64
|
}
|
|
100
65
|
}
|
package/src/core/tools/bash.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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 =
|
|
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(". ")
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/src/core/tools/edit.ts
CHANGED
|
@@ -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 = `${
|
|
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"
|
|
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,
|
|
314
|
+
text += ui.formatDiagnostics(result.details.diagnostics, expanded, (fp: string) =>
|
|
321
315
|
uiTheme.getLangIcon(getLanguageFromPath(fp)),
|
|
322
316
|
);
|
|
323
317
|
}
|