afk-code 0.1.0 → 0.1.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.
@@ -0,0 +1,1970 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/slack/session-manager.ts
13
+ import { watch } from "fs";
14
+ import { readdir, readFile, stat, unlink } from "fs/promises";
15
+ import { createServer } from "net";
16
+ import { createHash } from "crypto";
17
+ function hash(data) {
18
+ return createHash("md5").update(data).digest("hex");
19
+ }
20
+ var DAEMON_SOCKET2, SessionManager;
21
+ var init_session_manager = __esm({
22
+ "src/slack/session-manager.ts"() {
23
+ "use strict";
24
+ DAEMON_SOCKET2 = "/tmp/afk-code-daemon.sock";
25
+ SessionManager = class {
26
+ sessions = /* @__PURE__ */ new Map();
27
+ claimedFiles = /* @__PURE__ */ new Set();
28
+ events;
29
+ server = null;
30
+ constructor(events) {
31
+ this.events = events;
32
+ }
33
+ async start() {
34
+ try {
35
+ await unlink(DAEMON_SOCKET2);
36
+ } catch {
37
+ }
38
+ this.server = createServer((socket) => {
39
+ let messageBuffer = "";
40
+ socket.on("data", (data) => {
41
+ messageBuffer += data.toString();
42
+ const lines = messageBuffer.split("\n");
43
+ messageBuffer = lines.pop() || "";
44
+ for (const line of lines) {
45
+ if (!line.trim()) continue;
46
+ try {
47
+ const parsed = JSON.parse(line);
48
+ this.handleSessionMessage(socket, parsed);
49
+ } catch (error) {
50
+ console.error("[SessionManager] Error parsing message:", error);
51
+ }
52
+ }
53
+ });
54
+ socket.on("error", (error) => {
55
+ console.error("[SessionManager] Socket error:", error);
56
+ });
57
+ socket.on("close", () => {
58
+ for (const [id, session] of this.sessions) {
59
+ if (session.socket === socket) {
60
+ console.log(`[SessionManager] Session disconnected: ${id}`);
61
+ this.stopWatching(session);
62
+ this.sessions.delete(id);
63
+ this.events.onSessionEnd(id);
64
+ break;
65
+ }
66
+ }
67
+ });
68
+ });
69
+ this.server.listen(DAEMON_SOCKET2, () => {
70
+ console.log(`[SessionManager] Listening on ${DAEMON_SOCKET2}`);
71
+ });
72
+ }
73
+ stop() {
74
+ for (const session of this.sessions.values()) {
75
+ this.stopWatching(session);
76
+ }
77
+ this.sessions.clear();
78
+ if (this.server) {
79
+ this.server.close();
80
+ }
81
+ }
82
+ sendInput(sessionId, text) {
83
+ const session = this.sessions.get(sessionId);
84
+ if (!session) {
85
+ console.error(`[SessionManager] Session not found: ${sessionId}`);
86
+ return false;
87
+ }
88
+ try {
89
+ session.socket.write(JSON.stringify({ type: "input", text }) + "\n");
90
+ } catch (err) {
91
+ console.error(`[SessionManager] Failed to send input to ${sessionId}:`, err);
92
+ this.stopWatching(session);
93
+ this.sessions.delete(sessionId);
94
+ this.events.onSessionEnd(sessionId);
95
+ return false;
96
+ }
97
+ setTimeout(() => {
98
+ try {
99
+ session.socket.write(JSON.stringify({ type: "input", text: "\r" }) + "\n");
100
+ } catch {
101
+ }
102
+ }, 50);
103
+ return true;
104
+ }
105
+ getSession(sessionId) {
106
+ const session = this.sessions.get(sessionId);
107
+ if (!session) return void 0;
108
+ return {
109
+ id: session.id,
110
+ name: session.name,
111
+ cwd: session.cwd,
112
+ projectDir: session.projectDir,
113
+ status: session.status,
114
+ startedAt: session.startedAt
115
+ };
116
+ }
117
+ getAllSessions() {
118
+ return Array.from(this.sessions.values()).map((s) => ({
119
+ id: s.id,
120
+ name: s.name,
121
+ cwd: s.cwd,
122
+ projectDir: s.projectDir,
123
+ status: s.status,
124
+ startedAt: s.startedAt
125
+ }));
126
+ }
127
+ async handleSessionMessage(socket, message) {
128
+ switch (message.type) {
129
+ case "session_start": {
130
+ const initialFileStats = await this.snapshotJsonlFiles(message.projectDir);
131
+ const session = {
132
+ id: message.id,
133
+ name: message.name || message.command?.join(" ") || "Session",
134
+ cwd: message.cwd,
135
+ projectDir: message.projectDir,
136
+ socket,
137
+ status: "running",
138
+ seenMessages: /* @__PURE__ */ new Set(),
139
+ startedAt: /* @__PURE__ */ new Date(),
140
+ slugFound: false,
141
+ lastTodosHash: "",
142
+ inPlanMode: false,
143
+ initialFileStats
144
+ };
145
+ this.sessions.set(message.id, session);
146
+ console.log(`[SessionManager] Session started: ${message.id} - ${session.name}`);
147
+ console.log(`[SessionManager] Snapshot: ${initialFileStats.size} existing JSONL files`);
148
+ this.events.onSessionStart({
149
+ id: session.id,
150
+ name: session.name,
151
+ cwd: session.cwd,
152
+ projectDir: session.projectDir,
153
+ status: session.status,
154
+ startedAt: session.startedAt
155
+ });
156
+ this.startWatching(session);
157
+ break;
158
+ }
159
+ case "session_end": {
160
+ const session = this.sessions.get(message.sessionId);
161
+ if (session) {
162
+ console.log(`[SessionManager] Session ended: ${message.sessionId}`);
163
+ this.stopWatching(session);
164
+ this.sessions.delete(message.sessionId);
165
+ this.events.onSessionEnd(message.sessionId);
166
+ }
167
+ break;
168
+ }
169
+ }
170
+ }
171
+ async snapshotJsonlFiles(projectDir) {
172
+ const stats = /* @__PURE__ */ new Map();
173
+ try {
174
+ const files = await readdir(projectDir);
175
+ for (const f of files) {
176
+ if (f.endsWith(".jsonl") && !f.startsWith("agent-")) {
177
+ const path = `${projectDir}/${f}`;
178
+ const fileStat = await stat(path);
179
+ stats.set(path, fileStat.mtimeMs);
180
+ }
181
+ }
182
+ } catch {
183
+ }
184
+ return stats;
185
+ }
186
+ async hasConversationMessages(path) {
187
+ try {
188
+ const content = await readFile(path, "utf-8");
189
+ return content.includes('"type":"user"') || content.includes('"type":"assistant"');
190
+ } catch {
191
+ return false;
192
+ }
193
+ }
194
+ async findActiveJsonlFile(session) {
195
+ try {
196
+ const files = await readdir(session.projectDir);
197
+ const jsonlFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
198
+ const allPaths = jsonlFiles.map((f) => `${session.projectDir}/${f}`).filter((path) => !this.claimedFiles.has(path));
199
+ if (allPaths.length === 0) return null;
200
+ const fileStats = await Promise.all(
201
+ allPaths.map(async (path) => {
202
+ const fileStat = await stat(path);
203
+ return { path, mtime: fileStat.mtimeMs };
204
+ })
205
+ );
206
+ fileStats.sort((a, b) => b.mtime - a.mtime);
207
+ for (const { path, mtime } of fileStats) {
208
+ const initialMtime = session.initialFileStats.get(path);
209
+ if (initialMtime !== void 0 && mtime > initialMtime) {
210
+ if (await this.hasConversationMessages(path)) {
211
+ console.log(`[SessionManager] Found modified JSONL (--continue): ${path}`);
212
+ return path;
213
+ }
214
+ }
215
+ }
216
+ for (const { path } of fileStats) {
217
+ const initialMtime = session.initialFileStats.get(path);
218
+ if (initialMtime === void 0) {
219
+ if (await this.hasConversationMessages(path)) {
220
+ console.log(`[SessionManager] Found new JSONL: ${path}`);
221
+ return path;
222
+ }
223
+ }
224
+ }
225
+ return null;
226
+ } catch {
227
+ return null;
228
+ }
229
+ }
230
+ async processJsonlUpdates(session) {
231
+ if (!session.watchedFile) return;
232
+ try {
233
+ const content = await readFile(session.watchedFile, "utf-8");
234
+ const lines = content.split("\n").filter(Boolean);
235
+ for (const line of lines) {
236
+ const lineHash = hash(line);
237
+ if (session.seenMessages.has(lineHash)) continue;
238
+ session.seenMessages.add(lineHash);
239
+ if (!session.slugFound) {
240
+ const slug = this.extractSlug(line);
241
+ if (slug) {
242
+ session.slugFound = true;
243
+ session.name = slug;
244
+ console.log(`[SessionManager] Session ${session.id} name: ${slug}`);
245
+ this.events.onSessionUpdate(session.id, slug);
246
+ }
247
+ }
248
+ const todos = this.extractTodos(line);
249
+ if (todos) {
250
+ const todosHash = hash(JSON.stringify(todos));
251
+ if (todosHash !== session.lastTodosHash) {
252
+ session.lastTodosHash = todosHash;
253
+ this.events.onTodos(session.id, todos);
254
+ }
255
+ }
256
+ const planModeStatus = this.detectPlanMode(line);
257
+ if (planModeStatus !== null && planModeStatus !== session.inPlanMode) {
258
+ session.inPlanMode = planModeStatus;
259
+ console.log(`[SessionManager] Session ${session.id} plan mode: ${planModeStatus}`);
260
+ this.events.onPlanModeChange(session.id, planModeStatus);
261
+ }
262
+ const toolCalls = this.extractToolCalls(line);
263
+ for (const tool of toolCalls) {
264
+ this.events.onToolCall(session.id, tool);
265
+ }
266
+ const toolResults = this.extractToolResults(line);
267
+ for (const result of toolResults) {
268
+ this.events.onToolResult(session.id, result);
269
+ }
270
+ const parsed = this.parseJsonlLine(line);
271
+ if (parsed) {
272
+ const messageTime = new Date(parsed.timestamp);
273
+ if (messageTime < session.startedAt) continue;
274
+ this.events.onMessage(session.id, parsed.role, parsed.content);
275
+ }
276
+ }
277
+ } catch (err) {
278
+ console.error("[SessionManager] Error processing JSONL:", err);
279
+ }
280
+ }
281
+ async startWatching(session) {
282
+ const jsonlFile = await this.findActiveJsonlFile(session);
283
+ if (jsonlFile) {
284
+ session.watchedFile = jsonlFile;
285
+ this.claimedFiles.add(jsonlFile);
286
+ console.log(`[SessionManager] Watching: ${jsonlFile}`);
287
+ await this.processJsonlUpdates(session);
288
+ } else {
289
+ console.log(`[SessionManager] Waiting for JSONL changes in ${session.projectDir}`);
290
+ }
291
+ try {
292
+ session.watcher = watch(session.projectDir, { recursive: false }, async (_, filename) => {
293
+ if (!filename?.endsWith(".jsonl")) return;
294
+ if (!session.watchedFile) {
295
+ const newFile = await this.findActiveJsonlFile(session);
296
+ if (newFile) {
297
+ session.watchedFile = newFile;
298
+ this.claimedFiles.add(newFile);
299
+ }
300
+ }
301
+ const filePath = `${session.projectDir}/${filename}`;
302
+ if (session.watchedFile && filePath === session.watchedFile) {
303
+ await this.processJsonlUpdates(session);
304
+ }
305
+ });
306
+ } catch (err) {
307
+ console.error("[SessionManager] Error setting up watcher:", err);
308
+ }
309
+ const pollInterval = setInterval(async () => {
310
+ if (!this.sessions.has(session.id)) {
311
+ clearInterval(pollInterval);
312
+ return;
313
+ }
314
+ if (!session.watchedFile) {
315
+ const newFile = await this.findActiveJsonlFile(session);
316
+ if (newFile) {
317
+ session.watchedFile = newFile;
318
+ this.claimedFiles.add(newFile);
319
+ }
320
+ }
321
+ if (session.watchedFile) {
322
+ await this.processJsonlUpdates(session);
323
+ }
324
+ }, 1e3);
325
+ }
326
+ stopWatching(session) {
327
+ if (session.watcher) {
328
+ session.watcher.close();
329
+ }
330
+ if (session.watchedFile) {
331
+ this.claimedFiles.delete(session.watchedFile);
332
+ }
333
+ }
334
+ detectPlanMode(line) {
335
+ try {
336
+ const data = JSON.parse(line);
337
+ if (data.type !== "user") return null;
338
+ const content = data.message?.content;
339
+ if (typeof content !== "string") return null;
340
+ if (content.includes("<system-reminder>") && content.includes("Plan mode is active")) {
341
+ return true;
342
+ }
343
+ if (content.includes("Exited Plan Mode") || content.includes("exited plan mode")) {
344
+ return false;
345
+ }
346
+ return null;
347
+ } catch {
348
+ return null;
349
+ }
350
+ }
351
+ extractToolCalls(line) {
352
+ try {
353
+ const data = JSON.parse(line);
354
+ if (data.type !== "assistant") return [];
355
+ const content = data.message?.content;
356
+ if (!Array.isArray(content)) return [];
357
+ const tools = [];
358
+ for (const block of content) {
359
+ if (block.type === "tool_use" && block.id && block.name) {
360
+ tools.push({
361
+ id: block.id,
362
+ name: block.name,
363
+ input: block.input || {}
364
+ });
365
+ }
366
+ }
367
+ return tools;
368
+ } catch {
369
+ return [];
370
+ }
371
+ }
372
+ extractToolResults(line) {
373
+ try {
374
+ const data = JSON.parse(line);
375
+ if (data.type !== "user") return [];
376
+ const content = data.message?.content;
377
+ if (!Array.isArray(content)) return [];
378
+ const results = [];
379
+ for (const block of content) {
380
+ if (block.type === "tool_result" && block.tool_use_id) {
381
+ let text = "";
382
+ if (typeof block.content === "string") {
383
+ text = block.content;
384
+ } else if (Array.isArray(block.content)) {
385
+ text = block.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
386
+ }
387
+ results.push({
388
+ toolUseId: block.tool_use_id,
389
+ content: text,
390
+ isError: block.is_error === true
391
+ });
392
+ }
393
+ }
394
+ return results;
395
+ } catch {
396
+ return [];
397
+ }
398
+ }
399
+ extractSlug(line) {
400
+ try {
401
+ const data = JSON.parse(line);
402
+ if (data.slug && typeof data.slug === "string") {
403
+ return data.slug;
404
+ }
405
+ return null;
406
+ } catch {
407
+ return null;
408
+ }
409
+ }
410
+ extractTodos(line) {
411
+ try {
412
+ const data = JSON.parse(line);
413
+ if (data.todos && Array.isArray(data.todos) && data.todos.length > 0) {
414
+ return data.todos.map((t) => ({
415
+ content: t.content || "",
416
+ status: t.status || "pending",
417
+ activeForm: t.activeForm
418
+ }));
419
+ }
420
+ return null;
421
+ } catch {
422
+ return null;
423
+ }
424
+ }
425
+ parseJsonlLine(line) {
426
+ try {
427
+ const data = JSON.parse(line);
428
+ if (data.type !== "user" && data.type !== "assistant") return null;
429
+ if (data.isMeta || data.subtype) return null;
430
+ const message = data.message;
431
+ if (!message || !message.role) return null;
432
+ let content = "";
433
+ if (typeof message.content === "string") {
434
+ content = message.content;
435
+ } else if (Array.isArray(message.content)) {
436
+ for (const block of message.content) {
437
+ if (block.type === "text" && block.text) {
438
+ content += block.text;
439
+ }
440
+ }
441
+ }
442
+ if (!content.trim()) return null;
443
+ return {
444
+ role: message.role,
445
+ content: content.trim(),
446
+ timestamp: data.timestamp || (/* @__PURE__ */ new Date()).toISOString()
447
+ };
448
+ } catch {
449
+ return null;
450
+ }
451
+ }
452
+ };
453
+ }
454
+ });
455
+
456
+ // src/slack/channel-manager.ts
457
+ function sanitizeChannelName(name) {
458
+ return name.toLowerCase().replace(/[^a-z0-9-_\s]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 70);
459
+ }
460
+ var ChannelManager;
461
+ var init_channel_manager = __esm({
462
+ "src/slack/channel-manager.ts"() {
463
+ "use strict";
464
+ ChannelManager = class {
465
+ channels = /* @__PURE__ */ new Map();
466
+ channelToSession = /* @__PURE__ */ new Map();
467
+ client;
468
+ userId;
469
+ constructor(client, userId) {
470
+ this.client = client;
471
+ this.userId = userId;
472
+ }
473
+ async createChannel(sessionId, sessionName, cwd) {
474
+ if (this.channels.has(sessionId)) {
475
+ return this.channels.get(sessionId);
476
+ }
477
+ const folderName = cwd.split("/").filter(Boolean).pop() || "session";
478
+ const baseName = `afk-${sanitizeChannelName(folderName)}`;
479
+ let channelName = baseName;
480
+ let suffix = 1;
481
+ let result;
482
+ while (true) {
483
+ const nameToTry = channelName.length > 80 ? channelName.slice(0, 80) : channelName;
484
+ try {
485
+ result = await this.client.conversations.create({
486
+ name: nameToTry,
487
+ is_private: true
488
+ });
489
+ channelName = nameToTry;
490
+ break;
491
+ } catch (err) {
492
+ if (err.data?.error === "name_taken") {
493
+ suffix++;
494
+ channelName = `${baseName}-${suffix}`;
495
+ } else {
496
+ throw err;
497
+ }
498
+ }
499
+ }
500
+ if (!result?.channel?.id) {
501
+ console.error("[ChannelManager] Failed to create channel - no ID returned");
502
+ return null;
503
+ }
504
+ const mapping = {
505
+ sessionId,
506
+ channelId: result.channel.id,
507
+ channelName,
508
+ sessionName,
509
+ status: "running",
510
+ createdAt: /* @__PURE__ */ new Date()
511
+ };
512
+ this.channels.set(sessionId, mapping);
513
+ this.channelToSession.set(result.channel.id, sessionId);
514
+ try {
515
+ await this.client.conversations.setTopic({
516
+ channel: result.channel.id,
517
+ topic: `Claude Code session: ${sessionName}`
518
+ });
519
+ } catch (err) {
520
+ console.error("[ChannelManager] Failed to set topic:", err.message);
521
+ }
522
+ if (this.userId) {
523
+ try {
524
+ await this.client.conversations.invite({
525
+ channel: result.channel.id,
526
+ users: this.userId
527
+ });
528
+ console.log(`[ChannelManager] Invited user to channel`);
529
+ } catch (err) {
530
+ if (err.data?.error !== "already_in_channel") {
531
+ console.error("[ChannelManager] Failed to invite user:", err.message);
532
+ }
533
+ }
534
+ }
535
+ console.log(`[ChannelManager] Created channel #${channelName} for session ${sessionId}`);
536
+ return mapping;
537
+ }
538
+ async archiveChannel(sessionId) {
539
+ const mapping = this.channels.get(sessionId);
540
+ if (!mapping) return false;
541
+ try {
542
+ const timestamp = Date.now().toString(36);
543
+ const archivedName = `${mapping.channelName}-archived-${timestamp}`.slice(0, 80);
544
+ await this.client.conversations.rename({
545
+ channel: mapping.channelId,
546
+ name: archivedName
547
+ });
548
+ await this.client.conversations.archive({
549
+ channel: mapping.channelId
550
+ });
551
+ console.log(`[ChannelManager] Archived channel #${mapping.channelName}`);
552
+ return true;
553
+ } catch (err) {
554
+ console.error("[ChannelManager] Failed to archive channel:", err.message);
555
+ return false;
556
+ }
557
+ }
558
+ getChannel(sessionId) {
559
+ return this.channels.get(sessionId);
560
+ }
561
+ getSessionByChannel(channelId) {
562
+ return this.channelToSession.get(channelId);
563
+ }
564
+ updateStatus(sessionId, status) {
565
+ const mapping = this.channels.get(sessionId);
566
+ if (mapping) {
567
+ mapping.status = status;
568
+ }
569
+ }
570
+ updateName(sessionId, name) {
571
+ const mapping = this.channels.get(sessionId);
572
+ if (mapping) {
573
+ mapping.sessionName = name;
574
+ }
575
+ }
576
+ getAllActive() {
577
+ return Array.from(this.channels.values()).filter((c) => c.status !== "ended");
578
+ }
579
+ };
580
+ }
581
+ });
582
+
583
+ // src/slack/message-formatter.ts
584
+ function markdownToSlack(markdown) {
585
+ let text = markdown;
586
+ text = text.replace(/\*\*(.+?)\*\*/g, "*$1*");
587
+ text = text.replace(/^#{1,6}\s+(.+)$/gm, "*$1*");
588
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "<$2|$1>");
589
+ text = text.replace(/~~(.+?)~~/g, "~$1~");
590
+ return text;
591
+ }
592
+ function chunkMessage(text, maxLength = 39e3) {
593
+ if (text.length <= maxLength) return [text];
594
+ const chunks = [];
595
+ let remaining = text;
596
+ while (remaining.length > 0) {
597
+ if (remaining.length <= maxLength) {
598
+ chunks.push(remaining);
599
+ break;
600
+ }
601
+ let breakPoint = remaining.lastIndexOf("\n", maxLength);
602
+ if (breakPoint === -1 || breakPoint < maxLength / 2) {
603
+ breakPoint = remaining.lastIndexOf(" ", maxLength);
604
+ }
605
+ if (breakPoint === -1 || breakPoint < maxLength / 2) {
606
+ breakPoint = maxLength;
607
+ }
608
+ chunks.push(remaining.slice(0, breakPoint));
609
+ remaining = remaining.slice(breakPoint).trimStart();
610
+ }
611
+ return chunks;
612
+ }
613
+ function formatSessionStatus(status) {
614
+ const icons = {
615
+ running: ":hourglass_flowing_sand:",
616
+ idle: ":white_check_mark:",
617
+ ended: ":stop_sign:"
618
+ };
619
+ const labels = {
620
+ running: "Running",
621
+ idle: "Idle",
622
+ ended: "Ended"
623
+ };
624
+ return `${icons[status]} ${labels[status]}`;
625
+ }
626
+ function formatTodos(todos) {
627
+ if (todos.length === 0) return "";
628
+ const icons = {
629
+ pending: ":white_circle:",
630
+ in_progress: ":large_blue_circle:",
631
+ completed: ":white_check_mark:"
632
+ };
633
+ return todos.map((t) => {
634
+ const icon = icons[t.status] || ":white_circle:";
635
+ const text = t.status === "in_progress" && t.activeForm ? t.activeForm : t.content;
636
+ return `${icon} ${text}`;
637
+ }).join("\n");
638
+ }
639
+ var init_message_formatter = __esm({
640
+ "src/slack/message-formatter.ts"() {
641
+ "use strict";
642
+ }
643
+ });
644
+
645
+ // src/utils/image-extractor.ts
646
+ import { existsSync, statSync } from "fs";
647
+ import { resolve } from "path";
648
+ import { homedir as homedir2 } from "os";
649
+ function extractImagePaths(content, cwd) {
650
+ const images = [];
651
+ const seen = /* @__PURE__ */ new Set();
652
+ PATH_PATTERN.lastIndex = 0;
653
+ let match;
654
+ while ((match = PATH_PATTERN.exec(content)) !== null) {
655
+ const originalPath = (match[1] || match[2]).trim();
656
+ if (!originalPath || seen.has(originalPath)) continue;
657
+ seen.add(originalPath);
658
+ let resolvedPath = originalPath;
659
+ if (resolvedPath.startsWith("~/")) {
660
+ resolvedPath = resolve(homedir2(), resolvedPath.slice(2));
661
+ } else if (resolvedPath.startsWith("./") || resolvedPath.startsWith("../")) {
662
+ resolvedPath = resolve(cwd || process.cwd(), resolvedPath);
663
+ } else if (!resolvedPath.startsWith("/")) {
664
+ continue;
665
+ }
666
+ try {
667
+ if (existsSync(resolvedPath)) {
668
+ const stat2 = statSync(resolvedPath);
669
+ if (stat2.isFile()) {
670
+ const ext = resolvedPath.toLowerCase().slice(resolvedPath.lastIndexOf("."));
671
+ if (IMAGE_EXTENSIONS.has(ext)) {
672
+ images.push({ originalPath, resolvedPath });
673
+ }
674
+ }
675
+ }
676
+ } catch {
677
+ }
678
+ }
679
+ return images;
680
+ }
681
+ var IMAGE_EXTENSIONS, PATH_PATTERN;
682
+ var init_image_extractor = __esm({
683
+ "src/utils/image-extractor.ts"() {
684
+ "use strict";
685
+ IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
686
+ ".png",
687
+ ".jpg",
688
+ ".jpeg",
689
+ ".gif",
690
+ ".webp",
691
+ ".svg",
692
+ ".bmp",
693
+ ".ico",
694
+ ".tiff",
695
+ ".tif"
696
+ ]);
697
+ PATH_PATTERN = /(?:["'`]([^"'`\n]+\.(?:png|jpe?g|gif|webp|svg|bmp|ico|tiff?))|(?:^|[\s(])([~./][^\s)"'`\n]*\.(?:png|jpe?g|gif|webp|svg|bmp|ico|tiff?)))/gi;
698
+ }
699
+ });
700
+
701
+ // src/slack/slack-app.ts
702
+ var slack_app_exports = {};
703
+ __export(slack_app_exports, {
704
+ createSlackApp: () => createSlackApp
705
+ });
706
+ import { App, LogLevel } from "@slack/bolt";
707
+ import { createReadStream } from "fs";
708
+ function createSlackApp(config) {
709
+ const app = new App({
710
+ token: config.botToken,
711
+ appToken: config.appToken,
712
+ socketMode: true,
713
+ logLevel: LogLevel.INFO
714
+ });
715
+ const channelManager = new ChannelManager(app.client, config.userId);
716
+ const messageQueue = new MessageQueue();
717
+ const slackSentMessages = /* @__PURE__ */ new Set();
718
+ const sessionManager = new SessionManager({
719
+ onSessionStart: async (session) => {
720
+ const channel = await channelManager.createChannel(session.id, session.name, session.cwd);
721
+ if (channel) {
722
+ await messageQueue.add(
723
+ () => app.client.chat.postMessage({
724
+ channel: channel.channelId,
725
+ text: `${formatSessionStatus(session.status)} *Session started*
726
+ \`${session.cwd}\``,
727
+ mrkdwn: true
728
+ })
729
+ );
730
+ }
731
+ },
732
+ onSessionEnd: async (sessionId) => {
733
+ const channel = channelManager.getChannel(sessionId);
734
+ if (channel) {
735
+ channelManager.updateStatus(sessionId, "ended");
736
+ await messageQueue.add(
737
+ () => app.client.chat.postMessage({
738
+ channel: channel.channelId,
739
+ text: ":stop_sign: *Session ended* - this channel will be archived"
740
+ })
741
+ );
742
+ await channelManager.archiveChannel(sessionId);
743
+ }
744
+ },
745
+ onSessionUpdate: async (sessionId, name) => {
746
+ const channel = channelManager.getChannel(sessionId);
747
+ if (channel) {
748
+ channelManager.updateName(sessionId, name);
749
+ try {
750
+ await app.client.conversations.setTopic({
751
+ channel: channel.channelId,
752
+ topic: `Claude Code session: ${name}`
753
+ });
754
+ } catch (err) {
755
+ console.error("[Slack] Failed to update channel topic:", err);
756
+ }
757
+ }
758
+ },
759
+ onSessionStatus: async (sessionId, status) => {
760
+ const channel = channelManager.getChannel(sessionId);
761
+ if (channel) {
762
+ channelManager.updateStatus(sessionId, status);
763
+ }
764
+ },
765
+ onMessage: async (sessionId, role, content) => {
766
+ const channel = channelManager.getChannel(sessionId);
767
+ if (channel) {
768
+ const formatted = markdownToSlack(content);
769
+ if (role === "user") {
770
+ const contentKey = content.trim();
771
+ if (slackSentMessages.has(contentKey)) {
772
+ slackSentMessages.delete(contentKey);
773
+ return;
774
+ }
775
+ const chunks = chunkMessage(formatted);
776
+ for (const chunk of chunks) {
777
+ try {
778
+ const userInfo = await app.client.users.info({ user: config.userId });
779
+ const userName = userInfo.user?.real_name || userInfo.user?.name || "User";
780
+ const userIcon = userInfo.user?.profile?.image_72;
781
+ await messageQueue.add(
782
+ () => app.client.chat.postMessage({
783
+ channel: channel.channelId,
784
+ text: chunk,
785
+ username: userName,
786
+ icon_url: userIcon,
787
+ mrkdwn: true
788
+ })
789
+ );
790
+ } catch (err) {
791
+ console.error("[Slack] Failed to post message:", err);
792
+ }
793
+ }
794
+ } else {
795
+ const chunks = chunkMessage(formatted);
796
+ for (const chunk of chunks) {
797
+ try {
798
+ await messageQueue.add(
799
+ () => app.client.chat.postMessage({
800
+ channel: channel.channelId,
801
+ text: chunk,
802
+ username: "Claude Code",
803
+ icon_url: "https://claude.ai/favicon.ico",
804
+ mrkdwn: true
805
+ })
806
+ );
807
+ } catch (err) {
808
+ console.error("[Slack] Failed to post message:", err);
809
+ }
810
+ }
811
+ const session = sessionManager.getSession(sessionId);
812
+ const images = extractImagePaths(content, session?.cwd);
813
+ for (const image of images) {
814
+ try {
815
+ console.log(`[Slack] Uploading image: ${image.resolvedPath}`);
816
+ await messageQueue.add(
817
+ () => app.client.files.uploadV2({
818
+ channel_id: channel.channelId,
819
+ file: createReadStream(image.resolvedPath),
820
+ filename: image.resolvedPath.split("/").pop() || "image",
821
+ initial_comment: `\u{1F4CE} ${image.originalPath}`
822
+ })
823
+ );
824
+ } catch (err) {
825
+ console.error("[Slack] Failed to upload image:", err);
826
+ }
827
+ }
828
+ }
829
+ }
830
+ },
831
+ onTodos: async (sessionId, todos) => {
832
+ const channel = channelManager.getChannel(sessionId);
833
+ if (channel && todos.length > 0) {
834
+ const todosText = formatTodos(todos);
835
+ try {
836
+ await messageQueue.add(
837
+ () => app.client.chat.postMessage({
838
+ channel: channel.channelId,
839
+ text: `*Tasks:*
840
+ ${todosText}`,
841
+ mrkdwn: true
842
+ })
843
+ );
844
+ } catch (err) {
845
+ console.error("[Slack] Failed to post todos:", err);
846
+ }
847
+ }
848
+ },
849
+ onToolCall: async (_sessionId, _tool) => {
850
+ },
851
+ onToolResult: async (_sessionId, _result) => {
852
+ },
853
+ onPlanModeChange: async (sessionId, inPlanMode) => {
854
+ const channel = channelManager.getChannel(sessionId);
855
+ if (!channel) return;
856
+ const emoji = inPlanMode ? ":clipboard:" : ":hammer:";
857
+ const status = inPlanMode ? "Planning mode - Claude is designing a solution" : "Execution mode - Claude is implementing";
858
+ try {
859
+ await messageQueue.add(
860
+ () => app.client.chat.postMessage({
861
+ channel: channel.channelId,
862
+ text: `${emoji} ${status}`,
863
+ mrkdwn: true
864
+ })
865
+ );
866
+ } catch (err) {
867
+ console.error("[Slack] Failed to post plan mode change:", err);
868
+ }
869
+ }
870
+ });
871
+ app.message(async ({ message, say }) => {
872
+ if ("subtype" in message && message.subtype) return;
873
+ if (!("text" in message) || !message.text) return;
874
+ if (!("channel" in message) || !message.channel) return;
875
+ if ("bot_id" in message && message.bot_id) return;
876
+ if ("thread_ts" in message && message.thread_ts) return;
877
+ const sessionId = channelManager.getSessionByChannel(message.channel);
878
+ if (!sessionId) return;
879
+ const channel = channelManager.getChannel(sessionId);
880
+ if (!channel || channel.status === "ended") {
881
+ await say(":warning: This session has ended.");
882
+ return;
883
+ }
884
+ console.log(`[Slack] Sending input to session ${sessionId}: ${message.text.slice(0, 50)}...`);
885
+ slackSentMessages.add(message.text.trim());
886
+ const sent = sessionManager.sendInput(sessionId, message.text);
887
+ if (!sent) {
888
+ slackSentMessages.delete(message.text.trim());
889
+ await say(":warning: Failed to send input - session not connected.");
890
+ }
891
+ });
892
+ app.command("/afk", async ({ command: command2, ack, respond }) => {
893
+ await ack();
894
+ const subcommand = command2.text.trim().split(" ")[0];
895
+ if (subcommand === "sessions" || !subcommand) {
896
+ const active = channelManager.getAllActive();
897
+ if (active.length === 0) {
898
+ await respond("No active sessions. Start a session with `afk-code run -- claude`");
899
+ return;
900
+ }
901
+ const text = active.map((c) => `<#${c.channelId}> - ${formatSessionStatus(c.status)}`).join("\n");
902
+ await respond({
903
+ text: `*Active Sessions:*
904
+ ${text}`,
905
+ mrkdwn: true
906
+ });
907
+ } else {
908
+ await respond("Unknown command. Available: `/afk sessions`");
909
+ }
910
+ });
911
+ app.command("/background", async ({ command: command2, ack, respond }) => {
912
+ await ack();
913
+ const sessionId = channelManager.getSessionByChannel(command2.channel_id);
914
+ if (!sessionId) {
915
+ await respond(":warning: This channel is not associated with an active session.");
916
+ return;
917
+ }
918
+ const channel = channelManager.getChannel(sessionId);
919
+ if (!channel || channel.status === "ended") {
920
+ await respond(":warning: This session has ended.");
921
+ return;
922
+ }
923
+ const sent = sessionManager.sendInput(sessionId, "");
924
+ if (sent) {
925
+ await respond(":arrow_heading_down: Sent background command (Ctrl+B)");
926
+ } else {
927
+ await respond(":warning: Failed to send command - session not connected.");
928
+ }
929
+ });
930
+ app.command("/interrupt", async ({ command: command2, ack, respond }) => {
931
+ await ack();
932
+ const sessionId = channelManager.getSessionByChannel(command2.channel_id);
933
+ if (!sessionId) {
934
+ await respond(":warning: This channel is not associated with an active session.");
935
+ return;
936
+ }
937
+ const channel = channelManager.getChannel(sessionId);
938
+ if (!channel || channel.status === "ended") {
939
+ await respond(":warning: This session has ended.");
940
+ return;
941
+ }
942
+ const sent = sessionManager.sendInput(sessionId, "\x1B");
943
+ if (sent) {
944
+ await respond(":stop_sign: Sent interrupt (Escape)");
945
+ } else {
946
+ await respond(":warning: Failed to send command - session not connected.");
947
+ }
948
+ });
949
+ app.command("/mode", async ({ command: command2, ack, respond }) => {
950
+ await ack();
951
+ const sessionId = channelManager.getSessionByChannel(command2.channel_id);
952
+ if (!sessionId) {
953
+ await respond(":warning: This channel is not associated with an active session.");
954
+ return;
955
+ }
956
+ const channel = channelManager.getChannel(sessionId);
957
+ if (!channel || channel.status === "ended") {
958
+ await respond(":warning: This session has ended.");
959
+ return;
960
+ }
961
+ const sent = sessionManager.sendInput(sessionId, "\x1B[Z");
962
+ if (sent) {
963
+ await respond(":arrows_counterclockwise: Sent mode toggle (Shift+Tab)");
964
+ } else {
965
+ await respond(":warning: Failed to send command - session not connected.");
966
+ }
967
+ });
968
+ app.event("app_home_opened", async ({ event, client }) => {
969
+ const active = channelManager.getAllActive();
970
+ const blocks = [
971
+ {
972
+ type: "header",
973
+ text: { type: "plain_text", text: "AFK Code Sessions", emoji: true }
974
+ },
975
+ { type: "divider" }
976
+ ];
977
+ if (active.length === 0) {
978
+ blocks.push({
979
+ type: "section",
980
+ text: {
981
+ type: "mrkdwn",
982
+ text: "_No active sessions_\n\nStart a session with `afk-code run -- claude`"
983
+ }
984
+ });
985
+ } else {
986
+ for (const c of active) {
987
+ blocks.push({
988
+ type: "section",
989
+ text: {
990
+ type: "mrkdwn",
991
+ text: `*${c.sessionName}*
992
+ ${formatSessionStatus(c.status)}
993
+ <#${c.channelId}>`
994
+ }
995
+ });
996
+ }
997
+ }
998
+ try {
999
+ await client.views.publish({
1000
+ user_id: event.user,
1001
+ view: {
1002
+ type: "home",
1003
+ blocks
1004
+ }
1005
+ });
1006
+ } catch (err) {
1007
+ console.error("[Slack] Failed to publish home view:", err);
1008
+ }
1009
+ });
1010
+ return { app, sessionManager, channelManager };
1011
+ }
1012
+ var MessageQueue;
1013
+ var init_slack_app = __esm({
1014
+ "src/slack/slack-app.ts"() {
1015
+ "use strict";
1016
+ init_session_manager();
1017
+ init_channel_manager();
1018
+ init_message_formatter();
1019
+ init_image_extractor();
1020
+ MessageQueue = class {
1021
+ queue = [];
1022
+ processing = false;
1023
+ minDelay = 350;
1024
+ // ms between messages (Slack allows ~1/sec but be safe)
1025
+ async add(fn) {
1026
+ return new Promise((resolve2, reject) => {
1027
+ this.queue.push(async () => {
1028
+ try {
1029
+ const result = await fn();
1030
+ resolve2(result);
1031
+ } catch (err) {
1032
+ reject(err);
1033
+ }
1034
+ });
1035
+ this.process();
1036
+ });
1037
+ }
1038
+ async process() {
1039
+ if (this.processing) return;
1040
+ this.processing = true;
1041
+ while (this.queue.length > 0) {
1042
+ const fn = this.queue.shift();
1043
+ if (fn) {
1044
+ await fn();
1045
+ if (this.queue.length > 0) {
1046
+ await new Promise((r) => setTimeout(r, this.minDelay));
1047
+ }
1048
+ }
1049
+ }
1050
+ this.processing = false;
1051
+ }
1052
+ };
1053
+ }
1054
+ });
1055
+
1056
+ // src/discord/channel-manager.ts
1057
+ import { ChannelType } from "discord.js";
1058
+ function sanitizeChannelName2(name) {
1059
+ return name.toLowerCase().replace(/[^a-z0-9-_\s]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 90);
1060
+ }
1061
+ var ChannelManager2;
1062
+ var init_channel_manager2 = __esm({
1063
+ "src/discord/channel-manager.ts"() {
1064
+ "use strict";
1065
+ ChannelManager2 = class {
1066
+ channels = /* @__PURE__ */ new Map();
1067
+ channelToSession = /* @__PURE__ */ new Map();
1068
+ client;
1069
+ userId;
1070
+ guild = null;
1071
+ category = null;
1072
+ constructor(client, userId) {
1073
+ this.client = client;
1074
+ this.userId = userId;
1075
+ }
1076
+ async initialize() {
1077
+ const guilds = await this.client.guilds.fetch();
1078
+ if (guilds.size === 0) {
1079
+ throw new Error("Bot is not in any servers. Please invite the bot first.");
1080
+ }
1081
+ const guildId = guilds.first().id;
1082
+ this.guild = await this.client.guilds.fetch(guildId);
1083
+ const existingCategory = this.guild.channels.cache.find(
1084
+ (ch) => ch.type === ChannelType.GuildCategory && ch.name.toLowerCase() === "afk code sessions"
1085
+ );
1086
+ if (existingCategory) {
1087
+ this.category = existingCategory;
1088
+ } else {
1089
+ this.category = await this.guild.channels.create({
1090
+ name: "AFK Code Sessions",
1091
+ type: ChannelType.GuildCategory
1092
+ });
1093
+ }
1094
+ console.log(`[ChannelManager] Using guild: ${this.guild.name}`);
1095
+ console.log(`[ChannelManager] Using category: ${this.category.name}`);
1096
+ }
1097
+ async createChannel(sessionId, sessionName, cwd) {
1098
+ if (!this.guild || !this.category) {
1099
+ console.error("[ChannelManager] Not initialized");
1100
+ return null;
1101
+ }
1102
+ if (this.channels.has(sessionId)) {
1103
+ return this.channels.get(sessionId);
1104
+ }
1105
+ const folderName = cwd.split("/").filter(Boolean).pop() || "session";
1106
+ const baseName = `afk-${sanitizeChannelName2(folderName)}`;
1107
+ let channelName = baseName;
1108
+ let suffix = 1;
1109
+ let channel = null;
1110
+ while (true) {
1111
+ const nameToTry = channelName.length > 100 ? channelName.slice(0, 100) : channelName;
1112
+ const existing = this.guild.channels.cache.find(
1113
+ (ch) => ch.name === nameToTry && ch.parentId === this.category.id
1114
+ );
1115
+ if (!existing) {
1116
+ try {
1117
+ channel = await this.guild.channels.create({
1118
+ name: nameToTry,
1119
+ type: ChannelType.GuildText,
1120
+ parent: this.category,
1121
+ topic: `Claude Code session: ${sessionName}`
1122
+ });
1123
+ channelName = nameToTry;
1124
+ break;
1125
+ } catch (err) {
1126
+ console.error("[ChannelManager] Failed to create channel:", err.message);
1127
+ return null;
1128
+ }
1129
+ } else {
1130
+ suffix++;
1131
+ channelName = `${baseName}-${suffix}`;
1132
+ }
1133
+ }
1134
+ if (!channel) {
1135
+ return null;
1136
+ }
1137
+ const mapping = {
1138
+ sessionId,
1139
+ channelId: channel.id,
1140
+ channelName,
1141
+ sessionName,
1142
+ status: "running",
1143
+ createdAt: /* @__PURE__ */ new Date()
1144
+ };
1145
+ this.channels.set(sessionId, mapping);
1146
+ this.channelToSession.set(channel.id, sessionId);
1147
+ console.log(`[ChannelManager] Created channel #${channelName} for session ${sessionId}`);
1148
+ return mapping;
1149
+ }
1150
+ async archiveChannel(sessionId) {
1151
+ if (!this.guild) return false;
1152
+ const mapping = this.channels.get(sessionId);
1153
+ if (!mapping) return false;
1154
+ try {
1155
+ const channel = await this.guild.channels.fetch(mapping.channelId);
1156
+ if (channel && channel.type === ChannelType.GuildText) {
1157
+ const timestamp = Date.now().toString(36);
1158
+ const archivedName = `${mapping.channelName}-archived-${timestamp}`.slice(0, 100);
1159
+ await channel.setName(archivedName);
1160
+ console.log(`[ChannelManager] Archived channel #${mapping.channelName}`);
1161
+ }
1162
+ return true;
1163
+ } catch (err) {
1164
+ console.error("[ChannelManager] Failed to archive channel:", err.message);
1165
+ return false;
1166
+ }
1167
+ }
1168
+ getChannel(sessionId) {
1169
+ return this.channels.get(sessionId);
1170
+ }
1171
+ getSessionByChannel(channelId) {
1172
+ return this.channelToSession.get(channelId);
1173
+ }
1174
+ updateStatus(sessionId, status) {
1175
+ const mapping = this.channels.get(sessionId);
1176
+ if (mapping) {
1177
+ mapping.status = status;
1178
+ }
1179
+ }
1180
+ updateName(sessionId, name) {
1181
+ const mapping = this.channels.get(sessionId);
1182
+ if (mapping) {
1183
+ mapping.sessionName = name;
1184
+ }
1185
+ }
1186
+ getAllActive() {
1187
+ return Array.from(this.channels.values()).filter((c) => c.status !== "ended");
1188
+ }
1189
+ };
1190
+ }
1191
+ });
1192
+
1193
+ // src/discord/discord-app.ts
1194
+ var discord_app_exports = {};
1195
+ __export(discord_app_exports, {
1196
+ createDiscordApp: () => createDiscordApp
1197
+ });
1198
+ import { Client, GatewayIntentBits, Events, ChannelType as ChannelType2, AttachmentBuilder, REST, Routes, SlashCommandBuilder } from "discord.js";
1199
+ function createDiscordApp(config) {
1200
+ const client = new Client({
1201
+ intents: [
1202
+ GatewayIntentBits.Guilds,
1203
+ GatewayIntentBits.GuildMessages,
1204
+ GatewayIntentBits.MessageContent
1205
+ ]
1206
+ });
1207
+ const channelManager = new ChannelManager2(client, config.userId);
1208
+ const discordSentMessages = /* @__PURE__ */ new Set();
1209
+ const toolCallMessages = /* @__PURE__ */ new Map();
1210
+ const sessionManager = new SessionManager({
1211
+ onSessionStart: async (session) => {
1212
+ const channel = await channelManager.createChannel(session.id, session.name, session.cwd);
1213
+ if (channel) {
1214
+ const discordChannel = await client.channels.fetch(channel.channelId);
1215
+ if (discordChannel?.type === ChannelType2.GuildText) {
1216
+ await discordChannel.send(
1217
+ `${formatSessionStatus(session.status)} **Session started**
1218
+ \`${session.cwd}\``
1219
+ );
1220
+ }
1221
+ }
1222
+ },
1223
+ onSessionEnd: async (sessionId) => {
1224
+ const channel = channelManager.getChannel(sessionId);
1225
+ if (channel) {
1226
+ channelManager.updateStatus(sessionId, "ended");
1227
+ const discordChannel = await client.channels.fetch(channel.channelId);
1228
+ if (discordChannel?.type === ChannelType2.GuildText) {
1229
+ await discordChannel.send("\u{1F6D1} **Session ended** - this channel will be archived");
1230
+ }
1231
+ await channelManager.archiveChannel(sessionId);
1232
+ }
1233
+ },
1234
+ onSessionUpdate: async (sessionId, name) => {
1235
+ const channel = channelManager.getChannel(sessionId);
1236
+ if (channel) {
1237
+ channelManager.updateName(sessionId, name);
1238
+ try {
1239
+ const discordChannel = await client.channels.fetch(channel.channelId);
1240
+ if (discordChannel?.type === ChannelType2.GuildText) {
1241
+ await discordChannel.setTopic(`Claude Code session: ${name}`);
1242
+ }
1243
+ } catch (err) {
1244
+ console.error("[Discord] Failed to update channel topic:", err);
1245
+ }
1246
+ }
1247
+ },
1248
+ onSessionStatus: async (sessionId, status) => {
1249
+ const channel = channelManager.getChannel(sessionId);
1250
+ if (channel) {
1251
+ channelManager.updateStatus(sessionId, status);
1252
+ }
1253
+ },
1254
+ onMessage: async (sessionId, role, content) => {
1255
+ const channel = channelManager.getChannel(sessionId);
1256
+ if (channel) {
1257
+ const formatted = content;
1258
+ if (role === "user") {
1259
+ const contentKey = content.trim();
1260
+ if (discordSentMessages.has(contentKey)) {
1261
+ discordSentMessages.delete(contentKey);
1262
+ return;
1263
+ }
1264
+ const discordChannel = await client.channels.fetch(channel.channelId);
1265
+ if (discordChannel?.type === ChannelType2.GuildText) {
1266
+ const chunks = chunkMessage(formatted);
1267
+ for (const chunk of chunks) {
1268
+ await discordChannel.send(`**User:** ${chunk}`);
1269
+ }
1270
+ }
1271
+ } else {
1272
+ const discordChannel = await client.channels.fetch(channel.channelId);
1273
+ if (discordChannel?.type === ChannelType2.GuildText) {
1274
+ const chunks = chunkMessage(formatted);
1275
+ for (const chunk of chunks) {
1276
+ await discordChannel.send(chunk);
1277
+ }
1278
+ const session = sessionManager.getSession(sessionId);
1279
+ const images = extractImagePaths(content, session?.cwd);
1280
+ for (const image of images) {
1281
+ try {
1282
+ console.log(`[Discord] Uploading image: ${image.resolvedPath}`);
1283
+ const attachment = new AttachmentBuilder(image.resolvedPath);
1284
+ await discordChannel.send({
1285
+ content: `\u{1F4CE} ${image.originalPath}`,
1286
+ files: [attachment]
1287
+ });
1288
+ } catch (err) {
1289
+ console.error("[Discord] Failed to upload image:", err);
1290
+ }
1291
+ }
1292
+ }
1293
+ }
1294
+ }
1295
+ },
1296
+ onTodos: async (sessionId, todos) => {
1297
+ const channel = channelManager.getChannel(sessionId);
1298
+ if (channel && todos.length > 0) {
1299
+ const todosText = formatTodos(todos);
1300
+ try {
1301
+ const discordChannel = await client.channels.fetch(channel.channelId);
1302
+ if (discordChannel?.type === ChannelType2.GuildText) {
1303
+ await discordChannel.send(`**Tasks:**
1304
+ ${todosText}`);
1305
+ }
1306
+ } catch (err) {
1307
+ console.error("[Discord] Failed to post todos:", err);
1308
+ }
1309
+ }
1310
+ },
1311
+ onToolCall: async (sessionId, tool) => {
1312
+ const channel = channelManager.getChannel(sessionId);
1313
+ if (!channel) return;
1314
+ let inputSummary = "";
1315
+ if (tool.name === "Bash" && tool.input.command) {
1316
+ inputSummary = `\`${tool.input.command.slice(0, 100)}${tool.input.command.length > 100 ? "..." : ""}\``;
1317
+ } else if (tool.name === "Read" && tool.input.file_path) {
1318
+ inputSummary = `\`${tool.input.file_path}\``;
1319
+ } else if (tool.name === "Edit" && tool.input.file_path) {
1320
+ inputSummary = `\`${tool.input.file_path}\``;
1321
+ } else if (tool.name === "Write" && tool.input.file_path) {
1322
+ inputSummary = `\`${tool.input.file_path}\``;
1323
+ } else if (tool.name === "Grep" && tool.input.pattern) {
1324
+ inputSummary = `\`${tool.input.pattern}\``;
1325
+ } else if (tool.name === "Glob" && tool.input.pattern) {
1326
+ inputSummary = `\`${tool.input.pattern}\``;
1327
+ } else if (tool.name === "Task" && tool.input.description) {
1328
+ inputSummary = tool.input.description;
1329
+ }
1330
+ const text = inputSummary ? `\u{1F527} **${tool.name}**: ${inputSummary}` : `\u{1F527} **${tool.name}**`;
1331
+ try {
1332
+ const discordChannel = await client.channels.fetch(channel.channelId);
1333
+ if (discordChannel?.type === ChannelType2.GuildText) {
1334
+ const message = await discordChannel.send(text);
1335
+ toolCallMessages.set(tool.id, message.id);
1336
+ }
1337
+ } catch (err) {
1338
+ console.error("[Discord] Failed to post tool call:", err);
1339
+ }
1340
+ },
1341
+ onToolResult: async (sessionId, result) => {
1342
+ const channel = channelManager.getChannel(sessionId);
1343
+ if (!channel) return;
1344
+ const parentMessageId = toolCallMessages.get(result.toolUseId);
1345
+ if (!parentMessageId) return;
1346
+ const maxLen = 1800;
1347
+ let content = result.content;
1348
+ if (content.length > maxLen) {
1349
+ content = content.slice(0, maxLen) + "\n... (truncated)";
1350
+ }
1351
+ const prefix = result.isError ? "\u274C Error:" : "\u2705 Result:";
1352
+ const text = `${prefix}
1353
+ \`\`\`
1354
+ ${content}
1355
+ \`\`\``;
1356
+ try {
1357
+ const discordChannel = await client.channels.fetch(channel.channelId);
1358
+ if (discordChannel?.type === ChannelType2.GuildText) {
1359
+ const parentMessage = await discordChannel.messages.fetch(parentMessageId);
1360
+ if (parentMessage) {
1361
+ let thread = parentMessage.thread;
1362
+ if (!thread) {
1363
+ thread = await parentMessage.startThread({
1364
+ name: "Result",
1365
+ autoArchiveDuration: 60
1366
+ });
1367
+ }
1368
+ await thread.send(text);
1369
+ }
1370
+ toolCallMessages.delete(result.toolUseId);
1371
+ }
1372
+ } catch (err) {
1373
+ console.error("[Discord] Failed to post tool result:", err);
1374
+ }
1375
+ },
1376
+ onPlanModeChange: async (sessionId, inPlanMode) => {
1377
+ const channel = channelManager.getChannel(sessionId);
1378
+ if (!channel) return;
1379
+ const emoji = inPlanMode ? "\u{1F4CB}" : "\u{1F528}";
1380
+ const status = inPlanMode ? "Planning mode - Claude is designing a solution" : "Execution mode - Claude is implementing";
1381
+ try {
1382
+ const discordChannel = await client.channels.fetch(channel.channelId);
1383
+ if (discordChannel?.type === ChannelType2.GuildText) {
1384
+ await discordChannel.send(`${emoji} ${status}`);
1385
+ }
1386
+ } catch (err) {
1387
+ console.error("[Discord] Failed to post plan mode change:", err);
1388
+ }
1389
+ }
1390
+ });
1391
+ client.on(Events.MessageCreate, async (message) => {
1392
+ if (message.author.bot) return;
1393
+ if (!message.guild) return;
1394
+ const sessionId = channelManager.getSessionByChannel(message.channelId);
1395
+ if (!sessionId) return;
1396
+ const channel = channelManager.getChannel(sessionId);
1397
+ if (!channel || channel.status === "ended") {
1398
+ await message.reply("\u26A0\uFE0F This session has ended.");
1399
+ return;
1400
+ }
1401
+ console.log(`[Discord] Sending input to session ${sessionId}: ${message.content.slice(0, 50)}...`);
1402
+ discordSentMessages.add(message.content.trim());
1403
+ const sent = sessionManager.sendInput(sessionId, message.content);
1404
+ if (!sent) {
1405
+ discordSentMessages.delete(message.content.trim());
1406
+ await message.reply("\u26A0\uFE0F Failed to send input - session not connected.");
1407
+ }
1408
+ });
1409
+ client.once(Events.ClientReady, async (c) => {
1410
+ console.log(`[Discord] Logged in as ${c.user.tag}`);
1411
+ await channelManager.initialize();
1412
+ const commands = [
1413
+ new SlashCommandBuilder().setName("background").setDescription("Send Claude to background mode (Ctrl+B)"),
1414
+ new SlashCommandBuilder().setName("interrupt").setDescription("Interrupt Claude (Escape)"),
1415
+ new SlashCommandBuilder().setName("mode").setDescription("Toggle Claude mode (Shift+Tab)"),
1416
+ new SlashCommandBuilder().setName("afk").setDescription("List active Claude Code sessions")
1417
+ ];
1418
+ try {
1419
+ const rest = new REST({ version: "10" }).setToken(config.botToken);
1420
+ await rest.put(Routes.applicationCommands(c.user.id), {
1421
+ body: commands.map((cmd) => cmd.toJSON())
1422
+ });
1423
+ console.log("[Discord] Slash commands registered");
1424
+ } catch (err) {
1425
+ console.error("[Discord] Failed to register slash commands:", err);
1426
+ }
1427
+ });
1428
+ client.on(Events.InteractionCreate, async (interaction) => {
1429
+ if (!interaction.isChatInputCommand()) return;
1430
+ const { commandName, channelId } = interaction;
1431
+ if (commandName === "afk") {
1432
+ const active = channelManager.getAllActive();
1433
+ if (active.length === 0) {
1434
+ await interaction.reply("No active sessions. Start a session with `afk-code run -- claude`");
1435
+ return;
1436
+ }
1437
+ const text = active.map((c) => `<#${c.channelId}> - ${formatSessionStatus(c.status)}`).join("\n");
1438
+ await interaction.reply(`**Active Sessions:**
1439
+ ${text}`);
1440
+ return;
1441
+ }
1442
+ if (commandName === "background" || commandName === "interrupt" || commandName === "mode") {
1443
+ const sessionId = channelManager.getSessionByChannel(channelId);
1444
+ if (!sessionId) {
1445
+ await interaction.reply("\u26A0\uFE0F This channel is not associated with an active session.");
1446
+ return;
1447
+ }
1448
+ const channel = channelManager.getChannel(sessionId);
1449
+ if (!channel || channel.status === "ended") {
1450
+ await interaction.reply("\u26A0\uFE0F This session has ended.");
1451
+ return;
1452
+ }
1453
+ let key;
1454
+ let message;
1455
+ if (commandName === "background") {
1456
+ key = "";
1457
+ message = "\u2B07\uFE0F Sent background command (Ctrl+B)";
1458
+ } else if (commandName === "interrupt") {
1459
+ key = "\x1B";
1460
+ message = "\u{1F6D1} Sent interrupt (Escape)";
1461
+ } else {
1462
+ key = "\x1B[Z";
1463
+ message = "\u{1F504} Sent mode toggle (Shift+Tab)";
1464
+ }
1465
+ const sent = sessionManager.sendInput(sessionId, key);
1466
+ if (sent) {
1467
+ await interaction.reply(message);
1468
+ } else {
1469
+ await interaction.reply("\u26A0\uFE0F Failed to send command - session not connected.");
1470
+ }
1471
+ }
1472
+ });
1473
+ return { client, sessionManager, channelManager };
1474
+ }
1475
+ var init_discord_app = __esm({
1476
+ "src/discord/discord-app.ts"() {
1477
+ "use strict";
1478
+ init_session_manager();
1479
+ init_channel_manager2();
1480
+ init_message_formatter();
1481
+ init_image_extractor();
1482
+ }
1483
+ });
1484
+
1485
+ // src/cli/run.ts
1486
+ import { randomUUID } from "crypto";
1487
+ import { homedir } from "os";
1488
+ import { createConnection } from "net";
1489
+ import * as pty from "node-pty";
1490
+ var DAEMON_SOCKET = "/tmp/afk-code-daemon.sock";
1491
+ function getClaudeProjectDir(cwd) {
1492
+ const encodedPath = cwd.replace(/\//g, "-");
1493
+ return `${homedir()}/.claude/projects/${encodedPath}`;
1494
+ }
1495
+ function connectToDaemon(sessionId, projectDir, cwd, command2, onInput) {
1496
+ return new Promise((resolve2) => {
1497
+ const socket = createConnection(DAEMON_SOCKET);
1498
+ let messageBuffer = "";
1499
+ socket.on("connect", () => {
1500
+ socket.write(JSON.stringify({
1501
+ type: "session_start",
1502
+ id: sessionId,
1503
+ projectDir,
1504
+ cwd,
1505
+ command: command2,
1506
+ name: command2.join(" ")
1507
+ }) + "\n");
1508
+ resolve2({
1509
+ close: () => {
1510
+ socket.write(JSON.stringify({ type: "session_end", sessionId }) + "\n");
1511
+ socket.end();
1512
+ }
1513
+ });
1514
+ });
1515
+ socket.on("data", (data) => {
1516
+ messageBuffer += data.toString();
1517
+ const lines = messageBuffer.split("\n");
1518
+ messageBuffer = lines.pop() || "";
1519
+ for (const line of lines) {
1520
+ if (!line.trim()) continue;
1521
+ try {
1522
+ const msg = JSON.parse(line);
1523
+ if (msg.type === "input" && msg.text) {
1524
+ onInput(msg.text);
1525
+ }
1526
+ } catch {
1527
+ }
1528
+ }
1529
+ });
1530
+ socket.on("error", (error) => {
1531
+ resolve2(null);
1532
+ });
1533
+ });
1534
+ }
1535
+ async function run(command2) {
1536
+ const sessionId = randomUUID().slice(0, 8);
1537
+ const cwd = process.cwd();
1538
+ const projectDir = getClaudeProjectDir(cwd);
1539
+ const cols = process.stdout.columns || 80;
1540
+ const rows = process.stdout.rows || 24;
1541
+ const ptyProcess = pty.spawn(command2[0], command2.slice(1), {
1542
+ name: process.env.TERM || "xterm-256color",
1543
+ cols,
1544
+ rows,
1545
+ cwd,
1546
+ env: process.env
1547
+ });
1548
+ const daemon = await connectToDaemon(
1549
+ sessionId,
1550
+ projectDir,
1551
+ cwd,
1552
+ command2,
1553
+ (text) => {
1554
+ ptyProcess.write(text);
1555
+ }
1556
+ );
1557
+ if (process.stdin.isTTY) {
1558
+ process.stdin.setRawMode(true);
1559
+ }
1560
+ ptyProcess.onData((data) => {
1561
+ process.stdout.write(data);
1562
+ });
1563
+ const onStdinData = (data) => {
1564
+ ptyProcess.write(data.toString());
1565
+ };
1566
+ process.stdin.on("data", onStdinData);
1567
+ process.stdout.on("resize", () => {
1568
+ ptyProcess.resize(process.stdout.columns || 80, process.stdout.rows || 24);
1569
+ });
1570
+ await new Promise((resolve2) => {
1571
+ ptyProcess.onExit(() => {
1572
+ process.stdin.removeListener("data", onStdinData);
1573
+ if (process.stdin.isTTY) {
1574
+ process.stdin.setRawMode(false);
1575
+ }
1576
+ process.stdin.unref();
1577
+ daemon?.close();
1578
+ resolve2();
1579
+ });
1580
+ });
1581
+ }
1582
+
1583
+ // src/cli/slack.ts
1584
+ import { homedir as homedir3 } from "os";
1585
+ import { mkdir, writeFile, readFile as readFile2, access } from "fs/promises";
1586
+ import * as readline from "readline";
1587
+ var CONFIG_DIR = `${homedir3()}/.afk-code`;
1588
+ var SLACK_CONFIG_FILE = `${CONFIG_DIR}/slack.env`;
1589
+ var MANIFEST_URL = "https://github.com/clharman/afk-code/blob/main/slack-manifest.json";
1590
+ function prompt(question) {
1591
+ const rl = readline.createInterface({
1592
+ input: process.stdin,
1593
+ output: process.stdout
1594
+ });
1595
+ return new Promise((resolve2) => {
1596
+ rl.question(question, (answer) => {
1597
+ rl.close();
1598
+ resolve2(answer.trim());
1599
+ });
1600
+ });
1601
+ }
1602
+ async function fileExists(path) {
1603
+ try {
1604
+ await access(path);
1605
+ return true;
1606
+ } catch {
1607
+ return false;
1608
+ }
1609
+ }
1610
+ async function slackSetup() {
1611
+ console.log(`
1612
+ \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
1613
+ \u2502 AFK Code Slack Setup \u2502
1614
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
1615
+
1616
+ This will guide you through setting up the Slack bot for
1617
+ monitoring Claude Code sessions.
1618
+
1619
+ Step 1: Create a Slack App
1620
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1621
+ 1. Go to: https://api.slack.com/apps
1622
+ 2. Click "Create New App" \u2192 "From manifest"
1623
+ 3. Select your workspace
1624
+ 4. Paste the manifest from: ${MANIFEST_URL}
1625
+ (Or copy from slack-manifest.json in this repo)
1626
+ 5. Click "Create"
1627
+
1628
+ Step 2: Install the App
1629
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1630
+ 1. Go to "Install App" in the sidebar
1631
+ 2. Click "Install to Workspace"
1632
+ 3. Authorize the app
1633
+
1634
+ Step 3: Get Your Tokens
1635
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1636
+ `);
1637
+ await prompt("Press Enter when you have created and installed the app...");
1638
+ console.log(`
1639
+ Now let's collect your tokens:
1640
+
1641
+ \u2022 Bot Token: "OAuth & Permissions" \u2192 "Bot User OAuth Token" (starts with xoxb-)
1642
+ \u2022 App Token: "Basic Information" \u2192 "App-Level Tokens" \u2192 Generate one with
1643
+ "connections:write" scope (starts with xapp-)
1644
+ \u2022 User ID: Click your profile in Slack \u2192 "..." \u2192 "Copy member ID"
1645
+ `);
1646
+ const botToken = await prompt("Bot Token (xoxb-...): ");
1647
+ if (!botToken.startsWith("xoxb-")) {
1648
+ console.error("Invalid bot token. Should start with xoxb-");
1649
+ process.exit(1);
1650
+ }
1651
+ const appToken = await prompt("App Token (xapp-...): ");
1652
+ if (!appToken.startsWith("xapp-")) {
1653
+ console.error("Invalid app token. Should start with xapp-");
1654
+ process.exit(1);
1655
+ }
1656
+ const userId = await prompt("Your Slack User ID (U...): ");
1657
+ if (!userId.startsWith("U")) {
1658
+ console.error("Invalid user ID. Should start with U");
1659
+ process.exit(1);
1660
+ }
1661
+ await mkdir(CONFIG_DIR, { recursive: true });
1662
+ const envContent = `# AFK Code Slack Configuration
1663
+ SLACK_BOT_TOKEN=${botToken}
1664
+ SLACK_APP_TOKEN=${appToken}
1665
+ SLACK_USER_ID=${userId}
1666
+ `;
1667
+ await writeFile(SLACK_CONFIG_FILE, envContent);
1668
+ console.log(`
1669
+ \u2713 Configuration saved to ${SLACK_CONFIG_FILE}
1670
+
1671
+ To start the Slack bot, run:
1672
+ afk-code slack
1673
+
1674
+ Then start a Claude Code session with:
1675
+ afk-code run -- claude
1676
+ `);
1677
+ }
1678
+ async function loadEnvFile(path) {
1679
+ if (!await fileExists(path)) return {};
1680
+ const content = await readFile2(path, "utf-8");
1681
+ const config = {};
1682
+ for (const line of content.split("\n")) {
1683
+ if (line.startsWith("#") || !line.includes("=")) continue;
1684
+ const [key, ...valueParts] = line.split("=");
1685
+ config[key.trim()] = valueParts.join("=").trim();
1686
+ }
1687
+ return config;
1688
+ }
1689
+ async function slackRun() {
1690
+ const globalConfig = await loadEnvFile(SLACK_CONFIG_FILE);
1691
+ const localConfig = await loadEnvFile(`${process.cwd()}/.env`);
1692
+ const config = {
1693
+ ...globalConfig,
1694
+ ...localConfig
1695
+ };
1696
+ if (process.env.SLACK_BOT_TOKEN) config.SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN;
1697
+ if (process.env.SLACK_APP_TOKEN) config.SLACK_APP_TOKEN = process.env.SLACK_APP_TOKEN;
1698
+ if (process.env.SLACK_USER_ID) config.SLACK_USER_ID = process.env.SLACK_USER_ID;
1699
+ const required = ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN", "SLACK_USER_ID"];
1700
+ const missing = required.filter((key) => !config[key]);
1701
+ if (missing.length > 0) {
1702
+ console.error(`Missing config: ${missing.join(", ")}`);
1703
+ console.error("");
1704
+ console.error("Provide tokens via:");
1705
+ console.error(" - Environment variables (SLACK_BOT_TOKEN, SLACK_APP_TOKEN, SLACK_USER_ID)");
1706
+ console.error(" - Local .env file");
1707
+ console.error(' - Run "afk-code slack setup" for guided configuration');
1708
+ process.exit(1);
1709
+ }
1710
+ process.env.SLACK_BOT_TOKEN = config.SLACK_BOT_TOKEN;
1711
+ process.env.SLACK_APP_TOKEN = config.SLACK_APP_TOKEN;
1712
+ process.env.SLACK_USER_ID = config.SLACK_USER_ID;
1713
+ const { createSlackApp: createSlackApp2 } = await Promise.resolve().then(() => (init_slack_app(), slack_app_exports));
1714
+ const localEnvExists = await fileExists(`${process.cwd()}/.env`);
1715
+ const globalEnvExists = await fileExists(SLACK_CONFIG_FILE);
1716
+ const source = localEnvExists ? ".env" : globalEnvExists ? SLACK_CONFIG_FILE : "environment";
1717
+ console.log(`[AFK Code] Loaded config from ${source}`);
1718
+ console.log("[AFK Code] Starting Slack bot...");
1719
+ const slackConfig = {
1720
+ botToken: config.SLACK_BOT_TOKEN,
1721
+ appToken: config.SLACK_APP_TOKEN,
1722
+ signingSecret: "",
1723
+ userId: config.SLACK_USER_ID
1724
+ };
1725
+ const { app, sessionManager } = createSlackApp2(slackConfig);
1726
+ try {
1727
+ await sessionManager.start();
1728
+ console.log("[AFK Code] Session manager started");
1729
+ } catch (err) {
1730
+ console.error("[AFK Code] Failed to start session manager:", err);
1731
+ process.exit(1);
1732
+ }
1733
+ try {
1734
+ await app.start();
1735
+ console.log("[AFK Code] Slack bot is running!");
1736
+ console.log("");
1737
+ console.log("Start a Claude Code session with: afk-code run -- claude");
1738
+ console.log("Each session will create a private #afk-* channel");
1739
+ } catch (err) {
1740
+ console.error("[AFK Code] Failed to start Slack app:", err);
1741
+ process.exit(1);
1742
+ }
1743
+ }
1744
+
1745
+ // src/cli/discord.ts
1746
+ import { homedir as homedir4 } from "os";
1747
+ import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile3, access as access2 } from "fs/promises";
1748
+ import * as readline2 from "readline";
1749
+ var CONFIG_DIR2 = `${homedir4()}/.afk-code`;
1750
+ var DISCORD_CONFIG_FILE = `${CONFIG_DIR2}/discord.env`;
1751
+ function prompt2(question) {
1752
+ const rl = readline2.createInterface({
1753
+ input: process.stdin,
1754
+ output: process.stdout
1755
+ });
1756
+ return new Promise((resolve2) => {
1757
+ rl.question(question, (answer) => {
1758
+ rl.close();
1759
+ resolve2(answer.trim());
1760
+ });
1761
+ });
1762
+ }
1763
+ async function fileExists2(path) {
1764
+ try {
1765
+ await access2(path);
1766
+ return true;
1767
+ } catch {
1768
+ return false;
1769
+ }
1770
+ }
1771
+ async function discordSetup() {
1772
+ console.log(`
1773
+ \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
1774
+ \u2502 AFK Code Discord Setup \u2502
1775
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
1776
+
1777
+ This will guide you through setting up the Discord bot for
1778
+ monitoring Claude Code sessions.
1779
+
1780
+ Step 1: Create a Discord Application
1781
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1782
+ 1. Go to: https://discord.com/developers/applications
1783
+ 2. Click "New Application"
1784
+ 3. Give it a name (e.g., "AFK Code")
1785
+ 4. Click "Create"
1786
+
1787
+ Step 2: Create a Bot
1788
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1789
+ 1. Go to "Bot" in the sidebar
1790
+ 2. Click "Add Bot" \u2192 "Yes, do it!"
1791
+ 3. Under "Privileged Gateway Intents", enable:
1792
+ \u2022 MESSAGE CONTENT INTENT
1793
+ 4. Click "Reset Token" and copy the token
1794
+
1795
+ Step 3: Invite the Bot
1796
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1797
+ 1. Go to "OAuth2" \u2192 "URL Generator"
1798
+ 2. Select scopes: "bot"
1799
+ 3. Select permissions:
1800
+ \u2022 Send Messages
1801
+ \u2022 Manage Channels
1802
+ \u2022 Read Message History
1803
+ 4. Copy the URL and open it to invite the bot to your server
1804
+ `);
1805
+ await prompt2("Press Enter when you have created and invited the bot...");
1806
+ console.log(`
1807
+ Now let's collect your credentials:
1808
+
1809
+ \u2022 Bot Token: "Bot" \u2192 "Token" (click "Reset Token" if needed)
1810
+ \u2022 Your User ID: Enable Developer Mode in Discord settings,
1811
+ then right-click your name \u2192 "Copy User ID"
1812
+ `);
1813
+ const botToken = await prompt2("Bot Token: ");
1814
+ if (!botToken || botToken.length < 50) {
1815
+ console.error("Invalid bot token.");
1816
+ process.exit(1);
1817
+ }
1818
+ const userId = await prompt2("Your Discord User ID: ");
1819
+ if (!userId || !/^\d+$/.test(userId)) {
1820
+ console.error("Invalid user ID. Should be a number.");
1821
+ process.exit(1);
1822
+ }
1823
+ await mkdir2(CONFIG_DIR2, { recursive: true });
1824
+ const envContent = `# AFK Code Discord Configuration
1825
+ DISCORD_BOT_TOKEN=${botToken}
1826
+ DISCORD_USER_ID=${userId}
1827
+ `;
1828
+ await writeFile2(DISCORD_CONFIG_FILE, envContent);
1829
+ console.log(`
1830
+ \u2713 Configuration saved to ${DISCORD_CONFIG_FILE}
1831
+
1832
+ To start the Discord bot, run:
1833
+ afk-code discord
1834
+
1835
+ Then start a Claude Code session with:
1836
+ afk-code run -- claude
1837
+ `);
1838
+ }
1839
+ async function loadEnvFile2(path) {
1840
+ if (!await fileExists2(path)) return {};
1841
+ const content = await readFile3(path, "utf-8");
1842
+ const config = {};
1843
+ for (const line of content.split("\n")) {
1844
+ if (line.startsWith("#") || !line.includes("=")) continue;
1845
+ const [key, ...valueParts] = line.split("=");
1846
+ config[key.trim()] = valueParts.join("=").trim();
1847
+ }
1848
+ return config;
1849
+ }
1850
+ async function discordRun() {
1851
+ const globalConfig = await loadEnvFile2(DISCORD_CONFIG_FILE);
1852
+ const localConfig = await loadEnvFile2(`${process.cwd()}/.env`);
1853
+ const config = {
1854
+ ...globalConfig,
1855
+ ...localConfig
1856
+ };
1857
+ if (process.env.DISCORD_BOT_TOKEN) config.DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN;
1858
+ if (process.env.DISCORD_USER_ID) config.DISCORD_USER_ID = process.env.DISCORD_USER_ID;
1859
+ const required = ["DISCORD_BOT_TOKEN", "DISCORD_USER_ID"];
1860
+ const missing = required.filter((key) => !config[key]);
1861
+ if (missing.length > 0) {
1862
+ console.error(`Missing config: ${missing.join(", ")}`);
1863
+ console.error("");
1864
+ console.error("Provide tokens via:");
1865
+ console.error(" - Environment variables (DISCORD_BOT_TOKEN, DISCORD_USER_ID)");
1866
+ console.error(" - Local .env file");
1867
+ console.error(' - Run "afk-code discord setup" for guided configuration');
1868
+ process.exit(1);
1869
+ }
1870
+ const { createDiscordApp: createDiscordApp2 } = await Promise.resolve().then(() => (init_discord_app(), discord_app_exports));
1871
+ const localEnvExists = await fileExists2(`${process.cwd()}/.env`);
1872
+ const globalEnvExists = await fileExists2(DISCORD_CONFIG_FILE);
1873
+ const source = localEnvExists ? ".env" : globalEnvExists ? DISCORD_CONFIG_FILE : "environment";
1874
+ console.log(`[AFK Code] Loaded config from ${source}`);
1875
+ console.log("[AFK Code] Starting Discord bot...");
1876
+ const discordConfig = {
1877
+ botToken: config.DISCORD_BOT_TOKEN,
1878
+ userId: config.DISCORD_USER_ID
1879
+ };
1880
+ const { client, sessionManager } = createDiscordApp2(discordConfig);
1881
+ try {
1882
+ await sessionManager.start();
1883
+ console.log("[AFK Code] Session manager started");
1884
+ } catch (err) {
1885
+ console.error("[AFK Code] Failed to start session manager:", err);
1886
+ process.exit(1);
1887
+ }
1888
+ try {
1889
+ await client.login(config.DISCORD_BOT_TOKEN);
1890
+ console.log("[AFK Code] Discord bot is running!");
1891
+ console.log("");
1892
+ console.log("Start a Claude Code session with: afk-code run -- claude");
1893
+ console.log("Each session will create an #afk-* channel");
1894
+ } catch (err) {
1895
+ console.error("[AFK Code] Failed to start Discord bot:", err);
1896
+ process.exit(1);
1897
+ }
1898
+ }
1899
+
1900
+ // src/cli/index.ts
1901
+ var args = process.argv.slice(2);
1902
+ var command = args[0];
1903
+ async function main() {
1904
+ switch (command) {
1905
+ case "run": {
1906
+ const separatorIndex = args.indexOf("--");
1907
+ if (separatorIndex === -1) {
1908
+ console.error("Usage: afk-code run -- <command> [args...]");
1909
+ console.error("Example: afk-code run -- claude");
1910
+ process.exit(1);
1911
+ }
1912
+ const cmd = args.slice(separatorIndex + 1);
1913
+ if (cmd.length === 0) {
1914
+ console.error("No command specified after --");
1915
+ process.exit(1);
1916
+ }
1917
+ await run(cmd);
1918
+ break;
1919
+ }
1920
+ case "slack": {
1921
+ if (args[1] === "setup") {
1922
+ await slackSetup();
1923
+ } else {
1924
+ await slackRun();
1925
+ }
1926
+ break;
1927
+ }
1928
+ case "discord": {
1929
+ if (args[1] === "setup") {
1930
+ await discordSetup();
1931
+ } else {
1932
+ await discordRun();
1933
+ }
1934
+ break;
1935
+ }
1936
+ case "help":
1937
+ case "--help":
1938
+ case "-h":
1939
+ case void 0: {
1940
+ console.log(`
1941
+ AFK Code - Monitor Claude Code sessions from Slack/Discord
1942
+
1943
+ Commands:
1944
+ slack Run the Slack bot
1945
+ slack setup Configure Slack integration
1946
+ discord Run the Discord bot
1947
+ discord setup Configure Discord integration
1948
+ run -- <command> Start a monitored session
1949
+ help Show this help message
1950
+
1951
+ Examples:
1952
+ afk-code slack setup # First-time Slack configuration
1953
+ afk-code slack # Start the Slack bot
1954
+ afk-code discord setup # First-time Discord configuration
1955
+ afk-code discord # Start the Discord bot
1956
+ afk-code run -- claude # Start a Claude Code session
1957
+ `);
1958
+ break;
1959
+ }
1960
+ default: {
1961
+ console.error(`Unknown command: ${command}`);
1962
+ console.error('Run "afk-code help" for usage');
1963
+ process.exit(1);
1964
+ }
1965
+ }
1966
+ }
1967
+ main().catch((err) => {
1968
+ console.error("Error:", err.message);
1969
+ process.exit(1);
1970
+ });