@0xmaxma/claude-gateway 1.8.14 → 2.0.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 (582) hide show
  1. package/README.md +28 -1613
  2. package/config.template.json +7 -1
  3. package/dist/agent/runner.d.ts +377 -0
  4. package/dist/agent/runner.d.ts.map +1 -1
  5. package/dist/agent/runner.js +680 -24
  6. package/dist/agent/runner.js.map +1 -1
  7. package/dist/agent/skill-learning/index.d.ts +6 -4
  8. package/dist/agent/skill-learning/index.d.ts.map +1 -1
  9. package/dist/agent/skill-learning/index.js +3 -3
  10. package/dist/agent/skill-learning/index.js.map +1 -1
  11. package/dist/agent/skill-learning/notifier.d.ts +3 -4
  12. package/dist/agent/skill-learning/notifier.d.ts.map +1 -1
  13. package/dist/agent/skill-learning/notifier.js +15 -13
  14. package/dist/agent/skill-learning/notifier.js.map +1 -1
  15. package/dist/agent/skill-learning/reviewer.d.ts.map +1 -1
  16. package/dist/agent/skill-learning/reviewer.js +3 -1
  17. package/dist/agent/skill-learning/reviewer.js.map +1 -1
  18. package/dist/api/connectors-router.d.ts.map +1 -1
  19. package/dist/api/connectors-router.js +110 -4
  20. package/dist/api/connectors-router.js.map +1 -1
  21. package/dist/api/gateway-router.d.ts +1 -0
  22. package/dist/api/gateway-router.d.ts.map +1 -1
  23. package/dist/api/gateway-router.js +13 -1
  24. package/dist/api/gateway-router.js.map +1 -1
  25. package/dist/api/line-webhook-router.d.ts +2 -2
  26. package/dist/api/line-webhook-router.d.ts.map +1 -1
  27. package/dist/api/line-webhook-router.js +264 -230
  28. package/dist/api/line-webhook-router.js.map +1 -1
  29. package/dist/api/router.d.ts.map +1 -1
  30. package/dist/api/router.js +135 -10
  31. package/dist/api/router.js.map +1 -1
  32. package/dist/api/share-router.d.ts.map +1 -1
  33. package/dist/api/share-router.js +29 -10
  34. package/dist/api/share-router.js.map +1 -1
  35. package/dist/api/slack-client.d.ts +2 -0
  36. package/dist/api/slack-client.d.ts.map +1 -1
  37. package/dist/api/slack-client.js +6 -2
  38. package/dist/api/slack-client.js.map +1 -1
  39. package/dist/api/slack-webhook-router.d.ts +3 -0
  40. package/dist/api/slack-webhook-router.d.ts.map +1 -1
  41. package/dist/api/slack-webhook-router.js +226 -131
  42. package/dist/api/slack-webhook-router.js.map +1 -1
  43. package/dist/api/voice-router.d.ts +21 -0
  44. package/dist/api/voice-router.d.ts.map +1 -0
  45. package/dist/api/voice-router.js +372 -0
  46. package/dist/api/voice-router.js.map +1 -0
  47. package/dist/api/voice-settings-router.d.ts +5 -0
  48. package/dist/api/voice-settings-router.d.ts.map +1 -0
  49. package/dist/api/voice-settings-router.js +318 -0
  50. package/dist/api/voice-settings-router.js.map +1 -0
  51. package/dist/apps/agent-container-migration.d.ts +11 -0
  52. package/dist/apps/agent-container-migration.d.ts.map +1 -0
  53. package/dist/apps/agent-container-migration.js +85 -0
  54. package/dist/apps/agent-container-migration.js.map +1 -0
  55. package/dist/apps/installer.d.ts +1 -1
  56. package/dist/apps/installer.d.ts.map +1 -1
  57. package/dist/apps/installer.js +2 -1
  58. package/dist/apps/installer.js.map +1 -1
  59. package/dist/cli/commands/tasks.d.ts +5 -0
  60. package/dist/cli/commands/tasks.d.ts.map +1 -0
  61. package/dist/cli/commands/tasks.js +92 -0
  62. package/dist/cli/commands/tasks.js.map +1 -0
  63. package/dist/cli/http-client.d.ts +2 -0
  64. package/dist/cli/http-client.d.ts.map +1 -1
  65. package/dist/cli/http-client.js +8 -3
  66. package/dist/cli/http-client.js.map +1 -1
  67. package/dist/cli/index.d.ts.map +1 -1
  68. package/dist/cli/index.js +5 -1
  69. package/dist/cli/index.js.map +1 -1
  70. package/dist/config/config-write-lock.d.ts +2 -0
  71. package/dist/config/config-write-lock.d.ts.map +1 -1
  72. package/dist/config/config-write-lock.js +5 -0
  73. package/dist/config/config-write-lock.js.map +1 -1
  74. package/dist/config/loader.d.ts.map +1 -1
  75. package/dist/config/loader.js +87 -2
  76. package/dist/config/loader.js.map +1 -1
  77. package/dist/config/migrator.d.ts.map +1 -1
  78. package/dist/config/migrator.js +12 -1
  79. package/dist/config/migrator.js.map +1 -1
  80. package/dist/config/watcher.d.ts.map +1 -1
  81. package/dist/config/watcher.js +6 -0
  82. package/dist/config/watcher.js.map +1 -1
  83. package/dist/connectors/resolve.d.ts.map +1 -1
  84. package/dist/connectors/resolve.js +3 -1
  85. package/dist/connectors/resolve.js.map +1 -1
  86. package/dist/connectors/types.d.ts +8 -0
  87. package/dist/connectors/types.d.ts.map +1 -1
  88. package/dist/cron/manager.d.ts.map +1 -1
  89. package/dist/cron/manager.js +21 -2
  90. package/dist/cron/manager.js.map +1 -1
  91. package/dist/discord/receiver.d.ts +2 -1
  92. package/dist/discord/receiver.d.ts.map +1 -1
  93. package/dist/discord/receiver.js +5 -1
  94. package/dist/discord/receiver.js.map +1 -1
  95. package/dist/history/db.d.ts +3 -0
  96. package/dist/history/db.d.ts.map +1 -1
  97. package/dist/history/db.js +21 -1
  98. package/dist/history/db.js.map +1 -1
  99. package/dist/history/types.d.ts +2 -0
  100. package/dist/history/types.d.ts.map +1 -1
  101. package/dist/index.js +48 -9
  102. package/dist/index.js.map +1 -1
  103. package/dist/orchestration/bounded-queue.d.ts +15 -0
  104. package/dist/orchestration/bounded-queue.d.ts.map +1 -0
  105. package/dist/orchestration/bounded-queue.js +58 -0
  106. package/dist/orchestration/bounded-queue.js.map +1 -0
  107. package/dist/orchestration/bridge.d.ts +55 -0
  108. package/dist/orchestration/bridge.d.ts.map +1 -0
  109. package/dist/orchestration/bridge.js +234 -0
  110. package/dist/orchestration/bridge.js.map +1 -0
  111. package/dist/orchestration/browser-routing.d.ts +7 -0
  112. package/dist/orchestration/browser-routing.d.ts.map +1 -0
  113. package/dist/orchestration/browser-routing.js +22 -0
  114. package/dist/orchestration/browser-routing.js.map +1 -0
  115. package/dist/orchestration/capabilities.d.ts +49 -0
  116. package/dist/orchestration/capabilities.d.ts.map +1 -0
  117. package/dist/orchestration/capabilities.js +417 -0
  118. package/dist/orchestration/capabilities.js.map +1 -0
  119. package/dist/orchestration/capacity.d.ts +14 -0
  120. package/dist/orchestration/capacity.d.ts.map +1 -0
  121. package/dist/orchestration/capacity.js +52 -0
  122. package/dist/orchestration/capacity.js.map +1 -0
  123. package/dist/orchestration/channel-activity.d.ts +21 -0
  124. package/dist/orchestration/channel-activity.d.ts.map +1 -0
  125. package/dist/orchestration/channel-activity.js +72 -0
  126. package/dist/orchestration/channel-activity.js.map +1 -0
  127. package/dist/orchestration/channel-controls.d.ts +32 -0
  128. package/dist/orchestration/channel-controls.d.ts.map +1 -0
  129. package/dist/orchestration/channel-controls.js +105 -0
  130. package/dist/orchestration/channel-controls.js.map +1 -0
  131. package/dist/orchestration/channel-input-media.d.ts +11 -0
  132. package/dist/orchestration/channel-input-media.d.ts.map +1 -0
  133. package/dist/orchestration/channel-input-media.js +54 -0
  134. package/dist/orchestration/channel-input-media.js.map +1 -0
  135. package/dist/orchestration/channel-media.d.ts +4 -0
  136. package/dist/orchestration/channel-media.d.ts.map +1 -0
  137. package/dist/orchestration/channel-media.js +70 -0
  138. package/dist/orchestration/channel-media.js.map +1 -0
  139. package/dist/orchestration/channel-speech.d.ts +7 -0
  140. package/dist/orchestration/channel-speech.d.ts.map +1 -0
  141. package/dist/orchestration/channel-speech.js +65 -0
  142. package/dist/orchestration/channel-speech.js.map +1 -0
  143. package/dist/orchestration/cli-skills.d.ts +12 -0
  144. package/dist/orchestration/cli-skills.d.ts.map +1 -0
  145. package/dist/orchestration/cli-skills.js +132 -0
  146. package/dist/orchestration/cli-skills.js.map +1 -0
  147. package/dist/orchestration/config.d.ts +256 -0
  148. package/dist/orchestration/config.d.ts.map +1 -0
  149. package/dist/orchestration/config.js +110 -0
  150. package/dist/orchestration/config.js.map +1 -0
  151. package/dist/orchestration/container.d.ts +21 -0
  152. package/dist/orchestration/container.d.ts.map +1 -0
  153. package/dist/orchestration/container.js +195 -0
  154. package/dist/orchestration/container.js.map +1 -0
  155. package/dist/orchestration/control-delivery.d.ts +5 -0
  156. package/dist/orchestration/control-delivery.d.ts.map +1 -0
  157. package/dist/orchestration/control-delivery.js +50 -0
  158. package/dist/orchestration/control-delivery.js.map +1 -0
  159. package/dist/orchestration/conversation-intake.d.ts +32 -0
  160. package/dist/orchestration/conversation-intake.d.ts.map +1 -0
  161. package/dist/orchestration/conversation-intake.js +92 -0
  162. package/dist/orchestration/conversation-intake.js.map +1 -0
  163. package/dist/orchestration/decisions.d.ts +27 -0
  164. package/dist/orchestration/decisions.d.ts.map +1 -0
  165. package/dist/orchestration/decisions.js +148 -0
  166. package/dist/orchestration/decisions.js.map +1 -0
  167. package/dist/orchestration/delivery.d.ts +30 -0
  168. package/dist/orchestration/delivery.d.ts.map +1 -0
  169. package/dist/orchestration/delivery.js +219 -0
  170. package/dist/orchestration/delivery.js.map +1 -0
  171. package/dist/orchestration/display-stream.d.ts +4 -0
  172. package/dist/orchestration/display-stream.d.ts.map +1 -0
  173. package/dist/orchestration/display-stream.js +77 -0
  174. package/dist/orchestration/display-stream.js.map +1 -0
  175. package/dist/orchestration/events.d.ts +30 -0
  176. package/dist/orchestration/events.d.ts.map +1 -0
  177. package/dist/orchestration/events.js +65 -0
  178. package/dist/orchestration/events.js.map +1 -0
  179. package/dist/orchestration/execution-observation.d.ts +38 -0
  180. package/dist/orchestration/execution-observation.d.ts.map +1 -0
  181. package/dist/orchestration/execution-observation.js +25 -0
  182. package/dist/orchestration/execution-observation.js.map +1 -0
  183. package/dist/orchestration/file-delivery.d.ts +16 -0
  184. package/dist/orchestration/file-delivery.d.ts.map +1 -0
  185. package/dist/orchestration/file-delivery.js +139 -0
  186. package/dist/orchestration/file-delivery.js.map +1 -0
  187. package/dist/orchestration/gateway-config.d.ts +17 -0
  188. package/dist/orchestration/gateway-config.d.ts.map +1 -0
  189. package/dist/orchestration/gateway-config.js +128 -0
  190. package/dist/orchestration/gateway-config.js.map +1 -0
  191. package/dist/orchestration/history.d.ts +13 -0
  192. package/dist/orchestration/history.d.ts.map +1 -0
  193. package/dist/orchestration/history.js +48 -0
  194. package/dist/orchestration/history.js.map +1 -0
  195. package/dist/orchestration/identity.d.ts +6 -0
  196. package/dist/orchestration/identity.d.ts.map +1 -0
  197. package/dist/orchestration/identity.js +13 -0
  198. package/dist/orchestration/identity.js.map +1 -0
  199. package/dist/orchestration/inference-errors.d.ts +3 -0
  200. package/dist/orchestration/inference-errors.d.ts.map +1 -0
  201. package/dist/orchestration/inference-errors.js +16 -0
  202. package/dist/orchestration/inference-errors.js.map +1 -0
  203. package/dist/orchestration/input-images.d.ts +12 -0
  204. package/dist/orchestration/input-images.d.ts.map +1 -0
  205. package/dist/orchestration/input-images.js +66 -0
  206. package/dist/orchestration/input-images.js.map +1 -0
  207. package/dist/orchestration/instance-lock.d.ts +5 -0
  208. package/dist/orchestration/instance-lock.d.ts.map +1 -0
  209. package/dist/orchestration/instance-lock.js +33 -0
  210. package/dist/orchestration/instance-lock.js.map +1 -0
  211. package/dist/orchestration/line-loading.d.ts +21 -0
  212. package/dist/orchestration/line-loading.d.ts.map +1 -0
  213. package/dist/orchestration/line-loading.js +89 -0
  214. package/dist/orchestration/line-loading.js.map +1 -0
  215. package/dist/orchestration/media.d.ts +3 -0
  216. package/dist/orchestration/media.d.ts.map +1 -0
  217. package/dist/orchestration/media.js +46 -0
  218. package/dist/orchestration/media.js.map +1 -0
  219. package/dist/orchestration/migrations/schema.d.ts +3 -0
  220. package/dist/orchestration/migrations/schema.d.ts.map +1 -0
  221. package/dist/orchestration/migrations/schema.js +80 -0
  222. package/dist/orchestration/migrations/schema.js.map +1 -0
  223. package/dist/orchestration/notification-mailbox.d.ts +5 -0
  224. package/dist/orchestration/notification-mailbox.d.ts.map +1 -0
  225. package/dist/orchestration/notification-mailbox.js +29 -0
  226. package/dist/orchestration/notification-mailbox.js.map +1 -0
  227. package/dist/orchestration/process-activity.d.ts +23 -0
  228. package/dist/orchestration/process-activity.d.ts.map +1 -0
  229. package/dist/orchestration/process-activity.js +95 -0
  230. package/dist/orchestration/process-activity.js.map +1 -0
  231. package/dist/orchestration/process-supervisor.d.ts +15 -0
  232. package/dist/orchestration/process-supervisor.d.ts.map +1 -0
  233. package/dist/orchestration/process-supervisor.js +92 -0
  234. package/dist/orchestration/process-supervisor.js.map +1 -0
  235. package/dist/orchestration/process-turn.d.ts +35 -0
  236. package/dist/orchestration/process-turn.d.ts.map +1 -0
  237. package/dist/orchestration/process-turn.js +246 -0
  238. package/dist/orchestration/process-turn.js.map +1 -0
  239. package/dist/orchestration/progress-review.d.ts +28 -0
  240. package/dist/orchestration/progress-review.d.ts.map +1 -0
  241. package/dist/orchestration/progress-review.js +48 -0
  242. package/dist/orchestration/progress-review.js.map +1 -0
  243. package/dist/orchestration/recovery.d.ts +8 -0
  244. package/dist/orchestration/recovery.d.ts.map +1 -0
  245. package/dist/orchestration/recovery.js +35 -0
  246. package/dist/orchestration/recovery.js.map +1 -0
  247. package/dist/orchestration/reply-context.d.ts +7 -0
  248. package/dist/orchestration/reply-context.d.ts.map +1 -0
  249. package/dist/orchestration/reply-context.js +54 -0
  250. package/dist/orchestration/reply-context.js.map +1 -0
  251. package/dist/orchestration/runtime.d.ts +232 -0
  252. package/dist/orchestration/runtime.d.ts.map +1 -0
  253. package/dist/orchestration/runtime.js +962 -0
  254. package/dist/orchestration/runtime.js.map +1 -0
  255. package/dist/orchestration/skills.d.ts +17 -0
  256. package/dist/orchestration/skills.d.ts.map +1 -0
  257. package/dist/orchestration/skills.js +32 -0
  258. package/dist/orchestration/skills.js.map +1 -0
  259. package/dist/orchestration/source-policy.d.ts +6 -0
  260. package/dist/orchestration/source-policy.d.ts.map +1 -0
  261. package/dist/orchestration/source-policy.js +16 -0
  262. package/dist/orchestration/source-policy.js.map +1 -0
  263. package/dist/orchestration/speech.d.ts +24 -0
  264. package/dist/orchestration/speech.d.ts.map +1 -0
  265. package/dist/orchestration/speech.js +61 -0
  266. package/dist/orchestration/speech.js.map +1 -0
  267. package/dist/orchestration/stop-controls.d.ts +30 -0
  268. package/dist/orchestration/stop-controls.d.ts.map +1 -0
  269. package/dist/orchestration/stop-controls.js +79 -0
  270. package/dist/orchestration/stop-controls.js.map +1 -0
  271. package/dist/orchestration/store.d.ts +86 -0
  272. package/dist/orchestration/store.d.ts.map +1 -0
  273. package/dist/orchestration/store.js +275 -0
  274. package/dist/orchestration/store.js.map +1 -0
  275. package/dist/orchestration/task-controls.d.ts +130 -0
  276. package/dist/orchestration/task-controls.d.ts.map +1 -0
  277. package/dist/orchestration/task-controls.js +55 -0
  278. package/dist/orchestration/task-controls.js.map +1 -0
  279. package/dist/orchestration/task-files.d.ts +29 -0
  280. package/dist/orchestration/task-files.d.ts.map +1 -0
  281. package/dist/orchestration/task-files.js +200 -0
  282. package/dist/orchestration/task-files.js.map +1 -0
  283. package/dist/orchestration/task-report.d.ts +7 -0
  284. package/dist/orchestration/task-report.d.ts.map +1 -0
  285. package/dist/orchestration/task-report.js +33 -0
  286. package/dist/orchestration/task-report.js.map +1 -0
  287. package/dist/orchestration/tasks/checkpoint-hook.d.ts +19 -0
  288. package/dist/orchestration/tasks/checkpoint-hook.d.ts.map +1 -0
  289. package/dist/orchestration/tasks/checkpoint-hook.js +49 -0
  290. package/dist/orchestration/tasks/checkpoint-hook.js.map +1 -0
  291. package/dist/orchestration/tasks/cleanup.d.ts +18 -0
  292. package/dist/orchestration/tasks/cleanup.d.ts.map +1 -0
  293. package/dist/orchestration/tasks/cleanup.js +113 -0
  294. package/dist/orchestration/tasks/cleanup.js.map +1 -0
  295. package/dist/orchestration/tasks/driver.d.ts +23 -0
  296. package/dist/orchestration/tasks/driver.d.ts.map +1 -0
  297. package/dist/orchestration/tasks/driver.js +209 -0
  298. package/dist/orchestration/tasks/driver.js.map +1 -0
  299. package/dist/orchestration/tasks/failure.d.ts +5 -0
  300. package/dist/orchestration/tasks/failure.d.ts.map +1 -0
  301. package/dist/orchestration/tasks/failure.js +17 -0
  302. package/dist/orchestration/tasks/failure.js.map +1 -0
  303. package/dist/orchestration/tasks/pool.d.ts +18 -0
  304. package/dist/orchestration/tasks/pool.d.ts.map +1 -0
  305. package/dist/orchestration/tasks/pool.js +73 -0
  306. package/dist/orchestration/tasks/pool.js.map +1 -0
  307. package/dist/orchestration/tasks/scheduler.d.ts +38 -0
  308. package/dist/orchestration/tasks/scheduler.d.ts.map +1 -0
  309. package/dist/orchestration/tasks/scheduler.js +160 -0
  310. package/dist/orchestration/tasks/scheduler.js.map +1 -0
  311. package/dist/orchestration/tasks/service.d.ts +109 -0
  312. package/dist/orchestration/tasks/service.d.ts.map +1 -0
  313. package/dist/orchestration/tasks/service.js +697 -0
  314. package/dist/orchestration/tasks/service.js.map +1 -0
  315. package/dist/orchestration/tasks/shared-workspace.d.ts +25 -0
  316. package/dist/orchestration/tasks/shared-workspace.d.ts.map +1 -0
  317. package/dist/orchestration/tasks/shared-workspace.js +162 -0
  318. package/dist/orchestration/tasks/shared-workspace.js.map +1 -0
  319. package/dist/orchestration/tasks/task-directive.d.ts +6 -0
  320. package/dist/orchestration/tasks/task-directive.d.ts.map +1 -0
  321. package/dist/orchestration/tasks/task-directive.js +41 -0
  322. package/dist/orchestration/tasks/task-directive.js.map +1 -0
  323. package/dist/orchestration/tasks/timing.d.ts +15 -0
  324. package/dist/orchestration/tasks/timing.d.ts.map +1 -0
  325. package/dist/orchestration/tasks/timing.js +48 -0
  326. package/dist/orchestration/tasks/timing.js.map +1 -0
  327. package/dist/orchestration/tasks/tool-repetition.d.ts +3 -0
  328. package/dist/orchestration/tasks/tool-repetition.d.ts.map +1 -0
  329. package/dist/orchestration/tasks/tool-repetition.js +30 -0
  330. package/dist/orchestration/tasks/tool-repetition.js.map +1 -0
  331. package/dist/orchestration/tasks/workspace.d.ts +24 -0
  332. package/dist/orchestration/tasks/workspace.d.ts.map +1 -0
  333. package/dist/orchestration/tasks/workspace.js +147 -0
  334. package/dist/orchestration/tasks/workspace.js.map +1 -0
  335. package/dist/orchestration/telegram-speech.d.ts +17 -0
  336. package/dist/orchestration/telegram-speech.d.ts.map +1 -0
  337. package/dist/orchestration/telegram-speech.js +55 -0
  338. package/dist/orchestration/telegram-speech.js.map +1 -0
  339. package/dist/orchestration/telegram-tool-status.d.ts +23 -0
  340. package/dist/orchestration/telegram-tool-status.d.ts.map +1 -0
  341. package/dist/orchestration/telegram-tool-status.js +207 -0
  342. package/dist/orchestration/telegram-tool-status.js.map +1 -0
  343. package/dist/orchestration/telegram-voices.d.ts +42 -0
  344. package/dist/orchestration/telegram-voices.d.ts.map +1 -0
  345. package/dist/orchestration/telegram-voices.js +70 -0
  346. package/dist/orchestration/telegram-voices.js.map +1 -0
  347. package/dist/orchestration/tool-activity.d.ts +10 -0
  348. package/dist/orchestration/tool-activity.d.ts.map +1 -0
  349. package/dist/orchestration/tool-activity.js +38 -0
  350. package/dist/orchestration/tool-activity.js.map +1 -0
  351. package/dist/orchestration/types.d.ts +177 -0
  352. package/dist/orchestration/types.d.ts.map +1 -0
  353. package/dist/orchestration/types.js +13 -0
  354. package/dist/orchestration/types.js.map +1 -0
  355. package/dist/orchestration/voice-reply-policy.d.ts +8 -0
  356. package/dist/orchestration/voice-reply-policy.d.ts.map +1 -0
  357. package/dist/orchestration/voice-reply-policy.js +46 -0
  358. package/dist/orchestration/voice-reply-policy.js.map +1 -0
  359. package/dist/orchestration/worker-shares.d.ts +8 -0
  360. package/dist/orchestration/worker-shares.d.ts.map +1 -0
  361. package/dist/orchestration/worker-shares.js +59 -0
  362. package/dist/orchestration/worker-shares.js.map +1 -0
  363. package/dist/orchestration/workflow.d.ts +121 -0
  364. package/dist/orchestration/workflow.d.ts.map +1 -0
  365. package/dist/orchestration/workflow.js +69 -0
  366. package/dist/orchestration/workflow.js.map +1 -0
  367. package/dist/session/input-image.d.ts +10 -0
  368. package/dist/session/input-image.d.ts.map +1 -0
  369. package/dist/session/input-image.js +3 -0
  370. package/dist/session/input-image.js.map +1 -0
  371. package/dist/session/process.d.ts +15 -3
  372. package/dist/session/process.d.ts.map +1 -1
  373. package/dist/session/process.js +143 -35
  374. package/dist/session/process.js.map +1 -1
  375. package/dist/session/runtime-profile.d.ts +36 -0
  376. package/dist/session/runtime-profile.d.ts.map +1 -0
  377. package/dist/session/runtime-profile.js +63 -0
  378. package/dist/session/runtime-profile.js.map +1 -0
  379. package/dist/session/store.d.ts.map +1 -1
  380. package/dist/session/store.js +23 -1
  381. package/dist/session/store.js.map +1 -1
  382. package/dist/share/share-store.d.ts +3 -1
  383. package/dist/share/share-store.d.ts.map +1 -1
  384. package/dist/share/share-store.js +13 -2
  385. package/dist/share/share-store.js.map +1 -1
  386. package/dist/shared/line-request-order.d.ts +2 -0
  387. package/dist/shared/line-request-order.d.ts.map +1 -0
  388. package/dist/shared/line-request-order.js +16 -0
  389. package/dist/shared/line-request-order.js.map +1 -0
  390. package/dist/shared/task-elapsed.d.ts +3 -0
  391. package/dist/shared/task-elapsed.d.ts.map +1 -0
  392. package/dist/shared/task-elapsed.js +12 -0
  393. package/dist/shared/task-elapsed.js.map +1 -0
  394. package/dist/shared/task-updated-time.d.ts +3 -0
  395. package/dist/shared/task-updated-time.d.ts.map +1 -0
  396. package/dist/shared/task-updated-time.js +15 -0
  397. package/dist/shared/task-updated-time.js.map +1 -0
  398. package/dist/skills/loader.d.ts +2 -0
  399. package/dist/skills/loader.d.ts.map +1 -1
  400. package/dist/skills/loader.js.map +1 -1
  401. package/dist/telegram/chunks.d.ts +19 -0
  402. package/dist/telegram/chunks.d.ts.map +1 -0
  403. package/dist/telegram/chunks.js +129 -0
  404. package/dist/telegram/chunks.js.map +1 -0
  405. package/dist/telegram/markdown.d.ts.map +1 -1
  406. package/dist/telegram/markdown.js +18 -3
  407. package/dist/telegram/markdown.js.map +1 -1
  408. package/dist/telegram/receiver.d.ts +2 -1
  409. package/dist/telegram/receiver.d.ts.map +1 -1
  410. package/dist/telegram/receiver.js +6 -1
  411. package/dist/telegram/receiver.js.map +1 -1
  412. package/dist/types.d.ts +14 -0
  413. package/dist/types.d.ts.map +1 -1
  414. package/dist/ui/web-ui.d.ts.map +1 -1
  415. package/dist/ui/web-ui.js +31 -5
  416. package/dist/ui/web-ui.js.map +1 -1
  417. package/dist/voice/diagnostics.d.ts +29 -0
  418. package/dist/voice/diagnostics.d.ts.map +1 -0
  419. package/dist/voice/diagnostics.js +37 -0
  420. package/dist/voice/diagnostics.js.map +1 -0
  421. package/dist/voice/errors.d.ts +19 -0
  422. package/dist/voice/errors.d.ts.map +1 -0
  423. package/dist/voice/errors.js +133 -0
  424. package/dist/voice/errors.js.map +1 -0
  425. package/dist/voice/line-audio.d.ts +4 -0
  426. package/dist/voice/line-audio.d.ts.map +1 -0
  427. package/dist/voice/line-audio.js +27 -0
  428. package/dist/voice/line-audio.js.map +1 -0
  429. package/dist/voice/managed-quota.d.ts +5 -0
  430. package/dist/voice/managed-quota.d.ts.map +1 -0
  431. package/dist/voice/managed-quota.js +35 -0
  432. package/dist/voice/managed-quota.js.map +1 -0
  433. package/dist/voice/mp3.d.ts +3 -0
  434. package/dist/voice/mp3.d.ts.map +1 -0
  435. package/dist/voice/mp3.js +30 -0
  436. package/dist/voice/mp3.js.map +1 -0
  437. package/dist/voice/notes.d.ts +9 -0
  438. package/dist/voice/notes.d.ts.map +1 -0
  439. package/dist/voice/notes.js +68 -0
  440. package/dist/voice/notes.js.map +1 -0
  441. package/dist/voice/playback.d.ts +22 -0
  442. package/dist/voice/playback.d.ts.map +1 -0
  443. package/dist/voice/playback.js +52 -0
  444. package/dist/voice/playback.js.map +1 -0
  445. package/dist/voice/preview.d.ts +6 -0
  446. package/dist/voice/preview.d.ts.map +1 -0
  447. package/dist/voice/preview.js +44 -0
  448. package/dist/voice/preview.js.map +1 -0
  449. package/dist/voice/protocol.d.ts +10 -0
  450. package/dist/voice/protocol.d.ts.map +1 -0
  451. package/dist/voice/protocol.js +30 -0
  452. package/dist/voice/protocol.js.map +1 -0
  453. package/dist/voice/providers/cartesia-tts.d.ts +26 -0
  454. package/dist/voice/providers/cartesia-tts.d.ts.map +1 -0
  455. package/dist/voice/providers/cartesia-tts.js +95 -0
  456. package/dist/voice/providers/cartesia-tts.js.map +1 -0
  457. package/dist/voice/providers/deepgram-stt.d.ts +20 -0
  458. package/dist/voice/providers/deepgram-stt.d.ts.map +1 -0
  459. package/dist/voice/providers/deepgram-stt.js +66 -0
  460. package/dist/voice/providers/deepgram-stt.js.map +1 -0
  461. package/dist/voice/providers/elevenlabs-stt.d.ts +20 -0
  462. package/dist/voice/providers/elevenlabs-stt.d.ts.map +1 -0
  463. package/dist/voice/providers/elevenlabs-stt.js +92 -0
  464. package/dist/voice/providers/elevenlabs-stt.js.map +1 -0
  465. package/dist/voice/providers/elevenlabs-tts.d.ts +26 -0
  466. package/dist/voice/providers/elevenlabs-tts.d.ts.map +1 -0
  467. package/dist/voice/providers/elevenlabs-tts.js +82 -0
  468. package/dist/voice/providers/elevenlabs-tts.js.map +1 -0
  469. package/dist/voice/providers/encoded-audio-tts.d.ts +21 -0
  470. package/dist/voice/providers/encoded-audio-tts.d.ts.map +1 -0
  471. package/dist/voice/providers/encoded-audio-tts.js +175 -0
  472. package/dist/voice/providers/encoded-audio-tts.js.map +1 -0
  473. package/dist/voice/providers/fake.d.ts +39 -0
  474. package/dist/voice/providers/fake.d.ts.map +1 -0
  475. package/dist/voice/providers/fake.js +41 -0
  476. package/dist/voice/providers/fake.js.map +1 -0
  477. package/dist/voice/providers/gemini-live-stt.d.ts +21 -0
  478. package/dist/voice/providers/gemini-live-stt.d.ts.map +1 -0
  479. package/dist/voice/providers/gemini-live-stt.js +158 -0
  480. package/dist/voice/providers/gemini-live-stt.js.map +1 -0
  481. package/dist/voice/providers/gemini.d.ts +60 -0
  482. package/dist/voice/providers/gemini.d.ts.map +1 -0
  483. package/dist/voice/providers/gemini.js +124 -0
  484. package/dist/voice/providers/gemini.js.map +1 -0
  485. package/dist/voice/providers/model-ref.d.ts +9 -0
  486. package/dist/voice/providers/model-ref.d.ts.map +1 -0
  487. package/dist/voice/providers/model-ref.js +31 -0
  488. package/dist/voice/providers/model-ref.js.map +1 -0
  489. package/dist/voice/providers/openrouter.d.ts +39 -0
  490. package/dist/voice/providers/openrouter.d.ts.map +1 -0
  491. package/dist/voice/providers/openrouter.js +154 -0
  492. package/dist/voice/providers/openrouter.js.map +1 -0
  493. package/dist/voice/providers/paxalabs-stt.d.ts +12 -0
  494. package/dist/voice/providers/paxalabs-stt.d.ts.map +1 -0
  495. package/dist/voice/providers/paxalabs-stt.js +41 -0
  496. package/dist/voice/providers/paxalabs-stt.js.map +1 -0
  497. package/dist/voice/providers/paxalabs-tts.d.ts +23 -0
  498. package/dist/voice/providers/paxalabs-tts.d.ts.map +1 -0
  499. package/dist/voice/providers/paxalabs-tts.js +80 -0
  500. package/dist/voice/providers/paxalabs-tts.js.map +1 -0
  501. package/dist/voice/providers/recorded-stt.d.ts +34 -0
  502. package/dist/voice/providers/recorded-stt.d.ts.map +1 -0
  503. package/dist/voice/providers/recorded-stt.js +59 -0
  504. package/dist/voice/providers/recorded-stt.js.map +1 -0
  505. package/dist/voice/providers/registry.d.ts +10 -0
  506. package/dist/voice/providers/registry.d.ts.map +1 -0
  507. package/dist/voice/providers/registry.js +77 -0
  508. package/dist/voice/providers/registry.js.map +1 -0
  509. package/dist/voice/providers/socket.d.ts +10 -0
  510. package/dist/voice/providers/socket.d.ts.map +1 -0
  511. package/dist/voice/providers/socket.js +91 -0
  512. package/dist/voice/providers/socket.js.map +1 -0
  513. package/dist/voice/providers/upstream.d.ts +9 -0
  514. package/dist/voice/providers/upstream.d.ts.map +1 -0
  515. package/dist/voice/providers/upstream.js +51 -0
  516. package/dist/voice/providers/upstream.js.map +1 -0
  517. package/dist/voice/providers/voice-catalog.d.ts +17 -0
  518. package/dist/voice/providers/voice-catalog.d.ts.map +1 -0
  519. package/dist/voice/providers/voice-catalog.js +167 -0
  520. package/dist/voice/providers/voice-catalog.js.map +1 -0
  521. package/dist/voice/queue.d.ts +2 -0
  522. package/dist/voice/queue.d.ts.map +1 -0
  523. package/dist/voice/queue.js +6 -0
  524. package/dist/voice/queue.js.map +1 -0
  525. package/dist/voice/session.d.ts +86 -0
  526. package/dist/voice/session.d.ts.map +1 -0
  527. package/dist/voice/session.js +464 -0
  528. package/dist/voice/session.js.map +1 -0
  529. package/dist/voice/turn-manager.d.ts +27 -0
  530. package/dist/voice/turn-manager.d.ts.map +1 -0
  531. package/dist/voice/turn-manager.js +75 -0
  532. package/dist/voice/turn-manager.js.map +1 -0
  533. package/dist/voice/types.d.ts +78 -0
  534. package/dist/voice/types.d.ts.map +1 -0
  535. package/dist/voice/types.js +18 -0
  536. package/dist/voice/types.js.map +1 -0
  537. package/dist/voice/wav.d.ts +2 -0
  538. package/dist/voice/wav.d.ts.map +1 -0
  539. package/dist/voice/wav.js +21 -0
  540. package/dist/voice/wav.js.map +1 -0
  541. package/dist/wechat/ilink-client.d.ts +13 -0
  542. package/dist/wechat/ilink-client.d.ts.map +1 -1
  543. package/dist/wechat/ilink-client.js +19 -0
  544. package/dist/wechat/ilink-client.js.map +1 -1
  545. package/dist/whatsapp/manager.d.ts.map +1 -1
  546. package/dist/whatsapp/manager.js +51 -1
  547. package/dist/whatsapp/manager.js.map +1 -1
  548. package/mcp/capability-catalog.ts +206 -0
  549. package/mcp/modules.ts +56 -0
  550. package/mcp/server.ts +18 -37
  551. package/mcp/tools/browser/module.ts +1 -1
  552. package/mcp/tools/browser/skills/open-browser/SKILL.md +5 -3
  553. package/mcp/tools/discord/commands.ts +17 -1
  554. package/mcp/tools/discord/inbound.ts +11 -0
  555. package/mcp/tools/discord/module.ts +42 -2
  556. package/mcp/tools/discord/receiver-server.ts +10 -0
  557. package/mcp/tools/discord/types.ts +4 -1
  558. package/mcp/tools/image/module.ts +3 -1
  559. package/mcp/tools/receiver-spool.test.ts +28 -0
  560. package/mcp/tools/receiver-spool.ts +56 -0
  561. package/mcp/tools/share-file/module.ts +9 -1
  562. package/mcp/tools/shared/share-client.ts +19 -0
  563. package/mcp/tools/tasks/module.ts +35 -0
  564. package/mcp/tools/telegram/media-group.ts +21 -0
  565. package/mcp/tools/telegram/receiver-server.ts +232 -11
  566. package/mcp/tools/telegram/selection-confirmation.ts +20 -0
  567. package/mcp/tools/telegram/task-browser.ts +117 -0
  568. package/mcp/tools/telegram/task-detail.ts +36 -0
  569. package/mcp/tools/telegram/task-elapsed.ts +1 -0
  570. package/mcp/tools/telegram/typing.ts +6 -116
  571. package/mcp/tools/video/module.ts +1 -1
  572. package/mcp/types.ts +5 -0
  573. package/package.json +2 -1
  574. package/resource/claude_gateway.jpeg +0 -0
  575. package/scripts/orchestration/bench-store.cjs +30 -0
  576. package/scripts/orchestration/inspect.cjs +16 -0
  577. package/scripts/orchestration/probe-profile.cjs +67 -0
  578. package/scripts/orchestration/reconcile.cjs +21 -0
  579. package/scripts/orchestration/smoke-elevenlabs.cjs +90 -0
  580. package/scripts/orchestration/smoke-media.cjs +70 -0
  581. package/scripts/orchestration/smoke-text.cjs +101 -0
  582. package/scripts/orchestration/smoke-vision.cjs +49 -0
package/README.md CHANGED
@@ -1,19 +1,24 @@
1
-
2
1
  # Claude Gateway
3
2
 
4
- A self-hosted multi-agent gateway for Claude Code — with agents that improve themselves, manage their own memory through nightly dreaming, and build a searchable knowledge base from what they learn.
3
+ **An orchestrator, voice, and multi-channel platform for Claude Code.**
4
+
5
+ Claude Gateway keeps conversations responsive while Claude Code workers execute tasks. Talk or type through your connected channels, follow progress, and carry your agents' memory and skills across sessions.
5
6
 
6
7
  <p align="center">
7
- <img src="resource/claude_gateway.svg" alt="Claude Gateway" width="680" />
8
+ <img src="resource/claude_gateway.jpeg" alt="Claude Gateway" width="680" />
8
9
  </p>
9
10
 
10
- ---
11
+ [Documentation](https://0xmaxma.github.io/claude-gateway/) · [Quickstart](https://0xmaxma.github.io/claude-gateway/guide/quickstart.html) · [API reference](https://0xmaxma.github.io/claude-gateway/api/)
11
12
 
12
13
  ## Features
13
14
 
14
- - 🧠 **Skill self-improvement** agents learn reusable skills from their own work: after a substantive turn a background reviewer creates or updates a skill, hot-reloaded for the next turn. Provenance-guarded (never overwrites human-written skills), capped per day, and audited to `SKILLS_LEARNED.md`. See [`gateway.skillLearning`](#gatewayskilllearning)
15
- - 📚 **Knowledge base (two-lane memory)** — per-agent SQLite/FTS5 searchable archive exposed through `memory_search` / `memory_get` MCP tools, so agents recall notes that don't fit the always-injected core; chunks carry fail-closed provenance and the index is refreshed off the gateway event loop. See [`gateway.knowledge`](#gatewayknowledge)
16
- - 🌙 **Nightly dreaming** — background consolidation of long-term memory: a print-only reviewer proposes ops that a safe applier writes to `MEMORY.md` / `USER.md` (backup, bounded-loss, net-negative when over budget). Deterministic compaction, budget-scaled pruning, and staleness GC keep memory near budget without forgetting — archived entries stay searchable. See [`gateway.dreaming`](#gatewaydreaming)
15
+ - 🪄 **Agent Orchestration Engine (enabled by default)** responsive conversations while workers execute durable tasks. Existing configurations without the switch are upgraded automatically; explicit `false` remains an opt-out. See [orchestration settings](https://0xmaxma.github.io/claude-gateway/reference/orchestration-settings.html).
16
+
17
+ - 🔥 **Agent orchestration** — keep conversations responsive while reusable workers execute durable tasks, report progress, accept follow-up instructions, and support cancellation. See [orchestration and tasks](https://0xmaxma.github.io/claude-gateway/guide/orchestration.html).
18
+ - 🎙️ **Voice conversations** — speech recognition and spoken replies with per-agent models and voices, direct or upstream providers, live or recorded speech input, and audio replay. See [voice setup](https://0xmaxma.github.io/claude-gateway/guide/voice.html).
19
+ - 🧠 **Skill self-improvement** — agents learn reusable skills from their own work: after a substantive turn a background reviewer creates or updates a skill, hot-reloaded for the next turn. Provenance-guarded (never overwrites human-written skills), capped per day, and audited to `SKILLS_LEARNED.md`. See [`gateway.skillLearning`](https://0xmaxma.github.io/claude-gateway/reference/memory-settings.html#gateway-skilllearning)
20
+ - 📚 **Knowledge base (two-lane memory)** — per-agent SQLite/FTS5 searchable archive exposed through `memory_search` / `memory_get` MCP tools, so agents recall notes that don't fit the always-injected core; chunks carry fail-closed provenance and the index is refreshed off the gateway event loop. See [`gateway.knowledge`](https://0xmaxma.github.io/claude-gateway/reference/memory-settings.html#gateway-knowledge)
21
+ - 🌙 **Nightly dreaming** — background consolidation of long-term memory: a print-only reviewer proposes ops that a safe applier writes to `MEMORY.md` / `USER.md` (backup, bounded-loss, net-negative when over budget). Deterministic compaction, budget-scaled pruning, and staleness GC keep memory near budget without forgetting — archived entries stay searchable. See [`gateway.dreaming`](https://0xmaxma.github.io/claude-gateway/reference/memory-settings.html#gateway-dreaming)
17
22
  - 🤖 **Multi-agent** — run multiple bots from a single gateway, each with isolated sessions
18
23
  - 🔌 **Multi-channel MCP** — modular tool system per channel (Telegram, Discord, LINE, Slack, WhatsApp, Cron, Skills, extensible to more)
19
24
  - 🧩 **Agent skills** — extensible skill system via SKILL.md files; agents can create, delete, and install skills from URLs at runtime with hot-reload
@@ -32,1625 +37,35 @@ A self-hosted multi-agent gateway for Claude Code — with agents that improve t
32
37
  - 🛍️ **App Store** — install, update, and host Docker-compose apps on the gateway; apps get a reverse proxy at `/app/:name/:portName/*`, optional Unix socket bridge for host scripts, and optional AI agent injection
33
38
  - ⬆️ **Self-update** — check for newer versions of `claude-gateway` and `claude-code` and trigger an update via a single API call (no SSH or shell access needed), or from the terminal with `claude-gateway update` / `claude-gateway claude update`
34
39
  - 💾 **Session persistence** — conversation history saved and restored across restarts
35
- - 🖥️ **PTY shell (wrap-shell mode)** — optional interactive pseudo-terminal backend (`gateway.headless: false`) for tools that require a real TTY; includes a live browser viewer (xterm.js) and a `/api/v1/sessions/:sessionId/screen` endpoint that returns the visible screen as plain text — agents can poll it to detect hang states, menus, or unexpected output without parsing ANSI escape codes; a `/cli` chat command (Telegram/Discord/LINE) opens the same viewer for a single agent, agent-scoped and without an admin key; app-agents always stay headless
36
-
37
- ---
38
-
39
- ## Requirements
40
-
41
- - Node.js 22+
42
- - [Claude Code CLI](https://claude.ai/code) v2.1.0+ installed and authenticated — `channels mode` is required (`claude --version`)
43
- - The gateway must be able to find the `claude` executable: either have `claude` on the `PATH` of the process that launches the gateway, or set `CLAUDE_BIN` to its full path. When `CLAUDE_BIN` is unset, the gateway also probes the native-installer locations (`~/.local/bin/claude`, then `~/.local/share/claude/versions/`) and the legacy npm/nvm layout, so a Claude Code installer migration does not break new sessions. If none resolve, set `CLAUDE_BIN` explicitly (e.g. `CLAUDE_BIN=~/.local/bin/claude`).
44
- - [Bun](https://bun.sh) — runs the MCP server subprocess (`mcp/server.ts`)
45
- - A bot token per agent — Telegram (from [@BotFather](https://t.me/BotFather)) or Discord (from [Discord Developer Portal](https://discord.com/developers/applications))
46
- - **PTY backend only** (`claude.headless: false`): native build tools required for `node-pty` — `gcc`, `python3`, and `node-gyp` must be available at `npm install` time (pre-built binaries are included for common platforms; build tools are only needed if a pre-built binary is unavailable for your platform)
47
-
48
- ---
40
+ - 🖥️ **PTY shell (wrap-shell mode)** — optional interactive pseudo-terminal backend (`gateway.headless: false`) for tools that require a real TTY; includes a live browser viewer (xterm.js) and a `/api/v1/sessions/:sessionId/screen` endpoint that returns the visible screen as plain text — agents can poll it to detect hang states, menus, or unexpected output without parsing ANSI escape codes; a `/cli` chat command (Telegram/Discord/LINE) opens the same viewer for a single agent, agent-scoped and without an admin key; app-agents and orchestration always stay headless
49
41
 
50
- ## Quick Start
42
+ ## Get started
51
43
 
52
- ### Install via npm (for users)
53
-
54
- **1. Install**
44
+ Install Node.js 22+, Bun, and an authenticated Claude Code CLI with channels support. Orchestration requires Linux. Docker/Compose is needed for apps; some voice formats require `ffmpeg`.
55
45
 
56
46
  ```bash
57
47
  npm install -g @0xmaxma/claude-gateway
58
- ```
59
-
60
- Requires [Bun](https://bun.sh) — MCP server dependencies are installed automatically via `postinstall`.
61
-
62
- **2. Configure environment (optional)**
63
-
64
- The gateway auto-loads `~/.claude-gateway/.env` on startup:
65
-
66
- ```bash
67
- mkdir -p ~/.claude-gateway
68
- cat > ~/.claude-gateway/.env << 'EOF'
69
- # HTTP port (default: 10850)
70
- # PORT=10850
71
-
72
- # Bind address (default: 0.0.0.0 — all interfaces)
73
- # Set to 127.0.0.1 if a host-network reverse proxy (e.g. Traefik) is used
74
- # GATEWAY_BIND=127.0.0.1
75
-
76
- # Path to gateway config (default: ~/.claude-gateway/config.json)
77
- # GATEWAY_CONFIG=~/.claude-gateway/config.json
78
- EOF
79
- ```
80
-
81
- All variables are optional. Full list: [`.env.example`](.env.example)
82
-
83
- **3. Start**
84
-
85
- ```bash
86
48
  claude-gateway gateway start
87
49
  ```
88
50
 
89
- `claude-gateway` on its own prints help — starting the server is always the explicit
90
- `gateway start`, so a stray or mistyped command can never leave a gateway listening.
91
-
92
- No config file needed — on first run, if `~/.claude-gateway/config.json` doesn't exist yet, the gateway creates it automatically with `"agents": []` and a fresh random admin API key, and prints that key once:
93
-
94
- ```
95
- [gateway] No config found — created one at ~/.claude-gateway/config.json
96
- [gateway] Admin API key (save this now — it will not be shown again):
97
- [gateway] <random-hex-key>
98
- [gateway] The CLI (claude-gateway agents create, etc.) picks this up automatically from ~/.claude-gateway/config.json.
99
- ```
100
-
101
- Save that key somewhere safe — it isn't shown again (though you can always read it back from `config.json` on disk). See [`config.template.json`](config.template.json) for the full config format (models list, more options) if you want to customize it by hand later.
102
-
103
- **4. Create an agent**
104
-
105
- ```bash
106
- claude-gateway agents create
107
- ```
108
-
109
- Interactive wizard — describe the agent, Claude generates the workspace files, review and accept them, then optionally connect a Telegram or Discord bot. Hot-reloads immediately, no restart needed. The CLI picks up the admin key from `config.json` automatically — no need to pass `--key`. (You can also add an agent entry to `config.json` by hand instead — same template link as above.)
110
-
111
- **Run as a service (optional)**
112
-
113
- To keep the gateway running after you log out or the machine reboots, let the CLI install the
114
- service for you. It shows the exact unit it will write, asks before installing, and verifies
115
- `/health` afterwards:
116
-
117
- ```bash
118
- claude-gateway service install # systemd *user* unit — no sudo
119
- claude-gateway service install --print # just show what it would install
120
- claude-gateway service status
121
- claude-gateway service uninstall # asks first — this stops a running gateway
122
- claude-gateway service start # start the installed service (found even if inactive)
123
- claude-gateway service restart
124
- claude-gateway service stop
125
- ```
126
-
127
- Install and uninstall both prompt before acting; pass `--yes` in scripts (without it, a
128
- non-interactive run is refused rather than left hanging). Always stop the gateway through
129
- `service uninstall` or `systemctl --user stop claude-gateway.service` — a bare `kill <pid>` bypasses
130
- systemd's own stop tracking, so `Restart=always` brings it right back regardless of exit code.
131
-
132
- `install`, `start` and `restart` — the commands meant to leave a running gateway behind — share one
133
- exit-code contract with three outcomes: `0` fully healthy, `1` the action itself failed (or a
134
- validation/confirmation gate refused, so nothing was written), `2` the action succeeded but
135
- `/health` never answered within the poll window. A script that only checks the exit code — not the
136
- JSON result on stdout — can still tell "didn't happen" apart from "happened, health unconfirmed"
137
- this way. `start` on an already-running service is no exception: it still probes `/health` and can
138
- still exit `2`, because "the process manager calls it active" is not "the gateway answers".
139
-
140
- `install` also refuses (rather than just warning) if a `claude-gateway.service` unit already
141
- exists and is enabled or active at *system* scope (e.g. one written by provisioning outside this
142
- CLI) — installing a second, independent unit alongside it would race for the port on the next
143
- reboot. It prints the exact `sudo systemctl disable --now claude-gateway.service` to resolve it;
144
- pass `--force` to install anyway.
145
-
146
- The systemd path writes `~/.config/systemd/user/claude-gateway.service`. Run
147
- `loginctl enable-linger $USER` once if it must keep running while you're logged out.
148
- The unit sets `OOMPolicy=continue` so that an OOM-killed child process (e.g. a dev server an
149
- agent spawned on its own) doesn't take the whole gateway down with it — only `Restart=always`
150
- restarting the *gateway's own* process is intended. Re-running `claude-gateway service install`
151
- against an already-active unit whose rendered content changed (a newer CLI version, or different
152
- flags) automatically restarts it via `systemctl ... restart`, so the update takes effect
153
- immediately; re-running with unchanged content leaves the running unit alone.
154
- Prefer [PM2](https://pm2.keymetrics.io)? `claude-gateway service install --manager pm2` registers
155
- and saves the process instead (run `pm2 startup` separately for boot-time start).
156
-
157
- **System-scope installs (for automated/infra provisioning)**
158
-
159
- Pass `--scope system` to install a root-owned unit at `/etc/systemd/system/claude-gateway.service`
160
- instead — for provisioning that needs the gateway to run under a fixed system account rather than
161
- whoever happens to run the install interactively. It requires:
162
-
163
- ```bash
164
- sudo claude-gateway service install --scope system --run-as gwuser --yes
165
- ```
166
-
167
- - The caller must already be root — `--scope system` never escalates via `sudo` on its own, and
168
- refuses immediately if it isn't.
169
- - `--run-as <user>` is required and becomes the unit's `User=`; `WantedBy=` is
170
- `multi-user.target` instead of `default.target`, so it starts at boot regardless of any login
171
- session (the `loginctl enable-linger` hint is skipped — it's meaningless here).
172
- - `WorkingDirectory=`/`HOME=`/the config path all resolve to `--run-as`'s own home directory
173
- (looked up via `getent passwd`, not the installing root process's home) — the unit runs as that
174
- user, so its paths must be theirs. A `~/...` in `--config`/`--env-file` expands against that same
175
- home. `$GATEWAY_CONFIG` from the installing (root) process's own environment is **not** consulted
176
- for a system-scope install — only an explicit `--config` is — since it belongs to root's
177
- environment, not `--run-as`'s. If the user's `~/.claude-gateway` doesn't exist yet, the install
178
- creates it and `chown`s it to them; if it already exists but is owned by someone else (e.g. a
179
- prior install used a different `--run-as`), ownership is reassigned to match. An
180
- already-correctly-owned directory is left untouched. Refuses if `--run-as` doesn't resolve to a
181
- real user on this host.
182
- - A system-scope install never refuses itself over the system-scope conflict check described
183
- above — that check exists to protect a *user*-scope install from colliding with an externally
184
- provisioned system-scope unit, and a system-scope install *is* that unit.
185
- - `--after <target1,target2>`, `--env-file <path>`, and `--env KEY=VALUE[,KEY=VALUE...]` further
186
- customize the generated unit (both scopes): extra `After=` ordering targets, an
187
- `EnvironmentFile=-<path>` for feeding secrets in without ever writing them into the unit text,
188
- and additional non-secret `Environment=` lines. `--env` refuses to override `HOME`, `PATH`, or
189
- `GATEWAY_CONFIG` (the installer's own reserved names) — use `--env-file` for anything sensitive.
190
- - `service status --scope system` and `service uninstall --scope system` work the same way against
191
- the system-scope unit (uninstall also requires root).
192
-
193
- Once installed, drive it through the CLI — it detects whichever manager owns the process:
194
-
195
- ```bash
196
- claude-gateway gateway status # manager, URL, health
197
- claude-gateway gateway restart
198
- claude-gateway gateway stop
199
- claude-gateway gateway logs # tail the gateway's own log (works even when it is dead)
200
- ```
201
-
202
- `gateway restart`/`stop` only drive whatever manager is currently reported *active*. To start an
203
- installed service that is currently stopped — or to act on a specific `--manager`/`--scope`
204
- regardless of what else might be running — use `service start`/`stop`/`restart` instead; they
205
- discover the installed unit from disk the same way `service status`/`uninstall` do.
206
-
207
- Managing PM2 directly still works too:
208
-
209
- ```bash
210
- pm2 status # check gateway status
211
- pm2 logs gateway # tail logs
212
- pm2 restart gateway # restart
213
- pm2 stop gateway # stop
214
- pm2 delete gateway # remove from PM2
215
- ```
216
-
217
- ---
218
-
219
- ### For development
220
-
221
- ```bash
222
- git clone https://github.com/0xMaxMa/claude-gateway
223
- cd claude-gateway
224
- npm install # also runs bun install in mcp/
225
- npm run build
226
- ```
227
-
228
- ### Start the gateway
229
-
230
- ```bash
231
- npm start
232
- ```
233
-
234
- Config is auto-loaded from `~/.claude-gateway/config.json` — if it doesn't exist yet, `npm start` creates it automatically with `"agents": []` and a fresh admin key (see the Start step in the npm-install path above). Bot tokens are auto-loaded from `~/.claude-gateway/agents/<id>/.env`.
235
-
236
- ### Create an agent
237
-
238
- The interactive wizard handles everything — workspace files, bot token, and pairing:
51
+ In another terminal:
239
52
 
240
53
  ```bash
241
54
  claude-gateway agents create
242
55
  ```
243
56
 
244
- Steps:
245
- 1. Choose an agent id and describe its role — Claude generates workspace files
246
- 2. Review and accept the generated files
247
- 3. Optionally connect a channel: **Telegram** or **Discord** — paste the bot token, wizard verifies it automatically
248
- 4. Agent hot-reloads immediately — send any message to the bot, then approve pairing:
249
- ```bash
250
- claude-gateway channels approve --agent <id> --channel telegram --code <code>
251
- ```
252
-
253
- To manage an existing agent — regenerate `AGENTS.md`, or connect/update/disconnect Telegram, Discord, LINE, or Slack — run `claude-gateway agents update`.
254
-
255
- ---
256
-
257
- ## Workspace Files
258
-
259
- Each agent has a workspace directory with markdown files that define its behaviour:
260
-
261
- | File | Required | Purpose |
262
- |------|----------|---------|
263
- | `AGENTS.md` | **Yes** | Core identity, rules, capabilities |
264
- | `IDENTITY.md` | No | Agent name, emoji, avatar, personality identity |
265
- | `SOUL.md` | No | Tone, personality, speaking style |
266
- | `USER.md` | No | User profile and preferences |
267
- | `MEMORY.md` | No | Long-term memory (auto-appended by the agent) |
268
- | `HEARTBEAT.md` | No | Scheduled/proactive tasks |
269
- | `skills/` | No | Directory of SKILL.md files — agent-specific skills |
270
-
271
- On startup (and on any file change), all files are assembled into `CLAUDE.md` which the Claude subprocess reads as its system prompt. Do not edit `CLAUDE.md` directly.
272
-
273
- ---
274
-
275
- ## Configuration Reference
276
-
277
- Config lives at `~/.claude-gateway/config.json` (or set `GATEWAY_CONFIG` env var / `--config` flag).
278
-
279
- ```json
280
- {
281
- "configVersion": "1.0.0",
282
- "gateway": {
283
- "logDir": "~/.claude-gateway/logs",
284
- "logs": {
285
- "level": "info",
286
- "maxFileBytes": 16777216,
287
- "maxFiles": 3,
288
- "retentionDays": 14
289
- },
290
- "timezone": "Asia/Bangkok",
291
- "api": {
292
- "keys": [
293
- {
294
- "key": "${MY_API_KEY}",
295
- "description": "Internal app",
296
- "agents": ["alfred"]
297
- },
298
- {
299
- "key": "${ADMIN_API_KEY}",
300
- "description": "Admin",
301
- "agents": "*"
302
- }
303
- ]
304
- }
305
- },
306
- "agents": [
307
- {
308
- "id": "alfred",
309
- "description": "Personal assistant",
310
- "workspace": "~/.claude-gateway/agents/alfred/workspace",
311
- "env": "",
312
- "session": {
313
- "idleTimeoutMinutes": 30,
314
- "maxConcurrent": 20
315
- },
316
- "telegram": {
317
- "botToken": "${ALFRED_BOT_TOKEN}"
318
- },
319
- "claude": {
320
- "model": "claude-sonnet-4-6",
321
- "extraFlags": []
322
- },
323
- "heartbeat": {
324
- "rateLimitMinutes": 30
325
- }
326
- }
327
- ]
328
- }
329
- ```
330
-
331
- ### `gateway.timezone` (optional)
332
-
333
- IANA timezone, default `"UTC"`. Shared default for the per-feature scheduling
334
- timezones below when they are unset or invalid: `gateway.history.cleanupTimezone`,
335
- `gateway.appBackup.cleanupTimezone`, `gateway.skillLearning.pruneTimezone`,
336
- `gateway.dreaming.dreamTimezone`, and `gateway.knowledge.reflection.timezone`. A
337
- valid per-feature field still overrides this shared default for that one feature;
338
- an invalid per-feature value falls through to `gateway.timezone` rather than being
339
- treated as set. An invalid `gateway.timezone` itself falls back to `"UTC"` rather
340
- than crashing that scheduler.
341
-
342
- ### `gateway.publicUrl` (optional)
343
-
344
- The externally reachable gateway base URL. Set it manually to enable short-lived
345
- public file shares used by `generate_image` reference edits and `share_file`
346
- (formerly `share_image`, which still works as a deprecated image-only alias).
347
- The URL must end in `/gateway`; changing it requires a gateway restart.
348
-
349
- ```json
350
- {
351
- "gateway": {
352
- "publicUrl": "https://vm.example.com/gateway"
353
- }
354
- }
355
- ```
356
-
357
- Minted share URLs have the stable form
358
- `https://vm.example.com/gateway/shared/TOKEN`. When `publicUrl` is set the mint
359
- response includes this ready-built `url`; when it is unset the response still
360
- returns the `token` (the share endpoint stays enabled) and callers with their own
361
- public base — e.g. LINE, which derives its host from the inbound webhook — build
362
- `<base>/shared/<token>` themselves. HTTP is accepted only for local development
363
- hosts such as `http://host.docker.internal:10850/gateway`.
364
-
365
- ### `gateway.oauthReturnUrl` (optional)
366
-
367
- Where to send the browser after a connector OAuth sign-in finishes. The gateway is
368
- product-agnostic and never hardcodes a downstream app's domain, so this is opt-in.
369
-
370
- ```json
371
- {
372
- "gateway": {
373
- "oauthReturnUrl": "https://app.example.com/settings/connectors"
374
- }
375
- }
376
- ```
377
-
378
- Set, the callback issues a real `302` to it on **every** terminal outcome — success, and
379
- also a denied, expired or failed sign-in, which carries `?connector_oauth_error=<code>`.
380
- Unset, the callback renders a plain "Connected — you can close this tab" page instead.
381
- The value is validated once at startup: anything that isn't a well-formed `http(s)` URL
382
- is logged and ignored rather than injecting a broken redirect into every future callback.
383
- The scheme is part of that check — this value becomes the `Location` of a redirect sent
384
- to the end user's own browser from a public route, so a `javascript:` or `data:` URL
385
- here would be script running on every sign-in, and is refused like any other malformed
386
- value.
387
-
388
- ### `gateway.customConnectors` (optional)
389
-
390
- User-pasted MCP connectors, keyed by a slugified id. Normally written through the API
391
- (`POST /api/v1/connectors/custom`) rather than by hand.
392
-
393
- ```json
394
- {
395
- "gateway": {
396
- "customConnectors": {
397
- "firecrawl": {
398
- "label": "Firecrawl",
399
- "config": {
400
- "type": "streamable-http",
401
- "url": "https://mcp.firecrawl.dev/v2/mcp-oauth",
402
- "headers": { "Authorization": "Bearer {access_token}" }
403
- },
404
- "secretNames": ["access_token"],
405
- "credentialOwner": "gateway"
406
- }
407
- }
408
- }
409
- }
410
- ```
411
-
412
- Each entry is raw `mcpServers`-entry JSON with `{placeholder}` tokens standing in for
413
- secrets. `credentialOwner` records who holds the credential and keeps it valid — `none`,
414
- `static` (a pasted value), `gateway` (this gateway ran the OAuth flow and refreshes the
415
- token itself) or `external` (a control plane pushes tokens in). It is written by the
416
- route that creates the entry; see [API.md](./API.md#connectors-api). **Only the placeholder names are stored here** — the values live in
417
- `~/.claude-gateway/mcp-token.env` (mode `0600`), namespaced
418
- `CUSTOM__<connectorId>__<placeholderName>`, and are substituted in when a session spawns.
419
- Override that file's path with `GATEWAY_MCP_TOKEN_ENV_PATH`.
420
-
421
- Custom connectors are **admin-trusted but not code-reviewed** — the config is whatever
422
- the admin pasted, and it becomes an MCP server in every agent's session. Per-agent
423
- enablement is opt-out and lives on the agent instead (`PATCH /api/v1/agents/:id` with
424
- `connectors`); connecting a connector at all is the security gate. See
425
- [API.md](./API.md#connectors-api) for the full model, the OAuth flow, and the refresh
426
- behaviour.
427
-
428
- ### `gateway.connectorsDefaultEnabled` (optional)
429
-
430
- Whether a connected connector is available to an agent that has no explicit entry in its
431
- own `connectors` map. Defaults to `true` — opt-out: connecting a connector makes it
432
- available everywhere, and an agent only misses it if explicitly disabled.
433
-
434
- ```json
435
- {
436
- "gateway": {
437
- "connectorsDefaultEnabled": false
438
- }
439
- }
440
- ```
441
-
442
- Set it to `false` on a gateway that hosts agents for **more than one person**. The default
443
- suits the common single-operator install, but with several owners it hands a credential
444
- connected by one of them to every agent on the box — including agents whose chat users are
445
- not that person. With `false`, each agent has to be opted in explicitly (`PATCH
446
- /api/v1/agents/:id` with `{"connectors": {"<id>": {"enabled": true}}}`).
447
-
448
- Changing this affects the next session spawn, like any other connector change.
449
-
450
- ### `gateway.logs` (optional)
451
-
452
- Verbosity, rotation and retention for the files in `logDir`. The whole block is optional —
453
- omit it and the defaults below apply.
454
-
455
- | Field | Default | Description |
456
- |-------|---------|-------------|
457
- | `level` | `"info"` | Minimum level written, to both the file and stdout. One of `debug`, `info`, `warn`, `error` |
458
- | `maxFileBytes` | `16777216` (16 MiB) | Rotate `<name>.log` to `<name>.log.1` once an append would carry it past this size |
459
- | `maxFiles` | `3` | Rotated generations kept per stream; the oldest is deleted. Lowering it collects the generations it orphans at the next rotation. `0` = keep none |
460
- | `retentionDays` | `14` | Delete logs (live and rotated) older than this, at boot and once a day. `0` = keep forever |
461
-
462
- `level` is the one that governs disk usage. Session processes log every stream event at `debug`,
463
- which on a live host measured 19,995 `debug` lines to 5 `info` lines inside a single 217 MB file —
464
- so `debug` is off by default. Set `"level": "debug"` when you are actually chasing something, and
465
- expect the directory to grow quickly while it is on. Rotation and retention bound what is *kept*;
466
- only the level bounds what is *written*.
467
-
468
- Retention is age-based because each session writes its own `<agent>:session:<uuid>.log` and never
469
- returns to it — `maxFiles` prunes generations of one stream, so it can never reach them.
470
-
471
- This block is **hot-reloaded**: edit it in `config.json` and it applies on the next config reload,
472
- no restart. That matters because turning the level up is something you do while chasing a live
473
- problem, and a restart would kill the sessions you are trying to observe.
474
-
475
- ### `session`
476
-
477
- | Field | Default | Description |
478
- |-------|---------|-------------|
479
- | `idleTimeoutMinutes` | `30` | Kill idle session subprocess after N minutes of inactivity. Inactivity means no incoming message **and** no subprocess output — a session actively producing output (e.g. a self-paced `/loop`) is not treated as idle |
480
- | `maxConcurrent` | `20` | Max simultaneous active sessions per agent; oldest idle is evicted when exceeded |
481
-
482
- ### `gateway.history` (optional)
483
-
484
- Global default retention policy. Can be overridden per-agent with an `history` key inside the agent config.
485
-
486
- ```json
487
- {
488
- "gateway": {
489
- "history": {
490
- "retentionDays": 90,
491
- "maxHistoryMessages": 30,
492
- "cleanupHour": 3,
493
- "cleanupTimezone": "Asia/Bangkok"
494
- }
495
- }
496
- }
497
- ```
498
-
499
- | Field | Default | Description |
500
- |-------|---------|-------------|
501
- | `retentionDays` | `null` (keep forever) | Delete messages older than N days on each cleanup cycle |
502
- | `maxHistoryMessages` | `50` | Max history messages re-injected into a session at spawn. Lower it to shrink the context loaded at session start. `0` = inject no history |
503
- | `cleanupHour` | `3` | Hour of day to run cleanup (24h, in `cleanupTimezone`) |
504
- | `cleanupTimezone` | `"UTC"` | IANA timezone for the cleanup schedule; falls back to `gateway.timezone` when unset or invalid |
505
-
506
- Per-agent override example:
507
- ```json
508
- {
509
- "agents": [
510
- {
511
- "id": "alfred",
512
- "history": { "retentionDays": 30, "maxHistoryMessages": 30 }
513
- }
514
- ]
515
- }
516
- ```
517
-
518
- ### `dmPolicy`
519
-
520
- Access policy is configured per-channel in the agent's workspace state file, not in `config.json`:
521
-
522
- | File | Path |
523
- |------|------|
524
- | Telegram | `~/.claude-gateway/agents/<id>/workspace/.telegram-state/access.json` |
525
- | Discord | `~/.claude-gateway/agents/<id>/workspace/.discord-state/access.json` |
526
-
527
- | Value | Behaviour |
528
- |-------|-----------|
529
- | `allowlist` | Only user IDs in `allowFrom` can DM the agent (**default**) |
530
- | `open` | Anyone can DM the agent |
531
- | `pairing` | New users DM the bot to receive a pairing code; approve with `claude-gateway channels approve` |
532
-
533
- ### `gateway.headless`
534
-
535
- Controls the Claude subprocess backend for all non-app agents.
536
-
537
- | Value | Backend | Description |
538
- |-------|---------|-------------|
539
- | `true` *(default)* | Headless (`--print`) | Stateless invocation, lowest overhead |
540
- | `false` | PTY shell wrapper | Interactive pseudo-terminal — full TUI support |
541
-
542
- **App-agents always run headless** regardless of this setting.
543
-
544
- `--dangerously-skip-permissions` is always injected by the gateway automatically — there is no per-agent config field for it.
545
-
546
- In PTY mode that flag makes Claude Code open a "Bypass Permissions mode" confirmation dialog at startup, which the wrapper accepts on your behalf. How it is accepted depends on the Claude Code build: releases up to **2.1.247** render numbered options (`1. No, exit` / `2. Yes, I accept`) and are accepted with the digit, while **2.1.248 and newer** drop the numbers, so the wrapper walks the caret onto the accept row and only then presses Enter. If a future release changes the dialog beyond what the wrapper recognises, it deliberately sends **no** keystroke and leaves the dialog on screen rather than risk selecting "No, exit" (which would exit Claude Code) — set `PTY_SHELL_SKIP_DIALOG_DISMISS=1` to turn the auto-accept off entirely.
547
-
548
- ```json
549
- {
550
- "gateway": {
551
- "headless": false
552
- }
553
- }
554
- ```
555
-
556
- This setting is hot-reloadable — new sessions pick it up without a restart.
557
-
558
- ### `gateway.selfHealing.autoRecover`
559
-
560
- Opt-in self-healing for the turn-trace watchdog (Epic #195). When a turn stalls, the gateway always detects it, logs a scrubbed incident, and notifies the affected chat. This flag additionally controls whether the gateway may *act* on a stall.
561
-
562
- | Value | Behaviour |
563
- |-------|-----------|
564
- | `false` *(default)* | Detection + incident logging + notification only — no automatic action |
565
- | `true` | The watchdog may run a whitelisted recovery for a stalled turn: a keystroke into the TUI (esc / enter / arrow / menu selection), a session restart, a reversible safe-mode fallback to the headless backend, and — after a successful unblock — a guarded resend of the last message (only if the turn produced no output, so it is never double-submitted) |
566
-
567
- Recovery actions are clamped to a per-stage whitelist and a per-turn budget, and any local triage treats the on-screen text as untrusted data validated against a closed schema. Safe-mode auto-fallback on a hard PTY failure is independent of this flag (it is always reversible and never presses keys). In-memory only — a gateway restart re-reads your real config.
568
-
569
- ```json
570
- {
571
- "gateway": {
572
- "selfHealing": {
573
- "autoRecover": true
574
- }
575
- }
576
- }
577
- ```
578
-
579
- ### `gateway.skillLearning`
580
-
581
- Controls [skill self-improvement](#skill-self-improvement) — agents learning reusable skills from their own work. Telemetry capture is always on; the reviewer/writer/curator honor `enabled`.
582
-
583
- | Field | Default | Description |
584
- |-------|---------|-------------|
585
- | `enabled` | `true` | Master switch for the reviewer/writer/curator (telemetry is captured regardless) |
586
- | `mode` | `"auto"` | `auto` writes skills directly; `propose` queues them for approval instead |
587
- | `minToolCalls` | `5` | Minimum tool calls in a turn before it's eligible for review |
588
- | `reviewModel` | `claude-haiku-4-5-…` | Model used for the background review pass |
589
- | `maxAutoSkills` | `50` | Cap on the number of non-pinned `origin: auto` skills kept per agent (pinned skills are never evicted and don't count toward the cap) |
590
- | `maxAgeDays` | `30` | Curator prunes auto-skills older than this (with too few uses) |
591
- | `minUsesToKeep` | `2` | Auto-skills used fewer times than this are prune candidates |
592
- | `maxReviewsPerDay` | `20` | Per-day cap on background review runs |
593
- | `pruneHour` / `pruneTimezone` | `3` / `UTC` | When the daily curator runs; `pruneTimezone` falls back to `gateway.timezone` when unset or invalid |
594
- | `notify` | `true` | Push a per-write ping to every configured channel (see [notifications](#skill-self-improvement)); the `SKILLS_LEARNED.md` diary is written regardless |
595
-
596
- ```json
597
- {
598
- "gateway": {
599
- "skillLearning": {
600
- "enabled": true,
601
- "mode": "auto",
602
- "notify": true
603
- }
604
- }
605
- }
606
- ```
607
-
608
- Per-agent overrides are supported under the agent's own `skillLearning` block; unset fields fall back to the gateway default.
609
-
610
- ### `gateway.memory`
611
-
612
- Memory budget discipline. Self-authored memory files (`MEMORY.md`, `USER.md`) that exceed a **soft** char budget get a loud over-budget banner prepended to their `CLAUDE.md` section at compose time — instead of a silent `[TRUNCATED]` — nudging the agent to consolidate. The banner reaches the agent on its next spawn (frozen-at-spawn, no restart) and self-heals once the file is back under budget. The banner lives only in the composed `CLAUDE.md`; the source file on disk is never rewritten with it.
613
-
614
- | Field | Default | Description |
615
- |-------|---------|-------------|
616
- | `memoryBudgetChars` | `8000` | Soft budget for `MEMORY.md` (`0` = disabled) |
617
- | `userBudgetChars` | `3000` | Soft budget for `USER.md` (`0` = disabled) |
618
- | `overBudget` | `"warn"` | Banner severity: `warn` (⚠️) or `error` (🛑, stronger wording); an unknown value falls back to `warn` |
619
- | `writeRouting` | `true` | Inject the **two-tier write contract** into the Memory Rule (`MEMORY.md` = durable facts; task-log → `memory/<topic>.md`) and let nightly dreaming route episodic ops out. `false` = kill-switch (exact pre-routing behavior) |
620
- | `episodicArchiveDir` | `"memory"` | Workspace-relative dir episodic notes are written under (validated, path-traversal-guarded) |
621
-
622
- ```json
623
- {
624
- "gateway": {
625
- "memory": {
626
- "memoryBudgetChars": 8000,
627
- "userBudgetChars": 3000,
628
- "overBudget": "warn",
629
- "writeRouting": true,
630
- "episodicArchiveDir": "memory"
631
- }
632
- }
633
- }
634
- ```
635
-
636
- The soft budget sits well under the hard per-file limit (still applied as a context safety net); the banner is the primary over-budget signal for memory files.
637
-
638
- **Write routing (planning-65).** `MEMORY.md` is injected into every prompt, so it should hold only **durable semantic facts** (preferences, standing rules, identity, lessons). **Episodic task-log** (completed work, PR/issue status, dated events) belongs in `memory/<topic>.md` — indexed and retrieved on demand via `memory_search`, never carried in-prompt. When `writeRouting` is on, the Memory Rule states this tier contract to the agent, and the nightly dreaming reviewer may emit `tier:"episodic"` ops that the applier appends to `memory/<topic>.md` (slug-validated + realpath-confined; a memory-only change ⇒ no session restart). To drain an existing over-budget `MEMORY.md`, run the one-shot migration `node dist/agent/dreaming/migrate-cli.js <workspaceDir> [--apply]` — a deterministic terminal sweep (compactor) plus a gated episodic route-out (`propose` writes `.dreaming/migration-plan.md`; `--apply` performs the moves). Pinned sections (`## User`, `## Feedback`, `## Preferences`) are never moved, and every relocated entry stays searchable via `memory_search` (recall preserved). **planning-67:** with `gateway.dreaming.autoRouteOut` on (the default), the nightly dream performs this same route-out **automatically** whenever `MEMORY.md` is over budget — no manual per-agent run — and every over-budget net-shrink `remove` now **relocates** its block to `memory/archive/pruned.md` (searchable) before cutting it, so no dream op ever silently forgets.
639
-
640
- ### `gateway.dreaming`
641
-
642
- Nightly memory **dreaming** — background consolidation of an agent's long-term memory. A print-only `claude -p` reviewer (no tools, no `--dangerously-skip-permissions`) reads a lookback window of the agent's own session transcripts and proposes memory-consolidation ops. In **`auto`** mode (the default) a safe applier writes the ops to `MEMORY.md`/`USER.md` (rollback pre-image first; ordered apply with anchor re-resolution; bounded-loss + append-only fallback; net-negative when over budget) — a memory-only change, so no session is restarted. In **`propose`** mode the proposals are written **only** to a `DREAMS.md` diary + JSONL audit under `<workspace>/.dreaming/` — no memory file is modified (set `mode: "propose"` to keep this dry-run behavior).
643
-
644
- | Field | Default | Description |
645
- |-------|---------|-------------|
646
- | `enabled` | `true` | Master switch (`false` ⇒ no scheduler, no run) |
647
- | `mode` | `"auto"` | `auto` = apply ops via the safe applier (backup, bounded-loss, net-negative); `propose` = diary-only dry-run |
648
- | `dreamHour` / `dreamTimezone` | `3` / `UTC` | When the nightly dream runs (invalid tz → `gateway.timezone`, then UTC); `dreamTimezone` falls back to `gateway.timezone` when unset or invalid |
649
- | `dreamMinute` | `0` | Minute-of-hour the dream fires at, paired with `dreamHour` (0–59). Set with `staggerWindowMinutes: 0` to fire at an exact `HH:MM` (e.g. for a controlled re-test) |
650
- | `quietMinutes` | `30` | Skip a run if a session was active within this window |
651
- | `lookbackDays` | `3` | How far back to scan sessions |
652
- | `maxChangesPerRun` | `3` | Cap on proposed ops per run (`0` ⇒ no-op) |
653
- | `reviewModel` | `claude-haiku-4-5-…` | Cheap model for the reviewer |
654
- | `promotionThreshold` / `minRecallCount` | `0.6` / `2` | Scoring thresholds for promoting a fact |
655
- | `autoRouteOut` | `true` | planning-67: in `auto` mode, drain an **over-budget** `MEMORY.md` by routing its episodic task-log to `memory/<topic>.md` automatically each night (archive-safe, pinned excluded, idempotent) instead of a manual per-agent `migrate-cli`. `false` = kill-switch |
656
- | `staggerWindowMinutes` | `30` | planning-68: spread agents' nightly runs across a window (a deterministic per-agent jitter is added to the delay) so they don't all fire at `dreamHour:00` together. Clamped `[0,55]`; `0` = disabled (all fire at `dreamHour:00`) |
657
- | `staleness` | *(object)* | Archive staleness GC sub-config (planning-66) — see below |
658
-
659
- Per-agent overrides are supported under the agent's own `dreaming` block; unset fields fall back to the gateway default. `enabled:false` or `maxChangesPerRun:0` makes a run a no-op.
660
-
661
- > **⚠️ Upgrade note:** the default `mode` for both `gateway.dreaming` and `gateway.knowledge.shared` changed from `propose` (dry-run) to `auto` (configVersion 1.0.24). Once the K4 applier landed (backup + net-negative + bounded-loss + CAS + never-empty; memory-only write ⇒ no session restart), `auto` became the intended default: nightly dreaming now applies consolidation to `MEMORY.md`/`USER.md` and promotes durable memories to the shared vault. Like the `gateway.bind` migration, the migrator upgrades the *retired* `propose` default to `auto` once and logs a one-time warning; an explicit `mode` you set at 1.0.24+ is never touched. To keep dry-run, set `mode: "propose"` explicitly.
662
-
663
- **Keeping `MEMORY.md` near budget (`auto` mode).** Two mechanisms stop the on-disk `MEMORY.md` from growing unbounded while preserving recall:
664
-
665
- - **Deterministic compaction** — before the LLM reviewer, every `auto` run moves completed/terminal log entries out of `MEMORY.md` into `memory/archive/completed.md`, leaving a one-line pointer. It is **domain-agnostic** (not just dev): an entry is archived when its lead line carries an explicit done marker — an UPPERCASE status word (`DONE`, `COMPLETED`, `RESOLVED`, `CLOSED`, `CANCELLED`, `ARCHIVED`, `MERGED`, `SUPERSEDED`, `OBSOLETE`, `DEPRECATED`, `EXPIRED`, `SHIPPED`, `FINISHED`), a checked task box `[x]`, a ✅, or a ~~strikethrough~~ — and it works on both list bullets and `###` entry headers. The archive lives under `memory/` so it is still indexed and **searchable via `memory_search`** — the agent recalls completed work on demand instead of carrying its full changelog in-prompt. It is conservative (uppercase words only, so prose like "Closes #123", "we're not done", or an unchecked `[ ]` box is never archived), idempotent, and never drops an open/active item.
666
- - **Budget-scaled pruning** — when `MEMORY.md` is over its soft budget, the reviewer is put in an explicit net-shrink mode (propose only length-reducing ops) and `maxChangesPerRun` scales up **for removals** (the add cap stays tight), so an over-budget file converges toward budget instead of trickling at a few edits per night.
667
- - **Archive staleness GC (`gateway.dreaming.staleness`, planning-66)** — a deterministic pass that runs next to the compactor (auto mode) to keep the Lane-2 archive's **search quality** high. This is a **search-quality fix, not a prompt-budget one**: planning-65 already moved task-log off the injected prompt, so the point here is that `memory_search` should keep surfacing *current* truth instead of stale/superseded facts. Each nightly run **soft-invalidates** archive entries — superseded ones (a deterministic `supersedes/replaces/obsoletes #N` match, which finally populates the previously-inert `supersedes_key`) and aged-out ones (idle-since-last-**retrieval** past `staleTtlDays` and retrieved fewer than `minRetrievalKeep` times) — by **moving** them to `memory/archive/stale.md` and stamping `invalid_at`. It **never deletes**: a staled entry stays under `memory/` so it is still indexed and **searchable** (ยุบได้แต่ไม่ลืม). An entry that is **retrieved after** it was invalidated is **promoted back** to the active archive (the recall feedback loop — proof we aged it out too soon). Recall is fed by an append-only read-path log (`kb_retrieval_log`, gated by `recordRetrievals`) that the GC folds into each entry's `last_retrieved`. High-importance entries (`keepImportance`) and **pinned** files (`memory/pinned/**`) are never aged out; evergreen Lane-1 (`MEMORY.md`/`USER.md`) is structurally excluded. Every move is CAS-guarded with a timestamped backup, and — being a memory-only write — drops **no live session**. One run may soft-invalidate at most `staleness.maxInvalidationsPerRun` entries (default `50`), oldest-idle first, with the remainder resuming on later runs — aging is wall-clock driven, so without a ceiling the first run after anything that widens the GC's visibility (such as backfilling lifecycle rows for previously invisible sources) would relocate every already-expired entry in one night. Restores are never capped. Kill-switches: `staleness.enabled:false` (GC no-ops), `maxInvalidationsPerRun:0` (never invalidates, still restores) and `recordRetrievals:false` (age falls back to first-seen only).
668
-
669
- ### `gateway.knowledge`
670
-
671
- **Two-lane memory** — a per-agent searchable knowledge archive so an agent can recall what does not fit in the always-injected core. A SQLite/FTS5 index (`agents/<id>/kb.sqlite`, built on Node's built-in `node:sqlite` — no new dependency) covers the agent's `memory/*.md` notes plus the evergreen `MEMORY.md`/`USER.md`. Every chunk is tagged with **fail-closed provenance** (`owner`/`agent`/`untrusted`/`system`; unclassified ⇒ `untrusted`). The index is refreshed by a detached subprocess at session spawn, entirely **off the gateway event loop**.
672
-
673
- Two read-only MCP tools expose it to the agent: **`memory_search`** (keyword/FTS5 → ranked snippets with file+line, provenance, importance) and **`memory_get`** (bounded, path-traversal-guarded excerpt of a memory-scoped file). When `MEMORY.md` grows past its `gateway.memory` soft budget, compose injects a compact **auto-generated section index** + a pointer to `memory_search` instead of the truncated full text (**core-shrink**) — the on-disk file is never modified and its full content stays searchable. Whenever the archive is on, a short `--- MEMORY RETRIEVAL ---` note is also injected into every agent's system prompt so the tools stay discoverable at all times (not only when the file is over budget).
674
-
675
- | Field | Default | Description |
676
- |-------|---------|-------------|
677
- | `archive.enabled` | `true` | Master switch (`false` ⇒ complete no-op, no DB created, no core-shrink) |
678
- | `archive.tokenizer` | `"unicode61"` | FTS5 tokenizer (`"trigram"` for CJK/Thai) |
679
- | `archive.chunkTokens` | `400` | Target chunk size in ~tokens |
680
- | `archive.chunkOverlap` | `80` | Overlap between chunks (clamped below `chunkTokens`) |
681
- | `shared.enabled` | `true` | Enable the cross-agent shared KB |
682
- | `shared.project` | `"global"` | Sharing partition key (one safe path segment) — agents with the same value share one vault; `"global"` ⇒ shared-by-default |
683
- | `shared.root` | `~/.claude-gateway/shared/kb` | Shared vault root dir (`<root>/<project>/`) |
684
- | `shared.mode` | `"auto"` | Per-agent→shared promotion mode; `auto` = promote durable dreamed facts, `propose` = dry-run |
685
- | `shared.graph` | `false` | Compile the memory-wiki graph + dashboards over the shared vault to `<vault>/reports/*.md` (opt-in). Independent of the dashboard **Knowledge base** tab, which computes its graph on-demand |
686
- | `shared.staleness` | *(object)* | Shared-note TTL lifecycle GC; uses the same fields/defaults as `dreaming.staleness` (whole notes only; no numeric `supersedes #N` syntax) |
687
- | `reflection.enabled` | `true` | Enable the singleton, per-shared-vault reflection scheduler (daily timer; see cadence note below) |
688
- | `reflection.dayOfWeek` / `hour` / `minute` / `timezone` | `0` / `4` / `0` / `UTC` | `hour`/`minute` is the **daily** staleness-GC slot; `dayOfWeek` selects the weekday that additionally runs LLM consolidation (Sunday 04:00 UTC by default; invalid timezone falls back to `gateway.timezone`, then `UTC`) |
689
- | `reflection.maxClustersPerRun` / `reviewModel` | `5` / `claude-haiku-4-5-…` | Hard cap on changed linked-note clusters per consolidation run and the bounded synthesis model |
690
-
691
- **Shared KB.** A shared SQLite/FTS5 vault outside any single agent's workspace lets agents build a common knowledge base. Notes under `<root>/<project>/notes/*.md` are indexed and reachable via `memory_search` with `corpus:"shared"` (the shared vault) or `corpus:"all"` (this agent's memory + shared, merged by relevance). Concurrent writers are safe without a lock — atomic note writes (temp+rename) plus a cross-process `PRAGMA busy_timeout` on the index. Per-agent overrides under the agent's own `knowledge` block. The MCP layer runs under Bun, so the read tools query `kb.sqlite` via `bun:sqlite`. Two write paths feed the vault, sharing one freeform-name namespace (issue #386, no agent-id prefix, no ownership scoping): the nightly dreaming promoter (gated by `mode:"auto"`; it promotes only content that carries a real fact — content that is nothing but `MEMORY.md` index-pointer bullets is skipped, since those links resolve only inside the promoting agent's own workspace — and names each note after the proposal's `topic` slug when the reviewer supplied one, falling back to its `reason`, so a recurring fact updates the same note across nights instead of piling up near-duplicates; a fallback name that reads as an editing instruction rather than the name of a fact is passed over, and the note is named from the fact itself instead — the promotion is only abandoned when nothing nameable remains, and every skip is logged — including a write the note-size cap refuses and an unexpected write failure. A name that doesn't collide is checked against a near-duplicate search, but an unattended **merge** now also requires real token containment against the candidate — below that bar the fact gets its own note, since two notes are recoverable while two unrelated facts fused into one are not. `[[wikilink]]`s to related notes use a lower bar than merges, because a link is additive where a merge is destructive — and they are attached whether the fact merges or lands as a new note, so a note below the merge bar is never a disconnected graph node. Containment is scored against each candidate's full body rather than the matched chunk, though against a capped seed — the bar means "half of the fact's leading topic words are already here", not half of the whole fact. Retired `stale__*` notes are never merge targets; a recurrence of a retired name folds the retired body back in and removes the twin on both the create and the update path, because a retired note stays searchable and a twin beside a live note of the same name would answer every query twice forever. The twin is only dropped once the merged write lands (issue #398)) and the **`memory_shared_create`**/**`memory_shared_get`**/**`memory_shared_update`**/**`memory_shared_delete`** MCP tools, which let any agent create, read, update, or delete any note on demand regardless of `mode`. `memory_shared_create` warns instead of writing when it finds content-similar existing notes (pass `confirm:true` to proceed — related notes get `[[wikilink]]`ed into the new note rather than left disconnected); `memory_shared_update` warns instead of writing when the edit would drop 50%+ of the existing note's lines (same `confirm:true` escape hatch). Immediate reindex after every write or delete.
692
-
693
- **Shared lifecycle + reflection (issues #392, #398).** Each shared note receives a stable whole-file lifecycle identity during indexing — including notes whose content has not changed since they were first indexed, which are backfilled from their source mtime so their real age is preserved. Its deterministic TTL GC runs **daily**, soft-invalidating aged low-recall notes by moving them to `notes/stale__<name>.md` (never deleting them from the searchable vault); a retrieval after invalidation restores the original active name. Shared `memory_search` and `memory_shared_get` reads feed the same append-only retrieval log as personal archive recall. The singleton reflection scheduler runs **once per resolved shared-vault root**, not once per agent, and fires **daily** at `hour:minute` (a fire that lands a hair early re-arms on the *next* day's slot rather than serving the same one twice): every fire runs the inexpensive TTL GC (no model call), while graph/LLM consolidation runs only on `dayOfWeek` — and even then is skipped when `kb_index_state.revision` has not changed since the prior consolidation. Weekly model spend is therefore unchanged, while a note that is retired and then retrieved returns to the active set within a day instead of up to a week. For changed vaults it clusters only active wikilink-connected notes deterministically, then makes at most `reflection.maxClustersPerRun` bounded reviewer calls to merge genuinely duplicate clusters; related-but-distinct notes remain merely linked.
694
-
695
- **Knowledge base viewer.** The web dashboard's **Knowledge base** tab renders the shared vault as an Obsidian-style force-directed graph (nodes = notes sized by link degree and coloured by `type`; edges = `[[wiki-links]]`; contradicting claims and stale notes are flagged). It is fed by `GET /knowledge/graph`, which computes the model **on-demand** from the vault (no dependency on `shared.graph` or the nightly reindex). When the vault is empty it shows a clearly-labelled demo dataset (with a size selector for scale testing). A **source** selector switches the graph between the cross-agent Shared KB and any single agent's own Lane-2 memory (`workspace/memory`), a node **search** box filters the graph, and clicking a node opens its full note (fetched via `GET /knowledge/note`) rendered as Markdown below the graph.
696
-
697
- **Nightly dreaming viewer.** A **Nightly dreaming** tab renders each agent's memory-consolidation audit trail (`.dreaming/DREAMS.md` + `promotions.jsonl`) as a newest-first timeline of runs — mode (propose/auto), outcome, the proposed/applied changes with scores + anchors, and per-run token/session counts — fed by `GET /knowledge/dreams` and filterable by agent. For a `propose`-mode run you can **accept** proposals directly from the tab: an **Accept** button per proposal (and **Accept all** per run) POSTs to `POST /knowledge/dreams/apply`, which applies the selected ops to `MEMORY.md`/`USER.md` through the same K4 safe applier auto mode uses (backup + bounded-loss + net-negative + CAS; memory-only ⇒ no restart) and — when the shared KB is `auto` — promotes applied `add`s to the shared vault. Accepts are idempotent (recorded to `.dreaming/accepted.jsonl`); applied proposals show ✓ and a proposal whose anchor has since drifted is safely skipped and stays pending for a later retry.
698
-
699
- ### `gateway.bind`
700
-
701
- Network interface the HTTP/WebSocket server binds to. Defaults to `127.0.0.1` (localhost-only), so the dashboard and API are **not** exposed to the local network out of the box. Set to `0.0.0.0` to listen on all interfaces (for example when a containerized reverse proxy needs to reach the gateway). The `GATEWAY_BIND` environment variable, when set, takes precedence over this field.
702
-
703
- > **⚠️ Binding to `0.0.0.0`? Configure an admin key in `gateway.api.keys`.** The
704
- > monitoring surface (`/status`, `/processes`) and the dashboard require an
705
- > **admin** API key (`admin: true`) or a dashboard session when keys are
706
- > configured — a scoped or write-only key is rejected (`401`), because the
707
- > dashboard grants cross-agent, host-wide power (including PTY keystroke injection
708
- > into any session). The dashboard prompts for an admin key at `/dashboard` and
709
- > stores an `HttpOnly` session cookie (issued only to an admin key). `/health`
710
- > stays public but returns only `{"status":"ok"}` (no agent ids). With **no** keys
711
- > configured the gateway **fails closed on a non-loopback bind**: `/status`,
712
- > `/processes`, and `/dashboard` return `503` until you set `gateway.api.keys`
713
- > (a startup warning is logged); if keys are set but **none is admin**, the
714
- > dashboard is inaccessible and a startup warning is logged. On a loopback bind
715
- > they stay open, so local keyless installs are unaffected. The gateway serves
716
- > plain HTTP; put TLS in
717
- > front (reverse proxy) so credentials are not sent in the clear.
718
-
719
- ```json
720
- {
721
- "gateway": {
722
- "bind": "127.0.0.1"
723
- }
724
- }
725
- ```
726
-
727
- > **⚠️ Upgrade note:** the default bind changed from `0.0.0.0` to `127.0.0.1` (configVersion 1.0.13). To avoid silently cutting off external access, the config migrator is **behavior-preserving**: whenever it upgrades a config that never set `gateway.bind`, it pins `bind` to `0.0.0.0` and logs a one-time warning, so a deployment that was reachable from another host stays reachable. This applies to *any* upgraded config with no `bind` key — including one already stamped `1.0.13` that never received a bind (an earlier version gated this on `< 1.0.13` and left such configs stuck on the `127.0.0.1` default). New installs (no prior config, so no migration runs) keep the secure `127.0.0.1` default. If you *want* localhost-only after upgrading, set `gateway.bind` to `127.0.0.1` explicitly (or the `GATEWAY_BIND` env var).
728
-
729
- ### `gateway.publicUrl`
730
-
731
- Absolute, externally-reachable origin of the gateway (for example `https://gateway.example.com`, or `https://host.example.com/gateway` behind an ingress path prefix). The process cannot infer its own public URL — it binds localhost by default and sits behind a reverse proxy — so it must be set explicitly for features that hand out a phone-openable link. Currently that is the `/cli` terminal viewer; when `publicUrl` is unset, `/cli` replies that the viewer is not configured. Leave it blank to keep `/cli` disabled. A trailing slash is optional. Use an `https://` origin — Telegram Mini Apps require HTTPS.
732
-
733
- The CLI does **not** route through this URL when it runs on the gateway's own host: both addresses are the same server, and the public one only adds a reverse-proxy hop that may enforce its own authentication. It talks to the local bind instead, keeping `publicUrl` as a fallback if that address cannot be reached. Pass `--url` to exercise the proxy path deliberately. See [CLI.md](./CLI.md) for the full precedence.
734
-
735
- ```json
736
- {
737
- "gateway": {
738
- "publicUrl": "https://gateway.example.com"
739
- }
740
- }
741
- ```
742
-
743
- ### Terminal Viewer — interactive terminal mode
744
-
745
- The dashboard's **Terminal Viewer** opens read-only (a live mirror of the PTY). A toggle in the top-right of the viewer switches it into an **interactive terminal**: keystrokes typed into the panel — printable characters, Enter, arrows, Ctrl-combos, Esc — are streamed into the live PTY, and the panel title changes to reflect the active mode. This is a per-browser client-side choice (Issue #201); there is no server config flag to enable it.
746
-
747
- Because interactive mode turns a read-only view into a remote-write surface, access is protected upstream rather than by a feature flag:
748
-
749
- - **Authentication** — the WebSocket requires a valid dashboard ticket or **admin** API key. The ticket is minted at `POST /api/v1/pty-stream-ticket`, which itself requires an admin API key or a valid dashboard session cookie — so an unauthenticated (or non-admin) caller cannot obtain one. The dashboard gets its session by logging in with an admin key at `/dashboard` (`HttpOnly` cookie); no token is embedded in the page.
750
- - **`gateway.bind`** — the gateway binds to `127.0.0.1` (localhost) by default, so the dashboard is not reachable from the network out of the box. On a non-loopback bind (`0.0.0.0`), configure an admin key in `gateway.api.keys` so the dashboard and monitoring endpoints require an admin credential, and prefer a TLS-terminating reverse proxy so credentials are not sent in the clear.
751
-
752
- Inbound frames are always bounded (text-only, size-capped) and are dropped for headless sessions (no PTY).
753
-
754
- #### `/cli` — open the terminal viewer from chat
755
-
756
- The `/cli` command (Telegram, Discord, LINE) opens the same live terminal viewer for **one agent**, without an admin key. It requires `gateway.publicUrl` and an agent running with `gateway.headless: false`. Unlike the admin dashboard, a `/cli` session is **agent-scoped**: its cookie and PTY ticket can only reach the originating agent's own sessions — never another agent, the process tree, or a cross-agent stream.
757
-
758
- The viewer link is never a credential; unlocking it requires a proof tied to an allowlist-gated chat action:
759
-
760
- - **Telegram** opens a Mini App and the gateway verifies Telegram's signed `initData` (HMAC with the agent's own bot token) — nothing secret rides in the URL, and the `initData` user must match the user who ran `/cli`.
761
- - **Discord** and **LINE** send an open-viewer link plus an **Approve** button; the browser stays locked until you approve in the chat, so a leaked or forwarded link cannot be unlocked by anyone who cannot approve there.
762
-
763
- The first browser to open a link owns it (opening the link in a second browser is rejected), the viewer defaults to read-only (toggle for input), and viewer sessions expire (30 min) — send `/cli` again to reconnect.
764
-
765
- ### `gateway.api.keys`
766
-
767
- Each key has a `key` string (supports `${ENV_VAR}` interpolation), an optional `description`, and an `agents` field — either an array of agent IDs or `"*"` for full access. Keys support both `Authorization: Bearer` and `X-Api-Key` headers.
768
-
769
- ### Bot tokens
770
-
771
- Tokens are stored per-agent at `~/.claude-gateway/agents/<id>/.env` and auto-loaded at startup **and before every config reload** — so an agent added to `config.json` while the gateway is running starts without a restart, even though its token only exists in a brand-new `.env`. Use `${AGENT_BOT_TOKEN}` syntax in config to reference them, or set them as shell environment variables. Lines are `KEY=value`; `#` comments and blank lines are ignored, and surrounding quotes are stripped, the same as in `~/.claude-gateway/.env`.
772
-
773
- A variable you exported yourself always wins over the `.env` file and is never replaced by a reload. A token the gateway did read from a `.env` is refreshed when that file changes, so **rotating a token takes effect on the next config reload** rather than at the next restart. Note that only `config.json` is watched — editing a `.env` by hand applies on the following reload, while the MCP `agent_create` / `agent_update` tools write both files and so take effect immediately. If a `${VAR}` cannot be resolved from anywhere, that one agent is skipped — the rest of the gateway starts normally — and the skip is logged to `logs/gateway.log` with the name of the missing variable.
774
-
775
- WhatsApp Cloud's credentials (`accessToken`, `phoneNumberId`, `appSecret`, `verifyToken`) are plain fields under the agent's `whatsapp_cloud` config block, not a dedicated "bot token" field — but they resolve through the exact same mechanism as Telegram/Discord bot tokens: reference them as `${VAR}` in `config.json` and put the value in the agent's `.env` (or export it as a shell variable), same as above.
776
-
777
- ---
778
-
779
- ## Architecture
780
-
781
- ```
782
- ┌─────────────────────────────────────────────────┐
783
- │ Claude Gateway │
784
- │ │
785
- Telegram Bot A ──► TelegramReceiver(A) ──► AgentRunner(A) ─┬─► Session(chat:111) ──► Claude + MCP
786
- ├─► Session(chat:222) ──► Claude + MCP
787
- Telegram Bot B ──► TelegramReceiver(B) ──► AgentRunner(B) ──┴─► Session(chat:333) ──► Claude + MCP
788
-
789
- HTTP Client ──► POST /api/v1/.../messages ────────────────┴─► Session(api:uuid) ──► Claude
790
- (sync JSON or SSE stream)
791
- │ │
792
- │ GatewayRouter (/health, /status, /ui, /api) │
793
- │ CronScheduler (HEARTBEAT.md + REST API) │
794
- │ TypingManager (live status indicators) │
795
- └─────────────────────────────────────────────────┘
796
-
797
- ┌───────────────────────────────────┐
798
- │ MCP Server (per session) │
799
- │ mcp/server.ts │
800
- │ │
801
- │ telegram_reply │
802
- │ telegram_react │
803
- │ telegram_edit_message │
804
- │ telegram_download_attachment │
805
- │ cron_list / cron_create / ... │
806
- │ skill_create / skill_delete / ... │
807
- └───────────────────────────────────┘
808
- ```
809
-
810
- Each agent runs a **dedicated TelegramReceiver** (single poller per bot token) and a **session pool** of isolated Claude subprocesses — one per chat or API session. Each session gets its own **MCP server** (`mcp/server.ts`) exposing channel-specific tools (Telegram reply, react, cron management, skill management). Sessions persist history via `SessionStore`, so Claude remembers the conversation even after idle restart.
811
-
812
- ### Session Pool
813
-
814
- Each agent maintains a **session pool** — a separate Claude subprocess per chat ID (Telegram) or session UUID (API). Sessions are fully isolated: Claude sees only its own conversation history with no cross-session leakage.
815
-
816
- ```
817
- TelegramReceiver (1 per agent, spawned by gateway)
818
- - single long-poll connection per bot token
819
- - handles access control (allowlist / pairing)
820
- - runs as: bun mcp/tools/telegram/receiver-server.ts (RECEIVER_MODE)
821
- - POSTs incoming messages to AgentRunner callback
822
-
823
- AgentRunner (session pool manager)
824
- ├── SessionProcess(chat:111) ──► Claude subprocess + MCP server (SEND_ONLY)
825
- ├── SessionProcess(chat:222) ──► Claude subprocess + MCP server (SEND_ONLY)
826
- └── SessionProcess(api:uuid) ──► Claude subprocess (no MCP — API-only)
827
- ```
828
-
829
- ### MCP Tool System
830
-
831
- The MCP server (`mcp/server.ts`) uses a **modular multi-channel architecture**. Each channel is a separate module implementing `ChannelModule` or `ToolModule` interfaces:
832
-
833
- | Module | Interface | Tools | Purpose |
834
- |--------|-----------|-------|---------|
835
- | `telegram` | `ChannelModule` | `telegram_reply`, `telegram_react`, `telegram_edit_message`, `telegram_download_attachment` | Send messages, reactions, edit messages in Telegram |
836
- | `discord` | `ChannelModule` | `discord_reply`, `discord_react`, `discord_edit_message` | Send messages, reactions, edit messages in Discord |
837
- | `cron` | `ToolModule` | `cron_list`, `cron_create`, `cron_update`, `cron_delete`, `cron_run`, `cron_get_runs` | Manage scheduled jobs via gateway REST API |
838
- | `skills` | `ToolModule` | `skill_create`, `skill_delete`, `skill_install` | Create, delete, and install agent skills at runtime |
839
-
840
- Tools are **prefixed by channel name** to avoid collisions. Each module controls its own visibility and lifecycle.
841
-
842
- **Adding a new channel** (e.g. Slack) means implementing `ChannelModule` interface in `mcp/tools/slack/module.ts` and registering it in `server.ts`.
843
-
844
- **Connectors** are the other half of the MCP picture: where the modules above are tools the gateway itself implements, a connector is an **external** MCP server the gateway injects into a session's `mcp-config.json`. The gateway stores only the connector definition, the per-connector secret (`~/.claude-gateway/mcp-token.env`) and the per-agent enablement — Claude Code then talks to that server directly. See [`gateway.customConnectors`](#gatewaycustomconnectors-optional) and [API.md](./API.md#connectors-api).
845
-
846
- ### Process Modes
847
-
848
- | Mode | Process | Behaviour |
849
- |------|---------|-----------|
850
- | `TELEGRAM_RECEIVER_MODE` | `receiver-server.ts` | Polls Telegram, handles commands, POSTs to callback — **no MCP** |
851
- | `TELEGRAM_SEND_ONLY` | `server.ts` | Exposes MCP tools (`telegram_*`, `cron_*`) — **no polling** |
852
-
853
- #### Receiver lifecycle
854
-
855
- Receivers are child processes, so they only stop when the gateway runs its
856
- shutdown path. Two mechanisms keep them from outliving it:
857
-
858
- - **`SIGTERM`, `SIGINT` and `SIGHUP` all run the same graceful shutdown.**
859
- `SIGHUP` matters because Node's default action for it terminates the process
860
- *without* running handlers — so before this was wired, closing a tmux pane or
861
- dropping an SSH session killed the gateway and left every receiver reparented
862
- to `init`. Teardown escalates `SIGTERM` → `SIGKILL` after a short grace period,
863
- so a receiver wedged in an in-flight long-poll cannot survive it.
864
-
865
- - **A boot-time sweep reclaims leftovers.** `SIGKILL` and the OOM killer can
866
- never be handled in-process, so at startup the gateway terminates any
867
- `receiver-server.ts` process that was spawned from *its own* installation and
868
- has been reparented to `init` (proof that its supervisor is gone), logging how
869
- many it reclaimed — and separately warning about any it could **not** reclaim,
870
- since those are still running. Receivers belonging to another checkout on the
871
- same host, or to a gateway that is still running, are never touched.
872
-
873
- On a host where an ancestor is a child subreaper (`systemd --user`,
874
- `docker run --init`/tini, s6), orphans reparent to that subreaper instead of to
875
- `init` and the sweep finds nothing. Clean shutdown still works; what is lost is
876
- the `SIGKILL`/OOM recovery — though such a host usually has a supervisor that
877
- reaps the process group itself.
878
-
879
- ### Session Persistence
880
-
881
- History is persisted to `SessionStore` (`.jsonl` files) after each message. When a session is spawned after an idle restart, history is injected into the initial prompt so Claude resumes the conversation seamlessly.
882
-
883
- ---
884
-
885
- ## Live Status Messages
886
-
887
- While an agent is working, the gateway sends real-time status updates to Telegram showing what the agent is doing:
888
-
889
- ```
890
- ☑️ : 🧠 Analyzing the codebase structure...
891
- ☑️ : 📖 Reading: src/agent/runner.ts
892
- ☑️ : 🔍 Searching for: "sendMessage" in src/
893
- 🕐 : ✏️ Editing: mcp/tools/telegram/typing.ts
894
- (elapsed: 2m 30s)
895
- ```
896
-
897
- - **Tool tracking** — each tool call is displayed with a descriptive label (e.g. `📖 Reading: config.ts`, `⚡ Running: npm test`)
898
- - **History** — previous steps shown with ☑️, current step with 🕐
899
- - **Thinking** — agent's reasoning shown with 🧠
900
- - **Elapsed time** — total time since the agent started working
901
- - **Auto-cleanup** — status message is deleted when the agent finishes
902
-
903
- Status updates are sent every 5-10 seconds (first update at 5s, then every 10s). A single
904
- message is **edited in place** for the whole turn; a tick with nothing new to show issues no
905
- update at all, and the message is replaced only if it is deleted or becomes uneditable.
906
-
907
- ---
908
-
909
- ## Command Line (CLI)
910
-
911
- The `claude-gateway` binary doubles as a command-line client for a running gateway — a friendlier alternative to hand-built `curl` calls. It works the same whether the gateway was started with `make start`, pm2, or systemd (it resolves the target from your config). Run it with no arguments to see what it can do; **only `gateway start` boots the server**.
912
-
913
- ```bash
914
- claude-gateway # help (never starts a server)
915
- claude-gateway gateway start # run the gateway in the foreground
916
- claude-gateway gateway status # is it running? which manager owns it?
917
- claude-gateway gateway logs --follow # stream the gateway log (reads files, needs no server)
918
- claude-gateway service install # run it as a systemd-user (or --manager pm2) service
919
- claude-gateway service start|stop|restart # drive the installed service (found even if inactive)
920
- claude-gateway app list # installed Docker-compose apps and their status
921
- claude-gateway app install agent-note # install from the community registry
922
- claude-gateway app start|stop|restart <name>
923
- claude-gateway update check # newer claude-gateway published?
924
- claude-gateway claude update # update Claude Code via its own updater
925
- claude-gateway doctor # check config / key / connectivity
926
- claude-gateway agents create # interactive wizard — new agent + optional channel
927
- claude-gateway channels pending --agent alfred # incoming Telegram/Discord pairing requests
928
- claude-gateway crons list # friendly <noun> <verb> commands
929
- claude-gateway crons run <jobId>
930
- claude-gateway debug-bundle # small redacted bundle for a stuck session (works even if the server is down)
931
- claude-gateway api GET /v1/agents # escape hatch: call any endpoint directly
932
- ```
933
-
934
- ### Reading the logs
935
-
936
- `gateway logs` reads the log files directly, so it answers whether or not the gateway is
937
- running — which is usually exactly when you need it.
938
-
939
- ```bash
940
- claude-gateway gateway logs # last 50 lines of logs/gateway.log
941
- claude-gateway gateway logs --lines 200 --follow # more history, then stream
942
- claude-gateway gateway logs --agent alfred # that agent's stream instead
943
- claude-gateway gateway logs --json # the stored JSON lines, verbatim
944
- ```
945
-
946
- Each line is stored as one JSON object and rendered as `<ts> <LEVEL> <message>` with `data`
947
- appended; `--json` prints the stored line unchanged, for piping into `jq`. `--agent <id>` takes
948
- any stream id in the log directory — agents (`alfred`), receivers (`alfred:receiver`), and
949
- sessions (`alfred:session:<uuid>`) each get their own file — and an unknown id lists the ids that
950
- do exist rather than reporting an empty result. `--follow` survives a rotation: when the file it
951
- is watching is renamed away it reopens the new one instead of going quiet.
952
-
953
- Unlike `debug-bundle`, this output is **not redacted** — it is the local file you could already
954
- `cat`. Skim before pasting it anywhere.
955
-
956
- > **Upgrading from < 1.8:** a service unit that runs the binary with no command still starts the
957
- > gateway, with a deprecation warning. Point `ExecStart` at `claude-gateway gateway start`, or
958
- > reinstall the unit with `claude-gateway service install`.
959
-
960
- Working on the CLI itself? The globally installed `claude-gateway` is the published npm package, not
961
- your checkout, so a bare `claude-gateway` still runs whatever version is on your `PATH`. Use `make cli`
962
- to build and exercise the local sources instead:
963
-
964
- ```bash
965
- make cli ARGS="--help"
966
- make cli ARGS="gateway status"
967
- ```
968
-
969
- Commands are **generated from the same route manifest the server mounts**, so every endpoint exposed as a friendly command stays in sync with the API automatically. Global flags: `--url`, `--key`, `--json`, `--data <json>`, `--help`.
970
-
971
- See **[CLI.md](./CLI.md)** for the full command reference.
972
-
973
- ---
974
-
975
- ## HTTP API
976
-
977
- > For day-to-day operation, prefer the **[CLI](#command-line-cli)** above (`claude-gateway <noun> <verb>`) — it resolves the URL and key for you and is easier to read. This section is the **raw HTTP reference** for programmatic clients and integrations.
978
-
979
- When `gateway.api.keys` is configured, the gateway exposes a REST API for external clients.
980
-
981
- Pass API key via `X-Api-Key: <key>` or `Authorization: Bearer <key>` header.
982
-
983
- **Endpoints:**
984
-
985
- | Method | Path | Description |
986
- |--------|------|-------------|
987
- | `GET` | `/api/v1/agents` | List agents accessible by the provided key |
988
- | `POST` | `/api/v1/agents/:agentId/messages` | Send a message — sync JSON or SSE stream |
989
- | `GET` | `/api/v1/crons` | List cron jobs accessible by key |
990
- | `GET` | `/api/v1/crons/status` | Scheduler status |
991
- | `POST` | `/api/v1/crons` | Create a scheduled job |
992
- | `GET` | `/api/v1/crons/:id` | Get a single job |
993
- | `PUT` | `/api/v1/crons/:id` | Update a job |
994
- | `DELETE` | `/api/v1/crons/:id` | Delete a job |
995
- | `POST` | `/api/v1/crons/:id/run` | Trigger a job manually |
996
- | `GET` | `/api/v1/crons/:id/runs` | Get run history |
997
- | `GET` | `/api/v1/agents/sessions` | List all sessions across all agents (admin key) |
998
- | `GET` | `/api/v1/agents/:agentId/chats` | List chats for an agent |
999
- | `DELETE` | `/api/v1/agents/:agentId/chats/:chatId` | Delete a chat and all its messages |
1000
- | `GET` | `/api/v1/agents/:agentId/chats/:chatId/sessions` | List sessions for a specific chat |
1001
- | `GET` | `/api/v1/agents/:agentId/chats/:chatId/messages` | Paginated message history (cursor-based) |
1002
- | `POST` | `/api/v1/agents/:agentId/chats/:chatId/sessions/:sessionId/messages` | Inject a message into an existing session |
1003
- | `POST` | `/api/v1/agents/:agentId/media` | Upload a media file (image or PDF) |
1004
- | `GET` | `/api/v1/agents/:agentId/media/*` | Serve a media file by path |
1005
- | `PUT` | `/api/v1/agents/:agentId/avatar` | Upload or replace agent avatar (admin/write) |
1006
- | `DELETE` | `/api/v1/agents/:agentId/avatar` | Remove agent avatar (admin/write) |
1007
- | `GET` | `/api/v1/agents/:agentId/avatar` | Serve agent avatar image |
1008
- | `POST` | `/api/v1/agents/wizard/start` | Start wizard: generate agent workspace via Claude (admin) |
1009
- | `PUT` | `/api/v1/agents/wizard/:wizardId/avatar` | Upload avatar to wizard before confirm (admin) |
1010
- | `POST` | `/api/v1/agents/wizard/:wizardId/confirm` | Write workspace to disk and add agent to config (admin) |
1011
- | `POST` | `/api/v1/agents/wizard/:wizardId/channel` | Verify bot token and generate pairing code (admin) |
1012
- | `POST` | `/api/v1/agents/wizard/:wizardId/channel/verify` | Poll for pairing code confirmation (admin) |
1013
- | `POST` | `/api/v1/agents/wizard/:wizardId/complete` | Skip channel and finalise wizard (admin) |
1014
- | `GET` | `/api/v1/apps/registry` | Browse community app registry (admin key) |
1015
- | `POST` | `/api/v1/apps/install` | Install app from registry, GitHub, or local path → `jobId` (admin) |
1016
- | `POST` | `/api/v1/apps/inspect` | Preview a source's required/generated secrets before install, no install (admin) |
1017
- | `GET` | `/api/v1/apps/jobs/:jobId` | Poll install/update job status and logs |
1018
- | `GET` | `/api/v1/apps` | List installed apps |
1019
- | `GET` | `/api/v1/apps/:name` | Get app info |
1020
- | `DELETE` | `/api/v1/apps/:name` | Uninstall app (admin) |
1021
- | `POST` | `/api/v1/apps/:name/start\|stop\|restart` | Start/stop/restart app containers (admin) |
1022
- | `POST` | `/api/v1/apps/:name/update` | Blue-green update with auto-rollback → `jobId` (admin) |
1023
- | `POST` | `/api/v1/apps/:name/reconfigure` | Change env vars / host ports on an installed app, with rollback → `jobId` (admin) |
1024
- | `GET` | `/api/v1/connectors` | List connectors with connected state |
1025
- | `GET` | `/api/v1/connectors/:id/status` | Connected state for one connector (for polling) |
1026
- | `POST` | `/api/v1/connectors/:id/connect` | Store a pasted token (admin) |
1027
- | `POST` | `/api/v1/connectors/:id/oauth/receive` | Accept a token pushed by an external control plane (admin) |
1028
- | `DELETE` | `/api/v1/connectors/:id` | Disconnect a connector (admin) |
1029
- | `POST` | `/api/v1/connectors/custom` | Add a user-pasted connector (admin) |
1030
- | `POST` | `/api/v1/connectors/custom/:id/oauth/start` | Begin OAuth 2.1 + PKCE sign-in → `authorizeUrl` (admin) |
1031
- | `GET` | `/oauth/mcp/callback` | OAuth redirect target (public — guarded by a single-use `state`) |
1032
- | `GET` | `/app/:name/:portName/*` | Reverse proxy to installed app (no auth) |
1033
-
1034
- **Wizard API** — create agents programmatically with the same flow as the interactive `claude-gateway agents create` terminal wizard. The wizard generates workspace files via Claude, writes them on confirm, and optionally pairs a Telegram/Discord bot. State is in-memory with a 30-minute TTL; nothing is written until `/confirm`. See [API.md](./API.md) for the full wizard flow.
1035
-
1036
- See **[API.md](./API.md)** for full reference with request/response schemas and curl examples.
1037
-
1038
- ---
1039
-
1040
- ## App Store
1041
-
1042
- Install Docker-compose apps on the gateway. Apps get a reverse-proxied HTTP endpoint, an optional Unix socket bridge for executing host scripts, and optional AI agent injection.
1043
-
1044
- **Quick install from registry:**
1045
-
1046
- ```bash
1047
- curl -X POST http://localhost:10850/api/v1/apps/install \
1048
- -H "X-Api-Key: <admin-key>" \
1049
- -H "Content-Type: application/json" \
1050
- -d '{"registry_app": "getpod-manager", "env_vars": {"API_KEY": "<secret>"}}'
1051
- ```
1052
-
1053
- **Poll until done:**
1054
-
1055
- ```bash
1056
- curl http://localhost:10850/api/v1/apps/jobs/<jobId> -H "X-Api-Key: <key>" | jq .status
1057
- ```
1058
-
1059
- **Or use the CLI**, which wraps the same endpoints (see [CLI.md](./CLI.md) for the full reference):
1060
-
1061
- ```bash
1062
- claude-gateway app install agent-note --env-file ./agent-note.env --wait # follow the job to completion
1063
- claude-gateway app list # installed apps + status
1064
- claude-gateway app stop agent-note
1065
- claude-gateway app uninstall agent-note --yes
1066
- ```
1067
-
1068
- `--env-file` reads `KEY=VALUE` lines from a dotenv file. Prefer it over `--env` for anything secret:
1069
- a value passed on the command line is readable by every local user in `/proc/<pid>/cmdline` while the
1070
- install runs, and is written to your shell history. `--env` wins if both set the same variable.
1071
-
1072
- **App is then live at** `/app/getpod-manager/<portName>/`.
1073
-
1074
- Apps can also be installed from a GitHub URL (`github_url` + `commit`) or a local path (`local_path`) for development. Updates use a **blue-green swap with automatic rollback** — the old containers stay intact until the new version passes its healthcheck.
1075
-
1076
- The swap carries live bind-mount data forward into the new app directory. A data directory the app's own container created is owned by that image's uid (postgres leaves its `pgdata` mode 0700), and `rename(2)` on a directory needs write permission on the directory itself — so the gateway user cannot move it. Those paths are moved by a throwaway root helper container instead, mounting the nearest common ancestor of the two app directories so the move stays a real rename rather than a copy; each escalation is logged in the job. If a rollback cannot move such a path back, the update does **not** restart the app on a half-restored directory: the `-failed-` directory holding the live data is kept, the job fails with `ROLLBACK FAILED`, and the log names the paths and the directory to recover them from. That directory is kept for good — the boot sweep that reclaims update scratch dirs skips release snapshots, because it deletes with `sudo rm -rf` and a snapshot can hold the only copy of a database. It reports them on the console instead. The pre-update image tags are restored before that decision and the private `cg-rollback-*` tags are kept, so finishing the recovery by hand starts the restored source on its own build, not on the failed release's.
1077
-
1078
- **Reverse proxy configuration:**
1079
-
1080
- The gateway proxies `/app/:name/:portName/*` to the app containers. Two env vars control how the gateway reaches them:
1081
-
1082
- | Env var | Default | Description |
1083
- |---------|---------|-------------|
1084
- | `GATEWAY_BIND` | `127.0.0.1` | Gateway HTTP listen address. Overrides the `gateway.bind` config field when set. Defaults to localhost-only; set to `0.0.0.0` when a **containerized** reverse proxy (Caddy, nginx in Docker) needs to reach the gateway across container boundaries. A **host-network** proxy (Traefik on host) can keep the localhost default. |
1085
- | `DOCKER_HOST` | _(system default)_ | Docker socket/TCP address. When set to `tcp://host:port` (e.g. DinD), the gateway automatically uses the host extracted from `DOCKER_HOST` to proxy to app containers instead of `127.0.0.1`. |
1086
-
1087
- Example Caddyfile for apps behind Caddy in Docker:
1088
-
1089
- ```caddy
1090
- handle /app* {
1091
- reverse_proxy dev-server:10850
1092
- }
1093
- ```
1094
-
1095
- (`handle`, not `handle_path` — preserve the `/app` prefix so the gateway's router can match it.)
1096
-
1097
- See **[API.md — App Store section](./API.md#app-store-api)** for the full reference including `app.yaml` schema, `gateway_api` host-script bridge, and agent injection.
1098
-
1099
- ---
1100
-
1101
- ## File Structure
1102
-
1103
- ### Project
1104
-
1105
- ```
1106
- claude-gateway/
1107
- ├── Makefile ← make start / cli / mcp-install / release / pm2-* / system-*
1108
- ├── config.template.json ← config template (source of truth for migration)
1109
-
1110
- ├── src/ ← Gateway core (TypeScript, compiled to dist/)
1111
- │ ├── index.ts ← entrypoint — loads config, starts agents
1112
- │ ├── types.ts ← shared TypeScript types
1113
- │ ├── logger.ts ← structured logging with per-agent files
1114
- │ │
1115
- │ ├── agent/ ← Agent management
1116
- │ │ ├── runner.ts ← session pool manager (spawn/evict sessions)
1117
- │ │ ├── workspace-loader.ts ← assembles CLAUDE.md from workspace files + skills
1118
- │ │ └── context-isolation.ts ← context guard for session isolation
1119
- │ │
1120
- │ ├── session/ ← Session lifecycle
1121
- │ │ ├── process.ts ← single Claude subprocess per session
1122
- │ │ ├── store.ts ← persist/load conversation history (.jsonl)
1123
- │ │ └── compactor.ts ← summarise + compact old history
1124
- │ │
1125
- │ ├── telegram/ ← Telegram integration
1126
- │ │ ├── receiver.ts ← spawns TelegramReceiver subprocess per agent
1127
- │ │ └── markdown.ts ← markdown/HTML utilities
1128
- │ │
1129
- │ ├── api/ ← HTTP API
1130
- │ │ ├── gateway-router.ts ← HTTP server (/health, /status, /ui, /api)
1131
- │ │ ├── router.ts ← REST API router (sync + SSE streaming)
1132
- │ │ ├── auth.ts ← API key auth middleware (timing-safe)
1133
- │ │ └── cron-router.ts ← Cron API router (auth + agent-scoped access)
1134
- │ │
1135
- │ ├── config/ ← Configuration
1136
- │ │ ├── loader.ts ← load + validate config.json
1137
- │ │ ├── migrator.ts ← auto-migration for config schema changes
1138
- │ │ └── watcher.ts ← hot-reload config on file change
1139
- │ │
1140
- │ ├── cron/ ← Cron scheduling
1141
- │ │ ├── manager.ts ← persistent cron job manager (REST + agentTurn)
1142
- │ │ └── scheduler.ts ← heartbeat task scheduler
1143
- │ │
1144
- │ ├── heartbeat/ ← Proactive tasks
1145
- │ │ ├── parser.ts ← parse HEARTBEAT.md YAML
1146
- │ │ └── history.ts ← track scheduled task execution
1147
- │ │
1148
- │ ├── skills/ ← Agent skills system
1149
- │ │ ├── index.ts ← re-exports (parser, loader, invoker, watcher)
1150
- │ │ ├── parser.ts ← parse SKILL.md frontmatter + body
1151
- │ │ ├── loader.ts ← load skills from directories, build registry
1152
- │ │ ├── invoker.ts ← detect /skill-name in messages, inject context
1153
- │ │ └── watcher.ts ← hot-reload skills on file changes (chokidar)
1154
- │ │
1155
- │ ├── history/ ← Persistent chat history (Layer 2)
1156
- │ │ ├── db.ts ← SQLite WAL + FTS5 history DB (pruneOlderThan, listChats, search)
1157
- │ │ ├── cleanup.ts ← daily retention scheduler (scheduleCleanup, resolveRetentionDays)
1158
- │ │ ├── media-store.ts ← media file store with MIME allowlist and path traversal guard
1159
- │ │ └── types.ts ← HistoryMessage, ChatSummary, SessionSummary types
1160
- │ │
1161
- │ ├── memory/ ← Long-term memory
1162
- │ │ └── manager.ts ← memory persistence
1163
- │ │
1164
- │ ├── webhook/ ← Webhooks
1165
- │ │ └── manager.ts ← webhook event dispatch
1166
- │ │
1167
- │ └── ui/ ← Dashboard
1168
- │ └── web-ui.ts ← live HTML dashboard
1169
-
1170
- ├── scripts/
1171
- │ ├── gen-cli.ts ← generates src/cli/commands.generated.ts + CLI.md from the route registry
1172
- │ ├── mock-line-webhook.ts ← local LINE webhook simulator for dev testing
1173
- │ ├── release.sh ← interactive release (make release)
1174
- │ └── setup-claude-settings.js ← enables channelsEnabled in Claude Code
1175
-
1176
- └── mcp/ ← MCP server (runs in Bun, separate node_modules)
1177
- ├── package.json ← dependencies: grammy, @modelcontextprotocol/sdk
1178
- ├── server.ts ← MCP entry point — registers all tool modules
1179
- ├── types.ts ← ChannelModule / ToolModule interfaces
1180
- ├── channel-manager.ts ← module lifecycle (init, start, stop, restart)
1181
- ├── router.ts ← route resolution + channel context rendering
1182
-
1183
- └── tools/
1184
- ├── telegram/ ← Telegram channel module
1185
- │ ├── module.ts ← ChannelModule: telegram_reply, react, edit, download
1186
- │ ├── receiver-server.ts ← standalone receiver (polling mode, no MCP)
1187
- │ ├── pure.ts ← markdown → Telegram HTML conversion
1188
- │ ├── typing.ts ← typing indicator state
1189
- │ └── skills/
1190
- │ ├── access/SKILL.md ← /telegram:access skill
1191
- │ └── configure/SKILL.md ← /telegram:configure skill
1192
-
1193
- ├── cron/ ← Cron tool module
1194
- │ ├── module.ts ← ToolModule: cron_list, create, update, delete, run, get_runs
1195
- │ ├── client.ts ← HTTP client for gateway cron REST API
1196
- │ └── skills/
1197
- │ └── cron/SKILL.md ← /cron skill
1198
-
1199
- └── skills/ ← Skills tool module
1200
- ├── module.ts ← ToolModule: skill_create, skill_delete, skill_install
1201
- └── handlers.ts ← skill CRUD + URL install handlers
1202
- ```
1203
-
1204
- ### Runtime data (`~/.claude-gateway/`)
1205
-
1206
- ```
1207
- ~/.claude-gateway/
1208
- ├── config.json ← gateway config
1209
- ├── mcp-token.env ← connector secrets, mode 0600 (see `gateway.customConnectors`)
1210
- ├── logs/
1211
- │ ├── alfred.log
1212
- │ ├── alfred.log.1 ← rotated generation (see `gateway.logs`)
1213
- │ └── warrior.log
1214
- ├── shared-skills/ ← shared skills (synced to ~/.claude/skills/ on boot and on change)
1215
- │ └── <skill-name>/
1216
- │ └── SKILL.md ← skill definition (same format as agent skills)
1217
- └── agents/
1218
- └── alfred/
1219
- ├── .env ← bot token (auto-created by wizard)
1220
- ├── sessions/
1221
- │ └── <chat_id>.jsonl ← conversation history (SessionStore)
1222
- ├── history.db ← SQLite chat history (Layer 2 — survives /compact)
1223
- ├── history-cleanup.log ← cleanup run log (max 1 MB, auto-rotated)
1224
- ├── media/ ← uploaded media files (served via /api/v1/agents/:id/media/*)
1225
- └── workspace/
1226
- ├── CLAUDE.md ← auto-generated from workspace files, do not edit
1227
- ├── AGENTS.md ← agent identity, rules, capabilities
1228
- ├── IDENTITY.md ← name, emoji, avatar
1229
- ├── SOUL.md ← tone, personality, speaking style
1230
- ├── USER.md ← user profile and preferences
1231
- ├── MEMORY.md ← long-term memory (auto-appended)
1232
- ├── HEARTBEAT.md ← scheduled/proactive tasks
1233
- ├── skills/ ← agent-specific skills (hot-reloaded)
1234
- │ └── <skill-name>/
1235
- │ └── SKILL.md ← skill definition with frontmatter
1236
- ├── .sessions/ ← per-session MCP config
1237
- │ └── <session_id>/
1238
- │ └── mcp-config.json ← auto-generated MCP config for this session
1239
- ├── .telegram-state/
1240
- │ └── access.json ← Telegram allowlist and pairing state
1241
- └── .discord-state/
1242
- └── access.json ← Discord allowlist and pairing state
1243
- ```
1244
-
1245
- ---
1246
-
1247
- ## Heartbeat / Scheduled Tasks
1248
-
1249
- Define proactive tasks in `HEARTBEAT.md`:
1250
-
1251
- ```yaml
1252
- tasks:
1253
- - name: morning-brief
1254
- cron: "0 8 * * *"
1255
- prompt: "Give a brief morning summary."
1256
-
1257
- - name: check-in
1258
- interval: 6h
1259
- prompt: "Check if there are any reminders to send."
1260
- ```
1261
-
1262
- - `cron` — standard 5-field cron expression
1263
- - `interval` — shorthand: `30m`, `1h`, `6h`, `1d`, `1w`
1264
- - If the agent replies with `HEARTBEAT_OK` (case-insensitive), no message is sent to Telegram
1265
- - `rateLimitMinutes` in config suppresses tasks if a proactive message was already sent recently (default: 30 min)
1266
-
1267
- ---
1268
-
1269
- ## Agent Skills
1270
-
1271
- Skills are reusable capabilities defined as `SKILL.md` files with YAML frontmatter. They are injected into the agent's system prompt and can be invoked via `/skill-name` commands.
1272
-
1273
- ### Skill locations
1274
-
1275
- | Location | Scope | Description |
1276
- |----------|-------|-------------|
1277
- | `workspace/skills/<name>/SKILL.md` | Per-agent | Agent-specific skills |
1278
- | `~/.claude-gateway/shared-skills/<name>/SKILL.md` | All agents | Shared skills — synced to `~/.claude/skills/` at boot and on change |
1279
- | `mcp/tools/<channel>/skills/<name>/SKILL.md` | All agents | Built-in channel skills (e.g. `/telegram:access`) |
1280
-
1281
- ### SKILL.md format
1282
-
1283
- ```yaml
1284
- ---
1285
- name: my-skill
1286
- description: What this skill does
1287
- user_invocable: true # false = system-only, not shown to user
1288
- argument_description: "[args]" # optional, shown in /skill-name [args]
1289
- ---
1290
-
1291
- Skill instructions go here. Claude follows these instructions
1292
- when the user invokes /my-skill.
1293
- ```
1294
-
1295
- ### Runtime skill management
1296
-
1297
- Agents can manage skills at runtime via MCP tools:
1298
-
1299
- | Tool | Description |
1300
- |------|-------------|
1301
- | `skill_create` | Create a new skill in the workspace |
1302
- | `skill_delete` | Delete an existing skill |
1303
- | `skill_install` | Install a skill from a GitHub URL or raw URL |
1304
-
1305
- Skills are **hot-reloaded** — changes to skill files are detected automatically and the skill registry is updated without restarting the session.
1306
-
1307
- ### Shared skills sync
1308
-
1309
- Skills placed in `~/.claude-gateway/shared-skills/` are automatically synced to `~/.claude/skills/` — the user-level directory that Claude Code scans for every session:
1310
-
1311
- - **At boot** — gateway copies all shared skills before spawning any agent
1312
- - **On change** — any add, edit, or delete under `shared-skills/` triggers a re-sync
1313
- - **Cleanup** — each synced skill is tagged with a `.shared` marker file; if a skill is removed from `shared-skills/`, the marker is used to delete the stale copy from `~/.claude/skills/` automatically (user-installed skills without the marker are never touched)
1314
-
1315
- This means adding a skill to `shared-skills/` makes it available to **all agents** without per-agent setup or a gateway restart.
1316
-
1317
- ### Skill self-improvement
1318
-
1319
- Agents can **learn skills from their own work**. Telemetry is captured for every turn; when a turn does enough substantive work (default ≥ 5 tool calls) and the session goes idle, a lightweight background reviewer reads the transcript and decides whether a reusable skill should be **created or updated**. Written skills are **hot-reloaded** — usable in the next turn without a restart. Controlled by [`gateway.skillLearning`](#gatewayskilllearning) (enabled by default).
1320
-
1321
- - **Provenance guard** — the writer only ever creates new `origin: auto` skills or edits skills it previously authored. Hand-written / user skills are never overwritten.
1322
- - **Caps** — a per-day review cap and a maximum number of auto-skills bound the churn; a daily curator prunes the least-used auto-skills.
1323
- - **Audit diary** — every automatic write appends a line to `<workspace>/SKILLS_LEARNED.md` (always on, offline, immutable).
1324
- - **Notifications** — when `skillLearning.notify` is on (default), a short ping is fanned out to **every channel the agent has configured** (Telegram, Discord, and LINE when set up). Each channel resolves recipients from its own `.<channel>-state/access.json` allowlist. The web/`api` channel has no proactive push and is not notified. Bursts coalesce into a single digest.
1325
- - **Progressive disclosure** — auto-skill descriptions are truncated in the CLAUDE.md skill menu to keep per-turn context small; the full skill body still loads on invoke.
1326
-
1327
- Metrics are exposed via `GET /api/v1/agents/:agentId/skill-metrics` and the `skill_metrics` MCP tool (adoption funnel, cost-to-complete deltas, net-token ledger).
1328
-
1329
- ---
1330
-
1331
- ## Config Auto-Migration
1332
-
1333
- When the config schema changes (new fields added in `config.template.json`), the gateway automatically detects and migrates your `config.json`:
1334
-
1335
- - Preserves all existing values
1336
- - Adds missing fields with defaults from the template
1337
- - Migrates automatically on startup (no confirmation needed)
1338
- - Tracks schema version for future migrations
1339
-
1340
- ---
1341
-
1342
- ## Pairing New Users
1343
-
1344
- New agents default to `dmPolicy: "allowlist"` with the orthogonal `pairing`
1345
- toggle **on**, so pairing works out of the box — no setup needed.
1346
-
1347
- 1. Ask the user to DM the bot — they receive a 6-character pairing code
1348
- 2. Approve it:
1349
- ```bash
1350
- claude-gateway channels approve --agent alfred --channel discord --code abc123
1351
- ```
1352
- (use `--channel telegram` for Telegram; omit `--channel` on `channels pending` to check both)
1353
- 3. The bot confirms pairing within 5 seconds
1354
- 4. Lock down after everyone is paired (optional) — turn the pairing toggle off
1355
- so unknown senders are dropped silently (the base policy is already
1356
- `allowlist`):
1357
- ```
1358
- /gateway:discord-access dm-pairing off # Discord
1359
- /telegram:access pairing off # Telegram
1360
- ```
1361
-
1362
- `pairing` is an **orthogonal on/off toggle**, not a `dmPolicy` value: the base
1363
- policy stays `open` | `allowlist` | `disabled`, and pairing layers on top of
1364
- `allowlist`. A legacy `access.json` with `"dmPolicy": "pairing"` is migrated
1365
- automatically on read to `{ dmPolicy: "allowlist", pairing: true }`.
1366
-
1367
- To manage channels (add/remove Telegram or Discord) on an existing agent:
1368
- ```bash
1369
- claude-gateway agents update # choose "Connect/update a channel" or "Disconnect a channel"
1370
- ```
1371
-
1372
- ---
1373
-
1374
- ## Channel Conditions & Limitations
1375
-
1376
- Each channel gates inbound messages in two tiers — **DM/1:1** and **group** — and
1377
- each has platform-level conditions that must be met *before* the gateway ever
1378
- sees a message. If those aren't met the bot looks online but stays silent.
1379
-
1380
- | Channel | Scope | Message reaches the bot when… | Access gate | Answers in group when… |
1381
- |---------|-------|-------------------------------|-------------|------------------------|
1382
- | **Telegram** | DM | always (long-polling) | `dmPolicy` + `pairing` → `allowFrom` | — |
1383
- | | Group | bot is **Admin**, or **Privacy Mode is OFF** + re-added; otherwise only `/cmd`, @mentions, replies | `groupPolicy` + `groupAllowlist` | `requireMention` false, or @mentioned/replied |
1384
- | **Discord** | DM | **Message Content Intent** enabled | `dmPolicy` + `pairing` → `allowFrom` | — |
1385
- | | Guild | **Message Content Intent** + **View Channel** + **Read Message History** | `groupPolicy` + `guildAllowlist` (+ optional `channelAllowlist`/`roleAllowlist`) | `requireMention` false, or @mentioned/replied |
1386
- | **LINE** | 1:1 | webhook delivered (valid signature) | `dmPolicy` | — |
1387
- | | Group/Room | webhook delivered + bot is a member | `groupPolicy` + `groupAllowlist` | `requireMention` false, or **native** @mention |
1388
- | **WhatsApp (Baileys)** | DM | number is linked (QR/pairing-code device link) | `dmPolicy` + `pairing` → `dmAllowlist` (per account) | — |
1389
- | | Group | number is linked and is a member of the group | `groupPolicy` + `groupAllowlist` (per account) | `requireMention` false, or @mentioned |
1390
- | **WhatsApp (Cloud API)** | DM | webhook delivered (valid `X-Hub-Signature-256`) | `dmPolicy` + `pairing` → `dmAllowlist` | — (DM-only, no group concept) |
1391
- | **WeChat** | DM | iLink long-poll (`getupdates`) delivers a new message | `dmPolicy` + `pairing` → `dmAllowlist` | — (DM-only, no group concept) |
1392
-
1393
- **Telegram limits**
1394
- - Exactly one process may poll a bot token — a second poller causes `409 Conflict`.
1395
- - Bot **commands are DM-only**; in groups they're silently dropped.
1396
- - Group **Privacy Mode is ON by default** — see [Telegram Groups](#telegram-groups). Admin status bypasses it; a Privacy-Mode change only applies after the bot is removed and re-added.
1397
- - Pairing codes: DM knocks reply the code privately; group knocks post the code in the group (needs a message that actually reaches the bot, i.e. Admin/Privacy-off).
1398
-
1399
- **Discord limits**
1400
- - **MESSAGE CONTENT INTENT** is a privileged intent — without it message text arrives empty, so the bot can neither answer nor pair. Enable it in the Developer Portal.
1401
- - The bot needs channel permissions **View Channel**, **Read Message History**, **Send Messages** (+ **Create Public Threads** / **Send Messages in Threads** if `DISCORD_AUTO_THREAD=true`).
1402
- - `channelAllowlist` / `roleAllowlist` are backend-only filters (no web UI) applied after the guild gate.
1403
-
1404
- **LINE limits**
1405
- - Inbound arrives via the Express **webhook**, not polling; the signature is verified over the **exact raw bytes**. Front it with the bun CORS proxy (see `/tunnel`) — never point cloudflared straight at the gateway, or chunked bodies break the signature and webhooks are dropped.
1406
- - Handled inbound message types are **text, image, and file** (documents up to the 20 MB media cap). Sticker, video, audio, and location are ignored. LINE reports no MIME type for a file, so its extension is derived from the sender-supplied name and sanitized before use. A file the gateway cannot fetch (too large, empty, or a failed transfer) still reaches the agent — as a message that says the attachment is unavailable, rather than one that looks like a file waiting to be read.
1407
- - Group/room `requireMention` uses LINE's **native mention** only (`mention.mentionees[].isSelf`). Typing the bot's name as plain text does **not** count, and `@All` does **not** count as a bot mention. LINE attaches mentions to **text messages only**, so an image or file posted in a group cannot satisfy the gate — send media in a DM, or set `requireMention: false` for that agent.
1408
- - Delivery is **reply-token-first (free) → push fallback (metered)**. The single-use reply token lives only ~1 min; after that, replies consume the OA's monthly push quota.
1409
- - Max **5 message objects** per reply/push request (the gateway auto-chunks to fit).
1410
-
1411
- **WhatsApp limits**
1412
- - Two independent modes, configured separately: the **Baileys** device-link bridge (`whatsapp` config block) and the **Cloud API** (`whatsapp_cloud` config block).
1413
- - **Baileys** requires linking a device per number — QR code or a text pairing code, the same one-time handshake as WhatsApp Web. Multi-account: an agent can hold several linked numbers at once (`whatsapp.accounts[]`), each with its own DM/group policy and allowlist.
1414
- - **Cloud API is DM-only** — a WhatsApp Business number has no group concept, so there's no `groupPolicy`/`groupAllowlist`/`requireMention` for it.
1415
- - **Cloud API's inbound webhook requires a valid `X-Hub-Signature-256`** (HMAC-SHA256 of the raw body against `appSecret`); a bad or missing signature is rejected with `401` before the payload is parsed.
1416
- - **Cloud API's 24-hour customer-service window**: free-form text replies only work within 24h of the user's last inbound message; outside that window only a pre-approved message template can reach them, and template sending is off by default (`templatesEnabled: false`) since it's the one send that can reach a user outside that window.
1417
- - Inbound media cap is **20 MB** on both modes (same `MediaStore` cap LINE's file uploads use).
1418
-
1419
- **WeChat limits**
1420
- - Single personal account per agent, linked via **QR code only** (no pairing-code option, no multi-account support in v1) through Tencent's own iLink Bot API bridge (`wechat` config block) — Tencent's self-serve product, not a third-party bridge.
1421
- - **DM-only** — no `groupPolicy`/`groupAllowlist`/`requireMention` fields exist because the iLink bridge cannot reliably deliver WeChat group events.
1422
- - Inbound delivery is **long-polling** (`getupdates`, 35s timeout), not a webhook — there is no `/webhooks/wechat/...` route.
1423
- - Outbound text is capped at **4000 characters** per message (iLink's documented limit); longer replies are auto-chunked on line boundaries with a short delay between chunks.
1424
- - The whole channel can be disabled without a redeploy via `WECHAT_CHANNEL_DISABLED=true` (opt-out, enabled by default) — see [WeChat Channel API](API.md#wechat-channel-api).
1425
-
1426
- ---
1427
-
1428
- ## Telegram Groups
1429
-
1430
- The bot can respond in Telegram groups and supergroups. A group must be in the
1431
- agent's `groupAllowlist` before the bot will answer there.
1432
-
1433
- ### Delivery gotcha: Privacy Mode (read this first)
1434
-
1435
- Telegram bots ship with **Privacy Mode ON** (`getMe` returns
1436
- `can_read_all_group_messages: false`). A privacy-mode bot only *receives*, inside
1437
- a group:
1438
-
1439
- - messages that start with `/` (commands),
1440
- - messages that @mention the bot's username, and
1441
- - replies to the bot's own messages.
1442
-
1443
- Everything else is filtered by Telegram **before it reaches the gateway** — the
1444
- bot looks online but never sees the message, so it can neither answer nor mint a
1445
- pairing code. On top of that, bot commands (`/start`, `/status`, …) are
1446
- **DM-only**: the receiver silently drops them in groups so pairing codes can't
1447
- leak to other members. Net effect in a default-privacy group: a plain message is
1448
- invisible and a command is dropped, so nothing happens.
1449
-
1450
- Do one of these so the bot actually receives group messages:
1451
-
1452
- - **Promote the bot to Admin in the group (easiest).** An admin bot receives
1453
- every message regardless of Privacy Mode — no BotFather change, no re-add. Any
1454
- admin role works, even the most restricted.
1455
- - **Disable Privacy Mode**, then **remove and re-add the bot** to the group (the
1456
- new setting only applies on re-join): [@BotFather](https://t.me/BotFather) →
1457
- `/setprivacy` → pick the bot → **Disable**.
1458
-
1459
- ### Register the group
1460
-
1461
- Once the bot can receive group messages, add the group to `groupAllowlist` one of
1462
- two ways.
1463
-
1464
- **Option A — pairing code (recommended).** With `groupPolicy: "allowlist"` and
1465
- `pairing: true` (both defaults), send any message in the group. The bot replies
1466
- with a 6-character code. Approve it from a gateway agent session:
1467
-
1468
- ```
1469
- /telegram:access pair <code>
1470
- ```
1471
-
1472
- That adds the group id to `groupAllowlist` (the code also lands in the agent's
1473
- `pending` as a `"kind": "group"` entry).
1474
-
1475
- **Option B — edit `access.json` directly.** Get the group id by forwarding any
1476
- group message to [@userinfobot](https://t.me/userinfobot) — a negative number
1477
- like `-1001234567890` — then edit:
1478
-
1479
- ```
1480
- ~/.claude-gateway/agents/<your-agent-id>/workspace/.telegram-state/access.json
1481
- ```
1482
-
1483
- ```json
1484
- {
1485
- "dmPolicy": "allowlist",
1486
- "pairing": true,
1487
- "allowFrom": ["..."],
1488
- "groupPolicy": "allowlist",
1489
- "groupAllowlist": ["-1001234567890"],
1490
- "requireMention": true
1491
- }
1492
- ```
1493
-
1494
- `access.json` is re-read on every inbound message — changes take effect
1495
- immediately, no restart.
1496
-
1497
- ### Mention gate
1498
-
1499
- `requireMention` is a single top-level boolean (default `true`):
1500
-
1501
- - `true` — the bot answers in an allowlisted group only when @mentioned or
1502
- replied to. This relies on Telegram delivering the @mention; if the bot ignores
1503
- mentions, make it an Admin (see above).
1504
- - `false` — the bot answers **every** message in an allowlisted group. This only
1505
- does anything if the bot can *see* every message, i.e. you also promoted it to
1506
- Admin or disabled Privacy Mode.
1507
-
1508
- Toggle it with `/telegram:access group mention <on|off>`.
1509
-
1510
- > **Legacy schema note:** older docs showed a per-group `"groups": { "<id>": {…} }`
1511
- > map. That form is still accepted and auto-migrated on read to the flat
1512
- > `groupAllowlist` + top-level `requireMention` shown above, but new setups should
1513
- > use the flat schema. A per-group member restriction from the old schema is
1514
- > preserved under `legacyGroupAllowFrom`; there is no command to edit it.
1515
-
1516
- ---
1517
-
1518
- ## Telegram Commands
1519
-
1520
- Bot commands are **DM-only** — sent in a group they are silently ignored (this
1521
- keeps pairing codes and session state from leaking to other members). Once
1522
- paired, the following commands are available in a private chat:
1523
-
1524
- **Session management**
1525
-
1526
- | Command | Description |
1527
- |---------|-------------|
1528
- | `/session` | Show current session info (name, message count, context %) |
1529
- | `/sessions` | List all sessions with inline keyboard — switch or delete |
1530
- | `/new <name>` | Create a new session, optionally with a name |
1531
- | `/rename <name>` | Rename the current session |
1532
- | `/clear` | Clear current session history (with confirmation) |
1533
- | `/compact` | Summarise old history and keep only recent messages |
1534
- | `/stop` | Interrupt the in-flight turn (gateway sends SIGINT to the subprocess) |
1535
- | `/restart` | Graceful session restart — shows a confirmation button; confirms and notifies when the session is back online |
1536
-
1537
- **Agent**
1538
-
1539
- | Command | Description |
1540
- |---------|-------------|
1541
- | `/model` | Show the current AI model. On Discord and LINE, `/model <id or alias>` also switches to any model in the list — an id the list does not contain is refused rather than written into `config.json`. **Direct messages only**: switching rewrites `config.json` for the whole agent and restarts every session in every chat, and the group access gates check the guild, channel and mention but never the user. Listing is unrestricted |
1542
- | `/models` | Switch AI model. On Telegram this is an inline keyboard; selecting a model triggers a graceful restart and notifies when back online, and **Dismiss** closes the picker without changing the model. Discord and LINE have no inline keyboard, so they get the same list as text plus `/model <id or alias>` to pick from it |
1543
-
1544
- The list behind both commands is the live catalog from `{ANTHROPIC_BASE_URL}/v1/models` when a base URL is configured, falling back to `gateway.models` in `config.json` — see [GET /api/v1/models](API.md#get-apiv1models). Before this, `config.json`'s list was written once at provisioning and never re-read, so a catalog that changed upstream could never reach the picker.
1545
-
1546
- **Account**
1547
-
1548
- | Command | Description |
1549
- |---------|-------------|
1550
- | `/start` | Pairing instructions |
1551
- | `/status` | Check your pairing state |
1552
- | `/help` | Show available commands |
1553
-
1554
- ---
1555
-
1556
- ## Monitoring
1557
-
1558
- The gateway runs an HTTP server on port 10850 (set `PORT` env var to change, `GATEWAY_BIND` to set the bind address):
1559
-
1560
- | Endpoint | Description |
1561
- |----------|-------------|
1562
- | `GET /health` | All agent IDs and running status |
1563
- | `GET /status` | JSON stats per agent (sessions, uptime) |
1564
- | `GET /ui` | Live HTML dashboard (auto-refreshes every 5s) |
1565
- | `POST /api/v1/agents/:id/messages` | Send a message to an agent (requires API key) |
1566
- | `GET /api/v1/agents` | List accessible agents (requires API key) |
1567
- | `/api/v1/crons/*` | Cron job management — see [API.md](./API.md) |
1568
-
1569
- ---
1570
-
1571
- ## Development
1572
-
1573
- ```bash
1574
- # Build TypeScript
1575
- npm run build
1576
-
1577
- # Unit tests only (fast, no external deps)
1578
- npm run test:unit
1579
-
1580
- # Integration tests
1581
- npm run integration
1582
-
1583
- # All tests
1584
- npm test
1585
-
1586
- # Type check without building
1587
- npm run typecheck
1588
- ```
1589
-
1590
- ### Writing tests that wait
1591
-
1592
- Two rules, enforced by `tests/unit/test-timing-hygiene.test.ts`:
1593
-
1594
- - **Wait for a signal, never for a duration.** `createWatcher()` / `watchWorkspace()` /
1595
- `watchSkills()` return a handle with a `ready` promise; the PTY wrapper announces itself
1596
- with a `system/init` event. Sleeping "long enough" instead is a bet on how fast the machine
1597
- is — and chokidar runs with `ignoreInitial: true`, so a write that lands before its initial
1598
- scan finishes emits *nothing* and the test waits out its whole deadline for an event that
1599
- will never arrive.
1600
- - **Poll with the shared helper**, `tests/helpers/wait-for.ts`, rather than a local copy. Its
1601
- timeout is a safety net sized so only a broken build hits it, and on a timeout it reports
1602
- the predicate it was waiting on instead of a bare "timeout exceeded".
1603
-
1604
- A fixed sleep is still fine for asserting that something *doesn't* happen — there the sleep
1605
- only bounds how hard the test looks, so a slow machine can't turn correct behaviour red.
1606
-
1607
- ---
1608
-
1609
- ## Troubleshooting
1610
-
1611
- **Agent fails to start**
1612
- - Check workspace path exists and contains `AGENTS.md`
1613
- - Check logs in `~/.claude-gateway/logs/<id>.log`
1614
-
1615
- **Agent not responding to messages**
1616
- - Verify `dmPolicy` in `access.json` — if `allowlist`, check the user's ID is in `allowFrom`
1617
- - Ensure no other process is polling the same bot token (causes 409 Conflict)
1618
- - Only `TelegramReceiver` polls Telegram — MCP session subprocesses run in `SEND_ONLY` mode (no polling)
1619
-
1620
- **Bot silent in a Telegram group**
1621
- - The group must be in `groupAllowlist` — see [Telegram Groups](#telegram-groups). An empty `pending` after messaging usually means the message never reached the bot.
1622
- - Most common cause: **Privacy Mode** (default ON). A non-admin bot only receives commands, @mentions, and replies in groups — a plain message needed to mint the pairing code is filtered by Telegram. Promote the bot to Admin, or disable Privacy Mode in BotFather and re-add it.
1623
- - `/start` and other commands are dropped in groups by design — use a normal message (or an @mention) to trigger the pairing code.
1624
- - If `requireMention: true`, the bot only answers when @mentioned or replied to.
1625
-
1626
- **Bot silent in a Discord server (guild)**
1627
- - Enable the **MESSAGE CONTENT INTENT** in the Discord Developer Portal (Bot settings) — without it the bot receives events but empty message text, so it can't respond or pair.
1628
- - The guild must be in `guildAllowlist` (`groupPolicy: allowlist`), and the bot needs **View Channel** + **Read Message History** in that channel.
1629
- - If `requireMention: true`, the bot only answers when @mentioned or replied to.
1630
-
1631
- **Session loses memory after restart**
1632
- - History is persisted in `~/.claude-gateway/agents/<id>/sessions/<chat_id>.jsonl`
1633
- - If the file is missing, the session starts fresh (no error)
1634
-
1635
- **Personality not applied**
1636
- - `CLAUDE.md` is auto-regenerated from workspace files on startup and on any file change
1637
- - Trigger a reload by saving any `.md` file in the workspace
1638
-
1639
- **Heartbeat not firing**
1640
- - Verify `HEARTBEAT.md` YAML is valid
1641
- - Check cron expression (5 fields: `min hour day month weekday`)
1642
- - Check rate limit — default 30 min between proactive messages
57
+ Follow the [quickstart](https://0xmaxma.github.io/claude-gateway/guide/quickstart.html), then [enable orchestration](https://0xmaxma.github.io/claude-gateway/guide/orchestration.html), [connect a channel](https://0xmaxma.github.io/claude-gateway/guide/channels.html), and optionally [configure voice](https://0xmaxma.github.io/claude-gateway/guide/voice.html).
1643
58
 
1644
- **API returns 403**
1645
- - Check the key value matches exactly (env var interpolation uses `${VAR}` syntax)
1646
- - Verify the key's `agents` list includes the target agent ID, or set `"agents": "*"`
59
+ ## Learn more
1647
60
 
1648
- **MCP tools not working (telegram_reply, cron_list, etc.)**
1649
- - Ensure `mcp/node_modules/` exists — run `make mcp-install` if not
1650
- - Check that `mcp-config.json` is generated in the session directory
1651
- - Verify Bun is installed (`bun --version`)
61
+ | Topic | Documentation |
62
+ | --- | --- |
63
+ | Configuration and credentials | [Configuration](https://0xmaxma.github.io/claude-gateway/reference/configuration.html) |
64
+ | Agents, workers, and task controls | [Orchestration](https://0xmaxma.github.io/claude-gateway/guide/orchestration.html) |
65
+ | Platform tokens and channel setup | [Channels](https://0xmaxma.github.io/claude-gateway/guide/channels.html) |
66
+ | STT, TTS, providers, and replay | [Voice](https://0xmaxma.github.io/claude-gateway/guide/voice.html) |
67
+ | Requests, responses, streams, and permissions | [API reference](https://0xmaxma.github.io/claude-gateway/api/) |
68
+ | Running, upgrading, and troubleshooting | [Operations](https://0xmaxma.github.io/claude-gateway/guide/operations.html) |
69
+ | Source builds and contributions | [Development](https://0xmaxma.github.io/claude-gateway/reference/development.html) |
1652
70
 
1653
- **Status messages not appearing in Telegram**
1654
- - First status update is sent after 5 seconds — very fast tasks may complete before it fires
1655
- - Check that the MCP server is running in `SEND_ONLY` mode for session subprocesses
1656
- - Verify the bot has permission to send messages in the chat
71
+ API documentation lives on the documentation website. [CLI command reference](https://0xmaxma.github.io/claude-gateway/reference/cli.html) is the generated command reference. To edit or preview this site, see [website development](website/README.md).