diff --git a/README.md b/README.md index 72085f0e..0a805865 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Agent Memory = Long-Term Memory + Short-Term Memory ## 📰 Latest Updates -- **[2026-02]** 💻 ReMeCli: A terminal-based AI chat assistant with built-in memory management. Automatically compacts long conversations into summaries to free up context space, and persists important information as Markdown files for retrieval in future sessions. Memory design inspired by [OpenClaw](https://github.com/openclaw/openclaw). ([Quick Start](docs/cli/quick_start_en.md)) +- **[2026-02]** 💻 ReMeCli: A terminal-based AI chat assistant with built-in memory management. Automatically compacts long conversations into summaries to free up context space, and persists important information as Markdown files for retrieval in future sessions. Memory design inspired by [OpenClaw](https://github.com/openclaw/openclaw). Try the `/horse` Easter egg for a Year of the Horse 2026 surprise -- fireworks, a galloping horse, and a random blessing. ([Quick Start](docs/cli/quick_start_en.md)) diff --git a/README_ZH.md b/README_ZH.md index 843d532c..4e453ecd 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -41,7 +41,7 @@ Agent Memory = Long-Term Memory + Short-Term Memory ## 📰 最新进展 -- **[2026-02]** 💻 ReMeCli:终端 AI 聊天助手,内置记忆管理能力。当对话过长时自动将旧内容压缩为摘要以释放上下文空间,同时将重要信息以 Markdown 文件持久化存储,供未来会话自动检索使用。记忆设计灵感来源于 [OpenClaw](https://github.com/openclaw/openclaw)。([快速开始](docs/cli/quick_start_en.md)) +- **[2026-02]** 💻 ReMeCli:终端 AI 聊天助手,内置记忆管理能力。当对话过长时自动将旧内容压缩为摘要以释放上下文空间,同时将重要信息以 Markdown 文件持久化存储,供未来会话自动检索使用。记忆设计灵感来源于 [OpenClaw](https://github.com/openclaw/openclaw)。输入 `/horse` 触发马年彩蛋——烟花、奔马动画和随机马年祝福。([快速开始](docs/cli/quick_start_en.md))
diff --git a/reme/horse.py b/reme/horse.py new file mode 100644 index 00000000..a11ac43c --- /dev/null +++ b/reme/horse.py @@ -0,0 +1,165 @@ +"""Horse Easter egg: fireworks, galloping horse animation, and a blessing.""" + +import math +import random +import shutil +import sys +import time + + +def _mirror_frame(frame: str) -> str: + """Mirror ASCII art horizontally.""" + mirror_map = str.maketrans(r"()/\<>[]{}", r")(\/><][}{") + lines = frame.split("\n") + max_len = max(len(line) for line in lines) if lines else 0 + mirrored = [] + for line in lines: + padded = line.ljust(max_len) + reversed_line = padded[::-1].translate(mirror_map) + mirrored.append(reversed_line) + return "\n".join(mirrored) + + +def _play_horse_easter_egg() -> None: + """Play the /horse Easter egg: fireworks, galloping horse, and a blessing.""" + cols = shutil.get_terminal_size((80, 24)).columns + rows = shutil.get_terminal_size((80, 24)).lines + + # -- Fireworks animation (~4 seconds at 8 fps = 32 frames) -- + firework_colors = [ + "\033[91m", # red + "\033[93m", # yellow + "\033[92m", # green + "\033[96m", # cyan + "\033[95m", # magenta + "\033[94m", # blue + ] + reset = "\033[0m" + particles_chars = ["*", ".", "o", "+", "x", "'", "`"] + + class Firework: + """A single firework burst with radial particles.""" + + def __init__(self, cx: int, cy: int, color: str, birth: int): + self.cx = cx + self.cy = cy + self.color = color + self.birth = birth + self.num = random.randint(12, 20) + self.angles = [random.uniform(0, 2 * math.pi) for _ in range(self.num)] + self.speeds = [random.uniform(0.5, 1.5) for _ in range(self.num)] + self.chars = [random.choice(particles_chars) for _ in range(self.num)] + + def particles(self, frame: int): + """Return list of (x, y, char) particle positions for the given frame.""" + age = frame - self.birth + if age < 0 or age > 10: + return [] + pts = [] + for i in range(self.num): + r = self.speeds[i] * age + px = self.cx + int(r * math.cos(self.angles[i]) * 2) # *2 for aspect ratio + py = self.cy + int(r * math.sin(self.angles[i])) + if 0 <= px < cols and 0 <= py < rows - 1: + pts.append((px, py, self.chars[i])) + return pts + + # Hide cursor + sys.stdout.write("\033[?25l") + sys.stdout.flush() + + try: + fireworks: list[Firework] = [] + total_frames = 32 + for f in range(total_frames): + # Spawn new fireworks periodically + if f % 4 == 0: + cx = random.randint(10, cols - 10) + cy = random.randint(2, rows // 2) + color = random.choice(firework_colors) + fireworks.append(Firework(cx, cy, color, f)) + + # Build frame buffer (blank) + buf: dict[tuple[int, int], tuple[str, str]] = {} + for fw in fireworks: + for px, py, ch in fw.particles(f): + buf[(px, py)] = (fw.color, ch) + + # Render + sys.stdout.write("\033[H\033[2J") # clear screen + for y in range(rows - 1): + line_parts: list[str] = [] + x = 0 + for x_pos in sorted(px for (px, py) in buf if py == y): + if x_pos >= x: + line_parts.append(" " * (x_pos - x)) + color, ch = buf[(x_pos, y)] + line_parts.append(f"{color}{ch}{reset}") + x = x_pos + 1 + sys.stdout.write("".join(line_parts) + "\n") + sys.stdout.flush() + time.sleep(1 / 8) # 8 fps + + # Prune old fireworks + fireworks.clear() + + # -- Horse ASCII art (bold yellow) -- + frame_1 = r""" + >>\. + /_ )`. + / _)`^)`. _.---. + (_,' \ `^---- `. + | | + \ / + / \ /___ / \ + / / | \ \ | +""" + frame_2 = r""" + >>\. + /_ )`. + / _)`^)`. _.---. + (_,' \ `^---- `. + | | + \ / + // / ___ / | + / / / | \ \ | +""" + + mirrored_1 = _mirror_frame(frame_1) + mirrored_2 = _mirror_frame(frame_2) + + bold_yellow = "\033[1;33m" + sys.stdout.write("\033[H\033[2J") # clear + + # Short galloping animation (8 cycles) + for i in range(8): + sys.stdout.write("\033[H\033[2J") + horse = mirrored_1 if i % 2 == 0 else mirrored_2 + indent = " " * (i * 3) + print("\n" * 3) + for line in horse.split("\n"): + if line.strip(): + print(f"{bold_yellow}{indent}{line}{reset}") + print(f"{bold_yellow}{'-' * min(i * 3 + 40, cols - 1)}{reset}") + sys.stdout.flush() + time.sleep(0.2) + + # -- Random blessing -- + blessings = [ + ("\u9a6c\u5230\u6210\u529f", "Succeed immediately"), + ("\u9f99\u9a6c\u7cbe\u795e", "Full of vitality"), + ("\u4e07\u9a6c\u5954\u817e", "Thousands of horses galloping"), + ("\u9a6c\u4e0d\u505c\u8e44", "Never stop striving"), + ("\u5feb\u9a6c\u52a0\u97ad", "Full speed ahead"), + ("\u4e00\u9a6c\u5f53\u5148", "Take the lead"), + ] + cn, en = random.choice(blessings) + print() + print(f"{bold_yellow} {cn} - {en}{reset}") + print(f"{bold_yellow} Happy Year of the Horse 2026!{reset}") + print() + + finally: + # Restore cursor + sys.stdout.write("\033[?25h") + sys.stdout.flush() diff --git a/reme/reme_cli.py b/reme/reme_cli.py index 27283887..ee6021f0 100644 --- a/reme/reme_cli.py +++ b/reme/reme_cli.py @@ -23,6 +23,7 @@ from .tool.fs import ( ) from .tool.gallery import ExecuteCode from .tool.search import DashscopeSearch, TavilySearch +from .horse import _play_horse_easter_egg class ReMeCli(ReMeFs): @@ -37,6 +38,7 @@ class ReMeCli(ReMeFs): "/exit": "Exit the application.", "/clear": "Clear the history.", "/help": "Show help.", + "/horse": "A surprise.", } self.working_dir = self.service_config.working_dir @@ -139,6 +141,10 @@ class ReMeCli(ReMeFs): print(f" {command}: {description}") continue + if user_input == "/horse": + _play_horse_easter_egg() + continue + # Stream processing state in_thinking = False in_answer = False diff --git a/tests/test_horse.py b/tests/test_horse.py new file mode 100644 index 00000000..86f6c989 --- /dev/null +++ b/tests/test_horse.py @@ -0,0 +1,81 @@ +"""Interactive horse ASCII art animation demo.""" + +import os +import time + +from reme.horse import _mirror_frame + + +def clear_screen(): + """Clear the terminal screen.""" + os.system("cls" if os.name == "nt" else "clear") + + +def running_horse(): + """Run a galloping horse animation across the terminal.""" + # 定义两帧动画,模拟腿部动作 + frame_1 = r""" + >>\. + /_ )`. + / _)`^)`. _.---. + (_,' \ `^---- `. + | | + \ / + / \ /___ / \ + / / | \ \ | + """ + + frame_2 = r""" + >>\. + /_ )`. + / _)`^)`. _.---. + (_,' \ `^---- `. + | | + \ / + // / ___ / | + / / / | \ \ | + """ + + frame_1 = _mirror_frame(frame_1) + frame_2 = _mirror_frame(frame_2) + + frames = [frame_1, frame_2] + distance = 0 + + try: + while True: + # 1. 清理屏幕 + clear_screen() + + # 2. 获取当前帧(通过取余数在两帧之间切换) + current_frame = frames[distance % 2] + + # 3. 增加左侧空格,产生向右移动的效果 + indent = " " * distance + + # 4. 打印带缩进的每一行 + print("\n" * 5) # 顶部留白 + for line in current_frame.split("\n"): + # 只有非空行才打印,避免格式错乱 + if line.strip() != "": + print(indent + line) + else: + print() + + # 5. 打印地面 + print("-" * (distance + 40)) + + # 6. 更新距离并暂停 + distance += 1 + time.sleep(0.2) # 控制速度,0.2秒一帧 + + # 跑到屏幕边缘重置(可选) + if distance > 60: + distance = 0 + + except KeyboardInterrupt: + print("\n马儿休息了。(程序已停止)") + + +if __name__ == "__main__": + running_horse()