@aigne/afs 1.4.0-beta.1 → 1.4.0-beta.10

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/lib/dts/type.d.ts CHANGED
@@ -1,9 +1,26 @@
1
1
  import type { Emitter } from "strict-event-emitter";
2
- import { type ZodType } from "zod";
3
- export interface AFSListOptions {
2
+ import { type ZodType, z } from "zod";
3
+ /**
4
+ * Access mode for AFS modules and root.
5
+ * - "readonly": Only read operations are allowed (list, read, search)
6
+ * - "readwrite": All operations are allowed
7
+ */
8
+ export type AFSAccessMode = "readonly" | "readwrite";
9
+ /**
10
+ * Zod schema for access mode validation.
11
+ * Can be reused across modules that support access mode configuration.
12
+ */
13
+ export declare const accessModeSchema: z.ZodOptional<z.ZodEnum<["readonly", "readwrite"]>>;
14
+ export interface AFSOperationOptions {
15
+ context?: any;
16
+ }
17
+ export interface AFSListOptions extends AFSOperationOptions {
4
18
  filter?: {
19
+ agentId?: string;
5
20
  userId?: string;
6
21
  sessionId?: string;
22
+ before?: string;
23
+ after?: string;
7
24
  };
8
25
  maxDepth?: number;
9
26
  limit?: number;
@@ -15,46 +32,45 @@ export interface AFSListOptions {
15
32
  * @default false
16
33
  */
17
34
  disableGitignore?: boolean;
18
- context?: any;
35
+ /**
36
+ * Glob pattern to filter entries by path.
37
+ * Examples: "*.ts", "**\/*.js", "src/**\/*.{ts,tsx}"
38
+ */
39
+ pattern?: string;
19
40
  }
20
41
  export interface AFSListResult {
21
42
  data: AFSEntry[];
22
43
  message?: string;
23
- context?: any;
24
44
  }
25
- export interface AFSSearchOptions {
45
+ export interface AFSSearchOptions extends AFSOperationOptions {
26
46
  limit?: number;
27
47
  caseSensitive?: boolean;
28
- context?: any;
29
48
  }
30
49
  export interface AFSSearchResult {
31
50
  data: AFSEntry[];
32
51
  message?: string;
33
52
  }
34
- export interface AFSReadOptions {
35
- context?: any;
53
+ export interface AFSReadOptions extends AFSOperationOptions {
54
+ filter?: AFSListOptions["filter"];
36
55
  }
37
56
  export interface AFSReadResult {
38
57
  data?: AFSEntry;
39
58
  message?: string;
40
59
  }
41
- export interface AFSDeleteOptions {
60
+ export interface AFSDeleteOptions extends AFSOperationOptions {
42
61
  recursive?: boolean;
43
- context?: any;
44
62
  }
45
63
  export interface AFSDeleteResult {
46
64
  message?: string;
47
65
  }
48
- export interface AFSRenameOptions {
66
+ export interface AFSRenameOptions extends AFSOperationOptions {
49
67
  overwrite?: boolean;
50
- context?: any;
51
68
  }
52
69
  export interface AFSRenameResult {
53
70
  message?: string;
54
71
  }
55
- export interface AFSWriteOptions {
72
+ export interface AFSWriteOptions extends AFSOperationOptions {
56
73
  append?: boolean;
57
- context?: any;
58
74
  }
59
75
  export interface AFSWriteResult {
60
76
  data: AFSEntry;
@@ -63,8 +79,7 @@ export interface AFSWriteResult {
63
79
  }
64
80
  export interface AFSWriteEntryPayload extends Omit<AFSEntry, "id" | "path"> {
65
81
  }
66
- export interface AFSExecOptions {
67
- context: any;
82
+ export interface AFSExecOptions extends AFSOperationOptions {
68
83
  }
69
84
  export interface AFSExecResult {
70
85
  data: Record<string, any>;
@@ -72,7 +87,21 @@ export interface AFSExecResult {
72
87
  export interface AFSModule {
73
88
  readonly name: string;
74
89
  readonly description?: string;
90
+ /**
91
+ * Access mode for this module.
92
+ * - "readonly": Only read operations are allowed
93
+ * - "readwrite": All operations are allowed
94
+ * Default behavior is implementation-specific.
95
+ */
96
+ readonly accessMode?: AFSAccessMode;
97
+ /**
98
+ * Enable automatic agent skill scanning for this module.
99
+ * When set to true, the system will scan this module for agent skills.
100
+ * @default false
101
+ */
102
+ readonly agentSkills?: boolean;
75
103
  onMount?(root: AFSRoot): void;
104
+ symlinkToPhysical?(path: string): Promise<void>;
76
105
  list?(path: string, options?: AFSListOptions): Promise<AFSListResult>;
77
106
  read?(path: string, options?: AFSReadOptions): Promise<AFSReadResult>;
78
107
  write?(path: string, content: AFSWriteEntryPayload, options?: AFSWriteOptions): Promise<AFSWriteResult>;
@@ -81,14 +110,53 @@ export interface AFSModule {
81
110
  search?(path: string, query: string, options?: AFSSearchOptions): Promise<AFSSearchResult>;
82
111
  exec?(path: string, args: Record<string, any>, options: AFSExecOptions): Promise<AFSExecResult>;
83
112
  }
113
+ /**
114
+ * Parameters for loading a module from configuration.
115
+ */
116
+ export interface AFSModuleLoadParams {
117
+ /** Path to the configuration file */
118
+ filepath: string;
119
+ /** Parsed configuration object */
120
+ parsed?: object;
121
+ }
122
+ /**
123
+ * Interface for module classes that support schema validation and loading from configuration.
124
+ * This describes the static part of a module class.
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * class MyModule implements AFSModule {
129
+ * static schema() { return mySchema; }
130
+ * static async load(params: AFSModuleLoadParams) { ... }
131
+ * // ...
132
+ * }
133
+ *
134
+ * // Type check
135
+ * const _check: AFSModuleClass<MyModule, MyModuleOptions> = MyModule;
136
+ * ```
137
+ */
138
+ export interface AFSModuleClass<T extends AFSModule = AFSModule, O extends object = object> {
139
+ /** Returns the Zod schema for validating module configuration */
140
+ schema(): ZodType<O>;
141
+ /** Loads a module instance from configuration file path and parsed config */
142
+ load(params: AFSModuleLoadParams): Promise<T>;
143
+ /** Constructor */
144
+ new (options: O): T;
145
+ }
84
146
  export type AFSRootEvents = {
85
- agentSucceed: [{
86
- input: object;
87
- output: object;
88
- }];
147
+ agentSucceed: [
148
+ {
149
+ agentId?: string;
150
+ userId?: string;
151
+ sessionId?: string;
152
+ input: object;
153
+ output: object;
154
+ messages?: object[];
155
+ }
156
+ ];
89
157
  historyCreated: [{
90
158
  entry: AFSEntry;
91
- }];
159
+ }, options: AFSOperationOptions];
92
160
  };
93
161
  export interface AFSRootListOptions extends AFSListOptions, AFSContextPreset {
94
162
  preset?: string;
@@ -105,6 +173,8 @@ export interface AFSRootSearchResult extends Omit<AFSSearchResult, "data"> {
105
173
  export interface AFSRoot extends Emitter<AFSRootEvents>, AFSModule {
106
174
  list(path: string, options?: AFSRootListOptions): Promise<AFSRootListResult>;
107
175
  search(path: string, query: string, options: AFSRootSearchOptions): Promise<AFSRootSearchResult>;
176
+ initializePhysicalPath(): Promise<string>;
177
+ cleanupPhysicalPath(): Promise<void>;
108
178
  }
109
179
  export interface AFSEntryMetadata extends Record<string, any> {
110
180
  execute?: {
@@ -115,12 +185,14 @@ export interface AFSEntryMetadata extends Record<string, any> {
115
185
  };
116
186
  childrenCount?: number;
117
187
  childrenTruncated?: boolean;
188
+ gitignored?: boolean;
118
189
  }
119
190
  export interface AFSEntry<T = any> {
120
191
  id: string;
121
192
  createdAt?: Date;
122
193
  updatedAt?: Date;
123
194
  path: string;
195
+ agentId?: string | null;
124
196
  userId?: string | null;
125
197
  sessionId?: string | null;
126
198
  summary?: string | null;
@@ -151,7 +223,7 @@ export interface AFSContextPreset {
151
223
  }, {
152
224
  data: unknown;
153
225
  }>;
154
- format?: "default" | "tree" | AFSContextPresetOptionAgent<{
226
+ format?: "default" | "simple-list" | "tree" | AFSContextPresetOptionAgent<{
155
227
  data: unknown;
156
228
  }, {
157
229
  data: unknown;
package/lib/esm/afs.d.ts CHANGED
@@ -9,6 +9,11 @@ export declare class AFS extends Emitter<AFSRootEvents> implements AFSRoot {
9
9
  name: string;
10
10
  constructor(options?: AFSOptions);
11
11
  private modules;
12
+ /**
13
+ * Check if write operations are allowed for the given module.
14
+ * Throws AFSReadonlyError if not allowed.
15
+ */
16
+ private checkWritePermission;
12
17
  mount(module: AFSModule): this;
13
18
  listModules(): Promise<{
14
19
  name: string;
@@ -28,5 +33,10 @@ export declare class AFS extends Emitter<AFSRootEvents> implements AFSRoot {
28
33
  private _search;
29
34
  private findModules;
30
35
  exec(path: string, args: Record<string, any>, options: AFSExecOptions): Promise<AFSExecResult>;
36
+ private buildSimpleListView;
31
37
  private buildTreeView;
38
+ private buildMetadataSuffix;
39
+ private physicalPath?;
40
+ initializePhysicalPath(): Promise<string>;
41
+ cleanupPhysicalPath(): Promise<void>;
32
42
  }
package/lib/esm/afs.js CHANGED
@@ -1,6 +1,9 @@
1
+ import { nodejs } from "@aigne/platform-helpers/nodejs/index.js";
2
+ import { v7 } from "@aigne/uuid";
1
3
  import { Emitter } from "strict-event-emitter";
2
4
  import { joinURL } from "ufo";
3
5
  import { z } from "zod";
6
+ import { AFSReadonlyError } from "./error.js";
4
7
  import { afsEntrySchema, } from "./type.js";
5
8
  const DEFAULT_MAX_DEPTH = 1;
6
9
  const MODULES_ROOT_DIR = "/modules";
@@ -15,12 +18,22 @@ export class AFS extends Emitter {
15
18
  }
16
19
  }
17
20
  modules = new Map();
21
+ /**
22
+ * Check if write operations are allowed for the given module.
23
+ * Throws AFSReadonlyError if not allowed.
24
+ */
25
+ checkWritePermission(module, operation, path) {
26
+ // Module-level readonly (undefined means readonly by default)
27
+ if (module.accessMode !== "readwrite") {
28
+ throw new AFSReadonlyError(`Module '${module.name}' is readonly, cannot perform ${operation} to ${path}`);
29
+ }
30
+ }
18
31
  mount(module) {
19
- let path = joinURL("/", module.name);
20
- if (!/^\/[^/]+$/.test(path)) {
21
- throw new Error(`Invalid mount path: ${path}. Must start with '/' and contain no other '/'`);
32
+ // Validate module name (should not contain '/')
33
+ if (module.name.includes("/")) {
34
+ throw new Error(`Invalid module name: ${module.name}. Module name must not contain '/'`);
22
35
  }
23
- path = joinURL(MODULES_ROOT_DIR, path);
36
+ const path = joinURL(MODULES_ROOT_DIR, module.name);
24
37
  if (this.modules.has(path)) {
25
38
  throw new Error(`Module already mounted at path: ${path}`);
26
39
  }
@@ -50,14 +63,36 @@ export class AFS extends Emitter {
50
63
  }
51
64
  async _list(path, options = {}) {
52
65
  const results = [];
66
+ // Special case: listing root "/" should return /modules directory
67
+ if (path === "/" && this.modules.size > 0) {
68
+ const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
69
+ // Always include /modules directory first
70
+ results.push({
71
+ id: "modules",
72
+ path: MODULES_ROOT_DIR,
73
+ summary: "All mounted modules",
74
+ });
75
+ if (maxDepth === 1) {
76
+ // Only show /modules directory
77
+ return { data: results };
78
+ }
79
+ // For maxDepth > 1, also get children of /modules with reduced depth
80
+ const childrenResult = await this._list(MODULES_ROOT_DIR, {
81
+ ...options,
82
+ maxDepth: maxDepth - 1,
83
+ });
84
+ results.push(...childrenResult.data);
85
+ return { data: results };
86
+ }
53
87
  const matches = this.findModules(path, options);
54
88
  for (const matched of matches) {
55
- const moduleEntry = {
56
- id: matched.module.name,
57
- path: matched.remainedModulePath,
58
- summary: matched.module.description,
59
- };
60
89
  if (matched.maxDepth === 0) {
90
+ // When maxDepth is 0, show the module entry
91
+ const moduleEntry = {
92
+ id: matched.module.name,
93
+ path: matched.modulePath,
94
+ summary: matched.module.description,
95
+ };
61
96
  results.push(moduleEntry);
62
97
  continue;
63
98
  }
@@ -68,18 +103,16 @@ export class AFS extends Emitter {
68
103
  ...options,
69
104
  maxDepth: matched.maxDepth,
70
105
  });
71
- if (data.length) {
72
- results.push(...data.map((entry) => ({
73
- ...entry,
74
- path: joinURL(matched.modulePath, entry.path),
75
- })));
76
- }
77
- else {
78
- results.push(moduleEntry);
79
- }
106
+ const children = data.map((entry) => ({
107
+ ...entry,
108
+ path: joinURL(matched.modulePath, entry.path),
109
+ }));
110
+ // Always include all nodes (including the current path itself)
111
+ // This ensures consistent behavior across all listing scenarios
112
+ results.push(...children);
80
113
  }
81
114
  catch (error) {
82
- console.error(`Error listing from module at ${matched.modulePath}`, error);
115
+ throw new Error(`Error listing from module at ${matched.modulePath}: ${error.message}`);
83
116
  }
84
117
  }
85
118
  return { data: results };
@@ -104,6 +137,7 @@ export class AFS extends Emitter {
104
137
  const module = this.findModules(path, { exactMatch: true })[0];
105
138
  if (!module?.module.write)
106
139
  throw new Error(`No module found for path: ${path}`);
140
+ this.checkWritePermission(module.module, "write", path);
107
141
  const res = await module.module.write(module.subpath, content, options);
108
142
  return {
109
143
  ...res,
@@ -117,6 +151,7 @@ export class AFS extends Emitter {
117
151
  const module = this.findModules(path, { exactMatch: true })[0];
118
152
  if (!module?.module.delete)
119
153
  throw new Error(`No module found for path: ${path}`);
154
+ this.checkWritePermission(module.module, "delete", path);
120
155
  return await module.module.delete(module.subpath, options);
121
156
  }
122
157
  async rename(oldPath, newPath, options) {
@@ -129,6 +164,7 @@ export class AFS extends Emitter {
129
164
  if (!oldModule.module.rename) {
130
165
  throw new Error(`Module does not support rename operation: ${oldModule.modulePath}`);
131
166
  }
167
+ this.checkWritePermission(oldModule.module, "rename", oldPath);
132
168
  return await oldModule.module.rename(oldModule.subpath, newModule.subpath, options);
133
169
  }
134
170
  async search(path, query, options = {}) {
@@ -158,12 +194,14 @@ export class AFS extends Emitter {
158
194
  ? await dedupe.invoke({ data: mapped }, options).then((res) => res.data)
159
195
  : mapped;
160
196
  let formatted = deduped;
161
- if (format === "tree") {
197
+ if (format === "simple-list" || format === "tree") {
162
198
  const valid = z.array(afsEntrySchema).safeParse(deduped);
163
- if (valid.data)
164
- formatted = this.buildTreeView(valid.data);
165
- else
199
+ if (!valid.data)
166
200
  throw new Error("Tree format requires entries to be AFSEntry objects");
201
+ if (format === "tree")
202
+ formatted = this.buildTreeView(valid.data);
203
+ else if (format === "simple-list")
204
+ formatted = this.buildSimpleListView(valid.data);
167
205
  }
168
206
  else if (typeof format === "object" && typeof format.invoke === "function") {
169
207
  formatted = await format.invoke({ data: deduped }, options).then((res) => res.data);
@@ -191,7 +229,7 @@ export class AFS extends Emitter {
191
229
  messages.push(message);
192
230
  }
193
231
  catch (error) {
194
- console.error(`Error searching in module at ${modulePath}`, error);
232
+ throw new Error(`Error searching in module at ${modulePath}: ${error.message}`);
195
233
  }
196
234
  }
197
235
  return { data: results, message: messages.join("; ") };
@@ -230,6 +268,9 @@ export class AFS extends Emitter {
230
268
  throw new Error(`No module found for path: ${path}`);
231
269
  return await module.module.exec(module.subpath, args, options);
232
270
  }
271
+ buildSimpleListView(entries) {
272
+ return entries.map((entry) => `${entry.path}${this.buildMetadataSuffix(entry)}`);
273
+ }
233
274
  buildTreeView(entries) {
234
275
  const tree = {};
235
276
  const entryMap = new Map();
@@ -251,23 +292,7 @@ export class AFS extends Emitter {
251
292
  const isLast = index === keys.length - 1;
252
293
  const fullPath = currentPath ? `${currentPath}/${key}` : `/${key}`;
253
294
  const entry = entryMap.get(fullPath);
254
- // Build metadata suffix
255
- const metadataParts = [];
256
- // Children count
257
- const childrenCount = entry?.metadata?.childrenCount;
258
- if (typeof childrenCount === "number") {
259
- metadataParts.push(`${childrenCount} items`);
260
- }
261
- // Children truncated
262
- if (entry?.metadata?.childrenTruncated) {
263
- metadataParts.push("truncated");
264
- }
265
- // Executable
266
- if (entry?.metadata?.execute) {
267
- metadataParts.push("executable");
268
- }
269
- const metadataSuffix = metadataParts.length > 0 ? ` [${metadataParts.join(", ")}]` : "";
270
- result += `${prefix}${isLast ? "└── " : "├── "}${key}${metadataSuffix}`;
295
+ result += `${prefix}${isLast ? "└── " : "├── "}${key}${entry ? this.buildMetadataSuffix(entry) : ""}`;
271
296
  result += `\n`;
272
297
  result += renderTree(node[key], `${prefix}${isLast ? " " : "│ "}`, fullPath);
273
298
  });
@@ -275,4 +300,47 @@ export class AFS extends Emitter {
275
300
  };
276
301
  return renderTree(tree);
277
302
  }
303
+ buildMetadataSuffix(entry) {
304
+ // Build metadata suffix
305
+ const metadataParts = [];
306
+ // Children count
307
+ const childrenCount = entry?.metadata?.childrenCount;
308
+ if (typeof childrenCount === "number") {
309
+ metadataParts.push(`${childrenCount} items`);
310
+ }
311
+ // Children truncated
312
+ if (entry?.metadata?.childrenTruncated) {
313
+ metadataParts.push("truncated");
314
+ }
315
+ // Gitignored
316
+ if (entry?.metadata?.gitignored) {
317
+ metadataParts.push("gitignored");
318
+ }
319
+ // Executable
320
+ if (entry?.metadata?.execute) {
321
+ metadataParts.push("executable");
322
+ }
323
+ const metadataSuffix = metadataParts.length > 0 ? ` [${metadataParts.join(", ")}]` : "";
324
+ return metadataSuffix;
325
+ }
326
+ physicalPath;
327
+ async initializePhysicalPath() {
328
+ this.physicalPath ??= (async () => {
329
+ const rootDir = nodejs.path.join(nodejs.os.tmpdir(), v7());
330
+ await nodejs.fs.mkdir(rootDir, { recursive: true });
331
+ for (const [modulePath, module] of this.modules) {
332
+ const physicalModulePath = nodejs.path.join(rootDir, modulePath);
333
+ await nodejs.fs.mkdir(nodejs.path.dirname(physicalModulePath), { recursive: true });
334
+ await module.symlinkToPhysical?.(physicalModulePath);
335
+ }
336
+ return rootDir;
337
+ })();
338
+ return this.physicalPath;
339
+ }
340
+ async cleanupPhysicalPath() {
341
+ if (this.physicalPath) {
342
+ await nodejs.fs.rm(await this.physicalPath, { recursive: true, force: true });
343
+ this.physicalPath = undefined;
344
+ }
345
+ }
278
346
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Base error class for all AFS errors.
3
+ */
4
+ export declare class AFSError extends Error {
5
+ readonly code: string;
6
+ constructor(message: string, code: string);
7
+ }
8
+ /**
9
+ * Error thrown when attempting write operations on a readonly AFS or module.
10
+ */
11
+ export declare class AFSReadonlyError extends AFSError {
12
+ constructor(message: string);
13
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Base error class for all AFS errors.
3
+ */
4
+ export class AFSError extends Error {
5
+ code;
6
+ constructor(message, code) {
7
+ super(message);
8
+ this.name = "AFSError";
9
+ this.code = code;
10
+ }
11
+ }
12
+ /**
13
+ * Error thrown when attempting write operations on a readonly AFS or module.
14
+ */
15
+ export class AFSReadonlyError extends AFSError {
16
+ constructor(message) {
17
+ super(message, "AFS_READONLY");
18
+ this.name = "AFSReadonlyError";
19
+ }
20
+ }
@@ -1,2 +1,3 @@
1
1
  export * from "./afs.js";
2
+ export * from "./error.js";
2
3
  export * from "./type.js";
package/lib/esm/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./afs.js";
2
+ export * from "./error.js";
2
3
  export * from "./type.js";