oh-my-agent-commandcode 0.0.0-stage → 20.38.3

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 (334) hide show
  1. package/README.md +35 -2
  2. package/filter-test-output.sh +52 -0
  3. package/mods/oma.ts +347 -0
  4. package/package.json +30 -4
  5. package/skills/_shared/conditional/experiment-ledger.md +28 -0
  6. package/skills/_shared/conditional/exploration-loop.md +22 -0
  7. package/skills/_shared/conditional/quality-score.md +33 -0
  8. package/skills/_shared/core/anti-ai-prose.md +191 -0
  9. package/skills/_shared/core/api-contracts/README.md +28 -0
  10. package/skills/_shared/core/api-contracts/template.md +90 -0
  11. package/skills/_shared/core/clarification-protocol.md +23 -0
  12. package/skills/_shared/core/code-intelligence.md +46 -0
  13. package/skills/_shared/core/collaboration-policy.md +28 -0
  14. package/skills/_shared/core/common-checklist.md +28 -0
  15. package/skills/_shared/core/context-budget.md +22 -0
  16. package/skills/_shared/core/context-loading.md +34 -0
  17. package/skills/_shared/core/difficulty-guide.md +26 -0
  18. package/skills/_shared/core/execution-policy.md +31 -0
  19. package/skills/_shared/core/lessons-learned.md +33 -0
  20. package/skills/_shared/core/prompt-structure.md +18 -0
  21. package/skills/_shared/core/quality-principles.md +11 -0
  22. package/skills/_shared/core/session-metrics.md +35 -0
  23. package/skills/_shared/core/skill-routing.md +39 -0
  24. package/skills/_shared/core/test-approach.md +54 -0
  25. package/skills/_shared/core/vendor-detection.md +52 -0
  26. package/skills/_shared/runtime/memory-protocol.md +118 -0
  27. package/skills/_shared/runtime/result-contract.md +64 -0
  28. package/skills/_shared/ui-interaction-quality.md +133 -0
  29. package/skills/oma-academic-writing/SKILL.md +181 -0
  30. package/skills/oma-academic-writing/resources/abstract-guide.md +51 -0
  31. package/skills/oma-academic-writing/resources/academic-verb-tiers.md +42 -0
  32. package/skills/oma-academic-writing/resources/anti-ai-checklist.md +52 -0
  33. package/skills/oma-academic-writing/resources/hedging-guide.md +132 -0
  34. package/skills/oma-academic-writing/resources/sentence-structure-reference.md +151 -0
  35. package/skills/oma-architecture/SKILL.md +203 -0
  36. package/skills/oma-architecture/resources/api-evolution.md +41 -0
  37. package/skills/oma-architecture/resources/checklist.md +42 -0
  38. package/skills/oma-architecture/resources/execution-protocol.md +115 -0
  39. package/skills/oma-architecture/resources/methodology-selection.md +92 -0
  40. package/skills/oma-architecture/resources/migration-patterns.md +28 -0
  41. package/skills/oma-architecture/resources/output-templates.md +111 -0
  42. package/skills/oma-architecture/resources/stakeholder-synthesis.md +80 -0
  43. package/skills/oma-backend/SKILL.md +202 -0
  44. package/skills/oma-backend/resources/checklist.md +45 -0
  45. package/skills/oma-backend/resources/error-playbook.md +97 -0
  46. package/skills/oma-backend/resources/execution-protocol.md +39 -0
  47. package/skills/oma-backend/resources/orm-reference.md +61 -0
  48. package/skills/oma-backend/variants/node/api-template.ts +315 -0
  49. package/skills/oma-backend/variants/node/snippets.md +475 -0
  50. package/skills/oma-backend/variants/node/stack.yaml +21 -0
  51. package/skills/oma-backend/variants/node/tech-stack.md +33 -0
  52. package/skills/oma-backend/variants/python/api-template.py +378 -0
  53. package/skills/oma-backend/variants/python/snippets.md +199 -0
  54. package/skills/oma-backend/variants/python/stack.yaml +18 -0
  55. package/skills/oma-backend/variants/python/tech-stack.md +45 -0
  56. package/skills/oma-backend/variants/rust/api-template.rs +635 -0
  57. package/skills/oma-backend/variants/rust/snippets.md +471 -0
  58. package/skills/oma-backend/variants/rust/stack.yaml +20 -0
  59. package/skills/oma-backend/variants/rust/tech-stack.md +36 -0
  60. package/skills/oma-backend/variants/stack.schema.json +73 -0
  61. package/skills/oma-brainstorm/SKILL.md +166 -0
  62. package/skills/oma-brainstorm/resources/triz-lite.md +117 -0
  63. package/skills/oma-coordination/SKILL.md +171 -0
  64. package/skills/oma-db/SKILL.md +201 -0
  65. package/skills/oma-db/resources/anti-patterns.md +177 -0
  66. package/skills/oma-db/resources/checklist.md +67 -0
  67. package/skills/oma-db/resources/document-templates.md +107 -0
  68. package/skills/oma-db/resources/error-playbook.md +50 -0
  69. package/skills/oma-db/resources/examples.md +170 -0
  70. package/skills/oma-db/resources/execution-protocol.md +127 -0
  71. package/skills/oma-db/resources/iso-controls.md +98 -0
  72. package/skills/oma-db/resources/migration-playbook.md +93 -0
  73. package/skills/oma-db/resources/query-tuning.md +57 -0
  74. package/skills/oma-db/resources/vector-db.md +159 -0
  75. package/skills/oma-debug/SKILL.md +146 -0
  76. package/skills/oma-debug/resources/bug-report-template.md +327 -0
  77. package/skills/oma-debug/resources/checklist.md +31 -0
  78. package/skills/oma-debug/resources/common-patterns.md +734 -0
  79. package/skills/oma-debug/resources/debugging-checklist.md +364 -0
  80. package/skills/oma-debug/resources/error-playbook.md +94 -0
  81. package/skills/oma-debug/resources/execution-protocol.md +40 -0
  82. package/skills/oma-design/SKILL.md +135 -0
  83. package/skills/oma-design/resources/asset-strategy.md +34 -0
  84. package/skills/oma-design/resources/checklist.md +63 -0
  85. package/skills/oma-design/resources/data-grid.md +48 -0
  86. package/skills/oma-design/resources/design-md-spec.md +107 -0
  87. package/skills/oma-design/resources/design-tokens.md +53 -0
  88. package/skills/oma-design/resources/error-playbook.md +18 -0
  89. package/skills/oma-design/resources/execution-protocol.md +95 -0
  90. package/skills/oma-design/resources/external-design-skills.md +130 -0
  91. package/skills/oma-design/resources/getdesign-fetcher.md +104 -0
  92. package/skills/oma-design/resources/product-interface-review.md +59 -0
  93. package/skills/oma-design/resources/redesign-protocol.md +52 -0
  94. package/skills/oma-design/resources/stitch-integration.md +244 -0
  95. package/skills/oma-dev-workflow/SKILL.md +375 -0
  96. package/skills/oma-dev-workflow/resources/affected-checks.py +58 -0
  97. package/skills/oma-dev-workflow/resources/api-workflows.md +58 -0
  98. package/skills/oma-dev-workflow/resources/database-patterns.md +107 -0
  99. package/skills/oma-dev-workflow/resources/i18n-patterns.md +63 -0
  100. package/skills/oma-dev-workflow/resources/release-coordination.md +240 -0
  101. package/skills/oma-dev-workflow/resources/tooling-profiles.md +217 -0
  102. package/skills/oma-dev-workflow/resources/troubleshooting.md +78 -0
  103. package/skills/oma-dev-workflow/resources/validation-pipeline.md +246 -0
  104. package/skills/oma-docs/SKILL.md +77 -0
  105. package/skills/oma-docs/resources/commands.md +76 -0
  106. package/skills/oma-explanation/SKILL.md +212 -0
  107. package/skills/oma-explanation/resources/document-structure.md +115 -0
  108. package/skills/oma-explanation/resources/draft-format.md +199 -0
  109. package/skills/oma-explanation/resources/html-contract.md +102 -0
  110. package/skills/oma-frontend/SKILL.md +227 -0
  111. package/skills/oma-frontend/resources/angular-rules.md +110 -0
  112. package/skills/oma-frontend/resources/checklist.md +76 -0
  113. package/skills/oma-frontend/resources/error-playbook.md +125 -0
  114. package/skills/oma-frontend/resources/execution-protocol.md +46 -0
  115. package/skills/oma-frontend/resources/snippets.md +354 -0
  116. package/skills/oma-frontend/resources/specialized-visual-implementation.md +193 -0
  117. package/skills/oma-frontend/resources/tailwind-rules.md +83 -0
  118. package/skills/oma-frontend/resources/tech-stack.md +156 -0
  119. package/skills/oma-hwp/SKILL.md +166 -0
  120. package/skills/oma-hwp/config/hwp-config.yaml +35 -0
  121. package/skills/oma-hwp/resources/balanced-tables.ts +39 -0
  122. package/skills/oma-hwp/resources/execution-protocol.md +145 -0
  123. package/skills/oma-hwp/resources/flatten-tables.ts +101 -0
  124. package/skills/oma-hwp/resources/package.json +13 -0
  125. package/skills/oma-hwp/resources/troubleshooting.md +70 -0
  126. package/skills/oma-hwp/resources/tsconfig.json +10 -0
  127. package/skills/oma-hwp/resources/turndown-plugin-gfm.d.ts +9 -0
  128. package/skills/oma-image/SKILL.md +142 -0
  129. package/skills/oma-image/config/image-config.yaml +70 -0
  130. package/skills/oma-image/resources/checklist.md +18 -0
  131. package/skills/oma-image/resources/execution-protocol.md +117 -0
  132. package/skills/oma-image/resources/invocation.md +105 -0
  133. package/skills/oma-image/resources/prompt-tips.md +71 -0
  134. package/skills/oma-image/resources/vendor-matrix.md +90 -0
  135. package/skills/oma-market/SKILL.md +142 -0
  136. package/skills/oma-market/resources/checklist.md +12 -0
  137. package/skills/oma-market/resources/error-playbook.md +12 -0
  138. package/skills/oma-market/resources/execution-protocol.md +60 -0
  139. package/skills/oma-market/resources/frameworks/pestel.md +101 -0
  140. package/skills/oma-market/resources/frameworks/porters-5f.md +90 -0
  141. package/skills/oma-market/resources/frameworks/swot.md +61 -0
  142. package/skills/oma-market/resources/intent-rules.md +35 -0
  143. package/skills/oma-market/resources/output-laws.md +53 -0
  144. package/skills/oma-mobile/SKILL.md +168 -0
  145. package/skills/oma-mobile/resources/checklist.md +77 -0
  146. package/skills/oma-mobile/resources/error-playbook.md +198 -0
  147. package/skills/oma-mobile/resources/execution-protocol.md +47 -0
  148. package/skills/oma-mobile/resources/screen-template.dart +331 -0
  149. package/skills/oma-mobile/resources/screen-template.swift +239 -0
  150. package/skills/oma-mobile/resources/screen-template.tsx +179 -0
  151. package/skills/oma-mobile/resources/tech-stack.md +69 -0
  152. package/skills/oma-mobile/variants/README.md +45 -0
  153. package/skills/oma-mobile/variants/flutter/api-template.dart +461 -0
  154. package/skills/oma-mobile/variants/flutter/snippets.md +1353 -0
  155. package/skills/oma-mobile/variants/flutter/stack.yaml +21 -0
  156. package/skills/oma-mobile/variants/flutter/tech-stack.md +201 -0
  157. package/skills/oma-mobile/variants/kotlin-android/api-template.kt +182 -0
  158. package/skills/oma-mobile/variants/kotlin-android/snippets.md +871 -0
  159. package/skills/oma-mobile/variants/kotlin-android/stack.yaml +32 -0
  160. package/skills/oma-mobile/variants/kotlin-android/tech-stack.md +159 -0
  161. package/skills/oma-mobile/variants/react-native/api-template.ts +531 -0
  162. package/skills/oma-mobile/variants/react-native/snippets.md +1014 -0
  163. package/skills/oma-mobile/variants/react-native/stack.yaml +22 -0
  164. package/skills/oma-mobile/variants/react-native/tech-stack.md +192 -0
  165. package/skills/oma-mobile/variants/stack.schema.json +68 -0
  166. package/skills/oma-mobile/variants/swift-ios/api-template.swift +165 -0
  167. package/skills/oma-mobile/variants/swift-ios/snippets.md +1028 -0
  168. package/skills/oma-mobile/variants/swift-ios/stack.yaml +25 -0
  169. package/skills/oma-mobile/variants/swift-ios/tech-stack.md +174 -0
  170. package/skills/oma-observability/SKILL.md +225 -0
  171. package/skills/oma-observability/resources/anti-patterns.md +555 -0
  172. package/skills/oma-observability/resources/boundaries/cross-application.md +348 -0
  173. package/skills/oma-observability/resources/boundaries/multi-tenant.md +290 -0
  174. package/skills/oma-observability/resources/boundaries/release.md +372 -0
  175. package/skills/oma-observability/resources/boundaries/slo.md +225 -0
  176. package/skills/oma-observability/resources/checklist.md +123 -0
  177. package/skills/oma-observability/resources/examples.md +448 -0
  178. package/skills/oma-observability/resources/execution-protocol.md +166 -0
  179. package/skills/oma-observability/resources/incident-forensics.md +348 -0
  180. package/skills/oma-observability/resources/intent-rules.md +307 -0
  181. package/skills/oma-observability/resources/layers/L3-network.md +281 -0
  182. package/skills/oma-observability/resources/layers/L4-transport.md +299 -0
  183. package/skills/oma-observability/resources/layers/L7-application/crash-analytics.md +359 -0
  184. package/skills/oma-observability/resources/layers/L7-application/mobile-rum.md +278 -0
  185. package/skills/oma-observability/resources/layers/L7-application/waf.md +246 -0
  186. package/skills/oma-observability/resources/layers/L7-application/web-rum.md +350 -0
  187. package/skills/oma-observability/resources/layers/mesh.md +368 -0
  188. package/skills/oma-observability/resources/matrix.md +177 -0
  189. package/skills/oma-observability/resources/meta-observability.md +605 -0
  190. package/skills/oma-observability/resources/observability-as-code.md +666 -0
  191. package/skills/oma-observability/resources/signals/audit.md +310 -0
  192. package/skills/oma-observability/resources/signals/cost.md +303 -0
  193. package/skills/oma-observability/resources/signals/logs.md +263 -0
  194. package/skills/oma-observability/resources/signals/metrics.md +478 -0
  195. package/skills/oma-observability/resources/signals/privacy.md +351 -0
  196. package/skills/oma-observability/resources/signals/profiles.md +245 -0
  197. package/skills/oma-observability/resources/signals/traces.md +360 -0
  198. package/skills/oma-observability/resources/standards.md +214 -0
  199. package/skills/oma-observability/resources/transport/collector-topology.md +308 -0
  200. package/skills/oma-observability/resources/transport/otlp-grpc-vs-http.md +256 -0
  201. package/skills/oma-observability/resources/transport/sampling-recipes.md +297 -0
  202. package/skills/oma-observability/resources/transport/udp-statsd-mtu.md +167 -0
  203. package/skills/oma-observability/resources/vendor-categories.md +352 -0
  204. package/skills/oma-orchestration/SKILL.md +302 -0
  205. package/skills/oma-orchestration/config/cli-config.yaml +92 -0
  206. package/skills/oma-orchestration/resources/memory-schema.md +232 -0
  207. package/skills/oma-orchestration/resources/subagent-prompt-template.md +30 -0
  208. package/skills/oma-orchestration/scripts/parallel-run.sh +6 -0
  209. package/skills/oma-orchestration/scripts/spawn-agent.sh +5 -0
  210. package/skills/oma-orchestration/scripts/verify.sh +20 -0
  211. package/skills/oma-orchestration/templates/backend-task.md +19 -0
  212. package/skills/oma-orchestration/templates/debug-task.md +16 -0
  213. package/skills/oma-orchestration/templates/frontend-task.md +18 -0
  214. package/skills/oma-orchestration/templates/mobile-task.md +18 -0
  215. package/skills/oma-orchestration/templates/qa-task.md +16 -0
  216. package/skills/oma-orchestration/templates/tasks-example.yaml +15 -0
  217. package/skills/oma-pdf/SKILL.md +162 -0
  218. package/skills/oma-pdf/config/pdf-config.yaml +27 -0
  219. package/skills/oma-pdf/resources/execution-protocol.md +127 -0
  220. package/skills/oma-pm/SKILL.md +157 -0
  221. package/skills/oma-pm/resources/error-playbook.md +75 -0
  222. package/skills/oma-pm/resources/examples.md +439 -0
  223. package/skills/oma-pm/resources/execution-protocol.md +62 -0
  224. package/skills/oma-pm/resources/iso-planning.md +110 -0
  225. package/skills/oma-pm/resources/plan-phase-protocol.md +66 -0
  226. package/skills/oma-pm/resources/task-template.json +86 -0
  227. package/skills/oma-qa/SKILL.md +141 -0
  228. package/skills/oma-qa/resources/checklist.md +339 -0
  229. package/skills/oma-qa/resources/error-playbook.md +98 -0
  230. package/skills/oma-qa/resources/execution-protocol.md +169 -0
  231. package/skills/oma-qa/resources/iso-quality.md +82 -0
  232. package/skills/oma-qa/resources/verify-ship-protocol.md +116 -0
  233. package/skills/oma-recap/SKILL.md +245 -0
  234. package/skills/oma-recap/resources/output-formats.md +83 -0
  235. package/skills/oma-refactor/SKILL.md +184 -0
  236. package/skills/oma-refactor/resources/definition.md +108 -0
  237. package/skills/oma-refactor/resources/governance.md +33 -0
  238. package/skills/oma-refactor/resources/hotspots.py +44 -0
  239. package/skills/oma-refactor/resources/measurement.md +93 -0
  240. package/skills/oma-scholar/SKILL.md +276 -0
  241. package/skills/oma-scholar/config/scholar-config.yaml +79 -0
  242. package/skills/oma-scholar/resources/api-endpoints.md +174 -0
  243. package/skills/oma-scholar/resources/checklist.md +92 -0
  244. package/skills/oma-scholar/resources/execution-protocol.md +224 -0
  245. package/skills/oma-scholar/resources/fallback-providers.md +143 -0
  246. package/skills/oma-scholar/resources/setup-openalex.md +85 -0
  247. package/skills/oma-scholar/resources/sidecar-spec.md +341 -0
  248. package/skills/oma-scholar/resources/upstream-spec-cache.md +972 -0
  249. package/skills/oma-scm/SKILL.md +103 -0
  250. package/skills/oma-scm/resources/cm-operations.md +56 -0
  251. package/skills/oma-scm/resources/codeowners-playbook.md +45 -0
  252. package/skills/oma-scm/resources/conventional-commits.md +108 -0
  253. package/skills/oma-scm/resources/merge-risk.md +76 -0
  254. package/skills/oma-scm/resources/onboarding-risk-signals.md +71 -0
  255. package/skills/oma-search/SKILL.md +125 -0
  256. package/skills/oma-search/resources/checklist.md +36 -0
  257. package/skills/oma-search/resources/error-playbook.md +66 -0
  258. package/skills/oma-search/resources/examples.md +80 -0
  259. package/skills/oma-search/resources/execution-protocol.md +86 -0
  260. package/skills/oma-search/resources/intent-rules.md +47 -0
  261. package/skills/oma-search/resources/trust-registry.md +53 -0
  262. package/skills/oma-security/SKILL.md +172 -0
  263. package/skills/oma-security/resources/artex.md +108 -0
  264. package/skills/oma-security/resources/ci.md +134 -0
  265. package/skills/oma-security/resources/cisco-source.md +108 -0
  266. package/skills/oma-security/resources/deepsec-config.md +45 -0
  267. package/skills/oma-security/resources/deepsec-matchers.md +35 -0
  268. package/skills/oma-security/resources/deepsec-scanning.md +45 -0
  269. package/skills/oma-security/resources/deepsec-setup.md +41 -0
  270. package/skills/oma-security/resources/findings-contract.md +408 -0
  271. package/skills/oma-security/resources/mcp-scanning.md +122 -0
  272. package/skills/oma-security/resources/skill-scanning.md +106 -0
  273. package/skills/oma-security/resources/validation.md +71 -0
  274. package/skills/oma-skill-creation/SKILL.md +84 -0
  275. package/skills/oma-skill-creation/resources/prompt-evaluation.md +36 -0
  276. package/skills/oma-skill-creation/resources/ssl-lite-template.md +108 -0
  277. package/skills/oma-skill-creation/resources/validation-checklist.md +101 -0
  278. package/skills/oma-slide/SKILL.md +277 -0
  279. package/skills/oma-slide/resources/animation-patterns.md +245 -0
  280. package/skills/oma-slide/resources/assets/deck-stage.js +514 -0
  281. package/skills/oma-slide/resources/assets/viewport-base.css +257 -0
  282. package/skills/oma-slide/resources/canva-integration.md +203 -0
  283. package/skills/oma-slide/resources/checklist.md +46 -0
  284. package/skills/oma-slide/resources/design-doctrine.md +260 -0
  285. package/skills/oma-slide/resources/fixed-stage.md +232 -0
  286. package/skills/oma-slide/resources/generation-protocol.md +393 -0
  287. package/skills/oma-slide/resources/native-pptx.md +84 -0
  288. package/skills/oma-slide/resources/selection-index.json +461 -0
  289. package/skills/oma-slide/resources/style-presets.LICENSE +10 -0
  290. package/skills/oma-slide/resources/style-presets.md +520 -0
  291. package/skills/oma-tf-infra/SKILL.md +201 -0
  292. package/skills/oma-tf-infra/resources/checklist.md +53 -0
  293. package/skills/oma-tf-infra/resources/cost-optimization.md +574 -0
  294. package/skills/oma-tf-infra/resources/error-playbook.md +68 -0
  295. package/skills/oma-tf-infra/resources/examples.md +38 -0
  296. package/skills/oma-tf-infra/resources/execution-protocol.md +129 -0
  297. package/skills/oma-tf-infra/resources/iso-42001-infra.md +229 -0
  298. package/skills/oma-tf-infra/resources/multi-cloud-examples.md +241 -0
  299. package/skills/oma-tf-infra/resources/policy-testing-examples.md +332 -0
  300. package/skills/oma-translation/SKILL.md +229 -0
  301. package/skills/oma-translation/resources/anti-ai-patterns.md +104 -0
  302. package/skills/oma-translation/resources/lang/_template.md +73 -0
  303. package/skills/oma-translation/resources/lang/en.md +123 -0
  304. package/skills/oma-translation/resources/lang/ja.md +274 -0
  305. package/skills/oma-translation/resources/lang/ko.md +228 -0
  306. package/skills/oma-translation/resources/lang/zh.md +285 -0
  307. package/skills/oma-translation/resources/style-skills.md +104 -0
  308. package/skills/oma-translation/resources/translation-rubric.md +126 -0
  309. package/skills/oma-video/SKILL.md +229 -0
  310. package/skills/oma-video/config/video-config.yaml +60 -0
  311. package/skills/oma-video/resources/checklist.md +26 -0
  312. package/skills/oma-video/resources/execution-protocol.md +122 -0
  313. package/skills/oma-video/resources/existing-media.md +185 -0
  314. package/skills/oma-video/resources/hyperframes-authoring/README.md +44 -0
  315. package/skills/oma-video/resources/hyperframes-authoring/demo.md +31 -0
  316. package/skills/oma-video/resources/hyperframes-authoring/explainer.md +10 -0
  317. package/skills/oma-video/resources/hyperframes-authoring/shorts.md +10 -0
  318. package/skills/oma-video/resources/motion-principles.md +81 -0
  319. package/skills/oma-video/resources/mpt/driver.py +301 -0
  320. package/skills/oma-video/resources/prompt-tips.md +75 -0
  321. package/skills/oma-video/resources/script-schema.md +81 -0
  322. package/skills/oma-video/resources/sound-design.md +127 -0
  323. package/skills/oma-video/resources/strudel/package.json +11 -0
  324. package/skills/oma-video/resources/strudel/page.html +87 -0
  325. package/skills/oma-video/resources/strudel/render.mjs +295 -0
  326. package/skills/oma-video/resources/vendor-matrix.md +124 -0
  327. package/skills/oma-voice/SKILL.md +249 -0
  328. package/skills/oma-voice/config/voice-config.yaml +30 -0
  329. package/skills/oma-voice/resources/audio-processing.md +119 -0
  330. package/skills/oma-voice/resources/checklist.md +47 -0
  331. package/skills/oma-voice/resources/execution-protocol.md +215 -0
  332. package/skills/oma-voice/resources/prompt-tips.md +43 -0
  333. package/skills/oma-voice/resources/voice-matrix.md +51 -0
  334. package/skills/oma-voice/resources/voicestudio.md +87 -0
@@ -0,0 +1,75 @@
1
+ # Prompt Tips
2
+
3
+ A good video brief specifies the **mode**, the **topic/source**, and the
4
+ **arc** (what the viewer should feel/learn). The brief drives the script; the
5
+ script's per-scene `visual.prompt` then drives oma-image (English prompts —
6
+ image models are trained predominantly on English captions).
7
+
8
+ ## Brief structure
9
+
10
+ ```
11
+ Mode + audience → Topic/source → Arc (hook → body → payoff) → Tone/pacing
12
+ ```
13
+
14
+ Example (shorts): `30s vertical short for a dev audience: how oma-video turns a topic into a finished clip; hook with the pain, show the 3-step flow, end on the one-command CTA; upbeat, fast cuts`
15
+
16
+ ## Per-mode guidance
17
+
18
+ ### shorts (9:16)
19
+ - **Hook in the first 1.5s** — the first scene's on-screen text must earn the swipe.
20
+ - Keep scenes 2–4s; favor 6–10 scenes for a 30s clip (≤ `max_scenes` 40).
21
+ - oma-image stills stay static (no Ken Burns) — write prompts that frame one clear subject.
22
+ - TikTok captions are centered, static windowed cues (one SRT cue on screen at a time, CSS-wrapped — no per-word animation); keep narration lines short (≤ ~8 words/segment) so cues switch cleanly.
23
+
24
+ ### explainer (16:9 / 9:16)
25
+ - Source is a README / code / data — point the brief at the file and the *one* thing to teach.
26
+ - Mix oma-slide frames (structure, bullet beats) with oma-image diagrams (concepts) and code frames.
27
+ - Code frames use one fixed deterministic theme (v1). Keep snippets short and legible at 1920×1080.
28
+ - Narration should explain *why*, not read the code line-by-line.
29
+ - One diagram per scene, about 15–20s each; see `motion-principles.md` for the scene cast and diagram steps.
30
+
31
+ ### demo (16:9)
32
+ - The visual is a human-recorded capture (`--capture`). The brief drives the zoom/callout beats and outro.
33
+ - Call out *where to look* on screen — HyperFrames adds zoom + callouts over the capture.
34
+ - Open on the product; add a title card only when the user asks for one.
35
+
36
+ ## Per-scene image prompts (oma-image)
37
+
38
+ The script's `scenes[].visual.prompt` is forwarded to oma-image. Write them with
39
+ the same structure oma-image expects:
40
+
41
+ ```
42
+ Scene/backdrop → Subject → Details → Constraints
43
+ ```
44
+
45
+ | Mode | Example per-scene prompt |
46
+ |------|--------------------------|
47
+ | shorts | `Aerial drone shot of Jeju coastline, turquoise water meeting volcanic rock, golden hour, vertical composition with headroom for a slow zoom` |
48
+ | shorts | `Close-up of a hand pouring espresso into a glass over ice, warm cafe light, shallow depth of field` |
49
+ | explainer | `Clean isometric diagram of a 3-stage pipeline (script → assets → render), flat vector, labeled boxes, neutral background` |
50
+ | demo | `Minimal title card: product name centered, dark UI background, subtle accent gradient, 16:9` |
51
+
52
+ ## Do's
53
+
54
+ - Anchor the **arc**: hook → body → payoff. A short without a hook gets swiped past.
55
+ - Match **aspect to mode** (9:16 shorts, 16:9 explainer/demo) or use `auto`.
56
+ - Keep narration **per-scene and short** so caption pages and scene boundaries align.
57
+ - Pick **music** that matches pacing (`upbeat` for shorts, `calm` for explainer) — Strudel renders the bed offline and mixes it at −18 dB under narration. Needs a one-time `oma video doctor --install-strudel`; without it the run still succeeds, just silent.
58
+
59
+ ## Don'ts
60
+
61
+ - Don't write a brief with no mode and no topic — the agent will have to clarify.
62
+ - Don't request paid stock/AIGC visuals without the key — the chain silently falls back to oma-image stills (annotated in `warnings`), which may surprise you.
63
+ - Don't exceed `max_duration_sec` (180) or `max_scenes` (40).
64
+ - Don't put secrets in the brief unless `--no-brief-in-manifest` is set.
65
+
66
+ ## Localization
67
+
68
+ - Narration + on-screen text are authored in `--locale`. For a non-source locale, oma-translation translates the script text before TTS/captions (key-free). If the translator is absent, the source text is kept and a warning is recorded.
69
+ - oma-image prompts are sent in **English** regardless of `--locale`; translate the user's subject and show the translated prompt during amplification.
70
+
71
+ ## Determinism
72
+
73
+ - `--seed <n>` makes the render reproducible. The same `render-spec.json` + assets + seed + embedded Pretendard font produce a byte-stable render. The font is fetched once by `oma video doctor --install`; if it is missing (e.g. the fetch ran offline), the render gracefully falls back to system fonts and byte-identical output is only guaranteed once the font is present.
74
+ - Re-render an existing run with `oma video render <runDir>` — it consumes `render-spec.json` only, so script/voice/visual generation is not re-run.
75
+ - `OMA_VIDEO_MOCK=1` replays golden fixtures for deterministic tests.
@@ -0,0 +1,81 @@
1
+ # script.json Schema — agent-as-key authoring reference
2
+
3
+ `oma video generate --script <path>` hard-validates the file against this
4
+ schema (mirrors `cli/commands/video/types.ts` `ScriptSchema`). A mismatch is
5
+ exit 4 (invalid-input) with the offending field named. **`schemaVersion` is
6
+ required** — omitting it is the most common authoring failure.
7
+
8
+ ## Fields
9
+
10
+ | Field | Type | Required | Notes |
11
+ |-------|------|:---:|-------|
12
+ | `schemaVersion` | `"1.0"` (literal) | ✅ | exactly the string `"1.0"` |
13
+ | `mode` | `shorts` \| `explainer` \| `demo` | ✅ | must match the run's `--mode` |
14
+ | `aspect` | `9:16` \| `16:9` \| `1:1` | ✅ | concrete value — `auto` is a CLI flag, not a script value |
15
+ | `locale` | string (min 1) | ✅ | narration/caption language tag, e.g. `en`, `ko` |
16
+ | `title` | string (min 1) | ✅ | also drives the output filename slug: `<mode>-<slug>.mp4` |
17
+ | `scenes` | array (min 1, ≤ 40) | ✅ | see per-scene fields below |
18
+ | `music` | `upbeat` \| `calm` \| `cinematic` \| `lofi` \| `piano` \| `none` | ✅ | drives the Strudel BGM bed; mixed at −18 dB under narration |
19
+ | `brand` | object | — | free-form; defaults to `{}` |
20
+
21
+ ### Per-scene fields (`scenes[]`)
22
+
23
+ | Field | Type | Required | Notes |
24
+ |-------|------|:---:|-------|
25
+ | `id` | string (min 1) | ✅ | convention: `scene-01`, `scene-02`, … |
26
+ | `durationSec` | number > 0 | ✅ | sum ≤ 180 (`limits.max_duration_sec`); also anchors estimated caption timing |
27
+ | `narration` | string | ✅ | may be `""` for a silent scene; keep ≤ ~8 words/segment for clean caption cues |
28
+ | `onScreenText` | string[] | — | overlay lines; defaults to `[]` |
29
+ | `visual` | object | ✅ | see below |
30
+ | `visual.kind` | `still` \| `clip` \| `mixed` \| `slide` \| `capture` | ✅ | `still` (oma-image), `slide` (oma-slide, explainer), `capture` (demo footage), `clip`/`mixed` (stock/AIGC) |
31
+ | `visual.prompt` | string | — | English image prompt forwarded to oma-image (see `prompt-tips.md`) |
32
+ | `visual.ref` | string | — | reference to an existing asset (e.g. capture segment) |
33
+ | `visual.source` | string | — | provenance marker (e.g. `agent-authored`) |
34
+ | `transition` | string | — | transition-out hint |
35
+
36
+ ## Minimal valid example
37
+
38
+ ```json
39
+ {
40
+ "schemaVersion": "1.0",
41
+ "mode": "shorts",
42
+ "aspect": "9:16",
43
+ "locale": "en",
44
+ "title": "Jeju coffee in 30 seconds",
45
+ "music": "none",
46
+ "brand": {},
47
+ "scenes": [
48
+ {
49
+ "id": "scene-01",
50
+ "durationSec": 3,
51
+ "narration": "Jeju's coffee scene is exploding.",
52
+ "onScreenText": ["JEJU ☕"],
53
+ "visual": {
54
+ "kind": "still",
55
+ "prompt": "Aerial drone shot of Jeju coastline at golden hour, vertical composition with headroom",
56
+ "source": "agent-authored"
57
+ }
58
+ },
59
+ {
60
+ "id": "scene-02",
61
+ "durationSec": 4,
62
+ "narration": "Volcanic soil, ocean air, and a cafe on every corner.",
63
+ "onScreenText": [],
64
+ "visual": {
65
+ "kind": "still",
66
+ "prompt": "Close-up of a hand pouring espresso over ice in a seaside cafe, warm light, shallow depth of field",
67
+ "source": "agent-authored"
68
+ }
69
+ }
70
+ ]
71
+ }
72
+ ```
73
+
74
+ ## Authoring checklist
75
+
76
+ - [ ] `schemaVersion: "1.0"` present (required literal).
77
+ - [ ] `aspect` is concrete (`9:16` / `16:9` / `1:1`) — never `auto`.
78
+ - [ ] Scene count ≤ 40, total `durationSec` ≤ 180.
79
+ - [ ] Every scene has a `visual.kind` from the enum; `still` scenes carry an English `visual.prompt`.
80
+ - [ ] `music` is set (use `"none"` unless the user asked for a bed).
81
+ - [ ] Validate cheaply before rendering: pass the file via `--script` with `--dry-run` first.
@@ -0,0 +1,127 @@
1
+ # Sound Effects and Soundtrack Listening
2
+
3
+ Use this branch when a video needs sound effects, foley, ambience or a
4
+ sound-designed soundtrack, or when the audio of a rendered or supplied video
5
+ must be judged: sync to picture, loudness, mix, or a choice between candidate
6
+ sounds. Narration, transcription and TTS stay with `oma-voice`; the Strudel
7
+ music bed stays with the creation pipeline. Sound effects are off unless the
8
+ brief asks for them or the user accepts a proposal.
9
+
10
+ ## Route to the upstream skill
11
+
12
+ `Leonxlnx/earshot` ships two skills. Route each request to the one that
13
+ supports it:
14
+
15
+ | Request | Upstream skill | Upstream section |
16
+ |---|---|---|
17
+ | Effects for a generated video (ticks, pops, whooshes, hits, typing, a lockup) | `earshot` | Loop 5: `sfx` / `kit` |
18
+ | Choose, trim or compare candidate sound files | `earshot` | Loop 1, Loop 3 |
19
+ | Review a video's audio: events, sync, loudness, launch-film scorecard | `earshot` | Loop 2 |
20
+ | Make a new sound in code, only when explicitly requested | `earshot` | Loop 4: `forge` |
21
+ | A soundtrack from real licensed recordings (foley, ambience, a licensed music track) | `real-sound` | its full process |
22
+
23
+ The shipped bank is made in code, not recorded. When the brief asks for
24
+ recorded or real sound, route to `real-sound`; never present a bank take as a
25
+ recording.
26
+
27
+ 1. Check the runtime skill catalog for `earshot` or `real-sound` from
28
+ `https://github.com/Leonxlnx/earshot`. Reuse its advertised installed
29
+ location and read its complete `SKILL.md` when available.
30
+ 2. If it is absent, load it without a persistent installation or another agent:
31
+
32
+ ```bash
33
+ bunx skills use Leonxlnx/earshot@earshot
34
+ bunx skills use Leonxlnx/earshot@real-sound
35
+ ```
36
+
37
+ Omit `--agent`. Read the emitted instructions and use the actual
38
+ supporting-files directory from the result for every relative path.
39
+ `real-sound` needs its complete `scripts/`, `templates/` and `references/`.
40
+ 3. A loaded skill is instructions, not the runtime. Resolve earshot in this
41
+ order: an already-connected earshot MCP server (discover its actual tools
42
+ before calling them), `EARSHOT_PYTHON`, `earshot` on PATH, then a Python
43
+ where `import earshot` succeeds. FFmpeg/FFprobe must be on PATH. Record the
44
+ resolved interpreter, the earshot version and the bank version from
45
+ `kit.lock.json`.
46
+ 4. If no runtime resolves, report the missing capability with the upstream
47
+ setup steps and stop this branch. Do not clone the repository, create a
48
+ virtual environment, install torch or fetch models without the user's
49
+ consent. The video can still render without effects; say so in the report.
50
+ 5. Follow the loaded upstream instructions for commands, flags, kit schema,
51
+ presets, roles, alignment, levels and QA thresholds. Do not copy its cookbook
52
+ into OMA resources.
53
+
54
+ `sfx`, `kit` and `measure` load no models. `describe`, `match`, `rank`, `find`,
55
+ `realness`, `timeline`, `sheet` and `critique` load CLAP and AST (about 1.1 GB
56
+ on first use); for more than a few calls, use the MCP server or one Python
57
+ process. `earshot llm` uploads audio to Google: run it only when a Gemini key is
58
+ set and the user explicitly agreed to the upload. Never pass `--yes-upload` or
59
+ `consent=true` on your own.
60
+
61
+ ## Bank effects in an OMA video
62
+
63
+ OMA owns the cue times and the composition; earshot owns the voices.
64
+
65
+ 1. Take cue times from the picture: scene boundaries in `render-spec.json`
66
+ (`fromFrame / fps`), on-screen text entrances, transitions and the authored
67
+ GSAP tween times in `hyperframes/index.html`. Write them in the upstream cue
68
+ format at `<runDir>/audio/sfx/cues.json`. Follow the upstream restraint
69
+ rules; hard cuts and cursor motion usually get no sound.
70
+ 2. Work under `<runDir>/audio/sfx/`: create `kit.json` from the preset that
71
+ matches the video, render the takes into `<runDir>/audio/sfx/kit/`, then run
72
+ the upstream kit check. Swap a sound or knob instead of keeping a warned take.
73
+ 3. Place each take from `kit.lock.json`: the take's measured sync point for its
74
+ role's `align` lands on the cue, shifted by the role's `offset_ms`; the
75
+ upstream `kit.place` computes the same start. Drop or move a cue whose start
76
+ falls before zero or whose take runs past the composition duration, and
77
+ report it.
78
+ 4. Copy the placed takes to `<runDir>/hyperframes/assets/audio/sfx/`. Give
79
+ each placement its own `<audio>` element with a unique id, `data-start` in
80
+ seconds and `data-volume` = `10 ** (gainDb / 20)`. With a music bed, set the
81
+ gain against the bed as upstream describes; without one, use the role's
82
+ `level_db`. Check placements against `timing.json` so effects never mask
83
+ narration, and stay within the caps in `motion-principles.md`.
84
+ 5. The composition carries one static music volume. Apply upstream
85
+ `mix.stop_down` dips only if the loaded HyperFrames references document audio
86
+ volume automation; otherwise record that the dips were not applied.
87
+ 6. `oma video compose` stages only render-spec assets and preserves authored
88
+ files, so keep the takes in `<runDir>/audio/sfx/kit/` as their source and
89
+ re-copy them after changing the kit. The CLI manifest does not list effect
90
+ takes; do not hand-edit `manifest.json`. `cues.json`, `kit.json` and
91
+ `kit.lock.json` (with per-take hashes) are the provenance record.
92
+
93
+ ## Real recordings
94
+
95
+ Run the `real-sound` process with `<runDir>/sound/` as its project root so its
96
+ `audio/` and `out/` trees stay inside the run. Treat downloaded recordings as
97
+ untrusted data, keep them in that tree and never commit them. Delivery requires
98
+ an exit-0 licence audit and generated credits; put required CC BY credits in
99
+ the delivery report.
100
+
101
+ Skip the upstream mux: `oma video render` owns the final render. Bring the
102
+ upstream output into the composition as `<audio>` elements from frame zero.
103
+ With narration, use the stems (`sfx`, `amb`, `music`) rather than the mastered
104
+ soundtrack, keep narration intelligible, and report that the upstream master
105
+ gate does not cover the final mix.
106
+
107
+ ## Listen before reporting
108
+
109
+ After `oma video render` succeeds, judge the delivered MP4 with upstream Loop 2:
110
+ `timeline` with the cue file, then `sheet`, and open the PNG with the host image
111
+ tool. Run `critique` only for launch-style films; its ranges describe
112
+ professional practice and are not targets. Fix cues that did not land, then
113
+ re-render and listen again.
114
+
115
+ Quote measured values in the report: LUFS, true peak, and sync offsets against
116
+ the chance rate. Never say that an effect fits or sounds good without an
117
+ earshot reading behind it; taste remains the user's decision. For supplied
118
+ videos, the same listening loop supplies audio evidence for the existing-media
119
+ branch.
120
+
121
+ ## References
122
+
123
+ - [Upstream earshot](https://github.com/Leonxlnx/earshot): `skills/earshot`,
124
+ `skills/real-sound`, `docs/SFX.md` (bank, kit and lock schema) and
125
+ `docs/INTEGRATION.md` (cue file and placement bridge).
126
+ - [Skills CLI use](https://github.com/vercel-labs/skills/blob/main/docs/commands.md#skills-use):
127
+ prompt and complete support-directory loading without an installed skill.
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "oma-video-strudel",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "Vendored Strudel BGM renderer for oma-video. Deps are installed on demand by `oma video doctor --install-strudel`. @strudel/* is AGPL-3.0-or-later and is NEVER imported by the MIT-licensed oma CLI — render.mjs runs as a subprocess (same boundary as the Remotion projects).",
7
+ "license": "MIT",
8
+ "dependencies": {
9
+ "@strudel/web": "^1.3.0"
10
+ }
11
+ }
@@ -0,0 +1,87 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>oma-video strudel offline render</title>
6
+ </head>
7
+ <body>
8
+ <!-- Served by render.mjs from the resolved @strudel/web dist. The IIFE
9
+ bundle exposes everything on `window.strudel` + `window.initStrudel`. -->
10
+ <script src="/strudel.js"></script>
11
+ <script>
12
+ window.__omaLog = [];
13
+ window.addEventListener('error', (e) =>
14
+ window.__omaLog.push('error: ' + e.message),
15
+ );
16
+ window.addEventListener('unhandledrejection', (e) =>
17
+ window.__omaLog.push('rejection: ' + (e.reason?.message ?? e.reason)),
18
+ );
19
+
20
+ /**
21
+ * Render `code` to a WAV and stash it on window as base64.
22
+ *
23
+ * `strudel.renderPatternAudio()` is a real OfflineAudioContext render
24
+ * (faster than realtime, no audio device involved) but it delivers the
25
+ * finished wav as a browser DOWNLOAD. Rather than plumb a download
26
+ * directory through CDP, we intercept URL.createObjectURL to capture the
27
+ * Blob in-page and hand the bytes back over the debugger protocol.
28
+ *
29
+ * Returns the base64 length; the driver pulls window.__omaWav in chunks.
30
+ */
31
+ window.__omaRender = async function (code, seconds, cps, sampleRate) {
32
+ await window.initStrudel();
33
+
34
+ // repl.evaluate(code, autostart=false) transpiles + evaluates without
35
+ // starting the realtime scheduler, and returns the Pattern itself.
36
+ const evaluated = await window.strudel.evaluate(code, false);
37
+ const pattern = evaluated?.pattern ?? evaluated;
38
+ if (!pattern || typeof pattern.queryArc !== 'function') {
39
+ throw new Error(
40
+ 'strudel: pattern code did not evaluate to a Pattern (got ' +
41
+ typeof pattern +
42
+ ')',
43
+ );
44
+ }
45
+
46
+ let captured = null;
47
+ const realCreate = URL.createObjectURL.bind(URL);
48
+ const realRevoke = URL.revokeObjectURL.bind(URL);
49
+ URL.createObjectURL = (blob) => {
50
+ captured = blob;
51
+ return 'blob:oma-captured';
52
+ };
53
+ URL.revokeObjectURL = () => {};
54
+ try {
55
+ await window.strudel.renderPatternAudio(
56
+ pattern,
57
+ cps,
58
+ 0,
59
+ seconds * cps,
60
+ sampleRate,
61
+ 128,
62
+ false,
63
+ 'oma-bgm',
64
+ );
65
+ } finally {
66
+ URL.createObjectURL = realCreate;
67
+ URL.revokeObjectURL = realRevoke;
68
+ }
69
+ if (!captured) {
70
+ throw new Error('strudel: renderPatternAudio produced no audio blob');
71
+ }
72
+
73
+ const bytes = new Uint8Array(await captured.arrayBuffer());
74
+ let binary = '';
75
+ const CHUNK = 0x8000; // stay under the String.fromCharCode arg limit
76
+ for (let i = 0; i < bytes.length; i += CHUNK) {
77
+ binary += String.fromCharCode.apply(
78
+ null,
79
+ bytes.subarray(i, i + CHUNK),
80
+ );
81
+ }
82
+ window.__omaWav = btoa(binary);
83
+ return window.__omaWav.length;
84
+ };
85
+ </script>
86
+ </body>
87
+ </html>
@@ -0,0 +1,295 @@
1
+ #!/usr/bin/env node
2
+ // Strudel BGM renderer — boundary-safe subprocess entrypoint.
3
+ //
4
+ // oma-video NEVER imports Strudel. @strudel/* is AGPL-3.0-or-later while the
5
+ // oma CLI is MIT, so the TypeScript provider (`providers/music-strudel.ts` +
6
+ // `internal/strudel-project.ts`) only ever LOCATES this project on disk and
7
+ // spawns *this* script. Same boundary the Remotion projects use.
8
+ //
9
+ // MECHANISM: `@strudel/webaudio.renderPatternAudio()` renders a pattern through
10
+ // an OfflineAudioContext — faster than realtime, no audio device, no autoplay
11
+ // gesture. A 30s bed renders in well under a second. The bundle only runs in a
12
+ // browser, so we drive a headless Chrome over raw CDP (Node's global WebSocket;
13
+ // zero npm deps here beyond @strudel/web itself) and serve the bundle from a
14
+ // loopback-only HTTP server.
15
+ //
16
+ // DETERMINISM: oscillator sounds (sine/triangle/square/sawtooth) render
17
+ // byte-identically across runs. Noise-based sounds (white/pink/brown) do NOT —
18
+ // superdough fills those buffers from Math.random(). Patterns that must be
19
+ // reproducible should stay oscillator-only.
20
+ //
21
+ // Contract — flags in, ONE JSON result line out (stdout's LAST line):
22
+ // IN --pattern-file <path> file holding the strudel pattern code (required)
23
+ // --out <abs wav path> output wav, inside a run dir (required)
24
+ // --chrome <path> Chrome/Chromium executable (required)
25
+ // --seconds <float> bed length in seconds (required)
26
+ // --cps <float> cycles per second (default 0.5)
27
+ // --sample-rate <int> default 44100
28
+ // --timeout <ms> hard ceiling for the whole render (default 120000)
29
+ // OUT {"ok":true,"output":"<abs wav>","seconds":<f>,"bytes":<n>,"sha256":"<hex>"}
30
+ // | {"ok":false,"error":"<message>","code":"<reason>"}
31
+ //
32
+ // Exit code is 0 on success, 1 on failure; the JSON result line is authoritative.
33
+ import { spawn } from "node:child_process";
34
+ import { createHash } from "node:crypto";
35
+ import { existsSync, readFileSync } from "node:fs";
36
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
37
+ import { createServer } from "node:http";
38
+ import { tmpdir } from "node:os";
39
+ import path from "node:path";
40
+ import process from "node:process";
41
+ import { fileURLToPath } from "node:url";
42
+
43
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
44
+ const DIST = path.join(HERE, "node_modules", "@strudel", "web", "dist");
45
+
46
+ const MIME = {
47
+ ".js": "text/javascript",
48
+ ".mjs": "text/javascript",
49
+ ".html": "text/html",
50
+ ".json": "application/json",
51
+ ".wasm": "application/wasm",
52
+ };
53
+
54
+ let server;
55
+ let chrome;
56
+ let userDataDir;
57
+ let hardTimer;
58
+
59
+ function fail(message, code = "render_failed") {
60
+ process.stdout.write(`${JSON.stringify({ ok: false, error: message, code })}\n`);
61
+ cleanup().finally(() => process.exit(1));
62
+ }
63
+
64
+ async function cleanup() {
65
+ if (hardTimer) clearTimeout(hardTimer);
66
+ try {
67
+ chrome?.kill("SIGTERM");
68
+ } catch {}
69
+ try {
70
+ server?.close();
71
+ } catch {}
72
+ if (userDataDir) {
73
+ await rm(userDataDir, { recursive: true, force: true }).catch(() => {});
74
+ }
75
+ }
76
+
77
+ function parseArgs(argv) {
78
+ const out = {};
79
+ for (let i = 0; i < argv.length; i++) {
80
+ const token = argv[i];
81
+ if (!token.startsWith("--")) continue;
82
+ const eq = token.indexOf("=");
83
+ if (eq !== -1) {
84
+ out[token.slice(2, eq)] = token.slice(eq + 1);
85
+ } else {
86
+ out[token.slice(2)] = argv[i + 1]?.startsWith("--") ? "" : argv[++i];
87
+ }
88
+ }
89
+ return out;
90
+ }
91
+
92
+ const args = parseArgs(process.argv.slice(2));
93
+ const patternFile = args["pattern-file"];
94
+ const outPath = args.out;
95
+ const chromePath = args.chrome;
96
+ const seconds = Number(args.seconds);
97
+ const cps = Number(args.cps ?? 0.5);
98
+ const sampleRate = Number(args["sample-rate"] ?? 44100);
99
+ const timeoutMs = Number(args.timeout ?? 120000);
100
+
101
+ if (!patternFile || !outPath || !chromePath) {
102
+ fail("missing required flag: --pattern-file / --out / --chrome", "bad_args");
103
+ } else if (!Number.isFinite(seconds) || seconds <= 0) {
104
+ fail(`--seconds must be a positive number (got ${args.seconds})`, "bad_args");
105
+ } else if (!existsSync(DIST)) {
106
+ fail(
107
+ `@strudel/web is not installed in ${HERE} — run \`oma video doctor --install-strudel\``,
108
+ "not_installed",
109
+ );
110
+ } else if (!existsSync(chromePath)) {
111
+ fail(`chrome executable not found: ${chromePath}`, "no_chrome");
112
+ } else {
113
+ main().catch((err) => fail(err?.message ?? String(err)));
114
+ }
115
+
116
+ async function main() {
117
+ hardTimer = setTimeout(
118
+ () => fail(`render exceeded ${timeoutMs}ms`, "timeout"),
119
+ timeoutMs,
120
+ );
121
+ hardTimer.unref?.();
122
+
123
+ const code = await readFile(patternFile, "utf8");
124
+
125
+ // 1. Loopback-only static server for the page + the strudel bundle. Paths are
126
+ // confined to this dir and the resolved dist (no traversal).
127
+ server = createServer(async (req, res) => {
128
+ const pathname = new URL(req.url, "http://127.0.0.1").pathname;
129
+ let file;
130
+ if (pathname === "/" || pathname === "/page.html") {
131
+ file = path.join(HERE, "page.html");
132
+ } else if (pathname === "/strudel.js") {
133
+ file = path.join(DIST, "index.js");
134
+ } else {
135
+ const resolved = path.resolve(DIST, `.${pathname}`);
136
+ file = resolved.startsWith(DIST) ? resolved : null;
137
+ }
138
+ if (!file) {
139
+ res.writeHead(403).end("forbidden");
140
+ return;
141
+ }
142
+ try {
143
+ const body = await readFile(file);
144
+ res.writeHead(200, {
145
+ "content-type": MIME[path.extname(file)] ?? "application/octet-stream",
146
+ "cross-origin-opener-policy": "same-origin",
147
+ "cross-origin-embedder-policy": "require-corp",
148
+ "cross-origin-resource-policy": "cross-origin",
149
+ });
150
+ res.end(body);
151
+ } catch {
152
+ res.writeHead(404).end("not found");
153
+ }
154
+ });
155
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
156
+ const httpPort = server.address().port;
157
+
158
+ // 2. Headless Chrome. No audio device is needed — OfflineAudioContext renders
159
+ // into a buffer, so this works on a bare CI box.
160
+ userDataDir = await mkdtemp(path.join(tmpdir(), "oma-strudel-"));
161
+ chrome = spawn(
162
+ chromePath,
163
+ [
164
+ "--headless=new",
165
+ "--disable-gpu",
166
+ "--no-first-run",
167
+ "--no-default-browser-check",
168
+ "--disable-dev-shm-usage",
169
+ `--user-data-dir=${userDataDir}`,
170
+ "--remote-debugging-port=0",
171
+ "about:blank",
172
+ ],
173
+ { stdio: ["ignore", "ignore", "pipe"] },
174
+ );
175
+ chrome.on("error", (err) => fail(`chrome failed to start: ${err.message}`, "no_chrome"));
176
+
177
+ const devtoolsPort = await waitForDevtoolsPort(
178
+ path.join(userDataDir, "DevToolsActivePort"),
179
+ );
180
+
181
+ // 3. Drive it over raw CDP.
182
+ const targets = await (
183
+ await fetch(`http://127.0.0.1:${devtoolsPort}/json/list`)
184
+ ).json();
185
+ const page = targets.find((t) => t.type === "page");
186
+ if (!page) throw new Error("chrome exposed no page target");
187
+
188
+ const cdp = await connect(page.webSocketDebuggerUrl);
189
+ await cdp.send("Runtime.enable");
190
+ await cdp.send("Page.enable");
191
+ await cdp.send("Page.navigate", {
192
+ url: `http://127.0.0.1:${httpPort}/page.html`,
193
+ });
194
+ await cdp.waitForLoad();
195
+
196
+ const base64Length = await cdp.evaluate(
197
+ `window.__omaRender(${JSON.stringify(code)}, ${seconds}, ${cps}, ${sampleRate})`,
198
+ );
199
+ if (!base64Length || base64Length <= 0) {
200
+ throw new Error("strudel produced an empty buffer");
201
+ }
202
+
203
+ // 4. Pull the wav back in slices so no single CDP message gets huge.
204
+ const CHUNK = 4 * 1024 * 1024;
205
+ let b64 = "";
206
+ for (let offset = 0; offset < base64Length; offset += CHUNK) {
207
+ b64 += await cdp.evaluate(
208
+ `window.__omaWav.slice(${offset}, ${offset + CHUNK})`,
209
+ );
210
+ }
211
+ const wav = Buffer.from(b64, "base64");
212
+ await writeFile(outPath, wav);
213
+
214
+ process.stdout.write(
215
+ `${JSON.stringify({
216
+ ok: true,
217
+ output: outPath,
218
+ seconds,
219
+ bytes: wav.length,
220
+ sha256: createHash("sha256").update(wav).digest("hex"),
221
+ })}\n`,
222
+ );
223
+ await cleanup();
224
+ process.exit(0);
225
+ }
226
+
227
+ /** Chrome writes its chosen debugger port here once it is listening. */
228
+ async function waitForDevtoolsPort(portFile) {
229
+ for (let i = 0; i < 200; i++) {
230
+ if (existsSync(portFile)) {
231
+ const [line] = readFileSync(portFile, "utf8").split("\n");
232
+ const port = Number(line);
233
+ if (Number.isFinite(port) && port > 0) return port;
234
+ }
235
+ await new Promise((resolve) => setTimeout(resolve, 50));
236
+ }
237
+ throw new Error("chrome never reported a devtools port");
238
+ }
239
+
240
+ /** Minimal CDP client over Node's global WebSocket. */
241
+ async function connect(wsUrl) {
242
+ const ws = new WebSocket(wsUrl);
243
+ await new Promise((resolve, reject) => {
244
+ ws.onopen = resolve;
245
+ ws.onerror = () => reject(new Error("cdp websocket failed to open"));
246
+ });
247
+
248
+ let nextId = 0;
249
+ const pending = new Map();
250
+ let loaded = false;
251
+ const loadWaiters = [];
252
+
253
+ ws.onmessage = (event) => {
254
+ const msg = JSON.parse(event.data);
255
+ if (msg.id && pending.has(msg.id)) {
256
+ const { resolve, reject } = pending.get(msg.id);
257
+ pending.delete(msg.id);
258
+ if (msg.error) reject(new Error(JSON.stringify(msg.error)));
259
+ else resolve(msg.result);
260
+ return;
261
+ }
262
+ if (msg.method === "Page.loadEventFired") {
263
+ loaded = true;
264
+ while (loadWaiters.length) loadWaiters.shift()();
265
+ }
266
+ };
267
+
268
+ const send = (method, params = {}) =>
269
+ new Promise((resolve, reject) => {
270
+ const id = ++nextId;
271
+ pending.set(id, { resolve, reject });
272
+ ws.send(JSON.stringify({ id, method, params }));
273
+ });
274
+
275
+ return {
276
+ send,
277
+ waitForLoad: () =>
278
+ loaded ? Promise.resolve() : new Promise((resolve) => loadWaiters.push(resolve)),
279
+ async evaluate(expression) {
280
+ const res = await send("Runtime.evaluate", {
281
+ expression,
282
+ awaitPromise: true,
283
+ returnByValue: true,
284
+ });
285
+ if (res.exceptionDetails) {
286
+ const detail =
287
+ res.exceptionDetails.exception?.description ??
288
+ res.exceptionDetails.text ??
289
+ "unknown page error";
290
+ throw new Error(detail.split("\n")[0]);
291
+ }
292
+ return res.result.value;
293
+ },
294
+ };
295
+ }