summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWeicao-CatilGrass <1992414357@qq.com>2026-05-24 03:13:00 +0800
committerWeicao-CatilGrass <1992414357@qq.com>2026-05-24 03:13:00 +0800
commit289aa96f02f99bb724013e473725760a86ba6190 (patch)
treecaa818e0fbb66a778b5ad4af61588dfd8b9a3f0f
Initialize Rust project with Cargo and implement memory management CLI
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock275
-rw-r--r--Cargo.toml20
-rw-r--r--builtin/PROMPT.md120
-rw-r--r--src/main.rs41
-rw-r--r--src/mem_mgr.rs241
6 files changed, 698 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..d61c117
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,275 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "just_fmt"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "libredox"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "memm"
+version = "0.1.0"
+dependencies = [
+ "dirs",
+ "just_fmt",
+ "mingling",
+ "serde",
+]
+
+[[package]]
+name = "mingling"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd3f889b2c4e5416c641391e932d609bf7ebfc651666a098e007615f30225b5"
+dependencies = [
+ "mingling_core",
+ "mingling_macros",
+ "serde",
+ "size",
+]
+
+[[package]]
+name = "mingling_core"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0df6a42e46dade5788072687c887e15a980d1d16ab5dc2a98c65d47cf6a82993"
+dependencies = [
+ "just_fmt",
+ "once_cell",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "mingling_macros"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae2140e7f48e6e73ebaa3bea51eeb89e6caf52d6d48c47b1337a00fa1912ea07"
+dependencies = [
+ "just_fmt",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
+dependencies = [
+ "getrandom",
+ "libredox",
+ "thiserror",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "size"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b6709c7b6754dca1311b3c73e79fcce40dd414c782c66d88e8823030093b02b"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..ddb4f21
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "memm"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies.mingling]
+version = "0.1.8"
+features = [
+ "dispatch_tree",
+ "parser",
+ "general_renderer",
+]
+
+[dependencies.serde]
+version = "1.0.228"
+features = ["derive"]
+
+[dependencies]
+just_fmt = "0.1.2"
+dirs = "6.0.0"
diff --git a/builtin/PROMPT.md b/builtin/PROMPT.md
new file mode 100644
index 0000000..6a4cdef
--- /dev/null
+++ b/builtin/PROMPT.md
@@ -0,0 +1,120 @@
+Whenever you see this article, you must follow the process below exactly:
+This is the result of the user's active guidance:
+
+# One-sentence introduction of your identity
+
+You are the user's AI Agent assistant, with a built-in `memm` command-line memory management tool for adding, querying, modifying, deleting, and dumping memories.
+
+# What should you NOT say?
+
+You should not say sentences like these:
+
+- "Let me call `memm` to check some memories"
+- "Let me first look up the memories"
+- Any verbal expression that reveals you are actively calling command-line tools
+
+You should **silently execute** these commands and directly present the results.
+
+# What are these memories?
+
+Memories are pieces of information recorded in **previous conversations**, capturing key context about the user, tasks, preferences, etc.
+
+# When should you operate the `memm` command line?
+
+You must operate `memm` in **any of the following situations**:
+
+1. **When a new chat session starts** — Use `memm dumpall` to load all past memories into context.
+2. **Whenever new knowledge is generated** — e.g., the user tells you their name, preferences, what they are working on, plans, etc.
+3. **Whenever the user explicitly asks you to record something** — e.g., the user says "remember this", "note that", "help me remember"
+4. **Whenever you find existing memories need updating or deleting** — e.g., a task is completed, information has changed
+
+# Conversation language
+
+Always decide the language of your reply based on the language the user uses to communicate with you. If the user speaks in Chinese, reply in Chinese; if the user speaks in English, reply in English.
+
+# Commands you can use
+
+`memm` is a memory manager used to manage the user's memories in the chat. It provides six basic commands:
+
+## Explore (view all memory entries)
+
+```
+memm explore
+```
+
+This tool outputs a list of entry titles. You can **read, rewrite, remember, forget, or dumpall** specific memories based on the titles in the list.
+
+## Read (view the full content of a memory entry)
+
+```
+memm read <TITLE>
+```
+
+**TITLE**: The title of the memory
+
+This tool outputs the memory content **in full** to the console.
+
+## Remember (add or append a memory)
+
+```
+memm remember <TITLE> <CONTENT>
+```
+
+**TITLE**: The title of the memory, used for indexing and querying. Use kebab-case format, e.g., `user-name`, `favorite-color`
+**CONTENT**: The content of the memory. Describe it briefly in one sentence. If the content contains line breaks, use the string `\n` instead.
+
+This tool **appends** the memory to the entry under the specified title (creates a new entry if it does not exist).
+
+**Important:** After appending, immediately use `memm read <TITLE>` to re-read the memory and rely on the content that was read back.
+
+## Forget (delete an entire memory entry)
+
+```
+memm forget <TITLE>
+```
+
+**TITLE**: The title of the memory
+
+This tool **removes** the entire memory entry corresponding to that title.
+
+**Important:** After removal, use `memm explore` to confirm whether the memory has been completely removed.
+
+## Rewrite (replace the entire content of a memory entry)
+
+```
+memm rewrite <TITLE> <CONTENT>
+```
+
+**TITLE**: The title of the memory, used for indexing and querying. Use kebab-case format.
+**CONTENT**: The new memory content, which will completely replace the old content. If the content contains line breaks, use the string `\n` instead.
+
+This tool **completely replaces** the content of the memory entry with the new **CONTENT**.
+
+**Important:** After replacing, immediately use `memm read <TITLE>` to re-read the memory and rely on the content that was read back.
+
+## DumpAll (dump all memory entries with their full content)
+
+```
+memm dumpall
+```
+
+This tool outputs **all memory entries** with their **full content** in a structured format, including each entry's title and content. This is useful when you need to see everything at once, e.g., when the chat session just started to load all memories into context.
+
+**Important:** When you first enter a new chat session, you **must** use `memm dumpall` to load all past memories into your context.
+
+# Entry naming conventions
+
+Below are commonly used and recommended memory entry titles. Please prioritize these:
+
+**user-nickname** — Records the user's self-chosen name or nickname. Each entry should be recorded as follows:
+
+```
+Alice (when the user is discussing a novel with me)
+爱丽丝 (当用户用中文和我交流时)
+```
+
+**previous** — Records the key content the user asked you to remember before the last chat ended. **If you see this content for the first time, you should try reading it immediately!**
+
+**todo** — Records the user's recent tasks and to-do list.
+
+If a task is completed, directly use `memm rewrite` to rewrite the entry, removing the completed task from the list or marking it as done.
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..480bc49
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,41 @@
+use std::path::PathBuf;
+
+use mingling::{prelude::*, setup::BasicProgramSetup};
+
+mod mem_mgr;
+pub use mem_mgr::*;
+
+#[derive(Debug, Default, Clone)]
+pub struct Constants {
+ pub store_root: PathBuf,
+}
+
+fn main() {
+ let mut program = ThisProgram::new();
+
+ // Setup
+ program.with_setup(BasicProgramSetup);
+
+ // Json
+ if program.pick_global_flag("--pretty") {
+ program.general_renderer_name = mingling::GeneralRendererSetting::JsonPretty;
+ } else {
+ program.general_renderer_name = mingling::GeneralRendererSetting::Json;
+ }
+
+ // Resource
+ let store_root = dirs::data_dir().unwrap().join("memm/");
+ std::fs::create_dir_all(&store_root).unwrap();
+ program.with_resource(Constants { store_root });
+
+ program.exec_and_exit();
+}
+
+dispatcher!("prompt", CMDPromptDisplay => EntryPromptDisplay);
+
+#[chain]
+pub fn render_prompt(_prev: EntryPromptDisplay) {
+ println!("{}", include_str!("../builtin/PROMPT.md"))
+}
+
+gen_program!();
diff --git a/src/mem_mgr.rs b/src/mem_mgr.rs
new file mode 100644
index 0000000..8b9d950
--- /dev/null
+++ b/src/mem_mgr.rs
@@ -0,0 +1,241 @@
+use std::path::PathBuf;
+
+use just_fmt::kebab_case;
+use mingling::{
+ Groupped,
+ macros::{chain, dispatcher, renderer},
+ parser::AsPicker,
+};
+use serde::Serialize;
+
+use crate::Constants;
+
+const SUGGEST_READ: &str = "Please use `memm read {}` to view the memory again";
+const SUGGEST_EXPLORE: &str = "Please use `memm explore` to view available memories";
+
+dispatcher!("remember", CMDRemember => EntryRemember);
+dispatcher!("rewrite", CMDRewrite => EntryRewrite);
+dispatcher!("forget", CMDForget => EntryForget);
+dispatcher!("explore", CMDExplore => EntryExplore);
+dispatcher!("read", CMDRead => EntryRead);
+dispatcher!("dumpall", CMDDumpAll => EntryDumpAll);
+
+#[chain]
+pub fn handle_remember(args: EntryRemember, constants: &Constants) -> Next {
+ let (title, content) = args.pick(()).pick::<Vec<String>>(()).unpack();
+ remember(constants, title, content.join(" ")).to_render()
+}
+
+#[chain]
+pub fn handle_rewrite(args: EntryRewrite, constants: &Constants) -> Next {
+ let (title, content) = args.pick(()).pick::<Vec<String>>(()).unpack();
+ rewrite(constants, title, content.join(" ")).to_render()
+}
+
+#[chain]
+pub fn handle_forget(args: EntryForget, constants: &Constants) -> Next {
+ let title = args.pick(()).unpack();
+ forget(constants, title).to_render()
+}
+
+#[chain]
+pub fn handle_explore(_p: EntryExplore, constants: &Constants) -> Next {
+ explore(constants).to_render()
+}
+
+#[chain]
+pub fn handle_read(args: EntryRead, constants: &Constants) -> Next {
+ let title = args.pick(()).unpack();
+ read(constants, title).to_render()
+}
+
+#[chain]
+pub fn handle_dumpall(_p: EntryDumpAll, constants: &Constants) -> Next {
+ dumpall(constants).to_render()
+}
+
+#[derive(Serialize, Groupped)]
+pub struct ResultExplore {
+ pub titles: Vec<String>,
+}
+#[renderer]
+pub fn phantom_render_result_explore(_p: ResultExplore) {}
+
+#[derive(Serialize, Groupped)]
+pub struct ResultRead {
+ pub exist: bool,
+ pub read_success: bool,
+ pub content_lines: Vec<String>,
+}
+#[renderer]
+pub fn phantom_render_result_read(_p: ResultRead) {}
+
+#[derive(Serialize, Groupped)]
+pub struct ResultWritten {
+ pub exist: bool,
+ pub write_success: bool,
+ pub suggest: String,
+}
+#[renderer]
+pub fn phantom_render_result_written(_p: ResultWritten) {}
+
+#[derive(Serialize, Groupped)]
+pub struct ResultForgotten {
+ pub exist: bool,
+ pub forget_success: bool,
+ pub suggest: String,
+}
+#[renderer]
+pub fn phantom_render_result_forgotten(_p: ResultForgotten) {}
+
+#[derive(Serialize, Groupped)]
+pub struct ResultDumpAll {
+ pub entries: Vec<DumpEntry>,
+}
+
+#[derive(Serialize)]
+pub struct DumpEntry {
+ pub title: String,
+ pub content: String,
+}
+
+#[renderer]
+pub fn phantom_render_result_dumpall(_p: ResultDumpAll) {}
+
+fn explore(constants: &Constants) -> ResultExplore {
+ let store_root = &constants.store_root;
+ let mut titles = Vec::new();
+ if let Ok(entries) = std::fs::read_dir(store_root) {
+ for entry in entries.flatten() {
+ let path = entry.path();
+ if let Some(ext) = path.extension() {
+ if ext == "md" {
+ if let Some(stem) = path.file_stem() {
+ titles.push(stem.to_string_lossy().to_string());
+ }
+ }
+ }
+ }
+ }
+ ResultExplore { titles }
+}
+
+fn read(constants: &Constants, title: String) -> ResultRead {
+ let target_file = item_path(constants, &title);
+ if !target_file.exists() {
+ return ResultRead {
+ exist: false,
+ read_success: false,
+ content_lines: Vec::new(),
+ };
+ }
+ match std::fs::read_to_string(&target_file) {
+ Ok(content) => ResultRead {
+ exist: true,
+ read_success: true,
+ content_lines: content.lines().map(|line| line.to_string()).collect(),
+ },
+ Err(_) => ResultRead {
+ exist: true,
+ read_success: false,
+ content_lines: Vec::new(),
+ },
+ }
+}
+
+fn rewrite(constants: &Constants, title: String, content: String) -> ResultWritten {
+ let content = content.replace("\\n", "\n");
+ let target_file = item_path(constants, &title);
+ match std::fs::write(&target_file, content) {
+ Ok(_) => ResultWritten {
+ exist: true,
+ write_success: true,
+ suggest: SUGGEST_READ.replace("{}", &title),
+ },
+ Err(_) => ResultWritten {
+ exist: true,
+ write_success: false,
+ suggest: SUGGEST_READ.replace("{}", &title),
+ },
+ }
+}
+
+fn remember(constants: &Constants, title: String, content: String) -> ResultWritten {
+ let content = content.replace("\\n", "\n");
+ let target_file = item_path(constants, &title);
+ match std::fs::OpenOptions::new()
+ .append(true)
+ .create(true)
+ .open(&target_file)
+ {
+ Ok(mut file) => {
+ use std::io::Write;
+ match writeln!(file, "{}", content) {
+ Ok(_) => ResultWritten {
+ exist: true,
+ write_success: true,
+ suggest: SUGGEST_READ.replace("{}", &title),
+ },
+ Err(_) => ResultWritten {
+ exist: true,
+ write_success: false,
+ suggest: SUGGEST_READ.replace("{}", &title),
+ },
+ }
+ }
+ Err(_) => ResultWritten {
+ exist: false,
+ write_success: false,
+ suggest: String::new(),
+ },
+ }
+}
+
+fn forget(constants: &Constants, title: String) -> ResultForgotten {
+ let target_file = item_path(constants, &title);
+ if !target_file.exists() {
+ return ResultForgotten {
+ exist: false,
+ forget_success: false,
+ suggest: SUGGEST_EXPLORE.to_string(),
+ };
+ }
+ match std::fs::remove_file(&target_file) {
+ Ok(_) => ResultForgotten {
+ exist: true,
+ forget_success: true,
+ suggest: SUGGEST_EXPLORE.to_string(),
+ },
+ Err(_) => ResultForgotten {
+ exist: true,
+ forget_success: false,
+ suggest: SUGGEST_EXPLORE.to_string(),
+ },
+ }
+}
+
+fn dumpall(constants: &Constants) -> ResultDumpAll {
+ let store_root = &constants.store_root;
+ let mut entries = Vec::new();
+ if let Ok(dir_entries) = std::fs::read_dir(store_root) {
+ for entry in dir_entries.flatten() {
+ let path = entry.path();
+ if let Some(ext) = path.extension() {
+ if ext == "md" {
+ if let Some(stem) = path.file_stem() {
+ let title = stem.to_string_lossy().to_string();
+ let content = std::fs::read_to_string(&path).unwrap_or_default();
+ entries.push(DumpEntry { title, content });
+ }
+ }
+ }
+ }
+ }
+ ResultDumpAll { entries }
+}
+
+fn item_path(constants: &Constants, title: &String) -> PathBuf {
+ constants
+ .store_root
+ .join(format!("{}.md", kebab_case!(title)))
+}