@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.
Files changed (164) hide show
  1. package/LICENSE.md +17 -84
  2. package/README.md +4 -13
  3. package/dist/_virtual/rolldown_runtime.mjs +7 -0
  4. package/dist/afs.cjs +1330 -0
  5. package/dist/afs.d.cts +275 -0
  6. package/dist/afs.d.cts.map +1 -0
  7. package/dist/afs.d.mts +275 -0
  8. package/dist/afs.d.mts.map +1 -0
  9. package/dist/afs.mjs +1331 -0
  10. package/dist/afs.mjs.map +1 -0
  11. package/dist/capabilities/index.d.mts +2 -0
  12. package/dist/capabilities/types.d.cts +100 -0
  13. package/dist/capabilities/types.d.cts.map +1 -0
  14. package/dist/capabilities/types.d.mts +100 -0
  15. package/dist/capabilities/types.d.mts.map +1 -0
  16. package/dist/capabilities/world-mapping.cjs +20 -0
  17. package/dist/capabilities/world-mapping.d.cts +139 -0
  18. package/dist/capabilities/world-mapping.d.cts.map +1 -0
  19. package/dist/capabilities/world-mapping.d.mts +139 -0
  20. package/dist/capabilities/world-mapping.d.mts.map +1 -0
  21. package/dist/capabilities/world-mapping.mjs +20 -0
  22. package/dist/capabilities/world-mapping.mjs.map +1 -0
  23. package/dist/error.cjs +63 -0
  24. package/dist/error.d.cts +39 -0
  25. package/dist/error.d.cts.map +1 -0
  26. package/dist/error.d.mts +39 -0
  27. package/dist/error.d.mts.map +1 -0
  28. package/dist/error.mjs +59 -0
  29. package/dist/error.mjs.map +1 -0
  30. package/dist/index.cjs +72 -345
  31. package/dist/index.d.cts +18 -300
  32. package/dist/index.d.mts +20 -300
  33. package/dist/index.mjs +16 -342
  34. package/dist/loader/index.cjs +110 -0
  35. package/dist/loader/index.d.cts +48 -0
  36. package/dist/loader/index.d.cts.map +1 -0
  37. package/dist/loader/index.d.mts +48 -0
  38. package/dist/loader/index.d.mts.map +1 -0
  39. package/dist/loader/index.mjs +110 -0
  40. package/dist/loader/index.mjs.map +1 -0
  41. package/dist/meta/index.cjs +4 -0
  42. package/dist/meta/index.mjs +6 -0
  43. package/dist/meta/kind.cjs +161 -0
  44. package/dist/meta/kind.d.cts +134 -0
  45. package/dist/meta/kind.d.cts.map +1 -0
  46. package/dist/meta/kind.d.mts +134 -0
  47. package/dist/meta/kind.d.mts.map +1 -0
  48. package/dist/meta/kind.mjs +157 -0
  49. package/dist/meta/kind.mjs.map +1 -0
  50. package/dist/meta/path.cjs +116 -0
  51. package/dist/meta/path.d.cts +43 -0
  52. package/dist/meta/path.d.cts.map +1 -0
  53. package/dist/meta/path.d.mts +43 -0
  54. package/dist/meta/path.d.mts.map +1 -0
  55. package/dist/meta/path.mjs +112 -0
  56. package/dist/meta/path.mjs.map +1 -0
  57. package/dist/meta/type.d.cts +96 -0
  58. package/dist/meta/type.d.cts.map +1 -0
  59. package/dist/meta/type.d.mts +96 -0
  60. package/dist/meta/type.d.mts.map +1 -0
  61. package/dist/meta/validation.cjs +77 -0
  62. package/dist/meta/validation.d.cts +19 -0
  63. package/dist/meta/validation.d.cts.map +1 -0
  64. package/dist/meta/validation.d.mts +19 -0
  65. package/dist/meta/validation.d.mts.map +1 -0
  66. package/dist/meta/validation.mjs +77 -0
  67. package/dist/meta/validation.mjs.map +1 -0
  68. package/dist/meta/well-known-kinds.cjs +228 -0
  69. package/dist/meta/well-known-kinds.d.cts +52 -0
  70. package/dist/meta/well-known-kinds.d.cts.map +1 -0
  71. package/dist/meta/well-known-kinds.d.mts +52 -0
  72. package/dist/meta/well-known-kinds.d.mts.map +1 -0
  73. package/dist/meta/well-known-kinds.mjs +219 -0
  74. package/dist/meta/well-known-kinds.mjs.map +1 -0
  75. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.cts +141 -0
  76. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.cts.map +1 -0
  77. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.mts +141 -0
  78. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.mts.map +1 -0
  79. package/dist/path.cjs +255 -0
  80. package/dist/path.d.cts +93 -0
  81. package/dist/path.d.cts.map +1 -0
  82. package/dist/path.d.mts +93 -0
  83. package/dist/path.d.mts.map +1 -0
  84. package/dist/path.mjs +249 -0
  85. package/dist/path.mjs.map +1 -0
  86. package/dist/provider/base.cjs +425 -0
  87. package/dist/provider/base.d.cts +175 -0
  88. package/dist/provider/base.d.cts.map +1 -0
  89. package/dist/provider/base.d.mts +175 -0
  90. package/dist/provider/base.d.mts.map +1 -0
  91. package/dist/provider/base.mjs +426 -0
  92. package/dist/provider/base.mjs.map +1 -0
  93. package/dist/provider/decorators.cjs +268 -0
  94. package/dist/provider/decorators.d.cts +244 -0
  95. package/dist/provider/decorators.d.cts.map +1 -0
  96. package/dist/provider/decorators.d.mts +244 -0
  97. package/dist/provider/decorators.d.mts.map +1 -0
  98. package/dist/provider/decorators.mjs +256 -0
  99. package/dist/provider/decorators.mjs.map +1 -0
  100. package/dist/provider/index.cjs +19 -0
  101. package/dist/provider/index.d.cts +5 -0
  102. package/dist/provider/index.d.mts +5 -0
  103. package/dist/provider/index.mjs +5 -0
  104. package/dist/provider/router.cjs +185 -0
  105. package/dist/provider/router.d.cts +50 -0
  106. package/dist/provider/router.d.cts.map +1 -0
  107. package/dist/provider/router.d.mts +50 -0
  108. package/dist/provider/router.d.mts.map +1 -0
  109. package/dist/provider/router.mjs +185 -0
  110. package/dist/provider/router.mjs.map +1 -0
  111. package/dist/provider/types.d.cts +113 -0
  112. package/dist/provider/types.d.cts.map +1 -0
  113. package/dist/provider/types.d.mts +113 -0
  114. package/dist/provider/types.d.mts.map +1 -0
  115. package/dist/registry.cjs +358 -0
  116. package/dist/registry.d.cts +96 -0
  117. package/dist/registry.d.cts.map +1 -0
  118. package/dist/registry.d.mts +96 -0
  119. package/dist/registry.d.mts.map +1 -0
  120. package/dist/registry.mjs +360 -0
  121. package/dist/registry.mjs.map +1 -0
  122. package/dist/type.cjs +34 -0
  123. package/dist/type.d.cts +420 -0
  124. package/dist/type.d.cts.map +1 -0
  125. package/dist/type.d.mts +420 -0
  126. package/dist/type.d.mts.map +1 -0
  127. package/dist/type.mjs +33 -0
  128. package/dist/type.mjs.map +1 -0
  129. package/dist/utils/camelize.d.cts.map +1 -1
  130. package/dist/utils/camelize.d.mts.map +1 -1
  131. package/dist/utils/schema.cjs +129 -0
  132. package/dist/utils/schema.d.cts +65 -0
  133. package/dist/utils/schema.d.cts.map +1 -0
  134. package/dist/utils/schema.d.mts +65 -0
  135. package/dist/utils/schema.d.mts.map +1 -0
  136. package/dist/utils/schema.mjs +124 -0
  137. package/dist/utils/schema.mjs.map +1 -0
  138. package/dist/utils/type-utils.d.cts.map +1 -1
  139. package/dist/utils/type-utils.d.mts.map +1 -1
  140. package/dist/utils/uri-template.cjs +123 -0
  141. package/dist/utils/uri-template.d.cts +48 -0
  142. package/dist/utils/uri-template.d.cts.map +1 -0
  143. package/dist/utils/uri-template.d.mts +48 -0
  144. package/dist/utils/uri-template.d.mts.map +1 -0
  145. package/dist/utils/uri-template.mjs +120 -0
  146. package/dist/utils/uri-template.mjs.map +1 -0
  147. package/dist/utils/uri.cjs +49 -0
  148. package/dist/utils/uri.d.cts +34 -0
  149. package/dist/utils/uri.d.cts.map +1 -0
  150. package/dist/utils/uri.d.mts +34 -0
  151. package/dist/utils/uri.d.mts.map +1 -0
  152. package/dist/utils/uri.mjs +49 -0
  153. package/dist/utils/uri.mjs.map +1 -0
  154. package/dist/utils/zod.cjs +6 -8
  155. package/dist/utils/zod.d.cts +2 -2
  156. package/dist/utils/zod.d.cts.map +1 -1
  157. package/dist/utils/zod.d.mts +2 -2
  158. package/dist/utils/zod.d.mts.map +1 -1
  159. package/dist/utils/zod.mjs +6 -8
  160. package/dist/utils/zod.mjs.map +1 -1
  161. package/package.json +27 -4
  162. package/dist/index.d.cts.map +0 -1
  163. package/dist/index.d.mts.map +0 -1
  164. 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