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.
- package/README.md +0 -0
- package/package.json +21 -2
- package/src/agent/agent.test.ts +2 -2
- package/src/agent/index.ts +1 -0
- package/src/agent/loop.ts +295 -0
- package/src/agent/service.ts +249 -211
- package/src/agent/types.ts +22 -1
- package/src/collections/index.ts +0 -0
- package/src/collections/service.ts +0 -0
- package/src/collections/types.ts +0 -0
- package/src/config/config.test.ts +0 -0
- package/src/config/index.ts +5 -4
- package/src/config/remote.ts +454 -0
- package/src/context/index.ts +0 -0
- package/src/context/transaction.ts +0 -0
- package/src/errors.ts +0 -0
- package/src/index.ts +25 -2
- package/src/metrics/index.ts +0 -0
- package/src/providers/auth.ts +139 -0
- package/src/providers/index.ts +14 -0
- package/src/providers/model.ts +124 -0
- package/src/providers/opencode.ts +142 -0
- package/src/providers/registry.ts +112 -0
- package/src/resources/helpers.ts +0 -0
- package/src/resources/impls/git.test.ts +0 -0
- package/src/resources/impls/git.ts +32 -5
- package/src/resources/index.ts +0 -0
- package/src/resources/schema.ts +0 -0
- 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 +98 -125
- package/src/stream/types.ts +12 -10
- package/src/tools/glob.ts +140 -0
- package/src/tools/grep.ts +165 -0
- package/src/tools/index.ts +10 -0
- package/src/tools/list.ts +182 -0
- package/src/tools/read.ts +255 -0
- package/src/tools/ripgrep.ts +348 -0
- package/src/tools/sandbox.ts +164 -0
- 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.
|
|
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
|
}
|
package/src/agent/agent.test.ts
CHANGED
|
@@ -103,8 +103,8 @@ describe('Agent', () => {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
expect(events.length).toBeGreaterThan(0);
|
|
106
|
-
// Should have received some
|
|
107
|
-
const textEvents = events.filter((e) => e.type === '
|
|
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
|
});
|
package/src/agent/index.ts
CHANGED
|
@@ -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
|
+
}
|