@soederpop/luca 0.0.2
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 +71 -0
- package/README.md +78 -0
- package/bun.lock +2928 -0
- package/bunfig.toml +3 -0
- package/commands/audit-docs.ts +740 -0
- package/commands/build-scaffolds.ts +154 -0
- package/commands/generate-api-docs.ts +114 -0
- package/commands/update-introspection.ts +67 -0
- package/docs/CLI.md +335 -0
- package/docs/README.md +88 -0
- package/docs/TABLE-OF-CONTENTS.md +157 -0
- package/docs/apis/clients/elevenlabs.md +84 -0
- package/docs/apis/clients/graph.md +56 -0
- package/docs/apis/clients/openai.md +69 -0
- package/docs/apis/clients/rest.md +41 -0
- package/docs/apis/clients/websocket.md +107 -0
- package/docs/apis/features/agi/assistant.md +471 -0
- package/docs/apis/features/agi/assistants-manager.md +154 -0
- package/docs/apis/features/agi/claude-code.md +602 -0
- package/docs/apis/features/agi/conversation-history.md +352 -0
- package/docs/apis/features/agi/conversation.md +333 -0
- package/docs/apis/features/agi/docs-reader.md +121 -0
- package/docs/apis/features/agi/openai-codex.md +318 -0
- package/docs/apis/features/agi/openapi.md +138 -0
- package/docs/apis/features/agi/semantic-search.md +387 -0
- package/docs/apis/features/agi/skills-library.md +216 -0
- package/docs/apis/features/node/container-link.md +133 -0
- package/docs/apis/features/node/content-db.md +313 -0
- package/docs/apis/features/node/disk-cache.md +379 -0
- package/docs/apis/features/node/dns.md +651 -0
- package/docs/apis/features/node/docker.md +705 -0
- package/docs/apis/features/node/downloader.md +81 -0
- package/docs/apis/features/node/esbuild.md +59 -0
- package/docs/apis/features/node/file-manager.md +182 -0
- package/docs/apis/features/node/fs.md +581 -0
- package/docs/apis/features/node/git.md +330 -0
- package/docs/apis/features/node/google-auth.md +174 -0
- package/docs/apis/features/node/google-calendar.md +187 -0
- package/docs/apis/features/node/google-docs.md +151 -0
- package/docs/apis/features/node/google-drive.md +225 -0
- package/docs/apis/features/node/google-sheets.md +179 -0
- package/docs/apis/features/node/grep.md +290 -0
- package/docs/apis/features/node/helpers.md +135 -0
- package/docs/apis/features/node/ink.md +334 -0
- package/docs/apis/features/node/ipc-socket.md +260 -0
- package/docs/apis/features/node/json-tree.md +86 -0
- package/docs/apis/features/node/launcher-app-command-listener.md +145 -0
- package/docs/apis/features/node/networking.md +281 -0
- package/docs/apis/features/node/nlp.md +133 -0
- package/docs/apis/features/node/opener.md +97 -0
- package/docs/apis/features/node/os.md +118 -0
- package/docs/apis/features/node/package-finder.md +402 -0
- package/docs/apis/features/node/postgres.md +212 -0
- package/docs/apis/features/node/proc.md +430 -0
- package/docs/apis/features/node/process-manager.md +210 -0
- package/docs/apis/features/node/python.md +278 -0
- package/docs/apis/features/node/repl.md +88 -0
- package/docs/apis/features/node/runpod.md +673 -0
- package/docs/apis/features/node/secure-shell.md +169 -0
- package/docs/apis/features/node/semantic-search.md +401 -0
- package/docs/apis/features/node/sqlite.md +211 -0
- package/docs/apis/features/node/telegram.md +254 -0
- package/docs/apis/features/node/tts.md +118 -0
- package/docs/apis/features/node/ui.md +703 -0
- package/docs/apis/features/node/vault.md +64 -0
- package/docs/apis/features/node/vm.md +84 -0
- package/docs/apis/features/node/window-manager.md +337 -0
- package/docs/apis/features/node/yaml-tree.md +85 -0
- package/docs/apis/features/node/yaml.md +176 -0
- package/docs/apis/features/web/asset-loader.md +47 -0
- package/docs/apis/features/web/container-link.md +133 -0
- package/docs/apis/features/web/esbuild.md +59 -0
- package/docs/apis/features/web/helpers.md +135 -0
- package/docs/apis/features/web/network.md +30 -0
- package/docs/apis/features/web/speech.md +55 -0
- package/docs/apis/features/web/vault.md +64 -0
- package/docs/apis/features/web/vm.md +84 -0
- package/docs/apis/features/web/voice.md +67 -0
- package/docs/apis/servers/express.md +127 -0
- package/docs/apis/servers/mcp.md +213 -0
- package/docs/apis/servers/websocket.md +99 -0
- package/docs/documentation-audit.md +134 -0
- package/docs/examples/content-db.md +77 -0
- package/docs/examples/disk-cache.md +83 -0
- package/docs/examples/docker.md +101 -0
- package/docs/examples/downloader.md +70 -0
- package/docs/examples/esbuild.md +80 -0
- package/docs/examples/file-manager.md +82 -0
- package/docs/examples/fs.md +83 -0
- package/docs/examples/git.md +85 -0
- package/docs/examples/google-auth.md +88 -0
- package/docs/examples/google-calendar.md +94 -0
- package/docs/examples/google-docs.md +82 -0
- package/docs/examples/google-drive.md +96 -0
- package/docs/examples/google-sheets.md +95 -0
- package/docs/examples/grep.md +85 -0
- package/docs/examples/ink-blocks.md +75 -0
- package/docs/examples/ink-renderer.md +41 -0
- package/docs/examples/ink.md +103 -0
- package/docs/examples/ipc-socket.md +103 -0
- package/docs/examples/json-tree.md +91 -0
- package/docs/examples/launcher-app-command-listener.md +120 -0
- package/docs/examples/networking.md +58 -0
- package/docs/examples/nlp.md +91 -0
- package/docs/examples/opener.md +78 -0
- package/docs/examples/os.md +72 -0
- package/docs/examples/package-finder.md +89 -0
- package/docs/examples/port-exposer.md +89 -0
- package/docs/examples/postgres.md +91 -0
- package/docs/examples/proc.md +81 -0
- package/docs/examples/process-manager.md +79 -0
- package/docs/examples/python.md +91 -0
- package/docs/examples/repl.md +93 -0
- package/docs/examples/runpod.md +119 -0
- package/docs/examples/secure-shell.md +92 -0
- package/docs/examples/sqlite.md +86 -0
- package/docs/examples/telegram.md +77 -0
- package/docs/examples/tts.md +86 -0
- package/docs/examples/ui.md +80 -0
- package/docs/examples/vault.md +70 -0
- package/docs/examples/vm.md +86 -0
- package/docs/examples/window-manager.md +125 -0
- package/docs/examples/yaml-tree.md +93 -0
- package/docs/examples/yaml.md +104 -0
- package/docs/ideas/class-registration-refactor-possibilities.md +197 -0
- package/docs/ideas/container-use-api.md +9 -0
- package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
- package/docs/ideas/feature-stacks.md +22 -0
- package/docs/ideas/luca-cli-self-sufficiency-demo.md +23 -0
- package/docs/ideas/mcp-design.md +9 -0
- package/docs/ideas/web-container-debugging-feature.md +13 -0
- package/docs/introspection-audit.md +49 -0
- package/docs/introspection.md +154 -0
- package/docs/mcp/readme.md +162 -0
- package/docs/models.ts +38 -0
- package/docs/philosophy.md +85 -0
- package/docs/principles.md +7 -0
- package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
- package/docs/prompts/mcp-test-easy-command.md +27 -0
- package/docs/reports/assistant-bugs.md +38 -0
- package/docs/reports/attach-pattern-usage.md +18 -0
- package/docs/reports/code-audit-results.md +391 -0
- package/docs/reports/introspection-audit-tasks.md +378 -0
- package/docs/reports/luca-mcp-improvements.md +128 -0
- package/docs/scaffolds/client.md +140 -0
- package/docs/scaffolds/command.md +106 -0
- package/docs/scaffolds/endpoint.md +176 -0
- package/docs/scaffolds/feature.md +148 -0
- package/docs/scaffolds/server.md +187 -0
- package/docs/tasks/web-container-helper-discovery.md +71 -0
- package/docs/todos.md +1 -0
- package/docs/tutorials/01-getting-started.md +106 -0
- package/docs/tutorials/02-container.md +210 -0
- package/docs/tutorials/03-scripts.md +194 -0
- package/docs/tutorials/04-features-overview.md +196 -0
- package/docs/tutorials/05-state-and-events.md +171 -0
- package/docs/tutorials/06-servers.md +157 -0
- package/docs/tutorials/07-endpoints.md +198 -0
- package/docs/tutorials/08-commands.md +171 -0
- package/docs/tutorials/09-clients.md +162 -0
- package/docs/tutorials/10-creating-features.md +198 -0
- package/docs/tutorials/11-contentbase.md +191 -0
- package/docs/tutorials/12-assistants.md +215 -0
- package/docs/tutorials/13-introspection.md +147 -0
- package/docs/tutorials/14-type-system.md +174 -0
- package/docs/tutorials/15-project-patterns.md +222 -0
- package/docs/tutorials/16-google-features.md +534 -0
- package/docs/tutorials/17-tui-blocks.md +530 -0
- package/docs/tutorials/18-semantic-search.md +334 -0
- package/index.ts +1 -0
- package/luca.console.ts +9 -0
- package/main.py +6 -0
- package/package.json +154 -0
- package/pyproject.toml +7 -0
- package/scripts/animations/chrome-glitch.ts +55 -0
- package/scripts/animations/index.ts +16 -0
- package/scripts/animations/neon-pulse.ts +64 -0
- package/scripts/animations/types.ts +6 -0
- package/scripts/build-web.ts +28 -0
- package/scripts/examples/ask-luca-expert.ts +42 -0
- package/scripts/examples/assistant-questions.ts +12 -0
- package/scripts/examples/excalidraw-expert.ts +75 -0
- package/scripts/examples/expert-chat.ts +0 -0
- package/scripts/examples/file-manager.ts +14 -0
- package/scripts/examples/ideas.ts +12 -0
- package/scripts/examples/interactive-chat.ts +20 -0
- package/scripts/examples/openai-tool-calls.ts +113 -0
- package/scripts/examples/opening-a-web-browser.ts +5 -0
- package/scripts/examples/telegram-bot.ts +79 -0
- package/scripts/examples/telegram-ink-ui.ts +302 -0
- package/scripts/examples/using-assistant-with-mcp.ts +560 -0
- package/scripts/examples/using-claude-code.ts +10 -0
- package/scripts/examples/using-contentdb.ts +35 -0
- package/scripts/examples/using-conversations.ts +35 -0
- package/scripts/examples/using-disk-cache.ts +10 -0
- package/scripts/examples/using-docker-shell.ts +75 -0
- package/scripts/examples/using-elevenlabs.ts +25 -0
- package/scripts/examples/using-google-calendar.ts +57 -0
- package/scripts/examples/using-google-docs.ts +74 -0
- package/scripts/examples/using-google-drive.ts +74 -0
- package/scripts/examples/using-google-sheets.ts +89 -0
- package/scripts/examples/using-nlp.ts +55 -0
- package/scripts/examples/using-ollama.ts +10 -0
- package/scripts/examples/using-openai-codex.ts +23 -0
- package/scripts/examples/using-postgres.ts +55 -0
- package/scripts/examples/using-runpod.ts +32 -0
- package/scripts/examples/using-tts.ts +40 -0
- package/scripts/examples/vm-loading-esm-modules.ts +16 -0
- package/scripts/scaffold.ts +391 -0
- package/scripts/scratch.ts +15 -0
- package/scripts/test-command-listener.ts +123 -0
- package/scripts/test-window-manager-lifecycle.ts +86 -0
- package/scripts/test-window-manager.ts +43 -0
- package/scripts/update-introspection-data.ts +58 -0
- package/src/agi/README.md +14 -0
- package/src/agi/container.server.ts +114 -0
- package/src/agi/endpoints/ask.ts +60 -0
- package/src/agi/endpoints/conversations/[id].ts +45 -0
- package/src/agi/endpoints/conversations.ts +31 -0
- package/src/agi/endpoints/experts.ts +37 -0
- package/src/agi/features/assistant.ts +767 -0
- package/src/agi/features/assistants-manager.ts +260 -0
- package/src/agi/features/claude-code.ts +1111 -0
- package/src/agi/features/conversation-history.ts +497 -0
- package/src/agi/features/conversation.ts +799 -0
- package/src/agi/features/openai-codex.ts +631 -0
- package/src/agi/features/openapi.ts +438 -0
- package/src/agi/features/skills-library.ts +425 -0
- package/src/agi/index.ts +6 -0
- package/src/agi/lib/token-counter.ts +122 -0
- package/src/browser.ts +25 -0
- package/src/bus.ts +100 -0
- package/src/cli/cli.ts +70 -0
- package/src/client.ts +461 -0
- package/src/clients/civitai/index.ts +541 -0
- package/src/clients/client-template.ts +41 -0
- package/src/clients/comfyui/index.ts +597 -0
- package/src/clients/elevenlabs/index.ts +291 -0
- package/src/clients/openai/index.ts +451 -0
- package/src/clients/supabase/index.ts +366 -0
- package/src/command.ts +164 -0
- package/src/commands/chat.ts +182 -0
- package/src/commands/console.ts +192 -0
- package/src/commands/describe.ts +433 -0
- package/src/commands/eval.ts +116 -0
- package/src/commands/help.ts +214 -0
- package/src/commands/index.ts +14 -0
- package/src/commands/mcp.ts +64 -0
- package/src/commands/prompt.ts +807 -0
- package/src/commands/run.ts +257 -0
- package/src/commands/sandbox-mcp.ts +439 -0
- package/src/commands/scaffold.ts +79 -0
- package/src/commands/serve.ts +172 -0
- package/src/container.ts +781 -0
- package/src/endpoint.ts +340 -0
- package/src/feature.ts +75 -0
- package/src/hash-object.ts +97 -0
- package/src/helper.ts +543 -0
- package/src/introspection/generated.agi.ts +23388 -0
- package/src/introspection/generated.node.ts +18899 -0
- package/src/introspection/generated.web.ts +2021 -0
- package/src/introspection/index.ts +256 -0
- package/src/introspection/scan.ts +912 -0
- package/src/node/container.ts +354 -0
- package/src/node/feature.ts +13 -0
- package/src/node/features/container-link.ts +558 -0
- package/src/node/features/content-db.ts +475 -0
- package/src/node/features/disk-cache.ts +382 -0
- package/src/node/features/dns.ts +655 -0
- package/src/node/features/docker.ts +912 -0
- package/src/node/features/downloader.ts +92 -0
- package/src/node/features/esbuild.ts +68 -0
- package/src/node/features/file-manager.ts +357 -0
- package/src/node/features/fs.ts +534 -0
- package/src/node/features/git.ts +492 -0
- package/src/node/features/google-auth.ts +502 -0
- package/src/node/features/google-calendar.ts +300 -0
- package/src/node/features/google-docs.ts +404 -0
- package/src/node/features/google-drive.ts +339 -0
- package/src/node/features/google-sheets.ts +279 -0
- package/src/node/features/grep.ts +406 -0
- package/src/node/features/helpers.ts +374 -0
- package/src/node/features/ink.ts +490 -0
- package/src/node/features/ipc-socket.ts +459 -0
- package/src/node/features/json-tree.ts +188 -0
- package/src/node/features/launcher-app-command-listener.ts +388 -0
- package/src/node/features/networking.ts +925 -0
- package/src/node/features/nlp.ts +211 -0
- package/src/node/features/opener.ts +166 -0
- package/src/node/features/os.ts +157 -0
- package/src/node/features/package-finder.ts +539 -0
- package/src/node/features/port-exposer.ts +342 -0
- package/src/node/features/postgres.ts +273 -0
- package/src/node/features/proc.ts +502 -0
- package/src/node/features/process-manager.ts +542 -0
- package/src/node/features/python.ts +444 -0
- package/src/node/features/repl.ts +194 -0
- package/src/node/features/runpod.ts +802 -0
- package/src/node/features/secure-shell.ts +248 -0
- package/src/node/features/semantic-search.ts +924 -0
- package/src/node/features/sqlite.ts +289 -0
- package/src/node/features/telegram.ts +342 -0
- package/src/node/features/tts.ts +184 -0
- package/src/node/features/ui.ts +857 -0
- package/src/node/features/vault.ts +164 -0
- package/src/node/features/vm.ts +312 -0
- package/src/node/features/window-manager.ts +804 -0
- package/src/node/features/yaml-tree.ts +149 -0
- package/src/node/features/yaml.ts +132 -0
- package/src/node.ts +70 -0
- package/src/react/index.ts +175 -0
- package/src/registry.ts +199 -0
- package/src/scaffolds/generated.ts +1613 -0
- package/src/scaffolds/template.ts +37 -0
- package/src/schemas/base.ts +255 -0
- package/src/server.ts +135 -0
- package/src/servers/express.ts +209 -0
- package/src/servers/mcp.ts +805 -0
- package/src/servers/socket.ts +120 -0
- package/src/state.ts +101 -0
- package/src/web/clients/socket.ts +82 -0
- package/src/web/container.ts +74 -0
- package/src/web/extension.ts +30 -0
- package/src/web/feature.ts +12 -0
- package/src/web/features/asset-loader.ts +64 -0
- package/src/web/features/container-link.ts +385 -0
- package/src/web/features/esbuild.ts +79 -0
- package/src/web/features/helpers.ts +267 -0
- package/src/web/features/network.ts +61 -0
- package/src/web/features/speech.ts +87 -0
- package/src/web/features/vault.ts +189 -0
- package/src/web/features/vm.ts +78 -0
- package/src/web/features/voice-recognition.ts +129 -0
- package/src/web/shims/isomorphic-vm.ts +149 -0
- package/test/bus.test.ts +134 -0
- package/test/clients-servers.test.ts +216 -0
- package/test/container-link.test.ts +274 -0
- package/test/features.test.ts +160 -0
- package/test/integration.test.ts +787 -0
- package/test/node-container.test.ts +121 -0
- package/test/rate-limit.test.ts +272 -0
- package/test/semantic-search.test.ts +550 -0
- package/test/state.test.ts +121 -0
- package/test-integration/assistant.test.ts +138 -0
- package/test-integration/assistants-manager.test.ts +123 -0
- package/test-integration/claude-code.test.ts +98 -0
- package/test-integration/conversation-history.test.ts +205 -0
- package/test-integration/conversation.test.ts +137 -0
- package/test-integration/elevenlabs.test.ts +55 -0
- package/test-integration/google-services.test.ts +80 -0
- package/test-integration/helpers.ts +89 -0
- package/test-integration/openai-codex.test.ts +93 -0
- package/test-integration/runpod.test.ts +58 -0
- package/test-integration/server-endpoints.test.ts +97 -0
- package/test-integration/skills-library.test.ts +157 -0
- package/test-integration/telegram.test.ts +46 -0
- package/tsconfig.json +58 -0
- package/uv.lock +8 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
3
|
+
import { Feature, features } 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
|
+
|
|
205
|
+
private _server?: NetServer
|
|
206
|
+
private _client?: ClientConnection
|
|
207
|
+
|
|
208
|
+
override get initialState(): LauncherAppCommandListenerState {
|
|
209
|
+
return {
|
|
210
|
+
...super.initialState,
|
|
211
|
+
listening: false,
|
|
212
|
+
clientConnected: false,
|
|
213
|
+
commandsReceived: 0,
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Whether the IPC server is currently listening. */
|
|
218
|
+
get isListening(): boolean {
|
|
219
|
+
return this.state.get('listening') || false
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Whether the native app client is currently connected. */
|
|
223
|
+
get isClientConnected(): boolean {
|
|
224
|
+
return this.state.get('clientConnected') || false
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
override async enable(options: any = {}): Promise<this> {
|
|
228
|
+
await super.enable(options)
|
|
229
|
+
|
|
230
|
+
if (this.options.autoListen) {
|
|
231
|
+
this.listen()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return this
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Start listening on the Unix domain socket for the native app to connect.
|
|
239
|
+
* Fire-and-forget — binds the socket and returns immediately. Sits quietly
|
|
240
|
+
* until the native app connects; does nothing visible if it never does.
|
|
241
|
+
*
|
|
242
|
+
* @param socketPath - Override the configured socket path
|
|
243
|
+
* @returns This feature instance for chaining
|
|
244
|
+
*/
|
|
245
|
+
listen(socketPath?: string): this {
|
|
246
|
+
if (this._server) return this
|
|
247
|
+
|
|
248
|
+
socketPath = socketPath || this.options.socketPath || DEFAULT_SOCKET_PATH
|
|
249
|
+
|
|
250
|
+
const dir = dirname(socketPath)
|
|
251
|
+
if (!existsSync(dir)) {
|
|
252
|
+
try {
|
|
253
|
+
mkdirSync(dir, { recursive: true })
|
|
254
|
+
} catch (error: any) {
|
|
255
|
+
this.setState({ lastError: `Failed to create socket directory ${dir}: ${error?.message || String(error)}` })
|
|
256
|
+
return this
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (existsSync(socketPath)) {
|
|
261
|
+
try {
|
|
262
|
+
unlinkSync(socketPath)
|
|
263
|
+
} catch (error: any) {
|
|
264
|
+
this.setState({ lastError: `Failed to remove stale socket at ${socketPath}: ${error?.message || String(error)}` })
|
|
265
|
+
return this
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const server = new NetServer((socket) => {
|
|
270
|
+
this.handleClientConnect(socket)
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
server.on('error', (err) => {
|
|
274
|
+
this.setState({ lastError: err.message })
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
const finalPath = socketPath
|
|
278
|
+
server.listen(finalPath, () => {
|
|
279
|
+
this._server = server
|
|
280
|
+
this.setState({ listening: true, socketPath: finalPath })
|
|
281
|
+
this.emit('listening')
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
return this
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Stop the IPC server and clean up all connections.
|
|
289
|
+
*
|
|
290
|
+
* @returns This feature instance for chaining
|
|
291
|
+
*/
|
|
292
|
+
async stop(): Promise<this> {
|
|
293
|
+
if (this._client) {
|
|
294
|
+
this._client.socket.destroy()
|
|
295
|
+
this._client = undefined
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const socketPath = this.state.get('socketPath')
|
|
299
|
+
|
|
300
|
+
if (this._server) {
|
|
301
|
+
await new Promise<void>((resolve) => {
|
|
302
|
+
this._server!.close(() => resolve())
|
|
303
|
+
})
|
|
304
|
+
this._server = undefined
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (socketPath && existsSync(socketPath)) {
|
|
308
|
+
try { unlinkSync(socketPath) } catch { /* ignore */ }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
this.setState({ listening: false, clientConnected: false, socketPath: undefined })
|
|
312
|
+
return this
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Write an NDJSON message to the connected app client.
|
|
317
|
+
*
|
|
318
|
+
* @param msg - The message object to send (will be JSON-serialized + newline)
|
|
319
|
+
* @returns True if the message was written, false if no client is connected
|
|
320
|
+
*/
|
|
321
|
+
send(msg: Record<string, any>): boolean {
|
|
322
|
+
if (!this._client) return false
|
|
323
|
+
this._client.socket.write(JSON.stringify(msg) + '\n')
|
|
324
|
+
return true
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// --- Private ---
|
|
328
|
+
|
|
329
|
+
/** Handle a new client connection from the native app. */
|
|
330
|
+
private handleClientConnect(socket: Socket): void {
|
|
331
|
+
const client: ClientConnection = { socket, buffer: '' }
|
|
332
|
+
|
|
333
|
+
if (this._client) {
|
|
334
|
+
this._client.socket.destroy()
|
|
335
|
+
}
|
|
336
|
+
this._client = client
|
|
337
|
+
|
|
338
|
+
this.setState({ clientConnected: true })
|
|
339
|
+
this.emit('clientConnected', socket)
|
|
340
|
+
|
|
341
|
+
socket.on('data', (chunk) => {
|
|
342
|
+
client.buffer += chunk.toString()
|
|
343
|
+
const lines = client.buffer.split('\n')
|
|
344
|
+
client.buffer = lines.pop() || ''
|
|
345
|
+
for (const line of lines) {
|
|
346
|
+
if (line.trim()) this.processLine(line)
|
|
347
|
+
}
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
socket.on('close', () => {
|
|
351
|
+
if (this._client === client) {
|
|
352
|
+
this._client = undefined
|
|
353
|
+
this.setState({ clientConnected: false })
|
|
354
|
+
this.emit('clientDisconnected')
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
socket.on('error', (err) => {
|
|
359
|
+
this.setState({ lastError: err.message })
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Process a single NDJSON line. Wraps commands in a CommandHandle; emits `message` for everything else. */
|
|
364
|
+
private processLine(line: string): void {
|
|
365
|
+
let msg: any
|
|
366
|
+
try {
|
|
367
|
+
msg = JSON.parse(line)
|
|
368
|
+
} catch {
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (msg.type === 'command') {
|
|
373
|
+
const handle = new CommandHandle(msg, (m) => this.send(m))
|
|
374
|
+
|
|
375
|
+
this.setState({
|
|
376
|
+
commandsReceived: (this.state.get('commandsReceived') ?? 0) + 1,
|
|
377
|
+
lastCommandText: handle.text,
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
this.emit('command', handle)
|
|
381
|
+
return
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
this.emit('message', msg)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export default features.register('launcherAppCommandListener', LauncherAppCommandListener)
|