deepagentsdk 0.9.2

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 (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +159 -0
  3. package/package.json +95 -0
  4. package/src/agent.ts +1230 -0
  5. package/src/backends/composite.ts +273 -0
  6. package/src/backends/filesystem.ts +692 -0
  7. package/src/backends/index.ts +22 -0
  8. package/src/backends/local-sandbox.ts +175 -0
  9. package/src/backends/persistent.ts +593 -0
  10. package/src/backends/sandbox.ts +510 -0
  11. package/src/backends/state.ts +244 -0
  12. package/src/backends/utils.ts +287 -0
  13. package/src/checkpointer/file-saver.ts +98 -0
  14. package/src/checkpointer/index.ts +5 -0
  15. package/src/checkpointer/kv-saver.ts +82 -0
  16. package/src/checkpointer/memory-saver.ts +82 -0
  17. package/src/checkpointer/types.ts +125 -0
  18. package/src/cli/components/ApiKeyInput.tsx +300 -0
  19. package/src/cli/components/FilePreview.tsx +237 -0
  20. package/src/cli/components/Input.tsx +277 -0
  21. package/src/cli/components/Message.tsx +93 -0
  22. package/src/cli/components/ModelSelection.tsx +338 -0
  23. package/src/cli/components/SlashMenu.tsx +101 -0
  24. package/src/cli/components/StatusBar.tsx +89 -0
  25. package/src/cli/components/Subagent.tsx +91 -0
  26. package/src/cli/components/TodoList.tsx +133 -0
  27. package/src/cli/components/ToolApproval.tsx +70 -0
  28. package/src/cli/components/ToolCall.tsx +144 -0
  29. package/src/cli/components/ToolCallSummary.tsx +175 -0
  30. package/src/cli/components/Welcome.tsx +75 -0
  31. package/src/cli/components/index.ts +24 -0
  32. package/src/cli/hooks/index.ts +12 -0
  33. package/src/cli/hooks/useAgent.ts +933 -0
  34. package/src/cli/index.tsx +1066 -0
  35. package/src/cli/theme.ts +205 -0
  36. package/src/cli/utils/model-list.ts +365 -0
  37. package/src/constants/errors.ts +29 -0
  38. package/src/constants/limits.ts +195 -0
  39. package/src/index.ts +176 -0
  40. package/src/middleware/agent-memory.ts +330 -0
  41. package/src/prompts.ts +196 -0
  42. package/src/skills/index.ts +2 -0
  43. package/src/skills/load.ts +191 -0
  44. package/src/skills/types.ts +53 -0
  45. package/src/tools/execute.ts +167 -0
  46. package/src/tools/filesystem.ts +418 -0
  47. package/src/tools/index.ts +39 -0
  48. package/src/tools/subagent.ts +443 -0
  49. package/src/tools/todos.ts +101 -0
  50. package/src/tools/web.ts +567 -0
  51. package/src/types/backend.ts +177 -0
  52. package/src/types/core.ts +220 -0
  53. package/src/types/events.ts +429 -0
  54. package/src/types/index.ts +94 -0
  55. package/src/types/structured-output.ts +43 -0
  56. package/src/types/subagent.ts +96 -0
  57. package/src/types.ts +22 -0
  58. package/src/utils/approval.ts +213 -0
  59. package/src/utils/events.ts +416 -0
  60. package/src/utils/eviction.ts +181 -0
  61. package/src/utils/index.ts +34 -0
  62. package/src/utils/model-parser.ts +38 -0
  63. package/src/utils/patch-tool-calls.ts +233 -0
  64. package/src/utils/project-detection.ts +32 -0
  65. package/src/utils/summarization.ts +254 -0
@@ -0,0 +1,692 @@
1
+ /**
2
+ * FilesystemBackend: Read and write files directly from the filesystem.
3
+ */
4
+
5
+ import * as fs from "fs/promises";
6
+ import * as fsSync from "fs";
7
+ import * as path from "path";
8
+ import { spawn } from "child_process";
9
+ import fg from "fast-glob";
10
+ import micromatch from "micromatch";
11
+ import type {
12
+ BackendProtocol,
13
+ EditResult,
14
+ FileData,
15
+ FileInfo,
16
+ GrepMatch,
17
+ WriteResult,
18
+ } from "../types";
19
+ import {
20
+ checkEmptyContent,
21
+ formatContentWithLineNumbers,
22
+ performStringReplacement,
23
+ } from "./utils";
24
+ import {
25
+ FILE_NOT_FOUND,
26
+ FILE_ALREADY_EXISTS,
27
+ } from "../constants/errors";
28
+ import { MAX_FILE_SIZE_MB, DEFAULT_READ_LIMIT } from "../constants/limits";
29
+
30
+ const SUPPORTS_NOFOLLOW = fsSync.constants.O_NOFOLLOW !== undefined;
31
+
32
+ /**
33
+ * Backend that reads and writes files directly from the filesystem.
34
+ *
35
+ * Files are persisted to disk, making them available across agent invocations.
36
+ * This backend provides real file I/O operations with security checks to prevent
37
+ * directory traversal attacks.
38
+ *
39
+ * @example Basic usage
40
+ * ```typescript
41
+ * const backend = new FilesystemBackend({ rootDir: './workspace' });
42
+ * const agent = createDeepAgent({
43
+ * model: anthropic('claude-sonnet-4-20250514'),
44
+ * backend,
45
+ * });
46
+ * ```
47
+ *
48
+ * @example With custom options
49
+ * ```typescript
50
+ * const backend = new FilesystemBackend({
51
+ * rootDir: './my-project',
52
+ * virtualMode: false,
53
+ * maxFileSizeMb: 50, // Allow larger files
54
+ * });
55
+ * ```
56
+ */
57
+ export class FilesystemBackend implements BackendProtocol {
58
+ private cwd: string;
59
+ private virtualMode: boolean;
60
+ private maxFileSizeBytes: number;
61
+
62
+ /**
63
+ * Create a new FilesystemBackend instance.
64
+ *
65
+ * @param options - Configuration options
66
+ * @param options.rootDir - Optional root directory for file operations (default: current working directory).
67
+ * All file paths are resolved relative to this directory.
68
+ * @param options.virtualMode - Optional flag for virtual mode (default: false).
69
+ * When true, files are stored in memory but paths are validated against filesystem.
70
+ * @param options.maxFileSizeMb - Optional maximum file size in MB (default: 10).
71
+ * Files larger than this will be rejected.
72
+ */
73
+ constructor(
74
+ options: {
75
+ rootDir?: string;
76
+ virtualMode?: boolean;
77
+ maxFileSizeMb?: number;
78
+ } = {}
79
+ ) {
80
+ const { rootDir, virtualMode = false, maxFileSizeMb = MAX_FILE_SIZE_MB } = options;
81
+ this.cwd = rootDir ? path.resolve(rootDir) : process.cwd();
82
+ this.virtualMode = virtualMode;
83
+ this.maxFileSizeBytes = maxFileSizeMb * 1024 * 1024;
84
+ }
85
+
86
+ /**
87
+ * Resolve a file path with security checks.
88
+ */
89
+ private resolvePath(key: string): string {
90
+ if (this.virtualMode) {
91
+ const vpath = key.startsWith("/") ? key : "/" + key;
92
+ if (vpath.includes("..") || vpath.startsWith("~")) {
93
+ throw new Error("Path traversal not allowed");
94
+ }
95
+ const full = path.resolve(this.cwd, vpath.substring(1));
96
+ const relative = path.relative(this.cwd, full);
97
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
98
+ throw new Error(`Path: ${full} outside root directory: ${this.cwd}`);
99
+ }
100
+ return full;
101
+ }
102
+
103
+ if (path.isAbsolute(key)) {
104
+ return key;
105
+ }
106
+ return path.resolve(this.cwd, key);
107
+ }
108
+
109
+ /**
110
+ * List files and directories in the specified directory (non-recursive).
111
+ */
112
+ async lsInfo(dirPath: string): Promise<FileInfo[]> {
113
+ try {
114
+ const resolvedPath = this.resolvePath(dirPath);
115
+ const stat = await fs.stat(resolvedPath);
116
+
117
+ if (!stat.isDirectory()) {
118
+ return [];
119
+ }
120
+
121
+ const entries = await fs.readdir(resolvedPath, { withFileTypes: true });
122
+ const results: FileInfo[] = [];
123
+
124
+ const cwdStr = this.cwd.endsWith(path.sep)
125
+ ? this.cwd
126
+ : this.cwd + path.sep;
127
+
128
+ for (const entry of entries) {
129
+ const fullPath = path.join(resolvedPath, entry.name);
130
+
131
+ try {
132
+ const entryStat = await fs.stat(fullPath);
133
+ const isFile = entryStat.isFile();
134
+ const isDir = entryStat.isDirectory();
135
+
136
+ if (!this.virtualMode) {
137
+ if (isFile) {
138
+ results.push({
139
+ path: fullPath,
140
+ is_dir: false,
141
+ size: entryStat.size,
142
+ modified_at: entryStat.mtime.toISOString(),
143
+ });
144
+ } else if (isDir) {
145
+ results.push({
146
+ path: fullPath + path.sep,
147
+ is_dir: true,
148
+ size: 0,
149
+ modified_at: entryStat.mtime.toISOString(),
150
+ });
151
+ }
152
+ } else {
153
+ let relativePath: string;
154
+ if (fullPath.startsWith(cwdStr)) {
155
+ relativePath = fullPath.substring(cwdStr.length);
156
+ } else if (fullPath.startsWith(this.cwd)) {
157
+ relativePath = fullPath
158
+ .substring(this.cwd.length)
159
+ .replace(/^[/\\]/, "");
160
+ } else {
161
+ relativePath = fullPath;
162
+ }
163
+
164
+ relativePath = relativePath.split(path.sep).join("/");
165
+ const virtPath = "/" + relativePath;
166
+
167
+ if (isFile) {
168
+ results.push({
169
+ path: virtPath,
170
+ is_dir: false,
171
+ size: entryStat.size,
172
+ modified_at: entryStat.mtime.toISOString(),
173
+ });
174
+ } else if (isDir) {
175
+ results.push({
176
+ path: virtPath + "/",
177
+ is_dir: true,
178
+ size: 0,
179
+ modified_at: entryStat.mtime.toISOString(),
180
+ });
181
+ }
182
+ }
183
+ } catch {
184
+ continue;
185
+ }
186
+ }
187
+
188
+ results.sort((a, b) => a.path.localeCompare(b.path));
189
+ return results;
190
+ } catch {
191
+ return [];
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Read file content with line numbers.
197
+ */
198
+ async read(
199
+ filePath: string,
200
+ offset: number = 0,
201
+ limit: number = DEFAULT_READ_LIMIT
202
+ ): Promise<string> {
203
+ try {
204
+ const resolvedPath = this.resolvePath(filePath);
205
+
206
+ let content: string;
207
+
208
+ if (SUPPORTS_NOFOLLOW) {
209
+ const stat = await fs.stat(resolvedPath);
210
+ if (!stat.isFile()) {
211
+ return FILE_NOT_FOUND(filePath);
212
+ }
213
+ const fd = await fs.open(
214
+ resolvedPath,
215
+ fsSync.constants.O_RDONLY | fsSync.constants.O_NOFOLLOW
216
+ );
217
+ try {
218
+ content = await fd.readFile({ encoding: "utf-8" });
219
+ } finally {
220
+ await fd.close();
221
+ }
222
+ } else {
223
+ const stat = await fs.lstat(resolvedPath);
224
+ if (stat.isSymbolicLink()) {
225
+ return `Error: Symlinks are not allowed: ${filePath}`;
226
+ }
227
+ if (!stat.isFile()) {
228
+ return FILE_NOT_FOUND(filePath);
229
+ }
230
+ content = await fs.readFile(resolvedPath, "utf-8");
231
+ }
232
+
233
+ const emptyMsg = checkEmptyContent(content);
234
+ if (emptyMsg) {
235
+ return emptyMsg;
236
+ }
237
+
238
+ const lines = content.split("\n");
239
+ const startIdx = offset;
240
+ const endIdx = Math.min(startIdx + limit, lines.length);
241
+
242
+ if (startIdx >= lines.length) {
243
+ return `Error: Line offset ${offset} exceeds file length (${lines.length} lines)`;
244
+ }
245
+
246
+ const selectedLines = lines.slice(startIdx, endIdx);
247
+ return formatContentWithLineNumbers(selectedLines, startIdx + 1);
248
+ } catch (e: unknown) {
249
+ const error = e as Error;
250
+ return `Error reading file '${filePath}': ${error.message}`;
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Read file content as raw FileData.
256
+ */
257
+ async readRaw(filePath: string): Promise<FileData> {
258
+ const resolvedPath = this.resolvePath(filePath);
259
+
260
+ let content: string;
261
+ let stat: fsSync.Stats;
262
+
263
+ if (SUPPORTS_NOFOLLOW) {
264
+ stat = await fs.stat(resolvedPath);
265
+ if (!stat.isFile()) throw new Error(`File '${filePath}' not found`);
266
+ const fd = await fs.open(
267
+ resolvedPath,
268
+ fsSync.constants.O_RDONLY | fsSync.constants.O_NOFOLLOW
269
+ );
270
+ try {
271
+ content = await fd.readFile({ encoding: "utf-8" });
272
+ } finally {
273
+ await fd.close();
274
+ }
275
+ } else {
276
+ stat = await fs.lstat(resolvedPath);
277
+ if (stat.isSymbolicLink()) {
278
+ throw new Error(`Symlinks are not allowed: ${filePath}`);
279
+ }
280
+ if (!stat.isFile()) throw new Error(`File '${filePath}' not found`);
281
+ content = await fs.readFile(resolvedPath, "utf-8");
282
+ }
283
+
284
+ return {
285
+ content: content.split("\n"),
286
+ created_at: stat.ctime.toISOString(),
287
+ modified_at: stat.mtime.toISOString(),
288
+ };
289
+ }
290
+
291
+ /**
292
+ * Create a new file with content.
293
+ */
294
+ async write(filePath: string, content: string): Promise<WriteResult> {
295
+ try {
296
+ const resolvedPath = this.resolvePath(filePath);
297
+
298
+ try {
299
+ const stat = await fs.lstat(resolvedPath);
300
+ if (stat.isSymbolicLink()) {
301
+ return {
302
+ success: false,
303
+ error: `Cannot write to ${filePath} because it is a symlink. Symlinks are not allowed.`,
304
+ };
305
+ }
306
+ return {
307
+ success: false,
308
+ error: FILE_ALREADY_EXISTS(filePath),
309
+ };
310
+ } catch {
311
+ // File doesn't exist, good to proceed
312
+ }
313
+
314
+ await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
315
+
316
+ if (SUPPORTS_NOFOLLOW) {
317
+ const flags =
318
+ fsSync.constants.O_WRONLY |
319
+ fsSync.constants.O_CREAT |
320
+ fsSync.constants.O_TRUNC |
321
+ fsSync.constants.O_NOFOLLOW;
322
+
323
+ const fd = await fs.open(resolvedPath, flags, 0o644);
324
+ try {
325
+ await fd.writeFile(content, "utf-8");
326
+ } finally {
327
+ await fd.close();
328
+ }
329
+ } else {
330
+ await fs.writeFile(resolvedPath, content, "utf-8");
331
+ }
332
+
333
+ return { success: true, path: filePath };
334
+ } catch (e: unknown) {
335
+ const error = e as Error;
336
+ return { success: false, error: `Error writing file '${filePath}': ${error.message}` };
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Edit a file by replacing string occurrences.
342
+ */
343
+ async edit(
344
+ filePath: string,
345
+ oldString: string,
346
+ newString: string,
347
+ replaceAll: boolean = false
348
+ ): Promise<EditResult> {
349
+ try {
350
+ const resolvedPath = this.resolvePath(filePath);
351
+
352
+ let content: string;
353
+
354
+ if (SUPPORTS_NOFOLLOW) {
355
+ const stat = await fs.stat(resolvedPath);
356
+ if (!stat.isFile()) {
357
+ return { success: false, error: FILE_NOT_FOUND(filePath) };
358
+ }
359
+
360
+ const fd = await fs.open(
361
+ resolvedPath,
362
+ fsSync.constants.O_RDONLY | fsSync.constants.O_NOFOLLOW
363
+ );
364
+ try {
365
+ content = await fd.readFile({ encoding: "utf-8" });
366
+ } finally {
367
+ await fd.close();
368
+ }
369
+ } else {
370
+ const stat = await fs.lstat(resolvedPath);
371
+ if (stat.isSymbolicLink()) {
372
+ return { success: false, error: `Error: Symlinks are not allowed: ${filePath}` };
373
+ }
374
+ if (!stat.isFile()) {
375
+ return { success: false, error: FILE_NOT_FOUND(filePath) };
376
+ }
377
+ content = await fs.readFile(resolvedPath, "utf-8");
378
+ }
379
+
380
+ const result = performStringReplacement(
381
+ content,
382
+ oldString,
383
+ newString,
384
+ replaceAll
385
+ );
386
+
387
+ if (typeof result === "string") {
388
+ return { success: false, error: result };
389
+ }
390
+
391
+ const [newContent, occurrences] = result;
392
+
393
+ if (SUPPORTS_NOFOLLOW) {
394
+ const flags =
395
+ fsSync.constants.O_WRONLY |
396
+ fsSync.constants.O_TRUNC |
397
+ fsSync.constants.O_NOFOLLOW;
398
+
399
+ const fd = await fs.open(resolvedPath, flags);
400
+ try {
401
+ await fd.writeFile(newContent, "utf-8");
402
+ } finally {
403
+ await fd.close();
404
+ }
405
+ } else {
406
+ await fs.writeFile(resolvedPath, newContent, "utf-8");
407
+ }
408
+
409
+ return { success: true, path: filePath, occurrences };
410
+ } catch (e: unknown) {
411
+ const error = e as Error;
412
+ return { success: false, error: `Error editing file '${filePath}': ${error.message}` };
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Structured search results or error string for invalid input.
418
+ */
419
+ async grepRaw(
420
+ pattern: string,
421
+ dirPath: string = "/",
422
+ glob: string | null = null
423
+ ): Promise<GrepMatch[] | string> {
424
+ // Validate regex
425
+ try {
426
+ new RegExp(pattern);
427
+ } catch (e: unknown) {
428
+ const error = e as Error;
429
+ return `Invalid regex pattern: ${error.message}`;
430
+ }
431
+
432
+ // Resolve base path
433
+ let baseFull: string;
434
+ try {
435
+ baseFull = this.resolvePath(dirPath || ".");
436
+ } catch {
437
+ return [];
438
+ }
439
+
440
+ try {
441
+ await fs.stat(baseFull);
442
+ } catch {
443
+ return [];
444
+ }
445
+
446
+ // Try ripgrep first, fallback to regex search
447
+ let results = await this.ripgrepSearch(pattern, baseFull, glob);
448
+ if (results === null) {
449
+ results = await this.regexSearch(pattern, baseFull, glob);
450
+ }
451
+
452
+ const matches: GrepMatch[] = [];
453
+ for (const [fpath, items] of Object.entries(results)) {
454
+ for (const [lineNum, lineText] of items) {
455
+ matches.push({ path: fpath, line: lineNum, text: lineText });
456
+ }
457
+ }
458
+ return matches;
459
+ }
460
+
461
+ /**
462
+ * Try to use ripgrep for fast searching.
463
+ */
464
+ private async ripgrepSearch(
465
+ pattern: string,
466
+ baseFull: string,
467
+ includeGlob: string | null
468
+ ): Promise<Record<string, Array<[number, string]>> | null> {
469
+ return new Promise((resolve) => {
470
+ const args = ["--json"];
471
+ if (includeGlob) {
472
+ args.push("--glob", includeGlob);
473
+ }
474
+ args.push("--", pattern, baseFull);
475
+
476
+ const proc = spawn("rg", args, { timeout: 30000 });
477
+ const results: Record<string, Array<[number, string]>> = {};
478
+ let output = "";
479
+
480
+ proc.stdout.on("data", (data) => {
481
+ output += data.toString();
482
+ });
483
+
484
+ proc.on("close", (code) => {
485
+ if (code !== 0 && code !== 1) {
486
+ resolve(null);
487
+ return;
488
+ }
489
+
490
+ for (const line of output.split("\n")) {
491
+ if (!line.trim()) continue;
492
+ try {
493
+ const data = JSON.parse(line);
494
+ if (data.type !== "match") continue;
495
+
496
+ const pdata = data.data || {};
497
+ const ftext = pdata.path?.text;
498
+ if (!ftext) continue;
499
+
500
+ let virtPath: string;
501
+ if (this.virtualMode) {
502
+ try {
503
+ const resolved = path.resolve(ftext);
504
+ const relative = path.relative(this.cwd, resolved);
505
+ if (relative.startsWith("..")) continue;
506
+ const normalizedRelative = relative.split(path.sep).join("/");
507
+ virtPath = "/" + normalizedRelative;
508
+ } catch {
509
+ continue;
510
+ }
511
+ } else {
512
+ virtPath = ftext;
513
+ }
514
+
515
+ const ln = pdata.line_number;
516
+ const lt = pdata.lines?.text?.replace(/\n$/, "") || "";
517
+ if (ln === undefined) continue;
518
+
519
+ if (!results[virtPath]) {
520
+ results[virtPath] = [];
521
+ }
522
+ results[virtPath]!.push([ln, lt]);
523
+ } catch {
524
+ continue;
525
+ }
526
+ }
527
+
528
+ resolve(results);
529
+ });
530
+
531
+ proc.on("error", () => {
532
+ resolve(null);
533
+ });
534
+ });
535
+ }
536
+
537
+ /**
538
+ * Fallback regex search implementation.
539
+ */
540
+ private async regexSearch(
541
+ pattern: string,
542
+ baseFull: string,
543
+ includeGlob: string | null
544
+ ): Promise<Record<string, Array<[number, string]>>> {
545
+ let regex: RegExp;
546
+ try {
547
+ regex = new RegExp(pattern);
548
+ } catch {
549
+ return {};
550
+ }
551
+
552
+ const results: Record<string, Array<[number, string]>> = {};
553
+ const stat = await fs.stat(baseFull);
554
+ const root = stat.isDirectory() ? baseFull : path.dirname(baseFull);
555
+
556
+ const files = await fg("**/*", {
557
+ cwd: root,
558
+ absolute: true,
559
+ onlyFiles: true,
560
+ dot: true,
561
+ });
562
+
563
+ for (const fp of files) {
564
+ try {
565
+ if (
566
+ includeGlob &&
567
+ !micromatch.isMatch(path.basename(fp), includeGlob)
568
+ ) {
569
+ continue;
570
+ }
571
+
572
+ const fileStat = await fs.stat(fp);
573
+ if (fileStat.size > this.maxFileSizeBytes) {
574
+ continue;
575
+ }
576
+
577
+ const content = await fs.readFile(fp, "utf-8");
578
+ const lines = content.split("\n");
579
+
580
+ for (let i = 0; i < lines.length; i++) {
581
+ const line = lines[i];
582
+ if (line && regex.test(line)) {
583
+ let virtPath: string;
584
+ if (this.virtualMode) {
585
+ try {
586
+ const relative = path.relative(this.cwd, fp);
587
+ if (relative.startsWith("..")) continue;
588
+ const normalizedRelative = relative.split(path.sep).join("/");
589
+ virtPath = "/" + normalizedRelative;
590
+ } catch {
591
+ continue;
592
+ }
593
+ } else {
594
+ virtPath = fp;
595
+ }
596
+
597
+ if (!results[virtPath]) {
598
+ results[virtPath] = [];
599
+ }
600
+ results[virtPath]!.push([i + 1, line]);
601
+ }
602
+ }
603
+ } catch {
604
+ continue;
605
+ }
606
+ }
607
+
608
+ return results;
609
+ }
610
+
611
+ /**
612
+ * Structured glob matching returning FileInfo objects.
613
+ */
614
+ async globInfo(pattern: string, searchPath: string = "/"): Promise<FileInfo[]> {
615
+ if (pattern.startsWith("/")) {
616
+ pattern = pattern.substring(1);
617
+ }
618
+
619
+ const resolvedSearchPath =
620
+ searchPath === "/" ? this.cwd : this.resolvePath(searchPath);
621
+
622
+ try {
623
+ const stat = await fs.stat(resolvedSearchPath);
624
+ if (!stat.isDirectory()) {
625
+ return [];
626
+ }
627
+ } catch {
628
+ return [];
629
+ }
630
+
631
+ const results: FileInfo[] = [];
632
+
633
+ try {
634
+ const matches = await fg(pattern, {
635
+ cwd: resolvedSearchPath,
636
+ absolute: true,
637
+ onlyFiles: true,
638
+ dot: true,
639
+ });
640
+
641
+ for (const matchedPath of matches) {
642
+ try {
643
+ const fileStat = await fs.stat(matchedPath);
644
+ if (!fileStat.isFile()) continue;
645
+
646
+ const normalizedPath = matchedPath.split("/").join(path.sep);
647
+
648
+ if (!this.virtualMode) {
649
+ results.push({
650
+ path: normalizedPath,
651
+ is_dir: false,
652
+ size: fileStat.size,
653
+ modified_at: fileStat.mtime.toISOString(),
654
+ });
655
+ } else {
656
+ const cwdStr = this.cwd.endsWith(path.sep)
657
+ ? this.cwd
658
+ : this.cwd + path.sep;
659
+ let relativePath: string;
660
+
661
+ if (normalizedPath.startsWith(cwdStr)) {
662
+ relativePath = normalizedPath.substring(cwdStr.length);
663
+ } else if (normalizedPath.startsWith(this.cwd)) {
664
+ relativePath = normalizedPath
665
+ .substring(this.cwd.length)
666
+ .replace(/^[/\\]/, "");
667
+ } else {
668
+ relativePath = normalizedPath;
669
+ }
670
+
671
+ relativePath = relativePath.split(path.sep).join("/");
672
+ const virt = "/" + relativePath;
673
+ results.push({
674
+ path: virt,
675
+ is_dir: false,
676
+ size: fileStat.size,
677
+ modified_at: fileStat.mtime.toISOString(),
678
+ });
679
+ }
680
+ } catch {
681
+ continue;
682
+ }
683
+ }
684
+ } catch {
685
+ // Ignore glob errors
686
+ }
687
+
688
+ results.sort((a, b) => a.path.localeCompare(b.path));
689
+ return results;
690
+ }
691
+ }
692
+
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Backends for pluggable file storage and command execution.
3
+ */
4
+
5
+ // Standard backends (BackendProtocol)
6
+ export { StateBackend } from "./state";
7
+ export { FilesystemBackend } from "./filesystem";
8
+ export { CompositeBackend } from "./composite";
9
+ export {
10
+ PersistentBackend,
11
+ InMemoryStore,
12
+ type KeyValueStore,
13
+ type PersistentBackendOptions,
14
+ } from "./persistent";
15
+
16
+ // Sandbox backends (SandboxBackendProtocol)
17
+ export { BaseSandbox } from "./sandbox";
18
+ export { LocalSandbox, type LocalSandboxOptions } from "./local-sandbox";
19
+
20
+ // Re-export utilities
21
+ export * from "./utils";
22
+