@cleocode/caamp 2026.9.18 → 2026.9.20

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.
@@ -6,15 +6,17 @@ import {
6
6
  } from "./chunk-HDZYZTR2.js";
7
7
 
8
8
  // src/core/instructions/injector.ts
9
- import { existsSync } from "fs";
9
+ import { existsSync as existsSync2 } from "fs";
10
10
  import { readFile as readFile3, stat as stat3 } from "fs/promises";
11
11
  import { homedir as homedir2 } from "os";
12
- import { dirname as dirname4, join as join2 } from "path";
13
- import { writeFileAtomic } from "@cleocode/core/tools/fs.js";
12
+ import { dirname as dirname4, isAbsolute, join as join2 } from "path";
13
+ import { writeFileAtomic as writeFileAtomic2 } from "@cleocode/core/tools/fs.js";
14
14
 
15
15
  // src/core/fs/atomic.ts
16
+ import { existsSync } from "fs";
16
17
  import { mkdir, open, readFile, rm, stat, writeFile } from "fs/promises";
17
18
  import { dirname } from "path";
19
+ import { writeFileAtomic } from "@cleocode/core/tools/fs.js";
18
20
  var DEFAULT_STALE_LOCK_MS = 3e4;
19
21
  var DEFAULT_LOCK_RETRIES = 400;
20
22
  var DEFAULT_LOCK_DELAY_MS = 25;
@@ -77,6 +79,50 @@ function assertNotTornRead(filePath, content, sizeOnDisk) {
77
79
  );
78
80
  }
79
81
  }
82
+ function isJsonObject(value) {
83
+ return typeof value === "object" && value !== null && !Array.isArray(value);
84
+ }
85
+ var JsonConfigParseError = class extends Error {
86
+ /** Path of the file that failed to parse. */
87
+ filePath;
88
+ /**
89
+ * @param filePath - Path of the file that failed to parse
90
+ * @param reason - Parser message
91
+ */
92
+ constructor(filePath, reason) {
93
+ super(`Refusing to rewrite ${filePath}: it is not a valid JSON object (${reason}).`);
94
+ this.name = "JsonConfigParseError";
95
+ this.filePath = filePath;
96
+ }
97
+ };
98
+ async function updateJsonConfigFile(filePath, mutate) {
99
+ return withFileLock(filePath, async () => {
100
+ let config = {};
101
+ if (existsSync(filePath)) {
102
+ const raw = await readFile(filePath, "utf-8");
103
+ if (raw.length === 0) assertNotTornRead(filePath, raw, (await stat(filePath)).size);
104
+ if (raw.trim().length > 0) {
105
+ let parsed;
106
+ try {
107
+ parsed = JSON.parse(raw);
108
+ } catch (err) {
109
+ throw new JsonConfigParseError(
110
+ filePath,
111
+ err instanceof Error ? err.message : String(err)
112
+ );
113
+ }
114
+ if (!isJsonObject(parsed)) {
115
+ throw new JsonConfigParseError(filePath, "top-level value is not an object");
116
+ }
117
+ config = parsed;
118
+ }
119
+ }
120
+ if (!mutate(config)) return false;
121
+ await writeFileAtomic({ path: filePath, content: `${JSON.stringify(config, null, 2)}
122
+ ` });
123
+ return true;
124
+ });
125
+ }
80
126
 
81
127
  // src/core/registry/providers.ts
82
128
  import { readFileSync } from "fs";
@@ -279,6 +325,11 @@ function getSpawnCapableProviders() {
279
325
  function getProvidersBySpawnCapability(flag) {
280
326
  return getAllProviders().filter((p) => p.capabilities.spawn[flag] === true);
281
327
  }
328
+ function resetRegistry() {
329
+ _registry = null;
330
+ _providers = null;
331
+ _aliasMap = null;
332
+ }
282
333
  function getProviderInstructionReferences(idOrAlias) {
283
334
  const provider = getProvider(idOrAlias);
284
335
  return provider?.instructionReferences ? [...provider.instructionReferences] : [];
@@ -355,8 +406,25 @@ import {
355
406
  CAAMP_DAMAGED_END_PATTERN_SOURCE,
356
407
  CAAMP_DAMAGED_START_PATTERN_SOURCE,
357
408
  CAAMP_MARKER_END,
358
- CAAMP_MARKER_START
409
+ CAAMP_MARKER_START,
410
+ CAAMP_SOURCE_STAMP_PATTERN_SOURCE
359
411
  } from "@cleocode/contracts/caamp-markers";
412
+ var EmbeddedDeliveryDowngradeError = class extends Error {
413
+ /** Stable error code for envelopes and tests. */
414
+ code = "E_CAAMP_EMBEDDED_DOWNGRADE";
415
+ /**
416
+ * @param detail - Which file or block the refused write targeted.
417
+ */
418
+ constructor(detail) {
419
+ super(
420
+ `Refusing to replace an embedded CAAMP delivery with reference-only content (${detail}). Regenerate it from its sources: cleo install-global (global) or cleo upgrade (project).`
421
+ );
422
+ this.name = "EmbeddedDeliveryDowngradeError";
423
+ }
424
+ };
425
+ function isEmbeddedDelivery(body) {
426
+ return new RegExp(CAAMP_SOURCE_STAMP_PATTERN_SOURCE, "m").test(body);
427
+ }
360
428
  function blockPattern() {
361
429
  return new RegExp(CAAMP_BLOCK_PATTERN_SOURCE, "g");
362
430
  }
@@ -417,6 +485,9 @@ function reconcile(existing, desiredContent, insert = "prepend") {
417
485
  const blocks = parseBlocks(healed);
418
486
  const desiredBlock = buildBlock(desiredContent.trim());
419
487
  assertBalancedMarkers(desiredBlock);
488
+ if (!isEmbeddedDelivery(desiredContent) && blocks.some((b) => isEmbeddedDelivery(b.content))) {
489
+ throw new EmbeddedDeliveryDowngradeError("existing block carries CAAMP:SOURCE stamps");
490
+ }
420
491
  if (blocks.length === 0) {
421
492
  if (healed.length === 0) return { content: `${desiredBlock}
422
493
  `, blocksBefore: 0, repaired };
@@ -650,7 +721,7 @@ function parseCaampBlocks(fileContent) {
650
721
  return parseBlocks(fileContent);
651
722
  }
652
723
  async function dedupeFile(filePath) {
653
- if (!existsSync(filePath)) {
724
+ if (!existsSync2(filePath)) {
654
725
  return { filePath, removed: 0, kept: 0, modified: false, repaired: 0 };
655
726
  }
656
727
  return withFileLock(filePath, async () => {
@@ -660,7 +731,7 @@ async function dedupeFile(filePath) {
660
731
  const blocks = parseBlocks(healed);
661
732
  if (blocks.length === 0) {
662
733
  if (repaired > 0 && healed !== original) {
663
- await writeFileAtomic({ path: filePath, content: healed });
734
+ await writeFileAtomic2({ path: filePath, content: healed });
664
735
  return { filePath, removed: 0, kept: 0, modified: true, repaired };
665
736
  }
666
737
  return { filePath, removed: 0, kept: 0, modified: false, repaired };
@@ -687,7 +758,7 @@ async function dedupeFile(filePath) {
687
758
  if (result === original) {
688
759
  return { filePath, removed: 0, kept: blocks.length, modified: false, repaired };
689
760
  }
690
- await writeFileAtomic({ path: filePath, content: result });
761
+ await writeFileAtomic2({ path: filePath, content: result });
691
762
  return { filePath, removed, kept: keepSet.size, modified: true, repaired };
692
763
  });
693
764
  }
@@ -706,12 +777,20 @@ function instructionFileCascade(projectDir, providers) {
706
777
  join2(projectDir, "GEMINI.md")
707
778
  ];
708
779
  for (const provider of providers) {
709
- paths.push(join2(provider.pathGlobal, provider.instructFile));
780
+ const globalPath = resolveGlobalInstructionPath(provider);
781
+ if (globalPath !== null) paths.push(globalPath);
710
782
  }
711
783
  return [...new Set(paths)];
712
784
  }
785
+ function resolveGlobalInstructionPath(provider) {
786
+ if (!provider.pathGlobal || !isAbsolute(provider.pathGlobal)) return null;
787
+ return join2(provider.pathGlobal, provider.instructFile);
788
+ }
789
+ function scopedInstructionPath(provider, projectDir, scope) {
790
+ return scope === "global" ? resolveGlobalInstructionPath(provider) : join2(projectDir, provider.instructFile);
791
+ }
713
792
  async function repairInstructionFiles(projectDir, providers) {
714
- const paths = instructionFileCascade(projectDir, providers).filter((p) => existsSync(p));
793
+ const paths = instructionFileCascade(projectDir, providers).filter((p) => existsSync2(p));
715
794
  const files = [];
716
795
  for (const filePath of paths) {
717
796
  files.push(
@@ -725,7 +804,7 @@ async function repairInstructionFiles(projectDir, providers) {
725
804
  if (content === original) {
726
805
  return { filePath, removed: 0, kept: blocksBefore, modified: false, repaired };
727
806
  }
728
- await writeFileAtomic({ path: filePath, content });
807
+ await writeFileAtomic2({ path: filePath, content });
729
808
  return {
730
809
  filePath,
731
810
  removed,
@@ -744,7 +823,7 @@ async function repairInstructionFiles(projectDir, providers) {
744
823
  };
745
824
  }
746
825
  async function checkInjection(filePath, expectedContent) {
747
- if (!existsSync(filePath)) return "missing";
826
+ if (!existsSync2(filePath)) return "missing";
748
827
  const raw = await readFile3(filePath, "utf-8");
749
828
  const { content, repaired } = normalizeMarkers(raw);
750
829
  const blocks = parseBlocks(content);
@@ -759,8 +838,8 @@ async function inject(filePath, content) {
759
838
  const body = content.trim();
760
839
  assertBalancedMarkers(buildBlock(body));
761
840
  return withFileLock(filePath, async () => {
762
- if (!existsSync(filePath)) {
763
- await writeFileAtomic({ path: filePath, content: `${buildBlock(body)}
841
+ if (!existsSync2(filePath)) {
842
+ await writeFileAtomic2({ path: filePath, content: `${buildBlock(body)}
764
843
  ` });
765
844
  return "created";
766
845
  }
@@ -770,7 +849,7 @@ async function inject(filePath, content) {
770
849
  }
771
850
  const { content: next, blocksBefore, repaired } = reconcile(existing, body);
772
851
  if (next === existing) return "intact";
773
- await writeFileAtomic({ path: filePath, content: next });
852
+ await writeFileAtomic2({ path: filePath, content: next });
774
853
  if (blocksBefore === 0) return "added";
775
854
  if (repaired > 0) return "repaired";
776
855
  if (blocksBefore > 1) return "consolidated";
@@ -778,7 +857,7 @@ async function inject(filePath, content) {
778
857
  });
779
858
  }
780
859
  async function removeInjection(filePath) {
781
- if (!existsSync(filePath)) return false;
860
+ if (!existsSync2(filePath)) return false;
782
861
  return withFileLock(filePath, async () => {
783
862
  const original = await readFile3(filePath, "utf-8");
784
863
  const { content } = normalizeMarkers(original);
@@ -789,7 +868,7 @@ async function removeInjection(filePath) {
789
868
  const { rm: rm2 } = await import("fs/promises");
790
869
  await rm2(filePath);
791
870
  } else {
792
- await writeFileAtomic({ path: filePath, content: cleaned });
871
+ await writeFileAtomic2({ path: filePath, content: cleaned });
793
872
  }
794
873
  return true;
795
874
  });
@@ -798,7 +877,8 @@ async function checkAllInjections(providers, projectDir, scope, expectedContent)
798
877
  const results = [];
799
878
  const checked = /* @__PURE__ */ new Set();
800
879
  for (const provider of providers) {
801
- const filePath = scope === "global" ? join2(provider.pathGlobal, provider.instructFile) : join2(projectDir, provider.instructFile);
880
+ const filePath = scopedInstructionPath(provider, projectDir, scope);
881
+ if (filePath === null) continue;
802
882
  if (checked.has(filePath)) continue;
803
883
  checked.add(filePath);
804
884
  let status = await checkInjection(filePath, expectedContent);
@@ -810,7 +890,7 @@ async function checkAllInjections(providers, projectDir, scope, expectedContent)
810
890
  reason: "Managed content differs from the expected version."
811
891
  });
812
892
  }
813
- if (existsSync(filePath) && parseBlocks(await readFile3(filePath, "utf8")).length > 1) {
893
+ if (existsSync2(filePath) && parseBlocks(await readFile3(filePath, "utf8")).length > 1) {
814
894
  delivery.findings.push({
815
895
  kind: "duplicate",
816
896
  path: filePath,
@@ -824,7 +904,7 @@ async function checkAllInjections(providers, projectDir, scope, expectedContent)
824
904
  file: filePath,
825
905
  provider: provider.id,
826
906
  status,
827
- fileExists: existsSync(filePath)
907
+ fileExists: existsSync2(filePath)
828
908
  });
829
909
  }
830
910
  return results;
@@ -833,7 +913,8 @@ async function injectAll(providers, projectDir, scope, content) {
833
913
  const results = /* @__PURE__ */ new Map();
834
914
  const injected = /* @__PURE__ */ new Set();
835
915
  for (const provider of providers) {
836
- const filePath = scope === "global" ? join2(provider.pathGlobal, provider.instructFile) : join2(projectDir, provider.instructFile);
916
+ const filePath = scopedInstructionPath(provider, projectDir, scope);
917
+ if (filePath === null) continue;
837
918
  if (injected.has(filePath)) continue;
838
919
  injected.add(filePath);
839
920
  const action = await inject(filePath, content);
@@ -847,10 +928,13 @@ async function ensureProviderInstructionFile(providerId, projectDir, options) {
847
928
  throw new Error(`Unknown provider: "${providerId}". Check CAAMP provider registry.`);
848
929
  }
849
930
  const scope = options.scope ?? "project";
850
- const filePath = scope === "global" ? join2(provider.pathGlobal, provider.instructFile) : join2(projectDir, provider.instructFile);
931
+ const filePath = scopedInstructionPath(provider, projectDir, scope);
932
+ if (filePath === null) {
933
+ throw new Error(`Provider "${providerId}" has no global instruction file (T12379).`);
934
+ }
851
935
  let references = options.references ?? getProviderInstructionReferences(providerId);
852
936
  let content = options.content;
853
- if (options.delivery !== "verified-references" && references.includes("@.cleo/memory-bridge.md") && !existsSync(join2(dirname4(filePath), ".cleo", "memory-bridge.md"))) {
937
+ if (options.delivery !== "verified-references" && references.includes("@.cleo/memory-bridge.md") && !existsSync2(join2(dirname4(filePath), ".cleo", "memory-bridge.md"))) {
854
938
  references = references.filter((reference) => reference !== "@.cleo/memory-bridge.md");
855
939
  content = [
856
940
  ...content ?? [],
@@ -880,6 +964,57 @@ async function ensureProviderInstructionFile(providerId, projectDir, options) {
880
964
  providerId: provider.id
881
965
  };
882
966
  }
967
+ function stripBareReferenceLines(content, references) {
968
+ const refs = new Set(references);
969
+ const kept = content.split("\n").filter((line) => !refs.has(line.trim()));
970
+ return kept.join("\n").replace(/\n{3,}/g, "\n\n");
971
+ }
972
+ async function ensureProviderRuleFile(providerId, projectDir, options) {
973
+ const provider = getProvider(providerId);
974
+ if (!provider) {
975
+ throw new Error(`Unknown provider: "${providerId}". Check CAAMP provider registry.`);
976
+ }
977
+ const filePath = join2(projectDir, options.relativePath);
978
+ if (options.onlyIfExists === true && !existsSync2(filePath)) return null;
979
+ const references = options.references ?? getProviderInstructionReferences(providerId);
980
+ const body = buildInjectionContent({ references }).trim();
981
+ const block = buildBlock(body);
982
+ assertBalancedMarkers(block);
983
+ const owned = options.preamble !== void 0 ? `${options.preamble}
984
+ ${block}
985
+ ` : null;
986
+ return withFileLock(filePath, async () => {
987
+ const result = (action) => ({
988
+ filePath,
989
+ action,
990
+ providerId: provider.id
991
+ });
992
+ if (!existsSync2(filePath)) {
993
+ if (options.onlyIfExists === true) return null;
994
+ await writeFileAtomic2({ path: filePath, content: owned ?? `${block}
995
+ ` });
996
+ return result("created");
997
+ }
998
+ const existing = await readFile3(filePath, "utf-8");
999
+ if (existing.length === 0) {
1000
+ assertNotTornRead(filePath, existing, (await stat3(filePath)).size);
1001
+ }
1002
+ if (owned !== null) {
1003
+ if (existing === owned) return result("intact");
1004
+ await writeFileAtomic2({ path: filePath, content: owned });
1005
+ return result("updated");
1006
+ }
1007
+ const hasBlock = parseBlocks(normalizeMarkers(existing).content).length > 0;
1008
+ const base = hasBlock ? existing : stripBareReferenceLines(existing, references).trimEnd();
1009
+ const { content: next, blocksBefore, repaired } = reconcile(base, body, "append");
1010
+ if (next === existing) return result("intact");
1011
+ await writeFileAtomic2({ path: filePath, content: next });
1012
+ if (blocksBefore === 0) return result("added");
1013
+ if (repaired > 0) return result("repaired");
1014
+ if (blocksBefore > 1) return result("consolidated");
1015
+ return result("updated");
1016
+ });
1017
+ }
883
1018
  async function ensureAllProviderInstructionFiles(providerIds, projectDir, options) {
884
1019
  const results = [];
885
1020
  const processed = /* @__PURE__ */ new Set();
@@ -889,7 +1024,8 @@ async function ensureAllProviderInstructionFiles(providerIds, projectDir, option
889
1024
  throw new Error(`Unknown provider: "${providerId}". Check CAAMP provider registry.`);
890
1025
  }
891
1026
  const scope = options.scope ?? "project";
892
- const filePath = scope === "global" ? join2(provider.pathGlobal, provider.instructFile) : join2(projectDir, provider.instructFile);
1027
+ const filePath = scopedInstructionPath(provider, projectDir, scope);
1028
+ if (filePath === null) continue;
893
1029
  if (processed.has(filePath)) continue;
894
1030
  processed.add(filePath);
895
1031
  const references = options.references ?? getProviderInstructionReferences(providerId);
@@ -949,7 +1085,7 @@ async function writeAgentFileToAllProviders(providerIds, options) {
949
1085
  continue;
950
1086
  }
951
1087
  processed.add(filePath);
952
- if (options.skipMissingFolders === true && !existsSync(folder)) {
1088
+ if (options.skipMissingFolders === true && !existsSync2(folder)) {
953
1089
  continue;
954
1090
  }
955
1091
  const action = await inject(filePath, options.content);
@@ -961,6 +1097,8 @@ async function writeAgentFileToAllProviders(providerIds, options) {
961
1097
  export {
962
1098
  withFileLock,
963
1099
  assertNotTornRead,
1100
+ JsonConfigParseError,
1101
+ updateJsonConfigFile,
964
1102
  getAllProviders,
965
1103
  getProvider,
966
1104
  resolveAlias,
@@ -976,12 +1114,15 @@ export {
976
1114
  providerSupports,
977
1115
  getSpawnCapableProviders,
978
1116
  getProvidersBySpawnCapability,
1117
+ resetRegistry,
979
1118
  getProviderInstructionReferences,
980
1119
  getProvidersBySkillsPrecedence,
981
1120
  getEffectiveSkillsPaths,
982
1121
  buildSkillsMap,
983
1122
  getProviderCapabilities,
984
1123
  providerSupportsById,
1124
+ EmbeddedDeliveryDowngradeError,
1125
+ isEmbeddedDelivery,
985
1126
  blockPattern,
986
1127
  normalizeMarkers,
987
1128
  parseBlocks,
@@ -999,6 +1140,7 @@ export {
999
1140
  dedupeFile,
1000
1141
  dedupeFiles,
1001
1142
  instructionFileCascade,
1143
+ resolveGlobalInstructionPath,
1002
1144
  repairInstructionFiles,
1003
1145
  checkInjection,
1004
1146
  inject,
@@ -1006,8 +1148,9 @@ export {
1006
1148
  checkAllInjections,
1007
1149
  injectAll,
1008
1150
  ensureProviderInstructionFile,
1151
+ ensureProviderRuleFile,
1009
1152
  ensureAllProviderInstructionFiles,
1010
1153
  getProviderAgentFolder,
1011
1154
  writeAgentFileToAllProviders
1012
1155
  };
1013
- //# sourceMappingURL=chunk-G3CEXEW2.js.map
1156
+ //# sourceMappingURL=chunk-JELNPPLN.js.map