hive-am 1.5.0 → 1.7.0

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 (142) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/README.md +5 -4
  3. package/package.json +1 -1
  4. package/server/dist/connections/format.js +26 -0
  5. package/server/dist/connections/index.js +2 -0
  6. package/server/dist/connections/manager.js +3 -3
  7. package/server/dist/connections/router.js +42 -12
  8. package/server/dist/connections/secrets.js +1 -1
  9. package/server/dist/connections/slack.js +488 -0
  10. package/server/dist/connections/store.js +2 -0
  11. package/server/dist/db/migrations.js +12 -0
  12. package/server/dist/db.js +15 -3
  13. package/server/dist/dispatches.js +126 -0
  14. package/server/dist/http/routes/orchestration.js +13 -2
  15. package/server/dist/http/validate.js +22 -2
  16. package/server/dist/index.js +2 -0
  17. package/server/dist/instructions.js +1 -1
  18. package/server/dist/retention.js +4 -2
  19. package/server/dist/runtime.js +17 -6
  20. package/server/dist/services/connections.js +18 -3
  21. package/server/dist/skills/defaults.js +6 -4
  22. package/server/dist/watch.js +1 -1
  23. package/server/mcp/dispatch.mjs +48 -5
  24. package/web/.next/BUILD_ID +1 -1
  25. package/web/.next/app-build-manifest.json +91 -75
  26. package/web/.next/app-path-routes-manifest.json +5 -5
  27. package/web/.next/build-manifest.json +2 -2
  28. package/web/.next/prerender-manifest.json +61 -61
  29. package/web/.next/server/app/_not-found/page.js +2 -2
  30. package/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  31. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  32. package/web/.next/server/app/_not-found.html +1 -1
  33. package/web/.next/server/app/_not-found.rsc +3 -3
  34. package/web/.next/server/app/agents/[id]/page.js +4 -4
  35. package/web/.next/server/app/agents/[id]/page.js.nft.json +1 -1
  36. package/web/.next/server/app/agents/[id]/page_client-reference-manifest.js +1 -1
  37. package/web/.next/server/app/agents/page.js +2 -2
  38. package/web/.next/server/app/agents/page.js.nft.json +1 -1
  39. package/web/.next/server/app/agents/page_client-reference-manifest.js +1 -1
  40. package/web/.next/server/app/agents.html +1 -1
  41. package/web/.next/server/app/agents.rsc +21 -18
  42. package/web/.next/server/app/chat/page.js +2 -2
  43. package/web/.next/server/app/chat/page.js.nft.json +1 -1
  44. package/web/.next/server/app/chat/page_client-reference-manifest.js +1 -1
  45. package/web/.next/server/app/chat.html +1 -1
  46. package/web/.next/server/app/chat.rsc +4 -4
  47. package/web/.next/server/app/connections/page.js +2 -2
  48. package/web/.next/server/app/connections/page.js.nft.json +1 -1
  49. package/web/.next/server/app/connections/page_client-reference-manifest.js +1 -1
  50. package/web/.next/server/app/connections.html +1 -1
  51. package/web/.next/server/app/connections.rsc +4 -4
  52. package/web/.next/server/app/index.html +1 -1
  53. package/web/.next/server/app/index.rsc +4 -4
  54. package/web/.next/server/app/page.js +2 -2
  55. package/web/.next/server/app/page.js.nft.json +1 -1
  56. package/web/.next/server/app/page_client-reference-manifest.js +1 -1
  57. package/web/.next/server/app/relations/page.js +2 -2
  58. package/web/.next/server/app/relations/page.js.nft.json +1 -1
  59. package/web/.next/server/app/relations/page_client-reference-manifest.js +1 -1
  60. package/web/.next/server/app/relations.html +1 -1
  61. package/web/.next/server/app/relations.rsc +4 -4
  62. package/web/.next/server/app/resources/[id]/page.js +2 -2
  63. package/web/.next/server/app/resources/[id]/page.js.nft.json +1 -1
  64. package/web/.next/server/app/resources/[id]/page_client-reference-manifest.js +1 -1
  65. package/web/.next/server/app/resources/page.js +2 -2
  66. package/web/.next/server/app/resources/page.js.nft.json +1 -1
  67. package/web/.next/server/app/resources/page_client-reference-manifest.js +1 -1
  68. package/web/.next/server/app/resources.html +1 -1
  69. package/web/.next/server/app/resources.rsc +4 -4
  70. package/web/.next/server/app/schedules/page.js +2 -2
  71. package/web/.next/server/app/schedules/page.js.nft.json +1 -1
  72. package/web/.next/server/app/schedules/page_client-reference-manifest.js +1 -1
  73. package/web/.next/server/app/schedules.html +1 -1
  74. package/web/.next/server/app/schedules.rsc +4 -4
  75. package/web/.next/server/app/sessions/page.js +2 -2
  76. package/web/.next/server/app/sessions/page.js.nft.json +1 -1
  77. package/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  78. package/web/.next/server/app/sessions.html +1 -1
  79. package/web/.next/server/app/sessions.rsc +4 -4
  80. package/web/.next/server/app/skills/page.js +2 -2
  81. package/web/.next/server/app/skills/page.js.nft.json +1 -1
  82. package/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
  83. package/web/.next/server/app/skills.html +1 -1
  84. package/web/.next/server/app/skills.rsc +4 -4
  85. package/web/.next/server/app/types/page.js +2 -2
  86. package/web/.next/server/app/types/page.js.nft.json +1 -1
  87. package/web/.next/server/app/types/page_client-reference-manifest.js +1 -1
  88. package/web/.next/server/app/types.html +1 -1
  89. package/web/.next/server/app/types.rsc +4 -4
  90. package/web/.next/server/app-paths-manifest.json +5 -5
  91. package/web/.next/server/chunks/{505.js → 111.js} +1 -1
  92. package/web/.next/server/chunks/143.js +1 -1
  93. package/web/.next/server/chunks/716.js +2 -2
  94. package/web/.next/server/chunks/741.js +2 -0
  95. package/web/.next/server/chunks/837.js +1 -0
  96. package/web/.next/server/chunks/{435.js → 939.js} +3 -3
  97. package/web/.next/server/chunks/944.js +1 -0
  98. package/web/.next/server/pages/404.html +1 -1
  99. package/web/.next/server/pages/500.html +1 -1
  100. package/web/.next/server/server-reference-manifest.json +1 -1
  101. package/web/.next/static/chunks/162-68ba190086f5ad39.js +1 -0
  102. package/web/.next/static/chunks/214-35b88db02113ae02.js +1 -0
  103. package/web/.next/static/chunks/281-a855bf409ef9eebd.js +1 -0
  104. package/web/.next/static/chunks/350-bdf6b8994a28f2b4.js +1 -0
  105. package/web/.next/static/chunks/491-196bcb3dd11bb460.js +1 -0
  106. package/web/.next/static/chunks/517-8f2db464d10a4624.js +1 -0
  107. package/web/.next/static/chunks/{586-f7017d585a84738d.js → 586-4f67b823df060b46.js} +1 -1
  108. package/web/.next/static/chunks/{587-5b511f5f93910180.js → 587-3c53d91a1c4f99d6.js} +1 -1
  109. package/web/.next/static/chunks/642-dc874c8caf203ee1.js +1 -0
  110. package/web/.next/static/chunks/771-db19da9b1a24820c.js +1 -0
  111. package/web/.next/static/chunks/928-09d4a58e977641dc.js +1 -0
  112. package/web/.next/static/chunks/app/agents/[id]/page-9f191f25a366b26b.js +1 -0
  113. package/web/.next/static/chunks/app/agents/layout-71dd98d696935e29.js +1 -0
  114. package/web/.next/static/chunks/app/connections/page-b0ea2eae62cef74e.js +1 -0
  115. package/web/.next/static/chunks/app/layout-cb5af29b0742c513.js +1 -0
  116. package/web/.next/static/chunks/app/page-5e547e87de23d9fa.js +1 -0
  117. package/web/.next/static/chunks/app/resources/[id]/page-1e2afddcb1f04884.js +1 -0
  118. package/web/.next/static/chunks/app/resources/{page-61cccd8fdcc2f6c8.js → page-195b0af83b8ace0d.js} +1 -1
  119. package/web/.next/static/chunks/app/sessions/{page-85638705fdc18174.js → page-f5a98ec3eaffd7a6.js} +1 -1
  120. package/web/.next/static/chunks/app/types/page-c4c2b2217c4667e8.js +1 -0
  121. package/web/.next/static/css/f5438d03b796884e.css +1 -0
  122. package/web/.next/trace +30 -30
  123. package/web/.next/types/routes.d.ts +2 -1
  124. package/web/.next/types/validator.ts +9 -0
  125. package/web/.next/server/chunks/274.js +0 -1
  126. package/web/.next/server/chunks/612.js +0 -1
  127. package/web/.next/static/chunks/107-77dd87cc2ca4f941.js +0 -1
  128. package/web/.next/static/chunks/281-30e32d932b8b5a75.js +0 -1
  129. package/web/.next/static/chunks/350-0fe82e622e7a670f.js +0 -1
  130. package/web/.next/static/chunks/391-18768499e741112e.js +0 -1
  131. package/web/.next/static/chunks/517-b3b1c9044d2ef23c.js +0 -1
  132. package/web/.next/static/chunks/656-f3cc78328603241a.js +0 -1
  133. package/web/.next/static/chunks/705-ce86175dea00e6a9.js +0 -1
  134. package/web/.next/static/chunks/app/agents/[id]/page-638e12ba3cb813c3.js +0 -1
  135. package/web/.next/static/chunks/app/connections/page-06bd2d7d9495126a.js +0 -1
  136. package/web/.next/static/chunks/app/layout-fad968a635f9a4b3.js +0 -1
  137. package/web/.next/static/chunks/app/page-0062ebc30322b1ae.js +0 -1
  138. package/web/.next/static/chunks/app/resources/[id]/page-fc494e85b01e8cb7.js +0 -1
  139. package/web/.next/static/chunks/app/types/page-8e25f503bdcd72ad.js +0 -1
  140. package/web/.next/static/css/dfcaa19a8d46e60d.css +0 -1
  141. /package/web/.next/static/{UDA414PFPu9ZROltNmjbX → gqk-RaeJBRWW1cWGRixT2}/_buildManifest.js +0 -0
  142. /package/web/.next/static/{UDA414PFPu9ZROltNmjbX → gqk-RaeJBRWW1cWGRixT2}/_ssgManifest.js +0 -0
package/CHANGELOG.md CHANGED
@@ -6,6 +6,35 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.7.0] - 2026-10-21
10
+
11
+ ### Added
12
+
13
+ - **Compact chat view.** A **Normal / Compact** switch under the chat box, next to **Expand all tools**, is remembered in the browser for every agent's chat (Normal stays the default and unchanged). In Compact each reasoning block and tool call is a small pill with a grey icon on the agent's row, with a short label and the full command or path as tooltip; clicking it opens the usual detail (command and output with copy buttons, diffs) under the row, and several can be open at once. Earlier replies fold into one summary pill (`3 steps · 1 tool · 4.9s`) that shows their pills on click; failed steps and the summaries that hide them are tinted red with the failed count; a running tool keeps the honey border and live timer and is never folded; **Files changed** is a pill with the `+added −removed` totals that stays visible. **Expand all tools** also opens every pill and unfolds every summary. Opening a pill higher up no longer moves the view.
14
+
15
+ ## [1.6.0] - 2026-10-20
16
+
17
+ ### Added
18
+
19
+ - **Fold the main menu to icons.** A button next to the hive-am name folds the sidebar from 220 px to a 64 px column of icons, giving the chat and every other screen more room. Each icon keeps its name as a tooltip and for screen readers, counts stay as small badges and the EN/ES switch stays at hand. The choice is remembered and applied before the page paints, so a reload never flashes the wide menu. On narrow screens (under 760 px) the menu stays the top bar with its labels.
20
+ - **Fold the colony groups of the chat's agent list.** Each colony heading in the side list of the chat is now a button that folds or unfolds its group (mouse or keyboard), and the folded groups are remembered. A folded group still shows the agent you are on and a pulsing dot when one of its agents is working; while searching every group with a match is open. The collapsed list (avatars only) keeps the groups apart with a compact heading per colony (its initials in its color and the count, the full name on hover) that folds the same groups. Orchestrators come first in each group in both modes, the headings are larger and easier to read, and a hive with no agents says "No agents yet" instead of an empty search message.
21
+ - **Slack connections.** A connection can now link an agent to a Slack app, with everything Telegram does: DMs and channels, the allow list (Slack member and channel ids), files in and out, reactions (by Slack name) and the shared single conversation. It runs over **Socket Mode**, so no public URL is needed and hive-am can stay on a laptop; the socket is refreshed and reconnected on its own, and a wrong token says which one. The agent answers **in a thread opened on the message** and keeps answering there; scheduled messages, reminders and the `wake_when_done` notice go to the channel. Replies are sent as `mrkdwn`. The connection form has a working platform selector, the two tokens and Slack's own words; the manual explains how to create the app. Tested against a fake Slack (`sim-slack.ts`).
22
+ - **`/help` and `/id` commands, and `/clear` and `/reset` as other names for `/new`**, on Telegram and Slack alike. `/id` answers the sender's id and the chat's, so an admin can add them. On Slack they come as slash commands (`/hive status`, `/hive-status` or `/status`).
23
+ - **Limit who can talk inside an authorized group or channel.** Each allowed Telegram group or Slack channel now has a mode in the connection form: **Everyone in it** (the default, and what every saved chat keeps, with no migration) or **Only these people**, with its own list of person ids (Telegram user ids, Slack member ids `U…` or `W…`). In a restricted chat only those people can talk, in every topic and thread of it; a member is an admin (`/new`) only if they are also on the connection's people list with **Admin**. Anyone else is ignored, and if they call the agent they are told that the chat only admits its list, with their id and the chat's. With "Listens and chimes in" their messages do not reach the agent even as context. DMs are unchanged. The server refuses an unknown mode, more than 50 members per chat and ids that do not look like the platform's. Tested in `test-chat-access.ts` (no model).
24
+ - **Delegate in the background.** `dispatch` now returns at once with a task id by default (`background` is true unless you pass `background: false` to wait for the answer), the orchestrator can keep answering the person or delegate to other subagents, and when the subagent finishes the orchestrator receives a message with the task id, the subagent and the result, in the place where it was asked (its web conversation, or the same Telegram or Slack thread). The new `dispatch_status` tool reads the state of a task (queued, running, done, failed or interrupted) and its result, and can wait up to 60 seconds for the end. The result lives in hive-am, not in the call: it is announced exactly once, even across a restart; a task left running by a restart is marked interrupted and its orchestrator is told to check the work and send it again; pruning keeps a result nobody has had yet. An orchestrator can have up to 10 unfinished background tasks. The orchestrator's instructions tell it to wait with `background: false` only for a very short task whose answer it needs right now. Tested in `test-dispatch-background.ts` (no model).
25
+
26
+ ### Changed
27
+
28
+ - **The agent and agent-type forms are one component.** The types screen now renders the agent form (`AgentForm`) in type mode instead of its own copy of the fields, so a new agent field is written once. Nothing visible changes, and saving a type sends only the fields a type stores.
29
+ - **The connection's people list is for direct messages, and each authorized group follows its own rule.** In the form it is now **People (direct messages)**, apart from **Groups** and **Allowed groups** (**Allowed channels** on Slack), each with its own help. The people list still talks in DMs and in groups that are not listed, but no longer opens a group limited to **Only these people**: only that group's members get in, and an empty member list admits nobody. If you already limited a group and someone on the people list talked there, add them to the group's list too. Rows wrap in a narrow drawer.
30
+ - **Someone without access is told at every message aimed at the agent**, not only the first time, so the bot no longer looks dead. To keep a stranger from flooding a chat, it sends at most 5 notices per person and chat and 20 per chat in a minute, then stays silent. The notice of a limited chat now only points to the chat's members ("channel" on Slack).
31
+
32
+ ### Fixed
33
+
34
+ - **Picking an agent in the chat's side list no longer sends the list back to the top.** The whole agent page was remounted for each agent, list included, so after scrolling down and choosing an agent the list jumped to the start and the chosen agent went out of view. The list now stays mounted (it keeps its scroll and its search text) while the chat still resets its draft, tab and viewed session, and opening or reloading an agent further down scrolls the list just enough to show it.
35
+ - **A long answer from a delegated task, a wake-up or a schedule is no longer refused in a chat.** These notices went through the 8000 character limit of incoming messages, so a subagent's answer longer than that came back as "Message too long." and never reached the orchestrator in its Telegram or Slack thread (it stayed readable with `dispatch_status`). The limit now applies only to what people write.
36
+ - **A delegation longer than five minutes no longer fails with `fetch failed` and loses the answer.** The synchronous `dispatch` held one HTTP request until the subagent finished, and Node's `fetch` gives up after 300 seconds without an answer. It now starts the task and waits with short `dispatch_status` calls (25 seconds each), so it returns the subagent's answer however long it takes. The orchestrator's own CLI is still cut after 10 minutes of silence while it waits on a tool, which is why `dispatch` goes in the background by default. The Recent delegations list shows a task waiting behind a busy subagent as queued (it used to look failed), and its status on hover.
37
+
9
38
  ## [1.5.0] - 2026-10-19
10
39
 
11
40
  ### Added
@@ -212,7 +241,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
212
241
  - Complete documentation in English and Spanish.
213
242
  - npm package with the `hive-am` command.
214
243
 
215
- [Unreleased]: https://github.com/hughcbscrt/hive-am/compare/v1.5.0...HEAD
244
+ [Unreleased]: https://github.com/hughcbscrt/hive-am/compare/v1.7.0...HEAD
245
+ [1.7.0]: https://github.com/hughcbscrt/hive-am/compare/v1.6.0...v1.7.0
246
+ [1.6.0]: https://github.com/hughcbscrt/hive-am/compare/v1.5.0...v1.6.0
216
247
  [1.5.0]: https://github.com/hughcbscrt/hive-am/compare/v1.4.0...v1.5.0
217
248
  [1.4.0]: https://github.com/hughcbscrt/hive-am/compare/v1.3.0...v1.4.0
218
249
  [1.3.0]: https://github.com/hughcbscrt/hive-am/compare/v1.2.2...v1.3.0
package/README.md CHANGED
@@ -33,6 +33,7 @@ have installed; it does not replace them and it never copies your conversations
33
33
  - **Skills.** Ten skills ship with hive-am (all ordinary: edit or delete them). Assign them to an agent, a type or a colony, and choose per assignment whether they are **always** in the instructions or loaded **on demand** with a `skill_read` tool. Searchable picker with token weights.
34
34
  - **Notebook.** An agent can keep a persistent notebook of what it learns (no credentials, no duplicates, every note with its source). You can read and edit it from the agent's settings.
35
35
  - **Telegram.** Link a bot to an agent: private chats, groups and topics, one shared session across all of them. In groups the agent can listen to everything and speak only when it adds something; people can mute it per thread and bring it back.
36
+ - **Slack.** The same over Socket Mode (no public URL): DMs and channels, the agent answers in a thread opened on the message, scheduled messages land in the channel, `/hive` slash commands.
36
37
  - **Files and images.** People send documents and photos to the agent; the agent sends files back. An optional image model can describe pictures for models that cannot see them.
37
38
  - **Permissions that hold.** *Read-only*, *Edit files* and *Full access* are enforced by the CLI itself in all three providers.
38
39
  - **Changes explorer.** A Git tab per agent: file tree with highlighting, diff viewer, history, commit, pull, push and branches.
@@ -115,14 +116,14 @@ Data lives in `~/.hive-am/hive-am.db` (override with `HIVE_AM_HOME`). The server
115
116
  In *Edit files* the agent cannot touch `.git` or private keys (`*.pem`, `*.key`) and cannot write outside its folder. Details and the
116
117
  few differences between providers are in [docs/06](docs/reference/en/06-providers.md) and [docs/12](docs/reference/en/12-operations-and-troubleshooting.md).
117
118
 
118
- ## Connections (Telegram)
119
+ ## Connections (Telegram and Slack)
119
120
 
120
- 1. Create a bot with @BotFather and copy its token.
121
- 2. In **Connections**, add a Telegram connection, pick the agent that answers, and list the people (and groups) allowed to talk to it.
121
+ 1. Create a bot with @BotFather and copy its token, or create a Slack app with Socket Mode and copy its bot token (`xoxb-`) and app token (`xapp-`) ([manual, chapter 9](docs/manual/en/09-telegram-connections.md#slack)).
122
+ 2. In **Connections**, add a Telegram or Slack connection, pick the agent that answers, and list the people (and groups or channels) allowed to talk to it.
122
123
  3. Optional: set the group mode (`mention` or `open`), an alias, whether files are accepted, and an image model.
123
124
 
124
125
  The agent answers through a `channel_reply` tool: its plain text is never delivered. Only authorized people and groups reach it,
125
- checked on the server. The bot token is stored in `~/.hive-am/hive-am.db` and never returned by the API; see the note about it in
126
+ checked on the server. Tokens are stored in `~/.hive-am/hive-am.db` and never returned by the API; see the note about it in
126
127
  [docs/14](docs/reference/en/14-external-connections.md).
127
128
 
128
129
  ## How it is built
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hive-am",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "A colony of coding agents: orchestrators and subagents over Claude Code, OpenCode and Kiro, with real session resume, a browser UI and Telegram.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -37,3 +37,29 @@ export function mdToTelegramHtml(md) {
37
37
  s = s.replace(/^#{1,6}\s+(.+)$/gm, '<b>$1</b>');
38
38
  return s.replace(/\u0000(\d+)\u0000/g, (_m, i) => stash[Number(i)]);
39
39
  }
40
+ /** Slack's three control characters; everything else goes as it is. */
41
+ export const escapeSlack = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
42
+ /**
43
+ * Markdown → Slack's `mrkdwn`: `*bold*`, `_italic_`, `~strike~`, code as it is, `<url|label>` links, headings as bold lines.
44
+ * Slack's own tokens the agent may write (`<@U123>`, `<#C123>`, `<!here>`, `<https://…|…>`) are kept; any other `&`, `<`, `>` is escaped.
45
+ */
46
+ export function mdToSlackMrkdwn(md) {
47
+ md = emojiPresentation(md);
48
+ const stash = [];
49
+ const keep = (s) => `\u0000${stash.push(s) - 1}\u0000`;
50
+ let s = md.replace(/```[^\n]*\n?([\s\S]*?)```/g, (_m, code) => keep(`\`\`\`\n${escapeSlack(code.replace(/\n$/, ''))}\n\`\`\``));
51
+ s = s.replace(/`([^`\n]+)`/g, (_m, code) => keep(`\`${escapeSlack(code)}\``));
52
+ s = s.replace(/<(?:[@#][A-Z0-9]+(?:\|[^>\n]*)?|![a-z]+(?:\^[A-Z0-9]+)?(?:\|[^>\n]*)?|(?:https?|mailto):[^\s|>]+(?:\|[^>\n]+)?)>/g, (t) => keep(t));
53
+ s = s.replace(/\[([^\]\n]+)\]\(((?:https?:\/\/|mailto:)[^\s)]+)\)/g, (_m, label, url) => keep(`<${url}|${escapeSlack(label).replace(/\|/g, '¦')}>`));
54
+ s = escapeSlack(s);
55
+ s = s.replace(/^#{1,6}\s+(.+)$/gm, (_m, h) => keep(`*${h.replace(/\*\*/g, '').trim()}*`));
56
+ s = s.replace(/(^|[\s(])\*([^*\n]+)\*(?=$|[\s).,!?:;])/gm, '$1_$2_');
57
+ s = s.replace(/\*\*([^*\n]+)\*\*/g, (_m, b) => keep(`*${b}*`)).replace(/__([^_\n]+)__/g, (_m, b) => keep(`*${b}*`));
58
+ s = s.replace(/~~([^~\n]+)~~/g, '~$1~');
59
+ s = s.replace(/^(\s*)[-*]\s+/gm, '$1• ');
60
+ s = s.replace(/^&gt;\s?/gm, '> ');
61
+ // Stashed pieces can hold other stashed pieces (a link inside a heading): resolve until none is left.
62
+ for (let i = 0; i < 3 && s.includes('\u0000'); i++)
63
+ s = s.replace(/\u0000(\d+)\u0000/g, (_m, n) => stash[Number(n)]);
64
+ return s;
65
+ }
@@ -1,3 +1,5 @@
1
1
  import { registerFactory } from './manager.js';
2
+ import { SlackAdapter } from './slack.js';
2
3
  import { TelegramAdapter } from './telegram.js';
3
4
  registerFactory('telegram', (c) => new TelegramAdapter(c));
5
+ registerFactory('slack', (c) => new SlackAdapter(c));
@@ -126,7 +126,7 @@ export async function channelReply(agentId, text) {
126
126
  console.warn(`[connections] blocked a reply of agent ${agentId}: it seemed to contain ${leak}`);
127
127
  throw new ChannelError(SECRET_REFUSAL(leak));
128
128
  }
129
- const { externalId } = await adapter.send(thread.target, text);
129
+ const { externalId } = await adapter.send(origin.target ?? thread.target, text);
130
130
  threadMessages.record(thread.id, 'out', externalId, { name: agents.get(agentId)?.name }, text);
131
131
  origin.replied = true;
132
132
  return { sent: true, parts: 1 };
@@ -148,7 +148,7 @@ export async function channelReact(agentId, emoji) {
148
148
  if (!thread)
149
149
  throw new ChannelError('The thread no longer exists.');
150
150
  try {
151
- await adapter.react(thread.target, origin.messageId, e);
151
+ await adapter.react(origin.target ?? thread.target, origin.messageId, e);
152
152
  }
153
153
  catch (err) {
154
154
  throw new ChannelError(err instanceof Error ? err.message : 'The reaction failed');
@@ -176,7 +176,7 @@ export async function channelSendFile(agentId, path, caption) {
176
176
  console.warn(`[connections] blocked a file of agent ${agentId}: it seemed to contain ${leak}`);
177
177
  throw new ChannelError(SECRET_REFUSAL(leak).replace('the message', `the ${caption && findSecret(caption) ? 'caption' : 'file'}`));
178
178
  }
179
- const { externalId } = await adapter.sendFile(thread.target, file, caption?.trim() || undefined);
179
+ const { externalId } = await adapter.sendFile(origin.target ?? thread.target, file, caption?.trim() || undefined);
180
180
  threadMessages.record(thread.id, 'out', externalId, { name: agent.name }, `${caption?.trim() ? `${caption.trim()} ` : ''}[sent file: ${file.name}]`);
181
181
  origin.replied = true;
182
182
  return { sent: true, name: file.name, kind: file.kind };
@@ -10,6 +10,8 @@ import { connections, threadMessages, threads } from './store.js';
10
10
  const MAX_INPUT = 8000;
11
11
  const DEFAULT_RATE = 20; // messages per user per minute
12
12
  const DEFAULT_CHATTER_PER_MINUTE = 10; // times per minute that chatter nobody aimed at the agent may wake it, per thread
13
+ const DENIED_PER_MINUTE = 5; // access notices per person and chat per minute, so a stranger can't make the bot spam a chat
14
+ const DENIED_CHAT_PER_MINUTE = 20; // access notices per chat per minute, whoever asks
13
15
  /** Sent once when a turn aimed at the agent ended without a reply. */
14
16
  const NUDGE = '[hive:channel] System reminder: your last turn ended without calling `channel_reply`, so the person received nothing. If you had an answer, send it now with `channel_reply` (if the tool says it is unknown, call it again: it was still connecting). If you chose not to answer, say so in one short sentence with `channel_reply`.';
15
17
  import { validEffort } from '../efforts.js';
@@ -34,8 +36,15 @@ const T = {
34
36
  newDone: { es: '🆕 Conversación nueva.', en: '🆕 New conversation.' },
35
37
  muted: { es: '🔇 Me quedo callado en este hilo. Menciónenme o respondan a un mensaje mío para que vuelva a hablar.', en: '🔇 I\'ll stay quiet in this thread. Mention me or reply to one of my messages to bring me back.' },
36
38
  unmuted: { es: '🔊 Listo, vuelvo a participar en este hilo.', en: '🔊 Done, I\'m back in this thread.' },
39
+ help: { es: 'Comandos: /new (o /clear, /reset) empieza una conversación nueva (solo administradores) · /stop detiene el turno en curso · /status estado del agente · /mute y /unmute me callan o me reactivan en este hilo · /id tu id y el de este chat · /help esta ayuda.', en: 'Commands: /new (or /clear, /reset) starts a new conversation (admins only) · /stop stops the turn in progress · /status the agent\'s state · /mute and /unmute silence me or bring me back in this thread · /id your id and this chat\'s · /help this help.' },
40
+ ids: (user, chat) => ({ es: `Tu id: ${user} · este chat: ${chat}`, en: `Your id: ${user} · this chat: ${chat}` }),
37
41
  notAllowedChat: (user, chat) => ({ es: `No tienes acceso. Para usarme aquí, añade tu id (${user}) o este chat (${chat}) en la conexión.`, en: `You don't have access. To use me here, add your id (${user}) or this chat (${chat}) to the connection.` }),
42
+ notAllowedMember: (user, chat, slack) => slack
43
+ ? { es: `Este canal solo admite a personas de su lista. Para usarme aquí, añade tu id (${user}) a los miembros de este canal (${chat}).`, en: `This channel only admits the people on its list. To use me here, add your id (${user}) to this channel's members (${chat}).` }
44
+ : { es: `Este chat solo admite a personas de su lista. Para usarme aquí, añade tu id (${user}) a los miembros de este chat (${chat}).`, en: `This chat only admits the people on its list. To use me here, add your id (${user}) to this chat's members (${chat}).` },
38
45
  };
46
+ /** The same commands on every platform; these are other names for them. */
47
+ const COMMAND_ALIASES = { clear: 'new', reset: 'new' };
39
48
  const say = (c, m) => (c.config.lang === 'en' ? m.en : m.es);
40
49
  const hits = new Map();
41
50
  function tooFast(connId, user, limit) {
@@ -45,8 +54,20 @@ function tooFast(connId, user, limit) {
45
54
  hits.set(key, arr);
46
55
  return arr.length > limit;
47
56
  }
48
- const told = new Set(); // users already told how to get access, so strangers can't make us spam them
49
57
  const chatsOf = (c) => (Array.isArray(c.config.chats) ? c.config.chats : []);
58
+ /** Who may talk: in a DM and in a group with no entry of its own, the connection's people list (with their admin flag); in a listed group, that group's own rule: everyone in it, or only its members (the people list does not open it, and an empty list admits nobody). A member keeps admin only if the people list gives it. The chat is `target.chat`, so a rule covers every topic and thread in it. */
59
+ export function senderOf(c, m) {
60
+ const listed = c.allowed.find((u) => u.id === m.userId);
61
+ if (!m.group)
62
+ return listed;
63
+ const chat = chatsOf(c).find((x) => x.id === m.target.chat);
64
+ if (!chat)
65
+ return listed;
66
+ if (chat.mode !== 'members')
67
+ return listed ?? { id: m.userId, name: m.userName };
68
+ const member = (Array.isArray(chat.members) ? chat.members : []).find((x) => x.id === m.userId);
69
+ return member ? { id: m.userId, name: member.name ?? m.userName, ...(listed?.admin ? { admin: true } : {}) } : undefined;
70
+ }
50
71
  const aliasesOf = (c) => (Array.isArray(c.config.aliases) ? c.config.aliases : []).map((a) => String(a).trim()).filter(Boolean);
51
72
  /** Group chatter that speaks TO the agent by one of its aliases counts as aimed at it, even without @mention. */
52
73
  const selfNames = (c) => { const n = (c.agent_id ? agents.get(c.agent_id)?.name : '') ?? ''; return n ? [n, n.split(/\s+/)[0]] : []; };
@@ -72,16 +93,16 @@ export async function handleInbound(connectionId, adapter, m) {
72
93
  // Group chatter is only heard when the connection is open to it.
73
94
  if (group && !addressed && conn.config.group_mode !== 'open')
74
95
  return;
75
- const user = conn.allowed.find((u) => u.id === m.userId)
76
- ?? (group && chatsOf(conn).some((c) => c.id === m.target.chat) ? { id: m.userId, name: m.userName } : undefined);
96
+ const user = senderOf(conn, m);
77
97
  if (!user) {
78
98
  if (!addressed)
79
99
  return; // never answer strangers' chatter
80
- const key = `${connectionId}:${m.userId}`;
81
- if (!told.has(key)) {
82
- told.add(key);
83
- await reply(say(conn, group ? T.notAllowedChat(m.userId, m.target.chat) : T.notAllowed(m.userId)));
100
+ // Told at every message, so the bot never looks dead; capped per person and per chat (topics and threads share it) so nobody can make it flood a chat.
101
+ if (!allow(`denied:${connectionId}:${m.userId}:${m.target.chat}`, DENIED_PER_MINUTE) || !allow(`denied-chat:${connectionId}:${m.target.chat}`, DENIED_CHAT_PER_MINUTE)) {
102
+ return;
84
103
  }
104
+ const restricted = group && chatsOf(conn).some((c) => c.id === m.target.chat && c.mode === 'members');
105
+ await reply(say(conn, restricted ? T.notAllowedMember(m.userId, m.target.chat, conn.kind === 'slack') : group ? T.notAllowedChat(m.userId, m.target.chat) : T.notAllowed(m.userId)));
85
106
  return;
86
107
  }
87
108
  const thread = threads.touch(connectionId, m.externalKey, { title: m.place, target: m.target, user: m.userName });
@@ -117,7 +138,16 @@ export async function handleInbound(connectionId, adapter, m) {
117
138
  } // only files, and none could be saved: nothing to hand over
118
139
  }
119
140
  if (m.command) {
120
- const { name, args } = m.command;
141
+ const { args } = m.command;
142
+ const name = COMMAND_ALIASES[m.command.name] ?? m.command.name;
143
+ if (name === 'help') {
144
+ await reply(say(conn, T.help));
145
+ return;
146
+ }
147
+ if (name === 'id') {
148
+ await reply(say(conn, T.ids(m.userId, m.target.chat)));
149
+ return;
150
+ }
121
151
  if (name === 'stop') {
122
152
  await reply(say(conn, stopAgent(agent.id) ? T.stopped : T.nothingToStop));
123
153
  return;
@@ -190,7 +220,7 @@ async function deliver(conn, adapter, thread, m, msgId, addressed, scheduled = f
190
220
  return;
191
221
  const reply = (text) => adapter.send(m.target, text).catch(() => undefined);
192
222
  const fresh = threads.get(thread.id) ?? thread;
193
- if (m.text.length > MAX_INPUT) {
223
+ if (!scheduled && m.text.length > MAX_INPUT) {
194
224
  if (addressed)
195
225
  await reply(say(conn, T.tooLong));
196
226
  return;
@@ -205,7 +235,7 @@ async function deliver(conn, adapter, thread, m, msgId, addressed, scheduled = f
205
235
  await reply(say(conn, T.queued(ahead)));
206
236
  const unseen = threadMessages.unseen(thread.id, fresh.seen_id, msgId).map((r) => ({ name: r.user_name ?? r.user_id ?? '?', text: r.text, files: r.files ? parseJson(r.files, undefined, `thread message ${r.id} files`) : undefined }));
207
237
  threads.markSeen(thread.id, msgId);
208
- const origin = { connectionId: conn.id, threadId: thread.id, externalKey: m.externalKey, platform: conn.kind, place: m.place, userName: m.userName, userId: m.userId, group: !!m.group, mention: m.mention, addressed, replied: false, messageId: m.externalId, effort: validEffort(conn.config.effort) ? conn.config.effort : undefined };
238
+ const origin = { connectionId: conn.id, threadId: thread.id, externalKey: m.externalKey, platform: conn.kind, place: m.place, userName: m.userName, userId: m.userId, group: !!m.group, mention: m.mention, addressed, replied: false, messageId: m.externalId, target: m.target, effort: validEffort(conn.config.effort) ? conn.config.effort : undefined };
209
239
  if (addressed)
210
240
  await adapter.busy(m.target, 'working', m.externalId).catch(() => undefined);
211
241
  const startedAt = Date.now();
@@ -248,10 +278,10 @@ async function deliver(conn, adapter, thread, m, msgId, addressed, scheduled = f
248
278
  await adapter.busy(m.target, 'done', m.externalId).catch(() => undefined);
249
279
  }
250
280
  }
251
- /** A message the agent asked to receive later (a wake-up, a schedule): it reaches the agent like a message of that thread, so it can answer there. */
281
+ /** A message the agent asked to receive later (a wake-up, a schedule): it reaches the agent like a message of that thread, so it can answer there (on Slack, in the channel: see `ChannelAdapter.proactive`). */
252
282
  export async function deliverScheduled(conn, adapter, thread, text, mention) {
253
283
  const m = {
254
- externalId: `sched-${Date.now()}`, externalKey: thread.external_key, userId: '', userName: 'hive-am (scheduled)', place: thread.title || 'chat', target: thread.target, addressed: true, text, mention,
284
+ externalId: `sched-${Date.now()}`, externalKey: thread.external_key, userId: '', userName: 'hive-am (scheduled)', place: thread.title || 'chat', target: adapter.proactive?.(thread.target) ?? thread.target, addressed: true, text, mention,
255
285
  };
256
286
  await deliver(conn, adapter, thread, m, threadMessages.lastId(thread.id), true, true);
257
287
  }
@@ -11,7 +11,7 @@ const SHAPES = [
11
11
  [/\b\d{8,10}:[A-Za-z0-9_-]{30,}\b/, 'a Telegram bot token'],
12
12
  [/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/, 'an AWS access key'],
13
13
  [/\bgh[pousr]_[A-Za-z0-9]{30,}\b|\bgithub_pat_[A-Za-z0-9_]{30,}\b/, 'a GitHub token'],
14
- [/\bxox[abprs]-[A-Za-z0-9-]{10,}\b/, 'a Slack token'],
14
+ [/\bxox[abprs]-[A-Za-z0-9-]{10,}\b|\bxapp-\d+-[A-Za-z0-9-]{10,}\b/, 'a Slack token'],
15
15
  [/\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b/, 'a Stripe key'],
16
16
  [/\bsk-[A-Za-z0-9_-]{32,}\b/, 'an API key'],
17
17
  [/\bAIza[0-9A-Za-z_-]{35}\b/, 'a Google API key'],