btca-server 1.0.962 → 2.0.1

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 (43) hide show
  1. package/package.json +3 -3
  2. package/src/agent/agent.test.ts +31 -24
  3. package/src/agent/index.ts +8 -2
  4. package/src/agent/loop.ts +303 -346
  5. package/src/agent/service.ts +252 -233
  6. package/src/agent/types.ts +2 -2
  7. package/src/collections/index.ts +2 -1
  8. package/src/collections/service.ts +352 -345
  9. package/src/config/config.test.ts +3 -1
  10. package/src/config/index.ts +615 -727
  11. package/src/config/remote.ts +214 -369
  12. package/src/context/index.ts +6 -12
  13. package/src/context/transaction.ts +23 -30
  14. package/src/effect/errors.ts +45 -0
  15. package/src/effect/layers.ts +26 -0
  16. package/src/effect/runtime.ts +19 -0
  17. package/src/effect/services.ts +154 -0
  18. package/src/index.ts +291 -369
  19. package/src/metrics/index.ts +46 -46
  20. package/src/pricing/models-dev.ts +104 -106
  21. package/src/providers/auth.ts +159 -200
  22. package/src/providers/index.ts +19 -2
  23. package/src/providers/model.ts +115 -135
  24. package/src/providers/openai.ts +3 -3
  25. package/src/resources/impls/git.ts +123 -146
  26. package/src/resources/impls/npm.test.ts +16 -5
  27. package/src/resources/impls/npm.ts +66 -75
  28. package/src/resources/index.ts +6 -1
  29. package/src/resources/schema.ts +7 -6
  30. package/src/resources/service.test.ts +13 -12
  31. package/src/resources/service.ts +153 -112
  32. package/src/stream/index.ts +1 -1
  33. package/src/stream/service.test.ts +5 -5
  34. package/src/stream/service.ts +282 -293
  35. package/src/tools/glob.ts +126 -141
  36. package/src/tools/grep.ts +205 -210
  37. package/src/tools/index.ts +8 -4
  38. package/src/tools/list.ts +118 -140
  39. package/src/tools/read.ts +209 -235
  40. package/src/tools/virtual-sandbox.ts +91 -83
  41. package/src/validation/index.ts +18 -22
  42. package/src/vfs/virtual-fs.test.ts +37 -25
  43. package/src/vfs/virtual-fs.ts +218 -216
@@ -1,381 +1,388 @@
1
1
  import path from 'node:path';
2
2
 
3
- import { Result } from 'better-result';
3
+ import { Effect } from 'effect';
4
4
 
5
- import { Config } from '../config/index.ts';
6
- import { Transaction } from '../context/transaction.ts';
5
+ import type { ConfigService as ConfigServiceShape } from '../config/index.ts';
6
+ import { runTransaction } from '../context/transaction.ts';
7
7
  import { CommonHints, getErrorHint, getErrorMessage } from '../errors.ts';
8
- import { Metrics } from '../metrics/index.ts';
9
- import { Resources } from '../resources/service.ts';
8
+ import { metricsInfo } from '../metrics/index.ts';
9
+ import type { ResourcesService } from '../resources/service.ts';
10
10
  import { isGitResource, isNpmResource } from '../resources/schema.ts';
11
11
  import { FS_RESOURCE_SYSTEM_NOTE, type BtcaFsResource } from '../resources/types.ts';
12
12
  import { parseNpmReference } from '../validation/index.ts';
13
13
  import { CollectionError, getCollectionKey, type CollectionResult } from './types.ts';
14
- import { VirtualFs } from '../vfs/virtual-fs.ts';
14
+ import {
15
+ createVirtualFs,
16
+ disposeVirtualFs,
17
+ importDirectoryIntoVirtualFs,
18
+ mkdirVirtualFs,
19
+ rmVirtualFs
20
+ } from '../vfs/virtual-fs.ts';
15
21
  import {
16
22
  clearVirtualCollectionMetadata,
17
23
  setVirtualCollectionMetadata,
18
24
  type VirtualResourceMetadata
19
25
  } from './virtual-metadata.ts';
20
26
 
21
- export namespace Collections {
22
- export type Service = {
23
- load: (args: {
24
- resourceNames: readonly string[];
25
- quiet?: boolean;
26
- }) => Promise<CollectionResult>;
27
- };
28
-
29
- const encodePathSegments = (value: string) => value.split('/').map(encodeURIComponent).join('/');
30
-
31
- const trimGitSuffix = (url: string) => url.replace(/\.git$/u, '').replace(/\/+$/u, '');
32
- const getNpmCitationAlias = (metadata?: VirtualResourceMetadata) => {
33
- if (!metadata?.package) return undefined;
34
- return `npm:${metadata.package}@${metadata.version ?? 'latest'}`;
35
- };
36
-
37
- const createCollectionInstructionBlock = (
38
- resource: BtcaFsResource,
39
- metadata?: VirtualResourceMetadata
40
- ): string => {
41
- const focusLines = resource.repoSubPaths.map(
42
- (subPath) => `Focus: ./${resource.fsName}/${subPath}`
43
- );
44
- const gitRef = metadata?.branch ?? metadata?.commit;
45
- const githubPrefix =
46
- resource.type === 'git' && metadata?.url && gitRef
47
- ? `${trimGitSuffix(metadata.url)}/blob/${encodeURIComponent(gitRef)}`
48
- : undefined;
49
- const npmCitationAlias = resource.type === 'npm' ? getNpmCitationAlias(metadata) : undefined;
50
- const lines = [
51
- `## Resource: ${resource.name}`,
52
- FS_RESOURCE_SYSTEM_NOTE,
53
- `Path: ./${resource.fsName}`,
54
- resource.type === 'git' && metadata?.url ? `Repo URL: ${trimGitSuffix(metadata.url)}` : '',
55
- resource.type === 'git' && metadata?.branch ? `Repo Branch: ${metadata.branch}` : '',
56
- resource.type === 'git' && metadata?.commit ? `Repo Commit: ${metadata.commit}` : '',
57
- resource.type === 'npm' && metadata?.package ? `NPM Package: ${metadata.package}` : '',
58
- resource.type === 'npm' && metadata?.version ? `NPM Version: ${metadata.version}` : '',
59
- resource.type === 'npm' && metadata?.url ? `NPM URL: ${metadata.url}` : '',
60
- npmCitationAlias ? `NPM Citation Alias: ${npmCitationAlias}` : '',
61
- githubPrefix ? `GitHub Blob Prefix: ${githubPrefix}` : '',
62
- githubPrefix
63
- ? `GitHub Citation Rule: Convert virtual paths under ./${resource.fsName}/ to repo-relative paths, then encode each path segment for GitHub URLs (example segment: "+page.server.js" -> "${encodeURIComponent('+page.server.js')}").`
64
- : '',
65
- githubPrefix
66
- ? `GitHub Citation Example: ${githubPrefix}/${encodePathSegments('src/routes/blog/+page.server.js')}`
67
- : '',
68
- resource.type !== 'git'
69
- ? 'Citation Rule: Cite local file paths only for this resource (no GitHub URL).'
70
- : '',
71
- npmCitationAlias
72
- ? `NPM Citation Rule: In "Sources", cite npm files using "${npmCitationAlias}/<file>" (for example, "${npmCitationAlias}/README.md"). Do not cite encoded virtual folder names.`
73
- : '',
74
- ...focusLines,
75
- resource.specialAgentInstructions ? `Notes: ${resource.specialAgentInstructions}` : ''
76
- ].filter(Boolean);
77
-
78
- return lines.join('\n');
79
- };
80
-
81
- const ignoreErrors = async (action: () => Promise<unknown>) => {
82
- const result = await Result.tryPromise(action);
83
- result.match({
84
- ok: () => undefined,
85
- err: () => undefined
27
+ export type CollectionsService = {
28
+ load: (args: { resourceNames: readonly string[]; quiet?: boolean }) => Promise<CollectionResult>;
29
+ loadEffect: (args: {
30
+ resourceNames: readonly string[];
31
+ quiet?: boolean;
32
+ }) => Effect.Effect<CollectionResult, CollectionError>;
33
+ };
34
+
35
+ const encodePathSegments = (value: string) => value.split('/').map(encodeURIComponent).join('/');
36
+
37
+ const trimGitSuffix = (url: string) => url.replace(/\.git$/u, '').replace(/\/+$/u, '');
38
+ const getNpmCitationAlias = (metadata?: VirtualResourceMetadata) => {
39
+ if (!metadata?.package) return undefined;
40
+ return `npm:${metadata.package}@${metadata.version ?? 'latest'}`;
41
+ };
42
+
43
+ const createCollectionInstructionBlock = (
44
+ resource: BtcaFsResource,
45
+ metadata?: VirtualResourceMetadata
46
+ ): string => {
47
+ const focusLines = resource.repoSubPaths.map(
48
+ (subPath) => `Focus: ./${resource.fsName}/${subPath}`
49
+ );
50
+ const gitRef = metadata?.branch ?? metadata?.commit;
51
+ const githubPrefix =
52
+ resource.type === 'git' && metadata?.url && gitRef
53
+ ? `${trimGitSuffix(metadata.url)}/blob/${encodeURIComponent(gitRef)}`
54
+ : undefined;
55
+ const npmCitationAlias = resource.type === 'npm' ? getNpmCitationAlias(metadata) : undefined;
56
+ const lines = [
57
+ `## Resource: ${resource.name}`,
58
+ FS_RESOURCE_SYSTEM_NOTE,
59
+ `Path: ./${resource.fsName}`,
60
+ resource.type === 'git' && metadata?.url ? `Repo URL: ${trimGitSuffix(metadata.url)}` : '',
61
+ resource.type === 'git' && metadata?.branch ? `Repo Branch: ${metadata.branch}` : '',
62
+ resource.type === 'git' && metadata?.commit ? `Repo Commit: ${metadata.commit}` : '',
63
+ resource.type === 'npm' && metadata?.package ? `NPM Package: ${metadata.package}` : '',
64
+ resource.type === 'npm' && metadata?.version ? `NPM Version: ${metadata.version}` : '',
65
+ resource.type === 'npm' && metadata?.url ? `NPM URL: ${metadata.url}` : '',
66
+ npmCitationAlias ? `NPM Citation Alias: ${npmCitationAlias}` : '',
67
+ githubPrefix ? `GitHub Blob Prefix: ${githubPrefix}` : '',
68
+ githubPrefix
69
+ ? `GitHub Citation Rule: Convert virtual paths under ./${resource.fsName}/ to repo-relative paths, then encode each path segment for GitHub URLs (example segment: "+page.server.js" -> "${encodeURIComponent('+page.server.js')}").`
70
+ : '',
71
+ githubPrefix
72
+ ? `GitHub Citation Example: ${githubPrefix}/${encodePathSegments('src/routes/blog/+page.server.js')}`
73
+ : '',
74
+ resource.type !== 'git'
75
+ ? 'Citation Rule: Cite local file paths only for this resource (no GitHub URL).'
76
+ : '',
77
+ npmCitationAlias
78
+ ? `NPM Citation Rule: In "Sources", cite npm files using "${npmCitationAlias}/<file>" (for example, "${npmCitationAlias}/README.md"). Do not cite encoded virtual folder names.`
79
+ : '',
80
+ ...focusLines,
81
+ resource.specialAgentInstructions ? `Notes: ${resource.specialAgentInstructions}` : ''
82
+ ].filter(Boolean);
83
+
84
+ return lines.join('\n');
85
+ };
86
+
87
+ const ignoreErrors = async (action: () => Promise<unknown>) => {
88
+ try {
89
+ await action();
90
+ } catch {
91
+ return;
92
+ }
93
+ };
94
+
95
+ const initVirtualRoot = async (collectionPath: string, vfsId: string) => {
96
+ try {
97
+ await mkdirVirtualFs(collectionPath, { recursive: true }, vfsId);
98
+ } catch (cause) {
99
+ throw new CollectionError({
100
+ message: `Failed to initialize virtual collection root: "${collectionPath}"`,
101
+ hint: 'Check that the virtual filesystem is available.',
102
+ cause
86
103
  });
87
- };
88
-
89
- const initVirtualRoot = (collectionPath: string, vfsId: string) =>
90
- Result.tryPromise({
91
- try: () => VirtualFs.mkdir(collectionPath, { recursive: true }, vfsId),
92
- catch: (cause) =>
93
- new CollectionError({
94
- message: `Failed to initialize virtual collection root: "${collectionPath}"`,
95
- hint: 'Check that the virtual filesystem is available.',
96
- cause
97
- })
104
+ }
105
+ };
106
+
107
+ const loadResource = async (resources: ResourcesService, name: string, quiet: boolean) => {
108
+ try {
109
+ return await resources.load(name, { quiet });
110
+ } catch (cause) {
111
+ const underlyingHint = getErrorHint(cause);
112
+ const underlyingMessage = getErrorMessage(cause);
113
+ throw new CollectionError({
114
+ message: `Failed to load resource "${name}": ${underlyingMessage}`,
115
+ hint:
116
+ underlyingHint ??
117
+ `${CommonHints.CLEAR_CACHE} Check that the resource "${name}" is correctly configured.`,
118
+ cause
98
119
  });
99
-
100
- const loadResource = (resources: Resources.Service, name: string, quiet: boolean) =>
101
- Result.tryPromise({
102
- try: () => resources.load(name, { quiet }),
103
- catch: (cause) => {
104
- const underlyingHint = getErrorHint(cause);
105
- const underlyingMessage = getErrorMessage(cause);
106
- return new CollectionError({
107
- message: `Failed to load resource "${name}": ${underlyingMessage}`,
108
- hint:
109
- underlyingHint ??
110
- `${CommonHints.CLEAR_CACHE} Check that the resource "${name}" is correctly configured.`,
111
- cause
112
- });
113
- }
120
+ }
121
+ };
122
+
123
+ const resolveResourcePath = async (resource: BtcaFsResource) => {
124
+ try {
125
+ return await resource.getAbsoluteDirectoryPath();
126
+ } catch (cause) {
127
+ throw new CollectionError({
128
+ message: `Failed to get path for resource "${resource.name}"`,
129
+ hint: CommonHints.CLEAR_CACHE,
130
+ cause
114
131
  });
115
-
116
- const resolveResourcePath = (resource: BtcaFsResource) =>
117
- Result.tryPromise({
118
- try: () => resource.getAbsoluteDirectoryPath(),
119
- catch: (cause) =>
120
- new CollectionError({
121
- message: `Failed to get path for resource "${resource.name}"`,
122
- hint: CommonHints.CLEAR_CACHE,
123
- cause
124
- })
132
+ }
133
+ };
134
+
135
+ const virtualizeResource = async (args: {
136
+ resource: BtcaFsResource;
137
+ resourcePath: string;
138
+ virtualResourcePath: string;
139
+ vfsId: string;
140
+ }) => {
141
+ try {
142
+ await importDirectoryIntoVirtualFs({
143
+ sourcePath: args.resourcePath,
144
+ destinationPath: args.virtualResourcePath,
145
+ vfsId: args.vfsId,
146
+ ignore: (relativePath) => {
147
+ const normalized = relativePath.split(path.sep).join('/');
148
+ return (
149
+ normalized === '.git' || normalized.startsWith('.git/') || normalized.includes('/.git/')
150
+ );
151
+ }
125
152
  });
126
-
127
- const virtualizeResource = (args: {
128
- resource: BtcaFsResource;
129
- resourcePath: string;
130
- virtualResourcePath: string;
131
- vfsId: string;
132
- }) =>
133
- Result.tryPromise({
134
- try: () =>
135
- VirtualFs.importDirectoryFromDisk({
136
- sourcePath: args.resourcePath,
137
- destinationPath: args.virtualResourcePath,
138
- vfsId: args.vfsId,
139
- ignore: (relativePath) => {
140
- const normalized = relativePath.split(path.sep).join('/');
141
- return (
142
- normalized === '.git' ||
143
- normalized.startsWith('.git/') ||
144
- normalized.includes('/.git/')
145
- );
146
- }
147
- }),
148
- catch: (cause) =>
149
- new CollectionError({
150
- message: `Failed to virtualize resource "${args.resource.name}"`,
151
- hint: CommonHints.CLEAR_CACHE,
152
- cause
153
- })
153
+ } catch (cause) {
154
+ throw new CollectionError({
155
+ message: `Failed to virtualize resource "${args.resource.name}"`,
156
+ hint: CommonHints.CLEAR_CACHE,
157
+ cause
154
158
  });
155
-
156
- const getGitHeadHash = async (resourcePath: string) => {
157
- const result = await Result.tryPromise(async () => {
158
- const proc = Bun.spawn(['git', 'rev-parse', 'HEAD'], {
159
- cwd: resourcePath,
160
- stdout: 'pipe',
161
- stderr: 'pipe'
162
- });
163
- const stdout = await new Response(proc.stdout).text();
164
- const exitCode = await proc.exited;
165
- if (exitCode !== 0) return undefined;
166
- const trimmed = stdout.trim();
167
- return trimmed.length > 0 ? trimmed : undefined;
159
+ }
160
+ };
161
+
162
+ const getGitHeadHash = async (resourcePath: string) => {
163
+ try {
164
+ const proc = Bun.spawn(['git', 'rev-parse', 'HEAD'], {
165
+ cwd: resourcePath,
166
+ stdout: 'pipe',
167
+ stderr: 'pipe'
168
168
  });
169
-
170
- return result.match({
171
- ok: (value) => value,
172
- err: () => undefined
169
+ const stdout = await new Response(proc.stdout).text();
170
+ const exitCode = await proc.exited;
171
+ if (exitCode !== 0) return undefined;
172
+ const trimmed = stdout.trim();
173
+ return trimmed.length > 0 ? trimmed : undefined;
174
+ } catch {
175
+ return undefined;
176
+ }
177
+ };
178
+
179
+ const getGitHeadBranch = async (resourcePath: string) => {
180
+ try {
181
+ const proc = Bun.spawn(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], {
182
+ cwd: resourcePath,
183
+ stdout: 'pipe',
184
+ stderr: 'pipe'
173
185
  });
186
+ const stdout = await new Response(proc.stdout).text();
187
+ const exitCode = await proc.exited;
188
+ if (exitCode !== 0) return undefined;
189
+ const trimmed = stdout.trim();
190
+ if (!trimmed || trimmed === 'HEAD') return undefined;
191
+ return trimmed;
192
+ } catch {
193
+ return undefined;
194
+ }
195
+ };
196
+
197
+ const ANON_PREFIX = 'anonymous:';
198
+ const getAnonymousUrlFromName = (name: string) =>
199
+ name.startsWith(ANON_PREFIX) ? name.slice(ANON_PREFIX.length) : undefined;
200
+ const NPM_ANON_PREFIX = `${ANON_PREFIX}npm:`;
201
+ const NPM_META_FILE = '.btca-npm-meta.json';
202
+ const getAnonymousNpmReferenceFromName = (name: string) =>
203
+ name.startsWith(NPM_ANON_PREFIX) ? name.slice(ANON_PREFIX.length) : undefined;
204
+
205
+ const readNpmMeta = async (resourcePath: string) => {
206
+ try {
207
+ const content = await Bun.file(path.join(resourcePath, NPM_META_FILE)).text();
208
+ return JSON.parse(content) as {
209
+ packageName?: string;
210
+ resolvedVersion?: string;
211
+ packageUrl?: string;
212
+ };
213
+ } catch {
214
+ return null;
215
+ }
216
+ };
217
+
218
+ const buildVirtualMetadata = async (args: {
219
+ resource: BtcaFsResource;
220
+ resourcePath: string;
221
+ loadedAt: string;
222
+ definition?: ReturnType<ConfigServiceShape['getResource']>;
223
+ }) => {
224
+ const base = {
225
+ name: args.resource.name,
226
+ fsName: args.resource.fsName,
227
+ type: args.resource.type,
228
+ path: args.resourcePath,
229
+ repoSubPaths: args.resource.repoSubPaths,
230
+ loadedAt: args.loadedAt
174
231
  };
175
232
 
176
- const getGitHeadBranch = async (resourcePath: string) => {
177
- const result = await Result.tryPromise(async () => {
178
- const proc = Bun.spawn(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], {
179
- cwd: resourcePath,
180
- stdout: 'pipe',
181
- stderr: 'pipe'
182
- });
183
- const stdout = await new Response(proc.stdout).text();
184
- const exitCode = await proc.exited;
185
- if (exitCode !== 0) return undefined;
186
- const trimmed = stdout.trim();
187
- if (!trimmed || trimmed === 'HEAD') return undefined;
188
- return trimmed;
189
- });
233
+ if (args.resource.type === 'npm') {
234
+ const configuredDefinition =
235
+ args.definition && isNpmResource(args.definition) ? args.definition : null;
236
+ const anonymousReference = getAnonymousNpmReferenceFromName(args.resource.name);
237
+ const anonymousNpm = anonymousReference ? parseNpmReference(anonymousReference) : null;
238
+ const cached = await readNpmMeta(args.resourcePath);
239
+ const packageName =
240
+ configuredDefinition?.package ?? cached?.packageName ?? anonymousNpm?.packageName;
241
+ const version =
242
+ configuredDefinition?.version ?? cached?.resolvedVersion ?? anonymousNpm?.version;
243
+ const url = cached?.packageUrl ?? anonymousNpm?.packageUrl;
190
244
 
191
- return result.match({
192
- ok: (value) => value,
193
- err: () => undefined
194
- });
195
- };
245
+ return {
246
+ ...base,
247
+ ...(packageName ? { package: packageName } : {}),
248
+ ...(version ? { version } : {}),
249
+ ...(url ? { url } : {})
250
+ };
251
+ }
196
252
 
197
- const ANON_PREFIX = 'anonymous:';
198
- const getAnonymousUrlFromName = (name: string) =>
199
- name.startsWith(ANON_PREFIX) ? name.slice(ANON_PREFIX.length) : undefined;
200
- const NPM_ANON_PREFIX = `${ANON_PREFIX}npm:`;
201
- const NPM_META_FILE = '.btca-npm-meta.json';
202
- const getAnonymousNpmReferenceFromName = (name: string) =>
203
- name.startsWith(NPM_ANON_PREFIX) ? name.slice(ANON_PREFIX.length) : undefined;
204
-
205
- const readNpmMeta = async (resourcePath: string) => {
206
- const result = await Result.gen(async function* () {
207
- const content = yield* Result.await(
208
- Result.tryPromise(() => Bun.file(path.join(resourcePath, NPM_META_FILE)).text())
209
- );
210
- const parsed = yield* Result.try(
211
- () =>
212
- JSON.parse(content) as {
213
- packageName?: string;
214
- resolvedVersion?: string;
215
- packageUrl?: string;
216
- }
217
- );
218
- return Result.ok(parsed);
219
- });
253
+ if (args.resource.type !== 'git') return base;
220
254
 
221
- return result.match({
222
- ok: (value) => value,
223
- err: () => null
224
- });
255
+ const configuredDefinition =
256
+ args.definition && isGitResource(args.definition) ? args.definition : null;
257
+ const url = configuredDefinition?.url ?? getAnonymousUrlFromName(args.resource.name);
258
+ const branch = configuredDefinition?.branch ?? (await getGitHeadBranch(args.resourcePath));
259
+ const commit = await getGitHeadHash(args.resourcePath);
260
+
261
+ return {
262
+ ...base,
263
+ ...(url ? { url } : {}),
264
+ ...(branch ? { branch } : {}),
265
+ ...(commit ? { commit } : {})
225
266
  };
267
+ };
268
+
269
+ export const createCollectionsService = (args: {
270
+ config: ConfigServiceShape;
271
+ resources: ResourcesService;
272
+ }): CollectionsService => {
273
+ const load: CollectionsService['load'] = ({ resourceNames, quiet = false }) =>
274
+ runTransaction('collections.load', async () => {
275
+ const uniqueNames = Array.from(new Set(resourceNames));
276
+ if (uniqueNames.length === 0)
277
+ throw new CollectionError({
278
+ message: 'Cannot create collection with no resources',
279
+ hint: `${CommonHints.LIST_RESOURCES} ${CommonHints.ADD_RESOURCE}`
280
+ });
226
281
 
227
- const buildVirtualMetadata = async (args: {
228
- resource: BtcaFsResource;
229
- resourcePath: string;
230
- loadedAt: string;
231
- definition?: ReturnType<Config.Service['getResource']>;
232
- }) => {
233
- const base = {
234
- name: args.resource.name,
235
- fsName: args.resource.fsName,
236
- type: args.resource.type,
237
- path: args.resourcePath,
238
- repoSubPaths: args.resource.repoSubPaths,
239
- loadedAt: args.loadedAt
240
- };
282
+ metricsInfo('collections.load', { resources: uniqueNames, quiet });
241
283
 
242
- if (args.resource.type === 'npm') {
243
- const configuredDefinition =
244
- args.definition && isNpmResource(args.definition) ? args.definition : null;
245
- const anonymousReference = getAnonymousNpmReferenceFromName(args.resource.name);
246
- const anonymousNpm = anonymousReference ? parseNpmReference(anonymousReference) : null;
247
- const cached = await readNpmMeta(args.resourcePath);
248
- const packageName =
249
- configuredDefinition?.package ?? cached?.packageName ?? anonymousNpm?.packageName;
250
- const version =
251
- configuredDefinition?.version ?? cached?.resolvedVersion ?? anonymousNpm?.version;
252
- const url = cached?.packageUrl ?? anonymousNpm?.packageUrl;
253
-
254
- return {
255
- ...base,
256
- ...(packageName ? { package: packageName } : {}),
257
- ...(version ? { version } : {}),
258
- ...(url ? { url } : {})
284
+ const sortedNames = [...uniqueNames].sort((a, b) => a.localeCompare(b));
285
+ const key = getCollectionKey(sortedNames);
286
+ const collectionPath = '/';
287
+ const vfsId = createVirtualFs();
288
+ const cleanupVirtual = () => {
289
+ disposeVirtualFs(vfsId);
290
+ clearVirtualCollectionMetadata(vfsId);
259
291
  };
260
- }
261
-
262
- if (args.resource.type !== 'git') return base;
263
-
264
- const configuredDefinition =
265
- args.definition && isGitResource(args.definition) ? args.definition : null;
266
- const url = configuredDefinition?.url ?? getAnonymousUrlFromName(args.resource.name);
267
- const branch = configuredDefinition?.branch ?? (await getGitHeadBranch(args.resourcePath));
268
- const commit = await getGitHeadHash(args.resourcePath);
269
-
270
- return {
271
- ...base,
272
- ...(url ? { url } : {}),
273
- ...(branch ? { branch } : {}),
274
- ...(commit ? { commit } : {})
275
- };
276
- };
292
+ const cleanupResources = (resources: BtcaFsResource[]) =>
293
+ Promise.all(
294
+ resources.map(async (resource) => {
295
+ if (!resource.cleanup) return;
296
+ await ignoreErrors(() => resource.cleanup!());
297
+ })
298
+ );
299
+
300
+ const loadedResources: BtcaFsResource[] = [];
301
+
302
+ try {
303
+ await initVirtualRoot(collectionPath, vfsId);
304
+
305
+ for (const name of sortedNames) {
306
+ const resource = await loadResource(args.resources, name, quiet);
307
+ loadedResources.push(resource);
308
+ }
309
+
310
+ const metadataResources: VirtualResourceMetadata[] = [];
311
+ const loadedAt = new Date().toISOString();
312
+ for (const resource of loadedResources) {
313
+ const resourcePath = await resolveResourcePath(resource);
314
+ const virtualResourcePath = path.posix.join('/', resource.fsName);
315
+
316
+ await ignoreErrors(() =>
317
+ rmVirtualFs(virtualResourcePath, { recursive: true, force: true }, vfsId)
318
+ );
319
+
320
+ await virtualizeResource({
321
+ resource,
322
+ resourcePath,
323
+ virtualResourcePath,
324
+ vfsId
325
+ });
277
326
 
278
- export const create = (args: {
279
- config: Config.Service;
280
- resources: Resources.Service;
281
- }): Service => {
282
- return {
283
- load: ({ resourceNames, quiet = false }) =>
284
- Transaction.run('collections.load', async () => {
285
- const uniqueNames = Array.from(new Set(resourceNames));
286
- if (uniqueNames.length === 0)
287
- throw new CollectionError({
288
- message: 'Cannot create collection with no resources',
289
- hint: `${CommonHints.LIST_RESOURCES} ${CommonHints.ADD_RESOURCE}`
290
- });
291
-
292
- Metrics.info('collections.load', { resources: uniqueNames, quiet });
293
-
294
- const sortedNames = [...uniqueNames].sort((a, b) => a.localeCompare(b));
295
- const key = getCollectionKey(sortedNames);
296
- const collectionPath = '/';
297
- const vfsId = VirtualFs.create();
298
- const cleanupVirtual = () => {
299
- VirtualFs.dispose(vfsId);
300
- clearVirtualCollectionMetadata(vfsId);
301
- };
302
- const cleanupResources = (resources: BtcaFsResource[]) =>
303
- Promise.all(
304
- resources.map(async (resource) => {
305
- if (!resource.cleanup) return;
306
- await ignoreErrors(() => resource.cleanup!());
307
- })
308
- );
309
-
310
- const loadedResources: BtcaFsResource[] = [];
311
- const result = await Result.gen(async function* () {
312
- yield* Result.await(initVirtualRoot(collectionPath, vfsId));
313
-
314
- for (const name of sortedNames) {
315
- const resource = yield* Result.await(loadResource(args.resources, name, quiet));
316
- loadedResources.push(resource);
317
- }
318
-
319
- const metadataResources: VirtualResourceMetadata[] = [];
320
- const loadedAt = new Date().toISOString();
321
- for (const resource of loadedResources) {
322
- const resourcePath = yield* Result.await(resolveResourcePath(resource));
323
- const virtualResourcePath = path.posix.join('/', resource.fsName);
324
-
325
- await ignoreErrors(() =>
326
- VirtualFs.rm(virtualResourcePath, { recursive: true, force: true }, vfsId)
327
- );
328
-
329
- yield* Result.await(
330
- virtualizeResource({
331
- resource,
332
- resourcePath,
333
- virtualResourcePath,
334
- vfsId
335
- })
336
- );
337
-
338
- const definition = args.config.getResource(resource.name);
339
- const metadata = await buildVirtualMetadata({
340
- resource,
341
- resourcePath,
342
- loadedAt,
343
- definition
344
- });
345
- if (metadata) metadataResources.push(metadata);
346
- }
347
-
348
- setVirtualCollectionMetadata({
349
- vfsId,
350
- collectionKey: key,
351
- createdAt: loadedAt,
352
- resources: metadataResources
353
- });
354
-
355
- const metadataByName = new Map(
356
- metadataResources.map((resource) => [resource.name, resource])
357
- );
358
- const instructionBlocks = loadedResources.map((resource) =>
359
- createCollectionInstructionBlock(resource, metadataByName.get(resource.name))
360
- );
361
-
362
- return Result.ok({
363
- path: collectionPath,
364
- agentInstructions: instructionBlocks.join('\n\n'),
365
- vfsId,
366
- cleanup: async () => {
367
- await cleanupResources(loadedResources);
368
- }
369
- });
327
+ const definition = args.config.getResource(resource.name);
328
+ const metadata = await buildVirtualMetadata({
329
+ resource,
330
+ resourcePath,
331
+ loadedAt,
332
+ definition
370
333
  });
334
+ if (metadata) metadataResources.push(metadata);
335
+ }
336
+
337
+ setVirtualCollectionMetadata({
338
+ vfsId,
339
+ collectionKey: key,
340
+ createdAt: loadedAt,
341
+ resources: metadataResources
342
+ });
371
343
 
372
- if (!Result.isOk(result)) {
373
- cleanupVirtual();
344
+ const metadataByName = new Map(
345
+ metadataResources.map((resource) => [resource.name, resource])
346
+ );
347
+ const instructionBlocks = loadedResources.map((resource) =>
348
+ createCollectionInstructionBlock(resource, metadataByName.get(resource.name))
349
+ );
350
+
351
+ return {
352
+ path: collectionPath,
353
+ agentInstructions: instructionBlocks.join('\n\n'),
354
+ vfsId,
355
+ cleanup: async () => {
374
356
  await cleanupResources(loadedResources);
375
- throw result.error;
376
357
  }
377
- return result.value;
378
- })
379
- };
358
+ };
359
+ } catch (cause) {
360
+ cleanupVirtual();
361
+ await cleanupResources(loadedResources);
362
+ if (cause instanceof CollectionError) throw cause;
363
+ throw new CollectionError({
364
+ message: 'Failed to load resource collection',
365
+ hint: CommonHints.CLEAR_CACHE,
366
+ cause
367
+ });
368
+ }
369
+ });
370
+
371
+ const loadEffect: CollectionsService['loadEffect'] = ({ resourceNames, quiet }) =>
372
+ Effect.tryPromise({
373
+ try: () => load({ resourceNames, quiet }),
374
+ catch: (cause) =>
375
+ cause instanceof CollectionError
376
+ ? cause
377
+ : new CollectionError({
378
+ message: 'Failed to load resource collection',
379
+ hint: CommonHints.CLEAR_CACHE,
380
+ cause
381
+ })
382
+ });
383
+
384
+ return {
385
+ load,
386
+ loadEffect
380
387
  };
381
- }
388
+ };