@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.
- package/README.md +357 -0
- package/dist/agent/context.d.ts +33 -0
- package/dist/agent/context.js +169 -0
- package/dist/agent/modes.d.ts +21 -0
- package/dist/agent/modes.js +84 -0
- package/dist/agent/prompt-builder.d.ts +9 -0
- package/dist/agent/prompt-builder.js +31 -0
- package/dist/agent/sessions.d.ts +38 -0
- package/dist/agent/sessions.js +130 -0
- package/dist/agent/todo.d.ts +18 -0
- package/dist/agent/todo.js +61 -0
- package/dist/agent/turn.d.ts +309 -0
- package/dist/agent/turn.js +1253 -0
- package/dist/approval/policy.d.ts +81 -0
- package/dist/approval/policy.js +157 -0
- package/dist/config.d.ts +49 -0
- package/dist/config.js +156 -0
- package/dist/git/status.d.ts +89 -0
- package/dist/git/status.js +226 -0
- package/dist/headless.d.ts +72 -0
- package/dist/headless.js +330 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +511 -0
- package/dist/protocol/client.d.ts +123 -0
- package/dist/protocol/client.js +250 -0
- package/dist/protocol/sse-frames.d.ts +6 -0
- package/dist/protocol/sse-frames.js +75 -0
- package/dist/protocol/types.d.ts +200 -0
- package/dist/protocol/types.js +8 -0
- package/dist/runtime.d.ts +38 -0
- package/dist/runtime.js +166 -0
- package/dist/sanitize.d.ts +1 -0
- package/dist/sanitize.js +21 -0
- package/dist/skills/discovery.d.ts +24 -0
- package/dist/skills/discovery.js +109 -0
- package/dist/tools/binary.d.ts +2 -0
- package/dist/tools/binary.js +22 -0
- package/dist/tools/diff.d.ts +1 -0
- package/dist/tools/diff.js +49 -0
- package/dist/tools/find.d.ts +2 -0
- package/dist/tools/find.js +61 -0
- package/dist/tools/fs.d.ts +2 -0
- package/dist/tools/fs.js +276 -0
- package/dist/tools/glob.d.ts +6 -0
- package/dist/tools/glob.js +131 -0
- package/dist/tools/grep.d.ts +3 -0
- package/dist/tools/grep.js +228 -0
- package/dist/tools/paths.d.ts +27 -0
- package/dist/tools/paths.js +124 -0
- package/dist/tools/registry.d.ts +13 -0
- package/dist/tools/registry.js +38 -0
- package/dist/tools/shell.d.ts +2 -0
- package/dist/tools/shell.js +136 -0
- package/dist/tools/skills.d.ts +2 -0
- package/dist/tools/skills.js +36 -0
- package/dist/tools/todo.d.ts +2 -0
- package/dist/tools/todo.js +43 -0
- package/dist/tools/transfer.d.ts +2 -0
- package/dist/tools/transfer.js +145 -0
- package/dist/tools/truncate.d.ts +12 -0
- package/dist/tools/truncate.js +46 -0
- package/dist/tools/types.d.ts +85 -0
- package/dist/tools/types.js +63 -0
- package/dist/ui/app.d.ts +39 -0
- package/dist/ui/app.js +1061 -0
- package/dist/ui/colors.d.ts +100 -0
- package/dist/ui/colors.js +169 -0
- package/dist/ui/components.d.ts +267 -0
- package/dist/ui/components.js +811 -0
- package/dist/ui/diff.d.ts +37 -0
- package/dist/ui/diff.js +143 -0
- package/dist/ui/format.d.ts +28 -0
- package/dist/ui/format.js +76 -0
- package/dist/ui/help.d.ts +6 -0
- package/dist/ui/help.js +45 -0
- package/dist/ui/highlight.d.ts +20 -0
- package/dist/ui/highlight.js +210 -0
- package/dist/ui/logo.d.ts +24 -0
- package/dist/ui/logo.js +106 -0
- package/dist/ui/model-list.d.ts +10 -0
- package/dist/ui/model-list.js +33 -0
- package/dist/ui/quit-confirm.d.ts +8 -0
- package/dist/ui/quit-confirm.js +40 -0
- package/dist/ui/select-popup.d.ts +30 -0
- package/dist/ui/select-popup.js +54 -0
- package/dist/ui/theme.d.ts +4 -0
- package/dist/ui/theme.js +41 -0
- package/dist/ui/tool-view.d.ts +20 -0
- package/dist/ui/tool-view.js +326 -0
- package/package.json +44 -0
- package/skills/git-commit/SKILL.md +27 -0
package/dist/runtime.js
ADDED
|
@@ -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;
|
package/dist/sanitize.js
ADDED
|
@@ -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,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,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;
|