@aexol/spectral 0.7.1 → 0.7.5
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 +5 -0
- package/dist/agent/agents.js +1 -1
- package/dist/agent/index.js +199 -184
- package/dist/commands/serve.js +0 -3
- package/dist/designer/data/systems/renault/DESIGN.md +1 -1
- package/dist/designer/philosophies.js +668 -0
- package/dist/mcp/sampling-handler.js +1 -1
- package/dist/memory/commands/status.js +1 -1
- package/dist/memory/compaction.js +2 -2
- package/dist/memory/config.js +1 -1
- package/dist/memory/debug-log.js +1 -1
- package/dist/memory/hooks/compaction-hook.js +29 -0
- package/dist/memory/index.js +2 -0
- package/dist/memory/observer.js +2 -2
- package/dist/memory/project-observations-store.js +14 -0
- package/dist/memory/tokens.js +1 -1
- package/dist/memory/tools/read-project-observations.js +82 -0
- package/dist/memory/tools/recall-observation.js +2 -2
- package/dist/pi/agent-core/agent-loop.js +501 -0
- package/dist/pi/agent-core/agent.js +401 -0
- package/dist/pi/agent-core/harness/agent-harness.js +899 -0
- package/dist/pi/agent-core/harness/compaction/branch-summarization.js +173 -0
- package/dist/pi/agent-core/harness/compaction/compaction.js +532 -0
- package/dist/pi/agent-core/harness/compaction/utils.js +130 -0
- package/dist/pi/agent-core/harness/env/nodejs.js +485 -0
- package/dist/pi/agent-core/harness/messages.js +101 -0
- package/dist/pi/agent-core/harness/prompt-templates.js +229 -0
- package/dist/pi/agent-core/harness/session/jsonl-repo.js +100 -0
- package/dist/pi/agent-core/harness/session/jsonl-storage.js +230 -0
- package/dist/pi/agent-core/harness/session/memory-repo.js +41 -0
- package/dist/pi/agent-core/harness/session/memory-storage.js +113 -0
- package/dist/pi/agent-core/harness/session/repo-utils.js +38 -0
- package/dist/pi/agent-core/harness/session/session.js +196 -0
- package/dist/pi/agent-core/harness/session/uuid.js +49 -0
- package/dist/pi/agent-core/harness/skills.js +310 -0
- package/dist/pi/agent-core/harness/system-prompt.js +29 -0
- package/dist/pi/agent-core/harness/types.js +93 -0
- package/dist/pi/agent-core/harness/utils/shell-output.js +125 -0
- package/dist/pi/agent-core/harness/utils/truncate.js +289 -0
- package/dist/pi/agent-core/index.js +24 -0
- package/dist/pi/agent-core/node.js +2 -0
- package/dist/pi/agent-core/proxy.js +277 -0
- package/dist/pi/agent-core/types.js +1 -0
- package/dist/pi/ai/api-registry.js +43 -0
- package/dist/pi/ai/cli.js +120 -0
- package/dist/pi/ai/env-api-keys.js +169 -0
- package/dist/pi/ai/image-models.generated.js +441 -0
- package/dist/pi/ai/image-models.js +22 -0
- package/dist/pi/ai/images-api-registry.js +21 -0
- package/dist/pi/ai/images.js +13 -0
- package/dist/pi/ai/index.js +18 -0
- package/dist/pi/ai/models.generated.js +16220 -0
- package/dist/pi/ai/models.js +70 -0
- package/dist/pi/ai/oauth.js +1 -0
- package/dist/pi/ai/providers/anthropic.js +945 -0
- package/dist/pi/ai/providers/faux.js +367 -0
- package/dist/pi/ai/providers/github-copilot-headers.js +28 -0
- package/dist/pi/ai/providers/openai-completions.js +945 -0
- package/dist/pi/ai/providers/openai-prompt-cache.js +9 -0
- package/dist/pi/ai/providers/register-builtins.js +97 -0
- package/dist/pi/ai/providers/simple-options.js +40 -0
- package/dist/pi/ai/providers/transform-messages.js +183 -0
- package/dist/pi/ai/session-resources.js +21 -0
- package/dist/pi/ai/stream.js +26 -0
- package/dist/pi/ai/types.js +1 -0
- package/dist/pi/ai/utils/diagnostics.js +24 -0
- package/dist/pi/ai/utils/event-stream.js +80 -0
- package/dist/pi/ai/utils/hash.js +13 -0
- package/dist/pi/ai/utils/headers.js +7 -0
- package/dist/pi/ai/utils/json-parse.js +112 -0
- package/dist/pi/ai/utils/node-http-proxy.js +96 -0
- package/dist/pi/ai/utils/oauth/anthropic.js +334 -0
- package/dist/pi/ai/utils/oauth/device-code.js +54 -0
- package/dist/pi/ai/utils/oauth/github-copilot.js +270 -0
- package/dist/pi/ai/utils/oauth/index.js +121 -0
- package/dist/pi/ai/utils/oauth/oauth-page.js +104 -0
- package/dist/pi/ai/utils/oauth/openai-codex.js +384 -0
- package/dist/pi/ai/utils/oauth/pkce.js +30 -0
- package/dist/pi/ai/utils/oauth/types.js +1 -0
- package/dist/pi/ai/utils/overflow.js +150 -0
- package/dist/pi/ai/utils/sanitize-unicode.js +25 -0
- package/dist/pi/ai/utils/typebox-helpers.js +20 -0
- package/dist/pi/ai/utils/validation.js +280 -0
- package/dist/pi/coding-agent/bun/cli.js +7 -0
- package/dist/pi/coding-agent/bun/restore-sandbox-env.js +31 -0
- package/dist/pi/coding-agent/cli/args.js +340 -0
- package/dist/pi/coding-agent/cli/file-processor.js +82 -0
- package/dist/pi/coding-agent/cli/initial-message.js +21 -0
- package/dist/pi/coding-agent/cli.js +17 -0
- package/dist/pi/coding-agent/config.js +414 -0
- package/dist/pi/coding-agent/core/agent-session-runtime.js +299 -0
- package/dist/pi/coding-agent/core/agent-session-services.js +117 -0
- package/dist/pi/coding-agent/core/agent-session.js +2498 -0
- package/dist/pi/coding-agent/core/auth-guidance.js +20 -0
- package/dist/pi/coding-agent/core/auth-storage.js +441 -0
- package/dist/pi/coding-agent/core/bash-executor.js +110 -0
- package/dist/pi/coding-agent/core/compaction/branch-summarization.js +242 -0
- package/dist/pi/coding-agent/core/compaction/compaction.js +624 -0
- package/dist/pi/coding-agent/core/compaction/index.js +6 -0
- package/dist/pi/coding-agent/core/compaction/utils.js +152 -0
- package/dist/pi/coding-agent/core/defaults.js +1 -0
- package/dist/pi/coding-agent/core/diagnostics.js +1 -0
- package/dist/pi/coding-agent/core/event-bus.js +24 -0
- package/dist/pi/coding-agent/core/exec.js +74 -0
- package/dist/pi/coding-agent/core/export-html/ansi-to-html.js +248 -0
- package/dist/pi/coding-agent/core/export-html/index.js +225 -0
- package/dist/pi/coding-agent/core/export-html/tool-renderer.js +107 -0
- package/dist/pi/coding-agent/core/extensions/index.js +8 -0
- package/dist/pi/coding-agent/core/extensions/loader.js +485 -0
- package/dist/pi/coding-agent/core/extensions/runner.js +824 -0
- package/dist/pi/coding-agent/core/extensions/types.js +44 -0
- package/dist/pi/coding-agent/core/extensions/wrapper.js +21 -0
- package/dist/pi/coding-agent/core/footer-data-provider.js +309 -0
- package/dist/pi/coding-agent/core/http-dispatcher.js +47 -0
- package/dist/pi/coding-agent/core/index.js +11 -0
- package/dist/pi/coding-agent/core/keybindings.js +294 -0
- package/dist/pi/coding-agent/core/messages.js +122 -0
- package/dist/pi/coding-agent/core/model-registry.js +728 -0
- package/dist/pi/coding-agent/core/model-resolver.js +494 -0
- package/dist/pi/coding-agent/core/output-guard.js +58 -0
- package/dist/pi/coding-agent/core/package-manager.js +2020 -0
- package/dist/pi/coding-agent/core/prompt-templates.js +237 -0
- package/dist/pi/coding-agent/core/provider-display-names.js +32 -0
- package/dist/pi/coding-agent/core/resolve-config-value.js +125 -0
- package/dist/pi/coding-agent/core/resource-loader.js +733 -0
- package/dist/pi/coding-agent/core/sdk.js +282 -0
- package/dist/pi/coding-agent/core/session-cwd.js +37 -0
- package/dist/pi/coding-agent/core/session-manager.js +1146 -0
- package/dist/pi/coding-agent/core/settings-manager.js +794 -0
- package/dist/pi/coding-agent/core/skills.js +386 -0
- package/dist/pi/coding-agent/core/slash-commands.js +24 -0
- package/dist/pi/coding-agent/core/source-info.js +18 -0
- package/dist/pi/coding-agent/core/system-prompt.js +122 -0
- package/dist/pi/coding-agent/core/telemetry.js +8 -0
- package/dist/pi/coding-agent/core/timings.js +30 -0
- package/dist/pi/coding-agent/core/tools/bash.js +341 -0
- package/dist/pi/coding-agent/core/tools/edit-diff.js +344 -0
- package/dist/pi/coding-agent/core/tools/edit.js +324 -0
- package/dist/pi/coding-agent/core/tools/file-mutation-queue.js +36 -0
- package/dist/pi/coding-agent/core/tools/find.js +297 -0
- package/dist/pi/coding-agent/core/tools/grep.js +303 -0
- package/dist/pi/coding-agent/core/tools/index.js +111 -0
- package/dist/pi/coding-agent/core/tools/ls.js +168 -0
- package/dist/pi/coding-agent/core/tools/output-accumulator.js +183 -0
- package/dist/pi/coding-agent/core/tools/path-utils.js +61 -0
- package/dist/pi/coding-agent/core/tools/read.js +288 -0
- package/dist/pi/coding-agent/core/tools/render-utils.js +48 -0
- package/dist/pi/coding-agent/core/tools/tool-definition-wrapper.js +33 -0
- package/dist/pi/coding-agent/core/tools/truncate.js +214 -0
- package/dist/pi/coding-agent/core/tools/write.js +212 -0
- package/dist/pi/coding-agent/index.js +41 -0
- package/dist/pi/coding-agent/main.js +5 -0
- package/dist/pi/coding-agent/migrations.js +280 -0
- package/dist/pi/coding-agent/modes/index.js +7 -0
- package/dist/pi/coding-agent/modes/interactive/components/diff.js +132 -0
- package/dist/pi/coding-agent/modes/interactive/components/keybinding-hints.js +35 -0
- package/dist/pi/coding-agent/modes/interactive/components/visual-truncate.js +32 -0
- package/dist/pi/coding-agent/modes/interactive/interactive-mode.js +3 -0
- package/dist/pi/coding-agent/modes/interactive/theme/theme.js +1023 -0
- package/dist/pi/coding-agent/modes/print-mode.js +130 -0
- package/dist/pi/coding-agent/modes/rpc/jsonl.js +48 -0
- package/dist/pi/coding-agent/modes/rpc/rpc-client.js +409 -0
- package/dist/pi/coding-agent/modes/rpc/rpc-mode.js +600 -0
- package/dist/pi/coding-agent/modes/rpc/rpc-types.js +7 -0
- package/dist/pi/coding-agent/utils/ansi.js +51 -0
- package/dist/pi/coding-agent/utils/changelog.js +86 -0
- package/dist/pi/coding-agent/utils/child-process.js +87 -0
- package/dist/pi/coding-agent/utils/clipboard-image.js +244 -0
- package/dist/pi/coding-agent/utils/clipboard-native.js +13 -0
- package/dist/pi/coding-agent/utils/clipboard.js +116 -0
- package/dist/pi/coding-agent/utils/exif-orientation.js +157 -0
- package/dist/pi/coding-agent/utils/frontmatter.js +25 -0
- package/dist/pi/coding-agent/utils/fs-watch.js +24 -0
- package/dist/pi/coding-agent/utils/git.js +162 -0
- package/dist/pi/coding-agent/utils/html.js +39 -0
- package/dist/pi/coding-agent/utils/image-convert.js +38 -0
- package/dist/pi/coding-agent/utils/image-resize.js +136 -0
- package/dist/pi/coding-agent/utils/mime.js +68 -0
- package/dist/pi/coding-agent/utils/paths.js +91 -0
- package/dist/pi/coding-agent/utils/photon.js +120 -0
- package/dist/pi/coding-agent/utils/pi-user-agent.js +4 -0
- package/dist/pi/coding-agent/utils/shell.js +194 -0
- package/dist/pi/coding-agent/utils/sleep.js +16 -0
- package/dist/pi/coding-agent/utils/syntax-highlight.js +117 -0
- package/dist/pi/coding-agent/utils/tools-manager.js +327 -0
- package/dist/pi/coding-agent/utils/version-check.js +81 -0
- package/dist/pi/coding-agent/utils/windows-self-update.js +76 -0
- package/dist/pi/tui/autocomplete.js +631 -0
- package/dist/pi/tui/components/box.js +103 -0
- package/dist/pi/tui/components/cancellable-loader.js +34 -0
- package/dist/pi/tui/components/editor.js +1915 -0
- package/dist/pi/tui/components/image.js +88 -0
- package/dist/pi/tui/components/input.js +425 -0
- package/dist/pi/tui/components/loader.js +68 -0
- package/dist/pi/tui/components/markdown.js +633 -0
- package/dist/pi/tui/components/select-list.js +158 -0
- package/dist/pi/tui/components/settings-list.js +184 -0
- package/dist/pi/tui/components/spacer.js +22 -0
- package/dist/pi/tui/components/text.js +88 -0
- package/dist/pi/tui/components/truncated-text.js +50 -0
- package/dist/pi/tui/editor-component.js +1 -0
- package/dist/pi/tui/fuzzy.js +109 -0
- package/dist/pi/tui/index.js +31 -0
- package/dist/pi/tui/keybindings.js +173 -0
- package/dist/pi/tui/keys.js +1172 -0
- package/dist/pi/tui/kill-ring.js +43 -0
- package/dist/pi/tui/stdin-buffer.js +360 -0
- package/dist/pi/tui/terminal-image.js +335 -0
- package/dist/pi/tui/terminal.js +324 -0
- package/dist/pi/tui/tui.js +1076 -0
- package/dist/pi/tui/undo-stack.js +24 -0
- package/dist/pi/tui/utils.js +1016 -0
- package/dist/relay/dispatcher.js +30 -0
- package/dist/server/handlers/queue.js +52 -0
- package/dist/server/pi-bridge.js +9 -1
- package/dist/server/session-stream.js +76 -111
- package/dist/server/storage.js +154 -2
- package/dist/server/title-generator.js +14 -153
- package/package.json +24 -6
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { Box, Container, Spacer, Text } from "../../../tui/index.js";
|
|
2
|
+
import { constants } from "fs";
|
|
3
|
+
import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises";
|
|
4
|
+
import { Type } from "typebox";
|
|
5
|
+
import { renderDiff } from "../../modes/interactive/components/diff.js";
|
|
6
|
+
import { applyEditsToNormalizedContent, computeEditsDiff, detectLineEnding, generateDiffString, generateUnifiedPatch, normalizeToLF, restoreLineEndings, stripBom, } from "./edit-diff.js";
|
|
7
|
+
import { withFileMutationQueue } from "./file-mutation-queue.js";
|
|
8
|
+
import { resolveToCwd } from "./path-utils.js";
|
|
9
|
+
import { invalidArgText, shortenPath, str } from "./render-utils.js";
|
|
10
|
+
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
|
11
|
+
const replaceEditSchema = Type.Object({
|
|
12
|
+
oldText: Type.String({
|
|
13
|
+
description: "Exact text for one targeted replacement. It must be unique in the original file and must not overlap with any other edits[].oldText in the same call.",
|
|
14
|
+
}),
|
|
15
|
+
newText: Type.String({ description: "Replacement text for this targeted edit." }),
|
|
16
|
+
}, { additionalProperties: false });
|
|
17
|
+
const editSchema = Type.Object({
|
|
18
|
+
path: Type.String({ description: "Path to the file to edit (relative or absolute)" }),
|
|
19
|
+
edits: Type.Array(replaceEditSchema, {
|
|
20
|
+
description: "One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into one edit instead.",
|
|
21
|
+
}),
|
|
22
|
+
}, { additionalProperties: false });
|
|
23
|
+
const defaultEditOperations = {
|
|
24
|
+
readFile: (path) => fsReadFile(path),
|
|
25
|
+
writeFile: (path, content) => fsWriteFile(path, content, "utf-8"),
|
|
26
|
+
access: (path) => fsAccess(path, constants.R_OK | constants.W_OK),
|
|
27
|
+
};
|
|
28
|
+
function prepareEditArguments(input) {
|
|
29
|
+
if (!input || typeof input !== "object") {
|
|
30
|
+
return input;
|
|
31
|
+
}
|
|
32
|
+
const args = input;
|
|
33
|
+
// Some models (Opus 4.6, GLM-5.1) send edits as a JSON string instead of an array
|
|
34
|
+
if (typeof args.edits === "string") {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(args.edits);
|
|
37
|
+
if (Array.isArray(parsed))
|
|
38
|
+
args.edits = parsed;
|
|
39
|
+
}
|
|
40
|
+
catch { }
|
|
41
|
+
}
|
|
42
|
+
const legacy = args;
|
|
43
|
+
if (typeof legacy.oldText !== "string" || typeof legacy.newText !== "string") {
|
|
44
|
+
return args;
|
|
45
|
+
}
|
|
46
|
+
const edits = Array.isArray(legacy.edits) ? [...legacy.edits] : [];
|
|
47
|
+
edits.push({ oldText: legacy.oldText, newText: legacy.newText });
|
|
48
|
+
const { oldText: _oldText, newText: _newText, ...rest } = legacy;
|
|
49
|
+
return { ...rest, edits };
|
|
50
|
+
}
|
|
51
|
+
function validateEditInput(input) {
|
|
52
|
+
if (!Array.isArray(input.edits) || input.edits.length === 0) {
|
|
53
|
+
throw new Error("Edit tool input is invalid. edits must contain at least one replacement.");
|
|
54
|
+
}
|
|
55
|
+
return { path: input.path, edits: input.edits };
|
|
56
|
+
}
|
|
57
|
+
function createEditCallRenderComponent() {
|
|
58
|
+
return Object.assign(new Box(1, 1, (text) => text), {
|
|
59
|
+
preview: undefined,
|
|
60
|
+
previewArgsKey: undefined,
|
|
61
|
+
previewPending: false,
|
|
62
|
+
settledError: false,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function getEditCallRenderComponent(state, lastComponent) {
|
|
66
|
+
if (lastComponent instanceof Box) {
|
|
67
|
+
const component = lastComponent;
|
|
68
|
+
state.callComponent = component;
|
|
69
|
+
return component;
|
|
70
|
+
}
|
|
71
|
+
if (state.callComponent) {
|
|
72
|
+
return state.callComponent;
|
|
73
|
+
}
|
|
74
|
+
const component = createEditCallRenderComponent();
|
|
75
|
+
state.callComponent = component;
|
|
76
|
+
return component;
|
|
77
|
+
}
|
|
78
|
+
function getRenderablePreviewInput(args) {
|
|
79
|
+
if (!args) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const path = typeof args.path === "string" ? args.path : typeof args.file_path === "string" ? args.file_path : null;
|
|
83
|
+
if (!path) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(args.edits) &&
|
|
87
|
+
args.edits.length > 0 &&
|
|
88
|
+
args.edits.every((edit) => typeof edit?.oldText === "string" && typeof edit?.newText === "string")) {
|
|
89
|
+
return { path, edits: args.edits };
|
|
90
|
+
}
|
|
91
|
+
if (typeof args.oldText === "string" && typeof args.newText === "string") {
|
|
92
|
+
return { path, edits: [{ oldText: args.oldText, newText: args.newText }] };
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
function formatEditCall(args, theme) {
|
|
97
|
+
const invalidArg = invalidArgText(theme);
|
|
98
|
+
const rawPath = str(args?.file_path ?? args?.path);
|
|
99
|
+
const path = rawPath !== null ? shortenPath(rawPath) : null;
|
|
100
|
+
const pathDisplay = path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "...");
|
|
101
|
+
return `${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}`;
|
|
102
|
+
}
|
|
103
|
+
function formatEditResult(args, preview, result, theme, isError) {
|
|
104
|
+
const rawPath = str(args?.file_path ?? args?.path);
|
|
105
|
+
const previewDiff = preview && !("error" in preview) ? preview.diff : undefined;
|
|
106
|
+
const previewError = preview && "error" in preview ? preview.error : undefined;
|
|
107
|
+
if (isError) {
|
|
108
|
+
const errorText = result.content
|
|
109
|
+
.filter((c) => c.type === "text")
|
|
110
|
+
.map((c) => c.text || "")
|
|
111
|
+
.join("\n");
|
|
112
|
+
if (!errorText || errorText === previewError) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
return theme.fg("error", errorText);
|
|
116
|
+
}
|
|
117
|
+
const resultDiff = result.details?.diff;
|
|
118
|
+
if (resultDiff && resultDiff !== previewDiff) {
|
|
119
|
+
return renderDiff(resultDiff, { filePath: rawPath ?? undefined });
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
function getEditHeaderBg(preview, settledError, theme) {
|
|
124
|
+
if (preview) {
|
|
125
|
+
if ("error" in preview) {
|
|
126
|
+
return (text) => theme.bg("toolErrorBg", text);
|
|
127
|
+
}
|
|
128
|
+
return (text) => theme.bg("toolSuccessBg", text);
|
|
129
|
+
}
|
|
130
|
+
if (settledError) {
|
|
131
|
+
return (text) => theme.bg("toolErrorBg", text);
|
|
132
|
+
}
|
|
133
|
+
return (text) => theme.bg("toolPendingBg", text);
|
|
134
|
+
}
|
|
135
|
+
function buildEditCallComponent(component, args, theme) {
|
|
136
|
+
component.setBgFn(getEditHeaderBg(component.preview, component.settledError, theme));
|
|
137
|
+
component.clear();
|
|
138
|
+
component.addChild(new Text(formatEditCall(args, theme), 0, 0));
|
|
139
|
+
if (!component.preview) {
|
|
140
|
+
return component;
|
|
141
|
+
}
|
|
142
|
+
const body = "error" in component.preview ? theme.fg("error", component.preview.error) : renderDiff(component.preview.diff);
|
|
143
|
+
component.addChild(new Spacer(1));
|
|
144
|
+
component.addChild(new Text(body, 0, 0));
|
|
145
|
+
return component;
|
|
146
|
+
}
|
|
147
|
+
function setEditPreview(component, preview, argsKey) {
|
|
148
|
+
const current = component.preview;
|
|
149
|
+
const changed = current === undefined ||
|
|
150
|
+
("error" in current && "error" in preview
|
|
151
|
+
? current.error !== preview.error
|
|
152
|
+
: "error" in current !== "error" in preview) ||
|
|
153
|
+
(!("error" in current) &&
|
|
154
|
+
!("error" in preview) &&
|
|
155
|
+
(current.diff !== preview.diff || current.firstChangedLine !== preview.firstChangedLine));
|
|
156
|
+
component.preview = preview;
|
|
157
|
+
component.previewArgsKey = argsKey;
|
|
158
|
+
component.previewPending = false;
|
|
159
|
+
return changed;
|
|
160
|
+
}
|
|
161
|
+
export function createEditToolDefinition(cwd, options) {
|
|
162
|
+
const ops = options?.operations ?? defaultEditOperations;
|
|
163
|
+
return {
|
|
164
|
+
name: "edit",
|
|
165
|
+
label: "edit",
|
|
166
|
+
description: "Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.",
|
|
167
|
+
promptSnippet: "Make precise file edits with exact text replacement, including multiple disjoint edits in one call",
|
|
168
|
+
promptGuidelines: [
|
|
169
|
+
"Use edit for precise changes (edits[].oldText must match exactly)",
|
|
170
|
+
"When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls",
|
|
171
|
+
"Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.",
|
|
172
|
+
"Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.",
|
|
173
|
+
],
|
|
174
|
+
parameters: editSchema,
|
|
175
|
+
renderShell: "self",
|
|
176
|
+
prepareArguments: prepareEditArguments,
|
|
177
|
+
async execute(_toolCallId, input, signal, _onUpdate, _ctx) {
|
|
178
|
+
const { path, edits } = validateEditInput(input);
|
|
179
|
+
const absolutePath = resolveToCwd(path, cwd);
|
|
180
|
+
return withFileMutationQueue(absolutePath, () => new Promise((resolve, reject) => {
|
|
181
|
+
// Check if already aborted.
|
|
182
|
+
if (signal?.aborted) {
|
|
183
|
+
reject(new Error("Operation aborted"));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
let aborted = false;
|
|
187
|
+
// Set up abort handler.
|
|
188
|
+
const onAbort = () => {
|
|
189
|
+
aborted = true;
|
|
190
|
+
reject(new Error("Operation aborted"));
|
|
191
|
+
};
|
|
192
|
+
if (signal) {
|
|
193
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
194
|
+
}
|
|
195
|
+
// Perform the edit operation.
|
|
196
|
+
void (async () => {
|
|
197
|
+
try {
|
|
198
|
+
// Check if file exists.
|
|
199
|
+
try {
|
|
200
|
+
await ops.access(absolutePath);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
const errorMessage = error instanceof Error && "code" in error ? `Error code: ${error.code}` : String(error);
|
|
204
|
+
if (signal) {
|
|
205
|
+
signal.removeEventListener("abort", onAbort);
|
|
206
|
+
}
|
|
207
|
+
reject(new Error(`Could not edit file: ${path}. ${errorMessage}.`));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// Check if aborted before reading.
|
|
211
|
+
if (aborted) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// Read the file.
|
|
215
|
+
const buffer = await ops.readFile(absolutePath);
|
|
216
|
+
const rawContent = buffer.toString("utf-8");
|
|
217
|
+
// Check if aborted after reading.
|
|
218
|
+
if (aborted) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// Strip BOM before matching. The model will not include an invisible BOM in oldText.
|
|
222
|
+
const { bom, text: content } = stripBom(rawContent);
|
|
223
|
+
const originalEnding = detectLineEnding(content);
|
|
224
|
+
const normalizedContent = normalizeToLF(content);
|
|
225
|
+
const { baseContent, newContent } = applyEditsToNormalizedContent(normalizedContent, edits, path);
|
|
226
|
+
// Check if aborted before writing.
|
|
227
|
+
if (aborted) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const finalContent = bom + restoreLineEndings(newContent, originalEnding);
|
|
231
|
+
await ops.writeFile(absolutePath, finalContent);
|
|
232
|
+
// Check if aborted after writing.
|
|
233
|
+
if (aborted) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
// Clean up abort handler.
|
|
237
|
+
if (signal) {
|
|
238
|
+
signal.removeEventListener("abort", onAbort);
|
|
239
|
+
}
|
|
240
|
+
const diffResult = generateDiffString(baseContent, newContent);
|
|
241
|
+
const patch = generateUnifiedPatch(path, baseContent, newContent);
|
|
242
|
+
resolve({
|
|
243
|
+
content: [
|
|
244
|
+
{
|
|
245
|
+
type: "text",
|
|
246
|
+
text: `Successfully replaced ${edits.length} block(s) in ${path}.`,
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
details: { diff: diffResult.diff, patch, firstChangedLine: diffResult.firstChangedLine },
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
// Clean up abort handler.
|
|
254
|
+
if (signal) {
|
|
255
|
+
signal.removeEventListener("abort", onAbort);
|
|
256
|
+
}
|
|
257
|
+
if (!aborted) {
|
|
258
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
})();
|
|
262
|
+
}));
|
|
263
|
+
},
|
|
264
|
+
renderCall(args, theme, context) {
|
|
265
|
+
const component = getEditCallRenderComponent(context.state, context.lastComponent);
|
|
266
|
+
const previewInput = getRenderablePreviewInput(args);
|
|
267
|
+
const argsKey = previewInput
|
|
268
|
+
? JSON.stringify({ path: previewInput.path, edits: previewInput.edits })
|
|
269
|
+
: undefined;
|
|
270
|
+
if (component.previewArgsKey !== argsKey) {
|
|
271
|
+
component.preview = undefined;
|
|
272
|
+
component.previewArgsKey = argsKey;
|
|
273
|
+
component.previewPending = false;
|
|
274
|
+
component.settledError = false;
|
|
275
|
+
}
|
|
276
|
+
if (context.argsComplete && previewInput && !component.preview && !component.previewPending) {
|
|
277
|
+
component.previewPending = true;
|
|
278
|
+
const requestKey = argsKey;
|
|
279
|
+
void computeEditsDiff(previewInput.path, previewInput.edits, context.cwd).then((preview) => {
|
|
280
|
+
if (component.previewArgsKey === requestKey) {
|
|
281
|
+
setEditPreview(component, preview, requestKey);
|
|
282
|
+
context.invalidate();
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return buildEditCallComponent(component, args, theme);
|
|
287
|
+
},
|
|
288
|
+
renderResult(result, _options, theme, context) {
|
|
289
|
+
const callComponent = context.state.callComponent;
|
|
290
|
+
const previewInput = getRenderablePreviewInput(context.args);
|
|
291
|
+
const argsKey = previewInput
|
|
292
|
+
? JSON.stringify({ path: previewInput.path, edits: previewInput.edits })
|
|
293
|
+
: undefined;
|
|
294
|
+
const typedResult = result;
|
|
295
|
+
const resultDiff = !context.isError ? typedResult.details?.diff : undefined;
|
|
296
|
+
let changed = false;
|
|
297
|
+
if (callComponent) {
|
|
298
|
+
if (typeof resultDiff === "string") {
|
|
299
|
+
changed =
|
|
300
|
+
setEditPreview(callComponent, { diff: resultDiff, firstChangedLine: typedResult.details?.firstChangedLine }, argsKey) || changed;
|
|
301
|
+
}
|
|
302
|
+
if (callComponent.settledError !== context.isError) {
|
|
303
|
+
callComponent.settledError = context.isError;
|
|
304
|
+
changed = true;
|
|
305
|
+
}
|
|
306
|
+
if (changed) {
|
|
307
|
+
buildEditCallComponent(callComponent, context.args, theme);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const output = formatEditResult(context.args, callComponent?.preview, typedResult, theme, context.isError);
|
|
311
|
+
const component = context.lastComponent ?? new Container();
|
|
312
|
+
component.clear();
|
|
313
|
+
if (!output) {
|
|
314
|
+
return component;
|
|
315
|
+
}
|
|
316
|
+
component.addChild(new Spacer(1));
|
|
317
|
+
component.addChild(new Text(output, 1, 0));
|
|
318
|
+
return component;
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
export function createEditTool(cwd, options) {
|
|
323
|
+
return wrapToolDefinition(createEditToolDefinition(cwd, options));
|
|
324
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
const fileMutationQueues = new Map();
|
|
4
|
+
function getMutationQueueKey(filePath) {
|
|
5
|
+
const resolvedPath = resolve(filePath);
|
|
6
|
+
try {
|
|
7
|
+
return realpathSync.native(resolvedPath);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return resolvedPath;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Serialize file mutation operations targeting the same file.
|
|
15
|
+
* Operations for different files still run in parallel.
|
|
16
|
+
*/
|
|
17
|
+
export async function withFileMutationQueue(filePath, fn) {
|
|
18
|
+
const key = getMutationQueueKey(filePath);
|
|
19
|
+
const currentQueue = fileMutationQueues.get(key) ?? Promise.resolve();
|
|
20
|
+
let releaseNext;
|
|
21
|
+
const nextQueue = new Promise((resolveQueue) => {
|
|
22
|
+
releaseNext = resolveQueue;
|
|
23
|
+
});
|
|
24
|
+
const chainedQueue = currentQueue.then(() => nextQueue);
|
|
25
|
+
fileMutationQueues.set(key, chainedQueue);
|
|
26
|
+
await currentQueue;
|
|
27
|
+
try {
|
|
28
|
+
return await fn();
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
releaseNext();
|
|
32
|
+
if (fileMutationQueues.get(key) === chainedQueue) {
|
|
33
|
+
fileMutationQueues.delete(key);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import { Text } from "../../../tui/index.js";
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { Type } from "typebox";
|
|
7
|
+
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
|
|
8
|
+
import { ensureTool } from "../../utils/tools-manager.js";
|
|
9
|
+
import { resolveToCwd } from "./path-utils.js";
|
|
10
|
+
import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js";
|
|
11
|
+
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
|
12
|
+
import { DEFAULT_MAX_BYTES, formatSize, truncateHead } from "./truncate.js";
|
|
13
|
+
function toPosixPath(value) {
|
|
14
|
+
return value.split(path.sep).join("/");
|
|
15
|
+
}
|
|
16
|
+
const findSchema = Type.Object({
|
|
17
|
+
pattern: Type.String({
|
|
18
|
+
description: "Glob pattern to match files, e.g. '*.ts', '**/*.json', or 'src/**/*.spec.ts'",
|
|
19
|
+
}),
|
|
20
|
+
path: Type.Optional(Type.String({ description: "Directory to search in (default: current directory)" })),
|
|
21
|
+
limit: Type.Optional(Type.Number({ description: "Maximum number of results (default: 1000)" })),
|
|
22
|
+
});
|
|
23
|
+
const DEFAULT_LIMIT = 1000;
|
|
24
|
+
const defaultFindOperations = {
|
|
25
|
+
exists: existsSync,
|
|
26
|
+
// This is a placeholder. Actual fd execution happens in execute() when no custom glob is provided.
|
|
27
|
+
glob: () => [],
|
|
28
|
+
};
|
|
29
|
+
function formatFindCall(args, theme) {
|
|
30
|
+
const pattern = str(args?.pattern);
|
|
31
|
+
const rawPath = str(args?.path);
|
|
32
|
+
const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
|
|
33
|
+
const limit = args?.limit;
|
|
34
|
+
const invalidArg = invalidArgText(theme);
|
|
35
|
+
let text = theme.fg("toolTitle", theme.bold("find")) +
|
|
36
|
+
" " +
|
|
37
|
+
(pattern === null ? invalidArg : theme.fg("accent", pattern || "")) +
|
|
38
|
+
theme.fg("toolOutput", ` in ${path === null ? invalidArg : path}`);
|
|
39
|
+
if (limit !== undefined) {
|
|
40
|
+
text += theme.fg("toolOutput", ` (limit ${limit})`);
|
|
41
|
+
}
|
|
42
|
+
return text;
|
|
43
|
+
}
|
|
44
|
+
function formatFindResult(result, options, theme, showImages) {
|
|
45
|
+
const output = getTextOutput(result, showImages).trim();
|
|
46
|
+
let text = "";
|
|
47
|
+
if (output) {
|
|
48
|
+
const lines = output.split("\n");
|
|
49
|
+
const maxLines = options.expanded ? lines.length : 20;
|
|
50
|
+
const displayLines = lines.slice(0, maxLines);
|
|
51
|
+
const remaining = lines.length - maxLines;
|
|
52
|
+
text += `\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`;
|
|
53
|
+
if (remaining > 0) {
|
|
54
|
+
text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const resultLimit = result.details?.resultLimitReached;
|
|
58
|
+
const truncation = result.details?.truncation;
|
|
59
|
+
if (resultLimit || truncation?.truncated) {
|
|
60
|
+
const warnings = [];
|
|
61
|
+
if (resultLimit)
|
|
62
|
+
warnings.push(`${resultLimit} results limit`);
|
|
63
|
+
if (truncation?.truncated)
|
|
64
|
+
warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);
|
|
65
|
+
text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
|
|
66
|
+
}
|
|
67
|
+
return text;
|
|
68
|
+
}
|
|
69
|
+
export function createFindToolDefinition(cwd, options) {
|
|
70
|
+
const customOps = options?.operations;
|
|
71
|
+
return {
|
|
72
|
+
name: "find",
|
|
73
|
+
label: "find",
|
|
74
|
+
description: `Search for files by glob pattern. Returns matching file paths relative to the search directory. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} results or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,
|
|
75
|
+
promptSnippet: "Find files by glob pattern (respects .gitignore)",
|
|
76
|
+
parameters: findSchema,
|
|
77
|
+
async execute(_toolCallId, { pattern, path: searchDir, limit }, signal, _onUpdate, _ctx) {
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
if (signal?.aborted) {
|
|
80
|
+
reject(new Error("Operation aborted"));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
let settled = false;
|
|
84
|
+
let stopChild;
|
|
85
|
+
const settle = (fn) => {
|
|
86
|
+
if (settled)
|
|
87
|
+
return;
|
|
88
|
+
settled = true;
|
|
89
|
+
signal?.removeEventListener("abort", onAbort);
|
|
90
|
+
stopChild = undefined;
|
|
91
|
+
fn();
|
|
92
|
+
};
|
|
93
|
+
const onAbort = () => {
|
|
94
|
+
stopChild?.();
|
|
95
|
+
settle(() => reject(new Error("Operation aborted")));
|
|
96
|
+
};
|
|
97
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
98
|
+
(async () => {
|
|
99
|
+
try {
|
|
100
|
+
const searchPath = resolveToCwd(searchDir || ".", cwd);
|
|
101
|
+
const effectiveLimit = limit ?? DEFAULT_LIMIT;
|
|
102
|
+
const ops = customOps ?? defaultFindOperations;
|
|
103
|
+
// If custom operations provide glob(), use that instead of fd.
|
|
104
|
+
if (customOps?.glob) {
|
|
105
|
+
if (!(await ops.exists(searchPath))) {
|
|
106
|
+
settle(() => reject(new Error(`Path not found: ${searchPath}`)));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (signal?.aborted) {
|
|
110
|
+
settle(() => reject(new Error("Operation aborted")));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const results = await ops.glob(pattern, searchPath, {
|
|
114
|
+
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
115
|
+
limit: effectiveLimit,
|
|
116
|
+
});
|
|
117
|
+
if (signal?.aborted) {
|
|
118
|
+
settle(() => reject(new Error("Operation aborted")));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (results.length === 0) {
|
|
122
|
+
settle(() => resolve({
|
|
123
|
+
content: [{ type: "text", text: "No files found matching pattern" }],
|
|
124
|
+
details: undefined,
|
|
125
|
+
}));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Relativize paths against the search root for stable output.
|
|
129
|
+
const relativized = results.map((p) => {
|
|
130
|
+
if (p.startsWith(searchPath))
|
|
131
|
+
return toPosixPath(p.slice(searchPath.length + 1));
|
|
132
|
+
return toPosixPath(path.relative(searchPath, p));
|
|
133
|
+
});
|
|
134
|
+
const resultLimitReached = relativized.length >= effectiveLimit;
|
|
135
|
+
const rawOutput = relativized.join("\n");
|
|
136
|
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
137
|
+
let resultOutput = truncation.content;
|
|
138
|
+
const details = {};
|
|
139
|
+
const notices = [];
|
|
140
|
+
if (resultLimitReached) {
|
|
141
|
+
notices.push(`${effectiveLimit} results limit reached`);
|
|
142
|
+
details.resultLimitReached = effectiveLimit;
|
|
143
|
+
}
|
|
144
|
+
if (truncation.truncated) {
|
|
145
|
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
146
|
+
details.truncation = truncation;
|
|
147
|
+
}
|
|
148
|
+
if (notices.length > 0) {
|
|
149
|
+
resultOutput += `\n\n[${notices.join(". ")}]`;
|
|
150
|
+
}
|
|
151
|
+
settle(() => resolve({
|
|
152
|
+
content: [{ type: "text", text: resultOutput }],
|
|
153
|
+
details: Object.keys(details).length > 0 ? details : undefined,
|
|
154
|
+
}));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// Default implementation uses fd.
|
|
158
|
+
const fdPath = await ensureTool("fd", true);
|
|
159
|
+
if (signal?.aborted) {
|
|
160
|
+
settle(() => reject(new Error("Operation aborted")));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (!fdPath) {
|
|
164
|
+
settle(() => reject(new Error("fd is not available and could not be downloaded")));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// Build fd arguments. --no-require-git makes fd apply hierarchical .gitignore
|
|
168
|
+
// semantics whether or not the search path is inside a git repository, without
|
|
169
|
+
// leaking sibling-directory rules the way --ignore-file (a global source) would.
|
|
170
|
+
const args = [
|
|
171
|
+
"--glob",
|
|
172
|
+
"--color=never",
|
|
173
|
+
"--hidden",
|
|
174
|
+
"--no-require-git",
|
|
175
|
+
"--max-results",
|
|
176
|
+
String(effectiveLimit),
|
|
177
|
+
];
|
|
178
|
+
// fd --glob matches against the basename unless --full-path is set; in --full-path
|
|
179
|
+
// mode it matches against the absolute candidate path, so a path-containing
|
|
180
|
+
// pattern like 'src/**/*.spec.ts' needs a leading '**/' to match anything.
|
|
181
|
+
let effectivePattern = pattern;
|
|
182
|
+
if (pattern.includes("/")) {
|
|
183
|
+
args.push("--full-path");
|
|
184
|
+
if (!pattern.startsWith("/") && !pattern.startsWith("**/") && pattern !== "**") {
|
|
185
|
+
effectivePattern = `**/${pattern}`;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
args.push("--", effectivePattern, searchPath);
|
|
189
|
+
const child = spawn(fdPath, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
190
|
+
const rl = createInterface({ input: child.stdout });
|
|
191
|
+
let stderr = "";
|
|
192
|
+
const lines = [];
|
|
193
|
+
stopChild = () => {
|
|
194
|
+
if (!child.killed) {
|
|
195
|
+
child.kill();
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
const cleanup = () => {
|
|
199
|
+
rl.close();
|
|
200
|
+
};
|
|
201
|
+
child.stderr?.on("data", (chunk) => {
|
|
202
|
+
stderr += chunk.toString();
|
|
203
|
+
});
|
|
204
|
+
rl.on("line", (line) => {
|
|
205
|
+
lines.push(line);
|
|
206
|
+
});
|
|
207
|
+
child.on("error", (error) => {
|
|
208
|
+
cleanup();
|
|
209
|
+
settle(() => reject(new Error(`Failed to run fd: ${error.message}`)));
|
|
210
|
+
});
|
|
211
|
+
child.on("close", (code) => {
|
|
212
|
+
cleanup();
|
|
213
|
+
if (signal?.aborted) {
|
|
214
|
+
settle(() => reject(new Error("Operation aborted")));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const output = lines.join("\n");
|
|
218
|
+
if (code !== 0) {
|
|
219
|
+
const errorMsg = stderr.trim() || `fd exited with code ${code}`;
|
|
220
|
+
if (!output) {
|
|
221
|
+
settle(() => reject(new Error(errorMsg)));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!output) {
|
|
226
|
+
settle(() => resolve({
|
|
227
|
+
content: [{ type: "text", text: "No files found matching pattern" }],
|
|
228
|
+
details: undefined,
|
|
229
|
+
}));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const relativized = [];
|
|
233
|
+
for (const rawLine of lines) {
|
|
234
|
+
const line = rawLine.replace(/\r$/, "").trim();
|
|
235
|
+
if (!line)
|
|
236
|
+
continue;
|
|
237
|
+
const hadTrailingSlash = line.endsWith("/") || line.endsWith("\\");
|
|
238
|
+
let relativePath = line;
|
|
239
|
+
if (line.startsWith(searchPath)) {
|
|
240
|
+
relativePath = line.slice(searchPath.length + 1);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
relativePath = path.relative(searchPath, line);
|
|
244
|
+
}
|
|
245
|
+
if (hadTrailingSlash && !relativePath.endsWith("/"))
|
|
246
|
+
relativePath += "/";
|
|
247
|
+
relativized.push(toPosixPath(relativePath));
|
|
248
|
+
}
|
|
249
|
+
const resultLimitReached = relativized.length >= effectiveLimit;
|
|
250
|
+
const rawOutput = relativized.join("\n");
|
|
251
|
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
252
|
+
let resultOutput = truncation.content;
|
|
253
|
+
const details = {};
|
|
254
|
+
const notices = [];
|
|
255
|
+
if (resultLimitReached) {
|
|
256
|
+
notices.push(`${effectiveLimit} results limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`);
|
|
257
|
+
details.resultLimitReached = effectiveLimit;
|
|
258
|
+
}
|
|
259
|
+
if (truncation.truncated) {
|
|
260
|
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
261
|
+
details.truncation = truncation;
|
|
262
|
+
}
|
|
263
|
+
if (notices.length > 0) {
|
|
264
|
+
resultOutput += `\n\n[${notices.join(". ")}]`;
|
|
265
|
+
}
|
|
266
|
+
settle(() => resolve({
|
|
267
|
+
content: [{ type: "text", text: resultOutput }],
|
|
268
|
+
details: Object.keys(details).length > 0 ? details : undefined,
|
|
269
|
+
}));
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
catch (e) {
|
|
273
|
+
if (signal?.aborted) {
|
|
274
|
+
settle(() => reject(new Error("Operation aborted")));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
278
|
+
settle(() => reject(error));
|
|
279
|
+
}
|
|
280
|
+
})();
|
|
281
|
+
});
|
|
282
|
+
},
|
|
283
|
+
renderCall(args, theme, context) {
|
|
284
|
+
const text = context.lastComponent ?? new Text("", 0, 0);
|
|
285
|
+
text.setText(formatFindCall(args, theme));
|
|
286
|
+
return text;
|
|
287
|
+
},
|
|
288
|
+
renderResult(result, options, theme, context) {
|
|
289
|
+
const text = context.lastComponent ?? new Text("", 0, 0);
|
|
290
|
+
text.setText(formatFindResult(result, options, theme, context.showImages));
|
|
291
|
+
return text;
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
export function createFindTool(cwd, options) {
|
|
296
|
+
return wrapToolDefinition(createFindToolDefinition(cwd, options));
|
|
297
|
+
}
|