Skip to main content

chronicle/cli/
mod.rs

1pub mod annotate;
2pub mod backfill;
3pub mod context;
4pub mod contracts;
5pub mod correct;
6pub mod decisions;
7pub mod deps;
8pub mod doctor;
9pub mod export;
10pub mod flag;
11pub mod history;
12pub mod import;
13pub mod init;
14pub mod knowledge;
15pub mod lookup;
16pub mod note;
17pub mod read;
18pub mod reconfigure;
19pub mod schema;
20pub mod setup;
21pub mod show;
22pub mod status;
23pub mod summary;
24pub mod sync;
25pub(crate) mod util;
26#[cfg(feature = "web")]
27pub mod web;
28
29use clap::{Parser, Subcommand};
30
31#[derive(Parser)]
32#[command(
33    name = "git-chronicle",
34    version,
35    about = "AI-powered commit annotation"
36)]
37pub struct Cli {
38    #[command(subcommand)]
39    pub command: Commands,
40}
41
42#[derive(Subcommand)]
43pub enum Commands {
44    /// One-time machine-wide setup (provider, skills, hooks, CLAUDE.md)
45    Setup {
46        /// Overwrite existing files without prompting
47        #[arg(long)]
48        force: bool,
49
50        /// Print what would be done without writing
51        #[arg(long)]
52        dry_run: bool,
53
54        /// Skip installing Claude Code skills
55        #[arg(long)]
56        skip_skills: bool,
57
58        /// Skip installing Claude Code hooks
59        #[arg(long)]
60        skip_hooks: bool,
61
62        /// Skip modifying ~/.claude/CLAUDE.md
63        #[arg(long)]
64        skip_claude_md: bool,
65    },
66
67    /// Rerun the LLM provider selection prompt
68    Reconfigure,
69
70    /// Annotate historical commits that lack Chronicle annotations
71    Backfill {
72        /// Maximum number of commits to annotate
73        #[arg(long, default_value = "20")]
74        limit: usize,
75
76        /// List commits that would be annotated without calling the LLM
77        #[arg(long)]
78        dry_run: bool,
79    },
80
81    /// Initialize chronicle in the current repository
82    Init {
83        /// Disable notes sync (sync is enabled by default)
84        #[arg(long)]
85        no_sync: bool,
86
87        /// Skip hook installation
88        #[arg(long)]
89        no_hooks: bool,
90
91        /// LLM provider to use
92        #[arg(long)]
93        provider: Option<String>,
94
95        /// LLM model to use
96        #[arg(long)]
97        model: Option<String>,
98
99        /// Run backfill after init (annotate last 20 commits)
100        #[arg(long)]
101        backfill: bool,
102    },
103
104    /// Manage annotation context
105    Context {
106        #[command(subcommand)]
107        action: ContextAction,
108    },
109
110    /// Read annotations for a file
111    Read {
112        /// File path to read annotations for
113        path: String,
114
115        /// Filter by AST anchor name
116        #[arg(long)]
117        anchor: Option<String>,
118
119        /// Filter by line range (format: start:end)
120        #[arg(long)]
121        lines: Option<String>,
122    },
123
124    /// Annotate a specific commit
125    Annotate {
126        /// Commit to annotate (default: HEAD)
127        #[arg(long, default_value = "HEAD")]
128        commit: String,
129
130        /// Read AnnotateInput JSON from stdin (live annotation path, zero LLM cost)
131        #[arg(long)]
132        live: bool,
133
134        /// Comma-separated source commit SHAs for squash synthesis (CI usage)
135        #[arg(long)]
136        squash_sources: Option<String>,
137
138        /// Old commit SHA to migrate annotation from (amend re-annotation)
139        #[arg(long)]
140        amend_source: Option<String>,
141
142        /// Quick annotation: provide summary directly on command line
143        #[arg(long, conflicts_with_all = ["live", "json_input", "squash_sources", "amend_source"])]
144        summary: Option<String>,
145
146        /// Provide full annotation JSON on command line
147        #[arg(long = "json", conflicts_with_all = ["live", "summary", "squash_sources", "amend_source"])]
148        json_input: Option<String>,
149
150        /// Auto-annotate using the commit message as summary
151        #[arg(long, conflicts_with_all = ["live", "summary", "json_input", "squash_sources", "amend_source"])]
152        auto: bool,
153    },
154
155    /// Flag a region annotation as potentially inaccurate
156    Flag {
157        /// File path relative to repository root
158        path: String,
159
160        /// Optional AST anchor name to scope the flag to a specific region
161        anchor: Option<String>,
162
163        /// Reason for flagging this annotation
164        #[arg(long)]
165        reason: String,
166    },
167
168    /// Apply a precise correction to a specific annotation field
169    Correct {
170        /// Commit SHA of the annotation to correct
171        sha: String,
172
173        /// AST anchor name of the region within the annotation
174        #[arg(long)]
175        region: String,
176
177        /// Annotation field to correct (intent, reasoning, constraints, risk_notes, semantic_dependencies, tags)
178        #[arg(long)]
179        field: String,
180
181        /// Specific value to remove or mark as incorrect
182        #[arg(long)]
183        remove: Option<String>,
184
185        /// Replacement or amendment text
186        #[arg(long)]
187        amend: Option<String>,
188    },
189
190    /// Manage notes sync with remotes
191    Sync {
192        #[command(subcommand)]
193        action: SyncAction,
194    },
195
196    /// Export annotations as JSONL
197    Export {
198        /// Write to file instead of stdout
199        #[arg(short, long)]
200        output: Option<String>,
201    },
202
203    /// Import annotations from a JSONL file
204    Import {
205        /// JSONL file to import
206        file: String,
207
208        /// Overwrite existing annotations
209        #[arg(long)]
210        force: bool,
211
212        /// Show what would be imported without writing
213        #[arg(long)]
214        dry_run: bool,
215    },
216
217    /// Run diagnostic checks on the chronicle setup
218    Doctor {
219        /// Output as JSON
220        #[arg(long)]
221        json: bool,
222
223        /// Include annotation staleness check (scans recent annotations)
224        #[arg(long)]
225        staleness: bool,
226    },
227
228    /// Find code that depends on a given file/anchor (dependency inversion)
229    Deps {
230        /// File path to query
231        path: String,
232
233        /// AST anchor name to query
234        anchor: Option<String>,
235
236        /// Output format (json or pretty)
237        #[arg(long, default_value = "json")]
238        format: String,
239
240        /// Maximum number of results to return
241        #[arg(long, default_value = "50")]
242        max_results: u32,
243
244        /// Maximum number of commits to scan
245        #[arg(long, default_value = "500")]
246        scan_limit: u32,
247
248        /// Omit metadata (schema, query echo, stats) from JSON output
249        #[arg(long)]
250        compact: bool,
251    },
252
253    /// Show annotation timeline for a file/anchor across commits
254    History {
255        /// File path to query
256        path: String,
257
258        /// AST anchor name to query
259        anchor: Option<String>,
260
261        /// Maximum number of timeline entries
262        #[arg(long, default_value = "10")]
263        limit: u32,
264
265        /// Output format (json or pretty)
266        #[arg(long, default_value = "json")]
267        format: String,
268
269        /// Omit metadata (schema, query echo, stats) from JSON output
270        #[arg(long)]
271        compact: bool,
272    },
273
274    /// Interactive TUI explorer for annotated source code
275    Show {
276        /// File path to show
277        path: String,
278
279        /// Focus on a specific AST anchor
280        anchor: Option<String>,
281
282        /// Commit to show file at
283        #[arg(long, default_value = "HEAD")]
284        commit: String,
285
286        /// Force non-interactive plain-text output
287        #[arg(long)]
288        no_tui: bool,
289    },
290
291    /// Print JSON Schema for annotation types (self-documenting for AI agents)
292    Schema {
293        /// Schema name: annotate-input, annotation
294        name: String,
295    },
296
297    /// Query contracts and dependencies for a file/anchor ("What must I not break?")
298    Contracts {
299        /// File path to query
300        path: String,
301
302        /// AST anchor name to query
303        #[arg(long)]
304        anchor: Option<String>,
305
306        /// Output format (json or pretty)
307        #[arg(long, default_value = "json")]
308        format: String,
309
310        /// Omit metadata (schema, query echo, stats) from JSON output
311        #[arg(long)]
312        compact: bool,
313    },
314
315    /// Query design decisions and rejected alternatives ("What was decided?")
316    Decisions {
317        /// File path to scope decisions to (omit for all)
318        path: Option<String>,
319
320        /// Output format (json or pretty)
321        #[arg(long, default_value = "json")]
322        format: String,
323
324        /// Omit metadata (schema, query echo) from JSON output
325        #[arg(long)]
326        compact: bool,
327    },
328
329    /// Show condensed annotation summary for a file
330    Summary {
331        /// File path to query
332        path: String,
333
334        /// Filter to a specific AST anchor
335        #[arg(long)]
336        anchor: Option<String>,
337
338        /// Output format (json or pretty)
339        #[arg(long, default_value = "json")]
340        format: String,
341
342        /// Omit metadata (schema, query echo, stats) from JSON output
343        #[arg(long)]
344        compact: bool,
345    },
346
347    /// One-stop context lookup for a file (contracts + decisions + history)
348    Lookup {
349        /// File path to query
350        path: String,
351
352        /// AST anchor name
353        #[arg(long)]
354        anchor: Option<String>,
355
356        /// Output format (json or pretty)
357        #[arg(long, default_value = "json")]
358        format: String,
359
360        /// Compact output (payload only)
361        #[arg(long)]
362        compact: bool,
363    },
364
365    /// Show annotation status and coverage for the repository
366    Status {
367        /// Output format
368        #[arg(long, default_value = "json")]
369        format: String,
370    },
371
372    /// Stage a note for the next annotation (captured context during work)
373    Note {
374        /// The note text to stage (omit to list or clear)
375        text: Option<String>,
376
377        /// List current staged notes
378        #[arg(long)]
379        list: bool,
380
381        /// Clear all staged notes
382        #[arg(long)]
383        clear: bool,
384    },
385
386    /// Launch web viewer for browsing annotations
387    #[cfg(feature = "web")]
388    Web {
389        /// Port to listen on
390        #[arg(long, default_value = "3000")]
391        port: u16,
392
393        /// Open browser automatically
394        #[arg(long)]
395        open: bool,
396    },
397
398    /// Manage repo-level knowledge (conventions, boundaries, anti-patterns)
399    Knowledge {
400        #[command(subcommand)]
401        action: KnowledgeAction,
402    },
403}
404
405#[derive(Subcommand)]
406#[allow(clippy::large_enum_variant)]
407pub enum KnowledgeAction {
408    /// List all knowledge entries
409    List {
410        /// Output as JSON
411        #[arg(long)]
412        json: bool,
413    },
414
415    /// Add a new knowledge entry
416    Add {
417        /// Type of entry: convention, boundary, anti-pattern
418        #[arg(long = "type")]
419        entry_type: String,
420
421        /// Stable ID (auto-generated if omitted)
422        #[arg(long)]
423        id: Option<String>,
424
425        /// File/directory scope (for conventions)
426        #[arg(long)]
427        scope: Option<String>,
428
429        /// The rule text (for conventions)
430        #[arg(long)]
431        rule: Option<String>,
432
433        /// Module directory (for boundaries)
434        #[arg(long)]
435        module: Option<String>,
436
437        /// What the module owns (for boundaries)
438        #[arg(long)]
439        owns: Option<String>,
440
441        /// What must not cross the boundary (for boundaries)
442        #[arg(long)]
443        boundary: Option<String>,
444
445        /// The anti-pattern to avoid
446        #[arg(long)]
447        pattern: Option<String>,
448
449        /// What to do instead (for anti-patterns)
450        #[arg(long)]
451        instead: Option<String>,
452
453        /// Stability level: permanent, provisional, experimental
454        #[arg(long)]
455        stability: Option<String>,
456
457        /// Commit SHA where this was decided
458        #[arg(long)]
459        decided_in: Option<String>,
460
461        /// Where this was learned (for anti-patterns)
462        #[arg(long)]
463        learned_from: Option<String>,
464    },
465
466    /// Remove a knowledge entry by ID
467    Remove {
468        /// ID of the entry to remove
469        id: String,
470    },
471}
472
473#[derive(Subcommand)]
474pub enum SyncAction {
475    /// Enable notes sync for a remote
476    Enable {
477        /// Remote name (default: origin)
478        #[arg(long, default_value = "origin")]
479        remote: String,
480    },
481
482    /// Show sync status
483    Status {
484        /// Remote name (default: origin)
485        #[arg(long, default_value = "origin")]
486        remote: String,
487    },
488
489    /// Fetch and merge remote notes
490    Pull {
491        /// Remote name (default: origin)
492        #[arg(long, default_value = "origin")]
493        remote: String,
494    },
495}
496
497#[derive(Subcommand)]
498pub enum ContextAction {
499    /// Set pending context for the next commit
500    Set {
501        #[arg(long)]
502        task: Option<String>,
503
504        #[arg(long)]
505        reasoning: Option<String>,
506
507        #[arg(long)]
508        dependencies: Option<String>,
509
510        #[arg(long)]
511        tags: Vec<String>,
512    },
513
514    /// Show current pending context
515    Show,
516
517    /// Clear pending context
518    Clear,
519}