@hera-al/server 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +325 -0
  3. package/bundled/apple-notes/SKILL.md +77 -0
  4. package/bundled/apple-reminders/SKILL.md +96 -0
  5. package/bundled/blogwatcher/SKILL.md +69 -0
  6. package/bundled/camsnap/SKILL.md +45 -0
  7. package/bundled/discord/SKILL.md +578 -0
  8. package/bundled/gemini/SKILL.md +43 -0
  9. package/bundled/gifgrep/SKILL.md +79 -0
  10. package/bundled/github/SKILL.md +77 -0
  11. package/bundled/gog/SKILL.md +116 -0
  12. package/bundled/goplaces/SKILL.md +52 -0
  13. package/bundled/himalaya/SKILL.md +257 -0
  14. package/bundled/himalaya/references/configuration.md +184 -0
  15. package/bundled/himalaya/references/message-composition.md +199 -0
  16. package/bundled/homebrew/SKILL.md +82 -0
  17. package/bundled/local-places/SERVER_README.md +101 -0
  18. package/bundled/local-places/SKILL.md +102 -0
  19. package/bundled/local-places/pyproject.toml +21 -0
  20. package/bundled/local-places/src/local_places/__init__.py +2 -0
  21. package/bundled/local-places/src/local_places/google_places.py +314 -0
  22. package/bundled/local-places/src/local_places/main.py +65 -0
  23. package/bundled/local-places/src/local_places/schemas.py +107 -0
  24. package/bundled/markitdown/SKILL.md +96 -0
  25. package/bundled/mcporter/SKILL.md +61 -0
  26. package/bundled/merge-pr/SKILL.md +187 -0
  27. package/bundled/merge-pr/agents/openai.yaml +4 -0
  28. package/bundled/nano-banana-pro/SKILL.md +58 -0
  29. package/bundled/nano-banana-pro/scripts/generate_image.py +184 -0
  30. package/bundled/nano-pdf/SKILL.md +38 -0
  31. package/bundled/open-prose/README.md +25 -0
  32. package/bundled/open-prose/index.ts +5 -0
  33. package/bundled/open-prose/openclaw.plugin.json +11 -0
  34. package/bundled/open-prose/package.json +15 -0
  35. package/bundled/open-prose/skills/prose/LICENSE +21 -0
  36. package/bundled/open-prose/skills/prose/SKILL.md +323 -0
  37. package/bundled/open-prose/skills/prose/alt-borges.md +141 -0
  38. package/bundled/open-prose/skills/prose/alts/arabian-nights.md +358 -0
  39. package/bundled/open-prose/skills/prose/alts/borges.md +360 -0
  40. package/bundled/open-prose/skills/prose/alts/folk.md +322 -0
  41. package/bundled/open-prose/skills/prose/alts/homer.md +346 -0
  42. package/bundled/open-prose/skills/prose/alts/kafka.md +373 -0
  43. package/bundled/open-prose/skills/prose/compiler.md +2971 -0
  44. package/bundled/open-prose/skills/prose/examples/01-hello-world.prose +4 -0
  45. package/bundled/open-prose/skills/prose/examples/02-research-and-summarize.prose +6 -0
  46. package/bundled/open-prose/skills/prose/examples/03-code-review.prose +17 -0
  47. package/bundled/open-prose/skills/prose/examples/04-write-and-refine.prose +14 -0
  48. package/bundled/open-prose/skills/prose/examples/05-debug-issue.prose +20 -0
  49. package/bundled/open-prose/skills/prose/examples/06-explain-codebase.prose +17 -0
  50. package/bundled/open-prose/skills/prose/examples/07-refactor.prose +20 -0
  51. package/bundled/open-prose/skills/prose/examples/08-blog-post.prose +20 -0
  52. package/bundled/open-prose/skills/prose/examples/09-research-with-agents.prose +25 -0
  53. package/bundled/open-prose/skills/prose/examples/10-code-review-agents.prose +32 -0
  54. package/bundled/open-prose/skills/prose/examples/11-skills-and-imports.prose +27 -0
  55. package/bundled/open-prose/skills/prose/examples/12-secure-agent-permissions.prose +43 -0
  56. package/bundled/open-prose/skills/prose/examples/13-variables-and-context.prose +51 -0
  57. package/bundled/open-prose/skills/prose/examples/14-composition-blocks.prose +48 -0
  58. package/bundled/open-prose/skills/prose/examples/15-inline-sequences.prose +23 -0
  59. package/bundled/open-prose/skills/prose/examples/16-parallel-reviews.prose +19 -0
  60. package/bundled/open-prose/skills/prose/examples/17-parallel-research.prose +19 -0
  61. package/bundled/open-prose/skills/prose/examples/18-mixed-parallel-sequential.prose +36 -0
  62. package/bundled/open-prose/skills/prose/examples/19-advanced-parallel.prose +71 -0
  63. package/bundled/open-prose/skills/prose/examples/20-fixed-loops.prose +20 -0
  64. package/bundled/open-prose/skills/prose/examples/21-pipeline-operations.prose +35 -0
  65. package/bundled/open-prose/skills/prose/examples/22-error-handling.prose +51 -0
  66. package/bundled/open-prose/skills/prose/examples/23-retry-with-backoff.prose +63 -0
  67. package/bundled/open-prose/skills/prose/examples/24-choice-blocks.prose +86 -0
  68. package/bundled/open-prose/skills/prose/examples/25-conditionals.prose +114 -0
  69. package/bundled/open-prose/skills/prose/examples/26-parameterized-blocks.prose +100 -0
  70. package/bundled/open-prose/skills/prose/examples/27-string-interpolation.prose +105 -0
  71. package/bundled/open-prose/skills/prose/examples/28-automated-pr-review.prose +37 -0
  72. package/bundled/open-prose/skills/prose/examples/28-gas-town.prose +1572 -0
  73. package/bundled/open-prose/skills/prose/examples/29-captains-chair.prose +218 -0
  74. package/bundled/open-prose/skills/prose/examples/30-captains-chair-simple.prose +42 -0
  75. package/bundled/open-prose/skills/prose/examples/31-captains-chair-with-memory.prose +145 -0
  76. package/bundled/open-prose/skills/prose/examples/33-pr-review-autofix.prose +168 -0
  77. package/bundled/open-prose/skills/prose/examples/34-content-pipeline.prose +204 -0
  78. package/bundled/open-prose/skills/prose/examples/35-feature-factory.prose +296 -0
  79. package/bundled/open-prose/skills/prose/examples/36-bug-hunter.prose +237 -0
  80. package/bundled/open-prose/skills/prose/examples/37-the-forge.prose +1474 -0
  81. package/bundled/open-prose/skills/prose/examples/38-skill-scan.prose +455 -0
  82. package/bundled/open-prose/skills/prose/examples/39-architect-by-simulation.prose +277 -0
  83. package/bundled/open-prose/skills/prose/examples/40-rlm-self-refine.prose +32 -0
  84. package/bundled/open-prose/skills/prose/examples/41-rlm-divide-conquer.prose +38 -0
  85. package/bundled/open-prose/skills/prose/examples/42-rlm-filter-recurse.prose +46 -0
  86. package/bundled/open-prose/skills/prose/examples/43-rlm-pairwise.prose +50 -0
  87. package/bundled/open-prose/skills/prose/examples/44-run-endpoint-ux-test.prose +261 -0
  88. package/bundled/open-prose/skills/prose/examples/45-plugin-release.prose +159 -0
  89. package/bundled/open-prose/skills/prose/examples/45-run-endpoint-ux-test-with-remediation.prose +637 -0
  90. package/bundled/open-prose/skills/prose/examples/46-run-endpoint-ux-test-fast.prose +148 -0
  91. package/bundled/open-prose/skills/prose/examples/46-workflow-crystallizer.prose +225 -0
  92. package/bundled/open-prose/skills/prose/examples/47-language-self-improvement.prose +356 -0
  93. package/bundled/open-prose/skills/prose/examples/48-habit-miner.prose +445 -0
  94. package/bundled/open-prose/skills/prose/examples/49-prose-run-retrospective.prose +210 -0
  95. package/bundled/open-prose/skills/prose/examples/README.md +391 -0
  96. package/bundled/open-prose/skills/prose/examples/roadmap/README.md +22 -0
  97. package/bundled/open-prose/skills/prose/examples/roadmap/iterative-refinement.prose +20 -0
  98. package/bundled/open-prose/skills/prose/examples/roadmap/parallel-review.prose +18 -0
  99. package/bundled/open-prose/skills/prose/examples/roadmap/simple-pipeline.prose +17 -0
  100. package/bundled/open-prose/skills/prose/examples/roadmap/syntax/open-prose-syntax.prose +223 -0
  101. package/bundled/open-prose/skills/prose/guidance/antipatterns.md +951 -0
  102. package/bundled/open-prose/skills/prose/guidance/patterns.md +700 -0
  103. package/bundled/open-prose/skills/prose/guidance/system-prompt.md +180 -0
  104. package/bundled/open-prose/skills/prose/help.md +144 -0
  105. package/bundled/open-prose/skills/prose/lib/README.md +108 -0
  106. package/bundled/open-prose/skills/prose/lib/calibrator.prose +215 -0
  107. package/bundled/open-prose/skills/prose/lib/cost-analyzer.prose +174 -0
  108. package/bundled/open-prose/skills/prose/lib/error-forensics.prose +250 -0
  109. package/bundled/open-prose/skills/prose/lib/inspector.prose +196 -0
  110. package/bundled/open-prose/skills/prose/lib/profiler.prose +460 -0
  111. package/bundled/open-prose/skills/prose/lib/program-improver.prose +275 -0
  112. package/bundled/open-prose/skills/prose/lib/project-memory.prose +118 -0
  113. package/bundled/open-prose/skills/prose/lib/user-memory.prose +93 -0
  114. package/bundled/open-prose/skills/prose/lib/vm-improver.prose +243 -0
  115. package/bundled/open-prose/skills/prose/primitives/session.md +593 -0
  116. package/bundled/open-prose/skills/prose/prose.md +1237 -0
  117. package/bundled/open-prose/skills/prose/state/filesystem.md +498 -0
  118. package/bundled/open-prose/skills/prose/state/in-context.md +384 -0
  119. package/bundled/open-prose/skills/prose/state/postgres.md +880 -0
  120. package/bundled/open-prose/skills/prose/state/sqlite.md +574 -0
  121. package/bundled/peekaboo/SKILL.md +190 -0
  122. package/bundled/prepare-pr/SKILL.md +277 -0
  123. package/bundled/prepare-pr/agents/openai.yaml +4 -0
  124. package/bundled/review-pr/SKILL.md +228 -0
  125. package/bundled/review-pr/agents/openai.yaml +4 -0
  126. package/bundled/sag/SKILL.md +87 -0
  127. package/bundled/skill-creator/SKILL.md +370 -0
  128. package/bundled/skill-creator/license.txt +202 -0
  129. package/bundled/skill-creator/scripts/init_skill.py +378 -0
  130. package/bundled/skill-creator/scripts/package_skill.py +111 -0
  131. package/bundled/skill-creator/scripts/quick_validate.py +101 -0
  132. package/bundled/spotify-player/SKILL.md +64 -0
  133. package/bundled/ssh/SKILL.md +119 -0
  134. package/bundled/summarize/SKILL.md +87 -0
  135. package/bundled/video-frames/SKILL.md +46 -0
  136. package/bundled/video-frames/scripts/frame.sh +81 -0
  137. package/bundled/voice-call/SKILL.md +45 -0
  138. package/bundled/wacli/SKILL.md +72 -0
  139. package/bundled/weather/SKILL.md +54 -0
  140. package/dist/agent/agent-service.d.ts +88 -0
  141. package/dist/agent/agent-service.js +1 -0
  142. package/dist/agent/message-queue.d.ts +24 -0
  143. package/dist/agent/message-queue.js +1 -0
  144. package/dist/agent/prompt-builder.d.ts +58 -0
  145. package/dist/agent/prompt-builder.js +1 -0
  146. package/dist/agent/session-agent.d.ts +197 -0
  147. package/dist/agent/session-agent.js +1 -0
  148. package/dist/agent/session-db.d.ts +26 -0
  149. package/dist/agent/session-db.js +1 -0
  150. package/dist/agent/session-error-handler.d.ts +37 -0
  151. package/dist/agent/session-error-handler.js +1 -0
  152. package/dist/agent/session-manager.d.ts +19 -0
  153. package/dist/agent/session-manager.js +1 -0
  154. package/dist/agent/workspace-files.d.ts +51 -0
  155. package/dist/agent/workspace-files.js +1 -0
  156. package/dist/auth/auth-middleware.d.ts +9 -0
  157. package/dist/auth/auth-middleware.js +1 -0
  158. package/dist/auth/node-signature-db.d.ts +30 -0
  159. package/dist/auth/node-signature-db.js +1 -0
  160. package/dist/auth/token-db.d.ts +38 -0
  161. package/dist/auth/token-db.js +1 -0
  162. package/dist/browser/browser-service.d.ts +9 -0
  163. package/dist/browser/browser-service.js +1 -0
  164. package/dist/channels/channel.d.ts +2 -0
  165. package/dist/channels/channel.js +1 -0
  166. package/dist/channels/responses.d.ts +21 -0
  167. package/dist/channels/responses.js +1 -0
  168. package/dist/commands/clear.d.ts +7 -0
  169. package/dist/commands/clear.js +1 -0
  170. package/dist/commands/cmd.d.ts +7 -0
  171. package/dist/commands/cmd.js +1 -0
  172. package/dist/commands/coder.d.ts +12 -0
  173. package/dist/commands/coder.js +1 -0
  174. package/dist/commands/command-registry.d.ts +12 -0
  175. package/dist/commands/command-registry.js +1 -0
  176. package/dist/commands/command.d.ts +22 -0
  177. package/dist/commands/command.js +1 -0
  178. package/dist/commands/compact.d.ts +7 -0
  179. package/dist/commands/compact.js +1 -0
  180. package/dist/commands/customsubagents.d.ts +15 -0
  181. package/dist/commands/customsubagents.js +1 -0
  182. package/dist/commands/help.d.ts +9 -0
  183. package/dist/commands/help.js +1 -0
  184. package/dist/commands/mcp.d.ts +9 -0
  185. package/dist/commands/mcp.js +1 -0
  186. package/dist/commands/model.d.ts +22 -0
  187. package/dist/commands/model.js +1 -0
  188. package/dist/commands/models.d.ts +11 -0
  189. package/dist/commands/models.js +1 -0
  190. package/dist/commands/new.d.ts +7 -0
  191. package/dist/commands/new.js +1 -0
  192. package/dist/commands/plugin.d.ts +7 -0
  193. package/dist/commands/plugin.js +1 -0
  194. package/dist/commands/sandbox.d.ts +12 -0
  195. package/dist/commands/sandbox.js +1 -0
  196. package/dist/commands/showtool.d.ts +12 -0
  197. package/dist/commands/showtool.js +1 -0
  198. package/dist/commands/status.d.ts +24 -0
  199. package/dist/commands/status.js +1 -0
  200. package/dist/commands/stop.d.ts +10 -0
  201. package/dist/commands/stop.js +1 -0
  202. package/dist/commands/subagents.d.ts +12 -0
  203. package/dist/commands/subagents.js +1 -0
  204. package/dist/commands/usage.d.ts +25 -0
  205. package/dist/commands/usage.js +1 -0
  206. package/dist/commands/useplugin.d.ts +7 -0
  207. package/dist/commands/useplugin.js +1 -0
  208. package/dist/config-watcher.d.ts +14 -0
  209. package/dist/config-watcher.js +1 -0
  210. package/dist/config.d.ts +267 -0
  211. package/dist/config.js +1 -0
  212. package/dist/cron/cron-service.d.ts +57 -0
  213. package/dist/cron/cron-service.js +1 -0
  214. package/dist/cron/heartbeat-token.d.ts +29 -0
  215. package/dist/cron/heartbeat-token.js +1 -0
  216. package/dist/cron/schedule.d.ts +3 -0
  217. package/dist/cron/schedule.js +1 -0
  218. package/dist/cron/store.d.ts +4 -0
  219. package/dist/cron/store.js +1 -0
  220. package/dist/cron/types.d.ts +47 -0
  221. package/dist/cron/types.js +1 -0
  222. package/dist/gateway/bridge.d.ts +38 -0
  223. package/dist/gateway/bridge.js +1 -0
  224. package/dist/gateway/channel-manager.d.ts +45 -0
  225. package/dist/gateway/channel-manager.js +1 -0
  226. package/dist/gateway/channels/qr-image.d.ts +5 -0
  227. package/dist/gateway/channels/qr-image.js +1 -0
  228. package/dist/gateway/channels/telegram.d.ts +39 -0
  229. package/dist/gateway/channels/telegram.js +1 -0
  230. package/dist/gateway/channels/webchat.d.ts +51 -0
  231. package/dist/gateway/channels/webchat.js +1 -0
  232. package/dist/gateway/channels/whatsapp.d.ts +40 -0
  233. package/dist/gateway/channels/whatsapp.js +1 -0
  234. package/dist/gateway/node-registry.d.ts +38 -0
  235. package/dist/gateway/node-registry.js +1 -0
  236. package/dist/heracli/index.d.ts +3 -0
  237. package/dist/heracli/index.js +2 -0
  238. package/dist/heracli/logs.d.ts +13 -0
  239. package/dist/heracli/logs.js +1 -0
  240. package/dist/heracli/security/audit.d.ts +17 -0
  241. package/dist/heracli/security/audit.js +1 -0
  242. package/dist/heracli/security/checks/channel-policies.d.ts +6 -0
  243. package/dist/heracli/security/checks/channel-policies.js +1 -0
  244. package/dist/heracli/security/checks/credentials.d.ts +6 -0
  245. package/dist/heracli/security/checks/credentials.js +1 -0
  246. package/dist/heracli/security/checks/fs-permissions.d.ts +6 -0
  247. package/dist/heracli/security/checks/fs-permissions.js +1 -0
  248. package/dist/heracli/security/checks/network.d.ts +4 -0
  249. package/dist/heracli/security/checks/network.js +1 -0
  250. package/dist/heracli/security/report.d.ts +4 -0
  251. package/dist/heracli/security/report.js +1 -0
  252. package/dist/index.d.ts +3 -0
  253. package/dist/index.js +2 -0
  254. package/dist/installer/hera.d.ts +3 -0
  255. package/dist/installer/hera.js +2 -0
  256. package/dist/media/message-processor.d.ts +23 -0
  257. package/dist/media/message-processor.js +1 -0
  258. package/dist/memory/memory-manager.d.ts +21 -0
  259. package/dist/memory/memory-manager.js +1 -0
  260. package/dist/memory/memory-provider.d.ts +22 -0
  261. package/dist/memory/memory-provider.js +1 -0
  262. package/dist/memory/memory-search.d.ts +102 -0
  263. package/dist/memory/memory-search.js +1 -0
  264. package/dist/memory/recall-strategies.d.ts +2 -0
  265. package/dist/memory/recall-strategies.js +1 -0
  266. package/dist/nostromo/auth.d.ts +29 -0
  267. package/dist/nostromo/auth.js +1 -0
  268. package/dist/nostromo/nostromo.d.ts +23 -0
  269. package/dist/nostromo/nostromo.js +1 -0
  270. package/dist/nostromo/ui-html-layout.d.ts +3 -0
  271. package/dist/nostromo/ui-html-layout.js +1 -0
  272. package/dist/nostromo/ui-html-modals.d.ts +3 -0
  273. package/dist/nostromo/ui-html-modals.js +1 -0
  274. package/dist/nostromo/ui-js-agent.d.ts +3 -0
  275. package/dist/nostromo/ui-js-agent.js +1 -0
  276. package/dist/nostromo/ui-js-channels.d.ts +3 -0
  277. package/dist/nostromo/ui-js-channels.js +1 -0
  278. package/dist/nostromo/ui-js-competences.d.ts +3 -0
  279. package/dist/nostromo/ui-js-competences.js +1 -0
  280. package/dist/nostromo/ui-js-config.d.ts +3 -0
  281. package/dist/nostromo/ui-js-config.js +1 -0
  282. package/dist/nostromo/ui-js-core.d.ts +3 -0
  283. package/dist/nostromo/ui-js-core.js +1 -0
  284. package/dist/nostromo/ui-js-ops.d.ts +3 -0
  285. package/dist/nostromo/ui-js-ops.js +1 -0
  286. package/dist/nostromo/ui-js-prompts.d.ts +3 -0
  287. package/dist/nostromo/ui-js-prompts.js +1 -0
  288. package/dist/nostromo/ui-styles.d.ts +3 -0
  289. package/dist/nostromo/ui-styles.js +1 -0
  290. package/dist/nostromo/ui.d.ts +2 -0
  291. package/dist/nostromo/ui.js +1 -0
  292. package/dist/server.d.ts +80 -0
  293. package/dist/server.js +1 -0
  294. package/dist/stt/local-whisper.d.ts +9 -0
  295. package/dist/stt/local-whisper.js +1 -0
  296. package/dist/stt/openai-whisper.d.ts +14 -0
  297. package/dist/stt/openai-whisper.js +1 -0
  298. package/dist/stt/stt-loader.d.ts +4 -0
  299. package/dist/stt/stt-loader.js +1 -0
  300. package/dist/stt/stt-provider.d.ts +4 -0
  301. package/dist/stt/stt-provider.js +1 -0
  302. package/dist/tools/browser-tools.d.ts +9 -0
  303. package/dist/tools/browser-tools.js +1 -0
  304. package/dist/tools/cron-tools.d.ts +4 -0
  305. package/dist/tools/cron-tools.js +1 -0
  306. package/dist/tools/memory-tools.d.ts +3 -0
  307. package/dist/tools/memory-tools.js +1 -0
  308. package/dist/tools/message-tools.d.ts +5 -0
  309. package/dist/tools/message-tools.js +1 -0
  310. package/dist/tools/node-tools.d.ts +3 -0
  311. package/dist/tools/node-tools.js +1 -0
  312. package/dist/tools/server-tools.d.ts +2 -0
  313. package/dist/tools/server-tools.js +1 -0
  314. package/dist/tools/tts-tools.d.ts +3 -0
  315. package/dist/tools/tts-tools.js +1 -0
  316. package/dist/tts/tts-service.d.ts +19 -0
  317. package/dist/tts/tts-service.js +1 -0
  318. package/dist/utils/chunk.d.ts +3 -0
  319. package/dist/utils/chunk.js +1 -0
  320. package/dist/utils/logger.d.ts +16 -0
  321. package/dist/utils/logger.js +1 -0
  322. package/dist/utils/markdown/fences.d.ts +11 -0
  323. package/dist/utils/markdown/fences.js +1 -0
  324. package/dist/utils/markdown/ir.d.ts +33 -0
  325. package/dist/utils/markdown/ir.js +1 -0
  326. package/dist/utils/markdown/render.d.ts +19 -0
  327. package/dist/utils/markdown/render.js +1 -0
  328. package/dist/utils/markdown/tables.d.ts +3 -0
  329. package/dist/utils/markdown/tables.js +1 -0
  330. package/dist/utils/media-response.d.ts +29 -0
  331. package/dist/utils/media-response.js +1 -0
  332. package/dist/utils/package-paths.d.ts +5 -0
  333. package/dist/utils/package-paths.js +1 -0
  334. package/dist/utils/telegram-format.d.ts +13 -0
  335. package/dist/utils/telegram-format.js +1 -0
  336. package/installationPkg/.env.example +26 -0
  337. package/installationPkg/AGENTS.md +143 -0
  338. package/installationPkg/BOOTSTRAP.md +45 -0
  339. package/installationPkg/CBINT.json +16 -0
  340. package/installationPkg/HEARTBEAT.md +5 -0
  341. package/installationPkg/IDENTITY.md +7 -0
  342. package/installationPkg/SOUL.md +36 -0
  343. package/installationPkg/SYSTEM_PROMPT.md +55 -0
  344. package/installationPkg/SYSTEM_PROMPT_SUBAGENT.md +40 -0
  345. package/installationPkg/TOOLS.md +36 -0
  346. package/installationPkg/USER.md +11 -0
  347. package/installationPkg/config.example.yaml +291 -0
  348. package/package.json +95 -0
@@ -0,0 +1,51 @@
1
+ import type { WebSocket } from "ws";
2
+ import type { ChannelAdapter, MessageHandler, InlineButton } from "../bridge.js";
3
+ /**
4
+ * Build a deterministic webchat session key from a node's display name and ID.
5
+ * Format: `webchat:{sanitizedName}-{nodeId first 8 chars}`
6
+ */
7
+ export declare function buildWebChatId(displayName: string, nodeId: string): string;
8
+ export declare class WebChatChannel implements ChannelAdapter {
9
+ readonly name = "webchat";
10
+ private connections;
11
+ private onMessage;
12
+ private typingIntervals;
13
+ private inflightCount;
14
+ start(onMessage: MessageHandler): Promise<void>;
15
+ registerConnection(chatId: string, ws: WebSocket): void;
16
+ unregisterConnection(chatId: string): void;
17
+ /**
18
+ * Remove all connections associated with a specific WebSocket.
19
+ * Called on node disconnect — a single WS may have multiple chatIds.
20
+ */
21
+ unregisterByWs(ws: WebSocket): void;
22
+ /**
23
+ * Handle a chat message arriving from a paired node.
24
+ * Fire-and-forget: caller does not await.
25
+ */
26
+ handleNodeChat(chatId: string, nodeId: string, msg: {
27
+ text?: string;
28
+ attachments?: RawAttachment[];
29
+ }): Promise<void>;
30
+ sendText(chatId: string, text: string): Promise<void>;
31
+ sendButtons(chatId: string, text: string, buttons: InlineButton[]): Promise<void>;
32
+ setTyping(chatId: string): Promise<void>;
33
+ clearTyping(chatId: string): Promise<void>;
34
+ releaseTyping(chatId: string): Promise<void>;
35
+ sendAudio(chatId: string, filePath: string, asVoice?: boolean): Promise<void>;
36
+ stop(): Promise<void>;
37
+ private startTypingInterval;
38
+ private stopTypingInterval;
39
+ private resendTypingIfActive;
40
+ private sendWs;
41
+ }
42
+ interface RawAttachment {
43
+ type: string;
44
+ mimeType?: string;
45
+ fileName?: string;
46
+ duration?: number;
47
+ caption?: string;
48
+ data: string;
49
+ }
50
+ export {};
51
+ //# sourceMappingURL=webchat.d.ts.map
@@ -0,0 +1 @@
1
+ import{readFileSync as t}from"node:fs";import{basename as e,extname as n}from"node:path";import{parseMediaLines as s}from"../../utils/media-response.js";import{createLogger as i}from"../../utils/logger.js";const a=i("WebChat");export function buildWebChatId(t,e){return`${(t||"node").toLowerCase().replace(/[^a-z0-9_-]/g,"_").replace(/_+/g,"_").replace(/^_|_$/g,"")||"node"}-${e.slice(0,8)}`}export class WebChatChannel{name="webchat";connections=new Map;onMessage=null;typingIntervals=new Map;inflightCount=new Map;async start(t){this.onMessage=t,a.info("WebChat channel started (virtual — connections managed by Nostromo)")}registerConnection(t,e){const n=!this.connections.has(t);this.connections.set(t,e),n&&a.info(`WebChat connection registered: ${t}`)}unregisterConnection(t){this.connections.delete(t)&&a.info(`WebChat connection unregistered: ${t}`)}unregisterByWs(t){for(const[e,n]of this.connections)n===t&&(this.connections.delete(e),a.info(`WebChat connection unregistered: ${e}`))}async handleNodeChat(t,e,n){if(!this.onMessage)return void a.warn("WebChat: message received but no onMessage handler registered");const i=[];if(n.attachments&&Array.isArray(n.attachments))for(const t of n.attachments)i.push({type:t.type,mimeType:t.mimeType,fileName:t.fileName,duration:t.duration,caption:t.caption,getBuffer:()=>Promise.resolve(Buffer.from(t.data,"base64"))});const o={chatId:t,userId:e,channelName:"webchat",text:n.text,attachments:i,username:t};this.startTypingInterval(t);try{const e=await this.onMessage(o),{textParts:n,mediaEntries:i}=s(e);for(const e of i)try{await this.sendAudio(t,e.path,e.asVoice)}catch(e){a.error(`WebChat: failed to send audio to ${t}: ${e}`)}const r=n.join("\n").trim();r&&this.sendWs(t,{type:"chat_response",role:"assistant",text:r}),this.resendTypingIfActive(t)}catch(e){a.error(`WebChat: error handling message from ${t}: ${e}`),this.sendWs(t,{type:"chat_response",role:"assistant",text:"Error processing message."})}}async sendText(t,e){this.sendWs(t,{type:"chat_message",role:"assistant",text:e}),this.resendTypingIfActive(t)}async sendButtons(t,e,n){this.sendWs(t,{type:"chat_message",role:"assistant",text:e,buttons:n.map(t=>({text:t.text,callbackData:t.callbackData??t.text,...t.url?{url:t.url}:{}}))}),this.resendTypingIfActive(t)}async setTyping(t){this.typingIntervals.has(t)?this.sendWs(t,{type:"typing_indicator",typing:!0}):this.startTypingInterval(t)}async clearTyping(t){this.inflightCount.delete(t);const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.sendWs(t,{type:"typing_indicator",typing:!1})}async releaseTyping(t){this.stopTypingInterval(t)}async sendAudio(s,i,o){try{const a=t(i),r=e(i),c=n(i).toLowerCase().replace(".",""),h={mp3:"audio/mpeg",ogg:"audio/ogg",opus:"audio/ogg",wav:"audio/wav",m4a:"audio/mp4",flac:"audio/flac"}[c]||"audio/mpeg";this.sendWs(s,{type:"chat_media",role:"assistant",mediaType:"audio",mimeType:h,fileName:r,data:a.toString("base64"),asVoice:o??!1}),this.resendTypingIfActive(s)}catch(t){a.error(`WebChat: failed to read audio file ${i}: ${t}`)}}async stop(){for(const[t,e]of this.typingIntervals)clearInterval(e),this.sendWs(t,{type:"typing_indicator",typing:!1});this.typingIntervals.clear(),this.inflightCount.clear(),this.onMessage=null,a.info("WebChat channel stopped")}startTypingInterval(t){const e=(this.inflightCount.get(t)??0)+1;if(this.inflightCount.set(t,e),e>1)return;const n=this.typingIntervals.get(t);n&&(clearInterval(n),this.typingIntervals.delete(t)),this.sendWs(t,{type:"typing_indicator",typing:!0});const s=setInterval(()=>{(this.inflightCount.get(t)??0)>0?this.sendWs(t,{type:"typing_indicator",typing:!0}):(clearInterval(s),this.typingIntervals.delete(t),this.sendWs(t,{type:"typing_indicator",typing:!1}))},4e3);this.typingIntervals.set(t,s)}stopTypingInterval(t){const e=(this.inflightCount.get(t)??1)-1;if(e>0)this.inflightCount.set(t,e);else{this.inflightCount.delete(t);const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.sendWs(t,{type:"typing_indicator",typing:!1})}}resendTypingIfActive(t){this.typingIntervals.has(t)&&this.sendWs(t,{type:"typing_indicator",typing:!0})}sendWs(t,e){const n=this.connections.get(t);if(n&&n.readyState===n.OPEN)try{n.send(JSON.stringify({...e,chatId:t}))}catch(e){a.error(`WebChat: failed to send to ${t}: ${e}`)}else a.warn(`WebChat: no active connection for chatId ${t}`)}}
@@ -0,0 +1,40 @@
1
+ import type { ChannelAdapter, MessageHandler } from "../bridge.js";
2
+ export interface WhatsAppAccountConfig {
3
+ authDir: string;
4
+ dmPolicy: string;
5
+ allowFrom?: Array<string | number>;
6
+ }
7
+ export type QrCallback = (dataUrl: string | null, connected: boolean, error?: string) => void;
8
+ export declare class WhatsAppChannel implements ChannelAdapter {
9
+ readonly name = "whatsapp";
10
+ private sock;
11
+ private config;
12
+ private qrCallback;
13
+ private connected;
14
+ private stopping;
15
+ private typingIntervals;
16
+ private inflightTyping;
17
+ private inflightCount;
18
+ constructor(config: WhatsAppAccountConfig, inflightTyping?: boolean);
19
+ setQrCallback(cb: QrCallback): void;
20
+ isConnected(): boolean;
21
+ start(onMessage: MessageHandler): Promise<void>;
22
+ private connect;
23
+ /**
24
+ * Process an incoming message in the background.
25
+ * Not awaited by the Baileys event handler so the event loop stays unblocked.
26
+ */
27
+ private handleIncoming;
28
+ private checkAccess;
29
+ sendText(chatId: string, text: string): Promise<void>;
30
+ setTyping(chatId: string): Promise<void>;
31
+ /** Re-send composing if an interval is active (another handler is still processing). */
32
+ private resendTypingIfActive;
33
+ clearTyping(chatId: string): Promise<void>;
34
+ releaseTyping(chatId: string): Promise<void>;
35
+ private startTypingInterval;
36
+ private stopTypingInterval;
37
+ sendAudio(chatId: string, filePath: string, asVoice?: boolean): Promise<void>;
38
+ stop(): Promise<void>;
39
+ }
40
+ //# sourceMappingURL=whatsapp.d.ts.map
@@ -0,0 +1 @@
1
+ import{DisconnectReason as t,fetchLatestBaileysVersion as e,makeCacheableSignalKeyStore as s,makeWASocket as n,useMultiFileAuthState as i}from"@whiskeysockets/baileys";import{mkdirSync as o,existsSync as a,readFileSync as c}from"node:fs";import{resolve as r}from"node:path";import{renderQrPngBase64 as h}from"./qr-image.js";import{convertMarkdownTables as l}from"../../utils/markdown/tables.js";import{chunkText as p}from"../../utils/chunk.js";import{createLogger as g}from"../../utils/logger.js";const d=g("WhatsApp");export class WhatsAppChannel{name="whatsapp";sock=null;config;qrCallback=null;connected=!1;stopping=!1;typingIntervals=new Map;inflightTyping;inflightCount=new Map;constructor(t,e=!0){this.config=t,this.inflightTyping=e}setQrCallback(t){this.qrCallback=t}isConnected(){return this.connected}async start(t){const e=r(this.config.authDir);a(e)||o(e,{recursive:!0}),await this.connect(e,t)}async connect(o,a){const{state:c,saveCreds:r}=await i(o),{version:l}=await e(),p={level:"silent",trace:()=>{},debug:()=>{},info:()=>{},warn:()=>{},error:()=>{},fatal:()=>{},child:()=>p};this.sock=n({auth:{creds:c.creds,keys:s(c.keys,p)},version:l,logger:p,printQRInTerminal:!1,browser:["GrabMeABeer","Web","1.0"],syncFullHistory:!1,markOnlineOnConnect:!1}),this.sock.ev.on("creds.update",r),this.sock.ev.on("connection.update",async e=>{try{const{connection:s,lastDisconnect:n,qr:i}=e;if(i){d.info("QR code received, rendering...");const t=`data:image/png;base64,${await h(i)}`;this.qrCallback?.(t,!1)}if("open"===s&&(this.connected=!0,d.info("WhatsApp connected"),this.qrCallback?.(null,!0)),"close"===s){this.connected=!1;const e=n?.error?.output?.statusCode??n?.error?.status;e===t.loggedOut?(d.warn("WhatsApp session logged out. Re-scan QR via Nostromo."),this.qrCallback?.(null,!1,"Session logged out. Please re-scan QR code.")):this.stopping||(d.info(`WhatsApp disconnected (code ${e}), reconnecting...`),setTimeout(()=>this.connect(o,a),3e3))}}catch(t){d.error(`connection.update handler error: ${t}`)}}),this.sock.ws&&"function"==typeof this.sock.ws.on&&this.sock.ws.on("error",t=>{d.error(`WebSocket error: ${t.message}`)}),this.sock.ev.on("messages.upsert",({messages:t,type:e})=>{if("notify"===e)for(const e of t){if(!e.message||e.key.fromMe)continue;const t=e.key.remoteJid;if(!t)continue;const s=t.replace(/@s\.whatsapp\.net$/,""),n=e.message.conversation??e.message.extendedTextMessage?.text??void 0;if(!this.checkAccess(s)){d.warn(`Unauthorized message from ${s}`),this.sock?.sendMessage(t,{text:"Not authorized."});continue}if(!n)continue;const i={chatId:t,userId:s,channelName:"whatsapp",text:n,attachments:[],username:e.pushName??void 0};this.handleIncoming(i,t,s,a)}})}async handleIncoming(t,e,s,n){this.startTypingInterval(e);try{const s=await n(t),i=l(s,"code"),o=p(i,4e3);for(const t of o)await(this.sock?.sendMessage(e,{text:t}));await this.resendTypingIfActive(e)}catch(t){d.error(`Error handling message from ${s}: ${t}`)}}checkAccess(t){const e=this.config.dmPolicy||"allowlist";if("open"===e)return!0;if("allowlist"===e){return(this.config.allowFrom??[]).some(e=>String(e)===t)}return!0}async sendText(t,e){if(!this.sock)return;const s=l(e,"code"),n=p(s,4e3);for(const e of n)await this.sock.sendMessage(t,{text:e});await this.resendTypingIfActive(t)}async setTyping(t){this.sock&&(this.typingIntervals.has(t)?await this.sock.sendPresenceUpdate("composing",t).catch(()=>{}):this.startTypingInterval(t))}async resendTypingIfActive(t){this.typingIntervals.has(t)&&await(this.sock?.sendPresenceUpdate("composing",t).catch(()=>{}))}async clearTyping(t){this.inflightCount.delete(t);const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.sock?.sendPresenceUpdate("paused",t).catch(()=>{})}async releaseTyping(t){this.stopTypingInterval(t)}startTypingInterval(t){if(this.inflightTyping){const e=(this.inflightCount.get(t)??0)+1;if(this.inflightCount.set(t,e),e>1)return}const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.sock?.sendPresenceUpdate("composing",t).catch(()=>{});const s=setInterval(()=>{const e=this.inflightCount.get(t)??0;!this.inflightTyping||e>0?this.sock?.sendPresenceUpdate("composing",t).catch(()=>{}):(clearInterval(s),this.typingIntervals.delete(t),this.sock?.sendPresenceUpdate("paused",t).catch(()=>{}))},4e3);this.typingIntervals.set(t,s)}stopTypingInterval(t){if(this.inflightTyping){const e=(this.inflightCount.get(t)??1)-1;return void(e>0?this.inflightCount.set(t,e):this.inflightCount.delete(t))}const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.sock?.sendPresenceUpdate("paused",t).catch(()=>{})}async sendAudio(t,e,s){if(!this.sock)return;const n=c(e);s?await this.sock.sendMessage(t,{audio:n,mimetype:"audio/ogg; codecs=opus",ptt:!0}):await this.sock.sendMessage(t,{audio:n,mimetype:"audio/mpeg"}),await this.resendTypingIfActive(t)}async stop(){this.stopping=!0;try{this.sock?.ws?.close()}catch{}this.sock=null,this.connected=!1,d.info("WhatsApp stopped")}}
@@ -0,0 +1,38 @@
1
+ import type { WebSocket } from "ws";
2
+ export interface NodeSession {
3
+ nodeId: string;
4
+ displayName?: string;
5
+ platform?: string;
6
+ arch?: string;
7
+ hostname?: string;
8
+ capabilities: string[];
9
+ commands: string[];
10
+ connectedAt: number;
11
+ ws: WebSocket;
12
+ }
13
+ export interface CommandResult {
14
+ ok: boolean;
15
+ result?: unknown;
16
+ error?: string;
17
+ }
18
+ export declare class NodeRegistry {
19
+ private nodes;
20
+ private pendingCommands;
21
+ register(nodeId: string, ws: WebSocket, info: Omit<NodeSession, "nodeId" | "connectedAt" | "ws">): void;
22
+ unregister(nodeId: string): void;
23
+ getNode(nodeId: string): NodeSession | undefined;
24
+ listNodes(): Array<Omit<NodeSession, "ws">>;
25
+ getCount(): number;
26
+ /**
27
+ * Send a command to a node via WebSocket and wait for the result.
28
+ * Returns a Promise that resolves when the node sends back a command_result
29
+ * with the matching request ID.
30
+ */
31
+ executeCommand(nodeId: string, command: string, params: unknown, timeout?: number): Promise<CommandResult>;
32
+ /**
33
+ * Called by the WS message handler (Nostromo) when a command_result
34
+ * message arrives from a node.
35
+ */
36
+ handleCommandResult(id: string, result: CommandResult): void;
37
+ }
38
+ //# sourceMappingURL=node-registry.d.ts.map
@@ -0,0 +1 @@
1
+ import{randomUUID as e}from"node:crypto";import{createLogger as o}from"../utils/logger.js";const t=o("NodeRegistry");export class NodeRegistry{nodes=new Map;pendingCommands=new Map;register(e,o,s){this.nodes.set(e,{nodeId:e,...s,connectedAt:Date.now(),ws:o}),t.info(`Node registered: ${e} (${s.displayName??"unnamed"})`)}unregister(e){this.nodes.delete(e)&&t.info(`Node unregistered: ${e}`)}getNode(e){return this.nodes.get(e)}listNodes(){return Array.from(this.nodes.values()).map(({ws:e,...o})=>o)}getCount(){return this.nodes.size}executeCommand(o,t,s,n){const r=this.nodes.get(o);if(!r)return Promise.resolve({ok:!1,error:`Node "${o}" not found`});if(r.ws.readyState!==r.ws.OPEN)return Promise.resolve({ok:!1,error:`Node "${o}" is not connected`});const d=e(),i=n??3e4;return new Promise(e=>{const o=setTimeout(()=>{this.pendingCommands.delete(d),e({ok:!1,error:`Command timed out after ${i}ms`})},i);this.pendingCommands.set(d,{resolve:e,timer:o}),r.ws.send(JSON.stringify({type:"command",id:d,command:t,params:s}))})}handleCommandResult(e,o){const t=this.pendingCommands.get(e);t&&(clearTimeout(t.timer),this.pendingCommands.delete(e),t.resolve(o))}}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import{loadConfig as o}from"../config.js";import{runSecurityAudit as n}from"./security/audit.js";import{formatTextReport as e,formatJsonReport as s}from"./security/report.js";import{runLogs as t}from"./logs.js";function i(){console.log("\nUsage: hera <command> [options]\n\nCommands:\n security audit [--fix] [--json] Run security audit\n logs [options] View log output\n\nLogs options:\n -n, --lines <N> Number of lines to show (default: 50)\n -f, --follow Follow log output in real time\n --level <level> Filter by minimum level (debug, info, warn, error)\n --tag <tag> Filter by component tag\n --since <duration> Show logs since duration (e.g. 1h, 30m, 2d) or ISO date\n --json Output as JSON\n --no-color Disable colors\n\nGeneral:\n --help Show this help message\n")}!function(){const l=process.argv.slice(2);if((0===l.length||1===l.length&&"--help"===l[0])&&(i(),process.exit(0)),"security"===l[0]&&"audit"===l[1]){const t=l.includes("--fix"),i=l.includes("--json"),r=o(),c=n(r,{fix:t});return i?console.log(s(c)):console.log(e(c)),void(!t&&c.summary.critical>0&&process.exit(1))}if("logs"===l[0]){o();const n=function(o){const n={};for(let e=0;e<o.length;e++){const s=o[e];"-f"===s||"--follow"===s?n.follow=!0:"-n"!==s&&"--lines"!==s||!o[e+1]?"--level"===s&&o[e+1]?n.level=o[++e]:"--tag"===s&&o[e+1]?n.tag=o[++e]:"--since"===s&&o[e+1]?n.since=o[++e]:"--json"===s?n.json=!0:"--no-color"===s&&(n.noColor=!0):n.lines=parseInt(o[++e])}return n}(l.slice(1));return void t(n)}console.error(`Unknown command: ${l.join(" ")}`),i(),process.exit(1)}();
@@ -0,0 +1,13 @@
1
+ type LogLevel = "debug" | "info" | "warn" | "error";
2
+ export interface LogsOptions {
3
+ follow?: boolean;
4
+ lines?: number;
5
+ level?: LogLevel;
6
+ since?: string;
7
+ tag?: string;
8
+ json?: boolean;
9
+ noColor?: boolean;
10
+ }
11
+ export declare function runLogs(opts: LogsOptions): void;
12
+ export {};
13
+ //# sourceMappingURL=logs.d.ts.map
@@ -0,0 +1 @@
1
+ import{existsSync as e,readFileSync as o,statSync as n,readdirSync as t,openSync as s,readSync as r,closeSync as l}from"node:fs";import{join as c}from"node:path";import{getDataDir as i}from"../config.js";const a={debug:0,info:1,warn:2,error:3},f={debug:"",info:"",warn:"",error:""},u="",g="";function m(e){const o=e.match(/^(\d{4}-\d{2}-\d{2}T[\d:.]+Z)\s+\[(\w+)\s*\]\s+\[([^\]]+)\]\s+(.*)/);if(!o)return null;const n=o[2].trim().toLowerCase();return a[n]||"debug"===n?{raw:e,ts:o[1],level:void 0!==a[n]?n:"info",tag:o[3],message:o[4]}:null}function d(e,o,n){if(o.level&&a[e.level]<a[o.level])return!1;if(o.tag&&!e.tag.toLowerCase().includes(o.tag.toLowerCase()))return!1;if(n){if(new Date(e.ts)<n)return!1}return!0}function p(e,o){if(o)return e.raw;const n=f[e.level]??"";return`${g}${e.ts}${u} ${n}[${e.level.toUpperCase().padEnd(5)}]${u} ${g}[${e.tag}]${u} ${e.message}`}function h(e,o){const t=n(e);if(0===t.size)return[];const c=s(e,"r"),i=[];let a="",f=t.size;try{for(;f>0&&i.length<o;){const e=Math.min(8192,f);f-=e;const n=Buffer.alloc(e);r(c,n,0,e,f);const t=(n.toString("utf-8")+a).split("\n");a=t[0];for(let e=t.length-1;e>=1&&(t[e]&&i.unshift(t[e]),!(i.length>=o));e--);}a&&i.length<o&&i.unshift(a)}finally{l(c)}return i.slice(-o)}export function runLogs(a){const f=c(i(),"logs"),u=c(f,"gmab.log");e(f)&&e(u)||(console.error(`No log files found at ${f}`),console.error("Hera may not have been started yet."),process.exit(1));const g=a.since?function(e){const o=e.match(/^(\d+)\s*(m|min|h|hr|d|s)$/i);if(!o){const o=new Date(e);if(!isNaN(o.getTime()))return o;throw new Error(`Invalid --since value: "${e}". Use e.g. 1h, 30m, 2d, or an ISO date.`)}const n=parseInt(o[1]),t=o[2].toLowerCase(),s=Date.now();switch(t){case"s":return new Date(s-1e3*n);case"m":case"min":return new Date(s-60*n*1e3);case"h":case"hr":default:return new Date(s-3600*n*1e3);case"d":return new Date(s-86400*n*1e3)}}(a.since):null,v=a.lines??50;if(a.follow)return void function(e,o,t){const c=h(e,o.lines??20);for(const e of c){const n=m(e);n&&d(n,o,t)&&console.log(p(n,o.noColor??!1))}let i=n(e).size,a="";const f=()=>{try{const t=n(e).size;if(t<i&&(i=0),t>i){const n=s(e,"r"),c=t-i,f=Buffer.alloc(c);r(n,f,0,c,i),l(n),i=t,a+=f.toString("utf-8");const u=a.split("\n");a=u.pop()??"";for(const e of u){if(!e.trim())continue;const n=m(e);n?d(n,o,null)&&(o.json?console.log(JSON.stringify({ts:n.ts,level:n.level,tag:n.tag,message:n.message})):console.log(p(n,o.noColor??!1))):console.log(e)}}}catch{}},u=setInterval(f,250);process.on("SIGINT",()=>{clearInterval(u),process.exit(0)}),process.on("SIGTERM",()=>{clearInterval(u),process.exit(0)})}(u,a,g);const w=function(){const o=c(i(),"logs");if(!e(o))return[];const n=[],s=t(o),r=[];for(const e of s){if("gmab.log"===e)continue;const o=e.match(/^gmab\.(\d+)\.log$/);o&&r.push({name:e,index:parseInt(o[1])})}r.sort((e,o)=>o.index-e.index);for(const e of r)n.push(c(o,e.name));const l=c(o,"gmab.log");return e(l)&&n.push(l),n}();let $=[];if(g||a.level||a.tag){for(const e of w){const n=o(e,"utf-8");for(const e of n.split("\n")){if(!e.trim())continue;const o=m(e);o&&(d(o,a,g)&&$.push(e))}}$=$.slice(-v)}else $=h(u,v);if(a.json){const e=$.map(m).filter(e=>null!==e).map(e=>({ts:e.ts,level:e.level,tag:e.tag,message:e.message}));return void console.log(JSON.stringify(e,null,2))}for(const e of $){const o=m(e);o?console.log(p(o,a.noColor??!1)):console.log(e)}}
@@ -0,0 +1,17 @@
1
+ import type { AppConfig } from "../../config.js";
2
+ export type Severity = "critical" | "warn" | "info";
3
+ export interface Finding {
4
+ id: string;
5
+ severity: Severity;
6
+ title: string;
7
+ detail: string;
8
+ fix?: string;
9
+ }
10
+ export interface AuditReport {
11
+ findings: Finding[];
12
+ summary: Record<Severity, number>;
13
+ }
14
+ export declare function runSecurityAudit(config: AppConfig, opts?: {
15
+ fix?: boolean;
16
+ }): AuditReport;
17
+ //# sourceMappingURL=audit.d.ts.map
@@ -0,0 +1 @@
1
+ import{checkFsPermissions as r}from"./checks/fs-permissions.js";import{checkChannelPolicies as s}from"./checks/channel-policies.js";import{checkCredentials as i}from"./checks/credentials.js";import{checkNetwork as o}from"./checks/network.js";export function runSecurityAudit(c,e={}){const n=[...r(c,e),...s(c,e),...i(c,e),...o(c)],t={critical:0,warn:0,info:0};for(const r of n)t[r.severity]++;return{findings:n,summary:t}}
@@ -0,0 +1,6 @@
1
+ import type { AppConfig } from "../../../config.js";
2
+ import type { Finding } from "../audit.js";
3
+ export declare function checkChannelPolicies(config: AppConfig, opts?: {
4
+ fix?: boolean;
5
+ }): Finding[];
6
+ //# sourceMappingURL=channel-policies.d.ts.map
@@ -0,0 +1 @@
1
+ import{readFileSync as l,writeFileSync as o,existsSync as e}from"node:fs";import{resolve as t}from"node:path";import{parse as a,stringify as i}from"yaml";import{backupConfig as n}from"../../../config.js";export function checkChannelPolicies(s,r={}){const c=[],m=t(process.cwd(),"config.yaml"),d=[],h=s.channels.telegram;if(h.enabled)for(const[l,o]of Object.entries(h.accounts)){const e=o;if("open"===e.dmPolicy&&(r.fix?(d.push(l),c.push({id:`channel.telegram.${l}.dm_open`,severity:"info",title:`telegram/${l}: dmPolicy changed to "allowlist"`,detail:'Auto-fixed dmPolicy from "open" to "allowlist".'})):c.push({id:`channel.telegram.${l}.dm_open`,severity:"critical",title:`telegram/${l}: dmPolicy is "open"`,detail:'Anyone can message this bot. Set dmPolicy to "allowlist" or "token".',fix:`Set channels.telegram.accounts.${l}.dmPolicy to "allowlist" in config.yaml`})),Array.isArray(e.allowFrom)){e.allowFrom.some(l=>"*"===String(l))&&c.push({id:`channel.telegram.${l}.allowfrom_wildcard`,severity:"critical",title:`telegram/${l}: allowFrom contains "*" wildcard`,detail:'The wildcard bypasses the allowlist entirely. Remove "*" from allowFrom.',fix:`Remove "*" from channels.telegram.accounts.${l}.allowFrom`})}"allowlist"===e.dmPolicy&&Array.isArray(e.allowFrom)&&0===e.allowFrom.length&&c.push({id:`channel.telegram.${l}.allowfrom_empty`,severity:"warn",title:`telegram/${l}: allowlist policy but allowFrom is empty`,detail:"No users can reach the bot. Add user IDs to allowFrom or change dmPolicy."})}const w=["whatsapp","discord","slack","signal","msteams","googlechat","line","matrix"];for(const l of w){const o=s.channels[l];if(o.enabled){"whatsapp"===l&&c.push({id:"channel.whatsapp.info",severity:"info",title:"WhatsApp channel is enabled",detail:"WhatsApp only supports allowlist-based access (no token auth). Verify allowFrom in your config."});for(const[e,t]of Object.entries(o.accounts)){const o=t;if("open"===o.dmPolicy&&c.push({id:`channel.${l}.${e}.dm_open`,severity:"critical",title:`${l}/${e}: dmPolicy is "open"`,detail:'Anyone can message via this channel. Set dmPolicy to "allowlist" or "token".',fix:`Set channels.${l}.accounts.${e}.dmPolicy to "allowlist" in config.yaml`}),Array.isArray(o.allowFrom)){o.allowFrom.some(l=>"*"===String(l))&&c.push({id:`channel.${l}.${e}.allowfrom_wildcard`,severity:"critical",title:`${l}/${e}: allowFrom contains "*" wildcard`,detail:'The wildcard bypasses the allowlist. Remove "*" from allowFrom.',fix:`Remove "*" from channels.${l}.accounts.${e}.allowFrom`}),"allowlist"===o.dmPolicy&&0===o.allowFrom.length&&c.push({id:`channel.${l}.${e}.allowfrom_empty`,severity:"warn",title:`${l}/${e}: allowlist policy but allowFrom is empty`,detail:"No users can reach the bot on this channel. Add user IDs to allowFrom."})}}}}if(r.fix&&d.length>0&&e(m))try{const e=l(m,"utf-8"),t=a(e)??{};for(const l of d)t.channels?.telegram?.accounts?.[l]&&(t.channels.telegram.accounts[l].dmPolicy="allowlist");n(m),o(m,i(t),"utf-8")}catch{}return c}
@@ -0,0 +1,6 @@
1
+ import type { AppConfig } from "../../../config.js";
2
+ import type { Finding } from "../audit.js";
3
+ export declare function checkCredentials(config: AppConfig, opts?: {
4
+ fix?: boolean;
5
+ }): Finding[];
6
+ //# sourceMappingURL=credentials.d.ts.map
@@ -0,0 +1 @@
1
+ import{readFileSync as e,existsSync as t}from"node:fs";import{resolve as n}from"node:path";import{isDefaultKey as i}from"../../../nostromo/auth.js";import{TokenDB as o}from"../../../auth/token-db.js";export function checkCredentials(s,r={}){const a=[],c=n(process.cwd(),"config.yaml"),l=n(process.cwd(),".env");if(t(c)){const t=function(e){const t=[],n=e.split("\n");for(const e of n){const n=e.trim();if(n.startsWith("#"))continue;const i=n.match(/^(apiKey|botToken|token|accessToken|appSecret|channelSecret)\s*:\s*(.+)/);if(!i)continue;const o=i[2].trim().replace(/^["']|["']$/g,"");o&&!o.startsWith("${")&&'""'!==o&&"''"!==o&&t.push(`${i[1]}: ${o.slice(0,8)}...`)}return t}(e(c,"utf-8"));t.length>0&&a.push({id:"cred.inline_apikey",severity:"critical",title:"Inline API keys found in config.yaml",detail:`Found ${t.length} key(s) written directly in config: ${t.join(", ")}. Use \${ENV_VAR} references instead.`,fix:"Move secrets to .env and use ${VAR_NAME} syntax in config.yaml"})}try{i()&&a.push({id:"cred.nostromo_default_key",severity:"critical",title:'Nostromo key is the default "0000"',detail:"Anyone who knows the default key can access the Nostromo admin panel. Log in to auto-rotate, or manually rotate from Settings.",fix:"Log into Nostromo to trigger automatic key rotation"})}catch{}if(t(c)){const n=e(c,"utf-8");/\$\{[^}]+\}/.test(n)&&!t(l)&&a.push({id:"cred.env_missing",severity:"warn",title:".env file missing but config references environment variables",detail:`config.yaml uses \${...} variable syntax but no .env file was found at ${l}. Variables may resolve to empty strings unless exported in the shell.`,fix:"Create a .env file with the referenced variables"})}if(s.models)for(const e of s.models)!e.apiKey||e.apiKey.startsWith("${")||e.useEnvVar||a.push({id:"cred.model_apikey_direct",severity:"warn",title:`Model "${e.name}" has apiKey set directly`,detail:`The model "${e.name}" (${e.id}) has an API key set directly. Use useEnvVar to reference an environment variable instead.`,fix:'Set useEnvVar: "YOUR_ENV_VAR" on the model entry and move the key to .env'});if(t(l)&&t(c)){const t=e(l,"utf-8"),n=t.indexOf("# HERA/NOSTROMO");if(-1!==n){const i=t.slice(n),o=e(c,"utf-8"),s=[];for(const e of i.split("\n")){const t=e.trim();if(!t||t.startsWith("#"))continue;const n=t.indexOf("=");n>0&&s.push(t.slice(0,n).trim())}const r=s.filter(e=>!o.includes(`\${${e}}`));r.length>0&&a.push({id:"cred.env_unused_keys",severity:"warn",title:"Unused keys found in .env (HERA/NOSTROMO section)",detail:`The following key(s) in .env are not referenced in config.yaml: ${r.join(", ")}. They may be stale or no longer needed.`,fix:"Remove unused keys from .env or add corresponding ${VAR} references in config.yaml"})}}if(s.channels.responses?.enabled)try{const e=new o(s.dbPath),t=e.listTokens().filter(e=>e.active);e.close(),0===t.length&&a.push({id:"cred.no_tokens",severity:"info",title:"No active tokens in the token database",detail:"The Responses API is enabled but no active Bearer tokens exist. Create tokens via Nostromo to allow API access."})}catch{}return a}
@@ -0,0 +1,6 @@
1
+ import type { AppConfig } from "../../../config.js";
2
+ import type { Finding } from "../audit.js";
3
+ export declare function checkFsPermissions(config: AppConfig, opts?: {
4
+ fix?: boolean;
5
+ }): Finding[];
6
+ //# sourceMappingURL=fs-permissions.d.ts.map
@@ -0,0 +1 @@
1
+ import{existsSync as e,statSync as t,chmodSync as i}from"node:fs";import{resolve as o}from"node:path";import{getNostromoKeyPath as d}from"../../../config.js";export function checkFsPermissions(s,r={}){if("win32"===process.platform)return[];const a=[{id:"fs.config.perms",label:"config.yaml",path:o(process.cwd(),"config.yaml"),expectedMode:384,expectedLabel:"600",severity:"critical"},{id:"fs.env.perms",label:".env",path:o(process.cwd(),".env"),expectedMode:384,expectedLabel:"600",severity:"critical"},{id:"fs.nostromo_key.perms",label:".nostromo-key",path:d(),expectedMode:384,expectedLabel:"600",severity:"critical"},{id:"fs.data_dir.perms",label:"data/",path:s.dataDir,expectedMode:448,expectedLabel:"700",severity:"warn"},{id:"fs.db.perms",label:"SQLite DB",path:s.dbPath,expectedMode:384,expectedLabel:"600",severity:"warn"}],p=[];for(const o of a){if(!e(o.path))continue;const d=511&t(o.path).mode;if(d!==o.expectedMode){const e=d.toString(8);r.fix?(i(o.path,o.expectedMode),p.push({id:o.id,severity:"info",title:`${o.label} permissions fixed`,detail:`Changed ${o.path} from ${e} to ${o.expectedLabel}.`})):p.push({id:o.id,severity:o.severity,title:`${o.label} has overly permissive permissions (${e})`,detail:`${o.path} should be ${o.expectedLabel}, currently ${e}.`,fix:`chmod ${o.expectedLabel} "${o.path}"`})}}return p}
@@ -0,0 +1,4 @@
1
+ import type { AppConfig } from "../../../config.js";
2
+ import type { Finding } from "../audit.js";
3
+ export declare function checkNetwork(config: AppConfig): Finding[];
4
+ //# sourceMappingURL=network.d.ts.map
@@ -0,0 +1 @@
1
+ export function checkNetwork(e){const o=[];return"0.0.0.0"===e.host&&o.push({id:"net.bind_all",severity:"critical",title:"Server bound to all interfaces (0.0.0.0)",detail:"The server is listening on all network interfaces, exposing it to the local network and potentially the internet. Bind to 127.0.0.1 unless external access is intended.",fix:'Set host: "127.0.0.1" in config.yaml'}),e.nostromo?.enabled&&e.host&&"127.0.0.1"!==e.host&&"localhost"!==e.host&&o.push({id:"net.nostromo_exposed",severity:"warn",title:"Nostromo admin panel exposed on non-localhost",detail:`Nostromo is running on ${e.host}:${e.nostromo.port}. The admin panel should only be accessible from localhost unless behind a reverse proxy with authentication.`,fix:'Set host: "127.0.0.1" in config.yaml or use a reverse proxy'}),e.channels.responses?.enabled&&o.push({id:"net.responses_enabled",severity:"info",title:"Responses API is enabled",detail:`The Responses API is active on port ${e.channels.responses.port}. All requests require a valid Bearer token.`}),o}
@@ -0,0 +1,4 @@
1
+ import type { AuditReport } from "./audit.js";
2
+ export declare function formatTextReport(report: AuditReport): string;
3
+ export declare function formatJsonReport(report: AuditReport): string;
4
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +1 @@
1
+ const i={critical:"",warn:"",info:""},n="",t="",r={critical:"!!",warn:" !",info:" i"};function o(o){const s=[` ${i[o.severity]}[${r[o.severity]}]${n} ${t}${o.title}${n}`,` ${o.detail}${n}`];return o.fix&&s.push(` Fix: ${o.fix}`),s.join("\n")}export function formatTextReport(r){const s=[];s.push(""),s.push(`${t}Hera Security Audit${n}`),s.push("=".repeat(40));const e=[["critical","Critical"],["warn","Warnings"],["info","Info"]];for(const[u,$]of e){const e=r.findings.filter(i=>i.severity===u);if(0===e.length)continue;const c=i[u];s.push(""),s.push(`${c}${t}${$} (${e.length})${n}`),s.push("-".repeat(30));for(const i of e)s.push(o(i))}s.push(""),s.push("-".repeat(40));const{critical:u,warn:$,info:c}=r.summary,a=[];return u>0&&a.push(`${i.critical}${u} critical${n}`),$>0&&a.push(`${i.warn}${$} warnings${n}`),c>0&&a.push(`${i.info}${c} info${n}`),0===a.length?s.push(`${t}No findings. Looking good.${n}`):s.push(`Summary: ${a.join(", ")}`),u>0&&s.push(`\nRun ${t}hera security audit --fix${n} to auto-fix where possible.`),s.push(""),s.join("\n")}export function formatJsonReport(i){return JSON.stringify(i,null,2)}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import{join as o}from"node:path";import{loadConfig as r}from"./config.js";import{Server as s}from"./server.js";import{Nostromo as t}from"./nostromo/nostromo.js";import{ConfigWatcher as e}from"./config-watcher.js";import{createLogger as n,setLogLevel as a,initLogFile as c}from"./utils/logger.js";const i=n("Main");let p,g,m;for(let o=2;o<process.argv.length;o++)"--port"===process.argv[o]&&process.argv[o+1]?(g=parseInt(process.argv[o+1]),o++):"--host"===process.argv[o]&&process.argv[o+1]?(m=process.argv[o+1],o++):process.argv[o].startsWith("--")||(p=process.argv[o]);(async function(){i.info("Loading configuration...");const n=r(p);a(n.logLevel),c(o(n.dataDir,"logs")),i.info("Configuration loaded");const f=new s(n);let l=null;if(n.nostromo.enabled){const o=m??n.host,r=g??n.nostromo.port;l=new t(f,o,r,f.getNodeRegistry(),n.nostromo.basePath),await l.start()}const v=new e(f,n.nostromo.configCheckInterval,p);v.start();const h=async()=>{i.info("Received shutdown signal"),v.stop(),l&&await l.stop(),await f.stop(),process.exit(0)};process.on("SIGINT",h),process.on("SIGTERM",h),await f.start()})().catch(o=>{i.error(`Fatal error: ${o}`),console.error(o),process.exit(1)});
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export type SetupMode = "tailscale" | "basic";
3
+ //# sourceMappingURL=hera.d.ts.map
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import{execSync as e}from"node:child_process";import{homedir as t}from"node:os";import{resolve as o,join as s}from"node:path";import{existsSync as n,mkdirSync as a,renameSync as r,readdirSync as l,copyFileSync as i,readFileSync as c,writeFileSync as $}from"node:fs";import{createServer as u}from"node:net";import{parse as p,stringify as d}from"yaml";import*as m from"@clack/prompts";import{resolveInstallPkgDir as g,resolvePackageAsset as f}from"../utils/package-paths.js";const h="",w="",v="",b=e=>`[38;5;${e}m`,S=e=>`[48;5;${e}m`,y=218,P=205,T=199,C=200,x=197,N=196,k=161,I=204;function A(e,t){const o=e*e+t*t-1;return o*o*o-e*e*t*t*t<=0}function H(e,t){const o=Math.abs(e),s=e<0;return t>.8?o>.65?y:t>1.6*o+.4?P:s?T:I:t>.3?o>.9?P:t>1.1*o+.05?s?T:C:x:t>-.1?t>.6*o?x:N:t>-.55&&t>.9*o-.55?N:k}function W(){const e=process.cwd();console.log("");for(const e of function(){const e=[];for(let t=0;t<28;t++){const o=[];for(let e=0;e<52;e++){const s=e/51*2.6-1.3,n=1.3-t/27*2.3;o.push(A(s,n)?H(s,n):0)}e.push(o)}const t=[];for(let o=0;o<28;o+=2){let s="",n=!1;for(let t=0;t<52;t++){const a=e[o]?.[t]??0,r=e[o+1]?.[t]??0;a&&r&&a!==r?(s+=b(a)+S(r)+"▀",n=!0):(n&&(s+=h,n=!1),s+=a&&r?b(a)+"█":a?b(a)+"▀":r?b(r)+"▄":" ")}t.push(" "+s+h)}for(;t.length>0&&""===t[t.length-1].replace(/\x1b\[[0-9;]*m/g,"").trim();)t.pop();return t}())console.log(e);console.log("");for(const e of[`${b(T)}${w} ██╗ ██╗ ███████╗ ██████╗ █████╗ ${h}`,`${b(T)}${w} ██║ ██║ ██╔════╝ ██╔══██╗ ██╔══██╗${h}`,`${b(C)}${w} ███████║ █████╗ ██████╔╝ ███████║${h}`,`${b(x)}${w} ██╔══██║ ██╔══╝ ██╔══██╗ ██╔══██║${h}`,`${b(N)}${w} ██║ ██║ ███████╗ ██║ ██║ ██║ ██║${h}`,`${b(k)}${w} ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝${h}`])console.log(" "+e);console.log("");let t="0.9.0",o="";try{const e=JSON.parse(c(f("package.json"),"utf-8"));t=e.version??t,o=e.codename?` [${e.codename}]`:""}catch{}console.log(` ${b(P)}${w}Welcome to Hera${h} ${v}v${t}${o} — Setup Wizard${h}`),console.log(` ${v}${"─".repeat(46)}${h}`),console.log(` ${v}📁 ${e}${h}`),console.log("")}function R(t,o){try{return e(`"${t}" serve --bg --https ${o} http://127.0.0.1:${o}`,{stdio:"inherit"}),!0}catch{return!1}}function D(e){return new Promise(t=>{const o=u();o.once("error",()=>t(!1)),o.listen(e,"127.0.0.1",()=>o.close(()=>t(!0)))})}const U=g();async function M(e){const t=s(e,"data");if(n(t)){const o=`data_backup_${function(){const e=new Date;return[e.getFullYear(),String(e.getMonth()+1).padStart(2,"0"),String(e.getDate()).padStart(2,"0"),"_",String(e.getHours()).padStart(2,"0"),String(e.getMinutes()).padStart(2,"0"),String(e.getSeconds()).padStart(2,"0")].join("")}()}`,n=s(e,o);m.log.warn(`Existing ${w}data/${h} folder found in ${w}${e}${h}\n It will be backed up to ${w}${o}${h}`);const a=await m.confirm({message:"Continue and backup existing data?"});!m.isCancel(a)&&a||(m.cancel("Setup cancelled. Your data folder was not modified."),process.exit(0)),r(t,n),m.log.success(`Backup saved to ${w}${n}${h}`)}a(t,{recursive:!0});const o=l(U).filter(e=>e.endsWith(".md")&&!e.startsWith(".")&&!e.startsWith("SYSTEM_PROMPT"));for(const e of o)i(s(U,e),s(t,e));const c=s(t,".templates");a(c,{recursive:!0});const $=l(U).filter(e=>e.startsWith("SYSTEM_PROMPT")||e.endsWith(".json"));for(const e of $)i(s(U,e),s(c,e));m.log.success(`Data provisioned: ${w}${t}${h}\n ${v}${o.length} prompt files + .templates/ (${$.length} files)${h}`)}(async function(){W(),m.intro(`${b(P)}Checking your environment...${h}`),n(U)||(m.log.error(`${w}Missing required files.${h}\n\n The ${w}./installationPkg${h} folder was not found.\n Please re-download the full Hera package and try again.`),m.outro("Setup aborted."),process.exit(1));let r="";try{r=e("claude --version",{encoding:"utf-8"}).trim()}catch{}if(r)m.log.success(`Claude Code ${v}${r}${h}`);else{m.log.error(`${w}Claude Code does not seem to be installed.${h}\n\n Hera requires Claude Code to be installed, configured\n and working before you can continue.\n\n ${b(P)}Install: ${w}https://docs.anthropic.com/en/docs/claude-code${h}`);const e=await m.confirm({message:"Continue anyway?"});!m.isCancel(e)&&e||(m.cancel("Setup cancelled. Install Claude Code and try again."),process.exit(0))}let l="basic",u=null;const g=function(){const t="/Applications/Tailscale.app/Contents/MacOS/Tailscale";try{return e(`"${t}" version`,{stdio:"ignore"}),t}catch{}try{const t=e("which tailscale",{encoding:"utf-8"}).trim();if(t)return t}catch{}return null}();if(g)if(u=function(t){try{const o=e(`"${t}" status --json`,{encoding:"utf-8"}),s=JSON.parse(o),n=s.Self??{};return{hostname:n.HostName??"unknown",dnsName:(n.DNSName??"").replace(/\.$/,""),ip:(n.TailscaleIPs??[])[0]??"",tailnet:s.MagicDNSSuffix??""}}catch{return null}}(g),u){m.log.success("Tailscale detected!"),m.note([` Hostname ${w}${u.hostname}${h}`,` Network ${w}${u.dnsName}${h}`,` IP ${w}${u.ip}${h}`,` Tailnet ${v}${u.tailnet}${h}`].join("\n"),"Your Tailscale identity");const e=await m.select({message:"Do you want to configure Tailscale Serve now?",options:[{value:"tailscale",label:"Yes, configure Tailscale now (recommended)",hint:"auto-configures Tailscale Serve for secure HTTPS access"},{value:"basic",label:"No, I'll do it later",hint:"manual setup instructions will be shown at the end"}]});m.isCancel(e)&&(m.cancel("Setup cancelled."),process.exit(0)),l=e}else{m.log.warn("Tailscale is installed but not running or not logged in.");const e=await m.confirm({message:"Continue with a basic setup?"});!m.isCancel(e)&&e||(m.cancel("Setup cancelled. Start Tailscale and try again."),process.exit(0)),m.log.info("Proceeding with basic setup.")}else{m.log.warn(`${w}Tailscale is not installed.${h}\n\n We recommend Tailscale to securely expose Hera\n to your devices without port forwarding or DNS setup.\n\n ${v}Tailscale Serve lets you access your bot from any device\n on your private network with a simple HTTPS address.${h}\n\n ${b(P)}Install: ${w}https://tailscale.com/download${h}`);const e=await m.confirm({message:"Continue with a basic setup (no Tailscale)?"});!m.isCancel(e)&&e||(m.cancel("Setup cancelled. Install Tailscale and try again."),process.exit(0)),m.log.info("Proceeding with basic setup.")}const f=process.env.GMAB_PATH,S=process.env.WORKSPACE_PATH,y=!(!f||!S),T=o(t(),"gmab"),C=o(process.cwd(),"workspace"),x=f||"/app/hera/gmab",N=S||"/app/hera/workspace",k=[{value:"basic",label:"Basic configuration",hint:`data: ${T}, workspace: ${C}`}];y&&k.push({value:"docker",label:"Docker configuration (recommended)",hint:`data: ${x}, workspace: ${N}`}),k.push({value:"custom",label:"Custom configuration",hint:"choose paths and ports manually"});const I=await m.select({message:"Which configuration profile do you want to use?",options:k});let A,H,j,O,B,E;if(m.isCancel(I)&&(m.cancel("Setup cancelled."),process.exit(0)),"docker"===I)A=x,H=N,j="0.0.0.0",O=3001,B="/nostromo",E=3002,m.note([` Data path ${w}${A}${h}`,` Workspace ${w}${H}${h}`,` Host ${w}${j}${h}`,` Nostromo port ${w}${O}${h}`,` Base path ${w}${B}${h}`,` Responses port ${w}${E}${h}`].join("\n"),"Docker configuration");else if("basic"===I){A=T,H=C,j="127.0.0.1",O=3001,B="/nostromo",E=3002;const e=await D(3001),t=await D(3002);m.note([` Data path ${w}${A}${h}`,` Workspace ${w}${H}${h}`,` Host ${w}${j}${h}`,` Nostromo port ${w}${O}${h}${e?"":` ${v}(appears in use)${h}`}`,` Base path ${w}${B}${h}`,` Responses port ${w}${E}${h}${t?"":` ${v}(appears in use)${h}`}`].join("\n"),"Basic configuration")}else{const e=y?x:T,s=await m.text({message:"Where should Hera store its data?",placeholder:e,defaultValue:e,validate(e=""){const s=o(e.replace(/^~/,t())),a=o(s,"..");if(!n(a))return`Parent folder does not exist: ${a}`}});m.isCancel(s)&&(m.cancel("Setup cancelled."),process.exit(0)),A=o(s.replace(/^~/,t())),n(A)?m.log.info(`Using existing folder: ${w}${A}${h}`):m.log.info(`Folder will be created: ${w}${A}${h}`);const a=y?N:C,r=await m.text({message:"Where should the agent workspace live?",placeholder:a,defaultValue:a,validate(e=""){const s=o(e.replace(/^~/,t())),a=o(s,"..");if(!n(a))return`Parent folder does not exist: ${a}`}});m.isCancel(r)&&(m.cancel("Setup cancelled."),process.exit(0)),H=o(r.replace(/^~/,t())),n(H)?m.log.info(`Using existing folder: ${w}${H}${h}`):m.log.info(`Folder will be created: ${w}${H}${h}`),m.log.info(`${w}Nostromo Admin UI${h} is your main entry point to Hera.\n Use it to configure channels, manage tokens, and monitor sessions.`);const l=await m.text({message:"Host for Nostromo Admin UI?",placeholder:y?"0.0.0.0":"127.0.0.1",defaultValue:y?"0.0.0.0":"127.0.0.1"});m.isCancel(l)&&(m.cancel("Setup cancelled."),process.exit(0)),j=l;const i=await D(3001),c=await m.select({message:"Port for Nostromo admin UI?",options:[{value:"3001",label:"3001"+(i?"":" (appears in use)"),hint:"default Nostromo port"},{value:"custom",label:"Custom",hint:"enter a different port"}]});if(m.isCancel(c)&&(m.cancel("Setup cancelled."),process.exit(0)),"custom"===c){const e=await m.text({message:"Enter port for Nostromo admin UI:",placeholder:"3001",validate(e=""){if(!e)return"Port is required";const t=Number(e);return!Number.isInteger(t)||t<3e3||t>65535?"Enter a valid port number (3000-65535)":void 0}});m.isCancel(e)&&(m.cancel("Setup cancelled."),process.exit(0)),O=Number(e)}else O=3001;const $=await m.text({message:"Base path for Nostromo Admin UI?",placeholder:"/nostromo",defaultValue:"/nostromo"});m.isCancel($)&&(m.cancel("Setup cancelled."),process.exit(0)),B=$.trim().replace(/\/+$/,"")||"/","/"===B||B.startsWith("/")||(B="/"+B),m.log.info(`Hera exposes an ${w}OpenAI-compatible Responses API${h}\n so chat clients can connect to your bot as if it were an OpenAI endpoint.`);const u=O+1,p=await D(u),d=await m.select({message:"Port for the Responses API?",options:[{value:"default",label:`${u}${p?"":" (appears in use)"}`,hint:"Nostromo port + 1"},{value:"custom",label:"Custom",hint:"enter a different port"}]});if(m.isCancel(d)&&(m.cancel("Setup cancelled."),process.exit(0)),"custom"===d){const e=await m.text({message:"Enter port for the Responses API:",placeholder:String(u),validate(e=""){if(!e)return"Port is required";const t=Number(e);return!Number.isInteger(t)||t<3e3||t>65535?"Enter a valid port number (3000-65535)":t===O?`Already used by Nostromo Admin UI (:${O})`:void 0}});m.isCancel(e)&&(m.cancel("Setup cancelled."),process.exit(0)),E=Number(e)}else E=u}a(A,{recursive:!0}),a(H,{recursive:!0}),await M(A);const _=m.spinner();_.start("Provisioning files..."),await async function(){const e=s(U,"config.example.yaml");n(e)&&(i(e,o("./config.yaml")),m.log.success(`Created ${w}./config.yaml${h}`));const t=s(U,".env.example");if(n(t)){const e=o("./.env");if(n(e)){const o=await m.confirm({message:".env already exists. Overwrite it?"});m.isCancel(o)||!o?m.log.info(`Kept existing ${w}./.env${h}`):(i(t,e),m.log.success(`Overwritten ${w}./.env${h}`))}else i(t,e),m.log.success(`Created ${w}./.env${h}`)}}(),function(e){const t=o("./config.yaml");if(!n(t))return;const s=c(t,"utf-8"),a=p(s);a.gmabPath=e.gmabPath,a.host=e.host,a.channels||(a.channels={}),a.channels.responses||(a.channels.responses={}),a.channels.responses.port=e.responsesPort,a.agent||(a.agent={}),a.agent.workspacePath=e.workspacePath,a.nostromo||(a.nostromo={}),a.nostromo.port=e.nostromoPort,a.nostromo.basePath=e.nostromoBasePath,$(t,d(a,{lineWidth:120}),"utf-8"),m.log.success(`Updated ${w}./config.yaml${h} with your settings`)}({gmabPath:A,host:j,workspacePath:H,nostromoPort:O,nostromoBasePath:B,responsesPort:E}),_.stop("Files provisioned.");const Y="tailscale"===l&&u?`https://${u.dnsName}:${O}${B}`:null,q=`http://${j}:${O}${B}`,F=`http://${j}:${E}/v1/responses`,V="tailscale"===l&&u?`https://${u.dnsName}:${E}/v1/responses`:null,J=`ws://${j}:${O}${B}/ws/nodes`,K="tailscale"===l&&u?`wss://${u.dnsName}:${O}${B}/ws/nodes`:null,z=Y??q;m.note(` ${w}${z}${h}`,`${b(P)} Nostromo Admin UI`);const G=[` Mode ${w}${"tailscale"===l?"Tailscale":"Basic"}${h}`,` Data path ${w}${A}${h}`,` Workspace ${w}${H}${h}`,"",` Admin UI ${w}${q}${h}`];Y&&G.push(` ${w}${Y}${h} ${v}(Tailscale)${h}`),G.push("",` Nodes WS ${w}${J}${h}`),K&&G.push(` ${w}${K}${h} ${v}(Tailscale)${h}`),G.push("",` Responses API ${w}${F}${h}`),V&&G.push(` ${w}${V}${h} ${v}(Tailscale)${h}`),m.note(G.join("\n"),"Setup summary");const L=m.spinner();L.start("Starting Hera...");try{e("npm run restart",{cwd:process.cwd(),stdio:"ignore"}),L.stop("Hera is running in background."),m.log.info(` ${v}npm run logs${h} ${v}— view logs${h}\n ${v}npm run down${h} ${v}— stop${h}\n ${v}npm run restart${h} ${v}— restart${h}\n ${v}npm run status${h} ${v}— check status${h}`)}catch{L.stop("Could not start Hera automatically."),m.log.warn(`Run ${w}npm run up${h} manually to start the service.`)}if(g&&u){const e=`tailscale serve --bg --https ${O} http://127.0.0.1:${O}`,t=`tailscale serve --bg --https ${E} http://127.0.0.1:${E}`;if("tailscale"===l){m.note([` ${w}${e}${h}`,` ${w}${t}${h}`].join("\n"),"Tailscale Serve — run manually if needed");const o=await m.confirm({message:"Do you want me to try configuring Tailscale Serve automatically?"});if(!m.isCancel(o)&&o){const e=m.spinner();e.start("Configuring Tailscale Serve...");const o=R(g,O),s=R(g,E);o&&s?(e.stop("Tailscale Serve configured."),m.log.success(`Nostromo at ${w}${Y}${h}`),m.log.success(`Responses API at ${w}https://${u.dnsName}:${E}${h}`)):o?(e.stop("Tailscale Serve partially configured."),m.log.success(`Nostromo at ${w}${Y}${h}`),m.log.warn(`Responses API failed. Run manually:\n ${w}${t}${h}`)):(e.stop("Could not configure Tailscale Serve."),m.log.warn("Run the commands shown above manually."))}else m.log.info(`Skipped automatic configuration.\n Please remember to run the commands above when ready to expose\n Nostromo (port ${w}${O}${h}) and the Responses API (port ${w}${E}${h}) through Tailscale.\n Without Tailscale Serve, these services will only be reachable on localhost.`)}else m.note([" When you're ready to expose Hera via Tailscale, run:","",` ${w}${e}${h}`,` ${w}${t}${h}`,""," This will make Hera available at:",` ${w}https://${u.dnsName}:${O}${B}${h} (Admin UI)`,` ${w}https://${u.dnsName}:${E}/v1/responses${h} (API)`].join("\n"),"Tailscale Serve — manual setup")}m.outro(`${b(P)}${w}All set!${h} Open ${w}${z}${h} to get started.`)})().catch(e=>{m.cancel(`Error: ${e.message}`),process.exit(1)});
@@ -0,0 +1,23 @@
1
+ import type { IncomingMessage } from "../gateway/bridge.js";
2
+ import type { STTProvider } from "../stt/stt-provider.js";
3
+ export type SaveFileFn = (sessionKey: string, buffer: Buffer, fileName: string) => Promise<string>;
4
+ export interface ContentBlock {
5
+ type: "text" | "image";
6
+ text?: string;
7
+ imageBase64?: string;
8
+ imageMimeType?: string;
9
+ }
10
+ export interface ProcessedMessage {
11
+ sessionKey: string;
12
+ contentBlocks: ContentBlock[];
13
+ savedFiles: string[];
14
+ }
15
+ export declare class MessageProcessor {
16
+ private stt;
17
+ private saveFn;
18
+ constructor(stt: STTProvider | null, saveFn: SaveFileFn | null);
19
+ process(msg: IncomingMessage): Promise<ProcessedMessage>;
20
+ private saveFile;
21
+ private processAttachment;
22
+ }
23
+ //# sourceMappingURL=message-processor.d.ts.map
@@ -0,0 +1 @@
1
+ import{createLogger as e}from"../utils/logger.js";const t=e("MessageProcessor");export class MessageProcessor{stt;saveFn;constructor(e,t){this.stt=e,this.saveFn=t}async process(e){const a=`${e.channelName}:${e.chatId}`,s=[],i=[];e.text&&s.push({type:"text",text:e.text});for(const o of e.attachments)try{await this.processAttachment(o,a,s,i)}catch(e){t.error(`Error processing attachment type=${o.type}: ${e}`),s.push({type:"text",text:`[Failed to process ${o.type} attachment: ${e}]`})}return 0===s.length&&s.push({type:"text",text:"[Empty message]"}),{sessionKey:a,contentBlocks:s,savedFiles:i}}async saveFile(e,t,a){return this.saveFn?this.saveFn(e,t,a):null}async processAttachment(e,t,a,s){switch(e.type){case"image":{const i=await e.getBuffer(),o=await this.saveFile(t,i,e.fileName??"image.jpg");o&&s.push(o);const c=i.toString("base64"),n=e.mimeType??"image/jpeg";a.push({type:"image",imageBase64:c,imageMimeType:n}),e.caption&&a.push({type:"text",text:e.caption});break}case"voice":case"video_note":{const i=await e.getBuffer();if(this.stt){const t=await this.stt.transcribe(i,e.mimeType??"audio/ogg");a.push({type:"text",text:`[Voice message]: ${t}`})}else{const o=await this.saveFile(t,i,e.fileName??"voice.ogg");o?(s.push(o),a.push({type:"text",text:`[Voice message saved to: ${o}] (STT not configured)`})):a.push({type:"text",text:"[Voice message received] (STT not configured, storage not available)"})}break}case"audio":{const i=await e.getBuffer(),o=await this.saveFile(t,i,e.fileName??"audio.mp3");o&&s.push(o);const c=o?`[Audio file saved to: ${o}]`:"[Audio file received]";if(this.stt)try{const t=await this.stt.transcribe(i,e.mimeType??"audio/mpeg");a.push({type:"text",text:`${c}\n[Transcription]: ${t}`})}catch{a.push({type:"text",text:c})}else a.push({type:"text",text:c});break}case"document":{const i=await e.getBuffer(),o=await this.saveFile(t,i,e.fileName??"document");o&&s.push(o),a.push({type:"text",text:`[Document${o?` saved to: ${o}`:" received"}]${e.caption?`\nCaption: ${e.caption}`:""}`});break}case"video":{const i=await e.getBuffer(),o=await this.saveFile(t,i,e.fileName??"video.mp4");o&&s.push(o),a.push({type:"text",text:`[Video${o?` saved to: ${o}`:" received"}]${e.caption?`\nCaption: ${e.caption}`:""}`});break}case"sticker":{const i=await e.getBuffer(),o=await this.saveFile(t,i,e.fileName??"sticker.webp");o&&s.push(o),a.push({type:"text",text:`[Sticker${o?` saved to: ${o}`:" received"}]`});break}case"location":{const t=e.metadata??{};a.push({type:"text",text:`[Location: lat=${t.latitude}, lon=${t.longitude}]`});break}case"contact":{const t=e.metadata??{},s=[t.firstName&&`Name: ${t.firstName}`,t.lastName&&` ${t.lastName}`,t.phoneNumber&&`Phone: ${t.phoneNumber}`].filter(Boolean);a.push({type:"text",text:`[Contact: ${s.join(", ")}]`});break}}}}
@@ -0,0 +1,21 @@
1
+ import type { MemoryProvider, ConversationMessage, MemorySearchResult } from "./memory-provider.js";
2
+ export declare class MemoryManager implements MemoryProvider {
3
+ private baseDir;
4
+ /** sessionKey → current stem (e.g. "2026-02-06-2324") */
5
+ private currentStem;
6
+ /** sessionKeys that have been cleared — next append creates a new file */
7
+ private cleared;
8
+ constructor(baseDir: string);
9
+ private getChatDir;
10
+ /** Returns the stem for the current session (e.g. "2026-02-06-2324") */
11
+ private getCurrentStem;
12
+ getConversationFile(sessionKey: string): string;
13
+ getAttachmentsDir(sessionKey: string): string;
14
+ private ensureConversationFile;
15
+ append(sessionKey: string, role: "user" | "assistant", content: string, attachments?: string[]): Promise<void>;
16
+ saveFile(sessionKey: string, buffer: Buffer, fileName: string): Promise<string>;
17
+ getConversationMessages(sessionKey: string): ConversationMessage[];
18
+ retrieveFromMemory(queryText: string): MemorySearchResult[];
19
+ clearSession(sessionKey: string): void;
20
+ }
21
+ //# sourceMappingURL=memory-manager.d.ts.map
@@ -0,0 +1 @@
1
+ import{mkdirSync as t,appendFileSync as e,readFileSync as n,writeFileSync as s,existsSync as r,readdirSync as i}from"node:fs";import{join as o}from"node:path";import{createLogger as a}from"../utils/logger.js";const c=a("MemoryManager");export class MemoryManager{baseDir;currentStem=new Map;cleared=new Set;constructor(e){this.baseDir=e,t(e,{recursive:!0})}getChatDir(e){const n=e.replace(/:/g,"_"),s=o(this.baseDir,n);return t(s,{recursive:!0}),s}getCurrentStem(t){const e=this.currentStem.get(t);if(e)return e;if(!this.cleared.has(t)){const e=this.getChatDir(t),n=i(e).filter(t=>t.endsWith(".md")).sort();if(n.length>0){const e=n[n.length-1].replace(".md","");return this.currentStem.set(t,e),e}}const n=function(){const t=new Date,e=t=>String(t).padStart(2,"0");return`${t.getFullYear()}-${e(t.getMonth()+1)}-${e(t.getDate())}-${e(t.getHours())}${e(t.getMinutes())}`}();return this.currentStem.set(t,n),this.cleared.delete(t),n}getConversationFile(t){const e=this.getCurrentStem(t);return o(this.getChatDir(t),`${e}.md`)}getAttachmentsDir(e){const n=this.getCurrentStem(e),s=o(this.getChatDir(e),n);return t(s,{recursive:!0}),s}ensureConversationFile(t){const n=this.getConversationFile(t);if(!r(n)){const s=[`# Memory: ${t}`,`- Started: ${(new Date).toISOString()}`,"","---",""].join("\n");e(n,s,"utf-8"),c.info(`Memory file created: ${n}`)}return n}async append(t,n,s,r){const i=this.ensureConversationFile(t);let o=`### ${n} (${(new Date).toISOString()})\n`;r&&r.length>0&&(o+=`[files: ${r.join(", ")}]\n`),o+=`\n${s}\n\n`,e(i,o,"utf-8")}async saveFile(t,e,n){const r=this.getAttachmentsDir(t),i=(new Date).toISOString().replace(/[-:]/g,"").replace("T","_").slice(0,15),a=n.replace(/[^a-zA-Z0-9._-]/g,"_").slice(0,100),l=o(r,`${i}_${a}`);return s(l,e),c.info(`File saved to memory: ${l} (${e.length} bytes)`),l}getConversationMessages(t){const e=this.getConversationFile(t);if(!r(e))return[];return l(n(e,"utf-8"))}retrieveFromMemory(t){const e=[];if(!r(this.baseDir))return e;const s=t.toLowerCase(),a=i(this.baseDir,{withFileTypes:!0});for(const t of a){if(!t.isDirectory())continue;const r=o(this.baseDir,t.name),a=i(r).filter(t=>t.endsWith(".md"));for(const i of a){const a=o(r,i),c=l(n(a,"utf-8")),h=[],u=new Set;let f="",g="";for(const t of c)if(t.content.toLowerCase().includes(s)){if(h.push(m(t.content,s)),!f&&t.timestamp){const e=new Date(t.timestamp);isNaN(e.getTime())||(f=e.toISOString().slice(0,10),g=e.toISOString().slice(11,19))}for(const e of t.attachments)u.add(e)}h.length>0&&e.push({memoryFile:a,sessionKey:t.name,date:f,time:g,snippets:h,files:Array.from(u)})}}return e}clearSession(t){this.currentStem.delete(t),this.cleared.add(t),c.info(`Memory session cleared for ${t} — next message starts a new file`)}}function l(t){const e=[],n=t.split(/^### /m).slice(1);for(const t of n){const n=t.match(/^(user|assistant)\s*\(([^)]+)\)\s*\n/);if(!n)continue;const s=n[1],r=n[2],i=t.slice(n[0].length);let o,a=[];const c=i.match(/^\[files:\s*(.+?)\]\s*\n/);c?(a=c[1].split(",").map(t=>t.trim()),o=i.slice(c[0].length).trim()):o=i.trim(),e.push({role:s,content:o,timestamp:r,attachments:a})}return e}function m(t,e,n=150){const s=t.toLowerCase().indexOf(e);if(-1===s)return t.slice(0,2*n);const r=Math.max(0,s-n),i=Math.min(t.length,s+e.length+n);let o=t.slice(r,i);return r>0&&(o="..."+o),i<t.length&&(o+="..."),o}
@@ -0,0 +1,22 @@
1
+ export interface ConversationMessage {
2
+ role: "user" | "assistant";
3
+ content: string;
4
+ timestamp: string;
5
+ attachments: string[];
6
+ }
7
+ export interface MemorySearchResult {
8
+ memoryFile: string;
9
+ sessionKey: string;
10
+ date: string;
11
+ time: string;
12
+ snippets: string[];
13
+ files: string[];
14
+ }
15
+ export interface MemoryProvider {
16
+ append(sessionKey: string, role: "user" | "assistant", content: string, attachments?: string[]): Promise<void>;
17
+ saveFile(sessionKey: string, buffer: Buffer, fileName: string): Promise<string>;
18
+ getConversationMessages(sessionKey: string): ConversationMessage[];
19
+ retrieveFromMemory(query: string): MemorySearchResult[];
20
+ clearSession(sessionKey: string): void;
21
+ }
22
+ //# sourceMappingURL=memory-provider.d.ts.map
@@ -0,0 +1 @@
1
+ export{};
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Hybrid memory search engine: BM25 (FTS5) + OpenAI embeddings (HNSW) + RRF fusion.
3
+ *
4
+ * Architecture — Indexer/Retriever split on separate SQLite databases + HNSW index:
5
+ *
6
+ * memory-index.db ← Indexer writes (always)
7
+ * memory-vectors.hnsw ← HNSW index (indexer side)
8
+ * memory-search.db ← Retriever reads (active)
9
+ * memory-search-next.db ← snapshot ready for swap
10
+ * memory-vectors-search.hnsw ← HNSW index (retriever side, active)
11
+ * memory-vectors-search-next.hnsw ← HNSW snapshot ready for swap
12
+ *
13
+ * Indexer cycle (reactive, two-phase):
14
+ * 1. fs.watch(memoryDir) → debounce → parse markdown → upsert chunks → FTS synced via triggers
15
+ * 2. Embed timer → find un-embedded chunks → batch OpenAI → store in HNSW + embeddings table
16
+ * 3. After each cycle: snapshot DB + HNSW → atomic swap
17
+ *
18
+ * Retriever (per search call):
19
+ * 1. Check if snapshots exist → swap if so
20
+ * 2. BM25 via FTS5 → top 20
21
+ * 3. Dense: embed query → HNSW searchKnn → top 20
22
+ * 4. RRF fusion (k=60) → return top N
23
+ */
24
+ export interface MemorySearchResult {
25
+ path: string;
26
+ sessionKey: string;
27
+ snippet: string;
28
+ score: number;
29
+ role: string;
30
+ timestamp: string;
31
+ }
32
+ export interface MemorySearchOptions {
33
+ apiKey: string;
34
+ baseURL?: string;
35
+ embeddingModel: string;
36
+ embeddingDimensions: number;
37
+ prefixQuery: string;
38
+ prefixDocument: string;
39
+ updateDebounceMs: number;
40
+ embedIntervalMs: number;
41
+ maxResults: number;
42
+ maxSnippetChars: number;
43
+ maxInjectedChars: number;
44
+ rrfK: number;
45
+ }
46
+ export declare class MemorySearch {
47
+ private memoryDir;
48
+ private dataDir;
49
+ private opts;
50
+ private indexDb;
51
+ private indexHnsw;
52
+ private watcher;
53
+ private debounceTimer;
54
+ private embedTimer;
55
+ private indexing;
56
+ private searchDb;
57
+ private searchHnsw;
58
+ private openai;
59
+ private indexDbPath;
60
+ private searchDbPath;
61
+ private searchNextDbPath;
62
+ private indexHnswPath;
63
+ private searchHnswPath;
64
+ private searchNextHnswPath;
65
+ constructor(memoryDir: string, dataDir: string, opts: MemorySearchOptions);
66
+ private isOpenAI;
67
+ getMaxInjectedChars(): number;
68
+ start(): Promise<void>;
69
+ stop(): void;
70
+ private migrateEmbeddingsTable;
71
+ /**
72
+ * Check if embedding model or dimensions changed since last run.
73
+ * If so, wipe embeddings + HNSW and re-embed everything.
74
+ * Must be called AFTER SCHEMA_SQL has been executed (meta table exists).
75
+ */
76
+ private checkEmbeddingConfigChange;
77
+ private initIndexHnsw;
78
+ private ensureHnswCapacity;
79
+ search(query: string, maxResults?: number): Promise<MemorySearchResult[]>;
80
+ readFile(path: string, from?: number, lines?: number): {
81
+ path: string;
82
+ content: string;
83
+ };
84
+ private bm25Search;
85
+ private denseSearch;
86
+ private startWatcher;
87
+ private runIndexCycle;
88
+ private indexFiles;
89
+ private embedPending;
90
+ private publishSnapshot;
91
+ private maybeSwap;
92
+ /** Apply a prefix template to text. If prefix is empty, returns text as-is. */
93
+ private applyPrefix;
94
+ /**
95
+ * Ollama / local providers: POST {baseURL}/embed { model, input }
96
+ * → { embeddings: [number[], ...] }
97
+ */
98
+ private fetchEmbeddings;
99
+ /** Embed a single query text, returns null on failure. */
100
+ private embedText;
101
+ }
102
+ //# sourceMappingURL=memory-search.d.ts.map
@@ -0,0 +1 @@
1
+ import{existsSync as e,readdirSync as t,readFileSync as n,statSync as s,copyFileSync as i,renameSync as r,unlinkSync as h,watch as a}from"node:fs";import{join as o,relative as c,basename as d,dirname as m}from"node:path";import l from"better-sqlite3";import u from"openai";import p from"hnswlib-node";const{HierarchicalNSW:E}=p;import{createLogger as b}from"../utils/logger.js";const x=b("MemorySearch"),f="cosine";export class MemorySearch{memoryDir;dataDir;opts;indexDb=null;indexHnsw=null;watcher=null;debounceTimer=null;embedTimer=null;indexing=!1;searchDb=null;searchHnsw=null;openai=null;indexDbPath;searchDbPath;searchNextDbPath;indexHnswPath;searchHnswPath;searchNextHnswPath;constructor(e,t,n){this.memoryDir=e,this.dataDir=t,this.opts=n,this.indexDbPath=o(t,"memory-index.db"),this.searchDbPath=o(t,"memory-search.db"),this.searchNextDbPath=o(t,"memory-search-next.db"),this.indexHnswPath=o(t,"memory-vectors.hnsw"),this.searchHnswPath=o(t,"memory-vectors-search.hnsw"),this.searchNextHnswPath=o(t,"memory-vectors-search-next.hnsw"),this.isOpenAI()&&(this.openai=new u({apiKey:n.apiKey,...n.baseURL?{baseURL:n.baseURL}:{}}))}isOpenAI(){return(this.opts.baseURL||"https://api.openai.com/v1").includes("openai.com")}getMaxInjectedChars(){return this.opts.maxInjectedChars}async start(){x.info("Starting memory search engine..."),this.indexDb=new l(this.indexDbPath),this.indexDb.pragma("journal_mode = WAL"),this.migrateEmbeddingsTable(),this.indexDb.exec("\n CREATE TABLE IF NOT EXISTS documents (\n path TEXT PRIMARY KEY,\n mtime_ms INTEGER NOT NULL,\n size INTEGER NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS chunks (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n doc_path TEXT NOT NULL,\n chunk_idx INTEGER NOT NULL,\n role TEXT NOT NULL DEFAULT '',\n timestamp TEXT NOT NULL DEFAULT '',\n session_key TEXT NOT NULL DEFAULT '',\n content TEXT NOT NULL,\n UNIQUE(doc_path, chunk_idx)\n );\n\n CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(\n content,\n content='chunks',\n content_rowid='id',\n tokenize='porter unicode61'\n );\n\n CREATE TRIGGER IF NOT EXISTS chunks_ai AFTER INSERT ON chunks BEGIN\n INSERT INTO chunks_fts(rowid, content) VALUES (new.id, new.content);\n END;\n\n CREATE TRIGGER IF NOT EXISTS chunks_ad AFTER DELETE ON chunks BEGIN\n INSERT INTO chunks_fts(chunks_fts, rowid, content) VALUES ('delete', old.id, old.content);\n END;\n\n CREATE TRIGGER IF NOT EXISTS chunks_au AFTER UPDATE ON chunks BEGIN\n INSERT INTO chunks_fts(chunks_fts, rowid, content) VALUES ('delete', old.id, old.content);\n INSERT INTO chunks_fts(rowid, content) VALUES (new.id, new.content);\n END;\n\n -- Lookup table only (no vector BLOB) — vectors live in HNSW index\n CREATE TABLE IF NOT EXISTS embeddings (\n chunk_id INTEGER PRIMARY KEY\n );\n\n -- Metadata for detecting config changes (model, dimensions)\n CREATE TABLE IF NOT EXISTS meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n );\n"),this.checkEmbeddingConfigChange(),this.initIndexHnsw(),await this.indexFiles(),await this.embedPending(),this.publishSnapshot(),this.maybeSwap(),this.startWatcher(),this.opts.embedIntervalMs>0&&(this.embedTimer=setInterval(()=>{this.embedPending().catch(e=>x.error(`Embed cycle error: ${e}`))},this.opts.embedIntervalMs)),x.info("Memory search engine started")}stop(){this.watcher&&(this.watcher.close(),this.watcher=null),this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.embedTimer&&(clearInterval(this.embedTimer),this.embedTimer=null),this.indexDb&&(this.indexDb.close(),this.indexDb=null),this.searchDb&&(this.searchDb.close(),this.searchDb=null),this.indexHnsw=null,this.searchHnsw=null,x.info("Memory search engine stopped")}migrateEmbeddingsTable(){if(!this.indexDb)return;if(this.indexDb.prepare("PRAGMA table_info(embeddings)").all().some(e=>"vector"===e.name)){x.info("Migrating: dropping old embeddings table (had vector BLOB). All embeddings will be re-created via HNSW."),this.indexDb.exec("DROP TABLE IF EXISTS embeddings");try{h(this.indexHnswPath)}catch{}}}checkEmbeddingConfigChange(){if(!this.indexDb)return;const e=this.indexDb.prepare("SELECT value FROM meta WHERE key = ?"),t=this.indexDb.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)"),n=e.get("embedding_model")?.value,s=e.get("embedding_dimensions")?.value,i=this.opts.embeddingModel,r=String(this.opts.embeddingDimensions),a=void 0!==n&&n!==i,o=void 0!==s&&s!==r;if(a||o){const e=[];a&&e.push(`model: ${n} → ${i}`),o&&e.push(`dimensions: ${s} → ${r}`),x.info(`Embedding config changed (${e.join(", ")}). Wiping embeddings + HNSW for full re-embed.`),this.indexDb.exec("DELETE FROM embeddings");try{h(this.indexHnswPath)}catch{}}t.run("embedding_model",i),t.run("embedding_dimensions",r)}initIndexHnsw(){const t=this.opts.embeddingDimensions;if(this.indexHnsw=new E(f,t),e(this.indexHnswPath))try{this.indexHnsw.readIndexSync(this.indexHnswPath,!0),x.info(`Loaded HNSW index: ${this.indexHnsw.getCurrentCount()} points`)}catch(e){x.warn(`Failed to load HNSW index, creating new: ${e}`),this.indexHnsw.initIndex({maxElements:1e4,m:16,efConstruction:200,allowReplaceDeleted:!0})}else this.indexHnsw.initIndex({maxElements:1e4,m:16,efConstruction:200,allowReplaceDeleted:!0}),x.info("Created new HNSW index")}ensureHnswCapacity(e){if(!this.indexHnsw)return;const t=this.indexHnsw.getMaxElements();if(this.indexHnsw.getCurrentCount()+e>t){const n=Math.max(2*t,this.indexHnsw.getCurrentCount()+e+1e3);this.indexHnsw.resizeIndex(n),x.info(`Resized HNSW index: ${t} → ${n}`)}}async search(e,t){const n=t??this.opts.maxResults;if(this.maybeSwap(),!this.searchDb)return x.warn("Search DB not available"),[];const s=this.bm25Search(e,20);let i=[];try{const t=await this.embedText(e);t&&this.searchHnsw&&this.searchHnsw.getCurrentCount()>0&&(i=this.denseSearch(t,20))}catch(e){x.warn(`Dense search failed, using BM25 only: ${e}`)}const r=function(e,t,n){const s=new Map;for(let t=0;t<e.length;t++){const{id:i}=e[t];s.set(i,(s.get(i)??0)+1/(n+t+1))}for(let e=0;e<t.length;e++){const{id:i}=t[e];s.set(i,(s.get(i)??0)+1/(n+e+1))}const i=Array.from(s.entries()).map(([e,t])=>({id:e,score:t})).sort((e,t)=>t.score-e.score);return i}(s,i,this.opts.rrfK),h=[],a=this.searchDb.prepare("SELECT doc_path, role, timestamp, session_key, content FROM chunks WHERE id = ?");for(const{id:e,score:t}of r.slice(0,n)){const n=a.get(e);if(!n)continue;const s=n.content.length>this.opts.maxSnippetChars?n.content.slice(0,this.opts.maxSnippetChars)+"...":n.content;h.push({path:n.doc_path,sessionKey:n.session_key,snippet:s,score:t,role:n.role,timestamp:n.timestamp})}return x.info(`Search "${e.slice(0,60)}": ${h.length} results (sparse=${s.length}, dense=${i.length})`),h}readFile(t,s,i){const r=o(this.memoryDir,t);if(!e(r))return{path:t,content:`[File not found: ${t}]`};const h=n(r,"utf-8"),a=h.split("\n");if(void 0!==s||void 0!==i){const e=Math.max(0,(s??1)-1),n=i??a.length;return{path:t,content:a.slice(e,e+n).join("\n")}}return{path:t,content:h}}bm25Search(e,t){if(!this.searchDb)return[];try{const n=function(e){const t=e.replace(/[^\w\s]/g," ").split(/\s+/).filter(e=>e.length>0);return 0===t.length?"":t.map(e=>`"${e}"`).join(" OR ")}(e);if(!n)return[];return this.searchDb.prepare("\n SELECT chunks.id, bm25(chunks_fts) as rank\n FROM chunks_fts\n JOIN chunks ON chunks.id = chunks_fts.rowid\n WHERE chunks_fts MATCH ?\n ORDER BY rank\n LIMIT ?\n ").all(n,t).map(e=>({id:e.id,score:-e.rank}))}catch(e){return x.warn(`BM25 search error: ${e}`),[]}}denseSearch(e,t){if(!this.searchHnsw||0===this.searchHnsw.getCurrentCount())return[];const n=Math.min(t,this.searchHnsw.getCurrentCount()),s=this.searchHnsw.searchKnn(Array.from(e),n);return s.neighbors.map((e,t)=>({id:e,score:1-s.distances[t]}))}startWatcher(){if(e(this.memoryDir))try{this.watcher=a(this.memoryDir,{recursive:!0},(e,t)=>{this.debounceTimer&&clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(()=>{this.runIndexCycle()},this.opts.updateDebounceMs)})}catch(e){x.warn(`Could not start file watcher: ${e}`)}}async runIndexCycle(){if(!this.indexing){this.indexing=!0;try{await this.indexFiles(),this.publishSnapshot()}catch(e){x.error(`Index cycle error: ${e}`)}finally{this.indexing=!1}}}async indexFiles(){if(!this.indexDb||!e(this.memoryDir))return;const i=function(e){const n=[];function i(r){let h;try{h=t(r,{withFileTypes:!0})}catch{return}for(const t of h){const h=o(r,t.name);if(t.isDirectory())i(h);else if(t.name.endsWith(".md"))try{const t=s(h),i=c(e,h),r=d(m(h));n.push({fullPath:h,relPath:i,sessionKey:r===d(e)?"":r,mtimeMs:Math.floor(t.mtimeMs),size:t.size})}catch{}}}return i(e),n}(this.memoryDir);let r=0;const h=this.indexDb.prepare("INSERT OR REPLACE INTO documents (path, mtime_ms, size) VALUES (?, ?, ?)"),a=this.indexDb.prepare("SELECT mtime_ms, size FROM documents WHERE path = ?"),l=this.indexDb.prepare("DELETE FROM chunks WHERE doc_path = ?"),u=this.indexDb.prepare("SELECT id FROM chunks WHERE doc_path = ?"),p=this.indexDb.prepare("DELETE FROM embeddings WHERE chunk_id IN (SELECT id FROM chunks WHERE doc_path = ?)"),E=this.indexDb.prepare("INSERT INTO chunks (doc_path, chunk_idx, role, timestamp, session_key, content) VALUES (?, ?, ?, ?, ?, ?)"),b=this.indexDb.prepare("SELECT path FROM documents").all().map(e=>e.path),f=new Set(i.map(e=>e.relPath));for(const e of b)if(!f.has(e)){const t=u.all(e);for(const{id:e}of t)try{this.indexHnsw?.markDelete(e)}catch{}p.run(e),l.run(e),this.indexDb.prepare("DELETE FROM documents WHERE path = ?").run(e),x.debug(`Removed deleted file from index: ${e}`)}for(const e of i){const t=a.get(e.relPath);if(t&&t.mtime_ms===e.mtimeMs&&t.size===e.size){r+=this.indexDb.prepare("SELECT COUNT(*) as c FROM chunks WHERE doc_path = ?").get(e.relPath).c;continue}const s=T(n(e.fullPath,"utf-8"),e.relPath,e.sessionKey),i=u.all(e.relPath);for(const{id:e}of i)try{this.indexHnsw?.markDelete(e)}catch{}p.run(e.relPath),l.run(e.relPath);this.indexDb.transaction(()=>{for(let t=0;t<s.length;t++){const n=s[t];E.run(e.relPath,t,n.role,n.timestamp,n.sessionKey,n.content)}h.run(e.relPath,e.mtimeMs,e.size)})(),r+=s.length,x.debug(`Indexed ${e.relPath}: ${s.length} chunks`)}x.info(`Indexed ${r} chunks from ${i.length} files`)}async embedPending(){if(!this.indexDb||!this.indexHnsw)return;const e=this.indexDb.prepare("\n SELECT c.id, c.content FROM chunks c\n LEFT JOIN embeddings e ON e.chunk_id = c.id\n WHERE e.chunk_id IS NULL\n ").all();if(0===e.length)return;x.info(`Embedding ${e.length} pending chunks...`),this.ensureHnswCapacity(e.length);const t=this.indexDb.prepare("INSERT OR REPLACE INTO embeddings (chunk_id) VALUES (?)");for(let n=0;n<e.length;n+=100){const s=e.slice(n,n+100),i=s.map(e=>this.applyPrefix(this.opts.prefixDocument,e.content).slice(0,8e3)),r=this.opts.prefixDocument.trim();r&&x.debug(`Using prefixDocument (template: ${r}) → result sample: [${i[0].slice(0,80)}]`);try{let e;if(this.openai){const t=await this.openai.embeddings.create({model:this.opts.embeddingModel,input:i,dimensions:this.opts.embeddingDimensions});e=t.data.sort((e,t)=>e.index-t.index).map(e=>e.embedding)}else e=await this.fetchEmbeddings(i);this.indexDb.transaction(()=>{for(let n=0;n<e.length;n++)this.indexHnsw.addPoint(e[n],s[n].id,!0),t.run(s[n].id)})(),x.debug(`Embedded batch ${n/100+1}: ${s.length} chunks`)}catch(e){x.error(`Embedding batch failed: ${e}`)}}this.indexHnsw.writeIndexSync(this.indexHnswPath),this.publishSnapshot(),x.info(`Embedded ${e.length} chunks (HNSW: ${this.indexHnsw.getCurrentCount()} total points)`)}publishSnapshot(){if(!this.indexDb)return;const t=o(this.dataDir,".memory-search-next.tmp"),n=o(this.dataDir,".memory-vectors-search-next.tmp");try{this.indexDb.pragma("wal_checkpoint(TRUNCATE)"),i(this.indexDbPath,t),r(t,this.searchNextDbPath),e(this.indexHnswPath)&&(i(this.indexHnswPath,n),r(n,this.searchNextHnswPath)),x.debug("Published search snapshot (DB + HNSW)")}catch(e){x.error(`Failed to publish snapshot: ${e}`);try{h(t)}catch{}try{h(n)}catch{}}}maybeSwap(){if(e(this.searchNextDbPath))try{this.searchDb&&(this.searchDb.close(),this.searchDb=null),this.searchHnsw=null,r(this.searchNextDbPath,this.searchDbPath),e(this.searchNextHnswPath)&&r(this.searchNextHnswPath,this.searchHnswPath),this.searchDb=new l(this.searchDbPath,{readonly:!0}),e(this.searchHnswPath)?(this.searchHnsw=new E(f,this.opts.embeddingDimensions),this.searchHnsw.readIndexSync(this.searchHnswPath),this.searchHnsw.setEf(50),x.debug(`Swapped to new search DB + HNSW (${this.searchHnsw.getCurrentCount()} points)`)):x.debug("Swapped to new search DB (no HNSW index yet)")}catch(t){x.error(`Failed to swap search DB: ${t}`);try{e(this.searchDbPath)&&(this.searchDb=new l(this.searchDbPath,{readonly:!0}))}catch{}}}applyPrefix(e,t){const n=e.trim();return n?n.replace(/\{content\}/g,()=>t):t}async fetchEmbeddings(e){const t=`${(this.opts.baseURL||"").replace(/\/+$/,"")}/embed`,n={"Content-Type":"application/json"};this.opts.apiKey&&(n.Authorization=`Bearer ${this.opts.apiKey}`);const s=await fetch(t,{method:"POST",headers:n,body:JSON.stringify({model:this.opts.embeddingModel,input:e})});if(!s.ok){const e=await s.text().catch(()=>"(no body)");throw new Error(`Embedding API ${s.status}: ${e.slice(0,300)}`)}const i=await s.json();if(Array.isArray(i.embeddings))return i.embeddings;throw new Error(`Unknown embedding response format. Keys: ${Object.keys(i).join(", ")}`)}async embedText(e){try{const t=this.applyPrefix(this.opts.prefixQuery,e),n=this.opts.prefixQuery.trim();if(n&&x.debug(`Using prefixQuery (template: ${n}) → result sample: [${t.slice(0,80)}]`),this.openai){const e=await this.openai.embeddings.create({model:this.opts.embeddingModel,input:t.slice(0,8e3),dimensions:this.opts.embeddingDimensions});return new Float32Array(e.data[0].embedding)}const s=await this.fetchEmbeddings([t.slice(0,8e3)]);return new Float32Array(s[0])}catch(e){return x.error(`Failed to embed query: ${e}`),null}}}function T(e,t,n){const s=[],i=e.split(/^### /m);for(const e of i){if(!e.trim())continue;const t=e.match(/^(user|assistant)\s*\(([^)]+)\)\s*\n/),i=t?t[1]:"",r=t?t[2]:"",h=t?e.slice(t[0].length).trim():e.trim();if(!h)continue;const a=1500,o=100;if(h.length<=a)s.push({role:i,timestamp:r,sessionKey:n,content:h});else{let e=0;for(;e<h.length;){const t=Math.min(e+a,h.length),c=h.slice(e,t);if(s.push({role:i,timestamp:r,sessionKey:n,content:c}),e=t-o,e+o>=h.length)break}}}return s}
@@ -0,0 +1,2 @@
1
+ export declare function getRecallStrategy(_name: string): string;
2
+ //# sourceMappingURL=recall-strategies.d.ts.map
@@ -0,0 +1 @@
1
+ export function getRecallStrategy(t){return t}