@a1hvdy/cc-openclaw 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (491) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +207 -0
  3. package/configs/.gitkeep +0 -0
  4. package/configs/council-reviewer-prompt.md +82 -0
  5. package/configs/council-system-prompt.md +141 -0
  6. package/dist/scripts/bench/ab-harness.d.ts +58 -0
  7. package/dist/scripts/bench/ab-harness.d.ts.map +1 -0
  8. package/dist/scripts/bench/ab-harness.js +78 -0
  9. package/dist/scripts/bench/ab-harness.js.map +1 -0
  10. package/dist/src/channels/adapter.d.ts +103 -0
  11. package/dist/src/channels/adapter.d.ts.map +1 -0
  12. package/dist/src/channels/adapter.js +38 -0
  13. package/dist/src/channels/adapter.js.map +1 -0
  14. package/dist/src/channels/telegram/completion-summary.d.ts +22 -0
  15. package/dist/src/channels/telegram/completion-summary.d.ts.map +1 -0
  16. package/dist/src/channels/telegram/completion-summary.js +186 -0
  17. package/dist/src/channels/telegram/completion-summary.js.map +1 -0
  18. package/dist/src/channels/telegram/error-renderer.d.ts +30 -0
  19. package/dist/src/channels/telegram/error-renderer.d.ts.map +1 -0
  20. package/dist/src/channels/telegram/error-renderer.js +133 -0
  21. package/dist/src/channels/telegram/error-renderer.js.map +1 -0
  22. package/dist/src/channels/telegram/event-reducer.d.ts +34 -0
  23. package/dist/src/channels/telegram/event-reducer.d.ts.map +1 -0
  24. package/dist/src/channels/telegram/event-reducer.js +579 -0
  25. package/dist/src/channels/telegram/event-reducer.js.map +1 -0
  26. package/dist/src/channels/telegram/index.d.ts +14 -0
  27. package/dist/src/channels/telegram/index.d.ts.map +1 -0
  28. package/dist/src/channels/telegram/index.js +14 -0
  29. package/dist/src/channels/telegram/index.js.map +1 -0
  30. package/dist/src/channels/telegram/injector.d.ts +54 -0
  31. package/dist/src/channels/telegram/injector.d.ts.map +1 -0
  32. package/dist/src/channels/telegram/injector.js +200 -0
  33. package/dist/src/channels/telegram/injector.js.map +1 -0
  34. package/dist/src/channels/telegram/live-card.d.ts +230 -0
  35. package/dist/src/channels/telegram/live-card.d.ts.map +1 -0
  36. package/dist/src/channels/telegram/live-card.js +916 -0
  37. package/dist/src/channels/telegram/live-card.js.map +1 -0
  38. package/dist/src/channels/telegram/state-machine.d.ts +23 -0
  39. package/dist/src/channels/telegram/state-machine.d.ts.map +1 -0
  40. package/dist/src/channels/telegram/state-machine.js +72 -0
  41. package/dist/src/channels/telegram/state-machine.js.map +1 -0
  42. package/dist/src/channels/telegram/tool-tracker.d.ts +147 -0
  43. package/dist/src/channels/telegram/tool-tracker.d.ts.map +1 -0
  44. package/dist/src/channels/telegram/tool-tracker.js +520 -0
  45. package/dist/src/channels/telegram/tool-tracker.js.map +1 -0
  46. package/dist/src/circuit-breaker.d.ts +22 -0
  47. package/dist/src/circuit-breaker.d.ts.map +1 -0
  48. package/dist/src/circuit-breaker.js +47 -0
  49. package/dist/src/circuit-breaker.js.map +1 -0
  50. package/dist/src/command-router/cc-handler.d.ts +67 -0
  51. package/dist/src/command-router/cc-handler.d.ts.map +1 -0
  52. package/dist/src/command-router/cc-handler.js +980 -0
  53. package/dist/src/command-router/cc-handler.js.map +1 -0
  54. package/dist/src/command-router/index.d.ts +3 -0
  55. package/dist/src/command-router/index.d.ts.map +1 -0
  56. package/dist/src/command-router/index.js +2 -0
  57. package/dist/src/command-router/index.js.map +1 -0
  58. package/dist/src/constants.d.ts +132 -0
  59. package/dist/src/constants.d.ts.map +1 -0
  60. package/dist/src/constants.js +140 -0
  61. package/dist/src/constants.js.map +1 -0
  62. package/dist/src/council/consensus.d.ts +21 -0
  63. package/dist/src/council/consensus.d.ts.map +1 -0
  64. package/dist/src/council/consensus.js +52 -0
  65. package/dist/src/council/consensus.js.map +1 -0
  66. package/dist/src/council/council.d.ts +68 -0
  67. package/dist/src/council/council.d.ts.map +1 -0
  68. package/dist/src/council/council.js +914 -0
  69. package/dist/src/council/council.js.map +1 -0
  70. package/dist/src/council/index.d.ts +3 -0
  71. package/dist/src/council/index.d.ts.map +1 -0
  72. package/dist/src/council/index.js +3 -0
  73. package/dist/src/council/index.js.map +1 -0
  74. package/dist/src/engines/base-oneshot-session.d.ts +88 -0
  75. package/dist/src/engines/base-oneshot-session.d.ts.map +1 -0
  76. package/dist/src/engines/base-oneshot-session.js +228 -0
  77. package/dist/src/engines/base-oneshot-session.js.map +1 -0
  78. package/dist/src/engines/index.d.ts +7 -0
  79. package/dist/src/engines/index.d.ts.map +1 -0
  80. package/dist/src/engines/index.js +7 -0
  81. package/dist/src/engines/index.js.map +1 -0
  82. package/dist/src/engines/persistent-codex-session.d.ts +17 -0
  83. package/dist/src/engines/persistent-codex-session.d.ts.map +1 -0
  84. package/dist/src/engines/persistent-codex-session.js +106 -0
  85. package/dist/src/engines/persistent-codex-session.js.map +1 -0
  86. package/dist/src/engines/persistent-cursor-session.d.ts +22 -0
  87. package/dist/src/engines/persistent-cursor-session.d.ts.map +1 -0
  88. package/dist/src/engines/persistent-cursor-session.js +242 -0
  89. package/dist/src/engines/persistent-cursor-session.js.map +1 -0
  90. package/dist/src/engines/persistent-custom-session.d.ts +79 -0
  91. package/dist/src/engines/persistent-custom-session.d.ts.map +1 -0
  92. package/dist/src/engines/persistent-custom-session.js +939 -0
  93. package/dist/src/engines/persistent-custom-session.js.map +1 -0
  94. package/dist/src/engines/persistent-gemini-session.d.ts +22 -0
  95. package/dist/src/engines/persistent-gemini-session.d.ts.map +1 -0
  96. package/dist/src/engines/persistent-gemini-session.js +217 -0
  97. package/dist/src/engines/persistent-gemini-session.js.map +1 -0
  98. package/dist/src/engines/persistent-session.d.ts +77 -0
  99. package/dist/src/engines/persistent-session.d.ts.map +1 -0
  100. package/dist/src/engines/persistent-session.js +730 -0
  101. package/dist/src/engines/persistent-session.js.map +1 -0
  102. package/dist/src/health/handler.d.ts +40 -0
  103. package/dist/src/health/handler.d.ts.map +1 -0
  104. package/dist/src/health/handler.js +70 -0
  105. package/dist/src/health/handler.js.map +1 -0
  106. package/dist/src/health/index.d.ts +2 -0
  107. package/dist/src/health/index.d.ts.map +1 -0
  108. package/dist/src/health/index.js +2 -0
  109. package/dist/src/health/index.js.map +1 -0
  110. package/dist/src/index.d.ts +49 -0
  111. package/dist/src/index.d.ts.map +1 -0
  112. package/dist/src/index.js +84 -0
  113. package/dist/src/index.js.map +1 -0
  114. package/dist/src/lib/auto-recovery.d.ts +45 -0
  115. package/dist/src/lib/auto-recovery.d.ts.map +1 -0
  116. package/dist/src/lib/auto-recovery.js +217 -0
  117. package/dist/src/lib/auto-recovery.js.map +1 -0
  118. package/dist/src/lib/cache-parity.d.ts +39 -0
  119. package/dist/src/lib/cache-parity.d.ts.map +1 -0
  120. package/dist/src/lib/cache-parity.js +92 -0
  121. package/dist/src/lib/cache-parity.js.map +1 -0
  122. package/dist/src/lib/circuit-breaker.d.ts +22 -0
  123. package/dist/src/lib/circuit-breaker.d.ts.map +1 -0
  124. package/dist/src/lib/circuit-breaker.js +47 -0
  125. package/dist/src/lib/circuit-breaker.js.map +1 -0
  126. package/dist/src/lib/config.d.ts +74 -0
  127. package/dist/src/lib/config.d.ts.map +1 -0
  128. package/dist/src/lib/config.js +244 -0
  129. package/dist/src/lib/config.js.map +1 -0
  130. package/dist/src/lib/drift-detector.d.ts +47 -0
  131. package/dist/src/lib/drift-detector.d.ts.map +1 -0
  132. package/dist/src/lib/drift-detector.js +192 -0
  133. package/dist/src/lib/drift-detector.js.map +1 -0
  134. package/dist/src/lib/error-formatter.d.ts +78 -0
  135. package/dist/src/lib/error-formatter.d.ts.map +1 -0
  136. package/dist/src/lib/error-formatter.js +149 -0
  137. package/dist/src/lib/error-formatter.js.map +1 -0
  138. package/dist/src/lib/heartbeat-workaround.d.ts +45 -0
  139. package/dist/src/lib/heartbeat-workaround.d.ts.map +1 -0
  140. package/dist/src/lib/heartbeat-workaround.js +61 -0
  141. package/dist/src/lib/heartbeat-workaround.js.map +1 -0
  142. package/dist/src/lib/index.d.ts +8 -0
  143. package/dist/src/lib/index.d.ts.map +1 -0
  144. package/dist/src/lib/index.js +8 -0
  145. package/dist/src/lib/index.js.map +1 -0
  146. package/dist/src/lib/register-guard.d.ts +49 -0
  147. package/dist/src/lib/register-guard.d.ts.map +1 -0
  148. package/dist/src/lib/register-guard.js +73 -0
  149. package/dist/src/lib/register-guard.js.map +1 -0
  150. package/dist/src/lib/route-flag.d.ts +50 -0
  151. package/dist/src/lib/route-flag.d.ts.map +1 -0
  152. package/dist/src/lib/route-flag.js +52 -0
  153. package/dist/src/lib/route-flag.js.map +1 -0
  154. package/dist/src/lib/sysprompt-strip.d.ts +54 -0
  155. package/dist/src/lib/sysprompt-strip.d.ts.map +1 -0
  156. package/dist/src/lib/sysprompt-strip.js +75 -0
  157. package/dist/src/lib/sysprompt-strip.js.map +1 -0
  158. package/dist/src/lib/telemetry.d.ts +39 -0
  159. package/dist/src/lib/telemetry.d.ts.map +1 -0
  160. package/dist/src/lib/telemetry.js +73 -0
  161. package/dist/src/lib/telemetry.js.map +1 -0
  162. package/dist/src/lib/test-mode.d.ts +27 -0
  163. package/dist/src/lib/test-mode.d.ts.map +1 -0
  164. package/dist/src/lib/test-mode.js +38 -0
  165. package/dist/src/lib/test-mode.js.map +1 -0
  166. package/dist/src/lib/vendor-paths.d.ts +15 -0
  167. package/dist/src/lib/vendor-paths.d.ts.map +1 -0
  168. package/dist/src/lib/vendor-paths.js +32 -0
  169. package/dist/src/lib/vendor-paths.js.map +1 -0
  170. package/dist/src/logger.d.ts +17 -0
  171. package/dist/src/logger.d.ts.map +1 -0
  172. package/dist/src/logger.js +46 -0
  173. package/dist/src/logger.js.map +1 -0
  174. package/dist/src/mcp/bridge.d.ts +22 -0
  175. package/dist/src/mcp/bridge.d.ts.map +1 -0
  176. package/dist/src/mcp/bridge.js +78 -0
  177. package/dist/src/mcp/bridge.js.map +1 -0
  178. package/dist/src/mcp/index.d.ts +3 -0
  179. package/dist/src/mcp/index.d.ts.map +1 -0
  180. package/dist/src/mcp/index.js +2 -0
  181. package/dist/src/mcp/index.js.map +1 -0
  182. package/dist/src/models.d.ts +70 -0
  183. package/dist/src/models.d.ts.map +1 -0
  184. package/dist/src/models.js +289 -0
  185. package/dist/src/models.js.map +1 -0
  186. package/dist/src/openai-compat/cli-stream-parser.d.ts +135 -0
  187. package/dist/src/openai-compat/cli-stream-parser.d.ts.map +1 -0
  188. package/dist/src/openai-compat/cli-stream-parser.js +195 -0
  189. package/dist/src/openai-compat/cli-stream-parser.js.map +1 -0
  190. package/dist/src/openai-compat/index.d.ts +2 -0
  191. package/dist/src/openai-compat/index.d.ts.map +1 -0
  192. package/dist/src/openai-compat/index.js +2 -0
  193. package/dist/src/openai-compat/index.js.map +1 -0
  194. package/dist/src/openai-compat/openai-compat.d.ts +281 -0
  195. package/dist/src/openai-compat/openai-compat.d.ts.map +1 -0
  196. package/dist/src/openai-compat/openai-compat.js +939 -0
  197. package/dist/src/openai-compat/openai-compat.js.map +1 -0
  198. package/dist/src/openai-compat/skill-resolver.d.ts +36 -0
  199. package/dist/src/openai-compat/skill-resolver.d.ts.map +1 -0
  200. package/dist/src/openai-compat/skill-resolver.js +134 -0
  201. package/dist/src/openai-compat/skill-resolver.js.map +1 -0
  202. package/dist/src/openai-compat/sse-translator.d.ts +32 -0
  203. package/dist/src/openai-compat/sse-translator.d.ts.map +1 -0
  204. package/dist/src/openai-compat/sse-translator.js +155 -0
  205. package/dist/src/openai-compat/sse-translator.js.map +1 -0
  206. package/dist/src/proxy/anthropic-adapter.d.ts +137 -0
  207. package/dist/src/proxy/anthropic-adapter.d.ts.map +1 -0
  208. package/dist/src/proxy/anthropic-adapter.js +392 -0
  209. package/dist/src/proxy/anthropic-adapter.js.map +1 -0
  210. package/dist/src/proxy/handler.d.ts +40 -0
  211. package/dist/src/proxy/handler.d.ts.map +1 -0
  212. package/dist/src/proxy/handler.js +378 -0
  213. package/dist/src/proxy/handler.js.map +1 -0
  214. package/dist/src/proxy/index.d.ts +5 -0
  215. package/dist/src/proxy/index.d.ts.map +1 -0
  216. package/dist/src/proxy/index.js +5 -0
  217. package/dist/src/proxy/index.js.map +1 -0
  218. package/dist/src/proxy/schema-cleaner.d.ts +12 -0
  219. package/dist/src/proxy/schema-cleaner.d.ts.map +1 -0
  220. package/dist/src/proxy/schema-cleaner.js +34 -0
  221. package/dist/src/proxy/schema-cleaner.js.map +1 -0
  222. package/dist/src/proxy/thought-cache.d.ts +20 -0
  223. package/dist/src/proxy/thought-cache.d.ts.map +1 -0
  224. package/dist/src/proxy/thought-cache.js +53 -0
  225. package/dist/src/proxy/thought-cache.js.map +1 -0
  226. package/dist/src/session/embedded-server.d.ts +26 -0
  227. package/dist/src/session/embedded-server.d.ts.map +1 -0
  228. package/dist/src/session/embedded-server.js +367 -0
  229. package/dist/src/session/embedded-server.js.map +1 -0
  230. package/dist/src/session/inbox-manager.d.ts +39 -0
  231. package/dist/src/session/inbox-manager.d.ts.map +1 -0
  232. package/dist/src/session/inbox-manager.js +111 -0
  233. package/dist/src/session/inbox-manager.js.map +1 -0
  234. package/dist/src/session/index.d.ts +4 -0
  235. package/dist/src/session/index.d.ts.map +1 -0
  236. package/dist/src/session/index.js +4 -0
  237. package/dist/src/session/index.js.map +1 -0
  238. package/dist/src/session/session-manager.d.ts +212 -0
  239. package/dist/src/session/session-manager.d.ts.map +1 -0
  240. package/dist/src/session/session-manager.js +1351 -0
  241. package/dist/src/session/session-manager.js.map +1 -0
  242. package/dist/src/session-bootstrap/cwd-patch.d.ts +51 -0
  243. package/dist/src/session-bootstrap/cwd-patch.d.ts.map +1 -0
  244. package/dist/src/session-bootstrap/cwd-patch.js +955 -0
  245. package/dist/src/session-bootstrap/cwd-patch.js.map +1 -0
  246. package/dist/src/session-bootstrap/index.d.ts +4 -0
  247. package/dist/src/session-bootstrap/index.d.ts.map +1 -0
  248. package/dist/src/session-bootstrap/index.js +4 -0
  249. package/dist/src/session-bootstrap/index.js.map +1 -0
  250. package/dist/src/session-bootstrap/sysprompt-strip.d.ts +26 -0
  251. package/dist/src/session-bootstrap/sysprompt-strip.d.ts.map +1 -0
  252. package/dist/src/session-bootstrap/sysprompt-strip.js +57 -0
  253. package/dist/src/session-bootstrap/sysprompt-strip.js.map +1 -0
  254. package/dist/src/session-bootstrap/think-conflict-resolver.d.ts +33 -0
  255. package/dist/src/session-bootstrap/think-conflict-resolver.d.ts.map +1 -0
  256. package/dist/src/session-bootstrap/think-conflict-resolver.js +234 -0
  257. package/dist/src/session-bootstrap/think-conflict-resolver.js.map +1 -0
  258. package/dist/src/types.d.ts +489 -0
  259. package/dist/src/types.d.ts.map +1 -0
  260. package/dist/src/types.js +8 -0
  261. package/dist/src/types.js.map +1 -0
  262. package/dist/src/validation.d.ts +32 -0
  263. package/dist/src/validation.d.ts.map +1 -0
  264. package/dist/src/validation.js +104 -0
  265. package/dist/src/validation.js.map +1 -0
  266. package/dist/tests/_helpers/subprocess-mock.d.ts +35 -0
  267. package/dist/tests/_helpers/subprocess-mock.d.ts.map +1 -0
  268. package/dist/tests/_helpers/subprocess-mock.js +136 -0
  269. package/dist/tests/_helpers/subprocess-mock.js.map +1 -0
  270. package/dist/tests/auto-recovery.test.d.ts +2 -0
  271. package/dist/tests/auto-recovery.test.d.ts.map +1 -0
  272. package/dist/tests/auto-recovery.test.js +189 -0
  273. package/dist/tests/auto-recovery.test.js.map +1 -0
  274. package/dist/tests/bench-harness.test.d.ts +2 -0
  275. package/dist/tests/bench-harness.test.d.ts.map +1 -0
  276. package/dist/tests/bench-harness.test.js +21 -0
  277. package/dist/tests/bench-harness.test.js.map +1 -0
  278. package/dist/tests/cache-parity.test.d.ts +2 -0
  279. package/dist/tests/cache-parity.test.d.ts.map +1 -0
  280. package/dist/tests/cache-parity.test.js +401 -0
  281. package/dist/tests/cache-parity.test.js.map +1 -0
  282. package/dist/tests/command-router.test.d.ts +2 -0
  283. package/dist/tests/command-router.test.d.ts.map +1 -0
  284. package/dist/tests/command-router.test.js +60 -0
  285. package/dist/tests/command-router.test.js.map +1 -0
  286. package/dist/tests/council.test.d.ts +2 -0
  287. package/dist/tests/council.test.d.ts.map +1 -0
  288. package/dist/tests/council.test.js +20 -0
  289. package/dist/tests/council.test.js.map +1 -0
  290. package/dist/tests/drift-detector.test.d.ts +2 -0
  291. package/dist/tests/drift-detector.test.d.ts.map +1 -0
  292. package/dist/tests/drift-detector.test.js +268 -0
  293. package/dist/tests/drift-detector.test.js.map +1 -0
  294. package/dist/tests/eager-bootstrap-gating.test.d.ts +9 -0
  295. package/dist/tests/eager-bootstrap-gating.test.d.ts.map +1 -0
  296. package/dist/tests/eager-bootstrap-gating.test.js +97 -0
  297. package/dist/tests/eager-bootstrap-gating.test.js.map +1 -0
  298. package/dist/tests/engines.test.d.ts +2 -0
  299. package/dist/tests/engines.test.d.ts.map +1 -0
  300. package/dist/tests/engines.test.js +8 -0
  301. package/dist/tests/engines.test.js.map +1 -0
  302. package/dist/tests/error-formatter.test.d.ts +2 -0
  303. package/dist/tests/error-formatter.test.d.ts.map +1 -0
  304. package/dist/tests/error-formatter.test.js +220 -0
  305. package/dist/tests/error-formatter.test.js.map +1 -0
  306. package/dist/tests/health.test.d.ts +2 -0
  307. package/dist/tests/health.test.d.ts.map +1 -0
  308. package/dist/tests/health.test.js +110 -0
  309. package/dist/tests/health.test.js.map +1 -0
  310. package/dist/tests/heartbeat-workaround.test.d.ts +2 -0
  311. package/dist/tests/heartbeat-workaround.test.d.ts.map +1 -0
  312. package/dist/tests/heartbeat-workaround.test.js +90 -0
  313. package/dist/tests/heartbeat-workaround.test.js.map +1 -0
  314. package/dist/tests/index.test.d.ts +2 -0
  315. package/dist/tests/index.test.d.ts.map +1 -0
  316. package/dist/tests/index.test.js +7 -0
  317. package/dist/tests/index.test.js.map +1 -0
  318. package/dist/tests/lib-sysprompt-strip.test.d.ts +2 -0
  319. package/dist/tests/lib-sysprompt-strip.test.d.ts.map +1 -0
  320. package/dist/tests/lib-sysprompt-strip.test.js +145 -0
  321. package/dist/tests/lib-sysprompt-strip.test.js.map +1 -0
  322. package/dist/tests/listener-activation.test.d.ts +2 -0
  323. package/dist/tests/listener-activation.test.d.ts.map +1 -0
  324. package/dist/tests/listener-activation.test.js +87 -0
  325. package/dist/tests/listener-activation.test.js.map +1 -0
  326. package/dist/tests/mcp-bridge.test.d.ts +2 -0
  327. package/dist/tests/mcp-bridge.test.d.ts.map +1 -0
  328. package/dist/tests/mcp-bridge.test.js +137 -0
  329. package/dist/tests/mcp-bridge.test.js.map +1 -0
  330. package/dist/tests/openai-compat.test.d.ts +2 -0
  331. package/dist/tests/openai-compat.test.d.ts.map +1 -0
  332. package/dist/tests/openai-compat.test.js +8 -0
  333. package/dist/tests/openai-compat.test.js.map +1 -0
  334. package/dist/tests/proxy-heartbeat-integration.test.d.ts +15 -0
  335. package/dist/tests/proxy-heartbeat-integration.test.d.ts.map +1 -0
  336. package/dist/tests/proxy-heartbeat-integration.test.js +122 -0
  337. package/dist/tests/proxy-heartbeat-integration.test.js.map +1 -0
  338. package/dist/tests/proxy.test.d.ts +2 -0
  339. package/dist/tests/proxy.test.d.ts.map +1 -0
  340. package/dist/tests/proxy.test.js +8 -0
  341. package/dist/tests/proxy.test.js.map +1 -0
  342. package/dist/tests/register-guard-stacking.test.d.ts +2 -0
  343. package/dist/tests/register-guard-stacking.test.d.ts.map +1 -0
  344. package/dist/tests/register-guard-stacking.test.js +61 -0
  345. package/dist/tests/register-guard-stacking.test.js.map +1 -0
  346. package/dist/tests/register-guard.test.d.ts +2 -0
  347. package/dist/tests/register-guard.test.d.ts.map +1 -0
  348. package/dist/tests/register-guard.test.js +129 -0
  349. package/dist/tests/register-guard.test.js.map +1 -0
  350. package/dist/tests/route-flag-rollback.test.d.ts +2 -0
  351. package/dist/tests/route-flag-rollback.test.d.ts.map +1 -0
  352. package/dist/tests/route-flag-rollback.test.js +70 -0
  353. package/dist/tests/route-flag-rollback.test.js.map +1 -0
  354. package/dist/tests/route-flag.test.d.ts +2 -0
  355. package/dist/tests/route-flag.test.d.ts.map +1 -0
  356. package/dist/tests/route-flag.test.js +101 -0
  357. package/dist/tests/route-flag.test.js.map +1 -0
  358. package/dist/tests/session-bootstrap.test.d.ts +2 -0
  359. package/dist/tests/session-bootstrap.test.d.ts.map +1 -0
  360. package/dist/tests/session-bootstrap.test.js +183 -0
  361. package/dist/tests/session-bootstrap.test.js.map +1 -0
  362. package/dist/tests/session.test.d.ts +2 -0
  363. package/dist/tests/session.test.d.ts.map +1 -0
  364. package/dist/tests/session.test.js +17 -0
  365. package/dist/tests/session.test.js.map +1 -0
  366. package/dist/tests/state-machine.test.d.ts +2 -0
  367. package/dist/tests/state-machine.test.d.ts.map +1 -0
  368. package/dist/tests/state-machine.test.js +133 -0
  369. package/dist/tests/state-machine.test.js.map +1 -0
  370. package/dist/tests/streaming/cli-stream-parser.test.d.ts +2 -0
  371. package/dist/tests/streaming/cli-stream-parser.test.d.ts.map +1 -0
  372. package/dist/tests/streaming/cli-stream-parser.test.js +233 -0
  373. package/dist/tests/streaming/cli-stream-parser.test.js.map +1 -0
  374. package/dist/tests/streaming/feature-flag.test.d.ts +14 -0
  375. package/dist/tests/streaming/feature-flag.test.d.ts.map +1 -0
  376. package/dist/tests/streaming/feature-flag.test.js +163 -0
  377. package/dist/tests/streaming/feature-flag.test.js.map +1 -0
  378. package/dist/tests/streaming/no-tools-prompt.test.d.ts +17 -0
  379. package/dist/tests/streaming/no-tools-prompt.test.d.ts.map +1 -0
  380. package/dist/tests/streaming/no-tools-prompt.test.js +229 -0
  381. package/dist/tests/streaming/no-tools-prompt.test.js.map +1 -0
  382. package/dist/tests/streaming/skill-plus-tools.test.d.ts +14 -0
  383. package/dist/tests/streaming/skill-plus-tools.test.d.ts.map +1 -0
  384. package/dist/tests/streaming/skill-plus-tools.test.js +234 -0
  385. package/dist/tests/streaming/skill-plus-tools.test.js.map +1 -0
  386. package/dist/tests/streaming/sse-translator.test.d.ts +2 -0
  387. package/dist/tests/streaming/sse-translator.test.d.ts.map +1 -0
  388. package/dist/tests/streaming/sse-translator.test.js +227 -0
  389. package/dist/tests/streaming/sse-translator.test.js.map +1 -0
  390. package/dist/tests/streaming/tool-result-roundtrip.test.d.ts +11 -0
  391. package/dist/tests/streaming/tool-result-roundtrip.test.d.ts.map +1 -0
  392. package/dist/tests/streaming/tool-result-roundtrip.test.js +215 -0
  393. package/dist/tests/streaming/tool-result-roundtrip.test.js.map +1 -0
  394. package/dist/tests/streaming/tool-use-translation.test.d.ts +10 -0
  395. package/dist/tests/streaming/tool-use-translation.test.d.ts.map +1 -0
  396. package/dist/tests/streaming/tool-use-translation.test.js +251 -0
  397. package/dist/tests/streaming/tool-use-translation.test.js.map +1 -0
  398. package/dist/tests/telegram-bridge.test.d.ts +2 -0
  399. package/dist/tests/telegram-bridge.test.d.ts.map +1 -0
  400. package/dist/tests/telegram-bridge.test.js +17 -0
  401. package/dist/tests/telegram-bridge.test.js.map +1 -0
  402. package/dist/tests/telegram-injector.test.d.ts +2 -0
  403. package/dist/tests/telegram-injector.test.d.ts.map +1 -0
  404. package/dist/tests/telegram-injector.test.js +74 -0
  405. package/dist/tests/telegram-injector.test.js.map +1 -0
  406. package/dist/tests/telemetry.test.d.ts +2 -0
  407. package/dist/tests/telemetry.test.d.ts.map +1 -0
  408. package/dist/tests/telemetry.test.js +405 -0
  409. package/dist/tests/telemetry.test.js.map +1 -0
  410. package/dist/tests/test-mode.test.d.ts +2 -0
  411. package/dist/tests/test-mode.test.d.ts.map +1 -0
  412. package/dist/tests/test-mode.test.js +39 -0
  413. package/dist/tests/test-mode.test.js.map +1 -0
  414. package/mcp-config.template.json +13 -0
  415. package/mcp-tools.json +1 -0
  416. package/openclaw-mcp-bridge.cjs +152 -0
  417. package/openclaw.plugin.json +30 -0
  418. package/package.json +45 -0
  419. package/skills/.gitkeep +0 -0
  420. package/stubs/commands-status-deps.runtime.js +10 -0
  421. package/stubs/status.runtime.js +149 -0
  422. package/vendor/base-oneshot-session.d.ts +87 -0
  423. package/vendor/base-oneshot-session.js +227 -0
  424. package/vendor/base-oneshot-session.js.map +1 -0
  425. package/vendor/circuit-breaker.d.ts +21 -0
  426. package/vendor/circuit-breaker.js +47 -0
  427. package/vendor/circuit-breaker.js.map +1 -0
  428. package/vendor/consensus.d.ts +20 -0
  429. package/vendor/consensus.js +52 -0
  430. package/vendor/consensus.js.map +1 -0
  431. package/vendor/constants.d.ts +130 -0
  432. package/vendor/constants.js +139 -0
  433. package/vendor/constants.js.map +1 -0
  434. package/vendor/council.d.ts +67 -0
  435. package/vendor/council.js +913 -0
  436. package/vendor/council.js.map +1 -0
  437. package/vendor/embedded-server.d.ts +25 -0
  438. package/vendor/embedded-server.js +360 -0
  439. package/vendor/embedded-server.js.map +1 -0
  440. package/vendor/inbox-manager.d.ts +38 -0
  441. package/vendor/inbox-manager.js +111 -0
  442. package/vendor/inbox-manager.js.map +1 -0
  443. package/vendor/index.d.ts +63 -0
  444. package/vendor/index.js +705 -0
  445. package/vendor/index.js.map +1 -0
  446. package/vendor/logger.d.ts +16 -0
  447. package/vendor/logger.js +44 -0
  448. package/vendor/logger.js.map +1 -0
  449. package/vendor/models.d.ts +69 -0
  450. package/vendor/models.js +289 -0
  451. package/vendor/models.js.map +1 -0
  452. package/vendor/openai-compat.d.ts +197 -0
  453. package/vendor/openai-compat.js +721 -0
  454. package/vendor/openai-compat.js.map +1 -0
  455. package/vendor/persistent-codex-session.d.ts +16 -0
  456. package/vendor/persistent-codex-session.js +105 -0
  457. package/vendor/persistent-codex-session.js.map +1 -0
  458. package/vendor/persistent-cursor-session.d.ts +21 -0
  459. package/vendor/persistent-cursor-session.js +241 -0
  460. package/vendor/persistent-cursor-session.js.map +1 -0
  461. package/vendor/persistent-custom-session.d.ts +78 -0
  462. package/vendor/persistent-custom-session.js +937 -0
  463. package/vendor/persistent-custom-session.js.map +1 -0
  464. package/vendor/persistent-gemini-session.d.ts +21 -0
  465. package/vendor/persistent-gemini-session.js +216 -0
  466. package/vendor/persistent-gemini-session.js.map +1 -0
  467. package/vendor/persistent-session.d.ts +74 -0
  468. package/vendor/persistent-session.js +684 -0
  469. package/vendor/persistent-session.js.map +1 -0
  470. package/vendor/proxy/anthropic-adapter.d.ts +136 -0
  471. package/vendor/proxy/anthropic-adapter.js +392 -0
  472. package/vendor/proxy/anthropic-adapter.js.map +1 -0
  473. package/vendor/proxy/handler.d.ts +39 -0
  474. package/vendor/proxy/handler.js +323 -0
  475. package/vendor/proxy/handler.js.map +1 -0
  476. package/vendor/proxy/schema-cleaner.d.ts +11 -0
  477. package/vendor/proxy/schema-cleaner.js +34 -0
  478. package/vendor/proxy/schema-cleaner.js.map +1 -0
  479. package/vendor/proxy/thought-cache.d.ts +19 -0
  480. package/vendor/proxy/thought-cache.js +53 -0
  481. package/vendor/proxy/thought-cache.js.map +1 -0
  482. package/vendor/session-manager.d.ts +211 -0
  483. package/vendor/session-manager.js +1345 -0
  484. package/vendor/session-manager.js.map +1 -0
  485. package/vendor/skill-resolver.js +107 -0
  486. package/vendor/types.d.ts +466 -0
  487. package/vendor/types.js +8 -0
  488. package/vendor/types.js.map +1 -0
  489. package/vendor/validation.d.ts +31 -0
  490. package/vendor/validation.js +104 -0
  491. package/vendor/validation.js.map +1 -0
@@ -0,0 +1,721 @@
1
+ /**
2
+ * OpenAI-compatible /v1/chat/completions endpoint.
3
+ *
4
+ * Bridges OpenAI API format to persistent Claude Code sessions, enabling
5
+ * webchat frontends (ChatGPT-Next-Web, Open WebUI, etc.) to use the plugin
6
+ * as a drop-in backend. Stateful sessions maximize Anthropic prompt caching.
7
+ */
8
+ import * as http from 'node:http';
9
+ import * as fs from 'node:fs';
10
+ import * as path from 'node:path';
11
+ import * as os from 'node:os';
12
+ import { randomUUID, createHash } from 'node:crypto';
13
+ import { resolveEngineAndModel } from './models.js';
14
+ import { OPENAI_COMPAT_DEFAULT_MODEL, OPENAI_COMPAT_AUTO_COMPACT_THRESHOLD, OPENAI_COMPAT_SESSION_PREFIX, } from './constants.js';
15
+ import { maybeInlineSkill } from './skill-resolver.js';
16
+ // ─── Session Key Resolution ──────────────────────────────────────────────────
17
+ /**
18
+ * Derive a session key from the request.
19
+ * Priority: X-Session-Id header > user field > sha1(model + systemPrompt) > "default"
20
+ *
21
+ * The system-prompt-hash fallback prevents the bug where every caller without
22
+ * X-Session-Id or `user` collapses onto a single shared "openai-default"
23
+ * plugin session. In multi-caller setups (OpenClaw routing the main agent,
24
+ * cron jobs, and subagents through the same gateway) that previously meant
25
+ * every request serialized against every other and frequently picked up the
26
+ * wrong session's appendSystemPrompt — also a privacy leak across callers.
27
+ *
28
+ * The model is mixed into the hash so that two callers with the same system
29
+ * prompt but different requested models don't collide and silently get
30
+ * responses from the wrong model. Originally diagnosed in PR #40 by
31
+ * @megayounus786.
32
+ */
33
+ export function resolveSessionKey(body, headers) {
34
+ const headerKey = headers['x-session-id'];
35
+ if (typeof headerKey === 'string' && headerKey.trim())
36
+ return headerKey.trim();
37
+ if (body.user && body.user.trim())
38
+ return body.user.trim();
39
+ const sys = (body.messages || [])
40
+ .filter((m) => m && m.role === 'system')
41
+ .map((m) => (typeof m.content === 'string' ? m.content : JSON.stringify(m.content)))
42
+ .join('\n');
43
+ const modelTag = (body.model || '').toString();
44
+ if (sys || modelTag) {
45
+ return ('sys-' +
46
+ createHash('sha1')
47
+ .update(modelTag + '\n' + sys)
48
+ .digest('hex')
49
+ .slice(0, 12));
50
+ }
51
+ return 'default';
52
+ }
53
+ /** Build the full session name from a key */
54
+ export function sessionNameFromKey(key) {
55
+ return `${OPENAI_COMPAT_SESSION_PREFIX}${key}`;
56
+ }
57
+ // ─── Function Calling Support ────────────────────────────────────────────────
58
+ /**
59
+ * Convert OpenAI tool definitions into a structured prompt block.
60
+ * Injected into the user message so the CLI model sees tool definitions
61
+ * and responds with <tool_calls> tags when it wants to invoke a function.
62
+ */
63
+ export function buildToolPromptBlock(tools) {
64
+ if (!tools?.length)
65
+ return '';
66
+ const toolDefs = tools
67
+ .map((t) => {
68
+ const fn = t.function;
69
+ const params = JSON.stringify(fn.parameters, null, 2);
70
+ return `### ${fn.name}\n${fn.description}\n\nParameters:\n\`\`\`json\n${params}\n\`\`\``;
71
+ })
72
+ .join('\n\n');
73
+ return ('<available_tools>\n' +
74
+ 'You have access to the following tools. When you need to use a tool, respond with a JSON array wrapped in <tool_calls> tags.\n\n' +
75
+ 'FORMAT:\n' +
76
+ '<tool_calls>\n' +
77
+ '[{"name": "tool_name", "arguments": {"param1": "value1"}}]\n' +
78
+ '</tool_calls>\n\n' +
79
+ 'If you do NOT need any tools, respond normally with text only (no <tool_calls> tags).\n\n' +
80
+ '## Available Tools\n\n' +
81
+ toolDefs +
82
+ '\n</available_tools>');
83
+ }
84
+ /**
85
+ * Parse tool_calls from CLI text output.
86
+ *
87
+ * Looks for <tool_calls>[...]</tool_calls> tags in the response text.
88
+ * Returns both the extracted text content (before/after tags) and any tool calls found.
89
+ */
90
+ export function parseToolCallsFromText(text) {
91
+ // Match ALL <tool_calls> blocks (model may output multiple)
92
+ const tagRegex = /<tool_calls>\s*([\s\S]*?)\s*<\/tool_calls>/g;
93
+ const allCalls = [];
94
+ let lastIndex = 0;
95
+ const textParts = [];
96
+ let m;
97
+ while ((m = tagRegex.exec(text)) !== null) {
98
+ // Collect text before this block
99
+ const before = text.slice(lastIndex, m.index).trim();
100
+ if (before)
101
+ textParts.push(before);
102
+ lastIndex = m.index + m[0].length;
103
+ try {
104
+ const parsed = JSON.parse(m[1].trim());
105
+ const arr = Array.isArray(parsed) ? parsed : [parsed];
106
+ for (const raw of arr) {
107
+ const call = raw;
108
+ if (!call || typeof call !== 'object' || typeof call.name !== 'string')
109
+ continue;
110
+ let args;
111
+ if (typeof call.arguments === 'string') {
112
+ try {
113
+ JSON.parse(call.arguments);
114
+ args = call.arguments;
115
+ }
116
+ catch {
117
+ args = JSON.stringify({ input: call.arguments });
118
+ }
119
+ }
120
+ else {
121
+ args = JSON.stringify(call.arguments ?? {});
122
+ }
123
+ allCalls.push({
124
+ id: `call_${randomUUID().replace(/-/g, '').slice(0, 24)}`,
125
+ type: 'function',
126
+ function: { name: call.name, arguments: args },
127
+ });
128
+ }
129
+ }
130
+ catch {
131
+ // One block failed — keep its text as content
132
+ textParts.push(m[0]);
133
+ }
134
+ }
135
+ // Collect text after last block
136
+ const after = text.slice(lastIndex).trim();
137
+ if (after)
138
+ textParts.push(after);
139
+ // Strip <tool_result> and <tool_results> tags that the model may echo back
140
+ // from the serialized tool results we injected earlier.
141
+ const stripToolResultTags = (s) => s
142
+ .replace(/<tool_results?>[\s\S]*?<\/tool_results?>/g, '')
143
+ .replace(/<tool_results?[^>]*>/g, '')
144
+ .trim();
145
+ if (allCalls.length > 0) {
146
+ const raw = textParts.join('\n').trim();
147
+ const cleaned = raw ? stripToolResultTags(raw) : null;
148
+ return { textContent: cleaned || null, toolCalls: allCalls };
149
+ }
150
+ const cleaned = text ? stripToolResultTags(text) : null;
151
+ return { textContent: cleaned || null, toolCalls: [] };
152
+ }
153
+ /**
154
+ * Serialize tool result messages into a text block for the CLI model.
155
+ * Converts OpenAI `tool` role messages into <tool_result> tags.
156
+ */
157
+ export function serializeToolResults(messages) {
158
+ const toolMessages = messages.filter((m) => m.role === 'tool');
159
+ if (!toolMessages.length)
160
+ return '';
161
+ const results = toolMessages
162
+ .map((m) => {
163
+ const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
164
+ return `<tool_result tool_call_id="${m.tool_call_id || 'unknown'}">\n${content}\n</tool_result>`;
165
+ })
166
+ .join('\n\n');
167
+ return `<tool_results>\n${results}\n</tool_results>\n\nAbove are the results of the tool calls you requested. Continue your response based on these results.`;
168
+ }
169
+ /**
170
+ * Extract the relevant parts from an OpenAI messages array.
171
+ *
172
+ * Sessions are stateful — we only need the last user message. The tricky
173
+ * question is whether to start a fresh session or append to the existing one.
174
+ *
175
+ * Default mode (no env var): only honor an explicit `X-Session-Reset: 1`
176
+ * header. This is correct for clients that maintain their own conversation
177
+ * transcript and forward only the latest user turn (OpenClaw main agent
178
+ * loop, cron jobs, subagents). The previous heuristic
179
+ * (`nonSystemMessages.length <= 1`) fired on every such request, killing the
180
+ * persistent CLI every turn and preventing Anthropic prompt caching from
181
+ * ever warming. Originally diagnosed in PR #40 by @megayounus786.
182
+ *
183
+ * Legacy mode (`OPENAI_COMPAT_NEW_CONVO_HEURISTIC=1`): restore the old
184
+ * `system + single user ⇒ new conversation` rule, for clients that re-send
185
+ * the full transcript on every turn (ChatGPT-Next-Web, Open WebUI, data
186
+ * labeling tools, etc). They use the transcript shape itself as their only
187
+ * "start a new conversation" signal.
188
+ *
189
+ * The env var is read on every call so ops can flip it via launchctl setenv
190
+ * without restarting the server.
191
+ */
192
+ export function extractUserMessage(messages, headers) {
193
+ if (!messages || messages.length === 0) {
194
+ throw new Error('messages array is empty');
195
+ }
196
+ // Normalize content from any message: OpenAI API allows content as a string
197
+ // OR an array of content parts (e.g. multimodal messages with text + images).
198
+ // We need a string for the CLI, so arrays are joined.
199
+ const textOf = (m) => {
200
+ if (typeof m.content === 'string')
201
+ return m.content;
202
+ if (Array.isArray(m.content)) {
203
+ return m.content
204
+ .map((p) => p.text || '')
205
+ .filter(Boolean)
206
+ .join('');
207
+ }
208
+ return m.content != null ? String(m.content) : '';
209
+ };
210
+ // Extract system prompt if present
211
+ const systemMessages = messages.filter((m) => m.role === 'system');
212
+ const systemPrompt = systemMessages.length > 0 ? systemMessages.map(textOf).join('\n') : undefined;
213
+ // Handle tool result messages — only when the LAST non-system message is
214
+ // a tool role (meaning we're in an active tool-use cycle). If the last
215
+ // message is a user role, it's a follow-up in an existing conversation
216
+ // and the old tool results are already in the CLI's history.
217
+ const lastNonSystem = [...messages].reverse().find((m) => m.role !== 'system');
218
+ if (lastNonSystem?.role === 'tool') {
219
+ const toolResultBlock = serializeToolResults(messages);
220
+ const userMessages = messages.filter((m) => m.role === 'user');
221
+ const lastUserText = userMessages.length > 0 ? textOf(userMessages[userMessages.length - 1]) : '';
222
+ const userMessage = lastUserText ? `${toolResultBlock}\n\n${lastUserText}` : toolResultBlock;
223
+ return { systemPrompt, userMessage, isNewConversation: false };
224
+ }
225
+ // Find last user message
226
+ const userMessages = messages.filter((m) => m.role === 'user');
227
+ if (userMessages.length === 0) {
228
+ throw new Error('No user message found in messages array');
229
+ }
230
+ const rawUserMessage = textOf(userMessages[userMessages.length - 1]);
231
+ // Workspace skill auto-inline: if the user typed /<skillname> [args] and
232
+ // ~/.openclaw/workspace/skills/*/SKILL.md has a matching `name:` frontmatter,
233
+ // inline the SKILL.md body so the model has full skill context without
234
+ // needing the Read tool (which cc-openclaw disables — see line 358).
235
+ const userMessage = maybeInlineSkill(rawUserMessage) ?? rawUserMessage;
236
+ // 1. Explicit reset header — honored in both modes. Normalize trim+lowercase
237
+ // so callers using `TRUE`, ` 1 `, etc. don't silently fail.
238
+ const rawReset = headers?.['x-session-reset'];
239
+ const resetHeader = typeof rawReset === 'string' ? rawReset.trim().toLowerCase() : '';
240
+ if (resetHeader === 'true' || resetHeader === '1') {
241
+ return { systemPrompt, userMessage, isNewConversation: true };
242
+ }
243
+ // 2. Legacy heuristic — only when explicitly opted in via env var.
244
+ if (process.env.OPENAI_COMPAT_NEW_CONVO_HEURISTIC === '1') {
245
+ const nonSystemMessages = messages.filter((m) => m.role !== 'system');
246
+ return { systemPrompt, userMessage, isNewConversation: nonSystemMessages.length <= 1 };
247
+ }
248
+ return { systemPrompt, userMessage, isNewConversation: false };
249
+ }
250
+ // ─── Response Formatting ─────────────────────────────────────────────────────
251
+ export function formatCompletionResponse(id, model, text, tokensIn, tokensOut, toolCalls) {
252
+ const hasToolCalls = toolCalls && toolCalls.length > 0;
253
+ return {
254
+ id,
255
+ object: 'chat.completion',
256
+ created: Math.floor(Date.now() / 1000),
257
+ model,
258
+ choices: [
259
+ {
260
+ index: 0,
261
+ message: {
262
+ role: 'assistant',
263
+ content: text || null,
264
+ ...(hasToolCalls ? { tool_calls: toolCalls } : {}),
265
+ },
266
+ finish_reason: hasToolCalls ? 'tool_calls' : 'stop',
267
+ },
268
+ ],
269
+ usage: {
270
+ prompt_tokens: tokensIn,
271
+ completion_tokens: tokensOut,
272
+ total_tokens: tokensIn + tokensOut,
273
+ },
274
+ };
275
+ }
276
+ export function formatCompletionChunk(id, model, delta, finishReason) {
277
+ return {
278
+ id,
279
+ object: 'chat.completion.chunk',
280
+ created: Math.floor(Date.now() / 1000),
281
+ model,
282
+ choices: [{ index: 0, delta, finish_reason: finishReason }],
283
+ };
284
+ }
285
+ export async function handleChatCompletion(manager, body, headers, res) {
286
+ // Validate before casting
287
+ if (!body.messages || !Array.isArray(body.messages) || body.messages.length === 0) {
288
+ res.writeHead(400, { 'Content-Type': 'application/json' });
289
+ res.end(JSON.stringify({
290
+ error: { message: 'messages is required and must be a non-empty array', type: 'invalid_request_error' },
291
+ }));
292
+ return;
293
+ }
294
+ // Safe cast: messages validated above, other fields are optional
295
+ const request = {
296
+ messages: body.messages,
297
+ model: body.model,
298
+ stream: body.stream,
299
+ temperature: body.temperature,
300
+ max_tokens: body.max_tokens,
301
+ user: body.user,
302
+ tools: body.tools,
303
+ };
304
+ // Validate max_tokens if provided
305
+ if (request.max_tokens !== undefined && (typeof request.max_tokens !== 'number' || request.max_tokens <= 0)) {
306
+ res.writeHead(400, { 'Content-Type': 'application/json' });
307
+ res.end(JSON.stringify({
308
+ error: { message: 'max_tokens must be a positive number', type: 'invalid_request_error' },
309
+ }));
310
+ return;
311
+ }
312
+ const modelStr = request.model || OPENAI_COMPAT_DEFAULT_MODEL;
313
+ const { engine, model: resolvedModel } = resolveEngineAndModel(modelStr);
314
+ const sessionKey = resolveSessionKey(request, headers);
315
+ const sessionName = sessionNameFromKey(sessionKey);
316
+ const isStreaming = request.stream === true;
317
+ let extracted;
318
+ try {
319
+ extracted = extractUserMessage(request.messages, headers);
320
+ }
321
+ catch (err) {
322
+ res.writeHead(400, { 'Content-Type': 'application/json' });
323
+ res.end(JSON.stringify({ error: { message: err.message, type: 'invalid_request_error' } }));
324
+ return;
325
+ }
326
+ // Check if session exists
327
+ const existingSessions = manager.listSessions().map((s) => s.name);
328
+ const sessionExists = existingSessions.includes(sessionName);
329
+ // If new conversation detected and session exists, stop old one first
330
+ if (extracted.isNewConversation && sessionExists) {
331
+ try {
332
+ await manager.stopSession(sessionName);
333
+ }
334
+ catch {
335
+ /* session may have already been cleaned up */
336
+ }
337
+ }
338
+ // Create session if needed
339
+ const needsCreate = !sessionExists || extracted.isNewConversation;
340
+ if (needsCreate) {
341
+ // OpenAI-compat sessions are API proxies, not coding sessions.
342
+ // Use a neutral empty temp dir so the CLI doesn't load CLAUDE.md,
343
+ // git state, or project context from wherever `serve` was started.
344
+ const sessionCwd = path.join(os.tmpdir(), `openclaw-compat-${sessionName}`);
345
+ if (!fs.existsSync(sessionCwd))
346
+ fs.mkdirSync(sessionCwd, { recursive: true });
347
+ const sessionConfig = {
348
+ name: sessionName,
349
+ cwd: sessionCwd,
350
+ engine,
351
+ model: resolvedModel,
352
+ permissionMode: 'bypassPermissions',
353
+ // skipPersistence: tells SessionManager not to write this session to
354
+ // the disk registry, preventing auto-resume of stale sessions.
355
+ // Note: noSessionPersistence (--no-session-persistence) is NOT set
356
+ // because some CLI forks don't support this flag.
357
+ skipPersistence: true,
358
+ };
359
+ // Phase 2 R5: tool-stream mode flag (mirrors src/openai-compat/openai-compat.ts).
360
+ // When CC_OPENCLAW_TOOL_STREAM=1 AND caller provides tools[], skip
361
+ // both the sessionConfig.tools='' defensive clear AND the no-tools
362
+ // system prompt — Claude CLI tool_use events flow through natively.
363
+ const toolStreamMode = process.env.CC_OPENCLAW_TOOL_STREAM === '1';
364
+ const useToolStreamPath = toolStreamMode && request.tools?.length;
365
+ // Phase 2 R5+R3: tool-stream mode forwards the allowlist of tool names
366
+ // from request.tools[] so Claude CLI knows which tools the model is
367
+ // allowed to invoke. Legacy mode clears tools entirely.
368
+ if (engine === 'claude') {
369
+ if (useToolStreamPath) {
370
+ const toolNames = request.tools
371
+ .map((t) => t?.function?.name)
372
+ .filter((n) => typeof n === 'string' && n.length > 0)
373
+ .join(',');
374
+ if (toolNames) sessionConfig.tools = toolNames;
375
+ }
376
+ else {
377
+ sessionConfig.tools = '';
378
+ }
379
+ }
380
+ // System prompt: legacy modes inject the defensive "no tools" or
381
+ // "pure text" prompt. Tool-stream mode passes the caller's system
382
+ // prompt through unchanged (Claude CLI gets native tools).
383
+ if (engine === 'claude') {
384
+ if (useToolStreamPath) {
385
+ if (extracted.systemPrompt) {
386
+ sessionConfig.systemPrompt = extracted.systemPrompt;
387
+ }
388
+ }
389
+ else if (request.tools?.length) {
390
+ const noToolsPrompt = 'You are a helpful AI assistant acting as a pure LLM behind an API proxy.\n' +
391
+ 'You do NOT have access to any tools such as Bash, Read, Write, Edit, Glob, Grep, or any other built-in tools.\n' +
392
+ 'Do NOT attempt to call any tools or execute any commands.\n' +
393
+ 'When you need to perform an action, use ONLY the tools defined in <available_tools> tags in the user message, ' +
394
+ 'and respond with <tool_calls> tags as instructed there.\n' +
395
+ 'If no <available_tools> are provided, respond with text only.';
396
+ sessionConfig.systemPrompt = extracted.systemPrompt
397
+ ? `${noToolsPrompt}\n\n${extracted.systemPrompt}`
398
+ : noToolsPrompt;
399
+ }
400
+ else {
401
+ const pureTextPrompt = 'You are a helpful AI assistant acting as a pure LLM behind an API proxy.\n' +
402
+ 'You do NOT have access to any tools. Do NOT attempt to call any tools or execute any commands.\n' +
403
+ 'Respond with text only.';
404
+ sessionConfig.systemPrompt = extracted.systemPrompt
405
+ ? `${pureTextPrompt}\n\n${extracted.systemPrompt}`
406
+ : pureTextPrompt;
407
+ }
408
+ }
409
+ try {
410
+ await manager.startSession(sessionConfig);
411
+ }
412
+ catch (err) {
413
+ res.writeHead(503, { 'Content-Type': 'application/json' });
414
+ res.end(JSON.stringify({
415
+ error: { message: `Failed to start session: ${err.message}`, type: 'server_error' },
416
+ }));
417
+ return;
418
+ }
419
+ }
420
+ // Auto-compact if context is getting full
421
+ if (sessionExists && !needsCreate) {
422
+ try {
423
+ const status = manager.getStatus(sessionName);
424
+ if (status.stats.contextPercent > OPENAI_COMPAT_AUTO_COMPACT_THRESHOLD) {
425
+ await manager.compactSession(sessionName);
426
+ }
427
+ }
428
+ catch {
429
+ /* best effort — session may not support compact */
430
+ }
431
+ }
432
+ // For non-claude engines (Cursor, Codex, Gemini), their CLIs don't support
433
+ // --append-system-prompt. Prepend the upstream system prompt to the user
434
+ // message on EVERY turn so the model sees the caller's identity, tool
435
+ // definitions, and workspace context. This is done here (not at session
436
+ // creation) because these engines spawn a fresh CLI process per turn —
437
+ // there's no persistent session to carry the system prompt forward.
438
+ let userMessage = extracted.userMessage;
439
+ if (extracted.systemPrompt && engine !== 'claude') {
440
+ userMessage = `<system>\n${extracted.systemPrompt}\n</system>\n\n${userMessage}`;
441
+ }
442
+ // Inject tool definitions into the user message
443
+ const hasTools = !!request.tools?.length;
444
+ if (hasTools) {
445
+ const toolBlock = buildToolPromptBlock(request.tools);
446
+ userMessage = `${toolBlock}\n\n${userMessage}`;
447
+ }
448
+ const completionId = `chatcmpl-${randomUUID().replace(/-/g, '').slice(0, 29)}`;
449
+ if (isStreaming) {
450
+ await handleStreaming(manager, sessionName, resolvedModel, userMessage, completionId, res, hasTools);
451
+ }
452
+ else {
453
+ await handleNonStreaming(manager, sessionName, resolvedModel, userMessage, completionId, res, hasTools);
454
+ }
455
+ // Clean up ephemeral sessions immediately after response.
456
+ // When X-Session-Reset is set, each request creates a fresh session that
457
+ // should not persist — leaving it alive leaks CLI subprocesses until TTL.
458
+ if (extracted.isNewConversation) {
459
+ manager.stopSession(sessionName).catch(() => { });
460
+ }
461
+ }
462
+ // ─── Status Reporting ───────────────────────────────────────────────────────
463
+ // Push tool/thinking status to an external webhook so a webchat status bar
464
+ // can show what the CLI agent is doing. Best-effort fire-and-forget.
465
+ /**
466
+ * Optional status webhook — set `OPENAI_COMPAT_STATUS_URL` to an HTTP endpoint
467
+ * that accepts `POST { state, activity, tool }`. The bridge will fire-and-forget
468
+ * status updates when the CLI agent uses tools, so an external dashboard (e.g.
469
+ * a webchat status bar) can show real-time progress.
470
+ *
471
+ * Example: `OPENAI_COMPAT_STATUS_URL=http://127.0.0.1:18795/my-app/agent-status`
472
+ */
473
+ function reportStatus(state, activity, tool) {
474
+ const url = process.env.OPENAI_COMPAT_STATUS_URL;
475
+ if (!url)
476
+ return;
477
+ const payload = JSON.stringify({ state, activity, tool: tool || null });
478
+ const req = http.request(url, {
479
+ method: 'POST',
480
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
481
+ timeout: 2000,
482
+ }, () => { });
483
+ req.on('error', () => { });
484
+ req.write(payload);
485
+ req.end();
486
+ }
487
+ function getToolDescription(toolName, toolInput) {
488
+ switch (toolName) {
489
+ case 'Bash':
490
+ case 'exec': {
491
+ const cmd = String(toolInput?.command || '');
492
+ return `Running: ${cmd.length > 50 ? cmd.slice(0, 50) + '...' : cmd}`;
493
+ }
494
+ case 'Read':
495
+ case 'read':
496
+ return `Reading: ${String(toolInput?.file_path || toolInput?.path || 'file')
497
+ .split('/')
498
+ .pop()}`;
499
+ case 'Write':
500
+ case 'write':
501
+ return `Writing: ${String(toolInput?.file_path || toolInput?.path || 'file')
502
+ .split('/')
503
+ .pop()}`;
504
+ case 'Edit':
505
+ case 'edit':
506
+ return `Editing: ${String(toolInput?.file_path || toolInput?.path || 'file')
507
+ .split('/')
508
+ .pop()}`;
509
+ case 'Glob':
510
+ case 'glob':
511
+ return `Searching files: ${String(toolInput?.pattern || '')}`;
512
+ case 'Grep':
513
+ case 'grep':
514
+ return `Searching content: ${String(toolInput?.pattern || '')}`;
515
+ case 'WebSearch':
516
+ return `Web search: ${String(toolInput?.query || '')}`;
517
+ case 'Agent':
518
+ return `Spawning sub-agent...`;
519
+ default:
520
+ return `Using tool: ${toolName}`;
521
+ }
522
+ }
523
+ // ─── Non-Streaming ───────────────────────────────────────────────────────────
524
+ async function handleNonStreaming(manager, sessionName, model, userMessage, completionId, res, hasTools) {
525
+ try {
526
+ reportStatus('thinking', 'Processing request...');
527
+ const result = await manager.sendMessage(sessionName, userMessage, {
528
+ onEvent: (event) => {
529
+ if (event.type === 'tool_use' && event.tool?.name) {
530
+ const desc = getToolDescription(event.tool.name, event.tool.input);
531
+ reportStatus('working', desc, event.tool.name);
532
+ }
533
+ },
534
+ });
535
+ reportStatus('idle', 'Ready');
536
+ let tokensIn = 0;
537
+ let tokensOut = 0;
538
+ try {
539
+ const status = manager.getStatus(sessionName);
540
+ tokensIn = status.stats.tokensIn;
541
+ tokensOut = status.stats.tokensOut;
542
+ }
543
+ catch {
544
+ /* stats unavailable */
545
+ }
546
+ // Parse tool_calls from response text when caller provided tools
547
+ if (hasTools) {
548
+ const parsed = parseToolCallsFromText(result.output);
549
+ const response = formatCompletionResponse(completionId, model, parsed.textContent ?? '', tokensIn, tokensOut, parsed.toolCalls.length > 0 ? parsed.toolCalls : undefined);
550
+ res.writeHead(200, { 'Content-Type': 'application/json' });
551
+ res.end(JSON.stringify(response));
552
+ }
553
+ else {
554
+ const response = formatCompletionResponse(completionId, model, result.output, tokensIn, tokensOut);
555
+ res.writeHead(200, { 'Content-Type': 'application/json' });
556
+ res.end(JSON.stringify(response));
557
+ }
558
+ }
559
+ catch (err) {
560
+ reportStatus('idle', 'Request failed');
561
+ res.writeHead(500, { 'Content-Type': 'application/json' });
562
+ res.end(JSON.stringify({ error: { message: err.message, type: 'server_error' } }));
563
+ }
564
+ }
565
+ // ─── Streaming ───────────────────────────────────────────────────────────────
566
+ async function handleStreaming(manager, sessionName, model, userMessage, completionId, res, hasTools) {
567
+ res.writeHead(200, {
568
+ 'Content-Type': 'text/event-stream',
569
+ 'Cache-Control': 'no-cache',
570
+ Connection: 'keep-alive',
571
+ 'X-Accel-Buffering': 'no',
572
+ });
573
+ let clientDisconnected = false;
574
+ res.on('close', () => {
575
+ clientDisconnected = true;
576
+ });
577
+ const writeSSE = (data) => {
578
+ if (!clientDisconnected) {
579
+ try {
580
+ res.write(`data: ${data}\n\n`);
581
+ }
582
+ catch {
583
+ clientDisconnected = true;
584
+ }
585
+ }
586
+ };
587
+ // Initial chunk with role
588
+ writeSSE(JSON.stringify(formatCompletionChunk(completionId, model, { role: 'assistant' }, null)));
589
+ // SSE keepalive heartbeat
590
+ const heartbeatTimer = setInterval(() => {
591
+ if (!clientDisconnected) {
592
+ try {
593
+ res.write(': keepalive\n\n');
594
+ }
595
+ catch {
596
+ clientDisconnected = true;
597
+ }
598
+ }
599
+ }, 30_000);
600
+ // When tools are present, buffer the full response to parse for tool_calls.
601
+ // Without tools, stream text chunks directly for low latency.
602
+ let bufferedText = '';
603
+ let streamedAnything = false;
604
+ try {
605
+ reportStatus('thinking', 'Processing request...');
606
+ const sendResult = await manager.sendMessage(sessionName, userMessage, {
607
+ onChunk: (chunk) => {
608
+ if (hasTools) {
609
+ bufferedText += chunk;
610
+ }
611
+ else {
612
+ streamedAnything = true;
613
+ writeSSE(JSON.stringify(formatCompletionChunk(completionId, model, { content: chunk }, null)));
614
+ }
615
+ },
616
+ onEvent: (event) => {
617
+ if (event.type === 'tool_use' && event.tool?.name) {
618
+ reportStatus('working', getToolDescription(event.tool.name, event.tool.input), event.tool.name);
619
+ }
620
+ },
621
+ });
622
+ // Fallback: if NOTHING reached the client (no streamed chunks AND no
623
+ // tools-buffered text) but sendResult has output, emit it. Catches the
624
+ // case where text comes only via the turn-complete event (e.g. after
625
+ // internal tool calls) and was never delivered via onChunk.
626
+ //
627
+ // Bug fix 2026-04-29: previously this condition was just
628
+ // `!bufferedText && !hasTools && sendResult.output`, which was
629
+ // ALWAYS true for no-tools turns (bufferedText is only populated
630
+ // when hasTools). Result: every no-tools turn re-emitted the full
631
+ // sendResult.output as a SECOND SSE chunk after the streamed chunks
632
+ // had already delivered the same text → doubled greeting on /new.
633
+ // Adding `streamedAnything` to the condition only fires the fallback
634
+ // when truly nothing reached the client.
635
+ if (!streamedAnything && !bufferedText && !hasTools && sendResult?.output) {
636
+ writeSSE(JSON.stringify(formatCompletionChunk(completionId, model, { content: sendResult.output }, null)));
637
+ }
638
+ reportStatus('idle', 'Ready');
639
+ // Get token usage for final chunk
640
+ let usage;
641
+ try {
642
+ const status = manager.getStatus(sessionName);
643
+ usage = {
644
+ prompt_tokens: status.stats.tokensIn,
645
+ completion_tokens: status.stats.tokensOut,
646
+ total_tokens: status.stats.tokensIn + status.stats.tokensOut,
647
+ };
648
+ }
649
+ catch {
650
+ /* best effort */
651
+ }
652
+ if (hasTools && bufferedText) {
653
+ const parsed = parseToolCallsFromText(bufferedText);
654
+ if (parsed.toolCalls.length > 0) {
655
+ // Emit text content if any
656
+ if (parsed.textContent) {
657
+ writeSSE(JSON.stringify(formatCompletionChunk(completionId, model, { content: parsed.textContent }, null)));
658
+ }
659
+ // Emit tool_call chunks
660
+ for (let i = 0; i < parsed.toolCalls.length; i++) {
661
+ const tc = parsed.toolCalls[i];
662
+ writeSSE(JSON.stringify({
663
+ id: completionId,
664
+ object: 'chat.completion.chunk',
665
+ created: Math.floor(Date.now() / 1000),
666
+ model,
667
+ choices: [
668
+ {
669
+ index: 0,
670
+ delta: {
671
+ tool_calls: [
672
+ {
673
+ index: i,
674
+ id: tc.id,
675
+ type: 'function',
676
+ function: { name: tc.function.name, arguments: tc.function.arguments },
677
+ },
678
+ ],
679
+ },
680
+ finish_reason: null,
681
+ },
682
+ ],
683
+ }));
684
+ }
685
+ // Final chunk with tool_calls finish reason
686
+ const finalChunk = formatCompletionChunk(completionId, model, {}, 'tool_calls');
687
+ if (usage)
688
+ finalChunk.usage = usage;
689
+ writeSSE(JSON.stringify(finalChunk));
690
+ }
691
+ else {
692
+ // No tool calls — emit buffered text as content
693
+ writeSSE(JSON.stringify(formatCompletionChunk(completionId, model, { content: bufferedText }, null)));
694
+ const finalChunk = formatCompletionChunk(completionId, model, {}, 'stop');
695
+ if (usage)
696
+ finalChunk.usage = usage;
697
+ writeSSE(JSON.stringify(finalChunk));
698
+ }
699
+ }
700
+ else {
701
+ // No tools — standard finish
702
+ const finalChunk = formatCompletionChunk(completionId, model, {}, 'stop');
703
+ if (usage)
704
+ finalChunk.usage = usage;
705
+ writeSSE(JSON.stringify(finalChunk));
706
+ }
707
+ writeSSE('[DONE]');
708
+ }
709
+ catch (err) {
710
+ reportStatus('idle', 'Request failed');
711
+ writeSSE(JSON.stringify({ error: { message: err.message, type: 'server_error' } }));
712
+ writeSSE('[DONE]');
713
+ }
714
+ finally {
715
+ clearInterval(heartbeatTimer);
716
+ }
717
+ if (!clientDisconnected) {
718
+ res.end();
719
+ }
720
+ }
721
+ //# sourceMappingURL=openai-compat.js.map