@cleocode/caamp 2026.10.8 → 2026.10.9
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.
- package/dist/atomic.d.ts +184 -0
- package/dist/atomic.js +20 -0
- package/dist/{chunk-2FQWBKEG.js → chunk-2RZGY4XT.js} +54 -166
- package/dist/chunk-2RZGY4XT.js.map +1 -0
- package/dist/{chunk-HDZYZTR2.js → chunk-5INWYMO4.js} +1 -8
- package/dist/{chunk-HDZYZTR2.js.map → chunk-5INWYMO4.js.map} +1 -1
- package/dist/{chunk-ZL6FLO2H.js → chunk-5UAFS46N.js} +9 -5
- package/dist/{chunk-ZL6FLO2H.js.map → chunk-5UAFS46N.js.map} +1 -1
- package/dist/chunk-D6CWC4BX.js +195 -0
- package/dist/chunk-D6CWC4BX.js.map +1 -0
- package/dist/chunk-IYSZL7N7.js +1239 -0
- package/dist/chunk-IYSZL7N7.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/cli.js +8 -6
- package/dist/cli.js.map +1 -1
- package/dist/hooks-HDXLOEX2.js +61 -0
- package/dist/hooks-HDXLOEX2.js.map +1 -0
- package/dist/hooks.d.ts +812 -0
- package/dist/{hooks-FHFZZENR.js → hooks.js} +8 -3
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +5 -916
- package/dist/index.js +15 -10
- package/dist/index.js.map +1 -1
- package/dist/{injector-FDRZD2DB.js → injector-3SMMI2FO.js} +5 -3
- package/dist/injector-3SMMI2FO.js.map +1 -0
- package/package.json +15 -7
- package/providers/hook-mappings.json +6 -2
- package/dist/chunk-2FQWBKEG.js.map +0 -1
- package/dist/chunk-YCAANESR.js +0 -294
- package/dist/chunk-YCAANESR.js.map +0 -1
- /package/dist/{hooks-FHFZZENR.js.map → atomic.js.map} +0 -0
- /package/dist/{injector-FDRZD2DB.js.map → chunk-PZ5AY32C.js.map} +0 -0
package/dist/atomic.d.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { HookConfigObject, HookConfigEdit } from '@cleocode/contracts/project-hook-delivery.js';
|
|
2
|
+
import { WriteFileAtomic } from '@cleocode/contracts/tools/atomic';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Atomic file writes and cross-process file locking.
|
|
6
|
+
*
|
|
7
|
+
* CAAMP mutates files that are shared by *every* project on the machine — most
|
|
8
|
+
* critically `~/.agents/AGENTS.md`, which every `cleo init`, `cleo upgrade` and
|
|
9
|
+
* `cleo doctor` run rewrites regardless of which project it was invoked from.
|
|
10
|
+
* A plain `writeFile` on such a path is `open(O_TRUNC)` followed by one or more
|
|
11
|
+
* `write(2)` calls: two processes interleaving there can leave a caller reading
|
|
12
|
+
* a half-written file, and a reader racing a writer can observe a truncated
|
|
13
|
+
* one.
|
|
14
|
+
*
|
|
15
|
+
* Two primitives remove that class of failure:
|
|
16
|
+
*
|
|
17
|
+
* - `writeFileAtomic` — the canonical tmp-then-rename primitive from
|
|
18
|
+
* `@cleocode/core/tools/fs.js`. `rename(2)` within a filesystem is atomic, so
|
|
19
|
+
* a concurrent reader sees either the whole old file or the whole new one,
|
|
20
|
+
* never a mixture. It is re-exported here for callers already importing this
|
|
21
|
+
* module; the definition lives in core, per the tools-vs-skills boundary.
|
|
22
|
+
* - {@link withFileLock} — serialise a whole read-modify-write cycle across
|
|
23
|
+
* processes via an `O_EXCL` guard file, so two writers cannot both read the
|
|
24
|
+
* pre-state and then clobber each other's result. Generalised from the
|
|
25
|
+
* bespoke copy that lived inline in `lock-utils.ts` (now a caller).
|
|
26
|
+
*
|
|
27
|
+
* @task T12051
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** Alias the canonical primitive through its shared contract, preserving runtime identity. */
|
|
31
|
+
declare const writeFileAtomic: WriteFileAtomic;
|
|
32
|
+
/**
|
|
33
|
+
* Options controlling {@link withFileLock}.
|
|
34
|
+
*
|
|
35
|
+
* @public
|
|
36
|
+
*/
|
|
37
|
+
interface FileLockOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Number of acquisition attempts before throwing.
|
|
40
|
+
*
|
|
41
|
+
* @defaultValue 400
|
|
42
|
+
*/
|
|
43
|
+
retries?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Delay between attempts, in milliseconds.
|
|
46
|
+
*
|
|
47
|
+
* @defaultValue 25
|
|
48
|
+
*/
|
|
49
|
+
delayMs?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Age at which an existing guard file is treated as abandoned and removed.
|
|
52
|
+
*
|
|
53
|
+
* @defaultValue 30000
|
|
54
|
+
*/
|
|
55
|
+
staleMs?: number;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Run `fn` while holding an exclusive cross-process lock on `targetPath`.
|
|
59
|
+
*
|
|
60
|
+
* The lock is a `<targetPath>.lock` guard file created with `O_EXCL`, which is
|
|
61
|
+
* atomic on POSIX and on Windows. A guard left behind by a crashed process is
|
|
62
|
+
* reclaimed once it exceeds `staleMs`.
|
|
63
|
+
*
|
|
64
|
+
* The guard is always released, including when `fn` throws.
|
|
65
|
+
*
|
|
66
|
+
* @param targetPath - Path being protected (the guard is a sibling of it)
|
|
67
|
+
* @param fn - Work to perform while holding the lock
|
|
68
|
+
* @param options - Retry, delay and staleness tuning
|
|
69
|
+
* @returns Whatever `fn` returns
|
|
70
|
+
* @throws Error if the lock cannot be acquired within `retries` attempts
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* const action = await withFileLock(agentsMd, async () => {
|
|
75
|
+
* const before = await readFile(agentsMd, "utf-8");
|
|
76
|
+
* await writeFileAtomic(agentsMd, transform(before));
|
|
77
|
+
* return "updated";
|
|
78
|
+
* });
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* @public
|
|
82
|
+
*/
|
|
83
|
+
declare function withFileLock<T>(targetPath: string, fn: () => Promise<T>, options?: FileLockOptions): Promise<T>;
|
|
84
|
+
/**
|
|
85
|
+
* Throw if a read looks like it landed inside another process's
|
|
86
|
+
* truncate-then-write window.
|
|
87
|
+
*
|
|
88
|
+
* `writeFileAtomic` makes *our* writes indivisible, but callers outside this
|
|
89
|
+
* package still rewrite instruction files with a plain `writeFile`, which is
|
|
90
|
+
* `open(O_TRUNC)` followed by `write(2)`. A read landing between those two
|
|
91
|
+
* returns an empty string for a file that is not empty on disk. Reconciling
|
|
92
|
+
* from that observation would replace every byte of the user's content with a
|
|
93
|
+
* lone CAAMP block.
|
|
94
|
+
*
|
|
95
|
+
* Failing closed is the right trade: the caller retries or reports, and the
|
|
96
|
+
* file is left exactly as it was.
|
|
97
|
+
*
|
|
98
|
+
* @param filePath - Path that was read, for the error message
|
|
99
|
+
* @param content - What the read returned
|
|
100
|
+
* @param sizeOnDisk - `stat().size` for the same path
|
|
101
|
+
* @throws Error when `content` is empty but `sizeOnDisk` is greater than zero
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```typescript
|
|
105
|
+
* const text = await readFile(p, "utf-8");
|
|
106
|
+
* if (text.length === 0) assertNotTornRead(p, text, (await stat(p)).size);
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* @public
|
|
110
|
+
*/
|
|
111
|
+
declare function assertNotTornRead(filePath: string, content: string, sizeOnDisk: number): void;
|
|
112
|
+
/**
|
|
113
|
+
* A JSON config file could not be parsed, so it was left untouched.
|
|
114
|
+
*
|
|
115
|
+
* @remarks
|
|
116
|
+
* Thrown by {@link updateJsonConfigFile} instead of treating the file as empty.
|
|
117
|
+
* The pre-T12385 provider adapters caught the parse error and started from
|
|
118
|
+
* `{}`, so a torn read of `~/.claude/settings.json` (Claude Code rewriting it
|
|
119
|
+
* at the same moment) replaced the user's permissions, env, model and hooks
|
|
120
|
+
* with CLEO's entries alone.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```typescript
|
|
124
|
+
* try {
|
|
125
|
+
* await updateJsonConfigFile(path, (config) => false);
|
|
126
|
+
* } catch (err) {
|
|
127
|
+
* if (err instanceof JsonConfigParseError) console.error(err.filePath);
|
|
128
|
+
* }
|
|
129
|
+
* ```
|
|
130
|
+
*
|
|
131
|
+
* @public
|
|
132
|
+
*/
|
|
133
|
+
declare class JsonConfigParseError extends Error {
|
|
134
|
+
/** Path of the file that failed to parse. */
|
|
135
|
+
readonly filePath: string;
|
|
136
|
+
/**
|
|
137
|
+
* @param filePath - Path of the file that failed to parse
|
|
138
|
+
* @param reason - Parser message
|
|
139
|
+
*/
|
|
140
|
+
constructor(filePath: string, reason: string);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Read-modify-write a JSON object file under the cross-process lock, writing
|
|
144
|
+
* atomically, and never replacing an unparseable file.
|
|
145
|
+
*
|
|
146
|
+
* @remarks
|
|
147
|
+
* Composes the two existing primitives — {@link withFileLock} and core's
|
|
148
|
+
* `writeFileAtomic` — for provider config files such as
|
|
149
|
+
* `~/.claude/settings.json` that the provider itself also rewrites (T12385):
|
|
150
|
+
*
|
|
151
|
+
* - The whole read → mutate → write cycle holds the `<file>.lock` guard, so
|
|
152
|
+
* two CLEO writers cannot both read the pre-state and clobber each other.
|
|
153
|
+
* - The write is tmp-then-rename, so a concurrent reader (including the
|
|
154
|
+
* provider) sees the whole old file or the whole new one.
|
|
155
|
+
* - A missing file starts from `{}`. A file that exists but is empty on read
|
|
156
|
+
* while non-empty on disk, is not valid JSON, or is not a JSON object throws
|
|
157
|
+
* ({@link assertNotTornRead} / {@link JsonConfigParseError}) and the file is
|
|
158
|
+
* left byte-identical.
|
|
159
|
+
*
|
|
160
|
+
* @param filePath - JSON file to update
|
|
161
|
+
* @param mutate - Edits the parsed object in place; returns `true` when it
|
|
162
|
+
* changed something that must be written
|
|
163
|
+
* @returns Whether the file was written
|
|
164
|
+
* @throws {@link JsonConfigParseError} when the existing file cannot be parsed
|
|
165
|
+
* as a JSON object; the file is not modified
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* ```typescript
|
|
169
|
+
* const wrote = await updateJsonConfigFile(settingsPath, (settings) => {
|
|
170
|
+
* if (settings.enabled === true) return false;
|
|
171
|
+
* settings.enabled = true;
|
|
172
|
+
* return true;
|
|
173
|
+
* });
|
|
174
|
+
* ```
|
|
175
|
+
*
|
|
176
|
+
* @public
|
|
177
|
+
*/
|
|
178
|
+
declare function updateJsonConfigFile(filePath: string, mutate: (config: Record<string, unknown>) => boolean): Promise<boolean>;
|
|
179
|
+
/** Apply surgical JSONC edits inside the existing cross-process atomic writer. */
|
|
180
|
+
declare function editJsonConfigFile(filePath: string, edit: (config: HookConfigObject) => HookConfigEdit[]): Promise<boolean>;
|
|
181
|
+
/** Read a bounded JSONC object without mutation, including in dry-run inspections. */
|
|
182
|
+
declare function readManagedJsonConfigFile(filePath: string): Promise<HookConfigObject>;
|
|
183
|
+
|
|
184
|
+
export { type FileLockOptions, JsonConfigParseError, assertNotTornRead, editJsonConfigFile, readManagedJsonConfigFile, updateJsonConfigFile, withFileLock, writeFileAtomic };
|
package/dist/atomic.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JsonConfigParseError,
|
|
3
|
+
assertNotTornRead,
|
|
4
|
+
editJsonConfigFile,
|
|
5
|
+
readManagedJsonConfigFile,
|
|
6
|
+
updateJsonConfigFile,
|
|
7
|
+
withFileLock,
|
|
8
|
+
writeFileAtomic
|
|
9
|
+
} from "./chunk-D6CWC4BX.js";
|
|
10
|
+
import "./chunk-PZ5AY32C.js";
|
|
11
|
+
export {
|
|
12
|
+
JsonConfigParseError,
|
|
13
|
+
assertNotTornRead,
|
|
14
|
+
editJsonConfigFile,
|
|
15
|
+
readManagedJsonConfigFile,
|
|
16
|
+
updateJsonConfigFile,
|
|
17
|
+
withFileLock,
|
|
18
|
+
writeFileAtomic
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=atomic.js.map
|
|
@@ -1,132 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertNotTornRead,
|
|
3
|
+
withFileLock
|
|
4
|
+
} from "./chunk-D6CWC4BX.js";
|
|
1
5
|
import {
|
|
2
6
|
getAgentsHome,
|
|
3
7
|
resolveProviderSkillsDir,
|
|
4
8
|
resolveProvidersRegistryPath,
|
|
5
9
|
resolveRegistryTemplatePath
|
|
6
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-5INWYMO4.js";
|
|
7
11
|
|
|
8
12
|
// src/core/instructions/injector.ts
|
|
9
|
-
import { existsSync as existsSync2 } from "fs";
|
|
10
|
-
import { readFile as readFile3, stat as stat3 } from "fs/promises";
|
|
11
|
-
import { homedir as homedir3 } from "os";
|
|
12
|
-
import { dirname as dirname4, isAbsolute, join as join2 } from "path";
|
|
13
|
-
import { writeFileAtomic as writeFileAtomic2 } from "@cleocode/core/tools/fs.js";
|
|
14
|
-
|
|
15
|
-
// src/core/fs/atomic.ts
|
|
16
13
|
import { existsSync } from "fs";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
14
|
+
import { readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
15
|
+
import { homedir as homedir3 } from "os";
|
|
16
|
+
import { dirname as dirname3, isAbsolute, join as join2 } from "path";
|
|
19
17
|
import { writeFileAtomic } from "@cleocode/core/tools/fs.js";
|
|
20
|
-
var DEFAULT_STALE_LOCK_MS = 3e4;
|
|
21
|
-
var DEFAULT_LOCK_RETRIES = 400;
|
|
22
|
-
var DEFAULT_LOCK_DELAY_MS = 25;
|
|
23
|
-
function sleep(ms) {
|
|
24
|
-
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
25
|
-
}
|
|
26
|
-
async function removeStaleGuard(guardPath, staleMs, expectedToken) {
|
|
27
|
-
try {
|
|
28
|
-
const info = await stat(guardPath);
|
|
29
|
-
if (Date.now() - info.mtimeMs <= staleMs) return false;
|
|
30
|
-
const current = await readFile(guardPath, "utf-8").catch(() => null);
|
|
31
|
-
if (expectedToken !== null && current !== null && current !== expectedToken) return false;
|
|
32
|
-
await rm(guardPath, { force: true });
|
|
33
|
-
return true;
|
|
34
|
-
} catch {
|
|
35
|
-
}
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
async function withFileLock(targetPath, fn, options = {}) {
|
|
39
|
-
const retries = options.retries ?? DEFAULT_LOCK_RETRIES;
|
|
40
|
-
const delayMs = options.delayMs ?? DEFAULT_LOCK_DELAY_MS;
|
|
41
|
-
const staleMs = options.staleMs ?? DEFAULT_STALE_LOCK_MS;
|
|
42
|
-
const guardPath = `${targetPath}.lock`;
|
|
43
|
-
const token = `${process.pid}:${Date.now()}:${Math.random().toString(36).slice(2, 12)}`;
|
|
44
|
-
await mkdir(dirname(targetPath), { recursive: true });
|
|
45
|
-
let acquired = false;
|
|
46
|
-
for (let attempt = 0; attempt < retries && !acquired; attempt += 1) {
|
|
47
|
-
try {
|
|
48
|
-
const handle = await open(guardPath, "wx");
|
|
49
|
-
await handle.close();
|
|
50
|
-
await writeFile(guardPath, token, "utf-8");
|
|
51
|
-
acquired = true;
|
|
52
|
-
} catch (error) {
|
|
53
|
-
const code = error.code;
|
|
54
|
-
if (code !== "EEXIST") throw error;
|
|
55
|
-
const observed = await readFile(guardPath, "utf-8").catch(() => null);
|
|
56
|
-
if (await removeStaleGuard(guardPath, staleMs, observed)) continue;
|
|
57
|
-
await sleep(delayMs);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
if (!acquired) {
|
|
61
|
-
throw new Error(
|
|
62
|
-
`Timed out acquiring lock for ${targetPath} after ${retries} attempts (~${Math.round(retries * delayMs / 1e3)}s)`
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
try {
|
|
66
|
-
return await fn();
|
|
67
|
-
} finally {
|
|
68
|
-
const current = await readFile(guardPath, "utf-8").catch(() => null);
|
|
69
|
-
if (current === null || current === token) {
|
|
70
|
-
await rm(guardPath, { force: true }).catch(() => {
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
function assertNotTornRead(filePath, content, sizeOnDisk) {
|
|
76
|
-
if (content.length === 0 && sizeOnDisk > 0) {
|
|
77
|
-
throw new Error(
|
|
78
|
-
`Refusing to rewrite ${filePath}: read 0 bytes but the file is ${sizeOnDisk} bytes on disk (torn read from a concurrent non-atomic writer).`
|
|
79
|
-
);
|
|
80
|
-
}
|
|
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
|
-
}
|
|
126
18
|
|
|
127
19
|
// src/core/registry/providers.ts
|
|
128
20
|
import { readFileSync } from "fs";
|
|
129
|
-
import { dirname
|
|
21
|
+
import { dirname, join } from "path";
|
|
130
22
|
import { fileURLToPath } from "url";
|
|
131
23
|
var DEFAULT_SKILLS_CAPABILITY = {
|
|
132
24
|
agentsGlobalPath: null,
|
|
@@ -205,7 +97,7 @@ function resolveCapabilities(raw) {
|
|
|
205
97
|
return { mcp, harness, skills, hooks, spawn };
|
|
206
98
|
}
|
|
207
99
|
function findRegistryPath() {
|
|
208
|
-
const thisDir =
|
|
100
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
209
101
|
return resolveProvidersRegistryPath(thisDir);
|
|
210
102
|
}
|
|
211
103
|
var _registry = null;
|
|
@@ -635,18 +527,18 @@ ${content}
|
|
|
635
527
|
${CAAMP_MARKER_END}`;
|
|
636
528
|
}
|
|
637
529
|
function assertBalancedMarkers(content) {
|
|
638
|
-
let
|
|
530
|
+
let open = false;
|
|
639
531
|
const markers = new RegExp(`${CAAMP_MARKER_START}|${CAAMP_MARKER_END}`, "g");
|
|
640
532
|
for (const match of content.matchAll(markers)) {
|
|
641
533
|
const opening = match[0] === CAAMP_MARKER_START;
|
|
642
|
-
if (opening ===
|
|
534
|
+
if (opening === open) {
|
|
643
535
|
throw new Error(
|
|
644
536
|
"Ambiguous CAAMP markers: nested or unmatched delimiter; original content preserved."
|
|
645
537
|
);
|
|
646
538
|
}
|
|
647
|
-
|
|
539
|
+
open = opening;
|
|
648
540
|
}
|
|
649
|
-
if (
|
|
541
|
+
if (open)
|
|
650
542
|
throw new Error(
|
|
651
543
|
"Ambiguous CAAMP markers: opening delimiter has no end; original content preserved."
|
|
652
544
|
);
|
|
@@ -711,9 +603,9 @@ function repairContent(existing) {
|
|
|
711
603
|
|
|
712
604
|
// src/core/instructions/templates.ts
|
|
713
605
|
import { createHash } from "crypto";
|
|
714
|
-
import { readFile
|
|
606
|
+
import { readFile, realpath, stat } from "fs/promises";
|
|
715
607
|
import { homedir as homedir2 } from "os";
|
|
716
|
-
import { dirname as
|
|
608
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
717
609
|
function buildInjectionContent(template) {
|
|
718
610
|
const lines = [];
|
|
719
611
|
for (const ref of template.references) {
|
|
@@ -806,7 +698,7 @@ async function resolveInstructionDelivery(content, baseDir) {
|
|
|
806
698
|
let sourcePath = stamp[1];
|
|
807
699
|
try {
|
|
808
700
|
sourcePath = decodeURIComponent(sourcePath);
|
|
809
|
-
const sourceSize = (await
|
|
701
|
+
const sourceSize = (await stat(sourcePath)).size;
|
|
810
702
|
bytes += sourceSize;
|
|
811
703
|
if (bytes > 524288) {
|
|
812
704
|
result.findings.push({
|
|
@@ -815,7 +707,7 @@ async function resolveInstructionDelivery(content, baseDir) {
|
|
|
815
707
|
reason: "Instruction byte budget exceeded."
|
|
816
708
|
});
|
|
817
709
|
} else {
|
|
818
|
-
const digest = createHash("sha256").update(await
|
|
710
|
+
const digest = createHash("sha256").update(await readFile(sourcePath)).digest("hex");
|
|
819
711
|
if (digest !== stamp[2])
|
|
820
712
|
result.findings.push({
|
|
821
713
|
kind: "stale",
|
|
@@ -862,7 +754,7 @@ async function resolveInstructionDelivery(content, baseDir) {
|
|
|
862
754
|
continue;
|
|
863
755
|
}
|
|
864
756
|
try {
|
|
865
|
-
bytes += (await
|
|
757
|
+
bytes += (await stat(path)).size;
|
|
866
758
|
if (bytes > 524288) {
|
|
867
759
|
result.findings.push({
|
|
868
760
|
kind: "limit",
|
|
@@ -871,13 +763,13 @@ async function resolveInstructionDelivery(content, baseDir) {
|
|
|
871
763
|
});
|
|
872
764
|
continue;
|
|
873
765
|
}
|
|
874
|
-
const source = await
|
|
766
|
+
const source = await readFile(path, "utf8");
|
|
875
767
|
visited.add(path);
|
|
876
768
|
result.sources.push(path);
|
|
877
769
|
lines.push(
|
|
878
770
|
`<!-- CAAMP:SOURCE ${encodeURIComponent(path)} ${createHash("sha256").update(source).digest("hex")} -->`
|
|
879
771
|
);
|
|
880
|
-
lines.push(await expand(source,
|
|
772
|
+
lines.push(await expand(source, dirname2(path), [...ancestors, path]));
|
|
881
773
|
} catch (error) {
|
|
882
774
|
result.findings.push({ kind: "missing-reference", path, reason: String(error) });
|
|
883
775
|
}
|
|
@@ -893,17 +785,17 @@ function parseCaampBlocks(fileContent) {
|
|
|
893
785
|
return parseBlocks(fileContent);
|
|
894
786
|
}
|
|
895
787
|
async function dedupeFile(filePath) {
|
|
896
|
-
if (!
|
|
788
|
+
if (!existsSync(filePath)) {
|
|
897
789
|
return { filePath, removed: 0, kept: 0, modified: false, repaired: 0 };
|
|
898
790
|
}
|
|
899
791
|
return withFileLock(filePath, async () => {
|
|
900
|
-
const original = await
|
|
792
|
+
const original = await readFile2(filePath, "utf-8");
|
|
901
793
|
const { content: healed, repaired } = normalizeMarkers(original);
|
|
902
794
|
assertBalancedMarkers(healed);
|
|
903
795
|
const blocks = parseBlocks(healed);
|
|
904
796
|
if (blocks.length === 0) {
|
|
905
797
|
if (repaired > 0 && healed !== original) {
|
|
906
|
-
await
|
|
798
|
+
await writeFileAtomic({ path: filePath, content: healed });
|
|
907
799
|
return { filePath, removed: 0, kept: 0, modified: true, repaired };
|
|
908
800
|
}
|
|
909
801
|
return { filePath, removed: 0, kept: 0, modified: false, repaired };
|
|
@@ -930,7 +822,7 @@ async function dedupeFile(filePath) {
|
|
|
930
822
|
if (result === original) {
|
|
931
823
|
return { filePath, removed: 0, kept: blocks.length, modified: false, repaired };
|
|
932
824
|
}
|
|
933
|
-
await
|
|
825
|
+
await writeFileAtomic({ path: filePath, content: result });
|
|
934
826
|
return { filePath, removed, kept: keepSet.size, modified: true, repaired };
|
|
935
827
|
});
|
|
936
828
|
}
|
|
@@ -962,12 +854,12 @@ function scopedInstructionPath(provider, projectDir, scope) {
|
|
|
962
854
|
return scope === "global" ? resolveGlobalInstructionPath(provider) : join2(projectDir, provider.instructFile);
|
|
963
855
|
}
|
|
964
856
|
async function repairInstructionFiles(projectDir, providers) {
|
|
965
|
-
const paths = instructionFileCascade(projectDir, providers).filter((p) =>
|
|
857
|
+
const paths = instructionFileCascade(projectDir, providers).filter((p) => existsSync(p));
|
|
966
858
|
const files = [];
|
|
967
859
|
for (const filePath of paths) {
|
|
968
860
|
files.push(
|
|
969
861
|
await withFileLock(filePath, async () => {
|
|
970
|
-
const original = await
|
|
862
|
+
const original = await readFile2(filePath, "utf-8");
|
|
971
863
|
const { content, blocksBefore, repaired } = repairContent(original);
|
|
972
864
|
const removed = Math.max(0, blocksBefore - 1);
|
|
973
865
|
if (removed === 0 && repaired === 0) {
|
|
@@ -976,7 +868,7 @@ async function repairInstructionFiles(projectDir, providers) {
|
|
|
976
868
|
if (content === original) {
|
|
977
869
|
return { filePath, removed: 0, kept: blocksBefore, modified: false, repaired };
|
|
978
870
|
}
|
|
979
|
-
await
|
|
871
|
+
await writeFileAtomic({ path: filePath, content });
|
|
980
872
|
return {
|
|
981
873
|
filePath,
|
|
982
874
|
removed,
|
|
@@ -995,8 +887,8 @@ async function repairInstructionFiles(projectDir, providers) {
|
|
|
995
887
|
};
|
|
996
888
|
}
|
|
997
889
|
async function checkInjection(filePath, expectedContent) {
|
|
998
|
-
if (!
|
|
999
|
-
const raw = await
|
|
890
|
+
if (!existsSync(filePath)) return "missing";
|
|
891
|
+
const raw = await readFile2(filePath, "utf-8");
|
|
1000
892
|
const { content, repaired } = normalizeMarkers(raw);
|
|
1001
893
|
const blocks = parseBlocks(content);
|
|
1002
894
|
if (blocks.length === 0) return "none";
|
|
@@ -1010,18 +902,18 @@ async function inject(filePath, content) {
|
|
|
1010
902
|
const body = content.trim();
|
|
1011
903
|
assertBalancedMarkers(buildBlock(body));
|
|
1012
904
|
return withFileLock(filePath, async () => {
|
|
1013
|
-
if (!
|
|
1014
|
-
await
|
|
905
|
+
if (!existsSync(filePath)) {
|
|
906
|
+
await writeFileAtomic({ path: filePath, content: `${buildBlock(body)}
|
|
1015
907
|
` });
|
|
1016
908
|
return "created";
|
|
1017
909
|
}
|
|
1018
|
-
const existing = await
|
|
910
|
+
const existing = await readFile2(filePath, "utf-8");
|
|
1019
911
|
if (existing.length === 0) {
|
|
1020
|
-
assertNotTornRead(filePath, existing, (await
|
|
912
|
+
assertNotTornRead(filePath, existing, (await stat2(filePath)).size);
|
|
1021
913
|
}
|
|
1022
914
|
const { content: next, blocksBefore, repaired } = reconcile(existing, body);
|
|
1023
915
|
if (next === existing) return "intact";
|
|
1024
|
-
await
|
|
916
|
+
await writeFileAtomic({ path: filePath, content: next });
|
|
1025
917
|
if (blocksBefore === 0) return "added";
|
|
1026
918
|
if (repaired > 0) return "repaired";
|
|
1027
919
|
if (blocksBefore > 1) return "consolidated";
|
|
@@ -1029,18 +921,18 @@ async function inject(filePath, content) {
|
|
|
1029
921
|
});
|
|
1030
922
|
}
|
|
1031
923
|
async function removeInjection(filePath) {
|
|
1032
|
-
if (!
|
|
924
|
+
if (!existsSync(filePath)) return false;
|
|
1033
925
|
return withFileLock(filePath, async () => {
|
|
1034
|
-
const original = await
|
|
926
|
+
const original = await readFile2(filePath, "utf-8");
|
|
1035
927
|
const { content } = normalizeMarkers(original);
|
|
1036
928
|
assertBalancedMarkers(content);
|
|
1037
929
|
if (parseBlocks(content).length === 0) return false;
|
|
1038
930
|
const cleaned = content.replace(blockPattern(), "");
|
|
1039
931
|
if (!cleaned) {
|
|
1040
|
-
const { rm
|
|
1041
|
-
await
|
|
932
|
+
const { rm } = await import("fs/promises");
|
|
933
|
+
await rm(filePath);
|
|
1042
934
|
} else {
|
|
1043
|
-
await
|
|
935
|
+
await writeFileAtomic({ path: filePath, content: cleaned });
|
|
1044
936
|
}
|
|
1045
937
|
return true;
|
|
1046
938
|
});
|
|
@@ -1054,7 +946,7 @@ async function checkAllInjections(providers, projectDir, scope, expectedContent)
|
|
|
1054
946
|
if (checked.has(filePath)) continue;
|
|
1055
947
|
checked.add(filePath);
|
|
1056
948
|
let status = await checkInjection(filePath, expectedContent);
|
|
1057
|
-
const delivery = await resolveInstructionDelivery(`@${filePath}`,
|
|
949
|
+
const delivery = await resolveInstructionDelivery(`@${filePath}`, dirname3(filePath));
|
|
1058
950
|
if (expectedContent !== void 0 && status === "outdated") {
|
|
1059
951
|
delivery.findings.push({
|
|
1060
952
|
kind: "stale",
|
|
@@ -1062,7 +954,7 @@ async function checkAllInjections(providers, projectDir, scope, expectedContent)
|
|
|
1062
954
|
reason: "Managed content differs from the expected version."
|
|
1063
955
|
});
|
|
1064
956
|
}
|
|
1065
|
-
if (
|
|
957
|
+
if (existsSync(filePath) && parseBlocks(await readFile2(filePath, "utf8")).length > 1) {
|
|
1066
958
|
delivery.findings.push({
|
|
1067
959
|
kind: "duplicate",
|
|
1068
960
|
path: filePath,
|
|
@@ -1076,7 +968,7 @@ async function checkAllInjections(providers, projectDir, scope, expectedContent)
|
|
|
1076
968
|
file: filePath,
|
|
1077
969
|
provider: provider.id,
|
|
1078
970
|
status,
|
|
1079
|
-
fileExists:
|
|
971
|
+
fileExists: existsSync(filePath)
|
|
1080
972
|
});
|
|
1081
973
|
}
|
|
1082
974
|
return results;
|
|
@@ -1124,7 +1016,7 @@ async function ensureProviderInstructionFile(providerId, projectDir, options) {
|
|
|
1124
1016
|
throw new HomeInstructionFileError(filePath);
|
|
1125
1017
|
let references = options.references ?? getProviderInstructionReferences(providerId);
|
|
1126
1018
|
let content = options.content;
|
|
1127
|
-
if (options.delivery !== "verified-references" && references.includes("@.cleo/memory-bridge.md") && !
|
|
1019
|
+
if (options.delivery !== "verified-references" && references.includes("@.cleo/memory-bridge.md") && !existsSync(join2(dirname3(filePath), ".cleo", "memory-bridge.md"))) {
|
|
1128
1020
|
references = references.filter((reference) => reference !== "@.cleo/memory-bridge.md");
|
|
1129
1021
|
content = [
|
|
1130
1022
|
...content ?? [],
|
|
@@ -1137,7 +1029,7 @@ async function ensureProviderInstructionFile(providerId, projectDir, options) {
|
|
|
1137
1029
|
};
|
|
1138
1030
|
let injectionContent = buildInjectionContent(template);
|
|
1139
1031
|
if (options.delivery !== "verified-references") {
|
|
1140
|
-
const delivery = await resolveInstructionDelivery(injectionContent,
|
|
1032
|
+
const delivery = await resolveInstructionDelivery(injectionContent, dirname3(filePath));
|
|
1141
1033
|
const failures = delivery.findings.filter((finding) => finding.kind !== "duplicate");
|
|
1142
1034
|
if (failures.length > 0) {
|
|
1143
1035
|
throw new Error(
|
|
@@ -1165,7 +1057,7 @@ async function ensureProviderRuleFile(providerId, projectDir, options) {
|
|
|
1165
1057
|
throw new Error(`Unknown provider: "${providerId}". Check CAAMP provider registry.`);
|
|
1166
1058
|
}
|
|
1167
1059
|
const filePath = join2(projectDir, options.relativePath);
|
|
1168
|
-
if (options.onlyIfExists === true && !
|
|
1060
|
+
if (options.onlyIfExists === true && !existsSync(filePath)) return null;
|
|
1169
1061
|
const references = options.references ?? getProviderInstructionReferences(providerId);
|
|
1170
1062
|
const body = buildInjectionContent({ references }).trim();
|
|
1171
1063
|
const block = buildBlock(body);
|
|
@@ -1179,26 +1071,26 @@ ${block}
|
|
|
1179
1071
|
action,
|
|
1180
1072
|
providerId: provider.id
|
|
1181
1073
|
});
|
|
1182
|
-
if (!
|
|
1074
|
+
if (!existsSync(filePath)) {
|
|
1183
1075
|
if (options.onlyIfExists === true) return null;
|
|
1184
|
-
await
|
|
1076
|
+
await writeFileAtomic({ path: filePath, content: owned ?? `${block}
|
|
1185
1077
|
` });
|
|
1186
1078
|
return result("created");
|
|
1187
1079
|
}
|
|
1188
|
-
const existing = await
|
|
1080
|
+
const existing = await readFile2(filePath, "utf-8");
|
|
1189
1081
|
if (existing.length === 0) {
|
|
1190
|
-
assertNotTornRead(filePath, existing, (await
|
|
1082
|
+
assertNotTornRead(filePath, existing, (await stat2(filePath)).size);
|
|
1191
1083
|
}
|
|
1192
1084
|
if (owned !== null) {
|
|
1193
1085
|
if (existing === owned) return result("intact");
|
|
1194
|
-
await
|
|
1086
|
+
await writeFileAtomic({ path: filePath, content: owned });
|
|
1195
1087
|
return result("updated");
|
|
1196
1088
|
}
|
|
1197
1089
|
const hasBlock = parseBlocks(normalizeMarkers(existing).content).length > 0;
|
|
1198
1090
|
const base = hasBlock ? existing : stripBareReferenceLines(existing, references).trimEnd();
|
|
1199
1091
|
const { content: next, blocksBefore, repaired } = reconcile(base, body, "append");
|
|
1200
1092
|
if (next === existing) return result("intact");
|
|
1201
|
-
await
|
|
1093
|
+
await writeFileAtomic({ path: filePath, content: next });
|
|
1202
1094
|
if (blocksBefore === 0) return result("added");
|
|
1203
1095
|
if (repaired > 0) return result("repaired");
|
|
1204
1096
|
if (blocksBefore > 1) return result("consolidated");
|
|
@@ -1277,7 +1169,7 @@ async function writeAgentFileToAllProviders(providerIds, options) {
|
|
|
1277
1169
|
continue;
|
|
1278
1170
|
}
|
|
1279
1171
|
processed.add(filePath);
|
|
1280
|
-
if (options.skipMissingFolders === true && !
|
|
1172
|
+
if (options.skipMissingFolders === true && !existsSync(folder)) {
|
|
1281
1173
|
continue;
|
|
1282
1174
|
}
|
|
1283
1175
|
const action = await inject(filePath, options.content);
|
|
@@ -1287,10 +1179,6 @@ async function writeAgentFileToAllProviders(providerIds, options) {
|
|
|
1287
1179
|
}
|
|
1288
1180
|
|
|
1289
1181
|
export {
|
|
1290
|
-
withFileLock,
|
|
1291
|
-
assertNotTornRead,
|
|
1292
|
-
JsonConfigParseError,
|
|
1293
|
-
updateJsonConfigFile,
|
|
1294
1182
|
getAllProviders,
|
|
1295
1183
|
getProvider,
|
|
1296
1184
|
resolveAlias,
|
|
@@ -1350,4 +1238,4 @@ export {
|
|
|
1350
1238
|
getProviderAgentFolder,
|
|
1351
1239
|
writeAgentFileToAllProviders
|
|
1352
1240
|
};
|
|
1353
|
-
//# sourceMappingURL=chunk-
|
|
1241
|
+
//# sourceMappingURL=chunk-2RZGY4XT.js.map
|