@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.
- package/commands/try-all-challenges.ts +1 -1
- package/docs/TABLE-OF-CONTENTS.md +0 -3
- package/docs/examples/structured-output-with-assistants.md +144 -0
- 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 +132 -2
- package/src/agi/features/browser-use.ts +623 -0
- package/src/agi/features/conversation.ts +135 -45
- package/src/agi/lib/interceptor-chain.ts +79 -0
- package/src/bootstrap/generated.ts +381 -308
- package/src/cli/build-info.ts +2 -2
- package/src/clients/rest.ts +7 -7
- package/src/commands/chat.ts +22 -0
- package/src/commands/describe.ts +67 -2
- package/src/commands/prompt.ts +23 -3
- package/src/container.ts +411 -113
- package/src/helper.ts +189 -5
- package/src/introspection/generated.agi.ts +17664 -11568
- package/src/introspection/generated.node.ts +4891 -1860
- 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 +37 -16
- package/src/node/features/fs.ts +64 -25
- package/src/node/features/git.ts +10 -10
- package/src/node/features/helpers.ts +25 -18
- 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 +6 -6
- package/src/servers/mcp.ts +4 -4
- package/src/servers/socket.ts +6 -6
- package/test/interceptor-chain.test.ts +61 -0
- 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-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
|