btca-server 1.0.63 → 1.0.71
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 +4 -1
- package/package.json +4 -2
- package/src/agent/agent.test.ts +114 -16
- package/src/agent/loop.ts +14 -11
- package/src/agent/service.ts +117 -86
- package/src/collections/index.ts +0 -0
- package/src/collections/service.ts +187 -57
- package/src/collections/types.ts +1 -0
- package/src/collections/virtual-metadata.ts +32 -0
- package/src/config/config.test.ts +0 -0
- package/src/config/index.ts +195 -127
- package/src/config/remote.ts +132 -79
- package/src/context/index.ts +0 -0
- package/src/context/transaction.ts +20 -15
- package/src/errors.ts +0 -0
- package/src/index.ts +29 -15
- package/src/metrics/index.ts +18 -13
- package/src/providers/auth.ts +38 -11
- package/src/providers/model.ts +3 -1
- package/src/providers/openrouter.ts +39 -0
- package/src/providers/registry.ts +2 -0
- package/src/resources/helpers.ts +0 -0
- package/src/resources/impls/git.test.ts +0 -0
- package/src/resources/impls/git.ts +160 -117
- package/src/resources/index.ts +0 -0
- package/src/resources/schema.ts +24 -27
- package/src/resources/service.ts +0 -0
- package/src/resources/types.ts +0 -0
- package/src/stream/index.ts +0 -0
- package/src/stream/service.ts +23 -14
- package/src/tools/context.ts +4 -0
- package/src/tools/glob.ts +72 -45
- package/src/tools/grep.ts +136 -57
- package/src/tools/index.ts +0 -2
- package/src/tools/list.ts +34 -53
- package/src/tools/read.ts +46 -32
- package/src/tools/virtual-sandbox.ts +103 -0
- package/src/validation/index.ts +12 -12
- package/src/vfs/virtual-fs.test.ts +107 -0
- package/src/vfs/virtual-fs.ts +273 -0
- package/src/tools/ripgrep.ts +0 -348
- package/src/tools/sandbox.ts +0 -164
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
+
import { Result } from 'better-result';
|
|
5
|
+
|
|
4
6
|
import { Metrics } from '../../metrics/index.ts';
|
|
5
7
|
import { CommonHints } from '../../errors.ts';
|
|
6
8
|
import { ResourceError, resourceNameToKey } from '../helpers.ts';
|
|
@@ -28,12 +30,11 @@ const validateSearchPath = (
|
|
|
28
30
|
};
|
|
29
31
|
|
|
30
32
|
const directoryExists = async (path: string): Promise<boolean> => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
33
|
+
const result = await Result.tryPromise(() => fs.stat(path));
|
|
34
|
+
return result.match({
|
|
35
|
+
ok: (stat) => stat.isDirectory(),
|
|
36
|
+
err: () => false
|
|
37
|
+
});
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
/**
|
|
@@ -146,6 +147,17 @@ interface GitRunResult {
|
|
|
146
147
|
stderr: string;
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
const runGitChecked = async (
|
|
151
|
+
args: string[],
|
|
152
|
+
options: { cwd?: string; quiet: boolean },
|
|
153
|
+
buildError: (result: GitRunResult) => ResourceError
|
|
154
|
+
) => {
|
|
155
|
+
const result = await Result.tryPromise(() => runGit(args, options));
|
|
156
|
+
return result.andThen((runResult) =>
|
|
157
|
+
runResult.exitCode === 0 ? Result.ok(runResult) : Result.err(buildError(runResult))
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
|
|
149
161
|
const runGit = async (
|
|
150
162
|
args: string[],
|
|
151
163
|
options: { cwd?: string; quiet: boolean }
|
|
@@ -236,54 +248,67 @@ const gitClone = async (args: {
|
|
|
236
248
|
]
|
|
237
249
|
: ['clone', '--depth', '1', '-b', args.repoBranch, args.repoUrl, args.localAbsolutePath];
|
|
238
250
|
|
|
239
|
-
const result = await
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
throw new ResourceError({
|
|
250
|
-
message,
|
|
251
|
-
hint,
|
|
252
|
-
cause: new Error(`git clone failed with exit code ${result.exitCode}: ${result.stderr}`)
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (needsSparseCheckout) {
|
|
257
|
-
const sparseResult = await runGit(['sparse-checkout', 'set', ...args.repoSubPaths], {
|
|
258
|
-
cwd: args.localAbsolutePath,
|
|
259
|
-
quiet: args.quiet
|
|
260
|
-
});
|
|
251
|
+
const result = await Result.gen(async function* () {
|
|
252
|
+
yield* Result.await(
|
|
253
|
+
runGitChecked(cloneArgs, { quiet: args.quiet }, (cloneResult) => {
|
|
254
|
+
const errorType = detectGitErrorType(cloneResult.stderr);
|
|
255
|
+
const { message, hint } = getGitErrorDetails(errorType, {
|
|
256
|
+
operation: 'clone',
|
|
257
|
+
branch: args.repoBranch,
|
|
258
|
+
url: args.repoUrl
|
|
259
|
+
});
|
|
261
260
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
261
|
+
return new ResourceError({
|
|
262
|
+
message,
|
|
263
|
+
hint,
|
|
264
|
+
cause: new Error(
|
|
265
|
+
`git clone failed with exit code ${cloneResult.exitCode}: ${cloneResult.stderr}`
|
|
266
|
+
)
|
|
267
|
+
});
|
|
268
|
+
})
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
if (needsSparseCheckout) {
|
|
272
|
+
yield* Result.await(
|
|
273
|
+
runGitChecked(
|
|
274
|
+
['sparse-checkout', 'set', ...args.repoSubPaths],
|
|
275
|
+
{ cwd: args.localAbsolutePath, quiet: args.quiet },
|
|
276
|
+
(sparseResult) =>
|
|
277
|
+
new ResourceError({
|
|
278
|
+
message: `Failed to set sparse-checkout path(s): "${args.repoSubPaths.join(', ')}"`,
|
|
279
|
+
hint: 'Verify the search paths exist in the repository. Check the repository structure to find the correct path.',
|
|
280
|
+
cause: new Error(
|
|
281
|
+
`git sparse-checkout failed with exit code ${sparseResult.exitCode}: ${sparseResult.stderr}`
|
|
282
|
+
)
|
|
283
|
+
})
|
|
268
284
|
)
|
|
269
|
-
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
yield* Result.await(
|
|
288
|
+
runGitChecked(
|
|
289
|
+
['checkout'],
|
|
290
|
+
{ cwd: args.localAbsolutePath, quiet: args.quiet },
|
|
291
|
+
(checkout) =>
|
|
292
|
+
new ResourceError({
|
|
293
|
+
message: 'Failed to checkout repository',
|
|
294
|
+
hint: CommonHints.CLEAR_CACHE,
|
|
295
|
+
cause: new Error(
|
|
296
|
+
`git checkout failed with exit code ${checkout.exitCode}: ${checkout.stderr}`
|
|
297
|
+
)
|
|
298
|
+
})
|
|
299
|
+
)
|
|
300
|
+
);
|
|
270
301
|
}
|
|
271
302
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
quiet: args.quiet
|
|
275
|
-
});
|
|
303
|
+
return Result.ok(undefined);
|
|
304
|
+
});
|
|
276
305
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
cause: new Error(
|
|
282
|
-
`git checkout failed with exit code ${checkoutResult.exitCode}: ${checkoutResult.stderr}`
|
|
283
|
-
)
|
|
284
|
-
});
|
|
306
|
+
result.match({
|
|
307
|
+
ok: () => undefined,
|
|
308
|
+
err: (error) => {
|
|
309
|
+
throw error;
|
|
285
310
|
}
|
|
286
|
-
}
|
|
311
|
+
});
|
|
287
312
|
};
|
|
288
313
|
|
|
289
314
|
const gitUpdate = async (args: {
|
|
@@ -292,73 +317,85 @@ const gitUpdate = async (args: {
|
|
|
292
317
|
repoSubPaths: readonly string[];
|
|
293
318
|
quiet: boolean;
|
|
294
319
|
}) => {
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
320
|
+
const result = await Result.gen(async function* () {
|
|
321
|
+
yield* Result.await(
|
|
322
|
+
runGitChecked(
|
|
323
|
+
['fetch', '--depth', '1', 'origin', args.branch],
|
|
324
|
+
{ cwd: args.localAbsolutePath, quiet: args.quiet },
|
|
325
|
+
(fetchResult) => {
|
|
326
|
+
const errorType = detectGitErrorType(fetchResult.stderr);
|
|
327
|
+
const { message, hint } = getGitErrorDetails(errorType, {
|
|
328
|
+
operation: 'fetch',
|
|
329
|
+
branch: args.branch
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
return new ResourceError({
|
|
333
|
+
message,
|
|
334
|
+
hint,
|
|
335
|
+
cause: new Error(
|
|
336
|
+
`git fetch failed with exit code ${fetchResult.exitCode}: ${fetchResult.stderr}`
|
|
337
|
+
)
|
|
338
|
+
});
|
|
339
|
+
}
|
|
312
340
|
)
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
yield* Result.await(
|
|
344
|
+
runGitChecked(
|
|
345
|
+
['reset', '--hard', `origin/${args.branch}`],
|
|
346
|
+
{ cwd: args.localAbsolutePath, quiet: args.quiet },
|
|
347
|
+
(resetResult) =>
|
|
348
|
+
new ResourceError({
|
|
349
|
+
message: 'Failed to update local repository',
|
|
350
|
+
hint: `${CommonHints.CLEAR_CACHE} This will re-clone the repository from scratch.`,
|
|
351
|
+
cause: new Error(
|
|
352
|
+
`git reset failed with exit code ${resetResult.exitCode}: ${resetResult.stderr}`
|
|
353
|
+
)
|
|
354
|
+
})
|
|
327
355
|
)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
if (args.repoSubPaths.length > 0) {
|
|
359
|
+
yield* Result.await(
|
|
360
|
+
runGitChecked(
|
|
361
|
+
['sparse-checkout', 'set', ...args.repoSubPaths],
|
|
362
|
+
{ cwd: args.localAbsolutePath, quiet: args.quiet },
|
|
363
|
+
(sparseResult) =>
|
|
364
|
+
new ResourceError({
|
|
365
|
+
message: `Failed to set sparse-checkout path(s): "${args.repoSubPaths.join(', ')}"`,
|
|
366
|
+
hint: 'Verify the search paths exist in the repository. Check the repository structure to find the correct path.',
|
|
367
|
+
cause: new Error(
|
|
368
|
+
`git sparse-checkout failed with exit code ${sparseResult.exitCode}: ${sparseResult.stderr}`
|
|
369
|
+
)
|
|
370
|
+
})
|
|
343
371
|
)
|
|
344
|
-
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
yield* Result.await(
|
|
375
|
+
runGitChecked(
|
|
376
|
+
['checkout'],
|
|
377
|
+
{ cwd: args.localAbsolutePath, quiet: args.quiet },
|
|
378
|
+
(checkoutResult) =>
|
|
379
|
+
new ResourceError({
|
|
380
|
+
message: 'Failed to checkout repository',
|
|
381
|
+
hint: CommonHints.CLEAR_CACHE,
|
|
382
|
+
cause: new Error(
|
|
383
|
+
`git checkout failed with exit code ${checkoutResult.exitCode}: ${checkoutResult.stderr}`
|
|
384
|
+
)
|
|
385
|
+
})
|
|
386
|
+
)
|
|
387
|
+
);
|
|
345
388
|
}
|
|
346
389
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
quiet: args.quiet
|
|
350
|
-
});
|
|
390
|
+
return Result.ok(undefined);
|
|
391
|
+
});
|
|
351
392
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
cause: new Error(
|
|
357
|
-
`git checkout failed with exit code ${checkoutResult.exitCode}: ${checkoutResult.stderr}`
|
|
358
|
-
)
|
|
359
|
-
});
|
|
393
|
+
result.match({
|
|
394
|
+
ok: () => undefined,
|
|
395
|
+
err: (error) => {
|
|
396
|
+
throw error;
|
|
360
397
|
}
|
|
361
|
-
}
|
|
398
|
+
});
|
|
362
399
|
};
|
|
363
400
|
|
|
364
401
|
/**
|
|
@@ -438,15 +475,21 @@ const ensureGitResource = async (config: BtcaGitResourceArgs): Promise<string> =
|
|
|
438
475
|
repoSubPaths: config.repoSubPaths
|
|
439
476
|
});
|
|
440
477
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
478
|
+
const mkdirResult = await Result.tryPromise({
|
|
479
|
+
try: () => fs.mkdir(config.resourcesDirectoryPath, { recursive: true }),
|
|
480
|
+
catch: (cause) =>
|
|
481
|
+
new ResourceError({
|
|
482
|
+
message: 'Failed to create resources directory',
|
|
483
|
+
hint: 'Check that you have write permissions to the btca data directory.',
|
|
484
|
+
cause
|
|
485
|
+
})
|
|
486
|
+
});
|
|
487
|
+
mkdirResult.match({
|
|
488
|
+
ok: () => undefined,
|
|
489
|
+
err: (error) => {
|
|
490
|
+
throw error;
|
|
491
|
+
}
|
|
492
|
+
});
|
|
450
493
|
|
|
451
494
|
await gitClone({
|
|
452
495
|
repoUrl: config.url,
|
package/src/resources/index.ts
CHANGED
|
File without changes
|
package/src/resources/schema.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Result } from 'better-result';
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
|
|
3
4
|
import { LIMITS } from '../validation/index.ts';
|
|
@@ -18,6 +19,12 @@ const RESOURCE_NAME_REGEX = /^@?[a-zA-Z0-9][a-zA-Z0-9._-]*(\/[a-zA-Z0-9][a-zA-Z0
|
|
|
18
19
|
*/
|
|
19
20
|
const BRANCH_NAME_REGEX = /^[a-zA-Z0-9/_.-]+$/;
|
|
20
21
|
|
|
22
|
+
const parseUrl = (value: string) =>
|
|
23
|
+
Result.try(() => new URL(value)).match({
|
|
24
|
+
ok: (url) => url,
|
|
25
|
+
err: () => null
|
|
26
|
+
});
|
|
27
|
+
|
|
21
28
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
29
|
// Field Schemas
|
|
23
30
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -52,43 +59,33 @@ const GitUrlSchema = z
|
|
|
52
59
|
.min(1, 'Git URL cannot be empty')
|
|
53
60
|
.refine(
|
|
54
61
|
(url) => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return parsed.protocol === 'https:';
|
|
58
|
-
} catch {
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
62
|
+
const parsed = parseUrl(url);
|
|
63
|
+
return parsed ? parsed.protocol === 'https:' : false;
|
|
61
64
|
},
|
|
62
65
|
{ message: 'Git URL must be a valid HTTPS URL' }
|
|
63
66
|
)
|
|
64
67
|
.refine(
|
|
65
68
|
(url) => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
} catch {
|
|
70
|
-
return true; // Let the URL check above handle invalid URLs
|
|
71
|
-
}
|
|
69
|
+
const parsed = parseUrl(url);
|
|
70
|
+
if (!parsed) return true;
|
|
71
|
+
return !parsed.username && !parsed.password;
|
|
72
72
|
},
|
|
73
73
|
{ message: 'Git URL must not contain embedded credentials' }
|
|
74
74
|
)
|
|
75
75
|
.refine(
|
|
76
76
|
(url) => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
} catch {
|
|
90
|
-
return true;
|
|
91
|
-
}
|
|
77
|
+
const parsed = parseUrl(url);
|
|
78
|
+
if (!parsed) return true;
|
|
79
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
80
|
+
return !(
|
|
81
|
+
hostname === 'localhost' ||
|
|
82
|
+
hostname.startsWith('127.') ||
|
|
83
|
+
hostname.startsWith('192.168.') ||
|
|
84
|
+
hostname.startsWith('10.') ||
|
|
85
|
+
hostname.match(/^172\.(1[6-9]|2[0-9]|3[0-1])\./) ||
|
|
86
|
+
hostname === '::1' ||
|
|
87
|
+
hostname === '0.0.0.0'
|
|
88
|
+
);
|
|
92
89
|
},
|
|
93
90
|
{ message: 'Git URL must not point to localhost or private IP addresses' }
|
|
94
91
|
);
|
package/src/resources/service.ts
CHANGED
|
File without changes
|
package/src/resources/types.ts
CHANGED
|
File without changes
|
package/src/stream/index.ts
CHANGED
|
File without changes
|
package/src/stream/service.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { stripUserQuestionFromStart, extractCoreQuestion } from '@btca/shared';
|
|
2
|
+
import { Result } from 'better-result';
|
|
3
|
+
|
|
1
4
|
import { getErrorMessage, getErrorTag } from '../errors.ts';
|
|
2
5
|
import { Metrics } from '../metrics/index.ts';
|
|
3
|
-
import { stripUserQuestionFromStart, extractCoreQuestion } from '@btca/shared';
|
|
4
6
|
import type { AgentLoop } from '../agent/loop.ts';
|
|
5
7
|
|
|
6
8
|
import type {
|
|
@@ -52,7 +54,7 @@ export namespace StreamService {
|
|
|
52
54
|
emit(controller, args.meta);
|
|
53
55
|
|
|
54
56
|
(async () => {
|
|
55
|
-
|
|
57
|
+
const result = await Result.tryPromise(async () => {
|
|
56
58
|
for await (const event of args.eventStream) {
|
|
57
59
|
switch (event.type) {
|
|
58
60
|
case 'text-delta': {
|
|
@@ -157,18 +159,25 @@ export namespace StreamService {
|
|
|
157
159
|
}
|
|
158
160
|
}
|
|
159
161
|
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
result.match({
|
|
165
|
+
ok: () => undefined,
|
|
166
|
+
err: (cause) => {
|
|
167
|
+
Metrics.error('stream.error', {
|
|
168
|
+
collectionKey: args.meta.collection.key,
|
|
169
|
+
error: Metrics.errorInfo(cause)
|
|
170
|
+
});
|
|
171
|
+
const err: BtcaStreamErrorEvent = {
|
|
172
|
+
type: 'error',
|
|
173
|
+
tag: getErrorTag(cause),
|
|
174
|
+
message: getErrorMessage(cause)
|
|
175
|
+
};
|
|
176
|
+
emit(controller, err);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
{
|
|
172
181
|
Metrics.info('stream.closed', { collectionKey: args.meta.collection.key });
|
|
173
182
|
controller.close();
|
|
174
183
|
}
|
package/src/tools/glob.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Glob Tool
|
|
3
|
-
* Fast file pattern matching
|
|
3
|
+
* Fast file pattern matching in-memory
|
|
4
4
|
*/
|
|
5
|
-
import * as fs from 'node:fs/promises';
|
|
6
5
|
import * as path from 'node:path';
|
|
7
6
|
import { z } from 'zod';
|
|
7
|
+
import { Result } from 'better-result';
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import type { ToolContext } from './context.ts';
|
|
10
|
+
import { VirtualSandbox } from './virtual-sandbox.ts';
|
|
11
|
+
import { VirtualFs } from '../vfs/virtual-fs.ts';
|
|
11
12
|
|
|
12
13
|
export namespace GlobTool {
|
|
13
14
|
// Configuration
|
|
@@ -36,32 +37,26 @@ export namespace GlobTool {
|
|
|
36
37
|
};
|
|
37
38
|
};
|
|
38
39
|
|
|
40
|
+
const safeStat = async (filePath: string, vfsId?: string) => {
|
|
41
|
+
const result = await Result.tryPromise(() => VirtualFs.stat(filePath, vfsId));
|
|
42
|
+
return result.match({
|
|
43
|
+
ok: (value) => value,
|
|
44
|
+
err: () => null
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
39
48
|
/**
|
|
40
49
|
* Execute the glob tool
|
|
41
50
|
*/
|
|
42
|
-
export async function execute(
|
|
43
|
-
|
|
44
|
-
context: { basePath: string }
|
|
45
|
-
): Promise<Result> {
|
|
46
|
-
const { basePath } = context;
|
|
51
|
+
export async function execute(params: ParametersType, context: ToolContext): Promise<Result> {
|
|
52
|
+
const { basePath, vfsId } = context;
|
|
47
53
|
|
|
48
54
|
// Resolve search path within sandbox
|
|
49
|
-
const searchPath = params.path ?
|
|
55
|
+
const searchPath = params.path ? VirtualSandbox.resolvePath(basePath, params.path) : basePath;
|
|
50
56
|
|
|
51
57
|
// Validate the search path exists and is a directory
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (!stats.isDirectory()) {
|
|
55
|
-
return {
|
|
56
|
-
title: params.pattern,
|
|
57
|
-
output: `Path is not a directory: ${params.path || '.'}`,
|
|
58
|
-
metadata: {
|
|
59
|
-
count: 0,
|
|
60
|
-
truncated: false
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
} catch {
|
|
58
|
+
const stats = await safeStat(searchPath, vfsId);
|
|
59
|
+
if (!stats) {
|
|
65
60
|
return {
|
|
66
61
|
title: params.pattern,
|
|
67
62
|
output: `Directory not found: ${params.path || '.'}`,
|
|
@@ -71,36 +66,32 @@ export namespace GlobTool {
|
|
|
71
66
|
}
|
|
72
67
|
};
|
|
73
68
|
}
|
|
69
|
+
if (!stats.isDirectory) {
|
|
70
|
+
return {
|
|
71
|
+
title: params.pattern,
|
|
72
|
+
output: `Path is not a directory: ${params.path || '.'}`,
|
|
73
|
+
metadata: {
|
|
74
|
+
count: 0,
|
|
75
|
+
truncated: false
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
74
79
|
|
|
75
80
|
// Collect files matching the pattern
|
|
76
81
|
const files: Array<{ path: string; mtime: number }> = [];
|
|
77
82
|
let truncated = false;
|
|
78
83
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
hidden: true
|
|
83
|
-
})) {
|
|
84
|
+
const patternRegex = globToRegExp(params.pattern);
|
|
85
|
+
const allFiles = await VirtualFs.listFilesRecursive(searchPath, vfsId);
|
|
86
|
+
for (const file of allFiles) {
|
|
84
87
|
if (files.length >= MAX_RESULTS) {
|
|
85
88
|
truncated = true;
|
|
86
89
|
break;
|
|
87
90
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const stats = await fs.stat(fullPath);
|
|
93
|
-
files.push({
|
|
94
|
-
path: fullPath,
|
|
95
|
-
mtime: stats.mtime.getTime()
|
|
96
|
-
});
|
|
97
|
-
} catch {
|
|
98
|
-
// Skip files we can't stat
|
|
99
|
-
files.push({
|
|
100
|
-
path: fullPath,
|
|
101
|
-
mtime: 0
|
|
102
|
-
});
|
|
103
|
-
}
|
|
91
|
+
const relative = path.posix.relative(searchPath, file);
|
|
92
|
+
if (!patternRegex.test(relative)) continue;
|
|
93
|
+
const fileStats = await safeStat(file, vfsId);
|
|
94
|
+
files.push({ path: file, mtime: fileStats?.mtimeMs ?? 0 });
|
|
104
95
|
}
|
|
105
96
|
|
|
106
97
|
if (files.length === 0) {
|
|
@@ -118,7 +109,7 @@ export namespace GlobTool {
|
|
|
118
109
|
files.sort((a, b) => b.mtime - a.mtime);
|
|
119
110
|
|
|
120
111
|
// Format output with relative paths
|
|
121
|
-
const outputLines = files.map((f) => path.relative(basePath, f.path));
|
|
112
|
+
const outputLines = files.map((f) => path.posix.relative(basePath, f.path));
|
|
122
113
|
|
|
123
114
|
// Add truncation notice
|
|
124
115
|
if (truncated) {
|
|
@@ -137,4 +128,40 @@ export namespace GlobTool {
|
|
|
137
128
|
}
|
|
138
129
|
};
|
|
139
130
|
}
|
|
131
|
+
|
|
132
|
+
function globToRegExp(pattern: string): RegExp {
|
|
133
|
+
let regex = '^';
|
|
134
|
+
let i = 0;
|
|
135
|
+
while (i < pattern.length) {
|
|
136
|
+
const char = pattern[i] ?? '';
|
|
137
|
+
const next = pattern[i + 1] ?? '';
|
|
138
|
+
if (char === '*' && next === '*') {
|
|
139
|
+
regexAdd('.*');
|
|
140
|
+
i += 2;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (char === '*') {
|
|
144
|
+
regexAdd('[^/]*');
|
|
145
|
+
i += 1;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (char === '?') {
|
|
149
|
+
regexAdd('[^/]');
|
|
150
|
+
i += 1;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if ('\\.^$+{}()|[]'.includes(char)) {
|
|
154
|
+
regexAdd('\\' + char);
|
|
155
|
+
} else {
|
|
156
|
+
regexAdd(char);
|
|
157
|
+
}
|
|
158
|
+
i += 1;
|
|
159
|
+
}
|
|
160
|
+
regex += '$';
|
|
161
|
+
return new RegExp(regex);
|
|
162
|
+
|
|
163
|
+
function regexAdd(value: string) {
|
|
164
|
+
regex += value;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
140
167
|
}
|