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.
- package/CHANGELOG.md +32 -1
- package/README.md +5 -4
- package/package.json +1 -1
- package/server/dist/connections/format.js +26 -0
- package/server/dist/connections/index.js +2 -0
- package/server/dist/connections/manager.js +3 -3
- package/server/dist/connections/router.js +42 -12
- package/server/dist/connections/secrets.js +1 -1
- package/server/dist/connections/slack.js +488 -0
- package/server/dist/connections/store.js +2 -0
- package/server/dist/db/migrations.js +12 -0
- package/server/dist/db.js +15 -3
- package/server/dist/dispatches.js +126 -0
- package/server/dist/http/routes/orchestration.js +13 -2
- package/server/dist/http/validate.js +22 -2
- package/server/dist/index.js +2 -0
- package/server/dist/instructions.js +1 -1
- package/server/dist/retention.js +4 -2
- package/server/dist/runtime.js +17 -6
- package/server/dist/services/connections.js +18 -3
- package/server/dist/skills/defaults.js +6 -4
- package/server/dist/watch.js +1 -1
- package/server/mcp/dispatch.mjs +48 -5
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/app-build-manifest.json +91 -75
- package/web/.next/app-path-routes-manifest.json +5 -5
- package/web/.next/build-manifest.json +2 -2
- package/web/.next/prerender-manifest.json +61 -61
- package/web/.next/server/app/_not-found/page.js +2 -2
- package/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/_not-found.html +1 -1
- package/web/.next/server/app/_not-found.rsc +3 -3
- package/web/.next/server/app/agents/[id]/page.js +4 -4
- package/web/.next/server/app/agents/[id]/page.js.nft.json +1 -1
- package/web/.next/server/app/agents/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/agents/page.js +2 -2
- package/web/.next/server/app/agents/page.js.nft.json +1 -1
- package/web/.next/server/app/agents/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/agents.html +1 -1
- package/web/.next/server/app/agents.rsc +21 -18
- package/web/.next/server/app/chat/page.js +2 -2
- package/web/.next/server/app/chat/page.js.nft.json +1 -1
- package/web/.next/server/app/chat/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/chat.html +1 -1
- package/web/.next/server/app/chat.rsc +4 -4
- package/web/.next/server/app/connections/page.js +2 -2
- package/web/.next/server/app/connections/page.js.nft.json +1 -1
- package/web/.next/server/app/connections/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/connections.html +1 -1
- package/web/.next/server/app/connections.rsc +4 -4
- package/web/.next/server/app/index.html +1 -1
- package/web/.next/server/app/index.rsc +4 -4
- package/web/.next/server/app/page.js +2 -2
- package/web/.next/server/app/page.js.nft.json +1 -1
- package/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/relations/page.js +2 -2
- package/web/.next/server/app/relations/page.js.nft.json +1 -1
- package/web/.next/server/app/relations/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/relations.html +1 -1
- package/web/.next/server/app/relations.rsc +4 -4
- package/web/.next/server/app/resources/[id]/page.js +2 -2
- package/web/.next/server/app/resources/[id]/page.js.nft.json +1 -1
- package/web/.next/server/app/resources/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/resources/page.js +2 -2
- package/web/.next/server/app/resources/page.js.nft.json +1 -1
- package/web/.next/server/app/resources/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/resources.html +1 -1
- package/web/.next/server/app/resources.rsc +4 -4
- package/web/.next/server/app/schedules/page.js +2 -2
- package/web/.next/server/app/schedules/page.js.nft.json +1 -1
- package/web/.next/server/app/schedules/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/schedules.html +1 -1
- package/web/.next/server/app/schedules.rsc +4 -4
- package/web/.next/server/app/sessions/page.js +2 -2
- package/web/.next/server/app/sessions/page.js.nft.json +1 -1
- package/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/sessions.html +1 -1
- package/web/.next/server/app/sessions.rsc +4 -4
- package/web/.next/server/app/skills/page.js +2 -2
- package/web/.next/server/app/skills/page.js.nft.json +1 -1
- package/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/skills.html +1 -1
- package/web/.next/server/app/skills.rsc +4 -4
- package/web/.next/server/app/types/page.js +2 -2
- package/web/.next/server/app/types/page.js.nft.json +1 -1
- package/web/.next/server/app/types/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/types.html +1 -1
- package/web/.next/server/app/types.rsc +4 -4
- package/web/.next/server/app-paths-manifest.json +5 -5
- package/web/.next/server/chunks/{505.js → 111.js} +1 -1
- package/web/.next/server/chunks/143.js +1 -1
- package/web/.next/server/chunks/716.js +2 -2
- package/web/.next/server/chunks/741.js +2 -0
- package/web/.next/server/chunks/837.js +1 -0
- package/web/.next/server/chunks/{435.js → 939.js} +3 -3
- package/web/.next/server/chunks/944.js +1 -0
- package/web/.next/server/pages/404.html +1 -1
- package/web/.next/server/pages/500.html +1 -1
- package/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/static/chunks/162-68ba190086f5ad39.js +1 -0
- package/web/.next/static/chunks/214-35b88db02113ae02.js +1 -0
- package/web/.next/static/chunks/281-a855bf409ef9eebd.js +1 -0
- package/web/.next/static/chunks/350-bdf6b8994a28f2b4.js +1 -0
- package/web/.next/static/chunks/491-196bcb3dd11bb460.js +1 -0
- package/web/.next/static/chunks/517-8f2db464d10a4624.js +1 -0
- package/web/.next/static/chunks/{586-f7017d585a84738d.js → 586-4f67b823df060b46.js} +1 -1
- package/web/.next/static/chunks/{587-5b511f5f93910180.js → 587-3c53d91a1c4f99d6.js} +1 -1
- package/web/.next/static/chunks/642-dc874c8caf203ee1.js +1 -0
- package/web/.next/static/chunks/771-db19da9b1a24820c.js +1 -0
- package/web/.next/static/chunks/928-09d4a58e977641dc.js +1 -0
- package/web/.next/static/chunks/app/agents/[id]/page-9f191f25a366b26b.js +1 -0
- package/web/.next/static/chunks/app/agents/layout-71dd98d696935e29.js +1 -0
- package/web/.next/static/chunks/app/connections/page-b0ea2eae62cef74e.js +1 -0
- package/web/.next/static/chunks/app/layout-cb5af29b0742c513.js +1 -0
- package/web/.next/static/chunks/app/page-5e547e87de23d9fa.js +1 -0
- package/web/.next/static/chunks/app/resources/[id]/page-1e2afddcb1f04884.js +1 -0
- package/web/.next/static/chunks/app/resources/{page-61cccd8fdcc2f6c8.js → page-195b0af83b8ace0d.js} +1 -1
- package/web/.next/static/chunks/app/sessions/{page-85638705fdc18174.js → page-f5a98ec3eaffd7a6.js} +1 -1
- package/web/.next/static/chunks/app/types/page-c4c2b2217c4667e8.js +1 -0
- package/web/.next/static/css/f5438d03b796884e.css +1 -0
- package/web/.next/trace +30 -30
- package/web/.next/types/routes.d.ts +2 -1
- package/web/.next/types/validator.ts +9 -0
- package/web/.next/server/chunks/274.js +0 -1
- package/web/.next/server/chunks/612.js +0 -1
- package/web/.next/static/chunks/107-77dd87cc2ca4f941.js +0 -1
- package/web/.next/static/chunks/281-30e32d932b8b5a75.js +0 -1
- package/web/.next/static/chunks/350-0fe82e622e7a670f.js +0 -1
- package/web/.next/static/chunks/391-18768499e741112e.js +0 -1
- package/web/.next/static/chunks/517-b3b1c9044d2ef23c.js +0 -1
- package/web/.next/static/chunks/656-f3cc78328603241a.js +0 -1
- package/web/.next/static/chunks/705-ce86175dea00e6a9.js +0 -1
- package/web/.next/static/chunks/app/agents/[id]/page-638e12ba3cb813c3.js +0 -1
- package/web/.next/static/chunks/app/connections/page-06bd2d7d9495126a.js +0 -1
- package/web/.next/static/chunks/app/layout-fad968a635f9a4b3.js +0 -1
- package/web/.next/static/chunks/app/page-0062ebc30322b1ae.js +0 -1
- package/web/.next/static/chunks/app/resources/[id]/page-fc494e85b01e8cb7.js +0 -1
- package/web/.next/static/chunks/app/types/page-8e25f503bdcd72ad.js +0 -1
- package/web/.next/static/css/dfcaa19a8d46e60d.css +0 -1
- /package/web/.next/static/{UDA414PFPu9ZROltNmjbX → gqk-RaeJBRWW1cWGRixT2}/_buildManifest.js +0 -0
- /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.
|
|
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.
|
|
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
|
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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(/^>\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
|
+
}
|
|
@@ -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
|
|
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
|
-
|
|
81
|
-
if (!
|
|
82
|
-
|
|
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 {
|
|
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'],
|