@soederpop/luca 0.0.3 → 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
package/src/client.ts
CHANGED
|
@@ -38,6 +38,9 @@ export class Client<
|
|
|
38
38
|
static override optionsSchema = ClientOptionsSchema
|
|
39
39
|
static override eventsSchema = ClientEventsSchema
|
|
40
40
|
|
|
41
|
+
/** Self-register a Client subclass from a static initialization block. */
|
|
42
|
+
static register: (SubClass: typeof Client, id?: string) => typeof Client
|
|
43
|
+
|
|
41
44
|
static attach(container: Container & ClientsInterface): any {
|
|
42
45
|
Object.assign(container, {
|
|
43
46
|
get clients() {
|
|
@@ -100,6 +103,61 @@ export class Client<
|
|
|
100
103
|
}
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
// --- Registry and Client.register must be defined BEFORE subclasses ---
|
|
107
|
+
// because static blocks in RestClient/GraphClient/WebSocketClient run at class declaration time.
|
|
108
|
+
|
|
109
|
+
export class ClientsRegistry extends Registry<Client<any>> {
|
|
110
|
+
override scope = "clients"
|
|
111
|
+
override baseClass = Client
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const clients = new ClientsRegistry();
|
|
115
|
+
|
|
116
|
+
export const helperCache = new Map();
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Self-register a Client subclass from a static initialization block.
|
|
120
|
+
* IMPORTANT: Place the static block AFTER all static override declarations
|
|
121
|
+
* so schemas, envVars, and other metadata are set before interceptRegistration fires.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```typescript
|
|
125
|
+
* export class OpenAIClient extends Client {
|
|
126
|
+
* static override stateSchema = OpenAIClientStateSchema
|
|
127
|
+
* static override optionsSchema = OpenAIClientOptionsSchema
|
|
128
|
+
* static { Client.register(this, 'openai') } // must come last
|
|
129
|
+
* }
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
Client.register = function registerClient(
|
|
133
|
+
SubClass: typeof Client,
|
|
134
|
+
id?: string,
|
|
135
|
+
) {
|
|
136
|
+
const registryId = id ?? SubClass.name[0]!.toLowerCase() + SubClass.name.slice(1)
|
|
137
|
+
|
|
138
|
+
// Auto-set shortcut if not explicitly overridden on this class
|
|
139
|
+
if (!Object.getOwnPropertyDescriptor(SubClass, 'shortcut')?.value ||
|
|
140
|
+
(SubClass as any).shortcut === 'unspecified' ||
|
|
141
|
+
(SubClass as any).shortcut === 'clients.base') {
|
|
142
|
+
;(SubClass as any).shortcut = `clients.${registryId}` as const
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Register in the clients registry (interceptRegistration sees all statics above)
|
|
146
|
+
clients.register(registryId, SubClass as any)
|
|
147
|
+
|
|
148
|
+
// Generate default attach() if not explicitly overridden on this class
|
|
149
|
+
if (!Object.getOwnPropertyDescriptor(SubClass, 'attach')) {
|
|
150
|
+
;(SubClass as any).attach = (container: any) => {
|
|
151
|
+
clients.register(registryId, SubClass as any)
|
|
152
|
+
return container
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return SubClass
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// --- Built-in client subclasses ---
|
|
160
|
+
|
|
103
161
|
export class RestClient<
|
|
104
162
|
T extends ClientState = ClientState,
|
|
105
163
|
K extends ClientOptions = ClientOptions
|
|
@@ -107,18 +165,15 @@ export class RestClient<
|
|
|
107
165
|
axios!: AxiosInstance;
|
|
108
166
|
|
|
109
167
|
static override shortcut: string = "clients.rest"
|
|
110
|
-
|
|
111
|
-
static override attach(container: Container & ClientsInterface): any {
|
|
112
|
-
return container
|
|
113
|
-
}
|
|
168
|
+
static { Client.register(this, 'rest') }
|
|
114
169
|
|
|
115
170
|
constructor(options: K, context: ContainerContext) {
|
|
116
171
|
super(options, context);
|
|
117
172
|
|
|
118
173
|
this.axios = axios.create({
|
|
119
|
-
baseURL: this.baseURL,
|
|
174
|
+
baseURL: this.baseURL,
|
|
120
175
|
});
|
|
121
|
-
|
|
176
|
+
|
|
122
177
|
if (this.useJSON) {
|
|
123
178
|
this.axios.defaults.headers.common = {
|
|
124
179
|
...this.axios.defaults.headers.common,
|
|
@@ -127,16 +182,16 @@ export class RestClient<
|
|
|
127
182
|
}
|
|
128
183
|
}
|
|
129
184
|
}
|
|
130
|
-
|
|
185
|
+
|
|
131
186
|
async beforeRequest() {
|
|
132
187
|
}
|
|
133
|
-
|
|
188
|
+
|
|
134
189
|
get useJSON() {
|
|
135
190
|
return !!this.options.json
|
|
136
191
|
}
|
|
137
|
-
|
|
192
|
+
|
|
138
193
|
override get baseURL() {
|
|
139
|
-
return this.options.baseURL || '/'
|
|
194
|
+
return this.options.baseURL || '/'
|
|
140
195
|
}
|
|
141
196
|
|
|
142
197
|
async patch(url: string, data: any = {}, options: AxiosRequestConfig = {}) {
|
|
@@ -156,7 +211,7 @@ export class RestClient<
|
|
|
156
211
|
}
|
|
157
212
|
});
|
|
158
213
|
}
|
|
159
|
-
|
|
214
|
+
|
|
160
215
|
async put(url: string, data: any = {}, options: AxiosRequestConfig = {}) {
|
|
161
216
|
await this.beforeRequest();
|
|
162
217
|
return this.axios({
|
|
@@ -174,7 +229,7 @@ export class RestClient<
|
|
|
174
229
|
}
|
|
175
230
|
});
|
|
176
231
|
}
|
|
177
|
-
|
|
232
|
+
|
|
178
233
|
async post(url: string, data: any = {}, options: AxiosRequestConfig = {}) {
|
|
179
234
|
await this.beforeRequest();
|
|
180
235
|
return this.axios({
|
|
@@ -249,6 +304,7 @@ export class GraphClient<
|
|
|
249
304
|
static override shortcut = "clients.graph" as const
|
|
250
305
|
static override optionsSchema = GraphClientOptionsSchema
|
|
251
306
|
static override eventsSchema = GraphClientEventsSchema
|
|
307
|
+
static { Client.register(this, 'graph') }
|
|
252
308
|
|
|
253
309
|
/** The GraphQL endpoint path. Defaults to '/graphql'. */
|
|
254
310
|
get endpoint() {
|
|
@@ -321,6 +377,7 @@ export class WebSocketClient<
|
|
|
321
377
|
static override stateSchema = WebSocketClientStateSchema
|
|
322
378
|
static override optionsSchema = WebSocketClientOptionsSchema
|
|
323
379
|
static override eventsSchema = WebSocketClientEventsSchema
|
|
380
|
+
static { Client.register(this, 'websocket') }
|
|
324
381
|
|
|
325
382
|
constructor(options?: K, context?: ContainerContext) {
|
|
326
383
|
super(options, context)
|
|
@@ -444,18 +501,4 @@ export class WebSocketClient<
|
|
|
444
501
|
}
|
|
445
502
|
}
|
|
446
503
|
|
|
447
|
-
|
|
448
|
-
export class ClientsRegistry extends Registry<Client<any>> {
|
|
449
|
-
override scope = "clients"
|
|
450
|
-
override baseClass = Client
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
export const clients = new ClientsRegistry();
|
|
454
|
-
|
|
455
|
-
clients.register("rest", RestClient);
|
|
456
|
-
clients.register("graph", GraphClient);
|
|
457
|
-
clients.register("websocket", WebSocketClient);
|
|
458
|
-
|
|
459
|
-
export const helperCache = new Map();
|
|
460
|
-
|
|
461
504
|
export default Client;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Client,
|
|
3
3
|
type ClientOptions,
|
|
4
|
-
type ClientsInterface,
|
|
5
4
|
RestClient,
|
|
6
5
|
} from "@soederpop/luca/client";
|
|
7
|
-
import {
|
|
6
|
+
import { type ContainerContext } from "@soederpop/luca/container";
|
|
8
7
|
import { isEmpty, maxBy, omitBy } from "lodash-es";
|
|
9
8
|
import { NodeContainer } from "@soederpop/luca/node/container";
|
|
10
9
|
import { z } from 'zod'
|
|
@@ -38,11 +37,8 @@ export type CivitaiClientState = z.infer<typeof CivitaiClientStateSchema>
|
|
|
38
37
|
*/
|
|
39
38
|
export class CivitaiClient<T extends CivitaiClientState> extends RestClient<T> {
|
|
40
39
|
static override stateSchema = CivitaiClientStateSchema;
|
|
41
|
-
|
|
42
|
-
static
|
|
43
|
-
container.clients.register("civitai", CivitaiClient);
|
|
44
|
-
return container
|
|
45
|
-
}
|
|
40
|
+
|
|
41
|
+
static { Client.register(this, 'civitai') }
|
|
46
42
|
|
|
47
43
|
constructor(options: ClientOptions, context: ContainerContext) {
|
|
48
44
|
options = {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
clients,
|
|
2
|
+
Client,
|
|
4
3
|
RestClient,
|
|
5
4
|
} from "@soederpop/luca/client";
|
|
6
|
-
import type {
|
|
5
|
+
import type { ContainerContext } from "@soederpop/luca/container";
|
|
7
6
|
import { z } from 'zod'
|
|
8
7
|
import { ClientStateSchema, ClientOptionsSchema } from '@soederpop/luca/schemas/base.js'
|
|
9
8
|
|
|
@@ -68,12 +67,9 @@ export class ComfyUIClient extends RestClient<ComfyUIClientState, ComfyUIClientO
|
|
|
68
67
|
static override stateSchema = ComfyUIClientStateSchema;
|
|
69
68
|
static override optionsSchema = ComfyUIClientOptionsSchema;
|
|
70
69
|
|
|
71
|
-
|
|
70
|
+
static { Client.register(this, 'comfyui') }
|
|
72
71
|
|
|
73
|
-
|
|
74
|
-
container.clients.register("comfyui", ComfyUIClient);
|
|
75
|
-
return container;
|
|
76
|
-
}
|
|
72
|
+
private ws: WebSocket | null = null;
|
|
77
73
|
|
|
78
74
|
constructor(options: ComfyUIClientOptions, context: ContainerContext) {
|
|
79
75
|
super(
|
|
@@ -594,4 +590,4 @@ export class ComfyUIClient extends RestClient<ComfyUIClientState, ComfyUIClientO
|
|
|
594
590
|
}
|
|
595
591
|
}
|
|
596
592
|
|
|
597
|
-
export default
|
|
593
|
+
export default ComfyUIClient;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { ClientStateSchema, ClientOptionsSchema, ClientEventsSchema } from '@soederpop/luca/schemas/base.js'
|
|
3
|
-
import {
|
|
4
|
-
import type {
|
|
5
|
-
import type { ClientsInterface, ClientOptions } from "@soederpop/luca/client";
|
|
3
|
+
import { Client, RestClient } from "@soederpop/luca/client";
|
|
4
|
+
import type { ContainerContext } from "@soederpop/luca/container";
|
|
6
5
|
import type { AxiosRequestConfig } from 'axios'
|
|
7
6
|
|
|
8
7
|
declare module "@soederpop/luca/client" {
|
|
@@ -48,6 +47,7 @@ export type SynthesizeOptions = {
|
|
|
48
47
|
modelId?: string
|
|
49
48
|
outputFormat?: string
|
|
50
49
|
voiceSettings?: ElevenLabsVoiceSettings
|
|
50
|
+
disableCache?: boolean
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/**
|
|
@@ -72,11 +72,7 @@ export class ElevenLabsClient extends RestClient<ElevenLabsClientState, ElevenLa
|
|
|
72
72
|
static override optionsSchema = ElevenLabsClientOptionsSchema
|
|
73
73
|
static override eventsSchema = ElevenLabsClientEventsSchema
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
static override attach(container: Container & ClientsInterface, options?: any) {
|
|
77
|
-
container.clients.register("elevenlabs", ElevenLabsClient);
|
|
78
|
-
return container
|
|
79
|
-
}
|
|
75
|
+
static { Client.register(this, 'elevenlabs') }
|
|
80
76
|
|
|
81
77
|
override get initialState(): ElevenLabsClientState {
|
|
82
78
|
return {
|
|
@@ -238,8 +234,40 @@ export class ElevenLabsClient extends RestClient<ElevenLabsClientState, ElevenLa
|
|
|
238
234
|
}
|
|
239
235
|
}
|
|
240
236
|
|
|
241
|
-
|
|
237
|
+
// Check disk cache for previously synthesized audio
|
|
238
|
+
if (!options.disableCache) {
|
|
239
|
+
const { hashObject } = this.container.utils
|
|
240
|
+
const cacheKey = `elevenlabs:${hashObject({ text, voiceId, modelId, outputFormat, voiceSettings: options.voiceSettings })}`
|
|
241
|
+
const diskCache = this.container.feature('diskCache')
|
|
242
|
+
|
|
243
|
+
if (await diskCache.has(cacheKey)) {
|
|
244
|
+
console.log(`Cache Hit: ${cacheKey}`)
|
|
245
|
+
const cached = await diskCache.get(cacheKey)
|
|
246
|
+
const audioBuffer = Buffer.from(cached, 'base64')
|
|
247
|
+
|
|
248
|
+
this.emit('speech', {
|
|
249
|
+
voiceId,
|
|
250
|
+
text,
|
|
251
|
+
audioSize: audioBuffer.length,
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
return audioBuffer
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const audioBuffer = await this.fetchSpeech(voiceId, outputFormat, body, text.length)
|
|
258
|
+
await diskCache.set(cacheKey, audioBuffer.toString('base64'))
|
|
259
|
+
|
|
260
|
+
this.emit('speech', { voiceId, text, audioSize: audioBuffer.length })
|
|
261
|
+
return audioBuffer
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const audioBuffer = await this.fetchSpeech(voiceId, outputFormat, body, text.length)
|
|
265
|
+
this.emit('speech', { voiceId, text, audioSize: audioBuffer.length })
|
|
266
|
+
return audioBuffer
|
|
267
|
+
}
|
|
242
268
|
|
|
269
|
+
private async fetchSpeech(voiceId: string, outputFormat: string, body: Record<string, any>, charCount: number): Promise<Buffer> {
|
|
270
|
+
this.trackRequest(charCount)
|
|
243
271
|
await this.beforeRequest()
|
|
244
272
|
|
|
245
273
|
const response = await this.axios({
|
|
@@ -254,15 +282,7 @@ export class ElevenLabsClient extends RestClient<ElevenLabsClientState, ElevenLa
|
|
|
254
282
|
},
|
|
255
283
|
})
|
|
256
284
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
this.emit('speech', {
|
|
260
|
-
voiceId,
|
|
261
|
-
text,
|
|
262
|
-
audioSize: audioBuffer.length,
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
return audioBuffer
|
|
285
|
+
return Buffer.from(response.data)
|
|
266
286
|
}
|
|
267
287
|
|
|
268
288
|
/**
|
|
@@ -287,5 +307,5 @@ export class ElevenLabsClient extends RestClient<ElevenLabsClientState, ElevenLa
|
|
|
287
307
|
}
|
|
288
308
|
}
|
|
289
309
|
|
|
290
|
-
export default
|
|
310
|
+
export default ElevenLabsClient
|
|
291
311
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { ClientStateSchema, ClientOptionsSchema } from '@soederpop/luca/schemas/base.js'
|
|
3
|
-
import {
|
|
3
|
+
import { Client } from "@soederpop/luca/client";
|
|
4
4
|
import type { Container, ContainerContext } from "@soederpop/luca/container";
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
import OpenAI from "openai";
|
|
7
7
|
|
|
8
8
|
export const OpenAIClientStateSchema = ClientStateSchema.extend({
|
|
@@ -48,9 +48,7 @@ export class OpenAIClient extends Client<OpenAIClientState, OpenAIClientOptions>
|
|
|
48
48
|
static override stateSchema = OpenAIClientStateSchema
|
|
49
49
|
static override optionsSchema = OpenAIClientOptionsSchema
|
|
50
50
|
|
|
51
|
-
static
|
|
52
|
-
return container;
|
|
53
|
-
}
|
|
51
|
+
static { Client.register(this, 'openai') }
|
|
54
52
|
|
|
55
53
|
/** Initial state with zeroed token usage counters. */
|
|
56
54
|
override get initialState(): OpenAIClientState {
|
|
@@ -446,6 +444,4 @@ export class OpenAIClient extends Client<OpenAIClientState, OpenAIClientOptions>
|
|
|
446
444
|
}
|
|
447
445
|
}
|
|
448
446
|
|
|
449
|
-
clients.register("openai", OpenAIClient)
|
|
450
|
-
|
|
451
447
|
export default OpenAIClient;
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Client,
|
|
3
|
-
type ClientOptions,
|
|
4
|
-
type ClientsInterface,
|
|
5
|
-
clients,
|
|
6
3
|
} from "@soederpop/luca/client";
|
|
7
|
-
import type {
|
|
4
|
+
import type { ContainerContext } from "@soederpop/luca/container";
|
|
8
5
|
import { z } from "zod";
|
|
9
6
|
import {
|
|
10
7
|
ClientStateSchema,
|
|
@@ -141,16 +138,11 @@ export class SupabaseClient extends Client<
|
|
|
141
138
|
static override optionsSchema = SupabaseClientOptionsSchema;
|
|
142
139
|
static override eventsSchema = SupabaseClientEventsSchema;
|
|
143
140
|
|
|
141
|
+
static { Client.register(this, 'supabase') }
|
|
142
|
+
|
|
144
143
|
private _sdk!: SupabaseSDKClient<any, any>;
|
|
145
144
|
private _channels = new Map<string, RealtimeChannel>();
|
|
146
145
|
|
|
147
|
-
// @ts-ignore - required options (supabaseUrl, supabaseKey) widen beyond base ClientOptions
|
|
148
|
-
static attach(container: Container & ClientsInterface, options?: any) {
|
|
149
|
-
// @ts-ignore
|
|
150
|
-
container.clients.register("supabase", SupabaseClient);
|
|
151
|
-
return container;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
146
|
constructor(options: SupabaseClientOptions, context: ContainerContext) {
|
|
155
147
|
super(options, context);
|
|
156
148
|
|
|
@@ -362,5 +354,4 @@ export class SupabaseClient extends Client<
|
|
|
362
354
|
}
|
|
363
355
|
}
|
|
364
356
|
|
|
365
|
-
|
|
366
|
-
clients.register("supabase", SupabaseClient);
|
|
357
|
+
export default SupabaseClient;
|
package/src/commands/console.ts
CHANGED
|
@@ -63,9 +63,6 @@ async function evalBeforeRepl(evalArg: string, container: any, featureContext: R
|
|
|
63
63
|
code = transformed
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
const hasTopLevelAwait = /\bawait\b/.test(code)
|
|
67
|
-
code = hasTopLevelAwait ? `(async function() { ${code} })()` : code
|
|
68
|
-
|
|
69
66
|
await vm.run(code, shared)
|
|
70
67
|
Object.assign(shared, container.context)
|
|
71
68
|
} else {
|
package/src/commands/eval.ts
CHANGED
|
@@ -34,7 +34,7 @@ export default async function evalCommand(options: z.infer<typeof argsSchema>, c
|
|
|
34
34
|
const vm = container.feature('vm')
|
|
35
35
|
|
|
36
36
|
// HACK
|
|
37
|
-
Array(container.argv.enable).map((id) => {
|
|
37
|
+
Array(container.argv.enable).filter(Boolean).map((id) => {
|
|
38
38
|
container.feature(id, { ...container.argv, enable: true }).enable()
|
|
39
39
|
})
|
|
40
40
|
|
package/src/commands/index.ts
CHANGED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { commands } from '../command.js'
|
|
3
|
+
import { CommandOptionsSchema } from '../schemas/base.js'
|
|
4
|
+
import type { ContainerContext } from '../container.js'
|
|
5
|
+
import '../introspection/scan.js'
|
|
6
|
+
|
|
7
|
+
declare module '../command.js' {
|
|
8
|
+
interface AvailableCommands {
|
|
9
|
+
introspect: ReturnType<typeof commands.registerHandler>
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const argsSchema = CommandOptionsSchema.extend({
|
|
14
|
+
src: z.array(z.string()).optional().describe('Source directories to scan (default: auto-discover features/, clients/, servers/)'),
|
|
15
|
+
output: z.string().optional().describe('Output file path (default: features/introspection.generated.ts)'),
|
|
16
|
+
'dry-run': z.boolean().default(false).describe('Preview without writing'),
|
|
17
|
+
'include-private': z.boolean().default(false).describe('Include private methods in output'),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
async function introspect(options: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
21
|
+
const container = context.container as any
|
|
22
|
+
const { fs, paths } = container
|
|
23
|
+
|
|
24
|
+
// Detect if we're inside the luca library itself
|
|
25
|
+
const cwd = paths.cwd
|
|
26
|
+
const isLucaLibrary = fs.exists(paths.resolve('src/introspection/scan.ts'))
|
|
27
|
+
|
|
28
|
+
if (isLucaLibrary) {
|
|
29
|
+
// Inside luca itself — delegate to the multi-target update-introspection behavior
|
|
30
|
+
const { NodeContainer } = await import('../node/container.js')
|
|
31
|
+
const lucaContainer = new NodeContainer()
|
|
32
|
+
|
|
33
|
+
const targets = [
|
|
34
|
+
{
|
|
35
|
+
name: 'node',
|
|
36
|
+
src: ['src/node/features', 'src/servers', 'src/container.ts', 'src/node/container.ts'],
|
|
37
|
+
outputPath: 'src/introspection/generated.node.ts',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'web',
|
|
41
|
+
src: ['src/web/features', 'src/container.ts', 'src/web/container.ts'],
|
|
42
|
+
outputPath: 'src/introspection/generated.web.ts',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'agi',
|
|
46
|
+
src: ['src/node/features', 'src/servers', 'src/agi/features', 'src/container.ts', 'src/node/container.ts', 'src/agi/container.server.ts'],
|
|
47
|
+
outputPath: 'src/introspection/generated.agi.ts',
|
|
48
|
+
},
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
for (const target of targets) {
|
|
52
|
+
console.log(`\nGenerating ${target.name} introspection data...`)
|
|
53
|
+
console.log(` Sources: ${target.src.join(', ')}`)
|
|
54
|
+
console.log(` Output: ${target.outputPath}`)
|
|
55
|
+
|
|
56
|
+
const scanner = lucaContainer.feature('introspectionScanner', {
|
|
57
|
+
src: target.src,
|
|
58
|
+
outputPath: options['dry-run'] ? undefined : target.outputPath,
|
|
59
|
+
includePrivate: options['include-private'],
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
scanner.on('scanCompleted', (data: any) => {
|
|
63
|
+
console.log(` Found ${data.results} helpers in ${data.files} files (${data.duration}ms)`)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
await scanner.scan()
|
|
67
|
+
const script = await scanner.generateRegistryScript()
|
|
68
|
+
|
|
69
|
+
if (options['dry-run']) {
|
|
70
|
+
console.log(`\n--- ${target.outputPath} (dry run) ---`)
|
|
71
|
+
console.log(script.slice(0, 500) + (script.length > 500 ? '\n...' : ''))
|
|
72
|
+
} else {
|
|
73
|
+
console.log(` Wrote ${target.outputPath}`)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log('\nAll introspection data generated.')
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Project-local mode: auto-discover source directories
|
|
82
|
+
const defaultSrc = ['features', 'clients', 'servers']
|
|
83
|
+
.filter(dir => fs.exists(paths.resolve(dir)))
|
|
84
|
+
|
|
85
|
+
// Also scan container.ts if it exists at project root
|
|
86
|
+
const containerFile = paths.resolve('container.ts')
|
|
87
|
+
if (fs.exists(containerFile)) defaultSrc.push('container.ts')
|
|
88
|
+
|
|
89
|
+
const src = options.src || defaultSrc
|
|
90
|
+
|
|
91
|
+
if (src.length === 0) {
|
|
92
|
+
console.log('No source directories found to scan. Use --src to specify directories.')
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const outputPath = options.output || 'features/introspection.generated.ts'
|
|
97
|
+
const importSource = '@soederpop/luca/introspection'
|
|
98
|
+
|
|
99
|
+
console.log(`Scanning: ${src.join(', ')}`)
|
|
100
|
+
console.log(`Output: ${outputPath}`)
|
|
101
|
+
|
|
102
|
+
const scanner = container.feature('introspectionScanner', {
|
|
103
|
+
src,
|
|
104
|
+
outputPath: options['dry-run'] ? undefined : outputPath,
|
|
105
|
+
includePrivate: options['include-private'],
|
|
106
|
+
importSource,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
scanner.on('scanCompleted', (data: any) => {
|
|
110
|
+
console.log(`Found ${data.results} helpers in ${data.files} files (${data.duration}ms)`)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
await scanner.scan()
|
|
114
|
+
const script = await scanner.generateRegistryScript()
|
|
115
|
+
|
|
116
|
+
if (options['dry-run']) {
|
|
117
|
+
console.log(`\n--- ${outputPath} (dry run) ---`)
|
|
118
|
+
console.log(script)
|
|
119
|
+
} else {
|
|
120
|
+
console.log(`Wrote ${outputPath}`)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
commands.registerHandler('introspect', {
|
|
125
|
+
description: 'Generate introspection metadata from source. Works in any luca project.',
|
|
126
|
+
argsSchema,
|
|
127
|
+
handler: introspect,
|
|
128
|
+
})
|
package/src/commands/prompt.ts
CHANGED
|
@@ -628,10 +628,7 @@ async function executePromptFile(resolvedPath: string, container: any): Promise<
|
|
|
628
628
|
code = transformed
|
|
629
629
|
}
|
|
630
630
|
|
|
631
|
-
|
|
632
|
-
if (hasTopLevelAwait) code = `(async function() { ${code} })()`
|
|
633
|
-
|
|
634
|
-
await vm.run(code, shared)
|
|
631
|
+
await vm.run(code, shared)
|
|
635
632
|
Object.assign(shared, container.context)
|
|
636
633
|
|
|
637
634
|
if (capturedLines.length) {
|
package/src/commands/run.ts
CHANGED
|
@@ -96,12 +96,7 @@ async function runMarkdown(scriptPath: string, options: z.infer<typeof argsSchem
|
|
|
96
96
|
const keysBefore = new Set(Object.keys(shared))
|
|
97
97
|
const { code: transformed } = esbuild.transformSync(source, { loader: 'tsx', format: 'cjs' })
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
const wrapped = hasTopLevelAwait
|
|
101
|
-
? `(async function() { ${transformed} })()`
|
|
102
|
-
: transformed
|
|
103
|
-
|
|
104
|
-
await vm.run(wrapped, shared)
|
|
99
|
+
await vm.run(transformed, shared)
|
|
105
100
|
|
|
106
101
|
// auto-register any new functions as blocks
|
|
107
102
|
for (const key of Object.keys(shared)) {
|
|
@@ -165,11 +160,6 @@ async function runMarkdown(scriptPath: string, options: z.infer<typeof argsSchem
|
|
|
165
160
|
code = transformed
|
|
166
161
|
}
|
|
167
162
|
|
|
168
|
-
const hasTopLevelAwait = /\bawait\b/.test(code)
|
|
169
|
-
code = hasTopLevelAwait
|
|
170
|
-
? `(async function() { ${code} })()`
|
|
171
|
-
: code
|
|
172
|
-
|
|
173
163
|
await vm.run(code, shared)
|
|
174
164
|
|
|
175
165
|
// if we enabled any features, they will be in the context object
|
|
@@ -186,13 +176,16 @@ async function runMarkdown(scriptPath: string, options: z.infer<typeof argsSchem
|
|
|
186
176
|
async function runScript(scriptPath: string, context: ContainerContext) {
|
|
187
177
|
const container = context.container as any
|
|
188
178
|
|
|
189
|
-
const { exitCode, stderr } = await container.proc.
|
|
179
|
+
const { exitCode, stderr } = await container.proc.execAndCapture(`bun run ${scriptPath}`, {
|
|
180
|
+
onOutput: (data: string) => process.stdout.write(data),
|
|
181
|
+
onError: (data: string) => process.stderr.write(data),
|
|
182
|
+
})
|
|
190
183
|
|
|
191
184
|
if (exitCode === 0) return
|
|
192
185
|
|
|
193
186
|
console.error(`\nScript failed with exit code ${exitCode}.\n`)
|
|
194
187
|
if (stderr.length) {
|
|
195
|
-
console.error(stderr
|
|
188
|
+
console.error(stderr)
|
|
196
189
|
}
|
|
197
190
|
}
|
|
198
191
|
|
|
@@ -154,19 +154,7 @@ export default async function mcpSandbox(options: z.infer<typeof argsSchema>, co
|
|
|
154
154
|
}),
|
|
155
155
|
handler: async (args) => {
|
|
156
156
|
try {
|
|
157
|
-
|
|
158
|
-
// Try to return the last expression's value by prepending `return` to the last statement.
|
|
159
|
-
let code = args.code
|
|
160
|
-
if (/\bawait\b/.test(code) && !/^\s*\(?\s*async\b/.test(code)) {
|
|
161
|
-
const lines = code.split('\n')
|
|
162
|
-
const lastLine = lines[lines.length - 1]
|
|
163
|
-
// If the last line doesn't start with a keyword that can't be returned, add return
|
|
164
|
-
if (!/^\s*(var|let|const|if|for|while|switch|try|throw|class|function)\b/.test(lastLine)) {
|
|
165
|
-
lines[lines.length - 1] = `return ${lastLine}`
|
|
166
|
-
}
|
|
167
|
-
code = `(async () => { ${lines.join('\n')} })()`
|
|
168
|
-
}
|
|
169
|
-
const result = await vmFeature.run(code, sandboxContext)
|
|
157
|
+
const result = await vmFeature.run(args.code, sandboxContext)
|
|
170
158
|
|
|
171
159
|
let text: string
|
|
172
160
|
if (result === undefined) {
|
package/src/feature.ts
CHANGED
|
@@ -22,6 +22,9 @@ export abstract class Feature<T extends FeatureState = FeatureState, K extends F
|
|
|
22
22
|
static override optionsSchema = FeatureOptionsSchema
|
|
23
23
|
static override eventsSchema = FeatureEventsSchema
|
|
24
24
|
|
|
25
|
+
/** Self-register a Feature subclass from a static initialization block. */
|
|
26
|
+
static register: (SubClass: typeof Feature, id?: string) => typeof Feature
|
|
27
|
+
|
|
25
28
|
get shortcut() {
|
|
26
29
|
return (this.constructor as any).shortcut as string
|
|
27
30
|
}
|
|
@@ -67,9 +70,49 @@ export abstract class Feature<T extends FeatureState = FeatureState, K extends F
|
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
export class FeaturesRegistry extends Registry<Feature<any, any>> {
|
|
73
|
+
export class FeaturesRegistry extends Registry<Feature<any, any>> {
|
|
71
74
|
override scope = "features"
|
|
72
75
|
override baseClass = Feature as any
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
export const features = new FeaturesRegistry()
|
|
78
|
+
export const features = new FeaturesRegistry()
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Self-register a Feature subclass from a static initialization block.
|
|
82
|
+
* IMPORTANT: Place the static block AFTER all static override declarations
|
|
83
|
+
* so schemas, envVars, and other metadata are set before interceptRegistration fires.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```typescript
|
|
87
|
+
* export default class DNS extends Feature<DnsState, DnsOptions> {
|
|
88
|
+
* static override stateSchema = DnsStateSchema
|
|
89
|
+
* static override optionsSchema = DnsOptionsSchema
|
|
90
|
+
* static { Feature.register(this, 'dns') } // must come last
|
|
91
|
+
* }
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
Feature.register = function registerFeature(
|
|
95
|
+
SubClass: typeof Feature,
|
|
96
|
+
id?: string,
|
|
97
|
+
) {
|
|
98
|
+
const registryId = id ?? SubClass.name[0]!.toLowerCase() + SubClass.name.slice(1)
|
|
99
|
+
|
|
100
|
+
// Auto-set shortcut if not explicitly overridden on this class
|
|
101
|
+
if (!Object.getOwnPropertyDescriptor(SubClass, 'shortcut')?.value ||
|
|
102
|
+
(SubClass as any).shortcut === 'unspecified') {
|
|
103
|
+
;(SubClass as any).shortcut = `features.${registryId}` as const
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Register in the features registry (interceptRegistration sees all statics above)
|
|
107
|
+
features.register(registryId, SubClass as any)
|
|
108
|
+
|
|
109
|
+
// Generate default attach() if not explicitly overridden on this class
|
|
110
|
+
if (!Object.getOwnPropertyDescriptor(SubClass, 'attach')) {
|
|
111
|
+
;(SubClass as any).attach = (container: any) => {
|
|
112
|
+
features.register(registryId, SubClass as any)
|
|
113
|
+
return container
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return SubClass
|
|
118
|
+
}
|