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.
@@ -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.25",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sphica",
3
- "version": "0.6.25",
3
+ "version": "0.6.26",
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/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"], { detached: true, stdio: "ignore" }).unref();
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
- function observe(cwdRoot) {
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
- try {
3426
- const list = JSON.parse(execFileSync("claude", ["plugin", "list", "--json"], {
3427
- encoding: "utf8",
3428
- stdio: ["ignore", "pipe", "ignore"],
3429
- timeout: 30000
3430
- }));
3431
- const m = list.find((p) => p.id.startsWith("sphica@") && p.scope === "user");
3432
- claude = m?.installPath ? { version: m.version ?? null, root: m.installPath } : null;
3433
- } catch {
3434
- claude = "unknown";
3435
- }
3436
- let codexHome = process.env.CODEX_HOME ?? path2.join(os.homedir(), ".codex");
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
- try {
3449
- const out = execFileSync("ps", ["-U", String(process.getuid?.()), "-o", "pid=,lstart=,args="], {
3450
- encoding: "utf8",
3451
- stdio: ["ignore", "pipe", "ignore"],
3452
- env: { ...process.env, LC_ALL: "C" },
3453
- timeout: 1e4
3454
- });
3455
- running = parsePs(out).flatMap((p) => {
3456
- const cwd = path2.isAbsolute(p.script) ? { dir: "/", replaced: false } : cwdOf(p.pid);
3457
- if (!cwd)
3458
- return [{ pid: p.pid, started: p.started, root: null, version: null }];
3459
- const root = path2.dirname(path2.dirname(path2.resolve(cwd.dir, p.script)));
3460
- const cached = CACHED.test(root);
3461
- const now = versionAt(root);
3462
- if (now === null && !cached)
3463
- return [];
3464
- let version2 = cwd.replaced || now === null ? cached ? path2.basename(root) : null : now;
3465
- if (!cached && version2 !== null) {
3466
- try {
3467
- const touched = Math.max(...[path2.join("dist", "mcp.js"), MANIFEST].map((f) => fs2.statSync(path2.join(root, f)).mtimeMs));
3468
- if (touched > p.started.getTime())
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
- } catch {
3471
- version2 = null;
3505
+ }
3472
3506
  }
3473
- }
3474
- return [{ pid: p.pid, started: p.started, root, version: version2, replaced: cwd.replaced }];
3475
- });
3476
- } catch {
3477
- running = null;
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
- try {
3481
- const at = path2.join(execFileSync("npm", ["root", "-g"], { encoding: "utf8" }).trim(), "sphica");
3482
- if (fs2.existsSync(at))
3483
- global = install(at);
3484
- } catch {
3485
- global = null;
3486
- }
3487
- return { repository, cli: install(ROOT), global, claude, codex, codexCache, running };
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 (s.global && path2.resolve(s.global.root) !== path2.resolve(s.cli.root)) {
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 === "unknown")
3585
- say("none", "Claude Code", "unknown (claude plugin list --json is unavailable)");
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 !== "unknown" && x && s.claude.version === x.version) {
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 === null)
3609
- say("none", "Running MCP", "unknown (ps is unavailable)");
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 !== "unknown" && installed.version && r2.version) {
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"], { detached: true, stdio: "ignore" }).unref();
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)