@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.
Files changed (358) hide show
  1. package/CLAUDE.md +71 -0
  2. package/README.md +78 -0
  3. package/bun.lock +2928 -0
  4. package/bunfig.toml +3 -0
  5. package/commands/audit-docs.ts +740 -0
  6. package/commands/build-scaffolds.ts +154 -0
  7. package/commands/generate-api-docs.ts +114 -0
  8. package/commands/update-introspection.ts +67 -0
  9. package/docs/CLI.md +335 -0
  10. package/docs/README.md +88 -0
  11. package/docs/TABLE-OF-CONTENTS.md +157 -0
  12. package/docs/apis/clients/elevenlabs.md +84 -0
  13. package/docs/apis/clients/graph.md +56 -0
  14. package/docs/apis/clients/openai.md +69 -0
  15. package/docs/apis/clients/rest.md +41 -0
  16. package/docs/apis/clients/websocket.md +107 -0
  17. package/docs/apis/features/agi/assistant.md +471 -0
  18. package/docs/apis/features/agi/assistants-manager.md +154 -0
  19. package/docs/apis/features/agi/claude-code.md +602 -0
  20. package/docs/apis/features/agi/conversation-history.md +352 -0
  21. package/docs/apis/features/agi/conversation.md +333 -0
  22. package/docs/apis/features/agi/docs-reader.md +121 -0
  23. package/docs/apis/features/agi/openai-codex.md +318 -0
  24. package/docs/apis/features/agi/openapi.md +138 -0
  25. package/docs/apis/features/agi/semantic-search.md +387 -0
  26. package/docs/apis/features/agi/skills-library.md +216 -0
  27. package/docs/apis/features/node/container-link.md +133 -0
  28. package/docs/apis/features/node/content-db.md +313 -0
  29. package/docs/apis/features/node/disk-cache.md +379 -0
  30. package/docs/apis/features/node/dns.md +651 -0
  31. package/docs/apis/features/node/docker.md +705 -0
  32. package/docs/apis/features/node/downloader.md +81 -0
  33. package/docs/apis/features/node/esbuild.md +59 -0
  34. package/docs/apis/features/node/file-manager.md +182 -0
  35. package/docs/apis/features/node/fs.md +581 -0
  36. package/docs/apis/features/node/git.md +330 -0
  37. package/docs/apis/features/node/google-auth.md +174 -0
  38. package/docs/apis/features/node/google-calendar.md +187 -0
  39. package/docs/apis/features/node/google-docs.md +151 -0
  40. package/docs/apis/features/node/google-drive.md +225 -0
  41. package/docs/apis/features/node/google-sheets.md +179 -0
  42. package/docs/apis/features/node/grep.md +290 -0
  43. package/docs/apis/features/node/helpers.md +135 -0
  44. package/docs/apis/features/node/ink.md +334 -0
  45. package/docs/apis/features/node/ipc-socket.md +260 -0
  46. package/docs/apis/features/node/json-tree.md +86 -0
  47. package/docs/apis/features/node/launcher-app-command-listener.md +145 -0
  48. package/docs/apis/features/node/networking.md +281 -0
  49. package/docs/apis/features/node/nlp.md +133 -0
  50. package/docs/apis/features/node/opener.md +97 -0
  51. package/docs/apis/features/node/os.md +118 -0
  52. package/docs/apis/features/node/package-finder.md +402 -0
  53. package/docs/apis/features/node/postgres.md +212 -0
  54. package/docs/apis/features/node/proc.md +430 -0
  55. package/docs/apis/features/node/process-manager.md +210 -0
  56. package/docs/apis/features/node/python.md +278 -0
  57. package/docs/apis/features/node/repl.md +88 -0
  58. package/docs/apis/features/node/runpod.md +673 -0
  59. package/docs/apis/features/node/secure-shell.md +169 -0
  60. package/docs/apis/features/node/semantic-search.md +401 -0
  61. package/docs/apis/features/node/sqlite.md +211 -0
  62. package/docs/apis/features/node/telegram.md +254 -0
  63. package/docs/apis/features/node/tts.md +118 -0
  64. package/docs/apis/features/node/ui.md +703 -0
  65. package/docs/apis/features/node/vault.md +64 -0
  66. package/docs/apis/features/node/vm.md +84 -0
  67. package/docs/apis/features/node/window-manager.md +337 -0
  68. package/docs/apis/features/node/yaml-tree.md +85 -0
  69. package/docs/apis/features/node/yaml.md +176 -0
  70. package/docs/apis/features/web/asset-loader.md +47 -0
  71. package/docs/apis/features/web/container-link.md +133 -0
  72. package/docs/apis/features/web/esbuild.md +59 -0
  73. package/docs/apis/features/web/helpers.md +135 -0
  74. package/docs/apis/features/web/network.md +30 -0
  75. package/docs/apis/features/web/speech.md +55 -0
  76. package/docs/apis/features/web/vault.md +64 -0
  77. package/docs/apis/features/web/vm.md +84 -0
  78. package/docs/apis/features/web/voice.md +67 -0
  79. package/docs/apis/servers/express.md +127 -0
  80. package/docs/apis/servers/mcp.md +213 -0
  81. package/docs/apis/servers/websocket.md +99 -0
  82. package/docs/documentation-audit.md +134 -0
  83. package/docs/examples/content-db.md +77 -0
  84. package/docs/examples/disk-cache.md +83 -0
  85. package/docs/examples/docker.md +101 -0
  86. package/docs/examples/downloader.md +70 -0
  87. package/docs/examples/esbuild.md +80 -0
  88. package/docs/examples/file-manager.md +82 -0
  89. package/docs/examples/fs.md +83 -0
  90. package/docs/examples/git.md +85 -0
  91. package/docs/examples/google-auth.md +88 -0
  92. package/docs/examples/google-calendar.md +94 -0
  93. package/docs/examples/google-docs.md +82 -0
  94. package/docs/examples/google-drive.md +96 -0
  95. package/docs/examples/google-sheets.md +95 -0
  96. package/docs/examples/grep.md +85 -0
  97. package/docs/examples/ink-blocks.md +75 -0
  98. package/docs/examples/ink-renderer.md +41 -0
  99. package/docs/examples/ink.md +103 -0
  100. package/docs/examples/ipc-socket.md +103 -0
  101. package/docs/examples/json-tree.md +91 -0
  102. package/docs/examples/launcher-app-command-listener.md +120 -0
  103. package/docs/examples/networking.md +58 -0
  104. package/docs/examples/nlp.md +91 -0
  105. package/docs/examples/opener.md +78 -0
  106. package/docs/examples/os.md +72 -0
  107. package/docs/examples/package-finder.md +89 -0
  108. package/docs/examples/port-exposer.md +89 -0
  109. package/docs/examples/postgres.md +91 -0
  110. package/docs/examples/proc.md +81 -0
  111. package/docs/examples/process-manager.md +79 -0
  112. package/docs/examples/python.md +91 -0
  113. package/docs/examples/repl.md +93 -0
  114. package/docs/examples/runpod.md +119 -0
  115. package/docs/examples/secure-shell.md +92 -0
  116. package/docs/examples/sqlite.md +86 -0
  117. package/docs/examples/telegram.md +77 -0
  118. package/docs/examples/tts.md +86 -0
  119. package/docs/examples/ui.md +80 -0
  120. package/docs/examples/vault.md +70 -0
  121. package/docs/examples/vm.md +86 -0
  122. package/docs/examples/window-manager.md +125 -0
  123. package/docs/examples/yaml-tree.md +93 -0
  124. package/docs/examples/yaml.md +104 -0
  125. package/docs/ideas/class-registration-refactor-possibilities.md +197 -0
  126. package/docs/ideas/container-use-api.md +9 -0
  127. package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
  128. package/docs/ideas/feature-stacks.md +22 -0
  129. package/docs/ideas/luca-cli-self-sufficiency-demo.md +23 -0
  130. package/docs/ideas/mcp-design.md +9 -0
  131. package/docs/ideas/web-container-debugging-feature.md +13 -0
  132. package/docs/introspection-audit.md +49 -0
  133. package/docs/introspection.md +154 -0
  134. package/docs/mcp/readme.md +162 -0
  135. package/docs/models.ts +38 -0
  136. package/docs/philosophy.md +85 -0
  137. package/docs/principles.md +7 -0
  138. package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
  139. package/docs/prompts/mcp-test-easy-command.md +27 -0
  140. package/docs/reports/assistant-bugs.md +38 -0
  141. package/docs/reports/attach-pattern-usage.md +18 -0
  142. package/docs/reports/code-audit-results.md +391 -0
  143. package/docs/reports/introspection-audit-tasks.md +378 -0
  144. package/docs/reports/luca-mcp-improvements.md +128 -0
  145. package/docs/scaffolds/client.md +140 -0
  146. package/docs/scaffolds/command.md +106 -0
  147. package/docs/scaffolds/endpoint.md +176 -0
  148. package/docs/scaffolds/feature.md +148 -0
  149. package/docs/scaffolds/server.md +187 -0
  150. package/docs/tasks/web-container-helper-discovery.md +71 -0
  151. package/docs/todos.md +1 -0
  152. package/docs/tutorials/01-getting-started.md +106 -0
  153. package/docs/tutorials/02-container.md +210 -0
  154. package/docs/tutorials/03-scripts.md +194 -0
  155. package/docs/tutorials/04-features-overview.md +196 -0
  156. package/docs/tutorials/05-state-and-events.md +171 -0
  157. package/docs/tutorials/06-servers.md +157 -0
  158. package/docs/tutorials/07-endpoints.md +198 -0
  159. package/docs/tutorials/08-commands.md +171 -0
  160. package/docs/tutorials/09-clients.md +162 -0
  161. package/docs/tutorials/10-creating-features.md +198 -0
  162. package/docs/tutorials/11-contentbase.md +191 -0
  163. package/docs/tutorials/12-assistants.md +215 -0
  164. package/docs/tutorials/13-introspection.md +147 -0
  165. package/docs/tutorials/14-type-system.md +174 -0
  166. package/docs/tutorials/15-project-patterns.md +222 -0
  167. package/docs/tutorials/16-google-features.md +534 -0
  168. package/docs/tutorials/17-tui-blocks.md +530 -0
  169. package/docs/tutorials/18-semantic-search.md +334 -0
  170. package/index.ts +1 -0
  171. package/luca.console.ts +9 -0
  172. package/main.py +6 -0
  173. package/package.json +154 -0
  174. package/pyproject.toml +7 -0
  175. package/scripts/animations/chrome-glitch.ts +55 -0
  176. package/scripts/animations/index.ts +16 -0
  177. package/scripts/animations/neon-pulse.ts +64 -0
  178. package/scripts/animations/types.ts +6 -0
  179. package/scripts/build-web.ts +28 -0
  180. package/scripts/examples/ask-luca-expert.ts +42 -0
  181. package/scripts/examples/assistant-questions.ts +12 -0
  182. package/scripts/examples/excalidraw-expert.ts +75 -0
  183. package/scripts/examples/expert-chat.ts +0 -0
  184. package/scripts/examples/file-manager.ts +14 -0
  185. package/scripts/examples/ideas.ts +12 -0
  186. package/scripts/examples/interactive-chat.ts +20 -0
  187. package/scripts/examples/openai-tool-calls.ts +113 -0
  188. package/scripts/examples/opening-a-web-browser.ts +5 -0
  189. package/scripts/examples/telegram-bot.ts +79 -0
  190. package/scripts/examples/telegram-ink-ui.ts +302 -0
  191. package/scripts/examples/using-assistant-with-mcp.ts +560 -0
  192. package/scripts/examples/using-claude-code.ts +10 -0
  193. package/scripts/examples/using-contentdb.ts +35 -0
  194. package/scripts/examples/using-conversations.ts +35 -0
  195. package/scripts/examples/using-disk-cache.ts +10 -0
  196. package/scripts/examples/using-docker-shell.ts +75 -0
  197. package/scripts/examples/using-elevenlabs.ts +25 -0
  198. package/scripts/examples/using-google-calendar.ts +57 -0
  199. package/scripts/examples/using-google-docs.ts +74 -0
  200. package/scripts/examples/using-google-drive.ts +74 -0
  201. package/scripts/examples/using-google-sheets.ts +89 -0
  202. package/scripts/examples/using-nlp.ts +55 -0
  203. package/scripts/examples/using-ollama.ts +10 -0
  204. package/scripts/examples/using-openai-codex.ts +23 -0
  205. package/scripts/examples/using-postgres.ts +55 -0
  206. package/scripts/examples/using-runpod.ts +32 -0
  207. package/scripts/examples/using-tts.ts +40 -0
  208. package/scripts/examples/vm-loading-esm-modules.ts +16 -0
  209. package/scripts/scaffold.ts +391 -0
  210. package/scripts/scratch.ts +15 -0
  211. package/scripts/test-command-listener.ts +123 -0
  212. package/scripts/test-window-manager-lifecycle.ts +86 -0
  213. package/scripts/test-window-manager.ts +43 -0
  214. package/scripts/update-introspection-data.ts +58 -0
  215. package/src/agi/README.md +14 -0
  216. package/src/agi/container.server.ts +114 -0
  217. package/src/agi/endpoints/ask.ts +60 -0
  218. package/src/agi/endpoints/conversations/[id].ts +45 -0
  219. package/src/agi/endpoints/conversations.ts +31 -0
  220. package/src/agi/endpoints/experts.ts +37 -0
  221. package/src/agi/features/assistant.ts +767 -0
  222. package/src/agi/features/assistants-manager.ts +260 -0
  223. package/src/agi/features/claude-code.ts +1111 -0
  224. package/src/agi/features/conversation-history.ts +497 -0
  225. package/src/agi/features/conversation.ts +799 -0
  226. package/src/agi/features/openai-codex.ts +631 -0
  227. package/src/agi/features/openapi.ts +438 -0
  228. package/src/agi/features/skills-library.ts +425 -0
  229. package/src/agi/index.ts +6 -0
  230. package/src/agi/lib/token-counter.ts +122 -0
  231. package/src/browser.ts +25 -0
  232. package/src/bus.ts +100 -0
  233. package/src/cli/cli.ts +70 -0
  234. package/src/client.ts +461 -0
  235. package/src/clients/civitai/index.ts +541 -0
  236. package/src/clients/client-template.ts +41 -0
  237. package/src/clients/comfyui/index.ts +597 -0
  238. package/src/clients/elevenlabs/index.ts +291 -0
  239. package/src/clients/openai/index.ts +451 -0
  240. package/src/clients/supabase/index.ts +366 -0
  241. package/src/command.ts +164 -0
  242. package/src/commands/chat.ts +182 -0
  243. package/src/commands/console.ts +192 -0
  244. package/src/commands/describe.ts +433 -0
  245. package/src/commands/eval.ts +116 -0
  246. package/src/commands/help.ts +214 -0
  247. package/src/commands/index.ts +14 -0
  248. package/src/commands/mcp.ts +64 -0
  249. package/src/commands/prompt.ts +807 -0
  250. package/src/commands/run.ts +257 -0
  251. package/src/commands/sandbox-mcp.ts +439 -0
  252. package/src/commands/scaffold.ts +79 -0
  253. package/src/commands/serve.ts +172 -0
  254. package/src/container.ts +781 -0
  255. package/src/endpoint.ts +340 -0
  256. package/src/feature.ts +75 -0
  257. package/src/hash-object.ts +97 -0
  258. package/src/helper.ts +543 -0
  259. package/src/introspection/generated.agi.ts +23388 -0
  260. package/src/introspection/generated.node.ts +18899 -0
  261. package/src/introspection/generated.web.ts +2021 -0
  262. package/src/introspection/index.ts +256 -0
  263. package/src/introspection/scan.ts +912 -0
  264. package/src/node/container.ts +354 -0
  265. package/src/node/feature.ts +13 -0
  266. package/src/node/features/container-link.ts +558 -0
  267. package/src/node/features/content-db.ts +475 -0
  268. package/src/node/features/disk-cache.ts +382 -0
  269. package/src/node/features/dns.ts +655 -0
  270. package/src/node/features/docker.ts +912 -0
  271. package/src/node/features/downloader.ts +92 -0
  272. package/src/node/features/esbuild.ts +68 -0
  273. package/src/node/features/file-manager.ts +357 -0
  274. package/src/node/features/fs.ts +534 -0
  275. package/src/node/features/git.ts +492 -0
  276. package/src/node/features/google-auth.ts +502 -0
  277. package/src/node/features/google-calendar.ts +300 -0
  278. package/src/node/features/google-docs.ts +404 -0
  279. package/src/node/features/google-drive.ts +339 -0
  280. package/src/node/features/google-sheets.ts +279 -0
  281. package/src/node/features/grep.ts +406 -0
  282. package/src/node/features/helpers.ts +374 -0
  283. package/src/node/features/ink.ts +490 -0
  284. package/src/node/features/ipc-socket.ts +459 -0
  285. package/src/node/features/json-tree.ts +188 -0
  286. package/src/node/features/launcher-app-command-listener.ts +388 -0
  287. package/src/node/features/networking.ts +925 -0
  288. package/src/node/features/nlp.ts +211 -0
  289. package/src/node/features/opener.ts +166 -0
  290. package/src/node/features/os.ts +157 -0
  291. package/src/node/features/package-finder.ts +539 -0
  292. package/src/node/features/port-exposer.ts +342 -0
  293. package/src/node/features/postgres.ts +273 -0
  294. package/src/node/features/proc.ts +502 -0
  295. package/src/node/features/process-manager.ts +542 -0
  296. package/src/node/features/python.ts +444 -0
  297. package/src/node/features/repl.ts +194 -0
  298. package/src/node/features/runpod.ts +802 -0
  299. package/src/node/features/secure-shell.ts +248 -0
  300. package/src/node/features/semantic-search.ts +924 -0
  301. package/src/node/features/sqlite.ts +289 -0
  302. package/src/node/features/telegram.ts +342 -0
  303. package/src/node/features/tts.ts +184 -0
  304. package/src/node/features/ui.ts +857 -0
  305. package/src/node/features/vault.ts +164 -0
  306. package/src/node/features/vm.ts +312 -0
  307. package/src/node/features/window-manager.ts +804 -0
  308. package/src/node/features/yaml-tree.ts +149 -0
  309. package/src/node/features/yaml.ts +132 -0
  310. package/src/node.ts +70 -0
  311. package/src/react/index.ts +175 -0
  312. package/src/registry.ts +199 -0
  313. package/src/scaffolds/generated.ts +1613 -0
  314. package/src/scaffolds/template.ts +37 -0
  315. package/src/schemas/base.ts +255 -0
  316. package/src/server.ts +135 -0
  317. package/src/servers/express.ts +209 -0
  318. package/src/servers/mcp.ts +805 -0
  319. package/src/servers/socket.ts +120 -0
  320. package/src/state.ts +101 -0
  321. package/src/web/clients/socket.ts +82 -0
  322. package/src/web/container.ts +74 -0
  323. package/src/web/extension.ts +30 -0
  324. package/src/web/feature.ts +12 -0
  325. package/src/web/features/asset-loader.ts +64 -0
  326. package/src/web/features/container-link.ts +385 -0
  327. package/src/web/features/esbuild.ts +79 -0
  328. package/src/web/features/helpers.ts +267 -0
  329. package/src/web/features/network.ts +61 -0
  330. package/src/web/features/speech.ts +87 -0
  331. package/src/web/features/vault.ts +189 -0
  332. package/src/web/features/vm.ts +78 -0
  333. package/src/web/features/voice-recognition.ts +129 -0
  334. package/src/web/shims/isomorphic-vm.ts +149 -0
  335. package/test/bus.test.ts +134 -0
  336. package/test/clients-servers.test.ts +216 -0
  337. package/test/container-link.test.ts +274 -0
  338. package/test/features.test.ts +160 -0
  339. package/test/integration.test.ts +787 -0
  340. package/test/node-container.test.ts +121 -0
  341. package/test/rate-limit.test.ts +272 -0
  342. package/test/semantic-search.test.ts +550 -0
  343. package/test/state.test.ts +121 -0
  344. package/test-integration/assistant.test.ts +138 -0
  345. package/test-integration/assistants-manager.test.ts +123 -0
  346. package/test-integration/claude-code.test.ts +98 -0
  347. package/test-integration/conversation-history.test.ts +205 -0
  348. package/test-integration/conversation.test.ts +137 -0
  349. package/test-integration/elevenlabs.test.ts +55 -0
  350. package/test-integration/google-services.test.ts +80 -0
  351. package/test-integration/helpers.ts +89 -0
  352. package/test-integration/openai-codex.test.ts +93 -0
  353. package/test-integration/runpod.test.ts +58 -0
  354. package/test-integration/server-endpoints.test.ts +97 -0
  355. package/test-integration/skills-library.test.ts +157 -0
  356. package/test-integration/telegram.test.ts +46 -0
  357. package/tsconfig.json +58 -0
  358. 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
+ })