@cairnkeep/cli 2.23.0 → 2.23.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 +22 -0
- package/docs/domain-knowledge.md +42 -4
- package/docs/learning/CURRICULUM-MAP.md +3 -3
- package/docs/learning/FEATURE-GUIDE.md +1 -1
- 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 +2 -2
- package/docs/learning/lessons/L03-first-project.md +1 -1
- package/docs/learning/lessons/L09-domain-knowledge.md +11 -0
- package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
- package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
- package/docs/learning/lessons/L21-context-packs.md +1 -1
- package/docs/learning/lessons/L22-native-windows.md +2 -2
- package/docs/learning/lessons/L23-guided-setup.md +1 -1
- package/docs/learning/lessons/L24-okf-exchange.md +1 -1
- package/docs/learning/lessons/L25-playbooks.md +1 -1
- package/docs/learning/lessons/L26-context-intelligence.md +1 -1
- package/docs/learning/lessons/L27-ecosystem-evidence.md +1 -1
- package/docs/learning/lessons/L28-security-assurance.md +2 -2
- package/docs/learning/video-scripts/V09-workspace-routing-supplement.md +36 -0
- package/examples/eval/bundled-fake.json +1 -1
- package/mcp-memory-server/dist/anythingllm-workspace.js +109 -0
- package/mcp-memory-server/dist/index.js +11 -4
- package/mcp-memory-server/package.json +3 -2
- package/package.json +2 -2
- package/scripts/test-dependency-security.mjs +40 -0
- package/scripts/test-dependency-security.sh +4 -0
- package/scripts/verify-dependency-security.mjs +19 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ All notable user-facing changes are documented here. This project follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [2.23.1] - 2026-10-22
|
|
9
|
+
|
|
10
|
+
### Domain knowledge
|
|
11
|
+
|
|
12
|
+
- Resolve local AnythingLLM defaults from the existing absolute project-path
|
|
13
|
+
mapping, with deepest-project matching and the sync script's discovery order.
|
|
14
|
+
Preserve explicit workspace overrides, legacy fallback, remote HTTP session
|
|
15
|
+
routing, OpenViking selection and synchronization approval requirements.
|
|
16
|
+
- Bound mapping reads and reject unsafe files, invalid slugs and ambiguous
|
|
17
|
+
canonical project declarations. Add pure, real stdio, HTTP and synchronization
|
|
18
|
+
regression controls; update operating guidance and the recording supplement.
|
|
19
|
+
|
|
20
|
+
### Maintenance
|
|
21
|
+
|
|
22
|
+
- Allocate the remote-routing test provider's loopback port dynamically; retain
|
|
23
|
+
the formerly failing PID as a regression instead of hitting fetch's blocked
|
|
24
|
+
port 10080. Production routing and transport policy are unchanged.
|
|
25
|
+
- Refresh pinned setup, artifact-upload and code-scanning actions; align the
|
|
26
|
+
packaged CLI and server on Zod 4.6.7 with dependency-parity mutation controls.
|
|
27
|
+
- Update the isolated historical Python MCP integration experiment to 2.2.0;
|
|
28
|
+
this does not add Python or that experiment to the shipped Node runtime.
|
|
29
|
+
|
|
8
30
|
## [2.23.0] - 2026-10-21
|
|
9
31
|
|
|
10
32
|
### Memory retrieval
|
package/docs/domain-knowledge.md
CHANGED
|
@@ -29,7 +29,7 @@ work exactly the same without them.
|
|
|
29
29
|
| `ANYTHINGLLM_API_KEY` | **Required** for the domain-knowledge tools. Unset → the tools error at call time (everything else works). |
|
|
30
30
|
| `ANYTHINGLLM_BASE_URL` | AnythingLLM base URL. Default `http://localhost:3001`. |
|
|
31
31
|
| `CAIRN_ANYTHINGLLM_SYNC_SCRIPT` | Optional path to a custom document-sync script. Unset -> `domain_knowledge_sync` uses the bundled deployment-neutral example. |
|
|
32
|
-
| `CAIRN_ANYTHINGLLM_PROJECTS_FILE` | Optional config path for the bundled sync script. Default `${XDG_CONFIG_HOME:-~/.config}/cairnkeep/anythingllm-projects.json`. |
|
|
32
|
+
| `CAIRN_ANYTHINGLLM_PROJECTS_FILE` | Optional config path for the bundled sync script, also used to pick the default workspace per repository. Default `${XDG_CONFIG_HOME:-~/.config}/cairnkeep/anythingllm-projects.json`. |
|
|
33
33
|
| `CAIRN_ANYTHINGLLM_STATE_FILE` | Optional state path for the bundled sync script. Default `${XDG_STATE_HOME:-~/.local/state}/cairnkeep/anythingllm-sync.json`. |
|
|
34
34
|
|
|
35
35
|
The query security follow-up validates HTTP(S) URLs and rejects embedded
|
|
@@ -54,9 +54,43 @@ first of `.agent/memory.json`, `.opencode/memory.json`, `.claude/memory.json`, o
|
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
### Which workspace a call targets
|
|
58
|
+
|
|
59
|
+
A local stdio tool call without an explicit `workspace` resolves one in this order:
|
|
60
|
+
|
|
61
|
+
1. The project in `anythingllm-projects.json` whose absolute `path` contains the current
|
|
62
|
+
working directory. The deepest match wins, so a checkout nested inside
|
|
63
|
+
another project resolves to its own workspace.
|
|
64
|
+
2. Otherwise the first entry in `anythingllm_workspaces` that is not
|
|
65
|
+
`engineering-patterns`, which is reserved for cross-project patterns and is
|
|
66
|
+
never chosen as a project default.
|
|
67
|
+
|
|
68
|
+
Step 1 matters when one machine holds several checkouts that share a single
|
|
69
|
+
global memory config. Without it every repository resolves to the same
|
|
70
|
+
workspace, and a query made from a different repository reports "there is no
|
|
71
|
+
relevant information" instead of revealing that it searched somewhere
|
|
72
|
+
unrelated. Because the mapping is the same file the sync script uses, the
|
|
73
|
+
workspace a repository reads from is the one it embeds into.
|
|
74
|
+
|
|
75
|
+
If the mapping is absent, unreadable, or malformed, resolution falls back to
|
|
76
|
+
step 2, so existing single-project setups are unaffected.
|
|
77
|
+
|
|
78
|
+
Mapping discovery follows the bundled sync script exactly:
|
|
79
|
+
`CAIRN_ANYTHINGLLM_PROJECTS_FILE`, then a legacy file beside the selected sync
|
|
80
|
+
script, then `${XDG_CONFIG_HOME:-~/.config}/cairnkeep/anythingllm-projects.json`.
|
|
81
|
+
A missing explicit override does not select another mapping implicitly. Custom
|
|
82
|
+
sync implementations must honor the same mapping or configure an explicit
|
|
83
|
+
workspace; Cairnkeep cannot infer their private lookup rules.
|
|
84
|
+
|
|
85
|
+
Mapping reads are descriptor-bound UTF-8 JSON, limited to 1 MiB and 4,096
|
|
86
|
+
entries. Symlinked, special, changing and oversized mapping files are not read
|
|
87
|
+
as trusted content. Project paths must be absolute, without tilde expansion,
|
|
88
|
+
matching the sync script. They are canonicalized; relative declarations,
|
|
89
|
+
invalid workspace identifiers and the shared `engineering-patterns` slug are
|
|
90
|
+
skipped. Conflicting slugs for the same deepest canonical project raise a
|
|
91
|
+
controlled ambiguity error: specify `workspace` or correct the declarations.
|
|
92
|
+
Explicit tool arguments always win. With no resolved default, the tool errors.
|
|
93
|
+
This lookup selects a destination; it does not grant approval to synchronize.
|
|
60
94
|
|
|
61
95
|
In local stdio mode, that config is read from the client project's working
|
|
62
96
|
directory. In remote HTTP mode, the server cannot read files from the client
|
|
@@ -64,6 +98,10 @@ PC. Configure `ANYTHINGLLM_BASE_URL`, `ANYTHINGLLM_API_KEY`, and any sync script
|
|
|
64
98
|
on the HTTP server host, then send the project workspaces in the
|
|
65
99
|
`X-Cairn-AnythingLLM-Workspaces` session header. See
|
|
66
100
|
[Memory storage and deployment](storage.md#per-project-remote-sessions).
|
|
101
|
+
HTTP calls never consult the server checkout's project mapping to choose a
|
|
102
|
+
client's default, including on the synchronization write path. The optional
|
|
103
|
+
OpenViking provider also retains its configured workspace selection rather
|
|
104
|
+
than consulting an AnythingLLM-specific mapping.
|
|
67
105
|
|
|
68
106
|
The HTTP server performs queries and runs the sync script on its own host. A
|
|
69
107
|
remote sync script must therefore obtain project documents from a server-side
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Curriculum Coverage Map
|
|
2
2
|
|
|
3
|
-
**Baseline:** Cairnkeep 2.23.
|
|
4
|
-
**Last reviewed:** 2026-10-
|
|
3
|
+
**Baseline:** Cairnkeep 2.23.1
|
|
4
|
+
**Last reviewed:** 2026-10-22
|
|
5
5
|
|
|
6
6
|
This map prevents the course from drifting behind the product. It assigns each
|
|
7
7
|
user-facing surface to a lesson and records whether the lesson is complete or
|
|
@@ -20,7 +20,7 @@ been rehearsed.
|
|
|
20
20
|
| Wiki, alignment, and repository quality workflows | L05-L06 | L05-L06 | Brief |
|
|
21
21
|
| Optional local code graph, graph inspection, and harness delegates | L18 | L18 | Brief; default off |
|
|
22
22
|
| Storage placement, export/import, and authenticated remote memory | L07 | L07-L08 | Brief |
|
|
23
|
-
| Document RAG | L00 | L09 | Brief; optional |
|
|
23
|
+
| Document RAG | L00 | L09 | Brief; optional; [V09 routing supplement](video-scripts/V09-workspace-routing-supplement.md) covers local mappings and HTTP isolation |
|
|
24
24
|
| token-miser exploration and routing | L00 | L10 | Brief; optional |
|
|
25
25
|
| Rootless containers and persistent service operation | L01 | L11 | L01 Ready; L11 Brief |
|
|
26
26
|
| Private managed distributions | L12 | L12 | Brief; optional |
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
**Status:** Ready
|
|
4
4
|
**Track:** Quickstart
|
|
5
5
|
**Time:** 15 minutes
|
|
6
|
-
**Tested with:** Cairnkeep 2.23.
|
|
6
|
+
**Tested with:** Cairnkeep 2.23.1
|
|
7
7
|
|
|
8
8
|
## Outcome
|
|
9
9
|
|
|
@@ -26,7 +26,7 @@ ephemeral; the named course volume is persistent.
|
|
|
26
26
|
1. Pull the public image:
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
podman pull ghcr.io/cairnkeep/cairnkeep:2.23.
|
|
29
|
+
podman pull ghcr.io/cairnkeep/cairnkeep:2.23.1
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
2. Start the stdio server with a read-only root filesystem and no Linux
|
|
@@ -38,7 +38,7 @@ ephemeral; the named course volume is persistent.
|
|
|
38
38
|
--read-only --cap-drop=all --security-opt=no-new-privileges \
|
|
39
39
|
--tmpfs=/tmp:rw,noexec,nosuid,size=64m,mode=1777 \
|
|
40
40
|
--volume cairnkeep-course-data:/data:Z,U \
|
|
41
|
-
ghcr.io/cairnkeep/cairnkeep:2.23.
|
|
41
|
+
ghcr.io/cairnkeep/cairnkeep:2.23.1 stdio
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
3. The process waits quietly for MCP input. Press `Ctrl-C` after confirming it
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
**Status:** Ready
|
|
4
4
|
**Track:** Quickstart
|
|
5
5
|
**Time:** 25 minutes
|
|
6
|
-
**Tested with:** Cairnkeep 2.23.
|
|
6
|
+
**Tested with:** Cairnkeep 2.23.1 and Node.js 22 or newer
|
|
7
7
|
|
|
8
8
|
## Outcome
|
|
9
9
|
|
|
@@ -47,7 +47,7 @@ sync. Codex project trust remains an explicit operator decision.
|
|
|
47
47
|
2. Install Cairnkeep:
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
|
-
npm install --global @cairnkeep/cli@2.23.
|
|
50
|
+
npm install --global @cairnkeep/cli@2.23.1
|
|
51
51
|
cairn --version
|
|
52
52
|
```
|
|
53
53
|
|
|
@@ -13,6 +13,8 @@ without affecting core Cairnkeep memory.
|
|
|
13
13
|
|
|
14
14
|
- Memory versus document RAG responsibilities.
|
|
15
15
|
- Workspace selection and project configuration.
|
|
16
|
+
- Explicit directory-to-workspace mapping for local stdio; HTTP session headers
|
|
17
|
+
remain authoritative for remote routing, not the server's current directory.
|
|
16
18
|
- Query-only setup before document synchronization.
|
|
17
19
|
- Incremental, full, and replacement synchronization boundaries.
|
|
18
20
|
- Endpoint, credential, and document-selection safety.
|
|
@@ -24,12 +26,21 @@ Index a synthetic three-document corpus into a disposable workspace, ask a
|
|
|
24
26
|
question requiring citations to two documents, update one document, resync, and
|
|
25
27
|
verify the new answer. Then unset the integration and prove memory still works.
|
|
26
28
|
|
|
29
|
+
Use two disposable checkouts sharing a global memory configuration. Declare
|
|
30
|
+
their absolute paths and distinct slugs in `anythingllm-projects.json`; verify
|
|
31
|
+
root and subdirectory queries report the expected workspace. Check an explicit
|
|
32
|
+
workspace override and an undeclared-directory fallback. Keep synchronization
|
|
33
|
+
synthetic and separately approved; a destination mapping is not write consent.
|
|
34
|
+
See the [recording supplement](../video-scripts/V09-workspace-routing-supplement.md).
|
|
35
|
+
|
|
27
36
|
## Acceptance criteria
|
|
28
37
|
|
|
29
38
|
- Only allowlisted synthetic documents are uploaded.
|
|
30
39
|
- The learner can identify the selected workspace and endpoint without printing
|
|
31
40
|
its credential.
|
|
32
41
|
- Updated content replaces stale retrieval results.
|
|
42
|
+
- The learner verifies the returned workspace before trusting an empty answer;
|
|
43
|
+
conflicting deepest declarations require an explicit destination or repair.
|
|
33
44
|
- Disabling RAG leaves `/remember` and `/recall` healthy.
|
|
34
45
|
|
|
35
46
|
## Source material
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
**Status:** Ready
|
|
4
4
|
**Track:** Operator
|
|
5
5
|
**Time:** 30 minutes
|
|
6
|
-
**Tested with:** Cairnkeep 2.23.
|
|
6
|
+
**Tested with:** Cairnkeep 2.23.1, Windows x64, and Node.js 22 or newer
|
|
7
7
|
|
|
8
8
|
## Outcome
|
|
9
9
|
|
|
@@ -16,7 +16,7 @@ perform a reversible uninstall without relying on WSL or Git Bash.
|
|
|
16
16
|
From PowerShell:
|
|
17
17
|
|
|
18
18
|
```powershell
|
|
19
|
-
npm install --global @cairnkeep/cli@2.23.
|
|
19
|
+
npm install --global @cairnkeep/cli@2.23.1
|
|
20
20
|
cairn version
|
|
21
21
|
|
|
22
22
|
$Lab = Join-Path $env:TEMP 'Cairnkeep Windows Lab'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# L28 - Verify Cairnkeep's security posture
|
|
2
2
|
|
|
3
3
|
**Status:** Ready
|
|
4
|
-
**Tested with:** Cairnkeep 2.23.
|
|
4
|
+
**Tested with:** Cairnkeep 2.23.1 and Node.js 22 or newer
|
|
5
5
|
|
|
6
6
|
## Outcome
|
|
7
7
|
|
|
@@ -11,7 +11,7 @@ and choose least authority for a network-facing MCP client.
|
|
|
11
11
|
|
|
12
12
|
## Prerequisites
|
|
13
13
|
|
|
14
|
-
- Install Cairnkeep 2.23.
|
|
14
|
+
- Install Cairnkeep 2.23.1 or use a disposable source checkout.
|
|
15
15
|
- Complete [L20 - Least-authority MCP tool profiles](L20-mcp-tool-profiles.md).
|
|
16
16
|
- Use synthetic values only; never paste a real bearer token into a recording,
|
|
17
17
|
terminal transcript, or issue.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# V09 supplement — The right documents for the right repository
|
|
2
|
+
|
|
3
|
+
Technical recording brief; no completed video or full Ready L09 lab is claimed.
|
|
4
|
+
Use invented repository names and a loopback mock, never private mappings,
|
|
5
|
+
provider keys, real documents or production synchronization.
|
|
6
|
+
|
|
7
|
+
## Real terminal demonstration
|
|
8
|
+
|
|
9
|
+
Leave a readable pause after each command:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm --prefix mcp-memory-server run build
|
|
13
|
+
npm --prefix mcp-memory-server run check:anythingllm-workspace
|
|
14
|
+
npm --prefix mcp-memory-server run check:remote-context
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Show two synthetic checkouts sharing one global configuration, then highlight
|
|
18
|
+
the distinct workspace reported by each real stdio MCP query. Zoom into a
|
|
19
|
+
subdirectory call and an explicit override. Explain that an empty answer from
|
|
20
|
+
the wrong corpus is not evidence that the requested information does not exist.
|
|
21
|
+
|
|
22
|
+
The mock synchronization prints arguments only; it uploads nothing. Contrast
|
|
23
|
+
destination selection with write consent and show replacement being refused
|
|
24
|
+
without confirmation. For HTTP, show the conflicting server-directory mapping
|
|
25
|
+
fixture and the client header still winning. Do not run a live provider or
|
|
26
|
+
silently activate RAG merely to record this supplement.
|
|
27
|
+
|
|
28
|
+
## Boundaries and recovery
|
|
29
|
+
|
|
30
|
+
Explain the lookup order: explicit mapping file, legacy mapping beside the
|
|
31
|
+
selected sync script, then XDG. Missing or invalid mappings keep the legacy
|
|
32
|
+
configured default; inspect the returned workspace. Ambiguous deepest entries
|
|
33
|
+
require an explicit workspace or a corrected mapping. Mapping files are bounded
|
|
34
|
+
and unsafe file types are rejected. Directory matching is routing, not an ACL
|
|
35
|
+
or publisher-authenticity guarantee. Keep the full L09 lesson marked Brief until
|
|
36
|
+
its separate live synthetic-corpus lab has been verified.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"identifier":"cairn-offline-fake-v1","package_version":"2.23.
|
|
1
|
+
{"identifier":"cairn-offline-fake-v1","package_version":"2.23.1","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
import { readStableJson } from "./stable-file.js";
|
|
5
|
+
/** Shared workspace that is never a project default; it holds cross-project patterns. */
|
|
6
|
+
const SHARED_WORKSPACE = "engineering-patterns";
|
|
7
|
+
const WORKSPACE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
8
|
+
const MAX_MAPPING_BYTES = 1024 * 1024;
|
|
9
|
+
const MAX_PROJECTS = 4096;
|
|
10
|
+
function expandHome(value) {
|
|
11
|
+
if (value === "~")
|
|
12
|
+
return homedir();
|
|
13
|
+
if (value.startsWith("~/"))
|
|
14
|
+
return join(homedir(), value.slice(2));
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Locate `anythingllm-projects.json`.
|
|
19
|
+
*
|
|
20
|
+
* Mirrors the precedence in `examples/anythingllm/sync_to_anythingllm.py` so the
|
|
21
|
+
* resolver and the sync script can never disagree about which mapping is live.
|
|
22
|
+
*/
|
|
23
|
+
export function anythingllmProjectsFile(env = process.env, legacyDir) {
|
|
24
|
+
const override = env.CAIRN_ANYTHINGLLM_PROJECTS_FILE;
|
|
25
|
+
if (override) {
|
|
26
|
+
const explicit = resolve(expandHome(override));
|
|
27
|
+
return existsSync(explicit) ? explicit : undefined;
|
|
28
|
+
}
|
|
29
|
+
if (legacyDir) {
|
|
30
|
+
const legacy = join(legacyDir, "anythingllm-projects.json");
|
|
31
|
+
if (existsSync(legacy))
|
|
32
|
+
return legacy;
|
|
33
|
+
}
|
|
34
|
+
const configHome = env.XDG_CONFIG_HOME
|
|
35
|
+
? resolve(expandHome(env.XDG_CONFIG_HOME))
|
|
36
|
+
: join(homedir(), ".config");
|
|
37
|
+
const standard = join(configHome, "cairnkeep", "anythingllm-projects.json");
|
|
38
|
+
if (existsSync(standard))
|
|
39
|
+
return standard;
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Workspace slug declared for the project containing `cwd`, if any.
|
|
44
|
+
*
|
|
45
|
+
* The deepest matching `path` wins so a checkout nested inside another project
|
|
46
|
+
* resolves to its own workspace rather than its parent's.
|
|
47
|
+
*/
|
|
48
|
+
export function workspaceForDirectory(cwd, env = process.env, legacyDir) {
|
|
49
|
+
const file = anythingllmProjectsFile(env, legacyDir);
|
|
50
|
+
if (!file)
|
|
51
|
+
return undefined;
|
|
52
|
+
let entries;
|
|
53
|
+
try {
|
|
54
|
+
const parsed = readStableJson(file, { label: "Workspace mapping", maxBytes: MAX_MAPPING_BYTES });
|
|
55
|
+
if (!Array.isArray(parsed?.projects) || parsed.projects.length > MAX_PROJECTS)
|
|
56
|
+
return undefined;
|
|
57
|
+
entries = parsed.projects;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// A malformed or unreadable mapping must not break workspace resolution;
|
|
61
|
+
// fall back to the configured list rather than failing the tool call.
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
let here;
|
|
65
|
+
try {
|
|
66
|
+
here = realpathSync.native(cwd);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
let bestSlug;
|
|
72
|
+
let bestDepth = -1;
|
|
73
|
+
let ambiguous = false;
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
const slug = typeof entry?.slug === "string" ? entry.slug : "";
|
|
76
|
+
const path = typeof entry?.path === "string" ? entry.path : "";
|
|
77
|
+
if (!WORKSPACE_PATTERN.test(slug) || slug === SHARED_WORKSPACE || !isAbsolute(path))
|
|
78
|
+
continue;
|
|
79
|
+
let root;
|
|
80
|
+
try {
|
|
81
|
+
root = realpathSync.native(path);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const rel = relative(root, here);
|
|
87
|
+
const contains = rel === "" || (rel !== ".." && !rel.startsWith("../") && !rel.startsWith("..\\") && !isAbsolute(rel));
|
|
88
|
+
if (contains && root.length > bestDepth) {
|
|
89
|
+
bestSlug = slug;
|
|
90
|
+
bestDepth = root.length;
|
|
91
|
+
ambiguous = false;
|
|
92
|
+
}
|
|
93
|
+
else if (contains && root.length === bestDepth && slug !== bestSlug) {
|
|
94
|
+
ambiguous = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (ambiguous)
|
|
98
|
+
throw new Error("Ambiguous project workspace mapping; specify workspace explicitly.");
|
|
99
|
+
return bestSlug;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Default workspace for a tool call: the project containing `cwd` when it is
|
|
103
|
+
* declared, otherwise the previous behaviour so existing single-project setups
|
|
104
|
+
* keep working unchanged.
|
|
105
|
+
*/
|
|
106
|
+
export function defaultAnythingLLMWorkspace(config, cwd = process.cwd(), env = process.env, legacyDir) {
|
|
107
|
+
return (workspaceForDirectory(cwd, env, legacyDir) ??
|
|
108
|
+
config.anythingllm_workspaces?.find((workspace) => workspace !== SHARED_WORKSPACE));
|
|
109
|
+
}
|
|
@@ -11,6 +11,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
11
11
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
12
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
13
13
|
import { z } from "zod";
|
|
14
|
+
import { defaultAnythingLLMWorkspace as resolveDefaultAnythingLLMWorkspace } from "./anythingllm-workspace.js";
|
|
14
15
|
import { artifactIdSchema, artifactKindSchema, artifactMediaTypeSchema, artifactNodeRefSchema, artifactProvenanceSchema, artifactSessionRefSchema, compactionSummaryInputContentSchema, diffContentSchema, generatedFileInputContentSchema, isArtifactHttpEnabled, isArtifactStoreEnabled, testOutputContentSchema, } from "./artifact-schema.js";
|
|
15
16
|
import { isCapabilityContractEnabled, resolveCapabilityStatus, } from "./capability-config.js";
|
|
16
17
|
import { EmbeddingCache, cosineSimilarity, embedTexts, getEmbeddingConfig, hashText, } from "./embeddings.js";
|
|
@@ -360,8 +361,14 @@ async function readStdin() {
|
|
|
360
361
|
}
|
|
361
362
|
return Buffer.concat(chunks).toString("utf8");
|
|
362
363
|
}
|
|
363
|
-
function defaultAnythingLLMWorkspace(config) {
|
|
364
|
-
|
|
364
|
+
function defaultAnythingLLMWorkspace(config, remote = false) {
|
|
365
|
+
// HTTP callers route by session metadata, never by the server's checkout.
|
|
366
|
+
if (remote || resolveDomainRetrievalProvider() !== "anythingllm") {
|
|
367
|
+
return config.anythingllm_workspaces?.find((workspace) => workspace !== "engineering-patterns");
|
|
368
|
+
}
|
|
369
|
+
// `anythingllm-projects.json` may also sit beside the bundled sync script,
|
|
370
|
+
// which is the legacy location the script still honours.
|
|
371
|
+
return resolveDefaultAnythingLLMWorkspace(config, process.cwd(), process.env, dirname(anythingllmSyncScript));
|
|
365
372
|
}
|
|
366
373
|
function entryText(entry) {
|
|
367
374
|
const metadata = [entry.node_type, ...(entry.tags ?? [])].filter(Boolean).join("\n");
|
|
@@ -1452,7 +1459,7 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability, toolProf
|
|
|
1452
1459
|
readOnlyHint: true,
|
|
1453
1460
|
},
|
|
1454
1461
|
}, async ({ workspace, query }) => {
|
|
1455
|
-
const workspaceSlug = workspace ?? defaultAnythingLLMWorkspace(memoryConfig());
|
|
1462
|
+
const workspaceSlug = workspace ?? defaultAnythingLLMWorkspace(memoryConfig(), context.remote);
|
|
1456
1463
|
if (!workspaceSlug) {
|
|
1457
1464
|
throw new Error("No AnythingLLM workspace provided and no project workspace found in memory config.");
|
|
1458
1465
|
}
|
|
@@ -1480,7 +1487,7 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability, toolProf
|
|
|
1480
1487
|
}
|
|
1481
1488
|
const syncMode = mode ?? "incremental";
|
|
1482
1489
|
const config = memoryConfig();
|
|
1483
|
-
const workspaceSlug = workspace ?? defaultAnythingLLMWorkspace(config);
|
|
1490
|
+
const workspaceSlug = workspace ?? defaultAnythingLLMWorkspace(config, context.remote);
|
|
1484
1491
|
if (!workspaceSlug) {
|
|
1485
1492
|
throw new Error("No AnythingLLM workspace provided and no project workspace found in memory config.");
|
|
1486
1493
|
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"study:memory-retrieval": "npm run build && node scripts/run-memory-retrieval-study.mjs --summary",
|
|
20
20
|
"check:scope-guard": "node scripts/smoke-scope-guard.mjs",
|
|
21
21
|
"check:http-guard": "node scripts/smoke-http-guard.mjs",
|
|
22
|
-
"check:remote-context": "node scripts/smoke-remote-context.mjs",
|
|
22
|
+
"check:remote-context": "node scripts/smoke-remote-context.mjs && node scripts/smoke-remote-context-ports.mjs",
|
|
23
23
|
"check:explore-guard": "node scripts/smoke-explore-guard.mjs",
|
|
24
24
|
"check:explore-cache": "node scripts/smoke-explore-cache.mjs",
|
|
25
25
|
"check:explore-crossref": "node scripts/smoke-explore-crossref.mjs",
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"check:context-pack-v216-compat": "node scripts/smoke-context-pack-v216-compat.mjs",
|
|
59
59
|
"check:context-usage": "node scripts/smoke-context-usage.mjs",
|
|
60
60
|
"check:domain-retrieval": "node scripts/smoke-domain-retrieval.mjs",
|
|
61
|
+
"check:anythingllm-workspace": "node scripts/smoke-anythingllm-workspace.mjs && node scripts/smoke-anythingllm-workspace-mcp.mjs",
|
|
61
62
|
"check:retrieval-benchmark": "node scripts/smoke-retrieval-benchmark.mjs && node scripts/run-retrieval-benchmark.mjs --check",
|
|
62
63
|
"benchmark:retrieval": "npm run build && node scripts/run-retrieval-benchmark.mjs",
|
|
63
64
|
"check:work-evidence": "node scripts/smoke-work-evidence.mjs",
|
|
@@ -77,7 +78,7 @@
|
|
|
77
78
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
78
79
|
"agentfs-sdk": "^0.6.4",
|
|
79
80
|
"yaml": "2.9.1",
|
|
80
|
-
"zod": "^4.6.
|
|
81
|
+
"zod": "^4.6.7"
|
|
81
82
|
},
|
|
82
83
|
"devDependencies": {
|
|
83
84
|
"@types/node": "^22.20.5",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cairnkeep/cli",
|
|
3
|
-
"version": "2.23.
|
|
3
|
+
"version": "2.23.1",
|
|
4
4
|
"description": "A durable, harness-agnostic memory + context layer for coding agents and MCP clients.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
42
42
|
"agentfs-sdk": "^0.6.4",
|
|
43
43
|
"yaml": "2.9.1",
|
|
44
|
-
"zod": "^4.6.
|
|
44
|
+
"zod": "^4.6.7"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build:server": "npm --prefix mcp-memory-server ci --include=dev --global=false --dry-run=false && npm --prefix mcp-memory-server run build --global=false --dry-run=false",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
const root = fileURLToPath(new URL("../", import.meta.url));
|
|
9
|
+
const policy = join(root, "scripts/verify-dependency-security.mjs");
|
|
10
|
+
const paths = ["package.json", "package-lock.json", "mcp-memory-server/package.json", "mcp-memory-server/package-lock.json"];
|
|
11
|
+
const baseline = Object.fromEntries(paths.map(path => [path, JSON.parse(readFileSync(join(root, path), "utf8"))]));
|
|
12
|
+
const scratch = mkdtempSync(join(tmpdir(), "cairn-dependency-policy-"));
|
|
13
|
+
function check(label, mutate, expected) {
|
|
14
|
+
const values = structuredClone(baseline);
|
|
15
|
+
mutate(values);
|
|
16
|
+
for (const path of paths) {
|
|
17
|
+
const target = join(scratch, path);
|
|
18
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
19
|
+
writeFileSync(target, JSON.stringify(values[path]));
|
|
20
|
+
}
|
|
21
|
+
const result = spawnSync(process.execPath, [policy], { cwd: scratch, encoding: "utf8", timeout: 10000 });
|
|
22
|
+
assert.ifError(result.error);
|
|
23
|
+
assert.equal(result.status, expected ? 1 : 0, `${label}: ${result.stderr}`);
|
|
24
|
+
if (expected) assert.match(result.stderr, expected, label);
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
check("valid dependency graph", () => {}, null);
|
|
28
|
+
check("root manifest/lock mismatch", x => { x["package.json"].dependencies.zod = "^4.0.0"; }, /manifest and lock root Zod ranges differ/);
|
|
29
|
+
check("server manifest/lock mismatch", x => { x["mcp-memory-server/package-lock.json"].packages[""].dependencies.zod = "^4.0.0"; }, /manifest and lock root Zod ranges differ/);
|
|
30
|
+
check("matched server range differs from packaged CLI", x => {
|
|
31
|
+
x["mcp-memory-server/package.json"].dependencies.zod = "^4.0.0";
|
|
32
|
+
x["mcp-memory-server/package-lock.json"].packages[""].dependencies.zod = "^4.0.0";
|
|
33
|
+
}, /root and server Zod ranges differ/);
|
|
34
|
+
check("different resolved schema version", x => { x["mcp-memory-server/package-lock.json"].packages["node_modules/zod"].version = "4.0.0"; }, /root and server resolved Zod versions differ/);
|
|
35
|
+
check("missing direct schema", x => { delete x["package-lock.json"].packages["node_modules/zod"]; }, /direct Zod: version must be a string/);
|
|
36
|
+
check("existing SDK advisory check retained", x => { x["package-lock.json"].packages["node_modules/@modelcontextprotocol/sdk"].version = "1.0.0"; }, /resolved @modelcontextprotocol\/sdk: requires/);
|
|
37
|
+
console.log("PASS: dependency policy positive control, schema parity mutations and SDK advisory rejection");
|
|
38
|
+
} finally {
|
|
39
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
40
|
+
}
|
|
@@ -130,8 +130,27 @@ function verifyPackageRoot({ label, directory }) {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
function verifySchemaDependencyParity() {
|
|
134
|
+
let expectedRange;
|
|
135
|
+
let expectedVersion;
|
|
136
|
+
for (const { label, directory } of PACKAGE_ROOTS) {
|
|
137
|
+
const manifest = readJson(`${directory}/package.json`);
|
|
138
|
+
const lock = readJson(`${directory}/package-lock.json`);
|
|
139
|
+
const range = manifest.dependencies?.zod;
|
|
140
|
+
assert.equal(typeof range, "string", `${label}: direct Zod range is missing`);
|
|
141
|
+
assert.equal(lock.packages?.[""].dependencies?.zod, range, `${label}: manifest and lock root Zod ranges differ`);
|
|
142
|
+
const version = lock.packages?.["node_modules/zod"]?.version;
|
|
143
|
+
parseVersion(version, `${label} direct Zod`);
|
|
144
|
+
expectedRange ??= range;
|
|
145
|
+
expectedVersion ??= version;
|
|
146
|
+
assert.equal(range, expectedRange, "root and server Zod ranges differ");
|
|
147
|
+
assert.equal(version, expectedVersion, "root and server resolved Zod versions differ");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
133
151
|
try {
|
|
134
152
|
PACKAGE_ROOTS.forEach(verifyPackageRoot);
|
|
153
|
+
verifySchemaDependencyParity();
|
|
135
154
|
process.stdout.write("PASS: dependency manifests and lock graphs exclude known MCP transport advisories\n");
|
|
136
155
|
} catch (error) {
|
|
137
156
|
process.stderr.write(`FAIL: dependency security policy: ${error instanceof Error ? error.message : String(error)}\n`);
|