1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
use std::path::PathBuf;
use clap::{Parser, Subcommand, ValueEnum};
use cartog_core::{EdgeKind, SymbolKind};
/// Extended version string printed by `cartog --version` (long form).
/// Short form (`-V`) keeps the bare semver. Populated by `build.rs`.
pub const LONG_VERSION: &str = concat!(
env!("CARGO_PKG_VERSION"),
"\nbuild: ",
env!("CARTOG_BUILD_SHA"),
"\nfeatures: ",
env!("CARTOG_BUILD_FEATURES"),
"\nrustc: ",
env!("CARGO_PKG_RUST_VERSION"),
" (MSRV)",
);
#[derive(Debug, Parser)]
#[command(name = "cartog")]
#[command(about = "Map your codebase. Navigate by graph, not grep.")]
#[command(version)]
#[command(long_version = LONG_VERSION)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
/// Output as JSON
#[arg(long, global = true)]
pub json: bool,
/// Limit human-readable output to approximately N tokens (ignored with --json)
#[arg(long, global = true)]
pub tokens: Option<u32>,
/// Path to the cartog database (overrides .cartog.toml and auto-detection).
/// Can also be set via the CARTOG_DB environment variable.
#[arg(long, global = true, value_name = "PATH", env = "CARTOG_DB")]
pub db: Option<PathBuf>,
}
/// Filter for symbol kinds in the search command.
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum SymbolKindFilter {
Function,
Class,
Method,
Variable,
Import,
Interface,
Enum,
TypeAlias,
Trait,
Module,
Document,
/// Include all symbol kinds (code + documents).
All,
}
impl From<SymbolKindFilter> for SymbolKind {
fn from(f: SymbolKindFilter) -> Self {
match f {
SymbolKindFilter::Function => SymbolKind::Function,
SymbolKindFilter::Class => SymbolKind::Class,
SymbolKindFilter::Method => SymbolKind::Method,
SymbolKindFilter::Variable => SymbolKind::Variable,
SymbolKindFilter::Import => SymbolKind::Import,
SymbolKindFilter::Interface => SymbolKind::Interface,
SymbolKindFilter::Enum => SymbolKind::Enum,
SymbolKindFilter::TypeAlias => SymbolKind::TypeAlias,
SymbolKindFilter::Trait => SymbolKind::Trait,
SymbolKindFilter::Module => SymbolKind::Module,
SymbolKindFilter::Document => SymbolKind::Document,
SymbolKindFilter::All => unreachable!("All is not a single SymbolKind"),
}
}
}
/// Filter for edge kinds in the refs command.
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum EdgeKindFilter {
Calls,
Imports,
Inherits,
References,
Raises,
Implements,
TypeOf,
}
impl From<EdgeKindFilter> for EdgeKind {
fn from(f: EdgeKindFilter) -> Self {
match f {
EdgeKindFilter::Calls => EdgeKind::Calls,
EdgeKindFilter::Imports => EdgeKind::Imports,
EdgeKindFilter::Inherits => EdgeKind::Inherits,
EdgeKindFilter::References => EdgeKind::References,
EdgeKindFilter::Raises => EdgeKind::Raises,
EdgeKindFilter::Implements => EdgeKind::Implements,
EdgeKindFilter::TypeOf => EdgeKind::TypeOf,
}
}
}
/// MCP client targeted by `cartog ide`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ClientKind {
Antigravity,
ClaudeCode,
ClaudeDesktop,
Codex,
Cursor,
Gemini,
Hermes,
Kiro,
Opencode,
Vscode,
Windsurf,
Zed,
}
/// Scope filter for `cartog ide`: project-scoped configs, user-scoped configs, or both.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, serde::Serialize, Default)]
#[serde(rename_all = "kebab-case")]
pub enum IdeScope {
Project,
User,
#[default]
All,
}
#[derive(Debug, Subcommand)]
pub enum Command {
/// Build or rebuild the code graph index
Index {
/// Directory to index (defaults to current directory)
#[arg(default_value = ".")]
path: String,
/// Force full re-index, bypassing change detection
#[arg(long)]
force: bool,
/// Disable LSP-based edge resolution (auto-detected by default when servers are on PATH)
#[arg(long)]
no_lsp: bool,
},
/// Show symbols and structure of a file
Outline {
/// File path to outline
file: String,
},
/// Find what a symbol calls
Callees {
/// Symbol name to search for
name: String,
},
/// Transitive impact analysis — what breaks if this changes?
Impact {
/// Symbol name to analyze
name: String,
/// Maximum depth of transitive analysis
#[arg(long, default_value = "3")]
depth: u32,
},
/// Build a one-shot task-context bundle: relevant symbols + bodies for a task
Context {
/// Natural-language description of the task
task: String,
/// Approximate token budget for the bundle
#[arg(long, default_value = "6000")]
tokens: u32,
},
/// Find a call path between two symbols, with each hop's body inline
Trace {
/// Starting symbol (the caller end of the path)
from: String,
/// Target symbol (the callee end of the path)
to: String,
/// Maximum path length to search
#[arg(long, default_value = "8")]
depth: u32,
},
/// All references to a symbol (calls, imports, inherits, references, raises)
Refs {
/// Symbol name to search for
name: String,
/// Filter by edge kind
#[arg(long)]
kind: Option<EdgeKindFilter>,
},
/// Show inheritance hierarchy for a class
Hierarchy {
/// Class name
name: String,
/// Render the hierarchy as a Mermaid `graph TD` diagram instead of
/// plain text. Paste the output into any Mermaid renderer (GitHub,
/// mermaid.live, ...). Ignored when `--json` is also set.
#[arg(long)]
mermaid: bool,
},
/// File-level import dependencies
Deps {
/// File path
file: String,
/// Render the imports as a Mermaid `graph LR` diagram instead of
/// plain text. Ignored when `--json` is also set.
#[arg(long)]
mermaid: bool,
},
/// Index statistics summary
Stats {
/// Show per-tool query counts and estimated tokens saved vs grep+read.
/// Reads the local query log; no network calls.
#[arg(long)]
savings: bool,
},
/// Per-tool query counts + estimated tokens saved.
///
/// Alias for `cartog stats --savings`. Shipped as a top-level verb because
/// it's the retention hook — surfaces ongoing ROI in one keystroke.
Savings,
/// Upload the local index to an S3-compatible remote (opt-in feature).
///
/// Reads `[remote].url` from `.cartog.toml` unless `--remote` is given.
/// Credentials come from the AWS environment chain (env vars / profile /
/// IMDS); cartog never reads credentials from `.cartog.toml`.
Push {
/// Override `s3://bucket/key` target.
#[arg(long)]
remote: Option<String>,
},
/// Download an index from an S3-compatible remote (opt-in feature).
///
/// Refuses to overwrite the local DB while a peer (`cartog serve` /
/// `cartog watch`) holds it open, unless `--force` is given. Verifies a
/// SHA-256 checksum and schema version before atomic rename.
Pull {
/// Override `s3://bucket/key` target.
#[arg(long)]
remote: Option<String>,
/// Overwrite the local DB even if a peer process is currently using it.
#[arg(long)]
force: bool,
/// Skip credential resolution and pull anonymously (public buckets).
#[arg(long)]
no_sign_request: bool,
},
/// Display the current configuration
Config,
/// Check that requirements are met and everything is working
Doctor,
/// Search symbols by name (case-insensitive prefix + substring match)
Search {
/// Query string to match against symbol names
query: String,
/// Filter by symbol kind
#[arg(long)]
kind: Option<SymbolKindFilter>,
/// Filter to a specific file path
#[arg(long)]
file: Option<String>,
/// Maximum results to return (default: 30, max: 100)
#[arg(long, default_value = "30")]
limit: u32,
},
/// Token-budget-aware codebase summary (file tree + top symbols by centrality)
Map {
/// Approximate token budget for the output (default: 4000)
#[arg(long, default_value = "4000")]
tokens: u32,
/// Render the file tree as a Mermaid `graph TD` diagram instead of
/// indented text. Token budget still applies. Ignored when `--json`
/// is also set.
#[arg(long)]
mermaid: bool,
},
/// Show symbols affected by recent git changes
Changes {
/// Number of recent commits to consider (default: 5)
#[arg(long, default_value = "5")]
commits: u32,
/// Filter by symbol kind
#[arg(long)]
kind: Option<SymbolKindFilter>,
},
/// Watch for file changes and auto-re-index
Watch {
/// Directory to watch (defaults to current directory)
#[arg(default_value = ".")]
path: String,
/// Debounce window in seconds
#[arg(long, default_value = "5")]
debounce: u64,
/// Enable automatic RAG embedding after index
#[arg(long)]
rag: bool,
/// Delay in seconds before batch embedding after last index
#[arg(long, default_value = "30")]
rag_delay: u64,
},
/// Bootstrap cartog config in the current project: scaffold a `.cartog.toml` template.
///
/// Run `cartog ide` afterwards to wire editor MCP entries, and `cartog index`
/// to build the code graph. Each verb does one job: edit the toml between
/// steps to change DB path or embedding provider before any heavy work runs.
Init {
/// Print planned changes without writing.
#[arg(long)]
dry_run: bool,
},
/// Wire `cartog serve` into one or all MCP-compatible editors.
///
/// Supports Claude Code, Claude Desktop, Cursor, VS Code, Codex CLI, Gemini CLI,
/// OpenCode, Windsurf, Zed. User-scope clients whose config directory does not
/// exist are skipped (not installed).
///
/// See also `cartog install <client>...` for a positional shorthand that
/// matches the brew/npm/pip convention.
Ide {
/// Target a single client. Default: configure all clients in scope.
#[arg(long, value_enum)]
client: Option<ClientKind>,
/// Filter by scope. `project` writes only .mcp.json / .cursor/mcp.json; `user`
/// writes only user-scope configs; `all` writes both.
#[arg(long, value_enum, default_value_t = IdeScope::All)]
scope: IdeScope,
/// Accept all prompts (non-interactive). Implied by --dry-run, --json, --client, or a non-TTY stdin.
#[arg(long, short = 'y')]
yes: bool,
/// Print planned changes without writing.
#[arg(long)]
dry_run: bool,
/// Omit `--watch` from Claude Code's serve args.
#[arg(long)]
no_watch: bool,
},
/// Install cartog MCP config into one or more editors.
///
/// Friendlier shape of `cartog ide`: takes editors as positional arguments
/// (`cartog install cursor`, `cartog install cursor zed codex`) so it
/// matches the brew/npm/pip/cargo convention. No positional clients =
/// install into every detected editor non-interactively.
///
/// Positional mode is always non-interactive (`--yes` implied). For the
/// interactive picker, use `cartog ide` directly.
Install {
/// One or more editors to wire up. Omit to install into every
/// detected editor non-interactively. Repeatable.
clients: Vec<ClientKind>,
/// Filter by scope. `project` writes only .mcp.json / .cursor/mcp.json;
/// `user` writes only user-scope configs; `all` writes both.
#[arg(long, value_enum, default_value_t = IdeScope::All)]
scope: IdeScope,
/// Print planned changes without writing.
#[arg(long)]
dry_run: bool,
/// Omit `--watch` from Claude Code's serve args.
#[arg(long)]
no_watch: bool,
},
/// Start MCP server over stdio (for Claude Code, Cursor, and other MCP clients)
Serve {
/// Enable file watching with auto-re-index during MCP session
#[arg(long)]
watch: bool,
/// Enable automatic RAG embedding when watching
#[arg(long)]
rag: bool,
},
/// Semantic code search (RAG pipeline)
#[command(subcommand)]
Rag(RagCommand),
/// Manage the cartog installation: upgrade, inspect, roll back
#[command(name = "self", subcommand)]
Self_(SelfCommand),
/// Generate shell completions for bash, zsh, fish, elvish, or powershell.
///
/// Example: `cartog completions bash > ~/.local/share/bash-completion/completions/cartog`
Completions {
/// Shell to generate completions for
shell: clap_complete::Shell,
},
/// Emit a troff-formatted manpage for `cartog` on stdout.
///
/// Example:
/// cartog manpage > cartog.1
/// man ./cartog.1
Manpage,
}
#[derive(Debug, Subcommand)]
pub enum RagCommand {
/// Download embedding + re-ranker models from HuggingFace
Setup,
/// Build embedding index for semantic search (requires setup first)
Index {
/// Directory to index (defaults to current directory)
#[arg(default_value = ".")]
path: String,
/// Force re-embed all symbols
#[arg(long)]
force: bool,
},
/// Semantic search over code symbols
Search {
/// Natural language query
query: String,
/// Filter by symbol kind
#[arg(long)]
kind: Option<SymbolKindFilter>,
/// Maximum results to return
#[arg(long, default_value = "10")]
limit: u32,
},
}
#[derive(Debug, Subcommand)]
pub enum SelfCommand {
/// Upgrade cartog in place (or check, defer, or apply a deferred update)
Update {
/// Report whether an update is available without modifying anything.
/// Exit codes: 0 up to date, 1 update available, 2 network/parse error.
#[arg(long, conflicts_with_all = ["defer", "apply_pending"])]
check: bool,
/// Arm a deferred update: record the target version in the state file
/// and exit WITHOUT swapping the binary. Succeeds even while a peer
/// `cartog serve`/`watch` is running — the swap happens later via
/// `--apply-pending` once the peer has exited. This is the right call
/// from inside a Claude Code session, where the MCP server is the peer.
/// Targets the latest stable release unless `--to` pins a version.
#[arg(long, conflicts_with_all = ["check", "apply_pending"])]
defer: bool,
/// With `--defer`, arm exactly this `MAJOR.MINOR.PATCH` version instead
/// of resolving the latest stable release. Used by `/cartog-install` to
/// arm the plugin's pinned version. Requires `--defer`.
#[arg(long, value_name = "VERSION", requires = "defer")]
to: Option<String>,
/// Apply a previously-armed deferred update (see `--defer`). Reads the
/// pending target from the state file, waits briefly for any peer lock
/// to clear, performs the swap, and clears the pending intent. Intended
/// to run from the SessionEnd hook once the serve process has exited.
#[arg(long, conflicts_with_all = ["check", "defer"])]
apply_pending: bool,
/// Suppress all output; the exit code is the sole signal.
#[arg(long)]
quiet: bool,
},
/// Show installed version, target triple, install source, and last check time
Version,
/// Restore the previous binary saved at `<bin>.old`
Rollback,
/// Move a legacy `.cartog.db` (+ WAL/SHM/backups) into `.cartog/db.sqlite`
///
/// Detects the project root via the same rules as the rest of cartog
/// (walk up to the git root, or use cwd). Refuses to run while another
/// cartog process holds the peer lock, and never overwrites files at
/// the destination.
#[command(name = "migrate-db")]
MigrateDb {
/// Print the planned moves without touching the filesystem.
#[arg(long)]
dry_run: bool,
},
}