@mariozechner/pi-coding-agent 0.6.2
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/README.md +485 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +21 -0
- package/dist/cli.js.map +1 -0
- package/dist/export-html.d.ts +7 -0
- package/dist/export-html.d.ts.map +1 -0
- package/dist/export-html.js +650 -0
- package/dist/export-html.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +514 -0
- package/dist/main.js.map +1 -0
- package/dist/session-manager.d.ts +70 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/session-manager.js +323 -0
- package/dist/session-manager.js.map +1 -0
- package/dist/tools/bash.d.ts +7 -0
- package/dist/tools/bash.d.ts.map +1 -0
- package/dist/tools/bash.js +130 -0
- package/dist/tools/bash.js.map +1 -0
- package/dist/tools/edit.d.ts +9 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +207 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/index.d.ts +19 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +10 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/read.d.ts +9 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +165 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/write.d.ts +8 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +81 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/tui/assistant-message.d.ts +11 -0
- package/dist/tui/assistant-message.d.ts.map +1 -0
- package/dist/tui/assistant-message.js +53 -0
- package/dist/tui/assistant-message.js.map +1 -0
- package/dist/tui/custom-editor.d.ts +10 -0
- package/dist/tui/custom-editor.d.ts.map +1 -0
- package/dist/tui/custom-editor.js +24 -0
- package/dist/tui/custom-editor.js.map +1 -0
- package/dist/tui/footer.d.ts +11 -0
- package/dist/tui/footer.d.ts.map +1 -0
- package/dist/tui/footer.js +101 -0
- package/dist/tui/footer.js.map +1 -0
- package/dist/tui/model-selector.d.ts +23 -0
- package/dist/tui/model-selector.d.ts.map +1 -0
- package/dist/tui/model-selector.js +157 -0
- package/dist/tui/model-selector.js.map +1 -0
- package/dist/tui/session-selector.d.ts +37 -0
- package/dist/tui/session-selector.d.ts.map +1 -0
- package/dist/tui/session-selector.js +176 -0
- package/dist/tui/session-selector.js.map +1 -0
- package/dist/tui/thinking-selector.d.ts +11 -0
- package/dist/tui/thinking-selector.d.ts.map +1 -0
- package/dist/tui/thinking-selector.js +48 -0
- package/dist/tui/thinking-selector.js.map +1 -0
- package/dist/tui/tool-execution.d.ts +26 -0
- package/dist/tui/tool-execution.d.ts.map +1 -0
- package/dist/tui/tool-execution.js +246 -0
- package/dist/tui/tool-execution.js.map +1 -0
- package/dist/tui/tui-renderer.d.ts +44 -0
- package/dist/tui/tui-renderer.d.ts.map +1 -0
- package/dist/tui/tui-renderer.js +539 -0
- package/dist/tui/tui-renderer.js.map +1 -0
- package/dist/tui/user-message.d.ts +9 -0
- package/dist/tui/user-message.d.ts.map +1 -0
- package/dist/tui/user-message.js +18 -0
- package/dist/tui/user-message.js.map +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import * as Diff from "diff";
|
|
4
|
+
import { constants } from "fs";
|
|
5
|
+
import { access, readFile, writeFile } from "fs/promises";
|
|
6
|
+
import { resolve as resolvePath } from "path";
|
|
7
|
+
/**
|
|
8
|
+
* Expand ~ to home directory
|
|
9
|
+
*/
|
|
10
|
+
function expandPath(filePath) {
|
|
11
|
+
if (filePath === "~") {
|
|
12
|
+
return os.homedir();
|
|
13
|
+
}
|
|
14
|
+
if (filePath.startsWith("~/")) {
|
|
15
|
+
return os.homedir() + filePath.slice(1);
|
|
16
|
+
}
|
|
17
|
+
return filePath;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Generate a unified diff string with line numbers and context
|
|
21
|
+
*/
|
|
22
|
+
function generateDiffString(oldContent, newContent, contextLines = 4) {
|
|
23
|
+
const parts = Diff.diffLines(oldContent, newContent);
|
|
24
|
+
const output = [];
|
|
25
|
+
const oldLines = oldContent.split("\n");
|
|
26
|
+
const newLines = newContent.split("\n");
|
|
27
|
+
const maxLineNum = Math.max(oldLines.length, newLines.length);
|
|
28
|
+
const lineNumWidth = String(maxLineNum).length;
|
|
29
|
+
let oldLineNum = 1;
|
|
30
|
+
let newLineNum = 1;
|
|
31
|
+
let lastWasChange = false;
|
|
32
|
+
for (let i = 0; i < parts.length; i++) {
|
|
33
|
+
const part = parts[i];
|
|
34
|
+
const raw = part.value.split("\n");
|
|
35
|
+
if (raw[raw.length - 1] === "") {
|
|
36
|
+
raw.pop();
|
|
37
|
+
}
|
|
38
|
+
if (part.added || part.removed) {
|
|
39
|
+
// Show the change
|
|
40
|
+
for (const line of raw) {
|
|
41
|
+
if (part.added) {
|
|
42
|
+
const lineNum = String(newLineNum).padStart(lineNumWidth, " ");
|
|
43
|
+
output.push(`+${lineNum} ${line}`);
|
|
44
|
+
newLineNum++;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// removed
|
|
48
|
+
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
|
49
|
+
output.push(`-${lineNum} ${line}`);
|
|
50
|
+
oldLineNum++;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
lastWasChange = true;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
// Context lines - only show a few before/after changes
|
|
57
|
+
const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);
|
|
58
|
+
if (lastWasChange || nextPartIsChange) {
|
|
59
|
+
// Show context
|
|
60
|
+
let linesToShow = raw;
|
|
61
|
+
let skipStart = 0;
|
|
62
|
+
let skipEnd = 0;
|
|
63
|
+
if (!lastWasChange) {
|
|
64
|
+
// Show only last N lines as leading context
|
|
65
|
+
skipStart = Math.max(0, raw.length - contextLines);
|
|
66
|
+
linesToShow = raw.slice(skipStart);
|
|
67
|
+
}
|
|
68
|
+
if (!nextPartIsChange && linesToShow.length > contextLines) {
|
|
69
|
+
// Show only first N lines as trailing context
|
|
70
|
+
skipEnd = linesToShow.length - contextLines;
|
|
71
|
+
linesToShow = linesToShow.slice(0, contextLines);
|
|
72
|
+
}
|
|
73
|
+
// Add ellipsis if we skipped lines at start
|
|
74
|
+
if (skipStart > 0) {
|
|
75
|
+
output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
|
|
76
|
+
}
|
|
77
|
+
for (const line of linesToShow) {
|
|
78
|
+
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
|
79
|
+
output.push(` ${lineNum} ${line}`);
|
|
80
|
+
oldLineNum++;
|
|
81
|
+
newLineNum++;
|
|
82
|
+
}
|
|
83
|
+
// Add ellipsis if we skipped lines at end
|
|
84
|
+
if (skipEnd > 0) {
|
|
85
|
+
output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
|
|
86
|
+
}
|
|
87
|
+
// Update line numbers for skipped lines
|
|
88
|
+
oldLineNum += skipStart + skipEnd;
|
|
89
|
+
newLineNum += skipStart + skipEnd;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// Skip these context lines entirely
|
|
93
|
+
oldLineNum += raw.length;
|
|
94
|
+
newLineNum += raw.length;
|
|
95
|
+
}
|
|
96
|
+
lastWasChange = false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return output.join("\n");
|
|
100
|
+
}
|
|
101
|
+
const editSchema = Type.Object({
|
|
102
|
+
path: Type.String({ description: "Path to the file to edit (relative or absolute)" }),
|
|
103
|
+
oldText: Type.String({ description: "Exact text to find and replace (must match exactly)" }),
|
|
104
|
+
newText: Type.String({ description: "New text to replace the old text with" }),
|
|
105
|
+
});
|
|
106
|
+
export const editTool = {
|
|
107
|
+
name: "edit",
|
|
108
|
+
label: "edit",
|
|
109
|
+
description: "Edit a file by replacing exact text. The oldText must match exactly (including whitespace). Use this for precise, surgical edits.",
|
|
110
|
+
parameters: editSchema,
|
|
111
|
+
execute: async (_toolCallId, { path, oldText, newText }, signal) => {
|
|
112
|
+
const absolutePath = resolvePath(expandPath(path));
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
// Check if already aborted
|
|
115
|
+
if (signal?.aborted) {
|
|
116
|
+
reject(new Error("Operation aborted"));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
let aborted = false;
|
|
120
|
+
// Set up abort handler
|
|
121
|
+
const onAbort = () => {
|
|
122
|
+
aborted = true;
|
|
123
|
+
reject(new Error("Operation aborted"));
|
|
124
|
+
};
|
|
125
|
+
if (signal) {
|
|
126
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
127
|
+
}
|
|
128
|
+
// Perform the edit operation
|
|
129
|
+
(async () => {
|
|
130
|
+
try {
|
|
131
|
+
// Check if file exists
|
|
132
|
+
try {
|
|
133
|
+
await access(absolutePath, constants.R_OK | constants.W_OK);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
if (signal) {
|
|
137
|
+
signal.removeEventListener("abort", onAbort);
|
|
138
|
+
}
|
|
139
|
+
reject(new Error(`File not found: ${path}`));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Check if aborted before reading
|
|
143
|
+
if (aborted) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// Read the file
|
|
147
|
+
const content = await readFile(absolutePath, "utf-8");
|
|
148
|
+
// Check if aborted after reading
|
|
149
|
+
if (aborted) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// Check if old text exists
|
|
153
|
+
if (!content.includes(oldText)) {
|
|
154
|
+
if (signal) {
|
|
155
|
+
signal.removeEventListener("abort", onAbort);
|
|
156
|
+
}
|
|
157
|
+
reject(new Error(`Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// Count occurrences
|
|
161
|
+
const occurrences = content.split(oldText).length - 1;
|
|
162
|
+
if (occurrences > 1) {
|
|
163
|
+
if (signal) {
|
|
164
|
+
signal.removeEventListener("abort", onAbort);
|
|
165
|
+
}
|
|
166
|
+
reject(new Error(`Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// Check if aborted before writing
|
|
170
|
+
if (aborted) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// Perform replacement
|
|
174
|
+
const newContent = content.replace(oldText, newText);
|
|
175
|
+
await writeFile(absolutePath, newContent, "utf-8");
|
|
176
|
+
// Check if aborted after writing
|
|
177
|
+
if (aborted) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// Clean up abort handler
|
|
181
|
+
if (signal) {
|
|
182
|
+
signal.removeEventListener("abort", onAbort);
|
|
183
|
+
}
|
|
184
|
+
resolve({
|
|
185
|
+
content: [
|
|
186
|
+
{
|
|
187
|
+
type: "text",
|
|
188
|
+
text: `Successfully replaced text in ${path}. Changed ${oldText.length} characters to ${newText.length} characters.`,
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
details: { diff: generateDiffString(content, newContent) },
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
// Clean up abort handler
|
|
196
|
+
if (signal) {
|
|
197
|
+
signal.removeEventListener("abort", onAbort);
|
|
198
|
+
}
|
|
199
|
+
if (!aborted) {
|
|
200
|
+
reject(error);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
})();
|
|
204
|
+
});
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
//# sourceMappingURL=edit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edit.js","sourceRoot":"","sources":["../../src/tools/edit.ts"],"names":[],"mappings":"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","sourcesContent":["import * as os from \"node:os\";\nimport type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport * as Diff from \"diff\";\nimport { constants } from \"fs\";\nimport { access, readFile, writeFile } from \"fs/promises\";\nimport { resolve as resolvePath } from \"path\";\n\n/**\n * Expand ~ to home directory\n */\nfunction expandPath(filePath: string): string {\n\tif (filePath === \"~\") {\n\t\treturn os.homedir();\n\t}\n\tif (filePath.startsWith(\"~/\")) {\n\t\treturn os.homedir() + filePath.slice(1);\n\t}\n\treturn filePath;\n}\n\n/**\n * Generate a unified diff string with line numbers and context\n */\nfunction generateDiffString(oldContent: string, newContent: string, contextLines = 4): string {\n\tconst parts = Diff.diffLines(oldContent, newContent);\n\tconst output: string[] = [];\n\n\tconst oldLines = oldContent.split(\"\\n\");\n\tconst newLines = newContent.split(\"\\n\");\n\tconst maxLineNum = Math.max(oldLines.length, newLines.length);\n\tconst lineNumWidth = String(maxLineNum).length;\n\n\tlet oldLineNum = 1;\n\tlet newLineNum = 1;\n\tlet lastWasChange = false;\n\n\tfor (let i = 0; i < parts.length; i++) {\n\t\tconst part = parts[i];\n\t\tconst raw = part.value.split(\"\\n\");\n\t\tif (raw[raw.length - 1] === \"\") {\n\t\t\traw.pop();\n\t\t}\n\n\t\tif (part.added || part.removed) {\n\t\t\t// Show the change\n\t\t\tfor (const line of raw) {\n\t\t\t\tif (part.added) {\n\t\t\t\t\tconst lineNum = String(newLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(`+${lineNum} ${line}`);\n\t\t\t\t\tnewLineNum++;\n\t\t\t\t} else {\n\t\t\t\t\t// removed\n\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(`-${lineNum} ${line}`);\n\t\t\t\t\toldLineNum++;\n\t\t\t\t}\n\t\t\t}\n\t\t\tlastWasChange = true;\n\t\t} else {\n\t\t\t// Context lines - only show a few before/after changes\n\t\t\tconst nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);\n\n\t\t\tif (lastWasChange || nextPartIsChange) {\n\t\t\t\t// Show context\n\t\t\t\tlet linesToShow = raw;\n\t\t\t\tlet skipStart = 0;\n\t\t\t\tlet skipEnd = 0;\n\n\t\t\t\tif (!lastWasChange) {\n\t\t\t\t\t// Show only last N lines as leading context\n\t\t\t\t\tskipStart = Math.max(0, raw.length - contextLines);\n\t\t\t\t\tlinesToShow = raw.slice(skipStart);\n\t\t\t\t}\n\n\t\t\t\tif (!nextPartIsChange && linesToShow.length > contextLines) {\n\t\t\t\t\t// Show only first N lines as trailing context\n\t\t\t\t\tskipEnd = linesToShow.length - contextLines;\n\t\t\t\t\tlinesToShow = linesToShow.slice(0, contextLines);\n\t\t\t\t}\n\n\t\t\t\t// Add ellipsis if we skipped lines at start\n\t\t\t\tif (skipStart > 0) {\n\t\t\t\t\toutput.push(` ${\"\".padStart(lineNumWidth, \" \")} ...`);\n\t\t\t\t}\n\n\t\t\t\tfor (const line of linesToShow) {\n\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\toldLineNum++;\n\t\t\t\t\tnewLineNum++;\n\t\t\t\t}\n\n\t\t\t\t// Add ellipsis if we skipped lines at end\n\t\t\t\tif (skipEnd > 0) {\n\t\t\t\t\toutput.push(` ${\"\".padStart(lineNumWidth, \" \")} ...`);\n\t\t\t\t}\n\n\t\t\t\t// Update line numbers for skipped lines\n\t\t\t\toldLineNum += skipStart + skipEnd;\n\t\t\t\tnewLineNum += skipStart + skipEnd;\n\t\t\t} else {\n\t\t\t\t// Skip these context lines entirely\n\t\t\t\toldLineNum += raw.length;\n\t\t\t\tnewLineNum += raw.length;\n\t\t\t}\n\n\t\t\tlastWasChange = false;\n\t\t}\n\t}\n\n\treturn output.join(\"\\n\");\n}\n\nconst editSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to edit (relative or absolute)\" }),\n\toldText: Type.String({ description: \"Exact text to find and replace (must match exactly)\" }),\n\tnewText: Type.String({ description: \"New text to replace the old text with\" }),\n});\n\nexport const editTool: AgentTool<typeof editSchema> = {\n\tname: \"edit\",\n\tlabel: \"edit\",\n\tdescription:\n\t\t\"Edit a file by replacing exact text. The oldText must match exactly (including whitespace). Use this for precise, surgical edits.\",\n\tparameters: editSchema,\n\texecute: async (\n\t\t_toolCallId: string,\n\t\t{ path, oldText, newText }: { path: string; oldText: string; newText: string },\n\t\tsignal?: AbortSignal,\n\t) => {\n\t\tconst absolutePath = resolvePath(expandPath(path));\n\n\t\treturn new Promise<{\n\t\t\tcontent: Array<{ type: \"text\"; text: string }>;\n\t\t\tdetails: { diff: string } | undefined;\n\t\t}>((resolve, reject) => {\n\t\t\t// Check if already aborted\n\t\t\tif (signal?.aborted) {\n\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet aborted = false;\n\n\t\t\t// Set up abort handler\n\t\t\tconst onAbort = () => {\n\t\t\t\taborted = true;\n\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t};\n\n\t\t\tif (signal) {\n\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t}\n\n\t\t\t// Perform the edit operation\n\t\t\t(async () => {\n\t\t\t\ttry {\n\t\t\t\t\t// Check if file exists\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait access(absolutePath, constants.R_OK | constants.W_OK);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treject(new Error(`File not found: ${path}`));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Check if aborted before reading\n\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Read the file\n\t\t\t\t\tconst content = await readFile(absolutePath, \"utf-8\");\n\n\t\t\t\t\t// Check if aborted after reading\n\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Check if old text exists\n\t\t\t\t\tif (!content.includes(oldText)) {\n\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treject(\n\t\t\t\t\t\t\tnew Error(\n\t\t\t\t\t\t\t\t`Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Count occurrences\n\t\t\t\t\tconst occurrences = content.split(oldText).length - 1;\n\n\t\t\t\t\tif (occurrences > 1) {\n\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treject(\n\t\t\t\t\t\t\tnew Error(\n\t\t\t\t\t\t\t\t`Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Check if aborted before writing\n\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Perform replacement\n\t\t\t\t\tconst newContent = content.replace(oldText, newText);\n\t\t\t\t\tawait writeFile(absolutePath, newContent, \"utf-8\");\n\n\t\t\t\t\t// Check if aborted after writing\n\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\tif (signal) {\n\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t}\n\n\t\t\t\t\tresolve({\n\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\ttext: `Successfully replaced text in ${path}. Changed ${oldText.length} characters to ${newText.length} characters.`,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t\tdetails: { diff: generateDiffString(content, newContent) },\n\t\t\t\t\t});\n\t\t\t\t} catch (error: any) {\n\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\tif (signal) {\n\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (!aborted) {\n\t\t\t\t\t\treject(error);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})();\n\t\t});\n\t},\n};\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { bashTool } from "./bash.js";
|
|
2
|
+
export { editTool } from "./edit.js";
|
|
3
|
+
export { readTool } from "./read.js";
|
|
4
|
+
export { writeTool } from "./write.js";
|
|
5
|
+
export declare const codingTools: (import("../../../ai/dist/index.js").AgentTool<import("@sinclair/typebox").TObject<{
|
|
6
|
+
command: import("@sinclair/typebox").TString;
|
|
7
|
+
}>, any> | import("../../../ai/dist/index.js").AgentTool<import("@sinclair/typebox").TObject<{
|
|
8
|
+
path: import("@sinclair/typebox").TString;
|
|
9
|
+
oldText: import("@sinclair/typebox").TString;
|
|
10
|
+
newText: import("@sinclair/typebox").TString;
|
|
11
|
+
}>, any> | import("../../../ai/dist/index.js").AgentTool<import("@sinclair/typebox").TObject<{
|
|
12
|
+
path: import("@sinclair/typebox").TString;
|
|
13
|
+
offset: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
14
|
+
limit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
15
|
+
}>, any> | import("../../../ai/dist/index.js").AgentTool<import("@sinclair/typebox").TObject<{
|
|
16
|
+
path: import("@sinclair/typebox").TString;
|
|
17
|
+
content: import("@sinclair/typebox").TString;
|
|
18
|
+
}>, any>)[];
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAOvC,eAAO,MAAM,WAAW;;;;;;;;;;;;;WAA4C,CAAC","sourcesContent":["export { bashTool } from \"./bash.js\";\nexport { editTool } from \"./edit.js\";\nexport { readTool } from \"./read.js\";\nexport { writeTool } from \"./write.js\";\n\nimport { bashTool } from \"./bash.js\";\nimport { editTool } from \"./edit.js\";\nimport { readTool } from \"./read.js\";\nimport { writeTool } from \"./write.js\";\n\nexport const codingTools = [readTool, bashTool, editTool, writeTool];\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { bashTool } from "./bash.js";
|
|
2
|
+
export { editTool } from "./edit.js";
|
|
3
|
+
export { readTool } from "./read.js";
|
|
4
|
+
export { writeTool } from "./write.js";
|
|
5
|
+
import { bashTool } from "./bash.js";
|
|
6
|
+
import { editTool } from "./edit.js";
|
|
7
|
+
import { readTool } from "./read.js";
|
|
8
|
+
import { writeTool } from "./write.js";
|
|
9
|
+
export const codingTools = [readTool, bashTool, editTool, writeTool];
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC","sourcesContent":["export { bashTool } from \"./bash.js\";\nexport { editTool } from \"./edit.js\";\nexport { readTool } from \"./read.js\";\nexport { writeTool } from \"./write.js\";\n\nimport { bashTool } from \"./bash.js\";\nimport { editTool } from \"./edit.js\";\nimport { readTool } from \"./read.js\";\nimport { writeTool } from \"./write.js\";\n\nexport const codingTools = [readTool, bashTool, editTool, writeTool];\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AgentTool } from "@mariozechner/pi-ai";
|
|
2
|
+
declare const readSchema: import("@sinclair/typebox").TObject<{
|
|
3
|
+
path: import("@sinclair/typebox").TString;
|
|
4
|
+
offset: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
5
|
+
limit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
6
|
+
}>;
|
|
7
|
+
export declare const readTool: AgentTool<typeof readSchema>;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=read.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAA6B,MAAM,qBAAqB,CAAC;AAsChF,QAAA,MAAM,UAAU;;;;EAId,CAAC;AAKH,eAAO,MAAM,QAAQ,EAAE,SAAS,CAAC,OAAO,UAAU,CAmJjD,CAAC","sourcesContent":["import * as os from \"node:os\";\nimport type { AgentTool, ImageContent, TextContent } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { constants } from \"fs\";\nimport { access, readFile } from \"fs/promises\";\nimport { extname, resolve as resolvePath } from \"path\";\n\n/**\n * Expand ~ to home directory\n */\nfunction expandPath(filePath: string): string {\n\tif (filePath === \"~\") {\n\t\treturn os.homedir();\n\t}\n\tif (filePath.startsWith(\"~/\")) {\n\t\treturn os.homedir() + filePath.slice(1);\n\t}\n\treturn filePath;\n}\n\n/**\n * Map of file extensions to MIME types for common image formats\n */\nconst IMAGE_MIME_TYPES: Record<string, string> = {\n\t\".jpg\": \"image/jpeg\",\n\t\".jpeg\": \"image/jpeg\",\n\t\".png\": \"image/png\",\n\t\".gif\": \"image/gif\",\n\t\".webp\": \"image/webp\",\n};\n\n/**\n * Check if a file is an image based on its extension\n */\nfunction isImageFile(filePath: string): string | null {\n\tconst ext = extname(filePath).toLowerCase();\n\treturn IMAGE_MIME_TYPES[ext] || null;\n}\n\nconst readSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\nconst MAX_LINES = 2000;\nconst MAX_LINE_LENGTH = 2000;\n\nexport const readTool: AgentTool<typeof readSchema> = {\n\tname: \"read\",\n\tlabel: \"read\",\n\tdescription:\n\t\t\"Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, defaults to first 2000 lines. Use offset/limit for large files.\",\n\tparameters: readSchema,\n\texecute: async (\n\t\t_toolCallId: string,\n\t\t{ path, offset, limit }: { path: string; offset?: number; limit?: number },\n\t\tsignal?: AbortSignal,\n\t) => {\n\t\tconst absolutePath = resolvePath(expandPath(path));\n\t\tconst mimeType = isImageFile(absolutePath);\n\n\t\treturn new Promise<{ content: (TextContent | ImageContent)[]; details: undefined }>((resolve, reject) => {\n\t\t\t// Check if already aborted\n\t\t\tif (signal?.aborted) {\n\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet aborted = false;\n\n\t\t\t// Set up abort handler\n\t\t\tconst onAbort = () => {\n\t\t\t\taborted = true;\n\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t};\n\n\t\t\tif (signal) {\n\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t}\n\n\t\t\t// Perform the read operation\n\t\t\t(async () => {\n\t\t\t\ttry {\n\t\t\t\t\t// Check if file exists\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait access(absolutePath, constants.R_OK);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: `Error: File not found: ${path}` }],\n\t\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Check if aborted before reading\n\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Read the file based on type\n\t\t\t\t\tlet content: (TextContent | ImageContent)[];\n\n\t\t\t\t\tif (mimeType) {\n\t\t\t\t\t\t// Read as image (binary)\n\t\t\t\t\t\tconst buffer = await readFile(absolutePath);\n\t\t\t\t\t\tconst base64 = buffer.toString(\"base64\");\n\n\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t{ type: \"text\", text: `Read image file: ${path}` },\n\t\t\t\t\t\t\t{ type: \"image\", data: base64, mimeType },\n\t\t\t\t\t\t];\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Read as text\n\t\t\t\t\t\tconst textContent = await readFile(absolutePath, \"utf-8\");\n\t\t\t\t\t\tconst lines = textContent.split(\"\\n\");\n\n\t\t\t\t\t\t// Apply offset and limit (matching Claude Code Read tool behavior)\n\t\t\t\t\t\tconst startLine = offset ? Math.max(0, offset - 1) : 0; // 1-indexed to 0-indexed\n\t\t\t\t\t\tconst maxLines = limit || MAX_LINES;\n\t\t\t\t\t\tconst endLine = Math.min(startLine + maxLines, lines.length);\n\n\t\t\t\t\t\t// Check if offset is out of bounds\n\t\t\t\t\t\tif (startLine >= lines.length) {\n\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\ttext: `Error: Offset ${offset} is beyond end of file (${lines.length} lines total)`,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t];\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Get the relevant lines\n\t\t\t\t\t\t\tconst selectedLines = lines.slice(startLine, endLine);\n\n\t\t\t\t\t\t\t// Truncate long lines and track which were truncated\n\t\t\t\t\t\t\tlet hadTruncatedLines = false;\n\t\t\t\t\t\t\tconst formattedLines = selectedLines.map((line) => {\n\t\t\t\t\t\t\t\tif (line.length > MAX_LINE_LENGTH) {\n\t\t\t\t\t\t\t\t\thadTruncatedLines = true;\n\t\t\t\t\t\t\t\t\treturn line.slice(0, MAX_LINE_LENGTH);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\treturn line;\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\tlet outputText = formattedLines.join(\"\\n\");\n\n\t\t\t\t\t\t\t// Add notices\n\t\t\t\t\t\t\tconst notices: string[] = [];\n\n\t\t\t\t\t\t\tif (hadTruncatedLines) {\n\t\t\t\t\t\t\t\tnotices.push(`Some lines were truncated to ${MAX_LINE_LENGTH} characters for display`);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (endLine < lines.length) {\n\t\t\t\t\t\t\t\tconst remaining = lines.length - endLine;\n\t\t\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t\t\t`${remaining} more lines not shown. Use offset=${endLine + 1} to continue reading`,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (notices.length > 0) {\n\t\t\t\t\t\t\t\toutputText += `\\n\\n... (${notices.join(\". \")})`;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: outputText }];\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Check if aborted after reading\n\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\tif (signal) {\n\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t}\n\n\t\t\t\t\tresolve({ content, details: undefined });\n\t\t\t\t} catch (error: any) {\n\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\tif (signal) {\n\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (!aborted) {\n\t\t\t\t\t\treject(error);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})();\n\t\t});\n\t},\n};\n"]}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import { constants } from "fs";
|
|
4
|
+
import { access, readFile } from "fs/promises";
|
|
5
|
+
import { extname, resolve as resolvePath } from "path";
|
|
6
|
+
/**
|
|
7
|
+
* Expand ~ to home directory
|
|
8
|
+
*/
|
|
9
|
+
function expandPath(filePath) {
|
|
10
|
+
if (filePath === "~") {
|
|
11
|
+
return os.homedir();
|
|
12
|
+
}
|
|
13
|
+
if (filePath.startsWith("~/")) {
|
|
14
|
+
return os.homedir() + filePath.slice(1);
|
|
15
|
+
}
|
|
16
|
+
return filePath;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Map of file extensions to MIME types for common image formats
|
|
20
|
+
*/
|
|
21
|
+
const IMAGE_MIME_TYPES = {
|
|
22
|
+
".jpg": "image/jpeg",
|
|
23
|
+
".jpeg": "image/jpeg",
|
|
24
|
+
".png": "image/png",
|
|
25
|
+
".gif": "image/gif",
|
|
26
|
+
".webp": "image/webp",
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Check if a file is an image based on its extension
|
|
30
|
+
*/
|
|
31
|
+
function isImageFile(filePath) {
|
|
32
|
+
const ext = extname(filePath).toLowerCase();
|
|
33
|
+
return IMAGE_MIME_TYPES[ext] || null;
|
|
34
|
+
}
|
|
35
|
+
const readSchema = Type.Object({
|
|
36
|
+
path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
|
|
37
|
+
offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
|
|
38
|
+
limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
|
|
39
|
+
});
|
|
40
|
+
const MAX_LINES = 2000;
|
|
41
|
+
const MAX_LINE_LENGTH = 2000;
|
|
42
|
+
export const readTool = {
|
|
43
|
+
name: "read",
|
|
44
|
+
label: "read",
|
|
45
|
+
description: "Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, defaults to first 2000 lines. Use offset/limit for large files.",
|
|
46
|
+
parameters: readSchema,
|
|
47
|
+
execute: async (_toolCallId, { path, offset, limit }, signal) => {
|
|
48
|
+
const absolutePath = resolvePath(expandPath(path));
|
|
49
|
+
const mimeType = isImageFile(absolutePath);
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
// Check if already aborted
|
|
52
|
+
if (signal?.aborted) {
|
|
53
|
+
reject(new Error("Operation aborted"));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
let aborted = false;
|
|
57
|
+
// Set up abort handler
|
|
58
|
+
const onAbort = () => {
|
|
59
|
+
aborted = true;
|
|
60
|
+
reject(new Error("Operation aborted"));
|
|
61
|
+
};
|
|
62
|
+
if (signal) {
|
|
63
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
64
|
+
}
|
|
65
|
+
// Perform the read operation
|
|
66
|
+
(async () => {
|
|
67
|
+
try {
|
|
68
|
+
// Check if file exists
|
|
69
|
+
try {
|
|
70
|
+
await access(absolutePath, constants.R_OK);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
if (signal) {
|
|
74
|
+
signal.removeEventListener("abort", onAbort);
|
|
75
|
+
}
|
|
76
|
+
resolve({
|
|
77
|
+
content: [{ type: "text", text: `Error: File not found: ${path}` }],
|
|
78
|
+
details: undefined,
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Check if aborted before reading
|
|
83
|
+
if (aborted) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Read the file based on type
|
|
87
|
+
let content;
|
|
88
|
+
if (mimeType) {
|
|
89
|
+
// Read as image (binary)
|
|
90
|
+
const buffer = await readFile(absolutePath);
|
|
91
|
+
const base64 = buffer.toString("base64");
|
|
92
|
+
content = [
|
|
93
|
+
{ type: "text", text: `Read image file: ${path}` },
|
|
94
|
+
{ type: "image", data: base64, mimeType },
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// Read as text
|
|
99
|
+
const textContent = await readFile(absolutePath, "utf-8");
|
|
100
|
+
const lines = textContent.split("\n");
|
|
101
|
+
// Apply offset and limit (matching Claude Code Read tool behavior)
|
|
102
|
+
const startLine = offset ? Math.max(0, offset - 1) : 0; // 1-indexed to 0-indexed
|
|
103
|
+
const maxLines = limit || MAX_LINES;
|
|
104
|
+
const endLine = Math.min(startLine + maxLines, lines.length);
|
|
105
|
+
// Check if offset is out of bounds
|
|
106
|
+
if (startLine >= lines.length) {
|
|
107
|
+
content = [
|
|
108
|
+
{
|
|
109
|
+
type: "text",
|
|
110
|
+
text: `Error: Offset ${offset} is beyond end of file (${lines.length} lines total)`,
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// Get the relevant lines
|
|
116
|
+
const selectedLines = lines.slice(startLine, endLine);
|
|
117
|
+
// Truncate long lines and track which were truncated
|
|
118
|
+
let hadTruncatedLines = false;
|
|
119
|
+
const formattedLines = selectedLines.map((line) => {
|
|
120
|
+
if (line.length > MAX_LINE_LENGTH) {
|
|
121
|
+
hadTruncatedLines = true;
|
|
122
|
+
return line.slice(0, MAX_LINE_LENGTH);
|
|
123
|
+
}
|
|
124
|
+
return line;
|
|
125
|
+
});
|
|
126
|
+
let outputText = formattedLines.join("\n");
|
|
127
|
+
// Add notices
|
|
128
|
+
const notices = [];
|
|
129
|
+
if (hadTruncatedLines) {
|
|
130
|
+
notices.push(`Some lines were truncated to ${MAX_LINE_LENGTH} characters for display`);
|
|
131
|
+
}
|
|
132
|
+
if (endLine < lines.length) {
|
|
133
|
+
const remaining = lines.length - endLine;
|
|
134
|
+
notices.push(`${remaining} more lines not shown. Use offset=${endLine + 1} to continue reading`);
|
|
135
|
+
}
|
|
136
|
+
if (notices.length > 0) {
|
|
137
|
+
outputText += `\n\n... (${notices.join(". ")})`;
|
|
138
|
+
}
|
|
139
|
+
content = [{ type: "text", text: outputText }];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Check if aborted after reading
|
|
143
|
+
if (aborted) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// Clean up abort handler
|
|
147
|
+
if (signal) {
|
|
148
|
+
signal.removeEventListener("abort", onAbort);
|
|
149
|
+
}
|
|
150
|
+
resolve({ content, details: undefined });
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
// Clean up abort handler
|
|
154
|
+
if (signal) {
|
|
155
|
+
signal.removeEventListener("abort", onAbort);
|
|
156
|
+
}
|
|
157
|
+
if (!aborted) {
|
|
158
|
+
reject(error);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
})();
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
//# sourceMappingURL=read.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAE9B,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,MAAM,CAAC;AAEvD;;GAEG;AACH,SAAS,UAAU,CAAC,QAAgB,EAAU;IAC7C,IAAI,QAAQ,KAAK,GAAG,EAAE,CAAC;QACtB,OAAO,EAAE,CAAC,OAAO,EAAE,CAAC;IACrB,CAAC;IACD,IAAI,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/B,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,QAAQ,CAAC;AAAA,CAChB;AAED;;GAEG;AACH,MAAM,gBAAgB,GAA2B;IAChD,MAAM,EAAE,YAAY;IACpB,OAAO,EAAE,YAAY;IACrB,MAAM,EAAE,WAAW;IACnB,MAAM,EAAE,WAAW;IACnB,OAAO,EAAE,YAAY;CACrB,CAAC;AAEF;;GAEG;AACH,SAAS,WAAW,CAAC,QAAgB,EAAiB;IACrD,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;IAC5C,OAAO,gBAAgB,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;AAAA,CACrC;AAED,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC;IAC9B,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,iDAAiD,EAAE,CAAC;IACrF,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,+CAA+C,EAAE,CAAC,CAAC;IACpG,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,iCAAiC,EAAE,CAAC,CAAC;CACrF,CAAC,CAAC;AAEH,MAAM,SAAS,GAAG,IAAI,CAAC;AACvB,MAAM,eAAe,GAAG,IAAI,CAAC;AAE7B,MAAM,CAAC,MAAM,QAAQ,GAAiC;IACrD,IAAI,EAAE,MAAM;IACZ,KAAK,EAAE,MAAM;IACb,WAAW,EACV,oMAAoM;IACrM,UAAU,EAAE,UAAU;IACtB,OAAO,EAAE,KAAK,EACb,WAAmB,EACnB,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAqD,EAC1E,MAAoB,EACnB,EAAE,CAAC;QACJ,MAAM,YAAY,GAAG,WAAW,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;QACnD,MAAM,QAAQ,GAAG,WAAW,CAAC,YAAY,CAAC,CAAC;QAE3C,OAAO,IAAI,OAAO,CAAkE,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;YACxG,2BAA2B;YAC3B,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;gBACrB,MAAM,CAAC,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAC;gBACvC,OAAO;YACR,CAAC;YAED,IAAI,OAAO,GAAG,KAAK,CAAC;YAEpB,uBAAuB;YACvB,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC;gBACrB,OAAO,GAAG,IAAI,CAAC;gBACf,MAAM,CAAC,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAC;YAAA,CACvC,CAAC;YAEF,IAAI,MAAM,EAAE,CAAC;gBACZ,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YAC3D,CAAC;YAED,6BAA6B;YAC7B,CAAC,KAAK,IAAI,EAAE,CAAC;gBACZ,IAAI,CAAC;oBACJ,uBAAuB;oBACvB,IAAI,CAAC;wBACJ,MAAM,MAAM,CAAC,YAAY,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;oBAC5C,CAAC;oBAAC,MAAM,CAAC;wBACR,IAAI,MAAM,EAAE,CAAC;4BACZ,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;wBAC9C,CAAC;wBACD,OAAO,CAAC;4BACP,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,0BAA0B,IAAI,EAAE,EAAE,CAAC;4BACnE,OAAO,EAAE,SAAS;yBAClB,CAAC,CAAC;wBACH,OAAO;oBACR,CAAC;oBAED,kCAAkC;oBAClC,IAAI,OAAO,EAAE,CAAC;wBACb,OAAO;oBACR,CAAC;oBAED,8BAA8B;oBAC9B,IAAI,OAAuC,CAAC;oBAE5C,IAAI,QAAQ,EAAE,CAAC;wBACd,yBAAyB;wBACzB,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,YAAY,CAAC,CAAC;wBAC5C,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;wBAEzC,OAAO,GAAG;4BACT,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,oBAAoB,IAAI,EAAE,EAAE;4BAClD,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE;yBACzC,CAAC;oBACH,CAAC;yBAAM,CAAC;wBACP,eAAe;wBACf,MAAM,WAAW,GAAG,MAAM,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;wBAC1D,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;wBAEtC,mEAAmE;wBACnE,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,yBAAyB;wBACjF,MAAM,QAAQ,GAAG,KAAK,IAAI,SAAS,CAAC;wBACpC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,GAAG,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;wBAE7D,mCAAmC;wBACnC,IAAI,SAAS,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;4BAC/B,OAAO,GAAG;gCACT;oCACC,IAAI,EAAE,MAAM;oCACZ,IAAI,EAAE,iBAAiB,MAAM,2BAA2B,KAAK,CAAC,MAAM,eAAe;iCACnF;6BACD,CAAC;wBACH,CAAC;6BAAM,CAAC;4BACP,yBAAyB;4BACzB,MAAM,aAAa,GAAG,KAAK,CAAC,KAAK,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;4BAEtD,qDAAqD;4BACrD,IAAI,iBAAiB,GAAG,KAAK,CAAC;4BAC9B,MAAM,cAAc,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;gCAClD,IAAI,IAAI,CAAC,MAAM,GAAG,eAAe,EAAE,CAAC;oCACnC,iBAAiB,GAAG,IAAI,CAAC;oCACzB,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC;gCACvC,CAAC;gCACD,OAAO,IAAI,CAAC;4BAAA,CACZ,CAAC,CAAC;4BAEH,IAAI,UAAU,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;4BAE3C,cAAc;4BACd,MAAM,OAAO,GAAa,EAAE,CAAC;4BAE7B,IAAI,iBAAiB,EAAE,CAAC;gCACvB,OAAO,CAAC,IAAI,CAAC,gCAAgC,eAAe,yBAAyB,CAAC,CAAC;4BACxF,CAAC;4BAED,IAAI,OAAO,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;gCAC5B,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,GAAG,OAAO,CAAC;gCACzC,OAAO,CAAC,IAAI,CACX,GAAG,SAAS,qCAAqC,OAAO,GAAG,CAAC,sBAAsB,CAClF,CAAC;4BACH,CAAC;4BAED,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gCACxB,UAAU,IAAI,YAAY,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;4BACjD,CAAC;4BAED,OAAO,GAAG,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;wBAChD,CAAC;oBACF,CAAC;oBAED,iCAAiC;oBACjC,IAAI,OAAO,EAAE,CAAC;wBACb,OAAO;oBACR,CAAC;oBAED,yBAAyB;oBACzB,IAAI,MAAM,EAAE,CAAC;wBACZ,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;oBAC9C,CAAC;oBAED,OAAO,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;gBAC1C,CAAC;gBAAC,OAAO,KAAU,EAAE,CAAC;oBACrB,yBAAyB;oBACzB,IAAI,MAAM,EAAE,CAAC;wBACZ,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;oBAC9C,CAAC;oBAED,IAAI,CAAC,OAAO,EAAE,CAAC;wBACd,MAAM,CAAC,KAAK,CAAC,CAAC;oBACf,CAAC;gBACF,CAAC;YAAA,CACD,CAAC,EAAE,CAAC;QAAA,CACL,CAAC,CAAC;IAAA,CACH;CACD,CAAC","sourcesContent":["import * as os from \"node:os\";\nimport type { AgentTool, ImageContent, TextContent } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { constants } from \"fs\";\nimport { access, readFile } from \"fs/promises\";\nimport { extname, resolve as resolvePath } from \"path\";\n\n/**\n * Expand ~ to home directory\n */\nfunction expandPath(filePath: string): string {\n\tif (filePath === \"~\") {\n\t\treturn os.homedir();\n\t}\n\tif (filePath.startsWith(\"~/\")) {\n\t\treturn os.homedir() + filePath.slice(1);\n\t}\n\treturn filePath;\n}\n\n/**\n * Map of file extensions to MIME types for common image formats\n */\nconst IMAGE_MIME_TYPES: Record<string, string> = {\n\t\".jpg\": \"image/jpeg\",\n\t\".jpeg\": \"image/jpeg\",\n\t\".png\": \"image/png\",\n\t\".gif\": \"image/gif\",\n\t\".webp\": \"image/webp\",\n};\n\n/**\n * Check if a file is an image based on its extension\n */\nfunction isImageFile(filePath: string): string | null {\n\tconst ext = extname(filePath).toLowerCase();\n\treturn IMAGE_MIME_TYPES[ext] || null;\n}\n\nconst readSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\nconst MAX_LINES = 2000;\nconst MAX_LINE_LENGTH = 2000;\n\nexport const readTool: AgentTool<typeof readSchema> = {\n\tname: \"read\",\n\tlabel: \"read\",\n\tdescription:\n\t\t\"Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, defaults to first 2000 lines. Use offset/limit for large files.\",\n\tparameters: readSchema,\n\texecute: async (\n\t\t_toolCallId: string,\n\t\t{ path, offset, limit }: { path: string; offset?: number; limit?: number },\n\t\tsignal?: AbortSignal,\n\t) => {\n\t\tconst absolutePath = resolvePath(expandPath(path));\n\t\tconst mimeType = isImageFile(absolutePath);\n\n\t\treturn new Promise<{ content: (TextContent | ImageContent)[]; details: undefined }>((resolve, reject) => {\n\t\t\t// Check if already aborted\n\t\t\tif (signal?.aborted) {\n\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet aborted = false;\n\n\t\t\t// Set up abort handler\n\t\t\tconst onAbort = () => {\n\t\t\t\taborted = true;\n\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t};\n\n\t\t\tif (signal) {\n\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t}\n\n\t\t\t// Perform the read operation\n\t\t\t(async () => {\n\t\t\t\ttry {\n\t\t\t\t\t// Check if file exists\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait access(absolutePath, constants.R_OK);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: `Error: File not found: ${path}` }],\n\t\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Check if aborted before reading\n\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Read the file based on type\n\t\t\t\t\tlet content: (TextContent | ImageContent)[];\n\n\t\t\t\t\tif (mimeType) {\n\t\t\t\t\t\t// Read as image (binary)\n\t\t\t\t\t\tconst buffer = await readFile(absolutePath);\n\t\t\t\t\t\tconst base64 = buffer.toString(\"base64\");\n\n\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t{ type: \"text\", text: `Read image file: ${path}` },\n\t\t\t\t\t\t\t{ type: \"image\", data: base64, mimeType },\n\t\t\t\t\t\t];\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Read as text\n\t\t\t\t\t\tconst textContent = await readFile(absolutePath, \"utf-8\");\n\t\t\t\t\t\tconst lines = textContent.split(\"\\n\");\n\n\t\t\t\t\t\t// Apply offset and limit (matching Claude Code Read tool behavior)\n\t\t\t\t\t\tconst startLine = offset ? Math.max(0, offset - 1) : 0; // 1-indexed to 0-indexed\n\t\t\t\t\t\tconst maxLines = limit || MAX_LINES;\n\t\t\t\t\t\tconst endLine = Math.min(startLine + maxLines, lines.length);\n\n\t\t\t\t\t\t// Check if offset is out of bounds\n\t\t\t\t\t\tif (startLine >= lines.length) {\n\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\ttext: `Error: Offset ${offset} is beyond end of file (${lines.length} lines total)`,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t];\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Get the relevant lines\n\t\t\t\t\t\t\tconst selectedLines = lines.slice(startLine, endLine);\n\n\t\t\t\t\t\t\t// Truncate long lines and track which were truncated\n\t\t\t\t\t\t\tlet hadTruncatedLines = false;\n\t\t\t\t\t\t\tconst formattedLines = selectedLines.map((line) => {\n\t\t\t\t\t\t\t\tif (line.length > MAX_LINE_LENGTH) {\n\t\t\t\t\t\t\t\t\thadTruncatedLines = true;\n\t\t\t\t\t\t\t\t\treturn line.slice(0, MAX_LINE_LENGTH);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\treturn line;\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\tlet outputText = formattedLines.join(\"\\n\");\n\n\t\t\t\t\t\t\t// Add notices\n\t\t\t\t\t\t\tconst notices: string[] = [];\n\n\t\t\t\t\t\t\tif (hadTruncatedLines) {\n\t\t\t\t\t\t\t\tnotices.push(`Some lines were truncated to ${MAX_LINE_LENGTH} characters for display`);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (endLine < lines.length) {\n\t\t\t\t\t\t\t\tconst remaining = lines.length - endLine;\n\t\t\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t\t\t`${remaining} more lines not shown. Use offset=${endLine + 1} to continue reading`,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (notices.length > 0) {\n\t\t\t\t\t\t\t\toutputText += `\\n\\n... (${notices.join(\". \")})`;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: outputText }];\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Check if aborted after reading\n\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\tif (signal) {\n\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t}\n\n\t\t\t\t\tresolve({ content, details: undefined });\n\t\t\t\t} catch (error: any) {\n\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\tif (signal) {\n\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (!aborted) {\n\t\t\t\t\t\treject(error);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})();\n\t\t});\n\t},\n};\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AgentTool } from "@mariozechner/pi-ai";
|
|
2
|
+
declare const writeSchema: import("@sinclair/typebox").TObject<{
|
|
3
|
+
path: import("@sinclair/typebox").TString;
|
|
4
|
+
content: import("@sinclair/typebox").TString;
|
|
5
|
+
}>;
|
|
6
|
+
export declare const writeTool: AgentTool<typeof writeSchema>;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=write.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../../src/tools/write.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAkBrD,QAAA,MAAM,WAAW;;;EAGf,CAAC;AAEH,eAAO,MAAM,SAAS,EAAE,SAAS,CAAC,OAAO,WAAW,CAsEnD,CAAC","sourcesContent":["import * as os from \"node:os\";\nimport type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { mkdir, writeFile } from \"fs/promises\";\nimport { dirname, resolve as resolvePath } from \"path\";\n\n/**\n * Expand ~ to home directory\n */\nfunction expandPath(filePath: string): string {\n\tif (filePath === \"~\") {\n\t\treturn os.homedir();\n\t}\n\tif (filePath.startsWith(\"~/\")) {\n\t\treturn os.homedir() + filePath.slice(1);\n\t}\n\treturn filePath;\n}\n\nconst writeSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to write (relative or absolute)\" }),\n\tcontent: Type.String({ description: \"Content to write to the file\" }),\n});\n\nexport const writeTool: AgentTool<typeof writeSchema> = {\n\tname: \"write\",\n\tlabel: \"write\",\n\tdescription:\n\t\t\"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.\",\n\tparameters: writeSchema,\n\texecute: async (_toolCallId: string, { path, content }: { path: string; content: string }, signal?: AbortSignal) => {\n\t\tconst absolutePath = resolvePath(expandPath(path));\n\t\tconst dir = dirname(absolutePath);\n\n\t\treturn new Promise<{ content: Array<{ type: \"text\"; text: string }>; details: undefined }>((resolve, reject) => {\n\t\t\t// Check if already aborted\n\t\t\tif (signal?.aborted) {\n\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet aborted = false;\n\n\t\t\t// Set up abort handler\n\t\t\tconst onAbort = () => {\n\t\t\t\taborted = true;\n\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t};\n\n\t\t\tif (signal) {\n\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t}\n\n\t\t\t// Perform the write operation\n\t\t\t(async () => {\n\t\t\t\ttry {\n\t\t\t\t\t// Create parent directories if needed\n\t\t\t\t\tawait mkdir(dir, { recursive: true });\n\n\t\t\t\t\t// Check if aborted before writing\n\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Write the file\n\t\t\t\t\tawait writeFile(absolutePath, content, \"utf-8\");\n\n\t\t\t\t\t// Check if aborted after writing\n\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\tif (signal) {\n\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t}\n\n\t\t\t\t\tresolve({\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: `Successfully wrote ${content.length} bytes to ${path}` }],\n\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t});\n\t\t\t\t} catch (error: any) {\n\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\tif (signal) {\n\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (!aborted) {\n\t\t\t\t\t\treject(error);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})();\n\t\t});\n\t},\n};\n"]}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
4
|
+
import { dirname, resolve as resolvePath } from "path";
|
|
5
|
+
/**
|
|
6
|
+
* Expand ~ to home directory
|
|
7
|
+
*/
|
|
8
|
+
function expandPath(filePath) {
|
|
9
|
+
if (filePath === "~") {
|
|
10
|
+
return os.homedir();
|
|
11
|
+
}
|
|
12
|
+
if (filePath.startsWith("~/")) {
|
|
13
|
+
return os.homedir() + filePath.slice(1);
|
|
14
|
+
}
|
|
15
|
+
return filePath;
|
|
16
|
+
}
|
|
17
|
+
const writeSchema = Type.Object({
|
|
18
|
+
path: Type.String({ description: "Path to the file to write (relative or absolute)" }),
|
|
19
|
+
content: Type.String({ description: "Content to write to the file" }),
|
|
20
|
+
});
|
|
21
|
+
export const writeTool = {
|
|
22
|
+
name: "write",
|
|
23
|
+
label: "write",
|
|
24
|
+
description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.",
|
|
25
|
+
parameters: writeSchema,
|
|
26
|
+
execute: async (_toolCallId, { path, content }, signal) => {
|
|
27
|
+
const absolutePath = resolvePath(expandPath(path));
|
|
28
|
+
const dir = dirname(absolutePath);
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
// Check if already aborted
|
|
31
|
+
if (signal?.aborted) {
|
|
32
|
+
reject(new Error("Operation aborted"));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
let aborted = false;
|
|
36
|
+
// Set up abort handler
|
|
37
|
+
const onAbort = () => {
|
|
38
|
+
aborted = true;
|
|
39
|
+
reject(new Error("Operation aborted"));
|
|
40
|
+
};
|
|
41
|
+
if (signal) {
|
|
42
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
43
|
+
}
|
|
44
|
+
// Perform the write operation
|
|
45
|
+
(async () => {
|
|
46
|
+
try {
|
|
47
|
+
// Create parent directories if needed
|
|
48
|
+
await mkdir(dir, { recursive: true });
|
|
49
|
+
// Check if aborted before writing
|
|
50
|
+
if (aborted) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Write the file
|
|
54
|
+
await writeFile(absolutePath, content, "utf-8");
|
|
55
|
+
// Check if aborted after writing
|
|
56
|
+
if (aborted) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Clean up abort handler
|
|
60
|
+
if (signal) {
|
|
61
|
+
signal.removeEventListener("abort", onAbort);
|
|
62
|
+
}
|
|
63
|
+
resolve({
|
|
64
|
+
content: [{ type: "text", text: `Successfully wrote ${content.length} bytes to ${path}` }],
|
|
65
|
+
details: undefined,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
// Clean up abort handler
|
|
70
|
+
if (signal) {
|
|
71
|
+
signal.removeEventListener("abort", onAbort);
|
|
72
|
+
}
|
|
73
|
+
if (!aborted) {
|
|
74
|
+
reject(error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
})();
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=write.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write.js","sourceRoot":"","sources":["../../src/tools/write.ts"],"names":[],"mappings":"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","sourcesContent":["import * as os from \"node:os\";\nimport type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { mkdir, writeFile } from \"fs/promises\";\nimport { dirname, resolve as resolvePath } from \"path\";\n\n/**\n * Expand ~ to home directory\n */\nfunction expandPath(filePath: string): string {\n\tif (filePath === \"~\") {\n\t\treturn os.homedir();\n\t}\n\tif (filePath.startsWith(\"~/\")) {\n\t\treturn os.homedir() + filePath.slice(1);\n\t}\n\treturn filePath;\n}\n\nconst writeSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to write (relative or absolute)\" }),\n\tcontent: Type.String({ description: \"Content to write to the file\" }),\n});\n\nexport const writeTool: AgentTool<typeof writeSchema> = {\n\tname: \"write\",\n\tlabel: \"write\",\n\tdescription:\n\t\t\"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.\",\n\tparameters: writeSchema,\n\texecute: async (_toolCallId: string, { path, content }: { path: string; content: string }, signal?: AbortSignal) => {\n\t\tconst absolutePath = resolvePath(expandPath(path));\n\t\tconst dir = dirname(absolutePath);\n\n\t\treturn new Promise<{ content: Array<{ type: \"text\"; text: string }>; details: undefined }>((resolve, reject) => {\n\t\t\t// Check if already aborted\n\t\t\tif (signal?.aborted) {\n\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet aborted = false;\n\n\t\t\t// Set up abort handler\n\t\t\tconst onAbort = () => {\n\t\t\t\taborted = true;\n\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t};\n\n\t\t\tif (signal) {\n\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t}\n\n\t\t\t// Perform the write operation\n\t\t\t(async () => {\n\t\t\t\ttry {\n\t\t\t\t\t// Create parent directories if needed\n\t\t\t\t\tawait mkdir(dir, { recursive: true });\n\n\t\t\t\t\t// Check if aborted before writing\n\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Write the file\n\t\t\t\t\tawait writeFile(absolutePath, content, \"utf-8\");\n\n\t\t\t\t\t// Check if aborted after writing\n\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\tif (signal) {\n\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t}\n\n\t\t\t\t\tresolve({\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: `Successfully wrote ${content.length} bytes to ${path}` }],\n\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t});\n\t\t\t\t} catch (error: any) {\n\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\tif (signal) {\n\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (!aborted) {\n\t\t\t\t\t\treject(error);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})();\n\t\t});\n\t},\n};\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
|
2
|
+
import { Container } from "@mariozechner/pi-tui";
|
|
3
|
+
/**
|
|
4
|
+
* Component that renders a complete assistant message
|
|
5
|
+
*/
|
|
6
|
+
export declare class AssistantMessageComponent extends Container {
|
|
7
|
+
private contentContainer;
|
|
8
|
+
constructor(message?: AssistantMessage);
|
|
9
|
+
updateContent(message: AssistantMessage): void;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=assistant-message.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistant-message.d.ts","sourceRoot":"","sources":["../../src/tui/assistant-message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,SAAS,EAA0B,MAAM,sBAAsB,CAAC;AAGzE;;GAEG;AACH,qBAAa,yBAA0B,SAAQ,SAAS;IACvD,OAAO,CAAC,gBAAgB,CAAY;IAEpC,YAAY,OAAO,CAAC,EAAE,gBAAgB,EAUrC;IAED,aAAa,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAuC7C;CACD","sourcesContent":["import type { AssistantMessage } from \"@mariozechner/pi-ai\";\nimport { Container, Markdown, Spacer, Text } from \"@mariozechner/pi-tui\";\nimport chalk from \"chalk\";\n\n/**\n * Component that renders a complete assistant message\n */\nexport class AssistantMessageComponent extends Container {\n\tprivate contentContainer: Container;\n\n\tconstructor(message?: AssistantMessage) {\n\t\tsuper();\n\n\t\t// Container for text/thinking content\n\t\tthis.contentContainer = new Container();\n\t\tthis.addChild(this.contentContainer);\n\n\t\tif (message) {\n\t\t\tthis.updateContent(message);\n\t\t}\n\t}\n\n\tupdateContent(message: AssistantMessage): void {\n\t\t// Clear content container\n\t\tthis.contentContainer.clear();\n\n\t\tif (\n\t\t\tmessage.content.length > 0 &&\n\t\t\tmessage.content.some(\n\t\t\t\t(c) => (c.type === \"text\" && c.text.trim()) || (c.type === \"thinking\" && c.thinking.trim()),\n\t\t\t)\n\t\t) {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t}\n\n\t\t// Render content in order\n\t\tfor (const content of message.content) {\n\t\t\tif (content.type === \"text\" && content.text.trim()) {\n\t\t\t\t// Assistant text messages with no background - trim the text\n\t\t\t\t// Set paddingY=0 to avoid extra spacing before tool executions\n\t\t\t\tthis.contentContainer.addChild(new Markdown(content.text.trim(), undefined, undefined, undefined, 1, 0));\n\t\t\t} else if (content.type === \"thinking\" && content.thinking.trim()) {\n\t\t\t\t// Thinking traces in dark gray italic\n\t\t\t\t// Use Markdown component because it preserves ANSI codes across wrapped lines\n\t\t\t\tconst thinkingText = chalk.gray.italic(content.thinking);\n\t\t\t\tthis.contentContainer.addChild(new Markdown(thinkingText, undefined, undefined, undefined, 1, 0));\n\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t}\n\t\t}\n\n\t\t// Check if aborted - show after partial content\n\t\t// But only if there are no tool calls (tool execution components will show the error)\n\t\tconst hasToolCalls = message.content.some((c) => c.type === \"toolCall\");\n\t\tif (!hasToolCalls) {\n\t\t\tif (message.stopReason === \"aborted\") {\n\t\t\t\tthis.contentContainer.addChild(new Text(chalk.red(\"Aborted\"), 1, 0));\n\t\t\t} else if (message.stopReason === \"error\") {\n\t\t\t\tconst errorMsg = message.errorMessage || \"Unknown error\";\n\t\t\t\tthis.contentContainer.addChild(new Text(chalk.red(`Error: ${errorMsg}`)));\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
|