@agi-cli/sdk 0.1.57 → 0.1.59
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 +1 -1
- package/src/core/src/tools/builtin/bash.ts +38 -7
- package/src/core/src/tools/builtin/glob.ts +116 -0
- package/src/core/src/tools/builtin/glob.txt +10 -0
- package/src/core/src/tools/builtin/grep.ts +8 -45
- package/src/core/src/tools/builtin/patch.ts +48 -7
- package/src/core/src/tools/loader.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { tool, type Tool } from 'ai';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
4
|
import DESCRIPTION from './bash.txt' with { type: 'text' };
|
|
5
5
|
|
|
6
6
|
function normalizePath(p: string) {
|
|
@@ -31,7 +31,7 @@ export function buildBashTool(projectRoot: string): {
|
|
|
31
31
|
description: DESCRIPTION,
|
|
32
32
|
inputSchema: z
|
|
33
33
|
.object({
|
|
34
|
-
cmd: z.string().describe('Shell command to run (bash -
|
|
34
|
+
cmd: z.string().describe('Shell command to run (bash -c <cmd>)'),
|
|
35
35
|
cwd: z
|
|
36
36
|
.string()
|
|
37
37
|
.default('.')
|
|
@@ -41,27 +41,45 @@ export function buildBashTool(projectRoot: string): {
|
|
|
41
41
|
.optional()
|
|
42
42
|
.default(false)
|
|
43
43
|
.describe('If true, do not throw on non-zero exit'),
|
|
44
|
+
timeout: z
|
|
45
|
+
.number()
|
|
46
|
+
.optional()
|
|
47
|
+
.default(300000)
|
|
48
|
+
.describe('Timeout in milliseconds (default: 300000 = 5 minutes)'),
|
|
44
49
|
})
|
|
45
50
|
.strict(),
|
|
46
51
|
async execute({
|
|
47
52
|
cmd,
|
|
48
53
|
cwd,
|
|
49
54
|
allowNonZeroExit,
|
|
55
|
+
timeout = 300000,
|
|
50
56
|
}: {
|
|
51
57
|
cmd: string;
|
|
52
58
|
cwd?: string;
|
|
53
59
|
allowNonZeroExit?: boolean;
|
|
60
|
+
timeout?: number;
|
|
54
61
|
}) {
|
|
55
62
|
const absCwd = resolveSafePath(projectRoot, cwd || '.');
|
|
56
63
|
|
|
57
64
|
return new Promise((resolve, reject) => {
|
|
58
|
-
|
|
65
|
+
// Use spawn with shell: true for cross-platform compatibility
|
|
66
|
+
const proc = spawn(cmd, {
|
|
59
67
|
cwd: absCwd,
|
|
60
|
-
|
|
68
|
+
shell: true,
|
|
69
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
61
70
|
});
|
|
62
71
|
|
|
63
72
|
let stdout = '';
|
|
64
73
|
let stderr = '';
|
|
74
|
+
let didTimeout = false;
|
|
75
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
76
|
+
|
|
77
|
+
if (timeout > 0) {
|
|
78
|
+
timeoutId = setTimeout(() => {
|
|
79
|
+
didTimeout = true;
|
|
80
|
+
proc.kill();
|
|
81
|
+
}, timeout);
|
|
82
|
+
}
|
|
65
83
|
|
|
66
84
|
proc.stdout?.on('data', (chunk) => {
|
|
67
85
|
stdout += chunk.toString();
|
|
@@ -72,8 +90,16 @@ export function buildBashTool(projectRoot: string): {
|
|
|
72
90
|
});
|
|
73
91
|
|
|
74
92
|
proc.on('close', (exitCode) => {
|
|
75
|
-
if (
|
|
76
|
-
|
|
93
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
94
|
+
|
|
95
|
+
if (didTimeout) {
|
|
96
|
+
reject(new Error(`Command timed out after ${timeout}ms: ${cmd}`));
|
|
97
|
+
} else if (exitCode !== 0 && !allowNonZeroExit) {
|
|
98
|
+
const errorMsg =
|
|
99
|
+
stderr.trim() ||
|
|
100
|
+
stdout.trim() ||
|
|
101
|
+
`Command failed with exit code ${exitCode}`;
|
|
102
|
+
const msg = `${errorMsg}\n\nCommand: ${cmd}\nExit code: ${exitCode}`;
|
|
77
103
|
reject(new Error(msg));
|
|
78
104
|
} else {
|
|
79
105
|
resolve({ exitCode: exitCode ?? 0, stdout, stderr });
|
|
@@ -81,7 +107,12 @@ export function buildBashTool(projectRoot: string): {
|
|
|
81
107
|
});
|
|
82
108
|
|
|
83
109
|
proc.on('error', (err) => {
|
|
84
|
-
|
|
110
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
111
|
+
reject(
|
|
112
|
+
new Error(
|
|
113
|
+
`Command execution failed: ${err.message}\n\nCommand: ${cmd}`,
|
|
114
|
+
),
|
|
115
|
+
);
|
|
85
116
|
});
|
|
86
117
|
});
|
|
87
118
|
},
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { tool, type Tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { stat } from 'node:fs/promises';
|
|
6
|
+
import DESCRIPTION from './glob.txt' with { type: 'text' };
|
|
7
|
+
import { defaultIgnoreGlobs } from './ignore.ts';
|
|
8
|
+
|
|
9
|
+
function expandTilde(p: string) {
|
|
10
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
11
|
+
if (!home) return p;
|
|
12
|
+
if (p === '~') return home;
|
|
13
|
+
if (p.startsWith('~/')) return `${home}/${p.slice(2)}`;
|
|
14
|
+
return p;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildGlobTool(projectRoot: string): {
|
|
18
|
+
name: string;
|
|
19
|
+
tool: Tool;
|
|
20
|
+
} {
|
|
21
|
+
const globTool = tool({
|
|
22
|
+
description: DESCRIPTION,
|
|
23
|
+
inputSchema: z.object({
|
|
24
|
+
pattern: z
|
|
25
|
+
.string()
|
|
26
|
+
.min(1)
|
|
27
|
+
.describe(
|
|
28
|
+
'Glob pattern to match files (e.g., "*.ts", "**/*.tsx", "src/**/*.{js,ts}")',
|
|
29
|
+
),
|
|
30
|
+
path: z
|
|
31
|
+
.string()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('Directory to search in (default: project root)'),
|
|
34
|
+
ignore: z
|
|
35
|
+
.array(z.string())
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('Additional glob patterns to exclude'),
|
|
38
|
+
limit: z
|
|
39
|
+
.number()
|
|
40
|
+
.int()
|
|
41
|
+
.min(1)
|
|
42
|
+
.max(1000)
|
|
43
|
+
.optional()
|
|
44
|
+
.default(100)
|
|
45
|
+
.describe('Maximum number of files to return'),
|
|
46
|
+
}),
|
|
47
|
+
async execute({
|
|
48
|
+
pattern,
|
|
49
|
+
path = '.',
|
|
50
|
+
ignore,
|
|
51
|
+
limit = 100,
|
|
52
|
+
}: {
|
|
53
|
+
pattern: string;
|
|
54
|
+
path?: string;
|
|
55
|
+
ignore?: string[];
|
|
56
|
+
limit?: number;
|
|
57
|
+
}) {
|
|
58
|
+
const p = expandTilde(String(path || '.')).trim();
|
|
59
|
+
const isAbs = p.startsWith('/') || /^[A-Za-z]:[\\/]/.test(p);
|
|
60
|
+
const searchPath = p ? (isAbs ? p : join(projectRoot, p)) : projectRoot;
|
|
61
|
+
|
|
62
|
+
// Build ignore patterns
|
|
63
|
+
const ignorePatterns = defaultIgnoreGlobs(ignore);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
// Use fast-glob to find matching files
|
|
67
|
+
const files = await fg(pattern, {
|
|
68
|
+
cwd: searchPath,
|
|
69
|
+
ignore: ignorePatterns,
|
|
70
|
+
onlyFiles: true,
|
|
71
|
+
absolute: false,
|
|
72
|
+
dot: false,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Get file stats for sorting by modification time
|
|
76
|
+
const filesWithStats = await Promise.all(
|
|
77
|
+
files.map(async (file) => {
|
|
78
|
+
const fullPath = join(searchPath, file);
|
|
79
|
+
try {
|
|
80
|
+
const stats = await stat(fullPath);
|
|
81
|
+
return {
|
|
82
|
+
file,
|
|
83
|
+
mtime: stats.mtime.getTime(),
|
|
84
|
+
};
|
|
85
|
+
} catch {
|
|
86
|
+
return {
|
|
87
|
+
file,
|
|
88
|
+
mtime: 0,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Sort by modification time (most recent first) and limit
|
|
95
|
+
filesWithStats.sort((a, b) => b.mtime - a.mtime);
|
|
96
|
+
const limitedFiles = filesWithStats.slice(0, limit).map((f) => f.file);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
count: limitedFiles.length,
|
|
100
|
+
total: files.length,
|
|
101
|
+
files: limitedFiles,
|
|
102
|
+
truncated: files.length > limit,
|
|
103
|
+
};
|
|
104
|
+
} catch (error: unknown) {
|
|
105
|
+
const err = error as { message?: string };
|
|
106
|
+
return {
|
|
107
|
+
count: 0,
|
|
108
|
+
total: 0,
|
|
109
|
+
files: [],
|
|
110
|
+
error: `Glob search failed: ${err.message || String(error)}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
return { name: 'glob', tool: globTool };
|
|
116
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
- Find files matching glob patterns (e.g., "*.ts", "**/*.tsx", "src/**/*.{js,ts}")
|
|
2
|
+
- Returns a list of matching file paths relative to the search directory
|
|
3
|
+
- Supports standard glob syntax: * (any chars), ** (any dirs), {a,b} (alternatives), [abc] (character sets)
|
|
4
|
+
- Automatically excludes common build/cache folders (node_modules, dist, .git, etc.)
|
|
5
|
+
- Results sorted by modification time (most recent first)
|
|
6
|
+
|
|
7
|
+
Usage tips:
|
|
8
|
+
- Use this tool to find files by name or extension patterns
|
|
9
|
+
- Use Grep or Ripgrep tools to search file contents
|
|
10
|
+
- Combine with path parameter to search in specific directories only
|
|
@@ -3,7 +3,6 @@ import { z } from 'zod';
|
|
|
3
3
|
import { exec } from 'node:child_process';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
-
import { stat } from 'node:fs/promises';
|
|
7
6
|
import DESCRIPTION from './grep.txt' with { type: 'text' };
|
|
8
7
|
import { defaultIgnoreGlobs } from './ignore.ts';
|
|
9
8
|
|
|
@@ -64,11 +63,7 @@ export function buildGrepTool(projectRoot: string): {
|
|
|
64
63
|
} catch (error: unknown) {
|
|
65
64
|
const err = error as { code?: number; stderr?: string };
|
|
66
65
|
if (err.code === 1) {
|
|
67
|
-
return {
|
|
68
|
-
title: pattern,
|
|
69
|
-
metadata: { matches: 0, truncated: false },
|
|
70
|
-
output: 'No files found',
|
|
71
|
-
};
|
|
66
|
+
return { count: 0, matches: [] };
|
|
72
67
|
}
|
|
73
68
|
const err2 = error as { stderr?: string; message?: string };
|
|
74
69
|
throw new Error(`ripgrep failed: ${err2.stderr || err2.message}`);
|
|
@@ -76,10 +71,9 @@ export function buildGrepTool(projectRoot: string): {
|
|
|
76
71
|
|
|
77
72
|
const lines = output.trim().split('\n');
|
|
78
73
|
const matches: Array<{
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
lineText: string;
|
|
74
|
+
file: string;
|
|
75
|
+
line: number;
|
|
76
|
+
text: string;
|
|
83
77
|
}> = [];
|
|
84
78
|
|
|
85
79
|
for (const line of lines) {
|
|
@@ -92,47 +86,16 @@ export function buildGrepTool(projectRoot: string): {
|
|
|
92
86
|
const lineText = line.slice(idx2 + 1);
|
|
93
87
|
const lineNum = parseInt(lineNumStr, 10);
|
|
94
88
|
if (!filePath || !Number.isFinite(lineNum)) continue;
|
|
95
|
-
|
|
96
|
-
.then((s) => s.mtime.getTime())
|
|
97
|
-
.catch(() => 0);
|
|
98
|
-
matches.push({ path: filePath, modTime: stats, lineNum, lineText });
|
|
89
|
+
matches.push({ file: filePath, line: lineNum, text: lineText });
|
|
99
90
|
}
|
|
100
91
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const limit = 100;
|
|
92
|
+
const limit = 500;
|
|
104
93
|
const truncated = matches.length > limit;
|
|
105
94
|
const finalMatches = truncated ? matches.slice(0, limit) : matches;
|
|
106
95
|
|
|
107
|
-
if (finalMatches.length === 0) {
|
|
108
|
-
return {
|
|
109
|
-
title: pattern,
|
|
110
|
-
metadata: { matches: 0, truncated: false },
|
|
111
|
-
output: 'No files found',
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const outputLines = [`Found ${finalMatches.length} matches`];
|
|
116
|
-
let currentFile = '';
|
|
117
|
-
for (const match of finalMatches) {
|
|
118
|
-
if (currentFile !== match.path) {
|
|
119
|
-
if (currentFile !== '') outputLines.push('');
|
|
120
|
-
currentFile = match.path;
|
|
121
|
-
outputLines.push(`${match.path}:`);
|
|
122
|
-
}
|
|
123
|
-
outputLines.push(` Line ${match.lineNum}: ${match.lineText}`);
|
|
124
|
-
}
|
|
125
|
-
if (truncated) {
|
|
126
|
-
outputLines.push('');
|
|
127
|
-
outputLines.push(
|
|
128
|
-
'(Results are truncated. Consider using a more specific path or pattern.)',
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
96
|
return {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
output: outputLines.join('\n'),
|
|
97
|
+
count: finalMatches.length,
|
|
98
|
+
matches: finalMatches,
|
|
136
99
|
};
|
|
137
100
|
},
|
|
138
101
|
});
|
|
@@ -70,16 +70,57 @@ async function applyEnvelopedPatch(projectRoot: string, patch: string) {
|
|
|
70
70
|
if (oldLines.length > 0) {
|
|
71
71
|
const oldText = oldLines.join('\n');
|
|
72
72
|
const newText = newLines.join('\n');
|
|
73
|
+
|
|
74
|
+
// Try exact match first
|
|
73
75
|
if (existingContent.includes(oldText)) {
|
|
74
76
|
newContent = existingContent.replace(oldText, newText);
|
|
75
77
|
} else {
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
// Try normalizing whitespace for more flexible matching
|
|
79
|
+
const normalizeWhitespace = (s: string) =>
|
|
80
|
+
s.replace(/\s+/g, ' ').trim();
|
|
81
|
+
const normalizedOld = normalizeWhitespace(oldText);
|
|
82
|
+
const normalizedExisting = normalizeWhitespace(existingContent);
|
|
83
|
+
|
|
84
|
+
if (normalizedExisting.includes(normalizedOld)) {
|
|
85
|
+
// Find the actual text in the file by matching normalized version
|
|
86
|
+
const lines = existingContent.split('\n');
|
|
87
|
+
let found = false;
|
|
88
|
+
|
|
89
|
+
for (let i = 0; i < lines.length; i++) {
|
|
90
|
+
const candidate = lines
|
|
91
|
+
.slice(i, i + oldLines.length)
|
|
92
|
+
.join('\n');
|
|
93
|
+
if (normalizeWhitespace(candidate) === normalizedOld) {
|
|
94
|
+
// Replace this section
|
|
95
|
+
const before = lines.slice(0, i).join('\n');
|
|
96
|
+
const after = lines.slice(i + oldLines.length).join('\n');
|
|
97
|
+
newContent =
|
|
98
|
+
before +
|
|
99
|
+
(before ? '\n' : '') +
|
|
100
|
+
newText +
|
|
101
|
+
(after ? '\n' : '') +
|
|
102
|
+
after;
|
|
103
|
+
found = true;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!found) {
|
|
109
|
+
const preview = oldText.substring(0, 100);
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
error: `Cannot find exact location to replace in ${currentFile}. Looking for: "${preview}${oldText.length > 100 ? '...' : ''}"`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
// Can't find even with normalized whitespace
|
|
117
|
+
const preview = oldText.substring(0, 100);
|
|
118
|
+
const filePreview = existingContent.substring(0, 200);
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
error: `Cannot find content to replace in ${currentFile}.\nLooking for: "${preview}${oldText.length > 100 ? '...' : ''}"\nFile contains: "${filePreview}${existingContent.length > 200 ? '...' : ''}"`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
83
124
|
}
|
|
84
125
|
} else if (newLines.length > 0) {
|
|
85
126
|
// Just appending new lines
|
|
@@ -7,6 +7,7 @@ import { progressUpdateTool } from './builtin/progress.ts';
|
|
|
7
7
|
import { buildBashTool } from './builtin/bash.ts';
|
|
8
8
|
import { buildRipgrepTool } from './builtin/ripgrep.ts';
|
|
9
9
|
import { buildGrepTool } from './builtin/grep.ts';
|
|
10
|
+
import { buildGlobTool } from './builtin/glob.ts';
|
|
10
11
|
import { buildApplyPatchTool } from './builtin/patch.ts';
|
|
11
12
|
import { updatePlanTool } from './builtin/plan.ts';
|
|
12
13
|
import { editTool } from './builtin/edit.ts';
|
|
@@ -107,6 +108,8 @@ export async function discoverProjectTools(
|
|
|
107
108
|
tools.set(rg.name, rg.tool);
|
|
108
109
|
const grep = buildGrepTool(projectRoot);
|
|
109
110
|
tools.set(grep.name, grep.tool);
|
|
111
|
+
const glob = buildGlobTool(projectRoot);
|
|
112
|
+
tools.set(glob.name, glob.tool);
|
|
110
113
|
// Patch/apply
|
|
111
114
|
const ap = buildApplyPatchTool(projectRoot);
|
|
112
115
|
tools.set(ap.name, ap.tool);
|