Skip to main content

contrail_memex/
lib.rs

1mod aliases;
2mod bundle;
3mod detect;
4mod explain;
5mod init;
6mod link;
7mod readers;
8mod render;
9mod search;
10mod share;
11mod sync;
12mod types;
13
14use anyhow::{Context, Result};
15use clap::{Parser, Subcommand};
16use std::path::PathBuf;
17use std::process::Command;
18
19#[derive(Parser)]
20#[command(name = "memex", about = "Self-managed context layer for coding agents")]
21struct Cli {
22    #[command(subcommand)]
23    command: Commands,
24}
25
26#[derive(Subcommand)]
27enum Commands {
28    /// Initialize .context/ in the current repo and wire up detected agents
29    Init,
30    /// Sync recent session transcripts from agent storage into .context/sessions/
31    Sync {
32        /// How many days of history to sync (default: 30)
33        #[arg(long, default_value_t = 30)]
34        days: u64,
35        /// Suppress output (for use in git hooks)
36        #[arg(long, default_value_t = false)]
37        quiet: bool,
38    },
39    /// Record a link between the current HEAD commit and active agent sessions
40    LinkCommit {
41        /// Suppress output (for use in git hooks)
42        #[arg(long, default_value_t = false)]
43        quiet: bool,
44    },
45    /// Show which agent sessions were active when a commit was made
46    Explain {
47        /// Commit SHA or prefix to look up
48        commit: String,
49    },
50    /// Greppable search across synced sessions + learnings
51    Search {
52        /// Literal text query (substring match, not regex)
53        query: String,
54        /// Only search session files modified in the last N days (learnings always searched)
55        #[arg(long, default_value_t = 30)]
56        days: u64,
57        /// Maximum number of matches to print (default: 200)
58        #[arg(long, default_value_t = 200)]
59        limit: usize,
60        /// Case-sensitive search (default: false)
61        #[arg(long, default_value_t = false)]
62        case_sensitive: bool,
63        /// Only print matching filenames (like `rg -l`)
64        #[arg(long, default_value_t = false)]
65        files: bool,
66    },
67    /// Encrypt sessions + learnings into .context/vault.age for sharing via git
68    Share {
69        /// Passphrase (required)
70        #[arg(long, conflicts_with = "passphrase_env")]
71        passphrase: Option<String>,
72        /// Name of environment variable containing passphrase
73        #[arg(long, value_name = "VAR", conflicts_with = "passphrase")]
74        passphrase_env: Option<String>,
75    },
76    /// Encrypt a single session transcript into a portable bundle under .context/bundles/
77    ShareSession {
78        /// Session filename under .context/sessions/ (e.g. 2026-02-10T12-00-00_codex-cli_abc123.md)
79        session: String,
80        /// Passphrase (required)
81        #[arg(long)]
82        passphrase: Option<String>,
83    },
84    /// Import a shared session bundle by ID (resolves from working tree first, then git history)
85    Import {
86        /// Bundle ID (the filename stem under .context/bundles/, without extension)
87        id: String,
88        /// Passphrase (required)
89        #[arg(long)]
90        passphrase: Option<String>,
91    },
92    /// Decrypt .context/vault.age back into sessions + learnings
93    Unlock {
94        /// Passphrase (required)
95        #[arg(long)]
96        passphrase: Option<String>,
97    },
98}
99
100pub fn run() -> Result<()> {
101    let cli = Cli::parse();
102    let repo_root = find_repo_root()?;
103
104    match cli.command {
105        Commands::Init => init::run_init(&repo_root),
106        Commands::Sync { days, quiet } => sync::run_sync(&repo_root, days, quiet),
107        Commands::LinkCommit { quiet } => link::run_link_commit(&repo_root, quiet),
108        Commands::Explain { commit } => explain::run_explain(&repo_root, &commit),
109        Commands::Search {
110            query,
111            days,
112            limit,
113            case_sensitive,
114            files,
115        } => search::run_search(&repo_root, &query, days, limit, case_sensitive, files),
116        Commands::Share {
117            passphrase,
118            passphrase_env,
119        } => share::run_share(&repo_root, passphrase, passphrase_env),
120        Commands::ShareSession {
121            session,
122            passphrase,
123        } => bundle::run_share_session(&repo_root, &session, passphrase),
124        Commands::Import { id, passphrase } => bundle::run_import(&repo_root, &id, passphrase),
125        Commands::Unlock { passphrase } => share::run_unlock(&repo_root, passphrase),
126    }
127}
128
129fn find_repo_root() -> Result<PathBuf> {
130    let output = Command::new("git")
131        .args(["rev-parse", "--show-toplevel"])
132        .output()
133        .context("failed to run git rev-parse")?;
134
135    if output.status.success() {
136        let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
137        Ok(PathBuf::from(path))
138    } else {
139        std::env::current_dir().context("failed to get current directory")
140    }
141}