Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
# Python bytecode files
__pycache__/
*.pyc

# Easy-memory logs
easy-memory/
21 changes: 21 additions & 0 deletions skills/.experimental/easy-memory/LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 [email protected]

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
135 changes: 135 additions & 0 deletions skills/.experimental/easy-memory/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
---
name: easy-memory
description: Project-local memory logging for history/notes stored as YYYY-MM-DD.log files in ./easy-memory relative to the current working directory. Use when you must read today's memory, search memory by keywords (English preferred), append structured log entries with IDs (English preferred; UTF-8 accepted), or update/delete entries by ID. Logs always go to the working directory even if the skill is installed globally.
---

# Easy Memory

## Overview

Maintain project-local memory logs inside the ./easy-memory directory under the current working directory. Each day uses one log file named `YYYY-MM-DD.log`.

## Initialization (must follow)

Run initialization once per project before any other scripts when:
- `./easy-memory` does not exist, or
- `./easy-memory/init.log` is missing.

Run:

```
python3 scripts/init_memory.py
```

Initialization behavior:
- Create `./easy-memory` if needed.
- Locate `AGENTS.MD` in the project root (current working directory). If missing, create it.
- If `AGENTS.MD` exists, append a rules block to the end:
```
## easy-memory rules
- At the start of the current session (before the first task), use the `easy-memory` skill and follow all rules and constraints in its `SKILL.md`.
- Only re-run memory read/search when necessary for the task.
```
- Create `./easy-memory/init.log` containing the same rules block and a `Date: YYYY-MM-DD` line at the end.
- If `init.log` already exists, initialization is skipped and `AGENTS.MD` is not modified.

If you need the absolute usage path for easy-memory scripts, use the directory
containing this `SKILL.md` (the `scripts/` folder sits alongside it). Avoid
persisting absolute paths in project `AGENTS.MD` because different environments
may maintain the same project.

All other scripts require `init.log` to exist and will exit if initialization has not been run.

## Mandatory workflow (must follow)

1. At the start of the current session (before the first task), run `scripts/read_today_log.py` to load the full log for today.
2. At the start of the current session (before the first task), run `scripts/search_memory.py` with English-preferred keywords for the session/task. Only repeat steps 1-2 when necessary for the task. Choose `--max-results` based on task complexity (this is the memory search depth).
3. Before finishing or submitting any task, append a new entry with `scripts/write_memory.py` following the log rules below.
4. Log entries should be written in English when possible; UTF-8 is accepted.

## Log entry format

Each entry is a single line and must end with a timestamp:

```
[ID:<unique-id>] [REF:<ref-level>] [FACT:<true|false>] <content> [TIME:YYYY-MM-DD:HH:MM]
```

Rules:
- Log file name must be `YYYY-MM-DD.log` and use the current date only.
- If today's log file does not exist, create it; otherwise append to the end.
- Entries should be written in English when possible; UTF-8 is accepted.
- The timestamp must be the final token of the line and must be accurate to minutes.
- Each entry must include a unique ID, a reference level, and a factual flag.

## Scripts

### Initialize memory

```
python3 scripts/init_memory.py
```

Runs one-time initialization to create `AGENTS.MD` rules and `./easy-memory/init.log`.

### Read today's log

```
python3 scripts/read_today_log.py
```

Reads the full log for the current date.

### Search memory

```
python3 scripts/search_memory.py <keyword1> <keyword2> --max-results 5
```

Searches all `.log` files in the ./easy-memory directory under the current working directory. Keywords should be English; UTF-8 is accepted. Default `--max-results` is 5.
Results are prioritized in this order:
- Factual entries (`FACT:true`) first
- Higher reference level first (`REF:critical` > `high` > `medium` > `low`, or higher numeric values)
- Newer timestamps first

### Write memory

```
python3 scripts/write_memory.py --content "..." --factual true --ref-level medium
```

Appends a new entry to today's log. Content should be English and single-line; UTF-8 is accepted. The script generates the unique ID and timestamp.

### Update memory

```
python3 scripts/update_memory.py --id <entry-id> --content "..." --ref-level high --factual false
```

Updates the entry matching the ID across all logs. The timestamp is refreshed to the current time.

Use update when:
- New factual findings contradict older memory entries (especially results from recent searches).
- The latest task outcomes refine or correct existing memory.

### Delete memory

```
python3 scripts/delete_memory.py --id <entry-id>
```

Deletes the entry matching the ID across all logs.

Use delete when:
- Older memory entries are no longer valuable or are misleading.
- A memory entry conflicts with verified facts and should be removed instead of updated.

## Log location rule

Logs are always stored under `./easy-memory` relative to the directory where you run the scripts. The skill can be installed globally; logs never go to the install directory.

## Reminder to repeat each session

- Log entries should be written in English when possible; UTF-8 is accepted.
- At the start of the current session (before the first task), run `scripts/read_today_log.py` and `scripts/search_memory.py` with English-preferred keywords; adjust `--max-results` based on task complexity. Only repeat when necessary.
- Before finishing or submitting any task, write a log entry using `scripts/write_memory.py` following the rules above.
54 changes: 54 additions & 0 deletions skills/.experimental/easy-memory/scripts/delete_memory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
from __future__ import annotations

import argparse

from memory_utils import (
list_log_files,
log_base_dir,
parse_entry_line,
require_initialized,
)


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Delete a memory entry by ID across all logs."
)
parser.add_argument("--id", required=True, help="Entry ID to delete.")
return parser.parse_args()


def main() -> int:
args = parse_args()
base_dir = log_base_dir(create=True)
require_initialized(base_dir)

matches: list[tuple] = []
for log_path in list_log_files(base_dir):
text = log_path.read_text(encoding="utf-8")
lines = text.splitlines()
for idx, line in enumerate(lines):
entry = parse_entry_line(line)
if entry and entry["id"] == args.id:
matches.append((log_path, lines, idx))

if not matches:
raise SystemExit("Entry ID not found.")
if len(matches) > 1:
raise SystemExit("Entry ID appears multiple times. Refine the logs manually.")

log_path, lines, idx = matches[0]
del lines[idx]

output = "\n".join(lines)
if output:
output += "\n"
log_path.write_text(output, encoding="utf-8")

print(f"Deleted entry ID: {args.id}")
return 0


if __name__ == "__main__":
raise SystemExit(main())
20 changes: 20 additions & 0 deletions skills/.experimental/easy-memory/scripts/init_memory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env python3
from __future__ import annotations

from memory_utils import ensure_initialized, init_log_path, log_base_dir


def main() -> int:
base_dir = log_base_dir(create=True)
init_log = init_log_path(base_dir)
if init_log.exists():
print("Initialization already completed.")
return 0

ensure_initialized(base_dir)
print(f"Initialized easy-memory in {base_dir}.")
return 0


if __name__ == "__main__":
raise SystemExit(main())
138 changes: 138 additions & 0 deletions skills/.experimental/easy-memory/scripts/memory_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
from __future__ import annotations

import re
from datetime import date, datetime
from pathlib import Path
from typing import Optional

ENTRY_RE = re.compile(
r"^\[ID:(?P<id>[^\]]+)\] "
r"\[REF:(?P<ref>[^\]]+)\] "
r"\[FACT:(?P<factual>true|false)\] "
r"(?P<content>.*) "
r"\[TIME:(?P<ts>\d{4}-\d{2}-\d{2}:\d{2}:\d{2})\]$"
)

_REF_LEVEL_RE = re.compile(r"^[A-Za-z0-9._-]+$")
INIT_LOG_NAME = "init.log"
AGENTS_FILE_NAME = "AGENTS.MD"


def log_base_dir(create: bool = False) -> Path:
base_dir = Path.cwd() / "easy-memory"
if create:
base_dir.mkdir(parents=True, exist_ok=True)
return base_dir


def log_path_for_date(log_date: date, base_dir: Path) -> Path:
return base_dir / f"{log_date.strftime('%Y-%m-%d')}.log"


def list_log_files(base_dir: Path) -> list[Path]:
if not base_dir.exists():
return []
return sorted(base_dir.glob("*.log"), reverse=True)


def init_log_path(base_dir: Path) -> Path:
return base_dir / INIT_LOG_NAME


def init_rules_block() -> str:
return "\n".join(
[
"## easy-memory rules",
"- At the start of the current session (before the first task), use the "
"`easy-memory` skill and follow all rules and constraints in its "
"`SKILL.md`.",
"- Only re-run memory read/search when necessary for the task.",
]
)


def ensure_initialized(base_dir: Path) -> None:
init_log = init_log_path(base_dir)
if init_log.exists():
return

base_dir.mkdir(parents=True, exist_ok=True)

rules_block = init_rules_block()
agents_path = Path.cwd() / AGENTS_FILE_NAME
if agents_path.exists():
existing = agents_path.read_text(encoding="utf-8")
if existing and not existing.endswith("\n"):
existing += "\n"
if existing.strip():
existing += "\n"
existing += f"{rules_block}\n"
agents_path.write_text(existing, encoding="utf-8")
else:
agents_path.write_text(f"{rules_block}\n", encoding="utf-8")

date_stamp = date.today().isoformat()
init_log_content = f"{rules_block}\nDate: {date_stamp}\n"
init_log.write_text(init_log_content, encoding="utf-8")


def require_initialized(base_dir: Path) -> None:
init_log = init_log_path(base_dir)
if not base_dir.exists() or not init_log.exists():
raise SystemExit(
"Initialization required. Run `python3 scripts/init_memory.py` "
"from the project root."
)


def ensure_single_line(text: str, label: str) -> None:
if "\n" in text or "\r" in text:
raise SystemExit(f"{label} must be a single line.")


def normalize_bool(value: str) -> bool:
normalized = value.strip().lower()
if normalized == "true":
return True
if normalized == "false":
return False
raise SystemExit("factual must be 'true' or 'false'.")


def validate_ref_level(value: str) -> str:
if not value:
raise SystemExit("ref-level must be a non-empty string.")
if not _REF_LEVEL_RE.match(value):
raise SystemExit("ref-level must match [A-Za-z0-9._-]+.")
return value


def format_timestamp(dt: datetime) -> str:
return dt.strftime("%Y-%m-%d:%H:%M")


def format_entry_line(
entry_id: str,
ref_level: str,
factual: bool,
content: str,
timestamp: str,
) -> str:
fact_value = "true" if factual else "false"
return (
f"[ID:{entry_id}] [REF:{ref_level}] [FACT:{fact_value}] {content} "
f"[TIME:{timestamp}]"
)


def parse_entry_line(line: str) -> Optional[dict]:
match = ENTRY_RE.match(line.strip())
if not match:
return None
return {
"id": match.group("id"),
"ref": match.group("ref"),
"factual": match.group("factual") == "true",
"content": match.group("content"),
"timestamp": match.group("ts"),
}
Loading