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 +4 -1
- package/package.json +1 -1
- package/src/config.ts +11 -0
- package/src/cozempic.ts +150 -0
- package/src/server.ts +4 -1
- package/src/session-manager.ts +12 -4
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
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)) {
|
package/src/cozempic.ts
ADDED
|
@@ -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
|
|
1427
|
+
resolvedSessionId !== undefined
|
|
1428
|
+
? { sessionId: resolvedSessionId, cleanSession: getCozempicAvailable() ? cleanSession : undefined }
|
|
1429
|
+
: undefined,
|
|
1427
1430
|
)
|
|
1428
1431
|
if (record) {
|
|
1429
1432
|
const sessions = readSessions()
|
package/src/session-manager.ts
CHANGED
|
@@ -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
|
|
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) {
|