@aigne/afs 1.4.0 → 1.11.0-beta
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 +95 -51
- package/dist/index.cjs +346 -0
- package/dist/index.d.cts +300 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +300 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +343 -0
- package/dist/index.mjs.map +1 -0
- package/dist/utils/camelize.cjs +34 -0
- package/dist/utils/camelize.d.cts +9 -0
- package/dist/utils/camelize.d.cts.map +1 -0
- package/dist/utils/camelize.d.mts +9 -0
- package/dist/utils/camelize.d.mts.map +1 -0
- package/dist/utils/camelize.mjs +33 -0
- package/dist/utils/camelize.mjs.map +1 -0
- package/dist/utils/type-utils.cjs +27 -0
- package/dist/utils/type-utils.d.cts +11 -0
- package/dist/utils/type-utils.d.cts.map +1 -0
- package/dist/utils/type-utils.d.mts +11 -0
- package/dist/utils/type-utils.d.mts.map +1 -0
- package/dist/utils/type-utils.mjs +23 -0
- package/dist/utils/type-utils.mjs.map +1 -0
- package/dist/utils/zod.cjs +35 -0
- package/dist/utils/zod.d.cts +23 -0
- package/dist/utils/zod.d.cts.map +1 -0
- package/dist/utils/zod.d.mts +23 -0
- package/dist/utils/zod.d.mts.map +1 -0
- package/dist/utils/zod.mjs +34 -0
- package/dist/utils/zod.mjs.map +1 -0
- package/package.json +44 -43
- package/CHANGELOG.md +0 -274
- package/lib/cjs/afs.d.ts +0 -42
- package/lib/cjs/afs.js +0 -350
- package/lib/cjs/error.d.ts +0 -13
- package/lib/cjs/error.js +0 -25
- package/lib/cjs/index.d.ts +0 -3
- package/lib/cjs/index.js +0 -19
- package/lib/cjs/package.json +0 -3
- package/lib/cjs/type.d.ts +0 -242
- package/lib/cjs/type.js +0 -25
- package/lib/dts/afs.d.ts +0 -42
- package/lib/dts/error.d.ts +0 -13
- package/lib/dts/index.d.ts +0 -3
- package/lib/dts/type.d.ts +0 -242
- package/lib/esm/afs.d.ts +0 -42
- package/lib/esm/afs.js +0 -346
- package/lib/esm/error.d.ts +0 -13
- package/lib/esm/error.js +0 -20
- package/lib/esm/index.d.ts +0 -3
- package/lib/esm/index.js +0 -3
- package/lib/esm/package.json +0 -3
- package/lib/esm/type.d.ts +0 -242
- package/lib/esm/type.js +0 -22
package/README.md
CHANGED
|
@@ -34,28 +34,30 @@ import { AFSHistory } from "@aigne/afs-history";
|
|
|
34
34
|
const afs = new AFS();
|
|
35
35
|
|
|
36
36
|
// Mount history module (optional)
|
|
37
|
-
afs.mount(
|
|
38
|
-
|
|
39
|
-
}
|
|
37
|
+
afs.mount(
|
|
38
|
+
new AFSHistory({
|
|
39
|
+
storage: { url: "file:./memory.sqlite3" },
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
40
42
|
|
|
41
43
|
// All modules are mounted under /modules
|
|
42
44
|
// List modules
|
|
43
45
|
const modules = await afs.listModules();
|
|
44
46
|
|
|
45
47
|
// List entries in a module
|
|
46
|
-
const { list } = await afs.list(
|
|
48
|
+
const { list } = await afs.list("/modules/history");
|
|
47
49
|
|
|
48
50
|
// Read an entry
|
|
49
|
-
const { result } = await afs.read(
|
|
51
|
+
const { result } = await afs.read("/modules/history/some-id");
|
|
50
52
|
|
|
51
53
|
// Write an entry (if module supports write)
|
|
52
|
-
await afs.write(
|
|
53
|
-
content:
|
|
54
|
-
summary:
|
|
54
|
+
await afs.write("/modules/history/notes", {
|
|
55
|
+
content: "My notes",
|
|
56
|
+
summary: "Personal notes",
|
|
55
57
|
});
|
|
56
58
|
|
|
57
59
|
// Search for content
|
|
58
|
-
const { list: results } = await afs.search(
|
|
60
|
+
const { list: results } = await afs.search("/modules/history", "search query");
|
|
59
61
|
```
|
|
60
62
|
|
|
61
63
|
## Core Concepts
|
|
@@ -66,16 +68,16 @@ All data in AFS is represented as `AFSEntry` objects:
|
|
|
66
68
|
|
|
67
69
|
```typescript
|
|
68
70
|
interface AFSEntry {
|
|
69
|
-
id: string;
|
|
70
|
-
path: string;
|
|
71
|
-
content?: any;
|
|
72
|
-
summary?: string;
|
|
73
|
-
metadata?: Record<string, any>;
|
|
74
|
-
createdAt?: Date;
|
|
75
|
-
updatedAt?: Date;
|
|
76
|
-
userId?: string;
|
|
77
|
-
sessionId?: string;
|
|
78
|
-
linkTo?: string;
|
|
71
|
+
id: string; // Unique identifier
|
|
72
|
+
path: string; // Full path in AFS
|
|
73
|
+
content?: any; // File/data content
|
|
74
|
+
summary?: string; // Optional summary
|
|
75
|
+
metadata?: Record<string, any>; // Custom metadata
|
|
76
|
+
createdAt?: Date; // Creation timestamp
|
|
77
|
+
updatedAt?: Date; // Last update timestamp
|
|
78
|
+
userId?: string; // Associated user
|
|
79
|
+
sessionId?: string; // Associated session
|
|
80
|
+
linkTo?: string; // Link to another entry
|
|
79
81
|
}
|
|
80
82
|
```
|
|
81
83
|
|
|
@@ -85,15 +87,29 @@ Modules are pluggable components that implement storage backends. All modules ar
|
|
|
85
87
|
|
|
86
88
|
```typescript
|
|
87
89
|
interface AFSModule {
|
|
88
|
-
name: string;
|
|
89
|
-
description?: string;
|
|
90
|
+
name: string; // Module name (used as mount path)
|
|
91
|
+
description?: string; // Description for AI agents
|
|
90
92
|
|
|
91
93
|
// Operations (all optional)
|
|
92
|
-
list?(
|
|
94
|
+
list?(
|
|
95
|
+
path: string,
|
|
96
|
+
options?: AFSListOptions,
|
|
97
|
+
): Promise<{ list: AFSEntry[]; message?: string }>;
|
|
93
98
|
read?(path: string): Promise<{ result?: AFSEntry; message?: string }>;
|
|
94
|
-
write?(
|
|
95
|
-
|
|
96
|
-
|
|
99
|
+
write?(
|
|
100
|
+
path: string,
|
|
101
|
+
entry: AFSWriteEntryPayload,
|
|
102
|
+
): Promise<{ result: AFSEntry; message?: string }>;
|
|
103
|
+
search?(
|
|
104
|
+
path: string,
|
|
105
|
+
query: string,
|
|
106
|
+
options?: AFSSearchOptions,
|
|
107
|
+
): Promise<{ list: AFSEntry[]; message?: string }>;
|
|
108
|
+
exec?(
|
|
109
|
+
path: string,
|
|
110
|
+
args: Record<string, any>,
|
|
111
|
+
options: { context: any },
|
|
112
|
+
): Promise<{ result: Record<string, any> }>;
|
|
97
113
|
|
|
98
114
|
// Lifecycle
|
|
99
115
|
onMount?(afs: AFSRoot): void;
|
|
@@ -114,11 +130,13 @@ new AFS(options?: AFSOptions)
|
|
|
114
130
|
```
|
|
115
131
|
|
|
116
132
|
Options:
|
|
133
|
+
|
|
117
134
|
- `modules`: Optional array of modules to mount on initialization
|
|
118
135
|
|
|
119
136
|
#### Methods
|
|
120
137
|
|
|
121
138
|
##### mount(module: AFSModule)
|
|
139
|
+
|
|
122
140
|
##### mount(path: string, module: AFSModule)
|
|
123
141
|
|
|
124
142
|
Mount a module. The module will be accessible under `/modules/{module.name}` or `/modules/{path}`:
|
|
@@ -145,12 +163,13 @@ const modules = await afs.listModules();
|
|
|
145
163
|
List entries in a directory:
|
|
146
164
|
|
|
147
165
|
```typescript
|
|
148
|
-
const { list, message } = await afs.list(
|
|
149
|
-
maxDepth: 2
|
|
166
|
+
const { list, message } = await afs.list("/modules/history", {
|
|
167
|
+
maxDepth: 2,
|
|
150
168
|
});
|
|
151
169
|
```
|
|
152
170
|
|
|
153
171
|
Options:
|
|
172
|
+
|
|
154
173
|
- `maxDepth`: Maximum recursion depth (default: 1)
|
|
155
174
|
|
|
156
175
|
##### read(path: string)
|
|
@@ -158,7 +177,7 @@ Options:
|
|
|
158
177
|
Read a specific entry:
|
|
159
178
|
|
|
160
179
|
```typescript
|
|
161
|
-
const { result, message } = await afs.read(
|
|
180
|
+
const { result, message } = await afs.read("/modules/history/uuid-123");
|
|
162
181
|
```
|
|
163
182
|
|
|
164
183
|
##### write(path: string, content: AFSWriteEntryPayload)
|
|
@@ -166,10 +185,10 @@ const { result, message } = await afs.read('/modules/history/uuid-123');
|
|
|
166
185
|
Write or update an entry:
|
|
167
186
|
|
|
168
187
|
```typescript
|
|
169
|
-
const { result, message } = await afs.write(
|
|
170
|
-
content:
|
|
171
|
-
summary:
|
|
172
|
-
metadata: { type:
|
|
188
|
+
const { result, message } = await afs.write("/modules/my-module/file.txt", {
|
|
189
|
+
content: "Hello, world!",
|
|
190
|
+
summary: "Greeting file",
|
|
191
|
+
metadata: { type: "greeting" },
|
|
173
192
|
});
|
|
174
193
|
```
|
|
175
194
|
|
|
@@ -178,7 +197,10 @@ const { result, message } = await afs.write('/modules/my-module/file.txt', {
|
|
|
178
197
|
Search for content:
|
|
179
198
|
|
|
180
199
|
```typescript
|
|
181
|
-
const { list, message } = await afs.search(
|
|
200
|
+
const { list, message } = await afs.search(
|
|
201
|
+
"/modules/history",
|
|
202
|
+
"authentication",
|
|
203
|
+
);
|
|
182
204
|
```
|
|
183
205
|
|
|
184
206
|
##### exec(path: string, args: Record<string, any>, options: { context: any })
|
|
@@ -186,7 +208,11 @@ const { list, message } = await afs.search('/modules/history', 'authentication')
|
|
|
186
208
|
Execute a module-specific operation:
|
|
187
209
|
|
|
188
210
|
```typescript
|
|
189
|
-
const { result } = await afs.exec(
|
|
211
|
+
const { result } = await afs.exec(
|
|
212
|
+
"/modules/my-module/action",
|
|
213
|
+
{ param: "value" },
|
|
214
|
+
{ context },
|
|
215
|
+
);
|
|
190
216
|
```
|
|
191
217
|
|
|
192
218
|
### Events
|
|
@@ -195,15 +221,16 @@ AFS uses an event system for module communication:
|
|
|
195
221
|
|
|
196
222
|
```typescript
|
|
197
223
|
// Modules can listen to events
|
|
198
|
-
afs.on(
|
|
199
|
-
console.log(
|
|
224
|
+
afs.on("agentSucceed", ({ input, output }) => {
|
|
225
|
+
console.log("Agent succeeded:", input, output);
|
|
200
226
|
});
|
|
201
227
|
|
|
202
228
|
// Modules can emit custom events
|
|
203
|
-
afs.emit(
|
|
229
|
+
afs.emit("customEvent", { data: "value" });
|
|
204
230
|
```
|
|
205
231
|
|
|
206
232
|
Common events from `AFSRootEvents`:
|
|
233
|
+
|
|
207
234
|
- `agentSucceed`: Emitted when an agent successfully completes
|
|
208
235
|
- `agentFail`: Emitted when an agent fails
|
|
209
236
|
|
|
@@ -214,6 +241,7 @@ Common events from `AFSRootEvents`:
|
|
|
214
241
|
The history module tracks conversation history. It is available as a separate package: `@aigne/afs-history`.
|
|
215
242
|
|
|
216
243
|
**Features:**
|
|
244
|
+
|
|
217
245
|
- Listens to `agentSucceed` events and records agent interactions
|
|
218
246
|
- Stores input/output pairs with UUID paths
|
|
219
247
|
- Supports list and read operations
|
|
@@ -221,6 +249,7 @@ The history module tracks conversation history. It is available as a separate pa
|
|
|
221
249
|
- Persistent SQLite storage
|
|
222
250
|
|
|
223
251
|
**Installation:**
|
|
252
|
+
|
|
224
253
|
```bash
|
|
225
254
|
npm install @aigne/afs-history
|
|
226
255
|
```
|
|
@@ -232,15 +261,18 @@ import { AFS } from "@aigne/afs";
|
|
|
232
261
|
import { AFSHistory } from "@aigne/afs-history";
|
|
233
262
|
|
|
234
263
|
const afs = new AFS();
|
|
235
|
-
afs.mount(
|
|
236
|
-
|
|
237
|
-
}
|
|
264
|
+
afs.mount(
|
|
265
|
+
new AFSHistory({
|
|
266
|
+
storage: { url: "file:./memory.sqlite3" },
|
|
267
|
+
}),
|
|
268
|
+
);
|
|
238
269
|
|
|
239
270
|
// History entries are accessible at /modules/history
|
|
240
|
-
const { list } = await afs.list(
|
|
271
|
+
const { list } = await afs.list("/modules/history");
|
|
241
272
|
```
|
|
242
273
|
|
|
243
274
|
**Configuration:**
|
|
275
|
+
|
|
244
276
|
- `storage`: Storage configuration (e.g., `{ url: "file:./memory.sqlite3" }`) or a SharedAFSStorage instance
|
|
245
277
|
|
|
246
278
|
**Note:** History is NOT automatically mounted. You must explicitly mount it if needed.
|
|
@@ -258,7 +290,10 @@ export class CustomModule implements AFSModule {
|
|
|
258
290
|
readonly name = "custom-module";
|
|
259
291
|
readonly description = "My custom storage";
|
|
260
292
|
|
|
261
|
-
async list(
|
|
293
|
+
async list(
|
|
294
|
+
path: string,
|
|
295
|
+
options?: AFSListOptions,
|
|
296
|
+
): Promise<{ list: AFSEntry[]; message?: string }> {
|
|
262
297
|
// path is the subpath within your module
|
|
263
298
|
// Implement your list logic
|
|
264
299
|
return { list: [] };
|
|
@@ -269,13 +304,20 @@ export class CustomModule implements AFSModule {
|
|
|
269
304
|
return { result: undefined };
|
|
270
305
|
}
|
|
271
306
|
|
|
272
|
-
async write(
|
|
307
|
+
async write(
|
|
308
|
+
path: string,
|
|
309
|
+
content: AFSWriteEntryPayload,
|
|
310
|
+
): Promise<{ result: AFSEntry; message?: string }> {
|
|
273
311
|
// Implement your write logic
|
|
274
|
-
const entry: AFSEntry = { id:
|
|
312
|
+
const entry: AFSEntry = { id: "id", path, ...content };
|
|
275
313
|
return { result: entry };
|
|
276
314
|
}
|
|
277
315
|
|
|
278
|
-
async search(
|
|
316
|
+
async search(
|
|
317
|
+
path: string,
|
|
318
|
+
query: string,
|
|
319
|
+
options?: AFSSearchOptions,
|
|
320
|
+
): Promise<{ list: AFSEntry[]; message?: string }> {
|
|
279
321
|
// Implement your search logic
|
|
280
322
|
return { list: [] };
|
|
281
323
|
}
|
|
@@ -284,7 +326,7 @@ export class CustomModule implements AFSModule {
|
|
|
284
326
|
console.log(`${this.name} mounted`);
|
|
285
327
|
|
|
286
328
|
// Listen to events
|
|
287
|
-
afs.on(
|
|
329
|
+
afs.on("agentSucceed", (data) => {
|
|
288
330
|
// Handle event
|
|
289
331
|
});
|
|
290
332
|
}
|
|
@@ -323,18 +365,20 @@ import { AFS } from "@aigne/afs";
|
|
|
323
365
|
import { AFSHistory } from "@aigne/afs-history";
|
|
324
366
|
|
|
325
367
|
const afs = new AFS();
|
|
326
|
-
afs.mount(
|
|
327
|
-
|
|
328
|
-
}
|
|
368
|
+
afs.mount(
|
|
369
|
+
new AFSHistory({
|
|
370
|
+
storage: { url: "file:./memory.sqlite3" },
|
|
371
|
+
}),
|
|
372
|
+
);
|
|
329
373
|
|
|
330
374
|
const agent = AIAgent.from({
|
|
331
375
|
name: "assistant",
|
|
332
|
-
afs: afs
|
|
376
|
+
afs: afs,
|
|
333
377
|
});
|
|
334
378
|
|
|
335
379
|
const context = aigne.newContext();
|
|
336
380
|
const result = await context.invoke(agent, {
|
|
337
|
-
message: "What's in my history?"
|
|
381
|
+
message: "What's in my history?",
|
|
338
382
|
});
|
|
339
383
|
```
|
|
340
384
|
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
let _aigne_uuid = require("@aigne/uuid");
|
|
2
|
+
let strict_event_emitter = require("strict-event-emitter");
|
|
3
|
+
let ufo = require("ufo");
|
|
4
|
+
let zod = require("zod");
|
|
5
|
+
|
|
6
|
+
//#region src/error.ts
|
|
7
|
+
/**
|
|
8
|
+
* Base error class for all AFS errors.
|
|
9
|
+
*/
|
|
10
|
+
var AFSError = class extends Error {
|
|
11
|
+
code;
|
|
12
|
+
constructor(message, code) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "AFSError";
|
|
15
|
+
this.code = code;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Error thrown when attempting write operations on a readonly AFS or module.
|
|
20
|
+
*/
|
|
21
|
+
var AFSReadonlyError = class extends AFSError {
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(message, "AFS_READONLY");
|
|
24
|
+
this.name = "AFSReadonlyError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/type.ts
|
|
30
|
+
/**
|
|
31
|
+
* Zod schema for access mode validation.
|
|
32
|
+
* Can be reused across modules that support access mode configuration.
|
|
33
|
+
*/
|
|
34
|
+
const accessModeSchema = zod.z.enum(["readonly", "readwrite"]).describe("Access mode for this module").optional();
|
|
35
|
+
const afsEntrySchema = zod.z.object({
|
|
36
|
+
id: zod.z.string(),
|
|
37
|
+
createdAt: zod.z.date().optional(),
|
|
38
|
+
updatedAt: zod.z.date().optional(),
|
|
39
|
+
path: zod.z.string(),
|
|
40
|
+
userId: zod.z.string().nullable().optional(),
|
|
41
|
+
sessionId: zod.z.string().nullable().optional(),
|
|
42
|
+
summary: zod.z.string().nullable().optional(),
|
|
43
|
+
description: zod.z.string().nullable().optional(),
|
|
44
|
+
metadata: zod.z.record(zod.z.any()).nullable().optional(),
|
|
45
|
+
linkTo: zod.z.string().nullable().optional(),
|
|
46
|
+
content: zod.z.any().optional()
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/afs.ts
|
|
51
|
+
const DEFAULT_MAX_DEPTH = 1;
|
|
52
|
+
const MODULES_ROOT_DIR = "/modules";
|
|
53
|
+
var AFS = class extends strict_event_emitter.Emitter {
|
|
54
|
+
name = "AFSRoot";
|
|
55
|
+
constructor(options = {}) {
|
|
56
|
+
super();
|
|
57
|
+
this.options = options;
|
|
58
|
+
for (const module of options?.modules ?? []) this.mount(module);
|
|
59
|
+
}
|
|
60
|
+
modules = /* @__PURE__ */ new Map();
|
|
61
|
+
/**
|
|
62
|
+
* Check if write operations are allowed for the given module.
|
|
63
|
+
* Throws AFSReadonlyError if not allowed.
|
|
64
|
+
*/
|
|
65
|
+
checkWritePermission(module, operation, path) {
|
|
66
|
+
if (module.accessMode !== "readwrite") throw new AFSReadonlyError(`Module '${module.name}' is readonly, cannot perform ${operation} to ${path}`);
|
|
67
|
+
}
|
|
68
|
+
mount(module) {
|
|
69
|
+
if (module.name.includes("/")) throw new Error(`Invalid module name: ${module.name}. Module name must not contain '/'`);
|
|
70
|
+
const path = (0, ufo.joinURL)(MODULES_ROOT_DIR, module.name);
|
|
71
|
+
if (this.modules.has(path)) throw new Error(`Module already mounted at path: ${path}`);
|
|
72
|
+
this.modules.set(path, module);
|
|
73
|
+
module.onMount?.(this);
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
async listModules() {
|
|
77
|
+
return Array.from(this.modules.entries()).map(([path, module]) => ({
|
|
78
|
+
path,
|
|
79
|
+
name: module.name,
|
|
80
|
+
description: module.description,
|
|
81
|
+
module
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
async list(path, options = {}) {
|
|
85
|
+
let preset;
|
|
86
|
+
if (options.preset) {
|
|
87
|
+
preset = this.options?.context?.list?.presets?.[options.preset];
|
|
88
|
+
if (!preset) throw new Error(`Preset not found: ${options.preset}`);
|
|
89
|
+
}
|
|
90
|
+
return await this.processWithPreset(path, void 0, preset, {
|
|
91
|
+
...options,
|
|
92
|
+
defaultSelect: () => this._list(path, options)
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
async _list(path, options = {}) {
|
|
96
|
+
const results = [];
|
|
97
|
+
if (path === "/" && this.modules.size > 0) {
|
|
98
|
+
const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
99
|
+
results.push({
|
|
100
|
+
id: "modules",
|
|
101
|
+
path: MODULES_ROOT_DIR,
|
|
102
|
+
summary: "All mounted modules"
|
|
103
|
+
});
|
|
104
|
+
if (maxDepth === 1) return { data: results };
|
|
105
|
+
const childrenResult = await this._list(MODULES_ROOT_DIR, {
|
|
106
|
+
...options,
|
|
107
|
+
maxDepth: maxDepth - 1
|
|
108
|
+
});
|
|
109
|
+
results.push(...childrenResult.data);
|
|
110
|
+
return { data: results };
|
|
111
|
+
}
|
|
112
|
+
const matches = this.findModules(path, options);
|
|
113
|
+
for (const matched of matches) {
|
|
114
|
+
if (matched.maxDepth === 0) {
|
|
115
|
+
const moduleEntry = {
|
|
116
|
+
id: matched.module.name,
|
|
117
|
+
path: matched.modulePath,
|
|
118
|
+
summary: matched.module.description
|
|
119
|
+
};
|
|
120
|
+
results.push(moduleEntry);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (!matched.module.list) continue;
|
|
124
|
+
try {
|
|
125
|
+
const { data } = await matched.module.list(matched.subpath, {
|
|
126
|
+
...options,
|
|
127
|
+
maxDepth: matched.maxDepth
|
|
128
|
+
});
|
|
129
|
+
const children = data.map((entry) => ({
|
|
130
|
+
...entry,
|
|
131
|
+
path: (0, ufo.joinURL)(matched.modulePath, entry.path)
|
|
132
|
+
}));
|
|
133
|
+
results.push(...children);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
throw new Error(`Error listing from module at ${matched.modulePath}: ${error.message}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { data: results };
|
|
139
|
+
}
|
|
140
|
+
async read(path, _options) {
|
|
141
|
+
const modules = this.findModules(path, { exactMatch: true });
|
|
142
|
+
for (const { module, modulePath, subpath } of modules) {
|
|
143
|
+
const res = await module.read?.(subpath);
|
|
144
|
+
if (res?.data) return {
|
|
145
|
+
...res,
|
|
146
|
+
data: {
|
|
147
|
+
...res.data,
|
|
148
|
+
path: (0, ufo.joinURL)(modulePath, res.data.path)
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
data: void 0,
|
|
154
|
+
message: "File not found"
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async write(path, content, options) {
|
|
158
|
+
const module = this.findModules(path, { exactMatch: true })[0];
|
|
159
|
+
if (!module?.module.write) throw new Error(`No module found for path: ${path}`);
|
|
160
|
+
this.checkWritePermission(module.module, "write", path);
|
|
161
|
+
const res = await module.module.write(module.subpath, content, options);
|
|
162
|
+
return {
|
|
163
|
+
...res,
|
|
164
|
+
data: {
|
|
165
|
+
...res.data,
|
|
166
|
+
path: (0, ufo.joinURL)(module.modulePath, res.data.path)
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
async delete(path, options) {
|
|
171
|
+
const module = this.findModules(path, { exactMatch: true })[0];
|
|
172
|
+
if (!module?.module.delete) throw new Error(`No module found for path: ${path}`);
|
|
173
|
+
this.checkWritePermission(module.module, "delete", path);
|
|
174
|
+
return await module.module.delete(module.subpath, options);
|
|
175
|
+
}
|
|
176
|
+
async rename(oldPath, newPath, options) {
|
|
177
|
+
const oldModule = this.findModules(oldPath, { exactMatch: true })[0];
|
|
178
|
+
const newModule = this.findModules(newPath, { exactMatch: true })[0];
|
|
179
|
+
if (!oldModule || !newModule || oldModule.modulePath !== newModule.modulePath) throw new Error(`Cannot rename across different modules. Both paths must be in the same module.`);
|
|
180
|
+
if (!oldModule.module.rename) throw new Error(`Module does not support rename operation: ${oldModule.modulePath}`);
|
|
181
|
+
this.checkWritePermission(oldModule.module, "rename", oldPath);
|
|
182
|
+
return await oldModule.module.rename(oldModule.subpath, newModule.subpath, options);
|
|
183
|
+
}
|
|
184
|
+
async search(path, query, options = {}) {
|
|
185
|
+
let preset;
|
|
186
|
+
if (options.preset) {
|
|
187
|
+
preset = this.options?.context?.search?.presets?.[options.preset];
|
|
188
|
+
if (!preset) throw new Error(`Preset not found: ${options.preset}`);
|
|
189
|
+
}
|
|
190
|
+
return await this.processWithPreset(path, query, preset, {
|
|
191
|
+
...options,
|
|
192
|
+
defaultSelect: () => this._search(path, query, options)
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
async processWithPreset(path, query, preset, options) {
|
|
196
|
+
const select = options.select || preset?.select;
|
|
197
|
+
const per = options.per || preset?.per;
|
|
198
|
+
const dedupe = options.dedupe || preset?.dedupe;
|
|
199
|
+
const format = options.format || preset?.format;
|
|
200
|
+
const entries = select ? (await this._select(path, query, select, options)).data : (await options.defaultSelect()).data;
|
|
201
|
+
const mapped = per ? await Promise.all(entries.map((data) => per.invoke({ data }, options).then((res) => res.data))) : entries;
|
|
202
|
+
const deduped = dedupe ? await dedupe.invoke({ data: mapped }, options).then((res) => res.data) : mapped;
|
|
203
|
+
let formatted = deduped;
|
|
204
|
+
if (format === "simple-list" || format === "tree") {
|
|
205
|
+
const valid = zod.z.array(afsEntrySchema).safeParse(deduped);
|
|
206
|
+
if (!valid.data) throw new Error("Tree format requires entries to be AFSEntry objects");
|
|
207
|
+
if (format === "tree") formatted = this.buildTreeView(valid.data);
|
|
208
|
+
else if (format === "simple-list") formatted = this.buildSimpleListView(valid.data);
|
|
209
|
+
} else if (typeof format === "object" && typeof format.invoke === "function") formatted = await format.invoke({ data: deduped }, options).then((res) => res.data);
|
|
210
|
+
return { data: formatted };
|
|
211
|
+
}
|
|
212
|
+
async _select(path, query, select, options) {
|
|
213
|
+
const { data } = await select.invoke({
|
|
214
|
+
path,
|
|
215
|
+
query
|
|
216
|
+
}, options);
|
|
217
|
+
return { data: (await Promise.all(data.map((p) => this.read(p).then((res) => res.data)))).filter((i) => !!i) };
|
|
218
|
+
}
|
|
219
|
+
async _search(path, query, options) {
|
|
220
|
+
const results = [];
|
|
221
|
+
const messages = [];
|
|
222
|
+
for (const { module, modulePath, subpath } of this.findModules(path)) {
|
|
223
|
+
if (!module.search) continue;
|
|
224
|
+
try {
|
|
225
|
+
const { data, message } = await module.search(subpath, query, options);
|
|
226
|
+
results.push(...data.map((entry) => ({
|
|
227
|
+
...entry,
|
|
228
|
+
path: (0, ufo.joinURL)(modulePath, entry.path)
|
|
229
|
+
})));
|
|
230
|
+
if (message) messages.push(message);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
throw new Error(`Error searching in module at ${modulePath}: ${error.message}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
data: results,
|
|
237
|
+
message: messages.join("; ")
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
findModules(path, options) {
|
|
241
|
+
const maxDepth = Math.max(options?.maxDepth ?? DEFAULT_MAX_DEPTH, 1);
|
|
242
|
+
const matched = [];
|
|
243
|
+
for (const [modulePath, module] of this.modules) {
|
|
244
|
+
const pathSegments = path.split("/").filter(Boolean);
|
|
245
|
+
const modulePathSegments = modulePath.split("/").filter(Boolean);
|
|
246
|
+
let newMaxDepth;
|
|
247
|
+
let subpath;
|
|
248
|
+
let remainedModulePath;
|
|
249
|
+
if (!options?.exactMatch && modulePath.startsWith(path)) {
|
|
250
|
+
newMaxDepth = Math.max(0, maxDepth - (modulePathSegments.length - pathSegments.length));
|
|
251
|
+
subpath = "/";
|
|
252
|
+
remainedModulePath = (0, ufo.joinURL)("/", ...modulePathSegments.slice(pathSegments.length).slice(0, maxDepth));
|
|
253
|
+
} else if (path.startsWith(modulePath)) {
|
|
254
|
+
newMaxDepth = maxDepth;
|
|
255
|
+
subpath = (0, ufo.joinURL)("/", ...pathSegments.slice(modulePathSegments.length));
|
|
256
|
+
remainedModulePath = "/";
|
|
257
|
+
} else continue;
|
|
258
|
+
if (newMaxDepth < 0) continue;
|
|
259
|
+
matched.push({
|
|
260
|
+
module,
|
|
261
|
+
modulePath,
|
|
262
|
+
maxDepth: newMaxDepth,
|
|
263
|
+
subpath,
|
|
264
|
+
remainedModulePath
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return matched;
|
|
268
|
+
}
|
|
269
|
+
async exec(path, args, options) {
|
|
270
|
+
const module = this.findModules(path)[0];
|
|
271
|
+
if (!module?.module.exec) throw new Error(`No module found for path: ${path}`);
|
|
272
|
+
return await module.module.exec(module.subpath, args, options);
|
|
273
|
+
}
|
|
274
|
+
buildSimpleListView(entries) {
|
|
275
|
+
return entries.map((entry) => `${entry.path}${this.buildMetadataSuffix(entry)}`);
|
|
276
|
+
}
|
|
277
|
+
buildTreeView(entries) {
|
|
278
|
+
const tree = {};
|
|
279
|
+
const entryMap = /* @__PURE__ */ new Map();
|
|
280
|
+
for (const entry of entries) {
|
|
281
|
+
entryMap.set(entry.path, entry);
|
|
282
|
+
const parts = entry.path.split("/").filter(Boolean);
|
|
283
|
+
let current = tree;
|
|
284
|
+
for (const part of parts) {
|
|
285
|
+
if (!current[part]) current[part] = {};
|
|
286
|
+
current = current[part];
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const renderTree = (node, prefix = "", currentPath = "") => {
|
|
290
|
+
let result = "";
|
|
291
|
+
const keys = Object.keys(node);
|
|
292
|
+
keys.forEach((key, index) => {
|
|
293
|
+
const isLast = index === keys.length - 1;
|
|
294
|
+
const fullPath = currentPath ? `${currentPath}/${key}` : `/${key}`;
|
|
295
|
+
const entry = entryMap.get(fullPath);
|
|
296
|
+
result += `${prefix}${isLast ? "└── " : "├── "}${key}${entry ? this.buildMetadataSuffix(entry) : ""}`;
|
|
297
|
+
result += `\n`;
|
|
298
|
+
result += renderTree(node[key], `${prefix}${isLast ? " " : "│ "}`, fullPath);
|
|
299
|
+
});
|
|
300
|
+
return result;
|
|
301
|
+
};
|
|
302
|
+
return renderTree(tree);
|
|
303
|
+
}
|
|
304
|
+
buildMetadataSuffix(entry) {
|
|
305
|
+
const metadataParts = [];
|
|
306
|
+
const childrenCount = entry?.metadata?.childrenCount;
|
|
307
|
+
if (typeof childrenCount === "number") metadataParts.push(`${childrenCount} items`);
|
|
308
|
+
if (entry?.metadata?.childrenTruncated) metadataParts.push("truncated");
|
|
309
|
+
if (entry?.metadata?.gitignored) metadataParts.push("gitignored");
|
|
310
|
+
if (entry?.metadata?.execute) metadataParts.push("executable");
|
|
311
|
+
return metadataParts.length > 0 ? ` [${metadataParts.join(", ")}]` : "";
|
|
312
|
+
}
|
|
313
|
+
physicalPath;
|
|
314
|
+
async initializePhysicalPath() {
|
|
315
|
+
this.physicalPath ??= (async () => {
|
|
316
|
+
const path = await import("node:path");
|
|
317
|
+
const os = await import("node:os");
|
|
318
|
+
const fs = await import("node:fs/promises");
|
|
319
|
+
const rootDir = path.join(os.tmpdir(), (0, _aigne_uuid.v7)());
|
|
320
|
+
await fs.mkdir(rootDir, { recursive: true });
|
|
321
|
+
for (const [modulePath, module] of this.modules) {
|
|
322
|
+
const physicalModulePath = path.join(rootDir, modulePath);
|
|
323
|
+
await fs.mkdir(path.dirname(physicalModulePath), { recursive: true });
|
|
324
|
+
await module.symlinkToPhysical?.(physicalModulePath);
|
|
325
|
+
}
|
|
326
|
+
return rootDir;
|
|
327
|
+
})();
|
|
328
|
+
return this.physicalPath;
|
|
329
|
+
}
|
|
330
|
+
async cleanupPhysicalPath() {
|
|
331
|
+
if (this.physicalPath) {
|
|
332
|
+
await (await import("node:fs/promises")).rm(await this.physicalPath, {
|
|
333
|
+
recursive: true,
|
|
334
|
+
force: true
|
|
335
|
+
});
|
|
336
|
+
this.physicalPath = void 0;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
//#endregion
|
|
342
|
+
exports.AFS = AFS;
|
|
343
|
+
exports.AFSError = AFSError;
|
|
344
|
+
exports.AFSReadonlyError = AFSReadonlyError;
|
|
345
|
+
exports.accessModeSchema = accessModeSchema;
|
|
346
|
+
exports.afsEntrySchema = afsEntrySchema;
|