File size: 7,231 Bytes
e63577a a83aca4 e63577a f908fa0 e63577a e42284c f908fa0 e63577a f908fa0 e63577a f908fa0 e63577a a83aca4 e63577a a83aca4 e63577a a83aca4 e63577a a83aca4 e63577a a83aca4 e63577a f908fa0 e63577a a83aca4 e63577a a83aca4 e63577a a83aca4 e63577a a83aca4 e63577a a83aca4 e63577a 93bb3e0 e63577a 93bb3e0 e63577a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
from __future__ import annotations
import os
from typing import Annotated, Optional
import gradio as gr
from app import _log_call_end, _log_call_start, _truncate_for_log
from ._docstrings import autodoc
from ._core import obsidian_sandbox, OBSIDIAN_ROOT
TOOL_SUMMARY = (
"Browse and search the Obsidian vault in read-only mode. "
"Actions: list, read, info, search, help. "
"All paths resolve within the vault root. Start paths with '/' (e.g., /Notes)."
)
HELP_TEXT = (
"Obsidian Vault — actions and usage\n\n"
"Root: Tools/Obsidian (override with OBSIDIAN_VAULT_ROOT). "
"Start paths with '/' to reference the vault root (e.g., /Projects/note.md). "
"Absolute paths are disabled unless UNSAFE_ALLOW_ABS_PATHS=1.\n\n"
"Actions and fields:\n"
"- list: path='/' (default), recursive=false, show_hidden=false, max_entries=20\n"
"- read: path (e.g., /Projects/note.md), offset=0, max_chars=4000 (shows next_cursor when truncated)\n"
"- info: path\n"
"- search: path (note or folder), query text in the Search field, recursive=false, show_hidden=false, max_entries=20, case_sensitive=false, offset=0\n"
"- help: show this guide\n\n"
"Errors are returned as JSON with fields: {status:'error', code, message, path?, hint?, data?}.\n\n"
"Examples:\n"
"- list current: action=list, path='/'\n"
"- read note: action=read, path='/Projects/note.md', max_chars=500\n"
"- show metadata: action=info, path='/Inbox'\n"
"- search notes: action=search, path='/Projects', query='deadline', recursive=true, max_entries=100\n"
"- case-sensitive search: action=search, query='TODO', case_sensitive=true\n"
"- page search results: action=search, query='TODO', offset=20\n"
)
# Use the pre-configured Obsidian sandbox
_sandbox = obsidian_sandbox
ROOT_DIR = OBSIDIAN_ROOT
# Convenience wrappers
def _resolve_path(path: str) -> tuple[str, str]:
return _sandbox.resolve_path(path)
def _display_path(abs_path: str) -> str:
return _sandbox.display_path(abs_path)
def _err(code: str, message: str, *, path: str | None = None, hint: str | None = None, data: dict | None = None) -> str:
return _sandbox.err(code, message, path=path, hint=hint, data=data)
@autodoc(summary=TOOL_SUMMARY)
def Obsidian_Vault(
action: Annotated[str, "Operation to perform: 'list', 'read', 'info', 'search', 'help'."],
path: Annotated[str, "Target path, relative to the vault root." ] = "/",
query: Annotated[Optional[str], "Text to search for when action=search."] = None,
recursive: Annotated[bool, "Recurse into subfolders when listing/searching."] = False,
show_hidden: Annotated[bool, "Include hidden files when listing/searching."] = False,
max_entries: Annotated[int, "Max entries to list or matches to return (for list/search)."] = 20,
offset: Annotated[int, "Start offset when reading files."] = 0,
max_chars: Annotated[int, "Max characters to return when reading (0 = full file)."] = 4000,
case_sensitive: Annotated[bool, "Match case when searching text."] = False,
) -> str:
_log_call_start(
"Obsidian_Vault",
action=action,
path=path,
query=query,
recursive=recursive,
show_hidden=show_hidden,
max_entries=max_entries,
offset=offset,
max_chars=max_chars,
case_sensitive=case_sensitive,
)
action = (action or "").strip().lower()
if action not in {"list", "read", "info", "search", "help"}:
result = _err(
"invalid_action",
"Invalid action.",
hint="Choose from: list, read, info, search, help.",
)
_log_call_end("Obsidian_Vault", _truncate_for_log(result))
return result
if action == "help":
result = HELP_TEXT
_log_call_end("Obsidian_Vault", _truncate_for_log(result))
return result
abs_path, err = _resolve_path(path)
if err:
_log_call_end("Obsidian_Vault", _truncate_for_log(err))
return err
try:
if action == "list":
if not os.path.exists(abs_path):
result = _err("path_not_found", f"Path not found: {_display_path(abs_path)}", path=_display_path(abs_path))
else:
result = _sandbox.list_dir(abs_path, show_hidden=show_hidden, recursive=recursive, max_entries=max_entries)
elif action == "read":
result = _sandbox.read_file(abs_path, offset=offset, max_chars=max_chars)
elif action == "search":
query_text = query or ""
if query_text.strip() == "":
result = _err(
"missing_search_query",
"Search query is required for the search action.",
hint="Provide text in the Search field to look for.",
)
else:
result = _sandbox.search_text(
abs_path,
query_text,
recursive=recursive,
show_hidden=show_hidden,
max_results=max_entries,
case_sensitive=case_sensitive,
start_index=offset,
)
else: # info
result = _sandbox.info(abs_path)
except Exception as exc:
result = _err("exception", "Unhandled error during operation.", data={"error": _sandbox.safe_err(exc)})
_log_call_end("Obsidian_Vault", _truncate_for_log(result))
return result
def build_interface() -> gr.Interface:
return gr.Interface(
fn=Obsidian_Vault,
inputs=[
gr.Radio(
label="Action",
choices=["list", "read", "info", "search", "help"],
value="help",
info="Operation to perform",
),
gr.Textbox(label="Path", placeholder="/ or /Notes/todo.md", max_lines=1, value="/", info="Target path (relative to vault root)"),
gr.Textbox(label="Search text", lines=3, placeholder="Text to search for...", info="Text to search for (Search only)"),
gr.Checkbox(label="Recursive", value=False, info="Recurse into subfolders (List/Search)"),
gr.Checkbox(label="Show hidden", value=False, info="Include hidden files (List/Search)"),
gr.Slider(minimum=10, maximum=5000, step=10, value=20, label="Max entries / matches", info="Max entries to list or matches to return (List/Search)"),
gr.Slider(minimum=0, maximum=1_000_000, step=100, value=0, label="Offset", info="Start offset (Read/Search)"),
gr.Slider(minimum=0, maximum=100_000, step=500, value=4000, label="Max chars", info="Max characters to return (Read, 0=all)"),
gr.Checkbox(label="Case sensitive search", value=False, info="Match case (Search)"),
],
outputs=gr.Textbox(label="Result", lines=20),
title="Obsidian Vault",
description=(
"<div style=\"text-align:center; overflow:hidden;\">Explore and search notes in the vault without modifying them." "</div>"
),
api_description=TOOL_SUMMARY,
flagging_mode="never",
submit_btn="Run",
)
__all__ = ["Obsidian_Vault", "build_interface"]
|