@cairnkeep/cli 2.17.1 → 2.17.3

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,27 @@ All notable user-facing changes are documented here. This project follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [2.17.3] - 2026-09-03
9
+
10
+ ### Security
11
+
12
+ - Refresh the production dependency locks to `fast-uri` 3.1.7 and `qs`
13
+ 6.16.0 so published server and workspace containers do not retain the URI
14
+ parsing, SSRF, host-confusion, or denial-of-service advisories affecting the
15
+ versions locked by 2.17.2. Runtime APIs and command behavior are unchanged.
16
+
17
+ ## [2.17.2] - 2026-09-02
18
+
19
+ ### Fixed
20
+
21
+ - Generate the project Codex memory entry as `mcp_servers.cairn-memory-local`
22
+ instead of `cairn-memory`: Codex merges the user-wide and project MCP server
23
+ tables by id, so a project stdio entry collided with a user-wide remote
24
+ (url) entry of the same id and Codex >= 0.152 refused to start ("url is not
25
+ supported for stdio"). `cairn doctor` now diagnoses that legacy conflict
26
+ with a targeted recovery, and still accepts the old id on machines without
27
+ a user-wide remote entry.
28
+
8
29
  ## [2.17.1] - 2026-08-31
9
30
 
10
31
  ### Fixed
@@ -229,11 +229,16 @@ Native Windows uses `.\.ai\start-codex.cmd`. The generated
229
229
  `.codex/config.toml` contains only:
230
230
 
231
231
  ```toml
232
- [mcp_servers.cairn-memory]
232
+ [mcp_servers.cairn-memory-local]
233
233
  command = "cairn"
234
234
  args = ["memory-server"]
235
235
  ```
236
236
 
237
+ The id is `cairn-memory-local`, not `cairn-memory`: Codex merges the user-wide
238
+ and project `mcp_servers` tables by id, so a project stdio entry named
239
+ `cairn-memory` collides with a user-wide remote (url) entry of the same id and
240
+ Codex >= 0.152 refuses to start.
241
+
237
242
  Review this file before accepting Codex's project-trust prompt. Cairnkeep does
238
243
  not grant trust, edit the user-wide Codex configuration, or start Codex during
239
244
  setup. If an operator-owned `.codex/config.toml` already differs, setup reports
@@ -252,10 +257,10 @@ writes during setup. An operator who has explicitly authorized a bounded,
252
257
  unattended workflow may add narrow per-tool policy to the isolated Codex config:
253
258
 
254
259
  ```toml
255
- [mcp_servers.cairn-memory.tools.memory_write]
260
+ [mcp_servers.cairn-memory-local.tools.memory_write]
256
261
  approval_mode = "approve"
257
262
 
258
- [mcp_servers.cairn-memory.tools.memory_supersede]
263
+ [mcp_servers.cairn-memory-local.tools.memory_supersede]
259
264
  approval_mode = "approve"
260
265
  ```
261
266
 
@@ -1,6 +1,6 @@
1
1
  # Curriculum Coverage Map
2
2
 
3
- **Baseline:** Cairnkeep 2.17.1
3
+ **Baseline:** Cairnkeep 2.17.3
4
4
  **Last reviewed:** 2026-08-19
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.17.1
3
+ **Baseline:** Cairnkeep 2.17.3
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.17.1
6
+ **Tested with:** Cairnkeep 2.17.3
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.17.1
6
+ **Tested with:** Cairnkeep 2.17.3
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.17.1 and Node.js 22 or newer
6
+ **Tested with:** Cairnkeep 2.17.3 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.17.1
6
+ **Tested with:** Cairnkeep 2.17.3
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.17.1
6
+ **Tested with:** Cairnkeep 2.17.3
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.17.1 and Node.js 22 or newer
4
+ **Tested with:** Cairnkeep 2.17.3 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.17.1 and Node.js 22 or newer
4
+ **Tested with:** Cairnkeep 2.17.3 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.17.1, Windows x64, and Node.js 22 or newer
6
+ **Tested with:** Cairnkeep 2.17.3, 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.17.1; Pi 0.84.1; Node.js 22, 24, and 26
6
+ **Tested with:** Cairnkeep 2.17.3; 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.17.1 and Node.js 22 or newer
4
+ **Tested with:** Cairnkeep 2.17.3 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.17.1 and Node.js 22 or newer
4
+ **Tested with:** Cairnkeep 2.17.3 and Node.js 22 or newer
5
5
  **Time:** 35 minutes
6
6
 
7
7
  ## Outcome
@@ -54,7 +54,7 @@ cairn doctor
54
54
  Review `.codex\config.toml` and accept Codex's project-trust prompt. Setup does
55
55
  not edit the user-wide Codex configuration or grant trust. It preserves a
56
56
  different existing project configuration and doctor verifies a manually merged
57
- `mcp_servers.cairn-memory` table.
57
+ `mcp_servers.cairn-memory-local` table.
58
58
 
59
59
  Setup and bootstrap write both the established Unix launchers and native Windows
60
60
  launchers. Each `.cmd` file enters `start-harness.ps1`, loads `.ai/.env`, changes
package/docs/operating.md CHANGED
@@ -127,7 +127,7 @@ cairn doctor
127
127
 
128
128
  Review `.codex/config.toml` and accept Codex's project-trust prompt before use.
129
129
  An existing different file is preserved and reported as skipped; merge the
130
- generated `mcp_servers.cairn-memory` table into it, then run `cairn doctor`.
130
+ generated `mcp_servers.cairn-memory-local` table into it, then run `cairn doctor`.
131
131
  Uninstall leaves that operator-owned file intact. Selecting
132
132
  `--memory none` creates the launcher but deliberately omits the MCP entry.
133
133
 
@@ -22,16 +22,19 @@ The wizard recommends the existing repository mode when the target is already
22
22
  a Git work tree. On native Windows, launch with `.\.ai\start-codex.cmd`.
23
23
 
24
24
  Before the first session, review `.codex/config.toml` and accept Codex's project
25
- trust prompt. The generated entry runs `cairn memory-server` locally. Setup
26
- does not edit the user-wide Codex configuration and does not enable networking,
27
- telemetry, embeddings, session capture, or context packs.
25
+ trust prompt. The generated entry runs `cairn memory-server` locally under the
26
+ server id `cairn-memory-local`: Codex merges the user-wide and project
27
+ `mcp_servers` tables by id, so a project entry named `cairn-memory` collides
28
+ with a user-wide remote (url) entry of the same id and Codex >= 0.152 refuses
29
+ to start. Setup does not edit the user-wide Codex configuration and does not
30
+ enable networking, telemetry, embeddings, session capture, or context packs.
28
31
 
29
32
  If `.codex/config.toml` already exists and differs, setup reports it as
30
33
  `skipped` and preserves it byte-for-byte. Merge this table into the existing
31
34
  file, then run `cairn doctor`:
32
35
 
33
36
  ```toml
34
- [mcp_servers.cairn-memory]
37
+ [mcp_servers.cairn-memory-local]
35
38
  command = "cairn"
36
39
  args = ["memory-server"]
37
40
  ```
@@ -1 +1 @@
1
- {"identifier":"cairn-offline-fake-v1","package_version":"2.17.1","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
1
+ {"identifier":"cairn-offline-fake-v1","package_version":"2.17.3","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cairnkeep/cli",
3
- "version": "2.17.1",
3
+ "version": "2.17.3",
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/setup.mjs CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  readFileSync,
11
11
  } from "node:fs";
12
12
  import { createHash } from "node:crypto";
13
+ import { homedir } from "node:os";
13
14
  import { join, resolve } from "node:path";
14
15
  import { createInterface } from "node:readline/promises";
15
16
  import { stdin as defaultInput, stdout as defaultOutput, stderr as defaultError } from "node:process";
@@ -353,33 +354,63 @@ function readPrivateState(path) {
353
354
  }
354
355
  }
355
356
 
356
- function hasCodexMemoryConfig(project) {
357
- const path = join(project, ".codex", "config.toml");
358
- if (!existsSync(path)) return false;
357
+ function readTomlConfig(path) {
358
+ if (!existsSync(path)) return null;
359
359
  const info = lstatSync(path);
360
- if (!info.isFile() || info.isSymbolicLink() || info.size > 1024 * 1024) return false;
360
+ if (!info.isFile() || info.isSymbolicLink() || info.size > 1024 * 1024) return null;
361
361
  let descriptor;
362
- let text;
363
362
  try {
364
363
  descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
365
364
  const opened = fstatSync(descriptor);
366
- if (!opened.isFile() || opened.dev !== info.dev || opened.ino !== info.ino || opened.size > 1024 * 1024) return false;
367
- text = readFileSync(descriptor, "utf8");
365
+ if (!opened.isFile() || opened.dev !== info.dev || opened.ino !== info.ino || opened.size > 1024 * 1024) return null;
366
+ return readFileSync(descriptor, "utf8");
368
367
  } finally {
369
368
  if (descriptor !== undefined) closeSync(descriptor);
370
369
  }
370
+ }
371
+
372
+ function codexMemorySection(text, id) {
371
373
  const lines = text.split(/\r?\n/);
372
374
  const headers = lines.flatMap((line, index) => (
373
- ["[mcp_servers.cairn-memory]", "[mcp_servers.\"cairn-memory\"]"].includes(line.trim()) ? [index] : []
375
+ [`[mcp_servers.${id}]`, `[mcp_servers."${id}"]`].includes(line.trim()) ? [index] : []
374
376
  ));
375
- if (headers.length !== 1) return false;
377
+ if (headers.length !== 1) return null;
376
378
  const [start] = headers;
377
379
  const next = lines.findIndex((line, index) => index > start && /^\s*\[[^\]]+\]\s*$/.test(line));
378
- const section = lines.slice(start + 1, next < 0 ? lines.length : next).join("\n");
380
+ return lines.slice(start + 1, next < 0 ? lines.length : next).join("\n");
381
+ }
382
+
383
+ function isLocalMemorySection(section) {
379
384
  return (section.match(/^\s*command\s*=\s*["']cairn["']\s*$/gm) ?? []).length === 1
380
385
  && (section.match(/^\s*args\s*=\s*\[\s*["']memory-server["']\s*\]\s*$/gm) ?? []).length === 1;
381
386
  }
382
387
 
388
+ function hasCodexMemoryConfig(project) {
389
+ const text = readTomlConfig(join(project, ".codex", "config.toml"));
390
+ if (text === null) return false;
391
+ // `cairn-memory-local` is the current template id; the bare `cairn-memory`
392
+ // stdio form is accepted so pre-existing setups still diagnose complete.
393
+ return ["cairn-memory-local", "cairn-memory"].some((id) => {
394
+ const section = codexMemorySection(text, id);
395
+ return section !== null && isLocalMemorySection(section);
396
+ });
397
+ }
398
+
399
+ // Codex merges the user-wide and project mcp_servers tables field by field, so
400
+ // a legacy project stdio `cairn-memory` collides with a user-wide remote (url)
401
+ // entry of the same id and Codex >= 0.152 refuses to start at all.
402
+ function hasCodexRemoteMemoryConflict(project) {
403
+ const projectText = readTomlConfig(join(project, ".codex", "config.toml"));
404
+ if (projectText === null) return false;
405
+ const legacy = codexMemorySection(projectText, "cairn-memory");
406
+ if (legacy === null || !isLocalMemorySection(legacy)) return false;
407
+ const userHome = process.env.CODEX_HOME || join(homedir(), ".codex");
408
+ const userText = readTomlConfig(join(userHome, "config.toml"));
409
+ if (userText === null) return false;
410
+ const remote = codexMemorySection(userText, "cairn-memory");
411
+ return remote !== null && /^\s*url\s*=/m.test(remote);
412
+ }
413
+
383
414
  export function diagnoseSetup(target = ".") {
384
415
  const project = resolve(target);
385
416
  const statePath = join(project, ".ai", "cairnkeep.json");
@@ -414,8 +445,11 @@ export function diagnoseSetup(target = ".") {
414
445
  }
415
446
  const requiredAssets = [...COMMON_SETUP_ASSETS, ...requiredHarnessAssetPaths(state.harnesses, state.memory)];
416
447
  if (requiredAssets.some((path) => !Object.hasOwn(state.assets, path))) return incompleteDiagnosis(recovery);
417
- if (state.memory === "local" && state.harnesses.includes("codex") && !hasCodexMemoryConfig(project)) {
418
- return incompleteDiagnosis(recovery);
448
+ if (state.memory === "local" && state.harnesses.includes("codex")) {
449
+ if (!hasCodexMemoryConfig(project)) return incompleteDiagnosis(recovery);
450
+ if (hasCodexRemoteMemoryConflict(project)) {
451
+ return incompleteDiagnosis(`rename the project table to [mcp_servers.cairn-memory-local] in .codex/config.toml (Codex >= 0.152 rejects a project stdio 'cairn-memory' when the user-wide config defines a remote 'cairn-memory'), or delete the file and re-run: ${recovery}`);
452
+ }
419
453
  }
420
454
  return Object.freeze({
421
455
  schema_version: 1,
@@ -57,7 +57,7 @@ const result = JSON.parse(setup.stdout);
57
57
  assert.deepEqual(result.harnesses, ["codex"]);
58
58
  assert.equal(result.machine_sync.command, null);
59
59
  assert.deepEqual(result.launch_commands, [".ai/start-codex.sh"]);
60
- assert.match(readFileSync(join(target, ".codex", "config.toml"), "utf8"), /\[mcp_servers\.cairn-memory\][\s\S]*command = "cairn"[\s\S]*args = \["memory-server"\]/);
60
+ assert.match(readFileSync(join(target, ".codex", "config.toml"), "utf8"), /\[mcp_servers\.cairn-memory-local\][\s\S]*command = "cairn"[\s\S]*args = \["memory-server"\]/);
61
61
 
62
62
  const fakeBin = join(sandbox, "bin");
63
63
  mkdirSync(fakeBin);
@@ -94,7 +94,21 @@ assert.equal(setupModule.diagnoseSetup(collision).status, "incomplete");
94
94
  writeFileSync(join(collision, ".codex", "config.toml"), [
95
95
  "[features]", "fixture = true", "", "[mcp_servers.cairn-memory]", 'command = "cairn"', 'args = ["memory-server"]', "",
96
96
  ].join("\n"));
97
+ // Isolate the user-wide Codex config: the legacy stdio id only conflicts when
98
+ // the user-wide layer defines a remote (url) cairn-memory.
99
+ const codexHome = join(sandbox, "codex-home");
100
+ mkdirSync(codexHome);
101
+ process.env.CODEX_HOME = codexHome;
97
102
  assert.equal(setupModule.diagnoseSetup(collision).status, "complete");
103
+ writeFileSync(join(codexHome, "config.toml"), '[mcp_servers.cairn-memory]\nurl = "http://127.0.0.1:8788/mcp"\n');
104
+ const conflict = setupModule.diagnoseSetup(collision);
105
+ assert.equal(conflict.status, "incomplete");
106
+ assert.match(conflict.recovery[0], /cairn-memory-local/);
107
+ writeFileSync(join(collision, ".codex", "config.toml"), [
108
+ "[features]", "fixture = true", "", "[mcp_servers.cairn-memory-local]", 'command = "cairn"', 'args = ["memory-server"]', "",
109
+ ].join("\n"));
110
+ assert.equal(setupModule.diagnoseSetup(collision).status, "complete");
111
+ delete process.env.CODEX_HOME;
98
112
  const uninstall = spawnSync(join(root, "bin", "cairn"), ["uninstall", "--yes", collision], {
99
113
  encoding: "utf8", shell: false, env: { ...process.env, HOME: join(sandbox, "home") },
100
114
  });
@@ -1,5 +1,10 @@
1
1
  # Cairnkeep project-scoped memory server. Codex loads project configuration only
2
2
  # after the project has been trusted.
3
- [mcp_servers.cairn-memory]
3
+ #
4
+ # The id is `cairn-memory-local`, not `cairn-memory`: Codex merges the user-wide
5
+ # and project mcp_servers tables field by field, so a project stdio entry named
6
+ # `cairn-memory` collides with a user-wide remote (url) entry of the same id and
7
+ # Codex >= 0.152 refuses to start ("url is not supported for stdio").
8
+ [mcp_servers.cairn-memory-local]
4
9
  command = "cairn"
5
10
  args = ["memory-server"]