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/README.md +96 -136
- package/config.example.json +2 -65
- package/package.json +1 -1
- package/src/card.js +2 -1
- package/src/cli.js +105 -24
- package/src/config.js +89 -0
- package/src/daemon.js +113 -19
- package/src/doctor.js +10 -30
- package/src/git.js +2 -2
- package/src/hook.js +1 -1
- package/src/install.js +51 -5
- package/src/pricing.js +29 -6
- package/src/privacy.js +1 -1
- package/src/scanner.js +2 -2
- package/src/server/api.js +6 -3
- package/src/server/page.js +52 -14
- package/src/server/sse.js +2 -2
- package/src/tui.js +6 -7
- package/src/ui.js +89 -0
- package/src/version.js +26 -0
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
16
|
+
return loadSharedConfig();
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export function rangeToDays(range) {
|
package/src/server/page.js
CHANGED
|
@@ -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.
|
|
2
|
+
// string constants so the bundler picks them up cleanly. Single export
|
|
3
|
+
// is `buildHtml({ port })`.
|
|
3
4
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 {
|
|
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 =
|
|
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();
|