@intella/sdk 0.0.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/README.md +492 -0
- package/examples/claude-code/README.md +178 -0
- package/examples/claude-code/advanced-config.ts +55 -0
- package/examples/claude-code/basic-usage.ts +56 -0
- package/examples/claude-code/model-comparison.ts +50 -0
- package/examples/claude-code/orchestration.ts +70 -0
- package/examples/claude-code/streaming.ts +69 -0
- package/examples/claude-code/tsconfig.json +19 -0
- package/examples/code-extractor/README.md +77 -0
- package/examples/code-extractor/example.ts +145 -0
- package/examples/filesystem/basic-usage.ts +84 -0
- package/examples/integrated-task/README.md +68 -0
- package/examples/integrated-task/integrated-usage.ts +193 -0
- package/examples/integrated-task/simple-example.ts +51 -0
- package/examples/integrated-task/tsconfig.json +19 -0
- package/examples/sandbox/basic-usage.ts +173 -0
- package/package.json +56 -0
- package/src/agent-manager.ts +104 -0
- package/src/agents/base-agent.ts +166 -0
- package/src/agents/claude-agent.ts +77 -0
- package/src/agents/codex-agent.ts +72 -0
- package/src/agents/intella-lite-agent.ts +55 -0
- package/src/agents/opencode-agent.ts +45 -0
- package/src/filesystem/agentfs-provider.ts +328 -0
- package/src/filesystem/base-provider.ts +98 -0
- package/src/filesystem/index.ts +5 -0
- package/src/filesystem/memory-provider.ts +267 -0
- package/src/filesystem-manager.ts +213 -0
- package/src/index.ts +66 -0
- package/src/orchestrator.ts +177 -0
- package/src/sandbox/base-provider.ts +184 -0
- package/src/sandbox/daytona-provider.ts +462 -0
- package/src/sandbox/e2b-provider.ts +419 -0
- package/src/sandbox/modal-provider.ts +597 -0
- package/src/sandbox-manager.ts +175 -0
- package/src/sdk.ts +401 -0
- package/src/types.ts +451 -0
- package/src/utils/code-extractor.ts +194 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
// Note: agentfs-sdk needs to be installed separately
|
|
2
|
+
// npm install agentfs-sdk
|
|
3
|
+
import { AgentFS } from 'agentfs-sdk';
|
|
4
|
+
import type {
|
|
5
|
+
FilesystemConfig,
|
|
6
|
+
FileStats,
|
|
7
|
+
DirectoryEntry,
|
|
8
|
+
ToolCallMetadata,
|
|
9
|
+
} from '../types.js';
|
|
10
|
+
import { BaseFilesystemProvider } from './base-provider.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* AgentFS filesystem provider
|
|
14
|
+
* Uses Turso's AgentFS SDK for SQLite-based agent filesystem
|
|
15
|
+
*/
|
|
16
|
+
export class AgentFSProvider extends BaseFilesystemProvider {
|
|
17
|
+
private agentFS: any = null; // AgentFS instance (typed as any due to SDK)
|
|
18
|
+
|
|
19
|
+
constructor() {
|
|
20
|
+
super('agentfs');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Initialize AgentFS filesystem
|
|
25
|
+
*/
|
|
26
|
+
async initialize(config?: FilesystemConfig): Promise<void> {
|
|
27
|
+
this.config = { ...this.config, ...config };
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Initialize AgentFS based on configuration
|
|
31
|
+
if (this.config.dbPath) {
|
|
32
|
+
// Use explicit database path
|
|
33
|
+
this.agentFS = await AgentFS.open({ path: this.config.dbPath });
|
|
34
|
+
} else if (this.config.agentId) {
|
|
35
|
+
// Use agent ID (creates .agentfs/{agentId}.db)
|
|
36
|
+
this.agentFS = await AgentFS.open({ id: this.config.agentId });
|
|
37
|
+
} else {
|
|
38
|
+
throw new Error('No agent ID or database path provided');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.initialized = true;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new Error(`Failed to initialize AgentFS: ${error instanceof Error ? error.message : String(error)}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Close AgentFS filesystem
|
|
49
|
+
*/
|
|
50
|
+
async close(): Promise<void> {
|
|
51
|
+
if (this.agentFS) {
|
|
52
|
+
// AgentFS doesn't expose a close method in the SDK,
|
|
53
|
+
// but we can clear the reference
|
|
54
|
+
this.agentFS = null;
|
|
55
|
+
this.initialized = false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Filesystem operations
|
|
60
|
+
async readFile(path: string): Promise<Buffer | string> {
|
|
61
|
+
this.ensureInitialized();
|
|
62
|
+
const normalizedPath = this.normalizePath(path);
|
|
63
|
+
try {
|
|
64
|
+
const content = await this.agentFS.fs.readFile(normalizedPath);
|
|
65
|
+
// AgentFS may return string or Buffer, return as-is
|
|
66
|
+
return content;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
throw new Error(`Failed to read file ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async writeFile(path: string, data: Buffer | string): Promise<void> {
|
|
73
|
+
this.ensureInitialized();
|
|
74
|
+
const normalizedPath = this.normalizePath(path);
|
|
75
|
+
try {
|
|
76
|
+
await this.agentFS.fs.writeFile(normalizedPath, data);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
throw new Error(`Failed to write file ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async appendFile(path: string, data: Buffer | string): Promise<void> {
|
|
83
|
+
this.ensureInitialized();
|
|
84
|
+
const normalizedPath = this.normalizePath(path);
|
|
85
|
+
try {
|
|
86
|
+
// Read existing content
|
|
87
|
+
let existingContent = '';
|
|
88
|
+
try {
|
|
89
|
+
const existing = await this.agentFS.fs.readFile(normalizedPath);
|
|
90
|
+
existingContent = existing instanceof Buffer ? existing.toString() : existing;
|
|
91
|
+
} catch {
|
|
92
|
+
// File doesn't exist, start with empty string
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Write combined content
|
|
96
|
+
const newContent = existingContent + (data instanceof Buffer ? data.toString() : data);
|
|
97
|
+
await this.agentFS.fs.writeFile(normalizedPath, newContent);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
throw new Error(`Failed to append to file ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async exists(path: string): Promise<boolean> {
|
|
104
|
+
this.ensureInitialized();
|
|
105
|
+
const normalizedPath = this.normalizePath(path);
|
|
106
|
+
try {
|
|
107
|
+
await this.agentFS.fs.stat(normalizedPath);
|
|
108
|
+
return true;
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async stat(path: string): Promise<FileStats> {
|
|
115
|
+
this.ensureInitialized();
|
|
116
|
+
const normalizedPath = this.normalizePath(path);
|
|
117
|
+
try {
|
|
118
|
+
const stats = await this.agentFS.fs.stat(normalizedPath);
|
|
119
|
+
return {
|
|
120
|
+
size: stats.size || 0,
|
|
121
|
+
isDirectory: stats.isDirectory || false,
|
|
122
|
+
isFile: stats.isFile || false,
|
|
123
|
+
createdAt: stats.createdAt,
|
|
124
|
+
modifiedAt: stats.modifiedAt,
|
|
125
|
+
};
|
|
126
|
+
} catch (error) {
|
|
127
|
+
throw new Error(`Failed to stat ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async readdir(path: string): Promise<DirectoryEntry[]> {
|
|
132
|
+
this.ensureInitialized();
|
|
133
|
+
const normalizedPath = this.normalizePath(path);
|
|
134
|
+
try {
|
|
135
|
+
const entries = await this.agentFS.fs.readdir(normalizedPath);
|
|
136
|
+
const result: DirectoryEntry[] = [];
|
|
137
|
+
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
const entryPath = normalizedPath === '' ? entry : `${normalizedPath}/${entry}`;
|
|
140
|
+
const stats = await this.stat(entryPath);
|
|
141
|
+
result.push({
|
|
142
|
+
name: entry,
|
|
143
|
+
path: entryPath,
|
|
144
|
+
stats,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return result;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
throw new Error(`Failed to read directory ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async mkdir(path: string, recursive = false): Promise<void> {
|
|
155
|
+
this.ensureInitialized();
|
|
156
|
+
const normalizedPath = this.normalizePath(path);
|
|
157
|
+
try {
|
|
158
|
+
// AgentFS may support recursive mkdir through the SDK
|
|
159
|
+
// If not, we'll create parent directories manually if recursive is true
|
|
160
|
+
if (recursive) {
|
|
161
|
+
const parts = normalizedPath.split('/');
|
|
162
|
+
let currentPath = '';
|
|
163
|
+
for (const part of parts) {
|
|
164
|
+
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
|
165
|
+
try {
|
|
166
|
+
await this.agentFS.fs.mkdir(currentPath);
|
|
167
|
+
} catch (err: any) {
|
|
168
|
+
// Ignore if directory already exists
|
|
169
|
+
if (!err?.message?.includes('exists')) {
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
await this.agentFS.fs.mkdir(normalizedPath);
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
throw new Error(`Failed to create directory ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async unlink(path: string, recursive = false): Promise<void> {
|
|
183
|
+
this.ensureInitialized();
|
|
184
|
+
const normalizedPath = this.normalizePath(path);
|
|
185
|
+
try {
|
|
186
|
+
const stats = await this.stat(normalizedPath);
|
|
187
|
+
if (stats.isDirectory) {
|
|
188
|
+
if (recursive) {
|
|
189
|
+
const entries = await this.readdir(normalizedPath);
|
|
190
|
+
for (const entry of entries) {
|
|
191
|
+
await this.unlink(entry.path, true);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
await this.agentFS.fs.rmdir(normalizedPath);
|
|
195
|
+
} else {
|
|
196
|
+
await this.agentFS.fs.unlink(normalizedPath);
|
|
197
|
+
}
|
|
198
|
+
} catch (error) {
|
|
199
|
+
throw new Error(`Failed to delete ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async copy(src: string, dest: string): Promise<void> {
|
|
204
|
+
this.ensureInitialized();
|
|
205
|
+
const normalizedSrc = this.normalizePath(src);
|
|
206
|
+
const normalizedDest = this.normalizePath(dest);
|
|
207
|
+
try {
|
|
208
|
+
const content = await this.readFile(normalizedSrc);
|
|
209
|
+
await this.writeFile(normalizedDest, content);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
throw new Error(`Failed to copy ${src} to ${dest}: ${error instanceof Error ? error.message : String(error)}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async move(src: string, dest: string): Promise<void> {
|
|
216
|
+
this.ensureInitialized();
|
|
217
|
+
const normalizedSrc = this.normalizePath(src);
|
|
218
|
+
const normalizedDest = this.normalizePath(dest);
|
|
219
|
+
try {
|
|
220
|
+
await this.copy(normalizedSrc, normalizedDest);
|
|
221
|
+
await this.unlink(normalizedSrc);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
throw new Error(`Failed to move ${src} to ${dest}: ${error instanceof Error ? error.message : String(error)}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Key-value operations
|
|
228
|
+
async get(key: string): Promise<unknown> {
|
|
229
|
+
this.ensureInitialized();
|
|
230
|
+
try {
|
|
231
|
+
return await this.agentFS.kv.get(key);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
throw new Error(`Failed to get key ${key}: ${error instanceof Error ? error.message : String(error)}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async set(key: string, value: unknown): Promise<void> {
|
|
238
|
+
this.ensureInitialized();
|
|
239
|
+
try {
|
|
240
|
+
await this.agentFS.kv.set(key, value);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
throw new Error(`Failed to set key ${key}: ${error instanceof Error ? error.message : String(error)}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async delete(key: string): Promise<void> {
|
|
247
|
+
this.ensureInitialized();
|
|
248
|
+
try {
|
|
249
|
+
await this.agentFS.kv.delete(key);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
throw new Error(`Failed to delete key ${key}: ${error instanceof Error ? error.message : String(error)}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async listKeys(prefix?: string): Promise<string[]> {
|
|
256
|
+
this.ensureInitialized();
|
|
257
|
+
try {
|
|
258
|
+
// AgentFS may have a list method or we need to query directly
|
|
259
|
+
// For now, we'll use a method that might exist or throw a not implemented error
|
|
260
|
+
if (this.agentFS.kv.list) {
|
|
261
|
+
return await this.agentFS.kv.list(prefix);
|
|
262
|
+
}
|
|
263
|
+
// If list method doesn't exist, we'll need to implement it differently
|
|
264
|
+
// This might require direct SQLite access which isn't exposed by the SDK
|
|
265
|
+
throw new Error('listKeys is not directly supported by AgentFS SDK. Consider using SQL queries directly.');
|
|
266
|
+
} catch (error) {
|
|
267
|
+
throw new Error(`Failed to list keys: ${error instanceof Error ? error.message : String(error)}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async hasKey(key: string): Promise<boolean> {
|
|
272
|
+
this.ensureInitialized();
|
|
273
|
+
try {
|
|
274
|
+
const value = await this.get(key);
|
|
275
|
+
return value !== null && value !== undefined;
|
|
276
|
+
} catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Tool call tracking
|
|
282
|
+
async recordToolCall(metadata: ToolCallMetadata): Promise<void> {
|
|
283
|
+
this.ensureInitialized();
|
|
284
|
+
try {
|
|
285
|
+
await this.agentFS.tools.record(
|
|
286
|
+
metadata.tool,
|
|
287
|
+
metadata.startedAt,
|
|
288
|
+
metadata.endedAt,
|
|
289
|
+
metadata.input || {},
|
|
290
|
+
metadata.output || {}
|
|
291
|
+
);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
throw new Error(`Failed to record tool call: ${error instanceof Error ? error.message : String(error)}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async getToolCallHistory(filter?: {
|
|
298
|
+
tool?: string;
|
|
299
|
+
status?: ToolCallMetadata['status'];
|
|
300
|
+
since?: number;
|
|
301
|
+
until?: number;
|
|
302
|
+
limit?: number;
|
|
303
|
+
}): Promise<ToolCallMetadata[]> {
|
|
304
|
+
this.ensureInitialized();
|
|
305
|
+
try {
|
|
306
|
+
// AgentFS tools API might have a query method
|
|
307
|
+
// If not, we'll need to query the SQLite database directly
|
|
308
|
+
if (this.agentFS.tools.query) {
|
|
309
|
+
const results = await this.agentFS.tools.query(filter);
|
|
310
|
+
return results.map((r: any) => ({
|
|
311
|
+
tool: r.tool,
|
|
312
|
+
startedAt: r.startedAt,
|
|
313
|
+
endedAt: r.endedAt,
|
|
314
|
+
input: r.input,
|
|
315
|
+
output: r.output,
|
|
316
|
+
status: r.status,
|
|
317
|
+
error: r.error,
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
// If query method doesn't exist, return empty array for now
|
|
321
|
+
// In a real implementation, you might want to access the database directly
|
|
322
|
+
throw new Error('getToolCallHistory is not directly supported by AgentFS SDK. Consider using SQL queries directly.');
|
|
323
|
+
} catch (error) {
|
|
324
|
+
throw new Error(`Failed to get tool call history: ${error instanceof Error ? error.message : String(error)}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FilesystemProviderType,
|
|
3
|
+
FilesystemConfig,
|
|
4
|
+
FileStats,
|
|
5
|
+
DirectoryEntry,
|
|
6
|
+
ToolCallMetadata,
|
|
7
|
+
IFilesystemProvider,
|
|
8
|
+
} from '../types.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Base filesystem provider
|
|
12
|
+
* Abstract implementation that provides common functionality
|
|
13
|
+
*/
|
|
14
|
+
export abstract class BaseFilesystemProvider implements IFilesystemProvider {
|
|
15
|
+
protected initialized = false;
|
|
16
|
+
protected config: FilesystemConfig = {};
|
|
17
|
+
public readonly type: FilesystemProviderType;
|
|
18
|
+
|
|
19
|
+
constructor(type: FilesystemProviderType) {
|
|
20
|
+
this.type = type;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Initialize the filesystem
|
|
25
|
+
*/
|
|
26
|
+
abstract initialize(config?: FilesystemConfig): Promise<void>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if filesystem is initialized
|
|
30
|
+
*/
|
|
31
|
+
isInitialized(): boolean {
|
|
32
|
+
return this.initialized;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Close/cleanup the filesystem
|
|
37
|
+
*/
|
|
38
|
+
abstract close(): Promise<void>;
|
|
39
|
+
|
|
40
|
+
// Filesystem operations
|
|
41
|
+
abstract readFile(path: string): Promise<Buffer | string>;
|
|
42
|
+
abstract writeFile(path: string, data: Buffer | string): Promise<void>;
|
|
43
|
+
abstract appendFile(path: string, data: Buffer | string): Promise<void>;
|
|
44
|
+
abstract exists(path: string): Promise<boolean>;
|
|
45
|
+
abstract stat(path: string): Promise<FileStats>;
|
|
46
|
+
abstract readdir(path: string): Promise<DirectoryEntry[]>;
|
|
47
|
+
abstract mkdir(path: string, recursive?: boolean): Promise<void>;
|
|
48
|
+
abstract unlink(path: string, recursive?: boolean): Promise<void>;
|
|
49
|
+
abstract copy(src: string, dest: string): Promise<void>;
|
|
50
|
+
abstract move(src: string, dest: string): Promise<void>;
|
|
51
|
+
|
|
52
|
+
// Key-value operations
|
|
53
|
+
abstract get(key: string): Promise<unknown>;
|
|
54
|
+
abstract set(key: string, value: unknown): Promise<void>;
|
|
55
|
+
abstract delete(key: string): Promise<void>;
|
|
56
|
+
abstract listKeys(prefix?: string): Promise<string[]>;
|
|
57
|
+
abstract hasKey(key: string): Promise<boolean>;
|
|
58
|
+
|
|
59
|
+
// Tool call tracking
|
|
60
|
+
abstract recordToolCall(metadata: ToolCallMetadata): Promise<void>;
|
|
61
|
+
abstract getToolCallHistory(filter?: {
|
|
62
|
+
tool?: string;
|
|
63
|
+
status?: ToolCallMetadata['status'];
|
|
64
|
+
since?: number;
|
|
65
|
+
until?: number;
|
|
66
|
+
limit?: number;
|
|
67
|
+
}): Promise<ToolCallMetadata[]>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Normalize path (remove leading/trailing slashes, handle ..)
|
|
71
|
+
*/
|
|
72
|
+
protected normalizePath(path: string): string {
|
|
73
|
+
return path
|
|
74
|
+
.split('/')
|
|
75
|
+
.filter((segment) => segment !== '' && segment !== '.')
|
|
76
|
+
.reduce((acc, segment) => {
|
|
77
|
+
if (segment === '..') {
|
|
78
|
+
acc.pop();
|
|
79
|
+
} else {
|
|
80
|
+
acc.push(segment);
|
|
81
|
+
}
|
|
82
|
+
return acc;
|
|
83
|
+
}, [] as string[])
|
|
84
|
+
.join('/');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Ensure filesystem is initialized before operation
|
|
89
|
+
*/
|
|
90
|
+
protected ensureInitialized(): void {
|
|
91
|
+
if (!this.initialized) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Filesystem provider ${this.type} is not initialized. Call initialize() first.`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FilesystemConfig,
|
|
3
|
+
FileStats,
|
|
4
|
+
DirectoryEntry,
|
|
5
|
+
ToolCallMetadata,
|
|
6
|
+
} from '../types.js';
|
|
7
|
+
import { BaseFilesystemProvider } from './base-provider.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* In-memory filesystem provider
|
|
11
|
+
* Useful for testing and ephemeral operations
|
|
12
|
+
*/
|
|
13
|
+
export class MemoryFilesystemProvider extends BaseFilesystemProvider {
|
|
14
|
+
private fs: Map<string, Buffer | string> = new Map();
|
|
15
|
+
private kv: Map<string, unknown> = new Map();
|
|
16
|
+
private toolCalls: ToolCallMetadata[] = [];
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
super('memory');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Initialize memory filesystem (no-op for memory provider)
|
|
24
|
+
*/
|
|
25
|
+
async initialize(config?: FilesystemConfig): Promise<void> {
|
|
26
|
+
this.config = { ...this.config, ...config };
|
|
27
|
+
this.initialized = true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Close memory filesystem (clears all data)
|
|
32
|
+
*/
|
|
33
|
+
async close(): Promise<void> {
|
|
34
|
+
this.fs.clear();
|
|
35
|
+
this.kv.clear();
|
|
36
|
+
this.toolCalls = [];
|
|
37
|
+
this.initialized = false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Filesystem operations
|
|
41
|
+
async readFile(path: string): Promise<Buffer | string> {
|
|
42
|
+
this.ensureInitialized();
|
|
43
|
+
const normalizedPath = this.normalizePath(path);
|
|
44
|
+
const content = this.fs.get(normalizedPath);
|
|
45
|
+
if (content === undefined) {
|
|
46
|
+
throw new Error(`File not found: ${path}`);
|
|
47
|
+
}
|
|
48
|
+
return content;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async writeFile(path: string, data: Buffer | string): Promise<void> {
|
|
52
|
+
this.ensureInitialized();
|
|
53
|
+
const normalizedPath = this.normalizePath(path);
|
|
54
|
+
// Ensure parent directory exists
|
|
55
|
+
const parentDir = normalizedPath.split('/').slice(0, -1).join('/');
|
|
56
|
+
if (parentDir && !this.fs.has(parentDir)) {
|
|
57
|
+
this.fs.set(parentDir, Buffer.from('')); // Mark as directory
|
|
58
|
+
}
|
|
59
|
+
this.fs.set(normalizedPath, typeof data === 'string' ? data : data);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async appendFile(path: string, data: Buffer | string): Promise<void> {
|
|
63
|
+
this.ensureInitialized();
|
|
64
|
+
const normalizedPath = this.normalizePath(path);
|
|
65
|
+
const existing = this.fs.get(normalizedPath);
|
|
66
|
+
const existingContent: string = existing instanceof Buffer
|
|
67
|
+
? existing.toString()
|
|
68
|
+
: (typeof existing === 'string' ? existing : '');
|
|
69
|
+
const newData: string = data instanceof Buffer ? data.toString() : (typeof data === 'string' ? data : '');
|
|
70
|
+
this.fs.set(normalizedPath, existingContent + newData);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async exists(path: string): Promise<boolean> {
|
|
74
|
+
this.ensureInitialized();
|
|
75
|
+
const normalizedPath = this.normalizePath(path);
|
|
76
|
+
return this.fs.has(normalizedPath);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async stat(path: string): Promise<FileStats> {
|
|
80
|
+
this.ensureInitialized();
|
|
81
|
+
const normalizedPath = this.normalizePath(path);
|
|
82
|
+
const content = this.fs.get(normalizedPath);
|
|
83
|
+
if (content === undefined) {
|
|
84
|
+
throw new Error(`Path not found: ${path}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const isDirectory = typeof content === 'string' && content === '';
|
|
88
|
+
const isFile = !isDirectory;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
size: isFile ? (content instanceof Buffer ? content.length : content.length) : 0,
|
|
92
|
+
isDirectory,
|
|
93
|
+
isFile,
|
|
94
|
+
createdAt: Date.now() / 1000,
|
|
95
|
+
modifiedAt: Date.now() / 1000,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async readdir(path: string): Promise<DirectoryEntry[]> {
|
|
100
|
+
this.ensureInitialized();
|
|
101
|
+
const normalizedPath = this.normalizePath(path);
|
|
102
|
+
const prefix = normalizedPath === '' ? '' : `${normalizedPath}/`;
|
|
103
|
+
const entries: DirectoryEntry[] = [];
|
|
104
|
+
const seen = new Set<string>();
|
|
105
|
+
|
|
106
|
+
for (const [filePath] of this.fs.entries()) {
|
|
107
|
+
if (filePath.startsWith(prefix) || (prefix === '' && !filePath.includes('/'))) {
|
|
108
|
+
const relativePath = filePath.startsWith(prefix) ? filePath.slice(prefix.length) : filePath;
|
|
109
|
+
const parts = relativePath.split('/');
|
|
110
|
+
const name = parts[0];
|
|
111
|
+
const entryPath = prefix === '' ? name : `${prefix}${name}`;
|
|
112
|
+
|
|
113
|
+
if (name && !seen.has(entryPath)) {
|
|
114
|
+
seen.add(entryPath);
|
|
115
|
+
try {
|
|
116
|
+
const stats = await this.stat(entryPath);
|
|
117
|
+
entries.push({
|
|
118
|
+
name,
|
|
119
|
+
path: entryPath,
|
|
120
|
+
stats,
|
|
121
|
+
});
|
|
122
|
+
} catch {
|
|
123
|
+
// Skip if stat fails
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return entries;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async mkdir(path: string, recursive = false): Promise<void> {
|
|
133
|
+
this.ensureInitialized();
|
|
134
|
+
const normalizedPath = this.normalizePath(path);
|
|
135
|
+
|
|
136
|
+
if (recursive) {
|
|
137
|
+
const parts = normalizedPath.split('/');
|
|
138
|
+
let currentPath = '';
|
|
139
|
+
for (const part of parts) {
|
|
140
|
+
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
|
141
|
+
if (!this.fs.has(currentPath)) {
|
|
142
|
+
this.fs.set(currentPath, Buffer.from('')); // Mark as directory
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
if (this.fs.has(normalizedPath)) {
|
|
147
|
+
throw new Error(`Directory already exists: ${path}`);
|
|
148
|
+
}
|
|
149
|
+
this.fs.set(normalizedPath, Buffer.from('')); // Mark as directory
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async unlink(path: string, recursive = false): Promise<void> {
|
|
154
|
+
this.ensureInitialized();
|
|
155
|
+
const normalizedPath = this.normalizePath(path);
|
|
156
|
+
|
|
157
|
+
if (!this.fs.has(normalizedPath)) {
|
|
158
|
+
throw new Error(`Path not found: ${path}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const stats = await this.stat(normalizedPath);
|
|
162
|
+
const prefix = `${normalizedPath}/`;
|
|
163
|
+
|
|
164
|
+
if (stats.isDirectory) {
|
|
165
|
+
if (recursive) {
|
|
166
|
+
// Remove all files/directories under this path
|
|
167
|
+
for (const [filePath] of this.fs.entries()) {
|
|
168
|
+
if (filePath.startsWith(prefix) || filePath === normalizedPath) {
|
|
169
|
+
this.fs.delete(filePath);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
// Check if directory is empty
|
|
174
|
+
const entries = await this.readdir(normalizedPath);
|
|
175
|
+
if (entries.length > 0) {
|
|
176
|
+
throw new Error(`Directory not empty: ${path}`);
|
|
177
|
+
}
|
|
178
|
+
this.fs.delete(normalizedPath);
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
this.fs.delete(normalizedPath);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async copy(src: string, dest: string): Promise<void> {
|
|
186
|
+
this.ensureInitialized();
|
|
187
|
+
const normalizedSrc = this.normalizePath(src);
|
|
188
|
+
const normalizedDest = this.normalizePath(dest);
|
|
189
|
+
const content = this.fs.get(normalizedSrc);
|
|
190
|
+
if (content === undefined) {
|
|
191
|
+
throw new Error(`Source not found: ${src}`);
|
|
192
|
+
}
|
|
193
|
+
this.fs.set(normalizedDest, content);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async move(src: string, dest: string): Promise<void> {
|
|
197
|
+
await this.copy(src, dest);
|
|
198
|
+
await this.unlink(src);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Key-value operations
|
|
202
|
+
async get(key: string): Promise<unknown> {
|
|
203
|
+
this.ensureInitialized();
|
|
204
|
+
return this.kv.get(key);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async set(key: string, value: unknown): Promise<void> {
|
|
208
|
+
this.ensureInitialized();
|
|
209
|
+
this.kv.set(key, value);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async delete(key: string): Promise<void> {
|
|
213
|
+
this.ensureInitialized();
|
|
214
|
+
this.kv.delete(key);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async listKeys(prefix?: string): Promise<string[]> {
|
|
218
|
+
this.ensureInitialized();
|
|
219
|
+
if (!prefix) {
|
|
220
|
+
return Array.from(this.kv.keys());
|
|
221
|
+
}
|
|
222
|
+
return Array.from(this.kv.keys()).filter((key) => key.startsWith(prefix));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async hasKey(key: string): Promise<boolean> {
|
|
226
|
+
this.ensureInitialized();
|
|
227
|
+
return this.kv.has(key);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Tool call tracking
|
|
231
|
+
async recordToolCall(metadata: ToolCallMetadata): Promise<void> {
|
|
232
|
+
this.ensureInitialized();
|
|
233
|
+
this.toolCalls.push(metadata);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async getToolCallHistory(filter?: {
|
|
237
|
+
tool?: string;
|
|
238
|
+
status?: ToolCallMetadata['status'];
|
|
239
|
+
since?: number;
|
|
240
|
+
until?: number;
|
|
241
|
+
limit?: number;
|
|
242
|
+
}): Promise<ToolCallMetadata[]> {
|
|
243
|
+
this.ensureInitialized();
|
|
244
|
+
let results = [...this.toolCalls];
|
|
245
|
+
|
|
246
|
+
if (filter) {
|
|
247
|
+
if (filter.tool) {
|
|
248
|
+
results = results.filter((tc) => tc.tool === filter.tool);
|
|
249
|
+
}
|
|
250
|
+
if (filter.status) {
|
|
251
|
+
results = results.filter((tc) => tc.status === filter.status);
|
|
252
|
+
}
|
|
253
|
+
if (filter.since !== undefined) {
|
|
254
|
+
results = results.filter((tc) => tc.startedAt >= filter.since!);
|
|
255
|
+
}
|
|
256
|
+
if (filter.until !== undefined) {
|
|
257
|
+
results = results.filter((tc) => tc.startedAt <= filter.until!);
|
|
258
|
+
}
|
|
259
|
+
if (filter.limit !== undefined) {
|
|
260
|
+
results = results.slice(0, filter.limit);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return results.reverse(); // Most recent first
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|