@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agi-cli/sdk",
3
- "version": "0.1.110",
3
+ "version": "0.1.112",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "ntishxyz",
6
6
  "license": "MIT",
@@ -68,3 +68,12 @@ export {
68
68
  openAuthUrl,
69
69
  createApiKey,
70
70
  } from './oauth.ts';
71
+
72
+ export {
73
+ authorizeOpenAI,
74
+ exchangeOpenAI,
75
+ refreshOpenAIToken,
76
+ openOpenAIAuthUrl,
77
+ obtainOpenAIApiKey,
78
+ type OpenAIOAuthResult,
79
+ } from './openai-oauth.ts';
@@ -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 { catalog, createSolforgeModel } from '../../../providers/src/index.ts';
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) 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
  });