@soederpop/luca 0.0.6 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +10 -1
- package/bun.lock +1 -1
- package/commands/build-bootstrap.ts +78 -0
- package/commands/build-scaffolds.ts +24 -2
- package/commands/try-all-challenges.ts +543 -0
- package/commands/try-challenge.ts +100 -0
- package/docs/README.md +52 -80
- package/docs/TABLE-OF-CONTENTS.md +82 -51
- package/docs/apis/clients/elevenlabs.md +232 -8
- package/docs/apis/clients/graph.md +59 -8
- package/docs/apis/clients/openai.md +362 -2
- package/docs/apis/clients/rest.md +122 -2
- package/docs/apis/clients/websocket.md +71 -17
- package/docs/apis/features/agi/assistant.md +9 -3
- package/docs/apis/features/agi/assistants-manager.md +2 -2
- package/docs/apis/features/agi/claude-code.md +153 -14
- package/docs/apis/features/agi/conversation-history.md +15 -3
- package/docs/apis/features/agi/conversation.md +133 -20
- package/docs/apis/features/agi/openai-codex.md +90 -12
- package/docs/apis/features/agi/skills-library.md +23 -5
- package/docs/apis/features/node/container-link.md +59 -0
- package/docs/apis/features/node/content-db.md +1 -1
- package/docs/apis/features/node/disk-cache.md +1 -1
- package/docs/apis/features/node/dns.md +1 -0
- package/docs/apis/features/node/docker.md +2 -1
- package/docs/apis/features/node/esbuild.md +4 -3
- package/docs/apis/features/node/file-manager.md +13 -4
- package/docs/apis/features/node/fs.md +726 -171
- package/docs/apis/features/node/git.md +1 -0
- package/docs/apis/features/node/google-auth.md +23 -4
- package/docs/apis/features/node/google-calendar.md +14 -2
- package/docs/apis/features/node/google-docs.md +15 -2
- package/docs/apis/features/node/google-drive.md +21 -3
- package/docs/apis/features/node/google-sheets.md +14 -2
- package/docs/apis/features/node/grep.md +2 -0
- package/docs/apis/features/node/helpers.md +29 -0
- package/docs/apis/features/node/ink.md +2 -2
- package/docs/apis/features/node/networking.md +39 -4
- package/docs/apis/features/node/os.md +28 -0
- package/docs/apis/features/node/postgres.md +26 -4
- package/docs/apis/features/node/proc.md +37 -28
- package/docs/apis/features/node/process-manager.md +33 -5
- package/docs/apis/features/node/repl.md +1 -1
- package/docs/apis/features/node/runpod.md +1 -0
- package/docs/apis/features/node/secure-shell.md +7 -0
- package/docs/apis/features/node/semantic-search.md +12 -5
- package/docs/apis/features/node/sqlite.md +26 -4
- package/docs/apis/features/node/telegram.md +30 -5
- package/docs/apis/features/node/tts.md +17 -2
- package/docs/apis/features/node/ui.md +1 -1
- package/docs/apis/features/node/vault.md +4 -9
- package/docs/apis/features/node/vm.md +3 -12
- package/docs/apis/features/node/window-manager.md +128 -20
- package/docs/apis/features/web/asset-loader.md +13 -1
- package/docs/apis/features/web/container-link.md +59 -0
- package/docs/apis/features/web/esbuild.md +4 -3
- package/docs/apis/features/web/helpers.md +29 -0
- package/docs/apis/features/web/network.md +16 -2
- package/docs/apis/features/web/speech.md +16 -2
- package/docs/apis/features/web/vault.md +4 -9
- package/docs/apis/features/web/vm.md +3 -12
- package/docs/apis/features/web/voice.md +18 -1
- package/docs/apis/servers/express.md +18 -2
- package/docs/apis/servers/mcp.md +29 -4
- package/docs/apis/servers/websocket.md +34 -6
- package/docs/bootstrap/CLAUDE.md +100 -0
- package/docs/bootstrap/SKILL.md +222 -0
- package/docs/bootstrap/templates/about-command.ts +41 -0
- package/docs/bootstrap/templates/docs-models.ts +22 -0
- package/docs/bootstrap/templates/docs-readme.md +43 -0
- package/docs/bootstrap/templates/example-feature.ts +53 -0
- package/docs/bootstrap/templates/health-endpoint.ts +15 -0
- package/docs/bootstrap/templates/luca-cli.ts +25 -0
- package/docs/challenges/caching-proxy.md +16 -0
- package/docs/challenges/content-db-round-trip.md +14 -0
- package/docs/challenges/custom-command.md +9 -0
- package/docs/challenges/file-watcher-pipeline.md +11 -0
- package/docs/challenges/grep-audit-report.md +15 -0
- package/docs/challenges/multi-feature-dashboard.md +14 -0
- package/docs/challenges/process-orchestrator.md +17 -0
- package/docs/challenges/rest-api-server-with-client.md +12 -0
- package/docs/challenges/script-runner-with-vm.md +11 -0
- package/docs/challenges/simple-rest-api.md +15 -0
- package/docs/challenges/websocket-serve-and-client.md +11 -0
- package/docs/challenges/yaml-config-system.md +14 -0
- package/docs/command-system-overhaul.md +94 -0
- package/docs/examples/assistant/CORE.md +18 -0
- package/docs/examples/assistant/hooks.ts +3 -0
- package/docs/examples/assistant/tools.ts +10 -0
- package/docs/examples/window-manager-layouts.md +180 -0
- package/docs/in-memory-fs.md +4 -0
- package/docs/models.ts +13 -10
- package/docs/philosophy.md +4 -3
- package/docs/reports/console-hmr-design.md +170 -0
- package/docs/reports/helper-semantic-search.md +72 -0
- package/docs/scaffolds/client.md +29 -20
- package/docs/scaffolds/command.md +64 -50
- package/docs/scaffolds/endpoint.md +31 -36
- package/docs/scaffolds/feature.md +28 -18
- package/docs/scaffolds/selector.md +91 -0
- package/docs/scaffolds/server.md +18 -9
- package/docs/selectors.md +115 -0
- package/docs/sessions/custom-command/attempt-log-2.md +195 -0
- package/docs/sessions/file-watcher-pipeline/attempt-log-1.md +728 -0
- package/docs/sessions/file-watcher-pipeline/attempt-log-2.md +555 -0
- package/docs/sessions/grep-audit-report/attempt-log-1.md +289 -0
- package/docs/sessions/multi-feature-dashboard/attempt-log-2.md +679 -0
- package/docs/sessions/rest-api-server-with-client/attempt-log-1.md +1 -0
- package/docs/sessions/rest-api-server-with-client/attempt-log-3.md +920 -0
- package/docs/sessions/simple-rest-api/attempt-log-1.md +593 -0
- package/docs/sessions/websocket-serve-and-client/attempt-log-2.md +995 -0
- package/docs/tutorials/00-bootstrap.md +148 -0
- package/docs/tutorials/07-endpoints.md +7 -7
- package/docs/tutorials/08-commands.md +153 -72
- package/luca.cli.ts +3 -0
- package/package.json +6 -5
- package/public/index.html +1430 -0
- package/scripts/examples/using-ollama.ts +2 -1
- package/scripts/update-introspection-data.ts +2 -2
- package/src/agi/endpoints/experts.ts +1 -1
- package/src/agi/features/assistant.ts +7 -0
- package/src/agi/features/assistants-manager.ts +5 -5
- package/src/agi/features/claude-code.ts +263 -3
- package/src/agi/features/conversation-history.ts +7 -1
- package/src/agi/features/conversation.ts +26 -3
- package/src/agi/features/openai-codex.ts +26 -2
- package/src/agi/features/openapi.ts +6 -1
- package/src/agi/features/skills-library.ts +9 -1
- package/src/bootstrap/generated.ts +540 -0
- package/src/cli/cli.ts +64 -21
- package/src/client.ts +23 -357
- package/src/clients/civitai/index.ts +1 -1
- package/src/clients/client-template.ts +1 -1
- package/src/clients/comfyui/index.ts +13 -2
- package/src/clients/elevenlabs/index.ts +2 -1
- package/src/clients/graph.ts +87 -0
- package/src/clients/openai/index.ts +10 -1
- package/src/clients/rest.ts +207 -0
- package/src/clients/websocket.ts +176 -0
- package/src/command.ts +281 -34
- package/src/commands/bootstrap.ts +181 -0
- package/src/commands/chat.ts +5 -4
- package/src/commands/describe.ts +225 -2
- package/src/commands/help.ts +35 -9
- package/src/commands/index.ts +3 -0
- package/src/commands/introspect.ts +92 -2
- package/src/commands/prompt.ts +5 -6
- package/src/commands/run.ts +33 -10
- package/src/commands/save-api-docs.ts +49 -0
- package/src/commands/scaffold.ts +169 -23
- package/src/commands/select.ts +94 -0
- package/src/commands/serve.ts +10 -1
- package/src/container.ts +15 -0
- package/src/endpoint.ts +19 -0
- package/src/graft.ts +181 -0
- package/src/introspection/generated.agi.ts +12458 -8968
- package/src/introspection/generated.node.ts +10573 -7145
- package/src/introspection/generated.web.ts +1 -1
- package/src/introspection/index.ts +26 -0
- package/src/node/container.ts +6 -7
- package/src/node/features/content-db.ts +49 -2
- package/src/node/features/disk-cache.ts +16 -9
- package/src/node/features/dns.ts +16 -3
- package/src/node/features/docker.ts +16 -4
- package/src/node/features/esbuild.ts +20 -0
- package/src/node/features/file-manager.ts +184 -29
- package/src/node/features/fs.ts +704 -248
- package/src/node/features/git.ts +21 -8
- package/src/node/features/grep.ts +23 -3
- package/src/node/features/helpers.ts +372 -43
- package/src/node/features/networking.ts +39 -4
- package/src/node/features/opener.ts +28 -15
- package/src/node/features/os.ts +76 -0
- package/src/node/features/port-exposer.ts +11 -1
- package/src/node/features/postgres.ts +17 -1
- package/src/node/features/proc.ts +4 -1
- package/src/node/features/python.ts +63 -14
- package/src/node/features/repl.ts +11 -7
- package/src/node/features/runpod.ts +16 -3
- package/src/node/features/secure-shell.ts +27 -2
- package/src/node/features/semantic-search.ts +12 -1
- package/src/node/features/ui.ts +5 -69
- package/src/node/features/vm.ts +17 -0
- package/src/node/features/window-manager.ts +68 -20
- package/src/node.ts +5 -0
- package/src/scaffolds/generated.ts +492 -290
- package/src/scaffolds/template.ts +9 -0
- package/src/schemas/base.ts +46 -5
- package/src/selector.ts +282 -0
- package/src/server.ts +11 -0
- package/src/servers/express.ts +27 -12
- package/src/servers/socket.ts +45 -11
- package/src/web/clients/socket.ts +4 -1
- package/src/web/container.ts +2 -1
- package/src/web/features/network.ts +7 -1
- package/src/web/features/voice-recognition.ts +16 -1
- package/test/clients-servers.test.ts +2 -1
- package/test/command.test.ts +267 -0
- package/test-integration/assistants-manager.test.ts +10 -20
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/1b/b5/c75b28794f00f94c4d609a98978e9420e9b7146d204a7fbf5b0b30477292581705d207c0100dabaac27eef540aaaece3374af75104a93219d4ec8bfb44e7 +1 -0
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/da/df/1d90ce4e042abeb035a197832c6d6893420a747a056be773eb00e4f745a037d505c8db13dde7d36b36b6b893addbb7df0f5fe9f0c13e665f20056447318b +1 -0
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/ed/04/e1d0c2a58c2db29b3921ca2affb3ea4febe831c53b38ebc21019fb799823aba6ed5b4611873d2cd25d422d49955b852a9c326da0d678899bc1c2c2960901 +1 -0
- package/tmp/.cache/luca-disk-cache/index-v5/00/13/572aa4c9a94f99eda999695d050cdd0ca7fe2d23a50af03234d4c8ce0791 +2 -0
- package/tmp/.cache/luca-disk-cache/index-v5/75/a9/cb61dc0f0589e8ec10a9aca27b834bc73884c479941042d22a2b22324cd3 +2 -0
- package/tmp/.cache/luca-disk-cache/index-v5/9f/0f/8b1f915ee64cfff7667dd96acd7a5ac0a96aa91a346e19cefd45909a9c9c +2 -0
- package/docs/apis/features/node/launcher-app-command-listener.md +0 -145
- package/docs/examples/launcher-app-command-listener.md +0 -120
- package/docs/tasks/web-container-helper-discovery.md +0 -71
- package/docs/todos.md +0 -1
- package/scripts/test-command-listener.ts +0 -123
- package/src/node/features/launcher-app-command-listener.ts +0 -389
|
@@ -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
|
package/docs/scaffolds/client.md
CHANGED
|
@@ -11,9 +11,8 @@ When to build a client:
|
|
|
11
11
|
|
|
12
12
|
```ts
|
|
13
13
|
import { z } from 'zod'
|
|
14
|
-
import { 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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
132
|
+
static { Client.register(this, '{{camelName}}') }
|
|
125
133
|
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
async afterInitialize() {
|
|
135
|
+
// Setup logic goes here — not in the constructor
|
|
128
136
|
}
|
|
129
137
|
}
|
|
130
138
|
|
|
131
|
-
export default
|
|
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
|
|
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
|
|
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
|
|
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
|
-
##
|
|
17
|
+
## Positional Arguments
|
|
19
18
|
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
##
|
|
26
|
+
## Args Schema
|
|
31
27
|
|
|
32
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
##
|
|
41
|
+
## Description
|
|
48
42
|
|
|
49
|
-
|
|
43
|
+
Export a description string for `luca --help` display:
|
|
50
44
|
|
|
51
45
|
```ts
|
|
52
|
-
|
|
53
|
-
description: '{{description}}',
|
|
54
|
-
argsSchema,
|
|
55
|
-
handler: {{camelName}},
|
|
56
|
-
})
|
|
46
|
+
export const description = '{{description}}'
|
|
57
47
|
```
|
|
58
48
|
|
|
59
|
-
##
|
|
49
|
+
## Handler
|
|
60
50
|
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
79
|
-
interface AvailableCommands {
|
|
80
|
-
{{camelName}}: ReturnType<typeof commands.registerHandler>
|
|
81
|
-
}
|
|
82
|
-
}
|
|
71
|
+
export const description = '{{description}}'
|
|
83
72
|
|
|
84
|
-
|
|
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
|
|
81
|
+
const { container } = context
|
|
88
82
|
const fs = container.feature('fs')
|
|
89
83
|
|
|
90
|
-
console.log('{{
|
|
84
|
+
console.log('{{kebabName}} running...', options.target)
|
|
91
85
|
}
|
|
86
|
+
```
|
|
92
87
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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/{{
|
|
103
|
-
- **Naming**:
|
|
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**:
|
|
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`.
|