@steipete/oracle 0.4.5 → 0.5.1

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.
Files changed (48) hide show
  1. package/README.md +11 -9
  2. package/dist/.DS_Store +0 -0
  3. package/dist/bin/oracle-cli.js +16 -48
  4. package/dist/scripts/agent-send.js +147 -0
  5. package/dist/scripts/docs-list.js +110 -0
  6. package/dist/scripts/git-policy.js +125 -0
  7. package/dist/scripts/runner.js +1378 -0
  8. package/dist/scripts/test-browser.js +103 -0
  9. package/dist/scripts/test-remote-chrome.js +68 -0
  10. package/dist/src/browser/actions/attachments.js +47 -16
  11. package/dist/src/browser/actions/promptComposer.js +67 -18
  12. package/dist/src/browser/actions/remoteFileTransfer.js +36 -4
  13. package/dist/src/browser/chromeCookies.js +44 -6
  14. package/dist/src/browser/chromeLifecycle.js +166 -25
  15. package/dist/src/browser/config.js +25 -1
  16. package/dist/src/browser/constants.js +22 -3
  17. package/dist/src/browser/index.js +384 -22
  18. package/dist/src/browser/profileSync.js +141 -0
  19. package/dist/src/browser/prompt.js +3 -1
  20. package/dist/src/browser/reattach.js +59 -0
  21. package/dist/src/browser/sessionRunner.js +15 -1
  22. package/dist/src/browser/windowsCookies.js +2 -1
  23. package/dist/src/cli/browserConfig.js +11 -0
  24. package/dist/src/cli/browserDefaults.js +41 -0
  25. package/dist/src/cli/detach.js +2 -2
  26. package/dist/src/cli/dryRun.js +4 -2
  27. package/dist/src/cli/engine.js +2 -2
  28. package/dist/src/cli/help.js +2 -2
  29. package/dist/src/cli/options.js +2 -1
  30. package/dist/src/cli/runOptions.js +1 -1
  31. package/dist/src/cli/sessionDisplay.js +102 -104
  32. package/dist/src/cli/sessionRunner.js +39 -6
  33. package/dist/src/cli/sessionTable.js +88 -0
  34. package/dist/src/cli/tui/index.js +19 -89
  35. package/dist/src/heartbeat.js +2 -2
  36. package/dist/src/oracle/background.js +10 -2
  37. package/dist/src/oracle/client.js +107 -0
  38. package/dist/src/oracle/config.js +10 -2
  39. package/dist/src/oracle/errors.js +24 -4
  40. package/dist/src/oracle/modelResolver.js +144 -0
  41. package/dist/src/oracle/oscProgress.js +1 -1
  42. package/dist/src/oracle/run.js +83 -34
  43. package/dist/src/oracle/runUtils.js +12 -8
  44. package/dist/src/remote/server.js +214 -23
  45. package/dist/src/sessionManager.js +5 -2
  46. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  47. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  48. package/package.json +14 -14
@@ -1,13 +1,32 @@
1
1
  import http from 'node:http';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
+ import net from 'node:net';
4
5
  import { randomBytes, randomUUID } from 'node:crypto';
6
+ import { spawn, spawnSync } from 'node:child_process';
7
+ import { existsSync } from 'node:fs';
5
8
  import { mkdtemp, rm, mkdir, writeFile } from 'node:fs/promises';
6
9
  import chalk from 'chalk';
7
10
  import { runBrowserMode } from '../browserMode.js';
8
11
  import { loadChromeCookies } from '../browser/chromeCookies.js';
9
12
  import { CHATGPT_URL } from '../browser/constants.js';
10
13
  import { normalizeChatgptUrl } from '../browser/utils.js';
14
+ async function findAvailablePort() {
15
+ return await new Promise((resolve, reject) => {
16
+ const srv = net.createServer();
17
+ srv.on('error', (err) => reject(err));
18
+ srv.listen(0, () => {
19
+ const address = srv.address();
20
+ if (typeof address === 'object' && address?.port) {
21
+ const port = address.port;
22
+ srv.close(() => resolve(port));
23
+ }
24
+ else {
25
+ srv.close(() => reject(new Error('Unable to allocate port')));
26
+ }
27
+ });
28
+ });
29
+ }
11
30
  export async function createRemoteServer(options = {}, deps = {}) {
12
31
  const runBrowser = deps.runBrowser ?? runBrowserMode;
13
32
  const server = http.createServer();
@@ -45,7 +64,6 @@ export async function createRemoteServer(options = {}, deps = {}) {
45
64
  res.end(JSON.stringify({ error: 'unauthorized' }));
46
65
  return;
47
66
  }
48
- // biome-ignore lint/nursery/noUnnecessaryConditions: busy guard protects single-run host
49
67
  if (busy) {
50
68
  if (verbose) {
51
69
  logger(`[serve] Busy: rejecting new run from ${formatSocket(req)} while another run is active`);
@@ -106,6 +124,18 @@ export async function createRemoteServer(options = {}, deps = {}) {
106
124
  payload.browserConfig.inlineCookiesSource = null;
107
125
  payload.browserConfig.cookieSync = true;
108
126
  }
127
+ else {
128
+ payload.browserConfig = {};
129
+ }
130
+ // Enforce manual-login profile when cookie sync is unavailable (e.g., Windows/WSL).
131
+ if (options.manualLoginDefault) {
132
+ payload.browserConfig.manualLogin = true;
133
+ payload.browserConfig.manualLoginProfileDir = options.manualLoginProfileDir;
134
+ payload.browserConfig.keepBrowser = true;
135
+ if (verbose) {
136
+ logger(`[serve] Enforcing manual-login profile at ${options.manualLoginProfileDir ?? 'default'} for remote run ${runId}`);
137
+ }
138
+ }
109
139
  const result = await runBrowser({
110
140
  prompt: payload.prompt,
111
141
  attachments,
@@ -146,6 +176,7 @@ export async function createRemoteServer(options = {}, deps = {}) {
146
176
  const also = extras.length ? `, also [${extras.join(', ')}]` : '';
147
177
  logger(color(chalk.cyanBright.bold, `Listening at ${primary}${also}`));
148
178
  logger(color(chalk.yellowBright, `Access token: ${authToken}`));
179
+ logger('Leave this terminal running; press Ctrl+C to stop oracle serve.');
149
180
  return {
150
181
  port: address.port,
151
182
  token: authToken,
@@ -157,15 +188,54 @@ export async function createRemoteServer(options = {}, deps = {}) {
157
188
  };
158
189
  }
159
190
  export async function serveRemote(options = {}) {
160
- // Warm-up: ensure this host has a ChatGPT login before accepting runs.
161
- const cookies = await loadLocalChatgptCookies(console.log, CHATGPT_URL);
191
+ const manualProfileDir = options.manualLoginProfileDir ?? path.join(os.homedir(), '.oracle', 'browser-profile');
192
+ const preferManualLogin = options.manualLoginDefault || process.platform === 'win32' || isWsl();
193
+ let cookies = null;
194
+ let opened = false;
195
+ if (isWsl() && process.env.ORACLE_ALLOW_WSL_SERVE !== '1') {
196
+ console.log('WSL detected. For reliable browser automation, run `oracle serve` from Windows PowerShell/Command Prompt so we can use your Windows Chrome profile.');
197
+ console.log('If you want to stay in WSL anyway, set ORACLE_ALLOW_WSL_SERVE=1 and ensure a Linux Chrome is installed, then rerun.');
198
+ console.log('Alternatively, start Windows Chrome with --remote-debugging-port=9222 and use `--remote-chrome <windows-ip>:9222`.');
199
+ return;
200
+ }
201
+ if (!preferManualLogin) {
202
+ // Warm-up: ensure this host has a ChatGPT login before accepting runs.
203
+ const result = await loadLocalChatgptCookies(console.log, CHATGPT_URL);
204
+ cookies = result.cookies;
205
+ opened = result.opened;
206
+ }
162
207
  if (!cookies || cookies.length === 0) {
163
208
  console.log('No ChatGPT cookies detected on this host.');
164
- console.log('Opened chatgpt.com for login. Sign in, then restart `oracle serve` to continue.');
165
- return;
209
+ if (preferManualLogin) {
210
+ await mkdir(manualProfileDir, { recursive: true });
211
+ console.log(`Cookie extraction is unavailable on this platform. Using manual-login Chrome profile at ${manualProfileDir}. Remote runs will reuse this profile; sign in once when the browser opens.`);
212
+ const devtoolsPortFile = path.join(manualProfileDir, 'DevToolsActivePort');
213
+ const alreadyRunning = existsSync(devtoolsPortFile);
214
+ if (alreadyRunning) {
215
+ console.log('Detected an existing automation Chrome session; will reuse it for manual login.');
216
+ }
217
+ else {
218
+ void launchManualLoginChrome(manualProfileDir, CHATGPT_URL, console.log);
219
+ }
220
+ }
221
+ else if (opened) {
222
+ console.log('Opened chatgpt.com for login. Sign in, then restart `oracle serve` to continue.');
223
+ return;
224
+ }
225
+ else {
226
+ console.log('Please open https://chatgpt.com/ in this host\'s browser and sign in; then rerun.');
227
+ console.log('Tip: install xdg-utils (xdg-open) to enable automatic browser opening on Linux/WSL.');
228
+ return;
229
+ }
230
+ }
231
+ else {
232
+ console.log(`Detected ${cookies.length} ChatGPT cookies on this host; runs will reuse this session.`);
166
233
  }
167
- console.log(`Detected ${cookies.length} ChatGPT cookies on this host; runs will reuse this session.`);
168
- const server = await createRemoteServer(options);
234
+ const server = await createRemoteServer({
235
+ ...options,
236
+ manualLoginDefault: preferManualLogin,
237
+ manualLoginProfileDir: manualProfileDir,
238
+ });
169
239
  await new Promise((resolve) => {
170
240
  const shutdown = () => {
171
241
  console.log('Shutting down remote service...');
@@ -263,32 +333,153 @@ async function loadLocalChatgptCookies(logger, targetUrl) {
263
333
  });
264
334
  if (!cookies || cookies.length === 0) {
265
335
  logger('No local ChatGPT cookies found on this host. Please log in once; opening ChatGPT...');
266
- triggerLocalLoginPrompt(logger, targetUrl);
267
- return null;
336
+ const opened = triggerLocalLoginPrompt(logger, targetUrl);
337
+ return { cookies: null, opened };
268
338
  }
269
339
  logger(`Loaded ${cookies.length} local ChatGPT cookies on this host.`);
270
- return cookies;
340
+ return { cookies, opened: false };
271
341
  }
272
342
  catch (error) {
273
- logger(`Unable to load local ChatGPT cookies on this host: ${error instanceof Error ? error.message : String(error)}`);
274
- triggerLocalLoginPrompt(logger, targetUrl);
275
- return null;
343
+ const message = error instanceof Error ? error.message : String(error);
344
+ const missingDbMatch = message.match(/Unable to locate Chrome cookie DB at (.+)/);
345
+ if (missingDbMatch) {
346
+ const lookedPath = missingDbMatch[1];
347
+ logger(`Chrome cookies not found at ${lookedPath}. Set --browser-cookie-path to your Chrome profile or log in manually.`);
348
+ }
349
+ else {
350
+ logger(`Unable to load local ChatGPT cookies on this host: ${message}`);
351
+ }
352
+ if (process.platform === 'linux' && isWsl()) {
353
+ logger('WSL hint: Chrome lives under /mnt/c/Users/<you>/AppData/Local/Google/Chrome/User Data/Default; pass --browser-cookie-path to that directory if auto-detect fails.');
354
+ }
355
+ const opened = triggerLocalLoginPrompt(logger, targetUrl);
356
+ return { cookies: null, opened };
276
357
  }
277
358
  }
278
359
  function triggerLocalLoginPrompt(logger, url) {
279
- const opener = process.platform === 'darwin'
280
- ? 'open'
281
- : process.platform === 'win32'
282
- ? 'start'
283
- : 'xdg-open';
360
+ const verbose = process.argv.includes('--verbose') || process.env.ORACLE_SERVE_VERBOSE === '1';
361
+ const openers = [];
362
+ if (process.platform === 'darwin') {
363
+ openers.push({ cmd: 'open' });
364
+ }
365
+ else if (process.platform === 'win32') {
366
+ openers.push({ cmd: 'start' });
367
+ }
368
+ else {
369
+ if (isWsl()) {
370
+ // Prefer wslview when available, then fall back to Windows start.exe to open in the host browser.
371
+ openers.push({ cmd: 'wslview' });
372
+ openers.push({ cmd: 'cmd.exe', args: ['/c', 'start', '', url] });
373
+ }
374
+ openers.push({ cmd: 'xdg-open' });
375
+ }
376
+ // Add a cross-platform, low-friction fallback when nothing above is available.
377
+ openers.push({ cmd: 'sensible-browser' });
284
378
  try {
285
379
  // Fire and forget; user completes login in the opened browser window.
286
- void import('node:child_process').then(({ spawn }) => {
287
- spawn(opener, [url], { stdio: 'ignore', detached: true }).unref();
288
- });
289
- logger(`Opened ${url} locally. Please sign in; subsequent runs will reuse the session.`);
380
+ if (verbose) {
381
+ logger(`[serve] Login opener candidates: ${openers.map((o) => o.cmd).join(', ')}`);
382
+ }
383
+ const candidate = openers.find((opener) => canSpawn(opener.cmd));
384
+ if (candidate) {
385
+ const child = spawn(candidate.cmd, candidate.args ?? [url], { stdio: 'ignore', detached: true });
386
+ child.unref();
387
+ child.once('error', (error) => {
388
+ if (verbose) {
389
+ logger(`[serve] Opener ${candidate.cmd} failed: ${error instanceof Error ? error.message : String(error)}`);
390
+ }
391
+ logger(`Please open ${url} in this host's browser and sign in; then rerun.`);
392
+ });
393
+ logger(`Opened ${url} locally via ${candidate.cmd}. Please sign in; subsequent runs will reuse the session.`);
394
+ if (verbose && candidate.args) {
395
+ logger(`[serve] Opener args: ${candidate.args.join(' ')}`);
396
+ }
397
+ return true;
398
+ }
399
+ if (verbose) {
400
+ logger('[serve] No available opener found; prompting manual login.');
401
+ }
402
+ return false;
290
403
  }
291
404
  catch {
292
- logger(`Please open ${url} in this host's browser and sign in; then rerun.`);
405
+ return false;
406
+ }
407
+ }
408
+ function isWsl() {
409
+ if (process.platform !== 'linux')
410
+ return false;
411
+ return Boolean(process.env.WSL_DISTRO_NAME || os.release().toLowerCase().includes('microsoft'));
412
+ }
413
+ function canSpawn(cmd) {
414
+ if (!cmd)
415
+ return false;
416
+ try {
417
+ if (process.platform === 'win32') {
418
+ // `where` returns non-zero when the command is not found.
419
+ const result = spawnSync('where', [cmd], { stdio: 'ignore' });
420
+ return result.status === 0;
421
+ }
422
+ // `command -v` is a shell builtin; run through sh. Fallback to `which`.
423
+ const shResult = spawnSync('sh', ['-c', `command -v ${cmd}`], { stdio: 'ignore' });
424
+ if (shResult.status === 0)
425
+ return true;
426
+ const whichResult = spawnSync('which', [cmd], { stdio: 'ignore' });
427
+ return whichResult.status === 0;
428
+ }
429
+ catch {
430
+ return false;
431
+ }
432
+ }
433
+ async function launchManualLoginChrome(profileDir, url, logger) {
434
+ const timeoutMs = 7000;
435
+ let finished = false;
436
+ const timeout = setTimeout(() => {
437
+ if (!finished) {
438
+ logger(`Timed out launching Chrome for manual login. Launch Chrome manually with --user-data-dir=${profileDir} and log in to ${url}.`);
439
+ }
440
+ }, timeoutMs);
441
+ try {
442
+ const chromeLauncher = await import('chrome-launcher');
443
+ const { launch } = chromeLauncher;
444
+ const debugPort = await findAvailablePort();
445
+ logger(`Planned manual-login Chrome DevTools port: ${debugPort}`);
446
+ const chrome = await launch({
447
+ // Expose DevTools so later runs can attach instead of spawning a second Chrome.
448
+ // Use a per-serve free port so the login window stays stable for all runs.
449
+ port: debugPort,
450
+ userDataDir: profileDir,
451
+ startingUrl: url,
452
+ chromeFlags: [
453
+ '--no-first-run',
454
+ '--no-default-browser-check',
455
+ `--user-data-dir=${profileDir}`,
456
+ '--remote-allow-origins=*',
457
+ `--remote-debugging-port=${debugPort}`, // ensure DevToolsActivePort is written even on Windows
458
+ ],
459
+ });
460
+ const chosenPort = chrome?.port ?? debugPort ?? null;
461
+ if (chosenPort) {
462
+ // Write DevToolsActivePort eagerly so maybeReuseRunningChrome can attach on the next run
463
+ const devtoolsFile = path.join(profileDir, 'DevToolsActivePort');
464
+ const devtoolsFileDefault = path.join(profileDir, 'Default', 'DevToolsActivePort');
465
+ const contents = `${chosenPort}\n/devtools/browser`;
466
+ await writeFile(devtoolsFile, contents).catch(() => undefined);
467
+ await writeFile(devtoolsFileDefault, contents).catch(() => undefined);
468
+ logger(`Manual-login Chrome DevTools port: ${chosenPort}`);
469
+ logger(`If needed, DevTools JSON at http://127.0.0.1:${chosenPort}/json/version`);
470
+ }
471
+ else {
472
+ logger('Warning: unable to determine manual-login Chrome DevTools port. Remote runs may fail to attach.');
473
+ }
474
+ finished = true;
475
+ clearTimeout(timeout);
476
+ const portInfo = chosenPort ? ` (DevTools port ${chosenPort})` : '';
477
+ logger(`Opened Chrome with manual-login profile at ${profileDir}${portInfo}. Complete login, then rerun remote sessions.`);
478
+ }
479
+ catch (error) {
480
+ finished = true;
481
+ clearTimeout(timeout);
482
+ const message = error instanceof Error ? error.message : String(error);
483
+ logger(`Unable to open Chrome for manual login (${message}). Launch Chrome manually with --user-data-dir=${profileDir} and log in to ${url}.`);
293
484
  }
294
485
  }
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import fs from 'node:fs/promises';
4
4
  import { createWriteStream } from 'node:fs';
5
5
  import { DEFAULT_MODEL } from './oracle.js';
6
+ import { safeModelSlug } from './oracle/modelResolver.js';
6
7
  const ORACLE_HOME = process.env.ORACLE_HOME_DIR ?? path.join(os.homedir(), '.oracle');
7
8
  const SESSIONS_DIR = path.join(ORACLE_HOME, 'sessions');
8
9
  const METADATA_FILENAME = 'meta.json';
@@ -67,10 +68,12 @@ function modelsDir(id) {
67
68
  return path.join(sessionDir(id), MODELS_DIRNAME);
68
69
  }
69
70
  function modelJsonPath(id, model) {
70
- return path.join(modelsDir(id), `${model}${MODEL_JSON_EXTENSION}`);
71
+ const slug = safeModelSlug(model);
72
+ return path.join(modelsDir(id), `${slug}${MODEL_JSON_EXTENSION}`);
71
73
  }
72
74
  function modelLogPath(id, model) {
73
- return path.join(modelsDir(id), `${model}${MODEL_LOG_EXTENSION}`);
75
+ const slug = safeModelSlug(model);
76
+ return path.join(modelsDir(id), `${slug}${MODEL_LOG_EXTENSION}`);
74
77
  }
75
78
  async function fileExists(targetPath) {
76
79
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steipete/oracle",
3
- "version": "0.4.5",
3
+ "version": "0.5.1",
4
4
  "description": "CLI wrapper around OpenAI Responses API with GPT-5.1 Pro, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
5
5
  "type": "module",
6
6
  "main": "dist/bin/oracle-cli.js",
@@ -52,46 +52,46 @@
52
52
  "chrome-cookies-secure": "3.0.0",
53
53
  "chrome-launcher": "^1.2.1",
54
54
  "chrome-remote-interface": "^0.33.3",
55
- "clipboardy": "^4.0.0",
55
+ "clipboardy": "^5.0.1",
56
56
  "commander": "^14.0.2",
57
57
  "dotenv": "^17.2.3",
58
58
  "fast-glob": "^3.3.3",
59
59
  "gpt-tokenizer": "^3.4.0",
60
- "inquirer": "^9.3.8",
60
+ "inquirer": "9.3.8",
61
61
  "json5": "^2.2.3",
62
62
  "keytar": "^7.9.0",
63
63
  "kleur": "^4.1.5",
64
64
  "markdansi": "^0.1.3",
65
- "openai": "^6.9.0",
65
+ "openai": "^6.9.1",
66
66
  "shiki": "^3.15.0",
67
67
  "sqlite3": "^5.1.7",
68
68
  "toasted-notifier": "^10.1.0",
69
- "zod": "^3.24.1"
69
+ "zod": "3.24.1"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@anthropic-ai/tokenizer": "^0.0.4",
73
- "@biomejs/biome": "^2.3.5",
73
+ "@biomejs/biome": "^2.3.7",
74
74
  "@cdktf/node-pty-prebuilt-multiarch": "0.10.2",
75
75
  "@types/chrome-remote-interface": "^0.31.14",
76
76
  "@types/inquirer": "^9.0.9",
77
- "@types/json5": "^0.0.30",
77
+ "@types/json5": "^2.2.0",
78
78
  "@types/node": "^24.10.1",
79
- "@vitest/coverage-v8": "4.0.9",
80
- "devtools-protocol": "^0.0.1545402",
79
+ "@vitest/coverage-v8": "4.0.13",
80
+ "devtools-protocol": "^0.0.1548823",
81
81
  "es-toolkit": "^1.42.0",
82
82
  "esbuild": "^0.27.0",
83
- "puppeteer-core": "^24.30.0",
83
+ "puppeteer-core": "^24.31.0",
84
84
  "tsx": "^4.20.6",
85
85
  "typescript": "^5.9.3",
86
- "vitest": "^4.0.9"
86
+ "vitest": "^4.0.13"
87
87
  },
88
88
  "optionalDependencies": {
89
- "win-dpapi": "^1.1.0"
89
+ "win-dpapi": "npm:@primno/dpapi@2.0.1"
90
90
  },
91
91
  "scripts": {
92
92
  "docs:list": "tsx scripts/docs-list.ts",
93
93
  "build": "tsc -p tsconfig.build.json && pnpm run build:vendor",
94
- "build:vendor": "mkdir -p dist/vendor && cp -R vendor/oracle-notifier dist/vendor/oracle-notifier || true",
94
+ "build:vendor": "node -e \"const fs=require('fs'); const path=require('path'); const src=path.join('vendor','oracle-notifier'); const dest=path.join('dist','vendor','oracle-notifier'); fs.mkdirSync(dest,{recursive:true}); if(fs.existsSync(src)){ fs.cpSync(src,dest,{recursive:true,force:true}); }\"",
95
95
  "start": "pnpm run build && node ./dist/scripts/run-cli.js",
96
96
  "oracle": "pnpm start",
97
97
  "check": "pnpm run typecheck",
@@ -101,7 +101,7 @@
101
101
  "test:mcp": "pnpm run build && pnpm run test:mcp:unit && pnpm run test:mcp:mcporter",
102
102
  "test:mcp:unit": "vitest run tests/mcp*.test.ts tests/mcp/**/*.test.ts",
103
103
  "test:mcp:mcporter": "npx -y mcporter list oracle-local --schema --config config/mcporter.json && npx -y mcporter call oracle-local.sessions limit:1 --config config/mcporter.json",
104
- "test:browser": "pnpm run build && ./scripts/browser-smoke.sh",
104
+ "test:browser": "pnpm run build && tsx scripts/test-browser.ts && ./scripts/browser-smoke.sh",
105
105
  "test:live": "ORACLE_LIVE_TEST=1 vitest run tests/live --exclude tests/live/openai-live.test.ts",
106
106
  "test:pro": "ORACLE_LIVE_TEST=1 vitest run tests/live/openai-live.test.ts",
107
107
  "test:coverage": "vitest run --coverage",