@soederpop/luca 0.0.29 → 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/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 +62 -1
- 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/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 +17148 -11148
- package/src/introspection/generated.node.ts +5179 -2200
- 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/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/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
|
@@ -241,7 +241,7 @@ Be specific and actionable. Reference concrete file paths, APIs, and patterns. T
|
|
|
241
241
|
'--in-folder', sessionFolder,
|
|
242
242
|
'--out-file', `${sessionFolder}/logs/synthesis-session.md`,
|
|
243
243
|
'--dont-touch-file',
|
|
244
|
-
'--
|
|
244
|
+
'--include-frontmatter',
|
|
245
245
|
], {
|
|
246
246
|
onOutput: (str: string) => { process.stdout.write(str) },
|
|
247
247
|
onError: (str: string) => { process.stderr.write(str) },
|
|
@@ -57,8 +57,6 @@
|
|
|
57
57
|
- [ui](./examples/ui.md)
|
|
58
58
|
- [Vault](./examples/vault.md)
|
|
59
59
|
- [vm](./examples/vm.md)
|
|
60
|
-
- [Window Manager](./examples/window-manager.md)
|
|
61
|
-
- [Window Manager Layouts](./examples/window-manager-layouts.md)
|
|
62
60
|
- [YAML](./examples/yaml.md)
|
|
63
61
|
- [YAML Tree](./examples/yaml-tree.md)
|
|
64
62
|
|
|
@@ -159,7 +157,6 @@
|
|
|
159
157
|
- [UI (features.ui)](./apis/features/node/ui.md)
|
|
160
158
|
- [WebVault (features.vault)](./apis/features/node/vault.md)
|
|
161
159
|
- [VM (features.vm)](./apis/features/node/vm.md)
|
|
162
|
-
- [WindowManager (features.windowManager)](./apis/features/node/window-manager.md)
|
|
163
160
|
- [YAML (features.yaml)](./apis/features/node/yaml.md)
|
|
164
161
|
- [YamlTree (features.yamlTree)](./apis/features/node/yaml-tree.md)
|
|
165
162
|
- [AssetLoader (features.assetLoader)](./apis/features/web/asset-loader.md)
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Browser: Import Luca from esm.sh"
|
|
3
|
+
tags:
|
|
4
|
+
- browser
|
|
5
|
+
- esm
|
|
6
|
+
- web
|
|
7
|
+
- quickstart
|
|
8
|
+
- cdn
|
|
9
|
+
---
|
|
10
|
+
# Browser: Import Luca from esm.sh
|
|
11
|
+
|
|
12
|
+
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.
|
|
13
|
+
|
|
14
|
+
## Basic Setup
|
|
15
|
+
|
|
16
|
+
```html
|
|
17
|
+
<script type="module">
|
|
18
|
+
import "https://esm.sh/@soederpop/luca/web"
|
|
19
|
+
|
|
20
|
+
const container = window.luca
|
|
21
|
+
console.log(container.uuid) // unique container ID
|
|
22
|
+
console.log(container.features.available) // ['assetLoader', 'voice', 'speech', 'network', 'vault', 'vm', 'esbuild', 'helpers', 'containerLink']
|
|
23
|
+
</script>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The import triggers module evaluation, which creates the `WebContainer` singleton and attaches it to `window.luca`. That's it.
|
|
27
|
+
|
|
28
|
+
If you prefer a named import:
|
|
29
|
+
|
|
30
|
+
```html
|
|
31
|
+
<script type="module">
|
|
32
|
+
import container from "https://esm.sh/@soederpop/luca/web"
|
|
33
|
+
// container === window.luca
|
|
34
|
+
</script>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Using Features
|
|
38
|
+
|
|
39
|
+
Once you have the container, features work exactly like they do on the server — lazy-loaded via `container.feature()`.
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<script type="module">
|
|
43
|
+
import "https://esm.sh/@soederpop/luca/web"
|
|
44
|
+
const { luca: container } = window
|
|
45
|
+
|
|
46
|
+
// Load a script from a CDN
|
|
47
|
+
const assetLoader = container.feature('assetLoader')
|
|
48
|
+
await assetLoader.loadScript('https://cdn.jsdelivr.net/npm/chart.js')
|
|
49
|
+
|
|
50
|
+
// Load a stylesheet
|
|
51
|
+
await assetLoader.loadStylesheet('https://cdn.jsdelivr.net/npm/water.css@2/out/water.css')
|
|
52
|
+
|
|
53
|
+
// Text-to-speech
|
|
54
|
+
const speech = container.feature('speech')
|
|
55
|
+
speech.speak('Hello from Luca')
|
|
56
|
+
|
|
57
|
+
// Voice recognition
|
|
58
|
+
const voice = container.feature('voice')
|
|
59
|
+
voice.on('transcript', ({ text }) => console.log('Heard:', text))
|
|
60
|
+
voice.start()
|
|
61
|
+
</script>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## State and Events
|
|
65
|
+
|
|
66
|
+
The container is a state machine and event bus. This works identically to the server.
|
|
67
|
+
|
|
68
|
+
```html
|
|
69
|
+
<script type="module">
|
|
70
|
+
import container from "https://esm.sh/@soederpop/luca/web"
|
|
71
|
+
|
|
72
|
+
// Listen for state changes
|
|
73
|
+
container.on('stateChanged', ({ changes }) => {
|
|
74
|
+
console.log('State changed:', changes)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// Feature-level state and events
|
|
78
|
+
const voice = container.feature('voice')
|
|
79
|
+
voice.on('stateChanged', ({ changes }) => {
|
|
80
|
+
document.getElementById('status').textContent = changes.listening ? 'Listening...' : 'Idle'
|
|
81
|
+
})
|
|
82
|
+
</script>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## REST Client
|
|
86
|
+
|
|
87
|
+
Make HTTP requests with the built-in REST client. Methods return parsed JSON directly.
|
|
88
|
+
|
|
89
|
+
```html
|
|
90
|
+
<script type="module">
|
|
91
|
+
import container from "https://esm.sh/@soederpop/luca/web"
|
|
92
|
+
|
|
93
|
+
const api = container.client('rest', { baseURL: 'https://jsonplaceholder.typicode.com' })
|
|
94
|
+
const posts = await api.get('/posts')
|
|
95
|
+
console.log(posts) // array of post objects, not a Response wrapper
|
|
96
|
+
</script>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## WebSocket Client
|
|
100
|
+
|
|
101
|
+
Connect to a WebSocket server:
|
|
102
|
+
|
|
103
|
+
```html
|
|
104
|
+
<script type="module">
|
|
105
|
+
import container from "https://esm.sh/@soederpop/luca/web"
|
|
106
|
+
|
|
107
|
+
const socket = container.client('socket', { url: 'ws://localhost:3000' })
|
|
108
|
+
socket.on('message', (data) => console.log('Received:', data))
|
|
109
|
+
socket.send({ type: 'hello' })
|
|
110
|
+
</script>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Extending: Custom Features
|
|
114
|
+
|
|
115
|
+
The container exposes the `Feature` class directly, so you can create your own features without any additional imports.
|
|
116
|
+
|
|
117
|
+
```html
|
|
118
|
+
<script type="module">
|
|
119
|
+
import container from "https://esm.sh/@soederpop/luca/web"
|
|
120
|
+
|
|
121
|
+
const { Feature } = container
|
|
122
|
+
|
|
123
|
+
class Theme extends Feature {
|
|
124
|
+
static shortcut = 'features.theme'
|
|
125
|
+
static { Feature.register(this, 'theme') }
|
|
126
|
+
|
|
127
|
+
get current() {
|
|
128
|
+
return this.state.get('mode') || 'light'
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
toggle() {
|
|
132
|
+
const next = this.current === 'light' ? 'dark' : 'light'
|
|
133
|
+
this.state.set('mode', next)
|
|
134
|
+
document.documentElement.setAttribute('data-theme', next)
|
|
135
|
+
this.emit('themeChanged', { mode: next })
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const theme = container.feature('theme')
|
|
140
|
+
theme.on('themeChanged', ({ mode }) => console.log('Theme:', mode))
|
|
141
|
+
theme.toggle() // => Theme: dark
|
|
142
|
+
</script>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Utilities
|
|
146
|
+
|
|
147
|
+
The container's built-in utilities are available in the browser too.
|
|
148
|
+
|
|
149
|
+
```html
|
|
150
|
+
<script type="module">
|
|
151
|
+
import container from "https://esm.sh/@soederpop/luca/web"
|
|
152
|
+
|
|
153
|
+
// UUID generation
|
|
154
|
+
const id = container.utils.uuid()
|
|
155
|
+
|
|
156
|
+
// Lodash helpers
|
|
157
|
+
const { groupBy, keyBy, pick } = container.utils.lodash
|
|
158
|
+
|
|
159
|
+
// String utilities
|
|
160
|
+
const { camelCase, kebabCase } = container.utils.stringUtils
|
|
161
|
+
</script>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Full Example: A Minimal App
|
|
165
|
+
|
|
166
|
+
```html
|
|
167
|
+
<!DOCTYPE html>
|
|
168
|
+
<html lang="en">
|
|
169
|
+
<head>
|
|
170
|
+
<meta charset="UTF-8">
|
|
171
|
+
<title>Luca Browser Demo</title>
|
|
172
|
+
</head>
|
|
173
|
+
<body>
|
|
174
|
+
<h1>Luca Browser Demo</h1>
|
|
175
|
+
<button id="speak">Speak</button>
|
|
176
|
+
<button id="theme">Toggle Theme</button>
|
|
177
|
+
<pre id="output"></pre>
|
|
178
|
+
|
|
179
|
+
<script type="module">
|
|
180
|
+
import container from "https://esm.sh/@soederpop/luca/web"
|
|
181
|
+
|
|
182
|
+
const log = (msg) => {
|
|
183
|
+
document.getElementById('output').textContent += msg + '\n'
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Load a stylesheet
|
|
187
|
+
const assets = container.feature('assetLoader')
|
|
188
|
+
await assets.loadStylesheet('https://cdn.jsdelivr.net/npm/water.css@2/out/water.css')
|
|
189
|
+
|
|
190
|
+
// Custom feature
|
|
191
|
+
const { Feature } = container
|
|
192
|
+
|
|
193
|
+
class Theme extends Feature {
|
|
194
|
+
static shortcut = 'features.theme'
|
|
195
|
+
static { Feature.register(this, 'theme') }
|
|
196
|
+
|
|
197
|
+
toggle() {
|
|
198
|
+
const next = (this.state.get('mode') || 'light') === 'light' ? 'dark' : 'light'
|
|
199
|
+
this.state.set('mode', next)
|
|
200
|
+
document.documentElement.style.colorScheme = next
|
|
201
|
+
this.emit('themeChanged', { mode: next })
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const theme = container.feature('theme')
|
|
206
|
+
theme.on('themeChanged', ({ mode }) => log(`Theme: ${mode}`))
|
|
207
|
+
|
|
208
|
+
// Speech
|
|
209
|
+
const speech = container.feature('speech')
|
|
210
|
+
|
|
211
|
+
document.getElementById('speak').onclick = () => speech.speak('Hello from Luca')
|
|
212
|
+
document.getElementById('theme').onclick = () => theme.toggle()
|
|
213
|
+
|
|
214
|
+
log(`Container ID: ${container.uuid}`)
|
|
215
|
+
log(`Features: ${container.features.available.join(', ')}`)
|
|
216
|
+
</script>
|
|
217
|
+
</body>
|
|
218
|
+
</html>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Save this as an HTML file, open it in a browser, and everything works — no npm, no bundler, no build step.
|
|
222
|
+
|
|
223
|
+
## Gotchas
|
|
224
|
+
|
|
225
|
+
- **esm.sh caches aggressively.** Pin a version if you need stability: `https://esm.sh/@soederpop/luca@0.0.29/web`
|
|
226
|
+
- **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.
|
|
227
|
+
- **`window.luca` is the singleton.** Don't call `createContainer()` — it just warns and returns the same instance. If you need isolation, use `container.subcontainer()`.
|
|
228
|
+
- **CORS applies.** REST client requests from the browser are subject to browser CORS rules. Your API must send the right headers.
|
|
229
|
+
|
|
230
|
+
## What's Next
|
|
231
|
+
|
|
232
|
+
- [State and Events](./05-state-and-events.md) — deep dive into the state machine and event bus (works identically in the browser)
|
|
233
|
+
- [Creating Features](./10-creating-features.md) — full anatomy of a feature with schemas, state, and events
|
|
234
|
+
- [Clients](./09-clients.md) — REST and WebSocket client APIs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soederpop/luca",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.30",
|
|
4
4
|
"website": "https://luca.soederpop.com",
|
|
5
5
|
"description": "lightweight universal conversational architecture AKA Le Ultimate Component Architecture AKA Last Universal Common Ancestor, part AI part Human",
|
|
6
6
|
"author": "jon soeder aka the people's champ <jon@soederpop.com>",
|
|
@@ -11,6 +11,7 @@ import { Assistant } from './features/assistant'
|
|
|
11
11
|
import { AssistantsManager } from './features/assistants-manager'
|
|
12
12
|
import { DocsReader } from './features/docs-reader'
|
|
13
13
|
import { SkillsLibrary } from './features/skills-library'
|
|
14
|
+
import { BrowserUse } from './features/browser-use'
|
|
14
15
|
import { SemanticSearch } from '@soederpop/luca/node/features/semantic-search'
|
|
15
16
|
import { ContentDb } from '@soederpop/luca/node/features/content-db'
|
|
16
17
|
|
|
@@ -26,6 +27,7 @@ export {
|
|
|
26
27
|
AssistantsManager,
|
|
27
28
|
DocsReader,
|
|
28
29
|
SkillsLibrary,
|
|
30
|
+
BrowserUse,
|
|
29
31
|
SemanticSearch,
|
|
30
32
|
ContentDb,
|
|
31
33
|
NodeContainer,
|
|
@@ -49,6 +51,7 @@ export interface AGIFeatures extends NodeFeatures {
|
|
|
49
51
|
assistantsManager: typeof AssistantsManager
|
|
50
52
|
docsReader: typeof DocsReader
|
|
51
53
|
skillsLibrary: typeof SkillsLibrary
|
|
54
|
+
browserUse: typeof BrowserUse
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
export interface ConversationFactoryOptions {
|
|
@@ -121,6 +124,7 @@ const container = new AGIContainer()
|
|
|
121
124
|
.use(AssistantsManager)
|
|
122
125
|
.use(DocsReader)
|
|
123
126
|
.use(SkillsLibrary)
|
|
127
|
+
.use(BrowserUse)
|
|
124
128
|
.use(SemanticSearch)
|
|
125
129
|
|
|
126
130
|
container.docs = container.feature('contentDb', {
|
|
@@ -86,6 +86,15 @@ export const AssistantOptionsSchema = FeatureOptionsSchema.extend({
|
|
|
86
86
|
|
|
87
87
|
/** When true, prepend a timestamp to each user message so the assistant can track the passage of time across sessions */
|
|
88
88
|
injectTimestamps: z.boolean().default(false).describe('Prepend timestamps to user messages so the assistant can perceive time passing between sessions'),
|
|
89
|
+
|
|
90
|
+
/** Strict allowlist of tool names to include. Only these tools will be available. Supports "*" glob matching. */
|
|
91
|
+
allowTools: z.array(z.string()).optional().describe('Strict allowlist of tool name patterns. Only matching tools are available. Supports * glob matching.'),
|
|
92
|
+
|
|
93
|
+
/** Denylist of tool names to exclude. Matching tools will be removed. Supports "*" glob matching. */
|
|
94
|
+
forbidTools: z.array(z.string()).optional().describe('Denylist of tool name patterns to exclude. Supports * glob matching.'),
|
|
95
|
+
|
|
96
|
+
/** Convenience alias for allowTools — an explicit list of tool names (exact matches only). */
|
|
97
|
+
toolNames: z.array(z.string()).optional().describe('Explicit list of tool names to include (exact match). Shorthand for allowTools without glob patterns.'),
|
|
89
98
|
})
|
|
90
99
|
|
|
91
100
|
export type AssistantState = z.infer<typeof AssistantStateSchema>
|
|
@@ -334,7 +343,59 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
334
343
|
|
|
335
344
|
/** The tools registered with this assistant. */
|
|
336
345
|
get tools(): Record<string, ConversationTool> {
|
|
337
|
-
|
|
346
|
+
const all = (this.state.get('tools') || {}) as Record<string, ConversationTool>
|
|
347
|
+
return this.applyToolFilters(all)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Apply allowTools, forbidTools, and toolNames filters from options.
|
|
352
|
+
* toolNames is treated as an exact-match allowlist. allowTools/forbidTools support "*" glob patterns.
|
|
353
|
+
* allowTools is applied first (strict allowlist), then forbidTools removes from whatever remains.
|
|
354
|
+
*/
|
|
355
|
+
private applyToolFilters(tools: Record<string, ConversationTool>): Record<string, ConversationTool> {
|
|
356
|
+
const { allowTools, forbidTools, toolNames } = this.options
|
|
357
|
+
if (!allowTools && !forbidTools && !toolNames) return tools
|
|
358
|
+
|
|
359
|
+
let names = Object.keys(tools)
|
|
360
|
+
|
|
361
|
+
// toolNames is a strict exact-match allowlist
|
|
362
|
+
if (toolNames) {
|
|
363
|
+
const allowed = new Set(toolNames)
|
|
364
|
+
names = names.filter(n => allowed.has(n))
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// allowTools: only keep names matching at least one pattern
|
|
368
|
+
if (allowTools) {
|
|
369
|
+
names = names.filter(n => allowTools.some(pattern => this.matchToolPattern(pattern, n)))
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// forbidTools: remove names matching any pattern
|
|
373
|
+
if (forbidTools) {
|
|
374
|
+
names = names.filter(n => !forbidTools.some(pattern => this.matchToolPattern(pattern, n)))
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const result: Record<string, ConversationTool> = {}
|
|
378
|
+
for (const n of names) {
|
|
379
|
+
result[n] = tools[n]
|
|
380
|
+
}
|
|
381
|
+
return result
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Match a tool name against a pattern that supports "*" as a wildcard.
|
|
386
|
+
* - "*" matches everything
|
|
387
|
+
* - "prefix*" matches names starting with prefix
|
|
388
|
+
* - "*suffix" matches names ending with suffix
|
|
389
|
+
* - "pre*suf" matches names starting with pre and ending with suf
|
|
390
|
+
* - exact string matches exactly
|
|
391
|
+
*/
|
|
392
|
+
private matchToolPattern(pattern: string, name: string): boolean {
|
|
393
|
+
if (pattern === '*') return true
|
|
394
|
+
if (!pattern.includes('*')) return pattern === name
|
|
395
|
+
|
|
396
|
+
// Convert glob pattern to regex: escape regex chars, replace * with .*
|
|
397
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')
|
|
398
|
+
return new RegExp(`^${escaped}$`).test(name)
|
|
338
399
|
}
|
|
339
400
|
|
|
340
401
|
/**
|