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,348 +0,0 @@
1
- /**
2
- * Ripgrep Binary Management
3
- * Handles downloading and caching the ripgrep binary
4
- */
5
- import * as fs from 'node:fs/promises';
6
- import * as os from 'node:os';
7
- import * as path from 'node:path';
8
-
9
- export namespace Ripgrep {
10
- const VERSION = '14.1.1';
11
-
12
- // Platform configurations
13
- const PLATFORM_CONFIG: Record<
14
- string,
15
- { platform: string; extension: 'tar.gz' | 'zip'; binaryName: string }
16
- > = {
17
- 'darwin-arm64': {
18
- platform: 'aarch64-apple-darwin',
19
- extension: 'tar.gz',
20
- binaryName: 'rg'
21
- },
22
- 'darwin-x64': {
23
- platform: 'x86_64-apple-darwin',
24
- extension: 'tar.gz',
25
- binaryName: 'rg'
26
- },
27
- 'linux-arm64': {
28
- platform: 'aarch64-unknown-linux-gnu',
29
- extension: 'tar.gz',
30
- binaryName: 'rg'
31
- },
32
- 'linux-x64': {
33
- platform: 'x86_64-unknown-linux-musl',
34
- extension: 'tar.gz',
35
- binaryName: 'rg'
36
- },
37
- 'win32-x64': {
38
- platform: 'x86_64-pc-windows-msvc',
39
- extension: 'zip',
40
- binaryName: 'rg.exe'
41
- }
42
- };
43
-
44
- /**
45
- * Get the btca data directory
46
- */
47
- function getDataPath(): string {
48
- const platform = os.platform();
49
-
50
- if (platform === 'win32') {
51
- const appdata = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
52
- return path.join(appdata, 'btca');
53
- }
54
-
55
- // Linux and macOS use XDG_DATA_HOME or ~/.local/share
56
- const xdgData = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
57
- return path.join(xdgData, 'btca');
58
- }
59
-
60
- /**
61
- * Get the bin directory for storing binaries
62
- */
63
- function getBinPath(): string {
64
- return path.join(getDataPath(), 'bin');
65
- }
66
-
67
- /**
68
- * Get the expected ripgrep binary path
69
- */
70
- function getRipgrepPath(): string {
71
- const platform = os.platform();
72
- const binaryName = platform === 'win32' ? 'rg.exe' : 'rg';
73
- return path.join(getBinPath(), binaryName);
74
- }
75
-
76
- /**
77
- * Check if ripgrep is already installed in PATH
78
- */
79
- async function findInPath(): Promise<string | null> {
80
- const rgPath = Bun.which('rg');
81
- return rgPath || null;
82
- }
83
-
84
- /**
85
- * Check if our cached ripgrep binary exists
86
- */
87
- async function findCached(): Promise<string | null> {
88
- const rgPath = getRipgrepPath();
89
- const file = Bun.file(rgPath);
90
- if (await file.exists()) {
91
- return rgPath;
92
- }
93
- return null;
94
- }
95
-
96
- /**
97
- * Get the platform configuration
98
- */
99
- function getPlatformConfig(): (typeof PLATFORM_CONFIG)[string] | null {
100
- const platform = os.platform();
101
- const arch = os.arch();
102
- const key = `${platform}-${arch}`;
103
- return PLATFORM_CONFIG[key] || null;
104
- }
105
-
106
- /**
107
- * Download ripgrep from GitHub releases
108
- */
109
- async function download(): Promise<string> {
110
- const config = getPlatformConfig();
111
- if (!config) {
112
- throw new Error(`Unsupported platform: ${os.platform()}-${os.arch()}`);
113
- }
114
-
115
- const binDir = getBinPath();
116
- const rgPath = getRipgrepPath();
117
-
118
- // Ensure bin directory exists
119
- await fs.mkdir(binDir, { recursive: true });
120
-
121
- // Build download URL
122
- const filename = `ripgrep-${VERSION}-${config.platform}.${config.extension}`;
123
- const url = `https://github.com/BurntSushi/ripgrep/releases/download/${VERSION}/${filename}`;
124
-
125
- console.log(`Downloading ripgrep from ${url}...`);
126
-
127
- // Download the archive
128
- const response = await fetch(url);
129
- if (!response.ok) {
130
- throw new Error(`Failed to download ripgrep: ${response.status} ${response.statusText}`);
131
- }
132
-
133
- const buffer = await response.arrayBuffer();
134
- const archivePath = path.join(binDir, filename);
135
-
136
- // Write archive to disk
137
- await Bun.write(archivePath, buffer);
138
-
139
- // Extract based on file type
140
- if (config.extension === 'tar.gz') {
141
- // Extract tar.gz
142
- const proc = Bun.spawn(['tar', '-xzf', archivePath, '--strip-components=1', '-C', binDir], {
143
- cwd: binDir,
144
- stdout: 'pipe',
145
- stderr: 'pipe'
146
- });
147
- await proc.exited;
148
-
149
- if (proc.exitCode !== 0) {
150
- throw new Error(`Failed to extract ripgrep: exit code ${proc.exitCode}`);
151
- }
152
- } else {
153
- // Extract zip (Windows)
154
- // Use unzip if available, otherwise use Bun's built-in zip handling
155
- const proc = Bun.spawn(['unzip', '-o', archivePath, '-d', binDir], {
156
- cwd: binDir,
157
- stdout: 'pipe',
158
- stderr: 'pipe'
159
- });
160
- await proc.exited;
161
-
162
- if (proc.exitCode !== 0) {
163
- throw new Error(`Failed to extract ripgrep: exit code ${proc.exitCode}`);
164
- }
165
- }
166
-
167
- // Clean up archive
168
- await fs.unlink(archivePath).catch(() => {});
169
-
170
- // Make binary executable (Unix only)
171
- if (os.platform() !== 'win32') {
172
- await fs.chmod(rgPath, 0o755);
173
- }
174
-
175
- console.log(`Ripgrep installed to ${rgPath}`);
176
-
177
- return rgPath;
178
- }
179
-
180
- /**
181
- * Get the path to the ripgrep binary
182
- * Downloads it if not found in PATH or cache
183
- */
184
- export async function filepath(): Promise<string> {
185
- // First check PATH
186
- const inPath = await findInPath();
187
- if (inPath) {
188
- return inPath;
189
- }
190
-
191
- // Then check cache
192
- const cached = await findCached();
193
- if (cached) {
194
- return cached;
195
- }
196
-
197
- // Download if not found
198
- return download();
199
- }
200
-
201
- /**
202
- * Run ripgrep with the given arguments
203
- */
204
- export async function run(
205
- args: string[],
206
- options: { cwd?: string } = {}
207
- ): Promise<{
208
- stdout: string;
209
- stderr: string;
210
- exitCode: number;
211
- }> {
212
- const rgPath = await filepath();
213
-
214
- const proc = Bun.spawn([rgPath, ...args], {
215
- cwd: options.cwd || process.cwd(),
216
- stdout: 'pipe',
217
- stderr: 'pipe'
218
- });
219
-
220
- const [stdout, stderr, exitCode] = await Promise.all([
221
- new Response(proc.stdout).text(),
222
- new Response(proc.stderr).text(),
223
- proc.exited
224
- ]);
225
-
226
- return { stdout, stderr, exitCode };
227
- }
228
-
229
- /**
230
- * Generator that yields file paths matching a glob pattern
231
- */
232
- export async function* files(options: {
233
- cwd: string;
234
- glob?: string[];
235
- hidden?: boolean;
236
- }): AsyncGenerator<string> {
237
- const rgPath = await filepath();
238
-
239
- const args = ['--files', '--follow', '--no-messages'];
240
-
241
- if (options.hidden) {
242
- args.push('--hidden');
243
- }
244
-
245
- if (options.glob) {
246
- for (const pattern of options.glob) {
247
- args.push('--glob', pattern);
248
- }
249
- }
250
-
251
- const proc = Bun.spawn([rgPath, ...args], {
252
- cwd: options.cwd,
253
- stdout: 'pipe',
254
- stderr: 'pipe'
255
- });
256
-
257
- const stdout = await new Response(proc.stdout).text();
258
- await proc.exited;
259
-
260
- for (const line of stdout.trim().split('\n')) {
261
- if (line) {
262
- yield line;
263
- }
264
- }
265
- }
266
-
267
- /**
268
- * Search for a pattern in files
269
- */
270
- export async function search(options: {
271
- cwd: string;
272
- pattern: string;
273
- glob?: string;
274
- hidden?: boolean;
275
- maxResults?: number;
276
- }): Promise<
277
- Array<{
278
- path: string;
279
- lineNumber: number;
280
- lineText: string;
281
- }>
282
- > {
283
- const rgPath = await filepath();
284
-
285
- const args = [
286
- '-n', // line numbers
287
- '-H', // filename
288
- '--follow', // follow symlinks
289
- '--no-messages', // suppress errors
290
- '--field-match-separator=|' // use | as separator
291
- ];
292
-
293
- if (options.hidden) {
294
- args.push('--hidden');
295
- }
296
-
297
- if (options.glob) {
298
- args.push('--glob', options.glob);
299
- }
300
-
301
- args.push('--regexp', options.pattern);
302
-
303
- const proc = Bun.spawn([rgPath, ...args], {
304
- cwd: options.cwd,
305
- stdout: 'pipe',
306
- stderr: 'pipe'
307
- });
308
-
309
- const stdout = await new Response(proc.stdout).text();
310
- await proc.exited;
311
-
312
- const results: Array<{
313
- path: string;
314
- lineNumber: number;
315
- lineText: string;
316
- }> = [];
317
-
318
- for (const line of stdout.trim().split('\n')) {
319
- if (!line) continue;
320
-
321
- // Parse format: filepath|lineNum|lineText
322
- const firstPipe = line.indexOf('|');
323
- if (firstPipe === -1) continue;
324
-
325
- const secondPipe = line.indexOf('|', firstPipe + 1);
326
- if (secondPipe === -1) continue;
327
-
328
- const filePath = line.substring(0, firstPipe);
329
- const lineNumStr = line.substring(firstPipe + 1, secondPipe);
330
- const lineText = line.substring(secondPipe + 1);
331
-
332
- const lineNumber = parseInt(lineNumStr, 10);
333
- if (isNaN(lineNumber)) continue;
334
-
335
- results.push({
336
- path: path.resolve(options.cwd, filePath),
337
- lineNumber,
338
- lineText
339
- });
340
-
341
- if (options.maxResults && results.length >= options.maxResults) {
342
- break;
343
- }
344
- }
345
-
346
- return results;
347
- }
348
- }
@@ -1,164 +0,0 @@
1
- /**
2
- * Path Sandboxing Utilities
3
- * Ensures all file operations stay within the collections directory
4
- */
5
- import * as fs from 'node:fs/promises';
6
- import * as path from 'node:path';
7
-
8
- export namespace Sandbox {
9
- export class PathEscapeError extends Error {
10
- readonly _tag = 'PathEscapeError';
11
- readonly requestedPath: string;
12
- readonly basePath: string;
13
-
14
- constructor(requestedPath: string, basePath: string) {
15
- super(
16
- `Path "${requestedPath}" is outside the allowed directory "${basePath}". Access denied.`
17
- );
18
- this.requestedPath = requestedPath;
19
- this.basePath = basePath;
20
- }
21
- }
22
-
23
- export class PathNotFoundError extends Error {
24
- readonly _tag = 'PathNotFoundError';
25
- readonly requestedPath: string;
26
-
27
- constructor(requestedPath: string) {
28
- super(`Path "${requestedPath}" does not exist.`);
29
- this.requestedPath = requestedPath;
30
- }
31
- }
32
-
33
- /**
34
- * Resolve a path relative to the base path and validate it stays within bounds
35
- *
36
- * @param basePath - The allowed base directory (collections path)
37
- * @param requestedPath - The path requested by the user/agent
38
- * @returns The resolved absolute path
39
- * @throws PathEscapeError if the path would escape the base directory
40
- */
41
- export function resolvePath(basePath: string, requestedPath: string): string {
42
- // Normalize the base path
43
- const normalizedBase = path.resolve(basePath);
44
-
45
- // Resolve the requested path relative to the base
46
- let resolved: string;
47
- if (path.isAbsolute(requestedPath)) {
48
- resolved = path.resolve(requestedPath);
49
- } else {
50
- resolved = path.resolve(normalizedBase, requestedPath);
51
- }
52
-
53
- // Normalize to remove any .. or . components
54
- resolved = path.normalize(resolved);
55
-
56
- // Check that the resolved path starts with the base path
57
- // We need to ensure the path is either exactly the base or within it
58
- const relative = path.relative(normalizedBase, resolved);
59
-
60
- // If the relative path starts with '..' or is absolute, it's outside the base
61
- if (relative.startsWith('..') || path.isAbsolute(relative)) {
62
- throw new PathEscapeError(requestedPath, basePath);
63
- }
64
-
65
- return resolved;
66
- }
67
-
68
- /**
69
- * Resolve a path and follow symlinks, validating both the path and its target
70
- *
71
- * @param basePath - The allowed base directory (collections path)
72
- * @param requestedPath - The path requested by the user/agent
73
- * @returns The resolved real path (after following symlinks)
74
- * @throws PathEscapeError if the path or symlink target would escape the base directory
75
- */
76
- export async function resolvePathWithSymlinks(
77
- basePath: string,
78
- requestedPath: string
79
- ): Promise<string> {
80
- // First validate the path itself
81
- const resolved = resolvePath(basePath, requestedPath);
82
-
83
- try {
84
- // Get the real path (follows symlinks)
85
- const realPath = await fs.realpath(resolved);
86
-
87
- // For symlinks pointing outside, we allow it since the collection
88
- // symlinks resources from various locations. The sandbox is about
89
- // what the agent can ACCESS through the collection, not where the
90
- // actual files live.
91
- //
92
- // The key security boundary is that:
93
- // 1. The agent can only request paths within the collection directory
94
- // 2. Those paths may be symlinks to actual resource locations
95
- // 3. This is intentional - the collection IS the set of accessible resources
96
-
97
- return realPath;
98
- } catch (error) {
99
- // If realpath fails, the file doesn't exist
100
- // Return the resolved path anyway for error messages
101
- return resolved;
102
- }
103
- }
104
-
105
- /**
106
- * Check if a path exists and is within the sandbox
107
- */
108
- export async function exists(basePath: string, requestedPath: string): Promise<boolean> {
109
- try {
110
- const resolved = resolvePath(basePath, requestedPath);
111
- const file = Bun.file(resolved);
112
- return await file.exists();
113
- } catch {
114
- return false;
115
- }
116
- }
117
-
118
- /**
119
- * Check if a path is a directory
120
- */
121
- export async function isDirectory(basePath: string, requestedPath: string): Promise<boolean> {
122
- try {
123
- const resolved = resolvePath(basePath, requestedPath);
124
- const stats = await fs.stat(resolved);
125
- return stats.isDirectory();
126
- } catch {
127
- return false;
128
- }
129
- }
130
-
131
- /**
132
- * Check if a path is a file
133
- */
134
- export async function isFile(basePath: string, requestedPath: string): Promise<boolean> {
135
- try {
136
- const resolved = resolvePath(basePath, requestedPath);
137
- const stats = await fs.stat(resolved);
138
- return stats.isFile();
139
- } catch {
140
- return false;
141
- }
142
- }
143
-
144
- /**
145
- * Validate a path exists and is within sandbox, throwing if not
146
- */
147
- export async function validatePath(basePath: string, requestedPath: string): Promise<string> {
148
- const resolved = resolvePath(basePath, requestedPath);
149
-
150
- const file = Bun.file(resolved);
151
- if (!(await file.exists())) {
152
- throw new PathNotFoundError(requestedPath);
153
- }
154
-
155
- return resolved;
156
- }
157
-
158
- /**
159
- * Get the relative path from base to the resolved path
160
- */
161
- export function getRelativePath(basePath: string, resolvedPath: string): string {
162
- return path.relative(basePath, resolvedPath);
163
- }
164
- }