bashkit 0.1.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.
Files changed (41) hide show
  1. package/AGENTS.md +442 -0
  2. package/LICENSE +21 -0
  3. package/README.md +713 -0
  4. package/dist/cli/init.d.ts +2 -0
  5. package/dist/cli/init.js +179 -0
  6. package/dist/index.d.ts +14 -0
  7. package/dist/index.js +1805 -0
  8. package/dist/middleware/anthropic-cache.d.ts +17 -0
  9. package/dist/middleware/index.d.ts +1 -0
  10. package/dist/sandbox/e2b.d.ts +9 -0
  11. package/dist/sandbox/index.d.ts +4 -0
  12. package/dist/sandbox/interface.d.ts +21 -0
  13. package/dist/sandbox/local.d.ts +5 -0
  14. package/dist/sandbox/vercel.d.ts +13 -0
  15. package/dist/setup/index.d.ts +2 -0
  16. package/dist/setup/setup-environment.d.ts +36 -0
  17. package/dist/setup/types.d.ts +47 -0
  18. package/dist/skills/discovery.d.ts +9 -0
  19. package/dist/skills/fetch.d.ts +56 -0
  20. package/dist/skills/index.d.ts +6 -0
  21. package/dist/skills/loader.d.ts +11 -0
  22. package/dist/skills/types.d.ts +29 -0
  23. package/dist/skills/xml.d.ts +26 -0
  24. package/dist/tools/bash.d.ts +18 -0
  25. package/dist/tools/edit.d.ts +16 -0
  26. package/dist/tools/exit-plan-mode.d.ts +11 -0
  27. package/dist/tools/glob.d.ts +14 -0
  28. package/dist/tools/grep.d.ts +42 -0
  29. package/dist/tools/index.d.ts +45 -0
  30. package/dist/tools/read.d.ts +25 -0
  31. package/dist/tools/task.d.ts +50 -0
  32. package/dist/tools/todo-write.d.ts +28 -0
  33. package/dist/tools/web-fetch.d.ts +20 -0
  34. package/dist/tools/web-search.d.ts +24 -0
  35. package/dist/tools/write.d.ts +14 -0
  36. package/dist/types.d.ts +32 -0
  37. package/dist/utils/compact-conversation.d.ts +85 -0
  38. package/dist/utils/context-status.d.ts +71 -0
  39. package/dist/utils/index.d.ts +3 -0
  40. package/dist/utils/prune-messages.d.ts +32 -0
  41. package/package.json +84 -0
package/dist/index.js ADDED
@@ -0,0 +1,1805 @@
1
+ import { createRequire } from "node:module";
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __toESM = (mod, isNodeMode, target) => {
8
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
9
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
+ for (let key of __getOwnPropNames(mod))
11
+ if (!__hasOwnProp.call(to, key))
12
+ __defProp(to, key, {
13
+ get: () => mod[key],
14
+ enumerable: true
15
+ });
16
+ return to;
17
+ };
18
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
+
20
+ // src/middleware/anthropic-cache.ts
21
+ function ensureCacheMarker(message) {
22
+ if (!message)
23
+ return;
24
+ if (!("content" in message))
25
+ return;
26
+ const content = message.content;
27
+ if (!content || !Array.isArray(content))
28
+ return;
29
+ const lastPart = content.at(-1);
30
+ if (!lastPart || typeof lastPart === "string")
31
+ return;
32
+ lastPart.providerOptions = {
33
+ ...lastPart.providerOptions,
34
+ anthropic: {
35
+ cacheControl: { type: "ephemeral" }
36
+ }
37
+ };
38
+ }
39
+ var anthropicPromptCacheMiddleware = {
40
+ transformParams: async ({
41
+ params
42
+ }) => {
43
+ const messages = params.prompt;
44
+ if (!messages || messages.length === 0) {
45
+ return params;
46
+ }
47
+ ensureCacheMarker(messages.at(-1));
48
+ ensureCacheMarker(messages.slice(0, -1).findLast((m) => m.role !== "assistant"));
49
+ return {
50
+ ...params,
51
+ prompt: messages
52
+ };
53
+ }
54
+ };
55
+ // src/sandbox/e2b.ts
56
+ import { Sandbox as E2BSandboxSDK } from "@e2b/code-interpreter";
57
+ function createE2BSandbox(config = {}) {
58
+ let sandbox = null;
59
+ const workingDirectory = config.cwd || "/home/user";
60
+ const timeout = config.timeout ?? 300000;
61
+ const ensureSandbox = async () => {
62
+ if (sandbox)
63
+ return sandbox;
64
+ sandbox = await E2BSandboxSDK.create({
65
+ apiKey: config.apiKey,
66
+ timeoutMs: timeout,
67
+ metadata: config.metadata
68
+ });
69
+ return sandbox;
70
+ };
71
+ const exec = async (command, options) => {
72
+ const sbx = await ensureSandbox();
73
+ const startTime = performance.now();
74
+ try {
75
+ const result = await sbx.commands.run(command, {
76
+ cwd: options?.cwd || workingDirectory,
77
+ timeoutMs: options?.timeout
78
+ });
79
+ const durationMs = Math.round(performance.now() - startTime);
80
+ return {
81
+ stdout: result.stdout,
82
+ stderr: result.stderr,
83
+ exitCode: result.exitCode,
84
+ durationMs,
85
+ interrupted: false
86
+ };
87
+ } catch (error) {
88
+ const durationMs = Math.round(performance.now() - startTime);
89
+ if (error instanceof Error && error.message.toLowerCase().includes("timeout")) {
90
+ return {
91
+ stdout: "",
92
+ stderr: "Command timed out",
93
+ exitCode: 124,
94
+ durationMs,
95
+ interrupted: true
96
+ };
97
+ }
98
+ if (error instanceof Error) {
99
+ const exitMatch = error.message.match(/exit status (\d+)/i);
100
+ const exitCode = exitMatch ? parseInt(exitMatch[1], 10) : 1;
101
+ return {
102
+ stdout: "",
103
+ stderr: error.message,
104
+ exitCode,
105
+ durationMs,
106
+ interrupted: false
107
+ };
108
+ }
109
+ throw error;
110
+ }
111
+ };
112
+ return {
113
+ exec,
114
+ async readFile(path) {
115
+ const result = await exec(`cat "${path}"`);
116
+ if (result.exitCode !== 0) {
117
+ throw new Error(`Failed to read file: ${result.stderr}`);
118
+ }
119
+ return result.stdout;
120
+ },
121
+ async writeFile(path, content) {
122
+ const sbx = await ensureSandbox();
123
+ await sbx.files.write(path, content);
124
+ },
125
+ async readDir(path) {
126
+ const result = await exec(`ls -1 "${path}"`);
127
+ if (result.exitCode !== 0) {
128
+ throw new Error(`Failed to read directory: ${result.stderr}`);
129
+ }
130
+ return result.stdout.split(`
131
+ `).filter(Boolean);
132
+ },
133
+ async fileExists(path) {
134
+ const result = await exec(`test -e "${path}"`);
135
+ return result.exitCode === 0;
136
+ },
137
+ async isDirectory(path) {
138
+ const result = await exec(`test -d "${path}"`);
139
+ return result.exitCode === 0;
140
+ },
141
+ async destroy() {
142
+ if (sandbox) {
143
+ await sandbox.kill();
144
+ sandbox = null;
145
+ }
146
+ }
147
+ };
148
+ }
149
+ // src/sandbox/local.ts
150
+ import { existsSync, mkdirSync } from "node:fs";
151
+ function createLocalSandbox(config = {}) {
152
+ const workingDirectory = config.cwd || "/tmp";
153
+ if (!existsSync(workingDirectory)) {
154
+ mkdirSync(workingDirectory, { recursive: true });
155
+ }
156
+ const exec = async (command, options) => {
157
+ const startTime = performance.now();
158
+ let interrupted = false;
159
+ const cwd = options?.cwd || workingDirectory;
160
+ if (!existsSync(cwd)) {
161
+ mkdirSync(cwd, { recursive: true });
162
+ }
163
+ const proc = Bun.spawn(["sh", "-c", command], {
164
+ cwd,
165
+ stdout: "pipe",
166
+ stderr: "pipe"
167
+ });
168
+ let timeoutId;
169
+ if (options?.timeout) {
170
+ timeoutId = setTimeout(() => {
171
+ interrupted = true;
172
+ proc.kill();
173
+ }, options.timeout);
174
+ }
175
+ const stdout = await new Response(proc.stdout).text();
176
+ const stderr = await new Response(proc.stderr).text();
177
+ const exitCode = await proc.exited;
178
+ if (timeoutId) {
179
+ clearTimeout(timeoutId);
180
+ }
181
+ const durationMs = Math.round(performance.now() - startTime);
182
+ return {
183
+ stdout,
184
+ stderr,
185
+ exitCode,
186
+ durationMs,
187
+ interrupted
188
+ };
189
+ };
190
+ return {
191
+ exec,
192
+ async readFile(path) {
193
+ const fullPath = path.startsWith("/") ? path : `${workingDirectory}/${path}`;
194
+ const file = Bun.file(fullPath);
195
+ return await file.text();
196
+ },
197
+ async writeFile(path, content) {
198
+ const fullPath = path.startsWith("/") ? path : `${workingDirectory}/${path}`;
199
+ await Bun.write(fullPath, content);
200
+ },
201
+ async readDir(path) {
202
+ const fullPath = path.startsWith("/") ? path : `${workingDirectory}/${path}`;
203
+ const fs = await import("fs/promises");
204
+ return await fs.readdir(fullPath);
205
+ },
206
+ async fileExists(path) {
207
+ const fullPath = path.startsWith("/") ? path : `${workingDirectory}/${path}`;
208
+ const fs = await import("fs/promises");
209
+ try {
210
+ await fs.stat(fullPath);
211
+ return true;
212
+ } catch {
213
+ return false;
214
+ }
215
+ },
216
+ async isDirectory(path) {
217
+ const fullPath = path.startsWith("/") ? path : `${workingDirectory}/${path}`;
218
+ const fs = await import("fs/promises");
219
+ try {
220
+ const stat = await fs.stat(fullPath);
221
+ return stat.isDirectory();
222
+ } catch {
223
+ return false;
224
+ }
225
+ },
226
+ async destroy() {}
227
+ };
228
+ }
229
+ // src/sandbox/vercel.ts
230
+ import { Sandbox as VercelSandboxSDK } from "@vercel/sandbox";
231
+ function createVercelSandbox(config = {}) {
232
+ let sandbox = null;
233
+ const workingDirectory = config.cwd || "/vercel/sandbox";
234
+ const resolvedConfig = {
235
+ runtime: config.runtime ?? "node22",
236
+ resources: config.resources ?? { vcpus: 2 },
237
+ timeout: config.timeout ?? 300000
238
+ };
239
+ const ensureSandbox = async () => {
240
+ if (sandbox)
241
+ return sandbox;
242
+ const createOptions = {
243
+ runtime: resolvedConfig.runtime,
244
+ resources: resolvedConfig.resources,
245
+ timeout: resolvedConfig.timeout
246
+ };
247
+ if (config.teamId && config.token) {
248
+ Object.assign(createOptions, {
249
+ teamId: config.teamId,
250
+ token: config.token
251
+ });
252
+ }
253
+ sandbox = await VercelSandboxSDK.create(createOptions);
254
+ return sandbox;
255
+ };
256
+ const exec = async (command, options) => {
257
+ const sbx = await ensureSandbox();
258
+ const startTime = performance.now();
259
+ let interrupted = false;
260
+ const abortController = new AbortController;
261
+ let timeoutId;
262
+ if (options?.timeout) {
263
+ timeoutId = setTimeout(() => {
264
+ interrupted = true;
265
+ abortController.abort();
266
+ }, options.timeout);
267
+ }
268
+ try {
269
+ const result = await sbx.runCommand({
270
+ cmd: "bash",
271
+ args: ["-c", command],
272
+ cwd: options?.cwd || workingDirectory,
273
+ signal: abortController.signal
274
+ });
275
+ if (timeoutId) {
276
+ clearTimeout(timeoutId);
277
+ }
278
+ const stdout = await result.stdout();
279
+ const stderr = await result.stderr();
280
+ const durationMs = Math.round(performance.now() - startTime);
281
+ return {
282
+ stdout,
283
+ stderr,
284
+ exitCode: result.exitCode,
285
+ durationMs,
286
+ interrupted
287
+ };
288
+ } catch (error) {
289
+ if (timeoutId) {
290
+ clearTimeout(timeoutId);
291
+ }
292
+ const durationMs = Math.round(performance.now() - startTime);
293
+ if (interrupted) {
294
+ return {
295
+ stdout: "",
296
+ stderr: "Command timed out",
297
+ exitCode: 124,
298
+ durationMs,
299
+ interrupted: true
300
+ };
301
+ }
302
+ throw error;
303
+ }
304
+ };
305
+ return {
306
+ exec,
307
+ async readFile(path) {
308
+ const sbx = await ensureSandbox();
309
+ const stream = await sbx.readFile({ path });
310
+ if (!stream) {
311
+ throw new Error(`File not found: ${path}`);
312
+ }
313
+ const chunks = [];
314
+ for await (const chunk of stream) {
315
+ chunks.push(Buffer.from(chunk));
316
+ }
317
+ return Buffer.concat(chunks).toString("utf-8");
318
+ },
319
+ async writeFile(path, content) {
320
+ const sbx = await ensureSandbox();
321
+ await sbx.writeFiles([
322
+ {
323
+ path,
324
+ content: Buffer.from(content, "utf-8")
325
+ }
326
+ ]);
327
+ },
328
+ async readDir(path) {
329
+ const result = await exec(`ls -1 ${path}`);
330
+ if (result.exitCode !== 0) {
331
+ throw new Error(`Failed to read directory: ${result.stderr}`);
332
+ }
333
+ return result.stdout.split(`
334
+ `).filter(Boolean);
335
+ },
336
+ async fileExists(path) {
337
+ const result = await exec(`test -e ${path}`);
338
+ return result.exitCode === 0;
339
+ },
340
+ async isDirectory(path) {
341
+ const result = await exec(`test -d ${path}`);
342
+ return result.exitCode === 0;
343
+ },
344
+ async destroy() {
345
+ if (sandbox) {
346
+ await sandbox.stop();
347
+ sandbox = null;
348
+ }
349
+ }
350
+ };
351
+ }
352
+ // src/types.ts
353
+ var DEFAULT_CONFIG = {
354
+ defaultTimeout: 120000,
355
+ workingDirectory: "/tmp",
356
+ tools: {
357
+ Bash: { maxOutputLength: 30000 }
358
+ }
359
+ };
360
+
361
+ // src/tools/bash.ts
362
+ import { tool, zodSchema } from "ai";
363
+ import { z } from "zod";
364
+ var bashInputSchema = z.object({
365
+ command: z.string().describe("The command to execute"),
366
+ timeout: z.number().optional().describe("Optional timeout in milliseconds (max 600000)"),
367
+ description: z.string().optional().describe("Clear, concise description of what this command does in 5-10 words"),
368
+ run_in_background: z.boolean().optional().describe("Set to true to run this command in the background")
369
+ });
370
+ var BASH_DESCRIPTION = `Executes bash commands in a persistent shell session with optional timeout and background execution.
371
+
372
+ **Important guidelines:**
373
+ - Always quote file paths containing spaces with double quotes (e.g., cd "/path/with spaces")
374
+ - Avoid using search commands like \`find\` and \`grep\` - use the Glob and Grep tools instead
375
+ - Avoid using \`cat\`, \`head\`, \`tail\` - use the Read tool instead
376
+ - When issuing multiple commands, use \`;\` or \`&&\` to separate them (not newlines)
377
+ - If output exceeds 30000 characters, it will be truncated
378
+ - Default timeout is 2 minutes; maximum is 10 minutes`;
379
+ function createBashTool(sandbox, config) {
380
+ const maxOutputLength = config?.maxOutputLength ?? 30000;
381
+ const defaultTimeout = config?.timeout ?? 120000;
382
+ return tool({
383
+ description: BASH_DESCRIPTION,
384
+ inputSchema: zodSchema(bashInputSchema),
385
+ execute: async ({
386
+ command,
387
+ timeout,
388
+ description: _description,
389
+ run_in_background: _run_in_background
390
+ }) => {
391
+ if (config?.blockedCommands) {
392
+ for (const blocked of config.blockedCommands) {
393
+ if (command.includes(blocked)) {
394
+ return {
395
+ error: `Command blocked: contains '${blocked}'`
396
+ };
397
+ }
398
+ }
399
+ }
400
+ try {
401
+ const effectiveTimeout = Math.min(timeout ?? defaultTimeout, 600000);
402
+ const result = await sandbox.exec(command, {
403
+ timeout: effectiveTimeout
404
+ });
405
+ let stdout = result.stdout;
406
+ let stderr = result.stderr;
407
+ if (stdout.length > maxOutputLength) {
408
+ stdout = stdout.slice(0, maxOutputLength) + `
409
+ [output truncated, ${stdout.length - maxOutputLength} chars omitted]`;
410
+ }
411
+ if (stderr.length > maxOutputLength) {
412
+ stderr = stderr.slice(0, maxOutputLength) + `
413
+ [output truncated, ${stderr.length - maxOutputLength} chars omitted]`;
414
+ }
415
+ return {
416
+ stdout,
417
+ stderr,
418
+ exit_code: result.exitCode,
419
+ interrupted: result.interrupted,
420
+ duration_ms: result.durationMs
421
+ };
422
+ } catch (error) {
423
+ return {
424
+ error: error instanceof Error ? error.message : "Unknown error"
425
+ };
426
+ }
427
+ }
428
+ });
429
+ }
430
+
431
+ // src/tools/edit.ts
432
+ import { tool as tool2, zodSchema as zodSchema2 } from "ai";
433
+ import { z as z2 } from "zod";
434
+ var editInputSchema = z2.object({
435
+ file_path: z2.string().describe("The absolute path to the file to modify"),
436
+ old_string: z2.string().describe("The text to replace"),
437
+ new_string: z2.string().describe("The text to replace it with (must be different from old_string)"),
438
+ replace_all: z2.boolean().optional().describe("Replace all occurrences of old_string (default false)")
439
+ });
440
+ function createEditTool(sandbox, config) {
441
+ return tool2({
442
+ description: "Performs exact string replacements in files.",
443
+ inputSchema: zodSchema2(editInputSchema),
444
+ execute: async ({
445
+ file_path,
446
+ old_string,
447
+ new_string,
448
+ replace_all = false
449
+ }) => {
450
+ if (old_string === new_string) {
451
+ return { error: "old_string and new_string must be different" };
452
+ }
453
+ if (config?.allowedPaths) {
454
+ const isAllowed = config.allowedPaths.some((allowed) => file_path.startsWith(allowed));
455
+ if (!isAllowed) {
456
+ return { error: `Path not allowed: ${file_path}` };
457
+ }
458
+ }
459
+ try {
460
+ const exists = await sandbox.fileExists(file_path);
461
+ if (!exists) {
462
+ return { error: `File not found: ${file_path}` };
463
+ }
464
+ const content = await sandbox.readFile(file_path);
465
+ const occurrences = content.split(old_string).length - 1;
466
+ if (occurrences === 0) {
467
+ return { error: `String not found in file: "${old_string}"` };
468
+ }
469
+ if (!replace_all && occurrences > 1) {
470
+ return {
471
+ error: `String appears ${occurrences} times in file. Use replace_all=true to replace all, or provide a more unique string.`
472
+ };
473
+ }
474
+ let newContent;
475
+ let replacements;
476
+ if (replace_all) {
477
+ newContent = content.split(old_string).join(new_string);
478
+ replacements = occurrences;
479
+ } else {
480
+ newContent = content.replace(old_string, new_string);
481
+ replacements = 1;
482
+ }
483
+ await sandbox.writeFile(file_path, newContent);
484
+ return {
485
+ message: `Successfully edited ${file_path}`,
486
+ file_path,
487
+ replacements
488
+ };
489
+ } catch (error) {
490
+ return {
491
+ error: error instanceof Error ? error.message : "Unknown error"
492
+ };
493
+ }
494
+ }
495
+ });
496
+ }
497
+
498
+ // src/tools/glob.ts
499
+ import { tool as tool3, zodSchema as zodSchema3 } from "ai";
500
+ import { z as z3 } from "zod";
501
+ var globInputSchema = z3.object({
502
+ pattern: z3.string().describe('Glob pattern to match files (e.g., "**/*.ts", "src/**/*.js", "*.md")'),
503
+ path: z3.string().optional().describe("Directory to search in (defaults to working directory)")
504
+ });
505
+ function createGlobTool(sandbox, config) {
506
+ return tool3({
507
+ description: "Search for files matching a glob pattern. Returns file paths sorted by modification time. Use this instead of `find` command.",
508
+ inputSchema: zodSchema3(globInputSchema),
509
+ execute: async ({
510
+ pattern,
511
+ path
512
+ }) => {
513
+ const searchPath = path || ".";
514
+ if (config?.allowedPaths) {
515
+ const isAllowed = config.allowedPaths.some((allowed) => searchPath.startsWith(allowed));
516
+ if (!isAllowed) {
517
+ return { error: `Path not allowed: ${searchPath}` };
518
+ }
519
+ }
520
+ try {
521
+ const result = await sandbox.exec(`find ${searchPath} -type f -name "${pattern}" 2>/dev/null | head -1000`, { timeout: config?.timeout });
522
+ if (result.exitCode !== 0 && result.stderr) {
523
+ return { error: result.stderr };
524
+ }
525
+ const matches = result.stdout.split(`
526
+ `).filter(Boolean).map((p) => p.trim());
527
+ return {
528
+ matches,
529
+ count: matches.length,
530
+ search_path: searchPath
531
+ };
532
+ } catch (error) {
533
+ return {
534
+ error: error instanceof Error ? error.message : "Unknown error"
535
+ };
536
+ }
537
+ }
538
+ });
539
+ }
540
+
541
+ // src/tools/grep.ts
542
+ import { tool as tool4, zodSchema as zodSchema4 } from "ai";
543
+ import { z as z4 } from "zod";
544
+ var grepInputSchema = z4.object({
545
+ pattern: z4.string().describe("The regular expression pattern to search for"),
546
+ path: z4.string().optional().describe("File or directory to search in (defaults to cwd)"),
547
+ glob: z4.string().optional().describe('Glob pattern to filter files (e.g. "*.js")'),
548
+ type: z4.string().optional().describe('File type to search (e.g. "js", "py", "rust")'),
549
+ output_mode: z4.enum(["content", "files_with_matches", "count"]).optional().describe('Output mode: "content", "files_with_matches", or "count"'),
550
+ "-i": z4.boolean().optional().describe("Case insensitive search"),
551
+ "-n": z4.boolean().optional().describe("Show line numbers (for content mode)"),
552
+ "-B": z4.number().optional().describe("Lines to show before each match"),
553
+ "-A": z4.number().optional().describe("Lines to show after each match"),
554
+ "-C": z4.number().optional().describe("Lines to show before and after each match"),
555
+ head_limit: z4.number().optional().describe("Limit output to first N lines/entries"),
556
+ multiline: z4.boolean().optional().describe("Enable multiline mode")
557
+ });
558
+ function createGrepTool(sandbox, config) {
559
+ return tool4({
560
+ description: "Powerful search tool built on ripgrep with regex support. Use this instead of the grep command.",
561
+ inputSchema: zodSchema4(grepInputSchema),
562
+ execute: async (input) => {
563
+ const {
564
+ pattern,
565
+ path,
566
+ glob,
567
+ type,
568
+ output_mode = "content",
569
+ "-i": caseInsensitive,
570
+ "-n": showLineNumbers = true,
571
+ "-B": beforeContext,
572
+ "-A": afterContext,
573
+ "-C": context,
574
+ head_limit,
575
+ multiline
576
+ } = input;
577
+ const searchPath = path || ".";
578
+ if (config?.allowedPaths) {
579
+ const isAllowed = config.allowedPaths.some((allowed) => searchPath.startsWith(allowed));
580
+ if (!isAllowed) {
581
+ return { error: `Path not allowed: ${searchPath}` };
582
+ }
583
+ }
584
+ try {
585
+ const flags = [];
586
+ if (caseInsensitive)
587
+ flags.push("-i");
588
+ if (showLineNumbers && output_mode === "content")
589
+ flags.push("-n");
590
+ if (multiline)
591
+ flags.push("-U");
592
+ if (context) {
593
+ flags.push(`-C ${context}`);
594
+ } else {
595
+ if (beforeContext)
596
+ flags.push(`-B ${beforeContext}`);
597
+ if (afterContext)
598
+ flags.push(`-A ${afterContext}`);
599
+ }
600
+ if (glob)
601
+ flags.push(`--include="${glob}"`);
602
+ if (type)
603
+ flags.push(`--include="*.${type}"`);
604
+ const flagStr = flags.join(" ");
605
+ const limit = head_limit || 1000;
606
+ let cmd;
607
+ if (output_mode === "files_with_matches") {
608
+ cmd = `grep -rl ${flagStr} "${pattern}" ${searchPath} 2>/dev/null | head -${limit}`;
609
+ const result = await sandbox.exec(cmd, { timeout: config?.timeout });
610
+ const files = result.stdout.split(`
611
+ `).filter(Boolean);
612
+ return {
613
+ files,
614
+ count: files.length
615
+ };
616
+ } else if (output_mode === "count") {
617
+ cmd = `grep -rc ${flagStr} "${pattern}" ${searchPath} 2>/dev/null | grep -v ':0$' | head -${limit}`;
618
+ const result = await sandbox.exec(cmd, { timeout: config?.timeout });
619
+ const lines = result.stdout.split(`
620
+ `).filter(Boolean);
621
+ const counts = lines.map((line) => {
622
+ const lastColon = line.lastIndexOf(":");
623
+ return {
624
+ file: line.slice(0, lastColon),
625
+ count: parseInt(line.slice(lastColon + 1), 10)
626
+ };
627
+ });
628
+ const total = counts.reduce((sum, c) => sum + c.count, 0);
629
+ return {
630
+ counts,
631
+ total
632
+ };
633
+ } else {
634
+ cmd = `grep -rn ${flagStr} "${pattern}" ${searchPath} 2>/dev/null | head -${limit}`;
635
+ const result = await sandbox.exec(cmd, { timeout: config?.timeout });
636
+ if (!result.stdout.trim()) {
637
+ return {
638
+ matches: [],
639
+ total_matches: 0
640
+ };
641
+ }
642
+ const lines = result.stdout.split(`
643
+ `).filter(Boolean);
644
+ const matches = [];
645
+ for (const line of lines) {
646
+ const colonMatch = line.match(/^(.+?):(\d+)[:|-](.*)$/);
647
+ if (colonMatch) {
648
+ const [, file, lineNum, content] = colonMatch;
649
+ matches.push({
650
+ file,
651
+ line_number: parseInt(lineNum, 10),
652
+ line: content
653
+ });
654
+ }
655
+ }
656
+ return {
657
+ matches,
658
+ total_matches: matches.length
659
+ };
660
+ }
661
+ } catch (error) {
662
+ return {
663
+ error: error instanceof Error ? error.message : "Unknown error"
664
+ };
665
+ }
666
+ }
667
+ });
668
+ }
669
+
670
+ // src/tools/read.ts
671
+ import { tool as tool5, zodSchema as zodSchema5 } from "ai";
672
+ import { z as z5 } from "zod";
673
+ var readInputSchema = z5.object({
674
+ file_path: z5.string().describe("Absolute path to file or directory"),
675
+ offset: z5.number().optional().describe("Line number to start reading from (1-indexed)"),
676
+ limit: z5.number().optional().describe("Maximum number of lines to read")
677
+ });
678
+ function createReadTool(sandbox, config) {
679
+ return tool5({
680
+ description: "Read the contents of a file or list directory entries. For text files, returns numbered lines with total line count. For directories, returns file/folder names. Use this instead of `cat`, `head`, or `tail` commands.",
681
+ inputSchema: zodSchema5(readInputSchema),
682
+ execute: async ({
683
+ file_path,
684
+ offset,
685
+ limit
686
+ }) => {
687
+ if (config?.allowedPaths) {
688
+ const isAllowed = config.allowedPaths.some((allowed) => file_path.startsWith(allowed));
689
+ if (!isAllowed) {
690
+ return { error: `Path not allowed: ${file_path}` };
691
+ }
692
+ }
693
+ try {
694
+ const exists = await sandbox.fileExists(file_path);
695
+ if (!exists) {
696
+ return { error: `Path not found: ${file_path}` };
697
+ }
698
+ const isDir = await sandbox.isDirectory(file_path);
699
+ if (isDir) {
700
+ const entries = await sandbox.readDir(file_path);
701
+ return {
702
+ type: "directory",
703
+ entries,
704
+ count: entries.length
705
+ };
706
+ }
707
+ const content = await sandbox.readFile(file_path);
708
+ const nullByteIndex = content.indexOf("\x00");
709
+ if (nullByteIndex !== -1 && nullByteIndex < 1000) {
710
+ const ext = file_path.split(".").pop()?.toLowerCase();
711
+ const binaryExtensions = [
712
+ "pdf",
713
+ "png",
714
+ "jpg",
715
+ "jpeg",
716
+ "gif",
717
+ "zip",
718
+ "tar",
719
+ "gz",
720
+ "exe",
721
+ "bin",
722
+ "so",
723
+ "dylib"
724
+ ];
725
+ if (binaryExtensions.includes(ext || "")) {
726
+ return {
727
+ error: `Cannot read binary file: ${file_path} (file exists, ${content.length} bytes). Use appropriate tools to process ${ext?.toUpperCase()} files (e.g., Python scripts for PDFs).`
728
+ };
729
+ }
730
+ }
731
+ const allLines = content.split(`
732
+ `);
733
+ const totalLines = allLines.length;
734
+ const maxLinesWithoutLimit = config?.maxFileSize || 500;
735
+ if (!limit && totalLines > maxLinesWithoutLimit) {
736
+ return {
737
+ error: `File is large (${totalLines} lines). Use 'offset' and 'limit' to read in chunks. Example: offset=1, limit=100 for first 100 lines.`
738
+ };
739
+ }
740
+ const startLine = offset ? offset - 1 : 0;
741
+ const endLine = limit ? startLine + limit : allLines.length;
742
+ const selectedLines = allLines.slice(startLine, endLine);
743
+ const lines = selectedLines.map((line, i) => ({
744
+ line_number: startLine + i + 1,
745
+ content: line
746
+ }));
747
+ return {
748
+ type: "text",
749
+ content: selectedLines.join(`
750
+ `),
751
+ lines,
752
+ total_lines: totalLines
753
+ };
754
+ } catch (error) {
755
+ return {
756
+ error: error instanceof Error ? error.message : "Unknown error"
757
+ };
758
+ }
759
+ }
760
+ });
761
+ }
762
+
763
+ // src/tools/web-fetch.ts
764
+ import { generateText, tool as tool6, zodSchema as zodSchema6 } from "ai";
765
+ import Parallel from "parallel-web";
766
+ import { z as z6 } from "zod";
767
+ var webFetchInputSchema = z6.object({
768
+ url: z6.string().describe("The URL to fetch content from"),
769
+ prompt: z6.string().describe("The prompt to run on the fetched content")
770
+ });
771
+ var RETRYABLE_CODES = [408, 429, 500, 502, 503];
772
+ function createWebFetchTool(config) {
773
+ const { apiKey, model } = config;
774
+ return tool6({
775
+ description: "Fetches content from a URL and processes it with an AI model. Use this to analyze web pages, extract information, or summarize content.",
776
+ inputSchema: zodSchema6(webFetchInputSchema),
777
+ execute: async (input) => {
778
+ const { url, prompt } = input;
779
+ try {
780
+ const client = new Parallel({ apiKey });
781
+ const extract = await client.beta.extract({
782
+ urls: [url],
783
+ excerpts: true,
784
+ full_content: true
785
+ });
786
+ if (!extract.results || extract.results.length === 0) {
787
+ return {
788
+ error: "No content extracted from URL",
789
+ status_code: 404,
790
+ retryable: false
791
+ };
792
+ }
793
+ const extractedResult = extract.results[0];
794
+ const content = extractedResult.full_content || extractedResult.excerpts?.join(`
795
+
796
+ `) || "";
797
+ if (!content) {
798
+ return {
799
+ error: "No content available from URL",
800
+ status_code: 404,
801
+ retryable: false
802
+ };
803
+ }
804
+ const result = await generateText({
805
+ model,
806
+ prompt: `${prompt}
807
+
808
+ Content from ${url}:
809
+
810
+ ${content}`
811
+ });
812
+ return {
813
+ response: result.text,
814
+ url,
815
+ final_url: extractedResult.url || url
816
+ };
817
+ } catch (error) {
818
+ if (error && typeof error === "object" && "status" in error) {
819
+ const statusCode = error.status;
820
+ const message = error.message || "API request failed";
821
+ return {
822
+ error: message,
823
+ status_code: statusCode,
824
+ retryable: RETRYABLE_CODES.includes(statusCode)
825
+ };
826
+ }
827
+ return {
828
+ error: error instanceof Error ? error.message : "Unknown error"
829
+ };
830
+ }
831
+ }
832
+ });
833
+ }
834
+
835
+ // src/tools/web-search.ts
836
+ import { tool as tool7, zodSchema as zodSchema7 } from "ai";
837
+ import Parallel2 from "parallel-web";
838
+ import { z as z7 } from "zod";
839
+ var webSearchInputSchema = z7.object({
840
+ query: z7.string().describe("The search query to use"),
841
+ allowed_domains: z7.array(z7.string()).optional().describe("Only include results from these domains"),
842
+ blocked_domains: z7.array(z7.string()).optional().describe("Never include results from these domains")
843
+ });
844
+ var RETRYABLE_CODES2 = [408, 429, 500, 502, 503];
845
+ function createWebSearchTool(config) {
846
+ const { apiKey } = config;
847
+ return tool7({
848
+ description: "Searches the web and returns formatted results. Use this to find current information, documentation, articles, and more.",
849
+ inputSchema: zodSchema7(webSearchInputSchema),
850
+ execute: async (input) => {
851
+ const { query, allowed_domains, blocked_domains } = input;
852
+ try {
853
+ const client = new Parallel2({ apiKey });
854
+ const sourcePolicy = allowed_domains || blocked_domains ? {
855
+ ...allowed_domains && { include_domains: allowed_domains },
856
+ ...blocked_domains && { exclude_domains: blocked_domains }
857
+ } : undefined;
858
+ const search = await client.beta.search({
859
+ mode: "agentic",
860
+ objective: query,
861
+ max_results: 10,
862
+ ...sourcePolicy && { source_policy: sourcePolicy }
863
+ });
864
+ const results = (search.results || []).map((result) => ({
865
+ title: result.title ?? "",
866
+ url: result.url ?? "",
867
+ snippet: result.excerpts?.join(`
868
+ `) ?? "",
869
+ metadata: result.publish_date ? { publish_date: result.publish_date } : undefined
870
+ }));
871
+ return {
872
+ results,
873
+ total_results: results.length,
874
+ query
875
+ };
876
+ } catch (error) {
877
+ if (error && typeof error === "object" && "status" in error) {
878
+ const statusCode = error.status;
879
+ const message = error.message || "API request failed";
880
+ return {
881
+ error: message,
882
+ status_code: statusCode,
883
+ retryable: RETRYABLE_CODES2.includes(statusCode)
884
+ };
885
+ }
886
+ return {
887
+ error: error instanceof Error ? error.message : "Unknown error"
888
+ };
889
+ }
890
+ }
891
+ });
892
+ }
893
+
894
+ // src/tools/write.ts
895
+ import { tool as tool8, zodSchema as zodSchema8 } from "ai";
896
+ import { z as z8 } from "zod";
897
+ var writeInputSchema = z8.object({
898
+ file_path: z8.string().describe("Path to the file to write"),
899
+ content: z8.string().describe("Content to write to the file")
900
+ });
901
+ function createWriteTool(sandbox, config) {
902
+ return tool8({
903
+ description: "Write content to a file. Creates the file if it does not exist, overwrites if it does.",
904
+ inputSchema: zodSchema8(writeInputSchema),
905
+ execute: async ({
906
+ file_path,
907
+ content
908
+ }) => {
909
+ const byteLength = Buffer.byteLength(content, "utf-8");
910
+ if (config?.maxFileSize && byteLength > config.maxFileSize) {
911
+ return {
912
+ error: `File content exceeds maximum size of ${config.maxFileSize} bytes (got ${byteLength})`
913
+ };
914
+ }
915
+ if (config?.allowedPaths) {
916
+ const isAllowed = config.allowedPaths.some((allowed) => file_path.startsWith(allowed));
917
+ if (!isAllowed) {
918
+ return { error: `Path not allowed: ${file_path}` };
919
+ }
920
+ }
921
+ try {
922
+ await sandbox.writeFile(file_path, content);
923
+ return {
924
+ message: `Successfully wrote to ${file_path}`,
925
+ bytes_written: byteLength,
926
+ file_path
927
+ };
928
+ } catch (error) {
929
+ return {
930
+ error: error instanceof Error ? error.message : "Unknown error"
931
+ };
932
+ }
933
+ }
934
+ });
935
+ }
936
+ // src/tools/exit-plan-mode.ts
937
+ import { tool as tool9, zodSchema as zodSchema9 } from "ai";
938
+ import { z as z9 } from "zod";
939
+ var exitPlanModeInputSchema = z9.object({
940
+ plan: z9.string().describe("The plan to present to the user for approval")
941
+ });
942
+ function createExitPlanModeTool(config, onPlanSubmit) {
943
+ return tool9({
944
+ description: "Exits planning mode and prompts the user to approve the plan. Use this when you have finished planning and want user confirmation before proceeding.",
945
+ inputSchema: zodSchema9(exitPlanModeInputSchema),
946
+ execute: async ({
947
+ plan
948
+ }) => {
949
+ try {
950
+ let approved;
951
+ if (onPlanSubmit) {
952
+ approved = await onPlanSubmit(plan);
953
+ }
954
+ return {
955
+ message: approved ? "Plan approved, proceeding with execution" : "Plan submitted for review",
956
+ approved
957
+ };
958
+ } catch (error) {
959
+ return {
960
+ error: error instanceof Error ? error.message : "Unknown error"
961
+ };
962
+ }
963
+ }
964
+ });
965
+ }
966
+ // src/tools/task.ts
967
+ import {
968
+ generateText as generateText2,
969
+ stepCountIs,
970
+ tool as tool10,
971
+ zodSchema as zodSchema10
972
+ } from "ai";
973
+ import { z as z10 } from "zod";
974
+ var taskInputSchema = z10.object({
975
+ description: z10.string().describe("A short (3-5 word) description of the task"),
976
+ prompt: z10.string().describe("The task for the agent to perform"),
977
+ subagent_type: z10.string().describe("The type of specialized agent to use for this task")
978
+ });
979
+ function filterTools(allTools, allowedTools) {
980
+ if (!allowedTools)
981
+ return allTools;
982
+ const filtered = {};
983
+ for (const name of allowedTools) {
984
+ if (allTools[name]) {
985
+ filtered[name] = allTools[name];
986
+ }
987
+ }
988
+ return filtered;
989
+ }
990
+ function createTaskTool(config) {
991
+ const {
992
+ model: defaultModel,
993
+ tools: allTools,
994
+ subagentTypes = {},
995
+ costPerInputToken = 0.000003,
996
+ costPerOutputToken = 0.000015,
997
+ defaultStopWhen,
998
+ defaultOnStepFinish
999
+ } = config;
1000
+ return tool10({
1001
+ description: "Launches a new agent to handle complex, multi-step tasks autonomously. Use this for tasks that require multiple steps, research, or specialized expertise.",
1002
+ inputSchema: zodSchema10(taskInputSchema),
1003
+ execute: async ({
1004
+ description,
1005
+ prompt,
1006
+ subagent_type
1007
+ }) => {
1008
+ const startTime = performance.now();
1009
+ try {
1010
+ const typeConfig = subagentTypes[subagent_type] || {};
1011
+ const model = typeConfig.model || defaultModel;
1012
+ const tools = filterTools(allTools, typeConfig.tools);
1013
+ const systemPrompt = typeConfig.systemPrompt;
1014
+ const result = await generateText2({
1015
+ model,
1016
+ tools,
1017
+ system: systemPrompt,
1018
+ prompt,
1019
+ stopWhen: typeConfig.stopWhen ?? defaultStopWhen ?? stepCountIs(15),
1020
+ prepareStep: typeConfig.prepareStep,
1021
+ onStepFinish: async (step) => {
1022
+ await typeConfig.onStepFinish?.(step);
1023
+ await defaultOnStepFinish?.({
1024
+ subagentType: subagent_type,
1025
+ description,
1026
+ step
1027
+ });
1028
+ }
1029
+ });
1030
+ const durationMs = Math.round(performance.now() - startTime);
1031
+ const usage = result.usage.inputTokens !== undefined && result.usage.outputTokens !== undefined ? {
1032
+ input_tokens: result.usage.inputTokens,
1033
+ output_tokens: result.usage.outputTokens
1034
+ } : undefined;
1035
+ let totalCostUsd;
1036
+ if (usage) {
1037
+ totalCostUsd = usage.input_tokens * costPerInputToken + usage.output_tokens * costPerOutputToken;
1038
+ }
1039
+ return {
1040
+ result: result.text,
1041
+ usage,
1042
+ total_cost_usd: totalCostUsd,
1043
+ duration_ms: durationMs
1044
+ };
1045
+ } catch (error) {
1046
+ return {
1047
+ error: error instanceof Error ? error.message : "Unknown error"
1048
+ };
1049
+ }
1050
+ }
1051
+ });
1052
+ }
1053
+ // src/tools/todo-write.ts
1054
+ import { tool as tool11, zodSchema as zodSchema11 } from "ai";
1055
+ import { z as z11 } from "zod";
1056
+ var todoWriteInputSchema = z11.object({
1057
+ todos: z11.array(z11.object({
1058
+ content: z11.string().describe("The task description"),
1059
+ status: z11.enum(["pending", "in_progress", "completed"]).describe("The task status"),
1060
+ activeForm: z11.string().describe("Active form of the task description")
1061
+ })).describe("The updated todo list")
1062
+ });
1063
+ function createTodoWriteTool(state, config, onUpdate) {
1064
+ return tool11({
1065
+ description: "Creates and manages a structured task list for tracking progress. Use this to plan complex tasks and track completion.",
1066
+ inputSchema: zodSchema11(todoWriteInputSchema),
1067
+ execute: async ({
1068
+ todos
1069
+ }) => {
1070
+ try {
1071
+ state.todos = todos;
1072
+ if (onUpdate) {
1073
+ onUpdate(todos);
1074
+ }
1075
+ const stats = {
1076
+ total: todos.length,
1077
+ pending: todos.filter((t) => t.status === "pending").length,
1078
+ in_progress: todos.filter((t) => t.status === "in_progress").length,
1079
+ completed: todos.filter((t) => t.status === "completed").length
1080
+ };
1081
+ return {
1082
+ message: "Todo list updated successfully",
1083
+ stats
1084
+ };
1085
+ } catch (error) {
1086
+ return {
1087
+ error: error instanceof Error ? error.message : "Unknown error"
1088
+ };
1089
+ }
1090
+ }
1091
+ });
1092
+ }
1093
+
1094
+ // src/tools/index.ts
1095
+ function createAgentTools(sandbox, config) {
1096
+ const toolsConfig = {
1097
+ ...DEFAULT_CONFIG.tools,
1098
+ ...config?.tools
1099
+ };
1100
+ const tools = {
1101
+ Bash: createBashTool(sandbox, toolsConfig.Bash),
1102
+ Read: createReadTool(sandbox, toolsConfig.Read),
1103
+ Write: createWriteTool(sandbox, toolsConfig.Write),
1104
+ Edit: createEditTool(sandbox, toolsConfig.Edit),
1105
+ Glob: createGlobTool(sandbox, toolsConfig.Glob),
1106
+ Grep: createGrepTool(sandbox, toolsConfig.Grep)
1107
+ };
1108
+ if (config?.webSearch) {
1109
+ tools.WebSearch = createWebSearchTool(config.webSearch);
1110
+ }
1111
+ if (config?.webFetch) {
1112
+ tools.WebFetch = createWebFetchTool(config.webFetch);
1113
+ }
1114
+ return tools;
1115
+ }
1116
+ // src/utils/compact-conversation.ts
1117
+ import { generateText as generateText3 } from "ai";
1118
+
1119
+ // src/utils/prune-messages.ts
1120
+ var DEFAULT_CONFIG2 = {
1121
+ targetTokens: 40000,
1122
+ minSavingsThreshold: 20000,
1123
+ protectLastNUserMessages: 3
1124
+ };
1125
+ function estimateTokens(text) {
1126
+ return Math.ceil(text.length / 4);
1127
+ }
1128
+ function estimateMessageTokens(message) {
1129
+ let tokens = 0;
1130
+ if (typeof message.content === "string") {
1131
+ tokens += estimateTokens(message.content);
1132
+ } else if (Array.isArray(message.content)) {
1133
+ for (const part of message.content) {
1134
+ if (typeof part === "string") {
1135
+ tokens += estimateTokens(part);
1136
+ } else if ("text" in part && typeof part.text === "string") {
1137
+ tokens += estimateTokens(part.text);
1138
+ } else if ("result" in part) {
1139
+ tokens += estimateTokens(JSON.stringify(part.result));
1140
+ } else if ("args" in part) {
1141
+ tokens += estimateTokens(JSON.stringify(part.args));
1142
+ } else {
1143
+ tokens += estimateTokens(JSON.stringify(part));
1144
+ }
1145
+ }
1146
+ }
1147
+ tokens += 4;
1148
+ return tokens;
1149
+ }
1150
+ function estimateMessagesTokens(messages) {
1151
+ return messages.reduce((sum, msg) => sum + estimateMessageTokens(msg), 0);
1152
+ }
1153
+ function findLastNUserMessageIndices(messages, n) {
1154
+ const indices = new Set;
1155
+ let count = 0;
1156
+ for (let i = messages.length - 1;i >= 0 && count < n; i--) {
1157
+ if (messages[i].role === "user") {
1158
+ indices.add(i);
1159
+ count++;
1160
+ }
1161
+ }
1162
+ return indices;
1163
+ }
1164
+ function findProtectedIndices(messages, protectLastN) {
1165
+ const userIndices = findLastNUserMessageIndices(messages, protectLastN);
1166
+ const protected_ = new Set;
1167
+ if (userIndices.size === 0)
1168
+ return protected_;
1169
+ const earliestProtected = Math.min(...userIndices);
1170
+ for (let i = earliestProtected;i < messages.length; i++) {
1171
+ protected_.add(i);
1172
+ }
1173
+ return protected_;
1174
+ }
1175
+ function pruneMessageContent(message) {
1176
+ if (message.role !== "assistant" || typeof message.content === "string") {
1177
+ return message;
1178
+ }
1179
+ if (!Array.isArray(message.content)) {
1180
+ return message;
1181
+ }
1182
+ const prunedContent = message.content.map((part) => {
1183
+ if (typeof part === "object" && "toolName" in part && "args" in part) {
1184
+ return {
1185
+ ...part,
1186
+ args: { _pruned: true, toolName: part.toolName }
1187
+ };
1188
+ }
1189
+ return part;
1190
+ });
1191
+ return { ...message, content: prunedContent };
1192
+ }
1193
+ function pruneToolMessage(message) {
1194
+ if (message.role !== "tool") {
1195
+ return message;
1196
+ }
1197
+ if (!Array.isArray(message.content)) {
1198
+ return message;
1199
+ }
1200
+ const prunedContent = message.content.map((part) => {
1201
+ if (typeof part === "object" && "result" in part) {
1202
+ return {
1203
+ ...part,
1204
+ result: { _pruned: true }
1205
+ };
1206
+ }
1207
+ return part;
1208
+ });
1209
+ return { ...message, content: prunedContent };
1210
+ }
1211
+ function pruneMessagesByTokens(messages, config) {
1212
+ const { targetTokens, minSavingsThreshold, protectLastNUserMessages } = {
1213
+ ...DEFAULT_CONFIG2,
1214
+ ...config
1215
+ };
1216
+ const totalTokens = estimateMessagesTokens(messages);
1217
+ const potentialSavings = totalTokens - targetTokens;
1218
+ if (potentialSavings <= minSavingsThreshold) {
1219
+ return messages;
1220
+ }
1221
+ const protectedIndices = findProtectedIndices(messages, protectLastNUserMessages);
1222
+ const prunedMessages = [];
1223
+ let currentTokens = 0;
1224
+ let savedTokens = 0;
1225
+ for (let i = 0;i < messages.length; i++) {
1226
+ const message = messages[i];
1227
+ const isProtected = protectedIndices.has(i);
1228
+ if (isProtected) {
1229
+ prunedMessages.push(message);
1230
+ currentTokens += estimateMessageTokens(message);
1231
+ } else {
1232
+ const originalTokens = estimateMessageTokens(message);
1233
+ if (currentTokens + originalTokens > targetTokens) {
1234
+ let prunedMessage = message;
1235
+ if (message.role === "assistant") {
1236
+ prunedMessage = pruneMessageContent(message);
1237
+ } else if (message.role === "tool") {
1238
+ prunedMessage = pruneToolMessage(message);
1239
+ }
1240
+ const prunedTokens = estimateMessageTokens(prunedMessage);
1241
+ savedTokens += originalTokens - prunedTokens;
1242
+ prunedMessages.push(prunedMessage);
1243
+ currentTokens += prunedTokens;
1244
+ } else {
1245
+ prunedMessages.push(message);
1246
+ currentTokens += originalTokens;
1247
+ }
1248
+ }
1249
+ }
1250
+ return prunedMessages;
1251
+ }
1252
+
1253
+ // src/utils/compact-conversation.ts
1254
+ async function compactConversation(messages, config, state = { conversationSummary: "" }) {
1255
+ const currentTokens = estimateMessagesTokens(messages);
1256
+ const threshold = config.compactionThreshold ?? 0.85;
1257
+ const limit = config.maxTokens * threshold;
1258
+ if (currentTokens < limit) {
1259
+ return { messages, state, didCompact: false };
1260
+ }
1261
+ const protectCount = config.protectRecentMessages ?? 10;
1262
+ const recentMessages = messages.slice(-protectCount);
1263
+ const oldMessages = messages.slice(0, -protectCount);
1264
+ if (oldMessages.length === 0) {
1265
+ return { messages, state, didCompact: false };
1266
+ }
1267
+ const newSummary = await summarizeMessages(oldMessages, config.summarizerModel, config.taskContext, state.conversationSummary);
1268
+ const compactedMessages = [
1269
+ {
1270
+ role: "user",
1271
+ content: `[Previous conversation summary]
1272
+
1273
+ ${newSummary}
1274
+
1275
+ [Continuing from recent messages below...]`
1276
+ },
1277
+ {
1278
+ role: "assistant",
1279
+ content: "I understand the context from the previous conversation. Continuing from where we left off."
1280
+ },
1281
+ ...recentMessages
1282
+ ];
1283
+ return {
1284
+ messages: compactedMessages,
1285
+ state: { conversationSummary: newSummary },
1286
+ didCompact: true
1287
+ };
1288
+ }
1289
+ var SUMMARIZATION_PROMPT = `<context>
1290
+ You are a conversation summarizer for an AI coding agent. The agent has been working on a task and the conversation has grown too long to fit in context. Your job is to create a comprehensive summary that allows the conversation to continue seamlessly.
1291
+ </context>
1292
+
1293
+ <task>
1294
+ Create a structured summary of the conversation below. This summary will replace the old messages, so it MUST preserve all information needed to continue the work.
1295
+ </task>
1296
+
1297
+ <original-goal>
1298
+ {{TASK_CONTEXT}}
1299
+ </original-goal>
1300
+
1301
+ <previous-summary>
1302
+ {{PREVIOUS_SUMMARY}}
1303
+ </previous-summary>
1304
+
1305
+ <conversation-to-summarize>
1306
+ {{CONVERSATION}}
1307
+ </conversation-to-summarize>
1308
+
1309
+ <output-format>
1310
+ Structure your summary with these sections:
1311
+
1312
+ ## Task Overview
1313
+ Brief description of what the user asked for and the current goal.
1314
+
1315
+ ## Progress Made
1316
+ - What has been accomplished so far
1317
+ - Key milestones reached
1318
+
1319
+ ## Files & Code
1320
+ - Files created: list with paths
1321
+ - Files modified: list with paths and what changed
1322
+ - Files read/analyzed: list with paths
1323
+ - Key code patterns or architecture decisions
1324
+
1325
+ ## Technical Decisions
1326
+ - Important choices made and why
1327
+ - Configurations or settings established
1328
+ - Dependencies or tools being used
1329
+
1330
+ ## Errors & Resolutions
1331
+ - Problems encountered
1332
+ - How they were solved (or if still unresolved)
1333
+
1334
+ ## Current State
1335
+ - Where the work left off
1336
+ - What was being worked on when summarized
1337
+ - Any pending questions or blockers
1338
+
1339
+ ## Key Context
1340
+ - Important facts, names, or values that must not be forgotten
1341
+ - User preferences or requirements mentioned
1342
+ </output-format>
1343
+
1344
+ <instructions>
1345
+ - Be thorough. Missing information cannot be recovered.
1346
+ - Preserve exact file paths, variable names, and code snippets where relevant.
1347
+ - If tool calls were made, note what tools were used and their outcomes.
1348
+ - Maintain the user's original terminology and naming.
1349
+ - Do not editorialize or add suggestions - just capture what happened.
1350
+ - Omit sections that have no relevant information.
1351
+ </instructions>`;
1352
+ async function summarizeMessages(messages, model, taskContext, previousSummary) {
1353
+ const prompt = SUMMARIZATION_PROMPT.replace("{{TASK_CONTEXT}}", taskContext || "Not specified").replace("{{PREVIOUS_SUMMARY}}", previousSummary || "None - this is the first compaction").replace("{{CONVERSATION}}", formatMessagesForSummary(messages));
1354
+ const result = await generateText3({
1355
+ model,
1356
+ messages: [
1357
+ {
1358
+ role: "user",
1359
+ content: prompt
1360
+ }
1361
+ ]
1362
+ });
1363
+ return result.text;
1364
+ }
1365
+ function formatMessagesForSummary(messages) {
1366
+ return messages.map((msg, index) => {
1367
+ const role = msg.role.toUpperCase();
1368
+ if (typeof msg.content === "string") {
1369
+ return `<message index="${index}" role="${role}">
1370
+ ${msg.content}
1371
+ </message>`;
1372
+ }
1373
+ if (Array.isArray(msg.content)) {
1374
+ const parts = msg.content.map((part) => {
1375
+ if (typeof part === "string") {
1376
+ return part;
1377
+ }
1378
+ if ("text" in part && typeof part.text === "string") {
1379
+ return part.text;
1380
+ }
1381
+ if ("toolName" in part && "args" in part) {
1382
+ return `[Tool Call: ${part.toolName}]
1383
+ Args: ${JSON.stringify(part.args, null, 2)}`;
1384
+ }
1385
+ if ("result" in part) {
1386
+ const resultStr = typeof part.result === "string" ? part.result : JSON.stringify(part.result, null, 2);
1387
+ return `[Tool Result]
1388
+ ${resultStr}`;
1389
+ }
1390
+ return JSON.stringify(part, null, 2);
1391
+ }).join(`
1392
+
1393
+ `);
1394
+ return `<message index="${index}" role="${role}">
1395
+ ${parts}
1396
+ </message>`;
1397
+ }
1398
+ return `<message index="${index}" role="${role}">
1399
+ ${JSON.stringify(msg.content, null, 2)}
1400
+ </message>`;
1401
+ }).join(`
1402
+
1403
+ `);
1404
+ }
1405
+ var MODEL_CONTEXT_LIMITS = {
1406
+ "claude-opus-4-5": 200000,
1407
+ "claude-sonnet-4-5": 200000,
1408
+ "claude-haiku-4": 200000,
1409
+ "gpt-4o": 128000,
1410
+ "gpt-4-turbo": 128000,
1411
+ "gpt-4": 8192,
1412
+ "gpt-3.5-turbo": 16385,
1413
+ "gemini-2.5-pro": 1e6,
1414
+ "gemini-2.5-flash": 1e6
1415
+ };
1416
+ function createCompactConfig(modelId, summarizerModel, overrides) {
1417
+ const maxTokens = MODEL_CONTEXT_LIMITS[modelId];
1418
+ return {
1419
+ maxTokens,
1420
+ summarizerModel,
1421
+ ...overrides
1422
+ };
1423
+ }
1424
+ // src/utils/context-status.ts
1425
+ var DEFAULT_CONFIG3 = {
1426
+ elevatedThreshold: 0.5,
1427
+ highThreshold: 0.7,
1428
+ criticalThreshold: 0.85
1429
+ };
1430
+ function defaultHighGuidance(metrics) {
1431
+ const used = Math.round(metrics.usagePercent * 100);
1432
+ const remaining = Math.round((1 - metrics.usagePercent) * 100);
1433
+ return `Context usage: ${used}%. You still have ${remaining}% remaining—no need to rush. Continue working thoroughly.`;
1434
+ }
1435
+ function defaultCriticalGuidance(metrics) {
1436
+ const used = Math.round(metrics.usagePercent * 100);
1437
+ return `Context usage: ${used}%. Consider wrapping up the current task or summarizing progress before continuing.`;
1438
+ }
1439
+ function getContextStatus(messages, maxTokens, config) {
1440
+ const { elevatedThreshold, highThreshold, criticalThreshold } = {
1441
+ ...DEFAULT_CONFIG3,
1442
+ ...config
1443
+ };
1444
+ const usedTokens = estimateMessagesTokens(messages);
1445
+ const usagePercent = usedTokens / maxTokens;
1446
+ const baseStatus = { usedTokens, maxTokens, usagePercent };
1447
+ if (usagePercent < elevatedThreshold) {
1448
+ return { ...baseStatus, status: "comfortable" };
1449
+ }
1450
+ if (usagePercent < highThreshold) {
1451
+ return { ...baseStatus, status: "elevated" };
1452
+ }
1453
+ if (usagePercent < criticalThreshold) {
1454
+ const guidance2 = typeof config?.highGuidance === "function" ? config.highGuidance(baseStatus) : config?.highGuidance ?? defaultHighGuidance(baseStatus);
1455
+ return { ...baseStatus, status: "high", guidance: guidance2 };
1456
+ }
1457
+ const guidance = typeof config?.criticalGuidance === "function" ? config.criticalGuidance(baseStatus) : config?.criticalGuidance ?? defaultCriticalGuidance(baseStatus);
1458
+ return { ...baseStatus, status: "critical", guidance };
1459
+ }
1460
+ function contextNeedsAttention(status) {
1461
+ return status.status === "high" || status.status === "critical";
1462
+ }
1463
+ function contextNeedsCompaction(status) {
1464
+ return status.status === "critical";
1465
+ }
1466
+ // src/skills/discovery.ts
1467
+ import { readdir, readFile, stat } from "node:fs/promises";
1468
+ import { homedir } from "node:os";
1469
+ import { join, resolve } from "node:path";
1470
+
1471
+ // src/skills/loader.ts
1472
+ function parseSkillMetadata(content, skillPath) {
1473
+ const frontmatter = extractFrontmatter(content);
1474
+ if (!frontmatter) {
1475
+ throw new Error(`No YAML frontmatter found in ${skillPath}`);
1476
+ }
1477
+ const parsed = parseYaml(frontmatter);
1478
+ if (!parsed.name || typeof parsed.name !== "string") {
1479
+ throw new Error(`Missing or invalid 'name' field in ${skillPath}`);
1480
+ }
1481
+ if (!parsed.description || typeof parsed.description !== "string") {
1482
+ throw new Error(`Missing or invalid 'description' field in ${skillPath}`);
1483
+ }
1484
+ const nameRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
1485
+ if (parsed.name.length > 64 || parsed.name.length > 1 && !nameRegex.test(parsed.name) || parsed.name.includes("--")) {
1486
+ throw new Error(`Invalid 'name' format in ${skillPath}: must be 1-64 lowercase chars/hyphens, no start/end/consecutive hyphens`);
1487
+ }
1488
+ let allowedTools;
1489
+ if (parsed["allowed-tools"]) {
1490
+ const toolsStr = String(parsed["allowed-tools"]);
1491
+ allowedTools = toolsStr.split(/\s+/).filter(Boolean);
1492
+ }
1493
+ let metadata;
1494
+ if (parsed.metadata && typeof parsed.metadata === "object") {
1495
+ metadata = {};
1496
+ for (const [key, value] of Object.entries(parsed.metadata)) {
1497
+ metadata[key] = String(value);
1498
+ }
1499
+ }
1500
+ return {
1501
+ name: parsed.name,
1502
+ description: parsed.description,
1503
+ path: skillPath,
1504
+ license: parsed.license ? String(parsed.license) : undefined,
1505
+ compatibility: parsed.compatibility ? String(parsed.compatibility) : undefined,
1506
+ metadata,
1507
+ allowedTools
1508
+ };
1509
+ }
1510
+ function extractFrontmatter(content) {
1511
+ const trimmed = content.trimStart();
1512
+ if (!trimmed.startsWith("---")) {
1513
+ return null;
1514
+ }
1515
+ const endIndex = trimmed.indexOf(`
1516
+ ---`, 3);
1517
+ if (endIndex === -1) {
1518
+ return null;
1519
+ }
1520
+ return trimmed.slice(3, endIndex).trim();
1521
+ }
1522
+ function parseYaml(yaml) {
1523
+ const result = {};
1524
+ const lines = yaml.split(`
1525
+ `);
1526
+ let currentKey = null;
1527
+ let currentObject = null;
1528
+ for (const line of lines) {
1529
+ if (!line.trim() || line.trim().startsWith("#")) {
1530
+ continue;
1531
+ }
1532
+ const nestedMatch = line.match(/^(\s{2,})(\w+):\s*(.*)$/);
1533
+ if (nestedMatch && currentKey && currentObject) {
1534
+ const [, , key, value] = nestedMatch;
1535
+ currentObject[key] = value.trim().replace(/^["']|["']$/g, "");
1536
+ continue;
1537
+ }
1538
+ const topMatch = line.match(/^(\w[\w-]*):\s*(.*)$/);
1539
+ if (topMatch) {
1540
+ if (currentKey && currentObject) {
1541
+ result[currentKey] = currentObject;
1542
+ currentObject = null;
1543
+ }
1544
+ const [, key, value] = topMatch;
1545
+ const trimmedValue = value.trim();
1546
+ if (trimmedValue === "" || trimmedValue === "|" || trimmedValue === ">") {
1547
+ currentKey = key;
1548
+ currentObject = {};
1549
+ } else {
1550
+ result[key] = trimmedValue.replace(/^["']|["']$/g, "");
1551
+ currentKey = null;
1552
+ }
1553
+ }
1554
+ }
1555
+ if (currentKey && currentObject && Object.keys(currentObject).length > 0) {
1556
+ result[currentKey] = currentObject;
1557
+ }
1558
+ return result;
1559
+ }
1560
+
1561
+ // src/skills/discovery.ts
1562
+ var DEFAULT_SKILL_PATHS = [".skills", "~/.bashkit/skills"];
1563
+ async function discoverSkills(options) {
1564
+ const cwd = options?.cwd ?? process.cwd();
1565
+ const searchPaths = options?.paths ?? DEFAULT_SKILL_PATHS;
1566
+ const skills = [];
1567
+ const seenNames = new Set;
1568
+ for (const searchPath of searchPaths) {
1569
+ const resolvedPath = resolvePath(searchPath, cwd);
1570
+ const foundSkills = await scanDirectory(resolvedPath);
1571
+ for (const skill of foundSkills) {
1572
+ if (!seenNames.has(skill.name)) {
1573
+ seenNames.add(skill.name);
1574
+ skills.push(skill);
1575
+ }
1576
+ }
1577
+ }
1578
+ return skills;
1579
+ }
1580
+ function resolvePath(path, cwd) {
1581
+ if (path.startsWith("~/")) {
1582
+ return join(homedir(), path.slice(2));
1583
+ }
1584
+ if (path.startsWith("/")) {
1585
+ return path;
1586
+ }
1587
+ return resolve(cwd, path);
1588
+ }
1589
+ async function scanDirectory(dirPath) {
1590
+ const skills = [];
1591
+ try {
1592
+ const entries = await readdir(dirPath, { withFileTypes: true });
1593
+ for (const entry of entries) {
1594
+ if (!entry.isDirectory()) {
1595
+ continue;
1596
+ }
1597
+ const skillPath = join(dirPath, entry.name, "SKILL.md");
1598
+ try {
1599
+ const skillStat = await stat(skillPath);
1600
+ if (!skillStat.isFile()) {
1601
+ continue;
1602
+ }
1603
+ const content = await readFile(skillPath, "utf-8");
1604
+ const metadata = parseSkillMetadata(content, skillPath);
1605
+ if (metadata.name !== entry.name) {
1606
+ console.warn(`Skill name "${metadata.name}" does not match folder name "${entry.name}" in ${skillPath}`);
1607
+ }
1608
+ skills.push(metadata);
1609
+ } catch {}
1610
+ }
1611
+ } catch {
1612
+ return [];
1613
+ }
1614
+ return skills;
1615
+ }
1616
+ // src/skills/fetch.ts
1617
+ function parseGitHubRef(ref) {
1618
+ const parts = ref.split("/");
1619
+ if (parts.length < 3) {
1620
+ throw new Error(`Invalid skill reference "${ref}". Expected format: owner/repo/skillName (e.g., anthropics/skills/pdf)`);
1621
+ }
1622
+ const owner = parts[0];
1623
+ const repo = parts[1];
1624
+ const skillName = parts[parts.length - 1];
1625
+ return { owner, repo, skillName };
1626
+ }
1627
+ async function fetchDirectoryContents(owner, repo, path, basePath, branch = "main") {
1628
+ const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`;
1629
+ const response = await fetch(apiUrl, {
1630
+ headers: {
1631
+ Accept: "application/vnd.github.v3+json",
1632
+ "User-Agent": "bashkit"
1633
+ }
1634
+ });
1635
+ if (!response.ok) {
1636
+ throw new Error(`Failed to fetch directory contents from "${path}": ${response.status} ${response.statusText}`);
1637
+ }
1638
+ const items = await response.json();
1639
+ const files = {};
1640
+ await Promise.all(items.map(async (item) => {
1641
+ if (item.type === "file" && item.download_url) {
1642
+ const fileResponse = await fetch(item.download_url);
1643
+ if (fileResponse.ok) {
1644
+ const relativePath = item.path.slice(basePath.length + 1);
1645
+ files[relativePath] = await fileResponse.text();
1646
+ }
1647
+ } else if (item.type === "dir") {
1648
+ const subFiles = await fetchDirectoryContents(owner, repo, item.path, basePath, branch);
1649
+ for (const [subPath, content] of Object.entries(subFiles)) {
1650
+ files[subPath] = content;
1651
+ }
1652
+ }
1653
+ }));
1654
+ return files;
1655
+ }
1656
+ async function fetchSkill(ref) {
1657
+ const { owner, repo, skillName } = parseGitHubRef(ref);
1658
+ const skillPath = `skills/${skillName}`;
1659
+ const files = await fetchDirectoryContents(owner, repo, skillPath, skillPath);
1660
+ if (!files["SKILL.md"]) {
1661
+ throw new Error(`Skill "${ref}" does not contain a SKILL.md file. Found files: ${Object.keys(files).join(", ")}`);
1662
+ }
1663
+ return {
1664
+ name: skillName,
1665
+ files
1666
+ };
1667
+ }
1668
+ async function fetchSkills(refs) {
1669
+ const results = await Promise.all(refs.map(async (ref) => {
1670
+ const bundle = await fetchSkill(ref);
1671
+ return { name: bundle.name, bundle };
1672
+ }));
1673
+ return Object.fromEntries(results.map(({ name, bundle }) => [name, bundle]));
1674
+ }
1675
+ // src/skills/xml.ts
1676
+ function skillsToXml(skills) {
1677
+ if (skills.length === 0) {
1678
+ return `<available_skills>
1679
+ </available_skills>`;
1680
+ }
1681
+ const skillElements = skills.map((skill) => {
1682
+ const name = escapeXml(skill.name);
1683
+ const description = escapeXml(skill.description);
1684
+ const location = escapeXml(skill.path);
1685
+ return ` <skill>
1686
+ <name>${name}</name>
1687
+ <description>${description}</description>
1688
+ <location>${location}</location>
1689
+ </skill>`;
1690
+ }).join(`
1691
+ `);
1692
+ return `<available_skills>
1693
+ ${skillElements}
1694
+ </available_skills>`;
1695
+ }
1696
+ function escapeXml(str) {
1697
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1698
+ }
1699
+ // src/setup/setup-environment.ts
1700
+ function isSkillBundle(content) {
1701
+ return typeof content === "object" && "files" in content;
1702
+ }
1703
+ async function setupAgentEnvironment(sandbox, config) {
1704
+ const skills = [];
1705
+ if (config.workspace) {
1706
+ for (const path of Object.values(config.workspace)) {
1707
+ await createDirectory(sandbox, path);
1708
+ }
1709
+ }
1710
+ if (config.skills) {
1711
+ await createDirectory(sandbox, ".skills");
1712
+ for (const [name, content] of Object.entries(config.skills)) {
1713
+ const skillDir = `.skills/${name}`;
1714
+ if (isSkillBundle(content)) {
1715
+ await seedSkillBundle(sandbox, skillDir, content);
1716
+ const skillMdContent = content.files["SKILL.md"];
1717
+ if (skillMdContent) {
1718
+ try {
1719
+ const metadata = parseSkillMetadata(skillMdContent, `${skillDir}/SKILL.md`);
1720
+ skills.push(metadata);
1721
+ } catch {
1722
+ skills.push({
1723
+ name,
1724
+ description: `Skill: ${name}`,
1725
+ path: `${skillDir}/SKILL.md`
1726
+ });
1727
+ }
1728
+ }
1729
+ } else {
1730
+ const skillPath = `${skillDir}/SKILL.md`;
1731
+ await createDirectory(sandbox, skillDir);
1732
+ await sandbox.writeFile(skillPath, content);
1733
+ try {
1734
+ const metadata = parseSkillMetadata(content, skillPath);
1735
+ skills.push(metadata);
1736
+ } catch {
1737
+ skills.push({
1738
+ name,
1739
+ description: `Skill: ${name}`,
1740
+ path: skillPath
1741
+ });
1742
+ }
1743
+ }
1744
+ }
1745
+ }
1746
+ return { skills };
1747
+ }
1748
+ async function seedSkillBundle(sandbox, skillDir, bundle) {
1749
+ await createDirectory(sandbox, skillDir);
1750
+ for (const [relativePath, content] of Object.entries(bundle.files)) {
1751
+ const fullPath = `${skillDir}/${relativePath}`;
1752
+ const parentDir = fullPath.substring(0, fullPath.lastIndexOf("/"));
1753
+ if (parentDir && parentDir !== skillDir) {
1754
+ await createDirectory(sandbox, parentDir);
1755
+ }
1756
+ await sandbox.writeFile(fullPath, content);
1757
+ }
1758
+ }
1759
+ async function createDirectory(sandbox, path) {
1760
+ const normalizedPath = path.replace(/\/+$/, "");
1761
+ if (!normalizedPath)
1762
+ return;
1763
+ const exists = await sandbox.fileExists(normalizedPath);
1764
+ if (exists) {
1765
+ const isDir = await sandbox.isDirectory(normalizedPath);
1766
+ if (isDir)
1767
+ return;
1768
+ }
1769
+ await sandbox.exec(`mkdir -p "${normalizedPath}"`);
1770
+ }
1771
+ export {
1772
+ skillsToXml,
1773
+ setupAgentEnvironment,
1774
+ pruneMessagesByTokens,
1775
+ parseSkillMetadata,
1776
+ getContextStatus,
1777
+ fetchSkills,
1778
+ fetchSkill,
1779
+ estimateTokens,
1780
+ estimateMessagesTokens,
1781
+ estimateMessageTokens,
1782
+ discoverSkills,
1783
+ createWriteTool,
1784
+ createWebSearchTool,
1785
+ createWebFetchTool,
1786
+ createVercelSandbox,
1787
+ createTodoWriteTool,
1788
+ createTaskTool,
1789
+ createReadTool,
1790
+ createLocalSandbox,
1791
+ createGrepTool,
1792
+ createGlobTool,
1793
+ createExitPlanModeTool,
1794
+ createEditTool,
1795
+ createE2BSandbox,
1796
+ createCompactConfig,
1797
+ createBashTool,
1798
+ createAgentTools,
1799
+ contextNeedsCompaction,
1800
+ contextNeedsAttention,
1801
+ compactConversation,
1802
+ anthropicPromptCacheMiddleware,
1803
+ MODEL_CONTEXT_LIMITS,
1804
+ DEFAULT_CONFIG
1805
+ };