eniac-slack 0.0.2

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.
Files changed (48) hide show
  1. package/SPEC.md +240 -0
  2. package/dist/app.d.ts +8 -0
  3. package/dist/app.d.ts.map +1 -0
  4. package/dist/app.js +44 -0
  5. package/dist/app.js.map +1 -0
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +39 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/handlers/mention.d.ts +10 -0
  11. package/dist/handlers/mention.d.ts.map +1 -0
  12. package/dist/handlers/mention.js +96 -0
  13. package/dist/handlers/mention.js.map +1 -0
  14. package/dist/handlers/thread.d.ts +8 -0
  15. package/dist/handlers/thread.d.ts.map +1 -0
  16. package/dist/handlers/thread.js +50 -0
  17. package/dist/handlers/thread.js.map +1 -0
  18. package/dist/services/claude.d.ts +27 -0
  19. package/dist/services/claude.d.ts.map +1 -0
  20. package/dist/services/claude.js +192 -0
  21. package/dist/services/claude.js.map +1 -0
  22. package/dist/services/git.d.ts +15 -0
  23. package/dist/services/git.d.ts.map +1 -0
  24. package/dist/services/git.js +81 -0
  25. package/dist/services/git.js.map +1 -0
  26. package/dist/services/permissions.d.ts +12 -0
  27. package/dist/services/permissions.d.ts.map +1 -0
  28. package/dist/services/permissions.js +98 -0
  29. package/dist/services/permissions.js.map +1 -0
  30. package/dist/services/slack-messenger.d.ts +11 -0
  31. package/dist/services/slack-messenger.d.ts.map +1 -0
  32. package/dist/services/slack-messenger.js +73 -0
  33. package/dist/services/slack-messenger.js.map +1 -0
  34. package/dist/utils/parse.d.ts +21 -0
  35. package/dist/utils/parse.d.ts.map +1 -0
  36. package/dist/utils/parse.js +51 -0
  37. package/dist/utils/parse.js.map +1 -0
  38. package/package.json +22 -0
  39. package/src/app.ts +54 -0
  40. package/src/cli.ts +47 -0
  41. package/src/handlers/mention.ts +119 -0
  42. package/src/handlers/thread.ts +61 -0
  43. package/src/services/claude.ts +280 -0
  44. package/src/services/git.ts +98 -0
  45. package/src/services/permissions.ts +131 -0
  46. package/src/services/slack-messenger.ts +102 -0
  47. package/src/utils/parse.ts +66 -0
  48. package/tsconfig.json +8 -0
package/SPEC.md ADDED
@@ -0,0 +1,240 @@
1
+ # eniac-slack — Slack Bot for Claude Code Sessions
2
+
3
+ ## Overview
4
+
5
+ Slack 소켓모드 봇으로, 멘션을 통해 Claude Code 세션을 시작하고 스레드 단위로 대화를 이어갈 수 있다. GitHub 레포지토리 연동 및 도구 실행 권한 승인을 Slack 버튼으로 처리한다.
6
+
7
+ ## Architecture
8
+
9
+ ```
10
+ ┌─────────────┐ Socket Mode ┌──────────────────┐
11
+ │ Slack API │◄──────────────────►│ Bolt App │
12
+ │ │ │ (app.ts) │
13
+ └─────────────┘ └────────┬─────────┘
14
+
15
+ ┌──────────────────┼──────────────────┐
16
+ │ │ │
17
+ ┌─────▼─────┐ ┌──────▼──────┐ ┌─────▼──────┐
18
+ │ Mention │ │ Thread │ │ Action │
19
+ │ Handler │ │ Handler │ │ Handler │
20
+ └─────┬─────┘ └──────┬──────┘ │ (buttons) │
21
+ │ │ └─────┬──────┘
22
+ └────────┬─────────┘ │
23
+ │ │
24
+ ┌──────▼──────┐ ┌──────▼──────┐
25
+ │ Claude │◄───────────►│ Permissions │
26
+ │ Service │ canUseTool │ Service │
27
+ │ (SDK API) │ callback │ │
28
+ └──────┬──────┘ └─────────────┘
29
+
30
+ ┌──────▼──────┐
31
+ │ Slack │
32
+ │ Messenger │
33
+ │ (streaming) │
34
+ └─────────────┘
35
+ ```
36
+
37
+ ## Modules
38
+
39
+ ### `cli.ts` — Entry Point
40
+
41
+ - `.env` 로드 (실행 디렉토리 및 상위 디렉토리)
42
+ - 필수 환경변수 검증: `SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN`
43
+ - `REPOS_BASE_DIR` (선택, 기본: `~/.eniac/repos`)
44
+ - Bolt 앱 시작
45
+
46
+ ### `app.ts` — Slack Bolt App
47
+
48
+ - Socket Mode로 Slack 연결
49
+ - 이벤트 등록:
50
+ - `app_mention` → `handleMention`
51
+ - `message` → `handleThreadMessage`
52
+ - 액션 등록:
53
+ - `approve_permission` → 도구 실행 승인
54
+ - `deny_permission` → 도구 실행 거부
55
+
56
+ ### `handlers/mention.ts` — @Mention Handler
57
+
58
+ **새 메시지 (thread 아닌 경우):**
59
+
60
+ 1. 메시지에서 봇 멘션 제거
61
+ 2. GitHub repo 패턴 감지:
62
+ - **있으면:** bare clone → worktree 생성 → Claude 세션 시작
63
+ - **없으면:** temp 디렉토리 생성 → Claude 세션 시작
64
+ 3. Claude 응답을 스레드에 스트리밍
65
+
66
+ **스레드 내 멘션:**
67
+
68
+ - 기존 세션이 있으면 이어서 진행
69
+ - 없으면 temp 디렉토리에 새 세션 생성
70
+
71
+ ### `handlers/thread.ts` — Thread Reply Handler
72
+
73
+ - 봇이 활성 세션을 가진 스레드의 일반 메시지 처리
74
+ - 봇 자신의 메시지, subtype 메시지 무시
75
+ - `file_share` subtype은 허용 (이미지 첨부 메시지)
76
+ - 기존 세션에 이어서 Claude에 전달
77
+
78
+ ### `services/claude.ts` — Claude Code Session Manager
79
+
80
+ **세션 관리:**
81
+
82
+ - `Map<thread_ts, Session>` 으로 메모리에 유지
83
+ - `~/.eniac/sessions.json`에 영구 저장 (서버 재시작 시 복원)
84
+ - 세션 ID: UUID (랜덤 생성)
85
+ - `hasStarted` 플래그로 첫 실행(`sessionId`) / 재개(`resume`) 구분
86
+
87
+ **세션 생명주기:**
88
+
89
+ - `createSession(threadTs, workDir, authorUserId)` → 새 세션 생성
90
+ - `getSession(threadTs)` → 기존 세션 조회
91
+ - `lastActivityAt` 매 대화마다 갱신
92
+ - 서버 시작 시 2주 이상 비활성 세션 자동 정리 (SDK `.jsonl` + worktree 삭제)
93
+
94
+ **Claude SDK 호출:**
95
+
96
+ ```typescript
97
+ query({
98
+ prompt: userMessage,
99
+ options: {
100
+ cwd: session.workDir,
101
+ canUseTool, // 커스텀 권한 콜백
102
+ permissionMode: "bypassPermissions", // SDK 내장 권한 비활성화
103
+ allowDangerouslySkipPermissions: true,
104
+ includePartialMessages: true, // 실시간 스트리밍
105
+ sessionId: session.sessionId, // 첫 호출
106
+ // 또는
107
+ resume: session.sessionId, // 이후 호출
108
+ },
109
+ });
110
+ ```
111
+
112
+ **도구 권한 분류:**
113
+
114
+ | 자동 허용 | Slack 버튼 승인 필요 |
115
+ |-----------|---------------------|
116
+ | Read, Glob, Grep | Bash |
117
+ | WebSearch, WebFetch | Edit, Write |
118
+ | Agent, TodoRead | 기타 모든 도구 |
119
+ | ListMcpResources, ReadMcpResource | |
120
+
121
+ **ChatEvent 타입:**
122
+
123
+ ```typescript
124
+ type ChatEvent =
125
+ | { type: "text"; content: string }
126
+ | { type: "error"; message: string };
127
+ ```
128
+
129
+ ### `services/permissions.ts` — Permission Bridge
130
+
131
+ **플로우:**
132
+
133
+ ```
134
+ Claude canUseTool 콜백 호출 (위험 도구)
135
+ → Slack에 Block Kit 버튼 메시지 포스트
136
+ ┌──────────────────────────────────┐
137
+ │ 🔒 Permission Required │
138
+ │ │
139
+ │ Tool: `Bash` │
140
+ │ ``` │
141
+ │ npm install express │
142
+ │ ``` │
143
+ │ │
144
+ │ [Approve] [Deny] │
145
+ └──────────────────────────────────┘
146
+ → Promise로 대기 (5분 타임아웃)
147
+ → 스레드 작성자가 버튼 클릭
148
+ → Promise resolve(true/false)
149
+ → 버튼 메시지를 결과로 업데이트
150
+ "✅ Approved by @user" 또는 "🚫 Denied by @user"
151
+ ```
152
+
153
+ **보안:**
154
+
155
+ - 스레드 작성자만 승인/거부 가능 (`authorUserId` 검증)
156
+ - 5분 타임아웃 후 자동 거부
157
+
158
+ **API:**
159
+
160
+ - `requestPermission(client, channel, threadTs, permissionId, toolName, description, authorUserId)` → `Promise<boolean>`
161
+ - `resolvePermission(client, permissionId, granted, clickedUserId)` → `Promise<void>`
162
+
163
+ ### `services/slack-messenger.ts` — Streaming Message Updater
164
+
165
+ - 초기 메시지 "⏳ Thinking..." 포스트
166
+ - `ChatEvent` 스트림을 소비하며:
167
+ - `text` → 텍스트 누적 후 메시지 업데이트
168
+ - `error` → 에러 메시지 추가
169
+ - 500ms 스로틀링으로 Slack rate limit 방지
170
+ - 중간 업데이트 시 단어 경계에서 텍스트 자르기 (한글 punycode 버그 방지)
171
+
172
+ ### `services/git.ts` — Repository Manager
173
+
174
+ **전략: bare repo + worktree**
175
+
176
+ ```
177
+ {REPOS_BASE_DIR}/
178
+ └── {owner}/
179
+ ├── {repo}.git/ # bare clone (공유)
180
+ └── {repo}/
181
+ └── worktrees/
182
+ ├── 1710000000/ # worktree 1 (branch: eniac/1710000000)
183
+ └── 1710000001/ # worktree 2 (branch: eniac/1710000001)
184
+ ```
185
+
186
+ 1. bare repo가 없으면 `git clone --bare`
187
+ 2. bare repo가 있으면 `git fetch --all`
188
+ 3. `git worktree add -b eniac/{timestamp} {path} {defaultBranch}` 로 작업 디렉토리 생성
189
+ 4. default branch는 `HEAD` symbolic ref에서 자동 감지
190
+ 5. 스레드마다 독립 브랜치 → PR 생성 가능
191
+
192
+ ### `utils/parse.ts` — Message Parser
193
+
194
+ - `removeMention(text)`: `<@BOTID>` 제거
195
+ - `extractGithubRepo(text)`: GitHub repo 추출
196
+ - `https://github.com/owner/repo` ✅
197
+ - `github.com/owner/repo` ✅
198
+ - `owner/repo` ✅ (유효한 GitHub 네이밍 규칙 충족 시)
199
+ - `../path` ❌ (파일 경로 형태 거부)
200
+
201
+ ## Data Storage
202
+
203
+ | 데이터 | 경로 | 내용 |
204
+ |--------|------|------|
205
+ | 세션 매핑 | `~/.eniac/sessions.json` | `threadTs → { sessionId, workDir, authorUserId, ... }` |
206
+ | SDK 대화 기록 | `~/.claude/projects/{cwd-encoded}/{sessionId}.jsonl` | 전체 대화 히스토리 (SDK 자동 관리) |
207
+ | Git bare repos | `{REPOS_BASE_DIR}/{owner}/{repo}.git/` | 공유 bare clone |
208
+ | Git worktrees | `{REPOS_BASE_DIR}/{owner}/{repo}/worktrees/{timestamp}/` | 스레드별 작업 디렉토리 |
209
+
210
+ **세션 만료:** `lastActivityAt` 기준 2주 경과 시 서버 시작 시점에 자동 정리 (세션 파일 + worktree 삭제)
211
+
212
+ ## Environment Variables
213
+
214
+ | 변수 | 필수 | 설명 |
215
+ |------|------|------|
216
+ | `SLACK_BOT_TOKEN` | ✅ | `xoxb-` 봇 토큰 |
217
+ | `SLACK_APP_TOKEN` | ✅ | `xapp-` 앱 레벨 토큰 (소켓모드) |
218
+ | `REPOS_BASE_DIR` | ❌ | 레포 저장 경로 (기본: `~/.eniac/repos`) |
219
+
220
+ ## Tech Stack
221
+
222
+ - **Runtime:** Node.js ≥ 18
223
+ - **Language:** TypeScript (ES2022, ESM)
224
+ - **Package Manager:** Yarn 4 (Berry, node-modules linker)
225
+ - **Monorepo:** Yarn Workspaces (`services/*`, `packages/*`)
226
+ - **Slack:** `@slack/bolt` v4 (Socket Mode)
227
+ - **Claude:** `@anthropic-ai/claude-agent-sdk` v0.2.72 (`query()` API)
228
+ - **Git:** `simple-git`
229
+
230
+ ## Slack App 설정 요구사항
231
+
232
+ 1. **Socket Mode** 활성화
233
+ 2. **Event Subscriptions:**
234
+ - `app_mention`
235
+ - `message.channels` / `message.groups`
236
+ 3. **Bot Token Scopes:**
237
+ - `app_mentions:read`
238
+ - `chat:write`
239
+ - `channels:history` / `groups:history`
240
+ 4. **Interactivity** 활성화 (버튼 클릭 처리용)
package/dist/app.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { App } from "@slack/bolt";
2
+ export interface AppConfig {
3
+ slackBotToken: string;
4
+ slackAppToken: string;
5
+ reposBaseDir: string;
6
+ }
7
+ export declare function createAndStartApp(config: AppConfig): Promise<App>;
8
+ //# sourceMappingURL=app.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAKlC,MAAM,WAAW,SAAS;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,CA0CvE"}
package/dist/app.js ADDED
@@ -0,0 +1,44 @@
1
+ import { App } from "@slack/bolt";
2
+ import { handleMention, setReposBaseDir } from "./handlers/mention.js";
3
+ import { handleThreadMessage } from "./handlers/thread.js";
4
+ import { resolvePermission } from "./services/permissions.js";
5
+ export async function createAndStartApp(config) {
6
+ const { slackBotToken, slackAppToken, reposBaseDir } = config;
7
+ setReposBaseDir(reposBaseDir);
8
+ const app = new App({
9
+ token: slackBotToken,
10
+ appToken: slackAppToken,
11
+ socketMode: true,
12
+ });
13
+ // Event handlers
14
+ app.event("app_mention", handleMention);
15
+ app.event("message", handleThreadMessage);
16
+ // Permission button handlers
17
+ app.action("approve_permission", async ({ action, ack, client, body }) => {
18
+ await ack();
19
+ console.log(`[action] approve_permission fired! action.type=${action.type}, body.user=${JSON.stringify(body.user)}`);
20
+ if (action.type !== "button")
21
+ return;
22
+ const permissionId = action.value;
23
+ const clickedUserId = body.user?.id ?? "someone";
24
+ console.log(`[action] approve: permissionId=${permissionId}, clickedUserId=${clickedUserId}`);
25
+ if (permissionId) {
26
+ await resolvePermission(client, permissionId, true, clickedUserId);
27
+ }
28
+ });
29
+ app.action("deny_permission", async ({ action, ack, client, body }) => {
30
+ await ack();
31
+ console.log(`[action] deny_permission fired!`);
32
+ if (action.type !== "button")
33
+ return;
34
+ const permissionId = action.value;
35
+ const clickedUserId = body.user?.id ?? "someone";
36
+ console.log(`[action] deny: permissionId=${permissionId}, clickedUserId=${clickedUserId}`);
37
+ if (permissionId) {
38
+ await resolvePermission(client, permissionId, false, clickedUserId);
39
+ }
40
+ });
41
+ await app.start();
42
+ return app;
43
+ }
44
+ //# sourceMappingURL=app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACvE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAQ9D,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,MAAiB;IACvD,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,GAAG,MAAM,CAAC;IAE9D,eAAe,CAAC,YAAY,CAAC,CAAC;IAE9B,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC;QAClB,KAAK,EAAE,aAAa;QACpB,QAAQ,EAAE,aAAa;QACvB,UAAU,EAAE,IAAI;KACjB,CAAC,CAAC;IAEH,iBAAiB;IACjB,GAAG,CAAC,KAAK,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;IACxC,GAAG,CAAC,KAAK,CAAC,SAAS,EAAE,mBAAmB,CAAC,CAAC;IAE1C,6BAA6B;IAC7B,GAAG,CAAC,MAAM,CAAC,oBAAoB,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE;QACvE,MAAM,GAAG,EAAE,CAAC;QACZ,OAAO,CAAC,GAAG,CAAC,kDAAkD,MAAM,CAAC,IAAI,eAAe,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACrH,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO;QACrC,MAAM,YAAY,GAAG,MAAM,CAAC,KAAK,CAAC;QAClC,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,EAAE,EAAE,IAAI,SAAS,CAAC;QACjD,OAAO,CAAC,GAAG,CAAC,kCAAkC,YAAY,mBAAmB,aAAa,EAAE,CAAC,CAAC;QAC9F,IAAI,YAAY,EAAE,CAAC;YACjB,MAAM,iBAAiB,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC;QACrE,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,MAAM,CAAC,iBAAiB,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE;QACpE,MAAM,GAAG,EAAE,CAAC;QACZ,OAAO,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;QAC/C,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO;QACrC,MAAM,YAAY,GAAG,MAAM,CAAC,KAAK,CAAC;QAClC,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,EAAE,EAAE,IAAI,SAAS,CAAC;QACjD,OAAO,CAAC,GAAG,CAAC,+BAA+B,YAAY,mBAAmB,aAAa,EAAE,CAAC,CAAC;QAC3F,IAAI,YAAY,EAAE,CAAC;YACjB,MAAM,iBAAiB,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC;QACtE,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC;IAClB,OAAO,GAAG,CAAC;AACb,CAAC"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import dotenv from "dotenv";
5
+ import { createAndStartApp } from "./app.js";
6
+ // Load .env — try cwd first, then walk up to find it
7
+ dotenv.config();
8
+ dotenv.config({ path: path.resolve(process.cwd(), "../../.env") });
9
+ function requireEnv(name) {
10
+ const value = process.env[name];
11
+ if (!value) {
12
+ console.error(`[eniac-slack] Missing required environment variable: ${name}`);
13
+ console.error(`Create a .env file or export ${name} in your shell.`);
14
+ process.exit(1);
15
+ }
16
+ return value;
17
+ }
18
+ async function main() {
19
+ const slackBotToken = requireEnv("SLACK_BOT_TOKEN");
20
+ const slackAppToken = requireEnv("SLACK_APP_TOKEN");
21
+ const reposBaseDir = process.env["REPOS_BASE_DIR"]?.replace(/^~/, os.homedir()) ??
22
+ path.join(os.homedir(), ".eniac", "repos");
23
+ console.log("[eniac-slack] Starting Slack bot...");
24
+ console.log(`[eniac-slack] Repos base directory: ${reposBaseDir}`);
25
+ try {
26
+ await createAndStartApp({
27
+ slackBotToken,
28
+ slackAppToken,
29
+ reposBaseDir,
30
+ });
31
+ console.log("[eniac-slack] Bot is running! Listening for events...");
32
+ }
33
+ catch (error) {
34
+ console.error("[eniac-slack] Failed to start:", error);
35
+ process.exit(1);
36
+ }
37
+ }
38
+ main();
39
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C,qDAAqD;AACrD,MAAM,CAAC,MAAM,EAAE,CAAC;AAChB,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,YAAY,CAAC,EAAE,CAAC,CAAC;AAEnE,SAAS,UAAU,CAAC,IAAY;IAC9B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,CAAC,KAAK,CAAC,wDAAwD,IAAI,EAAE,CAAC,CAAC;QAC9E,OAAO,CAAC,KAAK,CAAC,gCAAgC,IAAI,iBAAiB,CAAC,CAAC;QACrE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,aAAa,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC;IACpD,MAAM,aAAa,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC;IAEpD,MAAM,YAAY,GAChB,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC;QAC1D,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;IAE7C,OAAO,CAAC,GAAG,CAAC,qCAAqC,CAAC,CAAC;IACnD,OAAO,CAAC,GAAG,CAAC,uCAAuC,YAAY,EAAE,CAAC,CAAC;IAEnE,IAAI,CAAC;QACH,MAAM,iBAAiB,CAAC;YACtB,aAAa;YACb,aAAa;YACb,YAAY;SACb,CAAC,CAAC;QAEH,OAAO,CAAC,GAAG,CAAC,uDAAuD,CAAC,CAAC;IACvE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,gCAAgC,EAAE,KAAK,CAAC,CAAC;QACvD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { AllMiddlewareArgs, SlackEventMiddlewareArgs } from "@slack/bolt";
2
+ export declare function setReposBaseDir(dir: string): void;
3
+ /**
4
+ * Handle `app_mention` events.
5
+ *
6
+ * - New mention (not in a thread): start a new Claude session.
7
+ * - Mention in a thread: forward to thread handler logic.
8
+ */
9
+ export declare function handleMention(args: AllMiddlewareArgs & SlackEventMiddlewareArgs<"app_mention">): Promise<void>;
10
+ //# sourceMappingURL=mention.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mention.d.ts","sourceRoot":"","sources":["../../src/handlers/mention.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAW/E,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEjD;AASD;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,iBAAiB,GAAG,wBAAwB,CAAC,aAAa,CAAC,GAChE,OAAO,CAAC,IAAI,CAAC,CAgEf"}
@@ -0,0 +1,96 @@
1
+ import { removeMention, extractGithubRepo } from "../utils/parse.js";
2
+ import { createSession, getSession, chat } from "../services/claude.js";
3
+ import { prepareWorkDir } from "../services/git.js";
4
+ import { postStreamingReply } from "../services/slack-messenger.js";
5
+ import fs from "node:fs/promises";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ let reposBaseDir;
9
+ export function setReposBaseDir(dir) {
10
+ reposBaseDir = dir;
11
+ }
12
+ function getReposBaseDir() {
13
+ if (!reposBaseDir) {
14
+ reposBaseDir = path.join(os.homedir(), ".eniac", "repos");
15
+ }
16
+ return reposBaseDir;
17
+ }
18
+ /**
19
+ * Handle `app_mention` events.
20
+ *
21
+ * - New mention (not in a thread): start a new Claude session.
22
+ * - Mention in a thread: forward to thread handler logic.
23
+ */
24
+ export async function handleMention(args) {
25
+ const { event, client } = args;
26
+ const { text, channel, ts, thread_ts } = event;
27
+ const authorUserId = event.user ?? "unknown";
28
+ // If the mention is inside an existing thread, treat it as a thread reply
29
+ if (thread_ts) {
30
+ await handleThreadedMention(client, channel, thread_ts, text, authorUserId);
31
+ return;
32
+ }
33
+ // New conversation — ts becomes the thread_ts
34
+ const threadTs = ts;
35
+ const cleanText = removeMention(text);
36
+ if (!cleanText) {
37
+ await client.chat.postMessage({
38
+ channel,
39
+ thread_ts: threadTs,
40
+ text: "How can I help you? You can ask me coding questions, or mention a GitHub repo (e.g. `owner/repo`) to work with its code.",
41
+ });
42
+ return;
43
+ }
44
+ // Check for GitHub repo in message
45
+ const repoInfo = extractGithubRepo(cleanText);
46
+ let workDir;
47
+ if (repoInfo) {
48
+ try {
49
+ await client.chat.postMessage({
50
+ channel,
51
+ thread_ts: threadTs,
52
+ text: `:file_folder: Setting up repository \`${repoInfo.owner}/${repoInfo.repo}\`...`,
53
+ });
54
+ workDir = await prepareWorkDir(repoInfo, getReposBaseDir());
55
+ }
56
+ catch (error) {
57
+ const message = error instanceof Error ? error.message : String(error);
58
+ await client.chat.postMessage({
59
+ channel,
60
+ thread_ts: threadTs,
61
+ text: `:x: Failed to set up repository: ${message}`,
62
+ });
63
+ return;
64
+ }
65
+ }
66
+ else {
67
+ // Create a temp directory for non-repo conversations
68
+ workDir = await fs.mkdtemp(path.join(os.tmpdir(), "eniac-"));
69
+ }
70
+ // Create a Claude session
71
+ createSession(threadTs, workDir, authorUserId);
72
+ // Build context-aware first message
73
+ let userMessage = cleanText;
74
+ if (repoInfo) {
75
+ userMessage = `[Working directory: ${workDir} — Repository: ${repoInfo.owner}/${repoInfo.repo}]\n\n${cleanText}`;
76
+ }
77
+ // Stream the response
78
+ const stream = chat(threadTs, userMessage, client, channel);
79
+ await postStreamingReply(client, channel, threadTs, stream);
80
+ }
81
+ /**
82
+ * Handle a mention that occurs inside an existing thread.
83
+ */
84
+ async function handleThreadedMention(client, channel, threadTs, text, authorUserId) {
85
+ const cleanText = removeMention(text);
86
+ if (!cleanText)
87
+ return;
88
+ let session = getSession(threadTs);
89
+ if (!session) {
90
+ const workDir = await fs.mkdtemp(path.join(os.tmpdir(), "eniac-"));
91
+ session = createSession(threadTs, workDir, authorUserId);
92
+ }
93
+ const stream = chat(threadTs, cleanText, client, channel);
94
+ await postStreamingReply(client, channel, threadTs, stream);
95
+ }
96
+ //# sourceMappingURL=mention.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mention.js","sourceRoot":"","sources":["../../src/handlers/mention.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { AllMiddlewareArgs, SlackEventMiddlewareArgs } from "@slack/bolt";
2
+ /**
3
+ * Handle messages posted in threads where the bot has an active session.
4
+ *
5
+ * Only responds to human messages (ignores bot messages and subtypes).
6
+ */
7
+ export declare function handleThreadMessage(args: AllMiddlewareArgs & SlackEventMiddlewareArgs<"message">): Promise<void>;
8
+ //# sourceMappingURL=thread.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thread.d.ts","sourceRoot":"","sources":["../../src/handlers/thread.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAI/E;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,iBAAiB,GAAG,wBAAwB,CAAC,SAAS,CAAC,GAC5D,OAAO,CAAC,IAAI,CAAC,CAiDf"}
@@ -0,0 +1,50 @@
1
+ import { getSession, chat } from "../services/claude.js";
2
+ import { postStreamingReply } from "../services/slack-messenger.js";
3
+ /**
4
+ * Handle messages posted in threads where the bot has an active session.
5
+ *
6
+ * Only responds to human messages (ignores bot messages and subtypes).
7
+ */
8
+ export async function handleThreadMessage(args) {
9
+ const { event, client, context } = args;
10
+ const subtype = event.subtype ?? "none";
11
+ const threadTs = "thread_ts" in event ? event.thread_ts : undefined;
12
+ const botId = "bot_id" in event ? event.bot_id : undefined;
13
+ const userId = "user" in event ? event.user : undefined;
14
+ console.log(`[thread] message event: subtype=${subtype}, thread_ts=${threadTs ?? "none"}, bot_id=${botId ?? "none"}, user=${userId ?? "none"}`);
15
+ // Skip non-user subtypes (message_changed, bot_message, etc.)
16
+ // But allow file_share (image/file attachments) — those are real user messages
17
+ const allowedSubtypes = new Set(["file_share"]);
18
+ if (event.subtype !== undefined && !allowedSubtypes.has(event.subtype)) {
19
+ console.log(`[thread] skipped: subtype=${subtype}`);
20
+ return;
21
+ }
22
+ // Must be in a thread
23
+ if (!threadTs) {
24
+ console.log(`[thread] skipped: not in a thread`);
25
+ return;
26
+ }
27
+ // Ignore messages from the bot itself
28
+ if (botId || userId === context.botUserId) {
29
+ console.log(`[thread] skipped: bot message`);
30
+ return;
31
+ }
32
+ const text = "text" in event ? event.text : undefined;
33
+ const channel = event.channel;
34
+ // Only respond if we have an active session for this thread
35
+ const session = getSession(threadTs);
36
+ if (!session) {
37
+ console.log(`[thread] skipped: no session for thread_ts=${threadTs}`);
38
+ return;
39
+ }
40
+ console.log(`[thread] session found! sessionId=${session.sessionId}, text=${text?.slice(0, 50)}`);
41
+ if (!text || text.trim().length === 0)
42
+ return;
43
+ // Strip any bot mentions from the text (user might @mention the bot in-thread)
44
+ const cleanText = text.replace(/<@[A-Z0-9]+>/g, "").trim();
45
+ if (!cleanText)
46
+ return;
47
+ const stream = chat(threadTs, cleanText, client, channel);
48
+ await postStreamingReply(client, channel, threadTs, stream);
49
+ }
50
+ //# sourceMappingURL=thread.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thread.js","sourceRoot":"","sources":["../../src/handlers/thread.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAEpE;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,IAA6D;IAE7D,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAExC,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,IAAI,MAAM,CAAC;IACxC,MAAM,QAAQ,GAAG,WAAW,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;IACpE,MAAM,KAAK,GAAG,QAAQ,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;IAC3D,MAAM,MAAM,GAAG,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IACxD,OAAO,CAAC,GAAG,CAAC,mCAAmC,OAAO,eAAe,QAAQ,IAAI,MAAM,YAAY,KAAK,IAAI,MAAM,UAAU,MAAM,IAAI,MAAM,EAAE,CAAC,CAAC;IAEhJ,8DAA8D;IAC9D,+EAA+E;IAC/E,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC;IAChD,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACvE,OAAO,CAAC,GAAG,CAAC,6BAA6B,OAAO,EAAE,CAAC,CAAC;QACpD,OAAO;IACT,CAAC;IAED,sBAAsB;IACtB,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;QACjD,OAAO;IACT,CAAC;IAED,sCAAsC;IACtC,IAAI,KAAK,IAAI,MAAM,KAAK,OAAO,CAAC,SAAS,EAAE,CAAC;QAC1C,OAAO,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAC;QAC7C,OAAO;IACT,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IACtD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAE9B,4DAA4D;IAC5D,MAAM,OAAO,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IACrC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,CAAC,GAAG,CAAC,8CAA8C,QAAQ,EAAE,CAAC,CAAC;QACtE,OAAO;IACT,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,qCAAqC,OAAO,CAAC,SAAS,UAAU,IAAI,EAAE,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;IAElG,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAE9C,+EAA+E;IAC/E,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC3D,IAAI,CAAC,SAAS;QAAE,OAAO;IAEvB,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1D,MAAM,kBAAkB,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;AAC9D,CAAC"}
@@ -0,0 +1,27 @@
1
+ import type { WebClient } from "@slack/web-api";
2
+ interface Session {
3
+ sessionId: string;
4
+ workDir: string;
5
+ hasStarted: boolean;
6
+ authorUserId: string;
7
+ createdAt: number;
8
+ lastActivityAt: number;
9
+ }
10
+ export type ChatEvent = {
11
+ type: "text";
12
+ content: string;
13
+ } | {
14
+ type: "error";
15
+ message: string;
16
+ };
17
+ export declare function createSession(threadTs: string, workDir: string, authorUserId: string): Session;
18
+ export declare function getSession(threadTs: string): Session | undefined;
19
+ /**
20
+ * Send a message to a Claude session via the SDK.
21
+ *
22
+ * Uses `canUseTool` callback to handle permission requests
23
+ * through Slack interactive buttons.
24
+ */
25
+ export declare function chat(threadTs: string, userMessage: string, slackClient: WebClient, channel: string): AsyncGenerator<ChatEvent, void, unknown>;
26
+ export {};
27
+ //# sourceMappingURL=claude.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../src/services/claude.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,UAAU,OAAO;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,SAAS,GACjB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAgFvC,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,GACnB,OAAO,CAaT;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAEhE;AAkBD;;;;;GAKG;AACH,wBAAuB,IAAI,CACzB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,SAAS,EACtB,OAAO,EAAE,MAAM,GACd,cAAc,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,CA6H1C"}