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.
Files changed (40) hide show
  1. obsidian_agent_cli-0.1.0.dist-info/METADATA +225 -0
  2. obsidian_agent_cli-0.1.0.dist-info/RECORD +40 -0
  3. obsidian_agent_cli-0.1.0.dist-info/WHEEL +5 -0
  4. obsidian_agent_cli-0.1.0.dist-info/entry_points.txt +2 -0
  5. obsidian_agent_cli-0.1.0.dist-info/top_level.txt +1 -0
  6. obsidian_cli/__init__.py +0 -0
  7. obsidian_cli/canvas_builder.py +127 -0
  8. obsidian_cli/commands/__init__.py +0 -0
  9. obsidian_cli/commands/active.py +74 -0
  10. obsidian_cli/commands/batch_cmd.py +91 -0
  11. obsidian_cli/commands/canvas.py +123 -0
  12. obsidian_cli/commands/config_cmd.py +57 -0
  13. obsidian_cli/commands/core_cmd.py +83 -0
  14. obsidian_cli/commands/excalidraw.py +146 -0
  15. obsidian_cli/commands/export_cmd.py +40 -0
  16. obsidian_cli/commands/git_cmd.py +112 -0
  17. obsidian_cli/commands/kanban.py +113 -0
  18. obsidian_cli/commands/lint_cmd.py +26 -0
  19. obsidian_cli/commands/meta_cmd.py +97 -0
  20. obsidian_cli/commands/mover_cmd.py +78 -0
  21. obsidian_cli/commands/note.py +178 -0
  22. obsidian_cli/commands/periodic.py +127 -0
  23. obsidian_cli/commands/quickadd_cmd.py +76 -0
  24. obsidian_cli/commands/refactor_cmd.py +81 -0
  25. obsidian_cli/commands/run.py +26 -0
  26. obsidian_cli/commands/search.py +44 -0
  27. obsidian_cli/commands/status_cmd.py +13 -0
  28. obsidian_cli/commands/tags_cmd.py +22 -0
  29. obsidian_cli/commands/tasks_cmd.py +125 -0
  30. obsidian_cli/commands/teach.py +43 -0
  31. obsidian_cli/commands/template_cmd.py +45 -0
  32. obsidian_cli/commands/uri.py +59 -0
  33. obsidian_cli/commands/vault_cmd.py +40 -0
  34. obsidian_cli/commands/workspace_cmd.py +67 -0
  35. obsidian_cli/config.py +40 -0
  36. obsidian_cli/excalidraw_builder.py +210 -0
  37. obsidian_cli/kanban_builder.py +60 -0
  38. obsidian_cli/main.py +60 -0
  39. obsidian_cli/registry.py +98 -0
  40. 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))