@soederpop/luca 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/CLAUDE.md +10 -1
  2. package/RUNME.md +56 -0
  3. package/bun.lock +1 -1
  4. package/commands/build-bootstrap.ts +78 -0
  5. package/commands/build-scaffolds.ts +24 -2
  6. package/commands/try-all-challenges.ts +543 -0
  7. package/commands/try-challenge.ts +100 -0
  8. package/docs/README.md +52 -80
  9. package/docs/TABLE-OF-CONTENTS.md +82 -51
  10. package/docs/apis/clients/elevenlabs.md +232 -8
  11. package/docs/apis/clients/graph.md +59 -8
  12. package/docs/apis/clients/openai.md +362 -2
  13. package/docs/apis/clients/rest.md +122 -2
  14. package/docs/apis/clients/websocket.md +71 -17
  15. package/docs/apis/features/agi/assistant.md +9 -3
  16. package/docs/apis/features/agi/assistants-manager.md +2 -2
  17. package/docs/apis/features/agi/claude-code.md +153 -14
  18. package/docs/apis/features/agi/conversation-history.md +15 -3
  19. package/docs/apis/features/agi/conversation.md +133 -20
  20. package/docs/apis/features/agi/openai-codex.md +90 -12
  21. package/docs/apis/features/agi/skills-library.md +23 -5
  22. package/docs/apis/features/node/container-link.md +59 -0
  23. package/docs/apis/features/node/content-db.md +1 -1
  24. package/docs/apis/features/node/disk-cache.md +1 -1
  25. package/docs/apis/features/node/dns.md +1 -0
  26. package/docs/apis/features/node/docker.md +2 -1
  27. package/docs/apis/features/node/esbuild.md +4 -3
  28. package/docs/apis/features/node/file-manager.md +13 -4
  29. package/docs/apis/features/node/fs.md +726 -171
  30. package/docs/apis/features/node/git.md +1 -0
  31. package/docs/apis/features/node/google-auth.md +23 -4
  32. package/docs/apis/features/node/google-calendar.md +14 -2
  33. package/docs/apis/features/node/google-docs.md +15 -2
  34. package/docs/apis/features/node/google-drive.md +21 -3
  35. package/docs/apis/features/node/google-sheets.md +14 -2
  36. package/docs/apis/features/node/grep.md +2 -0
  37. package/docs/apis/features/node/helpers.md +29 -0
  38. package/docs/apis/features/node/ink.md +2 -2
  39. package/docs/apis/features/node/networking.md +39 -4
  40. package/docs/apis/features/node/os.md +28 -0
  41. package/docs/apis/features/node/postgres.md +26 -4
  42. package/docs/apis/features/node/proc.md +37 -28
  43. package/docs/apis/features/node/process-manager.md +33 -5
  44. package/docs/apis/features/node/repl.md +1 -1
  45. package/docs/apis/features/node/runpod.md +1 -0
  46. package/docs/apis/features/node/secure-shell.md +7 -0
  47. package/docs/apis/features/node/semantic-search.md +12 -5
  48. package/docs/apis/features/node/sqlite.md +26 -4
  49. package/docs/apis/features/node/telegram.md +30 -5
  50. package/docs/apis/features/node/tts.md +17 -2
  51. package/docs/apis/features/node/ui.md +1 -1
  52. package/docs/apis/features/node/vault.md +4 -9
  53. package/docs/apis/features/node/vm.md +3 -12
  54. package/docs/apis/features/node/window-manager.md +128 -20
  55. package/docs/apis/features/web/asset-loader.md +13 -1
  56. package/docs/apis/features/web/container-link.md +59 -0
  57. package/docs/apis/features/web/esbuild.md +4 -3
  58. package/docs/apis/features/web/helpers.md +29 -0
  59. package/docs/apis/features/web/network.md +16 -2
  60. package/docs/apis/features/web/speech.md +16 -2
  61. package/docs/apis/features/web/vault.md +4 -9
  62. package/docs/apis/features/web/vm.md +3 -12
  63. package/docs/apis/features/web/voice.md +18 -1
  64. package/docs/apis/servers/express.md +18 -2
  65. package/docs/apis/servers/mcp.md +29 -4
  66. package/docs/apis/servers/websocket.md +34 -6
  67. package/docs/bootstrap/CLAUDE.md +100 -0
  68. package/docs/bootstrap/SKILL.md +222 -0
  69. package/docs/bootstrap/templates/about-command.ts +41 -0
  70. package/docs/bootstrap/templates/docs-models.ts +22 -0
  71. package/docs/bootstrap/templates/docs-readme.md +43 -0
  72. package/docs/bootstrap/templates/example-feature.ts +53 -0
  73. package/docs/bootstrap/templates/health-endpoint.ts +15 -0
  74. package/docs/bootstrap/templates/luca-cli.ts +25 -0
  75. package/docs/bootstrap/templates/runme.md +54 -0
  76. package/docs/challenges/caching-proxy.md +16 -0
  77. package/docs/challenges/content-db-round-trip.md +14 -0
  78. package/docs/challenges/custom-command.md +9 -0
  79. package/docs/challenges/file-watcher-pipeline.md +11 -0
  80. package/docs/challenges/grep-audit-report.md +15 -0
  81. package/docs/challenges/multi-feature-dashboard.md +14 -0
  82. package/docs/challenges/process-orchestrator.md +17 -0
  83. package/docs/challenges/rest-api-server-with-client.md +12 -0
  84. package/docs/challenges/script-runner-with-vm.md +11 -0
  85. package/docs/challenges/simple-rest-api.md +15 -0
  86. package/docs/challenges/websocket-serve-and-client.md +11 -0
  87. package/docs/challenges/yaml-config-system.md +14 -0
  88. package/docs/command-system-overhaul.md +94 -0
  89. package/docs/examples/assistant/CORE.md +18 -0
  90. package/docs/examples/assistant/hooks.ts +3 -0
  91. package/docs/examples/assistant/tools.ts +10 -0
  92. package/docs/examples/window-manager-layouts.md +180 -0
  93. package/docs/in-memory-fs.md +4 -0
  94. package/docs/models.ts +13 -10
  95. package/docs/philosophy.md +4 -3
  96. package/docs/reports/console-hmr-design.md +170 -0
  97. package/docs/reports/helper-semantic-search.md +72 -0
  98. package/docs/scaffolds/client.md +29 -20
  99. package/docs/scaffolds/command.md +64 -50
  100. package/docs/scaffolds/endpoint.md +31 -36
  101. package/docs/scaffolds/feature.md +28 -18
  102. package/docs/scaffolds/selector.md +91 -0
  103. package/docs/scaffolds/server.md +18 -9
  104. package/docs/selectors.md +115 -0
  105. package/docs/sessions/custom-command/attempt-log-2.md +195 -0
  106. package/docs/sessions/file-watcher-pipeline/attempt-log-1.md +728 -0
  107. package/docs/sessions/file-watcher-pipeline/attempt-log-2.md +555 -0
  108. package/docs/sessions/grep-audit-report/attempt-log-1.md +289 -0
  109. package/docs/sessions/multi-feature-dashboard/attempt-log-2.md +679 -0
  110. package/docs/sessions/rest-api-server-with-client/attempt-log-1.md +1 -0
  111. package/docs/sessions/rest-api-server-with-client/attempt-log-3.md +920 -0
  112. package/docs/sessions/simple-rest-api/attempt-log-1.md +593 -0
  113. package/docs/sessions/websocket-serve-and-client/attempt-log-2.md +995 -0
  114. package/docs/tutorials/00-bootstrap.md +148 -0
  115. package/docs/tutorials/07-endpoints.md +7 -7
  116. package/docs/tutorials/08-commands.md +153 -72
  117. package/luca.cli.ts +3 -0
  118. package/package.json +6 -5
  119. package/public/index.html +1430 -0
  120. package/scripts/examples/using-ollama.ts +2 -1
  121. package/scripts/update-introspection-data.ts +2 -2
  122. package/src/agi/endpoints/experts.ts +1 -1
  123. package/src/agi/features/assistant.ts +7 -0
  124. package/src/agi/features/assistants-manager.ts +5 -5
  125. package/src/agi/features/claude-code.ts +263 -3
  126. package/src/agi/features/conversation-history.ts +7 -1
  127. package/src/agi/features/conversation.ts +26 -3
  128. package/src/agi/features/openai-codex.ts +26 -2
  129. package/src/agi/features/openapi.ts +6 -1
  130. package/src/agi/features/skills-library.ts +9 -1
  131. package/src/bootstrap/generated.ts +595 -0
  132. package/src/cli/cli.ts +64 -21
  133. package/src/client.ts +23 -357
  134. package/src/clients/civitai/index.ts +1 -1
  135. package/src/clients/client-template.ts +1 -1
  136. package/src/clients/comfyui/index.ts +13 -2
  137. package/src/clients/elevenlabs/index.ts +2 -1
  138. package/src/clients/graph.ts +87 -0
  139. package/src/clients/openai/index.ts +10 -1
  140. package/src/clients/rest.ts +207 -0
  141. package/src/clients/websocket.ts +176 -0
  142. package/src/command.ts +281 -34
  143. package/src/commands/bootstrap.ts +185 -0
  144. package/src/commands/chat.ts +5 -4
  145. package/src/commands/describe.ts +341 -4
  146. package/src/commands/help.ts +35 -9
  147. package/src/commands/index.ts +3 -0
  148. package/src/commands/introspect.ts +92 -2
  149. package/src/commands/prompt.ts +5 -6
  150. package/src/commands/run.ts +75 -10
  151. package/src/commands/save-api-docs.ts +49 -0
  152. package/src/commands/scaffold.ts +169 -23
  153. package/src/commands/select.ts +94 -0
  154. package/src/commands/serve.ts +10 -1
  155. package/src/container.ts +15 -0
  156. package/src/endpoint.ts +19 -0
  157. package/src/graft.ts +181 -0
  158. package/src/introspection/generated.agi.ts +12458 -8968
  159. package/src/introspection/generated.node.ts +10573 -7145
  160. package/src/introspection/generated.web.ts +1 -1
  161. package/src/introspection/index.ts +26 -0
  162. package/src/node/container.ts +6 -7
  163. package/src/node/features/content-db.ts +49 -2
  164. package/src/node/features/disk-cache.ts +16 -9
  165. package/src/node/features/dns.ts +16 -3
  166. package/src/node/features/docker.ts +16 -4
  167. package/src/node/features/esbuild.ts +22 -2
  168. package/src/node/features/file-manager.ts +184 -29
  169. package/src/node/features/fs.ts +704 -248
  170. package/src/node/features/git.ts +21 -8
  171. package/src/node/features/grep.ts +23 -3
  172. package/src/node/features/helpers.ts +372 -43
  173. package/src/node/features/networking.ts +39 -4
  174. package/src/node/features/opener.ts +28 -15
  175. package/src/node/features/os.ts +76 -0
  176. package/src/node/features/port-exposer.ts +11 -1
  177. package/src/node/features/postgres.ts +17 -1
  178. package/src/node/features/proc.ts +4 -1
  179. package/src/node/features/python.ts +63 -14
  180. package/src/node/features/repl.ts +11 -7
  181. package/src/node/features/runpod.ts +16 -3
  182. package/src/node/features/secure-shell.ts +27 -2
  183. package/src/node/features/semantic-search.ts +12 -1
  184. package/src/node/features/ui.ts +5 -69
  185. package/src/node/features/vm.ts +17 -0
  186. package/src/node/features/window-manager.ts +68 -20
  187. package/src/node.ts +5 -0
  188. package/src/scaffolds/generated.ts +492 -290
  189. package/src/scaffolds/template.ts +9 -0
  190. package/src/schemas/base.ts +46 -5
  191. package/src/selector.ts +282 -0
  192. package/src/server.ts +11 -0
  193. package/src/servers/express.ts +27 -12
  194. package/src/servers/socket.ts +45 -11
  195. package/src/web/clients/socket.ts +4 -1
  196. package/src/web/container.ts +2 -1
  197. package/src/web/features/network.ts +7 -1
  198. package/src/web/features/voice-recognition.ts +16 -1
  199. package/test/clients-servers.test.ts +2 -1
  200. package/test/command.test.ts +267 -0
  201. package/test/vm-context.test.ts +146 -0
  202. package/test-integration/assistants-manager.test.ts +10 -20
  203. package/docs/apis/features/node/launcher-app-command-listener.md +0 -145
  204. package/docs/examples/launcher-app-command-listener.md +0 -120
  205. package/docs/tasks/web-container-helper-discovery.md +0 -71
  206. package/docs/todos.md +0 -1
  207. package/scripts/test-command-listener.ts +0 -123
  208. package/src/node/features/launcher-app-command-listener.ts +0 -389
@@ -13,8 +13,41 @@ export const argsSchema = CommandOptionsSchema.extend({
13
13
  safe: z.boolean().default(false).describe('Require approval before each code block (markdown mode)'),
14
14
  console: z.boolean().default(false).describe('Start an interactive REPL after executing a markdown file, with all accumulated context'),
15
15
  onlySections: z.string().optional().describe('Comma-separated list of section headings to run (case-insensitive, markdown only)'),
16
+ dontInjectContext: z.boolean().default(false).describe('Skip auto-injecting container context into scripts (run with plain bun instead)'),
16
17
  })
17
18
 
19
+ /**
20
+ * Convert esbuild ESM output imports/exports to CJS require/module.exports
21
+ * so the code can run in a vm context that provides `require`.
22
+ */
23
+ function esmToCjs(code: string): string {
24
+ return code
25
+ // import { a, b } from 'x' → const { a, b } = require('x')
26
+ .replace(/^import\s+\{([^}]+)\}\s+from\s+(['"][^'"]+['"])\s*;?$/gm,
27
+ 'const {$1} = require($2);')
28
+ // import x from 'y' → const x = require('y').default ?? require('y')
29
+ .replace(/^import\s+(\w+)\s+from\s+(['"][^'"]+['"])\s*;?$/gm,
30
+ 'const $1 = require($2).default ?? require($2);')
31
+ // import * as x from 'y' → const x = require('y')
32
+ .replace(/^import\s+\*\s+as\s+(\w+)\s+from\s+(['"][^'"]+['"])\s*;?$/gm,
33
+ 'const $1 = require($2);')
34
+ // import 'y' → require('y')
35
+ .replace(/^import\s+(['"][^'"]+['"])\s*;?$/gm,
36
+ 'require($1);')
37
+ // export default → module.exports.default =
38
+ .replace(/^export\s+default\s+/gm, 'module.exports.default = ')
39
+ // export { ... } → strip (vars already in scope)
40
+ .replace(/^export\s+\{[^}]*\}\s*;?$/gm, '')
41
+ // export const/let/var → const/let/var
42
+ .replace(/^export\s+(const|let|var)\s+/gm, '$1 ')
43
+ }
44
+
45
+ function hasTLA(code: string): boolean {
46
+ // Quick check: contains await outside of async function bodies
47
+ // This is a heuristic — wrapTopLevelAwait does the real work
48
+ return /\bawait\b/.test(code) && !/^\s*\(?\s*async\b/.test(code)
49
+ }
50
+
18
51
  function resolveScript(ref: string, context: ContainerContext): string | null {
19
52
  const container = context.container as any
20
53
  const candidates = [
@@ -87,6 +120,7 @@ async function runMarkdown(scriptPath: string, options: z.infer<typeof argsSchem
87
120
  setTimeout, clearTimeout, setInterval, clearInterval,
88
121
  fetch, URL, URLSearchParams,
89
122
  ...container.context,
123
+ $doc: doc
90
124
  })
91
125
 
92
126
  // ─── Parse and register ## Blocks section ──────────────────────────
@@ -173,20 +207,51 @@ async function runMarkdown(scriptPath: string, options: z.infer<typeof argsSchem
173
207
  return shared
174
208
  }
175
209
 
176
- async function runScript(scriptPath: string, context: ContainerContext) {
210
+ async function runScript(scriptPath: string, context: ContainerContext, options: { dontInjectContext?: boolean } = {}) {
177
211
  const container = context.container as any
178
212
 
179
- const { exitCode, stderr } = await container.proc.execAndCapture(`bun run ${scriptPath}`, {
180
- onOutput: (data: string) => process.stdout.write(data),
181
- onError: (data: string) => process.stderr.write(data),
182
- })
213
+ if (options.dontInjectContext) {
214
+ const { exitCode, stderr } = await container.proc.execAndCapture(`bun run ${scriptPath}`, {
215
+ onOutput: (data: string) => process.stdout.write(data),
216
+ onError: (data: string) => process.stderr.write(data),
217
+ })
218
+
219
+ if (exitCode === 0) return
183
220
 
184
- if (exitCode === 0) return
221
+ console.error(`\nScript failed with exit code ${exitCode}.\n`)
222
+ if (stderr.length) {
223
+ console.error(stderr)
224
+ }
225
+ return
226
+ }
227
+
228
+ const vm = container.feature('vm')
229
+ const esbuild = container.feature('esbuild')
230
+ const raw = container.fs.readFile(scriptPath)
231
+
232
+ let code: string
233
+ if (hasTLA(raw)) {
234
+ // TLA is incompatible with CJS format, so transform as ESM (preserves await)
235
+ // then convert import/export statements to require/module.exports for the vm
236
+ const { code: esm } = esbuild.transformSync(raw, { format: 'esm' })
237
+ code = esmToCjs(esm)
238
+ } else {
239
+ const { code: cjs } = esbuild.transformSync(raw, { format: 'cjs' })
240
+ code = cjs
241
+ }
185
242
 
186
- console.error(`\nScript failed with exit code ${exitCode}.\n`)
187
- if (stderr.length) {
188
- console.error(stderr)
243
+ const ctx = {
244
+ require: vm.createRequireFor(scriptPath),
245
+ exports: {},
246
+ module: { exports: {} },
247
+ console,
248
+ setTimeout, setInterval, clearTimeout, clearInterval,
249
+ process, Buffer, URL, URLSearchParams,
250
+ fetch,
251
+ ...container.context,
189
252
  }
253
+
254
+ await vm.run(code, ctx)
190
255
  }
191
256
 
192
257
  async function diagnoseError(_scriptPath: string, error: Error, _context: ContainerContext) {
@@ -236,7 +301,7 @@ export default async function run(options: z.infer<typeof argsSchema>, context:
236
301
  })
237
302
  }
238
303
  } else {
239
- await runScript(scriptPath, context)
304
+ await runScript(scriptPath, context, { dontInjectContext: options.dontInjectContext })
240
305
  }
241
306
  } catch (err: any) {
242
307
  await diagnoseError(scriptPath, err instanceof Error ? err : new Error(String(err)), context)
@@ -0,0 +1,49 @@
1
+ import { z } from 'zod'
2
+ import { commands } from '../command.js'
3
+ import { CommandOptionsSchema } from '../schemas/base.js'
4
+ import type { ContainerContext } from '../container.js'
5
+
6
+ declare module '../command.js' {
7
+ interface AvailableCommands {
8
+ introspect: ReturnType<typeof commands.registerHandler>
9
+ }
10
+ }
11
+
12
+ export const argsSchema = CommandOptionsSchema.extend({
13
+ outputPath: z.string().default('docs/luca').describe('The path to save generated API docs to')
14
+ })
15
+
16
+ export async function apiDocs(options: z.infer<typeof argsSchema>, context: ContainerContext) {
17
+ const { container } = context
18
+ await container.helpers.discoverAll()
19
+ const outputFolder = options.outputPath ? container.paths.resolve(options.outputPath) : container.paths.resolve('docs','luca')
20
+
21
+ await container.fs.ensureFolder(
22
+ outputFolder
23
+ )
24
+
25
+ const mkPath = (...args) => container.paths.resolve(outputFolder, ...args)
26
+
27
+ const result = await container.fs.writeFileAsync(mkPath('agi-container.md'), container.inspectAsText())
28
+
29
+ for(let reg of ['features','clients','servers']) {
30
+ const helperIds = container[reg].available
31
+ const folder = mkPath(reg)
32
+ await container.fs.ensureFolder(folder)
33
+
34
+ await Promise.all(
35
+ helperIds.map((helperId) => container.fs.writeFileAsync(
36
+ container.paths.resolve(folder, `${helperId}.md`),
37
+ container[reg].describe(helperId)
38
+ ))
39
+ )
40
+ }
41
+
42
+ container.ui.print.green(`Finished saving API Docs`)
43
+ }
44
+
45
+ commands.registerHandler('api-docs', {
46
+ description: 'Save the helper introspection() content as markdown API docs in docs/luca',
47
+ argsSchema,
48
+ handler: apiDocs,
49
+ })
@@ -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