@cairnkeep/cli 1.0.3 → 1.0.5

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/README.md CHANGED
@@ -27,7 +27,8 @@ cairnkeep-org sibling project.
27
27
  embedding-ranked search against any OpenAI-compatible endpoint.
28
28
  - **`bin/cairn`** — the CLI. `cairn bootstrap [path]` scaffolds a project's
29
29
  `.ai/` launchers + env; `cairn doctor` health-checks the configured pieces;
30
- `cairn memory export|import` relocates the durable store between machines;
30
+ `cairn memory export|import` relocates the durable store between machines
31
+ (`export` requires the optional `sqlite3` CLI);
31
32
  `cairn audit-timer` installs the scheduled memory+wiki audit; `cairn uninstall`
32
33
  reverses the install (backup-first, revertible).
33
34
  - **`templates/`** — project scaffolding (generic launchers, env) plus the
@@ -60,6 +61,11 @@ operating layer (commands, agents, hooks) installed into your harness, and a
60
61
  bootstrapped project. `cairn bootstrap` does only the last of these — the full
61
62
  ordered walkthrough is in **[docs/operating.md](docs/operating.md)**.
62
63
 
64
+ This setup is local by default: the registered stdio server stores memory on
65
+ the user's computer. Cairnkeep never discovers or selects a remote host.
66
+ Remote HTTP storage requires an explicit server deployment and client URL; see
67
+ **[Memory storage and deployment](docs/storage.md)** before enabling it.
68
+
63
69
  The short version for Claude Code:
64
70
 
65
71
  ```bash
@@ -106,7 +112,7 @@ unless a wrapper opts in:
106
112
 
107
113
  **Contributor mode.** Working on a repo you don't own? `cairn bootstrap
108
114
  --untracked /path/to/project` additionally writes the scaffolded paths
109
- (`.ai/`, `.planning/`) into the repo's `.git/info/exclude`, so the workflow
115
+ (`.ai/`, `.planning/`, `.agentfs/`) into the repo's `.git/info/exclude`, so the workflow
110
116
  files stay purely local: nothing to commit or push, invisible to every other
111
117
  contributor, and no edit to the shared `.gitignore`. The trade-off is that
112
118
  untracked planning state lives only on that clone — deleting the clone
@@ -141,6 +147,7 @@ Without an API key, search degrades gracefully to substring matching.
141
147
  - **Optional document RAG** (`domain_knowledge_*` via AnythingLLM) — [docs/domain-knowledge.md](docs/domain-knowledge.md)
142
148
  - **Building a private overlay** (wrap cairnkeep for your org/provider) — [docs/building-an-overlay.md](docs/building-an-overlay.md)
143
149
  - **Full operating guide** — [docs/operating.md](docs/operating.md)
150
+ - **Memory storage and deployment** — [docs/storage.md](docs/storage.md)
144
151
  - **Git providers** — [docs/git-providers.md](docs/git-providers.md)
145
152
  - **Contributing** — [CONTRIBUTING.md](CONTRIBUTING.md)
146
153
  - **Security policy** — [SECURITY.md](SECURITY.md)
package/bin/cairn CHANGED
@@ -15,6 +15,43 @@ CAIRN_ROOT=$(cd -P "$(dirname "$SOURCE")/.." && pwd)
15
15
  cmd="${1:-help}"
16
16
  [[ $# -gt 0 ]] && shift
17
17
 
18
+ usage() {
19
+ cat <<'USAGE'
20
+ cairn — Cairnkeep CLI
21
+
22
+ Usage:
23
+ cairn bootstrap [--untracked] [path]
24
+ Scaffold launchers, planning files, and the private
25
+ project-memory ignore guard (default path: .).
26
+ --untracked keeps the scaffold out of git via the repo's
27
+ .git/info/exclude (contributor mode: nothing to commit,
28
+ invisible to other contributors, local-only).
29
+ cairn memory-server Run the memory MCP server (stdio). Register it with your
30
+ harness, e.g. claude mcp add cairn-memory -s user -- cairn memory-server.
31
+ cairn sync [--apply] [--live-root DIR]
32
+ Install/update the Claude Code operating layer (commands,
33
+ agents, hooks) into ~/.claude, or into DIR for a single
34
+ project (--live-root <project>/.claude).
35
+ cairn doctor Health-check the memory server and configured LLM /
36
+ embedding endpoints, git provider, and store.
37
+ cairn memory <path|export|import>
38
+ Relocate global-scope memory. `path` prints its location;
39
+ `export <file.tgz>` takes a WAL-safe snapshot (requires
40
+ sqlite3); `import <file.tgz>` restores it.
41
+ cairn audit-timer [--on-calendar SPEC] [--para-root PATH] [--render-only DIR]
42
+ Install (opt-in) a systemd user timer that runs the
43
+ memory+wiki audit on a schedule; prints a cron alternative.
44
+ cairn uninstall [--dry-run] [--yes] [--purge-memory] [--live-root DIR] [PROJECT ...]
45
+ Reverse what cairnkeep installed/modified (operating
46
+ layer, hook registrations, MCP registration, audit
47
+ timer, and any given projects' bootstrap scaffold).
48
+ Backs everything up first with a revert.sh; keeps the
49
+ durable memory store unless --purge-memory is given.
50
+ cairn version Print version
51
+ cairn help Show this help
52
+ USAGE
53
+ }
54
+
18
55
  case "$cmd" in
19
56
  bootstrap)
20
57
  exec "$CAIRN_ROOT/scripts/bootstrap.sh" "$@"
@@ -43,40 +80,12 @@ case "$cmd" in
43
80
  v=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$CAIRN_ROOT/package.json" 2>/dev/null | head -1)
44
81
  echo "cairnkeep ${v:-unknown}"
45
82
  ;;
46
- help | --help | -h | *)
47
- cat <<'USAGE'
48
- cairn — Cairnkeep CLI
49
-
50
- Usage:
51
- cairn bootstrap [--untracked] [path]
52
- Scaffold .ai/ launchers + env into a project (default: .).
53
- --untracked keeps the scaffold out of git via the repo's
54
- .git/info/exclude (contributor mode: nothing to commit,
55
- invisible to other contributors, local-only).
56
- cairn memory-server Run the memory MCP server (stdio). Register it with your
57
- harness, e.g. claude mcp add cairn-memory -s user -- cairn memory-server.
58
- cairn sync [--apply] [--live-root DIR]
59
- Install/update the Claude Code operating layer (commands,
60
- agents, hooks) into ~/.claude, or into DIR for a single
61
- project (--live-root <project>/.claude).
62
- cairn doctor Health-check the configured pieces (memory server, LLM /
63
- embedding endpoints, git provider, store); skips what is
64
- unconfigured, fails only on a configured-but-unreachable dep.
65
- cairn memory <path|export|import>
66
- Relocate the durable memory store. `path` prints its
67
- location; `export <file.tgz>` takes a WAL-safe snapshot;
68
- `import <file.tgz>` restores it on another machine.
69
- cairn audit-timer [--on-calendar SPEC] [--para-root PATH] [--render-only DIR]
70
- Install (opt-in) a systemd user timer that runs the
71
- memory+wiki audit on a schedule; prints a cron alternative.
72
- cairn uninstall [--dry-run] [--yes] [--purge-memory] [--live-root DIR] [PROJECT ...]
73
- Reverse what cairnkeep installed/modified (operating
74
- layer, hook registrations, MCP registration, audit
75
- timer, and any given projects' bootstrap scaffold).
76
- Backs everything up first with a revert.sh; keeps the
77
- durable memory store unless --purge-memory is given.
78
- cairn version Print version
79
- cairn help Show this help
80
- USAGE
83
+ help | --help | -h)
84
+ usage
85
+ ;;
86
+ *)
87
+ echo "Unknown command: $cmd" >&2
88
+ usage >&2
89
+ exit 2
81
90
  ;;
82
91
  esac
package/docs/operating.md CHANGED
@@ -18,12 +18,19 @@ installs. This guide covers all three in order.
18
18
 
19
19
  - Node.js (for the memory server) and a supported harness: Claude Code or
20
20
  OpenCode.
21
+ - Optional: the `sqlite3` CLI for `cairn memory export`. Runtime memory and
22
+ `cairn memory import` do not require it.
21
23
  - Optional: an OpenAI-compatible LLM endpoint for memory extraction and
22
24
  embedding-ranked search. Without it, memory search degrades to substring
23
25
  matching — everything else still works.
24
26
 
25
27
  ## Setup order (Claude Code)
26
28
 
29
+ The commands below install the default local topology: `cairn-memory` runs as a
30
+ stdio child process and writes SQLite databases on this computer. Nothing in
31
+ the installer discovers or selects a remote host. Read
32
+ [Memory storage and deployment](storage.md) before choosing remote HTTP mode.
33
+
27
34
  **Via npm** (simplest — everything is on `PATH` as `cairn`):
28
35
 
29
36
  ```bash
@@ -52,7 +59,7 @@ claude mcp add cairn-memory -s user -- node "$PWD/mcp-memory-server/dist/index.j
52
59
  scripts/sync-claude-assets.sh --apply
53
60
 
54
61
  # 4. Scaffold a target project
55
- cairn bootstrap /path/to/project
62
+ bin/cairn bootstrap /path/to/project
56
63
  cp /path/to/project/.ai/env.example /path/to/project/.ai/.env
57
64
  $EDITOR /path/to/project/.ai/.env # see "Configuration" below
58
65
  # Not the repo's owner? Add --untracked to keep the scaffold out of git
@@ -139,7 +146,7 @@ vendor or host.
139
146
  | `CAIRN_LLM_EXTRACTION_MODEL` | Chat model for `memory-capture` extraction |
140
147
  | `CAIRN_MEMORY_EMBEDDING_URL` | Embeddings endpoint (falls back to `CAIRN_LLM_API_URL`) |
141
148
  | `CAIRN_MEMORY_EMBEDDING_MODEL` | Embedding model name (required for semantic search) |
142
- | `CAIRN_AGENTFS_BASE_DIR` | Base dir for global memory scopes (default `~/.cairnkeep`) |
149
+ | `CAIRN_AGENTFS_BASE_DIR` | Server-side base dir for named/global memory scopes (default `~/.cairnkeep`); it does not affect `project` scope |
143
150
  | `CAIRN_GIT_PROVIDER` | Git host for collaboration commands: `github`\|`gitlab`\|`codeberg`\|`forgejo`\|`none`. See [git-providers.md](git-providers.md) |
144
151
  | `CAIRN_ROUTE_ENDPOINT` | Base URL of an already-running token-miser routing/tiering proxy (unset → the `route_check` tool is inert) |
145
152
  | `CAIRN_EXPLORE_BINARY` | Absolute path to the `token_miser` binary used by `context_explore` (unset → the tool throws at call time) |
@@ -243,10 +250,11 @@ Claude-Code-only path this milestone — there is no OpenCode parity plugin.
243
250
 
244
251
  ### HTTP transport (opt-in, network-facing)
245
252
 
246
- The server runs over stdio by default. Setting `MCP_HTTP_PORT` switches it to a
247
- streamable HTTP transport so one long-lived process can serve many clients — but
248
- because that exposes every memory tool over the network, HTTP mode is guarded and
249
- **fails closed**:
253
+ The server runs locally over stdio by default. Setting `MCP_HTTP_PORT` switches
254
+ it to a streamable HTTP transport so one long-lived process can serve many
255
+ clients within one trusted storage domain. The databases then live on the HTTP
256
+ server host. Because that exposes every memory tool over the network, HTTP mode
257
+ is guarded and **fails closed**:
250
258
 
251
259
  | Variable | Purpose |
252
260
  |---|---|
@@ -258,7 +266,10 @@ because that exposes every memory tool over the network, HTTP mode is guarded an
258
266
 
259
267
  Requests without a valid bearer token get `401`; requests with an unexpected
260
268
  `Host` header get `403`. Keep HTTP mode bound to `127.0.0.1` unless you have a
261
- specific reason to expose it, and use a long random token.
269
+ specific reason to expose it, and use a long random token. HTTP mode has no
270
+ per-user ACL or tenant isolation, and every client shares the server process's
271
+ `project` database. See [Memory storage and deployment](storage.md) for the
272
+ placement rules, client registration, TLS requirements, and backup boundaries.
262
273
 
263
274
  ## The workflow
264
275
 
@@ -335,9 +346,11 @@ its settings file and exports `CAIRN_EXTRA_SETTINGS` — no change to the launch
335
346
 
336
347
  ### `cairn doctor`
337
348
 
338
- Health-checks the configured pieces, reading only `./.ai/.env` (or the current
339
- env). Unconfigured optional dependencies are skipped; it exits non-zero only when
340
- a configured dependency (LLM/embedding endpoint, writable store) is unreachable.
349
+ Health-checks the bundled local memory server with a real MCP stdio handshake,
350
+ then checks `./.ai/.env` (or the current environment). It does not inspect a
351
+ harness's remote HTTP registration. Unconfigured optional dependencies are
352
+ skipped; it exits non-zero when the local server probe fails or a configured
353
+ dependency (LLM/embedding endpoint, writable store) is unreachable.
341
354
 
342
355
  ```bash
343
356
  cd /path/to/project && cairn doctor
@@ -345,7 +358,7 @@ cd /path/to/project && cairn doctor
345
358
 
346
359
  ### `cairn memory export|import|path`
347
360
 
348
- Relocate the durable memory store (one SQLite `.db` per scope under
361
+ Relocate named/global memory (one SQLite `.db` per scope under
349
362
  `CAIRN_AGENTFS_BASE_DIR`) between machines or backends:
350
363
 
351
364
  ```bash
@@ -354,6 +367,11 @@ cairn memory export store.tgz # WAL-safe snapshot of every scope db
354
367
  cairn memory import store.tgz # restore on another machine (backs up existing)
355
368
  ```
356
369
 
370
+ `cairn memory export` requires the `sqlite3` CLI so it can use SQLite's online
371
+ backup operation and produce a consistent snapshot while WAL mode is active.
372
+ It does not include project memory at `<project>/.agentfs/project.db`; see
373
+ [Memory storage and deployment](storage.md) for project backup instructions.
374
+
357
375
  ### `cairn audit-timer`
358
376
 
359
377
  `memory-wiki-audit.sh` is the deterministic invalidation backstop meant to run on
@@ -0,0 +1,134 @@
1
+ # Memory storage and deployment
2
+
3
+ ## The placement rule
4
+
5
+ Cairnkeep stores memory on the filesystem of the machine running the
6
+ `cairn-memory` server process. It does not discover a cloud service, VPS, or
7
+ shared host, and installing the npm package does not contact one.
8
+
9
+ The standard registration is local:
10
+
11
+ ```bash
12
+ claude mcp add cairn-memory -s user -- cairn memory-server
13
+ ```
14
+
15
+ This starts `cairn memory-server` as a local stdio child process. With that
16
+ registration, memory remains on that computer.
17
+
18
+ | Scope | Database path |
19
+ |---|---|
20
+ | `project` | `<server working directory>/.agentfs/project.db` |
21
+ | Any named/global scope, such as `identity` or `work` | `${CAIRN_AGENTFS_BASE_DIR:-~/.cairnkeep}/<scope>.db` |
22
+
23
+ For the local launchers, the server working directory is normally the project
24
+ root. Bootstrap installs `.agentfs/.gitignore` so private project memory is not
25
+ accidentally committed. SQLite may also create `-wal` and `-shm` sidecar files.
26
+
27
+ `CAIRN_LLM_API_URL` and `CAIRN_MEMORY_EMBEDDING_URL` select optional model
28
+ services used to process/search memory. They do not change where the SQLite
29
+ databases are stored. Git-provider and routing configuration do not change
30
+ storage either.
31
+
32
+ ## Remote HTTP mode
33
+
34
+ Remote storage is explicit. An operator starts Cairnkeep in HTTP mode on a
35
+ server, configures storage in that server process's environment, and registers
36
+ the resulting URL in each client harness. For example:
37
+
38
+ ```bash
39
+ # On the server host. Put TLS in front of this listener before remote use.
40
+ CAIRN_AGENTFS_BASE_DIR=/var/lib/cairnkeep \
41
+ CAIRN_MEMORY_HTTP_TOKEN="$(openssl rand -hex 32)" \
42
+ CAIRN_MEMORY_HTTP_ALLOWED_HOSTS=memory.example.com \
43
+ MCP_HTTP_HOST=127.0.0.1 MCP_HTTP_PORT=7801 \
44
+ cairn memory-server
45
+
46
+ # On a client. CAIRN_MEMORY_HTTP_TOKEN must contain the server's token.
47
+ claude mcp add --transport http -s user \
48
+ --header "Authorization: Bearer $CAIRN_MEMORY_HTTP_TOKEN" \
49
+ cairn-memory https://memory.example.com/mcp
50
+ ```
51
+
52
+ With this topology, the databases are on the server host, not the client PC.
53
+ Setting `CAIRN_AGENTFS_BASE_DIR` on the client does not redirect a remote
54
+ server; set it in the server service environment.
55
+
56
+ HTTP mode is one trusted storage domain:
57
+
58
+ - One bearer token grants access to every exposed memory tool.
59
+ - There is no per-user ACL, tenant isolation, or client-specific filesystem.
60
+ - `project` scope resolves from the server process's working directory, so all
61
+ HTTP clients of that process share the same `project.db`.
62
+ - Use one server instance per isolation boundary. Do not offer one instance to
63
+ mutually untrusted users.
64
+ - Keep the Cairnkeep listener on loopback behind a TLS reverse proxy, or use an
65
+ encrypted private network. Do not expose its raw HTTP listener publicly.
66
+
67
+ ## Inspecting and moving memory
68
+
69
+ For a local server, these commands report and move named/global scopes only:
70
+
71
+ ```bash
72
+ cairn memory path
73
+ cairn memory export global-memory.tgz
74
+ cairn memory import global-memory.tgz
75
+ ```
76
+
77
+ `cairn memory path` reports the local process's global-scope directory; it
78
+ cannot inspect a harness's remote HTTP registration. Export uses SQLite's
79
+ online backup operation and requires the `sqlite3` CLI.
80
+
81
+ Project memory is separate at `<project>/.agentfs/project.db` and is not
82
+ included in `cairn memory export`. Back it up while the server is stopped, or
83
+ take an online snapshot with SQLite:
84
+
85
+ ```bash
86
+ sqlite3 /path/to/project/.agentfs/project.db \
87
+ ".backup '/safe/path/project-memory.db'"
88
+ ```
89
+
90
+ Treat every database and export archive as sensitive. They may contain source
91
+ paths, decisions, incident details, and other project context.
92
+
93
+ ## Automating trusted personal clients
94
+
95
+ Keep fleet-specific values in a private dotfiles repository or secret manager,
96
+ not in Cairnkeep. A personal bootstrap can install the public package and then
97
+ perform the explicit remote registration:
98
+
99
+ ```bash
100
+ # Populate these from a private secret manager on each trusted PC.
101
+ export CAIRN_MEMORY_REMOTE_URL=https://memory.example.com/mcp
102
+ export CAIRN_MEMORY_HTTP_TOKEN=replace-from-secret-manager
103
+
104
+ npm install -g @cairnkeep/cli
105
+
106
+ claude mcp remove cairn-memory -s user 2>/dev/null || true
107
+ claude mcp add --transport http -s user \
108
+ cairn-memory "$CAIRN_MEMORY_REMOTE_URL" \
109
+ --header "Authorization: Bearer $CAIRN_MEMORY_HTTP_TOKEN"
110
+ ```
111
+
112
+ OpenCode is configured separately in its per-user `opencode.json`. It supports
113
+ environment references in remote MCP headers, so the token does not need to be
114
+ written literally into the JSON:
115
+
116
+ ```json
117
+ {
118
+ "mcp": {
119
+ "cairn-memory": {
120
+ "type": "remote",
121
+ "url": "https://memory.example.com/mcp",
122
+ "oauth": false,
123
+ "headers": {
124
+ "Authorization": "Bearer {env:CAIRN_MEMORY_HTTP_TOKEN}"
125
+ }
126
+ }
127
+ }
128
+ }
129
+ ```
130
+
131
+ Ensure the secret-manager bootstrap exports `CAIRN_MEMORY_HTTP_TOKEN` before
132
+ OpenCode starts. Preserve any other keys already present in `opencode.json`
133
+ rather than replacing the whole file. The URL, token, private host names, and
134
+ device-specific configuration must never be committed to this public project.
@@ -669,7 +669,7 @@ function gitToplevel(cwd) {
669
669
  // Factory: each MCP client/session needs its own McpServer instance (the SDK
670
670
  // only allows one connected transport per server). All instances share the
671
671
  // module-level helpers + AgentFS below. Enables a single long-lived process to
672
- // serve many concurrent clients (centralized AgentFS on the VPS).
672
+ // serve many concurrent clients within one trusted server-side storage domain.
673
673
  function createMemoryServer() {
674
674
  const server = new McpServer({ name: "cairn-memory", version: "0.1.0" });
675
675
  server.registerTool("memory_read", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cairnkeep/cli",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
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",
@@ -32,9 +32,12 @@
32
32
  "zod": "^3.25.76"
33
33
  },
34
34
  "scripts": {
35
+ "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",
36
+ "fix:perms": "git ls-files -s | awk '$1==\"100755\"{print $4}' | xargs chmod 755",
37
+ "prepack": "npm run build:server && npm run fix:perms",
35
38
  "test": "for test in scripts/test-*.sh; do \"$test\" || exit 1; done",
36
39
  "check:public": "scripts/verify-no-private-references.sh && scripts/verify-docs-parity.sh && npm test",
37
- "prepublishOnly": "npm run check:public && npm --prefix mcp-memory-server ci && npm --prefix mcp-memory-server test"
40
+ "prepublishOnly": "npm run check:public && npm run build:server && npm --prefix mcp-memory-server test --global=false --dry-run=false"
38
41
  },
39
42
  "repository": {
40
43
  "type": "git",
@@ -57,6 +57,10 @@ install_file "$TPL/start-claude.sh.template" "$target/.ai/start-claude.sh" 0
57
57
  install_file "$TPL/start-opencode.sh.template" "$target/.ai/start-opencode.sh" 0755
58
58
  install_file "$TPL/env.example.template" "$target/.ai/env.example" 0644
59
59
 
60
+ # Project-scope memory is private runtime data. Keep it out of version control
61
+ # even before the first .agentfs/project.db is created.
62
+ install_file "$TPL/agentfs-gitignore.template" "$target/.agentfs/.gitignore" 0644
63
+
60
64
  # .planning/ derived-knowledge layer (template : destination)
61
65
  planning_pairs=(
62
66
  "planning-config.json.template:.planning/config.json"
@@ -77,7 +81,7 @@ done
77
81
 
78
82
  if [[ $untracked -eq 1 ]]; then
79
83
  mkdir -p "$(dirname "$exclude_file")"
80
- for entry in "/${exclude_prefix}.ai/" "/${exclude_prefix}.planning/"; do
84
+ for entry in "/${exclude_prefix}.ai/" "/${exclude_prefix}.planning/" "/${exclude_prefix}.agentfs/"; do
81
85
  if grep -qxF "$entry" "$exclude_file" 2>/dev/null; then
82
86
  echo "skip (already excluded): $entry"
83
87
  else
@@ -93,7 +97,7 @@ echo
93
97
  echo "Cairnkeep bootstrapped into $target"
94
98
  echo "Next steps (full guide: docs/operating.md):"
95
99
  echo " 1. cp .ai/env.example .ai/.env and edit"
96
- echo " 2. Build + register the memory server: cd mcp-memory-server && npm install && npm run build,"
97
- echo " then: claude mcp add cairn-memory -s user -- node \"\$PWD/dist/index.js\""
98
- echo " 3. Install the operating layer (commands/agents/hooks): scripts/sync-claude-assets.sh --apply"
100
+ echo " 2. Register the memory server: claude mcp add cairn-memory -s user -- cairn memory-server"
101
+ echo " 3. Install the operating layer (commands/agents/hooks): cairn sync --apply"
102
+ echo " From a source clone, use bin/cairn in place of cairn."
99
103
  echo " 4. Launch: $target/.ai/start-claude.sh"
package/scripts/doctor.sh CHANGED
@@ -3,9 +3,9 @@
3
3
  #
4
4
  # Reports pass/warn/skip/fail for the pieces cairnkeep needs, reading only what
5
5
  # is already configured in ./.ai/.env (or the current environment). Unconfigured
6
- # optional dependencies are SKIPPED, never failed. Exits non-zero only if a
7
- # CONFIGURED dependency is unreachable, so a green run means "the things you
8
- # turned on actually work".
6
+ # optional dependencies are SKIPPED, never failed. The required memory server
7
+ # is checked with a real MCP initialize exchange. Exits non-zero if that probe
8
+ # fails or a configured dependency is unreachable.
9
9
  set -uo pipefail
10
10
 
11
11
  CAIRN_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
@@ -30,11 +30,18 @@ reachable() { curl -sS -m 5 -o /dev/null "$1" >/dev/null 2>&1; }
30
30
 
31
31
  echo "cairn doctor"
32
32
 
33
- # 1. Memory server built (local prerequisite; actionable warning, not a failure).
34
- if [[ -f "$CAIRN_ROOT/mcp-memory-server/dist/index.js" ]]; then
35
- pass "memory server built (mcp-memory-server/dist/index.js)"
33
+ # 1. Required memory server: prove that the shipped build and dependencies can
34
+ # complete an MCP stdio handshake, rather than only checking for one file.
35
+ server="$CAIRN_ROOT/mcp-memory-server/dist/index.js"
36
+ probe="$CAIRN_ROOT/scripts/probe-memory-server.mjs"
37
+ if ! command -v node >/dev/null 2>&1; then
38
+ fail "Node.js not found (Node.js 18 or newer is required)"
39
+ elif [[ ! -f "$server" ]]; then
40
+ fail "memory server not built — run: (cd \"$CAIRN_ROOT/mcp-memory-server\" && npm install && npm run build)"
41
+ elif node "$probe" "$server" >/dev/null 2>&1; then
42
+ pass "bundled local memory server responds over MCP stdio"
36
43
  else
37
- warn "memory server not built — run: (cd \"$CAIRN_ROOT/mcp-memory-server\" && npm install && npm run build)"
44
+ fail "memory server failed its MCP stdio probe — reinstall cairnkeep or rebuild mcp-memory-server"
38
45
  fi
39
46
 
40
47
  # 2. LLM extraction endpoint (optional; unset → substring-only memory search).
@@ -83,6 +90,14 @@ else
83
90
  else fail "memory store parent not writable: $parent"; fi
84
91
  fi
85
92
 
93
+ # 6. Optional backup dependency. Runtime memory and imports do not need it,
94
+ # but exports use sqlite3's .backup command for a consistent WAL snapshot.
95
+ if command -v sqlite3 >/dev/null 2>&1; then
96
+ pass "sqlite3 available (memory export enabled)"
97
+ else
98
+ warn "sqlite3 not found — memory export unavailable (runtime and import unaffected)"
99
+ fi
100
+
86
101
  echo
87
102
  if [[ "$fails" -gt 0 ]]; then
88
103
  echo "cairn doctor: $fails configured dependency check(s) failed."
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env bash
2
- # cairn memory <path|export|import> — relocate the durable memory store between
3
- # machines or backends.
2
+ # cairn memory <path|export|import> — relocate global-scope memory between
3
+ # machines or backends. Project-scope memory lives in <project>/.agentfs/ and
4
+ # is intentionally outside this command's base directory.
4
5
  #
5
- # The store is one SQLite .db per scope under CAIRN_AGENTFS_BASE_DIR
6
- # (default ~/.cairnkeep). `export` takes a WAL-safe snapshot of every scope db
7
- # into a portable archive; `import` restores them on another machine; `path`
8
- # prints the store location so nothing has to guess where it lives.
6
+ # The global store is one SQLite .db per named scope under
7
+ # CAIRN_AGENTFS_BASE_DIR (default ~/.cairnkeep). `export` takes a WAL-safe
8
+ # snapshot of every global-scope db into a portable archive; `import` restores
9
+ # them on another machine; `path` prints the global store location.
9
10
  set -euo pipefail
10
11
 
11
12
  base="${CAIRN_AGENTFS_BASE_DIR:-$HOME/.cairnkeep}"
@@ -14,9 +15,9 @@ base="${base/#\~/$HOME}"
14
15
  usage() {
15
16
  cat <<'EOF'
16
17
  Usage: cairn memory <path|export|import>
17
- path Print the memory store base directory.
18
- export <file.tgz> WAL-safe snapshot of every scope .db into a portable archive.
19
- import <file.tgz> Restore scope .db files from an archive (backs up existing).
18
+ path Print the global-scope memory base directory.
19
+ export <file.tgz> WAL-safe snapshot of every global-scope .db.
20
+ import <file.tgz> Restore global-scope .db files (backs up existing).
20
21
  EOF
21
22
  }
22
23
 
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+
4
+ const serverPath = process.argv[2];
5
+ if (!serverPath) {
6
+ console.error("usage: probe-memory-server.mjs PATH_TO_SERVER");
7
+ process.exit(2);
8
+ }
9
+
10
+ const child = spawn(process.execPath, [serverPath], {
11
+ env: process.env,
12
+ stdio: ["pipe", "pipe", "pipe"],
13
+ });
14
+
15
+ let stderr = "";
16
+ child.stderr.setEncoding("utf8");
17
+ child.stderr.on("data", (chunk) => {
18
+ stderr = (stderr + chunk).slice(-2000);
19
+ });
20
+
21
+ const response = new Promise((resolve, reject) => {
22
+ let buffered = "";
23
+ child.stdout.setEncoding("utf8");
24
+ child.stdout.on("data", (chunk) => {
25
+ buffered += chunk;
26
+ const lines = buffered.split("\n");
27
+ buffered = lines.pop() ?? "";
28
+ for (const line of lines) {
29
+ if (!line.trim()) continue;
30
+ try {
31
+ const message = JSON.parse(line);
32
+ if (message.id === 1 && message.result?.serverInfo?.name === "cairn-memory") {
33
+ resolve();
34
+ return;
35
+ }
36
+ } catch {
37
+ // Ignore non-protocol output while waiting for the initialize response.
38
+ }
39
+ }
40
+ });
41
+ child.once("error", reject);
42
+ child.once("exit", (code) => reject(new Error(`server exited with status ${code}`)));
43
+ });
44
+
45
+ const timeout = new Promise((_, reject) => {
46
+ setTimeout(() => reject(new Error("MCP initialize timed out")), 5000).unref();
47
+ });
48
+
49
+ const initialize = {
50
+ jsonrpc: "2.0",
51
+ id: 1,
52
+ method: "initialize",
53
+ params: {
54
+ protocolVersion: "2024-11-05",
55
+ capabilities: {},
56
+ clientInfo: { name: "cairn-doctor", version: "1.0" },
57
+ },
58
+ };
59
+
60
+ try {
61
+ child.stdin.end(`${JSON.stringify(initialize)}\n`);
62
+ await Promise.race([response, timeout]);
63
+ } catch (error) {
64
+ const detail = stderr.trim() || error.message;
65
+ console.error(`memory server probe failed: ${detail}`);
66
+ process.exitCode = 1;
67
+ } finally {
68
+ child.kill();
69
+ }
@@ -24,18 +24,22 @@ git -C "$tmp/repo" init -q
24
24
  "$ROOT/scripts/bootstrap.sh" --untracked "$tmp/repo" >/dev/null
25
25
  [[ -f "$tmp/repo/.ai/start-claude.sh" ]] || fail "scaffold missing"
26
26
  [[ -f "$tmp/repo/.planning/config.json" ]] || fail "planning layer missing"
27
+ [[ -f "$tmp/repo/.agentfs/.gitignore" ]] || fail "project-memory ignore guard missing"
27
28
  [[ -z "$(git -C "$tmp/repo" status --porcelain)" ]] || fail "scaffold visible to git"
28
29
  grep -qxF "/.ai/" "$tmp/repo/.git/info/exclude" || fail "missing /.ai/ exclude entry"
29
30
  grep -qxF "/.planning/" "$tmp/repo/.git/info/exclude" || fail "missing /.planning/ exclude entry"
31
+ grep -qxF "/.agentfs/" "$tmp/repo/.git/info/exclude" || fail "missing /.agentfs/ exclude entry"
30
32
 
31
33
  # Re-run is idempotent: no duplicate exclude entries
32
34
  "$ROOT/scripts/bootstrap.sh" --untracked "$tmp/repo" >/dev/null
33
35
  [[ $(grep -cxF "/.ai/" "$tmp/repo/.git/info/exclude") -eq 1 ]] || fail "duplicate exclude entries"
36
+ [[ $(grep -cxF "/.agentfs/" "$tmp/repo/.git/info/exclude") -eq 1 ]] || fail "duplicate .agentfs exclude entry"
34
37
 
35
38
  # Default mode is unchanged: scaffold stays visible to git
36
39
  mkdir "$tmp/repo2"
37
40
  git -C "$tmp/repo2" init -q
38
41
  "$ROOT/scripts/bootstrap.sh" "$tmp/repo2" >/dev/null
39
42
  git -C "$tmp/repo2" status --porcelain | grep -q "\.ai/" || fail "default mode should leave the scaffold tracked"
43
+ git -C "$tmp/repo2" status --porcelain | grep -q "\.agentfs/" || fail "default mode should track the memory ignore guard"
40
44
 
41
45
  echo "PASS: bootstrap --untracked contributor mode"
@@ -11,6 +11,9 @@ cairn="$ROOT/bin/cairn"
11
11
  "$cairn" help | grep -q "cairn sync" || fail "help missing sync"
12
12
  "$cairn" help | grep -q "cairn memory-server" || fail "help missing memory-server"
13
13
  "$cairn" sync --help >/dev/null 2>&1 || fail "cairn sync dispatch"
14
+ if "$cairn" not-a-command >/dev/null 2>&1; then
15
+ fail "unknown command should exit non-zero"
16
+ fi
14
17
 
15
18
  tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
16
19
  CAIRN_AGENTFS_BASE_DIR="$tmp/store" "$cairn" memory path | grep -qx "$tmp/store" || fail "cairn memory path dispatch"
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env bash
2
+ # Prove that the actual npm tarball installs and runs without relying on files
3
+ # or dependencies from the source checkout.
4
+ set -euo pipefail
5
+
6
+ ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
7
+ tmp=$(mktemp -d)
8
+ trap 'rm -rf "$tmp"' EXIT
9
+ fail() { echo "FAIL: $1" >&2; exit 1; }
10
+
11
+ cd "$ROOT"
12
+ npm pack --silent --pack-destination "$tmp" --dry-run=false >/dev/null
13
+ tarballs=("$tmp"/*.tgz)
14
+ [[ ${#tarballs[@]} -eq 1 && -f "${tarballs[0]}" ]] || fail "npm pack did not produce exactly one tarball"
15
+
16
+ npm install -g --silent --prefix "$tmp/prefix" --dry-run=false "${tarballs[0]}"
17
+ export HOME="$tmp/home"
18
+ export PATH="$tmp/prefix/bin:$PATH"
19
+ mkdir -p "$HOME" "$tmp/project"
20
+
21
+ expected=$(node -p 'require(process.argv[1]).version' "$ROOT/package.json")
22
+ [[ "$(cairn version)" == "cairnkeep $expected" ]] || fail "installed CLI reports the wrong version"
23
+
24
+ bootstrap_output=$(cairn bootstrap "$tmp/project")
25
+ [[ "$bootstrap_output" == *"cairn memory-server"* ]] || fail "bootstrap printed invalid memory-server setup instructions"
26
+ [[ "$bootstrap_output" == *"cairn sync --apply"* ]] || fail "bootstrap printed invalid operating-layer setup instructions"
27
+ [[ -x "$tmp/project/.ai/start-claude.sh" ]] || fail "bootstrap did not install an executable Claude launcher"
28
+ [[ -f "$tmp/project/.planning/config.json" ]] || fail "bootstrap did not install the planning scaffold"
29
+
30
+ (cd "$tmp/project" && cairn doctor) >/dev/null || fail "installed package failed cairn doctor"
31
+
32
+ echo "PASS: npm tarball installs a self-contained CLI and MCP server"
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env bash
2
+ # Regression guard for the 744-perms packaging bug. A root-owned `sudo npm i -g`
3
+ # makes root the file owner, so a non-root user can only run cairn if "other"
4
+ # has the execute bit. A publisher umask can strip it at pack time; the
5
+ # `fix:perms` script re-applies git's tracked executable bit. This verifies it
6
+ # heals a umask-mangled entrypoint back to 755 (world-executable).
7
+ set -euo pipefail
8
+ ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
9
+ cd "$ROOT"
10
+ fail() { echo "FAIL: $1" >&2; exit 1; }
11
+
12
+ fix_perms=$(node -e 'process.stdout.write(require("./package.json").scripts["fix:perms"]||"")')
13
+ [[ -n "$fix_perms" ]] || fail "package.json has no fix:perms script"
14
+
15
+ entry=bin/cairn
16
+ trap 'chmod 755 "$entry" 2>/dev/null || true' EXIT
17
+ chmod 744 "$entry" # simulate a stripping umask
18
+ sh -c "$fix_perms" # run the real packaging command
19
+ mode=$(stat -c '%a' "$entry" 2>/dev/null || stat -f '%Lp' "$entry") # GNU || BSD/macOS
20
+ [[ "$mode" == "755" ]] || fail "fix:perms did not restore $entry to 755 (got $mode) -- 744 packaging bug"
21
+ echo "OK: fix:perms restores shipped executables to 755"
@@ -58,11 +58,13 @@ EXCL="$PROJ/.git/info/exclude"
58
58
  mkdir -p "$SB/home/.cairnkeep"; echo data >"$SB/home/.cairnkeep/db"
59
59
  "$ROOT_DIR/scripts/uninstall.sh" --yes --purge-memory --live-root "$SB/home/.claude" "$PROJ" >/dev/null 2>&1
60
60
  check "project .ai removed" "$([[ -e "$PROJ/.ai" ]] && echo no || echo yes)" "yes"
61
- check "exclude lines stripped" "$(grep -cE '\.ai/|\.planning/' "$EXCL" 2>/dev/null || true)" "0"
61
+ check "project .agentfs removed" "$([[ -e "$PROJ/.agentfs" ]] && echo no || echo yes)" "yes"
62
+ check "exclude lines stripped" "$(grep -cE '\.ai/|\.planning/|\.agentfs/' "$EXCL" 2>/dev/null || true)" "0"
62
63
  check "memory store purged" "$([[ -e "$SB/home/.cairnkeep" ]] && echo no || echo yes)" "yes"
63
64
  BK2=$(ls -dt "$SB/home/.cairnkeep-uninstall-"* 2>/dev/null | head -1)
64
65
  bash "$BK2/revert.sh" >/dev/null 2>&1
65
66
  check "project scaffold restored" "$([[ -d "$PROJ/.ai" ]] && echo yes || echo no)" "yes"
67
+ check "project ignore restored" "$([[ -f "$PROJ/.agentfs/.gitignore" ]] && echo yes || echo no)" "yes"
66
68
  check "memory store restored" "$(cat "$SB/home/.cairnkeep/db" 2>/dev/null)" "data"
67
69
 
68
70
  echo
@@ -10,7 +10,8 @@
10
10
  # Undoes: the operating layer (`cairn sync --apply` — commands, agents,
11
11
  # templates, hooks + their settings.json registrations), the MCP registration
12
12
  # (`claude mcp add cairn-memory`), the opt-in audit timer, and — for any project
13
- # paths you pass — the `cairn bootstrap` scaffold. The npm package itself is
13
+ # paths you pass — the `cairn bootstrap` scaffold. Project `.agentfs/` memory is
14
+ # retained unless `--purge-memory` is given. The npm package itself is
14
15
  # npm's to remove; the final step is printed, not run (it would delete this
15
16
  # script mid-execution).
16
17
  set -uo pipefail
@@ -39,9 +40,8 @@ $HOME/.cairnkeep-uninstall-<ts>/ with a revert.sh before any change.
39
40
 
40
41
  --dry-run Print what would happen; touch nothing.
41
42
  --yes Do not prompt for confirmation.
42
- --purge-memory Also remove the durable memory store ($HOME/.cairnkeep or
43
- $CAIRN_AGENTFS_BASE_DIR). Backed up first, but this is your
44
- accumulated memory — off by default.
43
+ --purge-memory Also remove the global memory store and .agentfs/ memory in
44
+ each PROJECT. Backed up first; off by default.
45
45
  --live-root PATH Claude root to clean (default: $CLAUDE_CONFIG_DIR or ~/.claude).
46
46
  PROJECT ... Also revert `cairn bootstrap` in these project dirs
47
47
  (.ai/, .planning/, and any .git/info/exclude entries).
@@ -209,16 +209,26 @@ for proj in "${PROJECTS[@]:-}"; do
209
209
  echo "Project scaffold: $proj"
210
210
  remove_path "$proj/.ai"
211
211
  remove_path "$proj/.planning"
212
+ if [[ $PURGE_MEMORY -eq 1 ]]; then
213
+ remove_path "$proj/.agentfs"
214
+ elif [[ -d "$proj/.agentfs" ]]; then
215
+ echo " kept project memory: $proj/.agentfs"
216
+ fi
212
217
  # Strip the .git/info/exclude lines contributor-mode bootstrap added.
213
218
  if excl=$(cd "$proj" && git rev-parse --git-path info/exclude 2>/dev/null); then
214
219
  [[ "$excl" == /* ]] || excl="$proj/$excl"
215
220
  prefix=$(cd "$proj" && git rev-parse --show-prefix 2>/dev/null)
216
221
  if [[ -f "$excl" ]] && grep -qxF "/${prefix}.ai/" "$excl" 2>/dev/null; then
217
222
  if [[ $DRY_RUN -eq 1 ]]; then
218
- echo " would strip .ai/ + .planning/ from ${excl}"
223
+ echo " would strip scaffold entries from ${excl}"
219
224
  else
220
225
  back_up "$excl"
221
- grep -vxF -e "/${prefix}.ai/" -e "/${prefix}.planning/" "$excl" >"$excl.tmp" && mv "$excl.tmp" "$excl"
226
+ if [[ $PURGE_MEMORY -eq 1 ]]; then
227
+ grep -vxF -e "/${prefix}.ai/" -e "/${prefix}.planning/" -e "/${prefix}.agentfs/" "$excl" >"$excl.tmp" || true
228
+ else
229
+ grep -vxF -e "/${prefix}.ai/" -e "/${prefix}.planning/" "$excl" >"$excl.tmp" || true
230
+ fi
231
+ mv "$excl.tmp" "$excl"
222
232
  echo " stripped exclude entries from ${excl}"
223
233
  fi
224
234
  fi
@@ -0,0 +1,3 @@
1
+ # Project memory is private runtime data.
2
+ *
3
+ !.gitignore
@@ -12,9 +12,13 @@
12
12
  # One of: github | gitlab | codeberg | forgejo | none. See docs/git-providers.md.
13
13
  # CAIRN_GIT_PROVIDER=github
14
14
  #
15
- # The memory server runs over stdio by default. HTTP mode is opt-in and network
16
- # facing, so it is guarded: setting MCP_HTTP_PORT REQUIRES a bearer token, or the
17
- # server refuses to start. Clients must send `Authorization: Bearer <token>`.
15
+ # Storage belongs to the host running the memory-server process. Named/global
16
+ # scopes default to ~/.cairnkeep; project scope uses <server-cwd>/.agentfs/.
17
+ # CAIRN_AGENTFS_BASE_DIR=~/.cairnkeep
18
+ #
19
+ # The memory server runs locally over stdio by default. These HTTP settings are
20
+ # for the SERVER host of an explicit remote deployment, not a normal local
21
+ # client. Setting MCP_HTTP_PORT REQUIRES a bearer token, or startup fails.
18
22
  # CAIRN_MEMORY_HTTP_TOKEN=change-me-to-a-long-random-secret
19
23
  # MCP_HTTP_PORT=7801
20
24
  # MCP_HTTP_HOST=127.0.0.1