@zaly/agent 0.1.1 → 0.3.0

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.
Files changed (57) hide show
  1. package/dist/{agent-BRENvuvx.mjs → agent-GXRF7DUP.mjs} +102 -33
  2. package/dist/agent-GXRF7DUP.mjs.map +1 -0
  3. package/dist/{bash-DSy03sAj.mjs → bash-CB_VXpyd.mjs} +2 -2
  4. package/dist/{bash-DSy03sAj.mjs.map → bash-CB_VXpyd.mjs.map} +1 -1
  5. package/dist/{compactions-HFjEgrv3.mjs → compactions-92bMlcxJ.mjs} +25 -15
  6. package/dist/compactions-92bMlcxJ.mjs.map +1 -0
  7. package/dist/{edit-DT672zIy.mjs → edit-gkb6MCW-.mjs} +2 -2
  8. package/dist/{edit-DT672zIy.mjs.map → edit-gkb6MCW-.mjs.map} +1 -1
  9. package/dist/{fetch-BrtIqrid.mjs → fetch-D_GzL2yy.mjs} +4 -4
  10. package/dist/{fetch-BrtIqrid.mjs.map → fetch-D_GzL2yy.mjs.map} +1 -1
  11. package/dist/{find-B-vvdlHu.mjs → find-DlrYUTXT.mjs} +2 -2
  12. package/dist/{find-B-vvdlHu.mjs.map → find-DlrYUTXT.mjs.map} +1 -1
  13. package/dist/{grep-rvZ5S319.mjs → grep-BI8Rou2q.mjs} +2 -2
  14. package/dist/{grep-rvZ5S319.mjs.map → grep-BI8Rou2q.mjs.map} +1 -1
  15. package/dist/{html-VrAsoGBK.mjs → html-B-Lr-9W6.mjs} +9457 -4826
  16. package/dist/html-B-Lr-9W6.mjs.map +1 -0
  17. package/dist/{index-node-esm-BwaOzhXB.mjs → index-node-esm-75XSYyIw.mjs} +16 -13
  18. package/dist/index-node-esm-75XSYyIw.mjs.map +1 -0
  19. package/dist/index.d.mts +21 -6
  20. package/dist/index.d.mts.map +1 -1
  21. package/dist/index.mjs +2 -2
  22. package/dist/{manager-1zEE5WiA.mjs → manager-CEociHy_.mjs} +2 -2
  23. package/dist/{manager-1zEE5WiA.mjs.map → manager-CEociHy_.mjs.map} +1 -1
  24. package/dist/{masker-DEgR_ntk.mjs → masker-DWuL2Fjs.mjs} +16 -5
  25. package/dist/{masker-DEgR_ntk.mjs.map → masker-DWuL2Fjs.mjs.map} +1 -1
  26. package/dist/{read-C5keKntU.mjs → read-DLtNq4XJ.mjs} +2 -2
  27. package/dist/{read-C5keKntU.mjs.map → read-DLtNq4XJ.mjs.map} +1 -1
  28. package/dist/{registry-CntkeXlo.mjs → registry-7mJQhyBA.mjs} +3 -3
  29. package/dist/{registry-CntkeXlo.mjs.map → registry-7mJQhyBA.mjs.map} +1 -1
  30. package/dist/{rolldown-runtime-DYf_mQCB.mjs → rolldown-runtime-BVN1G_yT.mjs} +4 -4
  31. package/dist/{session-C8zqqbPE.mjs → session-C5ra-Hi1.mjs} +80 -45
  32. package/dist/session-C5ra-Hi1.mjs.map +1 -0
  33. package/dist/{session-Cq52cam5.d.mts → session-WtXqtAaq.d.mts} +19 -10
  34. package/dist/session-WtXqtAaq.d.mts.map +1 -0
  35. package/dist/session.d.mts +1 -1
  36. package/dist/session.mjs +1 -1
  37. package/dist/{src-C04Qk9KE.mjs → src-Di-H3Oya.mjs} +71 -41
  38. package/dist/src-Di-H3Oya.mjs.map +1 -0
  39. package/dist/{subagent-BbcxLzgC.mjs → subagent-BOK4GFd4.mjs} +3 -3
  40. package/dist/{subagent-BbcxLzgC.mjs.map → subagent-BOK4GFd4.mjs.map} +1 -1
  41. package/dist/{tasks-Dgsiuh0Q.mjs → tasks-BUFqFoh6.mjs} +2 -2
  42. package/dist/{tasks-Dgsiuh0Q.mjs.map → tasks-BUFqFoh6.mjs.map} +1 -1
  43. package/dist/{tasks-D76WXWr2.mjs → tasks-CkDWJk_3.mjs} +4 -4
  44. package/dist/tasks-CkDWJk_3.mjs.map +1 -0
  45. package/dist/{tools-DmoUYORG.mjs → tools-Ca7VsMg3.mjs} +2 -2
  46. package/dist/{tools-DmoUYORG.mjs.map → tools-Ca7VsMg3.mjs.map} +1 -1
  47. package/dist/{write-BRQFfWAn.mjs → write-y93TC5ng.mjs} +2 -2
  48. package/dist/{write-BRQFfWAn.mjs.map → write-y93TC5ng.mjs.map} +1 -1
  49. package/package.json +11 -11
  50. package/dist/agent-BRENvuvx.mjs.map +0 -1
  51. package/dist/compactions-HFjEgrv3.mjs.map +0 -1
  52. package/dist/html-VrAsoGBK.mjs.map +0 -1
  53. package/dist/index-node-esm-BwaOzhXB.mjs.map +0 -1
  54. package/dist/session-C8zqqbPE.mjs.map +0 -1
  55. package/dist/session-Cq52cam5.d.mts.map +0 -1
  56. package/dist/src-C04Qk9KE.mjs.map +0 -1
  57. package/dist/tasks-D76WXWr2.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"edit-DT672zIy.mjs","names":[],"sources":["../src/tools/edit.ts"],"sourcesContent":["import type { ToolContext } from \"@zaly/ai\"\nimport type { FileMeta } from \"./read.ts\"\n\nimport { AiError, defineTool } from \"@zaly/ai\"\nimport { normPath } from \"@zaly/shared\"\nimport { detectEol, normalizeEol } from \"@zaly/shared/text\"\nimport { readFile, stat, writeFile } from \"node:fs/promises\"\nimport { Type } from \"typebox\"\n\nexport type EditTool = typeof editTool\nexport type EditToolMeta = FileMeta & {\n /** File content before any edit was applied. */\n original: string\n /** File content after all edits were applied — exactly what's now on disk. */\n content: string\n}\n\n/**\n * Apply one or more exact-text replacements to a file.\n *\n * Semantics:\n * - All `oldText` matches are resolved against the *original* file\n * content. Edits are not applied sequentially — so edit #1's\n * `newText` cannot accidentally produce a match for edit #2's\n * `oldText`.\n * - Each `oldText` must occur exactly once in the file (uniqueness\n * forces the model to disambiguate via context rather than guessing).\n * - Edits must not overlap. Nearby changes should be combined into a\n * single edit covering the surrounding block.\n * - The file is rewritten atomically: either every edit applies\n * cleanly or none do.\n *\n * Use `read` first to inspect the file. Keep `oldText` minimal but\n * uniquely identifying. For full-file rewrites, use `write` instead.\n */\n// oxlint-disable-next-line sort-keys -- semantic field order: name, desc, params, call\nexport const editTool = defineTool({\n name: \"edit\",\n desc:\n \"Apply one or more exact-text replacements to a file. All matches are resolved \" +\n \"against the original content (not sequentially), each `oldText` must be unique, \" +\n \"and edits must not overlap. Atomic: all-or-nothing.\",\n // oxlint-disable-next-line sort-keys -- semantic param order\n params: Type.Object({\n path: Type.String({ description: \"Path to the file. Absolute or cwd-relative.\" }),\n edits: Type.Array(\n // oxlint-disable-next-line sort-keys -- semantic order: oldText then newText\n Type.Object({\n oldText: Type.String({ description: \"Exact text to find. Must occur once.\" }),\n newText: Type.String({ description: \"Replacement text.\" }),\n }),\n { description: \"Edits to apply atomically.\", minItems: 1 }\n ),\n }),\n\n async preflight(args, ctx: ToolContext<EditToolMeta>) {\n const path = normPath(ctx.cwd, args.path)\n // Edits ride the `write` scope — they're mutations.\n await ctx.need?.(\"write\", path)\n },\n\n async call(\n args,\n ctx: ToolContext<EditToolMeta>\n ): Promise<{ ok: true; path: string; bytes: number; lines: number; edits: number }> {\n const path = normPath(ctx.cwd, args.path)\n\n const original = await readFile(path, \"utf8\").catch((error: unknown) => {\n throw new AiError({\n cause: error,\n code: \"FILE_NOT_FOUND\",\n message: `cannot edit ${path}: file not found`,\n })\n })\n\n const updated = applyEdits(original, args.edits, path)\n await writeFile(path, updated, \"utf8\")\n const fstat = await stat(path)\n ctx.meta = { content: updated, kind: \"edit\", mtime: fstat.mtimeMs, original, path }\n\n const bytes = Buffer.byteLength(updated, \"utf8\")\n const lines = countLines(updated)\n return { bytes, edits: args.edits.length, lines, ok: true, path }\n },\n})\n\ninterface EditSpec {\n oldText: string\n newText: string\n}\n\n/** Resolve all edit positions against `original`, validate uniqueness +\n * non-overlap, then splice the replacements in.\n *\n * Line-ending fidelity: the model always emits LF, but the file may be\n * CRLF. We detect the file's eol once, then normalize each `oldText` /\n * `newText` to that eol before matching/splicing. The result has the\n * same line-ending style as the original file. */\nfunction applyEdits(original: string, edits: readonly EditSpec[], path: string): string {\n const eol = detectEol({ path, text: original })\n // 1. Locate every oldText. Reject not-found and non-unique up-front so\n // the model gets a precise error per failing edit rather than a\n // \"patch failed at line N\" mystery.\n const matches: { start: number; end: number; newText: string; spec: EditSpec }[] = []\n for (const [i, edit] of edits.entries()) {\n const oldText = normalizeEol(edit.oldText, { eol })\n const newText = normalizeEol(edit.newText, { eol })\n if (oldText === \"\") {\n throw new AiError({\n code: \"EMPTY_OLD_TEXT\",\n data: { editIndex: i },\n message: `edit #${i}: oldText is empty (use \\`write\\` to create or fully replace a file)`,\n })\n }\n const first = original.indexOf(oldText)\n if (first === -1) {\n throw new AiError({\n code: \"NOT_FOUND\",\n data: { editIndex: i, path, snippet: preview(oldText) },\n message: `edit #${i}: oldText not found in ${path}`,\n })\n }\n const next = original.indexOf(oldText, first + 1)\n if (next !== -1) {\n throw new AiError({\n code: \"NOT_UNIQUE\",\n data: { editIndex: i, occurrences: countOccurrences(original, oldText), path },\n message:\n `edit #${i}: oldText matches multiple locations in ${path}. ` +\n `Add surrounding context to make it unique.`,\n })\n }\n matches.push({\n end: first + oldText.length,\n newText,\n spec: edit,\n start: first,\n })\n }\n\n // 2. Sort by start; verify no overlap. Equal start positions are\n // impossible because oldText is unique.\n matches.sort((a, b) => a.start - b.start)\n for (let i = 1; i < matches.length; i++) {\n if (matches[i].start < matches[i - 1].end) {\n throw new AiError({\n code: \"OVERLAP\",\n data: { a: matches[i - 1], b: matches[i] },\n message:\n `edits overlap in ${path}. ` +\n `Combine adjacent changes into a single edit covering both.`,\n })\n }\n }\n\n // 3. Splice. Walk the original and emit untouched-prefix + replacement\n // chunks; cheaper than N independent string replacements.\n const out: string[] = []\n let cursor = 0\n for (const m of matches) {\n out.push(original.slice(cursor, m.start), m.newText)\n cursor = m.end\n }\n out.push(original.slice(cursor))\n return out.join(\"\")\n}\n\n/** Count lines the way an editor would — non-empty trailing line counts;\n * a trailing newline doesn't add a phantom blank line. */\nfunction countLines(content: string): number {\n if (content === \"\") return 0\n let n = 1\n for (const c of content) if (c === \"\\n\") n++\n if (content.endsWith(\"\\n\")) n--\n return n\n}\n\nfunction countOccurrences(haystack: string, needle: string): number {\n let n = 0\n let from = 0\n for (;;) {\n const i = haystack.indexOf(needle, from)\n if (i === -1) return n\n n++\n from = i + needle.length\n }\n}\n\nfunction preview(text: string): string {\n const trimmed = text.trim().replaceAll(/\\s+/g, \" \")\n return trimmed.length > 80 ? `${trimmed.slice(0, 79)}…` : trimmed\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAoCA,MAAa,WAAW,WAAW;CACjC,MAAM;CACN,MACE;CAIF,QAAQ,KAAK,OAAO;EAClB,MAAM,KAAK,OAAO,EAAE,aAAa,8CAA8C,CAAC;EAChF,OAAO,KAAK,MAEV,KAAK,OAAO;GACV,SAAS,KAAK,OAAO,EAAE,aAAa,uCAAuC,CAAC;GAC5E,SAAS,KAAK,OAAO,EAAE,aAAa,oBAAoB,CAAC;EAC3D,CAAC,GACD;GAAE,aAAa;GAA8B,UAAU;EAAE,CAC3D;CACF,CAAC;CAED,MAAM,UAAU,MAAM,KAAgC;EACpD,MAAM,OAAO,SAAS,IAAI,KAAK,KAAK,IAAI;EAExC,MAAM,IAAI,OAAO,SAAS,IAAI;CAChC;CAEA,MAAM,KACJ,MACA,KACkF;EAClF,MAAM,OAAO,SAAS,IAAI,KAAK,KAAK,IAAI;EAExC,MAAM,WAAW,MAAM,SAAS,MAAM,MAAM,CAAC,CAAC,OAAO,UAAmB;GACtE,MAAM,IAAI,QAAQ;IAChB,OAAO;IACP,MAAM;IACN,SAAS,eAAe,KAAK;GAC/B,CAAC;EACH,CAAC;EAED,MAAM,UAAU,WAAW,UAAU,KAAK,OAAO,IAAI;EACrD,MAAM,UAAU,MAAM,SAAS,MAAM;EAErC,IAAI,OAAO;GAAE,SAAS;GAAS,MAAM;GAAQ,QAAO,MADhC,KAAK,IAAI,EAAA,CAC6B;GAAS;GAAU;EAAK;EAElF,MAAM,QAAQ,OAAO,WAAW,SAAS,MAAM;EAC/C,MAAM,QAAQ,WAAW,OAAO;EAChC,OAAO;GAAE;GAAO,OAAO,KAAK,MAAM;GAAQ;GAAO,IAAI;GAAM;EAAK;CAClE;AACF,CAAC;;;;;;;;AAcD,SAAS,WAAW,UAAkB,OAA4B,MAAsB;CACtF,MAAM,MAAM,UAAU;EAAE;EAAM,MAAM;CAAS,CAAC;CAI9C,MAAM,UAA6E,CAAC;CACpF,KAAK,MAAM,CAAC,GAAG,SAAS,MAAM,QAAQ,GAAG;EACvC,MAAM,UAAU,aAAa,KAAK,SAAS,EAAE,IAAI,CAAC;EAClD,MAAM,UAAU,aAAa,KAAK,SAAS,EAAE,IAAI,CAAC;EAClD,IAAI,YAAY,IACd,MAAM,IAAI,QAAQ;GAChB,MAAM;GACN,MAAM,EAAE,WAAW,EAAE;GACrB,SAAS,SAAS,EAAE;EACtB,CAAC;EAEH,MAAM,QAAQ,SAAS,QAAQ,OAAO;EACtC,IAAI,UAAU,IACZ,MAAM,IAAI,QAAQ;GAChB,MAAM;GACN,MAAM;IAAE,WAAW;IAAG;IAAM,SAAS,QAAQ,OAAO;GAAE;GACtD,SAAS,SAAS,EAAE,yBAAyB;EAC/C,CAAC;EAGH,IADa,SAAS,QAAQ,SAAS,QAAQ,CACxC,MAAM,IACX,MAAM,IAAI,QAAQ;GAChB,MAAM;GACN,MAAM;IAAE,WAAW;IAAG,aAAa,iBAAiB,UAAU,OAAO;IAAG;GAAK;GAC7E,SACE,SAAS,EAAE,0CAA0C,KAAK;EAE9D,CAAC;EAEH,QAAQ,KAAK;GACX,KAAK,QAAQ,QAAQ;GACrB;GACA,MAAM;GACN,OAAO;EACT,CAAC;CACH;CAIA,QAAQ,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;CACxC,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAClC,IAAI,QAAQ,EAAE,CAAC,QAAQ,QAAQ,IAAI,EAAE,CAAC,KACpC,MAAM,IAAI,QAAQ;EAChB,MAAM;EACN,MAAM;GAAE,GAAG,QAAQ,IAAI;GAAI,GAAG,QAAQ;EAAG;EACzC,SACE,oBAAoB,KAAK;CAE7B,CAAC;CAML,MAAM,MAAgB,CAAC;CACvB,IAAI,SAAS;CACb,KAAK,MAAM,KAAK,SAAS;EACvB,IAAI,KAAK,SAAS,MAAM,QAAQ,EAAE,KAAK,GAAG,EAAE,OAAO;EACnD,SAAS,EAAE;CACb;CACA,IAAI,KAAK,SAAS,MAAM,MAAM,CAAC;CAC/B,OAAO,IAAI,KAAK,EAAE;AACpB;;;AAIA,SAAS,WAAW,SAAyB;CAC3C,IAAI,YAAY,IAAI,OAAO;CAC3B,IAAI,IAAI;CACR,KAAK,MAAM,KAAK,SAAS,IAAI,MAAM,MAAM;CACzC,IAAI,QAAQ,SAAS,IAAI,GAAG;CAC5B,OAAO;AACT;AAEA,SAAS,iBAAiB,UAAkB,QAAwB;CAClE,IAAI,IAAI;CACR,IAAI,OAAO;CACX,SAAS;EACP,MAAM,IAAI,SAAS,QAAQ,QAAQ,IAAI;EACvC,IAAI,MAAM,IAAI,OAAO;EACrB;EACA,OAAO,IAAI,OAAO;CACpB;AACF;AAEA,SAAS,QAAQ,MAAsB;CACrC,MAAM,UAAU,KAAK,KAAK,CAAC,CAAC,WAAW,QAAQ,GAAG;CAClD,OAAO,QAAQ,SAAS,KAAK,GAAG,QAAQ,MAAM,GAAG,EAAE,EAAE,KAAK;AAC5D"}
1
+ {"version":3,"file":"edit-gkb6MCW-.mjs","names":[],"sources":["../src/tools/edit.ts"],"sourcesContent":["import type { ToolContext } from \"@zaly/ai\"\nimport type { FileMeta } from \"./read.ts\"\n\nimport { AiError, defineTool } from \"@zaly/ai\"\nimport { normPath } from \"@zaly/shared\"\nimport { detectEol, normalizeEol } from \"@zaly/shared/text\"\nimport { readFile, stat, writeFile } from \"node:fs/promises\"\nimport { Type } from \"typebox\"\n\nexport type EditTool = typeof editTool\nexport type EditToolMeta = FileMeta & {\n /** File content before any edit was applied. */\n original: string\n /** File content after all edits were applied — exactly what's now on disk. */\n content: string\n}\n\n/**\n * Apply one or more exact-text replacements to a file.\n *\n * Semantics:\n * - All `oldText` matches are resolved against the *original* file\n * content. Edits are not applied sequentially — so edit #1's\n * `newText` cannot accidentally produce a match for edit #2's\n * `oldText`.\n * - Each `oldText` must occur exactly once in the file (uniqueness\n * forces the model to disambiguate via context rather than guessing).\n * - Edits must not overlap. Nearby changes should be combined into a\n * single edit covering the surrounding block.\n * - The file is rewritten atomically: either every edit applies\n * cleanly or none do.\n *\n * Use `read` first to inspect the file. Keep `oldText` minimal but\n * uniquely identifying. For full-file rewrites, use `write` instead.\n */\n// oxlint-disable-next-line sort-keys -- semantic field order: name, desc, params, call\nexport const editTool = defineTool({\n name: \"edit\",\n desc:\n \"Apply one or more exact-text replacements to a file. All matches are resolved \" +\n \"against the original content (not sequentially), each `oldText` must be unique, \" +\n \"and edits must not overlap. Atomic: all-or-nothing.\",\n // oxlint-disable-next-line sort-keys -- semantic param order\n params: Type.Object({\n path: Type.String({ description: \"Path to the file. Absolute or cwd-relative.\" }),\n edits: Type.Array(\n // oxlint-disable-next-line sort-keys -- semantic order: oldText then newText\n Type.Object({\n oldText: Type.String({ description: \"Exact text to find. Must occur once.\" }),\n newText: Type.String({ description: \"Replacement text.\" }),\n }),\n { description: \"Edits to apply atomically.\", minItems: 1 }\n ),\n }),\n\n async preflight(args, ctx: ToolContext<EditToolMeta>) {\n const path = normPath(ctx.cwd, args.path)\n // Edits ride the `write` scope — they're mutations.\n await ctx.need?.(\"write\", path)\n },\n\n async call(\n args,\n ctx: ToolContext<EditToolMeta>\n ): Promise<{ ok: true; path: string; bytes: number; lines: number; edits: number }> {\n const path = normPath(ctx.cwd, args.path)\n\n const original = await readFile(path, \"utf8\").catch((error: unknown) => {\n throw new AiError({\n cause: error,\n code: \"FILE_NOT_FOUND\",\n message: `cannot edit ${path}: file not found`,\n })\n })\n\n const updated = applyEdits(original, args.edits, path)\n await writeFile(path, updated, \"utf8\")\n const fstat = await stat(path)\n ctx.meta = { content: updated, kind: \"edit\", mtime: fstat.mtimeMs, original, path }\n\n const bytes = Buffer.byteLength(updated, \"utf8\")\n const lines = countLines(updated)\n return { bytes, edits: args.edits.length, lines, ok: true, path }\n },\n})\n\ninterface EditSpec {\n oldText: string\n newText: string\n}\n\n/** Resolve all edit positions against `original`, validate uniqueness +\n * non-overlap, then splice the replacements in.\n *\n * Line-ending fidelity: the model always emits LF, but the file may be\n * CRLF. We detect the file's eol once, then normalize each `oldText` /\n * `newText` to that eol before matching/splicing. The result has the\n * same line-ending style as the original file. */\nfunction applyEdits(original: string, edits: readonly EditSpec[], path: string): string {\n const eol = detectEol({ path, text: original })\n // 1. Locate every oldText. Reject not-found and non-unique up-front so\n // the model gets a precise error per failing edit rather than a\n // \"patch failed at line N\" mystery.\n const matches: { start: number; end: number; newText: string; spec: EditSpec }[] = []\n for (const [i, edit] of edits.entries()) {\n const oldText = normalizeEol(edit.oldText, { eol })\n const newText = normalizeEol(edit.newText, { eol })\n if (oldText === \"\") {\n throw new AiError({\n code: \"EMPTY_OLD_TEXT\",\n data: { editIndex: i },\n message: `edit #${i}: oldText is empty (use \\`write\\` to create or fully replace a file)`,\n })\n }\n const first = original.indexOf(oldText)\n if (first === -1) {\n throw new AiError({\n code: \"NOT_FOUND\",\n data: { editIndex: i, path, snippet: preview(oldText) },\n message: `edit #${i}: oldText not found in ${path}`,\n })\n }\n const next = original.indexOf(oldText, first + 1)\n if (next !== -1) {\n throw new AiError({\n code: \"NOT_UNIQUE\",\n data: { editIndex: i, occurrences: countOccurrences(original, oldText), path },\n message:\n `edit #${i}: oldText matches multiple locations in ${path}. ` +\n `Add surrounding context to make it unique.`,\n })\n }\n matches.push({\n end: first + oldText.length,\n newText,\n spec: edit,\n start: first,\n })\n }\n\n // 2. Sort by start; verify no overlap. Equal start positions are\n // impossible because oldText is unique.\n matches.sort((a, b) => a.start - b.start)\n for (let i = 1; i < matches.length; i++) {\n if (matches[i].start < matches[i - 1].end) {\n throw new AiError({\n code: \"OVERLAP\",\n data: { a: matches[i - 1], b: matches[i] },\n message:\n `edits overlap in ${path}. ` +\n `Combine adjacent changes into a single edit covering both.`,\n })\n }\n }\n\n // 3. Splice. Walk the original and emit untouched-prefix + replacement\n // chunks; cheaper than N independent string replacements.\n const out: string[] = []\n let cursor = 0\n for (const m of matches) {\n out.push(original.slice(cursor, m.start), m.newText)\n cursor = m.end\n }\n out.push(original.slice(cursor))\n return out.join(\"\")\n}\n\n/** Count lines the way an editor would — non-empty trailing line counts;\n * a trailing newline doesn't add a phantom blank line. */\nfunction countLines(content: string): number {\n if (content === \"\") return 0\n let n = 1\n for (const c of content) if (c === \"\\n\") n++\n if (content.endsWith(\"\\n\")) n--\n return n\n}\n\nfunction countOccurrences(haystack: string, needle: string): number {\n let n = 0\n let from = 0\n for (;;) {\n const i = haystack.indexOf(needle, from)\n if (i === -1) return n\n n++\n from = i + needle.length\n }\n}\n\nfunction preview(text: string): string {\n const trimmed = text.trim().replaceAll(/\\s+/g, \" \")\n return trimmed.length > 80 ? `${trimmed.slice(0, 79)}…` : trimmed\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { t as truncate } from "./truncate-CChlZ113.mjs";
2
+ import { defineTool, toAttachment, zalyUserAgent } from "@zaly/ai";
2
3
  import { randomHash } from "@zaly/shared";
3
4
  import { join } from "pathe";
4
- import { defineTool, toAttachment, zalyUserAgent } from "@zaly/ai";
5
5
  import { mkdir, writeFile } from "node:fs/promises";
6
6
  import { Type } from "typebox";
7
7
  import { tmpdir } from "node:os";
@@ -103,7 +103,7 @@ const fetchTool = defineTool({
103
103
  } catch {}
104
104
  if (args.format !== "raw" && !isJson && file?.type === "text") {
105
105
  if (file.format === "html" || args.format === "markdown" && file.format !== "markdown") {
106
- const { htmlToMarkdown } = await import("./html-VrAsoGBK.mjs");
106
+ const { htmlToMarkdown } = await import("./html-B-Lr-9W6.mjs");
107
107
  const page = htmlToMarkdown(text, res.url || url.href);
108
108
  body = page.markdown;
109
109
  outputFormat = "markdown";
@@ -113,7 +113,7 @@ const fetchTool = defineTool({
113
113
  }
114
114
  if (args.jsonpath !== void 0) {
115
115
  if (outputFormat !== "json") throw new Error("jsonpath requires a valid JSON response");
116
- const { JSONPath } = await import("./index-node-esm-BwaOzhXB.mjs");
116
+ const { JSONPath } = await import("./index-node-esm-75XSYyIw.mjs");
117
117
  body = JSONPath({
118
118
  json: body,
119
119
  path: args.jsonpath
@@ -155,4 +155,4 @@ const fetchTool = defineTool({
155
155
  });
156
156
  export { fetchTool };
157
157
 
158
- //# sourceMappingURL=fetch-BrtIqrid.mjs.map
158
+ //# sourceMappingURL=fetch-D_GzL2yy.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"fetch-BrtIqrid.mjs","names":[],"sources":["../src/tools/fetch.ts"],"sourcesContent":["import type { Attachment, MetaPart, TextPart, ToolContext } from \"@zaly/ai\"\n\nimport { defineTool, toAttachment, zalyUserAgent } from \"@zaly/ai\"\nimport { randomHash } from \"@zaly/shared\"\nimport { mkdir, writeFile } from \"node:fs/promises\"\nimport { tmpdir } from \"node:os\"\nimport { join } from \"pathe\"\nimport { Type } from \"typebox\"\nimport { truncate } from \"../utils/truncate.ts\"\n\nexport type FetchTool = typeof fetchTool\nexport type FetchToolMeta = {\n kind: \"fetch\"\n url: string\n type: \"text\" | Attachment[\"type\"]\n format?: string\n title?: string\n status: number\n statusText: string\n ok: boolean\n contentType?: string\n durationMs: number\n warning?: string\n truncated?: { fullOutputPath: string; totalBytes: number; totalLines: number }\n}\n\n/**\n * Fetch APIs or static pages. HTML becomes readable Markdown; raw mode\n * preserves the response text. Conversion never runs page scripts.\n * Truncated text is saved in full alongside the session, like bash output.\n *\n * Permissions: routes through the `fetch` scope when the agent wires up\n * tool-side permission checks. Until then, callers should restrict via\n * a wrapping permission layer (rule-based or explicit allowlist).\n */\n// oxlint-disable-next-line sort-keys\nexport const fetchTool = defineTool({\n desc:\n `Fetch a URL and return the response. JSON responses are parsed; use \\`jsonpath\\` (e.g. \\`$.items[*].name\\`) to extract a subset and minimise tokens. ` +\n `HTML pages are converted to readable Markdown without executing JavaScript; use a browser for interactive pages. ` +\n `Images & PDFs are returned as attachments. Truncated text is saved in full; read \\`truncated.fullOutputPath\\` to inspect it.`,\n name: \"fetch\",\n parallel: true,\n\n // oxlint-disable-next-line sort-keys -- semantic param order: url, method, headers, query, body, jsonpath\n params: Type.Object({\n url: Type.String({ description: \"Absolute URL.\" }),\n method: Type.Optional(\n Type.Union(\n [\n Type.Literal(\"GET\"),\n Type.Literal(\"POST\"),\n Type.Literal(\"PUT\"),\n Type.Literal(\"PATCH\"),\n Type.Literal(\"DELETE\"),\n ],\n { default: \"GET\", description: \"HTTP method.\" }\n )\n ),\n headers: Type.Optional(\n Type.Record(Type.String(), Type.String(), {\n description: \"Request headers.\",\n })\n ),\n query: Type.Optional(\n Type.Record(Type.String(), Type.String(), {\n description: \"Query string params, appended to the URL.\",\n })\n ),\n body: Type.Optional(\n Type.String({\n description:\n \"Request body as a string. For JSON, stringify it yourself and \" +\n \"set `headers: { 'Content-Type': 'application/json' }`.\",\n })\n ),\n format: Type.Optional(\n Type.Union([Type.Literal(\"auto\"), Type.Literal(\"markdown\"), Type.Literal(\"raw\")], {\n default: \"auto\",\n description:\n \"auto converts HTML to Markdown and parses JSON. markdown also treats unrecognized text as HTML. raw returns original text without conversion or JSON parsing. Attachments are unchanged.\",\n })\n ),\n limit: Type.Optional(\n Type.Integer({\n default: 2000,\n description:\n \"Maximum lines to keep inline, split between head and tail. Full output is saved when truncated.\",\n minimum: 1,\n })\n ),\n jsonpath: Type.Optional(\n Type.String({\n description:\n \"JSONPath expression applied to a JSON response, e.g. \" +\n \"`$.items[*].name`. Returns the matched values as an array.\",\n })\n ),\n }),\n async call(args, ctx: ToolContext<FetchToolMeta>): Promise<(Attachment | MetaPart | TextPart)[]> {\n if (args.format === \"raw\" && args.jsonpath !== undefined)\n throw new Error(\"jsonpath cannot be combined with format: raw\")\n const t0 = Date.now()\n const url = new URL(args.url)\n if (args.query) {\n for (const [k, v] of Object.entries(args.query)) url.searchParams.set(k, v)\n }\n\n const headers = new Headers(args.headers)\n if (!headers.has(\"user-agent\")) headers.set(\"User-Agent\", zalyUserAgent)\n const res = await fetch(url, {\n body: args.body,\n headers,\n method: args.method,\n signal: ctx.signal,\n })\n\n const contentType = res.headers.get(\"content-type\") ?? \"\"\n const bytes = new Uint8Array(await res.arrayBuffer())\n\n const meta: FetchToolMeta = {\n contentType: contentType || undefined,\n durationMs: Date.now() - t0,\n kind: \"fetch\",\n ok: res.ok,\n status: res.status,\n statusText: res.statusText,\n type: \"text\",\n url: res.url || url.href,\n }\n ctx.meta = meta\n\n // Magic-byte detection on the response bytes — catches mislabeled\n // content (HTML served at /file.pdf, images served as\n // application/octet-stream, etc.). Inlines as base64 (same shape as\n // the `read` tool) so the model gets the bytes without the provider\n // re-fetching from its own IP. Only attach kinds the model accepts;\n // unsupported kinds fall through to the text branch (rendered via\n // `<image>` / `<pdf>` placeholders by the wire pipeline).\n const { fileDetect } = await import(\"@zaly/shared/detect\")\n const file = await fileDetect({\n data: bytes,\n mime: contentType || undefined,\n url: res.url || url.href,\n })\n if (file) {\n const att = await toAttachment(file)\n if (att) {\n meta.type = att.type\n meta.durationMs = Date.now() - t0\n return [{ data: meta, tag: \"fetch\", type: \"meta\" }, att]\n }\n }\n\n const text = new TextDecoder().decode(bytes)\n // `detectTextFormat` (inside fileDetect) already weighs MIME,\n // extension, and content sniff — reuse its verdict instead of a\n // parallel `looksLikeJson` heuristic.\n const isJson = file?.type === \"text\" && file.format === \"json\"\n\n let body: unknown = text\n let outputFormat: string | undefined\n if (isJson && args.format !== \"raw\") {\n try {\n body = JSON.parse(text)\n outputFormat = \"json\"\n } catch {\n // Server lied about content-type; keep as text.\n }\n }\n if (args.format !== \"raw\" && !isJson && file?.type === \"text\") {\n if (file.format === \"html\" || (args.format === \"markdown\" && file.format !== \"markdown\")) {\n const { htmlToMarkdown } = await import(\"../utils/html.ts\")\n const page = htmlToMarkdown(text, res.url || url.href)\n body = page.markdown\n outputFormat = \"markdown\"\n if (page.title) meta.title = page.title\n if (page.warning) meta.warning = page.warning\n } else if (file.format === \"markdown\") outputFormat = \"markdown\"\n }\n\n if (args.jsonpath !== undefined) {\n if (outputFormat !== \"json\") throw new Error(\"jsonpath requires a valid JSON response\")\n // Lazy-import — `jsonpath-plus` is only paid for when actually used,\n // and it bundles to a few hundred KB which we'd rather not load on\n // every fetch that doesn't filter.\n const { JSONPath } = await import(\"jsonpath-plus\")\n // oxlint-disable-next-line new-cap -- jsonpath-plus exports as TitleCase\n body = JSONPath({ json: body as object, path: args.jsonpath })\n }\n\n // Stringify the body for display. JSON pretty-prints; raw text is\n // used as-is.\n const bodyText = typeof body === \"string\" ? body : JSON.stringify(body, undefined, 2)\n\n meta.format = outputFormat ?? (file?.type === \"text\" ? file.format : \"plain\")\n const summary = truncate(bodyText, { maxLines: args.limit, strategy: \"head+tail\" })\n if (summary.truncated) {\n let extension = \"txt\"\n if (outputFormat === \"markdown\") extension = \"md\"\n else if (outputFormat === \"json\") extension = \"json\"\n const dir = ctx.sessionDir ?? join(tmpdir(), \"zaly-fetch\")\n const fullOutputPath = join(dir, `fetch-${randomHash()}.${extension}`)\n await mkdir(dir, { recursive: true })\n await writeFile(fullOutputPath, bodyText, \"utf8\")\n meta.truncated = {\n fullOutputPath,\n totalBytes: summary.origBytes,\n totalLines: summary.origLines,\n }\n }\n meta.durationMs = Date.now() - t0\n const parts: (MetaPart | TextPart)[] = [{ data: meta, tag: \"fetch\", type: \"meta\" }]\n if (summary.text !== \"\") parts.push({ format: outputFormat, text: summary.text, type: \"text\" })\n return parts\n },\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"fetch-D_GzL2yy.mjs","names":[],"sources":["../src/tools/fetch.ts"],"sourcesContent":["import type { Attachment, MetaPart, TextPart, ToolContext } from \"@zaly/ai\"\n\nimport { defineTool, toAttachment, zalyUserAgent } from \"@zaly/ai\"\nimport { randomHash } from \"@zaly/shared\"\nimport { mkdir, writeFile } from \"node:fs/promises\"\nimport { tmpdir } from \"node:os\"\nimport { join } from \"pathe\"\nimport { Type } from \"typebox\"\nimport { truncate } from \"../utils/truncate.ts\"\n\nexport type FetchTool = typeof fetchTool\nexport type FetchToolMeta = {\n kind: \"fetch\"\n url: string\n type: \"text\" | Attachment[\"type\"]\n format?: string\n title?: string\n status: number\n statusText: string\n ok: boolean\n contentType?: string\n durationMs: number\n warning?: string\n truncated?: { fullOutputPath: string; totalBytes: number; totalLines: number }\n}\n\n/**\n * Fetch APIs or static pages. HTML becomes readable Markdown; raw mode\n * preserves the response text. Conversion never runs page scripts.\n * Truncated text is saved in full alongside the session, like bash output.\n *\n * Permissions: routes through the `fetch` scope when the agent wires up\n * tool-side permission checks. Until then, callers should restrict via\n * a wrapping permission layer (rule-based or explicit allowlist).\n */\n// oxlint-disable-next-line sort-keys\nexport const fetchTool = defineTool({\n desc:\n `Fetch a URL and return the response. JSON responses are parsed; use \\`jsonpath\\` (e.g. \\`$.items[*].name\\`) to extract a subset and minimise tokens. ` +\n `HTML pages are converted to readable Markdown without executing JavaScript; use a browser for interactive pages. ` +\n `Images & PDFs are returned as attachments. Truncated text is saved in full; read \\`truncated.fullOutputPath\\` to inspect it.`,\n name: \"fetch\",\n parallel: true,\n\n // oxlint-disable-next-line sort-keys -- semantic param order: url, method, headers, query, body, jsonpath\n params: Type.Object({\n url: Type.String({ description: \"Absolute URL.\" }),\n method: Type.Optional(\n Type.Union(\n [\n Type.Literal(\"GET\"),\n Type.Literal(\"POST\"),\n Type.Literal(\"PUT\"),\n Type.Literal(\"PATCH\"),\n Type.Literal(\"DELETE\"),\n ],\n { default: \"GET\", description: \"HTTP method.\" }\n )\n ),\n headers: Type.Optional(\n Type.Record(Type.String(), Type.String(), {\n description: \"Request headers.\",\n })\n ),\n query: Type.Optional(\n Type.Record(Type.String(), Type.String(), {\n description: \"Query string params, appended to the URL.\",\n })\n ),\n body: Type.Optional(\n Type.String({\n description:\n \"Request body as a string. For JSON, stringify it yourself and \" +\n \"set `headers: { 'Content-Type': 'application/json' }`.\",\n })\n ),\n format: Type.Optional(\n Type.Union([Type.Literal(\"auto\"), Type.Literal(\"markdown\"), Type.Literal(\"raw\")], {\n default: \"auto\",\n description:\n \"auto converts HTML to Markdown and parses JSON. markdown also treats unrecognized text as HTML. raw returns original text without conversion or JSON parsing. Attachments are unchanged.\",\n })\n ),\n limit: Type.Optional(\n Type.Integer({\n default: 2000,\n description:\n \"Maximum lines to keep inline, split between head and tail. Full output is saved when truncated.\",\n minimum: 1,\n })\n ),\n jsonpath: Type.Optional(\n Type.String({\n description:\n \"JSONPath expression applied to a JSON response, e.g. \" +\n \"`$.items[*].name`. Returns the matched values as an array.\",\n })\n ),\n }),\n async call(args, ctx: ToolContext<FetchToolMeta>): Promise<(Attachment | MetaPart | TextPart)[]> {\n if (args.format === \"raw\" && args.jsonpath !== undefined)\n throw new Error(\"jsonpath cannot be combined with format: raw\")\n const t0 = Date.now()\n const url = new URL(args.url)\n if (args.query) {\n for (const [k, v] of Object.entries(args.query)) url.searchParams.set(k, v)\n }\n\n const headers = new Headers(args.headers)\n if (!headers.has(\"user-agent\")) headers.set(\"User-Agent\", zalyUserAgent)\n const res = await fetch(url, {\n body: args.body,\n headers,\n method: args.method,\n signal: ctx.signal,\n })\n\n const contentType = res.headers.get(\"content-type\") ?? \"\"\n const bytes = new Uint8Array(await res.arrayBuffer())\n\n const meta: FetchToolMeta = {\n contentType: contentType || undefined,\n durationMs: Date.now() - t0,\n kind: \"fetch\",\n ok: res.ok,\n status: res.status,\n statusText: res.statusText,\n type: \"text\",\n url: res.url || url.href,\n }\n ctx.meta = meta\n\n // Magic-byte detection on the response bytes — catches mislabeled\n // content (HTML served at /file.pdf, images served as\n // application/octet-stream, etc.). Inlines as base64 (same shape as\n // the `read` tool) so the model gets the bytes without the provider\n // re-fetching from its own IP. Only attach kinds the model accepts;\n // unsupported kinds fall through to the text branch (rendered via\n // `<image>` / `<pdf>` placeholders by the wire pipeline).\n const { fileDetect } = await import(\"@zaly/shared/detect\")\n const file = await fileDetect({\n data: bytes,\n mime: contentType || undefined,\n url: res.url || url.href,\n })\n if (file) {\n const att = await toAttachment(file)\n if (att) {\n meta.type = att.type\n meta.durationMs = Date.now() - t0\n return [{ data: meta, tag: \"fetch\", type: \"meta\" }, att]\n }\n }\n\n const text = new TextDecoder().decode(bytes)\n // `detectTextFormat` (inside fileDetect) already weighs MIME,\n // extension, and content sniff — reuse its verdict instead of a\n // parallel `looksLikeJson` heuristic.\n const isJson = file?.type === \"text\" && file.format === \"json\"\n\n let body: unknown = text\n let outputFormat: string | undefined\n if (isJson && args.format !== \"raw\") {\n try {\n body = JSON.parse(text)\n outputFormat = \"json\"\n } catch {\n // Server lied about content-type; keep as text.\n }\n }\n if (args.format !== \"raw\" && !isJson && file?.type === \"text\") {\n if (file.format === \"html\" || (args.format === \"markdown\" && file.format !== \"markdown\")) {\n const { htmlToMarkdown } = await import(\"../utils/html.ts\")\n const page = htmlToMarkdown(text, res.url || url.href)\n body = page.markdown\n outputFormat = \"markdown\"\n if (page.title) meta.title = page.title\n if (page.warning) meta.warning = page.warning\n } else if (file.format === \"markdown\") outputFormat = \"markdown\"\n }\n\n if (args.jsonpath !== undefined) {\n if (outputFormat !== \"json\") throw new Error(\"jsonpath requires a valid JSON response\")\n // Lazy-import — `jsonpath-plus` is only paid for when actually used,\n // and it bundles to a few hundred KB which we'd rather not load on\n // every fetch that doesn't filter.\n const { JSONPath } = await import(\"jsonpath-plus\")\n // oxlint-disable-next-line new-cap -- jsonpath-plus exports as TitleCase\n body = JSONPath({ json: body as object, path: args.jsonpath })\n }\n\n // Stringify the body for display. JSON pretty-prints; raw text is\n // used as-is.\n const bodyText = typeof body === \"string\" ? body : JSON.stringify(body, undefined, 2)\n\n meta.format = outputFormat ?? (file?.type === \"text\" ? file.format : \"plain\")\n const summary = truncate(bodyText, { maxLines: args.limit, strategy: \"head+tail\" })\n if (summary.truncated) {\n let extension = \"txt\"\n if (outputFormat === \"markdown\") extension = \"md\"\n else if (outputFormat === \"json\") extension = \"json\"\n const dir = ctx.sessionDir ?? join(tmpdir(), \"zaly-fetch\")\n const fullOutputPath = join(dir, `fetch-${randomHash()}.${extension}`)\n await mkdir(dir, { recursive: true })\n await writeFile(fullOutputPath, bodyText, \"utf8\")\n meta.truncated = {\n fullOutputPath,\n totalBytes: summary.origBytes,\n totalLines: summary.origLines,\n }\n }\n meta.durationMs = Date.now() - t0\n const parts: (MetaPart | TextPart)[] = [{ data: meta, tag: \"fetch\", type: \"meta\" }]\n if (summary.text !== \"\") parts.push({ format: outputFormat, text: summary.text, type: \"text\" })\n return parts\n },\n})\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { t as truncate } from "./truncate-CChlZ113.mjs";
2
- import { normPath } from "@zaly/shared";
3
2
  import { AiError, defineTool } from "@zaly/ai";
3
+ import { normPath } from "@zaly/shared";
4
4
  import { Type } from "typebox";
5
5
  import { cleanTextTui } from "@zaly/shared/text";
6
6
  import { FindError, find } from "@zaly/shared/find";
@@ -87,4 +87,4 @@ const findTool = defineTool({
87
87
  });
88
88
  export { findTool };
89
89
 
90
- //# sourceMappingURL=find-B-vvdlHu.mjs.map
90
+ //# sourceMappingURL=find-DlrYUTXT.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"find-B-vvdlHu.mjs","names":[],"sources":["../src/tools/find.ts"],"sourcesContent":["import type { MetaPart, TextPart, ToolContext } from \"@zaly/ai\"\n\nimport { AiError, defineTool } from \"@zaly/ai\"\nimport { normPath } from \"@zaly/shared\"\nimport { find, FindError } from \"@zaly/shared/find\"\nimport { cleanTextTui } from \"@zaly/shared/text\"\nimport { Type } from \"typebox\"\nimport { truncate } from \"../utils/truncate.ts\"\n\nconst DEFAULT_LIMIT = 200\nconst MAX_LIMIT = 1000\n\nexport type FindTool = typeof findTool\nexport type FindToolMeta = {\n cwd: string\n glob: string | string[]\n matches: number\n truncated: boolean\n}\n\n// oxlint-disable-next-line sort-keys -- semantic field order: name, desc, params, call\nexport const findTool = defineTool({\n name: \"find\",\n desc:\n \"Find files using fd/fdfind or rg --files. `glob` is a filename/path glob, \" +\n \"array of globs, or plain substring; empty, `*`, or `**/*` lists all files. \" +\n \"Includes hidden files and respects .gitignore by default. Does not follow directory symlinks; \" +\n \"prefer this over bash find for file discovery.\",\n parallel: true,\n // oxlint-disable-next-line sort-keys -- semantic param order\n params: Type.Object({\n glob: Type.Optional(\n Type.Union([Type.String(), Type.Array(Type.String())], {\n default: \"\",\n description:\n \"Filename/path glob, array of globs, or plain substring. Empty, `*`, or `**/*` lists all files.\",\n })\n ),\n paths: Type.Optional(\n Type.Array(Type.String(), {\n description: \"Optional directories to search, relative to cwd or absolute.\",\n })\n ),\n hidden: Type.Optional(\n Type.Boolean({\n default: true,\n description: \"Include files in hidden directories and dotfiles.\",\n })\n ),\n ignore: Type.Optional(\n Type.Boolean({ default: true, description: \"Respect .gitignore/.ignore rules.\" })\n ),\n limit: Type.Optional(\n Type.Integer({\n default: DEFAULT_LIMIT,\n description: \"Maximum paths to keep inline.\",\n maximum: MAX_LIMIT,\n minimum: 1,\n })\n ),\n }),\n\n async preflight(args, ctx: ToolContext<FindToolMeta>) {\n const cwd = normPath(ctx.cwd, \".\")\n await ctx.need?.(\"read\", cwd)\n const paths = (args.paths?.length ? args.paths : [\".\"]).map((p) => normPath(cwd, p))\n await Promise.all(paths.map((path) => Promise.resolve(ctx.need?.(\"read\", path))))\n },\n\n async call(args, ctx: ToolContext<FindToolMeta>): Promise<(MetaPart | TextPart)[]> {\n const cwd = normPath(ctx.cwd, \".\")\n\n const limit = args.limit ?? DEFAULT_LIMIT\n const result: string[] = []\n let warning: FindError | undefined\n try {\n for await (const batch of find({\n ...args,\n cwd,\n limit: limit + 1,\n pattern: args.glob,\n signal: ctx.signal,\n })) {\n result.push(...batch)\n }\n } catch (error) {\n if (!(error instanceof FindError) || error.code !== \"FIND_FAILED\" || !result.length) {\n throw AiError.from(error)\n }\n warning = error\n }\n\n const summary = truncate(result.join(\"\\n\"), {\n maxLines: limit,\n strategy: \"head\",\n })\n\n const truncated = summary.truncated\n ctx.meta = {\n cwd,\n glob: args.glob ?? \"\",\n matches: summary.origLines,\n truncated,\n }\n\n const parts: (MetaPart | TextPart)[] = []\n if (warning) {\n parts.push({ data: { error: warning.data, incomplete: true }, tag: \"find\", type: \"meta\" })\n }\n parts.push({ text: summary.text || \"No files found.\", type: \"text\" })\n if (warning) {\n parts.push({\n text: `Warning: Search incomplete.\\n${cleanTextTui(warning.message)}`,\n type: \"text\",\n })\n }\n return parts\n },\n})\n"],"mappings":";;;;;;;AASA,MAAM,gBAAgB;AAYtB,MAAa,WAAW,WAAW;CACjC,MAAM;CACN,MACE;CAIF,UAAU;CAEV,QAAQ,KAAK,OAAO;EAClB,MAAM,KAAK,SACT,KAAK,MAAM,CAAC,KAAK,OAAO,GAAG,KAAK,MAAM,KAAK,OAAO,CAAC,CAAC,GAAG;GACrD,SAAS;GACT,aACE;EACJ,CAAC,CACH;EACA,OAAO,KAAK,SACV,KAAK,MAAM,KAAK,OAAO,GAAG,EACxB,aAAa,+DACf,CAAC,CACH;EACA,QAAQ,KAAK,SACX,KAAK,QAAQ;GACX,SAAS;GACT,aAAa;EACf,CAAC,CACH;EACA,QAAQ,KAAK,SACX,KAAK,QAAQ;GAAE,SAAS;GAAM,aAAa;EAAoC,CAAC,CAClF;EACA,OAAO,KAAK,SACV,KAAK,QAAQ;GACX,SAAS;GACT,aAAa;GACb,SAAS;GACT,SAAS;EACX,CAAC,CACH;CACF,CAAC;CAED,MAAM,UAAU,MAAM,KAAgC;EACpD,MAAM,MAAM,SAAS,IAAI,KAAK,GAAG;EACjC,MAAM,IAAI,OAAO,QAAQ,GAAG;EAC5B,MAAM,SAAS,KAAK,OAAO,SAAS,KAAK,QAAQ,CAAC,GAAG,EAAA,CAAG,KAAK,MAAM,SAAS,KAAK,CAAC,CAAC;EACnF,MAAM,QAAQ,IAAI,MAAM,KAAK,SAAS,QAAQ,QAAQ,IAAI,OAAO,QAAQ,IAAI,CAAC,CAAC,CAAC;CAClF;CAEA,MAAM,KAAK,MAAM,KAAkE;EACjF,MAAM,MAAM,SAAS,IAAI,KAAK,GAAG;EAEjC,MAAM,QAAQ,KAAK,SAAS;EAC5B,MAAM,SAAmB,CAAC;EAC1B,IAAI;EACJ,IAAI;GACF,WAAW,MAAM,SAAS,KAAK;IAC7B,GAAG;IACH;IACA,OAAO,QAAQ;IACf,SAAS,KAAK;IACd,QAAQ,IAAI;GACd,CAAC,GACC,OAAO,KAAK,GAAG,KAAK;EAExB,SAAS,OAAO;GACd,IAAI,EAAE,iBAAiB,cAAc,MAAM,SAAS,iBAAiB,CAAC,OAAO,QAC3E,MAAM,QAAQ,KAAK,KAAK;GAE1B,UAAU;EACZ;EAEA,MAAM,UAAU,SAAS,OAAO,KAAK,IAAI,GAAG;GAC1C,UAAU;GACV,UAAU;EACZ,CAAC;EAED,MAAM,YAAY,QAAQ;EAC1B,IAAI,OAAO;GACT;GACA,MAAM,KAAK,QAAQ;GACnB,SAAS,QAAQ;GACjB;EACF;EAEA,MAAM,QAAiC,CAAC;EACxC,IAAI,SACF,MAAM,KAAK;GAAE,MAAM;IAAE,OAAO,QAAQ;IAAM,YAAY;GAAK;GAAG,KAAK;GAAQ,MAAM;EAAO,CAAC;EAE3F,MAAM,KAAK;GAAE,MAAM,QAAQ,QAAQ;GAAmB,MAAM;EAAO,CAAC;EACpE,IAAI,SACF,MAAM,KAAK;GACT,MAAM,gCAAgC,aAAa,QAAQ,OAAO;GAClE,MAAM;EACR,CAAC;EAEH,OAAO;CACT;AACF,CAAC"}
1
+ {"version":3,"file":"find-DlrYUTXT.mjs","names":[],"sources":["../src/tools/find.ts"],"sourcesContent":["import type { MetaPart, TextPart, ToolContext } from \"@zaly/ai\"\n\nimport { AiError, defineTool } from \"@zaly/ai\"\nimport { normPath } from \"@zaly/shared\"\nimport { find, FindError } from \"@zaly/shared/find\"\nimport { cleanTextTui } from \"@zaly/shared/text\"\nimport { Type } from \"typebox\"\nimport { truncate } from \"../utils/truncate.ts\"\n\nconst DEFAULT_LIMIT = 200\nconst MAX_LIMIT = 1000\n\nexport type FindTool = typeof findTool\nexport type FindToolMeta = {\n cwd: string\n glob: string | string[]\n matches: number\n truncated: boolean\n}\n\n// oxlint-disable-next-line sort-keys -- semantic field order: name, desc, params, call\nexport const findTool = defineTool({\n name: \"find\",\n desc:\n \"Find files using fd/fdfind or rg --files. `glob` is a filename/path glob, \" +\n \"array of globs, or plain substring; empty, `*`, or `**/*` lists all files. \" +\n \"Includes hidden files and respects .gitignore by default. Does not follow directory symlinks; \" +\n \"prefer this over bash find for file discovery.\",\n parallel: true,\n // oxlint-disable-next-line sort-keys -- semantic param order\n params: Type.Object({\n glob: Type.Optional(\n Type.Union([Type.String(), Type.Array(Type.String())], {\n default: \"\",\n description:\n \"Filename/path glob, array of globs, or plain substring. Empty, `*`, or `**/*` lists all files.\",\n })\n ),\n paths: Type.Optional(\n Type.Array(Type.String(), {\n description: \"Optional directories to search, relative to cwd or absolute.\",\n })\n ),\n hidden: Type.Optional(\n Type.Boolean({\n default: true,\n description: \"Include files in hidden directories and dotfiles.\",\n })\n ),\n ignore: Type.Optional(\n Type.Boolean({ default: true, description: \"Respect .gitignore/.ignore rules.\" })\n ),\n limit: Type.Optional(\n Type.Integer({\n default: DEFAULT_LIMIT,\n description: \"Maximum paths to keep inline.\",\n maximum: MAX_LIMIT,\n minimum: 1,\n })\n ),\n }),\n\n async preflight(args, ctx: ToolContext<FindToolMeta>) {\n const cwd = normPath(ctx.cwd, \".\")\n await ctx.need?.(\"read\", cwd)\n const paths = (args.paths?.length ? args.paths : [\".\"]).map((p) => normPath(cwd, p))\n await Promise.all(paths.map((path) => Promise.resolve(ctx.need?.(\"read\", path))))\n },\n\n async call(args, ctx: ToolContext<FindToolMeta>): Promise<(MetaPart | TextPart)[]> {\n const cwd = normPath(ctx.cwd, \".\")\n\n const limit = args.limit ?? DEFAULT_LIMIT\n const result: string[] = []\n let warning: FindError | undefined\n try {\n for await (const batch of find({\n ...args,\n cwd,\n limit: limit + 1,\n pattern: args.glob,\n signal: ctx.signal,\n })) {\n result.push(...batch)\n }\n } catch (error) {\n if (!(error instanceof FindError) || error.code !== \"FIND_FAILED\" || !result.length) {\n throw AiError.from(error)\n }\n warning = error\n }\n\n const summary = truncate(result.join(\"\\n\"), {\n maxLines: limit,\n strategy: \"head\",\n })\n\n const truncated = summary.truncated\n ctx.meta = {\n cwd,\n glob: args.glob ?? \"\",\n matches: summary.origLines,\n truncated,\n }\n\n const parts: (MetaPart | TextPart)[] = []\n if (warning) {\n parts.push({ data: { error: warning.data, incomplete: true }, tag: \"find\", type: \"meta\" })\n }\n parts.push({ text: summary.text || \"No files found.\", type: \"text\" })\n if (warning) {\n parts.push({\n text: `Warning: Search incomplete.\\n${cleanTextTui(warning.message)}`,\n type: \"text\",\n })\n }\n return parts\n },\n})\n"],"mappings":";;;;;;;AASA,MAAM,gBAAgB;AAYtB,MAAa,WAAW,WAAW;CACjC,MAAM;CACN,MACE;CAIF,UAAU;CAEV,QAAQ,KAAK,OAAO;EAClB,MAAM,KAAK,SACT,KAAK,MAAM,CAAC,KAAK,OAAO,GAAG,KAAK,MAAM,KAAK,OAAO,CAAC,CAAC,GAAG;GACrD,SAAS;GACT,aACE;EACJ,CAAC,CACH;EACA,OAAO,KAAK,SACV,KAAK,MAAM,KAAK,OAAO,GAAG,EACxB,aAAa,+DACf,CAAC,CACH;EACA,QAAQ,KAAK,SACX,KAAK,QAAQ;GACX,SAAS;GACT,aAAa;EACf,CAAC,CACH;EACA,QAAQ,KAAK,SACX,KAAK,QAAQ;GAAE,SAAS;GAAM,aAAa;EAAoC,CAAC,CAClF;EACA,OAAO,KAAK,SACV,KAAK,QAAQ;GACX,SAAS;GACT,aAAa;GACb,SAAS;GACT,SAAS;EACX,CAAC,CACH;CACF,CAAC;CAED,MAAM,UAAU,MAAM,KAAgC;EACpD,MAAM,MAAM,SAAS,IAAI,KAAK,GAAG;EACjC,MAAM,IAAI,OAAO,QAAQ,GAAG;EAC5B,MAAM,SAAS,KAAK,OAAO,SAAS,KAAK,QAAQ,CAAC,GAAG,EAAA,CAAG,KAAK,MAAM,SAAS,KAAK,CAAC,CAAC;EACnF,MAAM,QAAQ,IAAI,MAAM,KAAK,SAAS,QAAQ,QAAQ,IAAI,OAAO,QAAQ,IAAI,CAAC,CAAC,CAAC;CAClF;CAEA,MAAM,KAAK,MAAM,KAAkE;EACjF,MAAM,MAAM,SAAS,IAAI,KAAK,GAAG;EAEjC,MAAM,QAAQ,KAAK,SAAS;EAC5B,MAAM,SAAmB,CAAC;EAC1B,IAAI;EACJ,IAAI;GACF,WAAW,MAAM,SAAS,KAAK;IAC7B,GAAG;IACH;IACA,OAAO,QAAQ;IACf,SAAS,KAAK;IACd,QAAQ,IAAI;GACd,CAAC,GACC,OAAO,KAAK,GAAG,KAAK;EAExB,SAAS,OAAO;GACd,IAAI,EAAE,iBAAiB,cAAc,MAAM,SAAS,iBAAiB,CAAC,OAAO,QAC3E,MAAM,QAAQ,KAAK,KAAK;GAE1B,UAAU;EACZ;EAEA,MAAM,UAAU,SAAS,OAAO,KAAK,IAAI,GAAG;GAC1C,UAAU;GACV,UAAU;EACZ,CAAC;EAED,MAAM,YAAY,QAAQ;EAC1B,IAAI,OAAO;GACT;GACA,MAAM,KAAK,QAAQ;GACnB,SAAS,QAAQ;GACjB;EACF;EAEA,MAAM,QAAiC,CAAC;EACxC,IAAI,SACF,MAAM,KAAK;GAAE,MAAM;IAAE,OAAO,QAAQ;IAAM,YAAY;GAAK;GAAG,KAAK;GAAQ,MAAM;EAAO,CAAC;EAE3F,MAAM,KAAK;GAAE,MAAM,QAAQ,QAAQ;GAAmB,MAAM;EAAO,CAAC;EACpE,IAAI,SACF,MAAM,KAAK;GACT,MAAM,gCAAgC,aAAa,QAAQ,OAAO;GAClE,MAAM;EACR,CAAC;EAEH,OAAO;CACT;AACF,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import { t as truncate } from "./truncate-CChlZ113.mjs";
2
- import { normPath } from "@zaly/shared";
3
2
  import { AiError, defineTool } from "@zaly/ai";
3
+ import { normPath } from "@zaly/shared";
4
4
  import { Type } from "typebox";
5
5
  import { Spawn, TextStream, which } from "@zaly/shared/process";
6
6
  import { cleanTextTui } from "@zaly/shared/text";
@@ -182,4 +182,4 @@ function grepGlobArgs(glob) {
182
182
  }
183
183
  export { grepTool };
184
184
 
185
- //# sourceMappingURL=grep-rvZ5S319.mjs.map
185
+ //# sourceMappingURL=grep-BI8Rou2q.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"grep-rvZ5S319.mjs","names":[],"sources":["../src/tools/grep.ts"],"sourcesContent":["import type { MetaPart, TextPart, ToolContext } from \"@zaly/ai\"\n\nimport { AiError, defineTool } from \"@zaly/ai\"\nimport { normPath } from \"@zaly/shared\"\nimport { defaultExcludes } from \"@zaly/shared/find\"\nimport { Spawn, TextStream, which } from \"@zaly/shared/process\"\nimport { cleanTextTui } from \"@zaly/shared/text\"\nimport { Type } from \"typebox\"\nimport { truncate } from \"../utils/truncate.ts\"\n\nconst MAX_BUFFER = 512 * 1024\nconst DEFAULT_LIMIT = 100\nconst MAX_LIMIT = 1000\nconst MAX_COLUMNS = 200\n\nexport type GrepTool = typeof grepTool\nexport type GrepToolMeta = {\n cwd: string\n pattern: string\n truncated: boolean\n cmd: string[]\n}\n\n// oxlint-disable-next-line sort-keys -- semantic field order: name, desc, params, call\nexport const grepTool = defineTool({\n name: \"grep\",\n desc:\n `Search file contents using rg/grep. Respects .gitignore by default when ripgrep is available; ` +\n \"prefer this over bash grep/find for code search. Returns human-readable grouped matches with line numbers.\",\n parallel: true,\n // oxlint-disable-next-line sort-keys -- semantic param order\n params: Type.Object({\n pattern: Type.String({\n description:\n \"Text or regex pattern. Uses smart case: case-insensitive unless uppercase is present.\",\n minLength: 1,\n }),\n paths: Type.Optional(\n Type.Array(Type.String(), {\n description: \"Optional files/directories to search, relative to cwd or absolute.\",\n })\n ),\n glob: Type.Optional(\n Type.Array(Type.String(), {\n description: \"Ripgrep glob filters, e.g. [`*.ts`, `!dist/**`].\",\n })\n ),\n fixed_strings: Type.Optional(\n Type.Boolean({\n default: false,\n description: \"Treat all patterns as literals instead of as regular expressions\",\n })\n ),\n context: Type.Optional(\n Type.Integer({\n default: 0,\n description: \"Context lines before/after each match. 0-10.\",\n maximum: 10,\n minimum: 0,\n })\n ),\n limit: Type.Optional(\n Type.Integer({\n default: DEFAULT_LIMIT,\n description: \"Maximum output lines to keep inline, including file headings and context.\",\n maximum: MAX_LIMIT,\n minimum: 1,\n })\n ),\n }),\n\n async preflight(args, ctx: ToolContext<GrepToolMeta>) {\n const cwd = normPath(ctx.cwd, \".\")\n await ctx.need?.(\"read\", cwd)\n\n const paths = (args.paths?.length ? args.paths : [\".\"]).map((p) => normPath(cwd, p))\n await Promise.all(paths.map((path) => Promise.resolve(ctx.need?.(\"read\", path))))\n },\n\n async call(args, ctx: ToolContext<GrepToolMeta>): Promise<(MetaPart | TextPart)[]> {\n const command = resolveGrep()\n if (!command) throw new AiError({ code: \"MISSING_TOOL\", message: \"grep requires rg or grep\" })\n\n const cwd = normPath(ctx.cwd, \".\")\n const paths = args.paths?.length ? args.paths : []\n\n const cmdArgs = buildArgs(command.kind, args, paths)\n const proc = new Spawn(command.cmd, cmdArgs, {\n cwd,\n maxBuffer: MAX_BUFFER,\n signal: ctx.signal,\n stderr: new TextStream(),\n stdout: new TextStream(),\n timeout: 60_000,\n })\n const result = await proc.result.catch((error: unknown) => {\n throw new AiError({ cause: error, code: \"GREP_FAILED\", message: String(error) })\n })\n\n const text = cleanTextTui(result.stdout)\n const stderr = cleanTextTui(result.stderr).trim()\n // rg/grep can report matches and still exit with an error for another search path.\n const failed = result.code > 1 && result.killReason !== \"maxBuffer\"\n if (failed && !text.trim()) {\n throw new AiError({\n code: \"GREP_FAILED\",\n data: { code: result.code, stderr },\n message: `${command.cmd} failed (${result.code}): ${stderr.slice(0, 500)}`,\n })\n }\n\n const limit = args.limit ?? DEFAULT_LIMIT\n const summary = truncate(text, {\n maxLineChars: command.kind === \"rg\" ? 500 : MAX_COLUMNS,\n maxLines: limit + 1,\n strategy: \"head\",\n })\n\n const truncated = summary.truncated || result.killReason === \"maxBuffer\"\n ctx.meta = {\n cmd: [command.cmd, ...cmdArgs],\n cwd,\n pattern: args.pattern,\n truncated,\n }\n\n const parts: (MetaPart | TextPart)[] = []\n\n if (stderr || result.killReason || failed) {\n const meta: Record<string, unknown> = {\n code: result.code,\n command: command.cmd,\n cwd,\n incomplete: failed || !!result.killReason,\n truncated,\n }\n if (result.killReason) meta.killReason = result.killReason\n if (stderr) meta.stderr = stderr.slice(0, 500)\n parts.push({ data: meta, tag: \"grep\", type: \"meta\" })\n }\n parts.push({ text: summary.text || \"No matches found.\", type: \"text\" })\n if (failed) {\n const warning = `Warning: Search incomplete (${command.kind} exited with code ${result.code}).`\n parts.push({ text: stderr ? `${warning}\\n${stderr.slice(0, 500)}` : warning, type: \"text\" })\n }\n return parts\n },\n})\n\ntype GrepArgs = Parameters<GrepTool[\"call\"]>[0]\ntype GrepBackend = { cmd: string; kind: \"rg\" | \"grep\" }\n\nfunction resolveGrep(): GrepBackend | undefined {\n const rg = which(\"rg\")\n if (rg) return { cmd: rg, kind: \"rg\" }\n const grep = which(\"grep\")\n return grep ? { cmd: grep, kind: \"grep\" } : undefined\n}\n\nfunction buildArgs(kind: GrepBackend[\"kind\"], args: GrepArgs, paths: string[]): string[] {\n return kind === \"rg\" ? buildRgArgs(args, paths) : buildGrepArgs(args, paths)\n}\n\nfunction buildRgArgs(args: GrepArgs, paths: string[]): string[] {\n const ret = [\n \"--color=always\",\n \"--heading\",\n \"--with-filename\",\n \"--line-number\",\n `--max-columns=${MAX_COLUMNS}`,\n \"--max-columns-preview\",\n \"--path-separator=/\",\n ]\n\n if (args.fixed_strings) ret.push(\"--fixed-strings\")\n ret.push(\"--smart-case\", \"--no-hidden\")\n const context = args.context ?? 0\n if (context > 0) ret.push(`--context=${context}`)\n\n for (const e of defaultExcludes(paths)) ret.push(\"--glob\", `!${e}`)\n for (const g of args.glob ?? []) ret.push(\"--glob\", g)\n\n ret.push(\"--\", args.pattern, ...paths)\n return ret\n}\n\nfunction buildGrepArgs(args: GrepArgs, paths: string[]): string[] {\n const ret = [\n \"--recursive\",\n \"--line-number\",\n \"--with-filename\",\n \"--binary-files=without-match\",\n \"--color=always\",\n ]\n if (args.fixed_strings) ret.push(\"--fixed-strings\")\n else ret.push(\"--extended-regexp\")\n const hasUpper = /[A-Z]/.test(args.pattern)\n if (!hasUpper) ret.push(\"--ignore-case\")\n const context = args.context ?? 0\n if (context > 0) ret.push(\"--context\", String(context))\n for (const e of defaultExcludes(paths)) ret.push(\"--exclude-dir\", e)\n for (const g of args.glob ?? []) ret.push(...grepGlobArgs(g))\n ret.push(\"--\", args.pattern, ...paths)\n return ret\n}\n\nfunction grepGlobArgs(glob: string): string[] {\n if (!glob.startsWith(\"!\")) return [\"--include\", glob]\n const exclude = glob.slice(1)\n if (exclude.endsWith(\"/**\") && !exclude.slice(0, -3).includes(\"*\")) {\n return [\"--exclude-dir\", exclude.slice(0, -3)]\n }\n return [\"--exclude\", exclude]\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"grep-BI8Rou2q.mjs","names":[],"sources":["../src/tools/grep.ts"],"sourcesContent":["import type { MetaPart, TextPart, ToolContext } from \"@zaly/ai\"\n\nimport { AiError, defineTool } from \"@zaly/ai\"\nimport { normPath } from \"@zaly/shared\"\nimport { defaultExcludes } from \"@zaly/shared/find\"\nimport { Spawn, TextStream, which } from \"@zaly/shared/process\"\nimport { cleanTextTui } from \"@zaly/shared/text\"\nimport { Type } from \"typebox\"\nimport { truncate } from \"../utils/truncate.ts\"\n\nconst MAX_BUFFER = 512 * 1024\nconst DEFAULT_LIMIT = 100\nconst MAX_LIMIT = 1000\nconst MAX_COLUMNS = 200\n\nexport type GrepTool = typeof grepTool\nexport type GrepToolMeta = {\n cwd: string\n pattern: string\n truncated: boolean\n cmd: string[]\n}\n\n// oxlint-disable-next-line sort-keys -- semantic field order: name, desc, params, call\nexport const grepTool = defineTool({\n name: \"grep\",\n desc:\n `Search file contents using rg/grep. Respects .gitignore by default when ripgrep is available; ` +\n \"prefer this over bash grep/find for code search. Returns human-readable grouped matches with line numbers.\",\n parallel: true,\n // oxlint-disable-next-line sort-keys -- semantic param order\n params: Type.Object({\n pattern: Type.String({\n description:\n \"Text or regex pattern. Uses smart case: case-insensitive unless uppercase is present.\",\n minLength: 1,\n }),\n paths: Type.Optional(\n Type.Array(Type.String(), {\n description: \"Optional files/directories to search, relative to cwd or absolute.\",\n })\n ),\n glob: Type.Optional(\n Type.Array(Type.String(), {\n description: \"Ripgrep glob filters, e.g. [`*.ts`, `!dist/**`].\",\n })\n ),\n fixed_strings: Type.Optional(\n Type.Boolean({\n default: false,\n description: \"Treat all patterns as literals instead of as regular expressions\",\n })\n ),\n context: Type.Optional(\n Type.Integer({\n default: 0,\n description: \"Context lines before/after each match. 0-10.\",\n maximum: 10,\n minimum: 0,\n })\n ),\n limit: Type.Optional(\n Type.Integer({\n default: DEFAULT_LIMIT,\n description: \"Maximum output lines to keep inline, including file headings and context.\",\n maximum: MAX_LIMIT,\n minimum: 1,\n })\n ),\n }),\n\n async preflight(args, ctx: ToolContext<GrepToolMeta>) {\n const cwd = normPath(ctx.cwd, \".\")\n await ctx.need?.(\"read\", cwd)\n\n const paths = (args.paths?.length ? args.paths : [\".\"]).map((p) => normPath(cwd, p))\n await Promise.all(paths.map((path) => Promise.resolve(ctx.need?.(\"read\", path))))\n },\n\n async call(args, ctx: ToolContext<GrepToolMeta>): Promise<(MetaPart | TextPart)[]> {\n const command = resolveGrep()\n if (!command) throw new AiError({ code: \"MISSING_TOOL\", message: \"grep requires rg or grep\" })\n\n const cwd = normPath(ctx.cwd, \".\")\n const paths = args.paths?.length ? args.paths : []\n\n const cmdArgs = buildArgs(command.kind, args, paths)\n const proc = new Spawn(command.cmd, cmdArgs, {\n cwd,\n maxBuffer: MAX_BUFFER,\n signal: ctx.signal,\n stderr: new TextStream(),\n stdout: new TextStream(),\n timeout: 60_000,\n })\n const result = await proc.result.catch((error: unknown) => {\n throw new AiError({ cause: error, code: \"GREP_FAILED\", message: String(error) })\n })\n\n const text = cleanTextTui(result.stdout)\n const stderr = cleanTextTui(result.stderr).trim()\n // rg/grep can report matches and still exit with an error for another search path.\n const failed = result.code > 1 && result.killReason !== \"maxBuffer\"\n if (failed && !text.trim()) {\n throw new AiError({\n code: \"GREP_FAILED\",\n data: { code: result.code, stderr },\n message: `${command.cmd} failed (${result.code}): ${stderr.slice(0, 500)}`,\n })\n }\n\n const limit = args.limit ?? DEFAULT_LIMIT\n const summary = truncate(text, {\n maxLineChars: command.kind === \"rg\" ? 500 : MAX_COLUMNS,\n maxLines: limit + 1,\n strategy: \"head\",\n })\n\n const truncated = summary.truncated || result.killReason === \"maxBuffer\"\n ctx.meta = {\n cmd: [command.cmd, ...cmdArgs],\n cwd,\n pattern: args.pattern,\n truncated,\n }\n\n const parts: (MetaPart | TextPart)[] = []\n\n if (stderr || result.killReason || failed) {\n const meta: Record<string, unknown> = {\n code: result.code,\n command: command.cmd,\n cwd,\n incomplete: failed || !!result.killReason,\n truncated,\n }\n if (result.killReason) meta.killReason = result.killReason\n if (stderr) meta.stderr = stderr.slice(0, 500)\n parts.push({ data: meta, tag: \"grep\", type: \"meta\" })\n }\n parts.push({ text: summary.text || \"No matches found.\", type: \"text\" })\n if (failed) {\n const warning = `Warning: Search incomplete (${command.kind} exited with code ${result.code}).`\n parts.push({ text: stderr ? `${warning}\\n${stderr.slice(0, 500)}` : warning, type: \"text\" })\n }\n return parts\n },\n})\n\ntype GrepArgs = Parameters<GrepTool[\"call\"]>[0]\ntype GrepBackend = { cmd: string; kind: \"rg\" | \"grep\" }\n\nfunction resolveGrep(): GrepBackend | undefined {\n const rg = which(\"rg\")\n if (rg) return { cmd: rg, kind: \"rg\" }\n const grep = which(\"grep\")\n return grep ? { cmd: grep, kind: \"grep\" } : undefined\n}\n\nfunction buildArgs(kind: GrepBackend[\"kind\"], args: GrepArgs, paths: string[]): string[] {\n return kind === \"rg\" ? buildRgArgs(args, paths) : buildGrepArgs(args, paths)\n}\n\nfunction buildRgArgs(args: GrepArgs, paths: string[]): string[] {\n const ret = [\n \"--color=always\",\n \"--heading\",\n \"--with-filename\",\n \"--line-number\",\n `--max-columns=${MAX_COLUMNS}`,\n \"--max-columns-preview\",\n \"--path-separator=/\",\n ]\n\n if (args.fixed_strings) ret.push(\"--fixed-strings\")\n ret.push(\"--smart-case\", \"--no-hidden\")\n const context = args.context ?? 0\n if (context > 0) ret.push(`--context=${context}`)\n\n for (const e of defaultExcludes(paths)) ret.push(\"--glob\", `!${e}`)\n for (const g of args.glob ?? []) ret.push(\"--glob\", g)\n\n ret.push(\"--\", args.pattern, ...paths)\n return ret\n}\n\nfunction buildGrepArgs(args: GrepArgs, paths: string[]): string[] {\n const ret = [\n \"--recursive\",\n \"--line-number\",\n \"--with-filename\",\n \"--binary-files=without-match\",\n \"--color=always\",\n ]\n if (args.fixed_strings) ret.push(\"--fixed-strings\")\n else ret.push(\"--extended-regexp\")\n const hasUpper = /[A-Z]/.test(args.pattern)\n if (!hasUpper) ret.push(\"--ignore-case\")\n const context = args.context ?? 0\n if (context > 0) ret.push(\"--context\", String(context))\n for (const e of defaultExcludes(paths)) ret.push(\"--exclude-dir\", e)\n for (const g of args.glob ?? []) ret.push(...grepGlobArgs(g))\n ret.push(\"--\", args.pattern, ...paths)\n return ret\n}\n\nfunction grepGlobArgs(glob: string): string[] {\n if (!glob.startsWith(\"!\")) return [\"--include\", glob]\n const exclude = glob.slice(1)\n if (exclude.endsWith(\"/**\") && !exclude.slice(0, -3).includes(\"*\")) {\n return [\"--exclude-dir\", exclude.slice(0, -3)]\n }\n return [\"--exclude\", exclude]\n}\n"],"mappings":"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"}