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
@@ -0,0 +1,192 @@
1
+ import { query } from "@anthropic-ai/claude-agent-sdk";
2
+ import { randomUUID } from "node:crypto";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import os from "node:os";
6
+ import { requestPermission } from "./permissions.js";
7
+ // --- Persistent session store ---
8
+ const SESSIONS_FILE = path.join(os.homedir(), ".eniac", "sessions.json");
9
+ function loadSessions() {
10
+ try {
11
+ const data = fs.readFileSync(SESSIONS_FILE, "utf-8");
12
+ const entries = JSON.parse(data);
13
+ console.log(`[sessions] loaded ${entries.length} sessions from disk`);
14
+ return new Map(entries);
15
+ }
16
+ catch {
17
+ return new Map();
18
+ }
19
+ }
20
+ function saveSessions() {
21
+ try {
22
+ const dir = path.dirname(SESSIONS_FILE);
23
+ fs.mkdirSync(dir, { recursive: true });
24
+ fs.writeFileSync(SESSIONS_FILE, JSON.stringify([...sessions.entries()], null, 2));
25
+ }
26
+ catch (err) {
27
+ console.warn("[sessions] failed to save:", err);
28
+ }
29
+ }
30
+ const SESSION_TTL_MS = 14 * 24 * 60 * 60 * 1000; // 2 weeks
31
+ function cleanupExpiredSessions() {
32
+ const now = Date.now();
33
+ let cleaned = 0;
34
+ for (const [threadTs, session] of sessions) {
35
+ const lastActivity = session.lastActivityAt ?? session.createdAt;
36
+ if (now - lastActivity < SESSION_TTL_MS)
37
+ continue;
38
+ console.log(`[sessions] cleaning expired session: threadTs=${threadTs}, sessionId=${session.sessionId}, lastActivity=${new Date(lastActivity).toISOString()}`);
39
+ // Delete SDK session file (~/.claude/projects/{cwd-encoded}/{sessionId}.jsonl)
40
+ const cwdEncoded = session.workDir.replace(/\//g, "-");
41
+ const sessionFile = path.join(os.homedir(), ".claude", "projects", cwdEncoded, `${session.sessionId}.jsonl`);
42
+ try {
43
+ fs.unlinkSync(sessionFile);
44
+ console.log(`[sessions] deleted session file: ${sessionFile}`);
45
+ }
46
+ catch {
47
+ // File may not exist
48
+ }
49
+ // Delete worktree directory
50
+ try {
51
+ fs.rmSync(session.workDir, { recursive: true, force: true });
52
+ console.log(`[sessions] deleted workDir: ${session.workDir}`);
53
+ }
54
+ catch {
55
+ // Directory may not exist
56
+ }
57
+ sessions.delete(threadTs);
58
+ cleaned++;
59
+ }
60
+ if (cleaned > 0) {
61
+ saveSessions();
62
+ console.log(`[sessions] cleaned ${cleaned} expired sessions`);
63
+ }
64
+ }
65
+ const sessions = loadSessions();
66
+ cleanupExpiredSessions();
67
+ export function createSession(threadTs, workDir, authorUserId) {
68
+ const now = Date.now();
69
+ const session = {
70
+ sessionId: randomUUID(),
71
+ workDir,
72
+ hasStarted: false,
73
+ authorUserId,
74
+ createdAt: now,
75
+ lastActivityAt: now,
76
+ };
77
+ sessions.set(threadTs, session);
78
+ saveSessions();
79
+ return session;
80
+ }
81
+ export function getSession(threadTs) {
82
+ return sessions.get(threadTs);
83
+ }
84
+ function describeToolInput(toolName, input) {
85
+ switch (toolName) {
86
+ case "Bash":
87
+ return `\`\`\`\n${input["command"] ?? ""}\n\`\`\``;
88
+ case "Edit":
89
+ case "Write":
90
+ case "Read":
91
+ return `File: \`${input["file_path"] ?? input["path"] ?? "unknown"}\``;
92
+ default:
93
+ return `\`\`\`\n${JSON.stringify(input, null, 2).slice(0, 500)}\n\`\`\``;
94
+ }
95
+ }
96
+ /**
97
+ * Send a message to a Claude session via the SDK.
98
+ *
99
+ * Uses `canUseTool` callback to handle permission requests
100
+ * through Slack interactive buttons.
101
+ */
102
+ export async function* chat(threadTs, userMessage, slackClient, channel) {
103
+ const session = sessions.get(threadTs);
104
+ if (!session) {
105
+ throw new Error(`No session found for thread ${threadTs}`);
106
+ }
107
+ // Tools that are safe to auto-allow without Slack approval
108
+ const AUTO_ALLOW_TOOLS = new Set([
109
+ "Read",
110
+ "Glob",
111
+ "Grep",
112
+ "WebSearch",
113
+ "WebFetch",
114
+ "Agent",
115
+ "TodoRead",
116
+ "ListMcpResources",
117
+ "ReadMcpResource",
118
+ ]);
119
+ const PERMISSION_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
120
+ // Permission handler — auto-allows safe tools, asks Slack for dangerous ones
121
+ const canUseTool = async (toolName, input, { signal }) => {
122
+ // Auto-allow safe read-only tools
123
+ if (AUTO_ALLOW_TOOLS.has(toolName)) {
124
+ console.log(`[claude] auto-allow: ${toolName}`);
125
+ return { behavior: "allow" };
126
+ }
127
+ const permId = randomUUID();
128
+ const description = describeToolInput(toolName, input);
129
+ console.log(`[claude] permission request: tool=${toolName}, id=${permId}`);
130
+ // Race between Slack button response and timeout
131
+ const granted = await Promise.race([
132
+ requestPermission(slackClient, channel, threadTs, permId, toolName, description, session.authorUserId),
133
+ new Promise((resolve) => setTimeout(() => {
134
+ console.log(`[claude] permission timeout: ${permId}`);
135
+ resolve(false);
136
+ }, PERMISSION_TIMEOUT_MS)),
137
+ ]);
138
+ console.log(`[claude] permission ${granted ? "approved" : "denied"}: tool=${toolName}`);
139
+ if (granted) {
140
+ return { behavior: "allow" };
141
+ }
142
+ else {
143
+ return { behavior: "deny", message: "User denied or timed out via Slack" };
144
+ }
145
+ };
146
+ session.lastActivityAt = Date.now();
147
+ saveSessions();
148
+ console.log(`[claude] starting query, cwd=${session.workDir}, hasStarted=${session.hasStarted}`);
149
+ try {
150
+ const q = query({
151
+ prompt: userMessage,
152
+ options: {
153
+ cwd: session.workDir,
154
+ canUseTool,
155
+ permissionMode: "bypassPermissions",
156
+ allowDangerouslySkipPermissions: true,
157
+ includePartialMessages: true,
158
+ ...(session.hasStarted
159
+ ? { resume: session.sessionId }
160
+ : { sessionId: session.sessionId }),
161
+ },
162
+ });
163
+ session.hasStarted = true;
164
+ saveSessions();
165
+ for await (const message of q) {
166
+ const msgType = message.type;
167
+ // Real-time streaming text deltas
168
+ if (msgType === "stream_event") {
169
+ const partial = message;
170
+ if (partial.event?.type === "content_block_delta") {
171
+ if (partial.event.delta?.type === "text_delta" &&
172
+ partial.event.delta.text) {
173
+ yield { type: "text", content: partial.event.delta.text };
174
+ }
175
+ }
176
+ continue;
177
+ }
178
+ // Final result
179
+ if (msgType === "result") {
180
+ const result = message;
181
+ console.log(`[claude] result: subtype=${result.subtype}, len=${result.result?.length ?? 0}`);
182
+ continue;
183
+ }
184
+ }
185
+ }
186
+ catch (error) {
187
+ const errMsg = error instanceof Error ? error.message : String(error);
188
+ console.error(`[claude] error: ${errMsg}`);
189
+ yield { type: "error", message: errMsg };
190
+ }
191
+ }
192
+ //# sourceMappingURL=claude.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude.js","sourceRoot":"","sources":["../../src/services/claude.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Prepare a working directory for a given GitHub repository.
3
+ *
4
+ * Strategy:
5
+ * 1. Maintain a bare clone at `{baseDir}/{owner}/{repo}.git`
6
+ * 2. Create a worktree from it at `{baseDir}/{owner}/{repo}/worktrees/{timestamp}`
7
+ *
8
+ * @returns The absolute path to the worktree directory.
9
+ */
10
+ export declare function prepareWorkDir(repoIdentifier: {
11
+ owner: string;
12
+ repo: string;
13
+ url: string;
14
+ }, baseDir: string): Promise<string>;
15
+ //# sourceMappingURL=git.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../src/services/git.ts"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAClC,cAAc,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EAC5D,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,CAiFjB"}
@@ -0,0 +1,81 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs/promises";
3
+ import simpleGit from "simple-git";
4
+ /**
5
+ * Prepare a working directory for a given GitHub repository.
6
+ *
7
+ * Strategy:
8
+ * 1. Maintain a bare clone at `{baseDir}/{owner}/{repo}.git`
9
+ * 2. Create a worktree from it at `{baseDir}/{owner}/{repo}/worktrees/{timestamp}`
10
+ *
11
+ * @returns The absolute path to the worktree directory.
12
+ */
13
+ export async function prepareWorkDir(repoIdentifier, baseDir) {
14
+ const { owner, repo, url } = repoIdentifier;
15
+ const bareRepoPath = path.join(baseDir, owner, `${repo}.git`);
16
+ const worktreeBase = path.join(baseDir, owner, repo, "worktrees");
17
+ const timestamp = Date.now().toString();
18
+ const worktreePath = path.join(worktreeBase, timestamp);
19
+ // Ensure directories exist
20
+ await fs.mkdir(path.dirname(bareRepoPath), { recursive: true });
21
+ await fs.mkdir(worktreeBase, { recursive: true });
22
+ const bareExists = await fs
23
+ .stat(bareRepoPath)
24
+ .then(() => true)
25
+ .catch(() => false);
26
+ if (!bareExists) {
27
+ // Clone as bare repository
28
+ console.log(`[git] Cloning bare repo: ${url} -> ${bareRepoPath}`);
29
+ const git = simpleGit();
30
+ try {
31
+ await git.clone(url, bareRepoPath, ["--bare"]);
32
+ }
33
+ catch (error) {
34
+ const message = error instanceof Error ? error.message : String(error);
35
+ throw new Error(`Failed to clone repository ${url}: ${message}`);
36
+ }
37
+ }
38
+ else {
39
+ // Fetch latest changes
40
+ console.log(`[git] Fetching latest for bare repo: ${bareRepoPath}`);
41
+ const git = simpleGit(bareRepoPath);
42
+ try {
43
+ await git.fetch(["--all"]);
44
+ }
45
+ catch (error) {
46
+ console.warn(`[git] Failed to fetch updates (continuing with existing data):`, error instanceof Error ? error.message : error);
47
+ }
48
+ }
49
+ // Determine the default branch
50
+ const bareGit = simpleGit(bareRepoPath);
51
+ let defaultBranch = "main";
52
+ try {
53
+ const headRef = await bareGit.raw(["symbolic-ref", "HEAD"]);
54
+ const match = headRef.trim().match(/^refs\/heads\/(.+)$/);
55
+ if (match) {
56
+ defaultBranch = match[1];
57
+ }
58
+ }
59
+ catch {
60
+ // Fall back to "main"
61
+ }
62
+ // Create worktree with a unique branch based on timestamp
63
+ const branchName = `eniac/${timestamp}`;
64
+ console.log(`[git] Creating worktree: ${worktreePath} (branch: ${branchName} from ${defaultBranch})`);
65
+ try {
66
+ await bareGit.raw([
67
+ "worktree",
68
+ "add",
69
+ "-b",
70
+ branchName,
71
+ worktreePath,
72
+ defaultBranch,
73
+ ]);
74
+ }
75
+ catch (error) {
76
+ const message = error instanceof Error ? error.message : String(error);
77
+ throw new Error(`Failed to create worktree at ${worktreePath}: ${message}`);
78
+ }
79
+ return worktreePath;
80
+ }
81
+ //# sourceMappingURL=git.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/services/git.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { WebClient } from "@slack/web-api";
2
+ /**
3
+ * Register a pending permission request and post Slack buttons.
4
+ * Returns a Promise that resolves when the user clicks Approve or Deny.
5
+ */
6
+ export declare function requestPermission(client: WebClient, channel: string, threadTs: string, permissionId: string, toolName: string, description: string, authorUserId?: string): Promise<boolean>;
7
+ /**
8
+ * Resolve a pending permission (called from Slack action handler).
9
+ * Only the original thread author can approve/deny.
10
+ */
11
+ export declare function resolvePermission(client: WebClient, permissionId: string, granted: boolean, clickedUserId: string): Promise<void>;
12
+ //# sourceMappingURL=permissions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../../src/services/permissions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAYhD;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,OAAO,CAAC,CAkDlB;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,SAAS,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,OAAO,EAChB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,IAAI,CAAC,CA6Cf"}
@@ -0,0 +1,98 @@
1
+ const pending = new Map();
2
+ /**
3
+ * Register a pending permission request and post Slack buttons.
4
+ * Returns a Promise that resolves when the user clicks Approve or Deny.
5
+ */
6
+ export async function requestPermission(client, channel, threadTs, permissionId, toolName, description, authorUserId) {
7
+ console.log(`[perm] posting buttons: tool=${toolName}, permId=${permissionId}, author=${authorUserId}`);
8
+ const result = await client.chat.postMessage({
9
+ channel,
10
+ thread_ts: threadTs,
11
+ text: `Permission request: ${toolName}`,
12
+ blocks: [
13
+ {
14
+ type: "section",
15
+ text: {
16
+ type: "mrkdwn",
17
+ text: `:lock: *Permission Required*\n\n*Tool:* \`${toolName}\`\n${description}`,
18
+ },
19
+ },
20
+ {
21
+ type: "actions",
22
+ block_id: `perm_${permissionId}`,
23
+ elements: [
24
+ {
25
+ type: "button",
26
+ text: { type: "plain_text", text: "Approve" },
27
+ style: "primary",
28
+ action_id: "approve_permission",
29
+ value: permissionId,
30
+ },
31
+ {
32
+ type: "button",
33
+ text: { type: "plain_text", text: "Deny" },
34
+ style: "danger",
35
+ action_id: "deny_permission",
36
+ value: permissionId,
37
+ },
38
+ ],
39
+ },
40
+ ],
41
+ });
42
+ console.log(`[perm] buttons posted: messageTs=${result.ts}`);
43
+ return new Promise((resolve) => {
44
+ pending.set(permissionId, {
45
+ resolve,
46
+ channel,
47
+ threadTs,
48
+ messageTs: result.ts,
49
+ authorUserId: authorUserId ?? "",
50
+ });
51
+ console.log(`[perm] pending entry set: permId=${permissionId}, pending.size=${pending.size}`);
52
+ });
53
+ }
54
+ /**
55
+ * Resolve a pending permission (called from Slack action handler).
56
+ * Only the original thread author can approve/deny.
57
+ */
58
+ export async function resolvePermission(client, permissionId, granted, clickedUserId) {
59
+ console.log(`[perm] resolvePermission called: permId=${permissionId}, granted=${granted}, clickedBy=${clickedUserId}`);
60
+ console.log(`[perm] pending keys: [${[...pending.keys()].join(", ")}]`);
61
+ const entry = pending.get(permissionId);
62
+ if (!entry) {
63
+ console.log(`[perm] ERROR: no pending entry found for ${permissionId}`);
64
+ return;
65
+ }
66
+ console.log(`[perm] entry found: authorUserId=${entry.authorUserId}, clickedUserId=${clickedUserId}`);
67
+ // Only the thread author can approve/deny
68
+ if (entry.authorUserId && clickedUserId !== entry.authorUserId) {
69
+ console.log(`[perm] REJECTED: author mismatch! expected=${entry.authorUserId}, got=${clickedUserId}`);
70
+ return;
71
+ }
72
+ pending.delete(permissionId);
73
+ // Update the button message to show the decision
74
+ if (entry.messageTs) {
75
+ const status = granted
76
+ ? `:white_check_mark: *Approved* by <@${clickedUserId}>`
77
+ : `:no_entry_sign: *Denied* by <@${clickedUserId}>`;
78
+ try {
79
+ await client.chat.update({
80
+ channel: entry.channel,
81
+ ts: entry.messageTs,
82
+ text: status,
83
+ blocks: [
84
+ {
85
+ type: "section",
86
+ text: { type: "mrkdwn", text: status },
87
+ },
88
+ ],
89
+ });
90
+ }
91
+ catch {
92
+ // best effort update
93
+ }
94
+ }
95
+ console.log(`[perm] resolving promise with granted=${granted}`);
96
+ entry.resolve(granted);
97
+ }
98
+ //# sourceMappingURL=permissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.js","sourceRoot":"","sources":["../../src/services/permissions.ts"],"names":[],"mappings":"AAUA,MAAM,OAAO,GAAG,IAAI,GAAG,EAA6B,CAAC;AAErD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,MAAiB,EACjB,OAAe,EACf,QAAgB,EAChB,YAAoB,EACpB,QAAgB,EAChB,WAAmB,EACnB,YAAqB;IAErB,OAAO,CAAC,GAAG,CAAC,gCAAgC,QAAQ,YAAY,YAAY,YAAY,YAAY,EAAE,CAAC,CAAC;IAExG,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC;QAC3C,OAAO;QACP,SAAS,EAAE,QAAQ;QACnB,IAAI,EAAE,uBAAuB,QAAQ,EAAE;QACvC,MAAM,EAAE;YACN;gBACE,IAAI,EAAE,SAAS;gBACf,IAAI,EAAE;oBACJ,IAAI,EAAE,QAAQ;oBACd,IAAI,EAAE,6CAA6C,QAAQ,OAAO,WAAW,EAAE;iBAChF;aACF;YACD;gBACE,IAAI,EAAE,SAAS;gBACf,QAAQ,EAAE,QAAQ,YAAY,EAAE;gBAChC,QAAQ,EAAE;oBACR;wBACE,IAAI,EAAE,QAAQ;wBACd,IAAI,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,SAAS,EAAE;wBAC7C,KAAK,EAAE,SAAS;wBAChB,SAAS,EAAE,oBAAoB;wBAC/B,KAAK,EAAE,YAAY;qBACpB;oBACD;wBACE,IAAI,EAAE,QAAQ;wBACd,IAAI,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE;wBAC1C,KAAK,EAAE,QAAQ;wBACf,SAAS,EAAE,iBAAiB;wBAC5B,KAAK,EAAE,YAAY;qBACpB;iBACF;aACF;SACF;KACF,CAAC,CAAC;IAEH,OAAO,CAAC,GAAG,CAAC,oCAAoC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;IAE7D,OAAO,IAAI,OAAO,CAAU,CAAC,OAAO,EAAE,EAAE;QACtC,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE;YACxB,OAAO;YACP,OAAO;YACP,QAAQ;YACR,SAAS,EAAE,MAAM,CAAC,EAAE;YACpB,YAAY,EAAE,YAAY,IAAI,EAAE;SACjC,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,oCAAoC,YAAY,kBAAkB,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAChG,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,MAAiB,EACjB,YAAoB,EACpB,OAAgB,EAChB,aAAqB;IAErB,OAAO,CAAC,GAAG,CAAC,2CAA2C,YAAY,aAAa,OAAO,eAAe,aAAa,EAAE,CAAC,CAAC;IACvH,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAExE,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,CAAC,GAAG,CAAC,4CAA4C,YAAY,EAAE,CAAC,CAAC;QACxE,OAAO;IACT,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,oCAAoC,KAAK,CAAC,YAAY,mBAAmB,aAAa,EAAE,CAAC,CAAC;IAEtG,0CAA0C;IAC1C,IAAI,KAAK,CAAC,YAAY,IAAI,aAAa,KAAK,KAAK,CAAC,YAAY,EAAE,CAAC;QAC/D,OAAO,CAAC,GAAG,CAAC,8CAA8C,KAAK,CAAC,YAAY,SAAS,aAAa,EAAE,CAAC,CAAC;QACtG,OAAO;IACT,CAAC;IAED,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IAE7B,iDAAiD;IACjD,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;QACpB,MAAM,MAAM,GAAG,OAAO;YACpB,CAAC,CAAC,sCAAsC,aAAa,GAAG;YACxD,CAAC,CAAC,iCAAiC,aAAa,GAAG,CAAC;QAEtD,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;gBACvB,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,EAAE,EAAE,KAAK,CAAC,SAAS;gBACnB,IAAI,EAAE,MAAM;gBACZ,MAAM,EAAE;oBACN;wBACE,IAAI,EAAE,SAAS;wBACf,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE;qBACvC;iBACF;aACF,CAAC,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;YACP,qBAAqB;QACvB,CAAC;IACH,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,yCAAyC,OAAO,EAAE,CAAC,CAAC;IAChE,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;AACzB,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { WebClient } from "@slack/web-api";
2
+ import type { ChatEvent } from "./claude.js";
3
+ /**
4
+ * Post a streaming reply in a Slack thread, handling both text and tool events.
5
+ *
6
+ * - Text chunks are accumulated and the message is updated in real-time.
7
+ * - Tool use / tool result events are logged but don't affect the main message.
8
+ * - Permission requests are handled by claude.ts (via permissions.ts).
9
+ */
10
+ export declare function postStreamingReply(client: WebClient, channel: string, threadTs: string, eventStream: AsyncGenerator<ChatEvent, void, unknown>): Promise<string>;
11
+ //# sourceMappingURL=slack-messenger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-messenger.d.ts","sourceRoot":"","sources":["../../src/services/slack-messenger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAI7C;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,cAAc,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,GACpD,OAAO,CAAC,MAAM,CAAC,CAoFjB"}
@@ -0,0 +1,73 @@
1
+ const THROTTLE_MS = 500;
2
+ /**
3
+ * Post a streaming reply in a Slack thread, handling both text and tool events.
4
+ *
5
+ * - Text chunks are accumulated and the message is updated in real-time.
6
+ * - Tool use / tool result events are logged but don't affect the main message.
7
+ * - Permission requests are handled by claude.ts (via permissions.ts).
8
+ */
9
+ export async function postStreamingReply(client, channel, threadTs, eventStream) {
10
+ const initial = await client.chat.postMessage({
11
+ channel,
12
+ thread_ts: threadTs,
13
+ text: ":hourglass_flowing_sand: Thinking...",
14
+ });
15
+ const messageTs = initial.ts;
16
+ if (!messageTs) {
17
+ throw new Error("Failed to post initial message — no ts returned");
18
+ }
19
+ let accumulated = "";
20
+ let lastUpdateTime = 0;
21
+ let pendingUpdate = false;
22
+ // Trim accumulated text to the last word boundary to avoid
23
+ // partial Korean characters being interpreted as punycode URLs by Slack
24
+ const safeSlice = (text) => {
25
+ // Find last whitespace or newline
26
+ const lastBreak = Math.max(text.lastIndexOf(" "), text.lastIndexOf("\n"), text.lastIndexOf("\t"));
27
+ // If break is near the end (within 20 chars), use it; otherwise send everything
28
+ if (lastBreak > 0 && text.length - lastBreak <= 20) {
29
+ return text.slice(0, lastBreak + 1);
30
+ }
31
+ return text;
32
+ };
33
+ const doUpdate = async (text, isFinal) => {
34
+ const now = Date.now();
35
+ const elapsed = now - lastUpdateTime;
36
+ if (!isFinal && elapsed < THROTTLE_MS) {
37
+ pendingUpdate = true;
38
+ return;
39
+ }
40
+ pendingUpdate = false;
41
+ lastUpdateTime = Date.now();
42
+ // For intermediate updates, trim to word boundary to prevent garbled display
43
+ const displayText = isFinal ? text : safeSlice(text);
44
+ try {
45
+ await client.chat.update({
46
+ channel,
47
+ ts: messageTs,
48
+ text: displayText || ":hourglass_flowing_sand: Thinking...",
49
+ });
50
+ }
51
+ catch (error) {
52
+ console.warn("[slack-messenger] Failed to update message:", error instanceof Error ? error.message : error);
53
+ }
54
+ };
55
+ for await (const event of eventStream) {
56
+ switch (event.type) {
57
+ case "text":
58
+ accumulated += event.content;
59
+ await doUpdate(accumulated, false);
60
+ break;
61
+ case "error":
62
+ accumulated += `\n\n:warning: ${event.message}`;
63
+ await doUpdate(accumulated, true);
64
+ break;
65
+ }
66
+ }
67
+ // Final flush
68
+ if (pendingUpdate || accumulated) {
69
+ await doUpdate(accumulated || ":warning: No response generated.", true);
70
+ }
71
+ return messageTs;
72
+ }
73
+ //# sourceMappingURL=slack-messenger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-messenger.js","sourceRoot":"","sources":["../../src/services/slack-messenger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Remove the bot mention (`<@BOTID>`) from message text.
3
+ */
4
+ export declare function removeMention(text: string): string;
5
+ export interface GithubRepo {
6
+ owner: string;
7
+ repo: string;
8
+ url: string;
9
+ }
10
+ /**
11
+ * Extract a GitHub repository identifier from text.
12
+ *
13
+ * Supported formats:
14
+ * - `https://github.com/owner/repo`
15
+ * - `github.com/owner/repo`
16
+ * - `owner/repo` (only when it looks like a valid GitHub identifier)
17
+ *
18
+ * Returns `null` if no match is found.
19
+ */
20
+ export declare function extractGithubRepo(text: string): GithubRepo | null;
21
+ //# sourceMappingURL=parse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/utils/parse.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CA0CjE"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Remove the bot mention (`<@BOTID>`) from message text.
3
+ */
4
+ export function removeMention(text) {
5
+ return text.replace(/<@[A-Z0-9]+>/g, "").trim();
6
+ }
7
+ /**
8
+ * Extract a GitHub repository identifier from text.
9
+ *
10
+ * Supported formats:
11
+ * - `https://github.com/owner/repo`
12
+ * - `github.com/owner/repo`
13
+ * - `owner/repo` (only when it looks like a valid GitHub identifier)
14
+ *
15
+ * Returns `null` if no match is found.
16
+ */
17
+ export function extractGithubRepo(text) {
18
+ // Full URL: https://github.com/owner/repo or github.com/owner/repo
19
+ const urlPattern = /(?:https?:\/\/)?github\.com\/([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)/;
20
+ const urlMatch = text.match(urlPattern);
21
+ if (urlMatch) {
22
+ const owner = urlMatch[1];
23
+ const repo = urlMatch[2].replace(/\.git$/, "");
24
+ return {
25
+ owner,
26
+ repo,
27
+ url: `https://github.com/${owner}/${repo}.git`,
28
+ };
29
+ }
30
+ // Short form: owner/repo — must be a standalone token with valid GitHub naming
31
+ const shortPattern = /\b([a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?)\/([a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?)\b/;
32
+ const shortMatch = text.match(shortPattern);
33
+ if (shortMatch) {
34
+ const owner = shortMatch[1];
35
+ const repo = shortMatch[2];
36
+ // Reject things that look like file paths or generic patterns
37
+ if (owner.includes("..") ||
38
+ repo.includes("..") ||
39
+ owner.startsWith(".") ||
40
+ repo.startsWith(".")) {
41
+ return null;
42
+ }
43
+ return {
44
+ owner,
45
+ repo,
46
+ url: `https://github.com/${owner}/${repo}.git`,
47
+ };
48
+ }
49
+ return null;
50
+ }
51
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/utils/parse.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;AAClD,CAAC;AAQD;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,mEAAmE;IACnE,MAAM,UAAU,GACd,mEAAmE,CAAC;IACtE,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IAExC,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAChD,OAAO;YACL,KAAK;YACL,IAAI;YACJ,GAAG,EAAE,sBAAsB,KAAK,IAAI,IAAI,MAAM;SAC/C,CAAC;IACJ,CAAC;IAED,+EAA+E;IAC/E,MAAM,YAAY,GAAG,gGAAgG,CAAC;IACtH,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IAE5C,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,KAAK,GAAG,UAAU,CAAC,CAAC,CAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,CAAE,CAAC;QAE5B,8DAA8D;QAC9D,IACE,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;YACpB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;YACnB,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;YACrB,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EACpB,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO;YACL,KAAK;YACL,IAAI;YACJ,GAAG,EAAE,sBAAsB,KAAK,IAAI,IAAI,MAAM;SAC/C,CAAC;IACJ,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "eniac-slack",
3
+ "version": "0.0.2",
4
+ "type": "module",
5
+ "bin": "./dist/cli.js",
6
+ "scripts": {
7
+ "dev": "tsx watch src/cli.ts",
8
+ "build": "tsc",
9
+ "start": "node dist/cli.js"
10
+ },
11
+ "dependencies": {
12
+ "@anthropic-ai/claude-agent-sdk": "^0.2.72",
13
+ "@slack/bolt": "^4.1.0",
14
+ "dotenv": "^16.4.0",
15
+ "simple-git": "^3.27.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^22.0.0",
19
+ "tsx": "^4.19.0",
20
+ "typescript": "^5.9.3"
21
+ }
22
+ }