diff --git a/AGENTS.md b/AGENTS.md index 4fa3086..2605f78 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,8 @@ REPL 内的会话管理命令优先使用 slash command。 `/history`、`/resume`、`/new` 由程序直接处理,不进入 agent loop。 +当前项目支持最小 memory:使用 `/remember` 保存长期信息,使用 `/memory` 查看。 + 未明确说明时,使用以下默认值: - 工作目录:当前进程目录 diff --git a/SKILLS/cli-core.md b/SKILLS/cli-core.md index 225935d..fff60e6 100644 --- a/SKILLS/cli-core.md +++ b/SKILLS/cli-core.md @@ -24,6 +24,7 @@ - 修改已有文件时,优先使用 `Edit`,而不是 `Bash` 或 `Write` - 需要创建或覆盖文件时,优先使用 `Write` 工具,而不是 `Bash` - 会话管理优先使用 slash command,而不是自然语言或 `Bash` 探查 session 文件 +- 长期有价值的项目约束或偏好,优先使用 `/remember` 保存 - Windows 下先验证 shell 兼容性,再选择命令写法 ## 沟通风格 diff --git a/src/cc_slim/engine.py b/src/cc_slim/engine.py index d6d22b0..e3eaf67 100644 --- a/src/cc_slim/engine.py +++ b/src/cc_slim/engine.py @@ -13,6 +13,7 @@ from typing import Any, Iterator from anthropic import Anthropic from openai import OpenAI +from cc_slim.memory import MemoryStore from cc_slim.session import SessionStore from cc_slim.tools import Tool @@ -148,6 +149,10 @@ class Agent: for path in sorted(skills_dir.glob("*.md"), key=lambda p: p.name): parts.append(path.read_text(encoding="utf-8")) + memory = MemoryStore(workspace).read() + if memory: + parts.append(f"# Memory\n\n{memory}") + return "\n\n".join(part.strip() for part in parts if part.strip()) def _build_runtime_summary(self, workspace: Path) -> str: diff --git a/src/cc_slim/main.py b/src/cc_slim/main.py index 99f165c..7cd318a 100644 --- a/src/cc_slim/main.py +++ b/src/cc_slim/main.py @@ -8,6 +8,7 @@ from rich.console import Console from rich.table import Table from cc_slim.engine import Agent, resolve_config +from cc_slim.memory import MemoryStore from cc_slim.session import SessionStore from cc_slim.tools import build_default_tools @@ -81,6 +82,7 @@ def build_agent( def handle_repl_command( user_input: str, store: SessionStore, + memory: MemoryStore, config: object, root: Path, agent: Agent, @@ -95,6 +97,20 @@ def handle_repl_command( console.print(f"已创建新 session: {session_meta.get('session_id')}") return build_agent(root, config, store, session_meta, []) + if command == "/memory": + content = memory.read() + console.print(content or "当前项目还没有 memory。") + return agent + + if command.startswith("/remember "): + text = user_input.split(" ", 1)[1].strip() + if not text: + console.print("[red]error:[/red] 缺少需要保存的 memory 内容") + return agent + path = memory.append(text) + console.print(f"已写入 memory: {path}") + return agent + if command.startswith("--resume ") or command.startswith("/resume "): target = command.split(maxsplit=1)[1].strip() if not target: @@ -123,6 +139,7 @@ def run( ) -> None: root = cwd.resolve() store = SessionStore(root) + memory = MemoryStore(root) if history: render_history(store) return @@ -168,7 +185,7 @@ def run( if user_input.strip().startswith("/") or user_input.strip().startswith("--history") or user_input.strip().startswith("--resume"): try: - agent = handle_repl_command(user_input, store, config, root, agent) + agent = handle_repl_command(user_input, store, memory, config, root, agent) except Exception as exc: # pragma: no cover console.print(f"[red]error:[/red] {exc}") continue diff --git a/src/cc_slim/memory.py b/src/cc_slim/memory.py new file mode 100644 index 0000000..fd3ffef --- /dev/null +++ b/src/cc_slim/memory.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import hashlib +import re +from pathlib import Path + + +class MemoryStore: + def __init__(self, cwd: Path) -> None: + self.cwd = cwd.resolve() + self.root = Path.home() / ".config" / "cc-slim" / "memory" / self._sanitize_cwd(self.cwd) + self.root.mkdir(parents=True, exist_ok=True) + + def append(self, text: str) -> Path: + path = self.path() + existing = self.read() + content = f"{existing}\n\n{text.strip()}" if existing else text.strip() + path.write_text(content + "\n", encoding="utf-8") + return path + + def read(self) -> str: + path = self.path() + if not path.exists(): + return "" + return path.read_text(encoding="utf-8").strip() + + def path(self) -> Path: + return self.root / "MEMORY.md" + + def _sanitize_cwd(self, cwd: Path) -> str: + text = re.sub(r"[^A-Za-z0-9._-]+", "_", str(cwd)) + digest = hashlib.sha1(str(cwd).encode("utf-8")).hexdigest()[:8] + return f"{text[:48]}-{digest}"