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.
@@ -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;
@@ -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
+ }
@@ -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
+ }