@aigne/afs 1.11.0-beta → 1.11.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/LICENSE.md +17 -84
- package/README.md +4 -13
- package/dist/_virtual/rolldown_runtime.mjs +7 -0
- package/dist/afs.cjs +1330 -0
- package/dist/afs.d.cts +275 -0
- package/dist/afs.d.cts.map +1 -0
- package/dist/afs.d.mts +275 -0
- package/dist/afs.d.mts.map +1 -0
- package/dist/afs.mjs +1331 -0
- package/dist/afs.mjs.map +1 -0
- package/dist/capabilities/index.d.mts +2 -0
- package/dist/capabilities/types.d.cts +100 -0
- package/dist/capabilities/types.d.cts.map +1 -0
- package/dist/capabilities/types.d.mts +100 -0
- package/dist/capabilities/types.d.mts.map +1 -0
- package/dist/capabilities/world-mapping.cjs +20 -0
- package/dist/capabilities/world-mapping.d.cts +139 -0
- package/dist/capabilities/world-mapping.d.cts.map +1 -0
- package/dist/capabilities/world-mapping.d.mts +139 -0
- package/dist/capabilities/world-mapping.d.mts.map +1 -0
- package/dist/capabilities/world-mapping.mjs +20 -0
- package/dist/capabilities/world-mapping.mjs.map +1 -0
- package/dist/error.cjs +63 -0
- package/dist/error.d.cts +39 -0
- package/dist/error.d.cts.map +1 -0
- package/dist/error.d.mts +39 -0
- package/dist/error.d.mts.map +1 -0
- package/dist/error.mjs +59 -0
- package/dist/error.mjs.map +1 -0
- package/dist/index.cjs +72 -345
- package/dist/index.d.cts +18 -300
- package/dist/index.d.mts +20 -300
- package/dist/index.mjs +16 -342
- package/dist/loader/index.cjs +110 -0
- package/dist/loader/index.d.cts +48 -0
- package/dist/loader/index.d.cts.map +1 -0
- package/dist/loader/index.d.mts +48 -0
- package/dist/loader/index.d.mts.map +1 -0
- package/dist/loader/index.mjs +110 -0
- package/dist/loader/index.mjs.map +1 -0
- package/dist/meta/index.cjs +4 -0
- package/dist/meta/index.mjs +6 -0
- package/dist/meta/kind.cjs +161 -0
- package/dist/meta/kind.d.cts +134 -0
- package/dist/meta/kind.d.cts.map +1 -0
- package/dist/meta/kind.d.mts +134 -0
- package/dist/meta/kind.d.mts.map +1 -0
- package/dist/meta/kind.mjs +157 -0
- package/dist/meta/kind.mjs.map +1 -0
- package/dist/meta/path.cjs +116 -0
- package/dist/meta/path.d.cts +43 -0
- package/dist/meta/path.d.cts.map +1 -0
- package/dist/meta/path.d.mts +43 -0
- package/dist/meta/path.d.mts.map +1 -0
- package/dist/meta/path.mjs +112 -0
- package/dist/meta/path.mjs.map +1 -0
- package/dist/meta/type.d.cts +96 -0
- package/dist/meta/type.d.cts.map +1 -0
- package/dist/meta/type.d.mts +96 -0
- package/dist/meta/type.d.mts.map +1 -0
- package/dist/meta/validation.cjs +77 -0
- package/dist/meta/validation.d.cts +19 -0
- package/dist/meta/validation.d.cts.map +1 -0
- package/dist/meta/validation.d.mts +19 -0
- package/dist/meta/validation.d.mts.map +1 -0
- package/dist/meta/validation.mjs +77 -0
- package/dist/meta/validation.mjs.map +1 -0
- package/dist/meta/well-known-kinds.cjs +228 -0
- package/dist/meta/well-known-kinds.d.cts +52 -0
- package/dist/meta/well-known-kinds.d.cts.map +1 -0
- package/dist/meta/well-known-kinds.d.mts +52 -0
- package/dist/meta/well-known-kinds.d.mts.map +1 -0
- package/dist/meta/well-known-kinds.mjs +219 -0
- package/dist/meta/well-known-kinds.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.cts +141 -0
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.cts.map +1 -0
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.mts +141 -0
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.mts.map +1 -0
- package/dist/path.cjs +255 -0
- package/dist/path.d.cts +93 -0
- package/dist/path.d.cts.map +1 -0
- package/dist/path.d.mts +93 -0
- package/dist/path.d.mts.map +1 -0
- package/dist/path.mjs +249 -0
- package/dist/path.mjs.map +1 -0
- package/dist/provider/base.cjs +425 -0
- package/dist/provider/base.d.cts +175 -0
- package/dist/provider/base.d.cts.map +1 -0
- package/dist/provider/base.d.mts +175 -0
- package/dist/provider/base.d.mts.map +1 -0
- package/dist/provider/base.mjs +426 -0
- package/dist/provider/base.mjs.map +1 -0
- package/dist/provider/decorators.cjs +268 -0
- package/dist/provider/decorators.d.cts +244 -0
- package/dist/provider/decorators.d.cts.map +1 -0
- package/dist/provider/decorators.d.mts +244 -0
- package/dist/provider/decorators.d.mts.map +1 -0
- package/dist/provider/decorators.mjs +256 -0
- package/dist/provider/decorators.mjs.map +1 -0
- package/dist/provider/index.cjs +19 -0
- package/dist/provider/index.d.cts +5 -0
- package/dist/provider/index.d.mts +5 -0
- package/dist/provider/index.mjs +5 -0
- package/dist/provider/router.cjs +185 -0
- package/dist/provider/router.d.cts +50 -0
- package/dist/provider/router.d.cts.map +1 -0
- package/dist/provider/router.d.mts +50 -0
- package/dist/provider/router.d.mts.map +1 -0
- package/dist/provider/router.mjs +185 -0
- package/dist/provider/router.mjs.map +1 -0
- package/dist/provider/types.d.cts +113 -0
- package/dist/provider/types.d.cts.map +1 -0
- package/dist/provider/types.d.mts +113 -0
- package/dist/provider/types.d.mts.map +1 -0
- package/dist/registry.cjs +358 -0
- package/dist/registry.d.cts +96 -0
- package/dist/registry.d.cts.map +1 -0
- package/dist/registry.d.mts +96 -0
- package/dist/registry.d.mts.map +1 -0
- package/dist/registry.mjs +360 -0
- package/dist/registry.mjs.map +1 -0
- package/dist/type.cjs +34 -0
- package/dist/type.d.cts +420 -0
- package/dist/type.d.cts.map +1 -0
- package/dist/type.d.mts +420 -0
- package/dist/type.d.mts.map +1 -0
- package/dist/type.mjs +33 -0
- package/dist/type.mjs.map +1 -0
- package/dist/utils/camelize.d.cts.map +1 -1
- package/dist/utils/camelize.d.mts.map +1 -1
- package/dist/utils/schema.cjs +129 -0
- package/dist/utils/schema.d.cts +65 -0
- package/dist/utils/schema.d.cts.map +1 -0
- package/dist/utils/schema.d.mts +65 -0
- package/dist/utils/schema.d.mts.map +1 -0
- package/dist/utils/schema.mjs +124 -0
- package/dist/utils/schema.mjs.map +1 -0
- package/dist/utils/type-utils.d.cts.map +1 -1
- package/dist/utils/type-utils.d.mts.map +1 -1
- package/dist/utils/uri-template.cjs +123 -0
- package/dist/utils/uri-template.d.cts +48 -0
- package/dist/utils/uri-template.d.cts.map +1 -0
- package/dist/utils/uri-template.d.mts +48 -0
- package/dist/utils/uri-template.d.mts.map +1 -0
- package/dist/utils/uri-template.mjs +120 -0
- package/dist/utils/uri-template.mjs.map +1 -0
- package/dist/utils/uri.cjs +49 -0
- package/dist/utils/uri.d.cts +34 -0
- package/dist/utils/uri.d.cts.map +1 -0
- package/dist/utils/uri.d.mts +34 -0
- package/dist/utils/uri.d.mts.map +1 -0
- package/dist/utils/uri.mjs +49 -0
- package/dist/utils/uri.mjs.map +1 -0
- package/dist/utils/zod.cjs +6 -8
- package/dist/utils/zod.d.cts +2 -2
- package/dist/utils/zod.d.cts.map +1 -1
- package/dist/utils/zod.d.mts +2 -2
- package/dist/utils/zod.d.mts.map +1 -1
- package/dist/utils/zod.mjs +6 -8
- package/dist/utils/zod.mjs.map +1 -1
- package/package.json +27 -4
- package/dist/index.d.cts.map +0 -1
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs.map +0 -1
package/dist/afs.mjs
ADDED
|
@@ -0,0 +1,1331 @@
|
|
|
1
|
+
import { AFSMountError, AFSNotFoundError, AFSReadonlyError, AFSValidationError } from "./error.mjs";
|
|
2
|
+
import { isCanonicalPath, parseCanonicalPath, validateModuleName, validatePath } from "./path.mjs";
|
|
3
|
+
import { v7 } from "@aigne/uuid";
|
|
4
|
+
import { joinURL } from "ufo";
|
|
5
|
+
|
|
6
|
+
//#region src/afs.ts
|
|
7
|
+
const DEFAULT_MAX_DEPTH = 1;
|
|
8
|
+
const MODULES_ROOT_DIR = "/modules";
|
|
9
|
+
/**
|
|
10
|
+
* Default timeout for mount check operations (10 seconds)
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_MOUNT_TIMEOUT = 1e4;
|
|
13
|
+
/**
|
|
14
|
+
* Execute a promise with a timeout.
|
|
15
|
+
* Throws an error if the promise does not resolve within the timeout.
|
|
16
|
+
*/
|
|
17
|
+
async function withTimeout(promise, ms) {
|
|
18
|
+
let timeoutId;
|
|
19
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
20
|
+
timeoutId = setTimeout(() => reject(/* @__PURE__ */ new Error(`Timeout after ${ms}ms`)), ms);
|
|
21
|
+
});
|
|
22
|
+
try {
|
|
23
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
24
|
+
} finally {
|
|
25
|
+
clearTimeout(timeoutId);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Get the timeout value for a provider.
|
|
30
|
+
* Returns provider.timeout if set, otherwise DEFAULT_MOUNT_TIMEOUT.
|
|
31
|
+
*/
|
|
32
|
+
function getTimeout(provider) {
|
|
33
|
+
return provider.timeout ?? DEFAULT_MOUNT_TIMEOUT;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get error message from an error, handling timeout specially.
|
|
37
|
+
*/
|
|
38
|
+
function getMountErrorMessage(err, timeout) {
|
|
39
|
+
if (err instanceof Error) {
|
|
40
|
+
if (err.message.includes("Timeout")) return `Timeout after ${timeout}ms`;
|
|
41
|
+
return err.message;
|
|
42
|
+
}
|
|
43
|
+
return String(err);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Characters forbidden in namespace names (security-sensitive)
|
|
47
|
+
*/
|
|
48
|
+
const NAMESPACE_FORBIDDEN_CHARS = [
|
|
49
|
+
"/",
|
|
50
|
+
"\\",
|
|
51
|
+
":",
|
|
52
|
+
";",
|
|
53
|
+
"|",
|
|
54
|
+
"&",
|
|
55
|
+
"`",
|
|
56
|
+
"$",
|
|
57
|
+
"(",
|
|
58
|
+
")",
|
|
59
|
+
">",
|
|
60
|
+
"<",
|
|
61
|
+
"\n",
|
|
62
|
+
"\r",
|
|
63
|
+
" ",
|
|
64
|
+
"\0"
|
|
65
|
+
];
|
|
66
|
+
/**
|
|
67
|
+
* Validate a namespace name for mount operations
|
|
68
|
+
* @throws Error if namespace is invalid
|
|
69
|
+
*/
|
|
70
|
+
function validateNamespaceName(namespace) {
|
|
71
|
+
if (namespace.trim() === "") throw new Error("Namespace cannot be empty or whitespace-only");
|
|
72
|
+
for (const char of NAMESPACE_FORBIDDEN_CHARS) if (namespace.includes(char)) throw new Error(`Namespace contains forbidden character: '${char}'`);
|
|
73
|
+
}
|
|
74
|
+
var AFS = class {
|
|
75
|
+
name = "AFSRoot";
|
|
76
|
+
/**
|
|
77
|
+
* Injectable method for loading and mounting a provider from a URI.
|
|
78
|
+
* Injected by CLI layer (afs-loader.ts) with full pipeline:
|
|
79
|
+
* 1. ProviderRegistry.createProvider({ uri, path, ...options }) → provider instance
|
|
80
|
+
* 2. afs.mount(provider, path)
|
|
81
|
+
*
|
|
82
|
+
* Used by root-level /.actions/mount action and registry mount action.
|
|
83
|
+
*/
|
|
84
|
+
loadProvider;
|
|
85
|
+
/**
|
|
86
|
+
* Injectable callback for removing a provider's config on unmount.
|
|
87
|
+
* Injected by CLI layer (afs-loader.ts) for persistence sync.
|
|
88
|
+
*/
|
|
89
|
+
unloadProvider;
|
|
90
|
+
/**
|
|
91
|
+
* Injectable callback for updating a provider's options in the config file.
|
|
92
|
+
* Injected by CLI layer (afs-loader.ts) for persistence sync.
|
|
93
|
+
* Used by providers to persist runtime config changes (e.g., default model selection).
|
|
94
|
+
*/
|
|
95
|
+
updateProviderConfig;
|
|
96
|
+
constructor(options = {}) {
|
|
97
|
+
this.options = options;
|
|
98
|
+
for (const module of options?.modules ?? []) this.mount(module, joinURL(MODULES_ROOT_DIR, module.name));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Internal storage: Map<compositeKey, MountEntry>
|
|
102
|
+
* compositeKey = `${namespace ?? ""}:${path}`
|
|
103
|
+
*/
|
|
104
|
+
mounts = /* @__PURE__ */ new Map();
|
|
105
|
+
/**
|
|
106
|
+
* Legacy compatibility: Map<path, AFSModule> for modules mounted via old API
|
|
107
|
+
* This is used internally by findModules for backward compatibility
|
|
108
|
+
*/
|
|
109
|
+
get modules() {
|
|
110
|
+
const map = /* @__PURE__ */ new Map();
|
|
111
|
+
for (const entry of this.mounts.values()) if (entry.namespace === null) map.set(entry.path, entry.module);
|
|
112
|
+
return map;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Create composite key for mount storage
|
|
116
|
+
*/
|
|
117
|
+
makeKey(namespace, path) {
|
|
118
|
+
return `${namespace ?? ""}:${path}`;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Check if write operations are allowed for the given module.
|
|
122
|
+
* Throws AFSReadonlyError if not allowed.
|
|
123
|
+
*/
|
|
124
|
+
/** Fire-and-forget change notification */
|
|
125
|
+
notifyChange(record) {
|
|
126
|
+
try {
|
|
127
|
+
this.options.onChange?.(record);
|
|
128
|
+
} catch {}
|
|
129
|
+
}
|
|
130
|
+
checkWritePermission(module, operation, path) {
|
|
131
|
+
if (module.accessMode !== "readwrite") throw new AFSReadonlyError(`Module '${module.name}' is readonly, cannot perform ${operation} to ${path}`);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Check provider availability on mount.
|
|
135
|
+
* Validates that the provider can successfully respond to stat/read
|
|
136
|
+
* and list (if childrenCount indicates children exist).
|
|
137
|
+
*
|
|
138
|
+
* @throws AFSMountError if validation fails
|
|
139
|
+
*/
|
|
140
|
+
async checkProviderOnMount(provider) {
|
|
141
|
+
const timeout = getTimeout(provider);
|
|
142
|
+
const name = provider.name;
|
|
143
|
+
let rootData;
|
|
144
|
+
if (provider.stat) try {
|
|
145
|
+
rootData = (await withTimeout(provider.stat("/"), timeout)).data;
|
|
146
|
+
} catch (err) {
|
|
147
|
+
throw new AFSMountError(name, "stat", getMountErrorMessage(err, timeout));
|
|
148
|
+
}
|
|
149
|
+
else if (provider.read) try {
|
|
150
|
+
const result = await withTimeout(provider.read("/"), timeout);
|
|
151
|
+
if (result.data) rootData = {
|
|
152
|
+
path: result.data.path,
|
|
153
|
+
meta: result.data.meta ?? void 0
|
|
154
|
+
};
|
|
155
|
+
} catch (err) {
|
|
156
|
+
throw new AFSMountError(name, "read", getMountErrorMessage(err, timeout));
|
|
157
|
+
}
|
|
158
|
+
else throw new AFSMountError(name, "read", "Provider has no stat or read method");
|
|
159
|
+
if (!rootData) throw new AFSMountError(name, provider.stat ? "stat" : "read", "Root path returned undefined data");
|
|
160
|
+
const childrenCount = rootData.meta?.childrenCount;
|
|
161
|
+
if (childrenCount === -1 || typeof childrenCount === "number" && childrenCount > 0) {
|
|
162
|
+
if (!provider.list) throw new AFSMountError(name, "list", "Provider has childrenCount but no list method");
|
|
163
|
+
try {
|
|
164
|
+
const listResult = await withTimeout(provider.list("/"), timeout);
|
|
165
|
+
if (!listResult.data || listResult.data.length === 0) throw new AFSMountError(name, "list", "childrenCount indicates children but list returned empty");
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (err instanceof AFSMountError) throw err;
|
|
168
|
+
throw new AFSMountError(name, "list", getMountErrorMessage(err, timeout));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Mount a module at a path in a namespace
|
|
174
|
+
*
|
|
175
|
+
* @param module - The module to mount
|
|
176
|
+
* @param path - The path to mount at (optional, defaults to /modules/{module.name} for backward compatibility)
|
|
177
|
+
* @param options - Mount options (namespace, replace)
|
|
178
|
+
*/
|
|
179
|
+
async mount(module, path, options) {
|
|
180
|
+
validateModuleName(module.name);
|
|
181
|
+
const normalizedPath = validatePath(path ?? joinURL(MODULES_ROOT_DIR, module.name));
|
|
182
|
+
const namespace = options?.namespace === void 0 ? null : options.namespace;
|
|
183
|
+
if (namespace !== null) {
|
|
184
|
+
if (namespace === "") throw new Error("Namespace cannot be empty or whitespace-only");
|
|
185
|
+
validateNamespaceName(namespace);
|
|
186
|
+
}
|
|
187
|
+
const key = this.makeKey(namespace, normalizedPath);
|
|
188
|
+
if (this.mounts.has(key)) if (options?.replace) this.mounts.delete(key);
|
|
189
|
+
else throw new Error(`Mount conflict: path '${normalizedPath}' already mounted in namespace '${namespace ?? "default"}'`);
|
|
190
|
+
for (const entry of this.mounts.values()) {
|
|
191
|
+
if (entry.namespace !== namespace) continue;
|
|
192
|
+
const existingPath = entry.path;
|
|
193
|
+
if (normalizedPath === "/" || existingPath === "/") throw new Error(`Mount conflict: path '${normalizedPath}' conflicts with existing mount '${existingPath}' in namespace '${namespace ?? "default"}'`);
|
|
194
|
+
if (existingPath.startsWith(normalizedPath) && (existingPath === normalizedPath || existingPath.length === normalizedPath.length || existingPath[normalizedPath.length] === "/")) throw new Error(`Mount conflict: path '${normalizedPath}' conflicts with existing mount '${existingPath}' in namespace '${namespace ?? "default"}'`);
|
|
195
|
+
if (normalizedPath.startsWith(existingPath) && (normalizedPath === existingPath || normalizedPath.length === existingPath.length || normalizedPath[existingPath.length] === "/")) throw new Error(`Mount conflict: path '${normalizedPath}' conflicts with existing mount '${existingPath}' in namespace '${namespace ?? "default"}'`);
|
|
196
|
+
}
|
|
197
|
+
await this.checkProviderOnMount(module);
|
|
198
|
+
this.mounts.set(key, {
|
|
199
|
+
namespace,
|
|
200
|
+
path: normalizedPath,
|
|
201
|
+
module
|
|
202
|
+
});
|
|
203
|
+
module.onMount?.(this, normalizedPath);
|
|
204
|
+
this.notifyChange({
|
|
205
|
+
kind: "mount",
|
|
206
|
+
path: normalizedPath,
|
|
207
|
+
moduleName: module.name,
|
|
208
|
+
namespace,
|
|
209
|
+
timestamp: Date.now()
|
|
210
|
+
});
|
|
211
|
+
return this;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Get all mounts, optionally filtered by namespace
|
|
215
|
+
*
|
|
216
|
+
* @param namespace - Filter by namespace (undefined = all, null = default only)
|
|
217
|
+
*/
|
|
218
|
+
getMounts(namespace) {
|
|
219
|
+
const result = [];
|
|
220
|
+
for (const entry of this.mounts.values()) if (namespace === void 0 || entry.namespace === namespace) result.push({
|
|
221
|
+
namespace: entry.namespace,
|
|
222
|
+
path: entry.path,
|
|
223
|
+
module: entry.module
|
|
224
|
+
});
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Get all unique namespaces that have mounts
|
|
229
|
+
*/
|
|
230
|
+
getNamespaces() {
|
|
231
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
232
|
+
for (const entry of this.mounts.values()) namespaces.add(entry.namespace);
|
|
233
|
+
return Array.from(namespaces);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Unmount a module at a path in a namespace
|
|
237
|
+
*
|
|
238
|
+
* @param path - The path to unmount
|
|
239
|
+
* @param namespace - The namespace (undefined/null for default namespace)
|
|
240
|
+
* @returns true if unmounted, false if not found
|
|
241
|
+
*/
|
|
242
|
+
unmount(path, namespace) {
|
|
243
|
+
const normalizedPath = validatePath(path);
|
|
244
|
+
const ns = namespace === void 0 ? null : namespace;
|
|
245
|
+
const key = this.makeKey(ns, normalizedPath);
|
|
246
|
+
const entry = this.mounts.get(key);
|
|
247
|
+
if (entry) {
|
|
248
|
+
this.mounts.delete(key);
|
|
249
|
+
this.notifyChange({
|
|
250
|
+
kind: "unmount",
|
|
251
|
+
path: normalizedPath,
|
|
252
|
+
moduleName: entry.module.name,
|
|
253
|
+
namespace: entry.namespace,
|
|
254
|
+
timestamp: Date.now()
|
|
255
|
+
});
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Check if a path is mounted in a namespace
|
|
262
|
+
*
|
|
263
|
+
* @param path - The path to check
|
|
264
|
+
* @param namespace - The namespace (undefined/null for default namespace)
|
|
265
|
+
*/
|
|
266
|
+
isMounted(path, namespace) {
|
|
267
|
+
const normalizedPath = validatePath(path);
|
|
268
|
+
const ns = namespace === void 0 ? null : namespace;
|
|
269
|
+
const key = this.makeKey(ns, normalizedPath);
|
|
270
|
+
return this.mounts.has(key);
|
|
271
|
+
}
|
|
272
|
+
async listModules() {
|
|
273
|
+
return Array.from(this.mounts.values()).map((entry) => ({
|
|
274
|
+
path: entry.path,
|
|
275
|
+
namespace: entry.namespace,
|
|
276
|
+
name: entry.module.name,
|
|
277
|
+
description: entry.module.description,
|
|
278
|
+
module: entry.module
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Parse a path and extract namespace if it's a canonical path
|
|
283
|
+
* Returns the namespace and the path within the namespace
|
|
284
|
+
*/
|
|
285
|
+
parsePathWithNamespace(inputPath) {
|
|
286
|
+
if (isCanonicalPath(inputPath)) {
|
|
287
|
+
const parsed = parseCanonicalPath(inputPath);
|
|
288
|
+
return {
|
|
289
|
+
namespace: parsed.namespace,
|
|
290
|
+
path: parsed.path
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
namespace: null,
|
|
295
|
+
path: validatePath(inputPath)
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Find modules that can handle a path in a specific namespace
|
|
300
|
+
*/
|
|
301
|
+
findModulesInNamespace(path, namespace, options) {
|
|
302
|
+
const maxDepth = Math.max(options?.maxDepth ?? DEFAULT_MAX_DEPTH, 1);
|
|
303
|
+
const matched = [];
|
|
304
|
+
for (const entry of this.mounts.values()) {
|
|
305
|
+
if (entry.namespace !== namespace) continue;
|
|
306
|
+
const modulePath = entry.path;
|
|
307
|
+
const module = entry.module;
|
|
308
|
+
const pathSegments = path.split("/").filter(Boolean);
|
|
309
|
+
const modulePathSegments = modulePath.split("/").filter(Boolean);
|
|
310
|
+
let newMaxDepth;
|
|
311
|
+
let subpath;
|
|
312
|
+
let remainedModulePath;
|
|
313
|
+
const moduleUnderPath = !options?.exactMatch && modulePath.startsWith(path) && (modulePath === path || path === "/" || modulePath[path.length] === "/");
|
|
314
|
+
const pathUnderModule = path.startsWith(modulePath) && (path === modulePath || modulePath === "/" || path[modulePath.length] === "/");
|
|
315
|
+
if (moduleUnderPath) {
|
|
316
|
+
newMaxDepth = Math.max(0, maxDepth - (modulePathSegments.length - pathSegments.length));
|
|
317
|
+
subpath = "/";
|
|
318
|
+
remainedModulePath = joinURL("/", ...modulePathSegments.slice(pathSegments.length).slice(0, maxDepth));
|
|
319
|
+
} else if (pathUnderModule) {
|
|
320
|
+
newMaxDepth = maxDepth;
|
|
321
|
+
subpath = joinURL("/", ...pathSegments.slice(modulePathSegments.length));
|
|
322
|
+
remainedModulePath = "/";
|
|
323
|
+
} else continue;
|
|
324
|
+
if (newMaxDepth < 0) continue;
|
|
325
|
+
matched.push({
|
|
326
|
+
module,
|
|
327
|
+
modulePath,
|
|
328
|
+
maxDepth: newMaxDepth,
|
|
329
|
+
subpath,
|
|
330
|
+
remainedModulePath
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
return matched;
|
|
334
|
+
}
|
|
335
|
+
async list(path, options = {}) {
|
|
336
|
+
const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
|
|
337
|
+
if (normalizedPath === "/.actions" && namespace === null) return this.listRootActions();
|
|
338
|
+
return await this._list(normalizedPath, namespace, options);
|
|
339
|
+
}
|
|
340
|
+
async _list(path, namespace, options = {}) {
|
|
341
|
+
if (options?.maxDepth === 0) return { data: [] };
|
|
342
|
+
const results = [];
|
|
343
|
+
const hasModulesMounts = namespace === null && [...this.mounts.values()].some((m) => m.namespace === null && m.path.startsWith(`${MODULES_ROOT_DIR}/`));
|
|
344
|
+
if (path === "/" && hasModulesMounts) {
|
|
345
|
+
const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
346
|
+
let moduleCount = 0;
|
|
347
|
+
for (const entry of this.mounts.values()) if (entry.namespace === namespace) moduleCount++;
|
|
348
|
+
results.push({
|
|
349
|
+
id: "modules",
|
|
350
|
+
path: MODULES_ROOT_DIR,
|
|
351
|
+
summary: "All mounted modules",
|
|
352
|
+
meta: {
|
|
353
|
+
childrenCount: moduleCount,
|
|
354
|
+
description: "All mounted modules"
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
if (maxDepth === 1) return { data: results };
|
|
358
|
+
const childrenResult = await this._list(MODULES_ROOT_DIR, namespace, {
|
|
359
|
+
...options,
|
|
360
|
+
maxDepth: maxDepth - 1
|
|
361
|
+
});
|
|
362
|
+
results.push(...childrenResult.data);
|
|
363
|
+
return { data: results };
|
|
364
|
+
}
|
|
365
|
+
const matches = this.findModulesInNamespace(path, namespace, options);
|
|
366
|
+
if (matches.length === 0) return { data: results };
|
|
367
|
+
const virtualDirs = /* @__PURE__ */ new Map();
|
|
368
|
+
for (const matched of matches) {
|
|
369
|
+
if (matched.maxDepth === 0) {
|
|
370
|
+
const truncatedPath = joinURL(path, matched.remainedModulePath);
|
|
371
|
+
if (truncatedPath === matched.modulePath) {
|
|
372
|
+
const moduleEntry = {
|
|
373
|
+
id: matched.module.name,
|
|
374
|
+
path: matched.modulePath,
|
|
375
|
+
summary: matched.module.description,
|
|
376
|
+
meta: {
|
|
377
|
+
childrenCount: -1,
|
|
378
|
+
description: matched.module.description
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
results.push(moduleEntry);
|
|
382
|
+
} else virtualDirs.set(truncatedPath, (virtualDirs.get(truncatedPath) ?? 0) + 1);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (!matched.module.list) {
|
|
386
|
+
if (matched.module.read) try {
|
|
387
|
+
const childrenCount = (await matched.module.read(matched.subpath)).data?.meta?.childrenCount;
|
|
388
|
+
if (childrenCount === void 0 || childrenCount === 0) continue;
|
|
389
|
+
throw new Error(`Provider '${matched.module.name}' has childrenCount=${childrenCount} but does not implement list(). Providers with children must implement the list() method.`);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
if (error instanceof Error && error.message.includes("does not implement list")) throw error;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
const result = await matched.module.list(matched.subpath, {
|
|
398
|
+
...options,
|
|
399
|
+
maxDepth: matched.maxDepth
|
|
400
|
+
});
|
|
401
|
+
const children = result.data.map((entry) => ({
|
|
402
|
+
...entry,
|
|
403
|
+
path: joinURL(matched.modulePath, entry.path)
|
|
404
|
+
}));
|
|
405
|
+
results.push(...children);
|
|
406
|
+
if (result.message && children.length === 0) return {
|
|
407
|
+
data: results,
|
|
408
|
+
message: result.message
|
|
409
|
+
};
|
|
410
|
+
} catch (error) {
|
|
411
|
+
throw new Error(`Error listing from module at ${matched.modulePath}: ${error.message}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
for (const [dirPath] of virtualDirs) {
|
|
415
|
+
const dirName = dirPath.split("/").filter(Boolean).pop() || "";
|
|
416
|
+
results.push({
|
|
417
|
+
id: dirName,
|
|
418
|
+
path: dirPath,
|
|
419
|
+
meta: { childrenCount: -1 }
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
return { data: results };
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Check if a path should skip auto-enrichment.
|
|
426
|
+
* Paths ending with /.meta or /.actions should not be enriched
|
|
427
|
+
* to avoid recursive fetches, but their children (e.g., /.meta/kinds)
|
|
428
|
+
* can still be enriched.
|
|
429
|
+
*/
|
|
430
|
+
shouldSkipEnrich(path) {
|
|
431
|
+
return path.endsWith("/.meta") || path.endsWith("/.actions");
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Fetch actions for a path by listing path/.actions.
|
|
435
|
+
* Returns ActionSummary[] on success, [] on failure.
|
|
436
|
+
*/
|
|
437
|
+
async fetchActions(module, subpath) {
|
|
438
|
+
try {
|
|
439
|
+
const actionsPath = joinURL(subpath, ".actions");
|
|
440
|
+
const result = await module.list?.(actionsPath);
|
|
441
|
+
if (!result?.data) return [];
|
|
442
|
+
return result.data.filter((entry) => entry.meta?.kind === "afs:executable").map((entry) => ({
|
|
443
|
+
name: entry.id,
|
|
444
|
+
description: entry.meta?.description,
|
|
445
|
+
inputSchema: entry.meta?.inputSchema
|
|
446
|
+
}));
|
|
447
|
+
} catch {
|
|
448
|
+
return [];
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Fetch meta for a path by reading path/.meta.
|
|
453
|
+
* Returns the meta content on success, null on failure.
|
|
454
|
+
*/
|
|
455
|
+
async fetchMeta(module, subpath) {
|
|
456
|
+
try {
|
|
457
|
+
const metaPath = joinURL(subpath, ".meta");
|
|
458
|
+
const result = await module.read?.(metaPath);
|
|
459
|
+
if (!result?.data?.content) return null;
|
|
460
|
+
const content = result.data.content;
|
|
461
|
+
if (typeof content === "object" && content !== null && !Array.isArray(content)) return content;
|
|
462
|
+
return null;
|
|
463
|
+
} catch {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Type for data that can be enriched (has path, optional actions, optional meta)
|
|
469
|
+
*/
|
|
470
|
+
async enrichData(data, module, subpath) {
|
|
471
|
+
if (this.shouldSkipEnrich(subpath)) return data;
|
|
472
|
+
const result = { ...data };
|
|
473
|
+
const enrichPromises = [];
|
|
474
|
+
if (result.actions === void 0) enrichPromises.push(this.fetchActions(module, subpath).then((actions) => {
|
|
475
|
+
result.actions = actions;
|
|
476
|
+
}));
|
|
477
|
+
if (result.meta?.kind === void 0) enrichPromises.push(this.fetchMeta(module, subpath).then((meta) => {
|
|
478
|
+
if (meta) result.meta = {
|
|
479
|
+
...result.meta,
|
|
480
|
+
...meta
|
|
481
|
+
};
|
|
482
|
+
}));
|
|
483
|
+
await Promise.all(enrichPromises);
|
|
484
|
+
return result;
|
|
485
|
+
}
|
|
486
|
+
async read(path, _options) {
|
|
487
|
+
const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
|
|
488
|
+
if (namespace === null) {
|
|
489
|
+
if (normalizedPath.startsWith("/.actions/")) return this.readRootAction(normalizedPath);
|
|
490
|
+
if (normalizedPath === "/.actions") return this.readRootActions();
|
|
491
|
+
if (normalizedPath === "/.meta") return this.readRootMeta();
|
|
492
|
+
}
|
|
493
|
+
if (normalizedPath === "/.meta/.capabilities") return { data: {
|
|
494
|
+
id: ".capabilities",
|
|
495
|
+
path: "/.meta/.capabilities",
|
|
496
|
+
content: await this.aggregateCapabilities(namespace),
|
|
497
|
+
meta: { kind: "afs:capabilities" }
|
|
498
|
+
} };
|
|
499
|
+
const modules = this.findModulesInNamespace(normalizedPath, namespace, { exactMatch: true });
|
|
500
|
+
for (const { module, modulePath, subpath } of modules) {
|
|
501
|
+
const res = await module.read?.(subpath);
|
|
502
|
+
if (res?.data) {
|
|
503
|
+
const enrichedData = await this.enrichData(res.data, module, subpath);
|
|
504
|
+
return {
|
|
505
|
+
...res,
|
|
506
|
+
data: {
|
|
507
|
+
...enrichedData,
|
|
508
|
+
path: joinURL(modulePath, res.data.path)
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const virtualDir = this.resolveVirtualDirectory(normalizedPath, namespace);
|
|
514
|
+
if (virtualDir) return virtualDir;
|
|
515
|
+
throw new AFSNotFoundError(path);
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Check if a path is a virtual intermediate directory.
|
|
519
|
+
* Returns a read result if the path is a parent of one or more mount paths.
|
|
520
|
+
*/
|
|
521
|
+
resolveVirtualDirectory(path, namespace) {
|
|
522
|
+
const pathSegments = path.split("/").filter(Boolean);
|
|
523
|
+
const childNames = /* @__PURE__ */ new Set();
|
|
524
|
+
for (const entry of this.mounts.values()) {
|
|
525
|
+
if (entry.namespace !== namespace) continue;
|
|
526
|
+
const moduleSegments = entry.path.split("/").filter(Boolean);
|
|
527
|
+
if (moduleSegments.length <= pathSegments.length) continue;
|
|
528
|
+
let match = true;
|
|
529
|
+
for (let i = 0; i < pathSegments.length; i++) if (moduleSegments[i] !== pathSegments[i]) {
|
|
530
|
+
match = false;
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
if (match) childNames.add(moduleSegments[pathSegments.length]);
|
|
534
|
+
}
|
|
535
|
+
if (childNames.size === 0) return null;
|
|
536
|
+
return { data: {
|
|
537
|
+
id: pathSegments[pathSegments.length - 1] || "/",
|
|
538
|
+
path,
|
|
539
|
+
meta: { childrenCount: childNames.size }
|
|
540
|
+
} };
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Aggregate capabilities from all mounted providers.
|
|
544
|
+
*
|
|
545
|
+
* For each provider:
|
|
546
|
+
* - Read /.meta/.capabilities
|
|
547
|
+
* - Merge tools with provider prefix and mount path prefix
|
|
548
|
+
* - Merge actions with mount path prefix on discovery.pathTemplate
|
|
549
|
+
* - Silently skip providers that fail or don't implement capabilities
|
|
550
|
+
*/
|
|
551
|
+
async aggregateCapabilities(namespace) {
|
|
552
|
+
const allTools = [];
|
|
553
|
+
const allActions = [];
|
|
554
|
+
const skipped = [];
|
|
555
|
+
const allOperations = [];
|
|
556
|
+
const mounts = this.getMounts(namespace);
|
|
557
|
+
for (const mount of mounts) {
|
|
558
|
+
const { path: mountPath, module: provider } = mount;
|
|
559
|
+
try {
|
|
560
|
+
const content = (await provider.read?.("/.meta/.capabilities"))?.data?.content;
|
|
561
|
+
if (!content) continue;
|
|
562
|
+
const manifest = content;
|
|
563
|
+
for (const tool of manifest.tools ?? []) allTools.push({
|
|
564
|
+
...tool,
|
|
565
|
+
name: `${manifest.provider}.${tool.name}`,
|
|
566
|
+
path: joinURL(mountPath, tool.path)
|
|
567
|
+
});
|
|
568
|
+
for (const actionCatalog of manifest.actions ?? []) allActions.push({
|
|
569
|
+
...actionCatalog,
|
|
570
|
+
discovery: {
|
|
571
|
+
...actionCatalog.discovery,
|
|
572
|
+
pathTemplate: joinURL(mountPath, actionCatalog.discovery.pathTemplate)
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
if (manifest.operations) allOperations.push(manifest.operations);
|
|
576
|
+
} catch {
|
|
577
|
+
skipped.push(mountPath);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const result = {
|
|
581
|
+
schemaVersion: 1,
|
|
582
|
+
provider: "afs",
|
|
583
|
+
description: "AFS aggregated capabilities",
|
|
584
|
+
tools: allTools,
|
|
585
|
+
actions: allActions
|
|
586
|
+
};
|
|
587
|
+
if (allOperations.length > 0) result.operations = {
|
|
588
|
+
read: allOperations.some((o) => o.read),
|
|
589
|
+
list: allOperations.some((o) => o.list),
|
|
590
|
+
write: allOperations.some((o) => o.write),
|
|
591
|
+
delete: allOperations.some((o) => o.delete),
|
|
592
|
+
search: allOperations.some((o) => o.search),
|
|
593
|
+
exec: allOperations.some((o) => o.exec),
|
|
594
|
+
stat: allOperations.some((o) => o.stat),
|
|
595
|
+
explain: allOperations.some((o) => o.explain)
|
|
596
|
+
};
|
|
597
|
+
if (skipped.length > 0) {
|
|
598
|
+
result.partial = true;
|
|
599
|
+
result.skipped = skipped;
|
|
600
|
+
}
|
|
601
|
+
return result;
|
|
602
|
+
}
|
|
603
|
+
async write(path, content, options) {
|
|
604
|
+
const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
|
|
605
|
+
const module = this.findModulesInNamespace(normalizedPath, namespace, { exactMatch: true })[0];
|
|
606
|
+
if (!module?.module.write) throw new Error(`No module found for path: ${normalizedPath} in namespace '${namespace ?? "default"}'`);
|
|
607
|
+
this.checkWritePermission(module.module, "write", path);
|
|
608
|
+
const res = await module.module.write(module.subpath, content, options);
|
|
609
|
+
const result = {
|
|
610
|
+
...res,
|
|
611
|
+
data: {
|
|
612
|
+
...res.data,
|
|
613
|
+
path: joinURL(module.modulePath, res.data.path)
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
this.notifyChange({
|
|
617
|
+
kind: "write",
|
|
618
|
+
path: result.data.path,
|
|
619
|
+
moduleName: module.module.name,
|
|
620
|
+
timestamp: Date.now()
|
|
621
|
+
});
|
|
622
|
+
return result;
|
|
623
|
+
}
|
|
624
|
+
async delete(path, options) {
|
|
625
|
+
const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
|
|
626
|
+
const module = this.findModulesInNamespace(normalizedPath, namespace, { exactMatch: true })[0];
|
|
627
|
+
if (!module?.module.delete) throw new Error(`No module found for path: ${normalizedPath} in namespace '${namespace ?? "default"}'`);
|
|
628
|
+
this.checkWritePermission(module.module, "delete", path);
|
|
629
|
+
const result = await module.module.delete(module.subpath, options);
|
|
630
|
+
this.notifyChange({
|
|
631
|
+
kind: "delete",
|
|
632
|
+
path: joinURL(module.modulePath, module.subpath),
|
|
633
|
+
moduleName: module.module.name,
|
|
634
|
+
timestamp: Date.now()
|
|
635
|
+
});
|
|
636
|
+
return result;
|
|
637
|
+
}
|
|
638
|
+
async rename(oldPath, newPath, options) {
|
|
639
|
+
const { namespace: oldNamespace, path: normalizedOldPath } = this.parsePathWithNamespace(oldPath);
|
|
640
|
+
const { namespace: newNamespace, path: normalizedNewPath } = this.parsePathWithNamespace(newPath);
|
|
641
|
+
if (oldNamespace !== newNamespace) throw new Error(`Cannot rename across different namespaces.`);
|
|
642
|
+
const oldModule = this.findModulesInNamespace(normalizedOldPath, oldNamespace, { exactMatch: true })[0];
|
|
643
|
+
const newModule = this.findModulesInNamespace(normalizedNewPath, newNamespace, { exactMatch: true })[0];
|
|
644
|
+
if (!oldModule || !newModule || oldModule.modulePath !== newModule.modulePath) throw new Error(`Cannot rename across different modules. Both paths must be in the same module.`);
|
|
645
|
+
if (!oldModule.module.rename) throw new Error(`Module does not support rename operation: ${oldModule.modulePath}`);
|
|
646
|
+
this.checkWritePermission(oldModule.module, "rename", oldPath);
|
|
647
|
+
const result = await oldModule.module.rename(oldModule.subpath, newModule.subpath, options);
|
|
648
|
+
this.notifyChange({
|
|
649
|
+
kind: "rename",
|
|
650
|
+
path: joinURL(oldModule.modulePath, oldModule.subpath),
|
|
651
|
+
moduleName: oldModule.module.name,
|
|
652
|
+
namespace: oldNamespace,
|
|
653
|
+
meta: { newPath: joinURL(newModule.modulePath, newModule.subpath) },
|
|
654
|
+
timestamp: Date.now()
|
|
655
|
+
});
|
|
656
|
+
return result;
|
|
657
|
+
}
|
|
658
|
+
async search(path, query, options = {}) {
|
|
659
|
+
const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
|
|
660
|
+
return await this._search(normalizedPath, namespace, query, options);
|
|
661
|
+
}
|
|
662
|
+
async _search(path, namespace, query, options) {
|
|
663
|
+
const results = [];
|
|
664
|
+
const messages = [];
|
|
665
|
+
for (const { module, modulePath, subpath } of this.findModulesInNamespace(path, namespace)) {
|
|
666
|
+
if (!module.search) continue;
|
|
667
|
+
try {
|
|
668
|
+
const { data, message } = await module.search(subpath, query, options);
|
|
669
|
+
results.push(...data.map((entry) => ({
|
|
670
|
+
...entry,
|
|
671
|
+
path: joinURL(modulePath, entry.path)
|
|
672
|
+
})));
|
|
673
|
+
if (message) messages.push(message);
|
|
674
|
+
} catch (error) {
|
|
675
|
+
throw new Error(`Error searching in module at ${modulePath}: ${error.message}`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
data: results,
|
|
680
|
+
message: messages.join("; ")
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
async exec(path, args, options = {}) {
|
|
684
|
+
const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
|
|
685
|
+
if (normalizedPath.startsWith("/.actions/")) return await this.execRootAction(normalizedPath, args);
|
|
686
|
+
const module = this.findModulesInNamespace(normalizedPath, namespace)[0];
|
|
687
|
+
if (!module?.module.exec) throw new Error(`No module found for path: ${normalizedPath} in namespace '${namespace ?? "default"}'`);
|
|
688
|
+
this.checkWritePermission(module.module, "exec", path);
|
|
689
|
+
await this.validateExecInput(module.module, module.subpath, args);
|
|
690
|
+
const enhancedOptions = {
|
|
691
|
+
...options,
|
|
692
|
+
context: {
|
|
693
|
+
...options?.context,
|
|
694
|
+
afs: this
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
return await module.module.exec(module.subpath, args, enhancedOptions);
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Read a root-level action entry (/.actions/mount, /.actions/unmount).
|
|
701
|
+
* Returns the action entry with metadata including inputSchema.
|
|
702
|
+
*/
|
|
703
|
+
async readRootAction(path) {
|
|
704
|
+
const actionName = path.slice(10);
|
|
705
|
+
const { data: actions } = await this.listRootActions();
|
|
706
|
+
const entry = actions.find((a) => a.id === actionName);
|
|
707
|
+
if (!entry) throw new AFSNotFoundError(path, `Root action not found: ${actionName}`);
|
|
708
|
+
const action = entry.actions?.[0];
|
|
709
|
+
return { data: {
|
|
710
|
+
...entry,
|
|
711
|
+
content: {
|
|
712
|
+
description: action?.description,
|
|
713
|
+
inputSchema: action?.inputSchema
|
|
714
|
+
},
|
|
715
|
+
meta: {
|
|
716
|
+
...entry.meta,
|
|
717
|
+
description: action?.description,
|
|
718
|
+
inputSchema: action?.inputSchema
|
|
719
|
+
}
|
|
720
|
+
} };
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Read root metadata (/.meta).
|
|
724
|
+
* Returns mounted providers list, root actions, and childrenCount.
|
|
725
|
+
*/
|
|
726
|
+
async readRootMeta() {
|
|
727
|
+
const mounts = this.getMounts(null);
|
|
728
|
+
const { data: actions } = await this.listRootActions();
|
|
729
|
+
const mountedProviders = mounts.map((m) => {
|
|
730
|
+
const mod = m.module;
|
|
731
|
+
const operations = [];
|
|
732
|
+
if (mod.read) operations.push("read");
|
|
733
|
+
if (mod.list) operations.push("list");
|
|
734
|
+
if (mod.stat) operations.push("stat");
|
|
735
|
+
if (mod.write) operations.push("write");
|
|
736
|
+
if (mod.delete) operations.push("delete");
|
|
737
|
+
if (mod.search) operations.push("search");
|
|
738
|
+
if (mod.exec) operations.push("exec");
|
|
739
|
+
if (mod.explain) operations.push("explain");
|
|
740
|
+
if (mod.rename) operations.push("rename");
|
|
741
|
+
return {
|
|
742
|
+
name: mod.name,
|
|
743
|
+
path: m.path,
|
|
744
|
+
description: mod.description,
|
|
745
|
+
operations
|
|
746
|
+
};
|
|
747
|
+
});
|
|
748
|
+
const rootActions = actions.map((a) => ({
|
|
749
|
+
name: a.id,
|
|
750
|
+
description: a.actions?.[0]?.description || a.summary || ""
|
|
751
|
+
}));
|
|
752
|
+
return { data: {
|
|
753
|
+
id: ".meta",
|
|
754
|
+
path: "/.meta",
|
|
755
|
+
content: {
|
|
756
|
+
description: "AFS root metadata",
|
|
757
|
+
childrenCount: mounts.length,
|
|
758
|
+
mountedProviders,
|
|
759
|
+
rootActions
|
|
760
|
+
},
|
|
761
|
+
meta: { kind: "afs:meta" }
|
|
762
|
+
} };
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Read root actions directory (/.actions).
|
|
766
|
+
* Returns content with actions list.
|
|
767
|
+
*/
|
|
768
|
+
async readRootActions() {
|
|
769
|
+
const { data: actions } = await this.listRootActions();
|
|
770
|
+
return { data: {
|
|
771
|
+
id: ".actions",
|
|
772
|
+
path: "/.actions",
|
|
773
|
+
content: { actions: actions.map((a) => ({
|
|
774
|
+
name: a.id,
|
|
775
|
+
path: a.path,
|
|
776
|
+
description: a.actions?.[0]?.description || a.summary || ""
|
|
777
|
+
})) },
|
|
778
|
+
meta: {
|
|
779
|
+
kind: "afs:directory",
|
|
780
|
+
childrenCount: actions.length
|
|
781
|
+
}
|
|
782
|
+
} };
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Stat a root action path (/.actions or /.actions/{name}).
|
|
786
|
+
*/
|
|
787
|
+
async statRootAction(path) {
|
|
788
|
+
if (path === "/.actions") {
|
|
789
|
+
const { data: actions } = await this.listRootActions();
|
|
790
|
+
return { data: {
|
|
791
|
+
id: ".actions",
|
|
792
|
+
path: "/.actions",
|
|
793
|
+
meta: {
|
|
794
|
+
kind: "afs:directory",
|
|
795
|
+
childrenCount: actions.length
|
|
796
|
+
}
|
|
797
|
+
} };
|
|
798
|
+
}
|
|
799
|
+
const readResult = await this.readRootAction(path);
|
|
800
|
+
if (readResult.data) {
|
|
801
|
+
const { content: _content, ...statData } = readResult.data;
|
|
802
|
+
return { data: statData };
|
|
803
|
+
}
|
|
804
|
+
throw new AFSNotFoundError(path);
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Stat a root meta path (/.meta or /.meta/{subpath}).
|
|
808
|
+
*/
|
|
809
|
+
async statRootMeta(path) {
|
|
810
|
+
if (path === "/.meta") return { data: {
|
|
811
|
+
id: ".meta",
|
|
812
|
+
path: "/.meta",
|
|
813
|
+
meta: {
|
|
814
|
+
kind: "afs:meta",
|
|
815
|
+
childrenCount: 1
|
|
816
|
+
}
|
|
817
|
+
} };
|
|
818
|
+
if (path === "/.meta/.capabilities") return { data: {
|
|
819
|
+
id: ".capabilities",
|
|
820
|
+
path: "/.meta/.capabilities",
|
|
821
|
+
meta: { kind: "afs:capabilities" }
|
|
822
|
+
} };
|
|
823
|
+
throw new AFSNotFoundError(path);
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* List root-level actions (/.actions).
|
|
827
|
+
*/
|
|
828
|
+
async listRootActions() {
|
|
829
|
+
const actions = [];
|
|
830
|
+
if (this.loadProvider) actions.push({
|
|
831
|
+
id: "mount",
|
|
832
|
+
path: "/.actions/mount",
|
|
833
|
+
summary: "Mount a new provider",
|
|
834
|
+
meta: {
|
|
835
|
+
kind: "afs:executable",
|
|
836
|
+
kinds: ["afs:executable", "afs:node"]
|
|
837
|
+
},
|
|
838
|
+
actions: [{
|
|
839
|
+
name: "mount",
|
|
840
|
+
description: "Mount a provider from URI",
|
|
841
|
+
inputSchema: {
|
|
842
|
+
type: "object",
|
|
843
|
+
properties: {
|
|
844
|
+
uri: {
|
|
845
|
+
type: "string",
|
|
846
|
+
description: "Provider URI"
|
|
847
|
+
},
|
|
848
|
+
path: {
|
|
849
|
+
type: "string",
|
|
850
|
+
description: "Mount path"
|
|
851
|
+
},
|
|
852
|
+
accessMode: {
|
|
853
|
+
type: "string",
|
|
854
|
+
enum: ["readonly", "readwrite"],
|
|
855
|
+
description: "Access mode (default: readonly)"
|
|
856
|
+
},
|
|
857
|
+
auth: {
|
|
858
|
+
type: "string",
|
|
859
|
+
description: "Authentication token"
|
|
860
|
+
},
|
|
861
|
+
description: {
|
|
862
|
+
type: "string",
|
|
863
|
+
description: "Human-readable description"
|
|
864
|
+
},
|
|
865
|
+
scope: {
|
|
866
|
+
type: "string",
|
|
867
|
+
enum: [
|
|
868
|
+
"cwd",
|
|
869
|
+
"project",
|
|
870
|
+
"user"
|
|
871
|
+
],
|
|
872
|
+
description: "Where to persist the mount config. 'cwd' (default) = current directory, 'project' = project root (.git), 'user' = user home"
|
|
873
|
+
},
|
|
874
|
+
sensitiveArgs: {
|
|
875
|
+
type: "array",
|
|
876
|
+
items: { type: "string" },
|
|
877
|
+
description: "Field names that should be treated as sensitive credentials (stored in credentials.toml instead of config.toml)"
|
|
878
|
+
}
|
|
879
|
+
},
|
|
880
|
+
required: ["uri", "path"]
|
|
881
|
+
}
|
|
882
|
+
}]
|
|
883
|
+
});
|
|
884
|
+
actions.push({
|
|
885
|
+
id: "unmount",
|
|
886
|
+
path: "/.actions/unmount",
|
|
887
|
+
summary: "Unmount a provider",
|
|
888
|
+
meta: {
|
|
889
|
+
kind: "afs:executable",
|
|
890
|
+
kinds: ["afs:executable", "afs:node"]
|
|
891
|
+
},
|
|
892
|
+
actions: [{
|
|
893
|
+
name: "unmount",
|
|
894
|
+
description: "Unmount a provider at a given path",
|
|
895
|
+
inputSchema: {
|
|
896
|
+
type: "object",
|
|
897
|
+
properties: {
|
|
898
|
+
path: {
|
|
899
|
+
type: "string",
|
|
900
|
+
description: "Path to unmount"
|
|
901
|
+
},
|
|
902
|
+
scope: {
|
|
903
|
+
type: "string",
|
|
904
|
+
enum: [
|
|
905
|
+
"cwd",
|
|
906
|
+
"project",
|
|
907
|
+
"user"
|
|
908
|
+
],
|
|
909
|
+
description: "Config scope to remove mount from. If omitted, searches all scopes."
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
required: ["path"]
|
|
913
|
+
}
|
|
914
|
+
}]
|
|
915
|
+
});
|
|
916
|
+
return { data: actions };
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Handle root-level action execution (/.actions/*).
|
|
920
|
+
* Supports:
|
|
921
|
+
* - /.actions/mount: Load and mount a provider via loadProvider
|
|
922
|
+
* - /.actions/unmount: Unmount a provider at a given path
|
|
923
|
+
*/
|
|
924
|
+
async execRootAction(path, args) {
|
|
925
|
+
const actionName = path.slice(10);
|
|
926
|
+
if (actionName === "mount") return await this.execRootMountAction(args);
|
|
927
|
+
if (actionName === "unmount") return await this.execRootUnmountAction(args);
|
|
928
|
+
throw new AFSNotFoundError(path, `Root action not found: ${actionName}`);
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Execute root-level mount action.
|
|
932
|
+
* Validates input and delegates to loadProvider.
|
|
933
|
+
*/
|
|
934
|
+
async execRootMountAction(args) {
|
|
935
|
+
if (typeof args.uri !== "string" || args.uri === "") throw new AFSValidationError("Input validation failed: uri: must be a non-empty string");
|
|
936
|
+
if (typeof args.path !== "string" || args.path === "") throw new AFSValidationError("Input validation failed: path: must be a non-empty string");
|
|
937
|
+
if (!this.loadProvider) throw new Error("loadProvider not configured");
|
|
938
|
+
const { uri, path, sensitiveArgs, ...options } = args;
|
|
939
|
+
if (Array.isArray(sensitiveArgs) && sensitiveArgs.length > 0) options._sensitiveArgs = sensitiveArgs;
|
|
940
|
+
await this.loadProvider(uri, path, Object.keys(options).length > 0 ? options : void 0);
|
|
941
|
+
return {
|
|
942
|
+
success: true,
|
|
943
|
+
data: {
|
|
944
|
+
uri,
|
|
945
|
+
path
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Execute root-level unmount action.
|
|
951
|
+
* Validates input and delegates to unmount().
|
|
952
|
+
*/
|
|
953
|
+
async execRootUnmountAction(args) {
|
|
954
|
+
if (typeof args.path !== "string" || args.path === "") throw new AFSValidationError("Input validation failed: path: must be a non-empty string");
|
|
955
|
+
if (!this.unmount(args.path)) return {
|
|
956
|
+
success: false,
|
|
957
|
+
error: {
|
|
958
|
+
code: "NOT_FOUND",
|
|
959
|
+
message: `No provider mounted at ${args.path}`
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
if (this.unloadProvider) try {
|
|
963
|
+
const { path: _path, ...options } = args;
|
|
964
|
+
await this.unloadProvider(args.path, Object.keys(options).length > 0 ? options : void 0);
|
|
965
|
+
} catch (err) {
|
|
966
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
967
|
+
console.warn(`[unmount] config persistence failed: ${msg}`);
|
|
968
|
+
}
|
|
969
|
+
return {
|
|
970
|
+
success: true,
|
|
971
|
+
data: { path: args.path }
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Validate exec input args against inputSchema.
|
|
976
|
+
* Throws AFSValidationError if validation fails.
|
|
977
|
+
* Uses zod-from-json-schema for full JSON Schema validation.
|
|
978
|
+
*/
|
|
979
|
+
async validateExecInput(module, subpath, args) {
|
|
980
|
+
let inputSchema;
|
|
981
|
+
if (module.read) try {
|
|
982
|
+
inputSchema = (await module.read(subpath)).data?.meta?.inputSchema;
|
|
983
|
+
} catch {
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
if (!inputSchema) return;
|
|
987
|
+
try {
|
|
988
|
+
const { convertJsonSchemaToZod } = await import("zod-from-json-schema");
|
|
989
|
+
const testResult = convertJsonSchemaToZod(inputSchema).safeParse(args);
|
|
990
|
+
if (!testResult.success) {
|
|
991
|
+
if (testResult.error.issues.some((issue) => issue.message === "Invalid input: expected never, received object" || issue.message.startsWith("Invalid input: expected never"))) return;
|
|
992
|
+
throw new AFSValidationError(`Input validation failed: ${testResult.error.issues.map((issue) => {
|
|
993
|
+
const path = issue.path.join(".");
|
|
994
|
+
return path ? `${path}: ${issue.message}` : issue.message;
|
|
995
|
+
}).join("; ")}`);
|
|
996
|
+
}
|
|
997
|
+
} catch (error) {
|
|
998
|
+
if (error instanceof AFSValidationError) throw error;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Get stat information for a path
|
|
1003
|
+
*
|
|
1004
|
+
* Resolution order:
|
|
1005
|
+
* 1. Provider's stat() method (if implemented)
|
|
1006
|
+
* 2. Fallback to read() - extracts stat data from AFSEntry
|
|
1007
|
+
*
|
|
1008
|
+
* This allows providers to implement only read() while stat() still works.
|
|
1009
|
+
*/
|
|
1010
|
+
async stat(path, options) {
|
|
1011
|
+
const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
|
|
1012
|
+
if (namespace === null) {
|
|
1013
|
+
if (normalizedPath === "/.actions" || normalizedPath.startsWith("/.actions/")) return this.statRootAction(normalizedPath);
|
|
1014
|
+
if (normalizedPath === "/.meta" || normalizedPath.startsWith("/.meta/")) return this.statRootMeta(normalizedPath);
|
|
1015
|
+
}
|
|
1016
|
+
const module = this.findModulesInNamespace(normalizedPath, namespace)[0];
|
|
1017
|
+
if (!module) {
|
|
1018
|
+
const virtualDir = this.resolveVirtualDirectory(normalizedPath, namespace);
|
|
1019
|
+
if (virtualDir?.data) return { data: virtualDir.data };
|
|
1020
|
+
try {
|
|
1021
|
+
const readResult = await this.read(path);
|
|
1022
|
+
if (readResult.data) {
|
|
1023
|
+
const { content: _content, ...statData } = readResult.data;
|
|
1024
|
+
return { data: statData };
|
|
1025
|
+
}
|
|
1026
|
+
} catch {}
|
|
1027
|
+
throw new AFSNotFoundError(path);
|
|
1028
|
+
}
|
|
1029
|
+
if (module.remainedModulePath !== "/") {
|
|
1030
|
+
const virtualDir = this.resolveVirtualDirectory(normalizedPath, namespace);
|
|
1031
|
+
if (virtualDir?.data) return { data: virtualDir.data };
|
|
1032
|
+
}
|
|
1033
|
+
if (module.module.stat) try {
|
|
1034
|
+
const result = await module.module.stat(module.subpath, options);
|
|
1035
|
+
if (result.data) {
|
|
1036
|
+
const enrichedData = await this.enrichData(result.data, module.module, module.subpath);
|
|
1037
|
+
return {
|
|
1038
|
+
...result,
|
|
1039
|
+
data: enrichedData
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
throw new AFSNotFoundError(path);
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
if (error instanceof AFSNotFoundError) throw error;
|
|
1045
|
+
}
|
|
1046
|
+
if (module.module.read) {
|
|
1047
|
+
const readResult = await module.module.read(module.subpath, options);
|
|
1048
|
+
if (readResult.data) {
|
|
1049
|
+
const { content: _content, ...statData } = readResult.data;
|
|
1050
|
+
return { data: await this.enrichData(statData, module.module, module.subpath) };
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
throw new AFSNotFoundError(path);
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Get human-readable explanation for a path
|
|
1057
|
+
*
|
|
1058
|
+
* Resolution order:
|
|
1059
|
+
* 1. Provider's explain() method (if implemented)
|
|
1060
|
+
* 2. Fallback to stat() - builds explanation from metadata
|
|
1061
|
+
*
|
|
1062
|
+
* This allows providers to skip implementing explain() while it still works.
|
|
1063
|
+
*/
|
|
1064
|
+
async explain(path, options) {
|
|
1065
|
+
const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
|
|
1066
|
+
if (namespace === null) {
|
|
1067
|
+
if (normalizedPath === "/") return this.explainRoot();
|
|
1068
|
+
if (normalizedPath === "/.actions" || normalizedPath.startsWith("/.actions/")) return this.explainRootAction(normalizedPath);
|
|
1069
|
+
if (normalizedPath === "/.meta" || normalizedPath.startsWith("/.meta/")) return this.explainRootMeta(normalizedPath);
|
|
1070
|
+
}
|
|
1071
|
+
const module = this.findModulesInNamespace(normalizedPath, namespace)[0];
|
|
1072
|
+
if (!module) {
|
|
1073
|
+
const virtualDir = this.resolveVirtualDirectory(normalizedPath, namespace);
|
|
1074
|
+
if (virtualDir?.data) return this.buildVirtualDirExplain(normalizedPath, virtualDir.data);
|
|
1075
|
+
try {
|
|
1076
|
+
const statResult$1 = await this.stat(path);
|
|
1077
|
+
if (statResult$1.data) return this.buildExplainFromStat(normalizedPath, statResult$1.data);
|
|
1078
|
+
} catch {}
|
|
1079
|
+
throw new AFSNotFoundError(path);
|
|
1080
|
+
}
|
|
1081
|
+
if (module.remainedModulePath !== "/") {
|
|
1082
|
+
const virtualDir = this.resolveVirtualDirectory(normalizedPath, namespace);
|
|
1083
|
+
if (virtualDir?.data) return this.buildVirtualDirExplain(normalizedPath, virtualDir.data);
|
|
1084
|
+
}
|
|
1085
|
+
if (module.module.explain) try {
|
|
1086
|
+
return await module.module.explain(module.subpath, options);
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
if (error instanceof AFSNotFoundError) throw error;
|
|
1089
|
+
}
|
|
1090
|
+
const statResult = await this.stat(path, options);
|
|
1091
|
+
if (statResult.data) return this.buildExplainFromStat(normalizedPath, statResult.data);
|
|
1092
|
+
throw new Error(`No explain or stat handler for path: ${normalizedPath} in namespace '${namespace ?? "default"}'`);
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Format bytes to human-readable string
|
|
1096
|
+
*/
|
|
1097
|
+
buildVirtualDirExplain(path, data) {
|
|
1098
|
+
const childrenCount = data.meta?.childrenCount;
|
|
1099
|
+
const lines = [];
|
|
1100
|
+
lines.push(`# ${path}`);
|
|
1101
|
+
lines.push("");
|
|
1102
|
+
lines.push("- **Type**: Virtual directory");
|
|
1103
|
+
if (childrenCount !== void 0) lines.push(`- **Children**: ${childrenCount} items`);
|
|
1104
|
+
return {
|
|
1105
|
+
format: "markdown",
|
|
1106
|
+
content: lines.join("\n")
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* Explain root path ('/').
|
|
1111
|
+
* Generates a complete navigation guide with 4 required sections.
|
|
1112
|
+
*/
|
|
1113
|
+
async explainRoot() {
|
|
1114
|
+
const lines = [];
|
|
1115
|
+
lines.push("# AFS Root");
|
|
1116
|
+
lines.push("");
|
|
1117
|
+
lines.push("## Mounted Providers");
|
|
1118
|
+
lines.push("");
|
|
1119
|
+
const mounts = this.getMounts(null);
|
|
1120
|
+
if (mounts.length === 0) {
|
|
1121
|
+
lines.push("No providers currently mounted. Use the `mount` action to add providers.");
|
|
1122
|
+
lines.push("");
|
|
1123
|
+
} else {
|
|
1124
|
+
for (const m of mounts) if (m.module.description) {
|
|
1125
|
+
const descLines = m.module.description.split("\n");
|
|
1126
|
+
lines.push(`- **${m.module.name}** (\`${m.path}\`) — ${descLines[0]}`);
|
|
1127
|
+
for (let i = 1; i < descLines.length; i++) lines.push(` ${descLines[i]}`);
|
|
1128
|
+
} else lines.push(`- **${m.module.name}** (\`${m.path}\`)`);
|
|
1129
|
+
lines.push("");
|
|
1130
|
+
}
|
|
1131
|
+
lines.push("## Standard Operations");
|
|
1132
|
+
lines.push("");
|
|
1133
|
+
lines.push("- **read** — Read file or node content");
|
|
1134
|
+
lines.push("- **list** — List children of a directory or container");
|
|
1135
|
+
lines.push("- **stat** — Get metadata without content");
|
|
1136
|
+
lines.push("- **explain** — Get human-readable documentation for a path");
|
|
1137
|
+
lines.push("- **search** — Search within a mounted provider");
|
|
1138
|
+
lines.push("- **write** — Create or update content");
|
|
1139
|
+
lines.push("- **delete** — Remove a file or node");
|
|
1140
|
+
lines.push("- **exec** — Execute an action at a path");
|
|
1141
|
+
lines.push("");
|
|
1142
|
+
lines.push("## Root Actions");
|
|
1143
|
+
lines.push("");
|
|
1144
|
+
const { data: actions } = await this.listRootActions();
|
|
1145
|
+
for (const action of actions) {
|
|
1146
|
+
const desc = action.actions?.[0]?.description || action.summary || "";
|
|
1147
|
+
lines.push(`- **${action.id}** — ${desc}`);
|
|
1148
|
+
}
|
|
1149
|
+
lines.push("");
|
|
1150
|
+
lines.push("Use `explain('/.actions/<name>')` for detailed usage.");
|
|
1151
|
+
lines.push("");
|
|
1152
|
+
lines.push("## Built-in Systems");
|
|
1153
|
+
lines.push("");
|
|
1154
|
+
lines.push("- **`.meta`** — Metadata for any node. Access via `read('/.meta')` at root, or append `/.meta` to any path.");
|
|
1155
|
+
lines.push("- **`.actions`** — Executable actions. Access via `read('/.actions')` to list, `exec('/.actions/<name>', args)` to run.");
|
|
1156
|
+
lines.push("");
|
|
1157
|
+
return {
|
|
1158
|
+
format: "markdown",
|
|
1159
|
+
content: lines.join("\n")
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Explain a root action path (/.actions or /.actions/{name}).
|
|
1164
|
+
*/
|
|
1165
|
+
async explainRootAction(path) {
|
|
1166
|
+
if (path === "/.actions") {
|
|
1167
|
+
const lines$1 = [];
|
|
1168
|
+
lines$1.push("# Root Actions");
|
|
1169
|
+
lines$1.push("");
|
|
1170
|
+
lines$1.push("Available actions at the AFS root level:");
|
|
1171
|
+
lines$1.push("");
|
|
1172
|
+
const { data: actions$1 } = await this.listRootActions();
|
|
1173
|
+
for (const action$1 of actions$1) {
|
|
1174
|
+
const desc = action$1.actions?.[0]?.description || action$1.summary || "";
|
|
1175
|
+
lines$1.push(`- **${action$1.id}** — ${desc}`);
|
|
1176
|
+
}
|
|
1177
|
+
lines$1.push("");
|
|
1178
|
+
lines$1.push("Use `explain('/.actions/<name>')` for detailed parameters and usage.");
|
|
1179
|
+
return {
|
|
1180
|
+
format: "markdown",
|
|
1181
|
+
content: lines$1.join("\n")
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
const actionName = path.slice(10);
|
|
1185
|
+
const { data: actions } = await this.listRootActions();
|
|
1186
|
+
const entry = actions.find((a) => a.id === actionName);
|
|
1187
|
+
if (!entry) throw new AFSNotFoundError(path, `Root action not found: ${actionName}`);
|
|
1188
|
+
const action = entry.actions?.[0];
|
|
1189
|
+
const lines = [];
|
|
1190
|
+
lines.push(`# ${actionName}`);
|
|
1191
|
+
lines.push("");
|
|
1192
|
+
if (action?.description) {
|
|
1193
|
+
lines.push(action.description);
|
|
1194
|
+
lines.push("");
|
|
1195
|
+
}
|
|
1196
|
+
const schema = action?.inputSchema;
|
|
1197
|
+
const properties = schema?.properties;
|
|
1198
|
+
const required = schema?.required || [];
|
|
1199
|
+
if (properties && Object.keys(properties).length > 0) {
|
|
1200
|
+
lines.push("## Parameters");
|
|
1201
|
+
lines.push("");
|
|
1202
|
+
lines.push("| Name | Type | Required | Description |");
|
|
1203
|
+
lines.push("|------|------|----------|-------------|");
|
|
1204
|
+
for (const [name, prop] of Object.entries(properties)) {
|
|
1205
|
+
const type = prop.type || "any";
|
|
1206
|
+
const isRequired = required.includes(name) ? "yes" : "no";
|
|
1207
|
+
const desc = prop.description || "";
|
|
1208
|
+
lines.push(`| ${name} | ${type} | ${isRequired} | ${desc} |`);
|
|
1209
|
+
}
|
|
1210
|
+
lines.push("");
|
|
1211
|
+
}
|
|
1212
|
+
lines.push("## Example");
|
|
1213
|
+
lines.push("");
|
|
1214
|
+
const exampleArgs = {};
|
|
1215
|
+
for (const name of required) exampleArgs[name] = `<${name}>`;
|
|
1216
|
+
lines.push(`\`\`\`\nexec('/.actions/${actionName}', ${JSON.stringify(exampleArgs, null, 2)})\n\`\`\``);
|
|
1217
|
+
return {
|
|
1218
|
+
format: "markdown",
|
|
1219
|
+
content: lines.join("\n")
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Explain a root meta path (/.meta or /.meta/{subpath}).
|
|
1224
|
+
*/
|
|
1225
|
+
async explainRootMeta(path) {
|
|
1226
|
+
if (path === "/.meta") {
|
|
1227
|
+
const lines = [];
|
|
1228
|
+
lines.push("# Root Metadata");
|
|
1229
|
+
lines.push("");
|
|
1230
|
+
lines.push("The `.meta` system provides metadata about any AFS node.");
|
|
1231
|
+
lines.push("");
|
|
1232
|
+
lines.push("## Available Sub-paths");
|
|
1233
|
+
lines.push("");
|
|
1234
|
+
lines.push("- **`.capabilities`** — Aggregated capabilities from all mounted providers");
|
|
1235
|
+
lines.push("");
|
|
1236
|
+
lines.push("Use `read('/.meta')` to get structured root metadata including mounted providers and available actions.");
|
|
1237
|
+
return {
|
|
1238
|
+
format: "markdown",
|
|
1239
|
+
content: lines.join("\n")
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
if (path === "/.meta/.capabilities") {
|
|
1243
|
+
const lines = [];
|
|
1244
|
+
lines.push("# Capabilities");
|
|
1245
|
+
lines.push("");
|
|
1246
|
+
lines.push("Aggregated capabilities manifest from all mounted providers.");
|
|
1247
|
+
lines.push("Describes the combined operations, tools, and action catalogs available across the system.");
|
|
1248
|
+
lines.push("");
|
|
1249
|
+
lines.push("Use `read('/.meta/.capabilities')` to get the full structured capabilities data.");
|
|
1250
|
+
return {
|
|
1251
|
+
format: "markdown",
|
|
1252
|
+
content: lines.join("\n")
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
throw new AFSNotFoundError(path);
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Build explain markdown from stat data.
|
|
1259
|
+
* Used by both the module-found fallback and the no-module safety net.
|
|
1260
|
+
*/
|
|
1261
|
+
buildExplainFromStat(path, data) {
|
|
1262
|
+
const lines = [];
|
|
1263
|
+
lines.push(`# ${path}`);
|
|
1264
|
+
lines.push("");
|
|
1265
|
+
const meta = data.meta || {};
|
|
1266
|
+
if (meta.size !== void 0) lines.push(`- **Size**: ${this.formatBytes(meta.size)}`);
|
|
1267
|
+
if (meta.childrenCount !== void 0) lines.push(`- **Children**: ${meta.childrenCount} items`);
|
|
1268
|
+
if (data.updatedAt) lines.push(`- **Modified**: ${data.updatedAt.toISOString()}`);
|
|
1269
|
+
if (meta.description) {
|
|
1270
|
+
lines.push("");
|
|
1271
|
+
lines.push("## Description");
|
|
1272
|
+
lines.push(String(meta.description));
|
|
1273
|
+
}
|
|
1274
|
+
if (meta.provider) lines.push(`- **Provider**: ${meta.provider}`);
|
|
1275
|
+
if (meta.kind) lines.push(`- **Kind**: ${meta.kind}`);
|
|
1276
|
+
if (meta.kinds && Array.isArray(meta.kinds)) lines.push(`- **Kinds**: ${meta.kinds.join(", ")}`);
|
|
1277
|
+
if (data.actions && data.actions.length > 0) {
|
|
1278
|
+
lines.push("");
|
|
1279
|
+
lines.push("## Actions");
|
|
1280
|
+
for (const action of data.actions) lines.push(`- **${action.name}**${action.description ? `: ${action.description}` : ""}`);
|
|
1281
|
+
}
|
|
1282
|
+
return {
|
|
1283
|
+
format: "markdown",
|
|
1284
|
+
content: lines.join("\n")
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
formatBytes(bytes) {
|
|
1288
|
+
if (bytes === 0) return "0 B";
|
|
1289
|
+
const k = 1024;
|
|
1290
|
+
const sizes = [
|
|
1291
|
+
"B",
|
|
1292
|
+
"KB",
|
|
1293
|
+
"MB",
|
|
1294
|
+
"GB",
|
|
1295
|
+
"TB"
|
|
1296
|
+
];
|
|
1297
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1298
|
+
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
|
|
1299
|
+
}
|
|
1300
|
+
physicalPath;
|
|
1301
|
+
async initializePhysicalPath() {
|
|
1302
|
+
this.physicalPath ??= (async () => {
|
|
1303
|
+
const path = await import("node:path");
|
|
1304
|
+
const os = await import("node:os");
|
|
1305
|
+
const fs = await import("node:fs/promises");
|
|
1306
|
+
const rootDir = path.join(os.tmpdir(), v7());
|
|
1307
|
+
await fs.mkdir(rootDir, { recursive: true });
|
|
1308
|
+
for (const entry of this.mounts.values()) {
|
|
1309
|
+
const namespacePart = entry.namespace ?? "_default";
|
|
1310
|
+
const physicalModulePath = path.join(rootDir, namespacePart, entry.path);
|
|
1311
|
+
await fs.mkdir(path.dirname(physicalModulePath), { recursive: true });
|
|
1312
|
+
await entry.module.symlinkToPhysical?.(physicalModulePath);
|
|
1313
|
+
}
|
|
1314
|
+
return rootDir;
|
|
1315
|
+
})();
|
|
1316
|
+
return this.physicalPath;
|
|
1317
|
+
}
|
|
1318
|
+
async cleanupPhysicalPath() {
|
|
1319
|
+
if (this.physicalPath) {
|
|
1320
|
+
await (await import("node:fs/promises")).rm(await this.physicalPath, {
|
|
1321
|
+
recursive: true,
|
|
1322
|
+
force: true
|
|
1323
|
+
});
|
|
1324
|
+
this.physicalPath = void 0;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1329
|
+
//#endregion
|
|
1330
|
+
export { AFS };
|
|
1331
|
+
//# sourceMappingURL=afs.mjs.map
|