obsidian-agent-cli-app 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
obsidian_agent/cli.py ADDED
@@ -0,0 +1,54 @@
1
+ """Command-line entry point for obsidian-agent, built on Typer for
2
+ clean --help output and a lower-boilerplate command structure."""
3
+
4
+ import asyncio
5
+
6
+ import typer
7
+
8
+ from .config import CONFIG_FILE, load_config
9
+ from .doctor import run_doctor
10
+ from .loop import main_loop
11
+
12
+ app = typer.Typer(
13
+ name="obsidian-agent",
14
+ help="Chat with your Obsidian vault via an MCP-connected AI agent.",
15
+ add_completion=True,
16
+ no_args_is_help=False,
17
+ )
18
+
19
+
20
+ @app.command()
21
+ def chat():
22
+ """Start an interactive chat session (default command)."""
23
+ config = load_config()
24
+ asyncio.run(main_loop(config))
25
+
26
+
27
+ @app.command()
28
+ def doctor():
29
+ """Check that obsidian-mcp is installed and can reach your vault."""
30
+ config = load_config()
31
+ run_doctor(config)
32
+
33
+
34
+ @app.command()
35
+ def config(reset: bool = typer.Option(False, "--reset", help="Re-run first-time setup.")):
36
+ """View or reset your configuration."""
37
+ if reset:
38
+ load_config(force_setup=True)
39
+ return
40
+ typer.echo(f"Config file: {CONFIG_FILE}")
41
+ if CONFIG_FILE.exists():
42
+ typer.echo(CONFIG_FILE.read_text())
43
+ else:
44
+ typer.echo("No config yet — run 'obsidian-agent config --reset' to create one.")
45
+
46
+
47
+ @app.callback(invoke_without_command=True)
48
+ def main(ctx: typer.Context):
49
+ if ctx.invoked_subcommand is None:
50
+ chat()
51
+
52
+
53
+ if __name__ == "__main__":
54
+ app()
@@ -0,0 +1,60 @@
1
+ """User configuration: a one-time interactive setup, stored under the
2
+ platform's standard config directory, with env-var overrides for power
3
+ users."""
4
+
5
+ import os
6
+ import tomllib
7
+ from pathlib import Path
8
+
9
+ CONFIG_DIR = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "obsidian-agent"
10
+ CONFIG_FILE = CONFIG_DIR / "config.toml"
11
+
12
+ DEFAULT_MODEL = "meta-llama/llama-3.1-8b-instruct"
13
+
14
+
15
+ def _write_config(config: dict):
16
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
17
+ with open(CONFIG_FILE, "w") as f:
18
+ for key, value in config.items():
19
+ f.write(f'{key} = "{value}"\n')
20
+ print(f"Saved config to {CONFIG_FILE}")
21
+
22
+
23
+ def run_first_time_setup() -> dict:
24
+ print("First-time setup for obsidian-agent.\n")
25
+ api_key = input("Enter your OpenRouter API key: ").strip()
26
+ vault_path = input("Enter the absolute path to your Obsidian vault: ").strip()
27
+ model = input(f"Model to use [{DEFAULT_MODEL}]: ").strip() or DEFAULT_MODEL
28
+
29
+ config = {
30
+ "openrouter_api_key": api_key,
31
+ "vault_path": vault_path,
32
+ "model": model,
33
+ }
34
+ _write_config(config)
35
+ return config
36
+
37
+
38
+ def load_config(force_setup: bool = False) -> dict:
39
+ if force_setup or not CONFIG_FILE.exists():
40
+ config = run_first_time_setup()
41
+ else:
42
+ with open(CONFIG_FILE, "rb") as f:
43
+ config = tomllib.load(f)
44
+
45
+ # Env vars always win, for power users / CI / testing.
46
+ config["openrouter_api_key"] = os.environ.get(
47
+ "OPENROUTER_API_KEY", config.get("openrouter_api_key")
48
+ )
49
+ config["vault_path"] = os.environ.get(
50
+ "OBSIDIAN_VAULT_PATH", config.get("vault_path")
51
+ )
52
+ config["model"] = os.environ.get("AGENT_MODEL", config.get("model", DEFAULT_MODEL))
53
+
54
+ missing = [k for k in ("openrouter_api_key", "vault_path") if not config.get(k)]
55
+ if missing:
56
+ raise ValueError(
57
+ f"Missing required config: {', '.join(missing)}. "
58
+ f"Run 'obsidian-agent config --reset' to set up again."
59
+ )
60
+ return config
obsidian_agent/core.py ADDED
@@ -0,0 +1,55 @@
1
+ """Shared building blocks: MCP session helpers, tool-schema conversion,
2
+ and the OpenRouter/OpenAI client. Both the chat loop and the doctor
3
+ command build on this."""
4
+
5
+ import os
6
+ from mcp import StdioServerParameters
7
+ from openai import OpenAI
8
+
9
+ SYSTEM_PROMPT = (
10
+ "You are a helpful assistant with access to the user's Obsidian vault. "
11
+ "Always read a note before updating it so you don't overwrite existing "
12
+ "content by accident, unless the user clearly asks you to replace it."
13
+ )
14
+
15
+
16
+ def build_llm_client(api_key: str) -> OpenAI:
17
+ return OpenAI(
18
+ api_key=api_key,
19
+ base_url="https://openrouter.ai/api/v1",
20
+ )
21
+
22
+
23
+ def build_server_params(vault_path: str) -> StdioServerParameters:
24
+ return StdioServerParameters(
25
+ command="obsidian-mcp",
26
+ args=[],
27
+ env={
28
+ **os.environ,
29
+ "OBSIDIAN_VAULT_PATH": vault_path,
30
+ # Quiet the server's own logging; we only want MCP protocol
31
+ # traffic on stdio, not its log lines colliding with our UI.
32
+ "OBSIDIAN_LOG_LEVEL": "ERROR",
33
+ },
34
+ )
35
+
36
+
37
+ def convert_mcp_tool_to_openai(tool) -> dict:
38
+ return {
39
+ "type": "function",
40
+ "function": {
41
+ "name": tool.name,
42
+ "description": tool.description,
43
+ "parameters": tool.inputSchema,
44
+ },
45
+ }
46
+
47
+
48
+ async def execute_tool_call(tool_name: str, arguments: dict, session) -> str:
49
+ try:
50
+ result = await session.call_tool(tool_name, arguments=arguments)
51
+ if result.content and len(result.content) > 0:
52
+ return result.content[0].text
53
+ return "Tool executed but returned no content."
54
+ except Exception as e:
55
+ return f"Error calling tool: {e}"
@@ -0,0 +1,67 @@
1
+ """Sanity checks: is obsidian-mcp installed, and can we actually reach
2
+ tools on the configured vault. This is test_obsidian_mcp.py, promoted to
3
+ a real subcommand with rich output."""
4
+
5
+ import asyncio
6
+ import json
7
+ import os
8
+ import shutil
9
+
10
+ from mcp import ClientSession
11
+ from mcp.client.stdio import stdio_client
12
+
13
+ from . import ui
14
+ from .core import build_server_params
15
+
16
+
17
+ def check_obsidian_mcp_installed() -> bool:
18
+ found = shutil.which("obsidian-mcp") is not None
19
+ if not found:
20
+ ui.console.print("[bold red]✗[/] obsidian-mcp was not found on your PATH.")
21
+ ui.console.print(" Install it with: [cyan]pip install obsidian-mcp[/]")
22
+ else:
23
+ ui.console.print("[bold green]✓[/] obsidian-mcp found on PATH.")
24
+ return found
25
+
26
+
27
+ async def check_vault_connection(vault_path: str):
28
+ server_params = build_server_params(vault_path)
29
+ ui.console.print(f"[dim]Connecting to vault:[/] {vault_path}")
30
+
31
+ # Same stderr redirect as loop.py — keep obsidian-mcp's own log
32
+ # lines from cluttering the doctor output.
33
+ with open(os.devnull, "w") as devnull:
34
+ async with stdio_client(server_params, errlog=devnull) as (read_stream, write_stream):
35
+ async with ClientSession(read_stream, write_stream) as session:
36
+ await session.initialize()
37
+
38
+ tools_result = await session.list_tools()
39
+ ui.console.print(f"[bold green]✓[/] Connected. {len(tools_result.tools)} tools available:")
40
+ for tool in tools_result.tools:
41
+ desc = (tool.description or "").strip().splitlines()
42
+ desc = desc[0] if desc else ""
43
+ ui.console.print(f" [cyan]{tool.name}[/] [dim]{desc}[/]")
44
+
45
+ ui.console.print("\n[dim]Listing notes in vault root...[/]")
46
+ result = await session.call_tool("list_notes_tool", arguments={"recursive": True})
47
+ content = result.content[0].text if result.content else "No data"
48
+ try:
49
+ data = json.loads(content)
50
+ items = data.get("items", [])
51
+ ui.console.print(f"[bold green]✓[/] Found {data.get('total', len(items))} notes. First 10:")
52
+ for note in items[:10]:
53
+ ui.console.print(f" [dim]-[/] {note.get('path')}")
54
+ except json.JSONDecodeError:
55
+ ui.console.print("[yellow]Raw response:[/]", content)
56
+
57
+
58
+ def run_doctor(config: dict):
59
+ ui.console.rule("[bold cyan]obsidian-agent doctor")
60
+ if not check_obsidian_mcp_installed():
61
+ raise SystemExit(1)
62
+ try:
63
+ asyncio.run(check_vault_connection(config["vault_path"]))
64
+ except Exception as e:
65
+ ui.print_error(f"Could not connect to vault: {e}")
66
+ raise SystemExit(1)
67
+ ui.console.print("\n[bold green]All checks passed.[/]")
obsidian_agent/loop.py ADDED
@@ -0,0 +1,148 @@
1
+ """The agentic chat loop: keeps calling tools until the model responds
2
+ with plain text instead of another tool call, instead of stopping after
3
+ one round. Now with streaming output and slash commands."""
4
+
5
+ import json
6
+ import os
7
+ from contextlib import AsyncExitStack
8
+
9
+ from mcp import ClientSession
10
+ from mcp.client.stdio import stdio_client
11
+
12
+ from . import ui
13
+ from .core import (
14
+ SYSTEM_PROMPT,
15
+ build_llm_client,
16
+ build_server_params,
17
+ convert_mcp_tool_to_openai,
18
+ execute_tool_call,
19
+ )
20
+
21
+ MAX_STEPS = 8
22
+ EXTRA_HEADERS = {
23
+ "HTTP-Referer": "http://localhost:8080",
24
+ "X-Title": "Obsidian Agent",
25
+ }
26
+
27
+
28
+ async def run_turn(client_llm, model, messages, tools, session, max_steps=MAX_STEPS):
29
+ """Keep calling tools, feeding results back to the model, until it
30
+ responds with plain text instead of another tool call, or the step
31
+ budget runs out."""
32
+ for _ in range(max_steps):
33
+ with ui.thinking_spinner():
34
+ response = client_llm.chat.completions.create(
35
+ model=model,
36
+ messages=messages,
37
+ tools=tools,
38
+ tool_choice="auto",
39
+ temperature=0.7,
40
+ extra_headers=EXTRA_HEADERS,
41
+ )
42
+ msg = response.choices[0].message
43
+ messages.append(msg.to_dict())
44
+
45
+ if not msg.tool_calls:
46
+ return msg.content
47
+
48
+ for tool_call in msg.tool_calls:
49
+ try:
50
+ args = json.loads(tool_call.function.arguments)
51
+ except json.JSONDecodeError:
52
+ result_text = "Error: malformed tool arguments"
53
+ args = {}
54
+ else:
55
+ ui.print_tool_call(tool_call.function.name, args)
56
+ result_text = await execute_tool_call(
57
+ tool_call.function.name, args, session
58
+ )
59
+ messages.append(
60
+ {
61
+ "role": "tool",
62
+ "tool_call_id": tool_call.id,
63
+ "content": result_text,
64
+ }
65
+ )
66
+
67
+ return "Stopped after too many steps — the task may be more complex than expected."
68
+
69
+
70
+ def handle_slash_command(command: str, messages: list, tools: list, system_prompt: str) -> bool:
71
+ """Returns True if the input was a slash command (and was handled)."""
72
+ if command == "/help":
73
+ ui.print_help()
74
+ return True
75
+ if command == "/tools":
76
+ ui.print_tools(tools)
77
+ return True
78
+ if command == "/clear":
79
+ messages.clear()
80
+ messages.append({"role": "system", "content": system_prompt})
81
+ ui.console.print("[dim]Conversation cleared.[/]\n")
82
+ return True
83
+ return False
84
+
85
+
86
+ async def main_loop(config: dict):
87
+ vault_path = config["vault_path"]
88
+ model = config["model"]
89
+ api_key = config["openrouter_api_key"]
90
+
91
+ client_llm = build_llm_client(api_key)
92
+ server_params = build_server_params(vault_path)
93
+
94
+ # AsyncExitStack keeps the stdio subprocess and MCP session alive for
95
+ # the whole chat loop. Returning a session from inside a narrower
96
+ # "async with" block would close the connection the moment that
97
+ # function returns — every tool call after that would silently fail.
98
+ async with AsyncExitStack() as stack:
99
+ with ui.console.status("[dim]starting obsidian-mcp...[/]"):
100
+ # Redirect the subprocess's stderr to devnull so its own log
101
+ # lines (fastmcp INFO logs, authlib deprecation warnings)
102
+ # don't interleave with our Rich output. A plain sync file
103
+ # object is fine here — errlog just needs to be file-like.
104
+ devnull = stack.enter_context(open(os.devnull, "w"))
105
+ read_stream, write_stream = await stack.enter_async_context(
106
+ stdio_client(server_params, errlog=devnull)
107
+ )
108
+ session = await stack.enter_async_context(
109
+ ClientSession(read_stream, write_stream)
110
+ )
111
+ await session.initialize()
112
+ tools_result = await session.list_tools()
113
+
114
+ if not tools_result.tools:
115
+ ui.print_error("No tools available from obsidian-mcp. Exiting.")
116
+ return
117
+
118
+ openai_tools = [convert_mcp_tool_to_openai(t) for t in tools_result.tools]
119
+ messages = [{"role": "system", "content": SYSTEM_PROMPT}]
120
+
121
+ ui.print_banner(vault_path, model)
122
+
123
+ while True:
124
+ try:
125
+ ui.print_turn_separator()
126
+ user_input = ui.prompt_user()
127
+ except (EOFError, KeyboardInterrupt):
128
+ ui.console.print("\n[dim]Goodbye.[/]")
129
+ break
130
+
131
+ stripped = user_input.strip()
132
+ if not stripped:
133
+ continue
134
+ if stripped.lower() in ("exit", "quit", "/exit", "/quit"):
135
+ ui.console.print("[dim]Goodbye.[/]")
136
+ break
137
+ if stripped.startswith("/"):
138
+ if handle_slash_command(stripped, messages, openai_tools, SYSTEM_PROMPT):
139
+ continue
140
+
141
+ messages.append({"role": "user", "content": user_input})
142
+
143
+ try:
144
+ reply = await run_turn(client_llm, model, messages, openai_tools, session)
145
+ messages.append({"role": "assistant", "content": reply})
146
+ ui.print_agent_reply(reply)
147
+ except Exception as e:
148
+ ui.print_error(str(e))
obsidian_agent/ui.py ADDED
@@ -0,0 +1,80 @@
1
+ """Terminal presentation layer: banner, spinners, markdown rendering,
2
+ colored tool-call output. Kept separate from the agent logic so the
3
+ core loop doesn't need to know or care how things are displayed."""
4
+
5
+ from rich.console import Console
6
+ from rich.markdown import Markdown
7
+ from rich.panel import Panel
8
+ from rich.spinner import Spinner
9
+ from rich.live import Live
10
+ from rich.text import Text
11
+ from rich.rule import Rule
12
+
13
+ console = Console()
14
+
15
+ BANNER = r"""
16
+ [bold cyan] ██████╗ ██████╗ ███████╗██╗██████╗ ██╗ █████╗ ███╗ ██╗[/]
17
+ [bold cyan]██╔═══██╗██╔══██╗██╔════╝██║██╔══██╗██║██╔══██╗████╗ ██║[/]
18
+ [bold cyan]██║ ██║██████╔╝███████╗██║██║ ██║██║███████║██╔██╗ ██║[/]
19
+ [bold cyan]██║ ██║██╔══██╗╚════██║██║██║ ██║██║██╔══██║██║╚██╗██║[/]
20
+ [bold cyan]╚██████╔╝██████╔╝███████║██║██████╔╝██║██║ ██║██║ ╚████║[/]
21
+ [bold cyan] ╚═════╝ ╚═════╝ ╚══════╝╚═╝╚═════╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝[/]
22
+ [dim] a g e n t[/]
23
+ """
24
+
25
+
26
+ def print_banner(vault_path: str, model: str):
27
+ console.print(BANNER)
28
+ console.print(f"[dim]vault:[/] {vault_path}")
29
+ console.print(f"[dim]model:[/] {model}")
30
+ console.print("[dim]Type a message, or /help for commands.[/]\n")
31
+
32
+
33
+ def print_help():
34
+ console.print(
35
+ Panel.fit(
36
+ "[bold]/help[/] show this message\n"
37
+ "[bold]/tools[/] list available vault tools\n"
38
+ "[bold]/clear[/] clear the conversation history\n"
39
+ "[bold]/exit[/] quit [dim](also: exit, quit, Ctrl+D)[/]",
40
+ title="commands",
41
+ border_style="cyan",
42
+ )
43
+ )
44
+
45
+
46
+ def print_tools(tools: list):
47
+ lines = []
48
+ for t in tools:
49
+ name = t["function"]["name"]
50
+ desc = (t["function"]["description"] or "").strip().splitlines()
51
+ desc = desc[0] if desc else ""
52
+ lines.append(f"[bold cyan]{name}[/] [dim]{desc}[/]")
53
+ console.print(Panel.fit("\n".join(lines), title=f"{len(tools)} tools available", border_style="cyan"))
54
+
55
+
56
+ def print_tool_call(name: str, arguments: dict):
57
+ args_str = ", ".join(f"{k}={v!r}" for k, v in arguments.items())
58
+ console.print(f" [dim]→ using[/] [bold yellow]{name}[/]([dim]{args_str}[/])")
59
+
60
+
61
+ def print_agent_reply(text: str):
62
+ console.print()
63
+ console.print("[bold magenta]◆ agent[/]")
64
+ console.print(Markdown(text or "*(no response)*"), style="")
65
+ console.print()
66
+
67
+
68
+ def print_error(message: str):
69
+ console.print(f"[bold red]Error:[/] {message}")
70
+
71
+
72
+ def thinking_spinner():
73
+ """Returns a context-manager Live spinner; use with `with thinking_spinner():`."""
74
+ return Live(Spinner("dots", text=Text(" thinking...", style="dim")), console=console, refresh_per_second=10, transient=True)
75
+
76
+ def print_turn_separator():
77
+ console.print(Rule(style="dim"))
78
+
79
+ def prompt_user() -> str:
80
+ return console.input("[bold cyan]❯[/] ")
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.4
2
+ Name: obsidian-agent-cli-app
3
+ Version: 0.1.0
4
+ Summary: A conversational AI agent for your Obsidian vault, connected via MCP.
5
+ License: MIT
6
+ License-File: LICENCE
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: fastmcp==2.8.1
9
+ Requires-Dist: mcp>=1.0.0
10
+ Requires-Dist: obsidian-mcp>=2.1.6
11
+ Requires-Dist: openai>=1.0.0
12
+ Requires-Dist: pydantic<2.12,>=2.0
13
+ Requires-Dist: rich>=13.0.0
14
+ Requires-Dist: typer>=0.12.0
15
+ Description-Content-Type: text/markdown
16
+
17
+ # obsidian-agent
18
+
19
+ A conversational AI agent for your Obsidian vault. Ask it to find notes,
20
+ summarize what you wrote last week, fix broken links, tag things, or
21
+ edit a note — it figures out which vault operations to run and does it,
22
+ chaining multiple steps together when needed.
23
+
24
+ Built on [MCP](https://modelcontextprotocol.io) (the same protocol Claude
25
+ Desktop uses for tool access) and [OpenRouter](https://openrouter.ai) for
26
+ the LLM, so you can point it at whichever model you like.
27
+
28
+ ## Requirements
29
+
30
+ - Python 3.11+
31
+ - An [OpenRouter](https://openrouter.ai/keys) API key (free tier available)
32
+ - An Obsidian vault on your local filesystem
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install git+https://github.com/ZedoMak/obsidian-agent.git
38
+ ```
39
+
40
+ This pulls in `obsidian-mcp` (the vault-access server) automatically —
41
+ no separate install step needed.
42
+
43
+ ## First run
44
+
45
+ ```bash
46
+ obsidian-agent
47
+ ```
48
+
49
+ You'll be asked three things once, and never again:
50
+
51
+ - your OpenRouter API key
52
+ - the absolute path to your vault
53
+ - which model to use (defaults to a free-tier model if you're not sure)
54
+
55
+ This gets saved to `~/.config/obsidian-agent/config.toml`. Change it later
56
+ with `obsidian-agent config --reset`, or override any single value with an
57
+ environment variable: `OPENROUTER_API_KEY`, `OBSIDIAN_VAULT_PATH`, `AGENT_MODEL`.
58
+
59
+ ## Usage
60
+
61
+ ```bash
62
+ obsidian-agent # start chatting with your vault
63
+ obsidian-agent doctor # troubleshooting: checks obsidian-mcp is installed
64
+ # and can actually reach your vault
65
+ obsidian-agent config # view current config
66
+ ```
67
+
68
+ Inside a chat session:
69
+
70
+ | Command | Does |
71
+ | -------- | --------------------------------------------- |
72
+ | `/help` | list commands |
73
+ | `/tools` | list every vault operation the agent can call |
74
+ | `/clear` | wipe conversation history, start fresh |
75
+ | `/exit` | quit (also: `exit`, `quit`, Ctrl+D) |
76
+
77
+ ## What it can actually do
78
+
79
+ Anything the underlying `obsidian-mcp` server exposes — reading, creating,
80
+ editing, and deleting notes; searching by text, tag, date, or regex;
81
+ managing tags; finding backlinks and broken links; listing orphaned notes.
82
+ Run `/tools` inside a session to see the live list for your installed version.
83
+
84
+ The agent always reads a note before editing it, so it won't blindly
85
+ overwrite content — this is a system-level instruction, not something you
86
+ need to ask for each time.
87
+
88
+ ## Troubleshooting
89
+
90
+ **`obsidian-mcp was not found on your PATH`**
91
+ Run `obsidian-agent doctor` — it'll tell you exactly what's missing and how
92
+ to fix it.
93
+
94
+ **Something else broke**
95
+ This project pins a few dependency versions deliberately
96
+ (`fastmcp==2.8.1`, `pydantic<2.12`) because of real breaking changes
97
+ upstream. If you're hacking on the source directly rather than using
98
+ `pip install`, don't loosen those pins without checking `obsidian-agent doctor`
99
+ still passes afterward.
100
+
101
+ ## Contributing
102
+
103
+ Issues and PRs welcome. This is a young project — expect rough edges.
104
+
105
+ ## License
106
+
107
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,12 @@
1
+ obsidian_agent/__init__.py,sha256=Pru0BlFBASFCFo7McHdohtKkUtgMPDwbGfyUZlE2_Vw,21
2
+ obsidian_agent/cli.py,sha256=IqyJtBUWFMQNWrNQcwmdU4okaM_V914bitZoxye3JkQ,1356
3
+ obsidian_agent/config.py,sha256=H0iqS1rvqKLZkDOPWUh_I8zyLiHMG72k_Moeqopg6As,2016
4
+ obsidian_agent/core.py,sha256=EnG8sgCAAMv9ZmSgq0caBbkK_dBZqy7pV8NTOHJsw_0,1713
5
+ obsidian_agent/doctor.py,sha256=qH4sTBs5h0Rm89UK5wPvDCsjGkTAR4Pg3KCTFvTxOgY,2856
6
+ obsidian_agent/loop.py,sha256=KMIatOJ2WzpJHiAo3XvlegXVksm41J0PUEXFJWbzu4A,5378
7
+ obsidian_agent/ui.py,sha256=ELMfuG4IlT0StN4jCziZJibbJuDw4oK8KIpRYlCd-NI,3422
8
+ obsidian_agent_cli_app-0.1.0.dist-info/METADATA,sha256=NMnAq83pkgOiofFttQr6qqtjEj-nxNrNlQMsP8gm7a4,3525
9
+ obsidian_agent_cli_app-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
10
+ obsidian_agent_cli_app-0.1.0.dist-info/entry_points.txt,sha256=OGzPSLDZvRQGVmdqu9RJtD2wUvB7QdKgdZsdCNSYReg,58
11
+ obsidian_agent_cli_app-0.1.0.dist-info/licenses/LICENCE,sha256=hZMBcHPqgQ9qHgCSKl9_HKpzVpV_HRS8D-G05n9PbP8,1064
12
+ obsidian_agent_cli_app-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ obsidian-agent = obsidian_agent.cli:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ZedoMak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.