@soederpop/luca 0.0.20 → 0.0.21
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/package.json +1 -1
- package/src/bootstrap/generated.ts +17 -1
- package/src/commands/chat.ts +14 -0
- package/src/commands/prompt.ts +204 -64
- package/src/introspection/generated.agi.ts +454 -454
- package/src/introspection/generated.node.ts +509 -509
- package/src/introspection/generated.web.ts +1 -1
- package/src/node/container.ts +4 -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soederpop/luca",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
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:24:16.832Z
|
|
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/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
|
@@ -17,11 +17,11 @@ export const argsSchema = CommandOptionsSchema.extend({
|
|
|
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
25
|
})
|
|
26
26
|
|
|
27
27
|
const CLI_TARGETS = new Set(['claude', 'codex'])
|
|
@@ -72,9 +72,10 @@ interface PreparedPrompt {
|
|
|
72
72
|
resolvedPath: string
|
|
73
73
|
promptContent: string
|
|
74
74
|
filename: string
|
|
75
|
+
agentOptions: Record<string, any>
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
async function runClaudeOrCodex(target: 'claude' | 'codex', promptContent: string, container: any, options: z.infer<typeof argsSchema>): Promise<RunStats> {
|
|
78
|
+
async function runClaudeOrCodex(target: 'claude' | 'codex', promptContent: string, container: any, options: z.infer<typeof argsSchema>, agentOptions: Record<string, any> = {}): Promise<RunStats> {
|
|
78
79
|
const ui = container.feature('ui')
|
|
79
80
|
const featureName = target === 'claude' ? 'claudeCode' : 'openaiCodex'
|
|
80
81
|
const feature = container.feature(featureName)
|
|
@@ -118,7 +119,7 @@ async function runClaudeOrCodex(target: 'claude' | 'codex', promptContent: strin
|
|
|
118
119
|
})
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
const runOptions: Record<string, any> = { streaming: true }
|
|
122
|
+
const runOptions: Record<string, any> = { streaming: true, ...agentOptions }
|
|
122
123
|
|
|
123
124
|
if (options['in-folder']) {
|
|
124
125
|
runOptions.cwd = container.paths.resolve(options['in-folder'])
|
|
@@ -129,6 +130,9 @@ async function runClaudeOrCodex(target: 'claude' | 'codex', promptContent: strin
|
|
|
129
130
|
if (options.chrome) runOptions.chrome = true
|
|
130
131
|
}
|
|
131
132
|
|
|
133
|
+
// CLI flags override agentOptions from frontmatter
|
|
134
|
+
if (options.model) runOptions.model = options.model
|
|
135
|
+
|
|
132
136
|
const startTime = Date.now()
|
|
133
137
|
const sessionId = await feature.start(promptContent, runOptions)
|
|
134
138
|
const session = await feature.waitForSession(sessionId)
|
|
@@ -143,7 +147,7 @@ async function runClaudeOrCodex(target: 'claude' | 'codex', promptContent: strin
|
|
|
143
147
|
return { collectedEvents, durationMs: Date.now() - startTime, outputTokens }
|
|
144
148
|
}
|
|
145
149
|
|
|
146
|
-
async function runAssistant(name: string, promptContent: string, options: z.infer<typeof argsSchema>, container: any): Promise<RunStats> {
|
|
150
|
+
async function runAssistant(name: string, promptContent: string, options: z.infer<typeof argsSchema>, container: any, agentOptions: Record<string, any> = {}): Promise<RunStats> {
|
|
147
151
|
const ui = container.feature('ui')
|
|
148
152
|
const manager = container.feature('assistantsManager')
|
|
149
153
|
await manager.discover()
|
|
@@ -156,7 +160,8 @@ async function runAssistant(name: string, promptContent: string, options: z.infe
|
|
|
156
160
|
process.exit(1)
|
|
157
161
|
}
|
|
158
162
|
|
|
159
|
-
const createOptions: Record<string, any> = {}
|
|
163
|
+
const createOptions: Record<string, any> = { ...agentOptions }
|
|
164
|
+
// CLI flags override agentOptions from frontmatter
|
|
160
165
|
if (options.model) createOptions.model = options.model
|
|
161
166
|
|
|
162
167
|
const assistant = manager.create(name, createOptions)
|
|
@@ -311,9 +316,11 @@ async function runParallel(
|
|
|
311
316
|
})
|
|
312
317
|
}
|
|
313
318
|
|
|
314
|
-
// Start all sessions
|
|
319
|
+
// Start all sessions — merge per-prompt agentOptions with shared runOptions
|
|
315
320
|
for (let i = 0; i < prepared.length; i++) {
|
|
316
|
-
const
|
|
321
|
+
const perPromptOptions = { ...prepared[i].agentOptions, ...runOptions }
|
|
322
|
+
if (options.model) perPromptOptions.model = options.model
|
|
323
|
+
const id = await feature.start(prepared[i].promptContent, perPromptOptions)
|
|
317
324
|
sessionMap.set(id, i)
|
|
318
325
|
}
|
|
319
326
|
|
|
@@ -348,12 +355,11 @@ async function runParallel(
|
|
|
348
355
|
process.exit(1)
|
|
349
356
|
}
|
|
350
357
|
|
|
351
|
-
const createOptions: Record<string, any> = {}
|
|
352
|
-
if (options.model) createOptions.model = options.model
|
|
353
|
-
|
|
354
358
|
const lineBuffers: string[] = prepared.map(() => '')
|
|
355
359
|
|
|
356
360
|
const assistants = prepared.map((p, i) => {
|
|
361
|
+
const createOptions: Record<string, any> = { ...p.agentOptions }
|
|
362
|
+
if (options.model) createOptions.model = options.model
|
|
357
363
|
const assistant = manager.create(target, createOptions)
|
|
358
364
|
|
|
359
365
|
assistant.on('chunk', (text: string) => {
|
|
@@ -526,24 +532,6 @@ async function runParallel(
|
|
|
526
532
|
// Wait for sessions to fully settle
|
|
527
533
|
await sessionPromise
|
|
528
534
|
|
|
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
535
|
// Post-completion: out-files
|
|
548
536
|
if (options['out-file']) {
|
|
549
537
|
const base = options['out-file']
|
|
@@ -573,26 +561,123 @@ async function runParallel(
|
|
|
573
561
|
}
|
|
574
562
|
}
|
|
575
563
|
|
|
576
|
-
|
|
564
|
+
interface InputDef {
|
|
565
|
+
description?: string
|
|
566
|
+
required?: boolean
|
|
567
|
+
default?: any
|
|
568
|
+
type?: string
|
|
569
|
+
choices?: string[]
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function parseInputDefs(meta: Record<string, any>): Record<string, InputDef> | null {
|
|
573
|
+
if (!meta?.inputs || typeof meta.inputs !== 'object') return null
|
|
574
|
+
const defs: Record<string, InputDef> = {}
|
|
575
|
+
for (const [key, val] of Object.entries(meta.inputs)) {
|
|
576
|
+
if (typeof val === 'object' && val !== null) {
|
|
577
|
+
defs[key] = val as InputDef
|
|
578
|
+
} else {
|
|
579
|
+
// Shorthand: `topic: "What to write about"` means description-only, required
|
|
580
|
+
defs[key] = { description: typeof val === 'string' ? val : String(val) }
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return Object.keys(defs).length ? defs : null
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async function resolveInputs(
|
|
587
|
+
inputDefs: Record<string, InputDef>,
|
|
588
|
+
options: z.infer<typeof argsSchema>,
|
|
589
|
+
container: any,
|
|
590
|
+
): Promise<Record<string, any>> {
|
|
591
|
+
const { fs, paths } = container
|
|
577
592
|
const yaml = container.feature('yaml')
|
|
593
|
+
const ui = container.feature('ui')
|
|
594
|
+
|
|
595
|
+
// Layer 1: inputs-file (lowest priority of supplied values)
|
|
596
|
+
let fileInputs: Record<string, any> = {}
|
|
597
|
+
if (options['inputs-file']) {
|
|
598
|
+
const filePath = paths.resolve(options['inputs-file'])
|
|
599
|
+
const raw = fs.readFile(filePath) as string
|
|
600
|
+
if (filePath.endsWith('.json')) {
|
|
601
|
+
fileInputs = JSON.parse(raw)
|
|
602
|
+
} else {
|
|
603
|
+
fileInputs = yaml.parse(raw) || {}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Layer 2: CLI flags (highest priority) — any unknown option that matches an input name
|
|
608
|
+
const cliInputs: Record<string, any> = {}
|
|
609
|
+
const argv = container.argv as Record<string, any>
|
|
610
|
+
for (const key of Object.keys(inputDefs)) {
|
|
611
|
+
if (argv[key] !== undefined) {
|
|
612
|
+
cliInputs[key] = argv[key]
|
|
613
|
+
}
|
|
614
|
+
}
|
|
578
615
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
616
|
+
// Merge: CLI > file > defaults
|
|
617
|
+
const supplied: Record<string, any> = {}
|
|
618
|
+
for (const [key, def] of Object.entries(inputDefs)) {
|
|
619
|
+
if (cliInputs[key] !== undefined) {
|
|
620
|
+
supplied[key] = cliInputs[key]
|
|
621
|
+
} else if (fileInputs[key] !== undefined) {
|
|
622
|
+
supplied[key] = fileInputs[key]
|
|
623
|
+
} else if (def.default !== undefined) {
|
|
624
|
+
supplied[key] = def.default
|
|
587
625
|
}
|
|
588
626
|
}
|
|
589
627
|
|
|
590
|
-
//
|
|
591
|
-
const
|
|
592
|
-
|
|
628
|
+
// Find missing required inputs
|
|
629
|
+
const missing: string[] = []
|
|
630
|
+
for (const [key, def] of Object.entries(inputDefs)) {
|
|
631
|
+
const isRequired = def.required !== false // default to required
|
|
632
|
+
if (isRequired && supplied[key] === undefined) {
|
|
633
|
+
missing.push(key)
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (missing.length === 0) return supplied
|
|
638
|
+
|
|
639
|
+
// In parallel mode, we can't run an interactive wizard
|
|
640
|
+
if ((options as any).parallel) {
|
|
641
|
+
console.error(`Missing required inputs for parallel mode (use --inputs-file or CLI flags): ${missing.join(', ')}`)
|
|
642
|
+
process.exit(1)
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Build wizard questions for missing inputs
|
|
646
|
+
const questions = missing.map((key) => {
|
|
647
|
+
const def = inputDefs[key]
|
|
648
|
+
const q: Record<string, any> = {
|
|
649
|
+
name: key,
|
|
650
|
+
message: def.description || key,
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Auto-infer type
|
|
654
|
+
if (def.choices?.length) {
|
|
655
|
+
q.type = 'list'
|
|
656
|
+
q.choices = def.choices
|
|
657
|
+
} else if (def.type) {
|
|
658
|
+
q.type = def.type
|
|
659
|
+
} else {
|
|
660
|
+
q.type = 'input'
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (def.default !== undefined) {
|
|
664
|
+
q.default = def.default
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return q
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
const answers = await ui.wizard(questions, supplied)
|
|
671
|
+
return { ...supplied, ...answers }
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function substituteInputs(content: string, inputs: Record<string, any>): string {
|
|
675
|
+
return content.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
676
|
+
return inputs[key] !== undefined ? String(inputs[key]) : match
|
|
677
|
+
})
|
|
593
678
|
}
|
|
594
679
|
|
|
595
|
-
async function executePromptFile(resolvedPath: string, container: any): Promise<string> {
|
|
680
|
+
async function executePromptFile(resolvedPath: string, container: any, inputs?: Record<string, any>): Promise<string> {
|
|
596
681
|
if (!container.docs.isLoaded) await container.docs.load()
|
|
597
682
|
const doc = await container.docs.parseMarkdownAtPath(resolvedPath)
|
|
598
683
|
const vm = container.feature('vm')
|
|
@@ -608,6 +693,7 @@ async function executePromptFile(resolvedPath: string, container: any): Promise<
|
|
|
608
693
|
|
|
609
694
|
const shared = vm.createContext({
|
|
610
695
|
...container.context,
|
|
696
|
+
INPUTS: inputs || {},
|
|
611
697
|
console: captureConsole,
|
|
612
698
|
setTimeout, clearTimeout, setInterval, clearInterval,
|
|
613
699
|
fetch, URL, URLSearchParams,
|
|
@@ -669,24 +755,40 @@ async function preparePrompt(
|
|
|
669
755
|
|
|
670
756
|
let content = fs.readFile(resolvedPath) as string
|
|
671
757
|
|
|
672
|
-
//
|
|
673
|
-
|
|
758
|
+
// Parse frontmatter for input definitions and agentOptions
|
|
759
|
+
let resolvedInputs: Record<string, any> = {}
|
|
760
|
+
let agentOptions: Record<string, any> = {}
|
|
761
|
+
let hasInputDefs = false
|
|
762
|
+
if (content.startsWith('---')) {
|
|
674
763
|
const fmEnd = content.indexOf('\n---', 3)
|
|
675
764
|
if (fmEnd !== -1) {
|
|
676
765
|
const yaml = container.feature('yaml')
|
|
677
766
|
const meta = yaml.parse(content.slice(4, fmEnd)) || {}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
767
|
+
const inputDefs = parseInputDefs(meta)
|
|
768
|
+
if (inputDefs) {
|
|
769
|
+
hasInputDefs = true
|
|
770
|
+
resolvedInputs = await resolveInputs(inputDefs, options, container)
|
|
771
|
+
}
|
|
772
|
+
if (meta.agentOptions && typeof meta.agentOptions === 'object') {
|
|
773
|
+
agentOptions = { ...meta.agentOptions }
|
|
681
774
|
}
|
|
682
775
|
}
|
|
683
776
|
}
|
|
684
777
|
|
|
778
|
+
if (options['inputs-file'] && !hasInputDefs) {
|
|
779
|
+
console.warn(`Warning: --inputs-file was provided but ${filePath} does not define any inputs in its frontmatter`)
|
|
780
|
+
}
|
|
781
|
+
|
|
685
782
|
let promptContent: string
|
|
686
783
|
if (options['preserve-frontmatter']) {
|
|
687
784
|
promptContent = content
|
|
688
785
|
} else {
|
|
689
|
-
promptContent = await executePromptFile(resolvedPath, container)
|
|
786
|
+
promptContent = await executePromptFile(resolvedPath, container, resolvedInputs)
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Substitute {{key}} placeholders with resolved input values
|
|
790
|
+
if (Object.keys(resolvedInputs).length) {
|
|
791
|
+
promptContent = substituteInputs(promptContent, resolvedInputs)
|
|
690
792
|
}
|
|
691
793
|
|
|
692
794
|
// Exclude sections by heading name
|
|
@@ -709,6 +811,7 @@ async function preparePrompt(
|
|
|
709
811
|
resolvedPath,
|
|
710
812
|
promptContent,
|
|
711
813
|
filename: paths.basename(resolvedPath),
|
|
814
|
+
agentOptions,
|
|
712
815
|
}
|
|
713
816
|
}
|
|
714
817
|
|
|
@@ -751,6 +854,28 @@ export default async function prompt(options: z.infer<typeof argsSchema>, contex
|
|
|
751
854
|
process.exit(1)
|
|
752
855
|
}
|
|
753
856
|
|
|
857
|
+
if (options['dry-run']) {
|
|
858
|
+
const ui = container.feature('ui')
|
|
859
|
+
console.log(ui.colors.bold('\n── Dry Run (Parallel) ──\n'))
|
|
860
|
+
console.log(ui.colors.bold('Target:'), target)
|
|
861
|
+
console.log(ui.colors.bold('Prompts:'), prepared.length)
|
|
862
|
+
for (const p of prepared) {
|
|
863
|
+
console.log(ui.colors.bold(`\n── ${p.filename} ──`))
|
|
864
|
+
console.log(ui.colors.dim(` Path: ${p.resolvedPath}`))
|
|
865
|
+
console.log(ui.colors.dim(` Length: ${p.promptContent.length} chars`))
|
|
866
|
+
if (Object.keys(p.agentOptions).length) {
|
|
867
|
+
console.log(ui.colors.dim(' Agent options:'))
|
|
868
|
+
for (const [key, val] of Object.entries(p.agentOptions)) {
|
|
869
|
+
const display = typeof val === 'object' ? JSON.stringify(val) : val
|
|
870
|
+
console.log(ui.colors.dim(` ${key}: ${display}`))
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
console.log('')
|
|
874
|
+
process.stdout.write(ui.markdown(p.promptContent))
|
|
875
|
+
}
|
|
876
|
+
return
|
|
877
|
+
}
|
|
878
|
+
|
|
754
879
|
if (prepared.length > 1) {
|
|
755
880
|
await runParallel(target, prepared, options, container)
|
|
756
881
|
return
|
|
@@ -767,28 +892,43 @@ export default async function prompt(options: z.infer<typeof argsSchema>, contex
|
|
|
767
892
|
}
|
|
768
893
|
|
|
769
894
|
const ui = container.feature('ui')
|
|
895
|
+
|
|
896
|
+
if (options['dry-run']) {
|
|
897
|
+
const runOptions: Record<string, any> = { ...p.agentOptions }
|
|
898
|
+
if (options.model) runOptions.model = options.model
|
|
899
|
+
if (options['in-folder']) runOptions.cwd = container.paths.resolve(options['in-folder'])
|
|
900
|
+
if (options['out-file']) runOptions.outFile = options['out-file']
|
|
901
|
+
if (options['include-output']) runOptions.includeOutput = true
|
|
902
|
+
if (options['exclude-sections']) runOptions.excludeSections = options['exclude-sections']
|
|
903
|
+
if (CLI_TARGETS.has(target)) {
|
|
904
|
+
runOptions.permissionMode = options['permission-mode']
|
|
905
|
+
if (options.chrome) runOptions.chrome = true
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
console.log(ui.colors.bold('\n── Dry Run ──\n'))
|
|
909
|
+
console.log(ui.colors.bold('Target:'), target)
|
|
910
|
+
console.log(ui.colors.bold('Prompt file:'), p.resolvedPath)
|
|
911
|
+
console.log(ui.colors.bold('Prompt length:'), `${p.promptContent.length} chars`)
|
|
912
|
+
if (Object.keys(runOptions).length) {
|
|
913
|
+
console.log(ui.colors.bold('Options:'))
|
|
914
|
+
for (const [key, val] of Object.entries(runOptions)) {
|
|
915
|
+
const display = typeof val === 'object' ? JSON.stringify(val) : val
|
|
916
|
+
console.log(` ${key}: ${display}`)
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
console.log(ui.colors.bold('\n── Prompt Content ──\n'))
|
|
920
|
+
process.stdout.write(ui.markdown(p.promptContent))
|
|
921
|
+
return
|
|
922
|
+
}
|
|
923
|
+
|
|
770
924
|
process.stdout.write(ui.markdown(p.promptContent))
|
|
771
925
|
|
|
772
926
|
let stats: RunStats
|
|
773
927
|
|
|
774
928
|
if (CLI_TARGETS.has(target)) {
|
|
775
|
-
stats = await runClaudeOrCodex(target as 'claude' | 'codex', p.promptContent, container, options)
|
|
929
|
+
stats = await runClaudeOrCodex(target as 'claude' | 'codex', p.promptContent, container, options, p.agentOptions)
|
|
776
930
|
} 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)
|
|
931
|
+
stats = await runAssistant(target, p.promptContent, options, container, p.agentOptions)
|
|
792
932
|
}
|
|
793
933
|
|
|
794
934
|
if (options['out-file'] && stats.collectedEvents.length) {
|