#!/usr/bin/env python3 from __future__ import annotations import argparse from pathlib import Path from vault_common import ROOT, all_records, json_print, rel, search_records, split_frontmatter, tokenize INBOX_DIRS = [ROOT / "00 Inbox", ROOT / "01 Ideas"] LIBRARY_HINTS = { "ansible": "90 Library/DevOps/Ansible", "ci": "90 Library/DevOps", "cd": "90 Library/DevOps", "cicd": "90 Library/DevOps", "docker": "90 Library/Containers/Docker", "git": "90 Library/DevOps", "gitlab": "90 Library/DevOps", "jenkins": "90 Library/DevOps", "kubernetes": "90 Library/Containers/Kubernetes", "linux": "90 Library/Linux", "nginx": "90 Library/HomeLab/Proxy", "rails": "90 Library/Programming/Ruby On Rails", "ruby": "90 Library/Programming/Ruby", "terraform": "90 Library/DevOps", } def read_path(path_arg: str) -> Path: path = Path(path_arg) if not path.is_absolute(): path = ROOT / path return path def scan() -> list[dict[str, object]]: items = [] for directory in INBOX_DIRS: if not directory.exists(): continue for path in sorted(directory.iterdir(), key=lambda item: item.name.lower()): if path.name == ".gitkeep" or not path.is_file(): continue text = path.read_text(encoding="utf-8", errors="replace") if path.suffix == ".md" else "" fm, body, _ = split_frontmatter(text) items.append( { "path": rel(path), "folder": rel(directory), "suffix": path.suffix, "title": fm.get("title") or path.stem, "tags": fm.get("tags") or [], "bytes": path.stat().st_size, "classification": classify_text(path, body, fm), } ) return items def classify_text(path: Path, body: str, frontmatter: dict[str, object]) -> dict[str, object]: tags = frontmatter.get("tags") or [] if not isinstance(tags, list): tags = [tags] content = f"{path.stem}\n{' '.join(map(str, tags))}\n{body[:4000]}".lower() tokens = tokenize(content) idea_markers = {"идея", "idea", "business", "product", "стартап", "концепция", "задумка"} if "01 Ideas" in rel(path) or tokens & idea_markers: kind = "idea" destination = "01 Ideas" else: kind = "library" destination = "90 Library/Other" for marker, folder in LIBRARY_HINTS.items(): if marker in content: destination = folder break return { "kind": kind, "destination": destination, "confidence": "heuristic", } def plan(path: Path) -> dict[str, object]: text = path.read_text(encoding="utf-8", errors="replace") fm, body, _ = split_frontmatter(text) classification = classify_text(path, body, fm) records = all_records() terms = " ".join([path.stem, str(fm.get("title") or ""), " ".join(map(str, fm.get("tags") or []))]) similar = [] for item in search_records(records, terms, limit=30): if item["path"] == rel(path): continue if item["path"].startswith(("03 Journal/", "99 System/Export/", "99 System/Cache/", "99 System/Template/")): continue similar.append(item) if len(similar) == 8: break wikilinks = [item["name"] for item in similar[:5]] return { "path": rel(path), "classification": classification, "similar_notes": similar, "suggested_wikilinks": wikilinks, "write_policy": "read-only plan; agent must ask before workflow changes", } def main() -> int: parser = argparse.ArgumentParser(description="Agent-facing ingest helper") sub = parser.add_subparsers(dest="command", required=True) sub.add_parser("scan", help="List files in 00 Inbox and 01 Ideas") classify = sub.add_parser("classify", help="Classify one source file") classify.add_argument("path") plan_cmd = sub.add_parser("plan", help="Build JSON ingest plan for one source file") plan_cmd.add_argument("path") args = parser.parse_args() if args.command == "scan": json_print({"ok": True, "items": scan()}) elif args.command == "classify": path = read_path(args.path) text = path.read_text(encoding="utf-8", errors="replace") fm, body, _ = split_frontmatter(text) json_print({"ok": True, "path": rel(path), "classification": classify_text(path, body, fm)}) elif args.command == "plan": json_print({"ok": True, "plan": plan(read_path(args.path))}) return 0 if __name__ == "__main__": raise SystemExit(main())