@soederpop/luca 0.0.3 → 0.0.4

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 (113) hide show
  1. package/AGENTS.md +98 -0
  2. package/CLAUDE.md +27 -0
  3. package/SPEC.md +304 -0
  4. package/bun.lock +110 -265
  5. package/docs/CLI.md +1 -1
  6. package/docs/apis/features/node/content-db.md +16 -0
  7. package/docs/apis/features/node/fs.md +24 -0
  8. package/docs/apis/features/node/ipc-socket.md +0 -1
  9. package/docs/apis/features/node/package-finder.md +1 -11
  10. package/docs/apis/features/node/proc.md +0 -41
  11. package/docs/apis/features/node/ui.md +0 -2
  12. package/package.json +12 -8
  13. package/src/agi/container.server.ts +16 -3
  14. package/src/agi/features/assistant.ts +3 -7
  15. package/src/agi/features/assistants-manager.ts +3 -7
  16. package/src/agi/features/claude-code.ts +3 -7
  17. package/src/agi/features/conversation-history.ts +3 -7
  18. package/src/agi/features/conversation.ts +4 -8
  19. package/src/agi/features/openai-codex.ts +3 -7
  20. package/src/agi/features/openapi.ts +4 -2
  21. package/src/agi/features/skills-library.ts +4 -8
  22. package/src/cli/cli.ts +22 -0
  23. package/src/client.ts +69 -26
  24. package/src/clients/civitai/index.ts +3 -7
  25. package/src/clients/comfyui/index.ts +5 -9
  26. package/src/clients/elevenlabs/index.ts +39 -19
  27. package/src/clients/openai/index.ts +3 -7
  28. package/src/clients/supabase/index.ts +4 -13
  29. package/src/commands/console.ts +0 -3
  30. package/src/commands/eval.ts +1 -1
  31. package/src/commands/index.ts +1 -0
  32. package/src/commands/introspect.ts +128 -0
  33. package/src/commands/prompt.ts +1 -4
  34. package/src/commands/run.ts +6 -13
  35. package/src/commands/sandbox-mcp.ts +1 -13
  36. package/src/feature.ts +45 -2
  37. package/src/introspection/generated.agi.ts +175 -101
  38. package/src/introspection/generated.node.ts +175 -101
  39. package/src/introspection/generated.web.ts +113 -29
  40. package/src/introspection/index.ts +1 -1
  41. package/src/introspection/scan.ts +3 -1
  42. package/src/node/features/container-link.ts +3 -2
  43. package/src/node/features/content-db.ts +10 -2
  44. package/src/node/features/disk-cache.ts +3 -4
  45. package/src/node/features/dns.ts +3 -2
  46. package/src/node/features/docker.ts +3 -2
  47. package/src/node/features/downloader.ts +3 -16
  48. package/src/node/features/esbuild.ts +3 -12
  49. package/src/node/features/file-manager.ts +3 -2
  50. package/src/node/features/fs.ts +12 -3
  51. package/src/node/features/git.ts +3 -2
  52. package/src/node/features/google-auth.ts +3 -2
  53. package/src/node/features/google-calendar.ts +3 -2
  54. package/src/node/features/google-docs.ts +3 -2
  55. package/src/node/features/google-drive.ts +3 -2
  56. package/src/node/features/google-sheets.ts +3 -2
  57. package/src/node/features/grep.ts +3 -2
  58. package/src/node/features/helpers.ts +13 -2
  59. package/src/node/features/ink.ts +3 -3
  60. package/src/node/features/ipc-socket.ts +3 -3
  61. package/src/node/features/json-tree.ts +3 -21
  62. package/src/node/features/launcher-app-command-listener.ts +3 -2
  63. package/src/node/features/networking.ts +3 -2
  64. package/src/node/features/nlp.ts +3 -2
  65. package/src/node/features/opener.ts +8 -7
  66. package/src/node/features/os.ts +3 -2
  67. package/src/node/features/package-finder.ts +3 -2
  68. package/src/node/features/port-exposer.ts +3 -4
  69. package/src/node/features/postgres.ts +3 -3
  70. package/src/node/features/proc.ts +37 -64
  71. package/src/node/features/process-manager.ts +3 -2
  72. package/src/node/features/python.ts +3 -3
  73. package/src/node/features/repl.ts +3 -2
  74. package/src/node/features/runpod.ts +3 -3
  75. package/src/node/features/secure-shell.ts +3 -2
  76. package/src/node/features/semantic-search.ts +4 -6
  77. package/src/node/features/sqlite.ts +3 -3
  78. package/src/node/features/telegram.ts +3 -2
  79. package/src/node/features/tts.ts +3 -2
  80. package/src/node/features/ui.ts +3 -3
  81. package/src/node/features/vault.ts +3 -14
  82. package/src/node/features/vm.ts +41 -3
  83. package/src/node/features/window-manager.ts +165 -22
  84. package/src/node/features/yaml-tree.ts +3 -4
  85. package/src/node/features/yaml.ts +3 -2
  86. package/src/registry.ts +1 -1
  87. package/src/scaffolds/generated.ts +1 -1
  88. package/src/server.ts +43 -0
  89. package/src/servers/express.ts +24 -8
  90. package/src/servers/mcp.ts +2 -6
  91. package/src/servers/socket.ts +22 -7
  92. package/src/web/clients/socket.ts +3 -5
  93. package/src/web/features/asset-loader.ts +20 -12
  94. package/src/web/features/container-link.ts +3 -6
  95. package/src/web/features/esbuild.ts +21 -7
  96. package/src/web/features/helpers.ts +4 -2
  97. package/src/web/features/network.ts +24 -7
  98. package/src/web/features/speech.ts +24 -7
  99. package/src/web/features/vault.ts +21 -3
  100. package/src/web/features/vm.ts +20 -13
  101. package/src/web/features/voice-recognition.ts +26 -9
  102. package/commands/update-introspection.ts +0 -67
  103. package/docs/ideas/class-registration-refactor-possibilities.md +0 -197
  104. package/docs/ideas/container-use-api.md +0 -9
  105. package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
  106. package/docs/ideas/feature-stacks.md +0 -22
  107. package/docs/ideas/luca-cli-self-sufficiency-demo.md +0 -23
  108. package/docs/ideas/mcp-design.md +0 -9
  109. package/docs/ideas/web-container-debugging-feature.md +0 -13
  110. package/scripts/animations/chrome-glitch.ts +0 -55
  111. package/scripts/animations/index.ts +0 -16
  112. package/scripts/animations/neon-pulse.ts +0 -64
  113. package/scripts/animations/types.ts +0 -6
package/AGENTS.md ADDED
@@ -0,0 +1,98 @@
1
+ # LUCA
2
+
3
+ Lightweight Universal Conversational Architecture. Runtime is bun.
4
+
5
+ The runtime is bun, that means no vitest.
6
+
7
+ Luca provides a system for building runtime `container` objects which provide server and browser applications with all of the dependencies they need to build complete applications. A `container` is a per process global singleton, event bus, state machine, and dependency injector. A `container` is either based on a node or browser runtime, and comes with features optimized for that environment. You can build your own container on top of it, with your own features, clients, servers. It is very much inspired by docker layer caching.
8
+
9
+ A `container` could be used for all "business logic" and state, and be a headless provider for an entire application. The UI, Scripting output, input, etc, are all just functional interfaces and event bindings to the core container and all of its helpers, and their state.
10
+
11
+ Dependencies consist of Helpers - Features, Clients, Servers, as well as primitives like event buses, observable state. The `container` contains registries of all available components: `container.features`, `container.clients`, `container.servers`, `container.commands`, `container.endpoints` as well as factory functions to create instances of them: `container.feature('fileManager')`, `container.server('express')`.
12
+
13
+ The `container` and its helpers are perfect for scripts and long running services on the backend, or highly reactive and stateful applications on the frontend. The components can easily talk to eachother, as the `container` on the server provides servers like `container.server('express')` and `container.server('websocket')` as well as `container.client('rest')` and `container.client('websocket')` and others.
14
+
15
+ On the frontend the browser container is perfect for highly reactive, stateful web applications, especially works well with React.
16
+
17
+ ## The `luca` CLI
18
+
19
+ - in dev, `bun run src/cli/cli.ts` is the same as `luca`
20
+
21
+ - in prod, or educational material, `luca` refers to the binary build. In this mode, it can work in any project, and load `commands/` and `endpoints/` through its VM and therefore allows folders of these modules which don't depend on anything from NPM to extend the CLI and be used in commands like `luca serve` to run a local express server
22
+
23
+ - The `luca` cli is an extremely helpful tool.
24
+ - it runs code `luca eval "container.features.available"`
25
+ - it generates docs:
26
+ - `luca describe diskCache`
27
+ - `luca describe` describe the container itself
28
+ - `luca describe servers` describe which servers are available
29
+ - the arguments to describe are pretty forgiving and permissive
30
+
31
+ **IMPORTANT NOTE** When trying to investigate features, clients, servers, etc, see if these tools can help you first instead of searching for files and reading them that way. If youw ant to understand what they do, vs how theyre actually implemented
32
+
33
+ ## Coding style and guidelines
34
+
35
+ - The container is intended to provide a collection of blessed, approved, audited modules that we've built and curated together. It is intended to be the primary API and interface through the system
36
+ - The container should provide you with everything you need, and you should not need to be importing dependencies or other modules. If you find yourself stuck by this constraint, raise this concern, and we can work on finding a way to bring in a feature or client
37
+ - When trying to find paths in the project, use `container.paths.resolve()` or `container.paths.join()` instead of `import { resolve } from 'path'`
38
+ - **NEVER import from `fs`, `path`, or other Node builtins when the container provides equivalents.** Use `container.feature('fs')` for file operations, `container.paths` for path operations. This applies to command handlers, scripts, and any code that has access to a container. The only exception is inside feature implementations themselves (e.g. `proc.ts`, `fs.ts`) where you ARE building the container primitive — those may use Node builtins directly since they can't depend on themselves.
39
+
40
+ ## Container Utilities
41
+
42
+ The container provides `container.utils` with common utilities. **Use these instead of importing packages directly** — they work in both node and web environments.
43
+
44
+ - `container.utils.uuid()` — generates a v4 UUID (use instead of importing `node-uuid` or `crypto`)
45
+ - `container.utils.hashObject(obj)` — deterministic hash of any object
46
+ - `container.utils.stringUtils` — `{ kebabCase, camelCase, upperFirst, lowerFirst, pluralize, singularize }`
47
+ - `container.utils.lodash` — `{ uniq, keyBy, uniqBy, groupBy, debounce, throttle, mapValues, mapKeys, pick, get, set, omit }`
48
+
49
+ Also available on every container:
50
+ - `container.uuid` — the container's own unique ID
51
+ - `container.paths.resolve()` / `container.paths.join()` — path operations
52
+
53
+ ## Adding a New Feature — Checklist
54
+
55
+ When creating a new feature (e.g. `gws`), all four of these steps must be completed or `container.feature('gws')` will fail silently or lack type safety:
56
+
57
+ 1. **Feature file** — `src/node/features/gws.ts`
58
+ - Export the class: `export class Gws extends Feature { ... }`
59
+ - Register at bottom: `export default features.register('gws', Gws)`
60
+
61
+ 2. **Side-effect import** — `src/node/container.ts` (import block ~line 20-63)
62
+ - Add `import "./features/gws";` (this triggers registration)
63
+
64
+ 3. **Type import + re-export** — `src/node/container.ts` (type imports ~line 65-148)
65
+ - Add `import type { Gws } from './features/gws';`
66
+ - Add `type Gws,` to the `export { ... }` block
67
+
68
+ 4. **Feature type mapping** — `src/node/container.ts` (`NodeFeatures` interface ~line 170-215)
69
+ - Add `gws: typeof Gws;` to the `NodeFeatures` interface
70
+
71
+ Missing step 2 = feature never registers (invisible).
72
+ Missing steps 3-4 = no autocomplete, `container.feature('gws')` returns `Feature` not `Gws`.
73
+
74
+ If the feature has a test, it goes in `test/gws.test.ts`.
75
+
76
+ ## Type Safety and Introspection
77
+
78
+ - Zod does a lot of the heavy lifting for us with its type inference
79
+ - For more descriptive things like class descriptions, method descriptions, we rely on jsdoc blocks. These are parsed and used to generate modules we commit to source. We shouldn't let these drift, so for this reason we have a pre-commit hook which ensures they're up to date
80
+ - We rely on module augmentation a lot to make sure `container.feature()` can provide type signatures for everything that gets added to it by extension modules down the road. ( kind of like we did with AGIContainer extending NodeContainer )
81
+
82
+ ## Testing
83
+
84
+ - Test runner is **bun** (not vitest). Do not import from or add vitest.
85
+ - `bun test` or `bun run test` — runs unit tests only (`test/*.test.ts`)
86
+ - `bun run test:integration` — runs integration tests in `test-integration/` that hit real APIs/CLIs (gated by env vars)
87
+ - Import `mock`, `spyOn` from `bun:test` when needed. If you import anything from `bun:test`, you must also import `describe`, `it`, `expect`, etc. from there (importing disables auto-globals).
88
+ - **ALL tests must pass. Zero tolerance for test failures.** The ESBuild feature's "service is no longer running" error is a known critical bug — if you encounter it, fix it. Do not ignore it, do not skip it, do not leave it broken. This applies to every test: if a test fails, that is a blocker. Fix the root cause.
89
+
90
+ ## API Docs
91
+
92
+ - See [docs/apis](./docs/apis/) for detailed API descriptions of the public methods and options for creating various helpers
93
+ - See [docs/examples](./docs/examples/) for examples of using each feature. NOTE: These docs are runnable so you can see the output of the code blocks. `luca run docs/examples/grep` for example
94
+ - See [docs/tutorials](./docs/tutorials/) for longer form tutorials on various subjects and best practices
95
+
96
+ ## Git Strategy
97
+
98
+ - We generally roll all on main. Commit your changes after you're done, only your changes. Leave a good message, tell me why don't just tell me what. Don't gimme that coauthored by whoever bullshit. The streets know we're one.
package/CLAUDE.md CHANGED
@@ -50,6 +50,29 @@ Also available on every container:
50
50
  - `container.uuid` — the container's own unique ID
51
51
  - `container.paths.resolve()` / `container.paths.join()` — path operations
52
52
 
53
+ ## Adding a New Feature — Checklist
54
+
55
+ When creating a new feature (e.g. `gws`), all four of these steps must be completed or `container.feature('gws')` will fail silently or lack type safety:
56
+
57
+ 1. **Feature file** — `src/node/features/gws.ts`
58
+ - Export the class: `export class Gws extends Feature { ... }`
59
+ - Register at bottom: `export default features.register('gws', Gws)`
60
+
61
+ 2. **Side-effect import** — `src/node/container.ts` (import block ~line 20-63)
62
+ - Add `import "./features/gws";` (this triggers registration)
63
+
64
+ 3. **Type import + re-export** — `src/node/container.ts` (type imports ~line 65-148)
65
+ - Add `import type { Gws } from './features/gws';`
66
+ - Add `type Gws,` to the `export { ... }` block
67
+
68
+ 4. **Feature type mapping** — `src/node/container.ts` (`NodeFeatures` interface ~line 170-215)
69
+ - Add `gws: typeof Gws;` to the `NodeFeatures` interface
70
+
71
+ Missing step 2 = feature never registers (invisible).
72
+ Missing steps 3-4 = no autocomplete, `container.feature('gws')` returns `Feature` not `Gws`.
73
+
74
+ If the feature has a test, it goes in `test/gws.test.ts`.
75
+
53
76
  ## Type Safety and Introspection
54
77
 
55
78
  - Zod does a lot of the heavy lifting for us with its type inference
@@ -69,3 +92,7 @@ Also available on every container:
69
92
  - See [docs/apis](./docs/apis/) for detailed API descriptions of the public methods and options for creating various helpers
70
93
  - See [docs/examples](./docs/examples/) for examples of using each feature. NOTE: These docs are runnable so you can see the output of the code blocks. `luca run docs/examples/grep` for example
71
94
  - See [docs/tutorials](./docs/tutorials/) for longer form tutorials on various subjects and best practices
95
+
96
+ ## Git Strategy
97
+
98
+ - We generally roll all on main. Commit your changes after you're done, only your changes. Leave a good message, tell me why don't just tell me what. Don't gimme that coauthored by whoever bullshit. The streets know we're one.
package/SPEC.md ADDED
@@ -0,0 +1,304 @@
1
+ ---
2
+ tags: [dx, registration, metaprogramming, luca, architecture, "1.0"]
3
+ status: exploring
4
+ goal: get-luca-to-1.0-release
5
+ ---
6
+
7
+ # Class Registration Refactor Possibilities
8
+
9
+ Exploring how static initialization blocks and class-level hooks could simplify helper registration, inspired by Ruby's `self.inherited(subclass)` pattern and how ActiveRecord::Base uses it.
10
+
11
+ ## Why This Matters for 1.0
12
+
13
+ Luca currently has **114 `.register()` calls** spread across 44 features, ~23 clients, and 3 servers. Adding a new feature requires touching **4 separate locations** (see the [Adding a New Feature checklist](../../../luca/CLAUDE.md)). This ceremony is the kind of friction that kills adoption — the [Luca 1.0 goal](../../goals/get-luca-to-1.0-release.md) explicitly requires that external developers can build their own features easily.
14
+
15
+ The 4-step checklist today:
16
+ 1. Feature file with class + trailing `features.register()` call
17
+ 2. Side-effect import in `container.ts` (lines 20-63 — currently 44 of these)
18
+ 3. Type import + re-export in `container.ts` (lines 65-148)
19
+ 4. `NodeFeatures` interface entry in `container.ts` (lines 170-215)
20
+
21
+ Steps 2-4 exist because registration is **disconnected from the class**. If the class declared itself, the container wiring could be derived.
22
+
23
+ ## The Ruby Inspiration
24
+
25
+ In Ruby, when you write `class Post < ActiveRecord::Base`, the `inherited` hook fires automatically:
26
+
27
+ ```ruby
28
+ class ActiveRecord::Base
29
+ def self.inherited(subclass)
30
+ subclass.table_name = subclass.name.tableize
31
+ subclass.establish_connection
32
+ # The parent configures the child at definition time
33
+ end
34
+ end
35
+
36
+ class Post < ActiveRecord::Base
37
+ end
38
+ # That's it. Full ORM-backed model. No registration call needed.
39
+ ```
40
+
41
+ The magic: `inherited` fires at **class definition time**, receives the subclass, and lets the parent **reach into the child and set it up**. The empty class that does everything.
42
+
43
+ ## Current Luca Registration Pattern
44
+
45
+ Today, every helper requires explicit registration at the bottom of the file:
46
+
47
+ ```typescript
48
+ // src/node/features/fs.ts
49
+ import { features, Feature } from "../feature.js"
50
+
51
+ export class FS extends Feature {
52
+ static override shortcut = "features.fs" as const
53
+ static override stateSchema = FeatureStateSchema
54
+ static override optionsSchema = FeatureOptionsSchema
55
+
56
+ // ... methods ...
57
+ }
58
+
59
+ export default features.register("fs", FS)
60
+ ```
61
+
62
+ And for extension features (AGI, etc), there's the `attach` pattern:
63
+
64
+ ```typescript
65
+ export class Assistant extends Feature<AssistantState, AssistantOptions> {
66
+ static attach(container: Container<AvailableFeatures, any>) {
67
+ features.register('assistant', Assistant)
68
+ return container
69
+ }
70
+ }
71
+
72
+ export default features.register('assistant', Assistant)
73
+ ```
74
+
75
+ The registration is always a separate, imperative call disconnected from the class definition itself. The class doesn't know it's been registered. The registry doesn't know until that line runs.
76
+
77
+ ### What the registry actually does
78
+
79
+ The core `Registry.register()` method (`luca/src/registry.ts:65-70`) does three things:
80
+ 1. Stores the constructor in a private `members` Map
81
+ 2. Calls `interceptRegistration()` for introspection metadata capture
82
+ 3. Emits a `'helperRegistered'` event on the registry bus
83
+
84
+ This is straightforward enough that the base class can own it entirely.
85
+
86
+ ## The Static Block Approach
87
+
88
+ ES2022 static initialization blocks let code run at **class definition time**, inside the class body. This is JavaScript's closest equivalent to Ruby's `inherited`:
89
+
90
+ ```typescript
91
+ class Feature {
92
+ static registry: Registry
93
+
94
+ // This is our "inherited" hook
95
+ static __initSubclass(SubClass: typeof Feature, id: string) {
96
+ this.registry.register(id, SubClass)
97
+ }
98
+ }
99
+ ```
100
+
101
+ ### What it could look like for authors
102
+
103
+ ```typescript
104
+ // The dream: define, register, and export in one declaration
105
+ export default class FS extends Feature {
106
+ static {
107
+ Feature.register(this, "fs")
108
+ }
109
+
110
+ static override shortcut = "features.fs" as const
111
+ static override stateSchema = FeatureStateSchema
112
+ static override optionsSchema = FeatureOptionsSchema
113
+
114
+ async readFile(path: string) { /* ... */ }
115
+ }
116
+ ```
117
+
118
+ No trailing `features.register()` call. No disconnect between the class and its registration. The class **declares itself** as a registered member of the system.
119
+
120
+ ### The id could be derived from convention
121
+
122
+ If we adopt a naming convention (like ActiveRecord derives table names from class names), we could eliminate the explicit id entirely:
123
+
124
+ ```typescript
125
+ export default class DiskCache extends Feature {
126
+ static {
127
+ Feature.register(this) // id inferred as "diskCache" from class name
128
+ }
129
+ }
130
+ ```
131
+
132
+ The `register` implementation:
133
+
134
+ ```typescript
135
+ class Feature {
136
+ static register(SubClass: typeof Feature, id?: string) {
137
+ // Convention: PascalCase class name -> camelCase registry id
138
+ const registryId = id ?? SubClass.name[0].toLowerCase() + SubClass.name.slice(1)
139
+ features.register(registryId, SubClass)
140
+ }
141
+ }
142
+ ```
143
+
144
+ ## Going Further: The Base Class Does All The Work
145
+
146
+ What if the base classes (`Feature`, `Client`, `Server`) provided a static `register` that also handled module augmentation hints and the `attach` pattern?
147
+
148
+ ```typescript
149
+ // Before: 15+ lines of boilerplate per feature
150
+ import { z } from 'zod'
151
+ import { features, Feature } from '@soederpop/luca/feature'
152
+ import type { AvailableFeatures } from '@soederpop/luca/feature'
153
+
154
+ declare module '@soederpop/luca/feature' {
155
+ interface AvailableFeatures {
156
+ conversation: typeof Conversation
157
+ }
158
+ }
159
+
160
+ export class Conversation extends Feature<ConversationState, ConversationOptions> {
161
+ static override shortcut = 'features.conversation' as const
162
+ static attach(container: Container<AvailableFeatures, any>) {
163
+ features.register('conversation', Conversation)
164
+ return container
165
+ }
166
+ // ...
167
+ }
168
+
169
+ export default features.register('conversation', Conversation)
170
+ ```
171
+
172
+ ```typescript
173
+ // After: the class IS the registration
174
+ import { Feature } from '@soederpop/luca/feature'
175
+
176
+ export default class Conversation extends Feature<ConversationState, ConversationOptions> {
177
+ static {
178
+ Feature.register(this, 'conversation')
179
+ }
180
+
181
+ // shortcut derived automatically from registry + id
182
+ // attach() provided by base class using the registered id
183
+ // ...
184
+ }
185
+ ```
186
+
187
+ The base `Feature.register()` could:
188
+ 1. Call `features.register(id, SubClass)`
189
+ 2. Set `SubClass.shortcut` automatically (`features.${id}`)
190
+ 3. Generate a default `attach()` that registers and returns the container
191
+ 4. Emit a `"subclass:registered"` event on the registry for any other wiring
192
+
193
+ ## Codebase Reality Check
194
+
195
+ **Current scale of the problem** (from `luca/src/`):
196
+ - 44 feature files, each with a trailing `features.register()` call
197
+ - 44 side-effect imports in `node/container.ts` lines 20-63
198
+ - 44 type imports in `node/container.ts` lines 65-108
199
+ - 44 entries in the `NodeFeatures` interface at lines 170-215
200
+ - ~23 client files with `clients.register()` calls
201
+ - 3 server files with `servers.register()` calls
202
+ - 0 files currently using static initialization blocks
203
+
204
+ **The `attach()` pattern is mostly ceremonial**: most `attach()` methods in features just call `register()` and return the container. The base classes (`Client`, `Server`, `Command`, `Endpoint`) all have their own `attach()` at the helper-type level (`luca/src/client.ts:41-69`, `luca/src/server.ts:49-73`, etc.) which wire up the registry and factory onto the container. Individual helpers rarely need custom `attach()` logic.
205
+
206
+ **The 4-step checklist creates coupling**: every new feature requires editing `container.ts` in three separate places. This is a maintenance burden and a contributor friction point — exactly the kind of thing that needs solving before 1.0.
207
+
208
+ ## Connection to Related Ideas
209
+
210
+ This refactor sits at the intersection of several exploring ideas:
211
+
212
+ - **[Feature Stacks](./feature-stacks.md)**: Stacks group features into lazy-loaded bundles. If features self-register via static blocks, stacks become simpler — a stack is just an async module that imports a set of feature files, and each feature registers itself on import. No stack-level registration wiring needed.
213
+
214
+ - **[Container `.use()` API](./container-use-api.md)**: The `.use()` API loads stacks/plugins via dynamic import. Static block registration means `container.use(import('./my-feature'))` just works — the import triggers the static block, which calls `Feature.register(this)`, and the feature is available. No `attach()` ceremony.
215
+
216
+ - **[Feature Authoring DX](./luca-feature-authoring-dx.md)**: Complementary, not competing. `defineFeature()` is for simple bag-of-methods features; static block registration is for full class-based helpers. Both reduce boilerplate, both feed into the "one file, one command" authoring goal.
217
+
218
+ - **[Introspection Enhancement](./introspection-enhancement.md)**: `interceptRegistration()` already fires during `register()`. If registration moves into the class body, introspection metadata can be co-located too — the static block could declare description, category, and other metadata that introspection currently scrapes from JSDoc.
219
+
220
+ ## Comparison With Other Approaches
221
+
222
+ | Approach | When it runs | Cooperation needed | Ceremony |
223
+ |----------|-------------|-------------------|----------|
224
+ | Ruby `inherited` | Class definition | None | Zero — just `extends` |
225
+ | Static block + `register` | Class definition | One line in static block | Minimal — inside the class |
226
+ | Trailing `registry.register()` | Module evaluation | Separate call after class | Moderate — disconnected |
227
+ | `defineFeature()` factory | Module evaluation | Wrap entire definition | Different paradigm entirely |
228
+ | Decorators (`@tracked`) | Class definition | One line above class | Minimal — but external |
229
+
230
+ The static block approach is the sweet spot for Luca: it's **standard JavaScript**, runs at **definition time**, lives **inside the class body**, and keeps the class as the primary authoring unit (unlike `defineFeature()` which replaces the class with a factory call).
231
+
232
+ ## The `defineFeature` Relationship
233
+
234
+ This is complementary to the [`defineFeature()` idea](./luca-feature-authoring-dx.md), not a replacement. They solve different problems:
235
+
236
+ - **`defineFeature()`** reduces boilerplate for simple features that are mostly a bag of methods — you skip writing a class entirely
237
+ - **Static block registration** reduces boilerplate for full class-based helpers — you still write the class, but registration is self-contained
238
+
239
+ For complex features that need class inheritance, lifecycle hooks, and full OOP — the static block pattern is cleaner. For simple features that are essentially a named collection of functions — `defineFeature()` is cleaner.
240
+
241
+ ## Implementation Sketch
242
+
243
+ ### Phase 1: Add `Helper.register()` to the base class
244
+
245
+ Each helper base class (`Feature`, `Client`, `Server`) gets a static `register()` that dispatches to its registry:
246
+
247
+ ```typescript
248
+ // In feature.ts
249
+ class Feature {
250
+ static register(SubClass: typeof Feature, id?: string) {
251
+ const registryId = id ?? SubClass.name[0].toLowerCase() + SubClass.name.slice(1)
252
+
253
+ // 1. Register in the features registry
254
+ features.register(registryId, SubClass as any)
255
+
256
+ // 2. Auto-set shortcut if not overridden
257
+ if (!Object.getOwnPropertyDescriptor(SubClass, 'shortcut')?.value) {
258
+ ;(SubClass as any).shortcut = `features.${registryId}` as const
259
+ }
260
+
261
+ // 3. Generate default attach() if not overridden
262
+ if (!Object.getOwnPropertyDescriptor(SubClass, 'attach')) {
263
+ ;(SubClass as any).attach = (container: any) => {
264
+ features.register(registryId, SubClass as any)
265
+ return container
266
+ }
267
+ }
268
+ }
269
+ }
270
+ ```
271
+
272
+ This is backward-compatible — existing `features.register()` calls still work. New features can opt into the static block pattern.
273
+
274
+ ### Phase 2: Migrate one feature as proof of concept
275
+
276
+ Pick a simple feature like `dns` or `yaml` and convert it:
277
+
278
+ ```typescript
279
+ // Before (dns.ts)
280
+ export class DNS extends Feature { /* ... */ }
281
+ export default features.register("dns", DNS)
282
+
283
+ // After (dns.ts)
284
+ export default class DNS extends Feature {
285
+ static { Feature.register(this, "dns") }
286
+ /* ... */
287
+ }
288
+ ```
289
+
290
+ ### Phase 3: Codegen for container.ts wiring
291
+
292
+ Build a `luca gen:features` command that scans `src/node/features/` and generates the side-effect imports, type imports, and `NodeFeatures` interface entries. This eliminates steps 2-4 of the checklist entirely — the only manual step is writing the feature file itself.
293
+
294
+ ### Phase 4: Gradual migration
295
+
296
+ Convert remaining features at leisure. The old and new patterns coexist since `features.register()` is the underlying mechanism either way.
297
+
298
+ ## Open Questions
299
+
300
+ - ~~**Registry detection**~~: **Yes.** `Helper.register(this)` walks the prototype chain to find which base class (`Feature`, `Client`, `Server`) owns the registry, then dispatches automatically. Verified in Bun. The `register` call also accepts an options bag (`{ id, stateSchema, optionsSchema }`) so schemas are set on the class *before* the registry call fires — meaning `interceptRegistration` sees a fully-decorated class. Id inference from class name works for most cases; acronym-style names (DNS, REST, SSH) need an explicit `id` passed.
301
+ - ~~**Module augmentation**~~: The `declare module` blocks are necessary boilerplate — they're the one piece that can't be eliminated by runtime mechanics. Codegen is off the table, and TS language service plugins only affect editor intellisense (not `tsc`), so they'd require non-standard toolchain hacks. Not worth pursuing. One `declare module` block per feature file, co-located with the class, is acceptable.
302
+ - ~~**Import side effects**~~: Not worth changing right now. Registration on import is fine — the static block just makes it more visible and intentional. Lazy registration via `container.use()` is a separate concern for later.
303
+ - ~~**`attach()` consolidation**~~: **Yes, do it.** `Helper.register()` should generate a default `attach()` that registers and returns the container. The vast majority of `attach()` methods are just that. The few that do more (wiring up other features) can still override.
304
+ - ~~**Bun compatibility**~~: **Verified on Bun 1.2.15** — `this` inside `static { }` correctly binds to the subclass being defined, not the parent. Name inference from `SubClass.name`, coexistence with other static properties, and registration dispatch all work as expected. No issues found.