@soederpop/luca 0.0.20 → 0.0.22
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/docs/bootstrap/SKILL.md +16 -0
- package/luca.cli.ts +14 -1
- package/package.json +1 -1
- package/src/bootstrap/generated.ts +17 -1
- package/src/cli/cli.ts +45 -6
- package/src/commands/chat.ts +14 -0
- package/src/commands/prompt.ts +218 -65
- package/src/introspection/generated.agi.ts +1534 -1199
- package/src/introspection/generated.node.ts +894 -559
- package/src/introspection/generated.web.ts +1 -1
- package/src/node/container.ts +35 -1
- package/src/node/features/google-auth.ts +1 -0
- package/src/node/features/google-calendar.ts +5 -0
- package/src/node/features/google-docs.ts +8 -1
- package/src/node/features/google-drive.ts +6 -0
- package/src/node/features/google-mail.ts +540 -0
- package/src/node/features/google-sheets.ts +6 -0
- package/src/scaffolds/generated.ts +1 -1
package/docs/bootstrap/SKILL.md
CHANGED
|
@@ -26,6 +26,13 @@ luca describe clients # index of all available clients
|
|
|
26
26
|
luca describe servers # index of all available servers
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
You can even learn about features in the browser container, or a specific platform (server, node are the same, browser,web are the same)
|
|
30
|
+
|
|
31
|
+
```shell
|
|
32
|
+
luca describe features --platform=web
|
|
33
|
+
luca describe features --platform=server
|
|
34
|
+
```
|
|
35
|
+
|
|
29
36
|
### Learn about specific helpers
|
|
30
37
|
|
|
31
38
|
```shell
|
|
@@ -230,6 +237,15 @@ This is useful inside commands and scripts where you need introspection data pro
|
|
|
230
237
|
|
|
231
238
|
---
|
|
232
239
|
|
|
240
|
+
## Server development troubleshooting
|
|
241
|
+
|
|
242
|
+
- You can use `container.proc.findPidsByPort(3000)` which will return an array of numbers.
|
|
243
|
+
- You can use `container.proc.kill(pid)` to kill that process
|
|
244
|
+
- You can combine these two functions in `luca eval` if a server you're developing won't start because a previous instance is running (common inside e.g. claude code sessions )
|
|
245
|
+
- `luca serve --force` will also replace the running process with the current one
|
|
246
|
+
- `luca serve --any-port` will open on any port
|
|
247
|
+
|
|
248
|
+
|
|
233
249
|
## Reference
|
|
234
250
|
|
|
235
251
|
See `references/api-docs/` for the full pre-generated API reference for every built-in feature, client, and server.
|
package/luca.cli.ts
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
-
export async function main(container) {
|
|
1
|
+
export async function main(container: any) {
|
|
2
2
|
container.addContext('luca', container)
|
|
3
|
+
|
|
4
|
+
try {
|
|
5
|
+
container.onMissingCommand(handleMissingCommand)
|
|
6
|
+
} catch(error) {
|
|
7
|
+
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async function handleMissingCommand({ words, phrase } : { words: string[], phrase: string }) {
|
|
12
|
+
const { ui } = container
|
|
13
|
+
|
|
14
|
+
ui.print.red('oh shit ' + phrase)
|
|
15
|
+
}
|
|
3
16
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soederpop/luca",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"website": "https://luca.soederpop.com",
|
|
5
5
|
"description": "lightweight universal conversational architecture AKA Le Ultimate Component Architecture AKA Last Universal Common Ancestor, part AI part Human",
|
|
6
6
|
"author": "jon soeder aka the people's champ <jon@soederpop.com>",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Auto-generated bootstrap content
|
|
2
|
-
// Generated at: 2026-03-
|
|
2
|
+
// Generated at: 2026-03-21T05:25:04.090Z
|
|
3
3
|
// Source: docs/bootstrap/*.md, docs/bootstrap/templates/*
|
|
4
4
|
//
|
|
5
5
|
// Do not edit manually. Run: luca build-bootstrap
|
|
@@ -33,6 +33,13 @@ luca describe clients # index of all available clients
|
|
|
33
33
|
luca describe servers # index of all available servers
|
|
34
34
|
\`\`\`
|
|
35
35
|
|
|
36
|
+
You can even learn about features in the browser container, or a specific platform (server, node are the same, browser,web are the same)
|
|
37
|
+
|
|
38
|
+
\`\`\`shell
|
|
39
|
+
luca describe features --platform=web
|
|
40
|
+
luca describe features --platform=server
|
|
41
|
+
\`\`\`
|
|
42
|
+
|
|
36
43
|
### Learn about specific helpers
|
|
37
44
|
|
|
38
45
|
\`\`\`shell
|
|
@@ -237,6 +244,15 @@ This is useful inside commands and scripts where you need introspection data pro
|
|
|
237
244
|
|
|
238
245
|
---
|
|
239
246
|
|
|
247
|
+
## Server development troubleshooting
|
|
248
|
+
|
|
249
|
+
- You can use \`container.proc.findPidsByPort(3000)\` which will return an array of numbers.
|
|
250
|
+
- You can use \`container.proc.kill(pid)\` to kill that process
|
|
251
|
+
- You can combine these two functions in \`luca eval\` if a server you're developing won't start because a previous instance is running (common inside e.g. claude code sessions )
|
|
252
|
+
- \`luca serve --force\` will also replace the running process with the current one
|
|
253
|
+
- \`luca serve --any-port\` will open on any port
|
|
254
|
+
|
|
255
|
+
|
|
240
256
|
## Reference
|
|
241
257
|
|
|
242
258
|
See \`references/api-docs/\` for the full pre-generated API reference for every built-in feature, client, and server.
|
package/src/cli/cli.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { join } from 'path'
|
|
|
19
19
|
async function main() {
|
|
20
20
|
const profile = process.env.LUCA_PROFILE === '1'
|
|
21
21
|
const t = (label?: string) => {
|
|
22
|
-
if (!profile) return () => {}
|
|
22
|
+
if (!profile) return () => { }
|
|
23
23
|
const start = performance.now()
|
|
24
24
|
return (suffix?: string) => {
|
|
25
25
|
const ms = (performance.now() - start).toFixed(1)
|
|
@@ -59,8 +59,8 @@ async function main() {
|
|
|
59
59
|
const afterUser = new Set(container.commands.available as string[])
|
|
60
60
|
const userCommands = new Set([...afterUser].filter((n) => !builtinCommands.has(n) && !projectCommands.has(n)))
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
// Store command sources for help display
|
|
63
|
+
; (container as any)._commandSources = { builtinCommands, projectCommands, userCommands }
|
|
64
64
|
|
|
65
65
|
// Load generated introspection data if present
|
|
66
66
|
done = t('loadProjectIntrospection')
|
|
@@ -82,9 +82,33 @@ async function main() {
|
|
|
82
82
|
await cmd.dispatch()
|
|
83
83
|
} else if (commandName) {
|
|
84
84
|
// not a known command — treat as implicit `run`
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
//
|
|
86
|
+
if (resolveScript(commandName, container)) {
|
|
87
|
+
container.argv._.splice(0, 0, 'run')
|
|
88
|
+
const cmd = container.command('run' as any)
|
|
89
|
+
await cmd.dispatch()
|
|
90
|
+
} else {
|
|
91
|
+
|
|
92
|
+
// @ts-ignore TODO come up with a typesafe way to do this
|
|
93
|
+
if (container.state.get('missingCommandHandler')) {
|
|
94
|
+
// @ts-ignore TODO come up with a typesafe way to do this
|
|
95
|
+
const missingCommandHandler = container.state.get('missingCommandHandler') as any
|
|
96
|
+
|
|
97
|
+
if (typeof missingCommandHandler === 'function') {
|
|
98
|
+
await missingCommandHandler({
|
|
99
|
+
words: container.argv._,
|
|
100
|
+
phrase: container.argv._.join(' ')
|
|
101
|
+
}).catch((err: any) => {
|
|
102
|
+
console.error(`Missing command handler error: ${err.message}`, err)
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
container.argv._.splice(0, 0, 'help')
|
|
107
|
+
const cmd = container.command('help' as any)
|
|
108
|
+
await cmd.dispatch()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
88
112
|
} else {
|
|
89
113
|
container.argv._.splice(0, 0, 'help')
|
|
90
114
|
const cmd = container.command('help' as any)
|
|
@@ -94,6 +118,21 @@ async function main() {
|
|
|
94
118
|
tTotal()
|
|
95
119
|
}
|
|
96
120
|
|
|
121
|
+
function resolveScript(ref: string, container: any) {
|
|
122
|
+
const candidates = [
|
|
123
|
+
ref,
|
|
124
|
+
`${ref}.ts`,
|
|
125
|
+
`${ref}.js`,
|
|
126
|
+
`${ref}.md`,
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
for (const candidate of candidates) {
|
|
130
|
+
const resolved = container.paths.resolve(candidate)
|
|
131
|
+
if (container.fs.exists(resolved)) return resolved
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
97
136
|
|
|
98
137
|
async function loadCliModule() {
|
|
99
138
|
const modulePath = container.paths.resolve('luca.cli.ts')
|
package/src/commands/chat.ts
CHANGED
|
@@ -187,8 +187,20 @@ export default async function chat(options: z.infer<typeof argsSchema>, context:
|
|
|
187
187
|
output: process.stdout,
|
|
188
188
|
})
|
|
189
189
|
|
|
190
|
+
let rlClosed = false
|
|
191
|
+
rl.on('close', () => { rlClosed = true })
|
|
192
|
+
|
|
193
|
+
function ensureRl() {
|
|
194
|
+
if (rlClosed) {
|
|
195
|
+
rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
196
|
+
rlClosed = false
|
|
197
|
+
rl.on('close', () => { rlClosed = true })
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
190
201
|
function prompt(): Promise<string> {
|
|
191
202
|
return new Promise((resolve) => {
|
|
203
|
+
ensureRl()
|
|
192
204
|
rl.question(ui.colors.dim(`\n${name} > `), (answer: string) => resolve(answer.trim()))
|
|
193
205
|
})
|
|
194
206
|
}
|
|
@@ -247,6 +259,8 @@ export default async function chat(options: z.infer<typeof argsSchema>, context:
|
|
|
247
259
|
console.log()
|
|
248
260
|
console.log(ui.colors.dim(` Back in chat with ${ui.colors.cyan(name)}.`))
|
|
249
261
|
rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
262
|
+
rlClosed = false
|
|
263
|
+
rl.on('close', () => { rlClosed = true })
|
|
250
264
|
continue
|
|
251
265
|
}
|
|
252
266
|
|
package/src/commands/prompt.ts
CHANGED
|
@@ -12,18 +12,26 @@ declare module '../command.js' {
|
|
|
12
12
|
|
|
13
13
|
export const argsSchema = CommandOptionsSchema.extend({
|
|
14
14
|
model: z.string().optional().describe('Override the LLM model (assistant mode only)'),
|
|
15
|
-
'preserve-frontmatter': z.boolean().default(false).describe('Keep YAML frontmatter in the prompt instead of stripping it'),
|
|
15
|
+
'preserve-frontmatter': z.boolean().default(false).describe('Keep YAML frontmatter in the prompt instead of stripping it before sending to the agent.'),
|
|
16
16
|
'permission-mode': z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan']).default('acceptEdits').describe('Permission mode for CLI agents (default: acceptEdits)'),
|
|
17
17
|
'in-folder': z.string().optional().describe('Run the CLI agent in this directory (resolved via container.paths)'),
|
|
18
18
|
'out-file': z.string().optional().describe('Save session output as a markdown file'),
|
|
19
19
|
'include-output': z.boolean().default(false).describe('Include tool call outputs in the markdown (requires --out-file)'),
|
|
20
|
-
'
|
|
21
|
-
'repeat-anyway': z.boolean().default(false).describe('Run even if repeatable is false and the prompt has already been run'),
|
|
20
|
+
'inputs-file': z.string().optional().describe('Path to a JSON or YAML file supplying input values'),
|
|
22
21
|
'parallel': z.boolean().default(false).describe('Run multiple prompt files in parallel with side-by-side terminal UI'),
|
|
23
22
|
'exclude-sections': z.string().optional().describe('Comma-separated list of section headings to exclude from the prompt'),
|
|
24
23
|
'chrome': z.boolean().default(false).describe('Launch Claude Code with a Chrome browser tool'),
|
|
24
|
+
'dry-run': z.boolean().default(false).describe('Display the resolved prompt and options without running the assistant'),
|
|
25
|
+
'local': z.boolean().default(false).describe('Use local models for assistant mode'),
|
|
25
26
|
})
|
|
26
27
|
|
|
28
|
+
function normalizeTarget(raw: string): string {
|
|
29
|
+
const lower = raw.toLowerCase().replace(/[-_]/g, '')
|
|
30
|
+
if (/claude/.test(lower)) return 'claude'
|
|
31
|
+
if (/codex/.test(lower) || /openai/.test(lower)) return 'codex'
|
|
32
|
+
return raw
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
const CLI_TARGETS = new Set(['claude', 'codex'])
|
|
28
36
|
|
|
29
37
|
function formatSessionMarkdown(events: any[], includeOutput: boolean): string {
|
|
@@ -72,9 +80,10 @@ interface PreparedPrompt {
|
|
|
72
80
|
resolvedPath: string
|
|
73
81
|
promptContent: string
|
|
74
82
|
filename: string
|
|
83
|
+
agentOptions: Record<string, any>
|
|
75
84
|
}
|
|
76
85
|
|
|
77
|
-
async function runClaudeOrCodex(target: 'claude' | 'codex', promptContent: string, container: any, options: z.infer<typeof argsSchema>): Promise<RunStats> {
|
|
86
|
+
async function runClaudeOrCodex(target: 'claude' | 'codex', promptContent: string, container: any, options: z.infer<typeof argsSchema>, agentOptions: Record<string, any> = {}): Promise<RunStats> {
|
|
78
87
|
const ui = container.feature('ui')
|
|
79
88
|
const featureName = target === 'claude' ? 'claudeCode' : 'openaiCodex'
|
|
80
89
|
const feature = container.feature(featureName)
|
|
@@ -118,7 +127,7 @@ async function runClaudeOrCodex(target: 'claude' | 'codex', promptContent: strin
|
|
|
118
127
|
})
|
|
119
128
|
}
|
|
120
129
|
|
|
121
|
-
const runOptions: Record<string, any> = { streaming: true }
|
|
130
|
+
const runOptions: Record<string, any> = { streaming: true, ...agentOptions }
|
|
122
131
|
|
|
123
132
|
if (options['in-folder']) {
|
|
124
133
|
runOptions.cwd = container.paths.resolve(options['in-folder'])
|
|
@@ -129,6 +138,9 @@ async function runClaudeOrCodex(target: 'claude' | 'codex', promptContent: strin
|
|
|
129
138
|
if (options.chrome) runOptions.chrome = true
|
|
130
139
|
}
|
|
131
140
|
|
|
141
|
+
// CLI flags override agentOptions from frontmatter
|
|
142
|
+
if (options.model) runOptions.model = options.model
|
|
143
|
+
|
|
132
144
|
const startTime = Date.now()
|
|
133
145
|
const sessionId = await feature.start(promptContent, runOptions)
|
|
134
146
|
const session = await feature.waitForSession(sessionId)
|
|
@@ -143,7 +155,7 @@ async function runClaudeOrCodex(target: 'claude' | 'codex', promptContent: strin
|
|
|
143
155
|
return { collectedEvents, durationMs: Date.now() - startTime, outputTokens }
|
|
144
156
|
}
|
|
145
157
|
|
|
146
|
-
async function runAssistant(name: string, promptContent: string, options: z.infer<typeof argsSchema>, container: any): Promise<RunStats> {
|
|
158
|
+
async function runAssistant(name: string, promptContent: string, options: z.infer<typeof argsSchema>, container: any, agentOptions: Record<string, any> = {}): Promise<RunStats> {
|
|
147
159
|
const ui = container.feature('ui')
|
|
148
160
|
const manager = container.feature('assistantsManager')
|
|
149
161
|
await manager.discover()
|
|
@@ -156,8 +168,10 @@ async function runAssistant(name: string, promptContent: string, options: z.infe
|
|
|
156
168
|
process.exit(1)
|
|
157
169
|
}
|
|
158
170
|
|
|
159
|
-
const createOptions: Record<string, any> = {}
|
|
171
|
+
const createOptions: Record<string, any> = { ...agentOptions }
|
|
172
|
+
// CLI flags override agentOptions from frontmatter
|
|
160
173
|
if (options.model) createOptions.model = options.model
|
|
174
|
+
if (options.local) createOptions.local = true
|
|
161
175
|
|
|
162
176
|
const assistant = manager.create(name, createOptions)
|
|
163
177
|
let isFirstChunk = true
|
|
@@ -311,9 +325,11 @@ async function runParallel(
|
|
|
311
325
|
})
|
|
312
326
|
}
|
|
313
327
|
|
|
314
|
-
// Start all sessions
|
|
328
|
+
// Start all sessions — merge per-prompt agentOptions with shared runOptions
|
|
315
329
|
for (let i = 0; i < prepared.length; i++) {
|
|
316
|
-
const
|
|
330
|
+
const perPromptOptions = { ...prepared[i].agentOptions, ...runOptions }
|
|
331
|
+
if (options.model) perPromptOptions.model = options.model
|
|
332
|
+
const id = await feature.start(prepared[i].promptContent, perPromptOptions)
|
|
317
333
|
sessionMap.set(id, i)
|
|
318
334
|
}
|
|
319
335
|
|
|
@@ -348,12 +364,12 @@ async function runParallel(
|
|
|
348
364
|
process.exit(1)
|
|
349
365
|
}
|
|
350
366
|
|
|
351
|
-
const createOptions: Record<string, any> = {}
|
|
352
|
-
if (options.model) createOptions.model = options.model
|
|
353
|
-
|
|
354
367
|
const lineBuffers: string[] = prepared.map(() => '')
|
|
355
368
|
|
|
356
369
|
const assistants = prepared.map((p, i) => {
|
|
370
|
+
const createOptions: Record<string, any> = { ...p.agentOptions }
|
|
371
|
+
if (options.model) createOptions.model = options.model
|
|
372
|
+
if (options.local) createOptions.local = true
|
|
357
373
|
const assistant = manager.create(target, createOptions)
|
|
358
374
|
|
|
359
375
|
assistant.on('chunk', (text: string) => {
|
|
@@ -526,24 +542,6 @@ async function runParallel(
|
|
|
526
542
|
// Wait for sessions to fully settle
|
|
527
543
|
await sessionPromise
|
|
528
544
|
|
|
529
|
-
// Post-completion: update frontmatter
|
|
530
|
-
if (!options['dont-touch-file']) {
|
|
531
|
-
for (let i = 0; i < promptStates.length; i++) {
|
|
532
|
-
const ps = promptStates[i]
|
|
533
|
-
if (ps.status === 'error') continue
|
|
534
|
-
const rawContent = fs.readFile(prepared[i].resolvedPath) as string
|
|
535
|
-
const updates: Record<string, any> = {
|
|
536
|
-
lastRanAt: Date.now(),
|
|
537
|
-
durationMs: ps.durationMs,
|
|
538
|
-
}
|
|
539
|
-
if (ps.outputTokens > 0) {
|
|
540
|
-
updates.outputTokens = ps.outputTokens
|
|
541
|
-
}
|
|
542
|
-
const updated = updateFrontmatter(rawContent, updates, container)
|
|
543
|
-
await Bun.write(prepared[i].resolvedPath, updated)
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
545
|
// Post-completion: out-files
|
|
548
546
|
if (options['out-file']) {
|
|
549
547
|
const base = options['out-file']
|
|
@@ -573,26 +571,123 @@ async function runParallel(
|
|
|
573
571
|
}
|
|
574
572
|
}
|
|
575
573
|
|
|
576
|
-
|
|
574
|
+
interface InputDef {
|
|
575
|
+
description?: string
|
|
576
|
+
required?: boolean
|
|
577
|
+
default?: any
|
|
578
|
+
type?: string
|
|
579
|
+
choices?: string[]
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function parseInputDefs(meta: Record<string, any>): Record<string, InputDef> | null {
|
|
583
|
+
if (!meta?.inputs || typeof meta.inputs !== 'object') return null
|
|
584
|
+
const defs: Record<string, InputDef> = {}
|
|
585
|
+
for (const [key, val] of Object.entries(meta.inputs)) {
|
|
586
|
+
if (typeof val === 'object' && val !== null) {
|
|
587
|
+
defs[key] = val as InputDef
|
|
588
|
+
} else {
|
|
589
|
+
// Shorthand: `topic: "What to write about"` means description-only, required
|
|
590
|
+
defs[key] = { description: typeof val === 'string' ? val : String(val) }
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return Object.keys(defs).length ? defs : null
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async function resolveInputs(
|
|
597
|
+
inputDefs: Record<string, InputDef>,
|
|
598
|
+
options: z.infer<typeof argsSchema>,
|
|
599
|
+
container: any,
|
|
600
|
+
): Promise<Record<string, any>> {
|
|
601
|
+
const { fs, paths } = container
|
|
577
602
|
const yaml = container.feature('yaml')
|
|
603
|
+
const ui = container.feature('ui')
|
|
604
|
+
|
|
605
|
+
// Layer 1: inputs-file (lowest priority of supplied values)
|
|
606
|
+
let fileInputs: Record<string, any> = {}
|
|
607
|
+
if (options['inputs-file']) {
|
|
608
|
+
const filePath = paths.resolve(options['inputs-file'])
|
|
609
|
+
const raw = fs.readFile(filePath) as string
|
|
610
|
+
if (filePath.endsWith('.json')) {
|
|
611
|
+
fileInputs = JSON.parse(raw)
|
|
612
|
+
} else {
|
|
613
|
+
fileInputs = yaml.parse(raw) || {}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Layer 2: CLI flags (highest priority) — any unknown option that matches an input name
|
|
618
|
+
const cliInputs: Record<string, any> = {}
|
|
619
|
+
const argv = container.argv as Record<string, any>
|
|
620
|
+
for (const key of Object.keys(inputDefs)) {
|
|
621
|
+
if (argv[key] !== undefined) {
|
|
622
|
+
cliInputs[key] = argv[key]
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Merge: CLI > file > defaults
|
|
627
|
+
const supplied: Record<string, any> = {}
|
|
628
|
+
for (const [key, def] of Object.entries(inputDefs)) {
|
|
629
|
+
if (cliInputs[key] !== undefined) {
|
|
630
|
+
supplied[key] = cliInputs[key]
|
|
631
|
+
} else if (fileInputs[key] !== undefined) {
|
|
632
|
+
supplied[key] = fileInputs[key]
|
|
633
|
+
} else if (def.default !== undefined) {
|
|
634
|
+
supplied[key] = def.default
|
|
635
|
+
}
|
|
636
|
+
}
|
|
578
637
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const newYaml = yaml.stringify(meta).trimEnd()
|
|
586
|
-
return `---\n${newYaml}\n---${fileContent.slice(endIndex + 4)}`
|
|
638
|
+
// Find missing required inputs
|
|
639
|
+
const missing: string[] = []
|
|
640
|
+
for (const [key, def] of Object.entries(inputDefs)) {
|
|
641
|
+
const isRequired = def.required !== false // default to required
|
|
642
|
+
if (isRequired && supplied[key] === undefined) {
|
|
643
|
+
missing.push(key)
|
|
587
644
|
}
|
|
588
645
|
}
|
|
589
646
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
647
|
+
if (missing.length === 0) return supplied
|
|
648
|
+
|
|
649
|
+
// In parallel mode, we can't run an interactive wizard
|
|
650
|
+
if ((options as any).parallel) {
|
|
651
|
+
console.error(`Missing required inputs for parallel mode (use --inputs-file or CLI flags): ${missing.join(', ')}`)
|
|
652
|
+
process.exit(1)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Build wizard questions for missing inputs
|
|
656
|
+
const questions = missing.map((key) => {
|
|
657
|
+
const def = inputDefs[key]
|
|
658
|
+
const q: Record<string, any> = {
|
|
659
|
+
name: key,
|
|
660
|
+
message: def.description || key,
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Auto-infer type
|
|
664
|
+
if (def.choices?.length) {
|
|
665
|
+
q.type = 'list'
|
|
666
|
+
q.choices = def.choices
|
|
667
|
+
} else if (def.type) {
|
|
668
|
+
q.type = def.type
|
|
669
|
+
} else {
|
|
670
|
+
q.type = 'input'
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (def.default !== undefined) {
|
|
674
|
+
q.default = def.default
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return q
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
const answers = await ui.wizard(questions, supplied)
|
|
681
|
+
return { ...supplied, ...answers }
|
|
593
682
|
}
|
|
594
683
|
|
|
595
|
-
|
|
684
|
+
function substituteInputs(content: string, inputs: Record<string, any>): string {
|
|
685
|
+
return content.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
686
|
+
return inputs[key] !== undefined ? String(inputs[key]) : match
|
|
687
|
+
})
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async function executePromptFile(resolvedPath: string, container: any, inputs?: Record<string, any>): Promise<string> {
|
|
596
691
|
if (!container.docs.isLoaded) await container.docs.load()
|
|
597
692
|
const doc = await container.docs.parseMarkdownAtPath(resolvedPath)
|
|
598
693
|
const vm = container.feature('vm')
|
|
@@ -608,6 +703,7 @@ async function executePromptFile(resolvedPath: string, container: any): Promise<
|
|
|
608
703
|
|
|
609
704
|
const shared = vm.createContext({
|
|
610
705
|
...container.context,
|
|
706
|
+
INPUTS: inputs || {},
|
|
611
707
|
console: captureConsole,
|
|
612
708
|
setTimeout, clearTimeout, setInterval, clearInterval,
|
|
613
709
|
fetch, URL, URLSearchParams,
|
|
@@ -669,24 +765,40 @@ async function preparePrompt(
|
|
|
669
765
|
|
|
670
766
|
let content = fs.readFile(resolvedPath) as string
|
|
671
767
|
|
|
672
|
-
//
|
|
673
|
-
|
|
768
|
+
// Parse frontmatter for input definitions and agentOptions
|
|
769
|
+
let resolvedInputs: Record<string, any> = {}
|
|
770
|
+
let agentOptions: Record<string, any> = {}
|
|
771
|
+
let hasInputDefs = false
|
|
772
|
+
if (content.startsWith('---')) {
|
|
674
773
|
const fmEnd = content.indexOf('\n---', 3)
|
|
675
774
|
if (fmEnd !== -1) {
|
|
676
775
|
const yaml = container.feature('yaml')
|
|
677
776
|
const meta = yaml.parse(content.slice(4, fmEnd)) || {}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
777
|
+
const inputDefs = parseInputDefs(meta)
|
|
778
|
+
if (inputDefs) {
|
|
779
|
+
hasInputDefs = true
|
|
780
|
+
resolvedInputs = await resolveInputs(inputDefs, options, container)
|
|
781
|
+
}
|
|
782
|
+
if (meta.agentOptions && typeof meta.agentOptions === 'object') {
|
|
783
|
+
agentOptions = { ...meta.agentOptions }
|
|
681
784
|
}
|
|
682
785
|
}
|
|
683
786
|
}
|
|
684
787
|
|
|
788
|
+
if (options['inputs-file'] && !hasInputDefs) {
|
|
789
|
+
console.warn(`Warning: --inputs-file was provided but ${filePath} does not define any inputs in its frontmatter`)
|
|
790
|
+
}
|
|
791
|
+
|
|
685
792
|
let promptContent: string
|
|
686
793
|
if (options['preserve-frontmatter']) {
|
|
687
794
|
promptContent = content
|
|
688
795
|
} else {
|
|
689
|
-
promptContent = await executePromptFile(resolvedPath, container)
|
|
796
|
+
promptContent = await executePromptFile(resolvedPath, container, resolvedInputs)
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Substitute {{key}} placeholders with resolved input values
|
|
800
|
+
if (Object.keys(resolvedInputs).length) {
|
|
801
|
+
promptContent = substituteInputs(promptContent, resolvedInputs)
|
|
690
802
|
}
|
|
691
803
|
|
|
692
804
|
// Exclude sections by heading name
|
|
@@ -709,6 +821,7 @@ async function preparePrompt(
|
|
|
709
821
|
resolvedPath,
|
|
710
822
|
promptContent,
|
|
711
823
|
filename: paths.basename(resolvedPath),
|
|
824
|
+
agentOptions,
|
|
712
825
|
}
|
|
713
826
|
}
|
|
714
827
|
|
|
@@ -728,6 +841,9 @@ export default async function prompt(options: z.infer<typeof argsSchema>, contex
|
|
|
728
841
|
}
|
|
729
842
|
}
|
|
730
843
|
|
|
844
|
+
// Normalize target aliases (e.g. claude-code → claude, openai-codex → codex)
|
|
845
|
+
if (target) target = normalizeTarget(target)
|
|
846
|
+
|
|
731
847
|
if (!target || allPaths.length === 0) {
|
|
732
848
|
console.error('Usage: luca prompt [claude|codex|assistant-name] <path/to/prompt.md> [more paths...]')
|
|
733
849
|
process.exit(1)
|
|
@@ -751,6 +867,28 @@ export default async function prompt(options: z.infer<typeof argsSchema>, contex
|
|
|
751
867
|
process.exit(1)
|
|
752
868
|
}
|
|
753
869
|
|
|
870
|
+
if (options['dry-run']) {
|
|
871
|
+
const ui = container.feature('ui')
|
|
872
|
+
console.log(ui.colors.bold('\n── Dry Run (Parallel) ──\n'))
|
|
873
|
+
console.log(ui.colors.bold('Target:'), target)
|
|
874
|
+
console.log(ui.colors.bold('Prompts:'), prepared.length)
|
|
875
|
+
for (const p of prepared) {
|
|
876
|
+
console.log(ui.colors.bold(`\n── ${p.filename} ──`))
|
|
877
|
+
console.log(ui.colors.dim(` Path: ${p.resolvedPath}`))
|
|
878
|
+
console.log(ui.colors.dim(` Length: ${p.promptContent.length} chars`))
|
|
879
|
+
if (Object.keys(p.agentOptions).length) {
|
|
880
|
+
console.log(ui.colors.dim(' Agent options:'))
|
|
881
|
+
for (const [key, val] of Object.entries(p.agentOptions)) {
|
|
882
|
+
const display = typeof val === 'object' ? JSON.stringify(val) : val
|
|
883
|
+
console.log(ui.colors.dim(` ${key}: ${display}`))
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
console.log('')
|
|
887
|
+
process.stdout.write(ui.markdown(p.promptContent))
|
|
888
|
+
}
|
|
889
|
+
return
|
|
890
|
+
}
|
|
891
|
+
|
|
754
892
|
if (prepared.length > 1) {
|
|
755
893
|
await runParallel(target, prepared, options, container)
|
|
756
894
|
return
|
|
@@ -767,28 +905,43 @@ export default async function prompt(options: z.infer<typeof argsSchema>, contex
|
|
|
767
905
|
}
|
|
768
906
|
|
|
769
907
|
const ui = container.feature('ui')
|
|
908
|
+
|
|
909
|
+
if (options['dry-run']) {
|
|
910
|
+
const runOptions: Record<string, any> = { ...p.agentOptions }
|
|
911
|
+
if (options.model) runOptions.model = options.model
|
|
912
|
+
if (options['in-folder']) runOptions.cwd = container.paths.resolve(options['in-folder'])
|
|
913
|
+
if (options['out-file']) runOptions.outFile = options['out-file']
|
|
914
|
+
if (options['include-output']) runOptions.includeOutput = true
|
|
915
|
+
if (options['exclude-sections']) runOptions.excludeSections = options['exclude-sections']
|
|
916
|
+
if (CLI_TARGETS.has(target)) {
|
|
917
|
+
runOptions.permissionMode = options['permission-mode']
|
|
918
|
+
if (options.chrome) runOptions.chrome = true
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
console.log(ui.colors.bold('\n── Dry Run ──\n'))
|
|
922
|
+
console.log(ui.colors.bold('Target:'), target)
|
|
923
|
+
console.log(ui.colors.bold('Prompt file:'), p.resolvedPath)
|
|
924
|
+
console.log(ui.colors.bold('Prompt length:'), `${p.promptContent.length} chars`)
|
|
925
|
+
if (Object.keys(runOptions).length) {
|
|
926
|
+
console.log(ui.colors.bold('Options:'))
|
|
927
|
+
for (const [key, val] of Object.entries(runOptions)) {
|
|
928
|
+
const display = typeof val === 'object' ? JSON.stringify(val) : val
|
|
929
|
+
console.log(` ${key}: ${display}`)
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
console.log(ui.colors.bold('\n── Prompt Content ──\n'))
|
|
933
|
+
process.stdout.write(ui.markdown(p.promptContent))
|
|
934
|
+
return
|
|
935
|
+
}
|
|
936
|
+
|
|
770
937
|
process.stdout.write(ui.markdown(p.promptContent))
|
|
771
938
|
|
|
772
939
|
let stats: RunStats
|
|
773
940
|
|
|
774
941
|
if (CLI_TARGETS.has(target)) {
|
|
775
|
-
stats = await runClaudeOrCodex(target as 'claude' | 'codex', p.promptContent, container, options)
|
|
942
|
+
stats = await runClaudeOrCodex(target as 'claude' | 'codex', p.promptContent, container, options, p.agentOptions)
|
|
776
943
|
} else {
|
|
777
|
-
stats = await runAssistant(target, p.promptContent, options, container)
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
// Update prompt file frontmatter with run stats
|
|
781
|
-
if (!options['dont-touch-file']) {
|
|
782
|
-
const rawContent = fs.readFile(p.resolvedPath) as string
|
|
783
|
-
const updates: Record<string, any> = {
|
|
784
|
-
lastRanAt: Date.now(),
|
|
785
|
-
durationMs: stats.durationMs,
|
|
786
|
-
}
|
|
787
|
-
if (stats.outputTokens > 0) {
|
|
788
|
-
updates.outputTokens = stats.outputTokens
|
|
789
|
-
}
|
|
790
|
-
const updated = updateFrontmatter(rawContent, updates, container)
|
|
791
|
-
await Bun.write(p.resolvedPath, updated)
|
|
944
|
+
stats = await runAssistant(target, p.promptContent, options, container, p.agentOptions)
|
|
792
945
|
}
|
|
793
946
|
|
|
794
947
|
if (options['out-file'] && stats.collectedEvents.length) {
|