@cleocode/caamp 2026.8.1 → 2026.8.2

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,4 +1,5 @@
1
1
  import {
2
+ getAgentsHome,
2
3
  resolveProviderSkillsDir,
3
4
  resolveProvidersRegistryPath,
4
5
  resolveRegistryTemplatePath
@@ -6,13 +7,80 @@ import {
6
7
 
7
8
  // src/core/instructions/injector.ts
8
9
  import { existsSync } from "fs";
9
- import { mkdir, readFile, writeFile } from "fs/promises";
10
+ import { readFile as readFile2, stat as stat2 } from "fs/promises";
10
11
  import { homedir } from "os";
11
- import { dirname as dirname2, join as join2 } from "path";
12
+ import { join as join2 } from "path";
13
+ import { writeFileAtomic } from "@cleocode/core/tools/fs.js";
14
+
15
+ // src/core/fs/atomic.ts
16
+ import { mkdir, open, readFile, rm, stat, writeFile } from "fs/promises";
17
+ import { dirname } from "path";
18
+ var DEFAULT_STALE_LOCK_MS = 3e4;
19
+ var DEFAULT_LOCK_RETRIES = 400;
20
+ var DEFAULT_LOCK_DELAY_MS = 25;
21
+ function sleep(ms) {
22
+ return new Promise((resolve) => setTimeout(resolve, ms));
23
+ }
24
+ async function removeStaleGuard(guardPath, staleMs, expectedToken) {
25
+ try {
26
+ const info = await stat(guardPath);
27
+ if (Date.now() - info.mtimeMs <= staleMs) return false;
28
+ const current = await readFile(guardPath, "utf-8").catch(() => null);
29
+ if (expectedToken !== null && current !== null && current !== expectedToken) return false;
30
+ await rm(guardPath, { force: true });
31
+ return true;
32
+ } catch {
33
+ }
34
+ return false;
35
+ }
36
+ async function withFileLock(targetPath, fn, options = {}) {
37
+ const retries = options.retries ?? DEFAULT_LOCK_RETRIES;
38
+ const delayMs = options.delayMs ?? DEFAULT_LOCK_DELAY_MS;
39
+ const staleMs = options.staleMs ?? DEFAULT_STALE_LOCK_MS;
40
+ const guardPath = `${targetPath}.lock`;
41
+ const token = `${process.pid}:${Date.now()}:${Math.random().toString(36).slice(2, 12)}`;
42
+ await mkdir(dirname(targetPath), { recursive: true });
43
+ let acquired = false;
44
+ for (let attempt = 0; attempt < retries && !acquired; attempt += 1) {
45
+ try {
46
+ const handle = await open(guardPath, "wx");
47
+ await handle.close();
48
+ await writeFile(guardPath, token, "utf-8");
49
+ acquired = true;
50
+ } catch (error) {
51
+ const code = error.code;
52
+ if (code !== "EEXIST") throw error;
53
+ const observed = await readFile(guardPath, "utf-8").catch(() => null);
54
+ if (await removeStaleGuard(guardPath, staleMs, observed)) continue;
55
+ await sleep(delayMs);
56
+ }
57
+ }
58
+ if (!acquired) {
59
+ throw new Error(
60
+ `Timed out acquiring lock for ${targetPath} after ${retries} attempts (~${Math.round(retries * delayMs / 1e3)}s)`
61
+ );
62
+ }
63
+ try {
64
+ return await fn();
65
+ } finally {
66
+ const current = await readFile(guardPath, "utf-8").catch(() => null);
67
+ if (current === null || current === token) {
68
+ await rm(guardPath, { force: true }).catch(() => {
69
+ });
70
+ }
71
+ }
72
+ }
73
+ function assertNotTornRead(filePath, content, sizeOnDisk) {
74
+ if (content.length === 0 && sizeOnDisk > 0) {
75
+ throw new Error(
76
+ `Refusing to rewrite ${filePath}: read 0 bytes but the file is ${sizeOnDisk} bytes on disk (torn read from a concurrent non-atomic writer).`
77
+ );
78
+ }
79
+ }
12
80
 
13
81
  // src/core/registry/providers.ts
14
82
  import { readFileSync } from "fs";
15
- import { dirname, join } from "path";
83
+ import { dirname as dirname2, join } from "path";
16
84
  import { fileURLToPath } from "url";
17
85
  var DEFAULT_SKILLS_CAPABILITY = {
18
86
  agentsGlobalPath: null,
@@ -91,7 +159,7 @@ function resolveCapabilities(raw) {
91
159
  return { mcp, harness, skills, hooks, spawn };
92
160
  }
93
161
  function findRegistryPath() {
94
- const thisDir = dirname(fileURLToPath(import.meta.url));
162
+ const thisDir = dirname2(fileURLToPath(import.meta.url));
95
163
  return resolveProvidersRegistryPath(thisDir);
96
164
  }
97
165
  var _registry = null;
@@ -281,6 +349,102 @@ function providerSupportsById(idOrAlias, capabilityPath) {
281
349
  return providerSupports(provider, capabilityPath);
282
350
  }
283
351
 
352
+ // src/core/instructions/markers.ts
353
+ import {
354
+ CAAMP_BLOCK_PATTERN_SOURCE,
355
+ CAAMP_DAMAGED_END_PATTERN_SOURCE,
356
+ CAAMP_DAMAGED_START_PATTERN_SOURCE,
357
+ CAAMP_MARKER_END,
358
+ CAAMP_MARKER_START
359
+ } from "@cleocode/contracts/caamp-markers";
360
+ function blockPattern() {
361
+ return new RegExp(CAAMP_BLOCK_PATTERN_SOURCE, "g");
362
+ }
363
+ function normalizeMarkers(content) {
364
+ let repaired = 0;
365
+ const heal = (input, source, canonical) => input.replace(new RegExp(source, "gmi"), (match) => {
366
+ if (match === canonical) return match;
367
+ repaired += 1;
368
+ return canonical;
369
+ });
370
+ let out = heal(content, CAAMP_DAMAGED_START_PATTERN_SOURCE, CAAMP_MARKER_START);
371
+ out = heal(out, CAAMP_DAMAGED_END_PATTERN_SOURCE, CAAMP_MARKER_END);
372
+ return { content: out, repaired };
373
+ }
374
+ function parseBlocks(fileContent) {
375
+ const blocks = [];
376
+ const pattern = blockPattern();
377
+ for (let match = pattern.exec(fileContent); match !== null; match = pattern.exec(fileContent)) {
378
+ const raw = match[0];
379
+ blocks.push({
380
+ raw,
381
+ content: (match[1] ?? "").trim(),
382
+ startIndex: match.index,
383
+ endIndex: match.index + raw.length
384
+ });
385
+ }
386
+ return blocks;
387
+ }
388
+ function buildBlock(content) {
389
+ return `${CAAMP_MARKER_START}
390
+ ${content}
391
+ ${CAAMP_MARKER_END}`;
392
+ }
393
+ function tidy(content) {
394
+ const collapsed = content.replace(/\n{3,}/g, "\n\n").trimEnd();
395
+ return collapsed.length > 0 ? `${collapsed}
396
+ ` : "";
397
+ }
398
+ function reconcile(existing, desiredContent, insert = "prepend") {
399
+ const { content: healed, repaired } = normalizeMarkers(existing);
400
+ const blocks = parseBlocks(healed);
401
+ const desiredBlock = buildBlock(desiredContent.trim());
402
+ if (blocks.length === 0) {
403
+ const body = healed.trim();
404
+ if (body.length === 0) return { content: tidy(desiredBlock), blocksBefore: 0, repaired };
405
+ return {
406
+ content: insert === "append" ? tidy(`${body}
407
+
408
+ ${desiredBlock}`) : tidy(`${desiredBlock}
409
+
410
+ ${body}`),
411
+ blocksBefore: 0,
412
+ repaired
413
+ };
414
+ }
415
+ let out = "";
416
+ let cursor = 0;
417
+ for (const [index, block] of blocks.entries()) {
418
+ out += healed.slice(cursor, block.startIndex);
419
+ cursor = block.endIndex;
420
+ if (index === 0) out += desiredBlock;
421
+ }
422
+ out += healed.slice(cursor);
423
+ return { content: tidy(out), blocksBefore: blocks.length, repaired };
424
+ }
425
+ function mergeBlockBodies(blocks) {
426
+ const seen = /* @__PURE__ */ new Set();
427
+ const lines = [];
428
+ for (const block of blocks) {
429
+ for (const line of block.content.split("\n")) {
430
+ const trimmed = line.trim();
431
+ if (trimmed.length === 0 || seen.has(trimmed)) continue;
432
+ seen.add(trimmed);
433
+ lines.push(trimmed);
434
+ }
435
+ }
436
+ return lines.join("\n");
437
+ }
438
+ function repairContent(existing) {
439
+ const { content: healed, repaired } = normalizeMarkers(existing);
440
+ const blocks = parseBlocks(healed);
441
+ if (blocks.length === 0) {
442
+ return { content: healed, blocksBefore: 0, repaired };
443
+ }
444
+ const merged = reconcile(healed, mergeBlockBodies(blocks));
445
+ return { content: merged.content, blocksBefore: blocks.length, repaired };
446
+ }
447
+
284
448
  // src/core/instructions/templates.ts
285
449
  function buildInjectionContent(template) {
286
450
  const lines = [];
@@ -350,56 +514,51 @@ function groupByInstructFile(providers) {
350
514
  }
351
515
 
352
516
  // src/core/instructions/injector.ts
353
- var MARKER_START = "<!-- CAAMP:START -->";
354
- var MARKER_END = "<!-- CAAMP:END -->";
355
- var MARKER_PATTERN = /<!-- CAAMP:START -->[\s\S]*?<!-- CAAMP:END -->/g;
356
- var MARKER_PATTERN_SINGLE = /<!-- CAAMP:START -->[\s\S]*?<!-- CAAMP:END -->/;
357
517
  function parseCaampBlocks(fileContent) {
358
- const blocks = [];
359
- const pattern = /<!-- CAAMP:START -->([\s\S]*?)<!-- CAAMP:END -->/g;
360
- for (let match = pattern.exec(fileContent); match !== null; match = pattern.exec(fileContent)) {
361
- const raw = match[0];
362
- const innerContent = match[1] ?? "";
363
- blocks.push({
364
- raw,
365
- content: innerContent.trim(),
366
- startIndex: match.index,
367
- endIndex: match.index + raw.length
368
- });
369
- }
370
- return blocks;
518
+ return parseBlocks(fileContent);
371
519
  }
372
520
  async function dedupeFile(filePath) {
373
521
  if (!existsSync(filePath)) {
374
- return { filePath, removed: 0, kept: 0, modified: false };
375
- }
376
- const fileContent = await readFile(filePath, "utf-8");
377
- const blocks = parseCaampBlocks(fileContent);
378
- if (blocks.length === 0) {
379
- return { filePath, removed: 0, kept: 0, modified: false };
380
- }
381
- const lastByContent = /* @__PURE__ */ new Map();
382
- for (const block of blocks) {
383
- lastByContent.set(block.content, block);
384
- }
385
- const keepSet = new Set(lastByContent.values());
386
- const removed = blocks.length - keepSet.size;
387
- if (removed === 0) {
388
- return { filePath, removed: 0, kept: blocks.length, modified: false };
522
+ return { filePath, removed: 0, kept: 0, modified: false, repaired: 0 };
389
523
  }
390
- let result = "";
391
- let cursor = 0;
392
- for (const block of blocks) {
393
- result += fileContent.slice(cursor, block.startIndex);
394
- cursor = block.endIndex;
395
- if (keepSet.has(block)) {
396
- result += block.raw;
524
+ return withFileLock(filePath, async () => {
525
+ const original = await readFile2(filePath, "utf-8");
526
+ const { content: healed, repaired } = normalizeMarkers(original);
527
+ const blocks = parseBlocks(healed);
528
+ if (blocks.length === 0) {
529
+ if (repaired > 0 && healed !== original) {
530
+ await writeFileAtomic({ path: filePath, content: healed });
531
+ return { filePath, removed: 0, kept: 0, modified: true, repaired };
532
+ }
533
+ return { filePath, removed: 0, kept: 0, modified: false, repaired };
397
534
  }
398
- }
399
- result += fileContent.slice(cursor);
400
- result = result.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
401
- await writeFile(filePath, result, "utf-8");
402
- return { filePath, removed, kept: keepSet.size, modified: true };
535
+ const lastByContent = /* @__PURE__ */ new Map();
536
+ for (const block of blocks) {
537
+ lastByContent.set(block.content, block);
538
+ }
539
+ const keepSet = new Set(lastByContent.values());
540
+ const removed = blocks.length - keepSet.size;
541
+ let result = "";
542
+ let cursor = 0;
543
+ for (const block of blocks) {
544
+ result += healed.slice(cursor, block.startIndex);
545
+ cursor = block.endIndex;
546
+ if (keepSet.has(block)) {
547
+ result += block.raw;
548
+ }
549
+ }
550
+ result += healed.slice(cursor);
551
+ result = `${result.replace(/\n{3,}/g, "\n\n").trimEnd()}
552
+ `;
553
+ if (removed === 0 && repaired === 0) {
554
+ return { filePath, removed: 0, kept: blocks.length, modified: false, repaired };
555
+ }
556
+ if (result === original) {
557
+ return { filePath, removed: 0, kept: blocks.length, modified: false, repaired };
558
+ }
559
+ await writeFileAtomic({ path: filePath, content: result });
560
+ return { filePath, removed, kept: keepSet.size, modified: true, repaired };
561
+ });
403
562
  }
404
563
  async function dedupeFiles(filePaths) {
405
564
  const results = [];
@@ -408,76 +567,102 @@ async function dedupeFiles(filePaths) {
408
567
  }
409
568
  return results;
410
569
  }
570
+ function instructionFileCascade(projectDir, providers) {
571
+ const paths = [
572
+ join2(getAgentsHome(), "AGENTS.md"),
573
+ join2(projectDir, "AGENTS.md"),
574
+ join2(projectDir, "CLAUDE.md"),
575
+ join2(projectDir, "GEMINI.md")
576
+ ];
577
+ for (const provider of providers) {
578
+ paths.push(join2(provider.pathGlobal, provider.instructFile));
579
+ }
580
+ return [...new Set(paths)];
581
+ }
582
+ async function repairInstructionFiles(projectDir, providers) {
583
+ const paths = instructionFileCascade(projectDir, providers).filter((p) => existsSync(p));
584
+ const files = [];
585
+ for (const filePath of paths) {
586
+ files.push(
587
+ await withFileLock(filePath, async () => {
588
+ const original = await readFile2(filePath, "utf-8");
589
+ const { content, blocksBefore, repaired } = repairContent(original);
590
+ const removed = Math.max(0, blocksBefore - 1);
591
+ if (removed === 0 && repaired === 0) {
592
+ return { filePath, removed: 0, kept: blocksBefore, modified: false, repaired: 0 };
593
+ }
594
+ if (content === original) {
595
+ return { filePath, removed: 0, kept: blocksBefore, modified: false, repaired };
596
+ }
597
+ await writeFileAtomic({ path: filePath, content });
598
+ return {
599
+ filePath,
600
+ removed,
601
+ kept: blocksBefore > 0 ? 1 : 0,
602
+ modified: true,
603
+ repaired
604
+ };
605
+ })
606
+ );
607
+ }
608
+ return {
609
+ files,
610
+ repaired: files.reduce((n, r) => n + r.repaired, 0),
611
+ removed: files.reduce((n, r) => n + r.removed, 0),
612
+ filesModified: files.filter((r) => r.modified).length
613
+ };
614
+ }
411
615
  async function checkInjection(filePath, expectedContent) {
412
616
  if (!existsSync(filePath)) return "missing";
413
- const content = await readFile(filePath, "utf-8");
414
- if (!MARKER_PATTERN_SINGLE.test(content)) return "none";
617
+ const raw = await readFile2(filePath, "utf-8");
618
+ const { content, repaired } = normalizeMarkers(raw);
619
+ const blocks = parseBlocks(content);
620
+ if (blocks.length === 0) return "none";
621
+ if (blocks.length > 1 || repaired > 0) return "outdated";
415
622
  if (expectedContent) {
416
- const blockContent = extractBlock(content);
417
- if (blockContent && blockContent.trim() === expectedContent.trim()) {
418
- return "current";
419
- }
420
- return "outdated";
623
+ return blocks[0]?.content === expectedContent.trim() ? "current" : "outdated";
421
624
  }
422
625
  return "current";
423
626
  }
424
- function extractBlock(content) {
425
- const match = content.match(MARKER_PATTERN_SINGLE);
426
- if (!match) return null;
427
- return match[0].replace(MARKER_START, "").replace(MARKER_END, "").trim();
428
- }
429
- function buildBlock(content) {
430
- return `${MARKER_START}
431
- ${content}
432
- ${MARKER_END}`;
433
- }
434
627
  async function inject(filePath, content) {
435
- const block = buildBlock(content);
436
- await mkdir(dirname2(filePath), { recursive: true });
628
+ const body = content.trim();
437
629
  if (!existsSync(filePath)) {
438
- await writeFile(filePath, `${block}
439
- `, "utf-8");
440
- return "created";
441
- }
442
- const existing = await readFile(filePath, "utf-8");
443
- const matches = existing.match(MARKER_PATTERN);
444
- if (matches && matches.length > 0) {
445
- if (matches.length > 1) {
446
- const updated3 = existing.replace(MARKER_PATTERN, "").replace(/^\n{2,}/, "\n").trim();
447
- const finalContent = updated3 ? `${block}
448
-
449
- ${updated3}` : `${block}
450
- `;
451
- await writeFile(filePath, finalContent, "utf-8");
452
- return "consolidated";
453
- }
454
- const existingBlock = extractBlock(existing);
455
- if (existingBlock !== null && existingBlock.trim() === content.trim()) {
456
- return "intact";
630
+ return withFileLock(filePath, async () => {
631
+ await writeFileAtomic({ path: filePath, content: `${buildBlock(body)}
632
+ ` });
633
+ return "created";
634
+ });
635
+ }
636
+ return withFileLock(filePath, async () => {
637
+ const existing = await readFile2(filePath, "utf-8");
638
+ if (existing.length === 0) {
639
+ assertNotTornRead(filePath, existing, (await stat2(filePath)).size);
457
640
  }
458
- const updated2 = existing.replace(MARKER_PATTERN_SINGLE, block);
459
- await writeFile(filePath, updated2, "utf-8");
641
+ const { content: next, blocksBefore, repaired } = reconcile(existing, body);
642
+ if (next === existing) return "intact";
643
+ await writeFileAtomic({ path: filePath, content: next });
644
+ if (blocksBefore === 0) return "added";
645
+ if (repaired > 0) return "repaired";
646
+ if (blocksBefore > 1) return "consolidated";
460
647
  return "updated";
461
- }
462
- const updated = `${block}
463
-
464
- ${existing}`;
465
- await writeFile(filePath, updated, "utf-8");
466
- return "added";
648
+ });
467
649
  }
468
650
  async function removeInjection(filePath) {
469
651
  if (!existsSync(filePath)) return false;
470
- const content = await readFile(filePath, "utf-8");
471
- if (!MARKER_PATTERN.test(content)) return false;
472
- const cleaned = content.replace(MARKER_PATTERN, "").replace(/^\n{2,}/, "\n").trim();
473
- if (!cleaned) {
474
- const { rm } = await import("fs/promises");
475
- await rm(filePath);
476
- } else {
477
- await writeFile(filePath, `${cleaned}
478
- `, "utf-8");
479
- }
480
- return true;
652
+ return withFileLock(filePath, async () => {
653
+ const original = await readFile2(filePath, "utf-8");
654
+ const { content } = normalizeMarkers(original);
655
+ if (parseBlocks(content).length === 0) return false;
656
+ const cleaned = content.replace(blockPattern(), "").replace(/^\n{2,}/, "\n").trim();
657
+ if (!cleaned) {
658
+ const { rm: rm2 } = await import("fs/promises");
659
+ await rm2(filePath);
660
+ } else {
661
+ await writeFileAtomic({ path: filePath, content: `${cleaned}
662
+ ` });
663
+ }
664
+ return true;
665
+ });
481
666
  }
482
667
  async function checkAllInjections(providers, projectDir, scope, expectedContent) {
483
668
  const results = [];
@@ -608,6 +793,8 @@ async function writeAgentFileToAllProviders(providerIds, options) {
608
793
  }
609
794
 
610
795
  export {
796
+ withFileLock,
797
+ assertNotTornRead,
611
798
  getAllProviders,
612
799
  getProvider,
613
800
  resolveAlias,
@@ -629,6 +816,13 @@ export {
629
816
  buildSkillsMap,
630
817
  getProviderCapabilities,
631
818
  providerSupportsById,
819
+ blockPattern,
820
+ normalizeMarkers,
821
+ parseBlocks,
822
+ buildBlock,
823
+ reconcile,
824
+ mergeBlockBodies,
825
+ repairContent,
632
826
  buildInjectionContent,
633
827
  parseInjectionContent,
634
828
  generateInjectionContent,
@@ -637,6 +831,8 @@ export {
637
831
  parseCaampBlocks,
638
832
  dedupeFile,
639
833
  dedupeFiles,
834
+ instructionFileCascade,
835
+ repairInstructionFiles,
640
836
  checkInjection,
641
837
  inject,
642
838
  removeInjection,
@@ -647,4 +843,4 @@ export {
647
843
  getProviderAgentFolder,
648
844
  writeAgentFileToAllProviders
649
845
  };
650
- //# sourceMappingURL=chunk-EH5U4PRC.js.map
846
+ //# sourceMappingURL=chunk-3IQUGSIV.js.map