@agi-cli/sdk 0.1.49 → 0.1.50
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/package.json +6 -5
- package/src/agent/types.ts +1 -1
- package/src/index.ts +10 -77
- 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,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
|
package/src/tools/builtin/git.ts
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { tool, type Tool } from 'ai';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { $ } from 'bun';
|
|
4
|
-
import GIT_STATUS_DESCRIPTION from './git.status.txt' with { type: 'text' };
|
|
5
|
-
import GIT_DIFF_DESCRIPTION from './git.diff.txt' with { type: 'text' };
|
|
6
|
-
import GIT_COMMIT_DESCRIPTION from './git.commit.txt' with { type: 'text' };
|
|
7
|
-
|
|
8
|
-
export function buildGitTools(
|
|
9
|
-
projectRoot: string,
|
|
10
|
-
): Array<{ name: string; tool: Tool }> {
|
|
11
|
-
// Helper to find git root directory
|
|
12
|
-
async function findGitRoot(): Promise<string> {
|
|
13
|
-
try {
|
|
14
|
-
const res = await $`git -C ${projectRoot} rev-parse --show-toplevel`
|
|
15
|
-
.quiet()
|
|
16
|
-
.text()
|
|
17
|
-
.catch(() => '');
|
|
18
|
-
return res.trim() || projectRoot;
|
|
19
|
-
} catch {
|
|
20
|
-
return projectRoot;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function inRepo(): Promise<boolean> {
|
|
25
|
-
const res = await $`git -C ${projectRoot} rev-parse --is-inside-work-tree`
|
|
26
|
-
.quiet()
|
|
27
|
-
.text()
|
|
28
|
-
.catch(() => '');
|
|
29
|
-
return res.trim() === 'true';
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const git_status = tool({
|
|
33
|
-
description: GIT_STATUS_DESCRIPTION,
|
|
34
|
-
inputSchema: z.object({}).optional(),
|
|
35
|
-
async execute() {
|
|
36
|
-
if (!(await inRepo())) throw new Error('Not a git repository');
|
|
37
|
-
const gitRoot = await findGitRoot();
|
|
38
|
-
const out = await $`git -C ${gitRoot} status --porcelain=v1`.text();
|
|
39
|
-
const lines = out.split('\n').filter(Boolean);
|
|
40
|
-
let staged = 0;
|
|
41
|
-
let unstaged = 0;
|
|
42
|
-
for (const line of lines) {
|
|
43
|
-
const x = line[0];
|
|
44
|
-
const y = line[1];
|
|
45
|
-
if (!x || !y) continue;
|
|
46
|
-
if (x === '!' && y === '!') continue; // ignored files
|
|
47
|
-
const isUntracked = x === '?' && y === '?';
|
|
48
|
-
if (x !== ' ' && !isUntracked) staged += 1;
|
|
49
|
-
if (isUntracked || y !== ' ') unstaged += 1;
|
|
50
|
-
}
|
|
51
|
-
return {
|
|
52
|
-
staged,
|
|
53
|
-
unstaged,
|
|
54
|
-
raw: lines.slice(0, 200),
|
|
55
|
-
};
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const git_diff = tool({
|
|
60
|
-
description: GIT_DIFF_DESCRIPTION,
|
|
61
|
-
inputSchema: z.object({ all: z.boolean().optional().default(false) }),
|
|
62
|
-
async execute({ all }: { all?: boolean }) {
|
|
63
|
-
if (!(await inRepo())) throw new Error('Not a git repository');
|
|
64
|
-
const gitRoot = await findGitRoot();
|
|
65
|
-
// When all=true, show full working tree diff relative to HEAD
|
|
66
|
-
// so both staged and unstaged changes are included. Otherwise,
|
|
67
|
-
// show only the staged diff (index vs HEAD).
|
|
68
|
-
const args = all ? ['diff', 'HEAD'] : ['diff', '--staged'];
|
|
69
|
-
const out = await $`git -C ${gitRoot} ${args}`.text();
|
|
70
|
-
const limited = out.split('\n').slice(0, 5000).join('\n');
|
|
71
|
-
return { all: !!all, patch: limited };
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
const git_commit = tool({
|
|
76
|
-
description: GIT_COMMIT_DESCRIPTION,
|
|
77
|
-
inputSchema: z.object({
|
|
78
|
-
message: z.string().min(5),
|
|
79
|
-
amend: z.boolean().optional().default(false),
|
|
80
|
-
signoff: z.boolean().optional().default(false),
|
|
81
|
-
}),
|
|
82
|
-
async execute({
|
|
83
|
-
message,
|
|
84
|
-
amend,
|
|
85
|
-
signoff,
|
|
86
|
-
}: {
|
|
87
|
-
message: string;
|
|
88
|
-
amend?: boolean;
|
|
89
|
-
signoff?: boolean;
|
|
90
|
-
}) {
|
|
91
|
-
if (!(await inRepo())) throw new Error('Not a git repository');
|
|
92
|
-
const gitRoot = await findGitRoot();
|
|
93
|
-
const args = ['commit', '-m', message];
|
|
94
|
-
if (amend) args.push('--amend');
|
|
95
|
-
if (signoff) args.push('--signoff');
|
|
96
|
-
const res = await $`git -C ${gitRoot} ${args}`
|
|
97
|
-
.quiet()
|
|
98
|
-
.text()
|
|
99
|
-
.catch(async (e) => {
|
|
100
|
-
const txt = typeof e?.stderr === 'string' ? e.stderr : String(e);
|
|
101
|
-
throw new Error(txt || 'git commit failed');
|
|
102
|
-
});
|
|
103
|
-
return { result: res.trim() };
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
return [
|
|
108
|
-
{ name: 'git_status', tool: git_status },
|
|
109
|
-
{ name: 'git_diff', tool: git_diff },
|
|
110
|
-
{ name: 'git_commit', tool: git_commit },
|
|
111
|
-
];
|
|
112
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { tool, type Tool } from 'ai';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { $ } from 'bun';
|
|
4
|
-
import { join, isAbsolute, resolve } from 'node:path';
|
|
5
|
-
import DESCRIPTION from './glob.txt' with { type: 'text' };
|
|
6
|
-
import { defaultIgnoreGlobs } from './ignore.ts';
|
|
7
|
-
|
|
8
|
-
// description imported above
|
|
9
|
-
|
|
10
|
-
export function buildGlobTool(projectRoot: string): {
|
|
11
|
-
name: string;
|
|
12
|
-
tool: Tool;
|
|
13
|
-
} {
|
|
14
|
-
const glob = tool({
|
|
15
|
-
description: DESCRIPTION,
|
|
16
|
-
inputSchema: z.object({
|
|
17
|
-
pattern: z
|
|
18
|
-
.string()
|
|
19
|
-
.describe('Glob pattern to match files (e.g., "**/*.ts")'),
|
|
20
|
-
path: z
|
|
21
|
-
.string()
|
|
22
|
-
.optional()
|
|
23
|
-
.describe('Directory to search in. Defaults to the project root.'),
|
|
24
|
-
ignore: z
|
|
25
|
-
.array(z.string())
|
|
26
|
-
.optional()
|
|
27
|
-
.describe('Glob patterns to exclude from results'),
|
|
28
|
-
}),
|
|
29
|
-
async execute(params) {
|
|
30
|
-
const limit = 100;
|
|
31
|
-
const search = params.path
|
|
32
|
-
? isAbsolute(params.path)
|
|
33
|
-
? params.path
|
|
34
|
-
: join(projectRoot, params.path)
|
|
35
|
-
: projectRoot;
|
|
36
|
-
const args = ['--files', '--color', 'never', '-g', params.pattern];
|
|
37
|
-
for (const g of defaultIgnoreGlobs(params.ignore)) {
|
|
38
|
-
args.push('-g', g);
|
|
39
|
-
}
|
|
40
|
-
const { exitCode, stdout, stderr } = await $`rg ${args}`
|
|
41
|
-
.cwd(search)
|
|
42
|
-
.nothrow();
|
|
43
|
-
if (exitCode !== 0) {
|
|
44
|
-
const msg = (stderr || stdout || 'rg failed').toString().trim();
|
|
45
|
-
throw new Error(`glob failed: ${msg}`);
|
|
46
|
-
}
|
|
47
|
-
const lines = stdout.split('\n').filter(Boolean);
|
|
48
|
-
const items: Array<{ path: string; mtime: number }> = [];
|
|
49
|
-
let truncated = false;
|
|
50
|
-
for (const rel of lines) {
|
|
51
|
-
if (items.length >= limit) {
|
|
52
|
-
truncated = true;
|
|
53
|
-
break;
|
|
54
|
-
}
|
|
55
|
-
const full = resolve(search, rel);
|
|
56
|
-
const mtime = await Bun.file(full)
|
|
57
|
-
.stat()
|
|
58
|
-
.then((s) => s.mtime.getTime())
|
|
59
|
-
.catch(() => 0);
|
|
60
|
-
items.push({ path: full, mtime });
|
|
61
|
-
}
|
|
62
|
-
items.sort((a, b) => b.mtime - a.mtime);
|
|
63
|
-
const output: string[] = [];
|
|
64
|
-
if (items.length === 0) output.push('No files found');
|
|
65
|
-
else {
|
|
66
|
-
output.push(...items.map((i) => i.path));
|
|
67
|
-
if (truncated) {
|
|
68
|
-
output.push('');
|
|
69
|
-
output.push(
|
|
70
|
-
'(Results are truncated. Consider using a more specific path or pattern.)',
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return {
|
|
75
|
-
title: isAbsolute(search) ? search : join(projectRoot, search),
|
|
76
|
-
metadata: { count: items.length, truncated },
|
|
77
|
-
output: output.join('\n'),
|
|
78
|
-
};
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
return { name: 'glob', tool: glob };
|
|
82
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
- Fast file pattern matching powered by ripgrep (rg)
|
|
2
|
-
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
|
|
3
|
-
- Returns absolute file paths sorted by modification time
|
|
4
|
-
- Skips common build and cache folders by default; add 'ignore' patterns to refine
|
|
5
|
-
|
|
6
|
-
Usage tips:
|
|
7
|
-
- Use the Grep tool for content searches
|
|
8
|
-
- Omit 'path' to search from the project root; set a subdirectory to narrow results
|