bashkit 0.2.3 → 0.3.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.
package/AGENTS.md CHANGED
@@ -34,9 +34,12 @@ Runs in isolated Firecracker microVMs on Vercel's infrastructure.
34
34
  ```typescript
35
35
  import { createAgentTools, createVercelSandbox } from "bashkit";
36
36
 
37
- const sandbox = createVercelSandbox({
37
+ // Async - automatically installs ripgrep for Grep tool
38
+ const sandbox = await createVercelSandbox({
38
39
  runtime: "node22",
39
40
  resources: { vcpus: 2 },
41
+ // ensureTools: true (default) - auto-setup ripgrep
42
+ // ensureTools: false - skip for faster startup if you don't need Grep
40
43
  });
41
44
  const { tools } = createAgentTools(sandbox);
42
45
 
@@ -51,8 +54,11 @@ Runs in E2B's cloud sandboxes. Requires `@e2b/code-interpreter` peer dependency.
51
54
  ```typescript
52
55
  import { createAgentTools, createE2BSandbox } from "bashkit";
53
56
 
54
- const sandbox = createE2BSandbox({
57
+ // Async - automatically installs ripgrep for Grep tool
58
+ const sandbox = await createE2BSandbox({
55
59
  apiKey: process.env.E2B_API_KEY,
60
+ // ensureTools: true (default) - auto-setup ripgrep
61
+ // ensureTools: false - skip for faster startup if you don't need Grep
56
62
  });
57
63
  const { tools } = createAgentTools(sandbox);
58
64
 
@@ -65,18 +71,17 @@ Cloud sandboxes (E2B, Vercel) support reconnection via the `id` property and `sa
65
71
 
66
72
  ```typescript
67
73
  // Create a new sandbox
68
- const sandbox = createE2BSandbox({ apiKey: process.env.E2B_API_KEY });
74
+ const sandbox = await createE2BSandbox({ apiKey: process.env.E2B_API_KEY });
69
75
 
70
- // After first operation, the sandbox ID is available
71
- await sandbox.exec("echo hello");
76
+ // Sandbox ID is available immediately after creation
72
77
  const sandboxId = sandbox.id; // "sbx_abc123..."
73
78
 
74
79
  // Store sandboxId in your database (e.g., chat metadata)
75
80
  await db.chat.update({ where: { id: chatId }, data: { sandboxId } });
76
81
 
77
- // Later: reconnect to the same sandbox
82
+ // Later: reconnect to the same sandbox (fast - ripgrep already installed)
78
83
  const savedId = chat.sandboxId;
79
- const reconnected = createE2BSandbox({
84
+ const reconnected = await createE2BSandbox({
80
85
  apiKey: process.env.E2B_API_KEY,
81
86
  sandboxId: savedId, // Reconnects instead of creating new
82
87
  });
@@ -442,7 +447,7 @@ const config = {
442
447
  },
443
448
  };
444
449
 
445
- const sandbox = createVercelSandbox({});
450
+ const sandbox = await createVercelSandbox({});
446
451
  const { skills } = await setupAgentEnvironment(sandbox, config);
447
452
 
448
453
  // Use same config in prompt - stays in sync!
package/README.md CHANGED
@@ -69,7 +69,8 @@ import { anthropic } from '@ai-sdk/anthropic';
69
69
  import { streamText, stepCountIs } from 'ai';
70
70
 
71
71
  // Create a Vercel sandbox (isolated Firecracker microVM)
72
- const sandbox = createVercelSandbox({
72
+ // Note: async - automatically sets up ripgrep for Grep tool
73
+ const sandbox = await createVercelSandbox({
73
74
  runtime: 'node22',
74
75
  resources: { vcpus: 2 },
75
76
  });
@@ -144,17 +145,19 @@ Runs in isolated Firecracker microVMs on Vercel's infrastructure. **Use when you
144
145
  ```typescript
145
146
  import { createVercelSandbox } from 'bashkit';
146
147
 
147
- const sandbox = createVercelSandbox({
148
+ // Async - automatically installs ripgrep for Grep tool
149
+ const sandbox = await createVercelSandbox({
148
150
  runtime: 'node22',
149
151
  resources: { vcpus: 2 },
152
+ // ensureTools: true (default) - auto-setup ripgrep
153
+ // ensureTools: false - skip for faster startup if you don't need Grep
150
154
  });
151
155
 
152
- // After first operation, get the sandbox ID for persistence
153
- await sandbox.exec('echo hello');
156
+ // Sandbox ID available immediately after creation
154
157
  console.log(sandbox.id); // Sandbox ID for reconnection
155
158
 
156
- // Later: reconnect to the same sandbox
157
- const reconnected = createVercelSandbox({
159
+ // Later: reconnect to the same sandbox (fast - ripgrep already installed)
160
+ const reconnected = await createVercelSandbox({
158
161
  sandboxId: 'existing-sandbox-id',
159
162
  });
160
163
  ```
@@ -166,16 +169,18 @@ Runs in E2B's cloud sandboxes. Requires `@e2b/code-interpreter` peer dependency.
166
169
  ```typescript
167
170
  import { createE2BSandbox } from 'bashkit';
168
171
 
169
- const sandbox = createE2BSandbox({
172
+ // Async - automatically installs ripgrep for Grep tool
173
+ const sandbox = await createE2BSandbox({
170
174
  apiKey: process.env.E2B_API_KEY,
175
+ // ensureTools: true (default) - auto-setup ripgrep
176
+ // ensureTools: false - skip for faster startup if you don't need Grep
171
177
  });
172
178
 
173
- // After first operation, get the sandbox ID for persistence
174
- await sandbox.exec('echo hello');
179
+ // Sandbox ID available immediately after creation
175
180
  console.log(sandbox.id); // "sbx_abc123..."
176
181
 
177
- // Later: reconnect to the same sandbox
178
- const reconnected = createE2BSandbox({
182
+ // Later: reconnect to the same sandbox (fast - ripgrep already installed)
183
+ const reconnected = await createE2BSandbox({
179
184
  apiKey: process.env.E2B_API_KEY,
180
185
  sandboxId: 'sbx_abc123...', // Reconnect to existing sandbox
181
186
  });
@@ -782,14 +787,20 @@ interface Sandbox {
782
787
  writeFile(path: string, content: string): Promise<void>;
783
788
  readDir(path: string): Promise<string[]>;
784
789
  fileExists(path: string): Promise<boolean>;
790
+ isDirectory(path: string): Promise<boolean>;
785
791
  destroy(): Promise<void>;
786
792
 
787
793
  // Optional: Sandbox ID for reconnection (cloud providers only)
788
794
  readonly id?: string;
795
+
796
+ // Path to ripgrep binary (set by ensureSandboxTools)
797
+ rgPath?: string;
789
798
  }
790
799
  ```
791
800
 
792
- The `id` property is available on cloud sandboxes (E2B, Vercel) after the first operation. Use it to persist the sandbox ID and reconnect later.
801
+ The `id` property is available on cloud sandboxes (E2B, Vercel) after creation. Use it to persist the sandbox ID and reconnect later.
802
+
803
+ The `rgPath` property is set by `ensureSandboxTools()` (called automatically during sandbox creation). It points to the ripgrep binary for the Grep tool. Supports x86_64 and ARM64 architectures.
793
804
 
794
805
  ### Custom Sandbox Example
795
806
 
@@ -866,9 +877,10 @@ Creates a set of agent tools bound to a sandbox instance.
866
877
 
867
878
  ### Sandbox Factories
868
879
 
869
- - `createLocalSandbox(config?)` - Local execution sandbox
870
- - `createVercelSandbox(config?)` - Vercel Firecracker sandbox
871
- - `createE2BSandbox(config?)` - E2B cloud sandbox
880
+ - `createLocalSandbox(config?)` - Local execution sandbox (sync)
881
+ - `createVercelSandbox(config?)` - Vercel Firecracker sandbox (async, auto-installs ripgrep)
882
+ - `createE2BSandbox(config?)` - E2B cloud sandbox (async, auto-installs ripgrep)
883
+ - `ensureSandboxTools(sandbox)` - Manually setup tools (called automatically by default)
872
884
 
873
885
  ### Workflow Tools
874
886
 
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export type { UIMessageStreamWriter, StreamTextResult, Tool, ToolSet, LanguageModel, LanguageModelMiddleware, Output, } from "ai";
2
2
  export { anthropicPromptCacheMiddleware, anthropicPromptCacheMiddlewareV2, } from "./middleware";
3
3
  export type { E2BSandboxConfig, LocalSandboxConfig, VercelSandboxConfig, } from "./sandbox";
4
- export { createE2BSandbox, createLocalSandbox, createVercelSandbox, } from "./sandbox";
4
+ export { createE2BSandbox, createLocalSandbox, createVercelSandbox, ensureSandboxTools, } from "./sandbox";
5
5
  export type { ExecOptions, ExecResult, Sandbox } from "./sandbox/interface";
6
6
  export type { AgentToolsResult, AskUserError, AskUserOutput, AskUserResponseHandler, BashError, BashOutput, EditError, EditOutput, EnterPlanModeError, EnterPlanModeOutput, ExitPlanModeError, ExitPlanModeOutput, PlanModeState, GlobError, GlobOutput, GrepContentOutput, GrepCountOutput, GrepError, GrepFilesOutput, GrepMatch, GrepOutput, ReadDirectoryOutput, ReadError, ReadOutput, ReadTextOutput, SkillError, SkillOutput, SkillToolConfig, SubagentEventData, SubagentStepEvent, SubagentTypeConfig, TaskError, TaskOutput, TaskToolConfig, TodoItem, TodoState, TodoWriteError, TodoWriteOutput, WebFetchError, WebFetchOutput, WebSearchError, WebSearchOutput, WebSearchResult, WriteError, WriteOutput, } from "./tools";
7
7
  export { createAgentTools, createAskUserTool, createBashTool, createEditTool, createEnterPlanModeTool, createExitPlanModeTool, createGlobTool, createGrepTool, createReadTool, createSkillTool, createTaskTool, createTodoWriteTool, createWebFetchTool, createWebSearchTool, createWriteTool, } from "./tools";
package/dist/index.js CHANGED
@@ -49,15 +49,71 @@ var anthropicPromptCacheMiddleware = {
49
49
  specificationVersion: "v3",
50
50
  transformParams: async ({ params }) => applyCacheMarkers(params)
51
51
  };
52
+ // src/sandbox/lazy-singleton.ts
53
+ function createLazySingleton(factory) {
54
+ let promise = null;
55
+ return {
56
+ get: () => {
57
+ if (!promise) {
58
+ promise = factory();
59
+ }
60
+ return promise;
61
+ },
62
+ reset: () => {
63
+ promise = null;
64
+ }
65
+ };
66
+ }
67
+
68
+ // src/sandbox/ensure-tools.ts
69
+ import { rgPath as bundledRgPath } from "@vscode/ripgrep";
70
+ var RIPGREP_VERSION = "14.1.0";
71
+ var ARCH_MAP = {
72
+ x86_64: "x86_64-unknown-linux-musl",
73
+ aarch64: "aarch64-unknown-linux-gnu",
74
+ arm64: "aarch64-unknown-linux-gnu"
75
+ };
76
+ async function ensureSandboxTools(sandbox) {
77
+ const bundledCheck = await sandbox.exec(`test -x "${bundledRgPath}" && echo found`);
78
+ if (bundledCheck.stdout.includes("found")) {
79
+ sandbox.rgPath = bundledRgPath;
80
+ return;
81
+ }
82
+ const tmpCheck = await sandbox.exec("test -x /tmp/rg && echo found");
83
+ if (tmpCheck.stdout.includes("found")) {
84
+ sandbox.rgPath = "/tmp/rg";
85
+ return;
86
+ }
87
+ const systemCheck = await sandbox.exec("which rg 2>/dev/null");
88
+ if (systemCheck.exitCode === 0 && systemCheck.stdout.trim()) {
89
+ sandbox.rgPath = systemCheck.stdout.trim();
90
+ return;
91
+ }
92
+ const archResult = await sandbox.exec("uname -m");
93
+ const arch = archResult.stdout.trim();
94
+ const ripgrepArch = ARCH_MAP[arch];
95
+ if (!ripgrepArch) {
96
+ throw new Error(`Unsupported architecture: ${arch}. Supported: ${Object.keys(ARCH_MAP).join(", ")}`);
97
+ }
98
+ const ripgrepUrl = `https://github.com/BurntSushi/ripgrep/releases/download/${RIPGREP_VERSION}/ripgrep-${RIPGREP_VERSION}-${ripgrepArch}.tar.gz`;
99
+ const tarPath = `ripgrep-${RIPGREP_VERSION}-${ripgrepArch}/rg`;
100
+ const installResult = await sandbox.exec(`
101
+ curl -sL "${ripgrepUrl}" |
102
+ tar xzf - -C /tmp --strip-components=1 ${tarPath} &&
103
+ chmod +x /tmp/rg
104
+ `);
105
+ if (installResult.exitCode !== 0) {
106
+ throw new Error(`Failed to install ripgrep: ${installResult.stderr}`);
107
+ }
108
+ sandbox.rgPath = "/tmp/rg";
109
+ }
110
+
52
111
  // src/sandbox/e2b.ts
53
- function createE2BSandbox(config = {}) {
54
- let sandbox = null;
112
+ async function createE2BSandbox(config = {}) {
55
113
  let sandboxId = config.sandboxId;
56
114
  const workingDirectory = config.cwd || "/home/user";
57
115
  const timeout = config.timeout ?? 300000;
58
- const ensureSandbox = async () => {
59
- if (sandbox)
60
- return sandbox;
116
+ const sandbox = createLazySingleton(async () => {
61
117
  let E2BSandboxSDK;
62
118
  try {
63
119
  const module = await import("@e2b/code-interpreter");
@@ -65,20 +121,21 @@ function createE2BSandbox(config = {}) {
65
121
  } catch {
66
122
  throw new Error("E2BSandbox requires @e2b/code-interpreter. Install with: npm install @e2b/code-interpreter");
67
123
  }
124
+ let sbx;
68
125
  if (config.sandboxId) {
69
- sandbox = await E2BSandboxSDK.connect(config.sandboxId);
126
+ sbx = await E2BSandboxSDK.connect(config.sandboxId);
70
127
  } else {
71
- sandbox = await E2BSandboxSDK.create({
128
+ sbx = await E2BSandboxSDK.create({
72
129
  apiKey: config.apiKey,
73
130
  timeoutMs: timeout,
74
131
  metadata: config.metadata
75
132
  });
76
- sandboxId = sandbox.sandboxId;
133
+ sandboxId = sbx.sandboxId;
77
134
  }
78
- return sandbox;
79
- };
135
+ return sbx;
136
+ });
80
137
  const exec = async (command, options) => {
81
- const sbx = await ensureSandbox();
138
+ const sbx = await sandbox.get();
82
139
  const startTime = performance.now();
83
140
  try {
84
141
  const result = await sbx.commands.run(command, {
@@ -118,11 +175,18 @@ function createE2BSandbox(config = {}) {
118
175
  throw error;
119
176
  }
120
177
  };
121
- return {
178
+ let rgPath;
179
+ const sandboxObj = {
122
180
  exec,
123
181
  get id() {
124
182
  return sandboxId;
125
183
  },
184
+ get rgPath() {
185
+ return rgPath;
186
+ },
187
+ set rgPath(path) {
188
+ rgPath = path;
189
+ },
126
190
  async readFile(path) {
127
191
  const result = await exec(`cat "${path}"`);
128
192
  if (result.exitCode !== 0) {
@@ -131,7 +195,7 @@ function createE2BSandbox(config = {}) {
131
195
  return result.stdout;
132
196
  },
133
197
  async writeFile(path, content) {
134
- const sbx = await ensureSandbox();
198
+ const sbx = await sandbox.get();
135
199
  await sbx.files.write(path, content);
136
200
  },
137
201
  async readDir(path) {
@@ -151,15 +215,21 @@ function createE2BSandbox(config = {}) {
151
215
  return result.exitCode === 0;
152
216
  },
153
217
  async destroy() {
154
- if (sandbox) {
155
- await sandbox.kill();
156
- sandbox = null;
157
- }
218
+ try {
219
+ const sbx = await sandbox.get();
220
+ await sbx.kill();
221
+ } catch {}
222
+ sandbox.reset();
158
223
  }
159
224
  };
225
+ if (config.ensureTools !== false) {
226
+ await ensureSandboxTools(sandboxObj);
227
+ }
228
+ return sandboxObj;
160
229
  }
161
230
  // src/sandbox/local.ts
162
231
  import { existsSync, mkdirSync } from "node:fs";
232
+ import { rgPath as bundledRgPath2 } from "@vscode/ripgrep";
163
233
  function createLocalSandbox(config = {}) {
164
234
  const workingDirectory = config.cwd || "/tmp";
165
235
  if (!existsSync(workingDirectory)) {
@@ -201,6 +271,7 @@ function createLocalSandbox(config = {}) {
201
271
  };
202
272
  return {
203
273
  exec,
274
+ rgPath: bundledRgPath2,
204
275
  async readFile(path) {
205
276
  const fullPath = path.startsWith("/") ? path : `${workingDirectory}/${path}`;
206
277
  const file = Bun.file(fullPath);
@@ -239,8 +310,7 @@ function createLocalSandbox(config = {}) {
239
310
  };
240
311
  }
241
312
  // src/sandbox/vercel.ts
242
- function createVercelSandbox(config = {}) {
243
- let sandbox = null;
313
+ async function createVercelSandbox(config = {}) {
244
314
  let sandboxId = config.sandboxId;
245
315
  const workingDirectory = config.cwd || "/vercel/sandbox";
246
316
  const resolvedConfig = {
@@ -248,9 +318,7 @@ function createVercelSandbox(config = {}) {
248
318
  resources: config.resources ?? { vcpus: 2 },
249
319
  timeout: config.timeout ?? 300000
250
320
  };
251
- const ensureSandbox = async () => {
252
- if (sandbox)
253
- return sandbox;
321
+ const sandbox = createLazySingleton(async () => {
254
322
  let VercelSandboxSDK;
255
323
  try {
256
324
  const module = await import("@vercel/sandbox");
@@ -269,16 +337,17 @@ function createVercelSandbox(config = {}) {
269
337
  token: config.token
270
338
  });
271
339
  }
340
+ let sbx;
272
341
  if (config.sandboxId) {
273
- sandbox = await VercelSandboxSDK.get({ sandboxId: config.sandboxId });
342
+ sbx = await VercelSandboxSDK.get({ sandboxId: config.sandboxId });
274
343
  } else {
275
- sandbox = await VercelSandboxSDK.create(createOptions);
344
+ sbx = await VercelSandboxSDK.create(createOptions);
276
345
  }
277
- sandboxId = sandbox.sandboxId;
278
- return sandbox;
279
- };
346
+ sandboxId = sbx.sandboxId;
347
+ return sbx;
348
+ });
280
349
  const exec = async (command, options) => {
281
- const sbx = await ensureSandbox();
350
+ const sbx = await sandbox.get();
282
351
  const startTime = performance.now();
283
352
  let interrupted = false;
284
353
  const abortController = new AbortController;
@@ -326,13 +395,20 @@ function createVercelSandbox(config = {}) {
326
395
  throw error;
327
396
  }
328
397
  };
329
- return {
398
+ let rgPath;
399
+ const sandboxObj = {
330
400
  exec,
331
401
  get id() {
332
402
  return sandboxId;
333
403
  },
404
+ get rgPath() {
405
+ return rgPath;
406
+ },
407
+ set rgPath(path) {
408
+ rgPath = path;
409
+ },
334
410
  async readFile(path) {
335
- const sbx = await ensureSandbox();
411
+ const sbx = await sandbox.get();
336
412
  const stream = await sbx.readFile({ path });
337
413
  if (!stream) {
338
414
  throw new Error(`File not found: ${path}`);
@@ -344,7 +420,7 @@ function createVercelSandbox(config = {}) {
344
420
  return Buffer.concat(chunks).toString("utf-8");
345
421
  },
346
422
  async writeFile(path, content) {
347
- const sbx = await ensureSandbox();
423
+ const sbx = await sandbox.get();
348
424
  await sbx.writeFiles([
349
425
  {
350
426
  path,
@@ -369,12 +445,17 @@ function createVercelSandbox(config = {}) {
369
445
  return result.exitCode === 0;
370
446
  },
371
447
  async destroy() {
372
- if (sandbox) {
373
- await sandbox.stop();
374
- sandbox = null;
375
- }
448
+ try {
449
+ const sbx = await sandbox.get();
450
+ await sbx.stop();
451
+ } catch {}
452
+ sandbox.reset();
376
453
  }
377
454
  };
455
+ if (config.ensureTools !== false) {
456
+ await ensureSandboxTools(sandboxObj);
457
+ }
458
+ return sandboxObj;
378
459
  }
379
460
  // src/cache/lru.ts
380
461
  class LRUCacheStore {
@@ -1004,13 +1085,13 @@ var grepInputSchema = z7.object({
1004
1085
  "-C": z7.number().optional().describe("Number of lines to show before and after each match. Requires output_mode: 'content'."),
1005
1086
  head_limit: z7.number().optional().describe("Limit output to first N lines/entries. Works across all output modes. Defaults to 0 (unlimited)."),
1006
1087
  offset: z7.number().optional().describe("Skip first N lines/entries before applying head_limit. Works across all output modes. Defaults to 0."),
1007
- multiline: z7.boolean().optional().describe("Enable multiline mode where patterns can span lines (requires ripgrep). Default: false.")
1088
+ multiline: z7.boolean().optional().describe("Enable multiline mode where patterns can span lines. Default: false.")
1008
1089
  });
1009
- var GREP_DESCRIPTION = `A powerful content search tool with regex support. Use this instead of running grep commands directly.
1090
+ var GREP_DESCRIPTION = `A powerful content search tool built on ripgrep with regex support.
1010
1091
 
1011
1092
  **Usage:**
1012
1093
  - ALWAYS use Grep for search tasks. NEVER invoke \`grep\` or \`rg\` as a Bash command.
1013
- - Supports regex syntax (e.g., "log.*Error", "function\\s+\\w+")
1094
+ - Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")
1014
1095
  - Filter files with glob parameter (e.g., "*.js", "**/*.tsx") or type parameter (e.g., "js", "py", "rust")
1015
1096
 
1016
1097
  **Output modes:**
@@ -1025,11 +1106,8 @@ var GREP_DESCRIPTION = `A powerful content search tool with regex support. Use t
1025
1106
 
1026
1107
  **Pagination:**
1027
1108
  - Use offset to skip results (useful for pagination)
1028
- - Use head_limit to limit total results returned
1029
-
1030
- **Note:** Set useRipgrep: true in config for better performance and multiline support (requires ripgrep installed).`;
1109
+ - Use head_limit to limit total results returned`;
1031
1110
  function createGrepTool(sandbox, config) {
1032
- const useRipgrep = config?.useRipgrep ?? false;
1033
1111
  return tool7({
1034
1112
  description: GREP_DESCRIPTION,
1035
1113
  inputSchema: zodSchema7(grepInputSchema),
@@ -1044,7 +1122,6 @@ function createGrepTool(sandbox, config) {
1044
1122
  type,
1045
1123
  output_mode = "files_with_matches",
1046
1124
  "-i": caseInsensitive,
1047
- "-n": showLineNumbers = true,
1048
1125
  "-B": beforeContext,
1049
1126
  "-A": afterContext,
1050
1127
  "-C": context,
@@ -1059,98 +1136,32 @@ function createGrepTool(sandbox, config) {
1059
1136
  return { error: `Path not allowed: ${searchPath}` };
1060
1137
  }
1061
1138
  }
1062
- if (multiline && !useRipgrep) {
1063
- return {
1064
- error: "Multiline mode requires ripgrep. Set useRipgrep: true in config."
1065
- };
1066
- }
1067
1139
  try {
1068
- let paginationSuffix = "";
1069
- if (offset > 0) {
1070
- paginationSuffix += ` | tail -n +${offset + 1}`;
1071
- }
1072
- if (head_limit && head_limit > 0) {
1073
- paginationSuffix += ` | head -${head_limit}`;
1074
- }
1075
- let cmd;
1076
- if (useRipgrep) {
1077
- cmd = buildRipgrepCommand({
1078
- pattern,
1079
- searchPath,
1080
- output_mode,
1081
- caseInsensitive,
1082
- showLineNumbers,
1083
- beforeContext,
1084
- afterContext,
1085
- context,
1086
- glob,
1087
- type,
1088
- multiline,
1089
- paginationSuffix
1090
- });
1091
- } else {
1092
- cmd = buildGrepCommand({
1093
- pattern,
1094
- searchPath,
1095
- output_mode,
1096
- caseInsensitive,
1097
- showLineNumbers,
1098
- beforeContext,
1099
- afterContext,
1100
- context,
1101
- glob,
1102
- type,
1103
- paginationSuffix
1104
- });
1140
+ if (!sandbox.rgPath) {
1141
+ return {
1142
+ error: "Ripgrep not available. Call ensureSandboxTools(sandbox) before using Grep with remote sandboxes."
1143
+ };
1105
1144
  }
1145
+ const cmd = buildRipgrepCommand({
1146
+ rgPath: sandbox.rgPath,
1147
+ pattern,
1148
+ searchPath,
1149
+ output_mode,
1150
+ caseInsensitive,
1151
+ beforeContext,
1152
+ afterContext,
1153
+ context,
1154
+ glob,
1155
+ type,
1156
+ multiline
1157
+ });
1106
1158
  const result = await sandbox.exec(cmd, { timeout: config?.timeout });
1107
1159
  if (output_mode === "files_with_matches") {
1108
- const files = result.stdout.split(`
1109
- `).filter(Boolean);
1110
- return {
1111
- files,
1112
- count: files.length
1113
- };
1160
+ return parseFilesOutput(result.stdout);
1114
1161
  } else if (output_mode === "count") {
1115
- const lines = result.stdout.split(`
1116
- `).filter(Boolean);
1117
- const counts = lines.map((line) => {
1118
- const lastColon = line.lastIndexOf(":");
1119
- return {
1120
- file: line.slice(0, lastColon),
1121
- count: parseInt(line.slice(lastColon + 1), 10)
1122
- };
1123
- });
1124
- const total = counts.reduce((sum, c) => sum + c.count, 0);
1125
- return {
1126
- counts,
1127
- total
1128
- };
1162
+ return parseCountOutput(result.stdout);
1129
1163
  } else {
1130
- if (!result.stdout.trim()) {
1131
- return {
1132
- matches: [],
1133
- total_matches: 0
1134
- };
1135
- }
1136
- const lines = result.stdout.split(`
1137
- `).filter(Boolean);
1138
- const matches = [];
1139
- for (const line of lines) {
1140
- const colonMatch = line.match(/^(.+?):(\d+)[:|-](.*)$/);
1141
- if (colonMatch) {
1142
- const [, file, lineNum, content] = colonMatch;
1143
- matches.push({
1144
- file,
1145
- line_number: parseInt(lineNum, 10),
1146
- line: content
1147
- });
1148
- }
1149
- }
1150
- return {
1151
- matches,
1152
- total_matches: matches.length
1153
- };
1164
+ return parseContentOutput(result.stdout, head_limit, offset);
1154
1165
  }
1155
1166
  } catch (error) {
1156
1167
  return {
@@ -1161,14 +1172,12 @@ function createGrepTool(sandbox, config) {
1161
1172
  });
1162
1173
  }
1163
1174
  function buildRipgrepCommand(opts) {
1164
- const flags = [];
1175
+ const flags = ["--json"];
1165
1176
  if (opts.caseInsensitive)
1166
1177
  flags.push("-i");
1167
1178
  if (opts.multiline)
1168
1179
  flags.push("-U", "--multiline-dotall");
1169
1180
  if (opts.output_mode === "content") {
1170
- if (opts.showLineNumbers)
1171
- flags.push("-n");
1172
1181
  if (opts.context) {
1173
1182
  flags.push(`-C ${opts.context}`);
1174
1183
  } else {
@@ -1183,42 +1192,137 @@ function buildRipgrepCommand(opts) {
1183
1192
  if (opts.type)
1184
1193
  flags.push(`-t ${opts.type}`);
1185
1194
  const flagStr = flags.join(" ");
1186
- if (opts.output_mode === "files_with_matches") {
1187
- return `rg -l ${flagStr} "${opts.pattern}" ${opts.searchPath} 2>/dev/null${opts.paginationSuffix}`;
1188
- } else if (opts.output_mode === "count") {
1189
- return `rg -c ${flagStr} "${opts.pattern}" ${opts.searchPath} 2>/dev/null${opts.paginationSuffix}`;
1190
- } else {
1191
- return `rg ${flagStr} "${opts.pattern}" ${opts.searchPath} 2>/dev/null${opts.paginationSuffix}`;
1195
+ return `${opts.rgPath} ${flagStr} "${opts.pattern}" ${opts.searchPath} 2>/dev/null`;
1196
+ }
1197
+ function parseFilesOutput(stdout) {
1198
+ const files = new Set;
1199
+ for (const line of stdout.split(`
1200
+ `).filter(Boolean)) {
1201
+ try {
1202
+ const msg = JSON.parse(line);
1203
+ if (msg.type === "begin") {
1204
+ const data = msg.data;
1205
+ files.add(data.path.text);
1206
+ }
1207
+ } catch {}
1192
1208
  }
1209
+ return {
1210
+ files: Array.from(files),
1211
+ count: files.size
1212
+ };
1193
1213
  }
1194
- function buildGrepCommand(opts) {
1195
- const flags = ["-r"];
1196
- if (opts.caseInsensitive)
1197
- flags.push("-i");
1198
- if (opts.output_mode === "content") {
1199
- if (opts.showLineNumbers)
1200
- flags.push("-n");
1201
- if (opts.context) {
1202
- flags.push(`-C ${opts.context}`);
1203
- } else {
1204
- if (opts.beforeContext)
1205
- flags.push(`-B ${opts.beforeContext}`);
1206
- if (opts.afterContext)
1207
- flags.push(`-A ${opts.afterContext}`);
1214
+ function parseCountOutput(stdout) {
1215
+ const counts = new Map;
1216
+ for (const line of stdout.split(`
1217
+ `).filter(Boolean)) {
1218
+ try {
1219
+ const msg = JSON.parse(line);
1220
+ if (msg.type === "end") {
1221
+ const data = msg.data;
1222
+ counts.set(data.path.text, data.stats.matches);
1223
+ }
1224
+ } catch {}
1225
+ }
1226
+ const countsArray = Array.from(counts.entries()).map(([file, count]) => ({
1227
+ file,
1228
+ count
1229
+ }));
1230
+ const total = countsArray.reduce((sum, c) => sum + c.count, 0);
1231
+ return {
1232
+ counts: countsArray,
1233
+ total
1234
+ };
1235
+ }
1236
+ function parseContentOutput(stdout, head_limit, offset = 0) {
1237
+ const fileData = new Map;
1238
+ for (const line of stdout.split(`
1239
+ `).filter(Boolean)) {
1240
+ try {
1241
+ const msg = JSON.parse(line);
1242
+ if (msg.type === "begin") {
1243
+ const data = msg.data;
1244
+ fileData.set(data.path.text, { matches: [], contexts: [] });
1245
+ } else if (msg.type === "context") {
1246
+ const data = msg.data;
1247
+ const fd = fileData.get(data.path.text);
1248
+ if (fd) {
1249
+ fd.contexts.push({
1250
+ line_number: data.line_number,
1251
+ text: data.lines.text.replace(/\n$/, "")
1252
+ });
1253
+ }
1254
+ } else if (msg.type === "match") {
1255
+ const data = msg.data;
1256
+ const fd = fileData.get(data.path.text);
1257
+ if (fd) {
1258
+ fd.matches.push({
1259
+ line_number: data.line_number,
1260
+ text: data.lines.text.replace(/\n$/, "")
1261
+ });
1262
+ }
1263
+ }
1264
+ } catch {}
1265
+ }
1266
+ const allMatches = [];
1267
+ for (const [file, { matches, contexts }] of fileData) {
1268
+ matches.sort((a, b) => a.line_number - b.line_number);
1269
+ contexts.sort((a, b) => a.line_number - b.line_number);
1270
+ const matchContexts = new Map;
1271
+ for (const match of matches) {
1272
+ matchContexts.set(match.line_number, { before: [], after: [] });
1273
+ }
1274
+ for (const ctx of contexts) {
1275
+ let bestMatch = null;
1276
+ let bestDistance = Infinity;
1277
+ let isBefore = false;
1278
+ for (const match of matches) {
1279
+ const distance = Math.abs(ctx.line_number - match.line_number);
1280
+ if (distance < bestDistance) {
1281
+ bestDistance = distance;
1282
+ bestMatch = match;
1283
+ isBefore = ctx.line_number < match.line_number;
1284
+ }
1285
+ }
1286
+ if (bestMatch) {
1287
+ const mc = matchContexts.get(bestMatch.line_number);
1288
+ if (mc) {
1289
+ if (isBefore) {
1290
+ mc.before.push(ctx.text);
1291
+ } else {
1292
+ mc.after.push(ctx.text);
1293
+ }
1294
+ }
1295
+ }
1296
+ }
1297
+ for (const match of matches) {
1298
+ const mc = matchContexts.get(match.line_number);
1299
+ allMatches.push({
1300
+ file,
1301
+ line_number: match.line_number,
1302
+ line: match.text,
1303
+ before_context: mc?.before ?? [],
1304
+ after_context: mc?.after ?? []
1305
+ });
1208
1306
  }
1209
1307
  }
1210
- if (opts.glob)
1211
- flags.push(`--include="${opts.glob}"`);
1212
- if (opts.type)
1213
- flags.push(`--include="*.${opts.type}"`);
1214
- const flagStr = flags.join(" ");
1215
- if (opts.output_mode === "files_with_matches") {
1216
- return `grep -l ${flagStr} "${opts.pattern}" ${opts.searchPath} 2>/dev/null${opts.paginationSuffix}`;
1217
- } else if (opts.output_mode === "count") {
1218
- return `grep -c ${flagStr} "${opts.pattern}" ${opts.searchPath} 2>/dev/null | grep -v ':0$'${opts.paginationSuffix}`;
1219
- } else {
1220
- return `grep ${flagStr} "${opts.pattern}" ${opts.searchPath} 2>/dev/null${opts.paginationSuffix}`;
1308
+ const grepMatches = allMatches.map((m) => ({
1309
+ file: m.file,
1310
+ line_number: m.line_number,
1311
+ line: m.line,
1312
+ before_context: m.before_context.length > 0 ? m.before_context : undefined,
1313
+ after_context: m.after_context.length > 0 ? m.after_context : undefined
1314
+ }));
1315
+ let result = grepMatches;
1316
+ if (offset > 0) {
1317
+ result = result.slice(offset);
1221
1318
  }
1319
+ if (head_limit && head_limit > 0) {
1320
+ result = result.slice(0, head_limit);
1321
+ }
1322
+ return {
1323
+ matches: result,
1324
+ total_matches: result.length
1325
+ };
1222
1326
  }
1223
1327
 
1224
1328
  // src/tools/read.ts
@@ -2760,6 +2864,7 @@ export {
2760
2864
  estimateTokens,
2761
2865
  estimateMessagesTokens,
2762
2866
  estimateMessageTokens,
2867
+ ensureSandboxTools,
2763
2868
  discoverSkills,
2764
2869
  createWriteTool,
2765
2870
  createWebSearchTool,
@@ -7,5 +7,10 @@ export interface E2BSandboxConfig {
7
7
  timeout?: number;
8
8
  cwd?: string;
9
9
  metadata?: Record<string, string>;
10
+ /**
11
+ * Ensure tools like ripgrep are available in the sandbox.
12
+ * Defaults to true. Set to false for faster startup if you don't need Grep.
13
+ */
14
+ ensureTools?: boolean;
10
15
  }
11
- export declare function createE2BSandbox(config?: E2BSandboxConfig): Sandbox;
16
+ export declare function createE2BSandbox(config?: E2BSandboxConfig): Promise<Sandbox>;
@@ -0,0 +1,22 @@
1
+ import type { Sandbox } from "./interface";
2
+ /**
3
+ * Ensures required tools (ripgrep) are available in the sandbox.
4
+ * Call this once during sandbox setup, before using tools like Grep.
5
+ *
6
+ * - For local sandboxes: verifies bundled binary exists
7
+ * - For remote sandboxes: installs ripgrep to /tmp/rg if not present
8
+ *
9
+ * Supports x86_64 and ARM64 architectures.
10
+ *
11
+ * After calling, `sandbox.rgPath` will be set to the correct path.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const sandbox = await createVercelSandbox();
16
+ * await ensureSandboxTools(sandbox);
17
+ *
18
+ * const { tools } = createAgentTools(sandbox);
19
+ * // Grep now works
20
+ * ```
21
+ */
22
+ export declare function ensureSandboxTools(sandbox: Sandbox): Promise<void>;
@@ -1,4 +1,5 @@
1
1
  export { createE2BSandbox, type E2BSandboxConfig } from "./e2b";
2
+ export { ensureSandboxTools } from "./ensure-tools";
2
3
  export type { ExecOptions, ExecResult, Sandbox } from "./interface";
3
4
  export { createLocalSandbox, type LocalSandboxConfig } from "./local";
4
5
  export { createVercelSandbox, type VercelSandboxConfig } from "./vercel";
@@ -25,4 +25,9 @@ export interface Sandbox {
25
25
  * - For local sandboxes: always undefined
26
26
  */
27
27
  readonly id?: string;
28
+ /**
29
+ * Path to ripgrep binary for this sandbox.
30
+ * Set by ensureSandboxTools() or defaults to bundled binary for local sandboxes.
31
+ */
32
+ rgPath?: string;
28
33
  }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Creates a lazy singleton that initializes on first access.
3
+ * Safe for concurrent calls - all callers await the same promise.
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * const sandbox = createLazySingleton(async () => {
8
+ * const sdk = await import("@vercel/sandbox");
9
+ * return sdk.Sandbox.create({ ... });
10
+ * });
11
+ *
12
+ * // Safe for parallel calls:
13
+ * const [a, b] = await Promise.all([sandbox.get(), sandbox.get()]);
14
+ * // a === b (same instance)
15
+ *
16
+ * // Reset for cleanup:
17
+ * sandbox.reset();
18
+ * ```
19
+ */
20
+ export declare function createLazySingleton<T>(factory: () => Promise<T>): {
21
+ /** Get the singleton instance, creating it if needed */
22
+ get: () => Promise<T>;
23
+ /** Reset the singleton, allowing a new instance to be created */
24
+ reset: () => void;
25
+ };
@@ -11,5 +11,10 @@ export interface VercelSandboxConfig {
11
11
  teamId?: string;
12
12
  projectId?: string;
13
13
  token?: string;
14
+ /**
15
+ * Ensure tools like ripgrep are available in the sandbox.
16
+ * Defaults to true. Set to false for faster startup if you don't need Grep.
17
+ */
18
+ ensureTools?: boolean;
14
19
  }
15
- export declare function createVercelSandbox(config?: VercelSandboxConfig): Sandbox;
20
+ export declare function createVercelSandbox(config?: VercelSandboxConfig): Promise<Sandbox>;
package/dist/types.d.ts CHANGED
@@ -21,10 +21,7 @@ export type ToolConfig = {
21
21
  allowedPaths?: string[];
22
22
  blockedCommands?: string[];
23
23
  } & SDKToolOptions;
24
- export type GrepToolConfig = ToolConfig & {
25
- /** Use ripgrep (rg) instead of grep. Requires ripgrep to be installed. Default: false */
26
- useRipgrep?: boolean;
27
- };
24
+ export type GrepToolConfig = ToolConfig;
28
25
  /**
29
26
  * Supported web search providers.
30
27
  * Currently only 'parallel' is implemented.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bashkit",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Agentic coding tools for the Vercel AI SDK",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "scripts": {
23
23
  "dev": "bun run src/index.ts",
24
24
  "build": "bun run build:js && bun run build:cli && bun run build:types",
25
- "build:js": "bun build src/index.ts --outdir dist --target node --format esm --external ai --external zod --external @ai-sdk/* --external @vercel/sandbox --external @e2b/code-interpreter --external parallel-web",
25
+ "build:js": "bun build src/index.ts --outdir dist --target node --format esm --external ai --external zod --external @ai-sdk/* --external @vercel/sandbox --external @e2b/code-interpreter --external parallel-web --external @vscode/ripgrep",
26
26
  "build:cli": "bun build src/cli/init.ts --outdir dist/cli --target node --format esm --external @clack/prompts && chmod +x dist/cli/init.js",
27
27
  "build:types": "tsc -p tsconfig.build.json",
28
28
  "typecheck": "tsc --noEmit",
@@ -51,7 +51,8 @@
51
51
  "url": "https://github.com/jbreite/bashkit"
52
52
  },
53
53
  "dependencies": {
54
- "@clack/prompts": "^0.7.0"
54
+ "@clack/prompts": "^0.7.0",
55
+ "@vscode/ripgrep": "^1.17.0"
55
56
  },
56
57
  "devDependencies": {
57
58
  "@ai-sdk/anthropic": "^3.0.1",