@cldmv/slothlet 2.10.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENT-USAGE.md CHANGED
@@ -144,6 +144,47 @@ export function rootFunctionShout(message) {
144
144
 
145
145
  > 📖 **See**: [API-RULES.md Rule 4](./API-RULES.md#rule-4-default-export-container-pattern) for root-level default export handling
146
146
 
147
+ ### Pattern 5: AddApi Special File Pattern (Rule 11)
148
+
149
+ **File**: `addapi.mjs` loaded via `addApi()` → **API**: Always flattened for API extensions
150
+
151
+ ```js
152
+ // File: plugins/addapi.mjs
153
+ /**
154
+ * Special addapi.mjs file for runtime API extensions.
155
+ * Always flattens regardless of autoFlatten setting.
156
+ */
157
+ export function initializePlugin() {
158
+ return "Plugin initialized";
159
+ }
160
+
161
+ export function cleanup() {
162
+ return "Plugin cleaned up";
163
+ }
164
+
165
+ export function configure(options) {
166
+ return `Configured with ${options}`;
167
+ }
168
+
169
+ // Usage:
170
+ await api.addApi("plugins", "./plugins-folder");
171
+
172
+ // Result: Always flattened (no .addapi. level)
173
+ api.plugins.initializePlugin(); // ✅ Direct extension
174
+ api.plugins.cleanup(); // ✅ No intermediate namespace
175
+ api.plugins.configure(opts); // ✅ Seamless integration
176
+ ```
177
+
178
+ **Result**: `addapi.mjs` always flattens → Perfect for plugin systems and runtime extensions
179
+
180
+ **Use Cases**:
181
+
182
+ - 🔌 **Plugin Systems**: Runtime plugin loading
183
+ - 🔄 **Hot Reloading**: Dynamic API updates during development
184
+ - 📦 **Modular Extensions**: Clean extension of existing API surfaces
185
+
186
+ > 📖 **See**: [API-RULES.md Rule 11](./API-RULES.md#rule-11-addapi-special-file-pattern) for technical implementation details
187
+
147
188
  ## 🔄 Cross-Module Communication Patterns
148
189
 
149
190
  ### ✅ Using Live Bindings
package/README.md CHANGED
@@ -35,14 +35,19 @@ The name might suggest we're taking it easy, but don't be fooled. **Slothlet del
35
35
 
36
36
  ## ✨ What's New
37
37
 
38
- ### Latest: v2.9 (December 30, 2025)
38
+ ### Latest: v2.11.0 (January 2025)
39
39
 
40
- - **Per-Request Context Isolation** - New `api.run()` and `api.scope()` methods for isolated context execution ([Documentation](https://github.com/CLDMV/slothlet/blob/master/docs/CONTEXT-PROPAGATION.md#per-request-context-isolation))
41
- - API Builder Modularization - Improved maintainability and code organization
42
- - [View Changelog](https://github.com/CLDMV/slothlet/blob/master/docs/changelog/v2.9.md)
40
+ - **AddApi Special File Pattern (Rule 11)** - Files named `addapi.mjs` now always flatten for seamless API namespace extensions
41
+ - **Filename-Matches-Container in addApi (Rule 1 Extension)** - Auto-flattening now works in runtime `addApi()` contexts
42
+ - **Enhanced addApi Content Preservation** - Fixed critical issue where multiple addApi calls were overwriting previous content instead of merging
43
+ - **Rule 12 Smart Flattening Enhancements** - Comprehensive smart flattening improvements with 168-scenario test coverage
44
+ - **API Documentation Suite Overhaul** - Enhanced 3-tier navigation system with verified examples and cross-references
45
+ - [View Changelog](./docs/changelog/v2.11.md)
43
46
 
44
47
  ### Recent Releases
45
48
 
49
+ - **v2.10.0** - Function metadata tagging and introspection capabilities ([Changelog](https://github.com/CLDMV/slothlet/blob/master/docs/changelog/v2.10.md))
50
+ - **v2.9** - Per-Request Context Isolation with `api.run()` and `api.scope()` methods ([Changelog](https://github.com/CLDMV/slothlet/blob/master/docs/changelog/v2.9.md))
46
51
  - **v2.8** - NPM security fixes and package workflow updates ([Changelog](https://github.com/CLDMV/slothlet/blob/master/docs/changelog/v2.8.md))
47
52
  - **v2.7** - Security updates ([Changelog](https://github.com/CLDMV/slothlet/blob/master/docs/changelog/v2.7.md))
48
53
  - **v2.6** - Hook System with 4 interceptor types ([Changelog](https://github.com/CLDMV/slothlet/blob/master/docs/changelog/v2.6.md))
@@ -130,7 +135,8 @@ Automatic context preservation across all asynchronous boundaries:
130
135
 
131
136
  ### Requirements
132
137
 
133
- - **Node.js v16.4.0 or higher** (required for AsyncLocalStorage support)
138
+ - **Node.js v16.20.2 or higher** (required for stack trace API fixes used in path resolution)
139
+ - Node.js 16.4-16.19 has a stack trace regression. For these versions, use slothlet 2.10.0: `npm install @cldmv/slothlet@2.10.0`
134
140
 
135
141
  ### Install
136
142
 
@@ -324,21 +330,22 @@ console.log("My version:", self.version);
324
330
 
325
331
  ## 📚 Configuration Options
326
332
 
327
- | Option | Type | Default | Description |
328
- | ------------------- | --------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
329
- | `dir` | `string` | `"api"` | Directory to load API modules from (absolute or relative path) |
330
- | `mode` | `string` | `"eager"` | **New** loading mode - `"lazy"` for on-demand loading, `"eager"` for immediate loading |
331
- | `lazy` | `boolean` | `false` | **Legacy** loading strategy (use `mode` instead) |
332
- | `engine` | `string` | `"singleton"` | Execution environment: `"singleton"`, `"vm"`, `"worker"`, or `"fork"` (experimental modes) |
333
- | `runtime` | `string` | `"async"` | Runtime binding system: `"async"` for AsyncLocalStorage (requires Node.js v16.4.0+), `"live"` for live-bindings (works on Node.js v12.20.0+) |
334
- | `apiDepth` | `number` | `Infinity` | Directory traversal depth - `0` for root only, `Infinity` for all levels |
335
- | `debug` | `boolean` | `false` | Enable verbose logging (also via `--slothletdebug` flag or `SLOTHLET_DEBUG=true` env var) |
336
- | `api_mode` | `string` | `"auto"` | API structure behavior: `"auto"` (detect), `"function"` (force callable), `"object"` (force object) |
337
- | `allowApiOverwrite` | `boolean` | `true` | Allow `addApi()` to overwrite existing endpoints (`false` = prevent overwrites with warning) |
338
- | `context` | `object` | `{}` | Context data injected into live-binding (available via `import { context } from "@cldmv/slothlet/runtime"`) |
339
- | `reference` | `object` | `{}` | Reference object merged into API root level |
340
- | `sanitize` | `object` | `{}` | Advanced filename-to-API transformation control with `lowerFirst`, `preserveAllUpper`, `preserveAllLower`, and `rules` (supports exact matches, glob patterns `*json*`, and boundary patterns `**url**`) |
341
- | `hooks` | `mixed` | `false` | Enable hook system: `true` (enable all), `"pattern"` (enable with pattern), or object with `enabled`, `pattern`, `suppressErrors` options |
333
+ | Option | Type | Default | Description |
334
+ | ----------------------- | --------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
335
+ | `dir` | `string` | `"api"` | Directory to load API modules from (absolute or relative path) |
336
+ | `mode` | `string` | `"eager"` | **New** loading mode - `"lazy"` for on-demand loading, `"eager"` for immediate loading |
337
+ | `lazy` | `boolean` | `false` | **Legacy** loading strategy (use `mode` instead) |
338
+ | `engine` | `string` | `"singleton"` | Execution environment: `"singleton"`, `"vm"`, `"worker"`, or `"fork"` (experimental modes) |
339
+ | `runtime` | `string` | `"async"` | Runtime binding system: `"async"` for AsyncLocalStorage (requires Node.js v16.20.2+), `"live"` for live-bindings (works on Node.js v12.20.0+) |
340
+ | `apiDepth` | `number` | `Infinity` | Directory traversal depth - `0` for root only, `Infinity` for all levels |
341
+ | `debug` | `boolean` | `false` | Enable verbose logging (also via `--slothletdebug` flag or `SLOTHLET_DEBUG=true` env var) |
342
+ | `api_mode` | `string` | `"auto"` | API structure behavior: `"auto"` (detect), `"function"` (force callable), `"object"` (force object) |
343
+ | `allowApiOverwrite` | `boolean` | `true` | Allow `addApi()` to overwrite existing endpoints (`false` = prevent overwrites with warning) |
344
+ | `enableModuleOwnership` | `boolean` | `false` | Enable module-based API ownership tracking (`true` = track ownership for selective overwrites, `false` = disabled for performance) |
345
+ | `context` | `object` | `{}` | Context data injected into live-binding (available via `import { context } from "@cldmv/slothlet/runtime"`) |
346
+ | `reference` | `object` | `{}` | Reference object merged into API root level |
347
+ | `sanitize` | `object` | `{}` | Advanced filename-to-API transformation control with `lowerFirst`, `preserveAllUpper`, `preserveAllLower`, and `rules` (supports exact matches, glob patterns `*json*`, and boundary patterns `**url**`) |
348
+ | `hooks` | `mixed` | `false` | Enable hook system: `true` (enable all), `"pattern"` (enable with pattern), or object with `enabled`, `pattern`, `suppressErrors` options |
342
349
 
343
350
  **For complete API documentation with detailed parameter descriptions and examples, see [docs/generated/API.md](https://github.com/CLDMV/slothlet/blob/master/docs/generated/API.md)**
344
351
 
@@ -24,7 +24,20 @@ import { resolvePathFromCaller } from "@cldmv/slothlet/helpers/resolve-from-call
24
24
  import { cleanMetadata, tagLoadedFunctions } from "./metadata.mjs";
25
25
 
26
26
 
27
- export async function addApiFromFolder({ apiPath, folderPath, instance, metadata = {} }) {
27
+
28
+
29
+ export async function addApiFromFolder({ apiPath, folderPath, instance, metadata = {}, options = {} }) {
30
+ const { forceOverwrite = false, moduleId } = options;
31
+
32
+
33
+ if (forceOverwrite && !moduleId) {
34
+ throw new Error(`[slothlet] Rule 12: forceOverwrite requires moduleId parameter for ownership tracking`);
35
+ }
36
+
37
+ if (forceOverwrite && !instance.config.enableModuleOwnership) {
38
+ throw new Error(`[slothlet] Rule 12: forceOverwrite requires enableModuleOwnership: true in slothlet configuration`);
39
+ }
40
+
28
41
  if (!instance.loaded) {
29
42
  throw new Error("[slothlet] Cannot add API: API not loaded. Call create() or load() first.");
30
43
  }
@@ -81,6 +94,74 @@ export async function addApiFromFolder({ apiPath, folderPath, instance, metadata
81
94
  }
82
95
 
83
96
 
97
+
98
+
99
+
100
+
101
+ let earlyCurrentTarget = instance.api;
102
+ let earlyCurrentBoundTarget = instance.boundapi;
103
+ const earlyPathParts = normalizedApiPath.split(".");
104
+
105
+ for (let i = 0; i < earlyPathParts.length - 1; i++) {
106
+ const part = earlyPathParts[i];
107
+ const key = instance._toapiPathKey(part);
108
+ if (earlyCurrentTarget[key]) {
109
+ earlyCurrentTarget = earlyCurrentTarget[key];
110
+ } else {
111
+ earlyCurrentTarget = null;
112
+ break;
113
+ }
114
+ if (earlyCurrentBoundTarget[key]) {
115
+ earlyCurrentBoundTarget = earlyCurrentBoundTarget[key];
116
+ } else {
117
+ earlyCurrentBoundTarget = null;
118
+ break;
119
+ }
120
+ }
121
+
122
+ const earlyFinalKey = instance._toapiPathKey(earlyPathParts[earlyPathParts.length - 1]);
123
+ let superEarlyExistingTargetContent = null;
124
+ let superEarlyExistingBoundContent = null;
125
+
126
+ if (earlyCurrentTarget && earlyCurrentTarget[earlyFinalKey]) {
127
+ if (typeof earlyCurrentTarget[earlyFinalKey] === "function" && earlyCurrentTarget[earlyFinalKey].__slothletPath) {
128
+ if (instance.config.debug) {
129
+ console.log(`[DEBUG] addApi: SUPER EARLY - Target is lazy proxy - materializing to capture existing content`);
130
+ }
131
+ const _ = earlyCurrentTarget[earlyFinalKey].__trigger;
132
+ await earlyCurrentTarget[earlyFinalKey]();
133
+ }
134
+ if (typeof earlyCurrentTarget[earlyFinalKey] === "object") {
135
+ superEarlyExistingTargetContent = { ...earlyCurrentTarget[earlyFinalKey] };
136
+ if (instance.config.debug) {
137
+ console.log(`[DEBUG] addApi: SUPER EARLY - Captured existing target content:`, Object.keys(superEarlyExistingTargetContent));
138
+ }
139
+ }
140
+ }
141
+
142
+ if (earlyCurrentBoundTarget && earlyCurrentBoundTarget[earlyFinalKey]) {
143
+ if (instance.config.debug) {
144
+ console.log(
145
+ `[DEBUG] addApi: SUPER EARLY - currentBoundTarget[${earlyFinalKey}] exists, type:`,
146
+ typeof earlyCurrentBoundTarget[earlyFinalKey]
147
+ );
148
+ }
149
+ if (typeof earlyCurrentBoundTarget[earlyFinalKey] === "function" && earlyCurrentBoundTarget[earlyFinalKey].__slothletPath) {
150
+ if (instance.config.debug) {
151
+ console.log(`[DEBUG] addApi: SUPER EARLY - Bound target is lazy proxy - materializing to capture existing bound content`);
152
+ }
153
+ const _ = earlyCurrentBoundTarget[earlyFinalKey].__trigger;
154
+ await earlyCurrentBoundTarget[earlyFinalKey]();
155
+ }
156
+ if (typeof earlyCurrentBoundTarget[earlyFinalKey] === "object") {
157
+ superEarlyExistingBoundContent = { ...earlyCurrentBoundTarget[earlyFinalKey] };
158
+ if (instance.config.debug) {
159
+ console.log(`[DEBUG] addApi: SUPER EARLY - Captured existing bound content:`, Object.keys(superEarlyExistingBoundContent));
160
+ }
161
+ }
162
+ }
163
+
164
+
84
165
  let newModules;
85
166
  if (instance.config.lazy) {
86
167
 
@@ -90,6 +171,87 @@ export async function addApiFromFolder({ apiPath, folderPath, instance, metadata
90
171
  newModules = await instance.modes.eager.create.call(instance, resolvedFolderPath, instance.config.apiDepth || Infinity, 0);
91
172
  }
92
173
 
174
+ if (instance.config.debug) {
175
+ console.log(`[DEBUG] addApi: Loaded modules structure:`, Object.keys(newModules || {}));
176
+ console.log(`[DEBUG] addApi: Full newModules:`, newModules);
177
+ }
178
+
179
+
180
+
181
+ if (newModules && typeof newModules === "object" && newModules.addapi) {
182
+ if (instance.config.debug) {
183
+ console.log(`[DEBUG] addApi: Found addapi.mjs - applying Rule 6 flattening`);
184
+ console.log(`[DEBUG] addApi: Original structure:`, Object.keys(newModules));
185
+ console.log(`[DEBUG] addApi: Addapi contents:`, Object.keys(newModules.addapi));
186
+ }
187
+
188
+
189
+ const addapiContent = newModules.addapi;
190
+
191
+
192
+ delete newModules.addapi;
193
+
194
+
195
+ if (addapiContent && typeof addapiContent === "object") {
196
+
197
+ Object.assign(newModules, addapiContent);
198
+
199
+ if (instance.config.debug) {
200
+ console.log(`[DEBUG] addApi: After addapi flattening:`, Object.keys(newModules));
201
+ }
202
+ } else if (typeof addapiContent === "function") {
203
+
204
+ Object.assign(newModules, addapiContent);
205
+
206
+ if (instance.config.debug) {
207
+ console.log(`[DEBUG] addApi: Flattened addapi function with properties:`, Object.keys(newModules));
208
+ }
209
+ }
210
+ }
211
+
212
+
213
+
214
+
215
+ const pathSegments = normalizedApiPath.split(".");
216
+ const lastSegment = pathSegments[pathSegments.length - 1];
217
+ let rootLevelFileContent = null;
218
+
219
+ if (newModules && typeof newModules === "object" && newModules[lastSegment]) {
220
+ if (instance.config.debug) {
221
+ console.log(`[DEBUG] addApi: Found root-level file matching API path segment "${lastSegment}" - applying Rule 7 flattening`);
222
+ }
223
+
224
+
225
+ let fileContent = newModules[lastSegment];
226
+ if (typeof fileContent === "function" && fileContent.name && fileContent.name.startsWith("lazyFolder_")) {
227
+
228
+ if (fileContent.__slothletPath) {
229
+ const _ = fileContent.__trigger;
230
+ await fileContent();
231
+
232
+ fileContent = newModules[lastSegment];
233
+ if (instance.config.debug) {
234
+ console.log(`[DEBUG] addApi: Materialized lazy proxy for root-level file:`, Object.keys(fileContent || {}));
235
+ }
236
+ }
237
+ }
238
+
239
+ if (instance.config.debug) {
240
+ console.log(`[DEBUG] addApi: Root-level file content:`, Object.keys(fileContent || {}));
241
+ }
242
+
243
+
244
+
245
+ rootLevelFileContent = fileContent && typeof fileContent === "object" ? { ...fileContent } : fileContent;
246
+
247
+
248
+ delete newModules[lastSegment];
249
+
250
+ if (instance.config.debug) {
251
+ console.log(`[DEBUG] addApi: After removing root-level file, remaining structure:`, Object.keys(newModules));
252
+ }
253
+ }
254
+
93
255
 
94
256
 
95
257
 
@@ -169,6 +331,14 @@ export async function addApiFromFolder({ apiPath, folderPath, instance, metadata
169
331
 
170
332
  const finalKey = instance._toapiPathKey(pathParts[pathParts.length - 1]);
171
333
 
334
+ if (instance.config.debug) {
335
+ console.log(`[DEBUG] addApi: Final assignment - newModules type:`, typeof newModules, "keys:", Object.keys(newModules || {}));
336
+ }
337
+
338
+
339
+
340
+
341
+
172
342
 
173
343
  if (typeof newModules === "function") {
174
344
 
@@ -177,7 +347,21 @@ export async function addApiFromFolder({ apiPath, folderPath, instance, metadata
177
347
  const existing = currentTarget[finalKey];
178
348
 
179
349
 
180
- if (instance.config.allowApiOverwrite === false) {
350
+ if (instance.config.enableModuleOwnership && existing) {
351
+ const normalizedPath = normalizedApiPath + (normalizedApiPath ? "." : "") + finalKey;
352
+ const canOverwrite = instance._validateModuleOwnership(normalizedPath, moduleId, forceOverwrite);
353
+
354
+ if (forceOverwrite && !canOverwrite) {
355
+ const existingOwner = instance._getApiOwnership(normalizedPath);
356
+ throw new Error(
357
+ `[slothlet] Rule 12: Cannot overwrite API "${normalizedPath}" - owned by module "${existingOwner}", ` +
358
+ `attempted by module "${moduleId}". Modules can only overwrite APIs they own.`
359
+ );
360
+ }
361
+ }
362
+
363
+
364
+ if (!forceOverwrite && instance.config.allowApiOverwrite === false) {
181
365
  console.warn(
182
366
  `[slothlet] Skipping addApi: API path "${normalizedApiPath}" final key "${finalKey}" ` +
183
367
  `already exists (type: "${typeof existing}"). Set allowApiOverwrite: true to allow overwrites.`
@@ -206,7 +390,21 @@ export async function addApiFromFolder({ apiPath, folderPath, instance, metadata
206
390
  const existing = currentTarget[finalKey];
207
391
 
208
392
 
209
- if (instance.config.allowApiOverwrite === false && existing !== undefined && existing !== null) {
393
+ if (instance.config.enableModuleOwnership && existing) {
394
+ const normalizedPath = normalizedApiPath + (normalizedApiPath ? "." : "") + finalKey;
395
+ const canOverwrite = instance._validateModuleOwnership(normalizedPath, moduleId, forceOverwrite);
396
+
397
+ if (forceOverwrite && !canOverwrite) {
398
+ const existingOwner = instance._getApiOwnership(normalizedPath);
399
+ throw new Error(
400
+ `[slothlet] Rule 12: Cannot overwrite API "${normalizedPath}" - owned by module "${existingOwner}", ` +
401
+ `attempted by module "${moduleId}". Modules can only overwrite APIs they own.`
402
+ );
403
+ }
404
+ }
405
+
406
+
407
+ if (!forceOverwrite && instance.config.allowApiOverwrite === false && existing !== undefined && existing !== null) {
210
408
 
211
409
  const hasContent = typeof existing === "object" ? Object.keys(existing).length > 0 : true;
212
410
  if (hasContent) {
@@ -267,8 +465,54 @@ export async function addApiFromFolder({ apiPath, folderPath, instance, metadata
267
465
 
268
466
 
269
467
 
468
+
469
+
470
+ if (superEarlyExistingTargetContent) {
471
+ if (instance.config.debug) {
472
+ console.log(`[DEBUG] addApi: Restoring existing content - keys:`, Object.keys(superEarlyExistingTargetContent));
473
+ }
474
+ Object.assign(currentTarget[finalKey], superEarlyExistingTargetContent);
475
+ }
476
+ if (superEarlyExistingBoundContent) {
477
+ if (instance.config.debug) {
478
+ console.log(`[DEBUG] addApi: Restoring existing BOUND content - keys:`, Object.keys(superEarlyExistingBoundContent));
479
+ }
480
+ Object.assign(currentBoundTarget[finalKey], superEarlyExistingBoundContent);
481
+ }
482
+
483
+
484
+ if (instance.config.debug) {
485
+ console.log(`[DEBUG] addApi: Before merging new modules - current keys:`, Object.keys(currentTarget[finalKey] || {}));
486
+ console.log(`[DEBUG] addApi: New modules to merge - keys:`, Object.keys(newModules));
487
+ }
270
488
  Object.assign(currentTarget[finalKey], newModules);
271
489
  Object.assign(currentBoundTarget[finalKey], newModules);
490
+ if (instance.config.debug) {
491
+ console.log(`[DEBUG] addApi: After merging new modules - keys:`, Object.keys(currentTarget[finalKey] || {}));
492
+ }
493
+
494
+
495
+ if (rootLevelFileContent !== null) {
496
+ if (instance.config.debug) {
497
+ console.log(`[DEBUG] addApi: Merging root-level file content into API path "${normalizedApiPath}"`);
498
+ console.log(`[DEBUG] addApi: Root-level file functions:`, Object.keys(rootLevelFileContent));
499
+ console.log(`[DEBUG] addApi: Target before root-level merge:`, Object.keys(currentTarget[finalKey]));
500
+ }
501
+
502
+
503
+ if (rootLevelFileContent && typeof rootLevelFileContent === "object") {
504
+ Object.assign(currentTarget[finalKey], rootLevelFileContent);
505
+ Object.assign(currentBoundTarget[finalKey], rootLevelFileContent);
506
+ } else if (typeof rootLevelFileContent === "function") {
507
+
508
+ Object.assign(currentTarget[finalKey], rootLevelFileContent);
509
+ Object.assign(currentBoundTarget[finalKey], rootLevelFileContent);
510
+ }
511
+
512
+ if (instance.config.debug) {
513
+ console.log(`[DEBUG] addApi: After merging root-level file, final API structure:`, Object.keys(currentTarget[finalKey]));
514
+ }
515
+ }
272
516
  } else if (newModules === null || newModules === undefined) {
273
517
 
274
518
  const receivedType = newModules === null ? "null" : "undefined";
@@ -284,7 +528,24 @@ export async function addApiFromFolder({ apiPath, folderPath, instance, metadata
284
528
  }
285
529
 
286
530
 
531
+ if (instance.config.debug) {
532
+ console.log(`[DEBUG] addApi: Before updateBindings - currentTarget[${finalKey}]:`, Object.keys(currentTarget[finalKey] || {}));
533
+ console.log(
534
+ `[DEBUG] addApi: Before updateBindings - currentBoundTarget[${finalKey}]:`,
535
+ Object.keys(currentBoundTarget[finalKey] || {})
536
+ );
537
+ }
538
+
539
+
540
+ if (instance.config.enableModuleOwnership && moduleId) {
541
+ const fullApiPath = normalizedApiPath + (normalizedApiPath ? "." : "") + finalKey;
542
+ instance._registerApiOwnership(fullApiPath, moduleId);
543
+ }
544
+
287
545
  instance.updateBindings(instance.context, instance.reference, instance.boundapi);
546
+ if (instance.config.debug) {
547
+ console.log(`[DEBUG] addApi: After updateBindings - api[${finalKey}]:`, Object.keys(instance.api[finalKey] || {}));
548
+ }
288
549
 
289
550
  if (instance.config.debug) {
290
551
  console.log(`[DEBUG] addApi: Successfully added modules at ${normalizedApiPath}`);
@@ -66,7 +66,8 @@ export async function buildCategoryStructure(categoryPath, options = {}) {
66
66
  maxDepth,
67
67
  mode,
68
68
  subdirHandler,
69
- instance
69
+ instance,
70
+ existingApi: options.existingApi
70
71
  });
71
72
 
72
73
 
@@ -320,6 +321,34 @@ export async function buildCategoryStructure(categoryPath, options = {}) {
320
321
  }
321
322
 
322
323
 
324
+
325
+ let defaultFunctions = [];
326
+ for (const moduleDecision of processedModules) {
327
+ const { mod, type } = moduleDecision;
328
+ if (type === "function" && typeof mod === "function") {
329
+ defaultFunctions.push({ mod, moduleDecision });
330
+ }
331
+ }
332
+
333
+
334
+
335
+
336
+ if (defaultFunctions.length === 1 && Object.keys(categoryModules).length === 1) {
337
+ const categoryDefaultFunction = defaultFunctions[0].mod;
338
+
339
+
340
+ if (categoryDefaultFunction.name !== categoryName) {
341
+ try {
342
+ Object.defineProperty(categoryDefaultFunction, "name", { value: categoryName, configurable: true });
343
+ } catch {
344
+
345
+ }
346
+ }
347
+
348
+ return categoryDefaultFunction;
349
+ }
350
+
351
+
323
352
  const keys = Object.keys(categoryModules);
324
353
  if (keys.length === 1) {
325
354
  const singleKey = keys[0];
@@ -416,7 +445,16 @@ export async function buildRootAPI(dir, options = {}) {
416
445
 
417
446
  if (lazy) {
418
447
 
419
- api[instance._toapiPathKey(entry.name)] = await instance._loadCategory(categoryPath, 0, maxDepth);
448
+ api[instance._toapiPathKey(entry.name)] = await buildCategoryStructure(categoryPath, {
449
+ currentDepth: 1,
450
+ maxDepth,
451
+ mode: "lazy",
452
+ subdirHandler: (ctx) => {
453
+
454
+ return instance._loadCategory(ctx.subDirPath, ctx.currentDepth, ctx.maxDepth);
455
+ },
456
+ instance
457
+ });
420
458
  } else {
421
459
 
422
460
  api[instance._toapiPathKey(entry.name)] = await buildCategoryStructure(categoryPath, {
@@ -321,10 +321,21 @@ export function processModuleForAPI(options) {
321
321
 
322
322
 
323
323
  for (const [key, value] of Object.entries(apiAssignments)) {
324
- if (debug && key && typeof value === "function" && value.name) {
325
- console.log(`[DEBUG] ${mode}: Assigning key '${key}' to function '${value.name}'`);
324
+ if (debug) {
325
+ console.log(`[DEBUG] ${mode}: About to assign key '${key}' - current api[${key}]:`, api[key] ? Object.keys(api[key]) : "undefined");
326
+ console.log(`[DEBUG] ${mode}: New value for key '${key}':`, typeof value === "object" && value ? Object.keys(value) : typeof value);
327
+ }
328
+
329
+
330
+ if (api[key] && typeof api[key] === "object" && typeof value === "object" && !Array.isArray(api[key]) && !Array.isArray(value)) {
331
+ if (debug) {
332
+ console.log(`[DEBUG] ${mode}: Merging objects for key '${key}' - existing:`, Object.keys(api[key]), "new:", Object.keys(value));
333
+ }
334
+
335
+ Object.assign(api[key], value);
336
+ } else {
337
+ api[key] = value;
326
338
  }
327
- api[key] = value;
328
339
  }
329
340
 
330
341
  return {
@@ -342,7 +353,7 @@ export function processModuleForAPI(options) {
342
353
 
343
354
 
344
355
  export async function buildCategoryDecisions(categoryPath, options = {}) {
345
- const { currentDepth = 0, maxDepth = Infinity, mode = "eager", subdirHandler } = options;
356
+ const { currentDepth = 0, maxDepth = Infinity, mode = "eager", subdirHandler, existingApi: _ } = options;
346
357
  const { instance } = options;
347
358
 
348
359
  if (!instance || typeof instance._toapiPathKey !== "function") {
@@ -55,37 +55,74 @@ function pickPrimaryBaseFile() {
55
55
  for (const cs of getStack(pickPrimaryBaseFile)) {
56
56
  const f = toFsPath(cs?.getFileName?.());
57
57
  if (!f) continue;
58
- if (f.startsWith?.("node:internal")) continue;
58
+
59
+ if (f.startsWith?.("node:")) continue;
59
60
  files.push(f);
60
61
  }
61
62
 
63
+ console.log(`[DEBUG_RESOLVE] pickPrimaryBaseFile - Total stack frames: ${files.length}`);
64
+ console.log(`[DEBUG_RESOLVE] ALL stack frames:`);
65
+ for (let i = 0; i < files.length; i++) {
66
+ console.log(`[DEBUG_RESOLVE] [${i}] ${files[i]}`);
67
+ }
68
+
62
69
  let iSloth = -1;
63
70
  for (let i = 0; i < files.length; i++) {
64
- if (path.basename(files[i]).toLowerCase() === "slothlet.mjs") iSloth = i;
71
+ if (path.basename(files[i]).toLowerCase() === "slothlet.mjs") {
72
+ iSloth = i;
73
+ }
65
74
  }
75
+
76
+ console.log(`[DEBUG_RESOLVE] Found slothlet.mjs at index: ${iSloth}, total files: ${files.length}`);
66
77
  if (iSloth !== -1) {
67
- const j = iSloth + 1;
68
- if (j < files.length) {
69
- const b = path.basename(files[j]).toLowerCase();
70
- if (/^index\.(mjs|cjs|js)$/.test(b) && j + 1 < files.length) return files[j + 1];
71
- return files[j];
78
+ console.log(`[DEBUG_RESOLVE] Files after slothlet.mjs:`);
79
+ for (let k = iSloth + 1; k < Math.min(files.length, iSloth + 5); k++) {
80
+ console.log(`[DEBUG_RESOLVE] [${k}] ${files[k]}`);
72
81
  }
73
82
  }
74
- return null;
83
+
84
+ if (iSloth !== -1) {
85
+
86
+ let j = iSloth + 1;
87
+
88
+
89
+ function isSlothletInternalFile(filePath) {
90
+ if (!filePath) return true;
91
+
92
+
93
+ if (filePath.includes(path.sep + "src" + path.sep + "lib" + path.sep)) return true;
94
+
95
+
96
+ if (path.basename(filePath).toLowerCase() === "slothlet.mjs") return true;
97
+
98
+
99
+ if (filePath.startsWith(THIS_DIR + path.sep)) return true;
100
+
101
+ return false;
75
102
  }
76
103
 
77
104
 
78
105
  function pickFallbackBaseFile() {
106
+ console.log("[DEBUG_RESOLVE] pickFallbackBaseFile called");
79
107
  for (const cs of getStack(pickFallbackBaseFile)) {
80
108
  const f = toFsPath(cs?.getFileName?.());
81
109
  if (!f) continue;
82
- if (f.startsWith?.("node:internal")) continue;
110
+
111
+ if (f.startsWith?.("node:")) continue;
83
112
  if (f === THIS_FILE) continue;
84
113
  if (f.startsWith(THIS_DIR + path.sep)) continue;
85
114
  if (path.basename(f).toLowerCase() === "slothlet.mjs") continue;
115
+ if (isSlothletInternalFile(f)) {
116
+ console.log(`[DEBUG_RESOLVE] Fallback skipping internal file: ${f}`);
117
+ continue;
118
+ }
119
+ console.log(`[DEBUG_RESOLVE] Fallback considering: ${f}`);
86
120
  return f;
87
121
  }
88
- return THIS_FILE;
122
+
123
+
124
+ console.log(`[DEBUG_RESOLVE] Fallback: No user code found in stack, using process.cwd(): ${process.cwd()}`);
125
+ return process.cwd();
89
126
  }
90
127
 
91
128
 
@@ -100,7 +100,35 @@ export async function create(dir, maxDepth = Infinity, currentDepth = 0) {
100
100
  for (const entry of entries) {
101
101
  if (entry.isDirectory() && !entry.name.startsWith(".") && currentDepth < maxDepth) {
102
102
  const categoryPath = path.join(dir, entry.name);
103
- api[this._toapiPathKey(entry.name)] = await this._loadCategory(categoryPath, currentDepth + 1, maxDepth);
103
+ const categoryKey = this._toapiPathKey(entry.name);
104
+ const categoryResult = await this._buildCategory(categoryPath, {
105
+ currentDepth: currentDepth + 1,
106
+ maxDepth,
107
+ mode: "eager",
108
+ existingApi: api
109
+ });
110
+
111
+
112
+ if (
113
+ api[categoryKey] &&
114
+ typeof api[categoryKey] === "object" &&
115
+ typeof categoryResult === "object" &&
116
+ !Array.isArray(api[categoryKey]) &&
117
+ !Array.isArray(categoryResult)
118
+ ) {
119
+ if (this.config.debug) {
120
+ console.log(
121
+ `[DEBUG] eager: Merging subdirectory '${categoryKey}' - existing:`,
122
+ Object.keys(api[categoryKey]),
123
+ "new:",
124
+ Object.keys(categoryResult)
125
+ );
126
+ }
127
+
128
+ Object.assign(api[categoryKey], categoryResult);
129
+ } else {
130
+ api[categoryKey] = categoryResult;
131
+ }
104
132
  }
105
133
  }
106
134