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.
- package/SPEC.md +240 -0
- package/dist/app.d.ts +8 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +44 -0
- package/dist/app.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +39 -0
- package/dist/cli.js.map +1 -0
- package/dist/handlers/mention.d.ts +10 -0
- package/dist/handlers/mention.d.ts.map +1 -0
- package/dist/handlers/mention.js +96 -0
- package/dist/handlers/mention.js.map +1 -0
- package/dist/handlers/thread.d.ts +8 -0
- package/dist/handlers/thread.d.ts.map +1 -0
- package/dist/handlers/thread.js +50 -0
- package/dist/handlers/thread.js.map +1 -0
- package/dist/services/claude.d.ts +27 -0
- package/dist/services/claude.d.ts.map +1 -0
- package/dist/services/claude.js +192 -0
- package/dist/services/claude.js.map +1 -0
- package/dist/services/git.d.ts +15 -0
- package/dist/services/git.d.ts.map +1 -0
- package/dist/services/git.js +81 -0
- package/dist/services/git.js.map +1 -0
- package/dist/services/permissions.d.ts +12 -0
- package/dist/services/permissions.d.ts.map +1 -0
- package/dist/services/permissions.js +98 -0
- package/dist/services/permissions.js.map +1 -0
- package/dist/services/slack-messenger.d.ts +11 -0
- package/dist/services/slack-messenger.d.ts.map +1 -0
- package/dist/services/slack-messenger.js +73 -0
- package/dist/services/slack-messenger.js.map +1 -0
- package/dist/utils/parse.d.ts +21 -0
- package/dist/utils/parse.d.ts.map +1 -0
- package/dist/utils/parse.js +51 -0
- package/dist/utils/parse.js.map +1 -0
- package/package.json +22 -0
- package/src/app.ts +54 -0
- package/src/cli.ts +47 -0
- package/src/handlers/mention.ts +119 -0
- package/src/handlers/thread.ts +61 -0
- package/src/services/claude.ts +280 -0
- package/src/services/git.ts +98 -0
- package/src/services/permissions.ts +131 -0
- package/src/services/slack-messenger.ts +102 -0
- package/src/utils/parse.ts +66 -0
- 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":"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"}
|
|
@@ -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
|
+
}
|