@cairnkeep/cli 2.8.0 → 2.11.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 +68 -1
- package/README.md +82 -27
- package/bin/cairn +6 -153
- package/bin/cairn-container +8 -0
- package/docs/building-an-overlay.md +41 -4
- package/docs/containers.md +10 -0
- package/docs/context-packs.md +94 -0
- package/docs/harness-compatibility.md +37 -3
- package/docs/learning/CURRICULUM-MAP.md +16 -5
- package/docs/learning/FEATURE-GUIDE.md +15 -4
- package/docs/learning/README.md +8 -2
- package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
- package/docs/learning/lessons/L01-safe-trial.md +3 -3
- package/docs/learning/lessons/L02-installation.md +11 -2
- package/docs/learning/lessons/L03-first-project.md +1 -1
- package/docs/learning/lessons/L19-validated-skill-improvement.md +52 -21
- package/docs/learning/lessons/L20-mcp-tool-profiles.md +67 -0
- package/docs/learning/lessons/L21-context-packs.md +79 -0
- package/docs/learning/lessons/L22-native-windows.md +83 -0
- package/docs/learning/lessons/L23-guided-setup.md +138 -0
- package/docs/learning/tracks/operator.md +10 -3
- package/docs/learning/tracks/quickstart.md +7 -3
- package/docs/mcp-tool-profiles.md +43 -0
- package/docs/native-windows.md +122 -0
- package/docs/operating.md +157 -18
- package/docs/privacy-and-data-flow.md +51 -1
- package/docs/storage.md +29 -0
- package/examples/eval/bundled-fake.json +1 -1
- package/mcp-memory-server/dist/artifact-store-windows-worker.js +74 -0
- package/mcp-memory-server/dist/artifact-store.js +196 -55
- package/mcp-memory-server/dist/capability-cli.js +2 -1
- package/mcp-memory-server/dist/capability-config.js +4 -3
- package/mcp-memory-server/dist/capability-harness.js +34 -7
- package/mcp-memory-server/dist/context-pack-cli.js +185 -0
- package/mcp-memory-server/dist/context-pack.js +889 -0
- package/mcp-memory-server/dist/eval-cli.js +6 -5
- package/mcp-memory-server/dist/eval-plan.js +35 -13
- package/mcp-memory-server/dist/eval-process.js +11 -4
- package/mcp-memory-server/dist/eval-report.js +8 -5
- package/mcp-memory-server/dist/eval-runner.js +2 -0
- package/mcp-memory-server/dist/eval-schema.js +1 -0
- package/mcp-memory-server/dist/graph-cli.js +7 -1
- package/mcp-memory-server/dist/index.js +87 -37
- package/mcp-memory-server/dist/mcp-tool-catalog.js +47 -0
- package/mcp-memory-server/dist/mcp-tool-cli.js +85 -0
- package/mcp-memory-server/dist/mcp-tool-profile.js +164 -0
- package/mcp-memory-server/dist/note-store.js +12 -3
- package/mcp-memory-server/dist/pi-mcp-bridge.js +185 -0
- package/mcp-memory-server/dist/platform-security.js +102 -0
- package/mcp-memory-server/dist/skill-store.js +27 -12
- package/mcp-memory-server/package.json +4 -1
- package/package.json +5 -4
- package/pi/extensions/cairnkeep-memory.ts +61 -0
- package/schemas/cairnkeep-setup-policy.schema.json +55 -0
- package/schemas/cairnkeep-setup.schema.json +36 -0
- package/schemas/context-pack.schema.json +33 -0
- package/schemas/eval-report.schema.json +1 -0
- package/scripts/cairn-cli.mjs +119 -0
- package/scripts/cairn-container-cli.mjs +121 -0
- package/scripts/cairn-hook.mjs +199 -0
- package/scripts/completion.sh +33 -5
- package/scripts/doctor.sh +96 -0
- package/scripts/fix-permissions.mjs +12 -0
- package/scripts/memory-wiki-audit.mjs +95 -0
- package/scripts/phase26-test-manifest.mjs +26 -0
- package/scripts/run-repository-tests.mjs +76 -0
- package/scripts/setup-core.mjs +310 -0
- package/scripts/setup-reconcile.mjs +331 -0
- package/scripts/setup.mjs +306 -0
- package/scripts/sync-pi-assets.sh +19 -4
- package/scripts/test-cli-dispatch.sh +6 -0
- package/scripts/test-completion.sh +44 -0
- package/scripts/test-doctor.sh +66 -0
- package/scripts/test-learning-docs.sh +53 -3
- package/scripts/test-package-install.sh +81 -0
- package/scripts/test-phase17-artifact-lifecycle.sh +1 -1
- package/scripts/test-phase18-capability-lifecycle.sh +1 -1
- package/scripts/test-pi-lifecycle.mjs +190 -0
- package/scripts/test-setup-compatibility.mjs +186 -0
- package/scripts/test-setup-output.sh +131 -0
- package/scripts/test-setup-overlay.sh +113 -0
- package/scripts/test-setup-preflight.mjs +281 -0
- package/scripts/test-setup-reconcile.mjs +257 -0
- package/scripts/test-uninstall.sh +71 -0
- package/scripts/test-windows-native.mjs +136 -0
- package/scripts/uninstall.sh +95 -7
- package/scripts/verify-pi-mcp-bridge.mjs +588 -0
- package/scripts/windows-platform.mjs +725 -0
- package/templates/env.example.template +11 -0
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import {
|
|
4
|
+
import { lstat, mkdir, open, readdir, realpath, rename, rm, stat, } from "node:fs/promises";
|
|
5
5
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { isCapabilityContractEnabled, resolveCapabilityStatus, } from "./capability-config.js";
|
|
8
8
|
import { operatingCapabilityHandleSchema, } from "./capability-adapter.js";
|
|
9
9
|
import { CAPABILITY_CALLBACK_SCHEMA_VERSION, isOperatingCapabilitySettled, issueOperatingCapability, settleOperatingCapability, } from "./capability-store.js";
|
|
10
10
|
import { isTrajectoryCaptureEnabled } from "./trajectory-schema.js";
|
|
11
|
+
import { hardenPrivatePath, privatePathIsSafe } from "./platform-security.js";
|
|
11
12
|
const LEASE_DIRECTORY_NAME = "capability-leases-v1";
|
|
12
13
|
const LEASE_SCHEMA_VERSION = 1;
|
|
13
14
|
const MAX_LEASE_BYTES = 4_096;
|
|
@@ -124,12 +125,13 @@ async function ensureLeaseDirectory(options = {}) {
|
|
|
124
125
|
const rootInfo = await lstat(root);
|
|
125
126
|
if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink())
|
|
126
127
|
throw new Error("Harness state root is unsafe.");
|
|
128
|
+
hardenPrivatePath(root);
|
|
127
129
|
const directory = getHarnessCapabilityLeaseDirectory(options);
|
|
128
130
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
129
131
|
const info = await lstat(directory);
|
|
130
132
|
if (!info.isDirectory() || info.isSymbolicLink())
|
|
131
133
|
throw new Error("Harness lease directory is unsafe.");
|
|
132
|
-
|
|
134
|
+
hardenPrivatePath(directory);
|
|
133
135
|
return directory;
|
|
134
136
|
}
|
|
135
137
|
async function canonicalProjectBinding(rawRoot) {
|
|
@@ -185,9 +187,9 @@ async function writeLease(lease, options = {}) {
|
|
|
185
187
|
await handle.sync();
|
|
186
188
|
await handle.close();
|
|
187
189
|
handle = undefined;
|
|
188
|
-
|
|
190
|
+
hardenPrivatePath(temporary);
|
|
189
191
|
await rename(temporary, path);
|
|
190
|
-
|
|
192
|
+
hardenPrivatePath(path);
|
|
191
193
|
}
|
|
192
194
|
finally {
|
|
193
195
|
if (handle)
|
|
@@ -200,7 +202,7 @@ async function readLease(path) {
|
|
|
200
202
|
try {
|
|
201
203
|
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
202
204
|
const info = await handle.stat();
|
|
203
|
-
if (!info.isFile() || info.size > MAX_LEASE_BYTES || (
|
|
205
|
+
if (!info.isFile() || info.size > MAX_LEASE_BYTES || !privatePathIsSafe(path))
|
|
204
206
|
return undefined;
|
|
205
207
|
const bytes = await handle.readFile();
|
|
206
208
|
if (bytes.byteLength > MAX_LEASE_BYTES)
|
|
@@ -216,7 +218,32 @@ async function readLease(path) {
|
|
|
216
218
|
}
|
|
217
219
|
}
|
|
218
220
|
async function removeLease(path) {
|
|
219
|
-
|
|
221
|
+
let lastError;
|
|
222
|
+
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
223
|
+
try {
|
|
224
|
+
await rm(path, { force: true });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
const code = error.code;
|
|
229
|
+
if (code !== "EPERM" && code !== "EBUSY")
|
|
230
|
+
throw error;
|
|
231
|
+
lastError = error;
|
|
232
|
+
if (process.platform === "win32" && attempt === 0) {
|
|
233
|
+
try {
|
|
234
|
+
const info = await lstat(path);
|
|
235
|
+
if (info.isFile() && !info.isSymbolicLink())
|
|
236
|
+
hardenPrivatePath(path);
|
|
237
|
+
}
|
|
238
|
+
catch (aclError) {
|
|
239
|
+
if (aclError.code === "ENOENT")
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, 25 * (attempt + 1)));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
throw lastError;
|
|
220
247
|
}
|
|
221
248
|
function capabilityState(snapshot, id) {
|
|
222
249
|
const state = snapshot.capabilities.find((row) => row.id === id);
|
|
@@ -280,7 +307,7 @@ async function matchingLeases(input, options = {}) {
|
|
|
280
307
|
let entries;
|
|
281
308
|
try {
|
|
282
309
|
const info = await lstat(directory);
|
|
283
|
-
if (!info.isDirectory() || info.isSymbolicLink() || (
|
|
310
|
+
if (!info.isDirectory() || info.isSymbolicLink() || !privatePathIsSafe(directory))
|
|
284
311
|
return [];
|
|
285
312
|
entries = (await readdir(directory, { withFileTypes: true }))
|
|
286
313
|
.sort((left, right) => left.name.localeCompare(right.name))
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { applyContextPackUpdate, approvePackSkill, contextPackBaseDirectory, disableContextPack, doctorContextPacks, enableContextPack, initializeContextPack, inspectContextPackUpdate, installContextPack, listInstalledContextPacks, listPackSkills, lockContextPack, readProjectPointer, removeContextPack, resolveInstalledPack, revokePackSkill, validateContextPack, } from "./context-pack.js";
|
|
5
|
+
function usage() {
|
|
6
|
+
process.stderr.write(`Usage:
|
|
7
|
+
cairn pack init [DIRECTORY] [--id ID] [--version VERSION] [--title TITLE] [--description TEXT] [--license LICENSE]
|
|
8
|
+
cairn pack lock|validate [DIRECTORY]
|
|
9
|
+
cairn pack install SOURCE [--ref REF]
|
|
10
|
+
cairn pack list [--json]
|
|
11
|
+
cairn pack show SELECTOR [--json]
|
|
12
|
+
cairn pack remove SELECTOR
|
|
13
|
+
cairn pack enable|disable SELECTOR [--project PATH | --project-id ID]
|
|
14
|
+
cairn pack update SELECTOR --check [--project PATH | --project-id ID]
|
|
15
|
+
cairn pack update SELECTOR --apply --confirm DIGEST [--project PATH | --project-id ID]
|
|
16
|
+
cairn pack skills [--project PATH | --project-id ID] [--json]
|
|
17
|
+
cairn pack approve-skill SELECTOR PATH --confirm FILE_DIGEST [--project PATH | --project-id ID]
|
|
18
|
+
cairn pack revoke-skill SELECTOR PATH [--project PATH | --project-id ID]
|
|
19
|
+
`);
|
|
20
|
+
process.exit(2);
|
|
21
|
+
}
|
|
22
|
+
function take(args, name) {
|
|
23
|
+
const index = args.indexOf(name);
|
|
24
|
+
if (index < 0)
|
|
25
|
+
return undefined;
|
|
26
|
+
const value = args[index + 1];
|
|
27
|
+
if (!value || value.startsWith("--"))
|
|
28
|
+
usage();
|
|
29
|
+
args.splice(index, 2);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
function flag(args, name) {
|
|
33
|
+
const index = args.indexOf(name);
|
|
34
|
+
if (index < 0)
|
|
35
|
+
return false;
|
|
36
|
+
args.splice(index, 1);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
function projectOptions(args) {
|
|
40
|
+
const projectRoot = take(args, "--project");
|
|
41
|
+
const projectId = take(args, "--project-id");
|
|
42
|
+
if (projectRoot && projectId)
|
|
43
|
+
usage();
|
|
44
|
+
return projectId ? { projectId } : { projectRoot: projectRoot ?? process.cwd() };
|
|
45
|
+
}
|
|
46
|
+
function output(value, json, human) {
|
|
47
|
+
process.stdout.write(`${json ? JSON.stringify(value) : human ?? JSON.stringify(value, null, 2)}\n`);
|
|
48
|
+
}
|
|
49
|
+
async function main() {
|
|
50
|
+
const args = process.argv.slice(2);
|
|
51
|
+
const command = args.shift();
|
|
52
|
+
const json = flag(args, "--json");
|
|
53
|
+
if (command === "init") {
|
|
54
|
+
const directory = args[0] && !args[0].startsWith("--") ? args.shift() : process.cwd();
|
|
55
|
+
const manifest = await initializeContextPack(directory, {
|
|
56
|
+
id: take(args, "--id"), version: take(args, "--version"), title: take(args, "--title"),
|
|
57
|
+
description: take(args, "--description"), license: take(args, "--license"),
|
|
58
|
+
});
|
|
59
|
+
if (args.length)
|
|
60
|
+
usage();
|
|
61
|
+
output(manifest, json, `Initialized ${resolve(directory)}/context-pack.json`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (command === "lock" || command === "validate") {
|
|
65
|
+
const directory = args.shift() ?? process.cwd();
|
|
66
|
+
if (args.length)
|
|
67
|
+
usage();
|
|
68
|
+
const result = command === "lock" ? await lockContextPack(directory) : await validateContextPack(directory);
|
|
69
|
+
output(result, json, command === "lock" ? `Locked ${resolve(directory)}/context-pack.json` : `Valid context pack: ${result.digest}`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (command === "install") {
|
|
73
|
+
const source = args.shift();
|
|
74
|
+
if (!source)
|
|
75
|
+
usage();
|
|
76
|
+
const ref = take(args, "--ref");
|
|
77
|
+
if (args.length)
|
|
78
|
+
usage();
|
|
79
|
+
const result = await installContextPack(source, { ref });
|
|
80
|
+
output({ id: result.pack.manifest.id, version: result.pack.manifest.version, digest: result.pack.digest, source: result.source, existing: result.existing }, json, `${result.existing ? "Already installed" : "Installed"} ${result.pack.manifest.id}@${result.pack.manifest.version} ${result.pack.digest}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (command === "list") {
|
|
84
|
+
if (args.length)
|
|
85
|
+
usage();
|
|
86
|
+
const packs = await listInstalledContextPacks();
|
|
87
|
+
output({ schema_version: 1, packs }, json, packs.length ? packs.map((pack) => `${pack.id}@${pack.version}\t${pack.digest}`).join("\n") : "No context packs installed.");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (command === "show") {
|
|
91
|
+
const selector = args.shift();
|
|
92
|
+
if (!selector || args.length)
|
|
93
|
+
usage();
|
|
94
|
+
const pack = await resolveInstalledPack(selector);
|
|
95
|
+
output({ ...pack.manifest, digest: pack.digest, total_bytes: pack.total_bytes }, json);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (command === "remove") {
|
|
99
|
+
const selector = args.shift();
|
|
100
|
+
if (!selector || args.length)
|
|
101
|
+
usage();
|
|
102
|
+
const digest = await removeContextPack(selector);
|
|
103
|
+
output({ removed: true, digest }, json, `Removed context pack ${digest}.`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (command === "enable" || command === "disable") {
|
|
107
|
+
const selector = args.shift();
|
|
108
|
+
if (!selector)
|
|
109
|
+
usage();
|
|
110
|
+
const project = projectOptions(args);
|
|
111
|
+
if (args.length)
|
|
112
|
+
usage();
|
|
113
|
+
const pointer = command === "enable" ? await enableContextPack(selector, project) : await disableContextPack(selector, project);
|
|
114
|
+
output(pointer, json, `${command === "enable" ? "Enabled" : "Disabled"} context pack ${selector}.`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (command === "update") {
|
|
118
|
+
const selector = args.shift();
|
|
119
|
+
if (!selector)
|
|
120
|
+
usage();
|
|
121
|
+
const check = flag(args, "--check");
|
|
122
|
+
const apply = flag(args, "--apply");
|
|
123
|
+
const confirm = take(args, "--confirm");
|
|
124
|
+
const project = projectOptions(args);
|
|
125
|
+
if (args.length || check === apply || (apply && !confirm) || (check && confirm))
|
|
126
|
+
usage();
|
|
127
|
+
const result = check ? await inspectContextPackUpdate(selector, project) : await applyContextPackUpdate(selector, confirm, project);
|
|
128
|
+
output(result, json, check ? `${result.changed ? "Update available" : "Up to date"}: ${result.candidate_digest}` : `Applied context pack update ${result.candidate_digest}.`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (command === "skills") {
|
|
132
|
+
const project = projectOptions(args);
|
|
133
|
+
if (args.length)
|
|
134
|
+
usage();
|
|
135
|
+
const skills = await listPackSkills(project);
|
|
136
|
+
output({ schema_version: 1, skills }, json, skills.length ? skills.map((row) => `${row.approved ? "approved" : "unapproved"}\t${row.pack_id}\t${row.path}\t${row.file_digest}`).join("\n") : "No context pack skills.");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (command === "approve-skill" || command === "revoke-skill") {
|
|
140
|
+
const selector = args.shift();
|
|
141
|
+
const path = args.shift();
|
|
142
|
+
if (!selector || !path)
|
|
143
|
+
usage();
|
|
144
|
+
const confirm = take(args, "--confirm");
|
|
145
|
+
const project = projectOptions(args);
|
|
146
|
+
if (args.length || (command === "approve-skill" && !confirm) || (command === "revoke-skill" && confirm))
|
|
147
|
+
usage();
|
|
148
|
+
const pointer = command === "approve-skill"
|
|
149
|
+
? await approvePackSkill(selector, path, confirm, project)
|
|
150
|
+
: await revokePackSkill(selector, path, project);
|
|
151
|
+
output(pointer, json, `${command === "approve-skill" ? "Approved" : "Revoked"} ${path}.`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (command === "doctor") {
|
|
155
|
+
if (args.length)
|
|
156
|
+
usage();
|
|
157
|
+
const result = await doctorContextPacks();
|
|
158
|
+
output(result, json, result.ok ? `Context packs healthy (${result.objects} objects, ${result.projects} projects).` : `Context pack problems: ${[...result.issues, ...result.temporary_remnants].join("; ")}`);
|
|
159
|
+
if (!result.ok)
|
|
160
|
+
process.exitCode = 1;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (command === "purge") {
|
|
164
|
+
if (!flag(args, "--yes") || args.length)
|
|
165
|
+
usage();
|
|
166
|
+
const base = contextPackBaseDirectory();
|
|
167
|
+
if (existsSync(base))
|
|
168
|
+
await rm(base, { recursive: true, force: true });
|
|
169
|
+
output({ purged: true, path: base }, json, `Purged context pack data at ${base}.`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (command === "status") {
|
|
173
|
+
const project = projectOptions(args);
|
|
174
|
+
if (args.length)
|
|
175
|
+
usage();
|
|
176
|
+
const pointer = readProjectPointer(project);
|
|
177
|
+
output(pointer, json);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
usage();
|
|
181
|
+
}
|
|
182
|
+
main().catch((error) => {
|
|
183
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
184
|
+
process.exitCode = 1;
|
|
185
|
+
});
|