@cairnkeep/cli 2.17.1 → 2.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/docs/harness-compatibility.md +8 -3
- package/docs/learning/CURRICULUM-MAP.md +1 -1
- package/docs/learning/FEATURE-GUIDE.md +1 -1
- package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
- package/docs/learning/lessons/L01-safe-trial.md +1 -1
- package/docs/learning/lessons/L02-installation.md +1 -1
- package/docs/learning/lessons/L03-first-project.md +1 -1
- package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
- package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
- package/docs/learning/lessons/L21-context-packs.md +1 -1
- package/docs/learning/lessons/L22-native-windows.md +1 -1
- package/docs/learning/lessons/L23-guided-setup.md +1 -1
- package/docs/learning/lessons/L24-okf-exchange.md +1 -1
- package/docs/learning/lessons/L25-playbooks.md +1 -1
- package/docs/native-windows.md +1 -1
- package/docs/operating.md +1 -1
- package/docs/quickstart.md +7 -4
- package/examples/eval/bundled-fake.json +1 -1
- package/package.json +1 -1
- package/scripts/setup.mjs +46 -12
- package/scripts/test-harness-registry.sh +15 -1
- package/templates/codex-config.toml.template +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable user-facing changes are documented here. This project follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [2.17.2] - 2026-09-02
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Generate the project Codex memory entry as `mcp_servers.cairn-memory-local`
|
|
13
|
+
instead of `cairn-memory`: Codex merges the user-wide and project MCP server
|
|
14
|
+
tables by id, so a project stdio entry collided with a user-wide remote
|
|
15
|
+
(url) entry of the same id and Codex >= 0.152 refused to start ("url is not
|
|
16
|
+
supported for stdio"). `cairn doctor` now diagnoses that legacy conflict
|
|
17
|
+
with a targeted recovery, and still accepts the old id on machines without
|
|
18
|
+
a user-wide remote entry.
|
|
19
|
+
|
|
8
20
|
## [2.17.1] - 2026-08-31
|
|
9
21
|
|
|
10
22
|
### 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
|
|
package/docs/native-windows.md
CHANGED
|
@@ -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
|
|
package/docs/quickstart.md
CHANGED
|
@@ -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
|
|
26
|
-
|
|
27
|
-
|
|
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
|
+
{"identifier":"cairn-offline-fake-v1","package_version":"2.17.2","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
|
package/package.json
CHANGED
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
|
|
357
|
-
|
|
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
|
|
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
|
|
367
|
-
|
|
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
|
-
[
|
|
375
|
+
[`[mcp_servers.${id}]`, `[mcp_servers."${id}"]`].includes(line.trim()) ? [index] : []
|
|
374
376
|
));
|
|
375
|
-
if (headers.length !== 1) return
|
|
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
|
-
|
|
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")
|
|
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
|
-
|
|
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"]
|