@cairnkeep/cli 1.1.3 → 2.1.0
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/CHANGELOG.md +42 -0
- package/CONTRIBUTING.md +3 -3
- package/README.md +4 -4
- package/docs/operating.md +2 -2
- package/docs/storage.md +25 -0
- package/mcp-memory-server/dist/index.js +251 -8
- package/mcp-memory-server/package.json +8 -4
- package/package.json +3 -3
- package/scripts/doctor.sh +6 -1
- package/scripts/test-doctor.sh +18 -1
- package/scripts/test-runtime-contract.sh +44 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,44 @@
|
|
|
3
3
|
All notable user-facing changes are documented here. This project follows
|
|
4
4
|
[Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [2.1.0] - 2026-07-16
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Add `memory_apply_reviewed` for idempotent application of externally reviewed
|
|
11
|
+
memory revisions with durable provenance and collision-safe history.
|
|
12
|
+
- Add `memory_invalidate_reviewed` for revision-matched retraction and durable
|
|
13
|
+
tombstones that prevent delayed writes from resurrecting invalid memory.
|
|
14
|
+
- Add end-to-end checks for replay, supersession, manual changes, retraction,
|
|
15
|
+
tombstones, and reserved provenance records.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Make history snapshot keys unique when multiple revisions occur in the same
|
|
20
|
+
millisecond.
|
|
21
|
+
|
|
22
|
+
The existing memory tools, database locations, and remote HTTP routing contract
|
|
23
|
+
are unchanged. Existing clients remain compatible and reviewed-memory records
|
|
24
|
+
are created only when an integration explicitly invokes the new tools.
|
|
25
|
+
|
|
26
|
+
## [2.0.0] - 2026-07-15
|
|
27
|
+
|
|
28
|
+
### Breaking
|
|
29
|
+
|
|
30
|
+
- Require Node.js 22 or newer. Node.js 18 and 20 are end-of-life upstream;
|
|
31
|
+
Cairnkeep 1.x remains available for machines that cannot upgrade yet.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- Upgrade the runtime schema dependency to Zod 4 in both package manifests.
|
|
36
|
+
- Build with TypeScript 7 against Node.js 22 type definitions.
|
|
37
|
+
- Exercise Node.js 22, 24, and 26 on Linux, retain real macOS installation
|
|
38
|
+
coverage, and verify the packed package at the Node.js 22 runtime floor.
|
|
39
|
+
|
|
40
|
+
The memory database format, default storage location, remote HTTP protocol,
|
|
41
|
+
and project scaffold format are unchanged. Upgrading does not migrate or delete
|
|
42
|
+
stored memories.
|
|
43
|
+
|
|
6
44
|
## [1.1.3] - 2026-07-14
|
|
7
45
|
|
|
8
46
|
### Fixed
|
|
@@ -51,6 +89,10 @@ All notable user-facing changes are documented here. This project follows
|
|
|
51
89
|
- Preserve executable permissions and Bash 3.2 portability on macOS.
|
|
52
90
|
- Add backup-first uninstall and SQLite-safe memory export/import guidance.
|
|
53
91
|
|
|
92
|
+
[2.1.0]: https://github.com/cairnkeep/cairnkeep/compare/v2.0.0...v2.1.0
|
|
93
|
+
[2.0.0]: https://github.com/cairnkeep/cairnkeep/compare/v1.1.3...v2.0.0
|
|
94
|
+
[1.1.3]: https://github.com/cairnkeep/cairnkeep/compare/v1.1.2...v1.1.3
|
|
95
|
+
[1.1.2]: https://github.com/cairnkeep/cairnkeep/compare/v1.1.1...v1.1.2
|
|
54
96
|
[1.1.1]: https://github.com/cairnkeep/cairnkeep/compare/v1.1.0...v1.1.1
|
|
55
97
|
[1.1.0]: https://github.com/cairnkeep/cairnkeep/compare/v1.0.5...v1.1.0
|
|
56
98
|
[1.0.5]: https://github.com/cairnkeep/cairnkeep/compare/v1.0.4...v1.0.5
|
package/CONTRIBUTING.md
CHANGED
|
@@ -15,9 +15,9 @@ Contributions are welcome through GitHub issues and pull requests.
|
|
|
15
15
|
|
|
16
16
|
## Development setup
|
|
17
17
|
|
|
18
|
-
The memory server requires Node.js
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
The memory server requires Node.js 22 or newer. CI builds and tests on Node.js
|
|
19
|
+
22, 24, and 26, including a clean packed-install boot check at the Node.js 22
|
|
20
|
+
runtime floor.
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
git clone https://github.com/cairnkeep/cairnkeep.git
|
package/README.md
CHANGED
|
@@ -26,9 +26,9 @@ cairnkeep-org sibling project.
|
|
|
26
26
|
|
|
27
27
|
## Compatibility
|
|
28
28
|
|
|
29
|
-
Node.js 22
|
|
30
|
-
1.x
|
|
31
|
-
|
|
29
|
+
Node.js 22, 24, and 26 are supported. Cairnkeep 2.x requires Node.js 22 or
|
|
30
|
+
newer; Cairnkeep 1.x remains available for older runtimes, although Node.js 18
|
|
31
|
+
and 20 are end-of-life upstream.
|
|
32
32
|
|
|
33
33
|
| Client or platform | Support level |
|
|
34
34
|
|---|---|
|
|
@@ -38,7 +38,7 @@ release.
|
|
|
38
38
|
| Other MCP clients | Memory and optional domain-knowledge MCP tools |
|
|
39
39
|
| Native Windows | Not supported by the Bash-based installer; use WSL (not yet CI-verified) |
|
|
40
40
|
|
|
41
|
-
Linux, macOS, Bash 3.2 portability, and
|
|
41
|
+
Linux, macOS, Bash 3.2 portability, and clean Node 22/24/26 runtime checks
|
|
42
42
|
are exercised in CI.
|
|
43
43
|
|
|
44
44
|
## Components
|
package/docs/operating.md
CHANGED
|
@@ -16,8 +16,8 @@ installs. This guide covers all three in order.
|
|
|
16
16
|
|
|
17
17
|
## Prerequisites
|
|
18
18
|
|
|
19
|
-
- Node.js (for the memory server) and a supported harness: Claude
|
|
20
|
-
OpenCode.
|
|
19
|
+
- Node.js 22 or newer (for the memory server) and a supported harness: Claude
|
|
20
|
+
Code or OpenCode.
|
|
21
21
|
- Optional: the `sqlite3` CLI for `cairn memory export`. Runtime memory and
|
|
22
22
|
`cairn memory import` do not require it.
|
|
23
23
|
- Optional: an OpenAI-compatible LLM endpoint for memory extraction and
|
package/docs/storage.md
CHANGED
|
@@ -112,6 +112,31 @@ sqlite3 /path/to/project/.agentfs/project.db \
|
|
|
112
112
|
Treat every database and export archive as sensitive. They may contain source
|
|
113
113
|
paths, decisions, incident details, and other project context.
|
|
114
114
|
|
|
115
|
+
## Reviewed-memory integration
|
|
116
|
+
|
|
117
|
+
External review systems should not compose `memory_write`, `memory_read`, and
|
|
118
|
+
`memory_delete` into their own promotion protocol. That sequence is not atomic
|
|
119
|
+
and can delete a newer manual revision during lifecycle invalidation.
|
|
120
|
+
|
|
121
|
+
Cairnkeep exposes two additive MCP tools for reviewed integrations:
|
|
122
|
+
|
|
123
|
+
- `memory_apply_reviewed` takes `scope`, `review_id`, `key`, and `value`. It is
|
|
124
|
+
idempotent for the same review and content, preserves a displaced live value
|
|
125
|
+
in memory history, and rejects reuse of the review ID with different content.
|
|
126
|
+
- `memory_invalidate_reviewed` takes `scope`, `review_id`, `key`, and an optional
|
|
127
|
+
`reason`. It removes the live value only when it still matches that reviewed
|
|
128
|
+
revision. If apply has not arrived yet, it records a tombstone so a delayed
|
|
129
|
+
apply cannot resurrect invalid memory.
|
|
130
|
+
|
|
131
|
+
Provenance and tombstones use a hidden reserved namespace in the same scoped
|
|
132
|
+
database. Generic memory writes and deletes cannot modify those records. Review
|
|
133
|
+
IDs must be stable, non-secret idempotency identifiers; evidence payloads and
|
|
134
|
+
credentials do not belong in them.
|
|
135
|
+
|
|
136
|
+
These tools are for an explicit, trusted integration. Cairnkeep itself does not
|
|
137
|
+
approve candidates, discover an evidence service, or promote memory
|
|
138
|
+
automatically. Existing clients and the original memory tools remain unchanged.
|
|
139
|
+
|
|
115
140
|
## Automating trusted personal clients
|
|
116
141
|
|
|
117
142
|
Keep fleet-specific values in a private dotfiles repository or secret manager,
|
|
@@ -13,6 +13,12 @@ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/
|
|
|
13
13
|
import { z } from "zod";
|
|
14
14
|
import { EmbeddingCache, cosineSimilarity, embedTexts, getEmbeddingConfig, hashText, } from "./embeddings.js";
|
|
15
15
|
import { computeRepoState, exploreCacheKey, normalizeExploreQuery, readExploreCache, writeExploreCache, } from "./explore-cache.js";
|
|
16
|
+
const MINIMUM_NODE_MAJOR = 22;
|
|
17
|
+
const nodeMajor = Number.parseInt(process.versions.node, 10);
|
|
18
|
+
if (!Number.isInteger(nodeMajor) || nodeMajor < MINIMUM_NODE_MAJOR) {
|
|
19
|
+
process.stderr.write(`cairn-memory requires Node.js ${MINIMUM_NODE_MAJOR} or newer; found ${process.versions.node}\n`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
16
22
|
class ClientContextError extends Error {
|
|
17
23
|
}
|
|
18
24
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -23,6 +29,8 @@ const anythingllmSyncScript = process.env.CAIRN_ANYTHINGLLM_SYNC_SCRIPT
|
|
|
23
29
|
? resolve(expandHome(process.env.CAIRN_ANYTHINGLLM_SYNC_SCRIPT))
|
|
24
30
|
: join(infraRoot, "examples", "anythingllm", "sync_to_anythingllm.py");
|
|
25
31
|
const HISTORY_NAMESPACE = "__history__";
|
|
32
|
+
const REVIEWED_NAMESPACE = "__reviewed__";
|
|
33
|
+
const REVIEW_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
26
34
|
const PROJECT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
27
35
|
const WORKSPACE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
28
36
|
function expandHome(value) {
|
|
@@ -174,17 +182,52 @@ async function openScope(scope, create, options = {}) {
|
|
|
174
182
|
function isHistoryKey(key) {
|
|
175
183
|
return key === HISTORY_NAMESPACE || key.startsWith(`${HISTORY_NAMESPACE}/`);
|
|
176
184
|
}
|
|
185
|
+
function isReviewedKey(key) {
|
|
186
|
+
return key === REVIEWED_NAMESPACE || key.startsWith(`${REVIEWED_NAMESPACE}/`);
|
|
187
|
+
}
|
|
188
|
+
function assertWritableMemoryKey(key) {
|
|
189
|
+
if (isHistoryKey(key) || isReviewedKey(key)) {
|
|
190
|
+
throw new Error(`Keys under ${HISTORY_NAMESPACE}/ and ${REVIEWED_NAMESPACE}/ are reserved.`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
177
193
|
function historyPrefix(baseKey) {
|
|
178
194
|
return `${HISTORY_NAMESPACE}/${baseKey}/`;
|
|
179
195
|
}
|
|
180
196
|
function historySnapshotKey(baseKey, timestamp) {
|
|
181
|
-
return `${historyPrefix(baseKey)}${timestamp}`;
|
|
197
|
+
return `${historyPrefix(baseKey)}${timestamp}-${randomUUID()}`;
|
|
198
|
+
}
|
|
199
|
+
const reviewedRecordSchema = z.object({
|
|
200
|
+
schema_version: z.literal(1),
|
|
201
|
+
review_id: z.string(),
|
|
202
|
+
key: z.string(),
|
|
203
|
+
value_hash: z.string(),
|
|
204
|
+
state: z.enum(["active", "superseded", "invalidated"]),
|
|
205
|
+
applied_at: z.string(),
|
|
206
|
+
superseded_at: z.string().optional(),
|
|
207
|
+
superseded_by: z.string().optional(),
|
|
208
|
+
invalidated_at: z.string().optional(),
|
|
209
|
+
invalidation_reason: z.string().optional(),
|
|
210
|
+
});
|
|
211
|
+
function reviewedRecordKey(reviewId) {
|
|
212
|
+
return `${REVIEWED_NAMESPACE}/${reviewId}`;
|
|
213
|
+
}
|
|
214
|
+
function parseReviewedRecord(value) {
|
|
215
|
+
const parsed = reviewedRecordSchema.safeParse(value);
|
|
216
|
+
if (!parsed.success) {
|
|
217
|
+
throw new Error("Stored reviewed-memory provenance is invalid; refusing to continue.");
|
|
218
|
+
}
|
|
219
|
+
return parsed.data;
|
|
220
|
+
}
|
|
221
|
+
async function inImmediateScopeTransaction(agent, operation) {
|
|
222
|
+
const transaction = agent.getDatabase().transaction(operation);
|
|
223
|
+
const immediate = transaction.immediate;
|
|
224
|
+
return immediate();
|
|
182
225
|
}
|
|
183
226
|
function visibleEntries(entries, includeHistory) {
|
|
184
227
|
if (includeHistory) {
|
|
185
228
|
return entries;
|
|
186
229
|
}
|
|
187
|
-
return entries.filter((entry) => !isHistoryKey(entry.key));
|
|
230
|
+
return entries.filter((entry) => !isHistoryKey(entry.key) && !isReviewedKey(entry.key));
|
|
188
231
|
}
|
|
189
232
|
async function listEntries(scope, prefix = "", options = {}) {
|
|
190
233
|
const agent = await openScope(scope, false, options);
|
|
@@ -753,9 +796,7 @@ function createMemoryServer(context = {}) {
|
|
|
753
796
|
promote_to: z.string().optional(),
|
|
754
797
|
}),
|
|
755
798
|
}, async ({ scope, key, value, promote_to }) => {
|
|
756
|
-
|
|
757
|
-
throw new Error(`Keys under ${HISTORY_NAMESPACE}/ are reserved for memory history.`);
|
|
758
|
-
}
|
|
799
|
+
assertWritableMemoryKey(key);
|
|
759
800
|
const targets = promote_to && promote_to !== scope ? [scope, promote_to] : [scope];
|
|
760
801
|
// Collision-safe: in the unified store, writes from different repos/machines
|
|
761
802
|
// can share a key. If a different value already exists, preserve the old one
|
|
@@ -818,6 +859,9 @@ function createMemoryServer(context = {}) {
|
|
|
818
859
|
key: z.string().min(1),
|
|
819
860
|
}),
|
|
820
861
|
}, async ({ scope, key }) => {
|
|
862
|
+
if (isReviewedKey(key)) {
|
|
863
|
+
throw new Error(`Keys under ${REVIEWED_NAMESPACE}/ are reserved for reviewed-memory provenance.`);
|
|
864
|
+
}
|
|
821
865
|
const agent = await openScope(scope, false, scopeOptions);
|
|
822
866
|
if (agent) {
|
|
823
867
|
try {
|
|
@@ -881,9 +925,7 @@ function createMemoryServer(context = {}) {
|
|
|
881
925
|
reason: z.string().optional(),
|
|
882
926
|
}),
|
|
883
927
|
}, async ({ scope, key, value, reason }) => {
|
|
884
|
-
|
|
885
|
-
throw new Error(`Keys under ${HISTORY_NAMESPACE}/ are reserved for memory history.`);
|
|
886
|
-
}
|
|
928
|
+
assertWritableMemoryKey(key);
|
|
887
929
|
const agent = await openScope(scope, true, scopeOptions);
|
|
888
930
|
if (!agent) {
|
|
889
931
|
throw new Error(`Unable to open scope ${scope}.`);
|
|
@@ -923,6 +965,207 @@ function createMemoryServer(context = {}) {
|
|
|
923
965
|
await agent.close();
|
|
924
966
|
}
|
|
925
967
|
});
|
|
968
|
+
server.registerTool("memory_apply_reviewed", {
|
|
969
|
+
description: "Idempotently apply a human-reviewed memory revision with durable provenance.",
|
|
970
|
+
inputSchema: z.object({
|
|
971
|
+
scope: z.string(),
|
|
972
|
+
review_id: z.string().regex(REVIEW_ID_PATTERN),
|
|
973
|
+
key: z.string().min(1),
|
|
974
|
+
value: z.string(),
|
|
975
|
+
}),
|
|
976
|
+
}, async ({ scope, review_id, key, value }) => {
|
|
977
|
+
assertWritableMemoryKey(key);
|
|
978
|
+
const agent = await openScope(scope, true, scopeOptions);
|
|
979
|
+
if (!agent) {
|
|
980
|
+
throw new Error(`Unable to open scope ${scope}.`);
|
|
981
|
+
}
|
|
982
|
+
try {
|
|
983
|
+
const payload = await inImmediateScopeTransaction(agent, async () => {
|
|
984
|
+
const recordKey = reviewedRecordKey(review_id);
|
|
985
|
+
const valueHash = hashText(value);
|
|
986
|
+
const stored = await agent.kv.get(recordKey);
|
|
987
|
+
if (stored !== undefined) {
|
|
988
|
+
const existing = parseReviewedRecord(stored);
|
|
989
|
+
if (existing.key !== key || existing.value_hash !== valueHash) {
|
|
990
|
+
throw new Error(`Review id ${review_id} was already used with different content.`);
|
|
991
|
+
}
|
|
992
|
+
if (existing.state !== "active") {
|
|
993
|
+
throw new Error(`Review id ${review_id} is ${existing.state} and cannot be reapplied.`);
|
|
994
|
+
}
|
|
995
|
+
return {
|
|
996
|
+
ok: true,
|
|
997
|
+
scope,
|
|
998
|
+
review_id,
|
|
999
|
+
key,
|
|
1000
|
+
applied: false,
|
|
1001
|
+
idempotent: true,
|
|
1002
|
+
snapshot_key: null,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
const appliedAt = new Date().toISOString();
|
|
1006
|
+
const provenance = await agent.kv.list(`${REVIEWED_NAMESPACE}/`);
|
|
1007
|
+
for (const entry of provenance) {
|
|
1008
|
+
const previousRecord = parseReviewedRecord(entry.value);
|
|
1009
|
+
if (previousRecord.state === "active" && previousRecord.key === key) {
|
|
1010
|
+
await agent.kv.set(entry.key, {
|
|
1011
|
+
...previousRecord,
|
|
1012
|
+
state: "superseded",
|
|
1013
|
+
superseded_at: appliedAt,
|
|
1014
|
+
superseded_by: review_id,
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
const previous = await agent.kv.get(key);
|
|
1019
|
+
const previousValue = previous === undefined ? undefined : normalizeValue(previous);
|
|
1020
|
+
let snapshotKey = null;
|
|
1021
|
+
if (previousValue !== undefined && previousValue !== value) {
|
|
1022
|
+
snapshotKey = historySnapshotKey(key, appliedAt);
|
|
1023
|
+
await agent.kv.set(snapshotKey, {
|
|
1024
|
+
value: previousValue,
|
|
1025
|
+
superseded_at: appliedAt,
|
|
1026
|
+
superseded_reason: `reviewed memory ${review_id}`,
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
await agent.kv.set(key, value);
|
|
1030
|
+
await agent.kv.set(recordKey, {
|
|
1031
|
+
schema_version: 1,
|
|
1032
|
+
review_id,
|
|
1033
|
+
key,
|
|
1034
|
+
value_hash: valueHash,
|
|
1035
|
+
state: "active",
|
|
1036
|
+
applied_at: appliedAt,
|
|
1037
|
+
});
|
|
1038
|
+
return {
|
|
1039
|
+
ok: true,
|
|
1040
|
+
scope,
|
|
1041
|
+
review_id,
|
|
1042
|
+
key,
|
|
1043
|
+
applied: true,
|
|
1044
|
+
idempotent: false,
|
|
1045
|
+
snapshot_key: snapshotKey,
|
|
1046
|
+
};
|
|
1047
|
+
});
|
|
1048
|
+
return {
|
|
1049
|
+
content: [{ type: "text", text: asToolText(payload) }],
|
|
1050
|
+
structuredContent: payload,
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
finally {
|
|
1054
|
+
await agent.close();
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
server.registerTool("memory_invalidate_reviewed", {
|
|
1058
|
+
description: "Invalidate a reviewed revision, removing it only when the live value still matches.",
|
|
1059
|
+
inputSchema: z.object({
|
|
1060
|
+
scope: z.string(),
|
|
1061
|
+
review_id: z.string().regex(REVIEW_ID_PATTERN),
|
|
1062
|
+
key: z.string().min(1),
|
|
1063
|
+
reason: z.string().max(512).optional(),
|
|
1064
|
+
}),
|
|
1065
|
+
}, async ({ scope, review_id, key, reason }) => {
|
|
1066
|
+
assertWritableMemoryKey(key);
|
|
1067
|
+
const agent = await openScope(scope, true, scopeOptions);
|
|
1068
|
+
if (!agent) {
|
|
1069
|
+
throw new Error(`Unable to open scope ${scope}.`);
|
|
1070
|
+
}
|
|
1071
|
+
try {
|
|
1072
|
+
const payload = await inImmediateScopeTransaction(agent, async () => {
|
|
1073
|
+
const recordKey = reviewedRecordKey(review_id);
|
|
1074
|
+
const stored = await agent.kv.get(recordKey);
|
|
1075
|
+
if (stored === undefined) {
|
|
1076
|
+
const invalidatedAt = new Date().toISOString();
|
|
1077
|
+
await agent.kv.set(recordKey, {
|
|
1078
|
+
schema_version: 1,
|
|
1079
|
+
review_id,
|
|
1080
|
+
key,
|
|
1081
|
+
value_hash: "",
|
|
1082
|
+
state: "invalidated",
|
|
1083
|
+
applied_at: invalidatedAt,
|
|
1084
|
+
invalidated_at: invalidatedAt,
|
|
1085
|
+
...(reason ? { invalidation_reason: reason } : {}),
|
|
1086
|
+
});
|
|
1087
|
+
return {
|
|
1088
|
+
ok: true,
|
|
1089
|
+
scope,
|
|
1090
|
+
review_id,
|
|
1091
|
+
key,
|
|
1092
|
+
invalidated: true,
|
|
1093
|
+
idempotent: false,
|
|
1094
|
+
missing: true,
|
|
1095
|
+
removed: false,
|
|
1096
|
+
current_changed: false,
|
|
1097
|
+
snapshot_key: null,
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
const record = parseReviewedRecord(stored);
|
|
1101
|
+
if (record.key !== key) {
|
|
1102
|
+
throw new Error(`Review id ${review_id} belongs to a different memory key.`);
|
|
1103
|
+
}
|
|
1104
|
+
if (record.state === "invalidated") {
|
|
1105
|
+
return {
|
|
1106
|
+
ok: true,
|
|
1107
|
+
scope,
|
|
1108
|
+
review_id,
|
|
1109
|
+
key: record.key,
|
|
1110
|
+
invalidated: false,
|
|
1111
|
+
idempotent: true,
|
|
1112
|
+
missing: record.value_hash === "",
|
|
1113
|
+
removed: false,
|
|
1114
|
+
current_changed: false,
|
|
1115
|
+
snapshot_key: null,
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
const invalidatedAt = new Date().toISOString();
|
|
1119
|
+
let currentChanged = false;
|
|
1120
|
+
let removed = false;
|
|
1121
|
+
let snapshotKey = null;
|
|
1122
|
+
if (record.state === "active") {
|
|
1123
|
+
const current = await agent.kv.get(record.key);
|
|
1124
|
+
if (current !== undefined) {
|
|
1125
|
+
const currentValue = normalizeValue(current);
|
|
1126
|
+
if (hashText(currentValue) === record.value_hash) {
|
|
1127
|
+
snapshotKey = historySnapshotKey(record.key, invalidatedAt);
|
|
1128
|
+
await agent.kv.set(snapshotKey, {
|
|
1129
|
+
value: currentValue,
|
|
1130
|
+
superseded_at: invalidatedAt,
|
|
1131
|
+
superseded_reason: reason ?? `reviewed memory ${review_id} invalidated`,
|
|
1132
|
+
});
|
|
1133
|
+
await agent.kv.delete(record.key);
|
|
1134
|
+
removed = true;
|
|
1135
|
+
}
|
|
1136
|
+
else {
|
|
1137
|
+
currentChanged = true;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
await agent.kv.set(recordKey, {
|
|
1142
|
+
...record,
|
|
1143
|
+
state: "invalidated",
|
|
1144
|
+
invalidated_at: invalidatedAt,
|
|
1145
|
+
...(reason ? { invalidation_reason: reason } : {}),
|
|
1146
|
+
});
|
|
1147
|
+
return {
|
|
1148
|
+
ok: true,
|
|
1149
|
+
scope,
|
|
1150
|
+
review_id,
|
|
1151
|
+
key: record.key,
|
|
1152
|
+
invalidated: true,
|
|
1153
|
+
idempotent: false,
|
|
1154
|
+
missing: false,
|
|
1155
|
+
removed,
|
|
1156
|
+
current_changed: currentChanged,
|
|
1157
|
+
snapshot_key: snapshotKey,
|
|
1158
|
+
};
|
|
1159
|
+
});
|
|
1160
|
+
return {
|
|
1161
|
+
content: [{ type: "text", text: asToolText(payload) }],
|
|
1162
|
+
structuredContent: payload,
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
finally {
|
|
1166
|
+
await agent.close();
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
926
1169
|
server.registerTool("memory_history", {
|
|
927
1170
|
description: "Read prior versions of a memory entry from the hidden history namespace.",
|
|
928
1171
|
inputSchema: z.object({
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
"description": "Durable, harness-agnostic memory MCP server for coding agents — the core of Cairnkeep.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22"
|
|
10
|
+
},
|
|
8
11
|
"scripts": {
|
|
9
12
|
"build": "tsc -p tsconfig.json",
|
|
10
13
|
"start": "node dist/index.js",
|
|
@@ -19,8 +22,9 @@
|
|
|
19
22
|
"check:explore-cache": "node scripts/smoke-explore-cache.mjs",
|
|
20
23
|
"check:explore-crossref": "node scripts/smoke-explore-crossref.mjs",
|
|
21
24
|
"check:route-guard": "node scripts/smoke-route-guard.mjs",
|
|
25
|
+
"check:reviewed-memory": "node scripts/smoke-reviewed-memory.mjs",
|
|
22
26
|
"test": "npm run test:smoke",
|
|
23
|
-
"test:smoke": "npm run build && npm run check:embeddings && npm run check:extract && npm run check:scope-guard && npm run check:http-guard && npm run check:remote-context && npm run check:explore-guard && npm run check:explore-cache && npm run check:explore-crossref && npm run check:route-guard",
|
|
27
|
+
"test:smoke": "npm run build && npm run check:embeddings && npm run check:extract && npm run check:scope-guard && npm run check:http-guard && npm run check:remote-context && npm run check:explore-guard && npm run check:explore-cache && npm run check:explore-crossref && npm run check:route-guard && npm run check:reviewed-memory",
|
|
24
28
|
"test:search": "npm run build && npm run check:search",
|
|
25
29
|
"test:memory": "npm run build && npm run check:embeddings && npm run check:extract && npm run check:search",
|
|
26
30
|
"test:all": "npm run test:memory"
|
|
@@ -28,10 +32,10 @@
|
|
|
28
32
|
"dependencies": {
|
|
29
33
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
30
34
|
"agentfs-sdk": "^0.6.4",
|
|
31
|
-
"zod": "^
|
|
35
|
+
"zod": "^4.4.3"
|
|
32
36
|
},
|
|
33
37
|
"devDependencies": {
|
|
34
|
-
"@types/node": "^22.
|
|
35
|
-
"typescript": "^
|
|
38
|
+
"@types/node": "^22.20.1",
|
|
39
|
+
"typescript": "^7.0.2"
|
|
36
40
|
}
|
|
37
41
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cairnkeep/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "A durable, harness-agnostic memory + context layer for coding agents (Claude Code, OpenCode).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
"AGENTS.md"
|
|
29
29
|
],
|
|
30
30
|
"engines": {
|
|
31
|
-
"node": ">=
|
|
31
|
+
"node": ">=22"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
35
35
|
"agentfs-sdk": "^0.6.4",
|
|
36
|
-
"zod": "^
|
|
36
|
+
"zod": "^4.4.3"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build:server": "npm --prefix mcp-memory-server ci --include=dev --global=false --dry-run=false && npm --prefix mcp-memory-server run build --global=false --dry-run=false",
|
package/scripts/doctor.sh
CHANGED
|
@@ -35,7 +35,12 @@ echo "cairn doctor"
|
|
|
35
35
|
server="$CAIRN_ROOT/mcp-memory-server/dist/index.js"
|
|
36
36
|
probe="$CAIRN_ROOT/scripts/probe-memory-server.mjs"
|
|
37
37
|
if ! command -v node >/dev/null 2>&1; then
|
|
38
|
-
fail "Node.js not found (Node.js
|
|
38
|
+
fail "Node.js not found (Node.js 22 or newer is required)"
|
|
39
|
+
elif ! node_major=$(node -p 'Number(process.versions.node.split(".")[0])' 2>/dev/null) ||
|
|
40
|
+
[[ ! "$node_major" =~ ^[0-9]+$ ]]; then
|
|
41
|
+
fail "could not determine the Node.js version (Node.js 22 or newer is required)"
|
|
42
|
+
elif [[ "$node_major" -lt 22 ]]; then
|
|
43
|
+
fail "Node.js $(node --version 2>/dev/null || printf unknown) is unsupported (Node.js 22 or newer is required)"
|
|
39
44
|
elif [[ ! -f "$server" ]]; then
|
|
40
45
|
fail "memory server not built — run: (cd \"$CAIRN_ROOT/mcp-memory-server\" && npm install && npm run build)"
|
|
41
46
|
elif node "$probe" "$server" >/dev/null 2>&1; then
|
package/scripts/test-doctor.sh
CHANGED
|
@@ -17,7 +17,24 @@ proj="$tmp/clean"; mkdir -p "$proj"
|
|
|
17
17
|
( cd "$proj" && "$doctor" ) >"$tmp/out1" 2>&1 || fail "doctor exited non-zero with nothing configured:\n$(cat "$tmp/out1")"
|
|
18
18
|
grep -q "\[SKIP\]" "$tmp/out1" || fail "expected SKIP lines when nothing is configured"
|
|
19
19
|
|
|
20
|
-
# 2.
|
|
20
|
+
# 2. An unsupported runtime is diagnosed before the server probe.
|
|
21
|
+
mkdir -p "$tmp/old-node"
|
|
22
|
+
cat > "$tmp/old-node/node" <<'EOF'
|
|
23
|
+
#!/usr/bin/env bash
|
|
24
|
+
case "${1:-}" in
|
|
25
|
+
-p) echo 20 ;;
|
|
26
|
+
--version) echo v20.19.0 ;;
|
|
27
|
+
*) exit 1 ;;
|
|
28
|
+
esac
|
|
29
|
+
EOF
|
|
30
|
+
chmod 755 "$tmp/old-node/node"
|
|
31
|
+
if ( cd "$proj" && PATH="$tmp/old-node:$PATH" "$doctor" ) >"$tmp/out-old-node" 2>&1; then
|
|
32
|
+
fail "doctor should exit non-zero for an unsupported Node.js runtime:\n$(cat "$tmp/out-old-node")"
|
|
33
|
+
fi
|
|
34
|
+
grep -q "Node.js v20.19.0 is unsupported" "$tmp/out-old-node" ||
|
|
35
|
+
fail "expected a clear unsupported-Node.js diagnostic"
|
|
36
|
+
|
|
37
|
+
# 3. A configured but unreachable endpoint → FAIL + non-zero exit.
|
|
21
38
|
if command -v curl >/dev/null 2>&1; then
|
|
22
39
|
proj2="$tmp/broken"; mkdir -p "$proj2/.ai"
|
|
23
40
|
echo 'CAIRN_LLM_API_URL=http://127.0.0.1:1' > "$proj2/.ai/.env"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
5
|
+
|
|
6
|
+
node --input-type=module - "$ROOT" <<'EOF'
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
const root = process.argv[2];
|
|
11
|
+
const rootPackage = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
12
|
+
const serverPackage = JSON.parse(
|
|
13
|
+
readFileSync(join(root, "mcp-memory-server", "package.json"), "utf8"),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const failures = [];
|
|
17
|
+
if (rootPackage.engines?.node !== ">=22") failures.push("root Node.js engine must be >=22");
|
|
18
|
+
if (serverPackage.engines?.node !== rootPackage.engines?.node) {
|
|
19
|
+
failures.push("root and memory-server Node.js engines differ");
|
|
20
|
+
}
|
|
21
|
+
if (rootPackage.dependencies?.zod !== serverPackage.dependencies?.zod) {
|
|
22
|
+
failures.push("root and memory-server Zod declarations differ");
|
|
23
|
+
}
|
|
24
|
+
if (!rootPackage.dependencies?.zod?.startsWith("^4.")) failures.push("Zod must use major 4");
|
|
25
|
+
if (!serverPackage.devDependencies?.typescript?.startsWith("^7.")) {
|
|
26
|
+
failures.push("TypeScript must use major 7");
|
|
27
|
+
}
|
|
28
|
+
if (!serverPackage.devDependencies?.["@types/node"]?.startsWith("^22.")) {
|
|
29
|
+
failures.push("Node.js types must match the Node.js 22 runtime floor");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (failures.length > 0) {
|
|
33
|
+
for (const failure of failures) console.error(`FAIL: ${failure}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
EOF
|
|
37
|
+
|
|
38
|
+
grep -q 'node: \["22", "24", "26"\]' "$ROOT/.github/workflows/ci.yml"
|
|
39
|
+
if grep -Eq 'node-version: "(18|20)"|node:(18|20)-slim' "$ROOT/.github/workflows/ci.yml"; then
|
|
40
|
+
echo "FAIL: CI still declares an end-of-life Node.js runtime" >&2
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
echo "PASS: runtime and toolchain declarations are aligned"
|