#!/usr/bin/env bash # sysjournal — shared system-knowledge journal for the Obsidian vault. # # One tool, used by every coding agent (Claude Code, Codex, Pi), so host/ # environment changes get recorded in ONE consistent, greppable place with # the frontmatter schema that "Tech/Infrastructure/System Log MOC.md" indexes # via Dataview (date, machine, type, status, tags, review-by). # # Journal HOST/ENVIRONMENT changes — system config, services, networking, # VMs, drivers, build toolchains, host-wiring of an app. NOT ordinary work # inside a code repo. See `sysjournal help`. # # Portable bash (invoked by agents in varied environments), not zsh. set -euo pipefail VAULT="${SYSJOURNAL_VAULT:-$HOME/Documents/Obsidian25}" SUBDIR="${SYSJOURNAL_SUBDIR:-Tech/Infrastructure}" INFRA="$VAULT/$SUBDIR" die() { printf 'sysjournal: %s\n' "$*" >&2; exit 1; } ensure_dir() { [ -d "$VAULT" ] || die "vault not found: $VAULT (set SYSJOURNAL_VAULT)" mkdir -p "$INFRA" } usage() { cat <<'EOF' sysjournal — record & recall host/environment changes in the Obsidian vault. USAGE sysjournal search Recall: grep the journal first (case-insensitive) sysjournal new "" [options] Scaffold a new note, print its path sysjournal list [N] List the N most-recent notes (default 20) sysjournal path Print the infrastructure folder path sysjournal help This help `new` OPTIONS --type <t> change | setup | debug | fix | incident | note (default: change) --status <s> deployed | resolved | unresolved | workaround | planned (default: deployed) --tags a,b,c comma-separated tags --machine <m> host the change was made on (default: `hostname -s`) --review-by <YYYY-MM-DD> optional follow-up/expiry date --summary "<one line>" optional lead line WHAT TO JOURNAL YES system config, services/daemons (systemd/launchd/cron), networking/DNS/ VPN/firewall, VMs & host containers, drivers/kernel/boot, disks/mounts, build toolchains & global package installs, wiring an app into the host. NO feature work, bug fixes, refactors, tests INSIDE a project repo. Discriminator: does it change state outside the repo, on the host? If no, skip. Straddle (build an app AND install it as a service): journal only the host-wiring part (the unit/cron/port), not the app code. EXAMPLES sysjournal search systemd relay port sysjournal new "WireGuard VPN to homelab" --type setup --tags wireguard,vpn,network sysjournal new "DNS resolution flaky after netplan change" --type debug --status unresolved EOF } cmd_search() { ensure_dir [ "$#" -ge 1 ] || die "search needs at least one keyword" # OR-match the keywords so a few loosely-related terms still surface notes. local pattern pattern=$(printf '%s|' "$@"); pattern="${pattern%|}" echo "# Journal matches in $SUBDIR for: $*" echo if ! rg -i --no-heading -n -C1 --color never -e "$pattern" "$INFRA" 2>/dev/null; then echo "(no matches — nothing journaled on this yet)" fi } cmd_list() { ensure_dir local n="${1:-20}" # Newest first by mtime; strip the vault prefix for readability. find "$INFRA" -maxdepth 1 -name '*.md' -printf '%T@ %p\n' 2>/dev/null \ | sort -rn | head -n "$n" | sed "s#[0-9.]* $INFRA/##" } cmd_path() { echo "$INFRA"; } cmd_new() { ensure_dir local title="" type="change" status="deployed" tags="" machine review_by="" summary="" machine="$(hostname -s 2>/dev/null || hostname)" # First non-flag arg is the title. while [ "$#" -gt 0 ]; do case "$1" in --type) type="${2:?--type needs a value}"; shift 2;; --status) status="${2:?--status needs a value}"; shift 2;; --tags) tags="${2:?--tags needs a value}"; shift 2;; --machine) machine="${2:?--machine needs a value}"; shift 2;; --review-by) review_by="${2:?--review-by needs a value}"; shift 2;; --summary) summary="${2:?--summary needs a value}"; shift 2;; --*) die "unknown option: $1";; *) [ -z "$title" ] && title="$1" || die "unexpected arg: $1"; shift;; esac done [ -n "$title" ] || die 'new needs a "<Title>"' # Filename: keep the human title (Obsidian-friendly), drop only path-illegal chars. local fname; fname=$(printf '%s' "$title" | tr '/\\' '--' | sed 's/[[:cntrl:]]//g; s/ */ /g; s/^ *//; s/ *$//') local file="$INFRA/$fname.md" if [ -e "$file" ]; then echo "$file" # already exists — recall, don't clobber; edit/append this note. echo "sysjournal: note already exists — edit it instead of creating a duplicate." >&2 return 0 fi # YAML frontmatter — inline tag list, matching the MOC's own `tags: [moc]` style. local yaml_tags="[]" if [ -n "$tags" ]; then yaml_tags="[$(printf '%s' "$tags" | sed 's/ *, */, /g')]" fi local date_today; date_today="$(date +%F)" { echo "---" echo "date: $date_today" echo "machine: $machine" echo "type: $type" echo "status: $status" echo "tags: $yaml_tags" [ -n "$review_by" ] && echo "review-by: $review_by" echo "---" echo echo "# $title" echo [ -n "$summary" ] && { echo "$summary"; echo; } echo "## Why" echo echo "## What changed" echo echo "## Design decisions / gotchas" echo echo "## Verify" echo echo '```' echo '# command run + observed result' echo '```' echo echo "## Status / follow-ups" echo echo "Related: " } > "$file" echo "$file" } main() { local sub="${1:-help}"; shift || true case "$sub" in search|recall|grep) cmd_search "$@";; new|add) cmd_new "$@";; list|ls) cmd_list "$@";; path|dir) cmd_path;; help|-h|--help) usage;; *) usage; die "unknown command: $sub";; esac } main "$@"