@soederpop/luca 0.0.19 → 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/describe.ts +557 -94
- package/src/commands/prompt.ts +204 -64
- package/src/introspection/generated.agi.ts +454 -454
- package/src/introspection/generated.node.ts +446 -446
- package/src/introspection/generated.web.ts +1 -1
- package/src/introspection/index.ts +7 -0
- package/src/node/container.ts +4 -0
- package/src/registry.ts +12 -1
- package/src/scaffolds/generated.ts +1 -1
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) {
|