@cairnkeep/cli 2.21.0 → 2.21.1

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 CHANGED
@@ -5,6 +5,22 @@ All notable user-facing changes are documented here. This project follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [2.21.1] - 2026-10-07
9
+
10
+ ### Fixed
11
+
12
+ - Classify embedding health failures by sanitized HTTP status so operators can
13
+ distinguish authorization, missing endpoint/model, throttling, transport,
14
+ and upstream failures without exposing provider response bodies, request
15
+ identifiers, or credentials.
16
+
17
+ ### CI
18
+
19
+ - Split all 48 memory-server smoke contracts exactly once across three
20
+ deterministically balanced native-Windows jobs while retaining Windows CLI,
21
+ package, global-install, Node 22/24/26, and full Linux matrix coverage. This
22
+ removes the serial 15-minute suite duplication from every Windows lane.
23
+
8
24
  ## [2.21.0] - 2026-10-07
9
25
 
10
26
  ### Added
@@ -1,6 +1,6 @@
1
1
  # Curriculum Coverage Map
2
2
 
3
- **Baseline:** Cairnkeep 2.21.0
3
+ **Baseline:** Cairnkeep 2.21.1
4
4
  **Last reviewed:** 2026-10-06
5
5
 
6
6
  This map prevents the course from drifting behind the product. It assigns each
@@ -1,6 +1,6 @@
1
1
  # Cairnkeep feature guide
2
2
 
3
- **Baseline:** Cairnkeep 2.21.0
3
+ **Baseline:** Cairnkeep 2.21.1
4
4
 
5
5
  This is the step-back map for choosing and operating Cairnkeep features. The
6
6
  [course labs](https://github.com/cairnkeep/cairnkeep-course-labs) provide one
@@ -3,7 +3,7 @@
3
3
  **Status:** Ready
4
4
  **Track:** Quickstart
5
5
  **Time:** 10 minutes
6
- **Tested with:** Cairnkeep 2.21.0
6
+ **Tested with:** Cairnkeep 2.21.1
7
7
 
8
8
  ## Outcome
9
9
 
@@ -3,7 +3,7 @@
3
3
  **Status:** Ready
4
4
  **Track:** Quickstart
5
5
  **Time:** 15 minutes
6
- **Tested with:** Cairnkeep 2.21.0
6
+ **Tested with:** Cairnkeep 2.21.1
7
7
 
8
8
  ## Outcome
9
9
 
@@ -3,7 +3,7 @@
3
3
  **Status:** Ready
4
4
  **Track:** Quickstart
5
5
  **Time:** 25 minutes
6
- **Tested with:** Cairnkeep 2.21.0 and Node.js 22 or newer
6
+ **Tested with:** Cairnkeep 2.21.1 and Node.js 22 or newer
7
7
 
8
8
  ## Outcome
9
9
 
@@ -3,7 +3,7 @@
3
3
  **Status:** Ready
4
4
  **Track:** Quickstart
5
5
  **Time:** 25 minutes
6
- **Tested with:** Cairnkeep 2.21.0
6
+ **Tested with:** Cairnkeep 2.21.1
7
7
 
8
8
  ## Outcome
9
9
 
@@ -3,7 +3,7 @@
3
3
  **Status:** Ready
4
4
  **Track:** Evidence and Evaluation
5
5
  **Time:** 50 minutes
6
- **Tested with:** Cairnkeep 2.21.0
6
+ **Tested with:** Cairnkeep 2.21.1
7
7
 
8
8
  ## Outcome
9
9
 
@@ -1,7 +1,7 @@
1
1
  # L20 - Least-authority MCP tool profiles
2
2
 
3
3
  **Status:** Ready
4
- **Tested with:** Cairnkeep 2.21.0 and Node.js 22 or newer
4
+ **Tested with:** Cairnkeep 2.21.1 and Node.js 22 or newer
5
5
  **Time:** 25 minutes
6
6
 
7
7
  ## Outcome
@@ -1,7 +1,7 @@
1
1
  # L21 - Immutable context packs and skill approval
2
2
 
3
3
  **Status:** Ready
4
- **Tested with:** Cairnkeep 2.21.0 and Node.js 22 or newer
4
+ **Tested with:** Cairnkeep 2.21.1 and Node.js 22 or newer
5
5
  **Time:** 40 minutes
6
6
 
7
7
  ## Outcome
@@ -3,7 +3,7 @@
3
3
  **Status:** Ready
4
4
  **Track:** Operator
5
5
  **Time:** 30 minutes
6
- **Tested with:** Cairnkeep 2.21.0, Windows x64, and Node.js 22 or newer
6
+ **Tested with:** Cairnkeep 2.21.1, Windows x64, and Node.js 22 or newer
7
7
 
8
8
  ## Outcome
9
9
 
@@ -3,7 +3,7 @@
3
3
  **Status:** Ready
4
4
  **Track:** Quickstart and Operator
5
5
  **Time:** 35 minutes
6
- **Tested with:** Cairnkeep 2.21.0; Pi 0.84.1; Node.js 22, 24, and 26
6
+ **Tested with:** Cairnkeep 2.21.1; Pi 0.84.1; Node.js 22, 24, and 26
7
7
 
8
8
  ## Outcome
9
9
 
@@ -1,7 +1,7 @@
1
1
  # L24 - Reviewed knowledge exchange
2
2
 
3
3
  **Status:** Ready
4
- **Tested with:** Cairnkeep 2.21.0 and Node.js 22 or newer
4
+ **Tested with:** Cairnkeep 2.21.1 and Node.js 22 or newer
5
5
  **Time:** 35 minutes
6
6
 
7
7
  ## Outcome
@@ -1,7 +1,7 @@
1
1
  # L25 - Bounded workflow playbooks
2
2
 
3
3
  **Status:** Ready
4
- **Tested with:** Cairnkeep 2.21.0 and Node.js 22 or newer
4
+ **Tested with:** Cairnkeep 2.21.1 and Node.js 22 or newer
5
5
  **Time:** 35 minutes
6
6
 
7
7
  ## Outcome
@@ -1,7 +1,7 @@
1
1
  # L26 - Context intelligence with explicit authority
2
2
 
3
3
  **Status:** Ready
4
- **Tested with:** Cairnkeep 2.21.0 and Node.js 22 or newer
4
+ **Tested with:** Cairnkeep 2.21.1 and Node.js 22 or newer
5
5
 
6
6
  ## Outcome
7
7
 
@@ -1,6 +1,6 @@
1
1
  # L27 - Evaluate context companions without moving authority
2
2
 
3
- **Tested with:** Cairnkeep 2.21.0 and Node.js 22 or newer
3
+ **Tested with:** Cairnkeep 2.21.1 and Node.js 22 or newer
4
4
 
5
5
  ## Outcome
6
6
 
@@ -1,7 +1,7 @@
1
1
  # L28 - Verify Cairnkeep's security posture
2
2
 
3
3
  **Status:** Ready
4
- **Tested with:** Cairnkeep 2.21.0 and Node.js 22 or newer
4
+ **Tested with:** Cairnkeep 2.21.1 and Node.js 22 or newer
5
5
 
6
6
  ## Outcome
7
7
 
@@ -11,7 +11,7 @@ and choose least authority for a network-facing MCP client.
11
11
 
12
12
  ## Prerequisites
13
13
 
14
- - Install Cairnkeep 2.21.0 or use a disposable source checkout.
14
+ - Install Cairnkeep 2.21.1 or use a disposable source checkout.
15
15
  - Complete [L20 - Least-authority MCP tool profiles](L20-mcp-tool-profiles.md).
16
16
  - Use synthetic values only; never paste a real bearer token into a recording,
17
17
  terminal transcript, or issue.
package/docs/operating.md CHANGED
@@ -1049,6 +1049,10 @@ then checks `./.ai/.env` (or the current environment). It does not inspect a
1049
1049
  harness's remote HTTP registration. Unconfigured optional dependencies are
1050
1050
  skipped; it exits non-zero when the local server probe fails or a configured
1051
1051
  dependency (LLM/embedding endpoint, writable store) is unreachable.
1052
+ Embedding probes report only a bounded classification and HTTP status, such as
1053
+ `authorization rejected (HTTP 403)` or `endpoint or model not found (HTTP
1054
+ 404)`. Provider response bodies and request identifiers are discarded so a
1055
+ health check cannot echo sensitive upstream diagnostics.
1052
1056
 
1053
1057
  It also checks an existing project-local trajectory database for SQLite
1054
1058
  integrity, schema compatibility and index consistency. Because capture is
package/docs/releasing.md CHANGED
@@ -29,6 +29,13 @@ from a workstation.
29
29
  9. Verify both image digests, SBOM attestations, provenance attestations, and an
30
30
  anonymous pull before announcing the release.
31
31
 
32
+ The native-Windows lanes retain stable check names for branch protection. Each
33
+ lane exercises the Windows CLI and packed global install on its Node version,
34
+ while the complete memory-server smoke catalog is deterministically balanced
35
+ across the three lanes. The Linux Node 22/24/26 matrix independently runs the
36
+ complete catalog on every supported runtime. This preserves coverage while
37
+ removing three serial copies of the slow Windows suite from the critical path.
38
+
32
39
  Stable versions must use a stable GitHub Release and publish to npm's `latest`
33
40
  tag. SemVer prerelease versions must use a GitHub prerelease and publish to
34
41
  `next`. A mismatch fails before publication.
@@ -86,6 +86,10 @@ punctuation.
86
86
  `cairn doctor` remains the operational dependency/store check. Run both before
87
87
  exposing HTTP, after changing tool authority, and before a security-sensitive
88
88
  release.
89
+ Its embedding probe retains only a sanitized failure class and HTTP status;
90
+ provider response bodies and request identifiers are never emitted. This makes
91
+ expired credentials distinguishable from a missing model without turning the
92
+ doctor output into a secondary secret or telemetry channel.
89
93
 
90
94
  ## Repository and release controls
91
95
 
@@ -120,3 +124,7 @@ The maintained gates are `npm run security:baseline`, both production
120
124
  security-assurance and HTTP-guard smoke tests, native-Windows tests, and the
121
125
  complete public test matrix. The baseline gate also rejects
122
126
  `pull_request_target` and every unpinned non-local GitHub Action.
127
+ The native-Windows matrix runs the CLI, package, and global-install contracts on
128
+ Node 22, 24, and 26. It deterministically partitions the complete memory-server
129
+ smoke catalog across those lanes exactly once; the Linux matrix still runs the
130
+ complete catalog independently on every supported Node version.
@@ -1 +1 @@
1
- {"identifier":"cairn-offline-fake-v1","package_version":"2.21.0","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
1
+ {"identifier":"cairn-offline-fake-v1","package_version":"2.21.1","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
@@ -62,7 +62,7 @@
62
62
  "check:security-assurance": "node scripts/smoke-security-assurance.mjs",
63
63
  "check:notes": "npm run build && npm run check:note-signatures && npm run check:note-distillation && npm run check:note-enrichment",
64
64
  "test": "npm run test:smoke",
65
- "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 && npm run check:memory-proposals && npm run check:trajectory-roundtrip && npm run check:trajectory-redaction && npm run check:trajectory-retention && npm run check:note-signatures && npm run check:note-distillation && npm run check:note-enrichment && npm run check:node-compat && npm run check:typed-nodes && npm run check:memory-import && npm run check:note-mcp && npm run check:node-doctor && npm run check:artifact-store && npm run check:artifact-mcp && npm run check:compaction-capture && npm run check:capability-contract && npm run check:capability-logging && npm run check:capability-harness && npm run check:capability-mcp && npm run check:eval-schema && npm run check:eval-process && npm run check:eval-statistics && npm run check:skill-lifecycle && npm run check:graph-cli && npm run check:mcp-trust && npm run check:pi-mcp-bridge && npm run check:pi-bridge-env && npm run check:pi-bridge-child && npm run check:context-pack && npm run check:context-pack-retrieval && npm run check:context-pack-v216-compat && npm run check:context-usage && npm run check:domain-retrieval && npm run check:retrieval-benchmark && npm run check:work-evidence && npm run check:okf && npm run check:playbook && npm run check:security-assurance",
65
+ "test:smoke": "npm run build && node scripts/run-smoke-suite.mjs",
66
66
  "test:search": "npm run build && npm run check:search",
67
67
  "test:memory": "npm run build && npm run check:embeddings && npm run check:extract && npm run check:search",
68
68
  "test:all": "npm run test:memory",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cairnkeep/cli",
3
- "version": "2.21.0",
3
+ "version": "2.21.1",
4
4
  "description": "A durable, harness-agnostic memory + context layer for coding agents and MCP clients.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
package/scripts/doctor.sh CHANGED
@@ -36,15 +36,31 @@ fail() { printf ' [FAIL] %s\n' "$1"; fails=$((fails + 1)); }
36
36
  # reachable); a connect/timeout failure returns non-zero.
37
37
  reachable() { curl -sS -m 5 -o /dev/null "$1" >/dev/null 2>&1; }
38
38
 
39
+ embedding_failure="request failed"
39
40
  embedding_works() {
40
- local payload
41
+ local payload http_status curl_status
41
42
  payload=$(node -e 'process.stdout.write(JSON.stringify({ model: process.argv[1], input: ["cairnkeep health check"] }))' \
42
43
  "$CAIRN_MEMORY_EMBEDDING_MODEL") || return 1
43
- curl -fsS -m 10 -o /dev/null \
44
+ http_status=$(curl -sS -m 10 -o /dev/null -w '%{http_code}' \
44
45
  -H "Authorization: Bearer $CAIRN_LLM_API_KEY" \
45
46
  -H "Content-Type: application/json" \
46
47
  --data "$payload" \
47
- "${CAIRN_MEMORY_EMBEDDING_URL%/}/embeddings" >/dev/null 2>&1
48
+ "${CAIRN_MEMORY_EMBEDDING_URL%/}/embeddings" 2>/dev/null)
49
+ curl_status=$?
50
+ if [[ $curl_status -ne 0 ]]; then
51
+ embedding_failure="transport failed"
52
+ return 1
53
+ fi
54
+ case "$http_status" in
55
+ 2??) return 0 ;;
56
+ 401|403) embedding_failure="authorization rejected (HTTP $http_status)" ;;
57
+ 404) embedding_failure="endpoint or model not found (HTTP 404)" ;;
58
+ 408|429) embedding_failure="temporarily unavailable (HTTP $http_status)" ;;
59
+ 5??) embedding_failure="upstream service failed (HTTP $http_status)" ;;
60
+ [0-9][0-9][0-9]) embedding_failure="request failed (HTTP $http_status)" ;;
61
+ *) embedding_failure="request failed" ;;
62
+ esac
63
+ return 1
48
64
  }
49
65
 
50
66
  echo "cairn doctor"
@@ -90,7 +106,7 @@ if [[ -n "${CAIRN_MEMORY_EMBEDDING_URL:-}" ]]; then
90
106
  elif embedding_works; then
91
107
  pass "embedding endpoint accepted model ${CAIRN_MEMORY_EMBEDDING_MODEL} (${CAIRN_MEMORY_EMBEDDING_URL})"
92
108
  else
93
- fail "embedding request failed for model ${CAIRN_MEMORY_EMBEDDING_MODEL} (${CAIRN_MEMORY_EMBEDDING_URL})"
109
+ fail "embedding ${embedding_failure} for model ${CAIRN_MEMORY_EMBEDDING_MODEL} (${CAIRN_MEMORY_EMBEDDING_URL})"
94
110
  fi
95
111
  else
96
112
  skip "embedding endpoint (CAIRN_MEMORY_EMBEDDING_URL unset)"
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
5
+ node --input-type=module - "$ROOT" <<'NODE'
6
+ import assert from "node:assert/strict";
7
+ import { readFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { pathToFileURL } from "node:url";
10
+
11
+ const root = process.argv[2];
12
+ const { buildShardPlan, smokeChecks } = await import(
13
+ pathToFileURL(join(root, "mcp-memory-server", "scripts", "run-smoke-suite.mjs")));
14
+ const plan = buildShardPlan(3);
15
+ const flattened = plan.flatMap(({ checks }) => checks.map(({ name }) => name));
16
+ assert.equal(flattened.length, smokeChecks.length);
17
+ assert.equal(new Set(flattened).size, smokeChecks.length);
18
+ assert.deepEqual([...flattened].sort(), smokeChecks.map(({ name }) => name).sort());
19
+ assert.ok(plan.every(({ checks }) => checks.length > 0));
20
+ const weights = plan.map(({ weight }) => weight);
21
+ assert.ok(Math.max(...weights) - Math.min(...weights) <= 5,
22
+ `native Windows shards are imbalanced: ${weights.join(", ")}`);
23
+
24
+ const pkg = JSON.parse(readFileSync(join(root, "mcp-memory-server", "package.json"), "utf8"));
25
+ assert.match(pkg.scripts["test:smoke"], /run-smoke-suite\.mjs/);
26
+ const workflow = readFileSync(join(root, ".github", "workflows", "ci.yml"), "utf8");
27
+ assert.match(workflow, /name: native-windows \(\$\{\{ matrix\.node \}\}\)/);
28
+ for (const shard of ["1/3", "2/3", "3/3"]) assert.ok(workflow.includes(`shard: "${shard}"`));
29
+ assert.match(workflow, /run-smoke-suite\.mjs --shard \$\{\{ matrix\.shard \}\}/);
30
+ assert.doesNotMatch(workflow.match(/ native-windows:[\s\S]*?\n repository:/)?.[0] ?? "",
31
+ /npm --prefix mcp-memory-server test/);
32
+ console.log(`PASS: ${smokeChecks.length} memory-server contracts split once across balanced Windows shards (${weights.join(", ")})`);
33
+ NODE
@@ -92,13 +92,15 @@ const server = createServer((request, response) => {
92
92
  request.on("end", () => {
93
93
  let body = {};
94
94
  try { body = JSON.parse(Buffer.concat(chunks).toString("utf8")); } catch {}
95
- const valid = request.url === "/v1/embeddings"
96
- && request.headers.authorization === "Bearer test-key"
97
- && body.model === "test-embedding";
98
- response.writeHead(valid ? 200 : 404, { "Content-Type": "application/json" });
99
- response.end(JSON.stringify(valid
95
+ const routeValid = request.url === "/v1/embeddings";
96
+ const keyValid = request.headers.authorization === "Bearer test-key";
97
+ const modelValid = body.model === "test-embedding";
98
+ const unauthorized = body.model === "unauthorized-model";
99
+ const status = routeValid && keyValid && modelValid ? 200 : unauthorized ? 403 : 404;
100
+ response.writeHead(status, { "Content-Type": "application/json" });
101
+ response.end(JSON.stringify(status === 200
100
102
  ? { data: [{ index: 0, embedding: [1, 0] }] }
101
- : { error: "invalid request" }));
103
+ : { message: "private upstream detail must not leak", request_id: "private-request-id" }));
102
104
  });
103
105
  });
104
106
  server.listen(0, "127.0.0.1", () => {
@@ -122,17 +124,33 @@ EOF
122
124
  grep -q "\[PASS\] embedding endpoint accepted model test-embedding" "$tmp/out3" ||
123
125
  fail "expected a functional embedding PASS line"
124
126
 
125
- proj4="$tmp/embedding-wrong-model"; mkdir -p "$proj4/.ai"
127
+ proj4="$tmp/embedding-unauthorized"; mkdir -p "$proj4/.ai"
126
128
  cat > "$proj4/.ai/.env" <<EOF
127
129
  CAIRN_MEMORY_EMBEDDING_URL=http://127.0.0.1:$port/v1
128
- CAIRN_MEMORY_EMBEDDING_MODEL=missing-model
130
+ CAIRN_MEMORY_EMBEDDING_MODEL=unauthorized-model
129
131
  CAIRN_LLM_API_KEY=test-key
130
132
  EOF
131
133
  if ( cd "$proj4" && "$doctor" ) >"$tmp/out4" 2>&1; then
132
- fail "doctor should reject a missing embedding model:\n$(cat "$tmp/out4")"
134
+ fail "doctor should reject an unauthorized embedding request:\n$(cat "$tmp/out4")"
135
+ fi
136
+ grep -q "\[FAIL\] embedding authorization rejected (HTTP 403) for model unauthorized-model" "$tmp/out4" ||
137
+ fail "expected a sanitized embedding authorization diagnostic"
138
+ ! grep -q "private upstream detail\|private-request-id" "$tmp/out4" ||
139
+ fail "embedding diagnostic leaked an upstream body or request id"
140
+
141
+ proj5="$tmp/embedding-missing-model"; mkdir -p "$proj5/.ai"
142
+ cat > "$proj5/.ai/.env" <<EOF
143
+ CAIRN_MEMORY_EMBEDDING_URL=http://127.0.0.1:$port/v1
144
+ CAIRN_MEMORY_EMBEDDING_MODEL=missing-model
145
+ CAIRN_LLM_API_KEY=test-key
146
+ EOF
147
+ if ( cd "$proj5" && "$doctor" ) >"$tmp/out5" 2>&1; then
148
+ fail "doctor should reject a missing embedding model:\n$(cat "$tmp/out5")"
133
149
  fi
134
- grep -q "\[FAIL\] embedding request failed for model missing-model" "$tmp/out4" ||
135
- fail "expected a functional embedding FAIL line"
150
+ grep -q "\[FAIL\] embedding endpoint or model not found (HTTP 404) for model missing-model" "$tmp/out5" ||
151
+ fail "expected a sanitized missing embedding model diagnostic"
152
+ ! grep -q "private upstream detail\|private-request-id" "$tmp/out5" ||
153
+ fail "missing-model diagnostic leaked an upstream body or request id"
136
154
  fi
137
155
 
138
156
  # 5. Artifact doctor passes a valid store, repairs only derived state, and