codex-copilot-dx 0.4.23 → 0.4.26

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
@@ -15,7 +15,7 @@ Supports both HTTP SSE streaming and non-streaming.
15
15
  ## Prerequisites
16
16
 
17
17
  - GitHub Copilot subscription (Individual, Business, or Enterprise)
18
- - Node.js 18+
18
+ - Node.js 18.17+
19
19
  - [Codex Desktop](https://openai.com/codex), [Claude Code](https://claude.com/claude-code), and/or Claude App installed
20
20
 
21
21
  ## Usage
@@ -28,7 +28,7 @@ npx codex-copilot-dx@latest
28
28
 
29
29
  On first run, it will:
30
30
  1. Authenticate with GitHub via device flow (if needed), after first trying compatible local Copilot token sources
31
- 2. Print the local package version and warn when a newer npm release is available
31
+ 2. Print the local package version and check for a newer npm release in the background
32
32
  3. Start the adapter on loopback (`127.0.0.1:2026`)
33
33
  4. Configure Codex (`~/.codex/config.toml`) to use the adapter, including stale shell env base URLs if present
34
34
  5. Configure Claude Code (`~/.claude/settings.json`) to use the adapter; it creates the file when missing, otherwise backs up `settings.json.bak` before updating the local API env keys
@@ -38,6 +38,8 @@ Claude Code picks up the new `ANTHROPIC_BASE_URL` on its next launch.
38
38
 
39
39
  If an existing `codex-copilot-dx` adapter is already running on the configured host and port, a second launch reuses it instead of starting another proxy. The second launch still refreshes Codex and Claude Code config, then exits.
40
40
 
41
+ The running adapter reports its package and protocol versions. After upgrading `codex-copilot-dx`, stop the old process before starting the new version; the new CLI refuses to silently reuse an incompatible adapter.
42
+
41
43
  Do not set Claude Code by manually exporting `ANTHROPIC_BASE_URL` or `ANTHROPIC_AUTH_TOKEN` in your shell. Let `codex-copilot-dx` write the local config files instead. If you previously exported those variables, remove them from shell startup files and restart the terminal before launching Claude Code.
42
44
 
43
45
  ### Diagnostics
@@ -48,9 +50,19 @@ Run a read-only config check without starting the adapter or changing files:
48
50
  codex-copilot-dx doctor
49
51
  ```
50
52
 
53
+ The command exits with status `1` when it finds an invalid configuration and `0` when checks contain only OK or warning results.
54
+
51
55
  The doctor checks the GitHub token, Codex config, Claude Code settings, Claude App gateway profile, and whether the local adapter port is listening.
52
56
 
53
- When the saved token is missing, `codex-copilot-dx` first looks for compatible local Copilot GitHub tokens, validates them with GitHub and Copilot, and imports a valid one before starting device login. It checks explicit token sources (`CCDX_GITHUB_TOKEN`, `CCDX_GITHUB_TOKEN_PATH`, `CCDX_GITHUB_TOKEN_PATHS`) plus common local `auth.json` layouts under application config directories. It does not rely on a specific app name. If a saved token later fails with `401` or `403`, the adapter repeats that discovery and retries once before asking you to reauthenticate.
57
+ For a read-only live check of the saved GitHub token, Copilot entitlement, and models endpoint, run:
58
+
59
+ ```bash
60
+ codex-copilot-dx doctor --online
61
+ ```
62
+
63
+ The online doctor never starts device flow, scans for replacement tokens, or changes the saved token.
64
+
65
+ When the saved token is missing, `codex-copilot-dx` first looks for compatible local Copilot GitHub tokens, validates them with GitHub and Copilot, and imports a valid one before starting device login. It checks explicit token sources (`CCDX_GITHUB_TOKEN`, `CCDX_GITHUB_TOKEN_PATH`, `CCDX_GITHUB_TOKEN_PATHS`) plus common local `auth.json` layouts under application config directories. It does not rely on a specific app name. Generic discovery refuses to choose silently when valid tokens for multiple GitHub accounts are found. After an account is selected, automatic `401`/`403` recovery accepts only the same GitHub account. Explicit token variables remain the intentional way to switch accounts.
54
66
 
55
67
  If Copilot token refresh still fails with `401` or `403`, the saved GitHub token may be expired, revoked, or missing Copilot access. Delete the saved token and start the tool again to trigger GitHub device login:
56
68
 
@@ -89,10 +101,13 @@ Environment variables:
89
101
  | `CCDX_MAX_BODY_BYTES` | `134217728` | Maximum compressed/raw request body size |
90
102
  | `CCDX_MAX_DECODED_BODY_BYTES` | `268435456` | Maximum decoded request body size after decompression |
91
103
  | `CCDX_UPSTREAM_TIMEOUT_MS` | `120000` | Timeout for non-streaming upstream Copilot requests |
104
+ | `CCDX_STREAM_HANDSHAKE_TIMEOUT_MS` | `120000` | Timeout while waiting for upstream streaming response headers |
105
+ | `CCDX_STREAM_IDLE_TIMEOUT_MS` | `120000` | Maximum idle time between upstream streaming body chunks |
92
106
  | `CCDX_UPSTREAM_RETRIES` | `2` | Retries for transient Copilot upstream network errors; capped at `5` |
93
107
  | `CCDX_UPSTREAM_RETRY_DELAY_MS` | `300` | Initial upstream retry backoff in milliseconds; capped at `5000` |
94
108
  | `CCDX_LOG_PATH` | unset | Mirror terminal logs to a file; set to `1` for `~/.local/share/codex-copilot-dx/debug.log` |
95
109
  | `CCDX_LOG_LEVEL` | `info` | Set to `debug` to include upstream request attempts, status codes, retry causes, and timings |
110
+ | `CCDX_LOG_MAX_BYTES` | `16777216` | Rotate the debug log at this size, retaining one `.1` backup; set to `0` to disable rotation |
96
111
  | `CCDX_IMG_MAX_DIM` | `2048` | Max long edge in pixels for image downscaling |
97
112
  | `CCDX_IMG_QUALITY` | `85` | WebP quality used when re-encoding images |
98
113
  | `CCDX_IMG_MIN_BYTES` | `100000` | Images smaller than this are left untouched |
@@ -104,12 +119,18 @@ Environment variables:
104
119
  | `CCDX_GITHUB_TOKEN` | unset | Explicit GitHub Copilot OAuth token to validate and import before device login |
105
120
  | `CCDX_GITHUB_TOKEN_PATH` | unset | Explicit file containing a GitHub Copilot OAuth token to validate and import before device login |
106
121
  | `CCDX_GITHUB_TOKEN_PATHS` | unset | Multiple token files separated by the platform path delimiter (`:` on macOS/Linux, `;` on Windows) |
122
+ | `CCDX_GITHUB_LOGIN` | saved account | Require automatic discovery and recovery to use this GitHub login |
107
123
  | `CCDX_DISABLE_TOKEN_DISCOVERY` | unset | Set to `1` to skip local token discovery and go straight to the saved token or device flow |
108
124
  | `CCDX_TOKEN_LOCK_TIMEOUT_MS` | `600000` | Maximum time to wait for another local process to finish GitHub token login/import |
109
125
  | `CCDX_TOKEN_LOCK_STALE_MS` | `900000` | Age after which a stale GitHub token lock file can be removed |
110
126
  | `CCDX_EXISTING_ADAPTER_TIMEOUT_MS` | `500` | Timeout for detecting an already-running local adapter during startup |
127
+ | `CCDX_MODEL_REFRESH_INTERVAL_MS` | `1800000` | Interval for refreshing Copilot model metadata; successful lists are cached locally as last-known-good data |
128
+ | `CCDX_RESPONSE_HISTORY_MAX_BYTES` | `67108864` | Total in-memory byte budget for locally expanded Responses history |
129
+ | `CCDX_RESPONSE_HISTORY_MAX_ENTRIES` | `4096` | Maximum stored incremental Responses history nodes |
111
130
  | `CCDX_USAGE_PATH` | `~/.local/share/codex-copilot-dx/usage.jsonl` | Local JSONL token usage log |
131
+ | `CCDX_USAGE_MAX_BYTES` | `33554432` | Rotate the usage log at this size, retaining one `.1` backup; set to `0` to disable rotation |
112
132
  | `CCDX_DISABLE_USAGE` | unset | Set to `1` to disable usage logging |
133
+ | `CCDX_SHUTDOWN_TIMEOUT_MS` | `5000` | Time to drain active HTTP connections before forcing shutdown |
113
134
 
114
135
  ### Usage logging
115
136
 
@@ -129,6 +150,17 @@ Long computer-use sessions can accumulate screenshots inside the conversation hi
129
150
 
130
151
  The adapter automatically downsamples embedded screenshots to long-edge <= 2048 px and re-encodes them as WebP before forwarding `/v1/responses`.
131
152
 
153
+ Newer ChatGPT/Codex clients can advertise an `image_gen` namespace that already exists upstream. The adapter removes that exact conflicting client tool before forwarding and retries once only when Copilot explicitly reports an image namespace collision. Image inputs and screenshot optimization remain enabled.
154
+
132
155
  ## License
133
156
 
134
157
  MIT
158
+
159
+ ## Development
160
+
161
+ ```bash
162
+ npm ci
163
+ npm run verify
164
+ ```
165
+
166
+ `npm test` runs the unit and handler-level suite. `npm run test:smoke` starts a real local HTTP adapter with fully injected offline upstreams. `npm run pack:check` verifies the npm tarball contents without publishing. The CI workflow runs all three checks on supported Node.js release lines.
package/bin/cli.mjs CHANGED
@@ -5,35 +5,93 @@ import { ensureCodexConfig } from "../src/config.mjs";
5
5
  import { ensureClaudeConfig } from "../src/claude-config.mjs";
6
6
  import { applyClaudeDesktopConfig, formatClaudeDesktopApplyResult, generatedClaudeDesktopApiKey } from "../src/claude-desktop-config.mjs";
7
7
  import { startAdapter } from "../src/adapter.mjs";
8
- import { listModels, refreshVSCodeVersion } from "../src/copilot.mjs";
8
+ import { cacheModelEndpoints, listModels, refreshVSCodeVersion } from "../src/copilot.mjs";
9
9
  import { claudeDesktopModelDefsFromCopilotModels, claudeDesktopModelIds, gptModelIdsFromCopilotModels, parseModelAliasEnv } from "../src/models.mjs";
10
10
  import { status } from "../src/status.mjs";
11
11
  import { configureLogging } from "../src/log.mjs";
12
- import { printUsageSummary } from "../src/usage.mjs";
12
+ import { flushUsageWrites, printUsageSummary } from "../src/usage.mjs";
13
13
  import { checkForUpdate, localPackageVersion } from "../src/version.mjs";
14
14
  import { runDoctor } from "../src/doctor.mjs";
15
15
  import { checkRunningAdapter } from "../src/running-adapter.mjs";
16
16
  import { assertSafeAdapterHost, isLoopbackHost } from "../src/security.mjs";
17
+ import { loadModelCache, saveModelCache } from "../src/model-cache.mjs";
18
+ import { initializeModelRegistry, runInBackground } from "../src/startup.mjs";
19
+ import { cliHelp, parseCliArgs, parseRuntimeOptions } from "../src/cli-options.mjs";
20
+ import { closeHttpServer } from "../src/shutdown.mjs";
17
21
 
18
- const ADAPTER_PORT = parseInt(process.env.ADAPTER_PORT || "2026");
19
- const ADAPTER_HOST = process.env.ADAPTER_HOST || "127.0.0.1";
20
- const MODEL_REFRESH_TIMEOUT_MS = parseInt(process.env.CCDX_MODEL_REFRESH_TIMEOUT_MS || "5000", 10);
21
- const EXISTING_ADAPTER_TIMEOUT_MS = parseInt(process.env.CCDX_EXISTING_ADAPTER_TIMEOUT_MS || "500", 10);
22
22
  const LOCAL_VERSION = localPackageVersion();
23
- const command = process.argv[2];
24
- const CONFIGURE_CLAUDE_DESKTOP = command === "--configure-claude-desktop" || process.env.CCDX_CONFIGURE_CLAUDE_DESKTOP === "1";
23
+
24
+ let CLI;
25
+ try {
26
+ CLI = parseCliArgs(process.argv.slice(2));
27
+ } catch (e) {
28
+ console.error(e.message);
29
+ console.error("Run codex-copilot-dx --help for usage.");
30
+ process.exit(2);
31
+ }
32
+
33
+ if (CLI.command === "help") {
34
+ console.log(cliHelp());
35
+ process.exit(0);
36
+ }
37
+ if (CLI.command === "version") {
38
+ console.log(`codex-copilot-dx v${LOCAL_VERSION}`);
39
+ process.exit(0);
40
+ }
41
+ if (CLI.command === "usage") {
42
+ await printUsageSummary();
43
+ process.exit(0);
44
+ }
45
+
46
+ let RUNTIME;
47
+ try {
48
+ RUNTIME = parseRuntimeOptions(process.env);
49
+ } catch (e) {
50
+ console.error(e.message);
51
+ process.exit(2);
52
+ }
53
+
54
+ const ADAPTER_PORT = RUNTIME.adapterPort;
55
+ const ADAPTER_HOST = RUNTIME.adapterHost;
56
+ const MODEL_REFRESH_TIMEOUT_MS = RUNTIME.modelRefreshTimeoutMs;
57
+ const EXISTING_ADAPTER_TIMEOUT_MS = RUNTIME.existingAdapterTimeoutMs;
58
+ const CONFIGURE_CLAUDE_DESKTOP = CLI.configureClaudeDesktop || process.env.CCDX_CONFIGURE_CLAUDE_DESKTOP === "1";
25
59
  const LOGGING = configureLogging();
60
+ const MODEL_REGISTRY = { modelDefs: undefined, source: "built-in" };
61
+ let activeServer = null;
62
+ let modelRefreshTimer = null;
63
+ let shuttingDown = false;
26
64
 
27
65
  if (LOGGING.filePath) {
28
66
  console.log(status("info", `Debug log: ${LOGGING.filePath}`));
29
67
  if (LOGGING.level === "debug") console.log(status("debug", "Debug logging enabled"));
30
68
  }
31
69
 
32
- async function refreshClaudeDesktopModelDefs() {
33
- if (parseModelAliasEnv(process.env.CCDX_CLAUDE_MODEL_ALIASES).length) {
34
- console.log(status("info", "Using CCDX_CLAUDE_MODEL_ALIASES for Claude models"));
35
- return undefined;
70
+ function applyModelsToRegistry(models, source, { updateClaudeModels = true } = {}) {
71
+ const modelDefs = updateClaudeModels
72
+ ? claudeDesktopModelDefsFromCopilotModels(models)
73
+ : MODEL_REGISTRY.modelDefs;
74
+ if (updateClaudeModels && !modelDefs.length) throw new Error("Copilot models response contained no Claude models");
75
+ cacheModelEndpoints(models);
76
+ if (updateClaudeModels) MODEL_REGISTRY.modelDefs = modelDefs;
77
+ MODEL_REGISTRY.source = source;
78
+ return modelDefs;
79
+ }
80
+
81
+ function loadCachedModelRegistry() {
82
+ const cached = loadModelCache();
83
+ if (!cached) return false;
84
+ try {
85
+ const customAliases = parseModelAliasEnv(process.env.CCDX_CLAUDE_MODEL_ALIASES).length > 0;
86
+ applyModelsToRegistry(cached, "cache", { updateClaudeModels: !customAliases });
87
+ return true;
88
+ } catch {
89
+ return false;
36
90
  }
91
+ }
92
+
93
+ async function refreshClaudeDesktopModelDefs() {
94
+ const customAliases = parseModelAliasEnv(process.env.CCDX_CLAUDE_MODEL_ALIASES).length > 0;
37
95
 
38
96
  const ctrl = new AbortController();
39
97
  const timer = setTimeout(() => ctrl.abort(), MODEL_REFRESH_TIMEOUT_MS);
@@ -50,13 +108,22 @@ async function refreshClaudeDesktopModelDefs() {
50
108
  console.log(status("warn", "Copilot models response contained no GPT models"));
51
109
  }
52
110
 
53
- const modelDefs = claudeDesktopModelDefsFromCopilotModels(models);
54
- if (!modelDefs.length) throw new Error("Copilot models response contained no Claude models");
55
- console.log(status("ok", `Refreshed Claude models from GitHub Copilot: ${modelDefs.map((model) => model.id).join(", ")}`));
111
+ const modelDefs = applyModelsToRegistry(models, "live", { updateClaudeModels: !customAliases });
112
+ try {
113
+ saveModelCache(models);
114
+ } catch (e) {
115
+ console.log(status("warn", `Could not persist the Copilot model cache (${e.message})`));
116
+ }
117
+ if (customAliases) {
118
+ console.log(status("info", "Using CCDX_CLAUDE_MODEL_ALIASES for Claude models"));
119
+ } else {
120
+ console.log(status("ok", `Refreshed Claude models from GitHub Copilot: ${modelDefs.map((model) => model.id).join(", ")}`));
121
+ }
56
122
  return modelDefs;
57
123
  } catch (e) {
58
- console.log(status("warn", `Could not refresh Claude models; using built-in model list (${e.message})`));
59
- return undefined;
124
+ const fallback = MODEL_REGISTRY.modelDefs?.length ? `${MODEL_REGISTRY.source} model list` : "built-in model list";
125
+ console.log(status("warn", `Could not refresh Claude models; using ${fallback} (${e.message})`));
126
+ return MODEL_REGISTRY.modelDefs;
60
127
  } finally {
61
128
  clearTimeout(timer);
62
129
  }
@@ -72,6 +139,10 @@ async function reuseRunningAdapterIfAvailable() {
72
139
  host: ADAPTER_HOST,
73
140
  timeoutMs: EXISTING_ADAPTER_TIMEOUT_MS,
74
141
  });
142
+ if (running.incompatible) {
143
+ const found = running.data?.version || "unknown";
144
+ throw new Error(`Adapter ${found} is already running at ${running.baseUrl}, but this CLI is ${LOCAL_VERSION}. Stop the existing process and run codex-copilot-dx again.`);
145
+ }
75
146
  if (!running.ok) return false;
76
147
 
77
148
  console.log(status("ok", `Using existing adapter at ${running.baseUrl}`));
@@ -105,19 +176,13 @@ async function reuseRunningAdapterIfAvailable() {
105
176
  return true;
106
177
  }
107
178
 
108
- if (command === "--version" || command === "-v" || command === "version") {
109
- console.log(`codex-copilot-dx v${LOCAL_VERSION}`);
110
- process.exit(0);
111
- }
112
-
113
- if (command === "usage") {
114
- await printUsageSummary();
115
- process.exit(0);
116
- }
117
-
118
- if (command === "doctor" || command === "--doctor") {
119
- await runDoctor({ port: ADAPTER_PORT, host: ADAPTER_HOST });
120
- process.exit(0);
179
+ if (CLI.command === "doctor") {
180
+ const checks = await runDoctor({
181
+ port: ADAPTER_PORT,
182
+ host: ADAPTER_HOST,
183
+ online: CLI.online,
184
+ });
185
+ process.exit(checks.some((check) => check.kind === "err") ? 1 : 0);
121
186
  }
122
187
 
123
188
  console.log(`
@@ -136,20 +201,22 @@ async function printUpdateNotice() {
136
201
  }
137
202
  }
138
203
 
139
- // Await the update check up front so the notice is shown even on the
140
- // "reuse existing adapter" path, which exits early below.
141
- await printUpdateNotice();
142
-
143
204
  try {
144
205
  assertSafeAdapterHost(ADAPTER_HOST, process.env);
206
+ void runInBackground(printUpdateNotice);
145
207
  if (await reuseRunningAdapterIfAvailable()) process.exit(0);
146
208
 
147
209
  // Ensure GitHub login, using device flow if no token exists.
148
210
  await ensureAuth();
149
211
 
150
212
  // Refresh the VS Code version in the background; fallback is non-blocking.
151
- refreshVSCodeVersion();
152
- const claudeDesktopModelDefs = await refreshClaudeDesktopModelDefs();
213
+ void refreshVSCodeVersion();
214
+ const modelInitialization = await initializeModelRegistry({
215
+ loadCached: loadCachedModelRegistry,
216
+ currentModelDefs: () => MODEL_REGISTRY.modelDefs,
217
+ refresh: refreshClaudeDesktopModelDefs,
218
+ });
219
+ const claudeDesktopModelDefs = modelInitialization.modelDefs;
153
220
 
154
221
  if (!isLoopbackHost(ADAPTER_HOST)) {
155
222
  console.log(status("warn", `ADAPTER_HOST=${ADAPTER_HOST} exposes the adapter beyond loopback because CCDX_ALLOW_LAN=1 is set. Use only on trusted networks.`));
@@ -160,7 +227,14 @@ try {
160
227
  : currentClaudeDesktopApiKey();
161
228
 
162
229
  // Start the in-process adapter.
163
- await startAdapter(ADAPTER_PORT, ADAPTER_HOST, { claudeDesktopApiKey, claudeDesktopModelDefs });
230
+ activeServer = await startAdapter(ADAPTER_PORT, ADAPTER_HOST, {
231
+ claudeDesktopApiKey,
232
+ claudeDesktopModelDefs,
233
+ modelRegistry: MODEL_REGISTRY,
234
+ upstreamTimeoutMs: RUNTIME.upstreamTimeoutMs,
235
+ streamHandshakeTimeoutMs: RUNTIME.streamHandshakeTimeoutMs,
236
+ streamIdleTimeoutMs: RUNTIME.streamIdleTimeoutMs,
237
+ });
164
238
 
165
239
  // Point Codex and Claude Code at the adapter.
166
240
  ensureCodexConfig(ADAPTER_PORT);
@@ -183,12 +257,12 @@ try {
183
257
 
184
258
  // Periodically refresh the model list + endpoint cache so a long-running
185
259
  // adapter picks up newly released models without a restart.
186
- const refreshIntervalMs = parseInt(process.env.CCDX_MODEL_REFRESH_INTERVAL_MS || String(30 * 60 * 1000), 10);
260
+ const refreshIntervalMs = RUNTIME.modelRefreshIntervalMs;
187
261
  if (Number.isFinite(refreshIntervalMs) && refreshIntervalMs > 0) {
188
- const timer = setInterval(() => {
262
+ modelRefreshTimer = setInterval(() => {
189
263
  refreshClaudeDesktopModelDefs().catch(() => {});
190
264
  }, refreshIntervalMs);
191
- if (typeof timer.unref === "function") timer.unref();
265
+ modelRefreshTimer.unref?.();
192
266
  }
193
267
 
194
268
  console.log(`
@@ -199,11 +273,30 @@ try {
199
273
  Press Ctrl+C to stop.
200
274
  `);
201
275
 
202
- process.on("SIGINT", () => {
203
- console.log(`\n${status("wait", "Shutting down...")}`);
204
- process.exit(0);
205
- });
276
+ const shutdown = async (signal) => {
277
+ if (shuttingDown) return;
278
+ shuttingDown = true;
279
+ console.log(`\n${status("wait", `Shutting down on ${signal}...`)}`);
280
+ let exitCode = 0;
281
+ try {
282
+ if (modelRefreshTimer) clearInterval(modelRefreshTimer);
283
+ const result = await closeHttpServer(activeServer, { timeoutMs: RUNTIME.shutdownTimeoutMs });
284
+ if (result.forced) console.warn(status("warn", "Forced remaining adapter connections closed"));
285
+ await flushUsageWrites();
286
+ } catch (e) {
287
+ exitCode = 1;
288
+ console.error(status("err", `Shutdown failed: ${e.message}`));
289
+ } finally {
290
+ LOGGING.cleanup();
291
+ process.exit(exitCode);
292
+ }
293
+ };
294
+ process.once("SIGINT", () => { void shutdown("SIGINT"); });
295
+ process.once("SIGTERM", () => { void shutdown("SIGTERM"); });
206
296
  } catch (e) {
207
297
  console.error(status("err", e.message));
298
+ await closeHttpServer(activeServer, { timeoutMs: RUNTIME.shutdownTimeoutMs }).catch(() => {});
299
+ await flushUsageWrites();
300
+ LOGGING.cleanup();
208
301
  process.exit(1);
209
302
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.4.23",
3
+ "version": "0.4.26",
4
4
  "description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,8 +25,15 @@
25
25
  "responses-api"
26
26
  ],
27
27
  "license": "MIT",
28
+ "engines": {
29
+ "node": ">=18.17"
30
+ },
28
31
  "scripts": {
29
- "test": "node --test"
32
+ "test": "node --test",
33
+ "test:smoke": "node scripts/offline-smoke.mjs",
34
+ "test:e2e": "bash test/e2e.sh",
35
+ "pack:check": "npm pack --dry-run --json",
36
+ "verify": "npm test && npm run test:smoke && npm run pack:check"
30
37
  },
31
38
  "dependencies": {
32
39
  "gpt-tokenizer": "^3.4.0",