@mewbleh/purrx 1.0.11 → 1.0.13

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.13",
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,38 @@
1
1
  import { API_BASE, CHATGPT_BASE } from "../config.js";
2
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
+ }
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
+ "User-Agent": "codex_cli_rs/0.0.0 (purrx)",
20
30
  };
21
31
  if (authInfo.accountId) {
22
- headers["chatgpt-account-id"] = authInfo.accountId;
32
+ // Casing matches the official client (BearerAuthProvider).
33
+ headers["ChatGPT-Account-ID"] = authInfo.accountId;
23
34
  }
35
+ if (sessionId) headers["session-id"] = sessionId;
24
36
  return { url: `${CHATGPT_BASE}/responses`, headers };
25
37
  }
26
38
  // API-key mode.
@@ -44,7 +56,7 @@ function buildRequest(authInfo) {
44
56
  * @param {import("../types.js").HistoryItem[]} req.input
45
57
  * @param {import("../types.js").ToolDefinition[]} [req.tools]
46
58
  * @param {string} [req.instructions]
47
- * @param {string} [req.sessionId] stable id used for prompt caching
59
+ * @param {string} [req.sessionId] stable id, sent as session-id + prompt_cache_key
48
60
  * @param {{ onText?: (delta: string) => void, onEvent?: (event: any) => void }} [handlers]
49
61
  * @returns {Promise<any>}
50
62
  */
@@ -52,26 +64,44 @@ export async function streamResponse(
52
64
  { authInfo, model, input, tools, instructions, sessionId },
53
65
  handlers = {}
54
66
  ) {
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.
67
+ const { url, headers } = buildRequest(authInfo, sessionId);
68
+
69
+ // Request body matching the official Codex ResponsesApiRequest. These fields
70
+ // are always present in the real client; sending a stripped-down body is what
71
+ // triggers the backend's misleading "model is not supported" rejection.
63
72
  /** @type {Record<string, any>} */
64
73
  const payload = {
65
74
  model,
66
75
  instructions: instructions || "",
67
76
  input,
68
- stream: true,
77
+ tool_choice: "auto",
78
+ parallel_tool_calls: false,
69
79
  store: false,
80
+ stream: true,
81
+ include: [],
70
82
  };
71
83
 
84
+ if (isReasoningModel(model)) {
85
+ payload.reasoning = { effort: "medium", summary: "auto" };
86
+ payload.include = ["reasoning.encrypted_content"];
87
+ }
88
+
89
+ if (sessionId) payload.prompt_cache_key = sessionId;
90
+
72
91
  if (tools && tools.length) {
73
92
  payload.tools = tools;
74
- payload.tool_choice = "auto";
93
+ }
94
+
95
+ // Debug: dump the exact outgoing request when PURRX_DEBUG is set. Auth tokens
96
+ // are redacted. This is the fastest way to diagnose backend rejections.
97
+ if (process.env.PURRX_DEBUG) {
98
+ const safeHeaders = { ...headers };
99
+ if (safeHeaders.Authorization) safeHeaders.Authorization = "Bearer <redacted>";
100
+ console.error(`\n[purrx] POST ${url}`);
101
+ console.error(`[purrx] headers: ${JSON.stringify(safeHeaders)}`);
102
+ console.error(
103
+ `[purrx] body: ${JSON.stringify({ ...payload, input: `<${input.length} items>` })}`
104
+ );
75
105
  }
76
106
 
77
107
  const resp = await fetch(url, {
@@ -82,6 +112,9 @@ export async function streamResponse(
82
112
 
83
113
  if (!resp.ok || !resp.body) {
84
114
  const text = await resp.text().catch(() => "");
115
+ if (process.env.PURRX_DEBUG) {
116
+ console.error(`[purrx] response ${resp.status}: ${text}`);
117
+ }
85
118
  throw new Error(`API request failed (${resp.status}): ${text}`);
86
119
  }
87
120
 
@@ -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) {