skalpel 4.0.2 → 4.0.4
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/install.mjs +1 -0
- package/package.json +1 -1
- package/reveal.mjs +61 -0
- package/skalpel-hook-session.mjs +1 -32
- package/skalpel-hook.mjs +8 -3
- package/skalpel-setup.mjs +4 -27
package/install.mjs
CHANGED
|
@@ -40,6 +40,7 @@ function stageHookRuntime() {
|
|
|
40
40
|
// this rule and is guarded by the all-or-nothing check below instead).
|
|
41
41
|
// a hook entrypoint staged without a module it imports — the exact 0.3.12–0.3.16 bug shape.
|
|
42
42
|
copyFileSync(join(PKG_DIR, "insights.mjs"), join(HOOKS_DIR, "insights.mjs")); // hooks import ./insights.mjs
|
|
43
|
+
copyFileSync(join(PKG_DIR, "reveal.mjs"), join(HOOKS_DIR, "reveal.mjs")); // both hooks import ./reveal.mjs
|
|
43
44
|
copyFileSync(join(PKG_DIR, "skalpel-hook.mjs"), HOOK_FILE);
|
|
44
45
|
copyFileSync(join(PKG_DIR, "skalpel-hook-session.mjs"), SESSION_FILE);
|
|
45
46
|
copyFileSync(join(PKG_DIR, "incremental-ingest.mjs"), join(HOOKS_DIR, "incremental-ingest.mjs"));
|
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
|
|
@@ -121,38 +122,6 @@ const API = process.env.SKALPEL_API || CFG.api || "https://graph.skalpel.ai";
|
|
|
121
122
|
// load with ~1s margin under the 8s backstop; warm it still returns in ~300ms, so no cost when hot.
|
|
122
123
|
const DEADLINE_MS = 7000;
|
|
123
124
|
|
|
124
|
-
// One-shot reveal: the detached background build drops ~/.skalpel/insights-ready.json when it
|
|
125
|
-
// finishes. Surface it ONCE at the next session start, then clear it — so the user sees "your graph is
|
|
126
|
-
// ready + here's what I learned" the moment it lands. This is the payoff of the async build.
|
|
127
|
-
function graphReadyNote() {
|
|
128
|
-
const p = join(homedir(), ".skalpel", "insights-ready.json");
|
|
129
|
-
let m;
|
|
130
|
-
try {
|
|
131
|
-
m = JSON.parse(readFileSync(p, "utf8"));
|
|
132
|
-
} catch {
|
|
133
|
-
return null; // nothing pending
|
|
134
|
-
}
|
|
135
|
-
if (!m || !m.ready) return null;
|
|
136
|
-
try {
|
|
137
|
-
writeFileSync(p, JSON.stringify({ ready: false })); // one-shot — clear so it never repeats
|
|
138
|
-
} catch {
|
|
139
|
-
/* if we can't clear it we'd rather drop the reveal than repeat it, but proceed */
|
|
140
|
-
}
|
|
141
|
-
const h = (m.report && m.report.headline) || {};
|
|
142
|
-
const trap = m.report && (m.report.discriminative || [])[0] && m.report.discriminative[0].seq;
|
|
143
|
-
const n = m.n_sessions || h.sessions || 0;
|
|
144
|
-
const bits = [];
|
|
145
|
-
if (h.hours) bits.push(`~${h.hours}h with AI`);
|
|
146
|
-
if (h.rework_pct) bits.push(`~${h.rework_pct}% of it went to rework`);
|
|
147
|
-
if (trap) bits.push(`your signature trap is ${trap}`);
|
|
148
|
-
const learned = bits.length ? ` I learned: ${bits.join(", ")}.` : "";
|
|
149
|
-
return (
|
|
150
|
-
`[skalpel — the user's behavioral graph just FINISHED building in the background ` +
|
|
151
|
-
`(${n} session${n === 1 ? "" : "s"}). Open THIS reply with ONE natural line so they see it landed, ` +
|
|
152
|
-
`in skalpel's voice: "🔬 skalpel · your graph is ready —${learned}" Then continue normally.]`
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
125
|
async function main() {
|
|
157
126
|
let payload = {};
|
|
158
127
|
try {
|
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
|
@@ -902,6 +902,9 @@ async function backgroundBuild() {
|
|
|
902
902
|
} catch {
|
|
903
903
|
/* profile fetch is best-effort — the graph is still built */
|
|
904
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.
|
|
905
908
|
try {
|
|
906
909
|
mkdirSync(join(homedir(), ".skalpel"), { recursive: true });
|
|
907
910
|
writeFileSync(
|
|
@@ -909,39 +912,13 @@ async function backgroundBuild() {
|
|
|
909
912
|
JSON.stringify({ ready: true, ts: Date.now(), n_sessions: built, report }),
|
|
910
913
|
);
|
|
911
914
|
} catch {
|
|
912
|
-
/* the
|
|
915
|
+
/* the reveal is a bonus; never block the build on it */
|
|
913
916
|
}
|
|
914
|
-
notifyDone(built);
|
|
915
917
|
} catch {
|
|
916
918
|
/* fail-open: a failed background build never surfaces; hooks fail open until it succeeds */
|
|
917
919
|
}
|
|
918
920
|
}
|
|
919
921
|
|
|
920
|
-
// Desktop notification when the background build finishes — cross-platform, best-effort, never throws.
|
|
921
|
-
function notifyDone(n) {
|
|
922
|
-
const title = "skalpel";
|
|
923
|
-
const msg = `Your graph is ready — learned from ${n} session${n === 1 ? "" : "s"}. Open Claude Code to see your insights.`;
|
|
924
|
-
try {
|
|
925
|
-
if (process.platform === "darwin") {
|
|
926
|
-
spawnSync(
|
|
927
|
-
"osascript",
|
|
928
|
-
["-e", `display notification ${JSON.stringify(msg)} with title ${JSON.stringify(title)}`],
|
|
929
|
-
{ stdio: "ignore" },
|
|
930
|
-
);
|
|
931
|
-
} else if (process.platform === "linux") {
|
|
932
|
-
spawnSync("notify-send", [title, msg], { stdio: "ignore" });
|
|
933
|
-
} else if (process.platform === "win32") {
|
|
934
|
-
spawnSync(
|
|
935
|
-
"powershell",
|
|
936
|
-
["-NoProfile", "-Command", `New-BurntToastNotification -Text '${title}','${msg}'`],
|
|
937
|
-
{ stdio: "ignore" },
|
|
938
|
-
);
|
|
939
|
-
}
|
|
940
|
-
} catch {
|
|
941
|
-
/* notifications are best-effort */
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
|
|
945
922
|
// `__build` = the detached background graph build spawned by setup; skips the banner/hooks entirely.
|
|
946
923
|
if (isMain && sub === "__build") {
|
|
947
924
|
backgroundBuild()
|