@bububuger/spanory 0.1.20 → 0.1.21

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 +182 -20
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4542,6 +4542,14 @@ ${notifyLine}
4542
4542
  function escapeTomlBasicString(value) {
4543
4543
  return String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"');
4544
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
+ }
4545
4553
  function codexNotifyScriptContent({ spanoryBin, codexHome, exportDir, logFile }) {
4546
4554
  return `#!/usr/bin/env bash
4547
4555
  set -euo pipefail
@@ -4629,6 +4637,50 @@ async function applyCodexSetup({ homeRoot, spanoryBin, dryRun }) {
4629
4637
  configBackup
4630
4638
  };
4631
4639
  }
4640
+ async function applyCodexWatchSetup({ homeRoot, dryRun }) {
4641
+ const codexHome = path8.join(homeRoot, ".codex");
4642
+ const scriptPath = path8.join(codexHome, "bin", "spanory-codex-notify.sh");
4643
+ const configPath = path8.join(codexHome, "config.toml");
4644
+ let currentConfig = "";
4645
+ try {
4646
+ currentConfig = await readFile8(configPath, "utf-8");
4647
+ } catch (error) {
4648
+ if (error?.code !== "ENOENT")
4649
+ throw error;
4650
+ }
4651
+ const nextConfig = stripSpanoryCodexNotifyConfig(currentConfig);
4652
+ const configChanged = currentConfig !== nextConfig;
4653
+ let scriptPresent = false;
4654
+ try {
4655
+ await stat4(scriptPath);
4656
+ scriptPresent = true;
4657
+ } catch {
4658
+ }
4659
+ const changed = configChanged || scriptPresent;
4660
+ let configBackup = null;
4661
+ if (changed && !dryRun) {
4662
+ if (configChanged) {
4663
+ await mkdir4(path8.dirname(configPath), { recursive: true });
4664
+ configBackup = await backupIfExists(configPath);
4665
+ await writeFile4(configPath, nextConfig, "utf-8");
4666
+ }
4667
+ if (scriptPresent) {
4668
+ await rm(scriptPath, { force: true });
4669
+ }
4670
+ }
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
+ };
4683
+ }
4632
4684
  function commandExists(command) {
4633
4685
  const result = runSystemCommand("which", [command], { env: process.env });
4634
4686
  return result.code === 0;
@@ -4671,7 +4723,102 @@ function openclawRuntimeHomeForSetup(homeRoot, explicitRuntimeHome) {
4671
4723
  function opencodeRuntimeHomeForSetup(homeRoot, explicitRuntimeHome) {
4672
4724
  return explicitRuntimeHome || path8.join(homeRoot, ".config", "opencode");
4673
4725
  }
4674
- function installOpenclawPlugin(runtimeHome) {
4726
+ function resolveOpenclawConfigPath(runtimeHome) {
4727
+ return path8.join(runtimeHome, "openclaw.json");
4728
+ }
4729
+ function canonicalPath(input) {
4730
+ const resolved = path8.resolve(String(input ?? ""));
4731
+ try {
4732
+ return realpathSync(resolved);
4733
+ } catch {
4734
+ return resolved;
4735
+ }
4736
+ }
4737
+ function readOpenclawPluginNameFromDir(dirPath) {
4738
+ const pkgPath = path8.join(dirPath, "package.json");
4739
+ if (!existsSync(pkgPath))
4740
+ return null;
4741
+ try {
4742
+ const parsed = JSON.parse(readFileSync(pkgPath, "utf-8"));
4743
+ const name = String(parsed?.name ?? "").trim();
4744
+ return name || null;
4745
+ } catch {
4746
+ return null;
4747
+ }
4748
+ }
4749
+ function isSpanoryOpenclawPluginPath(candidatePath) {
4750
+ const text = String(candidatePath ?? "").trim();
4751
+ if (!text)
4752
+ return false;
4753
+ const normalized = text.replaceAll("\\", "/").toLowerCase();
4754
+ if (normalized.includes("spanory") && normalized.includes("openclaw-plugin")) {
4755
+ return true;
4756
+ }
4757
+ const canonical = canonicalPath(text);
4758
+ const pluginName = readOpenclawPluginNameFromDir(canonical);
4759
+ return pluginName === "@bububuger/spanory-openclaw-plugin" || pluginName === "@alipay/spanory-openclaw-plugin";
4760
+ }
4761
+ async function normalizeOpenclawPluginLoadPaths(runtimeHome, pluginDir, dryRun) {
4762
+ const configPath = resolveOpenclawConfigPath(runtimeHome);
4763
+ let configRaw = "";
4764
+ try {
4765
+ configRaw = await readFile8(configPath, "utf-8");
4766
+ } catch (error) {
4767
+ if (error?.code === "ENOENT")
4768
+ return { changed: false, configPath, backup: null };
4769
+ throw error;
4770
+ }
4771
+ let parsed;
4772
+ try {
4773
+ parsed = JSON.parse(configRaw);
4774
+ } catch (error) {
4775
+ throw new Error(`failed to parse openclaw config ${configPath}: ${error?.message ?? String(error)}`);
4776
+ }
4777
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
4778
+ parsed = {};
4779
+ }
4780
+ if (!parsed.plugins || typeof parsed.plugins !== "object" || Array.isArray(parsed.plugins)) {
4781
+ parsed.plugins = {};
4782
+ }
4783
+ if (!parsed.plugins.load || typeof parsed.plugins.load !== "object" || Array.isArray(parsed.plugins.load)) {
4784
+ parsed.plugins.load = {};
4785
+ }
4786
+ const target = path8.resolve(pluginDir);
4787
+ const targetCanonical = canonicalPath(target);
4788
+ const currentPaths = Array.isArray(parsed.plugins.load.paths) ? parsed.plugins.load.paths : [];
4789
+ const nonSpanoryPaths = [];
4790
+ const seen = /* @__PURE__ */ new Set();
4791
+ for (const item of currentPaths) {
4792
+ if (typeof item !== "string")
4793
+ continue;
4794
+ const text = item.trim();
4795
+ if (!text)
4796
+ continue;
4797
+ if (isSpanoryOpenclawPluginPath(text))
4798
+ continue;
4799
+ const key = canonicalPath(text);
4800
+ if (seen.has(key))
4801
+ continue;
4802
+ seen.add(key);
4803
+ nonSpanoryPaths.push(text);
4804
+ }
4805
+ if (!seen.has(targetCanonical)) {
4806
+ nonSpanoryPaths.push(target);
4807
+ }
4808
+ parsed.plugins.load.paths = nonSpanoryPaths;
4809
+ const nextRaw = `${JSON.stringify(parsed, null, 2)}
4810
+ `;
4811
+ if (nextRaw === configRaw) {
4812
+ return { changed: false, configPath, backup: null };
4813
+ }
4814
+ let backup = null;
4815
+ if (!dryRun) {
4816
+ backup = await backupIfExists(configPath);
4817
+ await writeFile4(configPath, nextRaw, "utf-8");
4818
+ }
4819
+ return { changed: true, configPath, backup };
4820
+ }
4821
+ async function installOpenclawPlugin(runtimeHome, dryRun) {
4675
4822
  const pluginDir = path8.resolve(resolveOpenclawPluginDir());
4676
4823
  const installResult = runSystemCommand("openclaw", ["plugins", "install", "-l", pluginDir], {
4677
4824
  env: {
@@ -4691,9 +4838,12 @@ function installOpenclawPlugin(runtimeHome) {
4691
4838
  if (enableResult.code !== 0) {
4692
4839
  throw new Error(enableResult.stderr || enableResult.error || "openclaw plugins enable failed");
4693
4840
  }
4841
+ const pathNormalizeResult = await normalizeOpenclawPluginLoadPaths(runtimeHome, pluginDir, dryRun);
4694
4842
  return {
4843
+ pluginDir,
4695
4844
  installStdout: installResult.stdout.trim(),
4696
- enableStdout: enableResult.stdout.trim()
4845
+ enableStdout: enableResult.stdout.trim(),
4846
+ pathNormalizeResult
4697
4847
  };
4698
4848
  }
4699
4849
  async function installOpencodePlugin(runtimeHome, pluginDirOverride) {
@@ -4749,7 +4899,7 @@ async function runSetupDetect(options) {
4749
4899
  let codexNotifyConfigured = false;
4750
4900
  try {
4751
4901
  const config = await readFile8(codexConfigPath, "utf-8");
4752
- codexNotifyConfigured = /notify\s*=\s*\[[^\]]*spanory-codex-notify\.sh[^\]]*\]/m.test(config);
4902
+ codexNotifyConfigured = isSpanoryCodexNotifyConfigured(config);
4753
4903
  } catch {
4754
4904
  }
4755
4905
  let codexScriptPresent = true;
@@ -4761,10 +4911,11 @@ async function runSetupDetect(options) {
4761
4911
  report2.runtimes.push({
4762
4912
  runtime: "codex",
4763
4913
  available: commandExists("codex"),
4764
- configured: codexNotifyConfigured && codexScriptPresent,
4914
+ configured: !codexNotifyConfigured,
4765
4915
  details: {
4766
4916
  configPath: codexConfigPath,
4767
4917
  scriptPath: codexScriptPath,
4918
+ mode: "watch",
4768
4919
  notifyConfigured: codexNotifyConfigured,
4769
4920
  scriptPresent: codexScriptPresent
4770
4921
  }
@@ -4822,7 +4973,7 @@ async function runSetupDoctor(options) {
4822
4973
  let notifyConfigured = false;
4823
4974
  try {
4824
4975
  const config = await readFile8(configPath, "utf-8");
4825
- notifyConfigured = /notify\s*=\s*\[[^\]]*spanory-codex-notify\.sh[^\]]*\]/m.test(config);
4976
+ notifyConfigured = isSpanoryCodexNotifyConfigured(config);
4826
4977
  } catch {
4827
4978
  }
4828
4979
  let scriptPresent = false;
@@ -4832,15 +4983,15 @@ async function runSetupDoctor(options) {
4832
4983
  } catch {
4833
4984
  }
4834
4985
  checks.push({
4835
- id: "codex_notify_configured",
4986
+ id: "codex_watch_mode",
4836
4987
  runtime: "codex",
4837
- ok: notifyConfigured,
4988
+ ok: !notifyConfigured,
4838
4989
  detail: configPath
4839
4990
  });
4840
4991
  checks.push({
4841
- id: "codex_notify_script",
4992
+ id: "codex_notify_script_absent",
4842
4993
  runtime: "codex",
4843
- ok: scriptPresent,
4994
+ ok: !scriptPresent,
4844
4995
  detail: scriptPath
4845
4996
  });
4846
4997
  }
@@ -4883,7 +5034,7 @@ async function runSetupApply(options) {
4883
5034
  const selected = parseSetupRuntimes(options.runtimes);
4884
5035
  const spanoryBin = options.spanoryBin ?? "spanory";
4885
5036
  const dryRun = Boolean(options.dryRun);
4886
- const codexMode = options.codexMode ?? "notify";
5037
+ const codexMode = options.codexMode ?? process.env.SPANORY_CODEX_MODE ?? "watch";
4887
5038
  const results = [];
4888
5039
  if (selected.includes("claude-code")) {
4889
5040
  try {
@@ -4898,14 +5049,7 @@ async function runSetupApply(options) {
4898
5049
  }
4899
5050
  }
4900
5051
  if (selected.includes("codex")) {
4901
- if (codexMode !== "notify") {
4902
- results.push({
4903
- runtime: "codex",
4904
- ok: true,
4905
- skipped: true,
4906
- detail: `codex mode "${codexMode}" skips notify setup`
4907
- });
4908
- } else {
5052
+ if (codexMode === "notify") {
4909
5053
  try {
4910
5054
  const result = await applyCodexSetup({ homeRoot, spanoryBin, dryRun });
4911
5055
  results.push(result);
@@ -4916,6 +5060,24 @@ async function runSetupApply(options) {
4916
5060
  error: String(error?.message ?? error)
4917
5061
  });
4918
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
+ });
4919
5081
  }
4920
5082
  }
4921
5083
  if (selected.includes("openclaw")) {
@@ -4930,7 +5092,7 @@ async function runSetupApply(options) {
4930
5092
  const runtimeHome = openclawRuntimeHomeForSetup(homeRoot, options.openclawRuntimeHome);
4931
5093
  try {
4932
5094
  if (!dryRun)
4933
- installOpenclawPlugin(runtimeHome);
5095
+ await installOpenclawPlugin(runtimeHome, dryRun);
4934
5096
  const doctor = await runOpenclawPluginDoctor(runtimeHome);
4935
5097
  results.push({
4936
5098
  runtime: "openclaw",
@@ -5286,7 +5448,7 @@ setup.command("detect").description("Detect local runtime availability and setup
5286
5448
  const report2 = await runSetupDetect(options);
5287
5449
  console.log(JSON.stringify(report2, null, 2));
5288
5450
  });
5289
- setup.command("apply").description("Apply idempotent local setup for selected 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("--spanory-bin <path>", "Spanory binary/command to write into runtime configs", "spanory").option("--codex-mode <mode>", "Codex setup mode: notify | proxy (default: notify)", "notify").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) => {
5451
+ setup.command("apply").description("Apply idempotent local setup for selected 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("--spanory-bin <path>", "Spanory binary/command to write into runtime configs", "spanory").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) => {
5290
5452
  const report2 = await runSetupApply(options);
5291
5453
  console.log(JSON.stringify(report2, null, 2));
5292
5454
  if (!report2.ok)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bububuger/spanory",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "Spanory CLI for cross-runtime agent observability",
5
5
  "license": "MIT",
6
6
  "type": "module",