odcli 0.1.0__tar.gz → 0.1.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: odcli
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: A small CLI for reading and writing notes in a local Obsidian vault.
5
5
  Author: Chang LeHung
6
6
  Project-URL: Homepage, https://github.com/Chang-LeHung/obsidian-cli
@@ -38,7 +38,7 @@ Description-Content-Type: text/markdown
38
38
  ```bash
39
39
  cd /Users/huchang/agents/obsidian_cli
40
40
  uv sync
41
- uv run obsidian-cli --help
41
+ uv run odcli --help
42
42
  ```
43
43
 
44
44
  运行测试:
@@ -56,9 +56,17 @@ uv build
56
56
  ```
57
57
 
58
58
  发布到 PyPI 后,安装包名会是 `odcli`。
59
+ 安装后可执行命令同时支持 `odcli` 和 `obsidian-cli`。
59
60
 
60
61
  ## 直接运行
61
62
 
63
+ ```bash
64
+ cd /Users/huchang/agents/obsidian_cli
65
+ ./odcli --help
66
+ ```
67
+
68
+ 兼容入口仍然保留:
69
+
62
70
  ```bash
63
71
  cd /Users/huchang/agents/obsidian_cli
64
72
  ./obsidian-cli --help
@@ -85,19 +93,29 @@ uv sync
85
93
 
86
94
  1. `--vault /path/to/vault`
87
95
  2. 环境变量 `OBSIDIAN_VAULT`
96
+ 3. Obsidian 本地配置里最近打开的 vault
97
+ 4. 常见默认目录
98
+
99
+ 当前内置的默认目录包括:
100
+
101
+ - macOS: `~/Documents/Obsidian Vault`
102
+ - macOS: `~/Documents/Obsidian`
103
+ - macOS iCloud: `~/Library/Mobile Documents/iCloud~md~obsidian/Documents`
104
+ - Windows: `%USERPROFILE%\\Documents\\Obsidian Vault`
105
+ - Windows: `%USERPROFILE%\\Documents\\Obsidian`
88
106
 
89
107
  示例:
90
108
 
91
109
  ```bash
92
110
  export OBSIDIAN_VAULT="/Users/your-name/Documents/MyVault"
93
- ./obsidian-cli check
94
- ./obsidian-cli list
95
- ./obsidian-cli read Inbox/today.md
96
- ./obsidian-cli read-lines Inbox/today.md 3 8
97
- ./obsidian-cli write Inbox/today.md --content "# Today"
98
- ./obsidian-cli write-lines Inbox/today.md 3 4 --content "- replaced\n- lines\n"
99
- ./obsidian-cli append Inbox/today.md --content "\n- new item"
100
- ./obsidian-cli search "project alpha"
111
+ ./odcli check
112
+ ./odcli list
113
+ ./odcli read Inbox/today.md
114
+ ./odcli read-lines Inbox/today.md 3 8
115
+ ./odcli write Inbox/today.md --content "# Today"
116
+ ./odcli write-lines Inbox/today.md 3 4 --content "- replaced\n- lines\n"
117
+ ./odcli append Inbox/today.md --content "\n- new item"
118
+ ./odcli search "project alpha"
101
119
  ```
102
120
 
103
121
  ## 命令
@@ -18,7 +18,7 @@
18
18
  ```bash
19
19
  cd /Users/huchang/agents/obsidian_cli
20
20
  uv sync
21
- uv run obsidian-cli --help
21
+ uv run odcli --help
22
22
  ```
23
23
 
24
24
  运行测试:
@@ -36,9 +36,17 @@ uv build
36
36
  ```
37
37
 
38
38
  发布到 PyPI 后,安装包名会是 `odcli`。
39
+ 安装后可执行命令同时支持 `odcli` 和 `obsidian-cli`。
39
40
 
40
41
  ## 直接运行
41
42
 
43
+ ```bash
44
+ cd /Users/huchang/agents/obsidian_cli
45
+ ./odcli --help
46
+ ```
47
+
48
+ 兼容入口仍然保留:
49
+
42
50
  ```bash
43
51
  cd /Users/huchang/agents/obsidian_cli
44
52
  ./obsidian-cli --help
@@ -65,19 +73,29 @@ uv sync
65
73
 
66
74
  1. `--vault /path/to/vault`
67
75
  2. 环境变量 `OBSIDIAN_VAULT`
76
+ 3. Obsidian 本地配置里最近打开的 vault
77
+ 4. 常见默认目录
78
+
79
+ 当前内置的默认目录包括:
80
+
81
+ - macOS: `~/Documents/Obsidian Vault`
82
+ - macOS: `~/Documents/Obsidian`
83
+ - macOS iCloud: `~/Library/Mobile Documents/iCloud~md~obsidian/Documents`
84
+ - Windows: `%USERPROFILE%\\Documents\\Obsidian Vault`
85
+ - Windows: `%USERPROFILE%\\Documents\\Obsidian`
68
86
 
69
87
  示例:
70
88
 
71
89
  ```bash
72
90
  export OBSIDIAN_VAULT="/Users/your-name/Documents/MyVault"
73
- ./obsidian-cli check
74
- ./obsidian-cli list
75
- ./obsidian-cli read Inbox/today.md
76
- ./obsidian-cli read-lines Inbox/today.md 3 8
77
- ./obsidian-cli write Inbox/today.md --content "# Today"
78
- ./obsidian-cli write-lines Inbox/today.md 3 4 --content "- replaced\n- lines\n"
79
- ./obsidian-cli append Inbox/today.md --content "\n- new item"
80
- ./obsidian-cli search "project alpha"
91
+ ./odcli check
92
+ ./odcli list
93
+ ./odcli read Inbox/today.md
94
+ ./odcli read-lines Inbox/today.md 3 8
95
+ ./odcli write Inbox/today.md --content "# Today"
96
+ ./odcli write-lines Inbox/today.md 3 4 --content "- replaced\n- lines\n"
97
+ ./odcli append Inbox/today.md --content "\n- new item"
98
+ ./odcli search "project alpha"
81
99
  ```
82
100
 
83
101
  ## 命令
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "odcli"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "A small CLI for reading and writing notes in a local Obsidian vault."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -30,6 +30,7 @@ Repository = "https://github.com/Chang-LeHung/obsidian-cli"
30
30
  Issues = "https://github.com/Chang-LeHung/obsidian-cli/issues"
31
31
 
32
32
  [project.scripts]
33
+ odcli = "obsidian_cli.cli:main"
33
34
  obsidian-cli = "obsidian_cli.cli:main"
34
35
 
35
36
  [dependency-groups]
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
+ import json
4
5
  import os
5
6
  import sys
6
7
  from pathlib import Path
@@ -71,9 +72,86 @@ def build_parser() -> argparse.ArgumentParser:
71
72
 
72
73
  def resolve_vault_path(cli_value: str | None) -> Path:
73
74
  raw_value = cli_value or os.environ.get("OBSIDIAN_VAULT")
74
- if not raw_value:
75
- raise VaultError("vault path is required; use --vault or OBSIDIAN_VAULT")
76
- return Path(raw_value)
75
+ if raw_value:
76
+ return Path(raw_value)
77
+
78
+ discovered = discover_default_vault_path()
79
+ if discovered is not None:
80
+ return discovered
81
+
82
+ raise VaultError(
83
+ "vault path is required; use --vault, OBSIDIAN_VAULT, or place your vault in a default Obsidian location"
84
+ )
85
+
86
+
87
+ def discover_default_vault_path() -> Path | None:
88
+ vault_from_config = discover_vault_from_obsidian_config()
89
+ if vault_from_config is not None:
90
+ return vault_from_config
91
+
92
+ for candidate in iter_default_vault_candidates():
93
+ if (candidate / ".obsidian").is_dir():
94
+ return candidate
95
+ return None
96
+
97
+
98
+ def discover_vault_from_obsidian_config() -> Path | None:
99
+ for config_path in iter_obsidian_config_paths():
100
+ if not config_path.is_file():
101
+ continue
102
+ try:
103
+ payload = json.loads(config_path.read_text(encoding="utf-8"))
104
+ except (OSError, json.JSONDecodeError):
105
+ continue
106
+
107
+ vaults = payload.get("vaults")
108
+ if not isinstance(vaults, dict):
109
+ continue
110
+
111
+ candidates: list[tuple[int, str]] = []
112
+ for item in vaults.values():
113
+ if not isinstance(item, dict):
114
+ continue
115
+ path_value = item.get("path")
116
+ if not isinstance(path_value, str):
117
+ continue
118
+ ts_value = item.get("ts")
119
+ timestamp = ts_value if isinstance(ts_value, int) else -1
120
+ candidates.append((timestamp, path_value))
121
+
122
+ for _, path_value in sorted(candidates, reverse=True):
123
+ candidate = Path(path_value).expanduser()
124
+ if (candidate / ".obsidian").is_dir():
125
+ return candidate
126
+
127
+ return None
128
+
129
+
130
+ def iter_obsidian_config_paths() -> list[Path]:
131
+ home = Path.home()
132
+ appdata = os.environ.get("APPDATA")
133
+ candidates = [
134
+ home / "Library" / "Application Support" / "obsidian" / "obsidian.json",
135
+ home / ".config" / "obsidian" / "obsidian.json",
136
+ ]
137
+ if appdata:
138
+ candidates.append(Path(appdata) / "obsidian" / "obsidian.json")
139
+ else:
140
+ candidates.append(
141
+ home / "AppData" / "Roaming" / "obsidian" / "obsidian.json"
142
+ )
143
+ return candidates
144
+
145
+
146
+ def iter_default_vault_candidates() -> list[Path]:
147
+ home = Path.home()
148
+ return [
149
+ home / "Documents" / "Obsidian Vault",
150
+ home / "Documents" / "Obsidian",
151
+ home / "Library" / "Mobile Documents" / "iCloud~md~obsidian" / "Documents",
152
+ home / "AppData" / "Roaming" / "Obsidian",
153
+ home / "AppData" / "Roaming" / "obsidian" / "vaults" / "default",
154
+ ]
77
155
 
78
156
 
79
157
  def read_content_arg(content: str | None, use_stdin: bool) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: odcli
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: A small CLI for reading and writing notes in a local Obsidian vault.
5
5
  Author: Chang LeHung
6
6
  Project-URL: Homepage, https://github.com/Chang-LeHung/obsidian-cli
@@ -38,7 +38,7 @@ Description-Content-Type: text/markdown
38
38
  ```bash
39
39
  cd /Users/huchang/agents/obsidian_cli
40
40
  uv sync
41
- uv run obsidian-cli --help
41
+ uv run odcli --help
42
42
  ```
43
43
 
44
44
  运行测试:
@@ -56,9 +56,17 @@ uv build
56
56
  ```
57
57
 
58
58
  发布到 PyPI 后,安装包名会是 `odcli`。
59
+ 安装后可执行命令同时支持 `odcli` 和 `obsidian-cli`。
59
60
 
60
61
  ## 直接运行
61
62
 
63
+ ```bash
64
+ cd /Users/huchang/agents/obsidian_cli
65
+ ./odcli --help
66
+ ```
67
+
68
+ 兼容入口仍然保留:
69
+
62
70
  ```bash
63
71
  cd /Users/huchang/agents/obsidian_cli
64
72
  ./obsidian-cli --help
@@ -85,19 +93,29 @@ uv sync
85
93
 
86
94
  1. `--vault /path/to/vault`
87
95
  2. 环境变量 `OBSIDIAN_VAULT`
96
+ 3. Obsidian 本地配置里最近打开的 vault
97
+ 4. 常见默认目录
98
+
99
+ 当前内置的默认目录包括:
100
+
101
+ - macOS: `~/Documents/Obsidian Vault`
102
+ - macOS: `~/Documents/Obsidian`
103
+ - macOS iCloud: `~/Library/Mobile Documents/iCloud~md~obsidian/Documents`
104
+ - Windows: `%USERPROFILE%\\Documents\\Obsidian Vault`
105
+ - Windows: `%USERPROFILE%\\Documents\\Obsidian`
88
106
 
89
107
  示例:
90
108
 
91
109
  ```bash
92
110
  export OBSIDIAN_VAULT="/Users/your-name/Documents/MyVault"
93
- ./obsidian-cli check
94
- ./obsidian-cli list
95
- ./obsidian-cli read Inbox/today.md
96
- ./obsidian-cli read-lines Inbox/today.md 3 8
97
- ./obsidian-cli write Inbox/today.md --content "# Today"
98
- ./obsidian-cli write-lines Inbox/today.md 3 4 --content "- replaced\n- lines\n"
99
- ./obsidian-cli append Inbox/today.md --content "\n- new item"
100
- ./obsidian-cli search "project alpha"
111
+ ./odcli check
112
+ ./odcli list
113
+ ./odcli read Inbox/today.md
114
+ ./odcli read-lines Inbox/today.md 3 8
115
+ ./odcli write Inbox/today.md --content "# Today"
116
+ ./odcli write-lines Inbox/today.md 3 4 --content "- replaced\n- lines\n"
117
+ ./odcli append Inbox/today.md --content "\n- new item"
118
+ ./odcli search "project alpha"
101
119
  ```
102
120
 
103
121
  ## 命令
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  obsidian-cli = obsidian_cli.cli:main
3
+ odcli = obsidian_cli.cli:main
@@ -6,9 +6,11 @@ from pathlib import Path
6
6
  import sys
7
7
  from contextlib import redirect_stderr, redirect_stdout
8
8
  from io import StringIO
9
+ from unittest.mock import patch
9
10
 
10
11
  sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
11
12
 
13
+ from obsidian_cli import cli
12
14
  from obsidian_cli.cli import main
13
15
  from obsidian_cli.vault import ObsidianVault, VaultError
14
16
 
@@ -93,6 +95,27 @@ class VaultTests(unittest.TestCase):
93
95
  self.assertEqual(exit_code, 0)
94
96
  self.assertEqual(self.vault.read_note("Note.md"), "a\nbeta\nc\n")
95
97
 
98
+ def test_resolve_vault_path_prefers_env_var(self) -> None:
99
+ with patch.dict("os.environ", {"OBSIDIAN_VAULT": str(self.vault_root)}, clear=False):
100
+ resolved = cli.resolve_vault_path(None)
101
+ self.assertEqual(resolved, self.vault_root)
102
+
103
+ def test_resolve_vault_path_discovers_default_vault(self) -> None:
104
+ with patch.dict("os.environ", {}, clear=True):
105
+ with patch.object(cli, "discover_vault_from_obsidian_config", return_value=None):
106
+ with patch.object(cli, "iter_default_vault_candidates", return_value=[self.vault_root]):
107
+ resolved = cli.resolve_vault_path(None)
108
+ self.assertEqual(resolved, self.vault_root)
109
+
110
+ def test_resolve_vault_path_uses_obsidian_config_first(self) -> None:
111
+ config_vault = self.vault_root / "Configured"
112
+ config_vault.mkdir()
113
+ (config_vault / ".obsidian").mkdir()
114
+ with patch.dict("os.environ", {}, clear=True):
115
+ with patch.object(cli, "discover_vault_from_obsidian_config", return_value=config_vault):
116
+ resolved = cli.resolve_vault_path(None)
117
+ self.assertEqual(resolved, config_vault)
118
+
96
119
 
97
120
  if __name__ == "__main__":
98
121
  unittest.main()
File without changes
File without changes