@@ -79,6 +79,25 @@ enum Commands {
7979 raw : Option < String > ,
8080 } ,
8181
82+ /// Shorthand for `store` with positional content. Topic defaults to the
83+ /// auto-detected project name (git remote or cwd).
84+ Remember {
85+ /// Fact to remember
86+ content : String ,
87+
88+ /// Topic/category (default: auto-detected project name)
89+ #[ arg( short, long) ]
90+ topic : Option < String > ,
91+
92+ /// Importance level
93+ #[ arg( short, long, default_value = "medium" ) ]
94+ importance : CliImportance ,
95+
96+ /// Keywords (comma-separated)
97+ #[ arg( short, long) ]
98+ keywords : Option < String > ,
99+ } ,
100+
82101 /// Search memories
83102 Recall {
84103 /// Search query
@@ -1110,6 +1129,26 @@ fn main() -> Result<()> {
11101129 raw,
11111130 )
11121131 }
1132+ Commands :: Remember {
1133+ content,
1134+ topic,
1135+ importance,
1136+ keywords,
1137+ } => {
1138+ #[ cfg( feature = "embeddings" ) ]
1139+ let emb_ref = embedder. as_ref ( ) . map ( |e| e as & dyn icm_core:: Embedder ) ;
1140+ #[ cfg( not( feature = "embeddings" ) ) ]
1141+ let emb_ref: Option < & dyn icm_core:: Embedder > = None ;
1142+ cmd_remember (
1143+ & store,
1144+ emb_ref,
1145+ & cfg. memory ,
1146+ content,
1147+ topic,
1148+ importance. into ( ) ,
1149+ keywords,
1150+ )
1151+ }
11131152 Commands :: Recall {
11141153 query,
11151154 topic,
@@ -1611,6 +1650,38 @@ fn cmd_store(
16111650 Ok ( ( ) )
16121651}
16131652
1653+ /// `remember` is `store` with a positional content arg and an auto-detected
1654+ /// topic when `--topic` is omitted.
1655+ #[ allow( clippy:: too_many_arguments) ]
1656+ fn cmd_remember (
1657+ store : & SqliteStore ,
1658+ embedder : Option < & dyn icm_core:: Embedder > ,
1659+ memory_cfg : & crate :: config:: MemoryConfig ,
1660+ content : String ,
1661+ topic : Option < String > ,
1662+ importance : Importance ,
1663+ keywords : Option < String > ,
1664+ ) -> Result < ( ) > {
1665+ if content. trim ( ) . is_empty ( ) {
1666+ anyhow:: bail!( "content cannot be empty - provide something to remember" ) ;
1667+ }
1668+ let resolved_topic = topic. unwrap_or_else ( || {
1669+ let project = detect_project ( ) ;
1670+ eprintln ! ( "Project: {project}" ) ;
1671+ project
1672+ } ) ;
1673+ cmd_store (
1674+ store,
1675+ embedder,
1676+ memory_cfg,
1677+ resolved_topic,
1678+ content,
1679+ importance,
1680+ keywords,
1681+ None ,
1682+ )
1683+ }
1684+
16141685#[ allow( clippy:: too_many_arguments) ]
16151686fn cmd_recall (
16161687 store : & SqliteStore ,
@@ -8394,3 +8465,102 @@ mod hook_payload_tests {
83948465 ) ;
83958466 }
83968467}
8468+
8469+ #[ cfg( test) ]
8470+ mod cmd_remember_tests {
8471+ //! Parse `icm remember ...` through clap so a broken variant
8472+ //! (wrong positional, swapped fields, dropped default) fails here
8473+ //! rather than only at runtime.
8474+ use super :: * ;
8475+
8476+ /// Positional content, default topic None, default importance medium.
8477+ #[ test]
8478+ fn parses_positional_content_with_defaults ( ) {
8479+ let cli = Cli :: try_parse_from ( [ "icm" , "remember" , "some fact" ] ) . unwrap ( ) ;
8480+ let Commands :: Remember {
8481+ content,
8482+ topic,
8483+ importance,
8484+ keywords,
8485+ } = cli. command
8486+ else {
8487+ panic ! ( "expected Commands::Remember" ) ;
8488+ } ;
8489+ assert_eq ! ( content, "some fact" ) ;
8490+ assert_eq ! ( topic, None ) ;
8491+ assert ! ( matches!( importance, CliImportance :: Medium ) ) ;
8492+ assert_eq ! ( keywords, None ) ;
8493+ }
8494+
8495+ /// `--topic` and `--importance` overrides land on the Remember variant.
8496+ #[ test]
8497+ fn parses_topic_and_importance_overrides ( ) {
8498+ let cli = Cli :: try_parse_from ( [
8499+ "icm" ,
8500+ "remember" ,
8501+ "critical deployment constraint" ,
8502+ "--topic" ,
8503+ "preferences" ,
8504+ "--importance" ,
8505+ "high" ,
8506+ ] )
8507+ . unwrap ( ) ;
8508+ let Commands :: Remember {
8509+ content,
8510+ topic,
8511+ importance,
8512+ ..
8513+ } = cli. command
8514+ else {
8515+ panic ! ( "expected Commands::Remember" ) ;
8516+ } ;
8517+ assert_eq ! ( content, "critical deployment constraint" ) ;
8518+ assert_eq ! ( topic. as_deref( ) , Some ( "preferences" ) ) ;
8519+ assert ! ( matches!( importance, CliImportance :: High ) ) ;
8520+ }
8521+
8522+ /// Missing positional content is a parse error.
8523+ #[ test]
8524+ fn missing_content_is_a_parse_error ( ) {
8525+ assert ! ( Cli :: try_parse_from( [ "icm" , "remember" ] ) . is_err( ) ) ;
8526+ }
8527+
8528+ /// `remember` appends; prior memories under the same topic stay intact.
8529+ #[ test]
8530+ fn remember_appends_status_update_to_existing_memories ( ) {
8531+ use icm_core:: { Importance , MemoryStore } ;
8532+ use icm_store:: SqliteStore ;
8533+ let store = SqliteStore :: in_memory ( ) . unwrap ( ) ;
8534+ let cfg = crate :: config:: MemoryConfig :: default ( ) ;
8535+
8536+ cmd_store (
8537+ & store,
8538+ None ,
8539+ & cfg,
8540+ "icm" . into ( ) ,
8541+ "TODO: wire FTS5 trigger for memory updates" . into ( ) ,
8542+ Importance :: Medium ,
8543+ None ,
8544+ None ,
8545+ )
8546+ . unwrap ( ) ;
8547+
8548+ cmd_remember (
8549+ & store,
8550+ None ,
8551+ & cfg,
8552+ "FTS5 trigger now syncs on update; closes the recall gap" . into ( ) ,
8553+ Some ( "icm" . into ( ) ) ,
8554+ Importance :: Medium ,
8555+ None ,
8556+ )
8557+ . unwrap ( ) ;
8558+
8559+ let memories = store. get_by_topic ( "icm" ) . unwrap ( ) ;
8560+ assert_eq ! ( memories. len( ) , 2 , "remember appends, never overwrites" ) ;
8561+ assert ! ( memories. iter( ) . any( |m| m. summary. contains( "TODO" ) ) ) ;
8562+ assert ! ( memories
8563+ . iter( )
8564+ . any( |m| m. summary. contains( "closes the recall gap" ) ) ) ;
8565+ }
8566+ }
0 commit comments