pi-codex-search 0.1.1 → 0.1.3
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/README.md +198 -50
- package/index.ts +922 -93
- package/package.json +10 -4
- package/scripts/codex-e2e.ts +797 -0
- package/src/codex.ts +90 -352
- package/src/command.ts +564 -0
- package/src/config.ts +287 -0
- package/src/cookies.ts +131 -0
- package/src/errors.ts +56 -0
- package/src/modes/responses.ts +310 -0
- package/src/modes/standalone.ts +378 -0
- package/src/modes/types.ts +41 -0
- package/src/ref-store.ts +74 -0
- package/src/transport.ts +110 -0
- package/src/ua.ts +67 -0
|
@@ -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
|
+
}
|
package/src/ref-store.ts
ADDED
|
@@ -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
|
+
}
|
package/src/transport.ts
ADDED
|
@@ -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
|
+
}
|