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
//! Command-line argument definitions via `clap` derive.
//!
//! All CLI types live here so that `seshat-bin` stays thin — it only
//! parses args via [`Cli::parse()`] and delegates to this crate.
use std::path::PathBuf;
use clap::{Parser, Subcommand, ValueEnum};
use seshat_storage::DecisionState;
/// Full version string including git hash: "0.1.0 (abc1234)".
const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", env!("GIT_HASH"), ")");
/// Seshat — the operating manual for your codebase, written for AI agents.
#[derive(Debug, Parser)]
#[command(
name = "seshat",
version = VERSION,
about = "The operating manual for your codebase — written for AI agents",
long_about = None,
)]
pub struct Cli {
/// The subcommand to execute.
#[command(subcommand)]
pub command: Command,
}
/// Top-level subcommands.
#[derive(Debug, Subcommand)]
pub enum Command {
/// Scan a project directory and display analysis report.
Scan {
/// Path to the project directory to scan.
path: PathBuf,
/// Show verbose output: skipped files, detector details, timing.
#[arg(long, short)]
verbose: bool,
/// Show only errors and final summary.
#[arg(long, short)]
quiet: bool,
/// Exclude submodules from scanning (they are scanned by default).
#[arg(long)]
exclude_submodules: bool,
},
/// Start the MCP server for AI agent connections.
Serve {
/// Repository directory path or project name.
/// Auto-detected from current working directory if omitted.
repo: Option<PathBuf>,
/// Host to bind the HTTP/SSE transport to (overrides config).
#[arg(long)]
host: Option<String>,
/// Port for the HTTP/SSE transport (overrides config).
#[arg(long)]
port: Option<u16>,
/// Log MCP tool calls to JSONL file for analysis.
/// Default: $XDG_DATA_HOME/seshat/call-log.jsonl
#[arg(long, value_name = "PATH", num_args = 0..=1, default_missing_value = "")]
call_log: Option<PathBuf>,
},
/// Show indexed projects, submodules, and database info.
Status {
/// Show full database paths and additional detail.
#[arg(long, short)]
verbose: bool,
},
/// Interactive convention review.
///
/// On startup, compares the active branch's `last_scanned_commit` against
/// `git rev-parse HEAD` and runs an incremental sync to the current HEAD
/// before opening the TUI, so the review queue reflects the on-disk state.
Review {
/// Skip the pre-TUI freshness check and incremental sync.
///
/// Use for emergency / debug access to the existing snapshot when
/// sync would be slow or undesirable. Implies the queue may be stale.
#[arg(long)]
no_sync: bool,
},
/// Generate MCP configuration for detected AI clients.
///
/// Auto-detects installed AI coding clients. By default uses smart scope:
/// project-level config if it already exists, global config otherwise.
/// For JSON configs, offers to auto-patch with backup. For JSONC, shows
/// a copy-paste snippet.
Init {
/// Specific client to configure. Auto-detects all if omitted.
/// Supported: claude-code, claude-desktop, opencode, cursor
client: Option<String>,
/// Always use project-level configs (in CWD / git root).
/// Writes to .claude/settings.local.json, ./opencode.json, etc.
#[arg(long, conflicts_with = "global")]
project: bool,
/// Always use global user configs (default fallback behaviour).
#[arg(long, conflicts_with = "project")]
global: bool,
/// Show what would be done without writing any files.
#[arg(long)]
dry_run: bool,
/// Only write MCP config; skip agent instructions, skills, and hooks.
#[arg(long)]
skip_instructions: bool,
},
/// Check for newer versions or upgrade the seshat binary.
Update {
/// Only check whether a newer version exists (no installation).
#[arg(long)]
check: bool,
},
/// Print a shell completion script to stdout.
///
/// If the shell is omitted, it is auto-detected from the `$SHELL`
/// environment variable (or PowerShell on Windows). Pipe into the
/// shell's completion directory or `eval` it from a shell rc file.
/// Examples:
///
/// seshat completions # auto-detect
/// seshat completions bash > /etc/bash_completion.d/seshat
/// seshat completions zsh > "${fpath\[1\]}/_seshat"
/// seshat completions fish > ~/.config/fish/completions/seshat.fish
Completions {
/// Target shell. Auto-detected from `$SHELL` if omitted.
#[arg(value_enum)]
shell: Option<clap_complete::Shell>,
},
/// Debug: print all conventions with real evidence snippets from the DB.
///
/// Reads conventions from the current project's database and prints
/// description, nature, confidence, adoption stats, and full snippet
/// text for each evidence item. Use for debugging snippet extraction.
#[command(hide = true)]
DebugSnippets {
/// Path to project directory. Auto-detected from CWD if omitted.
path: Option<PathBuf>,
},
/// Manage user-recorded decisions stored in the project database.
///
/// Decisions are project-wide records of approve/reject/partial/recorded
/// outcomes for conventions. They survive branch deletion and are the
/// source of truth for the review queue's exclusion filter.
Decisions {
/// The decisions subcommand to execute.
#[command(subcommand)]
command: DecisionsCommand,
},
/// Remove all Seshat configuration from detected AI clients.
///
/// Reverses `seshat init`: removes MCP entries, instruction sections,
/// skill directories, and hook scripts. Does NOT remove the binary or DB files.
Uninstall {
/// Specific client to uninstall. Auto-detects all if omitted.
/// Supported: claude-code, claude-desktop, opencode, cursor
client: Option<String>,
/// Only uninstall from project-level configs.
#[arg(long, conflicts_with = "global")]
project: bool,
/// Only uninstall from global user configs.
#[arg(long, conflicts_with = "project")]
global: bool,
/// Show what would be removed without making changes.
#[arg(long)]
dry_run: bool,
},
}
/// Subcommands for `seshat decisions`.
#[derive(Debug, Subcommand)]
pub enum DecisionsCommand {
/// List recorded decisions, optionally filtered by state and branch.
List {
/// Restrict to decisions in a single state.
#[arg(long, value_enum)]
state: Option<DecisionStateFilter>,
/// Restrict to decisions whose `decided_on_branch` matches.
#[arg(long, value_name = "BRANCH")]
branch: Option<String>,
/// Output format.
#[arg(long, value_enum, default_value_t = DecisionsListFormat::Table)]
format: DecisionsListFormat,
},
/// Remove a recorded decision so the convention re-enters the review queue
/// on the next scan.
///
/// Lookup accepts either the full `description_hash` or an unambiguous
/// prefix of at least 4 characters. The matched decision is printed for
/// confirmation; pass `--yes` to skip the interactive prompt (useful for
/// scripts).
Forget {
/// Full `description_hash` or an unambiguous prefix (≥4 chars).
hash: String,
/// Skip the confirmation prompt and remove the decision unattended.
#[arg(long)]
yes: bool,
},
/// Export the project's decisions table to a JSON file (backup or share).
///
/// Writes the full project-wide decisions table as a pretty-printed JSON
/// array. The shape matches `seshat decisions list --format json` so a
/// round-trip back through `seshat decisions import` is lossless.
Export {
/// Output file path. Created (or overwritten) with the JSON array.
file: PathBuf,
},
/// Import a decisions JSON file produced by `seshat decisions export`.
///
/// Each row in the input file is UPSERTed into the project's decisions
/// table. On hash conflicts the row with the larger `decided_at` wins
/// silently; pass `--strict` to fail (no writes) on any conflict instead.
Import {
/// Input file path containing the decisions JSON array.
file: PathBuf,
/// Fail on any hash conflict instead of silently keeping the newer
/// decision (useful for CI / audit pipelines that want to surface
/// divergences before merging).
#[arg(long)]
strict: bool,
},
}
/// CLI-facing alias for [`DecisionState`] that derives [`ValueEnum`].
///
/// Kept separate from the storage enum so the storage crate stays independent
/// of `clap`. Convert with `DecisionState::from(filter)`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum DecisionStateFilter {
Approved,
Rejected,
Partial,
Recorded,
}
impl From<DecisionStateFilter> for DecisionState {
fn from(value: DecisionStateFilter) -> Self {
match value {
DecisionStateFilter::Approved => DecisionState::Approved,
DecisionStateFilter::Rejected => DecisionState::Rejected,
DecisionStateFilter::Partial => DecisionState::Partial,
DecisionStateFilter::Recorded => DecisionState::Recorded,
}
}
}
/// Output format for `seshat decisions list`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum DecisionsListFormat {
/// Human-readable aligned table (default).
Table,
/// JSON array of decision objects.
Json,
}