@mariozechner/pi-coding-agent 0.23.0 → 0.23.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/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ## [0.23.2] - 2025-12-17
6
+
7
+ ### Fixed
8
+
9
+ - Fixed Claude models via GitHub Copilot re-answering all previous prompts in multi-turn conversations. The issue was that assistant message content was sent as an array instead of a string, which Copilot's Claude adapter misinterpreted. Also added missing `Openai-Intent: conversation-edits` header and fixed `X-Initiator` logic to check for any assistant/tool message in history. ([#209](https://github.com/badlogic/pi-mono/issues/209))
10
+
11
+ - Detect image MIME type via file magic (read tool and `@file` attachments), not filename extension.
12
+
13
+ - Fixed markdown tables overflowing terminal width. Tables now wrap cell contents to fit available width instead of breaking borders mid-row. ([#206](https://github.com/badlogic/pi-mono/pull/206) by [@kim0](https://github.com/kim0))
14
+
15
+ ## [0.23.1] - 2025-12-17
16
+
17
+ ### Fixed
18
+
19
+ - Fixed TUI performance regression caused by Box component lacking render caching. Built-in tools now use Text directly (like v0.22.5), and Box has proper caching for custom tool rendering.
20
+
21
+ - Fixed custom tools failing to load from `~/.pi/agent/tools/` when pi is installed globally. Module imports (`@sinclair/typebox`, `@mariozechner/pi-tui`, `@mariozechner/pi-ai`) are now resolved via aliases.
22
+
3
23
  ## [0.23.0] - 2025-12-17
4
24
 
5
25
  ### Added
@@ -7,5 +7,5 @@ export interface ProcessedFiles {
7
7
  imageAttachments: Attachment[];
8
8
  }
9
9
  /** Process @file arguments into text content and image attachments */
10
- export declare function processFileArguments(fileArgs: string[]): ProcessedFiles;
10
+ export declare function processFileArguments(fileArgs: string[]): Promise<ProcessedFiles>;
11
11
  //# sourceMappingURL=file-processor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"file-processor.d.ts","sourceRoot":"","sources":["../../src/cli/file-processor.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAqB9D,MAAM,WAAW,cAAc;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,UAAU,EAAE,CAAC;CAC/B;AAED,sEAAsE;AACtE,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,cAAc,CAuDvE","sourcesContent":["/**\n * Process @file CLI arguments into text content and image attachments\n */\n\nimport type { Attachment } from \"@mariozechner/pi-agent-core\";\nimport chalk from \"chalk\";\nimport { existsSync, readFileSync, statSync } from \"fs\";\nimport { extname, resolve } from \"path\";\nimport { resolveReadPath } from \"../core/tools/path-utils.js\";\n\n/** Map of file extensions to MIME types for common image formats */\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/** Check if a file is an image based on its extension, returns MIME type or null */\nfunction isImageFile(filePath: string): string | null {\n\tconst ext = extname(filePath).toLowerCase();\n\treturn IMAGE_MIME_TYPES[ext] || null;\n}\n\nexport interface ProcessedFiles {\n\ttextContent: string;\n\timageAttachments: Attachment[];\n}\n\n/** Process @file arguments into text content and image attachments */\nexport function processFileArguments(fileArgs: string[]): ProcessedFiles {\n\tlet textContent = \"\";\n\tconst imageAttachments: Attachment[] = [];\n\n\tfor (const fileArg of fileArgs) {\n\t\t// Expand and resolve path (handles ~ expansion and macOS screenshot Unicode spaces)\n\t\tconst absolutePath = resolve(resolveReadPath(fileArg));\n\n\t\t// Check if file exists\n\t\tif (!existsSync(absolutePath)) {\n\t\t\tconsole.error(chalk.red(`Error: File not found: ${absolutePath}`));\n\t\t\tprocess.exit(1);\n\t\t}\n\n\t\t// Check if file is empty\n\t\tconst stats = statSync(absolutePath);\n\t\tif (stats.size === 0) {\n\t\t\t// Skip empty files\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst mimeType = isImageFile(absolutePath);\n\n\t\tif (mimeType) {\n\t\t\t// Handle image file\n\t\t\tconst content = readFileSync(absolutePath);\n\t\t\tconst base64Content = content.toString(\"base64\");\n\n\t\t\tconst attachment: Attachment = {\n\t\t\t\tid: `file-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,\n\t\t\t\ttype: \"image\",\n\t\t\t\tfileName: absolutePath.split(\"/\").pop() || absolutePath,\n\t\t\t\tmimeType,\n\t\t\t\tsize: stats.size,\n\t\t\t\tcontent: base64Content,\n\t\t\t};\n\n\t\t\timageAttachments.push(attachment);\n\n\t\t\t// Add text reference to image\n\t\t\ttextContent += `<file name=\"${absolutePath}\"></file>\\n`;\n\t\t} else {\n\t\t\t// Handle text file\n\t\t\ttry {\n\t\t\t\tconst content = readFileSync(absolutePath, \"utf-8\");\n\t\t\t\ttextContent += `<file name=\"${absolutePath}\">\\n${content}\\n</file>\\n`;\n\t\t\t} catch (error: unknown) {\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\tconsole.error(chalk.red(`Error: Could not read file ${absolutePath}: ${message}`));\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { textContent, imageAttachments };\n}\n"]}
1
+ {"version":3,"file":"file-processor.d.ts","sourceRoot":"","sources":["../../src/cli/file-processor.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAM9D,MAAM,WAAW,cAAc;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,UAAU,EAAE,CAAC;CAC/B;AAED,sEAAsE;AACtE,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAyDtF","sourcesContent":["/**\n * Process @file CLI arguments into text content and image attachments\n */\n\nimport { access, readFile, stat } from \"node:fs/promises\";\nimport type { Attachment } from \"@mariozechner/pi-agent-core\";\nimport chalk from \"chalk\";\nimport { resolve } from \"path\";\nimport { resolveReadPath } from \"../core/tools/path-utils.js\";\nimport { detectSupportedImageMimeTypeFromFile } from \"../utils/mime.js\";\n\nexport interface ProcessedFiles {\n\ttextContent: string;\n\timageAttachments: Attachment[];\n}\n\n/** Process @file arguments into text content and image attachments */\nexport async function processFileArguments(fileArgs: string[]): Promise<ProcessedFiles> {\n\tlet textContent = \"\";\n\tconst imageAttachments: Attachment[] = [];\n\n\tfor (const fileArg of fileArgs) {\n\t\t// Expand and resolve path (handles ~ expansion and macOS screenshot Unicode spaces)\n\t\tconst absolutePath = resolve(resolveReadPath(fileArg));\n\n\t\t// Check if file exists\n\t\ttry {\n\t\t\tawait access(absolutePath);\n\t\t} catch {\n\t\t\tconsole.error(chalk.red(`Error: File not found: ${absolutePath}`));\n\t\t\tprocess.exit(1);\n\t\t}\n\n\t\t// Check if file is empty\n\t\tconst stats = await stat(absolutePath);\n\t\tif (stats.size === 0) {\n\t\t\t// Skip empty files\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);\n\n\t\tif (mimeType) {\n\t\t\t// Handle image file\n\t\t\tconst content = await readFile(absolutePath);\n\t\t\tconst base64Content = content.toString(\"base64\");\n\n\t\t\tconst attachment: Attachment = {\n\t\t\t\tid: `file-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,\n\t\t\t\ttype: \"image\",\n\t\t\t\tfileName: absolutePath.split(\"/\").pop() || absolutePath,\n\t\t\t\tmimeType,\n\t\t\t\tsize: stats.size,\n\t\t\t\tcontent: base64Content,\n\t\t\t};\n\n\t\t\timageAttachments.push(attachment);\n\n\t\t\t// Add text reference to image\n\t\t\ttextContent += `<file name=\"${absolutePath}\"></file>\\n`;\n\t\t} else {\n\t\t\t// Handle text file\n\t\t\ttry {\n\t\t\t\tconst content = await readFile(absolutePath, \"utf-8\");\n\t\t\t\ttextContent += `<file name=\"${absolutePath}\">\\n${content}\\n</file>\\n`;\n\t\t\t} catch (error: unknown) {\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\tconsole.error(chalk.red(`Error: Could not read file ${absolutePath}: ${message}`));\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { textContent, imageAttachments };\n}\n"]}
@@ -1,45 +1,36 @@
1
1
  /**
2
2
  * Process @file CLI arguments into text content and image attachments
3
3
  */
4
+ import { access, readFile, stat } from "node:fs/promises";
4
5
  import chalk from "chalk";
5
- import { existsSync, readFileSync, statSync } from "fs";
6
- import { extname, resolve } from "path";
6
+ import { resolve } from "path";
7
7
  import { resolveReadPath } from "../core/tools/path-utils.js";
8
- /** Map of file extensions to MIME types for common image formats */
9
- const IMAGE_MIME_TYPES = {
10
- ".jpg": "image/jpeg",
11
- ".jpeg": "image/jpeg",
12
- ".png": "image/png",
13
- ".gif": "image/gif",
14
- ".webp": "image/webp",
15
- };
16
- /** Check if a file is an image based on its extension, returns MIME type or null */
17
- function isImageFile(filePath) {
18
- const ext = extname(filePath).toLowerCase();
19
- return IMAGE_MIME_TYPES[ext] || null;
20
- }
8
+ import { detectSupportedImageMimeTypeFromFile } from "../utils/mime.js";
21
9
  /** Process @file arguments into text content and image attachments */
22
- export function processFileArguments(fileArgs) {
10
+ export async function processFileArguments(fileArgs) {
23
11
  let textContent = "";
24
12
  const imageAttachments = [];
25
13
  for (const fileArg of fileArgs) {
26
14
  // Expand and resolve path (handles ~ expansion and macOS screenshot Unicode spaces)
27
15
  const absolutePath = resolve(resolveReadPath(fileArg));
28
16
  // Check if file exists
29
- if (!existsSync(absolutePath)) {
17
+ try {
18
+ await access(absolutePath);
19
+ }
20
+ catch {
30
21
  console.error(chalk.red(`Error: File not found: ${absolutePath}`));
31
22
  process.exit(1);
32
23
  }
33
24
  // Check if file is empty
34
- const stats = statSync(absolutePath);
25
+ const stats = await stat(absolutePath);
35
26
  if (stats.size === 0) {
36
27
  // Skip empty files
37
28
  continue;
38
29
  }
39
- const mimeType = isImageFile(absolutePath);
30
+ const mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);
40
31
  if (mimeType) {
41
32
  // Handle image file
42
- const content = readFileSync(absolutePath);
33
+ const content = await readFile(absolutePath);
43
34
  const base64Content = content.toString("base64");
44
35
  const attachment = {
45
36
  id: `file-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
@@ -56,7 +47,7 @@ export function processFileArguments(fileArgs) {
56
47
  else {
57
48
  // Handle text file
58
49
  try {
59
- const content = readFileSync(absolutePath, "utf-8");
50
+ const content = await readFile(absolutePath, "utf-8");
60
51
  textContent += `<file name="${absolutePath}">\n${content}\n</file>\n`;
61
52
  }
62
53
  catch (error) {
@@ -1 +1 @@
1
- {"version":3,"file":"file-processor.js","sourceRoot":"","sources":["../../src/cli/file-processor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Process @file CLI arguments into text content and image attachments\n */\n\nimport type { Attachment } from \"@mariozechner/pi-agent-core\";\nimport chalk from \"chalk\";\nimport { existsSync, readFileSync, statSync } from \"fs\";\nimport { extname, resolve } from \"path\";\nimport { resolveReadPath } from \"../core/tools/path-utils.js\";\n\n/** Map of file extensions to MIME types for common image formats */\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/** Check if a file is an image based on its extension, returns MIME type or null */\nfunction isImageFile(filePath: string): string | null {\n\tconst ext = extname(filePath).toLowerCase();\n\treturn IMAGE_MIME_TYPES[ext] || null;\n}\n\nexport interface ProcessedFiles {\n\ttextContent: string;\n\timageAttachments: Attachment[];\n}\n\n/** Process @file arguments into text content and image attachments */\nexport function processFileArguments(fileArgs: string[]): ProcessedFiles {\n\tlet textContent = \"\";\n\tconst imageAttachments: Attachment[] = [];\n\n\tfor (const fileArg of fileArgs) {\n\t\t// Expand and resolve path (handles ~ expansion and macOS screenshot Unicode spaces)\n\t\tconst absolutePath = resolve(resolveReadPath(fileArg));\n\n\t\t// Check if file exists\n\t\tif (!existsSync(absolutePath)) {\n\t\t\tconsole.error(chalk.red(`Error: File not found: ${absolutePath}`));\n\t\t\tprocess.exit(1);\n\t\t}\n\n\t\t// Check if file is empty\n\t\tconst stats = statSync(absolutePath);\n\t\tif (stats.size === 0) {\n\t\t\t// Skip empty files\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst mimeType = isImageFile(absolutePath);\n\n\t\tif (mimeType) {\n\t\t\t// Handle image file\n\t\t\tconst content = readFileSync(absolutePath);\n\t\t\tconst base64Content = content.toString(\"base64\");\n\n\t\t\tconst attachment: Attachment = {\n\t\t\t\tid: `file-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,\n\t\t\t\ttype: \"image\",\n\t\t\t\tfileName: absolutePath.split(\"/\").pop() || absolutePath,\n\t\t\t\tmimeType,\n\t\t\t\tsize: stats.size,\n\t\t\t\tcontent: base64Content,\n\t\t\t};\n\n\t\t\timageAttachments.push(attachment);\n\n\t\t\t// Add text reference to image\n\t\t\ttextContent += `<file name=\"${absolutePath}\"></file>\\n`;\n\t\t} else {\n\t\t\t// Handle text file\n\t\t\ttry {\n\t\t\t\tconst content = readFileSync(absolutePath, \"utf-8\");\n\t\t\t\ttextContent += `<file name=\"${absolutePath}\">\\n${content}\\n</file>\\n`;\n\t\t\t} catch (error: unknown) {\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\tconsole.error(chalk.red(`Error: Could not read file ${absolutePath}: ${message}`));\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { textContent, imageAttachments };\n}\n"]}
1
+ {"version":3,"file":"file-processor.js","sourceRoot":"","sources":["../../src/cli/file-processor.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAC/B,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,oCAAoC,EAAE,MAAM,kBAAkB,CAAC;AAOxE,sEAAsE;AACtE,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,QAAkB,EAA2B;IACvF,IAAI,WAAW,GAAG,EAAE,CAAC;IACrB,MAAM,gBAAgB,GAAiB,EAAE,CAAC;IAE1C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAChC,oFAAoF;QACpF,MAAM,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;QAEvD,uBAAuB;QACvB,IAAI,CAAC;YACJ,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;QAC5B,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,0BAA0B,YAAY,EAAE,CAAC,CAAC,CAAC;YACnE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACjB,CAAC;QAED,yBAAyB;QACzB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,CAAC;QACvC,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YACtB,mBAAmB;YACnB,SAAS;QACV,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,oCAAoC,CAAC,YAAY,CAAC,CAAC;QAE1E,IAAI,QAAQ,EAAE,CAAC;YACd,oBAAoB;YACpB,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,YAAY,CAAC,CAAC;YAC7C,MAAM,aAAa,GAAG,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAEjD,MAAM,UAAU,GAAe;gBAC9B,EAAE,EAAE,QAAQ,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE;gBAClE,IAAI,EAAE,OAAO;gBACb,QAAQ,EAAE,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,YAAY;gBACvD,QAAQ;gBACR,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,OAAO,EAAE,aAAa;aACtB,CAAC;YAEF,gBAAgB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAElC,8BAA8B;YAC9B,WAAW,IAAI,eAAe,YAAY,aAAa,CAAC;QACzD,CAAC;aAAM,CAAC;YACP,mBAAmB;YACnB,IAAI,CAAC;gBACJ,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;gBACtD,WAAW,IAAI,eAAe,YAAY,OAAO,OAAO,aAAa,CAAC;YACvE,CAAC;YAAC,OAAO,KAAc,EAAE,CAAC;gBACzB,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACvE,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,8BAA8B,YAAY,KAAK,OAAO,EAAE,CAAC,CAAC,CAAC;gBACnF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACjB,CAAC;QACF,CAAC;IACF,CAAC;IAED,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,CAAC;AAAA,CACzC","sourcesContent":["/**\n * Process @file CLI arguments into text content and image attachments\n */\n\nimport { access, readFile, stat } from \"node:fs/promises\";\nimport type { Attachment } from \"@mariozechner/pi-agent-core\";\nimport chalk from \"chalk\";\nimport { resolve } from \"path\";\nimport { resolveReadPath } from \"../core/tools/path-utils.js\";\nimport { detectSupportedImageMimeTypeFromFile } from \"../utils/mime.js\";\n\nexport interface ProcessedFiles {\n\ttextContent: string;\n\timageAttachments: Attachment[];\n}\n\n/** Process @file arguments into text content and image attachments */\nexport async function processFileArguments(fileArgs: string[]): Promise<ProcessedFiles> {\n\tlet textContent = \"\";\n\tconst imageAttachments: Attachment[] = [];\n\n\tfor (const fileArg of fileArgs) {\n\t\t// Expand and resolve path (handles ~ expansion and macOS screenshot Unicode spaces)\n\t\tconst absolutePath = resolve(resolveReadPath(fileArg));\n\n\t\t// Check if file exists\n\t\ttry {\n\t\t\tawait access(absolutePath);\n\t\t} catch {\n\t\t\tconsole.error(chalk.red(`Error: File not found: ${absolutePath}`));\n\t\t\tprocess.exit(1);\n\t\t}\n\n\t\t// Check if file is empty\n\t\tconst stats = await stat(absolutePath);\n\t\tif (stats.size === 0) {\n\t\t\t// Skip empty files\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);\n\n\t\tif (mimeType) {\n\t\t\t// Handle image file\n\t\t\tconst content = await readFile(absolutePath);\n\t\t\tconst base64Content = content.toString(\"base64\");\n\n\t\t\tconst attachment: Attachment = {\n\t\t\t\tid: `file-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,\n\t\t\t\ttype: \"image\",\n\t\t\t\tfileName: absolutePath.split(\"/\").pop() || absolutePath,\n\t\t\t\tmimeType,\n\t\t\t\tsize: stats.size,\n\t\t\t\tcontent: base64Content,\n\t\t\t};\n\n\t\t\timageAttachments.push(attachment);\n\n\t\t\t// Add text reference to image\n\t\t\ttextContent += `<file name=\"${absolutePath}\"></file>\\n`;\n\t\t} else {\n\t\t\t// Handle text file\n\t\t\ttry {\n\t\t\t\tconst content = await readFile(absolutePath, \"utf-8\");\n\t\t\t\ttextContent += `<file name=\"${absolutePath}\">\\n${content}\\n</file>\\n`;\n\t\t\t} catch (error: unknown) {\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\tconsole.error(chalk.red(`Error: Could not read file ${absolutePath}: ${message}`));\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { textContent, imageAttachments };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/custom-tools/loader.ts"],"names":[],"mappings":"AAAA;;GAEG;AASH,OAAO,KAAK,EAAqB,qBAAqB,EAAyC,MAAM,YAAY,CAAC;AAiIlH;;;;;GAKG;AACH,wBAAsB,eAAe,CACpC,KAAK,EAAE,MAAM,EAAE,EACf,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,qBAAqB,CAAC,CA8ChC;AAmBD;;;;;;;;;;GAUG;AACH,wBAAsB,0BAA0B,CAC/C,eAAe,EAAE,MAAM,EAAE,EACzB,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,qBAAqB,CAAC,CA2BhC","sourcesContent":["/**\n * Custom tool loader - loads TypeScript tool modules using jiti.\n */\n\nimport { spawn } from \"node:child_process\";\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir } from \"../../config.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type { CustomToolFactory, CustomToolsLoadResult, ExecResult, LoadedCustomTool, ToolAPI } from \"./types.js\";\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve tool path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveToolPath(toolPath: string, cwd: string): string {\n\tconst expanded = expandPath(toolPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Execute a command and return stdout/stderr/code.\n */\nasync function execCommand(command: string, args: string[], cwd: string): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, {\n\t\t\tcwd,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\n\t\tproc.stdout.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\tcode: code ?? 0,\n\t\t\t});\n\t\t});\n\n\t\tproc.on(\"error\", (err) => {\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr: stderr || err.message,\n\t\t\t\tcode: 1,\n\t\t\t});\n\t\t});\n\t});\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => null,\n\t\tconfirm: async () => false,\n\t\tinput: async () => null,\n\t\tnotify: () => {},\n\t};\n}\n\n/**\n * Load a single tool module using jiti.\n */\nasync function loadTool(\n\ttoolPath: string,\n\tcwd: string,\n\tsharedApi: ToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\tconst resolvedPath = resolveToolPath(toolPath, cwd);\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\tconst jiti = createJiti(import.meta.url);\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\t// Call factory with shared API\n\t\tconst result = await factory(sharedApi);\n\n\t\t// Handle single tool or array of tools\n\t\tconst toolsArray = Array.isArray(result) ? result : [result];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: toolPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load all tools from configuration.\n * @param paths - Array of tool file paths\n * @param cwd - Current working directory for resolving relative paths\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function loadCustomTools(\n\tpaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst tools: LoadedCustomTool[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst seenNames = new Set<string>(builtInToolNames);\n\n\t// Shared API object - all tools get the same instance\n\tconst sharedApi: ToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[]) => execCommand(command, args, cwd),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Returns all .ts files in the directory (non-recursive).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\t\treturn entries.filter((e) => e.isFile() && e.name.endsWith(\".ts\")).map((e) => path.join(dir, e.name));\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. ~/.pi/agent/tools/*.ts (global)\n * 2. cwd/.pi/tools/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings or CLI.\n *\n * @param configuredPaths - Explicit paths from settings.json and CLI --tool flags\n * @param cwd - Current working directory\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function discoverAndLoadCustomTools(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global tools: ~/.pi/agent/tools/\n\tconst globalToolsDir = path.join(getAgentDir(), \"tools\");\n\taddPaths(discoverToolsInDir(globalToolsDir));\n\n\t// 2. Project-local tools: cwd/.pi/tools/\n\tconst localToolsDir = path.join(cwd, \".pi\", \"tools\");\n\taddPaths(discoverToolsInDir(localToolsDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveToolPath(p, cwd)));\n\n\treturn loadCustomTools(allPaths, cwd, builtInToolNames);\n}\n"]}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/custom-tools/loader.ts"],"names":[],"mappings":"AAAA;;GAEG;AAWH,OAAO,KAAK,EAAqB,qBAAqB,EAAyC,MAAM,YAAY,CAAC;AAyJlH;;;;;GAKG;AACH,wBAAsB,eAAe,CACpC,KAAK,EAAE,MAAM,EAAE,EACf,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,qBAAqB,CAAC,CA8ChC;AAmBD;;;;;;;;;;GAUG;AACH,wBAAsB,0BAA0B,CAC/C,eAAe,EAAE,MAAM,EAAE,EACzB,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,qBAAqB,CAAC,CA2BhC","sourcesContent":["/**\n * Custom tool loader - loads TypeScript tool modules using jiti.\n */\n\nimport { spawn } from \"node:child_process\";\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir } from \"../../config.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type { CustomToolFactory, CustomToolsLoadResult, ExecResult, LoadedCustomTool, ToolAPI } from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": require.resolve(\"@sinclair/typebox\"),\n\t};\n\treturn _aliases;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve tool path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveToolPath(toolPath: string, cwd: string): string {\n\tconst expanded = expandPath(toolPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Execute a command and return stdout/stderr/code.\n */\nasync function execCommand(command: string, args: string[], cwd: string): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, {\n\t\t\tcwd,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\n\t\tproc.stdout.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\tcode: code ?? 0,\n\t\t\t});\n\t\t});\n\n\t\tproc.on(\"error\", (err) => {\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr: stderr || err.message,\n\t\t\t\tcode: 1,\n\t\t\t});\n\t\t});\n\t});\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => null,\n\t\tconfirm: async () => false,\n\t\tinput: async () => null,\n\t\tnotify: () => {},\n\t};\n}\n\n/**\n * Load a single tool module using jiti.\n */\nasync function loadTool(\n\ttoolPath: string,\n\tcwd: string,\n\tsharedApi: ToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\tconst resolvedPath = resolveToolPath(toolPath, cwd);\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since tools are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/tools) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\t// Call factory with shared API\n\t\tconst result = await factory(sharedApi);\n\n\t\t// Handle single tool or array of tools\n\t\tconst toolsArray = Array.isArray(result) ? result : [result];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: toolPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load all tools from configuration.\n * @param paths - Array of tool file paths\n * @param cwd - Current working directory for resolving relative paths\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function loadCustomTools(\n\tpaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst tools: LoadedCustomTool[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst seenNames = new Set<string>(builtInToolNames);\n\n\t// Shared API object - all tools get the same instance\n\tconst sharedApi: ToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[]) => execCommand(command, args, cwd),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Returns all .ts files in the directory (non-recursive).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\t\treturn entries.filter((e) => e.isFile() && e.name.endsWith(\".ts\")).map((e) => path.join(dir, e.name));\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. ~/.pi/agent/tools/*.ts (global)\n * 2. cwd/.pi/tools/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings or CLI.\n *\n * @param configuredPaths - Explicit paths from settings.json and CLI --tool flags\n * @param cwd - Current working directory\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function discoverAndLoadCustomTools(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global tools: ~/.pi/agent/tools/\n\tconst globalToolsDir = path.join(getAgentDir(), \"tools\");\n\taddPaths(discoverToolsInDir(globalToolsDir));\n\n\t// 2. Project-local tools: cwd/.pi/tools/\n\tconst localToolsDir = path.join(cwd, \".pi\", \"tools\");\n\taddPaths(discoverToolsInDir(localToolsDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveToolPath(p, cwd)));\n\n\treturn loadCustomTools(allPaths, cwd, builtInToolNames);\n}\n"]}
@@ -3,10 +3,29 @@
3
3
  */
4
4
  import { spawn } from "node:child_process";
5
5
  import * as fs from "node:fs";
6
+ import { createRequire } from "node:module";
6
7
  import * as os from "node:os";
7
8
  import * as path from "node:path";
9
+ import { fileURLToPath } from "node:url";
8
10
  import { createJiti } from "jiti";
9
11
  import { getAgentDir } from "../../config.js";
12
+ // Create require function to resolve module paths at runtime
13
+ const require = createRequire(import.meta.url);
14
+ // Lazily computed aliases - resolved at runtime to handle global installs
15
+ let _aliases = null;
16
+ function getAliases() {
17
+ if (_aliases)
18
+ return _aliases;
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const packageIndex = path.resolve(__dirname, "../..", "index.js");
21
+ _aliases = {
22
+ "@mariozechner/pi-coding-agent": packageIndex,
23
+ "@mariozechner/pi-tui": require.resolve("@mariozechner/pi-tui"),
24
+ "@mariozechner/pi-ai": require.resolve("@mariozechner/pi-ai"),
25
+ "@sinclair/typebox": require.resolve("@sinclair/typebox"),
26
+ };
27
+ return _aliases;
28
+ }
10
29
  const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
11
30
  function normalizeUnicodeSpaces(str) {
12
31
  return str.replace(UNICODE_SPACES, " ");
@@ -87,7 +106,11 @@ async function loadTool(toolPath, cwd, sharedApi) {
87
106
  const resolvedPath = resolveToolPath(toolPath, cwd);
88
107
  try {
89
108
  // Create jiti instance for TypeScript/ESM loading
90
- const jiti = createJiti(import.meta.url);
109
+ // Use aliases to resolve package imports since tools are loaded from user directories
110
+ // (e.g. ~/.pi/agent/tools) but import from packages installed with pi-coding-agent
111
+ const jiti = createJiti(import.meta.url, {
112
+ alias: getAliases(),
113
+ });
91
114
  // Import the module
92
115
  const module = await jiti.import(resolvedPath, { default: true });
93
116
  const factory = module;
@@ -1 +1 @@
1
- {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../../src/core/custom-tools/loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Custom tool loader - loads TypeScript tool modules using jiti.\n */\n\nimport { spawn } from \"node:child_process\";\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir } from \"../../config.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type { CustomToolFactory, CustomToolsLoadResult, ExecResult, LoadedCustomTool, ToolAPI } from \"./types.js\";\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve tool path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveToolPath(toolPath: string, cwd: string): string {\n\tconst expanded = expandPath(toolPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Execute a command and return stdout/stderr/code.\n */\nasync function execCommand(command: string, args: string[], cwd: string): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, {\n\t\t\tcwd,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\n\t\tproc.stdout.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\tcode: code ?? 0,\n\t\t\t});\n\t\t});\n\n\t\tproc.on(\"error\", (err) => {\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr: stderr || err.message,\n\t\t\t\tcode: 1,\n\t\t\t});\n\t\t});\n\t});\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => null,\n\t\tconfirm: async () => false,\n\t\tinput: async () => null,\n\t\tnotify: () => {},\n\t};\n}\n\n/**\n * Load a single tool module using jiti.\n */\nasync function loadTool(\n\ttoolPath: string,\n\tcwd: string,\n\tsharedApi: ToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\tconst resolvedPath = resolveToolPath(toolPath, cwd);\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\tconst jiti = createJiti(import.meta.url);\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\t// Call factory with shared API\n\t\tconst result = await factory(sharedApi);\n\n\t\t// Handle single tool or array of tools\n\t\tconst toolsArray = Array.isArray(result) ? result : [result];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: toolPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load all tools from configuration.\n * @param paths - Array of tool file paths\n * @param cwd - Current working directory for resolving relative paths\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function loadCustomTools(\n\tpaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst tools: LoadedCustomTool[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst seenNames = new Set<string>(builtInToolNames);\n\n\t// Shared API object - all tools get the same instance\n\tconst sharedApi: ToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[]) => execCommand(command, args, cwd),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Returns all .ts files in the directory (non-recursive).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\t\treturn entries.filter((e) => e.isFile() && e.name.endsWith(\".ts\")).map((e) => path.join(dir, e.name));\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. ~/.pi/agent/tools/*.ts (global)\n * 2. cwd/.pi/tools/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings or CLI.\n *\n * @param configuredPaths - Explicit paths from settings.json and CLI --tool flags\n * @param cwd - Current working directory\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function discoverAndLoadCustomTools(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global tools: ~/.pi/agent/tools/\n\tconst globalToolsDir = path.join(getAgentDir(), \"tools\");\n\taddPaths(discoverToolsInDir(globalToolsDir));\n\n\t// 2. Project-local tools: cwd/.pi/tools/\n\tconst localToolsDir = path.join(cwd, \".pi\", \"tools\");\n\taddPaths(discoverToolsInDir(localToolsDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveToolPath(p, cwd)));\n\n\treturn loadCustomTools(allPaths, cwd, builtInToolNames);\n}\n"]}
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../../src/core/custom-tools/loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Custom tool loader - loads TypeScript tool modules using jiti.\n */\n\nimport { spawn } from \"node:child_process\";\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir } from \"../../config.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type { CustomToolFactory, CustomToolsLoadResult, ExecResult, LoadedCustomTool, ToolAPI } from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": require.resolve(\"@sinclair/typebox\"),\n\t};\n\treturn _aliases;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve tool path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveToolPath(toolPath: string, cwd: string): string {\n\tconst expanded = expandPath(toolPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Execute a command and return stdout/stderr/code.\n */\nasync function execCommand(command: string, args: string[], cwd: string): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, {\n\t\t\tcwd,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\n\t\tproc.stdout.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\tcode: code ?? 0,\n\t\t\t});\n\t\t});\n\n\t\tproc.on(\"error\", (err) => {\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr: stderr || err.message,\n\t\t\t\tcode: 1,\n\t\t\t});\n\t\t});\n\t});\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => null,\n\t\tconfirm: async () => false,\n\t\tinput: async () => null,\n\t\tnotify: () => {},\n\t};\n}\n\n/**\n * Load a single tool module using jiti.\n */\nasync function loadTool(\n\ttoolPath: string,\n\tcwd: string,\n\tsharedApi: ToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\tconst resolvedPath = resolveToolPath(toolPath, cwd);\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since tools are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/tools) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\t// Call factory with shared API\n\t\tconst result = await factory(sharedApi);\n\n\t\t// Handle single tool or array of tools\n\t\tconst toolsArray = Array.isArray(result) ? result : [result];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: toolPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load all tools from configuration.\n * @param paths - Array of tool file paths\n * @param cwd - Current working directory for resolving relative paths\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function loadCustomTools(\n\tpaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst tools: LoadedCustomTool[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst seenNames = new Set<string>(builtInToolNames);\n\n\t// Shared API object - all tools get the same instance\n\tconst sharedApi: ToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[]) => execCommand(command, args, cwd),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Returns all .ts files in the directory (non-recursive).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\t\treturn entries.filter((e) => e.isFile() && e.name.endsWith(\".ts\")).map((e) => path.join(dir, e.name));\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. ~/.pi/agent/tools/*.ts (global)\n * 2. cwd/.pi/tools/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings or CLI.\n *\n * @param configuredPaths - Explicit paths from settings.json and CLI --tool flags\n * @param cwd - Current working directory\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function discoverAndLoadCustomTools(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global tools: ~/.pi/agent/tools/\n\tconst globalToolsDir = path.join(getAgentDir(), \"tools\");\n\taddPaths(discoverToolsInDir(globalToolsDir));\n\n\t// 2. Project-local tools: cwd/.pi/tools/\n\tconst localToolsDir = path.join(cwd, \".pi\", \"tools\");\n\taddPaths(discoverToolsInDir(localToolsDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveToolPath(p, cwd)));\n\n\treturn loadCustomTools(allPaths, cwd, builtInToolNames);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../../src/core/tools/read.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAA6B,MAAM,qBAAqB,CAAC;AA2BhF,QAAA,MAAM,UAAU;;;;EAId,CAAC;AAMH,eAAO,MAAM,QAAQ,EAAE,SAAS,CAAC,OAAO,UAAU,CAmJjD,CAAC","sourcesContent":["import 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\";\nimport { resolveReadPath } from \"./path-utils.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\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\ninterface ReadToolDetails {\n\ttruncation?: TruncationResult;\n}\n\nexport const readTool: AgentTool<typeof readSchema> = {\n\tname: \"read\",\n\tlabel: \"read\",\n\tdescription: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). 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(resolveReadPath(path));\n\t\tconst mimeType = isImageFile(absolutePath);\n\n\t\treturn new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>(\n\t\t\t(resolve, reject) => {\n\t\t\t\t// Check if already aborted\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tlet aborted = false;\n\n\t\t\t\t// Set up abort handler\n\t\t\t\tconst onAbort = () => {\n\t\t\t\t\taborted = true;\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t};\n\n\t\t\t\tif (signal) {\n\t\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\n\t\t\t\t// Perform the read operation\n\t\t\t\t(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// Check if file exists\n\t\t\t\t\t\tawait access(absolutePath, constants.R_OK);\n\n\t\t\t\t\t\t// Check if aborted before reading\n\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Read the file based on type\n\t\t\t\t\t\tlet content: (TextContent | ImageContent)[];\n\t\t\t\t\t\tlet details: ReadToolDetails | undefined;\n\n\t\t\t\t\t\tif (mimeType) {\n\t\t\t\t\t\t\t// Read as image (binary)\n\t\t\t\t\t\t\tconst buffer = await readFile(absolutePath);\n\t\t\t\t\t\t\tconst base64 = buffer.toString(\"base64\");\n\n\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t{ type: \"text\", text: `Read image file [${mimeType}]` },\n\t\t\t\t\t\t\t\t{ type: \"image\", data: base64, mimeType },\n\t\t\t\t\t\t\t];\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Read as text\n\t\t\t\t\t\t\tconst textContent = await readFile(absolutePath, \"utf-8\");\n\t\t\t\t\t\t\tconst allLines = textContent.split(\"\\n\");\n\t\t\t\t\t\t\tconst totalFileLines = allLines.length;\n\n\t\t\t\t\t\t\t// Apply offset if specified (1-indexed to 0-indexed)\n\t\t\t\t\t\t\tconst startLine = offset ? Math.max(0, offset - 1) : 0;\n\t\t\t\t\t\t\tconst startLineDisplay = startLine + 1; // For display (1-indexed)\n\n\t\t\t\t\t\t\t// Check if offset is out of bounds\n\t\t\t\t\t\t\tif (startLine >= allLines.length) {\n\t\t\t\t\t\t\t\tthrow new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// If limit is specified by user, use it; otherwise we'll let truncateHead decide\n\t\t\t\t\t\t\tlet selectedContent: string;\n\t\t\t\t\t\t\tlet userLimitedLines: number | undefined;\n\t\t\t\t\t\t\tif (limit !== undefined) {\n\t\t\t\t\t\t\t\tconst endLine = Math.min(startLine + limit, allLines.length);\n\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine, endLine).join(\"\\n\");\n\t\t\t\t\t\t\t\tuserLimitedLines = endLine - startLine;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine).join(\"\\n\");\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Apply truncation (respects both line and byte limits)\n\t\t\t\t\t\t\tconst truncation = truncateHead(selectedContent);\n\n\t\t\t\t\t\t\tlet outputText: string;\n\n\t\t\t\t\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t\t\t\t\t// First line at offset exceeds 30KB - tell model to use bash\n\t\t\t\t\t\t\t\tconst firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], \"utf-8\"));\n\t\t\t\t\t\t\t\toutputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;\n\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t} else if (truncation.truncated) {\n\t\t\t\t\t\t\t\t// Truncation occurred - build actionable notice\n\t\t\t\t\t\t\t\tconst endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n\t\t\t\t\t\t\t\tconst nextOffset = endLineDisplay + 1;\n\n\t\t\t\t\t\t\t\toutputText = truncation.content;\n\n\t\t\t\t\t\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {\n\t\t\t\t\t\t\t\t// User specified limit, there's more content, but no truncation\n\t\t\t\t\t\t\t\tconst endLineDisplay = startLineDisplay + userLimitedLines - 1;\n\t\t\t\t\t\t\t\tconst remaining = allLines.length - (startLine + userLimitedLines);\n\t\t\t\t\t\t\t\tconst nextOffset = startLine + userLimitedLines + 1;\n\n\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\toutputText += `\\n\\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// No truncation, no user limit exceeded\n\t\t\t\t\t\t\t\toutputText = truncation.content;\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\n\t\t\t\t\t\t// Check if aborted after reading\n\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Clean up abort handler\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\n\t\t\t\t\t\tresolve({ content, details });\n\t\t\t\t\t} catch (error: any) {\n\t\t\t\t\t\t// Clean up abort handler\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\n\t\t\t\t\t\tif (!aborted) {\n\t\t\t\t\t\t\treject(error);\n\t\t\t\t\t\t}\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/core/tools/read.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAA6B,MAAM,qBAAqB,CAAC;AAShF,QAAA,MAAM,UAAU;;;;EAId,CAAC;AAMH,eAAO,MAAM,QAAQ,EAAE,SAAS,CAAC,OAAO,UAAU,CAoJjD,CAAC","sourcesContent":["import type { AgentTool, ImageContent, TextContent } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { constants } from \"fs\";\nimport { access, readFile } from \"fs/promises\";\nimport { resolve as resolvePath } from \"path\";\nimport { detectSupportedImageMimeTypeFromFile } from \"../../utils/mime.js\";\nimport { resolveReadPath } from \"./path-utils.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\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\ninterface ReadToolDetails {\n\ttruncation?: TruncationResult;\n}\n\nexport const readTool: AgentTool<typeof readSchema> = {\n\tname: \"read\",\n\tlabel: \"read\",\n\tdescription: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). 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(resolveReadPath(path));\n\n\t\treturn new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>(\n\t\t\t(resolve, reject) => {\n\t\t\t\t// Check if already aborted\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tlet aborted = false;\n\n\t\t\t\t// Set up abort handler\n\t\t\t\tconst onAbort = () => {\n\t\t\t\t\taborted = true;\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t};\n\n\t\t\t\tif (signal) {\n\t\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\n\t\t\t\t// Perform the read operation\n\t\t\t\t(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// Check if file exists\n\t\t\t\t\t\tawait access(absolutePath, constants.R_OK);\n\n\t\t\t\t\t\t// Check if aborted before reading\n\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);\n\n\t\t\t\t\t\t// Read the file based on type\n\t\t\t\t\t\tlet content: (TextContent | ImageContent)[];\n\t\t\t\t\t\tlet details: ReadToolDetails | undefined;\n\n\t\t\t\t\t\tif (mimeType) {\n\t\t\t\t\t\t\t// Read as image (binary)\n\t\t\t\t\t\t\tconst buffer = await readFile(absolutePath);\n\t\t\t\t\t\t\tconst base64 = buffer.toString(\"base64\");\n\n\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t{ type: \"text\", text: `Read image file [${mimeType}]` },\n\t\t\t\t\t\t\t\t{ type: \"image\", data: base64, mimeType },\n\t\t\t\t\t\t\t];\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Read as text\n\t\t\t\t\t\t\tconst textContent = await readFile(absolutePath, \"utf-8\");\n\t\t\t\t\t\t\tconst allLines = textContent.split(\"\\n\");\n\t\t\t\t\t\t\tconst totalFileLines = allLines.length;\n\n\t\t\t\t\t\t\t// Apply offset if specified (1-indexed to 0-indexed)\n\t\t\t\t\t\t\tconst startLine = offset ? Math.max(0, offset - 1) : 0;\n\t\t\t\t\t\t\tconst startLineDisplay = startLine + 1; // For display (1-indexed)\n\n\t\t\t\t\t\t\t// Check if offset is out of bounds\n\t\t\t\t\t\t\tif (startLine >= allLines.length) {\n\t\t\t\t\t\t\t\tthrow new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// If limit is specified by user, use it; otherwise we'll let truncateHead decide\n\t\t\t\t\t\t\tlet selectedContent: string;\n\t\t\t\t\t\t\tlet userLimitedLines: number | undefined;\n\t\t\t\t\t\t\tif (limit !== undefined) {\n\t\t\t\t\t\t\t\tconst endLine = Math.min(startLine + limit, allLines.length);\n\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine, endLine).join(\"\\n\");\n\t\t\t\t\t\t\t\tuserLimitedLines = endLine - startLine;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine).join(\"\\n\");\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Apply truncation (respects both line and byte limits)\n\t\t\t\t\t\t\tconst truncation = truncateHead(selectedContent);\n\n\t\t\t\t\t\t\tlet outputText: string;\n\n\t\t\t\t\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t\t\t\t\t// First line at offset exceeds 30KB - tell model to use bash\n\t\t\t\t\t\t\t\tconst firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], \"utf-8\"));\n\t\t\t\t\t\t\t\toutputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;\n\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t} else if (truncation.truncated) {\n\t\t\t\t\t\t\t\t// Truncation occurred - build actionable notice\n\t\t\t\t\t\t\t\tconst endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n\t\t\t\t\t\t\t\tconst nextOffset = endLineDisplay + 1;\n\n\t\t\t\t\t\t\t\toutputText = truncation.content;\n\n\t\t\t\t\t\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {\n\t\t\t\t\t\t\t\t// User specified limit, there's more content, but no truncation\n\t\t\t\t\t\t\t\tconst endLineDisplay = startLineDisplay + userLimitedLines - 1;\n\t\t\t\t\t\t\t\tconst remaining = allLines.length - (startLine + userLimitedLines);\n\t\t\t\t\t\t\t\tconst nextOffset = startLine + userLimitedLines + 1;\n\n\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\toutputText += `\\n\\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// No truncation, no user limit exceeded\n\t\t\t\t\t\t\t\toutputText = truncation.content;\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\n\t\t\t\t\t\t// Check if aborted after reading\n\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Clean up abort handler\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\n\t\t\t\t\t\tresolve({ content, details });\n\t\t\t\t\t} catch (error: any) {\n\t\t\t\t\t\t// Clean up abort handler\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\n\t\t\t\t\t\tif (!aborted) {\n\t\t\t\t\t\t\treject(error);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t},\n\t\t);\n\t},\n};\n"]}
@@ -1,26 +1,10 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { constants } from "fs";
3
3
  import { access, readFile } from "fs/promises";
4
- import { extname, resolve as resolvePath } from "path";
4
+ import { resolve as resolvePath } from "path";
5
+ import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js";
5
6
  import { resolveReadPath } from "./path-utils.js";
6
7
  import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead } from "./truncate.js";
7
- /**
8
- * Map of file extensions to MIME types for common image formats
9
- */
10
- const IMAGE_MIME_TYPES = {
11
- ".jpg": "image/jpeg",
12
- ".jpeg": "image/jpeg",
13
- ".png": "image/png",
14
- ".gif": "image/gif",
15
- ".webp": "image/webp",
16
- };
17
- /**
18
- * Check if a file is an image based on its extension
19
- */
20
- function isImageFile(filePath) {
21
- const ext = extname(filePath).toLowerCase();
22
- return IMAGE_MIME_TYPES[ext] || null;
23
- }
24
8
  const readSchema = Type.Object({
25
9
  path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
26
10
  offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
@@ -33,7 +17,6 @@ export const readTool = {
33
17
  parameters: readSchema,
34
18
  execute: async (_toolCallId, { path, offset, limit }, signal) => {
35
19
  const absolutePath = resolvePath(resolveReadPath(path));
36
- const mimeType = isImageFile(absolutePath);
37
20
  return new Promise((resolve, reject) => {
38
21
  // Check if already aborted
39
22
  if (signal?.aborted) {
@@ -58,6 +41,7 @@ export const readTool = {
58
41
  if (aborted) {
59
42
  return;
60
43
  }
44
+ const mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);
61
45
  // Read the file based on type
62
46
  let content;
63
47
  let details;
@@ -1 +1 @@
1
- {"version":3,"file":"read.js","sourceRoot":"","sources":["../../../src/core/tools/read.ts"],"names":[],"mappings":"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","sourcesContent":["import 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\";\nimport { resolveReadPath } from \"./path-utils.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\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\ninterface ReadToolDetails {\n\ttruncation?: TruncationResult;\n}\n\nexport const readTool: AgentTool<typeof readSchema> = {\n\tname: \"read\",\n\tlabel: \"read\",\n\tdescription: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). 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(resolveReadPath(path));\n\t\tconst mimeType = isImageFile(absolutePath);\n\n\t\treturn new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>(\n\t\t\t(resolve, reject) => {\n\t\t\t\t// Check if already aborted\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tlet aborted = false;\n\n\t\t\t\t// Set up abort handler\n\t\t\t\tconst onAbort = () => {\n\t\t\t\t\taborted = true;\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t};\n\n\t\t\t\tif (signal) {\n\t\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\n\t\t\t\t// Perform the read operation\n\t\t\t\t(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// Check if file exists\n\t\t\t\t\t\tawait access(absolutePath, constants.R_OK);\n\n\t\t\t\t\t\t// Check if aborted before reading\n\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Read the file based on type\n\t\t\t\t\t\tlet content: (TextContent | ImageContent)[];\n\t\t\t\t\t\tlet details: ReadToolDetails | undefined;\n\n\t\t\t\t\t\tif (mimeType) {\n\t\t\t\t\t\t\t// Read as image (binary)\n\t\t\t\t\t\t\tconst buffer = await readFile(absolutePath);\n\t\t\t\t\t\t\tconst base64 = buffer.toString(\"base64\");\n\n\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t{ type: \"text\", text: `Read image file [${mimeType}]` },\n\t\t\t\t\t\t\t\t{ type: \"image\", data: base64, mimeType },\n\t\t\t\t\t\t\t];\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Read as text\n\t\t\t\t\t\t\tconst textContent = await readFile(absolutePath, \"utf-8\");\n\t\t\t\t\t\t\tconst allLines = textContent.split(\"\\n\");\n\t\t\t\t\t\t\tconst totalFileLines = allLines.length;\n\n\t\t\t\t\t\t\t// Apply offset if specified (1-indexed to 0-indexed)\n\t\t\t\t\t\t\tconst startLine = offset ? Math.max(0, offset - 1) : 0;\n\t\t\t\t\t\t\tconst startLineDisplay = startLine + 1; // For display (1-indexed)\n\n\t\t\t\t\t\t\t// Check if offset is out of bounds\n\t\t\t\t\t\t\tif (startLine >= allLines.length) {\n\t\t\t\t\t\t\t\tthrow new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// If limit is specified by user, use it; otherwise we'll let truncateHead decide\n\t\t\t\t\t\t\tlet selectedContent: string;\n\t\t\t\t\t\t\tlet userLimitedLines: number | undefined;\n\t\t\t\t\t\t\tif (limit !== undefined) {\n\t\t\t\t\t\t\t\tconst endLine = Math.min(startLine + limit, allLines.length);\n\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine, endLine).join(\"\\n\");\n\t\t\t\t\t\t\t\tuserLimitedLines = endLine - startLine;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine).join(\"\\n\");\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Apply truncation (respects both line and byte limits)\n\t\t\t\t\t\t\tconst truncation = truncateHead(selectedContent);\n\n\t\t\t\t\t\t\tlet outputText: string;\n\n\t\t\t\t\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t\t\t\t\t// First line at offset exceeds 30KB - tell model to use bash\n\t\t\t\t\t\t\t\tconst firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], \"utf-8\"));\n\t\t\t\t\t\t\t\toutputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;\n\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t} else if (truncation.truncated) {\n\t\t\t\t\t\t\t\t// Truncation occurred - build actionable notice\n\t\t\t\t\t\t\t\tconst endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n\t\t\t\t\t\t\t\tconst nextOffset = endLineDisplay + 1;\n\n\t\t\t\t\t\t\t\toutputText = truncation.content;\n\n\t\t\t\t\t\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {\n\t\t\t\t\t\t\t\t// User specified limit, there's more content, but no truncation\n\t\t\t\t\t\t\t\tconst endLineDisplay = startLineDisplay + userLimitedLines - 1;\n\t\t\t\t\t\t\t\tconst remaining = allLines.length - (startLine + userLimitedLines);\n\t\t\t\t\t\t\t\tconst nextOffset = startLine + userLimitedLines + 1;\n\n\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\toutputText += `\\n\\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// No truncation, no user limit exceeded\n\t\t\t\t\t\t\t\toutputText = truncation.content;\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\n\t\t\t\t\t\t// Check if aborted after reading\n\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Clean up abort handler\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\n\t\t\t\t\t\tresolve({ content, details });\n\t\t\t\t\t} catch (error: any) {\n\t\t\t\t\t\t// Clean up abort handler\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\n\t\t\t\t\t\tif (!aborted) {\n\t\t\t\t\t\t\treject(error);\n\t\t\t\t\t\t}\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/core/tools/read.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentTool, ImageContent, TextContent } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { constants } from \"fs\";\nimport { access, readFile } from \"fs/promises\";\nimport { resolve as resolvePath } from \"path\";\nimport { detectSupportedImageMimeTypeFromFile } from \"../../utils/mime.js\";\nimport { resolveReadPath } from \"./path-utils.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\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\ninterface ReadToolDetails {\n\ttruncation?: TruncationResult;\n}\n\nexport const readTool: AgentTool<typeof readSchema> = {\n\tname: \"read\",\n\tlabel: \"read\",\n\tdescription: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). 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(resolveReadPath(path));\n\n\t\treturn new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>(\n\t\t\t(resolve, reject) => {\n\t\t\t\t// Check if already aborted\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tlet aborted = false;\n\n\t\t\t\t// Set up abort handler\n\t\t\t\tconst onAbort = () => {\n\t\t\t\t\taborted = true;\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t};\n\n\t\t\t\tif (signal) {\n\t\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\n\t\t\t\t// Perform the read operation\n\t\t\t\t(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// Check if file exists\n\t\t\t\t\t\tawait access(absolutePath, constants.R_OK);\n\n\t\t\t\t\t\t// Check if aborted before reading\n\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);\n\n\t\t\t\t\t\t// Read the file based on type\n\t\t\t\t\t\tlet content: (TextContent | ImageContent)[];\n\t\t\t\t\t\tlet details: ReadToolDetails | undefined;\n\n\t\t\t\t\t\tif (mimeType) {\n\t\t\t\t\t\t\t// Read as image (binary)\n\t\t\t\t\t\t\tconst buffer = await readFile(absolutePath);\n\t\t\t\t\t\t\tconst base64 = buffer.toString(\"base64\");\n\n\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t{ type: \"text\", text: `Read image file [${mimeType}]` },\n\t\t\t\t\t\t\t\t{ type: \"image\", data: base64, mimeType },\n\t\t\t\t\t\t\t];\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Read as text\n\t\t\t\t\t\t\tconst textContent = await readFile(absolutePath, \"utf-8\");\n\t\t\t\t\t\t\tconst allLines = textContent.split(\"\\n\");\n\t\t\t\t\t\t\tconst totalFileLines = allLines.length;\n\n\t\t\t\t\t\t\t// Apply offset if specified (1-indexed to 0-indexed)\n\t\t\t\t\t\t\tconst startLine = offset ? Math.max(0, offset - 1) : 0;\n\t\t\t\t\t\t\tconst startLineDisplay = startLine + 1; // For display (1-indexed)\n\n\t\t\t\t\t\t\t// Check if offset is out of bounds\n\t\t\t\t\t\t\tif (startLine >= allLines.length) {\n\t\t\t\t\t\t\t\tthrow new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// If limit is specified by user, use it; otherwise we'll let truncateHead decide\n\t\t\t\t\t\t\tlet selectedContent: string;\n\t\t\t\t\t\t\tlet userLimitedLines: number | undefined;\n\t\t\t\t\t\t\tif (limit !== undefined) {\n\t\t\t\t\t\t\t\tconst endLine = Math.min(startLine + limit, allLines.length);\n\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine, endLine).join(\"\\n\");\n\t\t\t\t\t\t\t\tuserLimitedLines = endLine - startLine;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine).join(\"\\n\");\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Apply truncation (respects both line and byte limits)\n\t\t\t\t\t\t\tconst truncation = truncateHead(selectedContent);\n\n\t\t\t\t\t\t\tlet outputText: string;\n\n\t\t\t\t\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t\t\t\t\t// First line at offset exceeds 30KB - tell model to use bash\n\t\t\t\t\t\t\t\tconst firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], \"utf-8\"));\n\t\t\t\t\t\t\t\toutputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;\n\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t} else if (truncation.truncated) {\n\t\t\t\t\t\t\t\t// Truncation occurred - build actionable notice\n\t\t\t\t\t\t\t\tconst endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n\t\t\t\t\t\t\t\tconst nextOffset = endLineDisplay + 1;\n\n\t\t\t\t\t\t\t\toutputText = truncation.content;\n\n\t\t\t\t\t\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {\n\t\t\t\t\t\t\t\t// User specified limit, there's more content, but no truncation\n\t\t\t\t\t\t\t\tconst endLineDisplay = startLineDisplay + userLimitedLines - 1;\n\t\t\t\t\t\t\t\tconst remaining = allLines.length - (startLine + userLimitedLines);\n\t\t\t\t\t\t\t\tconst nextOffset = startLine + userLimitedLines + 1;\n\n\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\toutputText += `\\n\\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// No truncation, no user limit exceeded\n\t\t\t\t\t\t\t\toutputText = truncation.content;\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\n\t\t\t\t\t\t// Check if aborted after reading\n\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Clean up abort handler\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\n\t\t\t\t\t\tresolve({ content, details });\n\t\t\t\t\t} catch (error: any) {\n\t\t\t\t\t\t// Clean up abort handler\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\n\t\t\t\t\t\tif (!aborted) {\n\t\t\t\t\t\t\treject(error);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t},\n\t\t);\n\t},\n};\n"]}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- export { Type } from "@sinclair/typebox";
2
1
  export { AgentSession, type AgentSessionConfig, type AgentSessionEvent, type AgentSessionEventListener, type CompactionResult, type ModelCycleResult, type PromptOptions, type SessionStats, } from "./core/agent-session.js";
3
2
  export { type CutPointResult, calculateContextTokens, compact, DEFAULT_COMPACTION_SETTINGS, estimateTokens, findCutPoint, findTurnStartIndex, generateSummary, getLastAssistantUsage, shouldCompact, } from "./core/compaction.js";
4
3
  export type { CustomAgentTool, CustomToolFactory, CustomToolsLoadResult, ExecResult, LoadedCustomTool, RenderResultOptions, SessionEvent as ToolSessionEvent, ToolAPI, ToolUIContext, } from "./core/custom-tools/index.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EACN,YAAY,EACZ,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,yBAAyB,EAC9B,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,YAAY,GACjB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACN,KAAK,cAAc,EACnB,sBAAsB,EACtB,OAAO,EACP,2BAA2B,EAC3B,cAAc,EACd,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,aAAa,GACb,MAAM,sBAAsB,CAAC;AAE9B,YAAY,EACX,eAAe,EACf,iBAAiB,EACjB,qBAAqB,EACrB,UAAU,EACV,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,IAAI,gBAAgB,EAChC,OAAO,EACP,aAAa,GACb,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,0BAA0B,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAE3F,YAAY,EACX,aAAa,EACb,eAAe,EACf,WAAW,EACX,iBAAiB,EACjB,OAAO,EACP,SAAS,EACT,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,eAAe,EACf,qBAAqB,EACrB,YAAY,EACZ,cAAc,GACd,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EACN,KAAK,eAAe,EACpB,oBAAoB,EACpB,wBAAwB,EACxB,KAAK,aAAa,EAClB,sBAAsB,EACtB,KAAK,gBAAgB,EACrB,mBAAmB,EACnB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,cAAc,EACd,KAAK,mBAAmB,EACxB,cAAc,EACd,cAAc,EACd,KAAK,wBAAwB,GAC7B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACN,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,eAAe,GACf,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EACN,qBAAqB,EACrB,KAAK,wBAAwB,EAC7B,UAAU,EACV,iBAAiB,EACjB,KAAK,KAAK,EACV,KAAK,gBAAgB,GACrB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAG7F,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC","sourcesContent":["// Core session management\n\n// Re-export Type from typebox for custom tools\nexport { Type } from \"@sinclair/typebox\";\nexport {\n\tAgentSession,\n\ttype AgentSessionConfig,\n\ttype AgentSessionEvent,\n\ttype AgentSessionEventListener,\n\ttype CompactionResult,\n\ttype ModelCycleResult,\n\ttype PromptOptions,\n\ttype SessionStats,\n} from \"./core/agent-session.js\";\n// Compaction\nexport {\n\ttype CutPointResult,\n\tcalculateContextTokens,\n\tcompact,\n\tDEFAULT_COMPACTION_SETTINGS,\n\testimateTokens,\n\tfindCutPoint,\n\tfindTurnStartIndex,\n\tgenerateSummary,\n\tgetLastAssistantUsage,\n\tshouldCompact,\n} from \"./core/compaction.js\";\n// Custom tools\nexport type {\n\tCustomAgentTool,\n\tCustomToolFactory,\n\tCustomToolsLoadResult,\n\tExecResult,\n\tLoadedCustomTool,\n\tRenderResultOptions,\n\tSessionEvent as ToolSessionEvent,\n\tToolAPI,\n\tToolUIContext,\n} from \"./core/custom-tools/index.js\";\nexport { discoverAndLoadCustomTools, loadCustomTools } from \"./core/custom-tools/index.js\";\n// Hook system types\nexport type {\n\tAgentEndEvent,\n\tAgentStartEvent,\n\tBranchEvent,\n\tBranchEventResult,\n\tHookAPI,\n\tHookEvent,\n\tHookEventContext,\n\tHookFactory,\n\tHookUIContext,\n\tSessionEvent,\n\tToolCallEvent,\n\tToolCallEventResult,\n\tToolResultEvent,\n\tToolResultEventResult,\n\tTurnEndEvent,\n\tTurnStartEvent,\n} from \"./core/hooks/index.js\";\nexport { messageTransformer } from \"./core/messages.js\";\nexport {\n\ttype CompactionEntry,\n\tcreateSummaryMessage,\n\tgetLatestCompactionEntry,\n\ttype LoadedSession,\n\tloadSessionFromEntries,\n\ttype ModelChangeEntry,\n\tparseSessionEntries,\n\ttype SessionEntry,\n\ttype SessionHeader,\n\tSessionManager,\n\ttype SessionMessageEntry,\n\tSUMMARY_PREFIX,\n\tSUMMARY_SUFFIX,\n\ttype ThinkingLevelChangeEntry,\n} from \"./core/session-manager.js\";\nexport {\n\ttype CompactionSettings,\n\ttype RetrySettings,\n\ttype Settings,\n\tSettingsManager,\n} from \"./core/settings-manager.js\";\n// Skills\nexport {\n\tformatSkillsForPrompt,\n\ttype LoadSkillsFromDirOptions,\n\tloadSkills,\n\tloadSkillsFromDir,\n\ttype Skill,\n\ttype SkillFrontmatter,\n} from \"./core/skills.js\";\n// Tools\nexport { bashTool, codingTools, editTool, readTool, writeTool } from \"./core/tools/index.js\";\n\n// Main entry point\nexport { main } from \"./main.js\";\n"]}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACN,YAAY,EACZ,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,yBAAyB,EAC9B,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,YAAY,GACjB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACN,KAAK,cAAc,EACnB,sBAAsB,EACtB,OAAO,EACP,2BAA2B,EAC3B,cAAc,EACd,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,aAAa,GACb,MAAM,sBAAsB,CAAC;AAE9B,YAAY,EACX,eAAe,EACf,iBAAiB,EACjB,qBAAqB,EACrB,UAAU,EACV,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,IAAI,gBAAgB,EAChC,OAAO,EACP,aAAa,GACb,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,0BAA0B,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAE3F,YAAY,EACX,aAAa,EACb,eAAe,EACf,WAAW,EACX,iBAAiB,EACjB,OAAO,EACP,SAAS,EACT,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,eAAe,EACf,qBAAqB,EACrB,YAAY,EACZ,cAAc,GACd,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EACN,KAAK,eAAe,EACpB,oBAAoB,EACpB,wBAAwB,EACxB,KAAK,aAAa,EAClB,sBAAsB,EACtB,KAAK,gBAAgB,EACrB,mBAAmB,EACnB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,cAAc,EACd,KAAK,mBAAmB,EACxB,cAAc,EACd,cAAc,EACd,KAAK,wBAAwB,GAC7B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACN,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,eAAe,GACf,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EACN,qBAAqB,EACrB,KAAK,wBAAwB,EAC7B,UAAU,EACV,iBAAiB,EACjB,KAAK,KAAK,EACV,KAAK,gBAAgB,GACrB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAG7F,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC","sourcesContent":["// Core session management\nexport {\n\tAgentSession,\n\ttype AgentSessionConfig,\n\ttype AgentSessionEvent,\n\ttype AgentSessionEventListener,\n\ttype CompactionResult,\n\ttype ModelCycleResult,\n\ttype PromptOptions,\n\ttype SessionStats,\n} from \"./core/agent-session.js\";\n// Compaction\nexport {\n\ttype CutPointResult,\n\tcalculateContextTokens,\n\tcompact,\n\tDEFAULT_COMPACTION_SETTINGS,\n\testimateTokens,\n\tfindCutPoint,\n\tfindTurnStartIndex,\n\tgenerateSummary,\n\tgetLastAssistantUsage,\n\tshouldCompact,\n} from \"./core/compaction.js\";\n// Custom tools\nexport type {\n\tCustomAgentTool,\n\tCustomToolFactory,\n\tCustomToolsLoadResult,\n\tExecResult,\n\tLoadedCustomTool,\n\tRenderResultOptions,\n\tSessionEvent as ToolSessionEvent,\n\tToolAPI,\n\tToolUIContext,\n} from \"./core/custom-tools/index.js\";\nexport { discoverAndLoadCustomTools, loadCustomTools } from \"./core/custom-tools/index.js\";\n// Hook system types\nexport type {\n\tAgentEndEvent,\n\tAgentStartEvent,\n\tBranchEvent,\n\tBranchEventResult,\n\tHookAPI,\n\tHookEvent,\n\tHookEventContext,\n\tHookFactory,\n\tHookUIContext,\n\tSessionEvent,\n\tToolCallEvent,\n\tToolCallEventResult,\n\tToolResultEvent,\n\tToolResultEventResult,\n\tTurnEndEvent,\n\tTurnStartEvent,\n} from \"./core/hooks/index.js\";\nexport { messageTransformer } from \"./core/messages.js\";\nexport {\n\ttype CompactionEntry,\n\tcreateSummaryMessage,\n\tgetLatestCompactionEntry,\n\ttype LoadedSession,\n\tloadSessionFromEntries,\n\ttype ModelChangeEntry,\n\tparseSessionEntries,\n\ttype SessionEntry,\n\ttype SessionHeader,\n\tSessionManager,\n\ttype SessionMessageEntry,\n\tSUMMARY_PREFIX,\n\tSUMMARY_SUFFIX,\n\ttype ThinkingLevelChangeEntry,\n} from \"./core/session-manager.js\";\nexport {\n\ttype CompactionSettings,\n\ttype RetrySettings,\n\ttype Settings,\n\tSettingsManager,\n} from \"./core/settings-manager.js\";\n// Skills\nexport {\n\tformatSkillsForPrompt,\n\ttype LoadSkillsFromDirOptions,\n\tloadSkills,\n\tloadSkillsFromDir,\n\ttype Skill,\n\ttype SkillFrontmatter,\n} from \"./core/skills.js\";\n// Tools\nexport { bashTool, codingTools, editTool, readTool, writeTool } from \"./core/tools/index.js\";\n\n// Main entry point\nexport { main } from \"./main.js\";\n"]}
package/dist/index.js CHANGED
@@ -1,6 +1,4 @@
1
1
  // Core session management
2
- // Re-export Type from typebox for custom tools
3
- export { Type } from "@sinclair/typebox";
4
2
  export { AgentSession, } from "./core/agent-session.js";
5
3
  // Compaction
6
4
  export { calculateContextTokens, compact, DEFAULT_COMPACTION_SETTINGS, estimateTokens, findCutPoint, findTurnStartIndex, generateSummary, getLastAssistantUsage, shouldCompact, } from "./core/compaction.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,0BAA0B;AAE1B,+CAA+C;AAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EACN,YAAY,GAQZ,MAAM,yBAAyB,CAAC;AACjC,aAAa;AACb,OAAO,EAEN,sBAAsB,EACtB,OAAO,EACP,2BAA2B,EAC3B,cAAc,EACd,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,aAAa,GACb,MAAM,sBAAsB,CAAC;AAa9B,OAAO,EAAE,0BAA0B,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAoB3F,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAEN,oBAAoB,EACpB,wBAAwB,EAExB,sBAAsB,EAEtB,mBAAmB,EAGnB,cAAc,EAEd,cAAc,EACd,cAAc,GAEd,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAIN,eAAe,GACf,MAAM,4BAA4B,CAAC;AACpC,SAAS;AACT,OAAO,EACN,qBAAqB,EAErB,UAAU,EACV,iBAAiB,GAGjB,MAAM,kBAAkB,CAAC;AAC1B,QAAQ;AACR,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAE7F,mBAAmB;AACnB,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC","sourcesContent":["// Core session management\n\n// Re-export Type from typebox for custom tools\nexport { Type } from \"@sinclair/typebox\";\nexport {\n\tAgentSession,\n\ttype AgentSessionConfig,\n\ttype AgentSessionEvent,\n\ttype AgentSessionEventListener,\n\ttype CompactionResult,\n\ttype ModelCycleResult,\n\ttype PromptOptions,\n\ttype SessionStats,\n} from \"./core/agent-session.js\";\n// Compaction\nexport {\n\ttype CutPointResult,\n\tcalculateContextTokens,\n\tcompact,\n\tDEFAULT_COMPACTION_SETTINGS,\n\testimateTokens,\n\tfindCutPoint,\n\tfindTurnStartIndex,\n\tgenerateSummary,\n\tgetLastAssistantUsage,\n\tshouldCompact,\n} from \"./core/compaction.js\";\n// Custom tools\nexport type {\n\tCustomAgentTool,\n\tCustomToolFactory,\n\tCustomToolsLoadResult,\n\tExecResult,\n\tLoadedCustomTool,\n\tRenderResultOptions,\n\tSessionEvent as ToolSessionEvent,\n\tToolAPI,\n\tToolUIContext,\n} from \"./core/custom-tools/index.js\";\nexport { discoverAndLoadCustomTools, loadCustomTools } from \"./core/custom-tools/index.js\";\n// Hook system types\nexport type {\n\tAgentEndEvent,\n\tAgentStartEvent,\n\tBranchEvent,\n\tBranchEventResult,\n\tHookAPI,\n\tHookEvent,\n\tHookEventContext,\n\tHookFactory,\n\tHookUIContext,\n\tSessionEvent,\n\tToolCallEvent,\n\tToolCallEventResult,\n\tToolResultEvent,\n\tToolResultEventResult,\n\tTurnEndEvent,\n\tTurnStartEvent,\n} from \"./core/hooks/index.js\";\nexport { messageTransformer } from \"./core/messages.js\";\nexport {\n\ttype CompactionEntry,\n\tcreateSummaryMessage,\n\tgetLatestCompactionEntry,\n\ttype LoadedSession,\n\tloadSessionFromEntries,\n\ttype ModelChangeEntry,\n\tparseSessionEntries,\n\ttype SessionEntry,\n\ttype SessionHeader,\n\tSessionManager,\n\ttype SessionMessageEntry,\n\tSUMMARY_PREFIX,\n\tSUMMARY_SUFFIX,\n\ttype ThinkingLevelChangeEntry,\n} from \"./core/session-manager.js\";\nexport {\n\ttype CompactionSettings,\n\ttype RetrySettings,\n\ttype Settings,\n\tSettingsManager,\n} from \"./core/settings-manager.js\";\n// Skills\nexport {\n\tformatSkillsForPrompt,\n\ttype LoadSkillsFromDirOptions,\n\tloadSkills,\n\tloadSkillsFromDir,\n\ttype Skill,\n\ttype SkillFrontmatter,\n} from \"./core/skills.js\";\n// Tools\nexport { bashTool, codingTools, editTool, readTool, writeTool } from \"./core/tools/index.js\";\n\n// Main entry point\nexport { main } from \"./main.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,0BAA0B;AAC1B,OAAO,EACN,YAAY,GAQZ,MAAM,yBAAyB,CAAC;AACjC,aAAa;AACb,OAAO,EAEN,sBAAsB,EACtB,OAAO,EACP,2BAA2B,EAC3B,cAAc,EACd,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,aAAa,GACb,MAAM,sBAAsB,CAAC;AAa9B,OAAO,EAAE,0BAA0B,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAoB3F,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAEN,oBAAoB,EACpB,wBAAwB,EAExB,sBAAsB,EAEtB,mBAAmB,EAGnB,cAAc,EAEd,cAAc,EACd,cAAc,GAEd,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAIN,eAAe,GACf,MAAM,4BAA4B,CAAC;AACpC,SAAS;AACT,OAAO,EACN,qBAAqB,EAErB,UAAU,EACV,iBAAiB,GAGjB,MAAM,kBAAkB,CAAC;AAC1B,QAAQ;AACR,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAE7F,mBAAmB;AACnB,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC","sourcesContent":["// Core session management\nexport {\n\tAgentSession,\n\ttype AgentSessionConfig,\n\ttype AgentSessionEvent,\n\ttype AgentSessionEventListener,\n\ttype CompactionResult,\n\ttype ModelCycleResult,\n\ttype PromptOptions,\n\ttype SessionStats,\n} from \"./core/agent-session.js\";\n// Compaction\nexport {\n\ttype CutPointResult,\n\tcalculateContextTokens,\n\tcompact,\n\tDEFAULT_COMPACTION_SETTINGS,\n\testimateTokens,\n\tfindCutPoint,\n\tfindTurnStartIndex,\n\tgenerateSummary,\n\tgetLastAssistantUsage,\n\tshouldCompact,\n} from \"./core/compaction.js\";\n// Custom tools\nexport type {\n\tCustomAgentTool,\n\tCustomToolFactory,\n\tCustomToolsLoadResult,\n\tExecResult,\n\tLoadedCustomTool,\n\tRenderResultOptions,\n\tSessionEvent as ToolSessionEvent,\n\tToolAPI,\n\tToolUIContext,\n} from \"./core/custom-tools/index.js\";\nexport { discoverAndLoadCustomTools, loadCustomTools } from \"./core/custom-tools/index.js\";\n// Hook system types\nexport type {\n\tAgentEndEvent,\n\tAgentStartEvent,\n\tBranchEvent,\n\tBranchEventResult,\n\tHookAPI,\n\tHookEvent,\n\tHookEventContext,\n\tHookFactory,\n\tHookUIContext,\n\tSessionEvent,\n\tToolCallEvent,\n\tToolCallEventResult,\n\tToolResultEvent,\n\tToolResultEventResult,\n\tTurnEndEvent,\n\tTurnStartEvent,\n} from \"./core/hooks/index.js\";\nexport { messageTransformer } from \"./core/messages.js\";\nexport {\n\ttype CompactionEntry,\n\tcreateSummaryMessage,\n\tgetLatestCompactionEntry,\n\ttype LoadedSession,\n\tloadSessionFromEntries,\n\ttype ModelChangeEntry,\n\tparseSessionEntries,\n\ttype SessionEntry,\n\ttype SessionHeader,\n\tSessionManager,\n\ttype SessionMessageEntry,\n\tSUMMARY_PREFIX,\n\tSUMMARY_SUFFIX,\n\ttype ThinkingLevelChangeEntry,\n} from \"./core/session-manager.js\";\nexport {\n\ttype CompactionSettings,\n\ttype RetrySettings,\n\ttype Settings,\n\tSettingsManager,\n} from \"./core/settings-manager.js\";\n// Skills\nexport {\n\tformatSkillsForPrompt,\n\ttype LoadSkillsFromDirOptions,\n\tloadSkills,\n\tloadSkillsFromDir,\n\ttype Skill,\n\ttype SkillFrontmatter,\n} from \"./core/skills.js\";\n// Tools\nexport { bashTool, codingTools, editTool, readTool, writeTool } from \"./core/tools/index.js\";\n\n// Main entry point\nexport { main } from \"./main.js\";\n"]}