@agi-cli/sdk 0.1.26
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 +675 -0
- package/package.json +73 -0
- package/src/agent/types.ts +19 -0
- package/src/errors.ts +102 -0
- package/src/index.ts +109 -0
- package/src/providers/resolver.ts +84 -0
- package/src/streaming/artifacts.ts +41 -0
- package/src/tools/builtin/bash.ts +73 -0
- package/src/tools/builtin/bash.txt +7 -0
- package/src/tools/builtin/edit.ts +145 -0
- package/src/tools/builtin/edit.txt +7 -0
- package/src/tools/builtin/fileCache.ts +39 -0
- package/src/tools/builtin/finish.ts +13 -0
- package/src/tools/builtin/finish.txt +5 -0
- package/src/tools/builtin/fs/cd.ts +19 -0
- package/src/tools/builtin/fs/cd.txt +5 -0
- package/src/tools/builtin/fs/index.ts +20 -0
- package/src/tools/builtin/fs/ls.ts +57 -0
- package/src/tools/builtin/fs/ls.txt +8 -0
- package/src/tools/builtin/fs/pwd.ts +17 -0
- package/src/tools/builtin/fs/pwd.txt +5 -0
- package/src/tools/builtin/fs/read.ts +49 -0
- package/src/tools/builtin/fs/read.txt +8 -0
- package/src/tools/builtin/fs/tree.ts +67 -0
- package/src/tools/builtin/fs/tree.txt +8 -0
- package/src/tools/builtin/fs/util.ts +95 -0
- package/src/tools/builtin/fs/write.ts +61 -0
- package/src/tools/builtin/fs/write.txt +8 -0
- package/src/tools/builtin/git.commit.txt +6 -0
- package/src/tools/builtin/git.diff.txt +5 -0
- package/src/tools/builtin/git.status.txt +5 -0
- package/src/tools/builtin/git.ts +96 -0
- package/src/tools/builtin/glob.ts +82 -0
- package/src/tools/builtin/glob.txt +8 -0
- package/src/tools/builtin/grep.ts +138 -0
- package/src/tools/builtin/grep.txt +9 -0
- package/src/tools/builtin/ignore.ts +45 -0
- package/src/tools/builtin/patch.ts +273 -0
- package/src/tools/builtin/patch.txt +7 -0
- package/src/tools/builtin/plan.ts +58 -0
- package/src/tools/builtin/plan.txt +6 -0
- package/src/tools/builtin/progress.ts +55 -0
- package/src/tools/builtin/progress.txt +7 -0
- package/src/tools/builtin/ripgrep.ts +71 -0
- package/src/tools/builtin/ripgrep.txt +7 -0
- package/src/tools/loader.ts +383 -0
- package/src/types/index.ts +11 -0
- package/src/types/types.ts +4 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { tool, type Tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { finishTool } from './builtin/finish.ts';
|
|
4
|
+
import { buildFsTools } from './builtin/fs/index.ts';
|
|
5
|
+
import { buildGitTools } from './builtin/git.ts';
|
|
6
|
+
import { progressUpdateTool } from './builtin/progress.ts';
|
|
7
|
+
import { buildBashTool } from './builtin/bash.ts';
|
|
8
|
+
import { buildRipgrepTool } from './builtin/ripgrep.ts';
|
|
9
|
+
import { buildGrepTool } from './builtin/grep.ts';
|
|
10
|
+
import { buildGlobTool } from './builtin/glob.ts';
|
|
11
|
+
import { buildApplyPatchTool } from './builtin/patch.ts';
|
|
12
|
+
import { updatePlanTool } from './builtin/plan.ts';
|
|
13
|
+
import { editTool } from './builtin/edit.ts';
|
|
14
|
+
import { Glob } from 'bun';
|
|
15
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
16
|
+
import { pathToFileURL } from 'node:url';
|
|
17
|
+
import { promises as fs } from 'node:fs';
|
|
18
|
+
|
|
19
|
+
export type DiscoveredTool = { name: string; tool: Tool };
|
|
20
|
+
|
|
21
|
+
type PluginParameter = {
|
|
22
|
+
type: 'string' | 'number' | 'boolean';
|
|
23
|
+
description?: string;
|
|
24
|
+
default?: string | number | boolean;
|
|
25
|
+
enum?: string[];
|
|
26
|
+
optional?: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type PluginDescriptor = {
|
|
30
|
+
name?: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
parameters?: Record<string, PluginParameter>;
|
|
33
|
+
execute?: PluginExecutor;
|
|
34
|
+
run?: PluginExecutor;
|
|
35
|
+
handler?: PluginExecutor;
|
|
36
|
+
setup?: (context: PluginContext) => unknown | Promise<unknown>;
|
|
37
|
+
onInit?: (context: PluginContext) => unknown | Promise<unknown>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type PluginExecutor = (args: PluginExecuteArgs) => unknown | Promise<unknown>;
|
|
41
|
+
|
|
42
|
+
type PluginExecuteArgs = {
|
|
43
|
+
input: Record<string, unknown>;
|
|
44
|
+
project: string;
|
|
45
|
+
projectRoot: string;
|
|
46
|
+
directory: string;
|
|
47
|
+
worktree: string;
|
|
48
|
+
exec: ExecFn;
|
|
49
|
+
run: ExecFn;
|
|
50
|
+
$: TemplateExecFn;
|
|
51
|
+
fs: FsHelpers;
|
|
52
|
+
env: Record<string, string>;
|
|
53
|
+
context: PluginContext;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type PluginContext = {
|
|
57
|
+
project: string;
|
|
58
|
+
projectRoot: string;
|
|
59
|
+
directory: string;
|
|
60
|
+
worktree: string;
|
|
61
|
+
toolDir: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type ExecFn = (
|
|
65
|
+
command: string,
|
|
66
|
+
args?: string[] | ExecOptions,
|
|
67
|
+
options?: ExecOptions,
|
|
68
|
+
) => Promise<ExecResult>;
|
|
69
|
+
|
|
70
|
+
type TemplateExecFn = (
|
|
71
|
+
strings: TemplateStringsArray,
|
|
72
|
+
...values: unknown[]
|
|
73
|
+
) => Promise<ExecResult>;
|
|
74
|
+
|
|
75
|
+
type ExecOptions = {
|
|
76
|
+
cwd?: string;
|
|
77
|
+
env?: Record<string, string>;
|
|
78
|
+
allowNonZeroExit?: boolean;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type ExecResult = { exitCode: number; stdout: string; stderr: string };
|
|
82
|
+
|
|
83
|
+
type FsHelpers = {
|
|
84
|
+
readFile: (path: string, encoding?: BufferEncoding) => Promise<string>;
|
|
85
|
+
writeFile: (path: string, content: string) => Promise<void>;
|
|
86
|
+
exists: (path: string) => Promise<boolean>;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const pluginPatterns = ['tools/*/tool.js', 'tools/*/tool.mjs'];
|
|
90
|
+
|
|
91
|
+
export async function discoverProjectTools(
|
|
92
|
+
projectRoot: string,
|
|
93
|
+
globalConfigDir?: string,
|
|
94
|
+
): Promise<DiscoveredTool[]> {
|
|
95
|
+
const tools = new Map<string, Tool>();
|
|
96
|
+
for (const { name, tool } of buildFsTools(projectRoot)) tools.set(name, tool);
|
|
97
|
+
for (const { name, tool } of buildGitTools(projectRoot))
|
|
98
|
+
tools.set(name, tool);
|
|
99
|
+
// Built-ins
|
|
100
|
+
tools.set('finish', finishTool);
|
|
101
|
+
tools.set('progress_update', progressUpdateTool);
|
|
102
|
+
const bash = buildBashTool(projectRoot);
|
|
103
|
+
tools.set(bash.name, bash.tool);
|
|
104
|
+
// Search
|
|
105
|
+
const rg = buildRipgrepTool(projectRoot);
|
|
106
|
+
tools.set(rg.name, rg.tool);
|
|
107
|
+
const grep = buildGrepTool(projectRoot);
|
|
108
|
+
tools.set(grep.name, grep.tool);
|
|
109
|
+
const glob = buildGlobTool(projectRoot);
|
|
110
|
+
tools.set(glob.name, glob.tool);
|
|
111
|
+
// Patch/apply
|
|
112
|
+
const ap = buildApplyPatchTool(projectRoot);
|
|
113
|
+
tools.set(ap.name, ap.tool);
|
|
114
|
+
// Plan update
|
|
115
|
+
tools.set('update_plan', updatePlanTool);
|
|
116
|
+
// Edit
|
|
117
|
+
tools.set('edit', editTool);
|
|
118
|
+
|
|
119
|
+
async function loadFromBase(base: string | null | undefined) {
|
|
120
|
+
if (!base) return;
|
|
121
|
+
try {
|
|
122
|
+
await fs.readdir(base);
|
|
123
|
+
} catch {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
for (const pattern of pluginPatterns) {
|
|
127
|
+
const glob = new Glob(pattern);
|
|
128
|
+
for await (const rel of glob.scan(base)) {
|
|
129
|
+
const match = rel.match(/^tools\/([^/]+)\/tool\.(m?js)$/);
|
|
130
|
+
if (!match || !match[1]) continue;
|
|
131
|
+
const folder = match[1];
|
|
132
|
+
const absPath = join(base, rel).replace(/\\/g, '/');
|
|
133
|
+
try {
|
|
134
|
+
const plugin = await loadPlugin(absPath, folder, projectRoot);
|
|
135
|
+
if (plugin) tools.set(plugin.name, plugin.tool);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (process.env.AGI_DEBUG_TOOLS === '1')
|
|
138
|
+
console.error('Failed to load tool', absPath, err);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Fallback: manual directory scan
|
|
143
|
+
try {
|
|
144
|
+
const toolsDir = join(base, 'tools');
|
|
145
|
+
const entries = await fs.readdir(toolsDir).catch(() => [] as string[]);
|
|
146
|
+
for (const folder of entries) {
|
|
147
|
+
const js = join(toolsDir, folder, 'tool.js');
|
|
148
|
+
const mjs = join(toolsDir, folder, 'tool.mjs');
|
|
149
|
+
const candidate = await fs
|
|
150
|
+
.stat(js)
|
|
151
|
+
.then(() => js)
|
|
152
|
+
.catch(
|
|
153
|
+
async () =>
|
|
154
|
+
await fs
|
|
155
|
+
.stat(mjs)
|
|
156
|
+
.then(() => mjs)
|
|
157
|
+
.catch(() => null),
|
|
158
|
+
);
|
|
159
|
+
if (!candidate) continue;
|
|
160
|
+
try {
|
|
161
|
+
const plugin = await loadPlugin(
|
|
162
|
+
candidate.replace(/\\/g, '/'),
|
|
163
|
+
folder,
|
|
164
|
+
projectRoot,
|
|
165
|
+
);
|
|
166
|
+
if (plugin) tools.set(plugin.name, plugin.tool);
|
|
167
|
+
} catch {}
|
|
168
|
+
}
|
|
169
|
+
} catch {}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await loadFromBase(globalConfigDir);
|
|
173
|
+
await loadFromBase(join(projectRoot, '.agi'));
|
|
174
|
+
return Array.from(tools.entries()).map(([name, tool]) => ({ name, tool }));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function loadPlugin(
|
|
178
|
+
absPath: string,
|
|
179
|
+
folder: string,
|
|
180
|
+
projectRoot: string,
|
|
181
|
+
): Promise<DiscoveredTool | null> {
|
|
182
|
+
const mod = await import(`${pathToFileURL(absPath).href}?t=${Date.now()}`);
|
|
183
|
+
const candidate = resolveExport(mod);
|
|
184
|
+
if (!candidate) throw new Error('No plugin export found');
|
|
185
|
+
|
|
186
|
+
const context: PluginContext = {
|
|
187
|
+
project: projectRoot,
|
|
188
|
+
projectRoot,
|
|
189
|
+
directory: projectRoot,
|
|
190
|
+
worktree: projectRoot,
|
|
191
|
+
toolDir: absPath.slice(0, absPath.lastIndexOf('/')),
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
let descriptor: PluginDescriptor | null | undefined;
|
|
195
|
+
if (typeof candidate === 'function') descriptor = await candidate(context);
|
|
196
|
+
else descriptor = candidate;
|
|
197
|
+
if (!descriptor || typeof descriptor !== 'object')
|
|
198
|
+
throw new Error('Plugin must return an object descriptor');
|
|
199
|
+
|
|
200
|
+
if (typeof descriptor.setup === 'function') await descriptor.setup(context);
|
|
201
|
+
if (typeof descriptor.onInit === 'function') await descriptor.onInit(context);
|
|
202
|
+
|
|
203
|
+
const name = sanitizeName(descriptor.name ?? folder);
|
|
204
|
+
const description = descriptor.description ?? `Custom tool ${name}`;
|
|
205
|
+
const parameters = descriptor.parameters ?? {};
|
|
206
|
+
const inputSchema = createInputSchema(parameters);
|
|
207
|
+
const executor = resolveExecutor(descriptor);
|
|
208
|
+
|
|
209
|
+
const helpersFactory = createHelpers(projectRoot, context.toolDir);
|
|
210
|
+
|
|
211
|
+
const wrapped = tool({
|
|
212
|
+
description,
|
|
213
|
+
inputSchema,
|
|
214
|
+
async execute(input) {
|
|
215
|
+
const helpers = helpersFactory();
|
|
216
|
+
const result = await executor({
|
|
217
|
+
input: input as Record<string, unknown>,
|
|
218
|
+
project: helpers.context.project,
|
|
219
|
+
projectRoot: helpers.context.projectRoot,
|
|
220
|
+
directory: helpers.context.directory,
|
|
221
|
+
worktree: helpers.context.worktree,
|
|
222
|
+
exec: helpers.exec,
|
|
223
|
+
run: helpers.exec,
|
|
224
|
+
$: helpers.templateExec,
|
|
225
|
+
fs: helpers.fs,
|
|
226
|
+
env: helpers.env,
|
|
227
|
+
context: helpers.context,
|
|
228
|
+
});
|
|
229
|
+
return result ?? { ok: true };
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return { name, tool: wrapped };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function resolveExport(mod: Record<string, unknown>) {
|
|
237
|
+
if (mod.default) return mod.default;
|
|
238
|
+
if (mod.tool) return mod.tool;
|
|
239
|
+
if (mod.plugin) return mod.plugin;
|
|
240
|
+
if (mod.Tool) return mod.Tool;
|
|
241
|
+
const values = Object.values(mod);
|
|
242
|
+
return values.find(
|
|
243
|
+
(value) => typeof value === 'function' || typeof value === 'object',
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function resolveExecutor(descriptor: PluginDescriptor): PluginExecutor {
|
|
248
|
+
const fn = descriptor.execute ?? descriptor.run ?? descriptor.handler;
|
|
249
|
+
if (typeof fn !== 'function')
|
|
250
|
+
throw new Error('Plugin must provide an execute/run/handler function');
|
|
251
|
+
return fn;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function sanitizeName(name: string) {
|
|
255
|
+
const cleaned = name.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 128);
|
|
256
|
+
return cleaned || 'tool';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function createInputSchema(parameters: Record<string, PluginParameter>) {
|
|
260
|
+
const shape: Record<string, z.ZodTypeAny> = {};
|
|
261
|
+
for (const [key, def] of Object.entries(parameters)) {
|
|
262
|
+
let schema: z.ZodTypeAny;
|
|
263
|
+
if (def.type === 'string') {
|
|
264
|
+
const values = def.enum;
|
|
265
|
+
schema = values?.length
|
|
266
|
+
? z.enum(values as [string, ...string[]])
|
|
267
|
+
: z.string();
|
|
268
|
+
} else if (def.type === 'number') schema = z.number();
|
|
269
|
+
else schema = z.boolean();
|
|
270
|
+
if (def.description) schema = schema.describe(def.description);
|
|
271
|
+
if (def.default !== undefined)
|
|
272
|
+
schema = schema.default(def.default as never);
|
|
273
|
+
else if (def.optional) schema = schema.optional();
|
|
274
|
+
shape[key] = schema;
|
|
275
|
+
}
|
|
276
|
+
return Object.keys(shape).length ? z.object(shape).strict() : z.object({});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function createHelpers(projectRoot: string, toolDir: string) {
|
|
280
|
+
return () => {
|
|
281
|
+
const exec = createExec(projectRoot);
|
|
282
|
+
const fsHelpers = createFsHelpers(projectRoot);
|
|
283
|
+
const context: PluginContext = {
|
|
284
|
+
project: projectRoot,
|
|
285
|
+
projectRoot,
|
|
286
|
+
directory: projectRoot,
|
|
287
|
+
worktree: projectRoot,
|
|
288
|
+
toolDir,
|
|
289
|
+
};
|
|
290
|
+
const env: Record<string, string> = {};
|
|
291
|
+
for (const [key, value] of Object.entries(process.env))
|
|
292
|
+
if (typeof value === 'string') env[key] = value;
|
|
293
|
+
const templateExec: TemplateExecFn = (strings, ...values) => {
|
|
294
|
+
const commandLine = strings.reduce((acc, part, index) => {
|
|
295
|
+
const value = index < values.length ? String(values[index]) : '';
|
|
296
|
+
return acc + part + value;
|
|
297
|
+
}, '');
|
|
298
|
+
const pieces = commandLine.trim().split(/\s+/).filter(Boolean);
|
|
299
|
+
if (pieces.length === 0)
|
|
300
|
+
throw new Error('Empty command passed to template executor');
|
|
301
|
+
return exec(pieces[0]!, pieces.slice(1));
|
|
302
|
+
};
|
|
303
|
+
return {
|
|
304
|
+
exec,
|
|
305
|
+
fs: fsHelpers,
|
|
306
|
+
env,
|
|
307
|
+
templateExec,
|
|
308
|
+
context,
|
|
309
|
+
};
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function createExec(projectRoot: string): ExecFn {
|
|
314
|
+
return async (
|
|
315
|
+
command: string,
|
|
316
|
+
argsOrOptions?: string[] | ExecOptions,
|
|
317
|
+
maybeOptions?: ExecOptions,
|
|
318
|
+
) => {
|
|
319
|
+
let args: string[] = [];
|
|
320
|
+
let options: ExecOptions = {};
|
|
321
|
+
if (Array.isArray(argsOrOptions)) {
|
|
322
|
+
args = argsOrOptions;
|
|
323
|
+
options = maybeOptions ?? {};
|
|
324
|
+
} else if (argsOrOptions) options = argsOrOptions;
|
|
325
|
+
|
|
326
|
+
const cwd = options.cwd
|
|
327
|
+
? resolveWithinProject(projectRoot, options.cwd)
|
|
328
|
+
: projectRoot;
|
|
329
|
+
const env: Record<string, string> = {};
|
|
330
|
+
for (const [key, value] of Object.entries(process.env))
|
|
331
|
+
if (typeof value === 'string') env[key] = value;
|
|
332
|
+
if (options.env)
|
|
333
|
+
for (const [key, value] of Object.entries(options.env)) env[key] = value;
|
|
334
|
+
|
|
335
|
+
const proc = Bun.spawn([command, ...args], {
|
|
336
|
+
cwd,
|
|
337
|
+
env,
|
|
338
|
+
stdout: 'pipe',
|
|
339
|
+
stderr: 'pipe',
|
|
340
|
+
});
|
|
341
|
+
const exitCode = await proc.exited;
|
|
342
|
+
const stdout = await new Response(proc.stdout).text();
|
|
343
|
+
const stderr = await new Response(proc.stderr).text();
|
|
344
|
+
if (exitCode !== 0 && !options.allowNonZeroExit) {
|
|
345
|
+
const message = stderr.trim() || stdout.trim() || `${command} failed`;
|
|
346
|
+
throw new Error(`${command} exited with code ${exitCode}: ${message}`);
|
|
347
|
+
}
|
|
348
|
+
return { exitCode, stdout, stderr };
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function createFsHelpers(projectRoot: string): FsHelpers {
|
|
353
|
+
return {
|
|
354
|
+
async readFile(path: string, encoding: BufferEncoding = 'utf-8') {
|
|
355
|
+
const abs = resolveWithinProject(projectRoot, path);
|
|
356
|
+
return fs.readFile(abs, { encoding });
|
|
357
|
+
},
|
|
358
|
+
async writeFile(path: string, content: string) {
|
|
359
|
+
const abs = resolveWithinProject(projectRoot, path);
|
|
360
|
+
await fs.mkdir(dirname(abs), { recursive: true });
|
|
361
|
+
await fs.writeFile(abs, content, 'utf-8');
|
|
362
|
+
},
|
|
363
|
+
async exists(path: string) {
|
|
364
|
+
const abs = resolveWithinProject(projectRoot, path);
|
|
365
|
+
try {
|
|
366
|
+
await fs.access(abs);
|
|
367
|
+
return true;
|
|
368
|
+
} catch {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function resolveWithinProject(projectRoot: string, target: string) {
|
|
376
|
+
if (!target) return projectRoot;
|
|
377
|
+
if (target.startsWith('~/')) {
|
|
378
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
379
|
+
return join(home, target.slice(2));
|
|
380
|
+
}
|
|
381
|
+
if (isAbsolute(target)) return target;
|
|
382
|
+
return join(projectRoot, target);
|
|
383
|
+
}
|