skalpel 4.0.6 → 4.0.8

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 CHANGED
@@ -4,13 +4,18 @@ Behavioral graph for AI-assisted coding. Learns how you work from your Claude Co
4
4
  steers the model in real time — off the loops, re-explains, and re-fixes that eat your day.
5
5
 
6
6
  ```bash
7
- npm install -g skalpel
8
- skalpel # sign in with Google → build your graph → see your insights → you're live
7
+ npx skalpel@latest # sign in with Google → build your graph → see your insights → you're live
9
8
  ```
10
9
 
10
+ That's it — no global install. `npx` fetches the latest and runs the one-time setup (which copies
11
+ the hooks into `~/.skalpel` so they persist afterward). It sidesteps every machine-specific install
12
+ failure: no global-bin `PATH` to configure, no `sudo`, and it's always the newest version — so a
13
+ stale copy can never leave you on old code. Prefer a permanent install? `npm install -g skalpel`
14
+ still works and behaves identically.
15
+
11
16
  ## What it does
12
17
 
13
- - **`skalpel`** (or the postinstall nudge) runs the onboarding: red-banner wordmark → **Sign in with
18
+ - **`npx skalpel@latest`** (or `skalpel` after a global install) runs the onboarding: red-banner wordmark → **Sign in with
14
19
  Google** (AWS-hosted, no localhost) → **builds your graph** (async, live progress) → **reveals your
15
20
  insights** (your hours, rework %, signature trap, biggest time-sink) → wires the hooks → you're live.
16
21
  - After that, every prompt in Claude Code / Codex pulls your own behavioral graph via a fast hook, and a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skalpel",
3
- "version": "4.0.6",
3
+ "version": "4.0.8",
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": {
package/skalpel-setup.mjs CHANGED
@@ -424,6 +424,26 @@ async function reportLiveAndLaunch(message) {
424
424
  // arbitrary "cap to 100" that could upload fragments and MISS the real sessions.
425
425
  const MIN_USER_TURNS = Math.max(1, Number.parseInt(process.env.SKALPEL_MIN_USER_TURNS || "5", 10));
426
426
 
427
+ // USER QUERIES ONLY. Claude Code logs every tool RESULT back as a role:"user" message
428
+ // (content = [{type:"tool_result"}]). Counting those as "user turns" is the bug that classified
429
+ // internal tool traffic as real intents and inflated "found N" so the client promised sessions the
430
+ // server then dropped (empty graphs for quiet/agentic users). This mirrors the server's
431
+ // engine._user_turn_count EXACTLY so the client's count == what the server ingests. One definition,
432
+ // two implementations kept in lockstep — see docs/SESSION-COUNTING.md.
433
+ const _HUMAN_PROMPT_SOURCES = new Set(["typed", "queued", "suggestion_accepted"]);
434
+ function isGenuineUserTurn(e) {
435
+ if (!(e?.type === "user" || e?.role === "user")) return false;
436
+ const ps = e?.promptSource;
437
+ if (ps != null) return _HUMAN_PROMPT_SOURCES.has(ps); // newer CLI: trust the source label
438
+ // older/current builds omit promptSource — a row is a tool RESULT (not a query) when its content
439
+ // is a block list carrying a tool_result. Everything else that's role:user is a genuine query.
440
+ const c = e?.message?.content;
441
+ if (Array.isArray(c) && c.some((b) => b && typeof b === "object" && b.type === "tool_result")) {
442
+ return false;
443
+ }
444
+ return true;
445
+ }
446
+
427
447
  export async function userTurnCount(path, cap = 5) {
428
448
  let n = 0;
429
449
  const input = createReadStream(path, { encoding: "utf8" });
@@ -437,7 +457,7 @@ export async function userTurnCount(path, cap = 5) {
437
457
  } catch {
438
458
  continue;
439
459
  }
440
- if (e?.type === "user" || e?.role === "user") {
460
+ if (isGenuineUserTurn(e)) {
441
461
  n++;
442
462
  if (n >= cap) {
443
463
  rl.close();
@@ -727,6 +747,48 @@ async function revealInsights(report) {
727
747
  );
728
748
  }
729
749
 
750
+ // Self-heal preflight. The install failures friends hit were never our code — they were the MACHINE:
751
+ // a ~/.skalpel left root-owned by a past `sudo npm i -g` (so we can't stage hooks into it), or a Node
752
+ // too old for the hook runtime. Catch both HERE with a one-line fix instead of a cryptic EACCES stack
753
+ // trace five calls deep. Runs only on the setup path (login/logout/uninstall return before this).
754
+ function preflight() {
755
+ const major = Number.parseInt(process.versions.node.split(".")[0], 10);
756
+ if (major < 18) {
757
+ console.error(
758
+ ` ${R}✗${X} skalpel needs Node 18+ — you have ${process.versions.node}. Update Node, then re-run.`,
759
+ );
760
+ process.exit(1);
761
+ }
762
+ const dir = join(homedir(), ".skalpel");
763
+ try {
764
+ mkdirSync(dir, { recursive: true });
765
+ accessSync(dir, constants.W_OK);
766
+ return; // writable — good to go
767
+ } catch {
768
+ /* not writable — try to self-heal, then re-check below */
769
+ }
770
+ // If we're root via `sudo` (SUDO_UID set), hand ownership back to the real user so the later
771
+ // non-root hook writes don't EACCES. On Windows there's no uid model — getuid is undefined, skip.
772
+ if (typeof process.getuid === "function" && process.getuid() === 0 && process.env.SUDO_UID) {
773
+ spawnSync(
774
+ "chown",
775
+ ["-R", `${process.env.SUDO_UID}:${process.env.SUDO_GID || process.env.SUDO_UID}`, dir],
776
+ { stdio: "ignore" },
777
+ );
778
+ }
779
+ try {
780
+ accessSync(dir, constants.W_OK);
781
+ } catch {
782
+ const who = process.env.USER || process.env.LOGNAME || "$(whoami)";
783
+ console.error(
784
+ ` ${R}✗${X} ~/.skalpel isn't writable — a past \`sudo\` install left it root-owned.\n` +
785
+ ` One line fixes it, then re-run:\n\n` +
786
+ ` ${B}sudo chown -R ${who} ~/.skalpel${X}\n`,
787
+ );
788
+ process.exit(1);
789
+ }
790
+ }
791
+
730
792
  async function main() {
731
793
  if (sub === "uninstall") {
732
794
  spawnSync("node", [join(__dir, "install.mjs"), "--uninstall"], { stdio: "inherit" });
@@ -766,6 +828,7 @@ async function main() {
766
828
  mkdirSync(cfgDir, { recursive: true });
767
829
  writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
768
830
  }
831
+ preflight(); // fail fast + fixable if the machine can't take the install (root-owned dir / old Node)
769
832
  banner();
770
833
 
771
834
  // 1) SIGN IN with Google (once). SKALPEL_USER (dev) or an existing ~/.skalpel/auth.json skips it.