comfy-pr 0.2.26 → 1.0.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.
- package/README.md +258 -157
- package/bot/IdleWaiter.ts +31 -0
- package/bot/RestartManager.spec.ts +163 -0
- package/bot/RestartManager.ts +190 -0
- package/bot/WorkingTasksManager.spec.ts +154 -0
- package/bot/cli.ts +1171 -0
- package/bot/codesearch-ai-wip.ts +351 -0
- package/bot/error-collector.ts +133 -0
- package/bot/index.ts +14 -0
- package/bot/restart-example.ts +99 -0
- package/bot/slack-bolt.ts +688 -0
- package/bot/slack-bot.ts +1575 -0
- package/bot/state.ts +19 -0
- package/bot/templateLoader.test.ts +84 -0
- package/bot/templateLoader.ts +92 -0
- package/next.config.ts +26 -0
- package/package.json +161 -41
- package/post-summary.ts +44 -0
- package/src/Authors.ts +2 -2
- package/src/CMNodes.ts +3 -3
- package/src/CNRepos.ts +30 -7
- package/src/CRNodes.ts +11 -9
- package/src/CRPulls.ts +3 -0
- package/src/EmailTasks.ts +27 -9
- package/src/FORK_OWNER.ts +3 -1
- package/src/FollowRules.ts +3 -3
- package/src/GithubIssueComments.ts +1 -1
- package/src/Totals.ts +9 -8
- package/src/WorkerInstances.ts +31 -8
- package/src/addCommentAction.ts +19 -11
- package/src/analyzePullsStatus.ts +39 -17
- package/src/analyzeTotals.ts +19 -11
- package/src/bypassRepos.ts +6 -5
- package/src/checkPRsFailures.ts +13 -8
- package/src/cli.test.ts +2 -0
- package/src/cli.ts +1 -1
- package/src/constants.ts +2 -0
- package/src/createComfyRegistryPRsFromCandidates.ts +20 -12
- package/src/createComfyRegistryPullRequests.ts +35 -10
- package/src/createGithubForkForRepo.ts +36 -11
- package/src/createGithubPullRequest.ts +83 -33
- package/src/createIssueComment.ts +4 -4
- package/src/fetchComfyRegistryNodes.ts +34 -4
- package/src/fetchCurrentGeoInfo.ts +3 -1
- package/src/fetchRelatedPullWithComments.ts +1 -1
- package/src/fetchRepoDescriptionMap.ts +1 -4
- package/src/followRuleSchema.ts +10 -4
- package/src/getBranchWorkingDir.ts +3 -7
- package/src/getRepoWorkingDir.ts +2 -3
- package/src/gh.spec.ts +571 -0
- package/src/ghData.ts +13 -0
- package/src/ghPageFlow.ts +33 -0
- package/src/ghSubscriber.ts +76 -0
- package/src/ghUser.ts +28 -4
- package/src/index.ts +13 -4
- package/src/initializeFollowRules.ts +11 -4
- package/src/logger.spec.ts +95 -0
- package/src/logger.ts +46 -0
- package/src/makeGpl3LicenseBranch.ts +66 -0
- package/src/makePublishBranch.ts +21 -15
- package/src/makeTomlBranch.ts +48 -14
- package/src/makeUpdateTomlLicenseBranch.ts +40 -44
- package/src/matchRelatedPulls.ts +9 -4
- package/src/normalizeGithubUrl.spec.ts +131 -0
- package/src/normalizeGithubUrl.ts +64 -0
- package/src/parseIssueUrl.spec.ts +95 -0
- package/src/parseIssueUrl.ts +17 -3
- package/src/parseOwnerRepo.spec.ts +147 -0
- package/src/parseOwnerRepo.ts +8 -5
- package/src/parsePullsState.ts +2 -2
- package/src/parseTitleBodyOfMarkdown.ts +1 -1
- package/src/pickRepoInfo.ts +37 -0
- package/src/postSlackMessage.ts +5 -1
- package/src/preload.ts +30 -27
- package/src/readTemplateTitle.ts +1 -3
- package/src/sendEmailAction.ts +14 -10
- package/src/sendGmail.ts +3 -3
- package/src/updateAuthorsForGithub.ts +53 -35
- package/src/updateAuthorsFromCNRepo.ts +12 -5
- package/src/updateCMNodesDuplicationWarnings.ts +14 -9
- package/src/updateCMRepos.ts +1 -1
- package/src/updateCNRepos.ts +14 -14
- package/src/updateCNReposCRPullsComments.ts +7 -5
- package/src/updateCNReposInfo.ts +49 -40
- package/src/updateCNReposPRCandidate.ts +3 -3
- package/src/updateCNReposPulls.ts +9 -5
- package/src/updateCNReposPullsDashboard.ts +11 -9
- package/src/updateCNReposRelatedPulls.ts +3 -3
- package/src/updateCRNodes.ts +24 -5
- package/src/updateCRRepos.ts +1 -1
- package/src/updateComfyTotals.ts +7 -5
- package/src/updateFollowRuleSet.ts +17 -9
- package/src/updateOutdatedPullsTemplates.ts +61 -48
- package/src/updateSlackMessages.ts +8 -4
- package/tailwind.config.ts +7 -1
- package/next-env.d.ts +0 -5
- package/src/GIT_USEREMAIL.ts +0 -5
- package/src/GIT_USERNAME.ts +0 -9
- package/src/clone_modify_push_Branches.ts +0 -21
- package/src/tomlFillDescription.ts +0 -17
package/bot/slack-bot.ts
ADDED
|
@@ -0,0 +1,1575 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ComfyPR Bot
|
|
5
|
+
*
|
|
6
|
+
* Slack
|
|
7
|
+
|
|
8
|
+
*/
|
|
9
|
+
import { slack } from "@/lib";
|
|
10
|
+
import { yaml } from "@/src/utils/yaml";
|
|
11
|
+
import { SocketModeClient } from "@slack/socket-mode";
|
|
12
|
+
import {} from "@slack/bolt";
|
|
13
|
+
import DIE from "@snomiao/die";
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import { compareBy } from "comparing";
|
|
16
|
+
import { fromStdio } from "from-node-stream";
|
|
17
|
+
import { mkdir } from "fs/promises";
|
|
18
|
+
import sflow from "sflow";
|
|
19
|
+
import winston from "winston";
|
|
20
|
+
import zChatCompletion from "../lib/zChat";
|
|
21
|
+
import z from "zod";
|
|
22
|
+
import { IdleWaiter } from "./IdleWaiter";
|
|
23
|
+
import { RestartManager } from "./RestartManager";
|
|
24
|
+
import { parseSlackMessageToMarkdown } from "@/lib/slack/parseSlackMessageToMarkdown";
|
|
25
|
+
import { slackTsToISO } from "@/lib/slack/slackTsToISO";
|
|
26
|
+
import { safeSlackPostMessage, safeSlackUpdateMessage } from "@/lib/slack/safeSlackMessage";
|
|
27
|
+
import { slackMessageUrlParse } from "@/app/tasks/gh-design/slackMessageUrlParse";
|
|
28
|
+
import { TerminalTextRender } from "terminal-render";
|
|
29
|
+
import minimist from "minimist";
|
|
30
|
+
import { loadClaudeMd, loadSkills } from "./templateLoader";
|
|
31
|
+
import path from "path";
|
|
32
|
+
import { appendFile } from "fs/promises";
|
|
33
|
+
import fsp from "fs/promises";
|
|
34
|
+
import { mdFmt } from "@/app/tasks/gh-desktop-release-notification/upsertSlackMessage";
|
|
35
|
+
import { getSlackChannelName } from "@/lib/slack";
|
|
36
|
+
import { SlackBotState } from "./state";
|
|
37
|
+
import { ErrorCollector } from "./error-collector";
|
|
38
|
+
|
|
39
|
+
export const SLACK_ORG_DOMAIN_NAME = "comfy-organization";
|
|
40
|
+
// Configure winston logger
|
|
41
|
+
const logDate = new Date().toISOString().split("T")[0]; // YYYY-MM-DD format
|
|
42
|
+
const logger = winston.createLogger({
|
|
43
|
+
level: process.env.VERBOSE ? "debug" : process.env.LOG_LEVEL || "info",
|
|
44
|
+
format: winston.format.combine(
|
|
45
|
+
winston.format.timestamp(),
|
|
46
|
+
winston.format.errors({ stack: true }),
|
|
47
|
+
winston.format.printf(({ timestamp, level, message, ...meta }) => {
|
|
48
|
+
const metaStr = Object.keys(meta).length ? JSON.stringify(meta, null, 2) : "";
|
|
49
|
+
return `[${timestamp}] [${level.toUpperCase()}] ${message}${metaStr ? "\n" + metaStr : ""}`;
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
52
|
+
transports: [
|
|
53
|
+
new winston.transports.Console({
|
|
54
|
+
format: winston.format.combine(
|
|
55
|
+
winston.format.colorize(),
|
|
56
|
+
winston.format.printf(({ timestamp, level, message, ...meta }) => {
|
|
57
|
+
const metaStr = Object.keys(meta).length ? JSON.stringify(meta, null, 2) : "";
|
|
58
|
+
return `[${timestamp}] ${level}: ${message}${metaStr ? "\n" + metaStr : ""}`;
|
|
59
|
+
}),
|
|
60
|
+
),
|
|
61
|
+
}),
|
|
62
|
+
new winston.transports.File({
|
|
63
|
+
filename: `./.logs/bot-${logDate}.log`,
|
|
64
|
+
level: "debug",
|
|
65
|
+
format: winston.format.combine(
|
|
66
|
+
winston.format.timestamp(),
|
|
67
|
+
winston.format.printf(({ timestamp, level, message, ...meta }) => {
|
|
68
|
+
const metaStr = Object.keys(meta).length ? JSON.stringify(meta, null, 2) : "";
|
|
69
|
+
return `[${timestamp}] [${level.toUpperCase()}] ${message}${metaStr ? "\n" + metaStr : ""}`;
|
|
70
|
+
}),
|
|
71
|
+
),
|
|
72
|
+
}),
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const TaskInputFlows = new Map<string, TransformStream<string, string>>();
|
|
77
|
+
// https://comfy-pr-bot.pages.dev/
|
|
78
|
+
// Slack block type definition
|
|
79
|
+
const zSlackBlock = z
|
|
80
|
+
.object({
|
|
81
|
+
type: z.string(),
|
|
82
|
+
block_id: z.string().optional(),
|
|
83
|
+
elements: z.array(z.unknown()).optional(),
|
|
84
|
+
})
|
|
85
|
+
.passthrough();
|
|
86
|
+
|
|
87
|
+
// Slack attachment type definition
|
|
88
|
+
const zSlackAttachment = z
|
|
89
|
+
.object({
|
|
90
|
+
title: z.string().optional(),
|
|
91
|
+
title_link: z.string().optional(),
|
|
92
|
+
text: z.string().optional(),
|
|
93
|
+
fallback: z.string().optional(),
|
|
94
|
+
image_url: z.string().optional(),
|
|
95
|
+
from_url: z.string().optional(),
|
|
96
|
+
})
|
|
97
|
+
.passthrough();
|
|
98
|
+
|
|
99
|
+
const zAppMentionEvent = z.object({
|
|
100
|
+
type: z.literal("app_mention"),
|
|
101
|
+
user: z.string(),
|
|
102
|
+
ts: z.string(),
|
|
103
|
+
client_msg_id: z.string().optional(),
|
|
104
|
+
text: z.string(),
|
|
105
|
+
team: z.string(),
|
|
106
|
+
thread_ts: z.string().optional(),
|
|
107
|
+
parent_user_id: z.string().optional(),
|
|
108
|
+
blocks: z.array(zSlackBlock),
|
|
109
|
+
channel: z.string(),
|
|
110
|
+
assistant_thread: z.unknown().optional(),
|
|
111
|
+
attachments: z.array(zSlackAttachment).optional(),
|
|
112
|
+
event_ts: z.string(),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Helper functions to manage current working tasks
|
|
116
|
+
async function addWorkingTask(event: z.infer<typeof zAppMentionEvent>) {
|
|
117
|
+
const workingTasks = (await SlackBotState.get("current-working-tasks")) || {
|
|
118
|
+
workingMessageEvents: [],
|
|
119
|
+
};
|
|
120
|
+
const events = workingTasks.workingMessageEvents || [];
|
|
121
|
+
|
|
122
|
+
// Check if event already exists (by ts and channel)
|
|
123
|
+
const exists = events.some(
|
|
124
|
+
(e: z.infer<typeof zAppMentionEvent>) => e.ts === event.ts && e.channel === event.channel,
|
|
125
|
+
);
|
|
126
|
+
if (!exists) {
|
|
127
|
+
events.push(event);
|
|
128
|
+
await SlackBotState.set("current-working-tasks", { workingMessageEvents: events });
|
|
129
|
+
logger.info(`Added task to working list: ${event.ts} (total: ${events.length})`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function removeWorkingTask(event: z.infer<typeof zAppMentionEvent>) {
|
|
134
|
+
const workingTasks = (await SlackBotState.get("current-working-tasks")) || {
|
|
135
|
+
workingMessageEvents: [],
|
|
136
|
+
};
|
|
137
|
+
const events = workingTasks.workingMessageEvents || [];
|
|
138
|
+
|
|
139
|
+
// Remove event by ts and channel
|
|
140
|
+
const filtered = events.filter(
|
|
141
|
+
(e: z.infer<typeof zAppMentionEvent>) => !(e.ts === event.ts && e.channel === event.channel),
|
|
142
|
+
);
|
|
143
|
+
await SlackBotState.set("current-working-tasks", { workingMessageEvents: filtered });
|
|
144
|
+
logger.info(`Removed task from working list: ${event.ts} (remaining: ${filtered.length})`);
|
|
145
|
+
}
|
|
146
|
+
const g = globalThis as typeof globalThis & { instanceId?: string; hotId?: string };
|
|
147
|
+
const now = new Date().toISOString();
|
|
148
|
+
g.instanceId ??= now;
|
|
149
|
+
g.hotId = now;
|
|
150
|
+
|
|
151
|
+
if (import.meta.main) {
|
|
152
|
+
await startSlackBot();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function startSlackBot() {
|
|
156
|
+
console.log("Starting ComfyPR Bot...");
|
|
157
|
+
const argv = minimist(process.argv.slice(2));
|
|
158
|
+
const port = Number(process.env.PRBOT_PORT || DIE("missing env.PRBOT_PORT"));
|
|
159
|
+
|
|
160
|
+
// Step 1: Health check (only for non-PTY launches)
|
|
161
|
+
const isHumanLaunched = process.stdin.isTTY;
|
|
162
|
+
|
|
163
|
+
if (!isHumanLaunched) {
|
|
164
|
+
// Non-PTY launch (PM2): Poll for 10 seconds to ensure port is continuously unhealthy
|
|
165
|
+
logger.info(`Detected non-PTY launch - polling for 10s to ensure port ${port} is unhealthy`);
|
|
166
|
+
|
|
167
|
+
const pollDuration = 10000; // 10 seconds
|
|
168
|
+
const pollInterval = 1000; // 1 second
|
|
169
|
+
const startTime = Date.now();
|
|
170
|
+
let healthyInstanceFound = false;
|
|
171
|
+
|
|
172
|
+
while (Date.now() - startTime < pollDuration) {
|
|
173
|
+
try {
|
|
174
|
+
const statusResp = await fetch(`http://localhost:${port}/status`, {
|
|
175
|
+
signal: AbortSignal.timeout(1000),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (statusResp.ok) {
|
|
179
|
+
// Found a healthy instance - abort and exit
|
|
180
|
+
const statusData = await statusResp.json();
|
|
181
|
+
healthyInstanceFound = true;
|
|
182
|
+
|
|
183
|
+
// Try to get PID of existing process
|
|
184
|
+
let existingPid = "unknown";
|
|
185
|
+
try {
|
|
186
|
+
const lsofOutput = await Bun.$`lsof -ti:${port}`.text();
|
|
187
|
+
existingPid = lsofOutput.trim();
|
|
188
|
+
} catch {}
|
|
189
|
+
|
|
190
|
+
logger.info(
|
|
191
|
+
`Healthy instance detected (PID: ${existingPid}) - aborting launch to avoid conflict`,
|
|
192
|
+
);
|
|
193
|
+
logger.info(`Status: ${JSON.stringify(statusData)}`);
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
} catch (err) {
|
|
197
|
+
// Port is unhealthy/unreachable - this is expected
|
|
198
|
+
logger.debug(
|
|
199
|
+
`Health check: port ${port} is unhealthy (${Date.now() - startTime}ms elapsed)`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
await sleep(pollInterval);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!healthyInstanceFound) {
|
|
207
|
+
logger.info(`Port ${port} remained unhealthy for 10s - proceeding to launch`);
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
// PTY launch (human): Skip health check entirely
|
|
211
|
+
logger.info(`Detected PTY launch - skipping health check`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Step 2: Kill port and launch
|
|
215
|
+
logger.info(`Killing port ${port} and starting server`);
|
|
216
|
+
await Bun.$`npx -y kill-port ${port}`;
|
|
217
|
+
|
|
218
|
+
const server = Bun.serve({
|
|
219
|
+
port: port,
|
|
220
|
+
fetch: async (req: Request) => {
|
|
221
|
+
const url = new URL(req.url);
|
|
222
|
+
|
|
223
|
+
if (url.pathname === "/status") {
|
|
224
|
+
// Get current working tasks from state
|
|
225
|
+
const workingTasks = (await SlackBotState.get("current-working-tasks")) || {
|
|
226
|
+
workingMessageEvents: [],
|
|
227
|
+
};
|
|
228
|
+
const events = workingTasks.workingMessageEvents || [];
|
|
229
|
+
|
|
230
|
+
// Build message URLs from events
|
|
231
|
+
const processing_message_urls = events.map((event: z.infer<typeof zAppMentionEvent>) => {
|
|
232
|
+
const tsForUrl = event.ts.replace(".", "");
|
|
233
|
+
return `https://${SLACK_ORG_DOMAIN_NAME}.slack.com/archives/${event.channel}/p${tsForUrl}`;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const status = {
|
|
237
|
+
status: TaskInputFlows.size === 0 ? "idle" : "busy",
|
|
238
|
+
processing_message_urls,
|
|
239
|
+
processing_message_urls_count: processing_message_urls.length,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
return new Response(JSON.stringify(status, null, 2), {
|
|
243
|
+
status: 200,
|
|
244
|
+
headers: { "Content-Type": "application/json" },
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return new Response("ComfyPR Bot is running.\n", { status: 200 });
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// const missedMsg = "https://comfy-organization.slack.com/archives/C09QKKXK8RX/p1767849032076329?thread_ts=1767838632.470639&cid=C09QKKXK8RX"
|
|
253
|
+
// const missedMsg = "https://comfy-organization.slack.com/archives/C0A4XMHANP3/p1767893546609709?thread_ts=1767862331.962569&cid=C0A4XMHANP3"
|
|
254
|
+
// await spawnBotOnSlackMessageUrl(
|
|
255
|
+
// "https://comfy-organization.slack.com/archives/D09GGTE7S00/p1769576340892099",
|
|
256
|
+
// );
|
|
257
|
+
|
|
258
|
+
const msgs = await fsp
|
|
259
|
+
.readFile("./msgs.yaml", "utf-8")
|
|
260
|
+
.then((s) => yaml.parse(s))
|
|
261
|
+
.then((e) => z.object({ missed: z.string().array() }).parseAsync(e));
|
|
262
|
+
await fsp.writeFile("./msgs.yaml", "missed: []");
|
|
263
|
+
|
|
264
|
+
// clean the file
|
|
265
|
+
//
|
|
266
|
+
sflow(msgs.missed)
|
|
267
|
+
.forEach((url) => spawnBotOnSlackMessageUrl(url))
|
|
268
|
+
.run();
|
|
269
|
+
|
|
270
|
+
if (argv.continue) {
|
|
271
|
+
async () => {
|
|
272
|
+
logger.info("BOT - --continue flag detected, resuming crashed tasks...");
|
|
273
|
+
|
|
274
|
+
// Read current working tasks from state
|
|
275
|
+
const workingTasks = (await SlackBotState.get("current-working-tasks")) || {
|
|
276
|
+
workingMessageEvents: [],
|
|
277
|
+
};
|
|
278
|
+
const events = workingTasks.workingMessageEvents || [];
|
|
279
|
+
|
|
280
|
+
if (events.length === 0) {
|
|
281
|
+
logger.info("No working tasks to resume");
|
|
282
|
+
} else {
|
|
283
|
+
logger.info(`Found ${events.length} working task(s) to resume`);
|
|
284
|
+
|
|
285
|
+
for await (const event of events) {
|
|
286
|
+
if (event && event.ts) {
|
|
287
|
+
logger.info(
|
|
288
|
+
`Resuming task for event ${event.ts} in channel ${await getSlackChannelName(event.channel)}, text: ${event.text}`,
|
|
289
|
+
);
|
|
290
|
+
await spawnBotOnSlackMessageEvent(event).catch((err) => {
|
|
291
|
+
logger.error(`Error resuming task for event ${event.ts}`, { err });
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
logger.info(`Starting ComfyPR Bot... id: ${g.instanceId}, hotId: ${g.hotId}`);
|
|
300
|
+
|
|
301
|
+
// Setup smart restart manager (only restart when bot is idle)
|
|
302
|
+
if (!argv["no-watch"]) {
|
|
303
|
+
const restartManager = new RestartManager({
|
|
304
|
+
watchPaths: ["bot", "src", "lib"],
|
|
305
|
+
isIdle: () => TaskInputFlows.size === 0,
|
|
306
|
+
onRestart: () => {
|
|
307
|
+
logger.warn("🔄 Restarting bot process...");
|
|
308
|
+
process.exit(0);
|
|
309
|
+
},
|
|
310
|
+
idleCheckInterval: 5000,
|
|
311
|
+
debounceDelay: 1000,
|
|
312
|
+
logger: {
|
|
313
|
+
info: (msg, meta) => logger.info(`[RestartManager] ${msg}`, meta),
|
|
314
|
+
warn: (msg, meta) => logger.warn(`[RestartManager] ${msg}`, meta),
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
restartManager.start();
|
|
318
|
+
logger.info("Smart restart manager enabled (use --no-watch to disable)");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Initialize Socket Mode client with app-level token
|
|
322
|
+
const socketModeClient = new SocketModeClient({
|
|
323
|
+
appToken: process.env.SLACK_SOCKET_TOKEN || DIE("missing env.SLACK_SOCKET_TOKEN"),
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Handle all events via events_api envelope, https://docs.slack.dev/reference/events/message
|
|
327
|
+
socketModeClient
|
|
328
|
+
.on("app_mention", async ({ event, body, ack }) => {
|
|
329
|
+
const parsedEvent = await zAppMentionEvent.parseAsync(event);
|
|
330
|
+
|
|
331
|
+
// Acknowledge the event as its parsed
|
|
332
|
+
await ack();
|
|
333
|
+
await spawnBotOnSlackMessageEvent(parsedEvent);
|
|
334
|
+
})
|
|
335
|
+
.on("message", async ({ event, body, ack }) => {
|
|
336
|
+
// bot-1 | msg: {"type":"message","user":"U04F3GHTG2X","ts":"1767100459.669809","client_msg_id":"2fed13c0-9739-4888-a4f6-b876c25f1407","text":"test","team":"T0462DJ9G3C","blocks":[{"type":"rich_text","block_id":"gB9fq","elements":[{"type":"rich_text_section","elements":[{"type":"text","text":"test"}]}]}],"channel":"C0A6Y4AU52L","event_ts":"1767100459.669809","channel_type":"channel"}
|
|
337
|
+
// Parse the message event
|
|
338
|
+
const zSlackMessage = z
|
|
339
|
+
.object({
|
|
340
|
+
type: z.literal("message"),
|
|
341
|
+
user: z.string().optional(),
|
|
342
|
+
ts: z.string().optional(),
|
|
343
|
+
client_msg_id: z.string().optional(),
|
|
344
|
+
text: z.string().optional(),
|
|
345
|
+
team: z.string().optional(),
|
|
346
|
+
thread_ts: z.string().optional(),
|
|
347
|
+
parent_user_id: z.string().optional(),
|
|
348
|
+
blocks: z.array(zSlackBlock).optional(),
|
|
349
|
+
channel: z.string().optional(),
|
|
350
|
+
channel_type: z.string().optional(),
|
|
351
|
+
assistant_thread: z.unknown().optional(),
|
|
352
|
+
attachments: z.array(zSlackAttachment).optional(),
|
|
353
|
+
event_ts: z.string().optional(),
|
|
354
|
+
bot_id: z.string().optional(),
|
|
355
|
+
})
|
|
356
|
+
.passthrough();
|
|
357
|
+
|
|
358
|
+
const messageEvent = zSlackMessage.parse(event);
|
|
359
|
+
|
|
360
|
+
logger.debug("MESSAGE EVENT", { event });
|
|
361
|
+
logger.debug("parsed_text: " + (await parseSlackMessageToMarkdown(messageEvent.text || "")));
|
|
362
|
+
|
|
363
|
+
await ack();
|
|
364
|
+
|
|
365
|
+
// Skip bot messages
|
|
366
|
+
if (messageEvent.bot_id) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Get my bot user ID
|
|
371
|
+
const botUsername = "comfyprbot";
|
|
372
|
+
// TODO: fetch botUserId by botUsername or use slack api to "get my name"
|
|
373
|
+
const botUserId = process.env.SLACK_BOT_USER_ID || "U078499LK5K"; // ComfyPR-Bot user ID
|
|
374
|
+
|
|
375
|
+
// Check if message mentions the bot
|
|
376
|
+
const text = messageEvent.text || "";
|
|
377
|
+
const hasBotMention = text.includes(`<@${botUserId}>`);
|
|
378
|
+
|
|
379
|
+
// Handle DM messages (channel_type: "im") and treat them like app mentions
|
|
380
|
+
const isDM = messageEvent.channel_type === "im";
|
|
381
|
+
|
|
382
|
+
if (
|
|
383
|
+
(isDM || hasBotMention) &&
|
|
384
|
+
messageEvent.user &&
|
|
385
|
+
messageEvent.text &&
|
|
386
|
+
messageEvent.channel &&
|
|
387
|
+
messageEvent.ts &&
|
|
388
|
+
messageEvent.team &&
|
|
389
|
+
messageEvent.event_ts
|
|
390
|
+
) {
|
|
391
|
+
const eventType = isDM ? "DM" : "BOT MENTION";
|
|
392
|
+
logger.debug(`${eventType} DETECTED - Processing message as app_mention`, {
|
|
393
|
+
channel: messageEvent.channel,
|
|
394
|
+
ts: messageEvent.ts,
|
|
395
|
+
text: text.substring(0, 100),
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const mentionEvent: z.infer<typeof zAppMentionEvent> = {
|
|
399
|
+
type: "app_mention" as const,
|
|
400
|
+
user: messageEvent.user,
|
|
401
|
+
ts: messageEvent.ts,
|
|
402
|
+
client_msg_id: messageEvent.client_msg_id,
|
|
403
|
+
text: messageEvent.text,
|
|
404
|
+
team: messageEvent.team,
|
|
405
|
+
thread_ts: messageEvent.thread_ts,
|
|
406
|
+
parent_user_id: messageEvent.parent_user_id,
|
|
407
|
+
blocks: messageEvent.blocks || [],
|
|
408
|
+
channel: messageEvent.channel,
|
|
409
|
+
assistant_thread: messageEvent.assistant_thread,
|
|
410
|
+
attachments: messageEvent.attachments,
|
|
411
|
+
event_ts: messageEvent.event_ts,
|
|
412
|
+
};
|
|
413
|
+
await spawnBotOnSlackMessageEvent(mentionEvent);
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
.on("error", (error) => {
|
|
417
|
+
logger.error("Socket Mode error", { error });
|
|
418
|
+
})
|
|
419
|
+
.on("connect", () => logger.info("SOCKET - Slack connected"))
|
|
420
|
+
.on("disconnect", () => logger.info("SOCKET - Slack disconnected"))
|
|
421
|
+
.on("ready", () => logger.info("SOCKET - Ready to receive events"));
|
|
422
|
+
|
|
423
|
+
logger.info("BOT - Connecting to Slack Socket Mode...");
|
|
424
|
+
await socketModeClient.start();
|
|
425
|
+
logger.info("BOT - socketModeClient.start() returned");
|
|
426
|
+
return socketModeClient;
|
|
427
|
+
}
|
|
428
|
+
async function spawnBotOnSlackMessageEvent(event: z.infer<typeof zAppMentionEvent>) {
|
|
429
|
+
// msg dedup for same content
|
|
430
|
+
const eventProcessed = await SlackBotState.get(`msg-${event.ts}`);
|
|
431
|
+
// if (eventProcessed?.content === event.text) return;
|
|
432
|
+
if (+new Date() - (eventProcessed?.touchedAt ?? 0) <= 10e3) return; // debounce for 10s
|
|
433
|
+
await SlackBotState.set(`msg-${event.ts}`, { touchedAt: +new Date(), content: event.text });
|
|
434
|
+
|
|
435
|
+
logger.info(
|
|
436
|
+
await parseSlackMessageToMarkdown(
|
|
437
|
+
`SPAWN - Received Slack app_mention event in channel <#${event.channel}> from user <@${event.user}>`,
|
|
438
|
+
),
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
// whitelist channel name #comfypr-bot, for security reason, only runs agent against @mention messages in #comfyprbot channel
|
|
442
|
+
// you can forward other channel messages to #comfyprbot if needed, and the bot will read the context from the original thread messages
|
|
443
|
+
// DMs are also allowed to spawn agents directly
|
|
444
|
+
const channelInfo = await slack.conversations.info({ channel: event.channel });
|
|
445
|
+
const channelName = channelInfo.channel?.name;
|
|
446
|
+
const isDM = channelInfo.channel?.is_im === true;
|
|
447
|
+
const isAgentChannel = isDM || channelName?.match(/^(comfypr-bot|pr-bot)\b/); //starts with comfyprbot or pr-bot, will spawn agent without requireing @mention
|
|
448
|
+
|
|
449
|
+
const user =
|
|
450
|
+
(await slack.users.info({ user: event.user })).user ||
|
|
451
|
+
DIE("failed to fetch user info of <@" + event.user + ">");
|
|
452
|
+
if (user?.is_restricted || user?.is_ultra_restricted) {
|
|
453
|
+
logger.info(`User ${event.user} is a guest user, skipping processing.`);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const username =
|
|
458
|
+
user.name ||
|
|
459
|
+
user.id?.replace(/(.*)/, "<@$1>") ||
|
|
460
|
+
DIE("failed to get username of <@" + event.user + ">");
|
|
461
|
+
|
|
462
|
+
// task state
|
|
463
|
+
const workspaceId = event.thread_ts || event.ts;
|
|
464
|
+
const eventId = event.channel + "_" + event.ts;
|
|
465
|
+
logger.info(
|
|
466
|
+
`Processing Slack app_mention event in channel ${event.channel} (isAgentChannel: ${isAgentChannel}) with workspaceId: ${workspaceId}`,
|
|
467
|
+
);
|
|
468
|
+
const botWorkingDir = `/bot/slack/${sanitized(channelName || username)}/${workspaceId.replace(".", "-")}`;
|
|
469
|
+
const task = await SlackBotState.get(`task-${workspaceId}`);
|
|
470
|
+
|
|
471
|
+
// Allow append messages to running task
|
|
472
|
+
|
|
473
|
+
// grab 100 most nearby messages in this thread or channel
|
|
474
|
+
const nearbyMessagesResp = await slack.conversations.replies({
|
|
475
|
+
channel: event.channel,
|
|
476
|
+
ts: workspaceId,
|
|
477
|
+
limit: 100,
|
|
478
|
+
});
|
|
479
|
+
// Type definitions for Slack message components
|
|
480
|
+
type SlackFile = {
|
|
481
|
+
name?: string;
|
|
482
|
+
title?: string;
|
|
483
|
+
mimetype?: string;
|
|
484
|
+
size?: number;
|
|
485
|
+
url_private?: string;
|
|
486
|
+
permalink?: string;
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
type SlackReaction = {
|
|
490
|
+
name?: string;
|
|
491
|
+
count?: number;
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const nearbyMessages = (
|
|
495
|
+
await sflow(nearbyMessagesResp.messages || [])
|
|
496
|
+
.map(async (m) => ({
|
|
497
|
+
username: await slack.users
|
|
498
|
+
.info({ user: m.user || DIE("missing user id in message") })
|
|
499
|
+
.then((res) => res.user?.name || "<@" + m.user + ">"),
|
|
500
|
+
markdown: await parseSlackMessageToMarkdown(m.text || ""),
|
|
501
|
+
ts: m.ts,
|
|
502
|
+
iso: slackTsToISO(m.ts || DIE("missing ts")),
|
|
503
|
+
...(m.files &&
|
|
504
|
+
m.files.length > 0 && {
|
|
505
|
+
files: m.files.map((f: unknown) => {
|
|
506
|
+
const file = f as SlackFile;
|
|
507
|
+
return {
|
|
508
|
+
name: file.name,
|
|
509
|
+
title: file.title,
|
|
510
|
+
mimetype: file.mimetype,
|
|
511
|
+
size: file.size,
|
|
512
|
+
url_private: file.url_private,
|
|
513
|
+
permalink: file.permalink,
|
|
514
|
+
};
|
|
515
|
+
}),
|
|
516
|
+
}),
|
|
517
|
+
...(m.attachments &&
|
|
518
|
+
m.attachments.length > 0 && {
|
|
519
|
+
attachments: await Promise.all(
|
|
520
|
+
m.attachments.map(async (a: unknown) => {
|
|
521
|
+
const attachment = a as z.infer<typeof zSlackAttachment>;
|
|
522
|
+
// Parse from_url to extract channel name
|
|
523
|
+
let from_channel: string | undefined;
|
|
524
|
+
if (attachment.from_url) {
|
|
525
|
+
try {
|
|
526
|
+
const parsed = slackMessageUrlParse(attachment.from_url);
|
|
527
|
+
const channelInfo = await slack.conversations.info({ channel: parsed.channel });
|
|
528
|
+
from_channel = channelInfo.channel?.name
|
|
529
|
+
? `#${channelInfo.channel.name}`
|
|
530
|
+
: undefined;
|
|
531
|
+
} catch {
|
|
532
|
+
// Ignore parsing errors
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return {
|
|
536
|
+
title: attachment.title,
|
|
537
|
+
title_link: attachment.title_link,
|
|
538
|
+
text: attachment.text
|
|
539
|
+
? await parseSlackMessageToMarkdown(attachment.text)
|
|
540
|
+
: undefined,
|
|
541
|
+
fallback: attachment.fallback
|
|
542
|
+
? await parseSlackMessageToMarkdown(attachment.fallback)
|
|
543
|
+
: undefined,
|
|
544
|
+
image_url: attachment.image_url,
|
|
545
|
+
from_url: attachment.from_url,
|
|
546
|
+
from_channel, // Add resolved channel name
|
|
547
|
+
};
|
|
548
|
+
}),
|
|
549
|
+
),
|
|
550
|
+
}),
|
|
551
|
+
...(m.reactions &&
|
|
552
|
+
m.reactions.length > 0 && {
|
|
553
|
+
reactions: m.reactions.map((r: unknown) => {
|
|
554
|
+
const reaction = r as SlackReaction;
|
|
555
|
+
return {
|
|
556
|
+
name: reaction.name,
|
|
557
|
+
count: reaction.count,
|
|
558
|
+
};
|
|
559
|
+
}),
|
|
560
|
+
}),
|
|
561
|
+
}))
|
|
562
|
+
.toArray()
|
|
563
|
+
).toSorted(compareBy((e) => +(e.ts || 0))); // sort by ts asc
|
|
564
|
+
|
|
565
|
+
const existedTaskInputFlow = TaskInputFlows.get(workspaceId);
|
|
566
|
+
if (existedTaskInputFlow && false) {
|
|
567
|
+
// disable for now, lets use --queue to serialize tasks
|
|
568
|
+
// while agent still running, user sent new message in the same thread very quickly
|
|
569
|
+
// lets understand the user's intent and give a quick response, and then append the msg to the existing agent input flow
|
|
570
|
+
// const threadMessages = await pageFlow(undefined as undefined | string, async (cursor, limit = 100) => {
|
|
571
|
+
// const resp = await slack.conversations.replies({
|
|
572
|
+
// channel: event.channel,
|
|
573
|
+
// ts: workspaceId,
|
|
574
|
+
// cursor,
|
|
575
|
+
// limit,
|
|
576
|
+
// });
|
|
577
|
+
// return {
|
|
578
|
+
// data: resp.messages || [],
|
|
579
|
+
// next: resp.response_metadata?.next_cursor,
|
|
580
|
+
// };
|
|
581
|
+
// })
|
|
582
|
+
// .flat()
|
|
583
|
+
// .map(async (m) => ({
|
|
584
|
+
// ts: slackTsToISO(m.ts || DIE("missing ts")),
|
|
585
|
+
// username: await slack.users
|
|
586
|
+
// .info({ user: m.user || DIE("missing user id in message") })
|
|
587
|
+
// .then((res) => res.user?.name || "<@" + m.user + ">"),
|
|
588
|
+
// markdown: await parseSlackMessageToMarkdown(m.text || ""),
|
|
589
|
+
// }))
|
|
590
|
+
// .toArray();
|
|
591
|
+
|
|
592
|
+
// use LLM to understand the new message intent
|
|
593
|
+
const action = await zChatCompletion(
|
|
594
|
+
z.object({
|
|
595
|
+
user_intent: z.string(),
|
|
596
|
+
my_quick_respond: z.string(),
|
|
597
|
+
stop_existing_task: z.boolean(),
|
|
598
|
+
msg_to_append_to_agent: z.string(),
|
|
599
|
+
}),
|
|
600
|
+
{ model: "gpt-4o" },
|
|
601
|
+
)`
|
|
602
|
+
The user sent a new message in a Slack thread where I am already assisting them with an ongoing task. The new message is as follows:
|
|
603
|
+
${event.text}
|
|
604
|
+
|
|
605
|
+
The thread's recent messages are:
|
|
606
|
+
${((data: string) => {
|
|
607
|
+
logger.debug("Thread messages:", { data });
|
|
608
|
+
return data;
|
|
609
|
+
})(
|
|
610
|
+
yaml.stringify(
|
|
611
|
+
nearbyMessages.toSorted(compareBy((e) => +(e.ts || 0))), // sort by ts asc
|
|
612
|
+
),
|
|
613
|
+
)}
|
|
614
|
+
|
|
615
|
+
Based on the new message and the thread context,
|
|
616
|
+
|
|
617
|
+
Please analyze the new message and determine:
|
|
618
|
+
1. The user's intent behind this new message.
|
|
619
|
+
2. A quick response I can send to the user right away to acknowledge their new message.
|
|
620
|
+
3. Whether I should append this new message to the existing task's input flow for further processing.
|
|
621
|
+
4. Whether I should stop the existing task based on this new message.
|
|
622
|
+
|
|
623
|
+
Respond in JSON format with the following fields:
|
|
624
|
+
- user_intent: A brief description of the user's intent regarding the new message.
|
|
625
|
+
- my_quick_respond: A short message I can send to the user immediately.
|
|
626
|
+
- stop_existing_task: true or false, indicating whether to stop the existing task.
|
|
627
|
+
- msg_to_append_to_agent: The content of the new message to append to the existing task's input flow. Use empty string "" if not applicable.
|
|
628
|
+
`;
|
|
629
|
+
logger.info("New message intent analysis", { action });
|
|
630
|
+
|
|
631
|
+
// send quick response
|
|
632
|
+
const myQuickRespondMsg = await safeSlackPostMessage(slack, {
|
|
633
|
+
channel: event.channel,
|
|
634
|
+
thread_ts: event.ts,
|
|
635
|
+
text: action.my_quick_respond, // Fallback text for notifications
|
|
636
|
+
blocks: [
|
|
637
|
+
{
|
|
638
|
+
type: "markdown",
|
|
639
|
+
text: action.my_quick_respond,
|
|
640
|
+
},
|
|
641
|
+
],
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
if (action.stop_existing_task) {
|
|
645
|
+
// stop existing task
|
|
646
|
+
TaskInputFlows.delete(workspaceId);
|
|
647
|
+
await safeSlackPostMessage(slack, {
|
|
648
|
+
channel: event.channel,
|
|
649
|
+
thread_ts: event.thread_ts || event.ts,
|
|
650
|
+
text: `The existing task has been stopped as per your request.`, // Fallback text for notifications
|
|
651
|
+
blocks: [
|
|
652
|
+
{
|
|
653
|
+
type: "markdown",
|
|
654
|
+
text: `The existing task has been stopped as per your request.`,
|
|
655
|
+
},
|
|
656
|
+
],
|
|
657
|
+
});
|
|
658
|
+
await SlackBotState.set(`task-${workspaceId}`, {
|
|
659
|
+
...(await SlackBotState.get(`task-${workspaceId}`)),
|
|
660
|
+
status: "stopped_by_user",
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
// Remove task from working list
|
|
664
|
+
await removeWorkingTask(event);
|
|
665
|
+
|
|
666
|
+
return "existing task stopped by user";
|
|
667
|
+
}
|
|
668
|
+
if (action.msg_to_append_to_agent && action.msg_to_append_to_agent.trim()) {
|
|
669
|
+
if (!existedTaskInputFlow) {
|
|
670
|
+
logger.warn("No existing task input flow found");
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const w = existedTaskInputFlow!.writable.getWriter();
|
|
674
|
+
await w.write(
|
|
675
|
+
await parseSlackMessageToMarkdown(
|
|
676
|
+
`New message from <@${event.user}> in the thread:\n${event.text}\n\nMy quick response to the user: ${action.my_quick_respond}\n\n`,
|
|
677
|
+
),
|
|
678
|
+
);
|
|
679
|
+
w.releaseLock();
|
|
680
|
+
logger.info(`Appended new message to existing task ${workspaceId} input flow`);
|
|
681
|
+
return "msg appended to existing task";
|
|
682
|
+
}
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const taskInputFlow = new TransformStream<string, string>();
|
|
687
|
+
TaskInputFlows.set(workspaceId, taskInputFlow); // able to append more inputs later
|
|
688
|
+
|
|
689
|
+
// mark that msg as seeing
|
|
690
|
+
await SlackBotState.set(`task-${workspaceId}`, {
|
|
691
|
+
...(await SlackBotState.get(`task-${workspaceId}`)),
|
|
692
|
+
status: "checking",
|
|
693
|
+
event,
|
|
694
|
+
startTime: Date.now(),
|
|
695
|
+
});
|
|
696
|
+
await slack.reactions
|
|
697
|
+
.add({ name: "eyes", channel: event.channel, timestamp: event.ts })
|
|
698
|
+
.catch(() => {});
|
|
699
|
+
|
|
700
|
+
// quick-intent-detect-respond by chatgpt, give quick plan/context responds before start heavy agent work
|
|
701
|
+
const resp = await zChatCompletion(
|
|
702
|
+
z.object({
|
|
703
|
+
user_intent: z.string(),
|
|
704
|
+
my_respond_before_spawn_agent: z.string(),
|
|
705
|
+
should_spawn_agent: z.boolean(),
|
|
706
|
+
}),
|
|
707
|
+
{
|
|
708
|
+
model: "gpt-4o",
|
|
709
|
+
},
|
|
710
|
+
)`
|
|
711
|
+
The user mentioned me with the following message in Slack: ${event.text}
|
|
712
|
+
Based on this message, please determine the user's intent in a concise manner.
|
|
713
|
+
Also, provide a brief response that I can send to the user immediately to acknowledge their request.
|
|
714
|
+
Finally, I will spawn an agent to help with this request if necessary.
|
|
715
|
+
|
|
716
|
+
For context, Recent messages from this thread are as follows:
|
|
717
|
+
${nearbyMessages.map((m) => `- User ${m.username} said: ${JSON.stringify(m.markdown)}`).join("\n\n")}
|
|
718
|
+
|
|
719
|
+
Possible Context Repos:
|
|
720
|
+
- https://github.com/comfyanonymous/ComfyUI: The main ComfyUI repository containing the core application logic and features. Its a python backend to run unknown machine learning models and solves various machine learning tasks.
|
|
721
|
+
- https://github.com/Comfy-Org/ComfyUI_frontend: The frontend codebase for ComfyuUI, built with Vue and TypeScript.
|
|
722
|
+
- https://github.com/Comfy-Org/docs: Documentation for ComfyUI, including setup guides, tutorials, and API references.
|
|
723
|
+
- https://github.com/Comfy-Org/desktop: The desktop application for ComfyUI, providing a user-friendly interface and additional functionalities.
|
|
724
|
+
- https://github.com/Comfy-Org/registry: The registry.comfy.org, where users can share and discover ComfyUI custom-nodes, and extensions.
|
|
725
|
+
- https://github.com/Comfy-Org/workflow_templates: A collection of official shared workflow templates for ComfyUI to help users get started quickly.
|
|
726
|
+
|
|
727
|
+
- https://github.com/Comfy-Org/comfy-api: A RESTful API service for comfy-registry, it stores custom-node metadatas and user profile/billings informations.
|
|
728
|
+
|
|
729
|
+
- And also other repos under Comfy-Org organization on GitHub.
|
|
730
|
+
|
|
731
|
+
Respond in JSON format with the following fields:
|
|
732
|
+
- user_intent: A brief description of the user's intent. e.g. "The user is asking for help with setting up a CI/CD pipeline."
|
|
733
|
+
- my_respond_before_spawn_agent: A short message I can send to the user right away. e.g. "Got it, let me look into that for you."
|
|
734
|
+
- should_spawn_agent: true if further research needed
|
|
735
|
+
`;
|
|
736
|
+
|
|
737
|
+
const myResponseMessage = await mdFmt(resp.my_respond_before_spawn_agent);
|
|
738
|
+
// - spawn_agent?: true or false, indicating whether an agent is needed to handle this request. e.g. if the user is asking for complex tasks like searching the web, managing repositories, or interacting with other services, or need to check original thread, set this to true.
|
|
739
|
+
logger.info("Intent detection response", JSON.stringify({ resp }));
|
|
740
|
+
|
|
741
|
+
// upsert quick respond msg
|
|
742
|
+
type QuickRespondMsg = { ts: string; text: string; channel?: string; url?: string };
|
|
743
|
+
const quickRespondMsg = await SlackBotState.get(`task-quick-respond-msg-${eventId}`).then(
|
|
744
|
+
async (existing: QuickRespondMsg | undefined) => {
|
|
745
|
+
if (existing) {
|
|
746
|
+
await slack.reactions
|
|
747
|
+
.remove({ name: "x", channel: existing.channel!, timestamp: existing.ts! })
|
|
748
|
+
.catch(() => {});
|
|
749
|
+
|
|
750
|
+
// if its a DM, always create a new message
|
|
751
|
+
// if (isDM) {
|
|
752
|
+
// const newMsg = await slack.chat.postMessage({
|
|
753
|
+
// channel: event.channel,
|
|
754
|
+
// thread_ts: event.ts,
|
|
755
|
+
// text: myResponseMessage,
|
|
756
|
+
// blocks: [
|
|
757
|
+
// {
|
|
758
|
+
// type: "markdown",
|
|
759
|
+
// text: myResponseMessage,
|
|
760
|
+
// },
|
|
761
|
+
// ],
|
|
762
|
+
// });
|
|
763
|
+
// await State.set(`task-quick-respond-msg-${eventId}`, { ts: newMsg.ts!, text: myResponseMessage });
|
|
764
|
+
// return { ...newMsg, text: myResponseMessage };
|
|
765
|
+
// }
|
|
766
|
+
// actually lets always post new msg for now.
|
|
767
|
+
// if (true) {
|
|
768
|
+
// const newMsg = await slack.chat.postMessage({
|
|
769
|
+
// channel: event.channel,
|
|
770
|
+
// thread_ts: event.ts,
|
|
771
|
+
// text: myResponseMessage,
|
|
772
|
+
// blocks: [
|
|
773
|
+
// {
|
|
774
|
+
// type: "markdown",
|
|
775
|
+
// text: myResponseMessage,
|
|
776
|
+
// },
|
|
777
|
+
// ],
|
|
778
|
+
// });
|
|
779
|
+
// await State.set(`task-quick-respond-msg-${eventId}`, { ts: newMsg.ts!, text: myResponseMessage });
|
|
780
|
+
// return { ...newMsg, text: myResponseMessage };
|
|
781
|
+
// }
|
|
782
|
+
|
|
783
|
+
const msg = await safeSlackUpdateMessage(slack, {
|
|
784
|
+
channel: event.channel,
|
|
785
|
+
ts: existing.ts,
|
|
786
|
+
text: myResponseMessage, // Fallback text for notifications
|
|
787
|
+
blocks: [
|
|
788
|
+
{
|
|
789
|
+
type: "markdown",
|
|
790
|
+
text: myResponseMessage,
|
|
791
|
+
},
|
|
792
|
+
],
|
|
793
|
+
});
|
|
794
|
+
await SlackBotState.set(`task-quick-respond-msg-${eventId}`, {
|
|
795
|
+
ts: existing.ts,
|
|
796
|
+
text: myResponseMessage,
|
|
797
|
+
channel: event.channel,
|
|
798
|
+
url: `https://${SLACK_ORG_DOMAIN_NAME}.slack.com/archives/${event.channel}/p${existing.ts.replace(".", "")}`,
|
|
799
|
+
});
|
|
800
|
+
return { ...msg, text: myResponseMessage };
|
|
801
|
+
} else {
|
|
802
|
+
const newMsg = await safeSlackPostMessage(slack, {
|
|
803
|
+
channel: event.channel,
|
|
804
|
+
thread_ts: event.ts,
|
|
805
|
+
text: myResponseMessage, // Fallback text for notifications
|
|
806
|
+
blocks: [
|
|
807
|
+
{
|
|
808
|
+
type: "markdown",
|
|
809
|
+
text: myResponseMessage,
|
|
810
|
+
},
|
|
811
|
+
],
|
|
812
|
+
});
|
|
813
|
+
await SlackBotState.set(`task-quick-respond-msg-${eventId}`, {
|
|
814
|
+
ts: newMsg.ts!,
|
|
815
|
+
text: myResponseMessage,
|
|
816
|
+
channel: event.channel,
|
|
817
|
+
url: `https://${SLACK_ORG_DOMAIN_NAME}.slack.com/archives/${event.channel}/p${newMsg.ts!.replace(".", "")}`,
|
|
818
|
+
});
|
|
819
|
+
return { ...newMsg, text: myResponseMessage };
|
|
820
|
+
}
|
|
821
|
+
},
|
|
822
|
+
);
|
|
823
|
+
|
|
824
|
+
// and now, lets update quickRespondMsg freq until user is satisfied or agent finished its work
|
|
825
|
+
|
|
826
|
+
// spawn agent if needed & allowed
|
|
827
|
+
// if (!resp.should_spawn_agent) {
|
|
828
|
+
// // update status
|
|
829
|
+
// await slack.reactions.remove({ name: 'eyes', channel: event.channel, timestamp: event.ts, });
|
|
830
|
+
// await slack.reactions.add({ name: 'white_check_mark', channel: event.channel, timestamp: event.ts, });j
|
|
831
|
+
// await State.set(`task-${workspaceId}`, { ...await State.get(`task-${workspaceId}`), status: 'done' });
|
|
832
|
+
// return 'no agent spawned'
|
|
833
|
+
// }
|
|
834
|
+
|
|
835
|
+
// The problem not easy to solve in original thread, lets forward this message to #prbot channel, and then spawn agent using that message.
|
|
836
|
+
// if (!isAgentChannel) {
|
|
837
|
+
// // update status, remove eye, add forwarding reaction
|
|
838
|
+
// await slack.reactions.remove({ name: "eyes", channel: event.channel, timestamp: event.ts }).catch(() => { });
|
|
839
|
+
// await slack.reactions.add({ name: "arrow_right", channel: event.channel, timestamp: event.ts }).catch(() => { });
|
|
840
|
+
|
|
841
|
+
// const originalMessageUrl = `https://${event.team}.slack.com/archives/${event.channel}/p${event.ts.replace(".", "")}`;
|
|
842
|
+
// // forward msg to #prbot channel, mention original msg user:content, and the original msg url for agent to read
|
|
843
|
+
// const agentChannelId =
|
|
844
|
+
// (await slack.conversations.list({ types: "public_channel" })).channels?.find((c) => c.name === "pr-bot")?.id ||
|
|
845
|
+
// DIE("failed to find #prbot channel id");
|
|
846
|
+
// // this is a user facing msg to tell user we are forwarding the msg
|
|
847
|
+
// const text = `Forwarded message from <@${event.user}> in <#${event.channel}>:\n${await parseSlackMessageToMarkdown(event.text)}\n\nYou can view the original message here: ${originalMessageUrl}`;
|
|
848
|
+
// const forwardedMsg = await slack.chat.postMessage({
|
|
849
|
+
// channel: agentChannelId,
|
|
850
|
+
// text,
|
|
851
|
+
// });
|
|
852
|
+
|
|
853
|
+
// // mention forwarded msg in original thread says I will continue there
|
|
854
|
+
// await slack.chat.update({
|
|
855
|
+
// channel: event.channel,
|
|
856
|
+
// ts: quickRespondMsg.ts!,
|
|
857
|
+
// markdown_text: `${myResponseMessage}\n\nI have forwarded your message to <#${agentChannelId}>. I will continue the research there.`,
|
|
858
|
+
// });
|
|
859
|
+
// await State.set(`task-${workspaceId}`, { ...(await State.get(`task-${workspaceId}`)), status: "forward_to_pr_bot_channel" });
|
|
860
|
+
|
|
861
|
+
// // process the forwarded message in agent channel
|
|
862
|
+
// return await spawnBotOnSlackMessageEvent({
|
|
863
|
+
// ...event,
|
|
864
|
+
// channel: agentChannelId,
|
|
865
|
+
// ts: forwardedMsg.ts!,
|
|
866
|
+
// thread_ts: undefined,
|
|
867
|
+
// text: forwardedMsg.text || "",
|
|
868
|
+
// });
|
|
869
|
+
// }
|
|
870
|
+
|
|
871
|
+
await SlackBotState.set(`task-${workspaceId}`, {
|
|
872
|
+
...(await SlackBotState.get(`task-${workspaceId}`)),
|
|
873
|
+
status: "thinking",
|
|
874
|
+
event,
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
// Add task to working list
|
|
878
|
+
await addWorkingTask(event);
|
|
879
|
+
|
|
880
|
+
slack.reactions
|
|
881
|
+
.remove({ name: "eyes", channel: event.channel, timestamp: event.ts })
|
|
882
|
+
.catch(() => {});
|
|
883
|
+
slack.reactions
|
|
884
|
+
.add({ name: "thinking_face", channel: event.channel, timestamp: event.ts })
|
|
885
|
+
.catch(() => {});
|
|
886
|
+
|
|
887
|
+
const CLAUDEMD = loadClaudeMd({
|
|
888
|
+
EVENT_CHANNEL: event.channel,
|
|
889
|
+
QUICK_RESPOND_MSG_TS: quickRespondMsg.ts!,
|
|
890
|
+
USERNAME: username,
|
|
891
|
+
NEARBY_MESSAGES_YAML: yaml.stringify(nearbyMessages),
|
|
892
|
+
EVENT_TEXT_JSON: JSON.stringify(await parseSlackMessageToMarkdown(event.text)),
|
|
893
|
+
USER_INTENT: resp.user_intent,
|
|
894
|
+
MY_RESPONSE_MESSAGE_JSON: JSON.stringify(myResponseMessage),
|
|
895
|
+
EVENT_THREAD_TS: event.thread_ts || event.ts,
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
// const taskUser = `bot-user-${workspaceId.replace(".", "-")}`;
|
|
899
|
+
// const taskUser = `bot-user-${workspaceId.replace(".", "-")}`;
|
|
900
|
+
await mkdir(botWorkingDir, { recursive: true });
|
|
901
|
+
// todo: create a linux user for task
|
|
902
|
+
|
|
903
|
+
// fill initial files for agent
|
|
904
|
+
|
|
905
|
+
await Bun.write(`${botWorkingDir}/CLAUDE.md`, CLAUDEMD);
|
|
906
|
+
|
|
907
|
+
// clone https://github.com/Comfy-Org/Comfy-PR/tree/sno-bot to ./repos/prbot (branch: sno-bot)
|
|
908
|
+
const prBotRepoDir = `${botWorkingDir}/codes/Comfy-Org/pr-bot/tree/main`;
|
|
909
|
+
await mkdir(prBotRepoDir, { recursive: true });
|
|
910
|
+
await Bun.$`git clone --branch main https://github.com/Comfy-Org/Comfy-PR ${prBotRepoDir}`.catch(
|
|
911
|
+
() => null,
|
|
912
|
+
);
|
|
913
|
+
|
|
914
|
+
// await Bun.write(`${botWorkingDir}/PROMPT.txt`, agentPrompt);
|
|
915
|
+
|
|
916
|
+
// Add Claude Skills to working dir (.claude/skills)
|
|
917
|
+
// Reference: https://docs.claude.ai/en/claude-code/skills
|
|
918
|
+
const skillsBase = `${botWorkingDir}/.claude/skills`;
|
|
919
|
+
await mkdir(skillsBase, { recursive: true });
|
|
920
|
+
const skills = loadSkills({
|
|
921
|
+
EVENT_CHANNEL: event.channel,
|
|
922
|
+
QUICK_RESPOND_MSG_TS: quickRespondMsg.ts!,
|
|
923
|
+
EVENT_THREAD_TS: event.thread_ts || event.ts,
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
for (const [dir, content] of Object.entries(skills)) {
|
|
927
|
+
const p = `${skillsBase}/${dir}`;
|
|
928
|
+
await mkdir(p, { recursive: true });
|
|
929
|
+
await Bun.write(`${p}/SKILL.md`, content);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Index file to make skills easy to discover alongside CLAUDE.md
|
|
933
|
+
await Bun.write(
|
|
934
|
+
`${botWorkingDir}/SKILLS.txt`,
|
|
935
|
+
`
|
|
936
|
+
Available Skills (.claude/skills):
|
|
937
|
+
- slack-messaging: Communicate in Slack threads using prbot slack commands.
|
|
938
|
+
- slack-file-sharing: Upload and download files, share deliverables with users.
|
|
939
|
+
- github-pr-bot: Delegate all code changes via prbot pr command.
|
|
940
|
+
- code-search: Search ComfyUI code using prbot code search.
|
|
941
|
+
- github-issue-search: Search issues and PRs using prbot github-issue search.
|
|
942
|
+
- notion-search: Discover and cite internal Notion pages using prbot notion search.
|
|
943
|
+
- registry-search: Search custom nodes using prbot registry search.
|
|
944
|
+
- repo-reading: Clone and inspect Comfy-Org repos read-only, or use prbot code search.
|
|
945
|
+
- web-research: Pull in external context and cite sources.
|
|
946
|
+
|
|
947
|
+
Open the corresponding SKILL.md under .claude/skills/<name>/ for details.
|
|
948
|
+
`,
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
await Bun.write(
|
|
952
|
+
`${botWorkingDir}/TODO.md`,
|
|
953
|
+
`
|
|
954
|
+
# Task TODOs
|
|
955
|
+
|
|
956
|
+
- Analyze the user's request and gather necessary information.
|
|
957
|
+
- Search relevant documents, codebases, and resources using prbot CLI:
|
|
958
|
+
- Code search: prbot code search --query="<search terms>" [--repo=<owner/repo>]
|
|
959
|
+
- Issue search: prbot github-issue search --query="<search terms>"
|
|
960
|
+
- Notion search: prbot notion search --query="<search terms>"
|
|
961
|
+
- Registry search: prbot registry search --query="<search terms>"
|
|
962
|
+
- Coordinate with prbot agents for unknown coding tasks:
|
|
963
|
+
- prbot pr --repo=<owner/repo> --prompt="<detailed coding task>"
|
|
964
|
+
- For each deliverable: save to ./deliverable-<name>.md then immediately upload to Slack.
|
|
965
|
+
- Compile findings and provide a comprehensive response to the user.
|
|
966
|
+
|
|
967
|
+
## GitHub Changes
|
|
968
|
+
- IMPORTANT: Remember to use the prbot CLI for unknown GitHub code changes:
|
|
969
|
+
prbot pr --repo=<owner/repo> [--branch=<branch>] --prompt="<detailed coding task>"
|
|
970
|
+
|
|
971
|
+
## Deliverables Convention
|
|
972
|
+
- ALWAYS save any document, guide, report, or artifact to: ./deliverable-<name>.md
|
|
973
|
+
- Then IMMEDIATELY post to Slack (smart-post: short → inline message, long → file upload):
|
|
974
|
+
prbot slack post --channel=<channel> --file=./deliverable-<name>.md --title="<title>" --comment="<summary>" --thread=<thread_ts>
|
|
975
|
+
- Examples:
|
|
976
|
+
- ./deliverable-research-report.md
|
|
977
|
+
- ./deliverable-analysis.md
|
|
978
|
+
- ./deliverable-summary.md
|
|
979
|
+
|
|
980
|
+
## Tool Error Recovery
|
|
981
|
+
When a prbot CLI command fails:
|
|
982
|
+
1. Record error to ./TOOLS_ERRORS.md (command, error, context)
|
|
983
|
+
2. Read the failing tool's source in ./codes/Comfy-Org/Comfy-PR/tree/sno-bot to diagnose
|
|
984
|
+
3. Spawn a fix via: prbot pr --repo=Comfy-Org/Comfy-PR --prompt="Fix <tool>: <error>. Root cause: <analysis>. Fix: <change>"
|
|
985
|
+
4. Workaround to complete the user's task while the fix PR is open
|
|
986
|
+
|
|
987
|
+
`,
|
|
988
|
+
);
|
|
989
|
+
await Bun.$`code ${botWorkingDir}`.catch(() => null); // open the working dir in vscode for debugging
|
|
990
|
+
|
|
991
|
+
const agentPrompt = `
|
|
992
|
+
the @${username} intented to ${resp.user_intent}
|
|
993
|
+
Please assist them with their request using all your resources available.
|
|
994
|
+
|
|
995
|
+
IMPORTANT WORKSPACE CONVENTIONS:
|
|
996
|
+
- Save ALL deliverables (documents, guides, reports, summaries, analysis, code snippets, etc.) to ./deliverable-<name>.md in the current workspace directory. For example: ./deliverable-draft-pr-guide.md, ./deliverable-research-report.md
|
|
997
|
+
- Log any tool errors or failures to ./TOOLS_ERRORS.md
|
|
998
|
+
- Keep deliverables self-contained and well-formatted so they can be shared directly with the user
|
|
999
|
+
`;
|
|
1000
|
+
|
|
1001
|
+
// Write PROMPT.txt so claude-yes can read the user's intent
|
|
1002
|
+
await Bun.write(`${botWorkingDir}/PROMPT.txt`, agentPrompt);
|
|
1003
|
+
|
|
1004
|
+
logger.info(`Spawning agent in ${botWorkingDir} with prompt: ${JSON.stringify(agentPrompt)}`);
|
|
1005
|
+
// todo: spawn in a worker user
|
|
1006
|
+
|
|
1007
|
+
// await Bun.$.cwd(botWorkingDir)`claude-yes -- solve-everything-in=TODO.md, PROMPT.txt, current bot args --working-dir=${botWorkingDir} --slack-channel=${event.channel} --slack-thread-ts=${quickRespondMsg.ts!}`
|
|
1008
|
+
|
|
1009
|
+
// Create dedicated log files for this task (before spawning)
|
|
1010
|
+
const taskLogDir = `${botWorkingDir}/.logs`;
|
|
1011
|
+
await mkdir(taskLogDir, { recursive: true });
|
|
1012
|
+
const stdoutLogPath = `${taskLogDir}/claude-yes-stdout.log`;
|
|
1013
|
+
const stderrLogPath = `${taskLogDir}/claude-yes-stderr.log`;
|
|
1014
|
+
const statusLogPath = `${taskLogDir}/STATUS.txt`;
|
|
1015
|
+
|
|
1016
|
+
// create a user for task
|
|
1017
|
+
const exitCodePromise = Promise.withResolvers<number | null>();
|
|
1018
|
+
const sh = (() => {
|
|
1019
|
+
// if (process.env.CLI === "amp") {
|
|
1020
|
+
// // use amp
|
|
1021
|
+
// }
|
|
1022
|
+
// const continueArgs: string[] = [];
|
|
1023
|
+
// if (botworkingdir/.claude-yes have content)
|
|
1024
|
+
// then continueArgs.push('--continue')
|
|
1025
|
+
// TODO: maybe use smarter way to detect if need continue
|
|
1026
|
+
// if (existsSync(`${botWorkingDir}/.claude-yes`)) {
|
|
1027
|
+
// // const stat = Bun.statSync(`${botWorkingDir}/.claude-yes`)
|
|
1028
|
+
// // if (stat.isDirectory && stat.size > 0) {
|
|
1029
|
+
// // }
|
|
1030
|
+
// continueArgs.push('--continue')
|
|
1031
|
+
// }
|
|
1032
|
+
// const cmd = `bunx claude-yes -i=1d -- ${Bun.$.escape(agentPrompt)}`;
|
|
1033
|
+
// const cli = cmd.split(" ")[0];
|
|
1034
|
+
const cli = "claude-yes"; // Use the globally installed claude-yes (via bun)
|
|
1035
|
+
// Pass prompt to read PROMPT.txt and TODO.md
|
|
1036
|
+
const args = [
|
|
1037
|
+
"--exit-on-idle=1m",
|
|
1038
|
+
"--",
|
|
1039
|
+
"Please read PROMPT.txt and TODO.md in the current directory and complete all tasks listed there.",
|
|
1040
|
+
];
|
|
1041
|
+
logger.info(
|
|
1042
|
+
`Spawning process: ${cli} ${args.join(" ")} in ${botWorkingDir} with env GH_TOKEN_COMFY_PR_BOT=[REDACTED]`,
|
|
1043
|
+
);
|
|
1044
|
+
const shell = spawn(cli, args, {
|
|
1045
|
+
cwd: botWorkingDir,
|
|
1046
|
+
env: {
|
|
1047
|
+
...process.env,
|
|
1048
|
+
GH_TOKEN: process.env.GH_TOKEN_COMFY_PR_BOT || DIE("missing GH_TOKEN_COMFY_PR_BOT env"),
|
|
1049
|
+
GITHUB_TOKEN: process.env.GH_TOKEN_COMFY_PR_BOT || DIE("missing GH_TOKEN_COMFY_PR_BOT env"),
|
|
1050
|
+
},
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
// check if p spawned successfully
|
|
1054
|
+
shell.on("error", (err) => {
|
|
1055
|
+
logger.error(`Failed to start ${cli} process for task ${workspaceId}:`, { err });
|
|
1056
|
+
});
|
|
1057
|
+
shell.on("exit", (code, signal) => {
|
|
1058
|
+
logger.info(`process for task ${workspaceId} exited with code ${code} and signal ${signal}`);
|
|
1059
|
+
exitCodePromise.resolve(code);
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
// Auto-answer the trust prompt (option 1 = "Yes, proceed")
|
|
1063
|
+
if (shell.stdin) {
|
|
1064
|
+
shell.stdin.write("1\n");
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Stream stderr to log file and logger (async operations moved outside)
|
|
1068
|
+
shell.stderr?.on("data", (data) => {
|
|
1069
|
+
const text = data.toString();
|
|
1070
|
+
appendFile(stderrLogPath, text).catch(() => {});
|
|
1071
|
+
logger.warn(`[${cli} stderr]:`, { data: text });
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
// Check if stdout/stderr are available
|
|
1075
|
+
if (!shell.stdout) {
|
|
1076
|
+
logger.error(`Process ${cli} has no stdout stream!`);
|
|
1077
|
+
}
|
|
1078
|
+
if (!shell.stderr) {
|
|
1079
|
+
logger.warn(`Process ${cli} has no stderr stream`);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
return shell;
|
|
1083
|
+
})();
|
|
1084
|
+
|
|
1085
|
+
// Write initial status
|
|
1086
|
+
await Bun.write(
|
|
1087
|
+
statusLogPath,
|
|
1088
|
+
`Started: ${new Date().toISOString()}\nPID: ${sh.pid}\nStatus: Running\nLog: ${stdoutLogPath}\n`,
|
|
1089
|
+
);
|
|
1090
|
+
|
|
1091
|
+
const isDebugMode = process.env.DEBUG === "true" || process.env.DEBUG === "1";
|
|
1092
|
+
|
|
1093
|
+
logger.info(`Spawned claude-yes process with PID ${sh.pid} for task ${workspaceId}`);
|
|
1094
|
+
if (isDebugMode) {
|
|
1095
|
+
logger.info(`📝 Real-time logs: tail -f ${stdoutLogPath}`);
|
|
1096
|
+
logger.info(`📊 Status file: cat ${statusLogPath}`);
|
|
1097
|
+
logger.info(`💡 Debug commands: prbot debug watch ${botWorkingDir}`);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// Start error collector to monitor workspace for errors
|
|
1101
|
+
const errorLogPath = `${taskLogDir}/COLLECTED_ERRORS.md`;
|
|
1102
|
+
const errorCollector = new ErrorCollector({
|
|
1103
|
+
workspaceDir: botWorkingDir,
|
|
1104
|
+
outputLogPath: errorLogPath,
|
|
1105
|
+
onError: isDebugMode
|
|
1106
|
+
? (errorPath, content) => {
|
|
1107
|
+
logger.warn(`⚠️ Error detected in workspace: ${errorPath}`);
|
|
1108
|
+
logger.warn(`Error content preview: ${content.substring(0, 500)}...`);
|
|
1109
|
+
}
|
|
1110
|
+
: undefined,
|
|
1111
|
+
checkInterval: 10000, // Check every 10 seconds
|
|
1112
|
+
});
|
|
1113
|
+
await errorCollector.start();
|
|
1114
|
+
if (isDebugMode) {
|
|
1115
|
+
logger.info(`🔍 Error collector started, errors will be logged to: ${errorLogPath}`);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
await sflow(
|
|
1119
|
+
[""], // Initial Prompt to start the agent, could be empty
|
|
1120
|
+
)
|
|
1121
|
+
.merge(
|
|
1122
|
+
// append messages from taskInputFlow
|
|
1123
|
+
sflow(taskInputFlow.readable)
|
|
1124
|
+
// send original message and then write '\n' after 1s delay to simulate user press Enter
|
|
1125
|
+
.map(async (awaitableText) => awaitableText),
|
|
1126
|
+
)
|
|
1127
|
+
.by(fromStdio(sh))
|
|
1128
|
+
// convert buffer to string and write to log file
|
|
1129
|
+
.map(async (buffer) => {
|
|
1130
|
+
if (buffer === undefined || buffer === null) {
|
|
1131
|
+
logger.warn(`Received undefined/null buffer from process ${workspaceId}`);
|
|
1132
|
+
return "";
|
|
1133
|
+
}
|
|
1134
|
+
const text = buffer.toString();
|
|
1135
|
+
// Write raw output to dedicated stdout log file
|
|
1136
|
+
await appendFile(stdoutLogPath, text).catch(() => {});
|
|
1137
|
+
return text;
|
|
1138
|
+
})
|
|
1139
|
+
|
|
1140
|
+
// pipe to /botWorkingDir/.logs/bot-<date>.log to claude input
|
|
1141
|
+
.forkTo(async (e) => {
|
|
1142
|
+
const logDate = new Date().toISOString().split("T")[0];
|
|
1143
|
+
await mkdir(path.resolve(`${botWorkingDir}/.logs`), { recursive: true });
|
|
1144
|
+
await e.forEach(
|
|
1145
|
+
async (chunk) => await appendFile(`${botWorkingDir}/.logs/bot-${logDate}.log`, chunk),
|
|
1146
|
+
);
|
|
1147
|
+
})
|
|
1148
|
+
// show loading icon when unknown output activity, and remove the loading icon after idle for 5s
|
|
1149
|
+
.forkTo(async (e) => {
|
|
1150
|
+
const idleWaiter = new IdleWaiter();
|
|
1151
|
+
let isThinking = false;
|
|
1152
|
+
return await e
|
|
1153
|
+
.forEach(async () => {
|
|
1154
|
+
idleWaiter.ping();
|
|
1155
|
+
if (!isThinking && quickRespondMsg.ts && quickRespondMsg.channel) {
|
|
1156
|
+
isThinking = true;
|
|
1157
|
+
const msgChannel = quickRespondMsg.channel;
|
|
1158
|
+
const msgTs = quickRespondMsg.ts;
|
|
1159
|
+
await slack.reactions
|
|
1160
|
+
.add({
|
|
1161
|
+
name: "loading",
|
|
1162
|
+
channel: msgChannel,
|
|
1163
|
+
timestamp: msgTs,
|
|
1164
|
+
})
|
|
1165
|
+
.catch(() => {});
|
|
1166
|
+
idleWaiter.wait(5e3).finally(async () => {
|
|
1167
|
+
await slack.reactions
|
|
1168
|
+
.remove({
|
|
1169
|
+
name: "loading",
|
|
1170
|
+
channel: msgChannel,
|
|
1171
|
+
timestamp: msgTs,
|
|
1172
|
+
})
|
|
1173
|
+
.catch(() => {});
|
|
1174
|
+
isThinking = false;
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
})
|
|
1178
|
+
.onFlush(async () => {
|
|
1179
|
+
// remove loading icon
|
|
1180
|
+
if (isThinking && quickRespondMsg.ts && quickRespondMsg.channel) {
|
|
1181
|
+
isThinking = false;
|
|
1182
|
+
await slack.reactions
|
|
1183
|
+
.remove({
|
|
1184
|
+
name: "loading",
|
|
1185
|
+
channel: quickRespondMsg.channel,
|
|
1186
|
+
timestamp: quickRespondMsg.ts,
|
|
1187
|
+
})
|
|
1188
|
+
.catch(() => {});
|
|
1189
|
+
}
|
|
1190
|
+
})
|
|
1191
|
+
.run();
|
|
1192
|
+
})
|
|
1193
|
+
|
|
1194
|
+
// Render terminal text to plain text and show live updates in slack
|
|
1195
|
+
.forkTo(async (e) => {
|
|
1196
|
+
const tr = new TerminalTextRender();
|
|
1197
|
+
let sent = "";
|
|
1198
|
+
let lastOutputs: string[] = []; // keep 3 last outputs to detect stability
|
|
1199
|
+
|
|
1200
|
+
// logger.info('Rendered chunk size:', rendered.length, 'lines: ', rendered.split(/\r|\n/).length);
|
|
1201
|
+
const id = setInterval(async () => {
|
|
1202
|
+
const renderedText = tr.render();
|
|
1203
|
+
// diff from last, and send stable lines
|
|
1204
|
+
const common = commonPrefix(renderedText, ...lastOutputs);
|
|
1205
|
+
const newStable = renderedText.slice(0, common.length);
|
|
1206
|
+
// logger.debug({ common, newStable, lastOutputs, renderedText });
|
|
1207
|
+
|
|
1208
|
+
if (newStable !== sent) {
|
|
1209
|
+
const news = newStable.slice(sent.length);
|
|
1210
|
+
sent = newStable; // agent outputs have new lines to send
|
|
1211
|
+
if (news) logger.debug(JSON.stringify({ news }));
|
|
1212
|
+
logger.info(
|
|
1213
|
+
`New stable output detected, length: ${newStable.length}, news length: ${news.length}`,
|
|
1214
|
+
);
|
|
1215
|
+
|
|
1216
|
+
const rawTerminalOutput = tr.render().split("\n").slice(-80).join("\n");
|
|
1217
|
+
const my_internal_thoughts = cleanTerminalOutput(rawTerminalOutput);
|
|
1218
|
+
// const my_internal_thoughts = tr.tail(80);
|
|
1219
|
+
logger.debug(
|
|
1220
|
+
"Raw terminal output (before cleaning): " +
|
|
1221
|
+
yaml.stringify({ preview: rawTerminalOutput.slice(0, 200) }),
|
|
1222
|
+
);
|
|
1223
|
+
logger.info(
|
|
1224
|
+
"Cleaned output preview: " +
|
|
1225
|
+
yaml.stringify({
|
|
1226
|
+
preview: my_internal_thoughts.slice(0, 200),
|
|
1227
|
+
news_preview: news.slice(0, 200),
|
|
1228
|
+
}),
|
|
1229
|
+
);
|
|
1230
|
+
|
|
1231
|
+
// send update to slack
|
|
1232
|
+
const updateText = sent || "_(no output yet)_";
|
|
1233
|
+
const contexts = {
|
|
1234
|
+
my_internal_thoughts,
|
|
1235
|
+
news,
|
|
1236
|
+
user_original_intent: resp.user_intent,
|
|
1237
|
+
my_response_md_original: quickRespondMsg.text || "",
|
|
1238
|
+
};
|
|
1239
|
+
const updateResponseResp = (await zChatCompletion({
|
|
1240
|
+
my_response_md_updated: z.string(),
|
|
1241
|
+
})`
|
|
1242
|
+
TASK: Update my my_response_md_original based on agent's my_internal_thoughts findings, and give me my_response_md_updated to post in slack.
|
|
1243
|
+
|
|
1244
|
+
RULES:
|
|
1245
|
+
- Do not remove unknown parts from my_response_md_original that are not mentioned in my_internal_thoughts.
|
|
1246
|
+
- Preserve markdown formatting in my_response_md_original.
|
|
1247
|
+
- If my_internal_thoughts contains new information, append it to the relevant sections in my_response_md_original.
|
|
1248
|
+
- If my_internal_thoughts indicates completion of a task, add a "Tasks" section at the end of my_response_md_original with - [x] mark.
|
|
1249
|
+
- Ensure my_response_md_updated is clear and concise.
|
|
1250
|
+
- Use **bold** to highlight new sections or important updates. Remove previously highlighted sections if they're no longer relevant.
|
|
1251
|
+
- If all information from my_internal_thoughts is already contained in my_response_md_original, return: {my_response_md_updated: "__NOTHING_CHANGED__"}
|
|
1252
|
+
|
|
1253
|
+
CRITICAL FILTERING RULES (Non-negotiable):
|
|
1254
|
+
- KEEP ONLY: User-facing progress, task completion status, findings relevant to user's intent, next steps
|
|
1255
|
+
- REMOVE: File paths, system info, debug output, error stack traces, internal process details, development notes
|
|
1256
|
+
- EXAMPLES OF WHAT TO REMOVE:
|
|
1257
|
+
- "/bot/slack/channel-id/timestamp" (internal paths)
|
|
1258
|
+
- "undefined/null received in chunk" (internal errors)
|
|
1259
|
+
- "DEBUG: ..." (debug output)
|
|
1260
|
+
- "✓ Created /tmp/cache/..." (internal file operations)
|
|
1261
|
+
- "[2026-02-20T15:10:40.123Z]" (timestamps)
|
|
1262
|
+
|
|
1263
|
+
TONE & LENGTH:
|
|
1264
|
+
- KEEP message very short and informative, use url links to reference documents/repos instead of pasting large contents
|
|
1265
|
+
- Response should be up to 16 lines maximum (agent posts long reports as .md files)
|
|
1266
|
+
- Focus ONLY on end-user's question or intent's helpful contents
|
|
1267
|
+
- Describe current progress in up to 7 words (less is better)
|
|
1268
|
+
- Avoid jargon; write for non-technical users when possible
|
|
1269
|
+
|
|
1270
|
+
FORMAT REQUIREMENTS:
|
|
1271
|
+
- Output in standard markdown format (GitHub flavored)
|
|
1272
|
+
- YOU CAN ONLY change/remove/add up to 1 line per update!
|
|
1273
|
+
- LENGTH LIMIT: Must be within 4000 characters (system will truncate if exceeding)
|
|
1274
|
+
- MOST IMPORTANT: Keep my_response_md_original's context and formatting mostly unchanged, only update necessary lines
|
|
1275
|
+
|
|
1276
|
+
DO NOT:
|
|
1277
|
+
- Ask the user questions
|
|
1278
|
+
- Include error details (they're logged separately for developers)
|
|
1279
|
+
- Show code blocks or technical configs
|
|
1280
|
+
- Show internal process logs or environment variables
|
|
1281
|
+
- Show any paths starting with "/" or "./"
|
|
1282
|
+
|
|
1283
|
+
- Here's Contexts in YAML for your respondse:
|
|
1284
|
+
|
|
1285
|
+
<task-context-yaml>
|
|
1286
|
+
${yaml.stringify(contexts)}
|
|
1287
|
+
</task-context-yaml>
|
|
1288
|
+
|
|
1289
|
+
`) as { my_response_md_updated: string };
|
|
1290
|
+
|
|
1291
|
+
// Log raw my_response_md_updated to JSONL file for debugging
|
|
1292
|
+
const responseLogEntry = {
|
|
1293
|
+
timestamp: new Date().toISOString(),
|
|
1294
|
+
workspaceId,
|
|
1295
|
+
stage: "raw_from_claude",
|
|
1296
|
+
my_response_md_updated_raw: updateResponseResp.my_response_md_updated,
|
|
1297
|
+
my_internal_thoughts_preview: my_internal_thoughts.slice(0, 500),
|
|
1298
|
+
my_response_md_original: quickRespondMsg.text || "",
|
|
1299
|
+
};
|
|
1300
|
+
await appendFile(
|
|
1301
|
+
".logs/my_response_md_updated.jsonl",
|
|
1302
|
+
JSON.stringify(responseLogEntry) + "\n",
|
|
1303
|
+
).catch(() => {});
|
|
1304
|
+
|
|
1305
|
+
const updated_response_full = await mdFmt(
|
|
1306
|
+
updateResponseResp.my_response_md_updated
|
|
1307
|
+
.trim()
|
|
1308
|
+
.replace(/^__NOTHING_CHANGED__$/m, quickRespondMsg.text || ""),
|
|
1309
|
+
);
|
|
1310
|
+
|
|
1311
|
+
// truncate to 4000 chars, from the middle, replace to '...TRUNCATED...'
|
|
1312
|
+
const my_response_md_updated =
|
|
1313
|
+
updated_response_full.length > 4000
|
|
1314
|
+
? updated_response_full.slice(0, 2000) +
|
|
1315
|
+
"\n\n...TRUNCATED...\n\n" +
|
|
1316
|
+
updated_response_full.slice(-2000)
|
|
1317
|
+
: updated_response_full;
|
|
1318
|
+
|
|
1319
|
+
// Log final processed my_response_md_updated
|
|
1320
|
+
const finalLogEntry = {
|
|
1321
|
+
timestamp: new Date().toISOString(),
|
|
1322
|
+
workspaceId,
|
|
1323
|
+
stage: "final_processed",
|
|
1324
|
+
my_response_md_updated_final: my_response_md_updated,
|
|
1325
|
+
was_truncated: updated_response_full.length > 4000,
|
|
1326
|
+
original_length: updated_response_full.length,
|
|
1327
|
+
};
|
|
1328
|
+
await appendFile(
|
|
1329
|
+
".logs/my_response_md_updated.jsonl",
|
|
1330
|
+
JSON.stringify(finalLogEntry) + "\n",
|
|
1331
|
+
).catch(() => {});
|
|
1332
|
+
|
|
1333
|
+
if (quickRespondMsg.ts && quickRespondMsg.channel) {
|
|
1334
|
+
await safeSlackUpdateMessage(slack, {
|
|
1335
|
+
channel: quickRespondMsg.channel,
|
|
1336
|
+
ts: quickRespondMsg.ts,
|
|
1337
|
+
text: my_response_md_updated, // Fallback text for notifications
|
|
1338
|
+
blocks: [
|
|
1339
|
+
{
|
|
1340
|
+
type: "markdown",
|
|
1341
|
+
text: my_response_md_updated,
|
|
1342
|
+
},
|
|
1343
|
+
],
|
|
1344
|
+
});
|
|
1345
|
+
logger.debug("Updated quick respond message in slack:", {
|
|
1346
|
+
url: `https://${event.team}.slack.com/archives/${quickRespondMsg.channel}/p${quickRespondMsg.ts.replace(".", "")}`,
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
// update quickRespondMsg content
|
|
1350
|
+
quickRespondMsg.text = my_response_md_updated;
|
|
1351
|
+
await SlackBotState.set(`task-quick-respond-msg-${eventId}`, {
|
|
1352
|
+
ts: quickRespondMsg.ts,
|
|
1353
|
+
text: quickRespondMsg.text,
|
|
1354
|
+
channel: event.channel,
|
|
1355
|
+
url: `https://${SLACK_ORG_DOMAIN_NAME}.slack.com/archives/${event.channel}/p${quickRespondMsg.ts.replace(".", "")}`,
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
lastOutputs.push(renderedText);
|
|
1361
|
+
if (lastOutputs.length > 3) {
|
|
1362
|
+
lastOutputs.shift();
|
|
1363
|
+
}
|
|
1364
|
+
}, 1e3);
|
|
1365
|
+
|
|
1366
|
+
await e
|
|
1367
|
+
.forEach(async (chunk) => {
|
|
1368
|
+
if (chunk === undefined || chunk === null) {
|
|
1369
|
+
logger.warn(`Terminal render received undefined/null chunk for task ${workspaceId}`);
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
if (chunk === "") {
|
|
1373
|
+
// Empty string is valid, just skip rendering
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
try {
|
|
1377
|
+
const rendered = tr.write(chunk);
|
|
1378
|
+
} catch (err) {
|
|
1379
|
+
logger.error(`Error writing chunk to terminal render for task ${workspaceId}:`, {
|
|
1380
|
+
err,
|
|
1381
|
+
chunkType: typeof chunk,
|
|
1382
|
+
chunkLength: chunk?.length,
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
})
|
|
1386
|
+
.onFlush(() => clearInterval(id))
|
|
1387
|
+
.run();
|
|
1388
|
+
})
|
|
1389
|
+
|
|
1390
|
+
// show contents in console if needed for debugging
|
|
1391
|
+
// .forkTo((e) => e.pipeTo(fromWritable(process.stdout)))
|
|
1392
|
+
// .forkTo((e) => e.pipeTo(fromWritable(process.stdout)))
|
|
1393
|
+
.run();
|
|
1394
|
+
|
|
1395
|
+
TaskInputFlows.delete(workspaceId);
|
|
1396
|
+
|
|
1397
|
+
// Stop error collector
|
|
1398
|
+
errorCollector.stop();
|
|
1399
|
+
if (isDebugMode) {
|
|
1400
|
+
logger.info(`🔍 Error collector stopped for task ${workspaceId}`);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// check exit code, checkmark if claude-yes exited 0, cross if not
|
|
1404
|
+
|
|
1405
|
+
const exitCode = await exitCodePromise.promise;
|
|
1406
|
+
|
|
1407
|
+
// Update final status
|
|
1408
|
+
const finalStatus = exitCode === 0 ? "Completed Successfully" : `Failed (exit code ${exitCode})`;
|
|
1409
|
+
await Bun.write(
|
|
1410
|
+
statusLogPath,
|
|
1411
|
+
`Started: ${new Date().toISOString()}\nPID: ${sh.pid}\nStatus: ${finalStatus}\nExit Code: ${exitCode}\nEnded: ${new Date().toISOString()}\nLogs: ${stdoutLogPath}\nErrors: ${errorLogPath}\n`,
|
|
1412
|
+
).catch(() => {});
|
|
1413
|
+
|
|
1414
|
+
if (exitCode !== 0) {
|
|
1415
|
+
logger.error(`claude-yes process for task ${workspaceId} exited with code ${exitCode}`);
|
|
1416
|
+
// those error tasks will got retry after a restart
|
|
1417
|
+
// update my slack message reactions shows a cross mark and update it appending a error happened and say will retry later
|
|
1418
|
+
await slack.reactions
|
|
1419
|
+
.remove({ name: "thinking_face", channel: event.channel, timestamp: event.ts })
|
|
1420
|
+
.catch(() => {});
|
|
1421
|
+
if (quickRespondMsg.ts && quickRespondMsg.channel) {
|
|
1422
|
+
await slack.reactions
|
|
1423
|
+
.add({ name: "x", channel: quickRespondMsg.channel, timestamp: quickRespondMsg.ts })
|
|
1424
|
+
.catch(() => {});
|
|
1425
|
+
const errorText = await mdFmt(
|
|
1426
|
+
(quickRespondMsg.text || "") +
|
|
1427
|
+
`\n\n:warning: An error occurred while processing this request <@snomiao>, I will try it again later`,
|
|
1428
|
+
);
|
|
1429
|
+
await safeSlackUpdateMessage(slack, {
|
|
1430
|
+
channel: event.channel,
|
|
1431
|
+
ts: quickRespondMsg.ts,
|
|
1432
|
+
text: errorText, // Fallback text for notifications
|
|
1433
|
+
blocks: [
|
|
1434
|
+
{
|
|
1435
|
+
type: "markdown",
|
|
1436
|
+
text: errorText,
|
|
1437
|
+
},
|
|
1438
|
+
],
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// claude exited as no more inputs/outputs for a while, update the status message
|
|
1444
|
+
await slack.reactions
|
|
1445
|
+
.remove({ name: "thinking_face", channel: event.channel, timestamp: event.ts })
|
|
1446
|
+
.catch(() => {});
|
|
1447
|
+
await slack.reactions
|
|
1448
|
+
.add({ name: "white_check_mark", channel: event.channel, timestamp: event.ts })
|
|
1449
|
+
.catch(() => {});
|
|
1450
|
+
const taskState = await SlackBotState.get(`task-${workspaceId}`);
|
|
1451
|
+
const endTime = Date.now();
|
|
1452
|
+
const responseDuration = taskState?.startTime ? endTime - taskState.startTime : undefined;
|
|
1453
|
+
|
|
1454
|
+
await SlackBotState.set(`task-${workspaceId}`, {
|
|
1455
|
+
...taskState,
|
|
1456
|
+
status: "done",
|
|
1457
|
+
endTime,
|
|
1458
|
+
responseDuration,
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
// Remove task from working list
|
|
1462
|
+
await removeWorkingTask(event);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
function sleep(ms: number) {
|
|
1466
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
async function getSlackMessageFromUrl(url: string) {
|
|
1470
|
+
const { ts, channel } = slackMessageUrlParse(url);
|
|
1471
|
+
const page = await slack.conversations.history({
|
|
1472
|
+
channel,
|
|
1473
|
+
limit: 1,
|
|
1474
|
+
inclusive: true,
|
|
1475
|
+
latest: ts,
|
|
1476
|
+
});
|
|
1477
|
+
return page.messages?.[0] || DIE("not found");
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
function commonPrefix(...args: string[]): string {
|
|
1481
|
+
if (args.length === 0) return "";
|
|
1482
|
+
let prefix = args[0];
|
|
1483
|
+
for (let i = 1; i < args.length; i++) {
|
|
1484
|
+
let j = 0;
|
|
1485
|
+
while (j < prefix.length && j < args[i].length && prefix[j] === args[i][j]) {
|
|
1486
|
+
j++;
|
|
1487
|
+
}
|
|
1488
|
+
prefix = prefix.slice(0, j);
|
|
1489
|
+
if (prefix === "") break;
|
|
1490
|
+
}
|
|
1491
|
+
return prefix;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
/**
|
|
1495
|
+
* Clean terminal output by removing ANSI codes, debug info, and system paths
|
|
1496
|
+
* This ensures Claude only sees user-meaningful progress information
|
|
1497
|
+
*/
|
|
1498
|
+
function cleanTerminalOutput(text: string): string {
|
|
1499
|
+
// Remove ANSI color codes and escape sequences
|
|
1500
|
+
text = text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
1501
|
+
text = text.replace(/\x1b\[[^m]*m/g, "");
|
|
1502
|
+
text = text.replace(/\u0007/g, ""); // Bell character
|
|
1503
|
+
text = text.replace(/\r/g, ""); // Carriage returns
|
|
1504
|
+
|
|
1505
|
+
// Remove box drawing characters (Claude Code banner)
|
|
1506
|
+
text = text.replace(/[▐▛▜▘▝█▌▙▟▞▚░▒▓│┃├┤┬┴┼─═║╔╗╚╝╠╣╦╩╬]/g, "");
|
|
1507
|
+
|
|
1508
|
+
// Filter lines to remove debug noise
|
|
1509
|
+
const lines = text.split("\n").filter((line) => {
|
|
1510
|
+
const trimmed = line.trim();
|
|
1511
|
+
|
|
1512
|
+
// Skip empty or whitespace-only lines
|
|
1513
|
+
if (!trimmed) return true;
|
|
1514
|
+
|
|
1515
|
+
// Skip timestamp-prefixed log lines (multiple formats)
|
|
1516
|
+
// Format 1: [2026-02-20T15:10:40.123Z]
|
|
1517
|
+
if (/^\[[\d\-T:.Z]+\]/.test(trimmed)) return false;
|
|
1518
|
+
// Format 2: 2026-02-20 15:42:09 [info]:
|
|
1519
|
+
if (/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s+\[/.test(trimmed)) return false;
|
|
1520
|
+
|
|
1521
|
+
// Skip warning lines
|
|
1522
|
+
if (/^⚠|^Warning:|^WARN:|^\[warn\]/i.test(trimmed)) return false;
|
|
1523
|
+
|
|
1524
|
+
// Skip debug/verbose/trace/info prefixed lines
|
|
1525
|
+
if (/^(DEBUG|VERBOSE|TRACE|INFO):/i.test(trimmed)) return false;
|
|
1526
|
+
if (/\[(debug|verbose|trace|info)\]:/i.test(trimmed)) return false;
|
|
1527
|
+
|
|
1528
|
+
// Skip claude-yes specific output
|
|
1529
|
+
if (/\[claude-yes\]|claude-yes|Spawned claude|PID \d+/i.test(trimmed)) return false;
|
|
1530
|
+
if (/Claude Code v\d|Opus \d|Claude Max/i.test(trimmed)) return false;
|
|
1531
|
+
|
|
1532
|
+
// Skip lines containing system paths anywhere
|
|
1533
|
+
if (/\/bot\/slack\/|\/codes\/|\.logs\/|\/repos\/|\/tmp\//i.test(trimmed)) return false;
|
|
1534
|
+
|
|
1535
|
+
// Skip undefined/null error indicators
|
|
1536
|
+
if (/received undefined\/null|undefined\/null/i.test(trimmed)) return false;
|
|
1537
|
+
|
|
1538
|
+
// Skip deprecation warnings
|
|
1539
|
+
if (/deprecated|--exit-on-idle|-e are deprecated/i.test(trimmed)) return false;
|
|
1540
|
+
|
|
1541
|
+
// Skip pure terminal control output or lines that are mostly special chars
|
|
1542
|
+
if (/^(\s*|cursor\s+|bell|bel|\x07)$/i.test(trimmed)) return false;
|
|
1543
|
+
|
|
1544
|
+
// Skip lines that are mostly whitespace or contain only special characters
|
|
1545
|
+
if (/^[\s\u2000-\u206F\u2500-\u257F]*$/.test(trimmed)) return false;
|
|
1546
|
+
|
|
1547
|
+
return true;
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
return lines.join("\n").trim();
|
|
1551
|
+
}
|
|
1552
|
+
function sanitized(name: string) {
|
|
1553
|
+
return name.replace(/[^a-zA-Z0-9-_]/g, "_").slice(0, 50);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
export async function spawnBotOnSlackMessageUrl(url: string) {
|
|
1557
|
+
const { team, channel, ts } = await slackMessageUrlParse(url);
|
|
1558
|
+
const event = await slack.conversations
|
|
1559
|
+
.replies({
|
|
1560
|
+
channel: channel,
|
|
1561
|
+
ts: ts,
|
|
1562
|
+
limit: 1,
|
|
1563
|
+
})
|
|
1564
|
+
.then((res) => res.messages?.[0] || DIE("failed to fetch message from slack"));
|
|
1565
|
+
logger.info("Processing missed message " + JSON.stringify({ url, event }));
|
|
1566
|
+
// Parse the event to ensure it matches the expected type
|
|
1567
|
+
const mentionEvent = zAppMentionEvent.parse({
|
|
1568
|
+
...event,
|
|
1569
|
+
type: "app_mention",
|
|
1570
|
+
user: event.user || "",
|
|
1571
|
+
channel: channel,
|
|
1572
|
+
event_ts: event.ts || ts,
|
|
1573
|
+
});
|
|
1574
|
+
await spawnBotOnSlackMessageEvent(mentionEvent);
|
|
1575
|
+
}
|