@librechat/agents 3.1.90 → 3.1.92

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 (94) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +9 -5
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +48 -14
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/instrumentation.cjs +2 -7
  6. package/dist/cjs/instrumentation.cjs.map +1 -1
  7. package/dist/cjs/langfuse.cjs +285 -0
  8. package/dist/cjs/langfuse.cjs.map +1 -0
  9. package/dist/cjs/main.cjs +25 -0
  10. package/dist/cjs/main.cjs.map +1 -1
  11. package/dist/cjs/run.cjs +75 -44
  12. package/dist/cjs/run.cjs.map +1 -1
  13. package/dist/cjs/stream.cjs +10 -3
  14. package/dist/cjs/stream.cjs.map +1 -1
  15. package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs +380 -0
  16. package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs.map +1 -0
  17. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs +997 -0
  18. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -0
  19. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +575 -0
  20. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -0
  21. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs +165 -0
  22. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs.map +1 -0
  23. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +17 -5
  24. package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
  25. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +110 -6
  26. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -1
  27. package/dist/cjs/utils/callbacks.cjs +27 -0
  28. package/dist/cjs/utils/callbacks.cjs.map +1 -0
  29. package/dist/esm/agents/AgentContext.mjs +9 -5
  30. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  31. package/dist/esm/graphs/Graph.mjs +48 -14
  32. package/dist/esm/graphs/Graph.mjs.map +1 -1
  33. package/dist/esm/instrumentation.mjs +2 -7
  34. package/dist/esm/instrumentation.mjs.map +1 -1
  35. package/dist/esm/langfuse.mjs +275 -0
  36. package/dist/esm/langfuse.mjs.map +1 -0
  37. package/dist/esm/main.mjs +5 -1
  38. package/dist/esm/main.mjs.map +1 -1
  39. package/dist/esm/run.mjs +75 -44
  40. package/dist/esm/run.mjs.map +1 -1
  41. package/dist/esm/stream.mjs +10 -3
  42. package/dist/esm/stream.mjs.map +1 -1
  43. package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs +378 -0
  44. package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs.map +1 -0
  45. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs +994 -0
  46. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -0
  47. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +566 -0
  48. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -0
  49. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs +155 -0
  50. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs.map +1 -0
  51. package/dist/esm/tools/local/LocalExecutionEngine.mjs +17 -6
  52. package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
  53. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +111 -7
  54. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -1
  55. package/dist/esm/utils/callbacks.mjs +24 -0
  56. package/dist/esm/utils/callbacks.mjs.map +1 -0
  57. package/dist/types/agents/AgentContext.d.ts +4 -1
  58. package/dist/types/graphs/Graph.d.ts +6 -5
  59. package/dist/types/index.d.ts +1 -0
  60. package/dist/types/langfuse.d.ts +57 -0
  61. package/dist/types/tools/cloudflare/CloudflareBridgeRuntime.d.ts +23 -0
  62. package/dist/types/tools/cloudflare/CloudflareProgrammaticToolCalling.d.ts +4 -0
  63. package/dist/types/tools/cloudflare/CloudflareSandboxExecutionEngine.d.ts +21 -0
  64. package/dist/types/tools/cloudflare/CloudflareSandboxTools.d.ts +22 -0
  65. package/dist/types/tools/cloudflare/index.d.ts +4 -0
  66. package/dist/types/tools/local/LocalExecutionEngine.d.ts +1 -0
  67. package/dist/types/types/graph.d.ts +8 -0
  68. package/dist/types/types/run.d.ts +2 -2
  69. package/dist/types/types/tools.d.ts +118 -2
  70. package/dist/types/utils/callbacks.d.ts +5 -0
  71. package/package.json +4 -4
  72. package/src/__tests__/stream.eagerEventExecution.test.ts +66 -0
  73. package/src/agents/AgentContext.ts +13 -3
  74. package/src/graphs/Graph.ts +57 -16
  75. package/src/index.ts +1 -0
  76. package/src/instrumentation.ts +2 -7
  77. package/src/langfuse.ts +441 -0
  78. package/src/run.ts +105 -59
  79. package/src/specs/langfuse-callbacks.test.ts +75 -0
  80. package/src/specs/langfuse-config.test.ts +114 -0
  81. package/src/specs/langfuse-metadata.test.ts +19 -1
  82. package/src/stream.ts +13 -3
  83. package/src/tools/__tests__/CloudflareSandboxExecution.test.ts +537 -0
  84. package/src/tools/cloudflare/CloudflareBridgeRuntime.ts +480 -0
  85. package/src/tools/cloudflare/CloudflareProgrammaticToolCalling.ts +1162 -0
  86. package/src/tools/cloudflare/CloudflareSandboxExecutionEngine.ts +744 -0
  87. package/src/tools/cloudflare/CloudflareSandboxTools.ts +225 -0
  88. package/src/tools/cloudflare/index.ts +4 -0
  89. package/src/tools/local/LocalExecutionEngine.ts +20 -4
  90. package/src/tools/local/resolveLocalExecutionTools.ts +169 -7
  91. package/src/types/graph.ts +9 -0
  92. package/src/types/run.ts +2 -7
  93. package/src/types/tools.ts +141 -2
  94. package/src/utils/callbacks.ts +39 -0
@@ -0,0 +1,480 @@
1
+ import { posix as path } from 'path';
2
+ import type * as t from '@/types';
3
+
4
+ const DEFAULT_API_PREFIX = '/v1';
5
+ const DEFAULT_WORKSPACE_ROOT = '/workspace';
6
+ const DEFAULT_SHELL = 'bash';
7
+
8
+ export type CloudflareBridgeRuntimeConfig = {
9
+ /** Base URL of a Worker using `bridge()` from `@cloudflare/sandbox/bridge`. */
10
+ baseURL: string;
11
+ /** Bearer token stored in the bridge Worker as `SANDBOX_API_KEY`. */
12
+ apiKey?: string;
13
+ /** Existing sandbox id. If omitted, the adapter creates one lazily. */
14
+ sandboxId?: string;
15
+ /** Bridge API route prefix. Defaults to `/v1`. */
16
+ apiRoutePrefix?: string;
17
+ /** Workspace root used for path clamping. Defaults to `/workspace`. */
18
+ workspaceRoot?: string;
19
+ /** Optional bridge session id sent as `Session-Id`. */
20
+ sessionId?: string;
21
+ /** Shell used to run command strings over the bridge exec endpoint. */
22
+ shell?: string;
23
+ /** Optional fetch implementation. Defaults to global `fetch`. */
24
+ fetch?: typeof fetch;
25
+ };
26
+
27
+ export type CloudflareBridgeRuntime = t.CloudflareSandboxRuntime & {
28
+ getSandboxId(): Promise<string>;
29
+ };
30
+
31
+ type BridgeSSEEvent = {
32
+ event: string;
33
+ data: string;
34
+ };
35
+
36
+ function normalizeBaseURL(baseURL: string): string {
37
+ return baseURL.replace(/\/+$/, '');
38
+ }
39
+
40
+ function normalizePrefix(prefix: string | undefined): string {
41
+ const raw = prefix ?? DEFAULT_API_PREFIX;
42
+ const withLeadingSlash = raw.startsWith('/') ? raw : `/${raw}`;
43
+ return withLeadingSlash.replace(/\/+$/, '');
44
+ }
45
+
46
+ function normalizeWorkspaceRoot(workspaceRoot: string): string {
47
+ const normalized = path.normalize(workspaceRoot);
48
+ return normalized === '/' ? normalized : normalized.replace(/\/+$/, '');
49
+ }
50
+
51
+ function getFetch(config: CloudflareBridgeRuntimeConfig): typeof fetch {
52
+ const fetchImpl = config.fetch ?? globalThis.fetch;
53
+ return fetchImpl.bind(globalThis) as typeof fetch;
54
+ }
55
+
56
+ function quote(value: string): string {
57
+ if (value === '') {
58
+ return '\'\'';
59
+ }
60
+ if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(value)) {
61
+ return value;
62
+ }
63
+ return `'${value.replace(/'/g, '\'\\\'\'')}'`;
64
+ }
65
+
66
+ function encodeBridgePath(filePath: string): string {
67
+ return filePath
68
+ .replace(/^\/+/, '')
69
+ .split('/')
70
+ .map((part) => encodeURIComponent(part))
71
+ .join('/');
72
+ }
73
+
74
+ function toSandboxPath(filePath: string, workspaceRoot: string): string {
75
+ const raw = filePath === '' ? '.' : filePath;
76
+ const root = normalizeWorkspaceRoot(workspaceRoot);
77
+ const resolved = raw.startsWith('/')
78
+ ? path.normalize(raw)
79
+ : path.resolve(root, raw);
80
+ if (root === '/') {
81
+ return resolved;
82
+ }
83
+ if (resolved === root || resolved.startsWith(`${root}/`)) {
84
+ return resolved;
85
+ }
86
+ throw new Error(
87
+ `Path is outside the Cloudflare sandbox workspace: ${filePath}`
88
+ );
89
+ }
90
+
91
+ function typeFromFind(value: string): t.CloudflareSandboxFileInfo['type'] {
92
+ switch (value) {
93
+ case 'd':
94
+ return 'directory';
95
+ case 'l':
96
+ return 'symlink';
97
+ case 'f':
98
+ return 'file';
99
+ default:
100
+ return 'other';
101
+ }
102
+ }
103
+
104
+ function decodeBase64(value: string): string {
105
+ return Buffer.from(value, 'base64').toString('utf8');
106
+ }
107
+
108
+ async function readResponseText(response: Response): Promise<string> {
109
+ try {
110
+ return await response.text();
111
+ } catch {
112
+ return '';
113
+ }
114
+ }
115
+
116
+ async function assertOk(response: Response, operation: string): Promise<void> {
117
+ if (response.ok) {
118
+ return;
119
+ }
120
+ const text = await readResponseText(response);
121
+ throw new Error(
122
+ `Cloudflare sandbox bridge ${operation} failed (${response.status}): ${text}`
123
+ );
124
+ }
125
+
126
+ function createHeaders(
127
+ config: CloudflareBridgeRuntimeConfig,
128
+ extra?: HeadersInit
129
+ ): Headers {
130
+ const headers = new Headers(extra);
131
+ if (config.apiKey != null && config.apiKey !== '') {
132
+ headers.set('Authorization', `Bearer ${config.apiKey}`);
133
+ }
134
+ if (config.sessionId != null && config.sessionId !== '') {
135
+ headers.set('Session-Id', config.sessionId);
136
+ }
137
+ return headers;
138
+ }
139
+
140
+ function envScript(env?: Record<string, string | undefined>): string {
141
+ if (env == null) {
142
+ return '';
143
+ }
144
+ return Object.entries(env)
145
+ .filter((entry): entry is [string, string] => entry[1] != null)
146
+ .map(([key, value]) => `export ${key}=${quote(value)}`)
147
+ .join('\n');
148
+ }
149
+
150
+ function commandWithEnv(
151
+ command: string,
152
+ env?: Record<string, string | undefined>
153
+ ): string {
154
+ const exports = envScript(env);
155
+ return exports === '' ? command : `${exports}\n${command}`;
156
+ }
157
+
158
+ async function collectReadableStream(
159
+ stream: ReadableStream<Uint8Array>
160
+ ): Promise<Uint8Array> {
161
+ const reader = stream.getReader();
162
+ const chunks: Uint8Array[] = [];
163
+ let total = 0;
164
+ try {
165
+ for (;;) {
166
+ const { done, value } = await reader.read();
167
+ if (done) break;
168
+ chunks.push(value);
169
+ total += value.byteLength;
170
+ }
171
+ } finally {
172
+ reader.releaseLock();
173
+ }
174
+ const bytes = new Uint8Array(total);
175
+ let offset = 0;
176
+ for (const chunk of chunks) {
177
+ bytes.set(chunk, offset);
178
+ offset += chunk.byteLength;
179
+ }
180
+ return bytes;
181
+ }
182
+
183
+ async function normalizeWriteBody(
184
+ content: string | ReadableStream<Uint8Array>,
185
+ options?: { encoding?: string }
186
+ ): Promise<Uint8Array> {
187
+ if (typeof content !== 'string') {
188
+ return collectReadableStream(content);
189
+ }
190
+ if (options?.encoding === 'base64') {
191
+ return Buffer.from(content, 'base64');
192
+ }
193
+ return Buffer.from(content, options?.encoding === 'utf8' ? 'utf8' : 'utf8');
194
+ }
195
+
196
+ function parseSSEChunk(buffer: string): {
197
+ events: BridgeSSEEvent[];
198
+ remainder: string;
199
+ } {
200
+ const normalized = buffer.replace(/\r\n/g, '\n');
201
+ const parts = normalized.split('\n\n');
202
+ const remainder = parts.pop() ?? '';
203
+ const events = parts
204
+ .map((part) => {
205
+ let event = 'message';
206
+ const dataLines: string[] = [];
207
+ for (const line of part.split('\n')) {
208
+ if (line.startsWith('event:')) {
209
+ event = line.slice('event:'.length).trim();
210
+ } else if (line.startsWith('data:')) {
211
+ dataLines.push(line.slice('data:'.length).trimStart());
212
+ }
213
+ }
214
+ return { event, data: dataLines.join('\n') };
215
+ })
216
+ .filter((event) => event.data !== '' || event.event !== 'message');
217
+ return { events, remainder };
218
+ }
219
+
220
+ function parseExitCode(data: string): number {
221
+ try {
222
+ const parsed = JSON.parse(data) as { exit_code?: number };
223
+ return typeof parsed.exit_code === 'number' ? parsed.exit_code : 1;
224
+ } catch {
225
+ return 1;
226
+ }
227
+ }
228
+
229
+ function parseBridgeError(data: string): string {
230
+ try {
231
+ const parsed = JSON.parse(data) as { error?: string };
232
+ return parsed.error ?? data;
233
+ } catch {
234
+ return data;
235
+ }
236
+ }
237
+
238
+ export function createCloudflareBridgeRuntime(
239
+ config: CloudflareBridgeRuntimeConfig
240
+ ): CloudflareBridgeRuntime {
241
+ const baseURL = normalizeBaseURL(config.baseURL);
242
+ const apiPrefix = normalizePrefix(config.apiRoutePrefix);
243
+ const workspaceRoot = normalizeWorkspaceRoot(
244
+ config.workspaceRoot ?? DEFAULT_WORKSPACE_ROOT
245
+ );
246
+ const shell = config.shell ?? DEFAULT_SHELL;
247
+ const fetchImpl = getFetch(config);
248
+ let sandboxIdPromise: Promise<string> | undefined =
249
+ config.sandboxId != null ? Promise.resolve(config.sandboxId) : undefined;
250
+
251
+ function bridgeURL(suffix: string): string {
252
+ return `${baseURL}${apiPrefix}${suffix}`;
253
+ }
254
+
255
+ async function getSandboxId(): Promise<string> {
256
+ if (sandboxIdPromise == null) {
257
+ sandboxIdPromise = (async (): Promise<string> => {
258
+ const response = await fetchImpl(bridgeURL('/sandbox'), {
259
+ method: 'POST',
260
+ headers: createHeaders(config),
261
+ });
262
+ await assertOk(response, 'sandbox create');
263
+ const payload = (await response.json()) as { id?: string };
264
+ if (typeof payload.id !== 'string' || payload.id === '') {
265
+ throw new Error(
266
+ 'Cloudflare sandbox bridge create did not return an id.'
267
+ );
268
+ }
269
+ return payload.id;
270
+ })().catch((error: unknown) => {
271
+ sandboxIdPromise = undefined;
272
+ throw error;
273
+ });
274
+ }
275
+ return sandboxIdPromise;
276
+ }
277
+
278
+ async function exec(
279
+ command: string,
280
+ options: t.CloudflareSandboxExecOptions = {}
281
+ ): Promise<t.CloudflareSandboxExecResult> {
282
+ const sandboxId = await getSandboxId();
283
+ const sandboxPathId = encodeURIComponent(sandboxId);
284
+ const response = await fetchImpl(
285
+ bridgeURL(`/sandbox/${sandboxPathId}/exec`),
286
+ {
287
+ method: 'POST',
288
+ headers: createHeaders(config, {
289
+ 'Content-Type': 'application/json',
290
+ }),
291
+ body: JSON.stringify({
292
+ argv: [shell, '-lc', commandWithEnv(command, options.env)],
293
+ cwd: toSandboxPath(options.cwd ?? workspaceRoot, workspaceRoot),
294
+ timeout_ms: options.timeout,
295
+ }),
296
+ signal: options.signal,
297
+ }
298
+ );
299
+ await assertOk(response, 'exec');
300
+ if (response.body == null) {
301
+ throw new Error(
302
+ 'Cloudflare sandbox bridge exec response did not include a body.'
303
+ );
304
+ }
305
+
306
+ const decoder = new TextDecoder();
307
+ const reader = response.body.getReader();
308
+ let buffer = '';
309
+ let stdout = '';
310
+ let stderr = '';
311
+ let exitCode: number | undefined;
312
+
313
+ try {
314
+ for (;;) {
315
+ const { done, value } = await reader.read();
316
+ if (done) break;
317
+ buffer += decoder.decode(value, { stream: true });
318
+ const parsed = parseSSEChunk(buffer);
319
+ buffer = parsed.remainder;
320
+ for (const event of parsed.events) {
321
+ if (event.event === 'stdout') {
322
+ const decoded = decodeBase64(event.data);
323
+ stdout += decoded;
324
+ options.onOutput?.('stdout', decoded);
325
+ } else if (event.event === 'stderr') {
326
+ const decoded = decodeBase64(event.data);
327
+ stderr += decoded;
328
+ options.onOutput?.('stderr', decoded);
329
+ } else if (event.event === 'exit') {
330
+ exitCode = parseExitCode(event.data);
331
+ } else if (event.event === 'error') {
332
+ throw new Error(parseBridgeError(event.data));
333
+ }
334
+ }
335
+ }
336
+ buffer += decoder.decode();
337
+ const parsed = parseSSEChunk(buffer);
338
+ buffer = parsed.remainder;
339
+ for (const event of parsed.events) {
340
+ if (event.event === 'stdout') {
341
+ const decoded = decodeBase64(event.data);
342
+ stdout += decoded;
343
+ options.onOutput?.('stdout', decoded);
344
+ } else if (event.event === 'stderr') {
345
+ const decoded = decodeBase64(event.data);
346
+ stderr += decoded;
347
+ options.onOutput?.('stderr', decoded);
348
+ } else if (event.event === 'exit') {
349
+ exitCode = parseExitCode(event.data);
350
+ } else if (event.event === 'error') {
351
+ throw new Error(parseBridgeError(event.data));
352
+ }
353
+ }
354
+ } finally {
355
+ reader.releaseLock();
356
+ }
357
+
358
+ if (exitCode == null) {
359
+ throw new Error(
360
+ 'Cloudflare sandbox bridge exec stream closed before an exit event.'
361
+ );
362
+ }
363
+
364
+ return {
365
+ success: exitCode === 0,
366
+ exitCode,
367
+ stdout,
368
+ stderr,
369
+ command,
370
+ };
371
+ }
372
+
373
+ async function readFile(filePath: string): Promise<Buffer> {
374
+ const sandboxId = await getSandboxId();
375
+ const sandboxPathId = encodeURIComponent(sandboxId);
376
+ const resolvedPath = toSandboxPath(filePath, workspaceRoot);
377
+ const response = await fetchImpl(
378
+ bridgeURL(
379
+ `/sandbox/${sandboxPathId}/file/${encodeBridgePath(resolvedPath)}`
380
+ ),
381
+ {
382
+ headers: createHeaders(config),
383
+ }
384
+ );
385
+ await assertOk(response, 'readFile');
386
+ return Buffer.from(await response.arrayBuffer());
387
+ }
388
+
389
+ async function writeFile(
390
+ filePath: string,
391
+ content: string | ReadableStream<Uint8Array>,
392
+ options?: { encoding?: string }
393
+ ): Promise<unknown> {
394
+ const sandboxId = await getSandboxId();
395
+ const sandboxPathId = encodeURIComponent(sandboxId);
396
+ const resolvedPath = toSandboxPath(filePath, workspaceRoot);
397
+ const body = await normalizeWriteBody(content, options);
398
+ const response = await fetchImpl(
399
+ bridgeURL(
400
+ `/sandbox/${sandboxPathId}/file/${encodeBridgePath(resolvedPath)}`
401
+ ),
402
+ {
403
+ method: 'PUT',
404
+ headers: createHeaders(config, {
405
+ 'Content-Type': 'application/octet-stream',
406
+ }),
407
+ body,
408
+ }
409
+ );
410
+ await assertOk(response, 'writeFile');
411
+ return response.json().catch(() => ({ ok: true }));
412
+ }
413
+
414
+ async function mkdir(
415
+ filePath: string,
416
+ options?: { recursive?: boolean }
417
+ ): Promise<unknown> {
418
+ const resolvedPath = toSandboxPath(filePath, workspaceRoot);
419
+ const command = `${options?.recursive === true ? 'mkdir -p' : 'mkdir'} -- ${quote(resolvedPath)}`;
420
+ const result = await exec(command, { cwd: workspaceRoot });
421
+ if (result.exitCode !== 0) {
422
+ throw new Error(result.stderr || `mkdir failed for ${resolvedPath}`);
423
+ }
424
+ return { ok: true };
425
+ }
426
+
427
+ async function listFiles(
428
+ filePath: string,
429
+ options?: t.CloudflareSandboxListFilesOptions
430
+ ): Promise<t.CloudflareSandboxFileInfo[]> {
431
+ const resolvedPath = toSandboxPath(filePath, workspaceRoot);
432
+ const maxDepth = options?.recursive === true ? '' : '-maxdepth 1';
433
+ const hiddenFilter =
434
+ options?.includeHidden === true ? '' : ' \\( -name \'.*\' -prune \\) -o';
435
+ const quotedPath = quote(resolvedPath);
436
+ const command =
437
+ `[ -d ${quotedPath} ] || { printf '%s is not a directory\\n' ${quotedPath} >&2; exit 20; }; ` +
438
+ `find ${quotedPath} -mindepth 1 ${maxDepth}${hiddenFilter} ` +
439
+ '-printf \'%y\\t%s\\t%p\\n\'';
440
+ const result = await exec(command, { cwd: workspaceRoot });
441
+ if (result.exitCode !== 0) {
442
+ throw new Error(result.stderr || `listFiles failed for ${resolvedPath}`);
443
+ }
444
+ return result.stdout
445
+ .split('\n')
446
+ .filter(Boolean)
447
+ .map((line) => {
448
+ const [rawType, rawSize, ...pathParts] = line.split('\t');
449
+ const absolutePath = pathParts.join('\t');
450
+ return {
451
+ name: path.basename(absolutePath),
452
+ absolutePath,
453
+ relativePath: path.relative(resolvedPath, absolutePath),
454
+ type: typeFromFind(rawType),
455
+ size: Number.parseInt(rawSize, 10) || 0,
456
+ };
457
+ });
458
+ }
459
+
460
+ async function deleteFile(filePath: string): Promise<unknown> {
461
+ const resolvedPath = toSandboxPath(filePath, workspaceRoot);
462
+ const result = await exec(`rm -rf -- ${quote(resolvedPath)}`, {
463
+ cwd: workspaceRoot,
464
+ });
465
+ if (result.exitCode !== 0) {
466
+ throw new Error(result.stderr || `deleteFile failed for ${resolvedPath}`);
467
+ }
468
+ return { ok: true };
469
+ }
470
+
471
+ return {
472
+ getSandboxId,
473
+ exec,
474
+ readFile,
475
+ writeFile,
476
+ mkdir,
477
+ listFiles,
478
+ deleteFile,
479
+ };
480
+ }