bashkit 0.2.2 → 0.2.3

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
@@ -21,9 +21,9 @@ bun add bashkit ai @ai-sdk/anthropic
21
21
  Runs commands directly on the local machine. Use for development/testing only.
22
22
 
23
23
  ```typescript
24
- import { createAgentTools, LocalSandbox } from "bashkit";
24
+ import { createAgentTools, createLocalSandbox } from "bashkit";
25
25
 
26
- const sandbox = new LocalSandbox("/tmp/workspace");
26
+ const sandbox = createLocalSandbox({ cwd: "/tmp/workspace" });
27
27
  const { tools } = createAgentTools(sandbox);
28
28
  ```
29
29
 
@@ -32,9 +32,9 @@ const { tools } = createAgentTools(sandbox);
32
32
  Runs in isolated Firecracker microVMs on Vercel's infrastructure.
33
33
 
34
34
  ```typescript
35
- import { createAgentTools, VercelSandbox } from "bashkit";
35
+ import { createAgentTools, createVercelSandbox } from "bashkit";
36
36
 
37
- const sandbox = new VercelSandbox({
37
+ const sandbox = createVercelSandbox({
38
38
  runtime: "node22",
39
39
  resources: { vcpus: 2 },
40
40
  });
@@ -44,6 +44,49 @@ const { tools } = createAgentTools(sandbox);
44
44
  await sandbox.destroy();
45
45
  ```
46
46
 
47
+ ### E2BSandbox (Production)
48
+
49
+ Runs in E2B's cloud sandboxes. Requires `@e2b/code-interpreter` peer dependency.
50
+
51
+ ```typescript
52
+ import { createAgentTools, createE2BSandbox } from "bashkit";
53
+
54
+ const sandbox = createE2BSandbox({
55
+ apiKey: process.env.E2B_API_KEY,
56
+ });
57
+ const { tools } = createAgentTools(sandbox);
58
+
59
+ await sandbox.destroy();
60
+ ```
61
+
62
+ ### Sandbox Reconnection (Cloud Sandboxes)
63
+
64
+ Cloud sandboxes (E2B, Vercel) support reconnection via the `id` property and `sandboxId` config:
65
+
66
+ ```typescript
67
+ // Create a new sandbox
68
+ const sandbox = createE2BSandbox({ apiKey: process.env.E2B_API_KEY });
69
+
70
+ // After first operation, the sandbox ID is available
71
+ await sandbox.exec("echo hello");
72
+ const sandboxId = sandbox.id; // "sbx_abc123..."
73
+
74
+ // Store sandboxId in your database (e.g., chat metadata)
75
+ await db.chat.update({ where: { id: chatId }, data: { sandboxId } });
76
+
77
+ // Later: reconnect to the same sandbox
78
+ const savedId = chat.sandboxId;
79
+ const reconnected = createE2BSandbox({
80
+ apiKey: process.env.E2B_API_KEY,
81
+ sandboxId: savedId, // Reconnects instead of creating new
82
+ });
83
+ ```
84
+
85
+ This is useful for:
86
+ - Reusing sandboxes across multiple requests in the same conversation
87
+ - Persisting sandbox state between server restarts
88
+ - Reducing sandbox creation overhead
89
+
47
90
  ## Available Tools
48
91
 
49
92
  ### Default Tools (always included)
@@ -89,11 +132,11 @@ import { generateText, wrapLanguageModel, stepCountIs } from "ai";
89
132
  import { anthropic } from "@ai-sdk/anthropic";
90
133
  import {
91
134
  createAgentTools,
92
- LocalSandbox,
135
+ createLocalSandbox,
93
136
  anthropicPromptCacheMiddleware,
94
137
  } from "bashkit";
95
138
 
96
- const sandbox = new LocalSandbox("/tmp/workspace");
139
+ const sandbox = createLocalSandbox({ cwd: "/tmp/workspace" });
97
140
  const { tools } = createAgentTools(sandbox);
98
141
 
99
142
  // Wrap model with prompt caching (recommended)
@@ -311,12 +354,12 @@ const skills = await discoverSkills();
311
354
  ### Using Skills with Agents
312
355
 
313
356
  ```typescript
314
- import { discoverSkills, skillsToXml, createAgentTools, LocalSandbox } from "bashkit";
357
+ import { discoverSkills, skillsToXml, createAgentTools, createLocalSandbox } from "bashkit";
315
358
  import { generateText, stepCountIs } from "ai";
316
359
  import { anthropic } from "@ai-sdk/anthropic";
317
360
 
318
361
  const skills = await discoverSkills();
319
- const sandbox = new LocalSandbox("/tmp/workspace");
362
+ const sandbox = createLocalSandbox({ cwd: "/tmp/workspace" });
320
363
  const { tools } = createAgentTools(sandbox);
321
364
 
322
365
  const result = await generateText({
@@ -421,13 +464,13 @@ import {
421
464
  createAgentTools,
422
465
  createTaskTool,
423
466
  createTodoWriteTool,
424
- LocalSandbox,
467
+ createLocalSandbox,
425
468
  anthropicPromptCacheMiddleware,
426
469
  type TodoState,
427
470
  } from "bashkit";
428
471
 
429
472
  // 1. Create sandbox
430
- const sandbox = new LocalSandbox("/tmp/workspace");
473
+ const sandbox = createLocalSandbox({ cwd: "/tmp/workspace" });
431
474
 
432
475
  // 2. Create sandbox tools
433
476
  const { tools: sandboxTools } = createAgentTools(sandbox);
@@ -489,9 +532,9 @@ const { tools } = createAgentTools(sandbox, {
489
532
  Cache tool execution results to avoid redundant operations:
490
533
 
491
534
  ```typescript
492
- import { createAgentTools, LocalSandbox } from "bashkit";
535
+ import { createAgentTools, createLocalSandbox } from "bashkit";
493
536
 
494
- const sandbox = new LocalSandbox("/tmp/workspace");
537
+ const sandbox = createLocalSandbox({ cwd: "/tmp/workspace" });
495
538
 
496
539
  // Enable caching with defaults (LRU, 5min TTL)
497
540
  const { tools } = createAgentTools(sandbox, { cache: true });
package/README.md CHANGED
@@ -148,6 +148,15 @@ const sandbox = createVercelSandbox({
148
148
  runtime: 'node22',
149
149
  resources: { vcpus: 2 },
150
150
  });
151
+
152
+ // After first operation, get the sandbox ID for persistence
153
+ await sandbox.exec('echo hello');
154
+ console.log(sandbox.id); // Sandbox ID for reconnection
155
+
156
+ // Later: reconnect to the same sandbox
157
+ const reconnected = createVercelSandbox({
158
+ sandboxId: 'existing-sandbox-id',
159
+ });
151
160
  ```
152
161
 
153
162
  ### E2BSandbox
@@ -158,7 +167,17 @@ Runs in E2B's cloud sandboxes. Requires `@e2b/code-interpreter` peer dependency.
158
167
  import { createE2BSandbox } from 'bashkit';
159
168
 
160
169
  const sandbox = createE2BSandbox({
161
- // E2B config
170
+ apiKey: process.env.E2B_API_KEY,
171
+ });
172
+
173
+ // After first operation, get the sandbox ID for persistence
174
+ await sandbox.exec('echo hello');
175
+ console.log(sandbox.id); // "sbx_abc123..."
176
+
177
+ // Later: reconnect to the same sandbox
178
+ const reconnected = createE2BSandbox({
179
+ apiKey: process.env.E2B_API_KEY,
180
+ sandboxId: 'sbx_abc123...', // Reconnect to existing sandbox
162
181
  });
163
182
  ```
164
183
 
@@ -764,9 +783,14 @@ interface Sandbox {
764
783
  readDir(path: string): Promise<string[]>;
765
784
  fileExists(path: string): Promise<boolean>;
766
785
  destroy(): Promise<void>;
786
+
787
+ // Optional: Sandbox ID for reconnection (cloud providers only)
788
+ readonly id?: string;
767
789
  }
768
790
  ```
769
791
 
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.
793
+
770
794
  ### Custom Sandbox Example
771
795
 
772
796
  ```typescript
package/dist/index.js CHANGED
@@ -50,7 +50,6 @@ var anthropicPromptCacheMiddleware = {
50
50
  transformParams: async ({ params }) => applyCacheMarkers(params)
51
51
  };
52
52
  // src/sandbox/e2b.ts
53
- import { Sandbox as E2BSandboxSDK } from "@e2b/code-interpreter";
54
53
  function createE2BSandbox(config = {}) {
55
54
  let sandbox = null;
56
55
  let sandboxId = config.sandboxId;
@@ -59,6 +58,13 @@ function createE2BSandbox(config = {}) {
59
58
  const ensureSandbox = async () => {
60
59
  if (sandbox)
61
60
  return sandbox;
61
+ let E2BSandboxSDK;
62
+ try {
63
+ const module = await import("@e2b/code-interpreter");
64
+ E2BSandboxSDK = module.Sandbox;
65
+ } catch {
66
+ throw new Error("E2BSandbox requires @e2b/code-interpreter. Install with: npm install @e2b/code-interpreter");
67
+ }
62
68
  if (config.sandboxId) {
63
69
  sandbox = await E2BSandboxSDK.connect(config.sandboxId);
64
70
  } else {
@@ -233,7 +239,6 @@ function createLocalSandbox(config = {}) {
233
239
  };
234
240
  }
235
241
  // src/sandbox/vercel.ts
236
- import { Sandbox as VercelSandboxSDK } from "@vercel/sandbox";
237
242
  function createVercelSandbox(config = {}) {
238
243
  let sandbox = null;
239
244
  let sandboxId = config.sandboxId;
@@ -246,6 +251,13 @@ function createVercelSandbox(config = {}) {
246
251
  const ensureSandbox = async () => {
247
252
  if (sandbox)
248
253
  return sandbox;
254
+ let VercelSandboxSDK;
255
+ try {
256
+ const module = await import("@vercel/sandbox");
257
+ VercelSandboxSDK = module.Sandbox;
258
+ } catch {
259
+ throw new Error("VercelSandbox requires @vercel/sandbox. Install with: npm install @vercel/sandbox");
260
+ }
249
261
  const createOptions = {
250
262
  runtime: resolvedConfig.runtime,
251
263
  resources: resolvedConfig.resources,
@@ -1390,13 +1402,56 @@ function createSkillTool(config) {
1390
1402
 
1391
1403
  // src/tools/web-fetch.ts
1392
1404
  import { generateText, tool as tool10, zodSchema as zodSchema10 } from "ai";
1393
- import Parallel from "parallel-web";
1394
1405
  import { z as z10 } from "zod";
1395
1406
 
1396
1407
  // src/utils/http-constants.ts
1397
1408
  var RETRYABLE_STATUS_CODES = [408, 429, 500, 502, 503];
1398
1409
 
1399
1410
  // src/tools/web-fetch.ts
1411
+ var parallelModule = null;
1412
+ async function getParallelModule() {
1413
+ if (!parallelModule) {
1414
+ try {
1415
+ parallelModule = await import("parallel-web");
1416
+ } catch {
1417
+ throw new Error("WebFetch requires parallel-web. Install with: npm install parallel-web");
1418
+ }
1419
+ }
1420
+ return parallelModule;
1421
+ }
1422
+ async function fetchWithParallel(url, apiKey) {
1423
+ const { default: Parallel } = await getParallelModule();
1424
+ const client = new Parallel({ apiKey });
1425
+ const extract = await client.beta.extract({
1426
+ urls: [url],
1427
+ excerpts: true,
1428
+ full_content: true
1429
+ });
1430
+ if (!extract.results || extract.results.length === 0) {
1431
+ throw new Error("No content extracted from URL");
1432
+ }
1433
+ const result = extract.results[0];
1434
+ const content = result.full_content || result.excerpts?.join(`
1435
+
1436
+ `) || "";
1437
+ if (!content) {
1438
+ throw new Error("No content available from URL");
1439
+ }
1440
+ return {
1441
+ content,
1442
+ finalUrl: result.url
1443
+ };
1444
+ }
1445
+ async function fetchContent(url, apiKey, provider) {
1446
+ switch (provider) {
1447
+ case "parallel":
1448
+ return fetchWithParallel(url, apiKey);
1449
+ default: {
1450
+ const _exhaustive = provider;
1451
+ throw new Error(`Unknown provider: ${_exhaustive}`);
1452
+ }
1453
+ }
1454
+ }
1400
1455
  var webFetchInputSchema = z10.object({
1401
1456
  url: z10.string().describe("The URL to fetch content from"),
1402
1457
  prompt: z10.string().describe("The prompt to run on the fetched content")
@@ -1418,7 +1473,14 @@ Usage notes:
1418
1473
  - When a URL redirects to a different host, the tool will inform you and provide the redirect URL. You should then make a new WebFetch request with the redirect URL to fetch the content.
1419
1474
  `;
1420
1475
  function createWebFetchTool(config) {
1421
- const { apiKey, model, strict, needsApproval, providerOptions } = config;
1476
+ const {
1477
+ provider = "parallel",
1478
+ apiKey,
1479
+ model,
1480
+ strict,
1481
+ needsApproval,
1482
+ providerOptions
1483
+ } = config;
1422
1484
  return tool10({
1423
1485
  description: WEB_FETCH_DESCRIPTION,
1424
1486
  inputSchema: zodSchema10(webFetchInputSchema),
@@ -1428,30 +1490,7 @@ function createWebFetchTool(config) {
1428
1490
  execute: async (input) => {
1429
1491
  const { url, prompt } = input;
1430
1492
  try {
1431
- const client = new Parallel({ apiKey });
1432
- const extract = await client.beta.extract({
1433
- urls: [url],
1434
- excerpts: true,
1435
- full_content: true
1436
- });
1437
- if (!extract.results || extract.results.length === 0) {
1438
- return {
1439
- error: "No content extracted from URL",
1440
- status_code: 404,
1441
- retryable: false
1442
- };
1443
- }
1444
- const extractedResult = extract.results[0];
1445
- const content = extractedResult.full_content || extractedResult.excerpts?.join(`
1446
-
1447
- `) || "";
1448
- if (!content) {
1449
- return {
1450
- error: "No content available from URL",
1451
- status_code: 404,
1452
- retryable: false
1453
- };
1454
- }
1493
+ const { content, finalUrl } = await fetchContent(url, apiKey, provider);
1455
1494
  const result = await generateText({
1456
1495
  model,
1457
1496
  prompt: `${prompt}
@@ -1463,7 +1502,7 @@ ${content}`
1463
1502
  return {
1464
1503
  response: result.text,
1465
1504
  url,
1466
- final_url: extractedResult.url || url
1505
+ final_url: finalUrl || url
1467
1506
  };
1468
1507
  } catch (error) {
1469
1508
  if (error && typeof error === "object" && "status" in error) {
@@ -1485,8 +1524,53 @@ ${content}`
1485
1524
 
1486
1525
  // src/tools/web-search.ts
1487
1526
  import { tool as tool11, zodSchema as zodSchema11 } from "ai";
1488
- import Parallel2 from "parallel-web";
1489
1527
  import { z as z11 } from "zod";
1528
+ var parallelModule2 = null;
1529
+ async function getParallelModule2() {
1530
+ if (!parallelModule2) {
1531
+ try {
1532
+ parallelModule2 = await import("parallel-web");
1533
+ } catch {
1534
+ throw new Error("WebSearch requires parallel-web. Install with: npm install parallel-web");
1535
+ }
1536
+ }
1537
+ return parallelModule2;
1538
+ }
1539
+ async function searchWithParallel(apiKey, options) {
1540
+ const { default: Parallel } = await getParallelModule2();
1541
+ const client = new Parallel({ apiKey });
1542
+ const sourcePolicy = options.allowedDomains || options.blockedDomains ? {
1543
+ ...options.allowedDomains && {
1544
+ include_domains: options.allowedDomains
1545
+ },
1546
+ ...options.blockedDomains && {
1547
+ exclude_domains: options.blockedDomains
1548
+ }
1549
+ } : undefined;
1550
+ const search = await client.beta.search({
1551
+ mode: "agentic",
1552
+ objective: options.query,
1553
+ max_results: 10,
1554
+ ...sourcePolicy && { source_policy: sourcePolicy }
1555
+ });
1556
+ return (search.results || []).map((result) => ({
1557
+ title: result.title ?? "",
1558
+ url: result.url ?? "",
1559
+ snippet: result.excerpts?.join(`
1560
+ `) ?? "",
1561
+ metadata: result.publish_date ? { publish_date: result.publish_date } : undefined
1562
+ }));
1563
+ }
1564
+ async function searchContent(apiKey, provider, options) {
1565
+ switch (provider) {
1566
+ case "parallel":
1567
+ return searchWithParallel(apiKey, options);
1568
+ default: {
1569
+ const _exhaustive = provider;
1570
+ throw new Error(`Unknown provider: ${_exhaustive}`);
1571
+ }
1572
+ }
1573
+ }
1490
1574
  var webSearchInputSchema = z11.object({
1491
1575
  query: z11.string().describe("The search query to use"),
1492
1576
  allowed_domains: z11.array(z11.string()).optional().describe("Only include results from these domains"),
@@ -1513,7 +1597,13 @@ When searching for recent information, documentation, or current events, use the
1513
1597
  - allowed_domains: Only include results from these domains
1514
1598
  - blocked_domains: Never include results from these domains`;
1515
1599
  function createWebSearchTool(config) {
1516
- const { apiKey, strict, needsApproval, providerOptions } = config;
1600
+ const {
1601
+ provider = "parallel",
1602
+ apiKey,
1603
+ strict,
1604
+ needsApproval,
1605
+ providerOptions
1606
+ } = config;
1517
1607
  return tool11({
1518
1608
  description: WEB_SEARCH_DESCRIPTION,
1519
1609
  inputSchema: zodSchema11(webSearchInputSchema),
@@ -1523,24 +1613,11 @@ function createWebSearchTool(config) {
1523
1613
  execute: async (input) => {
1524
1614
  const { query, allowed_domains, blocked_domains } = input;
1525
1615
  try {
1526
- const client = new Parallel2({ apiKey });
1527
- const sourcePolicy = allowed_domains || blocked_domains ? {
1528
- ...allowed_domains && { include_domains: allowed_domains },
1529
- ...blocked_domains && { exclude_domains: blocked_domains }
1530
- } : undefined;
1531
- const search = await client.beta.search({
1532
- mode: "agentic",
1533
- objective: query,
1534
- max_results: 10,
1535
- ...sourcePolicy && { source_policy: sourcePolicy }
1616
+ const results = await searchContent(apiKey, provider, {
1617
+ query,
1618
+ allowedDomains: allowed_domains,
1619
+ blockedDomains: blocked_domains
1536
1620
  });
1537
- const results = (search.results || []).map((result) => ({
1538
- title: result.title ?? "",
1539
- url: result.url ?? "",
1540
- snippet: result.excerpts?.join(`
1541
- `) ?? "",
1542
- metadata: result.publish_date ? { publish_date: result.publish_date } : undefined
1543
- }));
1544
1621
  return {
1545
1622
  results,
1546
1623
  total_results: results.length,
@@ -69,7 +69,7 @@ export type { SubagentEventData, SubagentStepEvent, SubagentTypeConfig, TaskErro
69
69
  export { createTaskTool } from "./task";
70
70
  export type { TodoItem, TodoState, TodoWriteError, TodoWriteOutput, } from "./todo-write";
71
71
  export { createTodoWriteTool } from "./todo-write";
72
- export type { WebFetchError, WebFetchOutput } from "./web-fetch";
72
+ export type { ExtractResult, WebFetchError, WebFetchOutput } from "./web-fetch";
73
73
  export { createWebFetchTool } from "./web-fetch";
74
74
  export type { WebSearchError, WebSearchOutput, WebSearchResult, } from "./web-search";
75
75
  export { createWebSearchTool } from "./web-search";
@@ -10,6 +10,14 @@ export interface WebFetchError {
10
10
  status_code?: number;
11
11
  retryable?: boolean;
12
12
  }
13
+ /**
14
+ * Result from a web fetch provider's extract operation.
15
+ * New providers should return this shape.
16
+ */
17
+ export interface ExtractResult {
18
+ content: string;
19
+ finalUrl?: string;
20
+ }
13
21
  export declare function createWebFetchTool(config: WebFetchConfig): import("ai").Tool<{
14
22
  url: string;
15
23
  prompt: string;
package/dist/types.d.ts CHANGED
@@ -25,10 +25,26 @@ export type GrepToolConfig = ToolConfig & {
25
25
  /** Use ripgrep (rg) instead of grep. Requires ripgrep to be installed. Default: false */
26
26
  useRipgrep?: boolean;
27
27
  };
28
+ /**
29
+ * Supported web search providers.
30
+ * Currently only 'parallel' is implemented.
31
+ * Add new providers here as union types (e.g., 'parallel' | 'serper' | 'tavily')
32
+ */
33
+ export type WebSearchProvider = "parallel";
34
+ /**
35
+ * Supported web fetch providers.
36
+ * Currently only 'parallel' is implemented.
37
+ * Add new providers here as union types (e.g., 'parallel' | 'firecrawl' | 'jina')
38
+ */
39
+ export type WebFetchProvider = "parallel";
28
40
  export type WebSearchConfig = {
41
+ /** Provider to use for web search. Default: 'parallel' */
42
+ provider?: WebSearchProvider;
29
43
  apiKey: string;
30
44
  } & SDKToolOptions;
31
45
  export type WebFetchConfig = {
46
+ /** Provider to use for web fetching. Default: 'parallel' */
47
+ provider?: WebFetchProvider;
32
48
  apiKey: string;
33
49
  model: LanguageModel;
34
50
  } & SDKToolOptions;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bashkit",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Agentic coding tools for the Vercel AI SDK",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",