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
package/bot/cli.ts ADDED
@@ -0,0 +1,1171 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import path from "path";
4
+ import { hideBin } from "yargs/helpers";
5
+ import yargs from "yargs/yargs";
6
+
7
+ // GitHub abilities
8
+ import { spawnSubAgent } from "./code/pr-agent";
9
+ import { searchGitHubIssues } from "./code/issue-search";
10
+
11
+ // Registry ability
12
+ import { searchRegistryNodes } from "@/lib/registry/search";
13
+
14
+ // Slack abilities
15
+ import {
16
+ downloadSlackFile,
17
+ getSlackFileInfo,
18
+ postMessageWithFiles,
19
+ uploadSlackFile,
20
+ } from "@/lib/slack/file";
21
+ import { readNearbyMessages } from "@/lib/slack/msg-read-nearby";
22
+ import { readRecentMessages } from "@/lib/slack/msg-read-recent";
23
+ import { readSlackThread } from "@/lib/slack/msg-read-thread";
24
+ import { updateSlackMessage } from "@/lib/slack/msg-update";
25
+ import { smartPost } from "@/lib/slack/msg-post";
26
+ import { parseSlackUrl } from "@/lib/slack/parseSlackUrl";
27
+ import { parseSlackUrlSmart } from "@/lib/slack/parseSlackUrlSmart";
28
+ import { getMessageReactions } from "@/lib/slack/reactions";
29
+ import { searchMessages, searchFiles } from "@/lib/slack/search";
30
+ import { listPinnedMessages } from "@/lib/slack/pins";
31
+ import { listChannelBookmarks } from "@/lib/slack/bookmarks";
32
+ import { getMessagePermalink } from "@/lib/slack/permalink";
33
+ import { getChannelInfo } from "@/lib/slack/channel-info";
34
+ import { listChannelMembers } from "@/lib/slack/members";
35
+ import { getUserPresence, getBulkUserPresence } from "@/lib/slack/presence";
36
+ import { getCompleteMessageContext } from "@/lib/slack/context";
37
+ import yaml from "yaml";
38
+
39
+ // Notion ability
40
+ import { searchNotion } from "@/lib/notion/search";
41
+
42
+ // Video ability
43
+ import { readVideo } from "@/lib/video/read-video";
44
+
45
+ /**
46
+ * Load environment variables from .env.local in the project root
47
+ * This allows prbot to work from unknown directory
48
+ */
49
+ async function loadEnvLocal() {
50
+ const envPath = path.join(import.meta.dir, "../.env.local");
51
+
52
+ try {
53
+ const envFile = await Bun.file(envPath).text();
54
+ envFile.split("\n").forEach((line) => {
55
+ const trimmed = line.trim();
56
+ if (trimmed && !trimmed.startsWith("#")) {
57
+ const [key, ...valueParts] = trimmed.split("=");
58
+ if (key && valueParts.length > 0) {
59
+ const value = valueParts.join("=").replace(/^["']|["']$/g, "");
60
+ process.env[key.trim()] = value;
61
+ }
62
+ }
63
+ });
64
+ } catch (_e) {
65
+ // .env.local doesn't exist or can't be read, continue anyway
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Handle PR command with auto-generated branch names
71
+ */
72
+ async function handlePrCommand(args: {
73
+ repo: string;
74
+ base?: string;
75
+ head?: string;
76
+ prompt: string;
77
+ }) {
78
+ const { repo, base = "main", head, prompt } = args;
79
+
80
+ // Import here to avoid circular dependencies
81
+ const zChatCompletion = (await import("../lib/zChat")).default;
82
+ const z = (await import("zod")).default;
83
+
84
+ let finalHead = head;
85
+ let finalBase = base;
86
+
87
+ // Auto-generate head branch if not provided
88
+ if (!finalHead) {
89
+ console.log("Generating head branch name from prompt...");
90
+ const branchInfo = (await zChatCompletion(
91
+ z.object({
92
+ base: z.string(),
93
+ head: z.string(),
94
+ }),
95
+ {
96
+ model: "gpt-4o-mini",
97
+ },
98
+ )`Generate a git branch name following conventions:
99
+ - Format: <type>/<description>
100
+ - Types: feature/, fix/, refactor/, docs/, test/, chore/
101
+ - Description: kebab-case, short and descriptive
102
+
103
+ Base: ${finalBase}
104
+ Task: ${prompt}
105
+
106
+ Generate branch name.`) as { base: string; head: string };
107
+ finalBase = branchInfo.base;
108
+ finalHead = branchInfo.head;
109
+ console.log(`Generated head branch: ${finalHead}`);
110
+ }
111
+
112
+ await spawnSubAgent({
113
+ repo,
114
+ base: finalBase,
115
+ head: finalHead as string,
116
+ prompt,
117
+ });
118
+ }
119
+
120
+ async function main() {
121
+ const argv = yargs(hideBin(process.argv))
122
+ .scriptName("prbot")
123
+ .usage("$0 <command> [options]")
124
+ .command(
125
+ "code pr",
126
+ "Open an interactive coding sub-agent and automatically create a PR",
127
+ (y) =>
128
+ y
129
+ .option("repo", {
130
+ alias: "r",
131
+ type: "string",
132
+ describe: "owner/repo (e.g. Comfy-Org/ComfyUI)",
133
+ demandOption: true,
134
+ })
135
+ .option("base", {
136
+ type: "string",
137
+ describe: "Base branch to merge into (defaults to main)",
138
+ default: "main",
139
+ })
140
+ .option("head", {
141
+ type: "string",
142
+ describe: "Head branch to develop on (auto-generated if not provided)",
143
+ })
144
+ .option("prompt", {
145
+ alias: "p",
146
+ type: "string",
147
+ describe: "Task prompt for the coding agent",
148
+ demandOption: true,
149
+ }),
150
+ async (args) => {
151
+ await handlePrCommand({
152
+ repo: args.repo as string,
153
+ base: args.base as string | undefined,
154
+ head: args.head as string | undefined,
155
+ prompt: args.prompt as string,
156
+ });
157
+ },
158
+ )
159
+ .command(
160
+ "code search",
161
+ "Search ComfyUI code using comfy-codesearch service",
162
+ (y) =>
163
+ y
164
+ .option("query", {
165
+ alias: "q",
166
+ type: "string",
167
+ describe: "Search query (supports repo: and path: filters)",
168
+ demandOption: true,
169
+ })
170
+ .option("repo", {
171
+ type: "string",
172
+ describe: "Filter by repository (e.g. Comfy-Org/ComfyUI)",
173
+ })
174
+ .option("path", {
175
+ type: "string",
176
+ describe: "Filter by file path pattern",
177
+ }),
178
+ async (args) => {
179
+ await loadEnvLocal();
180
+
181
+ let query = args.query as string;
182
+ if (args.repo) {
183
+ query = `repo:${args.repo} ${query}`;
184
+ }
185
+ if (args.path) {
186
+ query = `path:${args.path} ${query}`;
187
+ }
188
+
189
+ const { $ } = await import("bun");
190
+ const result = await $`comfy-codesearch ${query}`.quiet();
191
+ console.log(result.stdout.toString());
192
+ },
193
+ )
194
+ .command(
195
+ "github-issue search",
196
+ "Search for issues across Comfy-Org repositories",
197
+ (y) =>
198
+ y
199
+ .option("query", {
200
+ alias: "q",
201
+ type: "string",
202
+ describe: "Search query",
203
+ demandOption: true,
204
+ })
205
+ .option("limit", {
206
+ alias: "l",
207
+ type: "number",
208
+ describe: "Maximum number of results",
209
+ default: 10,
210
+ }),
211
+ async (args) => {
212
+ await loadEnvLocal();
213
+ const results = await searchGitHubIssues(
214
+ args.query as string,
215
+ (args.limit as number) ?? 10,
216
+ );
217
+
218
+ console.log(`Found ${results.length} results for: "${args.query}"\n`);
219
+
220
+ for (const issue of results) {
221
+ console.log(`#${issue.number} - ${issue.title}`);
222
+ console.log(` Repository: ${issue.repository}`);
223
+ console.log(` State: ${issue.state}`);
224
+ console.log(` Type: ${issue.is_pull_request ? "Pull Request" : "Issue"}`);
225
+ console.log(` Author: ${issue.user}`);
226
+ if (issue.labels.length > 0) {
227
+ console.log(` Labels: ${issue.labels.join(", ")}`);
228
+ }
229
+ console.log(` URL: ${issue.url}`);
230
+ console.log(` Updated: ${issue.updated_at}`);
231
+ console.log("---");
232
+ }
233
+ },
234
+ )
235
+ .command(
236
+ "github pr",
237
+ "Open an interactive coding sub-agent and propose a PR",
238
+ (y) =>
239
+ y
240
+ .option("repo", {
241
+ alias: "r",
242
+ type: "string",
243
+ describe: "owner/repo (e.g. Comfy-Org/ComfyUI)",
244
+ demandOption: true,
245
+ })
246
+ .option("base", {
247
+ type: "string",
248
+ describe: "Base branch to merge into (defaults to main)",
249
+ default: "main",
250
+ })
251
+ .option("head", {
252
+ type: "string",
253
+ describe: "Head branch to develop on (auto-generated if not provided)",
254
+ })
255
+ .option("prompt", {
256
+ alias: "p",
257
+ type: "string",
258
+ describe: "Task prompt for the coding agent",
259
+ demandOption: true,
260
+ }),
261
+ async (args) => {
262
+ await handlePrCommand({
263
+ repo: args.repo as string,
264
+ base: args.base as string | undefined,
265
+ head: args.head as string | undefined,
266
+ prompt: args.prompt as string,
267
+ });
268
+ },
269
+ )
270
+ .command(
271
+ ["pr", "prbot"],
272
+ "Alias of code pr",
273
+ (y) =>
274
+ y
275
+ .option("repo", { alias: "r", type: "string", demandOption: true })
276
+ .option("base", { type: "string", default: "main" })
277
+ .option("head", { type: "string" })
278
+ .option("prompt", { alias: "p", type: "string", demandOption: true }),
279
+ async (args) => {
280
+ await handlePrCommand({
281
+ repo: args.repo as string,
282
+ base: args.base as string | undefined,
283
+ head: args.head as string | undefined,
284
+ prompt: args.prompt as string,
285
+ });
286
+ },
287
+ )
288
+ .command("slack", "Slack integration commands", (yargs) => {
289
+ return yargs
290
+ .command(
291
+ "read <url>",
292
+ "Smart read: Auto-detect URL type (message/file/channel) and read appropriately (YAML output)",
293
+ (y) =>
294
+ y.positional("url", {
295
+ type: "string",
296
+ describe: "Slack URL (message, file, or channel)",
297
+ demandOption: true,
298
+ }),
299
+ async (args) => {
300
+ await loadEnvLocal();
301
+
302
+ const url = args.url as string;
303
+ const parsed = parseSlackUrlSmart(url);
304
+
305
+ switch (parsed.type) {
306
+ case "message": {
307
+ // Read nearby messages with target highlighted
308
+ const messages = await readNearbyMessages(
309
+ parsed.channel!,
310
+ parsed.ts!,
311
+ 20, // 20 before
312
+ 20, // 20 after
313
+ );
314
+ console.log(yaml.stringify(messages));
315
+ break;
316
+ }
317
+
318
+ case "channel": {
319
+ // Read recent 10 messages
320
+ const messages = await readRecentMessages(parsed.channel!, 10);
321
+ console.log(yaml.stringify(messages));
322
+ break;
323
+ }
324
+
325
+ case "file": {
326
+ // Download file to current directory
327
+ if (!parsed.fileId) {
328
+ console.error("Could not extract file ID from URL");
329
+ process.exit(1);
330
+ }
331
+
332
+ const fileInfo = await getSlackFileInfo(parsed.fileId);
333
+ const fileName = fileInfo.name || `file-${parsed.fileId}`;
334
+ const outputPath = `./${fileName}`;
335
+
336
+ await downloadSlackFile(parsed.fileId, outputPath);
337
+
338
+ console.log(
339
+ yaml.stringify({
340
+ type: "file_downloaded",
341
+ file_id: parsed.fileId,
342
+ file_name: fileName,
343
+ file_size: fileInfo.size,
344
+ downloaded_to: outputPath,
345
+ }),
346
+ );
347
+ break;
348
+ }
349
+
350
+ default:
351
+ console.error(`Unknown or unsupported Slack URL type: ${url}`);
352
+ console.error("Supported formats:");
353
+ console.error(" - Message: https://workspace.slack.com/archives/C123/p1234567890");
354
+ console.error(" - Channel: https://workspace.slack.com/archives/C123");
355
+ console.error(" - File: https://files.slack.com/files-pri/T123-F456/file.pdf");
356
+ process.exit(1);
357
+ }
358
+ },
359
+ )
360
+ .command(
361
+ "update",
362
+ "Update a Slack message",
363
+ (y) =>
364
+ y
365
+ .option("channel", { alias: "c", type: "string", demandOption: true })
366
+ .option("ts", { alias: "t", type: "string", demandOption: true })
367
+ .option("text", { alias: "m", type: "string", demandOption: true }),
368
+ async (args) => {
369
+ await loadEnvLocal();
370
+ await updateSlackMessage(
371
+ args.channel as string,
372
+ args.ts as string,
373
+ args.text as string,
374
+ );
375
+ },
376
+ )
377
+ .command(
378
+ "post",
379
+ "Smart-post: short text as a message, long markdown as a file upload (auto-detected at 2900 chars)",
380
+ (y) =>
381
+ y
382
+ .option("channel", {
383
+ alias: "c",
384
+ type: "string",
385
+ demandOption: true,
386
+ describe: "Channel ID",
387
+ })
388
+ .option("text", {
389
+ alias: "m",
390
+ type: "string",
391
+ describe: "Text to post (or omit to read --file)",
392
+ })
393
+ .option("file", {
394
+ alias: "f",
395
+ type: "string",
396
+ describe: "File path to read content from",
397
+ })
398
+ .option("title", { type: "string", describe: "Title used when uploading as a file" })
399
+ .option("comment", { type: "string", describe: "Comment to accompany file upload" })
400
+ .option("thread", {
401
+ alias: "t",
402
+ type: "string",
403
+ describe: "Thread timestamp to reply in",
404
+ })
405
+ .check((argv) => {
406
+ if (!argv.text && !argv.file)
407
+ throw new Error("Either --text or --file is required");
408
+ return true;
409
+ }),
410
+ async (args) => {
411
+ await loadEnvLocal();
412
+ let text = args.text as string | undefined;
413
+ if (!text && args.file) {
414
+ text = await Bun.file(args.file as string).text();
415
+ }
416
+ const result = await smartPost(args.channel as string, text!, {
417
+ threadTs: args.thread as string | undefined,
418
+ title: args.title as string | undefined,
419
+ filePath: args.file as string | undefined,
420
+ comment: args.comment as string | undefined,
421
+ });
422
+ if (result.method === "message") {
423
+ console.log(`Posted as message (ts: ${result.ts})`);
424
+ } else {
425
+ console.log(`Uploaded as file: ${result.fileUrl}`);
426
+ }
427
+ },
428
+ )
429
+ .command(
430
+ "read-thread",
431
+ "Read and print a Slack thread (YAML)",
432
+ (y) =>
433
+ y
434
+ .option("url", { alias: "u", type: "string", describe: "Slack message URL" })
435
+ .option("channel", { alias: "c", type: "string", describe: "Slack channel ID" })
436
+ .option("ts", { alias: "t", type: "string", describe: "Thread timestamp" })
437
+ .option("limit", {
438
+ alias: "l",
439
+ type: "number",
440
+ default: 100,
441
+ describe: "Max messages",
442
+ })
443
+ .check((argv) => {
444
+ // Require either URL or (channel + ts)
445
+ if (!argv.url && (!argv.channel || !argv.ts)) {
446
+ throw new Error("Either --url or both --channel and --ts are required");
447
+ }
448
+ if (argv.url && (argv.channel || argv.ts)) {
449
+ throw new Error("Cannot use --url with --channel or --ts");
450
+ }
451
+ return true;
452
+ }),
453
+ async (args) => {
454
+ await loadEnvLocal();
455
+
456
+ let channel: string;
457
+ let ts: string;
458
+
459
+ // Parse from URL if provided
460
+ if (args.url) {
461
+ const parsed = parseSlackUrl(args.url as string);
462
+ if (!parsed) {
463
+ console.error("Failed to parse Slack URL. Please check the format.");
464
+ process.exit(1);
465
+ }
466
+ channel = parsed.channel;
467
+ ts = parsed.ts;
468
+ } else {
469
+ // Use channel and ts directly
470
+ channel = args.channel as string;
471
+ ts = args.ts as string;
472
+ }
473
+
474
+ const items = await readSlackThread(channel, ts, (args.limit as number) ?? 100);
475
+ console.log(yaml.stringify(items));
476
+ },
477
+ )
478
+ .command(
479
+ "read-nearby",
480
+ "Read nearby messages around a specific timestamp in a Slack channel",
481
+ (y) =>
482
+ y
483
+ .option("url", { alias: "u", type: "string", describe: "Slack message URL" })
484
+ .option("channel", { alias: "c", type: "string", describe: "Slack channel ID" })
485
+ .option("ts", { alias: "t", type: "string", describe: "Message timestamp" })
486
+ .option("before", {
487
+ alias: "b",
488
+ type: "number",
489
+ default: 10,
490
+ describe: "Messages before",
491
+ })
492
+ .option("after", {
493
+ alias: "a",
494
+ type: "number",
495
+ default: 10,
496
+ describe: "Messages after",
497
+ })
498
+ .check((argv) => {
499
+ // Require either URL or (channel + ts)
500
+ if (!argv.url && (!argv.channel || !argv.ts)) {
501
+ throw new Error("Either --url or both --channel and --ts are required");
502
+ }
503
+ if (argv.url && (argv.channel || argv.ts)) {
504
+ throw new Error("Cannot use --url with --channel or --ts");
505
+ }
506
+ return true;
507
+ }),
508
+ async (args) => {
509
+ await loadEnvLocal();
510
+
511
+ let channel: string;
512
+ let ts: string;
513
+
514
+ // Parse from URL if provided
515
+ if (args.url) {
516
+ const parsed = parseSlackUrl(args.url as string);
517
+ if (!parsed) {
518
+ console.error("Failed to parse Slack URL. Please check the format.");
519
+ process.exit(1);
520
+ }
521
+ channel = parsed.channel;
522
+ ts = parsed.ts;
523
+ } else {
524
+ // Use channel and ts directly
525
+ channel = args.channel as string;
526
+ ts = args.ts as string;
527
+ }
528
+
529
+ const items = await readNearbyMessages(
530
+ channel,
531
+ ts,
532
+ (args.before as number) ?? 10,
533
+ (args.after as number) ?? 10,
534
+ );
535
+ console.log(yaml.stringify(items));
536
+ },
537
+ )
538
+ .command(
539
+ "download-file",
540
+ "Download a file from Slack",
541
+ (y) =>
542
+ y
543
+ .option("fileId", {
544
+ alias: "f",
545
+ type: "string",
546
+ demandOption: true,
547
+ describe: "Slack file ID",
548
+ })
549
+ .option("output", {
550
+ alias: "o",
551
+ type: "string",
552
+ demandOption: true,
553
+ describe: "Output file path",
554
+ }),
555
+ async (args) => {
556
+ await loadEnvLocal();
557
+ await downloadSlackFile(args.fileId as string, args.output as string);
558
+ },
559
+ )
560
+ .command(
561
+ "file-info",
562
+ "Get information about a Slack file (YAML)",
563
+ (y) =>
564
+ y.option("fileId", {
565
+ alias: "f",
566
+ type: "string",
567
+ demandOption: true,
568
+ describe: "Slack file ID",
569
+ }),
570
+ async (args) => {
571
+ await loadEnvLocal();
572
+ const info = await getSlackFileInfo(args.fileId as string);
573
+ console.log(yaml.stringify(info));
574
+ },
575
+ )
576
+ .command(
577
+ "post-with-files",
578
+ "Post a message with file attachments",
579
+ (y) =>
580
+ y
581
+ .option("channel", {
582
+ alias: "c",
583
+ type: "string",
584
+ demandOption: true,
585
+ describe: "Channel ID",
586
+ })
587
+ .option("text", {
588
+ alias: "m",
589
+ type: "string",
590
+ demandOption: true,
591
+ describe: "Message text",
592
+ })
593
+ .option("file", {
594
+ alias: "f",
595
+ type: "array",
596
+ demandOption: true,
597
+ describe: "File path(s) to attach",
598
+ })
599
+ .option("thread", {
600
+ alias: "t",
601
+ type: "string",
602
+ describe: "Thread timestamp to reply in",
603
+ }),
604
+ async (args) => {
605
+ await loadEnvLocal();
606
+ const files = (args.file as string[]).filter((f) => typeof f === "string");
607
+ await postMessageWithFiles(
608
+ args.channel as string,
609
+ args.text as string,
610
+ files,
611
+ args.thread as string | undefined,
612
+ );
613
+ },
614
+ )
615
+ .command(
616
+ "upload-file",
617
+ "Upload a file to Slack",
618
+ (y) =>
619
+ y
620
+ .option("channel", {
621
+ alias: "c",
622
+ type: "string",
623
+ demandOption: true,
624
+ describe: "Channel ID",
625
+ })
626
+ .option("file", {
627
+ alias: "f",
628
+ type: "string",
629
+ demandOption: true,
630
+ describe: "File path to upload",
631
+ })
632
+ .option("title", { type: "string", describe: "File title" })
633
+ .option("comment", { alias: "m", type: "string", describe: "Initial comment" })
634
+ .option("thread", {
635
+ alias: "t",
636
+ type: "string",
637
+ describe: "Thread timestamp to reply in",
638
+ }),
639
+ async (args) => {
640
+ await loadEnvLocal();
641
+ await uploadSlackFile(args.channel as string, args.file as string, {
642
+ title: args.title as string | undefined,
643
+ initialComment: args.comment as string | undefined,
644
+ threadTs: args.thread as string | undefined,
645
+ });
646
+ },
647
+ )
648
+ .command(
649
+ "reactions <url>",
650
+ "Get reactions for a message",
651
+ (y) =>
652
+ y.positional("url", {
653
+ type: "string",
654
+ describe: "Slack message URL",
655
+ demandOption: true,
656
+ }),
657
+ async (args) => {
658
+ await loadEnvLocal();
659
+ const parsed = parseSlackUrl(args.url as string);
660
+ if (!parsed) {
661
+ console.error("Invalid Slack message URL");
662
+ process.exit(1);
663
+ }
664
+ const reactions = await getMessageReactions(parsed.channel, parsed.ts);
665
+ console.log(yaml.stringify(reactions));
666
+ },
667
+ )
668
+ .command(
669
+ "search",
670
+ "Search messages or files across workspace",
671
+ (y) =>
672
+ y
673
+ .option("query", {
674
+ alias: "q",
675
+ type: "string",
676
+ demandOption: true,
677
+ describe: "Search query",
678
+ })
679
+ .option("channel", {
680
+ alias: "c",
681
+ type: "string",
682
+ describe: "Filter by channel ID",
683
+ })
684
+ .option("limit", {
685
+ alias: "l",
686
+ type: "number",
687
+ default: 20,
688
+ describe: "Max results",
689
+ })
690
+ .option("type", {
691
+ type: "string",
692
+ default: "messages",
693
+ describe: "Search type: messages|files",
694
+ })
695
+ .option("sort", {
696
+ type: "string",
697
+ default: "timestamp",
698
+ describe: "Sort by: score|timestamp",
699
+ }),
700
+ async (args) => {
701
+ await loadEnvLocal();
702
+ const searchType = args.type === "files" ? "files" : "messages";
703
+ let results;
704
+ if (searchType === "files") {
705
+ results = await searchFiles(args.query as string, {
706
+ limit: args.limit as number,
707
+ sort: args.sort as "score" | "timestamp",
708
+ });
709
+ } else {
710
+ results = await searchMessages(args.query as string, {
711
+ channel: args.channel as string | undefined,
712
+ limit: args.limit as number,
713
+ sort: args.sort as "score" | "timestamp",
714
+ });
715
+ }
716
+ console.log(yaml.stringify(results));
717
+ },
718
+ )
719
+ .command(
720
+ "pins <url>",
721
+ "List pinned messages in a channel",
722
+ (y) =>
723
+ y.positional("url", {
724
+ type: "string",
725
+ describe: "Slack channel URL or message URL",
726
+ demandOption: true,
727
+ }),
728
+ async (args) => {
729
+ await loadEnvLocal();
730
+ const parsed = parseSlackUrlSmart(args.url as string);
731
+ if (!parsed.channel) {
732
+ console.error("Invalid Slack URL - must be a channel or message URL");
733
+ process.exit(1);
734
+ }
735
+ const pins = await listPinnedMessages(parsed.channel);
736
+ console.log(yaml.stringify(pins));
737
+ },
738
+ )
739
+ .command(
740
+ "bookmarks <url>",
741
+ "List bookmarks in a channel",
742
+ (y) =>
743
+ y.positional("url", {
744
+ type: "string",
745
+ describe: "Slack channel URL or message URL",
746
+ demandOption: true,
747
+ }),
748
+ async (args) => {
749
+ await loadEnvLocal();
750
+ const parsed = parseSlackUrlSmart(args.url as string);
751
+ if (!parsed.channel) {
752
+ console.error("Invalid Slack URL - must be a channel or message URL");
753
+ process.exit(1);
754
+ }
755
+ const bookmarks = await listChannelBookmarks(parsed.channel);
756
+ console.log(yaml.stringify(bookmarks));
757
+ },
758
+ )
759
+ .command(
760
+ "permalink <url>",
761
+ "Get permalink for a message",
762
+ (y) =>
763
+ y.positional("url", {
764
+ type: "string",
765
+ describe: "Slack message URL",
766
+ demandOption: true,
767
+ }),
768
+ async (args) => {
769
+ await loadEnvLocal();
770
+ const parsed = parseSlackUrl(args.url as string);
771
+ if (!parsed) {
772
+ console.error("Invalid Slack message URL");
773
+ process.exit(1);
774
+ }
775
+ const permalink = await getMessagePermalink(parsed.channel, parsed.ts);
776
+ console.log(yaml.stringify(permalink));
777
+ },
778
+ )
779
+ .command(
780
+ "channel-info <url>",
781
+ "Get detailed channel information",
782
+ (y) =>
783
+ y.positional("url", {
784
+ type: "string",
785
+ describe: "Slack channel URL or message URL",
786
+ demandOption: true,
787
+ }),
788
+ async (args) => {
789
+ await loadEnvLocal();
790
+ const parsed = parseSlackUrlSmart(args.url as string);
791
+ if (!parsed.channel) {
792
+ console.error("Invalid Slack URL - must be a channel or message URL");
793
+ process.exit(1);
794
+ }
795
+ const info = await getChannelInfo(parsed.channel);
796
+ console.log(yaml.stringify(info));
797
+ },
798
+ )
799
+ .command(
800
+ "members <url>",
801
+ "List channel members",
802
+ (y) =>
803
+ y
804
+ .positional("url", {
805
+ type: "string",
806
+ describe: "Slack channel URL or message URL",
807
+ demandOption: true,
808
+ })
809
+ .option("limit", {
810
+ alias: "l",
811
+ type: "number",
812
+ default: 100,
813
+ describe: "Max members",
814
+ }),
815
+ async (args) => {
816
+ await loadEnvLocal();
817
+ const parsed = parseSlackUrlSmart(args.url as string);
818
+ if (!parsed.channel) {
819
+ console.error("Invalid Slack URL - must be a channel or message URL");
820
+ process.exit(1);
821
+ }
822
+ const members = await listChannelMembers(parsed.channel, args.limit as number);
823
+ console.log(yaml.stringify(members));
824
+ },
825
+ )
826
+ .command(
827
+ "presence <user_id...>",
828
+ "Get user presence status",
829
+ (y) =>
830
+ y.positional("user_id", {
831
+ type: "string",
832
+ describe: "User ID(s)",
833
+ demandOption: true,
834
+ }),
835
+ async (args) => {
836
+ await loadEnvLocal();
837
+ const userIds = [args.user_id].flat() as string[];
838
+ let result;
839
+ if (userIds.length === 1) {
840
+ result = await getUserPresence(userIds[0]);
841
+ } else {
842
+ result = await getBulkUserPresence(userIds);
843
+ }
844
+ console.log(yaml.stringify(result));
845
+ },
846
+ )
847
+ .command(
848
+ "context <url>",
849
+ "Get complete message context (composite: message + reactions + thread + channel + user + permalink + pins)",
850
+ (y) =>
851
+ y.positional("url", {
852
+ type: "string",
853
+ describe: "Slack message URL",
854
+ demandOption: true,
855
+ }),
856
+ async (args) => {
857
+ await loadEnvLocal();
858
+ const parsed = parseSlackUrl(args.url as string);
859
+ if (!parsed) {
860
+ console.error("Invalid Slack message URL");
861
+ process.exit(1);
862
+ }
863
+ const context = await getCompleteMessageContext(parsed.channel, parsed.ts);
864
+ console.log(yaml.stringify(context));
865
+ },
866
+ )
867
+ .demandCommand(1, "Please specify a slack subcommand")
868
+ .help();
869
+ })
870
+ .command(
871
+ "notion search",
872
+ "Search Notion workspace pages",
873
+ (y) =>
874
+ y
875
+ .option("query", { alias: "q", type: "string", demandOption: true })
876
+ .option("limit", { alias: "l", type: "number", default: 10 }),
877
+ async (args) => {
878
+ await loadEnvLocal();
879
+ const { results, total, hasMore } = await searchNotion(
880
+ args.query as string,
881
+ (args.limit as number) ?? 10,
882
+ );
883
+ console.log(
884
+ `Found ${results.length} of ${total}${hasMore ? "+" : ""} results for: "${args.query}"\n`,
885
+ );
886
+ for (const r of results) {
887
+ console.log(`Title: ${r.title}`);
888
+ console.log(`URL: ${r.url}`);
889
+ console.log(`Last edited: ${r.last_edited_time}`);
890
+ console.log("---");
891
+ }
892
+ },
893
+ )
894
+ .command(
895
+ "registry search",
896
+ "Search ComfyUI custom nodes registry",
897
+ (y) =>
898
+ y
899
+ .option("query", { alias: "q", type: "string", demandOption: true })
900
+ .option("limit", { alias: "l", type: "number", default: 10 })
901
+ .option("include-deprecated", { type: "boolean", default: false }),
902
+ async (args) => {
903
+ const results = await searchRegistryNodes({
904
+ query: args.query as string,
905
+ limit: (args.limit as number) ?? 10,
906
+ includeDeprecated: args["include-deprecated"] as boolean,
907
+ });
908
+
909
+ console.log(`Found ${results.length} results for: "${args.query}"\n`);
910
+
911
+ for (const node of results) {
912
+ console.log(`📦 ${node.name} (${node.id})`);
913
+ console.log(
914
+ ` ${node.description.substring(0, 100)}${node.description.length > 100 ? "..." : ""}`,
915
+ );
916
+ console.log(` Publisher: ${node.publisher.name}`);
917
+ console.log(` Version: ${node.latest_version.version}`);
918
+ console.log(` Repository: ${node.repository}`);
919
+ console.log(` Downloads: ${node.downloads} | Stars: ${node.github_stars}`);
920
+ if (node.tags.length > 0) {
921
+ console.log(` Tags: ${node.tags.join(", ")}`);
922
+ }
923
+ console.log("---");
924
+ }
925
+ },
926
+ )
927
+ .command("video", "Video analysis commands", (yargs) => {
928
+ return yargs
929
+ .command(
930
+ "read",
931
+ "Analyze a video file using AI vision (Gemini or GPT-4o)",
932
+ (y) =>
933
+ y
934
+ .option("file", {
935
+ alias: "f",
936
+ type: "string",
937
+ describe: "Local video file path",
938
+ })
939
+ .option("slack-file", {
940
+ type: "string",
941
+ describe: "Slack file ID to download and analyze",
942
+ })
943
+ .option("slack-url", {
944
+ type: "string",
945
+ describe: "Slack file URL to download and analyze",
946
+ })
947
+ .option("model", {
948
+ alias: "m",
949
+ type: "string",
950
+ default: "gemini",
951
+ describe: "Model to use: gemini or gpt4o",
952
+ })
953
+ .option("prompt", {
954
+ alias: "p",
955
+ type: "string",
956
+ describe: "Custom analysis prompt",
957
+ })
958
+ .check((argv) => {
959
+ const sources = [argv.file, argv["slack-file"], argv["slack-url"]].filter(Boolean);
960
+ if (sources.length === 0) {
961
+ throw new Error("One of --file, --slack-file, or --slack-url is required");
962
+ }
963
+ if (sources.length > 1) {
964
+ throw new Error(
965
+ "Only one of --file, --slack-file, or --slack-url can be specified",
966
+ );
967
+ }
968
+ if (!["gemini", "gpt4o"].includes(argv.model as string)) {
969
+ throw new Error("Model must be 'gemini' or 'gpt4o'");
970
+ }
971
+ return true;
972
+ }),
973
+ async (args) => {
974
+ await loadEnvLocal();
975
+
976
+ let videoPath = args.file as string | undefined;
977
+
978
+ // Download from Slack if needed
979
+ if (args["slack-file"] || args["slack-url"]) {
980
+ let fileId = args["slack-file"] as string | undefined;
981
+
982
+ if (args["slack-url"]) {
983
+ const parsed = parseSlackUrlSmart(args["slack-url"] as string);
984
+ if (!parsed.fileId) {
985
+ console.error("Could not extract file ID from Slack URL");
986
+ process.exit(1);
987
+ }
988
+ fileId = parsed.fileId;
989
+ }
990
+
991
+ // Download to temp path
992
+ const fileInfo = await getSlackFileInfo(fileId!);
993
+ const fileName = fileInfo.name || `video-${fileId}`;
994
+ const tmpPath = `/tmp/${fileName}`;
995
+ console.log(`Downloading Slack file ${fileId} → ${tmpPath}`);
996
+ await downloadSlackFile(fileId!, tmpPath);
997
+ videoPath = tmpPath;
998
+ }
999
+
1000
+ console.log(`Analyzing video: ${videoPath}`);
1001
+ console.log(`Model: ${args.model}`);
1002
+ if (args.prompt)
1003
+ console.log(`Custom prompt: ${(args.prompt as string).substring(0, 80)}...`);
1004
+ console.log("---");
1005
+
1006
+ const result = await readVideo(videoPath!, {
1007
+ model: args.model as "gemini" | "gpt4o",
1008
+ prompt: args.prompt as string | undefined,
1009
+ });
1010
+
1011
+ console.log(result.description);
1012
+ console.log("\n---");
1013
+ console.log(`Model: ${result.model}`);
1014
+ console.log(`Usage: ${JSON.stringify(result.usage)}`);
1015
+ if (result.mdPath) console.log(`Report saved: ${result.mdPath}`);
1016
+ },
1017
+ )
1018
+ .demandCommand(1, "Please specify a video subcommand")
1019
+ .help();
1020
+ })
1021
+ .command("agent", "Agent control commands", (yargs) => {
1022
+ return yargs.command(
1023
+ "respond-slack-msg <url>",
1024
+ "Process a Slack message URL as if it was an app_mention event",
1025
+ (y) =>
1026
+ y.positional("url", {
1027
+ type: "string",
1028
+ describe: "Slack message URL to process",
1029
+ demandOption: true,
1030
+ }),
1031
+ async (args) => {
1032
+ await loadEnvLocal();
1033
+ const { spawnBotOnSlackMessageUrl } = await import("./slack-bot");
1034
+ const url = args.url as string;
1035
+ console.log(`Processing Slack message: ${url}`);
1036
+ await spawnBotOnSlackMessageUrl(url);
1037
+ console.log("✓ Message processing initiated");
1038
+ },
1039
+ );
1040
+ })
1041
+ .command("debug", "Debug and monitoring commands for bot tasks", (yargs) => {
1042
+ return yargs
1043
+ .command(
1044
+ "list",
1045
+ "List all active and recent claude-yes tasks",
1046
+ (y) => y,
1047
+ async () => {
1048
+ const { $ } = await import("bun");
1049
+ const scriptPath = path.join(import.meta.dir, "./list-tasks.sh");
1050
+ await $`bash ${scriptPath}`;
1051
+ },
1052
+ )
1053
+ .command(
1054
+ "watch <task_dir>",
1055
+ "Monitor a specific task with status, output, and errors",
1056
+ (y) =>
1057
+ y.positional("task_dir", {
1058
+ type: "string",
1059
+ describe: "Task directory path (e.g., /bot/slack/snomiao/1771137874-418759)",
1060
+ demandOption: true,
1061
+ }),
1062
+ async (args) => {
1063
+ const { $ } = await import("bun");
1064
+ const scriptPath = path.join(import.meta.dir, "./watch-task.sh");
1065
+ await $`bash ${scriptPath} ${args.task_dir}`;
1066
+ },
1067
+ )
1068
+ .command(
1069
+ "logs <task_dir>",
1070
+ "Show full stdout logs for a task",
1071
+ (y) =>
1072
+ y.positional("task_dir", {
1073
+ type: "string",
1074
+ describe: "Task directory path",
1075
+ demandOption: true,
1076
+ }),
1077
+ async (args) => {
1078
+ const logPath = `${args.task_dir}/.logs/claude-yes-stdout.log`;
1079
+ const { $ } = await import("bun");
1080
+ await $`tail -500 ${logPath}`;
1081
+ },
1082
+ )
1083
+ .command(
1084
+ "errors <task_dir>",
1085
+ "Show collected errors for a task",
1086
+ (y) =>
1087
+ y.positional("task_dir", {
1088
+ type: "string",
1089
+ describe: "Task directory path",
1090
+ demandOption: true,
1091
+ }),
1092
+ async (args) => {
1093
+ const errorsPath = `${args.task_dir}/.logs/COLLECTED_ERRORS.md`;
1094
+ const { $ } = await import("bun");
1095
+ try {
1096
+ await $`cat ${errorsPath}`;
1097
+ } catch {
1098
+ console.log("No errors collected for this task.");
1099
+ }
1100
+ },
1101
+ )
1102
+ .command(
1103
+ "status <task_dir>",
1104
+ "Show status file for a task",
1105
+ (y) =>
1106
+ y.positional("task_dir", {
1107
+ type: "string",
1108
+ describe: "Task directory path",
1109
+ demandOption: true,
1110
+ }),
1111
+ async (args) => {
1112
+ const statusPath = `${args.task_dir}/.logs/STATUS.txt`;
1113
+ const { $ } = await import("bun");
1114
+ await $`cat ${statusPath}`;
1115
+ },
1116
+ )
1117
+ .command(
1118
+ "tail <task_dir>",
1119
+ "Tail live stdout logs for a task",
1120
+ (y) =>
1121
+ y.positional("task_dir", {
1122
+ type: "string",
1123
+ describe: "Task directory path",
1124
+ demandOption: true,
1125
+ }),
1126
+ async (args) => {
1127
+ const logPath = `${args.task_dir}/.logs/claude-yes-stdout.log`;
1128
+ const { $ } = await import("bun");
1129
+ await $`tail -f ${logPath}`;
1130
+ },
1131
+ )
1132
+ .demandCommand(1, "Please specify a debug subcommand")
1133
+ .help();
1134
+ })
1135
+ .demandCommand(1, "Please specify a command")
1136
+ .strict()
1137
+ .help()
1138
+ .wrap(Math.min(100, yargs().terminalWidth()))
1139
+ .epilog(
1140
+ [
1141
+ "Examples:",
1142
+ " prbot code pr -r Comfy-Org/ComfyUI -b main -p 'Fix auth bug'",
1143
+ " prbot code search -q 'binarization' --repo Comfy-Org/ComfyUI",
1144
+ " prbot github-issue search -q 'authentication bug' -l 5",
1145
+ " prbot registry search -q 'video' -l 5",
1146
+ " prbot pr -r Comfy-Org/desktop -p 'Add spellcheck to editor'",
1147
+ " prbot agent respond-slack-msg 'https://workspace.slack.com/archives/C123/p1234567890'",
1148
+ " prbot debug list",
1149
+ " prbot debug watch /bot/slack/snomiao/1771137874-418759",
1150
+ " prbot debug tail /bot/slack/snomiao/1771137874-418759",
1151
+ " prbot slack update -c C123 -t 1234567890.123456 -m 'Working on it'",
1152
+ " prbot slack read-thread -c C123 -t 1234567890.123456",
1153
+ " prbot slack read-thread -u 'https://workspace.slack.com/archives/C123/p1234567890'",
1154
+ " prbot slack read-nearby -u 'https://workspace.slack.com/archives/C123/p1234567890' -b 20 -a 20",
1155
+ " prbot slack upload -c C123 -f ./report.pdf -m 'Here is the report'",
1156
+ " prbot slack post-with-files -c C123 -m 'Check these files' -f file1.pdf -f file2.png",
1157
+ " prbot slack download-file -f F123ABC -o ./downloaded.pdf",
1158
+ " prbot slack file-info -f F123ABC",
1159
+ " prbot notion search -q 'ComfyUI setup' -l 5",
1160
+ ].join("\n"),
1161
+ ).argv;
1162
+
1163
+ return argv;
1164
+ }
1165
+
1166
+ if (import.meta.main) {
1167
+ main().catch((err) => {
1168
+ console.error("CLI error:", err?.stack || err?.message || err);
1169
+ process.exit(1);
1170
+ });
1171
+ }