@mariozechner/pi-coding-agent 0.8.3 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.8.5] - 2025-11-21
6
+
7
+ ### Fixed
8
+
9
+ - **Path Completion Hanging**: Fixed catastrophic regex backtracking in path completion that caused the terminal to hang when text contained many `/` characters (e.g., URLs). Replaced complex regex with simple string operations. ([#18](https://github.com/badlogic/pi-mono/issues/18))
10
+ - **Autocomplete Arrow Keys**: Fixed issue where arrow keys would move both the autocomplete selection and the editor cursor simultaneously when the file selector list was shown.
11
+
12
+ ## [0.8.4] - 2025-11-21
13
+
14
+ ### Fixed
15
+
16
+ - **Read Tool Error Handling**: Fixed issue where the `read` tool would return errors as successful text content instead of throwing. Now properly throws errors for file not found and offset out of bounds conditions.
17
+
5
18
  ## [0.8.3] - 2025-11-21
6
19
 
7
20
  ### Improved
@@ -1 +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 [${mimeType}]` },\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"]}
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,CAiIjD,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\tawait access(absolutePath, constants.R_OK);\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 [${mimeType}]` },\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\tthrow new Error(`Offset ${offset} is beyond end of file (${lines.length} lines total)`);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Get the relevant lines\n\t\t\t\t\t\tconst selectedLines = lines.slice(startLine, endLine);\n\n\t\t\t\t\t\t// Truncate long lines and track which were truncated\n\t\t\t\t\t\tlet hadTruncatedLines = false;\n\t\t\t\t\t\tconst formattedLines = selectedLines.map((line) => {\n\t\t\t\t\t\t\tif (line.length > MAX_LINE_LENGTH) {\n\t\t\t\t\t\t\t\thadTruncatedLines = true;\n\t\t\t\t\t\t\t\treturn line.slice(0, MAX_LINE_LENGTH);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn line;\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tlet outputText = formattedLines.join(\"\\n\");\n\n\t\t\t\t\t\t// Add notices\n\t\t\t\t\t\tconst notices: string[] = [];\n\n\t\t\t\t\t\tif (hadTruncatedLines) {\n\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}\n\n\t\t\t\t\t\tif (endLine < lines.length) {\n\t\t\t\t\t\t\tconst remaining = lines.length - endLine;\n\t\t\t\t\t\t\tnotices.push(`${remaining} more lines not shown. Use offset=${endLine + 1} to continue reading`);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (notices.length > 0) {\n\t\t\t\t\t\t\toutputText += `\\n\\n... (${notices.join(\". \")})`;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tcontent = [{ type: \"text\", text: outputText }];\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"]}
@@ -66,19 +66,7 @@ export const readTool = {
66
66
  (async () => {
67
67
  try {
68
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
- }
69
+ await access(absolutePath, constants.R_OK);
82
70
  // Check if aborted before reading
83
71
  if (aborted) {
84
72
  return;
@@ -104,40 +92,33 @@ export const readTool = {
104
92
  const endLine = Math.min(startLine + maxLines, lines.length);
105
93
  // Check if offset is out of bounds
106
94
  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
- ];
95
+ throw new Error(`Offset ${offset} is beyond end of file (${lines.length} lines total)`);
113
96
  }
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`);
97
+ // Get the relevant lines
98
+ const selectedLines = lines.slice(startLine, endLine);
99
+ // Truncate long lines and track which were truncated
100
+ let hadTruncatedLines = false;
101
+ const formattedLines = selectedLines.map((line) => {
102
+ if (line.length > MAX_LINE_LENGTH) {
103
+ hadTruncatedLines = true;
104
+ return line.slice(0, MAX_LINE_LENGTH);
131
105
  }
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 }];
106
+ return line;
107
+ });
108
+ let outputText = formattedLines.join("\n");
109
+ // Add notices
110
+ const notices = [];
111
+ if (hadTruncatedLines) {
112
+ notices.push(`Some lines were truncated to ${MAX_LINE_LENGTH} characters for display`);
113
+ }
114
+ if (endLine < lines.length) {
115
+ const remaining = lines.length - endLine;
116
+ notices.push(`${remaining} more lines not shown. Use offset=${endLine + 1} to continue reading`);
117
+ }
118
+ if (notices.length > 0) {
119
+ outputText += `\n\n... (${notices.join(". ")})`;
140
120
  }
121
+ content = [{ type: "text", text: outputText }];
141
122
  }
142
123
  // Check if aborted after reading
143
124
  if (aborted) {
@@ -1 +1 @@
1
- {"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"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","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 [${mimeType}]` },\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"]}
1
+ {"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"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","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\tawait access(absolutePath, constants.R_OK);\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 [${mimeType}]` },\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\tthrow new Error(`Offset ${offset} is beyond end of file (${lines.length} lines total)`);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Get the relevant lines\n\t\t\t\t\t\tconst selectedLines = lines.slice(startLine, endLine);\n\n\t\t\t\t\t\t// Truncate long lines and track which were truncated\n\t\t\t\t\t\tlet hadTruncatedLines = false;\n\t\t\t\t\t\tconst formattedLines = selectedLines.map((line) => {\n\t\t\t\t\t\t\tif (line.length > MAX_LINE_LENGTH) {\n\t\t\t\t\t\t\t\thadTruncatedLines = true;\n\t\t\t\t\t\t\t\treturn line.slice(0, MAX_LINE_LENGTH);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn line;\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tlet outputText = formattedLines.join(\"\\n\");\n\n\t\t\t\t\t\t// Add notices\n\t\t\t\t\t\tconst notices: string[] = [];\n\n\t\t\t\t\t\tif (hadTruncatedLines) {\n\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}\n\n\t\t\t\t\t\tif (endLine < lines.length) {\n\t\t\t\t\t\t\tconst remaining = lines.length - endLine;\n\t\t\t\t\t\t\tnotices.push(`${remaining} more lines not shown. Use offset=${endLine + 1} to continue reading`);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (notices.length > 0) {\n\t\t\t\t\t\t\toutputText += `\\n\\n... (${notices.join(\". \")})`;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tcontent = [{ type: \"text\", text: outputText }];\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mariozechner/pi-coding-agent",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,9 +22,9 @@
22
22
  "prepublishOnly": "npm run clean && npm run build"
23
23
  },
24
24
  "dependencies": {
25
- "@mariozechner/pi-agent": "^0.8.3",
26
- "@mariozechner/pi-ai": "^0.8.3",
27
- "@mariozechner/pi-tui": "^0.8.3",
25
+ "@mariozechner/pi-agent": "^0.8.5",
26
+ "@mariozechner/pi-ai": "^0.8.5",
27
+ "@mariozechner/pi-tui": "^0.8.5",
28
28
  "chalk": "^5.5.0",
29
29
  "diff": "^8.0.2",
30
30
  "glob": "^11.0.3"