@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.
@@ -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 domain-knowledge sync script. Defaults to a script bundled next to
19
- // this repo, but a wrapper can point CAIRN_ANYTHINGLLM_SYNC_SCRIPT at its own copy
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 resolveScopePath(scope, cwd = process.cwd()) {
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
- return resolve(cwd, ".agentfs", "project.db");
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, cwd) {
127
- const dbPath = resolveScopePath(scope, cwd);
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.cwd);
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) => ({ scope: candidate, entries: await listEntries(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 = getMemoryConfig();
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), { includeHistory: true }))
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 answer = await callAnythingLLM(workspace, query);
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 = getMemoryConfig();
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
- res.writeHead(500).end(err instanceof Error ? err.message : String(err));
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.0.5",
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 run build:server && npm --prefix mcp-memory-server test --global=false --dry-run=false"
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 serverPath = process.argv[2];
5
- if (!serverPath) {
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 child = spawn(process.execPath, [serverPath], {
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 (message.id === 1 && message.result?.serverInfo?.name === "cairn-memory") {
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 node on
9
- # the repo's dist build — so it works both post-install and from a clone.
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
- server=(cairn memory-server)
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
- server=(node "$ROOT_DIR/mcp-memory-server/dist/index.js")
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
- # HOME points at a throwaway dir so the server's default store (~/.cairnkeep)
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