From b524dfe36fd6e1044e3a01e131d053d54461a762 Mon Sep 17 00:00:00 2001 From: can1357 Date: Tue, 11 Aug 2026 15:38:32 +0200 Subject: [PATCH] refactor: standardized outbound User-Agent headers on shared utility constant - Define a centralized `USER_AGENT` constant in `@oh-my-pi/pi-utils` formatted as `omp/`. - Replace hardcoded and platform-specific user agent strings across AI providers, catalog scrapers, tools, and search providers with the unified `USER_AGENT`. - Add unit tests for update-cli binary release distribution gating. --- packages/ai/CHANGELOG.md | 4 ++ .../src/providers/openai-codex-responses.ts | 5 +-- packages/ai/src/usage/openai-codex-reset.ts | 3 +- packages/ai/src/usage/openai-codex.ts | 3 +- packages/ai/src/usage/zai.ts | 3 +- packages/ai/src/utils/openrouter-headers.ts | 4 +- packages/catalog/CHANGELOG.md | 4 ++ .../src/provider-models/openai-compat.ts | 4 +- packages/coding-agent/CHANGELOG.md | 4 ++ packages/coding-agent/src/hindsight/client.ts | 2 +- packages/coding-agent/src/lib/xai-http.ts | 4 -- packages/coding-agent/src/tools/image-gen.ts | 8 ++-- packages/coding-agent/src/tools/tts.ts | 5 ++- .../coding-agent/src/utils/tools-manager.ts | 4 +- .../src/web/scrapers/crates-io.ts | 4 +- .../coding-agent/src/web/scrapers/discogs.ts | 4 +- .../coding-agent/src/web/scrapers/docs-rs.ts | 4 +- .../coding-agent/src/web/scrapers/github.ts | 4 +- .../src/web/scrapers/musicbrainz.ts | 3 +- .../coding-agent/src/web/scrapers/pubmed.ts | 4 +- .../src/web/scrapers/sec-edgar.ts | 4 +- .../src/web/search/providers/codex.ts | 6 +-- .../src/web/search/providers/gemini.ts | 6 +-- .../coding-agent/test/tools/image-gen.test.ts | 4 +- packages/coding-agent/test/update-cli.test.ts | 37 +++++++++++++++++++ packages/utils/CHANGELOG.md | 1 + packages/utils/src/dirs.ts | 3 ++ scripts/check-spoofed-versions.ts | 3 +- 28 files changed, 97 insertions(+), 47 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index e062d0096..fb069b25c 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Standardized first-party outbound User-Agent headers on `omp/` via the shared `USER_AGENT` utility. + ### Fixed - Fixed the Amazon Bedrock and Cursor transports ignoring `StreamOptions.headers`; both built their request headers from scratch, so caller-supplied tracing or attribution headers were silently dropped while working on every other provider ([#8107](https://github.com/can1357/oh-my-pi/pull/8107) by [@svperfecta](https://github.com/svperfecta)). diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index 9e320b70c..d6325cb45 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -1,4 +1,3 @@ -import * as os from "node:os"; import { scheduler } from "node:timers/promises"; import { type } from "@oh-my-pi/omptype"; import { calculateCost } from "@oh-my-pi/pi-catalog/models"; @@ -19,8 +18,8 @@ import { parseStreamingJson, readSseJson, structuredCloneJSON, + USER_AGENT, } from "@oh-my-pi/pi-utils"; -import packageJson from "../../package.json" with { type: "json" }; import * as AIError from "../error"; import { getEnvApiKey, isOfficialCodexApiUrl } from "../stream"; import type { @@ -4250,7 +4249,7 @@ function createCodexHeaders( headers.set(OPENAI_HEADERS.BETA, betaHeader); headers.set(OPENAI_HEADERS.ORIGINATOR, OPENAI_HEADER_VALUES.ORIGINATOR_CODEX); headers.set(OPENAI_HEADERS.VERSION, codexClientVersion); - headers.set("User-Agent", `pi/${packageJson.version} (${os.platform()} ${os.release()}; ${os.arch()})`); + headers.set("User-Agent", USER_AGENT); if (sessionId) { headers.set(OPENAI_HEADERS.CONVERSATION_ID, sessionId); headers.set(OPENAI_HEADERS.SESSION_ID, sessionId); diff --git a/packages/ai/src/usage/openai-codex-reset.ts b/packages/ai/src/usage/openai-codex-reset.ts index 944fe4205..50ea91c42 100644 --- a/packages/ai/src/usage/openai-codex-reset.ts +++ b/packages/ai/src/usage/openai-codex-reset.ts @@ -19,6 +19,7 @@ * share one wire contract. */ import { toNumber } from "@oh-my-pi/pi-catalog/utils"; +import { USER_AGENT } from "@oh-my-pi/pi-utils"; import type { FetchImpl } from "../types"; import { isRecord } from "../utils"; import { normalizeCodexBaseUrl } from "./openai-codex-base-url"; @@ -89,7 +90,7 @@ function buildUrl(baseUrl: string | undefined, routePath: string): string { function buildHeaders(auth: CodexResetAuth, json: boolean): Record { const headers: Record = { Authorization: `Bearer ${auth.accessToken}`, - "User-Agent": "OpenCode-Status-Plugin/1.0", + "User-Agent": USER_AGENT, }; if (auth.accountId) headers["ChatGPT-Account-Id"] = auth.accountId; if (json) headers["Content-Type"] = "application/json"; diff --git a/packages/ai/src/usage/openai-codex.ts b/packages/ai/src/usage/openai-codex.ts index 03ee5a213..8589c989f 100644 --- a/packages/ai/src/usage/openai-codex.ts +++ b/packages/ai/src/usage/openai-codex.ts @@ -1,5 +1,6 @@ import { Buffer } from "node:buffer"; import { toNumber } from "@oh-my-pi/pi-catalog/utils"; +import { USER_AGENT } from "@oh-my-pi/pi-utils"; import type { CredentialRankingContext, CredentialRankingStrategy, @@ -417,7 +418,7 @@ export const openaiCodexUsageProvider: UsageProvider = { const headers: Record = { Authorization: `Bearer ${accessToken}`, - "User-Agent": "OpenCode-Status-Plugin/1.0", + "User-Agent": USER_AGENT, }; if (accountId) { headers["ChatGPT-Account-Id"] = accountId; diff --git a/packages/ai/src/usage/zai.ts b/packages/ai/src/usage/zai.ts index 582cddb85..0f1712d22 100644 --- a/packages/ai/src/usage/zai.ts +++ b/packages/ai/src/usage/zai.ts @@ -1,4 +1,5 @@ import { toNumber } from "@oh-my-pi/pi-catalog/utils"; +import { USER_AGENT } from "@oh-my-pi/pi-utils"; import type { CredentialRankingStrategy, UsageAmount, @@ -228,7 +229,7 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext): const headers: Record = { Authorization: token, "Content-Type": "application/json", - "User-Agent": "OpenCode-Status-Plugin/1.0", + "User-Agent": USER_AGENT, }; let payload: ZaiQuotaPayload | null = null; diff --git a/packages/ai/src/utils/openrouter-headers.ts b/packages/ai/src/utils/openrouter-headers.ts index 5f4ba74b8..e884c0bc1 100644 --- a/packages/ai/src/utils/openrouter-headers.ts +++ b/packages/ai/src/utils/openrouter-headers.ts @@ -1,8 +1,8 @@ -import packageJson from "../../package.json" with { type: "json" }; +import { USER_AGENT } from "@oh-my-pi/pi-utils"; export function getOpenRouterHeaders(): Record { return { - "User-Agent": `omp/${packageJson.version}`, + "User-Agent": USER_AGENT, "HTTP-Referer": "https://omp.sh/", "X-OpenRouter-Title": "omp", "X-OpenRouter-Categories": "cli-agent", diff --git a/packages/catalog/CHANGELOG.md b/packages/catalog/CHANGELOG.md index 6b90d382f..6ce250d64 100644 --- a/packages/catalog/CHANGELOG.md +++ b/packages/catalog/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Standardized catalog discovery User-Agent headers on `omp/` via the shared `USER_AGENT` utility. + ### Fixed - Marked `meta/muse-spark-1.2` and `muse-spark-1.2-contributor` as image-capable (`input: ["text", "image"]`) with the same Responses reasoning, thinking, and cost metadata as `muse-spark-1.1` (contributor uses its discounted 0.1/0.2 pricing), so `omp models` no longer lists them as text-only. diff --git a/packages/catalog/src/provider-models/openai-compat.ts b/packages/catalog/src/provider-models/openai-compat.ts index eec801363..029622be7 100644 --- a/packages/catalog/src/provider-models/openai-compat.ts +++ b/packages/catalog/src/provider-models/openai-compat.ts @@ -1,4 +1,4 @@ -import { VERSION } from "@oh-my-pi/pi-utils"; +import { USER_AGENT } from "@oh-my-pi/pi-utils"; import * as logger from "@oh-my-pi/pi-utils/logger"; import { fetchOpenAICompatibleModels, @@ -111,7 +111,7 @@ const catalogSession: { hasPayload: boolean; } = { inflight: null, payload: undefined, etag: null, hasPayload: false }; -const CATALOG_USER_AGENT = `omp/${VERSION} (+https://omp.sh)`; +const CATALOG_USER_AGENT = USER_AGENT; /** * Fetches the models.dev catalog via catalog.stencil.so, which serves a diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 63a304a08..ccf7d52b5 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -22,6 +22,10 @@ - Added structured reset-reason logging to advisor context re-primes (issue #7226): every history-rewrite trigger (compact, auto-compaction, compaction-rescue, shake, drop-images, prune-tool-outputs, prune-stale-tool-results, conversation-boundary, context-maintenance) now emits an `advisor context reset` debug event with its reason, so full-transcript replays can be attributed to a concrete path. - Added `quarantine-recovery` and `quarantine-retry-exhausted` reset reasons to advisor context-reset debug logs, so advisor full re-primes after quarantined output remain attributable without changing quarantine retry semantics (issue #7226). +### Changed + +- Standardized first-party outbound User-Agent headers on `omp/` via the shared `USER_AGENT` utility. + ### Fixed - Fixed `/usage`, `/advisor status`, and every other panel command answering only after the agent stopped working. Since `17.0.1` their output was queued until the turn settled (to stop mid-turn transcript mounts duplicating rows in native scrollback, issues #4806/#6767), and the deferral was silent, so on a long turn the command was indistinguishable from a dead one. The panel now renders immediately above the editor in an anchored container that is cleared and rebuilt in place, never entering the transcript, and the full output still lands in the transcript at the next settle. The preview is capped to 40% of the viewport (minimum 6 rows) so a tall report cannot push the prompt off screen. diff --git a/packages/coding-agent/src/hindsight/client.ts b/packages/coding-agent/src/hindsight/client.ts index b4e0feb72..4260bf8ee 100644 --- a/packages/coding-agent/src/hindsight/client.ts +++ b/packages/coding-agent/src/hindsight/client.ts @@ -8,10 +8,10 @@ * tests to spy on. */ +import { USER_AGENT } from "@oh-my-pi/pi-utils"; import { isTimeoutError, withTimeoutSignal } from "../utils/fetch-timeout"; import type { HindsightConfig } from "./config"; -const USER_AGENT = "oh-my-pi-coding-agent"; const DEFAULT_USER_AGENT = USER_AGENT; /** Fallback deadlines (ms) applied when the caller supplies no override. */ const DEFAULT_REQUEST_TIMEOUT_MS = 30_000; diff --git a/packages/coding-agent/src/lib/xai-http.ts b/packages/coding-agent/src/lib/xai-http.ts index 63bf64a8c..a68218c37 100644 --- a/packages/coding-agent/src/lib/xai-http.ts +++ b/packages/coding-agent/src/lib/xai-http.ts @@ -12,10 +12,6 @@ interface XAICredentials { baseURL: string; } -export function ohMyPiXAIUserAgent(): string { - return "oh-my-pi/xai"; -} - /** xAI provider ids supported by shared HTTP tool transport resolution. */ export type XAIHttpProvider = "xai-oauth" | "xai"; diff --git a/packages/coding-agent/src/tools/image-gen.ts b/packages/coding-agent/src/tools/image-gen.ts index 360f4acbe..254cc7e78 100644 --- a/packages/coding-agent/src/tools/image-gen.ts +++ b/packages/coding-agent/src/tools/image-gen.ts @@ -19,13 +19,13 @@ import { ptree, readSseJson, Snowflake, + USER_AGENT, untilAborted, } from "@oh-my-pi/pi-utils"; -import packageJson from "../../package.json" with { type: "json" }; import { isAuthenticated, type ModelRegistry } from "../config/model-registry"; import { settings } from "../config/settings"; import type { CustomTool } from "../extensibility/custom-tools/types"; -import { ohMyPiXAIUserAgent, resolveXAIHttpCredentials } from "../lib/xai-http"; +import { resolveXAIHttpCredentials } from "../lib/xai-http"; import imageGenDescription from "../prompts/tools/image-gen.md" with { type: "text" }; import { AUTO_IMAGE_PROVIDER_ORDER, type ImageProvider, isImageProviderId } from "./image-providers"; import { resolveReadPath } from "./path-utils"; @@ -897,7 +897,7 @@ function buildOpenAIImageHeaders(model: Model, apiKey: string, sessionId: string } headers.set(OPENAI_HEADERS.BETA, OPENAI_HEADER_VALUES.BETA_RESPONSES); headers.set(OPENAI_HEADERS.ORIGINATOR, OPENAI_HEADER_VALUES.ORIGINATOR_CODEX); - headers.set("User-Agent", `pi/${packageJson.version} (${os.platform()} ${os.release()}; ${os.arch()})`); + headers.set("User-Agent", USER_AGENT); if (sessionId) { headers.set(OPENAI_HEADERS.CONVERSATION_ID, sessionId); headers.set(OPENAI_HEADERS.SESSION_ID, sessionId); @@ -1389,7 +1389,7 @@ export const imageGenTool: CustomTool = { Accept: "application/vnd.github.v3+json", - "User-Agent": "omp-web-fetch/1.0", + "User-Agent": USER_AGENT, }; // Use GITHUB_TOKEN if available diff --git a/packages/coding-agent/src/web/scrapers/musicbrainz.ts b/packages/coding-agent/src/web/scrapers/musicbrainz.ts index 4d6537e4b..dea914e59 100644 --- a/packages/coding-agent/src/web/scrapers/musicbrainz.ts +++ b/packages/coding-agent/src/web/scrapers/musicbrainz.ts @@ -2,7 +2,7 @@ * MusicBrainz URL handler for artists, releases, and recordings */ -import { tryParseJson } from "@oh-my-pi/pi-utils"; +import { tryParseJson, USER_AGENT } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; import { buildResult, formatMediaDuration, loadPage } from "./types"; @@ -64,7 +64,6 @@ interface MusicBrainzRelease { } const MUSICBRAINZ_HOSTS = new Set(["musicbrainz.org", "www.musicbrainz.org"]); -const USER_AGENT = "omp-web-fetch/1.0 (https://github.com/anthropics)"; const MAX_TRACKS = 50; function parseEntity(url: URL): { entity: MusicBrainzEntity; mbid: string } | null { diff --git a/packages/coding-agent/src/web/scrapers/pubmed.ts b/packages/coding-agent/src/web/scrapers/pubmed.ts index ebc7fdf4f..343343b2a 100644 --- a/packages/coding-agent/src/web/scrapers/pubmed.ts +++ b/packages/coding-agent/src/web/scrapers/pubmed.ts @@ -1,12 +1,12 @@ /** * PubMed handler for web-fetch */ -import { tryParseJson } from "@oh-my-pi/pi-utils"; +import { tryParseJson, USER_AGENT } from "@oh-my-pi/pi-utils"; import { buildResult, loadPage, type RenderResult, type SpecialHandler } from "./types"; const NCBI_HEADERS = { Accept: "application/json, text/plain;q=0.9, */*;q=0.8", - "User-Agent": "CodingAgent/1.0 (web scraper)", + "User-Agent": USER_AGENT, }; /** diff --git a/packages/coding-agent/src/web/scrapers/sec-edgar.ts b/packages/coding-agent/src/web/scrapers/sec-edgar.ts index 03f49ea7a..7e3ced29a 100644 --- a/packages/coding-agent/src/web/scrapers/sec-edgar.ts +++ b/packages/coding-agent/src/web/scrapers/sec-edgar.ts @@ -1,4 +1,4 @@ -import { tryParseJson } from "@oh-my-pi/pi-utils"; +import { tryParseJson, USER_AGENT } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; import { buildResult, loadPage } from "./types"; @@ -178,7 +178,7 @@ export const handleSecEdgar: SpecialHandler = async ( timeout, signal, headers: { - "User-Agent": "CodingAgent/1.0 (research tool)", + "User-Agent": USER_AGENT, Accept: "application/json", }, }); diff --git a/packages/coding-agent/src/web/search/providers/codex.ts b/packages/coding-agent/src/web/search/providers/codex.ts index 3813c02b8..580922d92 100644 --- a/packages/coding-agent/src/web/search/providers/codex.ts +++ b/packages/coding-agent/src/web/search/providers/codex.ts @@ -4,7 +4,6 @@ * Uses the configured Codex Responses transport for proxy/API-key setups and * the official ChatGPT backend for OAuth logins. */ -import * as os from "node:os"; import { type AuthStorage, type FetchImpl, @@ -22,8 +21,7 @@ import { OPENAI_HEADER_VALUES, OPENAI_HEADERS, } from "@oh-my-pi/pi-catalog/wire/codex"; -import { $env, readSseJson } from "@oh-my-pi/pi-utils"; -import packageJson from "../../../../package.json" with { type: "json" }; +import { $env, readSseJson, USER_AGENT } from "@oh-my-pi/pi-utils"; import type { ModelRegistry } from "../../../config/model-registry"; import type { SearchResponse, SearchSource } from "../../../web/search/types"; import { SearchProviderError } from "../../../web/search/types"; @@ -441,7 +439,7 @@ function buildCodexHeaders( headers.set(OPENAI_HEADERS.BETA, OPENAI_HEADER_VALUES.BETA_RESPONSES); headers.set(OPENAI_HEADERS.ORIGINATOR, OPENAI_HEADER_VALUES.ORIGINATOR_CODEX); headers.set(OPENAI_HEADERS.VERSION, CODEX_CLIENT_VERSION); - headers.set("User-Agent", `pi/${packageJson.version} (${os.platform()} ${os.release()}; ${os.arch()})`); + headers.set("User-Agent", USER_AGENT); headers.set("Accept", "text/event-stream"); headers.set("Content-Type", "application/json"); return headers; diff --git a/packages/coding-agent/src/web/search/providers/gemini.ts b/packages/coding-agent/src/web/search/providers/gemini.ts index b8c5db686..c9ba6d0a3 100644 --- a/packages/coding-agent/src/web/search/providers/gemini.ts +++ b/packages/coding-agent/src/web/search/providers/gemini.ts @@ -14,7 +14,7 @@ import { getAntigravityUserAgent, getGeminiCliHeaders, } from "@oh-my-pi/pi-catalog/wire/gemini-headers"; -import { fetchWithRetry } from "@oh-my-pi/pi-utils"; +import { fetchWithRetry, USER_AGENT } from "@oh-my-pi/pi-utils"; import type { SearchCitation, SearchResponse, SearchSource } from "../../../web/search/types"; import { SearchProviderError } from "../../../web/search/types"; @@ -436,8 +436,8 @@ async function callGeminiSearch( requestId: `agent-${crypto.randomUUID()}`, } : { - userAgent: "pi-coding-agent", - requestId: `pi-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`, + userAgent: USER_AGENT, + requestId: `omp-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`, }; const normalizedSystemPrompt = systemPrompt?.toWellFormed(); diff --git a/packages/coding-agent/test/tools/image-gen.test.ts b/packages/coding-agent/test/tools/image-gen.test.ts index fd6abef32..1ff0629e7 100644 --- a/packages/coding-agent/test/tools/image-gen.test.ts +++ b/packages/coding-agent/test/tools/image-gen.test.ts @@ -9,7 +9,7 @@ import { imageGenTool, setImageProviderOrder, } from "@oh-my-pi/pi-coding-agent/tools/image-gen"; -import { removeWithRetries } from "@oh-my-pi/pi-utils"; +import { removeWithRetries, USER_AGENT } from "@oh-my-pi/pi-utils"; const originalOpenRouterKey = Bun.env.OPENROUTER_API_KEY; const generatedImagePaths: string[] = []; @@ -618,7 +618,7 @@ describe("imageGenTool", () => { expect(requestUrl).toBe("https://api.x.ai/v1/images/generations"); expect(captured.authorization).toBe("Bearer test-xai-token"); - expect(captured.userAgent).toBe("oh-my-pi/xai"); + expect(captured.userAgent).toBe(USER_AGENT); expect(requestBody).toMatchObject({ model: "grok-imagine-image", prompt: "a cat.", diff --git a/packages/coding-agent/test/update-cli.test.ts b/packages/coding-agent/test/update-cli.test.ts index 4d731d4cc..24c52a97a 100644 --- a/packages/coding-agent/test/update-cli.test.ts +++ b/packages/coding-agent/test/update-cli.test.ts @@ -19,7 +19,9 @@ import { replaceBinaryForUpdate, resolveBunGlobalNodeModulesDirFromLocations, resolveReleaseBinaryAsset, + resolveReleaseDist, resolveUpdateMethodForTest, + shouldForceBinaryUpdate, sweepStaleBackups, updateViaBinaryAt, } from "@oh-my-pi/pi-coding-agent/cli/update-cli"; @@ -740,3 +742,38 @@ describe("update-cli stale backup sweep", () => { expect(await Bun.file(`${targetPath}.config.bak`).exists()).toBe(true); }); }); + +describe("update-cli binary-only release gating", () => { + it("honors an explicit omp.dist field from the registry manifest", () => { + expect(resolveReleaseDist({ omp: { dist: "binary" } })).toBe("binary"); + expect(resolveReleaseDist({ omp: { dist: "npm" } })).toBe("npm"); + }); + + it("treats unknown dist values as binary-only", () => { + expect(resolveReleaseDist({ omp: { dist: "cargo" } })).toBe("binary"); + }); + + it("returns undefined when the manifest carries no dist field", () => { + expect(resolveReleaseDist({ version: "1.2.3" })).toBeUndefined(); + expect(resolveReleaseDist({ omp: {} })).toBeUndefined(); + expect(resolveReleaseDist(undefined)).toBeUndefined(); + }); + + it("forces binary updates when dist is binary regardless of version", () => { + expect(shouldForceBinaryUpdate({ version: "1.2.3", dist: "binary" }, "1.2.2")).toBe(true); + }); + + it("allows package-manager updates across majors when dist is explicitly npm", () => { + expect(shouldForceBinaryUpdate({ version: "2.0.0", dist: "npm" }, "1.9.0")).toBe(false); + }); + + it("forces binary updates on a major bump without a dist field", () => { + expect(shouldForceBinaryUpdate({ version: "2.0.0" }, "1.9.0")).toBe(true); + expect(shouldForceBinaryUpdate({ version: "2.0.0-rc.1" }, "1.9.0")).toBe(true); + }); + + it("keeps package-manager updates within the same major and on downgrades", () => { + expect(shouldForceBinaryUpdate({ version: "1.10.0" }, "1.9.0")).toBe(false); + expect(shouldForceBinaryUpdate({ version: "1.0.0" }, "2.0.0")).toBe(false); + }); +}); diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md index b231472c1..f98d4598f 100644 --- a/packages/utils/CHANGELOG.md +++ b/packages/utils/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changed - Changed stale process-log retention from the newest five files globally to one newest file per completed process and day within the current and previous four local calendar days. This preserves bounded daily diagnostic coverage while continuing to remove one-use audit files. +- Changed outbound User-Agent consumers to share the versioned `USER_AGENT` constant (`omp/`). ### Fixed diff --git a/packages/utils/src/dirs.ts b/packages/utils/src/dirs.ts index be8752fdb..c33b1471a 100644 --- a/packages/utils/src/dirs.ts +++ b/packages/utils/src/dirs.ts @@ -28,6 +28,9 @@ export const MAIN_CONFIG_FILENAMES = ["config.yml", "config.yaml"] as const; /** Version (e.g. "1.0.0") */ export const VERSION: string = version; +/** Default User-Agent header string (e.g. "omp/17.2.12") */ +export const USER_AGENT = `omp/${VERSION}`; + /** Minimum Bun version */ export const MIN_BUN_VERSION: string = engines.bun.replace(/[^0-9.]/g, ""); diff --git a/scripts/check-spoofed-versions.ts b/scripts/check-spoofed-versions.ts index af9b4c557..be071af77 100755 --- a/scripts/check-spoofed-versions.ts +++ b/scripts/check-spoofed-versions.ts @@ -14,6 +14,7 @@ */ import * as path from "node:path"; +import { USER_AGENT } from "@oh-my-pi/pi-utils"; const PROVIDER_FILE = path.join(import.meta.dir, "../packages/catalog/src/wire/gemini-headers.ts"); @@ -36,7 +37,7 @@ async function fetchLatestGitHubRelease( try { // /releases/latest only returns non-prerelease, non-draft releases const res = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, { - headers: { Accept: "application/vnd.github+json", "User-Agent": "oh-my-pi/version-check" }, + headers: { Accept: "application/vnd.github+json", "User-Agent": USER_AGENT }, }); if (!res.ok) return null; const data = (await res.json()) as { tag_name?: string };