@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.
Files changed (89) hide show
  1. package/CHANGELOG.md +68 -1
  2. package/README.md +82 -27
  3. package/bin/cairn +6 -153
  4. package/bin/cairn-container +8 -0
  5. package/docs/building-an-overlay.md +41 -4
  6. package/docs/containers.md +10 -0
  7. package/docs/context-packs.md +94 -0
  8. package/docs/harness-compatibility.md +37 -3
  9. package/docs/learning/CURRICULUM-MAP.md +16 -5
  10. package/docs/learning/FEATURE-GUIDE.md +15 -4
  11. package/docs/learning/README.md +8 -2
  12. package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
  13. package/docs/learning/lessons/L01-safe-trial.md +3 -3
  14. package/docs/learning/lessons/L02-installation.md +11 -2
  15. package/docs/learning/lessons/L03-first-project.md +1 -1
  16. package/docs/learning/lessons/L19-validated-skill-improvement.md +52 -21
  17. package/docs/learning/lessons/L20-mcp-tool-profiles.md +67 -0
  18. package/docs/learning/lessons/L21-context-packs.md +79 -0
  19. package/docs/learning/lessons/L22-native-windows.md +83 -0
  20. package/docs/learning/lessons/L23-guided-setup.md +138 -0
  21. package/docs/learning/tracks/operator.md +10 -3
  22. package/docs/learning/tracks/quickstart.md +7 -3
  23. package/docs/mcp-tool-profiles.md +43 -0
  24. package/docs/native-windows.md +122 -0
  25. package/docs/operating.md +157 -18
  26. package/docs/privacy-and-data-flow.md +51 -1
  27. package/docs/storage.md +29 -0
  28. package/examples/eval/bundled-fake.json +1 -1
  29. package/mcp-memory-server/dist/artifact-store-windows-worker.js +74 -0
  30. package/mcp-memory-server/dist/artifact-store.js +196 -55
  31. package/mcp-memory-server/dist/capability-cli.js +2 -1
  32. package/mcp-memory-server/dist/capability-config.js +4 -3
  33. package/mcp-memory-server/dist/capability-harness.js +34 -7
  34. package/mcp-memory-server/dist/context-pack-cli.js +185 -0
  35. package/mcp-memory-server/dist/context-pack.js +889 -0
  36. package/mcp-memory-server/dist/eval-cli.js +6 -5
  37. package/mcp-memory-server/dist/eval-plan.js +35 -13
  38. package/mcp-memory-server/dist/eval-process.js +11 -4
  39. package/mcp-memory-server/dist/eval-report.js +8 -5
  40. package/mcp-memory-server/dist/eval-runner.js +2 -0
  41. package/mcp-memory-server/dist/eval-schema.js +1 -0
  42. package/mcp-memory-server/dist/graph-cli.js +7 -1
  43. package/mcp-memory-server/dist/index.js +87 -37
  44. package/mcp-memory-server/dist/mcp-tool-catalog.js +47 -0
  45. package/mcp-memory-server/dist/mcp-tool-cli.js +85 -0
  46. package/mcp-memory-server/dist/mcp-tool-profile.js +164 -0
  47. package/mcp-memory-server/dist/note-store.js +12 -3
  48. package/mcp-memory-server/dist/pi-mcp-bridge.js +185 -0
  49. package/mcp-memory-server/dist/platform-security.js +102 -0
  50. package/mcp-memory-server/dist/skill-store.js +27 -12
  51. package/mcp-memory-server/package.json +4 -1
  52. package/package.json +5 -4
  53. package/pi/extensions/cairnkeep-memory.ts +61 -0
  54. package/schemas/cairnkeep-setup-policy.schema.json +55 -0
  55. package/schemas/cairnkeep-setup.schema.json +36 -0
  56. package/schemas/context-pack.schema.json +33 -0
  57. package/schemas/eval-report.schema.json +1 -0
  58. package/scripts/cairn-cli.mjs +119 -0
  59. package/scripts/cairn-container-cli.mjs +121 -0
  60. package/scripts/cairn-hook.mjs +199 -0
  61. package/scripts/completion.sh +33 -5
  62. package/scripts/doctor.sh +96 -0
  63. package/scripts/fix-permissions.mjs +12 -0
  64. package/scripts/memory-wiki-audit.mjs +95 -0
  65. package/scripts/phase26-test-manifest.mjs +26 -0
  66. package/scripts/run-repository-tests.mjs +76 -0
  67. package/scripts/setup-core.mjs +310 -0
  68. package/scripts/setup-reconcile.mjs +331 -0
  69. package/scripts/setup.mjs +306 -0
  70. package/scripts/sync-pi-assets.sh +19 -4
  71. package/scripts/test-cli-dispatch.sh +6 -0
  72. package/scripts/test-completion.sh +44 -0
  73. package/scripts/test-doctor.sh +66 -0
  74. package/scripts/test-learning-docs.sh +53 -3
  75. package/scripts/test-package-install.sh +81 -0
  76. package/scripts/test-phase17-artifact-lifecycle.sh +1 -1
  77. package/scripts/test-phase18-capability-lifecycle.sh +1 -1
  78. package/scripts/test-pi-lifecycle.mjs +190 -0
  79. package/scripts/test-setup-compatibility.mjs +186 -0
  80. package/scripts/test-setup-output.sh +131 -0
  81. package/scripts/test-setup-overlay.sh +113 -0
  82. package/scripts/test-setup-preflight.mjs +281 -0
  83. package/scripts/test-setup-reconcile.mjs +257 -0
  84. package/scripts/test-uninstall.sh +71 -0
  85. package/scripts/test-windows-native.mjs +136 -0
  86. package/scripts/uninstall.sh +95 -7
  87. package/scripts/verify-pi-mcp-bridge.mjs +588 -0
  88. package/scripts/windows-platform.mjs +725 -0
  89. 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 { chmod, lstat, mkdir, open, readdir, realpath, rename, rm, stat, } from "node:fs/promises";
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
- await chmod(directory, 0o700);
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
- await chmod(temporary, 0o600);
190
+ hardenPrivatePath(temporary);
189
191
  await rename(temporary, path);
190
- await chmod(path, 0o600);
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 || (info.mode & 0o077) !== 0)
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
- await rm(path, { force: true });
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() || (info.mode & 0o077) !== 0)
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
+ });