btca-server 1.0.961 → 2.0.0

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
@@ -2,210 +2,172 @@
2
2
  * Agent Service
3
3
  * Refactored to use custom AI SDK loop instead of spawning OpenCode instances
4
4
  */
5
- import { Result } from 'better-result';
5
+ import { Effect } from 'effect';
6
6
 
7
- import { Config } from '../config/index.ts';
7
+ import type { ConfigService as ConfigServiceShape } from '../config/index.ts';
8
8
  import { getErrorHint, getErrorMessage, type TaggedErrorOptions } from '../errors.ts';
9
- import { Metrics } from '../metrics/index.ts';
10
- import { Auth, getSupportedProviders } from '../providers/index.ts';
9
+ import { metricsError, metricsErrorInfo, metricsInfo } from '../metrics/index.ts';
10
+ import {
11
+ getAuthenticatedProviders,
12
+ getProviderAuthHint,
13
+ getSupportedProviders,
14
+ isAuthenticated
15
+ } from '../providers/index.ts';
11
16
  import type { CollectionResult } from '../collections/types.ts';
12
17
  import { clearVirtualCollectionMetadata } from '../collections/virtual-metadata.ts';
13
- import { VirtualFs } from '../vfs/virtual-fs.ts';
18
+ import { disposeVirtualFs } from '../vfs/virtual-fs.ts';
14
19
  import type { AgentResult } from './types.ts';
15
- import { AgentLoop } from './loop.ts';
20
+ import { runAgentLoop, streamAgentLoop, type AgentEvent } from './loop.ts';
16
21
 
17
- export namespace Agent {
18
- // ─────────────────────────────────────────────────────────────────────────────
19
- // Error Classes
20
- // ─────────────────────────────────────────────────────────────────────────────
22
+ export class AgentError extends Error {
23
+ readonly _tag = 'AgentError';
24
+ override readonly cause?: unknown;
25
+ readonly hint?: string;
21
26
 
22
- export class AgentError extends Error {
23
- readonly _tag = 'AgentError';
24
- override readonly cause?: unknown;
25
- readonly hint?: string;
26
-
27
- constructor(args: TaggedErrorOptions) {
28
- super(args.message);
29
- this.cause = args.cause;
30
- this.hint = args.hint;
31
- }
27
+ constructor(args: TaggedErrorOptions) {
28
+ super(args.message);
29
+ this.cause = args.cause;
30
+ this.hint = args.hint;
32
31
  }
32
+ }
33
33
 
34
- export class InvalidProviderError extends Error {
35
- readonly _tag = 'InvalidProviderError';
36
- readonly providerId: string;
37
- readonly availableProviders: string[];
38
- readonly hint: string;
39
-
40
- constructor(args: { providerId: string; availableProviders: string[] }) {
41
- super(`Invalid provider: "${args.providerId}"`);
42
- this.providerId = args.providerId;
43
- this.availableProviders = args.availableProviders;
44
- this.hint = `Available providers: ${args.availableProviders.join(
45
- ', '
46
- )}. Update your config with a valid provider. Open an issue to request this provider: https://github.com/davis7dotsh/better-context/issues.`;
47
- }
34
+ export class InvalidProviderError extends Error {
35
+ readonly _tag = 'InvalidProviderError';
36
+ readonly providerId: string;
37
+ readonly availableProviders: string[];
38
+ readonly hint: string;
39
+
40
+ constructor(args: { providerId: string; availableProviders: string[] }) {
41
+ super(`Invalid provider: "${args.providerId}"`);
42
+ this.providerId = args.providerId;
43
+ this.availableProviders = args.availableProviders;
44
+ this.hint = `Available providers: ${args.availableProviders.join(
45
+ ', '
46
+ )}. Update your config with a valid provider. Open an issue to request this provider: https://github.com/davis7dotsh/better-context/issues.`;
48
47
  }
48
+ }
49
49
 
50
- export class InvalidModelError extends Error {
51
- readonly _tag = 'InvalidModelError';
52
- readonly providerId: string;
53
- readonly modelId: string;
54
- readonly availableModels: string[];
55
- readonly hint: string;
56
-
57
- constructor(args: { providerId: string; modelId: string; availableModels: string[] }) {
58
- super(`Invalid model "${args.modelId}" for provider "${args.providerId}"`);
59
- this.providerId = args.providerId;
60
- this.modelId = args.modelId;
61
- this.availableModels = args.availableModels;
62
- const modelList =
63
- args.availableModels.length <= 5
64
- ? args.availableModels.join(', ')
65
- : `${args.availableModels.slice(0, 5).join(', ')}... (${args.availableModels.length} total)`;
66
- this.hint = `Available models for ${args.providerId}: ${modelList}. Update your config with a valid model.`;
67
- }
50
+ export class InvalidModelError extends Error {
51
+ readonly _tag = 'InvalidModelError';
52
+ readonly providerId: string;
53
+ readonly modelId: string;
54
+ readonly availableModels: string[];
55
+ readonly hint: string;
56
+
57
+ constructor(args: { providerId: string; modelId: string; availableModels: string[] }) {
58
+ super(`Invalid model "${args.modelId}" for provider "${args.providerId}"`);
59
+ this.providerId = args.providerId;
60
+ this.modelId = args.modelId;
61
+ this.availableModels = args.availableModels;
62
+ const modelList =
63
+ args.availableModels.length <= 5
64
+ ? args.availableModels.join(', ')
65
+ : `${args.availableModels.slice(0, 5).join(', ')}... (${args.availableModels.length} total)`;
66
+ this.hint = `Available models for ${args.providerId}: ${modelList}. Update your config with a valid model.`;
68
67
  }
68
+ }
69
69
 
70
- export class ProviderNotConnectedError extends Error {
71
- readonly _tag = 'ProviderNotConnectedError';
72
- readonly providerId: string;
73
- readonly connectedProviders: string[];
74
- readonly hint: string;
75
-
76
- constructor(args: { providerId: string; connectedProviders: string[] }) {
77
- super(`Provider "${args.providerId}" is not connected`);
78
- this.providerId = args.providerId;
79
- this.connectedProviders = args.connectedProviders;
80
- const baseHint = Auth.getProviderAuthHint(args.providerId);
81
- if (args.connectedProviders.length > 0) {
82
- this.hint = `${baseHint} Connected providers: ${args.connectedProviders.join(', ')}.`;
83
- } else {
84
- this.hint = `${baseHint} No providers are currently connected.`;
85
- }
70
+ export class ProviderNotConnectedError extends Error {
71
+ readonly _tag = 'ProviderNotConnectedError';
72
+ readonly providerId: string;
73
+ readonly connectedProviders: string[];
74
+ readonly hint: string;
75
+
76
+ constructor(args: { providerId: string; connectedProviders: string[] }) {
77
+ super(`Provider "${args.providerId}" is not connected`);
78
+ this.providerId = args.providerId;
79
+ this.connectedProviders = args.connectedProviders;
80
+ const baseHint = getProviderAuthHint(args.providerId);
81
+ if (args.connectedProviders.length > 0) {
82
+ this.hint = `${baseHint} Connected providers: ${args.connectedProviders.join(', ')}.`;
83
+ } else {
84
+ this.hint = `${baseHint} No providers are currently connected.`;
86
85
  }
87
86
  }
87
+ }
88
88
 
89
- // ─────────────────────────────────────────────────────────────────────────────
90
- // Service Type
91
- // ─────────────────────────────────────────────────────────────────────────────
92
-
93
- export type Service = {
94
- askStream: (args: { collection: CollectionResult; question: string }) => Promise<{
95
- stream: AsyncIterable<AgentLoop.AgentEvent>;
89
+ export type AgentService = {
90
+ askStream: (args: { collection: CollectionResult; question: string }) => Promise<{
91
+ stream: AsyncIterable<AgentEvent>;
92
+ model: { provider: string; model: string };
93
+ }>;
94
+ askStreamEffect: (args: { collection: CollectionResult; question: string }) => Effect.Effect<
95
+ {
96
+ stream: AsyncIterable<AgentEvent>;
96
97
  model: { provider: string; model: string };
97
- }>;
98
-
99
- ask: (args: { collection: CollectionResult; question: string }) => Promise<AgentResult>;
100
-
101
- listProviders: () => Promise<{
98
+ },
99
+ unknown
100
+ >;
101
+
102
+ ask: (args: { collection: CollectionResult; question: string }) => Promise<AgentResult>;
103
+ askEffect: (args: {
104
+ collection: CollectionResult;
105
+ question: string;
106
+ }) => Effect.Effect<AgentResult, unknown>;
107
+
108
+ listProviders: () => Promise<{
109
+ all: { id: string; models: Record<string, unknown> }[];
110
+ connected: string[];
111
+ }>;
112
+ listProvidersEffect: () => Effect.Effect<
113
+ {
102
114
  all: { id: string; models: Record<string, unknown> }[];
103
115
  connected: string[];
104
- }>;
105
- };
106
-
107
- // ─────────────────────────────────────────────────────────────────────────────
108
- // Service Factory
109
- // ─────────────────────────────────────────────────────────────────────────────
110
-
111
- export const create = (config: Config.Service): Service => {
112
- /**
113
- * Ask a question and stream the response using the new AI SDK loop
114
- */
115
- const askStream: Service['askStream'] = async ({ collection, question }) => {
116
- Metrics.info('agent.ask.start', {
117
- provider: config.provider,
118
- model: config.model,
119
- questionLength: question.length
120
- });
121
-
122
- const cleanup = async () => {
123
- if (collection.vfsId) {
124
- VirtualFs.dispose(collection.vfsId);
125
- clearVirtualCollectionMetadata(collection.vfsId);
126
- }
127
- try {
128
- await collection.cleanup?.();
129
- } catch {
130
- // cleanup should never fail user-visible operations
131
- }
132
- };
133
-
134
- // Validate provider is authenticated
135
- const isAuthed = await Auth.isAuthenticated(config.provider);
136
- const requiresAuth = config.provider !== 'opencode' && config.provider !== 'openai-compat';
137
- if (!isAuthed && requiresAuth) {
138
- const authenticated = await Auth.getAuthenticatedProviders();
139
- await cleanup();
140
- throw new ProviderNotConnectedError({
141
- providerId: config.provider,
142
- connectedProviders: authenticated
143
- });
116
+ },
117
+ unknown
118
+ >;
119
+ };
120
+
121
+ export type Service = AgentService;
122
+
123
+ export const createAgentService = (config: ConfigServiceShape): AgentService => {
124
+ const cleanupCollection = (collection: CollectionResult) =>
125
+ Effect.promise(async () => {
126
+ if (collection.vfsId) {
127
+ disposeVirtualFs(collection.vfsId);
128
+ clearVirtualCollectionMetadata(collection.vfsId);
144
129
  }
145
-
146
- // Create a generator that wraps the AgentLoop stream
147
- const eventGenerator = (async function* () {
148
- try {
149
- const stream = AgentLoop.stream({
150
- providerId: config.provider,
151
- modelId: config.model,
152
- maxSteps: config.maxSteps,
153
- collectionPath: collection.path,
154
- vfsId: collection.vfsId,
155
- agentInstructions: collection.agentInstructions,
156
- question,
157
- providerOptions: config.getProviderOptions(config.provider)
158
- });
159
- for await (const event of stream) {
160
- yield event;
161
- }
162
- } finally {
163
- await cleanup();
164
- }
165
- })();
166
-
167
- return {
168
- stream: eventGenerator,
169
- model: { provider: config.provider, model: config.model }
170
- };
171
- };
172
-
173
- /**
174
- * Ask a question and return the complete response
175
- */
176
- const ask: Service['ask'] = async ({ collection, question }) => {
177
- Metrics.info('agent.ask.start', {
178
- provider: config.provider,
179
- model: config.model,
180
- questionLength: question.length
181
- });
182
-
183
- const cleanup = async () => {
184
- if (collection.vfsId) {
185
- VirtualFs.dispose(collection.vfsId);
186
- clearVirtualCollectionMetadata(collection.vfsId);
187
- }
188
- try {
189
- await collection.cleanup?.();
190
- } catch {
191
- // cleanup should never fail user-visible operations
192
- }
193
- };
194
-
195
- // Validate provider is authenticated
196
- const isAuthed = await Auth.isAuthenticated(config.provider);
197
- const requiresAuth = config.provider !== 'opencode' && config.provider !== 'openai-compat';
198
- if (!isAuthed && requiresAuth) {
199
- const authenticated = await Auth.getAuthenticatedProviders();
200
- await cleanup();
201
- throw new ProviderNotConnectedError({
202
- providerId: config.provider,
203
- connectedProviders: authenticated
204
- });
130
+ try {
131
+ await collection.cleanup?.();
132
+ } catch {
133
+ return;
205
134
  }
135
+ });
136
+
137
+ const ensureProviderConnected = Effect.fn(function* () {
138
+ const isAuthed = yield* Effect.tryPromise(() => isAuthenticated(config.provider));
139
+ const requiresAuth = config.provider !== 'opencode' && config.provider !== 'openai-compat';
140
+ if (isAuthed || !requiresAuth) return;
141
+ const authenticated = yield* Effect.tryPromise(() => getAuthenticatedProviders());
142
+ yield* Effect.fail(
143
+ new ProviderNotConnectedError({
144
+ providerId: config.provider,
145
+ connectedProviders: authenticated
146
+ })
147
+ );
148
+ });
149
+
150
+ /**
151
+ * Ask a question and stream the response using the new AI SDK loop
152
+ */
153
+ const askStream: AgentService['askStream'] = async ({ collection, question }) => {
154
+ metricsInfo('agent.ask.start', {
155
+ provider: config.provider,
156
+ model: config.model,
157
+ questionLength: question.length
158
+ });
159
+
160
+ try {
161
+ await Effect.runPromise(ensureProviderConnected());
162
+ } catch (error) {
163
+ await Effect.runPromise(cleanupCollection(collection));
164
+ throw error;
165
+ }
206
166
 
207
- const runResult = await Result.tryPromise(() =>
208
- AgentLoop.run({
167
+ // Create a generator that wraps the AgentLoop stream
168
+ const eventGenerator = (async function* () {
169
+ try {
170
+ const stream = streamAgentLoop({
209
171
  providerId: config.provider,
210
172
  modelId: config.model,
211
173
  maxSteps: config.maxSteps,
@@ -214,64 +176,121 @@ export namespace Agent {
214
176
  agentInstructions: collection.agentInstructions,
215
177
  question,
216
178
  providerOptions: config.getProviderOptions(config.provider)
217
- })
218
- );
219
-
220
- await cleanup();
221
-
222
- if (!Result.isOk(runResult)) {
223
- const cause = runResult.error;
224
- Metrics.error('agent.ask.error', { error: Metrics.errorInfo(cause) });
225
- throw new AgentError({
226
- message: getErrorMessage(cause),
227
- hint:
228
- getErrorHint(cause) ?? 'This may be a temporary issue. Try running the command again.',
229
- cause
230
179
  });
180
+ for await (const event of stream) {
181
+ yield event;
182
+ }
183
+ } finally {
184
+ await Effect.runPromise(cleanupCollection(collection));
231
185
  }
186
+ })();
232
187
 
233
- const result = runResult.value;
234
- Metrics.info('agent.ask.complete', {
235
- provider: config.provider,
236
- model: config.model,
237
- answerLength: result.answer.length,
238
- eventCount: result.events.length
239
- });
240
-
241
- return {
242
- answer: result.answer,
243
- model: result.model,
244
- events: result.events
245
- };
188
+ return {
189
+ stream: eventGenerator,
190
+ model: { provider: config.provider, model: config.model }
246
191
  };
192
+ };
247
193
 
248
- /**
249
- * List available providers using local auth data
250
- */
251
- const listProviders: Service['listProviders'] = async () => {
252
- // Get all supported providers from registry
253
- const supportedProviders = getSupportedProviders();
194
+ /**
195
+ * Ask a question and return the complete response
196
+ */
197
+ const ask: AgentService['ask'] = async ({ collection, question }) => {
198
+ return Effect.runPromise(
199
+ Effect.gen(function* () {
200
+ metricsInfo('agent.ask.start', {
201
+ provider: config.provider,
202
+ model: config.model,
203
+ questionLength: question.length
204
+ });
254
205
 
255
- // Get authenticated providers from OpenCode's auth storage
256
- const authenticatedProviders = await Auth.getAuthenticatedProviders();
206
+ yield* ensureProviderConnected();
207
+
208
+ const result = yield* Effect.tryPromise({
209
+ try: () =>
210
+ runAgentLoop({
211
+ providerId: config.provider,
212
+ modelId: config.model,
213
+ maxSteps: config.maxSteps,
214
+ collectionPath: collection.path,
215
+ vfsId: collection.vfsId,
216
+ agentInstructions: collection.agentInstructions,
217
+ question,
218
+ providerOptions: config.getProviderOptions(config.provider)
219
+ }),
220
+ catch: (cause) =>
221
+ new AgentError({
222
+ message: getErrorMessage(cause),
223
+ hint:
224
+ getErrorHint(cause) ??
225
+ 'This may be a temporary issue. Try running the command again.',
226
+ cause
227
+ })
228
+ });
257
229
 
258
- // Build the response - we don't have model lists without spawning OpenCode,
259
- // so we return empty models for now
260
- const all = supportedProviders.map((id) => ({
261
- id,
262
- models: {} as Record<string, unknown>
263
- }));
230
+ metricsInfo('agent.ask.complete', {
231
+ provider: config.provider,
232
+ model: config.model,
233
+ answerLength: result.answer.length,
234
+ eventCount: result.events.length
235
+ });
264
236
 
265
- return {
266
- all,
267
- connected: authenticatedProviders
268
- };
269
- };
237
+ return {
238
+ answer: result.answer,
239
+ model: result.model,
240
+ events: result.events
241
+ };
242
+ }).pipe(
243
+ Effect.tapError((error) =>
244
+ Effect.sync(() => metricsError('agent.ask.error', { error: metricsErrorInfo(error) }))
245
+ ),
246
+ Effect.ensuring(cleanupCollection(collection))
247
+ )
248
+ );
249
+ };
270
250
 
271
- return {
272
- askStream,
273
- ask,
274
- listProviders
275
- };
251
+ /**
252
+ * List available providers using local auth data
253
+ */
254
+ const listProviders: AgentService['listProviders'] = async () => {
255
+ return Effect.runPromise(
256
+ Effect.gen(function* () {
257
+ const supportedProviders = getSupportedProviders();
258
+ const authenticatedProviders = yield* Effect.tryPromise(() => getAuthenticatedProviders());
259
+ const all = supportedProviders.map((id) => ({
260
+ id,
261
+ models: {} as Record<string, unknown>
262
+ }));
263
+
264
+ return {
265
+ all,
266
+ connected: authenticatedProviders
267
+ };
268
+ })
269
+ );
276
270
  };
277
- }
271
+
272
+ const askStreamEffect: AgentService['askStreamEffect'] = (args) =>
273
+ Effect.tryPromise({
274
+ try: () => askStream(args),
275
+ catch: (cause) => cause
276
+ });
277
+ const askEffect: AgentService['askEffect'] = (args) =>
278
+ Effect.tryPromise({
279
+ try: () => ask(args),
280
+ catch: (cause) => cause
281
+ });
282
+ const listProvidersEffect: AgentService['listProvidersEffect'] = () =>
283
+ Effect.tryPromise({
284
+ try: () => listProviders(),
285
+ catch: (cause) => cause
286
+ });
287
+
288
+ return {
289
+ askStream,
290
+ ask,
291
+ listProviders,
292
+ askStreamEffect,
293
+ askEffect,
294
+ listProvidersEffect
295
+ };
296
+ };
@@ -1,7 +1,7 @@
1
- import type { AgentLoop } from './loop.ts';
1
+ import type { AgentEvent } from './loop.ts';
2
2
 
3
3
  export type AgentResult = {
4
4
  answer: string;
5
5
  model: { provider: string; model: string };
6
- events: AgentLoop.AgentEvent[];
6
+ events: AgentEvent[];
7
7
  };
@@ -1,2 +1,3 @@
1
- export { Collections } from './service.ts';
1
+ export { createCollectionsService } from './service.ts';
2
+ export type { CollectionsService } from './service.ts';
2
3
  export { CollectionError, getCollectionKey, type CollectionResult } from './types.ts';