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
mod aliases;
mod bundle;
mod detect;
mod explain;
mod init;
mod link;
mod readers;
mod render;
mod search;
mod share;
mod sync;
mod types;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use std::process::Command;
#[derive(Parser)]
#[command(name = "memex", about = "Self-managed context layer for coding agents")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Initialize .context/ in the current repo and wire up detected agents
Init,
/// Sync recent session transcripts from agent storage into .context/sessions/
Sync {
/// How many days of history to sync (default: 30)
#[arg(long, default_value_t = 30)]
days: u64,
/// Suppress output (for use in git hooks)
#[arg(long, default_value_t = false)]
quiet: bool,
},
/// Record a link between the current HEAD commit and active agent sessions
LinkCommit {
/// Suppress output (for use in git hooks)
#[arg(long, default_value_t = false)]
quiet: bool,
},
/// Show which agent sessions were active when a commit was made
Explain {
/// Commit SHA or prefix to look up
commit: String,
},
/// Greppable search across synced sessions + learnings
Search {
/// Literal text query (substring match, not regex)
query: String,
/// Only search session files modified in the last N days (learnings always searched)
#[arg(long, default_value_t = 30)]
days: u64,
/// Maximum number of matches to print (default: 200)
#[arg(long, default_value_t = 200)]
limit: usize,
/// Case-sensitive search (default: false)
#[arg(long, default_value_t = false)]
case_sensitive: bool,
/// Only print matching filenames (like `rg -l`)
#[arg(long, default_value_t = false)]
files: bool,
},
/// Encrypt sessions + learnings into .context/vault.age for sharing via git
Share {
/// Passphrase (required)
#[arg(long)]
passphrase: Option<String>,
},
/// Encrypt a single session transcript into a portable bundle under .context/bundles/
ShareSession {
/// Session filename under .context/sessions/ (e.g. 2026-02-10T12-00-00_codex-cli_abc123.md)
session: String,
/// Passphrase (required)
#[arg(long)]
passphrase: Option<String>,
},
/// Import a shared session bundle by ID (resolves from working tree first, then git history)
Import {
/// Bundle ID (the filename stem under .context/bundles/, without extension)
id: String,
/// Passphrase (required)
#[arg(long)]
passphrase: Option<String>,
},
/// Decrypt .context/vault.age back into sessions + learnings
Unlock {
/// Passphrase (required)
#[arg(long)]
passphrase: Option<String>,
},
}
fn main() -> Result<()> {
let cli = Cli::parse();
let repo_root = find_repo_root()?;
match cli.command {
Commands::Init => init::run_init(&repo_root),
Commands::Sync { days, quiet } => sync::run_sync(&repo_root, days, quiet),
Commands::LinkCommit { quiet } => link::run_link_commit(&repo_root, quiet),
Commands::Explain { commit } => explain::run_explain(&repo_root, &commit),
Commands::Search {
query,
days,
limit,
case_sensitive,
files,
} => search::run_search(&repo_root, &query, days, limit, case_sensitive, files),
Commands::Share { passphrase } => share::run_share(&repo_root, passphrase),
Commands::ShareSession {
session,
passphrase,
} => bundle::run_share_session(&repo_root, &session, passphrase),
Commands::Import { id, passphrase } => bundle::run_import(&repo_root, &id, passphrase),
Commands::Unlock { passphrase } => share::run_unlock(&repo_root, passphrase),
}
}
fn find_repo_root() -> Result<PathBuf> {
let output = Command::new("git")
.args(["rev-parse", "--show-toplevel"])
.output()
.context("failed to run git rev-parse")?;
if output.status.success() {
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(PathBuf::from(path))
} else {
// Fall back to current directory if not in a git repo
std::env::current_dir().context("failed to get current directory")
}
}