Support external vault roots across tooling
This commit is contained in:
@@ -18,6 +18,8 @@ from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
from vault_root import parse_vault_args
|
||||
|
||||
|
||||
# Obsidian-style wiki link: [[target#anchor|alias]]
|
||||
WIKI_LINK_RE = re.compile(r"\[\[([^\]|#]+)(?:#[^\]|]+)?(?:\|([^\]]+))?\]\]")
|
||||
@@ -438,7 +440,7 @@ def parse_args() -> argparse.Namespace:
|
||||
action="store_true",
|
||||
help="write index/graph.json (default unless --validate is used alone)",
|
||||
)
|
||||
return parser.parse_args()
|
||||
return parse_vault_args(parser)
|
||||
|
||||
|
||||
def print_unresolved(unresolved: List[UnresolvedLink]) -> None:
|
||||
@@ -502,7 +504,7 @@ def print_invalid_node_types(invalid: List[Dict[str, object]]) -> None:
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
root = args.vault_root
|
||||
output = root / "index" / "graph.json"
|
||||
should_write = args.write or not args.validate
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Install a vault-local hook that invokes this framework without copying its tools."""
|
||||
|
||||
import argparse
|
||||
import shlex
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from vault_root import FRAMEWORK_ROOT, parse_vault_args
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
root = parse_vault_args(parser).vault_root
|
||||
repo = subprocess.run(["git", "-C", str(root), "rev-parse", "--show-toplevel"],
|
||||
capture_output=True, text=True)
|
||||
if repo.returncode or repo.stdout.strip() != str(root):
|
||||
parser.error("vault root must be the Git repository root to install hooks")
|
||||
result = subprocess.run(
|
||||
["git", "-C", str(root), "rev-parse", "--path-format=absolute", "--git-path", "hooks"],
|
||||
capture_output=True, text=True, check=True,
|
||||
)
|
||||
hook = Path(result.stdout.strip()) / "pre-commit"
|
||||
marker = "# knowledge-base framework hook"
|
||||
if hook.exists() or hook.is_symlink():
|
||||
legacy = hook.is_symlink() and hook.resolve() in {
|
||||
root / "scripts/pre-commit", FRAMEWORK_ROOT / "scripts/pre-commit",
|
||||
}
|
||||
managed = not hook.is_symlink() and marker in hook.read_text()
|
||||
if not legacy and not managed:
|
||||
parser.error(f"refusing to replace unmanaged hook: {hook}")
|
||||
script = FRAMEWORK_ROOT / "scripts/pre-commit"
|
||||
content = f"#!/bin/sh\n{marker}\nexec bash {shlex.quote(str(script))} {shlex.quote(str(root))}\n"
|
||||
hook.parent.mkdir(parents=True, exist_ok=True)
|
||||
if hook.is_symlink():
|
||||
hook.unlink()
|
||||
hook.write_text(content)
|
||||
hook.chmod(0o755)
|
||||
print(f"Installed {hook} -> {script} (vault: {root})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -21,6 +21,8 @@ from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
from vault_root import parse_vault_args
|
||||
|
||||
# Reuse the parser/resolver from generate-graph.py (hyphenated name -> load by path).
|
||||
_GG_PATH = Path(__file__).resolve().with_name("generate-graph.py")
|
||||
_spec = importlib.util.spec_from_file_location("generate_graph", _GG_PATH)
|
||||
@@ -102,9 +104,9 @@ def main() -> None:
|
||||
default="grouped",
|
||||
help="output format (default: grouped text)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
args = parse_vault_args(parser)
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
root = args.vault_root
|
||||
rows = collect(root)
|
||||
if args.format == "tsv":
|
||||
print_tsv(rows)
|
||||
|
||||
+48
-5
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""MCP server for the knowledge-base repository.
|
||||
"""MCP server for a selected knowledge-base vault, using framework-owned tooling.
|
||||
|
||||
This server is the single interface an agent needs to work the vault over stdio
|
||||
with JSON-RPC 2.0. It exposes graph and note *querying* (graph_summary,
|
||||
with JSON-RPC 2.0. It provides framework operating instructions (read_instructions),
|
||||
and exposes graph and note *querying* (graph_summary,
|
||||
graph_search_nodes, graph_neighbors, read_note, refresh_graph), *writing*
|
||||
(write_note for text, capture_raw for verbatim bytes from a local file or an
|
||||
http(s) download) and *publishing* (edit_note, git_commit, git_push), so an
|
||||
@@ -18,6 +19,7 @@ It auto-detects the transport layer:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
@@ -30,6 +32,7 @@ from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from vault_root import FRAMEWORK_ROOT, parse_vault_args
|
||||
|
||||
SERVER_NAME = "knowledge-base-mcp"
|
||||
SERVER_VERSION = "0.1.0"
|
||||
@@ -227,6 +230,12 @@ def _relative_to_root(state: AppState, target: Path) -> str:
|
||||
|
||||
|
||||
def _run_git(state: AppState, args: List[str]) -> subprocess.CompletedProcess:
|
||||
repo = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"], cwd=state.root,
|
||||
check=False, capture_output=True, text=True,
|
||||
)
|
||||
if repo.returncode != 0 or Path(repo.stdout.strip()).resolve() != state.root.resolve():
|
||||
raise ValueError("vault root must be the Git repository root for Git operations")
|
||||
return subprocess.run(
|
||||
["git", *args],
|
||||
cwd=state.root,
|
||||
@@ -249,6 +258,19 @@ def _git_result(payload: Dict[str, Any], exit_code: int) -> Dict[str, Any]:
|
||||
|
||||
def _tool_definitions() -> List[Dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
"name": "read_instructions",
|
||||
"description": (
|
||||
"Read framework AGENTS.md and AGENT_QUERY_GUIDE.md before working "
|
||||
"with the vault. These operating documents live outside an "
|
||||
"external vault and do not require a generated graph."
|
||||
),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "graph_summary",
|
||||
"description": (
|
||||
@@ -474,6 +496,15 @@ def _text_result(text: str, structured: Dict[str, Any] | None = None) -> Dict[st
|
||||
return result
|
||||
|
||||
|
||||
def _tool_read_instructions() -> Dict[str, Any]:
|
||||
documents = [
|
||||
{"path": name, "content": (FRAMEWORK_ROOT / name).read_text(encoding="utf-8")}
|
||||
for name in ("AGENTS.md", "AGENT_QUERY_GUIDE.md")
|
||||
]
|
||||
payload = {"framework_root": str(FRAMEWORK_ROOT), "documents": documents}
|
||||
return _text_result(json.dumps(payload, indent=2), structured=payload)
|
||||
|
||||
|
||||
def _tool_graph_summary(state: AppState, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
graph = _load_graph(state)
|
||||
nodes = graph.get("nodes", [])
|
||||
@@ -624,7 +655,10 @@ def _tool_read_note(state: AppState, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
|
||||
|
||||
def _tool_refresh_graph(state: AppState, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
command = [sys.executable, "scripts/generate-graph.py"]
|
||||
command = [
|
||||
sys.executable, str(FRAMEWORK_ROOT / "scripts/generate-graph.py"),
|
||||
"--vault-root", str(state.root), "--write",
|
||||
]
|
||||
if bool(args.get("validate", False)):
|
||||
command.append("--validate")
|
||||
|
||||
@@ -884,6 +918,8 @@ def _tool_git_push(state: AppState, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
|
||||
|
||||
def _call_tool(state: AppState, name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if name == "read_instructions":
|
||||
return _tool_read_instructions()
|
||||
if name == "graph_summary":
|
||||
return _tool_graph_summary(state, arguments)
|
||||
if name == "graph_search_nodes":
|
||||
@@ -908,7 +944,7 @@ def _call_tool(state: AppState, name: str, arguments: Dict[str, Any]) -> Dict[st
|
||||
|
||||
|
||||
def _list_resources(state: AppState) -> Dict[str, Any]:
|
||||
return {
|
||||
result = {
|
||||
"resources": [
|
||||
{
|
||||
"uri": "repo://graph",
|
||||
@@ -924,6 +960,12 @@ def _list_resources(state: AppState) -> Dict[str, Any]:
|
||||
},
|
||||
]
|
||||
}
|
||||
if not (state.root / "README.md").is_file():
|
||||
result["resources"] = [
|
||||
resource for resource in result["resources"]
|
||||
if resource["uri"] != "repo://readme"
|
||||
]
|
||||
return result
|
||||
|
||||
|
||||
def _read_resource(state: AppState, uri: str) -> Dict[str, Any]:
|
||||
@@ -994,7 +1036,8 @@ def _handle_request(state: AppState, message: Dict[str, Any]) -> Dict[str, Any]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
args = parse_vault_args(argparse.ArgumentParser(description=__doc__))
|
||||
root = args.vault_root
|
||||
state = AppState(root=root, graph_path=root / "index" / "graph.json")
|
||||
|
||||
reader = _StdinReader(sys.stdin.buffer)
|
||||
|
||||
+7
-3
@@ -1,11 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Generate and validate the selected vault graph before accepting a commit.
|
||||
set -euo pipefail
|
||||
|
||||
framework_root="$(cd -- "$(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")")/.." && pwd)"
|
||||
vault_root="${1:-${KNOWLEDGE_BASE_ROOT:-$framework_root}}"
|
||||
echo "[pre-commit] Generating and validating graph..."
|
||||
make graph
|
||||
"${PYTHON:-python3}" "$framework_root/scripts/generate-graph.py" --vault-root "$vault_root" --write --validate
|
||||
|
||||
if ! git diff --quiet -- index/graph.json; then
|
||||
if ! git -C "$vault_root" ls-files --error-unmatch index/graph.json >/dev/null 2>&1 || \
|
||||
! git -C "$vault_root" diff --quiet -- index/graph.json; then
|
||||
echo "[pre-commit] index/graph.json was modified by generation."
|
||||
echo "[pre-commit] Run 'make graph' and stage index/graph.json before committing."
|
||||
echo "[pre-commit] Stage the generated index/graph.json before committing."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Resolve the selected data directory consistently for framework command-line tools."""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
FRAMEWORK_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def parse_vault_args(parser: argparse.ArgumentParser) -> argparse.Namespace:
|
||||
"""Add the shared CLI contract and return a validated absolute vault root."""
|
||||
parser.add_argument(
|
||||
"--vault-root",
|
||||
help="data directory (default: KNOWLEDGE_BASE_ROOT, then framework root)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
raw = args.vault_root if args.vault_root is not None else os.environ.get(
|
||||
"KNOWLEDGE_BASE_ROOT", str(FRAMEWORK_ROOT)
|
||||
)
|
||||
if not raw.strip():
|
||||
parser.error("vault root must not be empty")
|
||||
root = Path(raw).expanduser().resolve()
|
||||
if not root.is_dir():
|
||||
parser.error(f"vault root must be an existing directory: {root}")
|
||||
args.vault_root = root
|
||||
return args
|
||||
Reference in New Issue
Block a user