pi-codex-search 0.1.2 → 0.1.4

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.
@@ -0,0 +1,378 @@
1
+ import {
2
+ CodexError,
3
+ classifyHttpStatus,
4
+ formatHttpErrorBody,
5
+ isCloudflareChallenge,
6
+ } from "../errors.ts";
7
+ import type { CodexTransport } from "../transport.ts";
8
+ import type {
9
+ CodexWebSearchResult,
10
+ CodexCitation,
11
+ CodexSearchCall,
12
+ SearchContextSize,
13
+ Freshness,
14
+ StandaloneExternalWebAccess,
15
+ ResponseLength,
16
+ } from "./types.ts";
17
+
18
+ export interface SearchQuery {
19
+ q: string;
20
+ recency?: number;
21
+ domains?: string[];
22
+ }
23
+
24
+ export interface OpenCommand {
25
+ refId: string;
26
+ lineno?: number;
27
+ }
28
+
29
+ export interface FindCommand {
30
+ refId: string;
31
+ pattern: string;
32
+ }
33
+
34
+ export interface ClickCommand {
35
+ refId: string;
36
+ id: number;
37
+ }
38
+
39
+ export interface ScreenshotCommand {
40
+ refId: string;
41
+ pageno: number;
42
+ }
43
+
44
+ export interface FinanceCommand {
45
+ ticker: string;
46
+ type: "equity" | "fund" | "crypto" | "index";
47
+ market?: string;
48
+ }
49
+
50
+ export interface WeatherCommand {
51
+ location: string;
52
+ start?: string;
53
+ duration?: number;
54
+ }
55
+
56
+ export type SportsLeague =
57
+ | "nba"
58
+ | "wnba"
59
+ | "nfl"
60
+ | "nhl"
61
+ | "mlb"
62
+ | "epl"
63
+ | "ncaamb"
64
+ | "ncaawb"
65
+ | "ipl";
66
+
67
+ export interface SportsCommand {
68
+ fn: "schedule" | "standings";
69
+ league: SportsLeague;
70
+ team?: string;
71
+ opponent?: string;
72
+ date_from?: string;
73
+ date_to?: string;
74
+ num_games?: number;
75
+ locale?: string;
76
+ }
77
+
78
+ export interface TimeCommand {
79
+ utc_offset: string;
80
+ }
81
+
82
+ export interface StandaloneCommandsOptions {
83
+ model: string;
84
+ transport: CodexTransport;
85
+ sessionId: string;
86
+ searchQuery?: SearchQuery[];
87
+ imageQuery?: SearchQuery[];
88
+ open?: OpenCommand[];
89
+ find?: FindCommand[];
90
+ click?: ClickCommand[];
91
+ screenshot?: ScreenshotCommand[];
92
+ finance?: FinanceCommand[];
93
+ weather?: WeatherCommand[];
94
+ sports?: SportsCommand[];
95
+ time?: TimeCommand[];
96
+ freshness: Freshness;
97
+ searchContextSize?: SearchContextSize;
98
+ responseLength?: ResponseLength;
99
+ maxOutputTokens?: number;
100
+ signal?: AbortSignal;
101
+ }
102
+
103
+ interface StandaloneSearchResponse {
104
+ encrypted_output?: string;
105
+ output?: string;
106
+ }
107
+
108
+ export function externalWebAccessForFreshness(freshness: Freshness): StandaloneExternalWebAccess {
109
+ if (freshness === "cached") return false;
110
+ if (freshness === "indexed") return "indexed";
111
+ return true;
112
+ }
113
+
114
+ export function hasAnyCommand(options: StandaloneCommandsOptions): boolean {
115
+ return countCommands(options) > 0;
116
+ }
117
+
118
+ export function isUnsupportedStandaloneCombination(
119
+ searchContextSize: SearchContextSize | undefined,
120
+ _freshness: Freshness,
121
+ ): boolean {
122
+ return (searchContextSize ?? "medium") === "low";
123
+ }
124
+
125
+ export function assertSupportedStandaloneCombination(
126
+ searchContextSize: SearchContextSize | undefined,
127
+ freshness: Freshness,
128
+ ): void {
129
+ if (isUnsupportedStandaloneCombination(searchContextSize, freshness)) {
130
+ throw new CodexError(
131
+ "schema",
132
+ 'standalone/low is disabled because Codex returns Cloudflare challenges for low-context standalone requests. Use search_context_size "medium" or "high".',
133
+ );
134
+ }
135
+ }
136
+
137
+ function countCommands(options: StandaloneCommandsOptions): number {
138
+ return (
139
+ (options.searchQuery?.length ?? 0) +
140
+ (options.imageQuery?.length ?? 0) +
141
+ (options.open?.length ?? 0) +
142
+ (options.find?.length ?? 0) +
143
+ (options.click?.length ?? 0) +
144
+ (options.screenshot?.length ?? 0) +
145
+ (options.finance?.length ?? 0) +
146
+ (options.weather?.length ?? 0) +
147
+ (options.sports?.length ?? 0) +
148
+ (options.time?.length ?? 0)
149
+ );
150
+ }
151
+
152
+ export async function runStandaloneCommands(
153
+ options: StandaloneCommandsOptions,
154
+ ): Promise<CodexWebSearchResult> {
155
+ if (!hasAnyCommand(options)) {
156
+ throw new CodexError("schema", "Codex standalone commands require at least one command");
157
+ }
158
+ if (countCommands(options) > 1) {
159
+ throw new CodexError("schema", "Codex standalone actions must be sent one per request");
160
+ }
161
+ assertSupportedStandaloneCombination(options.searchContextSize, options.freshness);
162
+
163
+ const {
164
+ transport,
165
+ model,
166
+ sessionId,
167
+ freshness,
168
+ searchContextSize,
169
+ responseLength,
170
+ maxOutputTokens,
171
+ signal,
172
+ } = options;
173
+ const headers = transport.buildHeaders("application/json");
174
+ headers.set("OpenAI-Beta", "responses=experimental");
175
+ headers.set("content-type", "application/json");
176
+
177
+ const commands: Record<string, unknown> = {};
178
+ if (options.searchQuery?.length) commands.search_query = options.searchQuery;
179
+ if (options.imageQuery?.length) commands.image_query = options.imageQuery;
180
+ if (options.open?.length)
181
+ commands.open = options.open.map((c) => ({ ref_id: c.refId, lineno: c.lineno }));
182
+ if (options.find?.length)
183
+ commands.find = options.find.map((c) => ({ ref_id: c.refId, pattern: c.pattern }));
184
+ if (options.click?.length)
185
+ commands.click = options.click.map((c) => ({ ref_id: c.refId, id: c.id }));
186
+ if (options.screenshot?.length) {
187
+ commands.screenshot = options.screenshot.map((c) => ({ ref_id: c.refId, pageno: c.pageno }));
188
+ }
189
+ if (options.finance?.length) {
190
+ commands.finance = options.finance.map((c) => ({
191
+ ticker: c.ticker,
192
+ type: c.type,
193
+ market: c.market,
194
+ }));
195
+ }
196
+ if (options.weather?.length) {
197
+ commands.weather = options.weather.map((c) => ({
198
+ location: c.location,
199
+ start: c.start,
200
+ duration: c.duration,
201
+ }));
202
+ }
203
+ if (options.sports?.length) {
204
+ commands.sports = options.sports.map((c) => ({
205
+ fn: c.fn,
206
+ league: c.league,
207
+ team: c.team,
208
+ opponent: c.opponent,
209
+ date_from: c.date_from,
210
+ date_to: c.date_to,
211
+ num_games: c.num_games,
212
+ locale: c.locale,
213
+ }));
214
+ }
215
+ if (options.time?.length) commands.time = options.time.map((c) => ({ utc_offset: c.utc_offset }));
216
+ if (responseLength) commands.response_length = responseLength;
217
+
218
+ const body: Record<string, unknown> = {
219
+ id: sessionId,
220
+ model,
221
+ input: buildInput(options),
222
+ commands,
223
+ settings: {
224
+ search_context_size: searchContextSize ?? "medium",
225
+ allowed_callers: ["direct"],
226
+ external_web_access: externalWebAccessForFreshness(freshness),
227
+ },
228
+ };
229
+ body.max_output_tokens = maxOutputTokens ?? 8000;
230
+
231
+ const bodyText = JSON.stringify(body);
232
+ let response = await transport.fetch(transport.resolveSearchEndpoint(), {
233
+ method: "POST",
234
+ headers,
235
+ body: bodyText,
236
+ signal,
237
+ });
238
+
239
+ if (!response.ok) {
240
+ let status = response.status;
241
+ let rawText = await response.text();
242
+ if (status === 403 && isCloudflareChallenge(rawText) && !signal?.aborted) {
243
+ await delay(750, signal);
244
+ if (signal?.aborted) {
245
+ throw new CodexError("timeout", "Codex standalone request was aborted before retry.");
246
+ }
247
+ response = await transport.fetch(transport.resolveSearchEndpoint(), {
248
+ method: "POST",
249
+ headers,
250
+ body: bodyText,
251
+ signal,
252
+ });
253
+ if (!response.ok) {
254
+ status = response.status;
255
+ rawText = await response.text();
256
+ }
257
+ }
258
+ if (!response.ok) {
259
+ const text = formatHttpErrorBody(rawText, "standalone");
260
+ throw new CodexError(
261
+ classifyHttpStatus(status),
262
+ `Codex standalone search request failed: HTTP ${status}: ${text}`,
263
+ status,
264
+ );
265
+ }
266
+ }
267
+
268
+ const data = (await response.json()) as StandaloneSearchResponse;
269
+ const text = typeof data.output === "string" ? data.output : "";
270
+ const refIds = extractRefIds(text);
271
+ const searchCalls = inferSearchCalls(options);
272
+
273
+ const result: CodexWebSearchResult = {
274
+ model,
275
+ text,
276
+ searchCalls,
277
+ citations: extractMarkdownCitations(text),
278
+ refIds,
279
+ };
280
+ if (data.encrypted_output !== undefined) result.encryptedOutput = data.encrypted_output;
281
+ return result;
282
+ }
283
+
284
+ async function delay(ms: number, signal: AbortSignal | undefined): Promise<void> {
285
+ if (signal?.aborted) return;
286
+ await new Promise<void>((resolve) => {
287
+ const timeout = setTimeout(resolve, ms);
288
+ signal?.addEventListener(
289
+ "abort",
290
+ () => {
291
+ clearTimeout(timeout);
292
+ resolve();
293
+ },
294
+ { once: true },
295
+ );
296
+ });
297
+ }
298
+
299
+ function buildInput(options: StandaloneCommandsOptions): unknown[] {
300
+ const texts: string[] = [];
301
+ options.searchQuery?.forEach((q) => texts.push(q.q));
302
+ options.imageQuery?.forEach((q) => texts.push(q.q));
303
+ options.open?.forEach((c) => texts.push(c.refId));
304
+ options.find?.forEach((c) => texts.push(`find "${c.pattern}" in ${c.refId}`));
305
+ options.click?.forEach((c) => texts.push(`click ${c.id} in ${c.refId}`));
306
+ options.screenshot?.forEach((c) => texts.push(`screenshot ${c.pageno} of ${c.refId}`));
307
+ options.finance?.forEach((c) => texts.push(`finance ${c.ticker} ${c.type} ${c.market ?? ""}`));
308
+ options.weather?.forEach((c) => texts.push(`weather ${c.location}`));
309
+ options.sports?.forEach((c) => texts.push(`sports ${c.fn} ${c.league}`));
310
+ options.time?.forEach((c) => texts.push(`time ${c.utc_offset}`));
311
+
312
+ const prompt = texts.filter(Boolean).join("\n");
313
+ return [
314
+ {
315
+ type: "message",
316
+ role: "user",
317
+ content: [{ type: "input_text", text: prompt }],
318
+ },
319
+ ];
320
+ }
321
+
322
+ function inferSearchCalls(options: StandaloneCommandsOptions): CodexSearchCall[] {
323
+ const calls: CodexSearchCall[] = [];
324
+ options.searchQuery?.forEach((q) =>
325
+ calls.push({ status: "completed", query: q.q, actionType: "search_query" }),
326
+ );
327
+ options.imageQuery?.forEach((q) =>
328
+ calls.push({ status: "completed", query: q.q, actionType: "image_query" }),
329
+ );
330
+ options.open?.forEach((c) =>
331
+ calls.push({ status: "completed", refId: c.refId, actionType: "open_page" }),
332
+ );
333
+ options.find?.forEach((c) =>
334
+ calls.push({ status: "completed", refId: c.refId, actionType: "find_in_page" }),
335
+ );
336
+ options.click?.forEach((c) =>
337
+ calls.push({ status: "completed", refId: c.refId, actionType: "click" }),
338
+ );
339
+ options.screenshot?.forEach((c) =>
340
+ calls.push({ status: "completed", refId: c.refId, actionType: "screenshot" }),
341
+ );
342
+ options.finance?.forEach((c) =>
343
+ calls.push({ status: "completed", query: `${c.ticker}`, actionType: "finance" }),
344
+ );
345
+ options.weather?.forEach((c) =>
346
+ calls.push({ status: "completed", query: c.location, actionType: "weather" }),
347
+ );
348
+ options.sports?.forEach((c) =>
349
+ calls.push({ status: "completed", query: `${c.fn} ${c.league}`, actionType: "sports" }),
350
+ );
351
+ options.time?.forEach((c) =>
352
+ calls.push({ status: "completed", query: c.utc_offset, actionType: "time" }),
353
+ );
354
+ return calls;
355
+ }
356
+
357
+ const REF_ID_PATTERN = /\b(turn\d+(?:search|fetch|view)\d+)\b/g;
358
+
359
+ function extractRefIds(text: string): Record<string, string> {
360
+ const refs: Record<string, string> = {};
361
+ for (const match of text.matchAll(REF_ID_PATTERN)) {
362
+ const refId = match[1];
363
+ if (refId) refs[refId] = refId;
364
+ }
365
+ return refs;
366
+ }
367
+
368
+ function extractMarkdownCitations(text: string): CodexCitation[] {
369
+ const citations = new Map<string, CodexCitation>();
370
+ const markdownLinkPattern = /\[([^\]\n]{1,200})\]\((https?:\/\/[^)\s]+)\)/g;
371
+ for (const match of text.matchAll(markdownLinkPattern)) {
372
+ const title = match[1]?.trim();
373
+ const url = match[2]?.trim();
374
+ if (!url || citations.has(url)) continue;
375
+ citations.set(url, { title: title || url, url, startIndex: match.index });
376
+ }
377
+ return [...citations.values()];
378
+ }
@@ -0,0 +1,41 @@
1
+ export type SearchContextSize = "low" | "medium" | "high";
2
+ export type Freshness = "live" | "cached" | "indexed";
3
+ export type StandaloneExternalWebAccess = boolean | "indexed";
4
+ export type ResponseLength = "short" | "medium" | "long";
5
+
6
+ export interface CodexCitation {
7
+ title?: string;
8
+ url: string;
9
+ startIndex?: number;
10
+ endIndex?: number;
11
+ }
12
+
13
+ export interface CodexSearchCall {
14
+ id?: string;
15
+ status?: string;
16
+ query?: string;
17
+ url?: string;
18
+ actionType?: string;
19
+ refId?: string;
20
+ }
21
+
22
+ export interface CodexWebSearchResult {
23
+ responseId?: string;
24
+ model: string;
25
+ text: string;
26
+ searchCalls: CodexSearchCall[];
27
+ citations: CodexCitation[];
28
+ refIds?: Record<string, string>;
29
+ usage?: {
30
+ inputTokens?: number;
31
+ outputTokens?: number;
32
+ totalTokens?: number;
33
+ };
34
+ encryptedOutput?: string;
35
+ }
36
+
37
+ export interface CodexModel {
38
+ id: string;
39
+ name?: string;
40
+ isDefault?: boolean;
41
+ }
@@ -0,0 +1,74 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ export interface RefStore {
5
+ resolveRefId(url: string): string | undefined;
6
+ remember(url: string, refId: string): Promise<void>;
7
+ load(sessionDir: string): Promise<void>;
8
+ }
9
+
10
+ interface StoredRefs {
11
+ urlToRefId: Record<string, string>;
12
+ }
13
+
14
+ const STORE_FILE = "pi-codex-search-refs.json";
15
+ const PERSIST_QUEUES = new Map<string, Promise<void>>();
16
+
17
+ export function createRefStore(): RefStore {
18
+ const urlToRefId = new Map<string, string>();
19
+ let sessionDir: string | undefined;
20
+
21
+ return {
22
+ resolveRefId(url: string): string | undefined {
23
+ return urlToRefId.get(url) ?? undefined;
24
+ },
25
+
26
+ async remember(url: string, refId: string): Promise<void> {
27
+ urlToRefId.set(url, refId);
28
+ if (sessionDir) {
29
+ await enqueuePersist(sessionDir, urlToRefId);
30
+ }
31
+ },
32
+
33
+ async load(dir: string): Promise<void> {
34
+ sessionDir = dir;
35
+ const stored = await loadStored(dir);
36
+ for (const [url, refId] of Object.entries(stored.urlToRefId)) {
37
+ urlToRefId.set(url, refId);
38
+ }
39
+ },
40
+ };
41
+ }
42
+
43
+ async function loadStored(dir: string): Promise<StoredRefs> {
44
+ try {
45
+ const raw = await readFile(join(dir, STORE_FILE), "utf-8");
46
+ const parsed = JSON.parse(raw) as StoredRefs;
47
+ return { urlToRefId: parsed.urlToRefId ?? {} };
48
+ } catch (error) {
49
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return { urlToRefId: {} };
50
+ throw error;
51
+ }
52
+ }
53
+
54
+ async function enqueuePersist(dir: string, map: Map<string, string>): Promise<void> {
55
+ const previous = PERSIST_QUEUES.get(dir) ?? Promise.resolve();
56
+ const next = previous.catch(() => undefined).then(() => persistMerged(dir, map));
57
+ PERSIST_QUEUES.set(dir, next);
58
+ try {
59
+ await next;
60
+ } finally {
61
+ if (PERSIST_QUEUES.get(dir) === next) PERSIST_QUEUES.delete(dir);
62
+ }
63
+ }
64
+
65
+ async function persistMerged(dir: string, map: Map<string, string>): Promise<void> {
66
+ const current = await loadStored(dir);
67
+ const stored: StoredRefs = {
68
+ urlToRefId: {
69
+ ...current.urlToRefId,
70
+ ...Object.fromEntries(map),
71
+ },
72
+ };
73
+ await writeFile(join(dir, STORE_FILE), JSON.stringify(stored, null, 2), "utf-8");
74
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Codex-faithful HTTP transport.
3
+ *
4
+ * Builds the same headers and cookie behavior used by the codex Rust client:
5
+ * - originator: codex_cli_rs
6
+ * - User-Agent: codex_cli_rs/{ver} (os ver; arch) terminal
7
+ * - Authorization: Bearer {token}
8
+ * - ChatGPT-Account-ID: {account_id}
9
+ * - ChatGPT Cloudflare cookie store on all outbound/inbound requests
10
+ */
11
+
12
+ import { getCodexOriginator, buildCodexUserAgent } from "./ua.ts";
13
+ import { wrapFetchWithCookies, type FetchLike } from "./cookies.ts";
14
+
15
+ export interface TransportOptions {
16
+ token: string;
17
+ accountId: string;
18
+ baseUrl?: string;
19
+ fetchImpl?: FetchLike;
20
+ }
21
+
22
+ export const DEFAULT_BASE_URL = "https://chatgpt.com/backend-api";
23
+ export const DEFAULT_CLIENT_VERSION = "1.0.0";
24
+
25
+ export interface CodexTransport {
26
+ fetch: FetchLike;
27
+ baseUrl: string;
28
+ token: string;
29
+ accountId: string;
30
+ buildHeaders(accept: string): Headers;
31
+ resolveEndpoint(path: "models" | "responses"): string;
32
+ resolveSearchEndpoint(): string;
33
+ }
34
+
35
+ export function normalizeCodexBaseUrl(baseUrl: string | undefined): string {
36
+ const raw = baseUrl?.trim() ? baseUrl : DEFAULT_BASE_URL;
37
+ let normalized = raw.replace(/\/+$/, "");
38
+ if (normalized.endsWith("/codex/responses")) {
39
+ normalized = normalized.slice(0, -"/codex/responses".length);
40
+ }
41
+ if (normalized.endsWith("/codex")) {
42
+ normalized = normalized.slice(0, -"/codex".length);
43
+ }
44
+ return normalized;
45
+ }
46
+
47
+ function isOpenAiRootBaseUrl(baseUrl: string): boolean {
48
+ try {
49
+ const url = new URL(baseUrl);
50
+ return url.hostname === "api.openai.com" && (url.pathname === "" || url.pathname === "/");
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ export function resolveCodexEndpoint(
57
+ baseUrl: string | undefined,
58
+ path: "models" | "responses",
59
+ ): string {
60
+ return `${normalizeCodexBaseUrl(baseUrl)}/codex/${path}`;
61
+ }
62
+
63
+ export function resolveCodexSearchEndpoint(baseUrl: string | undefined): string {
64
+ const raw = baseUrl?.trim() ? baseUrl : DEFAULT_BASE_URL;
65
+ let normalized = raw.replace(/\/+$/, "");
66
+ if (normalized.endsWith("/codex/responses")) {
67
+ normalized = normalized.slice(0, -"/responses".length);
68
+ }
69
+ if (normalized.endsWith("/codex/models")) {
70
+ normalized = normalized.slice(0, -"/models".length);
71
+ }
72
+ if (normalized.endsWith("/codex/alpha/search") || normalized.endsWith("/alpha/search")) {
73
+ return normalized;
74
+ }
75
+ if (normalized.endsWith("/codex")) return `${normalized}/alpha/search`;
76
+ if (normalized.endsWith("/v1")) return `${normalized}/alpha/search`;
77
+ if (isOpenAiRootBaseUrl(normalized)) return `${normalized}/v1/alpha/search`;
78
+ return `${normalized}/codex/alpha/search`;
79
+ }
80
+
81
+ export function createTransport(options: TransportOptions): CodexTransport {
82
+ const baseUrl = normalizeCodexBaseUrl(options.baseUrl);
83
+ const rawFetch: FetchLike = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
84
+ const fetch = wrapFetchWithCookies(rawFetch);
85
+
86
+ return {
87
+ fetch,
88
+ baseUrl,
89
+ token: options.token,
90
+ accountId: options.accountId,
91
+ buildHeaders(accept: string): Headers {
92
+ const headers = new Headers();
93
+ headers.set("Authorization", `Bearer ${options.token}`);
94
+ headers.set("chatgpt-account-id", options.accountId);
95
+ headers.set("originator", getCodexOriginator());
96
+ headers.set("accept", accept);
97
+ if (accept === "text/event-stream") {
98
+ headers.set("content-type", "application/json");
99
+ }
100
+ headers.set("User-Agent", buildCodexUserAgent());
101
+ return headers;
102
+ },
103
+ resolveEndpoint(path) {
104
+ return resolveCodexEndpoint(baseUrl, path);
105
+ },
106
+ resolveSearchEndpoint() {
107
+ return resolveCodexSearchEndpoint(baseUrl);
108
+ },
109
+ };
110
+ }
package/src/ua.ts ADDED
@@ -0,0 +1,67 @@
1
+ import { release } from "node:os";
2
+
3
+ export const DEFAULT_CODEX_VERSION = "0.143.0";
4
+ export const DEFAULT_CODEX_ORIGINATOR = "codex_cli_rs";
5
+
6
+ function mapPlatform(platform: string): string {
7
+ switch (platform) {
8
+ case "darwin":
9
+ return "Mac OS";
10
+ case "win32":
11
+ return "Windows";
12
+ case "linux":
13
+ return "Linux";
14
+ case "freebsd":
15
+ return "FreeBSD";
16
+ default:
17
+ return platform;
18
+ }
19
+ }
20
+
21
+ function mapArch(arch: string): string {
22
+ switch (arch) {
23
+ case "x64":
24
+ return "x86_64";
25
+ case "arm64":
26
+ return "arm64";
27
+ case "arm":
28
+ return "arm";
29
+ default:
30
+ return arch;
31
+ }
32
+ }
33
+
34
+ function terminalUserAgent(): string {
35
+ const program = process.env.TERM_PROGRAM;
36
+ if (program) {
37
+ const version = process.env.TERM_PROGRAM_VERSION;
38
+ const suffix = version ? ` ${version}` : "";
39
+ return `${program}${suffix}`.trim();
40
+ }
41
+ if (process.env.WT_SESSION) {
42
+ return "WindowsTerminal";
43
+ }
44
+ if (process.env.KITTY_WINDOW_ID) {
45
+ return "kitty";
46
+ }
47
+ if (process.env.TMUX) {
48
+ return "tmux";
49
+ }
50
+ const term = process.env.TERM;
51
+ if (term && term !== "dumb") {
52
+ return term;
53
+ }
54
+ return "unknown";
55
+ }
56
+
57
+ export function buildCodexUserAgent(version = DEFAULT_CODEX_VERSION): string {
58
+ const osType = mapPlatform(process.platform);
59
+ const osVersion = release();
60
+ const arch = mapArch(process.arch);
61
+ const terminal = terminalUserAgent();
62
+ return `${DEFAULT_CODEX_ORIGINATOR}/${version} (${osType} ${osVersion}; ${arch}) ${terminal}`;
63
+ }
64
+
65
+ export function getCodexOriginator(): string {
66
+ return DEFAULT_CODEX_ORIGINATOR;
67
+ }