cartogopher 5.0.0

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 ADDED
@@ -0,0 +1,48 @@
1
+ # cartogopher
2
+
3
+ AI-native code intelligence: parses codebases into a searchable graph of functions, types, endpoints, and call relationships. Includes both a CLI and an MCP server.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g cartogopher
9
+ ```
10
+
11
+ npm only downloads the binary for your platform (darwin-arm64, darwin-x64, linux-x64, or linux-arm64).
12
+
13
+ Try it without installing globally:
14
+
15
+ ```bash
16
+ npx cartogopher bake
17
+ ```
18
+
19
+ ## Quick start (Claude Code)
20
+
21
+ ```bash
22
+ export CARTOGOPHER_API_KEY=cg_live_your_key
23
+ cd your-project
24
+ cartogopher bake # parse the codebase (~10–30s)
25
+ cartogopher install-skill # drop the skill into .claude/skills/cartogopher/
26
+ ```
27
+
28
+ Now any Claude Code agent in this project knows how to use cartogopher. It uses the regular `Bash` tool to call `cartogopher search`, `cartogopher impact`, etc.
29
+
30
+ For a system-wide install (works in every project):
31
+ ```bash
32
+ cartogopher install-skill --global # → ~/.claude/skills/cartogopher/SKILL.md
33
+ ```
34
+
35
+ ## MCP server (alternative to the skill)
36
+
37
+ If you'd rather plug in as an MCP server instead of a skill:
38
+
39
+ ```bash
40
+ cartogopher mcp
41
+ ```
42
+
43
+ Configure your MCP client to invoke that command. **The skill is generally cheaper to run** — see bench results in the repo's `tests/bench/engrambench/results/`.
44
+
45
+ ## Docs
46
+
47
+ - API key: https://cartogopher.com/download
48
+ - Docs: https://cartogopher.com/docs
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { spawnSync } = require("child_process");
5
+ const path = require("path");
6
+ const fs = require("fs");
7
+
8
+ const PLATFORM_PACKAGES = {
9
+ "darwin-arm64": "@cartogopher/darwin-arm64",
10
+ "darwin-x64": "@cartogopher/darwin-x64",
11
+ "linux-x64": "@cartogopher/linux-x64",
12
+ "linux-arm64": "@cartogopher/linux-arm64",
13
+ };
14
+
15
+ function resolveBinary() {
16
+ const key = `${process.platform}-${process.arch}`;
17
+ const pkg = PLATFORM_PACKAGES[key];
18
+ if (!pkg) {
19
+ console.error(
20
+ `cartogopher: unsupported platform ${key}. Supported: ${Object.keys(PLATFORM_PACKAGES).join(", ")}`
21
+ );
22
+ process.exit(1);
23
+ }
24
+
25
+ let pkgJsonPath;
26
+ try {
27
+ pkgJsonPath = require.resolve(`${pkg}/package.json`);
28
+ } catch (err) {
29
+ console.error(
30
+ `cartogopher: platform package '${pkg}' is not installed.\n` +
31
+ `This usually means npm skipped optional dependencies, or the platform package failed to download.\n` +
32
+ `Try: npm install -g ${pkg}`
33
+ );
34
+ process.exit(1);
35
+ }
36
+
37
+ const exeName = process.platform === "win32" ? "cartogopher.exe" : "cartogopher";
38
+ const binary = path.join(path.dirname(pkgJsonPath), "bin", exeName);
39
+ if (!fs.existsSync(binary)) {
40
+ console.error(`cartogopher: binary missing at ${binary}`);
41
+ process.exit(1);
42
+ }
43
+ return binary;
44
+ }
45
+
46
+ function mcpScriptPath() {
47
+ return path.join(__dirname, "..", "lib", "mcp-server.js");
48
+ }
49
+
50
+ function bundledSkillPath() {
51
+ return path.join(__dirname, "..", "share", "cartogopher", "SKILL.md");
52
+ }
53
+
54
+ function installSkill(argv) {
55
+ // Usage: cartogopher install-skill [--global] [--dest <dir>]
56
+ const flagGlobal = argv.includes("--global");
57
+ const destIdx = argv.indexOf("--dest");
58
+ const targetRoot = destIdx >= 0 && argv[destIdx + 1]
59
+ ? path.resolve(argv[destIdx + 1])
60
+ : flagGlobal
61
+ ? path.join(process.env.HOME || "", ".claude", "skills", "cartogopher")
62
+ : path.join(process.cwd(), ".claude", "skills", "cartogopher");
63
+
64
+ const src = bundledSkillPath();
65
+ if (!fs.existsSync(src)) {
66
+ console.error(`cartogopher install-skill: bundled SKILL.md not found at ${src}`);
67
+ process.exit(1);
68
+ }
69
+
70
+ fs.mkdirSync(targetRoot, { recursive: true });
71
+ const dest = path.join(targetRoot, "SKILL.md");
72
+ fs.copyFileSync(src, dest);
73
+ console.log(`Installed cartogopher skill → ${dest}`);
74
+ console.log("Claude Code agents in this project can now load it via /cartogopher.");
75
+ process.exit(0);
76
+ }
77
+
78
+ const args = process.argv.slice(2);
79
+ if (args[0] === "install-skill") {
80
+ installSkill(args.slice(1));
81
+ }
82
+
83
+ const binary = resolveBinary();
84
+ const env = { ...process.env, CARTOGOPHER_MCP_SCRIPT: mcpScriptPath() };
85
+
86
+ const result = spawnSync(binary, args, {
87
+ stdio: "inherit",
88
+ env,
89
+ });
90
+
91
+ if (result.error) {
92
+ console.error(`cartogopher: failed to exec binary: ${result.error.message}`);
93
+ process.exit(1);
94
+ }
95
+ process.exit(result.status === null ? 1 : result.status);
package/lib/.gitkeep ADDED
File without changes
@@ -0,0 +1,868 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
4
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
5
+ const {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ } = require('@modelcontextprotocol/sdk/types.js');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { execSync, execFileSync } = require('child_process');
12
+
13
+ class CartoGopherMCP {
14
+ constructor() {
15
+ this.rootDir = this.findProjectRoot();
16
+ this.bakesDir = path.join(this.rootDir, 'bakes');
17
+ this.docMode = false;
18
+ this.binary = this.findBinary();
19
+
20
+ if (process.env.DEBUG) {
21
+ console.error(`MCP v4 Server initialized:`);
22
+ console.error(` Root dir: ${this.rootDir}`);
23
+ console.error(` Bakes dir: ${this.bakesDir}`);
24
+ console.error(` Binary: ${this.binary || 'NOT FOUND'}`);
25
+ }
26
+
27
+ this.server = new Server(
28
+ { name: 'cartogopher-mcp-v4', version: '4.3.0' },
29
+ { capabilities: { tools: {} } }
30
+ );
31
+
32
+ this.setupHandlers();
33
+ }
34
+
35
+ // ═══════════════════════════════════════════════════════════════════════════
36
+ // Project & Binary Discovery (reused from v1)
37
+ // ═══════════════════════════════════════════════════════════════════════════
38
+
39
+ findProjectRoot() {
40
+ if (process.env.CARTOGOPHER_ROOT) return process.env.CARTOGOPHER_ROOT;
41
+ if (process.env.CURSOR_WORKSPACE) {
42
+ const ws = process.env.CURSOR_WORKSPACE;
43
+ if (this.isValidProject(ws)) return ws;
44
+ }
45
+ const cwd = process.cwd();
46
+ if (this.isValidProject(cwd)) return cwd;
47
+ const gitRoot = this.getGitRoot();
48
+ if (gitRoot && this.isValidProject(gitRoot)) return gitRoot;
49
+ return cwd;
50
+ }
51
+
52
+ getGitRoot() {
53
+ try {
54
+ return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
55
+ } catch { return null; }
56
+ }
57
+
58
+ isValidProject(dir) {
59
+ try {
60
+ const indicators = [
61
+ 'go.mod', 'go.sum', 'main.go', 'requirements.txt', 'setup.py',
62
+ 'pyproject.toml', 'Pipfile', 'Cargo.toml', 'Cargo.lock',
63
+ 'package.json', 'tsconfig.json', 'fast.yaml', '.git', 'Makefile', 'Dockerfile',
64
+ ];
65
+ if (indicators.some(f => fs.existsSync(path.join(dir, f)))) return true;
66
+ const exts = ['.go', '.py', '.rs', '.js', '.ts', '.html', '.css', '.vue', '.jsx', '.tsx'];
67
+ try {
68
+ if (fs.readdirSync(dir).some(f => exts.some(e => f.endsWith(e)))) return true;
69
+ } catch {}
70
+ if (fs.existsSync(path.join(dir, 'bakes'))) return true;
71
+ return false;
72
+ } catch { return false; }
73
+ }
74
+
75
+ findBinary() {
76
+ const candidates = [
77
+ process.env.CARTOGOPHER_INSTALL_ROOT ? path.join(process.env.CARTOGOPHER_INSTALL_ROOT, 'cartogopher') : null,
78
+ path.join(this.rootDir, 'cartogopher'),
79
+ path.join(path.dirname(path.dirname(process.argv[1])), 'cartogopher'),
80
+ 'cartogopher',
81
+ ].filter(Boolean);
82
+
83
+ for (const p of candidates) {
84
+ if (p === 'cartogopher') {
85
+ try { execSync('which cartogopher', { stdio: 'pipe' }); return p; } catch { continue; }
86
+ } else if (fs.existsSync(p)) {
87
+ return p;
88
+ }
89
+ }
90
+ return null;
91
+ }
92
+
93
+ // ═══════════════════════════════════════════════════════════════════════════
94
+ // CLI Execution Helper
95
+ // ═══════════════════════════════════════════════════════════════════════════
96
+
97
+ execCLI(args) {
98
+ if (!this.binary) {
99
+ throw new Error('cartogopher binary not found. Install with: go install github.com/jakenesler/CartoGopher/cmd/cartogopher@latest');
100
+ }
101
+
102
+ const env = { ...process.env };
103
+ if (process.env.CARTOGOPHER_API_KEY) {
104
+ env.CARTOGOPHER_API_KEY = process.env.CARTOGOPHER_API_KEY;
105
+ }
106
+
107
+ if (process.env.DEBUG) {
108
+ console.error(`execCLI: ${this.binary} ${args.join(' ')}`);
109
+ }
110
+
111
+ try {
112
+ const stdout = execFileSync(this.binary, args, {
113
+ timeout: 30000,
114
+ maxBuffer: 10 * 1024 * 1024,
115
+ encoding: 'utf8',
116
+ cwd: this.rootDir,
117
+ env,
118
+ });
119
+ return stdout.trim();
120
+ } catch (err) {
121
+ const msg = err.stderr ? err.stderr.toString().trim() : err.message;
122
+ throw new Error(msg || 'CLI command failed');
123
+ }
124
+ }
125
+
126
+ /** Resolve bakeDir from MCP args → --out value. Strips /latest suffix since CLI expects parent. */
127
+ resolveBakeDir(args) {
128
+ let dir = args?.bakeDir || args?.path || null;
129
+ if (!dir) return null;
130
+ // Strip trailing /latest — CLI expects the parent bakes dir
131
+ return dir.replace(/\/latest\/?$/, '');
132
+ }
133
+
134
+ // ═══════════════════════════════════════════════════════════════════════════
135
+ // Tool Schemas (identical to v1 for backward compatibility)
136
+ // ═══════════════════════════════════════════════════════════════════════════
137
+
138
+ getToolDefinitions() {
139
+ // bakeDir is accepted by most tools as an optional override; documented once
140
+ // here rather than on every field to keep system-prompt cost down.
141
+ const bakeDir = { type: 'string' };
142
+
143
+ return [
144
+ // ── Setup / modes ─────────────────────────────────────────────
145
+ {
146
+ name: 'llm_instructions',
147
+ description: 'Setup notes for this project.',
148
+ inputSchema: { type: 'object', properties: { path: { type: 'string' } } },
149
+ },
150
+ {
151
+ name: 'doc_mode_enable',
152
+ description: 'Verbose mode for doc generation; pair with doc_mode_disable.',
153
+ inputSchema: { type: 'object', properties: {} },
154
+ },
155
+ {
156
+ name: 'doc_mode_disable',
157
+ description: 'Exit verbose doc mode.',
158
+ inputSchema: { type: 'object', properties: {} },
159
+ },
160
+
161
+ // ── Core code intel ───────────────────────────────────────────
162
+ {
163
+ name: 'shake',
164
+ description: 'Repo overview: structure, packages, entry points.',
165
+ inputSchema: { type: 'object', properties: { path: { type: 'string' } } },
166
+ },
167
+ {
168
+ name: 'bake',
169
+ description: 'Full bake.json (large; prefer narrower tools).',
170
+ inputSchema: { type: 'object', properties: { bakeDir } },
171
+ },
172
+ {
173
+ name: 'symbol',
174
+ description: 'Function/type details: signature, calls, complexity, file:line.',
175
+ inputSchema: {
176
+ type: 'object',
177
+ properties: { name: { type: 'string' }, bakeDir },
178
+ required: ['name'],
179
+ },
180
+ },
181
+ {
182
+ name: 'full_symbol',
183
+ description: 'Verbose symbol details (needs doc_mode_enable).',
184
+ inputSchema: {
185
+ type: 'object',
186
+ properties: { name: { type: 'string' }, bakeDir },
187
+ required: ['name'],
188
+ },
189
+ },
190
+ {
191
+ name: 'search',
192
+ description: 'AST search for functions, types, packages.',
193
+ inputSchema: {
194
+ type: 'object',
195
+ properties: {
196
+ q: { type: 'string' },
197
+ limit: { type: 'integer' },
198
+ package: { type: 'string' },
199
+ bakeDir,
200
+ },
201
+ required: ['q'],
202
+ },
203
+ },
204
+ {
205
+ name: 'supersearch',
206
+ description: 'AST search with context/pattern filters (identifiers, calls, assigns, strings, etc.).',
207
+ inputSchema: {
208
+ type: 'object',
209
+ properties: {
210
+ query: { type: 'string' },
211
+ context: { type: 'string', description: 'all|strings|comments|identifiers|map-keys|imports|assignments' },
212
+ pattern: { type: 'string', description: 'call|assign|return|map-access' },
213
+ limit: { type: 'integer' },
214
+ show_context: { type: 'integer' },
215
+ exclude_tests: { type: 'boolean' },
216
+ package: { type: 'string' },
217
+ languages: { type: 'string' },
218
+ },
219
+ required: ['query'],
220
+ },
221
+ },
222
+ {
223
+ name: 'file_functions',
224
+ description: 'Functions in a file (no full read).',
225
+ inputSchema: {
226
+ type: 'object',
227
+ properties: {
228
+ file: { type: 'string' },
229
+ include_summaries: { type: 'boolean' },
230
+ bakeDir,
231
+ },
232
+ required: ['file'],
233
+ },
234
+ },
235
+ {
236
+ name: 'related_to',
237
+ description: 'Call graph: callers + callees of a symbol.',
238
+ inputSchema: {
239
+ type: 'object',
240
+ properties: {
241
+ symbol: { type: 'string' },
242
+ depth: { type: 'integer' },
243
+ bakeDir,
244
+ },
245
+ required: ['symbol'],
246
+ },
247
+ },
248
+ {
249
+ name: 'impact',
250
+ description: 'Refactor blast radius: callers, endpoints, CRUD, risk score.',
251
+ inputSchema: {
252
+ type: 'object',
253
+ properties: {
254
+ symbol: { type: 'string' },
255
+ depth: { type: 'integer' },
256
+ bakeDir,
257
+ },
258
+ required: ['symbol'],
259
+ },
260
+ },
261
+ {
262
+ name: 'api_surface',
263
+ description: 'Exported functions and types.',
264
+ inputSchema: {
265
+ type: 'object',
266
+ properties: {
267
+ package: { type: 'string' },
268
+ limit: { type: 'integer' },
269
+ bakeDir,
270
+ },
271
+ },
272
+ },
273
+ {
274
+ name: 'package_summary',
275
+ description: 'Package stats + top-complexity functions.',
276
+ inputSchema: {
277
+ type: 'object',
278
+ properties: { package: { type: 'string' }, bakeDir },
279
+ required: ['package'],
280
+ },
281
+ },
282
+ {
283
+ name: 'crud_operations',
284
+ description: 'CRUD by entity.',
285
+ inputSchema: {
286
+ type: 'object',
287
+ properties: { entity: { type: 'string' }, bakeDir },
288
+ },
289
+ },
290
+ {
291
+ name: 'architecture_map',
292
+ description: 'Directory map; with `intent`, suggests placement.',
293
+ inputSchema: {
294
+ type: 'object',
295
+ properties: { intent: { type: 'string' }, bakeDir },
296
+ },
297
+ },
298
+ {
299
+ name: 'suggest_placement',
300
+ description: 'Where to place a new function.',
301
+ inputSchema: {
302
+ type: 'object',
303
+ properties: {
304
+ function_name: { type: 'string' },
305
+ function_type: { type: 'string', description: 'handler|service|repository|model|util|test' },
306
+ related_to: { type: 'string' },
307
+ bakeDir,
308
+ },
309
+ required: ['function_name', 'function_type'],
310
+ },
311
+ },
312
+ {
313
+ name: 'find_docs',
314
+ description: 'Find README/.env/config files.',
315
+ inputSchema: {
316
+ type: 'object',
317
+ properties: {
318
+ type: { type: 'string', description: 'readme|env|config|all' },
319
+ root: { type: 'string' },
320
+ },
321
+ },
322
+ },
323
+
324
+ // ── API endpoints ─────────────────────────────────────────────
325
+ {
326
+ name: 'api_trace',
327
+ description: 'Trace endpoint: frontend → handler → CRUD.',
328
+ inputSchema: {
329
+ type: 'object',
330
+ properties: {
331
+ endpoint: { type: 'string' },
332
+ method: { type: 'string' },
333
+ bakeDir,
334
+ },
335
+ required: ['endpoint'],
336
+ },
337
+ },
338
+ {
339
+ name: 'all_endpoints',
340
+ description: 'All HTTP endpoints (backend + frontend-called).',
341
+ inputSchema: {
342
+ type: 'object',
343
+ properties: {
344
+ include_frontend: { type: 'boolean' },
345
+ include_backend: { type: 'boolean' },
346
+ bakeDir,
347
+ },
348
+ },
349
+ },
350
+
351
+ // ── File ops ──────────────────────────────────────────────────
352
+ {
353
+ name: 'slice',
354
+ description: 'Read lines [start, end] of a file.',
355
+ inputSchema: {
356
+ type: 'object',
357
+ properties: {
358
+ file: { type: 'string' },
359
+ start: { type: 'integer' },
360
+ end: { type: 'integer' },
361
+ root: { type: 'string' },
362
+ },
363
+ required: ['file', 'start', 'end'],
364
+ },
365
+ },
366
+ {
367
+ name: 'patch',
368
+ description: 'Replace lines [start, end], or create new file (omit start/end).',
369
+ inputSchema: {
370
+ type: 'object',
371
+ properties: {
372
+ file: { type: 'string' },
373
+ function_name: { type: 'string' },
374
+ start: { type: 'integer' },
375
+ end: { type: 'integer' },
376
+ new_content: { type: 'string' },
377
+ root: { type: 'string' },
378
+ },
379
+ required: ['file', 'new_content'],
380
+ },
381
+ },
382
+
383
+ // ── Frontend (single tool, many ops) ──────────────────────────
384
+ {
385
+ name: 'frontend',
386
+ description: 'Frontend (Vue/React/Svelte/Angular/Proto/GraphQL). See `op`.',
387
+ inputSchema: {
388
+ type: 'object',
389
+ properties: {
390
+ op: {
391
+ type: 'string',
392
+ enum: ['summary', 'search', 'template', 'script', 'renderers', 'component', 'props', 'store', 'composables', 'deps', 'react', 'react_component', 'react_hooks', 'svelte', 'svelte_component', 'svelte_stores', 'angular', 'angular_component', 'angular_services', 'angular_routes', 'proto', 'proto_service', 'proto_messages', 'graphql', 'graphql_type', 'graphql_operations'],
393
+ },
394
+ name: { type: 'string' },
395
+ type: { type: 'string', description: 'For search: template|script|all' },
396
+ depth: { type: 'integer' },
397
+ limit: { type: 'integer' },
398
+ bakeDir,
399
+ },
400
+ required: ['op'],
401
+ },
402
+ },
403
+
404
+ // ── External specs (OpenAPI / GraphQL) and academic papers ───
405
+ {
406
+ name: 'navigator',
407
+ description: 'Fetched OpenAPI specs. ops: list, search, endpoint.',
408
+ inputSchema: {
409
+ type: 'object',
410
+ properties: {
411
+ op: { type: 'string', enum: ['list', 'search', 'endpoint'] },
412
+ query: { type: 'string' },
413
+ spec: { type: 'string' },
414
+ path: { type: 'string' },
415
+ method: { type: 'string' },
416
+ limit: { type: 'integer' },
417
+ bakeDir,
418
+ },
419
+ required: ['op'],
420
+ },
421
+ },
422
+ {
423
+ name: 'gqlnav',
424
+ description: 'Fetched GraphQL schemas. ops: list, search, type, summary.',
425
+ inputSchema: {
426
+ type: 'object',
427
+ properties: {
428
+ op: { type: 'string', enum: ['list', 'search', 'type', 'summary'] },
429
+ query: { type: 'string' },
430
+ spec: { type: 'string' },
431
+ name: { type: 'string' },
432
+ kind: { type: 'string', description: 'type|input|enum|query|mutation|subscription|all' },
433
+ limit: { type: 'integer' },
434
+ bakeDir,
435
+ },
436
+ required: ['op'],
437
+ },
438
+ },
439
+ {
440
+ name: 'scholar',
441
+ description: 'Academic papers via Semantic Scholar / OpenAlex. ops: search, paper, citations, list.',
442
+ inputSchema: {
443
+ type: 'object',
444
+ properties: {
445
+ op: { type: 'string', enum: ['search', 'paper', 'citations', 'list'] },
446
+ query: { type: 'string' },
447
+ id: { type: 'string', description: 'S2 ID, DOI, ArXiv, or OpenAlex ID' },
448
+ direction: { type: 'string', description: 'citations|references' },
449
+ source: { type: 'string', description: 's2|openalex' },
450
+ year: { type: 'string' },
451
+ fields: { type: 'string' },
452
+ open_access: { type: 'boolean' },
453
+ limit: { type: 'integer' },
454
+ refresh: { type: 'boolean' },
455
+ bakeDir,
456
+ },
457
+ required: ['op'],
458
+ },
459
+ },
460
+
461
+ // ── Architecture analysis ─────────────────────────────────────
462
+ {
463
+ name: 'decompose',
464
+ description: 'Suggest microservice boundaries from call graph + CRUD ownership.',
465
+ inputSchema: { type: 'object', properties: { bakeDir } },
466
+ },
467
+ {
468
+ name: 'architecture_smells',
469
+ description: 'Anti-patterns: god functions, cyclic deps, mega-modules, N+1, layer violations.',
470
+ inputSchema: {
471
+ type: 'object',
472
+ properties: {
473
+ max_complexity: { type: 'integer' },
474
+ max_fan_out: { type: 'integer' },
475
+ max_fan_in: { type: 'integer' },
476
+ max_pkg_funcs: { type: 'integer' },
477
+ bakeDir,
478
+ },
479
+ },
480
+ },
481
+ ];
482
+ }
483
+
484
+ setupHandlers() {
485
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
486
+ tools: this.getToolDefinitions(),
487
+ }));
488
+
489
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
490
+ const { name, arguments: args = {} } = request.params;
491
+
492
+ try {
493
+ const result = this.handleToolCall(name, args);
494
+ return { content: [{ type: 'text', text: result }] };
495
+ } catch (error) {
496
+ return {
497
+ content: [{ type: 'text', text: `Error: ${error.message}` }],
498
+ isError: true,
499
+ };
500
+ }
501
+ });
502
+ }
503
+
504
+ handleToolCall(name, args = {}) {
505
+ if (name === 'doc_mode_enable') {
506
+ this.docMode = true;
507
+ return 'Documentation mode ENABLED. Full responses active. Remember to disable when done.';
508
+ }
509
+ if (name === 'doc_mode_disable') {
510
+ this.docMode = false;
511
+ return 'Documentation mode DISABLED. Back to ultra-compact mode.';
512
+ }
513
+ // Collapsed-family tools dispatch by args.op; legacy <family>_<op> names
514
+ // still route to the same handlers so existing callers don't break.
515
+ if (name === 'navigator' || name.startsWith('navigator_')) {
516
+ const op = name === 'navigator' ? (args.op || 'list') : name.substring('navigator_'.length);
517
+ return this.handleNavigatorTool(`navigator_${op}`, args);
518
+ }
519
+ if (name === 'gqlnav' || name.startsWith('gqlnav_')) {
520
+ const op = name === 'gqlnav' ? (args.op || 'list') : name.substring('gqlnav_'.length);
521
+ return this.handleGQLNavTool(`gqlnav_${op}`, args);
522
+ }
523
+ if (name === 'scholar' || name.startsWith('scholar_')) {
524
+ const op = name === 'scholar' ? (args.op || 'search') : name.substring('scholar_'.length);
525
+ return this.handleScholarTool(`scholar_${op}`, args);
526
+ }
527
+ return this.handleCoreTool(name, args);
528
+ }
529
+
530
+ handleCoreTool(name, args = {}) {
531
+ let result;
532
+ switch (name) {
533
+ case 'llm_instructions': {
534
+ const a = ['llm-instructions'];
535
+ a.push('--root', args?.path || this.rootDir);
536
+ result = this.execCLI(a);
537
+ break;
538
+ }
539
+ case 'shake': {
540
+ const a = ['shake-summary'];
541
+ const out = this.resolveBakeDir(args);
542
+ if (out) a.push('--out', out);
543
+ result = this.execCLI(a);
544
+ break;
545
+ }
546
+ case 'bake': {
547
+ const a = ['bake-summary'];
548
+ const out = this.resolveBakeDir(args);
549
+ if (out) a.push('--out', out);
550
+ result = this.execCLI(a);
551
+ break;
552
+ }
553
+ case 'full_symbol':
554
+ case 'symbol': {
555
+ const a = ['symbol', args.name];
556
+ const out = this.resolveBakeDir(args);
557
+ if (out) a.push('--out', out);
558
+ result = this.execCLI(a);
559
+ break;
560
+ }
561
+ case 'search': {
562
+ const a = ['search', args.q];
563
+ if (args.package) a.push('--pkg', args.package);
564
+ if (args.limit) a.push('-l', String(args.limit));
565
+ const out = this.resolveBakeDir(args);
566
+ if (out) a.push('--out', out);
567
+ result = this.execCLI(a);
568
+ break;
569
+ }
570
+ case 'supersearch': {
571
+ const a = ['search', args.query];
572
+ if (args.context) a.push('--in', args.context);
573
+ if (args.pattern) a.push('--pattern', args.pattern);
574
+ if (args.package) a.push('--pkg', args.package);
575
+ if (args.limit) a.push('-l', String(args.limit));
576
+ if (args.show_context) a.push('-C', String(args.show_context));
577
+ if (args.exclude_tests) a.push('--exclude-test');
578
+ if (args.languages) a.push('--lang', args.languages);
579
+ const out = this.resolveBakeDir(args);
580
+ if (out) a.push('--out', out);
581
+ result = this.execCLI(a);
582
+ break;
583
+ }
584
+ case 'slice': {
585
+ const a = ['slice', args.file, '--start', String(args.start), '--end', String(args.end)];
586
+ a.push('--root', args.root || this.rootDir);
587
+ result = this.execCLI(a);
588
+ break;
589
+ }
590
+ case 'patch': {
591
+ const a = ['patch', args.file, '--content', args.new_content];
592
+ if (args.start) a.push('--start', String(args.start));
593
+ if (args.end) a.push('--end', String(args.end));
594
+ if (args.function_name) a.push('--function', args.function_name);
595
+ a.push('--root', args.root || this.rootDir);
596
+ result = this.execCLI(a);
597
+ break;
598
+ }
599
+ case 'api_surface': {
600
+ const a = ['api-surface'];
601
+ if (args.package) a.push('--package', args.package);
602
+ if (args.limit) a.push('--limit', String(args.limit));
603
+ const out = this.resolveBakeDir(args);
604
+ if (out) a.push('--out', out);
605
+ result = this.execCLI(a);
606
+ break;
607
+ }
608
+ case 'related_to': {
609
+ const a = ['related-to', args.symbol];
610
+ if (args.depth) a.push('--depth', String(args.depth));
611
+ const out = this.resolveBakeDir(args);
612
+ if (out) a.push('--out', out);
613
+ result = this.execCLI(a);
614
+ break;
615
+ }
616
+ case 'package_summary': {
617
+ const a = ['package-summary', args.package];
618
+ const out = this.resolveBakeDir(args);
619
+ if (out) a.push('--out', out);
620
+ result = this.execCLI(a);
621
+ break;
622
+ }
623
+ case 'crud_operations': {
624
+ const a = ['crud'];
625
+ if (args.entity) a.push('--entity', args.entity);
626
+ const out = this.resolveBakeDir(args);
627
+ if (out) a.push('--out', out);
628
+ result = this.execCLI(a);
629
+ break;
630
+ }
631
+ case 'architecture_map': {
632
+ const a = ['architecture-map'];
633
+ if (args.intent) a.push('--intent', args.intent);
634
+ const out = this.resolveBakeDir(args);
635
+ if (out) a.push('--out', out);
636
+ result = this.execCLI(a);
637
+ break;
638
+ }
639
+ case 'file_functions': {
640
+ const a = ['file-functions', args.file];
641
+ const out = this.resolveBakeDir(args);
642
+ if (out) a.push('--out', out);
643
+ result = this.execCLI(a);
644
+ break;
645
+ }
646
+ case 'suggest_placement': {
647
+ const a = ['suggest-placement', '--name', args.function_name, '--type', args.function_type];
648
+ if (args.related_to) a.push('--related-to', args.related_to);
649
+ const out = this.resolveBakeDir(args);
650
+ if (out) a.push('--out', out);
651
+ result = this.execCLI(a);
652
+ break;
653
+ }
654
+ case 'find_docs': {
655
+ const a = ['find-docs'];
656
+ if (args.type) a.push('--type', args.type);
657
+ result = this.execCLI(a);
658
+ break;
659
+ }
660
+ case 'frontend': {
661
+ const a = ['frontend', args.op];
662
+ if (args.name) a.push('--name', args.name);
663
+ if (args.type) a.push('--search-type', args.type);
664
+ if (args.depth) a.push('--depth', String(args.depth));
665
+ if (args.limit) a.push('--limit', String(args.limit));
666
+ const out = this.resolveBakeDir(args);
667
+ if (out) a.push('--out', out);
668
+ result = this.execCLI(a);
669
+ break;
670
+ }
671
+ case 'api_trace': {
672
+ const a = ['api-trace', args.endpoint];
673
+ if (args.method) a.push('--method', args.method);
674
+ const out = this.resolveBakeDir(args);
675
+ if (out) a.push('--out', out);
676
+ result = this.execCLI(a);
677
+ break;
678
+ }
679
+ case 'all_endpoints': {
680
+ const a = ['all-endpoints'];
681
+ if (args.include_frontend === false) a.push('--frontend=false');
682
+ if (args.include_backend === false) a.push('--backend=false');
683
+ const out = this.resolveBakeDir(args);
684
+ if (out) a.push('--out', out);
685
+ result = this.execCLI(a);
686
+ break;
687
+ }
688
+ case 'impact': {
689
+ if (!args.symbol) throw new Error('Missing required parameter: symbol');
690
+ const a = ['impact', args.symbol];
691
+ if (args.depth) a.push('--depth', String(args.depth));
692
+ const out = this.resolveBakeDir(args);
693
+ if (out) a.push('--out', out);
694
+ result = this.execCLI(a);
695
+ break;
696
+ }
697
+ case 'decompose': {
698
+ const a = ['decompose'];
699
+ const out = this.resolveBakeDir(args);
700
+ if (out) a.push('--out', out);
701
+ result = this.execCLI(a);
702
+ break;
703
+ }
704
+ case 'architecture_smells': {
705
+ const a = ['architecture-smells'];
706
+ if (args.max_complexity) a.push('--max-complexity', String(args.max_complexity));
707
+ if (args.max_fan_out) a.push('--max-fan-out', String(args.max_fan_out));
708
+ if (args.max_fan_in) a.push('--max-fan-in', String(args.max_fan_in));
709
+ if (args.max_pkg_funcs) a.push('--max-pkg-funcs', String(args.max_pkg_funcs));
710
+ const out = this.resolveBakeDir(args);
711
+ if (out) a.push('--out', out);
712
+ result = this.execCLI(a);
713
+ break;
714
+ }
715
+ default:
716
+ throw new Error(`Unknown tool: ${name}`);
717
+ }
718
+ return result;
719
+ }
720
+
721
+ handleNavigatorTool(name, args = {}) {
722
+ let result;
723
+ switch (name) {
724
+ case 'navigator_list': {
725
+ const a = ['navigator', 'list'];
726
+ const out = this.resolveBakeDir(args);
727
+ if (out) a.push('--out', out);
728
+ result = this.execCLI(a);
729
+ break;
730
+ }
731
+ case 'navigator_search': {
732
+ const a = ['navigator', 'search', args.query];
733
+ if (args.spec) a.push('--spec', args.spec);
734
+ if (args.method) a.push('--method', args.method);
735
+ if (args.limit) a.push('--limit', String(args.limit));
736
+ const out = this.resolveBakeDir(args);
737
+ if (out) a.push('--out', out);
738
+ result = this.execCLI(a);
739
+ break;
740
+ }
741
+ case 'navigator_endpoint': {
742
+ const a = ['navigator', 'endpoint', args.spec, args.path];
743
+ if (args.method) a.push('--method', args.method);
744
+ const out = this.resolveBakeDir(args);
745
+ if (out) a.push('--out', out);
746
+ result = this.execCLI(a);
747
+ break;
748
+ }
749
+ default:
750
+ throw new Error(`Unknown navigator tool: ${name}`);
751
+ }
752
+ return result;
753
+ }
754
+
755
+ handleGQLNavTool(name, args = {}) {
756
+ let result;
757
+ switch (name) {
758
+ case 'gqlnav_summary': {
759
+ if (!args.spec) throw new Error('Missing required parameter: spec (schema name from gqlnav_list)');
760
+ const a = ['gql-nav', 'summary', args.spec];
761
+ const out = this.resolveBakeDir(args);
762
+ if (out) a.push('--out', out);
763
+ result = this.execCLI(a);
764
+ break;
765
+ }
766
+ case 'gqlnav_list': {
767
+ const a = ['gql-nav', 'list'];
768
+ const out = this.resolveBakeDir(args);
769
+ if (out) a.push('--out', out);
770
+ result = this.execCLI(a);
771
+ break;
772
+ }
773
+ case 'gqlnav_search': {
774
+ if (!args.query) throw new Error('Missing required parameter: query');
775
+ const a = ['gql-nav', 'search', args.query];
776
+ if (args.spec) a.push('--spec', args.spec);
777
+ if (args.kind) a.push('--kind', args.kind);
778
+ if (args.limit) a.push('--limit', String(args.limit));
779
+ const out = this.resolveBakeDir(args);
780
+ if (out) a.push('--out', out);
781
+ result = this.execCLI(a);
782
+ break;
783
+ }
784
+ case 'gqlnav_type': {
785
+ if (!args.spec) throw new Error('Missing required parameter: spec (schema name from gqlnav_list)');
786
+ if (!args.name) throw new Error('Missing required parameter: name (type/query/mutation name)');
787
+ const a = ['gql-nav', 'type', args.spec, args.name];
788
+ const out = this.resolveBakeDir(args);
789
+ if (out) a.push('--out', out);
790
+ result = this.execCLI(a);
791
+ break;
792
+ }
793
+ default:
794
+ throw new Error(`Unknown gqlnav tool: ${name}`);
795
+ }
796
+ return result;
797
+ }
798
+
799
+ handleScholarTool(name, args = {}) {
800
+ let result;
801
+ switch (name) {
802
+ case 'scholar_search': {
803
+ if (!args.query) throw new Error('Missing required parameter: query');
804
+ const a = ['scholar', 'search', args.query];
805
+ if (args.source) a.push('--source', args.source);
806
+ if (args.year) a.push('--year', args.year);
807
+ if (args.fields) a.push('--fields', args.fields);
808
+ if (args.open_access) a.push('--oa');
809
+ if (args.limit) a.push('--limit', String(args.limit));
810
+ if (args.refresh) a.push('--refresh');
811
+ const out = this.resolveBakeDir(args);
812
+ if (out) a.push('--out', out);
813
+ result = this.execCLI(a);
814
+ break;
815
+ }
816
+ case 'scholar_paper': {
817
+ if (!args.id) throw new Error('Missing required parameter: id (paper ID, DOI, ArXiv ID, or OpenAlex ID)');
818
+ const a = ['scholar', 'paper', args.id];
819
+ if (args.source) a.push('--source', args.source);
820
+ if (args.refresh) a.push('--refresh');
821
+ const out = this.resolveBakeDir(args);
822
+ if (out) a.push('--out', out);
823
+ result = this.execCLI(a);
824
+ break;
825
+ }
826
+ case 'scholar_citations': {
827
+ if (!args.id) throw new Error('Missing required parameter: id (paper ID)');
828
+ const direction = args.direction || 'citations';
829
+ const a = ['scholar', direction, args.id];
830
+ if (args.source) a.push('--source', args.source);
831
+ if (args.limit) a.push('--limit', String(args.limit));
832
+ if (args.refresh) a.push('--refresh');
833
+ const out = this.resolveBakeDir(args);
834
+ if (out) a.push('--out', out);
835
+ result = this.execCLI(a);
836
+ break;
837
+ }
838
+ case 'scholar_list': {
839
+ const a = ['scholar', 'list'];
840
+ const out = this.resolveBakeDir(args);
841
+ if (out) a.push('--out', out);
842
+ result = this.execCLI(a);
843
+ break;
844
+ }
845
+ default:
846
+ throw new Error(`Unknown scholar tool: ${name}`);
847
+ }
848
+ return result;
849
+ }
850
+
851
+ // ═══════════════════════════════════════════════════════════════════════════
852
+ // Server Lifecycle
853
+ // ═══════════════════════════════════════════════════════════════════════════
854
+
855
+ async run() {
856
+ const transport = new StdioServerTransport();
857
+ await this.server.connect(transport);
858
+
859
+ if (process.env.DEBUG) {
860
+ console.error('cartogopher MCP v4 server started');
861
+ console.error(` Root: ${this.rootDir}`);
862
+ console.error(` Binary: ${this.binary || 'NOT FOUND'}`);
863
+ }
864
+ }
865
+ }
866
+
867
+ const server = new CartoGopherMCP();
868
+ server.run().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "cartogopher",
3
+ "version": "5.0.0",
4
+ "description": "AI-native code intelligence: parses codebases into a searchable graph of functions, types, endpoints, and call relationships. CLI + MCP server.",
5
+ "bin": {
6
+ "cartogopher": "bin/cartogopher.js"
7
+ },
8
+ "files": [
9
+ "bin",
10
+ "lib",
11
+ "share",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "homepage": "https://cartogopher.com",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/JakeNesler/CartoGopher.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/JakeNesler/CartoGopher/issues"
24
+ },
25
+ "license": "SEE LICENSE IN LICENSE",
26
+ "author": "Jake Nesler",
27
+ "keywords": [
28
+ "code-intelligence",
29
+ "ast",
30
+ "mcp",
31
+ "llm",
32
+ "code-search",
33
+ "static-analysis"
34
+ ],
35
+ "dependencies": {
36
+ "@modelcontextprotocol/sdk": "^1.18.2"
37
+ },
38
+ "optionalDependencies": {
39
+ "@cartogopher/darwin-arm64": "5.0.0",
40
+ "@cartogopher/darwin-x64": "5.0.0",
41
+ "@cartogopher/linux-x64": "5.0.0",
42
+ "@cartogopher/linux-arm64": "5.0.0"
43
+ }
44
+ }
@@ -0,0 +1,113 @@
1
+ ---
2
+ name: cartogopher
3
+ description: Code intelligence CLI for analyzing codebases. Use when exploring code architecture, searching for functions, tracing API endpoints, understanding call graphs, or analyzing dependencies. Supports 20+ languages.
4
+ argument-hint: [command] [args]
5
+ allowed-tools: Bash(cartogopher:*) Read
6
+ ---
7
+
8
+ # CartoGopher CLI
9
+
10
+ Parses codebases into a searchable graph of functions, types, endpoints, and call relationships. Local-only — runs on a SQLite store on disk; no servers to set up.
11
+
12
+ ## Setup
13
+
14
+ ```bash
15
+ cartogopher bake # parse the current project (~10-30s for typical repos)
16
+ ```
17
+
18
+ That's it. Re-run `cartogopher bake` (or use `cartogopher watch`) when files change; it's incremental.
19
+
20
+ ## Commands
21
+
22
+ ### Search
23
+ ```bash
24
+ cartogopher search "CreateUser" # name search
25
+ cartogopher search "webhook" --in=strings # search string literals only
26
+ cartogopher search "TODO" --in=comments # search comments only
27
+ cartogopher search "Delete" --pattern=call # find function call sites
28
+ cartogopher search "handles payment" --mode=semantic # semantic vector search
29
+ ```
30
+
31
+ ### Symbol lookup
32
+ ```bash
33
+ cartogopher symbol CreateUser # function details: signature, calls, file, complexity
34
+ cartogopher symbol UserService # type details: fields, methods
35
+ ```
36
+
37
+ ### Call graph
38
+ ```bash
39
+ cartogopher related-to HandleRequest # callers + callees
40
+ cartogopher related-to HandleRequest --depth=3
41
+ ```
42
+
43
+ ### Impact analysis (refactor blast radius)
44
+ ```bash
45
+ cartogopher impact DeleteUser # transitive callers, affected endpoints, files, risk score
46
+ cartogopher impact DeleteUser --depth=5
47
+ ```
48
+
49
+ ### Architecture
50
+ ```bash
51
+ cartogopher architecture-map # directory map with purpose inference
52
+ cartogopher architecture-smells # detect anti-patterns (god functions, N+1, mega-modules)
53
+ cartogopher decompose # suggest microservice boundaries
54
+ cartogopher suggest-placement NewHandler service # where to put new code
55
+ ```
56
+
57
+ ### API endpoints
58
+ ```bash
59
+ cartogopher all-endpoints # every HTTP endpoint in the codebase
60
+ cartogopher api-trace /api/users # trace endpoint: frontend -> handler -> CRUD
61
+ cartogopher api-surface --package=controllers
62
+ ```
63
+
64
+ ### CRUD analysis
65
+ ```bash
66
+ cartogopher crud # entity CRUD breakdown
67
+ cartogopher crud --entity=User
68
+ ```
69
+
70
+ ### File operations
71
+ ```bash
72
+ cartogopher file-functions controllers/auth.go # list functions in a file (no full read)
73
+ cartogopher slice auth.go --start=10 --end=50 # read specific lines
74
+ cartogopher patch auth.go --start=10 --end=20 --content="new code"
75
+ ```
76
+
77
+ ### Frontend (Vue / React / Svelte / Angular / Proto / GraphQL)
78
+ ```bash
79
+ cartogopher frontend summary
80
+ cartogopher frontend component UserCard
81
+ cartogopher frontend search "payment"
82
+ cartogopher frontend deps UserCard
83
+ ```
84
+
85
+ ### Overview
86
+ ```bash
87
+ cartogopher shake # full repo overview (Shake.readme)
88
+ cartogopher package-summary controllers # package stats + top-complexity functions
89
+ cartogopher find-docs # discover README/.env/config files
90
+ ```
91
+
92
+ ## Common flags
93
+
94
+ | Flag | Description |
95
+ |---|---|
96
+ | `--root=PATH` | Project root (default: `.`) |
97
+ | `--limit=N` | Max results (default: 20) |
98
+ | `--depth=N` | Call graph / impact depth (default: 2) |
99
+ | `--package=PKG` | Filter by package |
100
+ | `--lang=LANG` | Filter by language |
101
+ | `--in=CONTEXT` | Search context: `all`, `strings`, `comments`, `identifiers`, `map-keys`, `imports` |
102
+ | `--pattern=TYPE` | Search pattern: `call`, `assign`, `return`, `map-access` |
103
+ | `--mode=semantic` | Vector search (requires `bake` to have populated embeddings) |
104
+
105
+ ## When to use what
106
+
107
+ - **"Where does X get used?"** → `related-to X` or `search X --pattern=call`
108
+ - **"What would break if I rename X?"** → `impact X`
109
+ - **"What does this codebase look like?"** → `shake` or `architecture-map`
110
+ - **"Are there any code smells?"** → `architecture-smells`
111
+ - **"What's in this file?"** → `file-functions path/to/file` (don't `Read` the whole file)
112
+ - **"What endpoints exist?"** → `all-endpoints`, then `api-trace` on a specific one
113
+ - **"What does this function call?"** → `symbol FunctionName`