@cairnkeep/cli 1.1.0 → 1.1.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 +49 -0
- package/CONTRIBUTING.md +6 -1
- package/README.md +43 -0
- package/docs/building-an-overlay.md +9 -0
- package/docs/domain-knowledge.md +14 -10
- package/docs/operating.md +2 -0
- package/docs/overlay-distributions.md +97 -0
- package/docs/privacy-and-data-flow.md +49 -0
- package/docs/releasing.md +33 -0
- package/docs/storage.md +24 -0
- package/examples/anythingllm/README.md +66 -0
- package/examples/anythingllm/anythingllm-projects.example.json +30 -0
- package/examples/anythingllm/sync_to_anythingllm.py +596 -0
- package/examples/overlay-distribution/cairnkeep.overlay.json +25 -0
- package/mcp-memory-server/dist/index.js +3 -4
- package/package.json +7 -3
- package/schemas/cairnkeep-overlay.schema.json +79 -0
- package/scripts/probe-memory-server.mjs +17 -5
- package/scripts/smoke-mcp-boot.sh +6 -13
- package/scripts/test-overlay-schema.sh +41 -0
- package/scripts/test-package-install.sh +4 -0
- package/templates/env.example.template +6 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/cairnkeep/cairnkeep/main/schemas/cairnkeep-overlay.schema.json",
|
|
4
|
+
"title": "Cairnkeep distribution profile",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "id", "name", "version", "core", "policy", "bootstrap"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schemaVersion": {"const": 1},
|
|
10
|
+
"id": {"type": "string", "pattern": "^[a-z][a-z0-9-]{1,63}$"},
|
|
11
|
+
"name": {"type": "string", "minLength": 1, "maxLength": 120},
|
|
12
|
+
"version": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$"
|
|
15
|
+
},
|
|
16
|
+
"core": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"additionalProperties": false,
|
|
19
|
+
"required": ["package", "version"],
|
|
20
|
+
"properties": {
|
|
21
|
+
"package": {"const": "@cairnkeep/cli"},
|
|
22
|
+
"version": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"policy": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"additionalProperties": false,
|
|
31
|
+
"required": ["memory", "rag", "gitProvider"],
|
|
32
|
+
"properties": {
|
|
33
|
+
"memory": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"additionalProperties": false,
|
|
36
|
+
"required": ["transport", "storage"],
|
|
37
|
+
"properties": {
|
|
38
|
+
"transport": {"enum": ["stdio", "http"]},
|
|
39
|
+
"storage": {"enum": ["local", "remote"]},
|
|
40
|
+
"baseDir": {"type": "string", "minLength": 1},
|
|
41
|
+
"endpoint": {"type": "string", "format": "uri"}
|
|
42
|
+
},
|
|
43
|
+
"allOf": [
|
|
44
|
+
{
|
|
45
|
+
"if": {"properties": {"storage": {"const": "local"}}},
|
|
46
|
+
"then": {"required": ["baseDir"]}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"if": {"properties": {"storage": {"const": "remote"}}},
|
|
50
|
+
"then": {"required": ["endpoint"]}
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"rag": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"additionalProperties": false,
|
|
57
|
+
"required": ["mode"],
|
|
58
|
+
"properties": {
|
|
59
|
+
"mode": {"enum": ["disabled", "local", "remote"]},
|
|
60
|
+
"baseUrl": {"type": "string", "format": "uri"}
|
|
61
|
+
},
|
|
62
|
+
"if": {"properties": {"mode": {"enum": ["local", "remote"]}}},
|
|
63
|
+
"then": {"required": ["baseUrl"]}
|
|
64
|
+
},
|
|
65
|
+
"gitProvider": {"type": "string", "minLength": 1}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"bootstrap": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"additionalProperties": false,
|
|
71
|
+
"required": ["defaultMode", "launcher", "profileLock"],
|
|
72
|
+
"properties": {
|
|
73
|
+
"defaultMode": {"enum": ["tracked", "untracked"]},
|
|
74
|
+
"launcher": {"type": "string", "minLength": 1},
|
|
75
|
+
"profileLock": {"type": "string", "minLength": 1}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
if (
|
|
6
|
-
console.error("usage: probe-memory-server.mjs PATH_TO_SERVER");
|
|
4
|
+
const probeArgs = process.argv.slice(2);
|
|
5
|
+
if (probeArgs.length === 0) {
|
|
6
|
+
console.error("usage: probe-memory-server.mjs PATH_TO_SERVER | --command COMMAND [ARGS...]");
|
|
7
7
|
process.exit(2);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const commandMode = probeArgs[0] === "--command";
|
|
11
|
+
const command = commandMode ? probeArgs[1] : process.execPath;
|
|
12
|
+
const commandArgs = commandMode ? probeArgs.slice(2) : [probeArgs[0]];
|
|
13
|
+
if (!command) {
|
|
14
|
+
console.error("probe-memory-server.mjs: --command requires a command");
|
|
15
|
+
process.exit(2);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const child = spawn(command, commandArgs, {
|
|
11
19
|
env: process.env,
|
|
12
20
|
stdio: ["pipe", "pipe", "pipe"],
|
|
13
21
|
});
|
|
@@ -29,7 +37,11 @@ const response = new Promise((resolve, reject) => {
|
|
|
29
37
|
if (!line.trim()) continue;
|
|
30
38
|
try {
|
|
31
39
|
const message = JSON.parse(line);
|
|
32
|
-
if (
|
|
40
|
+
if (
|
|
41
|
+
message.id === 1 &&
|
|
42
|
+
message.result?.serverInfo?.name === "cairn-memory" &&
|
|
43
|
+
message.result?.capabilities?.tools
|
|
44
|
+
) {
|
|
33
45
|
resolve();
|
|
34
46
|
return;
|
|
35
47
|
}
|
|
@@ -5,35 +5,28 @@
|
|
|
5
5
|
# transport works. This is the core product path, and nothing else tests it
|
|
6
6
|
# after packaging/installation.
|
|
7
7
|
#
|
|
8
|
-
# Prefers `cairn memory-server` (a global install), else falls back to
|
|
9
|
-
#
|
|
8
|
+
# Prefers `cairn memory-server` (a global install), else falls back to the
|
|
9
|
+
# repository build. The Node probe owns the timeout so this works on macOS,
|
|
10
|
+
# which does not ship GNU `timeout`.
|
|
10
11
|
set -uo pipefail
|
|
11
12
|
|
|
12
13
|
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
13
14
|
|
|
14
15
|
if command -v cairn >/dev/null 2>&1; then
|
|
15
|
-
|
|
16
|
+
probe=(node "$ROOT_DIR/scripts/probe-memory-server.mjs" --command cairn memory-server)
|
|
16
17
|
elif [[ -f "$ROOT_DIR/mcp-memory-server/dist/index.js" ]]; then
|
|
17
|
-
|
|
18
|
+
probe=(node "$ROOT_DIR/scripts/probe-memory-server.mjs" "$ROOT_DIR/mcp-memory-server/dist/index.js")
|
|
18
19
|
else
|
|
19
20
|
echo "smoke-mcp-boot: FAIL (no cairn on PATH and no dist build to run)"
|
|
20
21
|
exit 1
|
|
21
22
|
fi
|
|
22
23
|
|
|
23
|
-
req='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"ci","version":"0"}}}'
|
|
24
|
-
|
|
25
24
|
tmp=$(mktemp -d)
|
|
26
25
|
trap 'rm -rf "$tmp"' EXIT
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
# lands there; `head -1` closes stdin after the first reply, so the server sees
|
|
30
|
-
# EOF and exits cleanly.
|
|
31
|
-
out=$(printf '%s\n' "$req" | HOME="$tmp" timeout 15 "${server[@]}" 2>/dev/null | head -1)
|
|
32
|
-
|
|
33
|
-
if grep -q '"result"' <<<"$out" && grep -q '"tools"' <<<"$out"; then
|
|
27
|
+
if HOME="$tmp" "${probe[@]}"; then
|
|
34
28
|
echo "smoke-mcp-boot: OK (server booted, initialize returned tools capability)"
|
|
35
29
|
exit 0
|
|
36
30
|
fi
|
|
37
31
|
echo "smoke-mcp-boot: FAIL (no valid initialize response)"
|
|
38
|
-
echo " got: ${out:0:200}"
|
|
39
32
|
exit 1
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
5
|
+
|
|
6
|
+
node --input-type=module - "$ROOT" <<'NODE'
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
const root = process.argv[2];
|
|
11
|
+
const schema = JSON.parse(fs.readFileSync(path.join(root, "schemas/cairnkeep-overlay.schema.json"), "utf8"));
|
|
12
|
+
const example = JSON.parse(fs.readFileSync(path.join(root, "examples/overlay-distribution/cairnkeep.overlay.json"), "utf8"));
|
|
13
|
+
|
|
14
|
+
if (schema.$schema !== "https://json-schema.org/draft/2020-12/schema") throw new Error("unexpected schema draft");
|
|
15
|
+
if (schema.properties.schemaVersion.const !== 1) throw new Error("unexpected schema version");
|
|
16
|
+
for (const field of schema.required) {
|
|
17
|
+
if (!(field in example)) throw new Error(`example missing required field: ${field}`);
|
|
18
|
+
}
|
|
19
|
+
if (example.core.package !== "@cairnkeep/cli") throw new Error("example must consume the public core");
|
|
20
|
+
if (example.policy.memory.storage !== "local") throw new Error("example must remain local-first");
|
|
21
|
+
if (example.policy.memory.transport !== "stdio") throw new Error("example must remain local-first");
|
|
22
|
+
NODE
|
|
23
|
+
|
|
24
|
+
pack_manifest=$(mktemp)
|
|
25
|
+
trap 'rm -f "$pack_manifest"' EXIT
|
|
26
|
+
npm --prefix "$ROOT" pack --dry-run --json --ignore-scripts > "$pack_manifest"
|
|
27
|
+
node --input-type=module - "$pack_manifest" <<'NODE'
|
|
28
|
+
import fs from "node:fs";
|
|
29
|
+
|
|
30
|
+
const manifest = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
31
|
+
const files = new Set(manifest[0].files.map((item) => item.path));
|
|
32
|
+
for (const required of [
|
|
33
|
+
"schemas/cairnkeep-overlay.schema.json",
|
|
34
|
+
"examples/overlay-distribution/cairnkeep.overlay.json",
|
|
35
|
+
"docs/overlay-distributions.md",
|
|
36
|
+
]) {
|
|
37
|
+
if (!files.has(required)) throw new Error(`npm package missing ${required}`);
|
|
38
|
+
}
|
|
39
|
+
NODE
|
|
40
|
+
|
|
41
|
+
echo "overlay schema checks passed"
|
|
@@ -29,4 +29,8 @@ bootstrap_output=$(cairn bootstrap "$tmp/project")
|
|
|
29
29
|
|
|
30
30
|
(cd "$tmp/project" && cairn doctor) >/dev/null || fail "installed package failed cairn doctor"
|
|
31
31
|
|
|
32
|
+
installed_root="$tmp/prefix/lib/node_modules/@cairnkeep/cli"
|
|
33
|
+
[[ -f "$installed_root/examples/anythingllm/sync_to_anythingllm.py" ]] || \
|
|
34
|
+
fail "npm tarball omitted the default AnythingLLM sync script"
|
|
35
|
+
|
|
32
36
|
echo "PASS: npm tarball installs a self-contained CLI and MCP server"
|
|
@@ -27,3 +27,9 @@
|
|
|
27
27
|
# Optional: comma-separated allowed Host headers for DNS-rebinding protection
|
|
28
28
|
# (default: the bind host + localhost on the chosen port).
|
|
29
29
|
# CAIRN_MEMORY_HTTP_ALLOWED_HOSTS=
|
|
30
|
+
#
|
|
31
|
+
# Optional document-RAG sync. API credentials and endpoint may be local or remote.
|
|
32
|
+
# ANYTHINGLLM_BASE_URL=http://localhost:3001
|
|
33
|
+
# ANYTHINGLLM_API_KEY=
|
|
34
|
+
# CAIRN_ANYTHINGLLM_PROJECTS_FILE=~/.config/cairnkeep/anythingllm-projects.json
|
|
35
|
+
# CAIRN_ANYTHINGLLM_STATE_FILE=~/.local/state/cairnkeep/anythingllm-sync.json
|