Files
vault-cli/vault
T
Hermes Agent d81d156b79 vault-cli: flip analyze default to Opus, --haiku/--sonnet step down
Opus draws the auto-fix/flag line correctly (Haiku over-fixed on judgment
calls, verified apples-to-apples against the same vault baseline). At ~$0.14/run
it's cheap enough to run nightly. --sonnet/--haiku now step DOWN; --opus kept as
an explicit identity flag; raw -m/--provider still override entirely.

Hermes host defaults (config.yaml default preset, delegation/compression models)
are unchanged — this only affects which model the vault CLI's own hermes -z spawn uses.
2026-07-22 13:44:37 -04:00

196 lines
7.7 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
vault — CLI for Ryan's R/J/F/I Obsidian vault.
Clobber-safe. Dry-run by default; pass --execute to act.
Operates on BOTH trees (/vault map + /data territory) and the topology manifest.
Subcommands:
vault tree [SYS] print topology (optionally one system)
vault search <query> find IDs/notes by code, title, or content
vault show <ID> show an ID's note + its /data home + blob count
vault create <ID> "<title>" create ID note (folder+folder-note) in /vault AND /data
vault move <oldID> <newID> renumber: rename note+folder, rewrite backlinks, move /data dir
vault rename <ID> "<title>" retitle in place, rewrite backlinks
vault sync-data reconcile /data skeleton to match manifest (create missing dirs)
vault manifest regenerate the topology manifest note
vault report [--pretty] deterministic Librarian report (delegates to vault-librarian.py)
vault analyze [--haiku|--sonnet|--opus] [-m MODEL] [--provider PROVIDER]
spawn a Hermes Librarian pass. DEFAULT: Opus (smart —
draws the auto-fix/flag line correctly, ~$0.14/run so cheap
enough for nightly). --sonnet / --haiku step DOWN for
cheaper/faster passes; -m/--provider override entirely.
"""
import os, re, sys, subprocess
# Resolve sibling modules whether running from /var/lib/hermes, the repo, or the Nix store.
_SELF_DIR = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0, _SELF_DIR)
import vault_lib as vl
HERMES = "/run/current-system/sw/bin/hermes"
LIBRARIAN = os.path.join(_SELF_DIR, "vault-librarian.py")
PROMPT = os.path.join(_SELF_DIR, "vault-librarian-prompt.md")
# `vault analyze` model tiers. Default is Opus — smart enough to draw the auto-fix/
# flag line correctly, and ~$0.14/run so cheap enough for a nightly pass. --sonnet /
# --haiku step DOWN. IDs verified against this host's Hermes config + Bedrock provisioning.
ANALYZE_TIERS = {
"haiku": "us.anthropic.claude-haiku-4-5-20251001-v1:0",
"sonnet": "global.anthropic.claude-sonnet-5",
"opus": "global.anthropic.claude-opus-4-8",
}
# Only the haiku (us.anthropic.*) tier is known to route via an explicit `bedrock`
# provider. The global.anthropic.* IDs use the profile's default provider unless the
# caller passes --provider, to avoid a provider/model-registration mismatch.
ANALYZE_TIER_PROVIDER = {"haiku": "bedrock"}
WIKILINK_RE = re.compile(r'(!?\[\[)([^\]#|]+)((?:#[^\]|]+)?(?:\|[^\]]+)?)(\]\])')
def die(msg, code=1):
print("ERROR: " + msg, file=sys.stderr)
sys.exit(code)
def executing(argv):
return "--execute" in argv
def sys_root(sys_letter):
"""Return abs /vault path of a system root, e.g. 'R' -> /vault/R Ryan."""
s = vl.walk_structure(vl.VAULT, include_meta=True)["systems"]
if sys_letter not in s:
die(f"unknown system '{sys_letter}' (have {sorted(s)})")
return os.path.join(vl.VAULT, s[sys_letter])
def find_id_folder(root, id_code):
"""Return abs folder path for an ID code under root, or None."""
for dp, dns, fns in os.walk(root):
dns[:] = [d for d in dns if d not in vl.SKIP_DIRS]
if vl.id_code(os.path.basename(dp)) == id_code:
return dp
return None
# ---------- read-only commands ----------
def cmd_tree(argv):
s = vl.walk_structure(vl.VAULT, include_meta=True)
want = argv[0] if argv else None
for code in sorted(s["cats"]):
if want and not code.startswith(want):
continue
n = sum(1 for i in s["ids"] if i.startswith(code + "."))
print(f" {code:8} {os.path.basename(s['cats'][code])} ({n} IDs)")
def cmd_search(argv):
if not argv:
die("usage: vault search <query>")
q = " ".join(argv).lower()
hits = []
for p in vl.all_notes(vl.VAULT):
rel = os.path.relpath(p, vl.VAULT)
stem = os.path.splitext(os.path.basename(p))[0]
if q in stem.lower() or q in rel.lower():
hits.append((rel, "name"))
continue
try:
if q in open(p, encoding="utf-8").read().lower():
hits.append((rel, "content"))
except Exception:
pass
for rel, how in hits[:50]:
print(f" [{how}] {rel}")
print(f"\n{len(hits)} match(es).")
def cmd_show(argv):
if not argv:
die("usage: vault show <ID>")
code = argv[0]
root = sys_root(code[0])
folder = find_id_folder(root, code)
if not folder:
die(f"ID {code} not found in /vault")
print("VAULT folder: " + os.path.relpath(folder, vl.VAULT))
note = os.path.join(folder, os.path.basename(folder) + ".md")
print("Folder-note: " + ("present" if os.path.exists(note) else "MISSING"))
# /data home
drel = os.path.relpath(folder, vl.VAULT)
dpath = os.path.join(vl.DATA, drel)
if os.path.isdir(dpath):
nblobs = sum(len(fs) for _, _, fs in os.walk(dpath))
print(f"/data home: {drel} ({nblobs} blobs)")
else:
print(f"/data home: MISSING ({drel})")
def main():
if len(sys.argv) < 2:
print(__doc__)
sys.exit(0)
cmd, argv = sys.argv[1], sys.argv[2:]
dispatch = {
"tree": cmd_tree,
"search": cmd_search,
"show": cmd_show,
}
# write-commands live in vault_ops (imported lazily to keep this file readable)
if cmd in dispatch:
dispatch[cmd](argv)
elif cmd in ("create", "move", "rename", "sync-data", "manifest"):
import vault_ops
vault_ops.run(cmd, argv, executing(sys.argv))
elif cmd == "report":
sys.exit(subprocess.run([sys.executable, LIBRARIAN] + argv).returncode)
elif cmd == "analyze":
prompt = open(PROMPT, encoding="utf-8").read()
# Model tier: default Opus (smart — draws the auto-fix/flag line correctly,
# ~$0.14/run so cheap enough to run nightly). --haiku/--sonnet step DOWN.
# Raw -m/--provider override the tier entirely (escape hatch for future models
# or non-Bedrock providers). Tier flags are last-wins if combined.
tier = "opus"
if "--sonnet" in argv:
tier = "sonnet"
if "--haiku" in argv:
tier = "haiku"
if "--opus" in argv:
tier = "opus"
model = ANALYZE_TIERS[tier]
provider = ANALYZE_TIER_PROVIDER.get(tier) # None for sonnet/opus (use profile default)
if "-m" in argv:
i = argv.index("-m")
if i + 1 >= len(argv):
die("-m requires a model name")
model = argv[i + 1]
tier = "custom"
provider = None # custom model: don't force a provider unless asked
if "--provider" in argv:
i = argv.index("--provider")
if i + 1 >= len(argv):
die("--provider requires a provider name")
provider = argv[i + 1]
hcmd = [HERMES, "-z", prompt, "-m", model]
if provider:
hcmd += ["--provider", provider]
# Banner: headless oneshot buffers output, so show it's alive + what it costs.
print(f"→ spawning Hermes Librarian pass [tier={tier} provider={provider or 'profile-default'}]",
file=sys.stderr)
print(f" model: {model}", file=sys.stderr)
print(" output is buffered; a full pass can take 1–3 min before you see anything.",
file=sys.stderr)
if tier == "opus":
print(" step DOWN: vault analyze --sonnet (or --haiku) for cheaper/faster passes",
file=sys.stderr)
sys.stderr.flush()
subprocess.run(hcmd)
else:
die(f"unknown command '{cmd}'")
if __name__ == "__main__":
main()