@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
package/src/command.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { Helper } from './helper.js'
|
|
2
2
|
import type { Container, ContainerContext } from './container.js'
|
|
3
3
|
import { Registry } from './registry.js'
|
|
4
|
-
import { CommandStateSchema, CommandOptionsSchema, CommandEventsSchema } from './schemas/base.js'
|
|
4
|
+
import { CommandStateSchema, CommandOptionsSchema, CommandEventsSchema, type DispatchSource, type CommandRunResult } from './schemas/base.js'
|
|
5
5
|
import { z } from 'zod'
|
|
6
6
|
import { join } from 'path'
|
|
7
|
+
import { graftModule, isNativeHelperClass } from './graft.js'
|
|
8
|
+
|
|
9
|
+
export type { DispatchSource, CommandRunResult }
|
|
7
10
|
|
|
8
11
|
export type CommandState = z.infer<typeof CommandStateSchema>
|
|
9
12
|
export type CommandOptions = z.infer<typeof CommandOptionsSchema>
|
|
@@ -22,6 +25,24 @@ export interface CommandsInterface {
|
|
|
22
25
|
|
|
23
26
|
export type CommandHandler<T = any> = (options: T, context: ContainerContext) => Promise<void>
|
|
24
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Type helper for module-augmentation of AvailableCommands when using the
|
|
30
|
+
* SimpleCommand (module-based) pattern instead of a full class.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* declare module '@soederpop/luca' {
|
|
35
|
+
* interface AvailableCommands {
|
|
36
|
+
* serve: SimpleCommand<typeof argsSchema>
|
|
37
|
+
* }
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export type SimpleCommand<Schema extends z.ZodType = z.ZodType> = typeof Command & {
|
|
42
|
+
argsSchema: Schema
|
|
43
|
+
positionals: string[]
|
|
44
|
+
}
|
|
45
|
+
|
|
25
46
|
export class Command<
|
|
26
47
|
T extends CommandState = CommandState,
|
|
27
48
|
K extends CommandOptions = CommandOptions
|
|
@@ -34,35 +55,73 @@ export class Command<
|
|
|
34
55
|
|
|
35
56
|
static commandDescription: string = ''
|
|
36
57
|
static argsSchema: z.ZodType = CommandOptionsSchema
|
|
58
|
+
static positionals: string[] = []
|
|
59
|
+
|
|
60
|
+
/** Self-register a Command subclass from a static initialization block. */
|
|
61
|
+
static register: (SubClass: typeof Command, id?: string) => typeof Command
|
|
37
62
|
|
|
38
63
|
override get initialState(): T {
|
|
39
64
|
return ({ running: false } as unknown) as T
|
|
40
65
|
}
|
|
41
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Parse raw CLI argv against this command's argsSchema.
|
|
69
|
+
* Kept for backward compatibility with built-in commands.
|
|
70
|
+
*/
|
|
42
71
|
parseArgs(): any {
|
|
43
72
|
const schema = (this.constructor as typeof Command).argsSchema
|
|
44
73
|
return schema.parse(this.container.options)
|
|
45
74
|
}
|
|
46
75
|
|
|
47
|
-
|
|
48
|
-
|
|
76
|
+
/**
|
|
77
|
+
* The user-defined command payload. Override this in class-based commands,
|
|
78
|
+
* or export a `run` function in module-based commands.
|
|
79
|
+
*
|
|
80
|
+
* Receives clean, named args (positionals already mapped) and the container context.
|
|
81
|
+
*/
|
|
82
|
+
async run(_args: any, _context: ContainerContext): Promise<void> {
|
|
83
|
+
// override in subclass or grafted from module export
|
|
49
84
|
}
|
|
50
85
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Internal dispatch normalizer. Maps positionals for CLI, passes through named
|
|
88
|
+
* args for headless, validates via argsSchema, captures output when headless.
|
|
89
|
+
*
|
|
90
|
+
* @param rawInput - Named args object. When omitted, reads from container.argv.
|
|
91
|
+
* @param source - Override the dispatch source. Defaults to constructor option.
|
|
92
|
+
*/
|
|
93
|
+
async execute(rawInput?: Record<string, any>, source?: DispatchSource): Promise<CommandRunResult | void> {
|
|
94
|
+
const dispatchSource = source ?? (this._options as any).dispatchSource ?? 'cli'
|
|
95
|
+
const Cls = this.constructor as typeof Command
|
|
96
|
+
|
|
97
|
+
// Help intercept (CLI only) — must happen before schema validation
|
|
98
|
+
// so `luca somecommand --help` works even without required args
|
|
99
|
+
if (dispatchSource === 'cli' && (this.container as any).argv?.help) {
|
|
54
100
|
const { formatCommandHelp } = await import('./commands/help.js')
|
|
55
101
|
const ui = (this.container as any).feature('ui')
|
|
56
|
-
const name =
|
|
102
|
+
const name = Cls.shortcut?.replace('commands.', '') || 'unknown'
|
|
57
103
|
console.log(formatCommandHelp(name, this.constructor, ui.colors))
|
|
58
104
|
return
|
|
59
105
|
}
|
|
60
106
|
|
|
107
|
+
// Build named args from raw input
|
|
108
|
+
const raw = rawInput ?? { ...(this.container as any).argv }
|
|
109
|
+
const named = this._normalizeInput(raw, dispatchSource)
|
|
110
|
+
|
|
111
|
+
// Validate against argsSchema
|
|
112
|
+
const parsed = Cls.argsSchema.parse(named)
|
|
113
|
+
|
|
114
|
+
// For headless dispatch, capture stdout/stderr
|
|
115
|
+
if (dispatchSource !== 'cli') {
|
|
116
|
+
return this._runCaptured(parsed)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// CLI path — lifecycle events + run
|
|
61
120
|
this.state.set('running', true)
|
|
62
121
|
this.emit('started')
|
|
63
122
|
|
|
64
123
|
try {
|
|
65
|
-
await this.
|
|
124
|
+
await this.run(parsed, this.context)
|
|
66
125
|
this.state.set('running', false)
|
|
67
126
|
this.state.set('exitCode', 0)
|
|
68
127
|
this.emit('completed', 0)
|
|
@@ -74,6 +133,109 @@ export class Command<
|
|
|
74
133
|
}
|
|
75
134
|
}
|
|
76
135
|
|
|
136
|
+
/**
|
|
137
|
+
* The public entry point for dispatching a command.
|
|
138
|
+
* Called by the CLI and other dispatch surfaces.
|
|
139
|
+
*/
|
|
140
|
+
async dispatch(rawInput?: Record<string, any>, source?: DispatchSource): Promise<CommandRunResult | void> {
|
|
141
|
+
return this.execute(rawInput, source)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Map CLI positional args to named fields based on the command's positionals declaration.
|
|
146
|
+
* For non-CLI dispatch, args are already named — pass through.
|
|
147
|
+
*/
|
|
148
|
+
private _normalizeInput(raw: Record<string, any>, source: DispatchSource): Record<string, any> {
|
|
149
|
+
if (source !== 'cli') return raw
|
|
150
|
+
|
|
151
|
+
const Cls = this.constructor as typeof Command
|
|
152
|
+
const positionals = Cls.positionals
|
|
153
|
+
if (!positionals.length) return raw
|
|
154
|
+
|
|
155
|
+
const result = { ...raw }
|
|
156
|
+
|
|
157
|
+
// Map raw._[1], raw._[2], etc. (skipping _[0] which is command name) to named fields
|
|
158
|
+
const posArgs: string[] = (raw._ || []).slice(1)
|
|
159
|
+
for (let i = 0; i < positionals.length; i++) {
|
|
160
|
+
const name = positionals[i]!
|
|
161
|
+
if (result[name] !== undefined) continue
|
|
162
|
+
if (posArgs[i] === undefined) continue
|
|
163
|
+
|
|
164
|
+
// Last positional collects all remaining args if the schema expects an array
|
|
165
|
+
if (i === positionals.length - 1 && posArgs.length > positionals.length) {
|
|
166
|
+
const isArray = this._schemaExpectsArray(Cls.argsSchema, name)
|
|
167
|
+
if (isArray) {
|
|
168
|
+
result[name] = posArgs.slice(i)
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
result[name] = posArgs[i]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return result
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Check whether a Zod schema expects an array type for a given field.
|
|
181
|
+
* Unwraps ZodObject → ZodOptional/ZodDefault/ZodNullable → ZodArray.
|
|
182
|
+
*/
|
|
183
|
+
private _schemaExpectsArray(schema: z.ZodType, field: string): boolean {
|
|
184
|
+
try {
|
|
185
|
+
const shape = typeof (schema as any)?._def?.shape === 'function'
|
|
186
|
+
? (schema as any)._def.shape()
|
|
187
|
+
: (schema as any)?._def?.shape
|
|
188
|
+
if (!shape || !shape[field]) return false
|
|
189
|
+
let inner = shape[field]
|
|
190
|
+
// Unwrap wrappers (optional, default, nullable)
|
|
191
|
+
while (inner) {
|
|
192
|
+
if (inner instanceof z.ZodArray) return true
|
|
193
|
+
if (inner._def?.innerType) { inner = inner._def.innerType; continue }
|
|
194
|
+
if (inner._def?.schema) { inner = inner._def.schema; continue }
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
return false
|
|
198
|
+
} catch {
|
|
199
|
+
return false
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Run the command with stdout/stderr capture for headless dispatch.
|
|
205
|
+
* Returns a CommandRunResult with captured output.
|
|
206
|
+
*
|
|
207
|
+
* NOTE: Uses global console mutation — not safe for concurrent headless dispatches.
|
|
208
|
+
* A future iteration should pass a logger through context instead.
|
|
209
|
+
*/
|
|
210
|
+
private async _runCaptured(args: any): Promise<CommandRunResult> {
|
|
211
|
+
const captured = { stdout: '', stderr: '' }
|
|
212
|
+
const origLog = console.log.bind(console)
|
|
213
|
+
const origErr = console.error.bind(console)
|
|
214
|
+
|
|
215
|
+
console.log = (...a: any[]) => { captured.stdout += a.join(' ') + '\n' }
|
|
216
|
+
console.error = (...a: any[]) => { captured.stderr += a.join(' ') + '\n' }
|
|
217
|
+
|
|
218
|
+
let exitCode = 0
|
|
219
|
+
try {
|
|
220
|
+
this.state.set('running', true)
|
|
221
|
+
this.emit('started')
|
|
222
|
+
await this.run(args, this.context)
|
|
223
|
+
this.state.set('exitCode', 0)
|
|
224
|
+
this.emit('completed', 0)
|
|
225
|
+
} catch (err: any) {
|
|
226
|
+
exitCode = 1
|
|
227
|
+
this.state.set('exitCode', 1)
|
|
228
|
+
this.emit('failed', err)
|
|
229
|
+
captured.stderr += (err?.message || String(err)) + '\n'
|
|
230
|
+
} finally {
|
|
231
|
+
console.log = origLog
|
|
232
|
+
console.error = origErr
|
|
233
|
+
this.state.set('running', false)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return { exitCode, stdout: captured.stdout, stderr: captured.stderr }
|
|
237
|
+
}
|
|
238
|
+
|
|
77
239
|
static attach(container: Container<any> & CommandsInterface) {
|
|
78
240
|
container.commands = commands
|
|
79
241
|
|
|
@@ -104,6 +266,10 @@ export class CommandsRegistry extends Registry<Command<any>> {
|
|
|
104
266
|
override scope = 'commands'
|
|
105
267
|
override baseClass = Command as any
|
|
106
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Internal: register a command from a handler function.
|
|
271
|
+
* Used by built-in commands. External commands should use the module export pattern.
|
|
272
|
+
*/
|
|
107
273
|
registerHandler<T = any>(
|
|
108
274
|
name: string,
|
|
109
275
|
opts: {
|
|
@@ -112,27 +278,27 @@ export class CommandsRegistry extends Registry<Command<any>> {
|
|
|
112
278
|
handler: CommandHandler<T>
|
|
113
279
|
},
|
|
114
280
|
) {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
override async execute() {
|
|
127
|
-
await handler(this.parseArgs(), this.context)
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
Object.defineProperty(CommandClass, 'name', { value: `${name}Command` })
|
|
281
|
+
const CommandClass = graftModule(
|
|
282
|
+
Command as any,
|
|
283
|
+
{
|
|
284
|
+
description: opts.description,
|
|
285
|
+
argsSchema: opts.argsSchema,
|
|
286
|
+
handler: opts.handler,
|
|
287
|
+
},
|
|
288
|
+
name,
|
|
289
|
+
'commands',
|
|
290
|
+
) as any
|
|
132
291
|
|
|
133
|
-
return this.register(name, CommandClass
|
|
292
|
+
return this.register(name, CommandClass)
|
|
134
293
|
}
|
|
135
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Discover and register commands from a directory.
|
|
297
|
+
* Detection order:
|
|
298
|
+
* 1. Default export is a class extending Command → register directly
|
|
299
|
+
* 2. Module exports a `run` function → graft as SimpleCommand
|
|
300
|
+
* 3. Module exports a `handler` function → legacy graft
|
|
301
|
+
*/
|
|
136
302
|
async discover(options: { directory: string }) {
|
|
137
303
|
const { Glob } = globalThis.Bun || (await import('bun'))
|
|
138
304
|
const glob = new Glob('*.ts')
|
|
@@ -140,19 +306,53 @@ export class CommandsRegistry extends Registry<Command<any>> {
|
|
|
140
306
|
for await (const file of glob.scan({ cwd: options.directory })) {
|
|
141
307
|
if (file === 'index.ts') continue
|
|
142
308
|
|
|
309
|
+
const name = file.replace(/\.ts$/, '')
|
|
310
|
+
if (this.has(name)) continue
|
|
311
|
+
|
|
143
312
|
const mod = await import(join(options.directory, file))
|
|
313
|
+
|
|
314
|
+
// 1. Class-based: default export extends Command
|
|
315
|
+
if (isNativeHelperClass(mod.default, Command)) {
|
|
316
|
+
const ExportedClass = mod.default
|
|
317
|
+
if (!ExportedClass.shortcut || ExportedClass.shortcut === 'commands.base') {
|
|
318
|
+
ExportedClass.shortcut = `commands.${name}`
|
|
319
|
+
}
|
|
320
|
+
if (!ExportedClass.commandDescription && ExportedClass.description) {
|
|
321
|
+
ExportedClass.commandDescription = ExportedClass.description
|
|
322
|
+
}
|
|
323
|
+
this.register(name, ExportedClass)
|
|
324
|
+
continue
|
|
325
|
+
}
|
|
326
|
+
|
|
144
327
|
const commandModule = mod.default || mod
|
|
145
328
|
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
329
|
+
// 2. Module-based with `run` export (new SimpleCommand pattern)
|
|
330
|
+
if (typeof commandModule.run === 'function') {
|
|
331
|
+
const Grafted = graftModule(Command as any, commandModule, name, 'commands')
|
|
332
|
+
this.register(name, Grafted as any)
|
|
333
|
+
continue
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// 3. Legacy: `handler` export
|
|
337
|
+
if (typeof commandModule.handler === 'function') {
|
|
338
|
+
const Grafted = graftModule(Command as any, {
|
|
339
|
+
description: commandModule.description,
|
|
153
340
|
argsSchema: commandModule.argsSchema,
|
|
154
341
|
handler: commandModule.handler,
|
|
155
|
-
})
|
|
342
|
+
}, name, 'commands')
|
|
343
|
+
this.register(name, Grafted as any)
|
|
344
|
+
continue
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// 4. Plain default-exported function: export default async function name(options, context)
|
|
348
|
+
if (typeof mod.default === 'function' && !isNativeHelperClass(mod.default, Command)) {
|
|
349
|
+
const Grafted = graftModule(Command as any, {
|
|
350
|
+
description: mod.description || '',
|
|
351
|
+
argsSchema: mod.argsSchema,
|
|
352
|
+
positionals: mod.positionals,
|
|
353
|
+
handler: mod.default,
|
|
354
|
+
}, name, 'commands')
|
|
355
|
+
this.register(name, Grafted as any)
|
|
156
356
|
}
|
|
157
357
|
}
|
|
158
358
|
}
|
|
@@ -161,4 +361,51 @@ export class CommandsRegistry extends Registry<Command<any>> {
|
|
|
161
361
|
export const commands = new CommandsRegistry()
|
|
162
362
|
export const helperCache = new Map()
|
|
163
363
|
|
|
364
|
+
/**
|
|
365
|
+
* Self-register a Command subclass from a static initialization block.
|
|
366
|
+
* Mirrors Feature.register / Client.register / Server.register pattern.
|
|
367
|
+
*
|
|
368
|
+
* @example
|
|
369
|
+
* ```typescript
|
|
370
|
+
* export class DeployCommand extends Command {
|
|
371
|
+
* static override description = 'Deploy to production'
|
|
372
|
+
* static override argsSchema = deployArgsSchema
|
|
373
|
+
* static override positionals = ['environment']
|
|
374
|
+
* static { Command.register(this, 'deploy') }
|
|
375
|
+
*
|
|
376
|
+
* override async run(args, context) { ... }
|
|
377
|
+
* }
|
|
378
|
+
* ```
|
|
379
|
+
*/
|
|
380
|
+
Command.register = function registerCommand(
|
|
381
|
+
SubClass: typeof Command,
|
|
382
|
+
id?: string,
|
|
383
|
+
) {
|
|
384
|
+
const registryId = id ?? (SubClass.name
|
|
385
|
+
? SubClass.name[0]!.toLowerCase() + SubClass.name.slice(1).replace(/Command$/, '')
|
|
386
|
+
: `command_${Date.now()}`)
|
|
387
|
+
|
|
388
|
+
if (!Object.getOwnPropertyDescriptor(SubClass, 'shortcut')?.value ||
|
|
389
|
+
(SubClass as any).shortcut === 'commands.base') {
|
|
390
|
+
;(SubClass as any).shortcut = `commands.${registryId}`
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (!(SubClass as any).commandDescription && (SubClass as any).description) {
|
|
394
|
+
;(SubClass as any).commandDescription = (SubClass as any).description
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
commands.register(registryId, SubClass as any)
|
|
398
|
+
|
|
399
|
+
if (!Object.getOwnPropertyDescriptor(SubClass, 'attach')) {
|
|
400
|
+
;(SubClass as any).attach = (container: any) => {
|
|
401
|
+
commands.register(registryId, SubClass as any)
|
|
402
|
+
return container
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return SubClass
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export { graftModule, isNativeHelperClass } from './graft.js'
|
|
410
|
+
|
|
164
411
|
export default Command
|
|
@@ -0,0 +1,185 @@
|
|
|
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 { bootstrapFiles, bootstrapTemplates } from '../bootstrap/generated.js'
|
|
6
|
+
import { apiDocs } from './save-api-docs.js'
|
|
7
|
+
import { generateScaffold } from '../scaffolds/template.js'
|
|
8
|
+
|
|
9
|
+
declare module '../command.js' {
|
|
10
|
+
interface AvailableCommands {
|
|
11
|
+
bootstrap: ReturnType<typeof commands.registerHandler>
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const argsSchema = CommandOptionsSchema.extend({
|
|
16
|
+
output: z.string().default('.').describe('Output folder path (defaults to cwd)'),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
async function bootstrap(options: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
20
|
+
const { container } = context
|
|
21
|
+
const args = container.argv._ as string[]
|
|
22
|
+
const outputDir = container.paths.resolve(args[1] || options.output)
|
|
23
|
+
const fs = container.feature('fs')
|
|
24
|
+
const ui = container.feature('ui')
|
|
25
|
+
const proc = container.feature('proc')
|
|
26
|
+
|
|
27
|
+
await fs.ensureFolder(outputDir)
|
|
28
|
+
const mkPath = (...segments: string[]) => container.paths.resolve(outputDir, ...segments)
|
|
29
|
+
|
|
30
|
+
ui.print.cyan('\n luca bootstrap\n')
|
|
31
|
+
|
|
32
|
+
// ── Check for AI coding tools ──────────────────────────────────
|
|
33
|
+
await checkToolAvailability(ui, proc)
|
|
34
|
+
|
|
35
|
+
// ── 1. .env (only if missing) ──────────────────────────────────
|
|
36
|
+
const envPath = mkPath('.env')
|
|
37
|
+
if (!fs.exists(envPath)) {
|
|
38
|
+
await writeFile(fs, ui, envPath, '', '.env')
|
|
39
|
+
} else {
|
|
40
|
+
ui.print.dim(' .env already exists, skipping')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── 2. CLAUDE.md ───────────────────────────────────────────────
|
|
44
|
+
await writeFile(fs, ui, mkPath('CLAUDE.md'), bootstrapFiles['CLAUDE'] || '', 'CLAUDE.md')
|
|
45
|
+
|
|
46
|
+
// ── 3. .claude/skills/luca-framework/ ──────────────────────────
|
|
47
|
+
const skillDir = mkPath('.claude', 'skills', 'luca-framework')
|
|
48
|
+
await fs.ensureFolder(skillDir)
|
|
49
|
+
await writeFile(fs, ui, container.paths.resolve(skillDir, 'SKILL.md'), bootstrapFiles['SKILL'] || '', '.claude/skills/luca-framework/SKILL.md')
|
|
50
|
+
|
|
51
|
+
ui.print.cyan(' Generating API docs...')
|
|
52
|
+
const apiDocsPath = container.paths.resolve(skillDir, 'references', 'api-docs')
|
|
53
|
+
await apiDocs({ _: [], outputPath: apiDocsPath }, context)
|
|
54
|
+
|
|
55
|
+
// ── 4. docs/ folder ────────────────────────────────────────────
|
|
56
|
+
await fs.ensureFolder(mkPath('docs'))
|
|
57
|
+
await writeFile(fs, ui, mkPath('docs', 'models.ts'), bootstrapTemplates['docs-models'] || '', 'docs/models.ts')
|
|
58
|
+
await writeFile(fs, ui, mkPath('docs', 'README.md'), bootstrapTemplates['docs-readme'] || '', 'docs/README.md')
|
|
59
|
+
|
|
60
|
+
// ── 5. commands/about.ts ────────────────────────────────────────
|
|
61
|
+
await fs.ensureFolder(mkPath('commands'))
|
|
62
|
+
await writeFile(fs, ui, mkPath('commands', 'about.ts'), bootstrapTemplates['about-command'] || '', 'commands/about.ts')
|
|
63
|
+
|
|
64
|
+
// ── 6. features/example.ts (scaffold-based) ────────────────────
|
|
65
|
+
await fs.ensureFolder(mkPath('features'))
|
|
66
|
+
const featureCode = generateScaffold('feature', 'example', 'An example feature demonstrating the luca feature pattern')
|
|
67
|
+
|| bootstrapTemplates['example-feature'] || ''
|
|
68
|
+
await writeFile(fs, ui, mkPath('features', 'example.ts'), featureCode, 'features/example.ts')
|
|
69
|
+
|
|
70
|
+
// ── 7. endpoints/health.ts ─────────────────────────────────────
|
|
71
|
+
await fs.ensureFolder(mkPath('endpoints'))
|
|
72
|
+
await writeFile(fs, ui, mkPath('endpoints', 'health.ts'), bootstrapTemplates['health-endpoint'] || '', 'endpoints/health.ts')
|
|
73
|
+
|
|
74
|
+
// ── 8. luca.cli.ts ─────────────────────────────────────────────
|
|
75
|
+
await writeFile(fs, ui, mkPath('luca.cli.ts'), bootstrapTemplates['luca-cli'] || '', 'luca.cli.ts')
|
|
76
|
+
|
|
77
|
+
// ── 9. RUNME.md ────────────────────────────────────────────────
|
|
78
|
+
await writeFile(fs, ui, mkPath('RUNME.md'), bootstrapTemplates['runme'] || '', 'RUNME.md')
|
|
79
|
+
|
|
80
|
+
// ── 10. .claude/settings.json (permissions for AI coding tools) ──
|
|
81
|
+
const settingsPath = mkPath('.claude', 'settings.json')
|
|
82
|
+
const claudeSettings = {
|
|
83
|
+
permissions: {
|
|
84
|
+
allow: [
|
|
85
|
+
'Bash(luca *)',
|
|
86
|
+
'Bash(bun run *)',
|
|
87
|
+
'Bash(bun test *)',
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!fs.exists(settingsPath)) {
|
|
93
|
+
await fs.ensureFolder(mkPath('.claude'))
|
|
94
|
+
await writeFile(fs, ui, settingsPath, JSON.stringify(claudeSettings, null, 2) + '\n', '.claude/settings.json')
|
|
95
|
+
} else {
|
|
96
|
+
// Merge luca permissions into existing settings
|
|
97
|
+
try {
|
|
98
|
+
const existing = JSON.parse(fs.readFile(settingsPath) as string)
|
|
99
|
+
const perms = existing.permissions || {}
|
|
100
|
+
const allow = new Set(perms.allow || [])
|
|
101
|
+
for (const rule of claudeSettings.permissions.allow) {
|
|
102
|
+
allow.add(rule)
|
|
103
|
+
}
|
|
104
|
+
existing.permissions = { ...perms, allow: [...allow] }
|
|
105
|
+
await writeFile(fs, ui, settingsPath, JSON.stringify(existing, null, 2) + '\n', '.claude/settings.json (merged)')
|
|
106
|
+
} catch {
|
|
107
|
+
ui.print.yellow(' ⚠ Could not parse existing .claude/settings.json, skipping merge')
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Summary ────────────────────────────────────────────────────
|
|
112
|
+
ui.print('')
|
|
113
|
+
ui.print.green(' ✓ Bootstrap complete!\n')
|
|
114
|
+
ui.print(' Your project is ready. Here\'s what to try:\n')
|
|
115
|
+
ui.print(' luca — see available commands')
|
|
116
|
+
ui.print(' luca about — project info + discovered helpers')
|
|
117
|
+
ui.print(' luca serve — start the API server (try /api/health)')
|
|
118
|
+
ui.print(' luca describe fs — learn about any built-in feature')
|
|
119
|
+
ui.print(' luca RUNME — run the interactive markdown demo')
|
|
120
|
+
ui.print('')
|
|
121
|
+
ui.print(' Need to build something? Use scaffold:\n')
|
|
122
|
+
ui.print(' luca scaffold command deploy — add a CLI command')
|
|
123
|
+
ui.print(' luca scaffold feature cache — add a container feature')
|
|
124
|
+
ui.print(' luca scaffold endpoint users — add a REST route')
|
|
125
|
+
ui.print(' luca scaffold client github — add an API client')
|
|
126
|
+
ui.print(' luca scaffold server mqtt — add a server')
|
|
127
|
+
ui.print('')
|
|
128
|
+
ui.print.dim(' Run luca scaffold <type> --tutorial for a full guide on any type')
|
|
129
|
+
ui.print('')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
async function writeFile(fs: any, ui: any, path: string, content: string, label: string) {
|
|
135
|
+
ui.print.cyan(` Writing ${label}...`)
|
|
136
|
+
await fs.writeFileAsync(path, content)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function checkToolAvailability(ui: any, proc: any) {
|
|
140
|
+
const tools: { name: string; found: boolean; envKey?: string; envFound?: boolean }[] = []
|
|
141
|
+
|
|
142
|
+
for (const name of ['claude', 'codex']) {
|
|
143
|
+
let found = false
|
|
144
|
+
try {
|
|
145
|
+
const result = await proc.exec(`which ${name}`, { silent: true })
|
|
146
|
+
found = result.exitCode === 0 && result.stdout.trim().length > 0
|
|
147
|
+
} catch {
|
|
148
|
+
found = false
|
|
149
|
+
}
|
|
150
|
+
tools.push({ name, found })
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const openaiKey = !!process.env.OPENAI_API_KEY
|
|
154
|
+
const hasAnyCodingTool = tools.some(t => t.found)
|
|
155
|
+
|
|
156
|
+
if (!hasAnyCodingTool) {
|
|
157
|
+
ui.print.yellow(' ┌─────────────────────────────────────────────────────────────┐')
|
|
158
|
+
ui.print.yellow(' │ No AI coding assistant detected (claude, codex) │')
|
|
159
|
+
ui.print.yellow(' │ │')
|
|
160
|
+
ui.print.yellow(' │ Luca works best with an AI coding assistant. │')
|
|
161
|
+
ui.print.yellow(' │ │')
|
|
162
|
+
ui.print.yellow(' │ Claude Code: https://docs.anthropic.com/en/docs/claude-code│')
|
|
163
|
+
ui.print.yellow(' │ Codex CLI: https://github.com/openai/codex │')
|
|
164
|
+
ui.print.yellow(' └─────────────────────────────────────────────────────────────┘')
|
|
165
|
+
ui.print('')
|
|
166
|
+
} else {
|
|
167
|
+
for (const t of tools) {
|
|
168
|
+
if (t.found) ui.print.green(` ✓ ${t.name} detected`)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!openaiKey) {
|
|
173
|
+
ui.print.dim(' ℹ OPENAI_API_KEY not set (only needed for codex/OpenAI features)')
|
|
174
|
+
} else {
|
|
175
|
+
ui.print.green(' ✓ OPENAI_API_KEY set')
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
ui.print('')
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
commands.registerHandler('bootstrap', {
|
|
182
|
+
description: 'Scaffold a new luca project with commands, features, endpoints, docs, and AI assistant configuration',
|
|
183
|
+
argsSchema,
|
|
184
|
+
handler: bootstrap,
|
|
185
|
+
})
|
package/src/commands/chat.ts
CHANGED
|
@@ -12,7 +12,7 @@ declare module '../command.js' {
|
|
|
12
12
|
|
|
13
13
|
export const argsSchema = CommandOptionsSchema.extend({
|
|
14
14
|
model: z.string().optional().describe('Override the LLM model for the assistant'),
|
|
15
|
-
|
|
15
|
+
local: z.boolean().default(false).describe('Whether to use a local API server'),
|
|
16
16
|
resume: z.string().optional().describe('Thread ID or conversation ID to resume'),
|
|
17
17
|
list: z.boolean().optional().describe('List recent conversations and exit'),
|
|
18
18
|
historyMode: z.enum(['lifecycle', 'daily', 'persistent', 'session']).optional().describe('Override history persistence mode'),
|
|
@@ -23,14 +23,14 @@ export default async function chat(options: z.infer<typeof argsSchema>, context:
|
|
|
23
23
|
const container = context.container as any
|
|
24
24
|
const ui = container.feature('ui')
|
|
25
25
|
|
|
26
|
-
const manager = container.feature('assistantsManager'
|
|
27
|
-
manager.discover()
|
|
26
|
+
const manager = container.feature('assistantsManager')
|
|
27
|
+
await manager.discover()
|
|
28
28
|
|
|
29
29
|
const entries = manager.list()
|
|
30
30
|
|
|
31
31
|
if (entries.length === 0) {
|
|
32
32
|
console.error(ui.colors.red('No assistants found.'))
|
|
33
|
-
console.error(ui.colors.dim(` Create
|
|
33
|
+
console.error(ui.colors.dim(` Create a directory with a CORE.md file anywhere in the project.`))
|
|
34
34
|
process.exit(1)
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -68,6 +68,7 @@ export default async function chat(options: z.infer<typeof argsSchema>, context:
|
|
|
68
68
|
|
|
69
69
|
const createOptions: Record<string, any> = { historyMode }
|
|
70
70
|
if (options.model) createOptions.model = options.model
|
|
71
|
+
if (options.local) createOptions.local = options.local
|
|
71
72
|
|
|
72
73
|
const assistant = manager.create(name, createOptions)
|
|
73
74
|
|