Zalohovani vsech podstatnych souboru

This commit is contained in:
lachtan
2026-06-10 06:39:52 +02:00
parent 1e10891945
commit 67e29c8b88
69 changed files with 9115 additions and 0 deletions

88
skills/project/SKILL.md Normal file
View File

@@ -0,0 +1,88 @@
---
name: project
aliases: [proj]
description: >
Project management — long-running things with notes, next-steps, and status.
Use when user mentions "project".
---
# Project
File-backed project store in `projects/`. Each project is one markdown file
with YAML frontmatter (`status`, `priority`, `created`, `slug`) and a free-form
body for notes and next-steps.
## Backend
`skills/project/scripts/project.py` — deterministic CRUD for frontmatter and
basic operations. Agent handles all body edits via `edit_file` / `apply_patch`.
## Commands
### `project add <název>` — create
1. Run: `uv run skills/project/scripts/project.py add "<název>" [--priority high|medium|low]`
2. Default priority is `medium`.
3. Echo: `Created project '<slug>' (priority: <priority>)`
### `project list` — list active
1. Run: `uv run skills/project/scripts/project.py list`
2. Echo JSON output. Format as:
```
Active projects:
- <slug> (priority: high) — <first line of body / project name>
```
3. If empty → "No active projects."
### `project show <slug>` — display
1. Run: `uv run skills/project/scripts/project.py show <slug>`
2. Echo the full markdown file.
### `project status <slug> <active|paused|done>` — change status
1. Run: `uv run skills/project/scripts/project.py status <slug> <status>`
2. Echo: `Project '<slug>' is now <status>.`
### `project next <slug> <text>` — set next step
1. Read the project file.
2. Use `edit_file` to replace the content under `## Další krok` with the new text.
3. If the section does not exist, add it before the end of the file.
4. Echo: `Next step for '<slug>' updated.`
### `project note <slug> <text>` — add a note
1. Read the project file.
2. Use `edit_file` to append a bullet under `## Poznámky`:
`- <today>: <text>`
3. If `## Poznámky` does not exist, add it after the first heading.
4. Echo: `Note added to '<slug>'.`
### `project switch <slug>` — session context
1. Run `my(action="set", key="project_context", value="<slug>")`.
2. Echo: `Switched to project '<slug>'. Next project commands without slug will use this context.`
3. If a command is missing a slug and `project_context` is set, use it automatically.
## Rules
- **Slug** = kebab-case from first 4 words of the name. Used as filename (`<slug>.md`).
- **Frontmatter** is read-only for the agent — never edit it directly in the file.
Use `project.py status` to change status.
- **Body edits** (notes, next-step, structure changes) are always done by the agent
via `edit_file` / `apply_patch`.
- **No database** — pure markdown files. Git-friendly, one commit per change.
- **Session context** (`project switch`) lives only in `my` scratchpad and is lost
on restart. Re-run `project switch` after restart if needed.
- **Priority** = `high` | `medium` | `low`. `list` sorts by priority (high first).
- **Status** = `active` | `paused` | `done`. `list` shows only `active`.
## Edge cases
- `project add` with existing slug → error, do not overwrite.
- `project show` / `project status` / `project next` / `project note` with
missing slug → "Project '<slug>' not found."
- Missing `## Poznámky` or `## Další krok` → agent creates the section.
- Empty `projects/` → `list` returns empty array.

View File

@@ -0,0 +1,184 @@
#!/usr/bin/env -S uv run --script
# /// script
# dependencies = ["pyyaml"]
# ///
"""
project.py — backend for /project skill.
Deterministic CRUD for project markdown files with YAML frontmatter.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from datetime import date
from pathlib import Path
import yaml
WORKSPACE = Path(__file__).resolve().parent.parent.parent.parent
PROJECTS_DIR = WORKSPACE / "projects"
# Valid statuses and priorities
STATUSES = {"active", "paused", "done"}
PRIORITIES = {"high", "medium", "low"}
def _slugify(name: str) -> str:
"""Kebab-case slug from first few words of name. Max 4 words."""
words = re.sub(r"[^a-zA-Z0-9\s]", "", name).lower().split()
words = words[:4]
return "-".join(words) if words else "project"
def _list_projects() -> list[dict]:
"""Parse frontmatter from all .md files in projects/."""
if not PROJECTS_DIR.exists():
return []
projects = []
for path in sorted(PROJECTS_DIR.glob("*.md")):
text = path.read_text(encoding="utf-8")
frontmatter, _ = _split_frontmatter(text)
if frontmatter:
meta = yaml.safe_load(frontmatter) or {}
meta["_file"] = path.name
projects.append(meta)
return projects
def _split_frontmatter(text: str) -> tuple[str | None, str]:
"""Split YAML frontmatter from body. Returns (frontmatter_yaml, body)."""
if not text.startswith("---\n"):
return None, text
end = text.find("\n---\n", 4)
if end == -1:
return None, text
return text[4:end], text[end + 5 :]
def _load_file(slug: str) -> tuple[Path, str, str | None, str]:
"""Load project file. Returns (path, full_text, frontmatter_yaml, body)."""
path = PROJECTS_DIR / f"{slug}.md"
if not path.exists():
raise FileNotFoundError(f"Project '{slug}' not found ({path.name})")
text = path.read_text(encoding="utf-8")
fm, body = _split_frontmatter(text)
return path, text, fm, body
def _write_file(path: Path, frontmatter: dict, body: str) -> None:
"""Write project file with YAML frontmatter."""
fm_yaml = yaml.safe_dump(frontmatter, allow_unicode=True, sort_keys=False, default_flow_style=False)
path.write_text(f"---\n{fm_yaml}---\n{body}", encoding="utf-8")
# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------
def cmd_add(args: argparse.Namespace) -> int:
name = (args.name or "").strip()
if not name:
print(json.dumps({"error": "name must not be empty"}), file=sys.stderr)
return 1
slug = _slugify(name)
PROJECTS_DIR.mkdir(parents=True, exist_ok=True)
path = PROJECTS_DIR / f"{slug}.md"
if path.exists():
print(json.dumps({"error": f"project '{slug}' already exists"}), file=sys.stderr)
return 1
priority = (args.priority or "medium").lower()
if priority not in PRIORITIES:
print(json.dumps({"error": f"invalid priority '{priority}' — use high/medium/low"}), file=sys.stderr)
return 1
frontmatter = {
"status": "active",
"priority": priority,
"created": date.today().isoformat(),
"slug": slug,
}
body = f"# {name}\n\n## Poznámky\n\n## Další krok\n\n"
_write_file(path, frontmatter, body)
print(json.dumps({"added": {"slug": slug, "name": name, "path": str(path.relative_to(WORKSPACE))}}, ensure_ascii=False))
return 0
def cmd_list(_args: argparse.Namespace) -> int:
projects = _list_projects()
active = [p for p in projects if p.get("status") == "active"]
# Sort by priority: high > medium > low
priority_order = {"high": 0, "medium": 1, "low": 2}
active.sort(key=lambda p: priority_order.get(p.get("priority", "medium"), 1))
print(json.dumps({"projects": active}, ensure_ascii=False))
return 0
def cmd_show(args: argparse.Namespace) -> int:
slug = (args.slug or "").strip()
try:
_path, text, _fm, _body = _load_file(slug)
except FileNotFoundError as exc:
print(json.dumps({"error": str(exc)}), file=sys.stderr)
return 1
print(text)
return 0
def cmd_status(args: argparse.Namespace) -> int:
slug = (args.slug or "").strip()
new_status = (args.status or "").strip().lower()
if new_status not in STATUSES:
print(json.dumps({"error": f"invalid status '{new_status}' — use active/paused/done"}), file=sys.stderr)
return 1
try:
path, text, fm_yaml, body = _load_file(slug)
except FileNotFoundError as exc:
print(json.dumps({"error": str(exc)}), file=sys.stderr)
return 1
if not fm_yaml:
print(json.dumps({"error": "no frontmatter found"}), file=sys.stderr)
return 1
frontmatter = yaml.safe_load(fm_yaml) or {}
frontmatter["status"] = new_status
_write_file(path, frontmatter, body)
print(json.dumps({"updated": {"slug": slug, "status": new_status}}, ensure_ascii=False))
return 0
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> int:
parser = argparse.ArgumentParser(description="Project file backend")
sub = parser.add_subparsers(dest="command", required=True)
p_add = sub.add_parser("add", help="Create a new project")
p_add.add_argument("name", help="Project name")
p_add.add_argument("--priority", default="medium", help="Priority: high/medium/low")
sub.add_parser("list", help="List active projects")
p_show = sub.add_parser("show", help="Show full project file")
p_show.add_argument("slug", help="Project slug")
p_status = sub.add_parser("status", help="Change project status")
p_status.add_argument("slug", help="Project slug")
p_status.add_argument("status", help="New status: active/paused/done")
args = parser.parse_args()
dispatch = {"add": cmd_add, "list": cmd_list, "show": cmd_show, "status": cmd_status}
return dispatch[args.command](args)
if __name__ == "__main__":
sys.exit(main())