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.
- package/dist/integrations/cloudflare/agentfs.d.ts +120 -0
- package/dist/integrations/cloudflare/agentfs.js +951 -0
- package/dist/integrations/cloudflare/index.d.ts +30 -0
- package/dist/integrations/cloudflare/index.js +29 -0
- package/dist/integrations/just-bash/AgentFs.d.ts +24 -10
- package/dist/integrations/just-bash/AgentFs.js +29 -10
- package/dist/integrations/just-bash/index.d.ts +1 -0
- package/package.json +5 -1
|
@@ -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
|
|
2
|
+
* AgentFs - Read-write filesystem backed by any AgentFS FileSystem implementation
|
|
3
3
|
*
|
|
4
|
-
* Full read-write filesystem that persists to
|
|
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
|
-
*
|
|
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 {
|
|
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
|
|
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:
|
|
30
|
+
fs: FileSystem;
|
|
28
31
|
}
|
|
29
32
|
export interface AgentFsOptions {
|
|
30
33
|
/**
|
|
31
|
-
* The AgentFS handle
|
|
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
|
|
2
|
+
* AgentFs - Read-write filesystem backed by any AgentFS FileSystem implementation
|
|
3
3
|
*
|
|
4
|
-
* Full read-write filesystem that persists to
|
|
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
|
-
*
|
|
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
|
-
//
|
|
369
|
-
//
|
|
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
|
-
//
|
|
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 (
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentfs-sdk",
|
|
3
|
-
"version": "0.4.
|
|
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": [
|