sphica 0.6.25 → 0.6.26
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/README.md +1 -2
- package/dist/capture.js +5 -1
- package/dist/cli.js +118 -60
- package/dist/deliver.js +177 -19564
- package/dist/mcp-record.js +5 -1
- package/dist/mcp.js +2 -0
- package/hooks/hooks.json +37 -10
- 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.26",
|
|
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/README.md
CHANGED
|
@@ -38,7 +38,7 @@ The agent is told to treat records as history, not instructions, and to trust th
|
|
|
38
38
|
## Requirements
|
|
39
39
|
|
|
40
40
|
- Node.js 24.15 or later
|
|
41
|
-
- Claude Code or Codex, or both
|
|
41
|
+
- Claude Code 2.1.139 or later, or Codex, or both. Older Claude Code skips Sphica's hooks without a word, so nothing is captured or delivered (`sphica doctor` flags it)
|
|
42
42
|
- `git`, to identify the repositories you register
|
|
43
43
|
- For `/sphica:harvest` and fetching GitHub sources in `/sphica:glean`: the GitHub CLI (`gh`), signed in with `gh auth login`
|
|
44
44
|
|
|
@@ -139,7 +139,6 @@ The agent searches with Sphica's `search` and opens full records with `read`. `s
|
|
|
139
139
|
|
|
140
140
|
- Only what you traced, harvested, or gleaned becomes a structured record. The rest of a conversation is searchable as captured text (`search` with `sources: true`).
|
|
141
141
|
- For shell commands, Sphica only sees whether a command names a file. It may show decisions for a file the command never reads, and a shell command that edits a file is not treated as an edit (in Codex, a patch passed to `apply_patch` through the shell is treated as an edit).
|
|
142
|
-
- Commands run through Claude Code's PowerShell tool (Windows without Git Bash) do not get decisions yet.
|
|
143
142
|
- In Codex, `$sphica:trace`, `$sphica:harvest`, and `$sphica:glean` can write only when Codex tells Sphica which directory the session is in. Current Codex does. When it does not, they write nothing and tell you why.
|
|
144
143
|
- Showing a record does not make the agent follow it.
|
|
145
144
|
- A code location in a record is checked against your working tree when it is read. Finding the code name (a function name, say) the record points to does not mean the decision still holds.
|
package/dist/capture.js
CHANGED
|
@@ -12211,7 +12211,11 @@ async function main() {
|
|
|
12211
12211
|
if (notice)
|
|
12212
12212
|
process.stdout.write(JSON.stringify({ systemMessage: notice }));
|
|
12213
12213
|
if (send)
|
|
12214
|
-
spawn(process.execPath, [process.argv[1] ?? "", "--flush"], {
|
|
12214
|
+
spawn(process.execPath, [process.argv[1] ?? "", "--flush"], {
|
|
12215
|
+
detached: true,
|
|
12216
|
+
stdio: "ignore",
|
|
12217
|
+
windowsHide: true
|
|
12218
|
+
}).unref();
|
|
12215
12219
|
}
|
|
12216
12220
|
if (process.argv[1] && /capture\.(ts|js)$/.test(process.argv[1])) {
|
|
12217
12221
|
main().catch(() => {});
|
package/dist/cli.js
CHANGED
|
@@ -3413,8 +3413,24 @@ function cwdOf(pid) {
|
|
|
3413
3413
|
return null;
|
|
3414
3414
|
}
|
|
3415
3415
|
}
|
|
3416
|
+
var CLAUDE_CODE_MIN = "2.1.139";
|
|
3416
3417
|
var CACHED = /\/plugins\/cache\/[^/]+\/sphica\/[^/]+$/;
|
|
3417
|
-
|
|
3418
|
+
var unknown = (i2) => typeof i2 === "object" && i2 !== null && ("unknown" in i2);
|
|
3419
|
+
function npmCli(execPath, platform) {
|
|
3420
|
+
const dir = path2.dirname(execPath);
|
|
3421
|
+
const at = platform === "win32" ? path2.join(dir, "node_modules", "npm", "bin", "npm-cli.js") : path2.resolve(dir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js");
|
|
3422
|
+
return fs2.existsSync(at) ? at : null;
|
|
3423
|
+
}
|
|
3424
|
+
function findExe(name, pathEnv, platform) {
|
|
3425
|
+
const sep = platform === "win32" ? ";" : path2.delimiter;
|
|
3426
|
+
for (const dir of pathEnv.split(sep).filter(Boolean)) {
|
|
3427
|
+
const at = path2.join(dir, `${name}.exe`);
|
|
3428
|
+
if (fs2.existsSync(at))
|
|
3429
|
+
return at;
|
|
3430
|
+
}
|
|
3431
|
+
return null;
|
|
3432
|
+
}
|
|
3433
|
+
function observe(cwdRoot, platform = process.platform, env = process.env, execPath = process.execPath) {
|
|
3418
3434
|
const install = (root) => ({
|
|
3419
3435
|
version: versionAt(root),
|
|
3420
3436
|
packageVersion: packageVersionAt(root),
|
|
@@ -3422,18 +3438,32 @@ function observe(cwdRoot) {
|
|
|
3422
3438
|
});
|
|
3423
3439
|
const repository = [path2.dirname(ROOT), cwdRoot].filter((d) => fs2.existsSync(path2.join(d, ".claude-plugin", "marketplace.json"))).map((d) => install(path2.join(d, "plugin"))).find((r2) => r2.version !== null) ?? null;
|
|
3424
3440
|
let claude;
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3441
|
+
const claudeExe = platform === "win32" ? findExe("claude", env.PATH ?? env.Path ?? "", platform) : "claude";
|
|
3442
|
+
if (!claudeExe)
|
|
3443
|
+
claude = { unknown: "no claude.exe on PATH (an npm install puts claude.cmd there)" };
|
|
3444
|
+
else
|
|
3445
|
+
try {
|
|
3446
|
+
const list = JSON.parse(execFileSync(claudeExe, ["plugin", "list", "--json"], {
|
|
3447
|
+
encoding: "utf8",
|
|
3448
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
3449
|
+
timeout: 30000
|
|
3450
|
+
}));
|
|
3451
|
+
const m = list.find((p) => p.id.startsWith("sphica@") && p.scope === "user");
|
|
3452
|
+
claude = m?.installPath ? { version: m.version ?? null, root: m.installPath } : null;
|
|
3453
|
+
} catch {
|
|
3454
|
+
claude = { unknown: "claude plugin list --json failed" };
|
|
3455
|
+
}
|
|
3456
|
+
let claudeVersion;
|
|
3457
|
+
if (claudeExe)
|
|
3458
|
+
try {
|
|
3459
|
+
const out = execFileSync(claudeExe, ["--version"], {
|
|
3460
|
+
encoding: "utf8",
|
|
3461
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
3462
|
+
timeout: 1e4
|
|
3463
|
+
});
|
|
3464
|
+
claudeVersion = /^\s*(\d+\.\d+\.\d+)/.exec(out)?.[1];
|
|
3465
|
+
} catch {}
|
|
3466
|
+
let codexHome = env.CODEX_HOME ?? path2.join(os.homedir(), ".codex");
|
|
3437
3467
|
try {
|
|
3438
3468
|
codexHome = fs2.realpathSync(codexHome);
|
|
3439
3469
|
} catch {}
|
|
@@ -3445,46 +3475,59 @@ function observe(cwdRoot) {
|
|
|
3445
3475
|
}
|
|
3446
3476
|
}
|
|
3447
3477
|
let running;
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3478
|
+
if (platform === "win32")
|
|
3479
|
+
running = { unknown: "not checked on Windows" };
|
|
3480
|
+
else
|
|
3481
|
+
try {
|
|
3482
|
+
const out = execFileSync("ps", ["-U", String(process.getuid?.()), "-o", "pid=,lstart=,args="], {
|
|
3483
|
+
encoding: "utf8",
|
|
3484
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
3485
|
+
env: { ...process.env, LC_ALL: "C" },
|
|
3486
|
+
timeout: 1e4
|
|
3487
|
+
});
|
|
3488
|
+
running = parsePs(out).flatMap((p) => {
|
|
3489
|
+
const cwd = path2.isAbsolute(p.script) ? { dir: "/", replaced: false } : cwdOf(p.pid);
|
|
3490
|
+
if (!cwd)
|
|
3491
|
+
return [{ pid: p.pid, started: p.started, root: null, version: null }];
|
|
3492
|
+
const root = path2.dirname(path2.dirname(path2.resolve(cwd.dir, p.script)));
|
|
3493
|
+
const cached = CACHED.test(root);
|
|
3494
|
+
const now = versionAt(root);
|
|
3495
|
+
if (now === null && !cached)
|
|
3496
|
+
return [];
|
|
3497
|
+
let version2 = cwd.replaced || now === null ? cached ? path2.basename(root) : null : now;
|
|
3498
|
+
if (!cached && version2 !== null) {
|
|
3499
|
+
try {
|
|
3500
|
+
const touched = Math.max(...[path2.join("dist", "mcp.js"), MANIFEST].map((f) => fs2.statSync(path2.join(root, f)).mtimeMs));
|
|
3501
|
+
if (touched > p.started.getTime())
|
|
3502
|
+
version2 = null;
|
|
3503
|
+
} catch {
|
|
3469
3504
|
version2 = null;
|
|
3470
|
-
|
|
3471
|
-
version2 = null;
|
|
3505
|
+
}
|
|
3472
3506
|
}
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
}
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
}
|
|
3507
|
+
return [{ pid: p.pid, started: p.started, root, version: version2, replaced: cwd.replaced }];
|
|
3508
|
+
});
|
|
3509
|
+
} catch {
|
|
3510
|
+
running = { unknown: "ps failed" };
|
|
3511
|
+
}
|
|
3479
3512
|
let global = null;
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3513
|
+
const cli = npmCli(execPath, platform);
|
|
3514
|
+
if (!cli && platform === "win32")
|
|
3515
|
+
global = { unknown: "no npm CLI next to node" };
|
|
3516
|
+
else
|
|
3517
|
+
try {
|
|
3518
|
+
const [cmd, args] = cli ? [execPath, [cli, "root", "-g"]] : ["npm", ["root", "-g"]];
|
|
3519
|
+
const out = execFileSync(cmd, args, {
|
|
3520
|
+
encoding: "utf8",
|
|
3521
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
3522
|
+
timeout: 1e4
|
|
3523
|
+
});
|
|
3524
|
+
const at = path2.join(out.trim(), "sphica");
|
|
3525
|
+
if (fs2.existsSync(at))
|
|
3526
|
+
global = install(at);
|
|
3527
|
+
} catch {
|
|
3528
|
+
global = { unknown: "npm root -g failed" };
|
|
3529
|
+
}
|
|
3530
|
+
return { repository, cli: install(ROOT), global, claude, claudeVersion, codex, codexCache, running };
|
|
3488
3531
|
}
|
|
3489
3532
|
function safeDirs(dir) {
|
|
3490
3533
|
try {
|
|
@@ -3511,12 +3554,15 @@ var RELOAD = { claude: "/reload-plugins or a new session", codex: "reopening Cod
|
|
|
3511
3554
|
function report(s, now = new Date) {
|
|
3512
3555
|
const lines = [];
|
|
3513
3556
|
const issues = [];
|
|
3557
|
+
const failures = [];
|
|
3514
3558
|
const todo = new Set;
|
|
3515
3559
|
const home = os.homedir();
|
|
3516
3560
|
const short = (p) => p.startsWith(`${home}/`) ? `~${p.slice(home.length)}` : p;
|
|
3517
3561
|
const say = (m, label, text) => {
|
|
3518
3562
|
if (m === "warn" || m === "fail")
|
|
3519
3563
|
issues.push(label);
|
|
3564
|
+
if (m === "fail")
|
|
3565
|
+
failures.push(label);
|
|
3520
3566
|
lines.push(` ${mark(m)} ${pad(label, 19)}${text}`);
|
|
3521
3567
|
};
|
|
3522
3568
|
const row = (label, i2, note, aside = "", m = note ? "warn" : "ok") => {
|
|
@@ -3543,7 +3589,11 @@ ${indent}${caution(note)}` : ""}`);
|
|
|
3543
3589
|
if (s.repository)
|
|
3544
3590
|
row("repository", packageInstall(s.repository));
|
|
3545
3591
|
row("Running CLI", packageInstall(s.cli), packageAgainst(s.cli).note);
|
|
3546
|
-
if (
|
|
3592
|
+
if (unknown(s.global))
|
|
3593
|
+
say("none", "npm i -g CLI", `unknown (${s.global.unknown})`);
|
|
3594
|
+
else if (s.global === null)
|
|
3595
|
+
say("none", "npm i -g CLI", "not installed");
|
|
3596
|
+
else if (s.global && path2.resolve(s.global.root) !== path2.resolve(s.cli.root)) {
|
|
3547
3597
|
const { note, update } = packageAgainst(s.global);
|
|
3548
3598
|
if (update)
|
|
3549
3599
|
todo.add("global");
|
|
@@ -3581,8 +3631,8 @@ ${indent}${caution(note)}` : ""}`);
|
|
|
3581
3631
|
else
|
|
3582
3632
|
say("none", "repository", "not visible. Comparing against this CLI's version");
|
|
3583
3633
|
row("Plugin in this CLI", s.cli, against(s.cli).note);
|
|
3584
|
-
if (s.claude
|
|
3585
|
-
say("none", "Claude Code",
|
|
3634
|
+
if (unknown(s.claude))
|
|
3635
|
+
say("none", "Claude Code", `unknown (${s.claude.unknown})`);
|
|
3586
3636
|
else if (s.claude === null)
|
|
3587
3637
|
say("none", "Claude Code", "not installed");
|
|
3588
3638
|
else {
|
|
@@ -3591,6 +3641,8 @@ ${indent}${caution(note)}` : ""}`);
|
|
|
3591
3641
|
todo.add("claude");
|
|
3592
3642
|
row("Claude Code", s.claude, note);
|
|
3593
3643
|
}
|
|
3644
|
+
if (s.claudeVersion)
|
|
3645
|
+
say(compareVersions(s.claudeVersion, CLAUDE_CODE_MIN) < 0 ? "fail" : "ok", "Claude Code app", compareVersions(s.claudeVersion, CLAUDE_CODE_MIN) < 0 ? `${s.claudeVersion}. Sphica's hooks need ${CLAUDE_CODE_MIN} or later and do not run on this version: update Claude Code` : s.claudeVersion);
|
|
3594
3646
|
if (s.codex.length === 0)
|
|
3595
3647
|
say("none", "Codex", `not found (looked in ${short(s.codexCache)})`);
|
|
3596
3648
|
for (const x2 of s.codex) {
|
|
@@ -3600,16 +3652,16 @@ ${indent}${caution(note)}` : ""}`);
|
|
|
3600
3652
|
row("Codex", x2, note);
|
|
3601
3653
|
}
|
|
3602
3654
|
const x = s.codex.length === 1 ? s.codex[0] : undefined;
|
|
3603
|
-
if (!base && s.claude && s.claude
|
|
3655
|
+
if (!base && s.claude && !unknown(s.claude) && x && s.claude.version === x.version) {
|
|
3604
3656
|
if (fs2.existsSync(s.claude.root) && differingFiles(s.claude.root, x.root).length) {
|
|
3605
3657
|
say("warn", "Claude Code and Codex", "same version, different contents");
|
|
3606
3658
|
}
|
|
3607
3659
|
}
|
|
3608
|
-
if (s.running
|
|
3609
|
-
say("none", "Running MCP",
|
|
3660
|
+
if (unknown(s.running))
|
|
3661
|
+
say("none", "Running MCP", `unknown (${s.running.unknown})`);
|
|
3610
3662
|
else if (s.running.length === 0)
|
|
3611
3663
|
say("none", "Running MCP", "none");
|
|
3612
|
-
for (const r2 of s.running
|
|
3664
|
+
for (const r2 of unknown(s.running) ? [] : s.running) {
|
|
3613
3665
|
const when = r2.started.toLocaleString("sv-SE").slice(r2.started.toDateString() === now.toDateString() ? 11 : 5, 16);
|
|
3614
3666
|
const label = `MCP pid ${r2.pid}`;
|
|
3615
3667
|
const aside = ` (started ${when})`;
|
|
@@ -3630,7 +3682,7 @@ ${indent}${caution(note)}` : ""}`);
|
|
|
3630
3682
|
note = "reads this location directly, not a distributed cache (a directory marketplace or --plugin-dir)";
|
|
3631
3683
|
} else if (state === "orphaned")
|
|
3632
3684
|
note = `a version Claude Code replaced on update. Fix with ${again}`;
|
|
3633
|
-
else if (installed && installed
|
|
3685
|
+
else if (installed && !unknown(installed) && installed.version && r2.version) {
|
|
3634
3686
|
if (compareVersions(r2.version, installed.version) < 0)
|
|
3635
3687
|
note = `older than the installed ${installed.version}. Fix with ${again}`;
|
|
3636
3688
|
}
|
|
@@ -3640,7 +3692,7 @@ ${indent}${caution(note)}` : ""}`);
|
|
|
3640
3692
|
const u3 = UPDATE[k];
|
|
3641
3693
|
return k === "global" && packageBase.version ? { ...u3, command: `npm i -g sphica@${packageBase.version}` } : { ...u3 };
|
|
3642
3694
|
});
|
|
3643
|
-
return { lines, issues, updates };
|
|
3695
|
+
return { lines, issues, failures, updates };
|
|
3644
3696
|
}
|
|
3645
3697
|
|
|
3646
3698
|
// server/src/sqlite.ts
|
|
@@ -15621,7 +15673,11 @@ async function main() {
|
|
|
15621
15673
|
if (notice)
|
|
15622
15674
|
process.stdout.write(JSON.stringify({ systemMessage: notice }));
|
|
15623
15675
|
if (send)
|
|
15624
|
-
spawn(process.execPath, [process.argv[1] ?? "", "--flush"], {
|
|
15676
|
+
spawn(process.execPath, [process.argv[1] ?? "", "--flush"], {
|
|
15677
|
+
detached: true,
|
|
15678
|
+
stdio: "ignore",
|
|
15679
|
+
windowsHide: true
|
|
15680
|
+
}).unref();
|
|
15625
15681
|
}
|
|
15626
15682
|
if (process.argv[1] && /capture\.(ts|js)$/.test(process.argv[1])) {
|
|
15627
15683
|
main().catch(() => {});
|
|
@@ -16698,6 +16754,8 @@ async function doctor(cwd) {
|
|
|
16698
16754
|
console.log(title2("sphica doctor"));
|
|
16699
16755
|
const plugin = report(observe(identify(cwd)?.root ?? cwd));
|
|
16700
16756
|
issues.push(...plugin.issues);
|
|
16757
|
+
if (plugin.failures.length)
|
|
16758
|
+
process.exitCode = 1;
|
|
16701
16759
|
for (const line of plugin.lines)
|
|
16702
16760
|
console.log(/^\S/.test(line) ? section(line) : indent(line));
|
|
16703
16761
|
if (plugin.updates.length)
|