@soederpop/luca 0.0.28 → 0.0.30

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 (51) hide show
  1. package/commands/try-all-challenges.ts +1 -1
  2. package/docs/TABLE-OF-CONTENTS.md +0 -3
  3. package/docs/examples/structured-output-with-assistants.md +144 -0
  4. package/docs/tutorials/20-browser-esm.md +234 -0
  5. package/package.json +1 -1
  6. package/src/agi/container.server.ts +4 -0
  7. package/src/agi/features/assistant.ts +132 -2
  8. package/src/agi/features/browser-use.ts +623 -0
  9. package/src/agi/features/conversation.ts +135 -45
  10. package/src/agi/lib/interceptor-chain.ts +79 -0
  11. package/src/bootstrap/generated.ts +381 -308
  12. package/src/cli/build-info.ts +2 -2
  13. package/src/clients/rest.ts +7 -7
  14. package/src/commands/chat.ts +22 -0
  15. package/src/commands/describe.ts +67 -2
  16. package/src/commands/prompt.ts +23 -3
  17. package/src/container.ts +411 -113
  18. package/src/helper.ts +189 -5
  19. package/src/introspection/generated.agi.ts +17664 -11568
  20. package/src/introspection/generated.node.ts +4891 -1860
  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 +37 -16
  29. package/src/node/features/fs.ts +64 -25
  30. package/src/node/features/git.ts +10 -10
  31. package/src/node/features/helpers.ts +25 -18
  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 +6 -6
  42. package/src/servers/mcp.ts +4 -4
  43. package/src/servers/socket.ts +6 -6
  44. package/test/interceptor-chain.test.ts +61 -0
  45. package/docs/apis/features/node/window-manager.md +0 -445
  46. package/docs/examples/window-manager-layouts.md +0 -180
  47. package/docs/examples/window-manager.md +0 -125
  48. package/docs/window-manager-fix.md +0 -249
  49. package/scripts/test-window-manager-lifecycle.ts +0 -86
  50. package/scripts/test-window-manager.ts +0 -43
  51. package/src/node/features/window-manager.ts +0 -1603
@@ -1,5 +1,5 @@
1
1
  // Auto-generated bootstrap content
2
- // Generated at: 2026-03-23T07:45:58.711Z
2
+ // Generated at: 2026-03-24T06:38:37.146Z
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
@@ -1633,6 +1633,151 @@ for (const raw of commands) {
1633
1633
  ## Summary
1634
1634
 
1635
1635
  This demo covered the three main methods of the \`nlp\` feature: \`parse()\` for quick structural extraction from voice commands, \`analyze()\` for detailed POS tagging and entity recognition, and \`understand()\` for a combined view of both. The feature is well suited for building voice command interpreters, chatbot intent classifiers, and text analysis pipelines.
1636
+ `,
1637
+ "structured-output-with-assistants.md": `---
1638
+ title: "Structured Output with Assistants"
1639
+ tags: [assistant, conversation, structured-output, zod, openai]
1640
+ lastTested: null
1641
+ lastTestPassed: null
1642
+ ---
1643
+
1644
+ # Structured Output with Assistants
1645
+
1646
+ Get typed, schema-validated JSON responses from OpenAI instead of raw text strings.
1647
+
1648
+ ## Overview
1649
+
1650
+ OpenAI's Structured Outputs feature constrains the model to return JSON that exactly matches a schema you provide. Combined with Zod, this means \`ask()\` can return parsed objects instead of strings — no regex parsing, no "please respond in JSON", no malformed output.
1651
+
1652
+ Pass a \`schema\` option to \`ask()\` and the response comes back as a parsed object guaranteed to match your schema.
1653
+
1654
+ ## Basic: Extract Structured Data
1655
+
1656
+ The simplest use case — ask a question and get structured data back.
1657
+
1658
+ \`\`\`ts
1659
+ const { z } = container
1660
+ const conversation = container.feature('conversation', {
1661
+ model: 'gpt-4.1-mini',
1662
+ history: [{ role: 'system', content: 'You are a helpful data extraction assistant.' }]
1663
+ })
1664
+
1665
+ const result = await conversation.ask('The founders of Apple are Steve Jobs, Steve Wozniak, and Ronald Wayne. They started it in 1976 in Los Altos, California.', {
1666
+ schema: z.object({
1667
+ company: z.string(),
1668
+ foundedYear: z.number(),
1669
+ location: z.string(),
1670
+ founders: z.array(z.string()),
1671
+ }).describe('CompanyInfo')
1672
+ })
1673
+
1674
+ console.log('Company:', result.company)
1675
+ console.log('Founded:', result.foundedYear)
1676
+ console.log('Location:', result.location)
1677
+ console.log('Founders:', result.founders)
1678
+ \`\`\`
1679
+
1680
+ The \`.describe()\` on the schema gives OpenAI the schema name — keep it short and descriptive.
1681
+
1682
+ ## Enums and Categorization
1683
+
1684
+ Structured outputs work great for classification tasks where you want the model to pick from a fixed set of values.
1685
+
1686
+ \`\`\`ts
1687
+ const { z } = container
1688
+ const conversation = container.feature('conversation', {
1689
+ model: 'gpt-4.1-mini',
1690
+ history: [{ role: 'system', content: 'You are a helpful assistant.' }]
1691
+ })
1692
+
1693
+ const sentiment = await conversation.ask('I absolutely love this product, it changed my life!', {
1694
+ schema: z.object({
1695
+ sentiment: z.enum(['positive', 'negative', 'neutral', 'mixed']),
1696
+ confidence: z.number(),
1697
+ reasoning: z.string(),
1698
+ }).describe('SentimentAnalysis')
1699
+ })
1700
+
1701
+ console.log('Sentiment:', sentiment.sentiment)
1702
+ console.log('Confidence:', sentiment.confidence)
1703
+ console.log('Reasoning:', sentiment.reasoning)
1704
+ \`\`\`
1705
+
1706
+ Because the model is constrained by the schema, \`sentiment\` will always be one of the four allowed values.
1707
+
1708
+ ## Nested Objects and Arrays
1709
+
1710
+ Schemas can be as complex as you need. Here we extract a structured analysis with nested objects.
1711
+
1712
+ \`\`\`ts
1713
+ const { z } = container
1714
+ const conversation = container.feature('conversation', {
1715
+ model: 'gpt-4.1-mini',
1716
+ history: [{ role: 'system', content: 'You are a technical analyst.' }]
1717
+ })
1718
+
1719
+ const analysis = await conversation.ask(
1720
+ 'TypeScript 5.5 introduced inferred type predicates, which automatically narrow types in filter callbacks. It also added isolated declarations for faster builds in monorepos, and a new regex syntax checking feature.',
1721
+ {
1722
+ schema: z.object({
1723
+ subject: z.string(),
1724
+ version: z.string(),
1725
+ features: z.array(z.object({
1726
+ name: z.string(),
1727
+ category: z.enum(['type-system', 'performance', 'developer-experience', 'syntax', 'other']),
1728
+ summary: z.string(),
1729
+ })),
1730
+ featureCount: z.number(),
1731
+ }).describe('ReleaseAnalysis')
1732
+ }
1733
+ )
1734
+
1735
+ console.log('Subject:', analysis.subject, analysis.version)
1736
+ console.log('Features:')
1737
+ for (const f of analysis.features) {
1738
+ console.log(\` [\${f.category}] \${f.name}: \${f.summary}\`)
1739
+ }
1740
+ console.log('Total features:', analysis.featureCount)
1741
+ \`\`\`
1742
+
1743
+ Every level of nesting is validated — the model cannot return a feature without a category or skip required fields.
1744
+
1745
+ ## With an Assistant
1746
+
1747
+ Structured outputs work the same way through the assistant API. The schema passes straight through to the underlying conversation.
1748
+
1749
+ \`\`\`ts
1750
+ const { z } = container
1751
+ const assistant = container.feature('assistant', {
1752
+ systemPrompt: 'You are a code review assistant. You analyze code snippets and provide structured feedback.',
1753
+ model: 'gpt-4.1-mini',
1754
+ })
1755
+
1756
+ const review = await assistant.ask(
1757
+ 'Review this: function add(a, b) { return a + b }',
1758
+ {
1759
+ schema: z.object({
1760
+ issues: z.array(z.object({
1761
+ severity: z.enum(['info', 'warning', 'error']),
1762
+ message: z.string(),
1763
+ })),
1764
+ suggestion: z.string(),
1765
+ score: z.number(),
1766
+ }).describe('CodeReview')
1767
+ }
1768
+ )
1769
+
1770
+ console.log('Score:', review.score)
1771
+ console.log('Suggestion:', review.suggestion)
1772
+ console.log('Issues:')
1773
+ for (const issue of review.issues) {
1774
+ console.log(\` [\${issue.severity}] \${issue.message}\`)
1775
+ }
1776
+ \`\`\`
1777
+
1778
+ ## Summary
1779
+
1780
+ This demo covered extracting structured data, classification with enums, nested schema validation, and using structured outputs through both the conversation and assistant APIs. The key is passing a Zod schema via \`{ schema }\` in the options to \`ask()\` — OpenAI guarantees the response matches, and you get a parsed object back.
1636
1781
  `,
1637
1782
  "networking.md": `---
1638
1783
  title: "networking"
@@ -2628,132 +2773,6 @@ Supported loaders include \`ts\` (default), \`tsx\`, \`jsx\`, and \`js\`.
2628
2773
  ## Summary
2629
2774
 
2630
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.
2631
- `,
2632
- "window-manager.md": `---
2633
- title: "Window Manager"
2634
- tags: [windowManager, native, ipc, macos, browser, window]
2635
- lastTested: null
2636
- lastTestPassed: null
2637
- ---
2638
-
2639
- # windowManager
2640
-
2641
- 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.
2642
-
2643
- ## Overview
2644
-
2645
- 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.
2646
-
2647
- Requires the LucaVoiceLauncher native macOS app to be running and connected.
2648
-
2649
- ## Enabling the Feature
2650
-
2651
- \`\`\`ts
2652
- const wm = container.feature('windowManager', {
2653
- autoListen: false,
2654
- requestTimeoutMs: 10000
2655
- })
2656
- console.log('Window Manager feature created')
2657
- console.log('Listening:', wm.isListening)
2658
- console.log('Client connected:', wm.isClientConnected)
2659
- \`\`\`
2660
-
2661
- ## API Documentation
2662
-
2663
- \`\`\`ts
2664
- const info = await container.features.describe('windowManager')
2665
- console.log(info)
2666
- \`\`\`
2667
-
2668
- ## Spawning Windows
2669
-
2670
- Create native browser windows with configurable dimensions and chrome.
2671
-
2672
- \`\`\`ts skip
2673
- const result = await wm.spawn({
2674
- url: 'https://google.com',
2675
- width: 1024,
2676
- height: 768,
2677
- alwaysOnTop: true,
2678
- window: { decorations: 'hiddenTitleBar', shadow: true }
2679
- })
2680
- console.log('Window ID:', result.windowId)
2681
- \`\`\`
2682
-
2683
- 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.
2684
-
2685
- ## Navigation and JavaScript Evaluation
2686
-
2687
- Control window content after spawning.
2688
-
2689
- \`\`\`ts skip
2690
- const handle = wm.window(result.windowId)
2691
- await handle.navigate('https://news.ycombinator.com')
2692
- console.log('Navigated')
2693
-
2694
- const title = await handle.eval('document.title')
2695
- console.log('Page title:', title)
2696
-
2697
- await handle.focus()
2698
- await handle.close()
2699
- \`\`\`
2700
-
2701
- The \`window()\` method returns a \`WindowHandle\` for chainable operations on a specific window. Use \`eval()\` to run JavaScript in the window's web view.
2702
-
2703
- ## Screenshots and Video
2704
-
2705
- Capture visual output from windows.
2706
-
2707
- \`\`\`ts skip
2708
- await wm.screengrab({
2709
- windowId: result.windowId,
2710
- path: './screenshot.png'
2711
- })
2712
- console.log('Screenshot saved')
2713
-
2714
- await wm.video({
2715
- windowId: result.windowId,
2716
- path: './recording.mp4',
2717
- durationMs: 5000
2718
- })
2719
- console.log('Video recorded')
2720
- \`\`\`
2721
-
2722
- Screenshots are saved as PNG. Video recording captures for the specified duration.
2723
-
2724
- ## Terminal Windows
2725
-
2726
- Spawn native terminal windows that render command output with ANSI support.
2727
-
2728
- \`\`\`ts skip
2729
- const tty = await wm.spawnTTY({
2730
- command: 'htop',
2731
- title: 'System Monitor',
2732
- width: 900,
2733
- height: 600,
2734
- cols: 120,
2735
- rows: 40
2736
- })
2737
- console.log('TTY window:', tty.windowId)
2738
- \`\`\`
2739
-
2740
- Terminal windows are read-only displays of process output. Closing the window terminates the process.
2741
-
2742
- ## IPC Communication
2743
-
2744
- Other features can send arbitrary messages over the socket connection.
2745
-
2746
- \`\`\`ts skip
2747
- wm.listen()
2748
- wm.on('message', (msg) => console.log('App says:', msg))
2749
- wm.send({ id: 'abc', status: 'ready', speech: 'Window manager online' })
2750
- \`\`\`
2751
-
2752
- The \`message\` event fires for any non-windowAck message from the native app.
2753
-
2754
- ## Summary
2755
-
2756
- 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()\`.
2757
2776
  `,
2758
2777
  "proc.md": `---
2759
2778
  title: "proc"
@@ -2907,187 +2926,6 @@ console.log('Downloader is ready. Call downloader.download(url, path) to fetch f
2907
2926
  ## Summary
2908
2927
 
2909
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.
2910
- `,
2911
- "window-manager-layouts.md": `---
2912
- title: "Window Manager Layouts"
2913
- tags: [windowManager, layout, native, ipc, macos, multi-window]
2914
- lastTested: null
2915
- lastTestPassed: null
2916
- ---
2917
-
2918
- # Window Manager Layouts
2919
-
2920
- 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.
2921
-
2922
- ## Overview
2923
-
2924
- The \`windowManager\` feature exposes two layout methods:
2925
-
2926
- - **\`spawnLayout(config)\`** — spawns all entries in parallel, returns \`WindowHandle[]\`
2927
- - **\`spawnLayouts(configs)\`** — spawns multiple layouts sequentially (each layout's windows still spawn in parallel), returns \`WindowHandle[][]\`
2928
-
2929
- 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.
2930
-
2931
- ## Setup
2932
-
2933
- \`\`\`ts
2934
- const wm = container.feature('windowManager', {
2935
- autoListen: true,
2936
- requestTimeoutMs: 10000
2937
- })
2938
- console.log('Window Manager ready')
2939
- \`\`\`
2940
-
2941
- ## Single Layout — Parallel Windows
2942
-
2943
- Spawn a mix of browser and terminal windows that all open at the same time.
2944
-
2945
- \`\`\`ts
2946
- const handles = await wm.spawnLayout([
2947
- { url: 'https://github.com', width: '50%', height: '100%', x: 0, y: 0 },
2948
- { url: 'https://soederpop.com', width: '50%', height: '100%', x: '50%', y: 0 },
2949
- { command: 'top', title: 'System Monitor', width: 900, height: 400, x: 0, y: 720 },
2950
- ])
2951
-
2952
- console.log('Spawned', handles.length, 'windows')
2953
- handles.forEach((h, i) => console.log(\` [\${i}] windowId: \${h.windowId}\`))
2954
- \`\`\`
2955
-
2956
- 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.
2957
-
2958
- ## Percentage-Based Dimensions
2959
-
2960
- Dimensions (\`width\`, \`height\`, \`x\`, \`y\`) accept percentage strings resolved against the primary display. This makes layouts portable across different screen resolutions.
2961
-
2962
- \`\`\`ts skip
2963
- // Side-by-side: two windows each taking half the screen
2964
- const handles = await wm.spawnLayout([
2965
- { url: 'https://github.com', width: '50%', height: '100%', x: '0%', y: '0%' },
2966
- { url: 'https://news.ycombinator.com', width: '50%', height: '100%', x: '50%', y: '0%' },
2967
- ])
2968
- \`\`\`
2969
-
2970
- You can mix absolute and percentage values freely:
2971
-
2972
- \`\`\`ts skip
2973
- const handles = await wm.spawnLayout([
2974
- { url: 'https://example.com', width: '75%', height: 600, x: '12.5%', y: 50 },
2975
- { command: 'htop', width: '100%', height: '30%', x: '0%', y: '70%' },
2976
- ])
2977
- \`\`\`
2978
-
2979
- ## Explicit Type Field
2980
-
2981
- 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.
2982
-
2983
- \`\`\`ts skip
2984
- const handles = await wm.spawnLayout([
2985
- { type: 'window', url: 'https://example.com', width: 800, height: 600 },
2986
- { type: 'tty', command: 'tail -f /var/log/system.log', title: 'Logs' },
2987
- ])
2988
-
2989
- console.log('Browser window:', handles[0].windowId)
2990
- console.log('TTY window:', handles[1].windowId)
2991
- \`\`\`
2992
-
2993
- ## Sequential Layouts
2994
-
2995
- 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.
2996
-
2997
- \`\`\`ts skip
2998
- const [dashboards, tools] = await wm.spawnLayouts([
2999
- // First batch: main content
3000
- [
3001
- { url: 'https://grafana.internal/d/api-latency', width: 960, height: 800, x: 0, y: 0 },
3002
- { url: 'https://grafana.internal/d/error-rate', width: 960, height: 800, x: 970, y: 0 },
3003
- ],
3004
- // Second batch: supporting tools (opens after dashboards are ready)
3005
- [
3006
- { command: 'htop', title: 'CPU', width: 640, height: 400, x: 0, y: 820 },
3007
- { command: 'tail -f /var/log/system.log', title: 'Logs', width: 640, height: 400, x: 650, y: 820 },
3008
- ],
3009
- ])
3010
-
3011
- console.log('Dashboards:', dashboards.map(h => h.windowId))
3012
- console.log('Tools:', tools.map(h => h.windowId))
3013
- \`\`\`
3014
-
3015
- This is useful when the second batch depends on the first being visible — for example, positioning tool windows below dashboard windows.
3016
-
3017
- ## Lifecycle Events on Layout Handles
3018
-
3019
- 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.
3020
-
3021
- \`\`\`ts skip
3022
- const handles = await wm.spawnLayout([
3023
- { url: 'https://example.com', width: 800, height: 600 },
3024
- { command: 'sleep 5 && echo done', title: 'Short Task' },
3025
- ])
3026
-
3027
- const [browser, terminal] = handles
3028
-
3029
- browser.on('close', () => console.log('Browser window closed'))
3030
-
3031
- terminal.on('terminalExited', (info) => {
3032
- console.log('Terminal process finished:', info)
3033
- })
3034
- \`\`\`
3035
-
3036
- ## Window Chrome Options
3037
-
3038
- Layout entries support all the same window chrome options as regular \`spawn()\` calls.
3039
-
3040
- \`\`\`ts skip
3041
- const handles = await wm.spawnLayout([
3042
- {
3043
- url: 'https://example.com',
3044
- width: 400,
3045
- height: 300,
3046
- alwaysOnTop: true,
3047
- window: {
3048
- decorations: 'hiddenTitleBar',
3049
- transparent: true,
3050
- shadow: true,
3051
- opacity: 0.9,
3052
- }
3053
- },
3054
- {
3055
- url: 'https://example.com/overlay',
3056
- width: 200,
3057
- height: 100,
3058
- window: {
3059
- decorations: 'none',
3060
- clickThrough: true,
3061
- transparent: true,
3062
- }
3063
- },
3064
- ])
3065
- \`\`\`
3066
-
3067
- ## Operating on All Handles
3068
-
3069
- Since layouts return arrays of \`WindowHandle\`, you can easily batch operations across all windows.
3070
-
3071
- \`\`\`ts skip
3072
- const handles = await wm.spawnLayout([
3073
- { url: 'https://example.com/a', width: 600, height: 400 },
3074
- { url: 'https://example.com/b', width: 600, height: 400 },
3075
- { url: 'https://example.com/c', width: 600, height: 400 },
3076
- ])
3077
-
3078
- // Navigate all windows to the same URL
3079
- await Promise.all(handles.map(h => h.navigate('https://example.com/updated')))
3080
-
3081
- // Screenshot all windows
3082
- await Promise.all(handles.map((h, i) => h.screengrab(\`./layout-\${i}.png\`)))
3083
-
3084
- // Close all windows
3085
- await Promise.all(handles.map(h => h.close()))
3086
- \`\`\`
3087
-
3088
- ## Summary
3089
-
3090
- 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.
3091
2929
  `,
3092
2930
  "google-docs.md": `---
3093
2931
  title: "Google Docs"
@@ -7296,6 +7134,241 @@ const docs = container.inspectAsText()
7296
7134
  \`\`\`
7297
7135
 
7298
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
7299
7372
  `,
7300
7373
  "15-project-patterns.md": `---
7301
7374
  title: Project Patterns and Recipes