Skip to main content

sc/cli/
mod.rs

1//! CLI definitions using clap.
2
3use clap::{Args, Parser, Subcommand, ValueEnum};
4use std::path::PathBuf;
5
6/// Output format for list/query commands.
7#[derive(ValueEnum, Clone, Debug, Default, PartialEq, Eq)]
8pub enum OutputFormat {
9    /// Human-readable table (default)
10    #[default]
11    Table,
12    /// JSON (same as --json)
13    Json,
14    /// Comma-separated values
15    Csv,
16}
17
18pub mod commands;
19
20/// SaveContext CLI - The OS for AI coding agents
21#[derive(Parser, Debug)]
22#[command(name = "sc", author, version, about, long_about = None)]
23pub struct Cli {
24    #[command(subcommand)]
25    pub command: Commands,
26
27    /// Database path (default: ~/.savecontext/data/savecontext.db)
28    #[arg(long, global = true, env = "SC_DB")]
29    pub db: Option<PathBuf>,
30
31    /// Actor name for audit trail
32    #[arg(long, global = true, env = "SC_ACTOR")]
33    pub actor: Option<String>,
34
35    /// Active session ID (passed by MCP server)
36    #[arg(long, global = true, env = "SC_SESSION")]
37    pub session: Option<String>,
38
39    /// Output as JSON (for agent integration)
40    #[arg(long, alias = "robot", global = true)]
41    pub json: bool,
42
43    /// Output format (table, json, csv)
44    #[arg(long, value_enum, global = true, default_value_t)]
45    pub format: OutputFormat,
46
47    /// Output only the ID/key (for agent scripting)
48    #[arg(long, global = true)]
49    pub silent: bool,
50
51    /// Preview changes without writing to the database
52    #[arg(long, global = true)]
53    pub dry_run: bool,
54
55    /// Increase logging verbosity (-v, -vv)
56    #[arg(short, long, action = clap::ArgAction::Count, global = true)]
57    pub verbose: u8,
58
59    /// Quiet mode (no output except errors)
60    #[arg(short, long, global = true)]
61    pub quiet: bool,
62
63    /// Disable colored output
64    #[arg(long, global = true)]
65    pub no_color: bool,
66}
67
68#[derive(Subcommand, Debug)]
69pub enum Commands {
70    /// Initialize a SaveContext workspace
71    Init {
72        /// Use global location (~/.savecontext/)
73        #[arg(long)]
74        global: bool,
75
76        /// Overwrite existing database
77        #[arg(long)]
78        force: bool,
79    },
80
81    /// Print version information
82    Version,
83
84    /// Session management
85    Session {
86        #[command(subcommand)]
87        command: SessionCommands,
88    },
89
90    /// Show current session status
91    Status,
92
93    /// Save a context item
94    Save(SaveArgs),
95
96    /// Get/search context items
97    Get(GetArgs),
98
99    /// Delete a context item
100    Delete {
101        /// Key of the item to delete
102        key: String,
103    },
104
105    /// Update a context item
106    Update(UpdateArgs),
107
108    /// Tag context items
109    Tag {
110        #[command(subcommand)]
111        command: TagCommands,
112    },
113
114    /// Issue management
115    Issue {
116        #[command(subcommand)]
117        command: IssueCommands,
118    },
119
120    /// Checkpoint management
121    Checkpoint {
122        #[command(subcommand)]
123        command: CheckpointCommands,
124    },
125
126    /// Project memory (persistent across sessions)
127    Memory {
128        #[command(subcommand)]
129        command: MemoryCommands,
130    },
131
132    /// Sync with JSONL files
133    Sync {
134        #[command(subcommand)]
135        command: SyncCommands,
136    },
137
138    /// Project management
139    Project {
140        #[command(subcommand)]
141        command: ProjectCommands,
142    },
143
144    /// Plan management (PRDs, specs, feature docs)
145    Plan {
146        #[command(subcommand)]
147        command: PlanCommands,
148    },
149
150    /// Prepare context for compaction (auto-checkpoint + summary)
151    Compaction,
152
153    /// Generate context primer for agent injection
154    Prime {
155        /// Include Claude Code transcript summaries
156        #[arg(long)]
157        transcript: bool,
158
159        /// Maximum transcript entries to include
160        #[arg(long, default_value = "5")]
161        transcript_limit: usize,
162
163        /// Compact output for agent system prompt injection
164        #[arg(long)]
165        compact: bool,
166
167        /// Smart relevance-ranked context selection
168        #[arg(long)]
169        smart: bool,
170
171        /// Token budget for smart mode (default: 4000)
172        #[arg(long, default_value = "4000")]
173        budget: usize,
174
175        /// Topic query for semantic boosting in smart mode
176        #[arg(long)]
177        query: Option<String>,
178
179        /// Temporal decay half-life in days for smart mode (default: 14)
180        #[arg(long, default_value = "14")]
181        decay_days: u32,
182    },
183
184    /// Generate shell completions
185    Completions {
186        /// Shell to generate completions for
187        #[arg(value_enum)]
188        shell: Shell,
189    },
190
191    /// Embedding configuration and management
192    Embeddings {
193        #[command(subcommand)]
194        command: EmbeddingsCommands,
195    },
196
197    /// Install and manage skills, hooks, and status line
198    Skills {
199        #[command(subcommand)]
200        command: SkillsCommands,
201    },
202
203    /// Configuration management
204    Config {
205        #[command(subcommand)]
206        command: ConfigCommands,
207    },
208
209    /// Time tracking (billable hours)
210    Time {
211        #[command(subcommand)]
212        command: TimeCommands,
213    },
214
215    /// Run sc commands on a remote host via SSH
216    #[command(trailing_var_arg = true)]
217    Remote {
218        /// Arguments to pass to sc on the remote host
219        #[arg(num_args = 1..)]
220        args: Vec<String>,
221    },
222}
223
224/// Supported shells for completions.
225#[derive(clap::ValueEnum, Clone, Debug)]
226pub enum Shell {
227    Bash,
228    Zsh,
229    Fish,
230    PowerShell,
231    Elvish,
232}
233
234// ============================================================================
235// Session Commands
236// ============================================================================
237
238#[derive(Subcommand, Debug)]
239pub enum SessionCommands {
240    /// Start a new session
241    Start {
242        /// Session name
243        name: String,
244
245        /// Session description
246        #[arg(short, long)]
247        description: Option<String>,
248
249        /// Project path (defaults to current directory)
250        #[arg(short, long)]
251        project: Option<String>,
252
253        /// Channel name (auto-derived from git branch if not provided)
254        #[arg(long)]
255        channel: Option<String>,
256
257        /// Force create a new session instead of resuming existing one
258        #[arg(long)]
259        force_new: bool,
260    },
261
262    /// End current session
263    End,
264
265    /// Pause current session
266    Pause,
267
268    /// Resume a paused session
269    Resume {
270        /// Session ID to resume
271        id: String,
272    },
273
274    /// List sessions
275    List {
276        /// Filter by status (active, paused, completed, all)
277        #[arg(short, long, default_value = "active")]
278        status: String,
279
280        /// Maximum sessions to return
281        #[arg(short, long, default_value = "10")]
282        limit: usize,
283
284        /// Search sessions by name or description
285        #[arg(long)]
286        search: Option<String>,
287
288        /// Filter by project path
289        #[arg(short, long)]
290        project: Option<String>,
291
292        /// Show sessions from all projects (ignore project filter)
293        #[arg(long)]
294        all_projects: bool,
295
296        /// Include completed sessions (when status is not 'all' or 'completed')
297        #[arg(long)]
298        include_completed: bool,
299    },
300
301    /// Switch to a different session
302    Switch {
303        /// Session ID to switch to
304        id: String,
305    },
306
307    /// Rename current session
308    Rename {
309        /// New session name
310        name: String,
311    },
312
313    /// Delete a session permanently
314    Delete {
315        /// Session ID
316        id: String,
317
318        /// Skip confirmation and delete
319        #[arg(short, long)]
320        force: bool,
321    },
322
323    /// Add a project path to a session
324    AddPath {
325        /// Session ID (uses current active session if not specified)
326        #[arg(short, long)]
327        id: Option<String>,
328
329        /// Project path to add (defaults to current directory)
330        path: Option<String>,
331    },
332
333    /// Remove a project path from a session
334    RemovePath {
335        /// Session ID (uses current active session if not specified)
336        #[arg(short, long)]
337        id: Option<String>,
338
339        /// Project path to remove
340        path: String,
341    },
342}
343
344// ============================================================================
345// Context Item Commands (Save/Get)
346// ============================================================================
347
348#[derive(Args, Debug)]
349pub struct SaveArgs {
350    /// Unique key for this context item
351    pub key: String,
352
353    /// Value to save
354    pub value: String,
355
356    /// Category (reminder, decision, progress, note)
357    #[arg(short, long, default_value = "note")]
358    pub category: String,
359
360    /// Priority (high, normal, low)
361    #[arg(short, long, default_value = "normal")]
362    pub priority: String,
363}
364
365#[derive(Args, Debug, Default)]
366pub struct GetArgs {
367    /// Search query (smart semantic search when embeddings enabled, keyword fallback)
368    #[arg(short = 's', long)]
369    pub query: Option<String>,
370
371    /// Get by exact key
372    #[arg(short, long)]
373    pub key: Option<String>,
374
375    /// Filter by category
376    #[arg(short, long)]
377    pub category: Option<String>,
378
379    /// Filter by priority
380    #[arg(short = 'P', long)]
381    pub priority: Option<String>,
382
383    /// Search across all sessions (not just current)
384    #[arg(long)]
385    pub search_all_sessions: bool,
386
387    /// Semantic search threshold (0.0-1.0, lower = more results)
388    #[arg(long)]
389    pub threshold: Option<f64>,
390
391    /// Semantic search mode (fast, quality, tiered)
392    ///
393    /// - fast: Instant results using Model2Vec (lower accuracy)
394    /// - quality: Slower but more accurate results using Ollama/HuggingFace
395    /// - tiered: Fast candidates, quality re-ranking (default)
396    #[arg(long, value_parser = parse_search_mode)]
397    pub search_mode: Option<crate::embeddings::SearchMode>,
398
399    /// Pagination offset
400    #[arg(long)]
401    pub offset: Option<usize>,
402
403    /// Maximum items to return
404    #[arg(short, long, default_value = "50")]
405    pub limit: usize,
406}
407
408/// Parse search mode from string
409fn parse_search_mode(s: &str) -> std::result::Result<crate::embeddings::SearchMode, String> {
410    s.parse()
411}
412
413#[derive(Args, Debug)]
414pub struct UpdateArgs {
415    /// Key of the item to update
416    pub key: String,
417
418    /// New value
419    #[arg(long)]
420    pub value: Option<String>,
421
422    /// New category (reminder, decision, progress, note)
423    #[arg(short, long)]
424    pub category: Option<String>,
425
426    /// New priority (high, normal, low)
427    #[arg(short, long)]
428    pub priority: Option<String>,
429
430    /// New channel
431    #[arg(long)]
432    pub channel: Option<String>,
433}
434
435#[derive(Subcommand, Debug)]
436pub enum TagCommands {
437    /// Add tags to context items
438    Add {
439        /// Key of the item to tag
440        key: String,
441
442        /// Tags to add (comma-separated or multiple --tag flags)
443        #[arg(short, long, value_delimiter = ',', required = true)]
444        tags: Vec<String>,
445    },
446
447    /// Remove tags from context items
448    Remove {
449        /// Key of the item to untag
450        key: String,
451
452        /// Tags to remove (comma-separated or multiple --tag flags)
453        #[arg(short, long, value_delimiter = ',', required = true)]
454        tags: Vec<String>,
455    },
456}
457
458// ============================================================================
459// Issue Commands
460// ============================================================================
461
462#[derive(Subcommand, Debug)]
463pub enum IssueCommands {
464    /// Create a new issue
465    Create(IssueCreateArgs),
466
467    /// List issues
468    List(IssueListArgs),
469
470    /// Show issue details
471    Show {
472        /// Issue ID (short or full)
473        id: String,
474    },
475
476    /// Update an issue
477    Update(IssueUpdateArgs),
478
479    /// Mark issue(s) as complete
480    Complete {
481        /// Issue IDs (one or more)
482        ids: Vec<String>,
483
484        /// Reason for closing
485        #[arg(short = 'r', long)]
486        reason: Option<String>,
487    },
488
489    /// Claim issue(s) (assign to self)
490    Claim {
491        /// Issue IDs (one or more)
492        ids: Vec<String>,
493    },
494
495    /// Release issue(s)
496    Release {
497        /// Issue IDs (one or more)
498        ids: Vec<String>,
499    },
500
501    /// Delete issue(s)
502    Delete {
503        /// Issue IDs (one or more)
504        ids: Vec<String>,
505    },
506
507    /// Manage issue labels
508    Label {
509        #[command(subcommand)]
510        command: IssueLabelCommands,
511    },
512
513    /// Manage issue dependencies
514    Dep {
515        #[command(subcommand)]
516        command: IssueDepCommands,
517    },
518
519    /// Clone an issue
520    Clone {
521        /// Issue ID to clone
522        id: String,
523
524        /// New title (defaults to "Copy of <original>")
525        #[arg(short, long)]
526        title: Option<String>,
527    },
528
529    /// Mark issue as duplicate of another
530    Duplicate {
531        /// Issue ID to mark as duplicate
532        id: String,
533
534        /// Issue ID this is a duplicate of
535        #[arg(long)]
536        of: String,
537    },
538
539    /// List issues ready to work on
540    Ready {
541        /// Maximum issues to return
542        #[arg(short, long, default_value = "10")]
543        limit: usize,
544    },
545
546    /// Get next block of issues and claim them
547    NextBlock {
548        /// Number of issues to claim
549        #[arg(short, long, default_value = "3")]
550        count: usize,
551    },
552
553    /// Create multiple issues at once with dependencies
554    Batch {
555        /// JSON input containing issues array, dependencies, and optional planId
556        #[arg(long)]
557        json_input: String,
558    },
559
560    /// Count issues grouped by a field
561    Count {
562        /// Group by: status, type, priority, assignee
563        #[arg(short, long, default_value = "status")]
564        group_by: String,
565    },
566
567    /// List stale issues (not updated recently)
568    Stale {
569        /// Issues not updated in this many days
570        #[arg(short, long, default_value = "7")]
571        days: u64,
572
573        /// Maximum issues to return
574        #[arg(short, long, default_value = "50")]
575        limit: usize,
576    },
577
578    /// List blocked issues with their blockers
579    Blocked {
580        /// Maximum issues to return
581        #[arg(short, long, default_value = "50")]
582        limit: usize,
583    },
584}
585
586#[derive(Subcommand, Debug)]
587pub enum IssueLabelCommands {
588    /// Add labels to an issue
589    Add {
590        /// Issue ID
591        id: String,
592
593        /// Labels to add (comma-separated)
594        #[arg(short, long, value_delimiter = ',', required = true)]
595        labels: Vec<String>,
596    },
597
598    /// Remove labels from an issue
599    Remove {
600        /// Issue ID
601        id: String,
602
603        /// Labels to remove (comma-separated)
604        #[arg(short, long, value_delimiter = ',', required = true)]
605        labels: Vec<String>,
606    },
607}
608
609#[derive(Subcommand, Debug)]
610pub enum IssueDepCommands {
611    /// Add a dependency to an issue
612    Add {
613        /// Issue ID
614        id: String,
615
616        /// ID of issue this depends on
617        #[arg(long)]
618        depends_on: String,
619
620        /// Dependency type (blocks, related, parent-child, discovered-from)
621        #[arg(short = 't', long, default_value = "blocks")]
622        dep_type: String,
623    },
624
625    /// Remove a dependency from an issue
626    Remove {
627        /// Issue ID
628        id: String,
629
630        /// ID of issue to remove dependency on
631        #[arg(long)]
632        depends_on: String,
633    },
634
635    /// Show dependency tree for an issue or all epics
636    Tree {
637        /// Root issue ID (omit for all epics)
638        id: Option<String>,
639    },
640}
641
642#[derive(Args, Debug)]
643pub struct IssueCreateArgs {
644    /// Issue title
645    pub title: String,
646
647    /// Issue description
648    #[arg(short, long)]
649    pub description: Option<String>,
650
651    /// Implementation details or notes
652    #[arg(long)]
653    pub details: Option<String>,
654
655    /// Issue type (task, bug, feature, epic, chore)
656    #[arg(short = 't', long, default_value = "task")]
657    pub issue_type: String,
658
659    /// Priority (0=lowest to 4=critical)
660    #[arg(short, long, default_value = "2")]
661    pub priority: i32,
662
663    /// Parent issue ID (for subtasks)
664    #[arg(long)]
665    pub parent: Option<String>,
666
667    /// Link issue to a Plan (PRD/spec)
668    #[arg(long)]
669    pub plan_id: Option<String>,
670
671    /// Labels (-l bug -l security or -l bug,security)
672    #[arg(short, long, value_delimiter = ',')]
673    pub labels: Option<Vec<String>>,
674
675    /// Import issues from a JSONL file (one JSON object per line)
676    #[arg(short, long)]
677    pub file: Option<PathBuf>,
678}
679
680#[derive(Args, Debug, Default)]
681pub struct IssueListArgs {
682    /// Filter by specific issue ID (short or full)
683    #[arg(long)]
684    pub id: Option<String>,
685
686    /// Filter by status (backlog, open, in_progress, blocked, closed, deferred, all)
687    #[arg(short, long, default_value = "open")]
688    pub status: String,
689
690    /// Filter by exact priority (0-4)
691    #[arg(short, long)]
692    pub priority: Option<i32>,
693
694    /// Filter by minimum priority
695    #[arg(long)]
696    pub priority_min: Option<i32>,
697
698    /// Filter by maximum priority
699    #[arg(long)]
700    pub priority_max: Option<i32>,
701
702    /// Filter by type
703    #[arg(short = 't', long)]
704    pub issue_type: Option<String>,
705
706    /// Filter by labels (all must match, comma-separated)
707    #[arg(long, value_delimiter = ',')]
708    pub labels: Option<Vec<String>>,
709
710    /// Filter by labels (any must match, comma-separated)
711    #[arg(long, value_delimiter = ',')]
712    pub labels_any: Option<Vec<String>>,
713
714    /// Filter by parent issue ID
715    #[arg(long)]
716    pub parent: Option<String>,
717
718    /// Filter by plan ID
719    #[arg(long)]
720    pub plan: Option<String>,
721
722    /// Filter issues with subtasks
723    #[arg(long)]
724    pub has_subtasks: bool,
725
726    /// Filter issues without subtasks
727    #[arg(long)]
728    pub no_subtasks: bool,
729
730    /// Filter issues with dependencies
731    #[arg(long)]
732    pub has_deps: bool,
733
734    /// Filter issues without dependencies
735    #[arg(long)]
736    pub no_deps: bool,
737
738    /// Sort by field (priority, createdAt, updatedAt)
739    #[arg(long, default_value = "createdAt")]
740    pub sort: String,
741
742    /// Sort order (asc, desc)
743    #[arg(long, default_value = "desc")]
744    pub order: String,
745
746    /// Filter by issues created in last N days
747    #[arg(long)]
748    pub created_days: Option<i64>,
749
750    /// Filter by issues created in last N hours
751    #[arg(long)]
752    pub created_hours: Option<i64>,
753
754    /// Filter by issues updated in last N days
755    #[arg(long)]
756    pub updated_days: Option<i64>,
757
758    /// Filter by issues updated in last N hours
759    #[arg(long)]
760    pub updated_hours: Option<i64>,
761
762    /// Search in title/description
763    #[arg(long)]
764    pub search: Option<String>,
765
766    /// Filter by assignee
767    #[arg(long)]
768    pub assignee: Option<String>,
769
770    /// Search across all projects
771    #[arg(long)]
772    pub all_projects: bool,
773
774    /// Maximum issues to return
775    #[arg(short, long, default_value = "50")]
776    pub limit: usize,
777}
778
779#[derive(Args, Debug)]
780pub struct IssueUpdateArgs {
781    /// Issue ID
782    pub id: String,
783
784    /// New title
785    #[arg(long)]
786    pub title: Option<String>,
787
788    /// New description
789    #[arg(short, long)]
790    pub description: Option<String>,
791
792    /// New details
793    #[arg(long)]
794    pub details: Option<String>,
795
796    /// New status
797    #[arg(short, long)]
798    pub status: Option<String>,
799
800    /// New priority
801    #[arg(short, long)]
802    pub priority: Option<i32>,
803
804    /// New type
805    #[arg(short = 't', long)]
806    pub issue_type: Option<String>,
807
808    /// New parent issue ID
809    #[arg(long)]
810    pub parent: Option<String>,
811
812    /// New plan ID
813    #[arg(long)]
814    pub plan: Option<String>,
815}
816
817// ============================================================================
818// Checkpoint Commands
819// ============================================================================
820
821#[derive(Subcommand, Debug)]
822pub enum CheckpointCommands {
823    /// Create a checkpoint
824    Create {
825        /// Checkpoint name
826        name: String,
827
828        /// Description
829        #[arg(short, long)]
830        description: Option<String>,
831
832        /// Include git status
833        #[arg(long)]
834        include_git: bool,
835    },
836
837    /// List checkpoints
838    List {
839        /// Search checkpoints by name or description
840        #[arg(short, long)]
841        search: Option<String>,
842
843        /// Filter by session ID
844        #[arg(long)]
845        session: Option<String>,
846
847        /// Filter by project path
848        #[arg(long)]
849        project: Option<String>,
850
851        /// Include checkpoints from all projects
852        #[arg(long)]
853        all_projects: bool,
854
855        /// Maximum checkpoints to return
856        #[arg(short, long, default_value = "20")]
857        limit: usize,
858
859        /// Pagination offset
860        #[arg(long)]
861        offset: Option<usize>,
862    },
863
864    /// Show checkpoint details
865    Show {
866        /// Checkpoint ID
867        id: String,
868    },
869
870    /// Restore from checkpoint
871    Restore {
872        /// Checkpoint ID
873        id: String,
874
875        /// Only restore items in these categories (comma-separated)
876        #[arg(long, value_delimiter = ',')]
877        categories: Option<Vec<String>>,
878
879        /// Only restore items with these tags (comma-separated)
880        #[arg(long, value_delimiter = ',')]
881        tags: Option<Vec<String>>,
882    },
883
884    /// Delete a checkpoint
885    Delete {
886        /// Checkpoint ID
887        id: String,
888    },
889
890    /// Add items to an existing checkpoint
891    AddItems {
892        /// Checkpoint ID
893        id: String,
894
895        /// Context item keys to add (comma-separated)
896        #[arg(short, long, value_delimiter = ',', required = true)]
897        keys: Vec<String>,
898    },
899
900    /// Remove items from a checkpoint
901    RemoveItems {
902        /// Checkpoint ID
903        id: String,
904
905        /// Context item keys to remove (comma-separated)
906        #[arg(short, long, value_delimiter = ',', required = true)]
907        keys: Vec<String>,
908    },
909
910    /// List items in a checkpoint
911    Items {
912        /// Checkpoint ID
913        id: String,
914    },
915}
916
917// ============================================================================
918// Memory Commands (Project-level persistent storage)
919// ============================================================================
920
921#[derive(Subcommand, Debug)]
922pub enum MemoryCommands {
923    /// Save a memory item
924    Save {
925        /// Key
926        key: String,
927
928        /// Value
929        value: String,
930
931        /// Category (command, config, note)
932        #[arg(short, long, default_value = "command")]
933        category: String,
934    },
935
936    /// Get a memory item
937    Get {
938        /// Key
939        key: String,
940    },
941
942    /// List memory items
943    List {
944        /// Filter by category
945        #[arg(short, long)]
946        category: Option<String>,
947    },
948
949    /// Delete a memory item
950    Delete {
951        /// Key
952        key: String,
953    },
954}
955
956// ============================================================================
957// Sync Commands
958// ============================================================================
959
960#[derive(Subcommand, Debug)]
961pub enum SyncCommands {
962    /// Export to JSONL
963    Export {
964        /// Force export even if JSONL is newer
965        #[arg(long)]
966        force: bool,
967    },
968
969    /// Import from JSONL
970    Import {
971        /// Force import even with conflicts
972        #[arg(long)]
973        force: bool,
974    },
975
976    /// Show sync status
977    Status,
978
979    /// Push local JSONL exports to remote host via SCP
980    Push {
981        /// Force export before push
982        #[arg(long)]
983        force: bool,
984
985        /// Remote project path (defaults to local CWD path)
986        #[arg(long)]
987        remote_path: Option<String>,
988
989        /// Full database sync (SQLite backup) instead of JSONL
990        #[arg(long)]
991        full: bool,
992    },
993
994    /// Pull JSONL exports from remote host via SCP
995    Pull {
996        /// Force import after pull
997        #[arg(long)]
998        force: bool,
999
1000        /// Remote project path (defaults to local CWD path)
1001        #[arg(long)]
1002        remote_path: Option<String>,
1003
1004        /// Full database sync (SQLite backup) instead of JSONL
1005        #[arg(long)]
1006        full: bool,
1007    },
1008
1009    /// Create a local database backup (used by full sync)
1010    Backup {
1011        /// Output path for the backup file
1012        #[arg(long)]
1013        output: Option<String>,
1014    },
1015}
1016
1017// ============================================================================
1018// Project Commands
1019// ============================================================================
1020
1021#[derive(Subcommand, Debug)]
1022pub enum ProjectCommands {
1023    /// Create a new project
1024    Create(ProjectCreateArgs),
1025
1026    /// List all projects
1027    List {
1028        /// Include session count for each project
1029        #[arg(long)]
1030        session_count: bool,
1031
1032        /// Maximum projects to return
1033        #[arg(short, long, default_value = "50")]
1034        limit: usize,
1035    },
1036
1037    /// Show project details
1038    Show {
1039        /// Project ID or path
1040        id: String,
1041    },
1042
1043    /// Update a project
1044    Update(ProjectUpdateArgs),
1045
1046    /// Delete a project
1047    Delete {
1048        /// Project ID or path
1049        id: String,
1050
1051        /// Skip confirmation and delete
1052        #[arg(short, long)]
1053        force: bool,
1054    },
1055}
1056
1057#[derive(Args, Debug)]
1058pub struct ProjectCreateArgs {
1059    /// Project path (defaults to current directory)
1060    pub path: Option<String>,
1061
1062    /// Project name (defaults to directory name)
1063    #[arg(short, long)]
1064    pub name: Option<String>,
1065
1066    /// Project description
1067    #[arg(short, long)]
1068    pub description: Option<String>,
1069
1070    /// Issue ID prefix (e.g., "SC" creates SC-1, SC-2)
1071    #[arg(short = 'p', long)]
1072    pub issue_prefix: Option<String>,
1073}
1074
1075#[derive(Args, Debug)]
1076pub struct ProjectUpdateArgs {
1077    /// Project ID or path
1078    pub id: String,
1079
1080    /// New project name
1081    #[arg(short, long)]
1082    pub name: Option<String>,
1083
1084    /// New description
1085    #[arg(short, long)]
1086    pub description: Option<String>,
1087
1088    /// New issue ID prefix
1089    #[arg(short = 'p', long)]
1090    pub issue_prefix: Option<String>,
1091}
1092
1093// ============================================================================
1094// Plan Commands
1095// ============================================================================
1096
1097#[derive(Subcommand, Debug)]
1098pub enum PlanCommands {
1099    /// Create a new plan
1100    Create(PlanCreateArgs),
1101
1102    /// List plans
1103    List {
1104        /// Filter by status (draft, active, completed, all)
1105        #[arg(short, long, default_value = "active")]
1106        status: String,
1107
1108        /// Maximum plans to return
1109        #[arg(short, long, default_value = "50")]
1110        limit: usize,
1111
1112        /// Filter by session ID (use "current" for active TTY session)
1113        #[arg(long)]
1114        session: Option<String>,
1115    },
1116
1117    /// Show plan details
1118    Show {
1119        /// Plan ID
1120        id: String,
1121    },
1122
1123    /// Update a plan
1124    Update(PlanUpdateArgs),
1125
1126    /// Capture a plan from an AI coding agent's plan file
1127    Capture {
1128        /// Only look in a specific agent's directory (claude, gemini, opencode, cursor)
1129        #[arg(long)]
1130        agent: Option<String>,
1131
1132        /// Max age in minutes (default: 30)
1133        #[arg(long, default_value = "30")]
1134        max_age: u64,
1135
1136        /// Explicit plan file path (skip discovery)
1137        #[arg(long)]
1138        file: Option<PathBuf>,
1139    },
1140}
1141
1142#[derive(Args, Debug)]
1143pub struct PlanCreateArgs {
1144    /// Plan title
1145    pub title: String,
1146
1147    /// Plan content (markdown PRD/spec)
1148    #[arg(short, long)]
1149    pub content: Option<String>,
1150
1151    /// Plan status (draft, active, completed)
1152    #[arg(short, long, default_value = "active")]
1153    pub status: String,
1154
1155    /// Success criteria
1156    #[arg(long)]
1157    pub success_criteria: Option<String>,
1158
1159    /// Bind to a specific session (default: auto-resolve from TTY)
1160    #[arg(long)]
1161    pub session: Option<String>,
1162}
1163
1164#[derive(Args, Debug)]
1165pub struct PlanUpdateArgs {
1166    /// Plan ID
1167    pub id: String,
1168
1169    /// New title
1170    #[arg(long)]
1171    pub title: Option<String>,
1172
1173    /// New content
1174    #[arg(short, long)]
1175    pub content: Option<String>,
1176
1177    /// New status (draft, active, completed)
1178    #[arg(short, long)]
1179    pub status: Option<String>,
1180
1181    /// New success criteria
1182    #[arg(long)]
1183    pub success_criteria: Option<String>,
1184}
1185
1186// ============================================================================
1187// Embeddings Commands
1188// ============================================================================
1189
1190#[derive(Subcommand, Debug, Clone)]
1191pub enum EmbeddingsCommands {
1192    /// Show embeddings status and configuration
1193    Status,
1194
1195    /// Configure embedding provider
1196    Configure {
1197        /// Provider (ollama, huggingface)
1198        #[arg(short, long)]
1199        provider: Option<String>,
1200
1201        /// Enable embeddings
1202        #[arg(long)]
1203        enable: bool,
1204
1205        /// Disable embeddings
1206        #[arg(long)]
1207        disable: bool,
1208
1209        /// Model to use (provider-specific)
1210        #[arg(short, long)]
1211        model: Option<String>,
1212
1213        /// API endpoint (for custom servers)
1214        #[arg(long)]
1215        endpoint: Option<String>,
1216
1217        /// API token (for HuggingFace)
1218        #[arg(long)]
1219        token: Option<String>,
1220    },
1221
1222    /// Backfill embeddings for existing context items
1223    Backfill {
1224        /// Maximum items to process
1225        #[arg(short, long)]
1226        limit: Option<usize>,
1227
1228        /// Session ID to backfill (defaults to current)
1229        #[arg(short, long)]
1230        session: Option<String>,
1231
1232        /// Force regeneration of existing embeddings
1233        #[arg(long)]
1234        force: bool,
1235    },
1236
1237    /// Test embedding provider connectivity
1238    Test {
1239        /// Text to generate test embedding for
1240        #[arg(default_value = "Hello world")]
1241        text: String,
1242    },
1243
1244    /// Process pending embeddings in background (internal use)
1245    #[command(hide = true)]
1246    ProcessPending {
1247        /// Maximum items to process
1248        #[arg(short, long, default_value = "10")]
1249        limit: usize,
1250
1251        /// Run silently (no output)
1252        #[arg(long)]
1253        quiet: bool,
1254    },
1255
1256    /// Upgrade items with fast embeddings to quality embeddings
1257    ///
1258    /// Items saved with the 2-tier system get instant fast embeddings (Model2Vec).
1259    /// This command generates higher-quality embeddings (Ollama/HuggingFace)
1260    /// for items that only have fast embeddings.
1261    UpgradeQuality {
1262        /// Maximum items to process
1263        #[arg(short, long)]
1264        limit: Option<usize>,
1265
1266        /// Session ID to upgrade (defaults to all sessions)
1267        #[arg(short, long)]
1268        session: Option<String>,
1269    },
1270}
1271
1272// ============================================================================
1273// Skills Commands
1274// ============================================================================
1275
1276#[derive(Subcommand, Debug)]
1277pub enum SkillsCommands {
1278    /// Download and install skills, hooks, and status line
1279    Install {
1280        /// Target tool (claude-code, codex, gemini). Auto-detects if omitted.
1281        #[arg(long)]
1282        tool: Option<String>,
1283
1284        /// Skill mode: cli, mcp, or both (default: both)
1285        #[arg(long, default_value = "both")]
1286        mode: String,
1287
1288        /// Custom skills directory (overrides auto-detected path)
1289        #[arg(long)]
1290        path: Option<PathBuf>,
1291    },
1292
1293    /// Show installed skills and versions
1294    Status,
1295
1296    /// Re-download and install latest skills
1297    Update {
1298        /// Target tool (claude-code, codex, gemini). Updates all if omitted.
1299        #[arg(long)]
1300        tool: Option<String>,
1301    },
1302}
1303
1304// ============================================================================
1305// Config Commands
1306// ============================================================================
1307
1308#[derive(Subcommand, Debug)]
1309pub enum ConfigCommands {
1310    /// Remote host configuration
1311    Remote {
1312        #[command(subcommand)]
1313        command: ConfigRemoteCommands,
1314    },
1315}
1316
1317#[derive(Subcommand, Debug)]
1318pub enum ConfigRemoteCommands {
1319    /// Set remote host configuration
1320    Set(RemoteSetArgs),
1321
1322    /// Show current remote configuration
1323    Show,
1324
1325    /// Remove remote configuration
1326    Remove,
1327}
1328
1329#[derive(Args, Debug)]
1330pub struct RemoteSetArgs {
1331    /// Remote hostname or IP
1332    #[arg(long)]
1333    pub host: String,
1334
1335    /// SSH username
1336    #[arg(long)]
1337    pub user: String,
1338
1339    /// SSH port (default: 22)
1340    #[arg(long, default_value = "22")]
1341    pub port: u16,
1342
1343    /// Path to SSH identity file (private key)
1344    #[arg(long)]
1345    pub identity_file: Option<String>,
1346
1347    /// Path to sc binary on remote host (default: sc)
1348    #[arg(long, default_value = "sc")]
1349    pub remote_sc_path: Option<String>,
1350
1351    /// Default remote project path
1352    #[arg(long)]
1353    pub remote_project_path: Option<String>,
1354
1355    /// Path to SaveContext database on remote host
1356    #[arg(long)]
1357    pub remote_db_path: Option<String>,
1358}
1359
1360// ============================================================================
1361// Time Tracking Commands
1362// ============================================================================
1363
1364#[derive(Subcommand, Debug)]
1365pub enum TimeCommands {
1366    /// Log hours worked
1367    Log(TimeLogArgs),
1368
1369    /// List time entries
1370    List(TimeListArgs),
1371
1372    /// Show grouped summary with subtotals
1373    Summary {
1374        /// Filter by billing period
1375        #[arg(long)]
1376        period: Option<String>,
1377
1378        /// Group by: period (default), date, issue, status
1379        #[arg(long, default_value = "period")]
1380        group_by: String,
1381
1382        /// Filter by status (logged, reviewed, invoiced)
1383        #[arg(long)]
1384        status: Option<String>,
1385    },
1386
1387    /// Show total hours
1388    Total {
1389        /// Filter by billing period
1390        #[arg(long)]
1391        period: Option<String>,
1392
1393        /// Filter by status
1394        #[arg(long)]
1395        status: Option<String>,
1396    },
1397
1398    /// Update a time entry
1399    Update(TimeUpdateArgs),
1400
1401    /// Delete a time entry
1402    Delete {
1403        /// Time entry ID (full or short, e.g. TE-a1b2)
1404        id: String,
1405    },
1406
1407    /// Mark entries as invoiced for a billing period
1408    Invoice {
1409        /// Billing period to invoice
1410        #[arg(long)]
1411        period: String,
1412
1413        /// Source status to transition from (default: logged)
1414        #[arg(long, default_value = "logged")]
1415        from: String,
1416    },
1417}
1418
1419#[derive(Args, Debug)]
1420pub struct TimeLogArgs {
1421    /// Hours worked (e.g. 4, 1.5, 0.25)
1422    pub hours: f64,
1423
1424    /// Description of work performed
1425    pub description: String,
1426
1427    /// Link to an issue (short ID like SC-a1b2)
1428    #[arg(long)]
1429    pub issue: Option<String>,
1430
1431    /// Billing period identifier (e.g. GL-ABG-2026-001)
1432    #[arg(long)]
1433    pub period: Option<String>,
1434
1435    /// Work date (YYYY-MM-DD, defaults to today)
1436    #[arg(long)]
1437    pub date: Option<String>,
1438}
1439
1440#[derive(Args, Debug, Default)]
1441pub struct TimeListArgs {
1442    /// Filter by billing period
1443    #[arg(long)]
1444    pub period: Option<String>,
1445
1446    /// Filter by status (logged, reviewed, invoiced)
1447    #[arg(long)]
1448    pub status: Option<String>,
1449
1450    /// Filter by issue ID
1451    #[arg(long)]
1452    pub issue: Option<String>,
1453
1454    /// Filter from date (YYYY-MM-DD)
1455    #[arg(long)]
1456    pub from: Option<String>,
1457
1458    /// Filter to date (YYYY-MM-DD)
1459    #[arg(long)]
1460    pub to: Option<String>,
1461
1462    /// Maximum entries to return
1463    #[arg(short, long, default_value = "200")]
1464    pub limit: u32,
1465}
1466
1467#[derive(Args, Debug)]
1468pub struct TimeUpdateArgs {
1469    /// Time entry ID (full or short)
1470    pub id: String,
1471
1472    /// New hours
1473    #[arg(long)]
1474    pub hours: Option<f64>,
1475
1476    /// New description
1477    #[arg(long)]
1478    pub description: Option<String>,
1479
1480    /// New billing period
1481    #[arg(long)]
1482    pub period: Option<String>,
1483
1484    /// New issue link
1485    #[arg(long)]
1486    pub issue: Option<String>,
1487
1488    /// New work date
1489    #[arg(long)]
1490    pub date: Option<String>,
1491
1492    /// New status (logged, reviewed, invoiced)
1493    #[arg(long)]
1494    pub status: Option<String>,
1495}