@jcheesepkg/nanobot 0.9.1 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/README.md +18 -18
  2. package/dist/agent/context.d.mts +4 -4
  3. package/dist/agent/context.d.mts.map +1 -1
  4. package/dist/agent/context.mjs +27 -28
  5. package/dist/agent/context.mjs.map +1 -1
  6. package/dist/agent/loop.d.mts +5 -3
  7. package/dist/agent/loop.d.mts.map +1 -1
  8. package/dist/agent/loop.mjs +64 -55
  9. package/dist/agent/loop.mjs.map +1 -1
  10. package/dist/agent/memory.d.mts.map +1 -1
  11. package/dist/agent/memory.mjs +3 -3
  12. package/dist/agent/memory.mjs.map +1 -1
  13. package/dist/agent/skills.d.mts.map +1 -1
  14. package/dist/agent/skills.mjs +4 -4
  15. package/dist/agent/skills.mjs.map +1 -1
  16. package/dist/agent/subagent.d.mts.map +1 -1
  17. package/dist/agent/subagent.mjs +22 -22
  18. package/dist/agent/subagent.mjs.map +1 -1
  19. package/dist/agent/tools/base.mjs +2 -2
  20. package/dist/agent/tools/base.mjs.map +1 -1
  21. package/dist/agent/tools/cron.d.mts +1 -1
  22. package/dist/agent/tools/cron.d.mts.map +1 -1
  23. package/dist/agent/tools/cron.mjs +11 -11
  24. package/dist/agent/tools/cron.mjs.map +1 -1
  25. package/dist/agent/tools/filesystem.d.mts +4 -4
  26. package/dist/agent/tools/filesystem.d.mts.map +1 -1
  27. package/dist/agent/tools/filesystem.mjs +20 -20
  28. package/dist/agent/tools/filesystem.mjs.map +1 -1
  29. package/dist/agent/tools/flex.d.mts +1 -1
  30. package/dist/agent/tools/flex.d.mts.map +1 -1
  31. package/dist/agent/tools/flex.mjs +112 -112
  32. package/dist/agent/tools/flex.mjs.map +1 -1
  33. package/dist/agent/tools/flex.test.mjs +60 -59
  34. package/dist/agent/tools/flex.test.mjs.map +1 -1
  35. package/dist/agent/tools/message.d.mts +1 -1
  36. package/dist/agent/tools/message.d.mts.map +1 -1
  37. package/dist/agent/tools/message.mjs +4 -4
  38. package/dist/agent/tools/message.mjs.map +1 -1
  39. package/dist/agent/tools/registry.d.mts.map +1 -1
  40. package/dist/agent/tools/registry.mjs +4 -4
  41. package/dist/agent/tools/registry.mjs.map +1 -1
  42. package/dist/agent/tools/shell.d.mts +1 -1
  43. package/dist/agent/tools/shell.mjs +4 -4
  44. package/dist/agent/tools/shell.mjs.map +1 -1
  45. package/dist/agent/tools/spawn.d.mts +1 -1
  46. package/dist/agent/tools/spawn.d.mts.map +1 -1
  47. package/dist/agent/tools/spawn.mjs +4 -4
  48. package/dist/agent/tools/spawn.mjs.map +1 -1
  49. package/dist/agent/tools/web.d.mts +2 -2
  50. package/dist/agent/tools/web.d.mts.map +1 -1
  51. package/dist/agent/tools/web.mjs +36 -36
  52. package/dist/agent/tools/web.mjs.map +1 -1
  53. package/dist/bus/events.mjs +1 -1
  54. package/dist/bus/events.mjs.map +1 -1
  55. package/dist/bus/queue.d.mts.map +1 -1
  56. package/dist/bus/queue.mjs.map +1 -1
  57. package/dist/channels/base.d.mts.map +1 -1
  58. package/dist/channels/base.mjs +2 -2
  59. package/dist/channels/base.mjs.map +1 -1
  60. package/dist/channels/line.d.mts +1 -0
  61. package/dist/channels/line.d.mts.map +1 -1
  62. package/dist/channels/line.mjs +65 -65
  63. package/dist/channels/line.mjs.map +1 -1
  64. package/dist/channels/line.test.mjs +26 -27
  65. package/dist/channels/line.test.mjs.map +1 -1
  66. package/dist/channels/manager.d.mts.map +1 -1
  67. package/dist/channels/manager.mjs +9 -9
  68. package/dist/channels/manager.mjs.map +1 -1
  69. package/dist/channels/telegram.mjs +34 -34
  70. package/dist/channels/telegram.mjs.map +1 -1
  71. package/dist/cli/index.mjs +36 -36
  72. package/dist/cli/index.mjs.map +1 -1
  73. package/dist/config/loader.d.mts.map +1 -1
  74. package/dist/config/loader.mjs +1 -1
  75. package/dist/config/loader.mjs.map +1 -1
  76. package/dist/config/schema.d.mts +387 -387
  77. package/dist/config/schema.d.mts.map +1 -1
  78. package/dist/config/schema.mjs +42 -42
  79. package/dist/config/schema.mjs.map +1 -1
  80. package/dist/gateway/server.d.mts.map +1 -1
  81. package/dist/gateway/server.mjs +48 -54
  82. package/dist/gateway/server.mjs.map +1 -1
  83. package/dist/heartbeat/service.d.mts.map +1 -1
  84. package/dist/heartbeat/service.mjs +8 -8
  85. package/dist/heartbeat/service.mjs.map +1 -1
  86. package/dist/index.d.mts +1 -1
  87. package/dist/index.d.mts.map +1 -1
  88. package/dist/index.mjs +2 -2
  89. package/dist/index.mjs.map +1 -1
  90. package/dist/node_modules/{@jridgewell → .bun/@jridgewell_sourcemap-codec@1.5.5/node_modules/@jridgewell}/sourcemap-codec/dist/sourcemap-codec.mjs +1 -1
  91. package/dist/node_modules/.bun/@jridgewell_sourcemap-codec@1.5.5/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.mjs.map +1 -0
  92. package/dist/node_modules/{@vitest → .bun/@vitest_expect@2.1.9/node_modules/@vitest}/expect/dist/index.mjs +8 -8
  93. package/dist/node_modules/.bun/@vitest_expect@2.1.9/node_modules/@vitest/expect/dist/index.mjs.map +1 -0
  94. package/dist/node_modules/{@vitest → .bun/@vitest_pretty-format@2.1.9/node_modules/@vitest}/pretty-format/dist/index.mjs +2 -2
  95. package/dist/node_modules/.bun/@vitest_pretty-format@2.1.9/node_modules/@vitest/pretty-format/dist/index.mjs.map +1 -0
  96. package/dist/node_modules/{@vitest → .bun/@vitest_runner@2.1.9/node_modules/@vitest}/runner/dist/chunk-tasks.mjs +1 -1
  97. package/dist/node_modules/.bun/@vitest_runner@2.1.9/node_modules/@vitest/runner/dist/chunk-tasks.mjs.map +1 -0
  98. package/dist/node_modules/{@vitest → .bun/@vitest_runner@2.1.9/node_modules/@vitest}/runner/dist/index.mjs +6 -6
  99. package/dist/node_modules/.bun/@vitest_runner@2.1.9/node_modules/@vitest/runner/dist/index.mjs.map +1 -0
  100. package/dist/node_modules/{@vitest → .bun/@vitest_snapshot@2.1.9/node_modules/@vitest}/snapshot/dist/index.mjs +5 -5
  101. package/dist/node_modules/.bun/@vitest_snapshot@2.1.9/node_modules/@vitest/snapshot/dist/index.mjs.map +1 -0
  102. package/dist/node_modules/{@vitest → .bun/@vitest_spy@2.1.9/node_modules/@vitest}/spy/dist/index.mjs +2 -2
  103. package/dist/node_modules/.bun/@vitest_spy@2.1.9/node_modules/@vitest/spy/dist/index.mjs.map +1 -0
  104. package/dist/node_modules/{@vitest → .bun/@vitest_utils@2.1.9/node_modules/@vitest}/utils/dist/chunk-_commonjsHelpers.mjs +3 -3
  105. package/dist/node_modules/.bun/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/chunk-_commonjsHelpers.mjs.map +1 -0
  106. package/dist/node_modules/{@vitest → .bun/@vitest_utils@2.1.9/node_modules/@vitest}/utils/dist/diff.mjs +4 -4
  107. package/dist/node_modules/.bun/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/diff.mjs.map +1 -0
  108. package/dist/node_modules/{@vitest → .bun/@vitest_utils@2.1.9/node_modules/@vitest}/utils/dist/error.mjs +3 -3
  109. package/dist/node_modules/.bun/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/error.mjs.map +1 -0
  110. package/dist/node_modules/{@vitest → .bun/@vitest_utils@2.1.9/node_modules/@vitest}/utils/dist/helpers.mjs +1 -1
  111. package/dist/node_modules/.bun/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/helpers.mjs.map +1 -0
  112. package/dist/node_modules/{@vitest → .bun/@vitest_utils@2.1.9/node_modules/@vitest}/utils/dist/index.mjs +3 -3
  113. package/dist/node_modules/.bun/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/index.mjs.map +1 -0
  114. package/dist/node_modules/{@vitest → .bun/@vitest_utils@2.1.9/node_modules/@vitest}/utils/dist/source-map.mjs +1 -1
  115. package/dist/node_modules/.bun/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/source-map.mjs.map +1 -0
  116. package/dist/node_modules/{chai → .bun/chai@5.3.3/node_modules/chai}/index.mjs +1 -1
  117. package/dist/node_modules/.bun/chai@5.3.3/node_modules/chai/index.mjs.map +1 -0
  118. package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/arguments.mjs +1 -1
  119. package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/arguments.mjs.map +1 -0
  120. package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/array.mjs +1 -1
  121. package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/array.mjs.map +1 -0
  122. package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/bigint.mjs +1 -1
  123. package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/bigint.mjs.map +1 -0
  124. package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/class.mjs +1 -1
  125. package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/class.mjs.map +1 -0
  126. package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/date.mjs +1 -1
  127. package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/date.mjs.map +1 -0
  128. package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/error.mjs +1 -1
  129. package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/error.mjs.map +1 -0
  130. package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/function.mjs +1 -1
  131. package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/function.mjs.map +1 -0
  132. package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/helpers.mjs +1 -1
  133. package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/helpers.mjs.map +1 -0
  134. package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/html.mjs +1 -1
  135. package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/html.mjs.map +1 -0
  136. package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/index.mjs +1 -1
  137. package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/index.mjs.map +1 -0
  138. package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/map.mjs +1 -1
  139. package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/map.mjs.map +1 -0
  140. package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/number.mjs +1 -1
  141. package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/number.mjs.map +1 -0
  142. package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/object.mjs +1 -1
  143. package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/object.mjs.map +1 -0
  144. package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/promise.mjs +6 -0
  145. package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/promise.mjs.map +1 -0
  146. package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/regexp.mjs +1 -1
  147. package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/regexp.mjs.map +1 -0
  148. package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/set.mjs +1 -1
  149. package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/set.mjs.map +1 -0
  150. package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/string.mjs +1 -1
  151. package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/string.mjs.map +1 -0
  152. package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/symbol.mjs +1 -1
  153. package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/symbol.mjs.map +1 -0
  154. package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/typedarray.mjs +1 -1
  155. package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/typedarray.mjs.map +1 -0
  156. package/dist/node_modules/{magic-string → .bun/magic-string@0.30.21/node_modules/magic-string}/dist/magic-string.es.mjs +2 -2
  157. package/dist/node_modules/.bun/magic-string@0.30.21/node_modules/magic-string/dist/magic-string.es.mjs.map +1 -0
  158. package/dist/node_modules/{@vitest/snapshot → .bun/pathe@1.1.2}/node_modules/pathe/dist/shared/pathe.ff20891b.mjs +1 -1
  159. package/dist/node_modules/.bun/pathe@1.1.2/node_modules/pathe/dist/shared/pathe.ff20891b.mjs.map +1 -0
  160. package/dist/node_modules/{tinyrainbow → .bun/tinyrainbow@1.2.0/node_modules/tinyrainbow}/dist/chunk-BVHSVHOK.mjs +1 -1
  161. package/dist/node_modules/.bun/tinyrainbow@1.2.0/node_modules/tinyrainbow/dist/chunk-BVHSVHOK.mjs.map +1 -0
  162. package/dist/node_modules/{tinyrainbow → .bun/tinyrainbow@1.2.0/node_modules/tinyrainbow}/dist/node.mjs +1 -1
  163. package/dist/node_modules/.bun/tinyrainbow@1.2.0/node_modules/tinyrainbow/dist/node.mjs.map +1 -0
  164. package/dist/node_modules/{tinyspy → .bun/tinyspy@3.0.2/node_modules/tinyspy}/dist/index.mjs +1 -1
  165. package/dist/node_modules/.bun/tinyspy@3.0.2/node_modules/tinyspy/dist/index.mjs.map +1 -0
  166. package/dist/node_modules/{vitest → .bun/vitest@2.1.9_7700f9e9ace41f23/node_modules/vitest}/dist/chunks/_commonjsHelpers.BFTU3MAI.mjs +1 -1
  167. package/dist/node_modules/.bun/vitest@2.1.9_7700f9e9ace41f23/node_modules/vitest/dist/chunks/_commonjsHelpers.BFTU3MAI.mjs.map +1 -0
  168. package/dist/node_modules/{vitest → .bun/vitest@2.1.9_7700f9e9ace41f23/node_modules/vitest}/dist/chunks/date.W2xKR2qe.mjs +1 -1
  169. package/dist/node_modules/.bun/vitest@2.1.9_7700f9e9ace41f23/node_modules/vitest/dist/chunks/date.W2xKR2qe.mjs.map +1 -0
  170. package/dist/node_modules/{vitest → .bun/vitest@2.1.9_7700f9e9ace41f23/node_modules/vitest}/dist/chunks/utils.C8RiOc4B.mjs +2 -2
  171. package/dist/node_modules/.bun/vitest@2.1.9_7700f9e9ace41f23/node_modules/vitest/dist/chunks/utils.C8RiOc4B.mjs.map +1 -0
  172. package/dist/node_modules/{vitest → .bun/vitest@2.1.9_7700f9e9ace41f23/node_modules/vitest}/dist/chunks/vi.DgezovHB.mjs +11 -11
  173. package/dist/node_modules/.bun/vitest@2.1.9_7700f9e9ace41f23/node_modules/vitest/dist/chunks/vi.DgezovHB.mjs.map +1 -0
  174. package/dist/providers/base.d.mts +2 -2
  175. package/dist/providers/base.d.mts.map +1 -1
  176. package/dist/providers/openai-provider.d.mts.map +1 -1
  177. package/dist/providers/openai-provider.mjs +10 -9
  178. package/dist/providers/openai-provider.mjs.map +1 -1
  179. package/dist/providers/registry.d.mts +1 -1
  180. package/dist/providers/registry.d.mts.map +1 -1
  181. package/dist/providers/registry.mjs +99 -99
  182. package/dist/providers/registry.mjs.map +1 -1
  183. package/dist/session/manager.d.mts +2 -2
  184. package/dist/session/manager.d.mts.map +1 -1
  185. package/dist/session/manager.mjs +18 -19
  186. package/dist/session/manager.mjs.map +1 -1
  187. package/dist/utils/helpers.d.mts.map +1 -1
  188. package/dist/utils/helpers.mjs.map +1 -1
  189. package/package.json +11 -11
  190. package/skills/cron/SKILL.md +12 -8
  191. package/skills/daily-summary/SKILL.md +4 -0
  192. package/skills/english/SKILL.md +21 -7
  193. package/skills/expense/SKILL.md +11 -7
  194. package/skills/fortune/SKILL.md +24 -20
  195. package/skills/habit/SKILL.md +2 -1
  196. package/skills/hydration/SKILL.md +3 -0
  197. package/skills/memory/SKILL.md +1 -0
  198. package/skills/mood/SKILL.md +10 -6
  199. package/skills/skill-creator/SKILL.md +3 -0
  200. package/skills/summarize/SKILL.md +1 -0
  201. package/skills/weather/SKILL.md +10 -8
  202. package/dist/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.mjs.map +0 -1
  203. package/dist/node_modules/@vitest/expect/dist/index.mjs.map +0 -1
  204. package/dist/node_modules/@vitest/pretty-format/dist/index.mjs.map +0 -1
  205. package/dist/node_modules/@vitest/runner/dist/chunk-tasks.mjs.map +0 -1
  206. package/dist/node_modules/@vitest/runner/dist/index.mjs.map +0 -1
  207. package/dist/node_modules/@vitest/snapshot/dist/index.mjs.map +0 -1
  208. package/dist/node_modules/@vitest/snapshot/node_modules/pathe/dist/shared/pathe.ff20891b.mjs.map +0 -1
  209. package/dist/node_modules/@vitest/spy/dist/index.mjs.map +0 -1
  210. package/dist/node_modules/@vitest/utils/dist/chunk-_commonjsHelpers.mjs.map +0 -1
  211. package/dist/node_modules/@vitest/utils/dist/diff.mjs.map +0 -1
  212. package/dist/node_modules/@vitest/utils/dist/error.mjs.map +0 -1
  213. package/dist/node_modules/@vitest/utils/dist/helpers.mjs.map +0 -1
  214. package/dist/node_modules/@vitest/utils/dist/index.mjs.map +0 -1
  215. package/dist/node_modules/@vitest/utils/dist/source-map.mjs.map +0 -1
  216. package/dist/node_modules/chai/index.mjs.map +0 -1
  217. package/dist/node_modules/loupe/lib/arguments.mjs.map +0 -1
  218. package/dist/node_modules/loupe/lib/array.mjs.map +0 -1
  219. package/dist/node_modules/loupe/lib/bigint.mjs.map +0 -1
  220. package/dist/node_modules/loupe/lib/class.mjs.map +0 -1
  221. package/dist/node_modules/loupe/lib/date.mjs.map +0 -1
  222. package/dist/node_modules/loupe/lib/error.mjs.map +0 -1
  223. package/dist/node_modules/loupe/lib/function.mjs.map +0 -1
  224. package/dist/node_modules/loupe/lib/helpers.mjs.map +0 -1
  225. package/dist/node_modules/loupe/lib/html.mjs.map +0 -1
  226. package/dist/node_modules/loupe/lib/index.mjs.map +0 -1
  227. package/dist/node_modules/loupe/lib/map.mjs.map +0 -1
  228. package/dist/node_modules/loupe/lib/number.mjs.map +0 -1
  229. package/dist/node_modules/loupe/lib/object.mjs.map +0 -1
  230. package/dist/node_modules/loupe/lib/promise.mjs +0 -6
  231. package/dist/node_modules/loupe/lib/promise.mjs.map +0 -1
  232. package/dist/node_modules/loupe/lib/regexp.mjs.map +0 -1
  233. package/dist/node_modules/loupe/lib/set.mjs.map +0 -1
  234. package/dist/node_modules/loupe/lib/string.mjs.map +0 -1
  235. package/dist/node_modules/loupe/lib/symbol.mjs.map +0 -1
  236. package/dist/node_modules/loupe/lib/typedarray.mjs.map +0 -1
  237. package/dist/node_modules/magic-string/dist/magic-string.es.mjs.map +0 -1
  238. package/dist/node_modules/tinyrainbow/dist/chunk-BVHSVHOK.mjs.map +0 -1
  239. package/dist/node_modules/tinyrainbow/dist/node.mjs.map +0 -1
  240. package/dist/node_modules/tinyspy/dist/index.mjs.map +0 -1
  241. package/dist/node_modules/vitest/dist/chunks/_commonjsHelpers.BFTU3MAI.mjs.map +0 -1
  242. package/dist/node_modules/vitest/dist/chunks/date.W2xKR2qe.mjs.map +0 -1
  243. package/dist/node_modules/vitest/dist/chunks/utils.C8RiOc4B.mjs.map +0 -1
  244. package/dist/node_modules/vitest/dist/chunks/vi.DgezovHB.mjs.map +0 -1
  245. /package/dist/node_modules/{@vitest → .bun/@vitest_runner@2.1.9/node_modules/@vitest}/runner/dist/utils.mjs +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"line.test.mjs","names":[],"sources":["../../src/channels/line.test.ts"],"sourcesContent":["import { describe, it, expect } from \"vitest\";\nimport { LineChannel } from \"./line.js\";\nimport { MessageBus } from \"../bus/queue.js\";\n\n// Construct a minimal LineChannel for testing private methods\nconst bus = new MessageBus();\nconst channel = new LineChannel(\n { enabled: false, channelSecret: \"test\", channelAccessToken: \"test\", allowFrom: [] },\n bus,\n);\n\n// Access private methods via any\nconst ch = channel as unknown as {\n parseMessage(text: string): Array<{ type: string; text?: string; altText?: string; contents?: unknown }>;\n findJsonEnd(str: string): number;\n extractAltText(contents: Record<string, unknown>): string;\n returnFlexMessage(message: string): Array<{ type: string; altText?: string; contents?: unknown }>;\n};\n\n// ---------------------------------------------------------------------------\n// findJsonEnd\n// ---------------------------------------------------------------------------\ndescribe(\"findJsonEnd\", () => {\n it(\"finds end of simple object\", () => {\n expect(ch.findJsonEnd('{\"a\":1}')).toBe(7);\n });\n\n it(\"finds end of nested object\", () => {\n const json = '{\"a\":{\"b\":{\"c\":1}}}';\n expect(ch.findJsonEnd(json)).toBe(json.length);\n });\n\n it(\"finds end with trailing text\", () => {\n const str = '{\"type\":\"bubble\"} some trailing text';\n expect(ch.findJsonEnd(str)).toBe(17);\n });\n\n it(\"handles strings with escaped quotes\", () => {\n const json = '{\"text\":\"say \\\\\"hello\\\\\"\"}';\n expect(ch.findJsonEnd(json)).toBe(json.length);\n });\n\n it(\"handles strings with braces inside\", () => {\n const json = '{\"text\":\"{ not a real brace }\"}';\n expect(ch.findJsonEnd(json)).toBe(json.length);\n });\n\n it(\"handles escaped backslash before quote\", () => {\n // The string value is: path\\\\ (backslash is escaped, quote is not)\n const json = '{\"path\":\"C:\\\\\\\\\"}';\n expect(ch.findJsonEnd(json)).toBe(json.length);\n });\n\n it(\"returns -1 for non-object string\", () => {\n expect(ch.findJsonEnd(\"hello\")).toBe(-1);\n });\n\n it(\"returns -1 for unclosed object\", () => {\n expect(ch.findJsonEnd('{\"a\":1')).toBe(-1);\n });\n\n it(\"handles empty object\", () => {\n expect(ch.findJsonEnd(\"{}\")).toBe(2);\n });\n\n it(\"handles arrays inside objects\", () => {\n const json = '{\"items\":[1,2,3]}';\n expect(ch.findJsonEnd(json)).toBe(json.length);\n });\n});\n\n// ---------------------------------------------------------------------------\n// extractAltText\n// ---------------------------------------------------------------------------\ndescribe(\"extractAltText\", () => {\n it(\"extracts text from a simple bubble\", () => {\n const result = ch.extractAltText({\n type: \"bubble\",\n body: { type: \"box\", contents: [{ type: \"text\", text: \"Hello World\" }] },\n });\n expect(result).toBe(\"Hello World\");\n });\n\n it(\"extracts title when present\", () => {\n const result = ch.extractAltText({\n type: \"bubble\",\n title: \"Card Title\",\n body: { type: \"box\", contents: [] },\n });\n expect(result).toBe(\"Card Title\");\n });\n\n it(\"falls back to 'Flex Message' when no text found\", () => {\n const result = ch.extractAltText({ type: \"bubble\" });\n expect(result).toBe(\"Flex Message\");\n });\n\n it(\"truncates long text to 100 chars\", () => {\n const longText = \"x\".repeat(200);\n const result = ch.extractAltText({\n type: \"bubble\",\n body: { type: \"box\", contents: [{ type: \"text\", text: longText }] },\n });\n expect(result.length).toBe(100);\n });\n});\n\n// ---------------------------------------------------------------------------\n// parseMessage\n// ---------------------------------------------------------------------------\ndescribe(\"parseMessage\", () => {\n it(\"returns plain text for normal messages\", () => {\n const result = ch.parseMessage(\"Hello, world!\");\n expect(result).toHaveLength(1);\n expect(result[0].type).toBe(\"text\");\n expect(result[0].text).toBe(\"Hello, world!\");\n });\n\n it(\"returns (empty) for empty string\", () => {\n const result = ch.parseMessage(\"\");\n expect(result).toHaveLength(1);\n expect(result[0].text).toBe(\"(empty)\");\n });\n\n it(\"parses pure JSON flex message\", () => {\n const flex = JSON.stringify({\n type: \"bubble\",\n body: { type: \"box\", layout: \"vertical\", contents: [{ type: \"text\", text: \"Test\" }] },\n });\n const result = ch.parseMessage(flex);\n expect(result).toHaveLength(1);\n expect(result[0].type).toBe(\"flex\");\n expect(result[0].altText).toBe(\"Test\");\n });\n\n it(\"handles JSON with trailing text\", () => {\n const flex = JSON.stringify({\n type: \"bubble\",\n body: { type: \"box\", layout: \"vertical\", contents: [{ type: \"text\", text: \"Card\" }] },\n });\n const input = flex + \"\\n\\nHere is some extra text!\";\n const result = ch.parseMessage(input);\n expect(result).toHaveLength(2);\n expect(result[0].type).toBe(\"flex\");\n expect(result[1].type).toBe(\"text\");\n expect(result[1].text).toBe(\"Here is some extra text!\");\n });\n\n it(\"handles JSON with Japanese trailing text\", () => {\n const flex = JSON.stringify({\n type: \"bubble\",\n body: { type: \"box\", layout: \"vertical\", contents: [{ type: \"text\", text: \"運勢\" }] },\n });\n const input = flex + \"\\n\\n記録できた!今日もがんばろう。\";\n const result = ch.parseMessage(input);\n expect(result).toHaveLength(2);\n expect(result[0].type).toBe(\"flex\");\n expect(result[1].type).toBe(\"text\");\n expect(result[1].text).toBe(\"記録できた!今日もがんばろう。\");\n });\n\n it(\"handles whitespace around JSON\", () => {\n const flex = JSON.stringify({ type: \"bubble\", body: { type: \"box\", contents: [{ type: \"text\", text: \"OK\" }] } });\n const result = ch.parseMessage(\" \" + flex + \" \");\n expect(result).toHaveLength(1);\n expect(result[0].type).toBe(\"flex\");\n });\n\n it(\"falls back to text for invalid JSON starting with {\", () => {\n const result = ch.parseMessage(\"{not valid json at all}\");\n expect(result).toHaveLength(1);\n expect(result[0].type).toBe(\"text\");\n expect(result[0].text).toBe(\"{not valid json at all}\");\n });\n\n it(\"falls back to text for non-JSON curly brace text\", () => {\n const result = ch.parseMessage(\"{incomplete json\");\n expect(result).toHaveLength(1);\n expect(result[0].type).toBe(\"text\");\n });\n\n it(\"splits flex JSON with prefix and suffix text\", () => {\n const input = `完成!こんな感じ:{\"type\":\"bubble\",\"body\":{\"type\":\"box\",\"layout\":\"vertical\",\"contents\":[{\"type\":\"box\",\"layout\":\"vertical\",\"contents\":[{\"type\": \"text\",\"text\":\"🌱 水やり記録\",\"size\": \"xl\" ,\"weight\":\"bold\"},{\"type\": \"text\",\"text\":\"前回: 今日\\\\n植物たち元気?\", \"color\":\"#666666\",\"margin\":\"md\"}]},{\"type\": \"separator\",\"margin\":\"lg\"},{\"type\": \"box\",\"layout\":\"horizontal\",\"margin\":\"md\",\"spacing\":\"md\",\"contents\":[{\"type\": \"button\",\"style\": \"primary\",\"color\": \"#4CAF50\",\"action\":{\"type\": \"message\",\"label\": \"水やりした!\", \"text\": \"水やりした\"}},{\"type\": \"button\",\"style\": \"secondary\",\"action\":{\"type\": \"message\",\"label\": \"最後にいつ?\", \"text\": \"水やり確認\"}}]}]}}\\n\\n記録できた!今日水やりしたことになってる。緑色のボタンがカワイイでしょ。`;\n const result = ch.parseMessage(input);\n expect(result).toHaveLength(3);\n expect(result[0].type).toBe(\"text\");\n expect(result[0].text).toBe(\"完成!こんな感じ:\");\n expect(result[1].type).toBe(\"flex\");\n expect(result[1].altText).toBe(\"🌱 水やり記録\");\n expect(result[2].type).toBe(\"text\");\n expect(result[2].text).toBe(\"記録できた!今日水やりしたことになってる。緑色のボタンがカワイイでしょ。\");\n });\n\n it(\"splits real-world flex JSON with trailing Japanese text\", () => {\n const input = `{\"type\":\"bubble\",\"body\":{\"type\":\"box\",\"layout\":\"vertical\",\"contents\":[{\"type\":\"box\",\"layout\":\"vertical\",\"contents\":[{\"type\": \"text\",\"text\":\"🌱 水やり記録\",\"size\": \"xl\" ,\"weight\":\"bold\"},{\"type\": \"text\",\"text\":\"前回: 今日\\\\n植物たち元気?\", \"color\":\"#666666\",\"margin\":\"md\"}]},{\"type\": \"separator\",\"margin\":\"lg\"},{\"type\": \"box\",\"layout\":\"horizontal\",\"margin\":\"md\",\"spacing\":\"md\",\"contents\":[{\"type\": \"button\",\"style\": \"primary\",\"color\": \"#4CAF50\",\"action\":{\"type\": \"message\",\"label\": \"水やりした!\", \"text\": \"水やりした\"}},{\"type\": \"button\",\"style\": \"secondary\",\"action\":{\"type\": \"message\",\"label\": \"最後にいつ?\", \"text\": \"水やり確認\"}}]}]}}\\n\\n記録できた!今日水やりしたことになってる。緑色のボタンがカワイイでしょ。`;\n const result = ch.parseMessage(input);\n expect(result).toHaveLength(2);\n expect(result[0].type).toBe(\"flex\");\n expect(result[1].type).toBe(\"text\");\n expect(result[1].text).toBe(\"記録できた!今日水やりしたことになってる。緑色のボタンがカワイイでしょ。\");\n });\n});\n\n// ---------------------------------------------------------------------------\n// verifyLineSignature (exported function)\n// ---------------------------------------------------------------------------\nimport { verifyLineSignature } from \"./line.js\";\n\ndescribe(\"verifyLineSignature\", () => {\n it(\"returns true for valid signature\", () => {\n const secret = \"test-secret\";\n const body = '{\"events\":[]}';\n // Compute the expected signature\n const { createHmac } = require(\"node:crypto\");\n const expected = createHmac(\"sha256\", secret).update(body).digest(\"base64\");\n expect(verifyLineSignature(secret, body, expected)).toBe(true);\n });\n\n it(\"returns false for invalid signature\", () => {\n expect(verifyLineSignature(\"secret\", \"body\", \"bad-sig\")).toBe(false);\n });\n\n it(\"returns false for empty signature\", () => {\n expect(verifyLineSignature(\"secret\", \"body\", \"\")).toBe(false);\n });\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"line.test.mjs","names":[],"sources":["../../src/channels/line.test.ts"],"sourcesContent":["import { createHmac } from \"node:crypto\";\nimport { describe, it, expect } from \"vitest\";\n\nimport { MessageBus } from \"../bus/queue.js\";\nimport { LineChannel, verifyLineSignature } from \"./line.js\";\n\n// Construct a minimal LineChannel for testing private methods\nconst bus = new MessageBus();\nconst channel = new LineChannel(\n {\n allowFrom: [],\n channelAccessToken: \"test\",\n channelSecret: \"test\",\n enabled: false,\n },\n bus\n);\n\n// Access private methods via any\nconst ch = channel as unknown as {\n parseMessage(text: string): {\n type: string;\n text?: string;\n altText?: string;\n contents?: unknown;\n }[];\n findJsonEnd(str: string): number;\n extractAltText(contents: Record<string, unknown>): string;\n returnFlexMessage(\n message: string\n ): { type: string; altText?: string; contents?: unknown }[];\n};\n\n// ---------------------------------------------------------------------------\n// findJsonEnd\n// ---------------------------------------------------------------------------\ndescribe(\"findJsonEnd\", () => {\n it(\"finds end of simple object\", () => {\n expect(ch.findJsonEnd('{\"a\":1}')).toBe(7);\n });\n\n it(\"finds end of nested object\", () => {\n const json = '{\"a\":{\"b\":{\"c\":1}}}';\n expect(ch.findJsonEnd(json)).toBe(json.length);\n });\n\n it(\"finds end with trailing text\", () => {\n const str = '{\"type\":\"bubble\"} some trailing text';\n expect(ch.findJsonEnd(str)).toBe(17);\n });\n\n it(\"handles strings with escaped quotes\", () => {\n const json = '{\"text\":\"say \\\\\"hello\\\\\"\"}';\n expect(ch.findJsonEnd(json)).toBe(json.length);\n });\n\n it(\"handles strings with braces inside\", () => {\n const json = '{\"text\":\"{ not a real brace }\"}';\n expect(ch.findJsonEnd(json)).toBe(json.length);\n });\n\n it(\"handles escaped backslash before quote\", () => {\n // The string value is: path\\\\ (backslash is escaped, quote is not)\n const json = '{\"path\":\"C:\\\\\\\\\"}';\n expect(ch.findJsonEnd(json)).toBe(json.length);\n });\n\n it(\"returns -1 for non-object string\", () => {\n expect(ch.findJsonEnd(\"hello\")).toBe(-1);\n });\n\n it(\"returns -1 for unclosed object\", () => {\n expect(ch.findJsonEnd('{\"a\":1')).toBe(-1);\n });\n\n it(\"handles empty object\", () => {\n expect(ch.findJsonEnd(\"{}\")).toBe(2);\n });\n\n it(\"handles arrays inside objects\", () => {\n const json = '{\"items\":[1,2,3]}';\n expect(ch.findJsonEnd(json)).toBe(json.length);\n });\n});\n\n// ---------------------------------------------------------------------------\n// extractAltText\n// ---------------------------------------------------------------------------\ndescribe(\"extractAltText\", () => {\n it(\"extracts text from a simple bubble\", () => {\n const result = ch.extractAltText({\n body: { type: \"box\", contents: [{ type: \"text\", text: \"Hello World\" }] },\n type: \"bubble\",\n });\n expect(result).toBe(\"Hello World\");\n });\n\n it(\"extracts title when present\", () => {\n const result = ch.extractAltText({\n body: { type: \"box\", contents: [] },\n title: \"Card Title\",\n type: \"bubble\",\n });\n expect(result).toBe(\"Card Title\");\n });\n\n it(\"falls back to 'Flex Message' when no text found\", () => {\n const result = ch.extractAltText({ type: \"bubble\" });\n expect(result).toBe(\"Flex Message\");\n });\n\n it(\"truncates long text to 100 chars\", () => {\n const longText = \"x\".repeat(200);\n const result = ch.extractAltText({\n body: { type: \"box\", contents: [{ type: \"text\", text: longText }] },\n type: \"bubble\",\n });\n expect(result).toHaveLength(100);\n });\n});\n\n// ---------------------------------------------------------------------------\n// parseMessage\n// ---------------------------------------------------------------------------\ndescribe(\"parseMessage\", () => {\n it(\"returns plain text for normal messages\", () => {\n const result = ch.parseMessage(\"Hello, world!\");\n expect(result).toHaveLength(1);\n expect(result[0].type).toBe(\"text\");\n expect(result[0].text).toBe(\"Hello, world!\");\n });\n\n it(\"returns (empty) for empty string\", () => {\n const result = ch.parseMessage(\"\");\n expect(result).toHaveLength(1);\n expect(result[0].text).toBe(\"(empty)\");\n });\n\n it(\"parses pure JSON flex message\", () => {\n const flex = JSON.stringify({\n body: {\n type: \"box\",\n layout: \"vertical\",\n contents: [{ type: \"text\", text: \"Test\" }],\n },\n type: \"bubble\",\n });\n const result = ch.parseMessage(flex);\n expect(result).toHaveLength(1);\n expect(result[0].type).toBe(\"flex\");\n expect(result[0].altText).toBe(\"Test\");\n });\n\n it(\"handles JSON with trailing text\", () => {\n const flex = JSON.stringify({\n body: {\n type: \"box\",\n layout: \"vertical\",\n contents: [{ type: \"text\", text: \"Card\" }],\n },\n type: \"bubble\",\n });\n const input = flex + \"\\n\\nHere is some extra text!\";\n const result = ch.parseMessage(input);\n expect(result).toHaveLength(2);\n expect(result[0].type).toBe(\"flex\");\n expect(result[1].type).toBe(\"text\");\n expect(result[1].text).toBe(\"Here is some extra text!\");\n });\n\n it(\"handles JSON with Japanese trailing text\", () => {\n const flex = JSON.stringify({\n body: {\n type: \"box\",\n layout: \"vertical\",\n contents: [{ type: \"text\", text: \"運勢\" }],\n },\n type: \"bubble\",\n });\n const input = flex + \"\\n\\n記録できた!今日もがんばろう。\";\n const result = ch.parseMessage(input);\n expect(result).toHaveLength(2);\n expect(result[0].type).toBe(\"flex\");\n expect(result[1].type).toBe(\"text\");\n expect(result[1].text).toBe(\"記録できた!今日もがんばろう。\");\n });\n\n it(\"handles whitespace around JSON\", () => {\n const flex = JSON.stringify({\n body: { type: \"box\", contents: [{ type: \"text\", text: \"OK\" }] },\n type: \"bubble\",\n });\n const result = ch.parseMessage(\" \" + flex + \" \");\n expect(result).toHaveLength(1);\n expect(result[0].type).toBe(\"flex\");\n });\n\n it(\"falls back to text for invalid JSON starting with {\", () => {\n const result = ch.parseMessage(\"{not valid json at all}\");\n expect(result).toHaveLength(1);\n expect(result[0].type).toBe(\"text\");\n expect(result[0].text).toBe(\"{not valid json at all}\");\n });\n\n it(\"falls back to text for non-JSON curly brace text\", () => {\n const result = ch.parseMessage(\"{incomplete json\");\n expect(result).toHaveLength(1);\n expect(result[0].type).toBe(\"text\");\n });\n\n it(\"splits flex JSON with prefix and suffix text\", () => {\n const input = `完成!こんな感じ:{\"type\":\"bubble\",\"body\":{\"type\":\"box\",\"layout\":\"vertical\",\"contents\":[{\"type\":\"box\",\"layout\":\"vertical\",\"contents\":[{\"type\": \"text\",\"text\":\"🌱 水やり記録\",\"size\": \"xl\" ,\"weight\":\"bold\"},{\"type\": \"text\",\"text\":\"前回: 今日\\\\n植物たち元気?\", \"color\":\"#666666\",\"margin\":\"md\"}]},{\"type\": \"separator\",\"margin\":\"lg\"},{\"type\": \"box\",\"layout\":\"horizontal\",\"margin\":\"md\",\"spacing\":\"md\",\"contents\":[{\"type\": \"button\",\"style\": \"primary\",\"color\": \"#4CAF50\",\"action\":{\"type\": \"message\",\"label\": \"水やりした!\", \"text\": \"水やりした\"}},{\"type\": \"button\",\"style\": \"secondary\",\"action\":{\"type\": \"message\",\"label\": \"最後にいつ?\", \"text\": \"水やり確認\"}}]}]}}\\n\\n記録できた!今日水やりしたことになってる。緑色のボタンがカワイイでしょ。`;\n const result = ch.parseMessage(input);\n expect(result).toHaveLength(3);\n expect(result[0].type).toBe(\"text\");\n expect(result[0].text).toBe(\"完成!こんな感じ:\");\n expect(result[1].type).toBe(\"flex\");\n expect(result[1].altText).toBe(\"🌱 水やり記録\");\n expect(result[2].type).toBe(\"text\");\n expect(result[2].text).toBe(\n \"記録できた!今日水やりしたことになってる。緑色のボタンがカワイイでしょ。\"\n );\n });\n\n it(\"splits real-world flex JSON with trailing Japanese text\", () => {\n const input = `{\"type\":\"bubble\",\"body\":{\"type\":\"box\",\"layout\":\"vertical\",\"contents\":[{\"type\":\"box\",\"layout\":\"vertical\",\"contents\":[{\"type\": \"text\",\"text\":\"🌱 水やり記録\",\"size\": \"xl\" ,\"weight\":\"bold\"},{\"type\": \"text\",\"text\":\"前回: 今日\\\\n植物たち元気?\", \"color\":\"#666666\",\"margin\":\"md\"}]},{\"type\": \"separator\",\"margin\":\"lg\"},{\"type\": \"box\",\"layout\":\"horizontal\",\"margin\":\"md\",\"spacing\":\"md\",\"contents\":[{\"type\": \"button\",\"style\": \"primary\",\"color\": \"#4CAF50\",\"action\":{\"type\": \"message\",\"label\": \"水やりした!\", \"text\": \"水やりした\"}},{\"type\": \"button\",\"style\": \"secondary\",\"action\":{\"type\": \"message\",\"label\": \"最後にいつ?\", \"text\": \"水やり確認\"}}]}]}}\\n\\n記録できた!今日水やりしたことになってる。緑色のボタンがカワイイでしょ。`;\n const result = ch.parseMessage(input);\n expect(result).toHaveLength(2);\n expect(result[0].type).toBe(\"flex\");\n expect(result[1].type).toBe(\"text\");\n expect(result[1].text).toBe(\n \"記録できた!今日水やりしたことになってる。緑色のボタンがカワイイでしょ。\"\n );\n });\n});\n\n// ---------------------------------------------------------------------------\n// verifyLineSignature (exported function)\n// ---------------------------------------------------------------------------\ndescribe(\"verifyLineSignature works\", () => {\n it(\"returns true for valid signature\", () => {\n const secret = \"test-secret\";\n const body = '{\"events\":[]}';\n // Compute the expected signature\n const expected = createHmac(\"sha256\", secret).update(body).digest(\"base64\");\n expect(verifyLineSignature(secret, body, expected)).toBeTruthy();\n });\n\n it(\"returns false for invalid signature\", () => {\n expect(verifyLineSignature(\"secret\", \"body\", \"bad-sig\")).toBeFalsy();\n });\n\n it(\"returns false for empty signature\", () => {\n expect(verifyLineSignature(\"secret\", \"body\", \"\")).toBeFalsy();\n });\n});\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"manager.d.mts","names":[],"sources":["../../src/channels/manager.ts"],"mappings":";;;;;;;AAQA;cAAa,cAAA;EAAA,QACH,MAAA;EAAA,QACA,GAAA;EAAA,SACC,QAAA,EAAQ,GAAA,SAAA,WAAA;EAAA,QACT,aAAA;cAEI,MAAA,EAAQ,MAAA,EAAQ,GAAA,EAAK,UAAA;EAAA,QAKnB,YAAA;EAuCI;EALZ,IAAA,CAAA,GAAQ,OAAA;EAuEY;EAlEpB,QAAA,CAAA,GAAY,OAAA;EAsEC;EA5Cb,OAAA,CAAA,GAAW,OAAA;EAAA,QAkBH,gBAAA;EAsBd,UAAA,CAAW,IAAA,WAAe,WAAA;EAI1B,SAAA,CAAA,GAAa,MAAA;IAAiB,OAAA;IAAkB,OAAA;EAAA;EAAA,IAQ5C,eAAA,CAAA;AAAA"}
1
+ {"version":3,"file":"manager.d.mts","names":[],"sources":["../../src/channels/manager.ts"],"mappings":";;;;;;;AASA;cAAa,cAAA;EAAA,QACH,MAAA;EAAA,QACA,GAAA;EAAA,SACC,QAAA,EAAQ,GAAA,SAAA,WAAA;EAAA,QACT,aAAA;cAEI,MAAA,EAAQ,MAAA,EAAQ,GAAA,EAAK,UAAA;EAAA,QAKnB,YAAA;EAuCI;EALZ,IAAA,CAAA,GAAQ,OAAA;EAuEY;EAlEpB,QAAA,CAAA,GAAY,OAAA;EAsEC;EA5Cb,OAAA,CAAA,GAAW,OAAA;EAAA,QAkBH,gBAAA;EAsBd,UAAA,CAAW,IAAA,WAAe,WAAA;EAI1B,SAAA,CAAA,GAAa,MAAA;IAAiB,OAAA;IAAkB,OAAA;EAAA;EAAA,IAQ5C,eAAA,CAAA;AAAA"}
@@ -19,16 +19,16 @@ var ChannelManager = class {
19
19
  const channel = new TelegramChannel(this.config.channels.telegram, this.bus);
20
20
  this.channels.set("telegram", channel);
21
21
  console.log("Telegram channel enabled");
22
- } catch (err) {
23
- console.warn("Telegram channel not available:", err);
22
+ } catch (error) {
23
+ console.warn("Telegram channel not available:", error);
24
24
  }
25
25
  if (this.config.channels.line.enabled) try {
26
26
  const { LineChannel } = await import("./line.mjs");
27
27
  const channel = new LineChannel(this.config.channels.line, this.bus, getConfigWorkspacePath(this.config));
28
28
  this.channels.set("line", channel);
29
29
  console.log("LINE channel enabled (webhook mode)");
30
- } catch (err) {
31
- console.warn("LINE channel not available:", err);
30
+ } catch (error) {
31
+ console.warn("LINE channel not available:", error);
32
32
  }
33
33
  }
34
34
  /** Initialize channel instances (non-blocking). Call before startAll(). */
@@ -61,8 +61,8 @@ var ChannelManager = class {
61
61
  for (const [name, channel] of this.channels) try {
62
62
  await channel.stop();
63
63
  console.log(`Stopped ${name} channel`);
64
- } catch (err) {
65
- console.error(`Error stopping ${name}:`, err);
64
+ } catch (error) {
65
+ console.error(`Error stopping ${name}:`, error);
66
66
  }
67
67
  }
68
68
  async dispatchOutbound(signal) {
@@ -72,8 +72,8 @@ var ChannelManager = class {
72
72
  const channel = this.channels.get(msg.channel);
73
73
  if (channel) try {
74
74
  await channel.send(msg);
75
- } catch (err) {
76
- console.error(`Error sending to ${msg.channel}:`, err);
75
+ } catch (error) {
76
+ console.error(`Error sending to ${msg.channel}:`, error);
77
77
  }
78
78
  else console.warn(`Unknown channel: ${msg.channel}`);
79
79
  } catch {}
@@ -90,7 +90,7 @@ var ChannelManager = class {
90
90
  return status;
91
91
  }
92
92
  get enabledChannels() {
93
- return Array.from(this.channels.keys());
93
+ return [...this.channels.keys()];
94
94
  }
95
95
  };
96
96
 
@@ -1 +1 @@
1
- {"version":3,"file":"manager.mjs","names":[],"sources":["../../src/channels/manager.ts"],"sourcesContent":["import type { MessageBus } from \"../bus/queue.js\";\nimport type { BaseChannel } from \"./base.js\";\nimport type { Config } from \"../config/schema.js\";\nimport { getConfigWorkspacePath } from \"../config/schema.js\";\n\n/**\n * Manages chat channels and coordinates message routing.\n */\nexport class ChannelManager {\n private config: Config;\n private bus: MessageBus;\n readonly channels = new Map<string, BaseChannel>();\n private dispatchAbort: AbortController | null = null;\n\n constructor(config: Config, bus: MessageBus) {\n this.config = config;\n this.bus = bus;\n }\n\n private async initChannels(): Promise<void> {\n // Telegram channel\n if (this.config.channels.telegram.enabled) {\n try {\n const { TelegramChannel } = await import(\"./telegram.js\");\n const channel = new TelegramChannel(\n this.config.channels.telegram,\n this.bus,\n );\n this.channels.set(\"telegram\", channel);\n console.log(\"Telegram channel enabled\");\n } catch (err) {\n console.warn(\"Telegram channel not available:\", err);\n }\n }\n\n // LINE channel\n if (this.config.channels.line.enabled) {\n try {\n const { LineChannel } = await import(\"./line.js\");\n const channel = new LineChannel(\n this.config.channels.line,\n this.bus,\n getConfigWorkspacePath(this.config),\n );\n this.channels.set(\"line\", channel);\n console.log(\"LINE channel enabled (webhook mode)\");\n } catch (err) {\n console.warn(\"LINE channel not available:\", err);\n }\n }\n }\n\n /** Initialize channel instances (non-blocking). Call before startAll(). */\n async init(): Promise<void> {\n await this.initChannels();\n }\n\n /** Start all channels and the outbound dispatcher (blocks until stopped). */\n async startAll(): Promise<void> {\n // Init if not already done\n if (this.channels.size === 0) {\n await this.initChannels();\n }\n\n if (this.channels.size === 0) {\n console.warn(\"No channels enabled\");\n return;\n }\n\n // Start outbound dispatcher\n this.dispatchAbort = new AbortController();\n const dispatchPromise = this.dispatchOutbound(this.dispatchAbort.signal);\n\n // Start all channels\n const channelPromises: Promise<void>[] = [];\n for (const [name, channel] of this.channels) {\n console.log(`Starting ${name} channel...`);\n channelPromises.push(channel.start());\n }\n\n await Promise.all([dispatchPromise, ...channelPromises]);\n }\n\n /** Stop all channels and the dispatcher. */\n async stopAll(): Promise<void> {\n console.log(\"Stopping all channels...\");\n\n if (this.dispatchAbort) {\n this.dispatchAbort.abort();\n this.dispatchAbort = null;\n }\n\n for (const [name, channel] of this.channels) {\n try {\n await channel.stop();\n console.log(`Stopped ${name} channel`);\n } catch (err) {\n console.error(`Error stopping ${name}:`, err);\n }\n }\n }\n\n private async dispatchOutbound(signal: AbortSignal): Promise<void> {\n console.log(\"Outbound dispatcher started\");\n\n while (!signal.aborted) {\n try {\n const msg = await this.bus.consumeOutboundTimeout(1000);\n const channel = this.channels.get(msg.channel);\n if (channel) {\n try {\n await channel.send(msg);\n } catch (err) {\n console.error(`Error sending to ${msg.channel}:`, err);\n }\n } else {\n console.warn(`Unknown channel: ${msg.channel}`);\n }\n } catch {\n // timeout, continue\n }\n }\n }\n\n getChannel(name: string): BaseChannel | undefined {\n return this.channels.get(name);\n }\n\n getStatus(): Record<string, { enabled: boolean; running: boolean }> {\n const status: Record<string, { enabled: boolean; running: boolean }> = {};\n for (const [name, channel] of this.channels) {\n status[name] = { enabled: true, running: channel.isRunning };\n }\n return status;\n }\n\n get enabledChannels(): string[] {\n return Array.from(this.channels.keys());\n }\n}\n"],"mappings":";;;;;;AAQA,IAAa,iBAAb,MAA4B;CAC1B,AAAQ;CACR,AAAQ;CACR,AAAS,2BAAW,IAAI,KAA0B;CAClD,AAAQ,gBAAwC;CAEhD,YAAY,QAAgB,KAAiB;AAC3C,OAAK,SAAS;AACd,OAAK,MAAM;;CAGb,MAAc,eAA8B;AAE1C,MAAI,KAAK,OAAO,SAAS,SAAS,QAChC,KAAI;GACF,MAAM,EAAE,oBAAoB,MAAM,OAAO;GACzC,MAAM,UAAU,IAAI,gBAClB,KAAK,OAAO,SAAS,UACrB,KAAK,IACN;AACD,QAAK,SAAS,IAAI,YAAY,QAAQ;AACtC,WAAQ,IAAI,2BAA2B;WAChC,KAAK;AACZ,WAAQ,KAAK,mCAAmC,IAAI;;AAKxD,MAAI,KAAK,OAAO,SAAS,KAAK,QAC5B,KAAI;GACF,MAAM,EAAE,gBAAgB,MAAM,OAAO;GACrC,MAAM,UAAU,IAAI,YAClB,KAAK,OAAO,SAAS,MACrB,KAAK,KACL,uBAAuB,KAAK,OAAO,CACpC;AACD,QAAK,SAAS,IAAI,QAAQ,QAAQ;AAClC,WAAQ,IAAI,sCAAsC;WAC3C,KAAK;AACZ,WAAQ,KAAK,+BAA+B,IAAI;;;;CAMtD,MAAM,OAAsB;AAC1B,QAAM,KAAK,cAAc;;;CAI3B,MAAM,WAA0B;AAE9B,MAAI,KAAK,SAAS,SAAS,EACzB,OAAM,KAAK,cAAc;AAG3B,MAAI,KAAK,SAAS,SAAS,GAAG;AAC5B,WAAQ,KAAK,sBAAsB;AACnC;;AAIF,OAAK,gBAAgB,IAAI,iBAAiB;EAC1C,MAAM,kBAAkB,KAAK,iBAAiB,KAAK,cAAc,OAAO;EAGxE,MAAM,kBAAmC,EAAE;AAC3C,OAAK,MAAM,CAAC,MAAM,YAAY,KAAK,UAAU;AAC3C,WAAQ,IAAI,YAAY,KAAK,aAAa;AAC1C,mBAAgB,KAAK,QAAQ,OAAO,CAAC;;AAGvC,QAAM,QAAQ,IAAI,CAAC,iBAAiB,GAAG,gBAAgB,CAAC;;;CAI1D,MAAM,UAAyB;AAC7B,UAAQ,IAAI,2BAA2B;AAEvC,MAAI,KAAK,eAAe;AACtB,QAAK,cAAc,OAAO;AAC1B,QAAK,gBAAgB;;AAGvB,OAAK,MAAM,CAAC,MAAM,YAAY,KAAK,SACjC,KAAI;AACF,SAAM,QAAQ,MAAM;AACpB,WAAQ,IAAI,WAAW,KAAK,UAAU;WAC/B,KAAK;AACZ,WAAQ,MAAM,kBAAkB,KAAK,IAAI,IAAI;;;CAKnD,MAAc,iBAAiB,QAAoC;AACjE,UAAQ,IAAI,8BAA8B;AAE1C,SAAO,CAAC,OAAO,QACb,KAAI;GACF,MAAM,MAAM,MAAM,KAAK,IAAI,uBAAuB,IAAK;GACvD,MAAM,UAAU,KAAK,SAAS,IAAI,IAAI,QAAQ;AAC9C,OAAI,QACF,KAAI;AACF,UAAM,QAAQ,KAAK,IAAI;YAChB,KAAK;AACZ,YAAQ,MAAM,oBAAoB,IAAI,QAAQ,IAAI,IAAI;;OAGxD,SAAQ,KAAK,oBAAoB,IAAI,UAAU;UAE3C;;CAMZ,WAAW,MAAuC;AAChD,SAAO,KAAK,SAAS,IAAI,KAAK;;CAGhC,YAAoE;EAClE,MAAM,SAAiE,EAAE;AACzE,OAAK,MAAM,CAAC,MAAM,YAAY,KAAK,SACjC,QAAO,QAAQ;GAAE,SAAS;GAAM,SAAS,QAAQ;GAAW;AAE9D,SAAO;;CAGT,IAAI,kBAA4B;AAC9B,SAAO,MAAM,KAAK,KAAK,SAAS,MAAM,CAAC"}
1
+ {"version":3,"file":"manager.mjs","names":[],"sources":["../../src/channels/manager.ts"],"sourcesContent":["import type { MessageBus } from \"../bus/queue.js\";\nimport type { Config } from \"../config/schema.js\";\nimport type { BaseChannel } from \"./base.js\";\n\nimport { getConfigWorkspacePath } from \"../config/schema.js\";\n\n/**\n * Manages chat channels and coordinates message routing.\n */\nexport class ChannelManager {\n private config: Config;\n private bus: MessageBus;\n readonly channels = new Map<string, BaseChannel>();\n private dispatchAbort: AbortController | null = null;\n\n constructor(config: Config, bus: MessageBus) {\n this.config = config;\n this.bus = bus;\n }\n\n private async initChannels(): Promise<void> {\n // Telegram channel\n if (this.config.channels.telegram.enabled) {\n try {\n const { TelegramChannel } = await import(\"./telegram.js\");\n const channel = new TelegramChannel(\n this.config.channels.telegram,\n this.bus\n );\n this.channels.set(\"telegram\", channel);\n console.log(\"Telegram channel enabled\");\n } catch (error) {\n console.warn(\"Telegram channel not available:\", error);\n }\n }\n\n // LINE channel\n if (this.config.channels.line.enabled) {\n try {\n const { LineChannel } = await import(\"./line.js\");\n const channel = new LineChannel(\n this.config.channels.line,\n this.bus,\n getConfigWorkspacePath(this.config)\n );\n this.channels.set(\"line\", channel);\n console.log(\"LINE channel enabled (webhook mode)\");\n } catch (error) {\n console.warn(\"LINE channel not available:\", error);\n }\n }\n }\n\n /** Initialize channel instances (non-blocking). Call before startAll(). */\n async init(): Promise<void> {\n await this.initChannels();\n }\n\n /** Start all channels and the outbound dispatcher (blocks until stopped). */\n async startAll(): Promise<void> {\n // Init if not already done\n if (this.channels.size === 0) {\n await this.initChannels();\n }\n\n if (this.channels.size === 0) {\n console.warn(\"No channels enabled\");\n return;\n }\n\n // Start outbound dispatcher\n this.dispatchAbort = new AbortController();\n const dispatchPromise = this.dispatchOutbound(this.dispatchAbort.signal);\n\n // Start all channels\n const channelPromises: Promise<void>[] = [];\n for (const [name, channel] of this.channels) {\n console.log(`Starting ${name} channel...`);\n channelPromises.push(channel.start());\n }\n\n await Promise.all([dispatchPromise, ...channelPromises]);\n }\n\n /** Stop all channels and the dispatcher. */\n async stopAll(): Promise<void> {\n console.log(\"Stopping all channels...\");\n\n if (this.dispatchAbort) {\n this.dispatchAbort.abort();\n this.dispatchAbort = null;\n }\n\n for (const [name, channel] of this.channels) {\n try {\n await channel.stop();\n console.log(`Stopped ${name} channel`);\n } catch (error) {\n console.error(`Error stopping ${name}:`, error);\n }\n }\n }\n\n private async dispatchOutbound(signal: AbortSignal): Promise<void> {\n console.log(\"Outbound dispatcher started\");\n\n while (!signal.aborted) {\n try {\n const msg = await this.bus.consumeOutboundTimeout(1000);\n const channel = this.channels.get(msg.channel);\n if (channel) {\n try {\n await channel.send(msg);\n } catch (error) {\n console.error(`Error sending to ${msg.channel}:`, error);\n }\n } else {\n console.warn(`Unknown channel: ${msg.channel}`);\n }\n } catch {\n // timeout, continue\n }\n }\n }\n\n getChannel(name: string): BaseChannel | undefined {\n return this.channels.get(name);\n }\n\n getStatus(): Record<string, { enabled: boolean; running: boolean }> {\n const status: Record<string, { enabled: boolean; running: boolean }> = {};\n for (const [name, channel] of this.channels) {\n status[name] = { enabled: true, running: channel.isRunning };\n }\n return status;\n }\n\n get enabledChannels(): string[] {\n return [...this.channels.keys()];\n }\n}\n"],"mappings":";;;;;;AASA,IAAa,iBAAb,MAA4B;CAC1B,AAAQ;CACR,AAAQ;CACR,AAAS,2BAAW,IAAI,KAA0B;CAClD,AAAQ,gBAAwC;CAEhD,YAAY,QAAgB,KAAiB;AAC3C,OAAK,SAAS;AACd,OAAK,MAAM;;CAGb,MAAc,eAA8B;AAE1C,MAAI,KAAK,OAAO,SAAS,SAAS,QAChC,KAAI;GACF,MAAM,EAAE,oBAAoB,MAAM,OAAO;GACzC,MAAM,UAAU,IAAI,gBAClB,KAAK,OAAO,SAAS,UACrB,KAAK,IACN;AACD,QAAK,SAAS,IAAI,YAAY,QAAQ;AACtC,WAAQ,IAAI,2BAA2B;WAChC,OAAO;AACd,WAAQ,KAAK,mCAAmC,MAAM;;AAK1D,MAAI,KAAK,OAAO,SAAS,KAAK,QAC5B,KAAI;GACF,MAAM,EAAE,gBAAgB,MAAM,OAAO;GACrC,MAAM,UAAU,IAAI,YAClB,KAAK,OAAO,SAAS,MACrB,KAAK,KACL,uBAAuB,KAAK,OAAO,CACpC;AACD,QAAK,SAAS,IAAI,QAAQ,QAAQ;AAClC,WAAQ,IAAI,sCAAsC;WAC3C,OAAO;AACd,WAAQ,KAAK,+BAA+B,MAAM;;;;CAMxD,MAAM,OAAsB;AAC1B,QAAM,KAAK,cAAc;;;CAI3B,MAAM,WAA0B;AAE9B,MAAI,KAAK,SAAS,SAAS,EACzB,OAAM,KAAK,cAAc;AAG3B,MAAI,KAAK,SAAS,SAAS,GAAG;AAC5B,WAAQ,KAAK,sBAAsB;AACnC;;AAIF,OAAK,gBAAgB,IAAI,iBAAiB;EAC1C,MAAM,kBAAkB,KAAK,iBAAiB,KAAK,cAAc,OAAO;EAGxE,MAAM,kBAAmC,EAAE;AAC3C,OAAK,MAAM,CAAC,MAAM,YAAY,KAAK,UAAU;AAC3C,WAAQ,IAAI,YAAY,KAAK,aAAa;AAC1C,mBAAgB,KAAK,QAAQ,OAAO,CAAC;;AAGvC,QAAM,QAAQ,IAAI,CAAC,iBAAiB,GAAG,gBAAgB,CAAC;;;CAI1D,MAAM,UAAyB;AAC7B,UAAQ,IAAI,2BAA2B;AAEvC,MAAI,KAAK,eAAe;AACtB,QAAK,cAAc,OAAO;AAC1B,QAAK,gBAAgB;;AAGvB,OAAK,MAAM,CAAC,MAAM,YAAY,KAAK,SACjC,KAAI;AACF,SAAM,QAAQ,MAAM;AACpB,WAAQ,IAAI,WAAW,KAAK,UAAU;WAC/B,OAAO;AACd,WAAQ,MAAM,kBAAkB,KAAK,IAAI,MAAM;;;CAKrD,MAAc,iBAAiB,QAAoC;AACjE,UAAQ,IAAI,8BAA8B;AAE1C,SAAO,CAAC,OAAO,QACb,KAAI;GACF,MAAM,MAAM,MAAM,KAAK,IAAI,uBAAuB,IAAK;GACvD,MAAM,UAAU,KAAK,SAAS,IAAI,IAAI,QAAQ;AAC9C,OAAI,QACF,KAAI;AACF,UAAM,QAAQ,KAAK,IAAI;YAChB,OAAO;AACd,YAAQ,MAAM,oBAAoB,IAAI,QAAQ,IAAI,MAAM;;OAG1D,SAAQ,KAAK,oBAAoB,IAAI,UAAU;UAE3C;;CAMZ,WAAW,MAAuC;AAChD,SAAO,KAAK,SAAS,IAAI,KAAK;;CAGhC,YAAoE;EAClE,MAAM,SAAiE,EAAE;AACzE,OAAK,MAAM,CAAC,MAAM,YAAY,KAAK,SACjC,QAAO,QAAQ;GAAE,SAAS;GAAM,SAAS,QAAQ;GAAW;AAE9D,SAAO;;CAGT,IAAI,kBAA4B;AAC9B,SAAO,CAAC,GAAG,KAAK,SAAS,MAAM,CAAC"}
@@ -9,31 +9,31 @@ import { Bot } from "grammy";
9
9
  function markdownToTelegramHtml(text) {
10
10
  if (!text) return "";
11
11
  const codeBlocks = [];
12
- let result = text.replace(/```[\w]*\n?([\s\S]*?)```/g, (_m, code) => {
12
+ let result = text.replaceAll(/```[\w]*\n?([\s\S]*?)```/g, (_m, code) => {
13
13
  codeBlocks.push(code);
14
- return `\x00CB${codeBlocks.length - 1}\x00`;
14
+ return `\u0000CB${codeBlocks.length - 1}\u0000`;
15
15
  });
16
16
  const inlineCodes = [];
17
- result = result.replace(/`([^`]+)`/g, (_m, code) => {
17
+ result = result.replaceAll(/`([^`]+)`/g, (_m, code) => {
18
18
  inlineCodes.push(code);
19
- return `\x00IC${inlineCodes.length - 1}\x00`;
19
+ return `\u0000IC${inlineCodes.length - 1}\u0000`;
20
20
  });
21
- result = result.replace(/^#{1,6}\s+(.+)$/gm, "$1");
22
- result = result.replace(/^>\s*(.*)$/gm, "$1");
23
- result = result.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
24
- result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "<a href=\"$2\">$1</a>");
25
- result = result.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
26
- result = result.replace(/__(.+?)__/g, "<b>$1</b>");
27
- result = result.replace(/(?<![a-zA-Z0-9])_([^_]+)_(?![a-zA-Z0-9])/g, "<i>$1</i>");
28
- result = result.replace(/~~(.+?)~~/g, "<s>$1</s>");
29
- result = result.replace(/^[-*]\s+/gm, "• ");
30
- for (let i = 0; i < inlineCodes.length; i++) {
31
- const escaped = inlineCodes[i].replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
32
- result = result.replace(`\x00IC${i}\x00`, `<code>${escaped}</code>`);
21
+ result = result.replaceAll(/^#{1,6}\s+(.+)$/gm, "$1");
22
+ result = result.replaceAll(/^>\s*(.*)$/gm, "$1");
23
+ result = result.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
24
+ result = result.replaceAll(/\[([^\]]+)\]\(([^)]+)\)/g, "<a href=\"$2\">$1</a>");
25
+ result = result.replaceAll(/\*\*(.+?)\*\*/g, "<b>$1</b>");
26
+ result = result.replaceAll(/__(.+?)__/g, "<b>$1</b>");
27
+ result = result.replaceAll(/(?<![a-zA-Z0-9])_([^_]+)_(?![a-zA-Z0-9])/g, "<i>$1</i>");
28
+ result = result.replaceAll(/~~(.+?)~~/g, "<s>$1</s>");
29
+ result = result.replaceAll(/^[-*]\s+/gm, "• ");
30
+ for (let i = 0; i < inlineCodes.length; i += 1) {
31
+ const escaped = inlineCodes[i].replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
32
+ result = result.replace(`\u0000IC${i}\u0000`, `<code>${escaped}</code>`);
33
33
  }
34
- for (let i = 0; i < codeBlocks.length; i++) {
35
- const escaped = codeBlocks[i].replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
36
- result = result.replace(`\x00CB${i}\x00`, `<pre><code>${escaped}</code></pre>`);
34
+ for (let i = 0; i < codeBlocks.length; i += 1) {
35
+ const escaped = codeBlocks[i].replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
36
+ result = result.replace(`\u0000CB${i}\u0000`, `<pre><code>${escaped}</code></pre>`);
37
37
  }
38
38
  return result;
39
39
  }
@@ -72,20 +72,20 @@ var TelegramChannel = class extends BaseChannel {
72
72
  const me = await this.bot.api.getMe();
73
73
  console.log(`Telegram bot @${me.username} connected`);
74
74
  const maxRetries = 3;
75
- for (let attempt = 1; attempt <= maxRetries; attempt++) try {
75
+ for (let attempt = 1; attempt <= maxRetries; attempt += 1) try {
76
76
  await this.bot.start({
77
- drop_pending_updates: true,
78
- allowed_updates: ["message"]
77
+ allowed_updates: ["message"],
78
+ drop_pending_updates: true
79
79
  });
80
80
  return;
81
- } catch (err) {
82
- if (err instanceof Error && err.message.includes("409") && attempt < maxRetries && this._running) {
81
+ } catch (error) {
82
+ if (error instanceof Error && error.message.includes("409") && attempt < maxRetries && this._running) {
83
83
  const delay = attempt * 5;
84
84
  console.warn(`Telegram 409 conflict (attempt ${attempt}/${maxRetries}), retrying in ${delay}s...`);
85
85
  await new Promise((r) => setTimeout(r, delay * 1e3));
86
86
  continue;
87
87
  }
88
- throw err;
88
+ throw error;
89
89
  }
90
90
  }
91
91
  async stop() {
@@ -103,7 +103,7 @@ var TelegramChannel = class extends BaseChannel {
103
103
  }
104
104
  try {
105
105
  const chatId = Number(msg.chatId);
106
- if (isNaN(chatId)) {
106
+ if (Number.isNaN(chatId)) {
107
107
  console.error(`Invalid chat_id: ${msg.chatId}`);
108
108
  return;
109
109
  }
@@ -114,13 +114,13 @@ var TelegramChannel = class extends BaseChannel {
114
114
  console.warn("HTML parse failed, falling back to plain text");
115
115
  await this.bot.api.sendMessage(chatId, msg.content);
116
116
  }
117
- } catch (err) {
118
- console.error("Error sending Telegram message:", err);
117
+ } catch (error) {
118
+ console.error("Error sending Telegram message:", error);
119
119
  }
120
120
  }
121
121
  async onMessage(ctx) {
122
122
  const user = ctx.from;
123
- const message = ctx.message;
123
+ const { message } = ctx;
124
124
  if (!user || !message) return;
125
125
  const chatId = message.chat.id;
126
126
  let senderId = String(user.id);
@@ -130,20 +130,19 @@ var TelegramChannel = class extends BaseChannel {
130
130
  if (message.text) contentParts.push(message.text);
131
131
  if (message.caption) contentParts.push(message.caption);
132
132
  if (message.photo && message.photo.length > 0) {
133
- const photo = message.photo[message.photo.length - 1];
133
+ const photo = message.photo.at(-1);
134
134
  try {
135
- const file = await ctx.api.getFile(photo.file_id);
135
+ const file = await ctx.api.getFile(photo?.file_id ?? "");
136
136
  const mediaDir = join(homedir(), ".nanobot", "media");
137
137
  if (!existsSync(mediaDir)) mkdirSync(mediaDir, { recursive: true });
138
138
  contentParts.push(`[image: telegram file ${file.file_id}]`);
139
- } catch (err) {
139
+ } catch {
140
140
  contentParts.push("[image: download failed]");
141
141
  }
142
142
  }
143
143
  if (message.document) contentParts.push(`[file: ${message.document.file_name ?? message.document.file_id}]`);
144
144
  const content = contentParts.length > 0 ? contentParts.join("\n") : "[empty message]";
145
145
  await this.handleMessage({
146
- senderId,
147
146
  chatId: String(chatId),
148
147
  content,
149
148
  media: mediaPaths,
@@ -153,7 +152,8 @@ var TelegramChannel = class extends BaseChannel {
153
152
  username: user.username,
154
153
  firstName: user.first_name,
155
154
  isGroup: message.chat.type !== "private"
156
- }
155
+ },
156
+ senderId
157
157
  });
158
158
  }
159
159
  };
@@ -1 +1 @@
1
- {"version":3,"file":"telegram.mjs","names":[],"sources":["../../src/channels/telegram.ts"],"sourcesContent":["import { Bot, type Context } from \"grammy\";\nimport { mkdirSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport type { OutboundMessage } from \"../bus/events.js\";\nimport type { MessageBus } from \"../bus/queue.js\";\nimport { BaseChannel } from \"./base.js\";\nimport type { TelegramConfig } from \"../config/schema.js\";\n\n/** Convert markdown to Telegram-safe HTML. */\nfunction markdownToTelegramHtml(text: string): string {\n if (!text) return \"\";\n\n // 1. Extract and protect code blocks\n const codeBlocks: string[] = [];\n let result = text.replace(/```[\\w]*\\n?([\\s\\S]*?)```/g, (_m, code) => {\n codeBlocks.push(code);\n return `\\x00CB${codeBlocks.length - 1}\\x00`;\n });\n\n // 2. Extract and protect inline code\n const inlineCodes: string[] = [];\n result = result.replace(/`([^`]+)`/g, (_m, code) => {\n inlineCodes.push(code);\n return `\\x00IC${inlineCodes.length - 1}\\x00`;\n });\n\n // 3. Headers -> plain text\n result = result.replace(/^#{1,6}\\s+(.+)$/gm, \"$1\");\n\n // 4. Blockquotes -> plain text\n result = result.replace(/^>\\s*(.*)$/gm, \"$1\");\n\n // 5. Escape HTML\n result = result\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n\n // 6. Links\n result = result.replace(\n /\\[([^\\]]+)\\]\\(([^)]+)\\)/g,\n '<a href=\"$2\">$1</a>',\n );\n\n // 7. Bold\n result = result.replace(/\\*\\*(.+?)\\*\\*/g, \"<b>$1</b>\");\n result = result.replace(/__(.+?)__/g, \"<b>$1</b>\");\n\n // 8. Italic\n result = result.replace(\n /(?<![a-zA-Z0-9])_([^_]+)_(?![a-zA-Z0-9])/g,\n \"<i>$1</i>\",\n );\n\n // 9. Strikethrough\n result = result.replace(/~~(.+?)~~/g, \"<s>$1</s>\");\n\n // 10. Bullet lists\n result = result.replace(/^[-*]\\s+/gm, \"\\u2022 \");\n\n // 11. Restore inline code\n for (let i = 0; i < inlineCodes.length; i++) {\n const escaped = inlineCodes[i]\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n result = result.replace(\n `\\x00IC${i}\\x00`,\n `<code>${escaped}</code>`,\n );\n }\n\n // 12. Restore code blocks\n for (let i = 0; i < codeBlocks.length; i++) {\n const escaped = codeBlocks[i]\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n result = result.replace(\n `\\x00CB${i}\\x00`,\n `<pre><code>${escaped}</code></pre>`,\n );\n }\n\n return result;\n}\n\n/**\n * Telegram channel using grammY (long polling).\n */\nexport class TelegramChannel extends BaseChannel {\n readonly name = \"telegram\";\n private bot: Bot | null = null;\n private telegramConfig: TelegramConfig;\n\n constructor(config: TelegramConfig, bus: MessageBus) {\n super(config, bus);\n this.telegramConfig = config;\n }\n\n async start(): Promise<void> {\n if (!this.telegramConfig.token) {\n console.error(\"Telegram bot token not configured\");\n return;\n }\n\n this._running = true;\n this.bot = new Bot(this.telegramConfig.token);\n\n // Handle /start command\n this.bot.command(\"start\", async (ctx) => {\n const user = ctx.from;\n await ctx.reply(\n `Hi ${user?.first_name ?? \"there\"}! I'm nanobot.\\n\\nSend me a message and I'll respond!`,\n );\n });\n\n // Handle text messages\n this.bot.on(\"message:text\", async (ctx) => {\n await this.onMessage(ctx);\n });\n\n // Handle photos\n this.bot.on(\"message:photo\", async (ctx) => {\n await this.onMessage(ctx);\n });\n\n // Handle documents\n this.bot.on(\"message:document\", async (ctx) => {\n await this.onMessage(ctx);\n });\n\n console.log(\"Starting Telegram bot (polling mode)...\");\n\n const me = await this.bot.api.getMe();\n console.log(`Telegram bot @${me.username} connected`);\n\n // Start polling with retry — handles 409 Conflict when a stale\n // instance is still holding the long-poll connection.\n const maxRetries = 3;\n for (let attempt = 1; attempt <= maxRetries; attempt++) {\n try {\n await this.bot.start({\n drop_pending_updates: true,\n allowed_updates: [\"message\"],\n });\n return; // bot.start() blocks until stopped, so if we get here it was a clean stop\n } catch (err) {\n const is409 =\n err instanceof Error && err.message.includes(\"409\");\n if (is409 && attempt < maxRetries && this._running) {\n const delay = attempt * 5;\n console.warn(\n `Telegram 409 conflict (attempt ${attempt}/${maxRetries}), retrying in ${delay}s...`,\n );\n await new Promise((r) => setTimeout(r, delay * 1000));\n continue;\n }\n throw err;\n }\n }\n }\n\n async stop(): Promise<void> {\n this._running = false;\n if (this.bot) {\n console.log(\"Stopping Telegram bot...\");\n await this.bot.stop();\n this.bot = null;\n }\n }\n\n async send(msg: OutboundMessage): Promise<void> {\n if (!this.bot) {\n console.warn(\"Telegram bot not running\");\n return;\n }\n\n try {\n const chatId = Number(msg.chatId);\n if (isNaN(chatId)) {\n console.error(`Invalid chat_id: ${msg.chatId}`);\n return;\n }\n\n const htmlContent = markdownToTelegramHtml(msg.content);\n try {\n await this.bot.api.sendMessage(chatId, htmlContent, {\n parse_mode: \"HTML\",\n });\n } catch {\n // Fallback to plain text\n console.warn(\"HTML parse failed, falling back to plain text\");\n await this.bot.api.sendMessage(chatId, msg.content);\n }\n } catch (err) {\n console.error(\"Error sending Telegram message:\", err);\n }\n }\n\n private async onMessage(ctx: Context): Promise<void> {\n const user = ctx.from;\n const message = ctx.message;\n if (!user || !message) return;\n\n const chatId = message.chat.id;\n let senderId = String(user.id);\n if (user.username) {\n senderId = `${senderId}|${user.username}`;\n }\n\n const contentParts: string[] = [];\n const mediaPaths: string[] = [];\n\n // Text\n if (message.text) contentParts.push(message.text);\n if (message.caption) contentParts.push(message.caption);\n\n // Photos\n if (message.photo && message.photo.length > 0) {\n const photo = message.photo[message.photo.length - 1];\n try {\n const file = await ctx.api.getFile(photo.file_id);\n const mediaDir = join(homedir(), \".nanobot\", \"media\");\n if (!existsSync(mediaDir)) mkdirSync(mediaDir, { recursive: true });\n // Note: grammy doesn't have a built-in download_to_drive.\n // In WebContainer context, we just note the file_id.\n contentParts.push(`[image: telegram file ${file.file_id}]`);\n } catch (err) {\n contentParts.push(\"[image: download failed]\");\n }\n }\n\n // Documents\n if (message.document) {\n contentParts.push(\n `[file: ${message.document.file_name ?? message.document.file_id}]`,\n );\n }\n\n const content =\n contentParts.length > 0 ? contentParts.join(\"\\n\") : \"[empty message]\";\n\n await this.handleMessage({\n senderId,\n chatId: String(chatId),\n content,\n media: mediaPaths,\n metadata: {\n messageId: message.message_id,\n userId: user.id,\n username: user.username,\n firstName: user.first_name,\n isGroup: message.chat.type !== \"private\",\n },\n });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"telegram.mjs","names":[],"sources":["../../src/channels/telegram.ts"],"sourcesContent":["import type { Context } from \"grammy\";\n\nimport { Bot } from \"grammy\";\nimport { mkdirSync, existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nimport type { OutboundMessage } from \"../bus/events.js\";\nimport type { MessageBus } from \"../bus/queue.js\";\nimport type { TelegramConfig } from \"../config/schema.js\";\n\nimport { BaseChannel } from \"./base.js\";\n\n/** Convert markdown to Telegram-safe HTML. */\nfunction markdownToTelegramHtml(text: string): string {\n if (!text) {\n return \"\";\n }\n\n // 1. Extract and protect code blocks\n const codeBlocks: string[] = [];\n let result = text.replaceAll(/```[\\w]*\\n?([\\s\\S]*?)```/g, (_m, code) => {\n codeBlocks.push(code);\n return `\\u0000CB${codeBlocks.length - 1}\\u0000`;\n });\n\n // 2. Extract and protect inline code\n const inlineCodes: string[] = [];\n result = result.replaceAll(/`([^`]+)`/g, (_m, code) => {\n inlineCodes.push(code);\n return `\\u0000IC${inlineCodes.length - 1}\\u0000`;\n });\n\n // 3. Headers -> plain text\n result = result.replaceAll(/^#{1,6}\\s+(.+)$/gm, \"$1\");\n\n // 4. Blockquotes -> plain text\n result = result.replaceAll(/^>\\s*(.*)$/gm, \"$1\");\n\n // 5. Escape HTML\n result = result\n .replaceAll(\"&\", \"&amp;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\");\n\n // 6. Links\n result = result.replaceAll(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href=\"$2\">$1</a>');\n\n // 7. Bold\n result = result.replaceAll(/\\*\\*(.+?)\\*\\*/g, \"<b>$1</b>\");\n result = result.replaceAll(/__(.+?)__/g, \"<b>$1</b>\");\n\n // 8. Italic\n result = result.replaceAll(\n /(?<![a-zA-Z0-9])_([^_]+)_(?![a-zA-Z0-9])/g,\n \"<i>$1</i>\"\n );\n\n // 9. Strikethrough\n result = result.replaceAll(/~~(.+?)~~/g, \"<s>$1</s>\");\n\n // 10. Bullet lists\n result = result.replaceAll(/^[-*]\\s+/gm, \"\\u2022 \");\n\n // 11. Restore inline code\n for (let i = 0; i < inlineCodes.length; i += 1) {\n const escaped = inlineCodes[i]\n .replaceAll(\"&\", \"&amp;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\");\n result = result.replace(`\\u0000IC${i}\\u0000`, `<code>${escaped}</code>`);\n }\n\n // 12. Restore code blocks\n for (let i = 0; i < codeBlocks.length; i += 1) {\n const escaped = codeBlocks[i]\n .replaceAll(\"&\", \"&amp;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\");\n result = result.replace(\n `\\u0000CB${i}\\u0000`,\n `<pre><code>${escaped}</code></pre>`\n );\n }\n\n return result;\n}\n\n/**\n * Telegram channel using grammY (long polling).\n */\nexport class TelegramChannel extends BaseChannel {\n readonly name = \"telegram\";\n private bot: Bot | null = null;\n private telegramConfig: TelegramConfig;\n\n constructor(config: TelegramConfig, bus: MessageBus) {\n super(config, bus);\n this.telegramConfig = config;\n }\n\n async start(): Promise<void> {\n if (!this.telegramConfig.token) {\n console.error(\"Telegram bot token not configured\");\n return;\n }\n\n this._running = true;\n this.bot = new Bot(this.telegramConfig.token);\n\n // Handle /start command\n this.bot.command(\"start\", async (ctx) => {\n const user = ctx.from;\n await ctx.reply(\n `Hi ${user?.first_name ?? \"there\"}! I'm nanobot.\\n\\nSend me a message and I'll respond!`\n );\n });\n\n // Handle text messages\n this.bot.on(\"message:text\", async (ctx) => {\n await this.onMessage(ctx);\n });\n\n // Handle photos\n this.bot.on(\"message:photo\", async (ctx) => {\n await this.onMessage(ctx);\n });\n\n // Handle documents\n this.bot.on(\"message:document\", async (ctx) => {\n await this.onMessage(ctx);\n });\n\n console.log(\"Starting Telegram bot (polling mode)...\");\n\n const me = await this.bot.api.getMe();\n console.log(`Telegram bot @${me.username} connected`);\n\n // Start polling with retry — handles 409 Conflict when a stale\n // instance is still holding the long-poll connection.\n const maxRetries = 3;\n for (let attempt = 1; attempt <= maxRetries; attempt += 1) {\n try {\n await this.bot.start({\n allowed_updates: [\"message\"],\n drop_pending_updates: true,\n });\n return; // bot.start() blocks until stopped, so if we get here it was a clean stop\n } catch (error) {\n const is409 = error instanceof Error && error.message.includes(\"409\");\n if (is409 && attempt < maxRetries && this._running) {\n const delay = attempt * 5;\n console.warn(\n `Telegram 409 conflict (attempt ${attempt}/${maxRetries}), retrying in ${delay}s...`\n );\n // oxlint-disable-next-line no-promise-executor-return\n await new Promise((r) => setTimeout(r, delay * 1000));\n continue;\n }\n throw error;\n }\n }\n }\n\n async stop(): Promise<void> {\n this._running = false;\n if (this.bot) {\n console.log(\"Stopping Telegram bot...\");\n await this.bot.stop();\n this.bot = null;\n }\n }\n\n async send(msg: OutboundMessage): Promise<void> {\n if (!this.bot) {\n console.warn(\"Telegram bot not running\");\n return;\n }\n\n try {\n const chatId = Number(msg.chatId);\n if (Number.isNaN(chatId)) {\n console.error(`Invalid chat_id: ${msg.chatId}`);\n return;\n }\n\n const htmlContent = markdownToTelegramHtml(msg.content);\n try {\n await this.bot.api.sendMessage(chatId, htmlContent, {\n parse_mode: \"HTML\",\n });\n } catch {\n // Fallback to plain text\n console.warn(\"HTML parse failed, falling back to plain text\");\n await this.bot.api.sendMessage(chatId, msg.content);\n }\n } catch (error) {\n console.error(\"Error sending Telegram message:\", error);\n }\n }\n\n private async onMessage(ctx: Context): Promise<void> {\n const user = ctx.from;\n const { message } = ctx;\n if (!user || !message) {\n return;\n }\n\n const chatId = message.chat.id;\n let senderId = String(user.id);\n if (user.username) {\n senderId = `${senderId}|${user.username}`;\n }\n\n const contentParts: string[] = [];\n const mediaPaths: string[] = [];\n\n // Text\n if (message.text) {\n contentParts.push(message.text);\n }\n if (message.caption) {\n contentParts.push(message.caption);\n }\n\n // Photos\n if (message.photo && message.photo.length > 0) {\n const photo = message.photo.at(-1);\n try {\n const file = await ctx.api.getFile(photo?.file_id ?? \"\");\n const mediaDir = join(homedir(), \".nanobot\", \"media\");\n if (!existsSync(mediaDir)) {\n mkdirSync(mediaDir, { recursive: true });\n }\n // Note: grammy doesn't have a built-in download_to_drive.\n // In WebContainer context, we just note the file_id.\n contentParts.push(`[image: telegram file ${file.file_id}]`);\n } catch {\n contentParts.push(\"[image: download failed]\");\n }\n }\n\n // Documents\n if (message.document) {\n contentParts.push(\n `[file: ${message.document.file_name ?? message.document.file_id}]`\n );\n }\n\n const content =\n contentParts.length > 0 ? contentParts.join(\"\\n\") : \"[empty message]\";\n\n await this.handleMessage({\n chatId: String(chatId),\n content,\n media: mediaPaths,\n metadata: {\n messageId: message.message_id,\n userId: user.id,\n username: user.username,\n firstName: user.first_name,\n isGroup: message.chat.type !== \"private\",\n },\n senderId,\n });\n }\n}\n"],"mappings":"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"}
@@ -28,11 +28,11 @@ async function loadCustomTools(config) {
28
28
  } catch {
29
29
  instance = exported(entry.options ?? {});
30
30
  }
31
- else throw new Error(`Export '${exportName}' is not a function`);
31
+ else throw new TypeError(`Export '${exportName}' is not a function`);
32
32
  if (instance && typeof instance === "object" && "execute" in instance && "name" in instance) tools.push(instance);
33
33
  else throw new Error(`Export '${exportName}' did not return a Tool instance`);
34
- } catch (err) {
35
- console.warn(`Warning: Failed to load custom tool '${entry.module}': ${err instanceof Error ? err.message : String(err)}`);
34
+ } catch (error) {
35
+ console.warn(`Warning: Failed to load custom tool '${entry.module}': ${error instanceof Error ? error.message : String(error)}`);
36
36
  }
37
37
  return tools;
38
38
  }
@@ -58,6 +58,13 @@ You are a helpful AI assistant. Be concise, accurate, and friendly.
58
58
  - Ask for clarification when the request is ambiguous
59
59
  - Use tools to help accomplish tasks
60
60
  - Remember important information in memory/MEMORY.md; past events are logged in memory/HISTORY.md
61
+ `,
62
+ "HEARTBEAT.md": `# Heartbeat
63
+
64
+ This file is checked every 30 minutes. Add tasks or instructions below and the agent will act on them automatically.
65
+
66
+ ## Tasks
67
+
61
68
  `,
62
69
  "IDENTITY.md": `# IDENTITY.md - Who Am I?
63
70
 
@@ -103,13 +110,6 @@ Each session, you wake up fresh. These files _are_ your memory. Read them. Updat
103
110
  Name: (not set)
104
111
  Timezone: (not set)
105
112
  Language: (not set)
106
- `,
107
- "HEARTBEAT.md": `# Heartbeat
108
-
109
- This file is checked every 30 minutes. Add tasks or instructions below and the agent will act on them automatically.
110
-
111
- ## Tasks
112
-
113
113
  `
114
114
  })) {
115
115
  const filePath = join(workspace, filename);
@@ -149,7 +149,7 @@ This file stores important information that should persist across sessions.
149
149
  program.command("gateway").description("Start the nanobot gateway").option("-p, --port <number>", "Gateway port", "18790").option("--verbose", "Verbose output", false).action(async (opts) => {
150
150
  console.log(`${LOGO} Starting nanobot gateway on port ${opts.port}...`);
151
151
  const config = loadConfig();
152
- const model = config.agents.defaults.model;
152
+ const { model } = config.agents.defaults;
153
153
  const apiKey = getApiKey(config, model);
154
154
  const apiBase = getApiBase(config, model);
155
155
  const extraHeaders = getExtraHeaders(config, model);
@@ -165,33 +165,33 @@ program.command("gateway").description("Start the nanobot gateway").option("-p,
165
165
  const bus = new MessageBus();
166
166
  const workspace = getConfigWorkspacePath(config);
167
167
  const provider = new OpenAIProvider({
168
- apiKey,
169
168
  apiBase: apiBase ?? void 0,
169
+ apiKey,
170
170
  defaultModel: model,
171
171
  extraHeaders
172
172
  });
173
173
  const customTools = await loadCustomTools(config);
174
174
  const agent = new AgentLoop({
175
+ braveApiKey: config.tools.web.search.apiKey || void 0,
175
176
  bus,
176
- provider,
177
- workspace,
178
- model,
179
177
  consolidationModel: resolveConsolidationModel(config.agents.defaults),
180
- maxTokens: config.agents.defaults.maxTokens,
178
+ customTools,
179
+ execConfig: config.tools.exec,
181
180
  maxIterations: config.agents.defaults.maxToolIterations,
181
+ maxTokens: config.agents.defaults.maxTokens,
182
182
  memoryWindow: config.agents.defaults.memoryWindow,
183
- braveApiKey: config.tools.web.search.apiKey || void 0,
184
- execConfig: config.tools.exec,
183
+ model,
184
+ provider,
185
185
  restrictToWorkspace: config.tools.restrictToWorkspace,
186
- toolsEnabled: config.tools.enabled,
187
186
  toolsDisabled: config.tools.disabled,
188
- customTools
187
+ toolsEnabled: config.tools.enabled,
188
+ workspace
189
189
  });
190
190
  const channels = new ChannelManager(config, bus);
191
191
  const { HeartbeatService } = await import("../heartbeat/service.mjs");
192
192
  const heartbeat = new HeartbeatService({
193
- workspace,
194
- onHeartbeat: (prompt) => agent.processDirect(prompt, "heartbeat")
193
+ onHeartbeat: (prompt) => agent.processDirect(prompt, "heartbeat"),
194
+ workspace
195
195
  });
196
196
  console.log("Heartbeat: managed by DO (local endpoint /api/heartbeat)");
197
197
  console.log("Cron: managed by DO");
@@ -207,21 +207,21 @@ program.command("gateway").description("Start the nanobot gateway").option("-p,
207
207
  createGatewayServer({
208
208
  agent,
209
209
  bus,
210
- port: Number(opts.port),
211
210
  channels,
212
- heartbeat
211
+ heartbeat,
212
+ port: Number(opts.port)
213
213
  });
214
214
  try {
215
215
  await channels.init();
216
216
  await Promise.all([agent.run(), channels.startAll()]);
217
- } catch (err) {
218
- console.error("Gateway error:", err);
217
+ } catch (error) {
218
+ console.error("Gateway error:", error);
219
219
  process.exit(1);
220
220
  }
221
221
  });
222
222
  program.command("agent").description("Interact with the agent directly").option("-m, --message <text>", "Message to send to the agent").option("-s, --session <id>", "Session ID", "cli:default").action(async (opts) => {
223
223
  const config = loadConfig();
224
- const model = config.agents.defaults.model;
224
+ const { model } = config.agents.defaults;
225
225
  const apiKey = getApiKey(config, model);
226
226
  const apiBase = getApiBase(config, model);
227
227
  const extraHeaders = getExtraHeaders(config, model);
@@ -235,25 +235,25 @@ program.command("agent").description("Interact with the agent directly").option(
235
235
  const bus = new MessageBus();
236
236
  const workspace = getConfigWorkspacePath(config);
237
237
  const provider = new OpenAIProvider({
238
- apiKey,
239
238
  apiBase: apiBase ?? void 0,
239
+ apiKey,
240
240
  defaultModel: model,
241
241
  extraHeaders
242
242
  });
243
243
  const customTools = await loadCustomTools(config);
244
244
  const agentLoop = new AgentLoop({
245
+ braveApiKey: config.tools.web.search.apiKey || void 0,
245
246
  bus,
246
- provider,
247
- workspace,
248
247
  consolidationModel: resolveConsolidationModel(config.agents.defaults),
248
+ customTools,
249
+ execConfig: config.tools.exec,
249
250
  maxTokens: config.agents.defaults.maxTokens,
250
251
  memoryWindow: config.agents.defaults.memoryWindow,
251
- braveApiKey: config.tools.web.search.apiKey || void 0,
252
- execConfig: config.tools.exec,
252
+ provider,
253
253
  restrictToWorkspace: config.tools.restrictToWorkspace,
254
- toolsEnabled: config.tools.enabled,
255
254
  toolsDisabled: config.tools.disabled,
256
- customTools
255
+ toolsEnabled: config.tools.enabled,
256
+ workspace
257
257
  });
258
258
  if (opts.message) {
259
259
  const response = await agentLoop.processDirect(opts.message, opts.session);
@@ -274,8 +274,8 @@ program.command("agent").description("Interact with the agent directly").option(
274
274
  try {
275
275
  const response = await agentLoop.processDirect(trimmed, opts.session);
276
276
  console.log(`\n${LOGO} ${response}\n`);
277
- } catch (err) {
278
- console.error("Error:", err);
277
+ } catch (error) {
278
+ console.error("Error:", error);
279
279
  }
280
280
  ask();
281
281
  });