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.
Files changed (42) hide show
  1. package/README.md +4 -1
  2. package/package.json +4 -2
  3. package/src/agent/agent.test.ts +114 -16
  4. package/src/agent/loop.ts +14 -11
  5. package/src/agent/service.ts +117 -86
  6. package/src/collections/index.ts +0 -0
  7. package/src/collections/service.ts +187 -57
  8. package/src/collections/types.ts +1 -0
  9. package/src/collections/virtual-metadata.ts +32 -0
  10. package/src/config/config.test.ts +0 -0
  11. package/src/config/index.ts +195 -127
  12. package/src/config/remote.ts +132 -79
  13. package/src/context/index.ts +0 -0
  14. package/src/context/transaction.ts +20 -15
  15. package/src/errors.ts +0 -0
  16. package/src/index.ts +29 -15
  17. package/src/metrics/index.ts +18 -13
  18. package/src/providers/auth.ts +38 -11
  19. package/src/providers/model.ts +3 -1
  20. package/src/providers/openrouter.ts +39 -0
  21. package/src/providers/registry.ts +2 -0
  22. package/src/resources/helpers.ts +0 -0
  23. package/src/resources/impls/git.test.ts +0 -0
  24. package/src/resources/impls/git.ts +160 -117
  25. package/src/resources/index.ts +0 -0
  26. package/src/resources/schema.ts +24 -27
  27. package/src/resources/service.ts +0 -0
  28. package/src/resources/types.ts +0 -0
  29. package/src/stream/index.ts +0 -0
  30. package/src/stream/service.ts +23 -14
  31. package/src/tools/context.ts +4 -0
  32. package/src/tools/glob.ts +72 -45
  33. package/src/tools/grep.ts +136 -57
  34. package/src/tools/index.ts +0 -2
  35. package/src/tools/list.ts +34 -53
  36. package/src/tools/read.ts +46 -32
  37. package/src/tools/virtual-sandbox.ts +103 -0
  38. package/src/validation/index.ts +12 -12
  39. package/src/vfs/virtual-fs.test.ts +107 -0
  40. package/src/vfs/virtual-fs.ts +273 -0
  41. package/src/tools/ripgrep.ts +0 -348
  42. package/src/tools/sandbox.ts +0 -164
@@ -1,13 +1,21 @@
1
- import { promises as fs } from 'node:fs';
2
1
  import path from 'node:path';
3
2
 
3
+ import { Result } from 'better-result';
4
+
4
5
  import { Config } from '../config/index.ts';
5
6
  import { Transaction } from '../context/transaction.ts';
6
7
  import { CommonHints, getErrorHint, getErrorMessage } from '../errors.ts';
7
8
  import { Metrics } from '../metrics/index.ts';
8
9
  import { Resources } from '../resources/service.ts';
10
+ import { isGitResource } from '../resources/schema.ts';
9
11
  import { FS_RESOURCE_SYSTEM_NOTE, type BtcaFsResource } from '../resources/types.ts';
10
12
  import { CollectionError, getCollectionKey, type CollectionResult } from './types.ts';
13
+ import { VirtualFs } from '../vfs/virtual-fs.ts';
14
+ import {
15
+ clearVirtualCollectionMetadata,
16
+ setVirtualCollectionMetadata,
17
+ type VirtualResourceMetadata
18
+ } from './virtual-metadata.ts';
11
19
 
12
20
  export namespace Collections {
13
21
  export type Service = {
@@ -32,6 +40,126 @@ export namespace Collections {
32
40
  return lines.join('\n');
33
41
  };
34
42
 
43
+ const ignoreErrors = async (action: () => Promise<unknown>) => {
44
+ const result = await Result.tryPromise(action);
45
+ result.match({
46
+ ok: () => undefined,
47
+ err: () => undefined
48
+ });
49
+ };
50
+
51
+ const initVirtualRoot = (collectionPath: string, vfsId: string) =>
52
+ Result.tryPromise({
53
+ try: () => VirtualFs.mkdir(collectionPath, { recursive: true }, vfsId),
54
+ catch: (cause) =>
55
+ new CollectionError({
56
+ message: `Failed to initialize virtual collection root: "${collectionPath}"`,
57
+ hint: 'Check that the virtual filesystem is available.',
58
+ cause
59
+ })
60
+ });
61
+
62
+ const loadResource = (resources: Resources.Service, name: string, quiet: boolean) =>
63
+ Result.tryPromise({
64
+ try: () => resources.load(name, { quiet }),
65
+ catch: (cause) => {
66
+ const underlyingHint = getErrorHint(cause);
67
+ const underlyingMessage = getErrorMessage(cause);
68
+ return new CollectionError({
69
+ message: `Failed to load resource "${name}": ${underlyingMessage}`,
70
+ hint:
71
+ underlyingHint ??
72
+ `${CommonHints.CLEAR_CACHE} Check that the resource "${name}" is correctly configured.`,
73
+ cause
74
+ });
75
+ }
76
+ });
77
+
78
+ const resolveResourcePath = (resource: BtcaFsResource) =>
79
+ Result.tryPromise({
80
+ try: () => resource.getAbsoluteDirectoryPath(),
81
+ catch: (cause) =>
82
+ new CollectionError({
83
+ message: `Failed to get path for resource "${resource.name}"`,
84
+ hint: CommonHints.CLEAR_CACHE,
85
+ cause
86
+ })
87
+ });
88
+
89
+ const virtualizeResource = (args: {
90
+ resource: BtcaFsResource;
91
+ resourcePath: string;
92
+ virtualResourcePath: string;
93
+ vfsId: string;
94
+ }) =>
95
+ Result.tryPromise({
96
+ try: () =>
97
+ VirtualFs.importDirectoryFromDisk({
98
+ sourcePath: args.resourcePath,
99
+ destinationPath: args.virtualResourcePath,
100
+ vfsId: args.vfsId,
101
+ ignore: (relativePath) => {
102
+ const normalized = relativePath.split(path.sep).join('/');
103
+ return (
104
+ normalized === '.git' ||
105
+ normalized.startsWith('.git/') ||
106
+ normalized.includes('/.git/')
107
+ );
108
+ }
109
+ }),
110
+ catch: (cause) =>
111
+ new CollectionError({
112
+ message: `Failed to virtualize resource "${args.resource.name}"`,
113
+ hint: CommonHints.CLEAR_CACHE,
114
+ cause
115
+ })
116
+ });
117
+
118
+ const getGitHeadHash = async (resourcePath: string) => {
119
+ const result = await Result.tryPromise(async () => {
120
+ const proc = Bun.spawn(['git', 'rev-parse', 'HEAD'], {
121
+ cwd: resourcePath,
122
+ stdout: 'pipe',
123
+ stderr: 'pipe'
124
+ });
125
+ const stdout = await new Response(proc.stdout).text();
126
+ const exitCode = await proc.exited;
127
+ if (exitCode !== 0) return undefined;
128
+ const trimmed = stdout.trim();
129
+ return trimmed.length > 0 ? trimmed : undefined;
130
+ });
131
+
132
+ return result.match({
133
+ ok: (value) => value,
134
+ err: () => undefined
135
+ });
136
+ };
137
+
138
+ const buildVirtualMetadata = async (args: {
139
+ resource: BtcaFsResource;
140
+ resourcePath: string;
141
+ loadedAt: string;
142
+ definition?: ReturnType<Config.Service['getResource']>;
143
+ }) => {
144
+ if (!args.definition) return null;
145
+ const base = {
146
+ name: args.resource.name,
147
+ fsName: args.resource.fsName,
148
+ type: args.resource.type,
149
+ path: args.resourcePath,
150
+ repoSubPaths: args.resource.repoSubPaths,
151
+ loadedAt: args.loadedAt
152
+ };
153
+ if (!isGitResource(args.definition)) return base;
154
+ const commit = await getGitHeadHash(args.resourcePath);
155
+ return {
156
+ ...base,
157
+ url: args.definition.url,
158
+ branch: args.definition.branch,
159
+ commit
160
+ };
161
+ };
162
+
35
163
  export const create = (args: {
36
164
  config: Config.Service;
37
165
  resources: Resources.Service;
@@ -50,72 +178,74 @@ export namespace Collections {
50
178
 
51
179
  const sortedNames = [...uniqueNames].sort((a, b) => a.localeCompare(b));
52
180
  const key = getCollectionKey(sortedNames);
53
- const collectionPath = path.join(args.config.collectionsDirectory, key);
181
+ const collectionPath = '/';
182
+ const vfsId = VirtualFs.create();
183
+ const cleanupVirtual = () => {
184
+ VirtualFs.dispose(vfsId);
185
+ clearVirtualCollectionMetadata(vfsId);
186
+ };
54
187
 
55
- try {
56
- await fs.mkdir(collectionPath, { recursive: true });
57
- } catch (cause) {
58
- throw new CollectionError({
59
- message: `Failed to create collection directory: "${collectionPath}"`,
60
- hint: 'Check that you have write permissions to the btca data directory.',
61
- cause
62
- });
63
- }
188
+ const result = await Result.gen(async function* () {
189
+ yield* Result.await(initVirtualRoot(collectionPath, vfsId));
64
190
 
65
- const loadedResources: BtcaFsResource[] = [];
66
- for (const name of sortedNames) {
67
- try {
68
- loadedResources.push(await args.resources.load(name, { quiet }));
69
- } catch (cause) {
70
- // Preserve the hint from the underlying error if available
71
- const underlyingHint = getErrorHint(cause);
72
- const underlyingMessage = getErrorMessage(cause);
73
- throw new CollectionError({
74
- message: `Failed to load resource "${name}": ${underlyingMessage}`,
75
- hint:
76
- underlyingHint ??
77
- `${CommonHints.CLEAR_CACHE} Check that the resource "${name}" is correctly configured.`,
78
- cause
79
- });
191
+ const loadedResources: BtcaFsResource[] = [];
192
+ for (const name of sortedNames) {
193
+ const resource = yield* Result.await(loadResource(args.resources, name, quiet));
194
+ loadedResources.push(resource);
80
195
  }
81
- }
82
196
 
83
- for (const resource of loadedResources) {
84
- let resourcePath: string;
85
- try {
86
- resourcePath = await resource.getAbsoluteDirectoryPath();
87
- } catch (cause) {
88
- throw new CollectionError({
89
- message: `Failed to get path for resource "${resource.name}"`,
90
- hint: CommonHints.CLEAR_CACHE,
91
- cause
92
- });
93
- }
197
+ const metadataResources: VirtualResourceMetadata[] = [];
198
+ const loadedAt = new Date().toISOString();
199
+ for (const resource of loadedResources) {
200
+ const resourcePath = yield* Result.await(resolveResourcePath(resource));
201
+ const virtualResourcePath = path.posix.join('/', resource.fsName);
94
202
 
95
- const linkPath = path.join(collectionPath, resource.fsName);
96
- try {
97
- await fs.rm(linkPath, { recursive: true, force: true });
98
- } catch {
99
- // ignore
100
- }
203
+ await ignoreErrors(() =>
204
+ VirtualFs.rm(virtualResourcePath, { recursive: true, force: true }, vfsId)
205
+ );
206
+
207
+ yield* Result.await(
208
+ virtualizeResource({
209
+ resource,
210
+ resourcePath,
211
+ virtualResourcePath,
212
+ vfsId
213
+ })
214
+ );
101
215
 
102
- try {
103
- await fs.symlink(resourcePath, linkPath, 'junction');
104
- } catch (cause) {
105
- throw new CollectionError({
106
- message: `Failed to create symlink for resource "${resource.name}"`,
107
- hint: 'This may be a filesystem permissions issue or the link already exists.',
108
- cause
216
+ const definition = args.config.getResource(resource.name);
217
+ const metadata = await buildVirtualMetadata({
218
+ resource,
219
+ resourcePath,
220
+ loadedAt,
221
+ definition
109
222
  });
223
+ if (metadata) metadataResources.push(metadata);
110
224
  }
111
- }
112
225
 
113
- const instructionBlocks = loadedResources.map(createCollectionInstructionBlock);
226
+ setVirtualCollectionMetadata({
227
+ vfsId,
228
+ collectionKey: key,
229
+ createdAt: loadedAt,
230
+ resources: metadataResources
231
+ });
114
232
 
115
- return {
116
- path: collectionPath,
117
- agentInstructions: instructionBlocks.join('\n\n')
118
- };
233
+ const instructionBlocks = loadedResources.map(createCollectionInstructionBlock);
234
+
235
+ return Result.ok({
236
+ path: collectionPath,
237
+ agentInstructions: instructionBlocks.join('\n\n'),
238
+ vfsId
239
+ });
240
+ });
241
+
242
+ return result.match({
243
+ ok: (value) => value,
244
+ err: (error) => {
245
+ cleanupVirtual();
246
+ throw error;
247
+ }
248
+ });
119
249
  })
120
250
  };
121
251
  };
@@ -4,6 +4,7 @@ import { resourceNameToKey } from '../resources/helpers.ts';
4
4
  export type CollectionResult = {
5
5
  path: string;
6
6
  agentInstructions: string;
7
+ vfsId?: string;
7
8
  };
8
9
 
9
10
  export class CollectionError extends Error {
@@ -0,0 +1,32 @@
1
+ export type VirtualResourceMetadata = {
2
+ name: string;
3
+ fsName: string;
4
+ type: 'git' | 'local';
5
+ path: string;
6
+ repoSubPaths: readonly string[];
7
+ url?: string;
8
+ branch?: string;
9
+ commit?: string;
10
+ loadedAt: string;
11
+ };
12
+
13
+ export type VirtualCollectionMetadata = {
14
+ vfsId: string;
15
+ collectionKey: string;
16
+ createdAt: string;
17
+ resources: VirtualResourceMetadata[];
18
+ };
19
+
20
+ const metadataByVfsId = new Map<string, VirtualCollectionMetadata>();
21
+
22
+ export const setVirtualCollectionMetadata = (metadata: VirtualCollectionMetadata) => {
23
+ metadataByVfsId.set(metadata.vfsId, metadata);
24
+ };
25
+
26
+ export const getVirtualCollectionMetadata = (vfsId: string) => metadataByVfsId.get(vfsId);
27
+
28
+ export const clearVirtualCollectionMetadata = (vfsId: string) => metadataByVfsId.delete(vfsId);
29
+
30
+ export const clearAllVirtualCollectionMetadata = () => {
31
+ metadataByVfsId.clear();
32
+ };
File without changes