claude-slack-channel-bots 0.5.1 → 0.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.
- package/README.md +143 -67
- package/package.json +7 -3
- package/scripts/fixup-bun-cache.ts +125 -0
- package/src/agent-director-client.ts +133 -0
- package/src/agent-director-errors.ts +58 -0
- package/src/agent-director-startup.ts +361 -0
- package/src/agent-director-template.ts +157 -0
- package/src/cli.ts +102 -55
- package/src/config.ts +136 -0
- package/src/permission-action-id.ts +76 -0
- package/src/permission-click-handler.ts +168 -0
- package/src/permission-poller.ts +344 -0
- package/src/postinstall.ts +45 -0
- package/src/registry.ts +5 -50
- package/src/server.ts +152 -560
- package/src/session-manager.ts +880 -293
- package/src/startup-errors.ts +97 -0
- package/hooks/ask-relay.sh +0 -68
- package/hooks/permission-relay.sh +0 -89
- package/src/peer-pid.ts +0 -102
- package/src/sessions.ts +0 -97
- package/src/tmux.ts +0 -191
package/README.md
CHANGED
|
@@ -36,13 +36,25 @@ See the sections below for manual configuration details if you prefer not to use
|
|
|
36
36
|
|
|
37
37
|
## Prerequisites
|
|
38
38
|
|
|
39
|
-
- [Bun](https://bun.sh)
|
|
40
|
-
- [tmux](https://github.com/tmux/tmux) (required for server-managed sessions)
|
|
39
|
+
- [Bun](https://bun.sh) `>= 1.0.21` (agent-director minimum)
|
|
41
40
|
- [Claude Code](https://claude.ai/code) installed and authenticated
|
|
42
|
-
- `
|
|
43
|
-
- `curl` and `jq` on your `PATH` (required for the permission relay hooks)
|
|
41
|
+
- [`agent-director`](https://github.com/gabemahoney/agent-director) **runtime dependency** — pulled in transitively when you install this package. Hard required at server boot: CSCB refuses to start if the library is missing, the host platform is unsupported, Bun is too old, or the installed version is below `MIN_AD_VERSION` (`^0.5.5`). agent-director itself requires [tmux](https://github.com/tmux/tmux) on the operator's PATH; CSCB no longer probes for it directly.
|
|
44
42
|
- Slack workspace admin access (to create and configure the Slack app)
|
|
45
|
-
- **cozempic** (optional) — Python 3.10+ and `pip install cozempic` —
|
|
43
|
+
- **cozempic** (optional) — Python 3.10+ and `pip install cozempic` — used by JSONL path resolution helpers retained for downstream callers.
|
|
44
|
+
|
|
45
|
+
### Supported platforms (inherited from agent-director)
|
|
46
|
+
|
|
47
|
+
| Platform | Status |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `linux-x64` | Supported |
|
|
50
|
+
| `darwin-arm64` (Apple Silicon Mac) | Supported |
|
|
51
|
+
| `linux-arm64` | **Not supported** by agent-director |
|
|
52
|
+
| `darwin-x64` (Intel Mac) | **Not supported** by agent-director |
|
|
53
|
+
| Windows | **Not supported** by agent-director |
|
|
54
|
+
|
|
55
|
+
If the host is unsupported, the SR-5.1 startup gate exits non-zero at server boot with a typed error from agent-director (`ErrPlatformPackageMissing` or `ErrUnsupportedPlatform`) and writes the failure to `~/.claude/channels/slack/startup-errors.log` and stderr. See [Startup errors](#startup-errors) below.
|
|
56
|
+
|
|
57
|
+
> **Note on agent-director versions.** v0.4.1 is a zombie release (the published tarball is missing `dist/` and cannot be imported). v0.4.2 lacks the `MakeTemplateParams.overwrite` field CSCB needs for the boot-time template refresh. v0.5.4 and earlier lack `allow_pending` on `readPane`/`sendKeys`, causing `ErrSpawnNotInteractive` during dev-channels dialog approval on freshly-spawned bots. CSCB pins `^0.5.5` to get past all three.
|
|
46
58
|
|
|
47
59
|
---
|
|
48
60
|
|
|
@@ -121,6 +133,7 @@ A skeleton file is created by postinstall. Populate it before running `start`.
|
|
|
121
133
|
| `message_archive_db` | string | — | Path to a SQLite DB where every inbound Slack message is archived in real time. Parent directories are created if missing; schema is initialized on first open. Compatible with the `archive-messages.py` backfill script — both can write concurrently. Feature is disabled when absent. |
|
|
122
134
|
| `claude_config_dir` | string | — | Path to a Claude on-disk config directory. When set, managed sessions launch with `CLAUDE_CONFIG_DIR='<resolved-path>'` so the bot authenticates against a specific account. `~` is expanded and the path is resolved to absolute. Per-route `routes[id].claude_config_dir` overrides this top-level value for individual channels. When neither is set, Claude's own default applies. Must be non-empty when set. |
|
|
123
135
|
| `resume_enabled` | boolean | `true` | When `false`, the session manager always performs a fresh Claude session launch instead of resuming, both on startup and on runtime auto-restart, even when a stored session ID exists. Disabling this skips the `--resume` flag entirely. Use this as a workaround if your Claude Code version crashes with "sandbox required but unavailable" on `--resume` (a known regression in v2.1.120). |
|
|
136
|
+
| `agent_director_poll_interval_ms` | number | `1000` | Poll interval (ms) for the agent-director permission relay tick. Must be a positive integer in `[200, 3_600_000]`. Replaces the pre-rename `claude_director_poll_interval_ms` — the old name is rejected at startup. Unknown top-level config fields are also rejected to surface stale configs after the rename. |
|
|
124
137
|
|
|
125
138
|
#### Per-route `claude_config_dir` override
|
|
126
139
|
|
|
@@ -217,10 +230,11 @@ Checks prerequisites, then daemonizes the server.
|
|
|
217
230
|
|
|
218
231
|
**Prerequisite checks (in order):**
|
|
219
232
|
|
|
220
|
-
1. `
|
|
221
|
-
2. `
|
|
222
|
-
3. `
|
|
223
|
-
|
|
233
|
+
1. `SLACK_BOT_TOKEN` is set — fails with `missing prerequisite: SLACK_BOT_TOKEN environment variable` if absent.
|
|
234
|
+
2. `SLACK_APP_TOKEN` is set — fails with `missing prerequisite: SLACK_APP_TOKEN environment variable` if absent.
|
|
235
|
+
3. `config.json` exists at `STATE_DIR/config.json` — fails with the full path if not found.
|
|
236
|
+
|
|
237
|
+
Once the server daemonizes, the SR-5.1 startup gate runs inside the child process: it imports `agent-director`, constructs the singleton Client, runs `client.version()`, and verifies `~/.agent-director/state.db` is owned by the current user. Failures land in `startup-errors.log` (see [Startup errors](#startup-errors)). The previous `tmux -V` probe at the CLI level has been removed — agent-director enforces tmux availability at spawn time.
|
|
224
238
|
|
|
225
239
|
If all checks pass, the parent process spawns a detached child process and exits immediately, printing the child PID. The child starts the server and writes its PID to `STATE_DIR/server.pid`. Conversation context is preserved across server restarts when possible.
|
|
226
240
|
|
|
@@ -246,17 +260,27 @@ Gracefully exits all managed Claude Code sessions, then stops and starts the ser
|
|
|
246
260
|
claude-slack-channel-bots clean_restart
|
|
247
261
|
```
|
|
248
262
|
|
|
249
|
-
For each
|
|
263
|
+
For each configured route, calls `client.pause({claude_instance_id})` via agent-director and polls `client.status(...)` until the spawn transitions to `ended` / `missing` (or `client.list(...)` returns no row). If the spawn does not exit within `exit_timeout` seconds (default 120s), the spawn is force-killed via `client.kill(...)`. All routes are processed in parallel. Individual session errors are logged and do not abort the restart. After the server restarts, the SR-1.4 collision-then-act dispatcher decides resume-vs-fresh per route — agent-director owns Claude session-id state, not CSCB.
|
|
250
264
|
|
|
251
265
|
Behavior by case:
|
|
252
266
|
|
|
253
|
-
- **No
|
|
267
|
+
- **No configured routes:** skips the shutdown phase and proceeds directly to stop/start.
|
|
254
268
|
- **Server already stopped:** `stop` reports `server is not running`; `start` then brings up a fresh server.
|
|
255
269
|
|
|
256
270
|
### PID file
|
|
257
271
|
|
|
258
272
|
The PID file is stored at `STATE_DIR/server.pid` (default: `~/.claude/channels/slack/server.pid`). It is written on startup and removed on clean shutdown. A conflict check at startup prevents running two servers against the same state directory.
|
|
259
273
|
|
|
274
|
+
### Installing from a local worktree
|
|
275
|
+
|
|
276
|
+
To install the version of CSCB sitting in your working copy (so the globally-linked `claude-slack-channel-bots` binary runs your local sources), use the helper script rather than `bun install -g .`:
|
|
277
|
+
|
|
278
|
+
```sh
|
|
279
|
+
./scripts/install-local.sh
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
`bun install -g .` (and the equivalent `bun install -g <local-path>`) is broken on Bun 1.3.13 — it inserts an invalid empty-string dependency key into `~/.bun/install/global/package.json` and then any subsequent global op fails with `error: Package "@" has a dependency loop` (upstream: [oven-sh/bun#24207](https://github.com/oven-sh/bun/issues/24207)). The script uses `bun add -g file:<abs-path>` instead, and pre-emptively strips any empty-string entry a prior `bun install -g .` may have already left behind.
|
|
283
|
+
|
|
260
284
|
### Direct invocation for development
|
|
261
285
|
|
|
262
286
|
Skip the CLI and run the server directly with Bun for development or debugging:
|
|
@@ -345,67 +369,23 @@ On success, returns HTTP 200:
|
|
|
345
369
|
|
|
346
370
|
## Permission Relay
|
|
347
371
|
|
|
348
|
-
When Claude Code requires tool approval, the permission relay surfaces an interactive Slack message with **Allow** and **Deny** buttons instead of blocking the TUI.
|
|
349
|
-
|
|
350
|
-
The `ask-relay.sh` hook intercepts `AskUserQuestion` tool calls via `PreToolUse`, posts the question and its options to Slack as interactive buttons, and waits for the user's selection. The answer is returned to Claude Code via `updatedInput` without blocking the TUI.
|
|
372
|
+
When Claude Code requires tool approval, the permission relay surfaces an interactive Slack message with **Allow** and **Deny** buttons instead of blocking the TUI. Architecture is polling-based on the `agent-director` library — there are **no hook scripts to install** and no HTTP long-poll loops.
|
|
351
373
|
|
|
352
|
-
|
|
374
|
+
Flow:
|
|
353
375
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
376
|
+
1. agent-director moves the spawn into `check_permission` state when Claude requests a tool permission.
|
|
377
|
+
2. CSCB's poller (`src/permission-poller.ts`) runs `client.list({ state: ['check_permission'], label: ['service=cscb'] })` at the `agent_director_poll_interval_ms` cadence (default 1000 ms).
|
|
378
|
+
3. For each new spawn, `client.get(...)` returns the open `permission_request` (tool name + tool input + integer `request_id`). CSCB `chat.postMessage`s the Block Kit prompt to the spawn's `channel` label.
|
|
379
|
+
4. The operator clicks Allow / Deny in Slack. CSCB's interactive handler calls `client.decide({ claude_instance_id, decision })` and `chat.update`s the message to "Allowed/Denied by <user>".
|
|
380
|
+
5. If a tracked prompt drops out of `check_permission` for any reason other than a Slack click (timeout, external `decide`, crash), the next poller tick replaces the buttons with "expired".
|
|
358
381
|
|
|
359
382
|
### Slack app prerequisites
|
|
360
383
|
|
|
361
|
-
The Slack app must have **interactivity enabled** with **Socket Mode** as the delivery method.
|
|
362
|
-
|
|
363
|
-
To enable it: open your Slack app config → **Interactivity & Shortcuts** → toggle **Interactivity** on. No Request URL is needed — Socket Mode delivers interaction payloads over the existing socket connection. This is included automatically if you created the app from `slack-app-manifest.yml`.
|
|
364
|
-
|
|
365
|
-
### Hook installation
|
|
366
|
-
|
|
367
|
-
1. Copy the hook scripts from the repo to `~/.claude/hooks/`:
|
|
384
|
+
The Slack app must have **interactivity enabled** with **Socket Mode** as the delivery method. Open your Slack app config → **Interactivity & Shortcuts** → toggle **Interactivity** on. No Request URL is needed; Socket Mode delivers interaction payloads over the existing socket. This is included automatically if you created the app from `slack-app-manifest.yml`.
|
|
368
385
|
|
|
369
|
-
|
|
370
|
-
cp hooks/permission-relay.sh hooks/ask-relay.sh ~/.claude/hooks/
|
|
371
|
-
chmod +x ~/.claude/hooks/permission-relay.sh ~/.claude/hooks/ask-relay.sh
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
Alternatively, symlink them so updates to the repo are reflected automatically:
|
|
375
|
-
|
|
376
|
-
```sh
|
|
377
|
-
ln -sf /path/to/repo/hooks/permission-relay.sh ~/.claude/hooks/permission-relay.sh
|
|
378
|
-
ln -sf /path/to/repo/hooks/ask-relay.sh ~/.claude/hooks/ask-relay.sh
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
2. Ensure `curl` and `jq` are on your `PATH`.
|
|
382
|
-
|
|
383
|
-
3. Add the following to your Claude Code `settings.json`:
|
|
384
|
-
|
|
385
|
-
```jsonc
|
|
386
|
-
"PermissionRequest": [
|
|
387
|
-
{
|
|
388
|
-
"matcher": ".*",
|
|
389
|
-
"timeout": 2000000,
|
|
390
|
-
"hooks": [{ "type": "command", "command": "~/.claude/hooks/permission-relay.sh" }]
|
|
391
|
-
}
|
|
392
|
-
],
|
|
393
|
-
"PreToolUse": [
|
|
394
|
-
{
|
|
395
|
-
"matcher": "AskUserQuestion",
|
|
396
|
-
"timeout": 2000000,
|
|
397
|
-
"hooks": [{ "type": "command", "command": "~/.claude/hooks/ask-relay.sh" }]
|
|
398
|
-
}
|
|
399
|
-
]
|
|
400
|
-
```
|
|
401
|
-
|
|
402
|
-
`permission-relay.sh` relays tool permission requests (Allow/Deny) to Slack via `PermissionRequest`. `ask-relay.sh` relays `AskUserQuestion` calls to Slack via `PreToolUse`, returning the user's selection without blocking the TUI.
|
|
386
|
+
### AskUserQuestion
|
|
403
387
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
### Setup skill
|
|
407
|
-
|
|
408
|
-
The `update-config` skill can automate hook installation. It copies or symlinks the hooks and writes the `settings.json` entries in one step — use it if you prefer not to configure hooks manually.
|
|
388
|
+
The `AskUserQuestion` tool is denied for every CSCB-spawned bot via the agent-director template (`deny: ['AskUserQuestion']`). Bots respond to operator questions via the Slack `reply` MCP tool instead. There is no `ask-relay.sh` hook and no `/ask` HTTP route.
|
|
409
389
|
|
|
410
390
|
---
|
|
411
391
|
|
|
@@ -427,13 +407,13 @@ After inviting the bot to a channel, Slack may not deliver messages until the bo
|
|
|
427
407
|
Messages to channels not listed in `access.json → channels` and not present in `config.json → routes` are silently dropped. Use the `claude-slack-channels-config` skill or edit `access.json` directly to add the channel ID with a `ChannelPolicy` entry.
|
|
428
408
|
|
|
429
409
|
**Permission relay not working**
|
|
430
|
-
Check that the Slack app has interactivity enabled (Interactivity & Shortcuts → toggle on). Verify
|
|
410
|
+
Check that the Slack app has interactivity enabled (Interactivity & Shortcuts → toggle on). Verify the bot is in `check_permission` state via `agent-director list --state check_permission --label service=cscb` (operator CLI). Inspect `server.log` for `permission-poller:` lines — skipped-tick WARNs at 5+ consecutive skips signal that the poll interval is too tight; increase `agent_director_poll_interval_ms` in `config.json`.
|
|
431
411
|
|
|
432
412
|
**Session not restarting after crash**
|
|
433
413
|
After 3 consecutive launch failures for a route, auto-restart is suspended until the server is restarted. Restart the server with `claude-slack-channel-bots stop && claude-slack-channel-bots start`. To disable auto-restart entirely, set `session_restart_delay` to `0` in `config.json`.
|
|
434
414
|
|
|
435
415
|
**Session stuck during clean_restart**
|
|
436
|
-
If a session does not exit within `exit_timeout` seconds (default 120s), `clean_restart` force-kills
|
|
416
|
+
If a session does not exit within `exit_timeout` seconds (default 120s), `clean_restart` force-kills the spawn via `agent-director kill` and proceeds. To manually recover, run `agent-director list --label service=cscb` to find lingering spawns and `agent-director kill <claude_instance_id>` to clear them, then `claude-slack-channel-bots stop && claude-slack-channel-bots start`.
|
|
437
417
|
|
|
438
418
|
**Session crashes on resume with "sandbox required but unavailable"**
|
|
439
419
|
This is a known regression in certain Claude Code releases (e.g. v2.1.120) where `--resume` triggers a sandbox check that fails in headless environments. Set `resume_enabled: false` in `config.json` to disable `--resume` entirely — the bot will always start a fresh Claude session instead of resuming a prior conversation, both on startup and on runtime auto-restart:
|
|
@@ -444,3 +424,99 @@ This is a known regression in certain Claude Code releases (e.g. v2.1.120) where
|
|
|
444
424
|
"resume_enabled": false
|
|
445
425
|
}
|
|
446
426
|
```
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## Startup errors
|
|
431
|
+
|
|
432
|
+
CSCB writes fatal startup errors to `~/.claude/channels/slack/startup-errors.log` (override the directory with `SLACK_STATE_DIR`) in addition to stderr. Each entry is a single timestamped line. The file is append-only and never rotated by CSCB — copy `docs/logrotate-startup-errors.conf` into `/etc/logrotate.d/` if you want host-level rotation.
|
|
433
|
+
|
|
434
|
+
Classes you may see:
|
|
435
|
+
|
|
436
|
+
- `ad-platform-package-missing` — agent-director's platform-native peer dependency is absent. The host is unsupported by agent-director.
|
|
437
|
+
- `ad-unsupported-platform` — agent-director's runtime check rejected the host's `process.platform`/`process.arch` tuple.
|
|
438
|
+
- `ad-bun-version-too-old` — agent-director needs Bun `>= 1.0.21`. Upgrade Bun.
|
|
439
|
+
- `ad-cli-not-executable` — the resolved agent-director CLI binary exists but is not executable. Run `chmod +x` on the binary referenced in the log line.
|
|
440
|
+
- `ad-version-probe` — `agent-director` was loaded but the `version()` probe failed (subprocess invocation, platform binary, etc.).
|
|
441
|
+
- `ad-version-stale` — installed `agent-director` is below the minimum version this CSCB requires. Run `bun add agent-director@^<minimum>`.
|
|
442
|
+
- `ad-call-timeout` — an agent-director verb call exceeded the configured `callTimeoutMs` (default 30 s). Investigate the subprocess or increase the timeout.
|
|
443
|
+
- `ad-same-user` — `~/.agent-director/state.db` is owned by a different UID than the CSCB process. Reinstall agent-director as the correct user or remove the mismatched file.
|
|
444
|
+
- `ad-same-user-stat` — Non-ENOENT stat error on the state DB (permissions, I/O). Investigate the file before re-launching.
|
|
445
|
+
- `ad-template-install` — `client.makeTemplate(...)` rejected the boot-time refresh of the `slack-channel-bot` template. The line includes the agent-director `errName`.
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## Release process
|
|
450
|
+
|
|
451
|
+
CSCB releases are cut with the `/publish` skill from a clean checkout of `main` on a dev box that has `npm login` against the publishing account. The skill bumps the version, packs and smoke-tests the release tarball, commits and tags the release, pushes to GitHub, publishes to npm, polls the registry until the new version is visible, reinstalls the just-published version on the dev box, and prints a final summary.
|
|
452
|
+
|
|
453
|
+
### Invocation
|
|
454
|
+
|
|
455
|
+
```
|
|
456
|
+
/publish <patch|minor|major>
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
The bump kind is **required** — there is no default. The skill exits with a usage line if the argument is missing or not one of `patch`, `minor`, `major`.
|
|
460
|
+
|
|
461
|
+
### Preflight gates
|
|
462
|
+
|
|
463
|
+
Before any side-effecting step runs, `/publish` enforces six fail-fast gates. Any failure aborts before the version is bumped, the tarball is packed, or anything is committed:
|
|
464
|
+
|
|
465
|
+
1. **Clean working tree on `main` in sync with origin/main.** No uncommitted changes; HEAD branch is `main`; `main` is exactly equal to `origin/main` after `git fetch origin`.
|
|
466
|
+
2. **Tests exist and pass.** At least one `*.test.ts` file under `tests/` and `bun test` exits zero.
|
|
467
|
+
3. **Typecheck passes.** `bun run typecheck` exits zero.
|
|
468
|
+
4. **npm authenticated.** `npm whoami` exits zero (run `npm login` first if not).
|
|
469
|
+
5. **Next version not already published.** `npm view claude-slack-channel-bots@<next-version> version` must report nothing.
|
|
470
|
+
6. **`/ci` integration suite passes.** The full Docker-based integration test suite is run via the `/ci` skill and must report PASS. **`/ci` is mandatory and has no opt-out flag** — release without an unbroken integration run is not possible through this skill.
|
|
471
|
+
|
|
472
|
+
### What happens during a release
|
|
473
|
+
|
|
474
|
+
After all gates pass, the skill, in this order:
|
|
475
|
+
|
|
476
|
+
1. Bumps `package.json` and `bun.lock` to `<next-version>` (no commit, no tag yet).
|
|
477
|
+
2. Packs the release tarball with `bun pm pack` and verifies its internal version matches.
|
|
478
|
+
3. Scratch-installs the tarball into a temp `BUN_INSTALL` and runs the bin smoke check (non-zero exit + `Usage:` in stderr). Any failure here rolls back the working tree and aborts — no commit, no push, no publish.
|
|
479
|
+
4. Creates the `Release v<version>` commit and the annotated `v<version>` tag locally.
|
|
480
|
+
5. Pushes the release commit to `origin/main`.
|
|
481
|
+
6. Publishes the smoke-tested tarball with `npm publish <tarball-path>` (the smoke-tested artifact bytes — not a repack from CWD).
|
|
482
|
+
7. Pushes the `v<version>` tag to `origin`, bringing GitHub and npm into agreement.
|
|
483
|
+
8. Polls the npm registry every 5 seconds for up to 60 seconds until the new version is visible.
|
|
484
|
+
9. Sanitizes the bun-1.3.13 empty-string-dependency-key poison from the global `package.json` (see [Installing from a local worktree](#installing-from-a-local-worktree)), removes any pre-existing global install, then runs `bun install -g claude-slack-channel-bots@<version>` — the exact command an end user would run — and verifies the installed bin resolves under `~/.bun/install/global/` at the published version.
|
|
485
|
+
10. Prints a success summary identifying the published version, npm URL, GitHub release tag URL, resolved local install path, and the next-operator-action command.
|
|
486
|
+
|
|
487
|
+
### After the skill exits
|
|
488
|
+
|
|
489
|
+
The dev box now has the freshly-published version installed globally as a real copy, but the running CSCB daemon is still on the prior version. Swap the daemon over:
|
|
490
|
+
|
|
491
|
+
```sh
|
|
492
|
+
claude-slack-channel-bots clean_restart
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
This gracefully exits the managed Claude Code sessions, stops and restarts the server on the new binary, and brings each session back up. See [`clean_restart`](#claude-slack-channel-bots-clean_restart) above for full behavior.
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## Migration
|
|
500
|
+
|
|
501
|
+
For operators upgrading from a pre-`agent-director` install:
|
|
502
|
+
|
|
503
|
+
1. **Install the new CSCB**: `bun remove claude-director` (if present) and `bun install -g claude-slack-channel-bots@^<new>`. The `agent-director` library is pulled in transitively — no separate install step.
|
|
504
|
+
2. **Delete any old relay hooks** (CSCB no longer ships them — agent-director's relay machinery owns the tool-permission flow):
|
|
505
|
+
```sh
|
|
506
|
+
rm -f ~/.claude/hooks/permission-relay.sh ~/.claude/hooks/ask-relay.sh
|
|
507
|
+
```
|
|
508
|
+
Also remove their entries from `~/.claude/settings.json` if you wired them in by hand previously.
|
|
509
|
+
3. **Configure agent-director's `find-missing` sweep**. CSCB does NOT call `client.findMissing(...)`; reconciling stuck rows is the operator's responsibility. Add a cron entry (or systemd timer) that runs `agent-director find-missing` on a cadence that matches your tolerance — e.g. every minute on a busy host:
|
|
510
|
+
```cron
|
|
511
|
+
* * * * * /usr/local/bin/agent-director find-missing --timeout 30s
|
|
512
|
+
```
|
|
513
|
+
4. **Install the startup-errors logrotate snippet** so `~/.claude/channels/slack/startup-errors.log` doesn't grow unboundedly. Edit `USER` in the file to match the OS account running CSCB:
|
|
514
|
+
```sh
|
|
515
|
+
sudo cp docs/logrotate-startup-errors.conf /etc/logrotate.d/claude-slack-channel-bots
|
|
516
|
+
```
|
|
517
|
+
5. **Rename your config field**. Pre-Epic-2 configs used `claude_director_poll_interval_ms`; CSCB now expects `agent_director_poll_interval_ms` and rejects the old name (no silent alias). Edit `~/.claude/channels/slack/config.json` accordingly. Unknown top-level fields are also rejected — clear any other deprecated keys.
|
|
518
|
+
6. **Optional cleanup**: `~/.claude/channels/slack/sessions.json` and `sessions.json.last` are no longer read or written. CSCB ignores them; you can safely `rm` them after a successful boot.
|
|
519
|
+
7. **`tmux` is no longer a CSCB-direct prereq** but is still required transitively via agent-director — keep it installed.
|
|
520
|
+
|
|
521
|
+
After step 1, every CSCB bot is spawned through `client.spawn(...)` with `relay_mode='on'`. The green/red Slack button UX is byte-identical to the pre-migration behavior; the action_id shape changes from `perm_(allow|deny)_<uuid>` to `perm_(allow|deny)_cscb_<channelId>_<request_id>` but this is invisible to end users.
|
|
522
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-slack-channel-bots",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Multi-session Slack-to-Claude bridge — run multiple Claude Code bots across Slack channels via Socket Mode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"src/*.ts",
|
|
11
11
|
"!src/*.test.ts",
|
|
12
12
|
"hooks/",
|
|
13
|
+
"scripts/fixup-bun-cache.ts",
|
|
13
14
|
"slack-app-manifest.yml",
|
|
14
15
|
"README.md",
|
|
15
16
|
"skills/"
|
|
@@ -18,15 +19,18 @@
|
|
|
18
19
|
"bun": ">=1.0.0"
|
|
19
20
|
},
|
|
20
21
|
"scripts": {
|
|
21
|
-
"postinstall": "bun src/postinstall.ts",
|
|
22
|
+
"postinstall": "bun src/postinstall.ts && bun scripts/fixup-bun-cache.ts",
|
|
22
23
|
"start": "bun install --no-summary && bun src/server.ts",
|
|
24
|
+
"pretest": "bun scripts/fixup-bun-cache.ts && bun scripts/check-no-toplevel-mock-module.ts",
|
|
25
|
+
"check:mocks": "bun scripts/check-no-toplevel-mock-module.ts",
|
|
23
26
|
"test": "bun test",
|
|
24
27
|
"typecheck": "tsc --noEmit"
|
|
25
28
|
},
|
|
26
29
|
"dependencies": {
|
|
27
30
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
28
31
|
"@slack/socket-mode": "^2.0.0",
|
|
29
|
-
"@slack/web-api": "^7.0.0"
|
|
32
|
+
"@slack/web-api": "^7.0.0",
|
|
33
|
+
"agent-director": "^0.5.5"
|
|
30
34
|
},
|
|
31
35
|
"devDependencies": {
|
|
32
36
|
"@types/bun": "^1.0.0",
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* fixup-bun-cache.ts — Workaround for bun's tarball-extraction bug.
|
|
4
|
+
*
|
|
5
|
+
* bun (observed in 1.3.13) sometimes drops files during tarball extraction when
|
|
6
|
+
* a directory and a file share a name prefix in the same parent — e.g.
|
|
7
|
+
* node_modules/ajv/dist/core.js (file, dropped)
|
|
8
|
+
* node_modules/ajv/dist/compile/ (sibling directory)
|
|
9
|
+
* or
|
|
10
|
+
* node_modules/zod/v4/core/core.js (file, dropped)
|
|
11
|
+
* node_modules/zod/v4/core/ (parent directory of itself)
|
|
12
|
+
*
|
|
13
|
+
* The cache entry under ~/.bun/install/cache/<name>@<version>@@@N contains the
|
|
14
|
+
* complete tarball contents. Once a broken extraction has been hardlinked into
|
|
15
|
+
* node_modules, subsequent `bun install` invocations reuse the broken state.
|
|
16
|
+
*
|
|
17
|
+
* This script walks every installed package, finds its cache directory, and
|
|
18
|
+
* hardlinks (or copies) any file present in the cache but missing in node_modules.
|
|
19
|
+
* Safe to re-run.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
readdirSync,
|
|
24
|
+
readFileSync,
|
|
25
|
+
existsSync,
|
|
26
|
+
mkdirSync,
|
|
27
|
+
linkSync,
|
|
28
|
+
copyFileSync,
|
|
29
|
+
} from 'fs'
|
|
30
|
+
import { join, dirname } from 'path'
|
|
31
|
+
import { homedir } from 'os'
|
|
32
|
+
|
|
33
|
+
const ROOT = process.cwd()
|
|
34
|
+
const NM = join(ROOT, 'node_modules')
|
|
35
|
+
const CACHE = join(homedir(), '.bun', 'install', 'cache')
|
|
36
|
+
|
|
37
|
+
function* walkPackages(dir: string): Generator<string> {
|
|
38
|
+
if (!existsSync(dir)) return
|
|
39
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
40
|
+
if (!entry.isDirectory()) continue
|
|
41
|
+
if (entry.name === '.bin' || entry.name === '.cache') continue
|
|
42
|
+
const child = join(dir, entry.name)
|
|
43
|
+
if (entry.name.startsWith('@')) {
|
|
44
|
+
for (const inner of readdirSync(child, { withFileTypes: true })) {
|
|
45
|
+
if (inner.isDirectory()) {
|
|
46
|
+
const innerPath = join(child, inner.name)
|
|
47
|
+
yield innerPath
|
|
48
|
+
const nested = join(innerPath, 'node_modules')
|
|
49
|
+
if (existsSync(nested)) yield* walkPackages(nested)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
yield child
|
|
54
|
+
const nested = join(child, 'node_modules')
|
|
55
|
+
if (existsSync(nested)) yield* walkPackages(nested)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readVersion(pkgDir: string): string | null {
|
|
61
|
+
const pj = join(pkgDir, 'package.json')
|
|
62
|
+
if (!existsSync(pj)) return null
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(readFileSync(pj, 'utf-8')).version ?? null
|
|
65
|
+
} catch {
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function* walkFiles(dir: string, base = dir): Generator<string> {
|
|
71
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
72
|
+
const child = join(dir, entry.name)
|
|
73
|
+
if (entry.isDirectory()) yield* walkFiles(child, base)
|
|
74
|
+
else yield child.slice(base.length + 1)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function findCacheDir(scope: string, name: string, version: string): string | null {
|
|
79
|
+
const base = scope ? join(CACHE, `@${scope}`) : CACHE
|
|
80
|
+
const prefix = `${name}@${version}@@@`
|
|
81
|
+
if (!existsSync(base)) return null
|
|
82
|
+
for (const entry of readdirSync(base)) {
|
|
83
|
+
if (entry.startsWith(prefix)) return join(base, entry)
|
|
84
|
+
}
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function pkgIdentity(pkgDir: string): { scope: string; name: string } {
|
|
89
|
+
const tail = pkgDir.split('/node_modules/').pop()!
|
|
90
|
+
const parts = tail.split('/')
|
|
91
|
+
if (parts[0].startsWith('@')) return { scope: parts[0].slice(1), name: parts[1] }
|
|
92
|
+
return { scope: '', name: parts[0] }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let scanned = 0
|
|
96
|
+
let restored = 0
|
|
97
|
+
const restoredPaths: string[] = []
|
|
98
|
+
|
|
99
|
+
for (const pkgDir of walkPackages(NM)) {
|
|
100
|
+
scanned++
|
|
101
|
+
const version = readVersion(pkgDir)
|
|
102
|
+
if (!version) continue
|
|
103
|
+
const { scope, name } = pkgIdentity(pkgDir)
|
|
104
|
+
const cacheDir = findCacheDir(scope, name, version)
|
|
105
|
+
if (!cacheDir) continue
|
|
106
|
+
|
|
107
|
+
for (const relFile of walkFiles(cacheDir)) {
|
|
108
|
+
const installedPath = join(pkgDir, relFile)
|
|
109
|
+
if (existsSync(installedPath)) continue
|
|
110
|
+
const cachePath = join(cacheDir, relFile)
|
|
111
|
+
mkdirSync(dirname(installedPath), { recursive: true })
|
|
112
|
+
try {
|
|
113
|
+
linkSync(cachePath, installedPath)
|
|
114
|
+
} catch {
|
|
115
|
+
copyFileSync(cachePath, installedPath)
|
|
116
|
+
}
|
|
117
|
+
restored++
|
|
118
|
+
restoredPaths.push(installedPath.slice(NM.length + 1))
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (restored > 0) {
|
|
123
|
+
for (const p of restoredPaths) console.log(`fixup-bun-cache: restored ${p}`)
|
|
124
|
+
}
|
|
125
|
+
console.log(`fixup-bun-cache: scanned ${scanned} packages, restored ${restored} file(s)`)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-director-client.ts — Module-level singleton wrapper for the
|
|
3
|
+
* `agent-director` library Client (SR-0.1).
|
|
4
|
+
*
|
|
5
|
+
* Public API:
|
|
6
|
+
* - getClient(): Client — lazy-construct + return the singleton
|
|
7
|
+
* - closeClient(): void — idempotent shutdown for SR-11 Event 11
|
|
8
|
+
* - MIN_AD_VERSION: string — semver string derived from package.json
|
|
9
|
+
* - DEFAULT_STORE_PATH / DEFAULT_TEMPLATE_NAME — paths CSCB pins
|
|
10
|
+
* - resetClientForTests(): void — test-only handle reset
|
|
11
|
+
*
|
|
12
|
+
* MIN_AD_VERSION is read once at module init from this package.json's
|
|
13
|
+
* `dependencies['agent-director']` range, stripping leading semver operators
|
|
14
|
+
* (`^`, `~`, `>=`, etc.) so a single source of truth covers both the install-
|
|
15
|
+
* time dep declaration (SR-5.3) and the runtime version gate (SR-5.1 step 3).
|
|
16
|
+
*
|
|
17
|
+
* Construction uses `{ storePath, createIfMissing: true, logger: console }`
|
|
18
|
+
* per SR-0.1; tilde expansion is the library's responsibility.
|
|
19
|
+
*
|
|
20
|
+
* Concurrency: AD's Client is internally safe for concurrent verb calls
|
|
21
|
+
* (the subprocess-CLI transport serializes its own dispatch) per SR-0.1,
|
|
22
|
+
* so CSCB does NOT add its own mutex.
|
|
23
|
+
*
|
|
24
|
+
* SPDX-License-Identifier: MIT
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { readFileSync } from 'node:fs'
|
|
28
|
+
import { dirname, join } from 'node:path'
|
|
29
|
+
import { fileURLToPath } from 'node:url'
|
|
30
|
+
import { Client } from 'agent-director'
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Constants
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/** Default AD store path; tilde-expanded library-side. */
|
|
37
|
+
export const DEFAULT_STORE_PATH = '~/.agent-director/state.db'
|
|
38
|
+
|
|
39
|
+
/** Name of the CSCB-shipped template (SR-3.1). */
|
|
40
|
+
export const DEFAULT_TEMPLATE_NAME = 'slack-channel-bot'
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the package.json that ships with this module, then extract and
|
|
44
|
+
* normalize `dependencies['agent-director']` into a bare semver string.
|
|
45
|
+
*
|
|
46
|
+
* Throws at module init if the dep is missing or shaped wrong — that's a
|
|
47
|
+
* packaging bug worth failing loudly on, not a runtime condition.
|
|
48
|
+
*/
|
|
49
|
+
function readMinAdVersion(): string {
|
|
50
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url))
|
|
51
|
+
// src/agent-director-client.ts → package.json lives one level up
|
|
52
|
+
const pkgPath = join(moduleDir, '..', 'package.json')
|
|
53
|
+
const raw = readFileSync(pkgPath, 'utf-8')
|
|
54
|
+
const pkg = JSON.parse(raw) as { dependencies?: Record<string, string> }
|
|
55
|
+
const range = pkg.dependencies?.['agent-director']
|
|
56
|
+
if (typeof range !== 'string' || range.length === 0) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`agent-director-client: package.json dependencies['agent-director'] is missing or empty — cannot derive MIN_AD_VERSION`,
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
// Strip leading semver operators: ^, ~, >=, >, =, v. Anything left should
|
|
62
|
+
// parse as semver; we don't validate further here — the SR-5.1 version gate
|
|
63
|
+
// does the actual comparison.
|
|
64
|
+
const stripped = range.replace(/^[\^~]|^>=|^>|^=/, '').replace(/^v/, '').trim()
|
|
65
|
+
if (stripped.length === 0) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`agent-director-client: dependencies['agent-director']=${range} reduced to empty string after stripping operators`,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
return stripped
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Minimum agent-director version CSCB requires at runtime (SR-5.3). */
|
|
74
|
+
export const MIN_AD_VERSION: string = readMinAdVersion()
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Singleton state
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
let singleton: Client | null = null
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Return the singleton Client, lazy-constructing it on first call.
|
|
84
|
+
*
|
|
85
|
+
* Construction is synchronous: all platform / Bun / subprocess-resolution
|
|
86
|
+
* errors fire eagerly here per SR-0.1, not at first verb call. Typed `Err*`
|
|
87
|
+
* subclasses propagate; the SR-5.1 startup gate is the only intended
|
|
88
|
+
* caller-of-record that branches them.
|
|
89
|
+
*/
|
|
90
|
+
export function getClient(): Client {
|
|
91
|
+
if (singleton === null) {
|
|
92
|
+
singleton = new Client({
|
|
93
|
+
storePath: DEFAULT_STORE_PATH,
|
|
94
|
+
createIfMissing: true,
|
|
95
|
+
logger: console,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
return singleton
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Release the Client handle if open. Idempotent: a second call is a no-op.
|
|
103
|
+
* `client.close()` itself never throws per the library contract.
|
|
104
|
+
*
|
|
105
|
+
* Called from SR-11 Event 11 (graceful shutdown) inside a try/finally.
|
|
106
|
+
*/
|
|
107
|
+
export function closeClient(): void {
|
|
108
|
+
if (singleton !== null) {
|
|
109
|
+
singleton.close()
|
|
110
|
+
singleton = null
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Test-only helper: drop the cached singleton WITHOUT closing it. Use when
|
|
116
|
+
* tests construct fake Clients via the agent-director-stub and want a clean
|
|
117
|
+
* slate per test. Production code must use closeClient() instead.
|
|
118
|
+
*
|
|
119
|
+
* @internal
|
|
120
|
+
*/
|
|
121
|
+
export function resetClientForTests(): void {
|
|
122
|
+
singleton = null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Test-only helper: install a pre-built Client (typically a stub) as the
|
|
127
|
+
* singleton. Skip the Client construction path entirely.
|
|
128
|
+
*
|
|
129
|
+
* @internal
|
|
130
|
+
*/
|
|
131
|
+
export function setClientForTests(client: Client): void {
|
|
132
|
+
singleton = client
|
|
133
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-director-errors.ts — Re-export of typed Err* subclasses CSCB references.
|
|
3
|
+
*
|
|
4
|
+
* Per SR-0.2 the integration must not parse error strings or exit codes; every
|
|
5
|
+
* call site branches on `instanceof` against the typed classes below. This
|
|
6
|
+
* module gives the rest of CSCB a single import surface and a single place to
|
|
7
|
+
* keep the subset list in sync with SRD edits.
|
|
8
|
+
*
|
|
9
|
+
* Catalog (SR-0.2):
|
|
10
|
+
* - ErrUnsupportedPlatform (Client constructor / platform gate)
|
|
11
|
+
* - ErrBunVersionTooOld (Client constructor / Bun version gate)
|
|
12
|
+
* - ErrPlatformPackageMissing (Client constructor / @agent-director/<plat>)
|
|
13
|
+
* - ErrCliNotExecutable (Client constructor / CLI binary lacks +x)
|
|
14
|
+
* - ErrInstanceIdCollision (spawn / SR-1.4 idempotency)
|
|
15
|
+
* - ErrSpawnNotFound (get / status / decide on missing row)
|
|
16
|
+
* - ErrNoSessionId (resume / SR-1.3 fallthrough)
|
|
17
|
+
* - ErrJsonlMissing (resume / SR-1.3 fallthrough)
|
|
18
|
+
* - ErrSpawnNotResumable (resume / SR-1.3 collision-recovery)
|
|
19
|
+
* - ErrAlreadyDecided (decide / SR-2.2 treated-as-success)
|
|
20
|
+
* - ErrNoOpenPermissionRequest (decide / poller race)
|
|
21
|
+
* - ErrRelayModeOff (spawn / SR-1.2 abort)
|
|
22
|
+
* - ErrRelayModeInvalid (spawn / SR-1.2 abort)
|
|
23
|
+
* - ErrTemplateMalformed (makeTemplate / SR-3.2 fatal)
|
|
24
|
+
* - ErrTemplateExists (makeTemplate; only relevant pre-overwrite)
|
|
25
|
+
* - ErrTemplateNotFound (defensive — not raised by makeTemplate)
|
|
26
|
+
* - ErrTemplateNameUnsafe (makeTemplate / SR-3.2 fatal)
|
|
27
|
+
* - ErrClientClosed (post-close verb call; TS-only)
|
|
28
|
+
* - ErrCallTimeout (any verb / per-call timeout exceeded)
|
|
29
|
+
*
|
|
30
|
+
* ErrPauseTimeout is intentionally omitted (SR-0.2): SR-11 Event 12 owns its
|
|
31
|
+
* own pause timeout via CSCB-side polling and never relies on the library's
|
|
32
|
+
* pause budget, so the class never reaches a CSCB handler.
|
|
33
|
+
*
|
|
34
|
+
* SPDX-License-Identifier: MIT
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
AgentDirectorError,
|
|
39
|
+
ErrClientClosed,
|
|
40
|
+
ErrUnsupportedPlatform,
|
|
41
|
+
ErrPlatformPackageMissing,
|
|
42
|
+
ErrBunVersionTooOld,
|
|
43
|
+
ErrCliNotExecutable,
|
|
44
|
+
ErrCallTimeout,
|
|
45
|
+
ErrInstanceIdCollision,
|
|
46
|
+
ErrSpawnNotFound,
|
|
47
|
+
ErrNoSessionId,
|
|
48
|
+
ErrJsonlMissing,
|
|
49
|
+
ErrSpawnNotResumable,
|
|
50
|
+
ErrAlreadyDecided,
|
|
51
|
+
ErrNoOpenPermissionRequest,
|
|
52
|
+
ErrRelayModeOff,
|
|
53
|
+
ErrRelayModeInvalid,
|
|
54
|
+
ErrTemplateMalformed,
|
|
55
|
+
ErrTemplateExists,
|
|
56
|
+
ErrTemplateNotFound,
|
|
57
|
+
ErrTemplateNameUnsafe,
|
|
58
|
+
} from 'agent-director'
|