@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,631 @@
1
+ // @ts-nocheck
2
+ import { z } from 'zod'
3
+ import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
4
+ import type { Container } from '@soederpop/luca/container'
5
+ import { type AvailableFeatures } from '@soederpop/luca/feature'
6
+ import { features, Feature } from '@soederpop/luca/feature'
7
+
8
+ declare module '@soederpop/luca/feature' {
9
+ interface AvailableFeatures {
10
+ openaiCodex: typeof OpenAICodex
11
+ }
12
+ }
13
+
14
+ // --- Stream JSON types from the Codex CLI (codex exec --json) ---
15
+
16
+ export interface CodexItem {
17
+ id: string
18
+ type: 'agent_message' | 'reasoning' | 'command_execution' | string
19
+ text?: string
20
+ command?: string
21
+ aggregated_output?: string
22
+ exit_code?: number | null
23
+ status?: 'in_progress' | 'completed' | string
24
+ }
25
+
26
+ export interface CodexItemEvent {
27
+ type: 'item.completed' | 'item.started'
28
+ item: CodexItem
29
+ }
30
+
31
+ export interface CodexTurnEvent {
32
+ type: 'turn.completed' | 'turn.started'
33
+ usage?: { input_tokens?: number; cached_input_tokens?: number; output_tokens?: number }
34
+ }
35
+
36
+ export interface CodexThreadEvent {
37
+ type: 'thread.started'
38
+ thread_id: string
39
+ }
40
+
41
+ /** Normalized message emitted via session:message for downstream consumers. */
42
+ export interface CodexMessageEvent {
43
+ type: 'message'
44
+ role: 'assistant' | 'system'
45
+ content: Array<{ type: 'text'; text: string } | { type: string; [key: string]: any }>
46
+ }
47
+
48
+ export interface CodexExecEvent {
49
+ type: 'exec'
50
+ command: string
51
+ cwd?: string
52
+ exit_code?: number | null
53
+ stdout?: string
54
+ stderr?: string
55
+ }
56
+
57
+ export type CodexEvent =
58
+ | CodexItemEvent
59
+ | CodexTurnEvent
60
+ | CodexThreadEvent
61
+ | { type: string; [key: string]: any }
62
+
63
+ // --- Session types ---
64
+
65
+ export interface CodexSession {
66
+ id: string
67
+ status: 'idle' | 'running' | 'completed' | 'error'
68
+ prompt: string
69
+ result?: string
70
+ error?: string
71
+ turns: number
72
+ messages: CodexMessageEvent[]
73
+ executions: CodexExecEvent[]
74
+ items: CodexItem[]
75
+ process?: any
76
+ threadId?: string
77
+ usage?: { input_tokens?: number; output_tokens?: number }
78
+ }
79
+
80
+ // --- Feature state and options ---
81
+
82
+ export const OpenAICodexStateSchema = FeatureStateSchema.extend({
83
+ sessions: z.record(z.string(), z.any()).describe('Map of session IDs to CodexSession objects'),
84
+ activeSessions: z.array(z.string()).describe('List of currently running session IDs'),
85
+ codexAvailable: z.boolean().describe('Whether the codex CLI binary is available'),
86
+ codexVersion: z.string().optional().describe('Detected codex CLI version string'),
87
+ })
88
+
89
+ export const OpenAICodexOptionsSchema = FeatureOptionsSchema.extend({
90
+ codexPath: z.string().optional().describe('Path to the codex CLI binary'),
91
+ model: z.string().optional().describe('Default model to use for sessions'),
92
+ cwd: z.string().optional().describe('Default working directory for sessions'),
93
+ sandbox: z.enum(['read-only', 'workspace-write', 'danger-full-access']).optional().describe('Sandbox policy for shell commands'),
94
+ approvalMode: z.enum(['suggest', 'auto-edit', 'full-auto']).optional().describe('Approval mode for codex operations'),
95
+ projectDoc: z.string().optional().describe('Path to additional project doc to include'),
96
+ noProjectDoc: z.boolean().optional().describe('Disable automatic codex.md inclusion'),
97
+ fullStdout: z.boolean().optional().describe('Do not truncate stdout/stderr from command outputs'),
98
+ })
99
+
100
+ export type OpenAICodexState = z.infer<typeof OpenAICodexStateSchema>
101
+ export type OpenAICodexOptions = z.infer<typeof OpenAICodexOptionsSchema>
102
+
103
+ export interface CodexRunOptions {
104
+ model?: string
105
+ cwd?: string
106
+ sandbox?: 'read-only' | 'workspace-write' | 'danger-full-access'
107
+ approvalMode?: 'suggest' | 'auto-edit' | 'full-auto'
108
+ projectDoc?: string
109
+ noProjectDoc?: boolean
110
+ fullStdout?: boolean
111
+ images?: string[]
112
+ fullAuto?: boolean
113
+ /** Resume a previous session by ID. */
114
+ resumeSessionId?: string
115
+ /** Resume the most recent session. */
116
+ resumeLast?: boolean
117
+ /** Skip all approvals and sandboxing. */
118
+ dangerouslyAutoApproveEverything?: boolean
119
+ /** Additional CLI flags. */
120
+ extraArgs?: string[]
121
+ }
122
+
123
+ /**
124
+ * OpenAI Codex CLI wrapper feature. Spawns and manages Codex sessions
125
+ * as subprocesses, streaming structured JSON events back through the
126
+ * container's event system.
127
+ *
128
+ * Mirrors the ClaudeCode feature pattern: each call to `run()` spawns a
129
+ * `codex exec --json` process, parses NDJSON from stdout line-by-line,
130
+ * and emits typed events on the feature's event bus.
131
+ *
132
+ * @extends Feature
133
+ *
134
+ * @example
135
+ * ```typescript
136
+ * const codex = container.feature('openaiCodex')
137
+ *
138
+ * // Listen for events
139
+ * codex.on('session:message', ({ sessionId, message }) => console.log(message))
140
+ * codex.on('session:patch', ({ sessionId, patch }) => console.log('File changed:', patch.path))
141
+ *
142
+ * // Run a prompt
143
+ * const session = await codex.run('Fix the failing tests in src/')
144
+ * console.log(session.result)
145
+ * ```
146
+ */
147
+ export class OpenAICodex extends Feature<OpenAICodexState, OpenAICodexOptions> {
148
+ static override stateSchema = OpenAICodexStateSchema
149
+ static override optionsSchema = OpenAICodexOptionsSchema
150
+ static override shortcut = 'features.openaiCodex' as const
151
+
152
+ static attach(container: Container<AvailableFeatures, any>) {
153
+ container.features.register('openaiCodex', OpenAICodex)
154
+ return container
155
+ }
156
+
157
+ override get initialState(): OpenAICodexState {
158
+ return {
159
+ ...super.initialState,
160
+ sessions: {},
161
+ activeSessions: [],
162
+ codexAvailable: false
163
+ }
164
+ }
165
+
166
+ /** @returns The path to the codex CLI binary, falling back to 'codex' on the PATH. */
167
+ get codexPath(): string {
168
+ return this.options.codexPath || 'codex'
169
+ }
170
+
171
+ /**
172
+ * Check if the Codex CLI is available and capture its version.
173
+ *
174
+ * @returns {Promise<boolean>} True if the codex binary was found and responded to --version
175
+ */
176
+ async checkAvailability(): Promise<boolean> {
177
+ try {
178
+ const proc = this.container.feature('proc')
179
+ const result = await proc.spawnAndCapture(this.codexPath, ['--version'])
180
+ const stdout = result.stdout
181
+ const exitCode = result.exitCode
182
+
183
+ if (exitCode === 0) {
184
+ const version = stdout.trim()
185
+ this.setState({ codexAvailable: true, codexVersion: version })
186
+ return true
187
+ }
188
+
189
+ this.setState({ codexAvailable: false })
190
+ return false
191
+ } catch {
192
+ this.setState({ codexAvailable: false })
193
+ return false
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Build the argument array for a codex CLI invocation.
199
+ */
200
+ private buildArgs(options: CodexRunOptions = {}): string[] {
201
+ const args: string[] = ['exec', '--json']
202
+
203
+ const model = options.model ?? this.options.model
204
+ if (model) args.push('--model', model)
205
+
206
+ const sandbox = options.sandbox ?? this.options.sandbox
207
+ if (sandbox) args.push('--sandbox', sandbox)
208
+
209
+ if (options.fullAuto) {
210
+ args.push('--full-auto')
211
+ } else {
212
+ const approvalMode = options.approvalMode ?? this.options.approvalMode
213
+ if (approvalMode === 'auto-edit') args.push('--auto-edit')
214
+ else if (approvalMode === 'full-auto') args.push('--full-auto')
215
+ }
216
+
217
+ const noProjectDoc = options.noProjectDoc ?? this.options.noProjectDoc
218
+ if (noProjectDoc) args.push('--no-project-doc')
219
+
220
+ const projectDoc = options.projectDoc ?? this.options.projectDoc
221
+ if (projectDoc) args.push('--project-doc', projectDoc)
222
+
223
+ const fullStdout = options.fullStdout ?? this.options.fullStdout
224
+ if (fullStdout) args.push('--full-stdout')
225
+
226
+ if (options.images?.length) {
227
+ for (const img of options.images) {
228
+ args.push('--image', img)
229
+ }
230
+ }
231
+
232
+ if (options.resumeSessionId) {
233
+ args.push('resume', options.resumeSessionId)
234
+ } else if (options.resumeLast) {
235
+ args.push('resume', '--last')
236
+ }
237
+
238
+ if (options.dangerouslyAutoApproveEverything) {
239
+ args.push('--dangerously-auto-approve-everything')
240
+ }
241
+
242
+ if (options.extraArgs?.length) {
243
+ args.push(...options.extraArgs)
244
+ }
245
+
246
+ // Read the prompt from stdin to avoid prompt content being parsed as flags.
247
+ args.push('-')
248
+
249
+ return args
250
+ }
251
+
252
+ private createSessionId(): string {
253
+ return crypto.randomUUID()
254
+ }
255
+
256
+ private updateSession(id: string, update: Partial<CodexSession>): void {
257
+ const sessions = { ...this.state.current.sessions }
258
+ const existing = sessions[id]
259
+ if (existing) {
260
+ sessions[id] = { ...existing, ...update }
261
+ this.setState({ sessions })
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Process a parsed JSON event from the Codex CLI stream.
267
+ *
268
+ * The codex CLI (codex exec --json) emits NDJSON with these event types:
269
+ * thread.started — { thread_id }
270
+ * turn.started — (no payload)
271
+ * item.started — { item: { id, type, ... } }
272
+ * item.completed — { item: { id, type, text?, command?, exit_code?, ... } }
273
+ * turn.completed — { usage: { input_tokens, output_tokens } }
274
+ *
275
+ * Item types within item.completed:
276
+ * agent_message — assistant text response
277
+ * reasoning — model thinking/reasoning
278
+ * command_execution — shell command with output
279
+ */
280
+ private handleEvent(sessionId: string, event: CodexEvent): void {
281
+ this.emit('session:event', { sessionId, event })
282
+
283
+ switch (event.type) {
284
+ case 'thread.started': {
285
+ const threadEvent = event as CodexThreadEvent
286
+ this.updateSession(sessionId, { threadId: threadEvent.thread_id })
287
+ break
288
+ }
289
+
290
+ case 'turn.started': {
291
+ const session = this.state.current.sessions[sessionId]
292
+ if (session) {
293
+ this.updateSession(sessionId, { turns: session.turns + 1 })
294
+ }
295
+ break
296
+ }
297
+
298
+ case 'item.completed': {
299
+ const { item } = event as CodexItemEvent
300
+ const session = this.state.current.sessions[sessionId]
301
+ if (!session) break
302
+
303
+ this.updateSession(sessionId, { items: [...session.items, item] })
304
+
305
+ if (item.type === 'agent_message' && item.text) {
306
+ // Normalize to a CodexMessageEvent for downstream consumers
307
+ const msg: CodexMessageEvent = {
308
+ type: 'message',
309
+ role: 'assistant',
310
+ content: [{ type: 'text', text: item.text }]
311
+ }
312
+ this.updateSession(sessionId, { messages: [...session.messages, msg] })
313
+ this.emit('session:delta', { sessionId, text: item.text, role: 'assistant' })
314
+ this.emit('session:message', { sessionId, message: msg })
315
+ } else if (item.type === 'command_execution') {
316
+ const exec: CodexExecEvent = {
317
+ type: 'exec',
318
+ command: item.command || '',
319
+ exit_code: item.exit_code,
320
+ stdout: item.aggregated_output,
321
+ }
322
+ this.updateSession(sessionId, { executions: [...session.executions, exec] })
323
+ this.emit('session:exec', { sessionId, exec })
324
+ } else if (item.type === 'reasoning' && item.text) {
325
+ this.emit('session:reasoning', { sessionId, text: item.text })
326
+ }
327
+ break
328
+ }
329
+
330
+ case 'item.started': {
331
+ const { item } = event as CodexItemEvent
332
+ if (item.type === 'command_execution' && item.command) {
333
+ this.emit('session:exec-start', { sessionId, command: item.command })
334
+ }
335
+ break
336
+ }
337
+
338
+ case 'turn.completed': {
339
+ const turnEvent = event as CodexTurnEvent
340
+ if (turnEvent.usage) {
341
+ this.updateSession(sessionId, { usage: turnEvent.usage })
342
+ }
343
+ break
344
+ }
345
+
346
+ default: {
347
+ // Forward unknown events for extensibility
348
+ this.emit(`session:${event.type}`, { sessionId, event })
349
+ break
350
+ }
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Run a prompt in a new Codex session. Spawns a subprocess,
356
+ * streams NDJSON events, and resolves when the session completes.
357
+ *
358
+ * @param {string} prompt - The natural language instruction for the Codex agent
359
+ * @param {CodexRunOptions} [options] - Optional overrides for model, cwd, sandbox policy, etc.
360
+ * @returns {Promise<CodexSession>} The completed session with result, messages, patches, and executions
361
+ *
362
+ * @example
363
+ * ```typescript
364
+ * const session = await codex.run('Fix the failing tests')
365
+ * console.log(session.result)
366
+ *
367
+ * const session = await codex.run('Refactor the auth module', {
368
+ * model: 'o4-mini',
369
+ * fullAuto: true,
370
+ * cwd: '/path/to/project'
371
+ * })
372
+ * ```
373
+ */
374
+ async run(prompt: string, options: CodexRunOptions = {}): Promise<CodexSession> {
375
+ const id = this.createSessionId()
376
+ const args = this.buildArgs(options)
377
+ const cwd = options.cwd ?? this.options.cwd ?? (this.container as any).cwd
378
+
379
+ const session: CodexSession = {
380
+ id,
381
+ status: 'running',
382
+ prompt,
383
+ turns: 0,
384
+ messages: [],
385
+ executions: [],
386
+ items: []
387
+ }
388
+
389
+ const sessions = { ...this.state.current.sessions, [id]: session }
390
+ const activeSessions = [...this.state.current.activeSessions, id]
391
+ this.setState({ sessions, activeSessions })
392
+
393
+ this.emit('session:start', { sessionId: id, prompt })
394
+
395
+ const proc = this.container.feature('proc').spawn(this.codexPath, args, {
396
+ cwd,
397
+ stdout: 'pipe',
398
+ stderr: 'pipe',
399
+ stdin: Buffer.from(prompt),
400
+ environment: { ...process.env },
401
+ })
402
+
403
+ this.updateSession(id, { process: proc })
404
+ await this.consumeStream(id, proc)
405
+
406
+ return this.state.current.sessions[id]!
407
+ }
408
+
409
+ /**
410
+ * Run a prompt without waiting for completion. Returns the session ID
411
+ * immediately so you can subscribe to events.
412
+ *
413
+ * @param {string} prompt - The natural language instruction for the Codex agent
414
+ * @param {CodexRunOptions} [options] - Optional overrides for model, cwd, sandbox policy, etc.
415
+ * @returns {string} The session ID, which can be used with getSession() or waitForSession()
416
+ *
417
+ * @example
418
+ * ```typescript
419
+ * const sessionId = codex.start('Build a REST API for users')
420
+ *
421
+ * codex.on('session:delta', ({ sessionId: sid, text }) => {
422
+ * if (sid === sessionId) process.stdout.write(text)
423
+ * })
424
+ * ```
425
+ */
426
+ start(prompt: string, options: CodexRunOptions = {}): string {
427
+ const id = this.createSessionId()
428
+ const args = this.buildArgs(options)
429
+ const cwd = options.cwd ?? this.options.cwd ?? (this.container as any).cwd
430
+
431
+ const session: CodexSession = {
432
+ id,
433
+ status: 'running',
434
+ prompt,
435
+ turns: 0,
436
+ messages: [],
437
+ executions: [],
438
+ items: []
439
+ }
440
+
441
+ const sessions = { ...this.state.current.sessions, [id]: session }
442
+ const activeSessions = [...this.state.current.activeSessions, id]
443
+ this.setState({ sessions, activeSessions })
444
+
445
+ this.emit('session:start', { sessionId: id, prompt })
446
+
447
+ const proc = this.container.feature('proc').spawn(this.codexPath, args, {
448
+ cwd,
449
+ stdout: 'pipe',
450
+ stderr: 'pipe',
451
+ stdin: Buffer.from(prompt),
452
+ environment: { ...process.env },
453
+ })
454
+
455
+ this.updateSession(id, { process: proc })
456
+ this.consumeStream(id, proc)
457
+
458
+ return id
459
+ }
460
+
461
+ private async consumeStream(sessionId: string, proc: any): Promise<void> {
462
+ if (!proc?.stdout || !proc?.stderr) {
463
+ const error = 'Process streams are not available'
464
+ this.updateSession(sessionId, { status: 'error', error })
465
+ this.emit('session:error', { sessionId, error })
466
+ return
467
+ }
468
+
469
+ let buffer = ''
470
+ let lastText = ''
471
+ let stderr = ''
472
+
473
+ proc.stderr.on('data', (chunk: Buffer | string) => {
474
+ stderr += Buffer.isBuffer(chunk) ? chunk.toString() : String(chunk)
475
+ })
476
+
477
+ const stdoutDone = new Promise<void>((resolve, reject) => {
478
+ proc.stdout.on('data', (chunk: Buffer | string) => {
479
+ buffer += Buffer.isBuffer(chunk) ? chunk.toString() : String(chunk)
480
+ const lines = buffer.split('\n')
481
+ buffer = lines.pop() || ''
482
+
483
+ for (const line of lines) {
484
+ const trimmed = line.trim()
485
+ if (!trimmed) continue
486
+
487
+ try {
488
+ const event = JSON.parse(trimmed) as CodexEvent
489
+ this.handleEvent(sessionId, event)
490
+
491
+ if (event.type === 'item.completed') {
492
+ const { item } = event as CodexItemEvent
493
+ if (item.type === 'agent_message' && item.text) {
494
+ lastText = item.text
495
+ }
496
+ }
497
+ } catch {
498
+ this.emit('session:parse-error', { sessionId, line: trimmed })
499
+ }
500
+ }
501
+ })
502
+
503
+ proc.stdout.on('end', () => {
504
+ if (buffer.trim()) {
505
+ try {
506
+ const event = JSON.parse(buffer.trim()) as CodexEvent
507
+ this.handleEvent(sessionId, event)
508
+ } catch {
509
+ // ignore trailing partial data
510
+ }
511
+ }
512
+ resolve()
513
+ })
514
+
515
+ proc.stdout.on('error', reject)
516
+ })
517
+
518
+ const exitCodePromise = new Promise<number>((resolve, reject) => {
519
+ proc.once('error', reject)
520
+ proc.once('close', (code: number | null) => resolve(code ?? 0))
521
+ })
522
+
523
+ try {
524
+ await stdoutDone
525
+ } catch (err) {
526
+ this.updateSession(sessionId, {
527
+ status: 'error',
528
+ error: err instanceof Error ? err.message : String(err)
529
+ })
530
+ this.emit('session:error', { sessionId, error: err })
531
+ }
532
+
533
+ let exitCode = 1
534
+ try {
535
+ exitCode = await exitCodePromise
536
+ } catch (err) {
537
+ this.updateSession(sessionId, {
538
+ status: 'error',
539
+ error: err instanceof Error ? err.message : String(err),
540
+ })
541
+ this.emit('session:error', { sessionId, error: err })
542
+ }
543
+
544
+ if (exitCode !== 0 && this.state.current.sessions[sessionId]?.status !== 'completed') {
545
+ this.updateSession(sessionId, {
546
+ status: 'error',
547
+ error: stderr || `Process exited with code ${exitCode}`
548
+ })
549
+ this.emit('session:error', { sessionId, error: stderr, exitCode })
550
+ } else if (this.state.current.sessions[sessionId]?.status === 'running') {
551
+ this.updateSession(sessionId, {
552
+ status: 'completed',
553
+ result: lastText || undefined
554
+ })
555
+
556
+ const activeSessions = this.state.current.activeSessions.filter(s => s !== sessionId)
557
+ this.setState({ activeSessions })
558
+
559
+ this.emit('session:result', {
560
+ sessionId,
561
+ result: lastText,
562
+ })
563
+ }
564
+ }
565
+
566
+ /**
567
+ * Kill a running session's subprocess.
568
+ *
569
+ * @param {string} sessionId - The session ID to abort
570
+ * @returns {void}
571
+ */
572
+ abort(sessionId: string): void {
573
+ const session = this.state.current.sessions[sessionId]
574
+ if (session?.process && session.status === 'running') {
575
+ session.process.kill()
576
+ this.updateSession(sessionId, { status: 'error', error: 'Aborted by user' })
577
+ const activeSessions = this.state.current.activeSessions.filter(id => id !== sessionId)
578
+ this.setState({ activeSessions })
579
+ this.emit('session:abort', { sessionId })
580
+ }
581
+ }
582
+
583
+ /**
584
+ * Retrieve the current state of a session by its ID.
585
+ *
586
+ * @param {string} sessionId - The session ID to look up
587
+ * @returns {CodexSession | undefined} The session object, or undefined if not found
588
+ */
589
+ getSession(sessionId: string): CodexSession | undefined {
590
+ return this.state.current.sessions[sessionId]
591
+ }
592
+
593
+ /**
594
+ * Wait for a running session to complete or error. Resolves immediately
595
+ * if the session is already in a terminal state.
596
+ *
597
+ * @param {string} sessionId - The session ID to wait for
598
+ * @returns {Promise<CodexSession>} The completed or errored session
599
+ * @throws {Error} If the session ID is not found
600
+ */
601
+ async waitForSession(sessionId: string): Promise<CodexSession> {
602
+ const session = this.state.current.sessions[sessionId]
603
+ if (!session) throw new Error(`Session ${sessionId} not found`)
604
+ if (session.status === 'completed' || session.status === 'error') return session
605
+
606
+ return new Promise((resolve) => {
607
+ const handler = (data: { sessionId: string }) => {
608
+ if (data.sessionId === sessionId) {
609
+ this.off('session:result')
610
+ this.off('session:error')
611
+ resolve(this.state.current.sessions[sessionId]!)
612
+ }
613
+ }
614
+ this.on('session:result', handler)
615
+ this.on('session:error', handler)
616
+ })
617
+ }
618
+
619
+ /**
620
+ * Enable the feature. Delegates to the base Feature enable() lifecycle.
621
+ *
622
+ * @param {object} [options] - Options to merge into the feature configuration
623
+ * @returns {Promise<this>} This instance, for chaining
624
+ */
625
+ override async enable(options: any = {}): Promise<this> {
626
+ await super.enable(options)
627
+ return this
628
+ }
629
+ }
630
+
631
+ export default features.register('openaiCodex', OpenAICodex)