@agi-cli/sdk 0.1.48 → 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,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
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { tool, type Tool } from 'ai';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import DESCRIPTION from './plan.txt' with { type: 'text' };
|
|
4
|
-
|
|
5
|
-
const STATUS_ENUM = z.enum(['pending', 'in_progress', 'completed']);
|
|
6
|
-
|
|
7
|
-
const ITEM_SCHEMA = z
|
|
8
|
-
.union([
|
|
9
|
-
z.string().min(1, 'Plan steps must be non-empty'),
|
|
10
|
-
z.object({
|
|
11
|
-
step: z.string().min(1, 'Plan steps must be non-empty'),
|
|
12
|
-
status: STATUS_ENUM.optional(),
|
|
13
|
-
}),
|
|
14
|
-
])
|
|
15
|
-
.describe('Plan item');
|
|
16
|
-
|
|
17
|
-
type PlanItemInput = z.infer<typeof ITEM_SCHEMA>;
|
|
18
|
-
|
|
19
|
-
function normalizeItems(
|
|
20
|
-
raw: PlanItemInput[],
|
|
21
|
-
): Array<{ step: string; status: z.infer<typeof STATUS_ENUM> }> {
|
|
22
|
-
const normalized = raw.map((item) => {
|
|
23
|
-
if (typeof item === 'string') {
|
|
24
|
-
return { step: item.trim(), status: 'pending' as const };
|
|
25
|
-
}
|
|
26
|
-
const step = item.step.trim();
|
|
27
|
-
const status = item.status ?? 'pending';
|
|
28
|
-
return { step, status };
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
const filtered = normalized.filter((item) => item.step.length > 0);
|
|
32
|
-
if (!filtered.length) {
|
|
33
|
-
throw new Error('At least one plan step is required');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const inProgressCount = filtered.filter(
|
|
37
|
-
(item) => item.status === 'in_progress',
|
|
38
|
-
).length;
|
|
39
|
-
if (inProgressCount > 1) {
|
|
40
|
-
throw new Error('Only one plan step may be marked as in_progress');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return filtered;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export const updatePlanTool: Tool = tool({
|
|
47
|
-
description: DESCRIPTION,
|
|
48
|
-
inputSchema: z.object({
|
|
49
|
-
items: z.array(ITEM_SCHEMA).min(1).describe('Ordered list of plan steps'),
|
|
50
|
-
note: z
|
|
51
|
-
.string()
|
|
52
|
-
.optional()
|
|
53
|
-
.describe('Optional note or context for the plan update'),
|
|
54
|
-
}),
|
|
55
|
-
async execute({ items, note }: { items: PlanItemInput[]; note?: string }) {
|
|
56
|
-
return { items: normalizeItems(items), note };
|
|
57
|
-
},
|
|
58
|
-
});
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { tool } from 'ai';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import DESCRIPTION from './progress.txt' with { type: 'text' };
|
|
4
|
-
|
|
5
|
-
// Progress update tool: allows the model to emit lightweight status signals
|
|
6
|
-
// without revealing chain-of-thought. The runner/UI should surface these
|
|
7
|
-
// messages immediately.
|
|
8
|
-
const StageEnum = z.enum([
|
|
9
|
-
'planning',
|
|
10
|
-
'discovering',
|
|
11
|
-
'generating',
|
|
12
|
-
'preparing',
|
|
13
|
-
'writing',
|
|
14
|
-
'verifying',
|
|
15
|
-
]);
|
|
16
|
-
|
|
17
|
-
export const progressUpdateTool = tool({
|
|
18
|
-
description: DESCRIPTION,
|
|
19
|
-
inputSchema: z.object({
|
|
20
|
-
message: z
|
|
21
|
-
.string()
|
|
22
|
-
.min(1)
|
|
23
|
-
.max(200)
|
|
24
|
-
.describe('Short, user-facing status message (<= 200 chars).'),
|
|
25
|
-
pct: z
|
|
26
|
-
.number()
|
|
27
|
-
.min(0)
|
|
28
|
-
.max(100)
|
|
29
|
-
.optional()
|
|
30
|
-
.describe('Optional overall progress percent 0-100.'),
|
|
31
|
-
stage: StageEnum.optional().default('planning'),
|
|
32
|
-
}),
|
|
33
|
-
async execute({
|
|
34
|
-
message,
|
|
35
|
-
pct,
|
|
36
|
-
stage,
|
|
37
|
-
}: {
|
|
38
|
-
message: string;
|
|
39
|
-
pct?: number;
|
|
40
|
-
stage?: z.infer<typeof StageEnum>;
|
|
41
|
-
}) {
|
|
42
|
-
// Keep the tool lightweight; no side effects beyond the event itself.
|
|
43
|
-
// Returning the normalized payload allows generic renderers to inspect it if needed.
|
|
44
|
-
const normalizedPct =
|
|
45
|
-
typeof pct === 'number'
|
|
46
|
-
? Math.min(100, Math.max(0, Math.round(pct)))
|
|
47
|
-
: undefined;
|
|
48
|
-
return {
|
|
49
|
-
ok: true,
|
|
50
|
-
message,
|
|
51
|
-
pct: normalizedPct,
|
|
52
|
-
stage: stage ?? 'planning',
|
|
53
|
-
} as const;
|
|
54
|
-
},
|
|
55
|
-
});
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
- Emit a short, user-facing progress/status update
|
|
2
|
-
- Supports optional `pct` (0–100) and `stage` indicators
|
|
3
|
-
- Lightweight; intended for immediate UI display
|
|
4
|
-
|
|
5
|
-
Usage tips:
|
|
6
|
-
- Keep messages short (<= 200 chars) and informative
|
|
7
|
-
- Use multiple updates during long-running tasks
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { tool, type Tool } from 'ai';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { $ } from 'bun';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import DESCRIPTION from './ripgrep.txt' with { type: 'text' };
|
|
6
|
-
|
|
7
|
-
export function buildRipgrepTool(projectRoot: string): {
|
|
8
|
-
name: string;
|
|
9
|
-
tool: Tool;
|
|
10
|
-
} {
|
|
11
|
-
const rg = tool({
|
|
12
|
-
description: DESCRIPTION,
|
|
13
|
-
inputSchema: z.object({
|
|
14
|
-
query: z.string().min(1).describe('Search pattern (regex by default)'),
|
|
15
|
-
path: z
|
|
16
|
-
.string()
|
|
17
|
-
.optional()
|
|
18
|
-
.default('.')
|
|
19
|
-
.describe('Relative path to search in'),
|
|
20
|
-
ignoreCase: z.boolean().optional().default(false),
|
|
21
|
-
glob: z
|
|
22
|
-
.array(z.string())
|
|
23
|
-
.optional()
|
|
24
|
-
.describe('One or more glob patterns to include'),
|
|
25
|
-
maxResults: z.number().int().min(1).max(5000).optional().default(500),
|
|
26
|
-
}),
|
|
27
|
-
async execute({
|
|
28
|
-
query,
|
|
29
|
-
path = '.',
|
|
30
|
-
ignoreCase,
|
|
31
|
-
glob,
|
|
32
|
-
maxResults = 500,
|
|
33
|
-
}: {
|
|
34
|
-
query: string;
|
|
35
|
-
path?: string;
|
|
36
|
-
ignoreCase?: boolean;
|
|
37
|
-
glob?: string[];
|
|
38
|
-
maxResults?: number;
|
|
39
|
-
}) {
|
|
40
|
-
function expandTilde(p: string) {
|
|
41
|
-
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
42
|
-
if (!home) return p;
|
|
43
|
-
if (p === '~') return home;
|
|
44
|
-
if (p.startsWith('~/')) return `${home}/${p.slice(2)}`;
|
|
45
|
-
return p;
|
|
46
|
-
}
|
|
47
|
-
const p = expandTilde(String(path ?? '.')).trim();
|
|
48
|
-
const isAbs = p.startsWith('/') || /^[A-Za-z]:[\\/]/.test(p);
|
|
49
|
-
const target = p ? (isAbs ? p : join(projectRoot, p)) : projectRoot;
|
|
50
|
-
const args = ['--no-heading', '--line-number', '--color=never'];
|
|
51
|
-
if (ignoreCase) args.push('-i');
|
|
52
|
-
if (Array.isArray(glob)) for (const g of glob) args.push('-g', g);
|
|
53
|
-
args.push('--max-count', String(maxResults));
|
|
54
|
-
args.push(query, target);
|
|
55
|
-
try {
|
|
56
|
-
const output = await $`rg ${args}`.quiet().text();
|
|
57
|
-
const lines = output.split('\n').filter(Boolean).slice(0, maxResults);
|
|
58
|
-
const matches = lines.map((l) => {
|
|
59
|
-
const m = l.match(/^(.*?):(\d+):(.*)$/);
|
|
60
|
-
if (!m) return { file: '', line: 0, text: l };
|
|
61
|
-
return { file: m[1], line: Number(m[2]), text: m[3] };
|
|
62
|
-
});
|
|
63
|
-
return { count: matches.length, matches };
|
|
64
|
-
} catch (err) {
|
|
65
|
-
const stderr = (err as { stderr?: string })?.stderr ?? String(err);
|
|
66
|
-
return { count: 0, matches: [], error: stderr?.trim() };
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
return { name: 'ripgrep', tool: rg };
|
|
71
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
- Search files using ripgrep (rg) with regex patterns
|
|
2
|
-
- Returns a flat list of matches with `file`, `line`, and `text`
|
|
3
|
-
- Supports include globs and case-insensitive search
|
|
4
|
-
|
|
5
|
-
Usage tips:
|
|
6
|
-
- Use the Grep tool for a friendly summary grouped by file
|
|
7
|
-
- Use the Glob tool first to limit the search set if needed
|