im-hub-pro 0.2.29

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 (384) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +497 -0
  3. package/README.zh-CN.md +496 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +921 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/core/acp-server.d.ts +8 -0
  9. package/dist/core/acp-server.d.ts.map +1 -0
  10. package/dist/core/acp-server.js +266 -0
  11. package/dist/core/acp-server.js.map +1 -0
  12. package/dist/core/agent-base.d.ts +94 -0
  13. package/dist/core/agent-base.d.ts.map +1 -0
  14. package/dist/core/agent-base.js +374 -0
  15. package/dist/core/agent-base.js.map +1 -0
  16. package/dist/core/agent-cwd.d.ts +45 -0
  17. package/dist/core/agent-cwd.d.ts.map +1 -0
  18. package/dist/core/agent-cwd.js +178 -0
  19. package/dist/core/agent-cwd.js.map +1 -0
  20. package/dist/core/agent-cwd.test.d.ts +2 -0
  21. package/dist/core/agent-cwd.test.d.ts.map +1 -0
  22. package/dist/core/agent-cwd.test.js +149 -0
  23. package/dist/core/agent-cwd.test.js.map +1 -0
  24. package/dist/core/approval-bus.d.ts +232 -0
  25. package/dist/core/approval-bus.d.ts.map +1 -0
  26. package/dist/core/approval-bus.js +703 -0
  27. package/dist/core/approval-bus.js.map +1 -0
  28. package/dist/core/approval-bus.synthetic.test.d.ts +2 -0
  29. package/dist/core/approval-bus.synthetic.test.d.ts.map +1 -0
  30. package/dist/core/approval-bus.synthetic.test.js +182 -0
  31. package/dist/core/approval-bus.synthetic.test.js.map +1 -0
  32. package/dist/core/approval-bus.test.d.ts +2 -0
  33. package/dist/core/approval-bus.test.d.ts.map +1 -0
  34. package/dist/core/approval-bus.test.js +537 -0
  35. package/dist/core/approval-bus.test.js.map +1 -0
  36. package/dist/core/approval-router.d.ts +95 -0
  37. package/dist/core/approval-router.d.ts.map +1 -0
  38. package/dist/core/approval-router.js +450 -0
  39. package/dist/core/approval-router.js.map +1 -0
  40. package/dist/core/approval-router.test.d.ts +2 -0
  41. package/dist/core/approval-router.test.d.ts.map +1 -0
  42. package/dist/core/approval-router.test.js +413 -0
  43. package/dist/core/approval-router.test.js.map +1 -0
  44. package/dist/core/audit-log.d.ts +55 -0
  45. package/dist/core/audit-log.d.ts.map +1 -0
  46. package/dist/core/audit-log.js +203 -0
  47. package/dist/core/audit-log.js.map +1 -0
  48. package/dist/core/bgjob-reader.d.ts +65 -0
  49. package/dist/core/bgjob-reader.d.ts.map +1 -0
  50. package/dist/core/bgjob-reader.js +212 -0
  51. package/dist/core/bgjob-reader.js.map +1 -0
  52. package/dist/core/bgjob-reader.test.d.ts +2 -0
  53. package/dist/core/bgjob-reader.test.d.ts.map +1 -0
  54. package/dist/core/bgjob-reader.test.js +178 -0
  55. package/dist/core/bgjob-reader.test.js.map +1 -0
  56. package/dist/core/circuit-breaker.d.ts +37 -0
  57. package/dist/core/circuit-breaker.d.ts.map +1 -0
  58. package/dist/core/circuit-breaker.js +115 -0
  59. package/dist/core/circuit-breaker.js.map +1 -0
  60. package/dist/core/commands/agent.d.ts +4 -0
  61. package/dist/core/commands/agent.d.ts.map +1 -0
  62. package/dist/core/commands/agent.js +21 -0
  63. package/dist/core/commands/agent.js.map +1 -0
  64. package/dist/core/commands/approval.d.ts +3 -0
  65. package/dist/core/commands/approval.d.ts.map +1 -0
  66. package/dist/core/commands/approval.js +44 -0
  67. package/dist/core/commands/approval.js.map +1 -0
  68. package/dist/core/commands/approval.test.d.ts +2 -0
  69. package/dist/core/commands/approval.test.d.ts.map +1 -0
  70. package/dist/core/commands/approval.test.js +85 -0
  71. package/dist/core/commands/approval.test.js.map +1 -0
  72. package/dist/core/commands/audit.d.ts +3 -0
  73. package/dist/core/commands/audit.d.ts.map +1 -0
  74. package/dist/core/commands/audit.js +84 -0
  75. package/dist/core/commands/audit.js.map +1 -0
  76. package/dist/core/commands/builtin.d.ts +3 -0
  77. package/dist/core/commands/builtin.d.ts.map +1 -0
  78. package/dist/core/commands/builtin.js +26 -0
  79. package/dist/core/commands/builtin.js.map +1 -0
  80. package/dist/core/commands/job.d.ts +3 -0
  81. package/dist/core/commands/job.d.ts.map +1 -0
  82. package/dist/core/commands/job.js +195 -0
  83. package/dist/core/commands/job.js.map +1 -0
  84. package/dist/core/commands/model.d.ts +9 -0
  85. package/dist/core/commands/model.d.ts.map +1 -0
  86. package/dist/core/commands/model.js +183 -0
  87. package/dist/core/commands/model.js.map +1 -0
  88. package/dist/core/commands/plan.d.ts +3 -0
  89. package/dist/core/commands/plan.d.ts.map +1 -0
  90. package/dist/core/commands/plan.js +75 -0
  91. package/dist/core/commands/plan.js.map +1 -0
  92. package/dist/core/commands/plan.test.d.ts +2 -0
  93. package/dist/core/commands/plan.test.d.ts.map +1 -0
  94. package/dist/core/commands/plan.test.js +122 -0
  95. package/dist/core/commands/plan.test.js.map +1 -0
  96. package/dist/core/commands/router.d.ts +3 -0
  97. package/dist/core/commands/router.d.ts.map +1 -0
  98. package/dist/core/commands/router.js +71 -0
  99. package/dist/core/commands/router.js.map +1 -0
  100. package/dist/core/commands/schedule.d.ts +3 -0
  101. package/dist/core/commands/schedule.d.ts.map +1 -0
  102. package/dist/core/commands/schedule.js +123 -0
  103. package/dist/core/commands/schedule.js.map +1 -0
  104. package/dist/core/commands/sessions.d.ts +3 -0
  105. package/dist/core/commands/sessions.d.ts.map +1 -0
  106. package/dist/core/commands/sessions.js +88 -0
  107. package/dist/core/commands/sessions.js.map +1 -0
  108. package/dist/core/commands/stats.d.ts +3 -0
  109. package/dist/core/commands/stats.d.ts.map +1 -0
  110. package/dist/core/commands/stats.js +73 -0
  111. package/dist/core/commands/stats.js.map +1 -0
  112. package/dist/core/commands/think.d.ts +3 -0
  113. package/dist/core/commands/think.d.ts.map +1 -0
  114. package/dist/core/commands/think.js +28 -0
  115. package/dist/core/commands/think.js.map +1 -0
  116. package/dist/core/commands/workspaces.d.ts +3 -0
  117. package/dist/core/commands/workspaces.d.ts.map +1 -0
  118. package/dist/core/commands/workspaces.js +47 -0
  119. package/dist/core/commands/workspaces.js.map +1 -0
  120. package/dist/core/config-schema.d.ts +58 -0
  121. package/dist/core/config-schema.d.ts.map +1 -0
  122. package/dist/core/config-schema.js +63 -0
  123. package/dist/core/config-schema.js.map +1 -0
  124. package/dist/core/cron.d.ts +29 -0
  125. package/dist/core/cron.d.ts.map +1 -0
  126. package/dist/core/cron.js +184 -0
  127. package/dist/core/cron.js.map +1 -0
  128. package/dist/core/event-bus.d.ts +80 -0
  129. package/dist/core/event-bus.d.ts.map +1 -0
  130. package/dist/core/event-bus.js +62 -0
  131. package/dist/core/event-bus.js.map +1 -0
  132. package/dist/core/intent-llm.d.ts +27 -0
  133. package/dist/core/intent-llm.d.ts.map +1 -0
  134. package/dist/core/intent-llm.js +170 -0
  135. package/dist/core/intent-llm.js.map +1 -0
  136. package/dist/core/intent.d.ts +12 -0
  137. package/dist/core/intent.d.ts.map +1 -0
  138. package/dist/core/intent.js +187 -0
  139. package/dist/core/intent.js.map +1 -0
  140. package/dist/core/job-board.d.ts +84 -0
  141. package/dist/core/job-board.d.ts.map +1 -0
  142. package/dist/core/job-board.js +379 -0
  143. package/dist/core/job-board.js.map +1 -0
  144. package/dist/core/logger.d.ts +6 -0
  145. package/dist/core/logger.d.ts.map +1 -0
  146. package/dist/core/logger.js +54 -0
  147. package/dist/core/logger.js.map +1 -0
  148. package/dist/core/metrics.d.ts +55 -0
  149. package/dist/core/metrics.d.ts.map +1 -0
  150. package/dist/core/metrics.js +291 -0
  151. package/dist/core/metrics.js.map +1 -0
  152. package/dist/core/onboarding.d.ts +94 -0
  153. package/dist/core/onboarding.d.ts.map +1 -0
  154. package/dist/core/onboarding.js +426 -0
  155. package/dist/core/onboarding.js.map +1 -0
  156. package/dist/core/onboarding.test.d.ts +2 -0
  157. package/dist/core/onboarding.test.d.ts.map +1 -0
  158. package/dist/core/onboarding.test.js +112 -0
  159. package/dist/core/onboarding.test.js.map +1 -0
  160. package/dist/core/rate-limiter.d.ts +44 -0
  161. package/dist/core/rate-limiter.d.ts.map +1 -0
  162. package/dist/core/rate-limiter.js +115 -0
  163. package/dist/core/rate-limiter.js.map +1 -0
  164. package/dist/core/registry.d.ts +32 -0
  165. package/dist/core/registry.d.ts.map +1 -0
  166. package/dist/core/registry.js +122 -0
  167. package/dist/core/registry.js.map +1 -0
  168. package/dist/core/router.d.ts +41 -0
  169. package/dist/core/router.d.ts.map +1 -0
  170. package/dist/core/router.js +431 -0
  171. package/dist/core/router.js.map +1 -0
  172. package/dist/core/schedule.d.ts +65 -0
  173. package/dist/core/schedule.d.ts.map +1 -0
  174. package/dist/core/schedule.js +316 -0
  175. package/dist/core/schedule.js.map +1 -0
  176. package/dist/core/session-subtasks.test.d.ts +2 -0
  177. package/dist/core/session-subtasks.test.d.ts.map +1 -0
  178. package/dist/core/session-subtasks.test.js +88 -0
  179. package/dist/core/session-subtasks.test.js.map +1 -0
  180. package/dist/core/session.d.ts +182 -0
  181. package/dist/core/session.d.ts.map +1 -0
  182. package/dist/core/session.js +774 -0
  183. package/dist/core/session.js.map +1 -0
  184. package/dist/core/sqlite-helper.d.ts +37 -0
  185. package/dist/core/sqlite-helper.d.ts.map +1 -0
  186. package/dist/core/sqlite-helper.js +79 -0
  187. package/dist/core/sqlite-helper.js.map +1 -0
  188. package/dist/core/transcribe.d.ts +25 -0
  189. package/dist/core/transcribe.d.ts.map +1 -0
  190. package/dist/core/transcribe.js +217 -0
  191. package/dist/core/transcribe.js.map +1 -0
  192. package/dist/core/transcribe.test.d.ts +2 -0
  193. package/dist/core/transcribe.test.d.ts.map +1 -0
  194. package/dist/core/transcribe.test.js +163 -0
  195. package/dist/core/transcribe.test.js.map +1 -0
  196. package/dist/core/types.d.ts +352 -0
  197. package/dist/core/types.d.ts.map +1 -0
  198. package/dist/core/types.js +3 -0
  199. package/dist/core/types.js.map +1 -0
  200. package/dist/core/workspace.d.ts +67 -0
  201. package/dist/core/workspace.d.ts.map +1 -0
  202. package/dist/core/workspace.js +113 -0
  203. package/dist/core/workspace.js.map +1 -0
  204. package/dist/index.d.ts +5 -0
  205. package/dist/index.d.ts.map +1 -0
  206. package/dist/index.js +6 -0
  207. package/dist/index.js.map +1 -0
  208. package/dist/plugins/agents/acp/acp-adapter.d.ts +16 -0
  209. package/dist/plugins/agents/acp/acp-adapter.d.ts.map +1 -0
  210. package/dist/plugins/agents/acp/acp-adapter.js +49 -0
  211. package/dist/plugins/agents/acp/acp-adapter.js.map +1 -0
  212. package/dist/plugins/agents/acp/acp-client.d.ts +32 -0
  213. package/dist/plugins/agents/acp/acp-client.d.ts.map +1 -0
  214. package/dist/plugins/agents/acp/acp-client.js +175 -0
  215. package/dist/plugins/agents/acp/acp-client.js.map +1 -0
  216. package/dist/plugins/agents/acp/discovery.d.ts +19 -0
  217. package/dist/plugins/agents/acp/discovery.d.ts.map +1 -0
  218. package/dist/plugins/agents/acp/discovery.js +109 -0
  219. package/dist/plugins/agents/acp/discovery.js.map +1 -0
  220. package/dist/plugins/agents/acp/index.d.ts +4 -0
  221. package/dist/plugins/agents/acp/index.d.ts.map +1 -0
  222. package/dist/plugins/agents/acp/index.js +4 -0
  223. package/dist/plugins/agents/acp/index.js.map +1 -0
  224. package/dist/plugins/agents/acp/types.d.ts +62 -0
  225. package/dist/plugins/agents/acp/types.d.ts.map +1 -0
  226. package/dist/plugins/agents/acp/types.js +5 -0
  227. package/dist/plugins/agents/acp/types.js.map +1 -0
  228. package/dist/plugins/agents/claude-code/adapter.test.d.ts +2 -0
  229. package/dist/plugins/agents/claude-code/adapter.test.d.ts.map +1 -0
  230. package/dist/plugins/agents/claude-code/adapter.test.js +195 -0
  231. package/dist/plugins/agents/claude-code/adapter.test.js.map +1 -0
  232. package/dist/plugins/agents/claude-code/index.d.ts +25 -0
  233. package/dist/plugins/agents/claude-code/index.d.ts.map +1 -0
  234. package/dist/plugins/agents/claude-code/index.js +184 -0
  235. package/dist/plugins/agents/claude-code/index.js.map +1 -0
  236. package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts +42 -0
  237. package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts.map +1 -0
  238. package/dist/plugins/agents/claude-code/mcp-approval-server.js +235 -0
  239. package/dist/plugins/agents/claude-code/mcp-approval-server.js.map +1 -0
  240. package/dist/plugins/agents/claude-code/mcp-approval-server.test.d.ts +2 -0
  241. package/dist/plugins/agents/claude-code/mcp-approval-server.test.d.ts.map +1 -0
  242. package/dist/plugins/agents/claude-code/mcp-approval-server.test.js +188 -0
  243. package/dist/plugins/agents/claude-code/mcp-approval-server.test.js.map +1 -0
  244. package/dist/plugins/agents/codex/adapter.test.d.ts +2 -0
  245. package/dist/plugins/agents/codex/adapter.test.d.ts.map +1 -0
  246. package/dist/plugins/agents/codex/adapter.test.js +192 -0
  247. package/dist/plugins/agents/codex/adapter.test.js.map +1 -0
  248. package/dist/plugins/agents/codex/index.d.ts +37 -0
  249. package/dist/plugins/agents/codex/index.d.ts.map +1 -0
  250. package/dist/plugins/agents/codex/index.js +254 -0
  251. package/dist/plugins/agents/codex/index.js.map +1 -0
  252. package/dist/plugins/agents/copilot/index.d.ts +35 -0
  253. package/dist/plugins/agents/copilot/index.d.ts.map +1 -0
  254. package/dist/plugins/agents/copilot/index.js +182 -0
  255. package/dist/plugins/agents/copilot/index.js.map +1 -0
  256. package/dist/plugins/agents/opencode/adapter.test.d.ts +2 -0
  257. package/dist/plugins/agents/opencode/adapter.test.d.ts.map +1 -0
  258. package/dist/plugins/agents/opencode/adapter.test.js +139 -0
  259. package/dist/plugins/agents/opencode/adapter.test.js.map +1 -0
  260. package/dist/plugins/agents/opencode/http-adapter.test.d.ts +2 -0
  261. package/dist/plugins/agents/opencode/http-adapter.test.d.ts.map +1 -0
  262. package/dist/plugins/agents/opencode/http-adapter.test.js +492 -0
  263. package/dist/plugins/agents/opencode/http-adapter.test.js.map +1 -0
  264. package/dist/plugins/agents/opencode/index.d.ts +5 -0
  265. package/dist/plugins/agents/opencode/index.d.ts.map +1 -0
  266. package/dist/plugins/agents/opencode/index.js +30 -0
  267. package/dist/plugins/agents/opencode/index.js.map +1 -0
  268. package/dist/plugins/agents/opencode/opencode-http-adapter.d.ts +138 -0
  269. package/dist/plugins/agents/opencode/opencode-http-adapter.d.ts.map +1 -0
  270. package/dist/plugins/agents/opencode/opencode-http-adapter.js +549 -0
  271. package/dist/plugins/agents/opencode/opencode-http-adapter.js.map +1 -0
  272. package/dist/plugins/agents/opencode/opencode-stdio-adapter.d.ts +24 -0
  273. package/dist/plugins/agents/opencode/opencode-stdio-adapter.d.ts.map +1 -0
  274. package/dist/plugins/agents/opencode/opencode-stdio-adapter.js +103 -0
  275. package/dist/plugins/agents/opencode/opencode-stdio-adapter.js.map +1 -0
  276. package/dist/plugins/agents/opencode/serve-manager.d.ts +27 -0
  277. package/dist/plugins/agents/opencode/serve-manager.d.ts.map +1 -0
  278. package/dist/plugins/agents/opencode/serve-manager.js +190 -0
  279. package/dist/plugins/agents/opencode/serve-manager.js.map +1 -0
  280. package/dist/plugins/messengers/discord/discord-adapter.d.ts +22 -0
  281. package/dist/plugins/messengers/discord/discord-adapter.d.ts.map +1 -0
  282. package/dist/plugins/messengers/discord/discord-adapter.js +241 -0
  283. package/dist/plugins/messengers/discord/discord-adapter.js.map +1 -0
  284. package/dist/plugins/messengers/discord/discord-adapter.test.d.ts +2 -0
  285. package/dist/plugins/messengers/discord/discord-adapter.test.d.ts.map +1 -0
  286. package/dist/plugins/messengers/discord/discord-adapter.test.js +332 -0
  287. package/dist/plugins/messengers/discord/discord-adapter.test.js.map +1 -0
  288. package/dist/plugins/messengers/discord/index.d.ts +4 -0
  289. package/dist/plugins/messengers/discord/index.d.ts.map +1 -0
  290. package/dist/plugins/messengers/discord/index.js +4 -0
  291. package/dist/plugins/messengers/discord/index.js.map +1 -0
  292. package/dist/plugins/messengers/discord/markdown-to-discord.d.ts +11 -0
  293. package/dist/plugins/messengers/discord/markdown-to-discord.d.ts.map +1 -0
  294. package/dist/plugins/messengers/discord/markdown-to-discord.js +59 -0
  295. package/dist/plugins/messengers/discord/markdown-to-discord.js.map +1 -0
  296. package/dist/plugins/messengers/discord/types.d.ts +9 -0
  297. package/dist/plugins/messengers/discord/types.d.ts.map +1 -0
  298. package/dist/plugins/messengers/discord/types.js +3 -0
  299. package/dist/plugins/messengers/discord/types.js.map +1 -0
  300. package/dist/plugins/messengers/feishu/card-builder.d.ts +23 -0
  301. package/dist/plugins/messengers/feishu/card-builder.d.ts.map +1 -0
  302. package/dist/plugins/messengers/feishu/card-builder.js +89 -0
  303. package/dist/plugins/messengers/feishu/card-builder.js.map +1 -0
  304. package/dist/plugins/messengers/feishu/feishu-adapter.d.ts +33 -0
  305. package/dist/plugins/messengers/feishu/feishu-adapter.d.ts.map +1 -0
  306. package/dist/plugins/messengers/feishu/feishu-adapter.js +195 -0
  307. package/dist/plugins/messengers/feishu/feishu-adapter.js.map +1 -0
  308. package/dist/plugins/messengers/feishu/feishu-client.d.ts +44 -0
  309. package/dist/plugins/messengers/feishu/feishu-client.d.ts.map +1 -0
  310. package/dist/plugins/messengers/feishu/feishu-client.js +120 -0
  311. package/dist/plugins/messengers/feishu/feishu-client.js.map +1 -0
  312. package/dist/plugins/messengers/feishu/feishu-dedup.test.d.ts +2 -0
  313. package/dist/plugins/messengers/feishu/feishu-dedup.test.d.ts.map +1 -0
  314. package/dist/plugins/messengers/feishu/feishu-dedup.test.js +70 -0
  315. package/dist/plugins/messengers/feishu/feishu-dedup.test.js.map +1 -0
  316. package/dist/plugins/messengers/feishu/index.d.ts +4 -0
  317. package/dist/plugins/messengers/feishu/index.d.ts.map +1 -0
  318. package/dist/plugins/messengers/feishu/index.js +4 -0
  319. package/dist/plugins/messengers/feishu/index.js.map +1 -0
  320. package/dist/plugins/messengers/feishu/types.d.ts +113 -0
  321. package/dist/plugins/messengers/feishu/types.d.ts.map +1 -0
  322. package/dist/plugins/messengers/feishu/types.js +4 -0
  323. package/dist/plugins/messengers/feishu/types.js.map +1 -0
  324. package/dist/plugins/messengers/telegram/index.d.ts +4 -0
  325. package/dist/plugins/messengers/telegram/index.d.ts.map +1 -0
  326. package/dist/plugins/messengers/telegram/index.js +4 -0
  327. package/dist/plugins/messengers/telegram/index.js.map +1 -0
  328. package/dist/plugins/messengers/telegram/markdown-to-html.d.ts +5 -0
  329. package/dist/plugins/messengers/telegram/markdown-to-html.d.ts.map +1 -0
  330. package/dist/plugins/messengers/telegram/markdown-to-html.js +186 -0
  331. package/dist/plugins/messengers/telegram/markdown-to-html.js.map +1 -0
  332. package/dist/plugins/messengers/telegram/media-download.d.ts +51 -0
  333. package/dist/plugins/messengers/telegram/media-download.d.ts.map +1 -0
  334. package/dist/plugins/messengers/telegram/media-download.js +224 -0
  335. package/dist/plugins/messengers/telegram/media-download.js.map +1 -0
  336. package/dist/plugins/messengers/telegram/media-download.test.d.ts +2 -0
  337. package/dist/plugins/messengers/telegram/media-download.test.d.ts.map +1 -0
  338. package/dist/plugins/messengers/telegram/media-download.test.js +125 -0
  339. package/dist/plugins/messengers/telegram/media-download.test.js.map +1 -0
  340. package/dist/plugins/messengers/telegram/telegram-adapter.d.ts +62 -0
  341. package/dist/plugins/messengers/telegram/telegram-adapter.d.ts.map +1 -0
  342. package/dist/plugins/messengers/telegram/telegram-adapter.js +653 -0
  343. package/dist/plugins/messengers/telegram/telegram-adapter.js.map +1 -0
  344. package/dist/plugins/messengers/telegram/types.d.ts +47 -0
  345. package/dist/plugins/messengers/telegram/types.d.ts.map +1 -0
  346. package/dist/plugins/messengers/telegram/types.js +3 -0
  347. package/dist/plugins/messengers/telegram/types.js.map +1 -0
  348. package/dist/plugins/messengers/wechat/ilink-adapter.d.ts +68 -0
  349. package/dist/plugins/messengers/wechat/ilink-adapter.d.ts.map +1 -0
  350. package/dist/plugins/messengers/wechat/ilink-adapter.js +483 -0
  351. package/dist/plugins/messengers/wechat/ilink-adapter.js.map +1 -0
  352. package/dist/plugins/messengers/wechat/ilink-client.d.ts +66 -0
  353. package/dist/plugins/messengers/wechat/ilink-client.d.ts.map +1 -0
  354. package/dist/plugins/messengers/wechat/ilink-client.js +288 -0
  355. package/dist/plugins/messengers/wechat/ilink-client.js.map +1 -0
  356. package/dist/plugins/messengers/wechat/ilink-types.d.ts +173 -0
  357. package/dist/plugins/messengers/wechat/ilink-types.d.ts.map +1 -0
  358. package/dist/plugins/messengers/wechat/ilink-types.js +12 -0
  359. package/dist/plugins/messengers/wechat/ilink-types.js.map +1 -0
  360. package/dist/utils/backoff.d.ts +35 -0
  361. package/dist/utils/backoff.d.ts.map +1 -0
  362. package/dist/utils/backoff.js +59 -0
  363. package/dist/utils/backoff.js.map +1 -0
  364. package/dist/utils/cross-platform.d.ts +26 -0
  365. package/dist/utils/cross-platform.d.ts.map +1 -0
  366. package/dist/utils/cross-platform.js +58 -0
  367. package/dist/utils/cross-platform.js.map +1 -0
  368. package/dist/utils/message-split.d.ts +14 -0
  369. package/dist/utils/message-split.d.ts.map +1 -0
  370. package/dist/utils/message-split.js +65 -0
  371. package/dist/utils/message-split.js.map +1 -0
  372. package/dist/utils/safe-equal.d.ts +2 -0
  373. package/dist/utils/safe-equal.d.ts.map +1 -0
  374. package/dist/utils/safe-equal.js +11 -0
  375. package/dist/utils/safe-equal.js.map +1 -0
  376. package/dist/web/public/_app.js +196 -0
  377. package/dist/web/public/index.html +935 -0
  378. package/dist/web/public/settings.html +1181 -0
  379. package/dist/web/public/tasks.html +1827 -0
  380. package/dist/web/server.d.ts +11 -0
  381. package/dist/web/server.d.ts.map +1 -0
  382. package/dist/web/server.js +1820 -0
  383. package/dist/web/server.js.map +1 -0
  384. package/package.json +73 -0
@@ -0,0 +1,703 @@
1
+ // approval-bus — IM 端人工审批的进程内总线
2
+ //
3
+ // 角色:在 im-hub 主进程里跑一个 unix socket 服务,等 claude 子进程的
4
+ // MCP "approval sidecar" 通过 socket 连进来发审批请求。bus 自己不决策,
5
+ // 只做三件事:
6
+ // 1. 把请求转给 notifier(由 messenger 层注入:负责推 IM 卡片)
7
+ // 2. 维护 pending 队列,按 threadId 索引,等 resolvePending 回流决策
8
+ // 3. 超时 / 进程退出 / 连接断开 时自动 deny,保证 sidecar 端不会卡死
9
+ //
10
+ // 协议:unix socket + NDJSON,每行一个 JSON 对象。
11
+ // sidecar → bus: {v:1, type:"approval", runId, reqId, toolName, input, toolUseId}
12
+ // bus → sidecar: {v:1, type:"decision", reqId, behavior:"allow"|"deny", ...}
13
+ //
14
+ // 单实例 export approvalBus;测试可 new ApprovalBus({approvalTimeoutMs}) 调小超时。
15
+ import { createServer } from 'net';
16
+ import { unlink, stat as fsStat, chmod } from 'fs/promises';
17
+ import { tmpdir } from 'os';
18
+ import { join } from 'path';
19
+ import { randomBytes } from 'crypto';
20
+ import { logger as rootLogger } from './logger.js';
21
+ import { eventBus } from './event-bus.js';
22
+ const log = rootLogger.child({ component: 'approval-bus' });
23
+ // Bumped from 5 min to 30 min so slower IM channels (e.g. Telegram, where
24
+ // notifications can land minutes after the bot.api.sendMessage logs success)
25
+ // have time to round-trip a y/n reply. Aligned with Claude's own 30 min hard
26
+ // timeout so we never outlive the underlying agent process.
27
+ const DEFAULT_APPROVAL_TIMEOUT_MS = parseEnvMs(process.env.IMHUB_APPROVAL_TIMEOUT_MS, 30 * 60 * 1000);
28
+ const DEFAULT_AUTO_ALLOW_GRACE_MS = 5 * 1000;
29
+ function parseEnvMs(raw, fallback) {
30
+ if (!raw)
31
+ return fallback;
32
+ const n = Number(raw);
33
+ if (!Number.isFinite(n) || n <= 0)
34
+ return fallback;
35
+ return n;
36
+ }
37
+ const MAX_LINE_BYTES = 256 * 1024;
38
+ const MAX_BUFFER_BYTES = MAX_LINE_BYTES * 4;
39
+ /** Length of the input prefix used to fingerprint an auto-allow rule.
40
+ * `(toolName, input-prefix)` is the dedup key.
41
+ *
42
+ * Bumped from 5 → 10 (M13). 5 collapsed `bash::git s` so the rule
43
+ * matched `git status` AND `git stash` AND `git submodule update` — far
44
+ * broader than users meant when they OK'd a single command. 10 keeps
45
+ * the three common families distinct (`git status` ≠ `git stash` ≠
46
+ * `git submo`) while still grouping benign variations of the same
47
+ * operation (e.g. `git status` ≈ `git status -s`).
48
+ *
49
+ * Why not longer? Most realistic commands are 6–14 chars, and pushing
50
+ * past 10 means even `git status` vs `git status -s` are treated as
51
+ * distinct, defeating the "approve once for variants" UX. 10 is the
52
+ * sweet spot per CR-2026-05-06. */
53
+ const AUTO_ALLOW_PREFIX_LEN = 10;
54
+ export class ApprovalBus {
55
+ server = null;
56
+ socketPath = null;
57
+ approvalTimeoutMs;
58
+ autoAllowGraceMs;
59
+ runContexts = new Map();
60
+ pendingById = new Map();
61
+ pendingByThread = new Map();
62
+ connections = new Set();
63
+ notifier = null;
64
+ resolutionListener = null;
65
+ /** threadId → set of `${toolName}::${prefix}` keys the user has marked
66
+ * as auto-allow within this conversation. Cleared by clearAutoAllowForThread
67
+ * (called from session.resetConversation) and on stop(). */
68
+ autoAllowByThread = new Map();
69
+ /**
70
+ * Lifetime counters surfaced via {@link getMetrics}. Help ops detect
71
+ * leaks (pending growing unbounded), spikes (totalRequests rate), and
72
+ * approval skew (deny:allow ratio). Reset on stop() so the gauge for a
73
+ * fresh process starts at zero.
74
+ */
75
+ metricsSnapshot = {
76
+ totalRequests: 0,
77
+ totalResolved: 0,
78
+ totalAllowed: 0,
79
+ totalDenied: 0,
80
+ totalTimedOut: 0,
81
+ };
82
+ constructor(opts = {}) {
83
+ this.approvalTimeoutMs = opts.approvalTimeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS;
84
+ this.autoAllowGraceMs = opts.autoAllowGraceMs ?? DEFAULT_AUTO_ALLOW_GRACE_MS;
85
+ }
86
+ /** 注入"通知 IM 推送"的回调。messenger 层启动时调一次。 */
87
+ setNotifier(n) {
88
+ this.notifier = n;
89
+ }
90
+ /** Subscribe to resolution events. Replaces any previous listener.
91
+ * approval-router uses this to keep its UI cards in sync with bus-side
92
+ * cancellations (timeout / sidecar disconnect / run terminated). The
93
+ * user-driven path (button or y/n text) already edits its own card; the
94
+ * listener still fires there with cause='user' so consumers can dedup. */
95
+ setResolutionListener(l) {
96
+ this.resolutionListener = l;
97
+ }
98
+ /** 启动 unix socket 服务。返回最终使用的 socket 路径。 */
99
+ async start(socketPath) {
100
+ if (this.server)
101
+ throw new Error('approval-bus already started');
102
+ const path = socketPath ?? defaultSocketPath();
103
+ try {
104
+ await unlink(path);
105
+ }
106
+ catch { /* stale socket cleanup */ }
107
+ return new Promise((resolve, reject) => {
108
+ const server = createServer((socket) => this.handleConnection(socket));
109
+ const onErr = (err) => { reject(err); };
110
+ server.once('error', onErr);
111
+ server.listen(path, () => {
112
+ server.removeListener('error', onErr);
113
+ this.server = server;
114
+ this.socketPath = path;
115
+ // Harden file permissions: net.Server.listen creates the socket file
116
+ // with the current umask (typically 0022 → 0644 / 0666). chmod 0o600
117
+ // ensures only the current user can connect, and the post-chmod stat
118
+ // surfaces a warning if (somehow) it's still loose — useful in shared
119
+ // hosts where umask was relaxed at some startup script.
120
+ void (async () => {
121
+ try {
122
+ await chmod(path, 0o600);
123
+ const st = await fsStat(path);
124
+ if (process.platform !== 'win32' && (st.mode & 0o077) !== 0) {
125
+ log.warn({
126
+ event: 'approval.bus.socket_perms_loose',
127
+ mode: (st.mode & 0o777).toString(8),
128
+ path,
129
+ }, 'Approval socket file is group/world accessible — set umask=0077 to harden');
130
+ }
131
+ }
132
+ catch { /* non-fatal */ }
133
+ })();
134
+ log.info({ event: 'approval.bus.started', path });
135
+ resolve(path);
136
+ });
137
+ });
138
+ }
139
+ async stop() {
140
+ // Reject everything still pending
141
+ for (const p of [...this.pendingById.values()]) {
142
+ this.cancelPending(p, { behavior: 'deny', message: 'approval-bus shutting down' }, 'shutdown');
143
+ }
144
+ this.runContexts.clear();
145
+ this.pendingById.clear();
146
+ this.pendingByThread.clear();
147
+ this.autoAllowByThread.clear();
148
+ // Reset lifetime counters so a restarted process starts at 0 — keeps
149
+ // `rate(im_hub_approval_requests_total)` correct without ops needing
150
+ // to track process restarts in the alert query.
151
+ this.metricsSnapshot = { totalRequests: 0, totalResolved: 0, totalAllowed: 0, totalDenied: 0, totalTimedOut: 0 };
152
+ // server.close() doesn't terminate existing connections. Half-close each
153
+ // (so the deny payload buffered above gets flushed) and fall back to
154
+ // destroy() after a short grace window if the peer doesn't disconnect.
155
+ const conns = [...this.connections];
156
+ this.connections.clear();
157
+ await Promise.all(conns.map((s) => new Promise((resolve) => {
158
+ if (s.destroyed) {
159
+ resolve();
160
+ return;
161
+ }
162
+ s.once('close', () => resolve());
163
+ try {
164
+ s.end();
165
+ }
166
+ catch { /* ignore */ }
167
+ setTimeout(() => { if (!s.destroyed)
168
+ s.destroy(); }, 200).unref();
169
+ })));
170
+ const srv = this.server;
171
+ if (!srv)
172
+ return;
173
+ await new Promise((resolve) => srv.close(() => resolve()));
174
+ this.server = null;
175
+ if (this.socketPath) {
176
+ try {
177
+ await unlink(this.socketPath);
178
+ }
179
+ catch { /* ignore */ }
180
+ this.socketPath = null;
181
+ }
182
+ }
183
+ registerRun(runId, ctx) {
184
+ this.runContexts.set(runId, ctx);
185
+ }
186
+ /** 进程结束时调。pending 全 deny,runContext 清掉。 */
187
+ unregisterRun(runId) {
188
+ this.runContexts.delete(runId);
189
+ for (const p of [...this.pendingById.values()]) {
190
+ if (p.runId === runId) {
191
+ this.cancelPending(p, { behavior: 'deny', message: 'run terminated' }, 'run-terminated');
192
+ }
193
+ }
194
+ }
195
+ hasPendingFor(threadId) {
196
+ return (this.pendingByThread.get(threadId)?.length ?? 0) > 0;
197
+ }
198
+ /** True iff a notifier has been installed (i.e. messenger layer has wired
199
+ * the bus into IM). Callers that have a fallback path (e.g. the opencode
200
+ * HTTP adapter) check this before registerSyntheticPending so they can
201
+ * short-circuit when the bus is dormant — mostly relevant in tests and in
202
+ * non-IM call paths (web, scheduler). */
203
+ hasNotifier() {
204
+ return this.notifier !== null;
205
+ }
206
+ /**
207
+ * 由 messenger.onMessage 拦截层调用。把 thread 队列头部的 pending 用
208
+ * 给定决策 resolve 掉。返回被 resolve 的 pending 描述(platform / tool /
209
+ * fingerprint / 是否处于 auto-allow grace 模式);router 用这些信息发回执。
210
+ * 没有 pending 时返回 null。
211
+ *
212
+ * Auto-allow side-effect: a user-initiated deny against a pending that
213
+ * was running in auto-allow mode revokes the matching rule (the user is
214
+ * signaling "stop auto-approving this"). Revocation is intentionally
215
+ * scoped to this user-path so sidecar disconnects / shutdown / run-
216
+ * terminated denies don't accidentally clear rules the user still wants.
217
+ */
218
+ resolvePending(threadId, decision) {
219
+ const q = this.pendingByThread.get(threadId);
220
+ const head = q?.[0];
221
+ if (!head)
222
+ return null;
223
+ const platform = this.runContexts.get(head.runId)?.platform ?? '';
224
+ const info = {
225
+ runId: head.runId,
226
+ threadId: head.threadId,
227
+ platform,
228
+ toolName: head.toolName,
229
+ fingerprint: head.fingerprint,
230
+ wasAutoAllow: head.autoAllow,
231
+ };
232
+ if (decision.behavior === 'deny' && head.autoAllow) {
233
+ this.removeAutoAllowRule(head.threadId, head.toolName, head.fingerprint);
234
+ }
235
+ this.cancelPending(head, decision, 'user');
236
+ return info;
237
+ }
238
+ /** Drop every auto-allow rule registered for this thread. Called from
239
+ * session.resetConversation so `/new` truly returns to "ask every time". */
240
+ clearAutoAllowForThread(threadId) {
241
+ this.autoAllowByThread.delete(threadId);
242
+ }
243
+ /** Test/diagnostic helper — current rule keys for a thread. */
244
+ getAutoAllowKeys(threadId) {
245
+ return [...(this.autoAllowByThread.get(threadId) ?? [])];
246
+ }
247
+ /** 测试用:当前 socket 路径。 */
248
+ getSocketPath() { return this.socketPath; }
249
+ /**
250
+ * Operational metrics snapshot used by /api/metrics (M14). `pending` is
251
+ * a live count; the totals are lifetime counters that monotonically
252
+ * increase until stop(). The three result buckets (allowed / denied /
253
+ * timedOut) are mutually exclusive and sum to totalResolved — see
254
+ * cancelPending for the bucketing rule. Cheap to call — no allocations
255
+ * beyond the returned object.
256
+ */
257
+ getMetrics() {
258
+ return {
259
+ pending: this.pendingById.size,
260
+ totalRequests: this.metricsSnapshot.totalRequests,
261
+ totalResolved: this.metricsSnapshot.totalResolved,
262
+ totalAllowed: this.metricsSnapshot.totalAllowed,
263
+ totalDenied: this.metricsSnapshot.totalDenied,
264
+ totalTimedOut: this.metricsSnapshot.totalTimedOut,
265
+ };
266
+ }
267
+ /**
268
+ * Snapshot of every currently-pending approval, sanitized for surface
269
+ * to the operator dashboard. Used by the web `/api/approvals` endpoint
270
+ * (and any future ops tooling). Returns a stable JSON shape — input
271
+ * is included verbatim so the UI can render the same preview the
272
+ * IM-side card would; sockets / dispatch closures / timer handles are
273
+ * intentionally omitted.
274
+ *
275
+ * Sorted oldest-first so the queue head is at index 0 (matches the
276
+ * head-of-thread queue semantics used by resolvePending).
277
+ */
278
+ listPending() {
279
+ const now = Date.now();
280
+ const out = [];
281
+ for (const p of this.pendingById.values()) {
282
+ if (p.resolved)
283
+ continue;
284
+ out.push({
285
+ reqId: p.reqId,
286
+ runId: p.runId,
287
+ threadId: p.threadId,
288
+ platform: p.platform,
289
+ toolName: p.toolName,
290
+ input: p.input,
291
+ fingerprint: p.fingerprint,
292
+ autoAllow: p.autoAllow,
293
+ registeredAt: p.registeredAt,
294
+ ageMs: now - p.registeredAt,
295
+ });
296
+ }
297
+ out.sort((a, b) => a.registeredAt - b.registeredAt);
298
+ return out;
299
+ }
300
+ // --- internals ---
301
+ handleConnection(socket) {
302
+ this.connections.add(socket);
303
+ let buf = '';
304
+ socket.setEncoding('utf8');
305
+ socket.on('data', (chunk) => {
306
+ buf += chunk;
307
+ if (buf.length > MAX_BUFFER_BYTES) {
308
+ log.warn({ event: 'approval.bus.buffer_overflow', bytes: buf.length });
309
+ // L10: best-effort signal to the sidecar BEFORE we close. The
310
+ // legacy `socket.destroy()` left the sidecar's pending request
311
+ // hanging on its own timeout (Claude's MCP layer typically
312
+ // 30 min). A wire-shaped 'fatal' line gives the sidecar a
313
+ // recognizable reason; if it doesn't parse the new type it still
314
+ // observes the socket close and bails through its disconnect
315
+ // handler — strictly better than the silent destroy.
316
+ try {
317
+ socket.write(JSON.stringify({ v: 1, type: 'fatal', reason: 'buffer overflow' }) + '\n');
318
+ socket.end();
319
+ }
320
+ catch {
321
+ socket.destroy();
322
+ }
323
+ return;
324
+ }
325
+ let nl;
326
+ while ((nl = buf.indexOf('\n')) !== -1) {
327
+ const line = buf.slice(0, nl);
328
+ buf = buf.slice(nl + 1);
329
+ if (line.length === 0)
330
+ continue;
331
+ if (line.length > MAX_LINE_BYTES) {
332
+ log.warn({ event: 'approval.bus.line_too_long', len: line.length });
333
+ continue;
334
+ }
335
+ this.handleLine(line, socket).catch((err) => {
336
+ log.error({ event: 'approval.bus.handle_error', err: String(err) });
337
+ });
338
+ }
339
+ });
340
+ socket.on('error', (err) => {
341
+ log.warn({ event: 'approval.bus.socket_error', err: String(err) });
342
+ });
343
+ socket.on('close', () => {
344
+ this.connections.delete(socket);
345
+ // sidecar 掉线:相关 pending 全 deny(claude 那边大概率也已经死了,写不写都无所谓)
346
+ for (const p of [...this.pendingById.values()]) {
347
+ if (p.socket === socket) {
348
+ this.cancelPending(p, { behavior: 'deny', message: 'sidecar disconnected' }, 'sidecar-disconnect');
349
+ }
350
+ }
351
+ });
352
+ }
353
+ async handleLine(line, socket) {
354
+ let msg;
355
+ try {
356
+ msg = JSON.parse(line);
357
+ }
358
+ catch {
359
+ log.warn({ event: 'approval.bus.bad_json', line: line.slice(0, 200) });
360
+ return;
361
+ }
362
+ if (!msg || typeof msg !== 'object') {
363
+ log.warn({ event: 'approval.bus.bad_msg' });
364
+ return;
365
+ }
366
+ const m = msg;
367
+ if (m.v !== 1) {
368
+ log.warn({ event: 'approval.bus.unsupported_version', v: m.v });
369
+ return;
370
+ }
371
+ if (m.type === 'approval') {
372
+ await this.handleApproval(m, socket);
373
+ return;
374
+ }
375
+ log.warn({ event: 'approval.bus.unknown_type', type: m.type });
376
+ }
377
+ async handleApproval(m, socket) {
378
+ const runId = typeof m.runId === 'string' ? m.runId : null;
379
+ const reqId = typeof m.reqId === 'string' ? m.reqId : null;
380
+ const toolName = typeof m.toolName === 'string' ? m.toolName : null;
381
+ const toolUseId = typeof m.toolUseId === 'string' ? m.toolUseId : '';
382
+ const input = (m.input && typeof m.input === 'object' && !Array.isArray(m.input))
383
+ ? m.input
384
+ : {};
385
+ if (!reqId) {
386
+ log.warn({ event: 'approval.bus.missing_reqId' });
387
+ return; // 没 reqId 没法回包,丢弃
388
+ }
389
+ if (!runId || !toolName) {
390
+ this.sendDecision(socket, reqId, { behavior: 'deny', message: 'invalid approval message' });
391
+ return;
392
+ }
393
+ const ctx = this.runContexts.get(runId);
394
+ if (!ctx) {
395
+ this.sendDecision(socket, reqId, { behavior: 'deny', message: `unknown runId: ${runId}` });
396
+ return;
397
+ }
398
+ if (this.pendingById.has(reqId)) {
399
+ // 重复 reqId(sidecar bug):拒绝新的,老的留着
400
+ this.sendDecision(socket, reqId, { behavior: 'deny', message: 'duplicate reqId' });
401
+ return;
402
+ }
403
+ if (!this.notifier) {
404
+ this.sendDecision(socket, reqId, { behavior: 'deny', message: 'no notifier installed' });
405
+ return;
406
+ }
407
+ await this._registerPending({
408
+ runId, reqId, toolName, toolUseId, input, ctx,
409
+ transport: { socket },
410
+ });
411
+ }
412
+ /**
413
+ * Register an approval request that did NOT come from the unix-socket
414
+ * sidecar. Used by the opencode HTTP bridge (P2): SSE event from opencode
415
+ * → bridge calls this with a `dispatch` callback that POSTs the decision
416
+ * back to opencode's REST API.
417
+ *
418
+ * Behavior is identical to the socket path — same notifier, same timeout,
419
+ * same auto-allow rules — just the delivery channel differs. The
420
+ * `dispatch` is invoked with the final Decision exactly once, on:
421
+ * - user reply via {@link resolvePending}
422
+ * - timeout (deny in normal mode, allow in auto-allow mode)
423
+ * - {@link unregisterRun} (deny: "run terminated")
424
+ * - {@link stop} (deny: "approval-bus shutting down")
425
+ *
426
+ * dispatch errors are logged and swallowed — the bus must not crash on
427
+ * a misbehaving callback.
428
+ *
429
+ * Idempotent on duplicate reqId: returns silently without firing notify
430
+ * (matches the socket path's "duplicate reqId" handling, minus the wire
431
+ * deny since the synthetic caller has no socket to deny on).
432
+ *
433
+ * Throws synchronously only when the caller's bus state is invalid (no
434
+ * notifier installed). The caller should avoid registering the synthetic
435
+ * pending in that case and fall back to its own deny path.
436
+ */
437
+ async registerSyntheticPending(input) {
438
+ if (!this.notifier) {
439
+ // Caller is responsible for handling this case — they have the only
440
+ // backchannel to whatever spawned the request (opencode HTTP, etc.).
441
+ throw new Error('no notifier installed');
442
+ }
443
+ if (this.pendingById.has(input.reqId)) {
444
+ log.warn({ event: 'approval.bus.duplicate_reqId', reqId: input.reqId, source: 'synthetic' });
445
+ return;
446
+ }
447
+ // Register run context lazily so callers don't have to pre-call
448
+ // registerRun for every prompt — synthetic pendings already carry full
449
+ // ctx in their argument.
450
+ if (!this.runContexts.has(input.runId)) {
451
+ this.runContexts.set(input.runId, input.ctx);
452
+ }
453
+ await this._registerPending({
454
+ runId: input.runId,
455
+ reqId: input.reqId,
456
+ toolName: input.toolName,
457
+ toolUseId: input.toolUseId ?? '',
458
+ input: input.input,
459
+ ctx: input.ctx,
460
+ transport: { dispatch: input.dispatch },
461
+ });
462
+ }
463
+ /**
464
+ * Shared register-and-notify pipeline used by both the socket path and the
465
+ * synthetic path. Builds the PendingApproval, wires the timer, fires the
466
+ * notifier, and ensures the timer is cleared if the notifier itself throws.
467
+ */
468
+ async _registerPending(args) {
469
+ const fingerprint = inputFingerprint(args.input);
470
+ const ruleKey = autoAllowRuleKey(args.toolName, fingerprint);
471
+ const isAutoAllow = this.autoAllowByThread.get(args.ctx.threadId)?.has(ruleKey) ?? false;
472
+ const timeoutMs = isAutoAllow ? this.autoAllowGraceMs : this.approvalTimeoutMs;
473
+ const transport = args.transport;
474
+ const pending = {
475
+ runId: args.runId,
476
+ reqId: args.reqId,
477
+ toolName: args.toolName,
478
+ threadId: args.ctx.threadId,
479
+ platform: args.ctx.platform,
480
+ fingerprint,
481
+ input: args.input,
482
+ registeredAt: Date.now(),
483
+ socket: 'socket' in transport ? transport.socket : undefined,
484
+ dispatch: 'dispatch' in transport ? transport.dispatch : undefined,
485
+ resolved: false,
486
+ autoAllow: isAutoAllow,
487
+ timer: setTimeout(() => {
488
+ if (isAutoAllow) {
489
+ this.cancelPending(pending, { behavior: 'allow' }, 'timeout');
490
+ }
491
+ else {
492
+ this.cancelPending(pending, { behavior: 'deny', message: 'approval timeout' }, 'timeout');
493
+ }
494
+ }, timeoutMs),
495
+ };
496
+ this.pendingById.set(args.reqId, pending);
497
+ const q = this.pendingByThread.get(args.ctx.threadId) ?? [];
498
+ q.push(pending);
499
+ this.pendingByThread.set(args.ctx.threadId, q);
500
+ this.metricsSnapshot.totalRequests++;
501
+ // Event-bus fan-out so the dashboard's /events SSE consumer can pop
502
+ // up an "approval pending" indicator without waiting on /api/approvals
503
+ // poll. publish() swallows listener errors — never breaks the bus.
504
+ eventBus.publish({
505
+ type: 'approval',
506
+ phase: 'requested',
507
+ reqId: args.reqId,
508
+ threadId: args.ctx.threadId,
509
+ platform: args.ctx.platform,
510
+ toolName: args.toolName,
511
+ });
512
+ log.info({
513
+ event: 'approval.bus.request',
514
+ runId: args.runId, reqId: args.reqId, toolName: args.toolName,
515
+ threadId: args.ctx.threadId, autoAllow: isAutoAllow,
516
+ transport: 'socket' in transport ? 'socket' : 'synthetic',
517
+ });
518
+ try {
519
+ await this.notifier({
520
+ runId: args.runId, reqId: args.reqId, toolName: args.toolName,
521
+ input: args.input, toolUseId: args.toolUseId, ctx: args.ctx,
522
+ ...(isAutoAllow ? { autoAllow: { graceMs: this.autoAllowGraceMs } } : {}),
523
+ });
524
+ }
525
+ catch (err) {
526
+ log.error({ event: 'approval.bus.notifier_error', reqId: args.reqId, err: String(err) });
527
+ this.cancelPending(pending, { behavior: 'deny', message: 'notifier error' }, 'notifier-error');
528
+ }
529
+ }
530
+ cancelPending(p, decision, cause = 'sidecar-disconnect') {
531
+ if (p.resolved)
532
+ return;
533
+ p.resolved = true;
534
+ clearTimeout(p.timer);
535
+ // M14: account for the resolution. Bucket into exactly ONE of
536
+ // allowed / denied / timedOut so the three sub-counters sum to
537
+ // totalResolved (Prometheus counters must monotonically increase;
538
+ // double-counting could push a derived "allow = resolved-deny-timeout"
539
+ // negative, which Prom interprets as a counter reset). Timeouts win
540
+ // over the wire decision so an auto-allow grace expiry counts as
541
+ // "timeout" (we said yes by default), distinct from "user said yes",
542
+ // and a normal timeout-deny counts as "timeout", not "deny".
543
+ this.metricsSnapshot.totalResolved++;
544
+ let outcome;
545
+ if (cause === 'timeout') {
546
+ this.metricsSnapshot.totalTimedOut++;
547
+ outcome = 'timeout';
548
+ }
549
+ else if (decision.behavior === 'deny') {
550
+ this.metricsSnapshot.totalDenied++;
551
+ outcome = 'deny';
552
+ }
553
+ else {
554
+ this.metricsSnapshot.totalAllowed++;
555
+ outcome = 'allow';
556
+ }
557
+ // Event-bus fan-out (dashboard SSE). Publish post-counter-update so a
558
+ // metrics-snapshot consumer that reads on a 'resolved' event sees the
559
+ // already-incremented counter.
560
+ eventBus.publish({
561
+ type: 'approval',
562
+ phase: 'resolved',
563
+ reqId: p.reqId,
564
+ threadId: p.threadId,
565
+ platform: p.platform,
566
+ toolName: p.toolName,
567
+ outcome,
568
+ });
569
+ // The "register on allow+all" side-effect lives here (covers any path
570
+ // through cancelPending — both resolvePending and the grace timer).
571
+ // The "revoke on user-deny" side-effect lives in resolvePending instead,
572
+ // so non-user denies (run terminated, sidecar disconnect, shutdown)
573
+ // don't accidentally clear rules the user still wants.
574
+ if (decision.behavior === 'allow' && decision.autoAllowFurther) {
575
+ this.addAutoAllowRule(p.threadId, p.toolName, p.fingerprint);
576
+ }
577
+ if (p.dispatch) {
578
+ // Synthetic path (opencode HTTP bridge). Caller owns the wire — they
579
+ // translate Decision → their backend's reply schema. Errors thrown
580
+ // here are isolated; the bus just logs.
581
+ try {
582
+ p.dispatch(decision);
583
+ }
584
+ catch (err) {
585
+ log.warn({ event: 'approval.bus.dispatch_error', reqId: p.reqId, err: String(err) });
586
+ }
587
+ }
588
+ else if (p.socket) {
589
+ this.sendDecision(p.socket, p.reqId, decision);
590
+ }
591
+ // (Neither set is impossible — _registerPending guarantees one of the two.)
592
+ this.removePending(p);
593
+ // Fire resolution listener last so subscribers see fully-cleaned state
594
+ // (pending already removed, rules already updated). Listener errors are
595
+ // isolated — the bus must not crash on a bad subscriber.
596
+ if (this.resolutionListener) {
597
+ try {
598
+ this.resolutionListener({
599
+ reqId: p.reqId,
600
+ runId: p.runId,
601
+ threadId: p.threadId,
602
+ platform: p.platform,
603
+ toolName: p.toolName,
604
+ fingerprint: p.fingerprint,
605
+ decision,
606
+ wasAutoAllow: p.autoAllow,
607
+ cause,
608
+ });
609
+ }
610
+ catch (err) {
611
+ log.warn({ event: 'approval.bus.listener_error', reqId: p.reqId, err: String(err) });
612
+ }
613
+ }
614
+ }
615
+ addAutoAllowRule(threadId, toolName, fingerprint) {
616
+ const key = autoAllowRuleKey(toolName, fingerprint);
617
+ let set = this.autoAllowByThread.get(threadId);
618
+ if (!set) {
619
+ set = new Set();
620
+ this.autoAllowByThread.set(threadId, set);
621
+ }
622
+ set.add(key);
623
+ log.info({ event: 'approval.bus.autoallow_added', threadId, toolName, fingerprint });
624
+ }
625
+ removeAutoAllowRule(threadId, toolName, fingerprint) {
626
+ const key = autoAllowRuleKey(toolName, fingerprint);
627
+ const set = this.autoAllowByThread.get(threadId);
628
+ if (!set)
629
+ return;
630
+ if (set.delete(key) && set.size === 0)
631
+ this.autoAllowByThread.delete(threadId);
632
+ log.info({ event: 'approval.bus.autoallow_removed', threadId, toolName, fingerprint });
633
+ }
634
+ removePending(p) {
635
+ this.pendingById.delete(p.reqId);
636
+ const q = this.pendingByThread.get(p.threadId);
637
+ if (!q)
638
+ return;
639
+ const idx = q.indexOf(p);
640
+ if (idx >= 0)
641
+ q.splice(idx, 1);
642
+ if (q.length === 0)
643
+ this.pendingByThread.delete(p.threadId);
644
+ }
645
+ sendDecision(socket, reqId, decision) {
646
+ if (!socket.writable)
647
+ return;
648
+ // Strip internal-only flags (e.g. autoAllowFurther) — sidecar only
649
+ // understands the wire schema.
650
+ const wire = { v: 1, type: 'decision', reqId, behavior: decision.behavior };
651
+ if (decision.behavior === 'allow' && decision.updatedInput) {
652
+ wire.updatedInput = decision.updatedInput;
653
+ }
654
+ else if (decision.behavior === 'deny' && decision.message) {
655
+ wire.message = decision.message;
656
+ }
657
+ const payload = JSON.stringify(wire) + '\n';
658
+ socket.write(payload, (err) => {
659
+ if (err)
660
+ log.warn({ event: 'approval.bus.write_failed', reqId, err: String(err) });
661
+ });
662
+ }
663
+ }
664
+ function autoAllowRuleKey(toolName, fingerprint) {
665
+ return `${toolName}::${fingerprint}`;
666
+ }
667
+ /**
668
+ * Pick a stable, short prefix of the input as the auto-allow fingerprint.
669
+ *
670
+ * Strategy: try the field most users mean when they say "this kind of
671
+ * call" for the common Claude tools (Bash → command, Write/Edit/Read →
672
+ * file_path, etc.); fall back to a stringified snapshot. Always truncated
673
+ * to AUTO_ALLOW_PREFIX_LEN so the rule covers small variations of the
674
+ * same operation but not unrelated ones.
675
+ */
676
+ function inputFingerprint(input) {
677
+ const FIELDS = ['command', 'file_path', 'path', 'url', 'pattern', 'query'];
678
+ for (const f of FIELDS) {
679
+ const v = input[f];
680
+ if (typeof v === 'string' && v.length > 0) {
681
+ return v.slice(0, AUTO_ALLOW_PREFIX_LEN);
682
+ }
683
+ }
684
+ let s;
685
+ try {
686
+ s = JSON.stringify(input);
687
+ }
688
+ catch {
689
+ s = '';
690
+ }
691
+ return s.slice(0, AUTO_ALLOW_PREFIX_LEN);
692
+ }
693
+ function defaultSocketPath() {
694
+ // 16 random bytes (32 hex chars) — defeats path prediction attacks that the
695
+ // earlier `${pid}-${Date.now().toString(36)}` form was vulnerable to in
696
+ // multi-tenant containers where pid is often 1 and the timestamp window
697
+ // is small. With 128 bits of entropy a TOCTOU pre-occupy attempt is
698
+ // statistically infeasible.
699
+ return join(tmpdir(), `imhub-approval-${randomBytes(16).toString('hex')}.sock`);
700
+ }
701
+ /** 进程级单例。im-hub 启动时 await approvalBus.start() 一次。 */
702
+ export const approvalBus = new ApprovalBus();
703
+ //# sourceMappingURL=approval-bus.js.map