@soederpop/luca 0.0.5 → 0.0.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 (211) hide show
  1. package/CLAUDE.md +10 -1
  2. package/bun.lock +1 -1
  3. package/commands/build-bootstrap.ts +78 -0
  4. package/commands/build-scaffolds.ts +24 -2
  5. package/commands/try-all-challenges.ts +543 -0
  6. package/commands/try-challenge.ts +100 -0
  7. package/docs/README.md +52 -80
  8. package/docs/TABLE-OF-CONTENTS.md +82 -51
  9. package/docs/apis/clients/elevenlabs.md +232 -8
  10. package/docs/apis/clients/graph.md +59 -8
  11. package/docs/apis/clients/openai.md +362 -2
  12. package/docs/apis/clients/rest.md +122 -2
  13. package/docs/apis/clients/websocket.md +71 -17
  14. package/docs/apis/features/agi/assistant.md +9 -3
  15. package/docs/apis/features/agi/assistants-manager.md +2 -2
  16. package/docs/apis/features/agi/claude-code.md +153 -14
  17. package/docs/apis/features/agi/conversation-history.md +15 -3
  18. package/docs/apis/features/agi/conversation.md +133 -20
  19. package/docs/apis/features/agi/openai-codex.md +90 -12
  20. package/docs/apis/features/agi/skills-library.md +23 -5
  21. package/docs/apis/features/node/container-link.md +59 -0
  22. package/docs/apis/features/node/content-db.md +1 -1
  23. package/docs/apis/features/node/disk-cache.md +1 -1
  24. package/docs/apis/features/node/dns.md +1 -0
  25. package/docs/apis/features/node/docker.md +2 -1
  26. package/docs/apis/features/node/esbuild.md +4 -3
  27. package/docs/apis/features/node/file-manager.md +13 -4
  28. package/docs/apis/features/node/fs.md +726 -171
  29. package/docs/apis/features/node/git.md +1 -0
  30. package/docs/apis/features/node/google-auth.md +23 -4
  31. package/docs/apis/features/node/google-calendar.md +14 -2
  32. package/docs/apis/features/node/google-docs.md +15 -2
  33. package/docs/apis/features/node/google-drive.md +21 -3
  34. package/docs/apis/features/node/google-sheets.md +14 -2
  35. package/docs/apis/features/node/grep.md +2 -0
  36. package/docs/apis/features/node/helpers.md +29 -0
  37. package/docs/apis/features/node/ink.md +2 -2
  38. package/docs/apis/features/node/networking.md +39 -4
  39. package/docs/apis/features/node/os.md +28 -0
  40. package/docs/apis/features/node/postgres.md +26 -4
  41. package/docs/apis/features/node/proc.md +37 -28
  42. package/docs/apis/features/node/process-manager.md +33 -5
  43. package/docs/apis/features/node/repl.md +1 -1
  44. package/docs/apis/features/node/runpod.md +1 -0
  45. package/docs/apis/features/node/secure-shell.md +7 -0
  46. package/docs/apis/features/node/semantic-search.md +12 -5
  47. package/docs/apis/features/node/sqlite.md +26 -4
  48. package/docs/apis/features/node/telegram.md +30 -5
  49. package/docs/apis/features/node/tts.md +17 -2
  50. package/docs/apis/features/node/ui.md +1 -1
  51. package/docs/apis/features/node/vault.md +4 -9
  52. package/docs/apis/features/node/vm.md +3 -12
  53. package/docs/apis/features/node/window-manager.md +128 -20
  54. package/docs/apis/features/web/asset-loader.md +13 -1
  55. package/docs/apis/features/web/container-link.md +59 -0
  56. package/docs/apis/features/web/esbuild.md +4 -3
  57. package/docs/apis/features/web/helpers.md +29 -0
  58. package/docs/apis/features/web/network.md +16 -2
  59. package/docs/apis/features/web/speech.md +16 -2
  60. package/docs/apis/features/web/vault.md +4 -9
  61. package/docs/apis/features/web/vm.md +3 -12
  62. package/docs/apis/features/web/voice.md +18 -1
  63. package/docs/apis/servers/express.md +18 -2
  64. package/docs/apis/servers/mcp.md +29 -4
  65. package/docs/apis/servers/websocket.md +34 -6
  66. package/docs/bootstrap/CLAUDE.md +100 -0
  67. package/docs/bootstrap/SKILL.md +222 -0
  68. package/docs/bootstrap/templates/about-command.ts +41 -0
  69. package/docs/bootstrap/templates/docs-models.ts +22 -0
  70. package/docs/bootstrap/templates/docs-readme.md +43 -0
  71. package/docs/bootstrap/templates/example-feature.ts +53 -0
  72. package/docs/bootstrap/templates/health-endpoint.ts +15 -0
  73. package/docs/bootstrap/templates/luca-cli.ts +25 -0
  74. package/docs/challenges/caching-proxy.md +16 -0
  75. package/docs/challenges/content-db-round-trip.md +14 -0
  76. package/docs/challenges/custom-command.md +9 -0
  77. package/docs/challenges/file-watcher-pipeline.md +11 -0
  78. package/docs/challenges/grep-audit-report.md +15 -0
  79. package/docs/challenges/multi-feature-dashboard.md +14 -0
  80. package/docs/challenges/process-orchestrator.md +17 -0
  81. package/docs/challenges/rest-api-server-with-client.md +12 -0
  82. package/docs/challenges/script-runner-with-vm.md +11 -0
  83. package/docs/challenges/simple-rest-api.md +15 -0
  84. package/docs/challenges/websocket-serve-and-client.md +11 -0
  85. package/docs/challenges/yaml-config-system.md +14 -0
  86. package/docs/command-system-overhaul.md +94 -0
  87. package/docs/examples/assistant/CORE.md +18 -0
  88. package/docs/examples/assistant/hooks.ts +3 -0
  89. package/docs/examples/assistant/tools.ts +10 -0
  90. package/docs/examples/window-manager-layouts.md +180 -0
  91. package/docs/in-memory-fs.md +4 -0
  92. package/docs/models.ts +13 -10
  93. package/docs/philosophy.md +4 -3
  94. package/docs/reports/console-hmr-design.md +170 -0
  95. package/docs/reports/helper-semantic-search.md +72 -0
  96. package/docs/scaffolds/client.md +29 -20
  97. package/docs/scaffolds/command.md +64 -50
  98. package/docs/scaffolds/endpoint.md +31 -36
  99. package/docs/scaffolds/feature.md +28 -18
  100. package/docs/scaffolds/selector.md +91 -0
  101. package/docs/scaffolds/server.md +18 -9
  102. package/docs/selectors.md +115 -0
  103. package/docs/sessions/custom-command/attempt-log-2.md +195 -0
  104. package/docs/sessions/file-watcher-pipeline/attempt-log-1.md +728 -0
  105. package/docs/sessions/file-watcher-pipeline/attempt-log-2.md +555 -0
  106. package/docs/sessions/grep-audit-report/attempt-log-1.md +289 -0
  107. package/docs/sessions/multi-feature-dashboard/attempt-log-2.md +679 -0
  108. package/docs/sessions/rest-api-server-with-client/attempt-log-1.md +1 -0
  109. package/docs/sessions/rest-api-server-with-client/attempt-log-3.md +920 -0
  110. package/docs/sessions/simple-rest-api/attempt-log-1.md +593 -0
  111. package/docs/sessions/websocket-serve-and-client/attempt-log-2.md +995 -0
  112. package/docs/tutorials/00-bootstrap.md +148 -0
  113. package/docs/tutorials/07-endpoints.md +7 -7
  114. package/docs/tutorials/08-commands.md +153 -72
  115. package/luca.cli.ts +3 -0
  116. package/package.json +6 -5
  117. package/public/index.html +1430 -0
  118. package/scripts/examples/using-ollama.ts +2 -1
  119. package/scripts/update-introspection-data.ts +2 -2
  120. package/src/agi/endpoints/experts.ts +1 -1
  121. package/src/agi/features/assistant.ts +7 -0
  122. package/src/agi/features/assistants-manager.ts +5 -5
  123. package/src/agi/features/claude-code.ts +263 -3
  124. package/src/agi/features/conversation-history.ts +7 -1
  125. package/src/agi/features/conversation.ts +26 -3
  126. package/src/agi/features/openai-codex.ts +26 -2
  127. package/src/agi/features/openapi.ts +6 -1
  128. package/src/agi/features/skills-library.ts +9 -1
  129. package/src/bootstrap/generated.ts +540 -0
  130. package/src/cli/cli.ts +64 -21
  131. package/src/client.ts +23 -357
  132. package/src/clients/civitai/index.ts +1 -1
  133. package/src/clients/client-template.ts +1 -1
  134. package/src/clients/comfyui/index.ts +13 -2
  135. package/src/clients/elevenlabs/index.ts +2 -1
  136. package/src/clients/graph.ts +87 -0
  137. package/src/clients/openai/index.ts +10 -1
  138. package/src/clients/rest.ts +207 -0
  139. package/src/clients/websocket.ts +176 -0
  140. package/src/command.ts +281 -34
  141. package/src/commands/bootstrap.ts +181 -0
  142. package/src/commands/chat.ts +5 -4
  143. package/src/commands/describe.ts +225 -2
  144. package/src/commands/help.ts +35 -9
  145. package/src/commands/index.ts +3 -0
  146. package/src/commands/introspect.ts +92 -2
  147. package/src/commands/prompt.ts +5 -6
  148. package/src/commands/run.ts +33 -10
  149. package/src/commands/save-api-docs.ts +49 -0
  150. package/src/commands/scaffold.ts +169 -23
  151. package/src/commands/select.ts +94 -0
  152. package/src/commands/serve.ts +10 -1
  153. package/src/container.ts +15 -0
  154. package/src/endpoint.ts +19 -0
  155. package/src/graft.ts +181 -0
  156. package/src/introspection/generated.agi.ts +12458 -8968
  157. package/src/introspection/generated.node.ts +10573 -7145
  158. package/src/introspection/generated.web.ts +1 -1
  159. package/src/introspection/index.ts +26 -0
  160. package/src/node/container.ts +6 -7
  161. package/src/node/features/content-db.ts +49 -2
  162. package/src/node/features/disk-cache.ts +16 -9
  163. package/src/node/features/dns.ts +16 -3
  164. package/src/node/features/docker.ts +16 -4
  165. package/src/node/features/esbuild.ts +20 -0
  166. package/src/node/features/file-manager.ts +184 -29
  167. package/src/node/features/fs.ts +704 -248
  168. package/src/node/features/git.ts +21 -8
  169. package/src/node/features/grep.ts +23 -3
  170. package/src/node/features/helpers.ts +372 -43
  171. package/src/node/features/networking.ts +39 -4
  172. package/src/node/features/opener.ts +28 -15
  173. package/src/node/features/os.ts +76 -0
  174. package/src/node/features/port-exposer.ts +11 -1
  175. package/src/node/features/postgres.ts +17 -1
  176. package/src/node/features/proc.ts +4 -1
  177. package/src/node/features/python.ts +63 -14
  178. package/src/node/features/repl.ts +11 -7
  179. package/src/node/features/runpod.ts +16 -3
  180. package/src/node/features/secure-shell.ts +27 -2
  181. package/src/node/features/semantic-search.ts +12 -1
  182. package/src/node/features/ui.ts +5 -69
  183. package/src/node/features/vm.ts +17 -0
  184. package/src/node/features/window-manager.ts +68 -20
  185. package/src/node.ts +5 -0
  186. package/src/scaffolds/generated.ts +492 -290
  187. package/src/scaffolds/template.ts +9 -0
  188. package/src/schemas/base.ts +46 -5
  189. package/src/selector.ts +282 -0
  190. package/src/server.ts +11 -0
  191. package/src/servers/express.ts +27 -12
  192. package/src/servers/socket.ts +45 -11
  193. package/src/web/clients/socket.ts +4 -1
  194. package/src/web/container.ts +2 -1
  195. package/src/web/features/network.ts +7 -1
  196. package/src/web/features/voice-recognition.ts +16 -1
  197. package/test/clients-servers.test.ts +2 -1
  198. package/test/command.test.ts +267 -0
  199. package/test-integration/assistants-manager.test.ts +10 -20
  200. package/tmp/.cache/luca-disk-cache/content-v2/sha512/1b/b5/c75b28794f00f94c4d609a98978e9420e9b7146d204a7fbf5b0b30477292581705d207c0100dabaac27eef540aaaece3374af75104a93219d4ec8bfb44e7 +1 -0
  201. package/tmp/.cache/luca-disk-cache/content-v2/sha512/da/df/1d90ce4e042abeb035a197832c6d6893420a747a056be773eb00e4f745a037d505c8db13dde7d36b36b6b893addbb7df0f5fe9f0c13e665f20056447318b +1 -0
  202. package/tmp/.cache/luca-disk-cache/content-v2/sha512/ed/04/e1d0c2a58c2db29b3921ca2affb3ea4febe831c53b38ebc21019fb799823aba6ed5b4611873d2cd25d422d49955b852a9c326da0d678899bc1c2c2960901 +1 -0
  203. package/tmp/.cache/luca-disk-cache/index-v5/00/13/572aa4c9a94f99eda999695d050cdd0ca7fe2d23a50af03234d4c8ce0791 +2 -0
  204. package/tmp/.cache/luca-disk-cache/index-v5/75/a9/cb61dc0f0589e8ec10a9aca27b834bc73884c479941042d22a2b22324cd3 +2 -0
  205. package/tmp/.cache/luca-disk-cache/index-v5/9f/0f/8b1f915ee64cfff7667dd96acd7a5ac0a96aa91a346e19cefd45909a9c9c +2 -0
  206. package/docs/apis/features/node/launcher-app-command-listener.md +0 -145
  207. package/docs/examples/launcher-app-command-listener.md +0 -120
  208. package/docs/tasks/web-container-helper-discovery.md +0 -71
  209. package/docs/todos.md +0 -1
  210. package/scripts/test-command-listener.ts +0 -123
  211. package/src/node/features/launcher-app-command-listener.ts +0 -389
@@ -10,7 +10,7 @@ declare module '../command.js' {
10
10
  }
11
11
  }
12
12
 
13
- const REGISTRY_NAMES = ['features', 'clients', 'servers', 'commands', 'endpoints'] as const
13
+ const REGISTRY_NAMES = ['features', 'clients', 'servers', 'commands', 'endpoints', 'selectors'] as const
14
14
  type RegistryName = (typeof REGISTRY_NAMES)[number]
15
15
 
16
16
  /** Maps flag names to the section they represent. 'description' is handled specially. */
@@ -75,6 +75,24 @@ class DescribeError extends Error {
75
75
  }
76
76
  }
77
77
 
78
+ /**
79
+ * Extract a short summary from a potentially long description string.
80
+ * Takes text up to the first markdown heading, bullet list, or code block,
81
+ * capped at ~300 chars on a sentence boundary.
82
+ */
83
+ function extractSummary(description: string): string {
84
+ // Strip from the first markdown heading/bullet/code block onward
85
+ const cut = description.search(/\s\*\*[A-Z][\w\s]+:\*\*|```|^\s*[-*]\s/m)
86
+ const text = cut > 0 ? description.slice(0, cut).trim() : description
87
+
88
+ if (text.length <= 300) return text
89
+
90
+ // Truncate on sentence boundary
91
+ const sentenceEnd = text.lastIndexOf('. ', 300)
92
+ if (sentenceEnd > 100) return text.slice(0, sentenceEnd + 1)
93
+ return text.slice(0, 300).trim() + '...'
94
+ }
95
+
78
96
  /**
79
97
  * Normalize an identifier to a comparable form by stripping file extensions,
80
98
  * converting kebab-case and snake_case to lowercase-no-separators.
@@ -319,6 +337,77 @@ function getContainerData(container: any, sections: (IntrospectionSection | 'des
319
337
  }
320
338
  }
321
339
 
340
+ /**
341
+ * Walk the prototype chain of a base class to collect shared methods and getters.
342
+ * These are the methods/getters inherited by all helpers in a registry.
343
+ */
344
+ function collectSharedMembers(baseClass: any): { methods: string[]; getters: string[] } {
345
+ const methods: string[] = []
346
+ const getters: string[] = []
347
+
348
+ let proto = baseClass?.prototype
349
+ while (proto && proto.constructor.name !== 'Object') {
350
+ for (const k of Object.getOwnPropertyNames(proto)) {
351
+ if (k === 'constructor' || k.startsWith('_')) continue
352
+ const desc = Object.getOwnPropertyDescriptor(proto, k)
353
+ if (!desc) continue
354
+ if (desc.get && !getters.includes(k)) getters.push(k)
355
+ else if (typeof desc.value === 'function' && !methods.includes(k)) methods.push(k)
356
+ }
357
+ proto = Object.getPrototypeOf(proto)
358
+ }
359
+
360
+ return { methods: methods.sort(), getters: getters.sort() }
361
+ }
362
+
363
+ /**
364
+ * Find the intermediate parent class between a helper constructor and the registry's
365
+ * base class (e.g. RestClient between ElevenLabsClient and Client).
366
+ * Returns the intermediate class constructor, or null if the helper extends the base directly.
367
+ */
368
+ function findIntermediateParent(Ctor: any, baseClass: any): { name: string; methods: string[]; getters: string[] } | null {
369
+ if (!baseClass) return null
370
+
371
+ // Walk up from Ctor's parent to find the chain
372
+ let parent = Object.getPrototypeOf(Ctor)
373
+ if (!parent || parent === baseClass) return null
374
+
375
+ // Check if the parent itself is a direct child of baseClass (i.e., 2nd level)
376
+ // We want to find the class between Ctor and baseClass
377
+ const chain: any[] = []
378
+ let current = parent
379
+ while (current && current !== baseClass && current !== Function.prototype) {
380
+ chain.push(current)
381
+ current = Object.getPrototypeOf(current)
382
+ }
383
+
384
+ // If the chain is empty or parent IS the baseClass, no intermediate
385
+ if (chain.length === 0 || current !== baseClass) return null
386
+
387
+ // The first entry in the chain is the direct parent of Ctor — that's our intermediate
388
+ const intermediate = chain[0]
389
+ const methods: string[] = []
390
+ const getters: string[] = []
391
+
392
+ // Collect only the methods/getters defined directly on the intermediate class
393
+ const proto = intermediate?.prototype
394
+ if (proto) {
395
+ for (const k of Object.getOwnPropertyNames(proto)) {
396
+ if (k === 'constructor' || k.startsWith('_')) continue
397
+ const desc = Object.getOwnPropertyDescriptor(proto, k)
398
+ if (!desc) continue
399
+ if (desc.get && !getters.includes(k)) getters.push(k)
400
+ else if (typeof desc.value === 'function' && !methods.includes(k)) methods.push(k)
401
+ }
402
+ }
403
+
404
+ return {
405
+ name: intermediate.name,
406
+ methods: methods.sort(),
407
+ getters: getters.sort(),
408
+ }
409
+ }
410
+
322
411
  function getRegistryData(container: any, registryName: RegistryName, sections: (IntrospectionSection | 'description')[], noTitle = false, headingDepth = 1): { json: any; text: string } {
323
412
  const registry = container[registryName]
324
413
  const available: string[] = registry.available
@@ -327,6 +416,83 @@ function getRegistryData(container: any, registryName: RegistryName, sections: (
327
416
  return { json: {}, text: `No ${registryName} are registered.` }
328
417
  }
329
418
 
419
+ // When no section filters are specified, render a concise index (like describeAll)
420
+ // rather than full introspection for every single helper
421
+ if (sections.length === 0) {
422
+ const h = '#'.repeat(headingDepth)
423
+ const hSub = '#'.repeat(headingDepth + 1)
424
+ const jsonResult: Record<string, any> = {}
425
+ const textParts: string[] = [`${h} Available ${registryName} (${available.length})\n`]
426
+
427
+ // Show shared methods/getters from the base class at the top
428
+ const baseClass = registry.baseClass
429
+ if (baseClass) {
430
+ const shared = collectSharedMembers(baseClass)
431
+ const label = registryName === 'features' ? 'Feature'
432
+ : registryName === 'clients' ? 'Client'
433
+ : registryName === 'servers' ? 'Server'
434
+ : registryName[0]!.toUpperCase() + registryName.slice(1).replace(/s$/, '')
435
+
436
+ if (shared.getters.length) {
437
+ textParts.push(`**Shared ${label} Getters:** ${shared.getters.join(', ')}\n`)
438
+ }
439
+ if (shared.methods.length) {
440
+ textParts.push(`**Shared ${label} Methods:** ${shared.methods.map(m => m + '()').join(', ')}\n`)
441
+ }
442
+
443
+ jsonResult._shared = { methods: shared.methods, getters: shared.getters }
444
+ }
445
+
446
+ // Sort: core framework classes (direct children of baseClass) first, then the rest
447
+ const sorted = [...available].sort((a, b) => {
448
+ const aCtor = registry.lookup(a)
449
+ const bCtor = registry.lookup(b)
450
+ const aIsDirect = !findIntermediateParent(aCtor, baseClass)
451
+ const bIsDirect = !findIntermediateParent(bCtor, baseClass)
452
+ if (aIsDirect && !bIsDirect) return -1
453
+ if (!aIsDirect && bIsDirect) return 1
454
+ return 0
455
+ })
456
+
457
+ for (const id of sorted) {
458
+ const Ctor = registry.lookup(id)
459
+ const introspection = Ctor.introspect?.()
460
+ const description = introspection?.description || Ctor.description || 'No description provided'
461
+ // Take only the first 1-2 sentences as the summary
462
+ const summary = extractSummary(description)
463
+
464
+ const featureGetters = Object.keys(introspection?.getters || {}).sort()
465
+ const featureMethods = Object.keys(introspection?.methods || {}).sort()
466
+
467
+ // Detect intermediate parent class (e.g. RestClient between ElevenLabsClient and Client)
468
+ const intermediate = findIntermediateParent(Ctor, baseClass)
469
+
470
+ const entryJson: Record<string, any> = { description: summary, methods: featureMethods, getters: featureGetters }
471
+ if (intermediate) {
472
+ entryJson.extends = intermediate.name
473
+ entryJson.inheritedMethods = intermediate.methods
474
+ entryJson.inheritedGetters = intermediate.getters
475
+ }
476
+ jsonResult[id] = entryJson
477
+
478
+ const extendsLine = intermediate ? `\n> extends ${intermediate.name}\n` : ''
479
+
480
+ const memberLines: string[] = []
481
+ if (featureGetters.length) memberLines.push(` getters: ${featureGetters.join(', ')}`)
482
+ if (featureMethods.length) memberLines.push(` methods: ${featureMethods.map(m => m + '()').join(', ')}`)
483
+ if (intermediate) {
484
+ if (intermediate.getters.length) memberLines.push(` inherited getters: ${intermediate.getters.join(', ')}`)
485
+ if (intermediate.methods.length) memberLines.push(` inherited methods: ${intermediate.methods.map(m => m + '()').join(', ')}`)
486
+ }
487
+
488
+ const memberBlock = memberLines.length ? '\n' + memberLines.join('\n') + '\n' : ''
489
+ textParts.push(`${hSub} ${id}${extendsLine}\n${summary}\n${memberBlock}`)
490
+ }
491
+
492
+ return { json: jsonResult, text: textParts.join('\n') }
493
+ }
494
+
495
+ // When specific sections are requested, render full detail for each helper
330
496
  const jsonResult: Record<string, any> = {}
331
497
  const textParts: string[] = []
332
498
  for (const id of available) {
@@ -338,13 +504,70 @@ function getRegistryData(container: any, registryName: RegistryName, sections: (
338
504
  return { json: jsonResult, text: textParts.join('\n\n---\n\n') }
339
505
  }
340
506
 
507
+ /** Known top-level helper base class names — anything above these is "shared" */
508
+ const BASE_CLASS_NAMES = new Set(['Helper', 'Feature', 'Client', 'Server'])
509
+
510
+ /**
511
+ * Build a concise summary block for an individual helper listing its interface at a glance.
512
+ * Shows extends line if there's an intermediate parent, then own methods/getters,
513
+ * then inherited methods/getters from the intermediate parent.
514
+ */
515
+ function buildHelperSummary(Ctor: any): string {
516
+ const introspection = Ctor.introspect?.()
517
+ const ownMethods = Object.keys(introspection?.methods || {}).sort()
518
+ const ownGetters = Object.keys(introspection?.getters || {}).sort()
519
+
520
+ // Walk up the prototype chain to find an intermediate parent
521
+ const chain: any[] = []
522
+ let current = Object.getPrototypeOf(Ctor)
523
+ while (current && current.name && !BASE_CLASS_NAMES.has(current.name) && current !== Function.prototype) {
524
+ chain.push(current)
525
+ current = Object.getPrototypeOf(current)
526
+ }
527
+
528
+ const lines: string[] = []
529
+
530
+ if (chain.length > 0) {
531
+ lines.push(`> extends ${chain[0].name}`)
532
+ }
533
+
534
+ if (ownGetters.length) lines.push(`getters: ${ownGetters.join(', ')}`)
535
+ if (ownMethods.length) lines.push(`methods: ${ownMethods.map(m => m + '()').join(', ')}`)
536
+
537
+ // Collect inherited members from intermediate parent(s)
538
+ for (const parent of chain) {
539
+ const parentIntrospection = parent.introspect?.()
540
+ const inheritedMethods = Object.keys(parentIntrospection?.methods || {}).sort()
541
+ const inheritedGetters = Object.keys(parentIntrospection?.getters || {}).sort()
542
+ if (inheritedGetters.length) lines.push(`inherited getters (${parent.name}): ${inheritedGetters.join(', ')}`)
543
+ if (inheritedMethods.length) lines.push(`inherited methods (${parent.name}): ${inheritedMethods.map(m => m + '()').join(', ')}`)
544
+ }
545
+
546
+ return lines.join('\n')
547
+ }
548
+
341
549
  function getHelperData(container: any, registryName: RegistryName, id: string, sections: (IntrospectionSection | 'description')[], noTitle = false, headingDepth = 1): { json: any; text: string } {
342
550
  const registry = container[registryName]
343
551
  const Ctor = registry.lookup(id)
552
+ const text = renderHelperText(Ctor, sections, noTitle, headingDepth)
553
+
554
+ // Inject summary after the title + description block for full (no-section) renders
555
+ let finalText = text
556
+ if (sections.length === 0 && !noTitle) {
557
+ const summary = buildHelperSummary(Ctor)
558
+ if (summary) {
559
+ // Find the first ## heading and insert the summary before it
560
+ const sectionHeading = '#'.repeat(headingDepth + 1) + ' '
561
+ const idx = text.indexOf('\n' + sectionHeading)
562
+ if (idx >= 0) {
563
+ finalText = text.slice(0, idx) + '\n\n' + summary + '\n' + text.slice(idx)
564
+ }
565
+ }
566
+ }
344
567
 
345
568
  return {
346
569
  json: renderHelperJson(Ctor, sections, noTitle),
347
- text: renderHelperText(Ctor, sections, noTitle, headingDepth),
570
+ text: finalText,
348
571
  }
349
572
  }
350
573
 
@@ -13,7 +13,7 @@ export const argsSchema = CommandOptionsSchema.extend({})
13
13
 
14
14
  /** Hidden option prefixes — legacy aliases that shouldn't clutter help output. */
15
15
  const HIDDEN_PREFIXES = ['only-']
16
- const HIDDEN_KEYS = new Set(['_', 'name', '_cacheKey'])
16
+ const HIDDEN_KEYS = new Set(['_', 'name', '_cacheKey', 'dispatchSource'])
17
17
 
18
18
  /**
19
19
  * Extract CLI option info from a Zod schema.
@@ -176,17 +176,43 @@ export default async function help(_options: z.infer<typeof argsSchema>, context
176
176
  console.log()
177
177
  console.log(c.white(' Usage: ') + c.cyan('luca') + c.dim(' <command|file> [options]'))
178
178
  console.log()
179
+ const allNames = (container.commands.available as string[]).filter((n: string) => n !== 'help')
180
+ const maxNameLen = Math.max(...allNames.map((n: string) => n.length)) + 2
181
+
182
+ const sources = (container as any)._commandSources as
183
+ | { builtinCommands: Set<string>; projectCommands: Set<string>; userCommands: Set<string> }
184
+ | undefined
185
+
186
+ const printCommands = (names: string[]) => {
187
+ for (const name of names) {
188
+ const Cmd = container.commands.lookup(name) as any
189
+ const desc = Cmd.commandDescription || ''
190
+ console.log(` ${c.cyan(name.padEnd(maxNameLen))} ${c.dim(desc)}`)
191
+ }
192
+ }
193
+
194
+ // Built-in commands
195
+ const builtinNames = sources
196
+ ? allNames.filter((n) => sources.builtinCommands.has(n))
197
+ : allNames
179
198
  console.log(c.white(' Commands:'))
180
199
  console.log()
200
+ printCommands(builtinNames)
201
+
202
+ // Project-local commands
203
+ if (sources && sources.projectCommands.size > 0) {
204
+ console.log()
205
+ console.log(c.white(' Project Commands') + c.dim(' (./commands/*)'))
206
+ console.log()
207
+ printCommands(allNames.filter((n) => sources.projectCommands.has(n)))
208
+ }
181
209
 
182
- // Dynamic padding based on longest command name
183
- const commandNames = (container.commands.available as string[]).filter((n: string) => n !== 'help')
184
- const maxNameLen = Math.max(...commandNames.map((n: string) => n.length)) + 2
185
-
186
- for (const name of commandNames) {
187
- const Cmd = container.commands.lookup(name) as any
188
- const desc = Cmd.commandDescription || ''
189
- console.log(` ${c.cyan(name.padEnd(maxNameLen))} ${c.dim(desc)}`)
210
+ // User-level commands
211
+ if (sources && sources.userCommands.size > 0) {
212
+ console.log()
213
+ console.log(c.white(' User Commands') + c.dim(' (~/.luca/commands/*)'))
214
+ console.log()
215
+ printCommands(allNames.filter((n) => sources.userCommands.has(n)))
190
216
  }
191
217
 
192
218
  console.log()
@@ -13,3 +13,6 @@ import './eval.js'
13
13
  import './help.js'
14
14
  import './scaffold.js'
15
15
  import './introspect.js'
16
+ import './save-api-docs.js'
17
+ import './bootstrap.js'
18
+ import './select.js'
@@ -15,8 +15,81 @@ export const argsSchema = CommandOptionsSchema.extend({
15
15
  output: z.string().optional().describe('Output file path (default: features/introspection.generated.ts)'),
16
16
  'dry-run': z.boolean().default(false).describe('Preview without writing'),
17
17
  'include-private': z.boolean().default(false).describe('Include private methods in output'),
18
+ lint: z.boolean().default(false).describe('Warn about undocumented getters, methods, options, and class descriptions'),
18
19
  })
19
20
 
21
+ function lintResults(results: any[]) {
22
+ type LintWarning = { helper: string; category: string; member: string; message: string }
23
+ const warnings: LintWarning[] = []
24
+
25
+ for (const result of results) {
26
+ const helper = result.className || result.id
27
+
28
+ // Class-level description
29
+ if (!result.description || result.description === `${result.className} helper`) {
30
+ warnings.push({ helper, category: 'class', member: '', message: 'Missing class description (add a JSDoc block above the class)' })
31
+ }
32
+
33
+ // Methods
34
+ for (const [name, method] of Object.entries(result.methods || {}) as [string, any][]) {
35
+ if (!method.description) {
36
+ warnings.push({ helper, category: 'method', member: name, message: 'Missing JSDoc description' })
37
+ }
38
+ // Check for undocumented parameters (generic fallback description)
39
+ for (const [paramName, param] of Object.entries(method.parameters || {}) as [string, any][]) {
40
+ if (param.description === `Parameter ${paramName}`) {
41
+ warnings.push({ helper, category: 'method', member: `${name}(@param ${paramName})`, message: 'Missing @param description' })
42
+ }
43
+ }
44
+ if (method.returns === 'void' || method.returns === 'any') {
45
+ // not necessarily a problem, skip
46
+ }
47
+ }
48
+
49
+ // Getters
50
+ for (const [name, getter] of Object.entries(result.getters || {}) as [string, any][]) {
51
+ if (!getter.description) {
52
+ warnings.push({ helper, category: 'getter', member: name, message: 'Missing JSDoc description' })
53
+ }
54
+ }
55
+
56
+ // Options (populated at runtime from Zod, but build-time scan may have empty options)
57
+ // We check if options exist and have empty descriptions
58
+ for (const [name, opt] of Object.entries(result.options || {}) as [string, any][]) {
59
+ if (!opt.description) {
60
+ warnings.push({ helper, category: 'option', member: name, message: 'Missing description (add .describe() to the Zod schema field)' })
61
+ }
62
+ }
63
+ }
64
+
65
+ return warnings
66
+ }
67
+
68
+ function printLintReport(warnings: ReturnType<typeof lintResults>, label?: string) {
69
+ if (warnings.length === 0) {
70
+ console.log(label ? ` ${label}: No lint warnings.` : 'No lint warnings.')
71
+ return 0
72
+ }
73
+
74
+ // Group by helper
75
+ const byHelper = new Map<string, typeof warnings>()
76
+ for (const w of warnings) {
77
+ if (!byHelper.has(w.helper)) byHelper.set(w.helper, [])
78
+ byHelper.get(w.helper)!.push(w)
79
+ }
80
+
81
+ console.log(label ? `\n ${label}: ${warnings.length} lint warning(s)` : `\n${warnings.length} lint warning(s)`)
82
+ for (const [helper, ws] of byHelper) {
83
+ console.log(`\n ${helper}:`)
84
+ for (const w of ws) {
85
+ const target = w.member ? `${w.category} ${w.member}` : w.category
86
+ console.log(` - [${target}] ${w.message}`)
87
+ }
88
+ }
89
+
90
+ return warnings.length
91
+ }
92
+
20
93
  async function introspect(options: z.infer<typeof argsSchema>, context: ContainerContext) {
21
94
  const container = context.container as any
22
95
  const { fs, paths } = container
@@ -33,7 +106,7 @@ async function introspect(options: z.infer<typeof argsSchema>, context: Containe
33
106
  const targets = [
34
107
  {
35
108
  name: 'node',
36
- src: ['src/node/features', 'src/servers', 'src/container.ts', 'src/node/container.ts'],
109
+ src: ['src/node/features', 'src/clients', 'src/servers', 'src/container.ts', 'src/node/container.ts'],
37
110
  outputPath: 'src/introspection/generated.node.ts',
38
111
  },
39
112
  {
@@ -43,11 +116,13 @@ async function introspect(options: z.infer<typeof argsSchema>, context: Containe
43
116
  },
44
117
  {
45
118
  name: 'agi',
46
- src: ['src/node/features', 'src/servers', 'src/agi/features', 'src/container.ts', 'src/node/container.ts', 'src/agi/container.server.ts'],
119
+ src: ['src/node/features', 'src/clients', 'src/servers', 'src/agi/features', 'src/container.ts', 'src/node/container.ts', 'src/agi/container.server.ts'],
47
120
  outputPath: 'src/introspection/generated.agi.ts',
48
121
  },
49
122
  ]
50
123
 
124
+ let totalWarnings = 0
125
+
51
126
  for (const target of targets) {
52
127
  console.log(`\nGenerating ${target.name} introspection data...`)
53
128
  console.log(` Sources: ${target.src.join(', ')}`)
@@ -72,9 +147,18 @@ async function introspect(options: z.infer<typeof argsSchema>, context: Containe
72
147
  } else {
73
148
  console.log(` Wrote ${target.outputPath}`)
74
149
  }
150
+
151
+ if (options.lint) {
152
+ const scanResults = scanner.state.get('scanResults') || []
153
+ const warnings = lintResults(scanResults)
154
+ totalWarnings += printLintReport(warnings, target.name)
155
+ }
75
156
  }
76
157
 
77
158
  console.log('\nAll introspection data generated.')
159
+ if (options.lint && totalWarnings > 0) {
160
+ console.log(`\nTotal: ${totalWarnings} lint warning(s) across all targets.`)
161
+ }
78
162
  return
79
163
  }
80
164
 
@@ -119,6 +203,12 @@ async function introspect(options: z.infer<typeof argsSchema>, context: Containe
119
203
  } else {
120
204
  console.log(`Wrote ${outputPath}`)
121
205
  }
206
+
207
+ if (options.lint) {
208
+ const scanResults = scanner.state.get('scanResults') || []
209
+ const warnings = lintResults(scanResults)
210
+ printLintReport(warnings)
211
+ }
122
212
  }
123
213
 
124
214
  commands.registerHandler('introspect', {
@@ -12,8 +12,7 @@ declare module '../command.js' {
12
12
 
13
13
  export const argsSchema = CommandOptionsSchema.extend({
14
14
  model: z.string().optional().describe('Override the LLM model (assistant mode only)'),
15
- folder: z.string().default('assistants').describe('Directory containing assistant definitions'),
16
- 'preserve-frontmatter': z.boolean().default(false).describe('Keep YAML frontmatter in the prompt instead of stripping it'),
15
+ 'preserve-frontmatter': z.boolean().default(false).describe('Keep YAML frontmatter in the prompt instead of stripping it'),
17
16
  'permission-mode': z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan']).default('acceptEdits').describe('Permission mode for CLI agents (default: acceptEdits)'),
18
17
  'in-folder': z.string().optional().describe('Run the CLI agent in this directory (resolved via container.paths)'),
19
18
  'out-file': z.string().optional().describe('Save session output as a markdown file'),
@@ -144,8 +143,8 @@ async function runClaudeOrCodex(target: 'claude' | 'codex', promptContent: strin
144
143
 
145
144
  async function runAssistant(name: string, promptContent: string, options: z.infer<typeof argsSchema>, container: any): Promise<RunStats> {
146
145
  const ui = container.feature('ui')
147
- const manager = container.feature('assistantsManager', { folder: options.folder })
148
- manager.discover()
146
+ const manager = container.feature('assistantsManager')
147
+ await manager.discover()
149
148
 
150
149
  const entry = manager.get(name)
151
150
  if (!entry) {
@@ -335,8 +334,8 @@ async function runParallel(
335
334
  })
336
335
  } else {
337
336
  // Assistant targets
338
- const manager = container.feature('assistantsManager', { folder: options.folder })
339
- manager.discover()
337
+ const manager = container.feature('assistantsManager')
338
+ await manager.discover()
340
339
 
341
340
  const entry = manager.get(target)
342
341
  if (!entry) {
@@ -13,6 +13,7 @@ export const argsSchema = CommandOptionsSchema.extend({
13
13
  safe: z.boolean().default(false).describe('Require approval before each code block (markdown mode)'),
14
14
  console: z.boolean().default(false).describe('Start an interactive REPL after executing a markdown file, with all accumulated context'),
15
15
  onlySections: z.string().optional().describe('Comma-separated list of section headings to run (case-insensitive, markdown only)'),
16
+ dontInjectContext: z.boolean().default(false).describe('Skip auto-injecting container context into scripts (run with plain bun instead)'),
16
17
  })
17
18
 
18
19
  function resolveScript(ref: string, context: ContainerContext): string | null {
@@ -87,6 +88,7 @@ async function runMarkdown(scriptPath: string, options: z.infer<typeof argsSchem
87
88
  setTimeout, clearTimeout, setInterval, clearInterval,
88
89
  fetch, URL, URLSearchParams,
89
90
  ...container.context,
91
+ $doc: doc
90
92
  })
91
93
 
92
94
  // ─── Parse and register ## Blocks section ──────────────────────────
@@ -173,20 +175,41 @@ async function runMarkdown(scriptPath: string, options: z.infer<typeof argsSchem
173
175
  return shared
174
176
  }
175
177
 
176
- async function runScript(scriptPath: string, context: ContainerContext) {
178
+ async function runScript(scriptPath: string, context: ContainerContext, options: { dontInjectContext?: boolean } = {}) {
177
179
  const container = context.container as any
178
180
 
179
- const { exitCode, stderr } = await container.proc.execAndCapture(`bun run ${scriptPath}`, {
180
- onOutput: (data: string) => process.stdout.write(data),
181
- onError: (data: string) => process.stderr.write(data),
182
- })
181
+ if (options.dontInjectContext) {
182
+ const { exitCode, stderr } = await container.proc.execAndCapture(`bun run ${scriptPath}`, {
183
+ onOutput: (data: string) => process.stdout.write(data),
184
+ onError: (data: string) => process.stderr.write(data),
185
+ })
186
+
187
+ if (exitCode === 0) return
183
188
 
184
- if (exitCode === 0) return
189
+ console.error(`\nScript failed with exit code ${exitCode}.\n`)
190
+ if (stderr.length) {
191
+ console.error(stderr)
192
+ }
193
+ return
194
+ }
185
195
 
186
- console.error(`\nScript failed with exit code ${exitCode}.\n`)
187
- if (stderr.length) {
188
- console.error(stderr)
196
+ const vm = container.feature('vm')
197
+ const esbuild = container.feature('esbuild')
198
+ const raw = container.fs.readFile(scriptPath)
199
+ const { code } = esbuild.transformSync(raw, { format: 'cjs' })
200
+
201
+ const ctx = {
202
+ require: vm.createRequireFor(scriptPath),
203
+ exports: {},
204
+ module: { exports: {} },
205
+ console,
206
+ setTimeout, setInterval, clearTimeout, clearInterval,
207
+ process, Buffer, URL, URLSearchParams,
208
+ fetch,
209
+ ...container.context,
189
210
  }
211
+
212
+ await vm.run(code, ctx)
190
213
  }
191
214
 
192
215
  async function diagnoseError(_scriptPath: string, error: Error, _context: ContainerContext) {
@@ -236,7 +259,7 @@ export default async function run(options: z.infer<typeof argsSchema>, context:
236
259
  })
237
260
  }
238
261
  } else {
239
- await runScript(scriptPath, context)
262
+ await runScript(scriptPath, context, { dontInjectContext: options.dontInjectContext })
240
263
  }
241
264
  } catch (err: any) {
242
265
  await diagnoseError(scriptPath, err instanceof Error ? err : new Error(String(err)), context)
@@ -0,0 +1,49 @@
1
+ import { z } from 'zod'
2
+ import { commands } from '../command.js'
3
+ import { CommandOptionsSchema } from '../schemas/base.js'
4
+ import type { ContainerContext } from '../container.js'
5
+
6
+ declare module '../command.js' {
7
+ interface AvailableCommands {
8
+ introspect: ReturnType<typeof commands.registerHandler>
9
+ }
10
+ }
11
+
12
+ export const argsSchema = CommandOptionsSchema.extend({
13
+ outputPath: z.string().default('docs/luca').describe('The path to save generated API docs to')
14
+ })
15
+
16
+ export async function apiDocs(options: z.infer<typeof argsSchema>, context: ContainerContext) {
17
+ const { container } = context
18
+ await container.helpers.discoverAll()
19
+ const outputFolder = options.outputPath ? container.paths.resolve(options.outputPath) : container.paths.resolve('docs','luca')
20
+
21
+ await container.fs.ensureFolder(
22
+ outputFolder
23
+ )
24
+
25
+ const mkPath = (...args) => container.paths.resolve(outputFolder, ...args)
26
+
27
+ const result = await container.fs.writeFileAsync(mkPath('agi-container.md'), container.inspectAsText())
28
+
29
+ for(let reg of ['features','clients','servers']) {
30
+ const helperIds = container[reg].available
31
+ const folder = mkPath(reg)
32
+ await container.fs.ensureFolder(folder)
33
+
34
+ await Promise.all(
35
+ helperIds.map((helperId) => container.fs.writeFileAsync(
36
+ container.paths.resolve(folder, `${helperId}.md`),
37
+ container[reg].describe(helperId)
38
+ ))
39
+ )
40
+ }
41
+
42
+ container.ui.print.green(`Finished saving API Docs`)
43
+ }
44
+
45
+ commands.registerHandler('api-docs', {
46
+ description: 'Save the helper introspection() content as markdown API docs in docs/luca',
47
+ argsSchema,
48
+ handler: apiDocs,
49
+ })