agentfs-sdk 0.1.0-pre.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/filesystem.d.ts +68 -0
- package/dist/filesystem.js +362 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +57 -0
- package/dist/kvstore.d.ts +14 -0
- package/dist/kvstore.js +71 -0
- package/dist/toolcalls.d.ts +69 -0
- package/dist/toolcalls.js +212 -0
- package/package.json +39 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Database } from '@tursodatabase/database';
|
|
2
|
+
export interface Stats {
|
|
3
|
+
ino: number;
|
|
4
|
+
mode: number;
|
|
5
|
+
nlink: number;
|
|
6
|
+
uid: number;
|
|
7
|
+
gid: number;
|
|
8
|
+
size: number;
|
|
9
|
+
atime: number;
|
|
10
|
+
mtime: number;
|
|
11
|
+
ctime: number;
|
|
12
|
+
isFile(): boolean;
|
|
13
|
+
isDirectory(): boolean;
|
|
14
|
+
isSymbolicLink(): boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare class Filesystem {
|
|
17
|
+
private db;
|
|
18
|
+
private initialized;
|
|
19
|
+
private rootIno;
|
|
20
|
+
constructor(db: Database);
|
|
21
|
+
private initialize;
|
|
22
|
+
/**
|
|
23
|
+
* Ensure root directory exists
|
|
24
|
+
*/
|
|
25
|
+
private ensureRoot;
|
|
26
|
+
/**
|
|
27
|
+
* Normalize a path
|
|
28
|
+
*/
|
|
29
|
+
private normalizePath;
|
|
30
|
+
/**
|
|
31
|
+
* Split path into components
|
|
32
|
+
*/
|
|
33
|
+
private splitPath;
|
|
34
|
+
/**
|
|
35
|
+
* Resolve a path to an inode number
|
|
36
|
+
*/
|
|
37
|
+
private resolvePath;
|
|
38
|
+
/**
|
|
39
|
+
* Get parent directory inode and basename from path
|
|
40
|
+
*/
|
|
41
|
+
private resolveParent;
|
|
42
|
+
/**
|
|
43
|
+
* Create an inode
|
|
44
|
+
*/
|
|
45
|
+
private createInode;
|
|
46
|
+
/**
|
|
47
|
+
* Create a directory entry
|
|
48
|
+
*/
|
|
49
|
+
private createDentry;
|
|
50
|
+
/**
|
|
51
|
+
* Ensure parent directories exist
|
|
52
|
+
*/
|
|
53
|
+
private ensureParentDirs;
|
|
54
|
+
/**
|
|
55
|
+
* Get link count for an inode
|
|
56
|
+
*/
|
|
57
|
+
private getLinkCount;
|
|
58
|
+
writeFile(path: string, content: string | Buffer): Promise<void>;
|
|
59
|
+
private updateFileContent;
|
|
60
|
+
readFile(path: string): Promise<string>;
|
|
61
|
+
readdir(path: string): Promise<string[]>;
|
|
62
|
+
deleteFile(path: string): Promise<void>;
|
|
63
|
+
stat(path: string): Promise<Stats>;
|
|
64
|
+
/**
|
|
65
|
+
* Wait for initialization to complete
|
|
66
|
+
*/
|
|
67
|
+
ready(): Promise<void>;
|
|
68
|
+
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Filesystem = void 0;
|
|
4
|
+
// File types for mode field
|
|
5
|
+
const S_IFMT = 0o170000; // File type mask
|
|
6
|
+
const S_IFREG = 0o100000; // Regular file
|
|
7
|
+
const S_IFDIR = 0o040000; // Directory
|
|
8
|
+
const S_IFLNK = 0o120000; // Symbolic link
|
|
9
|
+
// Default permissions
|
|
10
|
+
const DEFAULT_FILE_MODE = S_IFREG | 0o644; // Regular file, rw-r--r--
|
|
11
|
+
const DEFAULT_DIR_MODE = S_IFDIR | 0o755; // Directory, rwxr-xr-x
|
|
12
|
+
class Filesystem {
|
|
13
|
+
constructor(db) {
|
|
14
|
+
this.rootIno = 1;
|
|
15
|
+
this.db = db;
|
|
16
|
+
this.initialized = this.initialize();
|
|
17
|
+
}
|
|
18
|
+
async initialize() {
|
|
19
|
+
// Ensure database is connected
|
|
20
|
+
try {
|
|
21
|
+
await this.db.connect();
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
// Ignore "already connected" errors
|
|
25
|
+
if (!error.message?.includes('already')) {
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Create the inode table
|
|
30
|
+
await this.db.exec(`
|
|
31
|
+
CREATE TABLE IF NOT EXISTS fs_inode (
|
|
32
|
+
ino INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
33
|
+
mode INTEGER NOT NULL,
|
|
34
|
+
uid INTEGER NOT NULL DEFAULT 0,
|
|
35
|
+
gid INTEGER NOT NULL DEFAULT 0,
|
|
36
|
+
size INTEGER NOT NULL DEFAULT 0,
|
|
37
|
+
atime INTEGER NOT NULL,
|
|
38
|
+
mtime INTEGER NOT NULL,
|
|
39
|
+
ctime INTEGER NOT NULL
|
|
40
|
+
)
|
|
41
|
+
`);
|
|
42
|
+
// Create the directory entry table
|
|
43
|
+
await this.db.exec(`
|
|
44
|
+
CREATE TABLE IF NOT EXISTS fs_dentry (
|
|
45
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
46
|
+
name TEXT NOT NULL,
|
|
47
|
+
parent_ino INTEGER NOT NULL,
|
|
48
|
+
ino INTEGER NOT NULL,
|
|
49
|
+
UNIQUE(parent_ino, name)
|
|
50
|
+
)
|
|
51
|
+
`);
|
|
52
|
+
// Create index for efficient path lookups
|
|
53
|
+
await this.db.exec(`
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_fs_dentry_parent
|
|
55
|
+
ON fs_dentry(parent_ino, name)
|
|
56
|
+
`);
|
|
57
|
+
// Create the data blocks table
|
|
58
|
+
await this.db.exec(`
|
|
59
|
+
CREATE TABLE IF NOT EXISTS fs_data (
|
|
60
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
61
|
+
ino INTEGER NOT NULL,
|
|
62
|
+
offset INTEGER NOT NULL,
|
|
63
|
+
size INTEGER NOT NULL,
|
|
64
|
+
data BLOB NOT NULL
|
|
65
|
+
)
|
|
66
|
+
`);
|
|
67
|
+
// Create index for efficient data block lookups
|
|
68
|
+
await this.db.exec(`
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_fs_data_ino_offset
|
|
70
|
+
ON fs_data(ino, offset)
|
|
71
|
+
`);
|
|
72
|
+
// Create the symlink table
|
|
73
|
+
await this.db.exec(`
|
|
74
|
+
CREATE TABLE IF NOT EXISTS fs_symlink (
|
|
75
|
+
ino INTEGER PRIMARY KEY,
|
|
76
|
+
target TEXT NOT NULL
|
|
77
|
+
)
|
|
78
|
+
`);
|
|
79
|
+
// Create root directory if it doesn't exist
|
|
80
|
+
await this.ensureRoot();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Ensure root directory exists
|
|
84
|
+
*/
|
|
85
|
+
async ensureRoot() {
|
|
86
|
+
const stmt = this.db.prepare('SELECT ino FROM fs_inode WHERE ino = ?');
|
|
87
|
+
const root = await stmt.get(this.rootIno);
|
|
88
|
+
if (!root) {
|
|
89
|
+
const now = Math.floor(Date.now() / 1000);
|
|
90
|
+
const insertStmt = this.db.prepare(`
|
|
91
|
+
INSERT INTO fs_inode (ino, mode, uid, gid, size, atime, mtime, ctime)
|
|
92
|
+
VALUES (?, ?, 0, 0, 0, ?, ?, ?)
|
|
93
|
+
`);
|
|
94
|
+
await insertStmt.run(this.rootIno, DEFAULT_DIR_MODE, now, now, now);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Normalize a path
|
|
99
|
+
*/
|
|
100
|
+
normalizePath(path) {
|
|
101
|
+
// Remove trailing slashes except for root
|
|
102
|
+
const normalized = path.replace(/\/+$/, '') || '/';
|
|
103
|
+
// Ensure leading slash
|
|
104
|
+
return normalized.startsWith('/') ? normalized : '/' + normalized;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Split path into components
|
|
108
|
+
*/
|
|
109
|
+
splitPath(path) {
|
|
110
|
+
const normalized = this.normalizePath(path);
|
|
111
|
+
if (normalized === '/')
|
|
112
|
+
return [];
|
|
113
|
+
return normalized.split('/').filter(p => p);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Resolve a path to an inode number
|
|
117
|
+
*/
|
|
118
|
+
async resolvePath(path) {
|
|
119
|
+
await this.initialized;
|
|
120
|
+
const normalized = this.normalizePath(path);
|
|
121
|
+
// Root directory
|
|
122
|
+
if (normalized === '/') {
|
|
123
|
+
return this.rootIno;
|
|
124
|
+
}
|
|
125
|
+
const parts = this.splitPath(normalized);
|
|
126
|
+
let currentIno = this.rootIno;
|
|
127
|
+
// Traverse the path
|
|
128
|
+
for (const name of parts) {
|
|
129
|
+
const stmt = this.db.prepare(`
|
|
130
|
+
SELECT ino FROM fs_dentry
|
|
131
|
+
WHERE parent_ino = ? AND name = ?
|
|
132
|
+
`);
|
|
133
|
+
const result = await stmt.get(currentIno, name);
|
|
134
|
+
if (!result) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
currentIno = result.ino;
|
|
138
|
+
}
|
|
139
|
+
return currentIno;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get parent directory inode and basename from path
|
|
143
|
+
*/
|
|
144
|
+
async resolveParent(path) {
|
|
145
|
+
const normalized = this.normalizePath(path);
|
|
146
|
+
if (normalized === '/') {
|
|
147
|
+
return null; // Root has no parent
|
|
148
|
+
}
|
|
149
|
+
const parts = this.splitPath(normalized);
|
|
150
|
+
const name = parts[parts.length - 1];
|
|
151
|
+
const parentPath = parts.length === 1 ? '/' : '/' + parts.slice(0, -1).join('/');
|
|
152
|
+
const parentIno = await this.resolvePath(parentPath);
|
|
153
|
+
if (parentIno === null) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return { parentIno, name };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Create an inode
|
|
160
|
+
*/
|
|
161
|
+
async createInode(mode, uid = 0, gid = 0) {
|
|
162
|
+
const now = Math.floor(Date.now() / 1000);
|
|
163
|
+
const stmt = this.db.prepare(`
|
|
164
|
+
INSERT INTO fs_inode (mode, uid, gid, size, atime, mtime, ctime)
|
|
165
|
+
VALUES (?, ?, ?, 0, ?, ?, ?)
|
|
166
|
+
`);
|
|
167
|
+
const result = await stmt.run(mode, uid, gid, now, now, now);
|
|
168
|
+
return Number(result.lastInsertRowid);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Create a directory entry
|
|
172
|
+
*/
|
|
173
|
+
async createDentry(parentIno, name, ino) {
|
|
174
|
+
const stmt = this.db.prepare(`
|
|
175
|
+
INSERT INTO fs_dentry (name, parent_ino, ino)
|
|
176
|
+
VALUES (?, ?, ?)
|
|
177
|
+
`);
|
|
178
|
+
await stmt.run(name, parentIno, ino);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Ensure parent directories exist
|
|
182
|
+
*/
|
|
183
|
+
async ensureParentDirs(path) {
|
|
184
|
+
const parts = this.splitPath(path);
|
|
185
|
+
// Remove the filename, keep only directory parts
|
|
186
|
+
parts.pop();
|
|
187
|
+
let currentIno = this.rootIno;
|
|
188
|
+
let currentPath = '';
|
|
189
|
+
for (const name of parts) {
|
|
190
|
+
currentPath += '/' + name;
|
|
191
|
+
// Check if this directory exists
|
|
192
|
+
const stmt = this.db.prepare(`
|
|
193
|
+
SELECT ino FROM fs_dentry
|
|
194
|
+
WHERE parent_ino = ? AND name = ?
|
|
195
|
+
`);
|
|
196
|
+
const result = await stmt.get(currentIno, name);
|
|
197
|
+
if (!result) {
|
|
198
|
+
// Create directory
|
|
199
|
+
const dirIno = await this.createInode(DEFAULT_DIR_MODE);
|
|
200
|
+
await this.createDentry(currentIno, name, dirIno);
|
|
201
|
+
currentIno = dirIno;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
currentIno = result.ino;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Get link count for an inode
|
|
210
|
+
*/
|
|
211
|
+
async getLinkCount(ino) {
|
|
212
|
+
const stmt = this.db.prepare('SELECT COUNT(*) as count FROM fs_dentry WHERE ino = ?');
|
|
213
|
+
const result = await stmt.get(ino);
|
|
214
|
+
return result.count;
|
|
215
|
+
}
|
|
216
|
+
async writeFile(path, content) {
|
|
217
|
+
await this.initialized;
|
|
218
|
+
// Ensure parent directories exist
|
|
219
|
+
await this.ensureParentDirs(path);
|
|
220
|
+
// Check if file already exists
|
|
221
|
+
const ino = await this.resolvePath(path);
|
|
222
|
+
if (ino !== null) {
|
|
223
|
+
// Update existing file
|
|
224
|
+
await this.updateFileContent(ino, content);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
// Create new file
|
|
228
|
+
const parent = await this.resolveParent(path);
|
|
229
|
+
if (!parent) {
|
|
230
|
+
throw new Error(`ENOENT: parent directory does not exist: ${path}`);
|
|
231
|
+
}
|
|
232
|
+
// Create inode
|
|
233
|
+
const fileIno = await this.createInode(DEFAULT_FILE_MODE);
|
|
234
|
+
// Create directory entry
|
|
235
|
+
await this.createDentry(parent.parentIno, parent.name, fileIno);
|
|
236
|
+
// Write content
|
|
237
|
+
await this.updateFileContent(fileIno, content);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async updateFileContent(ino, content) {
|
|
241
|
+
const buffer = typeof content === 'string' ? Buffer.from(content, 'utf-8') : content;
|
|
242
|
+
const now = Math.floor(Date.now() / 1000);
|
|
243
|
+
// Delete existing data blocks
|
|
244
|
+
const deleteStmt = this.db.prepare('DELETE FROM fs_data WHERE ino = ?');
|
|
245
|
+
await deleteStmt.run(ino);
|
|
246
|
+
// Write data in chunks (for now, single chunk, but can be extended)
|
|
247
|
+
const stmt = this.db.prepare(`
|
|
248
|
+
INSERT INTO fs_data (ino, offset, size, data)
|
|
249
|
+
VALUES (?, ?, ?, ?)
|
|
250
|
+
`);
|
|
251
|
+
await stmt.run(ino, 0, buffer.length, buffer);
|
|
252
|
+
// Update inode size and mtime
|
|
253
|
+
const updateStmt = this.db.prepare(`
|
|
254
|
+
UPDATE fs_inode
|
|
255
|
+
SET size = ?, mtime = ?
|
|
256
|
+
WHERE ino = ?
|
|
257
|
+
`);
|
|
258
|
+
await updateStmt.run(buffer.length, now, ino);
|
|
259
|
+
}
|
|
260
|
+
async readFile(path) {
|
|
261
|
+
await this.initialized;
|
|
262
|
+
const ino = await this.resolvePath(path);
|
|
263
|
+
if (ino === null) {
|
|
264
|
+
throw new Error(`ENOENT: no such file or directory, open '${path}'`);
|
|
265
|
+
}
|
|
266
|
+
// Get all data blocks
|
|
267
|
+
const stmt = this.db.prepare(`
|
|
268
|
+
SELECT data FROM fs_data
|
|
269
|
+
WHERE ino = ?
|
|
270
|
+
ORDER BY offset ASC
|
|
271
|
+
`);
|
|
272
|
+
const rows = await stmt.all(ino);
|
|
273
|
+
if (rows.length === 0) {
|
|
274
|
+
return '';
|
|
275
|
+
}
|
|
276
|
+
// Concatenate all chunks
|
|
277
|
+
const buffers = rows.map(row => row.data);
|
|
278
|
+
const combined = Buffer.concat(buffers);
|
|
279
|
+
// Update atime
|
|
280
|
+
const now = Math.floor(Date.now() / 1000);
|
|
281
|
+
const updateStmt = this.db.prepare('UPDATE fs_inode SET atime = ? WHERE ino = ?');
|
|
282
|
+
await updateStmt.run(now, ino);
|
|
283
|
+
return combined.toString('utf-8');
|
|
284
|
+
}
|
|
285
|
+
async readdir(path) {
|
|
286
|
+
await this.initialized;
|
|
287
|
+
const ino = await this.resolvePath(path);
|
|
288
|
+
if (ino === null) {
|
|
289
|
+
throw new Error(`ENOENT: no such file or directory, scandir '${path}'`);
|
|
290
|
+
}
|
|
291
|
+
// Get all directory entries
|
|
292
|
+
const stmt = this.db.prepare(`
|
|
293
|
+
SELECT name FROM fs_dentry
|
|
294
|
+
WHERE parent_ino = ?
|
|
295
|
+
ORDER BY name ASC
|
|
296
|
+
`);
|
|
297
|
+
const rows = await stmt.all(ino);
|
|
298
|
+
return rows.map(row => row.name);
|
|
299
|
+
}
|
|
300
|
+
async deleteFile(path) {
|
|
301
|
+
await this.initialized;
|
|
302
|
+
const ino = await this.resolvePath(path);
|
|
303
|
+
if (ino === null) {
|
|
304
|
+
throw new Error(`ENOENT: no such file or directory, unlink '${path}'`);
|
|
305
|
+
}
|
|
306
|
+
const parent = await this.resolveParent(path);
|
|
307
|
+
if (!parent) {
|
|
308
|
+
throw new Error(`Cannot delete root directory`);
|
|
309
|
+
}
|
|
310
|
+
// Delete the directory entry
|
|
311
|
+
const stmt = this.db.prepare(`
|
|
312
|
+
DELETE FROM fs_dentry
|
|
313
|
+
WHERE parent_ino = ? AND name = ?
|
|
314
|
+
`);
|
|
315
|
+
await stmt.run(parent.parentIno, parent.name);
|
|
316
|
+
// Check if this was the last link to the inode
|
|
317
|
+
const linkCount = await this.getLinkCount(ino);
|
|
318
|
+
if (linkCount === 0) {
|
|
319
|
+
// Delete the inode and all associated data (CASCADE will handle data blocks)
|
|
320
|
+
const deleteStmt = this.db.prepare('DELETE FROM fs_inode WHERE ino = ?');
|
|
321
|
+
await deleteStmt.run(ino);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async stat(path) {
|
|
325
|
+
await this.initialized;
|
|
326
|
+
const ino = await this.resolvePath(path);
|
|
327
|
+
if (ino === null) {
|
|
328
|
+
throw new Error(`ENOENT: no such file or directory, stat '${path}'`);
|
|
329
|
+
}
|
|
330
|
+
const stmt = this.db.prepare(`
|
|
331
|
+
SELECT ino, mode, uid, gid, size, atime, mtime, ctime
|
|
332
|
+
FROM fs_inode
|
|
333
|
+
WHERE ino = ?
|
|
334
|
+
`);
|
|
335
|
+
const row = await stmt.get(ino);
|
|
336
|
+
if (!row) {
|
|
337
|
+
throw new Error(`Inode not found: ${ino}`);
|
|
338
|
+
}
|
|
339
|
+
const nlink = await this.getLinkCount(ino);
|
|
340
|
+
return {
|
|
341
|
+
ino: row.ino,
|
|
342
|
+
mode: row.mode,
|
|
343
|
+
nlink: nlink,
|
|
344
|
+
uid: row.uid,
|
|
345
|
+
gid: row.gid,
|
|
346
|
+
size: row.size,
|
|
347
|
+
atime: row.atime,
|
|
348
|
+
mtime: row.mtime,
|
|
349
|
+
ctime: row.ctime,
|
|
350
|
+
isFile: () => (row.mode & S_IFMT) === S_IFREG,
|
|
351
|
+
isDirectory: () => (row.mode & S_IFMT) === S_IFDIR,
|
|
352
|
+
isSymbolicLink: () => (row.mode & S_IFMT) === S_IFLNK,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Wait for initialization to complete
|
|
357
|
+
*/
|
|
358
|
+
async ready() {
|
|
359
|
+
await this.initialized;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
exports.Filesystem = Filesystem;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Database } from '@tursodatabase/database';
|
|
2
|
+
import { KvStore } from './kvstore';
|
|
3
|
+
import { Filesystem } from './filesystem';
|
|
4
|
+
import { ToolCalls } from './toolcalls';
|
|
5
|
+
export declare class AgentFS {
|
|
6
|
+
private db;
|
|
7
|
+
readonly kv: KvStore;
|
|
8
|
+
readonly fs: Filesystem;
|
|
9
|
+
readonly tools: ToolCalls;
|
|
10
|
+
/**
|
|
11
|
+
* Private constructor - use AgentFS.create() instead
|
|
12
|
+
*/
|
|
13
|
+
private constructor();
|
|
14
|
+
/**
|
|
15
|
+
* Create a new AgentFS instance (async factory method)
|
|
16
|
+
* @param dbPath Path to the database file (defaults to ':memory:')
|
|
17
|
+
* @returns Fully initialized AgentFS instance
|
|
18
|
+
*/
|
|
19
|
+
static create(dbPath?: string): Promise<AgentFS>;
|
|
20
|
+
/**
|
|
21
|
+
* Get the underlying Database instance
|
|
22
|
+
*/
|
|
23
|
+
getDatabase(): Database;
|
|
24
|
+
/**
|
|
25
|
+
* Close the database connection
|
|
26
|
+
*/
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export { KvStore } from './kvstore';
|
|
30
|
+
export { Filesystem } from './filesystem';
|
|
31
|
+
export type { Stats } from './filesystem';
|
|
32
|
+
export { ToolCalls } from './toolcalls';
|
|
33
|
+
export type { ToolCall, ToolCallStats } from './toolcalls';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ToolCalls = exports.Filesystem = exports.KvStore = exports.AgentFS = void 0;
|
|
4
|
+
const database_1 = require("@tursodatabase/database");
|
|
5
|
+
const kvstore_1 = require("./kvstore");
|
|
6
|
+
const filesystem_1 = require("./filesystem");
|
|
7
|
+
const toolcalls_1 = require("./toolcalls");
|
|
8
|
+
class AgentFS {
|
|
9
|
+
/**
|
|
10
|
+
* Private constructor - use AgentFS.create() instead
|
|
11
|
+
*/
|
|
12
|
+
constructor(db, kv, fs, tools) {
|
|
13
|
+
this.db = db;
|
|
14
|
+
this.kv = kv;
|
|
15
|
+
this.fs = fs;
|
|
16
|
+
this.tools = tools;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Create a new AgentFS instance (async factory method)
|
|
20
|
+
* @param dbPath Path to the database file (defaults to ':memory:')
|
|
21
|
+
* @returns Fully initialized AgentFS instance
|
|
22
|
+
*/
|
|
23
|
+
static async create(dbPath = ':memory:') {
|
|
24
|
+
const db = new database_1.Database(dbPath);
|
|
25
|
+
// Connect to the database to ensure it's created
|
|
26
|
+
await db.connect();
|
|
27
|
+
// Create subsystems
|
|
28
|
+
const kv = new kvstore_1.KvStore(db);
|
|
29
|
+
const fs = new filesystem_1.Filesystem(db);
|
|
30
|
+
const tools = new toolcalls_1.ToolCalls(db);
|
|
31
|
+
// Wait for all subsystems to initialize
|
|
32
|
+
await kv.ready();
|
|
33
|
+
await fs.ready();
|
|
34
|
+
await tools.ready();
|
|
35
|
+
// Return fully initialized instance
|
|
36
|
+
return new AgentFS(db, kv, fs, tools);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get the underlying Database instance
|
|
40
|
+
*/
|
|
41
|
+
getDatabase() {
|
|
42
|
+
return this.db;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Close the database connection
|
|
46
|
+
*/
|
|
47
|
+
async close() {
|
|
48
|
+
await this.db.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.AgentFS = AgentFS;
|
|
52
|
+
var kvstore_2 = require("./kvstore");
|
|
53
|
+
Object.defineProperty(exports, "KvStore", { enumerable: true, get: function () { return kvstore_2.KvStore; } });
|
|
54
|
+
var filesystem_2 = require("./filesystem");
|
|
55
|
+
Object.defineProperty(exports, "Filesystem", { enumerable: true, get: function () { return filesystem_2.Filesystem; } });
|
|
56
|
+
var toolcalls_2 = require("./toolcalls");
|
|
57
|
+
Object.defineProperty(exports, "ToolCalls", { enumerable: true, get: function () { return toolcalls_2.ToolCalls; } });
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Database } from '@tursodatabase/database';
|
|
2
|
+
export declare class KvStore {
|
|
3
|
+
private db;
|
|
4
|
+
private initialized;
|
|
5
|
+
constructor(db: Database);
|
|
6
|
+
private initialize;
|
|
7
|
+
set(key: string, value: any): Promise<void>;
|
|
8
|
+
get(key: string): Promise<any>;
|
|
9
|
+
delete(key: string): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Wait for initialization to complete
|
|
12
|
+
*/
|
|
13
|
+
ready(): Promise<void>;
|
|
14
|
+
}
|
package/dist/kvstore.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KvStore = void 0;
|
|
4
|
+
class KvStore {
|
|
5
|
+
constructor(db) {
|
|
6
|
+
this.db = db;
|
|
7
|
+
this.initialized = this.initialize();
|
|
8
|
+
}
|
|
9
|
+
async initialize() {
|
|
10
|
+
// Ensure database is connected
|
|
11
|
+
try {
|
|
12
|
+
await this.db.connect();
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
// Ignore "already connected" errors
|
|
16
|
+
if (!error.message?.includes('already')) {
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// Create the key-value store table if it doesn't exist
|
|
21
|
+
await this.db.exec(`
|
|
22
|
+
CREATE TABLE IF NOT EXISTS kv_store (
|
|
23
|
+
key TEXT PRIMARY KEY,
|
|
24
|
+
value TEXT NOT NULL,
|
|
25
|
+
created_at INTEGER DEFAULT (unixepoch()),
|
|
26
|
+
updated_at INTEGER DEFAULT (unixepoch())
|
|
27
|
+
)
|
|
28
|
+
`);
|
|
29
|
+
// Create index on created_at for potential queries
|
|
30
|
+
await this.db.exec(`
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_kv_store_created_at
|
|
32
|
+
ON kv_store(created_at)
|
|
33
|
+
`);
|
|
34
|
+
}
|
|
35
|
+
async set(key, value) {
|
|
36
|
+
await this.initialized;
|
|
37
|
+
// Serialize the value to JSON
|
|
38
|
+
const serializedValue = JSON.stringify(value);
|
|
39
|
+
// Use prepared statement to insert or update
|
|
40
|
+
const stmt = this.db.prepare(`
|
|
41
|
+
INSERT INTO kv_store (key, value, updated_at)
|
|
42
|
+
VALUES (?, ?, unixepoch())
|
|
43
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
44
|
+
value = excluded.value,
|
|
45
|
+
updated_at = unixepoch()
|
|
46
|
+
`);
|
|
47
|
+
await stmt.run(key, serializedValue);
|
|
48
|
+
}
|
|
49
|
+
async get(key) {
|
|
50
|
+
await this.initialized;
|
|
51
|
+
const stmt = this.db.prepare(`SELECT value FROM kv_store WHERE key = ?`);
|
|
52
|
+
const row = await stmt.get(key);
|
|
53
|
+
if (!row) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
// Deserialize the JSON value
|
|
57
|
+
return JSON.parse(row.value);
|
|
58
|
+
}
|
|
59
|
+
async delete(key) {
|
|
60
|
+
await this.initialized;
|
|
61
|
+
const stmt = this.db.prepare(`DELETE FROM kv_store WHERE key = ?`);
|
|
62
|
+
await stmt.run(key);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Wait for initialization to complete
|
|
66
|
+
*/
|
|
67
|
+
async ready() {
|
|
68
|
+
await this.initialized;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.KvStore = KvStore;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Database } from '@tursodatabase/database';
|
|
2
|
+
export interface ToolCall {
|
|
3
|
+
id: number;
|
|
4
|
+
name: string;
|
|
5
|
+
parameters?: any;
|
|
6
|
+
result?: any;
|
|
7
|
+
error?: string;
|
|
8
|
+
status: 'pending' | 'success' | 'error';
|
|
9
|
+
started_at: number;
|
|
10
|
+
completed_at?: number;
|
|
11
|
+
duration_ms?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface ToolCallStats {
|
|
14
|
+
name: string;
|
|
15
|
+
total_calls: number;
|
|
16
|
+
successful: number;
|
|
17
|
+
failed: number;
|
|
18
|
+
avg_duration_ms: number;
|
|
19
|
+
}
|
|
20
|
+
export declare class ToolCalls {
|
|
21
|
+
private db;
|
|
22
|
+
private initialized;
|
|
23
|
+
constructor(db: Database);
|
|
24
|
+
private initialize;
|
|
25
|
+
/**
|
|
26
|
+
* Start a new tool call and mark it as pending
|
|
27
|
+
* Returns the ID of the created tool call record
|
|
28
|
+
*/
|
|
29
|
+
start(name: string, parameters?: any): Promise<number>;
|
|
30
|
+
/**
|
|
31
|
+
* Mark a tool call as successful
|
|
32
|
+
*/
|
|
33
|
+
success(id: number, result?: any): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Mark a tool call as failed
|
|
36
|
+
*/
|
|
37
|
+
error(id: number, error: string): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Record a completed tool call
|
|
40
|
+
* Either result or error should be provided, not both
|
|
41
|
+
* Returns the ID of the created tool call record
|
|
42
|
+
*/
|
|
43
|
+
record(name: string, started_at: number, completed_at: number, parameters?: any, result?: any, error?: string): Promise<number>;
|
|
44
|
+
/**
|
|
45
|
+
* Get a specific tool call by ID
|
|
46
|
+
*/
|
|
47
|
+
get(id: number): Promise<ToolCall | undefined>;
|
|
48
|
+
/**
|
|
49
|
+
* Query tool calls by name
|
|
50
|
+
*/
|
|
51
|
+
getByName(name: string, limit?: number): Promise<ToolCall[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Query recent tool calls
|
|
54
|
+
*/
|
|
55
|
+
getRecent(since: number, limit?: number): Promise<ToolCall[]>;
|
|
56
|
+
/**
|
|
57
|
+
* Get performance statistics for all tools
|
|
58
|
+
* Only includes completed calls (success or failed), not pending ones
|
|
59
|
+
*/
|
|
60
|
+
getStats(): Promise<ToolCallStats[]>;
|
|
61
|
+
/**
|
|
62
|
+
* Helper to convert database row to ToolCall object
|
|
63
|
+
*/
|
|
64
|
+
private rowToToolCall;
|
|
65
|
+
/**
|
|
66
|
+
* Wait for initialization to complete
|
|
67
|
+
*/
|
|
68
|
+
ready(): Promise<void>;
|
|
69
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ToolCalls = void 0;
|
|
4
|
+
class ToolCalls {
|
|
5
|
+
constructor(db) {
|
|
6
|
+
this.db = db;
|
|
7
|
+
this.initialized = this.initialize();
|
|
8
|
+
}
|
|
9
|
+
async initialize() {
|
|
10
|
+
// Ensure database is connected
|
|
11
|
+
try {
|
|
12
|
+
await this.db.connect();
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
// Ignore "already connected" errors
|
|
16
|
+
if (!error.message?.includes('already')) {
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// Create the tool_calls table
|
|
21
|
+
await this.db.exec(`
|
|
22
|
+
CREATE TABLE IF NOT EXISTS tool_calls (
|
|
23
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
24
|
+
name TEXT NOT NULL,
|
|
25
|
+
parameters TEXT,
|
|
26
|
+
result TEXT,
|
|
27
|
+
error TEXT,
|
|
28
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
29
|
+
started_at INTEGER NOT NULL,
|
|
30
|
+
completed_at INTEGER,
|
|
31
|
+
duration_ms INTEGER
|
|
32
|
+
)
|
|
33
|
+
`);
|
|
34
|
+
// Create indexes for efficient queries
|
|
35
|
+
await this.db.exec(`
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_name
|
|
37
|
+
ON tool_calls(name)
|
|
38
|
+
`);
|
|
39
|
+
await this.db.exec(`
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_started_at
|
|
41
|
+
ON tool_calls(started_at)
|
|
42
|
+
`);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Start a new tool call and mark it as pending
|
|
46
|
+
* Returns the ID of the created tool call record
|
|
47
|
+
*/
|
|
48
|
+
async start(name, parameters) {
|
|
49
|
+
await this.initialized;
|
|
50
|
+
const serializedParams = parameters !== undefined ? JSON.stringify(parameters) : null;
|
|
51
|
+
const started_at = Math.floor(Date.now() / 1000);
|
|
52
|
+
const stmt = this.db.prepare(`
|
|
53
|
+
INSERT INTO tool_calls (name, parameters, status, started_at)
|
|
54
|
+
VALUES (?, ?, 'pending', ?)
|
|
55
|
+
`);
|
|
56
|
+
const result = await stmt.run(name, serializedParams, started_at);
|
|
57
|
+
return Number(result.lastInsertRowid);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Mark a tool call as successful
|
|
61
|
+
*/
|
|
62
|
+
async success(id, result) {
|
|
63
|
+
await this.initialized;
|
|
64
|
+
const serializedResult = result !== undefined ? JSON.stringify(result) : null;
|
|
65
|
+
const completed_at = Math.floor(Date.now() / 1000);
|
|
66
|
+
// Get the started_at time to calculate duration
|
|
67
|
+
const getStmt = this.db.prepare('SELECT started_at FROM tool_calls WHERE id = ?');
|
|
68
|
+
const row = await getStmt.get(id);
|
|
69
|
+
if (!row) {
|
|
70
|
+
throw new Error(`Tool call with ID ${id} not found`);
|
|
71
|
+
}
|
|
72
|
+
const duration_ms = (completed_at - row.started_at) * 1000;
|
|
73
|
+
const updateStmt = this.db.prepare(`
|
|
74
|
+
UPDATE tool_calls
|
|
75
|
+
SET status = 'success', result = ?, completed_at = ?, duration_ms = ?
|
|
76
|
+
WHERE id = ?
|
|
77
|
+
`);
|
|
78
|
+
await updateStmt.run(serializedResult, completed_at, duration_ms, id);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Mark a tool call as failed
|
|
82
|
+
*/
|
|
83
|
+
async error(id, error) {
|
|
84
|
+
await this.initialized;
|
|
85
|
+
const completed_at = Math.floor(Date.now() / 1000);
|
|
86
|
+
// Get the started_at time to calculate duration
|
|
87
|
+
const getStmt = this.db.prepare('SELECT started_at FROM tool_calls WHERE id = ?');
|
|
88
|
+
const row = await getStmt.get(id);
|
|
89
|
+
if (!row) {
|
|
90
|
+
throw new Error(`Tool call with ID ${id} not found`);
|
|
91
|
+
}
|
|
92
|
+
const duration_ms = (completed_at - row.started_at) * 1000;
|
|
93
|
+
const updateStmt = this.db.prepare(`
|
|
94
|
+
UPDATE tool_calls
|
|
95
|
+
SET status = 'error', error = ?, completed_at = ?, duration_ms = ?
|
|
96
|
+
WHERE id = ?
|
|
97
|
+
`);
|
|
98
|
+
await updateStmt.run(error, completed_at, duration_ms, id);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Record a completed tool call
|
|
102
|
+
* Either result or error should be provided, not both
|
|
103
|
+
* Returns the ID of the created tool call record
|
|
104
|
+
*/
|
|
105
|
+
async record(name, started_at, completed_at, parameters, result, error) {
|
|
106
|
+
await this.initialized;
|
|
107
|
+
const serializedParams = parameters !== undefined ? JSON.stringify(parameters) : null;
|
|
108
|
+
const serializedResult = result !== undefined ? JSON.stringify(result) : null;
|
|
109
|
+
const duration_ms = (completed_at - started_at) * 1000;
|
|
110
|
+
const status = error ? 'error' : 'success';
|
|
111
|
+
const stmt = this.db.prepare(`
|
|
112
|
+
INSERT INTO tool_calls (name, parameters, result, error, status, started_at, completed_at, duration_ms)
|
|
113
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
114
|
+
`);
|
|
115
|
+
const result_row = await stmt.run(name, serializedParams, serializedResult, error || null, status, started_at, completed_at, duration_ms);
|
|
116
|
+
return Number(result_row.lastInsertRowid);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get a specific tool call by ID
|
|
120
|
+
*/
|
|
121
|
+
async get(id) {
|
|
122
|
+
await this.initialized;
|
|
123
|
+
const stmt = this.db.prepare(`
|
|
124
|
+
SELECT * FROM tool_calls WHERE id = ?
|
|
125
|
+
`);
|
|
126
|
+
const row = await stmt.get(id);
|
|
127
|
+
if (!row) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
return this.rowToToolCall(row);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Query tool calls by name
|
|
134
|
+
*/
|
|
135
|
+
async getByName(name, limit) {
|
|
136
|
+
await this.initialized;
|
|
137
|
+
const limitClause = limit !== undefined ? `LIMIT ${limit}` : '';
|
|
138
|
+
const stmt = this.db.prepare(`
|
|
139
|
+
SELECT * FROM tool_calls
|
|
140
|
+
WHERE name = ?
|
|
141
|
+
ORDER BY started_at DESC
|
|
142
|
+
${limitClause}
|
|
143
|
+
`);
|
|
144
|
+
const rows = await stmt.all(name);
|
|
145
|
+
return rows.map(row => this.rowToToolCall(row));
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Query recent tool calls
|
|
149
|
+
*/
|
|
150
|
+
async getRecent(since, limit) {
|
|
151
|
+
await this.initialized;
|
|
152
|
+
const limitClause = limit !== undefined ? `LIMIT ${limit}` : '';
|
|
153
|
+
const stmt = this.db.prepare(`
|
|
154
|
+
SELECT * FROM tool_calls
|
|
155
|
+
WHERE started_at > ?
|
|
156
|
+
ORDER BY started_at DESC
|
|
157
|
+
${limitClause}
|
|
158
|
+
`);
|
|
159
|
+
const rows = await stmt.all(since);
|
|
160
|
+
return rows.map(row => this.rowToToolCall(row));
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Get performance statistics for all tools
|
|
164
|
+
* Only includes completed calls (success or failed), not pending ones
|
|
165
|
+
*/
|
|
166
|
+
async getStats() {
|
|
167
|
+
await this.initialized;
|
|
168
|
+
const stmt = this.db.prepare(`
|
|
169
|
+
SELECT
|
|
170
|
+
name,
|
|
171
|
+
COUNT(*) as total_calls,
|
|
172
|
+
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successful,
|
|
173
|
+
SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as failed,
|
|
174
|
+
AVG(duration_ms) as avg_duration_ms
|
|
175
|
+
FROM tool_calls
|
|
176
|
+
WHERE status != 'pending'
|
|
177
|
+
GROUP BY name
|
|
178
|
+
ORDER BY total_calls DESC
|
|
179
|
+
`);
|
|
180
|
+
const rows = await stmt.all();
|
|
181
|
+
return rows.map(row => ({
|
|
182
|
+
name: row.name,
|
|
183
|
+
total_calls: row.total_calls,
|
|
184
|
+
successful: row.successful,
|
|
185
|
+
failed: row.failed,
|
|
186
|
+
avg_duration_ms: row.avg_duration_ms || 0,
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Helper to convert database row to ToolCall object
|
|
191
|
+
*/
|
|
192
|
+
rowToToolCall(row) {
|
|
193
|
+
return {
|
|
194
|
+
id: row.id,
|
|
195
|
+
name: row.name,
|
|
196
|
+
parameters: row.parameters !== null ? JSON.parse(row.parameters) : undefined,
|
|
197
|
+
result: row.result !== null ? JSON.parse(row.result) : undefined,
|
|
198
|
+
error: row.error !== null ? row.error : undefined,
|
|
199
|
+
status: row.status,
|
|
200
|
+
started_at: row.started_at,
|
|
201
|
+
completed_at: row.completed_at !== null ? row.completed_at : undefined,
|
|
202
|
+
duration_ms: row.duration_ms !== null ? row.duration_ms : undefined,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Wait for initialization to complete
|
|
207
|
+
*/
|
|
208
|
+
async ready() {
|
|
209
|
+
await this.initialized;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
exports.ToolCalls = ToolCalls;
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentfs-sdk",
|
|
3
|
+
"version": "0.1.0-pre.1",
|
|
4
|
+
"description": "AgentFS SDK",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"packageManager": "npm@10.9.0",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"watch": "tsc --watch",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:watch": "vitest watch",
|
|
13
|
+
"test:ui": "vitest --ui",
|
|
14
|
+
"test:coverage": "vitest run --coverage",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"ai",
|
|
19
|
+
"agent",
|
|
20
|
+
"turso",
|
|
21
|
+
"sqlite",
|
|
22
|
+
"key-value",
|
|
23
|
+
"filesystem"
|
|
24
|
+
],
|
|
25
|
+
"author": "",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^20.0.0",
|
|
29
|
+
"@vitest/ui": "^4.0.1",
|
|
30
|
+
"typescript": "^5.3.0",
|
|
31
|
+
"vitest": "^4.0.1"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@tursodatabase/database": "^0.3.2"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
]
|
|
39
|
+
}
|