@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.
Files changed (49) hide show
  1. package/package.json +6 -5
  2. package/src/agent/types.ts +1 -1
  3. package/src/index.ts +10 -77
  4. package/src/errors.ts +0 -102
  5. package/src/providers/resolver.ts +0 -84
  6. package/src/streaming/artifacts.ts +0 -41
  7. package/src/tools/builtin/bash.ts +0 -73
  8. package/src/tools/builtin/bash.txt +0 -7
  9. package/src/tools/builtin/edit.ts +0 -145
  10. package/src/tools/builtin/edit.txt +0 -7
  11. package/src/tools/builtin/file-cache.ts +0 -39
  12. package/src/tools/builtin/finish.ts +0 -11
  13. package/src/tools/builtin/finish.txt +0 -5
  14. package/src/tools/builtin/fs/cd.ts +0 -19
  15. package/src/tools/builtin/fs/cd.txt +0 -5
  16. package/src/tools/builtin/fs/index.ts +0 -20
  17. package/src/tools/builtin/fs/ls.ts +0 -57
  18. package/src/tools/builtin/fs/ls.txt +0 -8
  19. package/src/tools/builtin/fs/pwd.ts +0 -17
  20. package/src/tools/builtin/fs/pwd.txt +0 -5
  21. package/src/tools/builtin/fs/read.ts +0 -49
  22. package/src/tools/builtin/fs/read.txt +0 -8
  23. package/src/tools/builtin/fs/tree.ts +0 -67
  24. package/src/tools/builtin/fs/tree.txt +0 -8
  25. package/src/tools/builtin/fs/util.ts +0 -95
  26. package/src/tools/builtin/fs/write.ts +0 -61
  27. package/src/tools/builtin/fs/write.txt +0 -8
  28. package/src/tools/builtin/git.commit.txt +0 -6
  29. package/src/tools/builtin/git.diff.txt +0 -5
  30. package/src/tools/builtin/git.status.txt +0 -5
  31. package/src/tools/builtin/git.ts +0 -112
  32. package/src/tools/builtin/glob.ts +0 -82
  33. package/src/tools/builtin/glob.txt +0 -8
  34. package/src/tools/builtin/grep.ts +0 -138
  35. package/src/tools/builtin/grep.txt +0 -9
  36. package/src/tools/builtin/ignore.ts +0 -45
  37. package/src/tools/builtin/patch.ts +0 -273
  38. package/src/tools/builtin/patch.txt +0 -7
  39. package/src/tools/builtin/plan.ts +0 -58
  40. package/src/tools/builtin/plan.txt +0 -6
  41. package/src/tools/builtin/progress.ts +0 -55
  42. package/src/tools/builtin/progress.txt +0 -7
  43. package/src/tools/builtin/ripgrep.ts +0 -71
  44. package/src/tools/builtin/ripgrep.txt +0 -7
  45. package/src/tools/builtin/websearch.ts +0 -219
  46. package/src/tools/builtin/websearch.txt +0 -12
  47. package/src/tools/loader.ts +0 -390
  48. package/src/types/index.ts +0 -11
  49. 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,6 +0,0 @@
1
- - Publish or update the current execution plan
2
- - Displays ordered steps and optional note/context to the user
3
-
4
- Usage tips:
5
- - Keep steps concise (imperative verbs) and reflect progress updates
6
- - Update the plan whenever scope changes or new steps emerge
@@ -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