3b597aad7d
Affected files: 99 System/Export/agent-tools/AGENT_TOOLS.md 99 System/Export/agent-tools/vault_audit.py 99 System/Export/agent-tools/vault_common.py 99 System/Export/agent-tools/vault_frontmatter.py 99 System/Export/agent-tools/vault_index.py 99 System/Export/agent-tools/vault_ingest.py 99 System/INDEX.md AGENTS.md
138 lines
4.7 KiB
Python
138 lines
4.7 KiB
Python
#!/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())
|