Skill: fatcatmaofei/openclaw-enhanced-memory (version 1.0.0)

--- name: enhanced-memory description: > An enhanced memory system for OpenClaw agents that replaces the default single-file MEMORY.md with a complete memory architecture: hierarchical directory organization by category, [category:value] tag indexing with multi-tag AND search, automatic lifecycle management (active → archive, never delete), and intelligent cross-category retrieval that auto-routes queries to the right memory module. Gives your agent structured, searchable, long-lived memory out of the box. --- # Enhanced Memory A structured memory system that gives your OpenClaw agent **organized, searchable, long-lived memory** instead of a single monolithic `MEMORY.md`. ## Why? The default `MEMORY.md` approach hits a wall fast: - One file grows endlessly → slow to read, expensive on tokens - No categorization → food logs mixed with project notes mixed with relationship context - No retrieval strategy → agent re-reads everything or misses what matters - No lifecycle → old entries clutter active memory forever Tagged Memory fixes all of this. ## Core Features ### 1. Hierarchical Directory Organization Memories are stored in purpose-built directories: ``` memory/ ├── current/ # Active memories (last 6 months) ├── archived/ # Auto-archived older memories (permanent, never deleted) │ └── YYYY-MM/ # Organized by month ├── RELATION/ # One file per person (relationship context) ├── food/ # Meal and food logs ├── training/ # Exercise and workout records ├── connections.md # Global relationship graph ├── system/ # System config and logs └── misc/ # Everything else ``` ### 2. Tag-Based Indexing Tag any line in any memory file with `[category:value]` markers: ```markdown ## 2026-02-20 - Had lunch with Zhang Hao [人物:张浩东] [类型:聚餐] [地点:campus] - Discussed the new project deadline [项目:openclaw] [类型:会议] - Yoyo learned a new trick today [宠物:悠悠] [类型:milestone] ``` Tags support multi-tag AND search — find the exact memory you need: ```bash # Single tag search python3 scripts/memory_tag_search.py "人物:张浩东" # Multi-tag AND search (all tags must match) python3 scripts/memory_tag_search.py "人物:王隆哲" "类型:开票信息" # List all tags in the system python3 scripts/memory_tag_search.py --list-tags # List tags under a specific category python3 scripts/memory_tag_search.py --list-tags --category 人物 ``` ### 3. Lifecycle Management Memories age gracefully — never lost, always accessible: | Age | Location | Status | |-----|----------|--------| | 0–6 months | `memory/current/` | Active, auto-retrieved | | 6–12 months | `memory/archived/YYYY-MM/` | Archived, searchable on demand | | 12+ months | `memory/archived/` | Permanent archive, manual query | Run the lifecycle manager manually or via cron: ```bash # Default: archive memories older than 6 months python3 scripts/memory_lifecycle_manager.py # Custom threshold (e.g., 90 days) python3 scripts/memory_lifecycle_manager.py 90 ``` ### 4. Smart Cross-Category Retrieval The retrieval strategy script auto-classifies queries and searches the right directories: ```bash python3 scripts/memory_retrieval_strategy.py "What did I eat yesterday?" # → Searches memory/food/ + memory/current/ python3 scripts/memory_retrieval_strategy.py "How is Yoyo doing?" # → Searches memory/RELATION/悠悠.md + memory/connections.md python3 scripts/memory_retrieval_strategy.py "Yang Lingxiao" # → Searches memory/RELATION/ + memory/connections.md ``` Query type detection covers: food, training, relationships, pets, system, mood, projects, and more. ## Scripts | Script | Purpose | |--------|---------| | `scripts/memory_tag_search.py` | Tag-based indexing and search (single/multi-tag AND queries, tag listing) | | `scripts/memory_retrieval_strategy.py` | Smart retrieval — auto-classifies queries and routes to relevant memory directories | | `scripts/memory_lifecycle_manager.py` | Automatic archival of old memories (configurable threshold, never deletes) | ## Integration ### AGENTS.md Add the following to your `AGENTS.md` memory section: ```markdown ## Memory ### Directory Structure - `memory/current/` — active memories (6 months) - `memory/archived/` — permanent archive - `memory/RELATION/` — per-person relationship files - `memory/food/` — meal logs - `memory/training/` — workout logs ### Retrieval Strategy - Exact queries (names, dates, codes) → `grep` the file system - Fuzzy/semantic queries → `python3 scripts/memory_retrieval_strategy.py "<query>"` - Tag search → `python3 scripts/memory_tag_search.py "<category>:<value>"` ### Tagging Convention When writing memory entries, tag important lines: [人物:name] [类型:type] [地点:place] [项目:project] [情绪:mood] ``` ### HEARTBEAT.md Add periodic memory maintenance to your heartbeat checklist: ```markdown ## Memory Maintenance (every few days) - [ ] Run `python3 scripts/memory_lifecycle_manager.py` to archive old memories - [ ] Run `python3 scripts/memory_tag_search.py --list-tags` to review tag health - [ ] Check `memory/current/` file count — if growing large, verify archival is running ``` ### Cron (optional) Set up monthly auto-archival: ```bash # Run on the 1st of every month at 03:00 0 3 1 * * cd /path/to/workspace && python3 scripts/memory_lifecycle_manager.py ``` ## Customization - **Archive threshold**: Edit `ARCHIVE_THRESHOLD_DAYS` in `memory_lifecycle_manager.py` (default: 180 days) - **Query patterns**: Add new regex patterns to `QUERY_TYPES` in `memory_retrieval_strategy.py` - **Memory directories**: Add new modules to `MODULES_TO_ARCHIVE` in `memory_lifecycle_manager.py` - **Tag categories**: Tags are freeform — just use `[category:value]` in any `.md` file ## Requirements - Python 3.8+ - No external dependencies (stdlib only)

path: references/architecture.md
content: # Tagged Memory — Architecture Reference

## Overview

Tagged Memory is a file-system-based memory system designed for OpenClaw AI agents. It replaces the default flat `MEMORY.md` with a structured, categorized, and searchable memory store.

## Design Principles

1. **Files over databases** — Memory is plain Markdown files on disk. No external services, no API keys, no dependencies. `grep` always works.
2. **Categories over chronology** — Instead of one timeline, memories live in purpose-specific directories (food, training, relations, etc.).
3. **Tags over full-text search** — Inline `[category:value]` tags provide precise, structured retrieval without NLP overhead.
4. **Archive over delete** — Old memories move to `archived/` but are never removed. Nothing is lost.

## Component Diagram

```
┌─────────────────────────────────────────────┐
│                  Agent                       │
│                                              │
│  "What did I eat?"    "Who is Zhang Hao?"   │
└──────┬────────────────────────┬──────────────┘
       │                        │
       ▼                        ▼
┌──────────────────┐  ┌──────────────────────┐
│ memory_retrieval │  │  memory_tag_search   │
│ _strategy.py     │  │  .py                 │
│                  │  │                      │
│ Classifies query │  │ Scans [cat:val] tags │
│ → routes to dirs │  │ AND logic across     │
│                  │  │ all .md files        │
└──────┬───────────┘  └──────────┬───────────┘
       │                         │
       ▼                         ▼
┌─────────────────────────────────────────────┐
│              memory/ (filesystem)            │
│                                              │
│  current/   food/   training/   RELATION/   │
│  archived/  misc/   system/     connections │
└─────────────────────────────────────────────┘
       ▲
       │ monthly
┌──────┴───────────┐
│ memory_lifecycle │
│ _manager.py      │
│                  │
│ Moves files >6mo │
│ to archived/     │
└──────────────────┘
```

## Tag Format Specification

Tags follow the pattern: `[category:value]`

- **category**: A short classifier (e.g., 人物, 类型, 地点, 项目, 情绪)
- **value**: The specific entity or attribute
- Multiple tags per line are supported
- Search uses AND logic across multiple query tags

### Examples

```markdown
- Lunch at the new ramen place [类型:午餐] [地点:ramen-shop] [情绪:satisfied]
- Call with Liu Hui about invoice [人物:刘辉] [类型:开票信息] [项目:billing]
- Yoyo's vet checkup — all clear [宠物:悠悠] [类型:health-check]
```

## Retrieval Strategy — Query Classification

The retrieval strategy uses regex pattern matching to classify queries:

| Query Type | Trigger Keywords | Search Scope |
|-----------|-----------------|--------------|
| food | 吃, 午饭, 晚饭, 早餐, 外卖... | `food/`, `current/` |
| training | 训练, 运动, 健身, 跑步... | `training/`, `current/` |
| relation | Person names, 朋友, 家人... | `RELATION/`, `connections.md` |
| yoyo | 悠悠, 狗狗, 宠物... | `RELATION/悠悠.md`, `connections.md` |
| system | 系统, 配置, bug, 崩溃... | `system/` |
| mood | 心情, 开心, 难过... | `current/` |
| project | 项目, 代码, git... | `current/`, `misc/` |
| default | (unclassified) | `current/` |

## Lifecycle Rules

| Rule | Detail |
|------|--------|
| Active window | 180 days (configurable) |
| Archive trigger | File modification time > threshold |
| Archive location | `memory/archived/YYYY-MM/` |
| Deletion policy | **Never** — all archives are permanent |
| Modules archived | `current`, `food`, `training`, `misc` |
| Protected dirs | `RELATION/`, `connections.md`, `system/` (not auto-archived) |


path: scripts/memory_lifecycle_manager.py
content: #!/usr/bin/env python3
"""
记忆生命周期管理（Memory Lifecycle Manager）
自动归档旧记忆，实现持久化存储，永不删除
"""

import os
import re
import shutil
from pathlib import Path
from datetime import datetime, timedelta

MEMORY_BASE = Path("/home/clawdbot/.openclaw/workspace/memory")

# 归档配置
ARCHIVE_THRESHOLD_DAYS = 180  # 6个月
ARCHIVE_DIR = MEMORY_BASE / "archived"

# 要归档的记忆模块（相对于MEMORY_BASE）
MODULES_TO_ARCHIVE = [
    "current",
    "food",
    "training",
    "misc",
]


def get_file_modification_date(file_path: Path) -> datetime:
    """
    获取文件的修改日期
    """
    if file_path.exists():
        timestamp = file_path.stat().st_mtime
        return datetime.fromtimestamp(timestamp)
    else:
        return datetime.min


def should_archive_file(file_path: Path, threshold_days: int = ARCHIVE_THRESHOLD_DAYS) -> bool:
    """
    判断文件是否应该归档
    """
    mod_date = get_file_modification_date(file_path)
    age = datetime.now() - mod_date

    # 如果文件超过threshold_days天未修改，则归档
    return age > timedelta(days=threshold_days)


def create_archive_directory(year_month: str):
    """
    创建归档目录：memory/archived/YYYY-MM/
    """
    archive_dir = ARCHIVE_DIR / year_month
    archive_dir.mkdir(parents=True, exist_ok=True)
    return archive_dir


def archive_file(file_path: Path, target_dir: Path) -> Path:
    """
    归档单个文件
    """
    # 保留文件名，但添加时间戳后缀
    # 例如：2026-02-16.md → 2026-02-16_20260216.md
    original_name = file_path.name
    timestamp = datetime.now().strftime("%Y%m%d")
    new_name = f"{original_name}_{timestamp}.md"

    target_path = target_dir / new_name

    # 复制文件到归档目录（保留原始文件）
    shutil.copy2(file_path, target_path)

    # 从当前目录删除文件
    file_path.unlink()

    print(f"  📦 归档: {original_name} → {new_name}")
    return target_path


def archive_module(module_path: Path, threshold_days: int = ARCHIVE_THRESHOLD_DAYS) -> dict:
    """
    归档整个记忆模块
    返回：{module: {archived: int, skipped: int, total: int}}
    """
    results = {"archived": 0, "skipped": 0, "total": 0}

    if not module_path.exists():
        print(f"  ⚠️  模块不存在: {module_path}")
        return results

    print(f"\n📂 处理模块: {module_path.relative_to(MEMORY_BASE)}")

    # 获取模块下所有.md文件（递归）
    md_files = list(module_path.rglob("*.md"))

    for md_file in md_files:
        # 跳过archived目录（防止重复归档）
        if "archived" in str(md_file):
            continue

        results["total"] += 1

        # 检查是否应该归档
        if should_archive_file(md_file, threshold_days):
            # 计算文件所属的年月
            mod_date = get_file_modification_date(md_file)
            year_month = mod_date.strftime("%Y-%m")

            # 创建归档目录
            target_dir = create_archive_directory(year_month)

            # 归档文件
            archive_file(md_file, target_dir)
            results["archived"] += 1
        else:
            results["skipped"] += 1

    return results


def archive_all_modules(threshold_days: int = ARCHIVE_THRESHOLD_DAYS):
    """
    归档所有配置的记忆模块
    """
    print(f"\n🔍 记忆生命周期管理")
    print(f"⏰ 归档阈值: {threshold_days} 天 ({threshold_days//30} 个月)")
    print(f"📁 归档目录: {ARCHIVE_DIR}")

    # 检查归档目录是否存在
    if not ARCHIVE_DIR.exists():
        ARCHIVE_DIR.mkdir(parents=True, exist_ok=True)
        print(f"✅ 创建归档目录: {ARCHIVE_DIR}")

    # 统计信息
    total_archived = 0
    total_skipped = 0
    total_files = 0

    # 归档每个模块
    for module in MODULES_TO_ARCHIVE:
        module_path = MEMORY_BASE / module
        results = archive_module(module_path, threshold_days)

        total_archived += results["archived"]
        total_skipped += results["skipped"]
        total_files += results["total"]

        print(f"  ✅ 模块归档完成: {results['archived']} 个文件归档, {results['skipped']} 个跳过")

    # 总结
    print(f"\n📊 归档统计:")
    print(f"  总文件数: {total_files}")
    print(f"  已归档: {total_archived}")
    print(f"  跳过: {total_skipped}")

    # 检查当前活跃记忆文件数
    current_files = 0
    for module in MODULES_TO_ARCHIVE:
        module_path = MEMORY_BASE / module
        if module_path.exists():
            current_files += len(list(module_path.rglob("*.md")))

    print(f"\n🎯 当前活跃记忆文件数: {current_files}")
    print(f"📦 归档记忆文件数: {total_archived}")
    print(f"💾 磁盘占用: {'✅ 已持久化，永不删除'}")


def main():
    import sys

    if len(sys.argv) > 1:
        # 允许手动指定归档阈值
        try:
            days = int(sys.argv[1])
            archive_all_modules(days)
        except ValueError:
            print("❌ 错误：请输入有效的天数（例如：180 表示6个月）")
            sys.exit(1)
    else:
        # 默认使用配置的阈值
        archive_all_modules(ARCHIVE_THRESHOLD_DAYS)


if __name__ == "__main__":
    main()


path: scripts/memory_retrieval_strategy.py
content: #!/usr/bin/env python3
"""
记忆检索策略 (Memory Retrieval Strategy)
根据对话类型自动检索最相关的记忆模块
"""

import os
import re
from pathlib import Path

MEMORY_BASE = Path("/home/clawdbot/.openclaw/workspace/memory")

# 查询类型分类
QUERY_TYPES = {
    # 食物相关
    "food": [
        r"吃的|吃的什么|午饭|晚饭|早餐|中午|晚上|宵夜|点外卖|吃饭|菜|味道|好吃|难吃",
        r"食谱|菜谱|做法|配料|调味",
    ],

    # 训练/运动相关
    "training": [
        r"训练|运动|健身|爬山|攀岩|跑步|跳绳|力量|练肌肉|器材|场地|消耗|卡路里|体重",
        r"桌球|台球|羽毛球|篮球|足球|游戏",
    ],

    # 关系/人物相关
    "relation": [
        r"朋友|朋友叫什么|认识|谁认识谁|社交|聚会|关系",
        r"父母|家人|父母在干嘛|回家|视频",
        # 常见人名（高优先级匹配）
        r"(杨凌霄|张浩东|刘子锐|高东瑞|吕澄轩|廖擎杰|赵鸿剑|邱冠儒|王隆哲|母琳珲|刘辉|冯葵开|何展逸|范昕|刘泽洋|黄谢郁|张雨晨|刘妍君|李豆豆|咪咪豆|豆豆|吴疆|彭潇冉|胡慧琳|杨郑州|张仲光)",
        # 人名模式（匹配常见模式）
        r"[刘杨张王李黄何廖吕高邱赵][峰|浩|宇|洋|明|杰|磊|雨|泽|涛|洋|洋|鹏|军]?\s*哥?|"
        r"[王李张刘杨赵][辉|伟|明|杰|丽|芳|娜|婷]?\s*姐?|"
        r"[陈杨林何郭黄李王张刘][峰|宇|浩|洋|明|杰|磊|杰|雨|泽]?\s*哥?|"
        r"[何展逸|范昕|刘泽洋|黄谢郁|刘妍君|李豆豆|咪咪豆|豆豆|吴疆|彭潇冉|胡慧琳|杨郑州|张仲光]",
    ],

    # 悠悠相关（宠物）
    "yoyo": [
        r"悠悠|yoyo|狗狗|宠物|狗|泰迪|汪汪|叫",
    ],

    # 系统相关
    "system": [
        r"系统|配置|bug|报错|崩溃|重启|openclaw|gateway|机器人",
    ],

    # 情绪/心情相关
    "mood": [
        r"心情|开心|难过|生气|焦虑|想骂人|吐槽",
    ],

    # 莫莫咖相关
    "momonga": [
        r"莫莫咖|momonga|飞鼠|可爱|夸奖|抱抱|亲亲|干嘛|为什么",
    ],

    # 项目/工作相关
    "project": [
        r"项目|工作|开发|代码|git|commit|push|pull|bug|修复",
    ],
}

# 默认检索范围（如果不确定）
DEFAULT_SCOPE = "current"


def classify_query(query: str) -> str:
    """
    根据查询内容分类查询类型
    Returns: "food", "training", "relation", "yoyo", "system", "mood", "momonga", "project", 或 "default"
    """
    query_lower = query.lower()

    # 按优先级检查
    for query_type, patterns in QUERY_TYPES.items():
        for pattern in patterns:
            if re.search(pattern, query_lower, re.IGNORECASE):
                return query_type

    return "default"


def get_relevant_paths(query: str, top_k: int = 3) -> list:
    """
    根据查询类型返回最相关的记忆路径
    Returns: list of (path, score)
    """
    query_type = classify_query(query)
    relevant_paths = []

    # 定义各类型的检索范围（使用相对于 MEMORY_BASE 的路径）
    SCOPE_MAPPING = {
        "food": ["food", "current/food"],
        "training": ["training", "current/training"],
        "relation": ["RELATION", "connections.md"],
        "yoyo": ["RELATION/悠悠.md", "connections.md"],
        "system": ["system"],
        "mood": ["current"],
        "momonga": ["current"],
        "project": ["current", "misc"],
        "default": ["current", "2026-02"],
    }

    scopes = SCOPE_MAPPING.get(query_type, SCOPE_MAPPING["default"])

    # 扫描各个范围
    for scope in scopes:
        scope_path = MEMORY_BASE / scope
        if scope_path.exists():
            # 如果是文件
            if scope_path.is_file():
                relevant_paths.append((str(scope_path), 1.0))

            # 如果是目录，扫描所有 .md 文件
            elif scope_path.is_dir():
                # 提取人名列表
                names = re.findall(r"([刘杨张王李黄何廖吕高邱赵][峰|浩|宇|洋|明|杰|磊|雨|泽|涛|洋|洋|鹏|军]?\s*哥?|[王李张刘杨赵][辉|伟|明|杰|丽|芳|娜|婷]?\s*姐?|[何展逸|范昕|刘泽洋|黄谢郁|刘妍君|李豆豆|咪咪豆|豆豆|吴疆|彭潇冉|胡慧琳|杨郑州|张仲光])", query)

                for md_file in scope_path.rglob("*.md"):
                    # 跳过归档文件
                    if "archived" in str(md_file):
                        continue
                    # 计算相关性分数
                    score = 0.5  # 基础分数

                    # 人名匹配优先
                    if query_type == "relation":
                        file_name = md_file.stem
                        # 检查查询中是否包含文件名中的人名
                        for name in names:
                            if name and name.lower() in file_name.lower():
                                score = 1.0  # 完全匹配！
                                break
                        if score < 0.8:
                            score = 0.8  # 基础关系文件分数
                    elif query_type == "yoyo" and "悠悠" in str(md_file):
                        score = 1.0

                    relevant_paths.append((str(md_file), score))

    # 按分数排序
    relevant_paths.sort(key=lambda x: x[1], reverse=True)

    # 返回前 top_k 个路径
    return relevant_paths[:top_k]


def search_memory_smart(query: str, top_k: int = 5):
    """
    智能记忆检索 - 根据查询类型自动检索最相关的记忆
    """
    print(f"🔍 [Smart Search] Query: {query}")
    print(f"📊 Query Type: {classify_query(query)}")

    relevant_paths = get_relevant_paths(query, top_k)

    if not relevant_paths:
        print("⚠️ No relevant memory paths found.")
        return []

    print(f"📂 Relevant paths (top {len(relevant_paths)}):")
    for path, score in relevant_paths:
        print(f"   - {path} (score: {score})")

    # 这里可以继续调用 memory_skill.py 的 search 功能
    # 或者直接读取这些文件的内容

    return relevant_paths


if __name__ == "__main__":
    import sys

    if len(sys.argv) < 2:
        print("Usage: python3 memory_retrieval_strategy.py <query>")
        sys.exit(1)

    query = " ".join(sys.argv[1:])
    results = search_memory_smart(query)


path: scripts/memory_tag_search.py
content: #!/usr/bin/env python3
"""
记忆标签检索工具 (Memory Tag Search)
用法：
  python3 memory_tag_search.py "人物:刘辉"                    # 按单标签搜索
  python3 memory_tag_search.py "人物:王隆哲" "类型:开票信息"    # 多标签 AND 搜索
  python3 memory_tag_search.py --list-tags                     # 列出所有标签
  python3 memory_tag_search.py --list-tags --category 人物     # 列出某分类下的标签
"""

import os
import re
import sys
from collections import defaultdict

MEMORY_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "memory")
TAG_PATTERN = re.compile(r'\[([^:\[\]]+):([^:\[\]]+)\]')


def scan_files():
    """递归扫描 memory/ 下所有 .md 文件，返回 (filepath, lineno, line, tags) 列表"""
    results = []
    for root, _, files in os.walk(MEMORY_DIR):
        for fname in sorted(files):
            if not fname.endswith('.md'):
                continue
            fpath = os.path.join(root, fname)
            try:
                with open(fpath, 'r', encoding='utf-8') as f:
                    for lineno, line in enumerate(f, 1):
                        tags = TAG_PATTERN.findall(line)
                        if tags:
                            results.append((fpath, lineno, line.rstrip('\n'), tags))
            except (OSError, UnicodeDecodeError):
                continue
    return results


def search_tags(query_tags):
    """按标签搜索，query_tags 是 [(分类, 值), ...] 列表，AND 逻辑"""
    results = scan_files()
    matched = []
    for fpath, lineno, line, tags in results:
        tag_set = set(tags)
        if all(qt in tag_set for qt in query_tags):
            matched.append((fpath, lineno, line))
    return matched


def list_all_tags(category_filter=None):
    """列出所有标签及出现次数"""
    results = scan_files()
    tag_count = defaultdict(int)
    for _, _, _, tags in results:
        for tag in tags:
            if category_filter and tag[0] != category_filter:
                continue
            tag_count[tag] += 1
    return sorted(tag_count.items(), key=lambda x: (-x[1], x[0][0], x[0][1]))


def rel_path(fpath):
    """返回相对于工作目录的路径"""
    try:
        return os.path.relpath(fpath)
    except ValueError:
        return fpath


def main():
    args = sys.argv[1:]

    if not args:
        print(__doc__.strip())
        sys.exit(0)

    # --list-tags 模式
    if '--list-tags' in args:
        cat_filter = None
        if '--category' in args:
            idx = args.index('--category')
            if idx + 1 < len(args):
                cat_filter = args[idx + 1]
        tags = list_all_tags(cat_filter)
        if not tags:
            print("未找到任何标签。")
            sys.exit(0)
        print(f"共找到 {len(tags)} 个标签：\n")
        current_cat = None
        for (cat, val), count in tags:
            if cat != current_cat:
                if current_cat is not None:
                    print()
                print(f"【{cat}】")
                current_cat = cat
            print(f"  [{cat}:{val}]  ×{count}")
        sys.exit(0)

    # 搜索模式：解析查询标签
    query_tags = []
    for arg in args:
        # 支持 "人物:刘辉" 或 "[人物:刘辉]" 两种写法
        clean = arg.strip('[]')
        if ':' in clean:
            cat, val = clean.split(':', 1)
            query_tags.append((cat, val))
        else:
            print(f"无效标签格式: {arg}（应为 分类:值）")
            sys.exit(1)

    matched = search_tags(query_tags)

    if not matched:
        tag_str = ' AND '.join(f'[{c}:{v}]' for c, v in query_tags)
        print(f"未找到匹配 {tag_str} 的记录。")
        sys.exit(0)

    tag_str = ' AND '.join(f'[{c}:{v}]' for c, v in query_tags)
    print(f"搜索 {tag_str}，找到 {len(matched)} 条结果：\n")
    for fpath, lineno, line in matched:
        print(f"  {rel_path(fpath)}:{lineno}")
        print(f"    {line}\n")


if __name__ == '__main__':
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (HIGH)