@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/lib/dts/afs.d.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { Emitter } from "strict-event-emitter";
|
|
2
|
-
import { type AFSContext, type AFSDeleteOptions, type AFSDeleteResult, type AFSExecOptions, type AFSExecResult, type AFSModule, type AFSReadOptions, type AFSReadResult, type AFSRenameOptions, type AFSRenameResult, type AFSRoot, type AFSRootEvents, type AFSRootListOptions, type AFSRootListResult, type AFSRootSearchOptions, type AFSRootSearchResult, type AFSWriteEntryPayload, type AFSWriteOptions, type AFSWriteResult } from "./type.js";
|
|
3
|
-
export interface AFSOptions {
|
|
4
|
-
modules?: AFSModule[];
|
|
5
|
-
context?: AFSContext;
|
|
6
|
-
}
|
|
7
|
-
export declare class AFS extends Emitter<AFSRootEvents> implements AFSRoot {
|
|
8
|
-
options: AFSOptions;
|
|
9
|
-
name: string;
|
|
10
|
-
constructor(options?: AFSOptions);
|
|
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;
|
|
17
|
-
mount(module: AFSModule): this;
|
|
18
|
-
listModules(): Promise<{
|
|
19
|
-
name: string;
|
|
20
|
-
path: string;
|
|
21
|
-
description?: string;
|
|
22
|
-
module: AFSModule;
|
|
23
|
-
}[]>;
|
|
24
|
-
list(path: string, options?: AFSRootListOptions): Promise<AFSRootListResult>;
|
|
25
|
-
private _list;
|
|
26
|
-
read(path: string, _options?: AFSReadOptions): Promise<AFSReadResult>;
|
|
27
|
-
write(path: string, content: AFSWriteEntryPayload, options?: AFSWriteOptions): Promise<AFSWriteResult>;
|
|
28
|
-
delete(path: string, options?: AFSDeleteOptions): Promise<AFSDeleteResult>;
|
|
29
|
-
rename(oldPath: string, newPath: string, options?: AFSRenameOptions): Promise<AFSRenameResult>;
|
|
30
|
-
search(path: string, query: string, options?: AFSRootSearchOptions): Promise<AFSRootSearchResult>;
|
|
31
|
-
private processWithPreset;
|
|
32
|
-
private _select;
|
|
33
|
-
private _search;
|
|
34
|
-
private findModules;
|
|
35
|
-
exec(path: string, args: Record<string, any>, options: AFSExecOptions): Promise<AFSExecResult>;
|
|
36
|
-
private buildSimpleListView;
|
|
37
|
-
private buildTreeView;
|
|
38
|
-
private buildMetadataSuffix;
|
|
39
|
-
private physicalPath?;
|
|
40
|
-
initializePhysicalPath(): Promise<string>;
|
|
41
|
-
cleanupPhysicalPath(): Promise<void>;
|
|
42
|
-
}
|
package/lib/dts/error.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
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
|
-
}
|
package/lib/dts/index.d.ts
DELETED
package/lib/dts/type.d.ts
DELETED
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
import type { Emitter } from "strict-event-emitter";
|
|
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 {
|
|
18
|
-
filter?: {
|
|
19
|
-
agentId?: string;
|
|
20
|
-
userId?: string;
|
|
21
|
-
sessionId?: string;
|
|
22
|
-
before?: string;
|
|
23
|
-
after?: string;
|
|
24
|
-
};
|
|
25
|
-
maxDepth?: number;
|
|
26
|
-
limit?: number;
|
|
27
|
-
orderBy?: [string, "asc" | "desc"][];
|
|
28
|
-
maxChildren?: number;
|
|
29
|
-
onOverflow?: "truncate";
|
|
30
|
-
/**
|
|
31
|
-
* Whether to disable .gitignore files when listing files.
|
|
32
|
-
* @default false
|
|
33
|
-
*/
|
|
34
|
-
disableGitignore?: boolean;
|
|
35
|
-
/**
|
|
36
|
-
* Glob pattern to filter entries by path.
|
|
37
|
-
* Examples: "*.ts", "**\/*.js", "src/**\/*.{ts,tsx}"
|
|
38
|
-
*/
|
|
39
|
-
pattern?: string;
|
|
40
|
-
}
|
|
41
|
-
export interface AFSListResult {
|
|
42
|
-
data: AFSEntry[];
|
|
43
|
-
message?: string;
|
|
44
|
-
}
|
|
45
|
-
export interface AFSSearchOptions extends AFSOperationOptions {
|
|
46
|
-
limit?: number;
|
|
47
|
-
caseSensitive?: boolean;
|
|
48
|
-
}
|
|
49
|
-
export interface AFSSearchResult {
|
|
50
|
-
data: AFSEntry[];
|
|
51
|
-
message?: string;
|
|
52
|
-
}
|
|
53
|
-
export interface AFSReadOptions extends AFSOperationOptions {
|
|
54
|
-
filter?: AFSListOptions["filter"];
|
|
55
|
-
}
|
|
56
|
-
export interface AFSReadResult {
|
|
57
|
-
data?: AFSEntry;
|
|
58
|
-
message?: string;
|
|
59
|
-
}
|
|
60
|
-
export interface AFSDeleteOptions extends AFSOperationOptions {
|
|
61
|
-
recursive?: boolean;
|
|
62
|
-
}
|
|
63
|
-
export interface AFSDeleteResult {
|
|
64
|
-
message?: string;
|
|
65
|
-
}
|
|
66
|
-
export interface AFSRenameOptions extends AFSOperationOptions {
|
|
67
|
-
overwrite?: boolean;
|
|
68
|
-
}
|
|
69
|
-
export interface AFSRenameResult {
|
|
70
|
-
message?: string;
|
|
71
|
-
}
|
|
72
|
-
export interface AFSWriteOptions extends AFSOperationOptions {
|
|
73
|
-
append?: boolean;
|
|
74
|
-
}
|
|
75
|
-
export interface AFSWriteResult {
|
|
76
|
-
data: AFSEntry;
|
|
77
|
-
message?: string;
|
|
78
|
-
context?: any;
|
|
79
|
-
}
|
|
80
|
-
export interface AFSWriteEntryPayload extends Omit<AFSEntry, "id" | "path"> {
|
|
81
|
-
}
|
|
82
|
-
export interface AFSExecOptions extends AFSOperationOptions {
|
|
83
|
-
}
|
|
84
|
-
export interface AFSExecResult {
|
|
85
|
-
data: Record<string, any>;
|
|
86
|
-
}
|
|
87
|
-
export interface AFSModule {
|
|
88
|
-
readonly name: string;
|
|
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;
|
|
103
|
-
onMount?(root: AFSRoot): void;
|
|
104
|
-
symlinkToPhysical?(path: string): Promise<void>;
|
|
105
|
-
list?(path: string, options?: AFSListOptions): Promise<AFSListResult>;
|
|
106
|
-
read?(path: string, options?: AFSReadOptions): Promise<AFSReadResult>;
|
|
107
|
-
write?(path: string, content: AFSWriteEntryPayload, options?: AFSWriteOptions): Promise<AFSWriteResult>;
|
|
108
|
-
delete?(path: string, options?: AFSDeleteOptions): Promise<AFSDeleteResult>;
|
|
109
|
-
rename?(oldPath: string, newPath: string, options?: AFSRenameOptions): Promise<AFSRenameResult>;
|
|
110
|
-
search?(path: string, query: string, options?: AFSSearchOptions): Promise<AFSSearchResult>;
|
|
111
|
-
exec?(path: string, args: Record<string, any>, options: AFSExecOptions): Promise<AFSExecResult>;
|
|
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
|
-
}
|
|
146
|
-
export type AFSRootEvents = {
|
|
147
|
-
agentSucceed: [
|
|
148
|
-
{
|
|
149
|
-
agentId?: string;
|
|
150
|
-
userId?: string;
|
|
151
|
-
sessionId?: string;
|
|
152
|
-
input: object;
|
|
153
|
-
output: object;
|
|
154
|
-
messages?: object[];
|
|
155
|
-
}
|
|
156
|
-
];
|
|
157
|
-
historyCreated: [{
|
|
158
|
-
entry: AFSEntry;
|
|
159
|
-
}, options: AFSOperationOptions];
|
|
160
|
-
};
|
|
161
|
-
export interface AFSRootListOptions extends AFSListOptions, AFSContextPreset {
|
|
162
|
-
preset?: string;
|
|
163
|
-
}
|
|
164
|
-
export interface AFSRootListResult extends Omit<AFSListResult, "data"> {
|
|
165
|
-
data: any;
|
|
166
|
-
}
|
|
167
|
-
export interface AFSRootSearchOptions extends AFSSearchOptions, AFSContextPreset {
|
|
168
|
-
preset?: string;
|
|
169
|
-
}
|
|
170
|
-
export interface AFSRootSearchResult extends Omit<AFSSearchResult, "data"> {
|
|
171
|
-
data: any;
|
|
172
|
-
}
|
|
173
|
-
export interface AFSRoot extends Emitter<AFSRootEvents>, AFSModule {
|
|
174
|
-
list(path: string, options?: AFSRootListOptions): Promise<AFSRootListResult>;
|
|
175
|
-
search(path: string, query: string, options: AFSRootSearchOptions): Promise<AFSRootSearchResult>;
|
|
176
|
-
initializePhysicalPath(): Promise<string>;
|
|
177
|
-
cleanupPhysicalPath(): Promise<void>;
|
|
178
|
-
}
|
|
179
|
-
export interface AFSEntryMetadata extends Record<string, any> {
|
|
180
|
-
execute?: {
|
|
181
|
-
name: string;
|
|
182
|
-
description?: string;
|
|
183
|
-
inputSchema?: Record<string, any>;
|
|
184
|
-
outputSchema?: Record<string, any>;
|
|
185
|
-
};
|
|
186
|
-
childrenCount?: number;
|
|
187
|
-
childrenTruncated?: boolean;
|
|
188
|
-
gitignored?: boolean;
|
|
189
|
-
}
|
|
190
|
-
export interface AFSEntry<T = any> {
|
|
191
|
-
id: string;
|
|
192
|
-
createdAt?: Date;
|
|
193
|
-
updatedAt?: Date;
|
|
194
|
-
path: string;
|
|
195
|
-
agentId?: string | null;
|
|
196
|
-
userId?: string | null;
|
|
197
|
-
sessionId?: string | null;
|
|
198
|
-
summary?: string | null;
|
|
199
|
-
description?: string | null;
|
|
200
|
-
metadata?: AFSEntryMetadata | null;
|
|
201
|
-
linkTo?: string | null;
|
|
202
|
-
content?: T;
|
|
203
|
-
}
|
|
204
|
-
export declare const afsEntrySchema: ZodType<AFSEntry>;
|
|
205
|
-
export interface AFSContextPreset {
|
|
206
|
-
/**
|
|
207
|
-
* The view template for presenting the search results.
|
|
208
|
-
*/
|
|
209
|
-
view?: string;
|
|
210
|
-
select?: AFSContextPresetOptionAgent<{
|
|
211
|
-
path: string;
|
|
212
|
-
query?: string;
|
|
213
|
-
}, {
|
|
214
|
-
data: string[];
|
|
215
|
-
}>;
|
|
216
|
-
per?: AFSContextPresetOptionAgent<{
|
|
217
|
-
data: AFSEntry;
|
|
218
|
-
}, {
|
|
219
|
-
data: unknown;
|
|
220
|
-
}>;
|
|
221
|
-
dedupe?: AFSContextPresetOptionAgent<{
|
|
222
|
-
data: unknown[];
|
|
223
|
-
}, {
|
|
224
|
-
data: unknown;
|
|
225
|
-
}>;
|
|
226
|
-
format?: "default" | "simple-list" | "tree" | AFSContextPresetOptionAgent<{
|
|
227
|
-
data: unknown;
|
|
228
|
-
}, {
|
|
229
|
-
data: unknown;
|
|
230
|
-
}>;
|
|
231
|
-
}
|
|
232
|
-
export interface AFSContextPresetOptionAgent<I = any, O = any> {
|
|
233
|
-
invoke(input: I, options?: any): Promise<O>;
|
|
234
|
-
}
|
|
235
|
-
export interface AFSContext {
|
|
236
|
-
search?: {
|
|
237
|
-
presets?: Record<string, AFSContextPreset>;
|
|
238
|
-
};
|
|
239
|
-
list?: {
|
|
240
|
-
presets?: Record<string, AFSContextPreset>;
|
|
241
|
-
};
|
|
242
|
-
}
|
package/lib/esm/afs.d.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { Emitter } from "strict-event-emitter";
|
|
2
|
-
import { type AFSContext, type AFSDeleteOptions, type AFSDeleteResult, type AFSExecOptions, type AFSExecResult, type AFSModule, type AFSReadOptions, type AFSReadResult, type AFSRenameOptions, type AFSRenameResult, type AFSRoot, type AFSRootEvents, type AFSRootListOptions, type AFSRootListResult, type AFSRootSearchOptions, type AFSRootSearchResult, type AFSWriteEntryPayload, type AFSWriteOptions, type AFSWriteResult } from "./type.js";
|
|
3
|
-
export interface AFSOptions {
|
|
4
|
-
modules?: AFSModule[];
|
|
5
|
-
context?: AFSContext;
|
|
6
|
-
}
|
|
7
|
-
export declare class AFS extends Emitter<AFSRootEvents> implements AFSRoot {
|
|
8
|
-
options: AFSOptions;
|
|
9
|
-
name: string;
|
|
10
|
-
constructor(options?: AFSOptions);
|
|
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;
|
|
17
|
-
mount(module: AFSModule): this;
|
|
18
|
-
listModules(): Promise<{
|
|
19
|
-
name: string;
|
|
20
|
-
path: string;
|
|
21
|
-
description?: string;
|
|
22
|
-
module: AFSModule;
|
|
23
|
-
}[]>;
|
|
24
|
-
list(path: string, options?: AFSRootListOptions): Promise<AFSRootListResult>;
|
|
25
|
-
private _list;
|
|
26
|
-
read(path: string, _options?: AFSReadOptions): Promise<AFSReadResult>;
|
|
27
|
-
write(path: string, content: AFSWriteEntryPayload, options?: AFSWriteOptions): Promise<AFSWriteResult>;
|
|
28
|
-
delete(path: string, options?: AFSDeleteOptions): Promise<AFSDeleteResult>;
|
|
29
|
-
rename(oldPath: string, newPath: string, options?: AFSRenameOptions): Promise<AFSRenameResult>;
|
|
30
|
-
search(path: string, query: string, options?: AFSRootSearchOptions): Promise<AFSRootSearchResult>;
|
|
31
|
-
private processWithPreset;
|
|
32
|
-
private _select;
|
|
33
|
-
private _search;
|
|
34
|
-
private findModules;
|
|
35
|
-
exec(path: string, args: Record<string, any>, options: AFSExecOptions): Promise<AFSExecResult>;
|
|
36
|
-
private buildSimpleListView;
|
|
37
|
-
private buildTreeView;
|
|
38
|
-
private buildMetadataSuffix;
|
|
39
|
-
private physicalPath?;
|
|
40
|
-
initializePhysicalPath(): Promise<string>;
|
|
41
|
-
cleanupPhysicalPath(): Promise<void>;
|
|
42
|
-
}
|
package/lib/esm/afs.js
DELETED
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
import { nodejs } from "@aigne/platform-helpers/nodejs/index.js";
|
|
2
|
-
import { v7 } from "@aigne/uuid";
|
|
3
|
-
import { Emitter } from "strict-event-emitter";
|
|
4
|
-
import { joinURL } from "ufo";
|
|
5
|
-
import { z } from "zod";
|
|
6
|
-
import { AFSReadonlyError } from "./error.js";
|
|
7
|
-
import { afsEntrySchema, } from "./type.js";
|
|
8
|
-
const DEFAULT_MAX_DEPTH = 1;
|
|
9
|
-
const MODULES_ROOT_DIR = "/modules";
|
|
10
|
-
export class AFS extends Emitter {
|
|
11
|
-
options;
|
|
12
|
-
name = "AFSRoot";
|
|
13
|
-
constructor(options = {}) {
|
|
14
|
-
super();
|
|
15
|
-
this.options = options;
|
|
16
|
-
for (const module of options?.modules ?? []) {
|
|
17
|
-
this.mount(module);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
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
|
-
}
|
|
31
|
-
mount(module) {
|
|
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 '/'`);
|
|
35
|
-
}
|
|
36
|
-
const path = joinURL(MODULES_ROOT_DIR, module.name);
|
|
37
|
-
if (this.modules.has(path)) {
|
|
38
|
-
throw new Error(`Module already mounted at path: ${path}`);
|
|
39
|
-
}
|
|
40
|
-
this.modules.set(path, module);
|
|
41
|
-
module.onMount?.(this);
|
|
42
|
-
return this;
|
|
43
|
-
}
|
|
44
|
-
async listModules() {
|
|
45
|
-
return Array.from(this.modules.entries()).map(([path, module]) => ({
|
|
46
|
-
path,
|
|
47
|
-
name: module.name,
|
|
48
|
-
description: module.description,
|
|
49
|
-
module,
|
|
50
|
-
}));
|
|
51
|
-
}
|
|
52
|
-
async list(path, options = {}) {
|
|
53
|
-
let preset;
|
|
54
|
-
if (options.preset) {
|
|
55
|
-
preset = this.options?.context?.list?.presets?.[options.preset];
|
|
56
|
-
if (!preset)
|
|
57
|
-
throw new Error(`Preset not found: ${options.preset}`);
|
|
58
|
-
}
|
|
59
|
-
return await this.processWithPreset(path, undefined, preset, {
|
|
60
|
-
...options,
|
|
61
|
-
defaultSelect: () => this._list(path, options),
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
async _list(path, options = {}) {
|
|
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
|
-
}
|
|
87
|
-
const matches = this.findModules(path, options);
|
|
88
|
-
for (const matched of matches) {
|
|
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
|
-
};
|
|
96
|
-
results.push(moduleEntry);
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
if (!matched.module.list)
|
|
100
|
-
continue;
|
|
101
|
-
try {
|
|
102
|
-
const { data } = await matched.module.list(matched.subpath, {
|
|
103
|
-
...options,
|
|
104
|
-
maxDepth: matched.maxDepth,
|
|
105
|
-
});
|
|
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);
|
|
113
|
-
}
|
|
114
|
-
catch (error) {
|
|
115
|
-
throw new Error(`Error listing from module at ${matched.modulePath}: ${error.message}`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return { data: results };
|
|
119
|
-
}
|
|
120
|
-
async read(path, _options) {
|
|
121
|
-
const modules = this.findModules(path, { exactMatch: true });
|
|
122
|
-
for (const { module, modulePath, subpath } of modules) {
|
|
123
|
-
const res = await module.read?.(subpath);
|
|
124
|
-
if (res?.data) {
|
|
125
|
-
return {
|
|
126
|
-
...res,
|
|
127
|
-
data: {
|
|
128
|
-
...res.data,
|
|
129
|
-
path: joinURL(modulePath, res.data.path),
|
|
130
|
-
},
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return { data: undefined, message: "File not found" };
|
|
135
|
-
}
|
|
136
|
-
async write(path, content, options) {
|
|
137
|
-
const module = this.findModules(path, { exactMatch: true })[0];
|
|
138
|
-
if (!module?.module.write)
|
|
139
|
-
throw new Error(`No module found for path: ${path}`);
|
|
140
|
-
this.checkWritePermission(module.module, "write", path);
|
|
141
|
-
const res = await module.module.write(module.subpath, content, options);
|
|
142
|
-
return {
|
|
143
|
-
...res,
|
|
144
|
-
data: {
|
|
145
|
-
...res.data,
|
|
146
|
-
path: joinURL(module.modulePath, res.data.path),
|
|
147
|
-
},
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
async delete(path, options) {
|
|
151
|
-
const module = this.findModules(path, { exactMatch: true })[0];
|
|
152
|
-
if (!module?.module.delete)
|
|
153
|
-
throw new Error(`No module found for path: ${path}`);
|
|
154
|
-
this.checkWritePermission(module.module, "delete", path);
|
|
155
|
-
return await module.module.delete(module.subpath, options);
|
|
156
|
-
}
|
|
157
|
-
async rename(oldPath, newPath, options) {
|
|
158
|
-
const oldModule = this.findModules(oldPath, { exactMatch: true })[0];
|
|
159
|
-
const newModule = this.findModules(newPath, { exactMatch: true })[0];
|
|
160
|
-
// Both paths must be in the same module
|
|
161
|
-
if (!oldModule || !newModule || oldModule.modulePath !== newModule.modulePath) {
|
|
162
|
-
throw new Error(`Cannot rename across different modules. Both paths must be in the same module.`);
|
|
163
|
-
}
|
|
164
|
-
if (!oldModule.module.rename) {
|
|
165
|
-
throw new Error(`Module does not support rename operation: ${oldModule.modulePath}`);
|
|
166
|
-
}
|
|
167
|
-
this.checkWritePermission(oldModule.module, "rename", oldPath);
|
|
168
|
-
return await oldModule.module.rename(oldModule.subpath, newModule.subpath, options);
|
|
169
|
-
}
|
|
170
|
-
async search(path, query, options = {}) {
|
|
171
|
-
let preset;
|
|
172
|
-
if (options.preset) {
|
|
173
|
-
preset = this.options?.context?.search?.presets?.[options.preset];
|
|
174
|
-
if (!preset)
|
|
175
|
-
throw new Error(`Preset not found: ${options.preset}`);
|
|
176
|
-
}
|
|
177
|
-
return await this.processWithPreset(path, query, preset, {
|
|
178
|
-
...options,
|
|
179
|
-
defaultSelect: () => this._search(path, query, options),
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
async processWithPreset(path, query, preset, options) {
|
|
183
|
-
const select = options.select || preset?.select;
|
|
184
|
-
const per = options.per || preset?.per;
|
|
185
|
-
const dedupe = options.dedupe || preset?.dedupe;
|
|
186
|
-
const format = options.format || preset?.format;
|
|
187
|
-
const entries = select
|
|
188
|
-
? (await this._select(path, query, select, options)).data
|
|
189
|
-
: (await options.defaultSelect()).data;
|
|
190
|
-
const mapped = per
|
|
191
|
-
? await Promise.all(entries.map((data) => per.invoke({ data }, options).then((res) => res.data)))
|
|
192
|
-
: entries;
|
|
193
|
-
const deduped = dedupe
|
|
194
|
-
? await dedupe.invoke({ data: mapped }, options).then((res) => res.data)
|
|
195
|
-
: mapped;
|
|
196
|
-
let formatted = deduped;
|
|
197
|
-
if (format === "simple-list" || format === "tree") {
|
|
198
|
-
const valid = z.array(afsEntrySchema).safeParse(deduped);
|
|
199
|
-
if (!valid.data)
|
|
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);
|
|
205
|
-
}
|
|
206
|
-
else if (typeof format === "object" && typeof format.invoke === "function") {
|
|
207
|
-
formatted = await format.invoke({ data: deduped }, options).then((res) => res.data);
|
|
208
|
-
}
|
|
209
|
-
return { data: formatted };
|
|
210
|
-
}
|
|
211
|
-
async _select(path, query, select, options) {
|
|
212
|
-
const { data } = await select.invoke({ path, query }, options);
|
|
213
|
-
const results = (await Promise.all(data.map((p) => this.read(p).then((res) => res.data)))).filter((i) => !!i);
|
|
214
|
-
return { data: results };
|
|
215
|
-
}
|
|
216
|
-
async _search(path, query, options) {
|
|
217
|
-
const results = [];
|
|
218
|
-
const messages = [];
|
|
219
|
-
for (const { module, modulePath, subpath } of this.findModules(path)) {
|
|
220
|
-
if (!module.search)
|
|
221
|
-
continue;
|
|
222
|
-
try {
|
|
223
|
-
const { data, message } = await module.search(subpath, query, options);
|
|
224
|
-
results.push(...data.map((entry) => ({
|
|
225
|
-
...entry,
|
|
226
|
-
path: joinURL(modulePath, entry.path),
|
|
227
|
-
})));
|
|
228
|
-
if (message)
|
|
229
|
-
messages.push(message);
|
|
230
|
-
}
|
|
231
|
-
catch (error) {
|
|
232
|
-
throw new Error(`Error searching in module at ${modulePath}: ${error.message}`);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return { data: results, message: messages.join("; ") };
|
|
236
|
-
}
|
|
237
|
-
findModules(path, options) {
|
|
238
|
-
const maxDepth = Math.max(options?.maxDepth ?? DEFAULT_MAX_DEPTH, 1);
|
|
239
|
-
const matched = [];
|
|
240
|
-
for (const [modulePath, module] of this.modules) {
|
|
241
|
-
const pathSegments = path.split("/").filter(Boolean);
|
|
242
|
-
const modulePathSegments = modulePath.split("/").filter(Boolean);
|
|
243
|
-
let newMaxDepth;
|
|
244
|
-
let subpath;
|
|
245
|
-
let remainedModulePath;
|
|
246
|
-
if (!options?.exactMatch && modulePath.startsWith(path)) {
|
|
247
|
-
newMaxDepth = Math.max(0, maxDepth - (modulePathSegments.length - pathSegments.length));
|
|
248
|
-
subpath = "/";
|
|
249
|
-
remainedModulePath = joinURL("/", ...modulePathSegments.slice(pathSegments.length).slice(0, maxDepth));
|
|
250
|
-
}
|
|
251
|
-
else if (path.startsWith(modulePath)) {
|
|
252
|
-
newMaxDepth = maxDepth;
|
|
253
|
-
subpath = joinURL("/", ...pathSegments.slice(modulePathSegments.length));
|
|
254
|
-
remainedModulePath = "/";
|
|
255
|
-
}
|
|
256
|
-
else {
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
if (newMaxDepth < 0)
|
|
260
|
-
continue;
|
|
261
|
-
matched.push({ module, modulePath, maxDepth: newMaxDepth, subpath, remainedModulePath });
|
|
262
|
-
}
|
|
263
|
-
return matched;
|
|
264
|
-
}
|
|
265
|
-
async exec(path, args, options) {
|
|
266
|
-
const module = this.findModules(path)[0];
|
|
267
|
-
if (!module?.module.exec)
|
|
268
|
-
throw new Error(`No module found for path: ${path}`);
|
|
269
|
-
return await module.module.exec(module.subpath, args, options);
|
|
270
|
-
}
|
|
271
|
-
buildSimpleListView(entries) {
|
|
272
|
-
return entries.map((entry) => `${entry.path}${this.buildMetadataSuffix(entry)}`);
|
|
273
|
-
}
|
|
274
|
-
buildTreeView(entries) {
|
|
275
|
-
const tree = {};
|
|
276
|
-
const entryMap = new Map();
|
|
277
|
-
for (const entry of entries) {
|
|
278
|
-
entryMap.set(entry.path, entry);
|
|
279
|
-
const parts = entry.path.split("/").filter(Boolean);
|
|
280
|
-
let current = tree;
|
|
281
|
-
for (const part of parts) {
|
|
282
|
-
if (!current[part]) {
|
|
283
|
-
current[part] = {};
|
|
284
|
-
}
|
|
285
|
-
current = current[part];
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
const renderTree = (node, prefix = "", currentPath = "") => {
|
|
289
|
-
let result = "";
|
|
290
|
-
const keys = Object.keys(node);
|
|
291
|
-
keys.forEach((key, index) => {
|
|
292
|
-
const isLast = index === keys.length - 1;
|
|
293
|
-
const fullPath = currentPath ? `${currentPath}/${key}` : `/${key}`;
|
|
294
|
-
const entry = entryMap.get(fullPath);
|
|
295
|
-
result += `${prefix}${isLast ? "└── " : "├── "}${key}${entry ? this.buildMetadataSuffix(entry) : ""}`;
|
|
296
|
-
result += `\n`;
|
|
297
|
-
result += renderTree(node[key], `${prefix}${isLast ? " " : "│ "}`, fullPath);
|
|
298
|
-
});
|
|
299
|
-
return result;
|
|
300
|
-
};
|
|
301
|
-
return renderTree(tree);
|
|
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
|
-
}
|
|
346
|
-
}
|