comfy-pr 0.2.26 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +258 -157
- package/bot/IdleWaiter.ts +31 -0
- package/bot/RestartManager.spec.ts +163 -0
- package/bot/RestartManager.ts +190 -0
- package/bot/WorkingTasksManager.spec.ts +154 -0
- package/bot/cli.ts +1171 -0
- package/bot/codesearch-ai-wip.ts +351 -0
- package/bot/error-collector.ts +133 -0
- package/bot/index.ts +14 -0
- package/bot/restart-example.ts +99 -0
- package/bot/slack-bolt.ts +688 -0
- package/bot/slack-bot.ts +1575 -0
- package/bot/state.ts +19 -0
- package/bot/templateLoader.test.ts +84 -0
- package/bot/templateLoader.ts +92 -0
- package/next.config.ts +26 -0
- package/package.json +161 -41
- package/post-summary.ts +44 -0
- package/src/Authors.ts +2 -2
- package/src/CMNodes.ts +3 -3
- package/src/CNRepos.ts +30 -7
- package/src/CRNodes.ts +11 -9
- package/src/CRPulls.ts +3 -0
- package/src/EmailTasks.ts +27 -9
- package/src/FORK_OWNER.ts +3 -1
- package/src/FollowRules.ts +3 -3
- package/src/GithubIssueComments.ts +1 -1
- package/src/Totals.ts +9 -8
- package/src/WorkerInstances.ts +31 -8
- package/src/addCommentAction.ts +19 -11
- package/src/analyzePullsStatus.ts +39 -17
- package/src/analyzeTotals.ts +19 -11
- package/src/bypassRepos.ts +6 -5
- package/src/checkPRsFailures.ts +13 -8
- package/src/cli.test.ts +2 -0
- package/src/cli.ts +1 -1
- package/src/constants.ts +2 -0
- package/src/createComfyRegistryPRsFromCandidates.ts +20 -12
- package/src/createComfyRegistryPullRequests.ts +35 -10
- package/src/createGithubForkForRepo.ts +36 -11
- package/src/createGithubPullRequest.ts +83 -33
- package/src/createIssueComment.ts +4 -4
- package/src/fetchComfyRegistryNodes.ts +34 -4
- package/src/fetchCurrentGeoInfo.ts +3 -1
- package/src/fetchRelatedPullWithComments.ts +1 -1
- package/src/fetchRepoDescriptionMap.ts +1 -4
- package/src/followRuleSchema.ts +10 -4
- package/src/getBranchWorkingDir.ts +3 -7
- package/src/getRepoWorkingDir.ts +2 -3
- package/src/gh.spec.ts +571 -0
- package/src/ghData.ts +13 -0
- package/src/ghPageFlow.ts +33 -0
- package/src/ghSubscriber.ts +76 -0
- package/src/ghUser.ts +28 -4
- package/src/index.ts +13 -4
- package/src/initializeFollowRules.ts +11 -4
- package/src/logger.spec.ts +95 -0
- package/src/logger.ts +46 -0
- package/src/makeGpl3LicenseBranch.ts +66 -0
- package/src/makePublishBranch.ts +21 -15
- package/src/makeTomlBranch.ts +48 -14
- package/src/makeUpdateTomlLicenseBranch.ts +40 -44
- package/src/matchRelatedPulls.ts +9 -4
- package/src/normalizeGithubUrl.spec.ts +131 -0
- package/src/normalizeGithubUrl.ts +64 -0
- package/src/parseIssueUrl.spec.ts +95 -0
- package/src/parseIssueUrl.ts +17 -3
- package/src/parseOwnerRepo.spec.ts +147 -0
- package/src/parseOwnerRepo.ts +8 -5
- package/src/parsePullsState.ts +2 -2
- package/src/parseTitleBodyOfMarkdown.ts +1 -1
- package/src/pickRepoInfo.ts +37 -0
- package/src/postSlackMessage.ts +5 -1
- package/src/preload.ts +30 -27
- package/src/readTemplateTitle.ts +1 -3
- package/src/sendEmailAction.ts +14 -10
- package/src/sendGmail.ts +3 -3
- package/src/updateAuthorsForGithub.ts +53 -35
- package/src/updateAuthorsFromCNRepo.ts +12 -5
- package/src/updateCMNodesDuplicationWarnings.ts +14 -9
- package/src/updateCMRepos.ts +1 -1
- package/src/updateCNRepos.ts +14 -14
- package/src/updateCNReposCRPullsComments.ts +7 -5
- package/src/updateCNReposInfo.ts +49 -40
- package/src/updateCNReposPRCandidate.ts +3 -3
- package/src/updateCNReposPulls.ts +9 -5
- package/src/updateCNReposPullsDashboard.ts +11 -9
- package/src/updateCNReposRelatedPulls.ts +3 -3
- package/src/updateCRNodes.ts +24 -5
- package/src/updateCRRepos.ts +1 -1
- package/src/updateComfyTotals.ts +7 -5
- package/src/updateFollowRuleSet.ts +17 -9
- package/src/updateOutdatedPullsTemplates.ts +61 -48
- package/src/updateSlackMessages.ts +8 -4
- package/tailwind.config.ts +7 -1
- package/next-env.d.ts +0 -5
- package/src/GIT_USEREMAIL.ts +0 -5
- package/src/GIT_USERNAME.ts +0 -9
- package/src/clone_modify_push_Branches.ts +0 -21
- package/src/tomlFillDescription.ts +0 -17
|
@@ -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");
|