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,351 @@
1
+ #!/usr/bin/env bun --watch
2
+ import { anthropic } from "@ai-sdk/anthropic";
3
+ import { openai } from "@ai-sdk/openai";
4
+ import { generateText, stepCountIs, tool } from "ai";
5
+ import yargs from "yargs";
6
+ import { hideBin } from "yargs/helpers";
7
+ import { z } from "zod";
8
+
9
+ const systemPrompt = `You are a helpful ComfyUI expert assistant. You help users find code, understand implementations, and solve problems related to ComfyUI and its ecosystem.
10
+
11
+ ## Your Capabilities
12
+ - Search code across Comfy-Org repositories and 2000+ community custom nodes
13
+ - Explain ComfyUI concepts, nodes, and workflows
14
+ - Help debug issues and find relevant implementations
15
+
16
+ ## Code Search Guidelines
17
+ - When searching for code, use simple descriptive keywords (e.g., "binarization", "mask threshold", "video segmentation")
18
+ - Do NOT invent repository names - leave the repo filter empty to search all repositories
19
+ - Only use repo: filter if user specifically mentions a repository name
20
+ - After getting search results, summarize the most relevant findings with links
21
+
22
+ ## Response Style
23
+ - Be concise and direct, reduce lines where possible
24
+ - Provide code snippets or links when relevant
25
+ - If search returns no results, suggest alternative search terms
26
+ - ALWAYS provide a text summary after using tools - never end with just tool calls
27
+ - Summarize the top 3-5 most relevant results with clickable GitHub links
28
+
29
+ ## Available Repositories (for reference)
30
+ - Comfy-Org/ComfyUI: Main ComfyUI backend (Python)
31
+ - Comfy-Org/ComfyUI_frontend: Frontend (Vue + TypeScript)
32
+ - Comfy-Org/desktop: Desktop application
33
+ - Plus 2000+ community custom node repositories
34
+ `;
35
+
36
+ type ModelRun = { name: string; model: ReturnType<typeof openai> | ReturnType<typeof anthropic> };
37
+
38
+ const availableModels = new Map<string, ModelRun>([
39
+ ["gpt-4o", { name: "gpt-4o", model: openai("gpt-4o") }],
40
+ ["claude-sonnet-4-5", { name: "claude-sonnet-4-5", model: anthropic("claude-sonnet-4-5") }],
41
+ ]);
42
+
43
+ const resolveModels = (names: string[]): ModelRun[] => {
44
+ const requested = names.length > 0 ? names : ["gpt-4o", "claude-sonnet-4-5"];
45
+ const runs = requested.map((name) => {
46
+ const entry = availableModels.get(name);
47
+ if (!entry) {
48
+ throw new Error(`Unknown model: ${name}`);
49
+ }
50
+ return entry;
51
+ });
52
+ return runs;
53
+ };
54
+
55
+ const createTools = (resultLimit: number) => ({
56
+ code_search: tool({
57
+ description:
58
+ "Search for code in Comfy-Org and community custom nodes repositories. " +
59
+ "Supports filters: repo:<owner/repo> and path:<pattern>. " +
60
+ "Examples: 'binarization', 'repo:Comfy-Org/ComfyUI last_node_id', 'path:python auth'",
61
+ inputSchema: z.object({
62
+ query: z.string().describe("Search query with optional repo: and path: filters"),
63
+ repo: z.string().optional().describe("Filter by repository (e.g. Comfy-Org/ComfyUI)"),
64
+ path: z.string().optional().describe("Filter by file path pattern"),
65
+ }),
66
+ execute: async ({ query, repo, path }) => {
67
+ let fullQuery = query;
68
+ if (repo) fullQuery = `repo:${repo} ${fullQuery}`;
69
+ if (path) fullQuery = `path:${path} ${fullQuery}`;
70
+ if (!fullQuery.trim()) {
71
+ return { query: fullQuery, error: "empty query" };
72
+ }
73
+
74
+ const { $ } = await import("bun");
75
+ let result = await $`comfy-codesearch --format json ${{ raw: fullQuery }}`.nothrow();
76
+ let output = result.stdout.toString();
77
+
78
+ if (result.exitCode !== 0 || !output.trim()) {
79
+ const fallback = await $`comfy-codesearch ${{ raw: fullQuery }}`.nothrow();
80
+ if (fallback.stdout.toString().trim()) {
81
+ result = fallback;
82
+ output = fallback.stdout.toString();
83
+ }
84
+ }
85
+
86
+ if (!output.trim()) {
87
+ return {
88
+ query: fullQuery,
89
+ error: result.stderr.toString() || "empty output from comfy-codesearch",
90
+ };
91
+ }
92
+ try {
93
+ const parsed = JSON.parse(output);
94
+ return {
95
+ query: fullQuery,
96
+ resultCount: parsed.results?.length ?? 0,
97
+ results: parsed.results?.slice(0, resultLimit) ?? [],
98
+ };
99
+ } catch {
100
+ // comfy-codesearch returns YAML by default
101
+ const { parse: parseYaml } = await import("yaml");
102
+ try {
103
+ const parsed = parseYaml(output);
104
+ return {
105
+ query: fullQuery,
106
+ resultCount: parsed.results?.length ?? 0,
107
+ results: parsed.results?.slice(0, resultLimit) ?? [],
108
+ };
109
+ } catch {
110
+ return { query: fullQuery, raw: output.slice(0, 2000) };
111
+ }
112
+ }
113
+ },
114
+ }),
115
+ get_weather: tool({
116
+ description: "Get the current weather at a specific location",
117
+ inputSchema: z.object({ location: z.string().describe("The city and state") }),
118
+ execute: async ({ location }) => {
119
+ return { location, temperature: 72, condition: "Sunny" };
120
+ },
121
+ }),
122
+ });
123
+
124
+ type RunSummary = {
125
+ name: string;
126
+ durationMs: number;
127
+ textLength: number;
128
+ toolCallsCount: number;
129
+ toolResultsCount: number;
130
+ urlCount: number;
131
+ error?: string;
132
+ };
133
+
134
+ type CodeMatch = {
135
+ path: string;
136
+ url: string;
137
+ snippet: string;
138
+ };
139
+
140
+ const extractCodeMatches = (toolResults: unknown[]): CodeMatch[] => {
141
+ const seen = new Set<string>();
142
+ const matches: CodeMatch[] = [];
143
+ for (const tr of toolResults) {
144
+ const trObj = tr as Record<string, unknown>;
145
+ const result = (trObj?.result ?? trObj?.output ?? tr ?? {}) as Record<string, unknown>;
146
+ const items = (result?.results ?? []) as unknown[];
147
+ for (const item of items) {
148
+ const itemObj = item as Record<string, unknown>;
149
+ for (const match of (itemObj?.matches ?? []) as unknown[]) {
150
+ const matchObj = match as Record<string, unknown>;
151
+ if (
152
+ typeof matchObj?.url === "string" &&
153
+ matchObj.url.includes("github.com") &&
154
+ !seen.has(matchObj.url)
155
+ ) {
156
+ seen.add(matchObj.url);
157
+ matches.push({
158
+ path: (matchObj?.path as string) ?? "",
159
+ url: matchObj.url,
160
+ snippet: (matchObj?.snippet as string) ?? (matchObj?.content as string) ?? "",
161
+ });
162
+ }
163
+ }
164
+ }
165
+ }
166
+ return matches;
167
+ };
168
+
169
+ const summarizeCodeMatches = async (matches: CodeMatch[]): Promise<Map<string, string>> => {
170
+ if (matches.length === 0) return new Map();
171
+
172
+ const snippetsText = matches
173
+ .map((m, i) => `[${i}] ${m.path}:\n${m.snippet.slice(0, 500)}`)
174
+ .join("\n\n");
175
+
176
+ const { generateText: gen } = await import("ai");
177
+ const result = await gen({
178
+ model: openai("gpt-4o-mini"),
179
+ messages: [
180
+ {
181
+ role: "user",
182
+ content: `Summarize what each code snippet does in 5-10 words. Return one line per snippet in format: [index] summary
183
+
184
+ ${snippetsText}`,
185
+ },
186
+ ],
187
+ maxOutputTokens: 500,
188
+ });
189
+
190
+ const summaryMap = new Map<string, string>();
191
+ for (const line of result.text.split("\n")) {
192
+ const match = line.match(/^\[(\d+)\]\s*(.+)/);
193
+ if (match) {
194
+ const idx = parseInt(match[1], 10);
195
+ if (matches[idx]) {
196
+ summaryMap.set(matches[idx].url, match[2].trim());
197
+ }
198
+ }
199
+ }
200
+ return summaryMap;
201
+ };
202
+
203
+ const formatCodeMatch = (match: CodeMatch, summary: string): string => {
204
+ return `[${match.path} - ${summary}](${match.url})`;
205
+ };
206
+
207
+ const runAi = async (options: {
208
+ query: string;
209
+ models: string[];
210
+ maxSteps: number;
211
+ limit: number;
212
+ }) => {
213
+ const modelRuns = resolveModels(options.models);
214
+ const tools = createTools(options.limit);
215
+ const runSummaries: RunSummary[] = [];
216
+
217
+ console.log(
218
+ `🤖 Asking models (${modelRuns.map((run) => run.name).join(", ")}): ${options.query}`,
219
+ );
220
+ console.log("─".repeat(60));
221
+
222
+ for (const run of modelRuns) {
223
+ const startedAt = Date.now();
224
+ try {
225
+ const result = await generateText({
226
+ model: run.model,
227
+ system: systemPrompt,
228
+ messages: [{ role: "user", content: options.query }],
229
+ // temperature: 0,
230
+ stopWhen: stepCountIs(options.maxSteps),
231
+ tools,
232
+ });
233
+ const durationMs = Date.now() - startedAt;
234
+ const toolResults =
235
+ result.steps?.flatMap(
236
+ (step: unknown) => ((step as Record<string, unknown>).toolResults as unknown[]) ?? [],
237
+ ) ?? [];
238
+ const toolCalls =
239
+ result.steps?.flatMap(
240
+ (step: unknown) => ((step as Record<string, unknown>).toolCalls as unknown[]) ?? [],
241
+ ) ?? [];
242
+ const codeMatches = extractCodeMatches(toolResults);
243
+
244
+ console.log(`\n🤖 Model: ${run.name}`);
245
+ console.log("─".repeat(60));
246
+ if (codeMatches.length > 0) {
247
+ const summaryMap = await summarizeCodeMatches(codeMatches);
248
+ console.log(`\nResults (${codeMatches.length}):`);
249
+ codeMatches.forEach((match) => {
250
+ const summary = summaryMap.get(match.url) || "code snippet";
251
+ console.log(` • ${formatCodeMatch(match, summary)}`);
252
+ });
253
+ }
254
+ if (toolCalls.length > 0) {
255
+ const callList = toolCalls.map((call: unknown) => {
256
+ const callObj = call as Record<string, unknown>;
257
+ const args = callObj.args ? JSON.stringify(callObj.args) : "{}";
258
+ return `${callObj.toolName ?? "tool"}(${args.slice(0, 120)}${args.length > 120 ? "..." : ""})`;
259
+ });
260
+ console.log(`\nTool calls (${toolCalls.length}): ${callList.join(" | ")}`);
261
+ }
262
+
263
+ runSummaries.push({
264
+ name: run.name,
265
+ durationMs,
266
+ textLength: result.text?.length ?? 0,
267
+ toolCallsCount: toolCalls.length,
268
+ toolResultsCount: toolResults.length,
269
+ urlCount: codeMatches.length,
270
+ });
271
+ } catch (error) {
272
+ const durationMs = Date.now() - startedAt;
273
+ const message = error instanceof Error ? error.message : String(error);
274
+ console.log(`\n🤖 Model: ${run.name}`);
275
+ console.log("─".repeat(60));
276
+ console.log("\n⚠️ Error: " + message);
277
+ runSummaries.push({
278
+ name: run.name,
279
+ durationMs,
280
+ textLength: 0,
281
+ toolCallsCount: 0,
282
+ toolResultsCount: 0,
283
+ urlCount: 0,
284
+ error: message,
285
+ });
286
+ }
287
+ }
288
+
289
+ if (runSummaries.length > 0) {
290
+ console.log("\n📊 Model comparison:");
291
+ runSummaries.forEach((summary) => {
292
+ console.log(
293
+ ` • ${summary.name}: urls=${summary.urlCount}, textChars=${summary.textLength}, ` +
294
+ `toolCalls=${summary.toolCallsCount}, toolResults=${summary.toolResultsCount}, ` +
295
+ `timeMs=${summary.durationMs}${summary.error ? " (error)" : ""}`,
296
+ );
297
+ });
298
+
299
+ const ranked = [...runSummaries].sort((a, b) => {
300
+ if (b.urlCount !== a.urlCount) return b.urlCount - a.urlCount;
301
+ if (b.textLength !== a.textLength) return b.textLength - a.textLength;
302
+ return a.durationMs - b.durationMs;
303
+ });
304
+ if (ranked[0]) {
305
+ console.log(`\n🏁 Best overall: ${ranked[0].name}`);
306
+ }
307
+ }
308
+
309
+ console.log("\n" + "─".repeat(60));
310
+ console.log("\n✅ Done!");
311
+ };
312
+
313
+ if (import.meta.main) {
314
+ const argv = await yargs(hideBin(process.argv))
315
+ .command("$0 [query]", "Run ComfyUI assistant", (builder) =>
316
+ builder.positional("query", {
317
+ type: "string",
318
+ default: "binarization for videos in ComfyUI?",
319
+ describe: "Search prompt for the assistant",
320
+ }),
321
+ )
322
+ .option("models", {
323
+ type: "string",
324
+ default: "gpt-4o,claude-sonnet-4-5",
325
+ describe: "Comma-separated model list",
326
+ })
327
+ .option("maxSteps", {
328
+ type: "number",
329
+ default: 20, //
330
+ describe: "Max tool/response steps per model",
331
+ })
332
+ .option("limit", {
333
+ type: "number",
334
+ default: 50,
335
+ describe: "Max results per code search",
336
+ })
337
+ .strict()
338
+ .parse();
339
+
340
+ const modelList = String(argv.models)
341
+ .split(",")
342
+ .map((name) => name.trim())
343
+ .filter(Boolean);
344
+
345
+ await runAi({
346
+ query: String(argv.query),
347
+ models: modelList,
348
+ maxSteps: Number(argv.maxSteps),
349
+ limit: Number(argv.limit),
350
+ });
351
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Error Collector - Monitors child workspace for errors and collects them
3
+ */
4
+
5
+ import { readdir, readFile, appendFile, mkdir } from "fs/promises";
6
+ import { existsSync } from "fs";
7
+ import path from "path";
8
+
9
+ export interface ErrorCollectorOptions {
10
+ workspaceDir: string;
11
+ outputLogPath: string;
12
+ onError?: (errorPath: string, content: string) => void;
13
+ checkInterval?: number; // milliseconds
14
+ }
15
+
16
+ export class ErrorCollector {
17
+ private workspaceDir: string;
18
+ private outputLogPath: string;
19
+ private onError?: (errorPath: string, content: string) => void;
20
+ private checkInterval: number;
21
+ private intervalId?: Timer;
22
+ private processedErrors = new Set<string>();
23
+
24
+ constructor(options: ErrorCollectorOptions) {
25
+ this.workspaceDir = options.workspaceDir;
26
+ this.outputLogPath = options.outputLogPath;
27
+ this.onError = options.onError;
28
+ this.checkInterval = options.checkInterval || 10000; // 10 seconds default
29
+ }
30
+
31
+ async start() {
32
+ // Ensure output directory exists
33
+ await mkdir(path.dirname(this.outputLogPath), { recursive: true });
34
+
35
+ // Initial scan
36
+ await this.scanForErrors();
37
+
38
+ // Periodic scanning
39
+ this.intervalId = setInterval(() => {
40
+ this.scanForErrors().catch((err) => {
41
+ console.error("[ErrorCollector] Scan failed:", err);
42
+ });
43
+ }, this.checkInterval);
44
+ }
45
+
46
+ stop() {
47
+ if (this.intervalId) {
48
+ clearInterval(this.intervalId);
49
+ this.intervalId = undefined;
50
+ }
51
+ }
52
+
53
+ private async scanForErrors() {
54
+ const errorPatterns = [
55
+ "**/ERRORS.md",
56
+ "**/ERROR*.md",
57
+ "**/*-ERRORS.md",
58
+ "**/.logs/*error*.log",
59
+ "**/.logs/*ERROR*.log",
60
+ ];
61
+
62
+ for (const pattern of errorPatterns) {
63
+ await this.findAndProcessErrors(pattern);
64
+ }
65
+ }
66
+
67
+ private async findAndProcessErrors(_pattern: string) {
68
+ try {
69
+ // Simple glob-like search - check common locations
70
+ const logsDir = path.join(this.workspaceDir, ".logs");
71
+ const rootDir = this.workspaceDir;
72
+
73
+ const dirsToCheck = [logsDir, rootDir];
74
+
75
+ for (const dir of dirsToCheck) {
76
+ if (!existsSync(dir)) continue;
77
+
78
+ const files = await readdir(dir).catch(() => []);
79
+
80
+ for (const file of files) {
81
+ const filePath = path.join(dir, file);
82
+
83
+ // Check if file matches error patterns
84
+ const isErrorFile =
85
+ file.includes("ERROR") ||
86
+ file.includes("error") ||
87
+ file.match(/.*-ERRORS?\.md$/i) ||
88
+ file.match(/TOOLS[_-]ERRORS\.md$/i);
89
+
90
+ if (isErrorFile && !this.processedErrors.has(filePath)) {
91
+ await this.processErrorFile(filePath);
92
+ }
93
+ }
94
+ }
95
+ } catch (err) {
96
+ console.error("[ErrorCollector] Error scanning:", err);
97
+ }
98
+ }
99
+
100
+ private async processErrorFile(errorPath: string) {
101
+ try {
102
+ const content = await readFile(errorPath, "utf-8");
103
+
104
+ if (!content.trim()) {
105
+ return; // Skip empty files
106
+ }
107
+
108
+ // Mark as processed
109
+ this.processedErrors.add(errorPath);
110
+
111
+ // Log to output
112
+ const timestamp = new Date().toISOString();
113
+ const relPath = path.relative(this.workspaceDir, errorPath);
114
+ const logEntry = `
115
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
116
+ [${timestamp}] Error found in: ${relPath}
117
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
118
+ ${content}
119
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
120
+
121
+ `;
122
+
123
+ await appendFile(this.outputLogPath, logEntry);
124
+
125
+ // Call callback if provided
126
+ if (this.onError) {
127
+ this.onError(errorPath, content);
128
+ }
129
+ } catch (err) {
130
+ console.error(`[ErrorCollector] Failed to process ${errorPath}:`, err);
131
+ }
132
+ }
133
+ }
package/bot/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * ComfyPR Bot
4
+ *
5
+ * Slack Bot
6
+ * @author snomiao <snomiao@gmail.com>
7
+ */
8
+
9
+ // supports only slack now
10
+ if (import.meta.main) {
11
+ console.log("Starting ComfyPR Slack Bot...");
12
+ const client = await (await import("./slack-bot.ts")).startSlackBot();
13
+ console.log("ComfyPR Slack Bot Done.");
14
+ }
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Example demonstrating the RestartManager
4
+ *
5
+ * This script simulates a bot that:
6
+ * 1. Watches for file changes in the current directory
7
+ * 2. Simulates tasks being added and removed
8
+ * 3. Only restarts when idle (no active tasks)
9
+ *
10
+ * Try editing files in bot/ directory while this is running!
11
+ */
12
+
13
+ import { RestartManager } from "./RestartManager";
14
+
15
+ // Simulate task tracking (like TaskInputFlows in bot/index.ts)
16
+ const activeTasks = new Map<string, unknown>();
17
+
18
+ // Add a task
19
+ function addTask(id: string) {
20
+ console.log(`➕ Task ${id} started`);
21
+ activeTasks.set(id, { id, startTime: Date.now() });
22
+ }
23
+
24
+ // Remove a task
25
+ function removeTask(id: string) {
26
+ console.log(`✅ Task ${id} completed`);
27
+ activeTasks.delete(id);
28
+ }
29
+
30
+ // Check if bot is idle
31
+ function isIdle() {
32
+ return activeTasks.size === 0;
33
+ }
34
+
35
+ // Setup restart manager
36
+ const restartManager = new RestartManager({
37
+ watchPaths: ["bot"],
38
+ isIdle,
39
+ onRestart: () => {
40
+ console.log("\n🔄 Restarting process...\n");
41
+ process.exit(0);
42
+ },
43
+ idleCheckInterval: 2000,
44
+ debounceDelay: 1000,
45
+ logger: {
46
+ info: (msg) => console.log(`[RestartManager] ${msg}`),
47
+ warn: (msg) => console.warn(`[RestartManager] ${msg}`),
48
+ },
49
+ });
50
+
51
+ console.log("🤖 Bot Example Starting...\n");
52
+ console.log("This example demonstrates smart restart behavior:");
53
+ console.log("1. Edit unknown file in bot/ directory");
54
+ console.log("2. The restart will be queued");
55
+ console.log("3. Bot will only restart when idle (no active tasks)\n");
56
+ console.log("Try editing bot/index.ts or bot/RestartManager.ts!\n");
57
+
58
+ // Start watching
59
+ restartManager.start();
60
+
61
+ // Simulate some tasks
62
+ console.log("Simulating task workflow...\n");
63
+
64
+ // Add a task immediately
65
+ addTask("task-1");
66
+
67
+ // Remove it after 5 seconds
68
+ setTimeout(() => {
69
+ removeTask("task-1");
70
+ console.log("\n💤 Bot is now idle - if you edited files, restart will happen now!\n");
71
+ }, 5000);
72
+
73
+ // Add another task after 10 seconds
74
+ setTimeout(() => {
75
+ addTask("task-2");
76
+ console.log("\n📝 New task started - bot is busy again\n");
77
+ }, 10000);
78
+
79
+ // Remove it after 15 seconds
80
+ setTimeout(() => {
81
+ removeTask("task-2");
82
+ console.log("\n💤 Bot is idle again\n");
83
+ }, 15000);
84
+
85
+ // Keep the process running
86
+ setInterval(() => {
87
+ const status = isIdle() ? "💤 IDLE" : "⚙️ BUSY";
88
+ const taskCount = activeTasks.size;
89
+ console.log(`Status: ${status} | Active tasks: ${taskCount}`);
90
+ }, 3000);
91
+
92
+ // Handle graceful shutdown
93
+ process.on("SIGINT", () => {
94
+ console.log("\n\n👋 Shutting down gracefully...");
95
+ restartManager.stop();
96
+ process.exit(0);
97
+ });
98
+
99
+ console.log("Press Ctrl+C to stop\n");