@agi-cli/sdk 0.1.42 → 0.1.43
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
package/src/tools/builtin/git.ts
CHANGED
|
@@ -8,6 +8,19 @@ import GIT_COMMIT_DESCRIPTION from './git.commit.txt' with { type: 'text' };
|
|
|
8
8
|
export function buildGitTools(
|
|
9
9
|
projectRoot: string,
|
|
10
10
|
): Array<{ name: string; tool: Tool }> {
|
|
11
|
+
// Helper to find git root directory
|
|
12
|
+
async function findGitRoot(): Promise<string> {
|
|
13
|
+
try {
|
|
14
|
+
const res = await $`git -C ${projectRoot} rev-parse --show-toplevel`
|
|
15
|
+
.quiet()
|
|
16
|
+
.text()
|
|
17
|
+
.catch(() => '');
|
|
18
|
+
return res.trim() || projectRoot;
|
|
19
|
+
} catch {
|
|
20
|
+
return projectRoot;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
async function inRepo(): Promise<boolean> {
|
|
12
25
|
const res = await $`git -C ${projectRoot} rev-parse --is-inside-work-tree`
|
|
13
26
|
.quiet()
|
|
@@ -21,7 +34,8 @@ export function buildGitTools(
|
|
|
21
34
|
inputSchema: z.object({}).optional(),
|
|
22
35
|
async execute() {
|
|
23
36
|
if (!(await inRepo())) throw new Error('Not a git repository');
|
|
24
|
-
const
|
|
37
|
+
const gitRoot = await findGitRoot();
|
|
38
|
+
const out = await $`git -C ${gitRoot} status --porcelain=v1`.text();
|
|
25
39
|
const lines = out.split('\n').filter(Boolean);
|
|
26
40
|
let staged = 0;
|
|
27
41
|
let unstaged = 0;
|
|
@@ -47,11 +61,12 @@ export function buildGitTools(
|
|
|
47
61
|
inputSchema: z.object({ all: z.boolean().optional().default(false) }),
|
|
48
62
|
async execute({ all }: { all?: boolean }) {
|
|
49
63
|
if (!(await inRepo())) throw new Error('Not a git repository');
|
|
64
|
+
const gitRoot = await findGitRoot();
|
|
50
65
|
// When all=true, show full working tree diff relative to HEAD
|
|
51
66
|
// so both staged and unstaged changes are included. Otherwise,
|
|
52
67
|
// show only the staged diff (index vs HEAD).
|
|
53
68
|
const args = all ? ['diff', 'HEAD'] : ['diff', '--staged'];
|
|
54
|
-
const out = await $`git -C ${
|
|
69
|
+
const out = await $`git -C ${gitRoot} ${args}`.text();
|
|
55
70
|
const limited = out.split('\n').slice(0, 5000).join('\n');
|
|
56
71
|
return { all: !!all, patch: limited };
|
|
57
72
|
},
|
|
@@ -74,10 +89,11 @@ export function buildGitTools(
|
|
|
74
89
|
signoff?: boolean;
|
|
75
90
|
}) {
|
|
76
91
|
if (!(await inRepo())) throw new Error('Not a git repository');
|
|
92
|
+
const gitRoot = await findGitRoot();
|
|
77
93
|
const args = ['commit', '-m', message];
|
|
78
94
|
if (amend) args.push('--amend');
|
|
79
95
|
if (signoff) args.push('--signoff');
|
|
80
|
-
const res = await $`git -C ${
|
|
96
|
+
const res = await $`git -C ${gitRoot} ${args}`
|
|
81
97
|
.quiet()
|
|
82
98
|
.text()
|
|
83
99
|
.catch(async (e) => {
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { tool, type Tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import DESCRIPTION from './websearch.txt' with { type: 'text' };
|
|
4
|
+
|
|
5
|
+
export function buildWebSearchTool(): {
|
|
6
|
+
name: string;
|
|
7
|
+
tool: Tool;
|
|
8
|
+
} {
|
|
9
|
+
const websearch = tool({
|
|
10
|
+
description: DESCRIPTION,
|
|
11
|
+
inputSchema: z
|
|
12
|
+
.object({
|
|
13
|
+
url: z
|
|
14
|
+
.string()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe(
|
|
17
|
+
'URL to fetch content from (mutually exclusive with query)',
|
|
18
|
+
),
|
|
19
|
+
query: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe(
|
|
23
|
+
'Search query to search the web (mutually exclusive with url)',
|
|
24
|
+
),
|
|
25
|
+
maxLength: z
|
|
26
|
+
.number()
|
|
27
|
+
.optional()
|
|
28
|
+
.default(50000)
|
|
29
|
+
.describe(
|
|
30
|
+
'Maximum content length to return (default: 50000 characters)',
|
|
31
|
+
),
|
|
32
|
+
})
|
|
33
|
+
.strict()
|
|
34
|
+
.refine((data) => (data.url ? !data.query : !!data.query), {
|
|
35
|
+
message: 'Must provide either url or query, but not both',
|
|
36
|
+
}),
|
|
37
|
+
async execute({
|
|
38
|
+
url,
|
|
39
|
+
query,
|
|
40
|
+
maxLength,
|
|
41
|
+
}: {
|
|
42
|
+
url?: string;
|
|
43
|
+
query?: string;
|
|
44
|
+
maxLength?: number;
|
|
45
|
+
}) {
|
|
46
|
+
const maxLen = maxLength ?? 50000;
|
|
47
|
+
|
|
48
|
+
if (url) {
|
|
49
|
+
// Fetch URL content
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetch(url, {
|
|
52
|
+
headers: {
|
|
53
|
+
'User-Agent':
|
|
54
|
+
'Mozilla/5.0 (compatible; AGI-Bot/1.0; +https://github.com/anthropics/agi)',
|
|
55
|
+
Accept:
|
|
56
|
+
'text/html,application/xhtml+xml,application/xml;q=0.9,text/plain;q=0.8,*/*;q=0.7',
|
|
57
|
+
},
|
|
58
|
+
redirect: 'follow',
|
|
59
|
+
signal: AbortSignal.timeout(30000), // 30 second timeout
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`HTTP error! status: ${response.status} ${response.statusText}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const contentType = response.headers.get('content-type') || '';
|
|
69
|
+
let content = '';
|
|
70
|
+
|
|
71
|
+
if (
|
|
72
|
+
contentType.includes('text/') ||
|
|
73
|
+
contentType.includes('application/json') ||
|
|
74
|
+
contentType.includes('application/xml') ||
|
|
75
|
+
contentType.includes('application/xhtml')
|
|
76
|
+
) {
|
|
77
|
+
content = await response.text();
|
|
78
|
+
} else {
|
|
79
|
+
return {
|
|
80
|
+
error: `Unsupported content type: ${contentType}. Only text-based content can be fetched.`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Strip HTML tags for better readability (basic cleaning)
|
|
85
|
+
const cleanContent = content
|
|
86
|
+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
|
87
|
+
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
|
|
88
|
+
.replace(/<[^>]+>/g, ' ')
|
|
89
|
+
.replace(/\s+/g, ' ')
|
|
90
|
+
.trim();
|
|
91
|
+
|
|
92
|
+
const truncated = cleanContent.slice(0, maxLen);
|
|
93
|
+
const wasTruncated = cleanContent.length > maxLen;
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
url,
|
|
97
|
+
content: truncated,
|
|
98
|
+
contentLength: cleanContent.length,
|
|
99
|
+
truncated: wasTruncated,
|
|
100
|
+
contentType,
|
|
101
|
+
};
|
|
102
|
+
} catch (error) {
|
|
103
|
+
const errorMessage =
|
|
104
|
+
error instanceof Error ? error.message : String(error);
|
|
105
|
+
return {
|
|
106
|
+
error: `Failed to fetch URL: ${errorMessage}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (query) {
|
|
112
|
+
// Web search functionality
|
|
113
|
+
// Use DuckDuckGo's HTML search (doesn't require API key)
|
|
114
|
+
try {
|
|
115
|
+
const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
116
|
+
const response = await fetch(searchUrl, {
|
|
117
|
+
headers: {
|
|
118
|
+
'User-Agent':
|
|
119
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
|
120
|
+
Accept: 'text/html',
|
|
121
|
+
},
|
|
122
|
+
redirect: 'follow',
|
|
123
|
+
signal: AbortSignal.timeout(30000),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
throw new Error(`Search failed: ${response.status}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const html = await response.text();
|
|
131
|
+
|
|
132
|
+
// Parse DuckDuckGo results (basic parsing)
|
|
133
|
+
const results: Array<{
|
|
134
|
+
title: string;
|
|
135
|
+
url: string;
|
|
136
|
+
snippet: string;
|
|
137
|
+
}> = [];
|
|
138
|
+
|
|
139
|
+
// Match result blocks
|
|
140
|
+
const resultPattern =
|
|
141
|
+
/<a[^>]+class="result__a"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>[\s\S]*?<a[^>]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
142
|
+
|
|
143
|
+
let match: RegExpExecArray | null = null;
|
|
144
|
+
match = resultPattern.exec(html);
|
|
145
|
+
while (match !== null && results.length < 10) {
|
|
146
|
+
const url = match[1]?.trim();
|
|
147
|
+
const title = match[2]?.trim();
|
|
148
|
+
let snippet = match[3]?.trim();
|
|
149
|
+
|
|
150
|
+
if (url && title) {
|
|
151
|
+
// Clean snippet
|
|
152
|
+
snippet = snippet
|
|
153
|
+
?.replace(/<[^>]+>/g, '')
|
|
154
|
+
.replace(/\s+/g, ' ')
|
|
155
|
+
.trim();
|
|
156
|
+
|
|
157
|
+
results.push({
|
|
158
|
+
title,
|
|
159
|
+
url,
|
|
160
|
+
snippet: snippet || '',
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
match = resultPattern.exec(html);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Fallback: simpler pattern if the above doesn't work
|
|
167
|
+
if (results.length === 0) {
|
|
168
|
+
const simplePattern =
|
|
169
|
+
/<a[^>]+rel="nofollow"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/gi;
|
|
170
|
+
match = simplePattern.exec(html);
|
|
171
|
+
while (match !== null && results.length < 10) {
|
|
172
|
+
const url = match[1]?.trim();
|
|
173
|
+
const title = match[2]?.trim();
|
|
174
|
+
if (url && title && url.startsWith('http')) {
|
|
175
|
+
results.push({
|
|
176
|
+
title,
|
|
177
|
+
url,
|
|
178
|
+
snippet: '',
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
match = simplePattern.exec(html);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
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
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
query,
|
|
197
|
+
results,
|
|
198
|
+
count: results.length,
|
|
199
|
+
};
|
|
200
|
+
} catch (error) {
|
|
201
|
+
const errorMessage =
|
|
202
|
+
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
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
error: 'Must provide either url or query parameter',
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
return { name: 'websearch', tool: websearch };
|
|
219
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
- Search the web or fetch content from URLs
|
|
2
|
+
- Use `query` to search the web and get a list of results with titles, URLs, and snippets
|
|
3
|
+
- Use `url` to fetch and read the content of a specific webpage
|
|
4
|
+
- Returns cleaned, text-based content (HTML tags are stripped)
|
|
5
|
+
- Cannot be used for both search and URL fetch in the same call
|
|
6
|
+
|
|
7
|
+
Usage tips:
|
|
8
|
+
- For research: use `query` to find relevant pages, then `url` to read specific ones
|
|
9
|
+
- Search returns up to 10 results with titles, URLs, and snippets
|
|
10
|
+
- URL fetching works for text-based content (HTML, JSON, XML, plain text)
|
|
11
|
+
- Content is automatically truncated if it exceeds maxLength (default 50,000 chars)
|
|
12
|
+
- Use this to gather current information, read documentation, or verify facts
|
package/src/tools/loader.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { buildGlobTool } from './builtin/glob.ts';
|
|
|
11
11
|
import { buildApplyPatchTool } from './builtin/patch.ts';
|
|
12
12
|
import { updatePlanTool } from './builtin/plan.ts';
|
|
13
13
|
import { editTool } from './builtin/edit.ts';
|
|
14
|
+
import { buildWebSearchTool } from './builtin/websearch.ts';
|
|
14
15
|
import { Glob } from 'bun';
|
|
15
16
|
import { dirname, isAbsolute, join } from 'node:path';
|
|
16
17
|
import { pathToFileURL } from 'node:url';
|
|
@@ -115,6 +116,9 @@ export async function discoverProjectTools(
|
|
|
115
116
|
tools.set('update_plan', updatePlanTool);
|
|
116
117
|
// Edit
|
|
117
118
|
tools.set('edit', editTool);
|
|
119
|
+
// Web search
|
|
120
|
+
const ws = buildWebSearchTool();
|
|
121
|
+
tools.set(ws.name, ws.tool);
|
|
118
122
|
|
|
119
123
|
async function loadFromBase(base: string | null | undefined) {
|
|
120
124
|
if (!base) return;
|