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 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
- if (data.type === "assistant") {
5
- if (data.subtype === "text" && data.text) {
6
- return { type: "text", content: data.text };
7
- }
8
- if (data.subtype === "tool_use" && data.tool_name) {
9
- return { type: "tool_use", content: data.tool_name, tool: data.tool_name };
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, // 다음 작업의 --resume에 사용
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
- if (heartbeatFailures > 3) {
102
- console.warn(`heartbeat 연속 실패 (${heartbeatFailures}회)`);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fireqa-agent",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "FireQA Agent CLI — connect your AI CLI (Claude Code, Codex, Gemini) to FireQA",
5
5
  "type": "module",
6
6
  "bin": {