carto-md 2.0.4 → 2.0.6

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/CONTRIBUTING.md CHANGED
@@ -44,9 +44,9 @@ Framework-specific route and model extraction lives inside the language plugins.
44
44
 
45
45
  ---
46
46
 
47
- ## How to add a language (V2 pattern — tree-sitter based)
47
+ ## How to add a language (tree-sitter based)
48
48
 
49
- V2 uses tree-sitter for import and symbol extraction. Babel is only used for deep JS/TS route/model extraction on API handler files.
49
+ Carto uses tree-sitter for import and symbol extraction. Babel is only used for deep JS/TS route/model extraction on API handler files.
50
50
 
51
51
  ### Step 1: Install the grammar
52
52
 
@@ -177,7 +177,7 @@ Test on at least 2 real open-source projects using the framework.
177
177
 
178
178
  ---
179
179
 
180
- ## How domain clustering works (V2)
180
+ ## How domain clustering works
181
181
 
182
182
  Domain detection uses **Leiden+CPM graph clustering** (`src/agents/leiden.js`). Files that import each other heavily cluster together. Domain names are inferred from path tokens, with keyword hints for well-known patterns.
183
183
 
@@ -215,7 +215,7 @@ cd carto
215
215
  npm install
216
216
  node src/cli/index.js init # test in any project
217
217
  node src/cli/index.js serve # test MCP server
218
- npm test # run test suite (35 tests)
218
+ npm test # run test suite (62 tests)
219
219
  node test/correctness.js # run correctness tests (31 tests)
220
220
  node test/benchmark.js # run benchmarks against real repos
221
221
  ```
@@ -258,7 +258,7 @@ npm run test:bench-ci # self-bench (matches what bench.yml runs)
258
258
  - [ ] Extension added to `CODE_EXTS` and `detectLanguage()` in `sync-v2.js`
259
259
  - [ ] No changes to merger logic (unless explicitly fixing a merger bug)
260
260
  - [ ] No network calls added
261
- - [ ] `npm test` passes (35/35)
261
+ - [ ] `npm test` passes (62/62)
262
262
  - [ ] `node test/correctness.js` passes (31/31)
263
263
 
264
264
  ---
package/README.md CHANGED
@@ -138,7 +138,7 @@ Reads `tsconfig.json` / `jsconfig.json` for `paths` config. `@/components/Button
138
138
 
139
139
  ## Domain detection
140
140
 
141
- V2 uses **Leiden+CPM graph clustering** — files that import each other heavily cluster together. Domain names are inferred from path tokens, with keyword hints for well-known patterns (AUTH, PAYMENTS, DATABASE, etc.).
141
+ Carto uses **Leiden+CPM graph clustering** — files that import each other heavily cluster together. Domain names are inferred from path tokens, with keyword hints for well-known patterns (AUTH, PAYMENTS, DATABASE, etc.).
142
142
 
143
143
  Works on any repo — not just SaaS apps. vscode gets AUTH/EVENTS/DATABASE. zed (Rust) gets DATABASE/AUTH/EVENTS. A game engine would get RENDERER/PHYSICS/AUDIO.
144
144
 
@@ -227,27 +227,10 @@ Other agents are smart but blind. Carto sees the architecture:
227
227
 
228
228
  ---
229
229
 
230
- ## V1 → V2 migration
231
-
232
- Run `carto sync` — it auto-migrates your existing `graph-cache.json` and `map.json` into SQLite and renames them to `.bak`. No manual steps.
233
-
234
- What changed under the hood:
235
-
236
- | | V1 | V2 |
237
- |---|---|---|
238
- | Storage | JSON blobs | SQLite (WAL mode, indexed queries) |
239
- | Parsing | Babel-only | tree-sitter for all languages, Babel only for deep route/model extraction |
240
- | File limit | 300 cap | Unlimited |
241
- | Languages | JS/TS/Python/Go/R | + Rust/Java/C++/C#/Ruby |
242
- | Domain detection | Hardcoded keywords | Leiden+CPM graph clustering |
243
- | Watcher | Per-file chokidar | Single recursive directory watch (<20 file descriptors) |
244
- | MCP startup | Re-indexes on start | Opens SQLite instantly (<10ms) |
245
- | Path aliases | Not resolved | `@/`, `~/` resolved via tsconfig |
246
-
247
- ---
248
-
249
230
  ## MCP config (if auto-wire missed your IDE)
250
231
 
232
+ `carto init` auto-wires into Kiro, Cursor, and Claude Desktop. If it missed yours, add manually:
233
+
251
234
  **Kiro** — `~/.kiro/settings/mcp.json`
252
235
  ```json
253
236
  { "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
@@ -263,6 +246,16 @@ What changed under the hood:
263
246
  { "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
264
247
  ```
265
248
 
249
+ **VS Code Copilot** — `.vscode/mcp.json` in your project root, or Command Palette → `MCP: Add Server`
250
+ ```json
251
+ { "servers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
252
+ ```
253
+
254
+ **Windsurf** — `~/.windsurf/mcp.json`
255
+ ```json
256
+ { "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
257
+ ```
258
+
266
259
  ---
267
260
 
268
261
  ## What Carto never does
package/index.js CHANGED
@@ -4,17 +4,27 @@
4
4
  * carto-md — public module API
5
5
  *
6
6
  * Usage:
7
- * const { Carto } = require('carto-md');
8
- * const carto = new Carto();
7
+ * const { StoreAdapter } = require('carto-md');
8
+ * const carto = new StoreAdapter();
9
9
  * await carto.index('/path/to/project');
10
10
  *
11
- * // Get everything Kepler needs for a file
11
+ * // Get everything an editor agent needs for a file
12
12
  * const ctx = carto.getContextForFile('src/auth/auth.service.ts');
13
13
  *
14
- * // Listen for live updates
15
- * carto.on('updated', ({ file, blastRadius }) => { ... });
14
+ * // Read structural facts
15
+ * const routes = carto.getRoutes();
16
+ * const radius = carto.getBlastRadius('src/auth/auth.service.ts');
17
+ * carto.close();
18
+ *
19
+ * Back-compat: `Carto` is exported as an alias for `StoreAdapter`.
20
+ * Existing programs that did `const { Carto } = require('carto-md')`
21
+ * continue to work without changes. The alias (and the `terminate()`
22
+ * method on StoreAdapter) will be removed in 3.0.0.
16
23
  */
17
24
 
18
- const { Carto } = require('./src/engine/carto');
25
+ const { StoreAdapter } = require('./src/store/store-adapter');
19
26
 
20
- module.exports = { Carto };
27
+ module.exports = {
28
+ StoreAdapter,
29
+ Carto: StoreAdapter, // deprecated alias, removed in 3.0.0
30
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carto-md",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "description": "Structural intelligence layer for AI coding tools. Indexes your codebase into SQLite — routes, models, import graph, blast radius, domains — and exposes 16 MCP tools for Kiro, Cursor, and Claude.",
5
5
  "bin": {
6
6
  "carto": "src/cli/index.js"
@@ -3,7 +3,7 @@
3
3
  const crypto = require('crypto');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
- const { Carto } = require('../engine/carto');
6
+ const { StoreAdapter } = require('../store/store-adapter');
7
7
 
8
8
  class Session {
9
9
  constructor(id, workingDir) {
@@ -23,20 +23,16 @@ class Session {
23
23
  if (this._indexed) return null;
24
24
 
25
25
  const dbPath = path.join(this.workingDir, '.carto', 'carto.db');
26
- this.carto = new Carto();
26
+ const wasIndexed = fs.existsSync(dbPath);
27
27
 
28
- if (fs.existsSync(dbPath)) {
29
- // DB exists — just load the index (fast path, <10ms)
30
- await this.carto.index(this.workingDir);
31
- this._indexed = true;
32
- return null;
33
- }
34
-
35
- // No DB — run full indexing
28
+ this.carto = new StoreAdapter();
36
29
  const start = Date.now();
37
- await this.carto.index(this.workingDir, { useWorkers: true });
30
+ await this.carto.index(this.workingDir, { writeOutputs: false });
38
31
  this._indexed = true;
39
32
 
33
+ if (wasIndexed) return null;
34
+
35
+ // Fresh index — surface status message
40
36
  const meta = this.carto.getMeta();
41
37
  const duration = ((Date.now() - start) / 1000).toFixed(1);
42
38
  const domains = this.carto.getDomainsList();
@@ -64,6 +60,8 @@ class SessionManager {
64
60
  }
65
61
 
66
62
  delete(id) {
63
+ const s = this._sessions.get(id);
64
+ if (s && s.carto) s.carto.close();
67
65
  this._sessions.delete(id);
68
66
  }
69
67
  }
package/src/cli/check.js CHANGED
@@ -4,8 +4,10 @@ const { execSync } = require('child_process');
4
4
  const path = require('path');
5
5
  const fs = require('fs');
6
6
  const { SQLiteStore } = require('../store/sqlite-store');
7
+ const { checkForUpdate } = require('./update-check');
7
8
 
8
9
  async function run(projectRoot) {
10
+ checkForUpdate(); // fire and forget
9
11
  const dbPath = path.join(projectRoot, '.carto', 'carto.db');
10
12
 
11
13
  if (!fs.existsSync(dbPath)) {
package/src/cli/impact.js CHANGED
@@ -1,17 +1,18 @@
1
1
  'use strict';
2
2
 
3
- const path = require('path');
4
- const { Carto } = require('../../index.js');
3
+ const { StoreAdapter } = require('../store/store-adapter');
4
+ const { checkForUpdate } = require('./update-check');
5
5
 
6
6
  async function run(projectRoot, fileArg) {
7
+ checkForUpdate(); // fire and forget
7
8
  if (!fileArg) {
8
9
  console.error('[CARTO] Usage: carto impact <file>');
9
10
  process.exit(1);
10
11
  }
11
12
 
12
- const carto = new Carto();
13
+ const carto = new StoreAdapter();
13
14
  try {
14
- await carto.index(projectRoot, { useWorkers: false });
15
+ await carto.index(projectRoot);
15
16
  } catch (err) {
16
17
  console.error(`[CARTO] Error loading index: ${err.message}`);
17
18
  process.exit(1);
@@ -20,6 +21,7 @@ async function run(projectRoot, fileArg) {
20
21
  const br = carto.getBlastRadius(fileArg);
21
22
  if (!br) {
22
23
  console.error(`[CARTO] File not found in project graph: ${fileArg}`);
24
+ carto.close();
23
25
  process.exit(1);
24
26
  }
25
27
 
@@ -53,7 +55,7 @@ async function run(projectRoot, fileArg) {
53
55
  }
54
56
 
55
57
  console.log('');
56
- carto.terminate();
58
+ carto.close();
57
59
  }
58
60
 
59
61
  module.exports = { run };
package/src/cli/init.js CHANGED
@@ -3,8 +3,10 @@ const path = require('path');
3
3
  const { detectFramework } = require('../detector/framework');
4
4
  const { parseCartoIgnore } = require('../security/ignore');
5
5
  const { runSyncV2, discoverFiles: discoverFilesV2 } = require('../store/sync-v2');
6
+ const { checkForUpdate } = require('./update-check');
6
7
 
7
8
  async function run(projectRoot) {
9
+ checkForUpdate(); // fire and forget
8
10
  console.log('[CARTO] Detecting project...');
9
11
 
10
12
  const detection = detectFramework(projectRoot);
@@ -52,8 +54,6 @@ async function run(projectRoot) {
52
54
  installGitHook(projectRoot);
53
55
 
54
56
  // Run first sync — V2 SQLite-backed indexer.
55
- // (Previously: V1 runFullSync with empty file lists from resolveConfig — produced
56
- // a 23ms no-op that left .carto/carto.db missing and AGENTS.md unpopulated.)
57
57
  await runSyncV2({
58
58
  projectRoot,
59
59
  output: path.resolve(projectRoot, config.output || 'AGENTS.md')
@@ -65,22 +65,6 @@ async function run(projectRoot) {
65
65
  console.log('[CARTO] AGENTS.md generated. Carto will sync on every git commit.');
66
66
  }
67
67
 
68
- /**
69
- * Resolves config paths to absolute paths.
70
- * V2: no watch file lists — sync-v2 discovers files itself.
71
- */
72
- function resolveConfig(projectRoot, config) {
73
- return {
74
- watch: {
75
- routeFiles: (config.watch && config.watch.routeFiles || []).map(f => path.resolve(projectRoot, f)),
76
- modelFiles: (config.watch && config.watch.modelFiles || []).map(f => path.resolve(projectRoot, f)),
77
- frontendFiles: (config.watch && config.watch.frontendFiles || []).map(f => path.resolve(projectRoot, f))
78
- },
79
- output: path.resolve(projectRoot, config.output || 'AGENTS.md'),
80
- projectRoot
81
- };
82
- }
83
-
84
68
  function installGitHook(projectRoot) {
85
69
  const gitDir = path.join(projectRoot, '.git');
86
70
  if (!fs.existsSync(gitDir)) return;
@@ -161,4 +145,4 @@ function wireIDEs(projectRoot) {
161
145
  }
162
146
  }
163
147
 
164
- module.exports = { run, resolveConfig };
148
+ module.exports = { run };
package/src/cli/serve.js CHANGED
@@ -1,23 +1,24 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs');
3
+ const { checkForUpdate } = require('./update-check');
3
4
 
4
5
  function run(projectRoot) {
5
- // Prefer V2 SQLite-backed server
6
+ checkForUpdate(); // fire and forget
6
7
  const dbPath = path.join(projectRoot, '.carto', 'carto.db');
7
- if (fs.existsSync(dbPath)) {
8
- console.error('[CARTO] MCP server starting (V2 SQLite)...');
9
- require('../mcp/server-v2');
10
- return;
11
- }
12
8
 
13
- // Fallback to V1 if no SQLite DB exists
14
- const mapPath = path.join(projectRoot, '.carto', 'map.json');
15
- if (!fs.existsSync(mapPath)) {
16
- console.error('[CARTO] No index found. Run `carto sync` first.');
9
+ if (!fs.existsSync(dbPath)) {
10
+ // Could be pre-2.0.4 install with only map.json, or never initialized.
11
+ const mapPath = path.join(projectRoot, '.carto', 'map.json');
12
+ if (fs.existsSync(mapPath)) {
13
+ console.error('[CARTO] Legacy index found (map.json) but no SQLite DB. Run `carto init` to upgrade your index.');
14
+ } else {
15
+ console.error('[CARTO] No index found. Run `carto init` first.');
16
+ }
17
17
  process.exit(1);
18
18
  }
19
- console.error('[CARTO] MCP server starting (V1 compat)...');
20
- require('../mcp/server');
19
+
20
+ console.error('[CARTO] MCP server starting...');
21
+ require('../mcp/server-v2');
21
22
  }
22
23
 
23
24
  module.exports = { run };
package/src/cli/sync.js CHANGED
@@ -1,8 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { runSyncV2 } = require('../store/sync-v2');
4
- const { runFullSync } = require('../sync');
5
- const { resolveConfig } = require('./init');
6
4
  const { checkForUpdate } = require('./update-check');
7
5
 
8
6
  async function run(projectRoot) {
@@ -22,20 +20,10 @@ async function run(projectRoot) {
22
20
  process.exit(1);
23
21
  }
24
22
 
25
- // Use V2 SQLite-backed sync
26
- const v2Config = {
23
+ await runSyncV2({
27
24
  projectRoot,
28
25
  output: path.resolve(projectRoot, config.output || 'AGENTS.md')
29
- };
30
-
31
- try {
32
- await runSyncV2(v2Config);
33
- } catch (err) {
34
- // Fallback to V1 if V2 fails (e.g., better-sqlite3 not available)
35
- console.warn(`[CARTO] V2 sync failed (${err.message}), falling back to V1`);
36
- const resolved = resolveConfig(projectRoot, config);
37
- await runFullSync(resolved);
38
- }
26
+ });
39
27
 
40
28
  console.log('[CARTO] Sync complete.');
41
29
  }
@@ -1,12 +1,21 @@
1
+ 'use strict';
2
+
1
3
  const https = require('https');
2
4
  const pkg = require('../../package.json');
3
5
 
4
6
  /**
5
7
  * Fire-and-forget version check against the npm registry.
6
- * Prints a one-liner to stderr if a newer version exists.
7
- * Never throws, never blocks safe to call without await.
8
+ *
9
+ * Prints a visually distinct notice to stderr if a newer version is on npm.
10
+ * Never throws, never blocks (3s socket timeout, ignores all errors) —
11
+ * safe to call without await from any CLI command's entry point.
12
+ *
13
+ * Set `CARTO_NO_UPDATE_CHECK=1` to disable entirely (used in test runs and
14
+ * by users who don't want the egress).
8
15
  */
9
16
  function checkForUpdate() {
17
+ if (process.env.CARTO_NO_UPDATE_CHECK) return;
18
+
10
19
  const req = https.get('https://registry.npmjs.org/carto-md/latest', {
11
20
  timeout: 3000,
12
21
  }, (res) => {
@@ -16,10 +25,8 @@ function checkForUpdate() {
16
25
  try {
17
26
  const data = JSON.parse(body);
18
27
  const latest = data.version;
19
- if (latest && latest !== pkg.version && isNewer(latest, pkg.version)) {
20
- process.stderr.write(
21
- `[CARTO] Update available: ${pkg.version} → ${latest} | npm install -g carto-md\n`
22
- );
28
+ if (latest && isNewer(latest, pkg.version)) {
29
+ process.stderr.write(formatNotice(pkg.version, latest));
23
30
  }
24
31
  } catch (_) {
25
32
  // malformed JSON — ignore
@@ -27,6 +34,10 @@ function checkForUpdate() {
27
34
  });
28
35
  });
29
36
 
37
+ // unref so the pending request never delays process exit on its own —
38
+ // if the main command finishes first, the check is silently dropped.
39
+ if (typeof req.unref === 'function') req.unref();
40
+
30
41
  req.on('timeout', () => { req.destroy(); });
31
42
  req.on('error', () => { /* offline / DNS failure — ignore */ });
32
43
  }
@@ -45,4 +56,36 @@ function isNewer(a, b) {
45
56
  return false;
46
57
  }
47
58
 
48
- module.exports = { checkForUpdate };
59
+ /**
60
+ * formatNotice(current, latest) → multi-line stderr string.
61
+ *
62
+ * TTY-aware: emits ANSI yellow + bold when stderr is a TTY, plain text
63
+ * otherwise (so piped output and CI logs stay clean).
64
+ *
65
+ * Format (TTY example, ★ in yellow, version + command in bold):
66
+ *
67
+ * <blank line>
68
+ * [CARTO] ★ Update available: 2.0.1 → 2.0.5
69
+ * [CARTO] Run: npm install -g carto-md
70
+ * <blank line>
71
+ *
72
+ * Two blank lines (one before, one after) separate the notice from
73
+ * regular [CARTO] log output so the upgrade prompt stands out even when
74
+ * it lands mid-progress (the check is fire-and-forget, so the response
75
+ * arrives whenever the network resolves).
76
+ */
77
+ function formatNotice(current, latest) {
78
+ const useColor = !!process.stderr.isTTY && !process.env.NO_COLOR;
79
+ const yellow = (s) => useColor ? `\x1b[33m${s}\x1b[0m` : s;
80
+ const bold = (s) => useColor ? `\x1b[1m${s}\x1b[0m` : s;
81
+
82
+ return [
83
+ '',
84
+ `${yellow('[CARTO] ★ Update available:')} ${bold(`${current} → ${latest}`)}`,
85
+ `${yellow('[CARTO] Run:')} ${bold('npm install -g carto-md')}`,
86
+ '',
87
+ ''
88
+ ].join('\n');
89
+ }
90
+
91
+ module.exports = { checkForUpdate, isNewer, formatNotice };
@@ -6,6 +6,34 @@ const fs = require('fs');
6
6
 
7
7
  const SCHEMA_VERSION = '1';
8
8
 
9
+ /**
10
+ * normalizePath(p) — Canonicalize a relative path for storage and query.
11
+ *
12
+ * Converts backslashes to forward slashes (Windows → POSIX), strips leading
13
+ * './', and rejects absolute paths (callers must `path.relative(projectRoot, p)`
14
+ * before passing in).
15
+ *
16
+ * Single-source-of-truth for "what does a row in `files.path` look like?".
17
+ * Apply this at every boundary that writes or queries the column, and every
18
+ * downstream join (imports.from_file_id → files.path) stays consistent.
19
+ *
20
+ * Cross-platform invariant: same code, same DB, same query results on macOS,
21
+ * Linux, and Windows. Closes Bug 2 (carto impact path normalization) and the
22
+ * Windows-only "0 dependents" failures in the Store adapter test suite.
23
+ */
24
+ function normalizePath(p) {
25
+ if (typeof p !== 'string' || p.length === 0) return p;
26
+ let out = p;
27
+ // Backslash → forward slash (Windows → POSIX). path.sep is '\\' on Windows.
28
+ if (path.sep !== '/') out = out.split(path.sep).join('/');
29
+ // Some inputs may already use mixed separators (e.g., `src\utils/foo.js`).
30
+ // Normalize aggressively.
31
+ out = out.replace(/\\/g, '/');
32
+ // Strip leading './'
33
+ if (out.startsWith('./')) out = out.slice(2);
34
+ return out;
35
+ }
36
+
9
37
  class SQLiteStore {
10
38
  constructor(projectRoot) {
11
39
  this._projectRoot = projectRoot;
@@ -213,7 +241,7 @@ class SQLiteStore {
213
241
  // ─── File operations ───────────────────────────────────────────────────
214
242
 
215
243
  getFileByPath(relPath) {
216
- return this._db.prepare('SELECT * FROM files WHERE path = ?').get(relPath);
244
+ return this._db.prepare('SELECT * FROM files WHERE path = ?').get(normalizePath(relPath));
217
245
  }
218
246
 
219
247
  getFileById(id) {
@@ -230,7 +258,8 @@ class SQLiteStore {
230
258
  }
231
259
 
232
260
  upsertFile(relPath, { language, hash, mtime, size }) {
233
- const existing = this.getFileByPath(relPath);
261
+ const norm = normalizePath(relPath);
262
+ const existing = this.getFileByPath(norm);
234
263
  if (existing) {
235
264
  this._db.prepare(
236
265
  'UPDATE files SET language=?, hash=?, mtime=?, size=?, last_indexed_at=? WHERE id=?'
@@ -239,18 +268,18 @@ class SQLiteStore {
239
268
  }
240
269
  const info = this._db.prepare(
241
270
  'INSERT INTO files (path, language, hash, mtime, size, last_indexed_at) VALUES (?,?,?,?,?,?)'
242
- ).run(relPath, language, hash, mtime, size, Date.now());
271
+ ).run(norm, language, hash, mtime, size, Date.now());
243
272
  return info.lastInsertRowid;
244
273
  }
245
274
 
246
275
  updateFileMtime(relPath, mtime, size) {
247
276
  this._db.prepare(
248
277
  'UPDATE files SET mtime=?, size=? WHERE path=?'
249
- ).run(mtime, size, relPath);
278
+ ).run(mtime, size, normalizePath(relPath));
250
279
  }
251
280
 
252
281
  removeFile(relPath) {
253
- this._db.prepare('DELETE FROM files WHERE path = ?').run(relPath);
282
+ this._db.prepare('DELETE FROM files WHERE path = ?').run(normalizePath(relPath));
254
283
  }
255
284
 
256
285
  removeStaleFiles(currentPaths) {
@@ -744,4 +773,4 @@ class SQLiteStore {
744
773
  }
745
774
  }
746
775
 
747
- module.exports = { SQLiteStore };
776
+ module.exports = { SQLiteStore, normalizePath };