claude-rpc 0.5.0 → 0.6.0

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/src/install.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  readdirSync, unlinkSync,
9
9
  } from 'node:fs';
10
10
  import { dirname, join, resolve } from 'node:path';
11
- import { spawn } from 'node:child_process';
11
+ import { spawn, spawnSync } from 'node:child_process';
12
12
  import {
13
13
  CLAUDE_SETTINGS, CONFIG_PATH, USER_CONFIG_DIR, ROOT,
14
14
  HOOK_SCRIPT, IS_PACKAGED, IS_NPM_INSTALL,
@@ -135,10 +135,10 @@ function sweepStaleCanonicalBackups() {
135
135
  const prefix = CANONICAL_EXE_NAME + '.old-';
136
136
  for (const name of readdirSync(CANONICAL_INSTALL_DIR)) {
137
137
  if (name.startsWith(prefix)) {
138
- try { unlinkSync(join(CANONICAL_INSTALL_DIR, name)); } catch {}
138
+ try { unlinkSync(join(CANONICAL_INSTALL_DIR, name)); } catch { /* file locked or vanished — sweep is best-effort */ }
139
139
  }
140
140
  }
141
- } catch {}
141
+ } catch { /* install dir unreadable — nothing to sweep */ }
142
142
  }
143
143
 
144
144
  // Copy the running binary into CANONICAL_EXE if it's not already there.
@@ -160,7 +160,7 @@ export function ensureCanonicalExe(currentExe) {
160
160
  console.log(` exe already installed → ${CANONICAL_EXE}`);
161
161
  return CANONICAL_EXE;
162
162
  }
163
- } catch {}
163
+ } catch { /* stat failed — fall through to copy attempt */ }
164
164
  }
165
165
 
166
166
  try {
@@ -170,7 +170,7 @@ export function ensureCanonicalExe(currentExe) {
170
170
  // renamed copy when the process exits.
171
171
  if (process.platform === 'win32' && existsSync(CANONICAL_EXE)) {
172
172
  try { renameSync(CANONICAL_EXE, CANONICAL_EXE + '.old-' + Date.now()); }
173
- catch {}
173
+ catch { /* not running, no rename needed — copyFileSync below will just overwrite */ }
174
174
  }
175
175
  copyFileSync(currentExe, CANONICAL_EXE);
176
176
  if (process.platform !== 'win32') chmodSync(CANONICAL_EXE, 0o755);
@@ -270,6 +270,39 @@ export function migrateConfig() {
270
270
  return true;
271
271
  }
272
272
 
273
+ // Round-trip a synthetic SessionStart event through the same launcher
274
+ // shape that Claude Code itself will use. Proves the hook command actually
275
+ // resolves and runs — without this, `setup` could happily wire a broken
276
+ // command and report success, leaving the user to discover the breakage
277
+ // the next time they open Claude Code. Returns { ok, detail }.
278
+ function verifyHookPipe(exePath) {
279
+ const cmd = IS_PACKAGED ? exePath
280
+ : IS_NPM_INSTALL ? 'claude-rpc'
281
+ : process.execPath;
282
+ const args = IS_PACKAGED || IS_NPM_INSTALL
283
+ ? ['hook', 'SessionStart']
284
+ : [HOOK_SCRIPT, 'SessionStart'];
285
+ let result;
286
+ try {
287
+ result = spawnSync(cmd, args, {
288
+ input: '',
289
+ encoding: 'utf8',
290
+ timeout: 3000,
291
+ windowsHide: true,
292
+ });
293
+ } catch (e) {
294
+ return { ok: false, detail: `spawn failed: ${e.message}` };
295
+ }
296
+ if (result.error) return { ok: false, detail: `spawn error: ${result.error.message}` };
297
+ if (result.status !== 0) {
298
+ return { ok: false, detail: `hook exit ${result.status}: ${(result.stderr || '').trim().slice(0, 120)}` };
299
+ }
300
+ if (!result.stdout.includes('"continue"')) {
301
+ return { ok: false, detail: `unexpected hook output: ${result.stdout.trim().slice(0, 120)}` };
302
+ }
303
+ return { ok: true, detail: 'SessionStart round-trip succeeded' };
304
+ }
305
+
273
306
  export async function install({ exePath, withStartup = true } = {}) {
274
307
  if (process.platform !== 'win32' && withStartup) {
275
308
  console.warn('Note: startup registration only works on Windows; other steps still run.');
@@ -288,6 +321,18 @@ export async function install({ exePath, withStartup = true } = {}) {
288
321
  try { await addStartupEntry(target); }
289
322
  catch (e) { console.warn(` startup entry failed: ${e.message}`); }
290
323
  }
324
+
325
+ // Proof the hook pipe actually fires. A setup that returns success
326
+ // without verification is a lie — we caught broken-hook-path bugs
327
+ // twice during v0.3.x because no one ran a real event after install.
328
+ const probe = verifyHookPipe(target);
329
+ if (probe.ok) {
330
+ console.log(` hook pipe ✓ ${probe.detail}`);
331
+ } else {
332
+ console.warn(` hook pipe ✗ ${probe.detail}`);
333
+ console.warn(` ↳ run \`claude-rpc doctor\` for a full diagnostic`);
334
+ }
335
+
291
336
  console.log('\nDone.');
292
337
  console.log(`Edit ${CONFIG_PATH} to set your Discord clientId, then run:`);
293
338
  // Per-mode "start" instructions — packaged exe takes a daemon subcommand,
@@ -301,6 +346,7 @@ export async function install({ exePath, withStartup = true } = {}) {
301
346
  console.log(` node "${join(ROOT, 'src', 'daemon.js').replace(/\\/g, '/')}"`);
302
347
  console.log(` # or: claude-rpc start (if you've run \`npm link\`)`);
303
348
  }
349
+ console.log(`\nThen: \`claude-rpc doctor\` to verify everything is wired.`);
304
350
  }
305
351
 
306
352
  export async function uninstall() {
package/src/pricing.js CHANGED
@@ -29,16 +29,39 @@ const PRICING = {
29
29
 
30
30
  const DEFAULT = PRICING.sonnet;
31
31
 
32
+ const TIERS = new Set(['opus', 'sonnet', 'haiku']);
33
+
32
34
  // Map a model id like "claude-opus-4-7-20251101" to a pricing key.
35
+ //
36
+ // The old implementation did `s.includes(key)` against the PRICING keys
37
+ // sorted by descending length, which silently mis-routed any future model
38
+ // id whose name contained one of those substrings out of order
39
+ // (e.g. `claude-sonneteer-x` matching `sonnet`). Now: split the id on `-`,
40
+ // find the first explicit tier token, then read the version digits that
41
+ // follow. Falls back from `tier-major-minor` → `tier-major` → tier generic
42
+ // → sonnet, in that order.
33
43
  export function pricingKeyFor(modelId) {
34
44
  if (!modelId) return 'sonnet';
35
- const s = String(modelId).toLowerCase();
36
- // Most specific match wins.
37
- const candidates = Object.keys(PRICING).sort((a, b) => b.length - a.length);
38
- for (const key of candidates) {
39
- if (s.includes(key)) return key;
45
+ const parts = String(modelId).toLowerCase().split('-');
46
+
47
+ let tier = null;
48
+ let tierIdx = -1;
49
+ for (let i = 0; i < parts.length; i++) {
50
+ if (TIERS.has(parts[i])) { tier = parts[i]; tierIdx = i; break; }
51
+ }
52
+ if (!tier) return 'sonnet'; // unknown family — sonnet-class fallback
53
+
54
+ const major = parts[tierIdx + 1];
55
+ const minor = parts[tierIdx + 2];
56
+ if (major && /^\d+$/.test(major)) {
57
+ if (minor && /^\d+$/.test(minor)) {
58
+ const k = `${tier}-${major}-${minor}`;
59
+ if (PRICING[k]) return k;
60
+ }
61
+ const km = `${tier}-${major}`;
62
+ if (PRICING[km]) return km;
40
63
  }
41
- return 'sonnet';
64
+ return tier; // generic tier rates
42
65
  }
43
66
 
44
67
  export function ratesFor(modelId) {
package/src/privacy.js CHANGED
@@ -83,7 +83,7 @@ function readPrivateList() {
83
83
  try {
84
84
  const v = JSON.parse(readFileSync(PRIVATE_LIST_PATH, 'utf8'));
85
85
  if (Array.isArray(v?.paths)) return v;
86
- } catch {}
86
+ } catch { /* broken JSON ≡ no list (treat as empty rather than crash) */ }
87
87
  return { paths: [] };
88
88
  }
89
89
 
package/src/scanner.js CHANGED
@@ -358,7 +358,7 @@ function readTranscriptCwd(path, mtimeMs) {
358
358
  const r = safeJson(line);
359
359
  if (r?.cwd) { cwd = r.cwd; break; }
360
360
  }
361
- } catch {}
361
+ } catch { /* transcript head unreadable — cwd stays null, project name falls back to slug */ }
362
362
  cwdCache.set(path, { mtime: mtimeMs, cwd });
363
363
  return cwd;
364
364
  }
@@ -464,7 +464,7 @@ function readNotificationsByDay() {
464
464
  const k = dayKey(e.ts);
465
465
  out[k] = (out[k] || 0) + 1;
466
466
  }
467
- } catch {}
467
+ } catch { /* events log unreadable/truncated — return whatever we got, the aggregate will just under-count notifications */ }
468
468
  return out;
469
469
  }
470
470
 
package/src/server/api.js CHANGED
@@ -2,15 +2,18 @@
2
2
  // over the aggregate + state files; no HTTP concerns. Tested separately
3
3
  // from the routing layer.
4
4
 
5
- import { readFileSync } from 'node:fs';
6
5
  import { basename } from 'node:path';
7
6
  import { readState } from '../state.js';
8
7
  import { buildVars, fillTemplate, applyIdle, framePasses } from '../format.js';
9
8
  import { readAggregate, findLiveSessions, dayKey } from '../scanner.js';
10
- import { CONFIG_PATH } from '../paths.js';
9
+ import { loadConfig as loadSharedConfig } from '../config.js';
11
10
 
11
+ // Re-export under the historical name so any external callers (e.g. tests
12
+ // that did `import { loadConfig } from '../api.js'`) still resolve.
13
+ // Internally everything uses the shared loader so a bad config doesn't
14
+ // blank out the dashboard with `{}` — it falls back to defaults.
12
15
  export function loadConfig() {
13
- try { return JSON.parse(readFileSync(CONFIG_PATH, 'utf8')); } catch { return {}; }
16
+ return loadSharedConfig();
14
17
  }
15
18
 
16
19
  export function rangeToDays(range) {
@@ -1,17 +1,22 @@
1
1
  // All browser-side assets for the local web dashboard, packaged as JS
2
- // string constants so the bundler picks them up cleanly. Three blocks:
2
+ // string constants so the bundler picks them up cleanly. Single export
3
+ // is `buildHtml({ port })`.
3
4
  //
4
- // CSS — full stylesheet, dark + light themes
5
- // LANG_PALETTE per-language color JSON consumed by the
6
- // client-side language stack chart
7
- // HTML the full HTML scaffold, interpolating
8
- // both of the above plus the browser-side
9
- // JS produced by HTML_SCRIPT_PLACEHOLDER()
10
- // HTML_SCRIPT_PLACEHOLDER the client-side runtime (SSE wiring,
11
- // range selector, drilldowns, theme toggle,
12
- // charts, keyboard shortcuts)
5
+ // ┌─ TABLE OF CONTENTS ─────────────────────────────────────┐
6
+ // │ §1 CSS stylesheet (light + dark) │ ~line 26
7
+ // │ §2 LANG_PALETTE — language swatch hex │ ~line 400
8
+ // │ §3 buildHtml — HTML scaffold + interpolation │ ~line 420
9
+ // │ §4 HTML_SCRIPT_BODY — client runtime (SSE, charts, │ ~line 700
10
+ // │ range selector, drilldowns, │
11
+ // │ theme toggle, keyboard) │
12
+ // └─────────────────────────────────────────────────────────┘
13
13
  //
14
- // Only HTML is exported. Edit the three blocks below in isolation.
14
+ // Each section is just a string constant no runtime dependencies
15
+ // between them. Edit one without re-reading the others.
16
+
17
+ // ─────────────────────────────────────────────────────────────────────
18
+ // §1 CSS
19
+ // ─────────────────────────────────────────────────────────────────────
15
20
 
16
21
  const CSS = `
17
22
  :root {
@@ -389,7 +394,13 @@ footer a:hover { color: var(--text-2); }
389
394
  }
390
395
  `;
391
396
 
392
- // Color palette for languages, by name. Stable across renders.
397
+ // ─────────────────────────────────────────────────────────────────────
398
+ // §2 LANG_PALETTE — language → swatch hex
399
+ // ─────────────────────────────────────────────────────────────────────
400
+ //
401
+ // Stable across renders. Embedded as a JS object literal inside the
402
+ // client script template; the client parses it once at startup.
403
+
393
404
  const LANG_PALETTE = `{
394
405
  'JavaScript': '#f7df1e', 'TypeScript': '#3178c6', 'Python': '#3776ab', 'Rust': '#dea584',
395
406
  'Go': '#00add8', 'Ruby': '#cc342d', 'Java': '#b07219', 'Kotlin': '#a97bff',
@@ -410,6 +421,14 @@ const LANG_PALETTE = `{
410
421
  'Config': '#888', 'Git': '#f1502f',
411
422
  }`;
412
423
 
424
+ // ─────────────────────────────────────────────────────────────────────
425
+ // §3 buildHtml — HTML scaffold + interpolation
426
+ // ─────────────────────────────────────────────────────────────────────
427
+ //
428
+ // Returns the full HTML string. The only dynamic value is the port,
429
+ // which is fixed at server startup, so this is composed once and reused
430
+ // for every request (see src/server/index.js).
431
+
413
432
  function buildHtml({ port }) {
414
433
  const PORT = port;
415
434
  return String.raw`<!doctype html>
@@ -686,6 +705,25 @@ ${HTML_SCRIPT_PLACEHOLDER()}
686
705
  </html>`;
687
706
  }
688
707
 
708
+ // ─────────────────────────────────────────────────────────────────────
709
+ // §4 HTML_SCRIPT_BODY — client runtime
710
+ // ─────────────────────────────────────────────────────────────────────
711
+ //
712
+ // Returned as a string and inlined into the HTML at the bottom of the
713
+ // document. Vanilla browser JS — no bundler, no framework, no npm. The
714
+ // IIFE keeps all locals out of window.
715
+ //
716
+ // Sub-structure within the IIFE:
717
+ // • $ — DOM accessor
718
+ // • LANGS — parsed language palette
719
+ // • Utilities — fmtH / fmtN / fmtCost / escape / etc.
720
+ // • State buckets — liveData / aggData / allFrames / rotationTimer
721
+ // • Range pills — active selector + delta vs previous range
722
+ // • Charts — area chart, sparkline, heatmap, lang stack
723
+ // • Drilldowns — project drawer + day-detail modal
724
+ // • SSE wiring — EventSource('/events') → refresh handlers
725
+ // • Keyboard — 1..5 range, t theme, esc close, ? help
726
+
689
727
  function HTML_SCRIPT_PLACEHOLDER() {
690
728
  return `(() => {
691
729
  const $ = (id) => document.getElementById(id);
@@ -1239,10 +1277,10 @@ function HTML_SCRIPT_PLACEHOLDER() {
1239
1277
  await fetchAggregate();
1240
1278
  await fetchInsights();
1241
1279
  }
1242
- } catch {}
1280
+ } catch { /* malformed SSE frame — wait for the next one */ }
1243
1281
  };
1244
1282
  ev.onerror = () => { $('conn-state').textContent = 'reconnecting'; setTimeout(() => { $('conn-state').textContent = 'live'; }, 4000); };
1245
- } catch {}
1283
+ } catch { /* EventSource constructor failed (very old browser) — dashboard falls back to one-shot fetches */ }
1246
1284
  }
1247
1285
 
1248
1286
  async function refreshState() {
package/src/server/sse.js CHANGED
@@ -22,11 +22,11 @@ export function watchSources() {
22
22
  clearTimeout(stTimer);
23
23
  stTimer = setTimeout(() => broadcast({ type: 'state' }), 200);
24
24
  });
25
- } catch {}
25
+ } catch { /* state file not on disk yet — dashboard still works, just no SSE state events until daemon writes one */ }
26
26
  try {
27
27
  watch(AGGREGATE_PATH, () => {
28
28
  clearTimeout(agTimer);
29
29
  agTimer = setTimeout(() => broadcast({ type: 'aggregate' }), 200);
30
30
  });
31
- } catch {}
31
+ } catch { /* aggregate not on disk yet — dashboard renders empty stats until first scan completes */ }
32
32
  }
package/src/tui.js CHANGED
@@ -9,7 +9,8 @@ import { readFileSync, existsSync } from 'node:fs';
9
9
  import { readState } from './state.js';
10
10
  import { readAggregate, findLiveSessions, dayKey, weekKey } from './scanner.js';
11
11
  import { buildVars, applyIdle, humanProject } from './format.js';
12
- import { CONFIG_PATH, PID_PATH } from './paths.js';
12
+ import { loadConfig } from './config.js';
13
+ import { PID_PATH } from './paths.js';
13
14
  import { fmtCost } from './pricing.js';
14
15
  import { generateInsights } from './insights.js';
15
16
 
@@ -52,9 +53,7 @@ let exiting = false;
52
53
  function loadSnapshot() {
53
54
  let state = readState();
54
55
  state.liveSessions = findLiveSessions({ thresholdMs: 90_000 });
55
- const config = existsSync(CONFIG_PATH)
56
- ? (() => { try { return JSON.parse(readFileSync(CONFIG_PATH, 'utf8')); } catch { return {}; } })()
57
- : {};
56
+ const config = loadConfig();
58
57
  state = applyIdle(state, config);
59
58
  const aggregate = readAggregate() || {};
60
59
  const vars = buildVars(state, config, aggregate);
@@ -371,7 +370,7 @@ function cleanup() {
371
370
  if (exiting) return;
372
371
  exiting = true;
373
372
  if (refreshTimer) clearInterval(refreshTimer);
374
- try { process.stdin.setRawMode(false); } catch {}
373
+ try { process.stdin.setRawMode(false); } catch { /* not a tty (CI, pipe) — no-op */ }
375
374
  process.stdin.pause();
376
375
  process.stdout.write(SHOW_CURSOR + CLEAR + '\n');
377
376
  process.exit(0);
@@ -402,7 +401,7 @@ export function startTui() {
402
401
  }
403
402
  process.stdout.write(HIDE_CURSOR);
404
403
 
405
- try { process.stdin.setRawMode(true); } catch {}
404
+ try { process.stdin.setRawMode(true); } catch { /* not a tty — TUI will print once and exit */ }
406
405
  process.stdin.resume();
407
406
  process.stdin.on('data', handleKey);
408
407
 
@@ -410,7 +409,7 @@ export function startTui() {
410
409
  process.on('SIGTERM', cleanup);
411
410
  process.on('SIGHUP', cleanup);
412
411
  process.on('exit', () => {
413
- try { process.stdin.setRawMode(false); } catch {}
412
+ try { process.stdin.setRawMode(false); } catch { /* not a tty (CI, pipe) — no-op */ }
414
413
  process.stdout.write(SHOW_CURSOR);
415
414
  });
416
415
  process.stdout.on('resize', () => render());
package/src/ui.js ADDED
@@ -0,0 +1,89 @@
1
+ // Shared CLI output primitives.
2
+ //
3
+ // Three duplicates of the same ANSI table + symbol set existed in cli.js,
4
+ // doctor.js, and tui.js. This is the one place; everything else imports.
5
+ //
6
+ // All output goes to stdout/stderr via console.log/console.error. The
7
+ // daemon's file-bound `log()` is a separate concern (no tty, no color)
8
+ // and stays in src/daemon.js — these helpers are for human-facing
9
+ // surfaces only.
10
+ //
11
+ // Standard exit codes (also documented in --help):
12
+ // 0 success
13
+ // 1 user error — bad args, unknown command, malformed input
14
+ // 2 system error — IO failed, Discord unreachable, etc.
15
+ // 3 wrong state — daemon already running, no aggregate yet, etc.
16
+
17
+ import process from 'node:process';
18
+
19
+ const TTY = process.stdout.isTTY && !process.env.NO_COLOR;
20
+
21
+ export const c = {
22
+ reset: TTY ? '\x1b[0m' : '',
23
+ dim: TTY ? '\x1b[2m' : '',
24
+ bold: TTY ? '\x1b[1m' : '',
25
+ red: TTY ? '\x1b[31m' : '',
26
+ green: TTY ? '\x1b[32m' : '',
27
+ yellow: TTY ? '\x1b[33m' : '',
28
+ blue: TTY ? '\x1b[34m' : '',
29
+ magenta: TTY ? '\x1b[35m' : '',
30
+ cyan: TTY ? '\x1b[36m' : '',
31
+ gray: TTY ? '\x1b[90m' : '',
32
+ };
33
+
34
+ export const SYM_OK = TTY ? `${c.green}✓${c.reset}` : '[ok] ';
35
+ export const SYM_FAIL = TTY ? `${c.red}✗${c.reset}` : '[fail]';
36
+ export const SYM_WARN = TTY ? `${c.yellow}!${c.reset}` : '[warn]';
37
+ export const SYM_INFO = TTY ? `${c.cyan}·${c.reset}` : '[info]';
38
+
39
+ // Standard exit-code values. Use these instead of process.exit(1) so
40
+ // intent is visible in the source.
41
+ export const EX_OK = 0;
42
+ export const EX_USER_ERROR = 1;
43
+ export const EX_SYS_ERROR = 2;
44
+ export const EX_BAD_STATE = 3;
45
+
46
+ // Print a one-line message plus an indented dim hint on the next line.
47
+ // Hint is the tired-user safety net: every failure surface tells you
48
+ // what to type next. Empty hint omits the second line.
49
+ function withHint(sym, label, hint, stream = process.stdout) {
50
+ stream.write(` ${sym} ${label}\n`);
51
+ if (hint) stream.write(` ${c.gray}↳ ${hint}${c.reset}\n`);
52
+ }
53
+
54
+ export function ok(label, detail = '') {
55
+ process.stdout.write(` ${SYM_OK} ${label}${detail ? ` ${c.dim}${detail}${c.reset}` : ''}\n`);
56
+ }
57
+
58
+ export function info(label, detail = '') {
59
+ process.stdout.write(` ${SYM_INFO} ${label}${detail ? ` ${c.dim}${detail}${c.reset}` : ''}\n`);
60
+ }
61
+
62
+ export function warn(label, hint = '') {
63
+ withHint(SYM_WARN, label, hint);
64
+ }
65
+
66
+ // Print a failure with a hint and exit with the given code. The hint is
67
+ // the difference between a frustrated user and a fixed user — always
68
+ // supply one when you can, and default it to `claude-rpc doctor` when
69
+ // you have no better idea.
70
+ export function fail(label, { hint = 'run `claude-rpc doctor` for a full diagnostic', code = EX_USER_ERROR } = {}) {
71
+ withHint(SYM_FAIL, label, hint, process.stderr);
72
+ process.exit(code);
73
+ }
74
+
75
+ // Compatibility with doctor.js's existing API. Same `check(label, status,
76
+ // detail, hint)` signature; doctor.js can switch its private copy out for
77
+ // this without behavior change.
78
+ export function check(label, status, detail = '', hint = '') {
79
+ let sym;
80
+ if (status === 'pass') sym = SYM_OK;
81
+ else if (status === 'fail') sym = SYM_FAIL;
82
+ else if (status === 'warn') sym = SYM_WARN;
83
+ else sym = SYM_INFO;
84
+ const tail = detail ? ` ${c.dim}${detail}${c.reset}` : '';
85
+ process.stdout.write(` ${sym} ${label}${tail}\n`);
86
+ if (hint && status !== 'pass') {
87
+ process.stdout.write(` ${c.gray}↳ ${hint}${c.reset}\n`);
88
+ }
89
+ }
package/src/version.js ADDED
@@ -0,0 +1,26 @@
1
+ // Single source of truth for the user-visible version string.
2
+ //
3
+ // Read from package.json at module load (works in dev + npm-installed
4
+ // modes). For packaged SEA exes, package.json isn't shipped — `npm run
5
+ // build:exe` snapshots whatever package.json holds at build time and
6
+ // the BAKED fallback below catches the SEA-only "package.json missing"
7
+ // case. Bump BAKED when cutting a release; the test in
8
+ // test/version.test.js asserts the two stay in sync.
9
+
10
+ import { readFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { ROOT } from './paths.js';
13
+
14
+ const BAKED = '0.6.0';
15
+
16
+ function readPkgVersion() {
17
+ try {
18
+ const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'));
19
+ if (pkg && typeof pkg.version === 'string') return pkg.version;
20
+ } catch {
21
+ // package.json not on disk (SEA exe) or unreadable — fall back to BAKED.
22
+ }
23
+ return BAKED;
24
+ }
25
+
26
+ export const VERSION = readPkgVersion();