skalpel 4.0.1 → 4.0.3
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/package.json +1 -1
- package/reveal.mjs +61 -0
- package/skalpel-hook-session.mjs +5 -3
- package/skalpel-hook.mjs +8 -3
- package/skalpel-setup.mjs +36 -5
package/package.json
CHANGED
package/reveal.mjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// reveal.mjs — the one-shot "your graph is ready" reveal, shared by both hooks.
|
|
2
|
+
//
|
|
3
|
+
// The detached background build (skalpel-setup __build) drops ~/.skalpel/insights-ready.json when it
|
|
4
|
+
// finishes. The FIRST hook to fire after that (the next per-turn prompt OR the next session start)
|
|
5
|
+
// surfaces this reveal IN the Claude Code conversation — the native surface, no OS pop-up — then clears
|
|
6
|
+
// the marker so it shows exactly once.
|
|
7
|
+
//
|
|
8
|
+
// Tone: this is the first time the user is seeing what skalpel learned about them, so the note directs
|
|
9
|
+
// the model to TEACH them their own pattern in plain, warm English — never the internal notation, never
|
|
10
|
+
// a stats dump.
|
|
11
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
export function graphReadyNote() {
|
|
16
|
+
const p = join(homedir(), ".skalpel", "insights-ready.json");
|
|
17
|
+
let m;
|
|
18
|
+
try {
|
|
19
|
+
m = JSON.parse(readFileSync(p, "utf8"));
|
|
20
|
+
} catch {
|
|
21
|
+
return null; // nothing pending
|
|
22
|
+
}
|
|
23
|
+
if (!m || !m.ready) return null;
|
|
24
|
+
try {
|
|
25
|
+
writeFileSync(p, JSON.stringify({ ready: false })); // one-shot — clear so it never repeats
|
|
26
|
+
} catch {
|
|
27
|
+
/* if we can't clear it, better to drop the reveal than repeat it — but proceed once */
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const h = (m.report && m.report.headline) || {};
|
|
31
|
+
const trap = m.report && (m.report.discriminative || [])[0] && m.report.discriminative[0].seq;
|
|
32
|
+
const n = m.n_sessions || h.sessions || 0;
|
|
33
|
+
|
|
34
|
+
const facts = [];
|
|
35
|
+
if (h.hours)
|
|
36
|
+
facts.push(`they've spent about ${h.hours} hours working with AI across these sessions`);
|
|
37
|
+
if (h.rework_pct)
|
|
38
|
+
facts.push(
|
|
39
|
+
`roughly ${h.rework_pct}% of that time went into REWORK — going back to redo, re-explain, or ` +
|
|
40
|
+
`re-fix things they'd already worked on (that's the time skalpel is built to give back)`,
|
|
41
|
+
);
|
|
42
|
+
if (trap)
|
|
43
|
+
facts.push(
|
|
44
|
+
`the habit that most often kicks off that loop, in their own history, is the sequence "${trap}" ` +
|
|
45
|
+
`— TRANSLATE this into plain human language (e.g. "you ask me to explain something, then keep ` +
|
|
46
|
+
`refining the same thing in circles") and NEVER show the raw sequence or any arrow notation`,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
`[skalpel — the user's behavioral graph just finished building in the background, from ${n} of ` +
|
|
51
|
+
`their recent coding sessions. This is the FIRST time they're seeing what it learned about them, so ` +
|
|
52
|
+
`explain it like they've never seen their own patterns before: warm, plain, concrete — NO jargon, ` +
|
|
53
|
+
`NO internal notation, NO stats dump. Open your reply with ONE short line — ` +
|
|
54
|
+
`"🔬 skalpel · your graph's ready — here's what I noticed about how you work:" — then, in 2-3 ` +
|
|
55
|
+
`friendly plain-English sentences, tell them what it found` +
|
|
56
|
+
(facts.length ? `: ${facts.join("; ")}` : "") +
|
|
57
|
+
`. Close by reassuring them this is exactly the loop skalpel now quietly watches for and steers them ` +
|
|
58
|
+
`out of, live, while they code — they don't have to track it themselves. Then continue with whatever ` +
|
|
59
|
+
`they actually asked. Keep the whole thing short and human.]`
|
|
60
|
+
);
|
|
61
|
+
}
|
package/skalpel-hook-session.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { join } from "node:path";
|
|
|
12
12
|
import { identity } from "./auth.mjs";
|
|
13
13
|
import { record } from "./metrics.mjs";
|
|
14
14
|
import { recordInsight } from "./insights.mjs";
|
|
15
|
+
import { graphReadyNote } from "./reveal.mjs";
|
|
15
16
|
|
|
16
17
|
// The statusline counter is PER-SESSION: it shows the estimated rework skalpel flagged during THIS
|
|
17
18
|
// session, not a forever-accumulating lifetime total (which reads as a bragging trophy and is
|
|
@@ -169,9 +170,10 @@ async function main() {
|
|
|
169
170
|
record("session", outcome, Date.now() - t0);
|
|
170
171
|
}
|
|
171
172
|
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
const
|
|
173
|
+
// A just-finished background build leads (the async payoff — surface it the instant it's ready),
|
|
174
|
+
// then last session's recap, then the standing profile. Any can fire alone.
|
|
175
|
+
const readyNote = graphReadyNote();
|
|
176
|
+
const additionalContext = [readyNote, recapNote, context].filter(Boolean).join("\n\n");
|
|
175
177
|
|
|
176
178
|
// LOCAL INSIGHT RECORDS (~/.skalpel/insights.ndjson) — plain-English rows for the TUI, one per
|
|
177
179
|
// note that fired. recordInsight swallows everything, so these never affect the hook output.
|
package/skalpel-hook.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { join } from "node:path";
|
|
|
14
14
|
import { identity } from "./auth.mjs";
|
|
15
15
|
import { record } from "./metrics.mjs";
|
|
16
16
|
import { recordInsight } from "./insights.mjs";
|
|
17
|
+
import { graphReadyNote } from "./reveal.mjs";
|
|
17
18
|
|
|
18
19
|
// Config precedence: env > ~/.skalpel/client.json (written by `skalpel-prosumer setup`) > default.
|
|
19
20
|
function fileCfg() {
|
|
@@ -682,9 +683,13 @@ async function main() {
|
|
|
682
683
|
session_id: sessionId,
|
|
683
684
|
});
|
|
684
685
|
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
|
|
686
|
+
// A just-finished background build leads (one-shot: surface "your graph's ready" the instant the next
|
|
687
|
+
// prompt fires, so the user doesn't have to restart a session to see it), then ackNote (feedback
|
|
688
|
+
// confirmation), the reset interrupt (most acute), the server steer, and the win note. Any can fire alone.
|
|
689
|
+
const readyNote = graphReadyNote();
|
|
690
|
+
const additionalContext = [readyNote, ackNote, resetNote, winNote, injection]
|
|
691
|
+
.filter(Boolean)
|
|
692
|
+
.join("\n\n");
|
|
688
693
|
if (additionalContext) {
|
|
689
694
|
process.stdout.write(
|
|
690
695
|
JSON.stringify({
|
package/skalpel-setup.mjs
CHANGED
|
@@ -818,8 +818,12 @@ async function main() {
|
|
|
818
818
|
// so the user SEES skalpel already knows them before typing a prompt. Escape hatches:
|
|
819
819
|
// SKALPEL_SETUP_BACKGROUND=1 keeps the old fast "warming in the background" path; non-TTY (CI)
|
|
820
820
|
// always goes background/silent so a pipeline never blocks on the reveal.
|
|
821
|
+
// DEFAULT is now BACKGROUND: `skalpel` returns you to your terminal in seconds and the graph builds
|
|
822
|
+
// detached — the batches judge + fold server-side while you keep working, and a desktop notification
|
|
823
|
+
// + a "your graph is ready" line at your next Claude Code session surface the insights when it's done.
|
|
824
|
+
// Blocking in the foreground (watch all N batches) is opt-in for anyone who wants to see it live.
|
|
821
825
|
const wantReveal =
|
|
822
|
-
|
|
826
|
+
process.env.SKALPEL_SETUP_WAIT_FOR_GRAPH === "1" &&
|
|
823
827
|
process.env.SKALPEL_SETUP_BACKGROUND !== "1";
|
|
824
828
|
|
|
825
829
|
if (process.env.SKALPEL_DEMO) {
|
|
@@ -858,14 +862,20 @@ async function main() {
|
|
|
858
862
|
);
|
|
859
863
|
}
|
|
860
864
|
} else if (files.length) {
|
|
861
|
-
//
|
|
865
|
+
// DEFAULT async path: kick off the detached build and hand the terminal straight back.
|
|
862
866
|
startBackgroundBuild();
|
|
863
867
|
const capped = Math.min(
|
|
864
868
|
files.length,
|
|
865
|
-
Math.max(1, Number.parseInt(process.env.SKALPEL_MAX_UPLOAD_SESSIONS || "
|
|
869
|
+
Math.max(1, Number.parseInt(process.env.SKALPEL_MAX_UPLOAD_SESSIONS || "40", 10)),
|
|
870
|
+
);
|
|
871
|
+
console.log(
|
|
872
|
+
` ${G}✓${X} Building your graph from your newest ${capped} session${capped === 1 ? "" : "s"} — ${B}in the background${X}.`,
|
|
873
|
+
);
|
|
874
|
+
console.log(
|
|
875
|
+
` ${D}Keep coding. I'll notify you and surface your insights the moment it's ready.${X}`,
|
|
866
876
|
);
|
|
867
877
|
await reportLiveAndLaunch(
|
|
868
|
-
`\n ${B}${G}You're live.${X} ${D}
|
|
878
|
+
`\n ${B}${G}You're live.${X} ${D}every prompt already pulls your history as it learns.${X}\n`,
|
|
869
879
|
);
|
|
870
880
|
} else {
|
|
871
881
|
await reportLiveAndLaunch(
|
|
@@ -875,6 +885,8 @@ async function main() {
|
|
|
875
885
|
}
|
|
876
886
|
|
|
877
887
|
// The detached background build (spawned by main): does ONLY the upload+judge, no banner/hooks.
|
|
888
|
+
// On success it stashes the fresh profile (for the next Claude Code SessionStart to reveal) and fires
|
|
889
|
+
// a desktop notification, so the async build surfaces itself the moment it's ready.
|
|
878
890
|
async function backgroundBuild() {
|
|
879
891
|
const id = await identity();
|
|
880
892
|
if (!id) return; // not signed in — nothing to build under
|
|
@@ -882,7 +894,26 @@ async function backgroundBuild() {
|
|
|
882
894
|
if (!files.length) return;
|
|
883
895
|
const sp = { text: () => {}, succeed: () => {} }; // silent — no TTY in the background
|
|
884
896
|
try {
|
|
885
|
-
await buildGraph(sp, files, id);
|
|
897
|
+
const built = await buildGraph(sp, files, id);
|
|
898
|
+
if (!built) return;
|
|
899
|
+
let report = null;
|
|
900
|
+
try {
|
|
901
|
+
report = await fetchProfile(id);
|
|
902
|
+
} catch {
|
|
903
|
+
/* profile fetch is best-effort — the graph is still built */
|
|
904
|
+
}
|
|
905
|
+
// The reveal surfaces NATIVELY inside Claude Code — the next prompt's hook (or the next session)
|
|
906
|
+
// opens with "🔬 your graph's ready — here's what I noticed about how you work" from this marker.
|
|
907
|
+
// No OS pop-up: skalpel's whole surface is the conversation + the status line.
|
|
908
|
+
try {
|
|
909
|
+
mkdirSync(join(homedir(), ".skalpel"), { recursive: true });
|
|
910
|
+
writeFileSync(
|
|
911
|
+
join(homedir(), ".skalpel", "insights-ready.json"),
|
|
912
|
+
JSON.stringify({ ready: true, ts: Date.now(), n_sessions: built, report }),
|
|
913
|
+
);
|
|
914
|
+
} catch {
|
|
915
|
+
/* the reveal is a bonus; never block the build on it */
|
|
916
|
+
}
|
|
886
917
|
} catch {
|
|
887
918
|
/* fail-open: a failed background build never surfaces; hooks fail open until it succeeds */
|
|
888
919
|
}
|