@soederpop/luca 0.0.6 → 0.0.7
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/CLAUDE.md +10 -1
- package/bun.lock +1 -1
- package/commands/build-bootstrap.ts +78 -0
- package/commands/build-scaffolds.ts +24 -2
- package/commands/try-all-challenges.ts +543 -0
- package/commands/try-challenge.ts +100 -0
- package/docs/README.md +52 -80
- package/docs/TABLE-OF-CONTENTS.md +82 -51
- package/docs/apis/clients/elevenlabs.md +232 -8
- package/docs/apis/clients/graph.md +59 -8
- package/docs/apis/clients/openai.md +362 -2
- package/docs/apis/clients/rest.md +122 -2
- package/docs/apis/clients/websocket.md +71 -17
- package/docs/apis/features/agi/assistant.md +9 -3
- package/docs/apis/features/agi/assistants-manager.md +2 -2
- package/docs/apis/features/agi/claude-code.md +153 -14
- package/docs/apis/features/agi/conversation-history.md +15 -3
- package/docs/apis/features/agi/conversation.md +133 -20
- package/docs/apis/features/agi/openai-codex.md +90 -12
- package/docs/apis/features/agi/skills-library.md +23 -5
- package/docs/apis/features/node/container-link.md +59 -0
- package/docs/apis/features/node/content-db.md +1 -1
- package/docs/apis/features/node/disk-cache.md +1 -1
- package/docs/apis/features/node/dns.md +1 -0
- package/docs/apis/features/node/docker.md +2 -1
- package/docs/apis/features/node/esbuild.md +4 -3
- package/docs/apis/features/node/file-manager.md +13 -4
- package/docs/apis/features/node/fs.md +726 -171
- package/docs/apis/features/node/git.md +1 -0
- package/docs/apis/features/node/google-auth.md +23 -4
- package/docs/apis/features/node/google-calendar.md +14 -2
- package/docs/apis/features/node/google-docs.md +15 -2
- package/docs/apis/features/node/google-drive.md +21 -3
- package/docs/apis/features/node/google-sheets.md +14 -2
- package/docs/apis/features/node/grep.md +2 -0
- package/docs/apis/features/node/helpers.md +29 -0
- package/docs/apis/features/node/ink.md +2 -2
- package/docs/apis/features/node/networking.md +39 -4
- package/docs/apis/features/node/os.md +28 -0
- package/docs/apis/features/node/postgres.md +26 -4
- package/docs/apis/features/node/proc.md +37 -28
- package/docs/apis/features/node/process-manager.md +33 -5
- package/docs/apis/features/node/repl.md +1 -1
- package/docs/apis/features/node/runpod.md +1 -0
- package/docs/apis/features/node/secure-shell.md +7 -0
- package/docs/apis/features/node/semantic-search.md +12 -5
- package/docs/apis/features/node/sqlite.md +26 -4
- package/docs/apis/features/node/telegram.md +30 -5
- package/docs/apis/features/node/tts.md +17 -2
- package/docs/apis/features/node/ui.md +1 -1
- package/docs/apis/features/node/vault.md +4 -9
- package/docs/apis/features/node/vm.md +3 -12
- package/docs/apis/features/node/window-manager.md +128 -20
- package/docs/apis/features/web/asset-loader.md +13 -1
- package/docs/apis/features/web/container-link.md +59 -0
- package/docs/apis/features/web/esbuild.md +4 -3
- package/docs/apis/features/web/helpers.md +29 -0
- package/docs/apis/features/web/network.md +16 -2
- package/docs/apis/features/web/speech.md +16 -2
- package/docs/apis/features/web/vault.md +4 -9
- package/docs/apis/features/web/vm.md +3 -12
- package/docs/apis/features/web/voice.md +18 -1
- package/docs/apis/servers/express.md +18 -2
- package/docs/apis/servers/mcp.md +29 -4
- package/docs/apis/servers/websocket.md +34 -6
- package/docs/bootstrap/CLAUDE.md +100 -0
- package/docs/bootstrap/SKILL.md +222 -0
- package/docs/bootstrap/templates/about-command.ts +41 -0
- package/docs/bootstrap/templates/docs-models.ts +22 -0
- package/docs/bootstrap/templates/docs-readme.md +43 -0
- package/docs/bootstrap/templates/example-feature.ts +53 -0
- package/docs/bootstrap/templates/health-endpoint.ts +15 -0
- package/docs/bootstrap/templates/luca-cli.ts +25 -0
- package/docs/challenges/caching-proxy.md +16 -0
- package/docs/challenges/content-db-round-trip.md +14 -0
- package/docs/challenges/custom-command.md +9 -0
- package/docs/challenges/file-watcher-pipeline.md +11 -0
- package/docs/challenges/grep-audit-report.md +15 -0
- package/docs/challenges/multi-feature-dashboard.md +14 -0
- package/docs/challenges/process-orchestrator.md +17 -0
- package/docs/challenges/rest-api-server-with-client.md +12 -0
- package/docs/challenges/script-runner-with-vm.md +11 -0
- package/docs/challenges/simple-rest-api.md +15 -0
- package/docs/challenges/websocket-serve-and-client.md +11 -0
- package/docs/challenges/yaml-config-system.md +14 -0
- package/docs/command-system-overhaul.md +94 -0
- package/docs/examples/assistant/CORE.md +18 -0
- package/docs/examples/assistant/hooks.ts +3 -0
- package/docs/examples/assistant/tools.ts +10 -0
- package/docs/examples/window-manager-layouts.md +180 -0
- package/docs/in-memory-fs.md +4 -0
- package/docs/models.ts +13 -10
- package/docs/philosophy.md +4 -3
- package/docs/reports/console-hmr-design.md +170 -0
- package/docs/reports/helper-semantic-search.md +72 -0
- package/docs/scaffolds/client.md +29 -20
- package/docs/scaffolds/command.md +64 -50
- package/docs/scaffolds/endpoint.md +31 -36
- package/docs/scaffolds/feature.md +28 -18
- package/docs/scaffolds/selector.md +91 -0
- package/docs/scaffolds/server.md +18 -9
- package/docs/selectors.md +115 -0
- package/docs/sessions/custom-command/attempt-log-2.md +195 -0
- package/docs/sessions/file-watcher-pipeline/attempt-log-1.md +728 -0
- package/docs/sessions/file-watcher-pipeline/attempt-log-2.md +555 -0
- package/docs/sessions/grep-audit-report/attempt-log-1.md +289 -0
- package/docs/sessions/multi-feature-dashboard/attempt-log-2.md +679 -0
- package/docs/sessions/rest-api-server-with-client/attempt-log-1.md +1 -0
- package/docs/sessions/rest-api-server-with-client/attempt-log-3.md +920 -0
- package/docs/sessions/simple-rest-api/attempt-log-1.md +593 -0
- package/docs/sessions/websocket-serve-and-client/attempt-log-2.md +995 -0
- package/docs/tutorials/00-bootstrap.md +148 -0
- package/docs/tutorials/07-endpoints.md +7 -7
- package/docs/tutorials/08-commands.md +153 -72
- package/luca.cli.ts +3 -0
- package/package.json +6 -5
- package/public/index.html +1430 -0
- package/scripts/examples/using-ollama.ts +2 -1
- package/scripts/update-introspection-data.ts +2 -2
- package/src/agi/endpoints/experts.ts +1 -1
- package/src/agi/features/assistant.ts +7 -0
- package/src/agi/features/assistants-manager.ts +5 -5
- package/src/agi/features/claude-code.ts +263 -3
- package/src/agi/features/conversation-history.ts +7 -1
- package/src/agi/features/conversation.ts +26 -3
- package/src/agi/features/openai-codex.ts +26 -2
- package/src/agi/features/openapi.ts +6 -1
- package/src/agi/features/skills-library.ts +9 -1
- package/src/bootstrap/generated.ts +540 -0
- package/src/cli/cli.ts +64 -21
- package/src/client.ts +23 -357
- package/src/clients/civitai/index.ts +1 -1
- package/src/clients/client-template.ts +1 -1
- package/src/clients/comfyui/index.ts +13 -2
- package/src/clients/elevenlabs/index.ts +2 -1
- package/src/clients/graph.ts +87 -0
- package/src/clients/openai/index.ts +10 -1
- package/src/clients/rest.ts +207 -0
- package/src/clients/websocket.ts +176 -0
- package/src/command.ts +281 -34
- package/src/commands/bootstrap.ts +181 -0
- package/src/commands/chat.ts +5 -4
- package/src/commands/describe.ts +225 -2
- package/src/commands/help.ts +35 -9
- package/src/commands/index.ts +3 -0
- package/src/commands/introspect.ts +92 -2
- package/src/commands/prompt.ts +5 -6
- package/src/commands/run.ts +33 -10
- package/src/commands/save-api-docs.ts +49 -0
- package/src/commands/scaffold.ts +169 -23
- package/src/commands/select.ts +94 -0
- package/src/commands/serve.ts +10 -1
- package/src/container.ts +15 -0
- package/src/endpoint.ts +19 -0
- package/src/graft.ts +181 -0
- package/src/introspection/generated.agi.ts +12458 -8968
- package/src/introspection/generated.node.ts +10573 -7145
- package/src/introspection/generated.web.ts +1 -1
- package/src/introspection/index.ts +26 -0
- package/src/node/container.ts +6 -7
- package/src/node/features/content-db.ts +49 -2
- package/src/node/features/disk-cache.ts +16 -9
- package/src/node/features/dns.ts +16 -3
- package/src/node/features/docker.ts +16 -4
- package/src/node/features/esbuild.ts +20 -0
- package/src/node/features/file-manager.ts +184 -29
- package/src/node/features/fs.ts +704 -248
- package/src/node/features/git.ts +21 -8
- package/src/node/features/grep.ts +23 -3
- package/src/node/features/helpers.ts +372 -43
- package/src/node/features/networking.ts +39 -4
- package/src/node/features/opener.ts +28 -15
- package/src/node/features/os.ts +76 -0
- package/src/node/features/port-exposer.ts +11 -1
- package/src/node/features/postgres.ts +17 -1
- package/src/node/features/proc.ts +4 -1
- package/src/node/features/python.ts +63 -14
- package/src/node/features/repl.ts +11 -7
- package/src/node/features/runpod.ts +16 -3
- package/src/node/features/secure-shell.ts +27 -2
- package/src/node/features/semantic-search.ts +12 -1
- package/src/node/features/ui.ts +5 -69
- package/src/node/features/vm.ts +17 -0
- package/src/node/features/window-manager.ts +68 -20
- package/src/node.ts +5 -0
- package/src/scaffolds/generated.ts +492 -290
- package/src/scaffolds/template.ts +9 -0
- package/src/schemas/base.ts +46 -5
- package/src/selector.ts +282 -0
- package/src/server.ts +11 -0
- package/src/servers/express.ts +27 -12
- package/src/servers/socket.ts +45 -11
- package/src/web/clients/socket.ts +4 -1
- package/src/web/container.ts +2 -1
- package/src/web/features/network.ts +7 -1
- package/src/web/features/voice-recognition.ts +16 -1
- package/test/clients-servers.test.ts +2 -1
- package/test/command.test.ts +267 -0
- package/test-integration/assistants-manager.test.ts +10 -20
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/1b/b5/c75b28794f00f94c4d609a98978e9420e9b7146d204a7fbf5b0b30477292581705d207c0100dabaac27eef540aaaece3374af75104a93219d4ec8bfb44e7 +1 -0
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/da/df/1d90ce4e042abeb035a197832c6d6893420a747a056be773eb00e4f745a037d505c8db13dde7d36b36b6b893addbb7df0f5fe9f0c13e665f20056447318b +1 -0
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/ed/04/e1d0c2a58c2db29b3921ca2affb3ea4febe831c53b38ebc21019fb799823aba6ed5b4611873d2cd25d422d49955b852a9c326da0d678899bc1c2c2960901 +1 -0
- package/tmp/.cache/luca-disk-cache/index-v5/00/13/572aa4c9a94f99eda999695d050cdd0ca7fe2d23a50af03234d4c8ce0791 +2 -0
- package/tmp/.cache/luca-disk-cache/index-v5/75/a9/cb61dc0f0589e8ec10a9aca27b834bc73884c479941042d22a2b22324cd3 +2 -0
- package/tmp/.cache/luca-disk-cache/index-v5/9f/0f/8b1f915ee64cfff7667dd96acd7a5ac0a96aa91a346e19cefd45909a9c9c +2 -0
- package/docs/apis/features/node/launcher-app-command-listener.md +0 -145
- package/docs/examples/launcher-app-command-listener.md +0 -120
- package/docs/tasks/web-container-helper-discovery.md +0 -71
- package/docs/todos.md +0 -1
- package/scripts/test-command-listener.ts +0 -123
- package/src/node/features/launcher-app-command-listener.ts +0 -389
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
repeatable: false
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# Implement Helper Discovery for WebContainer
|
|
6
|
-
|
|
7
|
-
The NodeContainer has a `helpers` feature (`src/node/features/helpers.ts`) that provides unified discovery across all registries. The WebContainer has no equivalent. Implement helper discovery for the web container.
|
|
8
|
-
|
|
9
|
-
## Context
|
|
10
|
-
|
|
11
|
-
The node `Helpers` feature provides:
|
|
12
|
-
- `container.helpers.discover('features')` — scan conventional folders and register what it finds
|
|
13
|
-
- `container.helpers.discoverAll()` — discover across all registry types
|
|
14
|
-
- `container.helpers.available` — unified view of all registries
|
|
15
|
-
- `container.helpers.lookup(type, name)` and `container.helpers.describe(type, name)`
|
|
16
|
-
|
|
17
|
-
The web container has `features` and `clients` registries (Client is attached via `extension.ts`, which calls `container.use(Client)` and triggers `registerHelperType('clients', 'client')`). It has `RestClient` and `SocketClient` available. No servers, commands, or endpoints registries.
|
|
18
|
-
|
|
19
|
-
## What to Build
|
|
20
|
-
|
|
21
|
-
### 1. Create `src/web/features/helpers.ts`
|
|
22
|
-
|
|
23
|
-
Port the node `Helpers` feature to work in the browser environment. Key differences from the node version:
|
|
24
|
-
|
|
25
|
-
- **No filesystem scanning** — the browser can't scan directories. Instead, discovery should work via explicit registration or a manifest/config object that lists available helpers and their import paths.
|
|
26
|
-
- **Registry scope** — cover `features` and `clients` (both already attached). The `registryMap` should reflect what the web container actually has.
|
|
27
|
-
- **No dynamic `import()` from disk** — helper modules need to be bundled or loaded via URL. Consider accepting a map of `{ name: () => import('./my-feature.js') }` lazy loaders.
|
|
28
|
-
- **Keep the same public API surface** — `discover()`, `discoverAll()`, `available`, `lookup()`, `describe()` should all work identically from the consumer's perspective.
|
|
29
|
-
|
|
30
|
-
### 2. Register it in `src/web/extension.ts`
|
|
31
|
-
|
|
32
|
-
Add the helpers feature to the web extension so it's available as `container.feature('helpers')` / `container.helpers`.
|
|
33
|
-
|
|
34
|
-
### 3. Approach for Browser Discovery
|
|
35
|
-
|
|
36
|
-
Since there's no filesystem to scan, discovery needs a different mechanism. Recommended approach:
|
|
37
|
-
|
|
38
|
-
```typescript
|
|
39
|
-
// Option A: Manifest-based discovery
|
|
40
|
-
const helpers = container.feature('helpers', {
|
|
41
|
-
enable: true,
|
|
42
|
-
manifest: {
|
|
43
|
-
features: {
|
|
44
|
-
myFeature: () => import('./features/my-feature.js'),
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
})
|
|
48
|
-
await helpers.discoverAll()
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
This keeps the same `discover()` / `discoverAll()` API but replaces folder scanning with a lazy-import manifest. The manifest can be generated at build time by a bundler plugin or written by hand.
|
|
52
|
-
|
|
53
|
-
### 4. Shared Base
|
|
54
|
-
|
|
55
|
-
Look at whether a base `Helpers` class can be extracted to `src/features/helpers.ts` (universal, not node or web specific) with the shared API surface (`available`, `lookup`, `describe`, state/events schemas). Then `src/node/features/helpers.ts` and `src/web/features/helpers.ts` extend it with their environment-specific discovery strategies (filesystem vs manifest).
|
|
56
|
-
|
|
57
|
-
## Files to Touch
|
|
58
|
-
|
|
59
|
-
- `src/features/helpers.ts` — new, shared base class with common API
|
|
60
|
-
- `src/web/features/helpers.ts` — new, web-specific discovery via manifest
|
|
61
|
-
- `src/node/features/helpers.ts` — refactor to extend shared base
|
|
62
|
-
- `src/web/extension.ts` — register the web helpers feature
|
|
63
|
-
- `src/schemas/base.ts` — only if new shared schemas are needed
|
|
64
|
-
|
|
65
|
-
## Acceptance Criteria
|
|
66
|
-
|
|
67
|
-
- `container.helpers.available` works in both node and web containers
|
|
68
|
-
- `container.helpers.discover('features')` works in web via manifest config
|
|
69
|
-
- `container.helpers.lookup()` and `container.helpers.describe()` work in web
|
|
70
|
-
- Node behavior is unchanged (existing tests still pass)
|
|
71
|
-
- The shared base class eliminates duplicated logic between node and web
|
package/docs/todos.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
- make container.start() meaningful. i like the idea of container.use() accepting in addition to the current shape, an async function and container.start() basically running all of those functions.
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { NodeContainer } from '../src/node/container'
|
|
2
|
-
|
|
3
|
-
const container = new NodeContainer({ cwd: process.cwd() })
|
|
4
|
-
const listener = container.feature('launcherAppCommandListener', {
|
|
5
|
-
autoListen: true,
|
|
6
|
-
})
|
|
7
|
-
|
|
8
|
-
const windowManager = container.feature('windowManager')
|
|
9
|
-
|
|
10
|
-
console.log('Listening on:', listener.state.get('socketPath'))
|
|
11
|
-
console.log('Waiting for native app to connect...\n')
|
|
12
|
-
|
|
13
|
-
listener.enable()
|
|
14
|
-
|
|
15
|
-
listener.on('clientConnected', () => {
|
|
16
|
-
console.log('[connected] Native app connected')
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
listener.on('clientDisconnected', () => {
|
|
20
|
-
console.log('[disconnected] Native app disconnected')
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
listener.on('command', async (cmd) => {
|
|
24
|
-
console.log(`[command] "${cmd.text}" (source: ${cmd.source}, id: ${cmd.id})`)
|
|
25
|
-
|
|
26
|
-
const normalizedText = String(cmd.text).toLowerCase()
|
|
27
|
-
|
|
28
|
-
if (normalizedText.includes('terminal')) {
|
|
29
|
-
await container.sleep(1000)
|
|
30
|
-
cmd.ack('Sheeeeeeeit. I got you fam!')
|
|
31
|
-
await container.sleep(1000)
|
|
32
|
-
console.log('Spawning terminal')
|
|
33
|
-
const result = await windowManager.spawnTTY({
|
|
34
|
-
command: '/Users/jon/.bun/bin/bun',
|
|
35
|
-
args: ['run', '/Users/jon/@luca/src/cli/cli.ts', 'console'],
|
|
36
|
-
cwd: '/Users/jon/@soederpop',
|
|
37
|
-
title: 'The Console',
|
|
38
|
-
cols: 120,
|
|
39
|
-
rows: 40,
|
|
40
|
-
width: 1000,
|
|
41
|
-
height: 700,
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
await container.sleep(4000)
|
|
45
|
-
|
|
46
|
-
cmd.finish({ result: { action: 'completed', text: cmd.text }, speech: 'Check that shit out playboy. Fuckin terminal output.' })
|
|
47
|
-
|
|
48
|
-
return
|
|
49
|
-
} else if (normalizedText.includes('code')) {
|
|
50
|
-
await container.sleep(1000)
|
|
51
|
-
cmd.ack('Real talk, I feel for the homies we told to learn to code. Now that claude is on this shit?? I mean.')
|
|
52
|
-
await container.sleep(1000)
|
|
53
|
-
console.log('Spawning terminal')
|
|
54
|
-
const result = await windowManager.spawnTTY({
|
|
55
|
-
command: '/Users/jon/.bun/bin/claude',
|
|
56
|
-
cwd: '/Users/jon/@soederpop',
|
|
57
|
-
title: 'Claude',
|
|
58
|
-
cols: 120,
|
|
59
|
-
rows: 80,
|
|
60
|
-
width: 1000,
|
|
61
|
-
height: 700,
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
await container.sleep(4000)
|
|
65
|
-
|
|
66
|
-
cmd.finish({ result: { action: 'completed', text: cmd.text }, speech: 'Good luck with claude bro.' })
|
|
67
|
-
|
|
68
|
-
return
|
|
69
|
-
} else if (normalizedText.includes('web') || normalizedText.includes('browser')) {
|
|
70
|
-
cmd.ack('Yo.... Fuckin check this out, twin.')
|
|
71
|
-
await container.sleep(1000)
|
|
72
|
-
const result = await windowManager.spawn({
|
|
73
|
-
url: 'https://google.com',
|
|
74
|
-
width: 1000,
|
|
75
|
-
height: 700,
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
console.log('Web browser spawned', result)
|
|
79
|
-
|
|
80
|
-
await container.sleep(3000)
|
|
81
|
-
cmd.finish({ result: { action: 'completed', text: cmd.text }, speech: 'Motherfucker I can even launch web browsers' })
|
|
82
|
-
return
|
|
83
|
-
} else if (normalizedText.includes('write')) {
|
|
84
|
-
await container.sleep(1000)
|
|
85
|
-
cmd.ack('Aight. Sheeeit. We got a real fuckin earnest hemmingway up in here.')
|
|
86
|
-
const result = await windowManager.spawn({
|
|
87
|
-
url: 'http://localhost:3080',
|
|
88
|
-
width: 1200,
|
|
89
|
-
height: 900,
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
await container.sleep(4000)
|
|
93
|
-
cmd.finish({ result: { action: 'completed', text: cmd.text }, speech: 'Let the boy COOK' })
|
|
94
|
-
return
|
|
95
|
-
} else if (normalizedText.includes('track')) {
|
|
96
|
-
await container.sleep(1000)
|
|
97
|
-
cmd.ack('Better believe it. Aint nobody hiding from your boy.')
|
|
98
|
-
|
|
99
|
-
container.proc.spawnAndCapture('luca', ['serve', '--force', '--port', '3969', '--no-open'], {
|
|
100
|
-
cwd: '/Users/jon/@soederpop/playground/enemy-tracker'
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
await container.sleep(4000)
|
|
104
|
-
|
|
105
|
-
const result = await windowManager.spawn({
|
|
106
|
-
url: 'http://localhost:3969',
|
|
107
|
-
width: 1400,
|
|
108
|
-
height: 1000,
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
cmd.finish({ result: { action: 'completed', text: cmd.text }, speech: 'Get em dawg. Me and the homies are ready.' })
|
|
112
|
-
|
|
113
|
-
return
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
await container.sleep(4000)
|
|
117
|
-
cmd.ack('Look unc. I dont know the fuck you talmbout.')
|
|
118
|
-
cmd.finish({ result: { action: 'unknown' }})
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
listener.on('message', (msg) => {
|
|
122
|
-
console.log('[message]', JSON.stringify(msg))
|
|
123
|
-
})
|
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
3
|
-
import { Feature } from '../feature.js'
|
|
4
|
-
import { Server as NetServer, Socket } from 'net'
|
|
5
|
-
import { homedir } from 'os'
|
|
6
|
-
import { join, dirname } from 'path'
|
|
7
|
-
import { existsSync, unlinkSync, mkdirSync } from 'fs'
|
|
8
|
-
|
|
9
|
-
const DEFAULT_SOCKET_PATH = join(
|
|
10
|
-
homedir(),
|
|
11
|
-
'Library',
|
|
12
|
-
'Application Support',
|
|
13
|
-
'LucaVoiceLauncher',
|
|
14
|
-
'ipc-command.sock'
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
// --- CommandHandle ---
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* A handle to a single incoming command from the native app.
|
|
21
|
-
* Provides methods to acknowledge, report progress, and finish the command.
|
|
22
|
-
* All responses are automatically correlated by the command's `id`.
|
|
23
|
-
*/
|
|
24
|
-
export class CommandHandle {
|
|
25
|
-
/** The correlation UUID from the app. */
|
|
26
|
-
readonly id: string
|
|
27
|
-
/** The command text (e.g. "open notes"). */
|
|
28
|
-
readonly text: string
|
|
29
|
-
/** The input source (e.g. "voice", "hotkey"). */
|
|
30
|
-
readonly source: string
|
|
31
|
-
/** The full payload object from the app. */
|
|
32
|
-
readonly payload: any
|
|
33
|
-
/** The entire raw message from the app. */
|
|
34
|
-
readonly raw: any
|
|
35
|
-
|
|
36
|
-
private _send: (msg: Record<string, any>) => boolean
|
|
37
|
-
private _finished = false
|
|
38
|
-
|
|
39
|
-
constructor(msg: any, send: (msg: Record<string, any>) => boolean) {
|
|
40
|
-
this.id = msg.id
|
|
41
|
-
this.text = msg.payload?.text ?? ''
|
|
42
|
-
this.source = msg.payload?.source ?? ''
|
|
43
|
-
this.payload = msg.payload ?? {}
|
|
44
|
-
this.raw = msg
|
|
45
|
-
this._send = send
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Whether `finish()` or `fail()` has been called. */
|
|
49
|
-
get isFinished(): boolean {
|
|
50
|
-
return this._finished
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Send a processing acknowledgement to the app.
|
|
55
|
-
* Optionally include a speech phrase for TTS or an audio file path for playback.
|
|
56
|
-
*
|
|
57
|
-
* @param speechOrOpts - Text the app will speak, or an options object with speech and/or audioFile
|
|
58
|
-
*/
|
|
59
|
-
ack(speechOrOpts?: string | { speech?: string; audioFile?: string }): boolean {
|
|
60
|
-
const opts = typeof speechOrOpts === 'string' ? { speech: speechOrOpts } : speechOrOpts
|
|
61
|
-
return this._send({
|
|
62
|
-
id: this.id,
|
|
63
|
-
status: 'processing',
|
|
64
|
-
...(opts?.speech ? { speech: opts.speech } : {}),
|
|
65
|
-
...(opts?.audioFile ? { audioFile: opts.audioFile } : {}),
|
|
66
|
-
timestamp: new Date().toISOString(),
|
|
67
|
-
})
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Send a progress update to the app.
|
|
72
|
-
*
|
|
73
|
-
* @param progress - A number between 0 and 1
|
|
74
|
-
* @param message - Optional human-readable progress message
|
|
75
|
-
*/
|
|
76
|
-
progress(progress: number, message?: string): boolean {
|
|
77
|
-
return this._send({
|
|
78
|
-
id: this.id,
|
|
79
|
-
status: 'progress',
|
|
80
|
-
progress,
|
|
81
|
-
...(message ? { message } : {}),
|
|
82
|
-
timestamp: new Date().toISOString(),
|
|
83
|
-
})
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Mark the command as successfully finished.
|
|
88
|
-
* Can only be called once per command. All arguments are optional.
|
|
89
|
-
*
|
|
90
|
-
* @param opts - Optional result payload, speech phrase, and/or audio file path
|
|
91
|
-
*/
|
|
92
|
-
finish(opts?: { result?: Record<string, any>; speech?: string; audioFile?: string }): boolean {
|
|
93
|
-
if (this._finished) return false
|
|
94
|
-
this._finished = true
|
|
95
|
-
return this._send({
|
|
96
|
-
id: this.id,
|
|
97
|
-
status: 'finished',
|
|
98
|
-
success: true,
|
|
99
|
-
...(opts?.result ? { result: opts.result } : {}),
|
|
100
|
-
...(opts?.speech ? { speech: opts.speech } : {}),
|
|
101
|
-
...(opts?.audioFile ? { audioFile: opts.audioFile } : {}),
|
|
102
|
-
timestamp: new Date().toISOString(),
|
|
103
|
-
})
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Mark the command as failed.
|
|
108
|
-
* Can only be called once per command.
|
|
109
|
-
*
|
|
110
|
-
* @param opts - Optional error description, speech phrase, and/or audio file path
|
|
111
|
-
*/
|
|
112
|
-
fail(opts?: { error?: string; speech?: string; audioFile?: string }): boolean {
|
|
113
|
-
if (this._finished) return false
|
|
114
|
-
this._finished = true
|
|
115
|
-
return this._send({
|
|
116
|
-
id: this.id,
|
|
117
|
-
status: 'finished',
|
|
118
|
-
success: false,
|
|
119
|
-
...(opts?.error ? { error: opts.error } : {}),
|
|
120
|
-
...(opts?.speech ? { speech: opts.speech } : {}),
|
|
121
|
-
...(opts?.audioFile ? { audioFile: opts.audioFile } : {}),
|
|
122
|
-
timestamp: new Date().toISOString(),
|
|
123
|
-
})
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// --- Schemas ---
|
|
128
|
-
|
|
129
|
-
export const LauncherAppCommandListenerOptionsSchema = FeatureOptionsSchema.extend({
|
|
130
|
-
socketPath: z.string().default(DEFAULT_SOCKET_PATH)
|
|
131
|
-
.describe('Path to the Unix domain socket to listen on'),
|
|
132
|
-
autoListen: z.boolean().optional()
|
|
133
|
-
.describe('Automatically start listening when the feature is enabled'),
|
|
134
|
-
})
|
|
135
|
-
export type LauncherAppCommandListenerOptions = z.infer<typeof LauncherAppCommandListenerOptionsSchema>
|
|
136
|
-
|
|
137
|
-
export const LauncherAppCommandListenerStateSchema = FeatureStateSchema.extend({
|
|
138
|
-
listening: z.boolean().default(false)
|
|
139
|
-
.describe('Whether the IPC server is listening'),
|
|
140
|
-
clientConnected: z.boolean().default(false)
|
|
141
|
-
.describe('Whether the native launcher app is connected'),
|
|
142
|
-
socketPath: z.string().optional()
|
|
143
|
-
.describe('The socket path in use'),
|
|
144
|
-
commandsReceived: z.number().default(0)
|
|
145
|
-
.describe('Total number of commands received'),
|
|
146
|
-
lastCommandText: z.string().optional()
|
|
147
|
-
.describe('The text of the last received command'),
|
|
148
|
-
lastError: z.string().optional()
|
|
149
|
-
.describe('Last error message'),
|
|
150
|
-
})
|
|
151
|
-
export type LauncherAppCommandListenerState = z.infer<typeof LauncherAppCommandListenerStateSchema>
|
|
152
|
-
|
|
153
|
-
export const LauncherAppCommandListenerEventsSchema = FeatureEventsSchema.extend({
|
|
154
|
-
listening: z.tuple([]).describe('Emitted when the IPC server starts listening'),
|
|
155
|
-
clientConnected: z.tuple([z.any().describe('The client socket')]).describe('Emitted when the native app connects'),
|
|
156
|
-
clientDisconnected: z.tuple([]).describe('Emitted when the native app disconnects'),
|
|
157
|
-
command: z.tuple([z.any().describe('A CommandHandle for the incoming command')]).describe('Emitted when a command is received. The listener is responsible for calling ack(), finish(), or fail() on the handle.'),
|
|
158
|
-
message: z.tuple([z.any().describe('The parsed message')]).describe('Emitted for any non-command message from the app'),
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
// --- Private types ---
|
|
162
|
-
|
|
163
|
-
interface ClientConnection {
|
|
164
|
-
socket: Socket
|
|
165
|
-
buffer: string
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// --- Feature ---
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* LauncherAppCommandListener — IPC transport for commands from the LucaVoiceLauncher app
|
|
172
|
-
*
|
|
173
|
-
* Listens on a Unix domain socket for the native macOS launcher app to connect.
|
|
174
|
-
* When a command event arrives (voice, hotkey, text input), it wraps it in a
|
|
175
|
-
* `CommandHandle` and emits a `command` event. The consumer is responsible for
|
|
176
|
-
* acknowledging, processing, and finishing the command via the handle.
|
|
177
|
-
*
|
|
178
|
-
* Uses NDJSON (newline-delimited JSON) over the socket per the CLIENT_SPEC protocol.
|
|
179
|
-
*
|
|
180
|
-
* @example
|
|
181
|
-
* ```typescript
|
|
182
|
-
* const listener = container.feature('launcherAppCommandListener', {
|
|
183
|
-
* enable: true,
|
|
184
|
-
* autoListen: true,
|
|
185
|
-
* })
|
|
186
|
-
*
|
|
187
|
-
* listener.on('command', async (cmd) => {
|
|
188
|
-
* cmd.ack('Working on it!') // or just cmd.ack() for silent
|
|
189
|
-
*
|
|
190
|
-
* // ... do your actual work ...
|
|
191
|
-
* cmd.progress(0.5, 'Halfway there')
|
|
192
|
-
*
|
|
193
|
-
* cmd.finish() // silent finish
|
|
194
|
-
* cmd.finish({ result: { action: 'completed' }, speech: 'All done!' })
|
|
195
|
-
* // or: cmd.fail({ error: 'not found', speech: 'Sorry, that failed.' })
|
|
196
|
-
* })
|
|
197
|
-
* ```
|
|
198
|
-
*/
|
|
199
|
-
export class LauncherAppCommandListener extends Feature<LauncherAppCommandListenerState, LauncherAppCommandListenerOptions> {
|
|
200
|
-
static override shortcut = 'features.launcherAppCommandListener' as const
|
|
201
|
-
static override stateSchema = LauncherAppCommandListenerStateSchema
|
|
202
|
-
static override optionsSchema = LauncherAppCommandListenerOptionsSchema
|
|
203
|
-
static override eventsSchema = LauncherAppCommandListenerEventsSchema
|
|
204
|
-
static { Feature.register(this, 'launcherAppCommandListener') }
|
|
205
|
-
|
|
206
|
-
private _server?: NetServer
|
|
207
|
-
private _client?: ClientConnection
|
|
208
|
-
|
|
209
|
-
override get initialState(): LauncherAppCommandListenerState {
|
|
210
|
-
return {
|
|
211
|
-
...super.initialState,
|
|
212
|
-
listening: false,
|
|
213
|
-
clientConnected: false,
|
|
214
|
-
commandsReceived: 0,
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/** Whether the IPC server is currently listening. */
|
|
219
|
-
get isListening(): boolean {
|
|
220
|
-
return this.state.get('listening') || false
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** Whether the native app client is currently connected. */
|
|
224
|
-
get isClientConnected(): boolean {
|
|
225
|
-
return this.state.get('clientConnected') || false
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
override async enable(options: any = {}): Promise<this> {
|
|
229
|
-
await super.enable(options)
|
|
230
|
-
|
|
231
|
-
if (this.options.autoListen) {
|
|
232
|
-
this.listen()
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return this
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Start listening on the Unix domain socket for the native app to connect.
|
|
240
|
-
* Fire-and-forget — binds the socket and returns immediately. Sits quietly
|
|
241
|
-
* until the native app connects; does nothing visible if it never does.
|
|
242
|
-
*
|
|
243
|
-
* @param socketPath - Override the configured socket path
|
|
244
|
-
* @returns This feature instance for chaining
|
|
245
|
-
*/
|
|
246
|
-
listen(socketPath?: string): this {
|
|
247
|
-
if (this._server) return this
|
|
248
|
-
|
|
249
|
-
socketPath = socketPath || this.options.socketPath || DEFAULT_SOCKET_PATH
|
|
250
|
-
|
|
251
|
-
const dir = dirname(socketPath)
|
|
252
|
-
if (!existsSync(dir)) {
|
|
253
|
-
try {
|
|
254
|
-
mkdirSync(dir, { recursive: true })
|
|
255
|
-
} catch (error: any) {
|
|
256
|
-
this.setState({ lastError: `Failed to create socket directory ${dir}: ${error?.message || String(error)}` })
|
|
257
|
-
return this
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (existsSync(socketPath)) {
|
|
262
|
-
try {
|
|
263
|
-
unlinkSync(socketPath)
|
|
264
|
-
} catch (error: any) {
|
|
265
|
-
this.setState({ lastError: `Failed to remove stale socket at ${socketPath}: ${error?.message || String(error)}` })
|
|
266
|
-
return this
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const server = new NetServer((socket) => {
|
|
271
|
-
this.handleClientConnect(socket)
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
server.on('error', (err) => {
|
|
275
|
-
this.setState({ lastError: err.message })
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
const finalPath = socketPath
|
|
279
|
-
server.listen(finalPath, () => {
|
|
280
|
-
this._server = server
|
|
281
|
-
this.setState({ listening: true, socketPath: finalPath })
|
|
282
|
-
this.emit('listening')
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
return this
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Stop the IPC server and clean up all connections.
|
|
290
|
-
*
|
|
291
|
-
* @returns This feature instance for chaining
|
|
292
|
-
*/
|
|
293
|
-
async stop(): Promise<this> {
|
|
294
|
-
if (this._client) {
|
|
295
|
-
this._client.socket.destroy()
|
|
296
|
-
this._client = undefined
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const socketPath = this.state.get('socketPath')
|
|
300
|
-
|
|
301
|
-
if (this._server) {
|
|
302
|
-
await new Promise<void>((resolve) => {
|
|
303
|
-
this._server!.close(() => resolve())
|
|
304
|
-
})
|
|
305
|
-
this._server = undefined
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (socketPath && existsSync(socketPath)) {
|
|
309
|
-
try { unlinkSync(socketPath) } catch { /* ignore */ }
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
this.setState({ listening: false, clientConnected: false, socketPath: undefined })
|
|
313
|
-
return this
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Write an NDJSON message to the connected app client.
|
|
318
|
-
*
|
|
319
|
-
* @param msg - The message object to send (will be JSON-serialized + newline)
|
|
320
|
-
* @returns True if the message was written, false if no client is connected
|
|
321
|
-
*/
|
|
322
|
-
send(msg: Record<string, any>): boolean {
|
|
323
|
-
if (!this._client) return false
|
|
324
|
-
this._client.socket.write(JSON.stringify(msg) + '\n')
|
|
325
|
-
return true
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// --- Private ---
|
|
329
|
-
|
|
330
|
-
/** Handle a new client connection from the native app. */
|
|
331
|
-
private handleClientConnect(socket: Socket): void {
|
|
332
|
-
const client: ClientConnection = { socket, buffer: '' }
|
|
333
|
-
|
|
334
|
-
if (this._client) {
|
|
335
|
-
this._client.socket.destroy()
|
|
336
|
-
}
|
|
337
|
-
this._client = client
|
|
338
|
-
|
|
339
|
-
this.setState({ clientConnected: true })
|
|
340
|
-
this.emit('clientConnected', socket)
|
|
341
|
-
|
|
342
|
-
socket.on('data', (chunk) => {
|
|
343
|
-
client.buffer += chunk.toString()
|
|
344
|
-
const lines = client.buffer.split('\n')
|
|
345
|
-
client.buffer = lines.pop() || ''
|
|
346
|
-
for (const line of lines) {
|
|
347
|
-
if (line.trim()) this.processLine(line)
|
|
348
|
-
}
|
|
349
|
-
})
|
|
350
|
-
|
|
351
|
-
socket.on('close', () => {
|
|
352
|
-
if (this._client === client) {
|
|
353
|
-
this._client = undefined
|
|
354
|
-
this.setState({ clientConnected: false })
|
|
355
|
-
this.emit('clientDisconnected')
|
|
356
|
-
}
|
|
357
|
-
})
|
|
358
|
-
|
|
359
|
-
socket.on('error', (err) => {
|
|
360
|
-
this.setState({ lastError: err.message })
|
|
361
|
-
})
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/** Process a single NDJSON line. Wraps commands in a CommandHandle; emits `message` for everything else. */
|
|
365
|
-
private processLine(line: string): void {
|
|
366
|
-
let msg: any
|
|
367
|
-
try {
|
|
368
|
-
msg = JSON.parse(line)
|
|
369
|
-
} catch {
|
|
370
|
-
return
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (msg.type === 'command') {
|
|
374
|
-
const handle = new CommandHandle(msg, (m) => this.send(m))
|
|
375
|
-
|
|
376
|
-
this.setState({
|
|
377
|
-
commandsReceived: (this.state.get('commandsReceived') ?? 0) + 1,
|
|
378
|
-
lastCommandText: handle.text,
|
|
379
|
-
})
|
|
380
|
-
|
|
381
|
-
this.emit('command', handle)
|
|
382
|
-
return
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
this.emit('message', msg)
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
export default LauncherAppCommandListener
|