@zaly/agent 0.0.3
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/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/agent-BaD2kax9.mjs +68 -0
- package/dist/agent-BaD2kax9.mjs.map +1 -0
- package/dist/agent-GDLdDc2w.mjs +902 -0
- package/dist/agent-GDLdDc2w.mjs.map +1 -0
- package/dist/bash-QNMTQ1PD.mjs +142 -0
- package/dist/bash-QNMTQ1PD.mjs.map +1 -0
- package/dist/compactions-3AjPqjeZ.mjs +190 -0
- package/dist/compactions-3AjPqjeZ.mjs.map +1 -0
- package/dist/edit-Qxl4w1C2.mjs +170 -0
- package/dist/edit-Qxl4w1C2.mjs.map +1 -0
- package/dist/env-UUFv5Mye.mjs +30 -0
- package/dist/env-UUFv5Mye.mjs.map +1 -0
- package/dist/fetch-D8cmmA3E.mjs +111 -0
- package/dist/fetch-D8cmmA3E.mjs.map +1 -0
- package/dist/find-Bah9SKWX.mjs +87 -0
- package/dist/find-Bah9SKWX.mjs.map +1 -0
- package/dist/grep-B2Ax7ewy.mjs +244 -0
- package/dist/grep-B2Ax7ewy.mjs.map +1 -0
- package/dist/index-node-esm-D6YQRKEz.mjs +1537 -0
- package/dist/index-node-esm-D6YQRKEz.mjs.map +1 -0
- package/dist/index.d.mts +1605 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +948 -0
- package/dist/index.mjs.map +1 -0
- package/dist/jsonl-Dw_VBp46.mjs +134 -0
- package/dist/jsonl-Dw_VBp46.mjs.map +1 -0
- package/dist/manager-D-lWBTHG.mjs +271 -0
- package/dist/manager-D-lWBTHG.mjs.map +1 -0
- package/dist/markdown-Bv4J8cg3.mjs +53 -0
- package/dist/markdown-Bv4J8cg3.mjs.map +1 -0
- package/dist/masker-Bv9KjkDF.mjs +396 -0
- package/dist/masker-Bv9KjkDF.mjs.map +1 -0
- package/dist/model-_3ZA2xqm.mjs +27 -0
- package/dist/model-_3ZA2xqm.mjs.map +1 -0
- package/dist/notify-DpG_m9iP.mjs +96 -0
- package/dist/notify-DpG_m9iP.mjs.map +1 -0
- package/dist/read-Djsx2NZl.mjs +205 -0
- package/dist/read-Djsx2NZl.mjs.map +1 -0
- package/dist/registry-CFmR5G3J.mjs +564 -0
- package/dist/registry-CFmR5G3J.mjs.map +1 -0
- package/dist/search-CRw247ES.mjs +121 -0
- package/dist/search-CRw247ES.mjs.map +1 -0
- package/dist/session/claude.d.mts +59 -0
- package/dist/session/claude.d.mts.map +1 -0
- package/dist/session/claude.mjs +455 -0
- package/dist/session/claude.mjs.map +1 -0
- package/dist/session-CUUdWFC5.mjs +450 -0
- package/dist/session-CUUdWFC5.mjs.map +1 -0
- package/dist/session-Ds5nJz5A.d.mts +346 -0
- package/dist/session-Ds5nJz5A.d.mts.map +1 -0
- package/dist/session.d.mts +37 -0
- package/dist/session.d.mts.map +1 -0
- package/dist/session.mjs +106 -0
- package/dist/session.mjs.map +1 -0
- package/dist/subagent-baqzjRMc.mjs +136 -0
- package/dist/subagent-baqzjRMc.mjs.map +1 -0
- package/dist/swarm-BJ3gQA1Z.mjs +194 -0
- package/dist/swarm-BJ3gQA1Z.mjs.map +1 -0
- package/dist/swarm-BuFx_xeY.mjs +109 -0
- package/dist/swarm-BuFx_xeY.mjs.map +1 -0
- package/dist/tasks-D8rARHZ1.mjs +65 -0
- package/dist/tasks-D8rARHZ1.mjs.map +1 -0
- package/dist/tasks-WmC270X2.mjs +615 -0
- package/dist/tasks-WmC270X2.mjs.map +1 -0
- package/dist/tools-DW1dbcCz.mjs +695 -0
- package/dist/tools-DW1dbcCz.mjs.map +1 -0
- package/dist/truncate-BSMXgiXj.mjs +102 -0
- package/dist/truncate-BSMXgiXj.mjs.map +1 -0
- package/dist/uuid-ByXP5SO5.mjs +59 -0
- package/dist/uuid-ByXP5SO5.mjs.map +1 -0
- package/dist/wakeup-BYqTnfSp.mjs +29 -0
- package/dist/wakeup-BYqTnfSp.mjs.map +1 -0
- package/dist/write-D3L-92vj.mjs +89 -0
- package/dist/write-D3L-92vj.mjs.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { assertFresh } from "./read-Djsx2NZl.mjs";
|
|
2
|
+
import { normPath } from "@zaly/shared";
|
|
3
|
+
import { AiError, defineTool } from "@zaly/ai";
|
|
4
|
+
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
import { detectEol, normalizeEol } from "@zaly/shared/text";
|
|
7
|
+
//#region src/tools/edit.ts
|
|
8
|
+
/**
|
|
9
|
+
* Apply one or more exact-text replacements to a file.
|
|
10
|
+
*
|
|
11
|
+
* Semantics:
|
|
12
|
+
* - All `oldText` matches are resolved against the *original* file
|
|
13
|
+
* content. Edits are not applied sequentially — so edit #1's
|
|
14
|
+
* `newText` cannot accidentally produce a match for edit #2's
|
|
15
|
+
* `oldText`.
|
|
16
|
+
* - Each `oldText` must occur exactly once in the file (uniqueness
|
|
17
|
+
* forces the model to disambiguate via context rather than guessing).
|
|
18
|
+
* - Edits must not overlap. Nearby changes should be combined into a
|
|
19
|
+
* single edit covering the surrounding block.
|
|
20
|
+
* - The file is rewritten atomically: either every edit applies
|
|
21
|
+
* cleanly or none do.
|
|
22
|
+
*
|
|
23
|
+
* Use `read` first to inspect the file. Keep `oldText` minimal but
|
|
24
|
+
* uniquely identifying. For full-file rewrites, use `write` instead.
|
|
25
|
+
*/
|
|
26
|
+
const editTool = defineTool({
|
|
27
|
+
name: "edit",
|
|
28
|
+
desc: "Apply an exact-text replacement to a file, with optional extra replacements.. All matches are resolved against the original content (not sequentially), each `oldText` must be unique in the file, and edits must not overlap. Atomic: all-or-nothing.",
|
|
29
|
+
params: Type.Object({
|
|
30
|
+
path: Type.String({ description: "Path to the file. Absolute or cwd-relative." }),
|
|
31
|
+
oldText: Type.String({ description: "Exact text to find. Must occur once in the file." }),
|
|
32
|
+
newText: Type.String({ description: "Replacement text." }),
|
|
33
|
+
edits: Type.Optional(Type.Array(Type.Object({
|
|
34
|
+
oldText: Type.String({ description: "Old text" }),
|
|
35
|
+
newText: Type.String({ description: "New text" })
|
|
36
|
+
}), {
|
|
37
|
+
description: "Extra edits to apply atomically.",
|
|
38
|
+
minItems: 1
|
|
39
|
+
}))
|
|
40
|
+
}),
|
|
41
|
+
async preflight(args, ctx) {
|
|
42
|
+
const path = normPath(ctx.cwd, args.path);
|
|
43
|
+
await ctx.need?.("write", path);
|
|
44
|
+
},
|
|
45
|
+
async call(args, ctx) {
|
|
46
|
+
const path = normPath(ctx.cwd, args.path);
|
|
47
|
+
const edits = [{
|
|
48
|
+
newText: args.newText,
|
|
49
|
+
oldText: args.oldText
|
|
50
|
+
}, ...args.edits ?? []];
|
|
51
|
+
assertFresh(path, ctx);
|
|
52
|
+
const original = await readFile(path, "utf8").catch((error) => {
|
|
53
|
+
throw new AiError({
|
|
54
|
+
cause: error,
|
|
55
|
+
code: "FILE_NOT_FOUND",
|
|
56
|
+
message: `cannot edit ${path}: file not found`
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
const updated = applyEdits(original, edits, path);
|
|
60
|
+
await writeFile(path, updated, "utf8");
|
|
61
|
+
ctx.meta = {
|
|
62
|
+
content: updated,
|
|
63
|
+
kind: "edit",
|
|
64
|
+
mtime: (await stat(path)).mtimeMs,
|
|
65
|
+
original,
|
|
66
|
+
path
|
|
67
|
+
};
|
|
68
|
+
const bytes = Buffer.byteLength(updated, "utf8");
|
|
69
|
+
const lines = countLines(updated);
|
|
70
|
+
return {
|
|
71
|
+
bytes,
|
|
72
|
+
edits: edits.length,
|
|
73
|
+
lines,
|
|
74
|
+
ok: true,
|
|
75
|
+
path
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
/** Resolve all edit positions against `original`, validate uniqueness +
|
|
80
|
+
* non-overlap, then splice the replacements in.
|
|
81
|
+
*
|
|
82
|
+
* Line-ending fidelity: the model always emits LF, but the file may be
|
|
83
|
+
* CRLF. We detect the file's eol once, then normalize each `oldText` /
|
|
84
|
+
* `newText` to that eol before matching/splicing. The result has the
|
|
85
|
+
* same line-ending style as the original file. */
|
|
86
|
+
function applyEdits(original, edits, path) {
|
|
87
|
+
const eol = detectEol({
|
|
88
|
+
path,
|
|
89
|
+
text: original
|
|
90
|
+
});
|
|
91
|
+
const matches = [];
|
|
92
|
+
for (const [i, edit] of edits.entries()) {
|
|
93
|
+
const oldText = normalizeEol(edit.oldText, { eol });
|
|
94
|
+
const newText = normalizeEol(edit.newText, { eol });
|
|
95
|
+
if (oldText === "") throw new AiError({
|
|
96
|
+
code: "EMPTY_OLD_TEXT",
|
|
97
|
+
data: { editIndex: i },
|
|
98
|
+
message: `edit #${i}: oldText is empty (use \`write\` to create or fully replace a file)`
|
|
99
|
+
});
|
|
100
|
+
const first = original.indexOf(oldText);
|
|
101
|
+
if (first === -1) throw new AiError({
|
|
102
|
+
code: "NOT_FOUND",
|
|
103
|
+
data: {
|
|
104
|
+
editIndex: i,
|
|
105
|
+
path,
|
|
106
|
+
snippet: preview(oldText)
|
|
107
|
+
},
|
|
108
|
+
message: `edit #${i}: oldText not found in ${path}`
|
|
109
|
+
});
|
|
110
|
+
if (original.indexOf(oldText, first + 1) !== -1) throw new AiError({
|
|
111
|
+
code: "NOT_UNIQUE",
|
|
112
|
+
data: {
|
|
113
|
+
editIndex: i,
|
|
114
|
+
occurrences: countOccurrences(original, oldText),
|
|
115
|
+
path
|
|
116
|
+
},
|
|
117
|
+
message: `edit #${i}: oldText matches multiple locations in ${path}. Add surrounding context to make it unique.`
|
|
118
|
+
});
|
|
119
|
+
matches.push({
|
|
120
|
+
end: first + oldText.length,
|
|
121
|
+
newText,
|
|
122
|
+
spec: edit,
|
|
123
|
+
start: first
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
matches.sort((a, b) => a.start - b.start);
|
|
127
|
+
for (let i = 1; i < matches.length; i++) if (matches[i].start < matches[i - 1].end) throw new AiError({
|
|
128
|
+
code: "OVERLAP",
|
|
129
|
+
data: {
|
|
130
|
+
a: matches[i - 1],
|
|
131
|
+
b: matches[i]
|
|
132
|
+
},
|
|
133
|
+
message: `edits overlap in ${path}. Combine adjacent changes into a single edit covering both.`
|
|
134
|
+
});
|
|
135
|
+
const out = [];
|
|
136
|
+
let cursor = 0;
|
|
137
|
+
for (const m of matches) {
|
|
138
|
+
out.push(original.slice(cursor, m.start), m.newText);
|
|
139
|
+
cursor = m.end;
|
|
140
|
+
}
|
|
141
|
+
out.push(original.slice(cursor));
|
|
142
|
+
return out.join("");
|
|
143
|
+
}
|
|
144
|
+
/** Count lines the way an editor would — non-empty trailing line counts;
|
|
145
|
+
* a trailing newline doesn't add a phantom blank line. */
|
|
146
|
+
function countLines(content) {
|
|
147
|
+
if (content === "") return 0;
|
|
148
|
+
let n = 1;
|
|
149
|
+
for (const c of content) if (c === "\n") n++;
|
|
150
|
+
if (content.endsWith("\n")) n--;
|
|
151
|
+
return n;
|
|
152
|
+
}
|
|
153
|
+
function countOccurrences(haystack, needle) {
|
|
154
|
+
let n = 0;
|
|
155
|
+
let from = 0;
|
|
156
|
+
for (;;) {
|
|
157
|
+
const i = haystack.indexOf(needle, from);
|
|
158
|
+
if (i === -1) return n;
|
|
159
|
+
n++;
|
|
160
|
+
from = i + needle.length;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function preview(text) {
|
|
164
|
+
const trimmed = text.trim().replaceAll(/\s+/g, " ");
|
|
165
|
+
return trimmed.length > 80 ? `${trimmed.slice(0, 79)}…` : trimmed;
|
|
166
|
+
}
|
|
167
|
+
//#endregion
|
|
168
|
+
export { editTool };
|
|
169
|
+
|
|
170
|
+
//# sourceMappingURL=edit-Qxl4w1C2.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edit-Qxl4w1C2.mjs","names":[],"sources":["../src/tools/edit.ts"],"sourcesContent":["import type { ToolContext } from \"@zaly/ai\"\nimport type { FileMeta } from \"./read.ts\"\n\nimport { defineTool, AiError } from \"@zaly/ai\"\nimport { normPath } from \"@zaly/shared\"\nimport { detectEol, normalizeEol } from \"@zaly/shared/text\"\nimport { readFile, writeFile, stat } from \"node:fs/promises\"\nimport { Type } from \"typebox\"\nimport { assertFresh } from \"./read.ts\"\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 an exact-text replacement to a file, with optional extra replacements.. All matches are \" +\n \"resolved against the original content (not sequentially), each `oldText` \" +\n \"must be unique in the file, and edits must not overlap. \" +\n \"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 oldText: Type.String({ description: \"Exact text to find. Must occur once in the file.\" }),\n newText: Type.String({ description: \"Replacement text.\" }),\n edits: Type.Optional(\n Type.Array(\n // oxlint-disable-next-line sort-keys -- semantic order: oldText then newText\n Type.Object({\n oldText: Type.String({ description: \"Old text\" }),\n newText: Type.String({ description: \"New text\" }),\n }),\n { description: \"Extra edits to apply atomically.\", minItems: 1 }\n )\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 edits = [{ newText: args.newText, oldText: args.oldText }, ...(args.edits ?? [])]\n\n // Edit always requires freshness — the operation is content-aware,\n // so the model must have seen the current bytes.\n assertFresh(path, ctx)\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, 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: 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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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { which } from "@zaly/shared/process";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
//#region src/prompt/env.ts
|
|
4
|
+
function prompt(ctx) {
|
|
5
|
+
return [
|
|
6
|
+
"## Environment",
|
|
7
|
+
`- Platform: ${process.platform} (${os.release()})`,
|
|
8
|
+
`- Arch: ${process.arch}`,
|
|
9
|
+
process.env.SHELL ? `- Shell: ${process.env.SHELL}` : void 0,
|
|
10
|
+
`- Runtime: ${runtime()}`,
|
|
11
|
+
`- Cwd: ${ctx.cwd}`,
|
|
12
|
+
searchTools()
|
|
13
|
+
].filter(Boolean).join("\n");
|
|
14
|
+
}
|
|
15
|
+
function runtime() {
|
|
16
|
+
if (typeof Bun !== "undefined") return `bun ${Bun.version}`;
|
|
17
|
+
return `node ${process.versions.node}`;
|
|
18
|
+
}
|
|
19
|
+
function searchTools() {
|
|
20
|
+
const tools = [];
|
|
21
|
+
if (which("rg")) tools.push("rg");
|
|
22
|
+
if (which("fd")) tools.push("fd");
|
|
23
|
+
else if (which("fdfind")) tools.push("fdfind");
|
|
24
|
+
if (tools.length === 0) return;
|
|
25
|
+
return `- Search tools: ${tools.join(", ")} available; prefer them over grep/find because they are fast and respect .gitignore by default.`;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { prompt };
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=env-UUFv5Mye.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-UUFv5Mye.mjs","names":[],"sources":["../src/prompt/env.ts"],"sourcesContent":["import type { PromptCtx } from \"./registry.ts\"\n\nimport { which } from \"@zaly/shared/process\"\nimport os from \"node:os\"\n\nexport function prompt(ctx: PromptCtx): string {\n const lines = [\n \"## Environment\",\n `- Platform: ${process.platform} (${os.release()})`,\n `- Arch: ${process.arch}`,\n process.env.SHELL ? `- Shell: ${process.env.SHELL}` : undefined,\n `- Runtime: ${runtime()}`,\n `- Cwd: ${ctx.cwd}`,\n searchTools(),\n ]\n return lines.filter(Boolean).join(\"\\n\")\n}\n\nfunction runtime(): string {\n if (typeof Bun !== \"undefined\") return `bun ${Bun.version}`\n return `node ${process.versions.node}`\n}\n\nfunction searchTools(): string | undefined {\n const tools: string[] = []\n if (which(\"rg\")) tools.push(\"rg\")\n if (which(\"fd\")) tools.push(\"fd\")\n else if (which(\"fdfind\")) tools.push(\"fdfind\")\n if (tools.length === 0) return\n return `- Search tools: ${tools.join(\", \")} available; prefer them over grep/find because they are fast and respect .gitignore by default.`\n}\n"],"mappings":";;;AAKA,SAAgB,OAAO,KAAwB;CAU7C,OAAO;EARL;EACA,eAAe,QAAQ,SAAS,IAAI,GAAG,QAAQ,EAAE;EACjD,WAAW,QAAQ;EACnB,QAAQ,IAAI,QAAQ,YAAY,QAAQ,IAAI,UAAU,KAAA;EACtD,cAAc,QAAQ;EACtB,UAAU,IAAI;EACd,YAAY;CAEH,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,IAAI;AACxC;AAEA,SAAS,UAAkB;CACzB,IAAI,OAAO,QAAQ,aAAa,OAAO,OAAO,IAAI;CAClD,OAAO,QAAQ,QAAQ,SAAS;AAClC;AAEA,SAAS,cAAkC;CACzC,MAAM,QAAkB,CAAC;CACzB,IAAI,MAAM,IAAI,GAAG,MAAM,KAAK,IAAI;CAChC,IAAI,MAAM,IAAI,GAAG,MAAM,KAAK,IAAI;MAC3B,IAAI,MAAM,QAAQ,GAAG,MAAM,KAAK,QAAQ;CAC7C,IAAI,MAAM,WAAW,GAAG;CACxB,OAAO,mBAAmB,MAAM,KAAK,IAAI,EAAE;AAC7C"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { defineTool, toAttachment } from "@zaly/ai";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
//#region src/tools/fetch.ts
|
|
4
|
+
const DEFAULT_MAX_BODY_BYTES = 256 * 1024;
|
|
5
|
+
/**
|
|
6
|
+
* Fetch a URL and return the response — JSON when the server speaks JSON,
|
|
7
|
+
* raw text otherwise. Optional `jsonpath` filters the parsed JSON to a
|
|
8
|
+
* subset, which keeps tokens down on chatty APIs.
|
|
9
|
+
*
|
|
10
|
+
* Scope: API endpoints. For HTML pages (browsing, content extraction),
|
|
11
|
+
* the optional `@zaly/browser` package provides browser-backed tools that
|
|
12
|
+
* return a structured a11y tree — much better than raw HTML through this
|
|
13
|
+
* tool. Pointing this at an HTML URL works but the response is a string
|
|
14
|
+
* blob the model has to slog through.
|
|
15
|
+
*
|
|
16
|
+
* Permissions: routes through the `fetch` scope when the agent wires up
|
|
17
|
+
* tool-side permission checks. Until then, callers should restrict via
|
|
18
|
+
* a wrapping permission layer (rule-based or explicit allowlist).
|
|
19
|
+
*/
|
|
20
|
+
const fetchTool = defineTool({
|
|
21
|
+
desc: "Fetch a URL and return the response. JSON responses are parsed; use `jsonpath` (e.g. `$.items[*].name`) to extract a subset and minimise tokens.Image & PDF are returned as attachments. Best for APIs — for web pages, use the browser tool.",
|
|
22
|
+
name: "fetch",
|
|
23
|
+
parallel: true,
|
|
24
|
+
params: Type.Object({
|
|
25
|
+
url: Type.String({ description: "Absolute URL." }),
|
|
26
|
+
method: Type.Optional(Type.Union([
|
|
27
|
+
Type.Literal("GET"),
|
|
28
|
+
Type.Literal("POST"),
|
|
29
|
+
Type.Literal("PUT"),
|
|
30
|
+
Type.Literal("PATCH"),
|
|
31
|
+
Type.Literal("DELETE")
|
|
32
|
+
], {
|
|
33
|
+
default: "GET",
|
|
34
|
+
description: "HTTP method."
|
|
35
|
+
})),
|
|
36
|
+
headers: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Request headers." })),
|
|
37
|
+
query: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Query string params, appended to the URL." })),
|
|
38
|
+
body: Type.Optional(Type.String({ description: "Request body as a string. For JSON, stringify it yourself and set `headers: { 'Content-Type': 'application/json' }`." })),
|
|
39
|
+
jsonpath: Type.Optional(Type.String({ description: "JSONPath expression applied to a JSON response, e.g. `$.items[*].name`. Returns the matched values as an array." }))
|
|
40
|
+
}),
|
|
41
|
+
async call(args) {
|
|
42
|
+
const t0 = Date.now();
|
|
43
|
+
const url = new URL(args.url);
|
|
44
|
+
if (args.query) for (const [k, v] of Object.entries(args.query)) url.searchParams.set(k, v);
|
|
45
|
+
const res = await fetch(url, {
|
|
46
|
+
body: args.body,
|
|
47
|
+
headers: args.headers,
|
|
48
|
+
method: args.method
|
|
49
|
+
});
|
|
50
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
51
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
52
|
+
const meta = {
|
|
53
|
+
contentType: contentType || void 0,
|
|
54
|
+
durationMs: Date.now() - t0,
|
|
55
|
+
ok: res.ok,
|
|
56
|
+
status: res.status,
|
|
57
|
+
statusText: res.statusText
|
|
58
|
+
};
|
|
59
|
+
const { fileDetect } = await import("@zaly/shared/detect");
|
|
60
|
+
const file = await fileDetect({
|
|
61
|
+
data: bytes,
|
|
62
|
+
mime: contentType || void 0,
|
|
63
|
+
url: url.toString()
|
|
64
|
+
});
|
|
65
|
+
if (file) {
|
|
66
|
+
const att = await toAttachment(file);
|
|
67
|
+
if (att) return [{
|
|
68
|
+
data: meta,
|
|
69
|
+
tag: "fetch",
|
|
70
|
+
type: "meta"
|
|
71
|
+
}, att];
|
|
72
|
+
}
|
|
73
|
+
const text = new TextDecoder().decode(bytes);
|
|
74
|
+
const isJson = file?.type === "text" && file.format === "json";
|
|
75
|
+
let body = text;
|
|
76
|
+
if (isJson) try {
|
|
77
|
+
body = JSON.parse(text);
|
|
78
|
+
} catch {}
|
|
79
|
+
if (args.jsonpath !== void 0 && body !== text) {
|
|
80
|
+
const { JSONPath } = await import("./index-node-esm-D6YQRKEz.mjs");
|
|
81
|
+
body = JSONPath({
|
|
82
|
+
json: body,
|
|
83
|
+
path: args.jsonpath
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const bodyText = typeof body === "string" ? body : JSON.stringify(body, void 0, 2);
|
|
87
|
+
const totalBytes = Buffer.byteLength(bodyText, "utf8");
|
|
88
|
+
const truncated = totalBytes > DEFAULT_MAX_BODY_BYTES;
|
|
89
|
+
const displayed = truncated ? bodyText.slice(0, DEFAULT_MAX_BODY_BYTES) : bodyText;
|
|
90
|
+
if (truncated) meta.truncated = {
|
|
91
|
+
bytes: totalBytes,
|
|
92
|
+
hint: args.jsonpath === void 0 ? "body exceeded limit; pass `jsonpath` to filter, or use the browser tool for HTML." : "filtered body still exceeded limit; tighten the JSONPath expression.",
|
|
93
|
+
limit: DEFAULT_MAX_BODY_BYTES
|
|
94
|
+
};
|
|
95
|
+
const parts = [{
|
|
96
|
+
data: meta,
|
|
97
|
+
tag: "fetch",
|
|
98
|
+
type: "meta"
|
|
99
|
+
}];
|
|
100
|
+
if (displayed !== "") parts.push({
|
|
101
|
+
format: isJson ? "json" : void 0,
|
|
102
|
+
text: displayed,
|
|
103
|
+
type: "text"
|
|
104
|
+
});
|
|
105
|
+
return parts;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
//#endregion
|
|
109
|
+
export { fetchTool };
|
|
110
|
+
|
|
111
|
+
//# sourceMappingURL=fetch-D8cmmA3E.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-D8cmmA3E.mjs","names":[],"sources":["../src/tools/fetch.ts"],"sourcesContent":["import type { Attachment, MetaPart, TextPart } from \"@zaly/ai\"\n\nimport { defineTool, toAttachment } from \"@zaly/ai\"\nimport { Type } from \"typebox\"\n\nconst DEFAULT_MAX_BODY_BYTES = 256 * 1024 // 256 KB — generous for APIs, caps runaway HTML pages\n\n/**\n * Fetch a URL and return the response — JSON when the server speaks JSON,\n * raw text otherwise. Optional `jsonpath` filters the parsed JSON to a\n * subset, which keeps tokens down on chatty APIs.\n *\n * Scope: API endpoints. For HTML pages (browsing, content extraction),\n * the optional `@zaly/browser` package provides browser-backed tools that\n * return a structured a11y tree — much better than raw HTML through this\n * tool. Pointing this at an HTML URL works but the response is a string\n * blob the model has to slog through.\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 `Image & PDF are returned as attachments. Best for APIs — for web pages, use the browser tool.`,\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 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): Promise<(Attachment | MetaPart | TextPart)[]> {\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 res = await fetch(url, {\n body: args.body,\n headers: args.headers,\n method: args.method,\n })\n\n const contentType = res.headers.get(\"content-type\") ?? \"\"\n const bytes = new Uint8Array(await res.arrayBuffer())\n\n const meta: Record<string, unknown> = {\n contentType: contentType || undefined,\n durationMs: Date.now() - t0,\n ok: res.ok,\n status: res.status,\n statusText: res.statusText,\n }\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: url.toString(),\n })\n if (file) {\n const att = await toAttachment(file)\n if (att) return [{ data: meta, tag: \"fetch\", type: \"meta\" }, att]\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 if (isJson) {\n try {\n body = JSON.parse(text)\n } catch {\n // Server lied about content-type; keep as text.\n }\n }\n\n if (args.jsonpath !== undefined && body !== text) {\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 // Cap the body before it reaches the model. Total bytes received is\n // surfaced via the meta so the model can decide whether to refetch\n // with `jsonpath` or use a different tool (browser for HTML, etc.).\n const totalBytes = Buffer.byteLength(bodyText, \"utf8\")\n const truncated = totalBytes > DEFAULT_MAX_BODY_BYTES\n const displayed = truncated ? bodyText.slice(0, DEFAULT_MAX_BODY_BYTES) : bodyText\n\n if (truncated) {\n meta.truncated = {\n bytes: totalBytes,\n hint:\n args.jsonpath === undefined\n ? \"body exceeded limit; pass `jsonpath` to filter, or use the browser tool for HTML.\"\n : \"filtered body still exceeded limit; tighten the JSONPath expression.\",\n limit: DEFAULT_MAX_BODY_BYTES,\n }\n }\n\n const parts: (MetaPart | TextPart)[] = [{ data: meta, tag: \"fetch\", type: \"meta\" }]\n if (displayed !== \"\")\n parts.push({ format: isJson ? \"json\" : undefined, text: displayed, type: \"text\" })\n return parts\n },\n})\n"],"mappings":";;;AAKA,MAAM,yBAAyB,MAAM;;;;;;;;;;;;;;;;AAkBrC,MAAa,YAAY,WAAW;CAClC,MACE;CAEF,MAAM;CACN,UAAU;CAGV,QAAQ,KAAK,OAAO;EAClB,KAAK,KAAK,OAAO,EAAE,aAAa,gBAAgB,CAAC;EACjD,QAAQ,KAAK,SACX,KAAK,MACH;GACE,KAAK,QAAQ,KAAK;GAClB,KAAK,QAAQ,MAAM;GACnB,KAAK,QAAQ,KAAK;GAClB,KAAK,QAAQ,OAAO;GACpB,KAAK,QAAQ,QAAQ;EACvB,GACA;GAAE,SAAS;GAAO,aAAa;EAAe,CAChD,CACF;EACA,SAAS,KAAK,SACZ,KAAK,OAAO,KAAK,OAAO,GAAG,KAAK,OAAO,GAAG,EACxC,aAAa,mBACf,CAAC,CACH;EACA,OAAO,KAAK,SACV,KAAK,OAAO,KAAK,OAAO,GAAG,KAAK,OAAO,GAAG,EACxC,aAAa,4CACf,CAAC,CACH;EACA,MAAM,KAAK,SACT,KAAK,OAAO,EACV,aACE,uHAEJ,CAAC,CACH;EACA,UAAU,KAAK,SACb,KAAK,OAAO,EACV,aACE,kHAEJ,CAAC,CACH;CACF,CAAC;CACD,MAAM,KAAK,MAAqD;EAC9D,MAAM,KAAK,KAAK,IAAI;EACpB,MAAM,MAAM,IAAI,IAAI,KAAK,GAAG;EAC5B,IAAI,KAAK,OACP,KAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,KAAK,KAAK,GAAG,IAAI,aAAa,IAAI,GAAG,CAAC;EAG5E,MAAM,MAAM,MAAM,MAAM,KAAK;GAC3B,MAAM,KAAK;GACX,SAAS,KAAK;GACd,QAAQ,KAAK;EACf,CAAC;EAED,MAAM,cAAc,IAAI,QAAQ,IAAI,cAAc,KAAK;EACvD,MAAM,QAAQ,IAAI,WAAW,MAAM,IAAI,YAAY,CAAC;EAEpD,MAAM,OAAgC;GACpC,aAAa,eAAe,KAAA;GAC5B,YAAY,KAAK,IAAI,IAAI;GACzB,IAAI,IAAI;GACR,QAAQ,IAAI;GACZ,YAAY,IAAI;EAClB;EASA,MAAM,EAAE,eAAe,MAAM,OAAO;EACpC,MAAM,OAAO,MAAM,WAAW;GAC5B,MAAM;GACN,MAAM,eAAe,KAAA;GACrB,KAAK,IAAI,SAAS;EACpB,CAAC;EACD,IAAI,MAAM;GACR,MAAM,MAAM,MAAM,aAAa,IAAI;GACnC,IAAI,KAAK,OAAO,CAAC;IAAE,MAAM;IAAM,KAAK;IAAS,MAAM;GAAO,GAAG,GAAG;EAClE;EAEA,MAAM,OAAO,IAAI,YAAY,CAAC,CAAC,OAAO,KAAK;EAI3C,MAAM,SAAS,MAAM,SAAS,UAAU,KAAK,WAAW;EAExD,IAAI,OAAgB;EACpB,IAAI,QACF,IAAI;GACF,OAAO,KAAK,MAAM,IAAI;EACxB,QAAQ,CAER;EAGF,IAAI,KAAK,aAAa,KAAA,KAAa,SAAS,MAAM;GAIhD,MAAM,EAAE,aAAa,MAAM,OAAO;GAElC,OAAO,SAAS;IAAE,MAAM;IAAgB,MAAM,KAAK;GAAS,CAAC;EAC/D;EAIA,MAAM,WAAW,OAAO,SAAS,WAAW,OAAO,KAAK,UAAU,MAAM,KAAA,GAAW,CAAC;EAKpF,MAAM,aAAa,OAAO,WAAW,UAAU,MAAM;EACrD,MAAM,YAAY,aAAa;EAC/B,MAAM,YAAY,YAAY,SAAS,MAAM,GAAG,sBAAsB,IAAI;EAE1E,IAAI,WACF,KAAK,YAAY;GACf,OAAO;GACP,MACE,KAAK,aAAa,KAAA,IACd,sFACA;GACN,OAAO;EACT;EAGF,MAAM,QAAiC,CAAC;GAAE,MAAM;GAAM,KAAK;GAAS,MAAM;EAAO,CAAC;EAClF,IAAI,cAAc,IAChB,MAAM,KAAK;GAAE,QAAQ,SAAS,SAAS,KAAA;GAAW,MAAM;GAAW,MAAM;EAAO,CAAC;EACnF,OAAO;CACT;AACF,CAAC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { t as truncate } from "./truncate-BSMXgiXj.mjs";
|
|
2
|
+
import { normPath } from "@zaly/shared";
|
|
3
|
+
import { AiError, defineTool } from "@zaly/ai";
|
|
4
|
+
import { Type } from "typebox";
|
|
5
|
+
import { find } from "@zaly/shared/find";
|
|
6
|
+
//#region src/tools/find.ts
|
|
7
|
+
const DEFAULT_LIMIT = 200;
|
|
8
|
+
const findTool = defineTool({
|
|
9
|
+
name: "find",
|
|
10
|
+
desc: "Find files using fd/fdfind or rg --files. `glob` is a filename/path glob, array of globs, or plain substring; empty, `*`, or `**/*` lists all files. Respects .gitignore by default; prefer this over bash find for file discovery.",
|
|
11
|
+
parallel: true,
|
|
12
|
+
params: Type.Object({
|
|
13
|
+
glob: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())], {
|
|
14
|
+
default: "",
|
|
15
|
+
description: "Filename/path glob, array of globs, or plain substring. Empty, `*`, or `**/*` lists all files."
|
|
16
|
+
})),
|
|
17
|
+
cwd: Type.Optional(Type.String({ description: "Directory to search from. Defaults to cwd." })),
|
|
18
|
+
paths: Type.Optional(Type.Array(Type.String(), { description: "Optional directories to search, relative to cwd or absolute." })),
|
|
19
|
+
type: Type.Optional(Type.Union([
|
|
20
|
+
Type.Literal("file"),
|
|
21
|
+
Type.Literal("dir"),
|
|
22
|
+
Type.Literal("any")
|
|
23
|
+
], {
|
|
24
|
+
default: "file",
|
|
25
|
+
description: "Entry type to return. `dir` requires fd/fdfind or find fallback."
|
|
26
|
+
})),
|
|
27
|
+
exclude: Type.Optional(Type.Array(Type.String(), { description: "Patterns to exclude, e.g. [`node_modules`, `dist/**`]." })),
|
|
28
|
+
hidden: Type.Optional(Type.Boolean({
|
|
29
|
+
default: false,
|
|
30
|
+
description: "Include hidden files/directories."
|
|
31
|
+
})),
|
|
32
|
+
ignore: Type.Optional(Type.Boolean({
|
|
33
|
+
default: true,
|
|
34
|
+
description: "Respect .gitignore/.ignore rules."
|
|
35
|
+
})),
|
|
36
|
+
follow: Type.Optional(Type.Boolean({
|
|
37
|
+
default: false,
|
|
38
|
+
description: "Follow symlinks."
|
|
39
|
+
})),
|
|
40
|
+
limit: Type.Optional(Type.Integer({
|
|
41
|
+
default: DEFAULT_LIMIT,
|
|
42
|
+
description: "Maximum paths to keep inline.",
|
|
43
|
+
maximum: 1e3,
|
|
44
|
+
minimum: 1
|
|
45
|
+
}))
|
|
46
|
+
}),
|
|
47
|
+
async preflight(args, ctx) {
|
|
48
|
+
const cwd = normPath(ctx.cwd, args.cwd ?? ".");
|
|
49
|
+
await ctx.need?.("read", cwd);
|
|
50
|
+
const paths = (args.paths?.length ? args.paths : ["."]).map((p) => normPath(cwd, p));
|
|
51
|
+
await Promise.all(paths.map((path) => Promise.resolve(ctx.need?.("read", path))));
|
|
52
|
+
},
|
|
53
|
+
async call(args, ctx) {
|
|
54
|
+
const cwd = normPath(ctx.cwd, args.cwd ?? ".");
|
|
55
|
+
let result = [];
|
|
56
|
+
try {
|
|
57
|
+
result = (await Array.fromAsync(find({
|
|
58
|
+
...args,
|
|
59
|
+
cwd,
|
|
60
|
+
pattern: args.glob,
|
|
61
|
+
signal: ctx.signal
|
|
62
|
+
}))).flat();
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw AiError.from(error);
|
|
65
|
+
}
|
|
66
|
+
const limit = args.limit ?? DEFAULT_LIMIT;
|
|
67
|
+
const summary = truncate(result.join("\n"), {
|
|
68
|
+
maxLines: limit + 1,
|
|
69
|
+
strategy: "head"
|
|
70
|
+
});
|
|
71
|
+
const truncated = summary.truncated;
|
|
72
|
+
ctx.meta = {
|
|
73
|
+
cwd,
|
|
74
|
+
glob: args.glob ?? "",
|
|
75
|
+
matches: summary.origLines,
|
|
76
|
+
truncated
|
|
77
|
+
};
|
|
78
|
+
return [{
|
|
79
|
+
text: summary.text || "No files found.",
|
|
80
|
+
type: "text"
|
|
81
|
+
}];
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
//#endregion
|
|
85
|
+
export { findTool };
|
|
86
|
+
|
|
87
|
+
//# sourceMappingURL=find-Bah9SKWX.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"find-Bah9SKWX.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 } from \"@zaly/shared/find\"\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 \"Respects .gitignore by default; 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 cwd: Type.Optional(Type.String({ description: \"Directory to search from. Defaults to cwd.\" })),\n paths: Type.Optional(\n Type.Array(Type.String(), {\n description: \"Optional directories to search, relative to cwd or absolute.\",\n })\n ),\n type: Type.Optional(\n Type.Union([Type.Literal(\"file\"), Type.Literal(\"dir\"), Type.Literal(\"any\")], {\n default: \"file\",\n description: \"Entry type to return. `dir` requires fd/fdfind or find fallback.\",\n })\n ),\n exclude: Type.Optional(\n Type.Array(Type.String(), {\n description: \"Patterns to exclude, e.g. [`node_modules`, `dist/**`].\",\n })\n ),\n hidden: Type.Optional(\n Type.Boolean({ default: false, description: \"Include hidden files/directories.\" })\n ),\n ignore: Type.Optional(\n Type.Boolean({ default: true, description: \"Respect .gitignore/.ignore rules.\" })\n ),\n follow: Type.Optional(Type.Boolean({ default: false, description: \"Follow symlinks.\" })),\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, args.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, args.cwd ?? \".\")\n\n let result: string[] = []\n try {\n const batch = await Array.fromAsync(\n find({ ...args, cwd, pattern: args.glob, signal: ctx.signal })\n )\n result = batch.flat()\n } catch (error) {\n throw AiError.from(error)\n }\n\n const limit = args.limit ?? DEFAULT_LIMIT\n const summary = truncate(result.join(\"\\n\"), {\n maxLines: limit + 1,\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 return [{ text: summary.text || \"No files found.\", type: \"text\" }]\n },\n})\n"],"mappings":";;;;;;AAQA,MAAM,gBAAgB;AAYtB,MAAa,WAAW,WAAW;CACjC,MAAM;CACN,MACE;CAGF,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,KAAK,KAAK,SAAS,KAAK,OAAO,EAAE,aAAa,6CAA6C,CAAC,CAAC;EAC7F,OAAO,KAAK,SACV,KAAK,MAAM,KAAK,OAAO,GAAG,EACxB,aAAa,+DACf,CAAC,CACH;EACA,MAAM,KAAK,SACT,KAAK,MAAM;GAAC,KAAK,QAAQ,MAAM;GAAG,KAAK,QAAQ,KAAK;GAAG,KAAK,QAAQ,KAAK;EAAC,GAAG;GAC3E,SAAS;GACT,aAAa;EACf,CAAC,CACH;EACA,SAAS,KAAK,SACZ,KAAK,MAAM,KAAK,OAAO,GAAG,EACxB,aAAa,yDACf,CAAC,CACH;EACA,QAAQ,KAAK,SACX,KAAK,QAAQ;GAAE,SAAS;GAAO,aAAa;EAAoC,CAAC,CACnF;EACA,QAAQ,KAAK,SACX,KAAK,QAAQ;GAAE,SAAS;GAAM,aAAa;EAAoC,CAAC,CAClF;EACA,QAAQ,KAAK,SAAS,KAAK,QAAQ;GAAE,SAAS;GAAO,aAAa;EAAmB,CAAC,CAAC;EACvF,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,KAAK,OAAO,GAAG;EAC7C,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,KAAK,OAAO,GAAG;EAE7C,IAAI,SAAmB,CAAC;EACxB,IAAI;GAIF,UAAS,MAHW,MAAM,UACxB,KAAK;IAAE,GAAG;IAAM;IAAK,SAAS,KAAK;IAAM,QAAQ,IAAI;GAAO,CAAC,CAC/D,EAAA,CACe,KAAK;EACtB,SAAS,OAAO;GACd,MAAM,QAAQ,KAAK,KAAK;EAC1B;EAEA,MAAM,QAAQ,KAAK,SAAS;EAC5B,MAAM,UAAU,SAAS,OAAO,KAAK,IAAI,GAAG;GAC1C,UAAU,QAAQ;GAClB,UAAU;EACZ,CAAC;EAED,MAAM,YAAY,QAAQ;EAC1B,IAAI,OAAO;GACT;GACA,MAAM,KAAK,QAAQ;GACnB,SAAS,QAAQ;GACjB;EACF;EAEA,OAAO,CAAC;GAAE,MAAM,QAAQ,QAAQ;GAAmB,MAAM;EAAO,CAAC;CACnE;AACF,CAAC"}
|