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"