tankslab.com/logbook/source

bin/paths.py

115 lines, exactly as they ship in the download.

""" paths.py - where Logbook keeps things, on whichever machine you are on. WHY THIS EXISTS --------------- The internal build this grew from hardcoded /opt/friday/logbook. That is correct for a service on a box we own and wrong for something a stranger installs: it is not writable without root, it does not exist on Windows, and it puts a person's data next to the program instead of in their own space. WHERE THE DATA GOES, and this is what the README promises: Windows %LOCALAPPDATA%\\friday-logbook (usually C:\\Users\\<you>\\AppData\\Local) Linux $XDG_DATA_HOME/friday-logbook, else ~/.local/share/friday-logbook macOS ~/Library/Application Support/friday-logbook One directory, per user, in the place that platform already uses for application data. `logbook wipe` deletes exactly that directory, which is why it can honestly say "nothing of Logbook's remains". FRIDAY_LOGBOOK_ROOT overrides all of it, so you can put the store on an encrypted volume, a USB stick, or nowhere near your home directory if you would rather. """ from __future__ import annotations import os import sys from pathlib import Path # ⚠️ EVERY read_text/write_text/open IN THIS PACKAGE PASSES encoding="utf-8". # # Python on Windows does not default to UTF-8 - it uses the machine's legacy code # page. So a file this tool wrote as UTF-8 comes back as a UnicodeDecodeError on the # very next read, and a file it rewrites can be written back MANGLED. It bit the test # suite first, on the harmless bytes in a comment. # # The dangerous instance was in wipe.py, which reads the user's settings.json and # writes it back: on a Windows machine with anything non-ASCII in that file - an # accented path, a name in another language - the command whose whole job is to leave # the machine clean would have corrupted their editor configuration instead. Never # omit it, even where today's content happens to be ASCII: transcripts carry whatever # language the person writes in. APP = "friday-logbook" # This tool shipped as "Friday Recall" until 2026-10-20. OLD_APP / OLD_DB are the ONLY place the old # name survives: an install that still has the old data folder gets it renamed once, in place, the first # time the new version looks for its root - so nobody's logbook is left behind by the rename. OLD_APP = "friday-" + "recall" OLD_DB = "recall" + ".db" def _base(app: str) -> Path: if sys.platform.startswith("win"): base = os.environ.get("LOCALAPPDATA") or os.environ.get("APPDATA") return Path(base) / app if base else Path.home() / "AppData" / "Local" / app if sys.platform == "darwin": return Path.home() / "Library" / "Application Support" / app xdg = os.environ.get("XDG_DATA_HOME") return (Path(xdg) / app) if xdg else Path.home() / ".local" / "share" / app def _migrate_old_name(new: Path) -> None: """Rename the pre-rename data FOLDER once (one atomic rename). Never merges, never overwrites: if the new folder already exists, the old one is left exactly where it is. The database files inside are NOT renamed (Anvil redC): renaming a SQLite file apart from its -wal/-shm can strand recent writes - db() opens whichever name is there.""" old = _base(OLD_APP) if new.exists() or not old.is_dir(): return try: old.rename(new) except OSError: pass # a failed rename leaves the old folder intact; the tool starts fresh beside it def root() -> Path: env = os.environ.get("FRIDAY_LOGBOOK_ROOT") if env: return Path(env).expanduser() new = _base(APP) _migrate_old_name(new) return new def ensure_root() -> Path: r = root() r.mkdir(parents=True, exist_ok=True) return r def db() -> Path: env = os.environ.get("FRIDAY_LOGBOOK_DB") if env: return Path(env).expanduser() r = root() # a migrated install keeps its database under the old file name (never renamed apart from its -wal/-shm) return r / OLD_DB if (r / OLD_DB).exists() and not (r / "logbook.db").exists() else r / "logbook.db" def archive_dir() -> Path: return root() / "archive" def log_file() -> Path: return root() / "hook.log" def report_file() -> Path: return root() / "last_failure.txt" def transcripts_root() -> Path: """Claude Code's own project directory. Logbook READS this and never writes to it.""" return Path.home() / ".claude" / "projects"