cartog 0.27.0

Code graph indexer for LLM coding agents. Map your codebase, navigate by graph.
Documentation
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
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
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)]
#[command(propagate_version = true)]
#[command(after_help = "Docs: https://jrollin.github.io/cartog/")]
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)
    #[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`, promoted to a top-level verb so
    /// day-to-day savings are one keystroke away.
    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 (capped at 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
        #[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
        #[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,

        /// Turn on RAG embedding when auto-detection would leave it off
        ///
        /// Default is automatic: on when the repo already has embeddings.
        /// `[embedding] auto_embed` (config) and CARTOG_WATCH_RAG (env) take
        /// precedence over this flag.
        #[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.
    ///
    /// See `--client` for the supported editors. 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 every client's serve args (default wires `--watch` for all).
        #[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.
        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 every client's serve args (default wires `--watch` for all).
        #[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,

        /// Turn on RAG embedding when auto-detection would leave it off
        ///
        /// Default is automatic: on when the repo already has embeddings.
        /// `[embedding] auto_embed` (config) and CARTOG_WATCH_RAG (env) take
        /// precedence over this flag. Only meaningful with `--watch` (warns
        /// otherwise).
        #[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 without swapping the binary
        ///
        /// Records the target version in the state file and exits. 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
        ///
        /// Overrides 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,
    },
}