@bububuger/spanory 0.1.21 → 0.1.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.
Files changed (2) hide show
  1. package/dist/index.js +140 -130
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // dist/index.js
4
4
  import { existsSync, readFileSync, realpathSync } from "node:fs";
5
- import { chmod, copyFile, mkdir as mkdir4, readdir as readdir5, readFile as readFile8, rm, stat as stat4, writeFile as writeFile4 } from "node:fs/promises";
5
+ import { copyFile, mkdir as mkdir4, readdir as readdir5, readFile as readFile8, rm, stat as stat4, writeFile as writeFile4 } from "node:fs/promises";
6
6
  import path8 from "node:path";
7
7
  import { createHash as createHash4 } from "node:crypto";
8
8
  import { spawnSync } from "node:child_process";
@@ -4473,6 +4473,14 @@ async function backupIfExists(filePath) {
4473
4473
  await copyFile(filePath, backupPath);
4474
4474
  return backupPath;
4475
4475
  }
4476
+ function isSpanoryCodexNotifyConfigured(configText) {
4477
+ return /notify\s*=\s*\[[^\]]*spanory-codex-notify\.sh[^\]]*\]/m.test(String(configText ?? ""));
4478
+ }
4479
+ function stripSpanoryCodexNotifyConfig(configText) {
4480
+ const next = String(configText ?? "").replace(/^notify\s*=\s*\[[^\]]*spanory-codex-notify\.sh[^\]]*\]\s*$/gm, "").replace(/\n{3,}/g, "\n\n").trim();
4481
+ return next ? `${next}
4482
+ ` : "";
4483
+ }
4476
4484
  function isSpanoryHookCommand(command) {
4477
4485
  const text = String(command ?? "");
4478
4486
  return /\bspanory\b/.test(text) && /\bhook\b/.test(text);
@@ -4525,76 +4533,10 @@ async function applyClaudeSetup({ homeRoot, spanoryBin, dryRun }) {
4525
4533
  backup
4526
4534
  };
4527
4535
  }
4528
- function upsertCodexNotifyConfig(configText, notifyScriptRef) {
4529
- const notifyLine = `notify = ["${escapeTomlBasicString(notifyScriptRef)}"]`;
4530
- if (/^notify\s*=.*$/m.test(configText)) {
4531
- return configText.replace(/^notify\s*=.*$/m, notifyLine);
4532
- }
4533
- const withNewline = configText.trimEnd();
4534
- if (!withNewline)
4535
- return `${notifyLine}
4536
- `;
4537
- return `${withNewline}
4538
-
4539
- ${notifyLine}
4540
- `;
4541
- }
4542
- function escapeTomlBasicString(value) {
4543
- return String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"');
4544
- }
4545
- function isSpanoryCodexNotifyConfigured(configText) {
4546
- return /notify\s*=\s*\[[^\]]*spanory-codex-notify\.sh[^\]]*\]/m.test(String(configText ?? ""));
4547
- }
4548
- function stripSpanoryCodexNotifyConfig(configText) {
4549
- const next = String(configText ?? "").replace(/^notify\s*=\s*\[[^\]]*spanory-codex-notify\.sh[^\]]*\]\s*$/gm, "").replace(/\n{3,}/g, "\n\n").trim();
4550
- return next ? `${next}
4551
- ` : "";
4552
- }
4553
- function codexNotifyScriptContent({ spanoryBin, codexHome, exportDir, logFile }) {
4554
- return `#!/usr/bin/env bash
4555
- set -euo pipefail
4556
- payload="\${1:-}"
4557
- if [[ -z "$payload" ]] && [[ ! -t 0 ]]; then
4558
- IFS= read -r -t 0 payload || true
4559
- fi
4560
- if [[ -z "\${payload//[$'\\t\\r\\n ']/}" ]]; then
4561
- echo "skip=empty-payload source=codex-notify args=$#" >> "${logFile}"
4562
- exit 0
4563
- fi
4564
- payload_file="$(mktemp "\${TMPDIR:-/tmp}/spanory-codex-notify.XXXXXX")"
4565
- printf '%s' "$payload" > "$payload_file"
4566
- echo "$payload" | "${spanoryBin}" runtime codex hook \\
4567
- --last-turn-only \\
4568
- --runtime-home "${codexHome}" \\
4569
- --export-json-dir "${exportDir}" \\
4570
- >> "${logFile}" 2>&1 || true
4571
- (
4572
- sleep 2
4573
- cat "$payload_file" | "${spanoryBin}" runtime codex hook \\
4574
- --last-turn-only \\
4575
- --force \\
4576
- --runtime-home "${codexHome}" \\
4577
- --export-json-dir "${exportDir}" \\
4578
- >> "${logFile}" 2>&1 || true
4579
- rm -f "$payload_file"
4580
- ) >/dev/null 2>&1 &
4581
- `;
4582
- }
4583
- async function applyCodexSetup({ homeRoot, spanoryBin, dryRun }) {
4536
+ async function applyCodexWatchSetup({ homeRoot, dryRun }) {
4584
4537
  const codexHome = path8.join(homeRoot, ".codex");
4585
- const spanoryHome = process.env.SPANORY_HOME ?? path8.join(homeRoot, ".spanory");
4586
- const binDir = path8.join(codexHome, "bin");
4587
- const stateDir = path8.join(codexHome, "state", "spanory");
4588
- const scriptPath = path8.join(binDir, "spanory-codex-notify.sh");
4589
- const logFile = path8.join(spanoryHome, "logs", "codex-notify.log");
4538
+ const scriptPath = path8.join(codexHome, "bin", "spanory-codex-notify.sh");
4590
4539
  const configPath = path8.join(codexHome, "config.toml");
4591
- const notifyScriptRef = scriptPath;
4592
- const scriptContent = codexNotifyScriptContent({
4593
- spanoryBin,
4594
- codexHome,
4595
- exportDir: stateDir,
4596
- logFile
4597
- });
4598
4540
  let currentConfig = "";
4599
4541
  try {
4600
4542
  currentConfig = await readFile8(configPath, "utf-8");
@@ -4602,29 +4544,24 @@ async function applyCodexSetup({ homeRoot, spanoryBin, dryRun }) {
4602
4544
  if (error?.code !== "ENOENT")
4603
4545
  throw error;
4604
4546
  }
4605
- const nextConfig = upsertCodexNotifyConfig(currentConfig, notifyScriptRef);
4547
+ const nextConfig = stripSpanoryCodexNotifyConfig(currentConfig);
4606
4548
  const configChanged = currentConfig !== nextConfig;
4607
- let currentScript = "";
4549
+ let scriptPresent = false;
4608
4550
  try {
4609
- currentScript = await readFile8(scriptPath, "utf-8");
4610
- } catch (error) {
4611
- if (error?.code !== "ENOENT")
4612
- throw error;
4551
+ await stat4(scriptPath);
4552
+ scriptPresent = true;
4553
+ } catch {
4613
4554
  }
4614
- const scriptChanged = currentScript !== scriptContent;
4615
- const changed = configChanged || scriptChanged;
4555
+ const changed = configChanged || scriptPresent;
4616
4556
  let configBackup = null;
4617
4557
  if (changed && !dryRun) {
4618
- await mkdir4(binDir, { recursive: true });
4619
- await mkdir4(stateDir, { recursive: true });
4620
- await mkdir4(path8.dirname(logFile), { recursive: true });
4621
4558
  if (configChanged) {
4559
+ await mkdir4(path8.dirname(configPath), { recursive: true });
4622
4560
  configBackup = await backupIfExists(configPath);
4623
4561
  await writeFile4(configPath, nextConfig, "utf-8");
4624
4562
  }
4625
- if (scriptChanged) {
4626
- await writeFile4(scriptPath, scriptContent, "utf-8");
4627
- await chmod(scriptPath, 493);
4563
+ if (scriptPresent) {
4564
+ await rm(scriptPath, { force: true });
4628
4565
  }
4629
4566
  }
4630
4567
  return {
@@ -4632,15 +4569,58 @@ async function applyCodexSetup({ homeRoot, spanoryBin, dryRun }) {
4632
4569
  ok: true,
4633
4570
  changed,
4634
4571
  dryRun,
4572
+ mode: "watch",
4635
4573
  configPath,
4636
4574
  scriptPath,
4637
- configBackup
4575
+ configBackup,
4576
+ notifyConfigured: isSpanoryCodexNotifyConfigured(nextConfig),
4577
+ scriptPresent: false
4638
4578
  };
4639
4579
  }
4640
- async function applyCodexWatchSetup({ homeRoot, dryRun }) {
4580
+ function removeClaudeHooks(settings) {
4581
+ if (!settings.hooks || typeof settings.hooks !== "object")
4582
+ return;
4583
+ for (const eventName of Object.keys(settings.hooks)) {
4584
+ const groups = settings.hooks[eventName];
4585
+ if (!Array.isArray(groups))
4586
+ continue;
4587
+ for (const group of groups) {
4588
+ if (!group || !Array.isArray(group.hooks))
4589
+ continue;
4590
+ group.hooks = group.hooks.filter((hook) => !(hook && typeof hook === "object" && isSpanoryHookCommand(hook.command)));
4591
+ }
4592
+ settings.hooks[eventName] = groups.filter((group) => !group || !Array.isArray(group.hooks) || group.hooks.length > 0);
4593
+ if (settings.hooks[eventName].length === 0)
4594
+ delete settings.hooks[eventName];
4595
+ }
4596
+ if (Object.keys(settings.hooks).length === 0)
4597
+ delete settings.hooks;
4598
+ }
4599
+ async function teardownClaudeSetup({ homeRoot, dryRun }) {
4600
+ const settingsPath = path8.join(homeRoot, ".claude", "settings.json");
4601
+ let settings = {};
4602
+ try {
4603
+ settings = JSON.parse(await readFile8(settingsPath, "utf-8"));
4604
+ } catch (error) {
4605
+ if (error?.code !== "ENOENT")
4606
+ throw new Error(`failed to parse Claude settings: ${error.message ?? String(error)}`);
4607
+ return { runtime: "claude-code", ok: true, changed: false, dryRun, settingsPath, backup: null };
4608
+ }
4609
+ const before = JSON.stringify(settings);
4610
+ removeClaudeHooks(settings);
4611
+ const after = JSON.stringify(settings);
4612
+ const changed = before !== after;
4613
+ let backup = null;
4614
+ if (changed && !dryRun) {
4615
+ backup = await backupIfExists(settingsPath);
4616
+ await writeFile4(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
4617
+ }
4618
+ return { runtime: "claude-code", ok: true, changed, dryRun, settingsPath, backup };
4619
+ }
4620
+ async function teardownCodexSetup({ homeRoot, dryRun }) {
4641
4621
  const codexHome = path8.join(homeRoot, ".codex");
4642
- const scriptPath = path8.join(codexHome, "bin", "spanory-codex-notify.sh");
4643
4622
  const configPath = path8.join(codexHome, "config.toml");
4623
+ const scriptPath = path8.join(codexHome, "bin", "spanory-codex-notify.sh");
4644
4624
  let currentConfig = "";
4645
4625
  try {
4646
4626
  currentConfig = await readFile8(configPath, "utf-8");
@@ -4660,26 +4640,75 @@ async function applyCodexWatchSetup({ homeRoot, dryRun }) {
4660
4640
  let configBackup = null;
4661
4641
  if (changed && !dryRun) {
4662
4642
  if (configChanged) {
4663
- await mkdir4(path8.dirname(configPath), { recursive: true });
4664
4643
  configBackup = await backupIfExists(configPath);
4665
4644
  await writeFile4(configPath, nextConfig, "utf-8");
4666
4645
  }
4667
- if (scriptPresent) {
4646
+ if (scriptPresent)
4668
4647
  await rm(scriptPath, { force: true });
4648
+ }
4649
+ return { runtime: "codex", ok: true, changed, dryRun, configPath, scriptPath, configBackup, scriptRemoved: scriptPresent };
4650
+ }
4651
+ async function teardownOpenclawSetup({ homeRoot, openclawRuntimeHome, dryRun }) {
4652
+ if (!commandExists("openclaw")) {
4653
+ return { runtime: "openclaw", ok: true, skipped: true, detail: "openclaw command not found in PATH" };
4654
+ }
4655
+ const runtimeHome = openclawRuntimeHomeForSetup(homeRoot, openclawRuntimeHome);
4656
+ if (dryRun)
4657
+ return { runtime: "openclaw", ok: true, changed: true, dryRun };
4658
+ const result = runSystemCommand("openclaw", ["plugins", "uninstall", OPENCLAW_SPANORY_PLUGIN_ID], {
4659
+ env: { ...process.env, ...runtimeHome ? { OPENCLAW_STATE_DIR: resolveRuntimeHome3("openclaw", runtimeHome) } : {} }
4660
+ });
4661
+ if (result.code !== 0)
4662
+ throw new Error(result.stderr || result.error || "openclaw plugins uninstall failed");
4663
+ return { runtime: "openclaw", ok: true, changed: true, dryRun };
4664
+ }
4665
+ async function teardownOpencodeSetup({ homeRoot, opencodeRuntimeHome, dryRun }) {
4666
+ const runtimeHome = opencodeRuntimeHomeForSetup(homeRoot, opencodeRuntimeHome);
4667
+ const loaderFile = opencodePluginLoaderPath(runtimeHome);
4668
+ let present = false;
4669
+ try {
4670
+ await stat4(loaderFile);
4671
+ present = true;
4672
+ } catch {
4673
+ }
4674
+ if (present && !dryRun)
4675
+ await rm(loaderFile, { force: true });
4676
+ return { runtime: "opencode", ok: true, changed: present, dryRun, loaderFile };
4677
+ }
4678
+ async function runSetupTeardown(options) {
4679
+ const homeRoot = setupHomeRoot(options.home);
4680
+ const selected = parseSetupRuntimes(options.runtimes);
4681
+ const dryRun = Boolean(options.dryRun);
4682
+ const results = [];
4683
+ if (selected.includes("claude-code")) {
4684
+ try {
4685
+ results.push(await teardownClaudeSetup({ homeRoot, dryRun }));
4686
+ } catch (error) {
4687
+ results.push({ runtime: "claude-code", ok: false, error: String(error?.message ?? error) });
4669
4688
  }
4670
4689
  }
4671
- return {
4672
- runtime: "codex",
4673
- ok: true,
4674
- changed,
4675
- dryRun,
4676
- mode: "watch",
4677
- configPath,
4678
- scriptPath,
4679
- configBackup,
4680
- notifyConfigured: isSpanoryCodexNotifyConfigured(nextConfig),
4681
- scriptPresent: false
4682
- };
4690
+ if (selected.includes("codex")) {
4691
+ try {
4692
+ results.push(await teardownCodexSetup({ homeRoot, dryRun }));
4693
+ } catch (error) {
4694
+ results.push({ runtime: "codex", ok: false, error: String(error?.message ?? error) });
4695
+ }
4696
+ }
4697
+ if (selected.includes("openclaw")) {
4698
+ try {
4699
+ results.push(await teardownOpenclawSetup({ homeRoot, openclawRuntimeHome: options.openclawRuntimeHome, dryRun }));
4700
+ } catch (error) {
4701
+ results.push({ runtime: "openclaw", ok: false, error: String(error?.message ?? error) });
4702
+ }
4703
+ }
4704
+ if (selected.includes("opencode")) {
4705
+ try {
4706
+ results.push(await teardownOpencodeSetup({ homeRoot, opencodeRuntimeHome: options.opencodeRuntimeHome, dryRun }));
4707
+ } catch (error) {
4708
+ results.push({ runtime: "opencode", ok: false, error: String(error?.message ?? error) });
4709
+ }
4710
+ }
4711
+ return { ok: results.every((r) => r.ok), results };
4683
4712
  }
4684
4713
  function commandExists(command) {
4685
4714
  const result = runSystemCommand("which", [command], { env: process.env });
@@ -5034,7 +5063,6 @@ async function runSetupApply(options) {
5034
5063
  const selected = parseSetupRuntimes(options.runtimes);
5035
5064
  const spanoryBin = options.spanoryBin ?? "spanory";
5036
5065
  const dryRun = Boolean(options.dryRun);
5037
- const codexMode = options.codexMode ?? process.env.SPANORY_CODEX_MODE ?? "watch";
5038
5066
  const results = [];
5039
5067
  if (selected.includes("claude-code")) {
5040
5068
  try {
@@ -5049,35 +5077,11 @@ async function runSetupApply(options) {
5049
5077
  }
5050
5078
  }
5051
5079
  if (selected.includes("codex")) {
5052
- if (codexMode === "notify") {
5053
- try {
5054
- const result = await applyCodexSetup({ homeRoot, spanoryBin, dryRun });
5055
- results.push(result);
5056
- } catch (error) {
5057
- results.push({
5058
- runtime: "codex",
5059
- ok: false,
5060
- error: String(error?.message ?? error)
5061
- });
5062
- }
5063
- } else if (codexMode === "watch") {
5064
- try {
5065
- const result = await applyCodexWatchSetup({ homeRoot, dryRun });
5066
- results.push(result);
5067
- } catch (error) {
5068
- results.push({
5069
- runtime: "codex",
5070
- ok: false,
5071
- error: String(error?.message ?? error)
5072
- });
5073
- }
5074
- } else {
5075
- results.push({
5076
- runtime: "codex",
5077
- ok: true,
5078
- skipped: true,
5079
- detail: `codex mode "${codexMode}" has no setup action`
5080
- });
5080
+ try {
5081
+ const result = await applyCodexWatchSetup({ homeRoot, dryRun });
5082
+ results.push(result);
5083
+ } catch (error) {
5084
+ results.push({ runtime: "codex", ok: false, error: String(error?.message ?? error) });
5081
5085
  }
5082
5086
  }
5083
5087
  if (selected.includes("openclaw")) {
@@ -5460,6 +5464,12 @@ setup.command("doctor").description("Run setup diagnostics for selected runtimes
5460
5464
  if (!report2.ok)
5461
5465
  process.exitCode = 2;
5462
5466
  });
5467
+ setup.command("teardown").description("Remove all Spanory integration from local runtimes").option("--runtimes <csv>", `Comma-separated runtimes (default: ${DEFAULT_SETUP_RUNTIMES.join(",")})`, DEFAULT_SETUP_RUNTIMES.join(",")).option("--home <path>", "Home directory root override (default: $HOME)").option("--openclaw-runtime-home <path>", "Override OpenClaw runtime home (default: ~/.openclaw)").option("--opencode-runtime-home <path>", "Override OpenCode runtime home (default: ~/.config/opencode)").option("--dry-run", "Only print planned changes without writing files", false).action(async (options) => {
5468
+ const report2 = await runSetupTeardown(options);
5469
+ console.log(JSON.stringify(report2, null, 2));
5470
+ if (!report2.ok)
5471
+ process.exitCode = 2;
5472
+ });
5463
5473
  program.command("upgrade").description("Upgrade spanory CLI from npm registry").option("--dry-run", "Print upgrade command without executing", false).option("--manager <name>", "Package manager override: npm|tnpm").option("--scope <scope>", "Install scope override: global|local").action(async (options) => {
5464
5474
  const manager = options.manager === "tnpm" ? "tnpm" : detectUpgradePackageManager();
5465
5475
  const scope = options.scope === "local" ? "local" : options.scope === "global" ? "global" : detectUpgradeScope();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bububuger/spanory",
3
- "version": "0.1.21",
3
+ "version": "0.1.26",
4
4
  "description": "Spanory CLI for cross-runtime agent observability",
5
5
  "license": "MIT",
6
6
  "type": "module",