@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.
- package/commands/try-all-challenges.ts +1 -1
- package/docs/TABLE-OF-CONTENTS.md +0 -3
- package/docs/tutorials/20-browser-esm.md +234 -0
- package/package.json +1 -1
- package/src/agi/container.server.ts +4 -0
- package/src/agi/features/assistant.ts +120 -3
- package/src/agi/features/browser-use.ts +623 -0
- package/src/bootstrap/generated.ts +236 -308
- package/src/cli/build-info.ts +2 -2
- package/src/clients/rest.ts +7 -7
- package/src/command.ts +20 -1
- package/src/commands/chat.ts +22 -0
- package/src/commands/describe.ts +67 -2
- package/src/commands/prompt.ts +23 -3
- package/src/commands/serve.ts +27 -0
- package/src/container.ts +411 -113
- package/src/endpoint.ts +6 -0
- package/src/helper.ts +226 -5
- package/src/introspection/generated.agi.ts +16089 -10021
- package/src/introspection/generated.node.ts +5102 -2077
- package/src/introspection/generated.web.ts +379 -291
- package/src/introspection/index.ts +7 -0
- package/src/introspection/scan.ts +224 -7
- package/src/node/container.ts +31 -10
- package/src/node/features/content-db.ts +7 -7
- package/src/node/features/disk-cache.ts +11 -11
- package/src/node/features/esbuild.ts +3 -3
- package/src/node/features/file-manager.ts +15 -15
- package/src/node/features/fs.ts +23 -22
- package/src/node/features/git.ts +10 -10
- package/src/node/features/helpers.ts +5 -2
- package/src/node/features/ink.ts +13 -13
- package/src/node/features/ipc-socket.ts +8 -8
- package/src/node/features/networking.ts +3 -3
- package/src/node/features/os.ts +7 -7
- package/src/node/features/package-finder.ts +15 -15
- package/src/node/features/proc.ts +1 -1
- package/src/node/features/ui.ts +13 -13
- package/src/node/features/vm.ts +4 -4
- package/src/scaffolds/generated.ts +1 -1
- package/src/servers/express.ts +24 -6
- package/src/servers/mcp.ts +4 -4
- package/src/servers/socket.ts +6 -6
- package/docs/apis/features/node/window-manager.md +0 -445
- package/docs/examples/window-manager-layouts.md +0 -180
- package/docs/examples/window-manager.md +0 -125
- package/docs/window-manager-fix.md +0 -249
- package/scripts/test-window-manager-lifecycle.ts +0 -86
- package/scripts/test-window-manager.ts +0 -43
- package/src/node/features/window-manager.ts +0 -1603
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Auto-generated bootstrap content
|
|
2
|
-
// Generated at: 2026-03-
|
|
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
|
package/src/cli/build-info.ts
CHANGED
package/src/clients/rest.ts
CHANGED
|
@@ -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
|
-
|
|
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') {
|