fix(cli): fallback to user-local private key + record analytics

- cli.ts: key resolution now falls back to ~/.hermes/keys/pr-agent-key.pem
  when /opt/pr-agent-server/private-key.pem is EACCES/ENOENT (CLI as
  non-root user works out of the box)
- cli.ts+index.ts: export logReviewEvent; CLI now records an analytics
  event (describe/improve/review) in the legacy pr-agent.*.log format,
  best-effort (never fails the CLI on a log write), honoring
  PR_AGENT_ANALYTICS_DIR
- Verified: 16/16 tests, tsc clean, describe publishes, review publishes
  (comment 5757282509), webhook 403/ping/ignored paths correct
This commit is contained in:
asepharyana
2026-09-21 15:11:36 +07:00
parent 570b5b8707
commit 4fa7d8b6d0
2 changed files with 35 additions and 3 deletions
+34 -2
View File
@@ -5,6 +5,26 @@ import { loadConfig } from "./config";
import { runReview } from "./review"; import { runReview } from "./review";
import { runDescribe } from "./describe"; import { runDescribe } from "./describe";
import { runImprove } from "./improve"; import { runImprove } from "./improve";
import { logReviewEvent } from "./index";
import { join } from "node:path";
const ANALYTICS_DIR = process.env.PR_AGENT_ANALYTICS_DIR || "/var/lib/pr-agent-server/analytics";
function recordAnalytics(command: string, result: { status?: string; model?: string }): void {
try {
logReviewEvent(ANALYTICS_DIR, {
message: result.status === "success" ? "Generated code suggestions" : `Failed to generate (${result.status ?? "error"})`,
extra: {
command,
pr_url_short: "",
model: result.model ?? "",
error: result.status === "success" ? "" : (result.status ?? "error"),
},
});
} catch {
// analytics is best-effort; never fail the CLI on a log write
}
}
async function main() { async function main() {
const args = process.argv.slice(2); const args = process.argv.slice(2);
@@ -32,9 +52,18 @@ async function main() {
const tool = getArg("--tool") || "review"; const tool = getArg("--tool") || "review";
const cfg = loadConfig(); const cfg = loadConfig();
const keyPath = getArg("--private-key") || process.env.PRIVATE_KEY_PATH || "/opt/pr-agent-server/private-key.pem"; const keyPath =
getArg("--private-key") ||
process.env.PRIVATE_KEY_PATH ||
"/opt/pr-agent-server/private-key.pem";
const fs = await import("node:fs"); const fs = await import("node:fs");
const privateKey = fs.readFileSync(keyPath, "utf-8"); const fallbackKey = join(process.env.HOME ?? "/home/code", ".hermes", "keys", "pr-agent-key.pem");
let privateKey: string;
try {
privateKey = fs.readFileSync(keyPath, "utf-8");
} catch {
privateKey = fs.readFileSync(fallbackKey, "utf-8"); // EACCES/ENOENT on /opt → user-local copy
}
const publish = !has("--no-publish"); const publish = !has("--no-publish");
if (tool === "describe") { if (tool === "describe") {
@@ -54,6 +83,7 @@ async function main() {
)); ));
console.log("\n--- MARKDOWN ---\n"); console.log("\n--- MARKDOWN ---\n");
console.log(result.markdown); console.log(result.markdown);
recordAnalytics("describe", result);
return; return;
} }
@@ -74,6 +104,7 @@ async function main() {
)); ));
console.log("\n--- MARKDOWN ---\n"); console.log("\n--- MARKDOWN ---\n");
console.log(result.markdown); console.log(result.markdown);
recordAnalytics("improve", result);
return; return;
} }
@@ -95,6 +126,7 @@ async function main() {
)); ));
console.log("\n--- MARKDOWN ---\n"); console.log("\n--- MARKDOWN ---\n");
console.log(result.markdown); console.log(result.markdown);
recordAnalytics("review", result);
} }
main().catch((e) => { main().catch((e) => {
+1 -1
View File
@@ -384,7 +384,7 @@ export function readAnalyticsLogs(dir: string, maxFiles = 5): AnalyticsRecord[]
/** Append an analytics event in the legacy pr-agent JSONL shape so external /** Append an analytics event in the legacy pr-agent JSONL shape so external
* dashboards that parse pr-agent.*.log keep working. */ * dashboards that parse pr-agent.*.log keep working. */
function logReviewEvent(dir: string, event: Record<string, unknown>): void { export function logReviewEvent(dir: string, event: Record<string, unknown>): void {
if (!dir) return; if (!dir) return;
const fs = require("node:fs") as typeof import("node:fs"); const fs = require("node:fs") as typeof import("node:fs");
const path = require("node:path") as typeof import("node:path"); const path = require("node:path") as typeof import("node:path");