@soederpop/luca 0.0.2
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 +71 -0
- package/README.md +78 -0
- package/bun.lock +2928 -0
- package/bunfig.toml +3 -0
- package/commands/audit-docs.ts +740 -0
- package/commands/build-scaffolds.ts +154 -0
- package/commands/generate-api-docs.ts +114 -0
- package/commands/update-introspection.ts +67 -0
- package/docs/CLI.md +335 -0
- package/docs/README.md +88 -0
- package/docs/TABLE-OF-CONTENTS.md +157 -0
- package/docs/apis/clients/elevenlabs.md +84 -0
- package/docs/apis/clients/graph.md +56 -0
- package/docs/apis/clients/openai.md +69 -0
- package/docs/apis/clients/rest.md +41 -0
- package/docs/apis/clients/websocket.md +107 -0
- package/docs/apis/features/agi/assistant.md +471 -0
- package/docs/apis/features/agi/assistants-manager.md +154 -0
- package/docs/apis/features/agi/claude-code.md +602 -0
- package/docs/apis/features/agi/conversation-history.md +352 -0
- package/docs/apis/features/agi/conversation.md +333 -0
- package/docs/apis/features/agi/docs-reader.md +121 -0
- package/docs/apis/features/agi/openai-codex.md +318 -0
- package/docs/apis/features/agi/openapi.md +138 -0
- package/docs/apis/features/agi/semantic-search.md +387 -0
- package/docs/apis/features/agi/skills-library.md +216 -0
- package/docs/apis/features/node/container-link.md +133 -0
- package/docs/apis/features/node/content-db.md +313 -0
- package/docs/apis/features/node/disk-cache.md +379 -0
- package/docs/apis/features/node/dns.md +651 -0
- package/docs/apis/features/node/docker.md +705 -0
- package/docs/apis/features/node/downloader.md +81 -0
- package/docs/apis/features/node/esbuild.md +59 -0
- package/docs/apis/features/node/file-manager.md +182 -0
- package/docs/apis/features/node/fs.md +581 -0
- package/docs/apis/features/node/git.md +330 -0
- package/docs/apis/features/node/google-auth.md +174 -0
- package/docs/apis/features/node/google-calendar.md +187 -0
- package/docs/apis/features/node/google-docs.md +151 -0
- package/docs/apis/features/node/google-drive.md +225 -0
- package/docs/apis/features/node/google-sheets.md +179 -0
- package/docs/apis/features/node/grep.md +290 -0
- package/docs/apis/features/node/helpers.md +135 -0
- package/docs/apis/features/node/ink.md +334 -0
- package/docs/apis/features/node/ipc-socket.md +260 -0
- package/docs/apis/features/node/json-tree.md +86 -0
- package/docs/apis/features/node/launcher-app-command-listener.md +145 -0
- package/docs/apis/features/node/networking.md +281 -0
- package/docs/apis/features/node/nlp.md +133 -0
- package/docs/apis/features/node/opener.md +97 -0
- package/docs/apis/features/node/os.md +118 -0
- package/docs/apis/features/node/package-finder.md +402 -0
- package/docs/apis/features/node/postgres.md +212 -0
- package/docs/apis/features/node/proc.md +430 -0
- package/docs/apis/features/node/process-manager.md +210 -0
- package/docs/apis/features/node/python.md +278 -0
- package/docs/apis/features/node/repl.md +88 -0
- package/docs/apis/features/node/runpod.md +673 -0
- package/docs/apis/features/node/secure-shell.md +169 -0
- package/docs/apis/features/node/semantic-search.md +401 -0
- package/docs/apis/features/node/sqlite.md +211 -0
- package/docs/apis/features/node/telegram.md +254 -0
- package/docs/apis/features/node/tts.md +118 -0
- package/docs/apis/features/node/ui.md +703 -0
- package/docs/apis/features/node/vault.md +64 -0
- package/docs/apis/features/node/vm.md +84 -0
- package/docs/apis/features/node/window-manager.md +337 -0
- package/docs/apis/features/node/yaml-tree.md +85 -0
- package/docs/apis/features/node/yaml.md +176 -0
- package/docs/apis/features/web/asset-loader.md +47 -0
- package/docs/apis/features/web/container-link.md +133 -0
- package/docs/apis/features/web/esbuild.md +59 -0
- package/docs/apis/features/web/helpers.md +135 -0
- package/docs/apis/features/web/network.md +30 -0
- package/docs/apis/features/web/speech.md +55 -0
- package/docs/apis/features/web/vault.md +64 -0
- package/docs/apis/features/web/vm.md +84 -0
- package/docs/apis/features/web/voice.md +67 -0
- package/docs/apis/servers/express.md +127 -0
- package/docs/apis/servers/mcp.md +213 -0
- package/docs/apis/servers/websocket.md +99 -0
- package/docs/documentation-audit.md +134 -0
- package/docs/examples/content-db.md +77 -0
- package/docs/examples/disk-cache.md +83 -0
- package/docs/examples/docker.md +101 -0
- package/docs/examples/downloader.md +70 -0
- package/docs/examples/esbuild.md +80 -0
- package/docs/examples/file-manager.md +82 -0
- package/docs/examples/fs.md +83 -0
- package/docs/examples/git.md +85 -0
- package/docs/examples/google-auth.md +88 -0
- package/docs/examples/google-calendar.md +94 -0
- package/docs/examples/google-docs.md +82 -0
- package/docs/examples/google-drive.md +96 -0
- package/docs/examples/google-sheets.md +95 -0
- package/docs/examples/grep.md +85 -0
- package/docs/examples/ink-blocks.md +75 -0
- package/docs/examples/ink-renderer.md +41 -0
- package/docs/examples/ink.md +103 -0
- package/docs/examples/ipc-socket.md +103 -0
- package/docs/examples/json-tree.md +91 -0
- package/docs/examples/launcher-app-command-listener.md +120 -0
- package/docs/examples/networking.md +58 -0
- package/docs/examples/nlp.md +91 -0
- package/docs/examples/opener.md +78 -0
- package/docs/examples/os.md +72 -0
- package/docs/examples/package-finder.md +89 -0
- package/docs/examples/port-exposer.md +89 -0
- package/docs/examples/postgres.md +91 -0
- package/docs/examples/proc.md +81 -0
- package/docs/examples/process-manager.md +79 -0
- package/docs/examples/python.md +91 -0
- package/docs/examples/repl.md +93 -0
- package/docs/examples/runpod.md +119 -0
- package/docs/examples/secure-shell.md +92 -0
- package/docs/examples/sqlite.md +86 -0
- package/docs/examples/telegram.md +77 -0
- package/docs/examples/tts.md +86 -0
- package/docs/examples/ui.md +80 -0
- package/docs/examples/vault.md +70 -0
- package/docs/examples/vm.md +86 -0
- package/docs/examples/window-manager.md +125 -0
- package/docs/examples/yaml-tree.md +93 -0
- package/docs/examples/yaml.md +104 -0
- package/docs/ideas/class-registration-refactor-possibilities.md +197 -0
- package/docs/ideas/container-use-api.md +9 -0
- package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
- package/docs/ideas/feature-stacks.md +22 -0
- package/docs/ideas/luca-cli-self-sufficiency-demo.md +23 -0
- package/docs/ideas/mcp-design.md +9 -0
- package/docs/ideas/web-container-debugging-feature.md +13 -0
- package/docs/introspection-audit.md +49 -0
- package/docs/introspection.md +154 -0
- package/docs/mcp/readme.md +162 -0
- package/docs/models.ts +38 -0
- package/docs/philosophy.md +85 -0
- package/docs/principles.md +7 -0
- package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
- package/docs/prompts/mcp-test-easy-command.md +27 -0
- package/docs/reports/assistant-bugs.md +38 -0
- package/docs/reports/attach-pattern-usage.md +18 -0
- package/docs/reports/code-audit-results.md +391 -0
- package/docs/reports/introspection-audit-tasks.md +378 -0
- package/docs/reports/luca-mcp-improvements.md +128 -0
- package/docs/scaffolds/client.md +140 -0
- package/docs/scaffolds/command.md +106 -0
- package/docs/scaffolds/endpoint.md +176 -0
- package/docs/scaffolds/feature.md +148 -0
- package/docs/scaffolds/server.md +187 -0
- package/docs/tasks/web-container-helper-discovery.md +71 -0
- package/docs/todos.md +1 -0
- package/docs/tutorials/01-getting-started.md +106 -0
- package/docs/tutorials/02-container.md +210 -0
- package/docs/tutorials/03-scripts.md +194 -0
- package/docs/tutorials/04-features-overview.md +196 -0
- package/docs/tutorials/05-state-and-events.md +171 -0
- package/docs/tutorials/06-servers.md +157 -0
- package/docs/tutorials/07-endpoints.md +198 -0
- package/docs/tutorials/08-commands.md +171 -0
- package/docs/tutorials/09-clients.md +162 -0
- package/docs/tutorials/10-creating-features.md +198 -0
- package/docs/tutorials/11-contentbase.md +191 -0
- package/docs/tutorials/12-assistants.md +215 -0
- package/docs/tutorials/13-introspection.md +147 -0
- package/docs/tutorials/14-type-system.md +174 -0
- package/docs/tutorials/15-project-patterns.md +222 -0
- package/docs/tutorials/16-google-features.md +534 -0
- package/docs/tutorials/17-tui-blocks.md +530 -0
- package/docs/tutorials/18-semantic-search.md +334 -0
- package/index.ts +1 -0
- package/luca.console.ts +9 -0
- package/main.py +6 -0
- package/package.json +154 -0
- package/pyproject.toml +7 -0
- package/scripts/animations/chrome-glitch.ts +55 -0
- package/scripts/animations/index.ts +16 -0
- package/scripts/animations/neon-pulse.ts +64 -0
- package/scripts/animations/types.ts +6 -0
- package/scripts/build-web.ts +28 -0
- package/scripts/examples/ask-luca-expert.ts +42 -0
- package/scripts/examples/assistant-questions.ts +12 -0
- package/scripts/examples/excalidraw-expert.ts +75 -0
- package/scripts/examples/expert-chat.ts +0 -0
- package/scripts/examples/file-manager.ts +14 -0
- package/scripts/examples/ideas.ts +12 -0
- package/scripts/examples/interactive-chat.ts +20 -0
- package/scripts/examples/openai-tool-calls.ts +113 -0
- package/scripts/examples/opening-a-web-browser.ts +5 -0
- package/scripts/examples/telegram-bot.ts +79 -0
- package/scripts/examples/telegram-ink-ui.ts +302 -0
- package/scripts/examples/using-assistant-with-mcp.ts +560 -0
- package/scripts/examples/using-claude-code.ts +10 -0
- package/scripts/examples/using-contentdb.ts +35 -0
- package/scripts/examples/using-conversations.ts +35 -0
- package/scripts/examples/using-disk-cache.ts +10 -0
- package/scripts/examples/using-docker-shell.ts +75 -0
- package/scripts/examples/using-elevenlabs.ts +25 -0
- package/scripts/examples/using-google-calendar.ts +57 -0
- package/scripts/examples/using-google-docs.ts +74 -0
- package/scripts/examples/using-google-drive.ts +74 -0
- package/scripts/examples/using-google-sheets.ts +89 -0
- package/scripts/examples/using-nlp.ts +55 -0
- package/scripts/examples/using-ollama.ts +10 -0
- package/scripts/examples/using-openai-codex.ts +23 -0
- package/scripts/examples/using-postgres.ts +55 -0
- package/scripts/examples/using-runpod.ts +32 -0
- package/scripts/examples/using-tts.ts +40 -0
- package/scripts/examples/vm-loading-esm-modules.ts +16 -0
- package/scripts/scaffold.ts +391 -0
- package/scripts/scratch.ts +15 -0
- package/scripts/test-command-listener.ts +123 -0
- package/scripts/test-window-manager-lifecycle.ts +86 -0
- package/scripts/test-window-manager.ts +43 -0
- package/scripts/update-introspection-data.ts +58 -0
- package/src/agi/README.md +14 -0
- package/src/agi/container.server.ts +114 -0
- package/src/agi/endpoints/ask.ts +60 -0
- package/src/agi/endpoints/conversations/[id].ts +45 -0
- package/src/agi/endpoints/conversations.ts +31 -0
- package/src/agi/endpoints/experts.ts +37 -0
- package/src/agi/features/assistant.ts +767 -0
- package/src/agi/features/assistants-manager.ts +260 -0
- package/src/agi/features/claude-code.ts +1111 -0
- package/src/agi/features/conversation-history.ts +497 -0
- package/src/agi/features/conversation.ts +799 -0
- package/src/agi/features/openai-codex.ts +631 -0
- package/src/agi/features/openapi.ts +438 -0
- package/src/agi/features/skills-library.ts +425 -0
- package/src/agi/index.ts +6 -0
- package/src/agi/lib/token-counter.ts +122 -0
- package/src/browser.ts +25 -0
- package/src/bus.ts +100 -0
- package/src/cli/cli.ts +70 -0
- package/src/client.ts +461 -0
- package/src/clients/civitai/index.ts +541 -0
- package/src/clients/client-template.ts +41 -0
- package/src/clients/comfyui/index.ts +597 -0
- package/src/clients/elevenlabs/index.ts +291 -0
- package/src/clients/openai/index.ts +451 -0
- package/src/clients/supabase/index.ts +366 -0
- package/src/command.ts +164 -0
- package/src/commands/chat.ts +182 -0
- package/src/commands/console.ts +192 -0
- package/src/commands/describe.ts +433 -0
- package/src/commands/eval.ts +116 -0
- package/src/commands/help.ts +214 -0
- package/src/commands/index.ts +14 -0
- package/src/commands/mcp.ts +64 -0
- package/src/commands/prompt.ts +807 -0
- package/src/commands/run.ts +257 -0
- package/src/commands/sandbox-mcp.ts +439 -0
- package/src/commands/scaffold.ts +79 -0
- package/src/commands/serve.ts +172 -0
- package/src/container.ts +781 -0
- package/src/endpoint.ts +340 -0
- package/src/feature.ts +75 -0
- package/src/hash-object.ts +97 -0
- package/src/helper.ts +543 -0
- package/src/introspection/generated.agi.ts +23388 -0
- package/src/introspection/generated.node.ts +18899 -0
- package/src/introspection/generated.web.ts +2021 -0
- package/src/introspection/index.ts +256 -0
- package/src/introspection/scan.ts +912 -0
- package/src/node/container.ts +354 -0
- package/src/node/feature.ts +13 -0
- package/src/node/features/container-link.ts +558 -0
- package/src/node/features/content-db.ts +475 -0
- package/src/node/features/disk-cache.ts +382 -0
- package/src/node/features/dns.ts +655 -0
- package/src/node/features/docker.ts +912 -0
- package/src/node/features/downloader.ts +92 -0
- package/src/node/features/esbuild.ts +68 -0
- package/src/node/features/file-manager.ts +357 -0
- package/src/node/features/fs.ts +534 -0
- package/src/node/features/git.ts +492 -0
- package/src/node/features/google-auth.ts +502 -0
- package/src/node/features/google-calendar.ts +300 -0
- package/src/node/features/google-docs.ts +404 -0
- package/src/node/features/google-drive.ts +339 -0
- package/src/node/features/google-sheets.ts +279 -0
- package/src/node/features/grep.ts +406 -0
- package/src/node/features/helpers.ts +374 -0
- package/src/node/features/ink.ts +490 -0
- package/src/node/features/ipc-socket.ts +459 -0
- package/src/node/features/json-tree.ts +188 -0
- package/src/node/features/launcher-app-command-listener.ts +388 -0
- package/src/node/features/networking.ts +925 -0
- package/src/node/features/nlp.ts +211 -0
- package/src/node/features/opener.ts +166 -0
- package/src/node/features/os.ts +157 -0
- package/src/node/features/package-finder.ts +539 -0
- package/src/node/features/port-exposer.ts +342 -0
- package/src/node/features/postgres.ts +273 -0
- package/src/node/features/proc.ts +502 -0
- package/src/node/features/process-manager.ts +542 -0
- package/src/node/features/python.ts +444 -0
- package/src/node/features/repl.ts +194 -0
- package/src/node/features/runpod.ts +802 -0
- package/src/node/features/secure-shell.ts +248 -0
- package/src/node/features/semantic-search.ts +924 -0
- package/src/node/features/sqlite.ts +289 -0
- package/src/node/features/telegram.ts +342 -0
- package/src/node/features/tts.ts +184 -0
- package/src/node/features/ui.ts +857 -0
- package/src/node/features/vault.ts +164 -0
- package/src/node/features/vm.ts +312 -0
- package/src/node/features/window-manager.ts +804 -0
- package/src/node/features/yaml-tree.ts +149 -0
- package/src/node/features/yaml.ts +132 -0
- package/src/node.ts +70 -0
- package/src/react/index.ts +175 -0
- package/src/registry.ts +199 -0
- package/src/scaffolds/generated.ts +1613 -0
- package/src/scaffolds/template.ts +37 -0
- package/src/schemas/base.ts +255 -0
- package/src/server.ts +135 -0
- package/src/servers/express.ts +209 -0
- package/src/servers/mcp.ts +805 -0
- package/src/servers/socket.ts +120 -0
- package/src/state.ts +101 -0
- package/src/web/clients/socket.ts +82 -0
- package/src/web/container.ts +74 -0
- package/src/web/extension.ts +30 -0
- package/src/web/feature.ts +12 -0
- package/src/web/features/asset-loader.ts +64 -0
- package/src/web/features/container-link.ts +385 -0
- package/src/web/features/esbuild.ts +79 -0
- package/src/web/features/helpers.ts +267 -0
- package/src/web/features/network.ts +61 -0
- package/src/web/features/speech.ts +87 -0
- package/src/web/features/vault.ts +189 -0
- package/src/web/features/vm.ts +78 -0
- package/src/web/features/voice-recognition.ts +129 -0
- package/src/web/shims/isomorphic-vm.ts +149 -0
- package/test/bus.test.ts +134 -0
- package/test/clients-servers.test.ts +216 -0
- package/test/container-link.test.ts +274 -0
- package/test/features.test.ts +160 -0
- package/test/integration.test.ts +787 -0
- package/test/node-container.test.ts +121 -0
- package/test/rate-limit.test.ts +272 -0
- package/test/semantic-search.test.ts +550 -0
- package/test/state.test.ts +121 -0
- package/test-integration/assistant.test.ts +138 -0
- package/test-integration/assistants-manager.test.ts +123 -0
- package/test-integration/claude-code.test.ts +98 -0
- package/test-integration/conversation-history.test.ts +205 -0
- package/test-integration/conversation.test.ts +137 -0
- package/test-integration/elevenlabs.test.ts +55 -0
- package/test-integration/google-services.test.ts +80 -0
- package/test-integration/helpers.ts +89 -0
- package/test-integration/openai-codex.test.ts +93 -0
- package/test-integration/runpod.test.ts +58 -0
- package/test-integration/server-endpoints.test.ts +97 -0
- package/test-integration/skills-library.test.ts +157 -0
- package/test-integration/telegram.test.ts +46 -0
- package/tsconfig.json +58 -0
- package/uv.lock +8 -0
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { Document } from 'contentbase'
|
|
3
|
+
import { commands } from '../command.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
|
+
prompt: ReturnType<typeof commands.registerHandler>
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const argsSchema = CommandOptionsSchema.extend({
|
|
14
|
+
model: z.string().optional().describe('Override the LLM model (assistant mode only)'),
|
|
15
|
+
folder: z.string().default('assistants').describe('Directory containing assistant definitions'),
|
|
16
|
+
'preserve-frontmatter': z.boolean().default(false).describe('Keep YAML frontmatter in the prompt instead of stripping it'),
|
|
17
|
+
'permission-mode': z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan']).default('acceptEdits').describe('Permission mode for CLI agents (default: acceptEdits)'),
|
|
18
|
+
'in-folder': z.string().optional().describe('Run the CLI agent in this directory (resolved via container.paths)'),
|
|
19
|
+
'out-file': z.string().optional().describe('Save session output as a markdown file'),
|
|
20
|
+
'include-output': z.boolean().default(false).describe('Include tool call outputs in the markdown (requires --out-file)'),
|
|
21
|
+
'dont-touch-file': z.boolean().default(false).describe('Do not update the prompt file frontmatter with run stats'),
|
|
22
|
+
'repeat-anyway': z.boolean().default(false).describe('Run even if repeatable is false and the prompt has already been run'),
|
|
23
|
+
'parallel': z.boolean().default(false).describe('Run multiple prompt files in parallel with side-by-side terminal UI'),
|
|
24
|
+
'exclude-sections': z.string().optional().describe('Comma-separated list of section headings to exclude from the prompt'),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const CLI_TARGETS = new Set(['claude', 'codex'])
|
|
28
|
+
|
|
29
|
+
function formatSessionMarkdown(events: any[], includeOutput: boolean): string {
|
|
30
|
+
const lines: string[] = []
|
|
31
|
+
|
|
32
|
+
for (const event of events) {
|
|
33
|
+
if (event.type === 'assistant' || event.type === 'message') {
|
|
34
|
+
const role = event.message?.role ?? event.role
|
|
35
|
+
if (role && role !== 'assistant') continue
|
|
36
|
+
|
|
37
|
+
const content = event.message?.content ?? event.content
|
|
38
|
+
if (!Array.isArray(content)) continue
|
|
39
|
+
|
|
40
|
+
for (const block of content) {
|
|
41
|
+
if (block.type === 'text' && block.text) {
|
|
42
|
+
lines.push(block.text)
|
|
43
|
+
lines.push('')
|
|
44
|
+
} else if (block.type === 'tool_use') {
|
|
45
|
+
lines.push(`**${block.name}**`)
|
|
46
|
+
lines.push('```json')
|
|
47
|
+
lines.push(JSON.stringify(block.input, null, 2))
|
|
48
|
+
lines.push('```')
|
|
49
|
+
lines.push('')
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} else if ((event.type === 'tool_result' || event.type === 'function_call_output') && includeOutput) {
|
|
53
|
+
const rawContent = event.type === 'function_call_output' ? event.output : event.content
|
|
54
|
+
const content = typeof rawContent === 'string' ? rawContent : JSON.stringify(rawContent, null, 2)
|
|
55
|
+
lines.push('```')
|
|
56
|
+
lines.push(content)
|
|
57
|
+
lines.push('```')
|
|
58
|
+
lines.push('')
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return lines.join('\n')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface RunStats {
|
|
66
|
+
collectedEvents: any[]
|
|
67
|
+
durationMs: number
|
|
68
|
+
outputTokens: number
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface PreparedPrompt {
|
|
72
|
+
resolvedPath: string
|
|
73
|
+
promptContent: string
|
|
74
|
+
filename: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function runClaudeOrCodex(target: 'claude' | 'codex', promptContent: string, container: any, options: z.infer<typeof argsSchema>): Promise<RunStats> {
|
|
78
|
+
const ui = container.feature('ui')
|
|
79
|
+
const featureName = target === 'claude' ? 'claudeCode' : 'openaiCodex'
|
|
80
|
+
const feature = container.feature(featureName)
|
|
81
|
+
|
|
82
|
+
const available = await feature.checkAvailability()
|
|
83
|
+
if (!available) {
|
|
84
|
+
console.error(`${target} CLI is not available. Make sure it is installed and in your PATH.`)
|
|
85
|
+
process.exit(1)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let outputTokens = 0
|
|
89
|
+
|
|
90
|
+
// Render complete messages — text gets markdown formatting, tool_use gets a summary line
|
|
91
|
+
feature.on('session:message', ({ message }: { message: any }) => {
|
|
92
|
+
const role = message?.message?.role ?? message?.role
|
|
93
|
+
if (role && role !== 'assistant') return
|
|
94
|
+
|
|
95
|
+
const content = message?.message?.content ?? message?.content
|
|
96
|
+
if (!Array.isArray(content)) return
|
|
97
|
+
|
|
98
|
+
const usage = message?.message?.usage ?? message?.usage
|
|
99
|
+
if (usage?.output_tokens) outputTokens += usage.output_tokens
|
|
100
|
+
|
|
101
|
+
for (const block of content) {
|
|
102
|
+
if (block.type === 'text' && block.text) {
|
|
103
|
+
process.stdout.write(ui.markdown(block.text))
|
|
104
|
+
} else if (block.type === 'tool_use') {
|
|
105
|
+
const argsStr = JSON.stringify(block.input).slice(0, 120)
|
|
106
|
+
process.stdout.write(ui.colors.dim(`\n ⟳ ${block.name}`) + ui.colors.dim(`(${argsStr})\n`))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// Collect structured events for --out-file
|
|
112
|
+
const collectedEvents: any[] = []
|
|
113
|
+
if (options['out-file']) {
|
|
114
|
+
feature.on('session:event', ({ event }: { event: any }) => {
|
|
115
|
+
if (event.type === 'assistant' || event.type === 'tool_result' || event.type === 'message' || event.type === 'function_call_output' || event.type === 'item.completed' || event.type === 'turn.completed') {
|
|
116
|
+
collectedEvents.push(event)
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const runOptions: Record<string, any> = { streaming: true }
|
|
122
|
+
|
|
123
|
+
if (options['in-folder']) {
|
|
124
|
+
runOptions.cwd = container.paths.resolve(options['in-folder'])
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (target === 'claude') {
|
|
128
|
+
runOptions.permissionMode = options['permission-mode']
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const startTime = Date.now()
|
|
132
|
+
const sessionId = await feature.start(promptContent, runOptions)
|
|
133
|
+
const session = await feature.waitForSession(sessionId)
|
|
134
|
+
|
|
135
|
+
if (session.status === 'error') {
|
|
136
|
+
console.error(session.error || 'Session failed')
|
|
137
|
+
process.exit(1)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
process.stdout.write('\n')
|
|
141
|
+
|
|
142
|
+
return { collectedEvents, durationMs: Date.now() - startTime, outputTokens }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function runAssistant(name: string, promptContent: string, options: z.infer<typeof argsSchema>, container: any): Promise<RunStats> {
|
|
146
|
+
const ui = container.feature('ui')
|
|
147
|
+
const manager = container.feature('assistantsManager', { folder: options.folder })
|
|
148
|
+
manager.discover()
|
|
149
|
+
|
|
150
|
+
const entry = manager.get(name)
|
|
151
|
+
if (!entry) {
|
|
152
|
+
const entries = manager.list()
|
|
153
|
+
const available = entries.length ? entries.map((e: any) => e.name).join(', ') : '(none)'
|
|
154
|
+
console.error(`Assistant "${name}" not found. Available: ${available}`)
|
|
155
|
+
process.exit(1)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const createOptions: Record<string, any> = {}
|
|
159
|
+
if (options.model) createOptions.model = options.model
|
|
160
|
+
|
|
161
|
+
const assistant = manager.create(name, createOptions)
|
|
162
|
+
let isFirstChunk = true
|
|
163
|
+
|
|
164
|
+
// Collect structured events for --out-file
|
|
165
|
+
const collectedEvents: any[] = []
|
|
166
|
+
|
|
167
|
+
assistant.on('chunk', (text: string) => {
|
|
168
|
+
if (isFirstChunk) {
|
|
169
|
+
process.stdout.write('\n')
|
|
170
|
+
isFirstChunk = false
|
|
171
|
+
}
|
|
172
|
+
process.stdout.write(text)
|
|
173
|
+
if (options['out-file']) {
|
|
174
|
+
collectedEvents.push({ type: 'assistant', message: { content: [{ type: 'text', text }] } })
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
assistant.on('toolCall', (toolName: string, args: any) => {
|
|
179
|
+
const argsStr = JSON.stringify(args).slice(0, 120)
|
|
180
|
+
process.stdout.write(ui.colors.dim(`\n ⟳ ${toolName}`) + ui.colors.dim(`(${argsStr})\n`))
|
|
181
|
+
if (options['out-file']) {
|
|
182
|
+
collectedEvents.push({ type: 'assistant', message: { content: [{ type: 'tool_use', name: toolName, input: args }] } })
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
assistant.on('toolResult', (toolName: string, result: any) => {
|
|
187
|
+
const preview = typeof result === 'string' ? result.slice(0, 100) : JSON.stringify(result).slice(0, 100)
|
|
188
|
+
process.stdout.write(ui.colors.green(` ✓ ${toolName}`) + ui.colors.dim(` → ${preview}${preview.length >= 100 ? '…' : ''}\n`))
|
|
189
|
+
if (options['out-file']) {
|
|
190
|
+
collectedEvents.push({ type: 'tool_result', content: typeof result === 'string' ? result : JSON.stringify(result, null, 2) })
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
assistant.on('toolError', (toolName: string, error: any) => {
|
|
195
|
+
const msg = error?.message || String(error)
|
|
196
|
+
process.stdout.write(ui.colors.red(` ✗ ${toolName}: ${msg}\n`))
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
const startTime = Date.now()
|
|
200
|
+
await assistant.ask(promptContent)
|
|
201
|
+
process.stdout.write('\n')
|
|
202
|
+
|
|
203
|
+
return { collectedEvents, durationMs: Date.now() - startTime, outputTokens: 0 }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function runParallel(
|
|
207
|
+
target: string,
|
|
208
|
+
prepared: PreparedPrompt[],
|
|
209
|
+
options: z.infer<typeof argsSchema>,
|
|
210
|
+
container: any,
|
|
211
|
+
): Promise<void> {
|
|
212
|
+
const { fs, paths } = container
|
|
213
|
+
const ink = container.feature('ink', { enable: true })
|
|
214
|
+
await ink.loadModules()
|
|
215
|
+
|
|
216
|
+
const React = ink.React
|
|
217
|
+
const h = React.createElement
|
|
218
|
+
const { Box, Text } = ink.components
|
|
219
|
+
const { useApp, useInput, useStdout } = ink.hooks
|
|
220
|
+
const { useState, useEffect } = React
|
|
221
|
+
|
|
222
|
+
const MAX_LINES = 500
|
|
223
|
+
|
|
224
|
+
// Mutable state that event handlers write to directly.
|
|
225
|
+
// The Ink component reads this on a timer to trigger re-renders.
|
|
226
|
+
const promptStates = prepared.map((p) => ({
|
|
227
|
+
filename: p.filename,
|
|
228
|
+
resolvedPath: p.resolvedPath,
|
|
229
|
+
status: 'running' as 'running' | 'done' | 'error',
|
|
230
|
+
lines: [] as string[],
|
|
231
|
+
outputTokens: 0,
|
|
232
|
+
startTime: Date.now(),
|
|
233
|
+
durationMs: 0,
|
|
234
|
+
collectedEvents: [] as any[],
|
|
235
|
+
error: undefined as string | undefined,
|
|
236
|
+
}))
|
|
237
|
+
|
|
238
|
+
const sessionMap = new Map<string, number>()
|
|
239
|
+
let allDone = false
|
|
240
|
+
let userAborted = false
|
|
241
|
+
|
|
242
|
+
function pushLines(idx: number, text: string) {
|
|
243
|
+
const newLines = text.split('\n')
|
|
244
|
+
promptStates[idx].lines.push(...newLines)
|
|
245
|
+
if (promptStates[idx].lines.length > MAX_LINES) {
|
|
246
|
+
promptStates[idx].lines = promptStates[idx].lines.slice(-MAX_LINES)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function pushToolLine(idx: number, text: string) {
|
|
251
|
+
promptStates[idx].lines.push(text)
|
|
252
|
+
if (promptStates[idx].lines.length > MAX_LINES) {
|
|
253
|
+
promptStates[idx].lines.splice(0, 1)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const runOptions: Record<string, any> = { streaming: true }
|
|
258
|
+
if (options['in-folder']) {
|
|
259
|
+
runOptions.cwd = container.paths.resolve(options['in-folder'])
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const isCli = CLI_TARGETS.has(target)
|
|
263
|
+
let sessionPromise: Promise<any>
|
|
264
|
+
|
|
265
|
+
if (isCli) {
|
|
266
|
+
const featureName = target === 'claude' ? 'claudeCode' : 'openaiCodex'
|
|
267
|
+
const feature = container.feature(featureName)
|
|
268
|
+
|
|
269
|
+
const available = await feature.checkAvailability()
|
|
270
|
+
if (!available) {
|
|
271
|
+
console.error(`${target} CLI is not available. Make sure it is installed and in your PATH.`)
|
|
272
|
+
process.exit(1)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (target === 'claude') {
|
|
276
|
+
runOptions.permissionMode = options['permission-mode']
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
feature.on('session:message', ({ sessionId, message }: { sessionId: string; message: any }) => {
|
|
280
|
+
const idx = sessionMap.get(sessionId)
|
|
281
|
+
if (idx === undefined) return
|
|
282
|
+
|
|
283
|
+
const role = message?.message?.role ?? message?.role
|
|
284
|
+
if (role && role !== 'assistant') return
|
|
285
|
+
|
|
286
|
+
const content = message?.message?.content ?? message?.content
|
|
287
|
+
if (!Array.isArray(content)) return
|
|
288
|
+
|
|
289
|
+
const usage = message?.message?.usage ?? message?.usage
|
|
290
|
+
if (usage?.output_tokens) promptStates[idx].outputTokens += usage.output_tokens
|
|
291
|
+
|
|
292
|
+
for (const block of content) {
|
|
293
|
+
if (block.type === 'text' && block.text) {
|
|
294
|
+
pushLines(idx, block.text)
|
|
295
|
+
} else if (block.type === 'tool_use') {
|
|
296
|
+
const argsStr = JSON.stringify(block.input).slice(0, 80)
|
|
297
|
+
pushToolLine(idx, ` > ${block.name}(${argsStr})`)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
if (options['out-file']) {
|
|
303
|
+
feature.on('session:event', ({ sessionId, event }: { sessionId: string; event: any }) => {
|
|
304
|
+
const idx = sessionMap.get(sessionId)
|
|
305
|
+
if (idx === undefined) return
|
|
306
|
+
if (event.type === 'assistant' || event.type === 'tool_result' || event.type === 'message' || event.type === 'function_call_output') {
|
|
307
|
+
promptStates[idx].collectedEvents.push(event)
|
|
308
|
+
}
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Start all sessions
|
|
313
|
+
for (let i = 0; i < prepared.length; i++) {
|
|
314
|
+
const id = await feature.start(prepared[i].promptContent, runOptions)
|
|
315
|
+
sessionMap.set(id, i)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const ids = [...sessionMap.keys()]
|
|
319
|
+
sessionPromise = Promise.allSettled(ids.map((id) => feature.waitForSession(id))).then((results) => {
|
|
320
|
+
results.forEach((r, ri) => {
|
|
321
|
+
const id = ids[ri]
|
|
322
|
+
const idx = sessionMap.get(id)!
|
|
323
|
+
promptStates[idx].durationMs = Date.now() - promptStates[idx].startTime
|
|
324
|
+
if (r.status === 'fulfilled' && r.value?.status === 'error') {
|
|
325
|
+
promptStates[idx].status = 'error'
|
|
326
|
+
promptStates[idx].error = r.value?.error || 'Session failed'
|
|
327
|
+
} else if (r.status === 'rejected') {
|
|
328
|
+
promptStates[idx].status = 'error'
|
|
329
|
+
promptStates[idx].error = String(r.reason)
|
|
330
|
+
} else {
|
|
331
|
+
promptStates[idx].status = 'done'
|
|
332
|
+
}
|
|
333
|
+
})
|
|
334
|
+
allDone = true
|
|
335
|
+
})
|
|
336
|
+
} else {
|
|
337
|
+
// Assistant targets
|
|
338
|
+
const manager = container.feature('assistantsManager', { folder: options.folder })
|
|
339
|
+
manager.discover()
|
|
340
|
+
|
|
341
|
+
const entry = manager.get(target)
|
|
342
|
+
if (!entry) {
|
|
343
|
+
const entries = manager.list()
|
|
344
|
+
const available = entries.length ? entries.map((e: any) => e.name).join(', ') : '(none)'
|
|
345
|
+
console.error(`Assistant "${target}" not found. Available: ${available}`)
|
|
346
|
+
process.exit(1)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const createOptions: Record<string, any> = {}
|
|
350
|
+
if (options.model) createOptions.model = options.model
|
|
351
|
+
|
|
352
|
+
const lineBuffers: string[] = prepared.map(() => '')
|
|
353
|
+
|
|
354
|
+
const assistants = prepared.map((p, i) => {
|
|
355
|
+
const assistant = manager.create(target, createOptions)
|
|
356
|
+
|
|
357
|
+
assistant.on('chunk', (text: string) => {
|
|
358
|
+
lineBuffers[i] += text
|
|
359
|
+
const parts = lineBuffers[i].split('\n')
|
|
360
|
+
lineBuffers[i] = parts.pop() || ''
|
|
361
|
+
if (parts.length) {
|
|
362
|
+
promptStates[i].lines.push(...parts)
|
|
363
|
+
if (promptStates[i].lines.length > MAX_LINES) {
|
|
364
|
+
promptStates[i].lines = promptStates[i].lines.slice(-MAX_LINES)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (options['out-file']) {
|
|
368
|
+
promptStates[i].collectedEvents.push({ type: 'assistant', message: { content: [{ type: 'text', text }] } })
|
|
369
|
+
}
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
assistant.on('toolCall', (toolName: string, args: any) => {
|
|
373
|
+
const argsStr = JSON.stringify(args).slice(0, 80)
|
|
374
|
+
pushToolLine(i, ` > ${toolName}(${argsStr})`)
|
|
375
|
+
if (options['out-file']) {
|
|
376
|
+
promptStates[i].collectedEvents.push({
|
|
377
|
+
type: 'assistant',
|
|
378
|
+
message: { content: [{ type: 'tool_use', name: toolName, input: args }] },
|
|
379
|
+
})
|
|
380
|
+
}
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
assistant.on('toolResult', (toolName: string, result: any) => {
|
|
384
|
+
const preview = typeof result === 'string' ? result.slice(0, 60) : JSON.stringify(result).slice(0, 60)
|
|
385
|
+
pushToolLine(i, ` ✓ ${toolName} → ${preview}`)
|
|
386
|
+
if (options['out-file']) {
|
|
387
|
+
promptStates[i].collectedEvents.push({
|
|
388
|
+
type: 'tool_result',
|
|
389
|
+
content: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
|
|
390
|
+
})
|
|
391
|
+
}
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
assistant.on('toolError', (toolName: string, error: any) => {
|
|
395
|
+
const msg = error?.message || String(error)
|
|
396
|
+
pushToolLine(i, ` ✗ ${toolName}: ${msg}`)
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
return assistant
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
sessionPromise = Promise.allSettled(assistants.map((a, i) => a.ask(prepared[i].promptContent))).then(
|
|
403
|
+
(results) => {
|
|
404
|
+
results.forEach((r, i) => {
|
|
405
|
+
promptStates[i].durationMs = Date.now() - promptStates[i].startTime
|
|
406
|
+
// Flush remaining line buffer
|
|
407
|
+
if (lineBuffers[i]) {
|
|
408
|
+
promptStates[i].lines.push(lineBuffers[i])
|
|
409
|
+
lineBuffers[i] = ''
|
|
410
|
+
}
|
|
411
|
+
if (r.status === 'rejected') {
|
|
412
|
+
promptStates[i].status = 'error'
|
|
413
|
+
promptStates[i].error = String(r.reason)
|
|
414
|
+
} else {
|
|
415
|
+
promptStates[i].status = 'done'
|
|
416
|
+
}
|
|
417
|
+
})
|
|
418
|
+
allDone = true
|
|
419
|
+
},
|
|
420
|
+
)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// --- Ink React Component ---
|
|
424
|
+
function App() {
|
|
425
|
+
const { exit } = useApp()
|
|
426
|
+
const { stdout } = useStdout()
|
|
427
|
+
const [tick, setTick] = useState(0)
|
|
428
|
+
|
|
429
|
+
const cols = stdout?.columns || 120
|
|
430
|
+
const rows = stdout?.rows || 30
|
|
431
|
+
const numPrompts = prepared.length
|
|
432
|
+
const colWidth = Math.max(30, Math.floor(cols / numPrompts))
|
|
433
|
+
const visibleLines = Math.max(5, rows - 7)
|
|
434
|
+
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
const timer = setInterval(() => setTick((t: number) => t + 1), 200)
|
|
437
|
+
return () => clearInterval(timer)
|
|
438
|
+
}, [])
|
|
439
|
+
|
|
440
|
+
useEffect(() => {
|
|
441
|
+
if (allDone) {
|
|
442
|
+
setTimeout(() => exit(), 400)
|
|
443
|
+
}
|
|
444
|
+
}, [tick])
|
|
445
|
+
|
|
446
|
+
useInput((input: string, key: any) => {
|
|
447
|
+
if (input === 'q' || (key.ctrl && input === 'c')) {
|
|
448
|
+
userAborted = true
|
|
449
|
+
if (isCli) {
|
|
450
|
+
const featureName = target === 'claude' ? 'claudeCode' : 'openaiCodex'
|
|
451
|
+
const feature = container.feature(featureName)
|
|
452
|
+
for (const [sid] of sessionMap) {
|
|
453
|
+
try {
|
|
454
|
+
feature.abort(sid)
|
|
455
|
+
} catch {}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
exit()
|
|
459
|
+
}
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
const formatElapsed = (ms: number) => {
|
|
463
|
+
const s = Math.floor(ms / 1000)
|
|
464
|
+
const m = Math.floor(s / 60)
|
|
465
|
+
const sec = s % 60
|
|
466
|
+
return `${m}:${String(sec).padStart(2, '0')}`
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const runningCount = promptStates.filter((p) => p.status === 'running').length
|
|
470
|
+
|
|
471
|
+
return h(
|
|
472
|
+
Box,
|
|
473
|
+
{ flexDirection: 'column', width: cols },
|
|
474
|
+
// Header
|
|
475
|
+
h(
|
|
476
|
+
Box,
|
|
477
|
+
{ justifyContent: 'space-between', paddingX: 1, marginBottom: 1 },
|
|
478
|
+
h(Text, { bold: true, color: '#61dafb' }, 'LUCA PROMPT // PARALLEL'),
|
|
479
|
+
h(Text, { dimColor: true }, `${runningCount} running / ${numPrompts} total`),
|
|
480
|
+
),
|
|
481
|
+
// Columns
|
|
482
|
+
h(
|
|
483
|
+
Box,
|
|
484
|
+
{ flexDirection: 'row' },
|
|
485
|
+
...promptStates.map((ps, i) => {
|
|
486
|
+
const elapsed = ps.status === 'running' ? Date.now() - ps.startTime : ps.durationMs
|
|
487
|
+
const borderColor = ps.status === 'running' ? 'cyan' : ps.status === 'done' ? 'green' : 'red'
|
|
488
|
+
const statusLabel =
|
|
489
|
+
ps.status === 'running'
|
|
490
|
+
? `RUNNING ${formatElapsed(elapsed)}`
|
|
491
|
+
: ps.status === 'done'
|
|
492
|
+
? `DONE ${formatElapsed(ps.durationMs)}`
|
|
493
|
+
: `ERROR`
|
|
494
|
+
const tail = ps.lines.slice(-visibleLines)
|
|
495
|
+
|
|
496
|
+
return h(
|
|
497
|
+
Box,
|
|
498
|
+
{
|
|
499
|
+
key: String(i),
|
|
500
|
+
flexDirection: 'column',
|
|
501
|
+
width: colWidth,
|
|
502
|
+
borderStyle: 'round',
|
|
503
|
+
borderColor,
|
|
504
|
+
paddingX: 1,
|
|
505
|
+
height: visibleLines + 4,
|
|
506
|
+
},
|
|
507
|
+
h(Text, { bold: true }, ps.filename),
|
|
508
|
+
h(Text, { color: borderColor, dimColor: ps.status === 'done' }, statusLabel),
|
|
509
|
+
h(Text, { dimColor: true }, '\u2500'.repeat(Math.max(1, colWidth - 4))),
|
|
510
|
+
h(Text, { wrap: 'truncate' }, tail.join('\n')),
|
|
511
|
+
)
|
|
512
|
+
}),
|
|
513
|
+
),
|
|
514
|
+
// Footer
|
|
515
|
+
h(Box, { paddingX: 1 }, h(Text, { dimColor: true }, 'q: quit all')),
|
|
516
|
+
)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
await ink.render(h(App))
|
|
520
|
+
await ink.waitUntilExit()
|
|
521
|
+
|
|
522
|
+
if (userAborted) return
|
|
523
|
+
|
|
524
|
+
// Wait for sessions to fully settle
|
|
525
|
+
await sessionPromise
|
|
526
|
+
|
|
527
|
+
// Post-completion: update frontmatter
|
|
528
|
+
if (!options['dont-touch-file']) {
|
|
529
|
+
for (let i = 0; i < promptStates.length; i++) {
|
|
530
|
+
const ps = promptStates[i]
|
|
531
|
+
if (ps.status === 'error') continue
|
|
532
|
+
const rawContent = fs.readFile(prepared[i].resolvedPath) as string
|
|
533
|
+
const updates: Record<string, any> = {
|
|
534
|
+
lastRanAt: Date.now(),
|
|
535
|
+
durationMs: ps.durationMs,
|
|
536
|
+
}
|
|
537
|
+
if (ps.outputTokens > 0) {
|
|
538
|
+
updates.outputTokens = ps.outputTokens
|
|
539
|
+
}
|
|
540
|
+
const updated = updateFrontmatter(rawContent, updates, container)
|
|
541
|
+
await Bun.write(prepared[i].resolvedPath, updated)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Post-completion: out-files
|
|
546
|
+
if (options['out-file']) {
|
|
547
|
+
const base = options['out-file']
|
|
548
|
+
const dotIdx = base.lastIndexOf('.')
|
|
549
|
+
const ext = dotIdx > 0 ? base.slice(dotIdx) : '.md'
|
|
550
|
+
const stem = dotIdx > 0 ? base.slice(0, dotIdx) : base
|
|
551
|
+
|
|
552
|
+
for (let i = 0; i < promptStates.length; i++) {
|
|
553
|
+
const ps = promptStates[i]
|
|
554
|
+
if (!ps.collectedEvents.length) continue
|
|
555
|
+
const promptBasename = paths.basename(prepared[i].resolvedPath)
|
|
556
|
+
const promptStem = promptBasename.lastIndexOf('.') > 0 ? promptBasename.slice(0, promptBasename.lastIndexOf('.')) : promptBasename
|
|
557
|
+
const outPath = paths.resolve(`${stem}-${promptStem}${ext}`)
|
|
558
|
+
const markdown = formatSessionMarkdown(ps.collectedEvents, options['include-output'])
|
|
559
|
+
await Bun.write(outPath, markdown)
|
|
560
|
+
console.log(`Session saved to ${outPath}`)
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Print summary
|
|
565
|
+
const errors = promptStates.filter((p) => p.status === 'error')
|
|
566
|
+
if (errors.length) {
|
|
567
|
+
console.error(`\n${errors.length} prompt(s) failed:`)
|
|
568
|
+
for (const ps of errors) {
|
|
569
|
+
console.error(` ${ps.filename}: ${ps.error}`)
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function updateFrontmatter(fileContent: string, updates: Record<string, any>, container: any): string {
|
|
575
|
+
const yaml = container.feature('yaml')
|
|
576
|
+
|
|
577
|
+
if (fileContent.startsWith('---')) {
|
|
578
|
+
const endIndex = fileContent.indexOf('\n---', 3)
|
|
579
|
+
if (endIndex !== -1) {
|
|
580
|
+
const existingYaml = fileContent.slice(4, endIndex)
|
|
581
|
+
const meta = yaml.parse(existingYaml) || {}
|
|
582
|
+
Object.assign(meta, updates)
|
|
583
|
+
const newYaml = yaml.stringify(meta).trimEnd()
|
|
584
|
+
return `---\n${newYaml}\n---${fileContent.slice(endIndex + 4)}`
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// No existing frontmatter — prepend one
|
|
589
|
+
const newYaml = yaml.stringify(updates).trimEnd()
|
|
590
|
+
return `---\n${newYaml}\n---\n\n${fileContent}`
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async function executePromptFile(resolvedPath: string, container: any): Promise<string> {
|
|
594
|
+
if (!container.docs.isLoaded) await container.docs.load()
|
|
595
|
+
const doc = await container.docs.parseMarkdownAtPath(resolvedPath)
|
|
596
|
+
const vm = container.feature('vm')
|
|
597
|
+
const parts: string[] = []
|
|
598
|
+
|
|
599
|
+
const capturedLines: string[] = []
|
|
600
|
+
const captureConsole = {
|
|
601
|
+
log: (...args: any[]) => capturedLines.push(args.map(String).join(' ')),
|
|
602
|
+
error: (...args: any[]) => capturedLines.push(args.map(String).join(' ')),
|
|
603
|
+
warn: (...args: any[]) => capturedLines.push(args.map(String).join(' ')),
|
|
604
|
+
info: (...args: any[]) => capturedLines.push(args.map(String).join(' ')),
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const shared = vm.createContext({
|
|
608
|
+
...container.context,
|
|
609
|
+
console: captureConsole,
|
|
610
|
+
setTimeout, clearTimeout, setInterval, clearInterval,
|
|
611
|
+
fetch, URL, URLSearchParams,
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
for (const node of doc.ast.children) {
|
|
615
|
+
if (node.type === 'code') {
|
|
616
|
+
const { value, lang, meta } = node
|
|
617
|
+
if (!lang || !['ts', 'js', 'tsx', 'jsx'].includes(lang)) {
|
|
618
|
+
parts.push(doc.stringify({ type: 'root', children: [node] }))
|
|
619
|
+
continue
|
|
620
|
+
}
|
|
621
|
+
if (meta && typeof meta === 'string' && meta.toLowerCase().includes('skip')) continue
|
|
622
|
+
|
|
623
|
+
capturedLines.length = 0
|
|
624
|
+
let code = value
|
|
625
|
+
if (lang === 'tsx' || lang === 'jsx') {
|
|
626
|
+
const esbuild = container.feature('esbuild')
|
|
627
|
+
const { code: transformed } = esbuild.transformSync(value, { loader: lang as 'tsx' | 'jsx', format: 'cjs' })
|
|
628
|
+
code = transformed
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const hasTopLevelAwait = /\bawait\b/.test(code)
|
|
632
|
+
if (hasTopLevelAwait) code = `(async function() { ${code} })()`
|
|
633
|
+
|
|
634
|
+
await vm.run(code, shared)
|
|
635
|
+
Object.assign(shared, container.context)
|
|
636
|
+
|
|
637
|
+
if (capturedLines.length) {
|
|
638
|
+
parts.push(capturedLines.join('\n'))
|
|
639
|
+
}
|
|
640
|
+
} else {
|
|
641
|
+
parts.push(doc.stringify({ type: 'root', children: [node] }))
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return parts.join('\n\n')
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
async function preparePrompt(
|
|
649
|
+
filePath: string,
|
|
650
|
+
options: z.infer<typeof argsSchema>,
|
|
651
|
+
container: any,
|
|
652
|
+
): Promise<PreparedPrompt | null> {
|
|
653
|
+
const { fs, paths } = container
|
|
654
|
+
|
|
655
|
+
let resolvedPath = paths.resolve(filePath)
|
|
656
|
+
if (!fs.exists(resolvedPath)) {
|
|
657
|
+
// Try common fallbacks: add .md extension, docs/ prefix, or both
|
|
658
|
+
const candidates = [
|
|
659
|
+
`${resolvedPath}.md`,
|
|
660
|
+
paths.resolve('docs', filePath),
|
|
661
|
+
paths.resolve('docs', `${filePath}.md`),
|
|
662
|
+
]
|
|
663
|
+
const found = candidates.find((c) => fs.exists(c))
|
|
664
|
+
if (!found) {
|
|
665
|
+
console.error(`Prompt file not found: ${resolvedPath}`)
|
|
666
|
+
return null
|
|
667
|
+
}
|
|
668
|
+
resolvedPath = found
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
let content = fs.readFile(resolvedPath) as string
|
|
672
|
+
|
|
673
|
+
// Check repeatable gate
|
|
674
|
+
if (!options['repeat-anyway'] && content.startsWith('---')) {
|
|
675
|
+
const fmEnd = content.indexOf('\n---', 3)
|
|
676
|
+
if (fmEnd !== -1) {
|
|
677
|
+
const yaml = container.feature('yaml')
|
|
678
|
+
const meta = yaml.parse(content.slice(4, fmEnd)) || {}
|
|
679
|
+
if (meta.repeatable === false && meta.lastRanAt) {
|
|
680
|
+
console.error(`${filePath}: already run (lastRanAt: ${new Date(meta.lastRanAt).toLocaleString()}) and repeatable is false. Skipping.`)
|
|
681
|
+
return null
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
let promptContent: string
|
|
687
|
+
if (options['preserve-frontmatter']) {
|
|
688
|
+
promptContent = content
|
|
689
|
+
} else {
|
|
690
|
+
promptContent = await executePromptFile(resolvedPath, container)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Exclude sections by heading name
|
|
694
|
+
if (options['exclude-sections']) {
|
|
695
|
+
const headings = options['exclude-sections'].split(',').map((s) => s.trim()).filter(Boolean)
|
|
696
|
+
let doc = new Document({ id: filePath, content: promptContent, collection: null as any })
|
|
697
|
+
|
|
698
|
+
for (const heading of headings) {
|
|
699
|
+
try {
|
|
700
|
+
doc = doc.removeSection(heading)
|
|
701
|
+
} catch {
|
|
702
|
+
// Section not found — skip silently
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
promptContent = doc.content
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return {
|
|
710
|
+
resolvedPath,
|
|
711
|
+
promptContent,
|
|
712
|
+
filename: paths.basename(resolvedPath),
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
export default async function prompt(options: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
717
|
+
const container = context.container as any
|
|
718
|
+
const { fs, paths } = container
|
|
719
|
+
|
|
720
|
+
let target = container.argv._[1] as string | undefined
|
|
721
|
+
const allPaths = (container.argv._.slice(2) as string[]).filter(Boolean)
|
|
722
|
+
|
|
723
|
+
// If only one arg given and it looks like a file path, default target to claude
|
|
724
|
+
if (target && allPaths.length === 0) {
|
|
725
|
+
const candidate = paths.resolve(target)
|
|
726
|
+
if (fs.exists(candidate)) {
|
|
727
|
+
allPaths.push(target)
|
|
728
|
+
target = 'claude'
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (!target || allPaths.length === 0) {
|
|
733
|
+
console.error('Usage: luca prompt [claude|codex|assistant-name] <path/to/prompt.md> [more paths...]')
|
|
734
|
+
process.exit(1)
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// --- Parallel mode ---
|
|
738
|
+
if (options.parallel && allPaths.length > 1) {
|
|
739
|
+
if (allPaths.length > 4) {
|
|
740
|
+
console.error('--parallel supports a maximum of 4 concurrent prompts')
|
|
741
|
+
process.exit(1)
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const prepared: PreparedPrompt[] = []
|
|
745
|
+
for (const pp of allPaths) {
|
|
746
|
+
const p = await preparePrompt(pp, options, container)
|
|
747
|
+
if (p) prepared.push(p)
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (prepared.length === 0) {
|
|
751
|
+
console.error('No prompt files to run (all skipped).')
|
|
752
|
+
process.exit(1)
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (prepared.length > 1) {
|
|
756
|
+
await runParallel(target, prepared, options, container)
|
|
757
|
+
return
|
|
758
|
+
}
|
|
759
|
+
// Only 1 left after filtering — fall through to single mode
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// --- Single prompt mode ---
|
|
763
|
+
const promptPath = allPaths[0]
|
|
764
|
+
const p = await preparePrompt(promptPath, options, container)
|
|
765
|
+
|
|
766
|
+
if (!p) {
|
|
767
|
+
process.exit(1)
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const ui = container.feature('ui')
|
|
771
|
+
process.stdout.write(ui.markdown(p.promptContent))
|
|
772
|
+
|
|
773
|
+
let stats: RunStats
|
|
774
|
+
|
|
775
|
+
if (CLI_TARGETS.has(target)) {
|
|
776
|
+
stats = await runClaudeOrCodex(target as 'claude' | 'codex', p.promptContent, container, options)
|
|
777
|
+
} else {
|
|
778
|
+
stats = await runAssistant(target, p.promptContent, options, container)
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Update prompt file frontmatter with run stats
|
|
782
|
+
if (!options['dont-touch-file']) {
|
|
783
|
+
const rawContent = fs.readFile(p.resolvedPath) as string
|
|
784
|
+
const updates: Record<string, any> = {
|
|
785
|
+
lastRanAt: Date.now(),
|
|
786
|
+
durationMs: stats.durationMs,
|
|
787
|
+
}
|
|
788
|
+
if (stats.outputTokens > 0) {
|
|
789
|
+
updates.outputTokens = stats.outputTokens
|
|
790
|
+
}
|
|
791
|
+
const updated = updateFrontmatter(rawContent, updates, container)
|
|
792
|
+
await Bun.write(p.resolvedPath, updated)
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (options['out-file'] && stats.collectedEvents.length) {
|
|
796
|
+
const markdown = formatSessionMarkdown(stats.collectedEvents, options['include-output'])
|
|
797
|
+
const outPath = paths.resolve(options['out-file'])
|
|
798
|
+
await Bun.write(outPath, markdown)
|
|
799
|
+
console.log(`Session saved to ${outPath}`)
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
commands.registerHandler('prompt', {
|
|
804
|
+
description: 'Send a prompt file to an assistant, Claude Code, or OpenAI Codex',
|
|
805
|
+
argsSchema,
|
|
806
|
+
handler: prompt,
|
|
807
|
+
})
|