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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/dist/cli.js +228 -82
- package/package.json +1 -1
|
@@ -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.
|
|
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",
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
16221
|
-
|
|
16222
|
-
|
|
16223
|
-
|
|
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
|
|
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 =
|
|
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) &&
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
16534
|
+
fs8.rmSync(path8.join(held, f), { force: true });
|
|
16400
16535
|
}
|
|
16401
16536
|
function spool(record) {
|
|
16402
16537
|
const dir = spoolDir();
|
|
16403
|
-
|
|
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
|
-
|
|
16407
|
-
|
|
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 =
|
|
16413
|
-
const h2 =
|
|
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 (!
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
16643
|
+
fs8.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
16509
16644
|
const tmp = path8.join(dir, `.${path8.basename(file)}.${process.pid}.tmp`);
|
|
16510
|
-
|
|
16511
|
-
|
|
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) => (
|
|
16676
|
+
const fresh = (file) => (fs8.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? Date.now()) >= cutoff;
|
|
16542
16677
|
let names;
|
|
16543
16678
|
try {
|
|
16544
|
-
names =
|
|
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 (!
|
|
16685
|
+
if (!fs8.statSync(at, { throwIfNoEntry: false })?.isDirectory()) {
|
|
16551
16686
|
if (!fresh(at))
|
|
16552
|
-
|
|
16687
|
+
fs8.rmSync(at, { force: true });
|
|
16553
16688
|
} else {
|
|
16554
|
-
const files =
|
|
16689
|
+
const files = fs8.readdirSync(at);
|
|
16555
16690
|
if (files.length && !files.some((f) => fresh(path8.join(at, f))))
|
|
16556
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
16825
|
+
fs8.mkdirSync(spoolDir(), { recursive: true, mode: 448 });
|
|
16691
16826
|
for (let attempt2 = 0;attempt2 < 2; attempt2++) {
|
|
16692
16827
|
try {
|
|
16693
|
-
|
|
16828
|
+
fs8.writeFileSync(file, String(process.pid), { flag: "wx", mode: 384 });
|
|
16694
16829
|
return () => {
|
|
16695
16830
|
try {
|
|
16696
|
-
if (
|
|
16697
|
-
|
|
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 =
|
|
16839
|
+
const st = fs8.statSync(file, { throwIfNoEntry: false });
|
|
16705
16840
|
if (!st)
|
|
16706
16841
|
continue;
|
|
16707
|
-
const holder = Number(
|
|
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
|
-
|
|
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
|
|
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(
|
|
16965
|
+
r2 = current(JSON.parse(fs8.readFileSync(path8.join(from, name), "utf8")));
|
|
16831
16966
|
} catch {
|
|
16832
|
-
|
|
16833
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17000
|
+
fs8.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
|
|
16866
17001
|
for (const x of bad) {
|
|
16867
17002
|
try {
|
|
16868
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17502
|
+
fs9.renameSync(partial, done);
|
|
17368
17503
|
} catch (e) {
|
|
17369
17504
|
for (const f of [partial, `${partial}-wal`, `${partial}-shm`])
|
|
17370
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 (
|
|
17484
|
-
raw.exec(
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
17687
|
+
fs9.mkdirSync(path9.dirname(file), { recursive: true, mode: 448 });
|
|
17553
17688
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
17554
|
-
|
|
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(
|
|
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
|
-
|
|
17564
|
-
|
|
17565
|
-
|
|
17566
|
-
|
|
17567
|
-
|
|
17568
|
-
|
|
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
|
-
|
|
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) =>
|
|
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 (!
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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
|
-
|
|
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.
|
|
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",
|