@soederpop/luca 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +10 -1
- package/bun.lock +1 -1
- package/commands/build-bootstrap.ts +78 -0
- package/commands/build-scaffolds.ts +24 -2
- package/commands/try-all-challenges.ts +543 -0
- package/commands/try-challenge.ts +100 -0
- package/docs/README.md +52 -80
- package/docs/TABLE-OF-CONTENTS.md +82 -51
- package/docs/apis/clients/elevenlabs.md +232 -8
- package/docs/apis/clients/graph.md +59 -8
- package/docs/apis/clients/openai.md +362 -2
- package/docs/apis/clients/rest.md +122 -2
- package/docs/apis/clients/websocket.md +71 -17
- package/docs/apis/features/agi/assistant.md +9 -3
- package/docs/apis/features/agi/assistants-manager.md +2 -2
- package/docs/apis/features/agi/claude-code.md +153 -14
- package/docs/apis/features/agi/conversation-history.md +15 -3
- package/docs/apis/features/agi/conversation.md +133 -20
- package/docs/apis/features/agi/openai-codex.md +90 -12
- package/docs/apis/features/agi/skills-library.md +23 -5
- package/docs/apis/features/node/container-link.md +59 -0
- package/docs/apis/features/node/content-db.md +1 -1
- package/docs/apis/features/node/disk-cache.md +1 -1
- package/docs/apis/features/node/dns.md +1 -0
- package/docs/apis/features/node/docker.md +2 -1
- package/docs/apis/features/node/esbuild.md +4 -3
- package/docs/apis/features/node/file-manager.md +13 -4
- package/docs/apis/features/node/fs.md +726 -171
- package/docs/apis/features/node/git.md +1 -0
- package/docs/apis/features/node/google-auth.md +23 -4
- package/docs/apis/features/node/google-calendar.md +14 -2
- package/docs/apis/features/node/google-docs.md +15 -2
- package/docs/apis/features/node/google-drive.md +21 -3
- package/docs/apis/features/node/google-sheets.md +14 -2
- package/docs/apis/features/node/grep.md +2 -0
- package/docs/apis/features/node/helpers.md +29 -0
- package/docs/apis/features/node/ink.md +2 -2
- package/docs/apis/features/node/networking.md +39 -4
- package/docs/apis/features/node/os.md +28 -0
- package/docs/apis/features/node/postgres.md +26 -4
- package/docs/apis/features/node/proc.md +37 -28
- package/docs/apis/features/node/process-manager.md +33 -5
- package/docs/apis/features/node/repl.md +1 -1
- package/docs/apis/features/node/runpod.md +1 -0
- package/docs/apis/features/node/secure-shell.md +7 -0
- package/docs/apis/features/node/semantic-search.md +12 -5
- package/docs/apis/features/node/sqlite.md +26 -4
- package/docs/apis/features/node/telegram.md +30 -5
- package/docs/apis/features/node/tts.md +17 -2
- package/docs/apis/features/node/ui.md +1 -1
- package/docs/apis/features/node/vault.md +4 -9
- package/docs/apis/features/node/vm.md +3 -12
- package/docs/apis/features/node/window-manager.md +128 -20
- package/docs/apis/features/web/asset-loader.md +13 -1
- package/docs/apis/features/web/container-link.md +59 -0
- package/docs/apis/features/web/esbuild.md +4 -3
- package/docs/apis/features/web/helpers.md +29 -0
- package/docs/apis/features/web/network.md +16 -2
- package/docs/apis/features/web/speech.md +16 -2
- package/docs/apis/features/web/vault.md +4 -9
- package/docs/apis/features/web/vm.md +3 -12
- package/docs/apis/features/web/voice.md +18 -1
- package/docs/apis/servers/express.md +18 -2
- package/docs/apis/servers/mcp.md +29 -4
- package/docs/apis/servers/websocket.md +34 -6
- package/docs/bootstrap/CLAUDE.md +100 -0
- package/docs/bootstrap/SKILL.md +222 -0
- package/docs/bootstrap/templates/about-command.ts +41 -0
- package/docs/bootstrap/templates/docs-models.ts +22 -0
- package/docs/bootstrap/templates/docs-readme.md +43 -0
- package/docs/bootstrap/templates/example-feature.ts +53 -0
- package/docs/bootstrap/templates/health-endpoint.ts +15 -0
- package/docs/bootstrap/templates/luca-cli.ts +25 -0
- package/docs/challenges/caching-proxy.md +16 -0
- package/docs/challenges/content-db-round-trip.md +14 -0
- package/docs/challenges/custom-command.md +9 -0
- package/docs/challenges/file-watcher-pipeline.md +11 -0
- package/docs/challenges/grep-audit-report.md +15 -0
- package/docs/challenges/multi-feature-dashboard.md +14 -0
- package/docs/challenges/process-orchestrator.md +17 -0
- package/docs/challenges/rest-api-server-with-client.md +12 -0
- package/docs/challenges/script-runner-with-vm.md +11 -0
- package/docs/challenges/simple-rest-api.md +15 -0
- package/docs/challenges/websocket-serve-and-client.md +11 -0
- package/docs/challenges/yaml-config-system.md +14 -0
- package/docs/command-system-overhaul.md +94 -0
- package/docs/examples/assistant/CORE.md +18 -0
- package/docs/examples/assistant/hooks.ts +3 -0
- package/docs/examples/assistant/tools.ts +10 -0
- package/docs/examples/window-manager-layouts.md +180 -0
- package/docs/in-memory-fs.md +4 -0
- package/docs/models.ts +13 -10
- package/docs/philosophy.md +4 -3
- package/docs/reports/console-hmr-design.md +170 -0
- package/docs/reports/helper-semantic-search.md +72 -0
- package/docs/scaffolds/client.md +29 -20
- package/docs/scaffolds/command.md +64 -50
- package/docs/scaffolds/endpoint.md +31 -36
- package/docs/scaffolds/feature.md +28 -18
- package/docs/scaffolds/selector.md +91 -0
- package/docs/scaffolds/server.md +18 -9
- package/docs/selectors.md +115 -0
- package/docs/sessions/custom-command/attempt-log-2.md +195 -0
- package/docs/sessions/file-watcher-pipeline/attempt-log-1.md +728 -0
- package/docs/sessions/file-watcher-pipeline/attempt-log-2.md +555 -0
- package/docs/sessions/grep-audit-report/attempt-log-1.md +289 -0
- package/docs/sessions/multi-feature-dashboard/attempt-log-2.md +679 -0
- package/docs/sessions/rest-api-server-with-client/attempt-log-1.md +1 -0
- package/docs/sessions/rest-api-server-with-client/attempt-log-3.md +920 -0
- package/docs/sessions/simple-rest-api/attempt-log-1.md +593 -0
- package/docs/sessions/websocket-serve-and-client/attempt-log-2.md +995 -0
- package/docs/tutorials/00-bootstrap.md +148 -0
- package/docs/tutorials/07-endpoints.md +7 -7
- package/docs/tutorials/08-commands.md +153 -72
- package/luca.cli.ts +3 -0
- package/package.json +6 -5
- package/public/index.html +1430 -0
- package/scripts/examples/using-ollama.ts +2 -1
- package/scripts/update-introspection-data.ts +2 -2
- package/src/agi/endpoints/experts.ts +1 -1
- package/src/agi/features/assistant.ts +7 -0
- package/src/agi/features/assistants-manager.ts +5 -5
- package/src/agi/features/claude-code.ts +263 -3
- package/src/agi/features/conversation-history.ts +7 -1
- package/src/agi/features/conversation.ts +26 -3
- package/src/agi/features/openai-codex.ts +26 -2
- package/src/agi/features/openapi.ts +6 -1
- package/src/agi/features/skills-library.ts +9 -1
- package/src/bootstrap/generated.ts +540 -0
- package/src/cli/cli.ts +64 -21
- package/src/client.ts +23 -357
- package/src/clients/civitai/index.ts +1 -1
- package/src/clients/client-template.ts +1 -1
- package/src/clients/comfyui/index.ts +13 -2
- package/src/clients/elevenlabs/index.ts +2 -1
- package/src/clients/graph.ts +87 -0
- package/src/clients/openai/index.ts +10 -1
- package/src/clients/rest.ts +207 -0
- package/src/clients/websocket.ts +176 -0
- package/src/command.ts +281 -34
- package/src/commands/bootstrap.ts +181 -0
- package/src/commands/chat.ts +5 -4
- package/src/commands/describe.ts +225 -2
- package/src/commands/help.ts +35 -9
- package/src/commands/index.ts +3 -0
- package/src/commands/introspect.ts +92 -2
- package/src/commands/prompt.ts +5 -6
- package/src/commands/run.ts +33 -10
- package/src/commands/save-api-docs.ts +49 -0
- package/src/commands/scaffold.ts +169 -23
- package/src/commands/select.ts +94 -0
- package/src/commands/serve.ts +10 -1
- package/src/container.ts +15 -0
- package/src/endpoint.ts +19 -0
- package/src/graft.ts +181 -0
- package/src/introspection/generated.agi.ts +12458 -8968
- package/src/introspection/generated.node.ts +10573 -7145
- package/src/introspection/generated.web.ts +1 -1
- package/src/introspection/index.ts +26 -0
- package/src/node/container.ts +6 -7
- package/src/node/features/content-db.ts +49 -2
- package/src/node/features/disk-cache.ts +16 -9
- package/src/node/features/dns.ts +16 -3
- package/src/node/features/docker.ts +16 -4
- package/src/node/features/esbuild.ts +20 -0
- package/src/node/features/file-manager.ts +184 -29
- package/src/node/features/fs.ts +704 -248
- package/src/node/features/git.ts +21 -8
- package/src/node/features/grep.ts +23 -3
- package/src/node/features/helpers.ts +372 -43
- package/src/node/features/networking.ts +39 -4
- package/src/node/features/opener.ts +28 -15
- package/src/node/features/os.ts +76 -0
- package/src/node/features/port-exposer.ts +11 -1
- package/src/node/features/postgres.ts +17 -1
- package/src/node/features/proc.ts +4 -1
- package/src/node/features/python.ts +63 -14
- package/src/node/features/repl.ts +11 -7
- package/src/node/features/runpod.ts +16 -3
- package/src/node/features/secure-shell.ts +27 -2
- package/src/node/features/semantic-search.ts +12 -1
- package/src/node/features/ui.ts +5 -69
- package/src/node/features/vm.ts +17 -0
- package/src/node/features/window-manager.ts +68 -20
- package/src/node.ts +5 -0
- package/src/scaffolds/generated.ts +492 -290
- package/src/scaffolds/template.ts +9 -0
- package/src/schemas/base.ts +46 -5
- package/src/selector.ts +282 -0
- package/src/server.ts +11 -0
- package/src/servers/express.ts +27 -12
- package/src/servers/socket.ts +45 -11
- package/src/web/clients/socket.ts +4 -1
- package/src/web/container.ts +2 -1
- package/src/web/features/network.ts +7 -1
- package/src/web/features/voice-recognition.ts +16 -1
- package/test/clients-servers.test.ts +2 -1
- package/test/command.test.ts +267 -0
- package/test-integration/assistants-manager.test.ts +10 -20
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/1b/b5/c75b28794f00f94c4d609a98978e9420e9b7146d204a7fbf5b0b30477292581705d207c0100dabaac27eef540aaaece3374af75104a93219d4ec8bfb44e7 +1 -0
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/da/df/1d90ce4e042abeb035a197832c6d6893420a747a056be773eb00e4f745a037d505c8db13dde7d36b36b6b893addbb7df0f5fe9f0c13e665f20056447318b +1 -0
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/ed/04/e1d0c2a58c2db29b3921ca2affb3ea4febe831c53b38ebc21019fb799823aba6ed5b4611873d2cd25d422d49955b852a9c326da0d678899bc1c2c2960901 +1 -0
- package/tmp/.cache/luca-disk-cache/index-v5/00/13/572aa4c9a94f99eda999695d050cdd0ca7fe2d23a50af03234d4c8ce0791 +2 -0
- package/tmp/.cache/luca-disk-cache/index-v5/75/a9/cb61dc0f0589e8ec10a9aca27b834bc73884c479941042d22a2b22324cd3 +2 -0
- package/tmp/.cache/luca-disk-cache/index-v5/9f/0f/8b1f915ee64cfff7667dd96acd7a5ac0a96aa91a346e19cefd45909a9c9c +2 -0
- package/docs/apis/features/node/launcher-app-command-listener.md +0 -145
- package/docs/examples/launcher-app-command-listener.md +0 -120
- package/docs/tasks/web-container-helper-discovery.md +0 -71
- package/docs/todos.md +0 -1
- package/scripts/test-command-listener.ts +0 -123
- package/src/node/features/launcher-app-command-listener.ts +0 -389
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
|
package/src/graft.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { Helper } from './helper.js'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
export type GraftScope = 'features' | 'clients' | 'servers' | 'commands' | 'endpoints' | 'selectors'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Export names that map to static class properties or receive special handling.
|
|
8
|
+
* Everything else that is a function goes onto the prototype as a method.
|
|
9
|
+
*/
|
|
10
|
+
const RESERVED_EXPORTS = new Set([
|
|
11
|
+
'description', 'envVars',
|
|
12
|
+
'stateSchema', 'optionsSchema', 'eventsSchema',
|
|
13
|
+
'args', 'argsSchema',
|
|
14
|
+
'positionals',
|
|
15
|
+
'getters',
|
|
16
|
+
'run', 'handler',
|
|
17
|
+
'cacheKey', 'cacheable',
|
|
18
|
+
'default',
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Returns true when the candidate is a class (constructor function) whose
|
|
23
|
+
* prototype chain includes BaseClass. Handles cross-module-boundary cases
|
|
24
|
+
* by checking both reference equality and class name.
|
|
25
|
+
*/
|
|
26
|
+
export function isNativeHelperClass(candidate: unknown, BaseClass: typeof Helper | any): boolean {
|
|
27
|
+
if (typeof candidate !== 'function') return false
|
|
28
|
+
if (!BaseClass) return false
|
|
29
|
+
if (candidate === BaseClass) return true
|
|
30
|
+
|
|
31
|
+
let proto = Object.getPrototypeOf(candidate)
|
|
32
|
+
while (proto) {
|
|
33
|
+
if (proto === BaseClass) return true
|
|
34
|
+
if (BaseClass.name && proto.name === BaseClass.name) return true
|
|
35
|
+
proto = Object.getPrototypeOf(proto)
|
|
36
|
+
}
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Synthesize a Helper subclass from plain module exports.
|
|
42
|
+
*
|
|
43
|
+
* Given a set of exports from a module file and a Helper base class, produces a
|
|
44
|
+
* fully-formed subclass ready for registration in the appropriate registry.
|
|
45
|
+
*
|
|
46
|
+
* Static export mappings:
|
|
47
|
+
* description → static description
|
|
48
|
+
* stateSchema → static stateSchema
|
|
49
|
+
* optionsSchema → static optionsSchema (also aliased from `args`)
|
|
50
|
+
* eventsSchema → static eventsSchema
|
|
51
|
+
* envVars → static envVars
|
|
52
|
+
* argsSchema → static argsSchema (Command scope; also aliased from `args`)
|
|
53
|
+
* positionals → static positionals (Command scope; stored for CLI dispatch mapping)
|
|
54
|
+
* getters → Object.defineProperty(proto, key, { get }) per key
|
|
55
|
+
* run → override run() for Command scope (receives named args + context)
|
|
56
|
+
* handler → legacy: wired through parseArgs() for backward compat
|
|
57
|
+
* [fn exports] → prototype methods (all other function-valued named exports)
|
|
58
|
+
*/
|
|
59
|
+
export function graftModule<T extends typeof Helper>(
|
|
60
|
+
BaseClass: T,
|
|
61
|
+
moduleExports: Record<string, any>,
|
|
62
|
+
id: string,
|
|
63
|
+
scope: GraftScope,
|
|
64
|
+
): T {
|
|
65
|
+
// Resolve schemas — prefer explicit exports, fall back to whatever the base class has
|
|
66
|
+
const optionsSchema = moduleExports.optionsSchema
|
|
67
|
+
?? moduleExports.args
|
|
68
|
+
?? (BaseClass as any).optionsSchema
|
|
69
|
+
|
|
70
|
+
const stateSchema = moduleExports.stateSchema
|
|
71
|
+
?? (BaseClass as any).stateSchema
|
|
72
|
+
|
|
73
|
+
const eventsSchema = moduleExports.eventsSchema
|
|
74
|
+
?? (BaseClass as any).eventsSchema
|
|
75
|
+
|
|
76
|
+
// Build the subclass
|
|
77
|
+
const GraftedClass = class extends (BaseClass as any) {} as unknown as T & { prototype: any }
|
|
78
|
+
|
|
79
|
+
// Static overrides
|
|
80
|
+
const statics: Record<string, any> = {
|
|
81
|
+
shortcut: `${scope}.${id}`,
|
|
82
|
+
description: moduleExports.description ?? '',
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (moduleExports.envVars) statics.envVars = moduleExports.envVars
|
|
86
|
+
if (optionsSchema) statics.optionsSchema = optionsSchema
|
|
87
|
+
if (stateSchema) statics.stateSchema = stateSchema
|
|
88
|
+
if (eventsSchema) statics.eventsSchema = eventsSchema
|
|
89
|
+
|
|
90
|
+
// Command-specific statics
|
|
91
|
+
if (scope === 'commands') {
|
|
92
|
+
const argsSchema = moduleExports.argsSchema ?? moduleExports.args ?? optionsSchema
|
|
93
|
+
statics.argsSchema = argsSchema
|
|
94
|
+
statics.commandDescription = moduleExports.description ?? ''
|
|
95
|
+
if (moduleExports.positionals) {
|
|
96
|
+
statics.positionals = moduleExports.positionals
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Selector-specific statics (must be set before Object.assign)
|
|
101
|
+
if (scope === 'selectors') {
|
|
102
|
+
statics.cacheable = moduleExports.cacheable !== false
|
|
103
|
+
statics.selectorDescription = moduleExports.description ?? ''
|
|
104
|
+
statics.argsSchema = moduleExports.argsSchema ?? moduleExports.args ?? optionsSchema
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
Object.assign(GraftedClass, statics)
|
|
108
|
+
|
|
109
|
+
// Wire run() for Command scope
|
|
110
|
+
if (scope === 'commands') {
|
|
111
|
+
if (typeof moduleExports.run === 'function') {
|
|
112
|
+
const runFn = moduleExports.run
|
|
113
|
+
;(GraftedClass as any).prototype.run = async function (args: any, context: any) {
|
|
114
|
+
return runFn(args, context)
|
|
115
|
+
}
|
|
116
|
+
} else if (typeof moduleExports.handler === 'function') {
|
|
117
|
+
const handlerFn = moduleExports.handler
|
|
118
|
+
;(GraftedClass as any).prototype.run = async function (args: any, context: any) {
|
|
119
|
+
return handlerFn(args, context)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Wire run() and resolveCacheKey() for Selector scope
|
|
125
|
+
if (scope === 'selectors') {
|
|
126
|
+
if (typeof moduleExports.run === 'function') {
|
|
127
|
+
const runFn = moduleExports.run
|
|
128
|
+
;(GraftedClass as any).prototype.run = async function (args: any, context: any) {
|
|
129
|
+
return runFn(args, context)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (typeof moduleExports.cacheKey === 'function') {
|
|
134
|
+
const cacheKeyFn = moduleExports.cacheKey
|
|
135
|
+
;(GraftedClass as any).prototype.resolveCacheKey = function (args: any, context: any) {
|
|
136
|
+
return cacheKeyFn(args, context)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Install getters from the `getters` export
|
|
142
|
+
const getterMap: Record<string, () => any> = moduleExports.getters ?? {}
|
|
143
|
+
for (const [key, fn] of Object.entries(getterMap)) {
|
|
144
|
+
if (typeof fn === 'function') {
|
|
145
|
+
Object.defineProperty((GraftedClass as any).prototype, key, {
|
|
146
|
+
get: fn,
|
|
147
|
+
configurable: true,
|
|
148
|
+
enumerable: false,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Graft all exported functions (that are not reserved) as prototype methods
|
|
154
|
+
for (const [key, value] of Object.entries(moduleExports)) {
|
|
155
|
+
if (RESERVED_EXPORTS.has(key)) continue
|
|
156
|
+
if (typeof value !== 'function') continue
|
|
157
|
+
;(GraftedClass as any).prototype[key] = value
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Name the class
|
|
161
|
+
const suffix = SCOPE_SUFFIXES[scope] || ''
|
|
162
|
+
const className = pascalCase(id) + suffix
|
|
163
|
+
Object.defineProperty(GraftedClass, 'name', { value: className, configurable: true })
|
|
164
|
+
|
|
165
|
+
return GraftedClass as T
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const SCOPE_SUFFIXES: Record<string, string> = {
|
|
169
|
+
features: 'Feature',
|
|
170
|
+
clients: 'Client',
|
|
171
|
+
servers: 'Server',
|
|
172
|
+
commands: 'Command',
|
|
173
|
+
endpoints: 'Endpoint',
|
|
174
|
+
selectors: 'Selector',
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function pascalCase(id: string): string {
|
|
178
|
+
return id
|
|
179
|
+
.replace(/[-_](.)/g, (_: string, c: string) => c.toUpperCase())
|
|
180
|
+
.replace(/^(.)/, (_: string, c: string) => c.toUpperCase())
|
|
181
|
+
}
|