@aexol/spectral 0.9.159 → 0.9.161
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/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +29 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/memory/tool-output-compressor.d.ts +8 -2
- package/dist/memory/tool-output-compressor.d.ts.map +1 -1
- package/dist/memory/tool-output-compressor.js +8 -83
- package/dist/sdk/coding-agent/core/compaction/policy.js +6 -6
- package/dist/sdk/coding-agent/core/extensions/index.d.ts +2 -2
- package/dist/sdk/coding-agent/core/extensions/index.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/index.js +1 -1
- package/dist/sdk/coding-agent/core/extensions/types.d.ts +3 -44
- package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/types.js +0 -12
- package/dist/sdk/coding-agent/core/package-manager.d.ts +0 -15
- package/dist/sdk/coding-agent/core/package-manager.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/package-manager.js +0 -80
- package/dist/sdk/coding-agent/core/sdk.d.ts +2 -2
- package/dist/sdk/coding-agent/core/sdk.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/sdk.js +3 -3
- package/dist/sdk/coding-agent/core/tools/bash-blocklist.js +1 -1
- package/dist/sdk/coding-agent/core/tools/index.d.ts +1 -13
- package/dist/sdk/coding-agent/core/tools/index.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/index.js +0 -44
- package/dist/sdk/coding-agent/index.d.ts +4 -4
- package/dist/sdk/coding-agent/index.d.ts.map +1 -1
- package/dist/sdk/coding-agent/index.js +3 -3
- package/dist/server/agent-bridge.d.ts.map +1 -1
- package/dist/server/agent-bridge.js +1 -0
- package/package.json +1 -1
- package/dist/sdk/coding-agent/core/tools/edit.d.ts +0 -45
- package/dist/sdk/coding-agent/core/tools/edit.d.ts.map +0 -1
- package/dist/sdk/coding-agent/core/tools/edit.js +0 -216
- package/dist/sdk/coding-agent/core/tools/grep.d.ts +0 -37
- package/dist/sdk/coding-agent/core/tools/grep.d.ts.map +0 -1
- package/dist/sdk/coding-agent/core/tools/grep.js +0 -288
- package/dist/sdk/coding-agent/core/tools/ls.d.ts +0 -37
- package/dist/sdk/coding-agent/core/tools/ls.d.ts.map +0 -1
- package/dist/sdk/coding-agent/core/tools/ls.js +0 -156
- package/dist/sdk/coding-agent/core/tools/write.d.ts +0 -23
- package/dist/sdk/coding-agent/core/tools/write.d.ts.map +0 -1
- package/dist/sdk/coding-agent/core/tools/write.js +0 -134
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import { constants } from "fs";
|
|
2
|
-
import { access as fsAccess, readFile as fsReadFile, stat as fsStat, writeFile as fsWriteFile } from "fs/promises";
|
|
3
|
-
import { Type } from "typebox";
|
|
4
|
-
import { renderDiff } from "./render-diff.js";
|
|
5
|
-
import { applyEditsToNormalizedContent, detectLineEnding, generateDiffString, generateUnifiedPatch, normalizeToLF, restoreLineEndings, stripBom, } from "./edit-diff.js";
|
|
6
|
-
import { atomicWriteFile } from "./atomic-write.js";
|
|
7
|
-
import { withFileMutationQueue } from "./file-mutation-queue.js";
|
|
8
|
-
import { resolveToCwd } from "./path-utils.js";
|
|
9
|
-
import { 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
|
-
stat: (path) => fsStat(path),
|
|
28
|
-
};
|
|
29
|
-
function prepareEditArguments(input) {
|
|
30
|
-
if (!input || typeof input !== "object") {
|
|
31
|
-
return input;
|
|
32
|
-
}
|
|
33
|
-
const args = input;
|
|
34
|
-
// Some models (Opus 4.6, GLM-5.1) send edits as a JSON string instead of an array
|
|
35
|
-
if (typeof args.edits === "string") {
|
|
36
|
-
try {
|
|
37
|
-
const parsed = JSON.parse(args.edits);
|
|
38
|
-
if (Array.isArray(parsed))
|
|
39
|
-
args.edits = parsed;
|
|
40
|
-
}
|
|
41
|
-
catch (error) {
|
|
42
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
43
|
-
throw new Error(`Invalid "edits" argument for edit tool: expected an array but received a string that could not be parsed as JSON. Parse error: ${reason}`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
const legacy = args;
|
|
47
|
-
if (typeof legacy.oldText !== "string" || typeof legacy.newText !== "string") {
|
|
48
|
-
return args;
|
|
49
|
-
}
|
|
50
|
-
const edits = Array.isArray(legacy.edits) ? [...legacy.edits] : [];
|
|
51
|
-
edits.push({ oldText: legacy.oldText, newText: legacy.newText });
|
|
52
|
-
const { oldText: _oldText, newText: _newText, ...rest } = legacy;
|
|
53
|
-
return { ...rest, edits };
|
|
54
|
-
}
|
|
55
|
-
function validateEditInput(input) {
|
|
56
|
-
if (!Array.isArray(input.edits) || input.edits.length === 0) {
|
|
57
|
-
throw new Error("Edit tool input is invalid. edits must contain at least one replacement.");
|
|
58
|
-
}
|
|
59
|
-
for (const edit of input.edits) {
|
|
60
|
-
if (!edit ||
|
|
61
|
-
typeof edit !== "object" ||
|
|
62
|
-
typeof edit.oldText !== "string" ||
|
|
63
|
-
typeof edit.newText !== "string") {
|
|
64
|
-
throw new Error("Edit tool input is invalid. Each edit must have string oldText and newText properties.");
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return { path: input.path, edits: input.edits };
|
|
68
|
-
}
|
|
69
|
-
function formatEditCall(args, _theme) {
|
|
70
|
-
const rawPath = str(args?.file_path ?? args?.path);
|
|
71
|
-
const path = rawPath !== null ? shortenPath(rawPath) : null;
|
|
72
|
-
const pathDisplay = path === null ? "[invalid]" : path || "...";
|
|
73
|
-
return `edit ${pathDisplay}`;
|
|
74
|
-
}
|
|
75
|
-
function formatEditResult(args, result, _theme, isError) {
|
|
76
|
-
const rawPath = str(args?.file_path ?? args?.path);
|
|
77
|
-
if (isError) {
|
|
78
|
-
const errorText = result.content
|
|
79
|
-
.filter((c) => c.type === "text")
|
|
80
|
-
.map((c) => c.text || "")
|
|
81
|
-
.join("\n");
|
|
82
|
-
if (!errorText) {
|
|
83
|
-
return undefined;
|
|
84
|
-
}
|
|
85
|
-
return errorText;
|
|
86
|
-
}
|
|
87
|
-
const resultDiff = result.details?.diff;
|
|
88
|
-
if (resultDiff) {
|
|
89
|
-
return renderDiff(resultDiff, { filePath: rawPath ?? undefined });
|
|
90
|
-
}
|
|
91
|
-
return undefined;
|
|
92
|
-
}
|
|
93
|
-
export function createEditToolDefinition(cwd, options) {
|
|
94
|
-
const ops = options?.operations ?? defaultEditOperations;
|
|
95
|
-
return {
|
|
96
|
-
name: "edit",
|
|
97
|
-
label: "edit",
|
|
98
|
-
description: "Edit a single file using targeted text replacement. Prefer apply_patch for manual code edits when that tool is available; use edit for small exact replacements only. Prefer copying edits[].oldText from a fresh read of the current file. Every oldText must identify one unique, non-overlapping region of the original file snapshot for this tool call. 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.",
|
|
99
|
-
promptSnippet: "Make precise file edits with targeted text replacement, including multiple disjoint edits in one call",
|
|
100
|
-
promptGuidelines: [
|
|
101
|
-
"Prefer apply_patch for manual code edits when that tool is available, especially with OpenAI/GPT/Codex models; use edit only for small exact replacements where edits[].oldText is copied from the latest file contents.",
|
|
102
|
-
"Read or re-read the target file immediately before risky edits, especially after another tool or agent may have changed it.",
|
|
103
|
-
"Never use edit from memory or from stale snippets. oldText must be copied byte-for-byte from the latest read output, including indentation, blank lines, comments, and punctuation.",
|
|
104
|
-
"When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls.",
|
|
105
|
-
"Each edits[].oldText is matched against the original file snapshot for that edit call, not after earlier edits are applied. Do not emit sequential edits where a later oldText depends on an earlier replacement.",
|
|
106
|
-
"Do not emit overlapping or nested edits. If edits touch the same block or nearby lines, merge them into one edit.",
|
|
107
|
-
"Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.",
|
|
108
|
-
"If edit reports that oldText was not found, stop and re-read the file before retrying; do not retry the same oldText.",
|
|
109
|
-
],
|
|
110
|
-
parameters: editSchema,
|
|
111
|
-
prepareArguments: prepareEditArguments,
|
|
112
|
-
async execute(_toolCallId, input, signal, _onUpdate, _ctx) {
|
|
113
|
-
const { path, edits } = validateEditInput(input);
|
|
114
|
-
const absolutePath = resolveToCwd(path, cwd);
|
|
115
|
-
return withFileMutationQueue(absolutePath, () => new Promise((resolve, reject) => {
|
|
116
|
-
// Check if already aborted.
|
|
117
|
-
if (signal?.aborted) {
|
|
118
|
-
reject(new Error("Operation aborted"));
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
let aborted = false;
|
|
122
|
-
// Set up abort handler.
|
|
123
|
-
const onAbort = () => {
|
|
124
|
-
aborted = true;
|
|
125
|
-
reject(new Error("Operation aborted"));
|
|
126
|
-
};
|
|
127
|
-
if (signal) {
|
|
128
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
129
|
-
}
|
|
130
|
-
// Perform the edit operation.
|
|
131
|
-
void (async () => {
|
|
132
|
-
try {
|
|
133
|
-
// Check if file exists.
|
|
134
|
-
try {
|
|
135
|
-
await ops.access(absolutePath);
|
|
136
|
-
}
|
|
137
|
-
catch (error) {
|
|
138
|
-
const errorMessage = error instanceof Error && "code" in error ? `Error code: ${error.code}` : String(error);
|
|
139
|
-
if (signal) {
|
|
140
|
-
signal.removeEventListener("abort", onAbort);
|
|
141
|
-
}
|
|
142
|
-
reject(new Error(`Could not edit file: ${path}. ${errorMessage}.`));
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
// Check if aborted before reading.
|
|
146
|
-
if (aborted) {
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
// Read the file.
|
|
150
|
-
const buffer = await ops.readFile(absolutePath);
|
|
151
|
-
const rawContent = buffer.toString("utf-8");
|
|
152
|
-
// Capture a freshness token so we can detect stale reads before writing.
|
|
153
|
-
const freshnessTokenBefore = ops.stat ? (await ops.stat(absolutePath)).mtimeMs : undefined;
|
|
154
|
-
// Check if aborted after reading.
|
|
155
|
-
if (aborted) {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
// Strip BOM before matching. The model will not include an invisible BOM in oldText.
|
|
159
|
-
const { bom, text: content } = stripBom(rawContent);
|
|
160
|
-
const originalEnding = detectLineEnding(content);
|
|
161
|
-
const normalizedContent = normalizeToLF(content);
|
|
162
|
-
const { baseContent, newContent, usedFuzzyMatch } = applyEditsToNormalizedContent(normalizedContent, edits, path);
|
|
163
|
-
// Check if aborted before writing.
|
|
164
|
-
if (aborted) {
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
// Detect stale file: re-check freshness token before writing.
|
|
168
|
-
if (freshnessTokenBefore !== undefined && ops.stat) {
|
|
169
|
-
const freshnessTokenAfter = (await ops.stat(absolutePath)).mtimeMs;
|
|
170
|
-
if (freshnessTokenAfter !== freshnessTokenBefore) {
|
|
171
|
-
if (signal) {
|
|
172
|
-
signal.removeEventListener("abort", onAbort);
|
|
173
|
-
}
|
|
174
|
-
reject(new Error(`File ${path} was modified since read. Re-read the file and retry your edit.`));
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
const finalContent = bom + restoreLineEndings(newContent, originalEnding);
|
|
179
|
-
await atomicWriteFile(absolutePath, finalContent);
|
|
180
|
-
// Check if aborted after writing.
|
|
181
|
-
if (aborted) {
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
// Clean up abort handler.
|
|
185
|
-
if (signal) {
|
|
186
|
-
signal.removeEventListener("abort", onAbort);
|
|
187
|
-
}
|
|
188
|
-
const diffResult = generateDiffString(baseContent, newContent);
|
|
189
|
-
const patch = generateUnifiedPatch(path, baseContent, newContent);
|
|
190
|
-
resolve({
|
|
191
|
-
content: [
|
|
192
|
-
{
|
|
193
|
-
type: "text",
|
|
194
|
-
text: `Successfully replaced ${edits.length} block(s) in ${path}.${usedFuzzyMatch ? " (used tolerant matching — verify the change)" : ""}`,
|
|
195
|
-
},
|
|
196
|
-
],
|
|
197
|
-
details: { diff: diffResult.diff, patch, firstChangedLine: diffResult.firstChangedLine },
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
catch (error) {
|
|
201
|
-
// Clean up abort handler.
|
|
202
|
-
if (signal) {
|
|
203
|
-
signal.removeEventListener("abort", onAbort);
|
|
204
|
-
}
|
|
205
|
-
if (!aborted) {
|
|
206
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
})();
|
|
210
|
-
}));
|
|
211
|
-
},
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
export function createEditTool(cwd, options) {
|
|
215
|
-
return wrapToolDefinition(createEditToolDefinition(cwd, options));
|
|
216
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { AgentTool } from "../../../agent-core/index.js";
|
|
2
|
-
import { type Static, Type } from "typebox";
|
|
3
|
-
import type { ToolDefinition } from "../extensions/types.js";
|
|
4
|
-
import { type TruncationResult } from "./truncate.js";
|
|
5
|
-
declare const grepSchema: Type.TObject<{
|
|
6
|
-
pattern: Type.TString;
|
|
7
|
-
path: Type.TOptional<Type.TString>;
|
|
8
|
-
glob: Type.TOptional<Type.TString>;
|
|
9
|
-
ignoreCase: Type.TOptional<Type.TBoolean>;
|
|
10
|
-
literal: Type.TOptional<Type.TBoolean>;
|
|
11
|
-
context: Type.TOptional<Type.TNumber>;
|
|
12
|
-
limit: Type.TOptional<Type.TNumber>;
|
|
13
|
-
}>;
|
|
14
|
-
export type GrepToolInput = Static<typeof grepSchema>;
|
|
15
|
-
export interface GrepToolDetails {
|
|
16
|
-
truncation?: TruncationResult;
|
|
17
|
-
matchLimitReached?: number;
|
|
18
|
-
linesTruncated?: boolean;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Pluggable operations for the grep tool.
|
|
22
|
-
* Override these to delegate search to remote systems (for example SSH).
|
|
23
|
-
*/
|
|
24
|
-
export interface GrepOperations {
|
|
25
|
-
/** Check if path is a directory. Throws if path does not exist. */
|
|
26
|
-
isDirectory: (absolutePath: string) => Promise<boolean> | boolean;
|
|
27
|
-
/** Read file contents for context lines */
|
|
28
|
-
readFile: (absolutePath: string) => Promise<string> | string;
|
|
29
|
-
}
|
|
30
|
-
export interface GrepToolOptions {
|
|
31
|
-
/** Custom operations for grep. Default: local filesystem plus ripgrep */
|
|
32
|
-
operations?: GrepOperations;
|
|
33
|
-
}
|
|
34
|
-
export declare function createGrepToolDefinition(cwd: string, options?: GrepToolOptions): ToolDefinition<typeof grepSchema, GrepToolDetails | undefined>;
|
|
35
|
-
export declare function createGrepTool(cwd: string, options?: GrepToolOptions): AgentTool<typeof grepSchema>;
|
|
36
|
-
export {};
|
|
37
|
-
//# sourceMappingURL=grep.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"grep.d.ts","sourceRoot":"","sources":["../../../../../src/sdk/coding-agent/core/tools/grep.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAI9D,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAG5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAI7D,OAAO,EAIN,KAAK,gBAAgB,EAGrB,MAAM,eAAe,CAAC;AAEvB,QAAA,MAAM,UAAU;;;;;;;;EAYd,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAGtD,MAAM,WAAW,eAAe;IAC/B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B,mEAAmE;IACnE,WAAW,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IAClE,2CAA2C;IAC3C,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;CAC7D;AAOD,MAAM,WAAW,eAAe;IAC/B,yEAAyE;IACzE,UAAU,CAAC,EAAE,cAAc,CAAC;CAC5B;AAqDD,wBAAgB,wBAAwB,CACvC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,eAAe,GACvB,cAAc,CAAC,OAAO,UAAU,EAAE,eAAe,GAAG,SAAS,CAAC,CAqPhE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAEnG"}
|
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
import { createInterface } from "node:readline";
|
|
2
|
-
import { spawn } from "child_process";
|
|
3
|
-
import { readFileSync, statSync } from "fs";
|
|
4
|
-
import path from "path";
|
|
5
|
-
import { Type } from "typebox";
|
|
6
|
-
import { ensureTool } from "../../utils/tools-manager.js";
|
|
7
|
-
import { resolveToCwd } from "./path-utils.js";
|
|
8
|
-
import { getTextOutput, shortenPath, str } from "./render-utils.js";
|
|
9
|
-
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
|
10
|
-
import { DEFAULT_MAX_BYTES, formatSize, GREP_MAX_LINE_LENGTH, truncateHead, truncateLine, } from "./truncate.js";
|
|
11
|
-
const grepSchema = Type.Object({
|
|
12
|
-
pattern: Type.String({ description: "Search pattern (regex or literal string)" }),
|
|
13
|
-
path: Type.Optional(Type.String({ description: "Directory or file to search (default: current directory)" })),
|
|
14
|
-
glob: Type.Optional(Type.String({ description: "Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'" })),
|
|
15
|
-
ignoreCase: Type.Optional(Type.Boolean({ description: "Case-insensitive search (default: false)" })),
|
|
16
|
-
literal: Type.Optional(Type.Boolean({ description: "Treat pattern as literal string instead of regex (default: false)" })),
|
|
17
|
-
context: Type.Optional(Type.Number({ description: "Number of lines to show before and after each match (default: 0)" })),
|
|
18
|
-
limit: Type.Optional(Type.Number({ description: "Maximum number of matches to return (default: 100)" })),
|
|
19
|
-
});
|
|
20
|
-
const DEFAULT_LIMIT = 100;
|
|
21
|
-
const defaultGrepOperations = {
|
|
22
|
-
isDirectory: (p) => statSync(p).isDirectory(),
|
|
23
|
-
readFile: (p) => readFileSync(p, "utf-8"),
|
|
24
|
-
};
|
|
25
|
-
function formatGrepCall(args, _theme) {
|
|
26
|
-
const pattern = str(args?.pattern);
|
|
27
|
-
const rawPath = str(args?.path);
|
|
28
|
-
const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
|
|
29
|
-
const glob = str(args?.glob);
|
|
30
|
-
const limit = args?.limit;
|
|
31
|
-
const invalidArg = "[invalid]";
|
|
32
|
-
let text = `grep ${pattern === null ? invalidArg : `/${pattern || ""}/`} in ${path === null ? invalidArg : path}`;
|
|
33
|
-
if (glob)
|
|
34
|
-
text += ` (${glob})`;
|
|
35
|
-
if (limit !== undefined)
|
|
36
|
-
text += ` limit ${limit}`;
|
|
37
|
-
return text;
|
|
38
|
-
}
|
|
39
|
-
function formatGrepResult(result, options, _theme, showImages) {
|
|
40
|
-
const output = getTextOutput(result, showImages).trim();
|
|
41
|
-
let text = "";
|
|
42
|
-
if (output) {
|
|
43
|
-
const lines = output.split("\n");
|
|
44
|
-
const maxLines = options.expanded ? lines.length : 15;
|
|
45
|
-
const displayLines = lines.slice(0, maxLines);
|
|
46
|
-
const remaining = lines.length - maxLines;
|
|
47
|
-
text += `\n${displayLines.join("\n")}`;
|
|
48
|
-
if (remaining > 0) {
|
|
49
|
-
text += `\n... (${remaining} more lines)`;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
const matchLimit = result.details?.matchLimitReached;
|
|
53
|
-
const truncation = result.details?.truncation;
|
|
54
|
-
const linesTruncated = result.details?.linesTruncated;
|
|
55
|
-
if (matchLimit || truncation?.truncated || linesTruncated) {
|
|
56
|
-
const warnings = [];
|
|
57
|
-
if (matchLimit)
|
|
58
|
-
warnings.push(`${matchLimit} matches limit`);
|
|
59
|
-
if (truncation?.truncated)
|
|
60
|
-
warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);
|
|
61
|
-
if (linesTruncated)
|
|
62
|
-
warnings.push("some lines truncated");
|
|
63
|
-
text += `\n[Truncated: ${warnings.join(", ")}]`;
|
|
64
|
-
}
|
|
65
|
-
return text;
|
|
66
|
-
}
|
|
67
|
-
export function createGrepToolDefinition(cwd, options) {
|
|
68
|
-
const customOps = options?.operations;
|
|
69
|
-
return {
|
|
70
|
-
name: "grep",
|
|
71
|
-
label: "grep",
|
|
72
|
-
description: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} matches or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`,
|
|
73
|
-
promptSnippet: "Search file contents for patterns (respects .gitignore)",
|
|
74
|
-
parameters: grepSchema,
|
|
75
|
-
async execute(_toolCallId, { pattern, path: searchDir, glob, ignoreCase, literal, context, limit, }, signal, _onUpdate, _ctx) {
|
|
76
|
-
return new Promise((resolve, reject) => {
|
|
77
|
-
if (signal?.aborted) {
|
|
78
|
-
reject(new Error("Operation aborted"));
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
let settled = false;
|
|
82
|
-
const settle = (fn) => {
|
|
83
|
-
if (!settled) {
|
|
84
|
-
settled = true;
|
|
85
|
-
fn();
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
(async () => {
|
|
89
|
-
try {
|
|
90
|
-
const rgPath = await ensureTool("rg", true);
|
|
91
|
-
if (!rgPath) {
|
|
92
|
-
settle(() => reject(new Error("ripgrep (rg) is not available and could not be downloaded")));
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
const searchPath = resolveToCwd(searchDir || ".", cwd);
|
|
96
|
-
const ops = customOps ?? defaultGrepOperations;
|
|
97
|
-
let isDirectory;
|
|
98
|
-
try {
|
|
99
|
-
isDirectory = await ops.isDirectory(searchPath);
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
settle(() => reject(new Error(`Path not found: ${searchPath}`)));
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
const contextValue = context && context > 0 ? context : 0;
|
|
106
|
-
const effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT);
|
|
107
|
-
const formatPath = (filePath) => {
|
|
108
|
-
if (isDirectory) {
|
|
109
|
-
const relative = path.relative(searchPath, filePath);
|
|
110
|
-
if (relative && !relative.startsWith("..")) {
|
|
111
|
-
return relative.replace(/\\/g, "/");
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return path.basename(filePath);
|
|
115
|
-
};
|
|
116
|
-
const fileCache = new Map();
|
|
117
|
-
const getFileLines = async (filePath) => {
|
|
118
|
-
let lines = fileCache.get(filePath);
|
|
119
|
-
if (!lines) {
|
|
120
|
-
try {
|
|
121
|
-
const content = await ops.readFile(filePath);
|
|
122
|
-
lines = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
lines = [];
|
|
126
|
-
}
|
|
127
|
-
fileCache.set(filePath, lines);
|
|
128
|
-
}
|
|
129
|
-
return lines;
|
|
130
|
-
};
|
|
131
|
-
const args = ["--json", "--line-number", "--color=never", "--hidden"];
|
|
132
|
-
if (ignoreCase)
|
|
133
|
-
args.push("--ignore-case");
|
|
134
|
-
if (literal)
|
|
135
|
-
args.push("--fixed-strings");
|
|
136
|
-
if (glob)
|
|
137
|
-
args.push("--glob", glob);
|
|
138
|
-
args.push("--", pattern, searchPath);
|
|
139
|
-
const child = spawn(rgPath, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
140
|
-
const rl = createInterface({ input: child.stdout });
|
|
141
|
-
let stderr = "";
|
|
142
|
-
let matchCount = 0;
|
|
143
|
-
let matchLimitReached = false;
|
|
144
|
-
let linesTruncated = false;
|
|
145
|
-
let aborted = false;
|
|
146
|
-
let killedDueToLimit = false;
|
|
147
|
-
const outputLines = [];
|
|
148
|
-
const cleanup = () => {
|
|
149
|
-
rl.close();
|
|
150
|
-
signal?.removeEventListener("abort", onAbort);
|
|
151
|
-
};
|
|
152
|
-
const stopChild = (dueToLimit = false) => {
|
|
153
|
-
if (!child.killed) {
|
|
154
|
-
killedDueToLimit = dueToLimit;
|
|
155
|
-
child.kill();
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
const onAbort = () => {
|
|
159
|
-
aborted = true;
|
|
160
|
-
stopChild();
|
|
161
|
-
};
|
|
162
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
163
|
-
child.stderr?.on("data", (chunk) => {
|
|
164
|
-
stderr += chunk.toString();
|
|
165
|
-
});
|
|
166
|
-
const formatBlock = async (filePath, lineNumber) => {
|
|
167
|
-
const relativePath = formatPath(filePath);
|
|
168
|
-
const lines = await getFileLines(filePath);
|
|
169
|
-
if (!lines.length)
|
|
170
|
-
return [`${relativePath}:${lineNumber}: (unable to read file)`];
|
|
171
|
-
const block = [];
|
|
172
|
-
const start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber;
|
|
173
|
-
const end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber;
|
|
174
|
-
for (let current = start; current <= end; current++) {
|
|
175
|
-
const lineText = lines[current - 1] ?? "";
|
|
176
|
-
const sanitized = lineText.replace(/\r/g, "");
|
|
177
|
-
const isMatchLine = current === lineNumber;
|
|
178
|
-
// Truncate long lines so grep output stays compact.
|
|
179
|
-
const { text: truncatedText, wasTruncated } = truncateLine(sanitized);
|
|
180
|
-
if (wasTruncated)
|
|
181
|
-
linesTruncated = true;
|
|
182
|
-
if (isMatchLine)
|
|
183
|
-
block.push(`${relativePath}:${current}: ${truncatedText}`);
|
|
184
|
-
else
|
|
185
|
-
block.push(`${relativePath}-${current}- ${truncatedText}`);
|
|
186
|
-
}
|
|
187
|
-
return block;
|
|
188
|
-
};
|
|
189
|
-
// Collect matches during streaming, then format them after rg exits.
|
|
190
|
-
const matches = [];
|
|
191
|
-
rl.on("line", (line) => {
|
|
192
|
-
if (!line.trim() || matchCount >= effectiveLimit)
|
|
193
|
-
return;
|
|
194
|
-
let event;
|
|
195
|
-
try {
|
|
196
|
-
event = JSON.parse(line);
|
|
197
|
-
}
|
|
198
|
-
catch {
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
if (event.type === "match") {
|
|
202
|
-
matchCount++;
|
|
203
|
-
const filePath = event.data?.path?.text;
|
|
204
|
-
const lineNumber = event.data?.line_number;
|
|
205
|
-
const lineText = event.data?.lines?.text;
|
|
206
|
-
if (filePath && typeof lineNumber === "number")
|
|
207
|
-
matches.push({ filePath, lineNumber, lineText });
|
|
208
|
-
if (matchCount >= effectiveLimit) {
|
|
209
|
-
matchLimitReached = true;
|
|
210
|
-
stopChild(true);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
child.on("error", (error) => {
|
|
215
|
-
cleanup();
|
|
216
|
-
settle(() => reject(new Error(`Failed to run ripgrep: ${error.message}`)));
|
|
217
|
-
});
|
|
218
|
-
child.on("close", async (code) => {
|
|
219
|
-
cleanup();
|
|
220
|
-
if (aborted) {
|
|
221
|
-
settle(() => reject(new Error("Operation aborted")));
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
if (!killedDueToLimit && code !== 0 && code !== 1) {
|
|
225
|
-
const errorMsg = stderr.trim() || `ripgrep exited with code ${code}`;
|
|
226
|
-
settle(() => reject(new Error(errorMsg)));
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
if (matchCount === 0) {
|
|
230
|
-
settle(() => resolve({ content: [{ type: "text", text: "No matches found" }], details: undefined }));
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
// Format matches after streaming finishes so custom readFile() backends can be async.
|
|
234
|
-
for (const match of matches) {
|
|
235
|
-
if (contextValue === 0 && match.lineText !== undefined) {
|
|
236
|
-
const relativePath = formatPath(match.filePath);
|
|
237
|
-
const sanitized = match.lineText
|
|
238
|
-
.replace(/\r\n/g, "\n")
|
|
239
|
-
.replace(/\r/g, "")
|
|
240
|
-
.replace(/\n$/, "");
|
|
241
|
-
const { text: truncatedText, wasTruncated } = truncateLine(sanitized);
|
|
242
|
-
if (wasTruncated)
|
|
243
|
-
linesTruncated = true;
|
|
244
|
-
outputLines.push(`${relativePath}:${match.lineNumber}: ${truncatedText}`);
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
const block = await formatBlock(match.filePath, match.lineNumber);
|
|
248
|
-
outputLines.push(...block);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
const rawOutput = outputLines.join("\n");
|
|
252
|
-
// Apply byte truncation. There is no line limit here because the match limit already capped rows.
|
|
253
|
-
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
254
|
-
let output = truncation.content;
|
|
255
|
-
const details = {};
|
|
256
|
-
// Build actionable notices for truncation and match limits.
|
|
257
|
-
const notices = [];
|
|
258
|
-
if (matchLimitReached) {
|
|
259
|
-
notices.push(`${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`);
|
|
260
|
-
details.matchLimitReached = effectiveLimit;
|
|
261
|
-
}
|
|
262
|
-
if (truncation.truncated) {
|
|
263
|
-
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
264
|
-
details.truncation = truncation;
|
|
265
|
-
}
|
|
266
|
-
if (linesTruncated) {
|
|
267
|
-
notices.push(`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`);
|
|
268
|
-
details.linesTruncated = true;
|
|
269
|
-
}
|
|
270
|
-
if (notices.length > 0)
|
|
271
|
-
output += `\n\n[${notices.join(". ")}]`;
|
|
272
|
-
settle(() => resolve({
|
|
273
|
-
content: [{ type: "text", text: output }],
|
|
274
|
-
details: Object.keys(details).length > 0 ? details : undefined,
|
|
275
|
-
}));
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
catch (err) {
|
|
279
|
-
settle(() => reject(err));
|
|
280
|
-
}
|
|
281
|
-
})();
|
|
282
|
-
});
|
|
283
|
-
},
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
export function createGrepTool(cwd, options) {
|
|
287
|
-
return wrapToolDefinition(createGrepToolDefinition(cwd, options));
|
|
288
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { AgentTool } from "../../../agent-core/index.js";
|
|
2
|
-
import { type Static, Type } from "typebox";
|
|
3
|
-
import type { ToolDefinition } from "../extensions/types.js";
|
|
4
|
-
import { type TruncationResult } from "./truncate.js";
|
|
5
|
-
declare const lsSchema: Type.TObject<{
|
|
6
|
-
path: Type.TOptional<Type.TString>;
|
|
7
|
-
limit: Type.TOptional<Type.TNumber>;
|
|
8
|
-
}>;
|
|
9
|
-
export type LsToolInput = Static<typeof lsSchema>;
|
|
10
|
-
export interface LsToolDetails {
|
|
11
|
-
truncation?: TruncationResult;
|
|
12
|
-
entryLimitReached?: number;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Pluggable operations for the ls tool.
|
|
16
|
-
* Override these to delegate directory listing to remote systems (for example SSH).
|
|
17
|
-
*/
|
|
18
|
-
export interface LsOperations {
|
|
19
|
-
/** Check if path exists */
|
|
20
|
-
exists: (absolutePath: string) => Promise<boolean> | boolean;
|
|
21
|
-
/** Get file or directory stats. Throws if not found. */
|
|
22
|
-
stat: (absolutePath: string) => Promise<{
|
|
23
|
-
isDirectory: () => boolean;
|
|
24
|
-
}> | {
|
|
25
|
-
isDirectory: () => boolean;
|
|
26
|
-
};
|
|
27
|
-
/** Read directory entries */
|
|
28
|
-
readdir: (absolutePath: string) => Promise<string[]> | string[];
|
|
29
|
-
}
|
|
30
|
-
export interface LsToolOptions {
|
|
31
|
-
/** Custom operations for directory listing. Default: local filesystem */
|
|
32
|
-
operations?: LsOperations;
|
|
33
|
-
}
|
|
34
|
-
export declare function createLsToolDefinition(cwd: string, options?: LsToolOptions): ToolDefinition<typeof lsSchema, LsToolDetails | undefined>;
|
|
35
|
-
export declare function createLsTool(cwd: string, options?: LsToolOptions): AgentTool<typeof lsSchema>;
|
|
36
|
-
export {};
|
|
37
|
-
//# sourceMappingURL=ls.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ls.d.ts","sourceRoot":"","sources":["../../../../../src/sdk/coding-agent/core/tools/ls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAG9D,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAI7D,OAAO,EAAiC,KAAK,gBAAgB,EAAgB,MAAM,eAAe,CAAC;AAEnG,QAAA,MAAM,QAAQ;;;EAGZ,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,QAAQ,CAAC,CAAC;AAIlD,MAAM,WAAW,aAAa;IAC7B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC5B,2BAA2B;IAC3B,MAAM,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IAC7D,wDAAwD;IACxD,IAAI,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,WAAW,EAAE,MAAM,OAAO,CAAA;KAAE,CAAC,GAAG;QAAE,WAAW,EAAE,MAAM,OAAO,CAAA;KAAE,CAAC;IACzG,6BAA6B;IAC7B,OAAO,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC;CAChE;AAQD,MAAM,WAAW,aAAa;IAC7B,yEAAyE;IACzE,UAAU,CAAC,EAAE,YAAY,CAAC;CAC1B;AAkDD,wBAAgB,sBAAsB,CACrC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,aAAa,GACrB,cAAc,CAAC,OAAO,QAAQ,EAAE,aAAa,GAAG,SAAS,CAAC,CAiH5D;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC,OAAO,QAAQ,CAAC,CAE7F"}
|