agentfs-sdk 0.4.0 → 0.4.1

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.
@@ -0,0 +1,120 @@
1
+ /**
2
+ * AgentFS - FileSystem implementation using Cloudflare Durable Objects SQLite
3
+ *
4
+ * This implementation uses Cloudflare's Durable Objects SQLite storage API,
5
+ * allowing AgentFS to run on Cloudflare's edge platform.
6
+ *
7
+ * @see https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/
8
+ */
9
+ import { type Stats, type DirEntry, type FilesystemStats, type FileHandle, type FileSystem } from '../../filesystem/interface.js';
10
+ /**
11
+ * Cloudflare Durable Objects SqlStorage cursor interface
12
+ */
13
+ interface SqlStorageCursor<T = Record<string, unknown>> {
14
+ toArray(): T[];
15
+ one(): T;
16
+ raw(): IterableIterator<unknown[]>;
17
+ readonly columnNames: string[];
18
+ readonly rowsRead: number;
19
+ readonly rowsWritten: number;
20
+ next(): {
21
+ done: boolean;
22
+ value?: T;
23
+ };
24
+ [Symbol.iterator](): IterableIterator<T>;
25
+ }
26
+ /**
27
+ * Cloudflare Durable Objects SqlStorage interface
28
+ */
29
+ interface SqlStorage {
30
+ exec<T = Record<string, unknown>>(query: string, ...bindings: unknown[]): SqlStorageCursor<T>;
31
+ readonly databaseSize: number;
32
+ }
33
+ /**
34
+ * Cloudflare Durable Objects Storage interface (subset we need)
35
+ */
36
+ export interface CloudflareStorage {
37
+ readonly sql: SqlStorage;
38
+ transactionSync<T>(callback: () => T): T;
39
+ }
40
+ /**
41
+ * A filesystem backed by Cloudflare Durable Objects SQLite storage.
42
+ *
43
+ * AgentFS implements the FileSystem interface using Cloudflare's
44
+ * Durable Objects SQLite storage as the backing store.
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * // In a Durable Object class
49
+ * export class MyDurableObject extends DurableObject {
50
+ * private fs: AgentFS;
51
+ *
52
+ * constructor(ctx: DurableObjectState, env: Env) {
53
+ * super(ctx, env);
54
+ * this.fs = AgentFS.create(ctx.storage);
55
+ * }
56
+ *
57
+ * async fetch(request: Request) {
58
+ * await this.fs.writeFile('/hello.txt', 'Hello, World!');
59
+ * const content = await this.fs.readFile('/hello.txt', 'utf8');
60
+ * return new Response(content);
61
+ * }
62
+ * }
63
+ * ```
64
+ */
65
+ export declare class AgentFS implements FileSystem {
66
+ private storage;
67
+ private rootIno;
68
+ private chunkSize;
69
+ private constructor();
70
+ /**
71
+ * Create a AgentFS from a Durable Object storage context.
72
+ *
73
+ * @param storage - The ctx.storage from a Durable Object
74
+ */
75
+ static create(storage: CloudflareStorage): AgentFS;
76
+ getChunkSize(): number;
77
+ private initialize;
78
+ private ensureRoot;
79
+ private normalizePath;
80
+ private splitPath;
81
+ private resolvePath;
82
+ private resolvePathOrThrow;
83
+ private resolveParent;
84
+ private createInode;
85
+ private createDentry;
86
+ private ensureParentDirs;
87
+ private getLinkCount;
88
+ private getInodeMode;
89
+ writeFile(path: string, content: string | Buffer, options?: BufferEncoding | {
90
+ encoding?: BufferEncoding;
91
+ }): Promise<void>;
92
+ private updateFileContent;
93
+ readFile(path: string): Promise<Buffer>;
94
+ readFile(path: string, encoding: BufferEncoding): Promise<string>;
95
+ readFile(path: string, options: {
96
+ encoding: BufferEncoding;
97
+ }): Promise<string>;
98
+ readdir(path: string): Promise<string[]>;
99
+ readdirPlus(path: string): Promise<DirEntry[]>;
100
+ stat(path: string): Promise<Stats>;
101
+ lstat(path: string): Promise<Stats>;
102
+ mkdir(path: string): Promise<void>;
103
+ rmdir(path: string): Promise<void>;
104
+ unlink(path: string): Promise<void>;
105
+ rm(path: string, options?: {
106
+ force?: boolean;
107
+ recursive?: boolean;
108
+ }): Promise<void>;
109
+ private rmDirContentsRecursive;
110
+ private removeDentryAndMaybeInode;
111
+ rename(oldPath: string, newPath: string): Promise<void>;
112
+ copyFile(src: string, dest: string): Promise<void>;
113
+ symlink(target: string, linkpath: string): Promise<void>;
114
+ readlink(path: string): Promise<string>;
115
+ access(path: string): Promise<void>;
116
+ statfs(): Promise<FilesystemStats>;
117
+ open(path: string): Promise<FileHandle>;
118
+ deleteFile(path: string): Promise<void>;
119
+ }
120
+ export {};
@@ -0,0 +1,951 @@
1
+ /**
2
+ * AgentFS - FileSystem implementation using Cloudflare Durable Objects SQLite
3
+ *
4
+ * This implementation uses Cloudflare's Durable Objects SQLite storage API,
5
+ * allowing AgentFS to run on Cloudflare's edge platform.
6
+ *
7
+ * @see https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/
8
+ */
9
+ import { S_IFMT, S_IFDIR, S_IFLNK, DEFAULT_FILE_MODE, DEFAULT_DIR_MODE, createStats, } from '../../filesystem/interface.js';
10
+ const DEFAULT_CHUNK_SIZE = 4096;
11
+ function createFsError(opts) {
12
+ const err = new Error(`${opts.code}: ${opts.message}, ${opts.syscall} '${opts.path}'`);
13
+ err.code = opts.code;
14
+ err.syscall = opts.syscall;
15
+ err.path = opts.path;
16
+ return err;
17
+ }
18
+ /**
19
+ * An open file handle for AgentFS.
20
+ */
21
+ class AgentFSFile {
22
+ storage;
23
+ ino;
24
+ chunkSize;
25
+ constructor(storage, ino, chunkSize) {
26
+ this.storage = storage;
27
+ this.ino = ino;
28
+ this.chunkSize = chunkSize;
29
+ }
30
+ async pread(offset, size) {
31
+ const startChunk = Math.floor(offset / this.chunkSize);
32
+ const endChunk = Math.floor((offset + size - 1) / this.chunkSize);
33
+ const rows = this.storage.sql.exec(`SELECT chunk_index, data FROM fs_data
34
+ WHERE ino = ? AND chunk_index >= ? AND chunk_index <= ?
35
+ ORDER BY chunk_index ASC`, this.ino, startChunk, endChunk).toArray();
36
+ const buffers = [];
37
+ let bytesCollected = 0;
38
+ const startOffsetInChunk = offset % this.chunkSize;
39
+ for (const row of rows) {
40
+ const data = Buffer.from(row.data);
41
+ const skip = buffers.length === 0 ? startOffsetInChunk : 0;
42
+ if (skip >= data.length) {
43
+ continue;
44
+ }
45
+ const remaining = size - bytesCollected;
46
+ const take = Math.min(data.length - skip, remaining);
47
+ buffers.push(data.subarray(skip, skip + take));
48
+ bytesCollected += take;
49
+ }
50
+ if (buffers.length === 0) {
51
+ return Buffer.alloc(0);
52
+ }
53
+ return Buffer.concat(buffers);
54
+ }
55
+ async pwrite(offset, data) {
56
+ if (data.length === 0) {
57
+ return;
58
+ }
59
+ this.storage.transactionSync(() => {
60
+ const sizeRow = this.storage.sql.exec('SELECT size FROM fs_inode WHERE ino = ?', this.ino).toArray()[0];
61
+ const currentSize = sizeRow?.size ?? 0;
62
+ if (offset > currentSize) {
63
+ const zeros = Buffer.alloc(offset - currentSize);
64
+ this.writeDataAtOffset(currentSize, zeros);
65
+ }
66
+ this.writeDataAtOffset(offset, data);
67
+ const newSize = Math.max(currentSize, offset + data.length);
68
+ const now = Math.floor(Date.now() / 1000);
69
+ this.storage.sql.exec('UPDATE fs_inode SET size = ?, mtime = ? WHERE ino = ?', newSize, now, this.ino);
70
+ });
71
+ }
72
+ writeDataAtOffset(offset, data) {
73
+ const startChunk = Math.floor(offset / this.chunkSize);
74
+ const endChunk = Math.floor((offset + data.length - 1) / this.chunkSize);
75
+ for (let chunkIdx = startChunk; chunkIdx <= endChunk; chunkIdx++) {
76
+ const chunkStart = chunkIdx * this.chunkSize;
77
+ const chunkEnd = chunkStart + this.chunkSize;
78
+ const dataStart = Math.max(0, chunkStart - offset);
79
+ const dataEnd = Math.min(data.length, chunkEnd - offset);
80
+ const writeOffset = Math.max(0, offset - chunkStart);
81
+ const existingRows = this.storage.sql.exec('SELECT data FROM fs_data WHERE ino = ? AND chunk_index = ?', this.ino, chunkIdx).toArray();
82
+ let chunkData;
83
+ if (existingRows.length > 0) {
84
+ chunkData = Buffer.from(existingRows[0].data);
85
+ if (writeOffset + (dataEnd - dataStart) > chunkData.length) {
86
+ const newChunk = Buffer.alloc(writeOffset + (dataEnd - dataStart));
87
+ chunkData.copy(newChunk);
88
+ chunkData = newChunk;
89
+ }
90
+ }
91
+ else {
92
+ chunkData = Buffer.alloc(writeOffset + (dataEnd - dataStart));
93
+ }
94
+ data.copy(chunkData, writeOffset, dataStart, dataEnd);
95
+ this.storage.sql.exec(`INSERT INTO fs_data (ino, chunk_index, data) VALUES (?, ?, ?)
96
+ ON CONFLICT(ino, chunk_index) DO UPDATE SET data = excluded.data`, this.ino, chunkIdx, chunkData);
97
+ }
98
+ }
99
+ async truncate(newSize) {
100
+ this.storage.transactionSync(() => {
101
+ const sizeRow = this.storage.sql.exec('SELECT size FROM fs_inode WHERE ino = ?', this.ino).toArray()[0];
102
+ const currentSize = sizeRow?.size ?? 0;
103
+ if (newSize === 0) {
104
+ this.storage.sql.exec('DELETE FROM fs_data WHERE ino = ?', this.ino);
105
+ }
106
+ else if (newSize < currentSize) {
107
+ const lastChunkIdx = Math.floor((newSize - 1) / this.chunkSize);
108
+ this.storage.sql.exec('DELETE FROM fs_data WHERE ino = ? AND chunk_index > ?', this.ino, lastChunkIdx);
109
+ const offsetInChunk = newSize % this.chunkSize;
110
+ if (offsetInChunk > 0) {
111
+ const rows = this.storage.sql.exec('SELECT data FROM fs_data WHERE ino = ? AND chunk_index = ?', this.ino, lastChunkIdx).toArray();
112
+ if (rows.length > 0) {
113
+ const existingData = Buffer.from(rows[0].data);
114
+ if (existingData.length > offsetInChunk) {
115
+ const truncatedChunk = existingData.subarray(0, offsetInChunk);
116
+ this.storage.sql.exec('UPDATE fs_data SET data = ? WHERE ino = ? AND chunk_index = ?', truncatedChunk, this.ino, lastChunkIdx);
117
+ }
118
+ }
119
+ }
120
+ }
121
+ const now = Math.floor(Date.now() / 1000);
122
+ this.storage.sql.exec('UPDATE fs_inode SET size = ?, mtime = ? WHERE ino = ?', newSize, now, this.ino);
123
+ });
124
+ }
125
+ async fsync() {
126
+ // Cloudflare Durable Objects automatically persist data
127
+ // No explicit sync needed
128
+ }
129
+ async fstat() {
130
+ const rows = this.storage.sql.exec(`SELECT ino, mode, nlink, uid, gid, size, atime, mtime, ctime
131
+ FROM fs_inode WHERE ino = ?`, this.ino).toArray();
132
+ if (rows.length === 0) {
133
+ throw new Error('File handle refers to deleted inode');
134
+ }
135
+ return createStats(rows[0]);
136
+ }
137
+ }
138
+ /**
139
+ * A filesystem backed by Cloudflare Durable Objects SQLite storage.
140
+ *
141
+ * AgentFS implements the FileSystem interface using Cloudflare's
142
+ * Durable Objects SQLite storage as the backing store.
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * // In a Durable Object class
147
+ * export class MyDurableObject extends DurableObject {
148
+ * private fs: AgentFS;
149
+ *
150
+ * constructor(ctx: DurableObjectState, env: Env) {
151
+ * super(ctx, env);
152
+ * this.fs = AgentFS.create(ctx.storage);
153
+ * }
154
+ *
155
+ * async fetch(request: Request) {
156
+ * await this.fs.writeFile('/hello.txt', 'Hello, World!');
157
+ * const content = await this.fs.readFile('/hello.txt', 'utf8');
158
+ * return new Response(content);
159
+ * }
160
+ * }
161
+ * ```
162
+ */
163
+ export class AgentFS {
164
+ storage;
165
+ rootIno = 1;
166
+ chunkSize = DEFAULT_CHUNK_SIZE;
167
+ constructor(storage) {
168
+ this.storage = storage;
169
+ }
170
+ /**
171
+ * Create a AgentFS from a Durable Object storage context.
172
+ *
173
+ * @param storage - The ctx.storage from a Durable Object
174
+ */
175
+ static create(storage) {
176
+ const fs = new AgentFS(storage);
177
+ fs.initialize();
178
+ return fs;
179
+ }
180
+ getChunkSize() {
181
+ return this.chunkSize;
182
+ }
183
+ initialize() {
184
+ this.storage.sql.exec(`
185
+ CREATE TABLE IF NOT EXISTS fs_config (
186
+ key TEXT PRIMARY KEY,
187
+ value TEXT NOT NULL
188
+ )
189
+ `);
190
+ this.storage.sql.exec(`
191
+ CREATE TABLE IF NOT EXISTS fs_inode (
192
+ ino INTEGER PRIMARY KEY AUTOINCREMENT,
193
+ mode INTEGER NOT NULL,
194
+ nlink INTEGER NOT NULL DEFAULT 0,
195
+ uid INTEGER NOT NULL DEFAULT 0,
196
+ gid INTEGER NOT NULL DEFAULT 0,
197
+ size INTEGER NOT NULL DEFAULT 0,
198
+ atime INTEGER NOT NULL,
199
+ mtime INTEGER NOT NULL,
200
+ ctime INTEGER NOT NULL
201
+ )
202
+ `);
203
+ this.storage.sql.exec(`
204
+ CREATE TABLE IF NOT EXISTS fs_dentry (
205
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
206
+ name TEXT NOT NULL,
207
+ parent_ino INTEGER NOT NULL,
208
+ ino INTEGER NOT NULL,
209
+ UNIQUE(parent_ino, name)
210
+ )
211
+ `);
212
+ this.storage.sql.exec(`
213
+ CREATE INDEX IF NOT EXISTS idx_fs_dentry_parent
214
+ ON fs_dentry(parent_ino, name)
215
+ `);
216
+ this.storage.sql.exec(`
217
+ CREATE TABLE IF NOT EXISTS fs_data (
218
+ ino INTEGER NOT NULL,
219
+ chunk_index INTEGER NOT NULL,
220
+ data BLOB NOT NULL,
221
+ PRIMARY KEY (ino, chunk_index)
222
+ )
223
+ `);
224
+ this.storage.sql.exec(`
225
+ CREATE TABLE IF NOT EXISTS fs_symlink (
226
+ ino INTEGER PRIMARY KEY,
227
+ target TEXT NOT NULL
228
+ )
229
+ `);
230
+ this.chunkSize = this.ensureRoot();
231
+ }
232
+ ensureRoot() {
233
+ const configRows = this.storage.sql.exec("SELECT value FROM fs_config WHERE key = 'chunk_size'").toArray();
234
+ let chunkSize;
235
+ if (configRows.length === 0) {
236
+ this.storage.sql.exec("INSERT INTO fs_config (key, value) VALUES ('chunk_size', ?)", DEFAULT_CHUNK_SIZE.toString());
237
+ chunkSize = DEFAULT_CHUNK_SIZE;
238
+ }
239
+ else {
240
+ chunkSize = parseInt(configRows[0].value, 10) || DEFAULT_CHUNK_SIZE;
241
+ }
242
+ const rootRows = this.storage.sql.exec('SELECT ino FROM fs_inode WHERE ino = ?', this.rootIno).toArray();
243
+ if (rootRows.length === 0) {
244
+ const now = Math.floor(Date.now() / 1000);
245
+ this.storage.sql.exec(`INSERT INTO fs_inode (ino, mode, nlink, uid, gid, size, atime, mtime, ctime)
246
+ VALUES (?, ?, 1, 0, 0, 0, ?, ?, ?)`, this.rootIno, DEFAULT_DIR_MODE, now, now, now);
247
+ }
248
+ return chunkSize;
249
+ }
250
+ normalizePath(path) {
251
+ const normalized = path.replace(/\/+$/, '') || '/';
252
+ return normalized.startsWith('/') ? normalized : '/' + normalized;
253
+ }
254
+ splitPath(path) {
255
+ const normalized = this.normalizePath(path);
256
+ if (normalized === '/')
257
+ return [];
258
+ return normalized.split('/').filter(p => p);
259
+ }
260
+ resolvePath(path) {
261
+ const normalized = this.normalizePath(path);
262
+ if (normalized === '/') {
263
+ return this.rootIno;
264
+ }
265
+ const parts = this.splitPath(normalized);
266
+ let currentIno = this.rootIno;
267
+ for (const name of parts) {
268
+ const rows = this.storage.sql.exec('SELECT ino FROM fs_dentry WHERE parent_ino = ? AND name = ?', currentIno, name).toArray();
269
+ if (rows.length === 0) {
270
+ return null;
271
+ }
272
+ currentIno = rows[0].ino;
273
+ }
274
+ return currentIno;
275
+ }
276
+ resolvePathOrThrow(path, syscall) {
277
+ const normalizedPath = this.normalizePath(path);
278
+ const ino = this.resolvePath(normalizedPath);
279
+ if (ino === null) {
280
+ throw createFsError({
281
+ code: 'ENOENT',
282
+ syscall,
283
+ path: normalizedPath,
284
+ message: 'no such file or directory',
285
+ });
286
+ }
287
+ return { normalizedPath, ino };
288
+ }
289
+ resolveParent(path) {
290
+ const normalized = this.normalizePath(path);
291
+ if (normalized === '/') {
292
+ return null;
293
+ }
294
+ const parts = this.splitPath(normalized);
295
+ const name = parts[parts.length - 1];
296
+ const parentPath = parts.length === 1 ? '/' : '/' + parts.slice(0, -1).join('/');
297
+ const parentIno = this.resolvePath(parentPath);
298
+ if (parentIno === null) {
299
+ return null;
300
+ }
301
+ return { parentIno, name };
302
+ }
303
+ createInode(mode, uid = 0, gid = 0) {
304
+ const now = Math.floor(Date.now() / 1000);
305
+ this.storage.sql.exec(`INSERT INTO fs_inode (mode, uid, gid, size, atime, mtime, ctime)
306
+ VALUES (?, ?, ?, 0, ?, ?, ?)`, mode, uid, gid, now, now, now);
307
+ // Get the last inserted rowid
308
+ const rows = this.storage.sql.exec('SELECT last_insert_rowid() as ino').toArray();
309
+ return rows[0].ino;
310
+ }
311
+ createDentry(parentIno, name, ino) {
312
+ this.storage.sql.exec('INSERT INTO fs_dentry (name, parent_ino, ino) VALUES (?, ?, ?)', name, parentIno, ino);
313
+ this.storage.sql.exec('UPDATE fs_inode SET nlink = nlink + 1 WHERE ino = ?', ino);
314
+ }
315
+ ensureParentDirs(path) {
316
+ const parts = this.splitPath(path);
317
+ parts.pop();
318
+ let currentIno = this.rootIno;
319
+ for (const name of parts) {
320
+ const rows = this.storage.sql.exec('SELECT ino FROM fs_dentry WHERE parent_ino = ? AND name = ?', currentIno, name).toArray();
321
+ if (rows.length === 0) {
322
+ const dirIno = this.createInode(DEFAULT_DIR_MODE);
323
+ this.createDentry(currentIno, name, dirIno);
324
+ currentIno = dirIno;
325
+ }
326
+ else {
327
+ const mode = this.getInodeMode(rows[0].ino);
328
+ if (mode !== null && (mode & S_IFMT) !== S_IFDIR) {
329
+ throw createFsError({
330
+ code: 'ENOTDIR',
331
+ syscall: 'open',
332
+ path: this.normalizePath(path),
333
+ message: 'not a directory',
334
+ });
335
+ }
336
+ currentIno = rows[0].ino;
337
+ }
338
+ }
339
+ }
340
+ getLinkCount(ino) {
341
+ const rows = this.storage.sql.exec('SELECT nlink FROM fs_inode WHERE ino = ?', ino).toArray();
342
+ return rows[0]?.nlink ?? 0;
343
+ }
344
+ getInodeMode(ino) {
345
+ const rows = this.storage.sql.exec('SELECT mode FROM fs_inode WHERE ino = ?', ino).toArray();
346
+ return rows[0]?.mode ?? null;
347
+ }
348
+ // ==================== FileSystem Interface Implementation ====================
349
+ async writeFile(path, content, options) {
350
+ const encoding = typeof options === 'string'
351
+ ? options
352
+ : options?.encoding;
353
+ const buffer = typeof content === 'string'
354
+ ? Buffer.from(content, encoding ?? 'utf8')
355
+ : content;
356
+ this.storage.transactionSync(() => {
357
+ this.ensureParentDirs(path);
358
+ const ino = this.resolvePath(path);
359
+ const normalizedPath = this.normalizePath(path);
360
+ if (ino !== null) {
361
+ const mode = this.getInodeMode(ino);
362
+ if (mode !== null && (mode & S_IFMT) === S_IFDIR) {
363
+ throw createFsError({
364
+ code: 'EISDIR',
365
+ syscall: 'open',
366
+ path: normalizedPath,
367
+ message: 'illegal operation on a directory',
368
+ });
369
+ }
370
+ this.updateFileContent(ino, buffer);
371
+ }
372
+ else {
373
+ const parent = this.resolveParent(path);
374
+ if (!parent) {
375
+ throw createFsError({
376
+ code: 'ENOENT',
377
+ syscall: 'open',
378
+ path: normalizedPath,
379
+ message: 'no such file or directory',
380
+ });
381
+ }
382
+ const fileIno = this.createInode(DEFAULT_FILE_MODE);
383
+ this.createDentry(parent.parentIno, parent.name, fileIno);
384
+ this.updateFileContent(fileIno, buffer);
385
+ }
386
+ });
387
+ }
388
+ updateFileContent(ino, buffer) {
389
+ const now = Math.floor(Date.now() / 1000);
390
+ this.storage.sql.exec('DELETE FROM fs_data WHERE ino = ?', ino);
391
+ if (buffer.length > 0) {
392
+ let chunkIndex = 0;
393
+ for (let offset = 0; offset < buffer.length; offset += this.chunkSize) {
394
+ const chunk = buffer.subarray(offset, Math.min(offset + this.chunkSize, buffer.length));
395
+ this.storage.sql.exec('INSERT INTO fs_data (ino, chunk_index, data) VALUES (?, ?, ?)', ino, chunkIndex, chunk);
396
+ chunkIndex++;
397
+ }
398
+ }
399
+ this.storage.sql.exec('UPDATE fs_inode SET size = ?, mtime = ? WHERE ino = ?', buffer.length, now, ino);
400
+ }
401
+ async readFile(path, options) {
402
+ const encoding = typeof options === 'string'
403
+ ? options
404
+ : options?.encoding;
405
+ const { normalizedPath, ino } = this.resolvePathOrThrow(path, 'open');
406
+ const mode = this.getInodeMode(ino);
407
+ if (mode !== null && (mode & S_IFMT) === S_IFDIR) {
408
+ throw createFsError({
409
+ code: 'EISDIR',
410
+ syscall: 'open',
411
+ path: normalizedPath,
412
+ message: 'illegal operation on a directory',
413
+ });
414
+ }
415
+ const rows = this.storage.sql.exec('SELECT data FROM fs_data WHERE ino = ? ORDER BY chunk_index ASC', ino).toArray();
416
+ let combined;
417
+ if (rows.length === 0) {
418
+ combined = Buffer.alloc(0);
419
+ }
420
+ else {
421
+ const buffers = rows.map(row => Buffer.from(row.data));
422
+ combined = Buffer.concat(buffers);
423
+ }
424
+ const now = Math.floor(Date.now() / 1000);
425
+ this.storage.sql.exec('UPDATE fs_inode SET atime = ? WHERE ino = ?', now, ino);
426
+ if (encoding) {
427
+ return combined.toString(encoding);
428
+ }
429
+ return combined;
430
+ }
431
+ async readdir(path) {
432
+ const { normalizedPath, ino } = this.resolvePathOrThrow(path, 'scandir');
433
+ const mode = this.getInodeMode(ino);
434
+ if (mode !== null && (mode & S_IFMT) !== S_IFDIR) {
435
+ throw createFsError({
436
+ code: 'ENOTDIR',
437
+ syscall: 'scandir',
438
+ path: normalizedPath,
439
+ message: 'not a directory',
440
+ });
441
+ }
442
+ const rows = this.storage.sql.exec('SELECT name FROM fs_dentry WHERE parent_ino = ? ORDER BY name ASC', ino).toArray();
443
+ return rows.map(row => row.name);
444
+ }
445
+ async readdirPlus(path) {
446
+ const { normalizedPath, ino } = this.resolvePathOrThrow(path, 'scandir');
447
+ const mode = this.getInodeMode(ino);
448
+ if (mode !== null && (mode & S_IFMT) !== S_IFDIR) {
449
+ throw createFsError({
450
+ code: 'ENOTDIR',
451
+ syscall: 'scandir',
452
+ path: normalizedPath,
453
+ message: 'not a directory',
454
+ });
455
+ }
456
+ const rows = this.storage.sql.exec(`SELECT d.name, i.ino, i.mode, i.nlink, i.uid, i.gid, i.size, i.atime, i.mtime, i.ctime
457
+ FROM fs_dentry d
458
+ JOIN fs_inode i ON d.ino = i.ino
459
+ WHERE d.parent_ino = ?
460
+ ORDER BY d.name ASC`, ino).toArray();
461
+ return rows.map(row => ({
462
+ name: row.name,
463
+ stats: createStats({
464
+ ino: row.ino,
465
+ mode: row.mode,
466
+ nlink: row.nlink,
467
+ uid: row.uid,
468
+ gid: row.gid,
469
+ size: row.size,
470
+ atime: row.atime,
471
+ mtime: row.mtime,
472
+ ctime: row.ctime,
473
+ }),
474
+ }));
475
+ }
476
+ async stat(path) {
477
+ const { normalizedPath, ino } = this.resolvePathOrThrow(path, 'stat');
478
+ const rows = this.storage.sql.exec(`SELECT ino, mode, nlink, uid, gid, size, atime, mtime, ctime
479
+ FROM fs_inode WHERE ino = ?`, ino).toArray();
480
+ if (rows.length === 0) {
481
+ throw createFsError({
482
+ code: 'ENOENT',
483
+ syscall: 'stat',
484
+ path: normalizedPath,
485
+ message: 'no such file or directory',
486
+ });
487
+ }
488
+ return createStats(rows[0]);
489
+ }
490
+ async lstat(path) {
491
+ return this.stat(path);
492
+ }
493
+ async mkdir(path) {
494
+ const normalizedPath = this.normalizePath(path);
495
+ const existing = this.resolvePath(normalizedPath);
496
+ if (existing !== null) {
497
+ throw createFsError({
498
+ code: 'EEXIST',
499
+ syscall: 'mkdir',
500
+ path: normalizedPath,
501
+ message: 'file already exists',
502
+ });
503
+ }
504
+ const parent = this.resolveParent(normalizedPath);
505
+ if (!parent) {
506
+ throw createFsError({
507
+ code: 'ENOENT',
508
+ syscall: 'mkdir',
509
+ path: normalizedPath,
510
+ message: 'no such file or directory',
511
+ });
512
+ }
513
+ const parentMode = this.getInodeMode(parent.parentIno);
514
+ if (parentMode !== null && (parentMode & S_IFMT) !== S_IFDIR) {
515
+ throw createFsError({
516
+ code: 'ENOTDIR',
517
+ syscall: 'mkdir',
518
+ path: normalizedPath,
519
+ message: 'not a directory',
520
+ });
521
+ }
522
+ this.storage.transactionSync(() => {
523
+ const dirIno = this.createInode(DEFAULT_DIR_MODE);
524
+ try {
525
+ this.createDentry(parent.parentIno, parent.name, dirIno);
526
+ }
527
+ catch {
528
+ throw createFsError({
529
+ code: 'EEXIST',
530
+ syscall: 'mkdir',
531
+ path: normalizedPath,
532
+ message: 'file already exists',
533
+ });
534
+ }
535
+ });
536
+ }
537
+ async rmdir(path) {
538
+ const normalizedPath = this.normalizePath(path);
539
+ if (normalizedPath === '/') {
540
+ throw createFsError({
541
+ code: 'EPERM',
542
+ syscall: 'rmdir',
543
+ path: normalizedPath,
544
+ message: 'operation not permitted',
545
+ });
546
+ }
547
+ const { ino } = this.resolvePathOrThrow(normalizedPath, 'rmdir');
548
+ const mode = this.getInodeMode(ino);
549
+ if (mode === null || (mode & S_IFMT) !== S_IFDIR) {
550
+ throw createFsError({
551
+ code: 'ENOTDIR',
552
+ syscall: 'rmdir',
553
+ path: normalizedPath,
554
+ message: 'not a directory',
555
+ });
556
+ }
557
+ const children = this.storage.sql.exec('SELECT 1 as one FROM fs_dentry WHERE parent_ino = ? LIMIT 1', ino).toArray();
558
+ if (children.length > 0) {
559
+ throw createFsError({
560
+ code: 'ENOTEMPTY',
561
+ syscall: 'rmdir',
562
+ path: normalizedPath,
563
+ message: 'directory not empty',
564
+ });
565
+ }
566
+ const parent = this.resolveParent(normalizedPath);
567
+ if (!parent) {
568
+ throw createFsError({
569
+ code: 'EPERM',
570
+ syscall: 'rmdir',
571
+ path: normalizedPath,
572
+ message: 'operation not permitted',
573
+ });
574
+ }
575
+ this.removeDentryAndMaybeInode(parent.parentIno, parent.name, ino);
576
+ }
577
+ async unlink(path) {
578
+ const normalizedPath = this.normalizePath(path);
579
+ if (normalizedPath === '/') {
580
+ throw createFsError({
581
+ code: 'EPERM',
582
+ syscall: 'unlink',
583
+ path: normalizedPath,
584
+ message: 'operation not permitted',
585
+ });
586
+ }
587
+ const { ino } = this.resolvePathOrThrow(normalizedPath, 'unlink');
588
+ const mode = this.getInodeMode(ino);
589
+ if (mode !== null && (mode & S_IFMT) === S_IFDIR) {
590
+ throw createFsError({
591
+ code: 'EISDIR',
592
+ syscall: 'unlink',
593
+ path: normalizedPath,
594
+ message: 'illegal operation on a directory',
595
+ });
596
+ }
597
+ const parent = this.resolveParent(normalizedPath);
598
+ this.storage.transactionSync(() => {
599
+ this.storage.sql.exec('DELETE FROM fs_dentry WHERE parent_ino = ? AND name = ?', parent.parentIno, parent.name);
600
+ this.storage.sql.exec('UPDATE fs_inode SET nlink = nlink - 1 WHERE ino = ?', ino);
601
+ const linkCount = this.getLinkCount(ino);
602
+ if (linkCount === 0) {
603
+ this.storage.sql.exec('DELETE FROM fs_inode WHERE ino = ?', ino);
604
+ this.storage.sql.exec('DELETE FROM fs_data WHERE ino = ?', ino);
605
+ }
606
+ });
607
+ }
608
+ async rm(path, options) {
609
+ const normalizedPath = this.normalizePath(path);
610
+ const force = options?.force ?? false;
611
+ const recursive = options?.recursive ?? false;
612
+ if (normalizedPath === '/') {
613
+ throw createFsError({
614
+ code: 'EPERM',
615
+ syscall: 'rm',
616
+ path: normalizedPath,
617
+ message: 'operation not permitted',
618
+ });
619
+ }
620
+ const ino = this.resolvePath(normalizedPath);
621
+ if (ino === null) {
622
+ if (!force) {
623
+ throw createFsError({
624
+ code: 'ENOENT',
625
+ syscall: 'rm',
626
+ path: normalizedPath,
627
+ message: 'no such file or directory',
628
+ });
629
+ }
630
+ return;
631
+ }
632
+ const mode = this.getInodeMode(ino);
633
+ if (mode === null) {
634
+ if (!force) {
635
+ throw createFsError({
636
+ code: 'ENOENT',
637
+ syscall: 'rm',
638
+ path: normalizedPath,
639
+ message: 'no such file or directory',
640
+ });
641
+ }
642
+ return;
643
+ }
644
+ const parent = this.resolveParent(normalizedPath);
645
+ if (!parent) {
646
+ throw createFsError({
647
+ code: 'EPERM',
648
+ syscall: 'rm',
649
+ path: normalizedPath,
650
+ message: 'operation not permitted',
651
+ });
652
+ }
653
+ if ((mode & S_IFMT) === S_IFDIR) {
654
+ if (!recursive) {
655
+ throw createFsError({
656
+ code: 'EISDIR',
657
+ syscall: 'rm',
658
+ path: normalizedPath,
659
+ message: 'illegal operation on a directory',
660
+ });
661
+ }
662
+ this.storage.transactionSync(() => {
663
+ this.rmDirContentsRecursive(ino);
664
+ this.removeDentryAndMaybeInode(parent.parentIno, parent.name, ino);
665
+ });
666
+ return;
667
+ }
668
+ this.removeDentryAndMaybeInode(parent.parentIno, parent.name, ino);
669
+ }
670
+ rmDirContentsRecursive(dirIno) {
671
+ const children = this.storage.sql.exec('SELECT name, ino FROM fs_dentry WHERE parent_ino = ? ORDER BY name ASC', dirIno).toArray();
672
+ for (const child of children) {
673
+ const mode = this.getInodeMode(child.ino);
674
+ if (mode === null) {
675
+ continue;
676
+ }
677
+ if ((mode & S_IFMT) === S_IFDIR) {
678
+ this.rmDirContentsRecursive(child.ino);
679
+ this.removeDentryAndMaybeInode(dirIno, child.name, child.ino);
680
+ }
681
+ else {
682
+ this.removeDentryAndMaybeInode(dirIno, child.name, child.ino);
683
+ }
684
+ }
685
+ }
686
+ removeDentryAndMaybeInode(parentIno, name, ino) {
687
+ this.storage.sql.exec('DELETE FROM fs_dentry WHERE parent_ino = ? AND name = ?', parentIno, name);
688
+ this.storage.sql.exec('UPDATE fs_inode SET nlink = nlink - 1 WHERE ino = ?', ino);
689
+ const linkCount = this.getLinkCount(ino);
690
+ if (linkCount === 0) {
691
+ this.storage.sql.exec('DELETE FROM fs_inode WHERE ino = ?', ino);
692
+ this.storage.sql.exec('DELETE FROM fs_data WHERE ino = ?', ino);
693
+ this.storage.sql.exec('DELETE FROM fs_symlink WHERE ino = ?', ino);
694
+ }
695
+ }
696
+ async rename(oldPath, newPath) {
697
+ const oldNormalized = this.normalizePath(oldPath);
698
+ const newNormalized = this.normalizePath(newPath);
699
+ if (oldNormalized === newNormalized)
700
+ return;
701
+ if (oldNormalized === '/' || newNormalized === '/') {
702
+ throw createFsError({
703
+ code: 'EPERM',
704
+ syscall: 'rename',
705
+ path: oldNormalized === '/' ? oldNormalized : newNormalized,
706
+ message: 'operation not permitted',
707
+ });
708
+ }
709
+ const oldParent = this.resolveParent(oldNormalized);
710
+ if (!oldParent) {
711
+ throw createFsError({
712
+ code: 'EPERM',
713
+ syscall: 'rename',
714
+ path: oldNormalized,
715
+ message: 'operation not permitted',
716
+ });
717
+ }
718
+ const newParent = this.resolveParent(newNormalized);
719
+ if (!newParent) {
720
+ throw createFsError({
721
+ code: 'ENOENT',
722
+ syscall: 'rename',
723
+ path: newNormalized,
724
+ message: 'no such file or directory',
725
+ });
726
+ }
727
+ this.storage.transactionSync(() => {
728
+ const { ino: oldIno } = this.resolvePathOrThrow(oldNormalized, 'rename');
729
+ const oldMode = this.getInodeMode(oldIno);
730
+ if (oldMode === null) {
731
+ throw createFsError({
732
+ code: 'ENOENT',
733
+ syscall: 'rename',
734
+ path: oldNormalized,
735
+ message: 'no such file or directory',
736
+ });
737
+ }
738
+ const oldIsDir = (oldMode & S_IFMT) === S_IFDIR;
739
+ if (oldIsDir && newNormalized.startsWith(oldNormalized + '/')) {
740
+ throw createFsError({
741
+ code: 'EINVAL',
742
+ syscall: 'rename',
743
+ path: newNormalized,
744
+ message: 'invalid argument',
745
+ });
746
+ }
747
+ const newIno = this.resolvePath(newNormalized);
748
+ if (newIno !== null) {
749
+ const newMode = this.getInodeMode(newIno);
750
+ if (newMode !== null) {
751
+ const newIsDir = (newMode & S_IFMT) === S_IFDIR;
752
+ if (newIsDir && !oldIsDir) {
753
+ throw createFsError({
754
+ code: 'EISDIR',
755
+ syscall: 'rename',
756
+ path: newNormalized,
757
+ message: 'illegal operation on a directory',
758
+ });
759
+ }
760
+ if (!newIsDir && oldIsDir) {
761
+ throw createFsError({
762
+ code: 'ENOTDIR',
763
+ syscall: 'rename',
764
+ path: newNormalized,
765
+ message: 'not a directory',
766
+ });
767
+ }
768
+ if (newIsDir) {
769
+ const children = this.storage.sql.exec('SELECT 1 as one FROM fs_dentry WHERE parent_ino = ? LIMIT 1', newIno).toArray();
770
+ if (children.length > 0) {
771
+ throw createFsError({
772
+ code: 'ENOTEMPTY',
773
+ syscall: 'rename',
774
+ path: newNormalized,
775
+ message: 'directory not empty',
776
+ });
777
+ }
778
+ }
779
+ this.removeDentryAndMaybeInode(newParent.parentIno, newParent.name, newIno);
780
+ }
781
+ }
782
+ this.storage.sql.exec('UPDATE fs_dentry SET parent_ino = ?, name = ? WHERE parent_ino = ? AND name = ?', newParent.parentIno, newParent.name, oldParent.parentIno, oldParent.name);
783
+ const now = Math.floor(Date.now() / 1000);
784
+ this.storage.sql.exec('UPDATE fs_inode SET ctime = ? WHERE ino = ?', now, oldIno);
785
+ this.storage.sql.exec('UPDATE fs_inode SET mtime = ?, ctime = ? WHERE ino = ?', now, now, oldParent.parentIno);
786
+ if (newParent.parentIno !== oldParent.parentIno) {
787
+ this.storage.sql.exec('UPDATE fs_inode SET mtime = ?, ctime = ? WHERE ino = ?', now, now, newParent.parentIno);
788
+ }
789
+ });
790
+ }
791
+ async copyFile(src, dest) {
792
+ const srcNormalized = this.normalizePath(src);
793
+ const destNormalized = this.normalizePath(dest);
794
+ if (srcNormalized === destNormalized) {
795
+ throw createFsError({
796
+ code: 'EINVAL',
797
+ syscall: 'copyfile',
798
+ path: destNormalized,
799
+ message: 'invalid argument',
800
+ });
801
+ }
802
+ const { ino: srcIno } = this.resolvePathOrThrow(srcNormalized, 'copyfile');
803
+ const srcMode = this.getInodeMode(srcIno);
804
+ if (srcMode !== null && (srcMode & S_IFMT) === S_IFDIR) {
805
+ throw createFsError({
806
+ code: 'EISDIR',
807
+ syscall: 'copyfile',
808
+ path: srcNormalized,
809
+ message: 'illegal operation on a directory',
810
+ });
811
+ }
812
+ const srcRows = this.storage.sql.exec('SELECT mode, uid, gid, size FROM fs_inode WHERE ino = ?', srcIno).toArray();
813
+ if (srcRows.length === 0) {
814
+ throw createFsError({
815
+ code: 'ENOENT',
816
+ syscall: 'copyfile',
817
+ path: srcNormalized,
818
+ message: 'no such file or directory',
819
+ });
820
+ }
821
+ const srcRow = srcRows[0];
822
+ const destParent = this.resolveParent(destNormalized);
823
+ if (!destParent) {
824
+ throw createFsError({
825
+ code: 'ENOENT',
826
+ syscall: 'copyfile',
827
+ path: destNormalized,
828
+ message: 'no such file or directory',
829
+ });
830
+ }
831
+ this.storage.transactionSync(() => {
832
+ const now = Math.floor(Date.now() / 1000);
833
+ const destIno = this.resolvePath(destNormalized);
834
+ if (destIno !== null) {
835
+ const destMode = this.getInodeMode(destIno);
836
+ if (destMode !== null && (destMode & S_IFMT) === S_IFDIR) {
837
+ throw createFsError({
838
+ code: 'EISDIR',
839
+ syscall: 'copyfile',
840
+ path: destNormalized,
841
+ message: 'illegal operation on a directory',
842
+ });
843
+ }
844
+ this.storage.sql.exec('DELETE FROM fs_data WHERE ino = ?', destIno);
845
+ // Copy data chunks
846
+ const srcData = this.storage.sql.exec('SELECT chunk_index, data FROM fs_data WHERE ino = ? ORDER BY chunk_index ASC', srcIno).toArray();
847
+ for (const chunk of srcData) {
848
+ this.storage.sql.exec('INSERT INTO fs_data (ino, chunk_index, data) VALUES (?, ?, ?)', destIno, chunk.chunk_index, Buffer.from(chunk.data));
849
+ }
850
+ this.storage.sql.exec('UPDATE fs_inode SET mode = ?, uid = ?, gid = ?, size = ?, mtime = ?, ctime = ? WHERE ino = ?', srcRow.mode, srcRow.uid, srcRow.gid, srcRow.size, now, now, destIno);
851
+ }
852
+ else {
853
+ const destInoCreated = this.createInode(srcRow.mode, srcRow.uid, srcRow.gid);
854
+ this.createDentry(destParent.parentIno, destParent.name, destInoCreated);
855
+ // Copy data chunks
856
+ const srcData = this.storage.sql.exec('SELECT chunk_index, data FROM fs_data WHERE ino = ? ORDER BY chunk_index ASC', srcIno).toArray();
857
+ for (const chunk of srcData) {
858
+ this.storage.sql.exec('INSERT INTO fs_data (ino, chunk_index, data) VALUES (?, ?, ?)', destInoCreated, chunk.chunk_index, Buffer.from(chunk.data));
859
+ }
860
+ this.storage.sql.exec('UPDATE fs_inode SET size = ?, mtime = ?, ctime = ? WHERE ino = ?', srcRow.size, now, now, destInoCreated);
861
+ }
862
+ });
863
+ }
864
+ async symlink(target, linkpath) {
865
+ const normalizedLinkpath = this.normalizePath(linkpath);
866
+ const existing = this.resolvePath(normalizedLinkpath);
867
+ if (existing !== null) {
868
+ throw createFsError({
869
+ code: 'EEXIST',
870
+ syscall: 'open',
871
+ path: normalizedLinkpath,
872
+ message: 'file already exists',
873
+ });
874
+ }
875
+ const parent = this.resolveParent(normalizedLinkpath);
876
+ if (!parent) {
877
+ throw createFsError({
878
+ code: 'ENOENT',
879
+ syscall: 'open',
880
+ path: normalizedLinkpath,
881
+ message: 'no such file or directory',
882
+ });
883
+ }
884
+ this.storage.transactionSync(() => {
885
+ const mode = S_IFLNK | 0o777;
886
+ const symlinkIno = this.createInode(mode);
887
+ this.createDentry(parent.parentIno, parent.name, symlinkIno);
888
+ this.storage.sql.exec('INSERT INTO fs_symlink (ino, target) VALUES (?, ?)', symlinkIno, target);
889
+ this.storage.sql.exec('UPDATE fs_inode SET size = ? WHERE ino = ?', target.length, symlinkIno);
890
+ });
891
+ }
892
+ async readlink(path) {
893
+ const { normalizedPath, ino } = this.resolvePathOrThrow(path, 'open');
894
+ const mode = this.getInodeMode(ino);
895
+ if (mode === null || (mode & S_IFMT) !== S_IFLNK) {
896
+ throw createFsError({
897
+ code: 'EINVAL',
898
+ syscall: 'open',
899
+ path: normalizedPath,
900
+ message: 'invalid argument',
901
+ });
902
+ }
903
+ const rows = this.storage.sql.exec('SELECT target FROM fs_symlink WHERE ino = ?', ino).toArray();
904
+ if (rows.length === 0) {
905
+ throw createFsError({
906
+ code: 'ENOENT',
907
+ syscall: 'open',
908
+ path: normalizedPath,
909
+ message: 'no such file or directory',
910
+ });
911
+ }
912
+ return rows[0].target;
913
+ }
914
+ async access(path) {
915
+ const normalizedPath = this.normalizePath(path);
916
+ const ino = this.resolvePath(normalizedPath);
917
+ if (ino === null) {
918
+ throw createFsError({
919
+ code: 'ENOENT',
920
+ syscall: 'access',
921
+ path: normalizedPath,
922
+ message: 'no such file or directory',
923
+ });
924
+ }
925
+ }
926
+ async statfs() {
927
+ const inodeRows = this.storage.sql.exec('SELECT COUNT(*) as count FROM fs_inode').toArray();
928
+ const bytesRows = this.storage.sql.exec('SELECT COALESCE(SUM(LENGTH(data)), 0) as total FROM fs_data').toArray();
929
+ return {
930
+ inodes: inodeRows[0].count,
931
+ bytesUsed: bytesRows[0].total,
932
+ };
933
+ }
934
+ async open(path) {
935
+ const { normalizedPath, ino } = this.resolvePathOrThrow(path, 'open');
936
+ const mode = this.getInodeMode(ino);
937
+ if (mode !== null && (mode & S_IFMT) === S_IFDIR) {
938
+ throw createFsError({
939
+ code: 'EISDIR',
940
+ syscall: 'open',
941
+ path: normalizedPath,
942
+ message: 'illegal operation on a directory',
943
+ });
944
+ }
945
+ return new AgentFSFile(this.storage, ino, this.chunkSize);
946
+ }
947
+ // Legacy alias
948
+ async deleteFile(path) {
949
+ return await this.unlink(path);
950
+ }
951
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Cloudflare Durable Objects integration for AgentFS.
3
+ *
4
+ * Provides AgentFS - a FileSystem implementation that uses
5
+ * Cloudflare Durable Objects SQLite storage as its backend.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { AgentFS } from "agentfs-sdk/cloudflare";
10
+ *
11
+ * export class MyDurableObject extends DurableObject {
12
+ * private fs: AgentFS;
13
+ *
14
+ * constructor(ctx: DurableObjectState, env: Env) {
15
+ * super(ctx, env);
16
+ * this.fs = AgentFS.create(ctx.storage);
17
+ * }
18
+ *
19
+ * async fetch(request: Request) {
20
+ * await this.fs.writeFile('/hello.txt', 'Hello, World!');
21
+ * const content = await this.fs.readFile('/hello.txt', 'utf8');
22
+ * return new Response(content);
23
+ * }
24
+ * }
25
+ * ```
26
+ *
27
+ * @see https://developers.cloudflare.com/durable-objects/
28
+ */
29
+ export { AgentFS, type CloudflareStorage } from "./agentfs.js";
30
+ export type { FileSystem, Stats, DirEntry, FilesystemStats, FileHandle, } from "../../filesystem/interface.js";
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Cloudflare Durable Objects integration for AgentFS.
3
+ *
4
+ * Provides AgentFS - a FileSystem implementation that uses
5
+ * Cloudflare Durable Objects SQLite storage as its backend.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { AgentFS } from "agentfs-sdk/cloudflare";
10
+ *
11
+ * export class MyDurableObject extends DurableObject {
12
+ * private fs: AgentFS;
13
+ *
14
+ * constructor(ctx: DurableObjectState, env: Env) {
15
+ * super(ctx, env);
16
+ * this.fs = AgentFS.create(ctx.storage);
17
+ * }
18
+ *
19
+ * async fetch(request: Request) {
20
+ * await this.fs.writeFile('/hello.txt', 'Hello, World!');
21
+ * const content = await this.fs.readFile('/hello.txt', 'utf8');
22
+ * return new Response(content);
23
+ * }
24
+ * }
25
+ * ```
26
+ *
27
+ * @see https://developers.cloudflare.com/durable-objects/
28
+ */
29
+ export { AgentFS } from "./agentfs.js";
@@ -1,14 +1,16 @@
1
1
  /**
2
- * AgentFs - Read-write filesystem backed by AgentFS (Turso)
2
+ * AgentFs - Read-write filesystem backed by any AgentFS FileSystem implementation
3
3
  *
4
- * Full read-write filesystem that persists to an AgentFS SQLite database.
4
+ * Full read-write filesystem that persists to a SQLite database.
5
5
  * Designed for AI agents needing persistent, auditable file storage.
6
6
  *
7
- * This is a thin wrapper around AgentFS - uses native mkdir, rm, rename, etc.
7
+ * Supports any FileSystem implementation:
8
+ * - AgentFS (Turso/libsql) for Node.js and browser
9
+ * - Cloudflare AgentFS (Durable Objects) for Cloudflare Workers
8
10
  *
9
11
  * @see https://docs.turso.tech/agentfs/sdk/typescript
10
12
  */
11
- import type { AgentFS as Filesystem } from "../../filesystem/agentfs.js";
13
+ import type { FileSystem } from "../../filesystem/interface.js";
12
14
  import { type AgentFSOptions } from "../../index_node.js";
13
15
  import type { BufferEncoding, CpOptions, FsStat, IFileSystem, MkdirOptions, RmOptions } from "just-bash";
14
16
  /** Options for reading files */
@@ -21,14 +23,15 @@ interface WriteFileOptions {
21
23
  }
22
24
  type FileContent = string | Uint8Array;
23
25
  /**
24
- * Handle to an AgentFS instance (from AgentFS.open())
26
+ * Handle to any FileSystem implementation.
27
+ * Can be from AgentFS.open() (Turso) or AgentFS.create() (Cloudflare).
25
28
  */
26
29
  export interface AgentFsHandle {
27
- fs: Filesystem;
30
+ fs: FileSystem;
28
31
  }
29
32
  export interface AgentFsOptions {
30
33
  /**
31
- * The AgentFS handle from AgentFS.open()
34
+ * The AgentFS handle containing a FileSystem implementation.
32
35
  */
33
36
  fs: AgentFsHandle;
34
37
  /**
@@ -74,15 +77,26 @@ export declare class AgentFsWrapper implements IFileSystem {
74
77
  readlink(path: string): Promise<string>;
75
78
  }
76
79
  /**
77
- * Create a just-bash compatible filesystem backed by AgentFS.
80
+ * Create a just-bash compatible filesystem backed by any AgentFS FileSystem.
78
81
  *
79
- * @example
82
+ * @example Node.js with Turso:
80
83
  * ```ts
81
84
  * import { agentfs } from "agentfs-sdk/just-bash";
82
85
  *
83
86
  * const fs = await agentfs({ path: "agent.db" });
84
87
  * const bashTool = createBashTool({ fs });
85
88
  * ```
89
+ *
90
+ * @example Cloudflare Workers:
91
+ * ```ts
92
+ * import { agentfs } from "agentfs-sdk/just-bash";
93
+ * import { AgentFS } from "agentfs-sdk/cloudflare";
94
+ *
95
+ * // In a Durable Object:
96
+ * const agentFs = AgentFS.create(ctx.storage);
97
+ * const fs = agentfs({ fs: agentFs });
98
+ * const bashTool = createBashTool({ fs });
99
+ * ```
86
100
  */
87
- export declare function agentfs(handleOrOptions: AgentFsHandle | AgentFSOptions, mountPoint?: string): Promise<IFileSystem>;
101
+ export declare function agentfs(handleOrOptions: AgentFsHandle | FileSystem | AgentFSOptions, mountPoint?: string): Promise<IFileSystem>;
88
102
  export {};
@@ -1,10 +1,12 @@
1
1
  /**
2
- * AgentFs - Read-write filesystem backed by AgentFS (Turso)
2
+ * AgentFs - Read-write filesystem backed by any AgentFS FileSystem implementation
3
3
  *
4
- * Full read-write filesystem that persists to an AgentFS SQLite database.
4
+ * Full read-write filesystem that persists to a SQLite database.
5
5
  * Designed for AI agents needing persistent, auditable file storage.
6
6
  *
7
- * This is a thin wrapper around AgentFS - uses native mkdir, rm, rename, etc.
7
+ * Supports any FileSystem implementation:
8
+ * - AgentFS (Turso/libsql) for Node.js and browser
9
+ * - Cloudflare AgentFS (Durable Objects) for Cloudflare Workers
8
10
  *
9
11
  * @see https://docs.turso.tech/agentfs/sdk/typescript
10
12
  */
@@ -365,8 +367,8 @@ export class AgentFsWrapper {
365
367
  if (pathExists) {
366
368
  throw new Error(`EEXIST: file already exists, symlink '${linkPath}'`);
367
369
  }
368
- // AgentFS doesn't support symlinks natively yet
369
- // Create a special file that acts like a symlink
370
+ // Use JSON workaround for symlinks - this provides consistent behavior
371
+ // across all FileSystem implementations and allows reading symlink content
370
372
  const content = JSON.stringify({ __symlink: target });
371
373
  await this.writeFile(normalized, content);
372
374
  }
@@ -392,7 +394,7 @@ export class AgentFsWrapper {
392
394
  if (!pathExists) {
393
395
  throw new Error(`ENOENT: no such file or directory, readlink '${path}'`);
394
396
  }
395
- // Try to read as symlink (our special JSON format)
397
+ // Read as JSON symlink workaround
396
398
  try {
397
399
  const content = await this.readFile(normalized);
398
400
  const parsed = JSON.parse(content);
@@ -401,26 +403,43 @@ export class AgentFsWrapper {
401
403
  }
402
404
  }
403
405
  catch {
404
- // Not a symlink
406
+ // Not a JSON symlink
405
407
  }
406
408
  throw new Error(`EINVAL: invalid argument, readlink '${path}'`);
407
409
  }
408
410
  }
409
411
  /**
410
- * Create a just-bash compatible filesystem backed by AgentFS.
412
+ * Create a just-bash compatible filesystem backed by any AgentFS FileSystem.
411
413
  *
412
- * @example
414
+ * @example Node.js with Turso:
413
415
  * ```ts
414
416
  * import { agentfs } from "agentfs-sdk/just-bash";
415
417
  *
416
418
  * const fs = await agentfs({ path: "agent.db" });
417
419
  * const bashTool = createBashTool({ fs });
418
420
  * ```
421
+ *
422
+ * @example Cloudflare Workers:
423
+ * ```ts
424
+ * import { agentfs } from "agentfs-sdk/just-bash";
425
+ * import { AgentFS } from "agentfs-sdk/cloudflare";
426
+ *
427
+ * // In a Durable Object:
428
+ * const agentFs = AgentFS.create(ctx.storage);
429
+ * const fs = agentfs({ fs: agentFs });
430
+ * const bashTool = createBashTool({ fs });
431
+ * ```
419
432
  */
420
433
  export async function agentfs(handleOrOptions, mountPoint) {
421
- if ("fs" in handleOrOptions) {
434
+ // Check if it's a FileSystem directly (has stat method but not a handle)
435
+ if (typeof handleOrOptions.stat === 'function' && !('fs' in handleOrOptions)) {
436
+ return new AgentFsWrapper({ fs: { fs: handleOrOptions }, mountPoint });
437
+ }
438
+ // Check if it's an AgentFsHandle (has fs property containing a FileSystem)
439
+ if ("fs" in handleOrOptions && typeof handleOrOptions.fs?.stat === 'function') {
422
440
  return new AgentFsWrapper({ fs: handleOrOptions, mountPoint });
423
441
  }
442
+ // Otherwise it's AgentFSOptions for creating a new Turso-based AgentFS
424
443
  const handle = await AgentFS.open(handleOrOptions);
425
444
  return new AgentFsWrapper({ fs: handle, mountPoint });
426
445
  }
@@ -1 +1,2 @@
1
1
  export { agentfs, AgentFsWrapper, type AgentFsHandle, type AgentFsOptions } from "./AgentFs.js";
2
+ export type { FileSystem } from "../../filesystem/interface.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentfs-sdk",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "AgentFS SDK",
5
5
  "main": "dist/index_node.js",
6
6
  "types": "dist/index_node.d.ts",
@@ -25,6 +25,10 @@
25
25
  "./just-bash": {
26
26
  "import": "./dist/integrations/just-bash/index.js",
27
27
  "types": "./dist/integrations/just-bash/index.d.ts"
28
+ },
29
+ "./cloudflare": {
30
+ "import": "./dist/integrations/cloudflare/index.js",
31
+ "types": "./dist/integrations/cloudflare/index.d.ts"
28
32
  }
29
33
  },
30
34
  "keywords": [