fireqa-agent 0.1.4 → 0.1.6
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/dist/cli.js +69 -0
- package/dist/runner/output-parser.js +11 -7
- package/dist/runner/task-poller.js +13 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,11 @@ import { Command } from "commander";
|
|
|
3
3
|
import { ConfigStore } from "./config/store.js";
|
|
4
4
|
import { loginWithApiKey } from "./auth/api-key.js";
|
|
5
5
|
import { CLI_ADAPTERS } from "./runner/adapters.js";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import os from "os";
|
|
9
|
+
import { spawn } from "child_process";
|
|
10
|
+
const PID_FILE = path.join(os.homedir(), ".fireqa", "agent.pid");
|
|
6
11
|
const program = new Command();
|
|
7
12
|
const store = new ConfigStore();
|
|
8
13
|
program
|
|
@@ -51,7 +56,71 @@ program
|
|
|
51
56
|
.action(async (options) => {
|
|
52
57
|
const cliType = Object.keys(CLI_ADAPTERS).find(v => v === options.cliType) ?? "claude";
|
|
53
58
|
store.save({ cliType, cli: CLI_ADAPTERS[cliType].defaultCommand });
|
|
59
|
+
// PID 기록
|
|
60
|
+
const pidDir = path.dirname(PID_FILE);
|
|
61
|
+
if (!fs.existsSync(pidDir))
|
|
62
|
+
fs.mkdirSync(pidDir, { recursive: true });
|
|
63
|
+
fs.writeFileSync(PID_FILE, String(process.pid));
|
|
64
|
+
// 종료 시 PID 파일 삭제
|
|
65
|
+
const cleanupPid = () => { try {
|
|
66
|
+
fs.unlinkSync(PID_FILE);
|
|
67
|
+
}
|
|
68
|
+
catch { } };
|
|
69
|
+
process.on("exit", cleanupPid);
|
|
70
|
+
process.on("SIGINT", () => { cleanupPid(); process.exit(0); });
|
|
71
|
+
process.on("SIGTERM", () => { cleanupPid(); process.exit(0); });
|
|
54
72
|
const { startAgent } = await import("./runner/task-poller.js");
|
|
55
73
|
await startAgent(store);
|
|
56
74
|
});
|
|
75
|
+
program
|
|
76
|
+
.command("stop")
|
|
77
|
+
.description("실행 중인 에이전트 종료")
|
|
78
|
+
.action(() => {
|
|
79
|
+
if (!fs.existsSync(PID_FILE)) {
|
|
80
|
+
console.log("실행 중인 에이전트가 없습니다.");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
84
|
+
try {
|
|
85
|
+
process.kill(pid, "SIGTERM");
|
|
86
|
+
fs.unlinkSync(PID_FILE);
|
|
87
|
+
console.log(`에이전트 종료됨 (PID: ${pid})`);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
console.log("에이전트가 이미 종료되어 있습니다.");
|
|
91
|
+
try {
|
|
92
|
+
fs.unlinkSync(PID_FILE);
|
|
93
|
+
}
|
|
94
|
+
catch { }
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
program
|
|
98
|
+
.command("restart")
|
|
99
|
+
.description("에이전트 재시작 (최신 버전으로)")
|
|
100
|
+
.option("--cli-type <type>", "사용할 LLM CLI 타입 (claude | codex | gemini)")
|
|
101
|
+
.action((options) => {
|
|
102
|
+
// 실행 중인 프로세스 종료
|
|
103
|
+
if (fs.existsSync(PID_FILE)) {
|
|
104
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
105
|
+
try {
|
|
106
|
+
process.kill(pid, "SIGTERM");
|
|
107
|
+
try {
|
|
108
|
+
fs.unlinkSync(PID_FILE);
|
|
109
|
+
}
|
|
110
|
+
catch { }
|
|
111
|
+
console.log(`기존 에이전트 종료됨 (PID: ${pid})`);
|
|
112
|
+
}
|
|
113
|
+
catch { }
|
|
114
|
+
}
|
|
115
|
+
// 새 프로세스로 재시작
|
|
116
|
+
const args = ["fireqa-agent@latest", "start"];
|
|
117
|
+
if (options.cliType)
|
|
118
|
+
args.push("--cli-type", options.cliType);
|
|
119
|
+
const child = spawn("npx", args, { stdio: "inherit", detached: false });
|
|
120
|
+
child.on("error", (err) => {
|
|
121
|
+
console.error(`재시작 실패: ${err.message}`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
});
|
|
124
|
+
child.on("close", (code) => process.exit(code ?? 0));
|
|
125
|
+
});
|
|
57
126
|
program.parse();
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
export function parseStreamJsonLine(line) {
|
|
2
2
|
try {
|
|
3
3
|
const data = JSON.parse(line);
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
// Claude stream-json format: {"type":"assistant","message":{"content":[{"type":"text","text":"..."}]}}
|
|
5
|
+
if (data.type === "assistant" && data.message?.content) {
|
|
6
|
+
const content = data.message.content;
|
|
7
|
+
for (const block of content) {
|
|
8
|
+
if (block.type === "text" && block.text) {
|
|
9
|
+
return { type: "text", content: block.text };
|
|
10
|
+
}
|
|
11
|
+
if (block.type === "tool_use" && block.name) {
|
|
12
|
+
return { type: "tool_use", content: block.name, tool: block.name };
|
|
13
|
+
}
|
|
10
14
|
}
|
|
11
15
|
}
|
|
12
16
|
if (data.type === "tool_result") {
|
|
@@ -16,7 +20,7 @@ export function parseStreamJsonLine(line) {
|
|
|
16
20
|
return {
|
|
17
21
|
type: "text",
|
|
18
22
|
content: String(data.result ?? ""),
|
|
19
|
-
sessionId: data.session_id ?? undefined,
|
|
23
|
+
sessionId: data.session_id ?? undefined,
|
|
20
24
|
};
|
|
21
25
|
}
|
|
22
26
|
return null;
|
|
@@ -96,10 +96,20 @@ export async function startAgent(store) {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
-
catch {
|
|
99
|
+
catch (err) {
|
|
100
100
|
heartbeatFailures++;
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
102
|
+
if (heartbeatFailures === 1) {
|
|
103
|
+
console.warn(`heartbeat 실패: ${msg}`);
|
|
104
|
+
}
|
|
105
|
+
else if (heartbeatFailures > 3) {
|
|
106
|
+
console.warn(`heartbeat 연속 실패 (${heartbeatFailures}회): ${msg}`);
|
|
107
|
+
}
|
|
108
|
+
// 404: 서버에서 연결이 삭제됨 → 재시작 필요
|
|
109
|
+
if (msg.includes("404") && heartbeatFailures === 1) {
|
|
110
|
+
console.error("서버에서 연결 정보가 삭제되었습니다. 에이전트를 재시작하세요.");
|
|
111
|
+
clearInterval(heartbeatTimer);
|
|
112
|
+
process.exit(1);
|
|
103
113
|
}
|
|
104
114
|
}
|
|
105
115
|
}, 10_000);
|