@agi-cli/sdk 0.1.49 → 0.1.51
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 +237 -538
- package/package.json +11 -7
- package/src/agent/types.ts +1 -1
- package/src/index.ts +99 -64
- package/src/errors.ts +0 -102
- package/src/providers/resolver.ts +0 -84
- package/src/streaming/artifacts.ts +0 -41
- package/src/tools/builtin/bash.ts +0 -73
- package/src/tools/builtin/bash.txt +0 -7
- package/src/tools/builtin/edit.ts +0 -145
- package/src/tools/builtin/edit.txt +0 -7
- package/src/tools/builtin/file-cache.ts +0 -39
- package/src/tools/builtin/finish.ts +0 -11
- package/src/tools/builtin/finish.txt +0 -5
- package/src/tools/builtin/fs/cd.ts +0 -19
- package/src/tools/builtin/fs/cd.txt +0 -5
- package/src/tools/builtin/fs/index.ts +0 -20
- package/src/tools/builtin/fs/ls.ts +0 -57
- package/src/tools/builtin/fs/ls.txt +0 -8
- package/src/tools/builtin/fs/pwd.ts +0 -17
- package/src/tools/builtin/fs/pwd.txt +0 -5
- package/src/tools/builtin/fs/read.ts +0 -49
- package/src/tools/builtin/fs/read.txt +0 -8
- package/src/tools/builtin/fs/tree.ts +0 -67
- package/src/tools/builtin/fs/tree.txt +0 -8
- package/src/tools/builtin/fs/util.ts +0 -95
- package/src/tools/builtin/fs/write.ts +0 -61
- package/src/tools/builtin/fs/write.txt +0 -8
- package/src/tools/builtin/git.commit.txt +0 -6
- package/src/tools/builtin/git.diff.txt +0 -5
- package/src/tools/builtin/git.status.txt +0 -5
- package/src/tools/builtin/git.ts +0 -112
- package/src/tools/builtin/glob.ts +0 -82
- package/src/tools/builtin/glob.txt +0 -8
- package/src/tools/builtin/grep.ts +0 -138
- package/src/tools/builtin/grep.txt +0 -9
- package/src/tools/builtin/ignore.ts +0 -45
- package/src/tools/builtin/patch.ts +0 -273
- package/src/tools/builtin/patch.txt +0 -7
- package/src/tools/builtin/plan.ts +0 -58
- package/src/tools/builtin/plan.txt +0 -6
- package/src/tools/builtin/progress.ts +0 -55
- package/src/tools/builtin/progress.txt +0 -7
- package/src/tools/builtin/ripgrep.ts +0 -71
- package/src/tools/builtin/ripgrep.txt +0 -7
- package/src/tools/builtin/websearch.ts +0 -219
- package/src/tools/builtin/websearch.txt +0 -12
- package/src/tools/loader.ts +0 -390
- package/src/types/index.ts +0 -11
- package/src/types/types.ts +0 -4
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { tool, type Tool } from 'ai';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import DESCRIPTION from './edit.txt' with { type: 'text' };
|
|
4
|
-
|
|
5
|
-
const replaceOp = z.object({
|
|
6
|
-
type: z.literal('replace'),
|
|
7
|
-
find: z.string().describe('String or regex (when regex=true)'),
|
|
8
|
-
replace: z.string().default(''),
|
|
9
|
-
regex: z.boolean().optional().default(false),
|
|
10
|
-
flags: z.string().optional().default('g'),
|
|
11
|
-
count: z
|
|
12
|
-
.number()
|
|
13
|
-
.int()
|
|
14
|
-
.min(1)
|
|
15
|
-
.optional()
|
|
16
|
-
.describe('Limit number of replacements'),
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
const insertOp = z.object({
|
|
20
|
-
type: z.literal('insert'),
|
|
21
|
-
position: z.enum(['before', 'after', 'start', 'end']).default('after'),
|
|
22
|
-
pattern: z.string().optional().describe('Anchor pattern for before/after'),
|
|
23
|
-
content: z.string(),
|
|
24
|
-
once: z.boolean().optional().default(true),
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const deleteRangeOp = z.object({
|
|
28
|
-
type: z.literal('delete_range'),
|
|
29
|
-
start: z.string().describe('Start marker (first occurrence)'),
|
|
30
|
-
end: z.string().describe('End marker (first occurrence after start)'),
|
|
31
|
-
includeBoundaries: z.boolean().optional().default(false),
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const opSchema = z.discriminatedUnion('type', [
|
|
35
|
-
replaceOp,
|
|
36
|
-
insertOp,
|
|
37
|
-
deleteRangeOp,
|
|
38
|
-
]);
|
|
39
|
-
|
|
40
|
-
export const editTool: Tool = tool({
|
|
41
|
-
description: DESCRIPTION,
|
|
42
|
-
inputSchema: z.object({
|
|
43
|
-
path: z.string().min(1),
|
|
44
|
-
ops: z.array(opSchema).min(1),
|
|
45
|
-
create: z.boolean().optional().default(false),
|
|
46
|
-
}),
|
|
47
|
-
async execute({
|
|
48
|
-
path,
|
|
49
|
-
ops,
|
|
50
|
-
create,
|
|
51
|
-
}: {
|
|
52
|
-
path: string;
|
|
53
|
-
ops: z.infer<typeof opSchema>[];
|
|
54
|
-
create?: boolean;
|
|
55
|
-
}) {
|
|
56
|
-
const file = Bun.file(path);
|
|
57
|
-
if (!(await file.exists())) {
|
|
58
|
-
if (!create) throw new Error(`File not found: ${path}`);
|
|
59
|
-
await Bun.write(path, '');
|
|
60
|
-
}
|
|
61
|
-
let text = await Bun.file(path).text();
|
|
62
|
-
let applied = 0;
|
|
63
|
-
|
|
64
|
-
for (const op of ops) {
|
|
65
|
-
if (op.type === 'replace') {
|
|
66
|
-
const originalText = text;
|
|
67
|
-
if (op.regex) {
|
|
68
|
-
const re = new RegExp(op.find, op.flags || 'g');
|
|
69
|
-
if (op.count && op.count > 0) {
|
|
70
|
-
let n = 0;
|
|
71
|
-
text = text.replace(re, (m) => {
|
|
72
|
-
if (n < (op.count as number)) {
|
|
73
|
-
n += 1;
|
|
74
|
-
return op.replace;
|
|
75
|
-
}
|
|
76
|
-
return m;
|
|
77
|
-
});
|
|
78
|
-
} else text = text.replace(re, op.replace);
|
|
79
|
-
} else {
|
|
80
|
-
// Check if the text to find exists
|
|
81
|
-
if (!text.includes(op.find)) {
|
|
82
|
-
console.warn(
|
|
83
|
-
`Warning: Text not found for replace operation: "${op.find.substring(0, 50)}${op.find.length > 50 ? '...' : ''}"`,
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
if (op.count && op.count > 0) {
|
|
87
|
-
let remaining = op.count as number;
|
|
88
|
-
let idx = text.indexOf(op.find);
|
|
89
|
-
while (idx !== -1 && remaining > 0) {
|
|
90
|
-
text =
|
|
91
|
-
text.slice(0, idx) +
|
|
92
|
-
op.replace +
|
|
93
|
-
text.slice(idx + op.find.length);
|
|
94
|
-
remaining -= 1;
|
|
95
|
-
idx = text.indexOf(op.find, idx + op.replace.length);
|
|
96
|
-
}
|
|
97
|
-
} else {
|
|
98
|
-
text = text.split(op.find).join(op.replace);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
// Only count as applied if text actually changed
|
|
102
|
-
if (text !== originalText) {
|
|
103
|
-
applied += 1;
|
|
104
|
-
}
|
|
105
|
-
} else if (op.type === 'insert') {
|
|
106
|
-
if (op.position === 'start') {
|
|
107
|
-
text = `${op.content}${text}`;
|
|
108
|
-
applied += 1;
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
if (op.position === 'end') {
|
|
112
|
-
text = `${text}${op.content}`;
|
|
113
|
-
applied += 1;
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
if (!op.pattern)
|
|
117
|
-
throw new Error('insert requires pattern for before/after');
|
|
118
|
-
const idx = text.indexOf(op.pattern);
|
|
119
|
-
if (idx === -1) continue;
|
|
120
|
-
if (op.position === 'before')
|
|
121
|
-
text = text.slice(0, idx) + op.content + text.slice(idx);
|
|
122
|
-
else
|
|
123
|
-
text =
|
|
124
|
-
text.slice(0, idx + op.pattern.length) +
|
|
125
|
-
op.content +
|
|
126
|
-
text.slice(idx + op.pattern.length);
|
|
127
|
-
applied += 1;
|
|
128
|
-
if (op.once) continue;
|
|
129
|
-
} else if (op.type === 'delete_range') {
|
|
130
|
-
const startIdx = text.indexOf(op.start);
|
|
131
|
-
if (startIdx === -1) continue;
|
|
132
|
-
const after = startIdx + op.start.length;
|
|
133
|
-
const endIdx = text.indexOf(op.end, after);
|
|
134
|
-
if (endIdx === -1) continue;
|
|
135
|
-
const from = op.includeBoundaries ? startIdx : after;
|
|
136
|
-
const to = op.includeBoundaries ? endIdx + op.end.length : endIdx;
|
|
137
|
-
text = text.slice(0, from) + text.slice(to);
|
|
138
|
-
applied += 1;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
await Bun.write(path, text);
|
|
143
|
-
return { path, opsApplied: applied, bytes: text.length };
|
|
144
|
-
},
|
|
145
|
-
});
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
- Edit a file using structured operations
|
|
2
|
-
- Supported ops: `replace`, `insert`, `delete_range`
|
|
3
|
-
- Paths are relative to the project root; can create files if flagged
|
|
4
|
-
|
|
5
|
-
Usage tips:
|
|
6
|
-
- Prefer minimal, targeted changes for clarity
|
|
7
|
-
- For sweeping refactors, consider Write or Patch to replace full content
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* File content cache to track modifications during a session/step
|
|
3
|
-
* This helps ensure tools always work with the latest file content
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const fileContentCache = new Map<string, Map<string, string>>();
|
|
7
|
-
|
|
8
|
-
export function getFileCache(sessionId: string): Map<string, string> {
|
|
9
|
-
if (!fileContentCache.has(sessionId)) {
|
|
10
|
-
fileContentCache.set(sessionId, new Map());
|
|
11
|
-
}
|
|
12
|
-
return fileContentCache.get(sessionId) as Map<string, string>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function updateFileCache(
|
|
16
|
-
sessionId: string,
|
|
17
|
-
filePath: string,
|
|
18
|
-
content: string,
|
|
19
|
-
): void {
|
|
20
|
-
const cache = getFileCache(sessionId);
|
|
21
|
-
cache.set(filePath, content);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function getCachedContent(
|
|
25
|
-
sessionId: string,
|
|
26
|
-
filePath: string,
|
|
27
|
-
): string | undefined {
|
|
28
|
-
const cache = getFileCache(sessionId);
|
|
29
|
-
return cache.get(filePath);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function clearFileCache(sessionId: string): void {
|
|
33
|
-
fileContentCache.delete(sessionId);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function clearFileCacheEntry(sessionId: string, filePath: string): void {
|
|
37
|
-
const cache = getFileCache(sessionId);
|
|
38
|
-
cache.delete(filePath);
|
|
39
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { tool } from 'ai';
|
|
3
|
-
import DESCRIPTION from './finish.txt' with { type: 'text' };
|
|
4
|
-
|
|
5
|
-
export const finishTool = tool({
|
|
6
|
-
description: DESCRIPTION,
|
|
7
|
-
inputSchema: z.object({}),
|
|
8
|
-
async execute() {
|
|
9
|
-
return { done: true } as const;
|
|
10
|
-
},
|
|
11
|
-
});
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { tool, type Tool } from 'ai';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import DESCRIPTION from './cd.txt' with { type: 'text' };
|
|
4
|
-
|
|
5
|
-
// description imported above
|
|
6
|
-
|
|
7
|
-
export function buildCdTool(): { name: string; tool: Tool } {
|
|
8
|
-
const cd = tool({
|
|
9
|
-
description: DESCRIPTION,
|
|
10
|
-
inputSchema: z.object({
|
|
11
|
-
path: z.string().describe('Relative directory path'),
|
|
12
|
-
}),
|
|
13
|
-
async execute({ path }: { path: string }) {
|
|
14
|
-
// Actual cwd update is handled in the adapter; this is a placeholder schema
|
|
15
|
-
return { cwd: path };
|
|
16
|
-
},
|
|
17
|
-
});
|
|
18
|
-
return { name: 'cd', tool: cd };
|
|
19
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { Tool } from 'ai';
|
|
2
|
-
import { buildReadTool } from './read.ts';
|
|
3
|
-
import { buildWriteTool } from './write.ts';
|
|
4
|
-
import { buildLsTool } from './ls.ts';
|
|
5
|
-
import { buildTreeTool } from './tree.ts';
|
|
6
|
-
import { buildPwdTool } from './pwd.ts';
|
|
7
|
-
import { buildCdTool } from './cd.ts';
|
|
8
|
-
|
|
9
|
-
export function buildFsTools(
|
|
10
|
-
projectRoot: string,
|
|
11
|
-
): Array<{ name: string; tool: Tool }> {
|
|
12
|
-
const out: Array<{ name: string; tool: Tool }> = [];
|
|
13
|
-
out.push(buildReadTool(projectRoot));
|
|
14
|
-
out.push(buildWriteTool(projectRoot));
|
|
15
|
-
out.push(buildLsTool(projectRoot));
|
|
16
|
-
out.push(buildTreeTool(projectRoot));
|
|
17
|
-
out.push(buildPwdTool());
|
|
18
|
-
out.push(buildCdTool());
|
|
19
|
-
return out;
|
|
20
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { tool, type Tool } from 'ai';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { spawn } from 'bun';
|
|
4
|
-
import { expandTilde, isAbsoluteLike, resolveSafePath } from './util.ts';
|
|
5
|
-
import DESCRIPTION from './ls.txt' with { type: 'text' };
|
|
6
|
-
import { toIgnoredBasenames } from '../ignore.ts';
|
|
7
|
-
|
|
8
|
-
// description imported above
|
|
9
|
-
|
|
10
|
-
export function buildLsTool(projectRoot: string): { name: string; tool: Tool } {
|
|
11
|
-
const ls = tool({
|
|
12
|
-
description: DESCRIPTION,
|
|
13
|
-
inputSchema: z.object({
|
|
14
|
-
path: z
|
|
15
|
-
.string()
|
|
16
|
-
.default('.')
|
|
17
|
-
.describe(
|
|
18
|
-
"Directory path. Relative to project root by default; absolute ('/...') and home ('~/...') paths are allowed.",
|
|
19
|
-
),
|
|
20
|
-
ignore: z
|
|
21
|
-
.array(z.string())
|
|
22
|
-
.optional()
|
|
23
|
-
.describe('List of directory names/globs to ignore'),
|
|
24
|
-
}),
|
|
25
|
-
async execute({ path, ignore }: { path: string; ignore?: string[] }) {
|
|
26
|
-
const req = expandTilde(path || '.');
|
|
27
|
-
const abs = isAbsoluteLike(req)
|
|
28
|
-
? req
|
|
29
|
-
: resolveSafePath(projectRoot, req || '.');
|
|
30
|
-
const ignored = toIgnoredBasenames(ignore);
|
|
31
|
-
const proc = spawn({
|
|
32
|
-
cmd: ['ls', '-1p'],
|
|
33
|
-
cwd: abs,
|
|
34
|
-
stdout: 'pipe',
|
|
35
|
-
stderr: 'pipe',
|
|
36
|
-
});
|
|
37
|
-
const exitCode = await proc.exited;
|
|
38
|
-
const stdout = await new Response(proc.stdout).text();
|
|
39
|
-
const stderr = await new Response(proc.stderr).text();
|
|
40
|
-
if (exitCode !== 0) {
|
|
41
|
-
const message = (stderr || stdout || 'ls failed').trim();
|
|
42
|
-
throw new Error(`ls failed for ${req}: ${message}`);
|
|
43
|
-
}
|
|
44
|
-
const entries = stdout
|
|
45
|
-
.split('\n')
|
|
46
|
-
.map((line) => line.trim())
|
|
47
|
-
.filter((line) => line.length > 0 && !line.startsWith('.'))
|
|
48
|
-
.map((line) => ({
|
|
49
|
-
name: line.replace(/\/$/, ''),
|
|
50
|
-
type: line.endsWith('/') ? 'dir' : 'file',
|
|
51
|
-
}))
|
|
52
|
-
.filter((entry) => !(entry.type === 'dir' && ignored.has(entry.name)));
|
|
53
|
-
return { path: req, entries };
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
return { name: 'ls', tool: ls };
|
|
57
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
- Lists files and directories in a given path (non-recursive)
|
|
2
|
-
- Accepts absolute ('/...'), home ('~/...'), or project-relative paths
|
|
3
|
-
- Hides common build and cache folders by default (node_modules, dist, .git, etc.)
|
|
4
|
-
- Optional ignore patterns allow further filtering of directory names
|
|
5
|
-
|
|
6
|
-
Usage tips:
|
|
7
|
-
- Prefer the Glob tool for pattern-based discovery and Grep for content search
|
|
8
|
-
- Use the Tree tool for a hierarchical view with limited depth
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { tool, type Tool } from 'ai';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import DESCRIPTION from './pwd.txt' with { type: 'text' };
|
|
4
|
-
|
|
5
|
-
// description imported above
|
|
6
|
-
|
|
7
|
-
export function buildPwdTool(): { name: string; tool: Tool } {
|
|
8
|
-
const pwd = tool({
|
|
9
|
-
description: DESCRIPTION,
|
|
10
|
-
inputSchema: z.object({}).optional(),
|
|
11
|
-
async execute() {
|
|
12
|
-
// Actual cwd resolution is handled in the adapter; this is a placeholder schema
|
|
13
|
-
return { cwd: '.' };
|
|
14
|
-
},
|
|
15
|
-
});
|
|
16
|
-
return { name: 'pwd', tool: pwd };
|
|
17
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { tool, type Tool } from 'ai';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { expandTilde, isAbsoluteLike, resolveSafePath } from './util.ts';
|
|
4
|
-
import DESCRIPTION from './read.txt' with { type: 'text' };
|
|
5
|
-
|
|
6
|
-
const embeddedTextAssets: Record<string, string> = {};
|
|
7
|
-
|
|
8
|
-
// description imported above
|
|
9
|
-
|
|
10
|
-
export function buildReadTool(projectRoot: string): {
|
|
11
|
-
name: string;
|
|
12
|
-
tool: Tool;
|
|
13
|
-
} {
|
|
14
|
-
const read = tool({
|
|
15
|
-
description: DESCRIPTION,
|
|
16
|
-
inputSchema: z.object({
|
|
17
|
-
path: z
|
|
18
|
-
.string()
|
|
19
|
-
.describe(
|
|
20
|
-
"File path. Relative to project root by default; absolute ('/...') and home ('~/...') paths are allowed.",
|
|
21
|
-
),
|
|
22
|
-
}),
|
|
23
|
-
async execute({ path }: { path: string }) {
|
|
24
|
-
const req = expandTilde(path);
|
|
25
|
-
if (isAbsoluteLike(req)) {
|
|
26
|
-
const f = Bun.file(req);
|
|
27
|
-
if (await f.exists()) {
|
|
28
|
-
const content = await f.text();
|
|
29
|
-
return { path: req, content, size: content.length };
|
|
30
|
-
}
|
|
31
|
-
throw new Error(`File not found: ${req}`);
|
|
32
|
-
}
|
|
33
|
-
const abs = resolveSafePath(projectRoot, req);
|
|
34
|
-
const f = Bun.file(abs);
|
|
35
|
-
if (await f.exists()) {
|
|
36
|
-
const content = await f.text();
|
|
37
|
-
return { path: req, content, size: content.length };
|
|
38
|
-
}
|
|
39
|
-
const embedded = embeddedTextAssets[req];
|
|
40
|
-
if (embedded) {
|
|
41
|
-
const ef = Bun.file(embedded);
|
|
42
|
-
const content = await ef.text();
|
|
43
|
-
return { path: req, content, size: content.length };
|
|
44
|
-
}
|
|
45
|
-
throw new Error(`File not found: ${req}`);
|
|
46
|
-
},
|
|
47
|
-
});
|
|
48
|
-
return { name: 'read', tool: read };
|
|
49
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
- Read a text file from the workspace
|
|
2
|
-
- Accepts absolute ('/...'), home ('~/...'), or project-relative paths
|
|
3
|
-
- Returns file text and size in bytes
|
|
4
|
-
- May serve embedded text assets for some paths (internal optimization)
|
|
5
|
-
|
|
6
|
-
Usage tips:
|
|
7
|
-
- Prefer relative project paths when possible (more portable)
|
|
8
|
-
- For large files or searches, use the Grep or Ripgrep tool
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { tool, type Tool } from 'ai';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { spawn } from 'bun';
|
|
4
|
-
import { expandTilde, isAbsoluteLike, resolveSafePath } from './util.ts';
|
|
5
|
-
import DESCRIPTION from './tree.txt' with { type: 'text' };
|
|
6
|
-
import { toIgnoredBasenames } from '../ignore.ts';
|
|
7
|
-
|
|
8
|
-
// description imported above
|
|
9
|
-
|
|
10
|
-
export function buildTreeTool(projectRoot: string): {
|
|
11
|
-
name: string;
|
|
12
|
-
tool: Tool;
|
|
13
|
-
} {
|
|
14
|
-
const tree = tool({
|
|
15
|
-
description: DESCRIPTION,
|
|
16
|
-
inputSchema: z.object({
|
|
17
|
-
path: z.string().default('.'),
|
|
18
|
-
depth: z
|
|
19
|
-
.number()
|
|
20
|
-
.int()
|
|
21
|
-
.min(1)
|
|
22
|
-
.max(20)
|
|
23
|
-
.optional()
|
|
24
|
-
.describe('Optional depth limit (defaults to full depth).'),
|
|
25
|
-
ignore: z
|
|
26
|
-
.array(z.string())
|
|
27
|
-
.optional()
|
|
28
|
-
.describe('List of directory names/globs to ignore'),
|
|
29
|
-
}),
|
|
30
|
-
async execute({
|
|
31
|
-
path,
|
|
32
|
-
depth,
|
|
33
|
-
ignore,
|
|
34
|
-
}: {
|
|
35
|
-
path: string;
|
|
36
|
-
depth?: number;
|
|
37
|
-
ignore?: string[];
|
|
38
|
-
}) {
|
|
39
|
-
const req = expandTilde(path || '.');
|
|
40
|
-
const start = isAbsoluteLike(req)
|
|
41
|
-
? req
|
|
42
|
-
: resolveSafePath(projectRoot, req || '.');
|
|
43
|
-
const ignored = toIgnoredBasenames(ignore);
|
|
44
|
-
const args = ['tree'];
|
|
45
|
-
if (typeof depth === 'number') args.push('-L', String(depth));
|
|
46
|
-
if (ignored.size) args.push('-I', Array.from(ignored).join('|'));
|
|
47
|
-
args.push('.');
|
|
48
|
-
|
|
49
|
-
const proc = spawn({
|
|
50
|
-
cmd: args,
|
|
51
|
-
cwd: start,
|
|
52
|
-
stdout: 'pipe',
|
|
53
|
-
stderr: 'pipe',
|
|
54
|
-
});
|
|
55
|
-
const exitCode = await proc.exited;
|
|
56
|
-
const stdout = await new Response(proc.stdout).text();
|
|
57
|
-
const stderr = await new Response(proc.stderr).text();
|
|
58
|
-
if (exitCode !== 0) {
|
|
59
|
-
const message = (stderr || stdout || 'tree failed').trim();
|
|
60
|
-
throw new Error(`tree failed for ${req}: ${message}`);
|
|
61
|
-
}
|
|
62
|
-
const output = stdout.trimEnd();
|
|
63
|
-
return { path: req, depth: depth ?? null, tree: output };
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
return { name: 'tree', tool: tree };
|
|
67
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
- Render a shallow directory tree from a starting path
|
|
2
|
-
- Accepts absolute, home, or project-relative paths
|
|
3
|
-
- Skips common build/cache folders (node_modules, dist, .git, etc.) by default
|
|
4
|
-
- Depth is capped to avoid excessive output (1–5)
|
|
5
|
-
|
|
6
|
-
Usage tips:
|
|
7
|
-
- Use the LS tool for a flat listing of one directory
|
|
8
|
-
- Use the Glob and Grep tools for file pattern and content searches respectively
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { createTwoFilesPatch } from 'diff';
|
|
2
|
-
import { resolve as resolvePath } from 'node:path';
|
|
3
|
-
|
|
4
|
-
function normalizeForComparison(value: string) {
|
|
5
|
-
const withForwardSlashes = value.replace(/\\/g, '/');
|
|
6
|
-
return process.platform === 'win32'
|
|
7
|
-
? withForwardSlashes.toLowerCase()
|
|
8
|
-
: withForwardSlashes;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function resolveSafePath(projectRoot: string, p: string) {
|
|
12
|
-
const root = resolvePath(projectRoot);
|
|
13
|
-
const target = resolvePath(root, p || '.');
|
|
14
|
-
const rootNorm = (() => {
|
|
15
|
-
const normalized = normalizeForComparison(root);
|
|
16
|
-
if (normalized === '/') return '/';
|
|
17
|
-
return normalized.replace(/[\\/]+$/, '');
|
|
18
|
-
})();
|
|
19
|
-
const targetNorm = normalizeForComparison(target);
|
|
20
|
-
const rootWithSlash = rootNorm === '/' ? '/' : `${rootNorm}/`;
|
|
21
|
-
const inProject =
|
|
22
|
-
targetNorm === rootNorm || targetNorm.startsWith(rootWithSlash);
|
|
23
|
-
if (!inProject) throw new Error(`Path escapes project root: ${p}`);
|
|
24
|
-
return target;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function expandTilde(p: string): string {
|
|
28
|
-
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
29
|
-
if (!home) return p;
|
|
30
|
-
if (p === '~') return home;
|
|
31
|
-
if (p.startsWith('~/')) return `${home}/${p.slice(2)}`;
|
|
32
|
-
return p;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function isAbsoluteLike(p: string): boolean {
|
|
36
|
-
return p.startsWith('/') || /^[A-Za-z]:[\\/]/.test(p);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function buildWriteArtifact(
|
|
40
|
-
relPath: string,
|
|
41
|
-
existed: boolean,
|
|
42
|
-
oldText: string,
|
|
43
|
-
newText: string,
|
|
44
|
-
) {
|
|
45
|
-
let patch = '';
|
|
46
|
-
try {
|
|
47
|
-
patch = createTwoFilesPatch(
|
|
48
|
-
`a/${relPath}`,
|
|
49
|
-
`b/${relPath}`,
|
|
50
|
-
String(oldText ?? ''),
|
|
51
|
-
String(newText ?? ''),
|
|
52
|
-
'',
|
|
53
|
-
'',
|
|
54
|
-
{ context: 3 },
|
|
55
|
-
);
|
|
56
|
-
} catch {}
|
|
57
|
-
if (!patch || !patch.trim().length) {
|
|
58
|
-
const header = existed ? 'Update File' : 'Add File';
|
|
59
|
-
const oldLines = String(oldText ?? '').split('\n');
|
|
60
|
-
const newLines = String(newText ?? '').split('\n');
|
|
61
|
-
const lines: string[] = [];
|
|
62
|
-
lines.push('*** Begin Patch');
|
|
63
|
-
lines.push(`*** ${header}: ${relPath}`);
|
|
64
|
-
lines.push('@@');
|
|
65
|
-
if (existed) for (const l of oldLines) lines.push(`-${l}`);
|
|
66
|
-
for (const l of newLines) lines.push(`+${l}`);
|
|
67
|
-
lines.push('*** End Patch');
|
|
68
|
-
patch = lines.join('\n');
|
|
69
|
-
}
|
|
70
|
-
const { additions, deletions } = summarizePatchCounts(patch);
|
|
71
|
-
return {
|
|
72
|
-
kind: 'file_diff',
|
|
73
|
-
patch,
|
|
74
|
-
summary: { files: 1, additions, deletions },
|
|
75
|
-
} as const;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function summarizePatchCounts(patch: string): {
|
|
79
|
-
additions: number;
|
|
80
|
-
deletions: number;
|
|
81
|
-
} {
|
|
82
|
-
let adds = 0;
|
|
83
|
-
let dels = 0;
|
|
84
|
-
for (const line of String(patch || '').split('\n')) {
|
|
85
|
-
if (
|
|
86
|
-
line.startsWith('+++') ||
|
|
87
|
-
line.startsWith('---') ||
|
|
88
|
-
line.startsWith('diff ')
|
|
89
|
-
)
|
|
90
|
-
continue;
|
|
91
|
-
if (line.startsWith('+')) adds += 1;
|
|
92
|
-
else if (line.startsWith('-')) dels += 1;
|
|
93
|
-
}
|
|
94
|
-
return { additions: adds, deletions: dels };
|
|
95
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { tool, type Tool } from 'ai';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { $ } from 'bun';
|
|
4
|
-
import {
|
|
5
|
-
buildWriteArtifact,
|
|
6
|
-
resolveSafePath,
|
|
7
|
-
expandTilde,
|
|
8
|
-
isAbsoluteLike,
|
|
9
|
-
} from './util.ts';
|
|
10
|
-
import DESCRIPTION from './write.txt' with { type: 'text' };
|
|
11
|
-
|
|
12
|
-
// description imported above
|
|
13
|
-
|
|
14
|
-
export function buildWriteTool(projectRoot: string): {
|
|
15
|
-
name: string;
|
|
16
|
-
tool: Tool;
|
|
17
|
-
} {
|
|
18
|
-
const write = tool({
|
|
19
|
-
description: DESCRIPTION,
|
|
20
|
-
inputSchema: z.object({
|
|
21
|
-
path: z
|
|
22
|
-
.string()
|
|
23
|
-
.describe(
|
|
24
|
-
'Relative file path within the project. Writes outside the project are not allowed.',
|
|
25
|
-
),
|
|
26
|
-
content: z.string().describe('Text content to write'),
|
|
27
|
-
createDirs: z.boolean().optional().default(true),
|
|
28
|
-
}),
|
|
29
|
-
async execute({
|
|
30
|
-
path,
|
|
31
|
-
content,
|
|
32
|
-
createDirs,
|
|
33
|
-
}: {
|
|
34
|
-
path: string;
|
|
35
|
-
content: string;
|
|
36
|
-
createDirs?: boolean;
|
|
37
|
-
}) {
|
|
38
|
-
const req = expandTilde(path);
|
|
39
|
-
if (isAbsoluteLike(req)) {
|
|
40
|
-
throw new Error(
|
|
41
|
-
`Refusing to write outside project root: ${req}. Use a relative path within the project.`,
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
const abs = resolveSafePath(projectRoot, req);
|
|
45
|
-
if (createDirs) {
|
|
46
|
-
await $`mkdir -p ${abs.slice(0, abs.lastIndexOf('/'))}`;
|
|
47
|
-
}
|
|
48
|
-
let existed = false;
|
|
49
|
-
let oldText = '';
|
|
50
|
-
try {
|
|
51
|
-
const f = Bun.file(abs);
|
|
52
|
-
existed = await f.exists();
|
|
53
|
-
if (existed) oldText = await f.text();
|
|
54
|
-
} catch {}
|
|
55
|
-
await Bun.write(abs, content);
|
|
56
|
-
const artifact = await buildWriteArtifact(req, existed, oldText, content);
|
|
57
|
-
return { path: req, bytes: content.length, artifact } as const;
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
return { name: 'write', tool: write };
|
|
61
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
- Write text to a file in the workspace
|
|
2
|
-
- Creates the file if it does not exist (when allowed)
|
|
3
|
-
- Only writes within the project root; absolute paths are rejected
|
|
4
|
-
- Returns a compact patch artifact summarizing the change
|
|
5
|
-
|
|
6
|
-
Usage tips:
|
|
7
|
-
- Prefer idempotent writes by providing the full intended content
|
|
8
|
-
- For localized edits, consider the Edit tool with structured operations
|