@liquidmetal-ai/precip 1.0.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 (78) hide show
  1. package/.prettierrc +9 -0
  2. package/CHANGELOG.md +8 -0
  3. package/eslint.config.mjs +28 -0
  4. package/package.json +53 -0
  5. package/src/engine/agent.ts +478 -0
  6. package/src/engine/llm-provider.test.ts +275 -0
  7. package/src/engine/llm-provider.ts +330 -0
  8. package/src/engine/stream-parser.ts +170 -0
  9. package/src/index.ts +142 -0
  10. package/src/mounts/mount-manager.test.ts +516 -0
  11. package/src/mounts/mount-manager.ts +327 -0
  12. package/src/mounts/mount-registry.ts +196 -0
  13. package/src/mounts/zod-to-string.test.ts +154 -0
  14. package/src/mounts/zod-to-string.ts +213 -0
  15. package/src/presets/agent-tools.ts +57 -0
  16. package/src/presets/index.ts +5 -0
  17. package/src/sandbox/README.md +1321 -0
  18. package/src/sandbox/bridges/README.md +571 -0
  19. package/src/sandbox/bridges/actor.test.ts +229 -0
  20. package/src/sandbox/bridges/actor.ts +195 -0
  21. package/src/sandbox/bridges/bridge-fixes.test.ts +614 -0
  22. package/src/sandbox/bridges/bucket.test.ts +300 -0
  23. package/src/sandbox/bridges/cleanup-reproduction.test.ts +225 -0
  24. package/src/sandbox/bridges/console-multiple.test.ts +187 -0
  25. package/src/sandbox/bridges/console.test.ts +157 -0
  26. package/src/sandbox/bridges/console.ts +122 -0
  27. package/src/sandbox/bridges/fetch.ts +93 -0
  28. package/src/sandbox/bridges/index.ts +78 -0
  29. package/src/sandbox/bridges/readable-stream.ts +323 -0
  30. package/src/sandbox/bridges/response.test.ts +154 -0
  31. package/src/sandbox/bridges/response.ts +123 -0
  32. package/src/sandbox/bridges/review-fixes.test.ts +331 -0
  33. package/src/sandbox/bridges/search.test.ts +475 -0
  34. package/src/sandbox/bridges/search.ts +264 -0
  35. package/src/sandbox/bridges/shared/body-methods.ts +93 -0
  36. package/src/sandbox/bridges/shared/cleanup.ts +112 -0
  37. package/src/sandbox/bridges/shared/convert.ts +76 -0
  38. package/src/sandbox/bridges/shared/headers.ts +181 -0
  39. package/src/sandbox/bridges/shared/index.ts +36 -0
  40. package/src/sandbox/bridges/shared/json-helpers.ts +77 -0
  41. package/src/sandbox/bridges/shared/path-parser.ts +109 -0
  42. package/src/sandbox/bridges/shared/promise-helper.ts +108 -0
  43. package/src/sandbox/bridges/shared/registry-setup.ts +84 -0
  44. package/src/sandbox/bridges/shared/response-object.ts +280 -0
  45. package/src/sandbox/bridges/shared/result-builder.ts +130 -0
  46. package/src/sandbox/bridges/shared/scope-helpers.ts +44 -0
  47. package/src/sandbox/bridges/shared/stream-reader.ts +90 -0
  48. package/src/sandbox/bridges/storage-bridge.test.ts +893 -0
  49. package/src/sandbox/bridges/storage.ts +421 -0
  50. package/src/sandbox/bridges/text-decoder.ts +190 -0
  51. package/src/sandbox/bridges/text-encoder.ts +102 -0
  52. package/src/sandbox/bridges/types.ts +39 -0
  53. package/src/sandbox/bridges/utils.ts +123 -0
  54. package/src/sandbox/index.ts +6 -0
  55. package/src/sandbox/quickjs-wasm.d.ts +9 -0
  56. package/src/sandbox/sandbox.test.ts +191 -0
  57. package/src/sandbox/sandbox.ts +831 -0
  58. package/src/sandbox/test-helper.ts +43 -0
  59. package/src/sandbox/test-mocks.ts +154 -0
  60. package/src/sandbox/user-stream.test.ts +77 -0
  61. package/src/skills/frontmatter.test.ts +305 -0
  62. package/src/skills/frontmatter.ts +200 -0
  63. package/src/skills/index.ts +9 -0
  64. package/src/skills/skills-loader.test.ts +237 -0
  65. package/src/skills/skills-loader.ts +200 -0
  66. package/src/tools/actor-storage-tools.ts +250 -0
  67. package/src/tools/code-tools.test.ts +199 -0
  68. package/src/tools/code-tools.ts +444 -0
  69. package/src/tools/file-tools.ts +206 -0
  70. package/src/tools/registry.ts +125 -0
  71. package/src/tools/script-tools.ts +145 -0
  72. package/src/tools/smartbucket-tools.ts +203 -0
  73. package/src/tools/sql-tools.ts +213 -0
  74. package/src/tools/tool-factory.ts +119 -0
  75. package/src/types.ts +512 -0
  76. package/tsconfig.eslint.json +5 -0
  77. package/tsconfig.json +15 -0
  78. package/vitest.config.ts +33 -0
@@ -0,0 +1,444 @@
1
+ /**
2
+ * Code Execution Tool - Run JavaScript in a sandboxed environment
3
+ * All mounts are automatically available via path-based API
4
+ */
5
+
6
+ import type { Tool, ToolContext, MountInfo, MountProvider, SandboxGlobals } from '../types.js';
7
+ import { sandboxAsync } from '../types.js';
8
+ import type {
9
+ Bucket,
10
+ SmartBucket,
11
+ KvCache,
12
+ SqlDatabase,
13
+ ActorStorage
14
+ } from '@liquidmetal-ai/raindrop-framework';
15
+ import { Sandbox } from '../sandbox/index.js';
16
+ import { installStorage, type StorageMountInfo } from '../sandbox/bridges/storage.js';
17
+ import { installSearch, type SearchMountInfo } from '../sandbox/bridges/search.js';
18
+ import { installActor } from '../sandbox/bridges/actor.js';
19
+
20
+ /**
21
+ * Create bridge installers for all mounted resources.
22
+ * Shared by code-tools and script-tools to avoid duplication.
23
+ */
24
+ export function createBridgeInstallers(
25
+ mountManager: MountProvider
26
+ ): Array<(ctx: any) => void> {
27
+ const bridgeInstallers: Array<(ctx: any) => void> = [];
28
+
29
+ const bucketMounts = mountManager.getMountsByType('bucket');
30
+ const smartbucketMounts = mountManager.getMountsByType('smartbucket');
31
+ const kvMounts = mountManager.getMountsByType('kv');
32
+ const actorStorageMounts = mountManager.getMountsByType('actor-storage');
33
+ const allStorageMounts = [
34
+ ...bucketMounts,
35
+ ...smartbucketMounts,
36
+ ...kvMounts,
37
+ ...actorStorageMounts
38
+ ];
39
+
40
+ if (allStorageMounts.length > 0) {
41
+ const storageMountMap = new Map<string, StorageMountInfo>(
42
+ allStorageMounts.map(m => [
43
+ m.name,
44
+ {
45
+ name: m.name,
46
+ type: m.type as 'bucket' | 'smartbucket' | 'kv' | 'actor-storage',
47
+ resource: m.resource as Bucket | KvCache | ActorStorage,
48
+ mode: m.mode
49
+ }
50
+ ])
51
+ );
52
+ bridgeInstallers.push(ctx => installStorage(ctx, storageMountMap));
53
+ }
54
+
55
+ if (smartbucketMounts.length > 0) {
56
+ const searchMountMap = new Map<string, SearchMountInfo>(
57
+ smartbucketMounts.map(m => [
58
+ m.name,
59
+ {
60
+ name: m.name,
61
+ smartbucket: m.resource as SmartBucket
62
+ }
63
+ ])
64
+ );
65
+ bridgeInstallers.push(ctx => installSearch(ctx, searchMountMap));
66
+ }
67
+
68
+ const actorMounts = mountManager.getActorMounts();
69
+ if (actorMounts.size > 0) {
70
+ bridgeInstallers.push(ctx => installActor(ctx, actorMounts));
71
+ }
72
+
73
+ return bridgeInstallers;
74
+ }
75
+ /**
76
+ * Options for creating a customized code execution tool
77
+ */
78
+ export interface CodeToolOptions {
79
+ /**
80
+ * Custom functions/objects to expose in the sandbox.
81
+ * Can be a static object or a factory function that receives ToolContext.
82
+ * Values must be typed sandbox globals (use sandboxAsync, sandboxSync, sandboxObject helpers).
83
+ *
84
+ * @example
85
+ * // Static bridge
86
+ * bridge: {
87
+ * myApi: sandboxObject({
88
+ * getData: sandboxAsync(async (key) => storage.get(key)),
89
+ * setData: sandboxAsync(async (key, val) => storage.set(key, val))
90
+ * })
91
+ * }
92
+ *
93
+ * @example
94
+ * // Dynamic bridge with access to context
95
+ * bridge: (ctx) => ({
96
+ * rpc: sandboxObject({
97
+ * call: sandboxAsync(async (method, params) => client.call(method, params))
98
+ * })
99
+ * })
100
+ */
101
+ bridge?: SandboxGlobals | ((ctx: ToolContext) => SandboxGlobals);
102
+
103
+ /**
104
+ * Custom tool name (default: 'run_code')
105
+ */
106
+ name?: string;
107
+
108
+ /**
109
+ * Additional description to append to the base description.
110
+ * Use this to document your custom bridge functions.
111
+ */
112
+ descriptionSuffix?: string;
113
+ }
114
+
115
+ /**
116
+ * Base description for the code execution tool
117
+ */
118
+ const BASE_DESCRIPTION = `Execute JavaScript code in a secure sandbox with async/await support.
119
+
120
+ IMPORTANT RULES:
121
+ - Write plain top-level code. Do NOT wrap in an IIFE or async function wrapper.
122
+ - Do NOT use require() or import statements. All APIs below are pre-installed globals.
123
+ - fetch, console, TextEncoder, TextDecoder, and Response are all available as globals.
124
+ - Use \`return\` to produce a result value. Use console.log() for intermediate output.
125
+ - await is supported at the top level — just use it directly.
126
+
127
+ All storage is accessed via paths: /<mount-name>/path/to/resource
128
+
129
+ STORAGE API:
130
+ • read(path) - Read content from any mount
131
+ Returns: Response-like object with .text(), .json(), .arrayBuffer() methods
132
+ For files: also has .size, .uploaded, .body (ReadableStream)
133
+ For KV: also has .metadata (if set)
134
+ Example: const data = await (await read("/uploads/config.json")).json();
135
+ const text = await (await read("/cache/user:123")).text();
136
+ const state = await (await read("/state/counter")).json();
137
+
138
+ • write(path, content, options?) - Write content to any mount
139
+ Returns: { success: boolean }
140
+ Options vary by mount type:
141
+ KV cache: { ttl: 300 } or { expiration: 1720000000 } or { metadata: {...} }
142
+ File storage: { contentType: "application/json" } or { customMetadata: { author: "alice" } }
143
+ Actor state: (no options)
144
+ Example: await write("/uploads/output.txt", "Hello world");
145
+ await write("/cache/session:abc", JSON.stringify(data), { ttl: 300 });
146
+ await write("/state/prefs", JSON.stringify({ theme: "dark", lang: "en" }));
147
+
148
+ • list(path, options?) - List files or keys
149
+ Returns: { files: Array<{key, size?, uploaded?, expiration?}>, count: number }
150
+ Options vary by mount type:
151
+ Common: { limit: 100 }
152
+ File storage: { delimiter: "/", startAfter: "key" }
153
+ Actor state: { start: "a", end: "z", reverse: true, limit: 50 }
154
+ Example: const result = await list("/uploads/images/");
155
+ const entries = await list("/state/", { prefix: "user:", reverse: true });
156
+
157
+ • remove(path) - Delete a file or key
158
+ Returns: { success: boolean }
159
+ Example: await remove("/uploads/old-file.txt");
160
+ await remove("/cache/expired-key");
161
+
162
+ DATABASE API:
163
+ • query(path, sql, params?) - Execute SELECT query
164
+ Returns: { results: Array<object>, rowCount: number }
165
+ Example: const data = await query("/analytics", "SELECT * FROM users LIMIT 10");
166
+
167
+ • execute(path, sql, params?) - Execute INSERT/UPDATE/DELETE
168
+ Returns: { success: boolean, changes: number }
169
+ Example: await execute("/analytics", "INSERT INTO logs (msg) VALUES (?)", ["hello"]);
170
+
171
+ SEARCH API (for searchable storage):
172
+ • search(path, query) - Semantic search with pagination
173
+ Returns: { results, hasMore, nextPage(), total, page, pageSize }
174
+
175
+ Simple usage:
176
+ const results = await search("/knowledge", "machine learning");
177
+ for (const r of results.results) {
178
+ console.log(r.text, r.score);
179
+ }
180
+
181
+ With pagination:
182
+ let results = await search("/knowledge", "query");
183
+ while (results.hasMore) {
184
+ results = await results.nextPage();
185
+ // process results.results
186
+ }
187
+
188
+ Auto-pagination with for-await:
189
+ const results = await search("/knowledge", "query");
190
+ for await (const r of results) {
191
+ console.log(r.text); // automatically fetches next pages
192
+ }
193
+
194
+ • chunkSearch(path, query) - RAG chunk search for text passages
195
+ Returns: { results: Array<{text, source, score, chunkSignature}> }
196
+ Example: const results = await chunkSearch("/knowledge", "how to configure");
197
+
198
+ ACTOR API:
199
+ • actor(path, instanceId) - Get an actor stub for RPC calls
200
+ Returns: Actor stub with callable methods
201
+ Example: const session = await actor("/sessions", "user-123");
202
+ const state = await session.getState();
203
+ await session.updatePrefs({ theme: "dark" });
204
+
205
+ FETCH API:
206
+ • fetch(url, options?) - Make HTTP requests
207
+ Returns: Response object with json(), text(), arrayBuffer() methods
208
+ Example: const res = await fetch("https://api.example.com/data");
209
+ const data = await res.json();
210
+
211
+ CONSOLE:
212
+ • console.log(...) - Log output (shown in [stdout] section)
213
+ • console.warn(...), console.error(...) - Log with level prefix
214
+
215
+ UTILITIES:
216
+ • new TextEncoder() / new TextDecoder() - Encode/decode text
217
+ • new Response(body, init?) - Create Response objects
218
+
219
+ TIPS:
220
+ - All paths start with / followed by the mount name
221
+ - Always use await with async operations
222
+ - Always return a value so results are visible
223
+ - Use console.log() for debugging
224
+ - Do NOT wrap code in (async () => { })() — await works at the top level
225
+ - Do NOT use require() or import — all APIs are globals`;
226
+
227
+ /**
228
+ * Create a code execution tool with optional custom bridges
229
+ *
230
+ * @example
231
+ * // Basic usage - same as CodeExecutionTool
232
+ * const tool = createCodeExecutionTool();
233
+ *
234
+ * @example
235
+ * // With static bridge
236
+ * const tool = createCodeExecutionTool({
237
+ * bridge: {
238
+ * myApi: {
239
+ * getData: async (key) => storage.get(key)
240
+ * }
241
+ * }
242
+ * });
243
+ *
244
+ * @example
245
+ * // With dynamic bridge (has access to ToolContext)
246
+ * const tool = createCodeExecutionTool({
247
+ * bridge: (ctx) => ({
248
+ * rpc: createConnectClient(ctx.env.RPC_BINDING)
249
+ * }),
250
+ * descriptionSuffix: `
251
+ *
252
+ * RPC API:
253
+ * • rpc.users.getUser({ id }) - Get user by ID
254
+ * • rpc.users.createUser({ name, email }) - Create new user`
255
+ * });
256
+ */
257
+ export function createCodeExecutionTool(options: CodeToolOptions = {}): Tool {
258
+ const description = options.descriptionSuffix
259
+ ? BASE_DESCRIPTION + options.descriptionSuffix
260
+ : BASE_DESCRIPTION;
261
+
262
+ return {
263
+ definition: {
264
+ name: options.name || 'run_code',
265
+ description,
266
+ parameters: {
267
+ code: {
268
+ type: 'string',
269
+ description: 'JavaScript code to execute. Must use async/await for I/O operations.',
270
+ required: true
271
+ },
272
+ timeout: {
273
+ type: 'number',
274
+ description: 'Execution timeout in milliseconds (default: 30000)',
275
+ required: false
276
+ }
277
+ }
278
+ },
279
+
280
+ async execute(params: { code: string; timeout?: number }, context: ToolContext) {
281
+ const { mounts, logger } = context;
282
+ const mountManager = mounts;
283
+
284
+ // Resolve custom bridge - static object or dynamic factory
285
+ const customBridge =
286
+ typeof options.bridge === 'function' ? options.bridge(context) : options.bridge || {};
287
+
288
+ // Create mount globals (database functions only - storage and search handled by bridges)
289
+ const mountGlobals = createMountGlobals(mountManager);
290
+
291
+ // Merge mount globals with custom bridge (custom bridge takes precedence)
292
+ const asyncGlobals = { ...mountGlobals, ...customBridge };
293
+
294
+ // Create bridge installers for all mounted resources
295
+ const bridgeInstallers = createBridgeInstallers(mountManager);
296
+
297
+ // Execute in sandbox
298
+ const sandbox = await Sandbox.getInstance();
299
+ const result = await sandbox.execute(params.code, asyncGlobals, {
300
+ timeoutMs: params.timeout || 30000,
301
+ memoryLimitBytes: 64 * 1024 * 1024,
302
+ logger,
303
+ bridgeInstallers
304
+ });
305
+
306
+ // Format output to include console.log and result
307
+ return formatToolOutput(result);
308
+ }
309
+ };
310
+ }
311
+
312
+ /**
313
+ * Default code execution tool (backward compatible)
314
+ *
315
+ * Execute JavaScript code with access to all mounted resources
316
+ *
317
+ * Example: run_code(`
318
+ * const files = await bucket.list("@user-data/");
319
+ * return files.length;
320
+ * `)
321
+ */
322
+ export const CodeExecutionTool: Tool = createCodeExecutionTool();
323
+
324
+ /**
325
+ * Format tool output with console.log and result sections
326
+ * Returns a consistent string format for OpenAI chat completions
327
+ */
328
+ export function formatToolOutput(result: any): any {
329
+ const hasConsoleOutput = result.consoleOutput && result.consoleOutput.length > 0;
330
+ const hasError = !result.success && result.error;
331
+ // Only show result section if there's a meaningful result (not undefined)
332
+ const hasResult = result.success && 'result' in result && result.result !== undefined;
333
+
334
+ if (!hasConsoleOutput && !hasResult && !hasError) {
335
+ // No output at all - return empty
336
+ return { output: '(no output)' };
337
+ }
338
+
339
+ const lines: string[] = [];
340
+
341
+ if (hasError) {
342
+ lines.push('[error]');
343
+ lines.push(result.error);
344
+ lines.push('');
345
+ }
346
+
347
+ if (hasConsoleOutput) {
348
+ lines.push('[stdout]');
349
+ lines.push(...result.consoleOutput);
350
+ lines.push('');
351
+ }
352
+
353
+ if (hasResult) {
354
+ lines.push('[result]');
355
+ const MAX_OUTPUT_LEN = 50000;
356
+ const resultStr =
357
+ typeof result.result === 'object'
358
+ ? (() => {
359
+ try {
360
+ const json = JSON.stringify(result.result, null, 2);
361
+ return json.length > MAX_OUTPUT_LEN
362
+ ? json.substring(0, MAX_OUTPUT_LEN) + '\n...(truncated)'
363
+ : json;
364
+ } catch {
365
+ return String(result.result);
366
+ }
367
+ })()
368
+ : String(result.result).substring(0, MAX_OUTPUT_LEN);
369
+ lines.push(resultStr);
370
+ }
371
+
372
+ return {
373
+ output: lines.join('\n')
374
+ };
375
+ }
376
+
377
+ /**
378
+ * Create sandbox globals from mount manager
379
+ * Returns database and search functions (storage is handled by the storage bridge)
380
+ */
381
+ export function createMountGlobals(mountManager: MountProvider): SandboxGlobals {
382
+ const globals: SandboxGlobals = {};
383
+
384
+ // Helper to parse path and get mount
385
+ const resolvePath = (path: string): { mount: MountInfo; resourcePath: string } => {
386
+ const parsed = mountManager.parsePath(path);
387
+ const mount = mountManager.getMount(parsed.mountName);
388
+ if (!mount) {
389
+ const available = Array.from(mountManager.getAllMounts().keys()).join(', ');
390
+ throw new Error(`Mount not found: ${parsed.mountName}. Available: ${available}`);
391
+ }
392
+ return { mount, resourcePath: parsed.path };
393
+ };
394
+
395
+ // query(path, sql, params?) - Execute SELECT query
396
+ globals.query = sandboxAsync(async (path: string, sql: string, params?: any[]) => {
397
+ const { mount } = resolvePath(path);
398
+
399
+ if (mount.type !== 'database') {
400
+ throw new Error(`Not a database mount: ${path}`);
401
+ }
402
+
403
+ const db = mount.resource as SqlDatabase;
404
+ const stmt = params ? db.prepare(sql).bind(...params) : db.prepare(sql);
405
+ const result = await stmt.all();
406
+
407
+ return {
408
+ results: result.results,
409
+ rowCount: result.results?.length || 0
410
+ };
411
+ });
412
+
413
+ // execute(path, sql, params?) - Execute INSERT/UPDATE/DELETE
414
+ globals.execute = sandboxAsync(async (path: string, sql: string, params?: any[]) => {
415
+ const { mount } = resolvePath(path);
416
+
417
+ if (mount.type !== 'database') {
418
+ throw new Error(`Not a database mount: ${path}`);
419
+ }
420
+
421
+ // Check if mount allows writes (default is read-only)
422
+ if (mount.mode !== 'rw') {
423
+ throw new Error(`Cannot execute write operations on read-only mount: ${path}`);
424
+ }
425
+
426
+ const db = mount.resource as SqlDatabase;
427
+ const stmt = params ? db.prepare(sql).bind(...params) : db.prepare(sql);
428
+ const result = await stmt.run();
429
+
430
+ return {
431
+ success: result.success,
432
+ changes: result.meta?.changes || 0
433
+ };
434
+ });
435
+
436
+ // Note: search and chunkSearch are handled by the search bridge
437
+
438
+ return globals;
439
+ }
440
+
441
+ /**
442
+ * Export code tool
443
+ */
444
+ export const CodeTools: Tool[] = [CodeExecutionTool];
@@ -0,0 +1,206 @@
1
+ /**
2
+ * File Tools - Read, write, list, delete files from Buckets and SmartBuckets
3
+ * Mount-aware: operates on multiple buckets via path prefixes
4
+ * Supports both Bucket and SmartBucket (file operations only - use SmartBucket tools for AI search)
5
+ */
6
+
7
+ import type { Tool } from '../types.js';
8
+ import type { Bucket } from '@liquidmetal-ai/raindrop-framework';
9
+ import { createPathBasedTool } from './tool-factory.js';
10
+
11
+ /**
12
+ * Read a file from a mounted bucket or SmartBucket
13
+ *
14
+ * Example: read_file("/uploads/profile.json")
15
+ */
16
+ export const ReadFileTool: Tool = createPathBasedTool<{ path: string }, any>({
17
+ name: 'read_file',
18
+ description: `Read a file from storage and return its contents.
19
+
20
+ Path format: /mount-name/path/to/file.ext
21
+
22
+ Works with both Buckets and SmartBuckets. For AI-powered search on SmartBuckets, use semantic_search or chunk_search tools.
23
+
24
+ Returns: {path, content, size, uploaded, contentType}
25
+
26
+ Example: read_file("/uploads/profile.json")`,
27
+ parameters: {
28
+ path: {
29
+ type: 'string',
30
+ description: 'File path starting with /, e.g., /uploads/profile.json',
31
+ required: true
32
+ }
33
+ },
34
+ allowedMountTypes: ['bucket', 'smartbucket'],
35
+ mode: 'read',
36
+
37
+ async executor(mount, parsed, params) {
38
+ const bucket = mount.resource as Bucket;
39
+ const obj = await bucket.get(parsed.path);
40
+
41
+ if (!obj) {
42
+ throw new Error(`File not found: ${params.path}`);
43
+ }
44
+
45
+ const content = await obj.text();
46
+
47
+ return {
48
+ path: params.path,
49
+ content,
50
+ size: obj.size,
51
+ uploaded: obj.uploaded.toISOString(),
52
+ contentType: obj.httpMetadata?.contentType
53
+ };
54
+ }
55
+ });
56
+
57
+ /**
58
+ * Write a file to a mounted bucket or SmartBucket
59
+ *
60
+ * Example: write_file("/outputs/result.json", '{"status": "done"}')
61
+ */
62
+ export const WriteFileTool: Tool = createPathBasedTool<{ path: string; content: string; contentType?: string }, any>({
63
+ name: 'write_file',
64
+ description: `Write content to a file in storage. Creates or overwrites the file.
65
+
66
+ Path format: /mount-name/path/to/file.ext
67
+
68
+ Works with both Buckets and SmartBuckets. For SmartBuckets, uploaded content is automatically indexed for AI search.
69
+ Note: Mount must have mode 'rw' to allow writes.
70
+
71
+ Returns: {path, success, size}
72
+
73
+ Example: write_file("/outputs/result.txt", "Hello World")`,
74
+ parameters: {
75
+ path: {
76
+ type: 'string',
77
+ description: 'File path starting with /, e.g., /outputs/result.txt',
78
+ required: true
79
+ },
80
+ content: {
81
+ type: 'string',
82
+ description: 'Content to write to the file',
83
+ required: true
84
+ },
85
+ contentType: {
86
+ type: 'string',
87
+ description: 'Optional content type, e.g., application/json, text/plain',
88
+ required: false
89
+ }
90
+ },
91
+ allowedMountTypes: ['bucket', 'smartbucket'],
92
+ mode: 'write',
93
+
94
+ async executor(mount, parsed, params) {
95
+ const bucket = mount.resource as Bucket;
96
+
97
+ await bucket.put(parsed.path, params.content, {
98
+ httpMetadata: params.contentType
99
+ ? {
100
+ contentType: params.contentType
101
+ }
102
+ : undefined
103
+ });
104
+
105
+ return {
106
+ path: params.path,
107
+ success: true,
108
+ size: params.content.length
109
+ };
110
+ }
111
+ });
112
+
113
+ /**
114
+ * List files in a mounted bucket or SmartBucket
115
+ *
116
+ * Example: list_files("/uploads/") or list_files("/logs/2024/")
117
+ */
118
+ export const ListFilesTool: Tool = createPathBasedTool<{ path: string; limit?: number }, any>({
119
+ name: 'list_files',
120
+ description: `List files in storage with optional prefix filtering.
121
+
122
+ Path format: /mount-name/ or /mount-name/prefix/
123
+
124
+ Works with both Buckets and SmartBuckets.
125
+
126
+ Returns: {files: [{path, size, uploaded}], truncated, count}
127
+
128
+ Example: list_files("/uploads/")
129
+ Example with prefix: list_files("/logs/2024/")`,
130
+ parameters: {
131
+ path: {
132
+ type: 'string',
133
+ description: 'Mount path with optional prefix, e.g., /uploads/ or /logs/2024/',
134
+ required: true
135
+ },
136
+ limit: {
137
+ type: 'number',
138
+ description: 'Maximum number of files to return (default: 1000)',
139
+ required: false
140
+ }
141
+ },
142
+ allowedMountTypes: ['bucket', 'smartbucket'],
143
+ mode: 'read',
144
+
145
+ async executor(mount, parsed, params) {
146
+ const bucket = mount.resource as Bucket;
147
+ const listed = await bucket.list({
148
+ prefix: parsed.path || undefined,
149
+ limit: params.limit || 1000
150
+ });
151
+
152
+ return {
153
+ files: listed.objects.map(obj => ({
154
+ path: `/${mount.name}/${obj.key}`,
155
+ size: obj.size,
156
+ uploaded: obj.uploaded.toISOString()
157
+ })),
158
+ truncated: listed.truncated,
159
+ count: listed.objects.length
160
+ };
161
+ }
162
+ });
163
+
164
+ /**
165
+ * Delete a file from a mounted bucket or SmartBucket
166
+ *
167
+ * Example: delete_file("/logs/old-file.txt")
168
+ */
169
+ export const DeleteFileTool: Tool = createPathBasedTool<{ path: string }, any>({
170
+ name: 'delete_file',
171
+ description: `Delete a file from storage.
172
+
173
+ Path format: /mount-name/path/to/file.ext
174
+
175
+ Works with both Buckets and SmartBuckets. Note: Deleting from SmartBucket also removes its indexed content.
176
+ Note: Mount must have mode 'rw' to allow deletes.
177
+
178
+ Returns: {path, success, deleted}
179
+
180
+ Example: delete_file("/logs/old-file.txt")`,
181
+ parameters: {
182
+ path: {
183
+ type: 'string',
184
+ description: 'File path starting with /, e.g., /logs/old-file.txt',
185
+ required: true
186
+ }
187
+ },
188
+ allowedMountTypes: ['bucket', 'smartbucket'],
189
+ mode: 'write',
190
+
191
+ async executor(mount, parsed, params) {
192
+ const bucket = mount.resource as Bucket;
193
+ await bucket.delete(parsed.path);
194
+
195
+ return {
196
+ path: params.path,
197
+ success: true,
198
+ deleted: true
199
+ };
200
+ }
201
+ });
202
+
203
+ /**
204
+ * All file tools
205
+ */
206
+ export const FileTools: Tool[] = [ReadFileTool, WriteFileTool, ListFilesTool, DeleteFileTool];