comfy-pr 1.4.2 → 1.5.0

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/bot/cli.ts CHANGED
@@ -374,6 +374,39 @@ async function main() {
374
374
  await loadEnvLocal();
375
375
 
376
376
  const url = args.url as string;
377
+
378
+ // Handle @username — resolve to DM channel then read recent messages
379
+ if (url.startsWith("@")) {
380
+ const { getSlack } = await import("@/lib/slack");
381
+ const slack = getSlack();
382
+ const name = url.slice(1).toLowerCase();
383
+ type SlackUser = { id?: string; name?: string; real_name?: string };
384
+ let found: SlackUser | undefined;
385
+ let cursor: string | undefined;
386
+ do {
387
+ const res = await slack.users.list({ limit: 200, ...(cursor ? { cursor } : {}) });
388
+ found = (res.members as SlackUser[] | undefined)?.find(
389
+ (u) =>
390
+ (u.name ?? "").toLowerCase() === name ||
391
+ (u.real_name ?? "").toLowerCase() === name,
392
+ );
393
+ cursor = res.response_metadata?.next_cursor || undefined;
394
+ } while (!found && cursor);
395
+ if (!found?.id) {
396
+ console.error(`User not found: ${url}`);
397
+ process.exit(1);
398
+ }
399
+ const dmRes = await slack.conversations.open({ users: found.id });
400
+ const channelId = dmRes.channel?.id;
401
+ if (!channelId) {
402
+ console.error("Could not open DM");
403
+ process.exit(1);
404
+ }
405
+ const messages = await readRecentMessages(channelId, 20);
406
+ console.log(yaml.stringify(messages));
407
+ return;
408
+ }
409
+
377
410
  const parsed = parseSlackUrlSmart(url);
378
411
 
379
412
  switch (parsed.type) {
@@ -1,8 +1,12 @@
1
1
  /**
2
2
  * Error Collector - Monitors child workspace for errors and collects them
3
+ *
4
+ * Strategy: fs.watch for fast event-driven response, plus a slow safety-net
5
+ * poll (default 60s) since recursive watch can drop events on some FS layers
6
+ * and inside containers.
3
7
  */
4
8
 
5
- import { readdir, readFile, appendFile, mkdir } from "fs/promises";
9
+ import { readdir, readFile, appendFile, mkdir, watch } from "fs/promises";
6
10
  import { existsSync } from "fs";
7
11
  import path from "path";
8
12
 
@@ -19,13 +23,16 @@ export class ErrorCollector {
19
23
  private onError?: (errorPath: string, content: string) => void;
20
24
  private checkInterval: number;
21
25
  private intervalId?: Timer;
26
+ private watchAbort?: AbortController;
22
27
  private processedErrors = new Set<string>();
28
+ private debounceTimers = new Map<string, Timer>();
23
29
 
24
30
  constructor(options: ErrorCollectorOptions) {
25
31
  this.workspaceDir = options.workspaceDir;
26
32
  this.outputLogPath = options.outputLogPath;
27
33
  this.onError = options.onError;
28
- this.checkInterval = options.checkInterval || 10000; // 10 seconds default
34
+ // Slow safety-net poll. Real-time detection comes from fs.watch.
35
+ this.checkInterval = options.checkInterval || 60_000;
29
36
  }
30
37
 
31
38
  async start() {
@@ -35,7 +42,46 @@ export class ErrorCollector {
35
42
  // Initial scan
36
43
  await this.scanForErrors();
37
44
 
38
- // Periodic scanning
45
+ // Event-driven watcher (recursive). Bursts of writes are coalesced via
46
+ // a 500ms per-file debounce.
47
+ if (existsSync(this.workspaceDir)) {
48
+ this.watchAbort = new AbortController();
49
+ (async () => {
50
+ try {
51
+ const watcher = watch(this.workspaceDir, {
52
+ recursive: true,
53
+ signal: this.watchAbort!.signal,
54
+ });
55
+ for await (const ev of watcher) {
56
+ const filename = ev.filename;
57
+ if (!filename) continue;
58
+ const lower = filename.toLowerCase();
59
+ const looksLikeError =
60
+ lower.includes("error") ||
61
+ /-errors?\.md$/i.test(filename) ||
62
+ /tools[_-]errors\.md$/i.test(filename);
63
+ if (!looksLikeError) continue;
64
+
65
+ const full = path.join(this.workspaceDir, filename);
66
+ const prev = this.debounceTimers.get(full);
67
+ if (prev) clearTimeout(prev);
68
+ this.debounceTimers.set(
69
+ full,
70
+ setTimeout(() => {
71
+ this.debounceTimers.delete(full);
72
+ this.processErrorFile(full).catch(() => {});
73
+ }, 500),
74
+ );
75
+ }
76
+ } catch (err: unknown) {
77
+ if ((err as { name?: string })?.name !== "AbortError") {
78
+ console.error("[ErrorCollector] watch failed, falling back to poll only:", err);
79
+ }
80
+ }
81
+ })();
82
+ }
83
+
84
+ // Periodic scanning (safety net for FS layers that drop watch events)
39
85
  this.intervalId = setInterval(() => {
40
86
  this.scanForErrors().catch((err) => {
41
87
  console.error("[ErrorCollector] Scan failed:", err);
@@ -48,6 +94,10 @@ export class ErrorCollector {
48
94
  clearInterval(this.intervalId);
49
95
  this.intervalId = undefined;
50
96
  }
97
+ this.watchAbort?.abort();
98
+ this.watchAbort = undefined;
99
+ for (const t of this.debounceTimers.values()) clearTimeout(t);
100
+ this.debounceTimers.clear();
51
101
  }
52
102
 
53
103
  private async scanForErrors() {
package/bot/index.ts CHANGED
@@ -5,9 +5,56 @@
5
5
  * Slack Bot
6
6
  * @author snomiao <snomiao@gmail.com>
7
7
  */
8
+ import { join } from "path";
9
+
10
+ /**
11
+ * Load .env.local with override semantics so that values in the file
12
+ * take precedence over stale shell env vars injected by pm2/parent shell.
13
+ * Prevents the SLACK_SIGNING_SECRET mismatch incident (2026-04-24).
14
+ */
15
+ async function loadEnvLocalWithOverride() {
16
+ const envPath = join(import.meta.dir, "../.env.local");
17
+ try {
18
+ const text = await Bun.file(envPath).text();
19
+ let applied = 0;
20
+ for (const line of text.split("\n")) {
21
+ const trimmed = line.trim();
22
+ if (!trimmed || trimmed.startsWith("#")) continue;
23
+ const eq = trimmed.indexOf("=");
24
+ if (eq <= 0) continue;
25
+ const key = trimmed.slice(0, eq).trim();
26
+ const value = trimmed
27
+ .slice(eq + 1)
28
+ .trim()
29
+ .replace(/^["']|["']$/g, "");
30
+ process.env[key] = value;
31
+ applied++;
32
+ }
33
+ console.log(`[env] Loaded ${applied} entries from ${envPath} (override)`);
34
+ } catch {
35
+ console.log(`[env] ${envPath} not found, using shell env only`);
36
+ }
37
+
38
+ // Log short prefixes (≤6 chars) so an operator can confirm the right
39
+ // value loaded without exposing enough to attempt token reuse if the log
40
+ // ends up shared. PRBOT_PORT and NODE_ENV are not secrets so the full
41
+ // value is fine.
42
+ const prefix = (k: string) =>
43
+ `${k}=${process.env[k] ? process.env[k]!.slice(0, 6) + "…" : "(unset)"}`;
44
+ const literal = (k: string) => `${k}=${process.env[k] ?? "(unset)"}`;
45
+ console.log(
46
+ "[env] " +
47
+ [
48
+ prefix("SLACK_SIGNING_SECRET"),
49
+ prefix("SLACK_BOT_TOKEN"),
50
+ literal("PRBOT_PORT"),
51
+ literal("NODE_ENV"),
52
+ ].join(" | "),
53
+ );
54
+ }
8
55
 
9
- // supports only slack now
10
56
  if (import.meta.main) {
57
+ await loadEnvLocalWithOverride();
11
58
  console.log("Starting ComfyPR Slack Bot...");
12
59
  const client = await (await import("./slack-bot.ts")).startSlackBot();
13
60
  console.log("ComfyPR Slack Bot Done.");