@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
|
@@ -18,12 +18,14 @@ Run `luca serve` and they're automatically discovered and mounted.
|
|
|
18
18
|
|
|
19
19
|
## Imports
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
import
|
|
24
|
-
```
|
|
21
|
+
Endpoints are lightweight — just exports and handler functions. No imports are required.
|
|
22
|
+
|
|
23
|
+
If your project has `@soederpop/luca` as an npm dependency, you can import `z` from `zod` and `EndpointContext` from `@soederpop/luca` for type safety. Otherwise, use `any` types — the framework handles validation and context injection for you.
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
Access framework capabilities through the `ctx` parameter:
|
|
26
|
+
- `ctx.container.feature('fs')` for file operations
|
|
27
|
+
- `ctx.container.feature('yaml')` for YAML parsing
|
|
28
|
+
- `ctx.container.feature('sqlite')` for database access
|
|
27
29
|
|
|
28
30
|
## Required Exports
|
|
29
31
|
|
|
@@ -37,16 +39,16 @@ export const tags = ['{{camelName}}']
|
|
|
37
39
|
|
|
38
40
|
## Handler Functions
|
|
39
41
|
|
|
40
|
-
Export named functions for each HTTP method you support. Each receives validated parameters and
|
|
42
|
+
Export named functions for each HTTP method you support. Each receives validated parameters and a context object:
|
|
41
43
|
|
|
42
44
|
```ts
|
|
43
|
-
export async function get(params: any, ctx:
|
|
45
|
+
export async function get(params: any, ctx: any) {
|
|
44
46
|
const fs = ctx.container.feature('fs')
|
|
45
47
|
// Your logic here
|
|
46
48
|
return { message: 'ok' }
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
export async function post(params:
|
|
51
|
+
export async function post(params: any, ctx: any) {
|
|
50
52
|
// Create something
|
|
51
53
|
return { created: true }
|
|
52
54
|
}
|
|
@@ -64,9 +66,11 @@ Return any object — it's automatically JSON-serialized as the response.
|
|
|
64
66
|
|
|
65
67
|
## Validation Schemas
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+
If `zod` is available (via `@soederpop/luca` dependency or `node_modules`), export Zod schemas to validate parameters for each method. Name them `{method}Schema`:
|
|
68
70
|
|
|
69
71
|
```ts
|
|
72
|
+
import { z } from 'zod'
|
|
73
|
+
|
|
70
74
|
export const getSchema = z.object({
|
|
71
75
|
q: z.string().optional().describe('Search query'),
|
|
72
76
|
limit: z.number().default(20).describe('Max results'),
|
|
@@ -78,7 +82,7 @@ export const postSchema = z.object({
|
|
|
78
82
|
})
|
|
79
83
|
```
|
|
80
84
|
|
|
81
|
-
Invalid requests automatically return 400 with Zod error details. Schemas also feed the auto-generated OpenAPI spec.
|
|
85
|
+
Invalid requests automatically return 400 with Zod error details. Schemas also feed the auto-generated OpenAPI spec. If zod is not available, skip schema exports — the endpoint still works, you just lose automatic validation.
|
|
82
86
|
|
|
83
87
|
## Rate Limiting
|
|
84
88
|
|
|
@@ -94,42 +98,40 @@ export const postRateLimit = { maxRequests: 10, windowSeconds: 1 }
|
|
|
94
98
|
|
|
95
99
|
## Delete Handler
|
|
96
100
|
|
|
97
|
-
`delete` is a reserved word in JS. Use
|
|
101
|
+
`delete` is a reserved word in JS, so you can't use it as a function name directly. Use a named export alias:
|
|
98
102
|
|
|
99
103
|
```ts
|
|
100
|
-
|
|
104
|
+
// Use a local name, then re-export as `delete`
|
|
105
|
+
const del = async (params: any, ctx: any) => {
|
|
101
106
|
return { deleted: true }
|
|
102
107
|
}
|
|
103
108
|
export { del as delete }
|
|
104
109
|
```
|
|
105
110
|
|
|
111
|
+
You can also export `deleteSchema` and `deleteRateLimit` for validation and rate limiting on DELETE.
|
|
112
|
+
|
|
106
113
|
## Complete Example
|
|
107
114
|
|
|
108
|
-
A CRUD endpoint for a resource:
|
|
115
|
+
A CRUD endpoint for a resource (no external imports needed):
|
|
109
116
|
|
|
110
117
|
```ts
|
|
111
|
-
import { z } from 'zod'
|
|
112
|
-
import type { EndpointContext } from '@soederpop/luca'
|
|
113
|
-
|
|
114
118
|
export const path = '/api/{{camelName}}'
|
|
115
119
|
export const description = '{{description}}'
|
|
116
120
|
export const tags = ['{{camelName}}']
|
|
117
121
|
|
|
118
|
-
export
|
|
119
|
-
q: z.string().optional().describe('Search query'),
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
export async function get(params: z.infer<typeof getSchema>, ctx: EndpointContext) {
|
|
122
|
+
export async function get(params: any, ctx: any) {
|
|
123
123
|
return { items: [], total: 0 }
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
export
|
|
127
|
-
name: z.string().min(1).describe('Item name'),
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
export async function post(params: z.infer<typeof postSchema>, ctx: EndpointContext) {
|
|
126
|
+
export async function post(params: any, ctx: any) {
|
|
131
127
|
return { item: { id: '1', ...params }, message: 'Created' }
|
|
132
128
|
}
|
|
129
|
+
|
|
130
|
+
const del = async (params: any, ctx: any) => {
|
|
131
|
+
const { id } = ctx.params
|
|
132
|
+
return { message: `Deleted ${id}` }
|
|
133
|
+
}
|
|
134
|
+
export { del as delete }
|
|
133
135
|
```
|
|
134
136
|
|
|
135
137
|
## Dynamic Route Example
|
|
@@ -138,28 +140,21 @@ For routes with URL parameters, create a nested file:
|
|
|
138
140
|
|
|
139
141
|
```ts
|
|
140
142
|
// endpoints/{{camelName}}/[id].ts
|
|
141
|
-
import { z } from 'zod'
|
|
142
|
-
import type { EndpointContext } from '@soederpop/luca'
|
|
143
|
-
|
|
144
143
|
export const path = '/api/{{camelName}}/:id'
|
|
145
144
|
export const description = 'Get, update, or delete a specific item'
|
|
146
145
|
export const tags = ['{{camelName}}']
|
|
147
146
|
|
|
148
|
-
export async function get(params: any, ctx:
|
|
147
|
+
export async function get(params: any, ctx: any) {
|
|
149
148
|
const { id } = ctx.params
|
|
150
149
|
return { item: { id } }
|
|
151
150
|
}
|
|
152
151
|
|
|
153
|
-
export
|
|
154
|
-
name: z.string().min(1).optional().describe('Updated name'),
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
export async function put(params: z.infer<typeof putSchema>, ctx: EndpointContext) {
|
|
152
|
+
export async function put(params: any, ctx: any) {
|
|
158
153
|
const { id } = ctx.params
|
|
159
154
|
return { item: { id, ...params }, message: 'Updated' }
|
|
160
155
|
}
|
|
161
156
|
|
|
162
|
-
const del = async (params: any, ctx:
|
|
157
|
+
const del = async (params: any, ctx: any) => {
|
|
163
158
|
const { id } = ctx.params
|
|
164
159
|
return { message: `Deleted ${id}` }
|
|
165
160
|
}
|
|
@@ -12,12 +12,15 @@ When to build a feature:
|
|
|
12
12
|
```ts
|
|
13
13
|
import { z } from 'zod'
|
|
14
14
|
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '@soederpop/luca'
|
|
15
|
-
import { Feature
|
|
16
|
-
import type { ContainerContext } from '@soederpop/luca'
|
|
15
|
+
import { Feature } from '@soederpop/luca'
|
|
17
16
|
```
|
|
18
17
|
|
|
19
18
|
These are the only imports your feature file needs from luca. If your feature wraps a third-party library, import it here too — feature implementations are the ONE place where direct library imports are allowed.
|
|
20
19
|
|
|
20
|
+
The use of dynamic imports is encouraged here, only import libraries you need when the feature is used, and only when necessary in the lifecycle of the feature if it can be done.
|
|
21
|
+
|
|
22
|
+
feature's have built in ways to check if their requirements are supported and can be enabled cautiously.
|
|
23
|
+
|
|
21
24
|
## Schemas
|
|
22
25
|
|
|
23
26
|
Define the shape of your feature's state, options, and events using Zod. Every field must have a `.describe()` — this becomes the documentation.
|
|
@@ -45,11 +48,11 @@ export const {{PascalName}}EventsSchema = FeatureEventsSchema.extend({
|
|
|
45
48
|
|
|
46
49
|
The class extends `Feature` with your state and options types. Static properties drive registration and introspection. Every public method needs a JSDoc block with `@param`, `@returns`, and `@example`.
|
|
47
50
|
|
|
51
|
+
Running `luca introspect` captures JSDoc blocks and Zod schemas and includes them in the description whenever somebody calls `container.features.describe('{{camelName}}')` or `luca describe {{camelName}}`.
|
|
52
|
+
|
|
48
53
|
```ts
|
|
49
54
|
/**
|
|
50
55
|
* {{description}}
|
|
51
|
-
*
|
|
52
|
-
* @example
|
|
53
56
|
* ```typescript
|
|
54
57
|
* const {{camelName}} = container.feature('{{camelName}}')
|
|
55
58
|
* ```
|
|
@@ -61,17 +64,21 @@ export class {{PascalName}} extends Feature<{{PascalName}}State, {{PascalName}}O
|
|
|
61
64
|
static override stateSchema = {{PascalName}}StateSchema
|
|
62
65
|
static override optionsSchema = {{PascalName}}OptionsSchema
|
|
63
66
|
static override eventsSchema = {{PascalName}}EventsSchema
|
|
64
|
-
static override description = '{{description}}'
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
super(options, context)
|
|
68
|
-
// Initialize state, set up resources
|
|
69
|
-
}
|
|
68
|
+
static { Feature.register(this, '{{camelName}}') }
|
|
70
69
|
|
|
71
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Called after the feature is initialized. Use this for any setup logic
|
|
72
|
+
* instead of overriding the constructor.
|
|
73
|
+
*/
|
|
74
|
+
async afterInitialize() {
|
|
75
|
+
// Set up initial state, start background tasks, etc.
|
|
76
|
+
}
|
|
72
77
|
}
|
|
73
78
|
```
|
|
74
79
|
|
|
80
|
+
**Important**: You almost never need to override the constructor. Use `afterInitialize()` for any setup logic — it runs after the feature is fully wired into the container and has access to `this.container`, `this.options`, `this.state`, etc.
|
|
81
|
+
|
|
75
82
|
## Module Augmentation
|
|
76
83
|
|
|
77
84
|
This is what gives `container.feature('yourName')` TypeScript autocomplete. Without it, the feature works but TypeScript won't know about it.
|
|
@@ -86,10 +93,14 @@ declare module '@soederpop/luca' {
|
|
|
86
93
|
|
|
87
94
|
## Registration
|
|
88
95
|
|
|
89
|
-
|
|
96
|
+
Registration happens inside the class body using a static block. The default export is just the class itself.
|
|
90
97
|
|
|
91
98
|
```ts
|
|
92
|
-
|
|
99
|
+
// Inside the class:
|
|
100
|
+
static { Feature.register(this, '{{camelName}}') }
|
|
101
|
+
|
|
102
|
+
// At module level:
|
|
103
|
+
export default {{PascalName}}
|
|
93
104
|
```
|
|
94
105
|
|
|
95
106
|
## Complete Example
|
|
@@ -99,8 +110,7 @@ Here's a minimal but complete feature. This is what a real feature file looks li
|
|
|
99
110
|
```ts
|
|
100
111
|
import { z } from 'zod'
|
|
101
112
|
import { FeatureStateSchema, FeatureOptionsSchema } from '@soederpop/luca'
|
|
102
|
-
import { Feature
|
|
103
|
-
import type { ContainerContext } from '@soederpop/luca'
|
|
113
|
+
import { Feature } from '@soederpop/luca'
|
|
104
114
|
|
|
105
115
|
declare module '@soederpop/luca' {
|
|
106
116
|
interface AvailableFeatures {
|
|
@@ -128,14 +138,14 @@ export class {{PascalName}} extends Feature<{{PascalName}}State, {{PascalName}}O
|
|
|
128
138
|
static override shortcut = 'features.{{camelName}}' as const
|
|
129
139
|
static override stateSchema = {{PascalName}}StateSchema
|
|
130
140
|
static override optionsSchema = {{PascalName}}OptionsSchema
|
|
131
|
-
static
|
|
141
|
+
static { Feature.register(this, '{{camelName}}') }
|
|
132
142
|
|
|
133
|
-
|
|
134
|
-
|
|
143
|
+
async afterInitialize() {
|
|
144
|
+
// Setup logic goes here — not in the constructor
|
|
135
145
|
}
|
|
136
146
|
}
|
|
137
147
|
|
|
138
|
-
export default
|
|
148
|
+
export default {{PascalName}}
|
|
139
149
|
```
|
|
140
150
|
|
|
141
151
|
## Conventions
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Building a Selector
|
|
2
|
+
|
|
3
|
+
A selector returns data. Where commands perform actions, selectors query and return structured results with built-in caching. Selectors live in a project's `selectors/` folder and are automatically discovered.
|
|
4
|
+
|
|
5
|
+
When to build a selector:
|
|
6
|
+
- You need to query project data (package info, file listings, config values)
|
|
7
|
+
- The result benefits from caching (keyed by git SHA or custom key)
|
|
8
|
+
- You want the data available via `container.select('name')` or `luca select name`
|
|
9
|
+
|
|
10
|
+
## Imports
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { z } from 'zod'
|
|
14
|
+
import type { ContainerContext } from '@soederpop/luca'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Args Schema
|
|
18
|
+
|
|
19
|
+
Define the selector's input arguments with Zod.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
export const argsSchema = z.object({
|
|
23
|
+
// Add your input arguments here.
|
|
24
|
+
// Example: field: z.string().optional().describe('Specific field to return'),
|
|
25
|
+
})
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Description
|
|
29
|
+
|
|
30
|
+
Export a description string for discoverability:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
export const description = '{{description}}'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Caching
|
|
37
|
+
|
|
38
|
+
Selectors cache by default. The default cache key is `hashObject({ selectorName, args, gitSha })` — same args + same commit = cache hit.
|
|
39
|
+
|
|
40
|
+
To customize the cache key:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
export function cacheKey(args: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
44
|
+
return context.container.git.currentCommitSha
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
To disable caching:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
export const cacheable = false
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Handler
|
|
55
|
+
|
|
56
|
+
Export a `run` function that returns data. It receives parsed args and the container context.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
export async function run(args: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
60
|
+
const { container } = context
|
|
61
|
+
// Query and return your data
|
|
62
|
+
return { /* your data */ }
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Complete Example
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { z } from 'zod'
|
|
70
|
+
import type { ContainerContext } from '@soederpop/luca'
|
|
71
|
+
|
|
72
|
+
export const description = '{{description}}'
|
|
73
|
+
|
|
74
|
+
export const argsSchema = z.object({})
|
|
75
|
+
|
|
76
|
+
export async function run(args: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
77
|
+
const { container } = context
|
|
78
|
+
|
|
79
|
+
// Return your data here
|
|
80
|
+
return {}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Conventions
|
|
85
|
+
|
|
86
|
+
- **File location**: `selectors/{{kebabName}}.ts` in the project root. Discovered automatically.
|
|
87
|
+
- **Naming**: kebab-case for filename. `luca select {{kebabName}}` maps to `selectors/{{kebabName}}.ts`.
|
|
88
|
+
- **Use the container**: Never import `fs`, `path` directly. Use `container.feature('fs')`, `container.paths`.
|
|
89
|
+
- **Return data**: The `run` function must return the data. It gets wrapped in `{ data, cached, cacheKey }` by the framework.
|
|
90
|
+
- **Caching**: On by default. Override `cacheKey()` for custom invalidation, or set `cacheable = false` to skip.
|
|
91
|
+
- **CLI**: `luca select {{kebabName}}` runs the selector and prints JSON. Use `--json` for data only, `--no-cache` to force fresh.
|
package/docs/scaffolds/server.md
CHANGED
|
@@ -11,9 +11,9 @@ When to build a server:
|
|
|
11
11
|
|
|
12
12
|
```ts
|
|
13
13
|
import { z } from 'zod'
|
|
14
|
-
import { Server
|
|
14
|
+
import { Server } from '@soederpop/luca'
|
|
15
15
|
import { ServerStateSchema, ServerOptionsSchema, ServerEventsSchema } from '@soederpop/luca'
|
|
16
|
-
import type {
|
|
16
|
+
import type { NodeContainer } from '@soederpop/luca'
|
|
17
17
|
import type { ServersInterface } from '@soederpop/luca'
|
|
18
18
|
```
|
|
19
19
|
|
|
@@ -40,6 +40,8 @@ export const {{PascalName}}EventsSchema = ServerEventsSchema.extend({
|
|
|
40
40
|
|
|
41
41
|
## Class
|
|
42
42
|
|
|
43
|
+
Running `luca introspect` captures JSDoc blocks and Zod schemas and includes them in the description whenever somebody calls `container.servers.describe('{{camelName}}')` or `luca describe {{camelName}}`.
|
|
44
|
+
|
|
43
45
|
```ts
|
|
44
46
|
/**
|
|
45
47
|
* {{description}}
|
|
@@ -57,7 +59,7 @@ export class {{PascalName}} extends Server<{{PascalName}}State, {{PascalName}}Op
|
|
|
57
59
|
static override stateSchema = {{PascalName}}StateSchema
|
|
58
60
|
static override optionsSchema = {{PascalName}}OptionsSchema
|
|
59
61
|
static override eventsSchema = {{PascalName}}EventsSchema
|
|
60
|
-
static
|
|
62
|
+
static { Server.register(this, '{{camelName}}') }
|
|
61
63
|
|
|
62
64
|
static override attach(container: NodeContainer & ServersInterface) {
|
|
63
65
|
return container
|
|
@@ -103,17 +105,23 @@ declare module '@soederpop/luca' {
|
|
|
103
105
|
|
|
104
106
|
## Registration
|
|
105
107
|
|
|
108
|
+
Registration happens inside the class body using a static block. The default export is just the class itself.
|
|
109
|
+
|
|
106
110
|
```ts
|
|
107
|
-
|
|
111
|
+
// Inside the class:
|
|
112
|
+
static { Server.register(this, '{{camelName}}') }
|
|
113
|
+
|
|
114
|
+
// At module level:
|
|
115
|
+
export default {{PascalName}}
|
|
108
116
|
```
|
|
109
117
|
|
|
110
118
|
## Complete Example
|
|
111
119
|
|
|
112
120
|
```ts
|
|
113
121
|
import { z } from 'zod'
|
|
114
|
-
import { Server
|
|
122
|
+
import { Server } from '@soederpop/luca'
|
|
115
123
|
import { ServerStateSchema, ServerOptionsSchema, ServerEventsSchema } from '@soederpop/luca'
|
|
116
|
-
import type {
|
|
124
|
+
import type { NodeContainer } from '@soederpop/luca'
|
|
117
125
|
import type { ServersInterface } from '@soederpop/luca'
|
|
118
126
|
|
|
119
127
|
declare module '@soederpop/luca' {
|
|
@@ -146,7 +154,7 @@ export class {{PascalName}} extends Server<{{PascalName}}State, {{PascalName}}Op
|
|
|
146
154
|
static override stateSchema = {{PascalName}}StateSchema
|
|
147
155
|
static override optionsSchema = {{PascalName}}OptionsSchema
|
|
148
156
|
static override eventsSchema = {{PascalName}}EventsSchema
|
|
149
|
-
static
|
|
157
|
+
static { Server.register(this, '{{camelName}}') }
|
|
150
158
|
|
|
151
159
|
static override attach(container: NodeContainer & ServersInterface) {
|
|
152
160
|
return container
|
|
@@ -175,13 +183,14 @@ export class {{PascalName}} extends Server<{{PascalName}}State, {{PascalName}}Op
|
|
|
175
183
|
}
|
|
176
184
|
}
|
|
177
185
|
|
|
178
|
-
export default
|
|
186
|
+
export default {{PascalName}}
|
|
179
187
|
```
|
|
180
188
|
|
|
181
189
|
## Conventions
|
|
182
190
|
|
|
183
191
|
- **Lifecycle**: Implement `configure()`, `start()`, and `stop()`. Check guards (`isConfigured`, `isListening`, `isStopped`) at the top of each.
|
|
192
|
+
- **Use `afterInitialize()`**: For any setup logic instead of overriding the constructor. Lifecycle methods (`configure`, `start`, `stop`) handle the server's runtime phases.
|
|
184
193
|
- **State tracking**: Set `configured`, `listening`, `stopped`, and `port` on the state. This powers the introspection system.
|
|
185
194
|
- **attach() is static**: It runs when the container first loads the server class. Use it for container-level setup if needed.
|
|
186
195
|
- **Port from options**: Accept port via options schema and respect it in `start()`. Allow override via start options.
|
|
187
|
-
- **JSDoc everything**: Every public method needs `@param`, `@returns`, `@example`.
|
|
196
|
+
- **JSDoc everything**: Every public method needs `@param`, `@returns`, `@example`. Run `luca introspect` after changes to update generated docs.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Selector System
|
|
2
|
+
|
|
3
|
+
## Vision
|
|
4
|
+
|
|
5
|
+
Commands perform actions. Selectors return data. Together they form a complete agent tool interface — pick which commands and selectors you need and you get free assistant tools. Agents only need to know the external APIs of features, servers, and clients already in the container (which can be learned from `luca describe`).
|
|
6
|
+
|
|
7
|
+
## What Is a Selector?
|
|
8
|
+
|
|
9
|
+
A Selector is a new helper type (like Feature, Client, Server, Command) that:
|
|
10
|
+
|
|
11
|
+
- Extends `Helper` with a `select(options)` method that **returns data**
|
|
12
|
+
- Lives in a `SelectorsRegistry`, queryable via `container.selectors`
|
|
13
|
+
- Is instantiated via `container.selector('name', options)` factory
|
|
14
|
+
- Supports **caching** via `diskCache` — many selector values don't change if git SHAs don't change, so SHA makes a great cache key
|
|
15
|
+
- Is discoverable from a `selectors/` folder in projects, just like `commands/`
|
|
16
|
+
- Supports both **class-based** and **module-based** (export a `select` function) patterns
|
|
17
|
+
|
|
18
|
+
## Architecture — How It Maps to Existing Patterns
|
|
19
|
+
|
|
20
|
+
The implementation follows the exact same pattern as `Command`:
|
|
21
|
+
|
|
22
|
+
### Files to Create/Modify
|
|
23
|
+
|
|
24
|
+
1. **`src/schemas/base.ts`** — Add `SelectorStateSchema`, `SelectorOptionsSchema`, `SelectorEventsSchema`
|
|
25
|
+
2. **`src/selector.ts`** — New file mirroring `src/command.ts`:
|
|
26
|
+
- `Selector` class extending `Helper` with `select()` method
|
|
27
|
+
- `SelectorsRegistry` extending `Registry` with `discover()` support
|
|
28
|
+
- `selectors` singleton, `helperCache`, `Selector.register()`, `Selector.attach()`
|
|
29
|
+
- `AvailableSelectors` interface, `SelectorsInterface`, `SelectorFactory` type
|
|
30
|
+
- Module-based pattern: `SimpleSelector` type for graft compatibility
|
|
31
|
+
3. **`src/node/container.ts`** — Import `Selector` + `SelectorsInterface`, add to `ClientsAndServersInterface`, wire `this.use(Selector)`
|
|
32
|
+
4. **`src/node/features/helpers.ts`** — Add `selectors` to `registryMap` and `RegistryType`, add to `discoverAll()` iteration
|
|
33
|
+
|
|
34
|
+
### Selector Base Class Shape
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
class Selector<T, K> extends Helper<T, K> {
|
|
38
|
+
static shortcut = 'selectors.base'
|
|
39
|
+
static argsSchema: z.ZodType // schema for select() input
|
|
40
|
+
static cacheable: boolean = true // opt-out of caching
|
|
41
|
+
|
|
42
|
+
// The core method — override in subclass or graft from module export
|
|
43
|
+
async select(args, context): Promise<any> {}
|
|
44
|
+
|
|
45
|
+
// Dispatch normalizer (like Command.execute)
|
|
46
|
+
async resolve(args?, source?): Promise<SelectorResult> {}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Module-Based Pattern (selectors/ folder)
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// selectors/package-info.ts
|
|
54
|
+
export const description = 'Returns parsed package.json data'
|
|
55
|
+
export const argsSchema = z.object({ field: z.string().optional() })
|
|
56
|
+
export const cacheable = true
|
|
57
|
+
|
|
58
|
+
export function cacheKey(args, context) {
|
|
59
|
+
// return a string key — if unchanged, cached value is returned
|
|
60
|
+
return context.container.git.sha
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function select(args, context) {
|
|
64
|
+
const manifest = context.container.manifest
|
|
65
|
+
return args.field ? manifest[args.field] : manifest
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Caching Strategy
|
|
70
|
+
|
|
71
|
+
- Built into the `Selector` base class, powered by `diskCache` feature
|
|
72
|
+
- Subclasses/modules can export a `cacheKey(args, context)` function
|
|
73
|
+
- Default cache key strategy: `hashObject({ selectorName, args, gitSha })`
|
|
74
|
+
- `cacheable: false` opts out entirely
|
|
75
|
+
- Cache is key-invalidated (key changes = miss), no TTL by default
|
|
76
|
+
- The `resolve()` method checks cache before calling `select()`
|
|
77
|
+
|
|
78
|
+
## Open Questions (Needs Decision)
|
|
79
|
+
|
|
80
|
+
### 1. Return Shape
|
|
81
|
+
Should `select()` return data directly, or a wrapped result like `{ data, metadata, cached }`?
|
|
82
|
+
|
|
83
|
+
**Leaning toward:** data directly from `select()`, but `resolve()` (the dispatch method) returns `{ data, cached, cacheKey }` so callers know if it was a cache hit.
|
|
84
|
+
|
|
85
|
+
### 2. CLI Integration
|
|
86
|
+
Should there be a `luca select <name>` CLI command? Would make selectors usable from the terminal, printing JSON output.
|
|
87
|
+
|
|
88
|
+
### 3. Cache TTL
|
|
89
|
+
Is pure key-invalidation enough, or do some selectors need time-based expiry?
|
|
90
|
+
|
|
91
|
+
### 4. Scope of First Delivery
|
|
92
|
+
|
|
93
|
+
Proposed skateboard:
|
|
94
|
+
- `Selector` base class with `select()` and `resolve()` (cached dispatch)
|
|
95
|
+
- `SelectorsRegistry` with `discover()`
|
|
96
|
+
- Container wiring (`container.selectors`, `container.selector()`)
|
|
97
|
+
- Schemas in `schemas/base.ts`
|
|
98
|
+
- `selectors/` folder discovery via `Helpers` feature
|
|
99
|
+
- Caching integration with `diskCache`
|
|
100
|
+
- Module-based pattern support (export `select` + optional `cacheKey`)
|
|
101
|
+
- `Selector` + `selectors` exported from `@soederpop/luca` barrel + seeded in VM virtual modules
|
|
102
|
+
|
|
103
|
+
## Codebase Exploration Notes
|
|
104
|
+
|
|
105
|
+
These are the key files that were studied to inform this design:
|
|
106
|
+
|
|
107
|
+
- `src/command.ts` — The primary pattern to mirror. `Command` extends `Helper`, has `CommandsRegistry`, `commands` singleton, `Command.register()`, `Command.attach()`, `execute()`/`run()` split, headless capture, `graftModule` support.
|
|
108
|
+
- `src/helper.ts` — Base class providing state, events, options (Zod-validated), introspection, `afterInitialize()` hook.
|
|
109
|
+
- `src/registry.ts` — Abstract `Registry<T>` with `register()`, `lookup()`, `has()`, `available`, `describe()`, `describeAll()`, event bus.
|
|
110
|
+
- `src/graft.ts` — `graftModule()` synthesizes a class from plain module exports. `RESERVED_EXPORTS` list determines what becomes static vs prototype methods.
|
|
111
|
+
- `src/schemas/base.ts` — All Zod schemas. Each helper type has State/Options/Events schemas.
|
|
112
|
+
- `src/node/container.ts` — `NodeContainer` wires everything: side-effect imports, `NodeFeatures` interface, `ClientsAndServersInterface`, `this.use(Command)` pattern.
|
|
113
|
+
- `src/node/features/helpers.ts` — `Helpers` feature: `registryMap`, `RegistryType`, `discoverAll()`, class-based vs config-based discovery, VM module seeding.
|
|
114
|
+
- `src/node/features/disk-cache.ts` — `DiskCache` feature: `get/set/has/rm`, `cacache`-backed, per-project cache dir, `container.utils.hashObject()` for keys.
|
|
115
|
+
- `src/container.ts` — Base `Container`: `createHelperInstance()` with `helperCache` Map, `use()` plugin system, `registerHelperType()`.
|