@soederpop/luca 0.0.6 → 0.0.8

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 (208) hide show
  1. package/CLAUDE.md +10 -1
  2. package/RUNME.md +56 -0
  3. package/bun.lock +1 -1
  4. package/commands/build-bootstrap.ts +78 -0
  5. package/commands/build-scaffolds.ts +24 -2
  6. package/commands/try-all-challenges.ts +543 -0
  7. package/commands/try-challenge.ts +100 -0
  8. package/docs/README.md +52 -80
  9. package/docs/TABLE-OF-CONTENTS.md +82 -51
  10. package/docs/apis/clients/elevenlabs.md +232 -8
  11. package/docs/apis/clients/graph.md +59 -8
  12. package/docs/apis/clients/openai.md +362 -2
  13. package/docs/apis/clients/rest.md +122 -2
  14. package/docs/apis/clients/websocket.md +71 -17
  15. package/docs/apis/features/agi/assistant.md +9 -3
  16. package/docs/apis/features/agi/assistants-manager.md +2 -2
  17. package/docs/apis/features/agi/claude-code.md +153 -14
  18. package/docs/apis/features/agi/conversation-history.md +15 -3
  19. package/docs/apis/features/agi/conversation.md +133 -20
  20. package/docs/apis/features/agi/openai-codex.md +90 -12
  21. package/docs/apis/features/agi/skills-library.md +23 -5
  22. package/docs/apis/features/node/container-link.md +59 -0
  23. package/docs/apis/features/node/content-db.md +1 -1
  24. package/docs/apis/features/node/disk-cache.md +1 -1
  25. package/docs/apis/features/node/dns.md +1 -0
  26. package/docs/apis/features/node/docker.md +2 -1
  27. package/docs/apis/features/node/esbuild.md +4 -3
  28. package/docs/apis/features/node/file-manager.md +13 -4
  29. package/docs/apis/features/node/fs.md +726 -171
  30. package/docs/apis/features/node/git.md +1 -0
  31. package/docs/apis/features/node/google-auth.md +23 -4
  32. package/docs/apis/features/node/google-calendar.md +14 -2
  33. package/docs/apis/features/node/google-docs.md +15 -2
  34. package/docs/apis/features/node/google-drive.md +21 -3
  35. package/docs/apis/features/node/google-sheets.md +14 -2
  36. package/docs/apis/features/node/grep.md +2 -0
  37. package/docs/apis/features/node/helpers.md +29 -0
  38. package/docs/apis/features/node/ink.md +2 -2
  39. package/docs/apis/features/node/networking.md +39 -4
  40. package/docs/apis/features/node/os.md +28 -0
  41. package/docs/apis/features/node/postgres.md +26 -4
  42. package/docs/apis/features/node/proc.md +37 -28
  43. package/docs/apis/features/node/process-manager.md +33 -5
  44. package/docs/apis/features/node/repl.md +1 -1
  45. package/docs/apis/features/node/runpod.md +1 -0
  46. package/docs/apis/features/node/secure-shell.md +7 -0
  47. package/docs/apis/features/node/semantic-search.md +12 -5
  48. package/docs/apis/features/node/sqlite.md +26 -4
  49. package/docs/apis/features/node/telegram.md +30 -5
  50. package/docs/apis/features/node/tts.md +17 -2
  51. package/docs/apis/features/node/ui.md +1 -1
  52. package/docs/apis/features/node/vault.md +4 -9
  53. package/docs/apis/features/node/vm.md +3 -12
  54. package/docs/apis/features/node/window-manager.md +128 -20
  55. package/docs/apis/features/web/asset-loader.md +13 -1
  56. package/docs/apis/features/web/container-link.md +59 -0
  57. package/docs/apis/features/web/esbuild.md +4 -3
  58. package/docs/apis/features/web/helpers.md +29 -0
  59. package/docs/apis/features/web/network.md +16 -2
  60. package/docs/apis/features/web/speech.md +16 -2
  61. package/docs/apis/features/web/vault.md +4 -9
  62. package/docs/apis/features/web/vm.md +3 -12
  63. package/docs/apis/features/web/voice.md +18 -1
  64. package/docs/apis/servers/express.md +18 -2
  65. package/docs/apis/servers/mcp.md +29 -4
  66. package/docs/apis/servers/websocket.md +34 -6
  67. package/docs/bootstrap/CLAUDE.md +100 -0
  68. package/docs/bootstrap/SKILL.md +222 -0
  69. package/docs/bootstrap/templates/about-command.ts +41 -0
  70. package/docs/bootstrap/templates/docs-models.ts +22 -0
  71. package/docs/bootstrap/templates/docs-readme.md +43 -0
  72. package/docs/bootstrap/templates/example-feature.ts +53 -0
  73. package/docs/bootstrap/templates/health-endpoint.ts +15 -0
  74. package/docs/bootstrap/templates/luca-cli.ts +25 -0
  75. package/docs/bootstrap/templates/runme.md +54 -0
  76. package/docs/challenges/caching-proxy.md +16 -0
  77. package/docs/challenges/content-db-round-trip.md +14 -0
  78. package/docs/challenges/custom-command.md +9 -0
  79. package/docs/challenges/file-watcher-pipeline.md +11 -0
  80. package/docs/challenges/grep-audit-report.md +15 -0
  81. package/docs/challenges/multi-feature-dashboard.md +14 -0
  82. package/docs/challenges/process-orchestrator.md +17 -0
  83. package/docs/challenges/rest-api-server-with-client.md +12 -0
  84. package/docs/challenges/script-runner-with-vm.md +11 -0
  85. package/docs/challenges/simple-rest-api.md +15 -0
  86. package/docs/challenges/websocket-serve-and-client.md +11 -0
  87. package/docs/challenges/yaml-config-system.md +14 -0
  88. package/docs/command-system-overhaul.md +94 -0
  89. package/docs/examples/assistant/CORE.md +18 -0
  90. package/docs/examples/assistant/hooks.ts +3 -0
  91. package/docs/examples/assistant/tools.ts +10 -0
  92. package/docs/examples/window-manager-layouts.md +180 -0
  93. package/docs/in-memory-fs.md +4 -0
  94. package/docs/models.ts +13 -10
  95. package/docs/philosophy.md +4 -3
  96. package/docs/reports/console-hmr-design.md +170 -0
  97. package/docs/reports/helper-semantic-search.md +72 -0
  98. package/docs/scaffolds/client.md +29 -20
  99. package/docs/scaffolds/command.md +64 -50
  100. package/docs/scaffolds/endpoint.md +31 -36
  101. package/docs/scaffolds/feature.md +28 -18
  102. package/docs/scaffolds/selector.md +91 -0
  103. package/docs/scaffolds/server.md +18 -9
  104. package/docs/selectors.md +115 -0
  105. package/docs/sessions/custom-command/attempt-log-2.md +195 -0
  106. package/docs/sessions/file-watcher-pipeline/attempt-log-1.md +728 -0
  107. package/docs/sessions/file-watcher-pipeline/attempt-log-2.md +555 -0
  108. package/docs/sessions/grep-audit-report/attempt-log-1.md +289 -0
  109. package/docs/sessions/multi-feature-dashboard/attempt-log-2.md +679 -0
  110. package/docs/sessions/rest-api-server-with-client/attempt-log-1.md +1 -0
  111. package/docs/sessions/rest-api-server-with-client/attempt-log-3.md +920 -0
  112. package/docs/sessions/simple-rest-api/attempt-log-1.md +593 -0
  113. package/docs/sessions/websocket-serve-and-client/attempt-log-2.md +995 -0
  114. package/docs/tutorials/00-bootstrap.md +148 -0
  115. package/docs/tutorials/07-endpoints.md +7 -7
  116. package/docs/tutorials/08-commands.md +153 -72
  117. package/luca.cli.ts +3 -0
  118. package/package.json +6 -5
  119. package/public/index.html +1430 -0
  120. package/scripts/examples/using-ollama.ts +2 -1
  121. package/scripts/update-introspection-data.ts +2 -2
  122. package/src/agi/endpoints/experts.ts +1 -1
  123. package/src/agi/features/assistant.ts +7 -0
  124. package/src/agi/features/assistants-manager.ts +5 -5
  125. package/src/agi/features/claude-code.ts +263 -3
  126. package/src/agi/features/conversation-history.ts +7 -1
  127. package/src/agi/features/conversation.ts +26 -3
  128. package/src/agi/features/openai-codex.ts +26 -2
  129. package/src/agi/features/openapi.ts +6 -1
  130. package/src/agi/features/skills-library.ts +9 -1
  131. package/src/bootstrap/generated.ts +595 -0
  132. package/src/cli/cli.ts +64 -21
  133. package/src/client.ts +23 -357
  134. package/src/clients/civitai/index.ts +1 -1
  135. package/src/clients/client-template.ts +1 -1
  136. package/src/clients/comfyui/index.ts +13 -2
  137. package/src/clients/elevenlabs/index.ts +2 -1
  138. package/src/clients/graph.ts +87 -0
  139. package/src/clients/openai/index.ts +10 -1
  140. package/src/clients/rest.ts +207 -0
  141. package/src/clients/websocket.ts +176 -0
  142. package/src/command.ts +281 -34
  143. package/src/commands/bootstrap.ts +185 -0
  144. package/src/commands/chat.ts +5 -4
  145. package/src/commands/describe.ts +341 -4
  146. package/src/commands/help.ts +35 -9
  147. package/src/commands/index.ts +3 -0
  148. package/src/commands/introspect.ts +92 -2
  149. package/src/commands/prompt.ts +5 -6
  150. package/src/commands/run.ts +75 -10
  151. package/src/commands/save-api-docs.ts +49 -0
  152. package/src/commands/scaffold.ts +169 -23
  153. package/src/commands/select.ts +94 -0
  154. package/src/commands/serve.ts +10 -1
  155. package/src/container.ts +15 -0
  156. package/src/endpoint.ts +19 -0
  157. package/src/graft.ts +181 -0
  158. package/src/introspection/generated.agi.ts +12458 -8968
  159. package/src/introspection/generated.node.ts +10573 -7145
  160. package/src/introspection/generated.web.ts +1 -1
  161. package/src/introspection/index.ts +26 -0
  162. package/src/node/container.ts +6 -7
  163. package/src/node/features/content-db.ts +49 -2
  164. package/src/node/features/disk-cache.ts +16 -9
  165. package/src/node/features/dns.ts +16 -3
  166. package/src/node/features/docker.ts +16 -4
  167. package/src/node/features/esbuild.ts +22 -2
  168. package/src/node/features/file-manager.ts +184 -29
  169. package/src/node/features/fs.ts +704 -248
  170. package/src/node/features/git.ts +21 -8
  171. package/src/node/features/grep.ts +23 -3
  172. package/src/node/features/helpers.ts +372 -43
  173. package/src/node/features/networking.ts +39 -4
  174. package/src/node/features/opener.ts +28 -15
  175. package/src/node/features/os.ts +76 -0
  176. package/src/node/features/port-exposer.ts +11 -1
  177. package/src/node/features/postgres.ts +17 -1
  178. package/src/node/features/proc.ts +4 -1
  179. package/src/node/features/python.ts +63 -14
  180. package/src/node/features/repl.ts +11 -7
  181. package/src/node/features/runpod.ts +16 -3
  182. package/src/node/features/secure-shell.ts +27 -2
  183. package/src/node/features/semantic-search.ts +12 -1
  184. package/src/node/features/ui.ts +5 -69
  185. package/src/node/features/vm.ts +17 -0
  186. package/src/node/features/window-manager.ts +68 -20
  187. package/src/node.ts +5 -0
  188. package/src/scaffolds/generated.ts +492 -290
  189. package/src/scaffolds/template.ts +9 -0
  190. package/src/schemas/base.ts +46 -5
  191. package/src/selector.ts +282 -0
  192. package/src/server.ts +11 -0
  193. package/src/servers/express.ts +27 -12
  194. package/src/servers/socket.ts +45 -11
  195. package/src/web/clients/socket.ts +4 -1
  196. package/src/web/container.ts +2 -1
  197. package/src/web/features/network.ts +7 -1
  198. package/src/web/features/voice-recognition.ts +16 -1
  199. package/test/clients-servers.test.ts +2 -1
  200. package/test/command.test.ts +267 -0
  201. package/test/vm-context.test.ts +146 -0
  202. package/test-integration/assistants-manager.test.ts +10 -20
  203. package/docs/apis/features/node/launcher-app-command-listener.md +0 -145
  204. package/docs/examples/launcher-app-command-listener.md +0 -120
  205. package/docs/tasks/web-container-helper-discovery.md +0 -71
  206. package/docs/todos.md +0 -1
  207. package/scripts/test-command-listener.ts +0 -123
  208. package/src/node/features/launcher-app-command-listener.ts +0 -389
@@ -2,7 +2,7 @@ import { z } from 'zod'
2
2
  import { commands } from '../command.js'
3
3
  import { CommandOptionsSchema } from '../schemas/base.js'
4
4
  import type { ContainerContext } from '../container.js'
5
- import type { IntrospectionSection } from '../introspection/index.js'
5
+ import type { IntrospectionSection, MethodIntrospection, GetterIntrospection } from '../introspection/index.js'
6
6
 
7
7
  declare module '../command.js' {
8
8
  interface AvailableCommands {
@@ -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. */
@@ -67,6 +67,7 @@ type ResolvedTarget =
67
67
  | { kind: 'container' }
68
68
  | { kind: 'registry'; name: RegistryName }
69
69
  | { kind: 'helper'; registry: RegistryName; id: string }
70
+ | { kind: 'member'; registry: RegistryName; id: string; member: string; memberType: 'method' | 'getter' }
70
71
 
71
72
  class DescribeError extends Error {
72
73
  constructor(message: string) {
@@ -75,6 +76,24 @@ class DescribeError extends Error {
75
76
  }
76
77
  }
77
78
 
79
+ /**
80
+ * Extract a short summary from a potentially long description string.
81
+ * Takes text up to the first markdown heading, bullet list, or code block,
82
+ * capped at ~300 chars on a sentence boundary.
83
+ */
84
+ function extractSummary(description: string): string {
85
+ // Strip from the first markdown heading/bullet/code block onward
86
+ const cut = description.search(/\s\*\*[A-Z][\w\s]+:\*\*|```|^\s*[-*]\s/m)
87
+ const text = cut > 0 ? description.slice(0, cut).trim() : description
88
+
89
+ if (text.length <= 300) return text
90
+
91
+ // Truncate on sentence boundary
92
+ const sentenceEnd = text.lastIndexOf('. ', 300)
93
+ if (sentenceEnd > 100) return text.slice(0, sentenceEnd + 1)
94
+ return text.slice(0, 300).trim() + '...'
95
+ }
96
+
78
97
  /**
79
98
  * Normalize an identifier to a comparable form by stripping file extensions,
80
99
  * converting kebab-case and snake_case to lowercase-no-separators.
@@ -99,9 +118,44 @@ function fuzzyFind(registry: any, input: string): string | undefined {
99
118
  return (registry.available as string[]).find((id: string) => normalize(id) === norm)
100
119
  }
101
120
 
121
+ /**
122
+ * Try to resolve "helperName.memberName" by searching all registries for the helper,
123
+ * then checking if memberName is a method or getter on it.
124
+ * Returns a 'member' target or null if no match.
125
+ */
126
+ function resolveHelperMember(helperName: string, memberName: string, container: any): ResolvedTarget | null {
127
+ for (const registryName of REGISTRY_NAMES) {
128
+ const reg = container[registryName]
129
+ if (!reg) continue
130
+ const found = fuzzyFind(reg, helperName)
131
+ if (!found) continue
132
+
133
+ const Ctor = reg.lookup(found)
134
+ const introspection = Ctor.introspect?.()
135
+ if (!introspection) continue
136
+
137
+ if (introspection.methods?.[memberName]) {
138
+ return { kind: 'member', registry: registryName, id: found, member: memberName, memberType: 'method' }
139
+ }
140
+ if (introspection.getters?.[memberName]) {
141
+ return { kind: 'member', registry: registryName, id: found, member: memberName, memberType: 'getter' }
142
+ }
143
+
144
+ // If we found the helper but not the member, give a helpful error
145
+ const allMembers = [
146
+ ...Object.keys(introspection.methods || {}).map((m: string) => m + '()'),
147
+ ...Object.keys(introspection.getters || {}),
148
+ ].sort()
149
+ throw new DescribeError(
150
+ `"${memberName}" is not a known method or getter on ${found}.\n\nAvailable members:\n ${allMembers.join(', ')}`
151
+ )
152
+ }
153
+ return null
154
+ }
155
+
102
156
  /**
103
157
  * Parse a single target string into a resolved target.
104
- * Accepts: "container", "features", "features.fs", "fs", etc.
158
+ * Accepts: "container", "features", "features.fs", "fs", "ui.banner", etc.
105
159
  */
106
160
  function resolveTarget(target: string, container: any): ResolvedTarget {
107
161
  const lower = target.toLowerCase()
@@ -135,6 +189,12 @@ function resolveTarget(target: string, container: any): ResolvedTarget {
135
189
  }
136
190
  return { kind: 'helper', registry, id: resolved }
137
191
  }
192
+
193
+ // Not a registry prefix — try "helper.member" (e.g. "ui.banner", "fs.readFile")
194
+ const helperName = prefix!
195
+ const memberName = rest.join('.')
196
+ const memberResult = resolveHelperMember(helperName, memberName, container)
197
+ if (memberResult) return memberResult
138
198
  }
139
199
 
140
200
  // Unqualified name: search all registries (fuzzy)
@@ -319,6 +379,77 @@ function getContainerData(container: any, sections: (IntrospectionSection | 'des
319
379
  }
320
380
  }
321
381
 
382
+ /**
383
+ * Walk the prototype chain of a base class to collect shared methods and getters.
384
+ * These are the methods/getters inherited by all helpers in a registry.
385
+ */
386
+ function collectSharedMembers(baseClass: any): { methods: string[]; getters: string[] } {
387
+ const methods: string[] = []
388
+ const getters: string[] = []
389
+
390
+ let proto = baseClass?.prototype
391
+ while (proto && proto.constructor.name !== 'Object') {
392
+ for (const k of Object.getOwnPropertyNames(proto)) {
393
+ if (k === 'constructor' || k.startsWith('_')) continue
394
+ const desc = Object.getOwnPropertyDescriptor(proto, k)
395
+ if (!desc) continue
396
+ if (desc.get && !getters.includes(k)) getters.push(k)
397
+ else if (typeof desc.value === 'function' && !methods.includes(k)) methods.push(k)
398
+ }
399
+ proto = Object.getPrototypeOf(proto)
400
+ }
401
+
402
+ return { methods: methods.sort(), getters: getters.sort() }
403
+ }
404
+
405
+ /**
406
+ * Find the intermediate parent class between a helper constructor and the registry's
407
+ * base class (e.g. RestClient between ElevenLabsClient and Client).
408
+ * Returns the intermediate class constructor, or null if the helper extends the base directly.
409
+ */
410
+ function findIntermediateParent(Ctor: any, baseClass: any): { name: string; methods: string[]; getters: string[] } | null {
411
+ if (!baseClass) return null
412
+
413
+ // Walk up from Ctor's parent to find the chain
414
+ let parent = Object.getPrototypeOf(Ctor)
415
+ if (!parent || parent === baseClass) return null
416
+
417
+ // Check if the parent itself is a direct child of baseClass (i.e., 2nd level)
418
+ // We want to find the class between Ctor and baseClass
419
+ const chain: any[] = []
420
+ let current = parent
421
+ while (current && current !== baseClass && current !== Function.prototype) {
422
+ chain.push(current)
423
+ current = Object.getPrototypeOf(current)
424
+ }
425
+
426
+ // If the chain is empty or parent IS the baseClass, no intermediate
427
+ if (chain.length === 0 || current !== baseClass) return null
428
+
429
+ // The first entry in the chain is the direct parent of Ctor — that's our intermediate
430
+ const intermediate = chain[0]
431
+ const methods: string[] = []
432
+ const getters: string[] = []
433
+
434
+ // Collect only the methods/getters defined directly on the intermediate class
435
+ const proto = intermediate?.prototype
436
+ if (proto) {
437
+ for (const k of Object.getOwnPropertyNames(proto)) {
438
+ if (k === 'constructor' || k.startsWith('_')) continue
439
+ const desc = Object.getOwnPropertyDescriptor(proto, k)
440
+ if (!desc) continue
441
+ if (desc.get && !getters.includes(k)) getters.push(k)
442
+ else if (typeof desc.value === 'function' && !methods.includes(k)) methods.push(k)
443
+ }
444
+ }
445
+
446
+ return {
447
+ name: intermediate.name,
448
+ methods: methods.sort(),
449
+ getters: getters.sort(),
450
+ }
451
+ }
452
+
322
453
  function getRegistryData(container: any, registryName: RegistryName, sections: (IntrospectionSection | 'description')[], noTitle = false, headingDepth = 1): { json: any; text: string } {
323
454
  const registry = container[registryName]
324
455
  const available: string[] = registry.available
@@ -327,6 +458,83 @@ function getRegistryData(container: any, registryName: RegistryName, sections: (
327
458
  return { json: {}, text: `No ${registryName} are registered.` }
328
459
  }
329
460
 
461
+ // When no section filters are specified, render a concise index (like describeAll)
462
+ // rather than full introspection for every single helper
463
+ if (sections.length === 0) {
464
+ const h = '#'.repeat(headingDepth)
465
+ const hSub = '#'.repeat(headingDepth + 1)
466
+ const jsonResult: Record<string, any> = {}
467
+ const textParts: string[] = [`${h} Available ${registryName} (${available.length})\n`]
468
+
469
+ // Show shared methods/getters from the base class at the top
470
+ const baseClass = registry.baseClass
471
+ if (baseClass) {
472
+ const shared = collectSharedMembers(baseClass)
473
+ const label = registryName === 'features' ? 'Feature'
474
+ : registryName === 'clients' ? 'Client'
475
+ : registryName === 'servers' ? 'Server'
476
+ : registryName[0]!.toUpperCase() + registryName.slice(1).replace(/s$/, '')
477
+
478
+ if (shared.getters.length) {
479
+ textParts.push(`**Shared ${label} Getters:** ${shared.getters.join(', ')}\n`)
480
+ }
481
+ if (shared.methods.length) {
482
+ textParts.push(`**Shared ${label} Methods:** ${shared.methods.map(m => m + '()').join(', ')}\n`)
483
+ }
484
+
485
+ jsonResult._shared = { methods: shared.methods, getters: shared.getters }
486
+ }
487
+
488
+ // Sort: core framework classes (direct children of baseClass) first, then the rest
489
+ const sorted = [...available].sort((a, b) => {
490
+ const aCtor = registry.lookup(a)
491
+ const bCtor = registry.lookup(b)
492
+ const aIsDirect = !findIntermediateParent(aCtor, baseClass)
493
+ const bIsDirect = !findIntermediateParent(bCtor, baseClass)
494
+ if (aIsDirect && !bIsDirect) return -1
495
+ if (!aIsDirect && bIsDirect) return 1
496
+ return 0
497
+ })
498
+
499
+ for (const id of sorted) {
500
+ const Ctor = registry.lookup(id)
501
+ const introspection = Ctor.introspect?.()
502
+ const description = introspection?.description || Ctor.description || 'No description provided'
503
+ // Take only the first 1-2 sentences as the summary
504
+ const summary = extractSummary(description)
505
+
506
+ const featureGetters = Object.keys(introspection?.getters || {}).sort()
507
+ const featureMethods = Object.keys(introspection?.methods || {}).sort()
508
+
509
+ // Detect intermediate parent class (e.g. RestClient between ElevenLabsClient and Client)
510
+ const intermediate = findIntermediateParent(Ctor, baseClass)
511
+
512
+ const entryJson: Record<string, any> = { description: summary, methods: featureMethods, getters: featureGetters }
513
+ if (intermediate) {
514
+ entryJson.extends = intermediate.name
515
+ entryJson.inheritedMethods = intermediate.methods
516
+ entryJson.inheritedGetters = intermediate.getters
517
+ }
518
+ jsonResult[id] = entryJson
519
+
520
+ const extendsLine = intermediate ? `\n> extends ${intermediate.name}\n` : ''
521
+
522
+ const memberLines: string[] = []
523
+ if (featureGetters.length) memberLines.push(` getters: ${featureGetters.join(', ')}`)
524
+ if (featureMethods.length) memberLines.push(` methods: ${featureMethods.map(m => m + '()').join(', ')}`)
525
+ if (intermediate) {
526
+ if (intermediate.getters.length) memberLines.push(` inherited getters: ${intermediate.getters.join(', ')}`)
527
+ if (intermediate.methods.length) memberLines.push(` inherited methods: ${intermediate.methods.map(m => m + '()').join(', ')}`)
528
+ }
529
+
530
+ const memberBlock = memberLines.length ? '\n' + memberLines.join('\n') + '\n' : ''
531
+ textParts.push(`${hSub} ${id}${extendsLine}\n${summary}\n${memberBlock}`)
532
+ }
533
+
534
+ return { json: jsonResult, text: textParts.join('\n') }
535
+ }
536
+
537
+ // When specific sections are requested, render full detail for each helper
330
538
  const jsonResult: Record<string, any> = {}
331
539
  const textParts: string[] = []
332
540
  for (const id of available) {
@@ -338,13 +546,140 @@ function getRegistryData(container: any, registryName: RegistryName, sections: (
338
546
  return { json: jsonResult, text: textParts.join('\n\n---\n\n') }
339
547
  }
340
548
 
549
+ /** Known top-level helper base class names — anything above these is "shared" */
550
+ const BASE_CLASS_NAMES = new Set(['Helper', 'Feature', 'Client', 'Server'])
551
+
552
+ /**
553
+ * Build a concise summary block for an individual helper listing its interface at a glance.
554
+ * Shows extends line if there's an intermediate parent, then own methods/getters,
555
+ * then inherited methods/getters from the intermediate parent.
556
+ */
557
+ function buildHelperSummary(Ctor: any): string {
558
+ const introspection = Ctor.introspect?.()
559
+ const ownMethods = Object.keys(introspection?.methods || {}).sort()
560
+ const ownGetters = Object.keys(introspection?.getters || {}).sort()
561
+
562
+ // Walk up the prototype chain to find an intermediate parent
563
+ const chain: any[] = []
564
+ let current = Object.getPrototypeOf(Ctor)
565
+ while (current && current.name && !BASE_CLASS_NAMES.has(current.name) && current !== Function.prototype) {
566
+ chain.push(current)
567
+ current = Object.getPrototypeOf(current)
568
+ }
569
+
570
+ const lines: string[] = []
571
+
572
+ if (chain.length > 0) {
573
+ lines.push(`> extends ${chain[0].name}`)
574
+ }
575
+
576
+ if (ownGetters.length) lines.push(`getters: ${ownGetters.join(', ')}`)
577
+ if (ownMethods.length) lines.push(`methods: ${ownMethods.map(m => m + '()').join(', ')}`)
578
+
579
+ // Collect inherited members from intermediate parent(s)
580
+ for (const parent of chain) {
581
+ const parentIntrospection = parent.introspect?.()
582
+ const inheritedMethods = Object.keys(parentIntrospection?.methods || {}).sort()
583
+ const inheritedGetters = Object.keys(parentIntrospection?.getters || {}).sort()
584
+ if (inheritedGetters.length) lines.push(`inherited getters (${parent.name}): ${inheritedGetters.join(', ')}`)
585
+ if (inheritedMethods.length) lines.push(`inherited methods (${parent.name}): ${inheritedMethods.map(m => m + '()').join(', ')}`)
586
+ }
587
+
588
+ return lines.join('\n')
589
+ }
590
+
591
+ function getMemberData(container: any, registryName: RegistryName, id: string, member: string, memberType: 'method' | 'getter', headingDepth = 1): { json: any; text: string } {
592
+ const registry = container[registryName]
593
+ const Ctor = registry.lookup(id)
594
+ const introspection = Ctor.introspect?.()
595
+ const h = '#'.repeat(headingDepth)
596
+ const hSub = '#'.repeat(headingDepth + 1)
597
+
598
+ if (memberType === 'method') {
599
+ const method = introspection?.methods?.[member] as MethodIntrospection | undefined
600
+ if (!method) return { json: {}, text: `No introspection data for ${id}.${member}()` }
601
+
602
+ const parts: string[] = []
603
+ parts.push(`${h} ${id}.${member}()`)
604
+ parts.push(`> method on **${introspection.className || id}**`)
605
+
606
+ if (method.description) parts.push(method.description)
607
+
608
+ // Parameters
609
+ const paramEntries = Object.entries(method.parameters || {})
610
+ if (paramEntries.length > 0) {
611
+ const paramLines = [`${hSub} Parameters`, '']
612
+ for (const [name, info] of paramEntries) {
613
+ const req = (method.required || []).includes(name) ? ' *(required)*' : ''
614
+ paramLines.push(`- **${name}** \`${info.type}\`${req}${info.description ? ' — ' + info.description : ''}`)
615
+ // Nested properties (e.g. options objects)
616
+ if (info.properties) {
617
+ for (const [propName, propInfo] of Object.entries(info.properties)) {
618
+ paramLines.push(` - **${propName}** \`${propInfo.type}\`${propInfo.description ? ' — ' + propInfo.description : ''}`)
619
+ }
620
+ }
621
+ }
622
+ parts.push(paramLines.join('\n'))
623
+ }
624
+
625
+ // Returns
626
+ if (method.returns && method.returns !== 'void') {
627
+ parts.push(`${hSub} Returns\n\n\`${method.returns}\``)
628
+ }
629
+
630
+ // Examples
631
+ if (method.examples?.length) {
632
+ parts.push(`${hSub} Examples`)
633
+ for (const ex of method.examples) {
634
+ parts.push(`\`\`\`${ex.language || 'typescript'}\n${ex.code}\n\`\`\``)
635
+ }
636
+ }
637
+
638
+ return { json: { [member]: method, _helper: id, _type: 'method' }, text: parts.join('\n\n') }
639
+ }
640
+
641
+ // Getter
642
+ const getter = introspection?.getters?.[member] as GetterIntrospection | undefined
643
+ if (!getter) return { json: {}, text: `No introspection data for ${id}.${member}` }
644
+
645
+ const parts: string[] = []
646
+ parts.push(`${h} ${id}.${member}`)
647
+ parts.push(`> getter on **${introspection.className || id}** — returns \`${getter.returns || 'unknown'}\``)
648
+
649
+ if (getter.description) parts.push(getter.description)
650
+
651
+ if (getter.examples?.length) {
652
+ parts.push(`${hSub} Examples`)
653
+ for (const ex of getter.examples) {
654
+ parts.push(`\`\`\`${ex.language || 'typescript'}\n${ex.code}\n\`\`\``)
655
+ }
656
+ }
657
+
658
+ return { json: { [member]: getter, _helper: id, _type: 'getter' }, text: parts.join('\n\n') }
659
+ }
660
+
341
661
  function getHelperData(container: any, registryName: RegistryName, id: string, sections: (IntrospectionSection | 'description')[], noTitle = false, headingDepth = 1): { json: any; text: string } {
342
662
  const registry = container[registryName]
343
663
  const Ctor = registry.lookup(id)
664
+ const text = renderHelperText(Ctor, sections, noTitle, headingDepth)
665
+
666
+ // Inject summary after the title + description block for full (no-section) renders
667
+ let finalText = text
668
+ if (sections.length === 0 && !noTitle) {
669
+ const summary = buildHelperSummary(Ctor)
670
+ if (summary) {
671
+ // Find the first ## heading and insert the summary before it
672
+ const sectionHeading = '#'.repeat(headingDepth + 1) + ' '
673
+ const idx = text.indexOf('\n' + sectionHeading)
674
+ if (idx >= 0) {
675
+ finalText = text.slice(0, idx) + '\n\n' + summary + '\n' + text.slice(idx)
676
+ }
677
+ }
678
+ }
344
679
 
345
680
  return {
346
681
  json: renderHelperJson(Ctor, sections, noTitle),
347
- text: renderHelperText(Ctor, sections, noTitle, headingDepth),
682
+ text: finalText,
348
683
  }
349
684
  }
350
685
 
@@ -405,6 +740,8 @@ export default async function describe(options: z.infer<typeof argsSchema>, cont
405
740
  return getRegistryData(container, item.name, sections, noTitle, headingDepth)
406
741
  case 'helper':
407
742
  return getHelperData(container, item.registry, item.id, sections, noTitle, headingDepth)
743
+ case 'member':
744
+ return getMemberData(container, item.registry, item.id, item.member, item.memberType, headingDepth)
408
745
  }
409
746
  }
410
747
 
@@ -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) {