btca-server 1.0.50 → 1.0.60

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 (41) hide show
  1. package/README.md +0 -0
  2. package/package.json +21 -2
  3. package/src/agent/agent.test.ts +2 -2
  4. package/src/agent/index.ts +1 -0
  5. package/src/agent/loop.ts +295 -0
  6. package/src/agent/service.ts +249 -211
  7. package/src/agent/types.ts +22 -1
  8. package/src/collections/index.ts +0 -0
  9. package/src/collections/service.ts +0 -0
  10. package/src/collections/types.ts +0 -0
  11. package/src/config/config.test.ts +0 -0
  12. package/src/config/index.ts +5 -4
  13. package/src/config/remote.ts +454 -0
  14. package/src/context/index.ts +0 -0
  15. package/src/context/transaction.ts +0 -0
  16. package/src/errors.ts +0 -0
  17. package/src/index.ts +25 -2
  18. package/src/metrics/index.ts +0 -0
  19. package/src/providers/auth.ts +139 -0
  20. package/src/providers/index.ts +14 -0
  21. package/src/providers/model.ts +124 -0
  22. package/src/providers/opencode.ts +142 -0
  23. package/src/providers/registry.ts +112 -0
  24. package/src/resources/helpers.ts +0 -0
  25. package/src/resources/impls/git.test.ts +0 -0
  26. package/src/resources/impls/git.ts +32 -5
  27. package/src/resources/index.ts +0 -0
  28. package/src/resources/schema.ts +0 -0
  29. package/src/resources/service.ts +0 -0
  30. package/src/resources/types.ts +0 -0
  31. package/src/stream/index.ts +0 -0
  32. package/src/stream/service.ts +98 -125
  33. package/src/stream/types.ts +12 -10
  34. package/src/tools/glob.ts +140 -0
  35. package/src/tools/grep.ts +165 -0
  36. package/src/tools/index.ts +10 -0
  37. package/src/tools/list.ts +182 -0
  38. package/src/tools/read.ts +255 -0
  39. package/src/tools/ripgrep.ts +348 -0
  40. package/src/tools/sandbox.ts +164 -0
  41. package/src/validation/index.ts +0 -0
package/README.md CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "btca-server",
3
- "version": "1.0.50",
3
+ "version": "1.0.60",
4
4
  "description": "BTCA server for answering questions about your codebase using OpenCode AI",
5
5
  "author": "Ben Davis",
6
6
  "license": "MIT",
@@ -30,7 +30,9 @@
30
30
  "exports": {
31
31
  ".": "./src/index.ts",
32
32
  "./stream": "./src/stream/index.ts",
33
- "./stream/types": "./src/stream/types.ts"
33
+ "./stream/types": "./src/stream/types.ts",
34
+ "./config/remote": "./src/config/remote.ts",
35
+ "./resources/schema": "./src/resources/schema.ts"
34
36
  },
35
37
  "files": [
36
38
  "src",
@@ -49,9 +51,26 @@
49
51
  "prettier": "^3.7.4"
50
52
  },
51
53
  "dependencies": {
54
+ "@ai-sdk/amazon-bedrock": "^4.0.30",
55
+ "@ai-sdk/anthropic": "^3.0.23",
56
+ "@ai-sdk/azure": "^3.0.18",
57
+ "@ai-sdk/cerebras": "^2.0.20",
58
+ "@ai-sdk/cohere": "^3.0.11",
59
+ "@ai-sdk/deepinfra": "^2.0.19",
60
+ "@ai-sdk/google": "^3.0.13",
61
+ "@ai-sdk/google-vertex": "^4.0.28",
62
+ "@ai-sdk/groq": "^3.0.15",
63
+ "@ai-sdk/mistral": "^3.0.12",
64
+ "@ai-sdk/openai": "^3.0.18",
65
+ "@ai-sdk/openai-compatible": "^2.0.18",
66
+ "@ai-sdk/perplexity": "^3.0.11",
67
+ "@ai-sdk/togetherai": "^2.0.20",
68
+ "@ai-sdk/xai": "^3.0.34",
52
69
  "@btca/shared": "workspace:*",
53
70
  "@opencode-ai/sdk": "^1.1.28",
71
+ "ai": "^6.0.49",
54
72
  "hono": "^4.7.11",
73
+ "opencode-ai": "^1.1.36",
55
74
  "zod": "^3.25.76"
56
75
  }
57
76
  }
@@ -103,8 +103,8 @@ describe('Agent', () => {
103
103
  }
104
104
 
105
105
  expect(events.length).toBeGreaterThan(0);
106
- // Should have received some message.part.updated events
107
- const textEvents = events.filter((e) => e.type === 'message.part.updated');
106
+ // Should have received some text-delta events
107
+ const textEvents = events.filter((e) => e.type === 'text-delta');
108
108
  expect(textEvents.length).toBeGreaterThan(0);
109
109
  }, 60000);
110
110
  });
@@ -1,2 +1,3 @@
1
1
  export { Agent } from './service.ts';
2
+ export { AgentLoop } from './loop.ts';
2
3
  export type { AgentResult, OcEvent, SessionState } from './types.ts';
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Custom Agent Loop
3
+ * Uses AI SDK's streamText with custom tools
4
+ */
5
+ import { streamText, tool, stepCountIs, type ModelMessage } from 'ai';
6
+
7
+ import { Model } from '../providers/index.ts';
8
+ import { ReadTool, GrepTool, GlobTool, ListTool } from '../tools/index.ts';
9
+
10
+ export namespace AgentLoop {
11
+ // Event types for streaming
12
+ export type AgentEvent =
13
+ | { type: 'text-delta'; text: string }
14
+ | { type: 'tool-call'; toolName: string; input: unknown }
15
+ | { type: 'tool-result'; toolName: string; output: string }
16
+ | {
17
+ type: 'finish';
18
+ finishReason: string;
19
+ usage?: { inputTokens?: number; outputTokens?: number };
20
+ }
21
+ | { type: 'error'; error: Error };
22
+
23
+ // Options for the agent loop
24
+ export type Options = {
25
+ providerId: string;
26
+ modelId: string;
27
+ collectionPath: string;
28
+ agentInstructions: string;
29
+ question: string;
30
+ maxSteps?: number;
31
+ };
32
+
33
+ // Result type
34
+ export type Result = {
35
+ answer: string;
36
+ model: { provider: string; model: string };
37
+ events: AgentEvent[];
38
+ };
39
+
40
+ /**
41
+ * Build the system prompt for the agent
42
+ */
43
+ function buildSystemPrompt(agentInstructions: string): string {
44
+ return [
45
+ 'You are btca, an expert documentation search agent.',
46
+ 'Your job is to answer questions by searching through the collection of resources.',
47
+ '',
48
+ 'You have access to the following tools:',
49
+ '- read: Read file contents with line numbers',
50
+ '- grep: Search file contents using regex patterns',
51
+ '- glob: Find files matching glob patterns',
52
+ '- list: List directory contents',
53
+ '',
54
+ 'Guidelines:',
55
+ '- Use glob to find relevant files first, then read them',
56
+ '- Use grep to search for specific code patterns or text',
57
+ '- Always cite the source files in your answers',
58
+ '- Be concise but thorough in your responses',
59
+ '- If you cannot find the answer, say so clearly',
60
+ '',
61
+ agentInstructions
62
+ ].join('\n');
63
+ }
64
+
65
+ /**
66
+ * Create the tools for the agent
67
+ */
68
+ function createTools(basePath: string) {
69
+ return {
70
+ read: tool({
71
+ description: 'Read the contents of a file. Returns the file contents with line numbers.',
72
+ inputSchema: ReadTool.Parameters,
73
+ execute: async (params: ReadTool.ParametersType) => {
74
+ const result = await ReadTool.execute(params, { basePath });
75
+ return result.output;
76
+ }
77
+ }),
78
+
79
+ grep: tool({
80
+ description:
81
+ 'Search for a regex pattern in file contents. Returns matching lines with file paths and line numbers.',
82
+ inputSchema: GrepTool.Parameters,
83
+ execute: async (params: GrepTool.ParametersType) => {
84
+ const result = await GrepTool.execute(params, { basePath });
85
+ return result.output;
86
+ }
87
+ }),
88
+
89
+ glob: tool({
90
+ description:
91
+ 'Find files matching a glob pattern (e.g. "**/*.ts", "src/**/*.js"). Returns a list of matching file paths sorted by modification time.',
92
+ inputSchema: GlobTool.Parameters,
93
+ execute: async (params: GlobTool.ParametersType) => {
94
+ const result = await GlobTool.execute(params, { basePath });
95
+ return result.output;
96
+ }
97
+ }),
98
+
99
+ list: tool({
100
+ description:
101
+ 'List the contents of a directory. Returns files and subdirectories with their types.',
102
+ inputSchema: ListTool.Parameters,
103
+ execute: async (params: ListTool.ParametersType) => {
104
+ const result = await ListTool.execute(params, { basePath });
105
+ return result.output;
106
+ }
107
+ })
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Get initial context by listing the collection directory
113
+ */
114
+ async function getInitialContext(collectionPath: string): Promise<string> {
115
+ const result = await ListTool.execute({ path: '.' }, { basePath: collectionPath });
116
+ return `Collection contents:\n${result.output}`;
117
+ }
118
+
119
+ /**
120
+ * Run the agent loop and return the final answer
121
+ */
122
+ export async function run(options: Options): Promise<Result> {
123
+ const {
124
+ providerId,
125
+ modelId,
126
+ collectionPath,
127
+ agentInstructions,
128
+ question,
129
+ maxSteps = 40
130
+ } = options;
131
+
132
+ // Get the model
133
+ const model = await Model.getModel(providerId, modelId);
134
+
135
+ // Get initial context
136
+ const initialContext = await getInitialContext(collectionPath);
137
+
138
+ // Build messages
139
+ const messages: ModelMessage[] = [
140
+ {
141
+ role: 'user',
142
+ content: `${initialContext}\n\nQuestion: ${question}`
143
+ }
144
+ ];
145
+
146
+ // Create tools
147
+ const tools = createTools(collectionPath);
148
+
149
+ // Collect events
150
+ const events: AgentEvent[] = [];
151
+ let fullText = '';
152
+
153
+ // Run streamText with tool execution
154
+ const result = streamText({
155
+ model,
156
+ system: buildSystemPrompt(agentInstructions),
157
+ messages,
158
+ tools,
159
+ stopWhen: stepCountIs(maxSteps)
160
+ });
161
+
162
+ // Process the stream
163
+ for await (const part of result.fullStream) {
164
+ switch (part.type) {
165
+ case 'text-delta':
166
+ fullText += part.text;
167
+ events.push({ type: 'text-delta', text: part.text });
168
+ break;
169
+
170
+ case 'tool-call':
171
+ events.push({
172
+ type: 'tool-call',
173
+ toolName: part.toolName,
174
+ input: part.input
175
+ });
176
+ break;
177
+
178
+ case 'tool-result':
179
+ events.push({
180
+ type: 'tool-result',
181
+ toolName: part.toolName,
182
+ output: typeof part.output === 'string' ? part.output : JSON.stringify(part.output)
183
+ });
184
+ break;
185
+
186
+ case 'finish':
187
+ events.push({
188
+ type: 'finish',
189
+ finishReason: part.finishReason ?? 'unknown',
190
+ usage: {
191
+ inputTokens: part.totalUsage?.inputTokens,
192
+ outputTokens: part.totalUsage?.outputTokens
193
+ }
194
+ });
195
+ break;
196
+
197
+ case 'error':
198
+ events.push({
199
+ type: 'error',
200
+ error: part.error instanceof Error ? part.error : new Error(String(part.error))
201
+ });
202
+ break;
203
+ }
204
+ }
205
+
206
+ return {
207
+ answer: fullText.trim(),
208
+ model: { provider: providerId, model: modelId },
209
+ events
210
+ };
211
+ }
212
+
213
+ /**
214
+ * Run the agent loop and stream events
215
+ */
216
+ export async function* stream(options: Options): AsyncGenerator<AgentEvent> {
217
+ const {
218
+ providerId,
219
+ modelId,
220
+ collectionPath,
221
+ agentInstructions,
222
+ question,
223
+ maxSteps = 40
224
+ } = options;
225
+
226
+ // Get the model
227
+ const model = await Model.getModel(providerId, modelId);
228
+
229
+ // Get initial context
230
+ const initialContext = await getInitialContext(collectionPath);
231
+
232
+ // Build messages
233
+ const messages: ModelMessage[] = [
234
+ {
235
+ role: 'user',
236
+ content: `${initialContext}\n\nQuestion: ${question}`
237
+ }
238
+ ];
239
+
240
+ // Create tools
241
+ const tools = createTools(collectionPath);
242
+
243
+ // Run streamText with tool execution
244
+ const result = streamText({
245
+ model,
246
+ system: buildSystemPrompt(agentInstructions),
247
+ messages,
248
+ tools,
249
+ stopWhen: stepCountIs(maxSteps)
250
+ });
251
+
252
+ // Stream events
253
+ for await (const part of result.fullStream) {
254
+ switch (part.type) {
255
+ case 'text-delta':
256
+ yield { type: 'text-delta', text: part.text };
257
+ break;
258
+
259
+ case 'tool-call':
260
+ yield {
261
+ type: 'tool-call',
262
+ toolName: part.toolName,
263
+ input: part.input
264
+ };
265
+ break;
266
+
267
+ case 'tool-result':
268
+ yield {
269
+ type: 'tool-result',
270
+ toolName: part.toolName,
271
+ output: typeof part.output === 'string' ? part.output : JSON.stringify(part.output)
272
+ };
273
+ break;
274
+
275
+ case 'finish':
276
+ yield {
277
+ type: 'finish',
278
+ finishReason: part.finishReason ?? 'unknown',
279
+ usage: {
280
+ inputTokens: part.totalUsage?.inputTokens,
281
+ outputTokens: part.totalUsage?.outputTokens
282
+ }
283
+ };
284
+ break;
285
+
286
+ case 'error':
287
+ yield {
288
+ type: 'error',
289
+ error: part.error instanceof Error ? part.error : new Error(String(part.error))
290
+ };
291
+ break;
292
+ }
293
+ }
294
+ }
295
+ }