@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
package/docs/models.ts CHANGED
@@ -7,16 +7,8 @@ import {
7
7
  z,
8
8
  } from "contentbase";
9
9
 
10
- export const Idea = defineModel("Idea", {
11
- prefix: "ideas",
12
- meta: z.object({
13
- goal: z.string().optional().describe("Slug of the goal this idea is aligned to"),
14
- tags: z.array(z.string()).default([]).describe("Arbitrary tags for categorizing the idea"),
15
- status: z.enum(["spark", "exploring", "parked", "promoted"]).default("spark").describe("spark is a new raw idea, exploring means actively thinking about it, parked means on hold, promoted means it became a plan"),
16
- }),
17
- });
18
-
19
10
  export const Tutorial = defineModel("Tutorial", {
11
+ description: 'Tutorials on how to compose things together to do neat things',
20
12
  prefix: "tutorials",
21
13
  meta: z.object({
22
14
  tags: z.array(z.string()).default([]).describe("Arbitrary tags for categorizing the tutorial"),
@@ -24,6 +16,7 @@ export const Tutorial = defineModel("Tutorial", {
24
16
  })
25
17
 
26
18
  export const Report = defineModel("Report", {
19
+ description: 'Used for e.g. documentation audits, usability audits for agents, or anything else long form project related',
27
20
  prefix: "reports",
28
21
  meta: z.object({
29
22
  tags: z.array(z.string()).default([]).describe("Arbitrary tags for categorizing the report"),
@@ -31,8 +24,18 @@ export const Report = defineModel("Report", {
31
24
  })
32
25
 
33
26
  export const Example = defineModel("Example", {
27
+ description: 'Examples of using various luca features',
34
28
  prefix: "examples",
35
29
  meta: z.object({
36
30
  tags: z.array(z.string()).default([]).describe("Arbitrary tags for categorizing the example"),
37
31
  }),
38
- })
32
+ })
33
+
34
+ export const Challenge = defineModel('Challenge', {
35
+ description: 'challenges are used by our evaluation suite to measure the quality of the introspection content and tool, as well as the SKILL.md that gets generated to help coding assistants work with the luca framework',
36
+ prefix: 'challenges',
37
+ meta: z.object({
38
+ difficulty: z.enum(['easy','medium','hard']).default('easy'),
39
+ maxTime: z.number().default(5).describe('Number of seconds max time limit default to 5 minutes')
40
+ }),
41
+ })
@@ -1,17 +1,18 @@
1
1
  # Luca's Philosophy
2
2
 
3
- > LUCA - Lightweight Universal Conversational Architecture
3
+ > LUCA - Lightweight Universal Conversational Architecture.
4
4
 
5
5
  If you open up a Developer console on a blank HTML page, you have the `window` object and the `document` object, and from those objects you have everything you need to build every web application you've ever interacted with, IN THEORY, without ever reloading the page.
6
6
 
7
7
  Obviously nobody builds applications this way in a single REPL, but in 2026 developers are people and Agent AIs collaborating on the same codebase in a bigger Read, Eval, Print, Loop that takes place across more computers and brains.
8
8
 
9
- Luca aims to provide a `container` object that gets you 60% of the way there before you write a line of application code. As the types of applications you build and who you build them for further narrow, the container grows to cover 95%.
9
+ Luca aims to provide a `container` object that gets you 60% of the way to having ANY kind of real world, production grade full stack software, before you ever write your own line of application code. As the types of applications you build and who you build them for further narrow, the container can grow to cover 95% of every boring but necessary component you need.
10
10
 
11
11
  How? By layering, the way you layer dockerfiles. The things which change least frequently are solved once and cached. The things which change more frequently live in their own isolated layer. That loop is smaller, quicker — the difference between a dockerfile that reinstalls the OS every time you change the HTML file, and one that doesn't.
12
12
 
13
13
  ## The Layer Model
14
-
14
+ > Cache to the max == CA$H to the max
15
+ >
15
16
  Think of it like docker layers:
16
17
 
17
18
  **Layer 1: Platform** — `NodeContainer`, `WebContainer`. Features that are universally applicable to any application in that runtime. Filesystem, networking, process management, event buses, observable state. You solve this once.
@@ -0,0 +1,170 @@
1
+ ---
2
+ tags:
3
+ - console
4
+ - hmr
5
+ - repl
6
+ - design
7
+ ---
8
+ # Console HMR Design Research
9
+
10
+ Add a `--hmr` flag to `luca console` that watches source files and hot-swaps feature instances in the live REPL session, preserving state where possible.
11
+
12
+ ## How the Console Works Today
13
+
14
+ The `luca console` command (`src/commands/console.ts`) creates a REPL that:
15
+
16
+ 1. Calls `container.helpers.discoverAll()` to load all features, commands, endpoints
17
+ 2. Snapshots every available feature into a `featureContext` object via `container.feature(name)` for each name
18
+ 3. Optionally loads a `luca.console.ts` project module and merges its exports
19
+ 4. Optionally runs `--eval` code/script/markdown before the REPL starts
20
+ 5. Creates a `Repl` feature instance with a `vm.Context` built from the snapshot
21
+ 6. Enters a hand-rolled readline loop (`repl.ts`) that evaluates expressions in that VM context
22
+
23
+ The REPL's `_vmContext` is a **mutable plain object** — variables can be reassigned at runtime (`ctx.featureName = newInstance`). Tab completion reads from `Object.keys(ctx)` dynamically, so new/replaced bindings are immediately visible.
24
+
25
+ ## Architectural Facts Relevant to HMR
26
+
27
+ ### helperCache is module-private
28
+
29
+ `container.ts:618` — `const helperCache = new Map()`. There is no public API to evict or replace a cached feature instance. Calling `container.feature('fs')` with the same options always returns the same object. Any HMR implementation needs either:
30
+ - A new `container.evictHelper(cacheKey)` method
31
+ - A bypass that creates instances outside the cache
32
+
33
+ ### attachToContainer uses configurable: true
34
+
35
+ `feature.ts` — `Object.defineProperty(this.container, shortcutName, { get: () => this, configurable: true })`. The property descriptor **can** be redefined, which is the only existing affordance for swapping a feature on the container object.
36
+
37
+ ### vm.loadModule() always reads fresh from disk
38
+
39
+ `vm.ts` — reads file content via `container.fs.readFile()`, transpiles with esbuild `transformSync`, runs in a `vm.Script` context. No module cache to bust. But this runs in a CJS-like VM sandbox — real ES `import` statements inside the file won't work.
40
+
41
+ ### Bun import() cache busting
42
+
43
+ The only cache-busting pattern in the codebase is `Endpoint.reload()` (`endpoint.ts:176`): `import(\`${path}?t=${Date.now()}\`)`. This works for Bun's native module loader and preserves real ES module semantics.
44
+
45
+ ### State has no serialize/deserialize
46
+
47
+ `State` (`state.ts`) is an in-memory observable key-value bag with `set()`, `setState()`, `clear()`, and observer callbacks. There is no `toJSON()`/`fromSnapshot()` API. Transferring state between instances requires manually reading `state.current` from old and calling `state.setState()` on new.
48
+
49
+ ### FileManager has chokidar file watching
50
+
51
+ `file-manager.ts` — `fileManager.watch()` uses chokidar and emits `"file:change"` events with `{ type, path }`. Currently not wired to anything automatically. This is the primitive we'd compose for watching source files.
52
+
53
+ ### Feature self-registration is a static side effect
54
+
55
+ Features register via `static { Feature.register(this, 'name') }` which stores the **class constructor** in a module-level `FeaturesRegistry` Map. Re-importing a module would call `register()` again with a new constructor — the registry would need to handle overwrites.
56
+
57
+ ## The HMR Flow (Conceptual)
58
+
59
+ ```
60
+ [file change detected]
61
+ → identify which feature(s) the file maps to
62
+ → re-import the module (cache-busted)
63
+ → new class constructor registers over old one
64
+ → snapshot old instance state: state.current + any serializable instance data
65
+ → evict old instance from helperCache
66
+ → create new instance via container.feature(name)
67
+ → transfer state: newInstance.state.setState(oldState)
68
+ → patch REPL vm context: ctx[featureName] = newInstance
69
+ → re-define container shortcut property to point to new instance
70
+ → print "[HMR] Reloaded: featureName" in REPL
71
+ ```
72
+
73
+ ## Open Design Questions
74
+
75
+ ### 1. Scope — which files trigger a reload?
76
+
77
+ - Just feature source files (`src/node/features/*.ts`)?
78
+ - Also command files, `luca.console.ts`, endpoint files?
79
+ - Or anything under `src/`?
80
+
81
+ Recommendation: Start with feature files only. Commands and endpoints are less stateful and easier to add later.
82
+
83
+ ### 2. State transfer strategy
84
+
85
+ - **Best-effort `state.current` transfer**: Read `oldInstance.state.current`, call `newInstance.state.setState(snapshot)`. Simple, covers most cases.
86
+ - **Opt-in hooks**: Features declare `serialize()` / `deserialize()` methods for fine-grained control (e.g., FileManager could note which directories it was watching but not try to transfer the chokidar FSWatcher handle).
87
+ - **Hybrid**: Always transfer `state.current`, and if the feature has a `hmrSerialize()` hook, use that for additional instance data.
88
+
89
+ Non-state instance data (open file handles, chokidar watchers, readline interfaces, cached esbuild services) cannot be naively transferred. Features with complex resources would need explicit HMR support or accept that those resources restart fresh.
90
+
91
+ ### 3. Module re-import strategy
92
+
93
+ Two options:
94
+
95
+ **Option A — Bun `import()` with `?t=` cache busting**: Real ES module semantics, `import` statements inside the feature file work. The new module's `static {}` block re-registers the class. This is what `Endpoint.reload()` already does.
96
+
97
+ **Option B — `vm.loadModule()`**: Always reads fresh from disk, no cache issues. But runs in a CJS sandbox — internal `import` statements won't resolve. Feature files heavily use `import`, so this likely won't work.
98
+
99
+ Recommendation: Option A. It's proven in the codebase and preserves full module semantics.
100
+
101
+ ### 4. Registry re-registration
102
+
103
+ `Feature.register()` currently does `features.register(id, SubClass)` which calls `registry.members.set(id, SubClass)`. A re-import with a new class constructor would overwrite the old entry. This actually works — `Map.set` overwrites silently. But we should verify there are no side effects in `interceptRegistration` hooks or other registration logic that would break.
104
+
105
+ ### 5. REPL context patching — automatic vs explicit
106
+
107
+ - **Automatic**: FileManager watches, detects change, swaps feature, patches `ctx[name]`, prints HMR message. User sees updated behavior on next expression.
108
+ - **Explicit**: User types `hmr.reload('fs')` or similar in the REPL to trigger a reload manually.
109
+ - **Both**: Auto-reload on file change, plus a manual `hmr.reload('name')` for forcing reloads or reloading things that aren't file-backed.
110
+
111
+ Recommendation: Both. Auto is the main UX, manual is the escape hatch.
112
+
113
+ ### 6. Feedback in the REPL
114
+
115
+ Print a colored message when a feature reloads:
116
+ ```
117
+ [HMR] Reloaded: fs (state transferred)
118
+ [HMR] Reloaded: diskCache (fresh — no prior state)
119
+ [HMR] Error reloading vm: SyntaxError: Unexpected token (kept old instance)
120
+ ```
121
+
122
+ This should be non-intrusive — printed above the prompt line if possible.
123
+
124
+ ### 7. Failure mode
125
+
126
+ If the new code has a syntax error or the constructor throws:
127
+ - Keep the old instance alive
128
+ - Print the error in the REPL
129
+ - Do not crash the session
130
+
131
+ This is the only sane approach for a dev tool.
132
+
133
+ ## Implementation Sketch
134
+
135
+ ### New infrastructure needed
136
+
137
+ 1. **`container.evictHelper(type, id, options?)`** — public method on Container that deletes from `helperCache` and cleans up `featureIdToHelperCacheKeyMap` and `contextMap`
138
+ 2. **`Feature.prototype.hmrSerialize?()` / `hmrDeserialize?(data)`** — optional hooks for features that need custom state transfer beyond `state.current`
139
+ 3. **`registry.register()` handling overwrites** — verify this works cleanly, add a `"re-registered"` event if useful
140
+ 4. **File-to-feature mapping** — a way to know that `src/node/features/disk-cache.ts` corresponds to the `diskCache` feature ID
141
+
142
+ ### Changes to existing code
143
+
144
+ 1. **`src/commands/console.ts`** — add `--hmr` flag to `argsSchema`, wire up file watching and the reload loop when enabled
145
+ 2. **`src/container.ts`** — expose `evictHelper()` (or a more targeted `replaceFeature()`)
146
+ 3. **`src/node/features/repl.ts`** — expose a method to patch the VM context (or just expose `_vmContext` which is already accessible)
147
+
148
+ ### Rough dependency graph
149
+
150
+ ```
151
+ argsSchema adds --hmr flag
152
+ → console handler checks for --hmr
153
+ → starts FileManager.watch() on src/ directory
154
+ → subscribes to "file:change" events
155
+ → on change: resolveFeatureFromPath(changedFile)
156
+ → cache-bust import the module
157
+ → container.evictHelper('feature', featureId)
158
+ → newInstance = container.feature(featureId, { enable: wasEnabled })
159
+ → transfer state from old → new
160
+ → patch repl._vmContext[featureId] = newInstance
161
+ → print HMR message
162
+ ```
163
+
164
+ ## Risks and Unknowns
165
+
166
+ - **Circular dependency during re-import**: If feature A imports feature B at module level, and both are being reloaded, the order matters. May need to batch reloads or do a dependency-aware reload order.
167
+ - **Event listener cleanup**: Old feature instances may have registered listeners on the container event bus. Need to remove those or they'll fire on stale instances.
168
+ - **Observer cleanup**: State observers from the old instance need to be unsubscribed or they'll leak.
169
+ - **Features that modify globals**: Some features might set up global state (process event handlers, etc.) that won't be cleaned up by replacing the instance.
170
+ - **The `?t=` trick and TypeScript**: Bun handles `import('./foo.ts?t=123')` but we should verify this works for all feature files, especially those with complex re-exports.
@@ -0,0 +1,72 @@
1
+ ---
2
+ tags:
3
+ - feature-design
4
+ - semantic-search
5
+ - introspection
6
+ status: draft
7
+ ---
8
+ # Helper Semantic Search Feature
9
+
10
+ Build semantic search over all describable luca helpers so AI assistants can find the right feature/client/server by describing what they need.
11
+
12
+ ## Motivation
13
+
14
+ The `luca describe` system produces rich markdown for every helper (features, clients, servers, commands, endpoints, selectors). An AI assistant working with luca currently has to know the exact name of a helper to look it up. Semantic search would let it say "I need to run shell commands" and find `proc`, or "I need to cache things to disk" and find `diskCache`.
15
+
16
+ Storage location: `~/.luca/embeddings/`
17
+
18
+ ## What Already Exists
19
+
20
+ Two systems that combine perfectly:
21
+
22
+ 1. **SemanticSearch feature** (`src/node/features/semantic-search.ts`) — SQLite-backed embedding engine with OpenAI/local GGUF providers, section-based chunking, BM25 + vector + hybrid search with RRF fusion.
23
+
24
+ 2. **Introspection system** (`src/introspection/`) — Every helper produces structured `HelperIntrospection` JSON and rendered markdown via `Helper.introspectAsText()`. Build-time AST scanning (JSDoc) + runtime Zod schema reflection. The `__INTROSPECTION__` map holds everything.
25
+
26
+ ## Proposed Design
27
+
28
+ ### Approach
29
+
30
+ Reuse the existing `SemanticSearch` feature directly. Create a new feature (e.g. `helperSearch`) that:
31
+
32
+ 1. Iterates all registries, calls `introspectAsText()` on each helper to get markdown
33
+ 2. Structures the markdown as `DocumentInput` objects with sections (methods, getters, events, state, options)
34
+ 3. Feeds them to SemanticSearch for embedding and indexing
35
+ 4. Exposes a `search(query)` method that delegates to hybridSearch
36
+
37
+ ### Storage
38
+
39
+ DB stored at `~/.luca/embeddings/helpers.<provider>-<model>.sqlite`, scoped by provider+model like contentbase does.
40
+
41
+ ### When to Build Index
42
+
43
+ - Lazy on first search if no index exists or if stale
44
+ - Explicit rebuild via `luca search --rebuild`
45
+ - Content hash gating from SemanticSearch handles incremental updates automatically
46
+
47
+ ### CLI Surface
48
+
49
+ `luca search "file operations"` — returns ranked helpers with snippets showing why they matched.
50
+
51
+ ### MCP / AI Assistant Surface
52
+
53
+ Expose as a tool in the luca-sandbox MCP so AI assistants can search for helpers by describing what they need.
54
+
55
+ ## Open Questions
56
+
57
+ 1. **Scope** — Index just core luca helpers, or also project-level commands/endpoints/selectors discovered at runtime? Suggestion: core always, project-level optionally.
58
+
59
+ 2. **Granularity** — One document per helper (full describe output) vs chunked by section (methods, events, state). Suggestion: chunk by section so "run shell commands" matches `proc.exec` specifically.
60
+
61
+ 3. **Primary consumer** — MCP tool for AI assistants, CLI for humans, or both? Suggestion: both.
62
+
63
+ 4. **Embedding provider default** — OpenAI (higher quality, needs API key) or local GGUF (works offline, lower quality)? Suggestion: OpenAI default with local fallback.
64
+
65
+ ## Key Files
66
+
67
+ - `src/node/features/semantic-search.ts` — The embedding engine to reuse
68
+ - `src/introspection/index.ts` — `__INTROSPECTION__` map, `HelperIntrospection` type
69
+ - `src/helper.ts` — `Helper.introspect()`, `Helper.introspectAsText()`, markdown renderers
70
+ - `src/registry.ts` — Registry base class, `describe()`, `describeAll()`
71
+ - `src/commands/describe.ts` — Current describe command (target resolution, rendering)
72
+ - `src/node/features/content-db.ts` — Reference for how contentDb wraps SemanticSearch
@@ -11,9 +11,8 @@ When to build a client:
11
11
 
12
12
  ```ts
13
13
  import { z } from 'zod'
14
- import { Client, clients, RestClient } from '@soederpop/luca/client'
14
+ import { Client, RestClient } from '@soederpop/luca/client'
15
15
  import { ClientStateSchema, ClientOptionsSchema, ClientEventsSchema } from '@soederpop/luca'
16
- import type { ContainerContext } from '@soederpop/luca'
17
16
  ```
18
17
 
19
18
  Use `RestClient` for HTTP APIs (most common). It gives you `get`, `post`, `put`, `patch`, `delete` methods that handle JSON, headers, and error wrapping.
@@ -36,6 +35,8 @@ export type {{PascalName}}Options = z.infer<typeof {{PascalName}}OptionsSchema>
36
35
 
37
36
  ## Class
38
37
 
38
+ 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}}`.
39
+
39
40
  ```ts
40
41
  /**
41
42
  * {{description}}
@@ -51,14 +52,14 @@ export class {{PascalName}} extends RestClient<{{PascalName}}State, {{PascalName
51
52
  static override shortcut = 'clients.{{camelName}}' as const
52
53
  static override stateSchema = {{PascalName}}StateSchema
53
54
  static override optionsSchema = {{PascalName}}OptionsSchema
54
- static override description = '{{description}}'
55
-
56
- constructor(options: {{PascalName}}Options, context: ContainerContext) {
57
- options = {
58
- ...options,
59
- baseURL: options.baseURL || 'https://api.example.com',
60
- }
61
- super(options, context)
55
+ static { Client.register(this, '{{camelName}}') }
56
+
57
+ /**
58
+ * Called after the client is initialized. Use this for any setup logic
59
+ * instead of overriding the constructor.
60
+ */
61
+ async afterInitialize() {
62
+ // Set up default headers, configure auth, etc.
62
63
  }
63
64
 
64
65
  // Add API methods here. Each wraps an endpoint.
@@ -69,6 +70,8 @@ export class {{PascalName}} extends RestClient<{{PascalName}}State, {{PascalName
69
70
  }
70
71
  ```
71
72
 
73
+ **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.
74
+
72
75
  ## Module Augmentation
73
76
 
74
77
  ```ts
@@ -81,17 +84,22 @@ declare module '@soederpop/luca/client' {
81
84
 
82
85
  ## Registration
83
86
 
87
+ Registration happens inside the class body using a static block. The default export is just the class itself.
88
+
84
89
  ```ts
85
- export default clients.register('{{camelName}}', {{PascalName}})
90
+ // Inside the class:
91
+ static { Client.register(this, '{{camelName}}') }
92
+
93
+ // At module level:
94
+ export default {{PascalName}}
86
95
  ```
87
96
 
88
97
  ## Complete Example
89
98
 
90
99
  ```ts
91
100
  import { z } from 'zod'
92
- import { clients, RestClient } from '@soederpop/luca/client'
101
+ import { Client, RestClient } from '@soederpop/luca/client'
93
102
  import { ClientStateSchema, ClientOptionsSchema } from '@soederpop/luca'
94
- import type { ContainerContext } from '@soederpop/luca'
95
103
 
96
104
  declare module '@soederpop/luca/client' {
97
105
  interface AvailableClients {
@@ -121,20 +129,21 @@ export class {{PascalName}} extends RestClient<{{PascalName}}State, {{PascalName
121
129
  static override shortcut = 'clients.{{camelName}}' as const
122
130
  static override stateSchema = {{PascalName}}StateSchema
123
131
  static override optionsSchema = {{PascalName}}OptionsSchema
124
- static override description = '{{description}}'
132
+ static { Client.register(this, '{{camelName}}') }
125
133
 
126
- constructor(options: {{PascalName}}Options, context: ContainerContext) {
127
- super({ ...options, baseURL: options.baseURL }, context)
134
+ async afterInitialize() {
135
+ // Setup logic goes here — not in the constructor
128
136
  }
129
137
  }
130
138
 
131
- export default clients.register('{{camelName}}', {{PascalName}})
139
+ export default {{PascalName}}
132
140
  ```
133
141
 
134
142
  ## Conventions
135
143
 
136
144
  - **Extend RestClient for HTTP**: It gives you typed HTTP methods. Only use base `Client` if you need a non-HTTP protocol.
137
- - **Set baseURL in constructor**: Override options to hardcode or default the API base URL.
145
+ - **Set baseURL via options schema**: Use a Zod `.default()` on the `baseURL` field rather than overriding the constructor.
146
+ - **Use `afterInitialize()`**: For any setup logic (auth, default headers, etc.) instead of overriding the constructor.
138
147
  - **Wrap endpoints as methods**: Each API endpoint gets a method. Keep them thin — just map to HTTP calls.
139
- - **JSDoc everything**: Every public method needs `@param`, `@returns`, `@example`.
140
- - **Auth in options**: Pass API keys, tokens via options schema. Check them in the constructor or a setup method.
148
+ - **JSDoc everything**: Every public method needs `@param`, `@returns`, `@example`. Run `luca introspect` after changes to update generated docs.
149
+ - **Auth in options**: Pass API keys, tokens via options schema. Check them in `afterInitialize()` or a setup method.
@@ -1,6 +1,6 @@
1
1
  # Building a Command
2
2
 
3
- 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.
3
+ 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.
4
4
 
5
5
  When to build a command:
6
6
  - You need a CLI task for a project (build scripts, generators, automation)
@@ -11,60 +11,54 @@ When to build a command:
11
11
 
12
12
  ```ts
13
13
  import { z } from 'zod'
14
- import { commands, CommandOptionsSchema } from '@soederpop/luca'
15
14
  import type { ContainerContext } from '@soederpop/luca'
16
15
  ```
17
16
 
18
- ## Args Schema
17
+ ## Positional Arguments
19
18
 
20
- Define your command's arguments and flags. Extend `CommandOptionsSchema` which gives you `_` (positional args) and `name` for free.
19
+ 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.
21
20
 
22
21
  ```ts
23
- export const argsSchema = CommandOptionsSchema.extend({
24
- // Add your flags here. Each becomes a --flag on the CLI.
25
- // Example: verbose: z.boolean().default(false).describe('Enable verbose output'),
26
- // Example: output: z.string().optional().describe('Output file path'),
27
- })
22
+ // luca {{kebabName}} ./src => options.target === './src'
23
+ export const positionals = ['target']
28
24
  ```
29
25
 
30
- ## Handler
26
+ ## Args Schema
31
27
 
32
- The handler function receives parsed options and the container context. Use the container for all I/O.
28
+ 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.
33
29
 
34
30
  ```ts
35
- export default async function {{camelName}}(options: z.infer<typeof argsSchema>, context: ContainerContext) {
36
- const container = context.container as any
37
- const fs = container.feature('fs')
38
- const args = container.argv._ as string[]
31
+ export const argsSchema = z.object({
32
+ // Positional: first arg after command name (via positionals array above)
33
+ // target: z.string().optional().describe('The target to operate on'),
39
34
 
40
- // args[0] is your command name, args[1+] are positional arguments
41
- // options contains parsed --flags
42
-
43
- // Your implementation here
44
- }
35
+ // Flags: passed as --flag on the CLI
36
+ // verbose: z.boolean().default(false).describe('Enable verbose output'),
37
+ // output: z.string().optional().describe('Output file path'),
38
+ })
45
39
  ```
46
40
 
47
- ## Registration
41
+ ## Description
48
42
 
49
- Register the command at the bottom of the file. The `description` shows up in `luca --help`.
43
+ Export a description string for `luca --help` display:
50
44
 
51
45
  ```ts
52
- commands.registerHandler('{{camelName}}', {
53
- description: '{{description}}',
54
- argsSchema,
55
- handler: {{camelName}},
56
- })
46
+ export const description = '{{description}}'
57
47
  ```
58
48
 
59
- ## Module Augmentation
49
+ ## Handler
60
50
 
61
- Optional but gives TypeScript autocomplete for `commands.lookup('yourCommand')`.
51
+ 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`.
62
52
 
63
53
  ```ts
64
- declare module '@soederpop/luca' {
65
- interface AvailableCommands {
66
- {{camelName}}: ReturnType<typeof commands.registerHandler>
67
- }
54
+ export default async function {{camelName}}(options: z.infer<typeof argsSchema>, context: ContainerContext) {
55
+ const { container } = context
56
+ const fs = container.feature('fs')
57
+
58
+ // options.target is set from the first positional arg (via positionals export)
59
+ // options.verbose, options.output, etc. come from --flags
60
+
61
+ // Your implementation here
68
62
  }
69
63
  ```
70
64
 
@@ -72,35 +66,55 @@ declare module '@soederpop/luca' {
72
66
 
73
67
  ```ts
74
68
  import { z } from 'zod'
75
- import { commands, CommandOptionsSchema } from '@soederpop/luca'
76
69
  import type { ContainerContext } from '@soederpop/luca'
77
70
 
78
- declare module '@soederpop/luca' {
79
- interface AvailableCommands {
80
- {{camelName}}: ReturnType<typeof commands.registerHandler>
81
- }
82
- }
71
+ export const description = '{{description}}'
83
72
 
84
- export const argsSchema = CommandOptionsSchema.extend({})
73
+ // Map positional args to named options: luca {{kebabName}} myTarget => options.target === 'myTarget'
74
+ export const positionals = ['target']
75
+
76
+ export const argsSchema = z.object({
77
+ target: z.string().optional().describe('The target to operate on'),
78
+ })
85
79
 
86
80
  export default async function {{camelName}}(options: z.infer<typeof argsSchema>, context: ContainerContext) {
87
- const container = context.container as any
81
+ const { container } = context
88
82
  const fs = container.feature('fs')
89
83
 
90
- console.log('{{camelName}} running...')
84
+ console.log('{{kebabName}} running...', options.target)
91
85
  }
86
+ ```
92
87
 
93
- commands.registerHandler('{{camelName}}', {
94
- description: '{{description}}',
95
- argsSchema,
96
- handler: {{camelName}},
97
- })
88
+ ## Container Properties
89
+
90
+ The `context.container` object provides useful properties beyond features:
91
+
92
+ ```ts
93
+ export default async function {{camelName}}(options: z.infer<typeof argsSchema>, context: ContainerContext) {
94
+ const { container } = context
95
+
96
+ // Current working directory
97
+ container.cwd // '/path/to/project'
98
+
99
+ // Path utilities (scoped to cwd)
100
+ container.paths.resolve('src') // '/path/to/project/src'
101
+ container.paths.join('a', 'b') // '/path/to/project/a/b'
102
+ container.paths.relative('src') // 'src'
103
+
104
+ // Package manifest (parsed package.json)
105
+ container.manifest.name // 'my-project'
106
+ container.manifest.version // '1.0.0'
107
+
108
+ // Raw CLI arguments (from minimist) — prefer positionals export for positional args
109
+ container.argv // { _: ['{{kebabName}}', ...], verbose: true, ... }
110
+ }
98
111
  ```
99
112
 
100
113
  ## Conventions
101
114
 
102
- - **File location**: `commands/{{camelName}}.ts` in the project root. The `luca` CLI discovers these automatically.
103
- - **Naming**: camelCase for both file and registration ID. `luca my-command` maps to `commands/my-command.ts`.
115
+ - **File location**: `commands/{{kebabName}}.ts` in the project root. The `luca` CLI discovers these automatically.
116
+ - **Naming**: kebab-case for filename. `luca {{kebabName}}` maps to `commands/{{kebabName}}.ts`.
104
117
  - **Use the container**: Never import `fs`, `path`, `child_process` directly. Use `container.feature('fs')`, `container.paths`, `container.feature('proc')`.
105
- - **Positional args**: Access via `container.argv._` — it's an array where `_[0]` is the command name.
118
+ - **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.
106
119
  - **Exit codes**: Return nothing for success. Throw for errors — the CLI catches and reports them.
120
+ - **Help text**: Use `.describe()` on every schema field — it powers `luca {{kebabName}} --help`.