@galaxy-yearn/codex-deepseek-gateway 0.1.4 → 0.1.5

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 CHANGED
@@ -1,59 +1,49 @@
1
1
  # Codex DeepSeek Gateway
2
2
 
3
- A small local gateway for using DeepSeek models from Codex.
3
+ A lightweight local gateway for using DeepSeek models in Codex.
4
4
 
5
- Codex keeps sending OpenAI `Responses API` requests to a local endpoint. The gateway converts those requests to DeepSeek-compatible `Chat Completions`, calls DeepSeek, then converts the result back to Responses JSON or streaming `response.*` events.
5
+ Codex keeps using the OpenAI `Responses API` wire format. The gateway translates requests to DeepSeek-compatible `Chat Completions`, calls DeepSeek, then translates the result back to Responses JSON or `response.*` SSE events.
6
6
 
7
- NPM package: [@galaxy-yearn/codex-deepseek-gateway](https://www.npmjs.com/package/@galaxy-yearn/codex-deepseek-gateway)
8
-
9
- The runtime uses only the Node.js standard library. It installs under `~/.codex/deepseek-gateway`, runs as a detached background process, and does not edit your existing Codex config.
10
-
11
- Use this when you want Codex to stay on its normal Responses API path while the actual model is DeepSeek, including DeepSeek thinking output, function calls, local session resume help, and optional web search emulation through Tavily and Firecrawl.
7
+ Package: [@galaxy-yearn/codex-deepseek-gateway](https://www.npmjs.com/package/@galaxy-yearn/codex-deepseek-gateway)
12
8
 
13
9
  ## Requirements
14
10
 
15
- - Node.js 20 or newer
11
+ - Node.js 22 or newer
16
12
  - A DeepSeek API key
17
- - Codex configured from `~/.codex/config.toml`
13
+ - Codex CLI
18
14
 
19
15
  ## Install
20
16
 
21
- Run:
22
-
23
17
  ```sh
24
- npx @galaxy-yearn/codex-deepseek-gateway install
25
- ```
26
-
27
- This copies the runtime to:
28
-
29
- ```text
30
- ~/.codex/deepseek-gateway
18
+ npm install -g @galaxy-yearn/codex-deepseek-gateway
19
+ codex-deepseek-gateway --version
20
+ codex-deepseek-gateway install
31
21
  ```
32
22
 
33
- It creates two local config files:
23
+ The runtime is copied to `~/.codex/deepseek-gateway`. Put your DeepSeek API key in:
34
24
 
35
25
  ```text
36
26
  ~/.codex/deepseek-gateway/config/gateway.local.json
37
- ~/.codex/deepseek-gateway/config/model-aliases.json
38
27
  ```
39
28
 
40
- Put your DeepSeek API key in `gateway.local.json`:
41
-
42
29
  ```json
43
- "upstreamApiKey": "sk-..."
30
+ {
31
+ "upstreamApiKey": "sk-..."
32
+ }
44
33
  ```
45
34
 
46
- `model-aliases.json` controls the gateway-facing model IDs exposed on `GET /v1/models` and used by the `new` and `sessions` pickers. Edit it only if you want to add or rename model aliases.
47
-
48
- If the key is already configured, `install` also starts the gateway. If this is your first install, add the key and then run:
35
+ `install` preserves an existing `gateway.local.json`. If this is your first install, add the key and start the gateway:
49
36
 
50
37
  ```sh
51
- npx @galaxy-yearn/codex-deepseek-gateway start
38
+ codex-deepseek-gateway start
39
+ codex-deepseek-gateway status
52
40
  ```
53
41
 
54
- ## Configure Codex
42
+ `status` should show `"reachable": true`.
43
+
44
+ ## Codex Config
55
45
 
56
- Edit `~/.codex/config.toml`:
46
+ Add this provider to `~/.codex/config.toml`:
57
47
 
58
48
  ```toml
59
49
  model_provider = "deepseek-gateway"
@@ -68,225 +58,122 @@ base_url = "http://127.0.0.1:3000/v1"
68
58
  wire_api = "responses"
69
59
  ```
70
60
 
71
- Use `deepseek-v4-pro` instead of `deepseek-v4-flash` if you want the pro model.
72
-
73
- Restart Codex after changing `config.toml`.
61
+ Use `deepseek-v4-pro` if you want the pro model. Restart Codex after editing `config.toml`.
74
62
 
75
- ## Start And Verify
63
+ ## Usage
76
64
 
77
- Start, stop, and inspect the local background process:
65
+ Start a new Codex conversation with gateway overrides:
78
66
 
79
67
  ```sh
80
- npx @galaxy-yearn/codex-deepseek-gateway start
81
- npx @galaxy-yearn/codex-deepseek-gateway stop
82
- npx @galaxy-yearn/codex-deepseek-gateway status
68
+ codex-deepseek-gateway new
83
69
  ```
84
70
 
85
- `status` should show `"reachable": true`.
86
-
87
- Run `doctor` to check the active Codex config and the DeepSeek request shape the gateway will send:
71
+ Resume a Codex session from the current project:
88
72
 
89
73
  ```sh
90
- npx @galaxy-yearn/codex-deepseek-gateway doctor
91
- ```
92
-
93
- Important fields:
94
-
95
- - `codexConfigUsingGateway` should be `true`
96
- - `codexModel` should be your gateway model, for example `deepseek-v4-pro`
97
- - `codexReasoningEffort` should match `model_reasoning_effort`
98
- - `deepseekThinking` shows the DeepSeek `thinking` payload
99
- - `deepseekReasoningEffort` shows the DeepSeek effort sent upstream
100
- - `reasoningDisplayMode` shows whether reasoning is shown as `summary`, `hidden`, or `disabled`
101
- - `tavilyWebSearchReady` and `firecrawlWebFetchReady` show whether optional web search backends are usable
102
-
103
- Avoid running Codex through a proxy while using the local gateway. Some proxy clients intercept `http://127.0.0.1:3000` and can break local gateway requests.
104
-
105
- ## Models
106
-
107
- The installed `config/model-aliases.json` starts with:
108
-
109
- ```json
110
- {
111
- "deepseek-v4-flash": {
112
- "model": "deepseek-v4-flash",
113
- "thinking": "auto"
114
- },
115
- "deepseek-v4-pro": {
116
- "model": "deepseek-v4-pro",
117
- "thinking": "auto"
118
- }
119
- }
120
- ```
121
-
122
- The gateway serves these aliases directly on `GET /v1/models`. If you also want to merge DeepSeek's upstream `/models` list, set this in `gateway.local.json`:
123
-
124
- ```json
125
- "fetchUpstreamModels": true
126
- ```
127
-
128
- Whether Codex TUI `/model` shows custom provider models depends on the Codex client build. `config.toml`, `new`, and `sessions` remain the reliable ways to choose a model.
129
-
130
- ## Reasoning
131
-
132
- Codex reasoning effort maps to DeepSeek V4 thinking mode:
133
-
134
- | Codex `model_reasoning_effort` | DeepSeek request |
135
- | --- | --- |
136
- | `low` | `thinking.type = disabled` |
137
- | `medium` | `thinking.type = enabled`, `reasoning_effort = high` |
138
- | `high` | `thinking.type = enabled`, `reasoning_effort = high` |
139
- | `xhigh` | `thinking.type = enabled`, `reasoning_effort = max` |
140
-
141
- `model_supports_reasoning_summaries = true` and `model_reasoning_summary = "auto"` tell Codex to use its normal thinking UI path. When DeepSeek returns `reasoning_content`, the gateway keeps the raw text as `reasoning_text` and mirrors it into `summary_text` with a `Reasoning` heading for Codex TUI compatibility.
142
-
143
- When thinking is enabled, the gateway intentionally buffers visible assistant text and tool-call output until the upstream response completes, then emits reasoning first. This improves thinking order in Codex at the cost of higher first-token latency.
144
-
145
- ## Web Search
146
-
147
- Web search is off by default. To let Codex's native `web_search` or `web_search_preview` tool work while DeepSeek is active, configure the optional backends in `gateway.local.json`.
148
-
149
- Tavily provides search results:
150
-
151
- ```json
152
- "tavilyApiKey": "tvly-...",
153
- "tavilyWebSearchEnabled": true
154
- ```
155
-
156
- Firecrawl provides opened-page reading and focused page lookup:
157
-
158
- ```json
159
- "firecrawlApiKey": "fc-...",
160
- "firecrawlWebFetchEnabled": true,
161
- "firecrawlAutoScrapeTopResults": 3
74
+ codex-deepseek-gateway sessions
162
75
  ```
163
76
 
164
- Codex does not need MCP, a different tool name, or prompt changes. The gateway converts Codex web search requests into internal Chat Completions tools:
165
-
166
- - `tavily_search` searches the web and returns citation-ready snippets.
167
- - `firecrawl_open_page` reads a specific public page.
168
- - `firecrawl_find_in_page` reads a page with a focused query.
169
-
170
- DeepSeek receives compact text context containing the search query, sources, snippets, and optional opened-page excerpts. Codex receives Responses-compatible `web_search_call` items, final assistant messages, and URL citation annotations when the final text cites a matching source marker.
171
-
172
- The web payload is intentionally text-focused. Tavily is called with `include_raw_content: false`; Firecrawl defaults to main content, removes base64 images, rejects local/private URLs, and truncates page text before it reaches the model.
173
-
174
- ## New Conversations
175
-
176
- Start a new Codex conversation with a temporary gateway model override:
77
+ Useful non-interactive forms:
177
78
 
178
79
  ```sh
179
- npx @galaxy-yearn/codex-deepseek-gateway new
80
+ codex-deepseek-gateway new --model deepseek-v4-flash --reasoning-effort low
81
+ codex-deepseek-gateway new --print
82
+ codex-deepseek-gateway sessions --print
83
+ codex-deepseek-gateway sessions --all
84
+ codex-deepseek-gateway sessions --exec <id-or-row>
180
85
  ```
181
86
 
182
- The command first lets you choose a model from `~/.codex/deepseek-gateway/config/model-aliases.json`, then choose Codex reasoning effort, then runs:
87
+ `new` chooses a model, then Codex reasoning effort. `sessions` chooses a session first, then model and reasoning effort. Both launch Codex with:
183
88
 
184
89
  ```sh
185
- codex -c model_provider=deepseek-gateway -c model=<model> -c model_reasoning_effort=<effort>
90
+ codex -c model_provider=deepseek-gateway -c model=<model> -c model_reasoning_effort=<effort> -c model_supports_reasoning_summaries=true -c model_reasoning_summary=auto
186
91
  ```
187
92
 
188
- This does not edit `~/.codex/config.toml`; the `model_providers.deepseek-gateway` provider entry still needs to exist there.
93
+ Model aliases are read from:
189
94
 
190
- You can skip the picker:
191
-
192
- ```sh
193
- npx @galaxy-yearn/codex-deepseek-gateway new --model deepseek-v4-flash --reasoning-effort low
95
+ ```text
96
+ ~/.codex/deepseek-gateway/config/model-aliases.json
194
97
  ```
195
98
 
196
- Print the generated launch command instead of running Codex:
99
+ ## Commands
197
100
 
198
101
  ```sh
199
- npx @galaxy-yearn/codex-deepseek-gateway new --print
102
+ codex-deepseek-gateway install
103
+ codex-deepseek-gateway start
104
+ codex-deepseek-gateway stop
105
+ codex-deepseek-gateway status
106
+ codex-deepseek-gateway doctor
107
+ codex-deepseek-gateway new
108
+ codex-deepseek-gateway sessions
109
+ codex-deepseek-gateway uninstall
200
110
  ```
201
111
 
202
- ## Sessions
203
-
204
- Open a cross-provider session picker from a project:
112
+ `doctor` checks the active Codex config, DeepSeek request shape, reasoning mode, and optional web-search backend readiness.
205
113
 
206
- ```sh
207
- npx @galaxy-yearn/codex-deepseek-gateway sessions
208
- ```
209
-
210
- The picker is read-only. It scans Codex local transcript files, lists sessions for the current project across providers, then runs:
114
+ To remove the local runtime and then uninstall the global package:
211
115
 
212
116
  ```sh
213
- codex resume <session-id> -c model_provider=deepseek-gateway -c model=<model> -c model_reasoning_effort=<effort>
117
+ codex-deepseek-gateway uninstall
118
+ npm uninstall -g @galaxy-yearn/codex-deepseek-gateway
214
119
  ```
215
120
 
216
- Flow:
217
-
218
- - choose a model from `~/.codex/deepseek-gateway/config/model-aliases.json`
219
- - choose Codex reasoning effort
220
- - choose `[New conversation]` or the session to resume
221
- - use `↑/↓` to select, `Enter` to confirm, `←` to go back, and `Esc` to quit
222
-
223
- Print copyable resume commands instead of opening the picker:
224
-
225
- ```sh
226
- npx @galaxy-yearn/codex-deepseek-gateway sessions --print
227
- ```
121
+ ## Reasoning
228
122
 
229
- Include sessions outside the current project:
123
+ Codex effort maps to DeepSeek V4 thinking mode:
230
124
 
231
- ```sh
232
- npx @galaxy-yearn/codex-deepseek-gateway sessions --all
233
- ```
125
+ | Codex effort | DeepSeek request |
126
+ | --- | --- |
127
+ | `low` | `thinking.type = disabled` |
128
+ | `medium` | `thinking.type = enabled`, `reasoning_effort = high` |
129
+ | `high` | `thinking.type = enabled`, `reasoning_effort = high` |
130
+ | `xhigh` | `thinking.type = enabled`, `reasoning_effort = max` |
234
131
 
235
- Resume a matching session directly by row number or unique session id prefix:
132
+ When DeepSeek returns `reasoning_content`, the gateway preserves the raw text for DeepSeek history and sends a display-only Markdown-cleaned copy through Codex's reasoning summary UI. The raw reasoning is not duplicated into visible message content.
236
133
 
237
- ```sh
238
- npx @galaxy-yearn/codex-deepseek-gateway sessions --exec <id-or-row>
239
- ```
134
+ ## Web Search
240
135
 
241
- Limit how many session rows are shown or offered in the picker:
136
+ Web search is optional and off by default. Configure Tavily for search:
242
137
 
243
- ```sh
244
- npx @galaxy-yearn/codex-deepseek-gateway sessions --limit 50
138
+ ```json
139
+ {
140
+ "tavilyApiKey": "tvly-...",
141
+ "tavilyWebSearchEnabled": true
142
+ }
245
143
  ```
246
144
 
247
- The command does not edit session files, change provider ownership, or change Codex's native resume picker filters. It only helps you find the hidden session id and resume it with explicit config overrides.
145
+ Configure Firecrawl if you also want opened-page reading:
248
146
 
249
- ## Commands
250
-
251
- ```sh
252
- npx @galaxy-yearn/codex-deepseek-gateway install
253
- npx @galaxy-yearn/codex-deepseek-gateway start
254
- npx @galaxy-yearn/codex-deepseek-gateway stop
255
- npx @galaxy-yearn/codex-deepseek-gateway status
256
- npx @galaxy-yearn/codex-deepseek-gateway doctor
257
- npx @galaxy-yearn/codex-deepseek-gateway new
258
- npx @galaxy-yearn/codex-deepseek-gateway sessions
259
- npx @galaxy-yearn/codex-deepseek-gateway uninstall
147
+ ```json
148
+ {
149
+ "firecrawlApiKey": "fc-...",
150
+ "firecrawlWebFetchEnabled": true
151
+ }
260
152
  ```
261
153
 
262
- `start` launches a headless background Node.js process. `stop` stops it. `uninstall` stops the gateway and removes `~/.codex/deepseek-gateway`, but it does not edit `~/.codex/config.toml`.
154
+ Codex can keep requesting `web_search` / `web_search_preview`. The gateway exposes compact internal web tools to DeepSeek, executes Tavily/Firecrawl calls itself, feeds tool results back to the model, and returns Codex-compatible `web_search_call` items. `TAVILY_MAX_SEARCH_ROUNDS` defaults to `10` and is only a runaway/cost guardrail; when reached, the gateway disables web tools for one final-answer turn.
263
155
 
264
- If `start` returns without visible output on your terminal, run `status`; `"reachable": true` is the source of truth.
156
+ Final answers should include useful source titles and URLs directly.
265
157
 
266
- ## What Works
158
+ ## 0.1.5 Updates
267
159
 
268
- - Codex `POST /v1/responses`
269
- - streaming and non-streaming responses
270
- - text input and output
271
- - image, file, and audio content parts when DeepSeek accepts the corresponding Chat Completions shape
272
- - function tools and tool-call history
273
- - DeepSeek thinking mode and `reasoning_content`
274
- - lightweight local `previous_response_id` / `conversation` history while the gateway process is running
275
- - `GET /v1/models` with local model aliases and optional upstream discovery
276
- - optional Tavily/Firecrawl-backed `web_search` emulation
277
- - new-conversation launcher with per-run Codex config overrides
278
- - read-only cross-provider session picker
160
+ - Global CLI usage: install once with `npm install -g`, then run `codex-deepseek-gateway ...` from any directory.
161
+ - `install` preserves `gateway.local.json`; runtime defaults stay in code.
162
+ - `new` and `sessions` launch Codex with reasoning-summary config overrides.
163
+ - `sessions` first chooses the session, hides subagent transcripts, and sorts by the latest user-message date.
164
+ - DeepSeek-facing tool descriptions are compact; namespaced Codex local tools, including multi-agent tools, pass back to Codex with namespace preserved.
165
+ - Web search uses a model-driven loop: internal Tavily/Firecrawl tools run in the gateway, local Codex tools pass through to Codex, and final-answer turns no longer end as empty completions.
166
+ - Reasoning display now uses Codex summary events only, with display-only Markdown cleanup to avoid duplicate or truncated thinking text.
279
167
 
280
168
  ## Limits
281
169
 
282
- Chat Completions is not a full Responses API replacement. Some Responses features have no equivalent upstream field.
170
+ Chat Completions is not a full Responses API replacement.
283
171
 
284
- - Hosted tools such as file search, computer use, image generation, and code interpreter are represented as function-tool shims unless Codex executes matching tools locally. Web search is the only hosted tool the gateway can emulate directly.
285
- - Tavily and Firecrawl web emulation is text-focused. It does not expose raw HTML, screenshots, browser actions, crawl/map jobs, cookies, private network access, or provider-specific payloads through Codex `web_search`.
286
- - URL citations are returned in the Responses metadata path. Whether they appear as clickable links in the terminal depends on the Codex client build and how it renders custom-provider citation annotations.
172
+ - Hosted tools without a local Codex executor are represented as function shims. Web search is the only hosted tool the gateway emulates directly.
173
+ - Tavily/Firecrawl web emulation is text-focused; it does not provide browser control, screenshots, raw HTML, cookies, crawl jobs, or private-network access.
287
174
  - OpenAI `file_id` values are passed through; the gateway cannot fetch private OpenAI-hosted files.
288
- - In-memory conversation history is lost when the gateway restarts.
289
- - The gateway exposes model aliases on `/v1/models`, including aliases from `config/model-aliases.json`. Whether Codex TUI `/model` actually shows custom provider models depends on the Codex build. `config.toml`, `new`, and `sessions` remain the reliable ways to choose a model.
175
+ - In-memory `previous_response_id` / `conversation` history is lost when the gateway process restarts.
176
+ - The gateway exposes model aliases on `/v1/models`; whether Codex TUI `/model` shows them depends on the Codex build. `config.toml`, `new`, and `sessions` are the reliable model-selection paths.
290
177
 
291
178
  ## License
292
179
 
@@ -39,6 +39,7 @@ Usage:
39
39
  codex-deepseek-gateway uninstall
40
40
 
41
41
  Options:
42
+ -v, --version Print package version
42
43
  --dir <path> Install directory, defaults to ~/.codex/deepseek-gateway
43
44
  --no-edit Do not open the local config file after install
44
45
  --all With sessions, include sessions outside the current project
@@ -58,7 +59,9 @@ function parseArgs(argv) {
58
59
  const rest = [];
59
60
  for (let index = 0; index < argv.length; index += 1) {
60
61
  const arg = argv[index];
61
- if (arg === '--dir' || arg === '-d' || arg === '-InstallDir') {
62
+ if (arg === '--version' || arg === '-v') {
63
+ options.version = true;
64
+ } else if (arg === '--dir' || arg === '-d' || arg === '-InstallDir') {
62
65
  const value = argv[index + 1];
63
66
  if (!value) throw new Error(`${arg} requires a path`);
64
67
  options.dir = resolve(value);
@@ -253,6 +256,10 @@ async function install(options) {
253
256
  }
254
257
 
255
258
  async function start(options) {
259
+ if (!existsSync(options.dir)) {
260
+ throw new Error(`Missing runtime at ${options.dir}. Run install first.`);
261
+ }
262
+ copyRuntime(options.dir);
256
263
  if (!existsSync(serverPath(options.dir))) {
257
264
  throw new Error(`Missing runtime at ${options.dir}. Run install first.`);
258
265
  }
@@ -400,6 +407,10 @@ async function uninstall(options) {
400
407
 
401
408
  async function main() {
402
409
  const { command, options } = parseArgs(process.argv.slice(2));
410
+ if (options.version) {
411
+ print(`${packageJson.version}\n`);
412
+ return;
413
+ }
403
414
  if (!command || command === '-h' || command === '--help' || command === 'help') {
404
415
  usage();
405
416
  return;
@@ -1,23 +1,7 @@
1
1
  {
2
- "port": 3000,
3
- "host": "127.0.0.1",
4
- "upstreamProvider": "deepseek",
5
- "upstreamBaseUrl": "https://api.deepseek.com",
6
2
  "upstreamApiKey": "sk-REPLACE_ME",
7
- "upstreamTimeoutMs": 120000,
8
3
  "tavilyApiKey": "",
9
4
  "tavilyWebSearchEnabled": false,
10
- "tavilySearchDepth": "basic",
11
- "tavilyMaxResults": 5,
12
- "tavilyMaxSearchRounds": 2,
13
- "tavilyTimeoutMs": 15000,
14
5
  "firecrawlApiKey": "",
15
- "firecrawlWebFetchEnabled": false,
16
- "firecrawlAutoScrapeTopResults": 3,
17
- "firecrawlPageMaxChars": 5000,
18
- "firecrawlTimeoutMs": 30000,
19
- "fetchUpstreamModels": false,
20
- "modelsTimeoutMs": 5000,
21
- "modelsCacheMs": 60000,
22
- "debugPayload": false
6
+ "firecrawlWebFetchEnabled": false
23
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galaxy-yearn/codex-deepseek-gateway",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Local OpenAI Codex Responses API gateway for DeepSeek Chat Completions.",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
@@ -19,7 +19,7 @@
19
19
  "test": "node --test"
20
20
  },
21
21
  "engines": {
22
- "node": ">=20"
22
+ "node": ">=22"
23
23
  },
24
24
  "keywords": [
25
25
  "codex",
@@ -66,6 +66,10 @@ function configOverrideArgs(context) {
66
66
  `model=${JSON.stringify(context.model)}`,
67
67
  '-c',
68
68
  `model_reasoning_effort=${JSON.stringify(context.reasoningEffort)}`,
69
+ '-c',
70
+ 'model_supports_reasoning_summaries=true',
71
+ '-c',
72
+ 'model_reasoning_summary="auto"',
69
73
  ];
70
74
  }
71
75
 
@@ -77,6 +81,10 @@ function configOverrideCommandParts(context) {
77
81
  `model=${context.model}`,
78
82
  '-c',
79
83
  `model_reasoning_effort=${context.reasoningEffort}`,
84
+ '-c',
85
+ 'model_supports_reasoning_summaries=true',
86
+ '-c',
87
+ 'model_reasoning_summary=auto',
80
88
  ];
81
89
  }
82
90
 
@@ -127,7 +135,7 @@ function renderPicker(state) {
127
135
  const row = `${index === selected ? '>' : ' '} ${rows[index]}`;
128
136
  output += `${index === selected ? `${SELECTED_ROW}${row}${RESET_STYLE}` : row}\n`;
129
137
  }
130
- output += '\n↑/↓ select Enter confirm back Esc quit\n';
138
+ output += '\nUp/Down select Enter confirm Left back Esc quit\n';
131
139
  print(output);
132
140
  }
133
141
 
@@ -5,6 +5,7 @@ import {
5
5
  readFileSync,
6
6
  readdirSync,
7
7
  readSync,
8
+ statSync,
8
9
  } from 'node:fs';
9
10
  import { join, resolve } from 'node:path';
10
11
  import {
@@ -60,6 +61,48 @@ function parseJsonLine(line) {
60
61
  }
61
62
  }
62
63
 
64
+ function isSubagentSession(meta) {
65
+ return meta?.thread_source === 'subagent' || Boolean(meta?.parent_thread_id);
66
+ }
67
+
68
+ function timestampFromUserMessage(row) {
69
+ const payload = row?.payload;
70
+ if (row?.type === 'event_msg' && payload?.type === 'user_message') return row.timestamp || '';
71
+ if (row?.type === 'response_item' && payload?.type === 'message' && payload.role === 'user') return row.timestamp || '';
72
+ return '';
73
+ }
74
+
75
+ function readLastUserMessageTimestamp(file, chunkSize = 256 * 1024) {
76
+ const handle = openSync(file, 'r');
77
+ try {
78
+ let position = statSync(file).size;
79
+ let suffix = Buffer.alloc(0);
80
+ while (position > 0) {
81
+ const length = Math.min(chunkSize, position);
82
+ position -= length;
83
+ const buffer = Buffer.alloc(length);
84
+ const read = readSync(handle, buffer, 0, length, position);
85
+ let data = buffer.subarray(0, read);
86
+ if (suffix.length) data = Buffer.concat([data, suffix]);
87
+
88
+ let lineEnd = data.length;
89
+ for (let index = data.length - 1; index >= 0; index -= 1) {
90
+ if (data[index] !== 10) continue;
91
+ const row = parseJsonLine(data.subarray(index + 1, lineEnd).toString('utf8').trim());
92
+ const timestamp = timestampFromUserMessage(row);
93
+ if (timestamp) return timestamp;
94
+ lineEnd = index;
95
+ }
96
+ suffix = data.subarray(0, lineEnd);
97
+ }
98
+
99
+ const row = parseJsonLine(suffix.toString('utf8').trim());
100
+ return timestampFromUserMessage(row);
101
+ } finally {
102
+ closeSync(handle);
103
+ }
104
+ }
105
+
63
106
  function readSessionIndex(codexHome) {
64
107
  const file = join(codexHome, 'session_index.jsonl');
65
108
  const byId = new Map();
@@ -118,16 +161,17 @@ function isInsideProject(cwd, projectRoot) {
118
161
  return sessionCwd === root || sessionCwd.startsWith(`${root}${process.platform === 'win32' ? '\\' : '/'}`);
119
162
  }
120
163
 
121
- function readSession(file, indexById) {
164
+ export function readSession(file, indexById) {
122
165
  const lines = readStart(file).split(/\r?\n/).filter(Boolean);
123
166
  const meta = parseJsonLine(lines[0])?.payload;
124
167
  if (!meta?.id) return null;
168
+ if (isSubagentSession(meta)) return null;
125
169
  const index = indexById.get(meta.id) || {};
126
170
  return {
127
171
  id: meta.id,
128
172
  provider: meta.model_provider || '',
129
173
  cwd: meta.cwd || '',
130
- updatedAt: index.updated_at || meta.timestamp || '',
174
+ updatedAt: readLastUserMessageTimestamp(file) || index.updated_at || meta.timestamp || '',
131
175
  title: index.thread_name || firstUserPreview(lines) || '(untitled)',
132
176
  };
133
177
  }
@@ -180,9 +224,16 @@ async function chooseSessionFlow(allSessions, context, limit) {
180
224
  const listed = allSessions.slice(0, limit);
181
225
  const header = sessionHeader();
182
226
  return await withPickerScreen(async () => {
183
- let step = 'model';
227
+ let step = 'session';
184
228
  while (true) {
185
- if (step === 'model') {
229
+ if (step === 'session') {
230
+ const rows = [NEW_SESSION_ROW, ...sessionRows(listed)];
231
+ const result = await pick(`Choose Codex session`, rows, header);
232
+ if (result.action === 'cancel') return null;
233
+ if (result.action === 'back') return null;
234
+ context.selectedSession = result.index === 0 ? { newConversation: true } : listed[result.index - 1];
235
+ step = 'model';
236
+ } else if (step === 'model') {
186
237
  const action = await pickModel(context);
187
238
  if (action !== 'select') return null;
188
239
  step = 'reasoning';
@@ -190,14 +241,7 @@ async function chooseSessionFlow(allSessions, context, limit) {
190
241
  const action = await pickReasoning(context);
191
242
  if (action === 'cancel') return null;
192
243
  if (action === 'back') step = 'model';
193
- else step = 'session';
194
- } else {
195
- const rows = [NEW_SESSION_ROW, ...sessionRows(listed)];
196
- const result = await pick(`Choose Codex session for ${context.provider} / ${context.model} / ${context.reasoningEffort}`, rows, header);
197
- if (result.action === 'cancel') return null;
198
- if (result.action === 'back') step = 'reasoning';
199
- else if (result.index === 0) return { newConversation: true };
200
- else return listed[result.index - 1];
244
+ else return context.selectedSession;
201
245
  }
202
246
  }
203
247
  });
package/src/config.js CHANGED
@@ -25,7 +25,7 @@ export function loadConfig(env = process.env) {
25
25
  tavilyBaseUrl: mergedEnv.TAVILY_BASE_URL || 'https://api.tavily.com',
26
26
  tavilySearchDepth: mergedEnv.TAVILY_SEARCH_DEPTH || 'basic',
27
27
  tavilyMaxResults: Number(mergedEnv.TAVILY_MAX_RESULTS || 5),
28
- tavilyMaxSearchRounds: Number(mergedEnv.TAVILY_MAX_SEARCH_ROUNDS || 2),
28
+ tavilyMaxSearchRounds: Number(mergedEnv.TAVILY_MAX_SEARCH_ROUNDS || 10),
29
29
  tavilyTimeoutMs: Number(mergedEnv.TAVILY_TIMEOUT_MS || 15000),
30
30
  tavilySnippetChars: Number(mergedEnv.TAVILY_SNIPPET_CHARS || 650),
31
31
  tavilyResultMaxChars: Number(mergedEnv.TAVILY_RESULT_MAX_CHARS || 6000),
package/src/firecrawl.js CHANGED
@@ -304,12 +304,12 @@ export function formatFirecrawlScrapeResult({ url = '', title = '', summary = ''
304
304
  if (links?.length) {
305
305
  lines.push('Page links:');
306
306
  for (const [index, link] of links.entries()) {
307
- lines.push(`[L${index + 1}] ${link.title || link.url}`);
307
+ lines.push(`Link ${index + 1}: ${link.title || link.url}`);
308
308
  lines.push(`URL: ${link.url}`);
309
309
  }
310
310
  }
311
311
  }
312
- lines.push('Use this opened page only as source text. Cite it with the source number assigned by the search result when available.');
312
+ lines.push('Use this opened page only as source text. If it supports the answer, include the page title and URL.');
313
313
  const text = lines.filter(Boolean).join('\n');
314
314
  const maxChars = Number(config.firecrawlResultMaxChars) || DEFAULT_TOTAL_CHARS;
315
315
  if (text.length <= maxChars) return text;