Skip to content

Commit 568b1b1

Browse files
committed
feat(cli): handle memory commands
1 parent 0e37094 commit 568b1b1

6 files changed

Lines changed: 242 additions & 0 deletions

File tree

codex-rs/Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/cli/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ codex-login = { path = "../login" }
2727
codex-mcp-server = { path = "../mcp-server" }
2828
codex-protocol = { path = "../protocol" }
2929
codex-tui = { path = "../tui" }
30+
codex-memory = { path = "../memory", features = ["sqlite"] }
31+
chrono = "0.4"
32+
uuid = { version = "1", features = ["v4"] }
3033
serde_json = "1"
3134
tokio = { version = "1", features = [
3235
"io-std",
@@ -38,3 +41,6 @@ tokio = { version = "1", features = [
3841
tracing = "0.1.41"
3942
tracing-subscriber = "0.3.19"
4043
codex-protocol-ts = { path = "../protocol-ts" }
44+
45+
[dev-dependencies]
46+
tempfile = "3"

codex-rs/cli/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub mod debug_sandbox;
22
mod exit_status;
33
pub mod login;
4+
pub mod memory;
45
pub mod proto;
56

67
use clap::Parser;

codex-rs/cli/src/main.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use codex_tui::Cli as TuiCli;
1818
use std::path::PathBuf;
1919

2020
use crate::proto::ProtoCli;
21+
use codex_cli::memory::MemoryCli;
2122

2223
/// Codex CLI
2324
///
@@ -73,6 +74,9 @@ enum Subcommand {
7374
#[clap(visible_alias = "a")]
7475
Apply(ApplyCommand),
7576

77+
/// Manage persistent memory items.
78+
Memory(MemoryCli),
79+
7680
/// Internal: generate TypeScript protocol bindings.
7781
#[clap(hide = true)]
7882
GenerateTs(GenerateTsCommand),
@@ -209,6 +213,9 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
209213
prepend_config_flags(&mut apply_cli.config_overrides, cli.config_overrides);
210214
run_apply_command(apply_cli, None).await?;
211215
}
216+
Some(Subcommand::Memory(memory_cli)) => {
217+
codex_cli::memory::run(memory_cli)?;
218+
}
212219
Some(Subcommand::GenerateTs(gen_cli)) => {
213220
codex_protocol_ts::generate_ts(&gen_cli.out_dir, gen_cli.prettier.as_deref())?;
214221
}

codex-rs/cli/src/memory.rs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
use chrono::Utc;
2+
use clap::Parser;
3+
use codex_memory::factory;
4+
use codex_memory::types::{Counters, Kind, MemoryItem, RelevanceHints, Scope, Status};
5+
use std::path::PathBuf;
6+
use uuid::Uuid;
7+
8+
/// CLI for memory management commands.
9+
#[derive(Debug, Parser)]
10+
pub struct MemoryCli {
11+
#[command(subcommand)]
12+
pub cmd: MemoryCommand,
13+
}
14+
15+
/// Memory subcommands.
16+
#[derive(Debug, clap::Subcommand)]
17+
pub enum MemoryCommand {
18+
/// Add a new memory item with given content.
19+
Add { content: String },
20+
/// List memory items.
21+
List,
22+
/// Edit an existing memory item.
23+
Edit { id: String, content: String },
24+
/// Remove a memory item by id.
25+
Rm { id: String },
26+
/// Archive a memory item.
27+
Archive { id: String },
28+
/// Unarchive a memory item.
29+
Unarchive { id: String },
30+
/// Export memory items to stdout.
31+
Export,
32+
/// Import memory items from stdin.
33+
Import,
34+
/// Migrate a JSONL file to a SQLite database.
35+
Migrate { jsonl: PathBuf, sqlite: PathBuf },
36+
/// Show basic statistics about stored memories.
37+
Stats,
38+
/// Recall memories for a given prompt.
39+
Recall {
40+
#[arg(long = "for")]
41+
query: String,
42+
},
43+
}
44+
45+
/// Execute the memory command.
46+
pub fn run(cli: MemoryCli) -> anyhow::Result<()> {
47+
match cli.cmd {
48+
MemoryCommand::Migrate { jsonl, sqlite } => {
49+
codex_memory::migrate::migrate_jsonl_to_sqlite(&jsonl, &sqlite)?;
50+
}
51+
cmd => {
52+
let repo_root = std::env::current_dir()?;
53+
let store = factory::open_repo_store(&repo_root, None)?;
54+
match cmd {
55+
MemoryCommand::Add { content } => {
56+
let now = Utc::now().to_rfc3339();
57+
let item = MemoryItem {
58+
id: Uuid::new_v4().to_string(),
59+
created_at: now.clone(),
60+
updated_at: now,
61+
schema_version: 1,
62+
source: "codex-cli".into(),
63+
scope: Scope::Repo,
64+
status: Status::Active,
65+
kind: Kind::Note,
66+
content,
67+
tags: Vec::new(),
68+
relevance_hints: RelevanceHints {
69+
files: Vec::new(),
70+
crates: Vec::new(),
71+
languages: Vec::new(),
72+
commands: Vec::new(),
73+
},
74+
counters: Counters {
75+
seen_count: 0,
76+
used_count: 0,
77+
last_used_at: None,
78+
},
79+
expiry: None,
80+
};
81+
store.add(item)?;
82+
}
83+
MemoryCommand::List => {
84+
for item in store.list(None, None)? {
85+
println!("{}", item.content);
86+
}
87+
}
88+
MemoryCommand::Edit { id, content } => {
89+
if let Some(mut item) = store.get(&id)? {
90+
item.content = content;
91+
item.updated_at = Utc::now().to_rfc3339();
92+
store.update(&item)?;
93+
} else {
94+
anyhow::bail!("memory id not found: {id}");
95+
}
96+
}
97+
MemoryCommand::Rm { id } => {
98+
store.delete(&id)?;
99+
}
100+
MemoryCommand::Archive { id } => {
101+
store.archive(&id, true)?;
102+
}
103+
MemoryCommand::Unarchive { id } => {
104+
store.archive(&id, false)?;
105+
}
106+
MemoryCommand::Export => {
107+
let mut out = std::io::stdout();
108+
store.export(&mut out)?;
109+
}
110+
MemoryCommand::Import => {
111+
let mut input = std::io::stdin();
112+
let n = store.import(&mut input)?;
113+
println!("Imported {n} items");
114+
}
115+
MemoryCommand::Stats => {
116+
let stats = store.stats()?;
117+
println!("{stats}");
118+
}
119+
MemoryCommand::Recall { query } => {
120+
let ctx = codex_memory::recall::RecallContext {
121+
repo_root: Some(repo_root),
122+
dir: None,
123+
current_file: None,
124+
crate_name: None,
125+
language: None,
126+
command: None,
127+
now_rfc3339: Utc::now().to_rfc3339(),
128+
item_cap: 0,
129+
token_cap: 0,
130+
};
131+
let items = codex_memory::recall::recall(store.as_ref(), &query, &ctx)?;
132+
println!("{}", serde_json::to_string(&items)?);
133+
}
134+
MemoryCommand::Migrate { .. } => unreachable!(),
135+
}
136+
}
137+
}
138+
Ok(())
139+
}

codex-rs/cli/tests/memory.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
use chrono::Utc;
2+
use clap::Parser;
3+
use codex_cli::memory::{MemoryCli, MemoryCommand, run};
4+
use codex_memory::{
5+
factory,
6+
store::MemoryStore,
7+
types::{Counters, Kind, MemoryItem, RelevanceHints, Scope, Status},
8+
};
9+
use tempfile::tempdir;
10+
use uuid::Uuid;
11+
12+
#[test]
13+
fn parses_recall_for() {
14+
let cli = MemoryCli::parse_from(["memory", "recall", "--for", "hello"]);
15+
match cli.cmd {
16+
MemoryCommand::Recall { query } => assert_eq!(query, "hello"),
17+
_ => panic!("expected recall"),
18+
}
19+
}
20+
21+
#[test]
22+
fn sqlite_add_and_list() -> anyhow::Result<()> {
23+
let dir = tempdir()?;
24+
let prev = std::env::current_dir()?;
25+
std::env::set_current_dir(dir.path())?;
26+
std::fs::create_dir_all(dir.path().join(".codex/memory"))?;
27+
unsafe { std::env::set_var("CODEX_MEMORY_BACKEND", "sqlite") };
28+
run(MemoryCli {
29+
cmd: MemoryCommand::Add {
30+
content: "hello".into(),
31+
},
32+
})?;
33+
let store = factory::open_repo_store(dir.path(), Some(factory::Backend::Sqlite))?;
34+
let items = store.list(None, None)?;
35+
assert_eq!(items.len(), 1);
36+
assert_eq!(items[0].content, "hello");
37+
std::env::set_current_dir(prev)?;
38+
unsafe { std::env::remove_var("CODEX_MEMORY_BACKEND") };
39+
Ok(())
40+
}
41+
42+
#[test]
43+
fn migrate_jsonl_to_sqlite() -> anyhow::Result<()> {
44+
let dir = tempdir()?;
45+
let jsonl_path = dir.path().join("mem.jsonl");
46+
let sqlite_path = dir.path().join("mem.db");
47+
let now = Utc::now().to_rfc3339();
48+
let item = MemoryItem {
49+
id: Uuid::new_v4().to_string(),
50+
created_at: now.clone(),
51+
updated_at: now,
52+
schema_version: 1,
53+
source: "test".into(),
54+
scope: Scope::Repo,
55+
status: Status::Active,
56+
kind: Kind::Note,
57+
content: "hello".into(),
58+
tags: Vec::new(),
59+
relevance_hints: RelevanceHints {
60+
files: Vec::new(),
61+
crates: Vec::new(),
62+
languages: Vec::new(),
63+
commands: Vec::new(),
64+
},
65+
counters: Counters {
66+
seen_count: 0,
67+
used_count: 0,
68+
last_used_at: None,
69+
},
70+
expiry: None,
71+
};
72+
let line = serde_json::to_string(&item)?;
73+
std::fs::write(&jsonl_path, format!("{line}\n"))?;
74+
run(MemoryCli {
75+
cmd: MemoryCommand::Migrate {
76+
jsonl: jsonl_path.clone(),
77+
sqlite: sqlite_path.clone(),
78+
},
79+
})?;
80+
let store = codex_memory::store::sqlite::SqliteMemoryStore::new(sqlite_path);
81+
let items = store.list(None, None)?;
82+
assert_eq!(items.len(), 1);
83+
assert_eq!(items[0].content, "hello");
84+
Ok(())
85+
}

0 commit comments

Comments
 (0)