@agi-cli/sdk 0.1.53 → 0.1.55
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 +52 -27
- package/src/agent/types.ts +1 -1
- package/src/auth/src/index.ts +70 -0
- package/src/auth/src/oauth.ts +172 -0
- package/src/config/src/index.ts +120 -0
- package/src/config/src/manager.ts +102 -0
- package/src/config/src/paths.ts +98 -0
- package/src/core/src/errors.ts +102 -0
- package/src/core/src/index.ts +75 -0
- package/src/core/src/providers/resolver.ts +84 -0
- package/src/core/src/streaming/artifacts.ts +41 -0
- package/src/core/src/tools/builtin/bash.ts +90 -0
- package/src/core/src/tools/builtin/bash.txt +7 -0
- package/src/core/src/tools/builtin/edit.ts +152 -0
- package/src/core/src/tools/builtin/edit.txt +7 -0
- package/src/core/src/tools/builtin/file-cache.ts +39 -0
- package/src/core/src/tools/builtin/finish.ts +11 -0
- package/src/core/src/tools/builtin/finish.txt +5 -0
- package/src/core/src/tools/builtin/fs/cd.ts +19 -0
- package/src/core/src/tools/builtin/fs/cd.txt +5 -0
- package/src/core/src/tools/builtin/fs/index.ts +20 -0
- package/src/core/src/tools/builtin/fs/ls.ts +60 -0
- package/src/core/src/tools/builtin/fs/ls.txt +8 -0
- package/src/core/src/tools/builtin/fs/pwd.ts +17 -0
- package/src/core/src/tools/builtin/fs/pwd.txt +5 -0
- package/src/core/src/tools/builtin/fs/read.ts +80 -0
- package/src/core/src/tools/builtin/fs/read.txt +8 -0
- package/src/core/src/tools/builtin/fs/tree.ts +71 -0
- package/src/core/src/tools/builtin/fs/tree.txt +8 -0
- package/src/core/src/tools/builtin/fs/util.ts +95 -0
- package/src/core/src/tools/builtin/fs/write.ts +61 -0
- package/src/core/src/tools/builtin/fs/write.txt +8 -0
- package/src/core/src/tools/builtin/git.commit.txt +6 -0
- package/src/core/src/tools/builtin/git.diff.txt +5 -0
- package/src/core/src/tools/builtin/git.status.txt +5 -0
- package/src/core/src/tools/builtin/git.ts +128 -0
- package/src/core/src/tools/builtin/grep.ts +140 -0
- package/src/core/src/tools/builtin/grep.txt +9 -0
- package/src/core/src/tools/builtin/ignore.ts +45 -0
- package/src/core/src/tools/builtin/patch.ts +269 -0
- package/src/core/src/tools/builtin/patch.txt +7 -0
- package/src/core/src/tools/builtin/plan.ts +58 -0
- package/src/core/src/tools/builtin/plan.txt +6 -0
- package/src/core/src/tools/builtin/progress.ts +55 -0
- package/src/core/src/tools/builtin/progress.txt +7 -0
- package/src/core/src/tools/builtin/ripgrep.ts +102 -0
- package/src/core/src/tools/builtin/ripgrep.txt +7 -0
- package/src/core/src/tools/builtin/websearch.ts +219 -0
- package/src/core/src/tools/builtin/websearch.txt +12 -0
- package/src/core/src/tools/loader.ts +398 -0
- package/src/core/src/types/index.ts +11 -0
- package/src/core/src/types/types.ts +4 -0
- package/src/index.ts +57 -58
- package/src/prompts/src/agents/build.txt +5 -0
- package/src/prompts/src/agents/general.txt +6 -0
- package/src/prompts/src/agents/plan.txt +13 -0
- package/src/prompts/src/base.txt +14 -0
- package/src/prompts/src/debug.ts +104 -0
- package/src/prompts/src/index.ts +1 -0
- package/src/prompts/src/modes/oneshot.txt +9 -0
- package/src/prompts/src/providers/anthropic.txt +151 -0
- package/src/prompts/src/providers/anthropicSpoof.txt +1 -0
- package/src/prompts/src/providers/default.txt +310 -0
- package/src/prompts/src/providers/google.txt +155 -0
- package/src/prompts/src/providers/openai.txt +310 -0
- package/src/prompts/src/providers.ts +116 -0
- package/src/providers/src/authorization.ts +17 -0
- package/src/providers/src/catalog.ts +4201 -0
- package/src/providers/src/env.ts +26 -0
- package/src/providers/src/index.ts +12 -0
- package/src/providers/src/pricing.ts +135 -0
- package/src/providers/src/utils.ts +24 -0
- package/src/providers/src/validate.ts +39 -0
- package/src/types/src/auth.ts +26 -0
- package/src/types/src/config.ts +40 -0
- package/src/types/src/index.ts +14 -0
- package/src/types/src/provider.ts +28 -0
- package/src/global.d.ts +0 -4
- package/src/tools/builtin/fs.ts +0 -1
- package/src/tools/builtin/git.ts +0 -1
- package/src/web-ui.ts +0 -8
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { tool, type Tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { exec } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { expandTilde, isAbsoluteLike, resolveSafePath } from './util.ts';
|
|
6
|
+
import DESCRIPTION from './ls.txt' with { type: 'text' };
|
|
7
|
+
import { toIgnoredBasenames } from '../ignore.ts';
|
|
8
|
+
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
|
|
11
|
+
// description imported above
|
|
12
|
+
|
|
13
|
+
export function buildLsTool(projectRoot: string): { name: string; tool: Tool } {
|
|
14
|
+
const ls = tool({
|
|
15
|
+
description: DESCRIPTION,
|
|
16
|
+
inputSchema: z.object({
|
|
17
|
+
path: z
|
|
18
|
+
.string()
|
|
19
|
+
.default('.')
|
|
20
|
+
.describe(
|
|
21
|
+
"Directory path. Relative to project root by default; absolute ('/...') and home ('~/...') paths are allowed.",
|
|
22
|
+
),
|
|
23
|
+
ignore: z
|
|
24
|
+
.array(z.string())
|
|
25
|
+
.optional()
|
|
26
|
+
.describe('List of directory names/globs to ignore'),
|
|
27
|
+
}),
|
|
28
|
+
async execute({ path, ignore }: { path: string; ignore?: string[] }) {
|
|
29
|
+
const req = expandTilde(path || '.');
|
|
30
|
+
const abs = isAbsoluteLike(req)
|
|
31
|
+
? req
|
|
32
|
+
: resolveSafePath(projectRoot, req || '.');
|
|
33
|
+
const ignored = toIgnoredBasenames(ignore);
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const { stdout } = await execAsync('ls -1p', {
|
|
37
|
+
cwd: abs,
|
|
38
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
39
|
+
});
|
|
40
|
+
const entries = stdout
|
|
41
|
+
.split('\n')
|
|
42
|
+
.map((line) => line.trim())
|
|
43
|
+
.filter((line) => line.length > 0 && !line.startsWith('.'))
|
|
44
|
+
.map((line) => ({
|
|
45
|
+
name: line.replace(/\/$/, ''),
|
|
46
|
+
type: line.endsWith('/') ? 'dir' : 'file',
|
|
47
|
+
}))
|
|
48
|
+
.filter(
|
|
49
|
+
(entry) => !(entry.type === 'dir' && ignored.has(entry.name)),
|
|
50
|
+
);
|
|
51
|
+
return { path: req, entries };
|
|
52
|
+
} catch (error: unknown) {
|
|
53
|
+
const err = error as { stderr?: string; stdout?: string };
|
|
54
|
+
const message = (err.stderr || err.stdout || 'ls failed').trim();
|
|
55
|
+
throw new Error(`ls failed for ${req}: ${message}`);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
return { name: 'ls', tool: ls };
|
|
60
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { tool, type Tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { expandTilde, isAbsoluteLike, resolveSafePath } from './util.ts';
|
|
5
|
+
import DESCRIPTION from './read.txt' with { type: 'text' };
|
|
6
|
+
|
|
7
|
+
const embeddedTextAssets: Record<string, string> = {};
|
|
8
|
+
|
|
9
|
+
export function buildReadTool(projectRoot: string): {
|
|
10
|
+
name: string;
|
|
11
|
+
tool: Tool;
|
|
12
|
+
} {
|
|
13
|
+
const read = tool({
|
|
14
|
+
description: DESCRIPTION,
|
|
15
|
+
inputSchema: z.object({
|
|
16
|
+
path: z
|
|
17
|
+
.string()
|
|
18
|
+
.describe(
|
|
19
|
+
"File path. Relative to project root by default; absolute ('/...') and home ('~/...') paths are allowed.",
|
|
20
|
+
),
|
|
21
|
+
startLine: z
|
|
22
|
+
.number()
|
|
23
|
+
.int()
|
|
24
|
+
.min(1)
|
|
25
|
+
.optional()
|
|
26
|
+
.describe(
|
|
27
|
+
'Starting line number (1-indexed). If provided, only reads lines from startLine to endLine.',
|
|
28
|
+
),
|
|
29
|
+
endLine: z
|
|
30
|
+
.number()
|
|
31
|
+
.int()
|
|
32
|
+
.min(1)
|
|
33
|
+
.optional()
|
|
34
|
+
.describe(
|
|
35
|
+
'Ending line number (1-indexed, inclusive). Required if startLine is provided.',
|
|
36
|
+
),
|
|
37
|
+
}),
|
|
38
|
+
async execute({
|
|
39
|
+
path,
|
|
40
|
+
startLine,
|
|
41
|
+
endLine,
|
|
42
|
+
}: {
|
|
43
|
+
path: string;
|
|
44
|
+
startLine?: number;
|
|
45
|
+
endLine?: number;
|
|
46
|
+
}) {
|
|
47
|
+
const req = expandTilde(path);
|
|
48
|
+
const abs = isAbsoluteLike(req) ? req : resolveSafePath(projectRoot, req);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
let content = await readFile(abs, 'utf-8');
|
|
52
|
+
|
|
53
|
+
if (startLine !== undefined && endLine !== undefined) {
|
|
54
|
+
const lines = content.split('\n');
|
|
55
|
+
const start = Math.max(1, startLine) - 1;
|
|
56
|
+
const end = Math.min(lines.length, endLine);
|
|
57
|
+
const selectedLines = lines.slice(start, end);
|
|
58
|
+
content = selectedLines.join('\n');
|
|
59
|
+
return {
|
|
60
|
+
path: req,
|
|
61
|
+
content,
|
|
62
|
+
size: content.length,
|
|
63
|
+
lineRange: `@${startLine}-${endLine}`,
|
|
64
|
+
totalLines: lines.length,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { path: req, content, size: content.length };
|
|
69
|
+
} catch (_error: unknown) {
|
|
70
|
+
const embedded = embeddedTextAssets[req];
|
|
71
|
+
if (embedded) {
|
|
72
|
+
const content = await readFile(embedded, 'utf-8');
|
|
73
|
+
return { path: req, content, size: content.length };
|
|
74
|
+
}
|
|
75
|
+
throw new Error(`File not found: ${req}`);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
return { name: 'read', tool: read };
|
|
80
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { tool, type Tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { exec } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { expandTilde, isAbsoluteLike, resolveSafePath } from './util.ts';
|
|
6
|
+
import DESCRIPTION from './tree.txt' with { type: 'text' };
|
|
7
|
+
import { toIgnoredBasenames } from '../ignore.ts';
|
|
8
|
+
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
|
|
11
|
+
// description imported above
|
|
12
|
+
|
|
13
|
+
export function buildTreeTool(projectRoot: string): {
|
|
14
|
+
name: string;
|
|
15
|
+
tool: Tool;
|
|
16
|
+
} {
|
|
17
|
+
const tree = tool({
|
|
18
|
+
description: DESCRIPTION,
|
|
19
|
+
inputSchema: z.object({
|
|
20
|
+
path: z.string().default('.'),
|
|
21
|
+
depth: z
|
|
22
|
+
.number()
|
|
23
|
+
.int()
|
|
24
|
+
.min(1)
|
|
25
|
+
.max(20)
|
|
26
|
+
.optional()
|
|
27
|
+
.describe('Optional depth limit (defaults to full depth).'),
|
|
28
|
+
ignore: z
|
|
29
|
+
.array(z.string())
|
|
30
|
+
.optional()
|
|
31
|
+
.describe('List of directory names/globs to ignore'),
|
|
32
|
+
}),
|
|
33
|
+
async execute({
|
|
34
|
+
path,
|
|
35
|
+
depth,
|
|
36
|
+
ignore,
|
|
37
|
+
}: {
|
|
38
|
+
path: string;
|
|
39
|
+
depth?: number;
|
|
40
|
+
ignore?: string[];
|
|
41
|
+
}) {
|
|
42
|
+
const req = expandTilde(path || '.');
|
|
43
|
+
const start = isAbsoluteLike(req)
|
|
44
|
+
? req
|
|
45
|
+
: resolveSafePath(projectRoot, req || '.');
|
|
46
|
+
const ignored = toIgnoredBasenames(ignore);
|
|
47
|
+
|
|
48
|
+
let cmd = 'tree';
|
|
49
|
+
if (typeof depth === 'number') cmd += ` -L ${depth}`;
|
|
50
|
+
if (ignored.size) {
|
|
51
|
+
const pattern = Array.from(ignored).join('|');
|
|
52
|
+
cmd += ` -I '${pattern.replace(/'/g, "'\\''")}'`;
|
|
53
|
+
}
|
|
54
|
+
cmd += ' .';
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const { stdout } = await execAsync(cmd, {
|
|
58
|
+
cwd: start,
|
|
59
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
60
|
+
});
|
|
61
|
+
const output = stdout.trimEnd();
|
|
62
|
+
return { path: req, depth: depth ?? null, tree: output };
|
|
63
|
+
} catch (error: unknown) {
|
|
64
|
+
const err = error as { stderr?: string; stdout?: string };
|
|
65
|
+
const message = (err.stderr || err.stdout || 'tree failed').trim();
|
|
66
|
+
throw new Error(`tree failed for ${req}: ${message}`);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
return { name: 'tree', tool: tree };
|
|
71
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { tool, type Tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
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
|
+
const dirPath = abs.slice(0, abs.lastIndexOf('/'));
|
|
47
|
+
await mkdir(dirPath, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
let existed = false;
|
|
50
|
+
let oldText = '';
|
|
51
|
+
try {
|
|
52
|
+
oldText = await readFile(abs, 'utf-8');
|
|
53
|
+
existed = true;
|
|
54
|
+
} catch {}
|
|
55
|
+
await writeFile(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
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { tool, type Tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { exec } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import GIT_STATUS_DESCRIPTION from './git.status.txt' with { type: 'text' };
|
|
6
|
+
import GIT_DIFF_DESCRIPTION from './git.diff.txt' with { type: 'text' };
|
|
7
|
+
import GIT_COMMIT_DESCRIPTION from './git.commit.txt' with { type: 'text' };
|
|
8
|
+
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
|
|
11
|
+
export function buildGitTools(
|
|
12
|
+
projectRoot: string,
|
|
13
|
+
): Array<{ name: string; tool: Tool }> {
|
|
14
|
+
// Helper to find git root directory
|
|
15
|
+
async function findGitRoot(): Promise<string> {
|
|
16
|
+
try {
|
|
17
|
+
const { stdout } = await execAsync(
|
|
18
|
+
`git -C "${projectRoot}" rev-parse --show-toplevel`,
|
|
19
|
+
);
|
|
20
|
+
return stdout.trim() || projectRoot;
|
|
21
|
+
} catch {
|
|
22
|
+
return projectRoot;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function inRepo(): Promise<boolean> {
|
|
27
|
+
try {
|
|
28
|
+
const { stdout } = await execAsync(
|
|
29
|
+
`git -C "${projectRoot}" rev-parse --is-inside-work-tree`,
|
|
30
|
+
);
|
|
31
|
+
return stdout.trim() === 'true';
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const git_status = tool({
|
|
38
|
+
description: GIT_STATUS_DESCRIPTION,
|
|
39
|
+
inputSchema: z.object({}).optional(),
|
|
40
|
+
async execute() {
|
|
41
|
+
if (!(await inRepo())) throw new Error('Not a git repository');
|
|
42
|
+
const gitRoot = await findGitRoot();
|
|
43
|
+
const { stdout } = await execAsync(
|
|
44
|
+
`git -C "${gitRoot}" status --porcelain=v1`,
|
|
45
|
+
);
|
|
46
|
+
const lines = stdout.split('\n').filter(Boolean);
|
|
47
|
+
let staged = 0;
|
|
48
|
+
let unstaged = 0;
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
const x = line[0];
|
|
51
|
+
const y = line[1];
|
|
52
|
+
if (!x || !y) continue;
|
|
53
|
+
if (x === '!' && y === '!') continue; // ignored files
|
|
54
|
+
const isUntracked = x === '?' && y === '?';
|
|
55
|
+
if (x !== ' ' && !isUntracked) staged += 1;
|
|
56
|
+
if (isUntracked || y !== ' ') unstaged += 1;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
staged,
|
|
60
|
+
unstaged,
|
|
61
|
+
raw: lines.slice(0, 200),
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const git_diff = tool({
|
|
67
|
+
description: GIT_DIFF_DESCRIPTION,
|
|
68
|
+
inputSchema: z.object({ all: z.boolean().optional().default(false) }),
|
|
69
|
+
async execute({ all }: { all?: boolean }) {
|
|
70
|
+
if (!(await inRepo())) throw new Error('Not a git repository');
|
|
71
|
+
const gitRoot = await findGitRoot();
|
|
72
|
+
// When all=true, show full working tree diff relative to HEAD
|
|
73
|
+
// so both staged and unstaged changes are included. Otherwise,
|
|
74
|
+
// show only the staged diff (index vs HEAD).
|
|
75
|
+
const cmd = all
|
|
76
|
+
? `git -C "${gitRoot}" diff HEAD`
|
|
77
|
+
: `git -C "${gitRoot}" diff --staged`;
|
|
78
|
+
const { stdout } = await execAsync(cmd, { maxBuffer: 10 * 1024 * 1024 });
|
|
79
|
+
const limited = stdout.split('\n').slice(0, 5000).join('\n');
|
|
80
|
+
return { all: !!all, patch: limited };
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const git_commit = tool({
|
|
85
|
+
description: GIT_COMMIT_DESCRIPTION,
|
|
86
|
+
inputSchema: z.object({
|
|
87
|
+
message: z.string().min(5),
|
|
88
|
+
amend: z.boolean().optional().default(false),
|
|
89
|
+
signoff: z.boolean().optional().default(false),
|
|
90
|
+
}),
|
|
91
|
+
async execute({
|
|
92
|
+
message,
|
|
93
|
+
amend,
|
|
94
|
+
signoff,
|
|
95
|
+
}: {
|
|
96
|
+
message: string;
|
|
97
|
+
amend?: boolean;
|
|
98
|
+
signoff?: boolean;
|
|
99
|
+
}) {
|
|
100
|
+
if (!(await inRepo())) throw new Error('Not a git repository');
|
|
101
|
+
const gitRoot = await findGitRoot();
|
|
102
|
+
const args = [
|
|
103
|
+
'git',
|
|
104
|
+
'-C',
|
|
105
|
+
`"${gitRoot}"`,
|
|
106
|
+
'commit',
|
|
107
|
+
'-m',
|
|
108
|
+
`"${message.replace(/"/g, '\\"')}"`,
|
|
109
|
+
];
|
|
110
|
+
if (amend) args.push('--amend');
|
|
111
|
+
if (signoff) args.push('--signoff');
|
|
112
|
+
try {
|
|
113
|
+
const { stdout } = await execAsync(args.join(' '));
|
|
114
|
+
return { result: stdout.trim() };
|
|
115
|
+
} catch (error: unknown) {
|
|
116
|
+
const err = error as { stderr?: string; message?: string };
|
|
117
|
+
const txt = err.stderr || err.message || 'git commit failed';
|
|
118
|
+
throw new Error(txt);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return [
|
|
124
|
+
{ name: 'git_status', tool: git_status },
|
|
125
|
+
{ name: 'git_diff', tool: git_diff },
|
|
126
|
+
{ name: 'git_commit', tool: git_commit },
|
|
127
|
+
];
|
|
128
|
+
}
|