@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
|
@@ -7,6 +7,7 @@ import { isEvalEnabled, loadEvalPlan } from "./eval-plan.js";
|
|
|
7
7
|
import { diagnoseEvalReport, readEvalReport, renderEvalReport, } from "./eval-report.js";
|
|
8
8
|
import { buildAblationArms, runCapabilityAblation, runTwoPassExperiment, } from "./eval-runner.js";
|
|
9
9
|
import { canonicalJson, EVAL_SCHEMA_VERSION } from "./eval-schema.js";
|
|
10
|
+
import { privatePathIsSafe } from "./platform-security.js";
|
|
10
11
|
process.stdout.on("error", (error) => {
|
|
11
12
|
if (error.code === "EPIPE")
|
|
12
13
|
process.exit(0);
|
|
@@ -126,7 +127,7 @@ async function evalRoot() {
|
|
|
126
127
|
return null;
|
|
127
128
|
throw error;
|
|
128
129
|
}
|
|
129
|
-
if (info.isSymbolicLink() || !info.isDirectory() || (
|
|
130
|
+
if (info.isSymbolicLink() || !info.isDirectory() || !privatePathIsSafe(root)) {
|
|
130
131
|
throw new Error("unsafe_eval_root");
|
|
131
132
|
}
|
|
132
133
|
const resolvedRoot = await realpath(root);
|
|
@@ -185,7 +186,7 @@ async function diagnoseProjectReports(projectRoot) {
|
|
|
185
186
|
}
|
|
186
187
|
if (info.isSymbolicLink() || !info.isDirectory() || await realpath(directory) !== directory)
|
|
187
188
|
return "unsafe";
|
|
188
|
-
if (directory === root && (
|
|
189
|
+
if (directory === root && !privatePathIsSafe(directory))
|
|
189
190
|
return "unsafe";
|
|
190
191
|
}
|
|
191
192
|
const entries = await readdir(root, { withFileTypes: true });
|
|
@@ -201,7 +202,7 @@ async function diagnoseProjectReports(projectRoot) {
|
|
|
201
202
|
if (!isContained(root, experimentPath) || experimentPath === root)
|
|
202
203
|
return "unsafe";
|
|
203
204
|
const experimentInfo = await lstat(experimentPath);
|
|
204
|
-
if (experimentInfo.isSymbolicLink() || !experimentInfo.isDirectory() || (
|
|
205
|
+
if (experimentInfo.isSymbolicLink() || !experimentInfo.isDirectory() || !privatePathIsSafe(experimentPath)
|
|
205
206
|
|| await realpath(experimentPath) !== experimentPath) {
|
|
206
207
|
return "unsafe";
|
|
207
208
|
}
|
|
@@ -247,7 +248,7 @@ async function safeReportStore(experimentId) {
|
|
|
247
248
|
throw new Error("eval_report_not_found");
|
|
248
249
|
throw error;
|
|
249
250
|
}
|
|
250
|
-
if (directory.isSymbolicLink() || !directory.isDirectory() || (
|
|
251
|
+
if (directory.isSymbolicLink() || !directory.isDirectory() || !privatePathIsSafe(experimentPath)
|
|
251
252
|
|| await realpath(experimentPath) !== experimentPath) {
|
|
252
253
|
throw new Error("unsafe_experiment_path");
|
|
253
254
|
}
|
|
@@ -261,7 +262,7 @@ async function safeReportStore(experimentId) {
|
|
|
261
262
|
throw new Error("eval_report_not_found");
|
|
262
263
|
throw error;
|
|
263
264
|
}
|
|
264
|
-
if (reportInfo.isSymbolicLink() || !reportInfo.isFile() || (
|
|
265
|
+
if (reportInfo.isSymbolicLink() || !reportInfo.isFile() || !privatePathIsSafe(reportPath)
|
|
265
266
|
|| reportInfo.size > DEFAULT_REPORT_BYTES) {
|
|
266
267
|
throw new Error("unsafe_eval_report");
|
|
267
268
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
3
|
import { accessSync, closeSync, existsSync, fstatSync, lstatSync, openSync, readFileSync, realpathSync, } from "node:fs";
|
|
4
|
-
import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { basename, delimiter, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { CAPABILITY_IDS } from "./capability-schema.js";
|
|
7
7
|
import { EVAL_SCHEMA_VERSION, canonicalDigest, canonicalJson, evalAdapterConfigSchema, evalTaskSetSchema, } from "./eval-schema.js";
|
|
@@ -16,6 +16,19 @@ function isContained(root, candidate) {
|
|
|
16
16
|
const rel = relative(root, candidate);
|
|
17
17
|
return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel));
|
|
18
18
|
}
|
|
19
|
+
function isExistingPathContained(root, candidate) {
|
|
20
|
+
const rootInfo = lstatSync(root);
|
|
21
|
+
let current = realpathSync(candidate);
|
|
22
|
+
for (;;) {
|
|
23
|
+
const currentInfo = lstatSync(current);
|
|
24
|
+
if (currentInfo.dev === rootInfo.dev && currentInfo.ino === rootInfo.ino)
|
|
25
|
+
return true;
|
|
26
|
+
const parent = dirname(current);
|
|
27
|
+
if (parent === current)
|
|
28
|
+
return false;
|
|
29
|
+
current = parent;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
19
32
|
function assertRealDirectory(path, label) {
|
|
20
33
|
const info = lstatSync(path);
|
|
21
34
|
if (info.isSymbolicLink() || !info.isDirectory())
|
|
@@ -68,12 +81,16 @@ function resolveExactCommit(repository, revision) {
|
|
|
68
81
|
return resolved;
|
|
69
82
|
}
|
|
70
83
|
function parseCommittedTaskSet(taskSetPath) {
|
|
71
|
-
const
|
|
72
|
-
|
|
84
|
+
const manifestDirectory = dirname(taskSetPath);
|
|
85
|
+
const manifestRepository = assertRealDirectory(git(manifestDirectory, ["rev-parse", "--show-toplevel"], "Resolve task-set repository"), "Task-set repository");
|
|
86
|
+
if (!isExistingPathContained(manifestRepository, taskSetPath)) {
|
|
73
87
|
throw new Error("The task-set manifest must be contained by its Git repository.");
|
|
74
|
-
|
|
75
|
-
|
|
88
|
+
}
|
|
89
|
+
const manifestPrefix = git(manifestDirectory, ["rev-parse", "--show-prefix"], "Resolve task-set repository prefix");
|
|
90
|
+
const manifestPath = `${manifestPrefix}${basename(taskSetPath)}`.split("\\").join("/");
|
|
91
|
+
if (!manifestPath || manifestPath.startsWith("../") || isAbsolute(manifestPath)) {
|
|
76
92
|
throw new Error("The task-set manifest path is invalid.");
|
|
93
|
+
}
|
|
77
94
|
git(manifestRepository, ["ls-files", "--error-unmatch", "--", manifestPath], "Require a tracked evaluation task set");
|
|
78
95
|
const dirty = git(manifestRepository, ["status", "--porcelain", "--untracked-files=all", "--", manifestPath], "Check evaluation task set status");
|
|
79
96
|
if (dirty !== "")
|
|
@@ -111,15 +128,20 @@ function resolvePathProgram(program, base) {
|
|
|
111
128
|
function resolveProgram(program, base) {
|
|
112
129
|
if (isAbsolute(program) || program.includes("/") || program.includes("\\"))
|
|
113
130
|
return resolvePathProgram(program, base);
|
|
131
|
+
const names = process.platform === "win32" && extname(program) === ""
|
|
132
|
+
? [program, ...[".EXE", ".COM"].map((extension) => `${program}${extension}`)]
|
|
133
|
+
: [program];
|
|
114
134
|
for (const directory of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
135
|
+
for (const name of names) {
|
|
136
|
+
const candidate = join(directory, name);
|
|
137
|
+
try {
|
|
138
|
+
accessSync(candidate, constants.X_OK);
|
|
139
|
+
if (lstatSync(candidate).isFile() || lstatSync(candidate).isSymbolicLink())
|
|
140
|
+
return realpathSync(candidate);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Continue through PATH without executing the candidate.
|
|
144
|
+
}
|
|
123
145
|
}
|
|
124
146
|
}
|
|
125
147
|
throw new Error(`Evaluation program was not found on PATH: ${program}`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
2
|
import { evalAdapterRequestSchema, evalAdapterResultSchema, } from "./eval-schema.js";
|
|
3
3
|
export const EVAL_PROCESS_ERROR_CODES = [
|
|
4
4
|
"spawn_error",
|
|
@@ -33,7 +33,7 @@ export class EvalProcessError extends Error {
|
|
|
33
33
|
const DEFAULT_KILL_GRACE_MS = 1_000;
|
|
34
34
|
const MAX_TIMER_MS = 2_147_483_647;
|
|
35
35
|
function terminationScope() {
|
|
36
|
-
return process.platform === "win32" ? "
|
|
36
|
+
return process.platform === "win32" ? "process-tree" : "process-group";
|
|
37
37
|
}
|
|
38
38
|
function assertOptions(options) {
|
|
39
39
|
if (!Number.isSafeInteger(options.timeout_ms) || options.timeout_ms <= 0 || options.timeout_ms > MAX_TIMER_MS) {
|
|
@@ -59,7 +59,9 @@ function signalChild(child, signal) {
|
|
|
59
59
|
process.kill(-child.pid, signal);
|
|
60
60
|
return true;
|
|
61
61
|
}
|
|
62
|
-
|
|
62
|
+
const args = ["/PID", String(child.pid), "/T", ...(signal === "SIGKILL" ? ["/F"] : [])];
|
|
63
|
+
const result = spawnSync("taskkill.exe", args, { stdio: "ignore", windowsHide: true });
|
|
64
|
+
return result.status === 0 || child.kill(signal);
|
|
63
65
|
}
|
|
64
66
|
catch {
|
|
65
67
|
try {
|
|
@@ -90,7 +92,12 @@ async function runCommandInternal(options) {
|
|
|
90
92
|
let exitSignal = null;
|
|
91
93
|
let graceTimer;
|
|
92
94
|
let closeTimer;
|
|
93
|
-
|
|
95
|
+
// A shebang does not make a JavaScript file directly executable on
|
|
96
|
+
// Windows. Adapter contracts may point at JS programs, so launch
|
|
97
|
+
// those through the current runtime while preserving the declared
|
|
98
|
+
// command for hashing and provenance.
|
|
99
|
+
const javascriptProgram = /\.(?:cjs|mjs|js)$/i.test(options.command.program);
|
|
100
|
+
const child = spawn(javascriptProgram ? process.execPath : options.command.program, [...(javascriptProgram ? [options.command.program] : []), ...options.command.args], {
|
|
94
101
|
cwd: options.cwd,
|
|
95
102
|
env: options.env,
|
|
96
103
|
shell: false,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
|
-
import {
|
|
3
|
+
import { lstat, mkdir, open, readdir, realpath, rename, rm, } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { canonicalDigest, canonicalJson, evalAggregateSchema, evalReportSchema, } from "./eval-schema.js";
|
|
6
6
|
import { computeDifferenceInDifferences, computePairedEstimate, } from "./eval-statistics.js";
|
|
7
|
+
import { hardenPrivatePath, privatePathIsSafe } from "./platform-security.js";
|
|
7
8
|
const DEFAULT_MAX_REPORT_BYTES = 16 * 1024 * 1024;
|
|
8
9
|
const MAX_ALLOWED_REPORT_BYTES = 64 * 1024 * 1024;
|
|
9
10
|
const DEFAULT_MAX_EXPERIMENTS = 10_000;
|
|
@@ -36,7 +37,7 @@ async function ensureSafeDirectory(path, privateMode = false) {
|
|
|
36
37
|
if (info.isSymbolicLink() || !info.isDirectory())
|
|
37
38
|
throw new Error("unsafe_report_directory");
|
|
38
39
|
if (privateMode)
|
|
39
|
-
|
|
40
|
+
hardenPrivatePath(path);
|
|
40
41
|
return;
|
|
41
42
|
}
|
|
42
43
|
catch (error) {
|
|
@@ -48,7 +49,7 @@ async function ensureSafeDirectory(path, privateMode = false) {
|
|
|
48
49
|
throw new Error("unsafe_report_directory");
|
|
49
50
|
await ensureSafeDirectory(parent, false);
|
|
50
51
|
await mkdir(path, { mode: 0o700 });
|
|
51
|
-
|
|
52
|
+
hardenPrivatePath(path);
|
|
52
53
|
}
|
|
53
54
|
function assertLimits(maxReportBytes, maxExperiments) {
|
|
54
55
|
if (!Number.isSafeInteger(maxReportBytes) || maxReportBytes < 1 || maxReportBytes > MAX_ALLOWED_REPORT_BYTES) {
|
|
@@ -206,8 +207,9 @@ export async function checkpointEvalReport(store, report, options = {}) {
|
|
|
206
207
|
await handle.close();
|
|
207
208
|
handle = undefined;
|
|
208
209
|
injectFault(options, "after_close");
|
|
210
|
+
hardenPrivatePath(temporary);
|
|
209
211
|
await rename(temporary, store.report_path);
|
|
210
|
-
|
|
212
|
+
hardenPrivatePath(store.report_path);
|
|
211
213
|
injectFault(options, "after_rename");
|
|
212
214
|
}
|
|
213
215
|
finally {
|
|
@@ -239,7 +241,7 @@ export async function diagnoseEvalReport(store) {
|
|
|
239
241
|
});
|
|
240
242
|
if (!info)
|
|
241
243
|
return { state: "absent", code: "report_absent" };
|
|
242
|
-
if (info.isSymbolicLink() || !info.isFile() || (
|
|
244
|
+
if (info.isSymbolicLink() || !info.isFile() || !privatePathIsSafe(store.report_path)) {
|
|
243
245
|
return { state: "unsafe", code: "report_unsafe" };
|
|
244
246
|
}
|
|
245
247
|
const report = await readEvalReport(store);
|
|
@@ -578,6 +580,7 @@ export function renderEvalReport(value) {
|
|
|
578
580
|
`Cairn evaluation report: ${report.experiment_id}`,
|
|
579
581
|
`status: ${report.status}`,
|
|
580
582
|
`evidence: ${report.evidence.evidence_scope === "offline-framework" ? "framework-only" : "live-evaluation"}`,
|
|
583
|
+
`MCP tool profile digest: ${report.evidence.mcp_tool_profile_digest ?? "not-recorded"}`,
|
|
581
584
|
"All effects are estimates; small samples, missing intervals, and intervals crossing zero are inconclusive.",
|
|
582
585
|
"",
|
|
583
586
|
"Aggregates",
|
|
@@ -10,6 +10,7 @@ import { canonicalDigest, evalReportSchema, evalReportRuntimeContractDigest, } f
|
|
|
10
10
|
import { EvalProcessError, runBoundedCommand, runBoundedJsonAdapter, } from "./eval-process.js";
|
|
11
11
|
import { checkpointEvalReport, createEvalReportStore, } from "./eval-report.js";
|
|
12
12
|
import { applyEvalWorkspaceOverlay, cleanupEvalWorkspace, createEvalWorkspace, runTaskPreparation, runTaskVerifier, } from "./eval-workspace.js";
|
|
13
|
+
import { resolveMcpToolProfile } from "./mcp-tool-profile.js";
|
|
13
14
|
const moduleDirectory = dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
const publishedReportSchemaPath = resolve(moduleDirectory, "..", "..", "schemas", "eval-report.schema.json");
|
|
15
16
|
let reportSchemaDigestsCache;
|
|
@@ -735,6 +736,7 @@ function initialReport(plan, experimentId, fallbackExperimentKind) {
|
|
|
735
736
|
package_version: plan.source.kind === "bundled_fake" ? plan.source.package_version : "0.1.0",
|
|
736
737
|
runtime_id: "node-local",
|
|
737
738
|
task_set_digest: plan.task_set_digest,
|
|
739
|
+
mcp_tool_profile_digest: resolveMcpToolProfile({ projectRoot: process.cwd() }).profile_digest,
|
|
738
740
|
report_digest: canonicalDigest({ experiment_id: experimentId, plan_digest: plan.plan_digest }),
|
|
739
741
|
schema_digests: [...evalReportSchemaDigests()],
|
|
740
742
|
note_snapshot_digests: [],
|
|
@@ -309,6 +309,7 @@ const evidenceProvenanceSchema = z.strictObject({
|
|
|
309
309
|
package_version: z.string().min(1).max(128),
|
|
310
310
|
runtime_id: identifierSchema,
|
|
311
311
|
task_set_digest: digestSchema,
|
|
312
|
+
mcp_tool_profile_digest: digestSchema.optional(),
|
|
312
313
|
report_digest: digestSchema,
|
|
313
314
|
schema_digests: z.array(digestSchema).length(2),
|
|
314
315
|
note_snapshot_digests: z.array(digestSchema).max(MAX_TASKS),
|
|
@@ -90,7 +90,13 @@ function graphifyEnvironment() {
|
|
|
90
90
|
return environment;
|
|
91
91
|
}
|
|
92
92
|
function executeGraphify(projectRoot, args, timeout) {
|
|
93
|
-
const
|
|
93
|
+
const configuredProgram = process.env.CAIRN_GRAPHIFY_BINARY?.trim();
|
|
94
|
+
const graphifyProgram = configuredProgram || "graphify";
|
|
95
|
+
const javascriptProgram = /\.(?:cjs|mjs|js)$/i.test(graphifyProgram);
|
|
96
|
+
const result = spawnSync(javascriptProgram ? process.execPath : graphifyProgram, [
|
|
97
|
+
...(javascriptProgram ? [graphifyProgram] : []),
|
|
98
|
+
...args,
|
|
99
|
+
], {
|
|
94
100
|
cwd: projectRoot,
|
|
95
101
|
encoding: "utf8",
|
|
96
102
|
shell: false,
|
|
@@ -17,6 +17,9 @@ import { EmbeddingCache, cosineSimilarity, embedTexts, getEmbeddingConfig, hashT
|
|
|
17
17
|
import { computeRepoState, exploreCacheKey, normalizeExploreQuery, readExploreCache, writeExploreCache, } from "./explore-cache.js";
|
|
18
18
|
import { canonicalTagsSchema, isTypedMemoryNodesEnabled, memoryImportEnvelopeSchema, noteNodeSchema, nodeTypeSchema, } from "./node-schema.js";
|
|
19
19
|
import { attachNodeMetadata, applyReviewedTypedNode, commitMemoryImport, createTypedNode, deleteTypedNode, getTypedNode, invalidateReviewedTypedNode, listTypedHistory, planMemoryImport, supersedeTypedNode, } from "./node-store.js";
|
|
20
|
+
import { contextPackHttpEnabled, contextPacksEnabled, listVisibleContext, readVisibleContext, searchVisibleContext } from "./context-pack.js";
|
|
21
|
+
import { metadataForTool } from "./mcp-tool-catalog.js";
|
|
22
|
+
import { profileAllowsTool, resolveMcpToolProfile } from "./mcp-tool-profile.js";
|
|
20
23
|
const MINIMUM_NODE_MAJOR = 22;
|
|
21
24
|
const nodeMajor = Number.parseInt(process.versions.node, 10);
|
|
22
25
|
if (!Number.isInteger(nodeMajor) || nodeMajor < MINIMUM_NODE_MAJOR) {
|
|
@@ -758,7 +761,18 @@ async function runContextExplore(args) {
|
|
|
758
761
|
}
|
|
759
762
|
// --- Execution tier: return { ok: false, ... } (runtime problems, D-04) ---
|
|
760
763
|
if (!evidence) {
|
|
761
|
-
|
|
764
|
+
// Native Windows cannot execute a JavaScript file directly from a
|
|
765
|
+
// shebang. Treat JS helpers as Node programs while keeping compiled
|
|
766
|
+
// token_miser binaries byte-for-byte compatible on every platform.
|
|
767
|
+
const javascriptHelper = /\.(?:cjs|mjs|js)$/i.test(binaryPath);
|
|
768
|
+
const result = await runCommand(javascriptHelper ? process.execPath : binaryPath, [
|
|
769
|
+
...(javascriptHelper ? [binaryPath] : []),
|
|
770
|
+
"explore",
|
|
771
|
+
"--query",
|
|
772
|
+
query,
|
|
773
|
+
"--repo-root",
|
|
774
|
+
repoRoot,
|
|
775
|
+
], (timeoutSeconds ?? 120) * 1000, { ...process.env, NO_COLOR: "1" });
|
|
762
776
|
if (result.timedOut || result.exitCode !== 0) {
|
|
763
777
|
const payload = {
|
|
764
778
|
ok: false,
|
|
@@ -822,8 +836,18 @@ function gitToplevel(cwd) {
|
|
|
822
836
|
// only allows one connected transport per server). All instances share the
|
|
823
837
|
// module-level helpers + AgentFS below. Enables a single long-lived process to
|
|
824
838
|
// serve many concurrent clients within one trusted server-side storage domain.
|
|
825
|
-
function buildMemoryServer(context, capabilitySnapshot, withCapability) {
|
|
839
|
+
function buildMemoryServer(context, capabilitySnapshot, withCapability, toolProfile = resolveMcpToolProfile({ projectRoot: process.cwd() })) {
|
|
826
840
|
const server = new McpServer({ name: "cairn-memory", version: "0.1.0" });
|
|
841
|
+
const registerTool = (name, config, callback) => {
|
|
842
|
+
const metadata = metadataForTool(name);
|
|
843
|
+
if (!profileAllowsTool(toolProfile, name))
|
|
844
|
+
return;
|
|
845
|
+
server.registerTool(name, {
|
|
846
|
+
...config,
|
|
847
|
+
title: metadata.title,
|
|
848
|
+
annotations: metadata.annotations,
|
|
849
|
+
}, callback);
|
|
850
|
+
};
|
|
827
851
|
const capabilityEnabled = (id) => capabilitySnapshot?.capabilities.find((state) => state.id === id)?.enabled ?? true;
|
|
828
852
|
const registerCapabilityTool = (id, name, config, callback) => {
|
|
829
853
|
if (!capabilityEnabled(id))
|
|
@@ -840,15 +864,22 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability) {
|
|
|
840
864
|
},
|
|
841
865
|
}, callback)
|
|
842
866
|
: callback;
|
|
843
|
-
|
|
867
|
+
registerTool(name, config, handler);
|
|
844
868
|
};
|
|
845
869
|
const memoryConfig = () => context.memoryConfig ?? getMemoryConfig();
|
|
846
870
|
const scopeOptions = { projectId: context.projectId };
|
|
847
871
|
const typedNodesEnabled = isTypedMemoryNodesEnabled();
|
|
848
872
|
const artifactToolsEnabled = isArtifactStoreEnabled() && (!context.remote || isArtifactHttpEnabled());
|
|
873
|
+
const contextPackToolsEnabled = contextPacksEnabled() && (!context.remote || contextPackHttpEnabled());
|
|
849
874
|
if (artifactToolsEnabled && context.remote && !context.projectId) {
|
|
850
875
|
throw new ClientContextError("Remote artifact access requires X-Cairn-Project.");
|
|
851
876
|
}
|
|
877
|
+
if (contextPackToolsEnabled && context.remote && !context.projectId) {
|
|
878
|
+
throw new ClientContextError("Remote context pack access requires X-Cairn-Project.");
|
|
879
|
+
}
|
|
880
|
+
const contextPackProject = context.remote
|
|
881
|
+
? { projectId: context.projectId }
|
|
882
|
+
: { projectRoot: process.cwd() };
|
|
852
883
|
let fallbackArtifactSessionRef;
|
|
853
884
|
const resolveArtifactSessionRef = (sessionRef) => {
|
|
854
885
|
if (sessionRef)
|
|
@@ -892,7 +923,7 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability) {
|
|
|
892
923
|
generatedFileInputContentSchema,
|
|
893
924
|
]),
|
|
894
925
|
}).strict();
|
|
895
|
-
|
|
926
|
+
registerTool("artifact_write", {
|
|
896
927
|
description: "Write one bounded inline artifact to the immutable project artifact store.",
|
|
897
928
|
inputSchema: artifactWriteToolSchema,
|
|
898
929
|
annotations: {
|
|
@@ -920,7 +951,7 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability) {
|
|
|
920
951
|
structuredContent: payload,
|
|
921
952
|
};
|
|
922
953
|
});
|
|
923
|
-
|
|
954
|
+
registerTool("artifact_read", {
|
|
924
955
|
description: "Read one artifact by exact ID or an unambiguous ID prefix.",
|
|
925
956
|
inputSchema: z.object({ artifact_id: artifactIdentifierSchema }).strict(),
|
|
926
957
|
annotations: {
|
|
@@ -936,7 +967,7 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability) {
|
|
|
936
967
|
structuredContent: payload,
|
|
937
968
|
};
|
|
938
969
|
});
|
|
939
|
-
|
|
970
|
+
registerTool("artifact_list", {
|
|
940
971
|
description: "List artifact metadata newest first with exact filters and opaque pagination.",
|
|
941
972
|
inputSchema: z.object({
|
|
942
973
|
session_ref: artifactSessionRefSchema.optional(),
|
|
@@ -976,7 +1007,7 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability) {
|
|
|
976
1007
|
structuredContent: payload,
|
|
977
1008
|
};
|
|
978
1009
|
});
|
|
979
|
-
|
|
1010
|
+
registerTool("artifact_delete", {
|
|
980
1011
|
description: "Hard-delete one artifact by exact ID or an unambiguous ID prefix.",
|
|
981
1012
|
inputSchema: z.object({ artifact_id: artifactIdentifierSchema }).strict(),
|
|
982
1013
|
annotations: {
|
|
@@ -1013,7 +1044,7 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability) {
|
|
|
1013
1044
|
};
|
|
1014
1045
|
});
|
|
1015
1046
|
}
|
|
1016
|
-
|
|
1047
|
+
registerTool("memory_read", {
|
|
1017
1048
|
description: "Read an exact key or search memory entries across AgentFS scopes.",
|
|
1018
1049
|
// Plain object schema — a .refine() wrapper (ZodEffects) makes the SDK
|
|
1019
1050
|
// publish an empty JSON Schema, hiding the parameters from clients.
|
|
@@ -1151,7 +1182,7 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability) {
|
|
|
1151
1182
|
structuredContent: payload,
|
|
1152
1183
|
};
|
|
1153
1184
|
});
|
|
1154
|
-
|
|
1185
|
+
registerTool("memory_list", {
|
|
1155
1186
|
description: "List keys from a scoped AgentFS database.",
|
|
1156
1187
|
inputSchema: z.object({
|
|
1157
1188
|
scope: z.string(),
|
|
@@ -1189,7 +1220,7 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability) {
|
|
|
1189
1220
|
structuredContent: typedNodesEnabled ? { keys, nodes } : { keys },
|
|
1190
1221
|
};
|
|
1191
1222
|
});
|
|
1192
|
-
|
|
1223
|
+
registerTool("memory_delete", {
|
|
1193
1224
|
description: "Delete a key from a scoped AgentFS database.",
|
|
1194
1225
|
inputSchema: z.object({
|
|
1195
1226
|
scope: z.string(),
|
|
@@ -1273,7 +1304,7 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability) {
|
|
|
1273
1304
|
structuredContent: payload,
|
|
1274
1305
|
};
|
|
1275
1306
|
});
|
|
1276
|
-
|
|
1307
|
+
registerTool("memory_extract", {
|
|
1277
1308
|
description: "Extract durable memory candidates from a session summary or selected text. Review the returned candidates before writing them.",
|
|
1278
1309
|
inputSchema: z.object({
|
|
1279
1310
|
scope: z.string(),
|
|
@@ -1294,7 +1325,7 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability) {
|
|
|
1294
1325
|
structuredContent: payload,
|
|
1295
1326
|
};
|
|
1296
1327
|
});
|
|
1297
|
-
|
|
1328
|
+
registerTool("memory_supersede", {
|
|
1298
1329
|
description: "Preserve the current value of a memory entry in hidden history, then write a new live value to the base key.",
|
|
1299
1330
|
inputSchema: z.object({
|
|
1300
1331
|
scope: z.string(),
|
|
@@ -1380,7 +1411,7 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability) {
|
|
|
1380
1411
|
await agent.close();
|
|
1381
1412
|
}
|
|
1382
1413
|
});
|
|
1383
|
-
|
|
1414
|
+
registerTool("memory_apply_reviewed", {
|
|
1384
1415
|
description: "Idempotently apply a human-reviewed memory revision with durable provenance.",
|
|
1385
1416
|
inputSchema: z.object({
|
|
1386
1417
|
scope: z.string(),
|
|
@@ -1484,7 +1515,7 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability) {
|
|
|
1484
1515
|
await agent.close();
|
|
1485
1516
|
}
|
|
1486
1517
|
});
|
|
1487
|
-
|
|
1518
|
+
registerTool("memory_invalidate_reviewed", {
|
|
1488
1519
|
description: "Invalidate a reviewed revision, removing it only when the live value still matches.",
|
|
1489
1520
|
inputSchema: z.object({
|
|
1490
1521
|
scope: z.string(),
|
|
@@ -1609,7 +1640,7 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability) {
|
|
|
1609
1640
|
await agent.close();
|
|
1610
1641
|
}
|
|
1611
1642
|
});
|
|
1612
|
-
|
|
1643
|
+
registerTool("memory_history", {
|
|
1613
1644
|
description: "Read prior versions of a memory entry from the hidden history namespace.",
|
|
1614
1645
|
inputSchema: z.object({
|
|
1615
1646
|
scope: z.string(),
|
|
@@ -1678,7 +1709,7 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability) {
|
|
|
1678
1709
|
};
|
|
1679
1710
|
});
|
|
1680
1711
|
if (typedNodesEnabled) {
|
|
1681
|
-
|
|
1712
|
+
registerTool("memory_import", {
|
|
1682
1713
|
description: "Validate, plan, and atomically import a bounded batch of typed memory nodes.",
|
|
1683
1714
|
inputSchema: memoryImportEnvelopeSchema,
|
|
1684
1715
|
annotations: {
|
|
@@ -1698,30 +1729,18 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability) {
|
|
|
1698
1729
|
}
|
|
1699
1730
|
// Resolve containment before any database is opened or directory is created.
|
|
1700
1731
|
const scopePath = resolveScopePath(envelope.scope, scopeOptions);
|
|
1701
|
-
const existing = await openScope(envelope.scope, false, scopeOptions);
|
|
1702
|
-
let plan;
|
|
1703
|
-
try {
|
|
1704
|
-
plan = await planMemoryImport(existing, envelope);
|
|
1705
|
-
}
|
|
1706
|
-
finally {
|
|
1707
|
-
await existing?.close();
|
|
1708
|
-
}
|
|
1709
|
-
if (envelope.dry_run) {
|
|
1710
|
-
const payload = await commitMemoryImport(null, plan);
|
|
1711
|
-
return {
|
|
1712
|
-
content: [{ type: "text", text: asToolText(payload) }],
|
|
1713
|
-
structuredContent: payload,
|
|
1714
|
-
};
|
|
1715
|
-
}
|
|
1716
1732
|
const payload = await serializeScopeMutation(scopePath, async () => {
|
|
1717
|
-
const agent = await openScope(envelope.scope,
|
|
1718
|
-
if (!agent)
|
|
1719
|
-
throw new Error(`Unable to open scope ${envelope.scope}.`);
|
|
1733
|
+
const agent = await openScope(envelope.scope, !envelope.dry_run, scopeOptions);
|
|
1720
1734
|
try {
|
|
1735
|
+
const plan = await planMemoryImport(agent, envelope);
|
|
1736
|
+
if (envelope.dry_run)
|
|
1737
|
+
return commitMemoryImport(null, plan);
|
|
1738
|
+
if (!agent)
|
|
1739
|
+
throw new Error(`Unable to open scope ${envelope.scope}.`);
|
|
1721
1740
|
return await commitMemoryImport(agent, plan);
|
|
1722
1741
|
}
|
|
1723
1742
|
finally {
|
|
1724
|
-
await agent
|
|
1743
|
+
await agent?.close();
|
|
1725
1744
|
}
|
|
1726
1745
|
});
|
|
1727
1746
|
return {
|
|
@@ -1730,7 +1749,7 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability) {
|
|
|
1730
1749
|
};
|
|
1731
1750
|
});
|
|
1732
1751
|
}
|
|
1733
|
-
|
|
1752
|
+
registerTool("domain_knowledge_query", {
|
|
1734
1753
|
description: "Query an AnythingLLM workspace in query mode for domain knowledge.",
|
|
1735
1754
|
inputSchema: z.object({
|
|
1736
1755
|
workspace: z.string().min(1).optional(),
|
|
@@ -1750,7 +1769,7 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability) {
|
|
|
1750
1769
|
structuredContent: { workspace: workspaceSlug, answer },
|
|
1751
1770
|
};
|
|
1752
1771
|
});
|
|
1753
|
-
|
|
1772
|
+
registerTool("domain_knowledge_sync", {
|
|
1754
1773
|
description: "Upload and embed configured project documentation into an AnythingLLM workspace. Uses anythingllm-projects.json so include/exclude rules are honored. Use mode='replace' with confirm_replace=true when stale workspace docs must be removed before re-embedding.",
|
|
1755
1774
|
inputSchema: z.object({
|
|
1756
1775
|
workspace: z.string().min(1).optional(),
|
|
@@ -1879,6 +1898,37 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability) {
|
|
|
1879
1898
|
const payload = { ok: true, status: body.status, cluster_healthy: body.cluster_healthy ?? null };
|
|
1880
1899
|
return { content: [{ type: "text", text: asToolText(payload) }], structuredContent: payload };
|
|
1881
1900
|
});
|
|
1901
|
+
if (contextPackToolsEnabled) {
|
|
1902
|
+
registerTool("context_pack_list", {
|
|
1903
|
+
description: "List documents and explicitly approved skills from context packs enabled for this project.",
|
|
1904
|
+
inputSchema: z.object({}).strict(),
|
|
1905
|
+
}, async () => {
|
|
1906
|
+
const payload = await listVisibleContext(contextPackProject);
|
|
1907
|
+
return { content: [{ type: "text", text: asToolText(payload) }], structuredContent: payload };
|
|
1908
|
+
});
|
|
1909
|
+
registerTool("context_pack_search", {
|
|
1910
|
+
description: "Search documents and explicitly approved skills in context packs enabled for this project.",
|
|
1911
|
+
inputSchema: z.object({
|
|
1912
|
+
query: z.string().min(1).max(4096),
|
|
1913
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
1914
|
+
}).strict(),
|
|
1915
|
+
}, async ({ query, limit }) => {
|
|
1916
|
+
const payload = await searchVisibleContext(query, { ...contextPackProject, limit });
|
|
1917
|
+
return { content: [{ type: "text", text: asToolText(payload) }], structuredContent: payload };
|
|
1918
|
+
});
|
|
1919
|
+
registerTool("context_pack_read", {
|
|
1920
|
+
description: "Read a bounded UTF-8 range from one enabled context-pack document or approved skill.",
|
|
1921
|
+
inputSchema: z.object({
|
|
1922
|
+
pack: z.string().min(1).max(128),
|
|
1923
|
+
path: z.string().min(1).max(512),
|
|
1924
|
+
offset: z.number().int().min(0).optional(),
|
|
1925
|
+
max_bytes: z.number().int().min(1).max(8192).optional(),
|
|
1926
|
+
}).strict(),
|
|
1927
|
+
}, async ({ pack, path, offset, max_bytes }) => {
|
|
1928
|
+
const payload = await readVisibleContext(pack, path, { ...contextPackProject, offset, maxBytes: max_bytes });
|
|
1929
|
+
return { content: [{ type: "text", text: asToolText(payload) }], structuredContent: payload };
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1882
1932
|
return server;
|
|
1883
1933
|
}
|
|
1884
1934
|
function createMemoryServer(context = {}) {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const observation = (title, openWorldHint = false) => ({
|
|
2
|
+
title,
|
|
3
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint },
|
|
4
|
+
});
|
|
5
|
+
const additive = (title, openWorldHint = false) => ({
|
|
6
|
+
title,
|
|
7
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint },
|
|
8
|
+
});
|
|
9
|
+
const mutation = (title, openWorldHint = false) => ({
|
|
10
|
+
title,
|
|
11
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint },
|
|
12
|
+
});
|
|
13
|
+
// This is the authoritative MCP catalog. Adding a registered tool without an
|
|
14
|
+
// entry is a startup error, so clients can safely classify every Cairnkeep tool.
|
|
15
|
+
export const MCP_TOOL_CATALOG = {
|
|
16
|
+
artifact_write: additive("Write artifact"),
|
|
17
|
+
artifact_read: observation("Read artifact"),
|
|
18
|
+
artifact_list: observation("List artifacts"),
|
|
19
|
+
artifact_delete: mutation("Delete artifact"),
|
|
20
|
+
memory_read: observation("Read memory"),
|
|
21
|
+
memory_write: mutation("Write memory"),
|
|
22
|
+
memory_list: observation("List memory"),
|
|
23
|
+
memory_delete: mutation("Delete memory"),
|
|
24
|
+
memory_search: observation("Search memory"),
|
|
25
|
+
memory_extract: observation("Extract memory candidates", true),
|
|
26
|
+
memory_supersede: mutation("Supersede memory"),
|
|
27
|
+
memory_apply_reviewed: mutation("Apply reviewed memory"),
|
|
28
|
+
memory_invalidate_reviewed: mutation("Invalidate reviewed memory"),
|
|
29
|
+
memory_history: observation("Read memory history"),
|
|
30
|
+
memory_import: mutation("Import memory"),
|
|
31
|
+
domain_knowledge_query: observation("Query domain knowledge", true),
|
|
32
|
+
domain_knowledge_sync: mutation("Synchronize domain knowledge", true),
|
|
33
|
+
context_explore: observation("Explore repository context", true),
|
|
34
|
+
route_check: observation("Check route", true),
|
|
35
|
+
context_pack_list: observation("List context packs"),
|
|
36
|
+
context_pack_search: observation("Search context packs"),
|
|
37
|
+
context_pack_read: observation("Read context pack file"),
|
|
38
|
+
};
|
|
39
|
+
export const MCP_TOOL_NAMES = Object.freeze(Object.keys(MCP_TOOL_CATALOG));
|
|
40
|
+
export function isCairnToolName(value) {
|
|
41
|
+
return Object.hasOwn(MCP_TOOL_CATALOG, value);
|
|
42
|
+
}
|
|
43
|
+
export function metadataForTool(name) {
|
|
44
|
+
if (!isCairnToolName(name))
|
|
45
|
+
throw new Error(`MCP tool ${name} has no annotation contract.`);
|
|
46
|
+
return MCP_TOOL_CATALOG[name];
|
|
47
|
+
}
|