skalpel 4.0.1 → 4.0.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skalpel",
3
- "version": "4.0.1",
3
+ "version": "4.0.2",
4
4
  "description": "Behavioral graph for AI-assisted coding — learns how you work and steers Claude Code + Codex in real time.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -121,6 +121,38 @@ const API = process.env.SKALPEL_API || CFG.api || "https://graph.skalpel.ai";
121
121
  // load with ~1s margin under the 8s backstop; warm it still returns in ~300ms, so no cost when hot.
122
122
  const DEADLINE_MS = 7000;
123
123
 
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
+
124
156
  async function main() {
125
157
  let payload = {};
126
158
  try {
@@ -169,9 +201,10 @@ async function main() {
169
201
  record("session", outcome, Date.now() - t0);
170
202
  }
171
203
 
172
- // Recap (last session's felt wins) leads, then the standing profile. Either can fire alone a first
173
- // session has no recap; a session with no graph yet has no profile but can still recap.
174
- const additionalContext = [recapNote, context].filter(Boolean).join("\n\n");
204
+ // A just-finished background build leads (the async payoff surface it the instant it's ready),
205
+ // then last session's recap, then the standing profile. Any can fire alone.
206
+ const readyNote = graphReadyNote();
207
+ const additionalContext = [readyNote, recapNote, context].filter(Boolean).join("\n\n");
175
208
 
176
209
  // LOCAL INSIGHT RECORDS (~/.skalpel/insights.ndjson) — plain-English rows for the TUI, one per
177
210
  // note that fired. recordInsight swallows everything, so these never affect the hook output.
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
- (isTTY || process.env.SKALPEL_SETUP_WAIT_FOR_GRAPH === "1") &&
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
- // background/silent path (opt-out or CI): fast, no reveal
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 || "20", 10)),
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}hooks are wired; your graph is warming from the newest ${capped} session${capped === 1 ? "" : "s"} in the background.${X}\n`,
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,12 +894,54 @@ 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
+ try {
906
+ mkdirSync(join(homedir(), ".skalpel"), { recursive: true });
907
+ writeFileSync(
908
+ join(homedir(), ".skalpel", "insights-ready.json"),
909
+ JSON.stringify({ ready: true, ts: Date.now(), n_sessions: built, report }),
910
+ );
911
+ } catch {
912
+ /* the SessionStart reveal is a bonus; never block the build on it */
913
+ }
914
+ notifyDone(built);
886
915
  } catch {
887
916
  /* fail-open: a failed background build never surfaces; hooks fail open until it succeeds */
888
917
  }
889
918
  }
890
919
 
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
+
891
945
  // `__build` = the detached background graph build spawned by setup; skips the banner/hooks entirely.
892
946
  if (isMain && sub === "__build") {
893
947
  backgroundBuild()