comfy-pr 1.0.1 → 1.1.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 +48 -0
- package/bot/slack-bot.ts +249 -325
- package/package.json +4 -1
package/bot/cli.ts
CHANGED
|
@@ -42,6 +42,9 @@ import { searchNotion } from "@/lib/notion/search";
|
|
|
42
42
|
// Video ability
|
|
43
43
|
import { readVideo } from "@/lib/video/read-video";
|
|
44
44
|
|
|
45
|
+
// Feedback ability
|
|
46
|
+
import { postFeedback, type FeedbackType } from "@/lib/slack/feedback";
|
|
47
|
+
|
|
45
48
|
/**
|
|
46
49
|
* Load environment variables from .env.local in the project root
|
|
47
50
|
* This allows prbot to work from unknown directory
|
|
@@ -1132,6 +1135,43 @@ async function main() {
|
|
|
1132
1135
|
.demandCommand(1, "Please specify a debug subcommand")
|
|
1133
1136
|
.help();
|
|
1134
1137
|
})
|
|
1138
|
+
.command(
|
|
1139
|
+
"feedback",
|
|
1140
|
+
"Submit feedback (bugs, feature requests, errors) to the private #prbot-feedback Slack channel",
|
|
1141
|
+
(y) =>
|
|
1142
|
+
y
|
|
1143
|
+
.option("message", {
|
|
1144
|
+
alias: "m",
|
|
1145
|
+
type: "string",
|
|
1146
|
+
describe: "Feedback message describing the issue or request",
|
|
1147
|
+
demandOption: true,
|
|
1148
|
+
})
|
|
1149
|
+
.option("type", {
|
|
1150
|
+
alias: "t",
|
|
1151
|
+
type: "string",
|
|
1152
|
+
choices: ["bug", "feature", "error", "other"] as const,
|
|
1153
|
+
describe: "Type of feedback",
|
|
1154
|
+
default: "other",
|
|
1155
|
+
})
|
|
1156
|
+
.option("context", {
|
|
1157
|
+
type: "string",
|
|
1158
|
+
describe: "Additional context (error output, command that failed, etc.)",
|
|
1159
|
+
})
|
|
1160
|
+
.option("source", {
|
|
1161
|
+
type: "string",
|
|
1162
|
+
describe: "Who or what is submitting this (e.g. amp-agent, user name)",
|
|
1163
|
+
}),
|
|
1164
|
+
async (args) => {
|
|
1165
|
+
await loadEnvLocal();
|
|
1166
|
+
const ts = await postFeedback({
|
|
1167
|
+
message: args.message as string,
|
|
1168
|
+
type: args.type as FeedbackType,
|
|
1169
|
+
context: args.context as string | undefined,
|
|
1170
|
+
source: args.source as string | undefined,
|
|
1171
|
+
});
|
|
1172
|
+
console.log(`✓ Feedback posted to #prbot-feedback (ts: ${ts})`);
|
|
1173
|
+
},
|
|
1174
|
+
)
|
|
1135
1175
|
.demandCommand(1, "Please specify a command")
|
|
1136
1176
|
.strict()
|
|
1137
1177
|
.help()
|
|
@@ -1157,6 +1197,14 @@ async function main() {
|
|
|
1157
1197
|
" prbot slack download-file -f F123ABC -o ./downloaded.pdf",
|
|
1158
1198
|
" prbot slack file-info -f F123ABC",
|
|
1159
1199
|
" prbot notion search -q 'ComfyUI setup' -l 5",
|
|
1200
|
+
"",
|
|
1201
|
+
"Feedback — report bugs, missing features, or errors (posts to private Slack):",
|
|
1202
|
+
" prbot feedback -m 'Missing pr read command' -t feature",
|
|
1203
|
+
" prbot feedback -m 'search returned 0 for known PRs' -t bug --context '...'",
|
|
1204
|
+
" prbot feedback -m 'ReferenceError: X is not defined' -t error --source agent",
|
|
1205
|
+
"",
|
|
1206
|
+
"AI agents: if a command fails, is missing, or behaves unexpectedly,",
|
|
1207
|
+
"please run `prbot feedback` to report it so we can fix it.",
|
|
1160
1208
|
].join("\n"),
|
|
1161
1209
|
).argv;
|
|
1162
1210
|
|
package/bot/slack-bot.ts
CHANGED
|
@@ -11,9 +11,7 @@ import { yaml } from "@/src/utils/yaml";
|
|
|
11
11
|
import { SocketModeClient } from "@slack/socket-mode";
|
|
12
12
|
import {} from "@slack/bolt";
|
|
13
13
|
import DIE from "@snomiao/die";
|
|
14
|
-
import { spawn } from "node:child_process";
|
|
15
14
|
import { compareBy } from "comparing";
|
|
16
|
-
import { fromStdio } from "from-node-stream";
|
|
17
15
|
import { mkdir } from "fs/promises";
|
|
18
16
|
import sflow from "sflow";
|
|
19
17
|
import winston from "winston";
|
|
@@ -25,16 +23,15 @@ import { parseSlackMessageToMarkdown } from "@/lib/slack/parseSlackMessageToMark
|
|
|
25
23
|
import { slackTsToISO } from "@/lib/slack/slackTsToISO";
|
|
26
24
|
import { safeSlackPostMessage, safeSlackUpdateMessage } from "@/lib/slack/safeSlackMessage";
|
|
27
25
|
import { slackMessageUrlParse } from "@/app/tasks/gh-design/slackMessageUrlParse";
|
|
28
|
-
import { TerminalTextRender } from "terminal-render";
|
|
29
26
|
import minimist from "minimist";
|
|
30
27
|
import { loadClaudeMd, loadSkills } from "./templateLoader";
|
|
31
|
-
import path from "path";
|
|
32
28
|
import { appendFile } from "fs/promises";
|
|
33
29
|
import fsp from "fs/promises";
|
|
34
30
|
import { mdFmt } from "@/app/tasks/gh-desktop-release-notification/upsertSlackMessage";
|
|
35
31
|
import { getSlackChannelName } from "@/lib/slack";
|
|
36
32
|
import { SlackBotState } from "./state";
|
|
37
33
|
import { ErrorCollector } from "./error-collector";
|
|
34
|
+
import { query, type Query, type SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
38
35
|
|
|
39
36
|
export const SLACK_ORG_DOMAIN_NAME = "comfy-organization";
|
|
40
37
|
// Configure winston logger
|
|
@@ -256,10 +253,10 @@ export async function startSlackBot() {
|
|
|
256
253
|
// );
|
|
257
254
|
|
|
258
255
|
const msgs = await fsp
|
|
259
|
-
.readFile("./
|
|
256
|
+
.readFile("./inbox.yaml", "utf-8")
|
|
260
257
|
.then((s) => yaml.parse(s))
|
|
261
258
|
.then((e) => z.object({ missed: z.string().array() }).parseAsync(e));
|
|
262
|
-
await fsp.writeFile("./
|
|
259
|
+
await fsp.writeFile("./inbox.yaml", "missed: []");
|
|
263
260
|
|
|
264
261
|
// clean the file
|
|
265
262
|
//
|
|
@@ -377,7 +374,7 @@ export async function startSlackBot() {
|
|
|
377
374
|
const hasBotMention = text.includes(`<@${botUserId}>`);
|
|
378
375
|
|
|
379
376
|
// Handle DM messages (channel_type: "im") and treat them like app mentions
|
|
380
|
-
const isDM = messageEvent.channel_type === "im";
|
|
377
|
+
const isDM = messageEvent.channel_type === "im" || messageEvent.channel_type === "mpdm";
|
|
381
378
|
|
|
382
379
|
if (
|
|
383
380
|
(isDM || hasBotMention) &&
|
|
@@ -1004,241 +1001,108 @@ IMPORTANT WORKSPACE CONVENTIONS:
|
|
|
1004
1001
|
logger.info(`Spawning agent in ${botWorkingDir} with prompt: ${JSON.stringify(agentPrompt)}`);
|
|
1005
1002
|
// todo: spawn in a worker user
|
|
1006
1003
|
|
|
1007
|
-
//
|
|
1008
|
-
|
|
1009
|
-
// Create dedicated log files for this task (before spawning)
|
|
1004
|
+
// Create dedicated log files for this task
|
|
1010
1005
|
const taskLogDir = `${botWorkingDir}/.logs`;
|
|
1011
1006
|
await mkdir(taskLogDir, { recursive: true });
|
|
1012
|
-
const
|
|
1013
|
-
const stderrLogPath = `${taskLogDir}/claude-yes-stderr.log`;
|
|
1007
|
+
const agentLogPath = `${taskLogDir}/agent-output.log`;
|
|
1014
1008
|
const statusLogPath = `${taskLogDir}/STATUS.txt`;
|
|
1015
1009
|
|
|
1016
|
-
// create a user for task
|
|
1017
|
-
const exitCodePromise = Promise.withResolvers<number | null>();
|
|
1018
|
-
const sh = (() => {
|
|
1019
|
-
// if (process.env.CLI === "amp") {
|
|
1020
|
-
// // use amp
|
|
1021
|
-
// }
|
|
1022
|
-
// const continueArgs: string[] = [];
|
|
1023
|
-
// if (botworkingdir/.claude-yes have content)
|
|
1024
|
-
// then continueArgs.push('--continue')
|
|
1025
|
-
// TODO: maybe use smarter way to detect if need continue
|
|
1026
|
-
// if (existsSync(`${botWorkingDir}/.claude-yes`)) {
|
|
1027
|
-
// // const stat = Bun.statSync(`${botWorkingDir}/.claude-yes`)
|
|
1028
|
-
// // if (stat.isDirectory && stat.size > 0) {
|
|
1029
|
-
// // }
|
|
1030
|
-
// continueArgs.push('--continue')
|
|
1031
|
-
// }
|
|
1032
|
-
// const cmd = `bunx claude-yes -i=1d -- ${Bun.$.escape(agentPrompt)}`;
|
|
1033
|
-
// const cli = cmd.split(" ")[0];
|
|
1034
|
-
const cli = "claude-yes"; // Use the globally installed claude-yes (via bun)
|
|
1035
|
-
// Pass prompt to read PROMPT.txt and TODO.md
|
|
1036
|
-
const args = [
|
|
1037
|
-
"--exit-on-idle=1m",
|
|
1038
|
-
"--",
|
|
1039
|
-
"Please read PROMPT.txt and TODO.md in the current directory and complete all tasks listed there.",
|
|
1040
|
-
];
|
|
1041
|
-
logger.info(
|
|
1042
|
-
`Spawning process: ${cli} ${args.join(" ")} in ${botWorkingDir} with env GH_TOKEN_COMFY_PR_BOT=[REDACTED]`,
|
|
1043
|
-
);
|
|
1044
|
-
const shell = spawn(cli, args, {
|
|
1045
|
-
cwd: botWorkingDir,
|
|
1046
|
-
env: {
|
|
1047
|
-
...process.env,
|
|
1048
|
-
GH_TOKEN: process.env.GH_TOKEN_COMFY_PR_BOT || DIE("missing GH_TOKEN_COMFY_PR_BOT env"),
|
|
1049
|
-
GITHUB_TOKEN: process.env.GH_TOKEN_COMFY_PR_BOT || DIE("missing GH_TOKEN_COMFY_PR_BOT env"),
|
|
1050
|
-
},
|
|
1051
|
-
});
|
|
1052
|
-
|
|
1053
|
-
// check if p spawned successfully
|
|
1054
|
-
shell.on("error", (err) => {
|
|
1055
|
-
logger.error(`Failed to start ${cli} process for task ${workspaceId}:`, { err });
|
|
1056
|
-
});
|
|
1057
|
-
shell.on("exit", (code, signal) => {
|
|
1058
|
-
logger.info(`process for task ${workspaceId} exited with code ${code} and signal ${signal}`);
|
|
1059
|
-
exitCodePromise.resolve(code);
|
|
1060
|
-
});
|
|
1061
|
-
|
|
1062
|
-
// Auto-answer the trust prompt (option 1 = "Yes, proceed")
|
|
1063
|
-
if (shell.stdin) {
|
|
1064
|
-
shell.stdin.write("1\n");
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
// Stream stderr to log file and logger (async operations moved outside)
|
|
1068
|
-
shell.stderr?.on("data", (data) => {
|
|
1069
|
-
const text = data.toString();
|
|
1070
|
-
appendFile(stderrLogPath, text).catch(() => {});
|
|
1071
|
-
logger.warn(`[${cli} stderr]:`, { data: text });
|
|
1072
|
-
});
|
|
1073
|
-
|
|
1074
|
-
// Check if stdout/stderr are available
|
|
1075
|
-
if (!shell.stdout) {
|
|
1076
|
-
logger.error(`Process ${cli} has no stdout stream!`);
|
|
1077
|
-
}
|
|
1078
|
-
if (!shell.stderr) {
|
|
1079
|
-
logger.warn(`Process ${cli} has no stderr stream`);
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
return shell;
|
|
1083
|
-
})();
|
|
1084
|
-
|
|
1085
|
-
// Write initial status
|
|
1086
|
-
await Bun.write(
|
|
1087
|
-
statusLogPath,
|
|
1088
|
-
`Started: ${new Date().toISOString()}\nPID: ${sh.pid}\nStatus: Running\nLog: ${stdoutLogPath}\n`,
|
|
1089
|
-
);
|
|
1090
|
-
|
|
1091
1010
|
const isDebugMode = process.env.DEBUG === "true" || process.env.DEBUG === "1";
|
|
1092
1011
|
|
|
1093
|
-
logger.info(`Spawned claude-yes process with PID ${sh.pid} for task ${workspaceId}`);
|
|
1094
|
-
if (isDebugMode) {
|
|
1095
|
-
logger.info(`📝 Real-time logs: tail -f ${stdoutLogPath}`);
|
|
1096
|
-
logger.info(`📊 Status file: cat ${statusLogPath}`);
|
|
1097
|
-
logger.info(`💡 Debug commands: prbot debug watch ${botWorkingDir}`);
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
1012
|
// Start error collector to monitor workspace for errors
|
|
1101
1013
|
const errorLogPath = `${taskLogDir}/COLLECTED_ERRORS.md`;
|
|
1102
1014
|
const errorCollector = new ErrorCollector({
|
|
1103
1015
|
workspaceDir: botWorkingDir,
|
|
1104
1016
|
outputLogPath: errorLogPath,
|
|
1105
1017
|
onError: isDebugMode
|
|
1106
|
-
? (errorPath, content) => {
|
|
1107
|
-
logger.warn(
|
|
1018
|
+
? (errorPath: string, content: string) => {
|
|
1019
|
+
logger.warn(`Error detected in workspace: ${errorPath}`);
|
|
1108
1020
|
logger.warn(`Error content preview: ${content.substring(0, 500)}...`);
|
|
1109
1021
|
}
|
|
1110
1022
|
: undefined,
|
|
1111
|
-
checkInterval: 10000,
|
|
1023
|
+
checkInterval: 10000,
|
|
1112
1024
|
});
|
|
1113
1025
|
await errorCollector.start();
|
|
1114
|
-
if (isDebugMode) {
|
|
1115
|
-
logger.info(`🔍 Error collector started, errors will be logged to: ${errorLogPath}`);
|
|
1116
|
-
}
|
|
1117
1026
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1027
|
+
// --- Claude Agent SDK ---
|
|
1028
|
+
logger.info(
|
|
1029
|
+
`Spawning agent via SDK in ${botWorkingDir} with env GH_TOKEN_COMFY_PR_BOT=[REDACTED]`,
|
|
1030
|
+
);
|
|
1031
|
+
|
|
1032
|
+
const sdkPrompt =
|
|
1033
|
+
"Please read PROMPT.txt and TODO.md in the current directory and complete all tasks listed there.";
|
|
1034
|
+
|
|
1035
|
+
const abortController = new AbortController();
|
|
1036
|
+
|
|
1037
|
+
// Handle follow-up messages: when user sends more messages in the thread,
|
|
1038
|
+
// pipe them to the running agent via streamInput
|
|
1039
|
+
let agentQuery: Query | null = null;
|
|
1040
|
+
|
|
1041
|
+
// Drain taskInputFlow into the SDK agent
|
|
1042
|
+
const inputDrainPromise = (async () => {
|
|
1043
|
+
const reader = taskInputFlow.readable.getReader();
|
|
1044
|
+
try {
|
|
1045
|
+
while (true) {
|
|
1046
|
+
const { done, value } = await reader.read();
|
|
1047
|
+
if (done) break;
|
|
1048
|
+
if (value && agentQuery !== null) {
|
|
1049
|
+
const userMsg: SDKUserMessage = {
|
|
1050
|
+
type: "user" as const,
|
|
1051
|
+
message: { role: "user" as const, content: value },
|
|
1052
|
+
parent_tool_use_id: null,
|
|
1053
|
+
session_id: "",
|
|
1054
|
+
};
|
|
1055
|
+
await (agentQuery as Query).streamInput(
|
|
1056
|
+
(async function* () {
|
|
1057
|
+
yield userMsg;
|
|
1058
|
+
})(),
|
|
1059
|
+
);
|
|
1060
|
+
logger.info(`Injected follow-up message into SDK agent: ${value.slice(0, 100)}`);
|
|
1061
|
+
}
|
|
1133
1062
|
}
|
|
1134
|
-
|
|
1135
|
-
//
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
})
|
|
1063
|
+
} catch {
|
|
1064
|
+
// taskInputFlow closed
|
|
1065
|
+
}
|
|
1066
|
+
})();
|
|
1139
1067
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
async (chunk) => await appendFile(`${botWorkingDir}/.logs/bot-${logDate}.log`, chunk),
|
|
1146
|
-
);
|
|
1147
|
-
})
|
|
1148
|
-
// show loading icon when unknown output activity, and remove the loading icon after idle for 5s
|
|
1149
|
-
.forkTo(async (e) => {
|
|
1150
|
-
const idleWaiter = new IdleWaiter();
|
|
1151
|
-
let isThinking = false;
|
|
1152
|
-
return await e
|
|
1153
|
-
.forEach(async () => {
|
|
1154
|
-
idleWaiter.ping();
|
|
1155
|
-
if (!isThinking && quickRespondMsg.ts && quickRespondMsg.channel) {
|
|
1156
|
-
isThinking = true;
|
|
1157
|
-
const msgChannel = quickRespondMsg.channel;
|
|
1158
|
-
const msgTs = quickRespondMsg.ts;
|
|
1159
|
-
await slack.reactions
|
|
1160
|
-
.add({
|
|
1161
|
-
name: "loading",
|
|
1162
|
-
channel: msgChannel,
|
|
1163
|
-
timestamp: msgTs,
|
|
1164
|
-
})
|
|
1165
|
-
.catch(() => {});
|
|
1166
|
-
idleWaiter.wait(5e3).finally(async () => {
|
|
1167
|
-
await slack.reactions
|
|
1168
|
-
.remove({
|
|
1169
|
-
name: "loading",
|
|
1170
|
-
channel: msgChannel,
|
|
1171
|
-
timestamp: msgTs,
|
|
1172
|
-
})
|
|
1173
|
-
.catch(() => {});
|
|
1174
|
-
isThinking = false;
|
|
1175
|
-
});
|
|
1176
|
-
}
|
|
1177
|
-
})
|
|
1178
|
-
.onFlush(async () => {
|
|
1179
|
-
// remove loading icon
|
|
1180
|
-
if (isThinking && quickRespondMsg.ts && quickRespondMsg.channel) {
|
|
1181
|
-
isThinking = false;
|
|
1182
|
-
await slack.reactions
|
|
1183
|
-
.remove({
|
|
1184
|
-
name: "loading",
|
|
1185
|
-
channel: quickRespondMsg.channel,
|
|
1186
|
-
timestamp: quickRespondMsg.ts,
|
|
1187
|
-
})
|
|
1188
|
-
.catch(() => {});
|
|
1189
|
-
}
|
|
1190
|
-
})
|
|
1191
|
-
.run();
|
|
1192
|
-
})
|
|
1068
|
+
// Track agent output for Slack updates
|
|
1069
|
+
let agentOutput = "";
|
|
1070
|
+
let lastSentOutput = "";
|
|
1071
|
+
const idleWaiter = new IdleWaiter();
|
|
1072
|
+
let isThinking = false;
|
|
1193
1073
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
// logger.info('Rendered chunk size:', rendered.length, 'lines: ', rendered.split(/\r|\n/).length);
|
|
1201
|
-
const id = setInterval(async () => {
|
|
1202
|
-
const renderedText = tr.render();
|
|
1203
|
-
// diff from last, and send stable lines
|
|
1204
|
-
const common = commonPrefix(renderedText, ...lastOutputs);
|
|
1205
|
-
const newStable = renderedText.slice(0, common.length);
|
|
1206
|
-
// logger.debug({ common, newStable, lastOutputs, renderedText });
|
|
1207
|
-
|
|
1208
|
-
if (newStable !== sent) {
|
|
1209
|
-
const news = newStable.slice(sent.length);
|
|
1210
|
-
sent = newStable; // agent outputs have new lines to send
|
|
1211
|
-
if (news) logger.debug(JSON.stringify({ news }));
|
|
1212
|
-
logger.info(
|
|
1213
|
-
`New stable output detected, length: ${newStable.length}, news length: ${news.length}`,
|
|
1214
|
-
);
|
|
1074
|
+
// Slack update logic — extracted so it can be called from interval and finally
|
|
1075
|
+
let lastSlackUpdateTime = 0;
|
|
1076
|
+
const MIN_SLACK_UPDATE_INTERVAL_MS = 10_000; // minimum 10s between LLM-synthesized updates
|
|
1077
|
+
const sendSlackUpdate = async () => {
|
|
1078
|
+
if (agentOutput === lastSentOutput || !agentOutput) return;
|
|
1215
1079
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
logger.debug(
|
|
1220
|
-
"Raw terminal output (before cleaning): " +
|
|
1221
|
-
yaml.stringify({ preview: rawTerminalOutput.slice(0, 200) }),
|
|
1222
|
-
);
|
|
1223
|
-
logger.info(
|
|
1224
|
-
"Cleaned output preview: " +
|
|
1225
|
-
yaml.stringify({
|
|
1226
|
-
preview: my_internal_thoughts.slice(0, 200),
|
|
1227
|
-
news_preview: news.slice(0, 200),
|
|
1228
|
-
}),
|
|
1229
|
-
);
|
|
1080
|
+
const now = Date.now();
|
|
1081
|
+
if (now - lastSlackUpdateTime < MIN_SLACK_UPDATE_INTERVAL_MS) return;
|
|
1082
|
+
lastSlackUpdateTime = now;
|
|
1230
1083
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1084
|
+
const news = agentOutput.slice(lastSentOutput.length);
|
|
1085
|
+
lastSentOutput = agentOutput;
|
|
1086
|
+
|
|
1087
|
+
const my_internal_thoughts = agentOutput.split("\n").slice(-80).join("\n");
|
|
1088
|
+
logger.info(
|
|
1089
|
+
"Agent output preview: " +
|
|
1090
|
+
yaml.stringify({
|
|
1091
|
+
preview: my_internal_thoughts.slice(0, 200),
|
|
1092
|
+
news_preview: news.slice(0, 200),
|
|
1093
|
+
}),
|
|
1094
|
+
);
|
|
1095
|
+
|
|
1096
|
+
// GPT-4o synthesis for Slack update
|
|
1097
|
+
const contexts = {
|
|
1098
|
+
my_internal_thoughts,
|
|
1099
|
+
news,
|
|
1100
|
+
user_original_intent: resp.user_intent,
|
|
1101
|
+
my_response_md_original: quickRespondMsg.text || "",
|
|
1102
|
+
};
|
|
1103
|
+
const updateResponseResp = (await zChatCompletion({
|
|
1104
|
+
my_response_md_updated: z.string(),
|
|
1105
|
+
})`
|
|
1242
1106
|
TASK: Update my my_response_md_original based on agent's my_internal_thoughts findings, and give me my_response_md_updated to post in slack.
|
|
1243
1107
|
|
|
1244
1108
|
RULES:
|
|
@@ -1288,127 +1152,187 @@ ${yaml.stringify(contexts)}
|
|
|
1288
1152
|
|
|
1289
1153
|
`) as { my_response_md_updated: string };
|
|
1290
1154
|
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
);
|
|
1155
|
+
// Log raw response
|
|
1156
|
+
await appendFile(
|
|
1157
|
+
".logs/my_response_md_updated.jsonl",
|
|
1158
|
+
JSON.stringify({
|
|
1159
|
+
timestamp: new Date().toISOString(),
|
|
1160
|
+
workspaceId,
|
|
1161
|
+
stage: "raw_from_claude",
|
|
1162
|
+
my_response_md_updated_raw: updateResponseResp.my_response_md_updated,
|
|
1163
|
+
my_internal_thoughts_preview: my_internal_thoughts.slice(0, 500),
|
|
1164
|
+
my_response_md_original: quickRespondMsg.text || "",
|
|
1165
|
+
}) + "\n",
|
|
1166
|
+
).catch(() => {});
|
|
1167
|
+
|
|
1168
|
+
const updated_response_full = await mdFmt(
|
|
1169
|
+
updateResponseResp.my_response_md_updated
|
|
1170
|
+
.trim()
|
|
1171
|
+
.replace(/^__NOTHING_CHANGED__$/m, quickRespondMsg.text || ""),
|
|
1172
|
+
);
|
|
1310
1173
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
JSON.stringify(finalLogEntry) + "\n",
|
|
1331
|
-
).catch(() => {});
|
|
1332
|
-
|
|
1333
|
-
if (quickRespondMsg.ts && quickRespondMsg.channel) {
|
|
1334
|
-
await safeSlackUpdateMessage(slack, {
|
|
1335
|
-
channel: quickRespondMsg.channel,
|
|
1336
|
-
ts: quickRespondMsg.ts,
|
|
1337
|
-
text: my_response_md_updated, // Fallback text for notifications
|
|
1338
|
-
blocks: [
|
|
1339
|
-
{
|
|
1340
|
-
type: "markdown",
|
|
1341
|
-
text: my_response_md_updated,
|
|
1342
|
-
},
|
|
1343
|
-
],
|
|
1344
|
-
});
|
|
1345
|
-
logger.debug("Updated quick respond message in slack:", {
|
|
1346
|
-
url: `https://${event.team}.slack.com/archives/${quickRespondMsg.channel}/p${quickRespondMsg.ts.replace(".", "")}`,
|
|
1347
|
-
});
|
|
1174
|
+
// Truncate to 4000 chars from the middle
|
|
1175
|
+
const my_response_md_updated =
|
|
1176
|
+
updated_response_full.length > 4000
|
|
1177
|
+
? updated_response_full.slice(0, 2000) +
|
|
1178
|
+
"\n\n...TRUNCATED...\n\n" +
|
|
1179
|
+
updated_response_full.slice(-2000)
|
|
1180
|
+
: updated_response_full;
|
|
1181
|
+
|
|
1182
|
+
await appendFile(
|
|
1183
|
+
".logs/my_response_md_updated.jsonl",
|
|
1184
|
+
JSON.stringify({
|
|
1185
|
+
timestamp: new Date().toISOString(),
|
|
1186
|
+
workspaceId,
|
|
1187
|
+
stage: "final_processed",
|
|
1188
|
+
my_response_md_updated_final: my_response_md_updated,
|
|
1189
|
+
was_truncated: updated_response_full.length > 4000,
|
|
1190
|
+
original_length: updated_response_full.length,
|
|
1191
|
+
}) + "\n",
|
|
1192
|
+
).catch(() => {});
|
|
1348
1193
|
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1194
|
+
if (quickRespondMsg.ts && quickRespondMsg.channel) {
|
|
1195
|
+
await safeSlackUpdateMessage(slack, {
|
|
1196
|
+
channel: quickRespondMsg.channel,
|
|
1197
|
+
ts: quickRespondMsg.ts,
|
|
1198
|
+
text: my_response_md_updated,
|
|
1199
|
+
blocks: [{ type: "markdown", text: my_response_md_updated }],
|
|
1200
|
+
});
|
|
1201
|
+
quickRespondMsg.text = my_response_md_updated;
|
|
1202
|
+
await SlackBotState.set(`task-quick-respond-msg-${eventId}`, {
|
|
1203
|
+
ts: quickRespondMsg.ts,
|
|
1204
|
+
text: quickRespondMsg.text,
|
|
1205
|
+
channel: event.channel,
|
|
1206
|
+
url: `https://${SLACK_ORG_DOMAIN_NAME}.slack.com/archives/${event.channel}/p${quickRespondMsg.ts.replace(".", "")}`,
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1359
1210
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1211
|
+
// Periodic Slack update interval
|
|
1212
|
+
const slackUpdateInterval = setInterval(sendSlackUpdate, 10e3);
|
|
1213
|
+
|
|
1214
|
+
// Run the agent
|
|
1215
|
+
let exitCode: number | null = 0;
|
|
1216
|
+
try {
|
|
1217
|
+
agentQuery = query({
|
|
1218
|
+
prompt: sdkPrompt,
|
|
1219
|
+
options: {
|
|
1220
|
+
cwd: botWorkingDir,
|
|
1221
|
+
permissionMode: "bypassPermissions",
|
|
1222
|
+
allowDangerouslySkipPermissions: true,
|
|
1223
|
+
settingSources: ["project"], // loads CLAUDE.md from cwd
|
|
1224
|
+
maxTurns: 200,
|
|
1225
|
+
persistSession: false,
|
|
1226
|
+
abortController,
|
|
1227
|
+
env: {
|
|
1228
|
+
...process.env,
|
|
1229
|
+
GH_TOKEN: process.env.GH_TOKEN_COMFY_PR_BOT || DIE("missing GH_TOKEN_COMFY_PR_BOT env"),
|
|
1230
|
+
GITHUB_TOKEN:
|
|
1231
|
+
process.env.GH_TOKEN_COMFY_PR_BOT || DIE("missing GH_TOKEN_COMFY_PR_BOT env"),
|
|
1232
|
+
},
|
|
1233
|
+
stderr: (data: string) => {
|
|
1234
|
+
logger.warn(`[agent stderr]: ${data}`);
|
|
1235
|
+
},
|
|
1236
|
+
},
|
|
1237
|
+
});
|
|
1365
1238
|
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
})
|
|
1239
|
+
await Bun.write(
|
|
1240
|
+
statusLogPath,
|
|
1241
|
+
`Started: ${new Date().toISOString()}\nStatus: Running (SDK)\nLog: ${agentLogPath}\n`,
|
|
1242
|
+
);
|
|
1243
|
+
|
|
1244
|
+
for await (const message of agentQuery) {
|
|
1245
|
+
// Loading indicator management
|
|
1246
|
+
idleWaiter.ping();
|
|
1247
|
+
if (!isThinking && quickRespondMsg.ts && quickRespondMsg.channel) {
|
|
1248
|
+
isThinking = true;
|
|
1249
|
+
const msgChannel = quickRespondMsg.channel;
|
|
1250
|
+
const msgTs = quickRespondMsg.ts;
|
|
1251
|
+
slack.reactions
|
|
1252
|
+
.add({ name: "loading", channel: msgChannel, timestamp: msgTs })
|
|
1253
|
+
.catch(() => {});
|
|
1254
|
+
idleWaiter.wait(5e3).finally(async () => {
|
|
1255
|
+
await slack.reactions
|
|
1256
|
+
.remove({ name: "loading", channel: msgChannel, timestamp: msgTs })
|
|
1257
|
+
.catch(() => {});
|
|
1258
|
+
isThinking = false;
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// Process SDK messages
|
|
1263
|
+
if (message.type === "assistant") {
|
|
1264
|
+
const textBlocks = (message.message.content as Array<{ type: string; text?: string }>)
|
|
1265
|
+
.filter(
|
|
1266
|
+
(block): block is { type: "text"; text: string } =>
|
|
1267
|
+
block.type === "text" && typeof block.text === "string",
|
|
1268
|
+
)
|
|
1269
|
+
.map((block) => block.text);
|
|
1270
|
+
if (textBlocks.length > 0) {
|
|
1271
|
+
const text = textBlocks.join("\n");
|
|
1272
|
+
agentOutput += text + "\n";
|
|
1273
|
+
await appendFile(agentLogPath, text + "\n").catch(() => {});
|
|
1274
|
+
logger.debug(`Agent assistant text (${text.length} chars): ${text.slice(0, 200)}`);
|
|
1275
|
+
}
|
|
1276
|
+
} else if (message.type === "result") {
|
|
1277
|
+
if (message.subtype === "success") {
|
|
1278
|
+
exitCode = 0;
|
|
1279
|
+
logger.info(
|
|
1280
|
+
`Agent completed successfully. Turns: ${message.num_turns}, Cost: $${message.total_cost_usd.toFixed(4)}, Duration: ${(message.duration_ms / 1000).toFixed(1)}s`,
|
|
1281
|
+
);
|
|
1282
|
+
// Append final result to output for last Slack update
|
|
1283
|
+
if ("result" in message && message.result) {
|
|
1284
|
+
agentOutput += "\n" + message.result;
|
|
1384
1285
|
}
|
|
1286
|
+
} else {
|
|
1287
|
+
exitCode = 1;
|
|
1288
|
+
const errors = "errors" in message ? (message as { errors: string[] }).errors : [];
|
|
1289
|
+
logger.error(
|
|
1290
|
+
`Agent failed (${message.subtype}). Turns: ${message.num_turns}, Errors: ${errors.join(", ")}`,
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
await appendFile(
|
|
1294
|
+
agentLogPath,
|
|
1295
|
+
`\n--- Result: ${message.subtype} | Turns: ${message.num_turns} | Cost: $${message.total_cost_usd.toFixed(4)} ---\n`,
|
|
1296
|
+
).catch(() => {});
|
|
1297
|
+
} else {
|
|
1298
|
+
// Log other message types for debugging
|
|
1299
|
+
logger.debug(
|
|
1300
|
+
`SDK message: ${message.type}${"subtype" in message ? `.${(message as { subtype: string }).subtype}` : ""}`,
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
} catch (err) {
|
|
1305
|
+
exitCode = 1;
|
|
1306
|
+
logger.error("Agent SDK error:", { err });
|
|
1307
|
+
} finally {
|
|
1308
|
+
clearInterval(slackUpdateInterval);
|
|
1309
|
+
// Remove loading icon if still showing
|
|
1310
|
+
if (isThinking && quickRespondMsg.ts && quickRespondMsg.channel) {
|
|
1311
|
+
await slack.reactions
|
|
1312
|
+
.remove({
|
|
1313
|
+
name: "loading",
|
|
1314
|
+
channel: quickRespondMsg.channel,
|
|
1315
|
+
timestamp: quickRespondMsg.ts,
|
|
1385
1316
|
})
|
|
1386
|
-
.
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
//
|
|
1392
|
-
|
|
1393
|
-
|
|
1317
|
+
.catch(() => {});
|
|
1318
|
+
}
|
|
1319
|
+
// Send one final Slack update with complete output
|
|
1320
|
+
lastSlackUpdateTime = 0; // bypass throttle for final update
|
|
1321
|
+
await sendSlackUpdate().catch((err) => logger.error("Final Slack update error:", { err }));
|
|
1322
|
+
// Cancel input drain
|
|
1323
|
+
abortController.abort();
|
|
1324
|
+
}
|
|
1394
1325
|
|
|
1395
1326
|
TaskInputFlows.delete(workspaceId);
|
|
1396
1327
|
|
|
1397
1328
|
// Stop error collector
|
|
1398
1329
|
errorCollector.stop();
|
|
1399
|
-
if (isDebugMode) {
|
|
1400
|
-
logger.info(`🔍 Error collector stopped for task ${workspaceId}`);
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
// check exit code, checkmark if claude-yes exited 0, cross if not
|
|
1404
|
-
|
|
1405
|
-
const exitCode = await exitCodePromise.promise;
|
|
1406
1330
|
|
|
1407
|
-
//
|
|
1331
|
+
// Final status
|
|
1408
1332
|
const finalStatus = exitCode === 0 ? "Completed Successfully" : `Failed (exit code ${exitCode})`;
|
|
1409
1333
|
await Bun.write(
|
|
1410
1334
|
statusLogPath,
|
|
1411
|
-
`
|
|
1335
|
+
`Status: ${finalStatus}\nEnded: ${new Date().toISOString()}\nErrors: ${errorLogPath}\n`,
|
|
1412
1336
|
).catch(() => {});
|
|
1413
1337
|
|
|
1414
1338
|
if (exitCode !== 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "comfy-pr",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Make PRs that publishes ComfyUI Custom Nodes to [ComfyUI Registry]( https://registry.comfy.org/ ).",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"license": "ISC",
|
|
@@ -49,6 +49,8 @@
|
|
|
49
49
|
"release": "npx semantic-release",
|
|
50
50
|
"start": "next start",
|
|
51
51
|
"test": "bun test",
|
|
52
|
+
"subtree:pull": "bash scripts/subtree-sync.sh pull",
|
|
53
|
+
"subtree:push": "bash scripts/subtree-sync.sh push",
|
|
52
54
|
"test:watch": "bun test --watch",
|
|
53
55
|
"vercel:build": "vercel build",
|
|
54
56
|
"vercel:dev": "vercel dev"
|
|
@@ -56,6 +58,7 @@
|
|
|
56
58
|
"dependencies": {
|
|
57
59
|
"@ai-sdk/anthropic": "^3.0.23",
|
|
58
60
|
"@ai-sdk/openai": "^3.0.19",
|
|
61
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.85",
|
|
59
62
|
"@auth/mongodb-adapter": "^3.4.2",
|
|
60
63
|
"@ctrl/mac-address": "^3.0.3",
|
|
61
64
|
"@google/generative-ai": "^0.24.1",
|