@cairnkeep/cli 1.0.5 → 1.1.1
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 +40 -0
- package/CONTRIBUTING.md +6 -1
- package/README.md +42 -0
- package/docs/building-an-overlay.md +7 -0
- package/docs/domain-knowledge.md +26 -10
- package/docs/operating.md +7 -3
- package/docs/privacy-and-data-flow.md +49 -0
- package/docs/releasing.md +33 -0
- package/docs/storage.md +55 -2
- 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/mcp-memory-server/dist/index.js +86 -35
- package/mcp-memory-server/package.json +2 -1
- package/package.json +5 -3
- package/scripts/probe-memory-server.mjs +17 -5
- package/scripts/smoke-mcp-boot.sh +6 -13
- package/scripts/test-package-install.sh +4 -0
- package/templates/env.example.template +6 -0
|
@@ -13,15 +13,18 @@ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/
|
|
|
13
13
|
import { z } from "zod";
|
|
14
14
|
import { EmbeddingCache, cosineSimilarity, embedTexts, getEmbeddingConfig, hashText, } from "./embeddings.js";
|
|
15
15
|
import { computeRepoState, exploreCacheKey, normalizeExploreQuery, readExploreCache, writeExploreCache, } from "./explore-cache.js";
|
|
16
|
+
class ClientContextError extends Error {
|
|
17
|
+
}
|
|
16
18
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
17
19
|
const infraRoot = resolve(moduleDir, "..", "..");
|
|
18
|
-
// Path to the
|
|
19
|
-
//
|
|
20
|
-
// (the integration scripts often live in the wrapper, not the core).
|
|
20
|
+
// Path to the bundled deployment-neutral sync script. An overlay can replace it
|
|
21
|
+
// with a deployment-specific implementation.
|
|
21
22
|
const anythingllmSyncScript = process.env.CAIRN_ANYTHINGLLM_SYNC_SCRIPT
|
|
22
23
|
? resolve(expandHome(process.env.CAIRN_ANYTHINGLLM_SYNC_SCRIPT))
|
|
23
|
-
: join(infraRoot, "anythingllm", "sync_to_anythingllm.py");
|
|
24
|
+
: join(infraRoot, "examples", "anythingllm", "sync_to_anythingllm.py");
|
|
24
25
|
const HISTORY_NAMESPACE = "__history__";
|
|
26
|
+
const PROJECT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
27
|
+
const WORKSPACE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
25
28
|
function expandHome(value) {
|
|
26
29
|
if (value === "~") {
|
|
27
30
|
return homedir();
|
|
@@ -86,9 +89,44 @@ function assertSafeScope(scope) {
|
|
|
86
89
|
throw new Error(`Invalid scope "${scope}": must be kebab-case (^[a-z0-9][a-z0-9-]*$), "project", or "all".`);
|
|
87
90
|
}
|
|
88
91
|
}
|
|
89
|
-
function
|
|
92
|
+
function parseHeaderList(headers, name, validate) {
|
|
93
|
+
const raw = headers.get(name)?.trim();
|
|
94
|
+
if (!raw) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
if (raw.length > 4096) {
|
|
98
|
+
throw new ClientContextError(`${name} is too long.`);
|
|
99
|
+
}
|
|
100
|
+
const values = Array.from(new Set(raw.split(",").map((value) => value.trim()).filter(Boolean)));
|
|
101
|
+
if (values.length > 32 || values.some((value) => !validate(value))) {
|
|
102
|
+
throw new ClientContextError(`${name} contains an invalid value.`);
|
|
103
|
+
}
|
|
104
|
+
return values;
|
|
105
|
+
}
|
|
106
|
+
function parseServerContext(headers) {
|
|
107
|
+
const rawProjectId = headers.get("x-cairn-project")?.trim();
|
|
108
|
+
if (rawProjectId && !PROJECT_ID_PATTERN.test(rawProjectId)) {
|
|
109
|
+
throw new ClientContextError("X-Cairn-Project must be a kebab-case identifier of at most 64 characters.");
|
|
110
|
+
}
|
|
111
|
+
const scopes = parseHeaderList(headers, "X-Cairn-Scopes", (value) => value !== "all" && (value === "project" || SCOPE_PATTERN.test(value)));
|
|
112
|
+
const anythingllmWorkspaces = parseHeaderList(headers, "X-Cairn-AnythingLLM-Workspaces", (value) => WORKSPACE_PATTERN.test(value));
|
|
113
|
+
const memoryConfig = scopes || anythingllmWorkspaces
|
|
114
|
+
? {
|
|
115
|
+
scopes: scopes ?? ["identity"],
|
|
116
|
+
anythingllm_workspaces: anythingllmWorkspaces ?? [],
|
|
117
|
+
}
|
|
118
|
+
: undefined;
|
|
119
|
+
return {
|
|
120
|
+
projectId: rawProjectId || undefined,
|
|
121
|
+
memoryConfig,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function resolveScopePath(scope, options = {}) {
|
|
90
125
|
if (scope === "project") {
|
|
91
|
-
|
|
126
|
+
if (options.projectId) {
|
|
127
|
+
return resolve(getBaseDir(), "projects", `${options.projectId}.db`);
|
|
128
|
+
}
|
|
129
|
+
return resolve(options.cwd ?? process.cwd(), ".agentfs", "project.db");
|
|
92
130
|
}
|
|
93
131
|
// "all" is a read-only virtual scope: memory_read/memory_search fan it out
|
|
94
132
|
// over the configured scopes (via getSearchScopes) before resolving. It has
|
|
@@ -123,8 +161,8 @@ function normalizeValue(value) {
|
|
|
123
161
|
}
|
|
124
162
|
return JSON.stringify(value);
|
|
125
163
|
}
|
|
126
|
-
async function openScope(scope, create,
|
|
127
|
-
const dbPath = resolveScopePath(scope,
|
|
164
|
+
async function openScope(scope, create, options = {}) {
|
|
165
|
+
const dbPath = resolveScopePath(scope, options);
|
|
128
166
|
if (!create && !existsSync(dbPath)) {
|
|
129
167
|
return null;
|
|
130
168
|
}
|
|
@@ -149,7 +187,7 @@ function visibleEntries(entries, includeHistory) {
|
|
|
149
187
|
return entries.filter((entry) => !isHistoryKey(entry.key));
|
|
150
188
|
}
|
|
151
189
|
async function listEntries(scope, prefix = "", options = {}) {
|
|
152
|
-
const agent = await openScope(scope, false, options
|
|
190
|
+
const agent = await openScope(scope, false, options);
|
|
153
191
|
if (!agent) {
|
|
154
192
|
return [];
|
|
155
193
|
}
|
|
@@ -165,8 +203,8 @@ async function listEntries(scope, prefix = "", options = {}) {
|
|
|
165
203
|
await agent.close();
|
|
166
204
|
}
|
|
167
205
|
}
|
|
168
|
-
async function readKey(scope, key) {
|
|
169
|
-
const agent = await openScope(scope, false);
|
|
206
|
+
async function readKey(scope, key, options = {}) {
|
|
207
|
+
const agent = await openScope(scope, false, options);
|
|
170
208
|
if (!agent) {
|
|
171
209
|
return [];
|
|
172
210
|
}
|
|
@@ -385,14 +423,16 @@ async function callAnythingLLM(workspace, query) {
|
|
|
385
423
|
function entryText(entry) {
|
|
386
424
|
return entry.value ? `${entry.key}\n${entry.value}` : entry.key;
|
|
387
425
|
}
|
|
388
|
-
function embeddingCachePath(scope) {
|
|
389
|
-
return join(getBaseDir(), ".embeddings", `${hashText(resolveScopePath(scope))}.json`);
|
|
426
|
+
function embeddingCachePath(scope, projectId) {
|
|
427
|
+
return join(getBaseDir(), ".embeddings", `${hashText(resolveScopePath(scope, { projectId }))}.json`);
|
|
390
428
|
}
|
|
391
|
-
async function semanticSearch(scope, query, topK, minScore) {
|
|
392
|
-
const config = getMemoryConfig();
|
|
429
|
+
async function semanticSearch(scope, query, topK, minScore, config = getMemoryConfig(), projectId) {
|
|
393
430
|
const scopes = getSearchScopes(scope, config);
|
|
394
431
|
const embeddingConfig = getEmbeddingConfig();
|
|
395
|
-
const perScopeEntries = await Promise.all(scopes.map(async (candidate) => ({
|
|
432
|
+
const perScopeEntries = await Promise.all(scopes.map(async (candidate) => ({
|
|
433
|
+
scope: candidate,
|
|
434
|
+
entries: await listEntries(candidate, "", { projectId }),
|
|
435
|
+
})));
|
|
396
436
|
const allEntries = perScopeEntries.flatMap((group) => group.entries);
|
|
397
437
|
if (allEntries.length === 0) {
|
|
398
438
|
return { results: [], mode: embeddingConfig ? "semantic" : "substring" };
|
|
@@ -410,7 +450,7 @@ async function semanticSearch(scope, query, topK, minScore) {
|
|
|
410
450
|
const caches = [];
|
|
411
451
|
const vectors = new Map();
|
|
412
452
|
for (const group of perScopeEntries) {
|
|
413
|
-
const cache = new EmbeddingCache(embeddingCachePath(group.scope), embeddingConfig.model);
|
|
453
|
+
const cache = new EmbeddingCache(embeddingCachePath(group.scope, projectId), embeddingConfig.model);
|
|
414
454
|
caches.push(cache);
|
|
415
455
|
const misses = [];
|
|
416
456
|
for (const entry of group.entries) {
|
|
@@ -670,8 +710,10 @@ function gitToplevel(cwd) {
|
|
|
670
710
|
// only allows one connected transport per server). All instances share the
|
|
671
711
|
// module-level helpers + AgentFS below. Enables a single long-lived process to
|
|
672
712
|
// serve many concurrent clients within one trusted server-side storage domain.
|
|
673
|
-
function createMemoryServer() {
|
|
713
|
+
function createMemoryServer(context = {}) {
|
|
674
714
|
const server = new McpServer({ name: "cairn-memory", version: "0.1.0" });
|
|
715
|
+
const memoryConfig = () => context.memoryConfig ?? getMemoryConfig();
|
|
716
|
+
const scopeOptions = { projectId: context.projectId };
|
|
675
717
|
server.registerTool("memory_read", {
|
|
676
718
|
description: "Read an exact key or search memory entries across AgentFS scopes.",
|
|
677
719
|
// Plain object schema — a .refine() wrapper (ZodEffects) makes the SDK
|
|
@@ -689,11 +731,11 @@ function createMemoryServer() {
|
|
|
689
731
|
if (Boolean(key) === Boolean(query)) {
|
|
690
732
|
throw new Error("Provide exactly one of key or query.");
|
|
691
733
|
}
|
|
692
|
-
const config =
|
|
734
|
+
const config = memoryConfig();
|
|
693
735
|
const scopes = getSearchScopes(scope, config);
|
|
694
736
|
const results = key
|
|
695
|
-
? (await Promise.all(scopes.map((candidate) => readKey(candidate, key)))).flat()
|
|
696
|
-
: searchEntries((await Promise.all(scopes.map((candidate) => listEntries(candidate)))).flat(), query ?? "");
|
|
737
|
+
? (await Promise.all(scopes.map((candidate) => readKey(candidate, key, scopeOptions)))).flat()
|
|
738
|
+
: searchEntries((await Promise.all(scopes.map((candidate) => listEntries(candidate, "", scopeOptions)))).flat(), query ?? "");
|
|
697
739
|
const sorted = results.sort((left, right) => {
|
|
698
740
|
return `${left.scope}:${left.key}`.localeCompare(`${right.scope}:${right.key}`);
|
|
699
741
|
});
|
|
@@ -722,7 +764,7 @@ function createMemoryServer() {
|
|
|
722
764
|
// disambiguated (rename keys, memory_history to recover).
|
|
723
765
|
const collisions = [];
|
|
724
766
|
for (const target of targets) {
|
|
725
|
-
const agent = await openScope(target, true);
|
|
767
|
+
const agent = await openScope(target, true, scopeOptions);
|
|
726
768
|
if (!agent) {
|
|
727
769
|
throw new Error(`Unable to open scope ${target}.`);
|
|
728
770
|
}
|
|
@@ -762,7 +804,7 @@ function createMemoryServer() {
|
|
|
762
804
|
idempotentHint: true,
|
|
763
805
|
},
|
|
764
806
|
}, async ({ scope, prefix }) => {
|
|
765
|
-
const entries = await listEntries(scope, prefix ?? "");
|
|
807
|
+
const entries = await listEntries(scope, prefix ?? "", scopeOptions);
|
|
766
808
|
const keys = entries.map((entry) => entry.key).sort();
|
|
767
809
|
return {
|
|
768
810
|
content: [{ type: "text", text: asToolText(keys) }],
|
|
@@ -776,7 +818,7 @@ function createMemoryServer() {
|
|
|
776
818
|
key: z.string().min(1),
|
|
777
819
|
}),
|
|
778
820
|
}, async ({ scope, key }) => {
|
|
779
|
-
const agent = await openScope(scope, false);
|
|
821
|
+
const agent = await openScope(scope, false, scopeOptions);
|
|
780
822
|
if (agent) {
|
|
781
823
|
try {
|
|
782
824
|
await agent.kv.delete(key);
|
|
@@ -802,7 +844,7 @@ function createMemoryServer() {
|
|
|
802
844
|
readOnlyHint: true,
|
|
803
845
|
},
|
|
804
846
|
}, async ({ scope, query, top_k, min_score }) => {
|
|
805
|
-
const { results, mode, model } = await semanticSearch(scope, query, top_k ?? 8, min_score ?? 0);
|
|
847
|
+
const { results, mode, model } = await semanticSearch(scope, query, top_k ?? 8, min_score ?? 0, memoryConfig(), context.projectId);
|
|
806
848
|
const payload = { mode, model, count: results.length, results };
|
|
807
849
|
return {
|
|
808
850
|
content: [{ type: "text", text: asToolText(payload) }],
|
|
@@ -842,7 +884,7 @@ function createMemoryServer() {
|
|
|
842
884
|
if (isHistoryKey(key)) {
|
|
843
885
|
throw new Error(`Keys under ${HISTORY_NAMESPACE}/ are reserved for memory history.`);
|
|
844
886
|
}
|
|
845
|
-
const agent = await openScope(scope, true);
|
|
887
|
+
const agent = await openScope(scope, true, scopeOptions);
|
|
846
888
|
if (!agent) {
|
|
847
889
|
throw new Error(`Unable to open scope ${scope}.`);
|
|
848
890
|
}
|
|
@@ -892,8 +934,11 @@ function createMemoryServer() {
|
|
|
892
934
|
idempotentHint: true,
|
|
893
935
|
},
|
|
894
936
|
}, async ({ scope, key }) => {
|
|
895
|
-
const current = await readKey(scope, key);
|
|
896
|
-
const history = (await listEntries(scope, historyPrefix(key), {
|
|
937
|
+
const current = await readKey(scope, key, scopeOptions);
|
|
938
|
+
const history = (await listEntries(scope, historyPrefix(key), {
|
|
939
|
+
includeHistory: true,
|
|
940
|
+
...scopeOptions,
|
|
941
|
+
}))
|
|
897
942
|
.sort((left, right) => left.key.localeCompare(right.key));
|
|
898
943
|
const payload = {
|
|
899
944
|
scope,
|
|
@@ -909,17 +954,21 @@ function createMemoryServer() {
|
|
|
909
954
|
server.registerTool("domain_knowledge_query", {
|
|
910
955
|
description: "Query an AnythingLLM workspace in query mode for domain knowledge.",
|
|
911
956
|
inputSchema: z.object({
|
|
912
|
-
workspace: z.string().min(1),
|
|
957
|
+
workspace: z.string().min(1).optional(),
|
|
913
958
|
query: z.string().min(1),
|
|
914
959
|
}),
|
|
915
960
|
annotations: {
|
|
916
961
|
readOnlyHint: true,
|
|
917
962
|
},
|
|
918
963
|
}, async ({ workspace, query }) => {
|
|
919
|
-
const
|
|
964
|
+
const workspaceSlug = workspace ?? defaultAnythingLLMWorkspace(memoryConfig());
|
|
965
|
+
if (!workspaceSlug) {
|
|
966
|
+
throw new Error("No AnythingLLM workspace provided and no project workspace found in memory config.");
|
|
967
|
+
}
|
|
968
|
+
const answer = await callAnythingLLM(workspaceSlug, query);
|
|
920
969
|
return {
|
|
921
970
|
content: [{ type: "text", text: answer }],
|
|
922
|
-
structuredContent: { workspace, answer },
|
|
971
|
+
structuredContent: { workspace: workspaceSlug, answer },
|
|
923
972
|
};
|
|
924
973
|
});
|
|
925
974
|
server.registerTool("domain_knowledge_sync", {
|
|
@@ -932,7 +981,7 @@ function createMemoryServer() {
|
|
|
932
981
|
}),
|
|
933
982
|
}, async ({ workspace, mode, confirm_replace, timeout_seconds }) => {
|
|
934
983
|
const syncMode = mode ?? "incremental";
|
|
935
|
-
const config =
|
|
984
|
+
const config = memoryConfig();
|
|
936
985
|
const workspaceSlug = workspace ?? defaultAnythingLLMWorkspace(config);
|
|
937
986
|
if (!workspaceSlug) {
|
|
938
987
|
throw new Error("No AnythingLLM workspace provided and no project workspace found in memory config.");
|
|
@@ -1183,12 +1232,13 @@ if (httpPort > 0) {
|
|
|
1183
1232
|
}
|
|
1184
1233
|
// New session (first request = initialize). The transport mints a session
|
|
1185
1234
|
// id and the SDK returns it via the response header; client echoes it back.
|
|
1235
|
+
const context = parseServerContext(request.headers);
|
|
1186
1236
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
1187
1237
|
sessionIdGenerator: () => randomUUID(),
|
|
1188
1238
|
onsessioninitialized: (id) => { sessions.set(id, transport); },
|
|
1189
1239
|
onsessionclosed: (id) => { sessions.delete(id); },
|
|
1190
1240
|
});
|
|
1191
|
-
const session = createMemoryServer();
|
|
1241
|
+
const session = createMemoryServer(context);
|
|
1192
1242
|
await session.connect(transport);
|
|
1193
1243
|
return transport.handleRequest(request);
|
|
1194
1244
|
};
|
|
@@ -1198,7 +1248,7 @@ if (httpPort > 0) {
|
|
|
1198
1248
|
res.setHeader("Access-Control-Allow-Origin", allowOrigin);
|
|
1199
1249
|
res.setHeader("Vary", "Origin");
|
|
1200
1250
|
res.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, OPTIONS");
|
|
1201
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, mcp-session-id, Accept, Authorization");
|
|
1251
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, mcp-session-id, Accept, Authorization, X-Cairn-Project, X-Cairn-Scopes, X-Cairn-AnythingLLM-Workspaces");
|
|
1202
1252
|
}
|
|
1203
1253
|
if (req.method === "OPTIONS") {
|
|
1204
1254
|
res.writeHead(allowOrigin ? 204 : 403).end();
|
|
@@ -1235,7 +1285,8 @@ if (httpPort > 0) {
|
|
|
1235
1285
|
res.end(Buffer.from(await response.arrayBuffer()));
|
|
1236
1286
|
}
|
|
1237
1287
|
catch (err) {
|
|
1238
|
-
|
|
1288
|
+
const status = err instanceof ClientContextError ? 400 : 500;
|
|
1289
|
+
res.writeHead(status).end(err instanceof Error ? err.message : String(err));
|
|
1239
1290
|
}
|
|
1240
1291
|
});
|
|
1241
1292
|
httpServer.listen(httpPort, httpHost, () => {
|
|
@@ -14,12 +14,13 @@
|
|
|
14
14
|
"check:search": "node scripts/smoke-search-e2e.mjs",
|
|
15
15
|
"check:scope-guard": "node scripts/smoke-scope-guard.mjs",
|
|
16
16
|
"check:http-guard": "node scripts/smoke-http-guard.mjs",
|
|
17
|
+
"check:remote-context": "node scripts/smoke-remote-context.mjs",
|
|
17
18
|
"check:explore-guard": "node scripts/smoke-explore-guard.mjs",
|
|
18
19
|
"check:explore-cache": "node scripts/smoke-explore-cache.mjs",
|
|
19
20
|
"check:explore-crossref": "node scripts/smoke-explore-crossref.mjs",
|
|
20
21
|
"check:route-guard": "node scripts/smoke-route-guard.mjs",
|
|
21
22
|
"test": "npm run test:smoke",
|
|
22
|
-
"test:smoke": "npm run build && npm run check:embeddings && npm run check:extract && npm run check:scope-guard && npm run check:http-guard && npm run check:explore-guard && npm run check:explore-cache && npm run check:explore-crossref && npm run check:route-guard",
|
|
23
|
+
"test:smoke": "npm run build && npm run check:embeddings && npm run check:extract && npm run check:scope-guard && npm run check:http-guard && npm run check:remote-context && npm run check:explore-guard && npm run check:explore-cache && npm run check:explore-crossref && npm run check:route-guard",
|
|
23
24
|
"test:search": "npm run build && npm run check:search",
|
|
24
25
|
"test:memory": "npm run build && npm run check:embeddings && npm run check:extract && npm run check:search",
|
|
25
26
|
"test:all": "npm run test:memory"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cairnkeep/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "A durable, harness-agnostic memory + context layer for coding agents (Claude Code, OpenCode).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
"templates/",
|
|
17
17
|
"claude/",
|
|
18
18
|
"opencode/",
|
|
19
|
+
"examples/anythingllm/",
|
|
19
20
|
"docs/",
|
|
21
|
+
"CHANGELOG.md",
|
|
20
22
|
"CONTRIBUTING.md",
|
|
21
23
|
"SECURITY.md",
|
|
22
24
|
"mcp-memory-server/dist/",
|
|
@@ -36,8 +38,8 @@
|
|
|
36
38
|
"fix:perms": "git ls-files -s | awk '$1==\"100755\"{print $4}' | xargs chmod 755",
|
|
37
39
|
"prepack": "npm run build:server && npm run fix:perms",
|
|
38
40
|
"test": "for test in scripts/test-*.sh; do \"$test\" || exit 1; done",
|
|
39
|
-
"check:public": "scripts/verify-no-private-references.sh && scripts/verify-docs-parity.sh && npm test",
|
|
40
|
-
"prepublishOnly": "npm run check:public && npm
|
|
41
|
+
"check:public": "scripts/verify-no-private-references.sh && scripts/verify-docs-parity.sh && npm run build:server && npm test",
|
|
42
|
+
"prepublishOnly": "npm run check:public && npm --prefix mcp-memory-server test --global=false --dry-run=false"
|
|
41
43
|
},
|
|
42
44
|
"repository": {
|
|
43
45
|
"type": "git",
|
|
@@ -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
|
|
@@ -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
|