@misterhuydo/cairn-mcp 1.6.8 → 1.6.10

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
@@ -70,12 +70,49 @@ No manual steps. The index lives in `.cairn/index.db` inside your project — li
70
70
  | `cairn_resume` | Restore last session + re-index only changed files |
71
71
  | `cairn_search` | Find classes, functions, components by name or concept |
72
72
  | `cairn_describe` | Summarize what a folder or module does |
73
- | `cairn_outline` | Project-wide structural outline + heuristic issue detection (god classes, lifecycle gaps, naming inconsistencies, missing tests) |
73
+ | `cairn_outline` | Structural outline + heuristic issue detection. On large federated projects returns a per-service summary; use `repo` param to drill into one service |
74
74
  | `cairn_code_graph` | Dependency health — instability, cycles, load-bearing modules |
75
75
  | `cairn_security` | Scan for XSS, SQLi, hardcoded secrets, weak crypto, and more |
76
76
  | `cairn_todos` | Scan codebase for TODO/FIXME/HACK comments, add manual items, resolve and list them |
77
77
  | `cairn_bundle` | Minified source snapshot (auto-handled by hooks) |
78
78
  | `cairn_checkpoint` | Save session state (auto-handled by hooks) |
79
+ | `cairn_minify` | Minify a single file on demand (fallback when hooks are not installed) |
80
+ | `cairn_switch` | Switch active project root mid-session (use for maintenance on a sibling service) |
81
+ | `cairn_memo` | Save a preference, decision, or discovery to the project's persistent memory |
82
+ | `cairn_employ_memory` | Recall stored memories explicitly (call only when asked) |
83
+
84
+ ---
85
+
86
+ ## Multi-service / monorepo support
87
+
88
+ Cairn understands workspace structures where multiple services or packages live under a common parent folder.
89
+
90
+ **How it works:**
91
+
92
+ - Each service has its own `.cairn/index.db` — it is the authoritative index for that service
93
+ - When `cairn_maintain` runs at the **parent** folder, it skips files already covered by a sub-project and instead federates all sub-indexes via SQLite `ATTACH` — no duplication
94
+ - When a session opens **inside a subfolder** (e.g. `elprint-component-service/`), Cairn automatically discovers and mounts siblings:
95
+ 1. If the parent folder has a `.cairn/index.db` (already initialized) → uses its registry
96
+ 2. Otherwise → scans the parent directory for any sibling folders that already have a `.cairn/index.db`
97
+
98
+ The result: every service session has full cross-service visibility for search, outline, and bundle — no manual setup required.
99
+
100
+ **Drilling into a service from a federated session:**
101
+
102
+ ```
103
+ cairn_outline { repo: "elprint-component-service" } → outline one service
104
+ cairn_search { query: "PartRepository" } → searches all services
105
+ ```
106
+
107
+ **When you need to re-index a sibling service:**
108
+
109
+ ```
110
+ cairn_switch { path: "/path/to/elprint-project-service" }
111
+ cairn_maintain
112
+ cairn_switch { path: "/path/to/elprint-component-service" }
113
+ ```
114
+
115
+ `cairn_switch` is the explicit escape hatch for maintenance — it changes the active project root so `cairn_maintain` and `cairn_checkpoint` target the right service.
79
116
 
80
117
  ---
81
118
 
package/index.js CHANGED
@@ -17,8 +17,10 @@ import { todos } from './src/tools/todos.js';
17
17
  import { memo } from './src/tools/memo.js';
18
18
  import { switchProject } from './src/tools/switch.js';
19
19
  import { employMemory } from './src/tools/employMemory.js';
20
+ import { startBackgroundWatcher, deltaReindex, markFullyIndexed } from './src/indexer/liveIndex.js';
20
21
 
21
22
  const db = openDB();
23
+ startBackgroundWatcher(db);
22
24
 
23
25
  const server = new Server(
24
26
  { name: 'cairn', version: '1.0.0' },
@@ -281,17 +283,20 @@ Use to: get a project-wide bird's-eye view, triage code quality, find where to l
281
283
  ],
282
284
  }));
283
285
 
286
+ const READ_TOOLS = new Set(['cairn_search', 'cairn_bundle', 'cairn_describe', 'cairn_outline', 'cairn_code_graph', 'cairn_security']);
287
+
284
288
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
285
289
  const { name, arguments: args } = request.params;
290
+ if (READ_TOOLS.has(name)) await deltaReindex(db);
286
291
  switch (name) {
287
- case 'cairn_maintain': return await maintain(db, args);
292
+ case 'cairn_maintain': { const r = await maintain(db, args); markFullyIndexed(); return r; }
288
293
  case 'cairn_search': return search(db, args);
289
294
  case 'cairn_describe': return describe(db, args);
290
295
  case 'cairn_code_graph': return codeGraph(db, args);
291
296
  case 'cairn_security': return security(db, args);
292
297
  case 'cairn_bundle': return await bundle(db, args);
293
298
  case 'cairn_checkpoint': return checkpoint(db, args);
294
- case 'cairn_resume': return await resume(db);
299
+ case 'cairn_resume': { const r = await resume(db); markFullyIndexed(); return r; }
295
300
  case 'cairn_outline': return outlineProject(db, args);
296
301
  case 'cairn_minify': return minify(db, args);
297
302
  case 'cairn_todos': return await todos(db, args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/cairn-mcp",
3
- "version": "1.6.8",
3
+ "version": "1.6.10",
4
4
  "description": "MCP server that gives Claude Code persistent memory across sessions. Index your codebase once, search symbols, bundle source, scan for vulnerabilities, and checkpoint/resume work — across Java, TypeScript, Vue, Python, SQL and more.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -0,0 +1,51 @@
1
+ import fs from 'fs';
2
+ import { walkRepo } from './fileWalker.js';
3
+ import { getProjectRoot } from '../graph/cwd.js';
4
+ import { incrementalMaintain } from '../tools/maintain.js';
5
+
6
+ let lastIndexedAt = Date.now();
7
+ let reindexing = false;
8
+
9
+ /**
10
+ * Call after cairn_maintain or cairn_resume so the background watcher
11
+ * doesn't redundantly re-scan files that were just fully indexed.
12
+ */
13
+ export function markFullyIndexed() {
14
+ lastIndexedAt = Date.now();
15
+ }
16
+
17
+ /**
18
+ * Scan for files modified since lastIndexedAt and reindex them.
19
+ * Safe to call concurrently — skips if a run is already in progress.
20
+ */
21
+ export async function deltaReindex(db) {
22
+ if (reindexing) return;
23
+ reindexing = true;
24
+ const checkFrom = lastIndexedAt;
25
+ lastIndexedAt = Date.now();
26
+ try {
27
+ const allFiles = await walkRepo(getProjectRoot());
28
+ const stale = [];
29
+ for (const filePath of allFiles) {
30
+ try {
31
+ if (fs.statSync(filePath).mtimeMs > checkFrom) stale.push(filePath);
32
+ } catch {
33
+ // deleted or inaccessible — ignore
34
+ }
35
+ }
36
+ if (stale.length > 0) {
37
+ await incrementalMaintain(db, stale);
38
+ }
39
+ } finally {
40
+ reindexing = false;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Start a background interval that keeps the DB in sync with the filesystem.
46
+ * Uses unref() so the interval won't prevent clean process exit.
47
+ */
48
+ export function startBackgroundWatcher(db, intervalMs = 30_000) {
49
+ const timer = setInterval(() => deltaReindex(db), intervalMs);
50
+ if (timer.unref) timer.unref();
51
+ }
@@ -8,7 +8,7 @@ export const VULN_PATTERNS = [
8
8
 
9
9
  { id: 'CWE-89', lang: ['java'], severity: 'HIGH',
10
10
  name: 'SQL Injection',
11
- pattern: /"SELECT|INSERT|UPDATE|DELETE.*"\s*\+/i,
11
+ pattern: /"(SELECT|INSERT|UPDATE|DELETE)\b.*"\s*\+/i,
12
12
  fix: 'Use PreparedStatement with parameterized queries' },
13
13
 
14
14
  { id: 'CWE-326', lang: ['java'], severity: 'HIGH',
@@ -34,7 +34,7 @@ export const VULN_PATTERNS = [
34
34
 
35
35
  { id: 'CWE-89', lang: ['javascript', 'typescript'], severity: 'HIGH',
36
36
  name: 'SQL Injection (JS)',
37
- pattern: /`\s*(SELECT|INSERT|UPDATE|DELETE).*\$\{/i,
37
+ pattern: /`\s*(SELECT|INSERT|UPDATE|DELETE)\b.*\$\{/i,
38
38
  fix: 'Use parameterized queries or an ORM' },
39
39
 
40
40
  { id: 'CWE-798', lang: ['javascript', 'typescript', 'vue'], severity: 'HIGH',
@@ -3,6 +3,11 @@ import path from 'path';
3
3
  import { getCairnDir } from '../graph/cwd.js';
4
4
 
5
5
  export function checkpoint(_db, { message, active_files = [], notes = [] }) {
6
+ // Coerce notes to array in case the caller passed a pre-serialized JSON string
7
+ if (typeof notes === 'string') {
8
+ try { notes = JSON.parse(notes); } catch { notes = notes ? [notes] : []; }
9
+ }
10
+ if (!Array.isArray(notes)) notes = [];
6
11
  const cairnDir = getCairnDir();
7
12
  const sessionPath = path.join(cairnDir, 'session.json');
8
13
 
@@ -31,6 +31,11 @@ export async function resume(db) {
31
31
  }
32
32
 
33
33
  const session = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
34
+ // Guard against double-serialized notes (stored as JSON string instead of array)
35
+ if (typeof session.notes === 'string') {
36
+ try { session.notes = JSON.parse(session.notes); } catch { session.notes = []; }
37
+ }
38
+ if (!Array.isArray(session.notes)) session.notes = [];
34
39
 
35
40
  // Detect and auto-repair project relocation before any path comparisons
36
41
  let relocation = null;