@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/commands/run.ts
CHANGED
|
@@ -13,8 +13,41 @@ export const argsSchema = CommandOptionsSchema.extend({
|
|
|
13
13
|
safe: z.boolean().default(false).describe('Require approval before each code block (markdown mode)'),
|
|
14
14
|
console: z.boolean().default(false).describe('Start an interactive REPL after executing a markdown file, with all accumulated context'),
|
|
15
15
|
onlySections: z.string().optional().describe('Comma-separated list of section headings to run (case-insensitive, markdown only)'),
|
|
16
|
+
dontInjectContext: z.boolean().default(false).describe('Skip auto-injecting container context into scripts (run with plain bun instead)'),
|
|
16
17
|
})
|
|
17
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Convert esbuild ESM output imports/exports to CJS require/module.exports
|
|
21
|
+
* so the code can run in a vm context that provides `require`.
|
|
22
|
+
*/
|
|
23
|
+
function esmToCjs(code: string): string {
|
|
24
|
+
return code
|
|
25
|
+
// import { a, b } from 'x' → const { a, b } = require('x')
|
|
26
|
+
.replace(/^import\s+\{([^}]+)\}\s+from\s+(['"][^'"]+['"])\s*;?$/gm,
|
|
27
|
+
'const {$1} = require($2);')
|
|
28
|
+
// import x from 'y' → const x = require('y').default ?? require('y')
|
|
29
|
+
.replace(/^import\s+(\w+)\s+from\s+(['"][^'"]+['"])\s*;?$/gm,
|
|
30
|
+
'const $1 = require($2).default ?? require($2);')
|
|
31
|
+
// import * as x from 'y' → const x = require('y')
|
|
32
|
+
.replace(/^import\s+\*\s+as\s+(\w+)\s+from\s+(['"][^'"]+['"])\s*;?$/gm,
|
|
33
|
+
'const $1 = require($2);')
|
|
34
|
+
// import 'y' → require('y')
|
|
35
|
+
.replace(/^import\s+(['"][^'"]+['"])\s*;?$/gm,
|
|
36
|
+
'require($1);')
|
|
37
|
+
// export default → module.exports.default =
|
|
38
|
+
.replace(/^export\s+default\s+/gm, 'module.exports.default = ')
|
|
39
|
+
// export { ... } → strip (vars already in scope)
|
|
40
|
+
.replace(/^export\s+\{[^}]*\}\s*;?$/gm, '')
|
|
41
|
+
// export const/let/var → const/let/var
|
|
42
|
+
.replace(/^export\s+(const|let|var)\s+/gm, '$1 ')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function hasTLA(code: string): boolean {
|
|
46
|
+
// Quick check: contains await outside of async function bodies
|
|
47
|
+
// This is a heuristic — wrapTopLevelAwait does the real work
|
|
48
|
+
return /\bawait\b/.test(code) && !/^\s*\(?\s*async\b/.test(code)
|
|
49
|
+
}
|
|
50
|
+
|
|
18
51
|
function resolveScript(ref: string, context: ContainerContext): string | null {
|
|
19
52
|
const container = context.container as any
|
|
20
53
|
const candidates = [
|
|
@@ -87,6 +120,7 @@ async function runMarkdown(scriptPath: string, options: z.infer<typeof argsSchem
|
|
|
87
120
|
setTimeout, clearTimeout, setInterval, clearInterval,
|
|
88
121
|
fetch, URL, URLSearchParams,
|
|
89
122
|
...container.context,
|
|
123
|
+
$doc: doc
|
|
90
124
|
})
|
|
91
125
|
|
|
92
126
|
// ─── Parse and register ## Blocks section ──────────────────────────
|
|
@@ -173,20 +207,51 @@ async function runMarkdown(scriptPath: string, options: z.infer<typeof argsSchem
|
|
|
173
207
|
return shared
|
|
174
208
|
}
|
|
175
209
|
|
|
176
|
-
async function runScript(scriptPath: string, context: ContainerContext) {
|
|
210
|
+
async function runScript(scriptPath: string, context: ContainerContext, options: { dontInjectContext?: boolean } = {}) {
|
|
177
211
|
const container = context.container as any
|
|
178
212
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
213
|
+
if (options.dontInjectContext) {
|
|
214
|
+
const { exitCode, stderr } = await container.proc.execAndCapture(`bun run ${scriptPath}`, {
|
|
215
|
+
onOutput: (data: string) => process.stdout.write(data),
|
|
216
|
+
onError: (data: string) => process.stderr.write(data),
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
if (exitCode === 0) return
|
|
183
220
|
|
|
184
|
-
|
|
221
|
+
console.error(`\nScript failed with exit code ${exitCode}.\n`)
|
|
222
|
+
if (stderr.length) {
|
|
223
|
+
console.error(stderr)
|
|
224
|
+
}
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const vm = container.feature('vm')
|
|
229
|
+
const esbuild = container.feature('esbuild')
|
|
230
|
+
const raw = container.fs.readFile(scriptPath)
|
|
231
|
+
|
|
232
|
+
let code: string
|
|
233
|
+
if (hasTLA(raw)) {
|
|
234
|
+
// TLA is incompatible with CJS format, so transform as ESM (preserves await)
|
|
235
|
+
// then convert import/export statements to require/module.exports for the vm
|
|
236
|
+
const { code: esm } = esbuild.transformSync(raw, { format: 'esm' })
|
|
237
|
+
code = esmToCjs(esm)
|
|
238
|
+
} else {
|
|
239
|
+
const { code: cjs } = esbuild.transformSync(raw, { format: 'cjs' })
|
|
240
|
+
code = cjs
|
|
241
|
+
}
|
|
185
242
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
243
|
+
const ctx = {
|
|
244
|
+
require: vm.createRequireFor(scriptPath),
|
|
245
|
+
exports: {},
|
|
246
|
+
module: { exports: {} },
|
|
247
|
+
console,
|
|
248
|
+
setTimeout, setInterval, clearTimeout, clearInterval,
|
|
249
|
+
process, Buffer, URL, URLSearchParams,
|
|
250
|
+
fetch,
|
|
251
|
+
...container.context,
|
|
189
252
|
}
|
|
253
|
+
|
|
254
|
+
await vm.run(code, ctx)
|
|
190
255
|
}
|
|
191
256
|
|
|
192
257
|
async function diagnoseError(_scriptPath: string, error: Error, _context: ContainerContext) {
|
|
@@ -236,7 +301,7 @@ export default async function run(options: z.infer<typeof argsSchema>, context:
|
|
|
236
301
|
})
|
|
237
302
|
}
|
|
238
303
|
} else {
|
|
239
|
-
await runScript(scriptPath, context)
|
|
304
|
+
await runScript(scriptPath, context, { dontInjectContext: options.dontInjectContext })
|
|
240
305
|
}
|
|
241
306
|
} catch (err: any) {
|
|
242
307
|
await diagnoseError(scriptPath, err instanceof Error ? err : new Error(String(err)), context)
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
|
|
6
|
+
declare module '../command.js' {
|
|
7
|
+
interface AvailableCommands {
|
|
8
|
+
introspect: ReturnType<typeof commands.registerHandler>
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const argsSchema = CommandOptionsSchema.extend({
|
|
13
|
+
outputPath: z.string().default('docs/luca').describe('The path to save generated API docs to')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export async function apiDocs(options: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
17
|
+
const { container } = context
|
|
18
|
+
await container.helpers.discoverAll()
|
|
19
|
+
const outputFolder = options.outputPath ? container.paths.resolve(options.outputPath) : container.paths.resolve('docs','luca')
|
|
20
|
+
|
|
21
|
+
await container.fs.ensureFolder(
|
|
22
|
+
outputFolder
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const mkPath = (...args) => container.paths.resolve(outputFolder, ...args)
|
|
26
|
+
|
|
27
|
+
const result = await container.fs.writeFileAsync(mkPath('agi-container.md'), container.inspectAsText())
|
|
28
|
+
|
|
29
|
+
for(let reg of ['features','clients','servers']) {
|
|
30
|
+
const helperIds = container[reg].available
|
|
31
|
+
const folder = mkPath(reg)
|
|
32
|
+
await container.fs.ensureFolder(folder)
|
|
33
|
+
|
|
34
|
+
await Promise.all(
|
|
35
|
+
helperIds.map((helperId) => container.fs.writeFileAsync(
|
|
36
|
+
container.paths.resolve(folder, `${helperId}.md`),
|
|
37
|
+
container[reg].describe(helperId)
|
|
38
|
+
))
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
container.ui.print.green(`Finished saving API Docs`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
commands.registerHandler('api-docs', {
|
|
46
|
+
description: 'Save the helper introspection() content as markdown API docs in docs/luca',
|
|
47
|
+
argsSchema,
|
|
48
|
+
handler: apiDocs,
|
|
49
|
+
})
|
package/src/commands/scaffold.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { z } from 'zod'
|
|
|
2
2
|
import { commands } from '../command.js'
|
|
3
3
|
import { CommandOptionsSchema } from '../schemas/base.js'
|
|
4
4
|
import type { ContainerContext } from '../container.js'
|
|
5
|
-
import { scaffolds } from '../scaffolds/generated.js'
|
|
6
|
-
import { generateScaffold, toCamelCase } from '../scaffolds/template.js'
|
|
5
|
+
import { scaffolds, assistantFiles } from '../scaffolds/generated.js'
|
|
6
|
+
import { generateScaffold, toCamelCase, toKebabCase } from '../scaffolds/template.js'
|
|
7
7
|
|
|
8
8
|
declare module '../command.js' {
|
|
9
9
|
interface AvailableCommands {
|
|
@@ -11,69 +11,215 @@ declare module '../command.js' {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const validTypes = Object.keys(scaffolds)
|
|
14
|
+
const validTypes = [...Object.keys(scaffolds), 'assistant']
|
|
15
15
|
|
|
16
16
|
export const argsSchema = CommandOptionsSchema.extend({
|
|
17
17
|
description: z.string().optional().describe('Brief description of the helper'),
|
|
18
|
-
output: z.string().optional().describe('Output file path (
|
|
18
|
+
output: z.string().optional().describe('Output file path (overrides default location)'),
|
|
19
|
+
print: z.boolean().default(false).describe('Print the generated code to stdout instead of writing a file'),
|
|
19
20
|
tutorial: z.boolean().default(false).describe('Show the full tutorial instead of generating code'),
|
|
20
21
|
})
|
|
21
22
|
|
|
23
|
+
const TYPE_INFO: Record<string, { what: string; where: string; run: string }> = {
|
|
24
|
+
feature: {
|
|
25
|
+
what: 'A container-managed capability (caching, encryption, custom I/O)',
|
|
26
|
+
where: 'features/<name>.ts',
|
|
27
|
+
run: 'container.feature(\'<name>\') in any command, endpoint, or script',
|
|
28
|
+
},
|
|
29
|
+
client: {
|
|
30
|
+
what: 'A connection to an external service (REST API, WebSocket, GraphQL)',
|
|
31
|
+
where: 'clients/<name>.ts',
|
|
32
|
+
run: 'container.client(\'<name>\') in any command, endpoint, or script',
|
|
33
|
+
},
|
|
34
|
+
server: {
|
|
35
|
+
what: 'A listener accepting incoming connections (HTTP, WebSocket, custom protocol)',
|
|
36
|
+
where: 'servers/<name>.ts',
|
|
37
|
+
run: 'container.server(\'<name>\') then .start()',
|
|
38
|
+
},
|
|
39
|
+
command: {
|
|
40
|
+
what: 'A CLI task that extends `luca` (build scripts, generators, automation)',
|
|
41
|
+
where: 'commands/<name>.ts',
|
|
42
|
+
run: 'luca <name>',
|
|
43
|
+
},
|
|
44
|
+
endpoint: {
|
|
45
|
+
what: 'A REST API route auto-discovered by `luca serve`',
|
|
46
|
+
where: 'endpoints/<name>.ts',
|
|
47
|
+
run: 'luca serve → GET/POST /api/<name>',
|
|
48
|
+
},
|
|
49
|
+
selector: {
|
|
50
|
+
what: 'A cached data query — returns structured results from the container',
|
|
51
|
+
where: 'selectors/<name>.ts',
|
|
52
|
+
run: 'luca select <name> or container.select(\'<name>\')',
|
|
53
|
+
},
|
|
54
|
+
assistant: {
|
|
55
|
+
what: 'An AI assistant with a system prompt, tools, and lifecycle hooks',
|
|
56
|
+
where: 'assistants/<name>/',
|
|
57
|
+
run: 'luca chat <name>',
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
22
61
|
export default async function scaffoldCommand(options: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
23
62
|
const container = context.container as any
|
|
24
63
|
const args = container.argv._ as string[]
|
|
64
|
+
const ui = container.feature('ui')
|
|
25
65
|
|
|
26
66
|
// args: ["scaffold", type?, name?]
|
|
27
67
|
const type = args[1]
|
|
28
68
|
const name = args[2]
|
|
29
69
|
|
|
70
|
+
// ── No type or invalid type: show full help ──────────────────
|
|
30
71
|
if (!type || !validTypes.includes(type)) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
72
|
+
ui.print.cyan('\n luca scaffold — generate boilerplate for luca helpers\n')
|
|
73
|
+
ui.print(' Usage: luca scaffold <type> <name> [options]\n')
|
|
74
|
+
ui.print(' Options:')
|
|
75
|
+
ui.print(' --description "..." Brief description (shows in help text and docs)')
|
|
76
|
+
ui.print(' --output <path> Override the default output file path')
|
|
77
|
+
ui.print(' --print Print to stdout instead of writing a file')
|
|
78
|
+
ui.print(' --tutorial Show the full guide for a type instead of generating code\n')
|
|
79
|
+
|
|
80
|
+
ui.print(' Types:\n')
|
|
81
|
+
for (const [t, info] of Object.entries(TYPE_INFO)) {
|
|
82
|
+
ui.print.green(` ${t}`)
|
|
83
|
+
ui.print(` ${info.what}`)
|
|
84
|
+
ui.print.dim(` File: ${info.where} → Run: ${info.run}`)
|
|
85
|
+
ui.print('')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
ui.print(' Examples:\n')
|
|
89
|
+
ui.print(' # Generate a command (writes to commands/deploy.ts)')
|
|
90
|
+
ui.print(' luca scaffold command deploy --description "Deploy to production"\n')
|
|
91
|
+
ui.print(' # Generate a feature (writes to features/disk-cache.ts)')
|
|
92
|
+
ui.print(' luca scaffold feature disk-cache --description "File-backed key-value cache"\n')
|
|
93
|
+
ui.print(' # Print to stdout instead of writing')
|
|
94
|
+
ui.print(' luca scaffold endpoint users --print\n')
|
|
95
|
+
ui.print(' # Read the full tutorial for a type')
|
|
96
|
+
ui.print(' luca scaffold feature --tutorial')
|
|
97
|
+
ui.print(' luca scaffold endpoint --tutorial\n')
|
|
98
|
+
|
|
99
|
+
ui.print(' Workflow:\n')
|
|
100
|
+
ui.print(' 1. luca scaffold <type> <name> Generate the file')
|
|
101
|
+
ui.print(' 2. Edit the generated file — add your logic')
|
|
102
|
+
ui.print(' 3. luca about Verify it was discovered')
|
|
103
|
+
ui.print(' 4. luca describe <name> See the generated docs\n')
|
|
104
|
+
|
|
105
|
+
if (type && !validTypes.includes(type)) {
|
|
106
|
+
ui.print.yellow(` "${type}" is not a valid type. Available: ${validTypes.join(', ')}\n`)
|
|
107
|
+
}
|
|
38
108
|
return
|
|
39
109
|
}
|
|
40
110
|
|
|
41
|
-
// Tutorial mode
|
|
111
|
+
// ── Tutorial mode ────────────────────────────────────────────
|
|
42
112
|
if (options.tutorial) {
|
|
43
113
|
const scaffold = scaffolds[type]
|
|
44
114
|
if (scaffold?.tutorial) {
|
|
45
|
-
const ui = container.feature('ui')
|
|
46
115
|
console.log(ui.markdown(scaffold.tutorial))
|
|
47
116
|
} else {
|
|
48
|
-
|
|
117
|
+
ui.print.yellow(`No tutorial available for type: ${type}`)
|
|
49
118
|
}
|
|
50
119
|
return
|
|
51
120
|
}
|
|
52
121
|
|
|
122
|
+
// ── Missing name ─────────────────────────────────────────────
|
|
53
123
|
if (!name) {
|
|
54
|
-
|
|
124
|
+
const info = TYPE_INFO[type]
|
|
125
|
+
ui.print.cyan(`\n luca scaffold ${type} <name> [options]\n`)
|
|
126
|
+
ui.print(` ${info?.what || ''}\n`)
|
|
127
|
+
ui.print(' Examples:')
|
|
128
|
+
if (type === 'feature') {
|
|
129
|
+
ui.print(` luca scaffold feature diskCache --description "File-backed cache"`)
|
|
130
|
+
ui.print(` luca scaffold feature diskCache --output features/diskCache.ts`)
|
|
131
|
+
} else if (type === 'client') {
|
|
132
|
+
ui.print(` luca scaffold client github --description "GitHub API client"`)
|
|
133
|
+
ui.print(` luca scaffold client github --output clients/github.ts`)
|
|
134
|
+
} else if (type === 'server') {
|
|
135
|
+
ui.print(` luca scaffold server grpc --description "gRPC server"`)
|
|
136
|
+
ui.print(` luca scaffold server grpc --output servers/grpc.ts`)
|
|
137
|
+
} else if (type === 'command') {
|
|
138
|
+
ui.print(` luca scaffold command deploy --description "Deploy to production"`)
|
|
139
|
+
ui.print(` luca scaffold command deploy --output commands/deploy.ts`)
|
|
140
|
+
} else if (type === 'endpoint') {
|
|
141
|
+
ui.print(` luca scaffold endpoint users --description "User management API"`)
|
|
142
|
+
ui.print(` luca scaffold endpoint users --output endpoints/users.ts`)
|
|
143
|
+
} else if (type === 'selector') {
|
|
144
|
+
ui.print(` luca scaffold selector package-info --description "Returns parsed package.json data"`)
|
|
145
|
+
ui.print(` luca scaffold selector package-info --output selectors/package-info.ts`)
|
|
146
|
+
} else if (type === 'assistant') {
|
|
147
|
+
ui.print(` luca scaffold assistant chief-of-staff`)
|
|
148
|
+
ui.print(` luca scaffold assistant chief-of-staff --output assistants/chief-of-staff`)
|
|
149
|
+
}
|
|
150
|
+
ui.print(`\n luca scaffold ${type} --tutorial Full guide with patterns and conventions\n`)
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Assistant: multi-file scaffold ───────────────────────────
|
|
155
|
+
if (type === 'assistant') {
|
|
156
|
+
if (!assistantFiles || Object.keys(assistantFiles).length === 0) {
|
|
157
|
+
ui.print.yellow('No assistant scaffold files bundled. Rebuild with: luca build-scaffolds')
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const outputDir = options.output || `assistants/${toCamelCase(name)}`
|
|
162
|
+
const fs = container.feature('fs')
|
|
163
|
+
const resolvedDir = container.paths.resolve(outputDir)
|
|
164
|
+
|
|
165
|
+
await fs.ensureFolder(resolvedDir)
|
|
166
|
+
|
|
167
|
+
for (const [fileName, content] of Object.entries(assistantFiles)) {
|
|
168
|
+
const filePath = container.paths.resolve(resolvedDir, fileName)
|
|
169
|
+
await fs.writeFileAsync(filePath, content)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
ui.print.green(`\n ✓ Scaffolded assistant "${name}" in ${outputDir}/`)
|
|
173
|
+
ui.print.dim(` CORE.md — system prompt (edit this to define your assistant's personality)`)
|
|
174
|
+
ui.print.dim(` tools.ts — tool functions the assistant can call`)
|
|
175
|
+
ui.print.dim(` hooks.ts — lifecycle event handlers`)
|
|
176
|
+
ui.print(`\n Start chatting: luca chat ${name}\n`)
|
|
55
177
|
return
|
|
56
178
|
}
|
|
57
179
|
|
|
58
180
|
const code = generateScaffold(type, name, options.description)
|
|
59
181
|
|
|
60
182
|
if (!code) {
|
|
61
|
-
|
|
183
|
+
ui.print.yellow(`No scaffold template available for type: ${type}`)
|
|
62
184
|
return
|
|
63
185
|
}
|
|
64
186
|
|
|
65
|
-
//
|
|
66
|
-
if (options.
|
|
67
|
-
const fs = container.feature('fs')
|
|
68
|
-
await fs.writeFileAsync(options.output, code)
|
|
69
|
-
console.log(`Wrote ${type} scaffold to ${options.output}`)
|
|
70
|
-
} else {
|
|
187
|
+
// --print: just output to stdout
|
|
188
|
+
if (options.print) {
|
|
71
189
|
console.log(code)
|
|
190
|
+
return
|
|
72
191
|
}
|
|
192
|
+
|
|
193
|
+
// Default: write to file
|
|
194
|
+
const kebabName = toKebabCase(name)
|
|
195
|
+
const defaultPaths: Record<string, string> = {
|
|
196
|
+
feature: `features/${kebabName}.ts`,
|
|
197
|
+
client: `clients/${kebabName}.ts`,
|
|
198
|
+
server: `servers/${kebabName}.ts`,
|
|
199
|
+
command: `commands/${kebabName}.ts`,
|
|
200
|
+
endpoint: `endpoints/${kebabName}.ts`,
|
|
201
|
+
selector: `selectors/${kebabName}.ts`,
|
|
202
|
+
}
|
|
203
|
+
const outputPath = options.output || defaultPaths[type] || `${type}s/${kebabName}.ts`
|
|
204
|
+
const fs = container.feature('fs')
|
|
205
|
+
const resolvedPath = container.paths.resolve(outputPath)
|
|
206
|
+
|
|
207
|
+
// Check if file already exists
|
|
208
|
+
if (await fs.existsAsync(resolvedPath)) {
|
|
209
|
+
ui.print.yellow(` ✗ File already exists: ${outputPath}`)
|
|
210
|
+
ui.print.dim(` Use --output <path> to write to a different location, or --print to view the code\n`)
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const dir = container.paths.resolve(outputPath, '..')
|
|
215
|
+
await fs.ensureFolder(dir)
|
|
216
|
+
await fs.writeFileAsync(resolvedPath, code)
|
|
217
|
+
ui.print.green(` ✓ Wrote ${type} scaffold to ${outputPath}`)
|
|
218
|
+
ui.print.dim(` Next: edit the file, then run \`luca about\` to verify discovery\n`)
|
|
73
219
|
}
|
|
74
220
|
|
|
75
221
|
commands.registerHandler('scaffold', {
|
|
76
|
-
description: 'Generate boilerplate for a new luca feature, client, server, command, or
|
|
222
|
+
description: 'Generate boilerplate for a new luca feature, client, server, command, endpoint, or assistant',
|
|
77
223
|
argsSchema,
|
|
78
224
|
handler: scaffoldCommand,
|
|
79
225
|
})
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { commands } from '../command.js'
|
|
3
|
+
import { selectors } from '../selector.js'
|
|
4
|
+
import { CommandOptionsSchema } from '../schemas/base.js'
|
|
5
|
+
import type { ContainerContext } from '../container.js'
|
|
6
|
+
|
|
7
|
+
declare module '../command.js' {
|
|
8
|
+
interface AvailableCommands {
|
|
9
|
+
select: ReturnType<typeof commands.registerHandler>
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const argsSchema = CommandOptionsSchema.extend({
|
|
14
|
+
json: z.boolean().default(false).describe('Output result as raw JSON (data only, no metadata)'),
|
|
15
|
+
noCache: z.boolean().default(false).describe('Skip cache lookup and force a fresh run'),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export default async function selectCommand(options: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
19
|
+
const container = context.container as any
|
|
20
|
+
const ui = container.feature('ui')
|
|
21
|
+
const args = container.argv._ as string[]
|
|
22
|
+
const name = args[1]
|
|
23
|
+
|
|
24
|
+
// Discover project selectors
|
|
25
|
+
await container.helpers.discoverAll()
|
|
26
|
+
|
|
27
|
+
if (!name) {
|
|
28
|
+
const available = selectors.available
|
|
29
|
+
if (available.length === 0) {
|
|
30
|
+
ui.print('No selectors available.')
|
|
31
|
+
ui.print.dim('Create one: luca scaffold selector <name>')
|
|
32
|
+
} else {
|
|
33
|
+
ui.print.cyan('\n luca select <name> [--json] [--no-cache]\n')
|
|
34
|
+
ui.print(' Available selectors:\n')
|
|
35
|
+
for (const s of available) {
|
|
36
|
+
ui.print.green(` ${s}`)
|
|
37
|
+
}
|
|
38
|
+
ui.print('')
|
|
39
|
+
}
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const instance = container.select(name)
|
|
44
|
+
|
|
45
|
+
// Pass remaining args (after command name and selector name) as input
|
|
46
|
+
const selectorArgs: Record<string, any> = { ...options }
|
|
47
|
+
delete selectorArgs._
|
|
48
|
+
delete selectorArgs.json
|
|
49
|
+
delete selectorArgs.noCache
|
|
50
|
+
delete selectorArgs.cache
|
|
51
|
+
delete selectorArgs.dispatchSource
|
|
52
|
+
|
|
53
|
+
// minimist turns --no-cache into { cache: false }
|
|
54
|
+
const skipCache = options.noCache || (container.argv.cache === false)
|
|
55
|
+
|
|
56
|
+
if (skipCache) {
|
|
57
|
+
// Bypass cache by calling run() directly with proper lifecycle
|
|
58
|
+
const Cls = instance.constructor as any
|
|
59
|
+
const parsed = Cls.argsSchema.parse(selectorArgs)
|
|
60
|
+
instance.state.set('running', true)
|
|
61
|
+
instance.emit('started')
|
|
62
|
+
let data: any
|
|
63
|
+
try {
|
|
64
|
+
data = await instance.run(parsed, instance.context)
|
|
65
|
+
instance.state.set('running', false)
|
|
66
|
+
instance.state.set('lastRanAt', Date.now())
|
|
67
|
+
instance.emit('completed', data)
|
|
68
|
+
} catch (err: any) {
|
|
69
|
+
instance.state.set('running', false)
|
|
70
|
+
instance.emit('failed', err)
|
|
71
|
+
throw err
|
|
72
|
+
}
|
|
73
|
+
if (options.json) {
|
|
74
|
+
console.log(JSON.stringify(data, null, 2))
|
|
75
|
+
} else {
|
|
76
|
+
console.log(JSON.stringify({ data, cached: false }, null, 2))
|
|
77
|
+
}
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const result = await instance.select(selectorArgs)
|
|
82
|
+
|
|
83
|
+
if (options.json) {
|
|
84
|
+
console.log(JSON.stringify(result.data, null, 2))
|
|
85
|
+
} else {
|
|
86
|
+
console.log(JSON.stringify(result, null, 2))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
commands.registerHandler('select', {
|
|
91
|
+
description: 'Run a selector and display its cached or fresh data',
|
|
92
|
+
argsSchema,
|
|
93
|
+
handler: selectCommand,
|
|
94
|
+
})
|
package/src/commands/serve.ts
CHANGED
|
@@ -115,7 +115,16 @@ export default async function serve(options: z.infer<typeof argsSchema>, context
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
try {
|
|
119
|
+
await expressServer.start({ port })
|
|
120
|
+
} catch (error: any) {
|
|
121
|
+
if (error?.code === 'EADDRINUSE') {
|
|
122
|
+
console.error(`Port ${port} is already in use.`)
|
|
123
|
+
console.error(`Use --force to kill the process on this port, or --any-port to find another port.`)
|
|
124
|
+
process.exit(1)
|
|
125
|
+
}
|
|
126
|
+
throw error
|
|
127
|
+
}
|
|
119
128
|
|
|
120
129
|
const name = manifest.name || 'Server'
|
|
121
130
|
console.log(`\n${name} listening on http://localhost:${port}`)
|
package/src/container.ts
CHANGED
|
@@ -629,6 +629,21 @@ function presentContainerIntrospectionAsMarkdown(data: ContainerIntrospection, s
|
|
|
629
629
|
// Header
|
|
630
630
|
sections.push(`${heading(1)} ${data.className}\n\n${data.description || ''}`)
|
|
631
631
|
|
|
632
|
+
// Container Properties section (node containers expose these top-level getters)
|
|
633
|
+
if (data.environment?.isNode || data.environment?.isBun) {
|
|
634
|
+
sections.push([
|
|
635
|
+
`${heading(2)} Container Properties`,
|
|
636
|
+
'',
|
|
637
|
+
'| Property | Description |',
|
|
638
|
+
'|----------|-------------|',
|
|
639
|
+
'| `container.cwd` | Current working directory |',
|
|
640
|
+
'| `container.paths` | Path utilities scoped to cwd: `resolve()`, `join()`, `relative()`, `dirname()`, `basename()`, `parse()` |',
|
|
641
|
+
'| `container.manifest` | Parsed `package.json` for the current directory (`name`, `version`, `dependencies`, etc.) |',
|
|
642
|
+
'| `container.argv` | Raw parsed CLI arguments (from minimist). Prefer `positionals` export for positional args in commands |',
|
|
643
|
+
'| `container.utils` | Common utilities: `uuid()`, `hashObject()`, `stringUtils`, `lodash` |',
|
|
644
|
+
].join('\n'))
|
|
645
|
+
}
|
|
646
|
+
|
|
632
647
|
// Registries section
|
|
633
648
|
if (data.registries && data.registries.length > 0) {
|
|
634
649
|
sections.push(`${heading(2)} Registries`)
|
package/src/endpoint.ts
CHANGED
|
@@ -56,6 +56,14 @@ export interface EndpointModule {
|
|
|
56
56
|
|
|
57
57
|
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete'] as const
|
|
58
58
|
|
|
59
|
+
/** All recognized exports on an endpoint module */
|
|
60
|
+
const KNOWN_EXPORTS = new Set([
|
|
61
|
+
'path', 'description', 'tags', 'default', 'rateLimit',
|
|
62
|
+
...HTTP_METHODS,
|
|
63
|
+
...HTTP_METHODS.map(m => `${m}Schema`),
|
|
64
|
+
...HTTP_METHODS.map(m => `${m}RateLimit`),
|
|
65
|
+
])
|
|
66
|
+
|
|
59
67
|
/**
|
|
60
68
|
* Sliding-window rate limiter keyed by IP address.
|
|
61
69
|
* Tracks timestamps of requests and prunes entries older than the window.
|
|
@@ -177,6 +185,10 @@ export class Endpoint<
|
|
|
177
185
|
this._module = imported.default || imported
|
|
178
186
|
}
|
|
179
187
|
|
|
188
|
+
// Note: DELETE handlers should be exported as `export { del as delete }`.
|
|
189
|
+
// We no longer remap `destroy` → `delete` because ESM namespace objects
|
|
190
|
+
// are frozen and the mutation throws on Bun.
|
|
191
|
+
|
|
180
192
|
this.state.set('methods', this.methods)
|
|
181
193
|
this.state.set('path', this.path)
|
|
182
194
|
this.emit('loaded', this._module)
|
|
@@ -328,6 +340,13 @@ export class Endpoint<
|
|
|
328
340
|
}
|
|
329
341
|
}
|
|
330
342
|
|
|
343
|
+
export function warnUnknownExports(mod: Record<string, any>, filePath: string): void {
|
|
344
|
+
const unknown = Object.keys(mod).filter(k => !k.startsWith('__') && !KNOWN_EXPORTS.has(k))
|
|
345
|
+
if (unknown.length > 0) {
|
|
346
|
+
console.warn(`[endpoint] ${filePath}: unknown exports: ${unknown.join(', ')}`)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
331
350
|
export class EndpointsRegistry extends Registry<Endpoint<any>> {
|
|
332
351
|
override scope = 'endpoints'
|
|
333
352
|
override baseClass = Endpoint
|