@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
@@ -1,6 +1,6 @@
1
1
  // Auto-generated scaffold and MCP readme content
2
- // Generated at: 2026-03-10T18:58:23.358Z
3
- // Source: docs/scaffolds/*.md and docs/mcp/readme.md
2
+ // Generated at: 2026-03-19T00:28:06.880Z
3
+ // Source: docs/scaffolds/*.md, docs/examples/assistant/, and docs/mcp/readme.md
4
4
  //
5
5
  // Do not edit manually. Run: luca build-scaffolds
6
6
 
@@ -20,8 +20,7 @@ export const scaffolds: Record<string, ScaffoldData> = {
20
20
  sections: [
21
21
  { heading: "Imports", code: `import { z } from 'zod'
22
22
  import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '@soederpop/luca'
23
- import { Feature, features } from '@soederpop/luca'
24
- import type { ContainerContext } from '@soederpop/luca'` },
23
+ import { Feature } from '@soederpop/luca'` },
25
24
  { heading: "Schemas", code: `export const {{PascalName}}StateSchema = FeatureStateSchema.extend({
26
25
  // Add your state fields here. These are observable — changes emit events.
27
26
  // Example: itemCount: z.number().default(0).describe('Number of items stored'),
@@ -40,8 +39,6 @@ export const {{PascalName}}EventsSchema = FeatureEventsSchema.extend({
40
39
  })` },
41
40
  { heading: "Class", code: `/**
42
41
  * {{description}}
43
- *
44
- * @example
45
42
  * \`\`\`typescript
46
43
  * const {{camelName}} = container.feature('{{camelName}}')
47
44
  * \`\`\`
@@ -53,25 +50,30 @@ export class {{PascalName}} extends Feature<{{PascalName}}State, {{PascalName}}O
53
50
  static override stateSchema = {{PascalName}}StateSchema
54
51
  static override optionsSchema = {{PascalName}}OptionsSchema
55
52
  static override eventsSchema = {{PascalName}}EventsSchema
56
- static override description = '{{description}}'
57
53
 
58
- constructor(options: {{PascalName}}Options, context: ContainerContext) {
59
- super(options, context)
60
- // Initialize state, set up resources
61
- }
54
+ static { Feature.register(this, '{{camelName}}') }
62
55
 
63
- // Add your methods here. Every public method needs JSDoc.
56
+ /**
57
+ * Called after the feature is initialized. Use this for any setup logic
58
+ * instead of overriding the constructor.
59
+ */
60
+ async afterInitialize() {
61
+ // Set up initial state, start background tasks, etc.
62
+ }
64
63
  }` },
65
64
  { heading: "Module Augmentation", code: `declare module '@soederpop/luca' {
66
65
  interface AvailableFeatures {
67
66
  {{camelName}}: typeof {{PascalName}}
68
67
  }
69
68
  }` },
70
- { heading: "Registration", code: `export default features.register('{{camelName}}', {{PascalName}})` },
69
+ { heading: "Registration", code: `// Inside the class:
70
+ static { Feature.register(this, '{{camelName}}') }
71
+
72
+ // At module level:
73
+ export default {{PascalName}}` },
71
74
  { heading: "Complete Example", code: `import { z } from 'zod'
72
75
  import { FeatureStateSchema, FeatureOptionsSchema } from '@soederpop/luca'
73
- import { Feature, features } from '@soederpop/luca'
74
- import type { ContainerContext } from '@soederpop/luca'
76
+ import { Feature } from '@soederpop/luca'
75
77
 
76
78
  declare module '@soederpop/luca' {
77
79
  interface AvailableFeatures {
@@ -99,19 +101,18 @@ export class {{PascalName}} extends Feature<{{PascalName}}State, {{PascalName}}O
99
101
  static override shortcut = 'features.{{camelName}}' as const
100
102
  static override stateSchema = {{PascalName}}StateSchema
101
103
  static override optionsSchema = {{PascalName}}OptionsSchema
102
- static override description = '{{description}}'
104
+ static { Feature.register(this, '{{camelName}}') }
103
105
 
104
- constructor(options: {{PascalName}}Options, context: ContainerContext) {
105
- super(options, context)
106
+ async afterInitialize() {
107
+ // Setup logic goes here — not in the constructor
106
108
  }
107
109
  }
108
110
 
109
- export default features.register('{{camelName}}', {{PascalName}})` }
111
+ export default {{PascalName}}` }
110
112
  ],
111
113
  full: `import { z } from 'zod'
112
114
  import { FeatureStateSchema, FeatureOptionsSchema } from '@soederpop/luca'
113
- import { Feature, features } from '@soederpop/luca'
114
- import type { ContainerContext } from '@soederpop/luca'
115
+ import { Feature } from '@soederpop/luca'
115
116
 
116
117
  declare module '@soederpop/luca' {
117
118
  interface AvailableFeatures {
@@ -139,14 +140,14 @@ export class {{PascalName}} extends Feature<{{PascalName}}State, {{PascalName}}O
139
140
  static override shortcut = 'features.{{camelName}}' as const
140
141
  static override stateSchema = {{PascalName}}StateSchema
141
142
  static override optionsSchema = {{PascalName}}OptionsSchema
142
- static override description = '{{description}}'
143
+ static { Feature.register(this, '{{camelName}}') }
143
144
 
144
- constructor(options: {{PascalName}}Options, context: ContainerContext) {
145
- super(options, context)
145
+ async afterInitialize() {
146
+ // Setup logic goes here — not in the constructor
146
147
  }
147
148
  }
148
149
 
149
- export default features.register('{{camelName}}', {{PascalName}})`,
150
+ export default {{PascalName}}`,
150
151
  tutorial: `# Building a Feature
151
152
 
152
153
  A feature is a container-managed capability — something your application needs that lives on the machine (file I/O, caching, encryption, etc). Features are lazy-loaded, observable, and self-documenting.
@@ -161,12 +162,15 @@ When to build a feature:
161
162
  \`\`\`ts
162
163
  import { z } from 'zod'
163
164
  import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '@soederpop/luca'
164
- import { Feature, features } from '@soederpop/luca'
165
- import type { ContainerContext } from '@soederpop/luca'
165
+ import { Feature } from '@soederpop/luca'
166
166
  \`\`\`
167
167
 
168
168
  These are the only imports your feature file needs from luca. If your feature wraps a third-party library, import it here too — feature implementations are the ONE place where direct library imports are allowed.
169
169
 
170
+ The use of dynamic imports is encouraged here, only import libraries you need when the feature is used, and only when necessary in the lifecycle of the feature if it can be done.
171
+
172
+ feature's have built in ways to check if their requirements are supported and can be enabled cautiously.
173
+
170
174
  ## Schemas
171
175
 
172
176
  Define the shape of your feature's state, options, and events using Zod. Every field must have a \`.describe()\` — this becomes the documentation.
@@ -194,11 +198,11 @@ export const {{PascalName}}EventsSchema = FeatureEventsSchema.extend({
194
198
 
195
199
  The class extends \`Feature\` with your state and options types. Static properties drive registration and introspection. Every public method needs a JSDoc block with \`@param\`, \`@returns\`, and \`@example\`.
196
200
 
201
+ Running \`luca introspect\` captures JSDoc blocks and Zod schemas and includes them in the description whenever somebody calls \`container.features.describe('{{camelName}}')\` or \`luca describe {{camelName}}\`.
202
+
197
203
  \`\`\`ts
198
204
  /**
199
205
  * {{description}}
200
- *
201
- * @example
202
206
  * \`\`\`typescript
203
207
  * const {{camelName}} = container.feature('{{camelName}}')
204
208
  * \`\`\`
@@ -210,17 +214,21 @@ export class {{PascalName}} extends Feature<{{PascalName}}State, {{PascalName}}O
210
214
  static override stateSchema = {{PascalName}}StateSchema
211
215
  static override optionsSchema = {{PascalName}}OptionsSchema
212
216
  static override eventsSchema = {{PascalName}}EventsSchema
213
- static override description = '{{description}}'
214
217
 
215
- constructor(options: {{PascalName}}Options, context: ContainerContext) {
216
- super(options, context)
217
- // Initialize state, set up resources
218
- }
218
+ static { Feature.register(this, '{{camelName}}') }
219
219
 
220
- // Add your methods here. Every public method needs JSDoc.
220
+ /**
221
+ * Called after the feature is initialized. Use this for any setup logic
222
+ * instead of overriding the constructor.
223
+ */
224
+ async afterInitialize() {
225
+ // Set up initial state, start background tasks, etc.
226
+ }
221
227
  }
222
228
  \`\`\`
223
229
 
230
+ **Important**: You almost never need to override the constructor. Use \`afterInitialize()\` for any setup logic — it runs after the feature is fully wired into the container and has access to \`this.container\`, \`this.options\`, \`this.state\`, etc.
231
+
224
232
  ## Module Augmentation
225
233
 
226
234
  This is what gives \`container.feature('yourName')\` TypeScript autocomplete. Without it, the feature works but TypeScript won't know about it.
@@ -235,10 +243,14 @@ declare module '@soederpop/luca' {
235
243
 
236
244
  ## Registration
237
245
 
238
- The last line of the file registers the feature in the global registry. This must happen at module level (not inside a function).
246
+ Registration happens inside the class body using a static block. The default export is just the class itself.
239
247
 
240
248
  \`\`\`ts
241
- export default features.register('{{camelName}}', {{PascalName}})
249
+ // Inside the class:
250
+ static { Feature.register(this, '{{camelName}}') }
251
+
252
+ // At module level:
253
+ export default {{PascalName}}
242
254
  \`\`\`
243
255
 
244
256
  ## Complete Example
@@ -248,8 +260,7 @@ Here's a minimal but complete feature. This is what a real feature file looks li
248
260
  \`\`\`ts
249
261
  import { z } from 'zod'
250
262
  import { FeatureStateSchema, FeatureOptionsSchema } from '@soederpop/luca'
251
- import { Feature, features } from '@soederpop/luca'
252
- import type { ContainerContext } from '@soederpop/luca'
263
+ import { Feature } from '@soederpop/luca'
253
264
 
254
265
  declare module '@soederpop/luca' {
255
266
  interface AvailableFeatures {
@@ -277,14 +288,14 @@ export class {{PascalName}} extends Feature<{{PascalName}}State, {{PascalName}}O
277
288
  static override shortcut = 'features.{{camelName}}' as const
278
289
  static override stateSchema = {{PascalName}}StateSchema
279
290
  static override optionsSchema = {{PascalName}}OptionsSchema
280
- static override description = '{{description}}'
291
+ static { Feature.register(this, '{{camelName}}') }
281
292
 
282
- constructor(options: {{PascalName}}Options, context: ContainerContext) {
283
- super(options, context)
293
+ async afterInitialize() {
294
+ // Setup logic goes here — not in the constructor
284
295
  }
285
296
  }
286
297
 
287
- export default features.register('{{camelName}}', {{PascalName}})
298
+ export default {{PascalName}}
288
299
  \`\`\`
289
300
 
290
301
  ## Conventions
@@ -300,9 +311,8 @@ export default features.register('{{camelName}}', {{PascalName}})
300
311
  client: {
301
312
  sections: [
302
313
  { heading: "Imports", code: `import { z } from 'zod'
303
- import { Client, clients, RestClient } from '@soederpop/luca/client'
304
- import { ClientStateSchema, ClientOptionsSchema, ClientEventsSchema } from '@soederpop/luca'
305
- import type { ContainerContext } from '@soederpop/luca'` },
314
+ import { Client, RestClient } from '@soederpop/luca/client'
315
+ import { ClientStateSchema, ClientOptionsSchema, ClientEventsSchema } from '@soederpop/luca'` },
306
316
  { heading: "Schemas", code: `export const {{PascalName}}StateSchema = ClientStateSchema.extend({
307
317
  // Add your state fields here.
308
318
  // Example: authenticated: z.boolean().default(false).describe('Whether API auth is configured'),
@@ -328,14 +338,14 @@ export class {{PascalName}} extends RestClient<{{PascalName}}State, {{PascalName
328
338
  static override shortcut = 'clients.{{camelName}}' as const
329
339
  static override stateSchema = {{PascalName}}StateSchema
330
340
  static override optionsSchema = {{PascalName}}OptionsSchema
331
- static override description = '{{description}}'
332
-
333
- constructor(options: {{PascalName}}Options, context: ContainerContext) {
334
- options = {
335
- ...options,
336
- baseURL: options.baseURL || 'https://api.example.com',
337
- }
338
- super(options, context)
341
+ static { Client.register(this, '{{camelName}}') }
342
+
343
+ /**
344
+ * Called after the client is initialized. Use this for any setup logic
345
+ * instead of overriding the constructor.
346
+ */
347
+ async afterInitialize() {
348
+ // Set up default headers, configure auth, etc.
339
349
  }
340
350
 
341
351
  // Add API methods here. Each wraps an endpoint.
@@ -349,11 +359,14 @@ export class {{PascalName}} extends RestClient<{{PascalName}}State, {{PascalName
349
359
  {{camelName}}: typeof {{PascalName}}
350
360
  }
351
361
  }` },
352
- { heading: "Registration", code: `export default clients.register('{{camelName}}', {{PascalName}})` },
362
+ { heading: "Registration", code: `// Inside the class:
363
+ static { Client.register(this, '{{camelName}}') }
364
+
365
+ // At module level:
366
+ export default {{PascalName}}` },
353
367
  { heading: "Complete Example", code: `import { z } from 'zod'
354
- import { clients, RestClient } from '@soederpop/luca/client'
368
+ import { Client, RestClient } from '@soederpop/luca/client'
355
369
  import { ClientStateSchema, ClientOptionsSchema } from '@soederpop/luca'
356
- import type { ContainerContext } from '@soederpop/luca'
357
370
 
358
371
  declare module '@soederpop/luca/client' {
359
372
  interface AvailableClients {
@@ -383,19 +396,18 @@ export class {{PascalName}} extends RestClient<{{PascalName}}State, {{PascalName
383
396
  static override shortcut = 'clients.{{camelName}}' as const
384
397
  static override stateSchema = {{PascalName}}StateSchema
385
398
  static override optionsSchema = {{PascalName}}OptionsSchema
386
- static override description = '{{description}}'
399
+ static { Client.register(this, '{{camelName}}') }
387
400
 
388
- constructor(options: {{PascalName}}Options, context: ContainerContext) {
389
- super({ ...options, baseURL: options.baseURL }, context)
401
+ async afterInitialize() {
402
+ // Setup logic goes here — not in the constructor
390
403
  }
391
404
  }
392
405
 
393
- export default clients.register('{{camelName}}', {{PascalName}})` }
406
+ export default {{PascalName}}` }
394
407
  ],
395
408
  full: `import { z } from 'zod'
396
- import { clients, RestClient } from '@soederpop/luca/client'
409
+ import { Client, RestClient } from '@soederpop/luca/client'
397
410
  import { ClientStateSchema, ClientOptionsSchema } from '@soederpop/luca'
398
- import type { ContainerContext } from '@soederpop/luca'
399
411
 
400
412
  declare module '@soederpop/luca/client' {
401
413
  interface AvailableClients {
@@ -425,14 +437,14 @@ export class {{PascalName}} extends RestClient<{{PascalName}}State, {{PascalName
425
437
  static override shortcut = 'clients.{{camelName}}' as const
426
438
  static override stateSchema = {{PascalName}}StateSchema
427
439
  static override optionsSchema = {{PascalName}}OptionsSchema
428
- static override description = '{{description}}'
440
+ static { Client.register(this, '{{camelName}}') }
429
441
 
430
- constructor(options: {{PascalName}}Options, context: ContainerContext) {
431
- super({ ...options, baseURL: options.baseURL }, context)
442
+ async afterInitialize() {
443
+ // Setup logic goes here — not in the constructor
432
444
  }
433
445
  }
434
446
 
435
- export default clients.register('{{camelName}}', {{PascalName}})`,
447
+ export default {{PascalName}}`,
436
448
  tutorial: `# Building a Client
437
449
 
438
450
  A client is a container-managed connection to an external service. Clients handle network communication — HTTP APIs, WebSocket connections, GraphQL endpoints. They extend \`RestClient\` (for HTTP), \`WebSocketClient\` (for WS), or the base \`Client\` class.
@@ -446,9 +458,8 @@ When to build a client:
446
458
 
447
459
  \`\`\`ts
448
460
  import { z } from 'zod'
449
- import { Client, clients, RestClient } from '@soederpop/luca/client'
461
+ import { Client, RestClient } from '@soederpop/luca/client'
450
462
  import { ClientStateSchema, ClientOptionsSchema, ClientEventsSchema } from '@soederpop/luca'
451
- import type { ContainerContext } from '@soederpop/luca'
452
463
  \`\`\`
453
464
 
454
465
  Use \`RestClient\` for HTTP APIs (most common). It gives you \`get\`, \`post\`, \`put\`, \`patch\`, \`delete\` methods that handle JSON, headers, and error wrapping.
@@ -471,6 +482,8 @@ export type {{PascalName}}Options = z.infer<typeof {{PascalName}}OptionsSchema>
471
482
 
472
483
  ## Class
473
484
 
485
+ Running \`luca introspect\` captures JSDoc blocks and Zod schemas and includes them in the description whenever somebody calls \`container.clients.describe('{{camelName}}')\` or \`luca describe {{camelName}}\`.
486
+
474
487
  \`\`\`ts
475
488
  /**
476
489
  * {{description}}
@@ -486,14 +499,14 @@ export class {{PascalName}} extends RestClient<{{PascalName}}State, {{PascalName
486
499
  static override shortcut = 'clients.{{camelName}}' as const
487
500
  static override stateSchema = {{PascalName}}StateSchema
488
501
  static override optionsSchema = {{PascalName}}OptionsSchema
489
- static override description = '{{description}}'
490
-
491
- constructor(options: {{PascalName}}Options, context: ContainerContext) {
492
- options = {
493
- ...options,
494
- baseURL: options.baseURL || 'https://api.example.com',
495
- }
496
- super(options, context)
502
+ static { Client.register(this, '{{camelName}}') }
503
+
504
+ /**
505
+ * Called after the client is initialized. Use this for any setup logic
506
+ * instead of overriding the constructor.
507
+ */
508
+ async afterInitialize() {
509
+ // Set up default headers, configure auth, etc.
497
510
  }
498
511
 
499
512
  // Add API methods here. Each wraps an endpoint.
@@ -504,6 +517,8 @@ export class {{PascalName}} extends RestClient<{{PascalName}}State, {{PascalName
504
517
  }
505
518
  \`\`\`
506
519
 
520
+ **Important**: You almost never need to override the constructor. Use \`afterInitialize()\` for setup logic — it runs after the client is fully wired into the container. Set \`baseURL\` via the options schema default instead of constructor manipulation.
521
+
507
522
  ## Module Augmentation
508
523
 
509
524
  \`\`\`ts
@@ -516,17 +531,22 @@ declare module '@soederpop/luca/client' {
516
531
 
517
532
  ## Registration
518
533
 
534
+ Registration happens inside the class body using a static block. The default export is just the class itself.
535
+
519
536
  \`\`\`ts
520
- export default clients.register('{{camelName}}', {{PascalName}})
537
+ // Inside the class:
538
+ static { Client.register(this, '{{camelName}}') }
539
+
540
+ // At module level:
541
+ export default {{PascalName}}
521
542
  \`\`\`
522
543
 
523
544
  ## Complete Example
524
545
 
525
546
  \`\`\`ts
526
547
  import { z } from 'zod'
527
- import { clients, RestClient } from '@soederpop/luca/client'
548
+ import { Client, RestClient } from '@soederpop/luca/client'
528
549
  import { ClientStateSchema, ClientOptionsSchema } from '@soederpop/luca'
529
- import type { ContainerContext } from '@soederpop/luca'
530
550
 
531
551
  declare module '@soederpop/luca/client' {
532
552
  interface AvailableClients {
@@ -556,31 +576,32 @@ export class {{PascalName}} extends RestClient<{{PascalName}}State, {{PascalName
556
576
  static override shortcut = 'clients.{{camelName}}' as const
557
577
  static override stateSchema = {{PascalName}}StateSchema
558
578
  static override optionsSchema = {{PascalName}}OptionsSchema
559
- static override description = '{{description}}'
579
+ static { Client.register(this, '{{camelName}}') }
560
580
 
561
- constructor(options: {{PascalName}}Options, context: ContainerContext) {
562
- super({ ...options, baseURL: options.baseURL }, context)
581
+ async afterInitialize() {
582
+ // Setup logic goes here — not in the constructor
563
583
  }
564
584
  }
565
585
 
566
- export default clients.register('{{camelName}}', {{PascalName}})
586
+ export default {{PascalName}}
567
587
  \`\`\`
568
588
 
569
589
  ## Conventions
570
590
 
571
591
  - **Extend RestClient for HTTP**: It gives you typed HTTP methods. Only use base \`Client\` if you need a non-HTTP protocol.
572
- - **Set baseURL in constructor**: Override options to hardcode or default the API base URL.
592
+ - **Set baseURL via options schema**: Use a Zod \`.default()\` on the \`baseURL\` field rather than overriding the constructor.
593
+ - **Use \`afterInitialize()\`**: For any setup logic (auth, default headers, etc.) instead of overriding the constructor.
573
594
  - **Wrap endpoints as methods**: Each API endpoint gets a method. Keep them thin — just map to HTTP calls.
574
- - **JSDoc everything**: Every public method needs \`@param\`, \`@returns\`, \`@example\`.
575
- - **Auth in options**: Pass API keys, tokens via options schema. Check them in the constructor or a setup method.
595
+ - **JSDoc everything**: Every public method needs \`@param\`, \`@returns\`, \`@example\`. Run \`luca introspect\` after changes to update generated docs.
596
+ - **Auth in options**: Pass API keys, tokens via options schema. Check them in \`afterInitialize()\` or a setup method.
576
597
  `,
577
598
  },
578
599
  server: {
579
600
  sections: [
580
601
  { heading: "Imports", code: `import { z } from 'zod'
581
- import { Server, servers } from '@soederpop/luca'
602
+ import { Server } from '@soederpop/luca'
582
603
  import { ServerStateSchema, ServerOptionsSchema, ServerEventsSchema } from '@soederpop/luca'
583
- import type { ContainerContext, NodeContainer } from '@soederpop/luca'
604
+ import type { NodeContainer } from '@soederpop/luca'
584
605
  import type { ServersInterface } from '@soederpop/luca'` },
585
606
  { heading: "Schemas", code: `export const {{PascalName}}StateSchema = ServerStateSchema.extend({
586
607
  // Add your state fields here.
@@ -614,7 +635,7 @@ export class {{PascalName}} extends Server<{{PascalName}}State, {{PascalName}}Op
614
635
  static override stateSchema = {{PascalName}}StateSchema
615
636
  static override optionsSchema = {{PascalName}}OptionsSchema
616
637
  static override eventsSchema = {{PascalName}}EventsSchema
617
- static override description = '{{description}}'
638
+ static { Server.register(this, '{{camelName}}') }
618
639
 
619
640
  static override attach(container: NodeContainer & ServersInterface) {
620
641
  return container
@@ -651,11 +672,15 @@ export class {{PascalName}} extends Server<{{PascalName}}State, {{PascalName}}Op
651
672
  {{camelName}}: typeof {{PascalName}}
652
673
  }
653
674
  }` },
654
- { heading: "Registration", code: `export default servers.register('{{camelName}}', {{PascalName}})` },
675
+ { heading: "Registration", code: `// Inside the class:
676
+ static { Server.register(this, '{{camelName}}') }
677
+
678
+ // At module level:
679
+ export default {{PascalName}}` },
655
680
  { heading: "Complete Example", code: `import { z } from 'zod'
656
- import { Server, servers } from '@soederpop/luca'
681
+ import { Server } from '@soederpop/luca'
657
682
  import { ServerStateSchema, ServerOptionsSchema, ServerEventsSchema } from '@soederpop/luca'
658
- import type { ContainerContext, NodeContainer } from '@soederpop/luca'
683
+ import type { NodeContainer } from '@soederpop/luca'
659
684
  import type { ServersInterface } from '@soederpop/luca'
660
685
 
661
686
  declare module '@soederpop/luca' {
@@ -688,7 +713,7 @@ export class {{PascalName}} extends Server<{{PascalName}}State, {{PascalName}}Op
688
713
  static override stateSchema = {{PascalName}}StateSchema
689
714
  static override optionsSchema = {{PascalName}}OptionsSchema
690
715
  static override eventsSchema = {{PascalName}}EventsSchema
691
- static override description = '{{description}}'
716
+ static { Server.register(this, '{{camelName}}') }
692
717
 
693
718
  static override attach(container: NodeContainer & ServersInterface) {
694
719
  return container
@@ -717,12 +742,12 @@ export class {{PascalName}} extends Server<{{PascalName}}State, {{PascalName}}Op
717
742
  }
718
743
  }
719
744
 
720
- export default servers.register('{{camelName}}', {{PascalName}})` }
745
+ export default {{PascalName}}` }
721
746
  ],
722
747
  full: `import { z } from 'zod'
723
- import { Server, servers } from '@soederpop/luca'
748
+ import { Server } from '@soederpop/luca'
724
749
  import { ServerStateSchema, ServerOptionsSchema, ServerEventsSchema } from '@soederpop/luca'
725
- import type { ContainerContext, NodeContainer } from '@soederpop/luca'
750
+ import type { NodeContainer } from '@soederpop/luca'
726
751
  import type { ServersInterface } from '@soederpop/luca'
727
752
 
728
753
  declare module '@soederpop/luca' {
@@ -755,7 +780,7 @@ export class {{PascalName}} extends Server<{{PascalName}}State, {{PascalName}}Op
755
780
  static override stateSchema = {{PascalName}}StateSchema
756
781
  static override optionsSchema = {{PascalName}}OptionsSchema
757
782
  static override eventsSchema = {{PascalName}}EventsSchema
758
- static override description = '{{description}}'
783
+ static { Server.register(this, '{{camelName}}') }
759
784
 
760
785
  static override attach(container: NodeContainer & ServersInterface) {
761
786
  return container
@@ -784,7 +809,7 @@ export class {{PascalName}} extends Server<{{PascalName}}State, {{PascalName}}Op
784
809
  }
785
810
  }
786
811
 
787
- export default servers.register('{{camelName}}', {{PascalName}})`,
812
+ export default {{PascalName}}`,
788
813
  tutorial: `# Building a Server
789
814
 
790
815
  A server is a container-managed listener — something that accepts connections and handles requests. Servers manage their own lifecycle (configure, start, stop) and expose observable state.
@@ -798,9 +823,9 @@ When to build a server:
798
823
 
799
824
  \`\`\`ts
800
825
  import { z } from 'zod'
801
- import { Server, servers } from '@soederpop/luca'
826
+ import { Server } from '@soederpop/luca'
802
827
  import { ServerStateSchema, ServerOptionsSchema, ServerEventsSchema } from '@soederpop/luca'
803
- import type { ContainerContext, NodeContainer } from '@soederpop/luca'
828
+ import type { NodeContainer } from '@soederpop/luca'
804
829
  import type { ServersInterface } from '@soederpop/luca'
805
830
  \`\`\`
806
831
 
@@ -827,6 +852,8 @@ export const {{PascalName}}EventsSchema = ServerEventsSchema.extend({
827
852
 
828
853
  ## Class
829
854
 
855
+ Running \`luca introspect\` captures JSDoc blocks and Zod schemas and includes them in the description whenever somebody calls \`container.servers.describe('{{camelName}}')\` or \`luca describe {{camelName}}\`.
856
+
830
857
  \`\`\`ts
831
858
  /**
832
859
  * {{description}}
@@ -844,7 +871,7 @@ export class {{PascalName}} extends Server<{{PascalName}}State, {{PascalName}}Op
844
871
  static override stateSchema = {{PascalName}}StateSchema
845
872
  static override optionsSchema = {{PascalName}}OptionsSchema
846
873
  static override eventsSchema = {{PascalName}}EventsSchema
847
- static override description = '{{description}}'
874
+ static { Server.register(this, '{{camelName}}') }
848
875
 
849
876
  static override attach(container: NodeContainer & ServersInterface) {
850
877
  return container
@@ -890,17 +917,23 @@ declare module '@soederpop/luca' {
890
917
 
891
918
  ## Registration
892
919
 
920
+ Registration happens inside the class body using a static block. The default export is just the class itself.
921
+
893
922
  \`\`\`ts
894
- export default servers.register('{{camelName}}', {{PascalName}})
923
+ // Inside the class:
924
+ static { Server.register(this, '{{camelName}}') }
925
+
926
+ // At module level:
927
+ export default {{PascalName}}
895
928
  \`\`\`
896
929
 
897
930
  ## Complete Example
898
931
 
899
932
  \`\`\`ts
900
933
  import { z } from 'zod'
901
- import { Server, servers } from '@soederpop/luca'
934
+ import { Server } from '@soederpop/luca'
902
935
  import { ServerStateSchema, ServerOptionsSchema, ServerEventsSchema } from '@soederpop/luca'
903
- import type { ContainerContext, NodeContainer } from '@soederpop/luca'
936
+ import type { NodeContainer } from '@soederpop/luca'
904
937
  import type { ServersInterface } from '@soederpop/luca'
905
938
 
906
939
  declare module '@soederpop/luca' {
@@ -933,7 +966,7 @@ export class {{PascalName}} extends Server<{{PascalName}}State, {{PascalName}}Op
933
966
  static override stateSchema = {{PascalName}}StateSchema
934
967
  static override optionsSchema = {{PascalName}}OptionsSchema
935
968
  static override eventsSchema = {{PascalName}}EventsSchema
936
- static override description = '{{description}}'
969
+ static { Server.register(this, '{{camelName}}') }
937
970
 
938
971
  static override attach(container: NodeContainer & ServersInterface) {
939
972
  return container
@@ -962,100 +995,101 @@ export class {{PascalName}} extends Server<{{PascalName}}State, {{PascalName}}Op
962
995
  }
963
996
  }
964
997
 
965
- export default servers.register('{{camelName}}', {{PascalName}})
998
+ export default {{PascalName}}
966
999
  \`\`\`
967
1000
 
968
1001
  ## Conventions
969
1002
 
970
1003
  - **Lifecycle**: Implement \`configure()\`, \`start()\`, and \`stop()\`. Check guards (\`isConfigured\`, \`isListening\`, \`isStopped\`) at the top of each.
1004
+ - **Use \`afterInitialize()\`**: For any setup logic instead of overriding the constructor. Lifecycle methods (\`configure\`, \`start\`, \`stop\`) handle the server's runtime phases.
971
1005
  - **State tracking**: Set \`configured\`, \`listening\`, \`stopped\`, and \`port\` on the state. This powers the introspection system.
972
1006
  - **attach() is static**: It runs when the container first loads the server class. Use it for container-level setup if needed.
973
1007
  - **Port from options**: Accept port via options schema and respect it in \`start()\`. Allow override via start options.
974
- - **JSDoc everything**: Every public method needs \`@param\`, \`@returns\`, \`@example\`.
1008
+ - **JSDoc everything**: Every public method needs \`@param\`, \`@returns\`, \`@example\`. Run \`luca introspect\` after changes to update generated docs.
975
1009
  `,
976
1010
  },
977
1011
  command: {
978
1012
  sections: [
979
1013
  { heading: "Imports", code: `import { z } from 'zod'
980
- import { commands, CommandOptionsSchema } from '@soederpop/luca'
981
1014
  import type { ContainerContext } from '@soederpop/luca'` },
982
- { heading: "Args Schema", code: `export const argsSchema = CommandOptionsSchema.extend({
983
- // Add your flags here. Each becomes a --flag on the CLI.
984
- // Example: verbose: z.boolean().default(false).describe('Enable verbose output'),
985
- // Example: output: z.string().optional().describe('Output file path'),
1015
+ { heading: "Positional Arguments", code: `// luca {{kebabName}} ./src => options.target === './src'
1016
+ export const positionals = ['target']` },
1017
+ { heading: "Args Schema", code: `export const argsSchema = z.object({
1018
+ // Positional: first arg after command name (via positionals array above)
1019
+ // target: z.string().optional().describe('The target to operate on'),
1020
+
1021
+ // Flags: passed as --flag on the CLI
1022
+ // verbose: z.boolean().default(false).describe('Enable verbose output'),
1023
+ // output: z.string().optional().describe('Output file path'),
986
1024
  })` },
1025
+ { heading: "Description", code: `export const description = '{{description}}'` },
987
1026
  { heading: "Handler", code: `export default async function {{camelName}}(options: z.infer<typeof argsSchema>, context: ContainerContext) {
988
- const container = context.container as any
1027
+ const { container } = context
989
1028
  const fs = container.feature('fs')
990
- const args = container.argv._ as string[]
991
1029
 
992
- // args[0] is your command name, args[1+] are positional arguments
993
- // options contains parsed --flags
1030
+ // options.target is set from the first positional arg (via positionals export)
1031
+ // options.verbose, options.output, etc. come from --flags
994
1032
 
995
1033
  // Your implementation here
996
- }` },
997
- { heading: "Registration", code: `commands.registerHandler('{{camelName}}', {
998
- description: '{{description}}',
999
- argsSchema,
1000
- handler: {{camelName}},
1001
- })` },
1002
- { heading: "Module Augmentation", code: `declare module '@soederpop/luca' {
1003
- interface AvailableCommands {
1004
- {{camelName}}: ReturnType<typeof commands.registerHandler>
1005
- }
1006
1034
  }` },
1007
1035
  { heading: "Complete Example", code: `import { z } from 'zod'
1008
- import { commands, CommandOptionsSchema } from '@soederpop/luca'
1009
1036
  import type { ContainerContext } from '@soederpop/luca'
1010
1037
 
1011
- declare module '@soederpop/luca' {
1012
- interface AvailableCommands {
1013
- {{camelName}}: ReturnType<typeof commands.registerHandler>
1014
- }
1015
- }
1038
+ export const description = '{{description}}'
1016
1039
 
1017
- export const argsSchema = CommandOptionsSchema.extend({})
1040
+ // Map positional args to named options: luca {{kebabName}} myTarget => options.target === 'myTarget'
1041
+ export const positionals = ['target']
1042
+
1043
+ export const argsSchema = z.object({
1044
+ target: z.string().optional().describe('The target to operate on'),
1045
+ })
1018
1046
 
1019
1047
  export default async function {{camelName}}(options: z.infer<typeof argsSchema>, context: ContainerContext) {
1020
- const container = context.container as any
1048
+ const { container } = context
1021
1049
  const fs = container.feature('fs')
1022
1050
 
1023
- console.log('{{camelName}} running...')
1024
- }
1051
+ console.log('{{kebabName}} running...', options.target)
1052
+ }` },
1053
+ { heading: "Container Properties", code: `export default async function {{camelName}}(options: z.infer<typeof argsSchema>, context: ContainerContext) {
1054
+ const { container } = context
1055
+
1056
+ // Current working directory
1057
+ container.cwd // '/path/to/project'
1058
+
1059
+ // Path utilities (scoped to cwd)
1060
+ container.paths.resolve('src') // '/path/to/project/src'
1061
+ container.paths.join('a', 'b') // '/path/to/project/a/b'
1062
+ container.paths.relative('src') // 'src'
1063
+
1064
+ // Package manifest (parsed package.json)
1065
+ container.manifest.name // 'my-project'
1066
+ container.manifest.version // '1.0.0'
1025
1067
 
1026
- commands.registerHandler('{{camelName}}', {
1027
- description: '{{description}}',
1028
- argsSchema,
1029
- handler: {{camelName}},
1030
- })` }
1068
+ // Raw CLI arguments (from minimist) — prefer positionals export for positional args
1069
+ container.argv // { _: ['{{kebabName}}', ...], verbose: true, ... }
1070
+ }` }
1031
1071
  ],
1032
1072
  full: `import { z } from 'zod'
1033
- import { commands, CommandOptionsSchema } from '@soederpop/luca'
1034
1073
  import type { ContainerContext } from '@soederpop/luca'
1035
1074
 
1036
- declare module '@soederpop/luca' {
1037
- interface AvailableCommands {
1038
- {{camelName}}: ReturnType<typeof commands.registerHandler>
1039
- }
1040
- }
1075
+ export const description = '{{description}}'
1076
+
1077
+ // Map positional args to named options: luca {{kebabName}} myTarget => options.target === 'myTarget'
1078
+ export const positionals = ['target']
1041
1079
 
1042
- export const argsSchema = CommandOptionsSchema.extend({})
1080
+ export const argsSchema = z.object({
1081
+ target: z.string().optional().describe('The target to operate on'),
1082
+ })
1043
1083
 
1044
1084
  export default async function {{camelName}}(options: z.infer<typeof argsSchema>, context: ContainerContext) {
1045
- const container = context.container as any
1085
+ const { container } = context
1046
1086
  const fs = container.feature('fs')
1047
1087
 
1048
- console.log('{{camelName}} running...')
1049
- }
1050
-
1051
- commands.registerHandler('{{camelName}}', {
1052
- description: '{{description}}',
1053
- argsSchema,
1054
- handler: {{camelName}},
1055
- })`,
1088
+ console.log('{{kebabName}} running...', options.target)
1089
+ }`,
1056
1090
  tutorial: `# Building a Command
1057
1091
 
1058
- A command extends the \`luca\` CLI. Commands live in a project's \`commands/\` folder and are automatically discovered. They receive parsed options and a container context.
1092
+ A command extends the \`luca\` CLI. Commands live in a project's \`commands/\` folder and are automatically discovered. They are Helper subclasses under the hood — the framework grafts your module exports into a Command class at runtime.
1059
1093
 
1060
1094
  When to build a command:
1061
1095
  - You need a CLI task for a project (build scripts, generators, automation)
@@ -1066,60 +1100,54 @@ When to build a command:
1066
1100
 
1067
1101
  \`\`\`ts
1068
1102
  import { z } from 'zod'
1069
- import { commands, CommandOptionsSchema } from '@soederpop/luca'
1070
1103
  import type { ContainerContext } from '@soederpop/luca'
1071
1104
  \`\`\`
1072
1105
 
1073
- ## Args Schema
1106
+ ## Positional Arguments
1074
1107
 
1075
- Define your command's arguments and flags. Extend \`CommandOptionsSchema\` which gives you \`_\` (positional args) and \`name\` for free.
1108
+ Export a \`positionals\` array to map CLI positional args into named options fields. The first positional (\`_[0]\`) is always the command name\`positionals\` maps \`_[1]\`, \`_[2]\`, etc.
1076
1109
 
1077
1110
  \`\`\`ts
1078
- export const argsSchema = CommandOptionsSchema.extend({
1079
- // Add your flags here. Each becomes a --flag on the CLI.
1080
- // Example: verbose: z.boolean().default(false).describe('Enable verbose output'),
1081
- // Example: output: z.string().optional().describe('Output file path'),
1082
- })
1111
+ // luca {{kebabName}} ./src => options.target === './src'
1112
+ export const positionals = ['target']
1083
1113
  \`\`\`
1084
1114
 
1085
- ## Handler
1115
+ ## Args Schema
1086
1116
 
1087
- The handler function receives parsed options and the container context. Use the container for all I/O.
1117
+ Define your command's arguments and flags with Zod. Each field becomes a \`--flag\` on the CLI. Fields named in \`positionals\` also accept positional args.
1088
1118
 
1089
1119
  \`\`\`ts
1090
- export default async function {{camelName}}(options: z.infer<typeof argsSchema>, context: ContainerContext) {
1091
- const container = context.container as any
1092
- const fs = container.feature('fs')
1093
- const args = container.argv._ as string[]
1120
+ export const argsSchema = z.object({
1121
+ // Positional: first arg after command name (via positionals array above)
1122
+ // target: z.string().optional().describe('The target to operate on'),
1094
1123
 
1095
- // args[0] is your command name, args[1+] are positional arguments
1096
- // options contains parsed --flags
1097
-
1098
- // Your implementation here
1099
- }
1124
+ // Flags: passed as --flag on the CLI
1125
+ // verbose: z.boolean().default(false).describe('Enable verbose output'),
1126
+ // output: z.string().optional().describe('Output file path'),
1127
+ })
1100
1128
  \`\`\`
1101
1129
 
1102
- ## Registration
1130
+ ## Description
1103
1131
 
1104
- Register the command at the bottom of the file. The \`description\` shows up in \`luca --help\`.
1132
+ Export a description string for \`luca --help\` display:
1105
1133
 
1106
1134
  \`\`\`ts
1107
- commands.registerHandler('{{camelName}}', {
1108
- description: '{{description}}',
1109
- argsSchema,
1110
- handler: {{camelName}},
1111
- })
1135
+ export const description = '{{description}}'
1112
1136
  \`\`\`
1113
1137
 
1114
- ## Module Augmentation
1138
+ ## Handler
1115
1139
 
1116
- Optional but gives TypeScript autocomplete for \`commands.lookup('yourCommand')\`.
1140
+ Export a default async function. It receives parsed options and the container context. Use the container for all I/O. Positional args declared in the \`positionals\` export are available as named fields on \`options\`.
1117
1141
 
1118
1142
  \`\`\`ts
1119
- declare module '@soederpop/luca' {
1120
- interface AvailableCommands {
1121
- {{camelName}}: ReturnType<typeof commands.registerHandler>
1122
- }
1143
+ export default async function {{camelName}}(options: z.infer<typeof argsSchema>, context: ContainerContext) {
1144
+ const { container } = context
1145
+ const fs = container.feature('fs')
1146
+
1147
+ // options.target is set from the first positional arg (via positionals export)
1148
+ // options.verbose, options.output, etc. come from --flags
1149
+
1150
+ // Your implementation here
1123
1151
  }
1124
1152
  \`\`\`
1125
1153
 
@@ -1127,58 +1155,78 @@ declare module '@soederpop/luca' {
1127
1155
 
1128
1156
  \`\`\`ts
1129
1157
  import { z } from 'zod'
1130
- import { commands, CommandOptionsSchema } from '@soederpop/luca'
1131
1158
  import type { ContainerContext } from '@soederpop/luca'
1132
1159
 
1133
- declare module '@soederpop/luca' {
1134
- interface AvailableCommands {
1135
- {{camelName}}: ReturnType<typeof commands.registerHandler>
1136
- }
1137
- }
1160
+ export const description = '{{description}}'
1138
1161
 
1139
- export const argsSchema = CommandOptionsSchema.extend({})
1162
+ // Map positional args to named options: luca {{kebabName}} myTarget => options.target === 'myTarget'
1163
+ export const positionals = ['target']
1164
+
1165
+ export const argsSchema = z.object({
1166
+ target: z.string().optional().describe('The target to operate on'),
1167
+ })
1140
1168
 
1141
1169
  export default async function {{camelName}}(options: z.infer<typeof argsSchema>, context: ContainerContext) {
1142
- const container = context.container as any
1170
+ const { container } = context
1143
1171
  const fs = container.feature('fs')
1144
1172
 
1145
- console.log('{{camelName}} running...')
1173
+ console.log('{{kebabName}} running...', options.target)
1146
1174
  }
1175
+ \`\`\`
1147
1176
 
1148
- commands.registerHandler('{{camelName}}', {
1149
- description: '{{description}}',
1150
- argsSchema,
1151
- handler: {{camelName}},
1152
- })
1177
+ ## Container Properties
1178
+
1179
+ The \`context.container\` object provides useful properties beyond features:
1180
+
1181
+ \`\`\`ts
1182
+ export default async function {{camelName}}(options: z.infer<typeof argsSchema>, context: ContainerContext) {
1183
+ const { container } = context
1184
+
1185
+ // Current working directory
1186
+ container.cwd // '/path/to/project'
1187
+
1188
+ // Path utilities (scoped to cwd)
1189
+ container.paths.resolve('src') // '/path/to/project/src'
1190
+ container.paths.join('a', 'b') // '/path/to/project/a/b'
1191
+ container.paths.relative('src') // 'src'
1192
+
1193
+ // Package manifest (parsed package.json)
1194
+ container.manifest.name // 'my-project'
1195
+ container.manifest.version // '1.0.0'
1196
+
1197
+ // Raw CLI arguments (from minimist) — prefer positionals export for positional args
1198
+ container.argv // { _: ['{{kebabName}}', ...], verbose: true, ... }
1199
+ }
1153
1200
  \`\`\`
1154
1201
 
1155
1202
  ## Conventions
1156
1203
 
1157
- - **File location**: \`commands/{{camelName}}.ts\` in the project root. The \`luca\` CLI discovers these automatically.
1158
- - **Naming**: camelCase for both file and registration ID. \`luca my-command\` maps to \`commands/my-command.ts\`.
1204
+ - **File location**: \`commands/{{kebabName}}.ts\` in the project root. The \`luca\` CLI discovers these automatically.
1205
+ - **Naming**: kebab-case for filename. \`luca {{kebabName}}\` maps to \`commands/{{kebabName}}.ts\`.
1159
1206
  - **Use the container**: Never import \`fs\`, \`path\`, \`child_process\` directly. Use \`container.feature('fs')\`, \`container.paths\`, \`container.feature('proc')\`.
1160
- - **Positional args**: Access via \`container.argv._\` — it's an array where \`_[0]\` is the command name.
1207
+ - **Positional args**: Export \`positionals = ['name1', 'name2']\` to map CLI positional args into named options fields. For raw access, use \`container.argv._\` where \`_[0]\` is the command name.
1161
1208
  - **Exit codes**: Return nothing for success. Throw for errors — the CLI catches and reports them.
1209
+ - **Help text**: Use \`.describe()\` on every schema field — it powers \`luca {{kebabName}} --help\`.
1162
1210
  `,
1163
1211
  },
1164
1212
  endpoint: {
1165
1213
  sections: [
1166
- { heading: "Imports", code: `import { z } from 'zod'
1167
- import type { EndpointContext } from '@soederpop/luca'` },
1168
1214
  { heading: "Required Exports", code: `export const path = '/api/{{camelName}}'
1169
1215
  export const description = '{{description}}'
1170
1216
  export const tags = ['{{camelName}}']` },
1171
- { heading: "Handler Functions", code: `export async function get(params: any, ctx: EndpointContext) {
1217
+ { heading: "Handler Functions", code: `export async function get(params: any, ctx: any) {
1172
1218
  const fs = ctx.container.feature('fs')
1173
1219
  // Your logic here
1174
1220
  return { message: 'ok' }
1175
1221
  }
1176
1222
 
1177
- export async function post(params: z.infer<typeof postSchema>, ctx: EndpointContext) {
1223
+ export async function post(params: any, ctx: any) {
1178
1224
  // Create something
1179
1225
  return { created: true }
1180
1226
  }` },
1181
- { heading: "Validation Schemas", code: `export const getSchema = z.object({
1227
+ { heading: "Validation Schemas", code: `import { z } from 'zod'
1228
+
1229
+ export const getSchema = z.object({
1182
1230
  q: z.string().optional().describe('Search query'),
1183
1231
  limit: z.number().default(20).describe('Max results'),
1184
1232
  })
@@ -1192,82 +1240,66 @@ export const rateLimit = { maxRequests: 100, windowSeconds: 60 }
1192
1240
 
1193
1241
  // Per-method rate limit
1194
1242
  export const postRateLimit = { maxRequests: 10, windowSeconds: 1 }` },
1195
- { heading: "Delete Handler", code: `const del = async (params: any, ctx: EndpointContext) => {
1243
+ { heading: "Delete Handler", code: `// Use a local name, then re-export as \`delete\`
1244
+ const del = async (params: any, ctx: any) => {
1196
1245
  return { deleted: true }
1197
1246
  }
1198
1247
  export { del as delete }` },
1199
- { heading: "Complete Example", code: `import { z } from 'zod'
1200
- import type { EndpointContext } from '@soederpop/luca'
1201
-
1202
- export const path = '/api/{{camelName}}'
1248
+ { heading: "Complete Example", code: `export const path = '/api/{{camelName}}'
1203
1249
  export const description = '{{description}}'
1204
1250
  export const tags = ['{{camelName}}']
1205
1251
 
1206
- export const getSchema = z.object({
1207
- q: z.string().optional().describe('Search query'),
1208
- })
1209
-
1210
- export async function get(params: z.infer<typeof getSchema>, ctx: EndpointContext) {
1252
+ export async function get(params: any, ctx: any) {
1211
1253
  return { items: [], total: 0 }
1212
1254
  }
1213
1255
 
1214
- export const postSchema = z.object({
1215
- name: z.string().min(1).describe('Item name'),
1216
- })
1217
-
1218
- export async function post(params: z.infer<typeof postSchema>, ctx: EndpointContext) {
1256
+ export async function post(params: any, ctx: any) {
1219
1257
  return { item: { id: '1', ...params }, message: 'Created' }
1220
- }` },
1221
- { heading: "Dynamic Route Example", code: `// endpoints/{{camelName}}/[id].ts
1222
- import { z } from 'zod'
1223
- import type { EndpointContext } from '@soederpop/luca'
1258
+ }
1224
1259
 
1260
+ const del = async (params: any, ctx: any) => {
1261
+ const { id } = ctx.params
1262
+ return { message: \`Deleted \${id}\` }
1263
+ }
1264
+ export { del as delete }` },
1265
+ { heading: "Dynamic Route Example", code: `// endpoints/{{camelName}}/[id].ts
1225
1266
  export const path = '/api/{{camelName}}/:id'
1226
1267
  export const description = 'Get, update, or delete a specific item'
1227
1268
  export const tags = ['{{camelName}}']
1228
1269
 
1229
- export async function get(params: any, ctx: EndpointContext) {
1270
+ export async function get(params: any, ctx: any) {
1230
1271
  const { id } = ctx.params
1231
1272
  return { item: { id } }
1232
1273
  }
1233
1274
 
1234
- export const putSchema = z.object({
1235
- name: z.string().min(1).optional().describe('Updated name'),
1236
- })
1237
-
1238
- export async function put(params: z.infer<typeof putSchema>, ctx: EndpointContext) {
1275
+ export async function put(params: any, ctx: any) {
1239
1276
  const { id } = ctx.params
1240
1277
  return { item: { id, ...params }, message: 'Updated' }
1241
1278
  }
1242
1279
 
1243
- const del = async (params: any, ctx: EndpointContext) => {
1280
+ const del = async (params: any, ctx: any) => {
1244
1281
  const { id } = ctx.params
1245
1282
  return { message: \`Deleted \${id}\` }
1246
1283
  }
1247
1284
  export { del as delete }` }
1248
1285
  ],
1249
- full: `import { z } from 'zod'
1250
- import type { EndpointContext } from '@soederpop/luca'
1251
-
1252
- export const path = '/api/{{camelName}}'
1286
+ full: `export const path = '/api/{{camelName}}'
1253
1287
  export const description = '{{description}}'
1254
1288
  export const tags = ['{{camelName}}']
1255
1289
 
1256
- export const getSchema = z.object({
1257
- q: z.string().optional().describe('Search query'),
1258
- })
1259
-
1260
- export async function get(params: z.infer<typeof getSchema>, ctx: EndpointContext) {
1290
+ export async function get(params: any, ctx: any) {
1261
1291
  return { items: [], total: 0 }
1262
1292
  }
1263
1293
 
1264
- export const postSchema = z.object({
1265
- name: z.string().min(1).describe('Item name'),
1266
- })
1267
-
1268
- export async function post(params: z.infer<typeof postSchema>, ctx: EndpointContext) {
1294
+ export async function post(params: any, ctx: any) {
1269
1295
  return { item: { id: '1', ...params }, message: 'Created' }
1270
- }`,
1296
+ }
1297
+
1298
+ const del = async (params: any, ctx: any) => {
1299
+ const { id } = ctx.params
1300
+ return { message: \`Deleted \${id}\` }
1301
+ }
1302
+ export { del as delete }`,
1271
1303
  tutorial: `# Building an Endpoint
1272
1304
 
1273
1305
  An endpoint is a route handler that \`luca serve\` auto-discovers and mounts on an Express server. Endpoints live in \`endpoints/\` and follow a file-based routing convention — each file becomes an API route with automatic validation, OpenAPI spec generation, and rate limiting.
@@ -1288,12 +1320,14 @@ Run \`luca serve\` and they're automatically discovered and mounted.
1288
1320
 
1289
1321
  ## Imports
1290
1322
 
1291
- \`\`\`ts
1292
- import { z } from 'zod'
1293
- import type { EndpointContext } from '@soederpop/luca'
1294
- \`\`\`
1323
+ Endpoints are lightweight — just exports and handler functions. No imports are required.
1324
+
1325
+ If your project has \`@soederpop/luca\` as an npm dependency, you can import \`z\` from \`zod\` and \`EndpointContext\` from \`@soederpop/luca\` for type safety. Otherwise, use \`any\` types — the framework handles validation and context injection for you.
1295
1326
 
1296
- That's it. Endpoints are lightweight just exports and functions.
1327
+ Access framework capabilities through the \`ctx\` parameter:
1328
+ - \`ctx.container.feature('fs')\` for file operations
1329
+ - \`ctx.container.feature('yaml')\` for YAML parsing
1330
+ - \`ctx.container.feature('sqlite')\` for database access
1297
1331
 
1298
1332
  ## Required Exports
1299
1333
 
@@ -1307,16 +1341,16 @@ export const tags = ['{{camelName}}']
1307
1341
 
1308
1342
  ## Handler Functions
1309
1343
 
1310
- Export named functions for each HTTP method you support. Each receives validated parameters and an \`EndpointContext\`:
1344
+ Export named functions for each HTTP method you support. Each receives validated parameters and a context object:
1311
1345
 
1312
1346
  \`\`\`ts
1313
- export async function get(params: any, ctx: EndpointContext) {
1347
+ export async function get(params: any, ctx: any) {
1314
1348
  const fs = ctx.container.feature('fs')
1315
1349
  // Your logic here
1316
1350
  return { message: 'ok' }
1317
1351
  }
1318
1352
 
1319
- export async function post(params: z.infer<typeof postSchema>, ctx: EndpointContext) {
1353
+ export async function post(params: any, ctx: any) {
1320
1354
  // Create something
1321
1355
  return { created: true }
1322
1356
  }
@@ -1334,9 +1368,11 @@ Return any object — it's automatically JSON-serialized as the response.
1334
1368
 
1335
1369
  ## Validation Schemas
1336
1370
 
1337
- Export Zod schemas to validate parameters for each method. Name them \`{method}Schema\`:
1371
+ If \`zod\` is available (via \`@soederpop/luca\` dependency or \`node_modules\`), export Zod schemas to validate parameters for each method. Name them \`{method}Schema\`:
1338
1372
 
1339
1373
  \`\`\`ts
1374
+ import { z } from 'zod'
1375
+
1340
1376
  export const getSchema = z.object({
1341
1377
  q: z.string().optional().describe('Search query'),
1342
1378
  limit: z.number().default(20).describe('Max results'),
@@ -1348,7 +1384,7 @@ export const postSchema = z.object({
1348
1384
  })
1349
1385
  \`\`\`
1350
1386
 
1351
- Invalid requests automatically return 400 with Zod error details. Schemas also feed the auto-generated OpenAPI spec.
1387
+ Invalid requests automatically return 400 with Zod error details. Schemas also feed the auto-generated OpenAPI spec. If zod is not available, skip schema exports — the endpoint still works, you just lose automatic validation.
1352
1388
 
1353
1389
  ## Rate Limiting
1354
1390
 
@@ -1364,42 +1400,40 @@ export const postRateLimit = { maxRequests: 10, windowSeconds: 1 }
1364
1400
 
1365
1401
  ## Delete Handler
1366
1402
 
1367
- \`delete\` is a reserved word in JS. Use an alias:
1403
+ \`delete\` is a reserved word in JS, so you can't use it as a function name directly. Use a named export alias:
1368
1404
 
1369
1405
  \`\`\`ts
1370
- const del = async (params: any, ctx: EndpointContext) => {
1406
+ // Use a local name, then re-export as \`delete\`
1407
+ const del = async (params: any, ctx: any) => {
1371
1408
  return { deleted: true }
1372
1409
  }
1373
1410
  export { del as delete }
1374
1411
  \`\`\`
1375
1412
 
1413
+ You can also export \`deleteSchema\` and \`deleteRateLimit\` for validation and rate limiting on DELETE.
1414
+
1376
1415
  ## Complete Example
1377
1416
 
1378
- A CRUD endpoint for a resource:
1417
+ A CRUD endpoint for a resource (no external imports needed):
1379
1418
 
1380
1419
  \`\`\`ts
1381
- import { z } from 'zod'
1382
- import type { EndpointContext } from '@soederpop/luca'
1383
-
1384
1420
  export const path = '/api/{{camelName}}'
1385
1421
  export const description = '{{description}}'
1386
1422
  export const tags = ['{{camelName}}']
1387
1423
 
1388
- export const getSchema = z.object({
1389
- q: z.string().optional().describe('Search query'),
1390
- })
1391
-
1392
- export async function get(params: z.infer<typeof getSchema>, ctx: EndpointContext) {
1424
+ export async function get(params: any, ctx: any) {
1393
1425
  return { items: [], total: 0 }
1394
1426
  }
1395
1427
 
1396
- export const postSchema = z.object({
1397
- name: z.string().min(1).describe('Item name'),
1398
- })
1399
-
1400
- export async function post(params: z.infer<typeof postSchema>, ctx: EndpointContext) {
1428
+ export async function post(params: any, ctx: any) {
1401
1429
  return { item: { id: '1', ...params }, message: 'Created' }
1402
1430
  }
1431
+
1432
+ const del = async (params: any, ctx: any) => {
1433
+ const { id } = ctx.params
1434
+ return { message: \`Deleted \${id}\` }
1435
+ }
1436
+ export { del as delete }
1403
1437
  \`\`\`
1404
1438
 
1405
1439
  ## Dynamic Route Example
@@ -1408,28 +1442,21 @@ For routes with URL parameters, create a nested file:
1408
1442
 
1409
1443
  \`\`\`ts
1410
1444
  // endpoints/{{camelName}}/[id].ts
1411
- import { z } from 'zod'
1412
- import type { EndpointContext } from '@soederpop/luca'
1413
-
1414
1445
  export const path = '/api/{{camelName}}/:id'
1415
1446
  export const description = 'Get, update, or delete a specific item'
1416
1447
  export const tags = ['{{camelName}}']
1417
1448
 
1418
- export async function get(params: any, ctx: EndpointContext) {
1449
+ export async function get(params: any, ctx: any) {
1419
1450
  const { id } = ctx.params
1420
1451
  return { item: { id } }
1421
1452
  }
1422
1453
 
1423
- export const putSchema = z.object({
1424
- name: z.string().min(1).optional().describe('Updated name'),
1425
- })
1426
-
1427
- export async function put(params: z.infer<typeof putSchema>, ctx: EndpointContext) {
1454
+ export async function put(params: any, ctx: any) {
1428
1455
  const { id } = ctx.params
1429
1456
  return { item: { id, ...params }, message: 'Updated' }
1430
1457
  }
1431
1458
 
1432
- const del = async (params: any, ctx: EndpointContext) => {
1459
+ const del = async (params: any, ctx: any) => {
1433
1460
  const { id } = ctx.params
1434
1461
  return { message: \`Deleted \${id}\` }
1435
1462
  }
@@ -1444,10 +1471,185 @@ export { del as delete }
1444
1471
  - **Use the container**: Access features via \`ctx.container.feature('fs')\`, not Node.js imports.
1445
1472
  - **Return objects**: Handler return values are JSON-serialized. Use \`ctx.response\` only for streaming or custom status codes.
1446
1473
  - **OpenAPI for free**: Your \`path\`, \`description\`, \`tags\`, and schemas automatically generate an OpenAPI spec at \`/openapi.json\`.
1474
+ `,
1475
+ },
1476
+ selector: {
1477
+ sections: [
1478
+ { heading: "Imports", code: `import { z } from 'zod'
1479
+ import type { ContainerContext } from '@soederpop/luca'` },
1480
+ { heading: "Args Schema", code: `export const argsSchema = z.object({
1481
+ // Add your input arguments here.
1482
+ // Example: field: z.string().optional().describe('Specific field to return'),
1483
+ })` },
1484
+ { heading: "Description", code: `export const description = '{{description}}'` },
1485
+ { heading: "Caching", code: `export function cacheKey(args: z.infer<typeof argsSchema>, context: ContainerContext) {
1486
+ return context.container.git.currentCommitSha
1487
+ }` },
1488
+ { heading: "Caching", code: `export const cacheable = false` },
1489
+ { heading: "Handler", code: `export async function run(args: z.infer<typeof argsSchema>, context: ContainerContext) {
1490
+ const { container } = context
1491
+ // Query and return your data
1492
+ return { /* your data */ }
1493
+ }` },
1494
+ { heading: "Complete Example", code: `import { z } from 'zod'
1495
+ import type { ContainerContext } from '@soederpop/luca'
1496
+
1497
+ export const description = '{{description}}'
1498
+
1499
+ export const argsSchema = z.object({})
1500
+
1501
+ export async function run(args: z.infer<typeof argsSchema>, context: ContainerContext) {
1502
+ const { container } = context
1503
+
1504
+ // Return your data here
1505
+ return {}
1506
+ }` }
1507
+ ],
1508
+ full: `import { z } from 'zod'
1509
+ import type { ContainerContext } from '@soederpop/luca'
1510
+
1511
+ export const description = '{{description}}'
1512
+
1513
+ export const argsSchema = z.object({})
1514
+
1515
+ export async function run(args: z.infer<typeof argsSchema>, context: ContainerContext) {
1516
+ const { container } = context
1517
+
1518
+ // Return your data here
1519
+ return {}
1520
+ }`,
1521
+ tutorial: `# Building a Selector
1522
+
1523
+ A selector returns data. Where commands perform actions, selectors query and return structured results with built-in caching. Selectors live in a project's \`selectors/\` folder and are automatically discovered.
1524
+
1525
+ When to build a selector:
1526
+ - You need to query project data (package info, file listings, config values)
1527
+ - The result benefits from caching (keyed by git SHA or custom key)
1528
+ - You want the data available via \`container.select('name')\` or \`luca select name\`
1529
+
1530
+ ## Imports
1531
+
1532
+ \`\`\`ts
1533
+ import { z } from 'zod'
1534
+ import type { ContainerContext } from '@soederpop/luca'
1535
+ \`\`\`
1536
+
1537
+ ## Args Schema
1538
+
1539
+ Define the selector's input arguments with Zod.
1540
+
1541
+ \`\`\`ts
1542
+ export const argsSchema = z.object({
1543
+ // Add your input arguments here.
1544
+ // Example: field: z.string().optional().describe('Specific field to return'),
1545
+ })
1546
+ \`\`\`
1547
+
1548
+ ## Description
1549
+
1550
+ Export a description string for discoverability:
1551
+
1552
+ \`\`\`ts
1553
+ export const description = '{{description}}'
1554
+ \`\`\`
1555
+
1556
+ ## Caching
1557
+
1558
+ Selectors cache by default. The default cache key is \`hashObject({ selectorName, args, gitSha })\` — same args + same commit = cache hit.
1559
+
1560
+ To customize the cache key:
1561
+
1562
+ \`\`\`ts
1563
+ export function cacheKey(args: z.infer<typeof argsSchema>, context: ContainerContext) {
1564
+ return context.container.git.currentCommitSha
1565
+ }
1566
+ \`\`\`
1567
+
1568
+ To disable caching:
1569
+
1570
+ \`\`\`ts
1571
+ export const cacheable = false
1572
+ \`\`\`
1573
+
1574
+ ## Handler
1575
+
1576
+ Export a \`run\` function that returns data. It receives parsed args and the container context.
1577
+
1578
+ \`\`\`ts
1579
+ export async function run(args: z.infer<typeof argsSchema>, context: ContainerContext) {
1580
+ const { container } = context
1581
+ // Query and return your data
1582
+ return { /* your data */ }
1583
+ }
1584
+ \`\`\`
1585
+
1586
+ ## Complete Example
1587
+
1588
+ \`\`\`ts
1589
+ import { z } from 'zod'
1590
+ import type { ContainerContext } from '@soederpop/luca'
1591
+
1592
+ export const description = '{{description}}'
1593
+
1594
+ export const argsSchema = z.object({})
1595
+
1596
+ export async function run(args: z.infer<typeof argsSchema>, context: ContainerContext) {
1597
+ const { container } = context
1598
+
1599
+ // Return your data here
1600
+ return {}
1601
+ }
1602
+ \`\`\`
1603
+
1604
+ ## Conventions
1605
+
1606
+ - **File location**: \`selectors/{{kebabName}}.ts\` in the project root. Discovered automatically.
1607
+ - **Naming**: kebab-case for filename. \`luca select {{kebabName}}\` maps to \`selectors/{{kebabName}}.ts\`.
1608
+ - **Use the container**: Never import \`fs\`, \`path\` directly. Use \`container.feature('fs')\`, \`container.paths\`.
1609
+ - **Return data**: The \`run\` function must return the data. It gets wrapped in \`{ data, cached, cacheKey }\` by the framework.
1610
+ - **Caching**: On by default. Override \`cacheKey()\` for custom invalidation, or set \`cacheable = false\` to skip.
1611
+ - **CLI**: \`luca select {{kebabName}}\` runs the selector and prints JSON. Use \`--json\` for data only, \`--no-cache\` to force fresh.
1447
1612
  `,
1448
1613
  }
1449
1614
  }
1450
1615
 
1616
+ export const assistantFiles: Record<string, string> = {
1617
+ "CORE.md": `# Luca Assistant Example
1618
+
1619
+ You are currently an example / template "Assistant" provided by the Luca framework. ( You'll probably have no idea what that is, don't worry, it doesn't matter ).
1620
+
1621
+ You are what gets scaffolded when a user writes the \`luca scaffold assistant\` command.
1622
+
1623
+ In luca, an Assistant is backed by a folder which has a few components:
1624
+
1625
+ - CORE.md -- this is a markdown file that will get injected into the system prompt of a chat completion call
1626
+ - tools.ts -- this file is expected to export functions, and a schemas object whose keys are the names of the functions that get exported, and whose values are zod v4 schemas that describe the parameters
1627
+ - hooks.ts -- this file is expexted to export functions, whose names match the events emitted by the luca assistant helper
1628
+
1629
+ Currently, the user is chatting with you from the \`luca chat\` CLI.
1630
+
1631
+ You should tell them what each of these files is and how to edit them.
1632
+
1633
+ It is also important for them to know that the luca \`container\` is globally available for them in the context of the \`tools.ts\` and \`hooks.ts\` files.
1634
+
1635
+ `,
1636
+ "tools.ts": `import { z } from 'zod'
1637
+
1638
+ export const schemas = {
1639
+ README: z.object({}).describe('CALL THIS README FUNCTION AS EARLY AS POSSIBLE')
1640
+ }
1641
+
1642
+ export function README(options: z.infer<typeof schemas.README>) {
1643
+ return 'YO YO'
1644
+ }
1645
+
1646
+ `,
1647
+ "hooks.ts": `export function started() {
1648
+ console.log('Assistant started!')
1649
+ }
1650
+ `
1651
+ }
1652
+
1451
1653
  export const mcpReadme = `# Luca Development Guide
1452
1654
 
1453
1655
  You are working in a **luca project**. The luca container provides all capabilities your code needs. Do not install npm packages or import Node.js builtins directly.