@mewbleh/purrx 1.0.11 → 1.0.14

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/bin/purrx.js CHANGED
@@ -335,6 +335,15 @@ async function main() {
335
335
  case "status":
336
336
  cmdStatus();
337
337
  break;
338
+ case "version":
339
+ case "--version":
340
+ case "-v": {
341
+ const { createRequire } = await import("node:module");
342
+ const require = createRequire(import.meta.url);
343
+ const pkg = require("../package.json");
344
+ console.log(`purrx ${pkg.version}`);
345
+ break;
346
+ }
338
347
  case "help":
339
348
  case "--help":
340
349
  case "-h":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mewbleh/purrx",
3
- "version": "1.0.11",
3
+ "version": "1.0.14",
4
4
  "description": "purrx, a lightweight AI coding agent for your terminal",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/api/client.js CHANGED
@@ -1,26 +1,40 @@
1
- import { API_BASE, CHATGPT_BASE } from "../config.js";
1
+ import { API_BASE, CHATGPT_BASE, CODEX_CLI_VERSION } from "../config.js";
2
+
3
+ // Models that take the Responses API reasoning fields (gpt-5 family, o-series,
4
+ // and codex variants). The official Codex client always sends a reasoning block
5
+ // for these and requests encrypted reasoning back since store is false.
6
+ function isReasoningModel(model) {
7
+ return /^(gpt-5|o\d|codex)/i.test(model) || /-codex\b/i.test(model);
8
+ }
2
9
 
3
10
  // Builds the request URL and headers for the resolved auth mode.
11
+ //
12
+ // The ChatGPT-codex header set mirrors the official Codex CLI
13
+ // (codex-rs default_client + BearerAuthProvider + build_session_headers):
14
+ // Authorization, ChatGPT-Account-ID, originator, User-Agent,
15
+ // OpenAI-Beta, session-id.
4
16
  /**
5
17
  * @param {import("../types.js").AuthInfo} authInfo
18
+ * @param {string} [sessionId]
6
19
  * @returns {{ url: string, headers: Record<string, string> }}
7
20
  */
8
- function buildRequest(authInfo) {
21
+ function buildRequest(authInfo, sessionId) {
9
22
  if (authInfo.mode === "chatgpt") {
10
- // These headers match the proven-working ChatGPT-codex request shape.
11
- // The OpenAI-Beta header is required; the backend rejects requests without
12
- // it (with a misleading "model not supported" error). Do NOT add an
13
- // `originator` header here: the codex backend does not expect it on this
14
- // path and its presence can cause the request to be rejected.
15
23
  /** @type {Record<string, string>} */
16
24
  const headers = {
17
25
  "Content-Type": "application/json",
18
26
  Authorization: `Bearer ${authInfo.accessToken}`,
19
27
  "OpenAI-Beta": "responses=experimental",
28
+ originator: "codex_cli_rs",
29
+ // The codex backend requires this version header to accept the request.
30
+ version: CODEX_CLI_VERSION,
31
+ "User-Agent": `codex_cli_rs/${CODEX_CLI_VERSION} (purrx)`,
20
32
  };
21
33
  if (authInfo.accountId) {
22
- headers["chatgpt-account-id"] = authInfo.accountId;
34
+ // Casing matches the official client (BearerAuthProvider).
35
+ headers["ChatGPT-Account-ID"] = authInfo.accountId;
23
36
  }
37
+ if (sessionId) headers["session-id"] = sessionId;
24
38
  return { url: `${CHATGPT_BASE}/responses`, headers };
25
39
  }
26
40
  // API-key mode.
@@ -44,7 +58,7 @@ function buildRequest(authInfo) {
44
58
  * @param {import("../types.js").HistoryItem[]} req.input
45
59
  * @param {import("../types.js").ToolDefinition[]} [req.tools]
46
60
  * @param {string} [req.instructions]
47
- * @param {string} [req.sessionId] stable id used for prompt caching
61
+ * @param {string} [req.sessionId] stable id, sent as session-id + prompt_cache_key
48
62
  * @param {{ onText?: (delta: string) => void, onEvent?: (event: any) => void }} [handlers]
49
63
  * @returns {Promise<any>}
50
64
  */
@@ -52,26 +66,44 @@ export async function streamResponse(
52
66
  { authInfo, model, input, tools, instructions, sessionId },
53
67
  handlers = {}
54
68
  ) {
55
- const { url, headers } = buildRequest(authInfo);
56
-
57
- // Minimal request shape that the ChatGPT-codex backend accepts. The backend
58
- // requires `instructions` to be present (a string, even if empty) and
59
- // `store: false`. We deliberately avoid adding reasoning/include/cache fields
60
- // here: the backend rejects requests it doesn't recognize as a valid Codex
61
- // session with a misleading "model not supported" error, and the known-good
62
- // client sends only these fields.
69
+ const { url, headers } = buildRequest(authInfo, sessionId);
70
+
71
+ // Request body matching the official Codex ResponsesApiRequest. These fields
72
+ // are always present in the real client; sending a stripped-down body is what
73
+ // triggers the backend's misleading "model is not supported" rejection.
63
74
  /** @type {Record<string, any>} */
64
75
  const payload = {
65
76
  model,
66
77
  instructions: instructions || "",
67
78
  input,
68
- stream: true,
79
+ tool_choice: "auto",
80
+ parallel_tool_calls: false,
69
81
  store: false,
82
+ stream: true,
83
+ include: [],
70
84
  };
71
85
 
86
+ if (isReasoningModel(model)) {
87
+ payload.reasoning = { effort: "medium", summary: "auto" };
88
+ payload.include = ["reasoning.encrypted_content"];
89
+ }
90
+
91
+ if (sessionId) payload.prompt_cache_key = sessionId;
92
+
72
93
  if (tools && tools.length) {
73
94
  payload.tools = tools;
74
- payload.tool_choice = "auto";
95
+ }
96
+
97
+ // Debug: dump the exact outgoing request when PURRX_DEBUG is set. Auth tokens
98
+ // are redacted. This is the fastest way to diagnose backend rejections.
99
+ if (process.env.PURRX_DEBUG) {
100
+ const safeHeaders = { ...headers };
101
+ if (safeHeaders.Authorization) safeHeaders.Authorization = "Bearer <redacted>";
102
+ console.error(`\n[purrx] POST ${url}`);
103
+ console.error(`[purrx] headers: ${JSON.stringify(safeHeaders)}`);
104
+ console.error(
105
+ `[purrx] body: ${JSON.stringify({ ...payload, input: `<${input.length} items>` })}`
106
+ );
75
107
  }
76
108
 
77
109
  const resp = await fetch(url, {
@@ -82,6 +114,9 @@ export async function streamResponse(
82
114
 
83
115
  if (!resp.ok || !resp.body) {
84
116
  const text = await resp.text().catch(() => "");
117
+ if (process.env.PURRX_DEBUG) {
118
+ console.error(`[purrx] response ${resp.status}: ${text}`);
119
+ }
85
120
  throw new Error(`API request failed (${resp.status}): ${text}`);
86
121
  }
87
122
 
@@ -111,7 +111,7 @@ export async function refreshTokens(refreshToken) {
111
111
  client_id: OAUTH_CLIENT_ID,
112
112
  grant_type: "refresh_token",
113
113
  refresh_token: refreshToken,
114
- scope: "openid profile email",
114
+ scope: "openid profile email offline_access",
115
115
  }),
116
116
  });
117
117
  if (!resp.ok) {
package/src/config.js CHANGED
@@ -13,6 +13,12 @@ export const OAUTH_SCOPE =
13
13
  export const API_BASE = "https://api.openai.com/v1";
14
14
  export const CHATGPT_BASE = "https://chatgpt.com/backend-api/codex";
15
15
 
16
+ // The official Codex client sends a `version` header (its CLI version) on every
17
+ // request. The ChatGPT-codex backend uses it to recognize a legitimate Codex
18
+ // client; omitting it causes a generic "model not supported" rejection.
19
+ // Override with PURRX_CODEX_VERSION to match your installed Codex CLI.
20
+ export const CODEX_CLI_VERSION = process.env.PURRX_CODEX_VERSION || "0.5.0";
21
+
16
22
  // Default model. gpt-5-codex is the default Codex coding model and works on
17
23
  // both API-key and ChatGPT-account auth. Override with PURRX_MODEL.
18
24
  export const DEFAULT_MODEL = process.env.PURRX_MODEL || "gpt-5-codex";