@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
@@ -2,8 +2,8 @@ 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 { scaffolds } from '../scaffolds/generated.js'
6
- import { generateScaffold, toCamelCase } from '../scaffolds/template.js'
5
+ import { scaffolds, assistantFiles } from '../scaffolds/generated.js'
6
+ import { generateScaffold, toCamelCase, toKebabCase } from '../scaffolds/template.js'
7
7
 
8
8
  declare module '../command.js' {
9
9
  interface AvailableCommands {
@@ -11,69 +11,215 @@ declare module '../command.js' {
11
11
  }
12
12
  }
13
13
 
14
- const validTypes = Object.keys(scaffolds)
14
+ const validTypes = [...Object.keys(scaffolds), 'assistant']
15
15
 
16
16
  export const argsSchema = CommandOptionsSchema.extend({
17
17
  description: z.string().optional().describe('Brief description of the helper'),
18
- output: z.string().optional().describe('Output file path (defaults to stdout)'),
18
+ output: z.string().optional().describe('Output file path (overrides default location)'),
19
+ print: z.boolean().default(false).describe('Print the generated code to stdout instead of writing a file'),
19
20
  tutorial: z.boolean().default(false).describe('Show the full tutorial instead of generating code'),
20
21
  })
21
22
 
23
+ const TYPE_INFO: Record<string, { what: string; where: string; run: string }> = {
24
+ feature: {
25
+ what: 'A container-managed capability (caching, encryption, custom I/O)',
26
+ where: 'features/<name>.ts',
27
+ run: 'container.feature(\'<name>\') in any command, endpoint, or script',
28
+ },
29
+ client: {
30
+ what: 'A connection to an external service (REST API, WebSocket, GraphQL)',
31
+ where: 'clients/<name>.ts',
32
+ run: 'container.client(\'<name>\') in any command, endpoint, or script',
33
+ },
34
+ server: {
35
+ what: 'A listener accepting incoming connections (HTTP, WebSocket, custom protocol)',
36
+ where: 'servers/<name>.ts',
37
+ run: 'container.server(\'<name>\') then .start()',
38
+ },
39
+ command: {
40
+ what: 'A CLI task that extends `luca` (build scripts, generators, automation)',
41
+ where: 'commands/<name>.ts',
42
+ run: 'luca <name>',
43
+ },
44
+ endpoint: {
45
+ what: 'A REST API route auto-discovered by `luca serve`',
46
+ where: 'endpoints/<name>.ts',
47
+ run: 'luca serve → GET/POST /api/<name>',
48
+ },
49
+ selector: {
50
+ what: 'A cached data query — returns structured results from the container',
51
+ where: 'selectors/<name>.ts',
52
+ run: 'luca select <name> or container.select(\'<name>\')',
53
+ },
54
+ assistant: {
55
+ what: 'An AI assistant with a system prompt, tools, and lifecycle hooks',
56
+ where: 'assistants/<name>/',
57
+ run: 'luca chat <name>',
58
+ },
59
+ }
60
+
22
61
  export default async function scaffoldCommand(options: z.infer<typeof argsSchema>, context: ContainerContext) {
23
62
  const container = context.container as any
24
63
  const args = container.argv._ as string[]
64
+ const ui = container.feature('ui')
25
65
 
26
66
  // args: ["scaffold", type?, name?]
27
67
  const type = args[1]
28
68
  const name = args[2]
29
69
 
70
+ // ── No type or invalid type: show full help ──────────────────
30
71
  if (!type || !validTypes.includes(type)) {
31
- console.log(`Usage: luca scaffold <type> <name> [--description "..."] [--output path] [--tutorial]`)
32
- console.log(`\nTypes: ${validTypes.join(', ')}`)
33
- console.log(`\nExamples:`)
34
- console.log(` luca scaffold feature diskCache --description "File-backed key-value cache"`)
35
- console.log(` luca scaffold command deploy --output commands/deploy.ts`)
36
- console.log(` luca scaffold endpoint healthCheck`)
37
- console.log(` luca scaffold feature --tutorial`)
72
+ ui.print.cyan('\n luca scaffold generate boilerplate for luca helpers\n')
73
+ ui.print(' Usage: luca scaffold <type> <name> [options]\n')
74
+ ui.print(' Options:')
75
+ ui.print(' --description "..." Brief description (shows in help text and docs)')
76
+ ui.print(' --output <path> Override the default output file path')
77
+ ui.print(' --print Print to stdout instead of writing a file')
78
+ ui.print(' --tutorial Show the full guide for a type instead of generating code\n')
79
+
80
+ ui.print(' Types:\n')
81
+ for (const [t, info] of Object.entries(TYPE_INFO)) {
82
+ ui.print.green(` ${t}`)
83
+ ui.print(` ${info.what}`)
84
+ ui.print.dim(` File: ${info.where} → Run: ${info.run}`)
85
+ ui.print('')
86
+ }
87
+
88
+ ui.print(' Examples:\n')
89
+ ui.print(' # Generate a command (writes to commands/deploy.ts)')
90
+ ui.print(' luca scaffold command deploy --description "Deploy to production"\n')
91
+ ui.print(' # Generate a feature (writes to features/disk-cache.ts)')
92
+ ui.print(' luca scaffold feature disk-cache --description "File-backed key-value cache"\n')
93
+ ui.print(' # Print to stdout instead of writing')
94
+ ui.print(' luca scaffold endpoint users --print\n')
95
+ ui.print(' # Read the full tutorial for a type')
96
+ ui.print(' luca scaffold feature --tutorial')
97
+ ui.print(' luca scaffold endpoint --tutorial\n')
98
+
99
+ ui.print(' Workflow:\n')
100
+ ui.print(' 1. luca scaffold <type> <name> Generate the file')
101
+ ui.print(' 2. Edit the generated file — add your logic')
102
+ ui.print(' 3. luca about Verify it was discovered')
103
+ ui.print(' 4. luca describe <name> See the generated docs\n')
104
+
105
+ if (type && !validTypes.includes(type)) {
106
+ ui.print.yellow(` "${type}" is not a valid type. Available: ${validTypes.join(', ')}\n`)
107
+ }
38
108
  return
39
109
  }
40
110
 
41
- // Tutorial mode — show the full scaffold doc
111
+ // ── Tutorial mode ────────────────────────────────────────────
42
112
  if (options.tutorial) {
43
113
  const scaffold = scaffolds[type]
44
114
  if (scaffold?.tutorial) {
45
- const ui = container.feature('ui')
46
115
  console.log(ui.markdown(scaffold.tutorial))
47
116
  } else {
48
- console.log(`No tutorial available for type: ${type}`)
117
+ ui.print.yellow(`No tutorial available for type: ${type}`)
49
118
  }
50
119
  return
51
120
  }
52
121
 
122
+ // ── Missing name ─────────────────────────────────────────────
53
123
  if (!name) {
54
- console.log(`Usage: luca scaffold ${type} <name> [--description "..."] [--output path]`)
124
+ const info = TYPE_INFO[type]
125
+ ui.print.cyan(`\n luca scaffold ${type} <name> [options]\n`)
126
+ ui.print(` ${info?.what || ''}\n`)
127
+ ui.print(' Examples:')
128
+ if (type === 'feature') {
129
+ ui.print(` luca scaffold feature diskCache --description "File-backed cache"`)
130
+ ui.print(` luca scaffold feature diskCache --output features/diskCache.ts`)
131
+ } else if (type === 'client') {
132
+ ui.print(` luca scaffold client github --description "GitHub API client"`)
133
+ ui.print(` luca scaffold client github --output clients/github.ts`)
134
+ } else if (type === 'server') {
135
+ ui.print(` luca scaffold server grpc --description "gRPC server"`)
136
+ ui.print(` luca scaffold server grpc --output servers/grpc.ts`)
137
+ } else if (type === 'command') {
138
+ ui.print(` luca scaffold command deploy --description "Deploy to production"`)
139
+ ui.print(` luca scaffold command deploy --output commands/deploy.ts`)
140
+ } else if (type === 'endpoint') {
141
+ ui.print(` luca scaffold endpoint users --description "User management API"`)
142
+ ui.print(` luca scaffold endpoint users --output endpoints/users.ts`)
143
+ } else if (type === 'selector') {
144
+ ui.print(` luca scaffold selector package-info --description "Returns parsed package.json data"`)
145
+ ui.print(` luca scaffold selector package-info --output selectors/package-info.ts`)
146
+ } else if (type === 'assistant') {
147
+ ui.print(` luca scaffold assistant chief-of-staff`)
148
+ ui.print(` luca scaffold assistant chief-of-staff --output assistants/chief-of-staff`)
149
+ }
150
+ ui.print(`\n luca scaffold ${type} --tutorial Full guide with patterns and conventions\n`)
151
+ return
152
+ }
153
+
154
+ // ── Assistant: multi-file scaffold ───────────────────────────
155
+ if (type === 'assistant') {
156
+ if (!assistantFiles || Object.keys(assistantFiles).length === 0) {
157
+ ui.print.yellow('No assistant scaffold files bundled. Rebuild with: luca build-scaffolds')
158
+ return
159
+ }
160
+
161
+ const outputDir = options.output || `assistants/${toCamelCase(name)}`
162
+ const fs = container.feature('fs')
163
+ const resolvedDir = container.paths.resolve(outputDir)
164
+
165
+ await fs.ensureFolder(resolvedDir)
166
+
167
+ for (const [fileName, content] of Object.entries(assistantFiles)) {
168
+ const filePath = container.paths.resolve(resolvedDir, fileName)
169
+ await fs.writeFileAsync(filePath, content)
170
+ }
171
+
172
+ ui.print.green(`\n ✓ Scaffolded assistant "${name}" in ${outputDir}/`)
173
+ ui.print.dim(` CORE.md — system prompt (edit this to define your assistant's personality)`)
174
+ ui.print.dim(` tools.ts — tool functions the assistant can call`)
175
+ ui.print.dim(` hooks.ts — lifecycle event handlers`)
176
+ ui.print(`\n Start chatting: luca chat ${name}\n`)
55
177
  return
56
178
  }
57
179
 
58
180
  const code = generateScaffold(type, name, options.description)
59
181
 
60
182
  if (!code) {
61
- console.log(`No scaffold template available for type: ${type}`)
183
+ ui.print.yellow(`No scaffold template available for type: ${type}`)
62
184
  return
63
185
  }
64
186
 
65
- // Write to file or stdout
66
- if (options.output) {
67
- const fs = container.feature('fs')
68
- await fs.writeFileAsync(options.output, code)
69
- console.log(`Wrote ${type} scaffold to ${options.output}`)
70
- } else {
187
+ // --print: just output to stdout
188
+ if (options.print) {
71
189
  console.log(code)
190
+ return
72
191
  }
192
+
193
+ // Default: write to file
194
+ const kebabName = toKebabCase(name)
195
+ const defaultPaths: Record<string, string> = {
196
+ feature: `features/${kebabName}.ts`,
197
+ client: `clients/${kebabName}.ts`,
198
+ server: `servers/${kebabName}.ts`,
199
+ command: `commands/${kebabName}.ts`,
200
+ endpoint: `endpoints/${kebabName}.ts`,
201
+ selector: `selectors/${kebabName}.ts`,
202
+ }
203
+ const outputPath = options.output || defaultPaths[type] || `${type}s/${kebabName}.ts`
204
+ const fs = container.feature('fs')
205
+ const resolvedPath = container.paths.resolve(outputPath)
206
+
207
+ // Check if file already exists
208
+ if (await fs.existsAsync(resolvedPath)) {
209
+ ui.print.yellow(` ✗ File already exists: ${outputPath}`)
210
+ ui.print.dim(` Use --output <path> to write to a different location, or --print to view the code\n`)
211
+ return
212
+ }
213
+
214
+ const dir = container.paths.resolve(outputPath, '..')
215
+ await fs.ensureFolder(dir)
216
+ await fs.writeFileAsync(resolvedPath, code)
217
+ ui.print.green(` ✓ Wrote ${type} scaffold to ${outputPath}`)
218
+ ui.print.dim(` Next: edit the file, then run \`luca about\` to verify discovery\n`)
73
219
  }
74
220
 
75
221
  commands.registerHandler('scaffold', {
76
- description: 'Generate boilerplate for a new luca feature, client, server, command, or endpoint',
222
+ description: 'Generate boilerplate for a new luca feature, client, server, command, endpoint, or assistant',
77
223
  argsSchema,
78
224
  handler: scaffoldCommand,
79
225
  })
@@ -0,0 +1,94 @@
1
+ import { z } from 'zod'
2
+ import { commands } from '../command.js'
3
+ import { selectors } from '../selector.js'
4
+ import { CommandOptionsSchema } from '../schemas/base.js'
5
+ import type { ContainerContext } from '../container.js'
6
+
7
+ declare module '../command.js' {
8
+ interface AvailableCommands {
9
+ select: ReturnType<typeof commands.registerHandler>
10
+ }
11
+ }
12
+
13
+ export const argsSchema = CommandOptionsSchema.extend({
14
+ json: z.boolean().default(false).describe('Output result as raw JSON (data only, no metadata)'),
15
+ noCache: z.boolean().default(false).describe('Skip cache lookup and force a fresh run'),
16
+ })
17
+
18
+ export default async function selectCommand(options: z.infer<typeof argsSchema>, context: ContainerContext) {
19
+ const container = context.container as any
20
+ const ui = container.feature('ui')
21
+ const args = container.argv._ as string[]
22
+ const name = args[1]
23
+
24
+ // Discover project selectors
25
+ await container.helpers.discoverAll()
26
+
27
+ if (!name) {
28
+ const available = selectors.available
29
+ if (available.length === 0) {
30
+ ui.print('No selectors available.')
31
+ ui.print.dim('Create one: luca scaffold selector <name>')
32
+ } else {
33
+ ui.print.cyan('\n luca select <name> [--json] [--no-cache]\n')
34
+ ui.print(' Available selectors:\n')
35
+ for (const s of available) {
36
+ ui.print.green(` ${s}`)
37
+ }
38
+ ui.print('')
39
+ }
40
+ return
41
+ }
42
+
43
+ const instance = container.select(name)
44
+
45
+ // Pass remaining args (after command name and selector name) as input
46
+ const selectorArgs: Record<string, any> = { ...options }
47
+ delete selectorArgs._
48
+ delete selectorArgs.json
49
+ delete selectorArgs.noCache
50
+ delete selectorArgs.cache
51
+ delete selectorArgs.dispatchSource
52
+
53
+ // minimist turns --no-cache into { cache: false }
54
+ const skipCache = options.noCache || (container.argv.cache === false)
55
+
56
+ if (skipCache) {
57
+ // Bypass cache by calling run() directly with proper lifecycle
58
+ const Cls = instance.constructor as any
59
+ const parsed = Cls.argsSchema.parse(selectorArgs)
60
+ instance.state.set('running', true)
61
+ instance.emit('started')
62
+ let data: any
63
+ try {
64
+ data = await instance.run(parsed, instance.context)
65
+ instance.state.set('running', false)
66
+ instance.state.set('lastRanAt', Date.now())
67
+ instance.emit('completed', data)
68
+ } catch (err: any) {
69
+ instance.state.set('running', false)
70
+ instance.emit('failed', err)
71
+ throw err
72
+ }
73
+ if (options.json) {
74
+ console.log(JSON.stringify(data, null, 2))
75
+ } else {
76
+ console.log(JSON.stringify({ data, cached: false }, null, 2))
77
+ }
78
+ return
79
+ }
80
+
81
+ const result = await instance.select(selectorArgs)
82
+
83
+ if (options.json) {
84
+ console.log(JSON.stringify(result.data, null, 2))
85
+ } else {
86
+ console.log(JSON.stringify(result, null, 2))
87
+ }
88
+ }
89
+
90
+ commands.registerHandler('select', {
91
+ description: 'Run a selector and display its cached or fresh data',
92
+ argsSchema,
93
+ handler: selectCommand,
94
+ })
@@ -115,7 +115,16 @@ export default async function serve(options: z.infer<typeof argsSchema>, context
115
115
  }
116
116
  }
117
117
 
118
- await expressServer.start({ port })
118
+ try {
119
+ await expressServer.start({ port })
120
+ } catch (error: any) {
121
+ if (error?.code === 'EADDRINUSE') {
122
+ console.error(`Port ${port} is already in use.`)
123
+ console.error(`Use --force to kill the process on this port, or --any-port to find another port.`)
124
+ process.exit(1)
125
+ }
126
+ throw error
127
+ }
119
128
 
120
129
  const name = manifest.name || 'Server'
121
130
  console.log(`\n${name} listening on http://localhost:${port}`)
package/src/container.ts CHANGED
@@ -629,6 +629,21 @@ function presentContainerIntrospectionAsMarkdown(data: ContainerIntrospection, s
629
629
  // Header
630
630
  sections.push(`${heading(1)} ${data.className}\n\n${data.description || ''}`)
631
631
 
632
+ // Container Properties section (node containers expose these top-level getters)
633
+ if (data.environment?.isNode || data.environment?.isBun) {
634
+ sections.push([
635
+ `${heading(2)} Container Properties`,
636
+ '',
637
+ '| Property | Description |',
638
+ '|----------|-------------|',
639
+ '| `container.cwd` | Current working directory |',
640
+ '| `container.paths` | Path utilities scoped to cwd: `resolve()`, `join()`, `relative()`, `dirname()`, `basename()`, `parse()` |',
641
+ '| `container.manifest` | Parsed `package.json` for the current directory (`name`, `version`, `dependencies`, etc.) |',
642
+ '| `container.argv` | Raw parsed CLI arguments (from minimist). Prefer `positionals` export for positional args in commands |',
643
+ '| `container.utils` | Common utilities: `uuid()`, `hashObject()`, `stringUtils`, `lodash` |',
644
+ ].join('\n'))
645
+ }
646
+
632
647
  // Registries section
633
648
  if (data.registries && data.registries.length > 0) {
634
649
  sections.push(`${heading(2)} Registries`)
package/src/endpoint.ts CHANGED
@@ -56,6 +56,14 @@ export interface EndpointModule {
56
56
 
57
57
  const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete'] as const
58
58
 
59
+ /** All recognized exports on an endpoint module */
60
+ const KNOWN_EXPORTS = new Set([
61
+ 'path', 'description', 'tags', 'default', 'rateLimit',
62
+ ...HTTP_METHODS,
63
+ ...HTTP_METHODS.map(m => `${m}Schema`),
64
+ ...HTTP_METHODS.map(m => `${m}RateLimit`),
65
+ ])
66
+
59
67
  /**
60
68
  * Sliding-window rate limiter keyed by IP address.
61
69
  * Tracks timestamps of requests and prunes entries older than the window.
@@ -177,6 +185,10 @@ export class Endpoint<
177
185
  this._module = imported.default || imported
178
186
  }
179
187
 
188
+ // Note: DELETE handlers should be exported as `export { del as delete }`.
189
+ // We no longer remap `destroy` → `delete` because ESM namespace objects
190
+ // are frozen and the mutation throws on Bun.
191
+
180
192
  this.state.set('methods', this.methods)
181
193
  this.state.set('path', this.path)
182
194
  this.emit('loaded', this._module)
@@ -328,6 +340,13 @@ export class Endpoint<
328
340
  }
329
341
  }
330
342
 
343
+ export function warnUnknownExports(mod: Record<string, any>, filePath: string): void {
344
+ const unknown = Object.keys(mod).filter(k => !k.startsWith('__') && !KNOWN_EXPORTS.has(k))
345
+ if (unknown.length > 0) {
346
+ console.warn(`[endpoint] ${filePath}: unknown exports: ${unknown.join(', ')}`)
347
+ }
348
+ }
349
+
331
350
  export class EndpointsRegistry extends Registry<Endpoint<any>> {
332
351
  override scope = 'endpoints'
333
352
  override baseClass = Endpoint
package/src/graft.ts ADDED
@@ -0,0 +1,181 @@
1
+ import { Helper } from './helper.js'
2
+ import { z } from 'zod'
3
+
4
+ export type GraftScope = 'features' | 'clients' | 'servers' | 'commands' | 'endpoints' | 'selectors'
5
+
6
+ /**
7
+ * Export names that map to static class properties or receive special handling.
8
+ * Everything else that is a function goes onto the prototype as a method.
9
+ */
10
+ const RESERVED_EXPORTS = new Set([
11
+ 'description', 'envVars',
12
+ 'stateSchema', 'optionsSchema', 'eventsSchema',
13
+ 'args', 'argsSchema',
14
+ 'positionals',
15
+ 'getters',
16
+ 'run', 'handler',
17
+ 'cacheKey', 'cacheable',
18
+ 'default',
19
+ ])
20
+
21
+ /**
22
+ * Returns true when the candidate is a class (constructor function) whose
23
+ * prototype chain includes BaseClass. Handles cross-module-boundary cases
24
+ * by checking both reference equality and class name.
25
+ */
26
+ export function isNativeHelperClass(candidate: unknown, BaseClass: typeof Helper | any): boolean {
27
+ if (typeof candidate !== 'function') return false
28
+ if (!BaseClass) return false
29
+ if (candidate === BaseClass) return true
30
+
31
+ let proto = Object.getPrototypeOf(candidate)
32
+ while (proto) {
33
+ if (proto === BaseClass) return true
34
+ if (BaseClass.name && proto.name === BaseClass.name) return true
35
+ proto = Object.getPrototypeOf(proto)
36
+ }
37
+ return false
38
+ }
39
+
40
+ /**
41
+ * Synthesize a Helper subclass from plain module exports.
42
+ *
43
+ * Given a set of exports from a module file and a Helper base class, produces a
44
+ * fully-formed subclass ready for registration in the appropriate registry.
45
+ *
46
+ * Static export mappings:
47
+ * description → static description
48
+ * stateSchema → static stateSchema
49
+ * optionsSchema → static optionsSchema (also aliased from `args`)
50
+ * eventsSchema → static eventsSchema
51
+ * envVars → static envVars
52
+ * argsSchema → static argsSchema (Command scope; also aliased from `args`)
53
+ * positionals → static positionals (Command scope; stored for CLI dispatch mapping)
54
+ * getters → Object.defineProperty(proto, key, { get }) per key
55
+ * run → override run() for Command scope (receives named args + context)
56
+ * handler → legacy: wired through parseArgs() for backward compat
57
+ * [fn exports] → prototype methods (all other function-valued named exports)
58
+ */
59
+ export function graftModule<T extends typeof Helper>(
60
+ BaseClass: T,
61
+ moduleExports: Record<string, any>,
62
+ id: string,
63
+ scope: GraftScope,
64
+ ): T {
65
+ // Resolve schemas — prefer explicit exports, fall back to whatever the base class has
66
+ const optionsSchema = moduleExports.optionsSchema
67
+ ?? moduleExports.args
68
+ ?? (BaseClass as any).optionsSchema
69
+
70
+ const stateSchema = moduleExports.stateSchema
71
+ ?? (BaseClass as any).stateSchema
72
+
73
+ const eventsSchema = moduleExports.eventsSchema
74
+ ?? (BaseClass as any).eventsSchema
75
+
76
+ // Build the subclass
77
+ const GraftedClass = class extends (BaseClass as any) {} as unknown as T & { prototype: any }
78
+
79
+ // Static overrides
80
+ const statics: Record<string, any> = {
81
+ shortcut: `${scope}.${id}`,
82
+ description: moduleExports.description ?? '',
83
+ }
84
+
85
+ if (moduleExports.envVars) statics.envVars = moduleExports.envVars
86
+ if (optionsSchema) statics.optionsSchema = optionsSchema
87
+ if (stateSchema) statics.stateSchema = stateSchema
88
+ if (eventsSchema) statics.eventsSchema = eventsSchema
89
+
90
+ // Command-specific statics
91
+ if (scope === 'commands') {
92
+ const argsSchema = moduleExports.argsSchema ?? moduleExports.args ?? optionsSchema
93
+ statics.argsSchema = argsSchema
94
+ statics.commandDescription = moduleExports.description ?? ''
95
+ if (moduleExports.positionals) {
96
+ statics.positionals = moduleExports.positionals
97
+ }
98
+ }
99
+
100
+ // Selector-specific statics (must be set before Object.assign)
101
+ if (scope === 'selectors') {
102
+ statics.cacheable = moduleExports.cacheable !== false
103
+ statics.selectorDescription = moduleExports.description ?? ''
104
+ statics.argsSchema = moduleExports.argsSchema ?? moduleExports.args ?? optionsSchema
105
+ }
106
+
107
+ Object.assign(GraftedClass, statics)
108
+
109
+ // Wire run() for Command scope
110
+ if (scope === 'commands') {
111
+ if (typeof moduleExports.run === 'function') {
112
+ const runFn = moduleExports.run
113
+ ;(GraftedClass as any).prototype.run = async function (args: any, context: any) {
114
+ return runFn(args, context)
115
+ }
116
+ } else if (typeof moduleExports.handler === 'function') {
117
+ const handlerFn = moduleExports.handler
118
+ ;(GraftedClass as any).prototype.run = async function (args: any, context: any) {
119
+ return handlerFn(args, context)
120
+ }
121
+ }
122
+ }
123
+
124
+ // Wire run() and resolveCacheKey() for Selector scope
125
+ if (scope === 'selectors') {
126
+ if (typeof moduleExports.run === 'function') {
127
+ const runFn = moduleExports.run
128
+ ;(GraftedClass as any).prototype.run = async function (args: any, context: any) {
129
+ return runFn(args, context)
130
+ }
131
+ }
132
+
133
+ if (typeof moduleExports.cacheKey === 'function') {
134
+ const cacheKeyFn = moduleExports.cacheKey
135
+ ;(GraftedClass as any).prototype.resolveCacheKey = function (args: any, context: any) {
136
+ return cacheKeyFn(args, context)
137
+ }
138
+ }
139
+ }
140
+
141
+ // Install getters from the `getters` export
142
+ const getterMap: Record<string, () => any> = moduleExports.getters ?? {}
143
+ for (const [key, fn] of Object.entries(getterMap)) {
144
+ if (typeof fn === 'function') {
145
+ Object.defineProperty((GraftedClass as any).prototype, key, {
146
+ get: fn,
147
+ configurable: true,
148
+ enumerable: false,
149
+ })
150
+ }
151
+ }
152
+
153
+ // Graft all exported functions (that are not reserved) as prototype methods
154
+ for (const [key, value] of Object.entries(moduleExports)) {
155
+ if (RESERVED_EXPORTS.has(key)) continue
156
+ if (typeof value !== 'function') continue
157
+ ;(GraftedClass as any).prototype[key] = value
158
+ }
159
+
160
+ // Name the class
161
+ const suffix = SCOPE_SUFFIXES[scope] || ''
162
+ const className = pascalCase(id) + suffix
163
+ Object.defineProperty(GraftedClass, 'name', { value: className, configurable: true })
164
+
165
+ return GraftedClass as T
166
+ }
167
+
168
+ const SCOPE_SUFFIXES: Record<string, string> = {
169
+ features: 'Feature',
170
+ clients: 'Client',
171
+ servers: 'Server',
172
+ commands: 'Command',
173
+ endpoints: 'Endpoint',
174
+ selectors: 'Selector',
175
+ }
176
+
177
+ function pascalCase(id: string): string {
178
+ return id
179
+ .replace(/[-_](.)/g, (_: string, c: string) => c.toUpperCase())
180
+ .replace(/^(.)/, (_: string, c: string) => c.toUpperCase())
181
+ }