@soederpop/luca 0.0.2 → 0.0.4
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/AGENTS.md +98 -0
- package/CLAUDE.md +27 -0
- package/SPEC.md +304 -0
- package/bun.lock +110 -265
- package/docs/CLI.md +1 -1
- package/docs/apis/features/node/content-db.md +16 -0
- package/docs/apis/features/node/fs.md +24 -0
- package/docs/apis/features/node/ipc-socket.md +0 -1
- package/docs/apis/features/node/package-finder.md +1 -11
- package/docs/apis/features/node/proc.md +0 -41
- package/docs/apis/features/node/ui.md +0 -2
- package/package.json +12 -8
- package/src/agi/container.server.ts +16 -3
- package/src/agi/features/assistant.ts +3 -7
- package/src/agi/features/assistants-manager.ts +3 -7
- package/src/agi/features/claude-code.ts +3 -7
- package/src/agi/features/conversation-history.ts +3 -7
- package/src/agi/features/conversation.ts +4 -8
- package/src/agi/features/openai-codex.ts +3 -7
- package/src/agi/features/openapi.ts +4 -2
- package/src/agi/features/skills-library.ts +4 -8
- package/src/cli/cli.ts +22 -0
- package/src/client.ts +69 -26
- package/src/clients/civitai/index.ts +3 -7
- package/src/clients/comfyui/index.ts +5 -9
- package/src/clients/elevenlabs/index.ts +39 -19
- package/src/clients/openai/index.ts +3 -7
- package/src/clients/supabase/index.ts +4 -13
- package/src/commands/console.ts +0 -3
- package/src/commands/eval.ts +1 -1
- package/src/commands/index.ts +1 -0
- package/src/commands/introspect.ts +128 -0
- package/src/commands/prompt.ts +1 -4
- package/src/commands/run.ts +6 -13
- package/src/commands/sandbox-mcp.ts +1 -13
- package/src/feature.ts +45 -2
- package/src/introspection/generated.agi.ts +175 -101
- package/src/introspection/generated.node.ts +175 -101
- package/src/introspection/generated.web.ts +113 -29
- package/src/introspection/index.ts +1 -1
- package/src/introspection/scan.ts +3 -1
- package/src/node/features/container-link.ts +3 -2
- package/src/node/features/content-db.ts +10 -2
- package/src/node/features/disk-cache.ts +3 -4
- package/src/node/features/dns.ts +3 -2
- package/src/node/features/docker.ts +3 -2
- package/src/node/features/downloader.ts +3 -16
- package/src/node/features/esbuild.ts +3 -12
- package/src/node/features/file-manager.ts +3 -2
- package/src/node/features/fs.ts +12 -3
- package/src/node/features/git.ts +3 -2
- package/src/node/features/google-auth.ts +3 -2
- package/src/node/features/google-calendar.ts +3 -2
- package/src/node/features/google-docs.ts +3 -2
- package/src/node/features/google-drive.ts +3 -2
- package/src/node/features/google-sheets.ts +3 -2
- package/src/node/features/grep.ts +3 -2
- package/src/node/features/helpers.ts +13 -2
- package/src/node/features/ink.ts +3 -3
- package/src/node/features/ipc-socket.ts +3 -3
- package/src/node/features/json-tree.ts +3 -21
- package/src/node/features/launcher-app-command-listener.ts +3 -2
- package/src/node/features/networking.ts +3 -2
- package/src/node/features/nlp.ts +3 -2
- package/src/node/features/opener.ts +8 -7
- package/src/node/features/os.ts +3 -2
- package/src/node/features/package-finder.ts +3 -2
- package/src/node/features/port-exposer.ts +3 -4
- package/src/node/features/postgres.ts +3 -3
- package/src/node/features/proc.ts +37 -64
- package/src/node/features/process-manager.ts +3 -2
- package/src/node/features/python.ts +3 -3
- package/src/node/features/repl.ts +3 -2
- package/src/node/features/runpod.ts +3 -3
- package/src/node/features/secure-shell.ts +3 -2
- package/src/node/features/semantic-search.ts +4 -6
- package/src/node/features/sqlite.ts +3 -3
- package/src/node/features/telegram.ts +3 -2
- package/src/node/features/tts.ts +3 -2
- package/src/node/features/ui.ts +3 -3
- package/src/node/features/vault.ts +3 -14
- package/src/node/features/vm.ts +41 -3
- package/src/node/features/window-manager.ts +165 -22
- package/src/node/features/yaml-tree.ts +3 -4
- package/src/node/features/yaml.ts +3 -2
- package/src/registry.ts +1 -1
- package/src/scaffolds/generated.ts +1 -1
- package/src/server.ts +43 -0
- package/src/servers/express.ts +24 -8
- package/src/servers/mcp.ts +2 -6
- package/src/servers/socket.ts +22 -7
- package/src/web/clients/socket.ts +3 -5
- package/src/web/features/asset-loader.ts +20 -12
- package/src/web/features/container-link.ts +3 -6
- package/src/web/features/esbuild.ts +21 -7
- package/src/web/features/helpers.ts +4 -2
- package/src/web/features/network.ts +24 -7
- package/src/web/features/speech.ts +24 -7
- package/src/web/features/vault.ts +21 -3
- package/src/web/features/vm.ts +20 -13
- package/src/web/features/voice-recognition.ts +26 -9
- package/commands/update-introspection.ts +0 -67
- package/docs/ideas/class-registration-refactor-possibilities.md +0 -197
- package/docs/ideas/container-use-api.md +0 -9
- package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
- package/docs/ideas/feature-stacks.md +0 -22
- package/docs/ideas/luca-cli-self-sufficiency-demo.md +0 -23
- package/docs/ideas/mcp-design.md +0 -9
- package/docs/ideas/web-container-debugging-feature.md +0 -13
- package/scripts/animations/chrome-glitch.ts +0 -55
- package/scripts/animations/index.ts +0 -16
- package/scripts/animations/neon-pulse.ts +0 -64
- package/scripts/animations/types.ts +0 -6
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
|
|
3
|
-
import { Feature
|
|
3
|
+
import { Feature } from "../feature.js";
|
|
4
4
|
import vm from 'vm'
|
|
5
5
|
import readline from 'readline'
|
|
6
6
|
import { displayResult } from '../../commands/eval.js'
|
|
@@ -36,6 +36,7 @@ export class Repl<
|
|
|
36
36
|
static override shortcut = "features.repl" as const
|
|
37
37
|
static override stateSchema = ReplStateSchema
|
|
38
38
|
static override optionsSchema = ReplOptionsSchema
|
|
39
|
+
static { Feature.register(this, 'repl') }
|
|
39
40
|
|
|
40
41
|
/** Whether the REPL session is currently running. */
|
|
41
42
|
get isStarted() {
|
|
@@ -191,4 +192,4 @@ export class Repl<
|
|
|
191
192
|
}
|
|
192
193
|
}
|
|
193
194
|
|
|
194
|
-
export default
|
|
195
|
+
export default Repl
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
|
|
3
|
-
import { Feature
|
|
3
|
+
import { Feature } from '../feature'
|
|
4
4
|
import axios from 'axios'
|
|
5
5
|
|
|
6
6
|
export const RunpodStateSchema = FeatureStateSchema.extend({})
|
|
@@ -35,6 +35,7 @@ export class Runpod extends Feature<RunpodState, RunpodOptions> {
|
|
|
35
35
|
static override envVars = ['RUNPOD_API_KEY']
|
|
36
36
|
static override stateSchema = RunpodStateSchema
|
|
37
37
|
static override optionsSchema = RunpodOptionsSchema
|
|
38
|
+
static { Feature.register(this, 'runpod') }
|
|
38
39
|
|
|
39
40
|
/** The proc feature used for executing CLI commands like runpodctl. */
|
|
40
41
|
get proc() {
|
|
@@ -607,8 +608,7 @@ export class Runpod extends Feature<RunpodState, RunpodOptions> {
|
|
|
607
608
|
|
|
608
609
|
}
|
|
609
610
|
|
|
610
|
-
export default
|
|
611
|
-
|
|
611
|
+
export default Runpod
|
|
612
612
|
/** Shell-escape a string for safe use in SSH commands */
|
|
613
613
|
function esc(s: string): string {
|
|
614
614
|
return `'${s.replace(/'/g, "'\\''")}'`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
|
|
3
|
-
import { Feature
|
|
3
|
+
import { Feature } from '../feature.js'
|
|
4
4
|
|
|
5
5
|
export const SecureShellStateSchema = FeatureStateSchema.extend({
|
|
6
6
|
/** Whether an SSH connection is currently active */
|
|
@@ -49,6 +49,7 @@ export class SecureShell extends Feature<SecureShellState, SecureShellOptions> {
|
|
|
49
49
|
static override shortcut = 'features.secureShell' as const
|
|
50
50
|
static override stateSchema = SecureShellStateSchema
|
|
51
51
|
static override optionsSchema = SecureShellOptionsSchema
|
|
52
|
+
static { Feature.register(this, 'secureShell') }
|
|
52
53
|
|
|
53
54
|
override get initialState(): SecureShellState {
|
|
54
55
|
return {
|
|
@@ -245,4 +246,4 @@ export class SecureShell extends Feature<SecureShellState, SecureShellOptions> {
|
|
|
245
246
|
}
|
|
246
247
|
}
|
|
247
248
|
|
|
248
|
-
export default
|
|
249
|
+
export default SecureShell
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
|
|
3
|
-
import type { Container } from '@soederpop/luca/container'
|
|
4
3
|
import { type AvailableFeatures } from '@soederpop/luca/feature'
|
|
5
|
-
import {
|
|
4
|
+
import { Feature } from '../feature.js'
|
|
6
5
|
import { Database } from 'bun:sqlite'
|
|
7
6
|
import { createHash } from 'node:crypto'
|
|
8
7
|
import { mkdirSync, existsSync, statSync } from 'node:fs'
|
|
@@ -275,6 +274,7 @@ export class SemanticSearch extends Feature<SemanticSearchState, SemanticSearchO
|
|
|
275
274
|
static override stateSchema = SemanticSearchStateSchema
|
|
276
275
|
static override optionsSchema = SemanticSearchOptionsSchema
|
|
277
276
|
static override shortcut = 'features.semanticSearch' as const
|
|
277
|
+
static { Feature.register(this, 'semanticSearch') }
|
|
278
278
|
|
|
279
279
|
private _db: Database | null = null
|
|
280
280
|
private _llamaContext: any = null
|
|
@@ -283,10 +283,6 @@ export class SemanticSearch extends Feature<SemanticSearchState, SemanticSearchO
|
|
|
283
283
|
private _idleTimer: ReturnType<typeof setTimeout> | null = null
|
|
284
284
|
private _dimensions: number
|
|
285
285
|
|
|
286
|
-
static attach(container: Container<AvailableFeatures, any>) {
|
|
287
|
-
features.register('semanticSearch', SemanticSearch)
|
|
288
|
-
return container
|
|
289
|
-
}
|
|
290
286
|
|
|
291
287
|
override get initialState(): SemanticSearchState {
|
|
292
288
|
return {
|
|
@@ -922,3 +918,5 @@ export class SemanticSearch extends Feature<SemanticSearchState, SemanticSearchO
|
|
|
922
918
|
this.state.set('dbReady', false)
|
|
923
919
|
}
|
|
924
920
|
}
|
|
921
|
+
|
|
922
|
+
export default SemanticSearch
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { Database } from 'bun:sqlite'
|
|
3
|
-
import { Feature
|
|
3
|
+
import { Feature } from '../feature.js'
|
|
4
4
|
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
5
5
|
import type { ContainerContext } from '../../container.js'
|
|
6
6
|
|
|
@@ -69,6 +69,7 @@ export class Sqlite extends Feature<SqliteState, SqliteOptions> {
|
|
|
69
69
|
static override stateSchema = SqliteStateSchema
|
|
70
70
|
static override optionsSchema = SqliteOptionsSchema
|
|
71
71
|
static override eventsSchema = SqliteEventsSchema
|
|
72
|
+
static { Feature.register(this, 'sqlite') }
|
|
72
73
|
|
|
73
74
|
private _db: Database
|
|
74
75
|
|
|
@@ -247,8 +248,7 @@ export class Sqlite extends Feature<SqliteState, SqliteOptions> {
|
|
|
247
248
|
}
|
|
248
249
|
}
|
|
249
250
|
|
|
250
|
-
export default
|
|
251
|
-
|
|
251
|
+
export default Sqlite
|
|
252
252
|
declare module '../../feature.js' {
|
|
253
253
|
interface AvailableFeatures {
|
|
254
254
|
sqlite: typeof Sqlite
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
3
|
-
import { Feature
|
|
3
|
+
import { Feature } from '../feature.js'
|
|
4
4
|
import { Bot, webhookCallback, type Context, type Middleware } from 'grammy'
|
|
5
5
|
|
|
6
6
|
type UserFromGetMe = Awaited<ReturnType<Bot['api']['getMe']>>
|
|
@@ -80,6 +80,7 @@ export class Telegram extends Feature<TelegramState, TelegramOptions> {
|
|
|
80
80
|
static override stateSchema = TelegramStateSchema
|
|
81
81
|
static override optionsSchema = TelegramOptionsSchema
|
|
82
82
|
static override eventsSchema = TelegramEventsSchema
|
|
83
|
+
static { Feature.register(this, 'telegram') }
|
|
83
84
|
|
|
84
85
|
private _bot?: Bot
|
|
85
86
|
|
|
@@ -339,4 +340,4 @@ export class Telegram extends Feature<TelegramState, TelegramOptions> {
|
|
|
339
340
|
|
|
340
341
|
}
|
|
341
342
|
|
|
342
|
-
export default
|
|
343
|
+
export default Telegram
|
package/src/node/features/tts.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
3
|
-
import { Feature
|
|
3
|
+
import { Feature } from '../feature.js'
|
|
4
4
|
import { createHash } from 'crypto'
|
|
5
5
|
|
|
6
6
|
const CHATTERBOX_ENDPOINT = 'https://api.runpod.ai/v2/chatterbox-turbo/runsync'
|
|
@@ -61,6 +61,7 @@ export class TTS extends Feature<TTSState, TTSOptions> {
|
|
|
61
61
|
static override stateSchema = TTSStateSchema
|
|
62
62
|
static override optionsSchema = TTSOptionsSchema
|
|
63
63
|
static override eventsSchema = TTSEventsSchema
|
|
64
|
+
static { Feature.register(this, 'tts') }
|
|
64
65
|
|
|
65
66
|
/** RunPod API key from options or environment. */
|
|
66
67
|
get apiKey(): string {
|
|
@@ -181,4 +182,4 @@ export class TTS extends Feature<TTSState, TTSOptions> {
|
|
|
181
182
|
}
|
|
182
183
|
}
|
|
183
184
|
|
|
184
|
-
export default
|
|
185
|
+
export default TTS
|
package/src/node/features/ui.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { FeatureStateSchema } from '../../schemas/base.js'
|
|
3
|
-
import {
|
|
3
|
+
import { Feature } from "../feature.js";
|
|
4
4
|
import colors from "chalk";
|
|
5
5
|
import type { Fonts } from "figlet";
|
|
6
6
|
|
|
@@ -119,6 +119,7 @@ type ColoredPrintFunction = PrintFunction & {
|
|
|
119
119
|
* ```
|
|
120
120
|
*/
|
|
121
121
|
export class UI<T extends UIState = UIState> extends Feature<T> {
|
|
122
|
+
static { Feature.register(this, 'ui') }
|
|
122
123
|
/** The shortcut path for accessing this feature */
|
|
123
124
|
static override shortcut = "features.ui" as const
|
|
124
125
|
static override stateSchema = UIStateSchema
|
|
@@ -819,8 +820,7 @@ export class UI<T extends UIState = UIState> extends Feature<T> {
|
|
|
819
820
|
}
|
|
820
821
|
}
|
|
821
822
|
|
|
822
|
-
export default
|
|
823
|
-
|
|
823
|
+
export default UI
|
|
824
824
|
/**
|
|
825
825
|
* Predefined color palette for automatic color assignment.
|
|
826
826
|
*
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
|
|
3
3
|
import crypto from 'node:crypto'
|
|
4
|
-
import { Feature
|
|
5
|
-
import { type NodeContainer } from '../container.js'
|
|
4
|
+
import { Feature } from '../feature.js'
|
|
6
5
|
import { type ContainerContext } from '../../container.js'
|
|
7
6
|
|
|
8
7
|
export const VaultStateSchema = FeatureStateSchema.extend({
|
|
@@ -43,17 +42,8 @@ export class Vault extends Feature<VaultState, VaultOptions> {
|
|
|
43
42
|
static override shortcut = 'features.vault' as const
|
|
44
43
|
static override stateSchema = VaultStateSchema
|
|
45
44
|
static override optionsSchema = VaultOptionsSchema
|
|
45
|
+
static { Feature.register(this, 'vault') }
|
|
46
46
|
|
|
47
|
-
/**
|
|
48
|
-
* Attach hook for the Vault feature. Currently a no-op placeholder
|
|
49
|
-
* for future container-level initialization logic.
|
|
50
|
-
*
|
|
51
|
-
* @param c - The node container instance
|
|
52
|
-
*/
|
|
53
|
-
static attach(c: NodeContainer) {
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
47
|
constructor(options: VaultOptions, context: ContainerContext) {
|
|
58
48
|
let secret = options.secret
|
|
59
49
|
|
|
@@ -136,8 +126,7 @@ export class Vault extends Feature<VaultState, VaultOptions> {
|
|
|
136
126
|
}
|
|
137
127
|
}
|
|
138
128
|
|
|
139
|
-
export default
|
|
140
|
-
|
|
129
|
+
export default Vault
|
|
141
130
|
function generateSecretKey(): Buffer {
|
|
142
131
|
return crypto.randomBytes(32);
|
|
143
132
|
}
|
package/src/node/features/vm.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from 'zod'
|
|
|
2
2
|
import { createRequire } from 'module'
|
|
3
3
|
import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
|
|
4
4
|
import vm from 'vm'
|
|
5
|
-
import { Feature
|
|
5
|
+
import { Feature } from "../feature.js";
|
|
6
6
|
|
|
7
7
|
export const VMStateSchema = FeatureStateSchema.extend({})
|
|
8
8
|
export type VMState = z.infer<typeof VMStateSchema>
|
|
@@ -45,6 +45,7 @@ export class VM<
|
|
|
45
45
|
static override shortcut = "features.vm" as const
|
|
46
46
|
static override stateSchema = VMStateSchema
|
|
47
47
|
static override optionsSchema = VMOptionsSchema
|
|
48
|
+
static { Feature.register(this, 'vm') }
|
|
48
49
|
|
|
49
50
|
/** Map of virtual module IDs to their exports, consulted before Node's native require */
|
|
50
51
|
modules: Map<string, any> = new Map()
|
|
@@ -193,8 +194,45 @@ export class VM<
|
|
|
193
194
|
* }
|
|
194
195
|
* ```
|
|
195
196
|
*/
|
|
197
|
+
/**
|
|
198
|
+
* Wrap code containing top-level `await` in an async IIFE, injecting
|
|
199
|
+
* `return` before the last expression so the value is not lost.
|
|
200
|
+
*
|
|
201
|
+
* If the code does not contain `await`, or is already wrapped in an
|
|
202
|
+
* async function/arrow, it is returned unchanged.
|
|
203
|
+
*/
|
|
204
|
+
wrapTopLevelAwait(code: string): string {
|
|
205
|
+
if (!/\bawait\b/.test(code) || /^\s*\(?\s*async\b/.test(code)) {
|
|
206
|
+
return code
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const lines = code.split('\n')
|
|
210
|
+
|
|
211
|
+
// Find the last non-empty line
|
|
212
|
+
let lastIdx = lines.length - 1
|
|
213
|
+
while (lastIdx > 0 && !lines[lastIdx].trim()) lastIdx--
|
|
214
|
+
|
|
215
|
+
let lastLine = lines[lastIdx]!
|
|
216
|
+
|
|
217
|
+
// For single-line code with semicolons (e.g. CLI eval), split the last line
|
|
218
|
+
// into statements and only try to return the final statement.
|
|
219
|
+
const stmts = lastLine.split(';').map(s => s.trim()).filter(Boolean)
|
|
220
|
+
if (stmts.length > 1) {
|
|
221
|
+
const finalStmt = stmts[stmts.length - 1]!
|
|
222
|
+
if (!/^\s*(var|let|const|if|for|while|switch|try|throw|class|function|return)\b/.test(finalStmt)) {
|
|
223
|
+
stmts[stmts.length - 1] = `return ${finalStmt}`
|
|
224
|
+
}
|
|
225
|
+
lines[lastIdx] = stmts.join('; ')
|
|
226
|
+
} else if (!/^\s*(var|let|const|if|for|while|switch|try|throw|class|function|return)\b/.test(lastLine)) {
|
|
227
|
+
lines[lastIdx] = `return ${lastLine}`
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return `(async () => {\n${lines.join('\n')}\n})()`
|
|
231
|
+
}
|
|
232
|
+
|
|
196
233
|
async run<T extends any>(code: string, ctx: any = {}): Promise<T> {
|
|
197
|
-
const
|
|
234
|
+
const wrapped = this.wrapTopLevelAwait(code)
|
|
235
|
+
const script = this.createScript(wrapped)
|
|
198
236
|
const context = this.isContext(ctx) ? ctx : this.createContext(ctx)
|
|
199
237
|
|
|
200
238
|
return (await script.runInContext(context)) as T
|
|
@@ -309,4 +347,4 @@ export class VM<
|
|
|
309
347
|
}
|
|
310
348
|
}
|
|
311
349
|
|
|
312
|
-
export default
|
|
350
|
+
export default VM
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
3
|
-
import { Feature
|
|
3
|
+
import { Feature } from '../feature.js'
|
|
4
|
+
import { Bus } from '../../bus.js'
|
|
4
5
|
import { Server as NetServer, Socket } from 'net'
|
|
5
6
|
import { homedir } from 'os'
|
|
6
7
|
import { join, dirname } from 'path'
|
|
@@ -154,17 +155,76 @@ export interface WindowAckResult {
|
|
|
154
155
|
[key: string]: any
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
// --- Layout Types ---
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* A single entry in a layout configuration.
|
|
162
|
+
* Use `type: 'tty'` for terminal windows, `type: 'window'` (or omit) for browser windows.
|
|
163
|
+
* If `type` is omitted, entries with a `command` field are treated as TTY, otherwise as window.
|
|
164
|
+
*/
|
|
165
|
+
export type LayoutEntry =
|
|
166
|
+
| ({ type: 'window' } & SpawnOptions)
|
|
167
|
+
| ({ type: 'tty' } & SpawnTTYOptions)
|
|
168
|
+
| ({ type?: undefined } & SpawnOptions)
|
|
169
|
+
| ({ type?: undefined; command: string } & SpawnTTYOptions)
|
|
170
|
+
|
|
171
|
+
// --- WindowHandle Events ---
|
|
172
|
+
|
|
173
|
+
interface WindowHandleEvents {
|
|
174
|
+
close: [msg: any]
|
|
175
|
+
terminalExited: [msg: any]
|
|
176
|
+
}
|
|
177
|
+
|
|
157
178
|
// --- WindowHandle ---
|
|
158
179
|
|
|
159
180
|
/**
|
|
160
181
|
* A lightweight handle to a single native window.
|
|
161
182
|
* Delegates all operations back to the WindowManager instance.
|
|
183
|
+
* Emits lifecycle events (`close`, `terminalExited`) when the native app reports them.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```typescript
|
|
187
|
+
* const handle = await windowManager.spawn({ url: 'https://example.com' })
|
|
188
|
+
* handle.on('close', (msg) => console.log('window closed', msg))
|
|
189
|
+
* handle.on('terminalExited', (info) => console.log('process exited', info))
|
|
190
|
+
* ```
|
|
162
191
|
*/
|
|
163
192
|
export class WindowHandle {
|
|
193
|
+
private _events = new Bus<WindowHandleEvents>()
|
|
194
|
+
|
|
195
|
+
/** The original ack result from spawning this window. */
|
|
196
|
+
public result: WindowAckResult
|
|
197
|
+
|
|
164
198
|
constructor(
|
|
165
199
|
public readonly windowId: string,
|
|
166
|
-
private manager: WindowManager
|
|
167
|
-
|
|
200
|
+
private manager: WindowManager,
|
|
201
|
+
result?: WindowAckResult
|
|
202
|
+
) {
|
|
203
|
+
this.result = result ?? {}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Register a listener for a lifecycle event. */
|
|
207
|
+
on<E extends keyof WindowHandleEvents>(event: E, listener: (...args: WindowHandleEvents[E]) => void): this {
|
|
208
|
+
this._events.on(event, listener)
|
|
209
|
+
return this
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Remove a listener for a lifecycle event. */
|
|
213
|
+
off<E extends keyof WindowHandleEvents>(event: E, listener?: (...args: WindowHandleEvents[E]) => void): this {
|
|
214
|
+
this._events.off(event, listener)
|
|
215
|
+
return this
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Register a one-time listener for a lifecycle event. */
|
|
219
|
+
once<E extends keyof WindowHandleEvents>(event: E, listener: (...args: WindowHandleEvents[E]) => void): this {
|
|
220
|
+
this._events.once(event, listener)
|
|
221
|
+
return this
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Emit a lifecycle event on this handle. */
|
|
225
|
+
emit<E extends keyof WindowHandleEvents>(event: E, ...args: WindowHandleEvents[E]): void {
|
|
226
|
+
this._events.emit(event, ...args)
|
|
227
|
+
}
|
|
168
228
|
|
|
169
229
|
/** Bring this window to the front. */
|
|
170
230
|
async focus(): Promise<WindowAckResult> {
|
|
@@ -234,8 +294,8 @@ interface ClientConnection {
|
|
|
234
294
|
* ```typescript
|
|
235
295
|
* const wm = container.feature('windowManager', { enable: true, autoListen: true })
|
|
236
296
|
*
|
|
237
|
-
* const
|
|
238
|
-
*
|
|
297
|
+
* const handle = await wm.spawn({ url: 'https://google.com', width: 800, height: 600 })
|
|
298
|
+
* handle.on('close', (msg) => console.log('window closed'))
|
|
239
299
|
* await handle.navigate('https://news.ycombinator.com')
|
|
240
300
|
* const title = await handle.eval('document.title')
|
|
241
301
|
* await handle.close()
|
|
@@ -252,11 +312,13 @@ export class WindowManager extends Feature<WindowManagerState, WindowManagerOpti
|
|
|
252
312
|
static override stateSchema = WindowManagerStateSchema
|
|
253
313
|
static override optionsSchema = WindowManagerOptionsSchema
|
|
254
314
|
static override eventsSchema = WindowManagerEventsSchema
|
|
315
|
+
static { Feature.register(this, 'windowManager') }
|
|
255
316
|
|
|
256
317
|
private _server?: NetServer
|
|
257
318
|
private _client?: ClientConnection
|
|
258
319
|
private _pending = new Map<string, PendingRequest>()
|
|
259
320
|
private _trackedWindows = new Set<string>()
|
|
321
|
+
private _handles = new Map<string, WindowHandle>()
|
|
260
322
|
|
|
261
323
|
private normalizeRequestId(value: unknown): string | undefined {
|
|
262
324
|
if (typeof value !== 'string') return undefined
|
|
@@ -405,6 +467,7 @@ export class WindowManager extends Feature<WindowManagerState, WindowManagerOpti
|
|
|
405
467
|
}
|
|
406
468
|
this._pending.clear()
|
|
407
469
|
this._trackedWindows.clear()
|
|
470
|
+
this._handles.clear()
|
|
408
471
|
|
|
409
472
|
if (this._client) {
|
|
410
473
|
this._client.socket.destroy()
|
|
@@ -436,26 +499,29 @@ export class WindowManager extends Feature<WindowManagerState, WindowManagerOpti
|
|
|
436
499
|
* Sends a window dispatch to the app and waits for the ack.
|
|
437
500
|
*
|
|
438
501
|
* @param opts - Window configuration (url, dimensions, chrome options)
|
|
439
|
-
* @returns
|
|
502
|
+
* @returns A WindowHandle for the spawned window (with `.result` containing the ack data)
|
|
440
503
|
*/
|
|
441
|
-
async spawn(opts: SpawnOptions = {}): Promise<
|
|
504
|
+
async spawn(opts: SpawnOptions = {}): Promise<WindowHandle> {
|
|
442
505
|
const { window: windowChrome, ...flat } = opts
|
|
443
506
|
|
|
507
|
+
let ackResult: WindowAckResult
|
|
444
508
|
if (windowChrome) {
|
|
445
|
-
|
|
509
|
+
ackResult = await this.sendWindowCommand({
|
|
446
510
|
action: 'open',
|
|
447
511
|
request: {
|
|
448
512
|
...flat,
|
|
449
513
|
window: windowChrome,
|
|
450
514
|
},
|
|
451
515
|
})
|
|
516
|
+
} else {
|
|
517
|
+
ackResult = await this.sendWindowCommand({
|
|
518
|
+
action: 'open',
|
|
519
|
+
...flat,
|
|
520
|
+
alwaysOnTop: flat.alwaysOnTop ?? false,
|
|
521
|
+
})
|
|
452
522
|
}
|
|
453
523
|
|
|
454
|
-
return this.
|
|
455
|
-
action: 'open',
|
|
456
|
-
...flat,
|
|
457
|
-
alwaysOnTop: flat.alwaysOnTop ?? false,
|
|
458
|
-
})
|
|
524
|
+
return this.getOrCreateHandle(ackResult.windowId, ackResult)
|
|
459
525
|
}
|
|
460
526
|
|
|
461
527
|
/**
|
|
@@ -464,23 +530,26 @@ export class WindowManager extends Feature<WindowManagerState, WindowManagerOpti
|
|
|
464
530
|
* Closing the window terminates the process.
|
|
465
531
|
*
|
|
466
532
|
* @param opts - Terminal configuration (command, args, cwd, dimensions, etc.)
|
|
467
|
-
* @returns
|
|
533
|
+
* @returns A WindowHandle for the spawned terminal (with `.result` containing the ack data)
|
|
468
534
|
*/
|
|
469
|
-
async spawnTTY(opts: SpawnTTYOptions): Promise<
|
|
535
|
+
async spawnTTY(opts: SpawnTTYOptions): Promise<WindowHandle> {
|
|
470
536
|
const { window: windowChrome, ...flat } = opts
|
|
471
537
|
|
|
538
|
+
let ackResult: WindowAckResult
|
|
472
539
|
if (windowChrome) {
|
|
473
|
-
|
|
540
|
+
ackResult = await this.sendWindowCommand({
|
|
474
541
|
action: 'terminal',
|
|
475
542
|
...flat,
|
|
476
543
|
window: windowChrome,
|
|
477
544
|
})
|
|
545
|
+
} else {
|
|
546
|
+
ackResult = await this.sendWindowCommand({
|
|
547
|
+
action: 'terminal',
|
|
548
|
+
...flat,
|
|
549
|
+
})
|
|
478
550
|
}
|
|
479
551
|
|
|
480
|
-
return this.
|
|
481
|
-
action: 'terminal',
|
|
482
|
-
...flat,
|
|
483
|
-
})
|
|
552
|
+
return this.getOrCreateHandle(ackResult.windowId, ackResult)
|
|
484
553
|
}
|
|
485
554
|
|
|
486
555
|
/**
|
|
@@ -573,12 +642,78 @@ export class WindowManager extends Feature<WindowManagerState, WindowManagerOpti
|
|
|
573
642
|
|
|
574
643
|
/**
|
|
575
644
|
* Get a WindowHandle for chainable operations on a specific window.
|
|
645
|
+
* Returns the tracked handle if one exists, otherwise creates a new one.
|
|
576
646
|
*
|
|
577
647
|
* @param windowId - The window ID
|
|
578
648
|
* @returns A WindowHandle instance
|
|
579
649
|
*/
|
|
580
650
|
window(windowId: string): WindowHandle {
|
|
581
|
-
return
|
|
651
|
+
return this.getOrCreateHandle(windowId)
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Spawn multiple windows in parallel from a layout configuration.
|
|
656
|
+
* Returns handles in the same order as the config entries.
|
|
657
|
+
*
|
|
658
|
+
* @param config - Array of layout entries (window or tty)
|
|
659
|
+
* @returns Array of WindowHandle instances
|
|
660
|
+
*
|
|
661
|
+
* @example
|
|
662
|
+
* ```typescript
|
|
663
|
+
* const handles = await wm.spawnLayout([
|
|
664
|
+
* { type: 'window', url: 'https://google.com', width: 800, height: 600 },
|
|
665
|
+
* { type: 'tty', command: 'htop' },
|
|
666
|
+
* { url: 'https://github.com' }, // defaults to window
|
|
667
|
+
* ])
|
|
668
|
+
* ```
|
|
669
|
+
*/
|
|
670
|
+
async spawnLayout(config: LayoutEntry[]): Promise<WindowHandle[]> {
|
|
671
|
+
const promises = config.map(entry => {
|
|
672
|
+
if (entry.type === 'tty' || ('command' in entry && !entry.type)) {
|
|
673
|
+
const { type, ...opts } = entry as { type?: string } & SpawnTTYOptions
|
|
674
|
+
return this.spawnTTY(opts)
|
|
675
|
+
} else {
|
|
676
|
+
const { type, ...opts } = entry as { type?: string } & SpawnOptions
|
|
677
|
+
return this.spawn(opts)
|
|
678
|
+
}
|
|
679
|
+
})
|
|
680
|
+
return Promise.all(promises)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Spawn multiple layouts sequentially. Each layout's windows spawn in parallel,
|
|
685
|
+
* but the next layout waits for the previous one to fully complete.
|
|
686
|
+
*
|
|
687
|
+
* @param configs - Array of layout configurations
|
|
688
|
+
* @returns Array of handle arrays, one per layout
|
|
689
|
+
*
|
|
690
|
+
* @example
|
|
691
|
+
* ```typescript
|
|
692
|
+
* const [firstBatch, secondBatch] = await wm.spawnLayouts([
|
|
693
|
+
* [{ url: 'https://google.com' }, { url: 'https://github.com' }],
|
|
694
|
+
* [{ type: 'tty', command: 'htop' }],
|
|
695
|
+
* ])
|
|
696
|
+
* ```
|
|
697
|
+
*/
|
|
698
|
+
async spawnLayouts(configs: LayoutEntry[][]): Promise<WindowHandle[][]> {
|
|
699
|
+
const results: WindowHandle[][] = []
|
|
700
|
+
for (const config of configs) {
|
|
701
|
+
results.push(await this.spawnLayout(config))
|
|
702
|
+
}
|
|
703
|
+
return results
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/** Get or create a tracked WindowHandle for a given windowId. */
|
|
707
|
+
private getOrCreateHandle(windowId: string | undefined, result?: WindowAckResult): WindowHandle {
|
|
708
|
+
const id = windowId || randomUUID()
|
|
709
|
+
let handle = this._handles.get(id)
|
|
710
|
+
if (!handle) {
|
|
711
|
+
handle = new WindowHandle(id, this, result)
|
|
712
|
+
this._handles.set(id, handle)
|
|
713
|
+
} else if (result) {
|
|
714
|
+
handle.result = result
|
|
715
|
+
}
|
|
716
|
+
return handle
|
|
582
717
|
}
|
|
583
718
|
|
|
584
719
|
// --- Private internals ---
|
|
@@ -612,6 +747,7 @@ export class WindowManager extends Feature<WindowManagerState, WindowManagerOpti
|
|
|
612
747
|
if (this._client === client) {
|
|
613
748
|
this._client = undefined
|
|
614
749
|
this._trackedWindows.clear()
|
|
750
|
+
this._handles.clear()
|
|
615
751
|
this.setState({ clientConnected: false, windowCount: 0 })
|
|
616
752
|
|
|
617
753
|
// Resolve all pending requests — the app is gone, no acks coming
|
|
@@ -664,10 +800,17 @@ export class WindowManager extends Feature<WindowManagerState, WindowManagerOpti
|
|
|
664
800
|
|
|
665
801
|
if (msg.type === 'windowClosed') {
|
|
666
802
|
this.trackWindowClosed(msg.windowId)
|
|
803
|
+
const handle = this._handles.get(msg.windowId)
|
|
804
|
+
if (handle) {
|
|
805
|
+
handle.emit('close', msg)
|
|
806
|
+
this._handles.delete(msg.windowId)
|
|
807
|
+
}
|
|
667
808
|
this.emit('windowClosed', msg)
|
|
668
809
|
}
|
|
669
810
|
|
|
670
811
|
if (msg.type === 'terminalExited') {
|
|
812
|
+
const handle = msg.windowId ? this._handles.get(msg.windowId) : undefined
|
|
813
|
+
if (handle) handle.emit('terminalExited', msg)
|
|
671
814
|
this.emit('terminalExited', msg)
|
|
672
815
|
}
|
|
673
816
|
|
|
@@ -801,4 +944,4 @@ export class WindowManager extends Feature<WindowManagerState, WindowManagerOpti
|
|
|
801
944
|
}
|
|
802
945
|
}
|
|
803
946
|
|
|
804
|
-
export default
|
|
947
|
+
export default WindowManager
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
|
|
3
|
-
import { Feature
|
|
3
|
+
import { Feature } from "../feature.js";
|
|
4
4
|
import { NodeContainer } from "../container.js";
|
|
5
5
|
import { camelCase, omit, set } from 'lodash-es'
|
|
6
6
|
|
|
@@ -36,6 +36,7 @@ export type YamlTreeState = z.infer<typeof YamlTreeStateSchema>
|
|
|
36
36
|
* @extends {Feature<T>}
|
|
37
37
|
*/
|
|
38
38
|
export class YamlTree<T extends YamlTreeState = YamlTreeState> extends Feature<T> {
|
|
39
|
+
static { Feature.register(this, 'yamlTree') }
|
|
39
40
|
/** The shortcut path for accessing this feature */
|
|
40
41
|
static override shortcut = "features.yamlTree" as const
|
|
41
42
|
static override stateSchema = YamlTreeStateSchema
|
|
@@ -49,7 +50,6 @@ export class YamlTree<T extends YamlTreeState = YamlTreeState> extends Feature<T
|
|
|
49
50
|
* @returns The container for method chaining
|
|
50
51
|
*/
|
|
51
52
|
static attach(container: NodeContainer & { yamlTree?: YamlTree }) {
|
|
52
|
-
container.features.register("yamlTree", YamlTree);
|
|
53
53
|
container.yamlTree = container.feature("yamlTree", { enable: true });
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -145,5 +145,4 @@ export class YamlTree<T extends YamlTreeState = YamlTreeState> extends Feature<T
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
export default
|
|
149
|
-
|
|
148
|
+
export default YamlTree
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as yaml from 'js-yaml'
|
|
2
|
-
import { Feature
|
|
2
|
+
import { Feature } from '../feature.js'
|
|
3
3
|
import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
|
|
4
4
|
import { NodeContainer } from '../container.js'
|
|
5
5
|
|
|
@@ -33,6 +33,7 @@ export class YAML extends Feature {
|
|
|
33
33
|
static override shortcut = 'features.yaml' as const
|
|
34
34
|
static override stateSchema = FeatureStateSchema
|
|
35
35
|
static override optionsSchema = FeatureOptionsSchema
|
|
36
|
+
static { Feature.register(this, 'yaml') }
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* Automatically attaches the YAML feature to Node containers.
|
|
@@ -129,4 +130,4 @@ export class YAML extends Feature {
|
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
132
|
|
|
132
|
-
export default
|
|
133
|
+
export default YAML
|
package/src/registry.ts
CHANGED
|
@@ -101,7 +101,7 @@ abstract class Registry<T extends Helper> {
|
|
|
101
101
|
throw new Error(
|
|
102
102
|
`${this.scope} "${id}" is not registered.${suggestion}\n\n` +
|
|
103
103
|
`To fix this, ensure the module that defines "${id}" is imported (e.g. import './${this.scope}/${id}') ` +
|
|
104
|
-
`or registered on the container (e.g. container.use(${id[0]!.toUpperCase() + id.slice(1)})).`
|
|
104
|
+
`or registered on the container (e.g. container.use(${id ? id[0]!.toUpperCase() + id.slice(1) : 'MyHelper'})).`
|
|
105
105
|
)
|
|
106
106
|
}
|
|
107
107
|
|