claude-slack-channel-bots 0.3.0 → 0.4.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/README.md CHANGED
@@ -42,6 +42,7 @@ See the sections below for manual configuration details if you prefer not to use
42
42
  - `ss` from [iproute2](https://github.com/iproute2/iproute2) on your `PATH` (required for session ID discovery; pre-installed on most Linux distributions)
43
43
  - `curl` and `jq` on your `PATH` (required for the permission relay hooks)
44
44
  - Slack workspace admin access (to create and configure the Slack app)
45
+ - **cozempic** (optional) — Python 3.10+ and `pip install cozempic` — enables session file cleaning before `--resume` for faster load times
45
46
 
46
47
  ---
47
48
 
@@ -92,7 +93,8 @@ A skeleton file is created by postinstall. Populate it before running `start`.
92
93
  "health_check_interval": 120,
93
94
  "exit_timeout": 120,
94
95
  "stop_timeout": 30,
95
- "mcp_config_path": "~/.claude/slack-mcp.json"
96
+ "mcp_config_path": "~/.claude/slack-mcp.json",
97
+ "cozempic_prescription": "standard"
96
98
  }
97
99
  ```
98
100
 
@@ -111,6 +113,7 @@ A skeleton file is created by postinstall. Populate it before running `start`.
111
113
  | `stop_timeout` | number | `30` | Seconds to wait for the server process to exit after `SIGTERM` before escalating to `SIGKILL`. |
112
114
  | `mcp_config_path` | string | `~/.claude/slack-mcp.json` | Path to the MCP config file passed to Claude Code when launching managed sessions. |
113
115
  | `append_system_prompt_file` | string | — | Path to a file appended to every managed session's system prompt via `--append-system-prompt-file`. Missing file silently skipped. See `skills/EXAMPLE_CLAUDE.md` for a template. |
116
+ | `cozempic_prescription` | string | `"standard"` | Cozempic cleaning intensity before resume. Valid values: `gentle`, `standard`, `aggressive`. Has no effect if cozempic is not installed. |
114
117
 
115
118
  ---
116
119
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-slack-channel-bots",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Multi-session Slack-to-Claude bridge \u2014 run multiple Claude Code bots across Slack channels via Socket Mode",
5
5
  "type": "module",
6
6
  "bin": {
package/src/config.ts CHANGED
@@ -17,6 +17,7 @@ import { resolve } from 'path'
17
17
  // ---------------------------------------------------------------------------
18
18
 
19
19
  export const MCP_SERVER_NAME = 'slack-channel-router'
20
+ export const ALLOWED_PRESCRIPTIONS = ['gentle', 'standard', 'aggressive']
20
21
 
21
22
  // ---------------------------------------------------------------------------
22
23
  // Types
@@ -41,6 +42,7 @@ export interface RoutingConfigInput {
41
42
  stop_timeout?: number
42
43
  mcp_config_path?: string
43
44
  append_system_prompt_file?: string
45
+ cozempic_prescription?: string
44
46
  }
45
47
 
46
48
  /** Validated, fully-resolved routing configuration with all defaults applied. */
@@ -56,6 +58,7 @@ export interface RoutingConfig {
56
58
  stop_timeout: number
57
59
  mcp_config_path: string
58
60
  append_system_prompt_file?: string
61
+ cozempic_prescription: string
59
62
  }
60
63
 
61
64
  // ---------------------------------------------------------------------------
@@ -79,6 +82,7 @@ export function applyDefaults(input: RoutingConfigInput): RoutingConfig {
79
82
  stop_timeout: input.stop_timeout ?? 30,
80
83
  mcp_config_path: input.mcp_config_path ?? '~/.claude/slack-mcp.json',
81
84
  append_system_prompt_file: input.append_system_prompt_file,
85
+ cozempic_prescription: input.cozempic_prescription ?? 'standard',
82
86
  }
83
87
  }
84
88
 
@@ -152,6 +156,13 @@ export function validateConfig(config: RoutingConfig): void {
152
156
  )
153
157
  }
154
158
 
159
+ // cozempic_prescription must be one of the allowed values
160
+ if (!ALLOWED_PRESCRIPTIONS.includes(config.cozempic_prescription)) {
161
+ throw new Error(
162
+ `Routing config validation error: cozempic_prescription "${config.cozempic_prescription}" is invalid. Allowed values are: ${ALLOWED_PRESCRIPTIONS.join(', ')}.`,
163
+ )
164
+ }
165
+
155
166
  // default_dm_session must reference an existing route CWD
156
167
  if (config.default_dm_session !== undefined) {
157
168
  if (!seen.has(config.default_dm_session)) {
@@ -0,0 +1,150 @@
1
+ /**
2
+ * cozempic.ts — Session cleaning via the cozempic CLI.
3
+ *
4
+ * Provides PATH availability checking, JSONL path resolution, file size
5
+ * inspection, and async session cleaning using `cozempic treat`.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ */
9
+
10
+ import { spawn } from 'child_process'
11
+ import { statSync } from 'fs'
12
+ import { homedir } from 'os'
13
+ import { ALLOWED_PRESCRIPTIONS } from './config.ts'
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // PATH availability check
17
+ // ---------------------------------------------------------------------------
18
+
19
+ let cozempicAvailable: boolean | null = null
20
+
21
+ /**
22
+ * Checks whether `cozempic` is on PATH by spawning `which cozempic`.
23
+ * Sets the module-scoped flag and logs the result to stderr.
24
+ */
25
+ export async function checkCozempicAvailable(): Promise<void> {
26
+ return new Promise<void>((resolve) => {
27
+ let settled = false
28
+ const done = (available: boolean, msg: string) => {
29
+ if (settled) return
30
+ settled = true
31
+ cozempicAvailable = available
32
+ console.error(msg)
33
+ resolve()
34
+ }
35
+ const proc = spawn('which', ['cozempic'])
36
+ proc.on('error', () => {
37
+ done(false, '[slack] Warning: cozempic not found on PATH — session cleaning disabled')
38
+ })
39
+ proc.on('close', (code) => {
40
+ if (code === 0) {
41
+ done(true, '[slack] cozempic available')
42
+ } else {
43
+ done(false, '[slack] Warning: cozempic not found on PATH — session cleaning disabled')
44
+ }
45
+ })
46
+ })
47
+ }
48
+
49
+ /** Returns true if cozempic was found on PATH during the last availability check. */
50
+ export function getCozempicAvailable(): boolean {
51
+ return cozempicAvailable === true
52
+ }
53
+
54
+ /** Resets the availability flag to null (for testing). */
55
+ export function _resetCozempicAvailable(): void {
56
+ cozempicAvailable = null
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // JSONL path resolution
61
+ // ---------------------------------------------------------------------------
62
+
63
+ /**
64
+ * Builds the absolute JSONL path for a given cwd and session ID.
65
+ * Pure function — no I/O, no validation.
66
+ */
67
+ export function resolveJsonlPath(cwd: string, sessionId: string): string {
68
+ const slug = cwd.replace(/[^a-zA-Z0-9-]/g, '-')
69
+ return `${homedir()}/.claude/projects/${slug}/${sessionId}.jsonl`
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // File size helper
74
+ // ---------------------------------------------------------------------------
75
+
76
+ /**
77
+ * Returns the size of the file at `path` in bytes, or null if the file
78
+ * cannot be stat'd (missing, permission error, etc.).
79
+ */
80
+ export function readFileSizeBytes(path: string): number | null {
81
+ try {
82
+ return statSync(path).size
83
+ } catch {
84
+ return null
85
+ }
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Session cleaning
90
+ // ---------------------------------------------------------------------------
91
+
92
+ export type CleanSessionFn = (sessionId: string, cwd: string, prescription: string) => Promise<void>
93
+
94
+ /**
95
+ * Runs `cozempic treat <sessionId> -rx <prescription> --execute` against the
96
+ * session's JSONL file. Always resolves — never rejects.
97
+ *
98
+ * Skips if the JSONL is missing or empty. Streams cozempic output to stderr
99
+ * with the `[slack] cozempic:` prefix and logs before/after file sizes.
100
+ */
101
+ export async function cleanSession(sessionId: string, cwd: string, prescription: string): Promise<void> {
102
+ if (!ALLOWED_PRESCRIPTIONS.includes(prescription)) {
103
+ console.error(`[slack] cozempic: invalid prescription "${prescription}" — skipping clean session=${sessionId}`)
104
+ return
105
+ }
106
+ const path = resolveJsonlPath(cwd, sessionId)
107
+ const beforeSize = readFileSizeBytes(path)
108
+
109
+ if (beforeSize === null) {
110
+ console.error(`[slack] cozempic: JSONL not found — skipping clean session=${sessionId}`)
111
+ return
112
+ }
113
+
114
+ if (beforeSize === 0) {
115
+ console.error(`[slack] cozempic: JSONL empty — skipping clean session=${sessionId}`)
116
+ return
117
+ }
118
+
119
+ console.error(`[slack] cozempic: cleaning started session=${sessionId} size=${beforeSize}`)
120
+
121
+ return new Promise<void>((resolve) => {
122
+ const proc = spawn('cozempic', ['treat', sessionId, '-rx', prescription, '--execute'])
123
+
124
+ proc.stdout.on('data', (chunk: Buffer) => {
125
+ for (const line of chunk.toString().split('\n')) {
126
+ if (line.length > 0) console.error(`[slack] cozempic: ${line}`)
127
+ }
128
+ })
129
+
130
+ proc.stderr.on('data', (chunk: Buffer) => {
131
+ for (const line of chunk.toString().split('\n')) {
132
+ if (line.length > 0) console.error(`[slack] cozempic: ${line}`)
133
+ }
134
+ })
135
+
136
+ proc.on('error', (err) => {
137
+ console.error(`[slack] cozempic: spawn error session=${sessionId}: ${err.message}`)
138
+ resolve()
139
+ })
140
+
141
+ proc.on('close', (code) => {
142
+ const afterSize = readFileSizeBytes(path)
143
+ if (code !== 0 && code !== null) {
144
+ console.error(`[slack] cozempic: exit code ${code} session=${sessionId}`)
145
+ }
146
+ console.error(`[slack] cozempic: cleaning done session=${sessionId} size=${afterSize ?? 'unknown'}`)
147
+ resolve()
148
+ })
149
+ })
150
+ }
package/src/server.ts CHANGED
@@ -42,6 +42,7 @@ import { loadConfig, expandTilde, type RoutingConfig, MCP_SERVER_NAME } from './
42
42
  import { readSessions, writeSessions, rotateSessions } from './sessions.ts'
43
43
  import { defaultTmuxClient, sessionName, isClaudeRunning } from './tmux.ts'
44
44
  import { startupSessionManager, launchSession } from './session-manager.ts'
45
+ import { cleanSession, getCozempicAvailable } from './cozempic.ts'
45
46
  import {
46
47
  initRestart,
47
48
  scheduleRestart,
@@ -1423,7 +1424,9 @@ export async function main(): Promise<void> {
1423
1424
  const resolvedSessionId = stored !== 'pending' ? stored : undefined
1424
1425
  const record = await launchSession(
1425
1426
  channelId, cwd, routingConfig, defaultTmuxClient,
1426
- resolvedSessionId !== undefined ? { sessionId: resolvedSessionId } : undefined,
1427
+ resolvedSessionId !== undefined
1428
+ ? { sessionId: resolvedSessionId, cleanSession: getCozempicAvailable() ? cleanSession : undefined }
1429
+ : undefined,
1427
1430
  )
1428
1431
  if (record) {
1429
1432
  const sessions = readSessions()
@@ -10,8 +10,8 @@
10
10
  */
11
11
 
12
12
  import { accessSync, existsSync, constants } from 'fs'
13
- import { homedir } from 'node:os'
14
13
  import { type TmuxClient, sessionName, isClaudeRunning } from './tmux.ts'
14
+ import { type CleanSessionFn, checkCozempicAvailable, getCozempicAvailable, cleanSession as defaultCleanSession, resolveJsonlPath } from './cozempic.ts'
15
15
  import { type SessionsMap, type SessionRecord } from './sessions.ts'
16
16
  import { type RoutingConfig, MCP_SERVER_NAME } from './config.ts'
17
17
 
@@ -26,8 +26,7 @@ import { type RoutingConfig, MCP_SERVER_NAME } from './config.ts'
26
26
  */
27
27
  export function jsonlExistsForSession(cwd: string, sessionId: string): boolean {
28
28
  if (!/^[a-zA-Z0-9_-]+$/.test(sessionId)) return false
29
- const slug = cwd.replace(/[^a-zA-Z0-9-]/g, '-')
30
- const path = `${homedir()}/.claude/projects/${slug}/${sessionId}.jsonl`
29
+ const path = resolveJsonlPath(cwd, sessionId)
31
30
  return existsSync(path)
32
31
  }
33
32
 
@@ -40,6 +39,8 @@ export interface LaunchOptions {
40
39
  pollTimeout?: number
41
40
  /** Claude session UUID to resume. When provided, --resume <id> is appended to the CLI command. */
42
41
  sessionId?: string
42
+ /** Optional session cleaning function to run before --resume launches. */
43
+ cleanSession?: CleanSessionFn
43
44
  }
44
45
 
45
46
  export interface SessionStateResult {
@@ -155,6 +156,11 @@ export async function launchSession(
155
156
  await tmuxClient.newSession(name, cwd)
156
157
  console.error(`[slack] Session created: ${name} (cwd="${cwd}")`)
157
158
 
159
+ // Clean JSONL before resuming (if a cleanSession fn was provided)
160
+ if (resumeSessionId && resumeSessionId !== 'pending' && options?.cleanSession) {
161
+ await options.cleanSession(resumeSessionId, cwd, routingConfig.cozempic_prescription)
162
+ }
163
+
158
164
  // Attempt launch (with --resume if sessionId provided)
159
165
  let result = await attemptLaunch(resumeSessionId, name)
160
166
 
@@ -227,6 +233,8 @@ export async function startupSessionManager(
227
233
 
228
234
  console.error(`[slack] startupSessionManager: storedSessions=${JSON.stringify(storedSessions)}`)
229
235
 
236
+ await checkCozempicAvailable()
237
+
230
238
  const routeEntries = Object.entries(routingConfig.routes)
231
239
  const concurrency = options?.concurrency ?? 3
232
240
  const startupTimeout = options?.startupTimeout ?? 60_000
@@ -294,7 +302,7 @@ export async function startupSessionManager(
294
302
  console.error(`[slack] Dead/missing process with stored session ID — resuming: channel=${channelId} session=${name} sessionId=${storedSessionId}`)
295
303
  const record = await launchSession(
296
304
  channelId, route.cwd, routingConfig, tmuxClient,
297
- { ...launchOpts, sessionId: storedSessionId },
305
+ { ...launchOpts, sessionId: storedSessionId, cleanSession: getCozempicAvailable() ? defaultCleanSession : undefined },
298
306
  )
299
307
  const elapsed = Date.now() - routeStart
300
308
  if (record !== null) {