@ocis/myagent-cli 0.2.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 (91) hide show
  1. package/README.md +357 -0
  2. package/dist/agent/context.d.ts +33 -0
  3. package/dist/agent/context.js +169 -0
  4. package/dist/agent/modes.d.ts +21 -0
  5. package/dist/agent/modes.js +84 -0
  6. package/dist/agent/prompt-builder.d.ts +9 -0
  7. package/dist/agent/prompt-builder.js +31 -0
  8. package/dist/agent/sessions.d.ts +38 -0
  9. package/dist/agent/sessions.js +130 -0
  10. package/dist/agent/todo.d.ts +18 -0
  11. package/dist/agent/todo.js +61 -0
  12. package/dist/agent/turn.d.ts +309 -0
  13. package/dist/agent/turn.js +1253 -0
  14. package/dist/approval/policy.d.ts +81 -0
  15. package/dist/approval/policy.js +157 -0
  16. package/dist/config.d.ts +49 -0
  17. package/dist/config.js +156 -0
  18. package/dist/git/status.d.ts +89 -0
  19. package/dist/git/status.js +226 -0
  20. package/dist/headless.d.ts +72 -0
  21. package/dist/headless.js +330 -0
  22. package/dist/index.d.ts +60 -0
  23. package/dist/index.js +511 -0
  24. package/dist/protocol/client.d.ts +123 -0
  25. package/dist/protocol/client.js +250 -0
  26. package/dist/protocol/sse-frames.d.ts +6 -0
  27. package/dist/protocol/sse-frames.js +75 -0
  28. package/dist/protocol/types.d.ts +200 -0
  29. package/dist/protocol/types.js +8 -0
  30. package/dist/runtime.d.ts +38 -0
  31. package/dist/runtime.js +166 -0
  32. package/dist/sanitize.d.ts +1 -0
  33. package/dist/sanitize.js +21 -0
  34. package/dist/skills/discovery.d.ts +24 -0
  35. package/dist/skills/discovery.js +109 -0
  36. package/dist/tools/binary.d.ts +2 -0
  37. package/dist/tools/binary.js +22 -0
  38. package/dist/tools/diff.d.ts +1 -0
  39. package/dist/tools/diff.js +49 -0
  40. package/dist/tools/find.d.ts +2 -0
  41. package/dist/tools/find.js +61 -0
  42. package/dist/tools/fs.d.ts +2 -0
  43. package/dist/tools/fs.js +276 -0
  44. package/dist/tools/glob.d.ts +6 -0
  45. package/dist/tools/glob.js +131 -0
  46. package/dist/tools/grep.d.ts +3 -0
  47. package/dist/tools/grep.js +228 -0
  48. package/dist/tools/paths.d.ts +27 -0
  49. package/dist/tools/paths.js +124 -0
  50. package/dist/tools/registry.d.ts +13 -0
  51. package/dist/tools/registry.js +38 -0
  52. package/dist/tools/shell.d.ts +2 -0
  53. package/dist/tools/shell.js +136 -0
  54. package/dist/tools/skills.d.ts +2 -0
  55. package/dist/tools/skills.js +36 -0
  56. package/dist/tools/todo.d.ts +2 -0
  57. package/dist/tools/todo.js +43 -0
  58. package/dist/tools/transfer.d.ts +2 -0
  59. package/dist/tools/transfer.js +145 -0
  60. package/dist/tools/truncate.d.ts +12 -0
  61. package/dist/tools/truncate.js +46 -0
  62. package/dist/tools/types.d.ts +85 -0
  63. package/dist/tools/types.js +63 -0
  64. package/dist/ui/app.d.ts +39 -0
  65. package/dist/ui/app.js +1061 -0
  66. package/dist/ui/colors.d.ts +100 -0
  67. package/dist/ui/colors.js +169 -0
  68. package/dist/ui/components.d.ts +267 -0
  69. package/dist/ui/components.js +811 -0
  70. package/dist/ui/diff.d.ts +37 -0
  71. package/dist/ui/diff.js +143 -0
  72. package/dist/ui/format.d.ts +28 -0
  73. package/dist/ui/format.js +76 -0
  74. package/dist/ui/help.d.ts +6 -0
  75. package/dist/ui/help.js +45 -0
  76. package/dist/ui/highlight.d.ts +20 -0
  77. package/dist/ui/highlight.js +210 -0
  78. package/dist/ui/logo.d.ts +24 -0
  79. package/dist/ui/logo.js +106 -0
  80. package/dist/ui/model-list.d.ts +10 -0
  81. package/dist/ui/model-list.js +33 -0
  82. package/dist/ui/quit-confirm.d.ts +8 -0
  83. package/dist/ui/quit-confirm.js +40 -0
  84. package/dist/ui/select-popup.d.ts +30 -0
  85. package/dist/ui/select-popup.js +54 -0
  86. package/dist/ui/theme.d.ts +4 -0
  87. package/dist/ui/theme.js +41 -0
  88. package/dist/ui/tool-view.d.ts +20 -0
  89. package/dist/ui/tool-view.js +326 -0
  90. package/package.json +44 -0
  91. package/skills/git-commit/SKILL.md +27 -0
@@ -0,0 +1,166 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Runtime shims: Bun APIs on Bun, node: equivalents on Node.
3
+ //
4
+ // The CLI runs on both runtimes. Bun's file/spawn/stdin/which globals have no
5
+ // Node counterpart, so every call site goes through this module instead of
6
+ // touching `Bun.*` directly. The Bun branches are guarded by `typeof Bun`, so
7
+ // the emitted JS never touches the `Bun` global on Node.
8
+ //
9
+ // The subprocess shim normalizes both runtimes to one `ProcessHandle` (Web
10
+ // ReadableStreams for stdout/stderr, a FileSink-like stdin, `.pid`, `.kill()`,
11
+ // `.exited`) — the same shape `Bun.spawn` returns, so the shell/git/grep tools
12
+ // run unchanged on either runtime.
13
+ // ---------------------------------------------------------------------------
14
+ import { spawn } from "node:child_process";
15
+ import { realpathSync, statSync } from "node:fs";
16
+ import { readFile, stat, writeFile as writeFileFs } from "node:fs/promises";
17
+ import { Buffer } from "node:buffer";
18
+ import { delimiter, join } from "node:path";
19
+ import { Readable } from "node:stream";
20
+ import { fileURLToPath } from "node:url";
21
+ export const isBun = typeof Bun !== "undefined";
22
+ // --- files -----------------------------------------------------------------
23
+ export async function fileExists(path) {
24
+ if (isBun)
25
+ return Bun.file(path).exists();
26
+ try {
27
+ return (await stat(path)).isFile();
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ export async function readTextFile(path) {
34
+ if (isBun)
35
+ return Bun.file(path).text();
36
+ return readFile(path, "utf8");
37
+ }
38
+ export async function readJsonFile(path) {
39
+ return JSON.parse(await readTextFile(path));
40
+ }
41
+ export async function writeFile(path, data) {
42
+ if (isBun) {
43
+ await Bun.write(path, data);
44
+ return;
45
+ }
46
+ await writeFileFs(path, data);
47
+ }
48
+ // --- which -----------------------------------------------------------------
49
+ /** Resolve an executable on PATH (Bun.which on Bun, a PATH scan on Node). */
50
+ export function which(cmd) {
51
+ if (isBun && Bun.which)
52
+ return Bun.which(cmd);
53
+ const dirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
54
+ const exts = process.platform === "win32"
55
+ ? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)
56
+ : [""];
57
+ const names = cmd.includes(".") ? [cmd] : exts.map((ext) => cmd + ext);
58
+ for (const dir of dirs) {
59
+ for (const name of names) {
60
+ const candidate = join(dir, name);
61
+ try {
62
+ if (statSync(candidate).isFile())
63
+ return candidate;
64
+ }
65
+ catch {
66
+ /* keep looking */
67
+ }
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+ // --- stdin -----------------------------------------------------------------
73
+ /** Read all of stdin as UTF-8 text. Callers check `process.stdin.isTTY` first. */
74
+ export async function readStdinText() {
75
+ if (isBun)
76
+ return new Response(Bun.stdin.stream()).text();
77
+ const chunks = [];
78
+ for await (const chunk of process.stdin)
79
+ chunks.push(Buffer.from(chunk));
80
+ return Buffer.concat(chunks).toString("utf8");
81
+ }
82
+ // --- entry detection -------------------------------------------------------
83
+ /**
84
+ * True when the caller's module is the process entry point. `import.meta` is
85
+ * module-scoped, so the caller must pass its own (`isMainModule(import.meta)`).
86
+ * Bun and Node ≥24.2 expose `import.meta.main`; older Node falls back to
87
+ * comparing the resolved entry path (realpath, so npm/bun bin symlinks match).
88
+ */
89
+ export function isMainModule(meta) {
90
+ const main = meta.main;
91
+ if (typeof main === "boolean")
92
+ return main;
93
+ const entry = process.argv[1];
94
+ if (!entry)
95
+ return false;
96
+ try {
97
+ return realpathSync(entry) === realpathSync(fileURLToPath(meta.url));
98
+ }
99
+ catch {
100
+ return false;
101
+ }
102
+ }
103
+ export function spawnProcess(args, opts) {
104
+ return isBun ? spawnBun(args, opts) : spawnNode(args, opts);
105
+ }
106
+ function spawnBun(args, opts) {
107
+ const proc = Bun.spawn(args, {
108
+ cwd: opts?.cwd,
109
+ env: opts?.env ? { ...process.env, ...opts.env } : undefined,
110
+ stdin: opts?.stdin === "pipe" ? "pipe" : "ignore",
111
+ stdout: opts?.stdout === "pipe" ? "pipe" : "ignore",
112
+ stderr: opts?.stderr === "pipe" ? "pipe" : "ignore",
113
+ detached: opts?.detached,
114
+ });
115
+ return {
116
+ pid: proc.pid,
117
+ stdout: proc.stdout,
118
+ stderr: proc.stderr,
119
+ stdin: {
120
+ write: (data) => { proc.stdin.write(data); },
121
+ end: () => { proc.stdin.end(); },
122
+ },
123
+ exited: proc.exited,
124
+ kill: (signal) => { try {
125
+ proc.kill(signal);
126
+ }
127
+ catch { /* already gone */ } },
128
+ };
129
+ }
130
+ function spawnNode(args, opts) {
131
+ const child = spawn(args[0], args.slice(1), {
132
+ cwd: opts?.cwd,
133
+ env: opts?.env ? { ...process.env, ...opts.env } : process.env,
134
+ stdio: [
135
+ opts?.stdin === "pipe" ? "pipe" : "ignore",
136
+ opts?.stdout === "pipe" ? "pipe" : "ignore",
137
+ opts?.stderr === "pipe" ? "pipe" : "ignore",
138
+ ],
139
+ detached: opts?.detached ?? false,
140
+ });
141
+ // First of close/error wins: a spawn failure (ENOENT) may emit only `error`.
142
+ const exited = new Promise((resolve) => {
143
+ child.once("close", (code) => resolve(code ?? 0));
144
+ child.once("error", () => resolve(-1));
145
+ });
146
+ return {
147
+ pid: child.pid ?? 0,
148
+ stdout: child.stdout
149
+ ? Readable.toWeb(child.stdout)
150
+ : new ReadableStream(),
151
+ stderr: child.stderr
152
+ ? Readable.toWeb(child.stderr)
153
+ : new ReadableStream(),
154
+ stdin: {
155
+ write: (data) => { if (child.stdin)
156
+ child.stdin.write(Buffer.from(data)); },
157
+ end: () => { if (child.stdin)
158
+ child.stdin.end(); },
159
+ },
160
+ exited,
161
+ kill: (signal) => { try {
162
+ child.kill(signal);
163
+ }
164
+ catch { /* already dead */ } },
165
+ };
166
+ }
@@ -0,0 +1 @@
1
+ export declare function sanitizeForTerminal(text: string): string;
@@ -0,0 +1,21 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Terminal-sequence sanitizing for untrusted text rendered into the TUI.
3
+ //
4
+ // Tool output and file content come from the workspace (an untrusted repo the
5
+ // agent is working on) and are written to the user's real terminal — embedded
6
+ // OSC/CSI/C0 sequences could otherwise move the cursor, rewrite the title, or
7
+ // clear the screen. Shared by the git status parser (remote-controlled path
8
+ // names) and the tool-call renderer (raw output/file bodies).
9
+ // ---------------------------------------------------------------------------
10
+ export function sanitizeForTerminal(text) {
11
+ return text
12
+ .replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)?/g, "") // OSC … (BEL / ST)
13
+ // CSI: parameter + intermediate bytes then a final byte. SPACE (0x20) is a
14
+ // legal intermediate (`CSI Ps SP q` sets the cursor shape), so it belongs
15
+ // in the class — without it that sequence's tail leaked through as noise.
16
+ .replace(/\u001b\[[ 0-9;:?<=>!"#$%&'()*+,\-./]*[@-~]/g, "") // CSI sequences
17
+ .replace(/\u001b[@-Z\\\-_]/g, "") // remaining two-byte escapes
18
+ // Stray C0/C1 controls. \t is kept — a literal tab in a filename is
19
+ // visible and harmless, and \n cannot survive line-based output anyway.
20
+ .replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, "");
21
+ }
@@ -0,0 +1,24 @@
1
+ export interface LocalSkill {
2
+ name: string;
3
+ description: string;
4
+ dir: string;
5
+ files: string[];
6
+ /** Full SKILL.md text, read once during discovery. */
7
+ md: string;
8
+ }
9
+ /** Parse the leading `--- ... ---` frontmatter for name/description. */
10
+ export declare function parseFrontmatter(md: string): {
11
+ name?: string;
12
+ description?: string;
13
+ };
14
+ /** Discover skills across the given directories; earlier dirs win. */
15
+ export declare function discoverSkills(dirs: string[]): Promise<LocalSkill[]>;
16
+ /** One-line-per-skill listing (tool output + `myagent skills`). */
17
+ export declare function formatSkillList(skills: LocalSkill[]): string;
18
+ /** `<available_skills>` block for the system prompt (empty when no skills). */
19
+ export declare function buildSkillsCatalog(skills: LocalSkill[]): string;
20
+ /**
21
+ * Resolve the ordered, existing skill search paths. The global dir is created
22
+ * if missing; the others are included only when they already exist.
23
+ */
24
+ export declare function resolveSkillDirs(workspace: string, configDirPath: string, extraDirs?: string[]): Promise<string[]>;
@@ -0,0 +1,109 @@
1
+ // -------------------------------------------------------------------
2
+ // Local skills: SKILL.md discovery on the user's machine.
3
+ //
4
+ // A skill is a directory with a SKILL.md (YAML frontmatter name/description +
5
+ // instructions) that the agent loads on demand via load_local_skill. Search
6
+ // paths, in priority order (earlier wins on a name collision):
7
+ // 1. --skills-dir values (resolved absolute)
8
+ // 2. <workspace>/.myagent/skills
9
+ // 3. <configDir>/skills (created on first run so there's always a home)
10
+ // -------------------------------------------------------------------
11
+ import { mkdir, readdir, stat } from "node:fs/promises";
12
+ import { join, resolve } from "node:path";
13
+ import { readTextFile } from "../runtime.js";
14
+ /** Parse the leading `--- ... ---` frontmatter for name/description. */
15
+ export function parseFrontmatter(md) {
16
+ const match = md.match(/^---\r?\n([\s\S]*?)\r?\n---/);
17
+ if (!match)
18
+ return {};
19
+ const out = {};
20
+ for (const line of match[1].split("\n")) {
21
+ const m = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
22
+ if (m)
23
+ out[m[1]] = m[2].trim().replace(/^["']|["']$/g, "");
24
+ }
25
+ return { name: out.name, description: out.description };
26
+ }
27
+ async function loadSkillDir(dir, fallbackName) {
28
+ let md;
29
+ try {
30
+ md = await readTextFile(join(dir, "SKILL.md"));
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ const fm = parseFrontmatter(md);
36
+ const files = (await readdir(dir).catch(() => []));
37
+ return {
38
+ name: fm.name || fallbackName,
39
+ description: fm.description || "(no description)",
40
+ dir,
41
+ files,
42
+ md,
43
+ };
44
+ }
45
+ /** Discover skills across the given directories; earlier dirs win. */
46
+ export async function discoverSkills(dirs) {
47
+ const skills = [];
48
+ const seen = new Set();
49
+ for (const root of dirs) {
50
+ let entries;
51
+ try {
52
+ entries = await readdir(root, { withFileTypes: true });
53
+ }
54
+ catch {
55
+ continue;
56
+ }
57
+ const loaded = await Promise.all(entries.filter((e) => e.isDirectory()).map((e) => loadSkillDir(join(root, e.name), e.name)));
58
+ for (const skill of loaded) {
59
+ if (!skill || seen.has(skill.name))
60
+ continue;
61
+ seen.add(skill.name);
62
+ skills.push(skill);
63
+ }
64
+ }
65
+ return skills;
66
+ }
67
+ /** One-line-per-skill listing (tool output + `myagent skills`). */
68
+ export function formatSkillList(skills) {
69
+ if (skills.length === 0)
70
+ return "(no local skills installed)";
71
+ return skills.map((s) => `- ${s.name}: ${s.description}`).join("\n");
72
+ }
73
+ /** `<available_skills>` block for the system prompt (empty when no skills). */
74
+ export function buildSkillsCatalog(skills) {
75
+ if (skills.length === 0)
76
+ return "";
77
+ return [
78
+ "<available_skills>",
79
+ "Load a skill on demand with load_local_skill when a task matches:",
80
+ ...skills.map((s) => ` - ${s.name}: ${s.description}`),
81
+ "</available_skills>",
82
+ ].join("\n");
83
+ }
84
+ /**
85
+ * Resolve the ordered, existing skill search paths. The global dir is created
86
+ * if missing; the others are included only when they already exist.
87
+ */
88
+ export async function resolveSkillDirs(workspace, configDirPath, extraDirs = []) {
89
+ const globalDir = join(configDirPath, "skills");
90
+ await mkdir(globalDir, { recursive: true }).catch((err) => {
91
+ process.stderr.write(`[warn] Could not create global skills dir ${globalDir}: ${err instanceof Error ? err.message : String(err)}\n`);
92
+ });
93
+ const ordered = [
94
+ ...extraDirs.map((d) => resolve(d)),
95
+ join(workspace, ".myagent", "skills"),
96
+ globalDir,
97
+ ];
98
+ const dirs = [];
99
+ const seen = new Set();
100
+ for (const dir of ordered) {
101
+ if (seen.has(dir))
102
+ continue;
103
+ seen.add(dir);
104
+ const st = await stat(dir).catch(() => null);
105
+ if (st?.isDirectory())
106
+ dirs.push(dir);
107
+ }
108
+ return dirs;
109
+ }
@@ -0,0 +1,2 @@
1
+ /** True when the buffer looks like binary content (NUL byte or invalid UTF-8). */
2
+ export declare function isBinaryBuffer(buffer: Buffer): boolean;
@@ -0,0 +1,22 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Shared binary sniffing for local file tools.
3
+ //
4
+ // One implementation for every caller (local_read/local_edit and
5
+ // local_transfer_files) so they cannot drift: a file is binary when it
6
+ // contains a NUL byte or is not valid UTF-8 — the same rule the server's
7
+ // workspace API applies to uploads.
8
+ // ---------------------------------------------------------------------------
9
+ /** True when the buffer looks like binary content (NUL byte or invalid UTF-8). */
10
+ export function isBinaryBuffer(buffer) {
11
+ for (const byte of buffer) {
12
+ if (byte === 0)
13
+ return true;
14
+ }
15
+ try {
16
+ new TextDecoder("utf-8", { fatal: true }).decode(buffer);
17
+ return false;
18
+ }
19
+ catch {
20
+ return true;
21
+ }
22
+ }
@@ -0,0 +1 @@
1
+ export declare function generateDiff(path: string, oldText: string, newText: string): string;
@@ -0,0 +1,49 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Simplified unified diff for edit results and the transcript.
3
+ //
4
+ // Single hunk with 3 lines of context (trim common prefix/suffix once) — the
5
+ // same shape the myagent-studio SDK produces: enough for a human/model to see
6
+ // what changed without implementing a full Myers diff. Display colorization is
7
+ // applied by the UI, not here.
8
+ // ---------------------------------------------------------------------------
9
+ const CONTEXT = 3;
10
+ export function generateDiff(path, oldText, newText) {
11
+ if (oldText === newText)
12
+ return "";
13
+ const oldLines = oldText.split("\n");
14
+ const newLines = newText.split("\n");
15
+ // A trailing newline yields a final empty element on both sides — drop it
16
+ // only when BOTH sides have it, or the hunk ends with a phantom context
17
+ // line that matches no real file line. When only ONE side ends with a
18
+ // newline the difference is real and survives as an explicit +/- empty
19
+ // line (dropping it on one side alone would make the arrays compare equal
20
+ // and silently hide the change).
21
+ if (oldLines[oldLines.length - 1] === "" && newLines[newLines.length - 1] === "") {
22
+ oldLines.pop();
23
+ newLines.pop();
24
+ }
25
+ let prefix = 0;
26
+ while (prefix < oldLines.length && prefix < newLines.length && oldLines[prefix] === newLines[prefix]) {
27
+ prefix++;
28
+ }
29
+ let suffix = 0;
30
+ while (suffix < oldLines.length - prefix &&
31
+ suffix < newLines.length - prefix &&
32
+ oldLines[oldLines.length - 1 - suffix] === newLines[newLines.length - 1 - suffix]) {
33
+ suffix++;
34
+ }
35
+ const contextStart = Math.max(0, prefix - CONTEXT);
36
+ const contextEndOld = Math.min(oldLines.length, oldLines.length - suffix + CONTEXT);
37
+ const contextEndNew = Math.min(newLines.length, newLines.length - suffix + CONTEXT);
38
+ const removed = oldLines.slice(prefix, oldLines.length - suffix);
39
+ const added = newLines.slice(prefix, newLines.length - suffix);
40
+ const header = `--- a/${path}\n+++ b/${path}\n`;
41
+ const hunk = `@@ -${contextStart + 1},${contextEndOld - contextStart} +${contextStart + 1},${contextEndNew - contextStart} @@\n`;
42
+ const body = [
43
+ ...oldLines.slice(contextStart, prefix).map((l) => ` ${l}`),
44
+ ...removed.map((l) => `-${l}`),
45
+ ...added.map((l) => `+${l}`),
46
+ ...oldLines.slice(oldLines.length - suffix, contextEndOld).map((l) => ` ${l}`),
47
+ ].join("\n");
48
+ return header + hunk + body;
49
+ }
@@ -0,0 +1,2 @@
1
+ import type { ToolHandler } from "./types.js";
2
+ export declare const findToolInstance: ToolHandler;
@@ -0,0 +1,61 @@
1
+ // ---------------------------------------------------------------------------
2
+ // local_find — glob-based file discovery.
3
+ // ---------------------------------------------------------------------------
4
+ import { readdir } from "node:fs/promises";
5
+ import { join, relative } from "node:path";
6
+ import { defineTool, optionalString, requireString } from "./types.js";
7
+ import { resolveToolPath } from "./paths.js";
8
+ import { matchGlob } from "./glob.js";
9
+ import { SKIP_DIRS } from "./grep.js";
10
+ const MAX_RESULTS = 500;
11
+ const MAX_DEPTH = 15;
12
+ const findTool = defineTool({
13
+ name: "local_find",
14
+ description: "Find files by glob pattern (supports *, ?, **, {a,b}). Returns workspace-relative paths, capped at 500. Common build/VCS directories are skipped.",
15
+ parameters: {
16
+ type: "object",
17
+ properties: {
18
+ pattern: { type: "string", description: "Glob pattern, e.g. \"src/**/*.test.ts\"." },
19
+ path: { type: "string", description: "Directory to search from (default: workspace root)." },
20
+ },
21
+ required: ["pattern"],
22
+ },
23
+ summarize: (args) => `find ${String(args.pattern ?? "")}`,
24
+ run: async (args, ctx) => {
25
+ const pattern = requireString(args, "pattern");
26
+ const path = optionalString(args, "path") ?? ".";
27
+ const absolute = await resolveToolPath(path, ctx);
28
+ const results = [];
29
+ async function walk(dir, depth) {
30
+ if (results.length >= MAX_RESULTS || depth > MAX_DEPTH || ctx.signal?.aborted)
31
+ return;
32
+ let entries;
33
+ try {
34
+ entries = await readdir(dir, { withFileTypes: true });
35
+ }
36
+ catch {
37
+ return;
38
+ }
39
+ for (const entry of entries) {
40
+ if (results.length >= MAX_RESULTS)
41
+ return;
42
+ if (entry.isDirectory()) {
43
+ if (SKIP_DIRS.has(entry.name) || entry.name.startsWith("."))
44
+ continue;
45
+ await walk(join(dir, entry.name), depth + 1);
46
+ continue;
47
+ }
48
+ const full = join(dir, entry.name);
49
+ const rel = relative(absolute, full) || entry.name;
50
+ if (matchGlob(pattern, rel) || matchGlob(pattern, entry.name)) {
51
+ results.push(relative(ctx.workspace, full) || full);
52
+ }
53
+ }
54
+ }
55
+ await walk(absolute, 0);
56
+ if (results.length === 0)
57
+ return "No files matched.";
58
+ return results.sort().join("\n") + (results.length >= MAX_RESULTS ? `\n… (capped at ${MAX_RESULTS} results)` : "");
59
+ },
60
+ });
61
+ export const findToolInstance = findTool;
@@ -0,0 +1,2 @@
1
+ import type { ToolHandler } from "./types.js";
2
+ export declare const fsTools: ToolHandler[];