oh-my-codex 0.17.3 → 0.18.1

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 (381) hide show
  1. package/Cargo.lock +13 -5
  2. package/Cargo.toml +2 -1
  3. package/README.md +44 -19
  4. package/crates/omx-api/Cargo.toml +19 -0
  5. package/crates/omx-api/src/lib.rs +2997 -0
  6. package/crates/omx-api/src/main.rs +10 -0
  7. package/crates/omx-api/tests/cli.rs +558 -0
  8. package/crates/omx-explore/src/main.rs +4 -0
  9. package/crates/omx-sparkshell/src/codex_bridge.rs +437 -123
  10. package/crates/omx-sparkshell/src/exec.rs +127 -1
  11. package/crates/omx-sparkshell/src/main.rs +829 -30
  12. package/crates/omx-sparkshell/src/prompt.rs +25 -3
  13. package/crates/omx-sparkshell/src/redaction.rs +241 -0
  14. package/crates/omx-sparkshell/tests/execution.rs +702 -237
  15. package/dist/cli/__tests__/api.test.d.ts +2 -0
  16. package/dist/cli/__tests__/api.test.d.ts.map +1 -0
  17. package/dist/cli/__tests__/api.test.js +175 -0
  18. package/dist/cli/__tests__/api.test.js.map +1 -0
  19. package/dist/cli/__tests__/ask.test.js +72 -5
  20. package/dist/cli/__tests__/ask.test.js.map +1 -1
  21. package/dist/cli/__tests__/autoresearch-goal.test.js +14 -1
  22. package/dist/cli/__tests__/autoresearch-goal.test.js.map +1 -1
  23. package/dist/cli/__tests__/codex-plugin-layout.test.js +15 -7
  24. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  25. package/dist/cli/__tests__/doctor-warning-copy.test.js +76 -3
  26. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  27. package/dist/cli/__tests__/explore.test.js +23 -0
  28. package/dist/cli/__tests__/explore.test.js.map +1 -1
  29. package/dist/cli/__tests__/index.test.js +171 -5
  30. package/dist/cli/__tests__/index.test.js.map +1 -1
  31. package/dist/cli/__tests__/install-docs-contract.test.d.ts +2 -0
  32. package/dist/cli/__tests__/install-docs-contract.test.d.ts.map +1 -0
  33. package/dist/cli/__tests__/install-docs-contract.test.js +55 -0
  34. package/dist/cli/__tests__/install-docs-contract.test.js.map +1 -0
  35. package/dist/cli/__tests__/launch-fallback.test.js +191 -0
  36. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  37. package/dist/cli/__tests__/package-bin-contract.test.js +4 -3
  38. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  39. package/dist/cli/__tests__/question.test.js +27 -41
  40. package/dist/cli/__tests__/question.test.js.map +1 -1
  41. package/dist/cli/__tests__/setup-install-mode.test.js +232 -35
  42. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  43. package/dist/cli/__tests__/sparkshell-cli.test.js +25 -1
  44. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  45. package/dist/cli/__tests__/sparkshell-packaging.test.js +1 -0
  46. package/dist/cli/__tests__/sparkshell-packaging.test.js.map +1 -1
  47. package/dist/cli/__tests__/ultragoal.test.js +227 -4
  48. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  49. package/dist/cli/__tests__/update.test.js +72 -1
  50. package/dist/cli/__tests__/update.test.js.map +1 -1
  51. package/dist/cli/__tests__/version-sync-contract.test.js +4 -0
  52. package/dist/cli/__tests__/version-sync-contract.test.js.map +1 -1
  53. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +1 -1
  54. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  55. package/dist/cli/api.d.ts +26 -0
  56. package/dist/cli/api.d.ts.map +1 -0
  57. package/dist/cli/api.js +153 -0
  58. package/dist/cli/api.js.map +1 -0
  59. package/dist/cli/codex-feature-probe.d.ts +5 -0
  60. package/dist/cli/codex-feature-probe.d.ts.map +1 -1
  61. package/dist/cli/codex-feature-probe.js +13 -7
  62. package/dist/cli/codex-feature-probe.js.map +1 -1
  63. package/dist/cli/doctor.d.ts +7 -0
  64. package/dist/cli/doctor.d.ts.map +1 -1
  65. package/dist/cli/doctor.js +119 -10
  66. package/dist/cli/doctor.js.map +1 -1
  67. package/dist/cli/explore.d.ts +2 -0
  68. package/dist/cli/explore.d.ts.map +1 -1
  69. package/dist/cli/explore.js +43 -1
  70. package/dist/cli/explore.js.map +1 -1
  71. package/dist/cli/index.d.ts +12 -4
  72. package/dist/cli/index.d.ts.map +1 -1
  73. package/dist/cli/index.js +460 -87
  74. package/dist/cli/index.js.map +1 -1
  75. package/dist/cli/native-assets.d.ts +2 -1
  76. package/dist/cli/native-assets.d.ts.map +1 -1
  77. package/dist/cli/native-assets.js +1 -0
  78. package/dist/cli/native-assets.js.map +1 -1
  79. package/dist/cli/plugin-marketplace.d.ts +2 -0
  80. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  81. package/dist/cli/plugin-marketplace.js +15 -1
  82. package/dist/cli/plugin-marketplace.js.map +1 -1
  83. package/dist/cli/setup.d.ts.map +1 -1
  84. package/dist/cli/setup.js +71 -11
  85. package/dist/cli/setup.js.map +1 -1
  86. package/dist/cli/sparkshell.d.ts +7 -1
  87. package/dist/cli/sparkshell.d.ts.map +1 -1
  88. package/dist/cli/sparkshell.js +31 -4
  89. package/dist/cli/sparkshell.js.map +1 -1
  90. package/dist/cli/ultragoal.d.ts +1 -1
  91. package/dist/cli/ultragoal.d.ts.map +1 -1
  92. package/dist/cli/ultragoal.js +184 -10
  93. package/dist/cli/ultragoal.js.map +1 -1
  94. package/dist/cli/update.d.ts +2 -0
  95. package/dist/cli/update.d.ts.map +1 -1
  96. package/dist/cli/update.js +14 -3
  97. package/dist/cli/update.js.map +1 -1
  98. package/dist/compat/__tests__/doctor-contract.test.js +3 -0
  99. package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
  100. package/dist/config/__tests__/codex-feature-flags.test.js +11 -1
  101. package/dist/config/__tests__/codex-feature-flags.test.js.map +1 -1
  102. package/dist/config/__tests__/codex-hooks.test.js +19 -8
  103. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  104. package/dist/config/__tests__/commit-lore-guard.test.d.ts +2 -0
  105. package/dist/config/__tests__/commit-lore-guard.test.d.ts.map +1 -0
  106. package/dist/config/__tests__/commit-lore-guard.test.js +20 -0
  107. package/dist/config/__tests__/commit-lore-guard.test.js.map +1 -0
  108. package/dist/config/codex-feature-flags.d.ts +4 -0
  109. package/dist/config/codex-feature-flags.d.ts.map +1 -1
  110. package/dist/config/codex-feature-flags.js +4 -0
  111. package/dist/config/codex-feature-flags.js.map +1 -1
  112. package/dist/config/codex-hooks.js +6 -6
  113. package/dist/config/codex-hooks.js.map +1 -1
  114. package/dist/config/commit-lore-guard.d.ts +1 -0
  115. package/dist/config/commit-lore-guard.d.ts.map +1 -1
  116. package/dist/config/commit-lore-guard.js +29 -3
  117. package/dist/config/commit-lore-guard.js.map +1 -1
  118. package/dist/config/generator.d.ts +3 -1
  119. package/dist/config/generator.d.ts.map +1 -1
  120. package/dist/config/generator.js +114 -10
  121. package/dist/config/generator.js.map +1 -1
  122. package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -0
  123. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
  124. package/dist/goal-workflows/codex-goal-snapshot.js +5 -1
  125. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
  126. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +10 -6
  127. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  128. package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts +2 -0
  129. package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts.map +1 -0
  130. package/dist/hooks/__tests__/best-practice-research-skill.test.js +27 -0
  131. package/dist/hooks/__tests__/best-practice-research-skill.test.js.map +1 -0
  132. package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts +1 -1
  133. package/dist/hooks/__tests__/consensus-execution-handoff.test.js +13 -11
  134. package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
  135. package/dist/hooks/__tests__/deep-interview-contract.test.js +4 -3
  136. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  137. package/dist/hooks/__tests__/keyword-detector.test.js +15 -3
  138. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  139. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +6 -0
  140. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  141. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +33 -0
  142. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  143. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +4 -0
  144. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
  145. package/dist/hooks/extensibility/__tests__/dispatcher.test.js +26 -3
  146. package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
  147. package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
  148. package/dist/hooks/extensibility/dispatcher.js +29 -14
  149. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  150. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  151. package/dist/hooks/keyword-detector.js +8 -3
  152. package/dist/hooks/keyword-detector.js.map +1 -1
  153. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  154. package/dist/hooks/keyword-registry.js +1 -0
  155. package/dist/hooks/keyword-registry.js.map +1 -1
  156. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  157. package/dist/hooks/prompt-guidance-contract.js +3 -2
  158. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  159. package/dist/hud/__tests__/hud-tmux-injection.test.js +14 -8
  160. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  161. package/dist/hud/__tests__/reconcile.test.js +4 -4
  162. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  163. package/dist/hud/__tests__/resource-leak-watch.test.d.ts +2 -0
  164. package/dist/hud/__tests__/resource-leak-watch.test.d.ts.map +1 -0
  165. package/dist/hud/__tests__/resource-leak-watch.test.js +28 -0
  166. package/dist/hud/__tests__/resource-leak-watch.test.js.map +1 -0
  167. package/dist/hud/__tests__/tmux.test.js +23 -18
  168. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  169. package/dist/hud/index.d.ts +1 -1
  170. package/dist/hud/index.d.ts.map +1 -1
  171. package/dist/hud/index.js +10 -4
  172. package/dist/hud/index.js.map +1 -1
  173. package/dist/hud/tmux.d.ts.map +1 -1
  174. package/dist/hud/tmux.js +9 -8
  175. package/dist/hud/tmux.js.map +1 -1
  176. package/dist/mcp/__tests__/bootstrap.test.js +75 -1
  177. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  178. package/dist/mcp/bootstrap.d.ts +3 -1
  179. package/dist/mcp/bootstrap.d.ts.map +1 -1
  180. package/dist/mcp/bootstrap.js +71 -2
  181. package/dist/mcp/bootstrap.js.map +1 -1
  182. package/dist/notifications/__tests__/http-client-resource.test.d.ts +2 -0
  183. package/dist/notifications/__tests__/http-client-resource.test.d.ts.map +1 -0
  184. package/dist/notifications/__tests__/http-client-resource.test.js +41 -0
  185. package/dist/notifications/__tests__/http-client-resource.test.js.map +1 -0
  186. package/dist/notifications/__tests__/verbosity.test.js +20 -0
  187. package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
  188. package/dist/notifications/config.d.ts.map +1 -1
  189. package/dist/notifications/config.js +6 -3
  190. package/dist/notifications/config.js.map +1 -1
  191. package/dist/notifications/http-client.d.ts.map +1 -1
  192. package/dist/notifications/http-client.js +78 -27
  193. package/dist/notifications/http-client.js.map +1 -1
  194. package/dist/notifications/types.d.ts +2 -0
  195. package/dist/notifications/types.d.ts.map +1 -1
  196. package/dist/openclaw/__tests__/dispatcher.test.js +49 -1
  197. package/dist/openclaw/__tests__/dispatcher.test.js.map +1 -1
  198. package/dist/openclaw/dispatcher.d.ts +7 -4
  199. package/dist/openclaw/dispatcher.d.ts.map +1 -1
  200. package/dist/openclaw/dispatcher.js +32 -69
  201. package/dist/openclaw/dispatcher.js.map +1 -1
  202. package/dist/pipeline/__tests__/orchestrator.test.js +65 -3
  203. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  204. package/dist/pipeline/__tests__/stages.test.js +50 -5
  205. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  206. package/dist/pipeline/index.d.ts +8 -2
  207. package/dist/pipeline/index.d.ts.map +1 -1
  208. package/dist/pipeline/index.js +5 -2
  209. package/dist/pipeline/index.js.map +1 -1
  210. package/dist/pipeline/orchestrator.d.ts +5 -4
  211. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  212. package/dist/pipeline/orchestrator.js +56 -15
  213. package/dist/pipeline/orchestrator.js.map +1 -1
  214. package/dist/pipeline/stages/code-review.d.ts +2 -2
  215. package/dist/pipeline/stages/code-review.d.ts.map +1 -1
  216. package/dist/pipeline/stages/code-review.js +5 -3
  217. package/dist/pipeline/stages/code-review.js.map +1 -1
  218. package/dist/pipeline/stages/deep-interview.d.ts +15 -0
  219. package/dist/pipeline/stages/deep-interview.d.ts.map +1 -0
  220. package/dist/pipeline/stages/deep-interview.js +32 -0
  221. package/dist/pipeline/stages/deep-interview.js.map +1 -0
  222. package/dist/pipeline/stages/ralph-verify.d.ts +5 -5
  223. package/dist/pipeline/stages/ralph-verify.d.ts.map +1 -1
  224. package/dist/pipeline/stages/ralph-verify.js +2 -2
  225. package/dist/pipeline/stages/ralph-verify.js.map +1 -1
  226. package/dist/pipeline/stages/ultragoal.d.ts +19 -0
  227. package/dist/pipeline/stages/ultragoal.d.ts.map +1 -0
  228. package/dist/pipeline/stages/ultragoal.js +38 -0
  229. package/dist/pipeline/stages/ultragoal.js.map +1 -0
  230. package/dist/pipeline/stages/ultraqa.d.ts +30 -0
  231. package/dist/pipeline/stages/ultraqa.d.ts.map +1 -0
  232. package/dist/pipeline/stages/ultraqa.js +46 -0
  233. package/dist/pipeline/stages/ultraqa.js.map +1 -0
  234. package/dist/pipeline/types.d.ts +8 -6
  235. package/dist/pipeline/types.d.ts.map +1 -1
  236. package/dist/pipeline/types.js +2 -2
  237. package/dist/scripts/__tests__/codex-native-hook.test.js +1488 -117
  238. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  239. package/dist/scripts/__tests__/notify-dispatcher.test.js +183 -1
  240. package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
  241. package/dist/scripts/__tests__/smoke-packed-install.test.js +27 -2
  242. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  243. package/dist/scripts/__tests__/verify-native-agents.test.js +16 -1
  244. package/dist/scripts/__tests__/verify-native-agents.test.js.map +1 -1
  245. package/dist/scripts/build-api.d.ts +2 -0
  246. package/dist/scripts/build-api.d.ts.map +1 -0
  247. package/dist/scripts/build-api.js +44 -0
  248. package/dist/scripts/build-api.js.map +1 -0
  249. package/dist/scripts/cleanup-explore-harness.js +1 -0
  250. package/dist/scripts/cleanup-explore-harness.js.map +1 -1
  251. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  252. package/dist/scripts/codex-native-hook.js +364 -16
  253. package/dist/scripts/codex-native-hook.js.map +1 -1
  254. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  255. package/dist/scripts/codex-native-pre-post.js +98 -25
  256. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  257. package/dist/scripts/notify-dispatcher.js +88 -0
  258. package/dist/scripts/notify-dispatcher.js.map +1 -1
  259. package/dist/scripts/notify-hook/process-runner.d.ts.map +1 -1
  260. package/dist/scripts/notify-hook/process-runner.js +39 -17
  261. package/dist/scripts/notify-hook/process-runner.js.map +1 -1
  262. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  263. package/dist/scripts/notify-hook/team-dispatch.js +36 -14
  264. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  265. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  266. package/dist/scripts/notify-hook/team-leader-nudge.js +26 -11
  267. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  268. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +2 -1
  269. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  270. package/dist/scripts/notify-hook/team-tmux-guard.js +45 -1
  271. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  272. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  273. package/dist/scripts/notify-hook/team-worker-stop.js +27 -14
  274. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  275. package/dist/scripts/run-provider-advisor.js +9 -3
  276. package/dist/scripts/run-provider-advisor.js.map +1 -1
  277. package/dist/scripts/smoke-packed-install.d.ts +4 -1
  278. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  279. package/dist/scripts/smoke-packed-install.js +101 -1
  280. package/dist/scripts/smoke-packed-install.js.map +1 -1
  281. package/dist/scripts/sync-plugin-mirror.js +2 -2
  282. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  283. package/dist/scripts/verify-native-agents.js +2 -2
  284. package/dist/scripts/verify-native-agents.js.map +1 -1
  285. package/dist/sidecar/__tests__/resource-leak-watch.test.d.ts +2 -0
  286. package/dist/sidecar/__tests__/resource-leak-watch.test.d.ts.map +1 -0
  287. package/dist/sidecar/__tests__/resource-leak-watch.test.js +38 -0
  288. package/dist/sidecar/__tests__/resource-leak-watch.test.js.map +1 -0
  289. package/dist/sidecar/index.d.ts +1 -1
  290. package/dist/sidecar/index.d.ts.map +1 -1
  291. package/dist/sidecar/index.js +29 -12
  292. package/dist/sidecar/index.js.map +1 -1
  293. package/dist/state/__tests__/operations-ralph-phase.test.js +88 -1
  294. package/dist/state/__tests__/operations-ralph-phase.test.js.map +1 -1
  295. package/dist/state/operations.d.ts.map +1 -1
  296. package/dist/state/operations.js +11 -0
  297. package/dist/state/operations.js.map +1 -1
  298. package/dist/team/__tests__/runtime.test.js +2 -2
  299. package/dist/team/__tests__/runtime.test.js.map +1 -1
  300. package/dist/team/__tests__/tmux-session.test.js +207 -22
  301. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  302. package/dist/team/tmux-session.d.ts +1 -0
  303. package/dist/team/tmux-session.d.ts.map +1 -1
  304. package/dist/team/tmux-session.js +73 -28
  305. package/dist/team/tmux-session.js.map +1 -1
  306. package/dist/ultragoal/__tests__/artifacts.test.js +714 -10
  307. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  308. package/dist/ultragoal/__tests__/docs-contract.test.js +57 -1
  309. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  310. package/dist/ultragoal/__tests__/steering-fixtures.d.ts +68 -0
  311. package/dist/ultragoal/__tests__/steering-fixtures.d.ts.map +1 -0
  312. package/dist/ultragoal/__tests__/steering-fixtures.js +259 -0
  313. package/dist/ultragoal/__tests__/steering-fixtures.js.map +1 -0
  314. package/dist/ultragoal/__tests__/steering-fixtures.test.d.ts +2 -0
  315. package/dist/ultragoal/__tests__/steering-fixtures.test.d.ts.map +1 -0
  316. package/dist/ultragoal/__tests__/steering-fixtures.test.js +65 -0
  317. package/dist/ultragoal/__tests__/steering-fixtures.test.js.map +1 -0
  318. package/dist/ultragoal/artifacts.d.ts +97 -2
  319. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  320. package/dist/ultragoal/artifacts.js +811 -256
  321. package/dist/ultragoal/artifacts.js.map +1 -1
  322. package/dist/utils/__tests__/sleep-resource.test.d.ts +2 -0
  323. package/dist/utils/__tests__/sleep-resource.test.d.ts.map +1 -0
  324. package/dist/utils/__tests__/sleep-resource.test.js +39 -0
  325. package/dist/utils/__tests__/sleep-resource.test.js.map +1 -0
  326. package/dist/utils/sleep.d.ts.map +1 -1
  327. package/dist/utils/sleep.js +17 -6
  328. package/dist/utils/sleep.js.map +1 -1
  329. package/dist/verification/__tests__/ci-rust-gates.test.js +85 -10
  330. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  331. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +1 -0
  332. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  333. package/package.json +5 -3
  334. package/plugins/oh-my-codex/.codex-plugin/plugin.json +4 -3
  335. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +56 -0
  336. package/plugins/oh-my-codex/hooks/hooks.json +77 -0
  337. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +77 -47
  338. package/plugins/oh-my-codex/skills/best-practice-research/SKILL.md +83 -0
  339. package/plugins/oh-my-codex/skills/cancel/SKILL.md +2 -2
  340. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +9 -8
  341. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +1 -1
  342. package/plugins/oh-my-codex/skills/pipeline/SKILL.md +22 -11
  343. package/plugins/oh-my-codex/skills/plan/SKILL.md +8 -8
  344. package/plugins/oh-my-codex/skills/ralph/SKILL.md +7 -0
  345. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +5 -5
  346. package/plugins/oh-my-codex/skills/team/SKILL.md +1 -1
  347. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +38 -4
  348. package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +1 -1
  349. package/prompts/planner.md +1 -1
  350. package/prompts/researcher.md +15 -10
  351. package/skills/autopilot/SKILL.md +77 -47
  352. package/skills/best-practice-research/SKILL.md +83 -0
  353. package/skills/cancel/SKILL.md +2 -2
  354. package/skills/deep-interview/SKILL.md +9 -8
  355. package/skills/omx-setup/SKILL.md +1 -1
  356. package/skills/pipeline/SKILL.md +22 -11
  357. package/skills/plan/SKILL.md +8 -8
  358. package/skills/ralph/SKILL.md +7 -0
  359. package/skills/ralplan/SKILL.md +5 -5
  360. package/skills/team/SKILL.md +1 -1
  361. package/skills/ultragoal/SKILL.md +38 -4
  362. package/skills/ultrawork/SKILL.md +1 -1
  363. package/src/scripts/__tests__/codex-native-hook.test.ts +1758 -166
  364. package/src/scripts/__tests__/notify-dispatcher.test.ts +223 -1
  365. package/src/scripts/__tests__/smoke-packed-install.test.ts +39 -2
  366. package/src/scripts/__tests__/verify-native-agents.test.ts +21 -1
  367. package/src/scripts/build-api.ts +48 -0
  368. package/src/scripts/cleanup-explore-harness.ts +1 -0
  369. package/src/scripts/codex-native-hook.ts +416 -18
  370. package/src/scripts/codex-native-pre-post.ts +119 -25
  371. package/src/scripts/notify-dispatcher.ts +97 -0
  372. package/src/scripts/notify-hook/process-runner.ts +40 -16
  373. package/src/scripts/notify-hook/team-dispatch.ts +36 -13
  374. package/src/scripts/notify-hook/team-leader-nudge.ts +25 -11
  375. package/src/scripts/notify-hook/team-tmux-guard.ts +49 -0
  376. package/src/scripts/notify-hook/team-worker-stop.ts +24 -13
  377. package/src/scripts/run-provider-advisor.ts +11 -3
  378. package/src/scripts/smoke-packed-install.ts +107 -0
  379. package/src/scripts/sync-plugin-mirror.ts +3 -3
  380. package/src/scripts/verify-native-agents.ts +2 -2
  381. package/templates/catalog-manifest.json +7 -0
@@ -0,0 +1,10 @@
1
+ fn main() {
2
+ if let Err(error) = omx_api::run_cli(
3
+ std::env::args().skip(1),
4
+ std::io::stdout(),
5
+ std::io::stderr(),
6
+ ) {
7
+ eprintln!("{error}");
8
+ std::process::exit(1);
9
+ }
10
+ }
@@ -0,0 +1,558 @@
1
+ use omx_api::{http_request, http_request_with_bearer, read_daemon_state};
2
+ use serde_json::Value;
3
+ use std::io::{Read, Write};
4
+ use std::net::TcpListener;
5
+ use std::process::{Command, Stdio};
6
+ use std::sync::atomic::{AtomicU64, Ordering};
7
+ use std::thread;
8
+ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
9
+
10
+ fn temp_state_file(name: &str) -> std::path::PathBuf {
11
+ static COUNTER: AtomicU64 = AtomicU64::new(0);
12
+ let nanos = SystemTime::now()
13
+ .duration_since(UNIX_EPOCH)
14
+ .unwrap()
15
+ .as_nanos();
16
+ let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
17
+ std::env::temp_dir().join(format!(
18
+ "omx-api-{name}-{}-{nanos}-{counter}.json",
19
+ std::process::id()
20
+ ))
21
+ }
22
+
23
+ fn read_http_request_raw(stream: &mut std::net::TcpStream) -> String {
24
+ let mut raw_bytes = Vec::new();
25
+ let mut buffer = [0_u8; 1024];
26
+ let header_end = loop {
27
+ let read = stream.read(&mut buffer).expect("read request");
28
+ assert!(read > 0, "request closed before headers");
29
+ raw_bytes.extend_from_slice(&buffer[..read]);
30
+ if let Some(index) = raw_bytes.windows(4).position(|chunk| chunk == b"\r\n\r\n") {
31
+ break index + 4;
32
+ }
33
+ };
34
+ let head = String::from_utf8_lossy(&raw_bytes[..header_end]).to_string();
35
+ let content_length = head
36
+ .lines()
37
+ .find_map(|line| line.strip_prefix("Content-Length: "))
38
+ .and_then(|value| value.trim().parse::<usize>().ok())
39
+ .unwrap_or(0);
40
+ while raw_bytes.len() < header_end + content_length {
41
+ let read = stream.read(&mut buffer).expect("read body");
42
+ assert!(read > 0, "request closed before body");
43
+ raw_bytes.extend_from_slice(&buffer[..read]);
44
+ }
45
+ String::from_utf8_lossy(&raw_bytes).to_string()
46
+ }
47
+
48
+ fn fake_sse_text_response(text: &str) -> String {
49
+ format!(
50
+ "event: response.output_text.delta\ndata: {{\"type\":\"response.output_text.delta\",\"delta\":\"{text}\"}}\n\ndata: [DONE]\n\n"
51
+ )
52
+ }
53
+
54
+ fn real_private_once_request(
55
+ path: &str,
56
+ body: Value,
57
+ upstream_sse_body: String,
58
+ ) -> (String, String) {
59
+ let bin = env!("CARGO_BIN_EXE_omx-api");
60
+ let backend = TcpListener::bind(("127.0.0.1", 0)).expect("bind fake upstream");
61
+ let backend_port = backend.local_addr().expect("fake upstream addr").port();
62
+ let backend_handle = thread::spawn(move || {
63
+ let (mut stream, _) = backend.accept().expect("accept fake upstream request");
64
+ let raw = read_http_request_raw(&mut stream);
65
+ write!(
66
+ stream,
67
+ "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
68
+ upstream_sse_body.len(),
69
+ upstream_sse_body,
70
+ )
71
+ .expect("write fake upstream response");
72
+ raw
73
+ });
74
+
75
+ let state_file = temp_state_file("real-private-e2e");
76
+ let mut child = Command::new(bin)
77
+ .args([
78
+ "serve",
79
+ "--backend",
80
+ "real-private",
81
+ "--port",
82
+ "0",
83
+ "--once",
84
+ "--state-file",
85
+ state_file.to_str().unwrap(),
86
+ ])
87
+ .env("OMX_API_CODEX_OAUTH_TOKEN", "oauth-token-for-e2e")
88
+ .env("OMX_API_CODEX_ACCOUNT_ID", "account-e2e")
89
+ .env("OMX_API_CODEX_SESSION_ID", "session-e2e")
90
+ .env("OMX_API_CODEX_THREAD_ID", "thread-e2e")
91
+ .env(
92
+ "OMX_API_PRIVATE_BACKEND_URL",
93
+ format!("http://127.0.0.1:{backend_port}/backend-api/codex"),
94
+ )
95
+ .env(
96
+ "OMX_API_PRIVATE_IMAGE_BACKEND_URL",
97
+ format!("http://127.0.0.1:{backend_port}/backend-api/codex"),
98
+ )
99
+ .env("OMX_API_IMAGE_MODEL", "omx-private-image-test")
100
+ .stdout(Stdio::null())
101
+ .stderr(Stdio::piped())
102
+ .spawn()
103
+ .expect("spawn real-private omx-api serve");
104
+
105
+ let state = wait_for_daemon_state(&state_file, &mut child);
106
+ let token = state
107
+ .local_bearer_token_file
108
+ .as_ref()
109
+ .and_then(|path| std::fs::read_to_string(path).ok())
110
+ .expect("real-private serve should write a local bearer token");
111
+ let payload = serde_json::to_vec(&body).expect("request JSON");
112
+ let response = http_request_with_bearer(
113
+ &state.host,
114
+ state.port,
115
+ "POST",
116
+ path,
117
+ Some(&payload),
118
+ Some(token.trim()),
119
+ )
120
+ .unwrap();
121
+
122
+ let exit = child
123
+ .wait_timeout(Duration::from_secs(2))
124
+ .expect("wait for real-private child");
125
+ assert!(exit.is_some(), "real-private --once server did not exit");
126
+ let upstream_raw = backend_handle.join().expect("fake upstream thread");
127
+ (response, upstream_raw)
128
+ }
129
+
130
+ fn wait_for_daemon_state(
131
+ state_file: &std::path::Path,
132
+ child: &mut std::process::Child,
133
+ ) -> omx_api::DaemonState {
134
+ let deadline = Instant::now() + Duration::from_secs(5);
135
+ loop {
136
+ if let Some(state) = read_daemon_state(state_file).ok().flatten() {
137
+ return state;
138
+ }
139
+ if let Ok(Some(status)) = child.try_wait() {
140
+ let mut stderr = String::new();
141
+ if let Some(mut pipe) = child.stderr.take() {
142
+ use std::io::Read;
143
+ let _ = pipe.read_to_string(&mut stderr);
144
+ }
145
+ panic!("server exited before writing daemon state: status={status}; stderr={stderr}");
146
+ }
147
+ if Instant::now() >= deadline {
148
+ let _ = child.kill();
149
+ let _ = child.wait();
150
+ panic!("server did not write daemon state within 5s at {state_file:?}");
151
+ }
152
+ thread::sleep(Duration::from_millis(20));
153
+ }
154
+ }
155
+
156
+ #[test]
157
+ fn binary_system_dry_run_and_generate_emit_json() {
158
+ let bin = env!("CARGO_BIN_EXE_omx-api");
159
+ for action in ["dry-run", "generate"] {
160
+ let output = Command::new(bin)
161
+ .args(["system", action])
162
+ .output()
163
+ .expect("run omx-api system action");
164
+ assert!(
165
+ output.status.success(),
166
+ "stderr: {}",
167
+ String::from_utf8_lossy(&output.stderr)
168
+ );
169
+ let value: Value = serde_json::from_slice(&output.stdout).expect("json stdout");
170
+ assert_eq!(value["ok"], true);
171
+ assert_eq!(value["action"], format!("system.{action}"));
172
+ }
173
+ }
174
+
175
+ #[test]
176
+ fn binary_real_private_image_json_uses_fake_upstream_e2e() {
177
+ let (response, upstream_raw) = real_private_once_request(
178
+ "/v1/images/generations",
179
+ serde_json::json!({
180
+ "prompt": "image through upstream",
181
+ "size": "1024x1024"
182
+ }),
183
+ "event: image_generation.completed\ndata: {\"type\":\"image_generation.completed\",\"b64_json\":\"ZmFrZS1pbWFnZQ==\",\"revised_prompt\":\"image through upstream\"}\n\ndata: [DONE]\n\n".to_string(),
184
+ );
185
+
186
+ assert!(response.contains("200 OK"), "{response}");
187
+ let body = response.split("\r\n\r\n").nth(1).expect("response body");
188
+ let value: Value = serde_json::from_str(body).expect("image response JSON");
189
+ assert_eq!(value["backend"], "real-private");
190
+ assert_eq!(value["data"][0]["b64_json"], "ZmFrZS1pbWFnZQ==");
191
+ assert_eq!(value["data"][0]["revised_prompt"], "image through upstream");
192
+
193
+ assert!(upstream_raw.starts_with("POST /backend-api/codex/images/generations HTTP/1.1"));
194
+ assert!(upstream_raw.contains("Authorization: Bearer oauth-token-for-e2e\r\n"));
195
+ let forwarded_body = upstream_raw
196
+ .split("\r\n\r\n")
197
+ .nth(1)
198
+ .expect("upstream body");
199
+ let forwarded_json: Value = serde_json::from_str(forwarded_body).expect("upstream JSON");
200
+ assert_eq!(forwarded_json["model"], "omx-private-image-test");
201
+ assert_eq!(forwarded_json["prompt"], "image through upstream");
202
+ assert_eq!(forwarded_json["size"], "1024x1024");
203
+ }
204
+
205
+ #[test]
206
+ fn binary_real_private_responses_json_uses_fake_upstream_e2e() {
207
+ let (response, upstream_raw) = real_private_once_request(
208
+ "/v1/responses",
209
+ serde_json::json!({
210
+ "model": "gpt-5.3-codex",
211
+ "input": "hello from integration",
212
+ "reasoning": {"effort": "low"},
213
+ "instructions": "Use fake upstream."
214
+ }),
215
+ fake_sse_text_response("upstream-text-json"),
216
+ );
217
+
218
+ assert!(response.contains("200 OK"), "{response}");
219
+ let body = response.split("\r\n\r\n").nth(1).expect("response body");
220
+ let value: Value = serde_json::from_str(body).expect("response JSON");
221
+ assert_eq!(value["object"], "response");
222
+ assert_eq!(value["backend"], "real-private");
223
+ assert_eq!(value["output_text"], "upstream-text-json");
224
+ assert_eq!(
225
+ value["choices"][0]["message"]["content"],
226
+ "upstream-text-json"
227
+ );
228
+
229
+ assert!(upstream_raw.starts_with("POST /backend-api/codex/responses HTTP/1.1"));
230
+ assert!(upstream_raw.contains("Accept: text/event-stream\r\n"));
231
+ assert!(upstream_raw.contains("Authorization: Bearer oauth-token-for-e2e\r\n"));
232
+ assert!(upstream_raw.contains("ChatGPT-Account-ID: account-e2e\r\n"));
233
+ assert!(upstream_raw.contains("originator: codex_cli_rs\r\n"));
234
+ let forwarded_body = upstream_raw
235
+ .split("\r\n\r\n")
236
+ .nth(1)
237
+ .expect("upstream body");
238
+ let forwarded_json: Value = serde_json::from_str(forwarded_body).expect("upstream JSON");
239
+ assert_eq!(forwarded_json["model"], "gpt-5.3-codex");
240
+ assert_eq!(forwarded_json["stream"], true);
241
+ assert_eq!(
242
+ forwarded_json["reasoning"],
243
+ serde_json::json!({"effort": "low"})
244
+ );
245
+ assert_eq!(forwarded_json["instructions"], "Use fake upstream.");
246
+ assert_eq!(forwarded_json["prompt_cache_key"], "thread-e2e");
247
+ assert_eq!(
248
+ forwarded_json["input"][0]["content"][0]["text"],
249
+ "hello from integration"
250
+ );
251
+ }
252
+
253
+ #[test]
254
+ fn binary_real_private_responses_sse_uses_fake_upstream_e2e() {
255
+ let (response, upstream_raw) = real_private_once_request(
256
+ "/v1/responses",
257
+ serde_json::json!({
258
+ "model": "gpt-5.3-codex",
259
+ "input": "stream please",
260
+ "stream": true
261
+ }),
262
+ fake_sse_text_response("upstream-text-sse"),
263
+ );
264
+
265
+ assert!(response.contains("200 OK"), "{response}");
266
+ assert!(
267
+ response.contains("Content-Type: text/event-stream"),
268
+ "{response}"
269
+ );
270
+ assert!(response.contains("event: response.created"), "{response}");
271
+ assert!(
272
+ response.contains("event: response.output_text.delta"),
273
+ "{response}"
274
+ );
275
+ assert!(response.contains("upstream-text-sse"), "{response}");
276
+ assert!(response.contains("data: [DONE]"), "{response}");
277
+
278
+ let forwarded_body = upstream_raw
279
+ .split("\r\n\r\n")
280
+ .nth(1)
281
+ .expect("upstream body");
282
+ let forwarded_json: Value = serde_json::from_str(forwarded_body).expect("upstream JSON");
283
+ assert_eq!(forwarded_json["stream"], true);
284
+ assert_eq!(
285
+ forwarded_json["input"][0]["content"][0]["text"],
286
+ "stream please"
287
+ );
288
+ }
289
+
290
+ #[test]
291
+ fn binary_real_private_chat_sse_uses_chat_chunk_shape_with_fake_upstream_e2e() {
292
+ let (response, upstream_raw) = real_private_once_request(
293
+ "/v1/chat/completions",
294
+ serde_json::json!({
295
+ "model": "gpt-5.3-codex",
296
+ "messages": [{"role": "user", "content": "chat through upstream"}],
297
+ "stream": true
298
+ }),
299
+ fake_sse_text_response("chat-upstream-text"),
300
+ );
301
+
302
+ assert!(response.contains("200 OK"), "{response}");
303
+ assert!(
304
+ response.contains("Content-Type: text/event-stream"),
305
+ "{response}"
306
+ );
307
+ assert!(
308
+ response.contains("\"object\":\"chat.completion.chunk\""),
309
+ "{response}"
310
+ );
311
+ assert!(response.contains("chat-upstream-text"), "{response}");
312
+ assert!(response.contains("data: [DONE]"), "{response}");
313
+
314
+ let forwarded_body = upstream_raw
315
+ .split("\r\n\r\n")
316
+ .nth(1)
317
+ .expect("upstream body");
318
+ let forwarded_json: Value = serde_json::from_str(forwarded_body).expect("upstream JSON");
319
+ assert_eq!(forwarded_json["stream"], true);
320
+ assert_eq!(
321
+ forwarded_json["input"][0]["content"][0]["text"],
322
+ "chat through upstream"
323
+ );
324
+ }
325
+
326
+ #[test]
327
+ fn binary_serve_status_and_stop_work_together() {
328
+ let bin = env!("CARGO_BIN_EXE_omx-api");
329
+ let state_file = temp_state_file("serve-status-stop");
330
+ let mut child = Command::new(bin)
331
+ .args([
332
+ "serve",
333
+ "--port",
334
+ "0",
335
+ "--state-file",
336
+ state_file.to_str().unwrap(),
337
+ ])
338
+ .stdout(Stdio::null())
339
+ .stderr(Stdio::piped())
340
+ .spawn()
341
+ .expect("spawn omx-api serve");
342
+
343
+ let state = wait_for_daemon_state(&state_file, &mut child);
344
+
345
+ let status = Command::new(bin)
346
+ .args(["status", "--state-file", state_file.to_str().unwrap()])
347
+ .output()
348
+ .expect("run status");
349
+ assert!(status.status.success());
350
+ let status_json: Value = serde_json::from_slice(&status.stdout).unwrap();
351
+ assert_eq!(status_json["status"], "running");
352
+
353
+ let health = http_request(&state.host, state.port, "GET", "/health", None).unwrap();
354
+ assert!(health.contains("200 OK"));
355
+ assert!(health.contains("\"status\":\"ok\""));
356
+
357
+ let stop = Command::new(bin)
358
+ .args(["stop", "--state-file", state_file.to_str().unwrap()])
359
+ .output()
360
+ .expect("run stop");
361
+ assert!(
362
+ stop.status.success(),
363
+ "stderr: {}",
364
+ String::from_utf8_lossy(&stop.stderr)
365
+ );
366
+
367
+ let exit = child
368
+ .wait_timeout(Duration::from_secs(2))
369
+ .expect("wait for child");
370
+ if exit.is_none() {
371
+ let _ = child.kill();
372
+ panic!("server did not stop after stop command");
373
+ }
374
+ }
375
+
376
+ #[test]
377
+ fn binary_local_bearer_gate_rejects_missing_authorization() {
378
+ let bin = env!("CARGO_BIN_EXE_omx-api");
379
+ let state_file = temp_state_file("bearer-required");
380
+ let mut child = Command::new(bin)
381
+ .args([
382
+ "serve",
383
+ "--port",
384
+ "0",
385
+ "--once",
386
+ "--state-file",
387
+ state_file.to_str().unwrap(),
388
+ ])
389
+ .env("OMX_API_REQUIRE_LOCAL_BEARER", "1")
390
+ .stdout(Stdio::null())
391
+ .stderr(Stdio::piped())
392
+ .spawn()
393
+ .expect("spawn omx-api serve");
394
+
395
+ let state = wait_for_daemon_state(&state_file, &mut child);
396
+
397
+ let response = http_request(&state.host, state.port, "GET", "/v1/models", None).unwrap();
398
+ assert!(response.contains("401 Unauthorized"), "{response}");
399
+ assert!(
400
+ response.contains("local bearer token required"),
401
+ "{response}"
402
+ );
403
+
404
+ let exit = child
405
+ .wait_timeout(Duration::from_secs(2))
406
+ .expect("wait for child");
407
+ assert!(exit.is_some(), "server did not exit after --once request");
408
+ }
409
+
410
+ #[test]
411
+ fn binary_real_private_serve_generates_bearer_and_rejects_missing_authorization() {
412
+ let bin = env!("CARGO_BIN_EXE_omx-api");
413
+ let state_file = temp_state_file("real-private-bearer-default");
414
+ let mut child = Command::new(bin)
415
+ .args([
416
+ "serve",
417
+ "--backend",
418
+ "real-private",
419
+ "--port",
420
+ "0",
421
+ "--once",
422
+ "--state-file",
423
+ state_file.to_str().unwrap(),
424
+ ])
425
+ .env("OMX_API_REAL_PRIVATE_RESPONSE_TEXT", "fixture")
426
+ .stdout(Stdio::null())
427
+ .stderr(Stdio::piped())
428
+ .spawn()
429
+ .expect("spawn omx-api real-private serve");
430
+
431
+ let state = wait_for_daemon_state(&state_file, &mut child);
432
+ assert!(
433
+ state.local_bearer_token_file.is_some(),
434
+ "real-private direct serve should persist a bearer token file"
435
+ );
436
+
437
+ let response = http_request(&state.host, state.port, "GET", "/v1/models", None).unwrap();
438
+ assert!(response.contains("401 Unauthorized"), "{response}");
439
+ assert!(
440
+ response.contains("matching local bearer token required"),
441
+ "{response}"
442
+ );
443
+
444
+ let exit = child
445
+ .wait_timeout(Duration::from_secs(2))
446
+ .expect("wait for child");
447
+ assert!(exit.is_some(), "server did not exit after --once request");
448
+ }
449
+
450
+ #[test]
451
+ fn binary_daemon_token_is_not_printed_but_controls_generate_and_stop() {
452
+ let bin = env!("CARGO_BIN_EXE_omx-api");
453
+ let state_file = temp_state_file("daemon-token");
454
+ let start = Command::new(bin)
455
+ .args([
456
+ "serve",
457
+ "--port",
458
+ "0",
459
+ "--daemon",
460
+ "--state-file",
461
+ state_file.to_str().unwrap(),
462
+ ])
463
+ .output()
464
+ .expect("start daemon");
465
+ assert!(
466
+ start.status.success(),
467
+ "stderr: {}",
468
+ String::from_utf8_lossy(&start.stderr)
469
+ );
470
+ let stdout = String::from_utf8_lossy(&start.stdout);
471
+ assert!(!stdout.contains("local_bearer_token\":"), "{stdout}");
472
+
473
+ let state = read_daemon_state(&state_file).unwrap().unwrap();
474
+ assert!(state.local_bearer_token.is_none());
475
+ let token_file = state
476
+ .local_bearer_token_file
477
+ .clone()
478
+ .expect("token file path");
479
+ let token = std::fs::read_to_string(&token_file).expect("token file");
480
+ assert!(!stdout.contains(token.trim()), "daemon stdout leaked token");
481
+
482
+ let unauthorized = http_request(&state.host, state.port, "GET", "/v1/models", None).unwrap();
483
+ assert!(unauthorized.contains("401 Unauthorized"), "{unauthorized}");
484
+ let authorized = http_request_with_bearer(
485
+ &state.host,
486
+ state.port,
487
+ "GET",
488
+ "/v1/models",
489
+ None,
490
+ Some(token.trim()),
491
+ )
492
+ .unwrap();
493
+ assert!(authorized.contains("200 OK"), "{authorized}");
494
+
495
+ let generated = Command::new(bin)
496
+ .args([
497
+ "generate",
498
+ "text",
499
+ "hello",
500
+ "--state-file",
501
+ state_file.to_str().unwrap(),
502
+ ])
503
+ .output()
504
+ .expect("generate through daemon");
505
+ assert!(
506
+ generated.status.success(),
507
+ "stderr: {}",
508
+ String::from_utf8_lossy(&generated.stderr)
509
+ );
510
+ assert!(String::from_utf8_lossy(&generated.stdout).contains("omx mock response"));
511
+
512
+ let stop = Command::new(bin)
513
+ .args(["stop", "--state-file", state_file.to_str().unwrap()])
514
+ .output()
515
+ .expect("stop daemon");
516
+ assert!(
517
+ stop.status.success(),
518
+ "stderr: {}",
519
+ String::from_utf8_lossy(&stop.stderr)
520
+ );
521
+ assert!(!token_file.exists(), "token file should be removed on stop");
522
+ }
523
+
524
+ #[test]
525
+ fn binary_rejects_non_loopback_host() {
526
+ let bin = env!("CARGO_BIN_EXE_omx-api");
527
+ let output = Command::new(bin)
528
+ .args(["serve", "--host", "0.0.0.0", "--once"])
529
+ .output()
530
+ .expect("run omx-api serve");
531
+ assert!(!output.status.success());
532
+ assert!(String::from_utf8_lossy(&output.stderr).contains("localhost-only"));
533
+ }
534
+
535
+ trait WaitTimeout {
536
+ fn wait_timeout(
537
+ &mut self,
538
+ timeout: Duration,
539
+ ) -> std::io::Result<Option<std::process::ExitStatus>>;
540
+ }
541
+
542
+ impl WaitTimeout for std::process::Child {
543
+ fn wait_timeout(
544
+ &mut self,
545
+ timeout: Duration,
546
+ ) -> std::io::Result<Option<std::process::ExitStatus>> {
547
+ let start = std::time::Instant::now();
548
+ loop {
549
+ if let Some(status) = self.try_wait()? {
550
+ return Ok(Some(status));
551
+ }
552
+ if start.elapsed() >= timeout {
553
+ return Ok(None);
554
+ }
555
+ thread::sleep(Duration::from_millis(20));
556
+ }
557
+ }
558
+ }
@@ -2721,6 +2721,10 @@ sleep 30
2721
2721
  let TimedCommandOutput::TimedOut { .. } = result else {
2722
2722
  panic!("expected timeout");
2723
2723
  };
2724
+ let deadline = Instant::now() + Duration::from_secs(2);
2725
+ while !term_file.exists() && Instant::now() < deadline {
2726
+ std::thread::sleep(Duration::from_millis(10));
2727
+ }
2724
2728
  assert_eq!(read_to_string(&term_file).unwrap_or_default(), "term");
2725
2729
  }
2726
2730