obsidian-agent-cli 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.
- obsidian_agent_cli-0.1.0.dist-info/METADATA +225 -0
- obsidian_agent_cli-0.1.0.dist-info/RECORD +40 -0
- obsidian_agent_cli-0.1.0.dist-info/WHEEL +5 -0
- obsidian_agent_cli-0.1.0.dist-info/entry_points.txt +2 -0
- obsidian_agent_cli-0.1.0.dist-info/top_level.txt +1 -0
- obsidian_cli/__init__.py +0 -0
- obsidian_cli/canvas_builder.py +127 -0
- obsidian_cli/commands/__init__.py +0 -0
- obsidian_cli/commands/active.py +74 -0
- obsidian_cli/commands/batch_cmd.py +91 -0
- obsidian_cli/commands/canvas.py +123 -0
- obsidian_cli/commands/config_cmd.py +57 -0
- obsidian_cli/commands/core_cmd.py +83 -0
- obsidian_cli/commands/excalidraw.py +146 -0
- obsidian_cli/commands/export_cmd.py +40 -0
- obsidian_cli/commands/git_cmd.py +112 -0
- obsidian_cli/commands/kanban.py +113 -0
- obsidian_cli/commands/lint_cmd.py +26 -0
- obsidian_cli/commands/meta_cmd.py +97 -0
- obsidian_cli/commands/mover_cmd.py +78 -0
- obsidian_cli/commands/note.py +178 -0
- obsidian_cli/commands/periodic.py +127 -0
- obsidian_cli/commands/quickadd_cmd.py +76 -0
- obsidian_cli/commands/refactor_cmd.py +81 -0
- obsidian_cli/commands/run.py +26 -0
- obsidian_cli/commands/search.py +44 -0
- obsidian_cli/commands/status_cmd.py +13 -0
- obsidian_cli/commands/tags_cmd.py +22 -0
- obsidian_cli/commands/tasks_cmd.py +125 -0
- obsidian_cli/commands/teach.py +43 -0
- obsidian_cli/commands/template_cmd.py +45 -0
- obsidian_cli/commands/uri.py +59 -0
- obsidian_cli/commands/vault_cmd.py +40 -0
- obsidian_cli/commands/workspace_cmd.py +67 -0
- obsidian_cli/config.py +40 -0
- obsidian_cli/excalidraw_builder.py +210 -0
- obsidian_cli/kanban_builder.py +60 -0
- obsidian_cli/main.py +60 -0
- obsidian_cli/registry.py +98 -0
- obsidian_cli/transport.py +479 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import click
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from ..config import load_config
|
|
5
|
+
from ..transport import Transport
|
|
6
|
+
from ..canvas_builder import CanvasBuilder
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group()
|
|
10
|
+
def canvas():
|
|
11
|
+
"""Canvas creation and management."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@canvas.command()
|
|
15
|
+
@click.argument("name")
|
|
16
|
+
@click.option("--folder", default=None, help="Vault path for canvas (default: AI Workspace/)")
|
|
17
|
+
def create(name, folder):
|
|
18
|
+
cfg = load_config()
|
|
19
|
+
if folder is None:
|
|
20
|
+
folder = cfg.workspace_path
|
|
21
|
+
vault_path = f"{folder}/{name}.canvas"
|
|
22
|
+
abs_path = Path(cfg.vault_path) / vault_path
|
|
23
|
+
abs_path.parent.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
abs_path.write_text(json.dumps({"nodes": [], "edges": []}, indent=2), encoding="utf-8")
|
|
25
|
+
click.echo(json.dumps({"ok": True, "path": vault_path}))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@canvas.command()
|
|
29
|
+
@click.argument("canvas_path")
|
|
30
|
+
def read(canvas_path):
|
|
31
|
+
cfg = load_config()
|
|
32
|
+
abs_path = Path(cfg.vault_path) / canvas_path
|
|
33
|
+
if not abs_path.exists():
|
|
34
|
+
click.echo(json.dumps({"error": True, "message": f"Not found: {canvas_path}"}))
|
|
35
|
+
return
|
|
36
|
+
data = json.loads(abs_path.read_text(encoding="utf-8"))
|
|
37
|
+
click.echo(json.dumps(data))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@canvas.command("add-node")
|
|
41
|
+
@click.argument("canvas_path")
|
|
42
|
+
@click.option("--type", "node_type", required=True,
|
|
43
|
+
type=click.Choice(["text", "file", "link", "group"]))
|
|
44
|
+
@click.option("--text", default="")
|
|
45
|
+
@click.option("--file", "file_path", default="")
|
|
46
|
+
@click.option("--url", default="")
|
|
47
|
+
@click.option("--label", default="")
|
|
48
|
+
@click.option("--color", default="")
|
|
49
|
+
def add_node(canvas_path, node_type, text, file_path, url, label, color):
|
|
50
|
+
cfg = load_config()
|
|
51
|
+
abs_path = Path(cfg.vault_path) / canvas_path
|
|
52
|
+
if not abs_path.exists():
|
|
53
|
+
click.echo(json.dumps({"error": True, "message": f"Canvas not found: {canvas_path}"}))
|
|
54
|
+
return
|
|
55
|
+
data = json.loads(abs_path.read_text(encoding="utf-8"))
|
|
56
|
+
cb = CanvasBuilder()
|
|
57
|
+
cb._nodes = data["nodes"]
|
|
58
|
+
cb._edges = data["edges"]
|
|
59
|
+
if node_type == "text":
|
|
60
|
+
nid = cb.add_text_node(text, color=color)
|
|
61
|
+
elif node_type == "file":
|
|
62
|
+
nid = cb.add_file_node(file_path, color=color)
|
|
63
|
+
elif node_type == "link":
|
|
64
|
+
nid = cb.add_link_node(url, color=color)
|
|
65
|
+
elif node_type == "group":
|
|
66
|
+
nid = cb.add_group_node(label=label, color=color)
|
|
67
|
+
else:
|
|
68
|
+
click.echo(json.dumps({"error": True, "message": f"Unknown node type: {node_type}"}))
|
|
69
|
+
return
|
|
70
|
+
abs_path.write_text(cb.to_json(), encoding="utf-8")
|
|
71
|
+
click.echo(json.dumps({"ok": True, "node_id": nid}))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@canvas.command("add-edge")
|
|
75
|
+
@click.argument("canvas_path")
|
|
76
|
+
@click.argument("from_id")
|
|
77
|
+
@click.argument("to_id")
|
|
78
|
+
@click.option("--label", default="")
|
|
79
|
+
@click.option("--color", default="")
|
|
80
|
+
def add_edge(canvas_path, from_id, to_id, label, color):
|
|
81
|
+
cfg = load_config()
|
|
82
|
+
abs_path = Path(cfg.vault_path) / canvas_path
|
|
83
|
+
if not abs_path.exists():
|
|
84
|
+
click.echo(json.dumps({"error": True, "message": f"Canvas not found: {canvas_path}"}))
|
|
85
|
+
return
|
|
86
|
+
data = json.loads(abs_path.read_text(encoding="utf-8"))
|
|
87
|
+
cb = CanvasBuilder()
|
|
88
|
+
cb._nodes = data["nodes"]
|
|
89
|
+
cb._edges = data["edges"]
|
|
90
|
+
eid = cb.add_edge(from_id, to_id, label=label, color=color)
|
|
91
|
+
abs_path.write_text(cb.to_json(), encoding="utf-8")
|
|
92
|
+
click.echo(json.dumps({"ok": True, "edge_id": eid}))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@canvas.command()
|
|
96
|
+
@click.argument("name")
|
|
97
|
+
@click.option("--spec", required=True, help="JSON spec string")
|
|
98
|
+
@click.option("--folder", default=None)
|
|
99
|
+
def build(name, spec, folder):
|
|
100
|
+
cfg = load_config()
|
|
101
|
+
if folder is None:
|
|
102
|
+
folder = cfg.workspace_path
|
|
103
|
+
try:
|
|
104
|
+
spec_data = json.loads(spec)
|
|
105
|
+
except json.JSONDecodeError as e:
|
|
106
|
+
click.echo(json.dumps({"error": True, "message": f"Invalid JSON spec: {e}"}))
|
|
107
|
+
return
|
|
108
|
+
cb = CanvasBuilder.from_spec(spec_data)
|
|
109
|
+
vault_path = f"{folder}/{name}.canvas"
|
|
110
|
+
abs_path = Path(cfg.vault_path) / vault_path
|
|
111
|
+
abs_path.parent.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
abs_path.write_text(cb.to_json(), encoding="utf-8")
|
|
113
|
+
click.echo(json.dumps({"ok": True, "path": vault_path,
|
|
114
|
+
"nodes": len(cb._nodes), "edges": len(cb._edges)}))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@canvas.command("open")
|
|
118
|
+
@click.argument("canvas_path")
|
|
119
|
+
def open_canvas(canvas_path):
|
|
120
|
+
cfg = load_config()
|
|
121
|
+
t = Transport(cfg)
|
|
122
|
+
result = t.open_in_obsidian(canvas_path)
|
|
123
|
+
click.echo(json.dumps(result))
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import click
|
|
3
|
+
from ..config import load_config, save_config, Config
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@click.group("config")
|
|
7
|
+
def config_cmd():
|
|
8
|
+
"""View and update CLI configuration."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@config_cmd.command()
|
|
12
|
+
def show():
|
|
13
|
+
cfg = load_config()
|
|
14
|
+
data = {
|
|
15
|
+
"vault_path": cfg.vault_path,
|
|
16
|
+
"workspace_path": cfg.workspace_path,
|
|
17
|
+
"teach_path": cfg.teach_path,
|
|
18
|
+
"api_url": cfg.api_url,
|
|
19
|
+
"api_key": "***" if cfg.api_key else "(not set)",
|
|
20
|
+
"api_verify_ssl": cfg.api_verify_ssl,
|
|
21
|
+
}
|
|
22
|
+
click.echo(json.dumps(data, indent=2))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@config_cmd.command()
|
|
26
|
+
@click.argument("key")
|
|
27
|
+
@click.argument("value")
|
|
28
|
+
def set(key, value):
|
|
29
|
+
cfg = load_config()
|
|
30
|
+
if not hasattr(cfg, key):
|
|
31
|
+
click.echo(json.dumps({"error": True, "message": f"Unknown key: {key}"}))
|
|
32
|
+
return
|
|
33
|
+
field_type = type(getattr(cfg, key))
|
|
34
|
+
if field_type == bool:
|
|
35
|
+
value = value.lower() in ("true", "1", "yes")
|
|
36
|
+
if key == "api_key":
|
|
37
|
+
click.echo("Warning: api_key set via CLI is visible in shell history. Use obsidian config init instead.", err=True)
|
|
38
|
+
setattr(cfg, key, value)
|
|
39
|
+
save_config(cfg)
|
|
40
|
+
click.echo(json.dumps({"ok": True, "key": key}))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@config_cmd.command()
|
|
44
|
+
def init():
|
|
45
|
+
"""Interactive first-time setup."""
|
|
46
|
+
click.echo("Obsidian CLI Setup")
|
|
47
|
+
click.echo("------------------")
|
|
48
|
+
vault = click.prompt("Vault path (full path to your Obsidian vault folder)").lstrip("")
|
|
49
|
+
api_key = click.prompt("API key (from Obsidian Local REST API plugin settings)", default="").lstrip("")
|
|
50
|
+
api_url = click.prompt("API URL", default="http://127.0.0.1:27123").lstrip("")
|
|
51
|
+
cfg = Config(
|
|
52
|
+
vault_path=vault,
|
|
53
|
+
api_url=api_url,
|
|
54
|
+
api_key=api_key,
|
|
55
|
+
)
|
|
56
|
+
save_config(cfg)
|
|
57
|
+
click.echo(json.dumps({"ok": True, "message": "Config saved. Run: obsidian config show"}))
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import click
|
|
3
|
+
from ..config import load_config
|
|
4
|
+
from ..transport import Transport
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@click.group("core")
|
|
8
|
+
def core_cmd():
|
|
9
|
+
"""Core Obsidian commands (open, search, settings, command palette)."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@core_cmd.command("open")
|
|
13
|
+
@click.argument("note_path")
|
|
14
|
+
def open_note(note_path):
|
|
15
|
+
"""Open a note in the Obsidian UI."""
|
|
16
|
+
cfg = load_config()
|
|
17
|
+
t = Transport(cfg)
|
|
18
|
+
result = t.run_command(f"app:open-file:{note_path}")
|
|
19
|
+
click.echo(json.dumps({**result, "note": note_path}))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@core_cmd.command("palette")
|
|
23
|
+
def command_palette():
|
|
24
|
+
"""Open the Obsidian command palette."""
|
|
25
|
+
cfg = load_config()
|
|
26
|
+
t = Transport(cfg)
|
|
27
|
+
result = t.run_command("command-palette:open")
|
|
28
|
+
click.echo(json.dumps(result))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@core_cmd.command("search")
|
|
32
|
+
def open_search():
|
|
33
|
+
"""Open Obsidian global search."""
|
|
34
|
+
cfg = load_config()
|
|
35
|
+
t = Transport(cfg)
|
|
36
|
+
result = t.run_command("global-search:open")
|
|
37
|
+
click.echo(json.dumps(result))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@core_cmd.command("settings")
|
|
41
|
+
def open_settings():
|
|
42
|
+
"""Open Obsidian settings."""
|
|
43
|
+
cfg = load_config()
|
|
44
|
+
t = Transport(cfg)
|
|
45
|
+
result = t.run_command("app:open-settings")
|
|
46
|
+
click.echo(json.dumps(result))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@core_cmd.command("graph")
|
|
50
|
+
def open_graph():
|
|
51
|
+
"""Open Obsidian graph view."""
|
|
52
|
+
cfg = load_config()
|
|
53
|
+
t = Transport(cfg)
|
|
54
|
+
result = t.run_command("graph:open")
|
|
55
|
+
click.echo(json.dumps(result))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@core_cmd.command("new-tab")
|
|
59
|
+
def new_tab():
|
|
60
|
+
"""Open a new tab in Obsidian."""
|
|
61
|
+
cfg = load_config()
|
|
62
|
+
t = Transport(cfg)
|
|
63
|
+
result = t.run_command("workspace:new-tab")
|
|
64
|
+
click.echo(json.dumps(result))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@core_cmd.command("pin")
|
|
68
|
+
def pin_tab():
|
|
69
|
+
"""Pin/unpin the current tab."""
|
|
70
|
+
cfg = load_config()
|
|
71
|
+
t = Transport(cfg)
|
|
72
|
+
result = t.run_command("workspace:toggle-pin")
|
|
73
|
+
click.echo(json.dumps(result))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@core_cmd.command("split")
|
|
77
|
+
@click.argument("direction", type=click.Choice(["horizontal", "vertical"]))
|
|
78
|
+
def split_view(direction):
|
|
79
|
+
"""Split the current pane horizontally or vertically."""
|
|
80
|
+
cfg = load_config()
|
|
81
|
+
t = Transport(cfg)
|
|
82
|
+
result = t.run_command(f"workspace:split-{direction}")
|
|
83
|
+
click.echo(json.dumps(result))
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
import click
|
|
7
|
+
from ..config import load_config
|
|
8
|
+
from ..transport import Transport
|
|
9
|
+
from ..excalidraw_builder import ExcalidrawBuilder
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.group()
|
|
13
|
+
def excalidraw():
|
|
14
|
+
"""Excalidraw whiteboard diagrams (.excalidraw files)."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@excalidraw.command()
|
|
18
|
+
@click.argument("path")
|
|
19
|
+
def create(path):
|
|
20
|
+
"""Create a new empty Excalidraw drawing."""
|
|
21
|
+
cfg = load_config()
|
|
22
|
+
t = Transport(cfg)
|
|
23
|
+
eb = ExcalidrawBuilder()
|
|
24
|
+
result = t.write_file(path, eb.to_json())
|
|
25
|
+
if "ok" in result:
|
|
26
|
+
result["path"] = path
|
|
27
|
+
click.echo(json.dumps(result))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@excalidraw.command("add-shape")
|
|
31
|
+
@click.argument("path")
|
|
32
|
+
@click.option("--type", "shape_type",
|
|
33
|
+
type=click.Choice(["rectangle", "ellipse", "diamond", "text"]),
|
|
34
|
+
default="rectangle")
|
|
35
|
+
@click.option("--label", default="", help="Shape label or text content")
|
|
36
|
+
@click.option("--stroke-color", default="#1e1e2e")
|
|
37
|
+
@click.option("--background-color", default="transparent")
|
|
38
|
+
@click.option("--width", default=200, type=int)
|
|
39
|
+
@click.option("--height", default=100, type=int)
|
|
40
|
+
def add_shape(path, shape_type, label, stroke_color, background_color, width, height):
|
|
41
|
+
"""Add a shape to an existing Excalidraw drawing."""
|
|
42
|
+
cfg = load_config()
|
|
43
|
+
t = Transport(cfg)
|
|
44
|
+
raw = t.read_file(path)
|
|
45
|
+
if "error" in raw:
|
|
46
|
+
click.echo(json.dumps(raw))
|
|
47
|
+
return
|
|
48
|
+
data = json.loads(raw["content"])
|
|
49
|
+
eb = ExcalidrawBuilder()
|
|
50
|
+
eb._elements = data.get("elements", [])
|
|
51
|
+
if shape_type == "rectangle":
|
|
52
|
+
nid = eb.add_rectangle(label, stroke_color=stroke_color,
|
|
53
|
+
background_color=background_color, width=width, height=height)
|
|
54
|
+
elif shape_type == "ellipse":
|
|
55
|
+
nid = eb.add_ellipse(label, stroke_color=stroke_color,
|
|
56
|
+
background_color=background_color, width=width, height=height)
|
|
57
|
+
elif shape_type == "diamond":
|
|
58
|
+
nid = eb.add_diamond(label, stroke_color=stroke_color,
|
|
59
|
+
background_color=background_color, width=width, height=height)
|
|
60
|
+
else:
|
|
61
|
+
nid = eb.add_text(label)
|
|
62
|
+
data["elements"] = eb._elements
|
|
63
|
+
result = t.write_file(path, json.dumps(data, indent=2))
|
|
64
|
+
if "ok" in result:
|
|
65
|
+
result["element_id"] = nid
|
|
66
|
+
click.echo(json.dumps(result))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@excalidraw.command("add-arrow")
|
|
70
|
+
@click.argument("path")
|
|
71
|
+
@click.argument("from_id")
|
|
72
|
+
@click.argument("to_id")
|
|
73
|
+
@click.option("--label", default="")
|
|
74
|
+
@click.option("--stroke-color", default="#1e1e2e")
|
|
75
|
+
def add_arrow(path, from_id, to_id, label, stroke_color):
|
|
76
|
+
"""Add an arrow between two elements in an Excalidraw drawing."""
|
|
77
|
+
cfg = load_config()
|
|
78
|
+
t = Transport(cfg)
|
|
79
|
+
raw = t.read_file(path)
|
|
80
|
+
if "error" in raw:
|
|
81
|
+
click.echo(json.dumps(raw))
|
|
82
|
+
return
|
|
83
|
+
data = json.loads(raw["content"])
|
|
84
|
+
eb = ExcalidrawBuilder()
|
|
85
|
+
eb._elements = data.get("elements", [])
|
|
86
|
+
arrow_id = eb.add_arrow(from_id, to_id, label=label, stroke_color=stroke_color)
|
|
87
|
+
data["elements"] = eb._elements
|
|
88
|
+
result = t.write_file(path, json.dumps(data, indent=2))
|
|
89
|
+
if "ok" in result:
|
|
90
|
+
result["arrow_id"] = arrow_id
|
|
91
|
+
click.echo(json.dumps(result))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@excalidraw.command()
|
|
95
|
+
@click.argument("path")
|
|
96
|
+
def read(path):
|
|
97
|
+
"""Read an Excalidraw drawing and return its elements as JSON."""
|
|
98
|
+
cfg = load_config()
|
|
99
|
+
t = Transport(cfg)
|
|
100
|
+
raw = t.read_file(path)
|
|
101
|
+
if "error" in raw:
|
|
102
|
+
click.echo(json.dumps(raw))
|
|
103
|
+
return
|
|
104
|
+
data = json.loads(raw["content"])
|
|
105
|
+
click.echo(json.dumps({
|
|
106
|
+
"elements": data.get("elements", []),
|
|
107
|
+
"element_count": len(data.get("elements", [])),
|
|
108
|
+
"transport": raw["transport"],
|
|
109
|
+
}))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@excalidraw.command()
|
|
113
|
+
@click.argument("path")
|
|
114
|
+
@click.option("--spec", required=True, help="JSON spec: {elements: [...], arrows: [...]}")
|
|
115
|
+
def build(path, spec):
|
|
116
|
+
"""Build a complete Excalidraw drawing from a JSON spec in one shot."""
|
|
117
|
+
try:
|
|
118
|
+
spec_data = json.loads(spec)
|
|
119
|
+
except json.JSONDecodeError as e:
|
|
120
|
+
click.echo(json.dumps({"error": True, "message": f"Invalid JSON spec: {e}"}))
|
|
121
|
+
return
|
|
122
|
+
cfg = load_config()
|
|
123
|
+
t = Transport(cfg)
|
|
124
|
+
eb = ExcalidrawBuilder.from_spec(spec_data)
|
|
125
|
+
shape_count = sum(1 for e in eb._elements if e["type"] not in ("arrow", "line"))
|
|
126
|
+
arrow_count = sum(1 for e in eb._elements if e["type"] in ("arrow", "line"))
|
|
127
|
+
result = t.write_file(path, eb.to_json())
|
|
128
|
+
if "ok" in result:
|
|
129
|
+
result.update({"path": path, "elements": shape_count, "arrows": arrow_count})
|
|
130
|
+
click.echo(json.dumps(result))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@excalidraw.command("open")
|
|
134
|
+
@click.argument("path")
|
|
135
|
+
def open_drawing(path):
|
|
136
|
+
"""Open an Excalidraw drawing in Obsidian."""
|
|
137
|
+
cfg = load_config()
|
|
138
|
+
vault_name = Path(cfg.vault_path).name
|
|
139
|
+
uri = f"obsidian://open?vault={quote(vault_name)}&file={quote(path)}"
|
|
140
|
+
if sys.platform == "win32":
|
|
141
|
+
subprocess.Popen(["cmd", "/c", "start", uri], shell=False)
|
|
142
|
+
elif sys.platform == "darwin":
|
|
143
|
+
subprocess.Popen(["open", uri])
|
|
144
|
+
else:
|
|
145
|
+
subprocess.Popen(["xdg-open", uri])
|
|
146
|
+
click.echo(json.dumps({"ok": True, "uri": uri, "transport": "uri"}))
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import click
|
|
5
|
+
from ..config import load_config
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.group("export")
|
|
9
|
+
def export_cmd():
|
|
10
|
+
"""Export vault notes to portable formats."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@export_cmd.command("bundle")
|
|
14
|
+
@click.argument("output", default=None, required=False)
|
|
15
|
+
@click.option("--folder", default="AI Workspace", help="Vault folder to bundle")
|
|
16
|
+
@click.option("--separator", default="\n\n---\n\n", help="Separator between notes")
|
|
17
|
+
def export_bundle(output, folder, separator):
|
|
18
|
+
"""Concatenate notes into a single markdown file (useful for AI context)."""
|
|
19
|
+
cfg = load_config()
|
|
20
|
+
vault = Path(cfg.vault_path)
|
|
21
|
+
base = vault / folder if folder else vault
|
|
22
|
+
if not base.exists():
|
|
23
|
+
click.echo(json.dumps({"error": True, "message": f"Folder not found: {folder}"}))
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
if output is None:
|
|
27
|
+
ts = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
28
|
+
label = folder.replace("/", "-") or "vault"
|
|
29
|
+
output = str(vault.parent / f"obsidian-bundle-{label}-{ts}.md")
|
|
30
|
+
|
|
31
|
+
sections = []
|
|
32
|
+
for p in sorted(base.rglob("*.md")):
|
|
33
|
+
if ".obsidian" in p.parts:
|
|
34
|
+
continue
|
|
35
|
+
rel = str(p.relative_to(vault)).replace("\\", "/")
|
|
36
|
+
content = p.read_text(encoding="utf-8", errors="ignore")
|
|
37
|
+
sections.append(f"<!-- source: {rel} -->\n{content}")
|
|
38
|
+
|
|
39
|
+
Path(output).write_text(separator.join(sections), encoding="utf-8")
|
|
40
|
+
click.echo(json.dumps({"ok": True, "output": output, "files": len(sections)}))
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import click
|
|
3
|
+
from ..config import load_config
|
|
4
|
+
from ..transport import Transport
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@click.group("git")
|
|
8
|
+
def git_cmd():
|
|
9
|
+
"""Obsidian Git plugin commands (requires Obsidian Git plugin)."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@git_cmd.command()
|
|
13
|
+
@click.option("-m", "--message", default="", help="Commit message (enables commit-specified-message)")
|
|
14
|
+
def sync(message):
|
|
15
|
+
"""Commit all changes and push. Without -m uses auto message; with -m commits with specified message."""
|
|
16
|
+
cfg = load_config()
|
|
17
|
+
t = Transport(cfg)
|
|
18
|
+
if message:
|
|
19
|
+
result = t.run_command("obsidian-git:commit-push-specified-message")
|
|
20
|
+
else:
|
|
21
|
+
result = t.run_command("obsidian-git:push")
|
|
22
|
+
click.echo(json.dumps(result))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@git_cmd.command()
|
|
26
|
+
@click.option("-m", "--message", default="", help="Commit message")
|
|
27
|
+
def commit(message):
|
|
28
|
+
"""Commit all changes."""
|
|
29
|
+
cfg = load_config()
|
|
30
|
+
t = Transport(cfg)
|
|
31
|
+
if message:
|
|
32
|
+
result = t.run_command("obsidian-git:commit-specified-message")
|
|
33
|
+
else:
|
|
34
|
+
result = t.run_command("obsidian-git:commit")
|
|
35
|
+
click.echo(json.dumps(result))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@git_cmd.command()
|
|
39
|
+
def pull():
|
|
40
|
+
"""Pull latest changes from remote."""
|
|
41
|
+
cfg = load_config()
|
|
42
|
+
t = Transport(cfg)
|
|
43
|
+
result = t.run_command("obsidian-git:pull")
|
|
44
|
+
click.echo(json.dumps(result))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@git_cmd.command()
|
|
48
|
+
def push():
|
|
49
|
+
"""Push committed changes to remote."""
|
|
50
|
+
cfg = load_config()
|
|
51
|
+
t = Transport(cfg)
|
|
52
|
+
result = t.run_command("obsidian-git:push2")
|
|
53
|
+
click.echo(json.dumps(result))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@git_cmd.command()
|
|
57
|
+
def status():
|
|
58
|
+
"""List changed files in Obsidian Git source control view."""
|
|
59
|
+
cfg = load_config()
|
|
60
|
+
t = Transport(cfg)
|
|
61
|
+
result = t.run_command("obsidian-git:list-changed-files")
|
|
62
|
+
click.echo(json.dumps(result))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@git_cmd.command()
|
|
66
|
+
def pause():
|
|
67
|
+
"""Pause or resume Obsidian Git automatic routines."""
|
|
68
|
+
cfg = load_config()
|
|
69
|
+
t = Transport(cfg)
|
|
70
|
+
result = t.run_command("obsidian-git:pause-automatic-routines")
|
|
71
|
+
click.echo(json.dumps(result))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@git_cmd.command()
|
|
75
|
+
def fetch():
|
|
76
|
+
"""Fetch from remote without merging."""
|
|
77
|
+
cfg = load_config()
|
|
78
|
+
t = Transport(cfg)
|
|
79
|
+
result = t.run_command("obsidian-git:fetch")
|
|
80
|
+
click.echo(json.dumps(result))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@git_cmd.group()
|
|
84
|
+
def branch():
|
|
85
|
+
"""Branch operations."""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@branch.command("create")
|
|
89
|
+
def branch_create():
|
|
90
|
+
"""Create a new branch (opens Obsidian prompt)."""
|
|
91
|
+
cfg = load_config()
|
|
92
|
+
t = Transport(cfg)
|
|
93
|
+
result = t.run_command("obsidian-git:create-branch")
|
|
94
|
+
click.echo(json.dumps(result))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@branch.command("switch")
|
|
98
|
+
def branch_switch():
|
|
99
|
+
"""Switch to a branch (opens Obsidian branch picker)."""
|
|
100
|
+
cfg = load_config()
|
|
101
|
+
t = Transport(cfg)
|
|
102
|
+
result = t.run_command("obsidian-git:switch-branch")
|
|
103
|
+
click.echo(json.dumps(result))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@branch.command("delete")
|
|
107
|
+
def branch_delete():
|
|
108
|
+
"""Delete a branch (opens Obsidian branch picker)."""
|
|
109
|
+
cfg = load_config()
|
|
110
|
+
t = Transport(cfg)
|
|
111
|
+
result = t.run_command("obsidian-git:delete-branch")
|
|
112
|
+
click.echo(json.dumps(result))
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import click
|
|
3
|
+
from ..config import load_config
|
|
4
|
+
from ..transport import Transport
|
|
5
|
+
from ..kanban_builder import KanbanBuilder
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.group()
|
|
9
|
+
def kanban():
|
|
10
|
+
"""Obsidian Kanban board management (requires Kanban plugin)."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@kanban.command()
|
|
14
|
+
@click.argument("path")
|
|
15
|
+
@click.option("--lanes", default="To Do,In Progress,Done",
|
|
16
|
+
help="Comma-separated lane names")
|
|
17
|
+
def create(path, lanes):
|
|
18
|
+
"""Create a new Kanban board with the given lanes."""
|
|
19
|
+
cfg = load_config()
|
|
20
|
+
t = Transport(cfg)
|
|
21
|
+
kb = KanbanBuilder()
|
|
22
|
+
for lane in lanes.split(","):
|
|
23
|
+
kb.add_lane(lane.strip())
|
|
24
|
+
result = t.write_file(path, kb.to_markdown())
|
|
25
|
+
if "ok" in result:
|
|
26
|
+
result["path"] = path
|
|
27
|
+
result["lanes"] = [l.strip() for l in lanes.split(",")]
|
|
28
|
+
click.echo(json.dumps(result))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@kanban.command()
|
|
32
|
+
@click.argument("path")
|
|
33
|
+
def read(path):
|
|
34
|
+
"""Read a Kanban board and return its lanes and cards."""
|
|
35
|
+
cfg = load_config()
|
|
36
|
+
t = Transport(cfg)
|
|
37
|
+
raw = t.read_file(path)
|
|
38
|
+
if "error" in raw:
|
|
39
|
+
click.echo(json.dumps(raw))
|
|
40
|
+
return
|
|
41
|
+
kb = KanbanBuilder.from_markdown(raw["content"])
|
|
42
|
+
click.echo(json.dumps({
|
|
43
|
+
"lanes": {
|
|
44
|
+
lane: kb._lanes[lane]
|
|
45
|
+
for lane in kb._lane_order
|
|
46
|
+
},
|
|
47
|
+
"transport": raw["transport"],
|
|
48
|
+
}))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@kanban.group()
|
|
52
|
+
def card():
|
|
53
|
+
"""Card operations within a Kanban board."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@card.command("add")
|
|
57
|
+
@click.argument("path")
|
|
58
|
+
@click.argument("lane")
|
|
59
|
+
@click.argument("text")
|
|
60
|
+
@click.option("--done", is_flag=True, default=False, help="Mark card as done")
|
|
61
|
+
def card_add(path, lane, text, done):
|
|
62
|
+
"""Add a card to a lane in a Kanban board."""
|
|
63
|
+
cfg = load_config()
|
|
64
|
+
t = Transport(cfg)
|
|
65
|
+
raw = t.read_file(path)
|
|
66
|
+
if "error" in raw:
|
|
67
|
+
click.echo(json.dumps(raw))
|
|
68
|
+
return
|
|
69
|
+
kb = KanbanBuilder.from_markdown(raw["content"])
|
|
70
|
+
try:
|
|
71
|
+
kb.add_card(lane, text, done=done)
|
|
72
|
+
except ValueError as e:
|
|
73
|
+
click.echo(json.dumps({"error": True, "message": str(e)}))
|
|
74
|
+
return
|
|
75
|
+
result = t.write_file(path, kb.to_markdown())
|
|
76
|
+
if "ok" in result:
|
|
77
|
+
result["card"] = text
|
|
78
|
+
result["lane"] = lane
|
|
79
|
+
click.echo(json.dumps(result))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@kanban.group()
|
|
83
|
+
def lane():
|
|
84
|
+
"""Lane operations within a Kanban board."""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@lane.command("add")
|
|
88
|
+
@click.argument("path")
|
|
89
|
+
@click.argument("name")
|
|
90
|
+
def lane_add(path, name):
|
|
91
|
+
"""Add a new lane to an existing Kanban board."""
|
|
92
|
+
cfg = load_config()
|
|
93
|
+
t = Transport(cfg)
|
|
94
|
+
raw = t.read_file(path)
|
|
95
|
+
if "error" in raw:
|
|
96
|
+
click.echo(json.dumps(raw))
|
|
97
|
+
return
|
|
98
|
+
kb = KanbanBuilder.from_markdown(raw["content"])
|
|
99
|
+
kb.add_lane(name)
|
|
100
|
+
result = t.write_file(path, kb.to_markdown())
|
|
101
|
+
if "ok" in result:
|
|
102
|
+
result["lane"] = name
|
|
103
|
+
click.echo(json.dumps(result))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@kanban.command()
|
|
107
|
+
@click.argument("path")
|
|
108
|
+
def archive(path):
|
|
109
|
+
"""Archive completed cards (trigger Kanban plugin command)."""
|
|
110
|
+
cfg = load_config()
|
|
111
|
+
t = Transport(cfg)
|
|
112
|
+
result = t.run_command("obsidian-kanban:archive-completed-cards")
|
|
113
|
+
click.echo(json.dumps(result))
|