@soederpop/luca 0.0.6 → 0.0.8
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/RUNME.md +56 -0
- 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/bootstrap/templates/runme.md +54 -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 +595 -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 +185 -0
- package/src/commands/chat.ts +5 -4
- package/src/commands/describe.ts +341 -4
- 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 +75 -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 +22 -2
- 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/vm-context.test.ts +146 -0
- package/test-integration/assistants-manager.test.ts +10 -20
- 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,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
|
|
2
|
+
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
3
3
|
import { Feature } from "../../feature.js";
|
|
4
4
|
import { type ContainerContext } from '../container.js'
|
|
5
5
|
|
|
@@ -17,6 +17,20 @@ export const VoiceRecognitionStateSchema = FeatureStateSchema.extend({
|
|
|
17
17
|
export type VoiceRecognitionOptions = z.infer<typeof VoiceRecognitionOptionsSchema>
|
|
18
18
|
export type VoiceRecognitionState = z.infer<typeof VoiceRecognitionStateSchema>
|
|
19
19
|
|
|
20
|
+
export const VoiceRecognitionEventsSchema = FeatureEventsSchema.extend({
|
|
21
|
+
result: z.tuple([
|
|
22
|
+
z.object({
|
|
23
|
+
finalTranscript: z.string().describe('Accumulated final transcript text'),
|
|
24
|
+
interimTranscript: z.string().describe('Current interim transcript text'),
|
|
25
|
+
}).describe('Recognition result payload'),
|
|
26
|
+
]).describe('Fires when speech recognition produces a result'),
|
|
27
|
+
error: z.tuple([z.string().describe('Error message from the recognizer')]).describe('Fires when speech recognition encounters an error'),
|
|
28
|
+
end: z.tuple([]).describe('Fires when speech recognition ends'),
|
|
29
|
+
start: z.tuple([]).describe('Fires when speech recognition starts listening'),
|
|
30
|
+
stop: z.tuple([]).describe('Fires when speech recognition is manually stopped'),
|
|
31
|
+
abort: z.tuple([]).describe('Fires when speech recognition is aborted'),
|
|
32
|
+
}).describe('VoiceRecognition events')
|
|
33
|
+
|
|
20
34
|
/**
|
|
21
35
|
* Speech-to-text recognition using the Web Speech API (SpeechRecognition).
|
|
22
36
|
*
|
|
@@ -44,6 +58,7 @@ export class VoiceRecognition<T extends VoiceRecognitionState = VoiceRecognition
|
|
|
44
58
|
|
|
45
59
|
static override stateSchema = VoiceRecognitionStateSchema
|
|
46
60
|
static override optionsSchema = VoiceRecognitionOptionsSchema
|
|
61
|
+
static override eventsSchema = VoiceRecognitionEventsSchema
|
|
47
62
|
static override shortcut = "features.voice" as const
|
|
48
63
|
|
|
49
64
|
static { Feature.register(this as any, 'voice') }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, spyOn } from 'bun:test'
|
|
2
2
|
import { NodeContainer } from '../src/node/container'
|
|
3
|
-
import { WebSocketClient
|
|
3
|
+
import { WebSocketClient } from '../src/clients/websocket'
|
|
4
|
+
import { GraphClient } from '../src/clients/graph'
|
|
4
5
|
|
|
5
6
|
describe('Clients', () => {
|
|
6
7
|
it('container has clients registry after construction', () => {
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test'
|
|
2
|
+
import { Command, commands } from '../src/command'
|
|
3
|
+
import { graftModule, isNativeHelperClass } from '../src/graft'
|
|
4
|
+
import { NodeContainer } from '../src/node/container'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
import { CommandOptionsSchema } from '../src/schemas/base'
|
|
7
|
+
// Side-effect import to register built-in commands
|
|
8
|
+
import '../src/commands/index'
|
|
9
|
+
|
|
10
|
+
describe('graftModule', () => {
|
|
11
|
+
it('creates a Command subclass from a run export', () => {
|
|
12
|
+
const argsSchema = CommandOptionsSchema.extend({
|
|
13
|
+
file: z.string(),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const Grafted = graftModule(Command as any, {
|
|
17
|
+
description: 'Run a file',
|
|
18
|
+
argsSchema,
|
|
19
|
+
positionals: ['file'],
|
|
20
|
+
run: async (args: any) => {},
|
|
21
|
+
}, 'graft-run-test', 'commands')
|
|
22
|
+
|
|
23
|
+
expect((Grafted as any).shortcut).toBe('commands.graft-run-test')
|
|
24
|
+
expect((Grafted as any).description).toBe('Run a file')
|
|
25
|
+
expect((Grafted as any).commandDescription).toBe('Run a file')
|
|
26
|
+
expect((Grafted as any).positionals).toEqual(['file'])
|
|
27
|
+
expect((Grafted as any).argsSchema).toBe(argsSchema)
|
|
28
|
+
expect(Grafted.name).toBe('GraftRunTestCommand')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('creates a Command subclass from a handler export (legacy)', () => {
|
|
32
|
+
const Grafted = graftModule(Command as any, {
|
|
33
|
+
description: 'Legacy handler',
|
|
34
|
+
handler: async (opts: any, ctx: any) => {},
|
|
35
|
+
}, 'graft-handler-test', 'commands')
|
|
36
|
+
|
|
37
|
+
expect((Grafted as any).shortcut).toBe('commands.graft-handler-test')
|
|
38
|
+
expect((Grafted as any).description).toBe('Legacy handler')
|
|
39
|
+
expect(typeof (Grafted as any).prototype.run).toBe('function')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('grafts extra exported functions as prototype methods', () => {
|
|
43
|
+
const Grafted = graftModule(Command as any, {
|
|
44
|
+
run: async () => {},
|
|
45
|
+
formatOutput: function (data: any) { return JSON.stringify(data) },
|
|
46
|
+
}, 'graft-methods-test', 'commands')
|
|
47
|
+
|
|
48
|
+
expect(typeof (Grafted as any).prototype.formatOutput).toBe('function')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('grafts getters onto the prototype', () => {
|
|
52
|
+
const Grafted = graftModule(Command as any, {
|
|
53
|
+
run: async () => {},
|
|
54
|
+
getters: {
|
|
55
|
+
isReady() { return true },
|
|
56
|
+
},
|
|
57
|
+
}, 'graft-getters-test', 'commands')
|
|
58
|
+
|
|
59
|
+
const desc = Object.getOwnPropertyDescriptor((Grafted as any).prototype, 'isReady')
|
|
60
|
+
expect(desc).toBeDefined()
|
|
61
|
+
expect(typeof desc!.get).toBe('function')
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('isNativeHelperClass', () => {
|
|
66
|
+
it('returns true for a direct subclass', () => {
|
|
67
|
+
class MyCmd extends Command {}
|
|
68
|
+
expect(isNativeHelperClass(MyCmd, Command)).toBe(true)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('returns false for a plain function', () => {
|
|
72
|
+
function notACommand() {}
|
|
73
|
+
expect(isNativeHelperClass(notACommand, Command)).toBe(false)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('returns false for null/undefined', () => {
|
|
77
|
+
expect(isNativeHelperClass(null, Command)).toBe(false)
|
|
78
|
+
expect(isNativeHelperClass(undefined, Command)).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('returns true for the base class itself', () => {
|
|
82
|
+
expect(isNativeHelperClass(Command, Command)).toBe(true)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('Command.register', () => {
|
|
87
|
+
it('registers a class-based command and sets shortcut', () => {
|
|
88
|
+
class TestDeployCommand extends Command {
|
|
89
|
+
static override description = 'Deploy to production'
|
|
90
|
+
}
|
|
91
|
+
Command.register(TestDeployCommand, 'test-deploy')
|
|
92
|
+
|
|
93
|
+
expect(commands.has('test-deploy')).toBe(true)
|
|
94
|
+
expect((TestDeployCommand as any).shortcut).toBe('commands.test-deploy')
|
|
95
|
+
expect((TestDeployCommand as any).commandDescription).toBe('Deploy to production')
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('Command.dispatch', () => {
|
|
100
|
+
it('calls run() with parsed args for CLI dispatch', async () => {
|
|
101
|
+
let received: any = null
|
|
102
|
+
|
|
103
|
+
const argsSchema = CommandOptionsSchema.extend({
|
|
104
|
+
target: z.string().default('prod'),
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const Grafted = graftModule(Command as any, {
|
|
108
|
+
argsSchema,
|
|
109
|
+
run: async (args: any, ctx: any) => { received = args },
|
|
110
|
+
}, 'dispatch-cli-test', 'commands')
|
|
111
|
+
|
|
112
|
+
commands.register('dispatch-cli-test', Grafted as any)
|
|
113
|
+
const container = new NodeContainer()
|
|
114
|
+
const cmd = container.command('dispatch-cli-test' as any)
|
|
115
|
+
|
|
116
|
+
await cmd.dispatch({ _: ['dispatch-cli-test'], target: 'staging' }, 'cli')
|
|
117
|
+
|
|
118
|
+
expect(received).toBeDefined()
|
|
119
|
+
expect(received.target).toBe('staging')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('maps positionals to named args for CLI dispatch', async () => {
|
|
123
|
+
let received: any = null
|
|
124
|
+
|
|
125
|
+
const argsSchema = CommandOptionsSchema.extend({
|
|
126
|
+
file: z.string(),
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
const Grafted = graftModule(Command as any, {
|
|
130
|
+
argsSchema,
|
|
131
|
+
positionals: ['file'],
|
|
132
|
+
run: async (args: any, ctx: any) => { received = args },
|
|
133
|
+
}, 'dispatch-positional-test', 'commands')
|
|
134
|
+
|
|
135
|
+
commands.register('dispatch-positional-test', Grafted as any)
|
|
136
|
+
const container = new NodeContainer()
|
|
137
|
+
const cmd = container.command('dispatch-positional-test' as any)
|
|
138
|
+
|
|
139
|
+
// Simulate: luca dispatch-positional-test myfile.ts
|
|
140
|
+
// minimist produces: { _: ['dispatch-positional-test', 'myfile.ts'] }
|
|
141
|
+
await cmd.dispatch({ _: ['dispatch-positional-test', 'myfile.ts'] }, 'cli')
|
|
142
|
+
|
|
143
|
+
expect(received).toBeDefined()
|
|
144
|
+
expect(received.file).toBe('myfile.ts')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('collects remaining positionals into an array when schema expects one', async () => {
|
|
148
|
+
let received: any = null
|
|
149
|
+
|
|
150
|
+
const argsSchema = CommandOptionsSchema.extend({
|
|
151
|
+
action: z.string(),
|
|
152
|
+
files: z.array(z.string()),
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const Grafted = graftModule(Command as any, {
|
|
156
|
+
argsSchema,
|
|
157
|
+
positionals: ['action', 'files'],
|
|
158
|
+
run: async (args: any, ctx: any) => { received = args },
|
|
159
|
+
}, 'dispatch-glob-test', 'commands')
|
|
160
|
+
|
|
161
|
+
commands.register('dispatch-glob-test', Grafted as any)
|
|
162
|
+
const container = new NodeContainer()
|
|
163
|
+
const cmd = container.command('dispatch-glob-test' as any)
|
|
164
|
+
|
|
165
|
+
// Simulate: luca dispatch-glob-test process foo.md bar.md baz.md
|
|
166
|
+
// Shell expands *.md before luca sees it
|
|
167
|
+
await cmd.dispatch({ _: ['dispatch-glob-test', 'process', 'foo.md', 'bar.md', 'baz.md'] }, 'cli')
|
|
168
|
+
|
|
169
|
+
expect(received).toBeDefined()
|
|
170
|
+
expect(received.action).toBe('process')
|
|
171
|
+
expect(received.files).toEqual(['foo.md', 'bar.md', 'baz.md'])
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('passes named args through for headless dispatch', async () => {
|
|
175
|
+
let received: any = null
|
|
176
|
+
|
|
177
|
+
const argsSchema = CommandOptionsSchema.extend({
|
|
178
|
+
file: z.string(),
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
const Grafted = graftModule(Command as any, {
|
|
182
|
+
argsSchema,
|
|
183
|
+
positionals: ['file'],
|
|
184
|
+
run: async (args: any, ctx: any) => { received = args },
|
|
185
|
+
}, 'dispatch-headless-test', 'commands')
|
|
186
|
+
|
|
187
|
+
commands.register('dispatch-headless-test', Grafted as any)
|
|
188
|
+
const container = new NodeContainer()
|
|
189
|
+
const cmd = container.command('dispatch-headless-test' as any)
|
|
190
|
+
|
|
191
|
+
// Headless: named args directly, no positional mapping
|
|
192
|
+
await cmd.dispatch({ file: 'script.ts' }, 'headless')
|
|
193
|
+
|
|
194
|
+
expect(received).toBeDefined()
|
|
195
|
+
expect(received.file).toBe('script.ts')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('captures stdout/stderr for headless dispatch', async () => {
|
|
199
|
+
const argsSchema = CommandOptionsSchema.extend({})
|
|
200
|
+
|
|
201
|
+
const Grafted = graftModule(Command as any, {
|
|
202
|
+
argsSchema,
|
|
203
|
+
run: async (args: any, ctx: any) => {
|
|
204
|
+
console.log('hello from command')
|
|
205
|
+
console.error('warning: something')
|
|
206
|
+
},
|
|
207
|
+
}, 'dispatch-capture-test', 'commands')
|
|
208
|
+
|
|
209
|
+
commands.register('dispatch-capture-test', Grafted as any)
|
|
210
|
+
const container = new NodeContainer()
|
|
211
|
+
const cmd = container.command('dispatch-capture-test' as any)
|
|
212
|
+
|
|
213
|
+
const result = await cmd.dispatch({}, 'headless')
|
|
214
|
+
|
|
215
|
+
expect(result).toBeDefined()
|
|
216
|
+
expect(result!.exitCode).toBe(0)
|
|
217
|
+
expect(result!.stdout).toContain('hello from command')
|
|
218
|
+
expect(result!.stderr).toContain('warning: something')
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('captures errors for headless dispatch', async () => {
|
|
222
|
+
const argsSchema = CommandOptionsSchema.extend({})
|
|
223
|
+
|
|
224
|
+
const Grafted = graftModule(Command as any, {
|
|
225
|
+
argsSchema,
|
|
226
|
+
run: async () => { throw new Error('boom') },
|
|
227
|
+
}, 'dispatch-error-test', 'commands')
|
|
228
|
+
|
|
229
|
+
commands.register('dispatch-error-test', Grafted as any)
|
|
230
|
+
const container = new NodeContainer()
|
|
231
|
+
const cmd = container.command('dispatch-error-test' as any)
|
|
232
|
+
|
|
233
|
+
const result = await cmd.dispatch({}, 'headless')
|
|
234
|
+
|
|
235
|
+
expect(result).toBeDefined()
|
|
236
|
+
expect(result!.exitCode).toBe(1)
|
|
237
|
+
expect(result!.stderr).toContain('boom')
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
describe('Command registry', () => {
|
|
242
|
+
it('has built-in commands registered', () => {
|
|
243
|
+
const container = new NodeContainer()
|
|
244
|
+
expect(container.commands.has('run')).toBe(true)
|
|
245
|
+
expect(container.commands.has('help')).toBe(true)
|
|
246
|
+
expect(container.commands.has('eval')).toBe(true)
|
|
247
|
+
expect(container.commands.has('chat')).toBe(true)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('built-in commands still work through registerHandler', () => {
|
|
251
|
+
const container = new NodeContainer()
|
|
252
|
+
const RunClass = container.commands.lookup('run')
|
|
253
|
+
expect(typeof RunClass).toBe('function')
|
|
254
|
+
expect(typeof RunClass.prototype.run).toBe('function')
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
describe('SimpleCommand type', () => {
|
|
259
|
+
it('grafted commands have positionals as a static property', () => {
|
|
260
|
+
const Grafted = graftModule(Command as any, {
|
|
261
|
+
positionals: ['env', 'region'],
|
|
262
|
+
run: async () => {},
|
|
263
|
+
}, 'simple-cmd-type-test', 'commands')
|
|
264
|
+
|
|
265
|
+
expect((Grafted as any).positionals).toEqual(['env', 'region'])
|
|
266
|
+
})
|
|
267
|
+
})
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test'
|
|
2
|
+
import { NodeContainer } from '../src/node/container'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* These tests verify the VM context preservation behavior that `luca run`
|
|
6
|
+
* depends on for markdown execution. When running sequential code blocks
|
|
7
|
+
* in a shared context, variables defined in earlier blocks must be accessible
|
|
8
|
+
* in later blocks — as long as the blocks don't use top-level await (which
|
|
9
|
+
* wraps code in an async IIFE and isolates the scope).
|
|
10
|
+
*/
|
|
11
|
+
describe('VM context preservation across sequential runs', () => {
|
|
12
|
+
it('preserves const declarations across runs without await', async () => {
|
|
13
|
+
const c = new NodeContainer()
|
|
14
|
+
const vm = c.feature('vm')
|
|
15
|
+
const shared = vm.createContext({})
|
|
16
|
+
|
|
17
|
+
await vm.run('const x = 42', shared)
|
|
18
|
+
const result = await vm.run('x', shared)
|
|
19
|
+
|
|
20
|
+
expect(result).toBe(42)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('preserves let declarations across runs without await', async () => {
|
|
24
|
+
const c = new NodeContainer()
|
|
25
|
+
const vm = c.feature('vm')
|
|
26
|
+
const shared = vm.createContext({})
|
|
27
|
+
|
|
28
|
+
await vm.run('let count = 0', shared)
|
|
29
|
+
await vm.run('count += 10', shared)
|
|
30
|
+
const result = await vm.run('count', shared)
|
|
31
|
+
|
|
32
|
+
expect(result).toBe(10)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('preserves function declarations across runs', async () => {
|
|
36
|
+
const c = new NodeContainer()
|
|
37
|
+
const vm = c.feature('vm')
|
|
38
|
+
const shared = vm.createContext({})
|
|
39
|
+
|
|
40
|
+
await vm.run('function double(n) { return n * 2 }', shared)
|
|
41
|
+
const result = await vm.run('double(21)', shared)
|
|
42
|
+
|
|
43
|
+
expect(result).toBe(42)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('preserves arrow functions assigned to const', async () => {
|
|
47
|
+
const c = new NodeContainer()
|
|
48
|
+
const vm = c.feature('vm')
|
|
49
|
+
const shared = vm.createContext({})
|
|
50
|
+
|
|
51
|
+
await vm.run('const greet = (name) => `hello ${name}`', shared)
|
|
52
|
+
const result = await vm.run('greet("world")', shared)
|
|
53
|
+
|
|
54
|
+
expect(result).toBe('hello world')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('preserves objects and allows mutation across runs', async () => {
|
|
58
|
+
const c = new NodeContainer()
|
|
59
|
+
const vm = c.feature('vm')
|
|
60
|
+
const shared = vm.createContext({})
|
|
61
|
+
|
|
62
|
+
await vm.run('const data = { items: [] }', shared)
|
|
63
|
+
await vm.run('data.items.push("a", "b")', shared)
|
|
64
|
+
await vm.run('data.items.push("c")', shared)
|
|
65
|
+
const result = await vm.run('data.items', shared)
|
|
66
|
+
|
|
67
|
+
expect(result).toEqual(['a', 'b', 'c'])
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('preserves class declarations across runs', async () => {
|
|
71
|
+
const c = new NodeContainer()
|
|
72
|
+
const vm = c.feature('vm')
|
|
73
|
+
const shared = vm.createContext({})
|
|
74
|
+
|
|
75
|
+
await vm.run('class Counter { constructor() { this.n = 0 } inc() { this.n++ } }', shared)
|
|
76
|
+
await vm.run('const counter = new Counter(); counter.inc(); counter.inc()', shared)
|
|
77
|
+
const result = await vm.run('counter.n', shared)
|
|
78
|
+
|
|
79
|
+
expect(result).toBe(2)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('later blocks can compose values from earlier blocks', async () => {
|
|
83
|
+
const c = new NodeContainer()
|
|
84
|
+
const vm = c.feature('vm')
|
|
85
|
+
const shared = vm.createContext({})
|
|
86
|
+
|
|
87
|
+
await vm.run('const firstName = "Jane"', shared)
|
|
88
|
+
await vm.run('const lastName = "Doe"', shared)
|
|
89
|
+
await vm.run('const fullName = `${firstName} ${lastName}`', shared)
|
|
90
|
+
const result = await vm.run('fullName', shared)
|
|
91
|
+
|
|
92
|
+
expect(result).toBe('Jane Doe')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('top-level await blocks lose const/let scope (known limitation)', async () => {
|
|
96
|
+
const c = new NodeContainer()
|
|
97
|
+
const vm = c.feature('vm')
|
|
98
|
+
const shared = vm.createContext({})
|
|
99
|
+
|
|
100
|
+
// This block has await, so it gets wrapped in (async () => { ... })()
|
|
101
|
+
// The const is scoped to that IIFE and doesn't leak to the shared context
|
|
102
|
+
await vm.run('const val = await Promise.resolve(99)', shared)
|
|
103
|
+
const result = await vm.run('typeof val', shared)
|
|
104
|
+
|
|
105
|
+
expect(result).toBe('undefined')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('top-level await blocks can share state via pre-existing context properties', async () => {
|
|
109
|
+
const c = new NodeContainer()
|
|
110
|
+
const vm = c.feature('vm')
|
|
111
|
+
const shared = vm.createContext({ result: null })
|
|
112
|
+
|
|
113
|
+
// Assigning to an existing context property works even inside the IIFE
|
|
114
|
+
await vm.run('result = await Promise.resolve("async value")', shared)
|
|
115
|
+
const value = await vm.run('result', shared)
|
|
116
|
+
|
|
117
|
+
expect(value).toBe('async value')
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe('VM wrapTopLevelAwait', () => {
|
|
122
|
+
it('does not wrap code without await', () => {
|
|
123
|
+
const c = new NodeContainer()
|
|
124
|
+
const vm = c.feature('vm')
|
|
125
|
+
|
|
126
|
+
const code = 'const x = 42'
|
|
127
|
+
expect(vm.wrapTopLevelAwait(code)).toBe(code)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('wraps code containing await in an async IIFE', () => {
|
|
131
|
+
const c = new NodeContainer()
|
|
132
|
+
const vm = c.feature('vm')
|
|
133
|
+
|
|
134
|
+
const wrapped = vm.wrapTopLevelAwait('const x = await fetch("http://example.com")')
|
|
135
|
+
expect(wrapped).toContain('async')
|
|
136
|
+
expect(wrapped).toContain('await fetch')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('does not wrap code already in an async function', () => {
|
|
140
|
+
const c = new NodeContainer()
|
|
141
|
+
const vm = c.feature('vm')
|
|
142
|
+
|
|
143
|
+
const code = 'async function go() { await fetch("http://example.com") }'
|
|
144
|
+
expect(vm.wrapTopLevelAwait(code)).toBe(code)
|
|
145
|
+
})
|
|
146
|
+
})
|
|
@@ -51,20 +51,16 @@ describe('Assistants Manager Integration', () => {
|
|
|
51
51
|
})
|
|
52
52
|
|
|
53
53
|
it('discovers assistants from directory', () => {
|
|
54
|
-
const manager = container.feature('assistantsManager'
|
|
55
|
-
|
|
56
|
-
})
|
|
57
|
-
manager.discover()
|
|
54
|
+
const manager = container.feature('assistantsManager')
|
|
55
|
+
await manager.discover()
|
|
58
56
|
|
|
59
57
|
const list = manager.list()
|
|
60
58
|
expect(list.length).toBe(3)
|
|
61
59
|
})
|
|
62
60
|
|
|
63
61
|
it('returns correct entry metadata', () => {
|
|
64
|
-
const manager = container.feature('assistantsManager'
|
|
65
|
-
|
|
66
|
-
})
|
|
67
|
-
manager.discover()
|
|
62
|
+
const manager = container.feature('assistantsManager')
|
|
63
|
+
await manager.discover()
|
|
68
64
|
|
|
69
65
|
const entryA = manager.get('assistant-a')
|
|
70
66
|
expect(entryA).toBeDefined()
|
|
@@ -84,10 +80,8 @@ describe('Assistants Manager Integration', () => {
|
|
|
84
80
|
})
|
|
85
81
|
|
|
86
82
|
it('creates an assistant instance', () => {
|
|
87
|
-
const manager = container.feature('assistantsManager'
|
|
88
|
-
|
|
89
|
-
})
|
|
90
|
-
manager.discover()
|
|
83
|
+
const manager = container.feature('assistantsManager')
|
|
84
|
+
await manager.discover()
|
|
91
85
|
|
|
92
86
|
const assistant = manager.create('assistant-a', {
|
|
93
87
|
model: 'gpt-4o-mini',
|
|
@@ -99,10 +93,8 @@ describe('Assistants Manager Integration', () => {
|
|
|
99
93
|
})
|
|
100
94
|
|
|
101
95
|
it('generates a summary listing', () => {
|
|
102
|
-
const manager = container.feature('assistantsManager'
|
|
103
|
-
|
|
104
|
-
})
|
|
105
|
-
manager.discover()
|
|
96
|
+
const manager = container.feature('assistantsManager')
|
|
97
|
+
await manager.discover()
|
|
106
98
|
|
|
107
99
|
const summary = manager.toSummary()
|
|
108
100
|
expect(typeof summary).toBe('string')
|
|
@@ -112,10 +104,8 @@ describe('Assistants Manager Integration', () => {
|
|
|
112
104
|
})
|
|
113
105
|
|
|
114
106
|
it('get returns undefined for non-existent assistant', () => {
|
|
115
|
-
const manager = container.feature('assistantsManager'
|
|
116
|
-
|
|
117
|
-
})
|
|
118
|
-
manager.discover()
|
|
107
|
+
const manager = container.feature('assistantsManager')
|
|
108
|
+
await manager.discover()
|
|
119
109
|
|
|
120
110
|
const entry = manager.get('non-existent')
|
|
121
111
|
expect(entry).toBeUndefined()
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
# LauncherAppCommandListener (features.launcherAppCommandListener)
|
|
2
|
-
|
|
3
|
-
LauncherAppCommandListener — IPC transport for commands from the LucaVoiceLauncher app Listens on a Unix domain socket for the native macOS launcher app to connect. When a command event arrives (voice, hotkey, text input), it wraps it in a `CommandHandle` and emits a `command` event. The consumer is responsible for acknowledging, processing, and finishing the command via the handle. Uses NDJSON (newline-delimited JSON) over the socket per the CLIENT_SPEC protocol.
|
|
4
|
-
|
|
5
|
-
## Usage
|
|
6
|
-
|
|
7
|
-
```ts
|
|
8
|
-
container.feature('launcherAppCommandListener', {
|
|
9
|
-
// Path to the Unix domain socket to listen on
|
|
10
|
-
socketPath,
|
|
11
|
-
// Automatically start listening when the feature is enabled
|
|
12
|
-
autoListen,
|
|
13
|
-
})
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
## Options (Zod v4 schema)
|
|
17
|
-
|
|
18
|
-
| Property | Type | Description |
|
|
19
|
-
|----------|------|-------------|
|
|
20
|
-
| `socketPath` | `string` | Path to the Unix domain socket to listen on |
|
|
21
|
-
| `autoListen` | `boolean` | Automatically start listening when the feature is enabled |
|
|
22
|
-
|
|
23
|
-
## Methods
|
|
24
|
-
|
|
25
|
-
### enable
|
|
26
|
-
|
|
27
|
-
**Parameters:**
|
|
28
|
-
|
|
29
|
-
| Name | Type | Required | Description |
|
|
30
|
-
|------|------|----------|-------------|
|
|
31
|
-
| `options` | `any` | | Parameter options |
|
|
32
|
-
|
|
33
|
-
**Returns:** `Promise<this>`
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
### listen
|
|
38
|
-
|
|
39
|
-
Start listening on the Unix domain socket for the native app to connect. Fire-and-forget — binds the socket and returns immediately. Sits quietly until the native app connects; does nothing visible if it never does.
|
|
40
|
-
|
|
41
|
-
**Parameters:**
|
|
42
|
-
|
|
43
|
-
| Name | Type | Required | Description |
|
|
44
|
-
|------|------|----------|-------------|
|
|
45
|
-
| `socketPath` | `string` | | Override the configured socket path |
|
|
46
|
-
|
|
47
|
-
**Returns:** `this`
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
### stop
|
|
52
|
-
|
|
53
|
-
Stop the IPC server and clean up all connections.
|
|
54
|
-
|
|
55
|
-
**Returns:** `Promise<this>`
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
### send
|
|
60
|
-
|
|
61
|
-
Write an NDJSON message to the connected app client.
|
|
62
|
-
|
|
63
|
-
**Parameters:**
|
|
64
|
-
|
|
65
|
-
| Name | Type | Required | Description |
|
|
66
|
-
|------|------|----------|-------------|
|
|
67
|
-
| `msg` | `Record<string, any>` | ✓ | The message object to send (will be JSON-serialized + newline) |
|
|
68
|
-
|
|
69
|
-
**Returns:** `boolean`
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
## Getters
|
|
74
|
-
|
|
75
|
-
| Property | Type | Description |
|
|
76
|
-
|----------|------|-------------|
|
|
77
|
-
| `isListening` | `boolean` | Whether the IPC server is currently listening. |
|
|
78
|
-
| `isClientConnected` | `boolean` | Whether the native app client is currently connected. |
|
|
79
|
-
|
|
80
|
-
## Events (Zod v4 schema)
|
|
81
|
-
|
|
82
|
-
### listening
|
|
83
|
-
|
|
84
|
-
Event emitted by LauncherAppCommandListener
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
### clientConnected
|
|
89
|
-
|
|
90
|
-
Event emitted by LauncherAppCommandListener
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
### clientDisconnected
|
|
95
|
-
|
|
96
|
-
Event emitted by LauncherAppCommandListener
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
### command
|
|
101
|
-
|
|
102
|
-
Event emitted by LauncherAppCommandListener
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
### message
|
|
107
|
-
|
|
108
|
-
Event emitted by LauncherAppCommandListener
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
## State (Zod v4 schema)
|
|
113
|
-
|
|
114
|
-
| Property | Type | Description |
|
|
115
|
-
|----------|------|-------------|
|
|
116
|
-
| `enabled` | `boolean` | Whether this feature is currently enabled |
|
|
117
|
-
| `listening` | `boolean` | Whether the IPC server is listening |
|
|
118
|
-
| `clientConnected` | `boolean` | Whether the native launcher app is connected |
|
|
119
|
-
| `socketPath` | `string` | The socket path in use |
|
|
120
|
-
| `commandsReceived` | `number` | Total number of commands received |
|
|
121
|
-
| `lastCommandText` | `string` | The text of the last received command |
|
|
122
|
-
| `lastError` | `string` | Last error message |
|
|
123
|
-
|
|
124
|
-
## Examples
|
|
125
|
-
|
|
126
|
-
**features.launcherAppCommandListener**
|
|
127
|
-
|
|
128
|
-
```ts
|
|
129
|
-
const listener = container.feature('launcherAppCommandListener', {
|
|
130
|
-
enable: true,
|
|
131
|
-
autoListen: true,
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
listener.on('command', async (cmd) => {
|
|
135
|
-
cmd.ack('Working on it!') // or just cmd.ack() for silent
|
|
136
|
-
|
|
137
|
-
// ... do your actual work ...
|
|
138
|
-
cmd.progress(0.5, 'Halfway there')
|
|
139
|
-
|
|
140
|
-
cmd.finish() // silent finish
|
|
141
|
-
cmd.finish({ result: { action: 'completed' }, speech: 'All done!' })
|
|
142
|
-
// or: cmd.fail({ error: 'not found', speech: 'Sorry, that failed.' })
|
|
143
|
-
})
|
|
144
|
-
```
|
|
145
|
-
|