diff --git a/src/proxy.ts b/src/proxy.ts index 4b09c31..8595cb8 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -6,6 +6,7 @@ import { saveApiKey, promptForApiKey, readAuthKey, deleteAuth } from "@/auth.js" import { setupOpenCodeConfig } from "@/setup/opencode.js"; import { setupClaudeCodeConfig } from "@/setup/claude-code.js"; import { logger, initLogger } from "@/logger.js"; +import { getProxyVersion } from "@/version.js"; const args = process.argv.slice(2); @@ -74,7 +75,7 @@ if (!config.apiKey) { const server = createServer(config); server.listen(config.port, config.host, () => { - console.log(`\n Command Code API Proxy v${process.env.npm_package_version ?? "0.1.0"}`); + console.log(`\n Command Code API Proxy v${getProxyVersion()}`); console.log(` ${"=".repeat(50)}`); console.log(` Listening on http://${config.host}:${config.port}`); console.log(` Auth: ${config.apiKey ? "ENABLED (Bearer token or x-api-key)" : "DISABLED"}`); diff --git a/src/server.ts b/src/server.ts index 334a2ab..9ce31de 100644 --- a/src/server.ts +++ b/src/server.ts @@ -13,6 +13,7 @@ import type { CCEvent } from "@/translate/types.js"; import { formatSSE, formatSSEDone, formatAnthropicSSE } from "@/stream.js"; import { sendToCC, collectEvents, UpstreamError } from "@/upstream.js"; import { logger } from "@/logger.js"; +import { getProxyVersion } from "@/version.js"; import { validateOpenAIChatRequest, validateAnthropicRequest, @@ -242,7 +243,7 @@ async function pumpStream( function handleHealth(_req: http.IncomingMessage, res: http.ServerResponse): void { sendJson(res, 200, { status: "ok", - version: process.env.npm_package_version ?? "0.1.0", + version: getProxyVersion(), }); } @@ -353,10 +354,7 @@ async function handleChatCompletions( stream, res, (event) => encoder.emit(event).map((c) => formatSSE(c)), - () => - encoder.finished - ? [] - : encoder.finishChunks("stop").map((c) => formatSSE(c)), + () => (encoder.finished ? [] : encoder.finishChunks("stop").map((c) => formatSSE(c))), // Stream-level error (TCP failure, idle timeout, encoder throw). // Always emit a uniform content+finish chunk pair via streamErrorChunks // — mixing a non-chunk `{error:...}` envelope with valid chunks @@ -455,9 +453,7 @@ async function handleMessages(req: http.IncomingMessage, res: http.ServerRespons () => encoder.finished ? [] - : encoder - .finishRecords("end_turn") - .map((r) => formatAnthropicSSE(r.event, r.data)), + : encoder.finishRecords("end_turn").map((r) => formatAnthropicSSE(r.event, r.data)), (err) => { const records: AnthropicSSERecord[] = [ { diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 0000000..67323d1 --- /dev/null +++ b/src/version.ts @@ -0,0 +1,20 @@ +import { readFileSync } from "node:fs"; + +let cached: string | undefined; + +export function getProxyVersion(): string { + if (cached) return cached; + cached = readPackageJsonVersion() ?? process.env.npm_package_version ?? "0.0.0"; + return cached; +} + +function readPackageJsonVersion(): string | undefined { + try { + const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as { + version?: string; + }; + return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : undefined; + } catch { + return undefined; + } +} diff --git a/tests/server.test.ts b/tests/server.test.ts index d2f1991..f8128b0 100644 --- a/tests/server.test.ts +++ b/tests/server.test.ts @@ -1,8 +1,13 @@ import { describe, it, expect, beforeAll, afterAll } from "vitest"; import http from "node:http"; +import { readFileSync } from "node:fs"; import { loadConfig } from "@/config.js"; import { createServer } from "@/server.js"; +const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as { + version: string; +}; + describe("Server", () => { let server: http.Server; const port = 18987; @@ -32,11 +37,12 @@ describe("Server", () => { expect(body.error).toBe("Not found"); }); - it("returns health status", async () => { + it("returns health status with the real package version", async () => { const res = await fetch(`${baseUrl}/health`); expect(res.status).toBe(200); const body = (await res.json()) as any; expect(body.status).toBe("ok"); + expect(body.version).toBe(pkg.version); }); it("returns model list", async () => { diff --git a/tests/version.test.ts b/tests/version.test.ts new file mode 100644 index 0000000..ef3bae0 --- /dev/null +++ b/tests/version.test.ts @@ -0,0 +1,21 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import { readFileSync } from "node:fs"; +import { getProxyVersion } from "@/version.js"; + +const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as { + version: string; +}; + +describe("getProxyVersion", () => { + beforeAll(() => { + delete process.env.npm_package_version; + }); + + it("resolves the version from package.json without npm env vars", () => { + expect(getProxyVersion()).toBe(pkg.version); + }); + + it("does not fall back to the stale 0.1.0 default", () => { + expect(getProxyVersion()).not.toBe("0.1.0"); + }); +});