@jairodri/huckleberry-cli 1.0.1 → 1.1.0

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 (745) hide show
  1. package/README.md +85 -0
  2. package/agents/cos.json +2 -5
  3. package/agents/hermes/AGENTS.md +25 -49
  4. package/agents/hermes/fix-kimaki/SKILL.md +19 -92
  5. package/agents/hermes/hermes-fixer.md +44 -74
  6. package/agents/hermes.json +6 -18
  7. package/kanban/SKILL.md +18 -18
  8. package/kanban/package.json +1 -1
  9. package/kanban/src/dispatcher.mjs +1 -1
  10. package/kanban/templates/blog-social-campaign.yaml +6 -2
  11. package/kanban/templates/social-campaign.yaml +9 -4
  12. package/mcp/jev-calibration.mjs +335 -0
  13. package/mcp/jev-client.mjs +119 -0
  14. package/mcp/jev-thresholds.json +27 -0
  15. package/package.json +13 -9
  16. package/scripts/doctor-watchdog.sh +73 -0
  17. package/scripts/install-services.sh +34 -27
  18. package/scripts/self-improve-weekly.sh +34 -0
  19. package/scripts/sync-skills.mjs +121 -0
  20. package/skills/ab-test-setup/SKILL.md +266 -0
  21. package/skills/ab-test-setup/evals/evals.json +105 -0
  22. package/skills/ab-test-setup/references/sample-size-guide.md +263 -0
  23. package/skills/ab-test-setup/references/test-templates.md +277 -0
  24. package/skills/agent-reach/SKILL.md +132 -0
  25. package/skills/agent-reach/references/career.md +29 -0
  26. package/skills/agent-reach/references/dev.md +62 -0
  27. package/skills/agent-reach/references/search.md +33 -0
  28. package/skills/agent-reach/references/social.md +232 -0
  29. package/skills/agent-reach/references/video.md +130 -0
  30. package/skills/agent-reach/references/web.md +50 -0
  31. package/skills/ai-agent-seo/SKILL.md +450 -0
  32. package/skills/ai-cold-email-intelligence/SKILL.md +54 -0
  33. package/skills/ai-cold-email-intelligence/references/prompts.md +21 -0
  34. package/skills/ai-consensus-builder/SKILL.md +324 -0
  35. package/skills/ai-seo/SKILL.md +399 -0
  36. package/skills/ai-seo/evals/evals.json +90 -0
  37. package/skills/ai-seo/references/content-patterns.md +284 -0
  38. package/skills/ai-seo/references/platform-ranking-factors.md +152 -0
  39. package/skills/ai-visibility-audit/SKILL.md +322 -0
  40. package/skills/analytics-tracking/SKILL.md +309 -0
  41. package/skills/analytics-tracking/evals/evals.json +90 -0
  42. package/skills/analytics-tracking/references/event-library.md +260 -0
  43. package/skills/analytics-tracking/references/ga4-implementation.md +300 -0
  44. package/skills/analytics-tracking/references/gtm-implementation.md +390 -0
  45. package/skills/article-generator/SKILL.md +216 -0
  46. package/skills/auto-indexing/SKILL.md +179 -0
  47. package/skills/auto-indexing/scripts/auto-indexing.js +257 -0
  48. package/skills/auto-skill/SKILL.md +151 -0
  49. package/skills/auto-skill/correction-queue.js +102 -0
  50. package/skills/auto-skill/dedup.js +97 -0
  51. package/skills/auto-skill/skill-router.js +224 -0
  52. package/skills/autoresearch/SKILL.md +334 -0
  53. package/skills/autoresearch/autoresearch-autoresearch/SKILL.md.baseline +329 -0
  54. package/skills/autoresearch/autoresearch-autoresearch/changelog.md +34 -0
  55. package/skills/autoresearch/autoresearch-autoresearch/dashboard.html +116 -0
  56. package/skills/autoresearch/autoresearch-autoresearch/evals.md +39 -0
  57. package/skills/autoresearch/autoresearch-autoresearch/results.json +44 -0
  58. package/skills/autoresearch/autoresearch-autoresearch/results.tsv +3 -0
  59. package/skills/autoresearch/eval-guide.md +121 -0
  60. package/skills/b2b-cold-outreach/SKILL.md +322 -0
  61. package/skills/birdclaw/SKILL.md +82 -0
  62. package/skills/brainstorm/SKILL.md +168 -0
  63. package/skills/brand-kit-playground/SKILL.md +312 -0
  64. package/skills/brand-kit-playground/references/html-architecture.md +316 -0
  65. package/skills/brand-kit-playground/references/steal-patterns.md +88 -0
  66. package/skills/brand-voice/SKILL.md +223 -0
  67. package/skills/brand-voice/references/honesty-as-positioning.md +60 -0
  68. package/skills/brand-voice/references/voice-modes.md +1158 -0
  69. package/skills/business-advisory-council/README.md +136 -0
  70. package/skills/business-advisory-council/SKILL.md +94 -0
  71. package/skills/business-advisory-council/business-advisory-council.py +492 -0
  72. package/skills/business-advisory-council/discord-components-decision.json +8 -0
  73. package/skills/business-advisory-council/examples/steve-conley-partnership.json +6 -0
  74. package/skills/business-advisory-council/steve-conley-call-brief.json +6 -0
  75. package/skills/churn-prevention/SKILL.md +305 -0
  76. package/skills/churn-prevention/evals/evals.json +93 -0
  77. package/skills/churn-prevention/references/cancel-flow-patterns.md +316 -0
  78. package/skills/churn-prevention/references/dunning-playbook.md +408 -0
  79. package/skills/claw-compactor/LICENSE +21 -0
  80. package/skills/claw-compactor/README.md +459 -0
  81. package/skills/claw-compactor/SKILL.md +329 -0
  82. package/skills/claw-compactor/benchmark/RESULTS.md +104 -0
  83. package/skills/claw-compactor/benchmark/__init__.py +1 -0
  84. package/skills/claw-compactor/benchmark/compressors.py +520 -0
  85. package/skills/claw-compactor/benchmark/data/sample_01_devops.json +82 -0
  86. package/skills/claw-compactor/benchmark/data/sample_02_trading.json +54 -0
  87. package/skills/claw-compactor/benchmark/data/sample_03_ml_short.json +38 -0
  88. package/skills/claw-compactor/benchmark/data/sample_04_mixed_long.json +318 -0
  89. package/skills/claw-compactor/benchmark/data/sample_05_sysadmin.json +56 -0
  90. package/skills/claw-compactor/benchmark/evaluate.py +278 -0
  91. package/skills/claw-compactor/benchmark/report.py +241 -0
  92. package/skills/claw-compactor/benchmark/results/benchmark_results.json +405 -0
  93. package/skills/claw-compactor/benchmark/run_benchmark.py +238 -0
  94. package/skills/claw-compactor/engram.yaml +49 -0
  95. package/skills/claw-compactor/proxy/README.md +87 -0
  96. package/skills/claw-compactor/proxy/dashboard.html +804 -0
  97. package/skills/claw-compactor/proxy/event-log.mjs +213 -0
  98. package/skills/claw-compactor/proxy/fair-queue.mjs +288 -0
  99. package/skills/claw-compactor/proxy/metrics-store.mjs +425 -0
  100. package/skills/claw-compactor/proxy/package-lock.json +125 -0
  101. package/skills/claw-compactor/proxy/package.json +15 -0
  102. package/skills/claw-compactor/proxy/portal.html +360 -0
  103. package/skills/claw-compactor/proxy/process-registry.mjs +447 -0
  104. package/skills/claw-compactor/proxy/rate-limiter.mjs +117 -0
  105. package/skills/claw-compactor/proxy/redis-client.mjs +75 -0
  106. package/skills/claw-compactor/proxy/retry.mjs +182 -0
  107. package/skills/claw-compactor/proxy/server.mjs +2100 -0
  108. package/skills/claw-compactor/proxy/session-affinity.mjs +177 -0
  109. package/skills/claw-compactor/proxy/start.sh +23 -0
  110. package/skills/claw-compactor/proxy/test/event-log.test.mjs +237 -0
  111. package/skills/claw-compactor/proxy/test/helpers.mjs +103 -0
  112. package/skills/claw-compactor/proxy/test/integration.test.mjs +297 -0
  113. package/skills/claw-compactor/proxy/test/metrics-store.test.mjs +243 -0
  114. package/skills/claw-compactor/proxy/test/process-registry.test.mjs +390 -0
  115. package/skills/claw-compactor/proxy/test/rate-limiter.test.mjs +121 -0
  116. package/skills/claw-compactor/proxy/test/redis-client.test.mjs +62 -0
  117. package/skills/claw-compactor/proxy/test/token-tracker.test.mjs +200 -0
  118. package/skills/claw-compactor/proxy/token-tracker.mjs +482 -0
  119. package/skills/claw-compactor/pyproject.toml +26 -0
  120. package/skills/claw-compactor/references/README.md +44 -0
  121. package/skills/claw-compactor/references/architecture.md +197 -0
  122. package/skills/claw-compactor/references/benchmarks.md +94 -0
  123. package/skills/claw-compactor/references/compression-prompts.md +39 -0
  124. package/skills/claw-compactor/references/compression-techniques.md +219 -0
  125. package/skills/claw-compactor/references/testing.md +160 -0
  126. package/skills/claw-compactor/scripts/audit_memory.py +216 -0
  127. package/skills/claw-compactor/scripts/compress_memory.py +236 -0
  128. package/skills/claw-compactor/scripts/compressed_context.py +280 -0
  129. package/skills/claw-compactor/scripts/dedup_memory.py +147 -0
  130. package/skills/claw-compactor/scripts/dictionary_compress.py +170 -0
  131. package/skills/claw-compactor/scripts/engram-auto.sh +152 -0
  132. package/skills/claw-compactor/scripts/engram_auto.py +695 -0
  133. package/skills/claw-compactor/scripts/engram_cli.py +474 -0
  134. package/skills/claw-compactor/scripts/estimate_tokens.py +131 -0
  135. package/skills/claw-compactor/scripts/generate_summary_tiers.py +292 -0
  136. package/skills/claw-compactor/scripts/lib/__init__.py +34 -0
  137. package/skills/claw-compactor/scripts/lib/config.py +289 -0
  138. package/skills/claw-compactor/scripts/lib/dedup.py +119 -0
  139. package/skills/claw-compactor/scripts/lib/dictionary.py +281 -0
  140. package/skills/claw-compactor/scripts/lib/engram.py +793 -0
  141. package/skills/claw-compactor/scripts/lib/engram_prompts.py +175 -0
  142. package/skills/claw-compactor/scripts/lib/engram_storage.py +254 -0
  143. package/skills/claw-compactor/scripts/lib/exceptions.py +24 -0
  144. package/skills/claw-compactor/scripts/lib/markdown.py +312 -0
  145. package/skills/claw-compactor/scripts/lib/rle.py +165 -0
  146. package/skills/claw-compactor/scripts/lib/tokenizer_optimizer.py +188 -0
  147. package/skills/claw-compactor/scripts/lib/tokens.py +68 -0
  148. package/skills/claw-compactor/scripts/mem_compress.py +788 -0
  149. package/skills/claw-compactor/scripts/observation_compressor.py +346 -0
  150. package/skills/claw-compactor/tests/__init__.py +0 -0
  151. package/skills/claw-compactor/tests/conftest.py +94 -0
  152. package/skills/claw-compactor/tests/test_audit_comprehensive.py +123 -0
  153. package/skills/claw-compactor/tests/test_audit_memory.py +67 -0
  154. package/skills/claw-compactor/tests/test_benchmark.py +122 -0
  155. package/skills/claw-compactor/tests/test_cli_commands.py +228 -0
  156. package/skills/claw-compactor/tests/test_compress_memory.py +109 -0
  157. package/skills/claw-compactor/tests/test_compress_memory_comprehensive.py +191 -0
  158. package/skills/claw-compactor/tests/test_compressed_context.py +145 -0
  159. package/skills/claw-compactor/tests/test_config.py +54 -0
  160. package/skills/claw-compactor/tests/test_dedup_memory.py +124 -0
  161. package/skills/claw-compactor/tests/test_dictionary.py +327 -0
  162. package/skills/claw-compactor/tests/test_dictionary_comprehensive.py +241 -0
  163. package/skills/claw-compactor/tests/test_engram.py +1091 -0
  164. package/skills/claw-compactor/tests/test_engram_auto.py +529 -0
  165. package/skills/claw-compactor/tests/test_error_handling.py +194 -0
  166. package/skills/claw-compactor/tests/test_estimate_tokens.py +99 -0
  167. package/skills/claw-compactor/tests/test_generate_summary_tiers.py +105 -0
  168. package/skills/claw-compactor/tests/test_integration.py +249 -0
  169. package/skills/claw-compactor/tests/test_lib_dedup.py +135 -0
  170. package/skills/claw-compactor/tests/test_lib_markdown.py +388 -0
  171. package/skills/claw-compactor/tests/test_lib_tokens.py +82 -0
  172. package/skills/claw-compactor/tests/test_main_entry.py +176 -0
  173. package/skills/claw-compactor/tests/test_markdown_advanced.py +284 -0
  174. package/skills/claw-compactor/tests/test_new_features.py +77 -0
  175. package/skills/claw-compactor/tests/test_observation_comprehensive.py +261 -0
  176. package/skills/claw-compactor/tests/test_observation_compressor.py +128 -0
  177. package/skills/claw-compactor/tests/test_performance.py +172 -0
  178. package/skills/claw-compactor/tests/test_pipeline.py +109 -0
  179. package/skills/claw-compactor/tests/test_real_workspace.py +161 -0
  180. package/skills/claw-compactor/tests/test_rle.py +114 -0
  181. package/skills/claw-compactor/tests/test_rle_comprehensive.py +151 -0
  182. package/skills/claw-compactor/tests/test_roundtrip.py +253 -0
  183. package/skills/claw-compactor/tests/test_roundtrip_comprehensive.py +192 -0
  184. package/skills/claw-compactor/tests/test_tiers_comprehensive.py +171 -0
  185. package/skills/claw-compactor/tests/test_token_economics.py +237 -0
  186. package/skills/claw-compactor/tests/test_tokenizer_optimizer.py +175 -0
  187. package/skills/claw-compactor/tests/test_tokenizer_optimizer_comprehensive.py +241 -0
  188. package/skills/claw-compactor/tests/test_tokens.py +27 -0
  189. package/skills/cmo/CHANGELOG.md +61 -0
  190. package/skills/cmo/SKILL.md +500 -0
  191. package/skills/cmo/references/ai-slop-patterns.md +56 -0
  192. package/skills/cmo/references/analytics-guide.md +152 -0
  193. package/skills/cmo/references/ideas-library.md +197 -0
  194. package/skills/cmo/rules/brand-file-map.md +198 -0
  195. package/skills/cmo/rules/brand-memory.md +158 -0
  196. package/skills/cmo/rules/cli-runtime-index.md +95 -0
  197. package/skills/cmo/rules/command-reference.md +167 -0
  198. package/skills/cmo/rules/communication.md +15 -0
  199. package/skills/cmo/rules/context-switch.md +101 -0
  200. package/skills/cmo/rules/ecosystem.md +125 -0
  201. package/skills/cmo/rules/error-recovery.md +98 -0
  202. package/skills/cmo/rules/learning-loop.md +170 -0
  203. package/skills/cmo/rules/monorepo.md +78 -0
  204. package/skills/cmo/rules/output-format.md +124 -0
  205. package/skills/cmo/rules/persona.md +15 -0
  206. package/skills/cmo/rules/playbooks.md +327 -0
  207. package/skills/cmo/rules/progressive-enhancement.md +231 -0
  208. package/skills/cmo/rules/publish-index.md +106 -0
  209. package/skills/cmo/rules/quality-gate.md +90 -0
  210. package/skills/cmo/rules/recency-grounding.md +124 -0
  211. package/skills/cmo/rules/safety.md +80 -0
  212. package/skills/cmo/rules/studio-api-index.md +138 -0
  213. package/skills/cmo/rules/studio-integration.md +147 -0
  214. package/skills/cmo/rules/sub-agents.md +167 -0
  215. package/skills/cold-email/SKILL.md +240 -0
  216. package/skills/cold-email/evals/evals.json +94 -0
  217. package/skills/cold-email/references/benchmarks.md +83 -0
  218. package/skills/cold-email/references/follow-up-sequences.md +81 -0
  219. package/skills/cold-email/references/frameworks.md +90 -0
  220. package/skills/cold-email/references/personalization.md +79 -0
  221. package/skills/cold-email/references/subject-lines.md +53 -0
  222. package/skills/competitive-intel/SKILL.md +433 -0
  223. package/skills/competitive-intel/references/competitive-frameworks.md +145 -0
  224. package/skills/competitor-alternatives/SKILL.md +362 -0
  225. package/skills/competitor-alternatives/evals/evals.json +93 -0
  226. package/skills/competitor-alternatives/references/content-architecture.md +271 -0
  227. package/skills/competitor-alternatives/references/templates.md +223 -0
  228. package/skills/concreteness-fixer/SKILL.md +483 -0
  229. package/skills/concreteness-fixer/scripts/validation/concreteness-validator.js +221 -0
  230. package/skills/concreteness-fixer/utils/execFileNoThrow.js +16 -0
  231. package/skills/content-atomizer/SKILL.md +424 -0
  232. package/skills/content-atomizer/references/hook-strategies.md +56 -0
  233. package/skills/content-atomizer/references/platform-specs.md +118 -0
  234. package/skills/content-atomizer/references/platforms/bluesky.md +64 -0
  235. package/skills/content-atomizer/references/platforms/instagram.md +77 -0
  236. package/skills/content-atomizer/references/platforms/linkedin.md +120 -0
  237. package/skills/content-atomizer/references/platforms/reddit.md +73 -0
  238. package/skills/content-atomizer/references/platforms/threads.md +64 -0
  239. package/skills/content-atomizer/references/platforms/tiktok.md +55 -0
  240. package/skills/content-atomizer/references/platforms/twitter.md +123 -0
  241. package/skills/content-atomizer/references/platforms/youtube.md +56 -0
  242. package/skills/content-atomizer/references/tweet-patterns.md +40 -0
  243. package/skills/content-repost-manager/SKILL.md +245 -0
  244. package/skills/content-repost-manager/scripts/content-repost-scan.py +133 -0
  245. package/skills/context-recovery/.clawdhub/origin.json +7 -0
  246. package/skills/context-recovery/SKILL.md +198 -0
  247. package/skills/context-recovery/_meta.json +6 -0
  248. package/skills/conversion-flow-cro/SKILL.md +248 -0
  249. package/skills/conversion-flow-cro/references/flow-benchmarks.md +158 -0
  250. package/skills/copy-editing/SKILL.md +474 -0
  251. package/skills/copy-editing/evals/evals.json +89 -0
  252. package/skills/copy-editing/references/plain-english-alternatives.md +394 -0
  253. package/skills/copywriting/SKILL.md +349 -0
  254. package/skills/copywriting/evals/evals.json +111 -0
  255. package/skills/copywriting/references/awareness-level-guide.md +227 -0
  256. package/skills/copywriting/references/copy-frameworks.md +389 -0
  257. package/skills/copywriting/references/natural-transitions.md +272 -0
  258. package/skills/creative/SKILL.md +365 -0
  259. package/skills/creative/modes/ad-creative.md +1748 -0
  260. package/skills/creative/modes/product-photo.md +1520 -0
  261. package/skills/creative/modes/product-video.md +1657 -0
  262. package/skills/creative/modes/social-graphics.md +1511 -0
  263. package/skills/creative/modes/talking-head.md +2208 -0
  264. package/skills/creative/references/VISUAL_INTELLIGENCE.md +1233 -0
  265. package/skills/deep-serp-article/SKILL.md +677 -0
  266. package/skills/deep-serp-article/SKILL.md.bak +530 -0
  267. package/skills/earned-media/SKILL.md +260 -0
  268. package/skills/email-sequences/SKILL.md +446 -0
  269. package/skills/email-sequences/references/sequence-templates.md +1217 -0
  270. package/skills/firecrawl/.skill-meta.json +13 -0
  271. package/skills/firecrawl/SKILL.md +154 -0
  272. package/skills/firecrawl/rules/install.md +63 -0
  273. package/skills/firecrawl/rules/security.md +26 -0
  274. package/skills/form-cro/SKILL.md +429 -0
  275. package/skills/form-cro/evals/evals.json +90 -0
  276. package/skills/free-tool-strategy/SKILL.md +325 -0
  277. package/skills/free-tool-strategy/evals/evals.json +90 -0
  278. package/skills/free-tool-strategy/references/tool-types.md +253 -0
  279. package/skills/frontend-slides/SKILL.md +248 -0
  280. package/skills/frontend-slides/STYLE_PRESETS.md +347 -0
  281. package/skills/frontend-slides/animation-patterns.md +110 -0
  282. package/skills/frontend-slides/html-template.md +347 -0
  283. package/skills/frontend-slides/scripts/extract-pptx.py +96 -0
  284. package/skills/frontend-slides/viewport-base.css +153 -0
  285. package/skills/guardrail-framework/SKILL.md +319 -0
  286. package/skills/hook-generator/SKILL.md +438 -0
  287. package/skills/hook-generator/database/2026-PLATFORM-RESEARCH.md +211 -0
  288. package/skills/hook-generator/database/instagram-hooks.json +591 -0
  289. package/skills/hook-generator/database/linkedin-hooks.json +502 -0
  290. package/skills/hook-generator/database/medium-hooks.json +181 -0
  291. package/skills/hook-generator/database/tiktok-hooks.json +337 -0
  292. package/skills/hook-generator/database/twitter-hooks.json +567 -0
  293. package/skills/hook-generator/database/youtube-hooks.json +275 -0
  294. package/skills/hook-generator/package-lock.json +24 -0
  295. package/skills/hook-generator/package.json +5 -0
  296. package/skills/hook-generator/references/hook-generation-system.md +257 -0
  297. package/skills/hook-generator/references/hook-performance-analysis.json +153 -0
  298. package/skills/hook-generator/references/hooks-library.md +128 -0
  299. package/skills/hook-generator/references/platform-hook-frameworks.md +275 -0
  300. package/skills/hook-generator/scripts/generate-hooks-unified.js +722 -0
  301. package/skills/hook-generator/scripts/generate-hooks.js +225 -0
  302. package/skills/hook-generator/scripts/generate.sh +36 -0
  303. package/skills/hook-generator/scripts/ralph-hook-enhancer.js +864 -0
  304. package/skills/hook-generator/scripts/test-gemini-output.js +51 -0
  305. package/skills/humanizer/README.md +120 -0
  306. package/skills/humanizer/SKILL.md +699 -0
  307. package/skills/image-gen/SKILL.md +277 -0
  308. package/skills/image-gen/references/prompt-patterns.md +114 -0
  309. package/skills/image-gen/references/visual-metaphors.md +51 -0
  310. package/skills/impeccable/SKILL.md +86 -0
  311. package/skills/impeccable/reference/adapt.md +312 -0
  312. package/skills/impeccable/reference/adapt.native.md +58 -0
  313. package/skills/impeccable/reference/android.md +40 -0
  314. package/skills/impeccable/reference/animate.md +86 -0
  315. package/skills/impeccable/reference/audit.md +136 -0
  316. package/skills/impeccable/reference/audit.native.md +139 -0
  317. package/skills/impeccable/reference/bolder.md +31 -0
  318. package/skills/impeccable/reference/clarify.md +94 -0
  319. package/skills/impeccable/reference/colorize.md +86 -0
  320. package/skills/impeccable/reference/craft-floor.md +42 -0
  321. package/skills/impeccable/reference/craft.md +5 -0
  322. package/skills/impeccable/reference/critique.md +788 -0
  323. package/skills/impeccable/reference/degraded/asset-producer.md +90 -0
  324. package/skills/impeccable/reference/degraded/documenter.md +24 -0
  325. package/skills/impeccable/reference/degraded/finish-reviewer.md +37 -0
  326. package/skills/impeccable/reference/degraded/manual-edit-applier.md +92 -0
  327. package/skills/impeccable/reference/delight.md +70 -0
  328. package/skills/impeccable/reference/distill.md +111 -0
  329. package/skills/impeccable/reference/doctor.md +53 -0
  330. package/skills/impeccable/reference/document.md +416 -0
  331. package/skills/impeccable/reference/extract.md +69 -0
  332. package/skills/impeccable/reference/harden.md +336 -0
  333. package/skills/impeccable/reference/hooks.md +105 -0
  334. package/skills/impeccable/reference/init.md +125 -0
  335. package/skills/impeccable/reference/ios.md +45 -0
  336. package/skills/impeccable/reference/layout.md +84 -0
  337. package/skills/impeccable/reference/live-setup.md +102 -0
  338. package/skills/impeccable/reference/live.md +323 -0
  339. package/skills/impeccable/reference/new-work.md +110 -0
  340. package/skills/impeccable/reference/onboard.md +234 -0
  341. package/skills/impeccable/reference/operate.md +61 -0
  342. package/skills/impeccable/reference/optimize.md +258 -0
  343. package/skills/impeccable/reference/overdrive.md +127 -0
  344. package/skills/impeccable/reference/polish.md +97 -0
  345. package/skills/impeccable/reference/quieter.md +99 -0
  346. package/skills/impeccable/reference/routing.md +18 -0
  347. package/skills/impeccable/reference/shape.md +59 -0
  348. package/skills/impeccable/reference/typeset.md +80 -0
  349. package/skills/impeccable/reference/visualize.md +49 -0
  350. package/skills/impeccable/scripts/command-metadata.json +94 -0
  351. package/skills/impeccable/scripts/concept-seed.mjs +558 -0
  352. package/skills/impeccable/scripts/context-signals.mjs +334 -0
  353. package/skills/impeccable/scripts/context.mjs +1467 -0
  354. package/skills/impeccable/scripts/critique-storage.mjs +213 -0
  355. package/skills/impeccable/scripts/detect-csp.mjs +198 -0
  356. package/skills/impeccable/scripts/detect.mjs +21 -0
  357. package/skills/impeccable/scripts/detector/browser/injected/index.mjs +2023 -0
  358. package/skills/impeccable/scripts/detector/cli/main.mjs +438 -0
  359. package/skills/impeccable/scripts/detector/design-system.mjs +1046 -0
  360. package/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +8283 -0
  361. package/skills/impeccable/scripts/detector/detect-antipatterns.mjs +50 -0
  362. package/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +372 -0
  363. package/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +1168 -0
  364. package/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +1186 -0
  365. package/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +264 -0
  366. package/skills/impeccable/scripts/detector/engines/visual/screenshot-contrast.mjs +189 -0
  367. package/skills/impeccable/scripts/detector/findings.mjs +18 -0
  368. package/skills/impeccable/scripts/detector/node/file-system.mjs +203 -0
  369. package/skills/impeccable/scripts/detector/profile/profiler.mjs +166 -0
  370. package/skills/impeccable/scripts/detector/registry/antipatterns.mjs +617 -0
  371. package/skills/impeccable/scripts/detector/rules/checks.mjs +5580 -0
  372. package/skills/impeccable/scripts/detector/shared/color.mjs +124 -0
  373. package/skills/impeccable/scripts/detector/shared/constants.mjs +112 -0
  374. package/skills/impeccable/scripts/detector/shared/fonts.mjs +30 -0
  375. package/skills/impeccable/scripts/detector/shared/inline-ignores.mjs +148 -0
  376. package/skills/impeccable/scripts/detector/shared/page.mjs +7 -0
  377. package/skills/impeccable/scripts/doctor.mjs +336 -0
  378. package/skills/impeccable/scripts/embed-prompt.mjs +133 -0
  379. package/skills/impeccable/scripts/generate-image.mjs +240 -0
  380. package/skills/impeccable/scripts/hook-admin.mjs +801 -0
  381. package/skills/impeccable/scripts/hook-before-edit.mjs +508 -0
  382. package/skills/impeccable/scripts/hook-lib.mjs +2153 -0
  383. package/skills/impeccable/scripts/hook.mjs +78 -0
  384. package/skills/impeccable/scripts/lib/artifact-schema.mjs +93 -0
  385. package/skills/impeccable/scripts/lib/composition-catalog.mjs +200 -0
  386. package/skills/impeccable/scripts/lib/concept-catalog.mjs +384 -0
  387. package/skills/impeccable/scripts/lib/design-parser.mjs +925 -0
  388. package/skills/impeccable/scripts/lib/impeccable-config.mjs +658 -0
  389. package/skills/impeccable/scripts/lib/impeccable-paths.mjs +137 -0
  390. package/skills/impeccable/scripts/lib/is-generated.mjs +69 -0
  391. package/skills/impeccable/scripts/lib/provider.mjs +5 -0
  392. package/skills/impeccable/scripts/lib/roll-selection.mjs +362 -0
  393. package/skills/impeccable/scripts/lib/staleness-deep.mjs +478 -0
  394. package/skills/impeccable/scripts/lib/staleness-notice.mjs +169 -0
  395. package/skills/impeccable/scripts/lib/staleness.mjs +457 -0
  396. package/skills/impeccable/scripts/lib/surface-briefs.mjs +151 -0
  397. package/skills/impeccable/scripts/lib/target-args.mjs +42 -0
  398. package/skills/impeccable/scripts/lib/target-slug.mjs +33 -0
  399. package/skills/impeccable/scripts/lib/template-extensions.mjs +146 -0
  400. package/skills/impeccable/scripts/live/accept-css.mjs +617 -0
  401. package/skills/impeccable/scripts/live/accept-verify.mjs +60 -0
  402. package/skills/impeccable/scripts/live/browser-script-parts.mjs +55 -0
  403. package/skills/impeccable/scripts/live/completion.mjs +28 -0
  404. package/skills/impeccable/scripts/live/event-validation.mjs +199 -0
  405. package/skills/impeccable/scripts/live/frameworks/astro.mjs +47 -0
  406. package/skills/impeccable/scripts/live/frameworks/detect-utils.mjs +73 -0
  407. package/skills/impeccable/scripts/live/frameworks/index.mjs +143 -0
  408. package/skills/impeccable/scripts/live/frameworks/journal.mjs +197 -0
  409. package/skills/impeccable/scripts/live/frameworks/nextjs.mjs +49 -0
  410. package/skills/impeccable/scripts/live/frameworks/nuxt.mjs +161 -0
  411. package/skills/impeccable/scripts/live/frameworks/script-src.mjs +17 -0
  412. package/skills/impeccable/scripts/live/frameworks/static-html.mjs +26 -0
  413. package/skills/impeccable/scripts/live/frameworks/sveltekit.mjs +71 -0
  414. package/skills/impeccable/scripts/live/frameworks/tag-strategy.mjs +247 -0
  415. package/skills/impeccable/scripts/live/frameworks/tanstack-start.mjs +70 -0
  416. package/skills/impeccable/scripts/live/frameworks/vite-generic.mjs +42 -0
  417. package/skills/impeccable/scripts/live/generation-preflight.mjs +149 -0
  418. package/skills/impeccable/scripts/live/insert-ui.mjs +458 -0
  419. package/skills/impeccable/scripts/live/instructions.mjs +142 -0
  420. package/skills/impeccable/scripts/live/manual-apply.mjs +939 -0
  421. package/skills/impeccable/scripts/live/manual-edit-routes.mjs +357 -0
  422. package/skills/impeccable/scripts/live/manual-edits-buffer.mjs +152 -0
  423. package/skills/impeccable/scripts/live/poll-lanes.mjs +14 -0
  424. package/skills/impeccable/scripts/live/roots.mjs +508 -0
  425. package/skills/impeccable/scripts/live/session-store.mjs +563 -0
  426. package/skills/impeccable/scripts/live/source-lock.mjs +105 -0
  427. package/skills/impeccable/scripts/live/source-search.mjs +105 -0
  428. package/skills/impeccable/scripts/live/svelte-ast.mjs +961 -0
  429. package/skills/impeccable/scripts/live/svelte-component.mjs +1342 -0
  430. package/skills/impeccable/scripts/live/sveltekit-adapter.mjs +316 -0
  431. package/skills/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
  432. package/skills/impeccable/scripts/live/ui-core.mjs +180 -0
  433. package/skills/impeccable/scripts/live/vocabulary.mjs +171 -0
  434. package/skills/impeccable/scripts/live-accept.mjs +954 -0
  435. package/skills/impeccable/scripts/live-browser-dom.js +146 -0
  436. package/skills/impeccable/scripts/live-browser-session.js +123 -0
  437. package/skills/impeccable/scripts/live-browser.js +12520 -0
  438. package/skills/impeccable/scripts/live-commit-manual-edits.mjs +1244 -0
  439. package/skills/impeccable/scripts/live-complete.mjs +107 -0
  440. package/skills/impeccable/scripts/live-copy-edit-agent.mjs +683 -0
  441. package/skills/impeccable/scripts/live-discard-manual-edits.mjs +51 -0
  442. package/skills/impeccable/scripts/live-inject.mjs +503 -0
  443. package/skills/impeccable/scripts/live-insert.mjs +292 -0
  444. package/skills/impeccable/scripts/live-manual-edit-evidence.mjs +368 -0
  445. package/skills/impeccable/scripts/live-poll.mjs +429 -0
  446. package/skills/impeccable/scripts/live-resume.mjs +123 -0
  447. package/skills/impeccable/scripts/live-server.mjs +1669 -0
  448. package/skills/impeccable/scripts/live-status.mjs +71 -0
  449. package/skills/impeccable/scripts/live-target.mjs +30 -0
  450. package/skills/impeccable/scripts/live-wrap.mjs +927 -0
  451. package/skills/impeccable/scripts/live.mjs +359 -0
  452. package/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
  453. package/skills/impeccable/scripts/palette.mjs +628 -0
  454. package/skills/impeccable/scripts/pin.mjs +221 -0
  455. package/skills/impeccable/scripts/serve-question.mjs +932 -0
  456. package/skills/impeccable/scripts/surface-brief.mjs +74 -0
  457. package/skills/inkstone-admin/SKILL.md +66 -0
  458. package/skills/jev-hook-gate/SKILL.md +111 -0
  459. package/skills/keyword-research/SKILL.md +503 -0
  460. package/skills/keyword-research/references/dr-cap-and-confidence.md +62 -0
  461. package/skills/keyword-research/references/keyword-examples.md +769 -0
  462. package/skills/landing-page-architect/SKILL.md +585 -0
  463. package/skills/landing-page-architect/references/architecture-reference.md +32 -0
  464. package/skills/launch-strategy/SKILL.md +384 -0
  465. package/skills/launch-strategy/evals/evals.json +91 -0
  466. package/skills/launch-strategy/references/launch-checklist.md +209 -0
  467. package/skills/launch-strategy/references/launch-timeline.md +159 -0
  468. package/skills/lead-email-craft/SKILL.md +522 -0
  469. package/skills/lead-magnets/SKILL.md +310 -0
  470. package/skills/lead-magnets/references/benchmarks.md +129 -0
  471. package/skills/lead-magnets/references/format-guide.md +196 -0
  472. package/skills/link-building-seo/SKILL.md +210 -0
  473. package/skills/lint-report.txt +1 -0
  474. package/skills/manifest.json +129 -0
  475. package/skills/marketing-demo/SKILL.md +249 -0
  476. package/skills/marketing-demo/references/remotion-examples.md +281 -0
  477. package/skills/marketing-ideas/SKILL.md +167 -0
  478. package/skills/marketing-ideas/evals/evals.json +90 -0
  479. package/skills/marketing-ideas/references/ideas-by-category.md +366 -0
  480. package/skills/marketing-psychology/SKILL.md +374 -0
  481. package/skills/marketing-psychology/evals/evals.json +88 -0
  482. package/skills/marketing-psychology/references/case-studies.md +148 -0
  483. package/skills/narrative-research-article/SKILL.md +469 -0
  484. package/skills/narrative-research-article/references/lanusse-patterns.md +166 -0
  485. package/skills/notebooklm/SKILL.md +674 -0
  486. package/skills/off-page-seo/SKILL.md +219 -0
  487. package/skills/onboarding-cro/SKILL.md +220 -0
  488. package/skills/onboarding-cro/evals/evals.json +92 -0
  489. package/skills/onboarding-cro/references/experiments.md +258 -0
  490. package/skills/original-research/SKILL.md +388 -0
  491. package/skills/page-cro/SKILL.md +269 -0
  492. package/skills/page-cro/references/cro-benchmarks.md +140 -0
  493. package/skills/paper-marketing/SKILL.md +448 -0
  494. package/skills/paper-marketing/references/agent-quality-rubric.md +68 -0
  495. package/skills/paper-marketing/references/brand-system-template.md +120 -0
  496. package/skills/paper-marketing/references/paper-mcp-rules.md +71 -0
  497. package/skills/paper-marketing/references/platform-conventions.md +43 -0
  498. package/skills/paywall-upgrade-cro/SKILL.md +227 -0
  499. package/skills/paywall-upgrade-cro/evals/evals.json +93 -0
  500. package/skills/paywall-upgrade-cro/references/experiments.md +164 -0
  501. package/skills/platform-formatting/SKILL.md +51 -0
  502. package/skills/popup-cro/SKILL.md +454 -0
  503. package/skills/popup-cro/evals/evals.json +94 -0
  504. package/skills/positioning-angles/SKILL.md +524 -0
  505. package/skills/positioning-angles/references/ad-matrix-and-examples.md +176 -0
  506. package/skills/positioning-angles/references/angle-frameworks.md +187 -0
  507. package/skills/positioning-angles/references/dunford-positioning.md +135 -0
  508. package/skills/positioning-angles/references/hormozi-offer.md +247 -0
  509. package/skills/positioning-angles/references/schwartz-sophistication.md +166 -0
  510. package/skills/positioning-angles/references/unique-mechanism.md +162 -0
  511. package/skills/pricing-strategy/SKILL.md +346 -0
  512. package/skills/pricing-strategy/evals/evals.json +90 -0
  513. package/skills/pricing-strategy/references/research-methods.md +152 -0
  514. package/skills/pricing-strategy/references/tier-structure.md +307 -0
  515. package/skills/programmatic-seo/SKILL.md +238 -0
  516. package/skills/programmatic-seo/evals/evals.json +94 -0
  517. package/skills/programmatic-seo/references/playbooks.md +308 -0
  518. package/skills/project-rag/SKILL.md +127 -0
  519. package/skills/quality-gate/SKILL.md +519 -0
  520. package/skills/quality-gate/judge-personas/buyer.md +47 -0
  521. package/skills/quality-gate/judge-personas/expert.md +47 -0
  522. package/skills/quality-gate/judge-personas/outsider.md +47 -0
  523. package/skills/quality-gate/judge-personas/scanner.md +41 -0
  524. package/skills/quality-gate/judge-personas/skeptic.md +40 -0
  525. package/skills/referral-program/SKILL.md +290 -0
  526. package/skills/referral-program/evals/evals.json +89 -0
  527. package/skills/referral-program/references/affiliate-programs.md +163 -0
  528. package/skills/referral-program/references/program-examples.md +143 -0
  529. package/skills/review-sites/SKILL.md +279 -0
  530. package/skills/revops/SKILL.md +343 -0
  531. package/skills/revops/evals/evals.json +91 -0
  532. package/skills/revops/references/automation-playbooks.md +290 -0
  533. package/skills/revops/references/lifecycle-definitions.md +278 -0
  534. package/skills/revops/references/routing-rules.md +203 -0
  535. package/skills/revops/references/scoring-models.md +247 -0
  536. package/skills/saas-product-seo/SKILL.md +324 -0
  537. package/skills/sales-enablement/SKILL.md +349 -0
  538. package/skills/sales-enablement/evals/evals.json +91 -0
  539. package/skills/sales-enablement/references/deck-frameworks.md +263 -0
  540. package/skills/sales-enablement/references/demo-scripts.md +355 -0
  541. package/skills/sales-enablement/references/objection-library.md +270 -0
  542. package/skills/sales-enablement/references/one-pager-templates.md +208 -0
  543. package/skills/schema-markup/SKILL.md +179 -0
  544. package/skills/schema-markup/evals/evals.json +87 -0
  545. package/skills/schema-markup/references/schema-examples.md +398 -0
  546. package/skills/self-improve/SKILL.md +166 -0
  547. package/skills/self-improving-agent/README.md +243 -0
  548. package/skills/self-improving-agent/SKILL.md +437 -0
  549. package/skills/self-improving-agent/scripts/__init__.py +16 -0
  550. package/skills/self-improving-agent/scripts/__pycache__/auto_detect.cpython-314.pyc +0 -0
  551. package/skills/self-improving-agent/scripts/__pycache__/nlm_curation.cpython-314.pyc +0 -0
  552. package/skills/self-improving-agent/scripts/__pycache__/self_improving_agent.cpython-314.pyc +0 -0
  553. package/skills/self-improving-agent/scripts/auto_detect.py +270 -0
  554. package/skills/self-improving-agent/scripts/init_expertise_registry.py +89 -0
  555. package/skills/self-improving-agent/scripts/nlm_curation.py +259 -0
  556. package/skills/self-improving-agent/scripts/self-improving-agent +113 -0
  557. package/skills/self-improving-agent/scripts/self_improving_agent.py +603 -0
  558. package/skills/self-improving-agent/scripts/test_curation_scorer.py +20 -0
  559. package/skills/self-improving-agent/scripts/test_self_improving.py +184 -0
  560. package/skills/send-email/SKILL.md +426 -0
  561. package/skills/send-email/references/batch-email-examples.md +807 -0
  562. package/skills/send-email/references/best-practices.md +453 -0
  563. package/skills/send-email/references/installation.md +123 -0
  564. package/skills/send-email/references/single-email-examples.md +470 -0
  565. package/skills/send-email/references/webhooks.md +221 -0
  566. package/skills/seo-audit/SKILL.md +362 -0
  567. package/skills/seo-audit/references/ai-writing-detection.md +200 -0
  568. package/skills/seo-audit/references/architecture-patterns.md +271 -0
  569. package/skills/seo-audit/references/schema-templates.md +427 -0
  570. package/skills/seo-content/SKILL.md +510 -0
  571. package/skills/seo-content/references/anti-ai-detection.md +184 -0
  572. package/skills/seo-content/references/content-structures.md +148 -0
  573. package/skills/seo-content/references/eeat-examples.md +235 -0
  574. package/skills/seo-content/references/on-page-seo.md +261 -0
  575. package/skills/seo-content/references/pseo-playbooks.md +226 -0
  576. package/skills/seo-content/references/schema-templates.md +104 -0
  577. package/skills/seo-content/references/serp-analysis-format.md +151 -0
  578. package/skills/seo-machine/SKILL.md +331 -0
  579. package/skills/seo-machine/assets/brand-template.md +73 -0
  580. package/skills/seo-machine/assets/link-inventory-template.md +74 -0
  581. package/skills/seo-machine/assets/roadmap-template.md +184 -0
  582. package/skills/seo-machine/references/api-stack-recipes.md +230 -0
  583. package/skills/seo-machine/references/exa-recipes.md +260 -0
  584. package/skills/seo-machine/references/manual-research.md +73 -0
  585. package/skills/seo-machine/references/methodology.md +87 -0
  586. package/skills/seo-machine/references/off-page.md +123 -0
  587. package/skills/seo-machine/references/patterns/alternatives.md +176 -0
  588. package/skills/seo-machine/references/patterns/compare.md +105 -0
  589. package/skills/seo-machine/references/patterns/playbooks.md +106 -0
  590. package/skills/seo-machine/references/patterns/use-case.md +109 -0
  591. package/skills/seo-machine/references/quality-bars.md +99 -0
  592. package/skills/seo-machine/references/stacks/astro.md +156 -0
  593. package/skills/seo-machine/references/stacks/detection.md +61 -0
  594. package/skills/seo-machine/references/stacks/markdown-fallback.md +181 -0
  595. package/skills/seo-machine/references/stacks/nextjs.md +108 -0
  596. package/skills/seo-machine/references/stacks/rails-inertia.md +99 -0
  597. package/skills/seo-machine/references/striking-distance.md +116 -0
  598. package/skills/seo-machine/references/technical-audit.md +153 -0
  599. package/skills/seo-machine/scripts/link_audit.py +265 -0
  600. package/skills/seo-machine/scripts/tech_audit.py +221 -0
  601. package/skills/seo-machine/scripts/word_count.py +154 -0
  602. package/skills/sharp-thesis-validator/SKILL.md +393 -0
  603. package/skills/sharp-thesis-validator/examples.md +186 -0
  604. package/skills/signup-flow-cro/SKILL.md +359 -0
  605. package/skills/signup-flow-cro/evals/evals.json +88 -0
  606. package/skills/site-architecture/SKILL.md +357 -0
  607. package/skills/site-architecture/evals/evals.json +88 -0
  608. package/skills/site-architecture/references/mermaid-templates.md +216 -0
  609. package/skills/site-architecture/references/navigation-patterns.md +305 -0
  610. package/skills/site-architecture/references/site-type-templates.md +293 -0
  611. package/skills/skill-self-improvement/SKILL.md +151 -0
  612. package/skills/slideshow-script/SKILL.md +274 -0
  613. package/skills/slideshow-script/references/content-spec-schema.md +77 -0
  614. package/skills/slideshow-script/references/frameworks.md +84 -0
  615. package/skills/slideshow-script/references/paips-framework.md +37 -0
  616. package/skills/social-campaign/SKILL.md +419 -0
  617. package/skills/social-campaign/references/paper-mcp-workflow.md +48 -0
  618. package/skills/social-content/SKILL.md +359 -0
  619. package/skills/social-content/evals/evals.json +92 -0
  620. package/skills/social-content/references/platforms.md +170 -0
  621. package/skills/social-content/references/post-templates.md +177 -0
  622. package/skills/social-content/references/reverse-engineering.md +195 -0
  623. package/skills/social-worker/SKILL.md +204 -0
  624. package/skills/startup-launcher/SKILL.md +252 -0
  625. package/skills/startup-launcher/references/file-router.md +64 -0
  626. package/skills/startup-launcher/references/platform-matrix.md +65 -0
  627. package/skills/startup-launcher/references/platforms/alternativeto.md +283 -0
  628. package/skills/startup-launcher/references/platforms/appsumo.md +495 -0
  629. package/skills/startup-launcher/references/platforms/auraplusplus.md +736 -0
  630. package/skills/startup-launcher/references/platforms/betalist.md +98 -0
  631. package/skills/startup-launcher/references/platforms/capterra.md +276 -0
  632. package/skills/startup-launcher/references/platforms/crunchbase.md +514 -0
  633. package/skills/startup-launcher/references/platforms/dealify.md +291 -0
  634. package/skills/startup-launcher/references/platforms/dealmirror.md +313 -0
  635. package/skills/startup-launcher/references/platforms/desifounder.md +490 -0
  636. package/skills/startup-launcher/references/platforms/devhunt.md +210 -0
  637. package/skills/startup-launcher/references/platforms/devpost.md +448 -0
  638. package/skills/startup-launcher/references/platforms/fazier.md +56 -0
  639. package/skills/startup-launcher/references/platforms/firsto.md +489 -0
  640. package/skills/startup-launcher/references/platforms/g2.md +226 -0
  641. package/skills/startup-launcher/references/platforms/hackernews.md +99 -0
  642. package/skills/startup-launcher/references/platforms/hackernoon.md +385 -0
  643. package/skills/startup-launcher/references/platforms/indiedeals.md +367 -0
  644. package/skills/startup-launcher/references/platforms/indiehackers.md +80 -0
  645. package/skills/startup-launcher/references/platforms/kenmoo.md +231 -0
  646. package/skills/startup-launcher/references/platforms/labstartups.md +618 -0
  647. package/skills/startup-launcher/references/platforms/launchboard.md +307 -0
  648. package/skills/startup-launcher/references/platforms/launchfast.md +560 -0
  649. package/skills/startup-launcher/references/platforms/launchigniter.md +56 -0
  650. package/skills/startup-launcher/references/platforms/launchingnext.md +674 -0
  651. package/skills/startup-launcher/references/platforms/launchit.md +574 -0
  652. package/skills/startup-launcher/references/platforms/launchyourapp.md +677 -0
  653. package/skills/startup-launcher/references/platforms/libhunt.md +220 -0
  654. package/skills/startup-launcher/references/platforms/ltdhunt.md +221 -0
  655. package/skills/startup-launcher/references/platforms/microlaunch.md +45 -0
  656. package/skills/startup-launcher/references/platforms/openalternative.md +195 -0
  657. package/skills/startup-launcher/references/platforms/peerlist.md +56 -0
  658. package/skills/startup-launcher/references/platforms/peerpush.md +64 -0
  659. package/skills/startup-launcher/references/platforms/primeclub.md +282 -0
  660. package/skills/startup-launcher/references/platforms/productcanyon.md +190 -0
  661. package/skills/startup-launcher/references/platforms/producthunt.md +98 -0
  662. package/skills/startup-launcher/references/platforms/rankinpublic.md +315 -0
  663. package/skills/startup-launcher/references/platforms/rockethub.md +374 -0
  664. package/skills/startup-launcher/references/platforms/saasgenius.md +175 -0
  665. package/skills/startup-launcher/references/platforms/saashub.md +205 -0
  666. package/skills/startup-launcher/references/platforms/saasmantra.md +221 -0
  667. package/skills/startup-launcher/references/platforms/saaspirate.md +287 -0
  668. package/skills/startup-launcher/references/platforms/saaswarrior.md +190 -0
  669. package/skills/startup-launcher/references/platforms/saaszilla.md +194 -0
  670. package/skills/startup-launcher/references/platforms/selected.md +925 -0
  671. package/skills/startup-launcher/references/platforms/sideprojectors.md +86 -0
  672. package/skills/startup-launcher/references/platforms/softonic.md +240 -0
  673. package/skills/startup-launcher/references/platforms/sourceforge.md +251 -0
  674. package/skills/startup-launcher/references/platforms/stacksocial.md +244 -0
  675. package/skills/startup-launcher/references/platforms/startupstash.md +326 -0
  676. package/skills/startup-launcher/references/platforms/theresanaiforthat.md +253 -0
  677. package/skills/startup-launcher/references/platforms/tinylaunch.md +70 -0
  678. package/skills/startup-launcher/references/platforms/tinystartups.md +72 -0
  679. package/skills/startup-launcher/references/platforms/toolfolio.md +291 -0
  680. package/skills/startup-launcher/references/platforms/trustmrr.md +194 -0
  681. package/skills/startup-launcher/references/platforms/trylaunch.md +531 -0
  682. package/skills/startup-launcher/references/platforms/uneed.md +57 -0
  683. package/skills/startup-launcher/references/templates/asset-checklist.md +218 -0
  684. package/skills/startup-launcher/references/templates/launch-tracker.md +195 -0
  685. package/skills/startup-launcher/references/templates/product-brief.md +98 -0
  686. package/skills/summarize/.skill-meta.json +13 -0
  687. package/skills/summarize/SKILL.md +170 -0
  688. package/skills/summarize/rules/install.md +157 -0
  689. package/skills/summarize/rules/usage.md +207 -0
  690. package/skills/topical-authority/SKILL.md +307 -0
  691. package/skills/topical-authority/references/jake-ward-framework.md +201 -0
  692. package/skills/topical-authority/references/serp-clustering-algorithm.md +325 -0
  693. package/skills/topical-authority/scripts/orchestrator.js +367 -0
  694. package/skills/topical-authority/scripts/seo/analyze-competitor-coverage.js +3 -0
  695. package/skills/topical-authority/scripts/seo/audit-internal-links.js +3 -0
  696. package/skills/topical-authority/scripts/seo/build-clusters-from-serp.js +3 -0
  697. package/skills/topical-authority/scripts/seo/extract-keywords-from-forums.js +3 -0
  698. package/skills/topical-authority/scripts/seo/extract-paa-questions.js +3 -0
  699. package/skills/topical-authority/scripts/seo/fetch-serp-urls.js +3 -0
  700. package/skills/topical-authority/scripts/seo/gemini-serp-clustering.js +3 -0
  701. package/skills/topical-authority/scripts/seo/identify-content-gaps.js +3 -0
  702. package/skills/topical-authority/scripts/seo/identify-research-gaps.js +3 -0
  703. package/skills/topical-authority/scripts/seo/scrape-quora-questions.js +3 -0
  704. package/skills/topical-authority/scripts/seo/scrape-reddit-pain-points.js +3 -0
  705. package/skills/topical-authority/scripts/seo/weave-internal-links.js +3 -0
  706. package/skills/visual-style/SKILL.md +199 -0
  707. package/skills/voice-brand-alignment/SKILL.md +70 -0
  708. package/skills/voice-brand-alignment/references/ai-slop-patterns.md +405 -0
  709. package/skills/voice-brand-alignment/scripts/validation/gemini-voice-validator.js +96 -0
  710. package/skills/voice-extraction/SKILL.md +234 -0
  711. package/skills/voice-extraction/references/extraction-prompt.md +155 -0
  712. package/skills/voice-extraction/references/voice-dna-template.md +168 -0
  713. package/skills/voice-extraction/references/voice-elements.md +259 -0
  714. package/skills/voice-extraction/scripts/build-corpus.js +120 -0
  715. package/skills/voice-extraction/scripts/collect-sources.sh +168 -0
  716. package/skills/warm-outbound/SKILL.md +299 -0
  717. package/skills/warm-outbound/templates/warm-message.md +39 -0
  718. package/skills/weekly-newsletter-digest/SKILL.md +78 -0
  719. package/skills/x-playwriter/SKILL.md +428 -0
  720. package/skills/x-playwriter/scripts/x-browser.js +185 -0
  721. package/skills/x-post-generator-openclaw/SKILL.md +138 -0
  722. package/skills/x-post-generator-openclaw/scripts/generate-x-post.js +537 -0
  723. package/skills/x-posting-playwriter/SKILL.md +203 -0
  724. package/skills/x-research/SKILL.md +129 -0
  725. package/skills/x-research/x-research.sh +85 -0
  726. package/skills/youtube-summarize/SKILL.md +90 -0
  727. package/skills/youtube-summarize/scripts/youtube-summarize.sh +143 -0
  728. package/src/cli.js +349 -172
  729. package/src/harness/deepseek-adapter.js +102 -0
  730. package/src/harness/deepseek.js +225 -0
  731. package/src/harness/opencode-adapter.js +174 -0
  732. package/src/harness/pi-adapter.js +172 -0
  733. package/src/harness/registry.js +121 -0
  734. package/src/harness/telemetry.js +102 -0
  735. package/src/harness/types.js +165 -0
  736. package/src/interview.js +36 -4
  737. package/src/postinstall.js +10 -12
  738. package/templates/AGENTS.md +17 -17
  739. package/templates/ORG.md +1 -1
  740. package/templates/TOOLS.md +7 -2
  741. package/templates/opencode.json +26 -29
  742. package/wiki/doctor.md +54 -0
  743. package/wiki/inkstone.md +4 -4
  744. package/wiki/opencode.md +3 -3
  745. package/skills/curated.txt +0 -68
@@ -0,0 +1,2100 @@
1
+ /**
2
+ * Claude Code Proxy v0.5.0
3
+ *
4
+ * Central proxy for multiple OpenClaw instances.
5
+ * Wraps claude CLI into OpenAI-compatible API with:
6
+ * - Fair queuing (round-robin between sources)
7
+ * - Rate limiting (95% of Max plan limits)
8
+ * - Priority by model (opus=high, sonnet=normal, haiku=low)
9
+ * - Authentication via Bearer token
10
+ * - Per-source metrics & monitoring
11
+ * - Process registry with zombie detection & reaper
12
+ * - Retry with exponential backoff + jitter
13
+ * - Stream heartbeat & execution timeout
14
+ * - Graceful shutdown
15
+ *
16
+ * All OpenClaw instances point their claude-code provider to this proxy.
17
+ * Requests are queued fairly and processed through local claude CLI,
18
+ * using the Max subscription (flat monthly fee, no per-token cost).
19
+ */
20
+
21
+ import { createServer, request as httpRequest } from "node:http";
22
+ import { request as httpsRequest } from "node:https";
23
+ import { spawn } from "node:child_process";
24
+ import { randomUUID } from "node:crypto";
25
+ import { readFile } from "node:fs/promises";
26
+ import { fileURLToPath } from "node:url";
27
+ import { dirname, join } from "node:path";
28
+ import { createFairQueue } from "./fair-queue.mjs";
29
+ import { createProcessRegistry } from "./process-registry.mjs";
30
+ import { createRetryPolicy } from "./retry.mjs";
31
+ import { createEventLog } from "./event-log.mjs";
32
+ import { createTokenTracker } from "./token-tracker.mjs";
33
+ import { createMetricsStore } from "./metrics-store.mjs";
34
+ import { createRateLimiter } from "./rate-limiter.mjs";
35
+ import { createRedisClient } from "./redis-client.mjs";
36
+ import { createSessionAffinity } from "./session-affinity.mjs";
37
+
38
+ const __dirname = dirname(fileURLToPath(import.meta.url));
39
+
40
+ // ============================================================
41
+ // Configuration
42
+ // ============================================================
43
+
44
+ const PORT = parseInt(process.env.CLAUDE_PROXY_PORT || "8403", 10);
45
+ const CLAUDE_BIN = process.env.CLAUDE_BIN || "claude";
46
+ const AUTH_TOKEN = process.env.PROXY_AUTH_TOKEN || "local-proxy";
47
+
48
+ // ============================================================
49
+ // CLI Router Objects: smart routing with failover
50
+ // WORKERS env var: JSON array of {name, bin, token} objects
51
+ // Routing strategy:
52
+ // - Default: all traffic to PRIMARY_WORKER (env, default "2")
53
+ // - On rate limit: switch entirely to the other CLI router
54
+ // - Every HEALTH_CHECK_MS: probe if cooled-down router is back
55
+ // - When recovered: resume load-balancing (round-robin)
56
+ // ============================================================
57
+
58
+ const PRIMARY_WORKER = process.env.PRIMARY_WORKER || "2";
59
+ const HEALTH_CHECK_MS = parseInt(process.env.HEALTH_CHECK_MS || "600000", 10); // 10 min
60
+ // CLI agent mode: workers are full autonomous agents (all tools enabled, --dangerously-skip-permissions)
61
+ // When true, ALL requests route through CLI workers; API direct path is bypassed.
62
+ const USE_CLI_AGENTS = process.env.USE_CLI_AGENTS === "true";
63
+
64
+ // Default worker pool: Mac native + Ubuntu VM (OrbStack)
65
+ const DEFAULT_WORKERS = [
66
+ { name: "1", bin: "/opt/homebrew/bin/claude" },
67
+ { name: "2", bin: "/opt/homebrew/bin/claude-ubuntu" },
68
+ ];
69
+
70
+ let _workerPool = [];
71
+ try {
72
+ if (process.env.WORKERS) {
73
+ _workerPool = JSON.parse(process.env.WORKERS);
74
+ }
75
+ } catch (err) {
76
+ console.warn(`[CLIRouter] Failed to parse WORKERS env: ${err.message} — using defaults`);
77
+ }
78
+ if (_workerPool.length === 0) {
79
+ _workerPool = DEFAULT_WORKERS;
80
+ }
81
+ // Assign default names if missing
82
+ _workerPool.forEach((w, i) => { if (!w.name) w.name = String(i + 1); });
83
+
84
+ // Worker health state
85
+ const _workerHealth = new Map(); // name -> { limited: boolean, limitedAt: number }
86
+ for (const w of _workerPool) {
87
+ _workerHealth.set(w.name, { limited: false, limitedAt: 0 });
88
+ }
89
+
90
+ // Round-robin index for load-balancing mode
91
+ let _rrIndex = 0;
92
+
93
+ console.log(`[CLIRouter] Pool: ${_workerPool.map((w) => `${w.name}=${w.bin}`).join(" | ")}`);
94
+ console.log(`[CLIRouter] Primary: ${PRIMARY_WORKER} | Health check: ${HEALTH_CHECK_MS / 1000}s`);
95
+
96
+ // ============================================================
97
+ // Fallback API: last-resort model when all CLI routers fail
98
+ // Forwards as an OpenAI-compatible /v1/chat/completions request
99
+ // ============================================================
100
+
101
+ const FALLBACK_API = (() => {
102
+ if (process.env.FALLBACK_API_URL) {
103
+ return {
104
+ baseUrl: process.env.FALLBACK_API_URL,
105
+ apiKey: process.env.FALLBACK_API_KEY || "none",
106
+ model: process.env.FALLBACK_MODEL || "default",
107
+ name: process.env.FALLBACK_NAME || "fallback",
108
+ };
109
+ }
110
+ // Default: MiniMax local endpoint
111
+ return {
112
+ baseUrl: "http://172.28.216.81:8080/v1",
113
+ apiKey: "none",
114
+ model: "MiniMax-M2.5-Q8_0-00001-of-00006.gguf",
115
+ name: "minimax-local",
116
+ };
117
+ })();
118
+ console.log(`[Fallback] ${FALLBACK_API.name} → ${FALLBACK_API.baseUrl} model=${FALLBACK_API.model}`);
119
+
120
+ // ============================================================
121
+ // Anthropic Direct API — for tool-enabled requests (bypass CLI)
122
+ // When request includes `tools`, call Anthropic API directly so the
123
+ // gateway receives tool_calls in OpenAI format and executes them.
124
+ // CLI path remains for text-only requests (flat fee via Max sub).
125
+ // ============================================================
126
+
127
+ const ANTHROPIC_API_BASE = "https://api.anthropic.com";
128
+ const ANTHROPIC_API_VERSION = "2023-06-01";
129
+
130
+ const ANTHROPIC_MODEL_IDS = {
131
+ sonnet: process.env.ANTHROPIC_SONNET_MODEL || "claude-sonnet-4-6",
132
+ opus: process.env.ANTHROPIC_OPUS_MODEL || "claude-opus-4-6",
133
+ haiku: process.env.ANTHROPIC_HAIKU_MODEL || "claude-haiku-4-5-20251001",
134
+ };
135
+
136
+ // Token pool for API direct round-robin.
137
+ // Each token represents an independent OAuth credential with its own rate limits.
138
+ // OAuth requires `anthropic-beta: oauth-2025-04-20` header to work with the raw API.
139
+ const TOKEN_POOL = (() => {
140
+ const tokens = [];
141
+ // Worker tokens from WORKERS env (reuse existing config)
142
+ for (const w of _workerPool) {
143
+ if (w.token) tokens.push({ name: w.name, token: w.token, type: "oauth_flat" });
144
+ }
145
+ // Fallback to process-level CLAUDE_CODE_OAUTH_TOKEN
146
+ if (tokens.length === 0) {
147
+ const oat = process.env.CLAUDE_CODE_OAUTH_TOKEN;
148
+ if (oat) tokens.push({ name: "default", token: oat, type: "oauth_flat" });
149
+ }
150
+ // Last resort: API key (per-token billing)
151
+ if (tokens.length === 0) {
152
+ const key = process.env.ANTHROPIC_API_KEY;
153
+ if (key) tokens.push({ name: "apikey", token: key, type: "api_key_billed" });
154
+ }
155
+ return tokens;
156
+ })();
157
+ let _tokenRrIndex = 0;
158
+ function getNextToken() {
159
+ const idx = _tokenRrIndex++ % TOKEN_POOL.length;
160
+ return TOKEN_POOL[idx];
161
+ }
162
+ // Backward compat: ANTHROPIC_AUTH still used for logging/health checks
163
+ const ANTHROPIC_AUTH = TOKEN_POOL.length > 0 ? TOKEN_POOL[0] : null;
164
+ if (TOKEN_POOL.length > 0) {
165
+ console.log(`[ApiDirect] Token pool: ${TOKEN_POOL.length} token(s) — [${TOKEN_POOL.map(t => `${t.name}:${t.type}`).join(", ")}] — ALL requests via API direct`);
166
+ } else {
167
+ console.log(`[ApiDirect] No tokens configured — falling back to CLI workers`);
168
+ }
169
+
170
+ // ============================================================
171
+ // Worker traffic & error tracking — exposed via /metrics for dashboard
172
+ // ============================================================
173
+ const workerStats = {
174
+ // Per-worker traffic: { "1": { requests: 0, errors: 0 }, "3": { ... } }
175
+ traffic: Object.fromEntries(_workerPool.map(w => [w.name, { requests: 0, errors: 0, lastReqAt: null }])),
176
+ // Error categories: { cli_crash: N, cli_killed: N, context_overflow: N, ... }
177
+ errors: {
178
+ cli_crash: 0, // code=1 — CLI error (nested session, auth, prompt too long, etc.)
179
+ cli_killed: 0, // code=143 — SIGTERM (reaper killed, heartbeat timeout)
180
+ context_overflow: 0, // fallback API "Context size exceeded"
181
+ api_error: 0, // Anthropic API errors (401, 429, 500)
182
+ stream_retry: 0, // quick-fail retries on alternate worker
183
+ timeout: 0, // heartbeat or exec timeout
184
+ queue_timeout: 0, // queue timeout (waited too long)
185
+ other: 0,
186
+ },
187
+ // Recent error log (ring buffer, last 100)
188
+ recentErrors: [],
189
+ };
190
+ function recordWorkerRequest(workerName) {
191
+ const w = workerStats.traffic[workerName];
192
+ if (w) { w.requests++; w.lastReqAt = Date.now(); }
193
+ }
194
+ function recordWorkerError(workerName, category, detail) {
195
+ const w = workerStats.traffic[workerName];
196
+ if (w) w.errors++;
197
+ if (workerStats.errors[category] !== undefined) workerStats.errors[category]++;
198
+ else workerStats.errors.other++;
199
+ workerStats.recentErrors.push({ ts: Date.now(), worker: workerName, category, detail: (detail || "").slice(0, 200) });
200
+ if (workerStats.recentErrors.length > 100) workerStats.recentErrors.shift();
201
+ }
202
+
203
+ // _loadBalanceMode starts true: round-robin across all healthy workers
204
+ // Falls back to single-worker mode when one worker is rate-limited
205
+ let _loadBalanceMode = true;
206
+
207
+ // Active connection tracking — for least-connections routing
208
+ const _activeConns = new Map(_workerPool.map(w => [w.name, 0]));
209
+ function workerAcquire(name) { _activeConns.set(name, (_activeConns.get(name) || 0) + 1); }
210
+ function workerRelease(name) { const v = _activeConns.get(name) || 0; _activeConns.set(name, Math.max(0, v - 1)); }
211
+ function leastLoadedWorker(pool) {
212
+ let best = pool[0];
213
+ let bestConns = _activeConns.get(best.name) ?? Infinity;
214
+ let bestTotal = workerStats.traffic[best.name]?.requests ?? 0;
215
+ for (let i = 1; i < pool.length; i++) {
216
+ const c = _activeConns.get(pool[i].name) ?? 0;
217
+ const t = workerStats.traffic[pool[i].name]?.requests ?? 0;
218
+ // Primary: least active connections; Secondary: least total requests (evens out over time)
219
+ if (c < bestConns || (c === bestConns && t < bestTotal)) {
220
+ best = pool[i]; bestConns = c; bestTotal = t;
221
+ }
222
+ }
223
+ return best;
224
+ }
225
+
226
+ /**
227
+ * Get the next worker, respecting session affinity when available.
228
+ *
229
+ * @param {string} [sessionKey] - Session key for affinity lookup
230
+ * @returns {object} worker from _workerPool
231
+ */
232
+ function getNextWorker(sessionKey) {
233
+ const isHealthy = (name) => !_workerHealth.get(name)?.limited;
234
+ const healthy = _workerPool.filter((w) => isHealthy(w.name));
235
+
236
+ if (healthy.length === 0) {
237
+ // All workers limited — pick the one that was limited longest ago
238
+ const sorted = [..._workerPool].sort(
239
+ (a, b) => _workerHealth.get(a.name).limitedAt - _workerHealth.get(b.name).limitedAt,
240
+ );
241
+ console.log(`[CLIRouter] ALL LIMITED — trying oldest-limited: ${sorted[0].name}`);
242
+ return sorted[0];
243
+ }
244
+
245
+ if (healthy.length === 1) {
246
+ return healthy[0];
247
+ }
248
+
249
+ // Degraded mode: only use primary
250
+ if (!_loadBalanceMode) {
251
+ const primary = healthy.find((w) => w.name === PRIMARY_WORKER);
252
+ return primary || healthy[0];
253
+ }
254
+
255
+ // --- Least-connections is primary strategy ---
256
+ // Session affinity is only a tiebreaker when workers have equal load.
257
+ const least = leastLoadedWorker(healthy);
258
+ const leastConns = _activeConns.get(least.name) || 0;
259
+
260
+ if (sessionKey) {
261
+ const aff = sessionAffinity.lookup(sessionKey, isHealthy);
262
+ if (aff?.hit) {
263
+ const affinityWorker = _workerPool.find((w) => w.name === aff.workerName);
264
+ if (affinityWorker) {
265
+ const affConns = _activeConns.get(affinityWorker.name) || 0;
266
+ // Use affinity only if it's strictly less loaded (not just equal)
267
+ if (affConns < leastConns) return affinityWorker;
268
+ }
269
+ }
270
+ }
271
+
272
+ return least;
273
+ }
274
+
275
+ function markWorkerLimited(workerName) {
276
+ const h = _workerHealth.get(workerName);
277
+ if (h && !h.limited) {
278
+ h.limited = true;
279
+ h.limitedAt = Date.now();
280
+ _loadBalanceMode = false; // back to single-worker mode
281
+ const other = _workerPool.find((w) => w.name !== workerName);
282
+ console.log(`[CLIRouter] ${workerName} RATE LIMITED — switching all traffic to ${other?.name || "?"}`);
283
+ eventLog.push("worker_limited", { worker: workerName, switchedTo: other?.name });
284
+ }
285
+ }
286
+
287
+ function markWorkerRecovered(workerName) {
288
+ const h = _workerHealth.get(workerName);
289
+ if (h && h.limited) {
290
+ h.limited = false;
291
+ h.limitedAt = 0;
292
+ _loadBalanceMode = true; // both workers healthy → share the load
293
+ console.log(`[CLIRouter] ${workerName} RECOVERED — entering load-balance mode (round-robin)`);
294
+ eventLog.push("worker_recovered", { worker: workerName, loadBalance: true });
295
+ }
296
+ }
297
+
298
+ function isRateLimitError(exitCode, stderr) {
299
+ if (!stderr) return false;
300
+ const lower = stderr.toLowerCase();
301
+ return (
302
+ lower.includes("rate limit") ||
303
+ lower.includes("429") ||
304
+ lower.includes("too many requests") ||
305
+ lower.includes("overloaded") ||
306
+ lower.includes("you've hit your limit")
307
+ );
308
+ }
309
+
310
+ // Health check timer: every HEALTH_CHECK_MS, try to recover limited workers
311
+ setInterval(() => {
312
+ for (const w of _workerPool) {
313
+ const h = _workerHealth.get(w.name);
314
+ if (h.limited && Date.now() - h.limitedAt >= HEALTH_CHECK_MS) {
315
+ console.log(`[CLIRouter] Health check: ${w.name} cooldown expired (${Math.round((Date.now() - h.limitedAt) / 1000)}s) — marking recovered`);
316
+ markWorkerRecovered(w.name);
317
+ }
318
+ }
319
+ }, Math.min(HEALTH_CHECK_MS, 60000)); // Check at least every 60s
320
+
321
+ // Whitelist of env vars safe to pass to CLI workers.
322
+ // Everything else is blocked to prevent parent-process leakage
323
+ // (e.g. CLAUDECODE causing "nested session" crash).
324
+ const WORKER_ENV_WHITELIST = new Set([
325
+ // System essentials
326
+ "PATH", "HOME", "USER", "LOGNAME", "SHELL", "LANG", "LC_ALL", "LC_CTYPE",
327
+ "TMPDIR", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME",
328
+ // Node.js
329
+ "NODE_PATH", "NODE_OPTIONS", "NODE_EXTRA_CA_CERTS",
330
+ // SSH (agent forwarding, keys)
331
+ "SSH_AUTH_SOCK", "SSH_AGENT_PID",
332
+ // Proxy/network
333
+ "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy",
334
+ // Anthropic (will be overridden per-worker below)
335
+ "ANTHROPIC_API_KEY",
336
+ ]);
337
+
338
+ function workerEnv(worker) {
339
+ // Start from a clean env — only whitelisted vars from parent process
340
+ const env = {};
341
+ for (const key of WORKER_ENV_WHITELIST) {
342
+ if (process.env[key] !== undefined) env[key] = process.env[key];
343
+ }
344
+ // Ensure /opt/homebrew/bin + wrapper scripts dir are in PATH
345
+ const path = env.PATH || "/usr/bin:/bin";
346
+ const homeDir = process.env.HOME || "/Users/duke_nukem_opcdbase";
347
+ const extraPaths = [`${homeDir}/.openclaw/bin`, "/opt/homebrew/bin"];
348
+ let finalPath = path;
349
+ for (const p of extraPaths) {
350
+ if (!finalPath.includes(p)) finalPath = `${p}:${finalPath}`;
351
+ }
352
+ env.PATH = finalPath;
353
+ // Per-worker OAuth token (overrides any inherited value)
354
+ if (worker.token) {
355
+ env.CLAUDE_CODE_OAUTH_TOKEN = worker.token;
356
+ }
357
+ // Headless / non-interactive mode — prevent ALL macOS interactive prompts
358
+ env.CI = "true"; // suppress macOS permission popups
359
+ env.TERM_PROGRAM = "dumb"; // skip terminal-specific osascript detection
360
+ env.TERM = "dumb"; // reinforce non-interactive terminal
361
+ env.NO_COLOR = "1"; // no ANSI escape codes
362
+ env.ELECTRON_NO_ATTACH_CONSOLE = "1"; // suppress Electron console
363
+ env.ELECTRON_RUN_AS_NODE = "1"; // skip Electron UI/keychain integration
364
+ env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1"; // skip telemetry/updates
365
+ env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY = "1"; // skip interactive surveys
366
+ // Prevent macOS Keychain access prompts: if safeStorage is attempted,
367
+ // the OS prompts because node isn't in the Keychain ACL.
368
+ // Setting ELECTRON_RUN_AS_NODE bypasses Electron's safeStorage layer.
369
+ return env;
370
+ }
371
+ const MAX_CONCURRENT = parseInt(process.env.MAX_CONCURRENT || "10", 10);
372
+ const MAX_QUEUE_TOTAL = parseInt(process.env.MAX_QUEUE_TOTAL || "200", 10);
373
+ const MAX_QUEUE_PER_SOURCE = parseInt(process.env.MAX_QUEUE_PER_SOURCE || "50", 10);
374
+ const QUEUE_TIMEOUT_MS = parseInt(process.env.QUEUE_TIMEOUT_MS || "300000", 10);
375
+ const MAX_RETRIES = parseInt(process.env.MAX_RETRIES || "3", 10);
376
+ const RETRY_BASE_MS = parseInt(process.env.RETRY_BASE_MS || "2000", 10);
377
+ const STREAM_TIMEOUT_MS = parseInt(process.env.STREAM_TIMEOUT_MS || "1800000", 10); // 30 min (was 10 min)
378
+ const SYNC_TIMEOUT_MS = parseInt(process.env.SYNC_TIMEOUT_MS || "600000", 10); // 10 min (was 5 min)
379
+
380
+ // Per-model heartbeat timeouts — autonomous agents may go silent during tool execution
381
+ const HEARTBEAT_BY_MODEL = Object.freeze({
382
+ opus: 1800000, // 30 min — long tool chains (SSH compile, multi-step ops)
383
+ sonnet: 1200000, // 20 min
384
+ haiku: 600000, // 10 min
385
+ });
386
+ const DEFAULT_HEARTBEAT_MS = 1200000; // 20 min fallback
387
+ const MAX_PROCESS_AGE_MS = parseInt(process.env.MAX_PROCESS_AGE_MS || "1800000", 10); // 30 min (was 10 min)
388
+ const MAX_IDLE_MS = parseInt(process.env.MAX_IDLE_MS || "600000", 10); // 10 min (was 2 min)
389
+ const REAPER_INTERVAL_MS = parseInt(process.env.REAPER_INTERVAL_MS || "15000", 10);
390
+
391
+ // ============================================================
392
+ // Rate Limits — 95% of Claude Max plan limits (shared globally)
393
+ // ============================================================
394
+
395
+ const RATE_LIMITS = {
396
+ sonnet: { requestsPerMin: 57, tokensPerMin: 190000 },
397
+ opus: { requestsPerMin: 28, tokensPerMin: 57000 },
398
+ haiku: { requestsPerMin: 95, tokensPerMin: 380000 },
399
+ };
400
+
401
+ // Model priority mapping
402
+ const MODEL_PRIORITY = { opus: "high", sonnet: "normal", haiku: "low" };
403
+
404
+ // Model name -> CLI flag
405
+ const MODEL_MAP = {
406
+ "claude-code": "sonnet",
407
+ sonnet: "sonnet",
408
+ "sonnet-4.6": "sonnet",
409
+ "claude-sonnet-4-6": "sonnet",
410
+ opus: "opus",
411
+ "opus-4.6": "opus",
412
+ "claude-opus-4-6": "opus",
413
+ haiku: "haiku",
414
+ "haiku-4.5": "haiku",
415
+ "claude-haiku-4-5": "haiku",
416
+ };
417
+
418
+ function resolveModel(model) {
419
+ const stripped = (model || "sonnet").replace("claude-code/", "");
420
+ return MODEL_MAP[stripped] || "sonnet";
421
+ }
422
+
423
+ // ============================================================
424
+ // Redis + Module Instances
425
+ // ============================================================
426
+
427
+ // Redis — connect first, then pass to all modules
428
+ let redis = null;
429
+ try {
430
+ redis = await createRedisClient();
431
+ console.log("[Redis] Connected and ready");
432
+ } catch (err) {
433
+ console.warn(`[Redis] Connection failed: ${err.message} — running in memory-only mode`);
434
+ redis = null;
435
+ }
436
+
437
+ const queue = createFairQueue({
438
+ maxConcurrent: MAX_CONCURRENT,
439
+ maxPerSource: MAX_QUEUE_PER_SOURCE,
440
+ maxTotal: MAX_QUEUE_TOTAL,
441
+ queueTimeoutMs: QUEUE_TIMEOUT_MS,
442
+ maxLeaseMs: STREAM_TIMEOUT_MS + 60_000, // stream timeout + 1 min grace
443
+ });
444
+
445
+ const rateLimiter = createRateLimiter({ limits: RATE_LIMITS, redis });
446
+
447
+ const registry = createProcessRegistry({
448
+ maxProcessAgeMs: MAX_PROCESS_AGE_MS,
449
+ maxIdleMs: MAX_IDLE_MS,
450
+ reaperIntervalMs: REAPER_INTERVAL_MS,
451
+ redis,
452
+ });
453
+
454
+ const retryPolicy = createRetryPolicy({
455
+ maxRetries: MAX_RETRIES,
456
+ baseDelayMs: RETRY_BASE_MS,
457
+ });
458
+
459
+ const eventLog = createEventLog({ maxEvents: 500, redis });
460
+ const tokenTracker = createTokenTracker({ redis });
461
+
462
+ // Metrics store: persistent time-series data for dashboard charts
463
+ const metricsStore = createMetricsStore({ redis });
464
+
465
+ // Session affinity: sticky routing for conversation sessions
466
+ const sessionAffinity = createSessionAffinity({ ttlMs: 5 * 60 * 1000 }); // 5 min — short TTL for better distribution
467
+
468
+ // Wire reaper events into event log + SSE
469
+ registry.onReap((zombie) => {
470
+ const ageS = Math.round(zombie.age / 1000);
471
+ const idleS = Math.round(zombie.idle / 1000);
472
+ eventLog.push("reap", {
473
+ pid: zombie.pid,
474
+ reqId: zombie.requestId,
475
+ model: zombie.model,
476
+ mode: zombie.mode,
477
+ source: zombie.source,
478
+ ageSec: ageS,
479
+ idleSec: idleS,
480
+ });
481
+ sseBroadcast("reap", {
482
+ pid: zombie.pid,
483
+ reqId: zombie.requestId,
484
+ model: zombie.model,
485
+ ageSec: ageS,
486
+ idleSec: idleS,
487
+ });
488
+ });
489
+
490
+ // ============================================================
491
+ // SSE Broadcast — real-time stream to dashboard subscribers
492
+ // ============================================================
493
+
494
+ let sseClients = new Set();
495
+
496
+ function sseBroadcast(event, data) {
497
+ if (sseClients.size === 0) return;
498
+ const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
499
+ for (const client of sseClients) {
500
+ try {
501
+ client.write(payload);
502
+ } catch {
503
+ sseClients = new Set([...sseClients].filter((c) => c !== client));
504
+ }
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Gather current metrics snapshot for the metrics store sampler.
510
+ */
511
+ function gatherMetricsSnapshot() {
512
+ const qs = queue.getStats();
513
+ const rs = registry.getStats();
514
+ const counts = eventLog.getCounts();
515
+ return {
516
+ tokens: tokenTracker.getTotals(),
517
+ tokensByModel: tokenTracker.getByModel(),
518
+ queue: { active: qs.active, totalQueued: qs.totalQueued, metrics: qs.metrics },
519
+ processes: rs,
520
+ liveTokens: rs.liveTokens,
521
+ events: counts,
522
+ sessionAffinity: sessionAffinity.getStats(),
523
+ };
524
+ }
525
+
526
+ // ============================================================
527
+ // Auth & Source identification
528
+ // ============================================================
529
+
530
+ function authenticate(req) {
531
+ const authHeader = req.headers["authorization"] || "";
532
+ const apiKey = req.headers["x-api-key"] || "";
533
+ const bearer = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
534
+
535
+ // Accept any of: Bearer token, x-api-key, or query param
536
+ return bearer === AUTH_TOKEN || apiKey === AUTH_TOKEN || AUTH_TOKEN === "local-proxy";
537
+ }
538
+
539
+ function identifySource(req) {
540
+ // Priority: explicit header > api key > remote IP
541
+ return (
542
+ req.headers["x-openclaw-source"] ||
543
+ req.headers["x-source"] ||
544
+ req.headers["x-api-key"] ||
545
+ req.socket.remoteAddress ||
546
+ "unknown"
547
+ );
548
+ }
549
+
550
+ // ============================================================
551
+ // Claude CLI execution
552
+ // ============================================================
553
+
554
+ // Max prompt characters (~50K chars ≈ ~12K tokens, leaves room for CLI agent's own tool calls).
555
+ // Opus has 200K token context; we reserve most of it for the agent's multi-turn tool execution.
556
+ const MAX_PROMPT_CHARS = parseInt(process.env.MAX_PROMPT_CHARS || "50000", 10);
557
+
558
+ function extractPrompt(messages) {
559
+ if (!Array.isArray(messages) || messages.length === 0) {
560
+ return { prompt: "", systemPrompt: null };
561
+ }
562
+
563
+ let systemPrompt = null;
564
+ const systemMsg = messages.find((m) => m.role === "system" || m.role === "developer");
565
+ if (systemMsg) {
566
+ systemPrompt = typeof systemMsg.content === "string"
567
+ ? systemMsg.content
568
+ : JSON.stringify(systemMsg.content);
569
+ }
570
+
571
+ // Collect all non-system messages
572
+ const allParts = [];
573
+ for (const msg of messages) {
574
+ if (msg.role === "system" || msg.role === "developer") continue;
575
+ const text = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
576
+ if (msg.role === "user") allParts.push(text);
577
+ else if (msg.role === "assistant") allParts.push(`[Previous assistant]: ${text}`);
578
+ }
579
+
580
+ // Truncate from the front (keep recent messages) to fit within MAX_PROMPT_CHARS.
581
+ // Always keep the LAST message (the actual user request).
582
+ let totalLen = 0;
583
+ const kept = [];
584
+ for (let i = allParts.length - 1; i >= 0; i--) {
585
+ const part = allParts[i];
586
+ if (totalLen + part.length > MAX_PROMPT_CHARS && kept.length > 0) {
587
+ // Budget exceeded — prepend a truncation notice and stop
588
+ kept.unshift("[... earlier conversation history truncated ...]");
589
+ break;
590
+ }
591
+ totalLen += part.length;
592
+ kept.unshift(part);
593
+ }
594
+
595
+ return { prompt: kept.join("\n\n"), systemPrompt };
596
+ }
597
+
598
+ function buildCliArgs(prompt, model, systemPrompt, stream) {
599
+ // Pass prompt via stdin (not CLI arg) to avoid OrbStack/OS arg length limits.
600
+ // The `-p` flag without a positional prompt tells Claude CLI to read from stdin.
601
+ // --dangerously-skip-permissions: full autonomous agent, all tools enabled
602
+ // System prompt is prepended to stdin (not --system-prompt CLI arg) to avoid arg length limits.
603
+ const args = ["-p", "--model", model, "--dangerously-skip-permissions"];
604
+ if (stream) {
605
+ args.push("--output-format", "stream-json", "--verbose", "--include-partial-messages");
606
+ } else {
607
+ args.push("--output-format", "text");
608
+ }
609
+ return args;
610
+ }
611
+
612
+ // Build the full stdin payload: system prompt (if any) + user prompt
613
+ function buildStdinPayload(prompt, systemPrompt) {
614
+ if (systemPrompt) {
615
+ return `[System Instructions]\n${systemPrompt}\n\n[User Request]\n${prompt}`;
616
+ }
617
+ return prompt;
618
+ }
619
+
620
+ function runCliOnce(prompt, model, systemPrompt, requestId = "", source = "", workerOverride = null, sessionKey = "") {
621
+ return new Promise((resolve, reject) => {
622
+ const args = buildCliArgs(prompt, model, systemPrompt, false);
623
+ const worker = workerOverride || getNextWorker(sessionKey);
624
+ if (sessionKey) sessionAffinity.assign(sessionKey, worker.name);
625
+ console.log(`[${ts()}] CLIROUTER obj=${worker.name} bin=${worker.bin} reqId=${requestId} model=${model}`);
626
+ recordWorkerRequest(worker.name);
627
+ workerAcquire(worker.name);
628
+ const proc = spawn(worker.bin, args, {
629
+ env: workerEnv(worker),
630
+ stdio: ["pipe", "pipe", "pipe"],
631
+ });
632
+ // Write full payload (system prompt + user prompt) to stdin
633
+ if (proc.stdin) {
634
+ proc.stdin.write(buildStdinPayload(prompt, systemPrompt));
635
+ proc.stdin.end();
636
+ }
637
+
638
+ // Track in process registry
639
+ if (proc.pid) {
640
+ registry.register({
641
+ pid: proc.pid,
642
+ requestId,
643
+ model,
644
+ mode: "sync",
645
+ source,
646
+ worker: `${worker.name}:${worker.bin}`,
647
+ promptPreview: typeof prompt === "string" ? prompt.slice(0, 80) : "[structured]",
648
+ });
649
+ }
650
+
651
+ // Execution timeout — kill if running too long
652
+ const execTimer = setTimeout(() => {
653
+ eventLog.push("timeout", { kind: "sync", pid: proc.pid, reqId: requestId, model });
654
+ recordWorkerError(worker.name, "timeout", `sync_timeout pid=${proc.pid}`);
655
+ console.log(`[${ts()}] SYNC_TIMEOUT pid=${proc.pid} reqId=${requestId} model=${model}`);
656
+ try { proc.kill("SIGTERM"); } catch { /* ignore */ }
657
+ const err = new Error(`Execution timeout after ${SYNC_TIMEOUT_MS}ms`);
658
+ err.exitCode = -1;
659
+ err.workerName = worker.name;
660
+ reject(err);
661
+ }, SYNC_TIMEOUT_MS);
662
+
663
+ let stdout = "";
664
+ let stderr = "";
665
+ proc.stdout.on("data", (d) => {
666
+ stdout += d.toString();
667
+ if (proc.pid) registry.touch(proc.pid);
668
+ });
669
+ proc.stderr.on("data", (d) => { stderr += d.toString(); });
670
+ proc.on("close", (code) => {
671
+ clearTimeout(execTimer);
672
+ workerRelease(worker.name);
673
+ if (proc.pid) registry.unregister(proc.pid);
674
+ // Detect rate limit from stderr
675
+ if (isRateLimitError(code, stderr)) {
676
+ markWorkerLimited(worker.name);
677
+ }
678
+ if (code !== 0) {
679
+ const err = new Error(`CLI exit ${code}: ${stderr}`);
680
+ err.exitCode = code;
681
+ err.workerName = worker.name;
682
+ err.isRateLimit = isRateLimitError(code, stderr);
683
+ reject(err);
684
+ } else {
685
+ resolve(stdout.trim());
686
+ }
687
+ });
688
+ proc.on("error", (err) => {
689
+ clearTimeout(execTimer);
690
+ workerRelease(worker.name);
691
+ if (proc.pid) registry.unregister(proc.pid);
692
+ err.workerName = worker.name;
693
+ reject(err);
694
+ });
695
+ });
696
+ }
697
+
698
+ /**
699
+ * Run CLI with retry + exponential backoff + jitter.
700
+ * Uses retry policy for consistent retry behavior.
701
+ */
702
+ async function runCli(prompt, model, systemPrompt, requestId = "", source = "", sessionKey = "") {
703
+ return retryPolicy.withRetry(
704
+ () => runCliOnce(prompt, model, systemPrompt, requestId, source, null, sessionKey),
705
+ {
706
+ onRetry: (attempt, error, delayMs) => {
707
+ eventLog.push("retry", { reqId: requestId, attempt: attempt + 1, model, delay: delayMs, error: error.message });
708
+ console.log(
709
+ `[${ts()}] RETRY attempt=${attempt + 1}/${MAX_RETRIES} ` +
710
+ `model=${model} delay=${delayMs}ms err=${error.message}`
711
+ );
712
+ },
713
+ },
714
+ );
715
+ }
716
+
717
+ function spawnCliStream(prompt, model, systemPrompt, worker) {
718
+ const args = buildCliArgs(prompt, model, systemPrompt, true);
719
+ const proc = spawn(worker.bin, args, {
720
+ env: workerEnv(worker),
721
+ stdio: ["pipe", "pipe", "pipe"],
722
+ });
723
+ // Write full payload (system prompt + user prompt) to stdin
724
+ if (proc.stdin) {
725
+ proc.stdin.write(buildStdinPayload(prompt, systemPrompt));
726
+ proc.stdin.end();
727
+ }
728
+ proc._workerName = worker.name;
729
+ proc._spawnedAt = Date.now();
730
+ return proc;
731
+ }
732
+
733
+ function trackStreamProc(proc, requestId, model, source, worker) {
734
+ if (proc.pid) {
735
+ registry.register({
736
+ pid: proc.pid,
737
+ requestId,
738
+ model,
739
+ mode: "stream",
740
+ source,
741
+ worker: `${worker.name}:${worker.bin}`,
742
+ promptPreview: "[stream]",
743
+ liveInputTokens: 0,
744
+ liveOutputTokens: 0,
745
+ });
746
+ proc.on("close", () => registry.unregister(proc.pid));
747
+ proc.on("error", () => registry.unregister(proc.pid));
748
+ }
749
+ }
750
+
751
+ // Pick a specific worker by name, or fallback to round-robin
752
+ function getWorkerByName(name) {
753
+ return _workerPool.find((w) => w.name === name) || null;
754
+ }
755
+
756
+ function getAlternateWorker(excludeName) {
757
+ const healthy = _workerPool.filter(
758
+ (w) => w.name !== excludeName && !_workerHealth.get(w.name)?.limited
759
+ );
760
+ return healthy.length > 0 ? healthy[0] : null;
761
+ }
762
+
763
+ // ============================================================
764
+ // Fallback API: stream from an OpenAI-compatible HTTP endpoint
765
+ // Used as last resort when all CLI routers fail
766
+ // ============================================================
767
+
768
+ function streamFromFallbackApi(messages, model, reqId, source, res) {
769
+ const fb = FALLBACK_API;
770
+ const url = new URL(`${fb.baseUrl}/chat/completions`);
771
+ const isHttps = url.protocol === "https:";
772
+ const doRequest = isHttps ? httpsRequest : httpRequest;
773
+
774
+ const body = JSON.stringify({
775
+ model: fb.model,
776
+ messages,
777
+ stream: true,
778
+ });
779
+
780
+ console.log(`[${ts()}] FALLBACK reqId=${reqId} api=${fb.name} model=${fb.model} src=${source}`);
781
+ eventLog.push("fallback", { reqId, model, source, fallbackApi: fb.name, fallbackModel: fb.model });
782
+
783
+ // Safe write helper — prevent writing to already-closed response
784
+ const safeWrite = (data) => { if (!res.writableEnded) res.write(data); };
785
+ const safeEnd = () => { if (!res.writableEnded) res.end(); };
786
+
787
+ const apiReq = doRequest(
788
+ url,
789
+ {
790
+ method: "POST",
791
+ headers: {
792
+ "content-type": "application/json",
793
+ authorization: `Bearer ${fb.apiKey}`,
794
+ "content-length": Buffer.byteLength(body),
795
+ },
796
+ },
797
+ (apiRes) => {
798
+ if (apiRes.statusCode !== 200) {
799
+ let errBody = "";
800
+ apiRes.on("data", (d) => { errBody += d.toString(); });
801
+ apiRes.on("end", () => {
802
+ console.log(`[${ts()}] FALLBACK_ERROR reqId=${reqId} status=${apiRes.statusCode} body=${errBody.slice(0, 200)}`);
803
+ recordWorkerError("fallback", errBody.includes("Context size") ? "context_overflow" : "api_error", `HTTP ${apiRes.statusCode} ${errBody.slice(0, 100)}`);
804
+ safeWrite(sseChunk(reqId, `[Fallback ${fb.name} error: HTTP ${apiRes.statusCode}]`));
805
+ safeWrite(sseChunk(reqId, null, "stop"));
806
+ safeWrite("data: [DONE]\n\n");
807
+ safeEnd();
808
+ });
809
+ return;
810
+ }
811
+
812
+ // Pipe the SSE stream from the fallback API directly to the client
813
+ let buf = "";
814
+ let outputChars = 0;
815
+ apiRes.on("data", (chunk) => {
816
+ buf += chunk.toString();
817
+ const lines = buf.split("\n");
818
+ buf = lines.pop() || "";
819
+ for (const line of lines) {
820
+ const trimmed = line.trim();
821
+ if (!trimmed || trimmed === "data: [DONE]") {
822
+ if (trimmed === "data: [DONE]") {
823
+ safeWrite("data: [DONE]\n\n");
824
+ }
825
+ continue;
826
+ }
827
+ if (trimmed.startsWith("data: ")) {
828
+ try {
829
+ const ev = JSON.parse(trimmed.slice(6));
830
+ const delta = ev.choices?.[0]?.delta?.content;
831
+ const finish = ev.choices?.[0]?.finish_reason;
832
+ if (delta) {
833
+ safeWrite(sseChunk(reqId, delta));
834
+ outputChars += delta.length;
835
+ sseBroadcast("chunk", { reqId, model: fb.model, source, text: delta, tokens: outputChars });
836
+ }
837
+ if (finish) {
838
+ safeWrite(sseChunk(reqId, null, finish));
839
+ }
840
+ } catch { /* skip malformed */ }
841
+ }
842
+ }
843
+ });
844
+ apiRes.on("end", () => {
845
+ tokenTracker.record(reqId, fb.model, 0, Math.ceil(outputChars / 4));
846
+ eventLog.push("complete", { reqId, mode: "fallback", model: fb.model, source, exitCode: 0, outputChars });
847
+ sseBroadcast("complete", { reqId, model: fb.model, source, exitCode: 0 });
848
+ if (outputChars === 0) {
849
+ safeWrite(sseChunk(reqId, `[Fallback ${fb.name}: empty response]`));
850
+ }
851
+ safeWrite(sseChunk(reqId, null, "stop"));
852
+ safeWrite("data: [DONE]\n\n");
853
+ safeEnd();
854
+ });
855
+ },
856
+ );
857
+
858
+ apiReq.on("error", (err) => {
859
+ console.log(`[${ts()}] FALLBACK_NET_ERROR reqId=${reqId} err=${err.message}`);
860
+ safeWrite(sseChunk(reqId, `[Fallback ${fb.name} unreachable: ${err.message}]`));
861
+ safeWrite(sseChunk(reqId, null, "stop"));
862
+ safeWrite("data: [DONE]\n\n");
863
+ safeEnd();
864
+ });
865
+
866
+ apiReq.write(body);
867
+ apiReq.end();
868
+ }
869
+
870
+ // ============================================================
871
+ // OpenAI response formatting
872
+ // ============================================================
873
+
874
+ function sseChunk(id, content, finishReason = null) {
875
+ return `data: ${JSON.stringify({
876
+ id,
877
+ object: "chat.completion.chunk",
878
+ created: Math.floor(Date.now() / 1000),
879
+ model: "claude-code",
880
+ choices: [{ index: 0, delta: content ? { content } : {}, finish_reason: finishReason }],
881
+ })}\n\n`;
882
+ }
883
+
884
+ function completionResponse(id, content, model) {
885
+ return {
886
+ id,
887
+ object: "chat.completion",
888
+ created: Math.floor(Date.now() / 1000),
889
+ model,
890
+ choices: [{ index: 0, message: { role: "assistant", content }, finish_reason: "stop" }],
891
+ usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
892
+ };
893
+ }
894
+
895
+ // ============================================================
896
+ // Anthropic Direct API: format conversion + tool_calls support
897
+ // ============================================================
898
+
899
+ /**
900
+ * Convert OpenAI tool definitions to Anthropic format.
901
+ */
902
+ function convertToolsToAnthropic(openaiTools) {
903
+ if (!openaiTools || !Array.isArray(openaiTools)) return [];
904
+ return openaiTools
905
+ .filter(t => t.type === "function" && t.function)
906
+ .map(t => ({
907
+ name: t.function.name,
908
+ description: t.function.description || "",
909
+ input_schema: t.function.parameters || { type: "object", properties: {} },
910
+ }));
911
+ }
912
+
913
+ /**
914
+ * Convert OpenAI messages to Anthropic Messages API format.
915
+ * Returns { system, messages } — system extracted as separate param.
916
+ */
917
+ function convertMessagesToAnthropic(openaiMessages) {
918
+ let system;
919
+ const rawMsgs = [];
920
+
921
+ for (const msg of openaiMessages) {
922
+ if (msg.role === "system" || msg.role === "developer") {
923
+ const text = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
924
+ system = system ? `${system}\n\n${text}` : text;
925
+ continue;
926
+ }
927
+
928
+ if (msg.role === "user") {
929
+ const content = typeof msg.content === "string"
930
+ ? [{ type: "text", text: msg.content }]
931
+ : Array.isArray(msg.content) ? msg.content : [{ type: "text", text: String(msg.content) }];
932
+ rawMsgs.push({ role: "user", content });
933
+ continue;
934
+ }
935
+
936
+ if (msg.role === "assistant") {
937
+ const content = [];
938
+ if (msg.content) {
939
+ const text = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
940
+ if (text) content.push({ type: "text", text });
941
+ }
942
+ if (Array.isArray(msg.tool_calls)) {
943
+ for (const tc of msg.tool_calls) {
944
+ let input = {};
945
+ try {
946
+ input = typeof tc.function?.arguments === "string"
947
+ ? JSON.parse(tc.function.arguments)
948
+ : (tc.function?.arguments || {});
949
+ } catch { /* use empty object */ }
950
+ content.push({
951
+ type: "tool_use",
952
+ id: tc.id,
953
+ name: tc.function?.name || "unknown",
954
+ input,
955
+ });
956
+ }
957
+ }
958
+ if (content.length > 0) rawMsgs.push({ role: "assistant", content });
959
+ continue;
960
+ }
961
+
962
+ if (msg.role === "tool") {
963
+ rawMsgs.push({
964
+ role: "user",
965
+ content: [{
966
+ type: "tool_result",
967
+ tool_use_id: msg.tool_call_id,
968
+ content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content),
969
+ }],
970
+ });
971
+ continue;
972
+ }
973
+ }
974
+
975
+ // Merge consecutive same-role messages (Anthropic requires alternating roles)
976
+ const messages = [];
977
+ for (const msg of rawMsgs) {
978
+ const prev = messages[messages.length - 1];
979
+ if (prev && prev.role === msg.role) {
980
+ const prevContent = Array.isArray(prev.content)
981
+ ? prev.content : [{ type: "text", text: String(prev.content) }];
982
+ const newContent = Array.isArray(msg.content)
983
+ ? msg.content : [{ type: "text", text: String(msg.content) }];
984
+ prev.content = [...prevContent, ...newContent];
985
+ } else {
986
+ messages.push({
987
+ ...msg,
988
+ content: Array.isArray(msg.content) ? [...msg.content] : msg.content,
989
+ });
990
+ }
991
+ }
992
+
993
+ return { system, messages };
994
+ }
995
+
996
+ /** SSE chunk: initial tool_call with id + name. */
997
+ function sseToolCallStartChunk(id, index, callId, name) {
998
+ return `data: ${JSON.stringify({
999
+ id,
1000
+ object: "chat.completion.chunk",
1001
+ created: Math.floor(Date.now() / 1000),
1002
+ model: "claude-code",
1003
+ choices: [{
1004
+ index: 0,
1005
+ delta: {
1006
+ tool_calls: [{ index, id: callId, type: "function", function: { name, arguments: "" } }],
1007
+ },
1008
+ finish_reason: null,
1009
+ }],
1010
+ })}\n\n`;
1011
+ }
1012
+
1013
+ /** SSE chunk: streaming tool_call argument delta. */
1014
+ function sseToolCallDeltaChunk(id, index, argsDelta) {
1015
+ return `data: ${JSON.stringify({
1016
+ id,
1017
+ object: "chat.completion.chunk",
1018
+ created: Math.floor(Date.now() / 1000),
1019
+ model: "claude-code",
1020
+ choices: [{
1021
+ index: 0,
1022
+ delta: {
1023
+ tool_calls: [{ index, function: { arguments: argsDelta } }],
1024
+ },
1025
+ finish_reason: null,
1026
+ }],
1027
+ })}\n\n`;
1028
+ }
1029
+
1030
+ /** SSE chunk: finish_reason only (no content). */
1031
+ function sseFinishChunk(id, finishReason) {
1032
+ return `data: ${JSON.stringify({
1033
+ id,
1034
+ object: "chat.completion.chunk",
1035
+ created: Math.floor(Date.now() / 1000),
1036
+ model: "claude-code",
1037
+ choices: [{ index: 0, delta: {}, finish_reason: finishReason }],
1038
+ })}\n\n`;
1039
+ }
1040
+
1041
+ /** Non-streaming response with optional tool_calls. */
1042
+ function completionResponseWithTools(id, content, toolCalls, model, usage) {
1043
+ const message = { role: "assistant", content: content || null };
1044
+ if (toolCalls && toolCalls.length > 0) {
1045
+ message.tool_calls = toolCalls.map(tc => ({
1046
+ id: tc.id,
1047
+ type: "function",
1048
+ function: { name: tc.name, arguments: tc.arguments },
1049
+ }));
1050
+ }
1051
+ return {
1052
+ id,
1053
+ object: "chat.completion",
1054
+ created: Math.floor(Date.now() / 1000),
1055
+ model,
1056
+ choices: [{
1057
+ index: 0,
1058
+ message,
1059
+ finish_reason: toolCalls && toolCalls.length > 0 ? "tool_calls" : "stop",
1060
+ }],
1061
+ usage: usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
1062
+ };
1063
+ }
1064
+
1065
+ /**
1066
+ * Stream response from Anthropic Messages API, converting to OpenAI SSE.
1067
+ * Handles text content + tool_use blocks.
1068
+ */
1069
+ function streamFromAnthropicDirect(body, model, reqId, source, res, release, tokenEntry) {
1070
+ const anthropicModel = ANTHROPIC_MODEL_IDS[model] || ANTHROPIC_MODEL_IDS.sonnet;
1071
+ const anthropicTools = body.tools ? convertToolsToAnthropic(body.tools) : [];
1072
+ const { system, messages } = convertMessagesToAnthropic(body.messages);
1073
+
1074
+ const requestBody = {
1075
+ model: anthropicModel,
1076
+ max_tokens: body.max_tokens || 16384,
1077
+ stream: true,
1078
+ messages,
1079
+ };
1080
+ if (system) requestBody.system = system;
1081
+ if (anthropicTools.length > 0) requestBody.tools = anthropicTools;
1082
+ if (body.tool_choice) {
1083
+ if (body.tool_choice === "auto") requestBody.tool_choice = { type: "auto" };
1084
+ else if (body.tool_choice === "none") requestBody.tool_choice = { type: "none" };
1085
+ else if (body.tool_choice === "required") requestBody.tool_choice = { type: "any" };
1086
+ else if (body.tool_choice?.type === "function") {
1087
+ requestBody.tool_choice = { type: "tool", name: body.tool_choice.function.name };
1088
+ }
1089
+ }
1090
+
1091
+ const bodyStr = JSON.stringify(requestBody);
1092
+ const authHeaderName = tokenEntry.type === "oauth_flat" ? "authorization" : "x-api-key";
1093
+ const authHeaderValue = tokenEntry.type === "oauth_flat" ? `Bearer ${tokenEntry.token}` : tokenEntry.token;
1094
+ console.log(
1095
+ `[${ts()}] ANTHROPIC_STREAM reqId=${reqId} model=${anthropicModel} ` +
1096
+ `tools=${anthropicTools.length} msgs=${messages.length} auth=${tokenEntry.type} token=${tokenEntry.name} src=${source}`
1097
+ );
1098
+ eventLog.push("anthropic_direct", {
1099
+ reqId, model: anthropicModel, tools: anthropicTools.length, source, auth: tokenEntry.type, token: tokenEntry.name,
1100
+ });
1101
+
1102
+ const safeWrite = (data) => { if (!res.writableEnded) res.write(data); };
1103
+ const safeEnd = () => { if (!res.writableEnded) res.end(); };
1104
+ let released = false;
1105
+ const doRelease = () => { if (!released) { released = true; release(); } };
1106
+
1107
+ const url = new URL(`${ANTHROPIC_API_BASE}/v1/messages`);
1108
+ const headers = {
1109
+ "content-type": "application/json",
1110
+ "anthropic-version": ANTHROPIC_API_VERSION,
1111
+ ...(tokenEntry.type === "oauth_flat" ? { "anthropic-beta": "oauth-2025-04-20" } : {}),
1112
+ "content-length": String(Buffer.byteLength(bodyStr)),
1113
+ };
1114
+ headers[authHeaderName] = authHeaderValue;
1115
+
1116
+ const apiReq = httpsRequest(url, { method: "POST", headers }, (apiRes) => {
1117
+ if (apiRes.statusCode !== 200) {
1118
+ let errBody = "";
1119
+ apiRes.on("data", (d) => { errBody += d.toString(); });
1120
+ apiRes.on("end", () => {
1121
+ console.log(`[${ts()}] ANTHROPIC_ERROR reqId=${reqId} status=${apiRes.statusCode} body=${errBody.slice(0, 500)}`);
1122
+ eventLog.push("error", { reqId, mode: "anthropic_direct", model, source, status: apiRes.statusCode });
1123
+ safeWrite(sseChunk(reqId, `[Anthropic API error: HTTP ${apiRes.statusCode}]`));
1124
+ safeWrite(sseFinishChunk(reqId, "stop"));
1125
+ safeWrite("data: [DONE]\n\n");
1126
+ safeEnd();
1127
+ doRelease();
1128
+ });
1129
+ return;
1130
+ }
1131
+
1132
+ let buf = "";
1133
+ let toolCallIndex = -1;
1134
+ const toolCalls = [];
1135
+ let inputTokens = 0;
1136
+ let outputTokens = 0;
1137
+
1138
+ apiRes.on("data", (chunk) => {
1139
+ buf += chunk.toString();
1140
+ const lines = buf.split("\n");
1141
+ buf = lines.pop() || "";
1142
+
1143
+ for (const line of lines) {
1144
+ const trimmed = line.trim();
1145
+ if (!trimmed || !trimmed.startsWith("data: ")) continue;
1146
+
1147
+ let ev;
1148
+ try { ev = JSON.parse(trimmed.slice(6)); } catch { continue; }
1149
+
1150
+ if (ev.type === "message_start") {
1151
+ inputTokens = ev.message?.usage?.input_tokens || 0;
1152
+ } else if (ev.type === "content_block_start") {
1153
+ const block = ev.content_block;
1154
+ if (block?.type === "tool_use") {
1155
+ toolCallIndex++;
1156
+ toolCalls.push({ index: toolCallIndex, id: block.id, name: block.name, arguments: "" });
1157
+ safeWrite(sseToolCallStartChunk(reqId, toolCallIndex, block.id, block.name));
1158
+ }
1159
+ // text blocks and thinking blocks: no special start action needed
1160
+ } else if (ev.type === "content_block_delta") {
1161
+ if (ev.delta?.type === "text_delta" && ev.delta.text) {
1162
+ safeWrite(sseChunk(reqId, ev.delta.text));
1163
+ } else if (ev.delta?.type === "input_json_delta" && ev.delta.partial_json !== undefined) {
1164
+ const tc = toolCalls[toolCalls.length - 1];
1165
+ if (tc) {
1166
+ tc.arguments += ev.delta.partial_json;
1167
+ safeWrite(sseToolCallDeltaChunk(reqId, tc.index, ev.delta.partial_json));
1168
+ }
1169
+ }
1170
+ // thinking_delta: skip silently
1171
+ } else if (ev.type === "message_delta") {
1172
+ outputTokens = ev.usage?.output_tokens || outputTokens;
1173
+ const stop = ev.delta?.stop_reason;
1174
+ if (stop) {
1175
+ const finish = stop === "tool_use" ? "tool_calls"
1176
+ : stop === "end_turn" ? "stop"
1177
+ : stop === "max_tokens" ? "length"
1178
+ : "stop";
1179
+ safeWrite(sseFinishChunk(reqId, finish));
1180
+ }
1181
+ }
1182
+ // message_stop, content_block_stop, ping: no action needed
1183
+ }
1184
+ });
1185
+
1186
+ apiRes.on("end", () => {
1187
+ tokenTracker.record(reqId, model, inputTokens, outputTokens);
1188
+ eventLog.push("complete", {
1189
+ reqId, mode: "anthropic_direct", model, source,
1190
+ inputTokens, outputTokens, toolCalls: toolCalls.length,
1191
+ });
1192
+ sseBroadcast("complete", { reqId, model, source, inputTokens, outputTokens });
1193
+ safeWrite("data: [DONE]\n\n");
1194
+ safeEnd();
1195
+ doRelease();
1196
+ });
1197
+
1198
+ apiRes.on("error", (err) => {
1199
+ console.log(`[${ts()}] ANTHROPIC_STREAM_ERR reqId=${reqId} err=${err.message}`);
1200
+ safeWrite(sseChunk(reqId, `[Anthropic stream error: ${err.message}]`));
1201
+ safeWrite("data: [DONE]\n\n");
1202
+ safeEnd();
1203
+ doRelease();
1204
+ });
1205
+ });
1206
+
1207
+ // Client disconnect: abort Anthropic API request
1208
+ res.on("close", () => {
1209
+ if (!apiReq.destroyed) {
1210
+ console.log(`[${ts()}] CLIENT_DISCONNECT reqId=${reqId} — aborting Anthropic API request`);
1211
+ apiReq.destroy();
1212
+ }
1213
+ doRelease();
1214
+ });
1215
+
1216
+ apiReq.on("error", (err) => {
1217
+ console.log(`[${ts()}] ANTHROPIC_NET_ERR reqId=${reqId} err=${err.message}`);
1218
+ eventLog.push("error", { reqId, mode: "anthropic_direct", model, source, error: err.message });
1219
+ safeWrite(sseChunk(reqId, `[Anthropic API unreachable: ${err.message}]`));
1220
+ safeWrite(sseFinishChunk(reqId, "stop"));
1221
+ safeWrite("data: [DONE]\n\n");
1222
+ safeEnd();
1223
+ doRelease();
1224
+ });
1225
+
1226
+ apiReq.write(bodyStr);
1227
+ apiReq.end();
1228
+ }
1229
+
1230
+ /**
1231
+ * Call Anthropic Messages API synchronously (non-streaming).
1232
+ * Returns { content, toolCalls, usage, stopReason }.
1233
+ */
1234
+ function callAnthropicDirect(body, model, reqId, source, tokenEntry) {
1235
+ return new Promise((resolve, reject) => {
1236
+ const anthropicModel = ANTHROPIC_MODEL_IDS[model] || ANTHROPIC_MODEL_IDS.sonnet;
1237
+ const anthropicTools = body.tools ? convertToolsToAnthropic(body.tools) : [];
1238
+ const { system, messages } = convertMessagesToAnthropic(body.messages);
1239
+
1240
+ const requestBody = {
1241
+ model: anthropicModel,
1242
+ max_tokens: body.max_tokens || 16384,
1243
+ messages,
1244
+ };
1245
+ if (system) requestBody.system = system;
1246
+ if (anthropicTools.length > 0) requestBody.tools = anthropicTools;
1247
+
1248
+ const bodyStr = JSON.stringify(requestBody);
1249
+ const authHeaderName = tokenEntry.type === "oauth_flat" ? "authorization" : "x-api-key";
1250
+ const authHeaderValue = tokenEntry.type === "oauth_flat" ? `Bearer ${tokenEntry.token}` : tokenEntry.token;
1251
+ console.log(
1252
+ `[${ts()}] ANTHROPIC_SYNC reqId=${reqId} model=${anthropicModel} ` +
1253
+ `tools=${anthropicTools.length} auth=${tokenEntry.type} token=${tokenEntry.name} src=${source}`
1254
+ );
1255
+
1256
+ const url = new URL(`${ANTHROPIC_API_BASE}/v1/messages`);
1257
+ const headers = {
1258
+ "content-type": "application/json",
1259
+ "anthropic-version": ANTHROPIC_API_VERSION,
1260
+ ...(tokenEntry.type === "oauth_flat" ? { "anthropic-beta": "oauth-2025-04-20" } : {}),
1261
+ "content-length": String(Buffer.byteLength(bodyStr)),
1262
+ };
1263
+ headers[authHeaderName] = authHeaderValue;
1264
+
1265
+ const timer = setTimeout(() => {
1266
+ apiReq.destroy();
1267
+ reject(new Error(`Anthropic API timeout after ${SYNC_TIMEOUT_MS}ms`));
1268
+ }, SYNC_TIMEOUT_MS);
1269
+
1270
+ const apiReq = httpsRequest(url, { method: "POST", headers }, (apiRes) => {
1271
+ let resBody = "";
1272
+ apiRes.on("data", (d) => { resBody += d.toString(); });
1273
+ apiRes.on("end", () => {
1274
+ clearTimeout(timer);
1275
+ if (apiRes.statusCode !== 200) {
1276
+ return reject(new Error(`Anthropic API HTTP ${apiRes.statusCode}: ${resBody.slice(0, 500)}`));
1277
+ }
1278
+ try {
1279
+ const result = JSON.parse(resBody);
1280
+ let textContent = "";
1281
+ const toolCalls = [];
1282
+ for (const block of (result.content || [])) {
1283
+ if (block.type === "text") textContent += block.text;
1284
+ else if (block.type === "tool_use") {
1285
+ toolCalls.push({ id: block.id, name: block.name, arguments: JSON.stringify(block.input) });
1286
+ }
1287
+ }
1288
+ resolve({
1289
+ content: textContent || null,
1290
+ toolCalls,
1291
+ usage: {
1292
+ prompt_tokens: result.usage?.input_tokens || 0,
1293
+ completion_tokens: result.usage?.output_tokens || 0,
1294
+ total_tokens: (result.usage?.input_tokens || 0) + (result.usage?.output_tokens || 0),
1295
+ },
1296
+ stopReason: result.stop_reason,
1297
+ });
1298
+ } catch (err) {
1299
+ reject(new Error(`Failed to parse Anthropic response: ${err.message}`));
1300
+ }
1301
+ });
1302
+ });
1303
+
1304
+ apiReq.on("error", (err) => {
1305
+ clearTimeout(timer);
1306
+ reject(err);
1307
+ });
1308
+
1309
+ apiReq.write(bodyStr);
1310
+ apiReq.end();
1311
+ });
1312
+ }
1313
+
1314
+ /**
1315
+ * Handle ALL requests via direct Anthropic API with token round-robin.
1316
+ * Supports both tool-enabled and text-only requests.
1317
+ */
1318
+ async function handleApiDirect(body, model, stream, source, req, res) {
1319
+ const priority = MODEL_PRIORITY[model] || "normal";
1320
+ const estTokens = Math.min(Math.ceil(JSON.stringify(body.messages).length / 4), 5000);
1321
+ const reqId = `chatcmpl-${randomUUID().replace(/-/g, "").slice(0, 24)}`;
1322
+
1323
+ let release;
1324
+ try {
1325
+ release = await queue.acquire(source, priority);
1326
+ } catch (err) {
1327
+ return sendJson(res, 503, {
1328
+ error: { message: `Queue full: ${err.message}`, type: "queue_full", retry_after_ms: 10000 },
1329
+ }, { "retry-after": "10" });
1330
+ }
1331
+
1332
+ let rateWaitTotal = 0;
1333
+ while (true) {
1334
+ const rateCheck = rateLimiter.check(model, estTokens);
1335
+ if (rateCheck.ok) break;
1336
+ if (rateWaitTotal >= 300000) {
1337
+ release();
1338
+ return sendJson(res, 503, {
1339
+ error: { message: "Rate limit wait exceeded", type: "rate_limit_timeout" },
1340
+ });
1341
+ }
1342
+ const sleepMs = Math.min(rateCheck.waitMs, 5000);
1343
+ await new Promise(r => setTimeout(r, sleepMs));
1344
+ rateWaitTotal += sleepMs;
1345
+ }
1346
+
1347
+ rateLimiter.record(model, estTokens);
1348
+ eventLog.push("request", {
1349
+ reqId, mode: stream ? "stream_tools" : "sync_tools", model, source, priority,
1350
+ toolCount: body.tools?.length || 0,
1351
+ });
1352
+ sseBroadcast("request", {
1353
+ reqId, mode: stream ? "stream_tools" : "sync_tools", model, source, priority,
1354
+ });
1355
+ const tokenEntry = getNextToken();
1356
+ console.log(
1357
+ `[${ts()}] ${stream ? "STREAM" : "SYNC"}_API src=${source} model=${model} ` +
1358
+ `tools=${body.tools?.length || 0} token=${tokenEntry.name} reqId=${reqId}`
1359
+ );
1360
+
1361
+ if (stream) {
1362
+ res.writeHead(200, {
1363
+ "content-type": "text/event-stream",
1364
+ "cache-control": "no-cache",
1365
+ connection: "keep-alive",
1366
+ "x-accel-buffering": "no",
1367
+ });
1368
+ res.flushHeaders();
1369
+ if (res.socket) res.socket.setNoDelay(true);
1370
+
1371
+ streamFromAnthropicDirect(body, model, reqId, source, res, release, tokenEntry);
1372
+ } else {
1373
+ try {
1374
+ const result = await callAnthropicDirect(body, model, reqId, source, tokenEntry);
1375
+ release();
1376
+ tokenTracker.record(reqId, model, result.usage.prompt_tokens, result.usage.completion_tokens);
1377
+ eventLog.push("complete", {
1378
+ reqId, mode: "anthropic_direct_sync", model, source, ...result.usage,
1379
+ });
1380
+ sendJson(res, 200, completionResponseWithTools(
1381
+ reqId, result.content, result.toolCalls, model, result.usage,
1382
+ ));
1383
+ } catch (err) {
1384
+ release();
1385
+ console.error(`[${ts()}] TOOL_REQ_ERROR reqId=${reqId} src=${source} ${err.message}`);
1386
+ eventLog.push("error", { reqId, mode: "anthropic_direct", model, source, error: err.message });
1387
+ sendJson(res, 500, { error: { message: err.message, type: "anthropic_api_error" } });
1388
+ }
1389
+ }
1390
+ }
1391
+
1392
+ // ============================================================
1393
+ // Request handler: /v1/chat/completions
1394
+ // ============================================================
1395
+
1396
+ async function handleCompletions(req, res) {
1397
+ const source = identifySource(req);
1398
+ const chunks = [];
1399
+ for await (const chunk of req) chunks.push(chunk);
1400
+
1401
+ let body;
1402
+ try {
1403
+ body = JSON.parse(Buffer.concat(chunks).toString());
1404
+ } catch {
1405
+ return sendJson(res, 400, { error: { message: "Invalid JSON body" } });
1406
+ }
1407
+
1408
+ const { messages, model: rawModel = "claude-code", stream = false } = body;
1409
+ if (!messages || !Array.isArray(messages)) {
1410
+ return sendJson(res, 400, { error: { message: "messages array required" } });
1411
+ }
1412
+
1413
+ // API direct path: only when CLI agent mode is off and tokens are available
1414
+ if (!USE_CLI_AGENTS && TOKEN_POOL.length > 0) {
1415
+ return handleApiDirect(body, resolveModel(rawModel), stream, source, req, res);
1416
+ }
1417
+
1418
+ const { prompt, systemPrompt } = extractPrompt(messages);
1419
+ if (!prompt) {
1420
+ return sendJson(res, 400, { error: { message: "No user message found" } });
1421
+ }
1422
+
1423
+ // Session affinity: derive a key so the same conversation sticks to the same worker
1424
+ const sessionKey = sessionAffinity.deriveKey({
1425
+ source,
1426
+ sessionId: req.headers["x-session-id"] || "",
1427
+ systemPrompt: systemPrompt || "",
1428
+ });
1429
+
1430
+ const model = resolveModel(rawModel);
1431
+ const priority = MODEL_PRIORITY[model] || "normal";
1432
+ // Estimated tokens for rate-limiter: use a small fixed cap.
1433
+ // chars/4 wildly over-estimates (code/JSON has low token density).
1434
+ // The real rate limit is Anthropic's 429 response; our limiter is
1435
+ // just a courtesy throttle. Cap at 5000 so ~11 opus requests/min
1436
+ // can coexist (57000/5000). If Anthropic 429s, the retry loop handles it.
1437
+ const estTokens = Math.min(Math.ceil(prompt.length / 4), 5000);
1438
+ const reqId = `chatcmpl-${randomUUID().replace(/-/g, "").slice(0, 24)}`;
1439
+
1440
+ // Acquire slot via fair queue (waits for turn, never rejects)
1441
+ let release;
1442
+ try {
1443
+ release = await queue.acquire(source, priority);
1444
+ } catch (err) {
1445
+ // Only rejects if queue is truly full (100+ pending)
1446
+ console.log(`[${ts()}] QUEUE_FULL src=${source} model=${model} ${err.message}`);
1447
+ return sendJson(res, 503, {
1448
+ error: { message: `Queue full, try again shortly: ${err.message}`, type: "queue_full", retry_after_ms: 10000 },
1449
+ }, { "retry-after": "10" });
1450
+ }
1451
+
1452
+ // Wait for rate limit window (sleep instead of rejecting)
1453
+ let rateWaitTotal = 0;
1454
+ const MAX_RATE_WAIT_MS = 300000;
1455
+ while (true) {
1456
+ const rateCheck = rateLimiter.check(model, estTokens);
1457
+ if (rateCheck.ok) break;
1458
+ if (rateWaitTotal >= MAX_RATE_WAIT_MS) {
1459
+ release();
1460
+ console.log(`[${ts()}] RATE_TIMEOUT src=${source} model=${model} waited ${rateWaitTotal}ms`);
1461
+ return sendJson(res, 503, {
1462
+ error: { message: `Rate limit wait exceeded ${MAX_RATE_WAIT_MS}ms`, type: "rate_limit_timeout" },
1463
+ });
1464
+ }
1465
+ const sleepMs = Math.min(rateCheck.waitMs, 5000);
1466
+ console.log(`[${ts()}] RATE_WAIT src=${source} model=${model} sleeping ${sleepMs}ms (${rateCheck.reason})`);
1467
+ await new Promise((r) => setTimeout(r, sleepMs));
1468
+ rateWaitTotal += sleepMs;
1469
+ }
1470
+
1471
+ rateLimiter.record(model, estTokens);
1472
+ eventLog.push("request", { reqId, mode: stream ? "stream" : "sync", model, source, priority });
1473
+ sseBroadcast("request", { reqId, mode: stream ? "stream" : "sync", model, source, priority, promptPreview: prompt.slice(0, 80) });
1474
+ console.log(`[${ts()}] ${stream ? "STREAM" : "SYNC"} src=${source} model=${model} prio=${priority} session=${sessionKey.slice(0, 30)} prompt=${prompt.slice(0, 60)}...`);
1475
+
1476
+ if (stream) {
1477
+ res.writeHead(200, {
1478
+ "content-type": "text/event-stream",
1479
+ "cache-control": "no-cache",
1480
+ connection: "keep-alive",
1481
+ "x-accel-buffering": "no", // hint to reverse proxies: don't buffer
1482
+ });
1483
+ res.flushHeaders(); // force headers out immediately
1484
+ if (res.socket) {
1485
+ res.socket.setNoDelay(true); // disable Nagle — send chunks immediately
1486
+ }
1487
+ // Immediate keepalive: prevents Gateway from timing out while CLI spawns.
1488
+ // Without this, there's a 4-10s gap between headers and first CLI output,
1489
+ // causing ~49% of requests to be disconnected by Gateway.
1490
+ res.write(":proxy-accepted\n\n");
1491
+
1492
+ // Stream with auto-retry: if a worker fails quickly (<5s, no content),
1493
+ // automatically retry on a different worker before giving up.
1494
+ // If ALL CLI routers fail, fall back to the API endpoint (e.g. MiniMax).
1495
+ const QUICK_FAIL_MS = 5000;
1496
+ const MAX_RETRIES = _workerPool.length; // try each router once
1497
+ const inputEstimate = Math.ceil(prompt.length / 4);
1498
+ const originalMessages = messages; // preserve for fallback API
1499
+ let retryCount = 0;
1500
+ const triedRouters = new Set();
1501
+ let activeProc = null; // track current CLI process for client-disconnect cleanup
1502
+
1503
+ // If client disconnects, kill the CLI process to free resources
1504
+ res.on("close", () => {
1505
+ if (activeProc && !activeProc.killed) {
1506
+ console.log(`[${ts()}] CLIENT_DISCONNECT reqId=${reqId} — killing CLI pid=${activeProc.pid}`);
1507
+ try { activeProc.kill("SIGTERM"); } catch { /* ignore */ }
1508
+ }
1509
+ });
1510
+
1511
+ function pipeStream(workerOverride, isRetry) {
1512
+ const worker = workerOverride || getNextWorker(sessionKey);
1513
+ // Bind this session to the chosen worker
1514
+ sessionAffinity.assign(sessionKey, worker.name);
1515
+ triedRouters.add(worker.name);
1516
+ console.log(`[${ts()}] CLIROUTER obj=${worker.name} bin=${worker.bin} reqId=${reqId} model=${model} src=${source}${isRetry ? ` RETRY#${retryCount}` : ""}`);
1517
+ recordWorkerRequest(worker.name);
1518
+ workerAcquire(worker.name);
1519
+ const proc = spawnCliStream(prompt, model, systemPrompt, worker);
1520
+ activeProc = proc; // update for client-disconnect handler
1521
+ trackStreamProc(proc, reqId, model, source, worker);
1522
+
1523
+ let buffer = "";
1524
+ let stderrBuf = "";
1525
+ let sentContent = false;
1526
+ let reqTokens = { input: 0, output: 0 };
1527
+ let outputChars = 0;
1528
+ const spawnedAt = Date.now();
1529
+
1530
+ proc.stderr.on("data", (d) => { stderrBuf += d.toString(); });
1531
+
1532
+ // First-byte warning: if CLI hasn't produced stdout within 8s, log a warning.
1533
+ // This helps diagnose macOS auth dialogs, slow spawns, or keychain prompts.
1534
+ const FIRST_BYTE_WARN_MS = 8_000;
1535
+ const firstByteTimer = setTimeout(() => {
1536
+ console.log(`[${ts()}] SLOW_SPAWN pid=${proc.pid} reqId=${reqId} model=${model} router=${worker.name} elapsed=${FIRST_BYTE_WARN_MS}ms — no stdout yet (possible macOS dialog or slow startup)`);
1537
+ eventLog.push("timeout", { kind: "slow_spawn", pid: proc.pid, reqId, model, source, elapsed: FIRST_BYTE_WARN_MS });
1538
+ }, FIRST_BYTE_WARN_MS);
1539
+
1540
+ const heartbeatMs = HEARTBEAT_BY_MODEL[model] || DEFAULT_HEARTBEAT_MS;
1541
+ let heartbeatTimer = setTimeout(() => {
1542
+ eventLog.push("timeout", { kind: "heartbeat", pid: proc.pid, reqId, model, source, heartbeatMs });
1543
+ console.log(`[${ts()}] HEARTBEAT_TIMEOUT pid=${proc.pid} reqId=${reqId} model=${model} src=${source} limit=${heartbeatMs}ms`);
1544
+ try { proc.kill("SIGTERM"); } catch { /* ignore */ }
1545
+ }, heartbeatMs);
1546
+
1547
+ function resetHeartbeat() {
1548
+ clearTimeout(heartbeatTimer);
1549
+ heartbeatTimer = setTimeout(() => {
1550
+ eventLog.push("timeout", { kind: "heartbeat", pid: proc.pid, reqId, model, source, heartbeatMs });
1551
+ console.log(`[${ts()}] HEARTBEAT_TIMEOUT pid=${proc.pid} reqId=${reqId} model=${model} src=${source} limit=${heartbeatMs}ms`);
1552
+ try { proc.kill("SIGTERM"); } catch { /* ignore */ }
1553
+ }, heartbeatMs);
1554
+ }
1555
+
1556
+ const execTimer = setTimeout(() => {
1557
+ eventLog.push("timeout", { kind: "stream_exec", pid: proc.pid, reqId, model });
1558
+ console.log(`[${ts()}] STREAM_TIMEOUT pid=${proc.pid} reqId=${reqId} model=${model} age=${STREAM_TIMEOUT_MS}ms`);
1559
+ try { proc.kill("SIGTERM"); } catch { /* ignore */ }
1560
+ }, STREAM_TIMEOUT_MS);
1561
+
1562
+ // SSE keepalive: send comment lines to prevent upstream (Gateway) HTTP timeout.
1563
+ // SSE spec allows `:comment\n\n` — client parsers ignore it but the TCP stays alive.
1564
+ // Phase 1: fast keepalive (5s) during CLI startup; Phase 2: slow (30s) after first content.
1565
+ const FAST_KEEPALIVE_MS = 5_000;
1566
+ const SLOW_KEEPALIVE_MS = 30_000;
1567
+ let keepaliveMs = FAST_KEEPALIVE_MS;
1568
+ let keepaliveInterval = setInterval(() => {
1569
+ if (!res.writableEnded) {
1570
+ try { res.write(":keepalive\n\n"); } catch { /* ignore write errors */ }
1571
+ }
1572
+ }, keepaliveMs);
1573
+ function slowDownKeepalive() {
1574
+ if (keepaliveMs === FAST_KEEPALIVE_MS) {
1575
+ keepaliveMs = SLOW_KEEPALIVE_MS;
1576
+ clearInterval(keepaliveInterval);
1577
+ keepaliveInterval = setInterval(() => {
1578
+ if (!res.writableEnded) {
1579
+ try { res.write(":keepalive\n\n"); } catch { /* ignore */ }
1580
+ }
1581
+ }, SLOW_KEEPALIVE_MS);
1582
+ }
1583
+ }
1584
+
1585
+ proc.stdout.on("data", (data) => {
1586
+ clearTimeout(firstByteTimer); // CLI is alive — cancel slow-spawn warning
1587
+ resetHeartbeat();
1588
+ slowDownKeepalive(); // CLI is producing output, switch to slow keepalive
1589
+ buffer += data.toString();
1590
+ const lines = buffer.split("\n");
1591
+ buffer = lines.pop() || "";
1592
+ for (const line of lines) {
1593
+ if (!line.trim()) continue;
1594
+ try {
1595
+ const ev = JSON.parse(line);
1596
+ const canWrite = !res.writableEnded;
1597
+ // stream_event: incremental deltas from --include-partial-messages
1598
+ if (ev.type === "stream_event" && ev.event?.type === "content_block_delta") {
1599
+ const text = ev.event.delta?.text;
1600
+ if (text) {
1601
+ if (canWrite) res.write(sseChunk(reqId, text));
1602
+ outputChars += text.length;
1603
+ sentContent = true;
1604
+ sseBroadcast("chunk", { reqId, model, source, text, tokens: outputChars });
1605
+ }
1606
+ } else if (ev.type === "stream_event" && ev.event?.type === "message_delta") {
1607
+ const usage = ev.event.usage;
1608
+ if (usage) {
1609
+ // Total input = non-cached + cache-created + cache-read
1610
+ const totalInput = (usage.input_tokens || 0)
1611
+ + (usage.cache_creation_input_tokens || 0)
1612
+ + (usage.cache_read_input_tokens || 0);
1613
+ reqTokens = { input: totalInput, output: usage.output_tokens || 0 };
1614
+ }
1615
+ } else if (ev.type === "assistant" && ev.message?.content) {
1616
+ if (!sentContent) {
1617
+ for (const b of ev.message.content) {
1618
+ if (b.type === "text" && b.text) {
1619
+ if (canWrite) res.write(sseChunk(reqId, b.text));
1620
+ outputChars += b.text.length;
1621
+ sentContent = true;
1622
+ sseBroadcast("chunk", { reqId, model, source, text: b.text, tokens: outputChars });
1623
+ }
1624
+ }
1625
+ }
1626
+ } else if (ev.type === "content_block_delta" && ev.delta?.text) {
1627
+ if (canWrite) res.write(sseChunk(reqId, ev.delta.text));
1628
+ outputChars += ev.delta.text.length;
1629
+ sentContent = true;
1630
+ sseBroadcast("chunk", { reqId, model, source, text: ev.delta.text, tokens: outputChars });
1631
+ } else if (ev.type === "result" && ev.result && !sentContent) {
1632
+ if (canWrite) res.write(sseChunk(reqId, ev.result));
1633
+ sentContent = true;
1634
+ }
1635
+ // Capture token usage (include cached tokens in total input)
1636
+ const usage = ev.usage || ev.message?.usage;
1637
+ if (usage) {
1638
+ const totalInput = (usage.input_tokens || usage.prompt_tokens || 0)
1639
+ + (usage.cache_creation_input_tokens || 0)
1640
+ + (usage.cache_read_input_tokens || 0);
1641
+ reqTokens = {
1642
+ input: totalInput,
1643
+ output: usage.output_tokens || usage.completion_tokens || 0,
1644
+ };
1645
+ }
1646
+ } catch { /* non-JSON line, skip */ }
1647
+ }
1648
+ if (proc.pid) {
1649
+ const liveInput = reqTokens.input > 0 ? reqTokens.input : inputEstimate;
1650
+ const liveOutput = reqTokens.output > 0 ? reqTokens.output : Math.ceil(outputChars / 4);
1651
+ registry.touch(proc.pid, { liveInputTokens: liveInput, liveOutputTokens: liveOutput });
1652
+ }
1653
+ });
1654
+
1655
+ proc.on("close", (code) => {
1656
+ clearTimeout(firstByteTimer);
1657
+ clearTimeout(heartbeatTimer);
1658
+ clearTimeout(execTimer);
1659
+ clearInterval(keepaliveInterval);
1660
+ workerRelease(worker.name);
1661
+
1662
+ // Quick-fail auto-retry: if worker failed fast with no content, try another
1663
+ const elapsed = Date.now() - proc._spawnedAt;
1664
+ if (code !== 0 && !sentContent && elapsed < QUICK_FAIL_MS && retryCount < MAX_RETRIES) {
1665
+ // Find an untried router, or any alternate
1666
+ const untried = _workerPool.find(
1667
+ (w) => !triedRouters.has(w.name) && !_workerHealth.get(w.name)?.limited
1668
+ );
1669
+ const alt = untried || getAlternateWorker(worker.name);
1670
+ if (alt) {
1671
+ retryCount++;
1672
+ console.log(`[${ts()}] STREAM_RETRY reqId=${reqId} failedRouter=${worker.name} code=${code} elapsed=${elapsed}ms -> retrying on ${alt.name} (attempt ${retryCount}/${MAX_RETRIES})`);
1673
+ recordWorkerError(worker.name, "stream_retry", `code=${code} elapsed=${elapsed}ms`);
1674
+ eventLog.push("retry", { reqId, model, source, failedWorker: worker.name, retryWorker: alt.name, code, elapsed, retryCount });
1675
+ pipeStream(alt, true);
1676
+ return; // don't finalize response — retry will handle it
1677
+ }
1678
+ }
1679
+
1680
+ release();
1681
+ if (code !== 0) {
1682
+ const diag = stderrBuf.trim() || buffer.trim().slice(0, 200) || "(no output)";
1683
+ console.log(`[${ts()}] CLI_EXIT reqId=${reqId} code=${code} sent=${sentContent} router=${worker.name} stderr=${diag.slice(0, 300)}`);
1684
+ const errCat = code === 143 ? "cli_killed" : "cli_crash";
1685
+ recordWorkerError(worker.name, errCat, `code=${code} ${diag.slice(0, 100)}`);
1686
+ }
1687
+ if (proc._workerName && (isRateLimitError(code, stderrBuf) || (!sentContent && isRateLimitError(code, buffer)))) {
1688
+ markWorkerLimited(proc._workerName);
1689
+ }
1690
+ // Flush remaining buffer
1691
+ const canWrite = !res.writableEnded;
1692
+ if (buffer.trim()) {
1693
+ try {
1694
+ const ev = JSON.parse(buffer);
1695
+ if (ev.type === "assistant" && ev.message?.content) {
1696
+ for (const b of ev.message.content) {
1697
+ if (b.type === "text" && b.text && canWrite) res.write(sseChunk(reqId, b.text));
1698
+ }
1699
+ } else if (ev.type === "result" && ev.result && !sentContent && canWrite) {
1700
+ res.write(sseChunk(reqId, ev.result));
1701
+ }
1702
+ const usage = ev.usage || ev.message?.usage;
1703
+ if (usage) {
1704
+ const totalInput = (usage.input_tokens || usage.prompt_tokens || 0)
1705
+ + (usage.cache_creation_input_tokens || 0)
1706
+ + (usage.cache_read_input_tokens || 0);
1707
+ reqTokens = { input: totalInput, output: usage.output_tokens || usage.completion_tokens || 0 };
1708
+ }
1709
+ } catch { /* ignore */ }
1710
+ }
1711
+ const finalInput = reqTokens.input > 0 ? reqTokens.input : inputEstimate;
1712
+ const finalOutput = reqTokens.output > 0 ? reqTokens.output : Math.ceil(outputChars / 4);
1713
+ tokenTracker.record(reqId, model, finalInput, finalOutput);
1714
+ eventLog.push("complete", {
1715
+ reqId, mode: "stream", model, source, exitCode: code,
1716
+ inputTokens: finalInput, outputTokens: finalOutput,
1717
+ });
1718
+ sseBroadcast("complete", { reqId, model, source, exitCode: code, inputTokens: finalInput, outputTokens: finalOutput });
1719
+ if (code !== 0 && !sentContent) {
1720
+ // All CLI routers failed — fall back to API endpoint
1721
+ console.log(`[${ts()}] ALL_CLI_FAILED reqId=${reqId} retryCount=${retryCount} -> falling back to ${FALLBACK_API.name}`);
1722
+ streamFromFallbackApi(originalMessages, model, reqId, source, res);
1723
+ return; // fallback handles res.end()
1724
+ }
1725
+ if (canWrite) {
1726
+ res.write(sseChunk(reqId, null, "stop"));
1727
+ res.write("data: [DONE]\n\n");
1728
+ res.end();
1729
+ }
1730
+ });
1731
+
1732
+ proc.on("error", (err) => {
1733
+ clearTimeout(firstByteTimer);
1734
+ clearTimeout(heartbeatTimer);
1735
+ clearTimeout(execTimer);
1736
+ clearInterval(keepaliveInterval);
1737
+ workerRelease(worker.name);
1738
+ // Quick-fail auto-retry on spawn error too
1739
+ if (!sentContent && retryCount < MAX_RETRIES) {
1740
+ const untried = _workerPool.find(
1741
+ (w) => !triedRouters.has(w.name) && !_workerHealth.get(w.name)?.limited
1742
+ );
1743
+ const alt = untried || getAlternateWorker(worker.name);
1744
+ if (alt) {
1745
+ retryCount++;
1746
+ console.log(`[${ts()}] STREAM_RETRY reqId=${reqId} failedRouter=${worker.name} error=${err.message} -> retrying on ${alt.name} (attempt ${retryCount}/${MAX_RETRIES})`);
1747
+ pipeStream(alt, true);
1748
+ return;
1749
+ }
1750
+ }
1751
+ release();
1752
+ // All CLI routers errored — fall back to API endpoint
1753
+ console.log(`[${ts()}] ALL_CLI_FAILED reqId=${reqId} error=${err.message} -> falling back to ${FALLBACK_API.name}`);
1754
+ streamFromFallbackApi(originalMessages, model, reqId, source, res);
1755
+ });
1756
+ }
1757
+
1758
+ // Start the stream pipeline (first attempt, no retry flag)
1759
+ pipeStream(null, false);
1760
+ } else {
1761
+ try {
1762
+ const result = await runCli(prompt, model, systemPrompt, reqId, source, sessionKey);
1763
+ release();
1764
+ // Estimate tokens for sync: prompt chars/4 for input, result chars/4 for output
1765
+ const syncInputTokens = Math.ceil(prompt.length / 4);
1766
+ const syncOutputTokens = Math.ceil(result.length / 4);
1767
+ tokenTracker.record(reqId, model, syncInputTokens, syncOutputTokens);
1768
+ eventLog.push("complete", {
1769
+ reqId, mode: "sync", model, source,
1770
+ inputTokens: syncInputTokens, outputTokens: syncOutputTokens,
1771
+ });
1772
+ sendJson(res, 200, completionResponse(reqId, result, model));
1773
+ } catch (err) {
1774
+ release();
1775
+ eventLog.push("error", { reqId, mode: "sync", model, source, error: err.message });
1776
+ console.error(`[${ts()}] ERROR src=${source} ${err.message}`);
1777
+ sendJson(res, 500, { error: { message: err.message, type: "internal_error" } });
1778
+ }
1779
+ }
1780
+ }
1781
+
1782
+ // ============================================================
1783
+ // Other endpoints
1784
+ // ============================================================
1785
+
1786
+ function handleModels(req, res) {
1787
+ const models = Object.keys(MODEL_MAP).map((id) => ({
1788
+ id: `claude-code/${id}`,
1789
+ object: "model",
1790
+ created: Math.floor(Date.now() / 1000),
1791
+ owned_by: "claude-code-proxy",
1792
+ }));
1793
+ sendJson(res, 200, { object: "list", data: models });
1794
+ }
1795
+
1796
+ function handleHealth(req, res) {
1797
+ const qs = queue.getStats();
1798
+ const rs = registry.getStats();
1799
+ const workers = _workerPool.map((w) => {
1800
+ const h = _workerHealth.get(w.name);
1801
+ return {
1802
+ name: w.name,
1803
+ bin: w.bin,
1804
+ limited: h.limited,
1805
+ limitedAt: h.limitedAt || null,
1806
+ limitedAgoSec: h.limited ? Math.round((Date.now() - h.limitedAt) / 1000) : null,
1807
+ };
1808
+ });
1809
+ sendJson(res, 200, {
1810
+ status: "ok",
1811
+ version: "0.5.1",
1812
+ claude_bin: CLAUDE_BIN,
1813
+ port: PORT,
1814
+ redis: redis ? { connected: redis.isReady() } : { connected: false },
1815
+ cliRouters: workers,
1816
+ primaryRouter: PRIMARY_WORKER,
1817
+ queue: { active: qs.active, queued: qs.totalQueued, max: qs.maxConcurrent, sources: qs.sourceCount },
1818
+ processes: { tracked: rs.total, byMode: rs.byMode, liveTokens: rs.liveTokens },
1819
+ tokens: tokenTracker.getTotals(),
1820
+ sessionAffinity: sessionAffinity.getStats(),
1821
+ workerStats,
1822
+ });
1823
+ }
1824
+
1825
+ function handleMetrics(req, res) {
1826
+ const qs = queue.getStats();
1827
+ const rs = registry.getStats();
1828
+ const workers = _workerPool.map((w) => {
1829
+ const h = _workerHealth.get(w.name);
1830
+ return { name: w.name, limited: h.limited, limitedAt: h.limitedAt || null };
1831
+ });
1832
+ sendJson(res, 200, {
1833
+ rateLimits: RATE_LIMITS,
1834
+ rateUsage: rateLimiter.stats(),
1835
+ tokens: tokenTracker.getStats(),
1836
+ cliRouters: workers,
1837
+ loadBalanceMode: _loadBalanceMode,
1838
+ primaryRouter: PRIMARY_WORKER,
1839
+ queue: qs,
1840
+ processes: rs,
1841
+ config: {
1842
+ useCliAgents: USE_CLI_AGENTS,
1843
+ workerCount: _workerPool.length,
1844
+ loadBalanceAlgorithm: "least-connections",
1845
+ maxConcurrent: MAX_CONCURRENT,
1846
+ maxQueueTotal: MAX_QUEUE_TOTAL,
1847
+ maxQueuePerSource: MAX_QUEUE_PER_SOURCE,
1848
+ queueTimeoutMs: QUEUE_TIMEOUT_MS,
1849
+ heartbeatByModel: HEARTBEAT_BY_MODEL,
1850
+ defaultHeartbeatMs: DEFAULT_HEARTBEAT_MS,
1851
+ streamTimeoutMs: STREAM_TIMEOUT_MS,
1852
+ syncTimeoutMs: SYNC_TIMEOUT_MS,
1853
+ maxProcessAgeMs: MAX_PROCESS_AGE_MS,
1854
+ maxIdleMs: MAX_IDLE_MS,
1855
+ reaperIntervalMs: REAPER_INTERVAL_MS,
1856
+ sessionAffinityTtlMs: 5 * 60 * 1000,
1857
+ sseKeepaliveMs: 30_000,
1858
+ maxRetries: MAX_RETRIES,
1859
+ retryBaseMs: RETRY_BASE_MS,
1860
+ },
1861
+ sessionAffinity: sessionAffinity.getStats(),
1862
+ workerStats,
1863
+ activeConnections: Object.fromEntries(_activeConns),
1864
+ });
1865
+ }
1866
+
1867
+ function handleZombies(req, res) {
1868
+ const zombies = registry.getZombies();
1869
+ const qs = queue.getStats();
1870
+ sendJson(res, 200, {
1871
+ processes: registry.getAll(),
1872
+ zombies,
1873
+ stats: registry.getStats(),
1874
+ activeLeases: qs.activeLeases,
1875
+ });
1876
+ }
1877
+
1878
+ async function handleKillZombie(req, res) {
1879
+ const body = await readBody(req);
1880
+ let parsed;
1881
+ try {
1882
+ parsed = JSON.parse(body);
1883
+ } catch {
1884
+ return sendJson(res, 400, { error: { message: "Invalid JSON body" } });
1885
+ }
1886
+
1887
+ const { pid } = parsed;
1888
+ if (!pid) return sendJson(res, 400, { error: { message: "pid required" } });
1889
+
1890
+ const result = registry.kill(Number(pid));
1891
+ eventLog.push("kill", { pid: Number(pid), manual: true });
1892
+ sendJson(res, 200, { result });
1893
+ }
1894
+
1895
+ function handleEvents(req, res, url) {
1896
+ const sinceId = parseInt(url.searchParams.get("since_id") || "0", 10);
1897
+ const limit = parseInt(url.searchParams.get("limit") || "50", 10);
1898
+ const type = url.searchParams.get("type") || null;
1899
+ const events = eventLog.getRecent({ sinceId, limit, type });
1900
+ sendJson(res, 200, { events, counts: eventLog.getCounts() });
1901
+ }
1902
+
1903
+ function handleMetricsHistory(req, res, url) {
1904
+ const window = url.searchParams.get("window") || "1h";
1905
+ const validWindows = ["1h", "6h", "1d", "7d"];
1906
+ if (!validWindows.includes(window)) {
1907
+ return sendJson(res, 400, { error: { message: `Invalid window. Use: ${validWindows.join(", ")}` } });
1908
+ }
1909
+ const points = metricsStore.query(window);
1910
+ sendJson(res, 200, { window, points, count: points.length, bufferSize: metricsStore.getBufferSize() });
1911
+ }
1912
+
1913
+ async function handlePortal(req, res) {
1914
+ try {
1915
+ const html = await readFile(join(__dirname, "portal.html"), "utf-8");
1916
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
1917
+ res.end(html);
1918
+ } catch (err) {
1919
+ sendJson(res, 500, { error: { message: "Portal file not found: " + err.message } });
1920
+ }
1921
+ }
1922
+
1923
+ async function handleProxyDashboard(req, res) {
1924
+ try {
1925
+ const html = await readFile(join(__dirname, "dashboard.html"), "utf-8");
1926
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
1927
+ res.end(html);
1928
+ } catch (err) {
1929
+ sendJson(res, 500, { error: { message: "Dashboard file not found: " + err.message } });
1930
+ }
1931
+ }
1932
+
1933
+ // ============================================================
1934
+ // Utilities
1935
+ // ============================================================
1936
+
1937
+ function ts() {
1938
+ return new Date().toISOString();
1939
+ }
1940
+
1941
+ function sendJson(res, status, body, extraHeaders = {}) {
1942
+ res.writeHead(status, { "content-type": "application/json", ...extraHeaders });
1943
+ res.end(JSON.stringify(body));
1944
+ }
1945
+
1946
+ function readBody(req) {
1947
+ return new Promise((resolve) => {
1948
+ const chunks = [];
1949
+ req.on("data", (c) => chunks.push(c));
1950
+ req.on("end", () => resolve(Buffer.concat(chunks).toString()));
1951
+ });
1952
+ }
1953
+
1954
+ function handleSSEStream(req, res) {
1955
+ res.writeHead(200, {
1956
+ "content-type": "text/event-stream",
1957
+ "cache-control": "no-cache",
1958
+ "connection": "keep-alive",
1959
+ });
1960
+ res.write("event: connected\ndata: {}\n\n");
1961
+
1962
+ sseClients = new Set([...sseClients, res]);
1963
+ console.log(`[${ts()}] SSE_CLIENT connected (${sseClients.size} total)`);
1964
+
1965
+ req.on("close", () => {
1966
+ sseClients = new Set([...sseClients].filter((c) => c !== res));
1967
+ console.log(`[${ts()}] SSE_CLIENT disconnected (${sseClients.size} total)`);
1968
+ });
1969
+ }
1970
+
1971
+ // ============================================================
1972
+ // HTTP Server
1973
+ // ============================================================
1974
+
1975
+ const server = createServer(async (req, res) => {
1976
+ const url = new URL(req.url, `http://0.0.0.0:${PORT}`);
1977
+
1978
+ res.setHeader("access-control-allow-origin", "*");
1979
+ res.setHeader("access-control-allow-methods", "GET, POST, OPTIONS");
1980
+ res.setHeader("access-control-allow-headers", "content-type, authorization, x-api-key, x-openclaw-source, x-source, x-session-id");
1981
+
1982
+ if (req.method === "OPTIONS") {
1983
+ res.writeHead(204);
1984
+ res.end();
1985
+ return;
1986
+ }
1987
+
1988
+ // Auth check (skip for health, dashboard, events)
1989
+ const noAuthPaths = ["/health", "/dashboard", "/dashboard/", "/dashboard/proxy", "/dashboard/proxy/", "/events", "/metrics/history", "/stream"];
1990
+ if (!noAuthPaths.includes(url.pathname) && !authenticate(req)) {
1991
+ return sendJson(res, 401, { error: { message: "Unauthorized" } });
1992
+ }
1993
+
1994
+ try {
1995
+ if (url.pathname === "/v1/chat/completions" && req.method === "POST") {
1996
+ await handleCompletions(req, res);
1997
+ } else if (url.pathname === "/v1/models" && req.method === "GET") {
1998
+ handleModels(req, res);
1999
+ } else if (url.pathname === "/health" && req.method === "GET") {
2000
+ handleHealth(req, res);
2001
+ } else if (url.pathname === "/metrics" && req.method === "GET") {
2002
+ handleMetrics(req, res);
2003
+ } else if (url.pathname === "/rate-limits" && req.method === "GET") {
2004
+ handleMetrics(req, res); // backward compat
2005
+ } else if (url.pathname === "/zombies" && req.method === "GET") {
2006
+ handleZombies(req, res);
2007
+ } else if (url.pathname === "/zombies" && req.method === "POST") {
2008
+ await handleKillZombie(req, res);
2009
+ } else if (url.pathname === "/events" && req.method === "GET") {
2010
+ handleEvents(req, res, url);
2011
+ } else if (url.pathname === "/metrics/history" && req.method === "GET") {
2012
+ handleMetricsHistory(req, res, url);
2013
+ } else if (url.pathname === "/stream" && req.method === "GET") {
2014
+ handleSSEStream(req, res);
2015
+ } else if (url.pathname === "/dashboard/proxy" || url.pathname === "/dashboard/proxy/") {
2016
+ await handleProxyDashboard(req, res);
2017
+ } else if (url.pathname === "/dashboard" || url.pathname === "/dashboard/") {
2018
+ await handlePortal(req, res);
2019
+ } else {
2020
+ sendJson(res, 404, { error: { message: "Not found" } });
2021
+ }
2022
+ } catch (err) {
2023
+ console.error(`[${ts()}] UNHANDLED ${err.message}`);
2024
+ sendJson(res, 500, { error: { message: "Internal server error" } });
2025
+ }
2026
+ });
2027
+
2028
+ // ============================================================
2029
+ // Graceful shutdown
2030
+ // ============================================================
2031
+
2032
+ function shutdown(signal) {
2033
+ console.log(`[${ts()}] SHUTDOWN signal=${signal}`);
2034
+
2035
+ // Kill all tracked processes
2036
+ const allProcs = registry.getAll();
2037
+ for (const entry of allProcs) {
2038
+ console.log(`[${ts()}] SHUTDOWN_KILL pid=${entry.pid} reqId=${entry.requestId} model=${entry.model}`);
2039
+ registry.kill(entry.pid);
2040
+ }
2041
+
2042
+ registry.destroy();
2043
+ queue.destroy();
2044
+ metricsStore.destroy();
2045
+ sessionAffinity.shutdown();
2046
+
2047
+ // Close Redis connection
2048
+ if (redis) {
2049
+ redis.quit().catch(() => {});
2050
+ }
2051
+
2052
+ server.close(() => {
2053
+ console.log(`[${ts()}] SHUTDOWN complete`);
2054
+ process.exit(0);
2055
+ });
2056
+
2057
+ // Force exit after 5s if graceful close hangs
2058
+ setTimeout(() => {
2059
+ console.error(`[${ts()}] SHUTDOWN forced after 5s timeout`);
2060
+ process.exit(1);
2061
+ }, 5000).unref();
2062
+ }
2063
+
2064
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
2065
+ process.on("SIGINT", () => shutdown("SIGINT"));
2066
+
2067
+ // ============================================================
2068
+ // Start server
2069
+ // ============================================================
2070
+
2071
+ server.listen(PORT, "0.0.0.0", async () => {
2072
+ // Wait for all persistent stores to load from Redis / files
2073
+ await Promise.all([
2074
+ metricsStore.ready,
2075
+ tokenTracker.ready,
2076
+ eventLog.ready,
2077
+ registry.ready,
2078
+ ]);
2079
+
2080
+ eventLog.push("startup", { version: "0.5.0", port: PORT, redis: !!redis });
2081
+
2082
+ // Seed token tracker from all raw metrics snapshots (sums across server restarts)
2083
+ const rawSnapshots = metricsStore.getRawBuffer();
2084
+ tokenTracker.seedFromHistory(rawSnapshots);
2085
+
2086
+ metricsStore.startSampler(gatherMetricsSnapshot);
2087
+
2088
+ console.log(`Claude Code Proxy v0.5.1`);
2089
+ console.log(`Listening on http://0.0.0.0:${PORT}`);
2090
+ console.log(`CLI Routers: ${_workerPool.map((w) => `obj${w.name}=${w.bin}`).join(" | ")} | Primary: obj${PRIMARY_WORKER}`);
2091
+ console.log(`Auth token: ${AUTH_TOKEN === "local-proxy" ? "(open - no auth)" : "(enabled)"}`);
2092
+ console.log(`Concurrent: ${MAX_CONCURRENT} | Queue: ${MAX_QUEUE_TOTAL} total, ${MAX_QUEUE_PER_SOURCE}/source`);
2093
+ console.log(`Queue timeout: ${QUEUE_TIMEOUT_MS}ms`);
2094
+ console.log(`Models: ${Object.keys(MODEL_MAP).join(", ")}`);
2095
+ console.log(`Rate limits: sonnet ${RATE_LIMITS.sonnet.requestsPerMin}/min, opus ${RATE_LIMITS.opus.requestsPerMin}/min, haiku ${RATE_LIMITS.haiku.requestsPerMin}/min`);
2096
+ console.log(`Reaper: age=${MAX_PROCESS_AGE_MS}ms idle=${MAX_IDLE_MS}ms interval=${REAPER_INTERVAL_MS}ms`);
2097
+ console.log(`Timeouts: sync=${SYNC_TIMEOUT_MS}ms stream=${STREAM_TIMEOUT_MS}ms`);
2098
+ console.log(`Heartbeat: opus=${HEARTBEAT_BY_MODEL.opus}ms sonnet=${HEARTBEAT_BY_MODEL.sonnet}ms haiku=${HEARTBEAT_BY_MODEL.haiku}ms`);
2099
+ console.log(`Metrics store: ${metricsStore.getBufferSize()} historical snapshots loaded`);
2100
+ });