@oh-my-pi/pi-coding-agent 0.1.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 +1629 -0
- package/README.md +1041 -0
- package/docs/compaction.md +403 -0
- package/docs/config-usage.md +113 -0
- package/docs/custom-tools.md +541 -0
- package/docs/extension-loading.md +1004 -0
- package/docs/hooks.md +867 -0
- package/docs/rpc.md +1040 -0
- package/docs/sdk.md +994 -0
- package/docs/session-tree-plan.md +441 -0
- package/docs/session.md +240 -0
- package/docs/skills.md +290 -0
- package/docs/theme.md +670 -0
- package/docs/tree.md +197 -0
- package/docs/tui.md +341 -0
- package/examples/README.md +21 -0
- package/examples/custom-tools/README.md +124 -0
- package/examples/custom-tools/hello/index.ts +20 -0
- package/examples/custom-tools/question/index.ts +84 -0
- package/examples/custom-tools/subagent/README.md +172 -0
- package/examples/custom-tools/subagent/agents/planner.md +37 -0
- package/examples/custom-tools/subagent/agents/scout.md +50 -0
- package/examples/custom-tools/subagent/agents/worker.md +24 -0
- package/examples/custom-tools/subagent/agents.ts +156 -0
- package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
- package/examples/custom-tools/subagent/commands/implement.md +10 -0
- package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
- package/examples/custom-tools/subagent/index.ts +1002 -0
- package/examples/custom-tools/todo/index.ts +212 -0
- package/examples/hooks/README.md +56 -0
- package/examples/hooks/auto-commit-on-exit.ts +49 -0
- package/examples/hooks/confirm-destructive.ts +59 -0
- package/examples/hooks/custom-compaction.ts +116 -0
- package/examples/hooks/dirty-repo-guard.ts +52 -0
- package/examples/hooks/file-trigger.ts +41 -0
- package/examples/hooks/git-checkpoint.ts +53 -0
- package/examples/hooks/handoff.ts +150 -0
- package/examples/hooks/permission-gate.ts +34 -0
- package/examples/hooks/protected-paths.ts +30 -0
- package/examples/hooks/qna.ts +119 -0
- package/examples/hooks/snake.ts +343 -0
- package/examples/hooks/status-line.ts +40 -0
- package/examples/sdk/01-minimal.ts +22 -0
- package/examples/sdk/02-custom-model.ts +49 -0
- package/examples/sdk/03-custom-prompt.ts +44 -0
- package/examples/sdk/04-skills.ts +44 -0
- package/examples/sdk/05-tools.ts +90 -0
- package/examples/sdk/06-hooks.ts +61 -0
- package/examples/sdk/07-context-files.ts +36 -0
- package/examples/sdk/08-slash-commands.ts +42 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
- package/examples/sdk/10-settings.ts +38 -0
- package/examples/sdk/11-sessions.ts +48 -0
- package/examples/sdk/12-full-control.ts +95 -0
- package/examples/sdk/README.md +154 -0
- package/package.json +89 -0
- package/src/bun-imports.d.ts +16 -0
- package/src/capability/context-file.ts +40 -0
- package/src/capability/extension.ts +48 -0
- package/src/capability/hook.ts +40 -0
- package/src/capability/index.ts +616 -0
- package/src/capability/instruction.ts +37 -0
- package/src/capability/mcp.ts +52 -0
- package/src/capability/prompt.ts +35 -0
- package/src/capability/rule.ts +56 -0
- package/src/capability/settings.ts +35 -0
- package/src/capability/skill.ts +49 -0
- package/src/capability/slash-command.ts +40 -0
- package/src/capability/system-prompt.ts +35 -0
- package/src/capability/tool.ts +38 -0
- package/src/capability/types.ts +166 -0
- package/src/cli/args.ts +259 -0
- package/src/cli/file-processor.ts +121 -0
- package/src/cli/list-models.ts +104 -0
- package/src/cli/plugin-cli.ts +661 -0
- package/src/cli/session-picker.ts +41 -0
- package/src/cli/update-cli.ts +274 -0
- package/src/cli.ts +10 -0
- package/src/config.ts +391 -0
- package/src/core/agent-session.ts +2178 -0
- package/src/core/auth-storage.ts +258 -0
- package/src/core/bash-executor.ts +197 -0
- package/src/core/compaction/branch-summarization.ts +315 -0
- package/src/core/compaction/compaction.ts +664 -0
- package/src/core/compaction/index.ts +7 -0
- package/src/core/compaction/utils.ts +153 -0
- package/src/core/custom-commands/bundled/review/index.ts +156 -0
- package/src/core/custom-commands/index.ts +15 -0
- package/src/core/custom-commands/loader.ts +226 -0
- package/src/core/custom-commands/types.ts +112 -0
- package/src/core/custom-tools/index.ts +22 -0
- package/src/core/custom-tools/loader.ts +248 -0
- package/src/core/custom-tools/types.ts +185 -0
- package/src/core/custom-tools/wrapper.ts +29 -0
- package/src/core/exec.ts +139 -0
- package/src/core/export-html/index.ts +159 -0
- package/src/core/export-html/template.css +774 -0
- package/src/core/export-html/template.generated.ts +2 -0
- package/src/core/export-html/template.html +45 -0
- package/src/core/export-html/template.js +1185 -0
- package/src/core/export-html/template.macro.ts +24 -0
- package/src/core/file-mentions.ts +54 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/loader.ts +288 -0
- package/src/core/hooks/runner.ts +434 -0
- package/src/core/hooks/tool-wrapper.ts +98 -0
- package/src/core/hooks/types.ts +770 -0
- package/src/core/index.ts +53 -0
- package/src/core/logger.ts +112 -0
- package/src/core/mcp/client.ts +185 -0
- package/src/core/mcp/config.ts +248 -0
- package/src/core/mcp/index.ts +45 -0
- package/src/core/mcp/loader.ts +99 -0
- package/src/core/mcp/manager.ts +235 -0
- package/src/core/mcp/tool-bridge.ts +156 -0
- package/src/core/mcp/transports/http.ts +316 -0
- package/src/core/mcp/transports/index.ts +6 -0
- package/src/core/mcp/transports/stdio.ts +252 -0
- package/src/core/mcp/types.ts +228 -0
- package/src/core/messages.ts +211 -0
- package/src/core/model-registry.ts +334 -0
- package/src/core/model-resolver.ts +494 -0
- package/src/core/plugins/doctor.ts +67 -0
- package/src/core/plugins/index.ts +38 -0
- package/src/core/plugins/installer.ts +189 -0
- package/src/core/plugins/loader.ts +339 -0
- package/src/core/plugins/manager.ts +672 -0
- package/src/core/plugins/parser.ts +105 -0
- package/src/core/plugins/paths.ts +37 -0
- package/src/core/plugins/types.ts +190 -0
- package/src/core/sdk.ts +900 -0
- package/src/core/session-manager.ts +1837 -0
- package/src/core/settings-manager.ts +860 -0
- package/src/core/skills.ts +352 -0
- package/src/core/slash-commands.ts +132 -0
- package/src/core/system-prompt.ts +442 -0
- package/src/core/timings.ts +25 -0
- package/src/core/title-generator.ts +110 -0
- package/src/core/tools/ask.ts +193 -0
- package/src/core/tools/bash-interceptor.ts +120 -0
- package/src/core/tools/bash.ts +91 -0
- package/src/core/tools/context.ts +32 -0
- package/src/core/tools/edit-diff.ts +487 -0
- package/src/core/tools/edit.ts +140 -0
- package/src/core/tools/exa/company.ts +59 -0
- package/src/core/tools/exa/index.ts +63 -0
- package/src/core/tools/exa/linkedin.ts +59 -0
- package/src/core/tools/exa/mcp-client.ts +368 -0
- package/src/core/tools/exa/render.ts +200 -0
- package/src/core/tools/exa/researcher.ts +90 -0
- package/src/core/tools/exa/search.ts +338 -0
- package/src/core/tools/exa/types.ts +167 -0
- package/src/core/tools/exa/websets.ts +248 -0
- package/src/core/tools/find.ts +244 -0
- package/src/core/tools/grep.ts +584 -0
- package/src/core/tools/index.ts +283 -0
- package/src/core/tools/ls.ts +142 -0
- package/src/core/tools/lsp/client.ts +767 -0
- package/src/core/tools/lsp/clients/biome-client.ts +207 -0
- package/src/core/tools/lsp/clients/index.ts +49 -0
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
- package/src/core/tools/lsp/config.ts +845 -0
- package/src/core/tools/lsp/edits.ts +110 -0
- package/src/core/tools/lsp/index.ts +1364 -0
- package/src/core/tools/lsp/render.ts +560 -0
- package/src/core/tools/lsp/rust-analyzer.ts +145 -0
- package/src/core/tools/lsp/types.ts +495 -0
- package/src/core/tools/lsp/utils.ts +526 -0
- package/src/core/tools/notebook.ts +182 -0
- package/src/core/tools/output.ts +198 -0
- package/src/core/tools/path-utils.ts +61 -0
- package/src/core/tools/read.ts +507 -0
- package/src/core/tools/renderers.ts +820 -0
- package/src/core/tools/review.ts +275 -0
- package/src/core/tools/rulebook.ts +124 -0
- package/src/core/tools/task/agents.ts +158 -0
- package/src/core/tools/task/artifacts.ts +114 -0
- package/src/core/tools/task/commands.ts +157 -0
- package/src/core/tools/task/discovery.ts +217 -0
- package/src/core/tools/task/executor.ts +531 -0
- package/src/core/tools/task/index.ts +548 -0
- package/src/core/tools/task/model-resolver.ts +176 -0
- package/src/core/tools/task/parallel.ts +38 -0
- package/src/core/tools/task/render.ts +502 -0
- package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
- package/src/core/tools/task/types.ts +142 -0
- package/src/core/tools/truncate.ts +265 -0
- package/src/core/tools/web-fetch.ts +2511 -0
- package/src/core/tools/web-search/auth.ts +199 -0
- package/src/core/tools/web-search/index.ts +583 -0
- package/src/core/tools/web-search/providers/anthropic.ts +198 -0
- package/src/core/tools/web-search/providers/exa.ts +196 -0
- package/src/core/tools/web-search/providers/perplexity.ts +195 -0
- package/src/core/tools/web-search/render.ts +372 -0
- package/src/core/tools/web-search/types.ts +180 -0
- package/src/core/tools/write.ts +63 -0
- package/src/core/ttsr.ts +211 -0
- package/src/core/utils.ts +187 -0
- package/src/discovery/agents-md.ts +75 -0
- package/src/discovery/builtin.ts +647 -0
- package/src/discovery/claude.ts +623 -0
- package/src/discovery/cline.ts +104 -0
- package/src/discovery/codex.ts +571 -0
- package/src/discovery/cursor.ts +266 -0
- package/src/discovery/gemini.ts +368 -0
- package/src/discovery/github.ts +120 -0
- package/src/discovery/helpers.test.ts +127 -0
- package/src/discovery/helpers.ts +249 -0
- package/src/discovery/index.ts +84 -0
- package/src/discovery/mcp-json.ts +127 -0
- package/src/discovery/vscode.ts +99 -0
- package/src/discovery/windsurf.ts +219 -0
- package/src/index.ts +192 -0
- package/src/main.ts +507 -0
- package/src/migrations.ts +156 -0
- package/src/modes/cleanup.ts +23 -0
- package/src/modes/index.ts +48 -0
- package/src/modes/interactive/components/armin.ts +382 -0
- package/src/modes/interactive/components/assistant-message.ts +86 -0
- package/src/modes/interactive/components/bash-execution.ts +199 -0
- package/src/modes/interactive/components/bordered-loader.ts +41 -0
- package/src/modes/interactive/components/branch-summary-message.ts +42 -0
- package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
- package/src/modes/interactive/components/custom-editor.ts +122 -0
- package/src/modes/interactive/components/diff.ts +147 -0
- package/src/modes/interactive/components/dynamic-border.ts +25 -0
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +296 -0
- package/src/modes/interactive/components/extensions/extension-list.ts +479 -0
- package/src/modes/interactive/components/extensions/index.ts +9 -0
- package/src/modes/interactive/components/extensions/inspector-panel.ts +313 -0
- package/src/modes/interactive/components/extensions/state-manager.ts +558 -0
- package/src/modes/interactive/components/extensions/types.ts +191 -0
- package/src/modes/interactive/components/hook-editor.ts +117 -0
- package/src/modes/interactive/components/hook-input.ts +64 -0
- package/src/modes/interactive/components/hook-message.ts +96 -0
- package/src/modes/interactive/components/hook-selector.ts +91 -0
- package/src/modes/interactive/components/model-selector.ts +560 -0
- package/src/modes/interactive/components/oauth-selector.ts +136 -0
- package/src/modes/interactive/components/plugin-settings.ts +481 -0
- package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
- package/src/modes/interactive/components/session-selector.ts +220 -0
- package/src/modes/interactive/components/settings-defs.ts +597 -0
- package/src/modes/interactive/components/settings-selector.ts +545 -0
- package/src/modes/interactive/components/show-images-selector.ts +45 -0
- package/src/modes/interactive/components/status-line/index.ts +4 -0
- package/src/modes/interactive/components/status-line/presets.ts +94 -0
- package/src/modes/interactive/components/status-line/segments.ts +350 -0
- package/src/modes/interactive/components/status-line/separators.ts +55 -0
- package/src/modes/interactive/components/status-line/types.ts +81 -0
- package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
- package/src/modes/interactive/components/status-line.ts +384 -0
- package/src/modes/interactive/components/theme-selector.ts +62 -0
- package/src/modes/interactive/components/thinking-selector.ts +64 -0
- package/src/modes/interactive/components/tool-execution.ts +946 -0
- package/src/modes/interactive/components/tree-selector.ts +877 -0
- package/src/modes/interactive/components/ttsr-notification.ts +82 -0
- package/src/modes/interactive/components/user-message-selector.ts +159 -0
- package/src/modes/interactive/components/user-message.ts +18 -0
- package/src/modes/interactive/components/visual-truncate.ts +50 -0
- package/src/modes/interactive/components/welcome.ts +228 -0
- package/src/modes/interactive/interactive-mode.ts +2669 -0
- package/src/modes/interactive/theme/dark.json +102 -0
- package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
- package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
- package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
- package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
- package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
- package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
- package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
- package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
- package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
- package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
- package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
- package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
- package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
- package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
- package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
- package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
- package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
- package/src/modes/interactive/theme/defaults/index.ts +67 -0
- package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
- package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
- package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
- package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
- package/src/modes/interactive/theme/defaults/light-github.json +114 -0
- package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
- package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
- package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
- package/src/modes/interactive/theme/defaults/light-one.json +105 -0
- package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
- package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
- package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
- package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
- package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
- package/src/modes/interactive/theme/light.json +99 -0
- package/src/modes/interactive/theme/theme-schema.json +424 -0
- package/src/modes/interactive/theme/theme.ts +2211 -0
- package/src/modes/print-mode.ts +163 -0
- package/src/modes/rpc/rpc-client.ts +527 -0
- package/src/modes/rpc/rpc-mode.ts +494 -0
- package/src/modes/rpc/rpc-types.ts +203 -0
- package/src/prompts/architect-plan.md +10 -0
- package/src/prompts/branch-summary-preamble.md +3 -0
- package/src/prompts/branch-summary.md +28 -0
- package/src/prompts/browser.md +71 -0
- package/src/prompts/compaction-summary.md +34 -0
- package/src/prompts/compaction-turn-prefix.md +16 -0
- package/src/prompts/compaction-update-summary.md +41 -0
- package/src/prompts/explore.md +82 -0
- package/src/prompts/implement-with-critic.md +11 -0
- package/src/prompts/implement.md +11 -0
- package/src/prompts/init.md +30 -0
- package/src/prompts/plan.md +54 -0
- package/src/prompts/reviewer.md +81 -0
- package/src/prompts/summarization-system.md +3 -0
- package/src/prompts/system-prompt.md +27 -0
- package/src/prompts/task.md +56 -0
- package/src/prompts/title-system.md +8 -0
- package/src/prompts/tools/ask.md +24 -0
- package/src/prompts/tools/bash.md +23 -0
- package/src/prompts/tools/edit.md +9 -0
- package/src/prompts/tools/find.md +6 -0
- package/src/prompts/tools/grep.md +12 -0
- package/src/prompts/tools/lsp.md +14 -0
- package/src/prompts/tools/output.md +23 -0
- package/src/prompts/tools/read.md +25 -0
- package/src/prompts/tools/web-fetch.md +8 -0
- package/src/prompts/tools/web-search.md +10 -0
- package/src/prompts/tools/write.md +10 -0
- package/src/utils/changelog.ts +99 -0
- package/src/utils/clipboard.ts +265 -0
- package/src/utils/fuzzy.ts +108 -0
- package/src/utils/mime.ts +30 -0
- package/src/utils/shell-snapshot.ts +218 -0
- package/src/utils/shell.ts +364 -0
- package/src/utils/tools-manager.ts +265 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared diff computation utilities for the edit tool.
|
|
3
|
+
* Used by both edit.ts (for execution) and tool-execution.ts (for preview rendering).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { constants } from "node:fs";
|
|
7
|
+
import { access, readFile } from "node:fs/promises";
|
|
8
|
+
import * as Diff from "diff";
|
|
9
|
+
import { resolveToCwd } from "./path-utils";
|
|
10
|
+
|
|
11
|
+
export function detectLineEnding(content: string): "\r\n" | "\n" {
|
|
12
|
+
const crlfIdx = content.indexOf("\r\n");
|
|
13
|
+
const lfIdx = content.indexOf("\n");
|
|
14
|
+
if (lfIdx === -1) return "\n";
|
|
15
|
+
if (crlfIdx === -1) return "\n";
|
|
16
|
+
return crlfIdx < lfIdx ? "\r\n" : "\n";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function normalizeToLF(text: string): string {
|
|
20
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function restoreLineEndings(text: string, ending: "\r\n" | "\n"): string {
|
|
24
|
+
return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Strip UTF-8 BOM if present, return both the BOM (if any) and the text without it */
|
|
28
|
+
export function stripBom(content: string): { bom: string; text: string } {
|
|
29
|
+
return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const DEFAULT_FUZZY_THRESHOLD = 0.95;
|
|
33
|
+
|
|
34
|
+
export interface EditMatch {
|
|
35
|
+
actualText: string;
|
|
36
|
+
startIndex: number;
|
|
37
|
+
startLine: number;
|
|
38
|
+
confidence: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface EditMatchOutcome {
|
|
42
|
+
match?: EditMatch;
|
|
43
|
+
closest?: EditMatch;
|
|
44
|
+
occurrences?: number;
|
|
45
|
+
fuzzyMatches?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function countLeadingWhitespace(line: string): number {
|
|
49
|
+
let count = 0;
|
|
50
|
+
for (let i = 0; i < line.length; i++) {
|
|
51
|
+
const char = line[i];
|
|
52
|
+
if (char === " " || char === "\t") {
|
|
53
|
+
count++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
return count;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function computeRelativeIndentDepths(lines: string[]): number[] {
|
|
62
|
+
const indents = lines.map(countLeadingWhitespace);
|
|
63
|
+
const nonEmptyIndents: number[] = [];
|
|
64
|
+
for (let i = 0; i < lines.length; i++) {
|
|
65
|
+
if (lines[i].trim().length > 0) {
|
|
66
|
+
nonEmptyIndents.push(indents[i]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const minIndent = nonEmptyIndents.length > 0 ? Math.min(...nonEmptyIndents) : 0;
|
|
70
|
+
const indentSteps = nonEmptyIndents.map((indent) => indent - minIndent).filter((step) => step > 0);
|
|
71
|
+
const indentUnit = indentSteps.length > 0 ? Math.min(...indentSteps) : 1;
|
|
72
|
+
|
|
73
|
+
return lines.map((line, index) => {
|
|
74
|
+
if (line.trim().length === 0) {
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
if (indentUnit <= 0) {
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
const relativeIndent = indents[index] - minIndent;
|
|
81
|
+
return Math.round(relativeIndent / indentUnit);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizeLinesForMatch(lines: string[]): string[] {
|
|
86
|
+
const indentDepths = computeRelativeIndentDepths(lines);
|
|
87
|
+
return lines.map((line, index) => {
|
|
88
|
+
const trimmed = line.trim();
|
|
89
|
+
if (trimmed.length === 0) {
|
|
90
|
+
return `${indentDepths[index]}|`;
|
|
91
|
+
}
|
|
92
|
+
const collapsed = trimmed.replace(/[ \t]+/g, " ");
|
|
93
|
+
return `${indentDepths[index]}|${collapsed}`;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function levenshteinDistance(a: string, b: string): number {
|
|
98
|
+
if (a === b) return 0;
|
|
99
|
+
const aLen = a.length;
|
|
100
|
+
const bLen = b.length;
|
|
101
|
+
if (aLen === 0) return bLen;
|
|
102
|
+
if (bLen === 0) return aLen;
|
|
103
|
+
|
|
104
|
+
let prev = new Array<number>(bLen + 1);
|
|
105
|
+
let curr = new Array<number>(bLen + 1);
|
|
106
|
+
for (let j = 0; j <= bLen; j++) {
|
|
107
|
+
prev[j] = j;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (let i = 1; i <= aLen; i++) {
|
|
111
|
+
curr[0] = i;
|
|
112
|
+
const aCode = a.charCodeAt(i - 1);
|
|
113
|
+
for (let j = 1; j <= bLen; j++) {
|
|
114
|
+
const cost = aCode === b.charCodeAt(j - 1) ? 0 : 1;
|
|
115
|
+
const deletion = prev[j] + 1;
|
|
116
|
+
const insertion = curr[j - 1] + 1;
|
|
117
|
+
const substitution = prev[j - 1] + cost;
|
|
118
|
+
curr[j] = Math.min(deletion, insertion, substitution);
|
|
119
|
+
}
|
|
120
|
+
const tmp = prev;
|
|
121
|
+
prev = curr;
|
|
122
|
+
curr = tmp;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return prev[bLen];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function similarityScore(a: string, b: string): number {
|
|
129
|
+
if (a.length === 0 && b.length === 0) {
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
const maxLen = Math.max(a.length, b.length);
|
|
133
|
+
if (maxLen === 0) {
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
136
|
+
const distance = levenshteinDistance(a, b);
|
|
137
|
+
return 1 - distance / maxLen;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function computeLineOffsets(lines: string[]): number[] {
|
|
141
|
+
const offsets: number[] = [];
|
|
142
|
+
let offset = 0;
|
|
143
|
+
for (let i = 0; i < lines.length; i++) {
|
|
144
|
+
offsets.push(offset);
|
|
145
|
+
offset += lines[i].length;
|
|
146
|
+
if (i < lines.length - 1) {
|
|
147
|
+
offset += 1;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return offsets;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function findBestFuzzyMatch(
|
|
154
|
+
content: string,
|
|
155
|
+
target: string,
|
|
156
|
+
threshold: number,
|
|
157
|
+
): { best?: EditMatch; aboveThresholdCount: number } {
|
|
158
|
+
const contentLines = content.split("\n");
|
|
159
|
+
const targetLines = target.split("\n");
|
|
160
|
+
if (targetLines.length === 0 || target.length === 0) {
|
|
161
|
+
return { aboveThresholdCount: 0 };
|
|
162
|
+
}
|
|
163
|
+
if (targetLines.length > contentLines.length) {
|
|
164
|
+
return { aboveThresholdCount: 0 };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const targetNormalized = normalizeLinesForMatch(targetLines);
|
|
168
|
+
const offsets = computeLineOffsets(contentLines);
|
|
169
|
+
|
|
170
|
+
let best: EditMatch | undefined;
|
|
171
|
+
let bestScore = -1;
|
|
172
|
+
let aboveThresholdCount = 0;
|
|
173
|
+
|
|
174
|
+
for (let start = 0; start <= contentLines.length - targetLines.length; start++) {
|
|
175
|
+
const windowLines = contentLines.slice(start, start + targetLines.length);
|
|
176
|
+
const windowNormalized = normalizeLinesForMatch(windowLines);
|
|
177
|
+
let score = 0;
|
|
178
|
+
for (let i = 0; i < targetLines.length; i++) {
|
|
179
|
+
score += similarityScore(targetNormalized[i], windowNormalized[i]);
|
|
180
|
+
}
|
|
181
|
+
score = score / targetLines.length;
|
|
182
|
+
|
|
183
|
+
if (score >= threshold) {
|
|
184
|
+
aboveThresholdCount++;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (score > bestScore) {
|
|
188
|
+
bestScore = score;
|
|
189
|
+
best = {
|
|
190
|
+
actualText: windowLines.join("\n"),
|
|
191
|
+
startIndex: offsets[start],
|
|
192
|
+
startLine: start + 1,
|
|
193
|
+
confidence: score,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { best, aboveThresholdCount };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function findEditMatch(
|
|
202
|
+
content: string,
|
|
203
|
+
target: string,
|
|
204
|
+
options: { allowFuzzy: boolean; similarityThreshold?: number },
|
|
205
|
+
): EditMatchOutcome {
|
|
206
|
+
if (target.length === 0) {
|
|
207
|
+
return {};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const exactIndex = content.indexOf(target);
|
|
211
|
+
if (exactIndex !== -1) {
|
|
212
|
+
const occurrences = content.split(target).length - 1;
|
|
213
|
+
if (occurrences > 1) {
|
|
214
|
+
return { occurrences };
|
|
215
|
+
}
|
|
216
|
+
const startLine = content.slice(0, exactIndex).split("\n").length;
|
|
217
|
+
return {
|
|
218
|
+
match: {
|
|
219
|
+
actualText: target,
|
|
220
|
+
startIndex: exactIndex,
|
|
221
|
+
startLine,
|
|
222
|
+
confidence: 1,
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const threshold = options.similarityThreshold ?? DEFAULT_FUZZY_THRESHOLD;
|
|
228
|
+
const { best, aboveThresholdCount } = findBestFuzzyMatch(content, target, threshold);
|
|
229
|
+
if (!best) {
|
|
230
|
+
return {};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (options.allowFuzzy && best.confidence >= threshold && aboveThresholdCount === 1) {
|
|
234
|
+
return { match: best, closest: best };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return { closest: best, fuzzyMatches: aboveThresholdCount };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function findFirstDifferentLine(oldLines: string[], newLines: string[]): { oldLine: string; newLine: string } {
|
|
241
|
+
const max = Math.max(oldLines.length, newLines.length);
|
|
242
|
+
for (let i = 0; i < max; i++) {
|
|
243
|
+
const oldLine = oldLines[i] ?? "";
|
|
244
|
+
const newLine = newLines[i] ?? "";
|
|
245
|
+
if (oldLine !== newLine) {
|
|
246
|
+
return { oldLine, newLine };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return { oldLine: oldLines[0] ?? "", newLine: newLines[0] ?? "" };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export class EditMatchError extends Error {
|
|
253
|
+
constructor(
|
|
254
|
+
public readonly path: string,
|
|
255
|
+
public readonly normalizedOldText: string,
|
|
256
|
+
public readonly closest: EditMatch | undefined,
|
|
257
|
+
public readonly options: { allowFuzzy: boolean; similarityThreshold: number; fuzzyMatches?: number },
|
|
258
|
+
) {
|
|
259
|
+
super(EditMatchError.formatMessage(path, normalizedOldText, closest, options));
|
|
260
|
+
this.name = "EditMatchError";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
static formatMessage(
|
|
264
|
+
path: string,
|
|
265
|
+
normalizedOldText: string,
|
|
266
|
+
closest: EditMatch | undefined,
|
|
267
|
+
options: { allowFuzzy: boolean; similarityThreshold: number; fuzzyMatches?: number },
|
|
268
|
+
): string {
|
|
269
|
+
if (!closest) {
|
|
270
|
+
return options.allowFuzzy
|
|
271
|
+
? `Could not find a close enough match in ${path}.`
|
|
272
|
+
: `Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const similarity = Math.round(closest.confidence * 100);
|
|
276
|
+
const oldLines = normalizedOldText.split("\n");
|
|
277
|
+
const actualLines = closest.actualText.split("\n");
|
|
278
|
+
const { oldLine, newLine } = findFirstDifferentLine(oldLines, actualLines);
|
|
279
|
+
const thresholdPercent = Math.round(options.similarityThreshold * 100);
|
|
280
|
+
|
|
281
|
+
const hint = options.allowFuzzy
|
|
282
|
+
? options.fuzzyMatches && options.fuzzyMatches > 1
|
|
283
|
+
? `Found ${options.fuzzyMatches} high-confidence matches. Provide more context to make it unique.`
|
|
284
|
+
: `Closest match was below the ${thresholdPercent}% similarity threshold.`
|
|
285
|
+
: "Fuzzy matching is disabled. Enable 'Edit fuzzy match' in settings to accept high-confidence matches.";
|
|
286
|
+
|
|
287
|
+
return [
|
|
288
|
+
options.allowFuzzy
|
|
289
|
+
? `Could not find a close enough match in ${path}.`
|
|
290
|
+
: `Could not find the exact text in ${path}.`,
|
|
291
|
+
``,
|
|
292
|
+
`Closest match (${similarity}% similar) at line ${closest.startLine}:`,
|
|
293
|
+
` - ${oldLine}`,
|
|
294
|
+
` + ${newLine}`,
|
|
295
|
+
hint,
|
|
296
|
+
].join("\n");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Generate a unified diff string with line numbers and context.
|
|
302
|
+
* Returns both the diff string and the first changed line number (in the new file).
|
|
303
|
+
*/
|
|
304
|
+
export function generateDiffString(
|
|
305
|
+
oldContent: string,
|
|
306
|
+
newContent: string,
|
|
307
|
+
contextLines = 4,
|
|
308
|
+
): { diff: string; firstChangedLine: number | undefined } {
|
|
309
|
+
const parts = Diff.diffLines(oldContent, newContent);
|
|
310
|
+
const output: string[] = [];
|
|
311
|
+
|
|
312
|
+
const oldLines = oldContent.split("\n");
|
|
313
|
+
const newLines = newContent.split("\n");
|
|
314
|
+
const maxLineNum = Math.max(oldLines.length, newLines.length);
|
|
315
|
+
const lineNumWidth = String(maxLineNum).length;
|
|
316
|
+
|
|
317
|
+
let oldLineNum = 1;
|
|
318
|
+
let newLineNum = 1;
|
|
319
|
+
let lastWasChange = false;
|
|
320
|
+
let firstChangedLine: number | undefined;
|
|
321
|
+
|
|
322
|
+
for (let i = 0; i < parts.length; i++) {
|
|
323
|
+
const part = parts[i];
|
|
324
|
+
const raw = part.value.split("\n");
|
|
325
|
+
if (raw[raw.length - 1] === "") {
|
|
326
|
+
raw.pop();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (part.added || part.removed) {
|
|
330
|
+
// Capture the first changed line (in the new file)
|
|
331
|
+
if (firstChangedLine === undefined) {
|
|
332
|
+
firstChangedLine = newLineNum;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Show the change
|
|
336
|
+
for (const line of raw) {
|
|
337
|
+
if (part.added) {
|
|
338
|
+
const lineNum = String(newLineNum).padStart(lineNumWidth, " ");
|
|
339
|
+
output.push(`+${lineNum} ${line}`);
|
|
340
|
+
newLineNum++;
|
|
341
|
+
} else {
|
|
342
|
+
// removed
|
|
343
|
+
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
|
344
|
+
output.push(`-${lineNum} ${line}`);
|
|
345
|
+
oldLineNum++;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
lastWasChange = true;
|
|
349
|
+
} else {
|
|
350
|
+
// Context lines - only show a few before/after changes
|
|
351
|
+
const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);
|
|
352
|
+
|
|
353
|
+
if (lastWasChange || nextPartIsChange) {
|
|
354
|
+
// Show context
|
|
355
|
+
let linesToShow = raw;
|
|
356
|
+
let skipStart = 0;
|
|
357
|
+
let skipEnd = 0;
|
|
358
|
+
|
|
359
|
+
if (!lastWasChange) {
|
|
360
|
+
// Show only last N lines as leading context
|
|
361
|
+
skipStart = Math.max(0, raw.length - contextLines);
|
|
362
|
+
linesToShow = raw.slice(skipStart);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (!nextPartIsChange && linesToShow.length > contextLines) {
|
|
366
|
+
// Show only first N lines as trailing context
|
|
367
|
+
skipEnd = linesToShow.length - contextLines;
|
|
368
|
+
linesToShow = linesToShow.slice(0, contextLines);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Add ellipsis if we skipped lines at start
|
|
372
|
+
if (skipStart > 0) {
|
|
373
|
+
output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
|
|
374
|
+
// Update line numbers for the skipped leading context
|
|
375
|
+
oldLineNum += skipStart;
|
|
376
|
+
newLineNum += skipStart;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
for (const line of linesToShow) {
|
|
380
|
+
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
|
381
|
+
output.push(` ${lineNum} ${line}`);
|
|
382
|
+
oldLineNum++;
|
|
383
|
+
newLineNum++;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Add ellipsis if we skipped lines at end
|
|
387
|
+
if (skipEnd > 0) {
|
|
388
|
+
output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
|
|
389
|
+
// Update line numbers for the skipped trailing context
|
|
390
|
+
oldLineNum += skipEnd;
|
|
391
|
+
newLineNum += skipEnd;
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
// Skip these context lines entirely
|
|
395
|
+
oldLineNum += raw.length;
|
|
396
|
+
newLineNum += raw.length;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
lastWasChange = false;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return { diff: output.join("\n"), firstChangedLine };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export interface EditDiffResult {
|
|
407
|
+
diff: string;
|
|
408
|
+
firstChangedLine: number | undefined;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export interface EditDiffError {
|
|
412
|
+
error: string;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Compute the diff for an edit operation without applying it.
|
|
417
|
+
* Used for preview rendering in the TUI before the tool executes.
|
|
418
|
+
*/
|
|
419
|
+
export async function computeEditDiff(
|
|
420
|
+
path: string,
|
|
421
|
+
oldText: string,
|
|
422
|
+
newText: string,
|
|
423
|
+
cwd: string,
|
|
424
|
+
fuzzy = true,
|
|
425
|
+
): Promise<EditDiffResult | EditDiffError> {
|
|
426
|
+
const absolutePath = resolveToCwd(path, cwd);
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
// Check if file exists and is readable
|
|
430
|
+
try {
|
|
431
|
+
await access(absolutePath, constants.R_OK);
|
|
432
|
+
} catch {
|
|
433
|
+
return { error: `File not found: ${path}` };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Read the file
|
|
437
|
+
const rawContent = await readFile(absolutePath, "utf-8");
|
|
438
|
+
|
|
439
|
+
// Strip BOM before matching (LLM won't include invisible BOM in oldText)
|
|
440
|
+
const { text: content } = stripBom(rawContent);
|
|
441
|
+
|
|
442
|
+
const normalizedContent = normalizeToLF(content);
|
|
443
|
+
const normalizedOldText = normalizeToLF(oldText);
|
|
444
|
+
const normalizedNewText = normalizeToLF(newText);
|
|
445
|
+
|
|
446
|
+
const matchOutcome = findEditMatch(normalizedContent, normalizedOldText, {
|
|
447
|
+
allowFuzzy: fuzzy,
|
|
448
|
+
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
if (matchOutcome.occurrences && matchOutcome.occurrences > 1) {
|
|
452
|
+
return {
|
|
453
|
+
error: `Found ${matchOutcome.occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (!matchOutcome.match) {
|
|
458
|
+
return {
|
|
459
|
+
error: EditMatchError.formatMessage(path, normalizedOldText, matchOutcome.closest, {
|
|
460
|
+
allowFuzzy: fuzzy,
|
|
461
|
+
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
462
|
+
fuzzyMatches: matchOutcome.fuzzyMatches,
|
|
463
|
+
}),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const match = matchOutcome.match;
|
|
468
|
+
|
|
469
|
+
// Compute the new content
|
|
470
|
+
const normalizedNewContent =
|
|
471
|
+
normalizedContent.substring(0, match.startIndex) +
|
|
472
|
+
normalizedNewText +
|
|
473
|
+
normalizedContent.substring(match.startIndex + match.actualText.length);
|
|
474
|
+
|
|
475
|
+
// Check if it would actually change anything
|
|
476
|
+
if (normalizedContent === normalizedNewContent) {
|
|
477
|
+
return {
|
|
478
|
+
error: `No changes would be made to ${path}. The replacement produces identical content.`,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Generate the diff
|
|
483
|
+
return generateDiffString(normalizedContent, normalizedNewContent);
|
|
484
|
+
} catch (err) {
|
|
485
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
486
|
+
}
|
|
487
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import editDescription from "../../prompts/tools/edit.md" with { type: "text" };
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_FUZZY_THRESHOLD,
|
|
6
|
+
detectLineEnding,
|
|
7
|
+
EditMatchError,
|
|
8
|
+
findEditMatch,
|
|
9
|
+
generateDiffString,
|
|
10
|
+
normalizeToLF,
|
|
11
|
+
restoreLineEndings,
|
|
12
|
+
stripBom,
|
|
13
|
+
} from "./edit-diff";
|
|
14
|
+
import { type FileDiagnosticsResult, type WritethroughCallback, writethroughNoop } from "./lsp/index";
|
|
15
|
+
import { resolveToCwd } from "./path-utils";
|
|
16
|
+
|
|
17
|
+
const editSchema = Type.Object({
|
|
18
|
+
path: Type.String({ description: "Path to the file to edit (relative or absolute)" }),
|
|
19
|
+
oldText: Type.String({
|
|
20
|
+
description: "Text to find and replace (high-confidence fuzzy matching for whitespace/indentation is always on)",
|
|
21
|
+
}),
|
|
22
|
+
newText: Type.String({ description: "New text to replace the old text with" }),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export interface EditToolDetails {
|
|
26
|
+
/** Unified diff of the changes made */
|
|
27
|
+
diff: string;
|
|
28
|
+
/** Line number of the first change in the new file (for editor navigation) */
|
|
29
|
+
firstChangedLine?: number;
|
|
30
|
+
/** Diagnostic result (if available) */
|
|
31
|
+
diagnostics?: FileDiagnosticsResult;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface EditToolOptions {
|
|
35
|
+
/** Whether to accept high-confidence fuzzy matches for whitespace/indentation (default: true) */
|
|
36
|
+
fuzzyMatch?: boolean;
|
|
37
|
+
/** Writethrough callback to get LSP diagnostics after editing a file */
|
|
38
|
+
writethrough?: WritethroughCallback;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createEditTool(cwd: string, options: EditToolOptions = {}): AgentTool<typeof editSchema> {
|
|
42
|
+
const allowFuzzy = options.fuzzyMatch ?? true;
|
|
43
|
+
const writethrough = options.writethrough ?? writethroughNoop;
|
|
44
|
+
return {
|
|
45
|
+
name: "edit",
|
|
46
|
+
label: "Edit",
|
|
47
|
+
description: editDescription,
|
|
48
|
+
parameters: editSchema,
|
|
49
|
+
execute: async (
|
|
50
|
+
_toolCallId: string,
|
|
51
|
+
{ path, oldText, newText }: { path: string; oldText: string; newText: string },
|
|
52
|
+
signal?: AbortSignal,
|
|
53
|
+
) => {
|
|
54
|
+
// Reject .ipynb files - use NotebookEdit tool instead
|
|
55
|
+
if (path.endsWith(".ipynb")) {
|
|
56
|
+
throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const absolutePath = resolveToCwd(path, cwd);
|
|
60
|
+
|
|
61
|
+
const file = Bun.file(absolutePath);
|
|
62
|
+
if (!(await file.exists())) {
|
|
63
|
+
throw new Error(`File not found: ${path}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const rawContent = await file.text();
|
|
67
|
+
|
|
68
|
+
// Strip BOM before matching (LLM won't include invisible BOM in oldText)
|
|
69
|
+
const { bom, text: content } = stripBom(rawContent);
|
|
70
|
+
|
|
71
|
+
const originalEnding = detectLineEnding(content);
|
|
72
|
+
const normalizedContent = normalizeToLF(content);
|
|
73
|
+
const normalizedOldText = normalizeToLF(oldText);
|
|
74
|
+
const normalizedNewText = normalizeToLF(newText);
|
|
75
|
+
|
|
76
|
+
const matchOutcome = findEditMatch(normalizedContent, normalizedOldText, {
|
|
77
|
+
allowFuzzy,
|
|
78
|
+
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (matchOutcome.occurrences && matchOutcome.occurrences > 1) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Found ${matchOutcome.occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!matchOutcome.match) {
|
|
88
|
+
throw new EditMatchError(path, normalizedOldText, matchOutcome.closest, {
|
|
89
|
+
allowFuzzy,
|
|
90
|
+
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
91
|
+
fuzzyMatches: matchOutcome.fuzzyMatches,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const match = matchOutcome.match;
|
|
96
|
+
const normalizedNewContent =
|
|
97
|
+
normalizedContent.substring(0, match.startIndex) +
|
|
98
|
+
normalizedNewText +
|
|
99
|
+
normalizedContent.substring(match.startIndex + match.actualText.length);
|
|
100
|
+
|
|
101
|
+
// Verify the replacement actually changed something
|
|
102
|
+
if (normalizedContent === normalizedNewContent) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`No changes made to ${path}. The replacement produced identical content. This might indicate an issue with special characters or the text not existing as expected.`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const finalContent = bom + restoreLineEndings(normalizedNewContent, originalEnding);
|
|
109
|
+
const diagnostics = await writethrough(absolutePath, finalContent, signal, file);
|
|
110
|
+
|
|
111
|
+
const diffResult = generateDiffString(normalizedContent, normalizedNewContent);
|
|
112
|
+
|
|
113
|
+
// Build result text
|
|
114
|
+
let resultText = `Successfully replaced text in ${path}.`;
|
|
115
|
+
|
|
116
|
+
const messages = diagnostics?.messages;
|
|
117
|
+
if (messages && messages.length > 0) {
|
|
118
|
+
resultText += `\n\nLSP Diagnostics (${diagnostics.summary}):\n`;
|
|
119
|
+
resultText += messages.map((d) => ` ${d}`).join("\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
content: [
|
|
124
|
+
{
|
|
125
|
+
type: "text",
|
|
126
|
+
text: resultText,
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
details: {
|
|
130
|
+
diff: diffResult.diff,
|
|
131
|
+
firstChangedLine: diffResult.firstChangedLine,
|
|
132
|
+
diagnostics: diagnostics,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Default edit tool using process.cwd() - for backwards compatibility */
|
|
140
|
+
export const editTool = createEditTool(process.cwd());
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exa Company Tool
|
|
3
|
+
*
|
|
4
|
+
* Research companies using Exa's comprehensive data sources.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Type } from "@sinclair/typebox";
|
|
8
|
+
import type { CustomTool } from "../../custom-tools/types";
|
|
9
|
+
import type { ExaRenderDetails } from "./types";
|
|
10
|
+
|
|
11
|
+
/** exa_company - Company research */
|
|
12
|
+
export const companyTool: CustomTool<any, ExaRenderDetails> = {
|
|
13
|
+
name: "exa_company",
|
|
14
|
+
label: "Exa Company",
|
|
15
|
+
description: `Research companies using Exa's comprehensive data sources.
|
|
16
|
+
|
|
17
|
+
Returns detailed company information including overview, news, financials, and key people.
|
|
18
|
+
|
|
19
|
+
Parameters:
|
|
20
|
+
- company_name: Name of the company to research (e.g., "OpenAI", "Google", "Y Combinator")`,
|
|
21
|
+
|
|
22
|
+
parameters: Type.Object({
|
|
23
|
+
company_name: Type.String({ description: "Name of the company to research" }),
|
|
24
|
+
}),
|
|
25
|
+
|
|
26
|
+
async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
|
|
27
|
+
try {
|
|
28
|
+
const { findApiKey, callExaTool, formatSearchResults, isSearchResponse } = await import("./mcp-client.js");
|
|
29
|
+
|
|
30
|
+
const apiKey = await findApiKey();
|
|
31
|
+
if (!apiKey) {
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
|
|
34
|
+
details: { error: "EXA_API_KEY not found", toolName: "exa_company" },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const response = await callExaTool("company_research", params, apiKey);
|
|
38
|
+
|
|
39
|
+
if (isSearchResponse(response)) {
|
|
40
|
+
const formatted = formatSearchResults(response);
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text" as const, text: formatted }],
|
|
43
|
+
details: { response, toolName: "exa_company" },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
content: [{ type: "text" as const, text: JSON.stringify(response, null, 2) }],
|
|
49
|
+
details: { raw: response, toolName: "exa_company" },
|
|
50
|
+
};
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
+
return {
|
|
54
|
+
content: [{ type: "text" as const, text: `Error: ${message}` }],
|
|
55
|
+
details: { error: message, toolName: "exa_company" },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|