Files
vault-cli/vault
T
Hermes Agent b665413630 vault-cli: tiered analyze — Haiku default, --sonnet/--opus step up
vault analyze now defaults to Haiku/Bedrock (cheap+fast, the nightly tier)
instead of the profile default. --sonnet and --opus step intelligence/cost up
for harder judgment calls; raw -m/--provider still override entirely. Provider
is forced to bedrock only for the haiku (us.anthropic.*) tier; the
global.anthropic.* sonnet/opus IDs use the profile default provider. Adds an
alive banner to stderr (headless -z buffers, so a pass is silent 1-3 min).
2026-07-22 12:34:32 -04:00

193 lines
7.5 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 [--sonnet|--opus] [-m MODEL] [--provider PROVIDER]
spawn a Hermes Librarian pass. DEFAULT: Haiku/Bedrock
(cheap+fast — the nightly/weekly tier). --sonnet / --opus
step up intelligence & cost for harder judgment calls.
-m/--provider override entirely (future/non-Bedrock models).
"""
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 Haiku — cheap+fast for the nightly/weekly
# pass. --sonnet / --opus step up intelligence & cost inline. IDs verified against
# this host's Hermes config + the active 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 Haiku (cheap nightly). --sonnet/--opus step up.
# Raw -m/--provider override the tier entirely (escape hatch for future models
# or non-Bedrock providers). Tier flags are last-wins if combined.
tier = "haiku"
if "--sonnet" in argv:
tier = "sonnet"
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 == "haiku":
print(" step up: vault analyze --sonnet (or --opus) for harder judgment calls",
file=sys.stderr)
sys.stderr.flush()
subprocess.run(hcmd)
else:
die(f"unknown command '{cmd}'")
if __name__ == "__main__":
main()