@agi-cli/sdk 0.1.110 → 0.1.112
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/auth/src/index.ts +9 -0
- package/src/auth/src/openai-oauth.ts +283 -0
- package/src/core/src/providers/resolver.ts +21 -1
- package/src/core/src/tools/builtin/edit.ts +22 -5
- package/src/core/src/tools/builtin/finish.ts +3 -2
- package/src/core/src/tools/builtin/fs/ls.ts +23 -3
- package/src/core/src/tools/builtin/fs/tree.ts +15 -3
- package/src/core/src/tools/builtin/git.ts +26 -14
- package/src/core/src/tools/builtin/glob.ts +19 -7
- package/src/core/src/tools/builtin/grep.ts +30 -4
- package/src/core/src/tools/builtin/ripgrep.ts +26 -5
- package/src/core/src/tools/builtin/websearch.ts +54 -23
- package/src/index.ts +14 -0
- package/src/providers/src/catalog.ts +354 -13
- package/src/providers/src/index.ts +6 -0
- package/src/providers/src/openai-oauth-client.ts +206 -0
- package/src/providers/src/utils.ts +39 -0
- package/src/types/src/auth.ts +2 -0
package/package.json
CHANGED
package/src/auth/src/index.ts
CHANGED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomBytes, createHash } from 'node:crypto';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
|
|
5
|
+
const OPENAI_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
6
|
+
const OPENAI_ISSUER = 'https://auth.openai.com';
|
|
7
|
+
const OPENAI_CALLBACK_PORT = 1455;
|
|
8
|
+
|
|
9
|
+
function generatePKCE() {
|
|
10
|
+
const verifier = randomBytes(32)
|
|
11
|
+
.toString('base64')
|
|
12
|
+
.replace(/\+/g, '-')
|
|
13
|
+
.replace(/\//g, '_')
|
|
14
|
+
.replace(/=/g, '');
|
|
15
|
+
|
|
16
|
+
const challenge = createHash('sha256')
|
|
17
|
+
.update(verifier)
|
|
18
|
+
.digest('base64')
|
|
19
|
+
.replace(/\+/g, '-')
|
|
20
|
+
.replace(/\//g, '_')
|
|
21
|
+
.replace(/=/g, '');
|
|
22
|
+
|
|
23
|
+
return { verifier, challenge };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function generateState() {
|
|
27
|
+
return randomBytes(32)
|
|
28
|
+
.toString('base64')
|
|
29
|
+
.replace(/\+/g, '-')
|
|
30
|
+
.replace(/\//g, '_')
|
|
31
|
+
.replace(/=/g, '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function openBrowser(url: string) {
|
|
35
|
+
const platform = process.platform;
|
|
36
|
+
let command: string;
|
|
37
|
+
|
|
38
|
+
switch (platform) {
|
|
39
|
+
case 'darwin':
|
|
40
|
+
command = `open "${url}"`;
|
|
41
|
+
break;
|
|
42
|
+
case 'win32':
|
|
43
|
+
command = `start "${url}"`;
|
|
44
|
+
break;
|
|
45
|
+
default:
|
|
46
|
+
command = `xdg-open "${url}"`;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return new Promise<void>((resolve, reject) => {
|
|
51
|
+
const child = spawn(command, [], { shell: true });
|
|
52
|
+
child.on('error', reject);
|
|
53
|
+
child.on('exit', (code) => {
|
|
54
|
+
if (code === 0) resolve();
|
|
55
|
+
else reject(new Error(`Failed to open browser (exit code ${code})`));
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type OpenAIOAuthResult = {
|
|
61
|
+
url: string;
|
|
62
|
+
verifier: string;
|
|
63
|
+
waitForCallback: () => Promise<string>;
|
|
64
|
+
close: () => void;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export async function authorizeOpenAI(): Promise<OpenAIOAuthResult> {
|
|
68
|
+
const pkce = generatePKCE();
|
|
69
|
+
const state = generateState();
|
|
70
|
+
const redirectUri = `http://localhost:${OPENAI_CALLBACK_PORT}/auth/callback`;
|
|
71
|
+
|
|
72
|
+
const params = new URLSearchParams({
|
|
73
|
+
response_type: 'code',
|
|
74
|
+
client_id: OPENAI_CLIENT_ID,
|
|
75
|
+
redirect_uri: redirectUri,
|
|
76
|
+
scope: 'openid profile email offline_access',
|
|
77
|
+
code_challenge: pkce.challenge,
|
|
78
|
+
code_challenge_method: 'S256',
|
|
79
|
+
id_token_add_organizations: 'true',
|
|
80
|
+
codex_cli_simplified_flow: 'true',
|
|
81
|
+
state: state,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const authUrl = `${OPENAI_ISSUER}/oauth/authorize?${params.toString()}`;
|
|
85
|
+
|
|
86
|
+
let resolveCallback: (code: string) => void;
|
|
87
|
+
let rejectCallback: (error: Error) => void;
|
|
88
|
+
const callbackPromise = new Promise<string>((resolve, reject) => {
|
|
89
|
+
resolveCallback = resolve;
|
|
90
|
+
rejectCallback = reject;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const server = createServer((req, res) => {
|
|
94
|
+
const reqUrl = new URL(
|
|
95
|
+
req.url || '/',
|
|
96
|
+
`http://localhost:${OPENAI_CALLBACK_PORT}`,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (reqUrl.pathname === '/auth/callback') {
|
|
100
|
+
const code = reqUrl.searchParams.get('code');
|
|
101
|
+
const returnedState = reqUrl.searchParams.get('state');
|
|
102
|
+
const error = reqUrl.searchParams.get('error');
|
|
103
|
+
|
|
104
|
+
if (error) {
|
|
105
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
106
|
+
res.end(
|
|
107
|
+
`<html><body><h1>Authentication Failed</h1><p>${error}</p></body></html>`,
|
|
108
|
+
);
|
|
109
|
+
rejectCallback(new Error(`OAuth error: ${error}`));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (returnedState !== state) {
|
|
114
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
115
|
+
res.end(
|
|
116
|
+
'<html><body><h1>Invalid State</h1><p>State mismatch. Please try again.</p></body></html>',
|
|
117
|
+
);
|
|
118
|
+
rejectCallback(new Error('State mismatch'));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (code) {
|
|
123
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
124
|
+
res.end(`
|
|
125
|
+
<html>
|
|
126
|
+
<head><title>AGI - Authentication Successful</title></head>
|
|
127
|
+
<body style="font-family: system-ui; text-align: center; padding: 50px;">
|
|
128
|
+
<h1>✅ Authentication Successful</h1>
|
|
129
|
+
<p>You can close this window and return to the terminal.</p>
|
|
130
|
+
</body>
|
|
131
|
+
</html>
|
|
132
|
+
`);
|
|
133
|
+
resolveCallback(code);
|
|
134
|
+
} else {
|
|
135
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
136
|
+
res.end('<html><body><h1>Missing Code</h1></body></html>');
|
|
137
|
+
rejectCallback(new Error('No authorization code received'));
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
res.writeHead(404);
|
|
141
|
+
res.end('Not found');
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await new Promise<void>((resolve, reject) => {
|
|
146
|
+
server.on('error', (err: NodeJS.ErrnoException) => {
|
|
147
|
+
if (err.code === 'EADDRINUSE') {
|
|
148
|
+
reject(
|
|
149
|
+
new Error(
|
|
150
|
+
`Port ${OPENAI_CALLBACK_PORT} is already in use. Make sure no other OAuth flow is running (including the official Codex CLI).`,
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
} else {
|
|
154
|
+
reject(err);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
server.listen(OPENAI_CALLBACK_PORT, '127.0.0.1', () => resolve());
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
url: authUrl,
|
|
162
|
+
verifier: pkce.verifier,
|
|
163
|
+
waitForCallback: () => callbackPromise,
|
|
164
|
+
close: () => {
|
|
165
|
+
server.close();
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function exchangeOpenAI(code: string, verifier: string) {
|
|
171
|
+
const redirectUri = `http://localhost:${OPENAI_CALLBACK_PORT}/auth/callback`;
|
|
172
|
+
|
|
173
|
+
const response = await fetch(`${OPENAI_ISSUER}/oauth/token`, {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: {
|
|
176
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
177
|
+
},
|
|
178
|
+
body: new URLSearchParams({
|
|
179
|
+
grant_type: 'authorization_code',
|
|
180
|
+
code,
|
|
181
|
+
redirect_uri: redirectUri,
|
|
182
|
+
client_id: OPENAI_CLIENT_ID,
|
|
183
|
+
code_verifier: verifier,
|
|
184
|
+
}).toString(),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
const error = await response.text();
|
|
189
|
+
throw new Error(`Token exchange failed: ${error}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const json = (await response.json()) as {
|
|
193
|
+
id_token: string;
|
|
194
|
+
access_token: string;
|
|
195
|
+
refresh_token: string;
|
|
196
|
+
expires_in?: number;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
let accountId: string | undefined;
|
|
200
|
+
try {
|
|
201
|
+
const payload = JSON.parse(
|
|
202
|
+
Buffer.from(json.access_token.split('.')[1], 'base64').toString(),
|
|
203
|
+
);
|
|
204
|
+
accountId = payload['https://api.openai.com/auth']?.chatgpt_account_id;
|
|
205
|
+
} catch {}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
idToken: json.id_token,
|
|
209
|
+
access: json.access_token,
|
|
210
|
+
refresh: json.refresh_token,
|
|
211
|
+
expires: Date.now() + (json.expires_in || 3600) * 1000,
|
|
212
|
+
accountId,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function refreshOpenAIToken(refreshToken: string) {
|
|
217
|
+
const response = await fetch(`${OPENAI_ISSUER}/oauth/token`, {
|
|
218
|
+
method: 'POST',
|
|
219
|
+
headers: {
|
|
220
|
+
'Content-Type': 'application/json',
|
|
221
|
+
},
|
|
222
|
+
body: JSON.stringify({
|
|
223
|
+
client_id: OPENAI_CLIENT_ID,
|
|
224
|
+
grant_type: 'refresh_token',
|
|
225
|
+
refresh_token: refreshToken,
|
|
226
|
+
scope: 'openid profile email',
|
|
227
|
+
}),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (!response.ok) {
|
|
231
|
+
throw new Error('Failed to refresh OpenAI token');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const json = (await response.json()) as {
|
|
235
|
+
id_token?: string;
|
|
236
|
+
access_token?: string;
|
|
237
|
+
refresh_token?: string;
|
|
238
|
+
expires_in?: number;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
idToken: json.id_token,
|
|
243
|
+
access: json.access_token || '',
|
|
244
|
+
refresh: json.refresh_token || refreshToken,
|
|
245
|
+
expires: Date.now() + (json.expires_in || 3600) * 1000,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export async function openOpenAIAuthUrl(url: string) {
|
|
250
|
+
try {
|
|
251
|
+
await openBrowser(url);
|
|
252
|
+
return true;
|
|
253
|
+
} catch {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export async function obtainOpenAIApiKey(idToken: string): Promise<string> {
|
|
259
|
+
const response = await fetch(`${OPENAI_ISSUER}/oauth/token`, {
|
|
260
|
+
method: 'POST',
|
|
261
|
+
headers: {
|
|
262
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
263
|
+
},
|
|
264
|
+
body: new URLSearchParams({
|
|
265
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
|
|
266
|
+
client_id: OPENAI_CLIENT_ID,
|
|
267
|
+
requested_token: 'openai-api-key',
|
|
268
|
+
subject_token: idToken,
|
|
269
|
+
subject_token_type: 'urn:ietf:params:oauth:token-type:id_token',
|
|
270
|
+
}).toString(),
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
if (!response.ok) {
|
|
274
|
+
const error = await response.text();
|
|
275
|
+
throw new Error(`API key exchange failed: ${error}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const json = (await response.json()) as {
|
|
279
|
+
access_token: string;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
return json.access_token;
|
|
283
|
+
}
|
|
@@ -3,7 +3,12 @@ import { anthropic, createAnthropic } from '@ai-sdk/anthropic';
|
|
|
3
3
|
import { google, createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
4
4
|
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
|
5
5
|
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
catalog,
|
|
8
|
+
createSolforgeModel,
|
|
9
|
+
createOpenAIOAuthModel,
|
|
10
|
+
} from '../../../providers/src/index.ts';
|
|
11
|
+
import type { OAuth } from '../../../types/src/index.ts';
|
|
7
12
|
|
|
8
13
|
export type ProviderName =
|
|
9
14
|
| 'openai'
|
|
@@ -19,6 +24,8 @@ export type ModelConfig = {
|
|
|
19
24
|
apiKey?: string;
|
|
20
25
|
customFetch?: typeof fetch;
|
|
21
26
|
baseURL?: string;
|
|
27
|
+
oauth?: OAuth;
|
|
28
|
+
projectRoot?: string;
|
|
22
29
|
};
|
|
23
30
|
|
|
24
31
|
export async function resolveModel(
|
|
@@ -27,6 +34,19 @@ export async function resolveModel(
|
|
|
27
34
|
config: ModelConfig = {},
|
|
28
35
|
) {
|
|
29
36
|
if (provider === 'openai') {
|
|
37
|
+
if (config.oauth) {
|
|
38
|
+
return createOpenAIOAuthModel(model, {
|
|
39
|
+
oauth: config.oauth,
|
|
40
|
+
projectRoot: config.projectRoot,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (config.customFetch) {
|
|
44
|
+
const instance = createOpenAI({
|
|
45
|
+
apiKey: config.apiKey || 'oauth-token',
|
|
46
|
+
fetch: config.customFetch,
|
|
47
|
+
});
|
|
48
|
+
return instance(model);
|
|
49
|
+
}
|
|
30
50
|
if (config.apiKey) {
|
|
31
51
|
const instance = createOpenAI({ apiKey: config.apiKey });
|
|
32
52
|
return instance(model);
|
|
@@ -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)
|
|
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
|
-
|
|
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 }
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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({
|
|
77
|
+
async execute({
|
|
78
|
+
all,
|
|
79
|
+
}: {
|
|
80
|
+
all?: boolean;
|
|
81
|
+
}): Promise<ToolResponse<{ all: boolean; patch: string }>> {
|
|
77
82
|
if (!(await inRepo())) {
|
|
78
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
});
|