skalpel 4.0.58 → 4.0.59

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 CHANGED
@@ -587,32 +587,27 @@ function cleanupLocalData() {
587
587
  return cleaned;
588
588
  }
589
589
 
590
- // Plain uninstall: remove staged hooks + the raw transcript snapshots + all local state/logs, but
591
- // KEEP the saved auth (that's what `--purge` is for). List every file skalpel writes under ~/.skalpel
592
- // so nothing is silently left behind; auth.json (legacy local location) is deliberately preserved.
593
- rm(join(SKALPEL_DIR, "hooks"), "staged hooks");
594
- rm(join(SKALPEL_DIR, "ingest-outbox"), "raw transcript snapshots (ingest-outbox)");
595
- for (const f of [
596
- "stats.json", // legacy "time saved" accumulator nothing writes it now, but old installs left one
597
- "session.json",
598
- "steer.json",
599
- "traj.json",
600
- "pending.json",
601
- "prefs.json",
602
- "insights.ndjson",
603
- "insights-ready.json",
604
- "metrics.ndjson",
605
- "bootstrap.json",
606
- "bootstrap.lock",
607
- "client.json",
608
- "doctor.log",
609
- "hook.log",
610
- "verify-shadow.log",
611
- "verify-last.json",
612
- "verify-shadow.lock",
613
- "verify-reveal.json",
614
- ]) {
615
- rm(join(SKALPEL_DIR, f), f);
590
+ // Plain uninstall: remove EVERYTHING under ~/.skalpel staged hooks, transcript snapshots, all
591
+ // state/logs — but KEEP the saved auth (that's what `--purge` is for). This is a SWEEP, not a
592
+ // hand-maintained filename list: the old list silently drifted from what the runtime actually
593
+ // writes (steers.ndjson, verify-verdict.json, verify-ship-last.json, auth-notice.json,
594
+ // build-stalled.json, first-run.json, verify-reveal-notified.json all survived it), breaking the
595
+ // "nothing silently left behind" promise. Everything under ~/.skalpel is ours; only auth.json
596
+ // (legacy local session location) is deliberately preserved. rm()'s PKG_DIR guard still protects
597
+ // a self-hosted layout from deleting the running installer.
598
+ const LABELS = {
599
+ hooks: "staged hooks",
600
+ "ingest-outbox": "raw transcript snapshots (ingest-outbox)",
601
+ };
602
+ let entries = [];
603
+ try {
604
+ entries = existsSync(SKALPEL_DIR) ? readdirSync(SKALPEL_DIR).sort() : [];
605
+ } catch {
606
+ /* unreadable dir — best-effort; an uninstall never fails on it */
607
+ }
608
+ for (const name of entries) {
609
+ if (name === "auth.json") continue; // kept — `--purge` is the deliberate "forget me entirely"
610
+ rm(join(SKALPEL_DIR, name), LABELS[name] || name);
616
611
  }
617
612
  if (cleaned.length)
618
613
  cleaned.push("(auth kept — run `skalpel uninstall --purge` to remove it too)");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skalpel",
3
- "version": "4.0.58",
3
+ "version": "4.0.59",
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
@@ -16,7 +16,7 @@ import {
16
16
  constants,
17
17
  } from "node:fs";
18
18
  import { homedir } from "node:os";
19
- import { join, dirname, delimiter, sep as pathSep } from "node:path";
19
+ import { basename, join, dirname, delimiter, sep as pathSep } from "node:path";
20
20
  import { createInterface, emitKeypressEvents } from "node:readline";
21
21
  import { fileURLToPath } from "node:url";
22
22
  import { spawn, spawnSync } from "node:child_process";
@@ -591,7 +591,7 @@ export async function scanHistory(days = LOOKBACK_DAYS) {
591
591
  const files = walk(join(homedir(), ".claude", "projects"))
592
592
  .concat(walk(join(homedir(), ".codex", "sessions")))
593
593
  .filter((f) => {
594
- if (f.split("/").pop().startsWith("agent-")) return false; // sub-agent transcripts, not the user
594
+ if (basename(f).startsWith("agent-")) return false; // sub-agent transcripts, not the user
595
595
  try {
596
596
  return statSync(f).mtimeMs >= cutoff;
597
597
  } catch {
@@ -611,6 +611,19 @@ export async function scanHistory(days = LOOKBACK_DAYS) {
611
611
  return out;
612
612
  }
613
613
 
614
+ // Whether the 401 self-heal below may spawn the INTERACTIVE sign-in. login.mjs opens a Google
615
+ // OAuth browser tab and blocks (~180s) on the callback — acceptable only on a real terminal where
616
+ // the user can see why a tab just popped. The detached background build (`__build`, stdio:"ignore")
617
+ // has neither TTY: spawning login from there pops a zero-context browser tab and stalls the build,
618
+ // so it must instead let the 401 fall through to the ingest error, which backgroundBuild's
619
+ // build-stalled.json marker surfaces next session. Pure decision helper, exported for tests.
620
+ export function shouldSelfHealLogin(
621
+ stdoutIsTTY = process.stdout.isTTY,
622
+ stdinIsTTY = process.stdin.isTTY,
623
+ ) {
624
+ return Boolean(stdoutIsTTY && stdinIsTTY);
625
+ }
626
+
614
627
  // HOSTED model: the client is thin — it just UPLOADS the raw transcripts; the SERVER filters
615
628
  // (real-session detection) + judges (Gemini) + builds the Amplitude dash + aggregate DAG into the
616
629
  // hosted store under the user's uid. No Python, no engine, no graph-building on the user's machine.
@@ -669,7 +682,7 @@ async function buildGraph(sp, files, id) {
669
682
  curBytes = 0;
670
683
  }
671
684
  try {
672
- cur[f.split("/").pop()] = readFileSync(f, "utf8");
685
+ cur[basename(f)] = readFileSync(f, "utf8");
673
686
  curBytes += size;
674
687
  } catch {
675
688
  /* skip unreadable */
@@ -695,8 +708,9 @@ async function buildGraph(sp, files, id) {
695
708
  const label = batches.length > 1 ? ` (batch ${bi + 1}/${batches.length})` : "";
696
709
  sp.text(`Building your graph…${label}`);
697
710
  let r = await postBatch(batches[bi], token);
698
- // SELF-HEAL on 401: refresh the Google session once, then retry this batch.
699
- if (r.status === 401 && !process.env.SKALPEL_USER) {
711
+ // SELF-HEAL on 401: refresh the Google session once, then retry this batch. INTERACTIVE ONLY
712
+ // (shouldSelfHealLogin): in the detached/non-TTY case the 401 falls through to the throw below.
713
+ if (r.status === 401 && !process.env.SKALPEL_USER && shouldSelfHealLogin()) {
700
714
  sp.text("Session expired — re-opening sign-in…");
701
715
  spawnSync("node", [join(__dir, "login.mjs")], { stdio: "inherit" });
702
716
  const fresh = await identity();