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.
Files changed (100) hide show
  1. package/README.md +258 -157
  2. package/bot/IdleWaiter.ts +31 -0
  3. package/bot/RestartManager.spec.ts +163 -0
  4. package/bot/RestartManager.ts +190 -0
  5. package/bot/WorkingTasksManager.spec.ts +154 -0
  6. package/bot/cli.ts +1171 -0
  7. package/bot/codesearch-ai-wip.ts +351 -0
  8. package/bot/error-collector.ts +133 -0
  9. package/bot/index.ts +14 -0
  10. package/bot/restart-example.ts +99 -0
  11. package/bot/slack-bolt.ts +688 -0
  12. package/bot/slack-bot.ts +1575 -0
  13. package/bot/state.ts +19 -0
  14. package/bot/templateLoader.test.ts +84 -0
  15. package/bot/templateLoader.ts +92 -0
  16. package/next.config.ts +26 -0
  17. package/package.json +161 -41
  18. package/post-summary.ts +44 -0
  19. package/src/Authors.ts +2 -2
  20. package/src/CMNodes.ts +3 -3
  21. package/src/CNRepos.ts +30 -7
  22. package/src/CRNodes.ts +11 -9
  23. package/src/CRPulls.ts +3 -0
  24. package/src/EmailTasks.ts +27 -9
  25. package/src/FORK_OWNER.ts +3 -1
  26. package/src/FollowRules.ts +3 -3
  27. package/src/GithubIssueComments.ts +1 -1
  28. package/src/Totals.ts +9 -8
  29. package/src/WorkerInstances.ts +31 -8
  30. package/src/addCommentAction.ts +19 -11
  31. package/src/analyzePullsStatus.ts +39 -17
  32. package/src/analyzeTotals.ts +19 -11
  33. package/src/bypassRepos.ts +6 -5
  34. package/src/checkPRsFailures.ts +13 -8
  35. package/src/cli.test.ts +2 -0
  36. package/src/cli.ts +1 -1
  37. package/src/constants.ts +2 -0
  38. package/src/createComfyRegistryPRsFromCandidates.ts +20 -12
  39. package/src/createComfyRegistryPullRequests.ts +35 -10
  40. package/src/createGithubForkForRepo.ts +36 -11
  41. package/src/createGithubPullRequest.ts +83 -33
  42. package/src/createIssueComment.ts +4 -4
  43. package/src/fetchComfyRegistryNodes.ts +34 -4
  44. package/src/fetchCurrentGeoInfo.ts +3 -1
  45. package/src/fetchRelatedPullWithComments.ts +1 -1
  46. package/src/fetchRepoDescriptionMap.ts +1 -4
  47. package/src/followRuleSchema.ts +10 -4
  48. package/src/getBranchWorkingDir.ts +3 -7
  49. package/src/getRepoWorkingDir.ts +2 -3
  50. package/src/gh.spec.ts +571 -0
  51. package/src/ghData.ts +13 -0
  52. package/src/ghPageFlow.ts +33 -0
  53. package/src/ghSubscriber.ts +76 -0
  54. package/src/ghUser.ts +28 -4
  55. package/src/index.ts +13 -4
  56. package/src/initializeFollowRules.ts +11 -4
  57. package/src/logger.spec.ts +95 -0
  58. package/src/logger.ts +46 -0
  59. package/src/makeGpl3LicenseBranch.ts +66 -0
  60. package/src/makePublishBranch.ts +21 -15
  61. package/src/makeTomlBranch.ts +48 -14
  62. package/src/makeUpdateTomlLicenseBranch.ts +40 -44
  63. package/src/matchRelatedPulls.ts +9 -4
  64. package/src/normalizeGithubUrl.spec.ts +131 -0
  65. package/src/normalizeGithubUrl.ts +64 -0
  66. package/src/parseIssueUrl.spec.ts +95 -0
  67. package/src/parseIssueUrl.ts +17 -3
  68. package/src/parseOwnerRepo.spec.ts +147 -0
  69. package/src/parseOwnerRepo.ts +8 -5
  70. package/src/parsePullsState.ts +2 -2
  71. package/src/parseTitleBodyOfMarkdown.ts +1 -1
  72. package/src/pickRepoInfo.ts +37 -0
  73. package/src/postSlackMessage.ts +5 -1
  74. package/src/preload.ts +30 -27
  75. package/src/readTemplateTitle.ts +1 -3
  76. package/src/sendEmailAction.ts +14 -10
  77. package/src/sendGmail.ts +3 -3
  78. package/src/updateAuthorsForGithub.ts +53 -35
  79. package/src/updateAuthorsFromCNRepo.ts +12 -5
  80. package/src/updateCMNodesDuplicationWarnings.ts +14 -9
  81. package/src/updateCMRepos.ts +1 -1
  82. package/src/updateCNRepos.ts +14 -14
  83. package/src/updateCNReposCRPullsComments.ts +7 -5
  84. package/src/updateCNReposInfo.ts +49 -40
  85. package/src/updateCNReposPRCandidate.ts +3 -3
  86. package/src/updateCNReposPulls.ts +9 -5
  87. package/src/updateCNReposPullsDashboard.ts +11 -9
  88. package/src/updateCNReposRelatedPulls.ts +3 -3
  89. package/src/updateCRNodes.ts +24 -5
  90. package/src/updateCRRepos.ts +1 -1
  91. package/src/updateComfyTotals.ts +7 -5
  92. package/src/updateFollowRuleSet.ts +17 -9
  93. package/src/updateOutdatedPullsTemplates.ts +61 -48
  94. package/src/updateSlackMessages.ts +8 -4
  95. package/tailwind.config.ts +7 -1
  96. package/next-env.d.ts +0 -5
  97. package/src/GIT_USEREMAIL.ts +0 -5
  98. package/src/GIT_USERNAME.ts +0 -9
  99. package/src/clone_modify_push_Branches.ts +0 -21
  100. package/src/tomlFillDescription.ts +0 -17
@@ -0,0 +1,688 @@
1
+ #!/usr/bin/env bun --watch
2
+
3
+ /**
4
+ * ComfyPR Bot using @slack/bolt
5
+ *
6
+ * A cleaner implementation using the official Slack Bolt framework
7
+ * instead of raw SocketModeClient
8
+ */
9
+ import { App, LogLevel } from "@slack/bolt";
10
+ import { slack, slackCached } from "@/lib";
11
+ import winston from "winston";
12
+ import { parseSlackMessageToMarkdown } from "@/lib/slack/parseSlackMessageToMarkdown";
13
+ import sflow from "sflow";
14
+ import { streamText, tool, type ModelMessage, type ToolSet } from "ai";
15
+ import { openai } from "@ai-sdk/openai";
16
+ import { tsmatch } from "@/packages/mongodb-pipeline-ts/Task";
17
+ // tap and type imports removed (unused)
18
+ import { DIE } from "@snomiao/die";
19
+ import KeyvNedbStore from "keyv-nedb-store";
20
+ import { Keyv } from "keyv";
21
+ import { slackMessageUrlParse } from "@/app/tasks/gh-design/slackMessageUrlParse";
22
+ import prettier from "prettier";
23
+ import deferClose from "defer-close";
24
+ import { throttle } from "lodash-es";
25
+ import { compareBy } from "comparing";
26
+ import { z } from "zod";
27
+ import { yaml } from "@/src/utils/yaml";
28
+ import { formatMessage, formatMessages } from "@/lib/ai/format-message";
29
+
30
+ const SlackReplies = new Keyv<{
31
+ url: string;
32
+ text: string;
33
+ }>(new KeyvNedbStore("./.data/slack-replies.jsonl"));
34
+ const SlackRepliesSentBlocks = new Keyv<{
35
+ ui: UI;
36
+ }>(new KeyvNedbStore("./.data/slack-replies-sent-blocks.jsonl"));
37
+
38
+ const SlackUserPreferences = new Keyv<{
39
+ user_id: string; // userid
40
+ preferences: string; // md content
41
+ }>(new KeyvNedbStore("./.data/slack-user-preferences.jsonl"));
42
+ const system = `
43
+ You are @ComfyPR-Bot, an AI assistant that helps users with ComfyUI development tasks. Be concise and helpful.
44
+ ComfyPR Bot special in ComfyUI related tasks, including coding, GitHub PR/issue management, and documentation.
45
+
46
+ Do everything possible to help users achieve their goals efficiently, you should plan/research/compare/choose-the-best instead of ASK when multiple options are available.
47
+ IMPORTANT: You DONT need more information from users, just provide the best possible answer based on your knowledge and tools.
48
+ YOU ARE SMARTEST AI WHO NEVER SAY CAN'T DIRECTLY DO ANYTHING, YOU HAVE FULL ACCESS TO TOOLS THAT CAN DO ANYTHING that is enough to answer user's question in a direct result.
49
+
50
+ IMPORTANT: You must provide URLs for unknown references, documentation, or resources you mention.
51
+
52
+ OUTPUT is LIMITED in slack messages, so always be CONCISE and to the POINT. List your findings, steps, or recommendations clearly and briefly.
53
+ `;
54
+ const systemTools: ToolSet = {
55
+ pr_task_create: tool({
56
+ description: "Create a GitHub Pull Request in ComfyUI repositories by sub-agents",
57
+ inputSchema: z.object({
58
+ repo: z.string().describe("Repository name (e.g., ComfyUI/ComfyUI)"),
59
+ head: z.string().describe("Head Branch name for the Pull Request"),
60
+ base: z.string().describe("Base Branch name for the Pull Request"),
61
+ pr_task_prompt: z.string().optional().describe("Task description for the PR"),
62
+ }),
63
+ inputExamples: [
64
+ {
65
+ input: {
66
+ repo: "Comfy-Org/ComfyUI",
67
+ head: "feature/new-node",
68
+ base: "main",
69
+ pr_task_prompt: "Add a new node for image filtering",
70
+ },
71
+ },
72
+ ],
73
+ execute: async (args: {
74
+ head: string;
75
+ base: string;
76
+ repo: string;
77
+ pr_task_prompt?: string;
78
+ }) => {
79
+ // run PR creation agent
80
+ return `Created PR in ${args.repo} from ${args.head} to ${args.base} with task: ${
81
+ args.pr_task_prompt || "No description provided"
82
+ }`;
83
+ },
84
+ }),
85
+ user_preferences_read: tool({
86
+ description: "Read the Slack user profile information",
87
+ inputSchema: z.object({
88
+ slack_user_id: z.string().describe("Slack User ID to read profile for"),
89
+ }),
90
+ execute: async (args: { slack_user_id: string }) => {
91
+ const pref = await SlackUserPreferences.get(args.slack_user_id);
92
+ return pref ? pref.preferences : `No preferences set for user ID ${args.slack_user_id}`;
93
+ },
94
+ }),
95
+ user_preferences_write: tool({
96
+ description: "Update the Slack user profile information",
97
+ inputSchema: z.object({
98
+ slack_user_id: z.string().describe("Slack User ID to update profile for"),
99
+ preferences: z.string().describe("Markdown content of user preferences"),
100
+ }),
101
+ execute: async (args: { slack_user_id: string; preferences: string }) => {
102
+ await SlackUserPreferences.set(args.slack_user_id, {
103
+ user_id: args.slack_user_id,
104
+ preferences: args.preferences,
105
+ });
106
+ return `Updated preferences for user ID ${args.slack_user_id}`;
107
+ },
108
+ }),
109
+ // search: tool
110
+ comfy_customnodes_code_search: tool({
111
+ description: "Search code across ComfyUI and Custom Nodes repositories",
112
+ inputSchema: z.object({
113
+ query: z.string().describe("Search query string"),
114
+ }),
115
+ execute: async ({ query }) => {
116
+ return await Bun.$`prbot code search --query "${query}" --repo Comfy-Org/ComfyUI`.text();
117
+ },
118
+ }),
119
+ //
120
+ github_issues_search: tool({
121
+ description: "Search GitHub issues and pull requests across Comfy-Org repositories",
122
+ inputSchema: z.object({
123
+ query: z.string().describe("Search query string"),
124
+ limit: z.number().optional().describe("Maximum number of results to return (default: 10)"),
125
+ }),
126
+ execute: async ({ query, limit }) => {
127
+ const lim = limit || 10;
128
+ return await Bun.$`prbot gh issue-search --query "${query}" --limit ${lim}`.text();
129
+ },
130
+ }),
131
+ // notion
132
+ search_notion_docs: tool({
133
+ description:
134
+ "Search Notion documentation for ComfyOrg, including internal docs, project, tasks, and more",
135
+ inputSchema: z.object({
136
+ query: z.string().describe("Search query string"),
137
+ }),
138
+ execute: async ({ query }) => {
139
+ return await Bun.$`prbot notion search --query "${query}" --limit 5`.text();
140
+ },
141
+ }),
142
+ wait_a_while: tool({
143
+ description:
144
+ "Wait for a short period for external processes to complete, useful for wait+retry or delays",
145
+ inputSchema: z.object({
146
+ ms: z.string().describe("Search query string"),
147
+ }),
148
+ execute: async ({ ms }) => {
149
+ const msNum = parseInt(ms, 10) || 1000;
150
+ await new Promise((r) => setTimeout(r, msNum));
151
+ },
152
+ }),
153
+ };
154
+ type UIBlock = ModelMessage & { id: string; status: "in-progress" | "done" };
155
+ type UI = UIBlock[];
156
+ type MessageEvent = { url: string; text: string; ctx?: unknown };
157
+
158
+ const mq = new TransformStream<MessageEvent, MessageEvent>();
159
+ const mq_w = mq.writable.getWriter();
160
+
161
+ // Configure logger
162
+ const logDate = new Date().toISOString().split("T")[0];
163
+ const logger = winston.createLogger({
164
+ level: process.env.LOG_LEVEL || "info",
165
+ format: winston.format.combine(
166
+ winston.format.timestamp(),
167
+ winston.format.errors({ stack: true }),
168
+ winston.format.printf(({ timestamp, level, message, ...meta }) => {
169
+ const metaStr = Object.keys(meta).length ? JSON.stringify(meta, null, 2) : "";
170
+ return `[${timestamp}] [${level.toUpperCase()}] ${message}${metaStr ? "\n" + metaStr : ""}`;
171
+ }),
172
+ ),
173
+ transports: [
174
+ new winston.transports.Console({
175
+ format: winston.format.combine(
176
+ winston.format.colorize(),
177
+ winston.format.printf(({ timestamp, level, message, ...meta }) => {
178
+ const metaStr = Object.keys(meta).length ? JSON.stringify(meta, null, 2) : "";
179
+ return `[${timestamp}] ${level}: ${message}${metaStr ? "\n" + metaStr : ""}`;
180
+ }),
181
+ ),
182
+ }),
183
+ new winston.transports.File({
184
+ filename: `./.logs/slack-bolt-${logDate}.log`,
185
+ level: "debug",
186
+ }),
187
+ ],
188
+ });
189
+
190
+ // Initialize Bolt app
191
+ const app = new App({
192
+ token: process.env.SLACK_BOT_TOKEN,
193
+ socketMode: true,
194
+ appToken: process.env.SLACK_SOCKET_TOKEN,
195
+ logLevel: process.env.LOG_LEVEL === "debug" ? LogLevel.DEBUG : LogLevel.INFO,
196
+ });
197
+
198
+ // ============================================================================
199
+ // Event Listeners
200
+ // ============================================================================
201
+
202
+ // Handle app_mention events
203
+ app.event("app_mention", async ({ event, client, say, logger: boltLogger }) => {
204
+ logger.info(
205
+ await parseSlackMessageToMarkdown(
206
+ `Received app_mention from user <@${event.user}> in channel <#${event.channel}>`,
207
+ ),
208
+ );
209
+ await mq_w.write({
210
+ url:
211
+ (await slack.chat.getPermalink({ channel: event.channel, message_ts: event.ts })).permalink ||
212
+ DIE(""),
213
+ text: event.text,
214
+ ctx: {
215
+ user: event.user,
216
+ channel: event.channel,
217
+ timestamp: event.ts,
218
+ thread_ts: event.thread_ts,
219
+ },
220
+ });
221
+ });
222
+
223
+ // Handle direct messages
224
+ app.message(async ({ message, client, say, logger: boltLogger }) => {
225
+ // Only respond to messages in DMs (im type)
226
+ if (message.channel_type !== "im") return;
227
+ if (message.subtype) return; // Ignore message updates, deletions, etc.
228
+
229
+ const msg = message as {
230
+ text?: string;
231
+ user?: string;
232
+ ts: string;
233
+ channel: string;
234
+ thread_ts?: string;
235
+ };
236
+ const { text, user, ts: timestamp, channel } = msg;
237
+ logger.info(
238
+ await parseSlackMessageToMarkdown(
239
+ `Received DM from user <@${msg.user}>: ${msg.text?.slice(0, 100)}`,
240
+ ),
241
+ );
242
+ await mq_w.write({
243
+ url: (await slack.chat.getPermalink({ channel, message_ts: timestamp })).permalink || DIE(""),
244
+ text: text || "",
245
+ ctx: { user, channel, timestamp, thread_ts: msg.thread_ts },
246
+ });
247
+ });
248
+
249
+ // Handle app_home_opened event
250
+ app.event("app_home_opened", async ({ event, client, logger: boltLogger }) => {
251
+ if (event.tab !== "home") return;
252
+
253
+ logger.info(await parseSlackMessageToMarkdown(`App home opened by user <@${event.user}>`));
254
+
255
+ try {
256
+ await client.views.publish({
257
+ user_id: event.user,
258
+ view: {
259
+ type: "home",
260
+ blocks: [
261
+ {
262
+ type: "section",
263
+ text: {
264
+ type: "mrkdwn",
265
+ text: `*Welcome to ComfyPR Bot, <@${event.user}>!* :robot_face:`,
266
+ },
267
+ },
268
+ {
269
+ type: "divider",
270
+ },
271
+ {
272
+ type: "section",
273
+ text: {
274
+ type: "mrkdwn",
275
+ text: "*What I can do:*\n• Search code across ComfyUI repositories\n• Search GitHub issues and PRs\n• Search the custom nodes registry\n• Search Notion documentation\n• Spawn coding agents to create PRs",
276
+ },
277
+ },
278
+ {
279
+ type: "section",
280
+ text: {
281
+ type: "mrkdwn",
282
+ text: "*Getting Started:*\nMention me in a channel with `@ComfyPR Bot` followed by your question or task.",
283
+ },
284
+ },
285
+ {
286
+ type: "actions",
287
+ elements: [
288
+ {
289
+ type: "button",
290
+ text: {
291
+ type: "plain_text",
292
+ text: "View Documentation",
293
+ emoji: true,
294
+ },
295
+ url: "https://github.com/Comfy-Org/Comfy-PR",
296
+ action_id: "view_docs",
297
+ },
298
+ ],
299
+ },
300
+ ],
301
+ },
302
+ });
303
+ } catch (error) {
304
+ logger.error("Error publishing home view:", { error });
305
+ }
306
+ });
307
+
308
+ // ============================================================================
309
+ // Slash Commands
310
+ // ============================================================================
311
+
312
+ app.command("/pr", async ({ command, ack, respond, client }) => {
313
+ await ack();
314
+
315
+ logger.info(
316
+ await parseSlackMessageToMarkdown(
317
+ `Received /pr command from <@${command.user_id}>: ${command.text}`,
318
+ ),
319
+ );
320
+
321
+ try {
322
+ await respond({
323
+ text: `Processing command: \`${command.text}\`\nPlease wait...`,
324
+ response_type: "ephemeral",
325
+ });
326
+
327
+ // TODO: Add command processing logic
328
+ // Parse command.text and execute appropriate action
329
+ } catch (error) {
330
+ logger.error("Error handling /pr command:", { error });
331
+ await respond({
332
+ text: "Sorry, an error occurred while processing your command.",
333
+ response_type: "ephemeral",
334
+ });
335
+ }
336
+ });
337
+
338
+ // ============================================================================
339
+ // Action Handlers (Button clicks, etc.)
340
+ // ============================================================================
341
+
342
+ app.action("view_docs", async ({ ack, body, logger: boltLogger }) => {
343
+ await ack();
344
+ logger.info(`User clicked view_docs button`);
345
+ });
346
+
347
+ // ============================================================================
348
+ // Shortcuts
349
+ // ============================================================================
350
+
351
+ app.shortcut("search_code", async ({ shortcut, ack, client, logger: boltLogger }) => {
352
+ await ack();
353
+
354
+ logger.info(`User triggered search_code shortcut`);
355
+
356
+ try {
357
+ await client.views.open({
358
+ trigger_id: shortcut.trigger_id,
359
+ view: {
360
+ type: "modal",
361
+ callback_id: "search_code_modal",
362
+ title: {
363
+ type: "plain_text",
364
+ text: "Search Code",
365
+ },
366
+ blocks: [
367
+ {
368
+ type: "input",
369
+ block_id: "query_block",
370
+ element: {
371
+ type: "plain_text_input",
372
+ action_id: "query_input",
373
+ placeholder: {
374
+ type: "plain_text",
375
+ text: "Enter your search query...",
376
+ },
377
+ },
378
+ label: {
379
+ type: "plain_text",
380
+ text: "Search Query",
381
+ },
382
+ },
383
+ ],
384
+ submit: {
385
+ type: "plain_text",
386
+ text: "Search",
387
+ },
388
+ },
389
+ });
390
+ } catch (error) {
391
+ logger.error("Error opening search modal:", { error });
392
+ }
393
+ });
394
+
395
+ // ============================================================================
396
+ // View Submissions (Modal forms)
397
+ // ============================================================================
398
+
399
+ app.view("search_code_modal", async ({ ack, body, view, client }) => {
400
+ await ack();
401
+
402
+ const query = view.state.values.query_block.query_input.value;
403
+ const userId = body.user.id;
404
+
405
+ logger.info(`User ${userId} submitted search query: ${query}`);
406
+
407
+ try {
408
+ // Send result as DM
409
+ await client.chat.postMessage({
410
+ channel: userId,
411
+ text: `Searching for: \`${query}\`\n\n_Results will appear here..._`,
412
+ });
413
+
414
+ // TODO: Execute actual search and update message
415
+ } catch (error) {
416
+ logger.error("Error processing search:", { error });
417
+ }
418
+ });
419
+
420
+ // ============================================================================
421
+ // Start the app
422
+ // ============================================================================
423
+
424
+ export async function startSlackBoltApp() {
425
+ const port = Number(process.env.PRBOT_PORT || 3000);
426
+
427
+ // lock port to prevent multiple instances
428
+ const _server = Bun.serve({ port, fetch: () => new Response("Hello from ComfyPR Bot") });
429
+
430
+ try {
431
+ await app.start();
432
+ // logger.info(`ComfyPR Bot (Bolt) is running on port ${port}`);
433
+ } catch (error) {
434
+ logger.error("Failed to start Bolt app:", { error });
435
+ process.exit(1);
436
+ }
437
+
438
+ logger.info(`ComfyPR Bot (Bolt) is running on port ${port}`);
439
+
440
+ await sflow(mq.readable)
441
+ .forEach((m) => logger.info(`Processing Slack message from ${m.url}`))
442
+ .forEach(async (m) => {
443
+ // fetch ctx
444
+ const { channel, ts } = slackMessageUrlParse(m.url);
445
+ m.text ||=
446
+ (await slackCached.conversations
447
+ .history({ channel, latest: ts, inclusive: true, limit: 1 })
448
+ .then((res) => res.messages?.[0]?.text || "")) ||
449
+ DIE(`Failed to fetch Slack message content from ${m.url}`);
450
+
451
+ // check if user is restricted/bot
452
+ const userinfo =
453
+ (await slackCached.users.info({
454
+ user: ((m.ctx as Record<string, unknown>)?.user as string) || "",
455
+ })) || DIE("missing user info");
456
+ if (userinfo.user?.is_restricted) return;
457
+ if (userinfo.user?.is_bot) return;
458
+
459
+ await using _ = deferClose(
460
+ await slack.reactions.add({ channel, timestamp: ts, name: "eyes" }),
461
+ async () => await slack.reactions.remove({ channel, timestamp: ts, name: "eyes" }),
462
+ );
463
+
464
+ const upsertReply = throttle(
465
+ async (ui: UI) => {
466
+ const r = await SlackReplies.get(m.url);
467
+
468
+ const raw = ui
469
+ .slice(-1) // take last 1 blocks show to user
470
+ .map((b) => formatMessage(b, { showRole: false }))
471
+ .join("\n\n");
472
+ const text = await prettier.format(raw, { parser: "markdown" });
473
+
474
+ if (r) {
475
+ if (r.text !== text) {
476
+ // update existing message
477
+ const updated = await slack.chat.update({
478
+ ...slackMessageUrlParse(r.url),
479
+ text,
480
+ blocks: [{ type: "markdown", text }],
481
+ });
482
+ await SlackRepliesSentBlocks.set(r.url, { ui });
483
+
484
+ await SlackReplies.set(m.url, { ...r, text });
485
+ }
486
+ } else if (text.length) {
487
+ // post new message in thread
488
+ const posted = await slack.chat.postMessage({
489
+ ...slackMessageUrlParse(m.url),
490
+ text,
491
+ blocks: [{ type: "markdown", text }],
492
+ });
493
+ const url =
494
+ (
495
+ await slack.chat.getPermalink({
496
+ channel: posted.channel || DIE("missing channel"),
497
+ message_ts: posted.ts || DIE("missing message_ts"),
498
+ })
499
+ ).permalink || DIE("missing permalink");
500
+ await SlackRepliesSentBlocks.set(url, { ui });
501
+
502
+ await SlackReplies.set(m.url, {
503
+ url,
504
+ text,
505
+ });
506
+ }
507
+ },
508
+ 1000,
509
+ { leading: true, trailing: true },
510
+ );
511
+
512
+ await upsertReply([]);
513
+ const _respId = `resp-${Date.now()}`;
514
+
515
+ // read replies if have thread_ts, else read channel history.
516
+ const thread_ts =
517
+ ((m.ctx as Record<string, unknown>)?.thread_ts as string | undefined) ??
518
+ slackMessageUrlParse(m.url).thread_ts ??
519
+ undefined;
520
+ const limit = 20;
521
+ const threadMessages: ModelMessage[] = await sflow(
522
+ (
523
+ (thread_ts
524
+ ? await slack.conversations.replies({ channel, ts: thread_ts, limit })
525
+ : await slack.conversations.history({ channel, latest: ts, inclusive: true, limit })
526
+ ).messages || []
527
+ ).toSorted(compareBy((e) => Number(e.ts))), // sort by time ascending
528
+ )
529
+ .filter((e) => {
530
+ // special filter that removes content: "@comfyprbot: 新しいアシスタントスレッド",
531
+ if (e.text === "新しいアシスタントスレッド") return false;
532
+ return true;
533
+ })
534
+ .flatMap(async (msg) => {
535
+ const userinfo = await slackCached.users.info({
536
+ user: msg.user || DIE(`missing user id in msg ${JSON.stringify(msg)}`),
537
+ });
538
+ if (userinfo.user?.is_restricted) return [];
539
+ const isComfyPrBot = userinfo.user?.id === (await slackCached.auth.test()).user_id;
540
+ if (isComfyPrBot) {
541
+ const ui = await SlackRepliesSentBlocks.get(m.url).then((r) => r?.ui);
542
+ if (ui?.length) return ui as ModelMessage[];
543
+ }
544
+
545
+ const isOtherBot = userinfo.user?.is_bot || false;
546
+ return [
547
+ {
548
+ role: !isOtherBot ? ("user" as const) : ("assistant" as const),
549
+ content: `@${userinfo.user?.name || DIE("missing user name")}: ${msg.text || ""}`,
550
+ },
551
+ ];
552
+ })
553
+ .filter()
554
+ .toArray();
555
+ const theUserPreferences = `The request user is ${userinfo.user?.real_name || "Unknown User"} (@${
556
+ userinfo.user?.name || "unknown"
557
+ })${userinfo.user?.profile?.title ? `, titled "${userinfo.user?.profile?.title}"` : ""}.${
558
+ userinfo.user?.profile?.email ? ` Their email is ${userinfo.user?.profile?.email}.` : ""
559
+ } Their Slack ID is ${userinfo.user?.id}.
560
+
561
+ The User preferences are:
562
+ <preferences>
563
+ ${(await SlackUserPreferences.get(userinfo.user?.id || ""))?.preferences || "# No preferences set."}
564
+ </preferences>
565
+ `;
566
+ const messages = [
567
+ {
568
+ role: "system",
569
+ content: system,
570
+ },
571
+ {
572
+ role: "system",
573
+ content: theUserPreferences,
574
+ },
575
+ ...threadMessages,
576
+ ] satisfies ModelMessage[];
577
+ const tools: ToolSet = {
578
+ ...systemTools,
579
+ };
580
+ logger.debug("AI messages:\n" + formatMessages(messages));
581
+ const resp = streamText({
582
+ model: openai("gpt-5.1-codex-max"),
583
+ messages,
584
+ tools,
585
+ maxOutputTokens: 4000,
586
+ maxRetries: 3,
587
+ stopWhen: () => false,
588
+ presencePenalty: 1,
589
+ });
590
+ return (
591
+ sflow(resp.fullStream)
592
+ .reduce(async (ui: UI, part) => {
593
+ await tsmatch(part)
594
+ // common
595
+ .with({ type: "start" }, () => {}) // do nothing
596
+ .with({ type: "finish" }, () => {}) // do nothing
597
+ .with({ type: "start-step" }, () => {}) // do nothing
598
+ .with({ type: "finish-step" }, () => {}) // do nothing
599
+ // text
600
+ .with({ type: "text-start" }, (e) => {
601
+ ui.push({ id: e.id, content: "", status: "in-progress", role: "assistant" });
602
+ })
603
+ .with({ type: "text-delta" }, (e) => {
604
+ ui.findLast((u) => u.id === e.id)!.content += e.text;
605
+ })
606
+ .with({ type: "text-end" }, (e) => {
607
+ ui.findLast((u) => u.id === e.id)!.status = "done";
608
+ })
609
+ // tool, do nothing for now
610
+ .with({ type: "tool-input-start" }, (e) => {
611
+ logger.debug(`Tool ${e.toolName} input started`, e);
612
+
613
+ // ui.push({
614
+ // id: e.id,
615
+ // role: "tool",
616
+ // status: "in-progress",
617
+ // tool_name: e.toolName,
618
+ // arguments: {},
619
+ // });
620
+ })
621
+ .with({ type: "tool-input-delta" }, (_e) => {
622
+ // ui.findLast((u) => u.id === e.id)!.text += e.delta;
623
+ })
624
+ .with({ type: "tool-input-end" }, (_e) => {
625
+ // ui.findLast((u) => u.id === e.id)!.status = "done";
626
+ })
627
+ // .with({ type: "file" }, async (f) => {
628
+ // // upload to slack
629
+ // logger.debug(`File received: ${f.file.name} (${f.file.size} bytes)`, f);
630
+ // await slack.files.uploadV2({
631
+ // file: Buffer.from(f.file.uint8Array),
632
+ // channel_id: channel,
633
+ // initial_comment: `File uploaded by ComfyPR Bot in response to <@${m.ctx.user}>`,
634
+ // });
635
+ // ui.push({
636
+ // id: SHA256.hash(f.file.base64).toString(),
637
+ // content: `File received: ${f.file.name} (${f.file.size} bytes)`,
638
+ // status: "done",
639
+ // role: "assistant",
640
+ // });
641
+ // })
642
+ .with({ type: "tool-result" }, (e) => {
643
+ logger.debug(`Tool ${e.toolName} returned result`, e);
644
+ ui.push({
645
+ id: e.toolCallId,
646
+ content: `Tool ${e.toolName} returned: ${yaml.stringify(e.output)}`,
647
+ status: "done",
648
+ role: "assistant",
649
+ });
650
+ })
651
+ // .with({ type: "file" }, (e) => {
652
+ // ui.push({
653
+ // id: SHA256.hash(e.file.base64).toString(),
654
+ // content: e,
655
+ // });
656
+ // // upload file to slack?
657
+ // })
658
+ // reasoning hiding
659
+ // .with({ type: "reasoning-start" }, (e) => {
660
+ // ui.push({ id: e.id, type: "reasoning", text: "", status: "in-progress" });
661
+ // })
662
+ // .with({ type: "reasoning-delta" }, (e) => {
663
+ // ui.findLast((u) => u.id === e.id)!.text += e.text;
664
+ // })
665
+ // .with({ type: "reasoning-end" }, (e) => {
666
+ // ui.findLast((u) => u.id === e.id)!.status = "done";
667
+ // })
668
+ .otherwise(() => {
669
+ logger.warn(`Unknown stream part: ${JSON.stringify(part)}`);
670
+ });
671
+
672
+ return ui;
673
+ }, [])
674
+ // save ui state
675
+
676
+ .forEach(async (ui) => await upsertReply(ui))
677
+
678
+ .run()
679
+ );
680
+ })
681
+ .run();
682
+ }
683
+
684
+ if (import.meta.main) {
685
+ await startSlackBoltApp();
686
+ }
687
+
688
+ export { app };