mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-15 05:26:28 +03:00
6fe5aed86e
Rename the skill directory to writers-helper and update the name field, eval manifest skill_name, skill README title and example paths, root README catalog entry, and the skill-triggers index. Regenerate llms.txt, .claude-plugin/marketplace.json, .codex-plugin/plugin.json, and .agents/plugins/marketplace.json from their generators. Content is unchanged. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
196 lines
6.6 KiB
Python
196 lines
6.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Journal writing sessions and report progress.
|
|
|
|
Appends one session record per call to a JSONL file (default
|
|
~/.writing-habits.jsonl) and reports day/week/month totals, averages,
|
|
streaks, and best days. Non-interactive; --dry-run previews an append.
|
|
|
|
Examples:
|
|
habit-log.py --log 2026-08-08 850 45 7 "finished act two"
|
|
habit-log.py --report week
|
|
habit-log.py --report month --json
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import date, datetime, timedelta
|
|
from pathlib import Path
|
|
|
|
DEFAULT_FILE = os.path.join(Path.home(), ".writing-habits.jsonl")
|
|
FIELDS = ["date", "words", "minutes", "energy", "notes"]
|
|
|
|
|
|
def load_records(path):
|
|
records = []
|
|
if not path.is_file():
|
|
return records
|
|
with path.open(encoding="utf-8") as handle:
|
|
for line_number, line in enumerate(handle, start=1):
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
try:
|
|
record = json.loads(line)
|
|
except json.JSONDecodeError as error:
|
|
print(
|
|
f"warning: skipping malformed line {line_number} in {path}: {error}",
|
|
file=sys.stderr,
|
|
)
|
|
continue
|
|
records.append(record)
|
|
return records
|
|
|
|
|
|
def save_records(path, records):
|
|
with path.open("w", encoding="utf-8") as handle:
|
|
for record in records:
|
|
handle.write(json.dumps(record, sort_keys=True) + "\n")
|
|
|
|
|
|
def add_record(path, words, minutes, energy, notes, log_date):
|
|
records = load_records(path)
|
|
record = {
|
|
"date": log_date,
|
|
"words": words,
|
|
"minutes": minutes,
|
|
"energy": energy,
|
|
"notes": notes,
|
|
}
|
|
records.append(record)
|
|
save_records(path, records)
|
|
return record
|
|
|
|
|
|
def parse_date(value):
|
|
try:
|
|
return datetime.strptime(value, "%Y-%m-%d").date()
|
|
except ValueError as error:
|
|
raise ValueError(f"invalid date {value!r}, expected YYYY-MM-DD") from error
|
|
|
|
|
|
def summarize(records, start_date, label):
|
|
window = [r for r in records if r.get("date", "") >= start_date.isoformat()]
|
|
words = sum(int(r.get("words", 0)) for r in window)
|
|
minutes = sum(int(r.get("minutes", 0)) for r in window)
|
|
sessions = len(window)
|
|
days = {r.get("date", "") for r in window}
|
|
|
|
# Streak of consecutive writing days ending today or yesterday.
|
|
streak = 0
|
|
cursor = date.today()
|
|
if cursor.isoformat() not in days:
|
|
cursor = cursor - timedelta(days=1)
|
|
while cursor.isoformat() in days:
|
|
streak += 1
|
|
cursor = cursor - timedelta(days=1)
|
|
|
|
energy_values = [int(r["energy"]) for r in window if r.get("energy") is not None]
|
|
best = None
|
|
if window:
|
|
best = max(window, key=lambda r: int(r.get("words", 0)))
|
|
best = {"date": best.get("date", ""), "words": int(best.get("words", 0))}
|
|
|
|
return {
|
|
"period": label,
|
|
"sessions": sessions,
|
|
"writing_days": len(days),
|
|
"words": words,
|
|
"minutes": minutes,
|
|
"avg_words_per_session": round(words / sessions, 0) if sessions else 0,
|
|
"avg_minutes_per_session": round(minutes / sessions, 1) if sessions else 0,
|
|
"avg_energy": round(sum(energy_values) / len(energy_values), 1) if energy_values else None,
|
|
"current_streak_days": streak,
|
|
"best_day": best,
|
|
}
|
|
|
|
|
|
def main(argv=None):
|
|
parser = argparse.ArgumentParser(description="Journal writing sessions and report progress.")
|
|
parser.add_argument("--file", default=DEFAULT_FILE, help="Path to the JSONL log.")
|
|
parser.add_argument(
|
|
"--log",
|
|
nargs="+",
|
|
metavar="VALUE",
|
|
help="Append a session: DATE WORDS MINUTES ENERGY [NOTES...]",
|
|
)
|
|
parser.add_argument("--report", choices=["day", "week", "month"], help="Report period.")
|
|
parser.add_argument("--json", action="store_true", help="Emit JSON.")
|
|
parser.add_argument("--dry-run", action="store_true", help="Preview an append without writing.")
|
|
args = parser.parse_args(argv)
|
|
|
|
path = Path(args.file).expanduser()
|
|
|
|
if args.log:
|
|
if len(args.log) < 4:
|
|
parser.error("--log needs DATE WORDS MINUTES ENERGY [NOTES...]")
|
|
try:
|
|
log_date = parse_date(args.log[0]).isoformat()
|
|
except ValueError as error:
|
|
parser.error(str(error))
|
|
try:
|
|
words = int(args.log[1])
|
|
minutes = int(args.log[2])
|
|
energy = int(args.log[3])
|
|
except ValueError:
|
|
parser.error("WORDS, MINUTES, and ENERGY must be integers")
|
|
if words < 0 or minutes < 0:
|
|
parser.error("WORDS and MINUTES must be >= 0")
|
|
if not 1 <= energy <= 10:
|
|
parser.error("ENERGY must be 1-10")
|
|
notes = " ".join(args.log[4:])
|
|
if args.dry_run:
|
|
print(
|
|
f"would append to {path}: {log_date}, {words} words, "
|
|
f"{minutes} min, energy {energy}, notes {notes!r}"
|
|
)
|
|
return 0
|
|
try:
|
|
record = add_record(path, words, minutes, energy, notes, log_date)
|
|
except OSError as error:
|
|
print(f"error: cannot write {path}: {error}", file=sys.stderr)
|
|
return 1
|
|
if args.json:
|
|
print(json.dumps(record, indent=2))
|
|
else:
|
|
print(f"logged {log_date}: {words} words in {minutes} min, energy {energy}")
|
|
return 0
|
|
|
|
if args.report:
|
|
try:
|
|
records = load_records(path)
|
|
except OSError as error:
|
|
print(f"error: cannot read {path}: {error}", file=sys.stderr)
|
|
return 1
|
|
today = date.today()
|
|
if args.report == "day":
|
|
result = summarize(records, today, "day")
|
|
elif args.report == "week":
|
|
result = summarize(records, today - timedelta(days=6), "week")
|
|
else:
|
|
result = summarize(records, today - timedelta(days=29), "month")
|
|
if args.json:
|
|
print(json.dumps(result, indent=2))
|
|
return 0
|
|
print(
|
|
f"{result['period'].title()} report: {result['sessions']} sessions over "
|
|
f"{result['writing_days']} days, {result['words']} words, {result['minutes']} minutes"
|
|
)
|
|
print(
|
|
f"Avg {int(result['avg_words_per_session'])} words/session, "
|
|
f"{result['avg_minutes_per_session']} min/session, energy {result['avg_energy']}"
|
|
)
|
|
print(
|
|
f"Streak: {result['current_streak_days']} days Best day: "
|
|
f"{result['best_day']['date']} ({result['best_day']['words']} words)"
|
|
)
|
|
return 0
|
|
|
|
parser.error("either --log or --report is required")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|