claude-threads 1.26.0 → 1.28.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 +26 -0
- package/dist/index.js +63572 -62742
- package/dist/mcp/mcp-server.js +313 -195
- package/docs/CONFIGURATION.md +105 -0
- package/package.json +5 -5
package/docs/CONFIGURATION.md
CHANGED
|
@@ -129,6 +129,11 @@ stickyMessage:
|
|
|
129
129
|
| `outboundFiles` | No | `send_file` settings: `{ enabled, maxBytes }` (defaults: enabled `true`, `maxBytes` 100 MB) |
|
|
130
130
|
| `sessionHeader` | No | Per-thread header visibility: `full` (default) / `minimal` (status bar only) / `hidden` (no header post) |
|
|
131
131
|
| `stickyMessage` | No | Channel sticky visibility: `full` (default) / `minimal` (status bar only) / `hidden` (no sticky, no bumping) |
|
|
132
|
+
| `directChannelMode` | No | Direct channel mode: the whole channel is one session, and the bot replies with top-level channel posts instead of thread replies. `true` for defaults, or an options object (`respondTo`). See [Direct Channel Mode](#direct-channel-mode). |
|
|
133
|
+
| `approvals` | No | Who may answer tool-permission prompts and other reaction gates: `owner` (session participants) or `all_users` (everyone on `allowedUsers`). Unset keeps the historical default per mode — `all_users` for thread sessions, `owner` for direct channel mode. See [Approvals](#approvals). |
|
|
134
|
+
| `ackReaction` | No | Read receipt: react to every accepted message (session start, follow-up, resume) the instant it is accepted, before Claude produces output. `true` uses 👀 (`eyes`), a string names a custom emoji. Persistent, unlike the typing indicator — useful in busy channels and for messages queued behind an in-flight session start. The receipt means *accepted*, not *delivered*: a later failure (capacity limit, Claude not coming up) is still reported by its own post. `!commands` are not acked — they have their own immediate feedback, and neither are messages accepted through the message-approval flow (an authorized user approving a non-participant's message) — there the approval reaction is already the visible signal. Note: in direct channel mode this is one reaction API call per accepted message. Default off. |
|
|
135
|
+
| `auditLog` | No | Append-only audit trail of what the bot executed for this platform — tool calls (incl. subagents), session lifecycle, security-relevant commands, plan approvals. One JSONL stream per platform under `~/.claude-threads/audit/` (override: `CLAUDE_THREADS_AUDIT_DIR`), files `0600`. The bot never deletes it — rotation/retention is the operator's job (logrotate, SIEM ingestion). See [Audit log](#audit-log). Default off. |
|
|
136
|
+
| `directMessages` | No | Mattermost only: DM auto-discovery. A direct message from a user on `allowedUsers` spawns a derived direct-channel-mode instance for that DM conversation — no per-DM entry needed. See [DM auto-discovery](#dm-auto-discovery). |
|
|
132
137
|
|
|
133
138
|
### Slack
|
|
134
139
|
|
|
@@ -147,6 +152,106 @@ stickyMessage:
|
|
|
147
152
|
| `outboundFiles` | No | `send_file` settings: `{ enabled, maxBytes }` (defaults: enabled `true`, `maxBytes` 100 MB) |
|
|
148
153
|
| `sessionHeader` | No | Per-thread header visibility: `full` (default) / `minimal` (status bar only) / `hidden` (no header post) |
|
|
149
154
|
| `stickyMessage` | No | Channel sticky visibility: `full` (default) / `minimal` (status bar only) / `hidden` (no sticky, no bumping) |
|
|
155
|
+
| `directChannelMode` | No | Direct channel mode: the whole channel is one session, and the bot replies with top-level channel posts instead of thread replies. `true` for defaults, or an options object (`respondTo`). See [Direct Channel Mode](#direct-channel-mode). |
|
|
156
|
+
| `approvals` | No | Who may answer tool-permission prompts and other reaction gates: `owner` (session participants) or `all_users` (everyone on `allowedUsers`). Unset keeps the historical default per mode — `all_users` for thread sessions, `owner` for direct channel mode. See [Approvals](#approvals). |
|
|
157
|
+
| `ackReaction` | No | Read receipt: react to every accepted message (session start, follow-up, resume) the instant it is accepted, before Claude produces output. `true` uses 👀 (`eyes`), a string names a custom emoji. Persistent, unlike the typing indicator — useful in busy channels and for messages queued behind an in-flight session start. The receipt means *accepted*, not *delivered*: a later failure (capacity limit, Claude not coming up) is still reported by its own post. `!commands` are not acked — they have their own immediate feedback, and neither are messages accepted through the message-approval flow (an authorized user approving a non-participant's message) — there the approval reaction is already the visible signal. Note: in direct channel mode this is one reaction API call per accepted message. Default off. |
|
|
158
|
+
| `auditLog` | No | Append-only audit trail of what the bot executed for this platform — tool calls (incl. subagents), session lifecycle, security-relevant commands, plan approvals. One JSONL stream per platform under `~/.claude-threads/audit/` (override: `CLAUDE_THREADS_AUDIT_DIR`), files `0600`. The bot never deletes it — rotation/retention is the operator's job (logrotate, SIEM ingestion). See [Audit log](#audit-log). Default off. |
|
|
159
|
+
|
|
160
|
+
### Direct Channel Mode
|
|
161
|
+
|
|
162
|
+
`directChannelMode: true` turns the configured channel into a single, always-on conversation with the bot:
|
|
163
|
+
|
|
164
|
+
- Every message in the channel reaches the bot — no `@mention` required (messages starting with `@someone-else` are still treated as side conversations and ignored).
|
|
165
|
+
- The bot replies with **top-level channel posts** instead of thread replies, so the channel reads like a plain chat.
|
|
166
|
+
- Only **one session** exists per platform instance; internally it is keyed by the synthetic thread id `dcm:<platform id>`, so persistence, resume after bot restarts, emoji permission prompts, and `!commands` all work exactly as in thread sessions.
|
|
167
|
+
- Messages posted inside any thread of the channel are routed to the same session.
|
|
168
|
+
|
|
169
|
+
This is the mode to use for a dedicated channel with the bot (see issue #315). For shared channels where multiple parallel sessions are wanted, keep the default thread-per-session behavior.
|
|
170
|
+
|
|
171
|
+
The long form configures how the shared channel behaves:
|
|
172
|
+
|
|
173
|
+
```yaml
|
|
174
|
+
directChannelMode:
|
|
175
|
+
respondTo: all_messages # or: mention
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
| Option | Values | Default | Meaning |
|
|
179
|
+
|--------|--------|---------|---------|
|
|
180
|
+
| `respondTo` | `all_messages` / `mention` | `all_messages` | `all_messages`: every message from an allowed user reaches the bot. `mention`: the bot only reacts to messages that @mention it — useful when several people discuss in the channel and the bot should not join every exchange. Backed by the per-session quiet-mode flag, so `!mentions` toggles it at runtime. |
|
|
181
|
+
|
|
182
|
+
Who may approve tool use in the channel is controlled by the platform-level [`approvals`](#approvals) option (DCM defaults to `owner`).
|
|
183
|
+
|
|
184
|
+
### Approvals
|
|
185
|
+
|
|
186
|
+
The platform-level `approvals` option controls who may answer tool-permission prompts (👍/✅/👎) and the other reaction gates — plan approvals, question answers, and session resume:
|
|
187
|
+
|
|
188
|
+
- `owner` — the session participants: the starter plus explicitly `!invite`d users.
|
|
189
|
+
- `all_users` — everyone on the platform's `allowedUsers` list.
|
|
190
|
+
|
|
191
|
+
Unset keeps the historical default per mode, so existing setups are unaffected: thread sessions behave as before (`all_users`), direct channel mode defaults to the safer `owner`. Setting the option applies it to every session of that platform entry — including classic thread sessions, where `approvals: owner` is an opt-in hardening.
|
|
192
|
+
|
|
193
|
+
Under effective `owner` mode the scoping is enforced consistently across every path, so the boundary cannot be talked around: the text alternatives (`!approve`, message-based resume) apply the same participant check as their reaction counterparts, and the owner-gated session commands (`!invite`, `!kick`, `!cd`, `!permissions`, …) additionally require the caller to be a session participant — a platform-allowlisted non-participant can neither approve directly nor `!invite` themselves into the approval set.
|
|
194
|
+
|
|
195
|
+
The approval set is fixed when the Claude CLI is spawned; a later `!invite` extends message access immediately but reaches the approval set on the next CLI respawn (e.g. via `!cd` or `!permissions`).
|
|
196
|
+
|
|
197
|
+
### Audit log
|
|
198
|
+
|
|
199
|
+
`auditLog: true` writes an append-only JSONL stream per platform to `~/.claude-threads/audit/<platformId>.jsonl` (override the directory with `CLAUDE_THREADS_AUDIT_DIR`). One line per event:
|
|
200
|
+
|
|
201
|
+
- `tool_use` — every tool call Claude **issued** (including `server_tool_use` blocks), with the audit-relevant detail (Bash command line, file path, search pattern); subagent sidechain calls are included and marked `subagent: true`. Note the semantics: the audit records the *request* at the moment Claude emits it — an interactive permission denial can still stop the execution, and the denied attempt is exactly what an auditor wants to see.
|
|
202
|
+
- `session_start` / `session_resume` / `session_end` — lifecycle with the triggering user.
|
|
203
|
+
- `command` — security-relevant `!commands` with actor: `!cd`, `!invite`, `!kick`, `!permissions`, `!stop` (active and paused sessions), `!kill`, `!memory forget`, `!routines` management, routine creation, `!worktree remove`, `!plugin install`/`uninstall`.
|
|
204
|
+
- `plan_approval` — plan approved/denied, by whom, via reaction or `!approve`.
|
|
205
|
+
|
|
206
|
+
Notes for operators:
|
|
207
|
+
|
|
208
|
+
- **The bot never deletes audit files.** An audit trail that expires itself is not one — rotation and retention are yours (logrotate, or let your SIEM's file collector ingest and rotate). The writer holds the file descriptor open across writes, so use **`copytruncate`** (or restart the bot after rotating): a rename-based rotate never errors the cached fd, and the bot would keep appending to the rotated file until restart.
|
|
209
|
+
- **Entries contain command lines verbatim** (that is the point); files are `0600` in a `0700` directory — enforced on every start, including pre-existing files, and the writer refuses symlinked audit paths. Treat the directory with the same care as the thread logs.
|
|
210
|
+
- **Actor attribution is best effort**: the username whose (authorized) message triggered the current turn — resumes are attributed to the resuming user — falling back to the session starter. In fast multi-user threads a tool call can be attributed to the previous sender.
|
|
211
|
+
- **Tool-permission decisions (allow/deny of individual tool calls) are not recorded** — they are resolved inside the MCP permission server subprocess, which the bot process does not observe. Plan approvals and the audited commands cover the decisions that flow through the bot itself.
|
|
212
|
+
|
|
213
|
+
### Direct messages (DM)
|
|
214
|
+
|
|
215
|
+
**Mattermost only.** A Mattermost DM is just a private channel with its own id, so a bot DM conversation is direct channel mode pointed at that id — no separate feature needed. (This recipe does NOT work on Slack: Socket Mode distributes event envelopes across an app's active connections, so a second platform entry sharing the same app credentials can consume and discard events meant for the other entry. Slack DM support needs a single-connection, channel-aware implementation.)
|
|
216
|
+
|
|
217
|
+
```yaml
|
|
218
|
+
platforms:
|
|
219
|
+
- id: mattermost-dm
|
|
220
|
+
type: mattermost
|
|
221
|
+
url: https://chat.example.com
|
|
222
|
+
token: your-bot-token # same bot token as the main entry
|
|
223
|
+
channelId: <dm-channel-id>
|
|
224
|
+
botName: claude-code
|
|
225
|
+
directChannelMode: true
|
|
226
|
+
stickyMessage: hidden # a sticky makes little sense in a DM
|
|
227
|
+
allowedUsers: [you]
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Get the DM channel id with one API call: `POST /api/v4/channels/direct` with `["<bot-user-id>", "<your-user-id>"]` — the returned `id` is stable.
|
|
231
|
+
|
|
232
|
+
#### DM auto-discovery
|
|
233
|
+
|
|
234
|
+
Maintaining one static entry per DM conversation does not scale to a team. With `directMessages: true` on a Mattermost entry, anyone on that entry's `allowedUsers` can simply DM the bot "out of the cold":
|
|
235
|
+
|
|
236
|
+
```yaml
|
|
237
|
+
platforms:
|
|
238
|
+
- id: mattermost-main
|
|
239
|
+
type: mattermost
|
|
240
|
+
# ... regular entry ...
|
|
241
|
+
directMessages: true
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
On first contact the bot spawns a derived platform instance for that DM channel — a clone of the parent entry in direct channel mode, sticky hidden, `allowedUsers` scoped to the DM partner (which, with the DCM `approvals` default of `owner`, also scopes tool-permission prompts to that person). DM sessions persist and are reconstructed after a bot restart. Users not on the parent's `allowedUsers` are ignored.
|
|
245
|
+
|
|
246
|
+
Details and caveats:
|
|
247
|
+
|
|
248
|
+
- **Lifecycle**: when a DM session leaves the registry its derived instance and connection are torn down (after a short grace period). After an **idle timeout** the session is persisted — the next DM re-discovers the channel and resumes the conversation. After **`!stop`** the session is deliberately unpersisted — the next DM starts fresh. Instances that never produce a session are reaped after a TTL, so instances do not accumulate over uptime.
|
|
249
|
+
- **Multiple entries, one bot account**: the first entry to discover a DM channel owns it — other `directMessages: true` entries stay out, so the bot never double-replies.
|
|
250
|
+
- **Empty `allowedUsers`**: consistent with the rest of the bot, an empty list means *everyone* — combined with `directMessages: true` that is every user on the server who can DM the bot. Leave it empty only on servers you trust.
|
|
251
|
+
- **Renaming a platform entry** strands its persisted DM sessions (as it does any persisted session referencing the old id); they are skipped with a warning.
|
|
252
|
+
- Mattermost only — see the note above for why the multi-connection approach cannot work on Slack.
|
|
253
|
+
|
|
254
|
+
Limitations: the thread-context prompt ("include previous messages?") is skipped — there is no thread history to offer — and the `list_thread` MCP tool cannot resolve the synthetic session id (use `read_channel_history` instead).
|
|
150
255
|
|
|
151
256
|
### Permission Modes
|
|
152
257
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-threads",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.28.0",
|
|
4
4
|
"description": "Run Claude Code from Slack or Mattermost. Sessions stream live into threads where your whole team can watch and steer.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
"dev": "bun --watch src/index.ts",
|
|
14
14
|
"build": "bun build src/index.ts --outdir dist --target node && bun build src/mcp/mcp-server.ts --outdir dist/mcp --target node && bun build src/statusline/writer.ts --outdir dist/statusline --target node",
|
|
15
15
|
"start": "bun dist/index.js",
|
|
16
|
-
"test": "bun test src/",
|
|
17
|
-
"test:watch": "bun test src/ --watch",
|
|
18
|
-
"test:coverage": "bun test src/ --coverage",
|
|
16
|
+
"test": "bun test src/ --timeout 15000",
|
|
17
|
+
"test:watch": "bun test src/ --watch --timeout 15000",
|
|
18
|
+
"test:coverage": "bun test src/ --coverage --timeout 15000",
|
|
19
19
|
"test:integration:setup": "docker compose -f tests/integration/docker/docker-compose.yml up -d && bun run tests/integration/setup/wait-for-mattermost.ts && bun run tests/integration/setup/setup-mattermost.ts",
|
|
20
20
|
"test:integration:run": "INTEGRATION_TEST=1 bun test tests/integration/suites --timeout 120000",
|
|
21
21
|
"test:integration": "bun run test:integration:setup && bun run test:integration:run",
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
},
|
|
112
112
|
"lint-staged": {
|
|
113
113
|
"*.ts": "eslint --fix",
|
|
114
|
-
"*.test.ts": "bun test --bail",
|
|
114
|
+
"*.test.ts": "bun test --bail --timeout 15000",
|
|
115
115
|
"*.{ts,tsx}": "bash -c 'tsc --noEmit'",
|
|
116
116
|
"website/**/*.{html,css,js}": "prettier --write"
|
|
117
117
|
},
|