skalpel 3.4.6 → 3.4.7

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.
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ // skalpel statusline — reads the running "time saved" counter (bumped by skalpel-hook.mjs every time
3
+ // a real recurring pattern fires) and prints one line for Claude Code's status bar. Brave's
4
+ // ads-blocked-counter, but for AI-coding thrash. Fail-open: no stats yet -> print nothing.
5
+ import { readFileSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { join } from "node:path";
8
+
9
+ const STATS_PATH = join(homedir(), ".skalpel", "stats.json");
10
+ const STEER_PATH = join(homedir(), ".skalpel", "steer.json");
11
+
12
+ // ANSI — the status bar renders color. Emphasize the WIN (time saved) in green; show the live steer
13
+ // in accent; recede the secondary numbers so the eye lands on saved-time + what skalpel's doing now.
14
+ const GREEN = "\x1b[32m";
15
+ const CYAN = "\x1b[36m";
16
+ const BOLD = "\x1b[1m";
17
+ const DIM = "\x1b[2m";
18
+ const RESET = "\x1b[0m";
19
+
20
+ function main() {
21
+ let s;
22
+ try {
23
+ s = JSON.parse(readFileSync(STATS_PATH, "utf8"));
24
+ } catch {
25
+ return; // no stats yet — print nothing rather than a misleading "0 saved"
26
+ }
27
+ const sess = (s.session_hours || 0).toFixed(1);
28
+ const total = (s.total_hours || 0).toFixed(1);
29
+ const steers = s.total_events || 0;
30
+
31
+ // The LIVE steer (written by skalpel-hook.mjs each turn) — the deterministic awareness channel: the
32
+ // user SEES skalpel steering right now, even when the model doesn't surface a 🔬 line in the answer.
33
+ let steerLabel = null;
34
+ let creditMins = null;
35
+ try {
36
+ const st = JSON.parse(readFileSync(STEER_PATH, "utf8"));
37
+ steerLabel = st?.label || null;
38
+ creditMins = st?.credit || null;
39
+ } catch {
40
+ /* no steer/credit this turn — just show the counter */
41
+ }
42
+ // The felt "just delivered" tick — the +Xmin the n+1 check credited THIS turn. Hero position, bold
43
+ // green, right after the wordmark: you SEE it land, then it folds into the growing total.
44
+ // the per-turn tick sits to the RIGHT of the running total: total = the accumulation, tick = what
45
+ // THIS turn just added. Both green (the win).
46
+ const tick = creditMins ? ` · ${BOLD}${GREEN}+${creditMins} min saved this turn${RESET}` : "";
47
+ // Animate the steering label so it reads as LIVE — Claude Code re-invokes the statusline periodically,
48
+ // so a time-derived frame cycles a small spinner. (A single-line status bar can't move text vertically;
49
+ // this is the alive-feeling equivalent.) Only spins while a steer is on the board.
50
+ const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
51
+ const frame = FRAMES[Math.floor(Date.now() / 120) % FRAMES.length];
52
+ const steering = steerLabel ? ` · ${CYAN}${frame} steering: ${steerLabel}${RESET}` : "";
53
+
54
+ // LIVE session tally — how many loops skalpel has caught THIS session (peak-end: the "end" number
55
+ // stays visible so the user sees it before they quit, even though the recap itself lands next start).
56
+ void sess;
57
+ process.stdout.write(
58
+ `${BOLD}skalpel${RESET}${steering} · ${GREEN}~${total}h saved${RESET}${tick} · ${DIM}${steers} steers${RESET}`,
59
+ );
60
+ }
61
+
62
+ main();
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ // stats.mjs — fold ~/.skalpel/metrics.ndjson into a counter hashtable + latency view, so you can see
3
+ // injection success/failure (inject rate) vs cold-start failure (cold-abort rate) at a glance.
4
+ // Usage: node ~/.skalpel/hooks/stats.mjs (all time)
5
+ // node ~/.skalpel/hooks/stats.mjs --since 24h (last N h/m/d)
6
+ import { readFileSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+
10
+ const PATH = join(homedir(), ".skalpel", "metrics.ndjson");
11
+
12
+ function sinceMs(arg) {
13
+ const m = /^(\d+)([hmd])$/.exec(arg || "");
14
+ if (!m) return 0;
15
+ const n = +m[1];
16
+ return n * { m: 60e3, h: 3600e3, d: 86400e3 }[m[2]];
17
+ }
18
+ const sinceArgIdx = process.argv.indexOf("--since");
19
+ const window = sinceArgIdx > -1 ? sinceMs(process.argv[sinceArgIdx + 1]) : 0;
20
+ const cutoff = window ? Date.now() - window : 0;
21
+
22
+ let lines = [];
23
+ try {
24
+ lines = readFileSync(PATH, "utf8").trim().split("\n").filter(Boolean);
25
+ } catch {
26
+ console.log("no metrics yet (" + PATH + ")");
27
+ process.exit(0);
28
+ }
29
+
30
+ // counter hashtable: { prompt: {inject: n, silent: n, abort: n, ...}, session: {...} }
31
+ const counts = {};
32
+ const lat = {}; // event -> [ms, ...] for non-null ms
33
+ let total = 0;
34
+ for (const line of lines) {
35
+ let e;
36
+ try {
37
+ e = JSON.parse(line);
38
+ } catch {
39
+ continue;
40
+ }
41
+ if (cutoff && new Date(e.ts).getTime() < cutoff) continue;
42
+ total++;
43
+ (counts[e.event] ||= {})[e.outcome] = ((counts[e.event] ||= {})[e.outcome] || 0) + 1;
44
+ if (typeof e.ms === "number") (lat[e.event] ||= []).push(e.ms);
45
+ }
46
+
47
+ function pct(arr, p) {
48
+ if (!arr.length) return null;
49
+ const s = [...arr].sort((a, b) => a - b);
50
+ return s[Math.min(s.length - 1, Math.floor((p / 100) * s.length))];
51
+ }
52
+
53
+ const label = window ? `last ${process.argv[sinceArgIdx + 1]}` : "all time";
54
+ console.log(`\nskalpel injection telemetry — ${label} — ${total} events\n`);
55
+ for (const ev of Object.keys(counts)) {
56
+ const c = counts[ev];
57
+ const n = Object.values(c).reduce((a, b) => a + b, 0);
58
+ const ok = c.inject || 0;
59
+ const cold = c.abort || 0;
60
+ console.log(`■ ${ev} (${n})`);
61
+ for (const [k, v] of Object.entries(c).sort((a, b) => b[1] - a[1])) {
62
+ console.log(` ${k.padEnd(14)} ${String(v).padStart(5)} ${((100 * v) / n).toFixed(1)}%`);
63
+ }
64
+ const L = lat[ev] || [];
65
+ if (L.length) {
66
+ console.log(
67
+ ` latency p50=${pct(L, 50)}ms p95=${pct(L, 95)}ms max=${Math.max(...L)}ms`,
68
+ );
69
+ }
70
+ console.log(
71
+ ` → inject rate ${((100 * ok) / n).toFixed(1)}% cold-abort rate ${((100 * cold) / n).toFixed(1)}%\n`,
72
+ );
73
+ }