agim-cli 1.0.1

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 (453) hide show
  1. package/CHANGELOG.md +1234 -0
  2. package/LICENSE +21 -0
  3. package/README.md +422 -0
  4. package/README.zh-CN.md +414 -0
  5. package/dist/cli-ui/cmd-handlers.d.ts +11 -0
  6. package/dist/cli-ui/cmd-handlers.d.ts.map +1 -0
  7. package/dist/cli-ui/cmd-handlers.js +240 -0
  8. package/dist/cli-ui/cmd-handlers.js.map +1 -0
  9. package/dist/cli-ui/config-wizard.d.ts +3 -0
  10. package/dist/cli-ui/config-wizard.d.ts.map +1 -0
  11. package/dist/cli-ui/config-wizard.js +851 -0
  12. package/dist/cli-ui/config-wizard.js.map +1 -0
  13. package/dist/cli-ui/entry-menu.d.ts +28 -0
  14. package/dist/cli-ui/entry-menu.d.ts.map +1 -0
  15. package/dist/cli-ui/entry-menu.js +50 -0
  16. package/dist/cli-ui/entry-menu.js.map +1 -0
  17. package/dist/cli-ui/env-file.d.ts +35 -0
  18. package/dist/cli-ui/env-file.d.ts.map +1 -0
  19. package/dist/cli-ui/env-file.js +163 -0
  20. package/dist/cli-ui/env-file.js.map +1 -0
  21. package/dist/cli-ui/i18n.d.ts +204 -0
  22. package/dist/cli-ui/i18n.d.ts.map +1 -0
  23. package/dist/cli-ui/i18n.js +455 -0
  24. package/dist/cli-ui/i18n.js.map +1 -0
  25. package/dist/cli-ui/lang-picker.d.ts +10 -0
  26. package/dist/cli-ui/lang-picker.d.ts.map +1 -0
  27. package/dist/cli-ui/lang-picker.js +33 -0
  28. package/dist/cli-ui/lang-picker.js.map +1 -0
  29. package/dist/cli-ui/paths.d.ts +4 -0
  30. package/dist/cli-ui/paths.d.ts.map +1 -0
  31. package/dist/cli-ui/paths.js +11 -0
  32. package/dist/cli-ui/paths.js.map +1 -0
  33. package/dist/cli-ui/prompts.d.ts +65 -0
  34. package/dist/cli-ui/prompts.d.ts.map +1 -0
  35. package/dist/cli-ui/prompts.js +125 -0
  36. package/dist/cli-ui/prompts.js.map +1 -0
  37. package/dist/cli-ui/service.d.ts +41 -0
  38. package/dist/cli-ui/service.d.ts.map +1 -0
  39. package/dist/cli-ui/service.js +241 -0
  40. package/dist/cli-ui/service.js.map +1 -0
  41. package/dist/cli.d.ts +3 -0
  42. package/dist/cli.d.ts.map +1 -0
  43. package/dist/cli.js +1143 -0
  44. package/dist/cli.js.map +1 -0
  45. package/dist/core/acp-server.d.ts +8 -0
  46. package/dist/core/acp-server.d.ts.map +1 -0
  47. package/dist/core/acp-server.js +266 -0
  48. package/dist/core/acp-server.js.map +1 -0
  49. package/dist/core/agent-base.d.ts +94 -0
  50. package/dist/core/agent-base.d.ts.map +1 -0
  51. package/dist/core/agent-base.js +373 -0
  52. package/dist/core/agent-base.js.map +1 -0
  53. package/dist/core/agent-cwd.d.ts +48 -0
  54. package/dist/core/agent-cwd.d.ts.map +1 -0
  55. package/dist/core/agent-cwd.js +181 -0
  56. package/dist/core/agent-cwd.js.map +1 -0
  57. package/dist/core/agent-helper.d.ts +65 -0
  58. package/dist/core/agent-helper.d.ts.map +1 -0
  59. package/dist/core/agent-helper.js +150 -0
  60. package/dist/core/agent-helper.js.map +1 -0
  61. package/dist/core/agim-paths.d.ts +10 -0
  62. package/dist/core/agim-paths.d.ts.map +1 -0
  63. package/dist/core/agim-paths.js +64 -0
  64. package/dist/core/agim-paths.js.map +1 -0
  65. package/dist/core/approval-bus.d.ts +300 -0
  66. package/dist/core/approval-bus.d.ts.map +1 -0
  67. package/dist/core/approval-bus.js +990 -0
  68. package/dist/core/approval-bus.js.map +1 -0
  69. package/dist/core/approval-router.d.ts +101 -0
  70. package/dist/core/approval-router.d.ts.map +1 -0
  71. package/dist/core/approval-router.js +540 -0
  72. package/dist/core/approval-router.js.map +1 -0
  73. package/dist/core/audit-log.d.ts +55 -0
  74. package/dist/core/audit-log.d.ts.map +1 -0
  75. package/dist/core/audit-log.js +203 -0
  76. package/dist/core/audit-log.js.map +1 -0
  77. package/dist/core/bgjob-reader.d.ts +65 -0
  78. package/dist/core/bgjob-reader.d.ts.map +1 -0
  79. package/dist/core/bgjob-reader.js +212 -0
  80. package/dist/core/bgjob-reader.js.map +1 -0
  81. package/dist/core/circuit-breaker.d.ts +37 -0
  82. package/dist/core/circuit-breaker.d.ts.map +1 -0
  83. package/dist/core/circuit-breaker.js +115 -0
  84. package/dist/core/circuit-breaker.js.map +1 -0
  85. package/dist/core/commands/agent.d.ts +4 -0
  86. package/dist/core/commands/agent.d.ts.map +1 -0
  87. package/dist/core/commands/agent.js +40 -0
  88. package/dist/core/commands/agent.js.map +1 -0
  89. package/dist/core/commands/approval.d.ts +3 -0
  90. package/dist/core/commands/approval.d.ts.map +1 -0
  91. package/dist/core/commands/approval.js +85 -0
  92. package/dist/core/commands/approval.js.map +1 -0
  93. package/dist/core/commands/audit.d.ts +3 -0
  94. package/dist/core/commands/audit.d.ts.map +1 -0
  95. package/dist/core/commands/audit.js +84 -0
  96. package/dist/core/commands/audit.js.map +1 -0
  97. package/dist/core/commands/builtin.d.ts +3 -0
  98. package/dist/core/commands/builtin.d.ts.map +1 -0
  99. package/dist/core/commands/builtin.js +304 -0
  100. package/dist/core/commands/builtin.js.map +1 -0
  101. package/dist/core/commands/cron.d.ts +3 -0
  102. package/dist/core/commands/cron.d.ts.map +1 -0
  103. package/dist/core/commands/cron.js +128 -0
  104. package/dist/core/commands/cron.js.map +1 -0
  105. package/dist/core/commands/job.d.ts +3 -0
  106. package/dist/core/commands/job.d.ts.map +1 -0
  107. package/dist/core/commands/job.js +195 -0
  108. package/dist/core/commands/job.js.map +1 -0
  109. package/dist/core/commands/memo.d.ts +3 -0
  110. package/dist/core/commands/memo.d.ts.map +1 -0
  111. package/dist/core/commands/memo.js +151 -0
  112. package/dist/core/commands/memo.js.map +1 -0
  113. package/dist/core/commands/model.d.ts +9 -0
  114. package/dist/core/commands/model.d.ts.map +1 -0
  115. package/dist/core/commands/model.js +183 -0
  116. package/dist/core/commands/model.js.map +1 -0
  117. package/dist/core/commands/plan.d.ts +3 -0
  118. package/dist/core/commands/plan.d.ts.map +1 -0
  119. package/dist/core/commands/plan.js +75 -0
  120. package/dist/core/commands/plan.js.map +1 -0
  121. package/dist/core/commands/remind.d.ts +3 -0
  122. package/dist/core/commands/remind.d.ts.map +1 -0
  123. package/dist/core/commands/remind.js +271 -0
  124. package/dist/core/commands/remind.js.map +1 -0
  125. package/dist/core/commands/router.d.ts +3 -0
  126. package/dist/core/commands/router.d.ts.map +1 -0
  127. package/dist/core/commands/router.js +71 -0
  128. package/dist/core/commands/router.js.map +1 -0
  129. package/dist/core/commands/sessions.d.ts +3 -0
  130. package/dist/core/commands/sessions.d.ts.map +1 -0
  131. package/dist/core/commands/sessions.js +88 -0
  132. package/dist/core/commands/sessions.js.map +1 -0
  133. package/dist/core/commands/stats.d.ts +3 -0
  134. package/dist/core/commands/stats.d.ts.map +1 -0
  135. package/dist/core/commands/stats.js +73 -0
  136. package/dist/core/commands/stats.js.map +1 -0
  137. package/dist/core/commands/think.d.ts +3 -0
  138. package/dist/core/commands/think.d.ts.map +1 -0
  139. package/dist/core/commands/think.js +28 -0
  140. package/dist/core/commands/think.js.map +1 -0
  141. package/dist/core/commands/workspaces.d.ts +3 -0
  142. package/dist/core/commands/workspaces.d.ts.map +1 -0
  143. package/dist/core/commands/workspaces.js +47 -0
  144. package/dist/core/commands/workspaces.js.map +1 -0
  145. package/dist/core/config-schema.d.ts +60 -0
  146. package/dist/core/config-schema.d.ts.map +1 -0
  147. package/dist/core/config-schema.js +75 -0
  148. package/dist/core/config-schema.js.map +1 -0
  149. package/dist/core/coord-systems.d.ts +65 -0
  150. package/dist/core/coord-systems.d.ts.map +1 -0
  151. package/dist/core/coord-systems.js +229 -0
  152. package/dist/core/coord-systems.js.map +1 -0
  153. package/dist/core/cron.d.ts +29 -0
  154. package/dist/core/cron.d.ts.map +1 -0
  155. package/dist/core/cron.js +184 -0
  156. package/dist/core/cron.js.map +1 -0
  157. package/dist/core/event-bus.d.ts +80 -0
  158. package/dist/core/event-bus.d.ts.map +1 -0
  159. package/dist/core/event-bus.js +62 -0
  160. package/dist/core/event-bus.js.map +1 -0
  161. package/dist/core/intent-llm.d.ts +27 -0
  162. package/dist/core/intent-llm.d.ts.map +1 -0
  163. package/dist/core/intent-llm.js +170 -0
  164. package/dist/core/intent-llm.js.map +1 -0
  165. package/dist/core/intent.d.ts +12 -0
  166. package/dist/core/intent.d.ts.map +1 -0
  167. package/dist/core/intent.js +187 -0
  168. package/dist/core/intent.js.map +1 -0
  169. package/dist/core/job-board.d.ts +82 -0
  170. package/dist/core/job-board.d.ts.map +1 -0
  171. package/dist/core/job-board.js +379 -0
  172. package/dist/core/job-board.js.map +1 -0
  173. package/dist/core/location-context.d.ts +32 -0
  174. package/dist/core/location-context.d.ts.map +1 -0
  175. package/dist/core/location-context.js +69 -0
  176. package/dist/core/location-context.js.map +1 -0
  177. package/dist/core/location-token.d.ts +57 -0
  178. package/dist/core/location-token.d.ts.map +1 -0
  179. package/dist/core/location-token.js +128 -0
  180. package/dist/core/location-token.js.map +1 -0
  181. package/dist/core/logger.d.ts +6 -0
  182. package/dist/core/logger.d.ts.map +1 -0
  183. package/dist/core/logger.js +54 -0
  184. package/dist/core/logger.js.map +1 -0
  185. package/dist/core/memo-rpc.d.ts +13 -0
  186. package/dist/core/memo-rpc.d.ts.map +1 -0
  187. package/dist/core/memo-rpc.js +288 -0
  188. package/dist/core/memo-rpc.js.map +1 -0
  189. package/dist/core/memos.d.ts +163 -0
  190. package/dist/core/memos.d.ts.map +1 -0
  191. package/dist/core/memos.js +502 -0
  192. package/dist/core/memos.js.map +1 -0
  193. package/dist/core/metrics.d.ts +55 -0
  194. package/dist/core/metrics.d.ts.map +1 -0
  195. package/dist/core/metrics.js +291 -0
  196. package/dist/core/metrics.js.map +1 -0
  197. package/dist/core/onboarding.d.ts +99 -0
  198. package/dist/core/onboarding.d.ts.map +1 -0
  199. package/dist/core/onboarding.js +426 -0
  200. package/dist/core/onboarding.js.map +1 -0
  201. package/dist/core/pending-reminder.d.ts +25 -0
  202. package/dist/core/pending-reminder.d.ts.map +1 -0
  203. package/dist/core/pending-reminder.js +53 -0
  204. package/dist/core/pending-reminder.js.map +1 -0
  205. package/dist/core/rate-limiter.d.ts +44 -0
  206. package/dist/core/rate-limiter.d.ts.map +1 -0
  207. package/dist/core/rate-limiter.js +115 -0
  208. package/dist/core/rate-limiter.js.map +1 -0
  209. package/dist/core/registry.d.ts +32 -0
  210. package/dist/core/registry.d.ts.map +1 -0
  211. package/dist/core/registry.js +126 -0
  212. package/dist/core/registry.js.map +1 -0
  213. package/dist/core/remind-intent.d.ts +25 -0
  214. package/dist/core/remind-intent.d.ts.map +1 -0
  215. package/dist/core/remind-intent.js +196 -0
  216. package/dist/core/remind-intent.js.map +1 -0
  217. package/dist/core/reminder-rpc.d.ts +17 -0
  218. package/dist/core/reminder-rpc.d.ts.map +1 -0
  219. package/dist/core/reminder-rpc.js +169 -0
  220. package/dist/core/reminder-rpc.js.map +1 -0
  221. package/dist/core/reminders.d.ts +159 -0
  222. package/dist/core/reminders.d.ts.map +1 -0
  223. package/dist/core/reminders.js +977 -0
  224. package/dist/core/reminders.js.map +1 -0
  225. package/dist/core/router.d.ts +55 -0
  226. package/dist/core/router.d.ts.map +1 -0
  227. package/dist/core/router.js +497 -0
  228. package/dist/core/router.js.map +1 -0
  229. package/dist/core/schedule.d.ts +65 -0
  230. package/dist/core/schedule.d.ts.map +1 -0
  231. package/dist/core/schedule.js +323 -0
  232. package/dist/core/schedule.js.map +1 -0
  233. package/dist/core/session.d.ts +182 -0
  234. package/dist/core/session.d.ts.map +1 -0
  235. package/dist/core/session.js +807 -0
  236. package/dist/core/session.js.map +1 -0
  237. package/dist/core/sqlite-helper.d.ts +37 -0
  238. package/dist/core/sqlite-helper.d.ts.map +1 -0
  239. package/dist/core/sqlite-helper.js +79 -0
  240. package/dist/core/sqlite-helper.js.map +1 -0
  241. package/dist/core/transcribe.d.ts +25 -0
  242. package/dist/core/transcribe.d.ts.map +1 -0
  243. package/dist/core/transcribe.js +217 -0
  244. package/dist/core/transcribe.js.map +1 -0
  245. package/dist/core/types.d.ts +360 -0
  246. package/dist/core/types.d.ts.map +1 -0
  247. package/dist/core/types.js +3 -0
  248. package/dist/core/types.js.map +1 -0
  249. package/dist/core/workspace.d.ts +67 -0
  250. package/dist/core/workspace.d.ts.map +1 -0
  251. package/dist/core/workspace.js +113 -0
  252. package/dist/core/workspace.js.map +1 -0
  253. package/dist/index.d.ts +5 -0
  254. package/dist/index.d.ts.map +1 -0
  255. package/dist/index.js +6 -0
  256. package/dist/index.js.map +1 -0
  257. package/dist/plugins/agents/acp/acp-adapter.d.ts +16 -0
  258. package/dist/plugins/agents/acp/acp-adapter.d.ts.map +1 -0
  259. package/dist/plugins/agents/acp/acp-adapter.js +49 -0
  260. package/dist/plugins/agents/acp/acp-adapter.js.map +1 -0
  261. package/dist/plugins/agents/acp/acp-client.d.ts +32 -0
  262. package/dist/plugins/agents/acp/acp-client.d.ts.map +1 -0
  263. package/dist/plugins/agents/acp/acp-client.js +177 -0
  264. package/dist/plugins/agents/acp/acp-client.js.map +1 -0
  265. package/dist/plugins/agents/acp/discovery.d.ts +19 -0
  266. package/dist/plugins/agents/acp/discovery.d.ts.map +1 -0
  267. package/dist/plugins/agents/acp/discovery.js +111 -0
  268. package/dist/plugins/agents/acp/discovery.js.map +1 -0
  269. package/dist/plugins/agents/acp/index.d.ts +4 -0
  270. package/dist/plugins/agents/acp/index.d.ts.map +1 -0
  271. package/dist/plugins/agents/acp/index.js +4 -0
  272. package/dist/plugins/agents/acp/index.js.map +1 -0
  273. package/dist/plugins/agents/acp/types.d.ts +62 -0
  274. package/dist/plugins/agents/acp/types.d.ts.map +1 -0
  275. package/dist/plugins/agents/acp/types.js +5 -0
  276. package/dist/plugins/agents/acp/types.js.map +1 -0
  277. package/dist/plugins/agents/claude-code/index.d.ts +25 -0
  278. package/dist/plugins/agents/claude-code/index.d.ts.map +1 -0
  279. package/dist/plugins/agents/claude-code/index.js +184 -0
  280. package/dist/plugins/agents/claude-code/index.js.map +1 -0
  281. package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts +59 -0
  282. package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts.map +1 -0
  283. package/dist/plugins/agents/claude-code/mcp-approval-server.js +645 -0
  284. package/dist/plugins/agents/claude-code/mcp-approval-server.js.map +1 -0
  285. package/dist/plugins/agents/codex/build-mcp-cli-args.d.ts +28 -0
  286. package/dist/plugins/agents/codex/build-mcp-cli-args.d.ts.map +1 -0
  287. package/dist/plugins/agents/codex/build-mcp-cli-args.js +74 -0
  288. package/dist/plugins/agents/codex/build-mcp-cli-args.js.map +1 -0
  289. package/dist/plugins/agents/codex/index.d.ts +53 -0
  290. package/dist/plugins/agents/codex/index.d.ts.map +1 -0
  291. package/dist/plugins/agents/codex/index.js +341 -0
  292. package/dist/plugins/agents/codex/index.js.map +1 -0
  293. package/dist/plugins/agents/copilot/index.d.ts +35 -0
  294. package/dist/plugins/agents/copilot/index.d.ts.map +1 -0
  295. package/dist/plugins/agents/copilot/index.js +182 -0
  296. package/dist/plugins/agents/copilot/index.js.map +1 -0
  297. package/dist/plugins/agents/opencode/ensure-mcp-config.d.ts +11 -0
  298. package/dist/plugins/agents/opencode/ensure-mcp-config.d.ts.map +1 -0
  299. package/dist/plugins/agents/opencode/ensure-mcp-config.js +100 -0
  300. package/dist/plugins/agents/opencode/ensure-mcp-config.js.map +1 -0
  301. package/dist/plugins/agents/opencode/index.d.ts +5 -0
  302. package/dist/plugins/agents/opencode/index.d.ts.map +1 -0
  303. package/dist/plugins/agents/opencode/index.js +30 -0
  304. package/dist/plugins/agents/opencode/index.js.map +1 -0
  305. package/dist/plugins/agents/opencode/opencode-http-adapter.d.ts +166 -0
  306. package/dist/plugins/agents/opencode/opencode-http-adapter.d.ts.map +1 -0
  307. package/dist/plugins/agents/opencode/opencode-http-adapter.js +682 -0
  308. package/dist/plugins/agents/opencode/opencode-http-adapter.js.map +1 -0
  309. package/dist/plugins/agents/opencode/opencode-stdio-adapter.d.ts +32 -0
  310. package/dist/plugins/agents/opencode/opencode-stdio-adapter.d.ts.map +1 -0
  311. package/dist/plugins/agents/opencode/opencode-stdio-adapter.js +137 -0
  312. package/dist/plugins/agents/opencode/opencode-stdio-adapter.js.map +1 -0
  313. package/dist/plugins/agents/opencode/serve-manager.d.ts +27 -0
  314. package/dist/plugins/agents/opencode/serve-manager.d.ts.map +1 -0
  315. package/dist/plugins/agents/opencode/serve-manager.js +194 -0
  316. package/dist/plugins/agents/opencode/serve-manager.js.map +1 -0
  317. package/dist/plugins/messengers/dingtalk/dingtalk-adapter.d.ts +57 -0
  318. package/dist/plugins/messengers/dingtalk/dingtalk-adapter.d.ts.map +1 -0
  319. package/dist/plugins/messengers/dingtalk/dingtalk-adapter.js +409 -0
  320. package/dist/plugins/messengers/dingtalk/dingtalk-adapter.js.map +1 -0
  321. package/dist/plugins/messengers/dingtalk/dingtalk-client.d.ts +48 -0
  322. package/dist/plugins/messengers/dingtalk/dingtalk-client.d.ts.map +1 -0
  323. package/dist/plugins/messengers/dingtalk/dingtalk-client.js +236 -0
  324. package/dist/plugins/messengers/dingtalk/dingtalk-client.js.map +1 -0
  325. package/dist/plugins/messengers/dingtalk/index.d.ts +3 -0
  326. package/dist/plugins/messengers/dingtalk/index.d.ts.map +1 -0
  327. package/dist/plugins/messengers/dingtalk/index.js +3 -0
  328. package/dist/plugins/messengers/dingtalk/index.js.map +1 -0
  329. package/dist/plugins/messengers/dingtalk/link-coords.d.ts +23 -0
  330. package/dist/plugins/messengers/dingtalk/link-coords.d.ts.map +1 -0
  331. package/dist/plugins/messengers/dingtalk/link-coords.js +89 -0
  332. package/dist/plugins/messengers/dingtalk/link-coords.js.map +1 -0
  333. package/dist/plugins/messengers/dingtalk/media-store.d.ts +16 -0
  334. package/dist/plugins/messengers/dingtalk/media-store.d.ts.map +1 -0
  335. package/dist/plugins/messengers/dingtalk/media-store.js +77 -0
  336. package/dist/plugins/messengers/dingtalk/media-store.js.map +1 -0
  337. package/dist/plugins/messengers/dingtalk/types.d.ts +82 -0
  338. package/dist/plugins/messengers/dingtalk/types.d.ts.map +1 -0
  339. package/dist/plugins/messengers/dingtalk/types.js +14 -0
  340. package/dist/plugins/messengers/dingtalk/types.js.map +1 -0
  341. package/dist/plugins/messengers/discord/discord-adapter.d.ts +21 -0
  342. package/dist/plugins/messengers/discord/discord-adapter.d.ts.map +1 -0
  343. package/dist/plugins/messengers/discord/discord-adapter.js +238 -0
  344. package/dist/plugins/messengers/discord/discord-adapter.js.map +1 -0
  345. package/dist/plugins/messengers/discord/index.d.ts +4 -0
  346. package/dist/plugins/messengers/discord/index.d.ts.map +1 -0
  347. package/dist/plugins/messengers/discord/index.js +4 -0
  348. package/dist/plugins/messengers/discord/index.js.map +1 -0
  349. package/dist/plugins/messengers/discord/markdown-to-discord.d.ts +11 -0
  350. package/dist/plugins/messengers/discord/markdown-to-discord.d.ts.map +1 -0
  351. package/dist/plugins/messengers/discord/markdown-to-discord.js +59 -0
  352. package/dist/plugins/messengers/discord/markdown-to-discord.js.map +1 -0
  353. package/dist/plugins/messengers/discord/types.d.ts +9 -0
  354. package/dist/plugins/messengers/discord/types.d.ts.map +1 -0
  355. package/dist/plugins/messengers/discord/types.js +3 -0
  356. package/dist/plugins/messengers/discord/types.js.map +1 -0
  357. package/dist/plugins/messengers/email/email-adapter.d.ts +33 -0
  358. package/dist/plugins/messengers/email/email-adapter.d.ts.map +1 -0
  359. package/dist/plugins/messengers/email/email-adapter.js +137 -0
  360. package/dist/plugins/messengers/email/email-adapter.js.map +1 -0
  361. package/dist/plugins/messengers/feishu/card-builder.d.ts +23 -0
  362. package/dist/plugins/messengers/feishu/card-builder.d.ts.map +1 -0
  363. package/dist/plugins/messengers/feishu/card-builder.js +89 -0
  364. package/dist/plugins/messengers/feishu/card-builder.js.map +1 -0
  365. package/dist/plugins/messengers/feishu/feishu-adapter.d.ts +23 -0
  366. package/dist/plugins/messengers/feishu/feishu-adapter.d.ts.map +1 -0
  367. package/dist/plugins/messengers/feishu/feishu-adapter.js +250 -0
  368. package/dist/plugins/messengers/feishu/feishu-adapter.js.map +1 -0
  369. package/dist/plugins/messengers/feishu/feishu-client.d.ts +43 -0
  370. package/dist/plugins/messengers/feishu/feishu-client.d.ts.map +1 -0
  371. package/dist/plugins/messengers/feishu/feishu-client.js +118 -0
  372. package/dist/plugins/messengers/feishu/feishu-client.js.map +1 -0
  373. package/dist/plugins/messengers/feishu/index.d.ts +4 -0
  374. package/dist/plugins/messengers/feishu/index.d.ts.map +1 -0
  375. package/dist/plugins/messengers/feishu/index.js +4 -0
  376. package/dist/plugins/messengers/feishu/index.js.map +1 -0
  377. package/dist/plugins/messengers/feishu/types.d.ts +113 -0
  378. package/dist/plugins/messengers/feishu/types.d.ts.map +1 -0
  379. package/dist/plugins/messengers/feishu/types.js +4 -0
  380. package/dist/plugins/messengers/feishu/types.js.map +1 -0
  381. package/dist/plugins/messengers/telegram/index.d.ts +4 -0
  382. package/dist/plugins/messengers/telegram/index.d.ts.map +1 -0
  383. package/dist/plugins/messengers/telegram/index.js +4 -0
  384. package/dist/plugins/messengers/telegram/index.js.map +1 -0
  385. package/dist/plugins/messengers/telegram/markdown-to-html.d.ts +5 -0
  386. package/dist/plugins/messengers/telegram/markdown-to-html.d.ts.map +1 -0
  387. package/dist/plugins/messengers/telegram/markdown-to-html.js +186 -0
  388. package/dist/plugins/messengers/telegram/markdown-to-html.js.map +1 -0
  389. package/dist/plugins/messengers/telegram/media-download.d.ts +59 -0
  390. package/dist/plugins/messengers/telegram/media-download.d.ts.map +1 -0
  391. package/dist/plugins/messengers/telegram/media-download.js +228 -0
  392. package/dist/plugins/messengers/telegram/media-download.js.map +1 -0
  393. package/dist/plugins/messengers/telegram/telegram-adapter.d.ts +77 -0
  394. package/dist/plugins/messengers/telegram/telegram-adapter.d.ts.map +1 -0
  395. package/dist/plugins/messengers/telegram/telegram-adapter.js +880 -0
  396. package/dist/plugins/messengers/telegram/telegram-adapter.js.map +1 -0
  397. package/dist/plugins/messengers/telegram/types.d.ts +47 -0
  398. package/dist/plugins/messengers/telegram/types.d.ts.map +1 -0
  399. package/dist/plugins/messengers/telegram/types.js +3 -0
  400. package/dist/plugins/messengers/telegram/types.js.map +1 -0
  401. package/dist/plugins/messengers/wechat/context-store.d.ts +18 -0
  402. package/dist/plugins/messengers/wechat/context-store.d.ts.map +1 -0
  403. package/dist/plugins/messengers/wechat/context-store.js +105 -0
  404. package/dist/plugins/messengers/wechat/context-store.js.map +1 -0
  405. package/dist/plugins/messengers/wechat/ilink-adapter.d.ts +71 -0
  406. package/dist/plugins/messengers/wechat/ilink-adapter.d.ts.map +1 -0
  407. package/dist/plugins/messengers/wechat/ilink-adapter.js +664 -0
  408. package/dist/plugins/messengers/wechat/ilink-adapter.js.map +1 -0
  409. package/dist/plugins/messengers/wechat/ilink-client.d.ts +75 -0
  410. package/dist/plugins/messengers/wechat/ilink-client.d.ts.map +1 -0
  411. package/dist/plugins/messengers/wechat/ilink-client.js +331 -0
  412. package/dist/plugins/messengers/wechat/ilink-client.js.map +1 -0
  413. package/dist/plugins/messengers/wechat/ilink-types.d.ts +181 -0
  414. package/dist/plugins/messengers/wechat/ilink-types.d.ts.map +1 -0
  415. package/dist/plugins/messengers/wechat/ilink-types.js +22 -0
  416. package/dist/plugins/messengers/wechat/ilink-types.js.map +1 -0
  417. package/dist/plugins/messengers/wechat/media-download.d.ts +32 -0
  418. package/dist/plugins/messengers/wechat/media-download.d.ts.map +1 -0
  419. package/dist/plugins/messengers/wechat/media-download.js +78 -0
  420. package/dist/plugins/messengers/wechat/media-download.js.map +1 -0
  421. package/dist/scripts/migrate-gcj02-to-wgs84.d.ts +3 -0
  422. package/dist/scripts/migrate-gcj02-to-wgs84.d.ts.map +1 -0
  423. package/dist/scripts/migrate-gcj02-to-wgs84.js +52 -0
  424. package/dist/scripts/migrate-gcj02-to-wgs84.js.map +1 -0
  425. package/dist/utils/backoff.d.ts +35 -0
  426. package/dist/utils/backoff.d.ts.map +1 -0
  427. package/dist/utils/backoff.js +59 -0
  428. package/dist/utils/backoff.js.map +1 -0
  429. package/dist/utils/cross-platform.d.ts +26 -0
  430. package/dist/utils/cross-platform.d.ts.map +1 -0
  431. package/dist/utils/cross-platform.js +58 -0
  432. package/dist/utils/cross-platform.js.map +1 -0
  433. package/dist/utils/message-split.d.ts +14 -0
  434. package/dist/utils/message-split.d.ts.map +1 -0
  435. package/dist/utils/message-split.js +65 -0
  436. package/dist/utils/message-split.js.map +1 -0
  437. package/dist/utils/safe-equal.d.ts +2 -0
  438. package/dist/utils/safe-equal.d.ts.map +1 -0
  439. package/dist/utils/safe-equal.js +11 -0
  440. package/dist/utils/safe-equal.js.map +1 -0
  441. package/dist/web/public/_app.js +196 -0
  442. package/dist/web/public/index.html +936 -0
  443. package/dist/web/public/loc.html +305 -0
  444. package/dist/web/public/login.html +106 -0
  445. package/dist/web/public/memos.html +271 -0
  446. package/dist/web/public/reminders.html +234 -0
  447. package/dist/web/public/settings.html +1355 -0
  448. package/dist/web/public/tasks.html +1835 -0
  449. package/dist/web/server.d.ts +12 -0
  450. package/dist/web/server.d.ts.map +1 -0
  451. package/dist/web/server.js +2399 -0
  452. package/dist/web/server.js.map +1 -0
  453. package/package.json +92 -0
@@ -0,0 +1,807 @@
1
+ // Session manager — per-conversation state
2
+ //
3
+ // On-disk layout (one directory tree per home):
4
+ // ~/.im-hub/sessions/<safe-key>.json — metadata (no messages)
5
+ // ~/.im-hub/sessions/<safe-key>.log — append-only JSONL of messages
6
+ //
7
+ // Splitting the message log out of the JSON metadata avoids rewriting the
8
+ // entire history on every chat turn (the old behavior was an O(N) write
9
+ // per message). All metadata writes are atomic via writeFile→rename.
10
+ import { createHash, randomBytes } from 'node:crypto';
11
+ import { join } from 'node:path';
12
+ import { mkdir, readFile, writeFile, rename, unlink, appendFile, readdir } from 'node:fs/promises';
13
+ import { approvalBus } from './approval-bus.js';
14
+ import { logger as rootLogger } from './logger.js';
15
+ import { AGIM_HOME } from './agim-paths.js';
16
+ const log = rootLogger.child({ component: 'session' });
17
+ const SESSIONS_DIR = join(AGIM_HOME, 'sessions');
18
+ function sanitizeKey(raw) {
19
+ return raw.replace(/[^A-Za-z0-9_-]/g, (c) => {
20
+ return createHash('sha256').update(c).digest('hex').slice(0, 8);
21
+ });
22
+ }
23
+ function sessionFilePath(key) {
24
+ const safe = sanitizeKey(key);
25
+ return join(SESSIONS_DIR, `${safe}.json`);
26
+ }
27
+ function sessionLogPath(key) {
28
+ const safe = sanitizeKey(key);
29
+ return join(SESSIONS_DIR, `${safe}.log`);
30
+ }
31
+ // Two-tier TTL (split out to fix the "agent drift after long pause" issue):
32
+ //
33
+ // MESSAGES_TTL — how long the in-memory chat history sticks around before
34
+ // we drop it from RAM and delete the .log file. Short by
35
+ // default (30 min) because a long pause usually means the
36
+ // user has switched topics; replaying stale messages back to
37
+ // the agent just bloats the prompt.
38
+ //
39
+ // META_TTL — how long the *session metadata* (agent, model, variant,
40
+ // claudeSessionId, claudeSessionPrimed, usage stats) lives
41
+ // on disk. Long by default (7 days) so the thread's "sticky
42
+ // agent" and resumable claude-code session id survive
43
+ // overnight / weekend gaps. Without this, a 30-minute
44
+ // silence followed by a coding-keyword message would
45
+ // re-classify and switch agents (e.g. claude-code → opencode).
46
+ //
47
+ // Both are env-overridable for ops tuning.
48
+ function envInt(name, fallback) {
49
+ const raw = process.env[name];
50
+ if (!raw)
51
+ return fallback;
52
+ const n = parseInt(raw, 10);
53
+ return Number.isFinite(n) && n > 0 ? n : fallback;
54
+ }
55
+ const MESSAGES_TTL = envInt('IMHUB_SESSION_MESSAGES_TTL_MS', 30 * 60 * 1000);
56
+ const META_TTL = envInt('IMHUB_SESSION_META_TTL_MS', 7 * 24 * 60 * 60 * 1000);
57
+ // Back-compat: external callers (tests, schedule.ts) used to import DEFAULT_TTL
58
+ // to mean "the one ttl". Keep the symbol pointing at META_TTL so anywhere it
59
+ // still appears in logs/metrics gets the long-lived value.
60
+ const DEFAULT_TTL = META_TTL;
61
+ const CLEANUP_INTERVAL = 5 * 60 * 1000; // 5 minutes
62
+ function metaStale(s, now = Date.now()) {
63
+ return now - s.lastActivity.getTime() > META_TTL;
64
+ }
65
+ function messagesStale(s, now = Date.now()) {
66
+ return now - s.lastActivity.getTime() > MESSAGES_TTL;
67
+ }
68
+ class SessionManager {
69
+ sessions = new Map();
70
+ cleanupTimer;
71
+ /**
72
+ * Per-key promise chain used to serialize writes that perform a
73
+ * read-modify-write on the in-memory session + persistent JSONL log.
74
+ *
75
+ * Without this, two concurrent {@link addMessage} calls on the same key
76
+ * could interleave: A reads cached session, B reads cached session, both
77
+ * push a different message into the same array, then both write — the
78
+ * later write wins and the earlier message is lost from disk (the
79
+ * in-memory copy is fine because both pushes happened on the same object
80
+ * reference, but the JSONL log only reflects the most recent appendFile +
81
+ * meta save).
82
+ *
83
+ * The lock is keyed by `${platform}:${channelId}:${threadId}`; different
84
+ * threads still proceed in parallel.
85
+ */
86
+ writeQueues = new Map();
87
+ async start() {
88
+ // Ensure sessions directory exists
89
+ await mkdir(SESSIONS_DIR, { recursive: true });
90
+ // Start cleanup timer
91
+ this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL);
92
+ log.info({ dir: SESSIONS_DIR }, 'Session manager started');
93
+ }
94
+ stop() {
95
+ if (this.cleanupTimer) {
96
+ clearInterval(this.cleanupTimer);
97
+ }
98
+ }
99
+ /**
100
+ * Run `fn` while holding a per-key serial lock. Subsequent calls with the
101
+ * same key wait until prior ones settle (success OR failure). Returns the
102
+ * value of `fn`. Internal use only — keeps {@link addMessage} race-free
103
+ * without introducing an external dep like p-queue.
104
+ */
105
+ async withLock(key, fn) {
106
+ const prev = this.writeQueues.get(key) ?? Promise.resolve();
107
+ // Chain even on failure so a thrown error in one segment doesn't poison
108
+ // subsequent waiters with that same rejection.
109
+ const next = prev.then(fn, fn);
110
+ this.writeQueues.set(key, next);
111
+ try {
112
+ return await next;
113
+ }
114
+ finally {
115
+ // Only clear if we're still the tail; another caller may have queued
116
+ // behind us in the meantime and we shouldn't drop their reference.
117
+ if (this.writeQueues.get(key) === next) {
118
+ this.writeQueues.delete(key);
119
+ }
120
+ }
121
+ }
122
+ /**
123
+ * Get or create a session for a conversation
124
+ * Session key: `${platform}:${channelId}:${threadId}`
125
+ */
126
+ async getOrCreateSession(platform, channelId, threadId, agent) {
127
+ const key = `${platform}:${channelId}:${threadId}`;
128
+ return this.withLock(key, async () => {
129
+ const now = new Date();
130
+ // Check memory cache
131
+ let session = this.sessions.get(key);
132
+ if (session) {
133
+ if (metaStale(session, now.getTime())) {
134
+ session = undefined; // fully expired → create new below
135
+ }
136
+ else {
137
+ if (messagesStale(session, now.getTime()) && session.messages.length > 0) {
138
+ session.messages = [];
139
+ try {
140
+ await unlink(sessionLogPath(key));
141
+ }
142
+ catch { /* no log to drop */ }
143
+ }
144
+ session.lastActivity = now;
145
+ await this.saveSessionMeta(key, session);
146
+ return session;
147
+ }
148
+ }
149
+ // Try loading from disk
150
+ session = await this.loadSession(key);
151
+ if (session && !metaStale(session, now.getTime())) {
152
+ if (messagesStale(session, now.getTime()) && session.messages.length > 0) {
153
+ session.messages = [];
154
+ try {
155
+ await unlink(sessionLogPath(key));
156
+ }
157
+ catch { /* no log to drop */ }
158
+ }
159
+ session.lastActivity = now;
160
+ this.sessions.set(key, session);
161
+ await this.saveSessionMeta(key, session);
162
+ return session;
163
+ }
164
+ // Create new session
165
+ session = {
166
+ id: `${platform}-${channelId}-${threadId}-${Date.now()}-${randomBytes(4).toString('hex')}`,
167
+ channelId,
168
+ threadId,
169
+ platform,
170
+ agent,
171
+ createdAt: now,
172
+ lastActivity: now,
173
+ ttl: DEFAULT_TTL,
174
+ messages: [],
175
+ };
176
+ this.sessions.set(key, session);
177
+ await this.saveSession(key, session);
178
+ return session;
179
+ });
180
+ }
181
+ /**
182
+ * Get existing session without creating a new one
183
+ * Returns undefined if no session exists or it's expired
184
+ */
185
+ async getExistingSession(platform, channelId, threadId) {
186
+ const key = `${platform}:${channelId}:${threadId}`;
187
+ const now = new Date();
188
+ // Check memory cache
189
+ let session = this.sessions.get(key);
190
+ if (session) {
191
+ if (metaStale(session, now.getTime())) {
192
+ return undefined;
193
+ }
194
+ if (messagesStale(session, now.getTime()) && session.messages.length > 0) {
195
+ // Drop stale chat history but preserve metadata (sticky agent,
196
+ // claudeSessionId etc.) — that's the whole point of META_TTL.
197
+ session.messages = [];
198
+ try {
199
+ await unlink(sessionLogPath(key));
200
+ }
201
+ catch { /* ignore */ }
202
+ }
203
+ return session;
204
+ }
205
+ // Try loading from disk
206
+ session = await this.loadSession(key);
207
+ if (session && !metaStale(session, now.getTime())) {
208
+ if (messagesStale(session, now.getTime()) && session.messages.length > 0) {
209
+ session.messages = [];
210
+ try {
211
+ await unlink(sessionLogPath(key));
212
+ }
213
+ catch { /* ignore */ }
214
+ }
215
+ this.sessions.set(key, session);
216
+ return session;
217
+ }
218
+ return undefined;
219
+ }
220
+ /**
221
+ * Switch the agent for a session.
222
+ *
223
+ * Generates a new session id but preserves thread identity AND every
224
+ * thread-level field that isn't agent-specific:
225
+ * - usage (per-thread /stats roll-up)
226
+ * - subtasks/active (subtask state lives at thread level)
227
+ * - claudeSessionId (Claude UUID survives /oc → /cc round-trips so the
228
+ * underlying ~/.claude/projects jsonl keeps continuing
229
+ * when the user comes back to claude)
230
+ *
231
+ * `model` and `variant` are reset because they live in different namespaces
232
+ * across CLIs (`opencode` model ≠ `claude` model); carrying them across
233
+ * would just feed the new agent an unrecognized argument.
234
+ */
235
+ async switchAgent(platform, channelId, threadId, newAgent) {
236
+ const key = `${platform}:${channelId}:${threadId}`;
237
+ return this.withLock(key, async () => {
238
+ // Get existing session or create new
239
+ const existing = this.sessions.get(key) || await this.loadSession(key);
240
+ const now = new Date();
241
+ const session = {
242
+ id: `${platform}-${channelId}-${threadId}-${Date.now()}-${randomBytes(4).toString('hex')}`,
243
+ channelId,
244
+ threadId,
245
+ platform,
246
+ agent: newAgent,
247
+ createdAt: existing?.createdAt || now,
248
+ lastActivity: now,
249
+ ttl: DEFAULT_TTL,
250
+ messages: existing?.messages || [],
251
+ usage: existing?.usage,
252
+ activeSubtaskId: existing?.activeSubtaskId,
253
+ subtasks: existing?.subtasks,
254
+ subtaskCounter: existing?.subtaskCounter,
255
+ claudeSessionId: existing?.claudeSessionId,
256
+ claudeSessionPrimed: existing?.claudeSessionPrimed,
257
+ opencodeSessionId: existing?.opencodeSessionId,
258
+ codexSessionId: existing?.codexSessionId,
259
+ planMode: existing?.planMode,
260
+ };
261
+ this.sessions.set(key, session);
262
+ await this.saveSession(key, session);
263
+ return session;
264
+ });
265
+ }
266
+ /**
267
+ * Append a message to the session history.
268
+ *
269
+ * Performance: instead of re-serializing the entire session JSON every
270
+ * turn, the message body is appended to a JSONL log file alongside the
271
+ * metadata (which gets a tiny atomic update for `lastActivity`).
272
+ *
273
+ * Concurrency: the entire read-modify-write is serialized per session key
274
+ * via {@link withLock} so two concurrent calls on the same thread (e.g.
275
+ * the IM message landing event AND a tool-result event arriving within
276
+ * the same ms) cannot lose either message to a lost-update race.
277
+ */
278
+ async addMessage(platform, channelId, threadId, message) {
279
+ const key = `${platform}:${channelId}:${threadId}`;
280
+ return this.withLock(key, async () => {
281
+ const session = this.sessions.get(key) || await this.loadSession(key);
282
+ if (!session)
283
+ return;
284
+ session.messages.push(message);
285
+ session.lastActivity = new Date();
286
+ this.sessions.set(key, session);
287
+ // Append-only log avoids rewriting the entire history per turn.
288
+ try {
289
+ await appendFile(sessionLogPath(key), `${JSON.stringify(message)}\n`);
290
+ }
291
+ catch (appendErr) {
292
+ log.warn({ event: 'session.append_failed', key, err: String(appendErr) }, 'JSONL append failed, falling back to full save');
293
+ try {
294
+ await this.saveSession(key, session);
295
+ }
296
+ catch (saveErr) {
297
+ log.warn({ event: 'session.fullsave_failed', key, err: String(saveErr) }, 'Full session save also failed — message persisted only in memory');
298
+ }
299
+ }
300
+ // Persist metadata only (now small & cheap).
301
+ await this.saveSessionMeta(key, session);
302
+ });
303
+ }
304
+ /**
305
+ * Persist `model` / `variant` / arbitrary patchable fields. Used by
306
+ * `/model`, `/think` etc so the change survives a restart between turns.
307
+ * Mutates the in-memory session in place AND writes metadata atomically.
308
+ */
309
+ async patchSession(platform, channelId, threadId, patch) {
310
+ const key = `${platform}:${channelId}:${threadId}`;
311
+ return this.withLock(key, async () => {
312
+ const session = this.sessions.get(key) || await this.loadSession(key);
313
+ if (!session)
314
+ return undefined;
315
+ if (patch.model !== undefined)
316
+ session.model = patch.model || undefined;
317
+ if (patch.variant !== undefined)
318
+ session.variant = patch.variant || undefined;
319
+ if (patch.agent !== undefined)
320
+ session.agent = patch.agent;
321
+ if (patch.planMode !== undefined) {
322
+ // Normalize to canonical shape: true keeps the flag, false drops it.
323
+ // Storing only the truthy state keeps the on-disk JSON small and lets
324
+ // a missing field unambiguously mean "off".
325
+ if (patch.planMode)
326
+ session.planMode = true;
327
+ else
328
+ delete session.planMode;
329
+ }
330
+ session.lastActivity = new Date();
331
+ this.sessions.set(key, session);
332
+ await this.saveSessionMeta(key, session);
333
+ return session;
334
+ });
335
+ }
336
+ /**
337
+ * Persist claude-code resumable session bookkeeping (UUID + primed flag).
338
+ * Returns the updated session, or undefined if no session exists yet for
339
+ * this thread. Caller is expected to ensure the session exists first.
340
+ */
341
+ async setClaudeSessionId(platform, channelId, threadId, claudeSessionId) {
342
+ const key = `${platform}:${channelId}:${threadId}`;
343
+ return this.withLock(key, async () => {
344
+ const session = this.sessions.get(key) || await this.loadSession(key);
345
+ if (!session)
346
+ return undefined;
347
+ session.claudeSessionId = claudeSessionId;
348
+ session.lastActivity = new Date();
349
+ this.sessions.set(key, session);
350
+ await this.saveSessionMeta(key, session);
351
+ return session;
352
+ });
353
+ }
354
+ async markClaudeSessionPrimed(platform, channelId, threadId) {
355
+ const key = `${platform}:${channelId}:${threadId}`;
356
+ return this.withLock(key, async () => {
357
+ const session = this.sessions.get(key) || await this.loadSession(key);
358
+ if (!session || session.claudeSessionPrimed)
359
+ return;
360
+ session.claudeSessionPrimed = true;
361
+ session.lastActivity = new Date();
362
+ this.sessions.set(key, session);
363
+ await this.saveSessionMeta(key, session);
364
+ });
365
+ }
366
+ /**
367
+ * Persist opencode's native session id (`ses_…`) once we've seen it in the
368
+ * adapter's stream. Idempotent — calling with the same id is a no-op so
369
+ * the per-event callback can fire as many times as opencode sends events.
370
+ */
371
+ async setOpencodeSessionId(platform, channelId, threadId, opencodeSessionId) {
372
+ const key = `${platform}:${channelId}:${threadId}`;
373
+ return this.withLock(key, async () => {
374
+ const session = this.sessions.get(key) || await this.loadSession(key);
375
+ if (!session)
376
+ return undefined;
377
+ if (session.opencodeSessionId === opencodeSessionId)
378
+ return session;
379
+ session.opencodeSessionId = opencodeSessionId;
380
+ session.lastActivity = new Date();
381
+ this.sessions.set(key, session);
382
+ await this.saveSessionMeta(key, session);
383
+ return session;
384
+ });
385
+ }
386
+ /**
387
+ * Persist codex's native thread id (UUID) once we've seen it in the
388
+ * adapter's `thread.started` event. Idempotent — same id may fire multiple
389
+ * times per spawn. Mirrors setOpencodeSessionId.
390
+ */
391
+ async setCodexSessionId(platform, channelId, threadId, codexSessionId) {
392
+ const key = `${platform}:${channelId}:${threadId}`;
393
+ return this.withLock(key, async () => {
394
+ const session = this.sessions.get(key) || await this.loadSession(key);
395
+ if (!session)
396
+ return undefined;
397
+ if (session.codexSessionId === codexSessionId)
398
+ return session;
399
+ session.codexSessionId = codexSessionId;
400
+ session.lastActivity = new Date();
401
+ this.sessions.set(key, session);
402
+ await this.saveSessionMeta(key, session);
403
+ return session;
404
+ });
405
+ }
406
+ /**
407
+ * Increment the per-session usage roll-up after a successful agent
408
+ * invocation. Used by router.callAgentWithHistory to power /stats.
409
+ */
410
+ async recordUsage(platform, channelId, threadId, delta) {
411
+ const key = `${platform}:${channelId}:${threadId}`;
412
+ return this.withLock(key, async () => {
413
+ const session = this.sessions.get(key) || await this.loadSession(key);
414
+ if (!session)
415
+ return;
416
+ if (!session.usage) {
417
+ session.usage = {
418
+ turns: 0,
419
+ costUsd: 0,
420
+ promptChars: 0,
421
+ responseChars: 0,
422
+ durationMsTotal: 0,
423
+ startedAt: new Date().toISOString(),
424
+ };
425
+ }
426
+ session.usage.turns += 1;
427
+ session.usage.costUsd += Number.isFinite(delta.costUsd) ? delta.costUsd : 0;
428
+ session.usage.promptChars += Number.isFinite(delta.promptChars) ? delta.promptChars : 0;
429
+ session.usage.responseChars += Number.isFinite(delta.responseChars) ? delta.responseChars : 0;
430
+ session.usage.durationMsTotal += Number.isFinite(delta.durationMs) ? delta.durationMs : 0;
431
+ session.lastActivity = new Date();
432
+ this.sessions.set(key, session);
433
+ await this.saveSessionMeta(key, session);
434
+ });
435
+ }
436
+ /**
437
+ * Reset conversation history (keep session but clear messages)
438
+ */
439
+ async resetConversation(platform, channelId, threadId) {
440
+ const key = `${platform}:${channelId}:${threadId}`;
441
+ return this.withLock(key, async () => {
442
+ const session = this.sessions.get(key) || await this.loadSession(key);
443
+ if (session) {
444
+ session.messages = [];
445
+ session.lastActivity = new Date();
446
+ session.id = `${platform}-${channelId}-${threadId}-${Date.now()}-${randomBytes(4).toString('hex')}`; // New session ID
447
+ // Forget the old per-agent CLI sessions — /new should give a clean slate
448
+ // for both Claude (`--resume`) and opencode (`--session`).
449
+ delete session.claudeSessionId;
450
+ delete session.claudeSessionPrimed;
451
+ delete session.opencodeSessionId;
452
+ delete session.codexSessionId;
453
+ // Plan mode is per-conversation intent ("先规划再动手") — a fresh
454
+ // conversation always starts at "off" so users don't get a surprising
455
+ // read-only run after /new.
456
+ delete session.planMode;
457
+ // Drop any per-thread auto-allow approval rules so the new conversation
458
+ // starts back at "ask every time".
459
+ try {
460
+ approvalBus.clearAutoAllowForThread(key);
461
+ }
462
+ catch { /* ignore */ }
463
+ this.sessions.set(key, session);
464
+ await this.saveSession(key, session);
465
+ return session;
466
+ }
467
+ return undefined;
468
+ });
469
+ }
470
+ /**
471
+ * Get session with messages (convenience method)
472
+ */
473
+ async getSessionWithHistory(platform, channelId, threadId) {
474
+ const session = await this.getExistingSession(platform, channelId, threadId);
475
+ if (session) {
476
+ return { session, messages: session.messages };
477
+ }
478
+ return undefined;
479
+ }
480
+ /**
481
+ * Create or get a subtask session (independent from parent).
482
+ */
483
+ async getOrCreateSubSession(platform, channelId, threadId, subtaskId, agent) {
484
+ const key = `${platform}:${channelId}:${threadId}:sub:${subtaskId}`;
485
+ const now = new Date();
486
+ return this.withLock(key, async () => {
487
+ let session = this.sessions.get(key) || await this.loadSession(key);
488
+ if (session) {
489
+ session.lastActivity = now;
490
+ await this.saveSessionMeta(key, session);
491
+ return session;
492
+ }
493
+ session = {
494
+ id: `sub-${platform}-${channelId}-${threadId}-${subtaskId}`,
495
+ channelId, threadId, platform, agent,
496
+ createdAt: now, lastActivity: now, ttl: DEFAULT_TTL, messages: [],
497
+ };
498
+ this.sessions.set(key, session);
499
+ await this.saveSession(key, session);
500
+ return session;
501
+ });
502
+ }
503
+ /**
504
+ * Set active subtask id on parent session — subsequent messages route to the subtask.
505
+ */
506
+ async setActiveSubtask(platform, channelId, threadId, taskId) {
507
+ const key = `${platform}:${channelId}:${threadId}`;
508
+ return this.withLock(key, async () => {
509
+ const session = this.sessions.get(key) || await this.loadSession(key);
510
+ if (!session)
511
+ return;
512
+ session.activeSubtaskId = taskId;
513
+ this.sessions.set(key, session);
514
+ await this.saveSession(key, session);
515
+ });
516
+ }
517
+ /**
518
+ * Get subtask metadata list from parent session.
519
+ */
520
+ async getSubtasks(platform, channelId, threadId) {
521
+ const key = `${platform}:${channelId}:${threadId}`;
522
+ const session = this.sessions.get(key) || await this.loadSession(key);
523
+ return session?.subtasks || [];
524
+ }
525
+ /**
526
+ * Scan all session files on disk and return every subtask, flattened, with
527
+ * its parent platform/channelId/threadId/agent attached so the dashboard
528
+ * can render subtasks across all conversations.
529
+ *
530
+ * Session files live as `<sanitized-key>.json` under SESSIONS_DIR. The
531
+ * sanitized key is one-way (sha256-prefix per non-alnum char), so we
532
+ * cannot reverse it — but each session file preserves the original
533
+ * platform/channelId/threadId fields, which is what we need.
534
+ */
535
+ async listAllSubtasks(opts = {}) {
536
+ let names;
537
+ try {
538
+ names = await readdir(SESSIONS_DIR);
539
+ }
540
+ catch {
541
+ return [];
542
+ }
543
+ const out = [];
544
+ for (const name of names) {
545
+ if (!name.endsWith('.json'))
546
+ continue;
547
+ try {
548
+ const raw = await readFile(join(SESSIONS_DIR, name), 'utf-8');
549
+ const parsed = JSON.parse(raw);
550
+ if (!parsed.subtasks?.length)
551
+ continue;
552
+ // Filter by parent-agent up-front so we don't allocate items we'll
553
+ // discard. Subtasks inherit the parent session's agent — there is
554
+ // no per-subtask agent override today.
555
+ if (opts.agent && (parsed.agent || '') !== opts.agent)
556
+ continue;
557
+ for (const st of parsed.subtasks) {
558
+ out.push({
559
+ ...st,
560
+ createdAt: st.createdAt ? new Date(st.createdAt) : new Date(0),
561
+ completedAt: st.completedAt ? new Date(st.completedAt) : undefined,
562
+ platform: parsed.platform || '',
563
+ channelId: parsed.channelId || '',
564
+ threadId: parsed.threadId || '',
565
+ parentAgent: parsed.agent || '',
566
+ parentSessionId: parsed.id || '',
567
+ });
568
+ }
569
+ }
570
+ catch {
571
+ // skip corrupt session file
572
+ }
573
+ }
574
+ // newest first
575
+ out.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
576
+ return out;
577
+ }
578
+ /**
579
+ * Update subtask metadata in parent session.
580
+ */
581
+ async updateSubtask(platform, channelId, threadId, taskId, patch) {
582
+ const key = `${platform}:${channelId}:${threadId}`;
583
+ return this.withLock(key, async () => {
584
+ const session = this.sessions.get(key) || await this.loadSession(key);
585
+ if (!session)
586
+ return;
587
+ if (!session.subtasks) {
588
+ session.subtasks = [];
589
+ }
590
+ const idx = session.subtasks.findIndex(s => s.id === taskId);
591
+ if (idx >= 0) {
592
+ session.subtasks[idx] = { ...session.subtasks[idx], ...patch };
593
+ }
594
+ else {
595
+ session.subtasks.push({ id: taskId, ...patch });
596
+ }
597
+ this.sessions.set(key, session);
598
+ await this.saveSession(key, session);
599
+ });
600
+ }
601
+ /**
602
+ * Get next subtask id and persist the increment.
603
+ *
604
+ * Previously returned 1 when the parent session didn't exist yet, but
605
+ * never created one — second call returned 1 again, leading to subtask
606
+ * id collisions. Now we lazy-create the parent session so the counter
607
+ * increments durably from the first call.
608
+ */
609
+ async nextSubtaskId(platform, channelId, threadId, agent = '') {
610
+ const key = `${platform}:${channelId}:${threadId}`;
611
+ return this.withLock(key, async () => {
612
+ let session = this.sessions.get(key) || await this.loadSession(key);
613
+ if (!session) {
614
+ const now = new Date();
615
+ session = {
616
+ id: `${platform}-${channelId}-${threadId}-${Date.now()}-${randomBytes(4).toString('hex')}`,
617
+ channelId, threadId, platform, agent,
618
+ createdAt: now, lastActivity: now, ttl: DEFAULT_TTL, messages: [],
619
+ subtaskCounter: 0,
620
+ };
621
+ }
622
+ session.subtaskCounter = (session.subtaskCounter || 0) + 1;
623
+ this.sessions.set(key, session);
624
+ await this.saveSession(key, session);
625
+ return session.subtaskCounter;
626
+ });
627
+ }
628
+ /**
629
+ * Persist the full session (metadata + messages). Used for the legacy
630
+ * one-file format on resetConversation() and switchAgent() — anywhere
631
+ * the messages array itself was rewritten. Atomic via tmp+rename.
632
+ */
633
+ async saveSession(key, session) {
634
+ await this.saveSessionMeta(key, session);
635
+ // Rewrite the JSONL log to match the in-memory messages array. This is
636
+ // only called from paths that actually mutate `messages` wholesale
637
+ // (resetConversation, switchAgent). addMessage uses appendFile which
638
+ // is far cheaper for the hot path.
639
+ const logPath = sessionLogPath(key);
640
+ try {
641
+ const lines = session.messages.map((m) => JSON.stringify(m)).join('\n');
642
+ await this.atomicWrite(logPath, lines + (lines ? '\n' : ''));
643
+ }
644
+ catch {
645
+ // disk failure — in-memory state is still authoritative
646
+ }
647
+ }
648
+ /** Persist metadata only (no messages payload), atomically. */
649
+ async saveSessionMeta(key, session) {
650
+ const filePath = sessionFilePath(key);
651
+ try {
652
+ const meta = {
653
+ id: session.id,
654
+ channelId: session.channelId,
655
+ threadId: session.threadId,
656
+ platform: session.platform,
657
+ agent: session.agent,
658
+ model: session.model,
659
+ variant: session.variant,
660
+ usage: session.usage,
661
+ createdAt: session.createdAt,
662
+ lastActivity: session.lastActivity,
663
+ ttl: session.ttl,
664
+ activeSubtaskId: session.activeSubtaskId,
665
+ subtasks: session.subtasks,
666
+ subtaskCounter: session.subtaskCounter,
667
+ claudeSessionId: session.claudeSessionId,
668
+ claudeSessionPrimed: session.claudeSessionPrimed,
669
+ opencodeSessionId: session.opencodeSessionId,
670
+ codexSessionId: session.codexSessionId,
671
+ planMode: session.planMode,
672
+ messageCount: session.messages.length,
673
+ };
674
+ await this.atomicWrite(filePath, JSON.stringify(meta, null, 2));
675
+ }
676
+ catch {
677
+ // ignore
678
+ }
679
+ }
680
+ /** Crash-safe write: tmp file + atomic rename.
681
+ *
682
+ * Recovers from ENOENT (parent dir missing) by mkdir-recursive + retry
683
+ * exactly once. This keeps the manager robust to environments where
684
+ * start() wasn't called yet — tests in particular tend to import
685
+ * sessionManager without going through the start() lifecycle, and
686
+ * saveSessionMeta's outer try/catch would otherwise swallow the ENOENT
687
+ * and silently produce a no-op write (the bug that caused
688
+ * session-subtasks.test.ts to fail on a fresh CI runner). */
689
+ async atomicWrite(filePath, contents) {
690
+ const tmp = `${filePath}.${randomBytes(4).toString('hex')}.tmp`;
691
+ try {
692
+ await writeFile(tmp, contents);
693
+ }
694
+ catch (err) {
695
+ if (err.code === 'ENOENT') {
696
+ await mkdir(SESSIONS_DIR, { recursive: true });
697
+ await writeFile(tmp, contents);
698
+ }
699
+ else {
700
+ throw err;
701
+ }
702
+ }
703
+ try {
704
+ await rename(tmp, filePath);
705
+ }
706
+ catch (err) {
707
+ try {
708
+ await unlink(tmp);
709
+ }
710
+ catch { /* ignore */ }
711
+ throw err;
712
+ }
713
+ }
714
+ async loadSession(key) {
715
+ const filePath = sessionFilePath(key);
716
+ try {
717
+ const data = await readFile(filePath, 'utf-8');
718
+ const parsed = JSON.parse(data);
719
+ const session = {
720
+ id: parsed.id,
721
+ channelId: parsed.channelId,
722
+ threadId: parsed.threadId,
723
+ platform: parsed.platform,
724
+ agent: parsed.agent,
725
+ model: parsed.model,
726
+ variant: parsed.variant,
727
+ usage: parsed.usage,
728
+ createdAt: new Date(parsed.createdAt),
729
+ lastActivity: new Date(parsed.lastActivity),
730
+ ttl: parsed.ttl,
731
+ messages: parsed.messages || [], // legacy one-file format
732
+ activeSubtaskId: parsed.activeSubtaskId,
733
+ subtasks: parsed.subtasks,
734
+ subtaskCounter: parsed.subtaskCounter,
735
+ claudeSessionId: parsed.claudeSessionId,
736
+ claudeSessionPrimed: parsed.claudeSessionPrimed,
737
+ opencodeSessionId: parsed.opencodeSessionId,
738
+ codexSessionId: parsed.codexSessionId,
739
+ planMode: parsed.planMode,
740
+ };
741
+ // Convert message timestamps from legacy format if present
742
+ session.messages = session.messages.map((msg) => ({
743
+ ...msg,
744
+ timestamp: new Date(msg.timestamp),
745
+ }));
746
+ // Then merge in JSONL log entries (new format).
747
+ try {
748
+ const log = await readFile(sessionLogPath(key), 'utf-8');
749
+ const logged = [];
750
+ for (const line of log.split('\n')) {
751
+ if (!line.trim())
752
+ continue;
753
+ try {
754
+ const m = JSON.parse(line);
755
+ logged.push({ ...m, timestamp: new Date(m.timestamp) });
756
+ }
757
+ catch { /* skip corrupt line */ }
758
+ }
759
+ // The log is authoritative for new-format sessions. If both exist
760
+ // (rare, after a save followed by addMessage), the log wins.
761
+ if (logged.length > 0) {
762
+ session.messages = logged;
763
+ }
764
+ }
765
+ catch {
766
+ // No log file — legacy format only
767
+ }
768
+ return session;
769
+ }
770
+ catch {
771
+ return undefined;
772
+ }
773
+ }
774
+ async cleanup() {
775
+ const now = Date.now();
776
+ for (const [key, session] of this.sessions.entries()) {
777
+ const idle = now - session.lastActivity.getTime();
778
+ if (idle > META_TTL) {
779
+ // Full eviction: thread truly cold. Drop both files + cache entry.
780
+ this.sessions.delete(key);
781
+ const filePath = sessionFilePath(key);
782
+ const logPath = sessionLogPath(key);
783
+ try {
784
+ await unlink(filePath);
785
+ }
786
+ catch { /* ignore */ }
787
+ try {
788
+ await unlink(logPath);
789
+ }
790
+ catch { /* ignore */ }
791
+ }
792
+ else if (idle > MESSAGES_TTL && session.messages.length > 0) {
793
+ // Messages-only eviction: keep sticky agent / claudeSessionId on disk
794
+ // (meta file untouched), drop chat log + in-memory messages so the
795
+ // next turn starts with a fresh history but the same routing.
796
+ session.messages = [];
797
+ const logPath = sessionLogPath(key);
798
+ try {
799
+ await unlink(logPath);
800
+ }
801
+ catch { /* ignore */ }
802
+ }
803
+ }
804
+ }
805
+ }
806
+ export const sessionManager = new SessionManager();
807
+ //# sourceMappingURL=session.js.map