carto-md 2.0.6 → 2.0.7
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 +175 -132
- package/docs/screenshots/claude-code-supabase.png +0 -0
- package/package.json +2 -2
- package/src/acp/agent.js +6 -13
- package/src/acp/providers/index.js +4 -12
- package/src/cli/impact.js +6 -1
- package/src/cli/init.js +15 -0
- package/src/extractors/imports.js +5 -1
- package/src/mcp/server-v2.js +51 -5
- package/src/security/ignore.js +56 -1
- package/src/store/path-utils.js +50 -0
- package/src/store/sqlite-store.js +31 -5
package/README.md
CHANGED
|
@@ -7,93 +7,104 @@
|
|
|
7
7
|
|
|
8
8
|
**The structural intelligence layer for AI coding tools.**
|
|
9
9
|
|
|
10
|
+
Your AI coding tool sees files. Carto sees architecture — domains, blast radius, import graph, routes. Every AI tool you use stops guessing about your codebase and starts knowing it.
|
|
11
|
+
|
|
10
12
|
```bash
|
|
11
13
|
npm install -g carto-md
|
|
12
14
|
cd your-project
|
|
13
15
|
carto init
|
|
14
16
|
```
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
That's it. Carto auto-wires itself into every AI tool you have installed. Restart your AI tool and it now knows your codebase architecturally.
|
|
19
|
+
|
|
20
|
+
**Works with:** Cursor · Claude Code · Kiro · Claude Desktop · Windsurf · VS Code Copilot · JetBrains · Zed
|
|
17
21
|
|
|
18
22
|
---
|
|
19
23
|
|
|
20
|
-
##
|
|
24
|
+
## Use it with your AI tool
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
`carto init` auto-wires the MCP server into every AI tool it detects. If yours wasn't detected, here's the manual config — **one block, copy-paste, done.**
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|------|----------|-------------|---------|-----------|------------|---------|--------------|
|
|
26
|
-
| [prisma/prisma](https://github.com/prisma/prisma) | TypeScript | 3,303 | 3,303 | **1.6s** | **178ms** | 2.2 MB | 3,590 |
|
|
27
|
-
| [supabase/supabase](https://github.com/supabase/supabase) | TypeScript | 6,818 | 6,746 | **4.9s** | **725ms** | 4.3 MB | 5,754 |
|
|
28
|
-
| [microsoft/vscode](https://github.com/microsoft/vscode) | TypeScript | 10,565 | 10,565 | **9.7s** | **1.2s** | 10.6 MB | 19,769 |
|
|
29
|
-
| [zed-industries/zed](https://github.com/zed-industries/zed) | Rust | 1,837 | 1,837 | **2.7s** | **83ms** | 4.7 MB | 2,176 |
|
|
28
|
+
### Cursor
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
`carto init` writes this for you. Manual: `~/.cursor/mcp.json`
|
|
31
|
+
```json
|
|
32
|
+
{ "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
|
|
33
|
+
```
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
### Claude Code (CLI)
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
One command from your project root:
|
|
38
|
+
```bash
|
|
39
|
+
claude mcp add carto -- carto serve
|
|
40
|
+
```
|
|
41
|
+
Or create `.mcp.json` at the project root:
|
|
42
|
+
```json
|
|
43
|
+
{ "mcpServers": { "carto": { "command": "carto", "args": ["serve"] } } }
|
|
44
|
+
```
|
|
36
45
|
|
|
37
|
-
|
|
38
|
-
|------|---------|
|
|
39
|
-
| prisma | DATABASE · CORE · EVENTS · AUTH |
|
|
40
|
-
| supabase | CORE · AUTH · DATABASE · PAYMENTS · EVENTS · NOTIFICATIONS · TRPC |
|
|
41
|
-
| vscode | CORE · AUTH · EVENTS · DATABASE · NOTIFICATIONS |
|
|
42
|
-
| zed (Rust) | CORE · DATABASE · AUTH · EVENTS · PAYMENTS · NOTIFICATIONS |
|
|
46
|
+
### Kiro
|
|
43
47
|
|
|
44
|
-
|
|
48
|
+
`carto init` writes this for you. Manual: `~/.kiro/settings/mcp.json`
|
|
49
|
+
```json
|
|
50
|
+
{ "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
|
|
51
|
+
```
|
|
45
52
|
|
|
46
|
-
|
|
53
|
+
### Claude Desktop
|
|
54
|
+
|
|
55
|
+
`carto init` writes this for you. Manual: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
56
|
+
```json
|
|
57
|
+
{ "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
|
|
58
|
+
```
|
|
47
59
|
|
|
48
|
-
|
|
60
|
+
### VS Code Copilot
|
|
49
61
|
|
|
62
|
+
`.vscode/mcp.json` in your project root, or Command Palette → `MCP: Add Server`
|
|
63
|
+
```json
|
|
64
|
+
{ "servers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
|
|
50
65
|
```
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
Leiden+CPM graph clustering → auto-detects domains
|
|
58
|
-
Computes reverse deps → blast radius for every file
|
|
59
|
-
Writes AGENTS.md + .carto/context/*.md (lazy, on-demand)
|
|
60
|
-
Auto-wires MCP into Kiro, Cursor, Claude Desktop
|
|
61
|
-
↓
|
|
62
|
-
carto watch
|
|
63
|
-
↓
|
|
64
|
-
File saved → debounce 50ms → re-parse 1 file → SQLite write → <50ms
|
|
66
|
+
|
|
67
|
+
### Windsurf
|
|
68
|
+
|
|
69
|
+
`~/.windsurf/mcp.json`
|
|
70
|
+
```json
|
|
71
|
+
{ "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
|
|
65
72
|
```
|
|
66
73
|
|
|
74
|
+
### Zed / JetBrains / VS Code (full agent mode)
|
|
75
|
+
|
|
76
|
+
Carto also runs as a full **ACP agent** — not just a passive MCP server, but an active coding agent with architectural awareness. See [ACP Agent](#acp-agent-zed--jetbrains--vs-code) below.
|
|
77
|
+
|
|
67
78
|
---
|
|
68
79
|
|
|
69
|
-
##
|
|
80
|
+
## What changes for your AI
|
|
70
81
|
|
|
71
|
-
|
|
82
|
+
Without carto, your AI greps text and guesses. With carto, it queries structural facts:
|
|
72
83
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
84
|
+
> *"Add rate limiting to /api/users"*
|
|
85
|
+
|
|
86
|
+
**Without carto:** AI grep-searches your codebase, finds 12 files mentioning "users", picks 3 at random, hopes for the best.
|
|
87
|
+
|
|
88
|
+
**With carto:** AI calls `get_change_plan("add rate limiting to /api/users")` and gets back:
|
|
89
|
+
- The exact route handler file
|
|
90
|
+
- All 7 files that import it (blast radius)
|
|
91
|
+
- The AUTH domain it lives in
|
|
92
|
+
- Similar middleware patterns already in the codebase
|
|
93
|
+
- Cross-domain dependencies that could break
|
|
94
|
+
|
|
95
|
+
Your AI's response goes from "here's a guess" to "here's the right change with the architectural context."
|
|
96
|
+
|
|
97
|
+
### In the wild
|
|
98
|
+
|
|
99
|
+

|
|
100
|
+
|
|
101
|
+
*Claude Code analyzing the [supabase](https://github.com/supabase/supabase) repo via carto. Real session, no editing — 5,974 files indexed in ~780ms, 86 routes, 4,839 import edges, 7 domains. The agent's own verdict at the bottom: "useful, especially for a large codebase like supabase. The blast radius + cross-domain tools are the most valuable."*
|
|
91
102
|
|
|
92
103
|
---
|
|
93
104
|
|
|
94
105
|
## Languages and frameworks
|
|
95
106
|
|
|
96
|
-
### Import graph + symbols (
|
|
107
|
+
### Import graph + symbols (any repo)
|
|
97
108
|
|
|
98
109
|
| Language | Extensions |
|
|
99
110
|
|----------|-----------|
|
|
@@ -106,16 +117,16 @@ File saved → debounce 50ms → re-parse 1 file → SQLite write → <50ms
|
|
|
106
117
|
| C# | `.cs` |
|
|
107
118
|
| Ruby | `.rb` |
|
|
108
119
|
|
|
109
|
-
### Route extraction (framework-
|
|
120
|
+
### Route extraction (framework-aware)
|
|
110
121
|
|
|
111
122
|
| Framework | Language |
|
|
112
123
|
|-----------|---------|
|
|
113
|
-
| Express, Next.js (App + Pages
|
|
124
|
+
| Express, Next.js (App + Pages), tRPC, React Router | TypeScript / JavaScript |
|
|
114
125
|
| FastAPI, Flask, Django | Python |
|
|
115
126
|
| Gin, Echo, Chi, net/http | Go |
|
|
116
127
|
| Actix-web, Axum, Rocket | Rust |
|
|
117
128
|
| Spring MVC / Boot, JAX-RS | Java |
|
|
118
|
-
| ASP.NET Core
|
|
129
|
+
| ASP.NET Core | C# |
|
|
119
130
|
| Rails, Sinatra | Ruby |
|
|
120
131
|
|
|
121
132
|
### Model extraction
|
|
@@ -127,52 +138,18 @@ File saved → debounce 50ms → re-parse 1 file → SQLite write → <50ms
|
|
|
127
138
|
| Go structs | Go |
|
|
128
139
|
| Rust structs | Rust |
|
|
129
140
|
| JPA `@Entity`, Java records | Java |
|
|
130
|
-
| EF Core
|
|
141
|
+
| EF Core, C# records | C# |
|
|
131
142
|
| ActiveRecord | Ruby |
|
|
132
143
|
|
|
133
144
|
### TypeScript path aliases
|
|
134
145
|
|
|
135
|
-
Reads `tsconfig.json` / `jsconfig.json`
|
|
136
|
-
|
|
137
|
-
---
|
|
138
|
-
|
|
139
|
-
## Domain detection
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
Custom domains via `carto.config.json`:
|
|
146
|
-
|
|
147
|
-
```json
|
|
148
|
-
{
|
|
149
|
-
"domains": {
|
|
150
|
-
"EDITOR": ["editor", "monaco", "text"],
|
|
151
|
-
"WORKBENCH": ["workbench", "layout", "panel"]
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
---
|
|
157
|
-
|
|
158
|
-
## Commands
|
|
159
|
-
|
|
160
|
-
| Command | What it does |
|
|
161
|
-
|---------|-------------|
|
|
162
|
-
| `carto init` | Detect project, index codebase, generate AGENTS.md, wire MCP |
|
|
163
|
-
| `carto sync` | Full re-index (skips unchanged files via mtime+size cache) |
|
|
164
|
-
| `carto watch` | Incremental live re-index on every file save (<50ms) |
|
|
165
|
-
| `carto serve` | Start MCP server for Kiro / Cursor / Claude |
|
|
166
|
-
| `carto agent` | Start ACP agent mode (for Zed / JetBrains / VS Code) |
|
|
167
|
-
| `carto impact <file>` | Blast radius: risk level, affected files, routes at risk |
|
|
168
|
-
| `carto check` | Cross-domain violations, high-risk uncommitted changes, domain health |
|
|
169
|
-
| `carto remove` | Remove AGENTS.md and .carto/ from project |
|
|
146
|
+
Reads `tsconfig.json` / `jsconfig.json` `paths` config. `@/components/Button` resolves to the actual file in the import graph — blast radius works correctly for Next.js and Vite projects.
|
|
170
147
|
|
|
171
148
|
---
|
|
172
149
|
|
|
173
150
|
## ACP Agent (Zed / JetBrains / VS Code)
|
|
174
151
|
|
|
175
|
-
Carto
|
|
152
|
+
Beyond MCP, Carto runs as a full **ACP agent** — an active coding agent with built-in architectural awareness.
|
|
176
153
|
|
|
177
154
|
```
|
|
178
155
|
User: "Add rate limiting to /api/users"
|
|
@@ -189,20 +166,16 @@ Sends to LLM (your API key) → streams answer + diffs back to editor
|
|
|
189
166
|
|
|
190
167
|
### Setup in Zed
|
|
191
168
|
|
|
192
|
-
|
|
193
|
-
|
|
169
|
+
`~/.config/zed/settings.json`:
|
|
194
170
|
```json
|
|
195
171
|
{
|
|
196
172
|
"agent_servers": {
|
|
197
|
-
"Carto": {
|
|
198
|
-
"command": "carto",
|
|
199
|
-
"args": ["agent"]
|
|
200
|
-
}
|
|
173
|
+
"Carto": { "command": "carto", "args": ["agent"] }
|
|
201
174
|
}
|
|
202
175
|
}
|
|
203
176
|
```
|
|
204
177
|
|
|
205
|
-
###
|
|
178
|
+
### Bring Your Own Key
|
|
206
179
|
|
|
207
180
|
Carto supports any LLM provider — configure in your editor:
|
|
208
181
|
|
|
@@ -217,53 +190,123 @@ Carto supports any LLM provider — configure in your editor:
|
|
|
217
190
|
| Together AI | Open-source models |
|
|
218
191
|
| Azure OpenAI | Enterprise deployments |
|
|
219
192
|
|
|
220
|
-
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## The 16 MCP tools
|
|
196
|
+
|
|
197
|
+
Once carto is wired in, your AI tool can call any of these mid-task. You don't need to memorize them — your AI will pick the right ones.
|
|
198
|
+
|
|
199
|
+
| Tool | What it returns |
|
|
200
|
+
|------|----------------|
|
|
201
|
+
| `get_architecture()` | 500-word project overview: domains, entry points, stack, key patterns. **Use this first.** |
|
|
202
|
+
| `get_change_plan(intent)` | Given "add rate limiting to /api/users" → files to touch, domains affected, blast radius, similar patterns |
|
|
203
|
+
| `get_blast_radius(file)` | All files affected by changing a given file, with hop distance |
|
|
204
|
+
| `get_context(file)` | Everything about a file: domain, blast radius, neighbors, routes, models |
|
|
205
|
+
| `get_file_summary(file)` | What a file does, its role, key deps and dependents |
|
|
206
|
+
| `get_similar_patterns(file)` | Files with same domain, route shape, or shared deps — find conventions before writing new code |
|
|
207
|
+
| `get_routes()` | All API endpoints with file mapping |
|
|
208
|
+
| `get_structure()` | Import graph, entry points, high-impact files, tech stack |
|
|
209
|
+
| `get_domain(name)` | All routes, models, functions for a domain. Lazily regenerated when stale. |
|
|
210
|
+
| `get_neighbors(file, hops)` | Import graph neighbors: nodes and edges |
|
|
211
|
+
| `get_cross_domain()` | Import edges that cross domain boundaries |
|
|
212
|
+
| `search_routes(query)` | Search API routes by path or method |
|
|
213
|
+
| `get_models(domain?)` | All data models, optionally filtered by domain |
|
|
214
|
+
| `get_high_impact_files(n)` | Top N files by blast radius |
|
|
215
|
+
| `get_env_vars(domain?)` | All env vars with domain mapping |
|
|
216
|
+
| `get_domains_list()` | All detected domains with file, route, model counts |
|
|
221
217
|
|
|
222
|
-
|
|
223
|
-
- Auto-indexes your project on first session (1-10s depending on size)
|
|
224
|
-
- Injects structural context into every LLM call (blast radius, domains, routes)
|
|
225
|
-
- 12 internal tools the LLM can call during reasoning
|
|
226
|
-
- Zero cost to you beyond your own API key
|
|
218
|
+
**All MCP queries:** **<5ms** on every benchmarked repo.
|
|
227
219
|
|
|
228
220
|
---
|
|
229
221
|
|
|
230
|
-
##
|
|
222
|
+
## Domain detection
|
|
231
223
|
|
|
232
|
-
|
|
224
|
+
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.).
|
|
233
225
|
|
|
234
|
-
|
|
235
|
-
```json
|
|
236
|
-
{ "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
|
|
237
|
-
```
|
|
226
|
+
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.
|
|
238
227
|
|
|
239
|
-
|
|
228
|
+
Custom domains via `carto.config.json`:
|
|
240
229
|
```json
|
|
241
|
-
{
|
|
230
|
+
{
|
|
231
|
+
"domains": {
|
|
232
|
+
"EDITOR": ["editor", "monaco", "text"],
|
|
233
|
+
"WORKBENCH": ["workbench", "layout", "panel"]
|
|
234
|
+
}
|
|
235
|
+
}
|
|
242
236
|
```
|
|
243
237
|
|
|
244
|
-
|
|
245
|
-
```json
|
|
246
|
-
{ "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
|
|
247
|
-
```
|
|
238
|
+
---
|
|
248
239
|
|
|
249
|
-
|
|
250
|
-
```json
|
|
251
|
-
{ "servers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
|
|
252
|
-
```
|
|
240
|
+
## CLI commands
|
|
253
241
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
242
|
+
| Command | What it does |
|
|
243
|
+
|---------|-------------|
|
|
244
|
+
| `carto init` | Detect project, index codebase, generate AGENTS.md, auto-wire MCP into every AI tool found |
|
|
245
|
+
| `carto sync` | Full re-index (skips unchanged files via mtime+size cache) |
|
|
246
|
+
| `carto watch` | Incremental live re-index on every file save (<50ms) |
|
|
247
|
+
| `carto serve` | Start MCP server (called by your AI tool — usually you don't run this directly) |
|
|
248
|
+
| `carto agent` | Start ACP agent mode (for Zed / JetBrains / VS Code) |
|
|
249
|
+
| `carto impact <file>` | Blast radius: risk level, affected files, routes at risk |
|
|
250
|
+
| `carto check` | Cross-domain violations, high-risk uncommitted changes, domain health |
|
|
251
|
+
| `carto remove` | Remove AGENTS.md and .carto/ from project |
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Benchmarks
|
|
256
|
+
|
|
257
|
+
Measured on real open-source repos. Apple M-series, 8 CPUs, 8GB RAM. SHAs pinned in `~/carto-test-repos`.
|
|
258
|
+
|
|
259
|
+
| Repo | Language | Indexed Files | First Run | Second Run | DB Size | Import Edges |
|
|
260
|
+
|------|----------|---------------|-----------|------------|---------|--------------|
|
|
261
|
+
| [prisma/prisma](https://github.com/prisma/prisma) | TypeScript | 961 | **649ms** | **110ms** | 0.7 MB | 1,387 |
|
|
262
|
+
| [supabase/supabase](https://github.com/supabase/supabase) | TypeScript | 6,259 | **4.8s** | **751ms** | 4.0 MB | 5,321 |
|
|
263
|
+
| [microsoft/vscode](https://github.com/microsoft/vscode) | TypeScript | 7,567 | **7.8s** | **387ms** | 6.7 MB | 13,420 |
|
|
264
|
+
| [zed-industries/zed](https://github.com/zed-industries/zed) | Rust | 1,752 | **3.0s** | **182ms** | 4.4 MB | 2,113 |
|
|
265
|
+
|
|
266
|
+
**Indexed Files** counts what Carto actually parses — `.ts/.js/.py/.go/.rs/...` after excluding `node_modules`, build output, and per-file `*.test.*` / `*.spec.*` / `*.stories.*` / `test_*.py` patterns. The on-disk file count of the repo is larger.
|
|
267
|
+
|
|
268
|
+
**Second run** = only changed files re-parsed. mtime+size checked before reading content — if nothing changed, nothing is re-parsed.
|
|
269
|
+
|
|
270
|
+
### Domains detected
|
|
271
|
+
|
|
272
|
+
| Repo | Domains |
|
|
273
|
+
|------|---------|
|
|
274
|
+
| prisma | CORE · DATABASE · EVENTS · AUTH |
|
|
275
|
+
| supabase | CORE · AUTH · DATABASE · PAYMENTS · EVENTS · NOTIFICATIONS · TRPC |
|
|
276
|
+
| vscode | CORE · DATABASE · AUTH · EVENTS · NOTIFICATIONS |
|
|
277
|
+
| zed (Rust) | CORE · DATABASE · AUTH · EVENTS · PAYMENTS · NOTIFICATIONS · TRPC |
|
|
278
|
+
|
|
279
|
+
vscode at 7,567 indexed files in under 8 seconds. Rust import graph working on zed (2,113 edges from `mod` declarations and `use crate::` paths).
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## How it works
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
carto init / carto sync
|
|
287
|
+
↓
|
|
288
|
+
Discovers all files (no cap — SQLite handles the volume)
|
|
289
|
+
mtime+size check → skip unchanged files
|
|
290
|
+
tree-sitter parse → imports + symbols (0.05–0.2ms/file)
|
|
291
|
+
Babel deep parse → routes + models (API handler files only)
|
|
292
|
+
Leiden+CPM graph clustering → auto-detects domains
|
|
293
|
+
Computes reverse deps → blast radius for every file
|
|
294
|
+
Writes AGENTS.md + .carto/context/*.md (lazy, on-demand)
|
|
295
|
+
Auto-wires MCP into every AI tool found
|
|
296
|
+
↓
|
|
297
|
+
carto watch (optional)
|
|
298
|
+
↓
|
|
299
|
+
File saved → debounce 50ms → re-parse 1 file → SQLite write → <50ms
|
|
257
300
|
```
|
|
258
301
|
|
|
259
302
|
---
|
|
260
303
|
|
|
261
304
|
## What Carto never does
|
|
262
305
|
|
|
263
|
-
- Sends your code anywhere
|
|
264
|
-
- Writes secrets into AGENTS.md
|
|
265
|
-
- Touches your manual notes
|
|
266
|
-
- Costs money
|
|
306
|
+
- **Sends your code anywhere.** Local only. SQLite on disk.
|
|
307
|
+
- **Writes secrets into AGENTS.md.** `.cartoignore` blocks `.env` and credential files by default.
|
|
308
|
+
- **Touches your manual notes.** Writes only between `<!-- CARTO:AUTO:START -->` and `<!-- CARTO:AUTO:END -->`.
|
|
309
|
+
- **Costs money.** MIT license. Free forever.
|
|
267
310
|
|
|
268
311
|
---
|
|
269
312
|
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "carto-md",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.7",
|
|
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"
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"tree-sitter-typescript": "0.23.2"
|
|
30
30
|
},
|
|
31
31
|
"engines": {
|
|
32
|
-
"node": ">=18.0.0"
|
|
32
|
+
"node": ">=18.0.0 <26.0.0"
|
|
33
33
|
},
|
|
34
34
|
"author": {
|
|
35
35
|
"name": "Ansh Sonkar",
|
package/src/acp/agent.js
CHANGED
|
@@ -70,19 +70,12 @@ class CartoAgent {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
// Provider methods
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return {};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async unstable_disableProvider(_params) {
|
|
83
|
-
this.providers.disable();
|
|
84
|
-
return {};
|
|
85
|
-
}
|
|
73
|
+
// The custom `unstable_*` provider-management methods (list / set / disable)
|
|
74
|
+
// were removed in 2.0.7 (Spec 7 Bug 3). The ACP SDK only dispatches the
|
|
75
|
+
// methods declared in its AGENT_METHODS constant, so those custom routes
|
|
76
|
+
// returned -32601 "Method not found" anyway. Provider config flows
|
|
77
|
+
// through env vars / editor settings; ProviderRegistry remains as the
|
|
78
|
+
// internal configuration carrier.
|
|
86
79
|
|
|
87
80
|
// Session list/load stubs
|
|
88
81
|
async listSessions(_params) { return { sessions: [] }; }
|
|
@@ -30,20 +30,12 @@ class ProviderRegistry {
|
|
|
30
30
|
this._config = null;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
list() {
|
|
37
|
-
return SUPPORTED_PROVIDERS.map(p => ({
|
|
38
|
-
id: p.id,
|
|
39
|
-
name: p.name,
|
|
40
|
-
defaultModel: p.defaultModel,
|
|
41
|
-
models: p.models,
|
|
42
|
-
}));
|
|
43
|
-
}
|
|
33
|
+
// list() was removed in 2.0.7 (Spec 7 Bug 3). It was only called by the
|
|
34
|
+
// dropped custom provider-list ACP method, which the SDK never dispatched.
|
|
35
|
+
// SUPPORTED_PROVIDERS is still exported for external callers / tests.
|
|
44
36
|
|
|
45
37
|
/**
|
|
46
|
-
* set(params) — Configures the active provider
|
|
38
|
+
* set(params) — Configures the active provider.
|
|
47
39
|
* params: { providerId, apiKey, baseUrl?, model? }
|
|
48
40
|
*/
|
|
49
41
|
set(params) {
|
package/src/cli/impact.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { StoreAdapter } = require('../store/store-adapter');
|
|
4
|
+
const { normalizeFileArg } = require('../store/path-utils');
|
|
4
5
|
const { checkForUpdate } = require('./update-check');
|
|
5
6
|
|
|
6
7
|
async function run(projectRoot, fileArg) {
|
|
@@ -10,6 +11,10 @@ async function run(projectRoot, fileArg) {
|
|
|
10
11
|
process.exit(1);
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
// Normalize so `./lib/x.js`, absolute paths, and Windows separators
|
|
15
|
+
// all resolve to the same `files.path` value stored in SQLite.
|
|
16
|
+
const file = normalizeFileArg(projectRoot, fileArg);
|
|
17
|
+
|
|
13
18
|
const carto = new StoreAdapter();
|
|
14
19
|
try {
|
|
15
20
|
await carto.index(projectRoot);
|
|
@@ -18,7 +23,7 @@ async function run(projectRoot, fileArg) {
|
|
|
18
23
|
process.exit(1);
|
|
19
24
|
}
|
|
20
25
|
|
|
21
|
-
const br = carto.getBlastRadius(
|
|
26
|
+
const br = carto.getBlastRadius(file);
|
|
22
27
|
if (!br) {
|
|
23
28
|
console.error(`[CARTO] File not found in project graph: ${fileArg}`);
|
|
24
29
|
carto.close();
|
package/src/cli/init.js
CHANGED
|
@@ -95,6 +95,21 @@ function wireIDEs(projectRoot) {
|
|
|
95
95
|
const home = os.homedir();
|
|
96
96
|
const wired = [];
|
|
97
97
|
|
|
98
|
+
// Claude Code (project-scoped). Spec 7: write .mcp.json at the project root.
|
|
99
|
+
// Claude Code reads this file automatically when opened in the project — no
|
|
100
|
+
// host-level config needed, and no detection required (it's a project-local
|
|
101
|
+
// file that only takes effect when the user actually uses Claude Code here).
|
|
102
|
+
try {
|
|
103
|
+
const mcpJsonPath = path.join(projectRoot, '.mcp.json');
|
|
104
|
+
const config = fs.existsSync(mcpJsonPath)
|
|
105
|
+
? JSON.parse(fs.readFileSync(mcpJsonPath, 'utf-8'))
|
|
106
|
+
: { mcpServers: {} };
|
|
107
|
+
config.mcpServers = config.mcpServers || {};
|
|
108
|
+
config.mcpServers.carto = { command: 'carto', args: ['serve'] };
|
|
109
|
+
fs.writeFileSync(mcpJsonPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
110
|
+
wired.push('Claude Code');
|
|
111
|
+
} catch {}
|
|
112
|
+
|
|
98
113
|
// Kiro
|
|
99
114
|
const kiroDir = path.join(home, '.kiro', 'settings');
|
|
100
115
|
if (fs.existsSync(path.join(home, '.kiro'))) {
|
|
@@ -36,7 +36,11 @@ function extractImports(content, filePath, projectRoot) {
|
|
|
36
36
|
if (['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'].includes(ext)) {
|
|
37
37
|
rawImports = extractJSImports(content);
|
|
38
38
|
} else if (ext === '.py') {
|
|
39
|
-
|
|
39
|
+
// Python returns absolute paths from resolvePythonRelativeImport / tryResolvePythonModule.
|
|
40
|
+
// Relativize to project root and return early — the JS-style dedup loop below only handles
|
|
41
|
+
// `./` and `@/~/#` prefixed strings, so falling through silently drops every Python edge.
|
|
42
|
+
const abs = extractPythonImports(content, filePath, projectRoot);
|
|
43
|
+
return [...new Set(abs.map(p => path.relative(projectRoot, p)))].sort();
|
|
40
44
|
} else if (ext === '.r') {
|
|
41
45
|
return extractRImports(content, filePath, projectRoot);
|
|
42
46
|
} else if (ext === '.go') {
|
package/src/mcp/server-v2.js
CHANGED
|
@@ -7,9 +7,22 @@ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
|
7
7
|
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
8
8
|
const { CallToolRequestSchema, ListToolsRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
|
|
9
9
|
const { SQLiteStore } = require('../store/sqlite-store');
|
|
10
|
+
const { normalizeFileArg } = require('../store/path-utils');
|
|
10
11
|
|
|
11
12
|
const projectRoot = process.cwd();
|
|
12
13
|
|
|
14
|
+
// Process-level safety nets — Spec 7 Bug 5c. Without these, any error that
|
|
15
|
+
// escapes the request handler (very rare, but possible from native bindings
|
|
16
|
+
// or async stack frames) takes the whole MCP server down, and Claude Code /
|
|
17
|
+
// Kiro surface `-32000 Failed to reconnect`. We log to stderr (which the
|
|
18
|
+
// host logs but never terminates the JSON-RPC channel) and stay alive.
|
|
19
|
+
process.on('uncaughtException', (err) => {
|
|
20
|
+
process.stderr.write(`[CARTO MCP] Uncaught exception: ${err && err.stack ? err.stack : err}\n`);
|
|
21
|
+
});
|
|
22
|
+
process.on('unhandledRejection', (reason) => {
|
|
23
|
+
process.stderr.write(`[CARTO MCP] Unhandled rejection: ${reason && reason.stack ? reason.stack : reason}\n`);
|
|
24
|
+
});
|
|
25
|
+
|
|
13
26
|
// Open SQLite directly — no re-indexing, instant startup
|
|
14
27
|
let store = null;
|
|
15
28
|
|
|
@@ -17,8 +30,13 @@ function getStore() {
|
|
|
17
30
|
if (store) return store;
|
|
18
31
|
const dbPath = path.join(projectRoot, '.carto', 'carto.db');
|
|
19
32
|
if (!fs.existsSync(dbPath)) return null;
|
|
20
|
-
store
|
|
21
|
-
|
|
33
|
+
// Open BEFORE assigning to the module-scoped `store` so that if open()
|
|
34
|
+
// throws (corrupt DB, locked file, schema mismatch) we don't poison the
|
|
35
|
+
// cache with a broken instance — every subsequent call would otherwise
|
|
36
|
+
// return the broken object and never recover.
|
|
37
|
+
const s = new SQLiteStore(projectRoot);
|
|
38
|
+
s.open({ readonly: true }); // Spec 8 — defense in depth: MCP tools never write
|
|
39
|
+
store = s;
|
|
22
40
|
return store;
|
|
23
41
|
}
|
|
24
42
|
|
|
@@ -62,8 +80,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }))
|
|
|
62
80
|
|
|
63
81
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
64
82
|
const { name, arguments: args } = request.params;
|
|
65
|
-
|
|
66
|
-
|
|
83
|
+
// Normalize file-arg tools to the canonical SQLite-stored form so
|
|
84
|
+
// `./lib/x.js`, absolute paths, and Windows separators all resolve.
|
|
85
|
+
// Tools that don't take a `file` arg are unaffected.
|
|
86
|
+
if (args && typeof args.file === 'string') {
|
|
87
|
+
args.file = normalizeFileArg(projectRoot, args.file);
|
|
88
|
+
}
|
|
89
|
+
// Wrap entire handler body so any tool error (SQLite, null deref, bad
|
|
90
|
+
// input) returns a structured error response instead of crashing the
|
|
91
|
+
// MCP transport — Spec 7 Bug 5 fix. Pre-2.0.7, an unhandled throw here
|
|
92
|
+
// killed the stdio connection and Claude Code/Kiro would surface
|
|
93
|
+
// `-32000 Failed to reconnect`.
|
|
94
|
+
try {
|
|
95
|
+
const s = getStore();
|
|
96
|
+
if (!s) return notIndexed();
|
|
67
97
|
|
|
68
98
|
if (name === 'get_routes') {
|
|
69
99
|
const routes = s.getRoutes();
|
|
@@ -105,6 +135,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
105
135
|
}
|
|
106
136
|
|
|
107
137
|
if (name === 'get_domain') {
|
|
138
|
+
// Guard: AI clients sometimes call this with no/empty `domain` arg.
|
|
139
|
+
// Pre-2.0.7 we'd then call args.domain.toUpperCase() on undefined and crash.
|
|
140
|
+
if (!args.domain || typeof args.domain !== 'string') {
|
|
141
|
+
return text('Missing required argument: domain. Use get_domains_list to see available domains.');
|
|
142
|
+
}
|
|
108
143
|
const domain = s.getDomain(args.domain);
|
|
109
144
|
if (!domain) return text(`Domain not found: ${args.domain}. Use get_domains_list to see available domains.`);
|
|
110
145
|
|
|
@@ -133,7 +168,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
133
168
|
models: domain.models.map(m => ({
|
|
134
169
|
...m,
|
|
135
170
|
className: m.name,
|
|
136
|
-
|
|
171
|
+
// Wrap parse so one corrupt row doesn't take down the whole tool call.
|
|
172
|
+
fields: (() => {
|
|
173
|
+
if (!m.fields_json) return [];
|
|
174
|
+
try { return JSON.parse(m.fields_json); } catch { return []; }
|
|
175
|
+
})()
|
|
137
176
|
})),
|
|
138
177
|
functions: {},
|
|
139
178
|
envVars: [],
|
|
@@ -446,6 +485,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
446
485
|
}
|
|
447
486
|
|
|
448
487
|
return text(`Unknown tool: ${name}`);
|
|
488
|
+
} catch (err) {
|
|
489
|
+
process.stderr.write(`[CARTO MCP] Tool "${name}" error: ${err.stack || err.message || err}\n`);
|
|
490
|
+
return {
|
|
491
|
+
content: [{ type: 'text', text: `Error in ${name}: ${err.message || String(err)}` }],
|
|
492
|
+
isError: true,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
449
495
|
});
|
|
450
496
|
|
|
451
497
|
async function main() {
|
package/src/security/ignore.js
CHANGED
|
@@ -2,8 +2,11 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
4
|
const DEFAULT_IGNORE_PATTERNS = [
|
|
5
|
+
// Env files
|
|
5
6
|
'.env',
|
|
6
7
|
'.env.*',
|
|
8
|
+
|
|
9
|
+
// Generic secret/credential name globs
|
|
7
10
|
'*secret*',
|
|
8
11
|
'*SECRET*',
|
|
9
12
|
'*password*',
|
|
@@ -12,8 +15,60 @@ const DEFAULT_IGNORE_PATTERNS = [
|
|
|
12
15
|
'*CREDENTIAL*',
|
|
13
16
|
'*private_key*',
|
|
14
17
|
'*PRIVATE_KEY*',
|
|
18
|
+
|
|
19
|
+
// Generic key/cert extensions
|
|
15
20
|
'*.pem',
|
|
16
|
-
'*.key'
|
|
21
|
+
'*.key',
|
|
22
|
+
|
|
23
|
+
// SSH keys (no extension, won't be caught by *.key)
|
|
24
|
+
'id_rsa',
|
|
25
|
+
'id_rsa.pub',
|
|
26
|
+
'id_ed25519',
|
|
27
|
+
'id_ed25519.pub',
|
|
28
|
+
'id_ecdsa',
|
|
29
|
+
'id_ecdsa.pub',
|
|
30
|
+
'id_dsa',
|
|
31
|
+
'id_dsa.pub',
|
|
32
|
+
'authorized_keys',
|
|
33
|
+
'known_hosts',
|
|
34
|
+
|
|
35
|
+
// Package registry credentials
|
|
36
|
+
'.npmrc',
|
|
37
|
+
'.pypirc',
|
|
38
|
+
'.netrc',
|
|
39
|
+
'.pip/pip.conf',
|
|
40
|
+
'.gem/credentials',
|
|
41
|
+
'.cargo/credentials.toml',
|
|
42
|
+
|
|
43
|
+
// Cloud / K8s / Docker
|
|
44
|
+
'kubeconfig',
|
|
45
|
+
'*.kubeconfig',
|
|
46
|
+
'.docker/config.json',
|
|
47
|
+
'.dockercfg',
|
|
48
|
+
'*service-account*.json',
|
|
49
|
+
'*service_account*.json',
|
|
50
|
+
'*-credentials.json',
|
|
51
|
+
'.aws/credentials',
|
|
52
|
+
'.aws/config',
|
|
53
|
+
'.gcp/*',
|
|
54
|
+
'.gnupg/*',
|
|
55
|
+
|
|
56
|
+
// Cert / keystore formats (defense in depth — already excluded by CODE_EXTS today)
|
|
57
|
+
'*.crt',
|
|
58
|
+
'*.cer',
|
|
59
|
+
'*.p12',
|
|
60
|
+
'*.pfx',
|
|
61
|
+
'*.jks',
|
|
62
|
+
'*.keystore',
|
|
63
|
+
'*.pkcs12'
|
|
64
|
+
|
|
65
|
+
// NOTE: deliberately NOT including `*api_key*` / `*api-key*` globs.
|
|
66
|
+
// Real SaaS projects (cal.com, supabase, fastapi, zed) ship legitimate
|
|
67
|
+
// feature code under these filenames — `api_key.py`, `api-keys-service.ts`,
|
|
68
|
+
// `language_model/src/api_key.rs` — and a broad glob would silently delete
|
|
69
|
+
// 17+ files per repo from the import graph. Raw-credential files literally
|
|
70
|
+
// named `api_key.txt` are unusual; the realistic threat surface (.env,
|
|
71
|
+
// *credentials.json, .npmrc, id_rsa, kubeconfig, *secret*) is covered above.
|
|
17
72
|
];
|
|
18
73
|
|
|
19
74
|
/**
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* normalizeFileArg(projectRoot, fileArg) → string
|
|
7
|
+
*
|
|
8
|
+
* Convert a user-provided file path into the canonical form stored in the
|
|
9
|
+
* SQLite `files.path` column: relative-to-project-root, forward-slashed,
|
|
10
|
+
* no leading `./`.
|
|
11
|
+
*
|
|
12
|
+
* Handles every form a user / IDE / shell tab-completion realistically
|
|
13
|
+
* produces:
|
|
14
|
+
* - relative form already correct ("lib/application.js" → "lib/application.js")
|
|
15
|
+
* - leading "./" ("./lib/application.js" → "lib/application.js")
|
|
16
|
+
* - leading "/" absolute under projectRoot ("/abs/.../lib/application.js" → "lib/application.js")
|
|
17
|
+
* - Windows backslashes ("lib\\application.js" → "lib/application.js")
|
|
18
|
+
* - paths containing "../" (e.g. typed from a sibling dir) — resolved.
|
|
19
|
+
*
|
|
20
|
+
* Falsy / empty input returns the input unchanged so the caller's
|
|
21
|
+
* "missing argument" check still fires the same way.
|
|
22
|
+
*/
|
|
23
|
+
function normalizeFileArg(projectRoot, fileArg) {
|
|
24
|
+
if (typeof fileArg !== 'string' || fileArg.length === 0) return fileArg;
|
|
25
|
+
|
|
26
|
+
let p = fileArg;
|
|
27
|
+
|
|
28
|
+
// Normalize separators first so the absolute-path check works on Windows.
|
|
29
|
+
p = p.split('\\').join('/');
|
|
30
|
+
|
|
31
|
+
// Absolute → relative-to-project-root
|
|
32
|
+
if (path.isAbsolute(p) || /^[a-zA-Z]:\//.test(p)) {
|
|
33
|
+
p = path.relative(projectRoot, p);
|
|
34
|
+
p = p.split(path.sep).join('/');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Strip leading "./" (or repeated "./././")
|
|
38
|
+
while (p.startsWith('./')) p = p.slice(2);
|
|
39
|
+
|
|
40
|
+
// Resolve any embedded "../" segments without going absolute.
|
|
41
|
+
// e.g. "src/foo/../bar.js" → "src/bar.js"
|
|
42
|
+
if (p.includes('/../') || p.startsWith('../')) {
|
|
43
|
+
const abs = path.resolve(projectRoot, p);
|
|
44
|
+
p = path.relative(projectRoot, abs).split(path.sep).join('/');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return p;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { normalizeFileArg };
|
|
@@ -41,14 +41,35 @@ class SQLiteStore {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* open() — Opens or creates the database. Applies pragmas and schema.
|
|
44
|
+
* open(opts) — Opens or creates the database. Applies pragmas and schema.
|
|
45
|
+
*
|
|
46
|
+
* Options:
|
|
47
|
+
* readonly — Open the DB in read-only mode. Used by the MCP server
|
|
48
|
+
* (`carto serve`) so a malformed/buggy tool can never write
|
|
49
|
+
* through the SQLite layer. Skips mkdir, schema bootstrap, and
|
|
50
|
+
* WAL pragma (WAL needs write capability and would otherwise
|
|
51
|
+
* create `carto.db-wal`/`carto.db-shm` in repos that only run
|
|
52
|
+
* the MCP server). Sets `fileMustExist: true` so a missing DB
|
|
53
|
+
* returns a clear SQLite error instead of silently creating an
|
|
54
|
+
* empty file in the wrong location.
|
|
45
55
|
*/
|
|
46
|
-
open() {
|
|
56
|
+
open(opts = {}) {
|
|
47
57
|
const cartoDir = path.join(this._projectRoot, '.carto');
|
|
48
|
-
fs.mkdirSync(cartoDir, { recursive: true });
|
|
49
|
-
|
|
50
58
|
const dbPath = path.join(cartoDir, 'carto.db');
|
|
51
59
|
|
|
60
|
+
if (opts.readonly) {
|
|
61
|
+
// Read-only path: dir must already exist (a writer process — `carto sync`
|
|
62
|
+
// or `carto init` — created the DB). Don't mkdir, don't ensure schema,
|
|
63
|
+
// don't apply write-only pragmas.
|
|
64
|
+
this._db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
65
|
+
// Read-only safe pragmas only:
|
|
66
|
+
this._db.pragma('busy_timeout = 5000');
|
|
67
|
+
this._db.pragma('cache_size = -64000'); // 64MB cache
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fs.mkdirSync(cartoDir, { recursive: true });
|
|
72
|
+
|
|
52
73
|
try {
|
|
53
74
|
this._db = new Database(dbPath);
|
|
54
75
|
} catch (err) {
|
|
@@ -618,7 +639,12 @@ class SQLiteStore {
|
|
|
618
639
|
},
|
|
619
640
|
entryPoints,
|
|
620
641
|
highImpact,
|
|
621
|
-
|
|
642
|
+
// Defensive parse — a corrupt stack_json row must never crash callers.
|
|
643
|
+
// Spec 7 Bug 5f.
|
|
644
|
+
stack: (() => {
|
|
645
|
+
if (!stack) return [];
|
|
646
|
+
try { return JSON.parse(stack); } catch { return []; }
|
|
647
|
+
})(),
|
|
622
648
|
domains
|
|
623
649
|
};
|
|
624
650
|
}
|