@soederpop/luca 0.0.6 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +10 -1
- package/bun.lock +1 -1
- package/commands/build-bootstrap.ts +78 -0
- package/commands/build-scaffolds.ts +24 -2
- package/commands/try-all-challenges.ts +543 -0
- package/commands/try-challenge.ts +100 -0
- package/docs/README.md +52 -80
- package/docs/TABLE-OF-CONTENTS.md +82 -51
- package/docs/apis/clients/elevenlabs.md +232 -8
- package/docs/apis/clients/graph.md +59 -8
- package/docs/apis/clients/openai.md +362 -2
- package/docs/apis/clients/rest.md +122 -2
- package/docs/apis/clients/websocket.md +71 -17
- package/docs/apis/features/agi/assistant.md +9 -3
- package/docs/apis/features/agi/assistants-manager.md +2 -2
- package/docs/apis/features/agi/claude-code.md +153 -14
- package/docs/apis/features/agi/conversation-history.md +15 -3
- package/docs/apis/features/agi/conversation.md +133 -20
- package/docs/apis/features/agi/openai-codex.md +90 -12
- package/docs/apis/features/agi/skills-library.md +23 -5
- package/docs/apis/features/node/container-link.md +59 -0
- package/docs/apis/features/node/content-db.md +1 -1
- package/docs/apis/features/node/disk-cache.md +1 -1
- package/docs/apis/features/node/dns.md +1 -0
- package/docs/apis/features/node/docker.md +2 -1
- package/docs/apis/features/node/esbuild.md +4 -3
- package/docs/apis/features/node/file-manager.md +13 -4
- package/docs/apis/features/node/fs.md +726 -171
- package/docs/apis/features/node/git.md +1 -0
- package/docs/apis/features/node/google-auth.md +23 -4
- package/docs/apis/features/node/google-calendar.md +14 -2
- package/docs/apis/features/node/google-docs.md +15 -2
- package/docs/apis/features/node/google-drive.md +21 -3
- package/docs/apis/features/node/google-sheets.md +14 -2
- package/docs/apis/features/node/grep.md +2 -0
- package/docs/apis/features/node/helpers.md +29 -0
- package/docs/apis/features/node/ink.md +2 -2
- package/docs/apis/features/node/networking.md +39 -4
- package/docs/apis/features/node/os.md +28 -0
- package/docs/apis/features/node/postgres.md +26 -4
- package/docs/apis/features/node/proc.md +37 -28
- package/docs/apis/features/node/process-manager.md +33 -5
- package/docs/apis/features/node/repl.md +1 -1
- package/docs/apis/features/node/runpod.md +1 -0
- package/docs/apis/features/node/secure-shell.md +7 -0
- package/docs/apis/features/node/semantic-search.md +12 -5
- package/docs/apis/features/node/sqlite.md +26 -4
- package/docs/apis/features/node/telegram.md +30 -5
- package/docs/apis/features/node/tts.md +17 -2
- package/docs/apis/features/node/ui.md +1 -1
- package/docs/apis/features/node/vault.md +4 -9
- package/docs/apis/features/node/vm.md +3 -12
- package/docs/apis/features/node/window-manager.md +128 -20
- package/docs/apis/features/web/asset-loader.md +13 -1
- package/docs/apis/features/web/container-link.md +59 -0
- package/docs/apis/features/web/esbuild.md +4 -3
- package/docs/apis/features/web/helpers.md +29 -0
- package/docs/apis/features/web/network.md +16 -2
- package/docs/apis/features/web/speech.md +16 -2
- package/docs/apis/features/web/vault.md +4 -9
- package/docs/apis/features/web/vm.md +3 -12
- package/docs/apis/features/web/voice.md +18 -1
- package/docs/apis/servers/express.md +18 -2
- package/docs/apis/servers/mcp.md +29 -4
- package/docs/apis/servers/websocket.md +34 -6
- package/docs/bootstrap/CLAUDE.md +100 -0
- package/docs/bootstrap/SKILL.md +222 -0
- package/docs/bootstrap/templates/about-command.ts +41 -0
- package/docs/bootstrap/templates/docs-models.ts +22 -0
- package/docs/bootstrap/templates/docs-readme.md +43 -0
- package/docs/bootstrap/templates/example-feature.ts +53 -0
- package/docs/bootstrap/templates/health-endpoint.ts +15 -0
- package/docs/bootstrap/templates/luca-cli.ts +25 -0
- package/docs/challenges/caching-proxy.md +16 -0
- package/docs/challenges/content-db-round-trip.md +14 -0
- package/docs/challenges/custom-command.md +9 -0
- package/docs/challenges/file-watcher-pipeline.md +11 -0
- package/docs/challenges/grep-audit-report.md +15 -0
- package/docs/challenges/multi-feature-dashboard.md +14 -0
- package/docs/challenges/process-orchestrator.md +17 -0
- package/docs/challenges/rest-api-server-with-client.md +12 -0
- package/docs/challenges/script-runner-with-vm.md +11 -0
- package/docs/challenges/simple-rest-api.md +15 -0
- package/docs/challenges/websocket-serve-and-client.md +11 -0
- package/docs/challenges/yaml-config-system.md +14 -0
- package/docs/command-system-overhaul.md +94 -0
- package/docs/examples/assistant/CORE.md +18 -0
- package/docs/examples/assistant/hooks.ts +3 -0
- package/docs/examples/assistant/tools.ts +10 -0
- package/docs/examples/window-manager-layouts.md +180 -0
- package/docs/in-memory-fs.md +4 -0
- package/docs/models.ts +13 -10
- package/docs/philosophy.md +4 -3
- package/docs/reports/console-hmr-design.md +170 -0
- package/docs/reports/helper-semantic-search.md +72 -0
- package/docs/scaffolds/client.md +29 -20
- package/docs/scaffolds/command.md +64 -50
- package/docs/scaffolds/endpoint.md +31 -36
- package/docs/scaffolds/feature.md +28 -18
- package/docs/scaffolds/selector.md +91 -0
- package/docs/scaffolds/server.md +18 -9
- package/docs/selectors.md +115 -0
- package/docs/sessions/custom-command/attempt-log-2.md +195 -0
- package/docs/sessions/file-watcher-pipeline/attempt-log-1.md +728 -0
- package/docs/sessions/file-watcher-pipeline/attempt-log-2.md +555 -0
- package/docs/sessions/grep-audit-report/attempt-log-1.md +289 -0
- package/docs/sessions/multi-feature-dashboard/attempt-log-2.md +679 -0
- package/docs/sessions/rest-api-server-with-client/attempt-log-1.md +1 -0
- package/docs/sessions/rest-api-server-with-client/attempt-log-3.md +920 -0
- package/docs/sessions/simple-rest-api/attempt-log-1.md +593 -0
- package/docs/sessions/websocket-serve-and-client/attempt-log-2.md +995 -0
- package/docs/tutorials/00-bootstrap.md +148 -0
- package/docs/tutorials/07-endpoints.md +7 -7
- package/docs/tutorials/08-commands.md +153 -72
- package/luca.cli.ts +3 -0
- package/package.json +6 -5
- package/public/index.html +1430 -0
- package/scripts/examples/using-ollama.ts +2 -1
- package/scripts/update-introspection-data.ts +2 -2
- package/src/agi/endpoints/experts.ts +1 -1
- package/src/agi/features/assistant.ts +7 -0
- package/src/agi/features/assistants-manager.ts +5 -5
- package/src/agi/features/claude-code.ts +263 -3
- package/src/agi/features/conversation-history.ts +7 -1
- package/src/agi/features/conversation.ts +26 -3
- package/src/agi/features/openai-codex.ts +26 -2
- package/src/agi/features/openapi.ts +6 -1
- package/src/agi/features/skills-library.ts +9 -1
- package/src/bootstrap/generated.ts +540 -0
- package/src/cli/cli.ts +64 -21
- package/src/client.ts +23 -357
- package/src/clients/civitai/index.ts +1 -1
- package/src/clients/client-template.ts +1 -1
- package/src/clients/comfyui/index.ts +13 -2
- package/src/clients/elevenlabs/index.ts +2 -1
- package/src/clients/graph.ts +87 -0
- package/src/clients/openai/index.ts +10 -1
- package/src/clients/rest.ts +207 -0
- package/src/clients/websocket.ts +176 -0
- package/src/command.ts +281 -34
- package/src/commands/bootstrap.ts +181 -0
- package/src/commands/chat.ts +5 -4
- package/src/commands/describe.ts +225 -2
- package/src/commands/help.ts +35 -9
- package/src/commands/index.ts +3 -0
- package/src/commands/introspect.ts +92 -2
- package/src/commands/prompt.ts +5 -6
- package/src/commands/run.ts +33 -10
- package/src/commands/save-api-docs.ts +49 -0
- package/src/commands/scaffold.ts +169 -23
- package/src/commands/select.ts +94 -0
- package/src/commands/serve.ts +10 -1
- package/src/container.ts +15 -0
- package/src/endpoint.ts +19 -0
- package/src/graft.ts +181 -0
- package/src/introspection/generated.agi.ts +12458 -8968
- package/src/introspection/generated.node.ts +10573 -7145
- package/src/introspection/generated.web.ts +1 -1
- package/src/introspection/index.ts +26 -0
- package/src/node/container.ts +6 -7
- package/src/node/features/content-db.ts +49 -2
- package/src/node/features/disk-cache.ts +16 -9
- package/src/node/features/dns.ts +16 -3
- package/src/node/features/docker.ts +16 -4
- package/src/node/features/esbuild.ts +20 -0
- package/src/node/features/file-manager.ts +184 -29
- package/src/node/features/fs.ts +704 -248
- package/src/node/features/git.ts +21 -8
- package/src/node/features/grep.ts +23 -3
- package/src/node/features/helpers.ts +372 -43
- package/src/node/features/networking.ts +39 -4
- package/src/node/features/opener.ts +28 -15
- package/src/node/features/os.ts +76 -0
- package/src/node/features/port-exposer.ts +11 -1
- package/src/node/features/postgres.ts +17 -1
- package/src/node/features/proc.ts +4 -1
- package/src/node/features/python.ts +63 -14
- package/src/node/features/repl.ts +11 -7
- package/src/node/features/runpod.ts +16 -3
- package/src/node/features/secure-shell.ts +27 -2
- package/src/node/features/semantic-search.ts +12 -1
- package/src/node/features/ui.ts +5 -69
- package/src/node/features/vm.ts +17 -0
- package/src/node/features/window-manager.ts +68 -20
- package/src/node.ts +5 -0
- package/src/scaffolds/generated.ts +492 -290
- package/src/scaffolds/template.ts +9 -0
- package/src/schemas/base.ts +46 -5
- package/src/selector.ts +282 -0
- package/src/server.ts +11 -0
- package/src/servers/express.ts +27 -12
- package/src/servers/socket.ts +45 -11
- package/src/web/clients/socket.ts +4 -1
- package/src/web/container.ts +2 -1
- package/src/web/features/network.ts +7 -1
- package/src/web/features/voice-recognition.ts +16 -1
- package/test/clients-servers.test.ts +2 -1
- package/test/command.test.ts +267 -0
- package/test-integration/assistants-manager.test.ts +10 -20
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/1b/b5/c75b28794f00f94c4d609a98978e9420e9b7146d204a7fbf5b0b30477292581705d207c0100dabaac27eef540aaaece3374af75104a93219d4ec8bfb44e7 +1 -0
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/da/df/1d90ce4e042abeb035a197832c6d6893420a747a056be773eb00e4f745a037d505c8db13dde7d36b36b6b893addbb7df0f5fe9f0c13e665f20056447318b +1 -0
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/ed/04/e1d0c2a58c2db29b3921ca2affb3ea4febe831c53b38ebc21019fb799823aba6ed5b4611873d2cd25d422d49955b852a9c326da0d678899bc1c2c2960901 +1 -0
- package/tmp/.cache/luca-disk-cache/index-v5/00/13/572aa4c9a94f99eda999695d050cdd0ca7fe2d23a50af03234d4c8ce0791 +2 -0
- package/tmp/.cache/luca-disk-cache/index-v5/75/a9/cb61dc0f0589e8ec10a9aca27b834bc73884c479941042d22a2b22324cd3 +2 -0
- package/tmp/.cache/luca-disk-cache/index-v5/9f/0f/8b1f915ee64cfff7667dd96acd7a5ac0a96aa91a346e19cefd45909a9c9c +2 -0
- package/docs/apis/features/node/launcher-app-command-listener.md +0 -145
- package/docs/examples/launcher-app-command-listener.md +0 -120
- package/docs/tasks/web-container-helper-discovery.md +0 -71
- package/docs/todos.md +0 -1
- package/scripts/test-command-listener.ts +0 -123
- package/src/node/features/launcher-app-command-listener.ts +0 -389
|
@@ -13,6 +13,14 @@ export function toCamelCase(str: string): string {
|
|
|
13
13
|
return pascal.charAt(0).toLowerCase() + pascal.slice(1)
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/** Convert a string to kebab-case */
|
|
17
|
+
export function toKebabCase(str: string): string {
|
|
18
|
+
return str
|
|
19
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
20
|
+
.replace(/[_\s]+/g, '-')
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
/** Apply mustache-style template variables to scaffold code */
|
|
17
25
|
export function applyTemplate(template: string, vars: Record<string, string>): string {
|
|
18
26
|
let result = template
|
|
@@ -30,6 +38,7 @@ export function generateScaffold(type: string, name: string, description?: strin
|
|
|
30
38
|
const vars = {
|
|
31
39
|
PascalName: toPascalCase(name),
|
|
32
40
|
camelName: toCamelCase(name),
|
|
41
|
+
kebabName: toKebabCase(name),
|
|
33
42
|
description: description || `A ${type} that does something useful`,
|
|
34
43
|
}
|
|
35
44
|
|
package/src/schemas/base.ts
CHANGED
|
@@ -154,14 +154,45 @@ export const CommandStateSchema = HelperStateSchema.extend({
|
|
|
154
154
|
|
|
155
155
|
export const CommandOptionsSchema = HelperOptionsSchema.extend({
|
|
156
156
|
_: z.array(z.string()).default([]).describe('Positional arguments from minimist'),
|
|
157
|
+
dispatchSource: z.enum(['cli', 'headless', 'mcp', 'rpc']).default('cli').describe('How this command was invoked — controls arg normalization and output capture'),
|
|
157
158
|
}).describe('Base command options parsed from argv')
|
|
158
159
|
|
|
160
|
+
export type DispatchSource = 'cli' | 'headless' | 'mcp' | 'rpc'
|
|
161
|
+
|
|
162
|
+
export interface CommandRunResult {
|
|
163
|
+
exitCode: number
|
|
164
|
+
stdout: string
|
|
165
|
+
stderr: string
|
|
166
|
+
}
|
|
167
|
+
|
|
159
168
|
export const CommandEventsSchema = HelperEventsSchema.extend({
|
|
160
169
|
started: z.tuple([]).describe('Emitted when command execution begins'),
|
|
161
170
|
completed: z.tuple([z.number().describe('Exit code')]).describe('Emitted when command execution finishes'),
|
|
162
171
|
failed: z.tuple([z.any().describe('The error')]).describe('Emitted when command execution fails'),
|
|
163
172
|
}).describe('Base command events')
|
|
164
173
|
|
|
174
|
+
// Selector schemas
|
|
175
|
+
export const SelectorStateSchema = HelperStateSchema.extend({
|
|
176
|
+
running: z.boolean().default(false).describe('Whether the selector is currently running'),
|
|
177
|
+
lastRanAt: z.number().optional().describe('Unix timestamp of last successful run'),
|
|
178
|
+
}).describe('Base selector state')
|
|
179
|
+
|
|
180
|
+
export const SelectorOptionsSchema = HelperOptionsSchema.extend({
|
|
181
|
+
dispatchSource: z.enum(['cli', 'headless', 'mcp', 'rpc']).default('headless').describe('How this selector was invoked'),
|
|
182
|
+
}).describe('Base selector options')
|
|
183
|
+
|
|
184
|
+
export interface SelectorRunResult<T = any> {
|
|
185
|
+
data: T
|
|
186
|
+
cached: boolean
|
|
187
|
+
cacheKey: string
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export const SelectorEventsSchema = HelperEventsSchema.extend({
|
|
191
|
+
started: z.tuple([]).describe('Emitted when selector execution begins'),
|
|
192
|
+
completed: z.tuple([z.any().describe('The result data')]).describe('Emitted when selector execution finishes'),
|
|
193
|
+
failed: z.tuple([z.any().describe('The error')]).describe('Emitted when selector execution fails'),
|
|
194
|
+
}).describe('Base selector events')
|
|
195
|
+
|
|
165
196
|
// Endpoint schemas
|
|
166
197
|
export const EndpointStateSchema = HelperStateSchema.extend({
|
|
167
198
|
mounted: z.boolean().default(false).describe('Whether the endpoint is mounted on a server'),
|
|
@@ -230,12 +261,22 @@ export function describeEventsSchema(
|
|
|
230
261
|
// The event value is a tuple schema — its items describe positional args
|
|
231
262
|
const items = eventProp?.prefixItems || eventProp?.items
|
|
232
263
|
if (Array.isArray(items)) {
|
|
233
|
-
items.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
264
|
+
if (items.length === 1 && items[0].type === 'object' && items[0].properties) {
|
|
265
|
+
// Single object payload — expand its properties as named arguments
|
|
266
|
+
for (const [propName, propSchema] of Object.entries(items[0].properties) as [string, any][]) {
|
|
267
|
+
args[propName] = {
|
|
268
|
+
type: propSchema.enum ? propSchema.enum.map((v: any) => `'${v}'`).join(' | ') : (propSchema.type || 'any'),
|
|
269
|
+
description: propSchema.description || ''
|
|
270
|
+
}
|
|
237
271
|
}
|
|
238
|
-
}
|
|
272
|
+
} else {
|
|
273
|
+
items.forEach((item: any, index: number) => {
|
|
274
|
+
args[`arg${index}`] = {
|
|
275
|
+
type: item.type || 'any',
|
|
276
|
+
description: item.description || ''
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
}
|
|
239
280
|
}
|
|
240
281
|
|
|
241
282
|
result[eventName] = {
|
package/src/selector.ts
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { Helper } from './helper.js'
|
|
2
|
+
import type { Container, ContainerContext } from './container.js'
|
|
3
|
+
import { Registry } from './registry.js'
|
|
4
|
+
import { SelectorStateSchema, SelectorOptionsSchema, SelectorEventsSchema, type SelectorRunResult } from './schemas/base.js'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
import { join } from 'path'
|
|
7
|
+
import { graftModule, isNativeHelperClass } from './graft.js'
|
|
8
|
+
|
|
9
|
+
export type { SelectorRunResult }
|
|
10
|
+
|
|
11
|
+
export type SelectorState = z.infer<typeof SelectorStateSchema>
|
|
12
|
+
export type SelectorOptions = z.infer<typeof SelectorOptionsSchema>
|
|
13
|
+
|
|
14
|
+
export interface AvailableSelectors {}
|
|
15
|
+
|
|
16
|
+
export type SelectorFactory = <T extends keyof AvailableSelectors>(
|
|
17
|
+
key: T,
|
|
18
|
+
options?: ConstructorParameters<AvailableSelectors[T]>[0]
|
|
19
|
+
) => NonNullable<InstanceType<AvailableSelectors[T]>>
|
|
20
|
+
|
|
21
|
+
export interface SelectorsInterface {
|
|
22
|
+
selectors: SelectorsRegistry
|
|
23
|
+
select: SelectorFactory
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Type helper for module-augmentation of AvailableSelectors when using the
|
|
28
|
+
* module-based pattern.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* declare module '@soederpop/luca' {
|
|
33
|
+
* interface AvailableSelectors {
|
|
34
|
+
* packageInfo: SimpleSelector<typeof argsSchema>
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export type SimpleSelector<Schema extends z.ZodType = z.ZodType> = typeof Selector & {
|
|
40
|
+
argsSchema: Schema
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A Selector is a helper that returns data. Where Commands perform actions,
|
|
45
|
+
* Selectors query and return structured results with built-in caching.
|
|
46
|
+
*
|
|
47
|
+
* Module authors export a `run(args, context)` function that returns data.
|
|
48
|
+
* The `select()` dispatch method wraps `run()` with cache check/store and
|
|
49
|
+
* returns `{ data, cached, cacheKey }`.
|
|
50
|
+
*
|
|
51
|
+
* Caching is on by default and keyed by `hashObject({ selectorName, args, gitSha })`.
|
|
52
|
+
* Export a `cacheKey(args, context)` function to customize, or set `cacheable = false`
|
|
53
|
+
* to disable.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* // selectors/package-info.ts
|
|
58
|
+
* export const description = 'Returns parsed package.json data'
|
|
59
|
+
* export const argsSchema = z.object({ field: z.string().optional() })
|
|
60
|
+
*
|
|
61
|
+
* export function cacheKey(args, context) {
|
|
62
|
+
* return context.container.git.sha
|
|
63
|
+
* }
|
|
64
|
+
*
|
|
65
|
+
* export async function run(args, context) {
|
|
66
|
+
* const manifest = context.container.manifest
|
|
67
|
+
* return args.field ? manifest[args.field] : manifest
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export class Selector<
|
|
72
|
+
T extends SelectorState = SelectorState,
|
|
73
|
+
K extends SelectorOptions = SelectorOptions
|
|
74
|
+
> extends Helper<T, K> {
|
|
75
|
+
static override shortcut = 'selectors.base'
|
|
76
|
+
static override description = 'Base selector'
|
|
77
|
+
static override stateSchema = SelectorStateSchema
|
|
78
|
+
static override optionsSchema = SelectorOptionsSchema
|
|
79
|
+
static override eventsSchema = SelectorEventsSchema
|
|
80
|
+
|
|
81
|
+
static selectorDescription: string = ''
|
|
82
|
+
static argsSchema: z.ZodType = SelectorOptionsSchema
|
|
83
|
+
static cacheable: boolean = true
|
|
84
|
+
|
|
85
|
+
/** Self-register a Selector subclass from a static initialization block. */
|
|
86
|
+
static register: (SubClass: typeof Selector, id?: string) => typeof Selector
|
|
87
|
+
|
|
88
|
+
override get initialState(): T {
|
|
89
|
+
return ({ running: false } as unknown) as T
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The user-defined selector payload. Override this in module-based selectors
|
|
94
|
+
* by exporting a `run` function.
|
|
95
|
+
*
|
|
96
|
+
* Receives validated args and the container context. Must return data.
|
|
97
|
+
*/
|
|
98
|
+
async run(_args: any, _context: ContainerContext): Promise<any> {
|
|
99
|
+
// override via grafted module export
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Compute the cache key for a given set of args.
|
|
104
|
+
* Override by exporting a `cacheKey(args, context)` function in the module.
|
|
105
|
+
*
|
|
106
|
+
* Default: hashObject({ selectorName, args, gitSha })
|
|
107
|
+
*/
|
|
108
|
+
resolveCacheKey(args: any, _context: ContainerContext): string {
|
|
109
|
+
const name = (this.constructor as typeof Selector).shortcut || 'selector'
|
|
110
|
+
const gitSha = (this.container as any).git?.currentCommitSha ?? 'unknown'
|
|
111
|
+
return (this.container as any).utils.hashObject({ selectorName: name, args, gitSha })
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The public dispatch method. Checks cache, calls run(), stores result.
|
|
116
|
+
*
|
|
117
|
+
* @returns `{ data, cached, cacheKey }` — the result and cache metadata
|
|
118
|
+
*/
|
|
119
|
+
async select(args?: Record<string, any>): Promise<SelectorRunResult> {
|
|
120
|
+
const Cls = this.constructor as typeof Selector
|
|
121
|
+
const parsed = Cls.argsSchema.parse(args ?? {})
|
|
122
|
+
const resolvedCacheKey = this.resolveCacheKey(parsed, this.context)
|
|
123
|
+
|
|
124
|
+
// Cache check
|
|
125
|
+
if (Cls.cacheable) {
|
|
126
|
+
try {
|
|
127
|
+
const cache = this._getCache()
|
|
128
|
+
if (await cache.has(resolvedCacheKey)) {
|
|
129
|
+
const data = await cache.get(resolvedCacheKey, true)
|
|
130
|
+
return { data, cached: true, cacheKey: resolvedCacheKey }
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
// Cache miss or unavailable — proceed to run
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Run the selector
|
|
138
|
+
this.state.set('running' as any, true)
|
|
139
|
+
this.emit('started' as any)
|
|
140
|
+
|
|
141
|
+
let data: any
|
|
142
|
+
try {
|
|
143
|
+
data = await this.run(parsed, this.context)
|
|
144
|
+
this.state.set('running' as any, false)
|
|
145
|
+
this.state.set('lastRanAt' as any, Date.now())
|
|
146
|
+
this.emit('completed' as any, data)
|
|
147
|
+
} catch (err: any) {
|
|
148
|
+
this.state.set('running' as any, false)
|
|
149
|
+
this.emit('failed' as any, err)
|
|
150
|
+
throw err
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Cache store
|
|
154
|
+
if (Cls.cacheable) {
|
|
155
|
+
try {
|
|
156
|
+
await this._getCache().set(resolvedCacheKey, data)
|
|
157
|
+
} catch {
|
|
158
|
+
// Cache write failure is non-fatal
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { data, cached: false, cacheKey: resolvedCacheKey }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Lazily access diskCache. */
|
|
166
|
+
private _getCache(): any {
|
|
167
|
+
return (this.container as any).feature('diskCache', { enable: true })
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
static attach(container: Container<any> & SelectorsInterface) {
|
|
171
|
+
container.selectors = selectors
|
|
172
|
+
|
|
173
|
+
Object.assign(container, {
|
|
174
|
+
select<T extends keyof AvailableSelectors>(
|
|
175
|
+
id: T,
|
|
176
|
+
options?: ConstructorParameters<AvailableSelectors[T]>[0]
|
|
177
|
+
): NonNullable<InstanceType<AvailableSelectors[T]>> {
|
|
178
|
+
const BaseClass = selectors.lookup(id as string) as any
|
|
179
|
+
|
|
180
|
+
return container.createHelperInstance({
|
|
181
|
+
cache: selectorHelperCache,
|
|
182
|
+
type: 'selector',
|
|
183
|
+
id: String(id),
|
|
184
|
+
BaseClass,
|
|
185
|
+
options,
|
|
186
|
+
fallbackName: String(id),
|
|
187
|
+
}) as NonNullable<InstanceType<AvailableSelectors[T]>>
|
|
188
|
+
},
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
container.registerHelperType('selectors', 'select')
|
|
192
|
+
return container
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export class SelectorsRegistry extends Registry<Selector<any>> {
|
|
197
|
+
override scope = 'selectors'
|
|
198
|
+
override baseClass = Selector as any
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Discover and register selectors from a directory.
|
|
202
|
+
* Detection order:
|
|
203
|
+
* 1. Default export is a class extending Selector -> register directly
|
|
204
|
+
* 2. Module exports a `run` function -> graft as SimpleSelector
|
|
205
|
+
*/
|
|
206
|
+
async discover(options: { directory: string }) {
|
|
207
|
+
const { Glob } = globalThis.Bun || (await import('bun'))
|
|
208
|
+
const glob = new Glob('*.ts')
|
|
209
|
+
|
|
210
|
+
for await (const file of glob.scan({ cwd: options.directory })) {
|
|
211
|
+
if (file === 'index.ts') continue
|
|
212
|
+
|
|
213
|
+
const name = file.replace(/\.ts$/, '')
|
|
214
|
+
if (this.has(name)) continue
|
|
215
|
+
|
|
216
|
+
const mod = await import(join(options.directory, file))
|
|
217
|
+
|
|
218
|
+
// 1. Class-based: default export extends Selector
|
|
219
|
+
if (isNativeHelperClass(mod.default, Selector)) {
|
|
220
|
+
const ExportedClass = mod.default
|
|
221
|
+
if (!ExportedClass.shortcut || ExportedClass.shortcut === 'selectors.base') {
|
|
222
|
+
ExportedClass.shortcut = `selectors.${name}`
|
|
223
|
+
}
|
|
224
|
+
this.register(name, ExportedClass)
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const selectorModule = mod.default || mod
|
|
229
|
+
|
|
230
|
+
// 2. Module-based with `run` export
|
|
231
|
+
if (typeof selectorModule.run === 'function') {
|
|
232
|
+
const Grafted = graftModule(Selector as any, selectorModule, name, 'selectors')
|
|
233
|
+
this.register(name, Grafted as any)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export const selectors = new SelectorsRegistry()
|
|
240
|
+
export const selectorHelperCache = new Map()
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Self-register a Selector subclass from a static initialization block.
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```typescript
|
|
247
|
+
* export class PackageInfoSelector extends Selector {
|
|
248
|
+
* static override description = 'Returns parsed package.json data'
|
|
249
|
+
* static { Selector.register(this, 'packageInfo') }
|
|
250
|
+
*
|
|
251
|
+
* override async run(args, context) { return context.container.manifest }
|
|
252
|
+
* }
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
Selector.register = function registerSelector(
|
|
256
|
+
SubClass: typeof Selector,
|
|
257
|
+
id?: string,
|
|
258
|
+
) {
|
|
259
|
+
const registryId = id ?? (SubClass.name
|
|
260
|
+
? SubClass.name[0]!.toLowerCase() + SubClass.name.slice(1).replace(/Selector$/, '')
|
|
261
|
+
: `selector_${Date.now()}`)
|
|
262
|
+
|
|
263
|
+
if (!Object.getOwnPropertyDescriptor(SubClass, 'shortcut')?.value ||
|
|
264
|
+
(SubClass as any).shortcut === 'selectors.base') {
|
|
265
|
+
;(SubClass as any).shortcut = `selectors.${registryId}`
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
selectors.register(registryId, SubClass as any)
|
|
269
|
+
|
|
270
|
+
if (!Object.getOwnPropertyDescriptor(SubClass, 'attach')) {
|
|
271
|
+
;(SubClass as any).attach = (container: any) => {
|
|
272
|
+
selectors.register(registryId, SubClass as any)
|
|
273
|
+
return container
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return SubClass
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export { graftModule, isNativeHelperClass } from './graft.js'
|
|
281
|
+
|
|
282
|
+
export default Selector
|
package/src/server.ts
CHANGED
|
@@ -91,6 +91,7 @@ export class Server<T extends ServerState = ServerState, K extends ServerOptions
|
|
|
91
91
|
return !!this.state.get('stopped')
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/** The port this server will bind to. Reads from state first (set by start() or configure()), then constructor options, then defaults to 3000. */
|
|
94
95
|
get port() {
|
|
95
96
|
return this.state.get('port') || this.options.port || 3000
|
|
96
97
|
}
|
|
@@ -105,11 +106,21 @@ export class Server<T extends ServerState = ServerState, K extends ServerOptions
|
|
|
105
106
|
return this
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Start the server. Runtime options override constructor options and update state
|
|
111
|
+
* so that `server.port` always reflects the actual listening port.
|
|
112
|
+
*
|
|
113
|
+
* @param options - Optional runtime overrides for port and host
|
|
114
|
+
*/
|
|
108
115
|
async start(options?: StartOptions) {
|
|
109
116
|
if(this.isListening) {
|
|
110
117
|
return this
|
|
111
118
|
}
|
|
112
119
|
|
|
120
|
+
if (options?.port) {
|
|
121
|
+
this.state.set('port', options.port)
|
|
122
|
+
}
|
|
123
|
+
|
|
113
124
|
this.state.set('listening', true)
|
|
114
125
|
|
|
115
126
|
return this
|
package/src/servers/express.ts
CHANGED
|
@@ -4,7 +4,7 @@ import cors from 'cors'
|
|
|
4
4
|
import { z } from 'zod'
|
|
5
5
|
import { ServerStateSchema, ServerOptionsSchema } from '../schemas/base.js'
|
|
6
6
|
import { type StartOptions, Server, type ServerState } from '../server.js'
|
|
7
|
-
import { Endpoint, type EndpointModule } from '../endpoint.js'
|
|
7
|
+
import { Endpoint, type EndpointModule, warnUnknownExports } from '../endpoint.js'
|
|
8
8
|
|
|
9
9
|
declare module '../server' {
|
|
10
10
|
interface AvailableServers {
|
|
@@ -88,26 +88,34 @@ export class ExpressServer<T extends ServerState = ServerState, K extends Expres
|
|
|
88
88
|
|
|
89
89
|
// @ts-ignore-next-line
|
|
90
90
|
const server : Server = this
|
|
91
|
-
this.hooks.create(app, server)
|
|
92
|
-
|
|
93
91
|
this._app = this.hooks.create(app, server) || app
|
|
94
92
|
|
|
95
93
|
return app
|
|
96
94
|
}
|
|
97
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Start the Express HTTP server. A runtime `port` overrides the constructor
|
|
98
|
+
* option and is written to state so `server.port` always reflects reality.
|
|
99
|
+
*
|
|
100
|
+
* @param options - Optional runtime overrides for port and host
|
|
101
|
+
*/
|
|
98
102
|
override async start(options?: StartOptions) {
|
|
99
103
|
if (this.isListening) {
|
|
100
104
|
return this
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
// Apply runtime port to state so this.port reflects the override
|
|
108
|
+
if (options?.port) {
|
|
109
|
+
this.state.set('port', options.port)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const startOptions = {
|
|
113
|
+
port: this.port,
|
|
114
|
+
host: options?.host || this.options.host || '0.0.0.0',
|
|
107
115
|
}
|
|
108
116
|
|
|
109
117
|
// @ts-ignore-next-line
|
|
110
|
-
await this.hooks.beforeStart(
|
|
118
|
+
await this.hooks.beforeStart(startOptions, this)
|
|
111
119
|
|
|
112
120
|
// SPA history fallback: serve index.html for unmatched GET routes
|
|
113
121
|
if (this.options.historyFallback && this.options.static) {
|
|
@@ -118,12 +126,12 @@ export class ExpressServer<T extends ServerState = ServerState, K extends Expres
|
|
|
118
126
|
}
|
|
119
127
|
|
|
120
128
|
await new Promise((res) => {
|
|
121
|
-
this._listener = this.app.listen(
|
|
129
|
+
this._listener = this.app.listen(startOptions.port, startOptions.host, () => {
|
|
122
130
|
this.state.set('listening', true)
|
|
123
131
|
res(null)
|
|
124
132
|
})
|
|
125
133
|
})
|
|
126
|
-
|
|
134
|
+
|
|
127
135
|
return this
|
|
128
136
|
}
|
|
129
137
|
|
|
@@ -169,17 +177,24 @@ export class ExpressServer<T extends ServerState = ServerState, K extends Expres
|
|
|
169
177
|
}
|
|
170
178
|
|
|
171
179
|
async useEndpoints(dir: string): Promise<this> {
|
|
172
|
-
const
|
|
180
|
+
const { Glob } = globalThis.Bun || (await import('bun'))
|
|
181
|
+
const glob = new Glob('**/*.ts')
|
|
182
|
+
|
|
183
|
+
// Use the helpers feature's VM-aware loader so endpoints can resolve
|
|
184
|
+
// packages like zod and @soederpop/luca even from the compiled binary
|
|
185
|
+
const helpers = this.container.feature('helpers') as any
|
|
173
186
|
|
|
174
187
|
for await (const file of glob.scan({ cwd: dir, absolute: true })) {
|
|
175
188
|
try {
|
|
176
|
-
const mod = await
|
|
189
|
+
const mod = await helpers.loadModuleExports(file)
|
|
177
190
|
const endpointModule: EndpointModule = mod.default || mod
|
|
178
191
|
|
|
179
192
|
if (!endpointModule.path) {
|
|
180
193
|
continue
|
|
181
194
|
}
|
|
182
195
|
|
|
196
|
+
warnUnknownExports(mod, file)
|
|
197
|
+
|
|
183
198
|
const endpoint = new Endpoint(
|
|
184
199
|
{ path: endpointModule.path, filePath: file },
|
|
185
200
|
this.container.context
|
package/src/servers/socket.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import { ServerStateSchema, ServerOptionsSchema } from '../schemas/base.js'
|
|
2
|
+
import { ServerStateSchema, ServerOptionsSchema, ServerEventsSchema } from '../schemas/base.js'
|
|
3
3
|
import { type StartOptions, Server, type ServerState } from '../server.js';
|
|
4
4
|
import { WebSocketServer as BaseServer } from 'ws'
|
|
5
5
|
|
|
@@ -10,16 +10,24 @@ declare module '../server' {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export const SocketServerOptionsSchema = ServerOptionsSchema.extend({
|
|
13
|
-
json: z.boolean().optional().describe('
|
|
13
|
+
json: z.boolean().optional().describe('When enabled, incoming messages are automatically JSON-parsed before emitting the message event, and outgoing send/broadcast calls JSON-stringify the payload'),
|
|
14
14
|
})
|
|
15
15
|
export type SocketServerOptions = z.infer<typeof SocketServerOptionsSchema>
|
|
16
16
|
|
|
17
|
+
export const SocketServerEventsSchema = ServerEventsSchema.extend({
|
|
18
|
+
connection: z.tuple([z.any().describe('The raw WebSocket client instance from the ws library')]).describe('Fires when a new client connects'),
|
|
19
|
+
message: z.tuple([z.any().describe('The message data (JSON-parsed object when json option is enabled, raw Buffer/string otherwise)'), z.any().describe('The WebSocket client that sent the message — use with server.send(ws, data) to reply')]).describe('Fires when a message is received from a client. Handler signature: (data, ws)'),
|
|
20
|
+
}).describe('WebSocket server events')
|
|
21
|
+
|
|
17
22
|
/**
|
|
18
23
|
* WebSocket server built on the `ws` library with optional JSON message framing.
|
|
19
24
|
*
|
|
20
25
|
* Manages WebSocket connections, tracks connected clients, and bridges
|
|
21
|
-
* messages to Luca's event bus. When `json` mode is enabled,
|
|
22
|
-
* are automatically parsed
|
|
26
|
+
* messages to Luca's event bus. When `json` mode is enabled, incoming
|
|
27
|
+
* messages are automatically JSON-parsed (with `.toString()` for Buffer data)
|
|
28
|
+
* and outgoing messages via `send()` / `broadcast()` are JSON-stringified.
|
|
29
|
+
* When `json` mode is disabled, raw message data is emitted as-is and
|
|
30
|
+
* `send()` / `broadcast()` still JSON-stringify for safety.
|
|
23
31
|
*
|
|
24
32
|
* @extends Server
|
|
25
33
|
*
|
|
@@ -28,7 +36,7 @@ export type SocketServerOptions = z.infer<typeof SocketServerOptionsSchema>
|
|
|
28
36
|
* const ws = container.server('websocket', { json: true })
|
|
29
37
|
* await ws.start({ port: 8080 })
|
|
30
38
|
*
|
|
31
|
-
* ws.on('message', (
|
|
39
|
+
* ws.on('message', (data, client) => {
|
|
32
40
|
* console.log('Received:', data)
|
|
33
41
|
* ws.broadcast({ echo: data })
|
|
34
42
|
* })
|
|
@@ -38,6 +46,7 @@ export class WebsocketServer<T extends ServerState = ServerState, K extends Sock
|
|
|
38
46
|
static override shortcut = 'servers.websocket' as const
|
|
39
47
|
static override stateSchema = ServerStateSchema
|
|
40
48
|
static override optionsSchema = SocketServerOptionsSchema
|
|
49
|
+
static override eventsSchema = SocketServerEventsSchema
|
|
41
50
|
|
|
42
51
|
static { Server.register(this, 'websocket') }
|
|
43
52
|
|
|
@@ -68,19 +77,43 @@ export class WebsocketServer<T extends ServerState = ServerState, K extends Sock
|
|
|
68
77
|
return this
|
|
69
78
|
}
|
|
70
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Start the WebSocket server. A runtime `port` overrides the constructor
|
|
82
|
+
* option and is written to state before the underlying `ws.Server` is created,
|
|
83
|
+
* so the server binds to the correct port.
|
|
84
|
+
*
|
|
85
|
+
* @param options - Optional runtime overrides for port and host
|
|
86
|
+
*/
|
|
71
87
|
override async start(options?: StartOptions) {
|
|
72
|
-
if(
|
|
88
|
+
if (this.isListening) {
|
|
89
|
+
return this
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Apply runtime port to state before configure/wss touches it
|
|
93
|
+
if (options?.port) {
|
|
94
|
+
this.state.set('port', options.port)
|
|
95
|
+
// Reset cached wss so it rebinds to the new port
|
|
96
|
+
this._wss = undefined
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if(!this.isConfigured || options?.port) {
|
|
73
100
|
await this.configure()
|
|
74
101
|
}
|
|
75
|
-
|
|
102
|
+
|
|
76
103
|
const { wss } = this
|
|
77
104
|
|
|
78
105
|
wss.on('connection', (ws) => {
|
|
79
106
|
this.connections.add(ws)
|
|
80
107
|
this.emit('connection', ws)
|
|
81
|
-
|
|
82
|
-
ws.on('message', (
|
|
83
|
-
|
|
108
|
+
|
|
109
|
+
ws.on('message', (raw) => {
|
|
110
|
+
let data: any = raw
|
|
111
|
+
if (this.options.json) {
|
|
112
|
+
try {
|
|
113
|
+
data = JSON.parse(typeof raw === 'string' ? raw : raw.toString())
|
|
114
|
+
} catch {}
|
|
115
|
+
}
|
|
116
|
+
this.emit('message', data, ws)
|
|
84
117
|
})
|
|
85
118
|
})
|
|
86
119
|
|
|
@@ -127,8 +160,9 @@ export class WebsocketServer<T extends ServerState = ServerState, K extends Sock
|
|
|
127
160
|
return this
|
|
128
161
|
}
|
|
129
162
|
|
|
163
|
+
/** The port this server will bind to. Defaults to 8081 if not set via constructor options or start(). */
|
|
130
164
|
override get port() {
|
|
131
|
-
return this.state.get('port') || this.options.port || 8081
|
|
165
|
+
return this.state.get('port') || this.options.port || 8081
|
|
132
166
|
}
|
|
133
167
|
}
|
|
134
168
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import Websocket from 'isomorphic-ws'
|
|
2
|
-
import { Client
|
|
2
|
+
import { Client } from '../../client'
|
|
3
|
+
import { WebSocketClient, type WebSocketClientState, type WebSocketClientOptions } from '../../clients/websocket'
|
|
4
|
+
import { WebSocketClientEventsSchema } from '../../schemas/base.js'
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* Web-specific WebSocket client implementation using isomorphic-ws.
|
|
@@ -11,6 +13,7 @@ export class SocketClient<T extends WebSocketClientState = WebSocketClientState,
|
|
|
11
13
|
declare ws: Websocket | WebSocket
|
|
12
14
|
|
|
13
15
|
static override shortcut = 'clients.websocket' as const
|
|
16
|
+
static override eventsSchema = WebSocketClientEventsSchema
|
|
14
17
|
|
|
15
18
|
static { Client.register(this, 'websocket') }
|
|
16
19
|
|
package/src/web/container.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from '../container.js'
|
|
2
2
|
import { Container } from '../container.js'
|
|
3
|
-
import { Client
|
|
3
|
+
import { Client } from '../client.js'
|
|
4
|
+
import { RestClient } from '../clients/rest.js'
|
|
4
5
|
import { SocketClient } from './clients/socket.js'
|
|
5
6
|
import type { AvailableFeatures } from '../feature.js'
|
|
6
7
|
import type { ContainerState, ContainerArgv } from '../container.js'
|
|
@@ -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
|
|
|
@@ -9,6 +9,11 @@ export const NetworkStateSchema = FeatureStateSchema.extend({
|
|
|
9
9
|
|
|
10
10
|
export const NetworkOptionsSchema = FeatureOptionsSchema.extend({})
|
|
11
11
|
|
|
12
|
+
export const NetworkEventsSchema = FeatureEventsSchema.extend({
|
|
13
|
+
online: z.tuple([]).describe('Fires when the browser regains network connectivity'),
|
|
14
|
+
offline: z.tuple([]).describe('Fires when the browser loses network connectivity'),
|
|
15
|
+
}).describe('Network events')
|
|
16
|
+
|
|
12
17
|
export type NetworkState = z.infer<typeof NetworkStateSchema>
|
|
13
18
|
export type NetworkOptions = z.infer<typeof NetworkOptionsSchema>
|
|
14
19
|
|
|
@@ -37,6 +42,7 @@ export class Network<
|
|
|
37
42
|
> extends Feature<T, K> {
|
|
38
43
|
static override stateSchema = NetworkStateSchema
|
|
39
44
|
static override optionsSchema = NetworkOptionsSchema
|
|
45
|
+
static override eventsSchema = NetworkEventsSchema
|
|
40
46
|
static override shortcut = "features.network" as const
|
|
41
47
|
|
|
42
48
|
static { Feature.register(this as any, 'network') }
|