@mewbleh/purrx 1.0.14 → 1.0.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mewbleh/purrx",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
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
@@ -9,10 +9,13 @@ function isReasoningModel(model) {
9
9
 
10
10
  // Builds the request URL and headers for the resolved auth mode.
11
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.
12
+ // IMPORTANT: For the ChatGPT-codex backend we deliberately send the MINIMAL
13
+ // header set that the proven-working community proxy uses
14
+ // (Authorization + ChatGPT-Account-ID + OpenAI-Beta). We do NOT send
15
+ // `originator`/`version`/`session-id`: claiming to be the official Codex CLI
16
+ // makes the backend apply stricter validation that rejects the request with a
17
+ // misleading "model not supported" error. Set PURRX_CODEX_HEADERS=1 to opt into
18
+ // the full Codex-style header set for experimentation.
16
19
  /**
17
20
  * @param {import("../types.js").AuthInfo} authInfo
18
21
  * @param {string} [sessionId]
@@ -25,16 +28,17 @@ function buildRequest(authInfo, sessionId) {
25
28
  "Content-Type": "application/json",
26
29
  Authorization: `Bearer ${authInfo.accessToken}`,
27
30
  "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)`,
32
31
  };
33
32
  if (authInfo.accountId) {
34
- // Casing matches the official client (BearerAuthProvider).
35
- headers["ChatGPT-Account-ID"] = authInfo.accountId;
33
+ headers["chatgpt-account-id"] = authInfo.accountId;
34
+ }
35
+ // Opt-in: full Codex-CLI-style headers (originator/version/session-id).
36
+ if (process.env.PURRX_CODEX_HEADERS) {
37
+ headers.originator = "codex_cli_rs";
38
+ headers.version = CODEX_CLI_VERSION;
39
+ headers["User-Agent"] = `codex_cli_rs/${CODEX_CLI_VERSION} (purrx)`;
40
+ if (sessionId) headers["session-id"] = sessionId;
36
41
  }
37
- if (sessionId) headers["session-id"] = sessionId;
38
42
  return { url: `${CHATGPT_BASE}/responses`, headers };
39
43
  }
40
44
  // API-key mode.
@@ -67,31 +71,45 @@ export async function streamResponse(
67
71
  handlers = {}
68
72
  ) {
69
73
  const { url, headers } = buildRequest(authInfo, sessionId);
74
+ const isChatGpt = authInfo.mode === "chatgpt";
75
+
76
+ // Minimal body matching the proven-working community proxy: instructions,
77
+ // store, stream, plus input/tools. We avoid reasoning/include/prompt_cache_key
78
+ // and server-side tools (web_search) on the ChatGPT path, since those can
79
+ // trigger the backend's misleading "model not supported" rejection.
80
+ // Set PURRX_FULL_BODY=1 to send the richer Codex-style body.
81
+ const fullBody = !isChatGpt || process.env.PURRX_FULL_BODY;
70
82
 
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.
74
83
  /** @type {Record<string, any>} */
75
84
  const payload = {
76
85
  model,
77
86
  instructions: instructions || "",
78
87
  input,
79
- tool_choice: "auto",
80
- parallel_tool_calls: false,
81
88
  store: false,
82
89
  stream: true,
83
- include: [],
84
90
  };
85
91
 
86
- if (isReasoningModel(model)) {
87
- payload.reasoning = { effort: "medium", summary: "auto" };
88
- payload.include = ["reasoning.encrypted_content"];
92
+ if (fullBody) {
93
+ payload.tool_choice = "auto";
94
+ payload.parallel_tool_calls = false;
95
+ payload.include = [];
96
+ if (isReasoningModel(model)) {
97
+ payload.reasoning = { effort: "medium", summary: "auto" };
98
+ payload.include = ["reasoning.encrypted_content"];
99
+ }
100
+ if (sessionId) payload.prompt_cache_key = sessionId;
89
101
  }
90
102
 
91
- if (sessionId) payload.prompt_cache_key = sessionId;
92
-
93
103
  if (tools && tools.length) {
94
- payload.tools = tools;
104
+ // On the ChatGPT path, only send function tools (drop server-side tools
105
+ // like web_search which the codex backend may reject).
106
+ const filtered = isChatGpt
107
+ ? tools.filter((t) => t.type === "function")
108
+ : tools;
109
+ if (filtered.length) {
110
+ payload.tools = filtered;
111
+ if (!payload.tool_choice) payload.tool_choice = "auto";
112
+ }
95
113
  }
96
114
 
97
115
  // Debug: dump the exact outgoing request when PURRX_DEBUG is set. Auth tokens
package/src/auth/login.js CHANGED
@@ -25,6 +25,9 @@ function buildAuthorizeUrl(redirectUri, pkce, state) {
25
25
  id_token_add_organizations: "true",
26
26
  codex_cli_simplified_flow: "true",
27
27
  state,
28
+ // The official Codex authorize request includes the originator; the issued
29
+ // token's Codex entitlement can depend on it.
30
+ originator: "codex_cli_rs",
28
31
  });
29
32
  return `${OAUTH_ISSUER}/oauth/authorize?${params.toString()}`;
30
33
  }
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import {
3
3
  authFilePath,
4
+ resolveAuthFileForRead,
4
5
  purrxHome,
5
6
  OAUTH_CLIENT_ID,
6
7
  OAUTH_ISSUER,
@@ -15,7 +16,7 @@ import {
15
16
 
16
17
  /** @returns {AuthDotJson|null} */
17
18
  export function readAuth() {
18
- const file = authFilePath();
19
+ const file = resolveAuthFileForRead();
19
20
  try {
20
21
  const raw = fs.readFileSync(file, "utf8");
21
22
  return JSON.parse(raw);
package/src/config.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import path from "node:path";
2
+ import os from "node:os";
3
+ import fs from "node:fs";
2
4
  import { dataHome } from "./platform.js";
3
5
 
4
6
  // OAuth constants taken from the official OpenAI Codex CLI so we can reuse the
@@ -43,6 +45,26 @@ export function authFilePath() {
43
45
  return path.join(purrxHome(), "auth.json");
44
46
  }
45
47
 
48
+ // Resolves the auth.json to READ from. Prefers purrx's own file, then falls
49
+ // back to the official Codex CLI locations (CODEX_HOME, ~/.codex). This lets
50
+ // purrx reuse a known-good token minted by `codex login`.
51
+ export function resolveAuthFileForRead() {
52
+ const candidates = [
53
+ process.env.PURRX_HOME && path.join(process.env.PURRX_HOME, "auth.json"),
54
+ path.join(purrxHome(), "auth.json"),
55
+ process.env.CODEX_HOME && path.join(process.env.CODEX_HOME, "auth.json"),
56
+ path.join(os.homedir(), ".codex", "auth.json"),
57
+ ].filter(Boolean);
58
+ for (const candidate of candidates) {
59
+ try {
60
+ if (fs.existsSync(/** @type {string} */ (candidate))) return candidate;
61
+ } catch {
62
+ // ignore
63
+ }
64
+ }
65
+ return authFilePath();
66
+ }
67
+
46
68
  export function sessionsDir() {
47
69
  return path.join(purrxHome(), "sessions");
48
70
  }