@cairnkeep/cli 1.0.5 → 1.1.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.
|
@@ -107,6 +107,13 @@ All optional, all env-gated — a wrapper turns them on by setting the env:
|
|
|
107
107
|
Point the RAG sync script at your own copy (`CAIRN_ANYTHINGLLM_SYNC_SCRIPT`) so the
|
|
108
108
|
integration scripts live in your overlay, not the core.
|
|
109
109
|
|
|
110
|
+
For a fleet that shares one trusted HTTP service, have the overlay generate
|
|
111
|
+
project-local Claude Code and OpenCode MCP entries with `X-Cairn-Project`,
|
|
112
|
+
`X-Cairn-Scopes`, and `X-Cairn-AnythingLLM-Workspaces`. This preserves local
|
|
113
|
+
project isolation and workspace defaults while keeping the databases and RAG
|
|
114
|
+
credentials on the server host. Keep bearer tokens in environment-backed
|
|
115
|
+
secret storage, not generated project files.
|
|
116
|
+
|
|
110
117
|
## Checklist
|
|
111
118
|
|
|
112
119
|
- [ ] cairnkeep pinned to a version; no core files copied or patched
|
package/docs/domain-knowledge.md
CHANGED
|
@@ -45,6 +45,18 @@ first of `.agent/memory.json`, `.opencode/memory.json`, `.claude/memory.json`, o
|
|
|
45
45
|
configured workspace that is not `engineering-patterns` (the conventional shared
|
|
46
46
|
workspace). With no workspace given and none configured, it errors.
|
|
47
47
|
|
|
48
|
+
In local stdio mode, that config is read from the client project's working
|
|
49
|
+
directory. In remote HTTP mode, the server cannot read files from the client
|
|
50
|
+
PC. Configure `ANYTHINGLLM_BASE_URL`, `ANYTHINGLLM_API_KEY`, and any sync script
|
|
51
|
+
on the HTTP server host, then send the project workspaces in the
|
|
52
|
+
`X-Cairn-AnythingLLM-Workspaces` session header. See
|
|
53
|
+
[Memory storage and deployment](storage.md#per-project-remote-sessions).
|
|
54
|
+
|
|
55
|
+
The HTTP server performs queries and runs the sync script on its own host. A
|
|
56
|
+
remote sync script must therefore obtain project documents from a server-side
|
|
57
|
+
checkout, object store, or git host; it cannot read an arbitrary client PC's
|
|
58
|
+
working tree.
|
|
59
|
+
|
|
48
60
|
## Setup (query)
|
|
49
61
|
|
|
50
62
|
```bash
|
package/docs/operating.md
CHANGED
|
@@ -267,9 +267,11 @@ is guarded and **fails closed**:
|
|
|
267
267
|
Requests without a valid bearer token get `401`; requests with an unexpected
|
|
268
268
|
`Host` header get `403`. Keep HTTP mode bound to `127.0.0.1` unless you have a
|
|
269
269
|
specific reason to expose it, and use a long random token. HTTP mode has no
|
|
270
|
-
per-user ACL or tenant isolation
|
|
271
|
-
|
|
272
|
-
|
|
270
|
+
per-user ACL or tenant isolation. Clients may bind sessions to separate project
|
|
271
|
+
databases with validated `X-Cairn-Project` routing metadata, but that metadata
|
|
272
|
+
is not an authorization boundary. See [Memory storage and deployment](storage.md)
|
|
273
|
+
for the placement rules, client registration, TLS requirements, project headers,
|
|
274
|
+
and backup boundaries.
|
|
273
275
|
|
|
274
276
|
## The workflow
|
|
275
277
|
|
package/docs/storage.md
CHANGED
|
@@ -53,12 +53,34 @@ With this topology, the databases are on the server host, not the client PC.
|
|
|
53
53
|
Setting `CAIRN_AGENTFS_BASE_DIR` on the client does not redirect a remote
|
|
54
54
|
server; set it in the server service environment.
|
|
55
55
|
|
|
56
|
+
### Per-project remote sessions
|
|
57
|
+
|
|
58
|
+
Remote clients can bind an MCP session to a stable project identity and send
|
|
59
|
+
the memory configuration that a local server would read from `memory.json`:
|
|
60
|
+
|
|
61
|
+
| Header | Purpose |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `X-Cairn-Project` | Kebab-case project identity, up to 64 characters |
|
|
64
|
+
| `X-Cairn-Scopes` | Comma-separated scopes used when a tool reads scope `all` |
|
|
65
|
+
| `X-Cairn-AnythingLLM-Workspaces` | Comma-separated AnythingLLM workspace slugs; the first non-`engineering-patterns` workspace is the default |
|
|
66
|
+
|
|
67
|
+
When `X-Cairn-Project` is present, `project` scope is stored at
|
|
68
|
+
`${CAIRN_AGENTFS_BASE_DIR}/projects/<project-id>.db`. Sessions with different
|
|
69
|
+
project identities therefore do not share project memory. Without the header,
|
|
70
|
+
HTTP mode retains the legacy behavior and resolves `project` from the server
|
|
71
|
+
working directory.
|
|
72
|
+
|
|
73
|
+
These headers are session routing metadata, not authorization. The bearer
|
|
74
|
+
token still grants access to the entire server, including the ability to choose
|
|
75
|
+
another valid project identity. Use separate server instances for separate
|
|
76
|
+
trust domains.
|
|
77
|
+
|
|
56
78
|
HTTP mode is one trusted storage domain:
|
|
57
79
|
|
|
58
80
|
- One bearer token grants access to every exposed memory tool.
|
|
59
81
|
- There is no per-user ACL, tenant isolation, or client-specific filesystem.
|
|
60
|
-
- `
|
|
61
|
-
|
|
82
|
+
- Sessions without `X-Cairn-Project` share the project database resolved from
|
|
83
|
+
the server process's working directory.
|
|
62
84
|
- Use one server instance per isolation boundary. Do not offer one instance to
|
|
63
85
|
mutually untrusted users.
|
|
64
86
|
- Keep the Cairnkeep listener on loopback behind a TLS reverse proxy, or use an
|
|
@@ -132,3 +154,10 @@ Ensure the secret-manager bootstrap exports `CAIRN_MEMORY_HTTP_TOKEN` before
|
|
|
132
154
|
OpenCode starts. Preserve any other keys already present in `opencode.json`
|
|
133
155
|
rather than replacing the whole file. The URL, token, private host names, and
|
|
134
156
|
device-specific configuration must never be committed to this public project.
|
|
157
|
+
|
|
158
|
+
For distinct project memory, install a project-local MCP entry with the same
|
|
159
|
+
URL and authorization header plus the three routing headers. Claude Code
|
|
160
|
+
expands `${VAR}` references in project `.mcp.json` URL and header values;
|
|
161
|
+
OpenCode uses `{env:VAR}` references. A private overlay should generate and
|
|
162
|
+
merge these files so secrets remain in the process environment rather than the
|
|
163
|
+
repository.
|
|
@@ -13,6 +13,8 @@ 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
20
|
// Path to the domain-knowledge sync script. Defaults to a script bundled next to
|
|
@@ -22,6 +24,8 @@ const anythingllmSyncScript = process.env.CAIRN_ANYTHINGLLM_SYNC_SCRIPT
|
|
|
22
24
|
? resolve(expandHome(process.env.CAIRN_ANYTHINGLLM_SYNC_SCRIPT))
|
|
23
25
|
: join(infraRoot, "anythingllm", "sync_to_anythingllm.py");
|
|
24
26
|
const HISTORY_NAMESPACE = "__history__";
|
|
27
|
+
const PROJECT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
28
|
+
const WORKSPACE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
25
29
|
function expandHome(value) {
|
|
26
30
|
if (value === "~") {
|
|
27
31
|
return homedir();
|
|
@@ -86,9 +90,44 @@ function assertSafeScope(scope) {
|
|
|
86
90
|
throw new Error(`Invalid scope "${scope}": must be kebab-case (^[a-z0-9][a-z0-9-]*$), "project", or "all".`);
|
|
87
91
|
}
|
|
88
92
|
}
|
|
89
|
-
function
|
|
93
|
+
function parseHeaderList(headers, name, validate) {
|
|
94
|
+
const raw = headers.get(name)?.trim();
|
|
95
|
+
if (!raw) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
if (raw.length > 4096) {
|
|
99
|
+
throw new ClientContextError(`${name} is too long.`);
|
|
100
|
+
}
|
|
101
|
+
const values = Array.from(new Set(raw.split(",").map((value) => value.trim()).filter(Boolean)));
|
|
102
|
+
if (values.length > 32 || values.some((value) => !validate(value))) {
|
|
103
|
+
throw new ClientContextError(`${name} contains an invalid value.`);
|
|
104
|
+
}
|
|
105
|
+
return values;
|
|
106
|
+
}
|
|
107
|
+
function parseServerContext(headers) {
|
|
108
|
+
const rawProjectId = headers.get("x-cairn-project")?.trim();
|
|
109
|
+
if (rawProjectId && !PROJECT_ID_PATTERN.test(rawProjectId)) {
|
|
110
|
+
throw new ClientContextError("X-Cairn-Project must be a kebab-case identifier of at most 64 characters.");
|
|
111
|
+
}
|
|
112
|
+
const scopes = parseHeaderList(headers, "X-Cairn-Scopes", (value) => value !== "all" && (value === "project" || SCOPE_PATTERN.test(value)));
|
|
113
|
+
const anythingllmWorkspaces = parseHeaderList(headers, "X-Cairn-AnythingLLM-Workspaces", (value) => WORKSPACE_PATTERN.test(value));
|
|
114
|
+
const memoryConfig = scopes || anythingllmWorkspaces
|
|
115
|
+
? {
|
|
116
|
+
scopes: scopes ?? ["identity"],
|
|
117
|
+
anythingllm_workspaces: anythingllmWorkspaces ?? [],
|
|
118
|
+
}
|
|
119
|
+
: undefined;
|
|
120
|
+
return {
|
|
121
|
+
projectId: rawProjectId || undefined,
|
|
122
|
+
memoryConfig,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function resolveScopePath(scope, options = {}) {
|
|
90
126
|
if (scope === "project") {
|
|
91
|
-
|
|
127
|
+
if (options.projectId) {
|
|
128
|
+
return resolve(getBaseDir(), "projects", `${options.projectId}.db`);
|
|
129
|
+
}
|
|
130
|
+
return resolve(options.cwd ?? process.cwd(), ".agentfs", "project.db");
|
|
92
131
|
}
|
|
93
132
|
// "all" is a read-only virtual scope: memory_read/memory_search fan it out
|
|
94
133
|
// over the configured scopes (via getSearchScopes) before resolving. It has
|
|
@@ -123,8 +162,8 @@ function normalizeValue(value) {
|
|
|
123
162
|
}
|
|
124
163
|
return JSON.stringify(value);
|
|
125
164
|
}
|
|
126
|
-
async function openScope(scope, create,
|
|
127
|
-
const dbPath = resolveScopePath(scope,
|
|
165
|
+
async function openScope(scope, create, options = {}) {
|
|
166
|
+
const dbPath = resolveScopePath(scope, options);
|
|
128
167
|
if (!create && !existsSync(dbPath)) {
|
|
129
168
|
return null;
|
|
130
169
|
}
|
|
@@ -149,7 +188,7 @@ function visibleEntries(entries, includeHistory) {
|
|
|
149
188
|
return entries.filter((entry) => !isHistoryKey(entry.key));
|
|
150
189
|
}
|
|
151
190
|
async function listEntries(scope, prefix = "", options = {}) {
|
|
152
|
-
const agent = await openScope(scope, false, options
|
|
191
|
+
const agent = await openScope(scope, false, options);
|
|
153
192
|
if (!agent) {
|
|
154
193
|
return [];
|
|
155
194
|
}
|
|
@@ -165,8 +204,8 @@ async function listEntries(scope, prefix = "", options = {}) {
|
|
|
165
204
|
await agent.close();
|
|
166
205
|
}
|
|
167
206
|
}
|
|
168
|
-
async function readKey(scope, key) {
|
|
169
|
-
const agent = await openScope(scope, false);
|
|
207
|
+
async function readKey(scope, key, options = {}) {
|
|
208
|
+
const agent = await openScope(scope, false, options);
|
|
170
209
|
if (!agent) {
|
|
171
210
|
return [];
|
|
172
211
|
}
|
|
@@ -385,14 +424,16 @@ async function callAnythingLLM(workspace, query) {
|
|
|
385
424
|
function entryText(entry) {
|
|
386
425
|
return entry.value ? `${entry.key}\n${entry.value}` : entry.key;
|
|
387
426
|
}
|
|
388
|
-
function embeddingCachePath(scope) {
|
|
389
|
-
return join(getBaseDir(), ".embeddings", `${hashText(resolveScopePath(scope))}.json`);
|
|
427
|
+
function embeddingCachePath(scope, projectId) {
|
|
428
|
+
return join(getBaseDir(), ".embeddings", `${hashText(resolveScopePath(scope, { projectId }))}.json`);
|
|
390
429
|
}
|
|
391
|
-
async function semanticSearch(scope, query, topK, minScore) {
|
|
392
|
-
const config = getMemoryConfig();
|
|
430
|
+
async function semanticSearch(scope, query, topK, minScore, config = getMemoryConfig(), projectId) {
|
|
393
431
|
const scopes = getSearchScopes(scope, config);
|
|
394
432
|
const embeddingConfig = getEmbeddingConfig();
|
|
395
|
-
const perScopeEntries = await Promise.all(scopes.map(async (candidate) => ({
|
|
433
|
+
const perScopeEntries = await Promise.all(scopes.map(async (candidate) => ({
|
|
434
|
+
scope: candidate,
|
|
435
|
+
entries: await listEntries(candidate, "", { projectId }),
|
|
436
|
+
})));
|
|
396
437
|
const allEntries = perScopeEntries.flatMap((group) => group.entries);
|
|
397
438
|
if (allEntries.length === 0) {
|
|
398
439
|
return { results: [], mode: embeddingConfig ? "semantic" : "substring" };
|
|
@@ -410,7 +451,7 @@ async function semanticSearch(scope, query, topK, minScore) {
|
|
|
410
451
|
const caches = [];
|
|
411
452
|
const vectors = new Map();
|
|
412
453
|
for (const group of perScopeEntries) {
|
|
413
|
-
const cache = new EmbeddingCache(embeddingCachePath(group.scope), embeddingConfig.model);
|
|
454
|
+
const cache = new EmbeddingCache(embeddingCachePath(group.scope, projectId), embeddingConfig.model);
|
|
414
455
|
caches.push(cache);
|
|
415
456
|
const misses = [];
|
|
416
457
|
for (const entry of group.entries) {
|
|
@@ -670,8 +711,10 @@ function gitToplevel(cwd) {
|
|
|
670
711
|
// only allows one connected transport per server). All instances share the
|
|
671
712
|
// module-level helpers + AgentFS below. Enables a single long-lived process to
|
|
672
713
|
// serve many concurrent clients within one trusted server-side storage domain.
|
|
673
|
-
function createMemoryServer() {
|
|
714
|
+
function createMemoryServer(context = {}) {
|
|
674
715
|
const server = new McpServer({ name: "cairn-memory", version: "0.1.0" });
|
|
716
|
+
const memoryConfig = () => context.memoryConfig ?? getMemoryConfig();
|
|
717
|
+
const scopeOptions = { projectId: context.projectId };
|
|
675
718
|
server.registerTool("memory_read", {
|
|
676
719
|
description: "Read an exact key or search memory entries across AgentFS scopes.",
|
|
677
720
|
// Plain object schema — a .refine() wrapper (ZodEffects) makes the SDK
|
|
@@ -689,11 +732,11 @@ function createMemoryServer() {
|
|
|
689
732
|
if (Boolean(key) === Boolean(query)) {
|
|
690
733
|
throw new Error("Provide exactly one of key or query.");
|
|
691
734
|
}
|
|
692
|
-
const config =
|
|
735
|
+
const config = memoryConfig();
|
|
693
736
|
const scopes = getSearchScopes(scope, config);
|
|
694
737
|
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 ?? "");
|
|
738
|
+
? (await Promise.all(scopes.map((candidate) => readKey(candidate, key, scopeOptions)))).flat()
|
|
739
|
+
: searchEntries((await Promise.all(scopes.map((candidate) => listEntries(candidate, "", scopeOptions)))).flat(), query ?? "");
|
|
697
740
|
const sorted = results.sort((left, right) => {
|
|
698
741
|
return `${left.scope}:${left.key}`.localeCompare(`${right.scope}:${right.key}`);
|
|
699
742
|
});
|
|
@@ -722,7 +765,7 @@ function createMemoryServer() {
|
|
|
722
765
|
// disambiguated (rename keys, memory_history to recover).
|
|
723
766
|
const collisions = [];
|
|
724
767
|
for (const target of targets) {
|
|
725
|
-
const agent = await openScope(target, true);
|
|
768
|
+
const agent = await openScope(target, true, scopeOptions);
|
|
726
769
|
if (!agent) {
|
|
727
770
|
throw new Error(`Unable to open scope ${target}.`);
|
|
728
771
|
}
|
|
@@ -762,7 +805,7 @@ function createMemoryServer() {
|
|
|
762
805
|
idempotentHint: true,
|
|
763
806
|
},
|
|
764
807
|
}, async ({ scope, prefix }) => {
|
|
765
|
-
const entries = await listEntries(scope, prefix ?? "");
|
|
808
|
+
const entries = await listEntries(scope, prefix ?? "", scopeOptions);
|
|
766
809
|
const keys = entries.map((entry) => entry.key).sort();
|
|
767
810
|
return {
|
|
768
811
|
content: [{ type: "text", text: asToolText(keys) }],
|
|
@@ -776,7 +819,7 @@ function createMemoryServer() {
|
|
|
776
819
|
key: z.string().min(1),
|
|
777
820
|
}),
|
|
778
821
|
}, async ({ scope, key }) => {
|
|
779
|
-
const agent = await openScope(scope, false);
|
|
822
|
+
const agent = await openScope(scope, false, scopeOptions);
|
|
780
823
|
if (agent) {
|
|
781
824
|
try {
|
|
782
825
|
await agent.kv.delete(key);
|
|
@@ -802,7 +845,7 @@ function createMemoryServer() {
|
|
|
802
845
|
readOnlyHint: true,
|
|
803
846
|
},
|
|
804
847
|
}, async ({ scope, query, top_k, min_score }) => {
|
|
805
|
-
const { results, mode, model } = await semanticSearch(scope, query, top_k ?? 8, min_score ?? 0);
|
|
848
|
+
const { results, mode, model } = await semanticSearch(scope, query, top_k ?? 8, min_score ?? 0, memoryConfig(), context.projectId);
|
|
806
849
|
const payload = { mode, model, count: results.length, results };
|
|
807
850
|
return {
|
|
808
851
|
content: [{ type: "text", text: asToolText(payload) }],
|
|
@@ -842,7 +885,7 @@ function createMemoryServer() {
|
|
|
842
885
|
if (isHistoryKey(key)) {
|
|
843
886
|
throw new Error(`Keys under ${HISTORY_NAMESPACE}/ are reserved for memory history.`);
|
|
844
887
|
}
|
|
845
|
-
const agent = await openScope(scope, true);
|
|
888
|
+
const agent = await openScope(scope, true, scopeOptions);
|
|
846
889
|
if (!agent) {
|
|
847
890
|
throw new Error(`Unable to open scope ${scope}.`);
|
|
848
891
|
}
|
|
@@ -892,8 +935,11 @@ function createMemoryServer() {
|
|
|
892
935
|
idempotentHint: true,
|
|
893
936
|
},
|
|
894
937
|
}, async ({ scope, key }) => {
|
|
895
|
-
const current = await readKey(scope, key);
|
|
896
|
-
const history = (await listEntries(scope, historyPrefix(key), {
|
|
938
|
+
const current = await readKey(scope, key, scopeOptions);
|
|
939
|
+
const history = (await listEntries(scope, historyPrefix(key), {
|
|
940
|
+
includeHistory: true,
|
|
941
|
+
...scopeOptions,
|
|
942
|
+
}))
|
|
897
943
|
.sort((left, right) => left.key.localeCompare(right.key));
|
|
898
944
|
const payload = {
|
|
899
945
|
scope,
|
|
@@ -909,17 +955,21 @@ function createMemoryServer() {
|
|
|
909
955
|
server.registerTool("domain_knowledge_query", {
|
|
910
956
|
description: "Query an AnythingLLM workspace in query mode for domain knowledge.",
|
|
911
957
|
inputSchema: z.object({
|
|
912
|
-
workspace: z.string().min(1),
|
|
958
|
+
workspace: z.string().min(1).optional(),
|
|
913
959
|
query: z.string().min(1),
|
|
914
960
|
}),
|
|
915
961
|
annotations: {
|
|
916
962
|
readOnlyHint: true,
|
|
917
963
|
},
|
|
918
964
|
}, async ({ workspace, query }) => {
|
|
919
|
-
const
|
|
965
|
+
const workspaceSlug = workspace ?? defaultAnythingLLMWorkspace(memoryConfig());
|
|
966
|
+
if (!workspaceSlug) {
|
|
967
|
+
throw new Error("No AnythingLLM workspace provided and no project workspace found in memory config.");
|
|
968
|
+
}
|
|
969
|
+
const answer = await callAnythingLLM(workspaceSlug, query);
|
|
920
970
|
return {
|
|
921
971
|
content: [{ type: "text", text: answer }],
|
|
922
|
-
structuredContent: { workspace, answer },
|
|
972
|
+
structuredContent: { workspace: workspaceSlug, answer },
|
|
923
973
|
};
|
|
924
974
|
});
|
|
925
975
|
server.registerTool("domain_knowledge_sync", {
|
|
@@ -932,7 +982,7 @@ function createMemoryServer() {
|
|
|
932
982
|
}),
|
|
933
983
|
}, async ({ workspace, mode, confirm_replace, timeout_seconds }) => {
|
|
934
984
|
const syncMode = mode ?? "incremental";
|
|
935
|
-
const config =
|
|
985
|
+
const config = memoryConfig();
|
|
936
986
|
const workspaceSlug = workspace ?? defaultAnythingLLMWorkspace(config);
|
|
937
987
|
if (!workspaceSlug) {
|
|
938
988
|
throw new Error("No AnythingLLM workspace provided and no project workspace found in memory config.");
|
|
@@ -1183,12 +1233,13 @@ if (httpPort > 0) {
|
|
|
1183
1233
|
}
|
|
1184
1234
|
// New session (first request = initialize). The transport mints a session
|
|
1185
1235
|
// id and the SDK returns it via the response header; client echoes it back.
|
|
1236
|
+
const context = parseServerContext(request.headers);
|
|
1186
1237
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
1187
1238
|
sessionIdGenerator: () => randomUUID(),
|
|
1188
1239
|
onsessioninitialized: (id) => { sessions.set(id, transport); },
|
|
1189
1240
|
onsessionclosed: (id) => { sessions.delete(id); },
|
|
1190
1241
|
});
|
|
1191
|
-
const session = createMemoryServer();
|
|
1242
|
+
const session = createMemoryServer(context);
|
|
1192
1243
|
await session.connect(transport);
|
|
1193
1244
|
return transport.handleRequest(request);
|
|
1194
1245
|
};
|
|
@@ -1198,7 +1249,7 @@ if (httpPort > 0) {
|
|
|
1198
1249
|
res.setHeader("Access-Control-Allow-Origin", allowOrigin);
|
|
1199
1250
|
res.setHeader("Vary", "Origin");
|
|
1200
1251
|
res.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, OPTIONS");
|
|
1201
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, mcp-session-id, Accept, Authorization");
|
|
1252
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, mcp-session-id, Accept, Authorization, X-Cairn-Project, X-Cairn-Scopes, X-Cairn-AnythingLLM-Workspaces");
|
|
1202
1253
|
}
|
|
1203
1254
|
if (req.method === "OPTIONS") {
|
|
1204
1255
|
res.writeHead(allowOrigin ? 204 : 403).end();
|
|
@@ -1235,7 +1286,8 @@ if (httpPort > 0) {
|
|
|
1235
1286
|
res.end(Buffer.from(await response.arrayBuffer()));
|
|
1236
1287
|
}
|
|
1237
1288
|
catch (err) {
|
|
1238
|
-
|
|
1289
|
+
const status = err instanceof ClientContextError ? 400 : 500;
|
|
1290
|
+
res.writeHead(status).end(err instanceof Error ? err.message : String(err));
|
|
1239
1291
|
}
|
|
1240
1292
|
});
|
|
1241
1293
|
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"
|