@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 +38 -1
- package/index.js +7 -2
- package/package.json +1 -1
- package/src/indexer/liveIndex.js +51 -0
- package/src/indexer/securityScanner.js +2 -2
- package/src/tools/checkpoint.js +5 -0
- package/src/tools/resume.js +5 -0
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` |
|
|
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':
|
|
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':
|
|
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.
|
|
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',
|
package/src/tools/checkpoint.js
CHANGED
|
@@ -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
|
|
package/src/tools/resume.js
CHANGED
|
@@ -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;
|