OpenClaw: A Practical Guide to Running a Local Autonomous AI Agent — Part 5: Building a Custom Skill — mac-setup-master
Nine people can’t make a baby in a month, (regarding the addition of more programmers to get a project completed faster)– Fred Brooks
mac-setup-master
Step 1. Directory Layout.
mac-setup-master/
├── SKILL.md
├── requirements.txt # required for `uv pip install -r`
├── scripts/
│ └── mysystem.py
├── references/ # loaded on demand (large / situational)
├── troubleshooting.md
├── glossary.md
└── cached-answers/ # ⭐ dated, 7-day TTL cache
├── system-overview.md
├── aerospace-keybindings.md
└── ...
└── assets/ # loaded on demand
├── zsh-reference.md
[...]
A skill is a self-contained directory with one entry file (SKILL.md) and up to three optional subdirectories. The agent loads SKILL.md first; everything else is loaded on demand.
| Component | Role | When loaded |
|---|---|---|
SKILL.md frontmatter |
name, description, Trigger, compatibility |
Always (at discovery) |
SKILL.md body |
Instructions the agent follows | Always (when the skill fires) |
scripts/ |
Executable helpers (here: mysystem.py) |
When the agent needs live data |
references/ |
Deep reference docs (troubleshooting, glossary, cached answers) | Only when routing says so |
assets/ |
Static templates, images, quick-ref cards | Only when explicitly referenced |
This skill deliberately omits assets/ — all static content lives in references/, and no templates or images are shipped.
Rule of thumb: scripts/ produces data; references/ explains data; SKILL.md orchestrates both. Keep the entry file lean and put depth in references/.
Step 2. SKILL.md
---
name: mac-setup-master
description: Explains this Mac's setup, shell shortcuts and functions, AeroSpace keybindings, Espanso text expansions, Raycast commands, and Spotlight behaviour using the user's local configuration files (with GNU Stow symlink awareness).
Trigger: User asks about their macOS setup, shortcuts, keybindings, or configuration. Typical triggers are "How is my Mac configured?", "Show me my AeroSpace keybindings", "What shortcuts do I have?", "Check my Espanso setup", "Review my Zsh aliases and functions", "Run system overview".
compatibility: macOS 13+ (Ventura), Python 3.11+, zsh 5.8+
Last modified: 2026-11-12
---
Mac Setup Guide & Diagnostics
Help the user understand **their own** macOS setup by running read-only
diagnostic scripts and inspecting local dotfiles. Prefer exact data extracted
via `scripts/mysystem.py` or a terminal command over assumptions or generic
macOS defaults.
> ⚠️ **Read-only contract.** This skill explains configuration. It **never**
> executes shortcuts, never runs structural scripts (`aerospace.toml`,
> `.zshrc`), and never modifies dotfiles. If a user asks "run my AeroSpace
> reload", explain the command instead.
## 0. Quick start — choosing the right path
| User asks… | Do this | Output |
|---|---|---|
| "System overview" / "How is my Mac set up?" | `python scripts/mysystem.py info` | Table of OS, arch, RAM, disk, versions |
| "Show my AeroSpace keybindings" | `python scripts/mysystem.py aerospace` | Grouped by mode |
| "What Espanso triggers do I have?" | `python scripts/mysystem.py espanso` | Trigger → replacement list |
| "List my OpenClaw skills" | `python scripts/mysystem.py skills` | Name + description + path |
| "What aliases do I have?" | `zsh -ic "alias"` | Live alias dump |
| "Show me my `my_func` function" | `zsh -ic "which my_func"` | Function source |
| "What does Cmd+Enter do?" | Read `~/macOS/scripts/help.sh`, `sec_keyboard` | Shortcut → action, with `file#line` |
| "Where is `aerospace.toml`?" | Resolve via `readlink -f` (Stow-aware) | Real path under `~/macOS` |
If the user's question is ambiguous, **prefer `mysystem.py all`** and then summarize only the relevant section.
Always check the cache in §2 before running anything.
## 1. Prerequisites & Installation
`scripts/mysystem.py` needs **Python 3.11+** (for `tomllib`) and `PyYAML`.
```bash
cd ~/macOS/claude/skills/mac-setup-master
python3 -m venv .venv && source .venv/bin/activate
uv pip install -r requirements.txt
```
`requirements.txt` contains:
```text
PyYAML>=6.0
```
**Fallback if the venv is unavailable:** `python3 -m pip install --user pyyaml`
and confirm Python ≥ 3.11 via `python3 -V`. If the machine only has 3.9/3.10,
`mysystem.py` will exit with a clear message — surface that to the user
verbatim instead of guessing.
## 2. Cached answers — one-week freshness window
Every answer this skill produces is **cached on disk with a date stamp**.
The agent's first action is always to check the cache:
- **Fresh (< 7 days old)** → return the cached markdown verbatim. Do **not**
re-run `mysystem.py`, do **not** re-read `help.sh`, do **not** re-query `zsh`.
- **Stale (≥ 7 days old) or missing** → run the diagnostics, generate a fresh
answer, write it back to the cache, then return it.
### 2.1 Cache location and naming
`references/cached-answers/.md` — one file per answer topic.
| Topic key | Covers |
|---|---|
| `system-overview` | `get_sys_info()` output |
| `aerospace-keybindings` | `parse_aerospace()` output |
| `espanso-triggers` | `parse_espanso()` output |
| `skills-list` | `parse_skills()` output |
| `shortcuts` | `help.sh` sections (Spotlight, Raycast, keyboard) |
| `zsh-aliases` | `get_zsh_aliases()` output |
| `zsh-functions` | `get_zsh_functions()` output |
For a question that doesn't map to one of these keys, derive a slug from the
question (lowercase, hyphens) and use it.
### 2.2 Cache file format
```md
---
topic: aerospace-keybindings
created: 2026-11-14 # first generation — never overwritten
last_checked: 2026-11-14 # refreshed on every regeneration
tool: mysystem.py
---
# AeroSpace keybindings
**Mode `main`** — *Window Management* [Configured]
- `alt-h` → focus left (`~/macOS/aerospace/aerospace.toml#L12`)
...
```
`created` never changes; `last_checked` is stamped to today on every write.
Do **not** write these fields by hand — `mysystem.py` fills them in.
### 2.3 Workflow
```
topic = derive_topic(user_question)
status = mysystem.cache_status(topic) # "fresh" | "stale" | "missing"
if status["state"] == "fresh":
return mysystem.read_cache(topic) # verbatim, no diagnostics
else:
answer = generate_answer(topic) # calls mysystem.py + help.sh
mysystem.write_cache(topic, answer) # stamps created/last_checked
return answer
```
### 2.4 Commands the agent runs
```bash
# 1. Check the cache status
python scripts/mysystem.py cache _topic_ --status
# Status: fresh (age 3 days, created 2026-11-11)
# Status: stale (age 12 days, created 2026-11-02)
# Status: missing
# 2. Return the cached answer if fresh (exit 0 = fresh, exit 2 = stale/missing)
python scripts/mysystem.py cache _topic_
# 3. Write the freshly generated answer back
python scripts/mysystem.py cache _topic_ --write - <<'EOF'
_generated markdown, frontmatter optional_
EOF
# Written: references/cached-answers/.md (created 2026-11-14, last_checked 2026-11-14)
```
The `--write` flag accepts `-` (stdin) or a file path. Frontmatter in the
input is stripped and replaced with fresh metadata.
### 2.5 When NOT to cache
- One-off questions with a unique framing ("why did X fail on Tuesday?")
- Answers that depend on live state outside `~/macOS` (network, running apps)
- Security findings — redact secrets before caching, or skip the cache entirely
### 2.6 Staleness is advisory, not enforced
If the cached content still looks accurate at day 8, an agent **may** re-run
the scan to confirm — but re-running on every query defeats the purpose.
Treat the 7-day window as the default and justify any deviation.
## 3. System diagnostics — `scripts/mysystem.py`
A single-file Python helper. Run it via the code interpreter or as a
subprocess. It is read-only.
### Available functions
| Function | Returns | Notes |
|---|---|---|
| `get_sys_info()` | `dict` (and prints) | OS, arch, RAM, free disk, Zsh & AeroSpace versions. Uses `subprocess` with `check=False` — a missing tool becomes `"not installed"`, not a crash. |
| `parse_aerospace()` | `dict` of mode → bindings | Reads `~/.config/aerospace/aerospace.toml` via `tomllib`. |
| `parse_espanso()` | `list[dict]` | Recursively scans `$ESPANSO_MATCH_DIR` (default `~/Library/Application Support/espanso/match`) for `*.yml`/`*.yaml`. Handles `form`, `vars`, `image_path` (non-text) matches. |
| `parse_skills(root=...)` | `list[dict]` | Lists OpenClaw skills by parsing YAML frontmatter in every `SKILL.md` under `~/macOS/claude/skills`. |
| `parse_help_sections()` | `dict` of section → text | Extracts `sec_spotlight`, `sec_raycast`, `sec_keyboard` from `~/macOS/scripts/help.sh`. |
| `get_zsh_aliases()` | `dict` | Parses `zsh -ic "alias"` output. |
| `get_zsh_functions()` | `list[str]` | Lists non-underscore function names from `~/.zsh_functions`. |
### CLI
```bash
python scripts/mysystem.py # = all
python scripts/mysystem.py info
python scripts/mysystem.py aerospace
python scripts/mysystem.py espanso
python scripts/mysystem.py skills
python scripts/mysystem.py help
python scripts/mysystem.py zsh
python scripts/mysystem.py all --json # machine-readable
```
**Agent action:** summarize the output logically — group AeroSpace bindings by
mode (e.g. *Window Management*, *Focus*, *Workspaces*), translate AeroSpace
commands into plain English ("moves the focused window to the left half of
the screen") without executing them.
## 4. Configuration sources & GNU Stow awareness
The user manages dotfiles with **GNU Stow**. Files in standard paths are
often symlinks into `~/macOS`. Always resolve the real path before citing
`file#line` — otherwise the tag points at a symlink, not the source of truth.
| What | Standard path | Stow target (real) |
|---|---|---|
| Shell info | `~/macOS/scripts/help.sh` | (already in repo) |
| AeroSpace | `~/.config/aerospace/aerospace.toml` | `~/macOS/aerospace/aerospace.toml` |
| Zsh | `~/.zshrc`, `~/.zsh_aliases`, `~/.zsh_functions` | `~/macOS/zsh/...` |
| Espanso | `$ESPANSO_MATCH_DIR` (default `~/Library/Application Support/espanso/match`) | `~/macOS/Espanso/` |
| Raycast / Spotlight / Keyboard | `~/macOS/scripts/help.sh` (`sec_raycast`, `sec_spotlight`, `sec_keyboard`) | — |
**Resolve with:** `readlink -f ~/.config/aerospace/aerospace.toml` (macOS 13+
ships BSD `readlink`; use `python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' `
for portability).
## 5. Zsh aliases and functions
Do **not** manually parse `.zshrc` — aliases and functions are often loaded
through conditionals, `source` chains, and plugin managers, so static parsing
lies. Dump the live environment instead.
```bash
# All active aliases (interactive, non-login shell)
zsh -ic "alias"
# Function names defined in ~/.zsh_functions (excludes private _* helpers)
zsh -f -c 'source ~/.zsh_functions; print -l ${(k)functions:#_*}'
# Source + preceding comment for a specific function
grep -n -B 6 'my_function' ~/.zsh_functions
# Full source of any function (works for plugin-defined ones too)
zsh -ic "which my_function"
```
**Agent action:** describe the alias/function in plain language. If the user
asks for the source, quote `zsh -ic "which <name>"` output verbatim.
## 6. Spotlight, Raycast, and keyboard shortcuts
Read `~/macOS/scripts/help.sh` directly and extract the relevant section:
| Section | Contains |
|---|---|
| `sec_spotlight` | Spotlight shortcut, enabled categories |
| `sec_raycast` | Raycast hotkeys, hotkey conflicts, extensions |
| `sec_keyboard` | macOS key remaps, custom shortcuts, text replacements |
**Rules:**
- Read the file — do not `bash` it.
- Note invocation shortcuts explicitly (`Cmd+Space` for Spotlight,
`Option+Space` for Raycast, etc.).
- Clearly attribute a capability to **Raycast** vs. **Spotlight** so the user
knows which app to open.
## 7. Response style & formatting
### Tag every claim with its source
- **[Configured]** — statically defined in a dotfile (e.g. `aerospace.toml`).
- **[Detected]** — live data from a script or shell check (RAM, Zsh version).
- **[Unknown]** — data that could not be resolved (file missing, command
absent, parse error).
### Traceability
Append the **real** (Stow-resolved) path and line number as a subdued tag:
> `Cmd+Enter` → opens Terminal — **[Configured]** (`~/macOS/aerospace/aerospace.toml#L45`)
### Format
- Short, distinct lists.
- Group related items (AeroSpace by mode, Espanso by file, aliases by
prefix).
- Bold the user-facing shortcut; plain-text the action.
### Execution rule
Explain commands, shortcuts, and scripts. **NEVER** execute a shortcut or a
structural script (AeroSpace reload, Espanso restart, `zsh` re-source) during
a configuration review.
## 8. When to use reference files
| File | Open when… |
| references/glossary.md | User uses unfamiliar terms ("what's a Stow package?") |
| references/troubleshooting.md | A command failed, a file is missing, or output is empty |
> 📖 **Official documentation:** every tool's official docs are collected in
> `references/glossary.md` — the "Official documentation" table at the end,
> plus a link under each term. When the user needs depth beyond this skill's
> scope, send them there instead of expanding the answer inline.
## 9. Examples
**Q: "Show me my AeroSpace keybindings."**
```bash
python scripts/mysystem.py aerospace
```
> ### AeroSpace keybindings
>
> **Mode `main`** — *Window Management* [Configured]
> - `alt-h` → focus left (`~/macOS/aerospace/aerospace.toml#L12`)
> - `alt-shift-h` → move window to left half (`…#L18`)
> - `alt-1` → workspace 1 (`…#L30`)
>
> **Mode `service`** — *System* [Configured]
> - `esc` → back to `main` (`…#L60`)
>
> ℹ️ AeroSpace v0.15.1 [Detected]
**Q: "What does `Cmd+Space` do on my Mac?"**
> `Cmd+Space` opens **Spotlight** (not Raycast) — [Configured]
> (`~/macOS/scripts/help.sh`, `sec_spotlight`, line 42).
> Raycast is bound to `Option+Space` instead.
## 10. Troubleshooting (short list — full list in `references/troubleshooting.md`)
| Symptom | Likely cause | Next step |
|---|---|---|
| `mysystem.py` exits with "Python 3.11+" | System Python is 3.9/3.10 | Ask user to `brew install python@3.12` or activate `.venv` |
| `AeroSpace config not found` | Symlink broken or file absent | `readlink -f ~/.config/aerospace/aerospace.toml` |
| Empty AeroSpace output | TOML syntax error | Surface the `tomllib.TOMLDecodeError` line number |
| Espanso dir not found | `$ESPANSO_MATCH_DIR` unset and default path missing | Check `~/macOS/Espanso` exists; ask user |
| `zsh: command not found: alias` | Non-interactive shell | Re-run with `-i` flag |
| `skills` list empty | `~/macOS/claude/skills` missing | Ask user to confirm the OpenClaw skills root |
## 11. Security & privacy
- All scripts in `scripts/` are **read-only** by design. If you need to add
one, document the "no writes, no network, no execution of user config"
contract at the top of the file.
- Never print full file contents unless the user asked for source. Prefer
excerpts + line references.
- Never send dotfile contents to external services.
- Do not echo `zsh -ic` output containing secrets (tokens, API keys in
env vars). Redact with `***` and note the redaction.
3. requirements.txt
# mac-setup-master skill dependencies
PyYAML>=6.0,<7.0
4. Improved scripts/mysystem.py
#!/usr/bin/env python3
"""mac-setup-master diagnostic helpers.
READ-ONLY. Never writes to disk except the explicit cache directory
(``references/cached-answers/``), never executes the user's shell config,
never runs shortcuts. Safe to import from the Claude code interpreter.
Requires Python 3.11+ (stdlib ``tomllib``) and PyYAML.
CLI:
python scripts/mysystem.py [info|aerospace|espanso|skills|help|zsh|all]
[--json]
python scripts/mysystem.py cache [--status]
python scripts/mysystem.py cache --write -|path.md
Import:
from mysystem import (
get_sys_info, parse_aerospace, parse_espanso, parse_skills,
parse_help_sections, get_zsh_aliases, get_zsh_functions,
cache_status, read_cache, write_cache, get_cache_path,
)
"""
from __future__ import annotations
import argparse
import json
import os
import re
import shutil
import subprocess
import sys
from datetime import date
from pathlib import Path
from typing import Any
try:
import tomllib # type: ignore[import-not-found]
except ModuleNotFoundError: # pragma: no cover
sys.exit("mysystem.py requires Python 3.11+ (stdlib tomllib). Run `python3 -V`.")
import yaml # PyYAML
# --------------------------------------------------------------------------- #
# Helpers
# --------------------------------------------------------------------------- #
def _safe_version(cmd: str) -> str:
"""Return --version output, or 'not installed' if the binary is missing."""
if shutil.which(cmd.split()[0]) is None:
return "not installed"
try:
out = subprocess.run(
cmd, shell=True, capture_output=True, text=True, timeout=5, check=False
).stdout.strip()
except subprocess.TimeoutExpired:
return "timed out"
return out or "unknown"
# --------------------------------------------------------------------------- #
# System info
# --------------------------------------------------------------------------- #
def _disk_space(path: str) -> str:
try:
usage = shutil.disk_usage(path)
gib = 1024**3
return (
f"Total: {usage.total / gib:.2f} GB, "
f"Used: {usage.used / gib:.2f} GB, "
f"Free: {usage.free / gib:.2f} GB"
)
except OSError:
return "unavailable (path may be missing or not mounted)"
def get_sys_info(as_json: bool = False) -> dict[str, str]:
"""Return read-only system metrics. Also prints unless as_json."""
try:
ram_bytes = int(subprocess.getoutput("sysctl -n hw.memsize").strip())
ram = f"{ram_bytes / (1024**3):.2f} GB"
except ValueError:
ram = "unknown"
info: dict[str, str] = {
"OS_Version": _safe_version("sw_vers -productVersion"),
"Architecture": _safe_version("uname -m"),
"RAM": ram,
"Disk / (Macintosh HD)": _disk_space("/"),
"Disk /Volumes/MacDatosMaster": _disk_space("/Volumes/MacDatosMaster"),
"Disk /Volumes/DatosSlave": _disk_space("/Volumes/DatosSlave"),
"Zsh_Version": _safe_version("zsh --version"),
"AeroSpace": _safe_version("aerospace --version"),
}
if as_json:
return info
for key, value in info.items():
print(f"{key}: {value}")
return info
# --------------------------------------------------------------------------- #
# AeroSpace
# --------------------------------------------------------------------------- #
def parse_aerospace(as_json: bool = False) -> dict[str, dict[str, str]]:
path = Path("~/.config/aerospace/aerospace.toml").expanduser()
if not path.exists():
print(f"AeroSpace config not found at {path}", file=sys.stderr)
return {}
try:
with path.open("rb") as fh:
config = tomllib.load(fh)
except tomllib.TOMLDecodeError as exc:
print(f"Invalid TOML in {path}: {exc}", file=sys.stderr)
return {}
bindings: dict[str, dict[str, str]] = {
mode: dict(body.get("binding", {}))
for mode, body in config.get("mode", {}).items()
}
if as_json:
return bindings
print("--- AeroSpace Keybindings ---")
for mode, keys in bindings.items():
print(f"\nMode: [{mode}]")
for key, action in keys.items():
print(f" {key.ljust(18)} -> {action}")
if not bindings:
print("(no bindings found)")
return bindings
# --------------------------------------------------------------------------- #
# Espanso
# --------------------------------------------------------------------------- #
def parse_espanso(as_json: bool = False) -> list[dict[str, str]]:
match_dir = Path(
os.environ.get(
"ESPANSO_MATCH_DIR",
os.path.expanduser("~/Library/Application Support/espanso/match"),
)
).expanduser()
if not match_dir.exists():
print(f"Espanso match directory not found: {match_dir}", file=sys.stderr)
return []
results: list[dict[str, str]] = []
for path in sorted(match_dir.rglob("*")):
if path.suffix.lower() not in {".yml", ".yaml"}:
continue
try:
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
except (OSError, yaml.YAMLError) as exc:
print(f"Could not read {path}: {exc}", file=sys.stderr)
continue
for match in data.get("matches", []):
if not isinstance(match, dict):
continue
triggers = match.get("triggers", match.get("trigger", []))
if isinstance(triggers, str):
triggers = [triggers]
replacement = match.get("replace")
if replacement is None:
kind = (
"form" if "form" in match
else "vars" if "vars" in match
else "image" if "image_path" in match
else "dynamic"
)
replacement = f"<{kind} match>"
for trigger in triggers:
results.append({
"file": path.name,
"trigger": str(trigger),
"replacement": str(replacement),
})
if as_json:
return results
for row in results:
print(f"{row['file']}: {row['trigger']!r} -> {row['replacement']!r}")
return results
# --------------------------------------------------------------------------- #
# OpenClaw skills
# --------------------------------------------------------------------------- #
_FRONTMATTER_RE = re.compile(r"\A---\s*\n(.*?)\n---(?:\s|\Z)", re.S)
def search_skills(
query: str | None = None,
root: str = "~/macOS/claude/skills",
) -> list[dict[str, str]]:
root_path = Path(root).expanduser()
if not root_path.exists():
print(f"Skills root not found: {root_path}", file=sys.stderr)
return []
q = query.casefold() if query else None
results: list[dict[str, str]] = []
for skill_file in root_path.rglob("SKILL.md"):
text = skill_file.read_text(encoding="utf-8")
match = _FRONTMATTER_RE.match(text)
if not match:
continue
try:
meta = yaml.safe_load(match.group(1)) or {}
except yaml.YAMLError:
continue
name = meta.get("name", skill_file.parent.name)
description = (meta.get("description") or "").strip()
if q and q not in name.casefold() and q not in description.casefold():
continue
results.append({
"name": name,
"description": description,
"path": str(skill_file.parent),
})
return results
def parse_skills(as_json: bool = False) -> list[dict[str, str]]:
skills = search_skills()
if as_json:
return skills
if not skills:
print("(no skills found under ~/macOS/claude/skills)")
return skills
for skill in skills:
print(f"{skill['name']}: {skill['description']}\n {skill['path']}")
return skills
# --------------------------------------------------------------------------- #
# help.sh sections
# --------------------------------------------------------------------------- #
_HELP_SECTION_RE = re.compile(
r"^sec_(?Pspotlight|raycast|keyboard)\s*\(\)\s*\{(?P.*?)^\}" ,
re.S | re.M,
)
def parse_help_sections(as_json: bool = False) -> dict[str, str]:
path = Path("~/macOS/scripts/help.sh").expanduser()
if not path.exists():
print(f"help.sh not found at {path}", file=sys.stderr)
return {}
text = path.read_text(encoding="utf-8")
sections = {
m.group("name"): m.group("body").strip()
for m in _HELP_SECTION_RE.finditer(text)
}
if as_json:
return sections
for name, body in sections.items():
print(f"\n=== sec_{name} ===\n{body}")
return sections
# --------------------------------------------------------------------------- #
# Zsh helpers
# --------------------------------------------------------------------------- #
def get_zsh_aliases(as_json: bool = False) -> dict[str, str]:
out = subprocess.run(
["zsh", "-ic", "alias"], capture_output=True, text=True, check=False
).stdout
aliases: dict[str, str] = {}
for line in out.splitlines():
match = re.match(r"^(\S+)=(?:'([^']*)'|\"([^\"]*)\"|(.*))$", line.strip())
if not match:
continue
name = match.group(1)
value = next(g for g in match.groups()[1:] if g is not None)
aliases[name] = value
if as_json:
return aliases
for name, value in aliases.items():
print(f"{name}={value}")
return aliases
def get_zsh_functions(as_json: bool = False) -> list[str]:
out = subprocess.run(
["zsh", "-f", "-c",
"source ~/.zsh_functions; print -l ${(k)functions:#_*}"],
capture_output=True, text=True, check=False,
).stdout
names = sorted({line.strip() for line in out.splitlines() if line.strip()})
if as_json:
return names
for name in names:
print(name)
return names
# --------------------------------------------------------------------------- #
# Cache (references/cached-answers/.md, 7-day TTL)
# --------------------------------------------------------------------------- #
CACHE_DIR = Path("~/macOS/claude/skills/mac-setup-master/references/cached-answers").expanduser()
CACHE_TTL_DAYS = 7
_CACHE_FM_RE = re.compile(r"\A---\s*\n(.*?)\n---\s*\n", re.S)
def get_cache_path(topic: str) -> Path:
safe = re.sub(r"[^a-z0-9-]+", "-", topic.lower()).strip("-")
if not safe:
raise ValueError(f"Invalid cache topic: {topic!r}")
return CACHE_DIR / f"{safe}.md"
def _read_cache_meta(path: Path) -> dict[str, Any]:
if not path.exists():
return {}
m = _CACHE_FM_RE.match(path.read_text(encoding="utf-8"))
if not m:
return {}
try:
return yaml.safe_load(m.group(1)) or {}
except yaml.YAMLError:
return {}
def cache_status(topic: str, days: int = CACHE_TTL_DAYS) -> dict[str, Any]:
"""Return {'state': 'fresh'|'stale'|'missing', 'age_days': int|None, 'created': str|None}."""
path = get_cache_path(topic)
meta = _read_cache_meta(path)
if not meta:
return {"state": "missing", "age_days": None, "created": None}
try:
created = date.fromisoformat(str(meta.get("created", "")))
except ValueError:
return {"state": "missing", "age_days": None, "created": None}
age = (date.today() - created).days
state = "fresh" if age < days else "stale"
return {"state": state, "age_days": age, "created": created.isoformat()}
def read_cache(topic: str, days: int = CACHE_TTL_DAYS) -> str | None:
"""Return cached markdown if fresh, else None."""
if cache_status(topic, days)["state"] != "fresh":
return None
return get_cache_path(topic).read_text(encoding="utf-8")
def write_cache(topic: str, content: str) -> Path:
"""Write *content* to the cache, stripping any caller frontmatter and
stamping fresh ``created`` (preserved if present) / ``last_checked``."""
path = get_cache_path(topic)
path.parent.mkdir(parents=True, exist_ok=True)
existing = _read_cache_meta(path)
today = date.today().isoformat()
created = existing.get("created") or today
body = _CACHE_FM_RE.sub("", content, count=1)
header = (
f"---\n"
f"topic: {path.stem}\n"
f"created: {created}\n"
f"last_checked: {today}\n"
f"tool: mysystem.py\n"
f"---\n\n"
)
path.write_text(header + body.lstrip(), encoding="utf-8")
return path
# --------------------------------------------------------------------------- #
# CLI
# --------------------------------------------------------------------------- #
def _run_all(as_json: bool) -> None:
if as_json:
print(json.dumps({
"info": get_sys_info(as_json=True),
"aerospace": parse_aerospace(as_json=True),
"espanso": parse_espanso(as_json=True),
"skills": parse_skills(as_json=True),
"help_sections": parse_help_sections(as_json=True),
"zsh_aliases": get_zsh_aliases(as_json=True),
"zsh_functions": get_zsh_functions(as_json=True),
}, indent=2, ensure_ascii=False))
return
get_sys_info()
parse_aerospace()
parse_espanso()
parse_skills()
parse_help_sections()
print("\n--- Zsh aliases ---"); get_zsh_aliases()
print("\n--- Zsh functions ---"); get_zsh_functions()
def _cmd_cache(args: argparse.Namespace) -> int:
if args.status:
info = cache_status(args.topic)
if info["state"] == "missing":
print("Status: missing")
else:
print(f"Status: {info['state']} (age {info['age_days']} days, created {info['created']})")
return 0
if args.write is not None:
content = sys.stdin.read() if args.write == "-" else Path(args.write).read_text(encoding="utf-8")
path = write_cache(args.topic, content)
meta = _read_cache_meta(path)
print(f"Written: {path} (created {meta.get('created')}, last_checked {meta.get('last_checked')})")
return 0
content = read_cache(args.topic)
if content is None:
info = cache_status(args.topic)
age = f"{info['age_days']} days old" if info["age_days"] is not None else "missing"
print(f"STALE: cache {age}. Regenerate and write with --write.", file=sys.stderr)
return 2
print(content)
return 0
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
prog="mysystem.py",
description="Read-only diagnostics + answer cache for mac-setup-master.",
)
sub = parser.add_subparsers(dest="command", required=False)
for name in ("info", "aerospace", "espanso", "skills", "help", "zsh", "all"):
p = sub.add_parser(name)
p.add_argument("--json", action="store_true", help="machine-readable output")
p_cache = sub.add_parser("cache", help="read/write/status the answer cache")
p_cache.add_argument("topic", help="cache topic key (e.g. aerospace-keybindings)")
p_cache.add_argument("--status", action="store_true", help="print status only")
p_cache.add_argument(
"--write", nargs="?", const="-", default=None,
help="write from stdin ('-') or from a file path",
)
args = parser.parse_args(argv)
if args.command is None or args.command == "all":
_run_all(getattr(args, "json", False))
return 0
if args.command == "cache":
return _cmd_cache(args)
dispatch = {
"info": get_sys_info,
"aerospace": parse_aerospace,
"espanso": parse_espanso,
"skills": parse_skills,
"help": parse_help_sections,
"zsh": lambda as_json=False: (
print("--- Zsh aliases ---") or get_zsh_aliases(as_json)
if not as_json else get_zsh_aliases(as_json=True)
) and (
None if as_json else (print("\n--- Zsh functions ---"), get_zsh_functions())[0]
),
}
# zsh needs special handling because it returns two collections
if args.command == "zsh":
if args.json:
print(json.dumps({
"aliases": get_zsh_aliases(as_json=True),
"functions": get_zsh_functions(as_json=True),
}, indent=2, ensure_ascii=False))
else:
print("--- Zsh aliases ---"); get_zsh_aliases()
print("\n--- Zsh functions ---"); get_zsh_functions()
return 0
dispatch[args.command](as_json=args.json)
return 0
if __name__ == "__main__":
raise SystemExit(main())
6. Suggested references/ files
references/troubleshooting.md
# Troubleshooting — mac-setup-master
Indexed by symptom. Each entry follows **Symptom → Cause → Fix → Verify**.
Official documentation links are collected at the bottom of this file.
---
## 1. `mysystem.py` won't start
### 1.1 `No module named 'tomllib'`
| | |
|---|---|
| **Symptom** | Running `python scripts/mysystem.py` raises `ModuleNotFoundError: No module named 'tomllib'` |
| **Cause** | macOS ships `/usr/bin/python3` at **3.9**. `tomllib` only entered the standard library in **3.11**, so even if `python3 -V` reports 3.9, `tomllib` is absent. |
| **Fix** | Activate the skill's `.venv` (recommended), or install 3.11+: `brew install python@3.12` |
| **Verify** | `python3 -c "import tomllib; print('ok')"` prints `ok` |
[...]
references/glossary.md
# Glossary — mac-setup-master
Grouped by category. Every entry gives a **one-line definition** + **where/how it's used in this skill** + **official docs link**.
---
## Shell / Zsh
**Zsh**
: The UNIX command interpreter; the default interactive shell on macOS since 10.15. This skill reads aliases and functions via `zsh -ic` and `zsh -f -c` rather than statically parsing `.zshrc`.
: 📖 [zsh.sourceforge.io/Doc](https://zsh.sourceforge.io/Doc/)
[...]