cairn-ai 0.2.4 → 0.2.5

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,6 +1,6 @@
1
1
  {
2
2
  "name": "cairn",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Token-efficient agent harness with tool readiness for Codex, Claude Code, and Antigravity.",
5
5
  "author": {
6
6
  "name": "WkNam"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cairn-ai",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Token-efficient agent harness with tool readiness for Codex, Claude Code, and Antigravity",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createHash, randomUUID } from "node:crypto";
3
3
  import { spawnSync } from "node:child_process";
4
- import { realpathSync } from "node:fs";
4
+ import { constants as fsConstants, realpathSync } from "node:fs";
5
5
  import {
6
6
  copyFile, cp, lstat, mkdir, mkdtemp, open, readFile, readdir, rename, rm, rmdir, writeFile,
7
7
  } from "node:fs/promises";
@@ -119,17 +119,18 @@ async function uninstall() {
119
119
  const conflicts = [];
120
120
  for (const record of ownership.targets) {
121
121
  const current = await targetDigest(record.path, record.type);
122
- if (current !== record.installedDigest) conflicts.push(record.path);
122
+ if (record.type !== "config" && current !== record.installedDigest) conflicts.push(record.path);
123
123
  }
124
124
  if (conflicts.length > 0) throw new Error(`Refusing to uninstall modified managed artifacts: ${conflicts.join(", ")}`);
125
125
  const transaction = await createTransaction();
126
126
  try {
127
127
  const configRecord = ownership.targets.find((record) => record.type === "config");
128
- if (configRecord) {
128
+ if (configRecord && await exists(configRecord.path)) {
129
+ const snapshot = await readSharedConfigSnapshot(configRecord.path);
129
130
  const staged = join(transaction.stageRoot, "uninstall-config.toml");
130
131
  await mkdir(dirname(staged), { recursive: true });
131
- await writeFile(staged, removeCairnConfig(await readText(configRecord.path)));
132
- await replaceTarget({ ...configRecord, staged }, transaction);
132
+ await writeFile(staged, removeCairnConfig(snapshot.text));
133
+ await replaceTarget({ ...configRecord, staged, configSourceDigest: snapshot.digest }, transaction);
133
134
  }
134
135
  injectFailure("uninstall-after-config");
135
136
  for (const phase of ["antigravity", "claude"]) {
@@ -166,7 +167,7 @@ async function doctor() {
166
167
  checks.push(["installed CLI script", await exists(join(versionedPluginRoot, "scripts", "cairn.mjs"))]);
167
168
  checks.push(["installed plan template", await exists(join(versionedPluginRoot, "templates", "work-plan.md"))]);
168
169
  checks.push(["installed model guidance", await exists(join(versionedPluginRoot, "docs", "model-guidance", "codex.md"))]);
169
- const config = await readText(configPath);
170
+ const config = (await readSharedConfigSnapshot(configPath)).text;
170
171
  checks.push(["config marketplace", splitSections(config).some((section) => section.header === "marketplaces.cairn")]);
171
172
  checks.push(["config plugin enabled", hasSetting(config, 'plugins."cairn@cairn"', "enabled", "true")]);
172
173
  checks.push(["Claude runtime locator", await validRuntimeLocator(claudeRuntimeLocatorPath)]);
@@ -204,7 +205,7 @@ async function ownershipDigestsValid(ownership) {
204
205
 
205
206
  async function stageInstall(stageRoot) {
206
207
  const targets = [];
207
- const add = (id, phase, destination, staged, type = "tree") => targets.push({ id, phase, path: destination, staged, type });
208
+ const add = (id, phase, destination, staged, type = "tree", details = {}) => targets.push({ id, phase, path: destination, staged, type, ...details });
208
209
  const sourceStage = join(stageRoot, "codex-source");
209
210
  const runtimeStage = join(stageRoot, "codex-runtime");
210
211
  await copyPluginCandidate(sourceStage, versionedPluginRoot);
@@ -239,8 +240,9 @@ async function stageInstall(stageRoot) {
239
240
 
240
241
  const configStage = join(stageRoot, "config.toml");
241
242
  const hookStates = await trustedHookStates(runtimeStage);
242
- await writeFile(configStage, updateConfig(await readText(configPath), hookStates));
243
- add("codex-config", "config", configPath, configStage, "config");
243
+ const configSnapshot = await readSharedConfigSnapshot(configPath);
244
+ await writeFile(configStage, updateConfig(configSnapshot.text, hookStates));
245
+ add("codex-config", "config", configPath, configStage, "config", { configSourceDigest: configSnapshot.digest });
244
246
  for (const target of targets) await assertRegularTree(target.staged);
245
247
  return targets;
246
248
  }
@@ -465,6 +467,15 @@ function pidIsAlive(pid) {
465
467
  function delay(ms) { return new Promise((resolvePromise) => setTimeout(resolvePromise, ms)); }
466
468
 
467
469
  async function replaceTarget(target, transaction) {
470
+ let configProjection = null;
471
+ if (target.type === "config") {
472
+ const snapshot = await readSharedConfigSnapshot(target.path);
473
+ configProjection = cairnConfigProjection((await readSharedConfigSnapshot(target.staged)).text);
474
+ if (snapshot.digest !== target.configSourceDigest) {
475
+ await writeFile(target.staged, `${removeCairnConfig(snapshot.text)}${configProjection}`);
476
+ target.configSourceDigest = snapshot.digest;
477
+ }
478
+ }
468
479
  const existed = await exists(target.path);
469
480
  const backup = join(transaction.backupRoot, String(transaction.entries.length));
470
481
  await mkdir(dirname(backup), { recursive: true });
@@ -482,7 +493,30 @@ async function replaceTarget(target, transaction) {
482
493
  };
483
494
  transaction.entries.push(entry);
484
495
  await writeTransactionJournal(transaction);
485
- if (existed) await rename(target.path, backup);
496
+ if (target.type === "config" && process.env.CAIRN_TEST_APPEND_CONFIG_BEFORE_REPLACE) {
497
+ const snapshot = await readSharedConfigSnapshot(target.path);
498
+ await writeFile(target.path, `${snapshot.text}${process.env.CAIRN_TEST_APPEND_CONFIG_BEFORE_REPLACE}`);
499
+ }
500
+ if (target.type === "config" && process.env.CAIRN_TEST_REMOVE_CONFIG_BEFORE_REPLACE === "1") {
501
+ await rm(target.path, { force: true });
502
+ }
503
+ if (target.type === "config") {
504
+ let capturedExists = true;
505
+ try {
506
+ await rename(target.path, backup);
507
+ } catch (error) {
508
+ if (error?.code !== "ENOENT") throw error;
509
+ capturedExists = false;
510
+ }
511
+ if (process.env.CAIRN_TEST_CRASH_AFTER_CONFIG_CAPTURE === "1") process.exit(86);
512
+ const captured = await readSharedConfigSnapshot(backup);
513
+ if (capturedExists) await writeFile(target.staged, `${removeCairnConfig(captured.text)}${configProjection}`);
514
+ else await writeFile(target.staged, configProjection);
515
+ entry.existed = capturedExists;
516
+ entry.previousDigest = capturedExists ? await targetDigest(backup, target.type) : "missing";
517
+ entry.expectedNewDigest = await targetDigest(target.staged, target.type);
518
+ await writeTransactionJournal(transaction);
519
+ } else if (existed) await rename(target.path, backup);
486
520
  await mkdir(dirname(target.path), { recursive: true });
487
521
  await rename(target.staged, target.path);
488
522
  entry.status = "applied";
@@ -632,6 +666,18 @@ async function rollbackEntries(transaction) {
632
666
  const destinationExists = await exists(entry.path);
633
667
  const backupExists = await exists(entry.backup);
634
668
  if (backupExists) {
669
+ if (entry.type === "config" && entry.status === "prepared") {
670
+ const previousConfig = (await readSharedConfigSnapshot(entry.backup)).text;
671
+ if (destinationExists) {
672
+ const currentConfig = (await readSharedConfigSnapshot(entry.path)).text;
673
+ await writeFile(entry.path, `${removeCairnConfig(currentConfig)}${cairnConfigProjection(previousConfig)}`);
674
+ await rm(entry.backup, { force: true });
675
+ } else {
676
+ await mkdir(dirname(entry.path), { recursive: true });
677
+ await rename(entry.backup, entry.path);
678
+ }
679
+ continue;
680
+ }
635
681
  if (!entry.existed) throw new Error(`Cannot recover transaction because an unexpected backup exists: ${entry.path}`);
636
682
  if (await targetDigest(entry.backup, entry.type) !== entry.previousDigest) {
637
683
  throw new Error(`Cannot recover transaction because its durable backup changed: ${entry.path}`);
@@ -640,8 +686,8 @@ async function rollbackEntries(transaction) {
640
686
  const current = await targetDigest(entry.path, entry.type);
641
687
  if (current !== entry.expectedNewDigest) throw new Error(`Cannot recover transaction because destination changed: ${entry.path}`);
642
688
  if (entry.type === "config") {
643
- const currentConfig = await readText(entry.path);
644
- const previousConfig = await readText(entry.backup);
689
+ const currentConfig = (await readSharedConfigSnapshot(entry.path)).text;
690
+ const previousConfig = (await readSharedConfigSnapshot(entry.backup)).text;
645
691
  await writeFile(entry.path, `${removeCairnConfig(currentConfig)}${cairnConfigProjection(previousConfig)}`);
646
692
  await rm(entry.backup, { force: true });
647
693
  continue;
@@ -650,12 +696,14 @@ async function rollbackEntries(transaction) {
650
696
  }
651
697
  await mkdir(dirname(entry.path), { recursive: true });
652
698
  await rename(entry.backup, entry.path);
699
+ } else if (entry.type === "config" && entry.status === "prepared" && !destinationExists) {
700
+ continue;
653
701
  } else if (!entry.existed) {
654
702
  if (destinationExists) {
655
703
  const current = await targetDigest(entry.path, entry.type);
656
704
  if (current !== entry.expectedNewDigest) throw new Error(`Cannot recover new transaction target because it changed: ${entry.path}`);
657
705
  if (entry.type === "config") {
658
- const preserved = removeCairnConfig(await readText(entry.path));
706
+ const preserved = removeCairnConfig((await readSharedConfigSnapshot(entry.path)).text);
659
707
  if (preserved.length > 0) await writeFile(entry.path, preserved);
660
708
  else await rm(entry.path, { force: true });
661
709
  } else await rm(entry.path, { recursive: true, force: true });
@@ -688,7 +736,7 @@ async function assertTargetsReplaceable(targets, ownership) {
688
736
  }
689
737
  const record = records.get(target.id);
690
738
  if (!record) {
691
- if (target.type === "config" && cairnConfigProjection(await readText(target.path)).length === 0) continue;
739
+ if (target.type === "config" && cairnConfigProjection((await readSharedConfigSnapshot(target.path)).text).length === 0) continue;
692
740
  throw new Error(`Refusing to overwrite unmanaged artifact: ${target.path}`);
693
741
  }
694
742
  if (target.id === "codex-runtime" && ownership?.version !== releaseVersion) {
@@ -696,13 +744,14 @@ async function assertTargetsReplaceable(targets, ownership) {
696
744
  }
697
745
  if (record.path !== target.path || record.type !== target.type) throw new Error(`Ownership manifest target mismatch: ${target.id}`);
698
746
  const current = await targetDigest(target.path, target.type);
699
- if (current !== record.installedDigest) throw new Error(`Managed artifact was modified: ${target.path}`);
747
+ if (target.type !== "config" && current !== record.installedDigest) throw new Error(`Managed artifact was modified: ${target.path}`);
700
748
  }
701
749
  }
702
750
 
703
751
  async function assertOwnershipDigests(ownership) {
704
752
  for (const record of ownership.targets) {
705
- if (await targetDigest(record.path, record.type) !== record.installedDigest) {
753
+ const current = await targetDigest(record.path, record.type);
754
+ if (record.type !== "config" && current !== record.installedDigest) {
706
755
  throw new Error(`Managed artifact was modified: ${record.path}`);
707
756
  }
708
757
  }
@@ -806,7 +855,7 @@ function versionedRuntimeRoot(version) {
806
855
  async function anyManagedArtifactExists(targets) {
807
856
  for (const target of targets) {
808
857
  if (!(await exists(target.path))) continue;
809
- if (target.type !== "config" || cairnConfigProjection(await readText(target.path)).length > 0) return true;
858
+ if (target.type !== "config" || cairnConfigProjection((await readSharedConfigSnapshot(target.path)).text).length > 0) return true;
810
859
  }
811
860
  return false;
812
861
  }
@@ -826,7 +875,7 @@ async function adoptLegacy(targets, transaction) {
826
875
  }
827
876
  }
828
877
  if (target.type === "config") {
829
- const projection = cairnConfigProjection(await readText(target.path));
878
+ const projection = cairnConfigProjection((await readSharedConfigSnapshot(target.path)).text);
830
879
  if (!projection.includes("[marketplaces.cairn]") || !projection.includes('[plugins."cairn@cairn"]')) {
831
880
  throw new Error(`Legacy Cairn config cannot be identified: ${target.path}`);
832
881
  }
@@ -923,7 +972,7 @@ async function verifyLegacyMirror(target) {
923
972
 
924
973
  export async function targetDigest(path, type) {
925
974
  if (!(await exists(path))) return "missing";
926
- if (type === "config") return sha(cairnConfigProjection(await readText(path)));
975
+ if (type === "config") return sha(cairnConfigProjection((await readSharedConfigSnapshot(path)).text));
927
976
  const info = await lstat(path);
928
977
  if (info.isSymbolicLink() || (!info.isFile() && !info.isDirectory())) throw new Error(`Managed artifact is not a regular file/tree: ${path}`);
929
978
  if (info.isFile()) return sha(await readFile(path));
@@ -969,19 +1018,60 @@ export function updateConfig(config, hookStates, { marketplacePath = marketplace
969
1018
  }
970
1019
 
971
1020
  export function removeCairnConfig(config) {
972
- return splitSections(config).filter((section) => {
973
- if (section.header === null) return true;
974
- if (section.header === "marketplaces.cairn") return false;
975
- if (section.header === 'plugins."cairn@cairn"') return false;
976
- return !(section.header.startsWith("hooks.state.") && section.header.includes("cairn@cairn:"));
977
- }).map((section) => section.text).join("");
1021
+ return splitSections(config).map((section) => isCairnConfigSection(section)
1022
+ ? splitTrailingTomlTrivia(section.text).trivia
1023
+ : section.text).join("");
978
1024
  }
979
1025
 
980
1026
  function cairnConfigProjection(config) {
981
- return splitSections(config).filter((section) => section.header === "marketplaces.cairn"
1027
+ return splitSections(config).filter(isCairnConfigSection)
1028
+ .map((section) => splitTrailingTomlTrivia(section.text).body).join("");
1029
+ }
1030
+
1031
+ function isCairnConfigSection(section) {
1032
+ return section.header === "marketplaces.cairn"
982
1033
  || section.header === 'plugins."cairn@cairn"'
983
- || (section.header?.startsWith("hooks.state.") && section.header.includes("cairn@cairn:")))
984
- .map((section) => section.text).join("");
1034
+ || (section.header?.startsWith("hooks.state.") && section.header.includes("cairn@cairn:"));
1035
+ }
1036
+
1037
+ function splitTrailingTomlTrivia(text) {
1038
+ const lines = text.split(/(?<=\n)/);
1039
+ let boundary = lines.length;
1040
+ while (boundary > 0) {
1041
+ const line = lines[boundary - 1];
1042
+ if (line.length === 0 || line.trim().length === 0 || line.trimStart().startsWith("#")) boundary -= 1;
1043
+ else break;
1044
+ }
1045
+ return { body: lines.slice(0, boundary).join(""), trivia: lines.slice(boundary).join("") };
1046
+ }
1047
+
1048
+ async function readSharedConfigSnapshot(path) {
1049
+ let pathInfo;
1050
+ try {
1051
+ pathInfo = await lstat(path);
1052
+ } catch (error) {
1053
+ if (error?.code === "ENOENT") return { exists: false, text: "", digest: "missing" };
1054
+ throw error;
1055
+ }
1056
+ if (pathInfo.isSymbolicLink() || !pathInfo.isFile()) throw new Error(`Managed config is not a regular file: ${path}`);
1057
+ let handle;
1058
+ try {
1059
+ const flags = fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0) | (fsConstants.O_NONBLOCK ?? 0);
1060
+ handle = await open(path, flags);
1061
+ } catch (error) {
1062
+ if (["ELOOP", "EFTYPE", "ENXIO"].includes(error?.code)) throw new Error(`Managed config is not a regular file: ${path}`);
1063
+ throw error;
1064
+ }
1065
+ try {
1066
+ const openedInfo = await handle.stat();
1067
+ if (!openedInfo.isFile() || openedInfo.dev !== pathInfo.dev || openedInfo.ino !== pathInfo.ino) {
1068
+ throw new Error(`Managed config changed identity while opening: ${path}`);
1069
+ }
1070
+ const text = await handle.readFile("utf8");
1071
+ return { exists: true, text, digest: sha(text) };
1072
+ } finally {
1073
+ await handle.close();
1074
+ }
985
1075
  }
986
1076
 
987
1077
  export function ensureSetting(config, sectionName, key, value) {