@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
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
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { tool, type Tool } from 'ai';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import DESCRIPTION from './grep.txt' with { type: 'text' };
|
|
5
|
-
import { defaultIgnoreGlobs } from './ignore.ts';
|
|
6
|
-
|
|
7
|
-
function expandTilde(p: string) {
|
|
8
|
-
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
9
|
-
if (!home) return p;
|
|
10
|
-
if (p === '~') return home;
|
|
11
|
-
if (p.startsWith('~/')) return `${home}/${p.slice(2)}`;
|
|
12
|
-
return p;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function buildGrepTool(projectRoot: string): {
|
|
16
|
-
name: string;
|
|
17
|
-
tool: Tool;
|
|
18
|
-
} {
|
|
19
|
-
const grep = tool({
|
|
20
|
-
description: DESCRIPTION,
|
|
21
|
-
inputSchema: z.object({
|
|
22
|
-
pattern: z
|
|
23
|
-
.string()
|
|
24
|
-
.describe('Regex pattern to search for in file contents'),
|
|
25
|
-
path: z
|
|
26
|
-
.string()
|
|
27
|
-
.optional()
|
|
28
|
-
.describe('Directory to search in (default: project root).'),
|
|
29
|
-
include: z
|
|
30
|
-
.string()
|
|
31
|
-
.optional()
|
|
32
|
-
.describe('File glob to include (e.g., "*.js", "*.{ts,tsx}")'),
|
|
33
|
-
ignore: z
|
|
34
|
-
.array(z.string())
|
|
35
|
-
.optional()
|
|
36
|
-
.describe('Glob patterns to exclude from search'),
|
|
37
|
-
}),
|
|
38
|
-
async execute(params) {
|
|
39
|
-
const pattern = String(params.pattern || '');
|
|
40
|
-
if (!pattern) throw new Error('pattern is required');
|
|
41
|
-
|
|
42
|
-
const p = expandTilde(String(params.path || '')).trim();
|
|
43
|
-
const isAbs = p.startsWith('/') || /^[A-Za-z]:[\\/]/.test(p);
|
|
44
|
-
const searchPath = p ? (isAbs ? p : join(projectRoot, p)) : projectRoot;
|
|
45
|
-
|
|
46
|
-
const rgPath = 'rg';
|
|
47
|
-
const args: string[] = ['-n', '--color', 'never'];
|
|
48
|
-
for (const g of defaultIgnoreGlobs(params.ignore)) args.push('--glob', g);
|
|
49
|
-
if (params.include) {
|
|
50
|
-
args.push('--glob', params.include);
|
|
51
|
-
}
|
|
52
|
-
args.push(pattern, searchPath);
|
|
53
|
-
|
|
54
|
-
const proc = Bun.spawn([rgPath, ...args], {
|
|
55
|
-
stdout: 'pipe',
|
|
56
|
-
stderr: 'pipe',
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const output = await new Response(proc.stdout).text();
|
|
60
|
-
const errorOutput = await new Response(proc.stderr).text();
|
|
61
|
-
const exitCode = await proc.exited;
|
|
62
|
-
|
|
63
|
-
if (exitCode === 1) {
|
|
64
|
-
return {
|
|
65
|
-
title: pattern,
|
|
66
|
-
metadata: { matches: 0, truncated: false },
|
|
67
|
-
output: 'No files found',
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
if (exitCode !== 0) {
|
|
71
|
-
throw new Error(`ripgrep failed: ${errorOutput}`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const lines = output.trim().split('\n');
|
|
75
|
-
const matches: Array<{
|
|
76
|
-
path: string;
|
|
77
|
-
modTime: number;
|
|
78
|
-
lineNum: number;
|
|
79
|
-
lineText: string;
|
|
80
|
-
}> = [];
|
|
81
|
-
|
|
82
|
-
for (const line of lines) {
|
|
83
|
-
if (!line) continue;
|
|
84
|
-
const idx1 = line.indexOf(':');
|
|
85
|
-
const idx2 = idx1 === -1 ? -1 : line.indexOf(':', idx1 + 1);
|
|
86
|
-
if (idx1 === -1 || idx2 === -1) continue;
|
|
87
|
-
const filePath = line.slice(0, idx1);
|
|
88
|
-
const lineNumStr = line.slice(idx1 + 1, idx2);
|
|
89
|
-
const lineText = line.slice(idx2 + 1);
|
|
90
|
-
const lineNum = parseInt(lineNumStr, 10);
|
|
91
|
-
if (!filePath || !Number.isFinite(lineNum)) continue;
|
|
92
|
-
const stats = await Bun.file(filePath)
|
|
93
|
-
.stat()
|
|
94
|
-
.then((s) => s.mtime.getTime())
|
|
95
|
-
.catch(() => 0);
|
|
96
|
-
matches.push({ path: filePath, modTime: stats, lineNum, lineText });
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
matches.sort((a, b) => b.modTime - a.modTime);
|
|
100
|
-
|
|
101
|
-
const limit = 100;
|
|
102
|
-
const truncated = matches.length > limit;
|
|
103
|
-
const finalMatches = truncated ? matches.slice(0, limit) : matches;
|
|
104
|
-
|
|
105
|
-
if (finalMatches.length === 0) {
|
|
106
|
-
return {
|
|
107
|
-
title: pattern,
|
|
108
|
-
metadata: { matches: 0, truncated: false },
|
|
109
|
-
output: 'No files found',
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const outputLines = [`Found ${finalMatches.length} matches`];
|
|
114
|
-
let currentFile = '';
|
|
115
|
-
for (const match of finalMatches) {
|
|
116
|
-
if (currentFile !== match.path) {
|
|
117
|
-
if (currentFile !== '') outputLines.push('');
|
|
118
|
-
currentFile = match.path;
|
|
119
|
-
outputLines.push(`${match.path}:`);
|
|
120
|
-
}
|
|
121
|
-
outputLines.push(` Line ${match.lineNum}: ${match.lineText}`);
|
|
122
|
-
}
|
|
123
|
-
if (truncated) {
|
|
124
|
-
outputLines.push('');
|
|
125
|
-
outputLines.push(
|
|
126
|
-
'(Results are truncated. Consider using a more specific path or pattern.)',
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
title: pattern,
|
|
132
|
-
metadata: { matches: finalMatches.length, truncated },
|
|
133
|
-
output: outputLines.join('\n'),
|
|
134
|
-
};
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
return { name: 'grep', tool: grep };
|
|
138
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
- Fast content search tool powered by ripgrep (rg)
|
|
2
|
-
- Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")
|
|
3
|
-
- Optional include glob to filter files (e.g., "*.js", "*.{ts,tsx}")
|
|
4
|
-
- Returns files with at least one match and line previews, sorted by modification time
|
|
5
|
-
- Skips common build and cache folders by default; add 'ignore' patterns to refine
|
|
6
|
-
|
|
7
|
-
Usage tips:
|
|
8
|
-
- For counting matches, use the Bash tool with rg directly (do not use grep)
|
|
9
|
-
- Batch multiple searches when exploring a codebase broadly
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
export const IGNORE_PATTERNS: string[] = [
|
|
2
|
-
'node_modules/',
|
|
3
|
-
'__pycache__/',
|
|
4
|
-
'.git/',
|
|
5
|
-
'dist/',
|
|
6
|
-
'build/',
|
|
7
|
-
'target/',
|
|
8
|
-
'vendor/',
|
|
9
|
-
'bin/',
|
|
10
|
-
'obj/',
|
|
11
|
-
'.idea/',
|
|
12
|
-
'.vscode/',
|
|
13
|
-
'.zig-cache/',
|
|
14
|
-
'zig-out',
|
|
15
|
-
'.coverage',
|
|
16
|
-
'coverage/',
|
|
17
|
-
'vendor/',
|
|
18
|
-
'tmp/',
|
|
19
|
-
'temp/',
|
|
20
|
-
'.cache/',
|
|
21
|
-
'cache/',
|
|
22
|
-
'logs/',
|
|
23
|
-
'.venv/',
|
|
24
|
-
'venv/',
|
|
25
|
-
'env/',
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
export function defaultIgnoreGlobs(extra?: string[]): string[] {
|
|
29
|
-
const base = IGNORE_PATTERNS.map((p) => `!${p}*`);
|
|
30
|
-
if (Array.isArray(extra) && extra.length) return base.concat(extra);
|
|
31
|
-
return base;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function toIgnoredBasenames(extra?: string[]): Set<string> {
|
|
35
|
-
const names = new Set<string>();
|
|
36
|
-
for (const p of IGNORE_PATTERNS) {
|
|
37
|
-
const n = p.replace(/\/$/, '');
|
|
38
|
-
if (n) names.add(n);
|
|
39
|
-
}
|
|
40
|
-
for (const p of extra ?? []) {
|
|
41
|
-
const n = String(p).replace(/^!/, '').replace(/\/$/, '');
|
|
42
|
-
if (n) names.add(n);
|
|
43
|
-
}
|
|
44
|
-
return names;
|
|
45
|
-
}
|
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
import { tool, type Tool } from 'ai';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { $ } from 'bun';
|
|
4
|
-
import DESCRIPTION from './patch.txt' with { type: 'text' };
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Apply enveloped patch by directly modifying files
|
|
8
|
-
*/
|
|
9
|
-
async function applyEnvelopedPatch(projectRoot: string, patch: string) {
|
|
10
|
-
const lines = patch.split('\n');
|
|
11
|
-
let currentFile: string | null = null;
|
|
12
|
-
let operation: 'add' | 'update' | 'delete' | null = null;
|
|
13
|
-
let fileContent: string[] = [];
|
|
14
|
-
|
|
15
|
-
async function applyCurrentFile() {
|
|
16
|
-
if (!currentFile || !operation) return { ok: true };
|
|
17
|
-
|
|
18
|
-
const fullPath = `${projectRoot}/${currentFile}`;
|
|
19
|
-
|
|
20
|
-
if (operation === 'delete') {
|
|
21
|
-
try {
|
|
22
|
-
await Bun.write(fullPath, '');
|
|
23
|
-
} catch (e) {
|
|
24
|
-
return {
|
|
25
|
-
ok: false,
|
|
26
|
-
error: `Failed to delete ${currentFile}: ${e instanceof Error ? e.message : String(e)}`,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
} else if (operation === 'add') {
|
|
30
|
-
// For add, only use lines starting with +
|
|
31
|
-
const newContent = fileContent
|
|
32
|
-
.filter((l) => l.startsWith('+'))
|
|
33
|
-
.map((l) => l.substring(1))
|
|
34
|
-
.join('\n');
|
|
35
|
-
try {
|
|
36
|
-
await Bun.write(fullPath, newContent);
|
|
37
|
-
} catch (e) {
|
|
38
|
-
return {
|
|
39
|
-
ok: false,
|
|
40
|
-
error: `Failed to create ${currentFile}: ${e instanceof Error ? e.message : String(e)}`,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
} else if (operation === 'update') {
|
|
44
|
-
try {
|
|
45
|
-
// Read existing file
|
|
46
|
-
let existingContent = '';
|
|
47
|
-
try {
|
|
48
|
-
const file = Bun.file(fullPath);
|
|
49
|
-
existingContent = await file.text();
|
|
50
|
-
} catch {
|
|
51
|
-
// File doesn't exist yet
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Get the old content (lines starting with -)
|
|
55
|
-
const oldLines = fileContent
|
|
56
|
-
.filter((l) => l.startsWith('-'))
|
|
57
|
-
.map((l) => l.substring(1));
|
|
58
|
-
|
|
59
|
-
// Get the new content (lines starting with +)
|
|
60
|
-
const newLines = fileContent
|
|
61
|
-
.filter((l) => l.startsWith('+'))
|
|
62
|
-
.map((l) => l.substring(1));
|
|
63
|
-
|
|
64
|
-
// Simple replacement: if old content is empty, append
|
|
65
|
-
// Otherwise try to replace old with new
|
|
66
|
-
let newContent = existingContent;
|
|
67
|
-
if (oldLines.length > 0) {
|
|
68
|
-
const oldText = oldLines.join('\n');
|
|
69
|
-
const newText = newLines.join('\n');
|
|
70
|
-
if (existingContent.includes(oldText)) {
|
|
71
|
-
newContent = existingContent.replace(oldText, newText);
|
|
72
|
-
} else {
|
|
73
|
-
// Can't find exact match, this is where enveloped format fails
|
|
74
|
-
// Provide more context about what couldn't be found
|
|
75
|
-
const preview = oldText.substring(0, 100);
|
|
76
|
-
return {
|
|
77
|
-
ok: false,
|
|
78
|
-
error: `Cannot find content to replace in ${currentFile}. Looking for: "${preview}${oldText.length > 100 ? '...' : ''}"`,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
} else if (newLines.length > 0) {
|
|
82
|
-
// Just appending new lines
|
|
83
|
-
newContent =
|
|
84
|
-
existingContent +
|
|
85
|
-
(existingContent.endsWith('\n') ? '' : '\n') +
|
|
86
|
-
newLines.join('\n');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
await Bun.write(fullPath, newContent);
|
|
90
|
-
} catch (e) {
|
|
91
|
-
return {
|
|
92
|
-
ok: false,
|
|
93
|
-
error: `Failed to update ${currentFile}: ${e instanceof Error ? e.message : String(e)}`,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return { ok: true };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
for (const line of lines) {
|
|
101
|
-
if (line === '*** Begin Patch' || line === '*** End Patch') {
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
line.startsWith('*** Add File:') ||
|
|
107
|
-
line.startsWith('*** Update File:') ||
|
|
108
|
-
line.startsWith('*** Delete File:')
|
|
109
|
-
) {
|
|
110
|
-
// Apply previous file if any
|
|
111
|
-
const result = await applyCurrentFile();
|
|
112
|
-
if (!result.ok) return result;
|
|
113
|
-
|
|
114
|
-
// Start new file
|
|
115
|
-
if (line.startsWith('*** Add File:')) {
|
|
116
|
-
currentFile = line.replace('*** Add File:', '').trim();
|
|
117
|
-
operation = 'add';
|
|
118
|
-
} else if (line.startsWith('*** Update File:')) {
|
|
119
|
-
currentFile = line.replace('*** Update File:', '').trim();
|
|
120
|
-
operation = 'update';
|
|
121
|
-
} else if (line.startsWith('*** Delete File:')) {
|
|
122
|
-
currentFile = line.replace('*** Delete File:', '').trim();
|
|
123
|
-
operation = 'delete';
|
|
124
|
-
}
|
|
125
|
-
fileContent = [];
|
|
126
|
-
} else if (
|
|
127
|
-
currentFile &&
|
|
128
|
-
(line.startsWith('+') || line.startsWith('-') || line.startsWith(' '))
|
|
129
|
-
) {
|
|
130
|
-
// Collect patch content lines
|
|
131
|
-
fileContent.push(line);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Apply the last file
|
|
136
|
-
const result = await applyCurrentFile();
|
|
137
|
-
return result;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export function buildApplyPatchTool(projectRoot: string): {
|
|
141
|
-
name: string;
|
|
142
|
-
tool: Tool;
|
|
143
|
-
} {
|
|
144
|
-
const applyPatch = tool({
|
|
145
|
-
description: DESCRIPTION,
|
|
146
|
-
inputSchema: z.object({
|
|
147
|
-
patch: z.string().min(1).describe('Unified diff patch content'),
|
|
148
|
-
allowRejects: z
|
|
149
|
-
.boolean()
|
|
150
|
-
.optional()
|
|
151
|
-
.default(false)
|
|
152
|
-
.describe(
|
|
153
|
-
'Allow hunks to be rejected without failing the whole operation',
|
|
154
|
-
),
|
|
155
|
-
}),
|
|
156
|
-
async execute({
|
|
157
|
-
patch,
|
|
158
|
-
allowRejects,
|
|
159
|
-
}: {
|
|
160
|
-
patch: string;
|
|
161
|
-
allowRejects?: boolean;
|
|
162
|
-
}) {
|
|
163
|
-
// Check if this is an enveloped patch format
|
|
164
|
-
const isEnveloped =
|
|
165
|
-
patch.includes('*** Begin Patch') ||
|
|
166
|
-
patch.includes('*** Add File:') ||
|
|
167
|
-
patch.includes('*** Update File:');
|
|
168
|
-
|
|
169
|
-
if (isEnveloped) {
|
|
170
|
-
// Handle enveloped patches directly
|
|
171
|
-
const result = await applyEnvelopedPatch(projectRoot, patch);
|
|
172
|
-
const summary = summarizePatch(patch);
|
|
173
|
-
if (result.ok) {
|
|
174
|
-
return {
|
|
175
|
-
ok: true,
|
|
176
|
-
output: 'Applied enveloped patch',
|
|
177
|
-
artifact: { kind: 'file_diff', patch, summary },
|
|
178
|
-
} as const;
|
|
179
|
-
} else {
|
|
180
|
-
return {
|
|
181
|
-
ok: false,
|
|
182
|
-
error: result.error || 'Failed to apply enveloped patch',
|
|
183
|
-
artifact: { kind: 'file_diff', patch, summary },
|
|
184
|
-
} as const;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// For unified diffs, use git apply as before
|
|
189
|
-
const dir = `${projectRoot}/.agi/tmp`;
|
|
190
|
-
try {
|
|
191
|
-
await $`mkdir -p ${dir}`;
|
|
192
|
-
} catch {}
|
|
193
|
-
const file = `${dir}/patch-${Date.now()}.diff`;
|
|
194
|
-
await Bun.write(file, patch);
|
|
195
|
-
const summary = summarizePatch(patch);
|
|
196
|
-
// Try -p1 first for canonical git-style patches (a/ b/ prefixes), then fall back to -p0.
|
|
197
|
-
const baseArgs = ['apply', '--whitespace=nowarn'];
|
|
198
|
-
const rejectArg = allowRejects ? ['--reject'] : [];
|
|
199
|
-
const tries: Array<string[]> = [
|
|
200
|
-
[...baseArgs, ...rejectArg, '-p1'],
|
|
201
|
-
[...baseArgs, ...rejectArg, '-p0'],
|
|
202
|
-
];
|
|
203
|
-
let lastError = '';
|
|
204
|
-
for (const args of tries) {
|
|
205
|
-
const cmd = ['git', '-C', projectRoot, ...args, file];
|
|
206
|
-
const proc = await $`${cmd}`.quiet().nothrow();
|
|
207
|
-
const out = await proc.text();
|
|
208
|
-
// Capture error output for later use
|
|
209
|
-
if (proc.exitCode !== 0) {
|
|
210
|
-
lastError = out || `git apply failed with exit code ${proc.exitCode}`;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Check if the patch was actually applied by looking at git status
|
|
214
|
-
// Sometimes git apply returns non-zero but the patch is applied
|
|
215
|
-
if (proc.exitCode === 0 || proc.exitCode === 1) {
|
|
216
|
-
// Check if any files were actually modified
|
|
217
|
-
const statusProc = await $`git -C ${projectRoot} status --porcelain`
|
|
218
|
-
.quiet()
|
|
219
|
-
.nothrow();
|
|
220
|
-
const statusOut = await statusProc.text();
|
|
221
|
-
if (statusOut && statusOut.trim().length > 0) {
|
|
222
|
-
// Files were changed, so patch was likely applied
|
|
223
|
-
return {
|
|
224
|
-
ok: true,
|
|
225
|
-
output: out?.trim() ?? '',
|
|
226
|
-
artifact: { kind: 'file_diff', patch, summary },
|
|
227
|
-
} as const;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
// Only continue trying if patch wasn't applied
|
|
231
|
-
}
|
|
232
|
-
// If both attempts fail with exit code, check if files were modified anyway
|
|
233
|
-
// Sometimes git apply exits with warnings but the patch is applied
|
|
234
|
-
const statusProc = await $`git -C ${projectRoot} status --porcelain`
|
|
235
|
-
.quiet()
|
|
236
|
-
.nothrow();
|
|
237
|
-
const statusOut = await statusProc.text();
|
|
238
|
-
if (statusOut && statusOut.trim().length > 0) {
|
|
239
|
-
// Files were changed, so patch was likely applied despite the exit code
|
|
240
|
-
return {
|
|
241
|
-
ok: true,
|
|
242
|
-
output: 'Patch applied with warnings',
|
|
243
|
-
artifact: { kind: 'file_diff', patch, summary },
|
|
244
|
-
} as const;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// If both attempts fail and no files changed, return error with more context
|
|
248
|
-
const errorDetails = lastError.includes('patch does not apply')
|
|
249
|
-
? 'The patch cannot be applied because the target content has changed or does not match. The file may have been modified since the patch was created.'
|
|
250
|
-
: lastError ||
|
|
251
|
-
'git apply failed (tried -p1 and -p0) — ensure paths match project root';
|
|
252
|
-
return {
|
|
253
|
-
ok: false,
|
|
254
|
-
error: errorDetails,
|
|
255
|
-
artifact: { kind: 'file_diff', patch, summary },
|
|
256
|
-
} as const;
|
|
257
|
-
},
|
|
258
|
-
});
|
|
259
|
-
return { name: 'apply_patch', tool: applyPatch };
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function summarizePatch(patch: string) {
|
|
263
|
-
const lines = String(patch || '').split('\n');
|
|
264
|
-
let files = 0;
|
|
265
|
-
let additions = 0;
|
|
266
|
-
let deletions = 0;
|
|
267
|
-
for (const l of lines) {
|
|
268
|
-
if (/^\*\*\*\s+(Add|Update|Delete) File:/.test(l)) files += 1;
|
|
269
|
-
else if (l.startsWith('+') && !l.startsWith('+++')) additions += 1;
|
|
270
|
-
else if (l.startsWith('-') && !l.startsWith('---')) deletions += 1;
|
|
271
|
-
}
|
|
272
|
-
return { files, additions, deletions };
|
|
273
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
- Apply a unified diff patch (`*** Begin Patch`/`*** Update File`/`---`/`+++`/`@@`)
|
|
2
|
-
- Uses `git apply` under the hood; tries `-p1` then `-p0`
|
|
3
|
-
- Returns an artifact summary and any output from `git apply`
|
|
4
|
-
|
|
5
|
-
Usage tips:
|
|
6
|
-
- Ensure paths in the patch match the project root (prefer `a/` and `b/` prefixes)
|
|
7
|
-
- For small edits, consider the Edit or Write tools instead
|