sphica 0.6.31 → 0.6.33

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "sphica",
4
- "version": "0.6.31",
4
+ "version": "0.6.33",
5
5
  "description": "Records Claude Code and Codex sessions on your machine and keeps past implementation and decisions, with their sources, for your agent to find.",
6
6
  "author": {
7
7
  "name": "iroha924",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sphica",
3
- "version": "0.6.31",
3
+ "version": "0.6.33",
4
4
  "description": "Records Claude Code and Codex sessions on your machine and keeps past implementation and decisions, with their sources, for your agent to find.",
5
5
  "author": {
6
6
  "name": "iroha924",
package/dist/cli.js CHANGED
@@ -92,7 +92,8 @@ var require_src = __commonJS(function(exports, module) {
92
92
  });
93
93
 
94
94
  // server/src/cli.ts
95
- import fs9 from "node:fs";
95
+ import fs10 from "node:fs";
96
+ import os4 from "node:os";
96
97
  import path10 from "node:path";
97
98
 
98
99
  // server/node_modules/@clack/core/dist/index.mjs
@@ -2966,7 +2967,7 @@ async function run(app, inputs, context) {
2966
2967
  }
2967
2968
 
2968
2969
  // server/src/admin.ts
2969
- import fs8 from "node:fs";
2970
+ import fs9 from "node:fs";
2970
2971
  import path9 from "node:path";
2971
2972
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
2972
2973
 
@@ -5013,7 +5014,7 @@ function backups(file = dbFile()) {
5013
5014
 
5014
5015
  // server/src/capture.ts
5015
5016
  import { spawn } from "node:child_process";
5016
- import fs7 from "node:fs";
5017
+ import fs8 from "node:fs";
5017
5018
  import path8 from "node:path";
5018
5019
 
5019
5020
  // server/node_modules/kysely/dist/util/object-utils.js
@@ -16118,7 +16119,7 @@ var sessionId = (projectId, host, externalId) => uuidFrom(String(projectId), hos
16118
16119
 
16119
16120
  // server/src/project.ts
16120
16121
  import { execFileSync as execFileSync2 } from "node:child_process";
16121
- import fs5 from "node:fs";
16122
+ import fs6 from "node:fs";
16122
16123
  import os3 from "node:os";
16123
16124
  import path6 from "node:path";
16124
16125
 
@@ -16127,6 +16128,127 @@ import path5 from "node:path";
16127
16128
  var MAX_BYTES = 2 * 1024 * 1024;
16128
16129
  var leaves = (rel) => rel === ".." || rel.startsWith(`..${path5.sep}`) || path5.isAbsolute(rel);
16129
16130
 
16131
+ // server/src/file-lock.ts
16132
+ import fs5 from "node:fs";
16133
+ var WAIT_MS = 5000;
16134
+ var POLL_MS = 40;
16135
+ var TRIES = 5;
16136
+ var sleep = (ms) => {
16137
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
16138
+ };
16139
+ function retryBusy(op) {
16140
+ for (let attempt = 1;; attempt++) {
16141
+ try {
16142
+ op();
16143
+ return;
16144
+ } catch (e) {
16145
+ const code = e.code;
16146
+ if (attempt >= TRIES || !(code === "EPERM" || code === "EACCES" || code === "EBUSY"))
16147
+ throw e;
16148
+ sleep(50 * attempt);
16149
+ }
16150
+ }
16151
+ }
16152
+ function heldBy(lock) {
16153
+ let holder = 0;
16154
+ try {
16155
+ holder = Number(fs5.readFileSync(lock, "utf8").trim()) || 0;
16156
+ } catch {}
16157
+ if (holder <= 0)
16158
+ return `${lock} is held by another process. Run this again when it finishes.`;
16159
+ try {
16160
+ process.kill(holder, 0);
16161
+ } catch (e) {
16162
+ if (e.code === "ESRCH")
16163
+ return `${lock} is held by process ${holder}, which is not running. If no sphica init is running, delete ${lock} and run this again.`;
16164
+ }
16165
+ return `${lock} is held by process ${holder}. Run this again when it finishes.`;
16166
+ }
16167
+ function withFileLock(lock, fn, waitMs = WAIT_MS) {
16168
+ const mine = String(process.pid);
16169
+ const until = performance.now() + waitMs;
16170
+ for (;; ) {
16171
+ let fd = null;
16172
+ let refused = null;
16173
+ try {
16174
+ fd = fs5.openSync(lock, "wx");
16175
+ } catch (e) {
16176
+ const code = e.code;
16177
+ if (code === "EPERM" || code === "EACCES" || code === "EBUSY")
16178
+ refused = e;
16179
+ else if (code !== "EEXIST")
16180
+ throw e;
16181
+ }
16182
+ if (fd !== null) {
16183
+ try {
16184
+ try {
16185
+ fs5.writeFileSync(fd, mine);
16186
+ } finally {
16187
+ fs5.closeSync(fd);
16188
+ }
16189
+ } catch (e) {
16190
+ try {
16191
+ retryBusy(() => fs5.rmSync(lock, { force: true }));
16192
+ } catch {}
16193
+ throw e;
16194
+ }
16195
+ break;
16196
+ }
16197
+ if (performance.now() >= until)
16198
+ throw refused ?? new Error(heldBy(lock));
16199
+ sleep(POLL_MS);
16200
+ }
16201
+ let result;
16202
+ try {
16203
+ result = fn();
16204
+ } catch (e) {
16205
+ try {
16206
+ release(lock, mine);
16207
+ } catch {}
16208
+ throw e;
16209
+ }
16210
+ release(lock, mine);
16211
+ return result;
16212
+ }
16213
+ function release(lock, mine) {
16214
+ const cannot = (e) => new Error(`The work finished, but Sphica could not remove ${lock} (${e.message}). Delete it before running this again.`);
16215
+ let held = "";
16216
+ try {
16217
+ retryBusy(() => {
16218
+ held = fs5.readFileSync(lock, "utf8");
16219
+ });
16220
+ } catch (e) {
16221
+ if (e.code === "ENOENT")
16222
+ return;
16223
+ throw cannot(e);
16224
+ }
16225
+ if (held !== mine)
16226
+ return;
16227
+ try {
16228
+ retryBusy(() => fs5.rmSync(lock, { force: true }));
16229
+ } catch (e) {
16230
+ throw cannot(e);
16231
+ }
16232
+ }
16233
+ function replaceFile(file, text) {
16234
+ const tmp = `${file}.${process.pid}.tmp`;
16235
+ try {
16236
+ const fd = fs5.openSync(tmp, "w");
16237
+ try {
16238
+ fs5.writeFileSync(fd, text);
16239
+ fs5.fsyncSync(fd);
16240
+ } finally {
16241
+ fs5.closeSync(fd);
16242
+ }
16243
+ retryBusy(() => fs5.renameSync(tmp, file));
16244
+ } catch (e) {
16245
+ try {
16246
+ retryBusy(() => fs5.rmSync(tmp, { force: true }));
16247
+ } catch {}
16248
+ throw e;
16249
+ }
16250
+ }
16251
+
16130
16252
  // server/src/project.ts
16131
16253
  var localFile = () => path6.join(sphicaHome(), "projects.json");
16132
16254
  var LOCAL_KEY = /^[a-z0-9][a-z0-9._-]*$/;
@@ -16170,7 +16292,7 @@ var git = (dir, ...args) => {
16170
16292
  function localMap() {
16171
16293
  let raw;
16172
16294
  try {
16173
- raw = fs5.readFileSync(localFile(), "utf8");
16295
+ raw = fs6.readFileSync(localFile(), "utf8");
16174
16296
  } catch (e) {
16175
16297
  if (e.code === "ENOENT")
16176
16298
  return {};
@@ -16217,14 +16339,27 @@ function nameLocal(dir, name) {
16217
16339
  if (place?.key.startsWith("git:"))
16218
16340
  throw new Error(`${place.root} has a git remote, so its key is ${place.key}. Add it without --name.`);
16219
16341
  const root = rootOf(dir);
16220
- const m = localMap();
16221
- m[root] = name;
16222
- fs5.mkdirSync(path6.dirname(localFile()), { recursive: true, mode: 448 });
16223
- fs5.writeFileSync(localFile(), `${JSON.stringify(m, null, 2)}
16342
+ fs6.mkdirSync(path6.dirname(localFile()), { recursive: true, mode: 448 });
16343
+ withFileLock(`${localFile()}.lock`, () => {
16344
+ const m = localMap();
16345
+ m[root] = name;
16346
+ replaceFile(localFile(), `${JSON.stringify(m, null, 2)}
16224
16347
  `);
16348
+ });
16225
16349
  return { key: `local:${name}`, root, name };
16226
16350
  }
16227
- function localRoots(roots = [path6.join(os3.homedir(), "Projects")]) {
16351
+ function underHome(p, homes, api = path6) {
16352
+ for (const h2 of homes) {
16353
+ const rel = api.relative(h2, p);
16354
+ if (rel === "")
16355
+ return "~";
16356
+ if (rel !== ".." && !rel.startsWith(`..${api.sep}`) && !api.isAbsolute(rel))
16357
+ return `~${api.sep}${rel}`;
16358
+ }
16359
+ return p;
16360
+ }
16361
+ var projectsDir = () => path6.join(os3.homedir(), "Projects");
16362
+ function localRoots(roots = [projectsDir()]) {
16228
16363
  const seen = new Map;
16229
16364
  const add = (p) => {
16230
16365
  if (p)
@@ -16233,7 +16368,7 @@ function localRoots(roots = [path6.join(os3.homedir(), "Projects")]) {
16233
16368
  for (const r2 of roots) {
16234
16369
  let entries = [];
16235
16370
  try {
16236
- entries = fs5.readdirSync(r2, { withFileTypes: true });
16371
+ entries = fs6.readdirSync(r2, { withFileTypes: true });
16237
16372
  } catch {
16238
16373
  continue;
16239
16374
  }
@@ -16242,7 +16377,7 @@ function localRoots(roots = [path6.join(os3.homedir(), "Projects")]) {
16242
16377
  add(identify(path6.join(r2, e.name)));
16243
16378
  }
16244
16379
  for (const [root, name] of Object.entries(localMap())) {
16245
- if (LOCAL_KEY.test(name) && fs5.existsSync(root))
16380
+ if (LOCAL_KEY.test(name) && fs6.existsSync(root))
16246
16381
  add({ key: `local:${name}`, root, name });
16247
16382
  }
16248
16383
  const found = new Map;
@@ -16275,7 +16410,7 @@ function patchPaths(patch) {
16275
16410
 
16276
16411
  // server/src/worktree.ts
16277
16412
  import { execFileSync as execFileSync3 } from "node:child_process";
16278
- import fs6 from "node:fs";
16413
+ import fs7 from "node:fs";
16279
16414
  import path7 from "node:path";
16280
16415
  var MAX_PATHS = 200;
16281
16416
  var git2 = (root, args) => {
@@ -16310,7 +16445,7 @@ function snapshot(root) {
16310
16445
  continue;
16311
16446
  let st;
16312
16447
  try {
16313
- st = fs6.lstatSync(path7.join(root, p), { throwIfNoEntry: false });
16448
+ st = fs7.lstatSync(path7.join(root, p), { throwIfNoEntry: false });
16314
16449
  } catch {
16315
16450
  st = undefined;
16316
16451
  }
@@ -16388,7 +16523,7 @@ ${z}`,
16388
16523
  function prune(held) {
16389
16524
  let files;
16390
16525
  try {
16391
- files = fs7.readdirSync(held).filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort();
16526
+ files = fs8.readdirSync(held).filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort();
16392
16527
  } catch {
16393
16528
  return;
16394
16529
  }
@@ -16396,21 +16531,21 @@ function prune(held) {
16396
16531
  const stale = files.filter((f) => Number(f.split("-")[0]) < cutoff);
16397
16532
  const over = files.slice(0, Math.max(0, files.length - HOLD_MAX));
16398
16533
  for (const f of new Set([...stale, ...over]))
16399
- fs7.rmSync(path8.join(held, f), { force: true });
16534
+ fs8.rmSync(path8.join(held, f), { force: true });
16400
16535
  }
16401
16536
  function spool(record) {
16402
16537
  const dir = spoolDir();
16403
- fs7.mkdirSync(dir, { recursive: true, mode: 448 });
16538
+ fs8.mkdirSync(dir, { recursive: true, mode: 448 });
16404
16539
  const name = `${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 10)}.json`;
16405
16540
  const tmp = path8.join(dir, `.${name}`);
16406
- fs7.writeFileSync(tmp, JSON.stringify(record), { mode: 384 });
16407
- fs7.renameSync(tmp, path8.join(dir, name));
16541
+ fs8.writeFileSync(tmp, JSON.stringify(record), { mode: 384 });
16542
+ fs8.renameSync(tmp, path8.join(dir, name));
16408
16543
  }
16409
16544
  var branchOf = (root) => {
16410
16545
  try {
16411
16546
  const dotgit = path8.join(root, ".git");
16412
- const gitdir = fs7.statSync(dotgit).isFile() ? path8.resolve(root, fs7.readFileSync(dotgit, "utf8").match(/^gitdir: (.+)$/m)?.[1]?.trim() ?? "") : dotgit;
16413
- const h2 = fs7.readFileSync(path8.join(gitdir, "HEAD"), "utf8").trim();
16547
+ const gitdir = fs8.statSync(dotgit).isFile() ? path8.resolve(root, fs8.readFileSync(dotgit, "utf8").match(/^gitdir: (.+)$/m)?.[1]?.trim() ?? "") : dotgit;
16548
+ const h2 = fs8.readFileSync(path8.join(gitdir, "HEAD"), "utf8").trim();
16414
16549
  return h2.startsWith("ref: refs/heads/") ? h2.slice("ref: refs/heads/".length) : null;
16415
16550
  } catch {
16416
16551
  return null;
@@ -16456,7 +16591,7 @@ Notes: ${notes.trim()}` : "";
16456
16591
  };
16457
16592
  }
16458
16593
  function captureNotice(file = dbFile()) {
16459
- if (!fs7.existsSync(file))
16594
+ if (!fs8.existsSync(file))
16460
16595
  return panel("sphica: no database, so conversations are not recorded", [file], "Create it with sphica init");
16461
16596
  const s = readState();
16462
16597
  if (s.stuck)
@@ -16473,7 +16608,7 @@ var isStart = (v) => {
16473
16608
  };
16474
16609
  function readStart(file) {
16475
16610
  try {
16476
- const s = JSON.parse(fs7.readFileSync(file, "utf8"));
16611
+ const s = JSON.parse(fs8.readFileSync(file, "utf8"));
16477
16612
  return isStart(s) ? s : null;
16478
16613
  } catch {
16479
16614
  return null;
@@ -16482,7 +16617,7 @@ function readStart(file) {
16482
16617
  function readStarts(dir) {
16483
16618
  let names;
16484
16619
  try {
16485
- names = fs7.readdirSync(dir);
16620
+ names = fs8.readdirSync(dir);
16486
16621
  } catch (e) {
16487
16622
  return e.code === "ENOENT" ? [] : null;
16488
16623
  }
@@ -16505,10 +16640,10 @@ function newestTurn(starts) {
16505
16640
  return at.length === 1 ? at[0]?.turn ?? null : null;
16506
16641
  }
16507
16642
  function writeStart(dir, s, file = startFile(dir, s.turn)) {
16508
- fs7.mkdirSync(dir, { recursive: true, mode: 448 });
16643
+ fs8.mkdirSync(dir, { recursive: true, mode: 448 });
16509
16644
  const tmp = path8.join(dir, `.${path8.basename(file)}.${process.pid}.tmp`);
16510
- fs7.writeFileSync(tmp, JSON.stringify(s), { mode: 384 });
16511
- fs7.renameSync(tmp, file);
16645
+ fs8.writeFileSync(tmp, JSON.stringify(s), { mode: 384 });
16646
+ fs8.renameSync(tmp, file);
16512
16647
  }
16513
16648
  function openTurn(dir, turn, root) {
16514
16649
  if (readStart(startFile(dir, turn))?.running)
@@ -16538,22 +16673,22 @@ function stopTurn(dir, turn) {
16538
16673
  }
16539
16674
  function pruneBaselines() {
16540
16675
  const cutoff = Date.now() - HOLD_DAYS * 24 * 60 * 60 * 1000;
16541
- const fresh = (file) => (fs7.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? Date.now()) >= cutoff;
16676
+ const fresh = (file) => (fs8.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? Date.now()) >= cutoff;
16542
16677
  let names;
16543
16678
  try {
16544
- names = fs7.readdirSync(baselineDir());
16679
+ names = fs8.readdirSync(baselineDir());
16545
16680
  } catch {
16546
16681
  return;
16547
16682
  }
16548
16683
  for (const name of names) {
16549
16684
  const at = path8.join(baselineDir(), name);
16550
- if (!fs7.statSync(at, { throwIfNoEntry: false })?.isDirectory()) {
16685
+ if (!fs8.statSync(at, { throwIfNoEntry: false })?.isDirectory()) {
16551
16686
  if (!fresh(at))
16552
- fs7.rmSync(at, { force: true });
16687
+ fs8.rmSync(at, { force: true });
16553
16688
  } else {
16554
- const files = fs7.readdirSync(at);
16689
+ const files = fs8.readdirSync(at);
16555
16690
  if (files.length && !files.some((f) => fresh(path8.join(at, f))))
16556
- fs7.rmSync(at, { recursive: true, force: true });
16691
+ fs8.rmSync(at, { recursive: true, force: true });
16557
16692
  }
16558
16693
  }
16559
16694
  }
@@ -16572,7 +16707,7 @@ function onHook(host, input) {
16572
16707
  return { flush: false };
16573
16708
  const file = process.env.CLAUDE_ENV_FILE;
16574
16709
  if (file && input.session_id && /^[A-Za-z0-9_-]+$/.test(input.session_id)) {
16575
- fs7.appendFileSync(file, `export SPHICA_PARENT_SESSION=${input.session_id}
16710
+ fs8.appendFileSync(file, `export SPHICA_PARENT_SESSION=${input.session_id}
16576
16711
  `);
16577
16712
  }
16578
16713
  attempt(pruneBaselines);
@@ -16654,13 +16789,13 @@ function onHook(host, input) {
16654
16789
  }
16655
16790
  function writeState(s) {
16656
16791
  try {
16657
- fs7.writeFileSync(stateFile(), JSON.stringify(s));
16792
+ fs8.writeFileSync(stateFile(), JSON.stringify(s));
16658
16793
  } catch {}
16659
16794
  }
16660
16795
  function readState() {
16661
16796
  const count = (dir) => {
16662
16797
  try {
16663
- return fs7.readdirSync(dir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).length;
16798
+ return fs8.readdirSync(dir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).length;
16664
16799
  } catch {
16665
16800
  return 0;
16666
16801
  }
@@ -16672,7 +16807,7 @@ function readState() {
16672
16807
  };
16673
16808
  let raw = {};
16674
16809
  try {
16675
- const parsed = JSON.parse(fs7.readFileSync(stateFile(), "utf8"));
16810
+ const parsed = JSON.parse(fs8.readFileSync(stateFile(), "utf8"));
16676
16811
  if (parsed && typeof parsed === "object")
16677
16812
  raw = parsed;
16678
16813
  } catch {}
@@ -16687,24 +16822,24 @@ function readState() {
16687
16822
  }
16688
16823
  function lock() {
16689
16824
  const file = path8.join(spoolDir(), ".lock");
16690
- fs7.mkdirSync(spoolDir(), { recursive: true, mode: 448 });
16825
+ fs8.mkdirSync(spoolDir(), { recursive: true, mode: 448 });
16691
16826
  for (let attempt2 = 0;attempt2 < 2; attempt2++) {
16692
16827
  try {
16693
- fs7.writeFileSync(file, String(process.pid), { flag: "wx", mode: 384 });
16828
+ fs8.writeFileSync(file, String(process.pid), { flag: "wx", mode: 384 });
16694
16829
  return () => {
16695
16830
  try {
16696
- if (fs7.readFileSync(file, "utf8") === String(process.pid))
16697
- fs7.rmSync(file, { force: true });
16831
+ if (fs8.readFileSync(file, "utf8") === String(process.pid))
16832
+ fs8.rmSync(file, { force: true });
16698
16833
  } catch {}
16699
16834
  };
16700
16835
  } catch (e) {
16701
16836
  if (e.code !== "EEXIST")
16702
16837
  throw e;
16703
16838
  }
16704
- const st = fs7.statSync(file, { throwIfNoEntry: false });
16839
+ const st = fs8.statSync(file, { throwIfNoEntry: false });
16705
16840
  if (!st)
16706
16841
  continue;
16707
- const holder = Number(fs7.readFileSync(file, "utf8") || 0);
16842
+ const holder = Number(fs8.readFileSync(file, "utf8") || 0);
16708
16843
  const fresh = Date.now() - st.mtimeMs < 5 * 60000;
16709
16844
  const alive = (() => {
16710
16845
  if (holder <= 0)
@@ -16717,7 +16852,7 @@ function lock() {
16717
16852
  })();
16718
16853
  if (alive && fresh)
16719
16854
  return null;
16720
- fs7.rmSync(file, { force: true });
16855
+ fs8.rmSync(file, { force: true });
16721
16856
  }
16722
16857
  return null;
16723
16858
  }
@@ -16816,7 +16951,7 @@ var REJECTED = new Set([18, 19, 20, 25]);
16816
16951
  var rejected = (e) => REJECTED.has(sqliteCode(e) ?? -1);
16817
16952
  var queued = (from) => {
16818
16953
  try {
16819
- return fs7.readdirSync(from).filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort().map((name) => ({ name, from }));
16954
+ return fs8.readdirSync(from).filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort().map((name) => ({ name, from }));
16820
16955
  } catch {
16821
16956
  return [];
16822
16957
  }
@@ -16827,16 +16962,16 @@ async function sendBatch(db, names) {
16827
16962
  for (const { name, from } of names) {
16828
16963
  let r2;
16829
16964
  try {
16830
- r2 = current(JSON.parse(fs7.readFileSync(path8.join(from, name), "utf8")));
16965
+ r2 = current(JSON.parse(fs8.readFileSync(path8.join(from, name), "utf8")));
16831
16966
  } catch {
16832
- fs7.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
16833
- fs7.renameSync(path8.join(from, name), path8.join(rejectedDir(), name));
16967
+ fs8.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
16968
+ fs8.renameSync(path8.join(from, name), path8.join(rejectedDir(), name));
16834
16969
  continue;
16835
16970
  }
16836
16971
  if (r2)
16837
16972
  records.push({ name, from, r: r2 });
16838
16973
  else
16839
- fs7.rmSync(path8.join(from, name), { force: true });
16974
+ fs8.rmSync(path8.join(from, name), { force: true });
16840
16975
  }
16841
16976
  let sent = 0;
16842
16977
  let strayed = [];
@@ -16862,10 +16997,10 @@ async function sendBatch(db, names) {
16862
16997
  }
16863
16998
  }
16864
16999
  if (bad.length) {
16865
- fs7.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
17000
+ fs8.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
16866
17001
  for (const x of bad) {
16867
17002
  try {
16868
- fs7.renameSync(path8.join(x.from, x.name), path8.join(rejectedDir(), x.name));
17003
+ fs8.renameSync(path8.join(x.from, x.name), path8.join(rejectedDir(), x.name));
16869
17004
  } catch (e) {
16870
17005
  if (e.code !== "ENOENT")
16871
17006
  throw e;
@@ -16873,12 +17008,12 @@ async function sendBatch(db, names) {
16873
17008
  }
16874
17009
  }
16875
17010
  if (strayed.length) {
16876
- fs7.mkdirSync(held, { recursive: true, mode: 448 });
17011
+ fs8.mkdirSync(held, { recursive: true, mode: 448 });
16877
17012
  for (const x of strayed) {
16878
17013
  if (x.from === held)
16879
17014
  continue;
16880
17015
  try {
16881
- fs7.renameSync(path8.join(x.from, x.name), path8.join(held, x.name));
17016
+ fs8.renameSync(path8.join(x.from, x.name), path8.join(held, x.name));
16882
17017
  } catch (e) {
16883
17018
  if (e.code !== "ENOENT")
16884
17019
  throw e;
@@ -16888,7 +17023,7 @@ async function sendBatch(db, names) {
16888
17023
  const moved = new Set([...bad, ...strayed].map((x) => x.name));
16889
17024
  for (const x of records)
16890
17025
  if (!moved.has(x.name))
16891
- fs7.rmSync(path8.join(x.from, x.name), { force: true });
17026
+ fs8.rmSync(path8.join(x.from, x.name), { force: true });
16892
17027
  return { sent, rejected: bad.length };
16893
17028
  }
16894
17029
  async function flush(file = dbFile(), budgetMs = FLUSH_BUDGET_MS) {
@@ -17348,7 +17483,7 @@ var STOP2 = "sphica_migration_stop";
17348
17483
  var NOTE = "sphica_migration_note";
17349
17484
  var KEEP2 = 3;
17350
17485
  function backUp(raw, file, from) {
17351
- fs8.mkdirSync(backupDir(file), { recursive: true, mode: 448 });
17486
+ fs9.mkdirSync(backupDir(file), { recursive: true, mode: 448 });
17352
17487
  const done = backupPath(file, from);
17353
17488
  const partial = `${done}.partial`;
17354
17489
  try {
@@ -17364,10 +17499,10 @@ function backUp(raw, file, from) {
17364
17499
  } finally {
17365
17500
  look.close();
17366
17501
  }
17367
- fs8.renameSync(partial, done);
17502
+ fs9.renameSync(partial, done);
17368
17503
  } catch (e) {
17369
17504
  for (const f of [partial, `${partial}-wal`, `${partial}-shm`])
17370
- fs8.rmSync(f, { force: true });
17505
+ fs9.rmSync(f, { force: true });
17371
17506
  throw new Error(`Could not back up ${file} before migrating it, so nothing was migrated (${e.message}). Check free space in ${backupDir(file)}.`);
17372
17507
  }
17373
17508
  return done;
@@ -17381,7 +17516,7 @@ function prune2(file, kept) {
17381
17516
  }
17382
17517
  for (const old of all.filter((b) => b !== kept).slice(KEEP2 - 1))
17383
17518
  try {
17384
- fs8.rmSync(old, { force: true });
17519
+ fs9.rmSync(old, { force: true });
17385
17520
  } catch {}
17386
17521
  }
17387
17522
  var versionOf = (raw) => raw.prepare("pragma user_version").get().user_version;
@@ -17473,20 +17608,20 @@ function migrate(file = dbFile(), dir = MIGRATIONS()) {
17473
17608
  try {
17474
17609
  for (let r2 = from + 1;r2 <= SCHEMA_REVISION; r2++) {
17475
17610
  const script = MIGRATION(dir, r2);
17476
- if (!fs8.existsSync(script))
17611
+ if (!fs9.existsSync(script))
17477
17612
  throw new Error(`This Sphica has no migration script for revision ${r2} (${script} is missing). Reinstall sphica (\`npm i -g sphica@${packageVersionAt(ROOT) ?? "latest"}\`), then run \`sphica init\` again.`);
17478
17613
  raw.exec("pragma foreign_keys = off");
17479
17614
  try {
17480
17615
  const notes = immediate(raw, () => {
17481
17616
  if (versionOf(raw) !== r2 - 1)
17482
17617
  return [];
17483
- if (fs8.existsSync(CHECK(dir, r2))) {
17484
- raw.exec(fs8.readFileSync(CHECK(dir, r2), "utf8"));
17618
+ if (fs9.existsSync(CHECK(dir, r2))) {
17619
+ raw.exec(fs9.readFileSync(CHECK(dir, r2), "utf8"));
17485
17620
  const stops = taken(raw, STOP2);
17486
17621
  if (stops.length)
17487
17622
  throw new Stop(r2, stops);
17488
17623
  }
17489
- raw.exec(fs8.readFileSync(script, "utf8"));
17624
+ raw.exec(fs9.readFileSync(script, "utf8"));
17490
17625
  if (r2 === 10)
17491
17626
  judgeEveryRecord(raw);
17492
17627
  const broken = raw.prepare("pragma foreign_key_check").all().length;
@@ -17511,7 +17646,7 @@ ${e.list}` : "";
17511
17646
  if (now === from) {
17512
17647
  let left = "the backup made for this run was removed";
17513
17648
  try {
17514
- fs8.rmSync(backup, { force: true });
17649
+ fs9.rmSync(backup, { force: true });
17515
17650
  } catch {
17516
17651
  left = `the backup made for this run is still at ${backup}; delete it yourself`;
17517
17652
  }
@@ -17528,7 +17663,7 @@ ${e.list}` : "";
17528
17663
  });
17529
17664
  }
17530
17665
  function dbInit(file = dbFile()) {
17531
- if (fs8.existsSync(file)) {
17666
+ if (fs9.existsSync(file)) {
17532
17667
  const look = new DatabaseSync3(file, { readOnly: true });
17533
17668
  let generation;
17534
17669
  try {
@@ -17549,27 +17684,32 @@ function dbInit(file = dbFile()) {
17549
17684
  }
17550
17685
  return;
17551
17686
  }
17552
- fs8.mkdirSync(path9.dirname(file), { recursive: true, mode: 448 });
17687
+ fs9.mkdirSync(path9.dirname(file), { recursive: true, mode: 448 });
17553
17688
  const tmp = `${file}.${process.pid}.tmp`;
17554
- fs8.rmSync(tmp, { force: true });
17689
+ fs9.rmSync(tmp, { force: true });
17555
17690
  try {
17556
17691
  withOwner(tmp, (raw) => {
17557
17692
  raw.exec("pragma journal_mode = wal");
17558
- raw.exec(fs8.readFileSync(SCHEMA(), "utf8"));
17693
+ raw.exec(fs9.readFileSync(SCHEMA(), "utf8"));
17559
17694
  if (versionOf(raw) !== SCHEMA_REVISION)
17560
17695
  throw new Error(`db/schema.sql has user_version ${versionOf(raw)}, but the code expects ${SCHEMA_REVISION}`);
17561
17696
  optimize(raw);
17562
17697
  }, true);
17563
- try {
17564
- fs8.linkSync(tmp, file);
17565
- } catch (e) {
17566
- if (e.code === "EEXIST" || fs8.existsSync(file))
17567
- throw new Error(`${file} already exists (another sphica init created it first). Run this again to check it.`);
17568
- fs8.renameSync(tmp, file);
17569
- }
17698
+ const taken2 = () => new Error(`${file} already exists (another sphica init created it first). Run this again to check it.`);
17699
+ withFileLock(`${file}.init.lock`, () => {
17700
+ if (fs9.existsSync(file))
17701
+ throw taken2();
17702
+ try {
17703
+ fs9.linkSync(tmp, file);
17704
+ } catch (e) {
17705
+ if (e.code === "EEXIST")
17706
+ throw taken2();
17707
+ fs9.renameSync(tmp, file);
17708
+ }
17709
+ });
17570
17710
  } finally {
17571
17711
  for (const f of [tmp, `${tmp}-wal`, `${tmp}-shm`])
17572
- fs8.rmSync(f, { force: true });
17712
+ fs9.rmSync(f, { force: true });
17573
17713
  }
17574
17714
  say(`Created: ${file} (revision ${SCHEMA_REVISION})`);
17575
17715
  }
@@ -17605,7 +17745,7 @@ function reindex(file = dbFile()) {
17605
17745
  say(`Rebuilt the index: ${plural(counts.units, "unit")}, ${plural(counts.sources, "source")}`);
17606
17746
  }
17607
17747
  function inspect(file = dbFile()) {
17608
- const size = (f) => fs8.existsSync(f) ? fs8.statSync(f).size : 0;
17748
+ const size = (f) => fs9.existsSync(f) ? fs9.statSync(f).size : 0;
17609
17749
  return withOwner(file, (raw) => {
17610
17750
  const check = (table) => {
17611
17751
  try {
@@ -18329,7 +18469,7 @@ async function doctor(cwd) {
18329
18469
  let usable2 = false;
18330
18470
  if (!runtime)
18331
18471
  say2("none", "DB", "cannot check until Node is upgraded");
18332
- else if (!fs9.existsSync(file))
18472
+ else if (!fs10.existsSync(file))
18333
18473
  say2("fail", "DB", `missing (${file}). Create it with sphica init`);
18334
18474
  else {
18335
18475
  try {
@@ -18353,7 +18493,12 @@ async function doctor(cwd) {
18353
18493
  const twice = await db.selectFrom("unit").select("project_id").where("lifecycle", "in", ["active", "candidate"]).groupBy(["project_id", "content_hash"]).having((eb) => eb.fn.countAll(), ">", 1).execute();
18354
18494
  if (twice.length)
18355
18495
  say2("warn", "Records", `${plural(twice.length, "set")} of live records hold the same words. Read them with Sphica's read and withdraw the extra ones with /sphica:glean`);
18356
- const { found } = localRoots();
18496
+ const { found, ambiguous } = localRoots();
18497
+ const homes = [os4.homedir()];
18498
+ try {
18499
+ homes.push(fs10.realpathSync.native(os4.homedir()));
18500
+ } catch {}
18501
+ const tilde = (p) => underHome(p, homes);
18357
18502
  const rows = await db.selectFrom("project as p").select((eb) => [
18358
18503
  "p.key",
18359
18504
  "p.name",
@@ -18364,7 +18509,8 @@ async function doctor(cwd) {
18364
18509
  console.log(section("Projects", true));
18365
18510
  const column = Math.max(...rows.map((x) => width(inline(x.name)))) + 2;
18366
18511
  for (const x of rows) {
18367
- const where = found.get(x.key) ? "" : " (not on this machine)";
18512
+ const copies = ambiguous.get(x.key);
18513
+ const where = copies ? ` (${copies.length} copies: ${copies.map((c) => inline(tilde(c))).sort().join(", ")})` : found.get(x.key) ? "" : ` (not found in ${inline(tilde(projectsDir()))} or the named projects)`;
18368
18514
  console.log(indent(` ${mark("none")} ${pad(inline(x.name), column)}${plural(Number(x.records ?? 0), "record")}${x.extracted ? ` / last extraction ${new Date(x.extracted).toLocaleString("sv-SE")}` : ""}${where}`));
18369
18515
  }
18370
18516
  });
@@ -18409,7 +18555,7 @@ async function bindGitHub() {
18409
18555
  }
18410
18556
  async function init(flags) {
18411
18557
  const cwd = flags.cwd ?? process.cwd();
18412
- if (!fs9.statSync(cwd, { throwIfNoEntry: false })?.isDirectory())
18558
+ if (!fs10.statSync(cwd, { throwIfNoEntry: false })?.isDirectory())
18413
18559
  throw new Error(`${cwd} is not a directory`);
18414
18560
  const found = identify(cwd);
18415
18561
  if (flags.name !== undefined) {
@@ -18448,7 +18594,7 @@ async function uninstall(yes) {
18448
18594
  await boxed("sphica uninstall", async () => {
18449
18595
  if (process.env.SPHICA_HOME)
18450
18596
  throw new Error("SPHICA_HOME is set; uninstall deletes only ~/.sphica. Unset SPHICA_HOME and run it again");
18451
- if (!fs9.existsSync(home))
18597
+ if (!fs10.existsSync(home))
18452
18598
  console.log(indent(`${mark("none")} ${home} does not exist`));
18453
18599
  else {
18454
18600
  if (!yes) {
@@ -18460,7 +18606,7 @@ async function uninstall(yes) {
18460
18606
  if (isCancel(answer) || !answer)
18461
18607
  return "cancelled";
18462
18608
  }
18463
- fs9.rmSync(home, { recursive: true, force: true });
18609
+ fs10.rmSync(home, { recursive: true, force: true });
18464
18610
  console.log(indent(`${mark("ok")} deleted ${home}`));
18465
18611
  }
18466
18612
  if (where.length > 1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sphica",
3
- "version": "0.6.31",
3
+ "version": "0.6.33",
4
4
  "description": "Records Claude Code and Codex sessions on your machine and keeps past implementation and decisions, with their sources, for your agent to find.",
5
5
  "license": "MIT",
6
6
  "type": "module",