@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
@@ -44,22 +44,22 @@ var LineChannel = class extends BaseChannel {
44
44
  const messages = this.parseMessage(text);
45
45
  try {
46
46
  const res = await fetch("https://api.line.me/v2/bot/message/reply", {
47
- method: "POST",
47
+ body: JSON.stringify({
48
+ replyToken,
49
+ messages
50
+ }),
48
51
  headers: {
49
52
  "Content-Type": "application/json",
50
53
  Authorization: `Bearer ${this.lineConfig.channelAccessToken}`
51
54
  },
52
- body: JSON.stringify({
53
- replyToken,
54
- messages
55
- })
55
+ method: "POST"
56
56
  });
57
57
  if (!res.ok) {
58
58
  const err = await res.text();
59
59
  console.error(`LINE reply failed (${res.status}): ${err}`);
60
60
  }
61
- } catch (err) {
62
- console.error("LINE reply error:", err);
61
+ } catch (error) {
62
+ console.error("LINE reply error:", error);
63
63
  }
64
64
  }
65
65
  /** Send a push message (works any time, consumes monthly quota). */
@@ -67,22 +67,22 @@ var LineChannel = class extends BaseChannel {
67
67
  const messages = this.parseMessage(text);
68
68
  try {
69
69
  const res = await fetch("https://api.line.me/v2/bot/message/push", {
70
- method: "POST",
70
+ body: JSON.stringify({
71
+ to,
72
+ messages
73
+ }),
71
74
  headers: {
72
75
  "Content-Type": "application/json",
73
76
  Authorization: `Bearer ${this.lineConfig.channelAccessToken}`
74
77
  },
75
- body: JSON.stringify({
76
- to,
77
- messages
78
- })
78
+ method: "POST"
79
79
  });
80
80
  if (!res.ok) {
81
81
  const err = await res.text();
82
82
  console.error(`LINE push failed (${res.status}): ${err}`);
83
83
  }
84
- } catch (err) {
85
- console.error("LINE push error:", err);
84
+ } catch (error) {
85
+ console.error("LINE push error:", error);
86
86
  }
87
87
  }
88
88
  /**
@@ -94,7 +94,7 @@ var LineChannel = class extends BaseChannel {
94
94
  if (event.mode !== "active") continue;
95
95
  if (event.type === "message" && event.message) await this.onMessageEvent(event);
96
96
  else if (event.type === "follow") {
97
- const userId = event.source.userId;
97
+ const { userId } = event.source;
98
98
  if (userId) {
99
99
  console.log(`LINE: new follower ${userId}`);
100
100
  if (event.replyToken) await this.reply(event.replyToken, "Hi! I'm nanobot. Send me a message and I'll respond!");
@@ -106,7 +106,7 @@ var LineChannel = class extends BaseChannel {
106
106
  }
107
107
  async onMessageEvent(event) {
108
108
  const message = event.message;
109
- const source = event.source;
109
+ const { source } = event;
110
110
  const senderId = source.userId ?? "unknown";
111
111
  const chatId = source.type === "group" ? source.groupId ?? senderId : source.type === "room" ? source.roomId ?? senderId : senderId;
112
112
  let content;
@@ -139,7 +139,6 @@ var LineChannel = class extends BaseChannel {
139
139
  }
140
140
  if (message.quotedMessageId) content = `[Quoting message ${message.quotedMessageId}]\n${content}`;
141
141
  await this.handleMessage({
142
- senderId,
143
142
  chatId,
144
143
  content,
145
144
  media,
@@ -150,7 +149,8 @@ var LineChannel = class extends BaseChannel {
150
149
  quoteToken: message.quoteToken,
151
150
  ...message.quotedMessageId && { quotedMessageId: message.quotedMessageId },
152
151
  ...message.mention && { mention: message.mention }
153
- }
152
+ },
153
+ senderId
154
154
  });
155
155
  }
156
156
  /** Build content string for a text message, including mention/emoji info. */
@@ -199,7 +199,7 @@ var LineChannel = class extends BaseChannel {
199
199
  const parts = [];
200
200
  if (message.title) parts.push(message.title);
201
201
  if (message.address) parts.push(message.address);
202
- if (message.latitude != null && message.longitude != null) parts.push(`(${message.latitude}, ${message.longitude})`);
202
+ if (message.latitude !== null && message.longitude !== null) parts.push(`(${message.latitude}, ${message.longitude})`);
203
203
  return `[Location: ${parts.join(" | ")}]`;
204
204
  }
205
205
  /** Handle sticker message with resource type and keywords. */
@@ -229,8 +229,8 @@ var LineChannel = class extends BaseChannel {
229
229
  writeFileSync(filePath, buffer);
230
230
  console.log(`LINE: downloaded content ${messageId} (${buffer.length} bytes)`);
231
231
  return filePath;
232
- } catch (err) {
233
- console.error("LINE content download error:", err);
232
+ } catch (error) {
233
+ console.error("LINE content download error:", error);
234
234
  return null;
235
235
  }
236
236
  }
@@ -249,12 +249,12 @@ var LineChannel = class extends BaseChannel {
249
249
  const buffer = Buffer.from(await res.arrayBuffer());
250
250
  const dir = this.workspace ? join(this.workspace, "uploads") : join(tmpdir(), "nanobot-line-uploads");
251
251
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
252
- const filePath = join(dir, `${Date.now()}_${fileName.replace(/[^a-zA-Z0-9._-]/g, "_")}`);
252
+ const filePath = join(dir, `${Date.now()}_${fileName.replaceAll(/[^a-zA-Z0-9._-]/g, "_")}`);
253
253
  writeFileSync(filePath, buffer);
254
254
  console.log(`LINE: saved file ${fileName} (${buffer.length} bytes) -> ${filePath}`);
255
255
  return filePath;
256
- } catch (err) {
257
- console.error("LINE file download error:", err);
256
+ } catch (error) {
257
+ console.error("LINE file download error:", error);
258
258
  return null;
259
259
  }
260
260
  }
@@ -268,9 +268,9 @@ var LineChannel = class extends BaseChannel {
268
268
  returnFlexMessage(message) {
269
269
  const parsed = JSON.parse(message);
270
270
  return [{
271
- type: "flex",
272
271
  altText: this.extractAltText(parsed),
273
- contents: parsed
272
+ contents: parsed,
273
+ type: "flex"
274
274
  }];
275
275
  }
276
276
  /** Parse text into LINE message(s). If valid Flex JSON found, send as flex + surrounding text as separate messages. */
@@ -280,7 +280,7 @@ var LineChannel = class extends BaseChannel {
280
280
  return this.returnFlexMessage(trimmed);
281
281
  } catch {}
282
282
  const braceIdx = trimmed.indexOf("{");
283
- if (braceIdx >= 0) {
283
+ if (braceIdx !== -1) {
284
284
  const jsonEnd = this.findJsonEnd(trimmed.slice(braceIdx));
285
285
  if (jsonEnd > 0) {
286
286
  const jsonPart = trimmed.slice(braceIdx, braceIdx + jsonEnd);
@@ -289,21 +289,21 @@ var LineChannel = class extends BaseChannel {
289
289
  try {
290
290
  const messages = [];
291
291
  if (prefix) messages.push({
292
- type: "text",
293
- text: prefix
292
+ text: prefix,
293
+ type: "text"
294
294
  });
295
295
  messages.push(...this.returnFlexMessage(jsonPart));
296
296
  if (suffix) messages.push({
297
- type: "text",
298
- text: suffix
297
+ text: suffix,
298
+ type: "text"
299
299
  });
300
300
  return messages;
301
301
  } catch {}
302
302
  }
303
303
  }
304
304
  return [{
305
- type: "text",
306
- text: trimmed || "(empty)"
305
+ text: trimmed || "(empty)",
306
+ type: "text"
307
307
  }];
308
308
  }
309
309
  /** Find the end index of a top-level JSON object in a string. Returns -1 if not found. */
@@ -312,7 +312,7 @@ var LineChannel = class extends BaseChannel {
312
312
  let depth = 0;
313
313
  let inString = false;
314
314
  let escape = false;
315
- for (let i = 0; i < str.length; i++) {
315
+ for (let i = 0; i < str.length; i += 1) {
316
316
  const ch = str[i];
317
317
  if (escape) {
318
318
  escape = false;
@@ -327,49 +327,49 @@ var LineChannel = class extends BaseChannel {
327
327
  continue;
328
328
  }
329
329
  if (inString) continue;
330
- if (ch === "{") depth++;
330
+ if (ch === "{") depth += 1;
331
331
  else if (ch === "}") {
332
- depth--;
332
+ depth -= 1;
333
333
  if (depth === 0) return i + 1;
334
334
  }
335
335
  }
336
336
  return -1;
337
337
  }
338
- /** Extract alt text from flex content. */
339
- extractAltText(contents) {
340
- const extractText = (obj, depth = 0) => {
341
- if (depth > 5) return null;
342
- if (typeof obj === "string") return obj.slice(0, 100);
343
- if (!obj || typeof obj !== "object") return null;
344
- if (Array.isArray(obj)) {
345
- for (const item of obj) {
346
- const result = extractText(item, depth + 1);
347
- if (result) return result;
348
- }
349
- return null;
350
- }
351
- const record = obj;
352
- if (record.text && typeof record.text === "string") return record.text.slice(0, 100);
353
- if (record.title && typeof record.title === "string") return record.title.slice(0, 100);
354
- for (const key of [
355
- "contents",
356
- "body",
357
- "header",
358
- "hero"
359
- ]) {
360
- const result = extractText(record[key], depth + 1);
338
+ extractText = (obj, depth = 0) => {
339
+ if (depth > 5) return null;
340
+ if (typeof obj === "string") return obj.slice(0, 100);
341
+ if (!obj || typeof obj !== "object") return null;
342
+ if (Array.isArray(obj)) {
343
+ for (const item of obj) {
344
+ const result = this.extractText(item, depth + 1);
361
345
  if (result) return result;
362
346
  }
363
347
  return null;
364
- };
365
- return extractText(contents) || "Flex Message";
348
+ }
349
+ const record = obj;
350
+ if (record.text && typeof record.text === "string") return record.text.slice(0, 100);
351
+ if (record.title && typeof record.title === "string") return record.title.slice(0, 100);
352
+ for (const key of [
353
+ "contents",
354
+ "body",
355
+ "header",
356
+ "hero"
357
+ ]) {
358
+ const result = this.extractText(record[key], depth + 1);
359
+ if (result) return result;
360
+ }
361
+ return null;
362
+ };
363
+ /** Extract alt text from flex content. */
364
+ extractAltText(contents) {
365
+ return this.extractText(contents) || "Flex Message";
366
366
  }
367
367
  /** Format file size in human-readable form. */
368
368
  formatFileSize(bytes) {
369
- if (bytes == null) return "unknown size";
370
- if (bytes < 1024) return `${bytes} B`;
371
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
372
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
369
+ if (bytes === null) return "unknown size";
370
+ if (bytes !== void 0 && bytes < 1024) return `${bytes} B`;
371
+ if (bytes !== void 0 && bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
372
+ return `${((bytes ?? 0) / (1024 * 1024)).toFixed(1)} MB`;
373
373
  }
374
374
  };
375
375
 
@@ -1 +1 @@
1
- {"version":3,"file":"line.mjs","names":[],"sources":["../../src/channels/line.ts"],"sourcesContent":["import { createHmac, timingSafeEqual } from \"node:crypto\";\nimport { writeFileSync, mkdirSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\nimport type { messagingApi } from \"@line/bot-sdk\";\nimport type { OutboundMessage } from \"../bus/events.js\";\nimport type { MessageBus } from \"../bus/queue.js\";\nimport { BaseChannel } from \"./base.js\";\nimport type { LineConfig } from \"../config/schema.js\";\n\n// ---------------------------------------------------------------------------\n// LINE Messaging API types (webhook events)\n// ---------------------------------------------------------------------------\n\ninterface LineWebhookBody {\n destination: string;\n events: LineEvent[];\n}\n\ninterface LineSource {\n type: \"user\" | \"group\" | \"room\";\n userId?: string;\n groupId?: string;\n roomId?: string;\n}\n\ninterface LineEvent {\n type: string;\n timestamp: number;\n source: LineSource;\n replyToken?: string;\n mode: \"active\" | \"standby\";\n webhookEventId: string;\n deliveryContext: { isRedelivery: boolean };\n message?: LineMessage;\n}\n\n// -- Emoji in text messages ------------------------------------------------\n\ninterface LineEmoji {\n index: number;\n length: number;\n productId: string;\n emojiId: string;\n}\n\n// -- Mention in text messages ----------------------------------------------\n\ninterface LineMentionee {\n index: number;\n length: number;\n type: \"user\" | \"all\";\n userId?: string;\n isSelf?: boolean;\n}\n\ninterface LineMention {\n mentionees: LineMentionee[];\n}\n\n// -- Content provider (image, video, audio) --------------------------------\n\ninterface LineContentProvider {\n type: \"line\" | \"external\";\n originalContentUrl?: string;\n previewImageUrl?: string;\n}\n\n// -- Image set (multiple images sent simultaneously) -----------------------\n\ninterface LineImageSet {\n id: string;\n index?: number;\n total?: number;\n}\n\n// -- Union message type ----------------------------------------------------\n\ninterface LineMessage {\n type: string;\n id: string;\n quoteToken?: string;\n quotedMessageId?: string;\n\n // Text\n text?: string;\n emojis?: LineEmoji[];\n mention?: LineMention;\n\n // Image\n contentProvider?: LineContentProvider;\n imageSet?: LineImageSet;\n\n // Video / Audio\n duration?: number;\n\n // File\n fileName?: string;\n fileSize?: number;\n\n // Location\n title?: string;\n address?: string;\n latitude?: number;\n longitude?: number;\n\n // Sticker\n packageId?: string;\n stickerId?: string;\n stickerResourceType?: string;\n keywords?: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Signature verification\n// ---------------------------------------------------------------------------\n\nexport function verifyLineSignature(\n channelSecret: string,\n rawBody: string,\n signature: string,\n): boolean {\n const digest = createHmac(\"sha256\", channelSecret)\n .update(rawBody)\n .digest(\"base64\");\n try {\n return timingSafeEqual(Buffer.from(digest), Buffer.from(signature));\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// LINE Channel\n// ---------------------------------------------------------------------------\n\n/**\n * LINE Messaging API channel.\n *\n * Unlike Telegram (long-polling), LINE uses webhooks -- the HTTP server pushes\n * events into this channel via `handleWebhook()`. The channel is registered in\n * the gateway Hono server which forwards POST /webhook/line to it.\n */\nexport class LineChannel extends BaseChannel {\n readonly name = \"line\";\n private lineConfig: LineConfig;\n private workspace: string | null;\n\n constructor(config: LineConfig, bus: MessageBus, workspace?: string) {\n super(config, bus);\n this.lineConfig = config;\n this.workspace = workspace ?? null;\n }\n\n // LINE is webhook-driven; start/stop are no-ops.\n async start(): Promise<void> {\n this._running = true;\n console.log(\"LINE channel ready (webhook mode)\");\n }\n\n async stop(): Promise<void> {\n this._running = false;\n }\n\n // ------ Outbound: reply or push ------\n\n async send(msg: OutboundMessage): Promise<void> {\n // msg.chatId is the LINE userId (or groupId/roomId)\n // Reply tokens expire quickly, so we always use the push API\n // for outbound messages routed through the bus.\n await this.pushMessage(msg.chatId, msg.content);\n }\n\n /** Send a reply using a replyToken (fast, free, single-use). */\n async reply(replyToken: string, text: string): Promise<void> {\n const messages = this.parseMessage(text);\n try {\n const res = await fetch(\"https://api.line.me/v2/bot/message/reply\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n body: JSON.stringify({ replyToken, messages }),\n });\n if (!res.ok) {\n const err = await res.text();\n console.error(`LINE reply failed (${res.status}): ${err}`);\n }\n } catch (err) {\n console.error(\"LINE reply error:\", err);\n }\n }\n\n /** Send a push message (works any time, consumes monthly quota). */\n async pushMessage(to: string, text: string): Promise<void> {\n const messages = this.parseMessage(text);\n try {\n const res = await fetch(\"https://api.line.me/v2/bot/message/push\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n body: JSON.stringify({ to, messages }),\n });\n if (!res.ok) {\n const err = await res.text();\n console.error(`LINE push failed (${res.status}): ${err}`);\n }\n } catch (err) {\n console.error(\"LINE push error:\", err);\n }\n }\n\n // ------ Inbound: webhook events ------\n\n /**\n * Process a raw webhook request.\n * Called by the gateway Hono route after signature verification.\n */\n async handleWebhook(body: LineWebhookBody): Promise<void> {\n for (const event of body.events) {\n if (event.mode !== \"active\") continue;\n\n if (event.type === \"message\" && event.message) {\n await this.onMessageEvent(event);\n } else if (event.type === \"follow\") {\n const userId = event.source.userId;\n if (userId) {\n console.log(`LINE: new follower ${userId}`);\n if (event.replyToken) {\n await this.reply(\n event.replyToken,\n \"Hi! I'm nanobot. Send me a message and I'll respond!\",\n );\n }\n }\n } else if (event.type === \"unfollow\") {\n console.log(`LINE: unfollowed by ${event.source.userId ?? \"unknown\"}`);\n } else if (event.type === \"join\") {\n console.log(`LINE: joined ${event.source.type} ${event.source.groupId ?? event.source.roomId ?? \"?\"}`);\n } else if (event.type === \"leave\") {\n console.log(`LINE: left ${event.source.type} ${event.source.groupId ?? event.source.roomId ?? \"?\"}`);\n }\n }\n }\n\n private async onMessageEvent(event: LineEvent): Promise<void> {\n const message = event.message!;\n const source = event.source;\n\n // Determine senderId and chatId\n const senderId = source.userId ?? \"unknown\";\n const chatId =\n source.type === \"group\"\n ? (source.groupId ?? senderId)\n : source.type === \"room\"\n ? (source.roomId ?? senderId)\n : senderId;\n\n // Build content and optional media\n let content: string;\n const media: string[] = [];\n\n switch (message.type) {\n case \"text\":\n content = this.buildTextContent(message);\n break;\n\n case \"image\":\n content = await this.handleImageMessage(message, media);\n break;\n\n case \"video\":\n content = this.handleVideoMessage(message);\n break;\n\n case \"audio\":\n content = this.handleAudioMessage(message);\n break;\n\n case \"file\":\n content = await this.handleFileMessage(message);\n break;\n\n case \"location\":\n content = this.handleLocationMessage(message);\n break;\n\n case \"sticker\":\n content = this.handleStickerMessage(message);\n break;\n\n default:\n content = `[${message.type} message]`;\n break;\n }\n\n // Prepend quoted message context if this message quotes a previous one\n if (message.quotedMessageId) {\n content = `[Quoting message ${message.quotedMessageId}]\\n${content}`;\n }\n\n // Store the replyToken + rich metadata so downstream can use them\n await this.handleMessage({\n senderId,\n chatId,\n content,\n media,\n metadata: {\n messageId: message.id,\n replyToken: event.replyToken,\n sourceType: source.type,\n quoteToken: message.quoteToken,\n ...(message.quotedMessageId && { quotedMessageId: message.quotedMessageId }),\n ...(message.mention && { mention: message.mention }),\n },\n });\n }\n\n // ------ Message type handlers ------\n\n /** Build content string for a text message, including mention/emoji info. */\n private buildTextContent(message: LineMessage): string {\n let content = message.text ?? \"\";\n\n // If the text contains mentions, annotate them\n if (message.mention && message.mention.mentionees.length > 0) {\n const mentionInfo = message.mention.mentionees\n .map((m) => {\n const who = m.type === \"all\" ? \"@All\" : (m.userId ?? \"user\");\n const isSelf = m.isSelf ? \" (mentioning bot)\" : \"\";\n return `${who}${isSelf}`;\n })\n .join(\", \");\n content += `\\n[Mentions: ${mentionInfo}]`;\n }\n\n return content;\n }\n\n /** Handle image message: download from Content API if provider is LINE. */\n private async handleImageMessage(\n message: LineMessage,\n media: string[],\n ): Promise<string> {\n const provider = message.contentProvider;\n\n if (provider?.type === \"external\" && provider.originalContentUrl) {\n // External image: use the URL directly\n media.push(provider.originalContentUrl);\n return \"[User sent an image]\";\n }\n\n // LINE-hosted image: download via Content API\n const imagePath = await this.downloadContent(message.id, \"jpg\");\n if (imagePath) {\n media.push(imagePath);\n const setInfo = message.imageSet\n ? ` (${message.imageSet.index ?? \"?\"}/${message.imageSet.total ?? \"?\"})`\n : \"\";\n return `[User sent an image${setInfo}]`;\n }\n return \"[Image: failed to download]\";\n }\n\n /** Handle video message. */\n private handleVideoMessage(message: LineMessage): string {\n const duration = message.duration\n ? ` (${Math.round(message.duration / 1000)}s)`\n : \"\";\n return `[Video message${duration} -- video analysis not yet supported]`;\n }\n\n /** Handle audio message. */\n private handleAudioMessage(message: LineMessage): string {\n const duration = message.duration\n ? ` (${Math.round(message.duration / 1000)}s)`\n : \"\";\n return `[Audio message${duration} -- transcription not yet supported]`;\n }\n\n /** Handle file message: download from Content API to workspace. */\n private async handleFileMessage(message: LineMessage): Promise<string> {\n const fileName = message.fileName ?? `file_${message.id}`;\n const fileSize = this.formatFileSize(message.fileSize);\n\n const savedPath = await this.downloadToWorkspace(message.id, fileName);\n if (savedPath) {\n return `[File received: ${fileName} (${fileSize}) — saved to ${savedPath}]`;\n }\n return `[File: ${fileName} (${fileSize}) — download failed]`;\n }\n\n /** Handle location message with all available fields. */\n private handleLocationMessage(message: LineMessage): string {\n const parts: string[] = [];\n if (message.title) parts.push(message.title);\n if (message.address) parts.push(message.address);\n if (message.latitude != null && message.longitude != null) {\n parts.push(`(${message.latitude}, ${message.longitude})`);\n }\n return `[Location: ${parts.join(\" | \")}]`;\n }\n\n /** Handle sticker message with resource type and keywords. */\n private handleStickerMessage(message: LineMessage): string {\n const parts: string[] = [\"Sticker\"];\n\n // Include sticker text for message stickers\n if (message.text) {\n parts.push(`\"${message.text}\"`);\n }\n\n // Include keywords if available (gives the AI context about the sticker)\n if (message.keywords && message.keywords.length > 0) {\n parts.push(`(${message.keywords.join(\", \")})`);\n }\n\n // Resource type for logging\n const resType = message.stickerResourceType;\n if (resType && resType !== \"STATIC\") {\n parts.push(`[${resType.toLowerCase()}]`);\n }\n\n return `[${parts.join(\" \")}]`;\n }\n\n // ------ Content download ------\n\n /**\n * Download message content (image, video, audio, file) from LINE Content API.\n * Returns the local file path, or null on failure.\n */\n private async downloadContent(\n messageId: string,\n ext: string,\n ): Promise<string | null> {\n try {\n const res = await fetch(\n `https://api-data.line.me/v2/bot/message/${messageId}/content`,\n {\n headers: {\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n },\n );\n\n if (!res.ok) {\n console.error(`LINE content download failed (${res.status})`);\n return null;\n }\n\n const buffer = Buffer.from(await res.arrayBuffer());\n\n // Save to temp dir\n const dir = join(tmpdir(), \"nanobot-line-media\");\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n const filePath = join(dir, `${messageId}.${ext}`);\n writeFileSync(filePath, buffer);\n\n console.log(`LINE: downloaded content ${messageId} (${buffer.length} bytes)`);\n return filePath;\n } catch (err) {\n console.error(\"LINE content download error:\", err);\n return null;\n }\n }\n\n /**\n * Download message content to the workspace uploads directory.\n * Falls back to tmpdir if no workspace is configured.\n * Returns the saved file path, or null on failure.\n */\n private async downloadToWorkspace(\n messageId: string,\n fileName: string,\n ): Promise<string | null> {\n try {\n const res = await fetch(\n `https://api-data.line.me/v2/bot/message/${messageId}/content`,\n {\n headers: {\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n },\n );\n\n if (!res.ok) {\n console.error(`LINE content download failed (${res.status})`);\n return null;\n }\n\n const buffer = Buffer.from(await res.arrayBuffer());\n\n // Determine save directory\n const dir = this.workspace\n ? join(this.workspace, \"uploads\")\n : join(tmpdir(), \"nanobot-line-uploads\");\n\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n // Avoid filename collisions by prepending timestamp\n const safeName = `${Date.now()}_${fileName.replace(/[^a-zA-Z0-9._-]/g, \"_\")}`;\n const filePath = join(dir, safeName);\n writeFileSync(filePath, buffer);\n\n console.log(`LINE: saved file ${fileName} (${buffer.length} bytes) -> ${filePath}`);\n return filePath;\n } catch (err) {\n console.error(\"LINE file download error:\", err);\n return null;\n }\n }\n\n // ------ Signature ------\n\n /**\n * Verify webhook signature.\n * Exposed as a helper so the gateway route can call it.\n */\n verifySignature(rawBody: string, signature: string): boolean {\n return verifyLineSignature(\n this.lineConfig.channelSecret,\n rawBody,\n signature,\n );\n }\n\n // ------ Helpers ------\n\n private returnFlexMessage(message: string): messagingApi.Message[] {\n const parsed = JSON.parse(message);\n\n return [{\n type: \"flex\",\n altText: this.extractAltText(parsed),\n contents: parsed,\n }];\n }\n\n /** Parse text into LINE message(s). If valid Flex JSON found, send as flex + surrounding text as separate messages. */\n private parseMessage(text: string): messagingApi.Message[] {\n const trimmed = text.trim();\n\n // Try parsing the entire string as JSON first\n try {\n return this.returnFlexMessage(trimmed);\n } catch {\n // Not pure JSON — try to extract a JSON object from anywhere in the string\n }\n\n // Find first '{' in the string and try to extract a JSON object\n const braceIdx = trimmed.indexOf(\"{\");\n if (braceIdx >= 0) {\n const jsonEnd = this.findJsonEnd(trimmed.slice(braceIdx));\n if (jsonEnd > 0) {\n const jsonPart = trimmed.slice(braceIdx, braceIdx + jsonEnd);\n const prefix = trimmed.slice(0, braceIdx).trim();\n const suffix = trimmed.slice(braceIdx + jsonEnd).trim();\n try {\n const messages: messagingApi.Message[] = [];\n if (prefix) {\n messages.push({ type: \"text\", text: prefix });\n }\n messages.push(...this.returnFlexMessage(jsonPart));\n if (suffix) {\n messages.push({ type: \"text\", text: suffix });\n }\n return messages;\n } catch {\n // JSON was invalid flex, fall through\n }\n }\n }\n\n return [{ type: \"text\", text: trimmed || \"(empty)\" }];\n }\n\n /** Find the end index of a top-level JSON object in a string. Returns -1 if not found. */\n private findJsonEnd(str: string): number {\n if (str[0] !== \"{\") return -1;\n let depth = 0;\n let inString = false;\n let escape = false;\n\n for (let i = 0; i < str.length; i++) {\n const ch = str[i];\n\n if (escape) {\n escape = false;\n continue;\n }\n\n if (ch === \"\\\\\") {\n if (inString) escape = true;\n continue;\n }\n\n if (ch === '\"') {\n inString = !inString;\n continue;\n }\n\n if (inString) continue;\n\n if (ch === \"{\") depth++;\n else if (ch === \"}\") {\n depth--;\n if (depth === 0) return i + 1;\n }\n }\n\n return -1;\n }\n\n /** Extract alt text from flex content. */\n private extractAltText(contents: Record<string, unknown>): string {\n const extractText = (obj: unknown, depth = 0): string | null => {\n if (depth > 5) return null;\n if (typeof obj === \"string\") return obj.slice(0, 100);\n if (!obj || typeof obj !== \"object\") return null;\n\n // Handle arrays: iterate elements\n if (Array.isArray(obj)) {\n for (const item of obj) {\n const result = extractText(item, depth + 1);\n if (result) return result;\n }\n return null;\n }\n\n const record = obj as Record<string, unknown>;\n if (record.text && typeof record.text === \"string\") return record.text.slice(0, 100);\n if (record.title && typeof record.title === \"string\") return record.title.slice(0, 100);\n\n for (const key of [\"contents\", \"body\", \"header\", \"hero\"]) {\n const result = extractText(record[key], depth + 1);\n if (result) return result;\n }\n return null;\n };\n\n return extractText(contents) || \"Flex Message\";\n }\n\n /** Format file size in human-readable form. */\n private formatFileSize(bytes?: number): string {\n if (bytes == null) return \"unknown size\";\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"line.mjs","names":[],"sources":["../../src/channels/line.ts"],"sourcesContent":["import type { messagingApi } from \"@line/bot-sdk\";\n\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\nimport { writeFileSync, mkdirSync, existsSync } from \"node:fs\";\nimport { tmpdir } 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 { LineConfig } from \"../config/schema.js\";\n\nimport { BaseChannel } from \"./base.js\";\n\n// ---------------------------------------------------------------------------\n// LINE Messaging API types (webhook events)\n// ---------------------------------------------------------------------------\n\ninterface LineWebhookBody {\n destination: string;\n events: LineEvent[];\n}\n\ninterface LineSource {\n type: \"user\" | \"group\" | \"room\";\n userId?: string;\n groupId?: string;\n roomId?: string;\n}\n\ninterface LineEvent {\n type: string;\n timestamp: number;\n source: LineSource;\n replyToken?: string;\n mode: \"active\" | \"standby\";\n webhookEventId: string;\n deliveryContext: { isRedelivery: boolean };\n message?: LineMessage;\n}\n\n// -- Emoji in text messages ------------------------------------------------\n\ninterface LineEmoji {\n index: number;\n length: number;\n productId: string;\n emojiId: string;\n}\n\n// -- Mention in text messages ----------------------------------------------\n\ninterface LineMentionee {\n index: number;\n length: number;\n type: \"user\" | \"all\";\n userId?: string;\n isSelf?: boolean;\n}\n\ninterface LineMention {\n mentionees: LineMentionee[];\n}\n\n// -- Content provider (image, video, audio) --------------------------------\n\ninterface LineContentProvider {\n type: \"line\" | \"external\";\n originalContentUrl?: string;\n previewImageUrl?: string;\n}\n\n// -- Image set (multiple images sent simultaneously) -----------------------\n\ninterface LineImageSet {\n id: string;\n index?: number;\n total?: number;\n}\n\n// -- Union message type ----------------------------------------------------\n\ninterface LineMessage {\n type: string;\n id: string;\n quoteToken?: string;\n quotedMessageId?: string;\n\n // Text\n text?: string;\n emojis?: LineEmoji[];\n mention?: LineMention;\n\n // Image\n contentProvider?: LineContentProvider;\n imageSet?: LineImageSet;\n\n // Video / Audio\n duration?: number;\n\n // File\n fileName?: string;\n fileSize?: number;\n\n // Location\n title?: string;\n address?: string;\n latitude?: number;\n longitude?: number;\n\n // Sticker\n packageId?: string;\n stickerId?: string;\n stickerResourceType?: string;\n keywords?: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Signature verification\n// ---------------------------------------------------------------------------\n\nexport function verifyLineSignature(\n channelSecret: string,\n rawBody: string,\n signature: string\n): boolean {\n const digest = createHmac(\"sha256\", channelSecret)\n .update(rawBody)\n .digest(\"base64\");\n try {\n return timingSafeEqual(Buffer.from(digest), Buffer.from(signature));\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// LINE Channel\n// ---------------------------------------------------------------------------\n\n/**\n * LINE Messaging API channel.\n *\n * Unlike Telegram (long-polling), LINE uses webhooks -- the HTTP server pushes\n * events into this channel via `handleWebhook()`. The channel is registered in\n * the gateway Hono server which forwards POST /webhook/line to it.\n */\nexport class LineChannel extends BaseChannel {\n readonly name = \"line\";\n private lineConfig: LineConfig;\n private workspace: string | null;\n\n constructor(config: LineConfig, bus: MessageBus, workspace?: string) {\n super(config, bus);\n this.lineConfig = config;\n this.workspace = workspace ?? null;\n }\n\n // LINE is webhook-driven; start/stop are no-ops.\n async start(): Promise<void> {\n this._running = true;\n console.log(\"LINE channel ready (webhook mode)\");\n }\n\n async stop(): Promise<void> {\n this._running = false;\n }\n\n // ------ Outbound: reply or push ------\n\n async send(msg: OutboundMessage): Promise<void> {\n // msg.chatId is the LINE userId (or groupId/roomId)\n // Reply tokens expire quickly, so we always use the push API\n // for outbound messages routed through the bus.\n await this.pushMessage(msg.chatId, msg.content);\n }\n\n /** Send a reply using a replyToken (fast, free, single-use). */\n async reply(replyToken: string, text: string): Promise<void> {\n const messages = this.parseMessage(text);\n try {\n const res = await fetch(\"https://api.line.me/v2/bot/message/reply\", {\n body: JSON.stringify({ replyToken, messages }),\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n method: \"POST\",\n });\n if (!res.ok) {\n const err = await res.text();\n console.error(`LINE reply failed (${res.status}): ${err}`);\n }\n } catch (error) {\n console.error(\"LINE reply error:\", error);\n }\n }\n\n /** Send a push message (works any time, consumes monthly quota). */\n async pushMessage(to: string, text: string): Promise<void> {\n const messages = this.parseMessage(text);\n try {\n const res = await fetch(\"https://api.line.me/v2/bot/message/push\", {\n body: JSON.stringify({ to, messages }),\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n method: \"POST\",\n });\n if (!res.ok) {\n const err = await res.text();\n console.error(`LINE push failed (${res.status}): ${err}`);\n }\n } catch (error) {\n console.error(\"LINE push error:\", error);\n }\n }\n\n // ------ Inbound: webhook events ------\n\n /**\n * Process a raw webhook request.\n * Called by the gateway Hono route after signature verification.\n */\n async handleWebhook(body: LineWebhookBody): Promise<void> {\n for (const event of body.events) {\n if (event.mode !== \"active\") {\n continue;\n }\n\n if (event.type === \"message\" && event.message) {\n await this.onMessageEvent(event);\n } else if (event.type === \"follow\") {\n const { userId } = event.source;\n if (userId) {\n console.log(`LINE: new follower ${userId}`);\n if (event.replyToken) {\n await this.reply(\n event.replyToken,\n \"Hi! I'm nanobot. Send me a message and I'll respond!\"\n );\n }\n }\n } else if (event.type === \"unfollow\") {\n console.log(`LINE: unfollowed by ${event.source.userId ?? \"unknown\"}`);\n } else if (event.type === \"join\") {\n console.log(\n `LINE: joined ${event.source.type} ${event.source.groupId ?? event.source.roomId ?? \"?\"}`\n );\n } else if (event.type === \"leave\") {\n console.log(\n `LINE: left ${event.source.type} ${event.source.groupId ?? event.source.roomId ?? \"?\"}`\n );\n }\n }\n }\n\n private async onMessageEvent(event: LineEvent): Promise<void> {\n const message = event.message!;\n const { source } = event;\n\n // Determine senderId and chatId\n const senderId = source.userId ?? \"unknown\";\n const chatId =\n source.type === \"group\"\n ? (source.groupId ?? senderId)\n : source.type === \"room\"\n ? (source.roomId ?? senderId)\n : senderId;\n\n // Build content and optional media\n let content: string;\n const media: string[] = [];\n\n switch (message.type) {\n case \"text\": {\n content = this.buildTextContent(message);\n break;\n }\n\n case \"image\": {\n content = await this.handleImageMessage(message, media);\n break;\n }\n\n case \"video\": {\n content = this.handleVideoMessage(message);\n break;\n }\n\n case \"audio\": {\n content = this.handleAudioMessage(message);\n break;\n }\n\n case \"file\": {\n content = await this.handleFileMessage(message);\n break;\n }\n\n case \"location\": {\n content = this.handleLocationMessage(message);\n break;\n }\n\n case \"sticker\": {\n content = this.handleStickerMessage(message);\n break;\n }\n\n default: {\n content = `[${message.type} message]`;\n break;\n }\n }\n\n // Prepend quoted message context if this message quotes a previous one\n if (message.quotedMessageId) {\n content = `[Quoting message ${message.quotedMessageId}]\\n${content}`;\n }\n\n // Store the replyToken + rich metadata so downstream can use them\n await this.handleMessage({\n chatId,\n content,\n media,\n metadata: {\n messageId: message.id,\n replyToken: event.replyToken,\n sourceType: source.type,\n quoteToken: message.quoteToken,\n ...(message.quotedMessageId && {\n quotedMessageId: message.quotedMessageId,\n }),\n ...(message.mention && { mention: message.mention }),\n },\n senderId,\n });\n }\n\n // ------ Message type handlers ------\n\n /** Build content string for a text message, including mention/emoji info. */\n private buildTextContent(message: LineMessage): string {\n let content = message.text ?? \"\";\n\n // If the text contains mentions, annotate them\n if (message.mention && message.mention.mentionees.length > 0) {\n const mentionInfo = message.mention.mentionees\n .map((m) => {\n const who = m.type === \"all\" ? \"@All\" : (m.userId ?? \"user\");\n const isSelf = m.isSelf ? \" (mentioning bot)\" : \"\";\n return `${who}${isSelf}`;\n })\n .join(\", \");\n content += `\\n[Mentions: ${mentionInfo}]`;\n }\n\n return content;\n }\n\n /** Handle image message: download from Content API if provider is LINE. */\n private async handleImageMessage(\n message: LineMessage,\n media: string[]\n ): Promise<string> {\n const provider = message.contentProvider;\n\n if (provider?.type === \"external\" && provider.originalContentUrl) {\n // External image: use the URL directly\n media.push(provider.originalContentUrl);\n return \"[User sent an image]\";\n }\n\n // LINE-hosted image: download via Content API\n const imagePath = await this.downloadContent(message.id, \"jpg\");\n if (imagePath) {\n media.push(imagePath);\n const setInfo = message.imageSet\n ? ` (${message.imageSet.index ?? \"?\"}/${message.imageSet.total ?? \"?\"})`\n : \"\";\n return `[User sent an image${setInfo}]`;\n }\n return \"[Image: failed to download]\";\n }\n\n /** Handle video message. */\n private handleVideoMessage(message: LineMessage): string {\n const duration = message.duration\n ? ` (${Math.round(message.duration / 1000)}s)`\n : \"\";\n return `[Video message${duration} -- video analysis not yet supported]`;\n }\n\n /** Handle audio message. */\n private handleAudioMessage(message: LineMessage): string {\n const duration = message.duration\n ? ` (${Math.round(message.duration / 1000)}s)`\n : \"\";\n return `[Audio message${duration} -- transcription not yet supported]`;\n }\n\n /** Handle file message: download from Content API to workspace. */\n private async handleFileMessage(message: LineMessage): Promise<string> {\n const fileName = message.fileName ?? `file_${message.id}`;\n const fileSize = this.formatFileSize(message.fileSize);\n\n const savedPath = await this.downloadToWorkspace(message.id, fileName);\n if (savedPath) {\n return `[File received: ${fileName} (${fileSize}) — saved to ${savedPath}]`;\n }\n return `[File: ${fileName} (${fileSize}) — download failed]`;\n }\n\n /** Handle location message with all available fields. */\n private handleLocationMessage(message: LineMessage): string {\n const parts: string[] = [];\n if (message.title) {\n parts.push(message.title);\n }\n if (message.address) {\n parts.push(message.address);\n }\n if (message.latitude !== null && message.longitude !== null) {\n parts.push(`(${message.latitude}, ${message.longitude})`);\n }\n return `[Location: ${parts.join(\" | \")}]`;\n }\n\n /** Handle sticker message with resource type and keywords. */\n private handleStickerMessage(message: LineMessage): string {\n const parts: string[] = [\"Sticker\"];\n\n // Include sticker text for message stickers\n if (message.text) {\n parts.push(`\"${message.text}\"`);\n }\n\n // Include keywords if available (gives the AI context about the sticker)\n if (message.keywords && message.keywords.length > 0) {\n parts.push(`(${message.keywords.join(\", \")})`);\n }\n\n // Resource type for logging\n const resType = message.stickerResourceType;\n if (resType && resType !== \"STATIC\") {\n parts.push(`[${resType.toLowerCase()}]`);\n }\n\n return `[${parts.join(\" \")}]`;\n }\n\n // ------ Content download ------\n\n /**\n * Download message content (image, video, audio, file) from LINE Content API.\n * Returns the local file path, or null on failure.\n */\n private async downloadContent(\n messageId: string,\n ext: string\n ): Promise<string | null> {\n try {\n const res = await fetch(\n `https://api-data.line.me/v2/bot/message/${messageId}/content`,\n {\n headers: {\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n }\n );\n\n if (!res.ok) {\n console.error(`LINE content download failed (${res.status})`);\n return null;\n }\n\n const buffer = Buffer.from(await res.arrayBuffer());\n\n // Save to temp dir\n const dir = join(tmpdir(), \"nanobot-line-media\");\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n const filePath = join(dir, `${messageId}.${ext}`);\n writeFileSync(filePath, buffer);\n\n console.log(\n `LINE: downloaded content ${messageId} (${buffer.length} bytes)`\n );\n return filePath;\n } catch (error) {\n console.error(\"LINE content download error:\", error);\n return null;\n }\n }\n\n /**\n * Download message content to the workspace uploads directory.\n * Falls back to tmpdir if no workspace is configured.\n * Returns the saved file path, or null on failure.\n */\n private async downloadToWorkspace(\n messageId: string,\n fileName: string\n ): Promise<string | null> {\n try {\n const res = await fetch(\n `https://api-data.line.me/v2/bot/message/${messageId}/content`,\n {\n headers: {\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n }\n );\n\n if (!res.ok) {\n console.error(`LINE content download failed (${res.status})`);\n return null;\n }\n\n const buffer = Buffer.from(await res.arrayBuffer());\n\n // Determine save directory\n const dir = this.workspace\n ? join(this.workspace, \"uploads\")\n : join(tmpdir(), \"nanobot-line-uploads\");\n\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n // Avoid filename collisions by prepending timestamp\n const safeName = `${Date.now()}_${fileName.replaceAll(/[^a-zA-Z0-9._-]/g, \"_\")}`;\n const filePath = join(dir, safeName);\n writeFileSync(filePath, buffer);\n\n console.log(\n `LINE: saved file ${fileName} (${buffer.length} bytes) -> ${filePath}`\n );\n return filePath;\n } catch (error) {\n console.error(\"LINE file download error:\", error);\n return null;\n }\n }\n\n // ------ Signature ------\n\n /**\n * Verify webhook signature.\n * Exposed as a helper so the gateway route can call it.\n */\n verifySignature(rawBody: string, signature: string): boolean {\n return verifyLineSignature(\n this.lineConfig.channelSecret,\n rawBody,\n signature\n );\n }\n\n // ------ Helpers ------\n\n private returnFlexMessage(message: string): messagingApi.Message[] {\n const parsed = JSON.parse(message);\n\n return [\n {\n altText: this.extractAltText(parsed),\n contents: parsed,\n type: \"flex\",\n },\n ];\n }\n\n /** Parse text into LINE message(s). If valid Flex JSON found, send as flex + surrounding text as separate messages. */\n private parseMessage(text: string): messagingApi.Message[] {\n const trimmed = text.trim();\n\n // Try parsing the entire string as JSON first\n try {\n return this.returnFlexMessage(trimmed);\n } catch {\n // Not pure JSON — try to extract a JSON object from anywhere in the string\n }\n\n // Find first '{' in the string and try to extract a JSON object\n const braceIdx = trimmed.indexOf(\"{\");\n if (braceIdx !== -1) {\n const jsonEnd = this.findJsonEnd(trimmed.slice(braceIdx));\n if (jsonEnd > 0) {\n const jsonPart = trimmed.slice(braceIdx, braceIdx + jsonEnd);\n const prefix = trimmed.slice(0, braceIdx).trim();\n const suffix = trimmed.slice(braceIdx + jsonEnd).trim();\n try {\n const messages: messagingApi.Message[] = [];\n if (prefix) {\n messages.push({ text: prefix, type: \"text\" });\n }\n messages.push(...this.returnFlexMessage(jsonPart));\n if (suffix) {\n messages.push({ text: suffix, type: \"text\" });\n }\n return messages;\n } catch {\n // JSON was invalid flex, fall through\n }\n }\n }\n\n return [{ text: trimmed || \"(empty)\", type: \"text\" }];\n }\n\n /** Find the end index of a top-level JSON object in a string. Returns -1 if not found. */\n private findJsonEnd(str: string): number {\n if (str[0] !== \"{\") {\n return -1;\n }\n let depth = 0;\n let inString = false;\n let escape = false;\n\n for (let i = 0; i < str.length; i += 1) {\n const ch = str[i];\n\n if (escape) {\n escape = false;\n continue;\n }\n\n if (ch === \"\\\\\") {\n if (inString) {\n escape = true;\n }\n continue;\n }\n\n if (ch === '\"') {\n inString = !inString;\n continue;\n }\n\n if (inString) {\n continue;\n }\n\n if (ch === \"{\") {\n depth += 1;\n } else if (ch === \"}\") {\n depth -= 1;\n if (depth === 0) {\n return i + 1;\n }\n }\n }\n\n return -1;\n }\n\n private extractText = (obj: unknown, depth = 0): string | null => {\n if (depth > 5) {\n return null;\n }\n if (typeof obj === \"string\") {\n return obj.slice(0, 100);\n }\n if (!obj || typeof obj !== \"object\") {\n return null;\n }\n\n // Handle arrays: iterate elements\n if (Array.isArray(obj)) {\n for (const item of obj) {\n const result = this.extractText(item, depth + 1);\n if (result) {\n return result;\n }\n }\n return null;\n }\n\n const record = obj as Record<string, unknown>;\n if (record.text && typeof record.text === \"string\") {\n return record.text.slice(0, 100);\n }\n if (record.title && typeof record.title === \"string\") {\n return record.title.slice(0, 100);\n }\n\n for (const key of [\"contents\", \"body\", \"header\", \"hero\"]) {\n const result = this.extractText(record[key], depth + 1);\n if (result) {\n return result;\n }\n }\n return null;\n };\n /** Extract alt text from flex content. */\n private extractAltText(contents: Record<string, unknown>): string {\n return this.extractText(contents) || \"Flex Message\";\n }\n\n /** Format file size in human-readable form. */\n private formatFileSize(bytes?: number): string {\n if (bytes === null) {\n return \"unknown size\";\n }\n if (bytes !== undefined && bytes < 1024) {\n return `${bytes} B`;\n }\n if (bytes !== undefined && bytes < 1024 * 1024) {\n return `${(bytes / 1024).toFixed(1)} KB`;\n }\n return `${((bytes ?? 0) / (1024 * 1024)).toFixed(1)} MB`;\n }\n}\n"],"mappings":"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"}
@@ -1,15 +1,15 @@
1
- import { __require } from "../_virtual/_rolldown/runtime.mjs";
2
1
  import { MessageBus } from "../bus/queue.mjs";
3
2
  import { LineChannel, verifyLineSignature } from "./line.mjs";
4
- import { describe, it } from "../node_modules/@vitest/runner/dist/index.mjs";
5
- import { globalExpect } from "../node_modules/vitest/dist/chunks/vi.DgezovHB.mjs";
3
+ import { describe, it } from "../node_modules/.bun/@vitest_runner@2.1.9/node_modules/@vitest/runner/dist/index.mjs";
4
+ import { globalExpect } from "../node_modules/.bun/vitest@2.1.9_7700f9e9ace41f23/node_modules/vitest/dist/chunks/vi.DgezovHB.mjs";
5
+ import { createHmac } from "node:crypto";
6
6
 
7
7
  //#region src/channels/line.test.ts
8
8
  const ch = new LineChannel({
9
- enabled: false,
10
- channelSecret: "test",
9
+ allowFrom: [],
11
10
  channelAccessToken: "test",
12
- allowFrom: []
11
+ channelSecret: "test",
12
+ enabled: false
13
13
  }, new MessageBus());
14
14
  describe("findJsonEnd", () => {
15
15
  it("finds end of simple object", () => {
@@ -46,24 +46,24 @@ describe("findJsonEnd", () => {
46
46
  describe("extractAltText", () => {
47
47
  it("extracts text from a simple bubble", () => {
48
48
  globalExpect(ch.extractAltText({
49
- type: "bubble",
50
49
  body: {
51
50
  type: "box",
52
51
  contents: [{
53
52
  type: "text",
54
53
  text: "Hello World"
55
54
  }]
56
- }
55
+ },
56
+ type: "bubble"
57
57
  })).toBe("Hello World");
58
58
  });
59
59
  it("extracts title when present", () => {
60
60
  globalExpect(ch.extractAltText({
61
- type: "bubble",
62
- title: "Card Title",
63
61
  body: {
64
62
  type: "box",
65
63
  contents: []
66
- }
64
+ },
65
+ title: "Card Title",
66
+ type: "bubble"
67
67
  })).toBe("Card Title");
68
68
  });
69
69
  it("falls back to 'Flex Message' when no text found", () => {
@@ -72,15 +72,15 @@ describe("extractAltText", () => {
72
72
  it("truncates long text to 100 chars", () => {
73
73
  const longText = "x".repeat(200);
74
74
  globalExpect(ch.extractAltText({
75
- type: "bubble",
76
75
  body: {
77
76
  type: "box",
78
77
  contents: [{
79
78
  type: "text",
80
79
  text: longText
81
80
  }]
82
- }
83
- }).length).toBe(100);
81
+ },
82
+ type: "bubble"
83
+ })).toHaveLength(100);
84
84
  });
85
85
  });
86
86
  describe("parseMessage", () => {
@@ -97,7 +97,6 @@ describe("parseMessage", () => {
97
97
  });
98
98
  it("parses pure JSON flex message", () => {
99
99
  const flex = JSON.stringify({
100
- type: "bubble",
101
100
  body: {
102
101
  type: "box",
103
102
  layout: "vertical",
@@ -105,7 +104,8 @@ describe("parseMessage", () => {
105
104
  type: "text",
106
105
  text: "Test"
107
106
  }]
108
- }
107
+ },
108
+ type: "bubble"
109
109
  });
110
110
  const result = ch.parseMessage(flex);
111
111
  globalExpect(result).toHaveLength(1);
@@ -114,7 +114,6 @@ describe("parseMessage", () => {
114
114
  });
115
115
  it("handles JSON with trailing text", () => {
116
116
  const input = JSON.stringify({
117
- type: "bubble",
118
117
  body: {
119
118
  type: "box",
120
119
  layout: "vertical",
@@ -122,7 +121,8 @@ describe("parseMessage", () => {
122
121
  type: "text",
123
122
  text: "Card"
124
123
  }]
125
- }
124
+ },
125
+ type: "bubble"
126
126
  }) + "\n\nHere is some extra text!";
127
127
  const result = ch.parseMessage(input);
128
128
  globalExpect(result).toHaveLength(2);
@@ -132,7 +132,6 @@ describe("parseMessage", () => {
132
132
  });
133
133
  it("handles JSON with Japanese trailing text", () => {
134
134
  const input = JSON.stringify({
135
- type: "bubble",
136
135
  body: {
137
136
  type: "box",
138
137
  layout: "vertical",
@@ -140,7 +139,8 @@ describe("parseMessage", () => {
140
139
  type: "text",
141
140
  text: "運勢"
142
141
  }]
143
- }
142
+ },
143
+ type: "bubble"
144
144
  }) + "\n\n記録できた!今日もがんばろう。";
145
145
  const result = ch.parseMessage(input);
146
146
  globalExpect(result).toHaveLength(2);
@@ -150,14 +150,14 @@ describe("parseMessage", () => {
150
150
  });
151
151
  it("handles whitespace around JSON", () => {
152
152
  const flex = JSON.stringify({
153
- type: "bubble",
154
153
  body: {
155
154
  type: "box",
156
155
  contents: [{
157
156
  type: "text",
158
157
  text: "OK"
159
158
  }]
160
- }
159
+ },
160
+ type: "bubble"
161
161
  });
162
162
  const result = ch.parseMessage(" " + flex + " ");
163
163
  globalExpect(result).toHaveLength(1);
@@ -192,18 +192,17 @@ describe("parseMessage", () => {
192
192
  globalExpect(result[1].text).toBe("記録できた!今日水やりしたことになってる。緑色のボタンがカワイイでしょ。");
193
193
  });
194
194
  });
195
- describe("verifyLineSignature", () => {
195
+ describe("verifyLineSignature works", () => {
196
196
  it("returns true for valid signature", () => {
197
197
  const secret = "test-secret";
198
198
  const body = "{\"events\":[]}";
199
- const { createHmac } = __require("node:crypto");
200
- globalExpect(verifyLineSignature(secret, body, createHmac("sha256", secret).update(body).digest("base64"))).toBe(true);
199
+ globalExpect(verifyLineSignature(secret, body, createHmac("sha256", secret).update(body).digest("base64"))).toBeTruthy();
201
200
  });
202
201
  it("returns false for invalid signature", () => {
203
- globalExpect(verifyLineSignature("secret", "body", "bad-sig")).toBe(false);
202
+ globalExpect(verifyLineSignature("secret", "body", "bad-sig")).toBeFalsy();
204
203
  });
205
204
  it("returns false for empty signature", () => {
206
- globalExpect(verifyLineSignature("secret", "body", "")).toBe(false);
205
+ globalExpect(verifyLineSignature("secret", "body", "")).toBeFalsy();
207
206
  });
208
207
  });
209
208