@soederpop/luca 0.0.29 → 0.0.31

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 (50) hide show
  1. package/commands/try-all-challenges.ts +1 -1
  2. package/docs/TABLE-OF-CONTENTS.md +0 -3
  3. package/docs/tutorials/20-browser-esm.md +234 -0
  4. package/package.json +1 -1
  5. package/src/agi/container.server.ts +4 -0
  6. package/src/agi/features/assistant.ts +120 -3
  7. package/src/agi/features/browser-use.ts +623 -0
  8. package/src/bootstrap/generated.ts +236 -308
  9. package/src/cli/build-info.ts +2 -2
  10. package/src/clients/rest.ts +7 -7
  11. package/src/command.ts +20 -1
  12. package/src/commands/chat.ts +22 -0
  13. package/src/commands/describe.ts +67 -2
  14. package/src/commands/prompt.ts +23 -3
  15. package/src/commands/serve.ts +27 -0
  16. package/src/container.ts +411 -113
  17. package/src/endpoint.ts +6 -0
  18. package/src/helper.ts +226 -5
  19. package/src/introspection/generated.agi.ts +16089 -10021
  20. package/src/introspection/generated.node.ts +5102 -2077
  21. package/src/introspection/generated.web.ts +379 -291
  22. package/src/introspection/index.ts +7 -0
  23. package/src/introspection/scan.ts +224 -7
  24. package/src/node/container.ts +31 -10
  25. package/src/node/features/content-db.ts +7 -7
  26. package/src/node/features/disk-cache.ts +11 -11
  27. package/src/node/features/esbuild.ts +3 -3
  28. package/src/node/features/file-manager.ts +15 -15
  29. package/src/node/features/fs.ts +23 -22
  30. package/src/node/features/git.ts +10 -10
  31. package/src/node/features/helpers.ts +5 -2
  32. package/src/node/features/ink.ts +13 -13
  33. package/src/node/features/ipc-socket.ts +8 -8
  34. package/src/node/features/networking.ts +3 -3
  35. package/src/node/features/os.ts +7 -7
  36. package/src/node/features/package-finder.ts +15 -15
  37. package/src/node/features/proc.ts +1 -1
  38. package/src/node/features/ui.ts +13 -13
  39. package/src/node/features/vm.ts +4 -4
  40. package/src/scaffolds/generated.ts +1 -1
  41. package/src/servers/express.ts +24 -6
  42. package/src/servers/mcp.ts +4 -4
  43. package/src/servers/socket.ts +6 -6
  44. package/docs/apis/features/node/window-manager.md +0 -445
  45. package/docs/examples/window-manager-layouts.md +0 -180
  46. package/docs/examples/window-manager.md +0 -125
  47. package/docs/window-manager-fix.md +0 -249
  48. package/scripts/test-window-manager-lifecycle.ts +0 -86
  49. package/scripts/test-window-manager.ts +0 -43
  50. package/src/node/features/window-manager.ts +0 -1603
@@ -1,5 +1,5 @@
1
1
  // Auto-generated bootstrap content
2
- // Generated at: 2026-03-24T01:41:40.771Z
2
+ // Generated at: 2026-03-24T09:08:07.564Z
3
3
  // Source: docs/bootstrap/*.md, docs/bootstrap/templates/*, docs/examples/*.md, docs/tutorials/*.md
4
4
  //
5
5
  // Do not edit manually. Run: luca build-bootstrap
@@ -2773,132 +2773,6 @@ Supported loaders include \`ts\` (default), \`tsx\`, \`jsx\`, and \`js\`.
2773
2773
  ## Summary
2774
2774
 
2775
2775
  This demo covered synchronous and asynchronous transpilation, minification, and using different source loaders. The \`esbuild\` feature gives you runtime TypeScript-to-JavaScript compilation with zero configuration.
2776
- `,
2777
- "window-manager.md": `---
2778
- title: "Window Manager"
2779
- tags: [windowManager, native, ipc, macos, browser, window]
2780
- lastTested: null
2781
- lastTestPassed: null
2782
- ---
2783
-
2784
- # windowManager
2785
-
2786
- Native window control via LucaVoiceLauncher IPC. Communicates with the macOS launcher app over a Unix domain socket using NDJSON to spawn, navigate, screenshot, and manage native browser windows.
2787
-
2788
- ## Overview
2789
-
2790
- Use the \`windowManager\` feature when you need to control native browser windows from Luca. It acts as an IPC server that the LucaVoiceLauncher macOS app connects to. Through this connection you can spawn windows with configurable chrome, navigate URLs, evaluate JavaScript, capture screenshots, and record video.
2791
-
2792
- Requires the LucaVoiceLauncher native macOS app to be running and connected.
2793
-
2794
- ## Enabling the Feature
2795
-
2796
- \`\`\`ts
2797
- const wm = container.feature('windowManager', {
2798
- autoListen: false,
2799
- requestTimeoutMs: 10000
2800
- })
2801
- console.log('Window Manager feature created')
2802
- console.log('Listening:', wm.isListening)
2803
- console.log('Client connected:', wm.isClientConnected)
2804
- \`\`\`
2805
-
2806
- ## API Documentation
2807
-
2808
- \`\`\`ts
2809
- const info = await container.features.describe('windowManager')
2810
- console.log(info)
2811
- \`\`\`
2812
-
2813
- ## Spawning Windows
2814
-
2815
- Create native browser windows with configurable dimensions and chrome.
2816
-
2817
- \`\`\`ts skip
2818
- const result = await wm.spawn({
2819
- url: 'https://google.com',
2820
- width: 1024,
2821
- height: 768,
2822
- alwaysOnTop: true,
2823
- window: { decorations: 'hiddenTitleBar', shadow: true }
2824
- })
2825
- console.log('Window ID:', result.windowId)
2826
- \`\`\`
2827
-
2828
- The \`spawn()\` method sends a dispatch to the native app and waits for acknowledgement. Window options include position, size, transparency, click-through, and title bar style.
2829
-
2830
- ## Navigation and JavaScript Evaluation
2831
-
2832
- Control window content after spawning.
2833
-
2834
- \`\`\`ts skip
2835
- const handle = wm.window(result.windowId)
2836
- await handle.navigate('https://news.ycombinator.com')
2837
- console.log('Navigated')
2838
-
2839
- const title = await handle.eval('document.title')
2840
- console.log('Page title:', title)
2841
-
2842
- await handle.focus()
2843
- await handle.close()
2844
- \`\`\`
2845
-
2846
- The \`window()\` method returns a \`WindowHandle\` for chainable operations on a specific window. Use \`eval()\` to run JavaScript in the window's web view.
2847
-
2848
- ## Screenshots and Video
2849
-
2850
- Capture visual output from windows.
2851
-
2852
- \`\`\`ts skip
2853
- await wm.screengrab({
2854
- windowId: result.windowId,
2855
- path: './screenshot.png'
2856
- })
2857
- console.log('Screenshot saved')
2858
-
2859
- await wm.video({
2860
- windowId: result.windowId,
2861
- path: './recording.mp4',
2862
- durationMs: 5000
2863
- })
2864
- console.log('Video recorded')
2865
- \`\`\`
2866
-
2867
- Screenshots are saved as PNG. Video recording captures for the specified duration.
2868
-
2869
- ## Terminal Windows
2870
-
2871
- Spawn native terminal windows that render command output with ANSI support.
2872
-
2873
- \`\`\`ts skip
2874
- const tty = await wm.spawnTTY({
2875
- command: 'htop',
2876
- title: 'System Monitor',
2877
- width: 900,
2878
- height: 600,
2879
- cols: 120,
2880
- rows: 40
2881
- })
2882
- console.log('TTY window:', tty.windowId)
2883
- \`\`\`
2884
-
2885
- Terminal windows are read-only displays of process output. Closing the window terminates the process.
2886
-
2887
- ## IPC Communication
2888
-
2889
- Other features can send arbitrary messages over the socket connection.
2890
-
2891
- \`\`\`ts skip
2892
- wm.listen()
2893
- wm.on('message', (msg) => console.log('App says:', msg))
2894
- wm.send({ id: 'abc', status: 'ready', speech: 'Window manager online' })
2895
- \`\`\`
2896
-
2897
- The \`message\` event fires for any non-windowAck message from the native app.
2898
-
2899
- ## Summary
2900
-
2901
- The \`windowManager\` feature provides native window control through IPC with the LucaVoiceLauncher app. Spawn browser windows, navigate, evaluate JS, capture screenshots, and record video. Supports terminal windows for command output. Key methods: \`spawn()\`, \`navigate()\`, \`eval()\`, \`screengrab()\`, \`video()\`, \`spawnTTY()\`, \`window()\`.
2902
2776
  `,
2903
2777
  "proc.md": `---
2904
2778
  title: "proc"
@@ -3052,187 +2926,6 @@ console.log('Downloader is ready. Call downloader.download(url, path) to fetch f
3052
2926
  ## Summary
3053
2927
 
3054
2928
  This demo covered the \`downloader\` feature, which provides a simple one-method API for fetching remote files and saving them locally. It handles HTTP requests, buffering, and file writing, making it the right choice for any task that involves pulling assets from the network.
3055
- `,
3056
- "window-manager-layouts.md": `---
3057
- title: "Window Manager Layouts"
3058
- tags: [windowManager, layout, native, ipc, macos, multi-window]
3059
- lastTested: null
3060
- lastTestPassed: null
3061
- ---
3062
-
3063
- # Window Manager Layouts
3064
-
3065
- Spawn and manage multiple windows at once using the layout API. Layouts let you declare groups of browser and terminal windows that open in parallel, or sequence multiple groups so each batch waits for the previous one to finish.
3066
-
3067
- ## Overview
3068
-
3069
- The \`windowManager\` feature exposes two layout methods:
3070
-
3071
- - **\`spawnLayout(config)\`** — spawns all entries in parallel, returns \`WindowHandle[]\`
3072
- - **\`spawnLayouts(configs)\`** — spawns multiple layouts sequentially (each layout's windows still spawn in parallel), returns \`WindowHandle[][]\`
3073
-
3074
- Each entry in a layout is a \`LayoutEntry\` — either a browser window or a TTY window. Type detection is automatic: if the entry has a \`command\` field or \`type: 'tty'\`, it's a terminal. Otherwise it's a browser window.
3075
-
3076
- ## Setup
3077
-
3078
- \`\`\`ts
3079
- const wm = container.feature('windowManager', {
3080
- autoListen: true,
3081
- requestTimeoutMs: 10000
3082
- })
3083
- console.log('Window Manager ready')
3084
- \`\`\`
3085
-
3086
- ## Single Layout — Parallel Windows
3087
-
3088
- Spawn a mix of browser and terminal windows that all open at the same time.
3089
-
3090
- \`\`\`ts
3091
- const handles = await wm.spawnLayout([
3092
- { url: 'https://github.com', width: '50%', height: '100%', x: 0, y: 0 },
3093
- { url: 'https://soederpop.com', width: '50%', height: '100%', x: '50%', y: 0 },
3094
- { command: 'top', title: 'System Monitor', width: 900, height: 400, x: 0, y: 720 },
3095
- ])
3096
-
3097
- console.log('Spawned', handles.length, 'windows')
3098
- handles.forEach((h, i) => console.log(\` [\${i}] windowId: \${h.windowId}\`))
3099
- \`\`\`
3100
-
3101
- All three windows open simultaneously. The returned \`handles\` array preserves the same order as the config entries, so \`handles[0]\` corresponds to the GitHub window, \`handles[1]\` to HN, and \`handles[2]\` to htop.
3102
-
3103
- ## Percentage-Based Dimensions
3104
-
3105
- Dimensions (\`width\`, \`height\`, \`x\`, \`y\`) accept percentage strings resolved against the primary display. This makes layouts portable across different screen resolutions.
3106
-
3107
- \`\`\`ts skip
3108
- // Side-by-side: two windows each taking half the screen
3109
- const handles = await wm.spawnLayout([
3110
- { url: 'https://github.com', width: '50%', height: '100%', x: '0%', y: '0%' },
3111
- { url: 'https://news.ycombinator.com', width: '50%', height: '100%', x: '50%', y: '0%' },
3112
- ])
3113
- \`\`\`
3114
-
3115
- You can mix absolute and percentage values freely:
3116
-
3117
- \`\`\`ts skip
3118
- const handles = await wm.spawnLayout([
3119
- { url: 'https://example.com', width: '75%', height: 600, x: '12.5%', y: 50 },
3120
- { command: 'htop', width: '100%', height: '30%', x: '0%', y: '70%' },
3121
- ])
3122
- \`\`\`
3123
-
3124
- ## Explicit Type Field
3125
-
3126
- You can be explicit about entry types using the \`type\` field. This is equivalent to the implicit detection but more readable when mixing window types.
3127
-
3128
- \`\`\`ts skip
3129
- const handles = await wm.spawnLayout([
3130
- { type: 'window', url: 'https://example.com', width: 800, height: 600 },
3131
- { type: 'tty', command: 'tail -f /var/log/system.log', title: 'Logs' },
3132
- ])
3133
-
3134
- console.log('Browser window:', handles[0].windowId)
3135
- console.log('TTY window:', handles[1].windowId)
3136
- \`\`\`
3137
-
3138
- ## Sequential Layouts
3139
-
3140
- Use \`spawnLayouts()\` when you need windows to appear in stages. Each layout batch spawns in parallel, but the next batch waits until the previous one is fully ready.
3141
-
3142
- \`\`\`ts skip
3143
- const [dashboards, tools] = await wm.spawnLayouts([
3144
- // First batch: main content
3145
- [
3146
- { url: 'https://grafana.internal/d/api-latency', width: 960, height: 800, x: 0, y: 0 },
3147
- { url: 'https://grafana.internal/d/error-rate', width: 960, height: 800, x: 970, y: 0 },
3148
- ],
3149
- // Second batch: supporting tools (opens after dashboards are ready)
3150
- [
3151
- { command: 'htop', title: 'CPU', width: 640, height: 400, x: 0, y: 820 },
3152
- { command: 'tail -f /var/log/system.log', title: 'Logs', width: 640, height: 400, x: 650, y: 820 },
3153
- ],
3154
- ])
3155
-
3156
- console.log('Dashboards:', dashboards.map(h => h.windowId))
3157
- console.log('Tools:', tools.map(h => h.windowId))
3158
- \`\`\`
3159
-
3160
- This is useful when the second batch depends on the first being visible — for example, positioning tool windows below dashboard windows.
3161
-
3162
- ## Lifecycle Events on Layout Handles
3163
-
3164
- Every handle returned from a layout supports the same event API as a single \`spawn()\` call. You can listen for \`close\` and \`terminalExited\` events on each handle independently.
3165
-
3166
- \`\`\`ts skip
3167
- const handles = await wm.spawnLayout([
3168
- { url: 'https://example.com', width: 800, height: 600 },
3169
- { command: 'sleep 5 && echo done', title: 'Short Task' },
3170
- ])
3171
-
3172
- const [browser, terminal] = handles
3173
-
3174
- browser.on('close', () => console.log('Browser window closed'))
3175
-
3176
- terminal.on('terminalExited', (info) => {
3177
- console.log('Terminal process finished:', info)
3178
- })
3179
- \`\`\`
3180
-
3181
- ## Window Chrome Options
3182
-
3183
- Layout entries support all the same window chrome options as regular \`spawn()\` calls.
3184
-
3185
- \`\`\`ts skip
3186
- const handles = await wm.spawnLayout([
3187
- {
3188
- url: 'https://example.com',
3189
- width: 400,
3190
- height: 300,
3191
- alwaysOnTop: true,
3192
- window: {
3193
- decorations: 'hiddenTitleBar',
3194
- transparent: true,
3195
- shadow: true,
3196
- opacity: 0.9,
3197
- }
3198
- },
3199
- {
3200
- url: 'https://example.com/overlay',
3201
- width: 200,
3202
- height: 100,
3203
- window: {
3204
- decorations: 'none',
3205
- clickThrough: true,
3206
- transparent: true,
3207
- }
3208
- },
3209
- ])
3210
- \`\`\`
3211
-
3212
- ## Operating on All Handles
3213
-
3214
- Since layouts return arrays of \`WindowHandle\`, you can easily batch operations across all windows.
3215
-
3216
- \`\`\`ts skip
3217
- const handles = await wm.spawnLayout([
3218
- { url: 'https://example.com/a', width: 600, height: 400 },
3219
- { url: 'https://example.com/b', width: 600, height: 400 },
3220
- { url: 'https://example.com/c', width: 600, height: 400 },
3221
- ])
3222
-
3223
- // Navigate all windows to the same URL
3224
- await Promise.all(handles.map(h => h.navigate('https://example.com/updated')))
3225
-
3226
- // Screenshot all windows
3227
- await Promise.all(handles.map((h, i) => h.screengrab(\`./layout-\${i}.png\`)))
3228
-
3229
- // Close all windows
3230
- await Promise.all(handles.map(h => h.close()))
3231
- \`\`\`
3232
-
3233
- ## Summary
3234
-
3235
- The layout API builds on top of \`spawn()\` and \`spawnTTY()\` to orchestrate multi-window setups. Use \`spawnLayout()\` for a single batch of parallel windows, and \`spawnLayouts()\` when you need staged sequences. Every returned handle supports the full \`WindowHandle\` API — events, navigation, eval, screenshots, and more.
3236
2929
  `,
3237
2930
  "google-docs.md": `---
3238
2931
  title: "Google Docs"
@@ -7441,6 +7134,241 @@ const docs = container.inspectAsText()
7441
7134
  \`\`\`
7442
7135
 
7443
7136
  This is what makes Luca especially powerful for AI agents -- they can discover the entire API surface at runtime without reading documentation.
7137
+ `,
7138
+ "20-browser-esm.md": `---
7139
+ title: "Browser: Import Luca from esm.sh"
7140
+ tags:
7141
+ - browser
7142
+ - esm
7143
+ - web
7144
+ - quickstart
7145
+ - cdn
7146
+ ---
7147
+ # Browser: Import Luca from esm.sh
7148
+
7149
+ You can use Luca in any browser environment — no bundler, no build step. Import it from [esm.sh](https://esm.sh) and you get the singleton container on \`window.luca\`, ready to go. All the same APIs apply.
7150
+
7151
+ ## Basic Setup
7152
+
7153
+ \`\`\`html
7154
+ <script type="module">
7155
+ import "https://esm.sh/@soederpop/luca/web"
7156
+
7157
+ const container = window.luca
7158
+ console.log(container.uuid) // unique container ID
7159
+ console.log(container.features.available) // ['assetLoader', 'voice', 'speech', 'network', 'vault', 'vm', 'esbuild', 'helpers', 'containerLink']
7160
+ </script>
7161
+ \`\`\`
7162
+
7163
+ The import triggers module evaluation, which creates the \`WebContainer\` singleton and attaches it to \`window.luca\`. That's it.
7164
+
7165
+ If you prefer a named import:
7166
+
7167
+ \`\`\`html
7168
+ <script type="module">
7169
+ import container from "https://esm.sh/@soederpop/luca/web"
7170
+ // container === window.luca
7171
+ </script>
7172
+ \`\`\`
7173
+
7174
+ ## Using Features
7175
+
7176
+ Once you have the container, features work exactly like they do on the server — lazy-loaded via \`container.feature()\`.
7177
+
7178
+ \`\`\`html
7179
+ <script type="module">
7180
+ import "https://esm.sh/@soederpop/luca/web"
7181
+ const { luca: container } = window
7182
+
7183
+ // Load a script from a CDN
7184
+ const assetLoader = container.feature('assetLoader')
7185
+ await assetLoader.loadScript('https://cdn.jsdelivr.net/npm/chart.js')
7186
+
7187
+ // Load a stylesheet
7188
+ await assetLoader.loadStylesheet('https://cdn.jsdelivr.net/npm/water.css@2/out/water.css')
7189
+
7190
+ // Text-to-speech
7191
+ const speech = container.feature('speech')
7192
+ speech.speak('Hello from Luca')
7193
+
7194
+ // Voice recognition
7195
+ const voice = container.feature('voice')
7196
+ voice.on('transcript', ({ text }) => console.log('Heard:', text))
7197
+ voice.start()
7198
+ </script>
7199
+ \`\`\`
7200
+
7201
+ ## State and Events
7202
+
7203
+ The container is a state machine and event bus. This works identically to the server.
7204
+
7205
+ \`\`\`html
7206
+ <script type="module">
7207
+ import container from "https://esm.sh/@soederpop/luca/web"
7208
+
7209
+ // Listen for state changes
7210
+ container.on('stateChanged', ({ changes }) => {
7211
+ console.log('State changed:', changes)
7212
+ })
7213
+
7214
+ // Feature-level state and events
7215
+ const voice = container.feature('voice')
7216
+ voice.on('stateChanged', ({ changes }) => {
7217
+ document.getElementById('status').textContent = changes.listening ? 'Listening...' : 'Idle'
7218
+ })
7219
+ </script>
7220
+ \`\`\`
7221
+
7222
+ ## REST Client
7223
+
7224
+ Make HTTP requests with the built-in REST client. Methods return parsed JSON directly.
7225
+
7226
+ \`\`\`html
7227
+ <script type="module">
7228
+ import container from "https://esm.sh/@soederpop/luca/web"
7229
+
7230
+ const api = container.client('rest', { baseURL: 'https://jsonplaceholder.typicode.com' })
7231
+ const posts = await api.get('/posts')
7232
+ console.log(posts) // array of post objects, not a Response wrapper
7233
+ </script>
7234
+ \`\`\`
7235
+
7236
+ ## WebSocket Client
7237
+
7238
+ Connect to a WebSocket server:
7239
+
7240
+ \`\`\`html
7241
+ <script type="module">
7242
+ import container from "https://esm.sh/@soederpop/luca/web"
7243
+
7244
+ const socket = container.client('socket', { url: 'ws://localhost:3000' })
7245
+ socket.on('message', (data) => console.log('Received:', data))
7246
+ socket.send({ type: 'hello' })
7247
+ </script>
7248
+ \`\`\`
7249
+
7250
+ ## Extending: Custom Features
7251
+
7252
+ The container exposes the \`Feature\` class directly, so you can create your own features without any additional imports.
7253
+
7254
+ \`\`\`html
7255
+ <script type="module">
7256
+ import container from "https://esm.sh/@soederpop/luca/web"
7257
+
7258
+ const { Feature } = container
7259
+
7260
+ class Theme extends Feature {
7261
+ static shortcut = 'features.theme'
7262
+ static { Feature.register(this, 'theme') }
7263
+
7264
+ get current() {
7265
+ return this.state.get('mode') || 'light'
7266
+ }
7267
+
7268
+ toggle() {
7269
+ const next = this.current === 'light' ? 'dark' : 'light'
7270
+ this.state.set('mode', next)
7271
+ document.documentElement.setAttribute('data-theme', next)
7272
+ this.emit('themeChanged', { mode: next })
7273
+ }
7274
+ }
7275
+
7276
+ const theme = container.feature('theme')
7277
+ theme.on('themeChanged', ({ mode }) => console.log('Theme:', mode))
7278
+ theme.toggle() // => Theme: dark
7279
+ </script>
7280
+ \`\`\`
7281
+
7282
+ ## Utilities
7283
+
7284
+ The container's built-in utilities are available in the browser too.
7285
+
7286
+ \`\`\`html
7287
+ <script type="module">
7288
+ import container from "https://esm.sh/@soederpop/luca/web"
7289
+
7290
+ // UUID generation
7291
+ const id = container.utils.uuid()
7292
+
7293
+ // Lodash helpers
7294
+ const { groupBy, keyBy, pick } = container.utils.lodash
7295
+
7296
+ // String utilities
7297
+ const { camelCase, kebabCase } = container.utils.stringUtils
7298
+ </script>
7299
+ \`\`\`
7300
+
7301
+ ## Full Example: A Minimal App
7302
+
7303
+ \`\`\`html
7304
+ <!DOCTYPE html>
7305
+ <html lang="en">
7306
+ <head>
7307
+ <meta charset="UTF-8">
7308
+ <title>Luca Browser Demo</title>
7309
+ </head>
7310
+ <body>
7311
+ <h1>Luca Browser Demo</h1>
7312
+ <button id="speak">Speak</button>
7313
+ <button id="theme">Toggle Theme</button>
7314
+ <pre id="output"></pre>
7315
+
7316
+ <script type="module">
7317
+ import container from "https://esm.sh/@soederpop/luca/web"
7318
+
7319
+ const log = (msg) => {
7320
+ document.getElementById('output').textContent += msg + '\\n'
7321
+ }
7322
+
7323
+ // Load a stylesheet
7324
+ const assets = container.feature('assetLoader')
7325
+ await assets.loadStylesheet('https://cdn.jsdelivr.net/npm/water.css@2/out/water.css')
7326
+
7327
+ // Custom feature
7328
+ const { Feature } = container
7329
+
7330
+ class Theme extends Feature {
7331
+ static shortcut = 'features.theme'
7332
+ static { Feature.register(this, 'theme') }
7333
+
7334
+ toggle() {
7335
+ const next = (this.state.get('mode') || 'light') === 'light' ? 'dark' : 'light'
7336
+ this.state.set('mode', next)
7337
+ document.documentElement.style.colorScheme = next
7338
+ this.emit('themeChanged', { mode: next })
7339
+ }
7340
+ }
7341
+
7342
+ const theme = container.feature('theme')
7343
+ theme.on('themeChanged', ({ mode }) => log(\`Theme: \${mode}\`))
7344
+
7345
+ // Speech
7346
+ const speech = container.feature('speech')
7347
+
7348
+ document.getElementById('speak').onclick = () => speech.speak('Hello from Luca')
7349
+ document.getElementById('theme').onclick = () => theme.toggle()
7350
+
7351
+ log(\`Container ID: \${container.uuid}\`)
7352
+ log(\`Features: \${container.features.available.join(', ')}\`)
7353
+ </script>
7354
+ </body>
7355
+ </html>
7356
+ \`\`\`
7357
+
7358
+ Save this as an HTML file, open it in a browser, and everything works — no npm, no bundler, no build step.
7359
+
7360
+ ## Gotchas
7361
+
7362
+ - **esm.sh caches aggressively.** Pin a version if you need stability: \`https://esm.sh/@soederpop/luca@0.0.29/web\`
7363
+ - **Browser features only.** The web container doesn't include node-specific features like \`fs\`, \`git\`, \`proc\`, or \`docker\`. If you need server features, run Luca on the server and connect via the REST or WebSocket clients.
7364
+ - **\`window.luca\` is the singleton.** Don't call \`createContainer()\` — it just warns and returns the same instance. If you need isolation, use \`container.subcontainer()\`.
7365
+ - **CORS applies.** REST client requests from the browser are subject to browser CORS rules. Your API must send the right headers.
7366
+
7367
+ ## What's Next
7368
+
7369
+ - [State and Events](./05-state-and-events.md) — deep dive into the state machine and event bus (works identically in the browser)
7370
+ - [Creating Features](./10-creating-features.md) — full anatomy of a feature with schemas, state, and events
7371
+ - [Clients](./09-clients.md) — REST and WebSocket client APIs
7444
7372
  `,
7445
7373
  "15-project-patterns.md": `---
7446
7374
  title: Project Patterns and Recipes
@@ -1,4 +1,4 @@
1
1
  // Generated at compile time — do not edit manually
2
- export const BUILD_SHA = 'c1e466d'
2
+ export const BUILD_SHA = 'ff23ed7'
3
3
  export const BUILD_BRANCH = 'main'
4
- export const BUILD_DATE = '2026-03-24T01:41:40Z'
4
+ export const BUILD_DATE = '2026-03-24T09:08:07Z'
@@ -50,7 +50,7 @@ export class RestClient<
50
50
  }
51
51
  }
52
52
 
53
- async beforeRequest() {
53
+ async beforeRequest(): Promise<void> {
54
54
  }
55
55
 
56
56
  /** Whether JSON content-type headers should be set automatically. */
@@ -71,7 +71,7 @@ export class RestClient<
71
71
  * @param options - Additional axios request config
72
72
  * @returns Parsed response body
73
73
  */
74
- async patch(url: string, data: any = {}, options: AxiosRequestConfig = {}) {
74
+ async patch(url: string, data: any = {}, options: AxiosRequestConfig = {}): Promise<any> {
75
75
  await this.beforeRequest();
76
76
  return this.axios({
77
77
  ...options,
@@ -98,7 +98,7 @@ export class RestClient<
98
98
  * @param options - Additional axios request config
99
99
  * @returns Parsed response body
100
100
  */
101
- async put(url: string, data: any = {}, options: AxiosRequestConfig = {}) {
101
+ async put(url: string, data: any = {}, options: AxiosRequestConfig = {}): Promise<any> {
102
102
  await this.beforeRequest();
103
103
  return this.axios({
104
104
  ...options,
@@ -125,7 +125,7 @@ export class RestClient<
125
125
  * @param options - Additional axios request config
126
126
  * @returns Parsed response body
127
127
  */
128
- async post(url: string, data: any = {}, options: AxiosRequestConfig = {}) {
128
+ async post(url: string, data: any = {}, options: AxiosRequestConfig = {}): Promise<any> {
129
129
  await this.beforeRequest();
130
130
  return this.axios({
131
131
  ...options,
@@ -152,7 +152,7 @@ export class RestClient<
152
152
  * @param options - Additional axios request config
153
153
  * @returns Parsed response body
154
154
  */
155
- async delete(url: string, params: any = {}, options: AxiosRequestConfig = {}) {
155
+ async delete(url: string, params: any = {}, options: AxiosRequestConfig = {}): Promise<any> {
156
156
  await this.beforeRequest();
157
157
  return this.axios({
158
158
  ...options,
@@ -179,7 +179,7 @@ export class RestClient<
179
179
  * @param options - Additional axios request config
180
180
  * @returns Parsed response body
181
181
  */
182
- async get(url: string, params: any = {}, options: AxiosRequestConfig = {}) {
182
+ async get(url: string, params: any = {}, options: AxiosRequestConfig = {}): Promise<any> {
183
183
  await this.beforeRequest()
184
184
  return this.axios({
185
185
  ...options,
@@ -198,7 +198,7 @@ export class RestClient<
198
198
  }
199
199
 
200
200
  /** Handle an axios error by emitting 'failure' and returning the error as JSON. */
201
- async handleError(error: AxiosError) {
201
+ async handleError(error: AxiosError): Promise<object> {
202
202
  this.emit('failure', error)
203
203
  return error.toJSON();
204
204
  }
package/src/command.ts CHANGED
@@ -109,7 +109,26 @@ export class Command<
109
109
  const named = this._normalizeInput(raw, dispatchSource)
110
110
 
111
111
  // Validate against argsSchema
112
- const parsed = Cls.argsSchema.parse(named)
112
+ let parsed: any
113
+ try {
114
+ parsed = Cls.argsSchema.parse(named)
115
+ } catch (err: any) {
116
+ if (err?.name === 'ZodError' && dispatchSource === 'cli') {
117
+ const ui = (this.container as any).feature('ui')
118
+ const cmdName = Cls.shortcut?.replace('commands.', '') || 'unknown'
119
+ const issues = err.issues || []
120
+
121
+ ui.print.red(`\n Error: Invalid options for "${cmdName}"\n`)
122
+ for (const issue of issues) {
123
+ const path = issue.path?.length ? issue.path.join('.') : 'input'
124
+ ui.print(` ${ui.colors.yellow('→')} ${ui.colors.bold(path)}: ${issue.message}`)
125
+ }
126
+ ui.print('')
127
+ ui.print.dim(` Run ${ui.colors.cyan(`luca ${cmdName} --help`)} for usage info.\n`)
128
+ process.exit(1)
129
+ }
130
+ throw err
131
+ }
113
132
 
114
133
  // For headless dispatch, capture stdout/stderr
115
134
  if (dispatchSource !== 'cli') {