@agi-cli/sdk 0.1.110 → 0.1.111

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agi-cli/sdk",
3
- "version": "0.1.110",
3
+ "version": "0.1.111",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "ntishxyz",
6
6
  "license": "MIT",
@@ -3,6 +3,7 @@ import { z } from 'zod';
3
3
  import { readFile, writeFile, access } from 'node:fs/promises';
4
4
  import { constants } from 'node:fs';
5
5
  import DESCRIPTION from './edit.txt' with { type: 'text' };
6
+ import { createToolError, type ToolResponse } from '../error.ts';
6
7
 
7
8
  const replaceOp = z.object({
8
9
  type: z.literal('replace'),
@@ -54,7 +55,9 @@ export const editTool: Tool = tool({
54
55
  path: string;
55
56
  ops: z.infer<typeof opSchema>[];
56
57
  create?: boolean;
57
- }) {
58
+ }): Promise<
59
+ ToolResponse<{ path: string; opsApplied: number; bytes: number }>
60
+ > {
58
61
  let exists = false;
59
62
  try {
60
63
  await access(path, constants.F_OK);
@@ -62,7 +65,13 @@ export const editTool: Tool = tool({
62
65
  } catch {}
63
66
 
64
67
  if (!exists) {
65
- if (!create) throw new Error(`File not found: ${path}`);
68
+ if (!create) {
69
+ return createToolError(`File not found: ${path}`, 'not_found', {
70
+ parameter: 'path',
71
+ value: path,
72
+ suggestion: 'Set create: true to create a new file',
73
+ });
74
+ }
66
75
  await writeFile(path, '');
67
76
  }
68
77
  let text = await readFile(path, 'utf-8');
@@ -120,8 +129,16 @@ export const editTool: Tool = tool({
120
129
  applied += 1;
121
130
  continue;
122
131
  }
123
- if (!op.pattern)
124
- throw new Error('insert requires pattern for before/after');
132
+ if (!op.pattern) {
133
+ return createToolError(
134
+ 'insert requires pattern for before/after',
135
+ 'validation',
136
+ {
137
+ parameter: 'pattern',
138
+ suggestion: 'Provide a pattern to anchor the insertion',
139
+ },
140
+ );
141
+ }
125
142
  const idx = text.indexOf(op.pattern);
126
143
  if (idx === -1) continue;
127
144
  if (op.position === 'before')
@@ -147,6 +164,6 @@ export const editTool: Tool = tool({
147
164
  }
148
165
 
149
166
  await writeFile(path, text);
150
- return { path, opsApplied: applied, bytes: text.length };
167
+ return { ok: true, path, opsApplied: applied, bytes: text.length };
151
168
  },
152
169
  });
@@ -1,11 +1,12 @@
1
1
  import { z } from 'zod';
2
2
  import { tool } from 'ai';
3
3
  import DESCRIPTION from './finish.txt' with { type: 'text' };
4
+ import type { ToolResponse } from '../error.ts';
4
5
 
5
6
  export const finishTool = tool({
6
7
  description: DESCRIPTION,
7
8
  inputSchema: z.object({}),
8
- async execute() {
9
- return { done: true } as const;
9
+ async execute(): Promise<ToolResponse<{ done: true }>> {
10
+ return { ok: true, done: true };
10
11
  },
11
12
  });
@@ -5,6 +5,7 @@ import { promisify } from 'node:util';
5
5
  import { expandTilde, isAbsoluteLike, resolveSafePath } from './util.ts';
6
6
  import DESCRIPTION from './ls.txt' with { type: 'text' };
7
7
  import { toIgnoredBasenames } from '../ignore.ts';
8
+ import { createToolError, type ToolResponse } from '../../error.ts';
8
9
 
9
10
  const execAsync = promisify(exec);
10
11
 
@@ -25,7 +26,18 @@ export function buildLsTool(projectRoot: string): { name: string; tool: Tool } {
25
26
  .optional()
26
27
  .describe('List of directory names/globs to ignore'),
27
28
  }),
28
- async execute({ path, ignore }: { path: string; ignore?: string[] }) {
29
+ async execute({
30
+ path,
31
+ ignore,
32
+ }: {
33
+ path: string;
34
+ ignore?: string[];
35
+ }): Promise<
36
+ ToolResponse<{
37
+ path: string;
38
+ entries: Array<{ name: string; type: string }>;
39
+ }>
40
+ > {
29
41
  const req = expandTilde(path || '.');
30
42
  const abs = isAbsoluteLike(req)
31
43
  ? req
@@ -48,11 +60,19 @@ export function buildLsTool(projectRoot: string): { name: string; tool: Tool } {
48
60
  .filter(
49
61
  (entry) => !(entry.type === 'dir' && ignored.has(entry.name)),
50
62
  );
51
- return { path: req, entries };
63
+ return { ok: true, path: req, entries };
52
64
  } catch (error: unknown) {
53
65
  const err = error as { stderr?: string; stdout?: string };
54
66
  const message = (err.stderr || err.stdout || 'ls failed').trim();
55
- throw new Error(`ls failed for ${req}: ${message}`);
67
+ return createToolError(
68
+ `ls failed for ${req}: ${message}`,
69
+ 'execution',
70
+ {
71
+ parameter: 'path',
72
+ value: req,
73
+ suggestion: 'Check if the directory exists and is accessible',
74
+ },
75
+ );
56
76
  }
57
77
  },
58
78
  });
@@ -5,6 +5,7 @@ import { promisify } from 'node:util';
5
5
  import { expandTilde, isAbsoluteLike, resolveSafePath } from './util.ts';
6
6
  import DESCRIPTION from './tree.txt' with { type: 'text' };
7
7
  import { toIgnoredBasenames } from '../ignore.ts';
8
+ import { createToolError, type ToolResponse } from '../../error.ts';
8
9
 
9
10
  const execAsync = promisify(exec);
10
11
 
@@ -38,7 +39,9 @@ export function buildTreeTool(projectRoot: string): {
38
39
  path: string;
39
40
  depth?: number;
40
41
  ignore?: string[];
41
- }) {
42
+ }): Promise<
43
+ ToolResponse<{ path: string; depth: number | null; tree: string }>
44
+ > {
42
45
  const req = expandTilde(path || '.');
43
46
  const start = isAbsoluteLike(req)
44
47
  ? req
@@ -59,11 +62,20 @@ export function buildTreeTool(projectRoot: string): {
59
62
  maxBuffer: 10 * 1024 * 1024,
60
63
  });
61
64
  const output = stdout.trimEnd();
62
- return { path: req, depth: depth ?? null, tree: output };
65
+ return { ok: true, path: req, depth: depth ?? null, tree: output };
63
66
  } catch (error: unknown) {
64
67
  const err = error as { stderr?: string; stdout?: string };
65
68
  const message = (err.stderr || err.stdout || 'tree failed').trim();
66
- throw new Error(`tree failed for ${req}: ${message}`);
69
+ return createToolError(
70
+ `tree failed for ${req}: ${message}`,
71
+ 'execution',
72
+ {
73
+ parameter: 'path',
74
+ value: req,
75
+ suggestion:
76
+ 'Check if the directory exists and tree command is installed',
77
+ },
78
+ );
67
79
  }
68
80
  },
69
81
  });
@@ -5,6 +5,7 @@ import { promisify } from 'node:util';
5
5
  import GIT_STATUS_DESCRIPTION from './git.status.txt' with { type: 'text' };
6
6
  import GIT_DIFF_DESCRIPTION from './git.diff.txt' with { type: 'text' };
7
7
  import GIT_COMMIT_DESCRIPTION from './git.commit.txt' with { type: 'text' };
8
+ import { createToolError, type ToolResponse } from '../error.ts';
8
9
 
9
10
  const execAsync = promisify(exec);
10
11
 
@@ -37,14 +38,13 @@ export function buildGitTools(
37
38
  const git_status = tool({
38
39
  description: GIT_STATUS_DESCRIPTION,
39
40
  inputSchema: z.object({}).optional(),
40
- async execute() {
41
+ async execute(): Promise<
42
+ ToolResponse<{ staged: number; unstaged: number; raw: string[] }>
43
+ > {
41
44
  if (!(await inRepo())) {
42
- return {
43
- error: 'Not a git repository',
44
- staged: 0,
45
- unstaged: 0,
46
- raw: [],
47
- };
45
+ return createToolError('Not a git repository', 'not_found', {
46
+ suggestion: 'Initialize a git repository with git init',
47
+ });
48
48
  }
49
49
  const gitRoot = await findGitRoot();
50
50
  const { stdout } = await execAsync(
@@ -63,6 +63,7 @@ export function buildGitTools(
63
63
  if (isUntracked || y !== ' ') unstaged += 1;
64
64
  }
65
65
  return {
66
+ ok: true,
66
67
  staged,
67
68
  unstaged,
68
69
  raw: lines.slice(0, 200),
@@ -73,9 +74,15 @@ export function buildGitTools(
73
74
  const git_diff = tool({
74
75
  description: GIT_DIFF_DESCRIPTION,
75
76
  inputSchema: z.object({ all: z.boolean().optional().default(false) }),
76
- async execute({ all }: { all?: boolean }) {
77
+ async execute({
78
+ all,
79
+ }: {
80
+ all?: boolean;
81
+ }): Promise<ToolResponse<{ all: boolean; patch: string }>> {
77
82
  if (!(await inRepo())) {
78
- return { error: 'Not a git repository', all: !!all, patch: '' };
83
+ return createToolError('Not a git repository', 'not_found', {
84
+ suggestion: 'Initialize a git repository with git init',
85
+ });
79
86
  }
80
87
  const gitRoot = await findGitRoot();
81
88
  // When all=true, show full working tree diff relative to HEAD
@@ -86,7 +93,7 @@ export function buildGitTools(
86
93
  : `git -C "${gitRoot}" diff --staged`;
87
94
  const { stdout } = await execAsync(cmd, { maxBuffer: 10 * 1024 * 1024 });
88
95
  const limited = stdout.split('\n').slice(0, 5000).join('\n');
89
- return { all: !!all, patch: limited };
96
+ return { ok: true, all: !!all, patch: limited };
90
97
  },
91
98
  });
92
99
 
@@ -105,9 +112,11 @@ export function buildGitTools(
105
112
  message: string;
106
113
  amend?: boolean;
107
114
  signoff?: boolean;
108
- }) {
115
+ }): Promise<ToolResponse<{ result: string }>> {
109
116
  if (!(await inRepo())) {
110
- return { success: false, error: 'Not a git repository' };
117
+ return createToolError('Not a git repository', 'not_found', {
118
+ suggestion: 'Initialize a git repository with git init',
119
+ });
111
120
  }
112
121
  const gitRoot = await findGitRoot();
113
122
  const args = [
@@ -122,11 +131,14 @@ export function buildGitTools(
122
131
  if (signoff) args.push('--signoff');
123
132
  try {
124
133
  const { stdout } = await execAsync(args.join(' '));
125
- return { result: stdout.trim() };
134
+ return { ok: true, result: stdout.trim() };
126
135
  } catch (error: unknown) {
127
136
  const err = error as { stderr?: string; message?: string };
128
137
  const txt = err.stderr || err.message || 'git commit failed';
129
- throw new Error(txt);
138
+ return createToolError(txt, 'execution', {
139
+ suggestion:
140
+ 'Check if there are staged changes and the commit message is valid',
141
+ });
130
142
  }
131
143
  },
132
144
  });
@@ -5,6 +5,7 @@ import { join } from 'node:path';
5
5
  import { stat } from 'node:fs/promises';
6
6
  import DESCRIPTION from './glob.txt' with { type: 'text' };
7
7
  import { defaultIgnoreGlobs } from './ignore.ts';
8
+ import { createToolError, type ToolResponse } from '../error.ts';
8
9
 
9
10
  function expandTilde(p: string) {
10
11
  const home = process.env.HOME || process.env.USERPROFILE || '';
@@ -54,7 +55,14 @@ export function buildGlobTool(projectRoot: string): {
54
55
  path?: string;
55
56
  ignore?: string[];
56
57
  limit?: number;
57
- }) {
58
+ }): Promise<
59
+ ToolResponse<{
60
+ count: number;
61
+ total: number;
62
+ files: string[];
63
+ truncated: boolean;
64
+ }>
65
+ > {
58
66
  const p = expandTilde(String(path || '.')).trim();
59
67
  const isAbs = p.startsWith('/') || /^[A-Za-z]:[\\/]/.test(p);
60
68
  const searchPath = p ? (isAbs ? p : join(projectRoot, p)) : projectRoot;
@@ -96,6 +104,7 @@ export function buildGlobTool(projectRoot: string): {
96
104
  const limitedFiles = filesWithStats.slice(0, limit).map((f) => f.file);
97
105
 
98
106
  return {
107
+ ok: true,
99
108
  count: limitedFiles.length,
100
109
  total: files.length,
101
110
  files: limitedFiles,
@@ -103,12 +112,15 @@ export function buildGlobTool(projectRoot: string): {
103
112
  };
104
113
  } catch (error: unknown) {
105
114
  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
- };
115
+ return createToolError(
116
+ `Glob search failed: ${err.message || String(error)}`,
117
+ 'execution',
118
+ {
119
+ parameter: 'pattern',
120
+ value: pattern,
121
+ suggestion: 'Check if the pattern syntax is valid',
122
+ },
123
+ );
112
124
  }
113
125
  },
114
126
  });
@@ -5,6 +5,7 @@ import { promisify } from 'node:util';
5
5
  import { join } from 'node:path';
6
6
  import DESCRIPTION from './grep.txt' with { type: 'text' };
7
7
  import { defaultIgnoreGlobs } from './ignore.ts';
8
+ import { createToolError, type ToolResponse } from '../error.ts';
8
9
 
9
10
  const execAsync = promisify(exec);
10
11
 
@@ -39,9 +40,24 @@ export function buildGrepTool(projectRoot: string): {
39
40
  .optional()
40
41
  .describe('Glob patterns to exclude from search'),
41
42
  }),
42
- async execute(params) {
43
+ async execute(params: {
44
+ pattern: string;
45
+ path?: string;
46
+ include?: string;
47
+ ignore?: string[];
48
+ }): Promise<
49
+ ToolResponse<{
50
+ count: number;
51
+ matches: Array<{ file: string; line: number; text: string }>;
52
+ }>
53
+ > {
43
54
  const pattern = String(params.pattern || '');
44
- if (!pattern) throw new Error('pattern is required');
55
+ if (!pattern) {
56
+ return createToolError('pattern is required', 'validation', {
57
+ parameter: 'pattern',
58
+ suggestion: 'Provide a regex pattern to search for',
59
+ });
60
+ }
45
61
 
46
62
  const p = expandTilde(String(params.path || '')).trim();
47
63
  const isAbs = p.startsWith('/') || /^[A-Za-z]:[\\/]/.test(p);
@@ -63,10 +79,19 @@ export function buildGrepTool(projectRoot: string): {
63
79
  } catch (error: unknown) {
64
80
  const err = error as { code?: number; stderr?: string };
65
81
  if (err.code === 1) {
66
- return { count: 0, matches: [] };
82
+ return { ok: true, count: 0, matches: [] };
67
83
  }
68
84
  const err2 = error as { stderr?: string; message?: string };
69
- throw new Error(`ripgrep failed: ${err2.stderr || err2.message}`);
85
+ return createToolError(
86
+ `ripgrep failed: ${err2.stderr || err2.message}`,
87
+ 'execution',
88
+ {
89
+ parameter: 'pattern',
90
+ value: pattern,
91
+ suggestion:
92
+ 'Check if ripgrep (rg) is installed and the pattern is valid',
93
+ },
94
+ );
70
95
  }
71
96
 
72
97
  const lines = output.trim().split('\n');
@@ -94,6 +119,7 @@ export function buildGrepTool(projectRoot: string): {
94
119
  const finalMatches = truncated ? matches.slice(0, limit) : matches;
95
120
 
96
121
  return {
122
+ ok: true,
97
123
  count: finalMatches.length,
98
124
  matches: finalMatches,
99
125
  };
@@ -3,6 +3,7 @@ import { z } from 'zod';
3
3
  import { spawn } from 'node:child_process';
4
4
  import { join } from 'node:path';
5
5
  import DESCRIPTION from './ripgrep.txt' with { type: 'text' };
6
+ import { createToolError, type ToolResponse } from '../error.ts';
6
7
 
7
8
  export function buildRipgrepTool(projectRoot: string): {
8
9
  name: string;
@@ -36,7 +37,12 @@ export function buildRipgrepTool(projectRoot: string): {
36
37
  ignoreCase?: boolean;
37
38
  glob?: string[];
38
39
  maxResults?: number;
39
- }) {
40
+ }): Promise<
41
+ ToolResponse<{
42
+ count: number;
43
+ matches: Array<{ file: string; line: number; text: string }>;
44
+ }>
45
+ > {
40
46
  function expandTilde(p: string) {
41
47
  const home = process.env.HOME || process.env.USERPROFILE || '';
42
48
  if (!home) return p;
@@ -70,7 +76,16 @@ export function buildRipgrepTool(projectRoot: string): {
70
76
 
71
77
  proc.on('close', (code) => {
72
78
  if (code !== 0 && code !== 1) {
73
- resolve({ count: 0, matches: [], error: stderr.trim() });
79
+ resolve(
80
+ createToolError(
81
+ stderr.trim() || 'ripgrep failed',
82
+ 'execution',
83
+ {
84
+ suggestion:
85
+ 'Check if ripgrep (rg) is installed and the query is valid',
86
+ },
87
+ ),
88
+ );
74
89
  return;
75
90
  }
76
91
 
@@ -86,15 +101,21 @@ export function buildRipgrepTool(projectRoot: string): {
86
101
  const text = parts.slice(2).join(':');
87
102
  return { file, line, text };
88
103
  });
89
- resolve({ count: matches.length, matches });
104
+ resolve({ ok: true, count: matches.length, matches });
90
105
  });
91
106
 
92
107
  proc.on('error', (err) => {
93
- resolve({ count: 0, matches: [], error: String(err) });
108
+ resolve(
109
+ createToolError(String(err), 'execution', {
110
+ suggestion: 'Ensure ripgrep (rg) is installed',
111
+ }),
112
+ );
94
113
  });
95
114
  });
96
115
  } catch (err) {
97
- return { count: 0, matches: [], error: String(err) };
116
+ return createToolError(String(err), 'execution', {
117
+ suggestion: 'Ensure ripgrep (rg) is installed',
118
+ });
98
119
  }
99
120
  },
100
121
  });
@@ -1,6 +1,7 @@
1
1
  import { tool, type Tool } from 'ai';
2
2
  import { z } from 'zod';
3
3
  import DESCRIPTION from './websearch.txt' with { type: 'text' };
4
+ import { createToolError, type ToolResponse } from '../error.ts';
4
5
 
5
6
  export function buildWebSearchTool(): {
6
7
  name: string;
@@ -42,7 +43,22 @@ export function buildWebSearchTool(): {
42
43
  url?: string;
43
44
  query?: string;
44
45
  maxLength?: number;
45
- }) {
46
+ }): Promise<
47
+ ToolResponse<
48
+ | {
49
+ url: string;
50
+ content: string;
51
+ contentLength: number;
52
+ truncated: boolean;
53
+ contentType: string;
54
+ }
55
+ | {
56
+ query: string;
57
+ results: Array<{ title: string; url: string; snippet: string }>;
58
+ count: number;
59
+ }
60
+ >
61
+ > {
46
62
  const maxLen = maxLength ?? 50000;
47
63
 
48
64
  if (url) {
@@ -76,9 +92,11 @@ export function buildWebSearchTool(): {
76
92
  ) {
77
93
  content = await response.text();
78
94
  } else {
79
- return {
80
- error: `Unsupported content type: ${contentType}. Only text-based content can be fetched.`,
81
- };
95
+ return createToolError(
96
+ `Unsupported content type: ${contentType}. Only text-based content can be fetched.`,
97
+ 'unsupported',
98
+ { contentType },
99
+ );
82
100
  }
83
101
 
84
102
  // Strip HTML tags for better readability (basic cleaning)
@@ -93,6 +111,7 @@ export function buildWebSearchTool(): {
93
111
  const wasTruncated = cleanContent.length > maxLen;
94
112
 
95
113
  return {
114
+ ok: true,
96
115
  url,
97
116
  content: truncated,
98
117
  contentLength: cleanContent.length,
@@ -102,9 +121,11 @@ export function buildWebSearchTool(): {
102
121
  } catch (error) {
103
122
  const errorMessage =
104
123
  error instanceof Error ? error.message : String(error);
105
- return {
106
- error: `Failed to fetch URL: ${errorMessage}`,
107
- };
124
+ return createToolError(
125
+ `Failed to fetch URL: ${errorMessage}`,
126
+ 'execution',
127
+ { url },
128
+ );
108
129
  }
109
130
  }
110
131
 
@@ -183,16 +204,19 @@ export function buildWebSearchTool(): {
183
204
  }
184
205
 
185
206
  if (results.length === 0) {
186
- return {
187
- error:
188
- 'No search results found. The search service may have changed its format or blocked the request.',
189
- query,
190
- suggestion:
191
- 'Try using the url parameter to fetch a specific webpage instead.',
192
- };
207
+ return createToolError(
208
+ 'No search results found. The search service may have changed its format or blocked the request.',
209
+ 'execution',
210
+ {
211
+ query,
212
+ suggestion:
213
+ 'Try using the url parameter to fetch a specific webpage instead.',
214
+ },
215
+ );
193
216
  }
194
217
 
195
218
  return {
219
+ ok: true,
196
220
  query,
197
221
  results,
198
222
  count: results.length,
@@ -200,18 +224,25 @@ export function buildWebSearchTool(): {
200
224
  } catch (error) {
201
225
  const errorMessage =
202
226
  error instanceof Error ? error.message : String(error);
203
- return {
204
- error: `Search failed: ${errorMessage}`,
205
- query,
206
- suggestion:
207
- 'Search services may be temporarily unavailable. Try using the url parameter to fetch a specific webpage instead.',
208
- };
227
+ return createToolError(
228
+ `Search failed: ${errorMessage}`,
229
+ 'execution',
230
+ {
231
+ query,
232
+ suggestion:
233
+ 'Search services may be temporarily unavailable. Try using the url parameter to fetch a specific webpage instead.',
234
+ },
235
+ );
209
236
  }
210
237
  }
211
238
 
212
- return {
213
- error: 'Must provide either url or query parameter',
214
- };
239
+ return createToolError(
240
+ 'Must provide either url or query parameter',
241
+ 'validation',
242
+ {
243
+ suggestion: 'Provide either a url to fetch or a query to search',
244
+ },
245
+ );
215
246
  },
216
247
  });
217
248