skalpel 3.4.8 → 3.4.9

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": "3.4.8",
3
+ "version": "3.4.9",
4
4
  "description": "Skalpel — local proxy and TUI for coding agents (skalpel + skalpeld bundle).",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://skalpel.ai",
@@ -59,10 +59,10 @@
59
59
  "x64"
60
60
  ],
61
61
  "optionalDependencies": {
62
- "@skalpelai/skalpel-darwin-arm64": "3.4.8",
63
- "@skalpelai/skalpel-darwin-x64": "3.4.8",
64
- "@skalpelai/skalpel-linux-arm64": "3.4.8",
65
- "@skalpelai/skalpel-linux-x64": "3.4.8",
66
- "@skalpelai/skalpel-win32-x64": "3.4.8"
62
+ "@skalpelai/skalpel-darwin-arm64": "3.4.9",
63
+ "@skalpelai/skalpel-darwin-x64": "3.4.9",
64
+ "@skalpelai/skalpel-linux-arm64": "3.4.9",
65
+ "@skalpelai/skalpel-linux-x64": "3.4.9",
66
+ "@skalpelai/skalpel-win32-x64": "3.4.9"
67
67
  }
68
68
  }
@@ -147,8 +147,10 @@ async function userTurnCount(path, cap = MIN_USER_TURNS) {
147
147
  return count;
148
148
  }
149
149
 
150
- async function scanHistory() {
151
- const cutoff = Date.now() - LOOKBACK_DAYS * 864e5;
150
+ async function scanHistory(sinceMs = 0) {
151
+ // Full runs use the LOOKBACK window; an incremental catch-up passes the last
152
+ // ingest time so only genuinely-new sessions (mtime past both bounds) are picked.
153
+ const cutoff = Math.max(Date.now() - LOOKBACK_DAYS * 864e5, sinceMs);
152
154
  const candidates = [
153
155
  ...walk(join(homedir(), ".claude", "projects")),
154
156
  ...walk(join(homedir(), ".codex", "sessions")),
@@ -246,7 +248,7 @@ async function pollJob(jobId, uid, token) {
246
248
  if (status.state === "judging" && progress !== lastProgress) {
247
249
  lastProgress = progress;
248
250
  writeState({ state: "judging", user_id: uid, job_id: jobId, ...status });
249
- emit("judging", `Judging coding sessions ${progress}`, {
251
+ emit("judging", `Judging your sessions ${progress}`, {
250
252
  done: status.done ?? 0,
251
253
  total: status.total ?? 0,
252
254
  });
@@ -291,42 +293,10 @@ function recordProfile(profile, sessions) {
291
293
  }
292
294
  }
293
295
 
294
- async function main() {
295
- const auth = await identity();
296
- if (!auth.uid || !auth.token) throw new Error("sign in with `skalpel login` before learning history");
297
-
298
- const prior = readState();
299
- if (!FORCE && prior.state === "done" && prior.user_id === auth.uid) {
300
- emit("skipped", "Coding history already learned", { terminal: true });
301
- return;
302
- }
303
-
304
- emit("wiring", "Activating Claude Code and Codex hooks");
305
- const wired = spawnSync(process.execPath, [join(HERE, "install.mjs")], { stdio: "ignore" });
306
- if (wired.error || wired.status !== 0) throw new Error("could not activate coding-agent hooks");
307
-
308
- if (prior.user_id === auth.uid && prior.job_id && prior.state !== "done") {
309
- emit("resuming", "Resuming the existing graph build");
310
- const sessions = await pollJob(prior.job_id, auth.uid, auth.token);
311
- const profile = await fetchProfile(auth.uid, auth.token);
312
- recordProfile(profile, sessions);
313
- writeState({ state: "done", user_id: auth.uid, sessions });
314
- emit("complete", `Learned from ${sessions} coding sessions`, { terminal: true, sessions });
315
- return;
316
- }
317
-
318
- writeState({ state: "scanning", user_id: auth.uid });
319
- emit("scanning", `Scanning the last ${LOOKBACK_DAYS} days of Claude Code and Codex`);
320
- const files = await scanHistory();
321
- if (!files.length) {
322
- writeState({ state: "done", user_id: auth.uid, sessions: 0 });
323
- emit("complete", "Hooks are active; your graph will grow as you work", {
324
- terminal: true,
325
- sessions: 0,
326
- });
327
- return;
328
- }
329
-
296
+ // Upload judge → build → complete, shared by the fresh scan and the incremental
297
+ // catch-up. Emits the "building" beat (npx phrasing) between judging and complete, and
298
+ // stamps `last_ingest_at` (the run's start) so the next launch only re-scans newer work.
299
+ async function ingestAndFinish(files, auth, runStartMs) {
330
300
  const payload = buildPayload(files, auth.uid);
331
301
  writeState({ state: "uploading", user_id: auth.uid, sessions: payload.sessions });
332
302
  emit("uploading", `Uploading ${payload.sessions} coding sessions`);
@@ -350,7 +320,7 @@ async function main() {
350
320
  job_id: accepted.job_id,
351
321
  sessions: payload.sessions,
352
322
  });
353
- emit("judging", `Judging ${payload.sessions} coding sessions`, {
323
+ emit("judging", `Judging your sessions… 0/${payload.sessions}`, {
354
324
  done: 0,
355
325
  total: payload.sessions,
356
326
  });
@@ -359,10 +329,84 @@ async function main() {
359
329
  throw new Error("ingest response did not include a graph job");
360
330
  }
361
331
 
332
+ emit("building", `Building your graph from ${sessions} session${sessions === 1 ? "" : "s"}…`, {
333
+ sessions,
334
+ });
362
335
  const profile = await fetchProfile(auth.uid, auth.token);
363
336
  recordProfile(profile, sessions);
364
- writeState({ state: "done", user_id: auth.uid, sessions });
365
- emit("complete", `Learned from ${sessions} coding sessions`, { terminal: true, sessions });
337
+ writeState({
338
+ state: "done",
339
+ user_id: auth.uid,
340
+ sessions,
341
+ last_ingest_at: new Date(runStartMs).toISOString(),
342
+ });
343
+ emit("complete", `Learned from ${sessions} session${sessions === 1 ? "" : "s"}`, { terminal: true, sessions });
344
+ return sessions;
345
+ }
346
+
347
+ async function main() {
348
+ const auth = await identity();
349
+ if (!auth.uid || !auth.token) throw new Error("sign in with `skalpel login` before learning history");
350
+
351
+ const prior = readState();
352
+ const runStartMs = Date.now();
353
+
354
+ // Incremental catch-up: an already-built graph re-scans only sessions newer than the
355
+ // last ingest, so return visits stay fast. Nothing new → a quiet freshness signal (no bar).
356
+ if (!FORCE && prior.state === "done" && prior.user_id === auth.uid) {
357
+ const sinceMs = Date.parse(prior.last_ingest_at || prior.updated_at || "") || 0;
358
+ const fresh = await scanHistory(sinceMs);
359
+ if (!fresh.length) {
360
+ emit("skipped", `Graph up to date · ${prior.sessions ?? 0} sessions`, {
361
+ terminal: true,
362
+ sessions: prior.sessions ?? 0,
363
+ });
364
+ return;
365
+ }
366
+ await ingestAndFinish(fresh, auth, runStartMs);
367
+ return;
368
+ }
369
+
370
+ emit("wiring", "Wiring hooks into Claude Code + Codex");
371
+ const wired = spawnSync(process.execPath, [join(HERE, "install.mjs")], { stdio: "ignore" });
372
+ if (wired.error || wired.status !== 0) throw new Error("could not activate coding-agent hooks");
373
+
374
+ if (prior.user_id === auth.uid && prior.job_id && prior.state !== "done") {
375
+ emit("resuming", "Resuming the existing graph build");
376
+ const sessions = await pollJob(prior.job_id, auth.uid, auth.token);
377
+ emit("building", `Building your graph from ${sessions} session${sessions === 1 ? "" : "s"}…`, {
378
+ sessions,
379
+ });
380
+ const profile = await fetchProfile(auth.uid, auth.token);
381
+ recordProfile(profile, sessions);
382
+ writeState({
383
+ state: "done",
384
+ user_id: auth.uid,
385
+ sessions,
386
+ last_ingest_at: new Date(runStartMs).toISOString(),
387
+ });
388
+ emit("complete", `Learned from ${sessions} session${sessions === 1 ? "" : "s"}`, { terminal: true, sessions });
389
+ return;
390
+ }
391
+
392
+ writeState({ state: "scanning", user_id: auth.uid });
393
+ emit("scanning", `Reading your last ${LOOKBACK_DAYS} days of Claude Code + Codex`);
394
+ const files = await scanHistory();
395
+ if (!files.length) {
396
+ writeState({
397
+ state: "done",
398
+ user_id: auth.uid,
399
+ sessions: 0,
400
+ last_ingest_at: new Date(runStartMs).toISOString(),
401
+ });
402
+ emit("complete", "Hooks are active; your graph will grow as you work", {
403
+ terminal: true,
404
+ sessions: 0,
405
+ });
406
+ return;
407
+ }
408
+
409
+ await ingestAndFinish(files, auth, runStartMs);
366
410
  }
367
411
 
368
412
  if (!acquireLock()) {