@aigne/afs 1.11.0-beta.5 → 1.11.0-beta.7

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 (120) hide show
  1. package/dist/afs.cjs +463 -110
  2. package/dist/afs.d.cts +140 -53
  3. package/dist/afs.d.cts.map +1 -1
  4. package/dist/afs.d.mts +140 -53
  5. package/dist/afs.d.mts.map +1 -1
  6. package/dist/afs.mjs +464 -111
  7. package/dist/afs.mjs.map +1 -1
  8. package/dist/capabilities/index.d.mts +2 -0
  9. package/dist/capabilities/types.d.cts +100 -0
  10. package/dist/capabilities/types.d.cts.map +1 -0
  11. package/dist/capabilities/types.d.mts +100 -0
  12. package/dist/capabilities/types.d.mts.map +1 -0
  13. package/dist/capabilities/world-mapping.cjs +20 -0
  14. package/dist/capabilities/world-mapping.d.cts +139 -0
  15. package/dist/capabilities/world-mapping.d.cts.map +1 -0
  16. package/dist/capabilities/world-mapping.d.mts +139 -0
  17. package/dist/capabilities/world-mapping.d.mts.map +1 -0
  18. package/dist/capabilities/world-mapping.mjs +20 -0
  19. package/dist/capabilities/world-mapping.mjs.map +1 -0
  20. package/dist/error.cjs +38 -1
  21. package/dist/error.d.cts +23 -1
  22. package/dist/error.d.cts.map +1 -1
  23. package/dist/error.d.mts +23 -1
  24. package/dist/error.d.mts.map +1 -1
  25. package/dist/error.mjs +35 -1
  26. package/dist/error.mjs.map +1 -1
  27. package/dist/index.cjs +55 -1
  28. package/dist/index.d.cts +15 -3
  29. package/dist/index.d.mts +17 -3
  30. package/dist/index.mjs +13 -3
  31. package/dist/loader/index.cjs +67 -0
  32. package/dist/loader/index.d.cts +48 -0
  33. package/dist/loader/index.d.cts.map +1 -0
  34. package/dist/loader/index.d.mts +48 -0
  35. package/dist/loader/index.d.mts.map +1 -0
  36. package/dist/loader/index.mjs +66 -0
  37. package/dist/loader/index.mjs.map +1 -0
  38. package/dist/meta/index.cjs +4 -0
  39. package/dist/meta/index.mjs +6 -0
  40. package/dist/meta/kind.cjs +161 -0
  41. package/dist/meta/kind.d.cts +134 -0
  42. package/dist/meta/kind.d.cts.map +1 -0
  43. package/dist/meta/kind.d.mts +134 -0
  44. package/dist/meta/kind.d.mts.map +1 -0
  45. package/dist/meta/kind.mjs +157 -0
  46. package/dist/meta/kind.mjs.map +1 -0
  47. package/dist/meta/path.cjs +116 -0
  48. package/dist/meta/path.d.cts +43 -0
  49. package/dist/meta/path.d.cts.map +1 -0
  50. package/dist/meta/path.d.mts +43 -0
  51. package/dist/meta/path.d.mts.map +1 -0
  52. package/dist/meta/path.mjs +112 -0
  53. package/dist/meta/path.mjs.map +1 -0
  54. package/dist/meta/type.d.cts +96 -0
  55. package/dist/meta/type.d.cts.map +1 -0
  56. package/dist/meta/type.d.mts +96 -0
  57. package/dist/meta/type.d.mts.map +1 -0
  58. package/dist/meta/validation.cjs +77 -0
  59. package/dist/meta/validation.d.cts +19 -0
  60. package/dist/meta/validation.d.cts.map +1 -0
  61. package/dist/meta/validation.d.mts +19 -0
  62. package/dist/meta/validation.d.mts.map +1 -0
  63. package/dist/meta/validation.mjs +77 -0
  64. package/dist/meta/validation.mjs.map +1 -0
  65. package/dist/meta/well-known-kinds.cjs +228 -0
  66. package/dist/meta/well-known-kinds.d.cts +52 -0
  67. package/dist/meta/well-known-kinds.d.cts.map +1 -0
  68. package/dist/meta/well-known-kinds.d.mts +52 -0
  69. package/dist/meta/well-known-kinds.d.mts.map +1 -0
  70. package/dist/meta/well-known-kinds.mjs +219 -0
  71. package/dist/meta/well-known-kinds.mjs.map +1 -0
  72. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.cts +141 -0
  73. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.cts.map +1 -0
  74. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.mts +141 -0
  75. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.mts.map +1 -0
  76. package/dist/path.d.cts.map +1 -1
  77. package/dist/path.d.mts.map +1 -1
  78. package/dist/provider/base.cjs +425 -0
  79. package/dist/provider/base.d.cts +175 -0
  80. package/dist/provider/base.d.cts.map +1 -0
  81. package/dist/provider/base.d.mts +175 -0
  82. package/dist/provider/base.d.mts.map +1 -0
  83. package/dist/provider/base.mjs +426 -0
  84. package/dist/provider/base.mjs.map +1 -0
  85. package/dist/provider/decorators.cjs +268 -0
  86. package/dist/provider/decorators.d.cts +244 -0
  87. package/dist/provider/decorators.d.cts.map +1 -0
  88. package/dist/provider/decorators.d.mts +244 -0
  89. package/dist/provider/decorators.d.mts.map +1 -0
  90. package/dist/provider/decorators.mjs +256 -0
  91. package/dist/provider/decorators.mjs.map +1 -0
  92. package/dist/provider/index.cjs +19 -0
  93. package/dist/provider/index.d.cts +5 -0
  94. package/dist/provider/index.d.mts +5 -0
  95. package/dist/provider/index.mjs +5 -0
  96. package/dist/provider/router.cjs +185 -0
  97. package/dist/provider/router.d.cts +50 -0
  98. package/dist/provider/router.d.cts.map +1 -0
  99. package/dist/provider/router.d.mts +50 -0
  100. package/dist/provider/router.d.mts.map +1 -0
  101. package/dist/provider/router.mjs +185 -0
  102. package/dist/provider/router.mjs.map +1 -0
  103. package/dist/provider/types.d.cts +113 -0
  104. package/dist/provider/types.d.cts.map +1 -0
  105. package/dist/provider/types.d.mts +113 -0
  106. package/dist/provider/types.d.mts.map +1 -0
  107. package/dist/type.cjs +12 -3
  108. package/dist/type.d.cts +183 -100
  109. package/dist/type.d.cts.map +1 -1
  110. package/dist/type.d.mts +183 -100
  111. package/dist/type.d.mts.map +1 -1
  112. package/dist/type.mjs +12 -4
  113. package/dist/type.mjs.map +1 -1
  114. package/dist/utils/camelize.d.cts.map +1 -1
  115. package/dist/utils/camelize.d.mts.map +1 -1
  116. package/dist/utils/type-utils.d.cts.map +1 -1
  117. package/dist/utils/type-utils.d.mts.map +1 -1
  118. package/dist/utils/zod.d.cts.map +1 -1
  119. package/dist/utils/zod.d.mts.map +1 -1
  120. package/package.json +12 -1
package/dist/afs.cjs CHANGED
@@ -1,15 +1,48 @@
1
1
  const require_error = require('./error.cjs');
2
2
  const require_path = require('./path.cjs');
3
- const require_type = require('./type.cjs');
4
3
  let _aigne_uuid = require("@aigne/uuid");
5
- let strict_event_emitter = require("strict-event-emitter");
6
4
  let ufo = require("ufo");
7
- let zod = require("zod");
8
5
 
9
6
  //#region src/afs.ts
10
7
  const DEFAULT_MAX_DEPTH = 1;
11
8
  const MODULES_ROOT_DIR = "/modules";
12
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
+ /**
13
46
  * Characters forbidden in namespace names (security-sensitive)
14
47
  */
15
48
  const NAMESPACE_FORBIDDEN_CHARS = [
@@ -38,10 +71,19 @@ function validateNamespaceName(namespace) {
38
71
  if (namespace.trim() === "") throw new Error("Namespace cannot be empty or whitespace-only");
39
72
  for (const char of NAMESPACE_FORBIDDEN_CHARS) if (namespace.includes(char)) throw new Error(`Namespace contains forbidden character: '${char}'`);
40
73
  }
41
- var AFS = class extends strict_event_emitter.Emitter {
74
+ var AFS = class {
42
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 }) → provider instance
80
+ * 2. afs.mount(provider, path)
81
+ * 3. Persist to config.toml
82
+ *
83
+ * Used by root-level /.actions/mount action.
84
+ */
85
+ loadProvider;
43
86
  constructor(options = {}) {
44
- super();
45
87
  this.options = options;
46
88
  for (const module of options?.modules ?? []) this.mount(module, (0, ufo.joinURL)(MODULES_ROOT_DIR, module.name));
47
89
  }
@@ -69,17 +111,62 @@ var AFS = class extends strict_event_emitter.Emitter {
69
111
  * Check if write operations are allowed for the given module.
70
112
  * Throws AFSReadonlyError if not allowed.
71
113
  */
114
+ /** Fire-and-forget change notification */
115
+ notifyChange(record) {
116
+ try {
117
+ this.options.onChange?.(record);
118
+ } catch {}
119
+ }
72
120
  checkWritePermission(module, operation, path) {
73
121
  if (module.accessMode !== "readwrite") throw new require_error.AFSReadonlyError(`Module '${module.name}' is readonly, cannot perform ${operation} to ${path}`);
74
122
  }
75
123
  /**
124
+ * Check provider availability on mount.
125
+ * Validates that the provider can successfully respond to stat/read
126
+ * and list (if childrenCount indicates children exist).
127
+ *
128
+ * @throws AFSMountError if validation fails
129
+ */
130
+ async checkProviderOnMount(provider) {
131
+ const timeout = getTimeout(provider);
132
+ const name = provider.name;
133
+ let rootData;
134
+ if (provider.stat) try {
135
+ rootData = (await withTimeout(provider.stat("/"), timeout)).data;
136
+ } catch (err) {
137
+ throw new require_error.AFSMountError(name, "stat", getMountErrorMessage(err, timeout));
138
+ }
139
+ else if (provider.read) try {
140
+ const result = await withTimeout(provider.read("/"), timeout);
141
+ if (result.data) rootData = {
142
+ path: result.data.path,
143
+ meta: result.data.meta ?? void 0
144
+ };
145
+ } catch (err) {
146
+ throw new require_error.AFSMountError(name, "read", getMountErrorMessage(err, timeout));
147
+ }
148
+ else throw new require_error.AFSMountError(name, "read", "Provider has no stat or read method");
149
+ if (!rootData) throw new require_error.AFSMountError(name, provider.stat ? "stat" : "read", "Root path returned undefined data");
150
+ const childrenCount = rootData.meta?.childrenCount;
151
+ if (childrenCount === -1 || typeof childrenCount === "number" && childrenCount > 0) {
152
+ if (!provider.list) throw new require_error.AFSMountError(name, "list", "Provider has childrenCount but no list method");
153
+ try {
154
+ const listResult = await withTimeout(provider.list("/"), timeout);
155
+ if (!listResult.data || listResult.data.length === 0) throw new require_error.AFSMountError(name, "list", "childrenCount indicates children but list returned empty");
156
+ } catch (err) {
157
+ if (err instanceof require_error.AFSMountError) throw err;
158
+ throw new require_error.AFSMountError(name, "list", getMountErrorMessage(err, timeout));
159
+ }
160
+ }
161
+ }
162
+ /**
76
163
  * Mount a module at a path in a namespace
77
164
  *
78
165
  * @param module - The module to mount
79
166
  * @param path - The path to mount at (optional, defaults to /modules/{module.name} for backward compatibility)
80
167
  * @param options - Mount options (namespace, replace)
81
168
  */
82
- mount(module, path, options) {
169
+ async mount(module, path, options) {
83
170
  require_path.validateModuleName(module.name);
84
171
  const normalizedPath = require_path.validatePath(path ?? (0, ufo.joinURL)(MODULES_ROOT_DIR, module.name));
85
172
  const namespace = options?.namespace === void 0 ? null : options.namespace;
@@ -97,12 +184,20 @@ var AFS = class extends strict_event_emitter.Emitter {
97
184
  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"}'`);
98
185
  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"}'`);
99
186
  }
187
+ await this.checkProviderOnMount(module);
100
188
  this.mounts.set(key, {
101
189
  namespace,
102
190
  path: normalizedPath,
103
191
  module
104
192
  });
105
193
  module.onMount?.(this);
194
+ this.notifyChange({
195
+ kind: "mount",
196
+ path: normalizedPath,
197
+ moduleName: module.name,
198
+ namespace,
199
+ timestamp: Date.now()
200
+ });
106
201
  return this;
107
202
  }
108
203
  /**
@@ -138,8 +233,16 @@ var AFS = class extends strict_event_emitter.Emitter {
138
233
  const normalizedPath = require_path.validatePath(path);
139
234
  const ns = namespace === void 0 ? null : namespace;
140
235
  const key = this.makeKey(ns, normalizedPath);
141
- if (this.mounts.has(key)) {
236
+ const entry = this.mounts.get(key);
237
+ if (entry) {
142
238
  this.mounts.delete(key);
239
+ this.notifyChange({
240
+ kind: "unmount",
241
+ path: normalizedPath,
242
+ moduleName: entry.module.name,
243
+ namespace: entry.namespace,
244
+ timestamp: Date.now()
245
+ });
143
246
  return true;
144
247
  }
145
248
  return false;
@@ -197,11 +300,13 @@ var AFS = class extends strict_event_emitter.Emitter {
197
300
  let newMaxDepth;
198
301
  let subpath;
199
302
  let remainedModulePath;
200
- if (!options?.exactMatch && modulePath.startsWith(path)) {
303
+ const moduleUnderPath = !options?.exactMatch && modulePath.startsWith(path) && (modulePath === path || path === "/" || modulePath[path.length] === "/");
304
+ const pathUnderModule = path.startsWith(modulePath) && (path === modulePath || modulePath === "/" || path[modulePath.length] === "/");
305
+ if (moduleUnderPath) {
201
306
  newMaxDepth = Math.max(0, maxDepth - (modulePathSegments.length - pathSegments.length));
202
307
  subpath = "/";
203
308
  remainedModulePath = (0, ufo.joinURL)("/", ...modulePathSegments.slice(pathSegments.length).slice(0, maxDepth));
204
- } else if (path.startsWith(modulePath)) {
309
+ } else if (pathUnderModule) {
205
310
  newMaxDepth = maxDepth;
206
311
  subpath = (0, ufo.joinURL)("/", ...pathSegments.slice(modulePathSegments.length));
207
312
  remainedModulePath = "/";
@@ -219,27 +324,22 @@ var AFS = class extends strict_event_emitter.Emitter {
219
324
  }
220
325
  async list(path, options = {}) {
221
326
  const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
222
- let preset;
223
- if (options.preset) {
224
- preset = this.options?.context?.list?.presets?.[options.preset];
225
- if (!preset) throw new Error(`Preset not found: ${options.preset}`);
226
- }
227
- return await this.processWithPreset(normalizedPath, void 0, preset, {
228
- ...options,
229
- defaultSelect: () => this._list(normalizedPath, namespace, options)
230
- });
327
+ return await this._list(normalizedPath, namespace, options);
231
328
  }
232
329
  async _list(path, namespace, options = {}) {
330
+ if (options?.maxDepth === 0) return { data: [] };
233
331
  const results = [];
234
332
  const hasModulesMounts = namespace === null && [...this.mounts.values()].some((m) => m.namespace === null && m.path.startsWith(`${MODULES_ROOT_DIR}/`));
235
333
  if (path === "/" && hasModulesMounts) {
236
334
  const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
335
+ let moduleCount = 0;
336
+ for (const entry of this.mounts.values()) if (entry.namespace === namespace) moduleCount++;
237
337
  results.push({
238
338
  id: "modules",
239
339
  path: MODULES_ROOT_DIR,
240
340
  summary: "All mounted modules",
241
- metadata: {
242
- type: "directory",
341
+ meta: {
342
+ childrenCount: moduleCount,
243
343
  description: "All mounted modules"
244
344
  }
245
345
  });
@@ -259,48 +359,191 @@ var AFS = class extends strict_event_emitter.Emitter {
259
359
  id: matched.module.name,
260
360
  path: matched.modulePath,
261
361
  summary: matched.module.description,
262
- metadata: {
263
- type: "module",
362
+ meta: {
363
+ childrenCount: -1,
264
364
  description: matched.module.description
265
365
  }
266
366
  };
267
367
  results.push(moduleEntry);
268
368
  continue;
269
369
  }
270
- if (!matched.module.list) continue;
370
+ if (!matched.module.list) {
371
+ if (matched.module.read) try {
372
+ const childrenCount = (await matched.module.read(matched.subpath)).data?.meta?.childrenCount;
373
+ if (childrenCount === void 0 || childrenCount === 0) continue;
374
+ throw new Error(`Provider '${matched.module.name}' has childrenCount=${childrenCount} but does not implement list(). Providers with children must implement the list() method.`);
375
+ } catch (error) {
376
+ if (error instanceof Error && error.message.includes("does not implement list")) throw error;
377
+ continue;
378
+ }
379
+ continue;
380
+ }
271
381
  try {
272
- const { data } = await matched.module.list(matched.subpath, {
382
+ const result = await matched.module.list(matched.subpath, {
273
383
  ...options,
274
384
  maxDepth: matched.maxDepth
275
385
  });
276
- const children = data.map((entry) => ({
386
+ const children = result.data.map((entry) => ({
277
387
  ...entry,
278
388
  path: (0, ufo.joinURL)(matched.modulePath, entry.path)
279
389
  }));
280
390
  results.push(...children);
391
+ if (result.message && children.length === 0) return {
392
+ data: results,
393
+ message: result.message
394
+ };
281
395
  } catch (error) {
282
396
  throw new Error(`Error listing from module at ${matched.modulePath}: ${error.message}`);
283
397
  }
284
398
  }
285
399
  return { data: results };
286
400
  }
401
+ /**
402
+ * Check if a path should skip auto-enrichment.
403
+ * Paths ending with /.meta or /.actions should not be enriched
404
+ * to avoid recursive fetches, but their children (e.g., /.meta/kinds)
405
+ * can still be enriched.
406
+ */
407
+ shouldSkipEnrich(path) {
408
+ return path.endsWith("/.meta") || path.endsWith("/.actions");
409
+ }
410
+ /**
411
+ * Fetch actions for a path by listing path/.actions.
412
+ * Returns ActionSummary[] on success, [] on failure.
413
+ */
414
+ async fetchActions(module, subpath) {
415
+ try {
416
+ const actionsPath = (0, ufo.joinURL)(subpath, ".actions");
417
+ const result = await module.list?.(actionsPath);
418
+ if (!result?.data) return [];
419
+ return result.data.filter((entry) => entry.meta?.kind === "afs:executable").map((entry) => ({
420
+ name: entry.id,
421
+ description: entry.meta?.description,
422
+ inputSchema: entry.meta?.inputSchema
423
+ }));
424
+ } catch {
425
+ return [];
426
+ }
427
+ }
428
+ /**
429
+ * Fetch meta for a path by reading path/.meta.
430
+ * Returns the meta content on success, null on failure.
431
+ */
432
+ async fetchMeta(module, subpath) {
433
+ try {
434
+ const metaPath = (0, ufo.joinURL)(subpath, ".meta");
435
+ const result = await module.read?.(metaPath);
436
+ if (!result?.data?.content) return null;
437
+ const content = result.data.content;
438
+ if (typeof content === "object" && content !== null && !Array.isArray(content)) return content;
439
+ return null;
440
+ } catch {
441
+ return null;
442
+ }
443
+ }
444
+ /**
445
+ * Type for data that can be enriched (has path, optional actions, optional meta)
446
+ */
447
+ async enrichData(data, module, subpath) {
448
+ if (this.shouldSkipEnrich(subpath)) return data;
449
+ const result = { ...data };
450
+ const enrichPromises = [];
451
+ if (result.actions === void 0) enrichPromises.push(this.fetchActions(module, subpath).then((actions) => {
452
+ result.actions = actions;
453
+ }));
454
+ if (result.meta?.kind === void 0) enrichPromises.push(this.fetchMeta(module, subpath).then((meta) => {
455
+ if (meta) result.meta = {
456
+ ...result.meta,
457
+ ...meta
458
+ };
459
+ }));
460
+ await Promise.all(enrichPromises);
461
+ return result;
462
+ }
287
463
  async read(path, _options) {
288
464
  const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
465
+ if (normalizedPath === "/.meta/.capabilities") return { data: {
466
+ id: ".capabilities",
467
+ path: "/.meta/.capabilities",
468
+ content: await this.aggregateCapabilities(namespace),
469
+ meta: { kind: "afs:capabilities" }
470
+ } };
289
471
  const modules = this.findModulesInNamespace(normalizedPath, namespace, { exactMatch: true });
290
472
  for (const { module, modulePath, subpath } of modules) {
291
473
  const res = await module.read?.(subpath);
292
- if (res?.data) return {
293
- ...res,
294
- data: {
295
- ...res.data,
296
- path: (0, ufo.joinURL)(modulePath, res.data.path)
297
- }
298
- };
474
+ if (res?.data) {
475
+ const enrichedData = await this.enrichData(res.data, module, subpath);
476
+ return {
477
+ ...res,
478
+ data: {
479
+ ...enrichedData,
480
+ path: (0, ufo.joinURL)(modulePath, res.data.path)
481
+ }
482
+ };
483
+ }
299
484
  }
300
- return {
301
- data: void 0,
302
- message: "File not found"
485
+ throw new require_error.AFSNotFoundError(path);
486
+ }
487
+ /**
488
+ * Aggregate capabilities from all mounted providers.
489
+ *
490
+ * For each provider:
491
+ * - Read /.meta/.capabilities
492
+ * - Merge tools with provider prefix and mount path prefix
493
+ * - Merge actions with mount path prefix on discovery.pathTemplate
494
+ * - Silently skip providers that fail or don't implement capabilities
495
+ */
496
+ async aggregateCapabilities(namespace) {
497
+ const allTools = [];
498
+ const allActions = [];
499
+ const skipped = [];
500
+ const allOperations = [];
501
+ const mounts = this.getMounts(namespace);
502
+ for (const mount of mounts) {
503
+ const { path: mountPath, module: provider } = mount;
504
+ try {
505
+ const content = (await provider.read?.("/.meta/.capabilities"))?.data?.content;
506
+ if (!content) continue;
507
+ const manifest = content;
508
+ for (const tool of manifest.tools ?? []) allTools.push({
509
+ ...tool,
510
+ name: `${manifest.provider}.${tool.name}`,
511
+ path: (0, ufo.joinURL)(mountPath, tool.path)
512
+ });
513
+ for (const actionCatalog of manifest.actions ?? []) allActions.push({
514
+ ...actionCatalog,
515
+ discovery: {
516
+ ...actionCatalog.discovery,
517
+ pathTemplate: (0, ufo.joinURL)(mountPath, actionCatalog.discovery.pathTemplate)
518
+ }
519
+ });
520
+ if (manifest.operations) allOperations.push(manifest.operations);
521
+ } catch {
522
+ skipped.push(mountPath);
523
+ }
524
+ }
525
+ const result = {
526
+ schemaVersion: 1,
527
+ provider: "afs",
528
+ description: "AFS aggregated capabilities",
529
+ tools: allTools,
530
+ actions: allActions
531
+ };
532
+ if (allOperations.length > 0) result.operations = {
533
+ read: allOperations.some((o) => o.read),
534
+ list: allOperations.some((o) => o.list),
535
+ write: allOperations.some((o) => o.write),
536
+ delete: allOperations.some((o) => o.delete),
537
+ search: allOperations.some((o) => o.search),
538
+ exec: allOperations.some((o) => o.exec),
539
+ stat: allOperations.some((o) => o.stat),
540
+ explain: allOperations.some((o) => o.explain)
303
541
  };
542
+ if (skipped.length > 0) {
543
+ result.partial = true;
544
+ result.skipped = skipped;
545
+ }
546
+ return result;
304
547
  }
305
548
  async write(path, content, options) {
306
549
  const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
@@ -308,20 +551,34 @@ var AFS = class extends strict_event_emitter.Emitter {
308
551
  if (!module?.module.write) throw new Error(`No module found for path: ${normalizedPath} in namespace '${namespace ?? "default"}'`);
309
552
  this.checkWritePermission(module.module, "write", path);
310
553
  const res = await module.module.write(module.subpath, content, options);
311
- return {
554
+ const result = {
312
555
  ...res,
313
556
  data: {
314
557
  ...res.data,
315
558
  path: (0, ufo.joinURL)(module.modulePath, res.data.path)
316
559
  }
317
560
  };
561
+ this.notifyChange({
562
+ kind: "write",
563
+ path: result.data.path,
564
+ moduleName: module.module.name,
565
+ timestamp: Date.now()
566
+ });
567
+ return result;
318
568
  }
319
569
  async delete(path, options) {
320
570
  const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
321
571
  const module = this.findModulesInNamespace(normalizedPath, namespace, { exactMatch: true })[0];
322
572
  if (!module?.module.delete) throw new Error(`No module found for path: ${normalizedPath} in namespace '${namespace ?? "default"}'`);
323
573
  this.checkWritePermission(module.module, "delete", path);
324
- return await module.module.delete(module.subpath, options);
574
+ const result = await module.module.delete(module.subpath, options);
575
+ this.notifyChange({
576
+ kind: "delete",
577
+ path: (0, ufo.joinURL)(module.modulePath, module.subpath),
578
+ moduleName: module.module.name,
579
+ timestamp: Date.now()
580
+ });
581
+ return result;
325
582
  }
326
583
  async rename(oldPath, newPath, options) {
327
584
  const { namespace: oldNamespace, path: normalizedOldPath } = this.parsePathWithNamespace(oldPath);
@@ -332,43 +589,20 @@ var AFS = class extends strict_event_emitter.Emitter {
332
589
  if (!oldModule || !newModule || oldModule.modulePath !== newModule.modulePath) throw new Error(`Cannot rename across different modules. Both paths must be in the same module.`);
333
590
  if (!oldModule.module.rename) throw new Error(`Module does not support rename operation: ${oldModule.modulePath}`);
334
591
  this.checkWritePermission(oldModule.module, "rename", oldPath);
335
- return await oldModule.module.rename(oldModule.subpath, newModule.subpath, options);
592
+ const result = await oldModule.module.rename(oldModule.subpath, newModule.subpath, options);
593
+ this.notifyChange({
594
+ kind: "rename",
595
+ path: (0, ufo.joinURL)(oldModule.modulePath, oldModule.subpath),
596
+ moduleName: oldModule.module.name,
597
+ namespace: oldNamespace,
598
+ meta: { newPath: (0, ufo.joinURL)(newModule.modulePath, newModule.subpath) },
599
+ timestamp: Date.now()
600
+ });
601
+ return result;
336
602
  }
337
603
  async search(path, query, options = {}) {
338
604
  const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
339
- let preset;
340
- if (options.preset) {
341
- preset = this.options?.context?.search?.presets?.[options.preset];
342
- if (!preset) throw new Error(`Preset not found: ${options.preset}`);
343
- }
344
- return await this.processWithPreset(normalizedPath, query, preset, {
345
- ...options,
346
- defaultSelect: () => this._search(normalizedPath, namespace, query, options)
347
- });
348
- }
349
- async processWithPreset(path, query, preset, options) {
350
- const select = options.select || preset?.select;
351
- const per = options.per || preset?.per;
352
- const dedupe = options.dedupe || preset?.dedupe;
353
- const format = options.format || preset?.format;
354
- const entries = select ? (await this._select(path, query, select, options)).data : (await options.defaultSelect()).data;
355
- const mapped = per ? await Promise.all(entries.map((data) => per.invoke({ data }, options).then((res) => res.data))) : entries;
356
- const deduped = dedupe ? await dedupe.invoke({ data: mapped }, options).then((res) => res.data) : mapped;
357
- let formatted = deduped;
358
- if (format === "simple-list" || format === "tree") {
359
- const valid = zod.z.array(require_type.afsEntrySchema).safeParse(deduped);
360
- if (!valid.data) throw new Error("Tree format requires entries to be AFSEntry objects");
361
- if (format === "tree") formatted = this.buildTreeView(valid.data);
362
- else if (format === "simple-list") formatted = this.buildSimpleListView(valid.data);
363
- } else if (typeof format === "object" && typeof format.invoke === "function") formatted = await format.invoke({ data: deduped }, options).then((res) => res.data);
364
- return { data: formatted };
365
- }
366
- async _select(path, query, select, options) {
367
- const { data } = await select.invoke({
368
- path,
369
- query
370
- }, options);
371
- return { data: (await Promise.all(data.map((p) => this.read(p).then((res) => res.data)))).filter((i) => !!i) };
605
+ return await this._search(normalizedPath, namespace, query, options);
372
606
  }
373
607
  async _search(path, namespace, query, options) {
374
608
  const results = [];
@@ -391,50 +625,169 @@ var AFS = class extends strict_event_emitter.Emitter {
391
625
  message: messages.join("; ")
392
626
  };
393
627
  }
394
- async exec(path, args, options) {
628
+ async exec(path, args, options = {}) {
395
629
  const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
630
+ if (normalizedPath.startsWith("/.actions/")) return await this.execRootAction(normalizedPath, args);
396
631
  const module = this.findModulesInNamespace(normalizedPath, namespace)[0];
397
632
  if (!module?.module.exec) throw new Error(`No module found for path: ${normalizedPath} in namespace '${namespace ?? "default"}'`);
398
- return await module.module.exec(module.subpath, args, options);
399
- }
400
- buildSimpleListView(entries) {
401
- return entries.map((entry) => `${entry.path}${this.buildMetadataSuffix(entry)}`);
402
- }
403
- buildTreeView(entries) {
404
- const tree = {};
405
- const entryMap = /* @__PURE__ */ new Map();
406
- for (const entry of entries) {
407
- entryMap.set(entry.path, entry);
408
- const parts = entry.path.split("/").filter(Boolean);
409
- let current = tree;
410
- for (const part of parts) {
411
- if (!current[part]) current[part] = {};
412
- current = current[part];
633
+ this.checkWritePermission(module.module, "exec", path);
634
+ await this.validateExecInput(module.module, module.subpath, args);
635
+ const enhancedOptions = {
636
+ ...options,
637
+ context: {
638
+ ...options?.context,
639
+ afs: this
640
+ }
641
+ };
642
+ return await module.module.exec(module.subpath, args, enhancedOptions);
643
+ }
644
+ /**
645
+ * Handle root-level action execution (/.actions/*).
646
+ * Currently supports:
647
+ * - /.actions/mount: Load and mount a provider via loadProvider
648
+ */
649
+ async execRootAction(path, args) {
650
+ const actionName = path.slice(10);
651
+ if (actionName === "mount") return await this.execRootMountAction(args);
652
+ throw new require_error.AFSNotFoundError(path, `Root action not found: ${actionName}`);
653
+ }
654
+ /**
655
+ * Execute root-level mount action.
656
+ * Validates input and delegates to loadProvider.
657
+ */
658
+ async execRootMountAction(args) {
659
+ if (typeof args.uri !== "string" || args.uri === "") throw new require_error.AFSValidationError("Input validation failed: uri: must be a non-empty string");
660
+ if (typeof args.path !== "string" || args.path === "") throw new require_error.AFSValidationError("Input validation failed: path: must be a non-empty string");
661
+ if (!this.loadProvider) throw new Error("loadProvider not configured");
662
+ await this.loadProvider(args.uri, args.path);
663
+ return {
664
+ success: true,
665
+ data: {
666
+ uri: args.uri,
667
+ path: args.path
413
668
  }
414
- }
415
- const renderTree = (node, prefix = "", currentPath = "") => {
416
- let result = "";
417
- const keys = Object.keys(node);
418
- keys.forEach((key, index) => {
419
- const isLast = index === keys.length - 1;
420
- const fullPath = currentPath ? `${currentPath}/${key}` : `/${key}`;
421
- const entry = entryMap.get(fullPath);
422
- result += `${prefix}${isLast ? "└── " : "├── "}${key}${entry ? this.buildMetadataSuffix(entry) : ""}`;
423
- result += `\n`;
424
- result += renderTree(node[key], `${prefix}${isLast ? " " : "│ "}`, fullPath);
425
- });
426
- return result;
427
669
  };
428
- return renderTree(tree);
429
- }
430
- buildMetadataSuffix(entry) {
431
- const metadataParts = [];
432
- const childrenCount = entry?.metadata?.childrenCount;
433
- if (typeof childrenCount === "number") metadataParts.push(`${childrenCount} items`);
434
- if (entry?.metadata?.childrenTruncated) metadataParts.push("truncated");
435
- if (entry?.metadata?.gitignored) metadataParts.push("gitignored");
436
- if (entry?.metadata?.execute) metadataParts.push("executable");
437
- return metadataParts.length > 0 ? ` [${metadataParts.join(", ")}]` : "";
670
+ }
671
+ /**
672
+ * Validate exec input args against inputSchema.
673
+ * Throws AFSValidationError if validation fails.
674
+ * Uses zod-from-json-schema for full JSON Schema validation.
675
+ */
676
+ async validateExecInput(module, subpath, args) {
677
+ let inputSchema;
678
+ if (module.read) try {
679
+ inputSchema = (await module.read(subpath)).data?.meta?.inputSchema;
680
+ } catch {
681
+ return;
682
+ }
683
+ if (!inputSchema) return;
684
+ try {
685
+ const { convertJsonSchemaToZod } = await import("zod-from-json-schema");
686
+ convertJsonSchemaToZod(inputSchema).parse(args);
687
+ } catch (error) {
688
+ if (error instanceof Error && error.name === "ZodError") throw new require_error.AFSValidationError(`Input validation failed: ${error.issues.map((issue) => {
689
+ const path = issue.path.join(".");
690
+ return path ? `${path}: ${issue.message}` : issue.message;
691
+ }).join("; ")}`);
692
+ }
693
+ }
694
+ /**
695
+ * Get stat information for a path
696
+ *
697
+ * Resolution order:
698
+ * 1. Provider's stat() method (if implemented)
699
+ * 2. Fallback to read() - extracts stat data from AFSEntry
700
+ *
701
+ * This allows providers to implement only read() while stat() still works.
702
+ */
703
+ async stat(path, options) {
704
+ const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
705
+ const module = this.findModulesInNamespace(normalizedPath, namespace)[0];
706
+ if (!module) throw new require_error.AFSNotFoundError(path);
707
+ if (module.module.stat) try {
708
+ const result = await module.module.stat(module.subpath, options);
709
+ if (result.data) {
710
+ const enrichedData = await this.enrichData(result.data, module.module, module.subpath);
711
+ return {
712
+ ...result,
713
+ data: enrichedData
714
+ };
715
+ }
716
+ throw new require_error.AFSNotFoundError(path);
717
+ } catch (error) {
718
+ if (error instanceof require_error.AFSNotFoundError) throw error;
719
+ }
720
+ if (module.module.read) {
721
+ const readResult = await module.module.read(module.subpath, options);
722
+ if (readResult.data) {
723
+ const { content: _content, ...statData } = readResult.data;
724
+ return { data: await this.enrichData(statData, module.module, module.subpath) };
725
+ }
726
+ }
727
+ throw new require_error.AFSNotFoundError(path);
728
+ }
729
+ /**
730
+ * Get human-readable explanation for a path
731
+ *
732
+ * Resolution order:
733
+ * 1. Provider's explain() method (if implemented)
734
+ * 2. Fallback to stat() - builds explanation from metadata
735
+ *
736
+ * This allows providers to skip implementing explain() while it still works.
737
+ */
738
+ async explain(path, options) {
739
+ const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
740
+ const module = this.findModulesInNamespace(normalizedPath, namespace)[0];
741
+ if (!module) throw new require_error.AFSNotFoundError(path);
742
+ if (module.module.explain) try {
743
+ return await module.module.explain(module.subpath, options);
744
+ } catch (error) {
745
+ if (error instanceof require_error.AFSNotFoundError) throw error;
746
+ }
747
+ const data = (await this.stat(path, options)).data;
748
+ if (data) {
749
+ const lines = [];
750
+ lines.push(`# ${normalizedPath}`);
751
+ lines.push("");
752
+ const meta = data.meta || {};
753
+ if (meta.size !== void 0) lines.push(`- **Size**: ${this.formatBytes(meta.size)}`);
754
+ if (meta.childrenCount !== void 0) lines.push(`- **Children**: ${meta.childrenCount} items`);
755
+ if (data.updatedAt) lines.push(`- **Modified**: ${data.updatedAt.toISOString()}`);
756
+ if (meta.description) {
757
+ lines.push("");
758
+ lines.push("## Description");
759
+ lines.push(String(meta.description));
760
+ }
761
+ if (meta.provider) lines.push(`- **Provider**: ${meta.provider}`);
762
+ if (meta.kind) lines.push(`- **Kind**: ${meta.kind}`);
763
+ if (meta.kinds && Array.isArray(meta.kinds)) lines.push(`- **Kinds**: ${meta.kinds.join(", ")}`);
764
+ if (data.actions && data.actions.length > 0) {
765
+ lines.push("");
766
+ lines.push("## Actions");
767
+ for (const action of data.actions) lines.push(`- **${action.name}**${action.description ? `: ${action.description}` : ""}`);
768
+ }
769
+ return {
770
+ format: "markdown",
771
+ content: lines.join("\n")
772
+ };
773
+ }
774
+ throw new Error(`No explain or stat handler for path: ${normalizedPath} in namespace '${namespace ?? "default"}'`);
775
+ }
776
+ /**
777
+ * Format bytes to human-readable string
778
+ */
779
+ formatBytes(bytes) {
780
+ if (bytes === 0) return "0 B";
781
+ const k = 1024;
782
+ const sizes = [
783
+ "B",
784
+ "KB",
785
+ "MB",
786
+ "GB",
787
+ "TB"
788
+ ];
789
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
790
+ return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
438
791
  }
439
792
  physicalPath;
440
793
  async initializePhysicalPath() {