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 +8 -3
- package/package.json +1 -1
- package/skalpel-setup.mjs +64 -1
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
|
-
|
|
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
|
|
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
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
|
|
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.
|