Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions codex-rs/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ codex-login = { path = "../login" }
codex-mcp-server = { path = "../mcp-server" }
codex-protocol = { path = "../protocol" }
codex-tui = { path = "../tui" }
codex-memory = { path = "../memory", features = ["sqlite"] }
chrono = "0.4"
uuid = { version = "1", features = ["v4"] }
serde_json = "1"
tokio = { version = "1", features = [
"io-std",
Expand All @@ -38,3 +41,6 @@ tokio = { version = "1", features = [
tracing = "0.1.41"
tracing-subscriber = "0.3.19"
codex-protocol-ts = { path = "../protocol-ts" }

[dev-dependencies]
tempfile = "3"
1 change: 1 addition & 0 deletions codex-rs/cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod debug_sandbox;
mod exit_status;
pub mod login;
pub mod memory;
pub mod proto;

use clap::Parser;
Expand Down
7 changes: 7 additions & 0 deletions codex-rs/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use codex_tui::Cli as TuiCli;
use std::path::PathBuf;

use crate::proto::ProtoCli;
use codex_cli::memory::MemoryCli;

/// Codex CLI
///
Expand Down Expand Up @@ -73,6 +74,9 @@ enum Subcommand {
#[clap(visible_alias = "a")]
Apply(ApplyCommand),

/// Manage persistent memory items.
Memory(MemoryCli),

/// Internal: generate TypeScript protocol bindings.
#[clap(hide = true)]
GenerateTs(GenerateTsCommand),
Expand Down Expand Up @@ -209,6 +213,9 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
prepend_config_flags(&mut apply_cli.config_overrides, cli.config_overrides);
run_apply_command(apply_cli, None).await?;
}
Some(Subcommand::Memory(memory_cli)) => {
codex_cli::memory::run(memory_cli)?;
}
Some(Subcommand::GenerateTs(gen_cli)) => {
codex_protocol_ts::generate_ts(&gen_cli.out_dir, gen_cli.prettier.as_deref())?;
}
Expand Down
139 changes: 139 additions & 0 deletions codex-rs/cli/src/memory.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
use chrono::Utc;
use clap::Parser;
use codex_memory::factory;
use codex_memory::types::{Counters, Kind, MemoryItem, RelevanceHints, Scope, Status};
use std::path::PathBuf;
use uuid::Uuid;

/// CLI for memory management commands.
#[derive(Debug, Parser)]
pub struct MemoryCli {
#[command(subcommand)]
pub cmd: MemoryCommand,
}

/// Memory subcommands.
#[derive(Debug, clap::Subcommand)]
pub enum MemoryCommand {
/// Add a new memory item with given content.
Add { content: String },
/// List memory items.
List,
/// Edit an existing memory item.
Edit { id: String, content: String },
/// Remove a memory item by id.
Rm { id: String },
/// Archive a memory item.
Archive { id: String },
/// Unarchive a memory item.
Unarchive { id: String },
/// Export memory items to stdout.
Export,
/// Import memory items from stdin.
Import,
/// Migrate a JSONL file to a SQLite database.
Migrate { jsonl: PathBuf, sqlite: PathBuf },
/// Show basic statistics about stored memories.
Stats,
/// Recall memories for a given prompt.
Recall {
#[arg(long = "for")]
query: String,
},
}

/// Execute the memory command.
pub fn run(cli: MemoryCli) -> anyhow::Result<()> {
match cli.cmd {
MemoryCommand::Migrate { jsonl, sqlite } => {
codex_memory::migrate::migrate_jsonl_to_sqlite(&jsonl, &sqlite)?;
}
cmd => {
let repo_root = std::env::current_dir()?;
let store = factory::open_repo_store(&repo_root, None)?;
match cmd {
MemoryCommand::Add { content } => {
let now = Utc::now().to_rfc3339();
let item = MemoryItem {
id: Uuid::new_v4().to_string(),
created_at: now.clone(),
updated_at: now,
schema_version: 1,
source: "codex-cli".into(),
scope: Scope::Repo,
status: Status::Active,
kind: Kind::Note,
content,
tags: Vec::new(),
relevance_hints: RelevanceHints {
files: Vec::new(),
crates: Vec::new(),
languages: Vec::new(),
commands: Vec::new(),
},
counters: Counters {
seen_count: 0,
used_count: 0,
last_used_at: None,
},
expiry: None,
};
store.add(item)?;
}
MemoryCommand::List => {
for item in store.list(None, None)? {
println!("{}", item.content);
}
}
MemoryCommand::Edit { id, content } => {
if let Some(mut item) = store.get(&id)? {
item.content = content;
item.updated_at = Utc::now().to_rfc3339();
store.update(&item)?;
} else {
anyhow::bail!("memory id not found: {id}");
}
}
MemoryCommand::Rm { id } => {
store.delete(&id)?;
}
MemoryCommand::Archive { id } => {
store.archive(&id, true)?;
}
MemoryCommand::Unarchive { id } => {
store.archive(&id, false)?;
}
MemoryCommand::Export => {
let mut out = std::io::stdout();
store.export(&mut out)?;
}
MemoryCommand::Import => {
let mut input = std::io::stdin();
let n = store.import(&mut input)?;
println!("Imported {n} items");
}
MemoryCommand::Stats => {
let stats = store.stats()?;
println!("{stats}");
}
MemoryCommand::Recall { query } => {
let ctx = codex_memory::recall::RecallContext {
repo_root: Some(repo_root),
dir: None,
current_file: None,
crate_name: None,
language: None,
command: None,
now_rfc3339: Utc::now().to_rfc3339(),
item_cap: 0,
token_cap: 0,
};
let items = codex_memory::recall::recall(store.as_ref(), &query, &ctx)?;
println!("{}", serde_json::to_string(&items)?);
}
MemoryCommand::Migrate { .. } => unreachable!(),
}
}
}
Ok(())
}
85 changes: 85 additions & 0 deletions codex-rs/cli/tests/memory.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use chrono::Utc;
use clap::Parser;
use codex_cli::memory::{MemoryCli, MemoryCommand, run};
use codex_memory::{
factory,
store::MemoryStore,
types::{Counters, Kind, MemoryItem, RelevanceHints, Scope, Status},
};
use tempfile::tempdir;
use uuid::Uuid;

#[test]
fn parses_recall_for() {
let cli = MemoryCli::parse_from(["memory", "recall", "--for", "hello"]);
match cli.cmd {
MemoryCommand::Recall { query } => assert_eq!(query, "hello"),
_ => panic!("expected recall"),
}
}

#[test]
fn sqlite_add_and_list() -> anyhow::Result<()> {
let dir = tempdir()?;
let prev = std::env::current_dir()?;
std::env::set_current_dir(dir.path())?;
std::fs::create_dir_all(dir.path().join(".codex/memory"))?;
unsafe { std::env::set_var("CODEX_MEMORY_BACKEND", "sqlite") };
run(MemoryCli {
cmd: MemoryCommand::Add {
content: "hello".into(),
},
})?;
let store = factory::open_repo_store(dir.path(), Some(factory::Backend::Sqlite))?;
let items = store.list(None, None)?;
assert_eq!(items.len(), 1);
assert_eq!(items[0].content, "hello");
std::env::set_current_dir(prev)?;
unsafe { std::env::remove_var("CODEX_MEMORY_BACKEND") };
Ok(())
}

#[test]
fn migrate_jsonl_to_sqlite() -> anyhow::Result<()> {
let dir = tempdir()?;
let jsonl_path = dir.path().join("mem.jsonl");
let sqlite_path = dir.path().join("mem.db");
let now = Utc::now().to_rfc3339();
let item = MemoryItem {
id: Uuid::new_v4().to_string(),
created_at: now.clone(),
updated_at: now,
schema_version: 1,
source: "test".into(),
scope: Scope::Repo,
status: Status::Active,
kind: Kind::Note,
content: "hello".into(),
tags: Vec::new(),
relevance_hints: RelevanceHints {
files: Vec::new(),
crates: Vec::new(),
languages: Vec::new(),
commands: Vec::new(),
},
counters: Counters {
seen_count: 0,
used_count: 0,
last_used_at: None,
},
expiry: None,
};
let line = serde_json::to_string(&item)?;
std::fs::write(&jsonl_path, format!("{line}\n"))?;
run(MemoryCli {
cmd: MemoryCommand::Migrate {
jsonl: jsonl_path.clone(),
sqlite: sqlite_path.clone(),
},
})?;
let store = codex_memory::store::sqlite::SqliteMemoryStore::new(sqlite_path);
let items = store.list(None, None)?;
assert_eq!(items.len(), 1);
assert_eq!(items[0].content, "hello");
Ok(())
}
Loading