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.
- {odcli-0.1.0 → odcli-0.1.2}/PKG-INFO +28 -10
- {odcli-0.1.0 → odcli-0.1.2}/README.md +27 -9
- {odcli-0.1.0 → odcli-0.1.2}/pyproject.toml +2 -1
- {odcli-0.1.0 → odcli-0.1.2}/src/obsidian_cli/cli.py +81 -3
- {odcli-0.1.0 → odcli-0.1.2}/src/odcli.egg-info/PKG-INFO +28 -10
- {odcli-0.1.0 → odcli-0.1.2}/src/odcli.egg-info/entry_points.txt +1 -0
- {odcli-0.1.0 → odcli-0.1.2}/tests/test_cli.py +23 -0
- {odcli-0.1.0 → odcli-0.1.2}/setup.cfg +0 -0
- {odcli-0.1.0 → odcli-0.1.2}/src/obsidian_cli/__init__.py +0 -0
- {odcli-0.1.0 → odcli-0.1.2}/src/obsidian_cli/__main__.py +0 -0
- {odcli-0.1.0 → odcli-0.1.2}/src/obsidian_cli/vault.py +0 -0
- {odcli-0.1.0 → odcli-0.1.2}/src/odcli.egg-info/SOURCES.txt +0 -0
- {odcli-0.1.0 → odcli-0.1.2}/src/odcli.egg-info/dependency_links.txt +0 -0
- {odcli-0.1.0 → odcli-0.1.2}/src/odcli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: odcli
|
|
3
|
-
Version: 0.1.
|
|
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
|
|
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
|
-
./
|
|
94
|
-
./
|
|
95
|
-
./
|
|
96
|
-
./
|
|
97
|
-
./
|
|
98
|
-
./
|
|
99
|
-
./
|
|
100
|
-
./
|
|
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
|
|
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
|
-
./
|
|
74
|
-
./
|
|
75
|
-
./
|
|
76
|
-
./
|
|
77
|
-
./
|
|
78
|
-
./
|
|
79
|
-
./
|
|
80
|
-
./
|
|
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.
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
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.
|
|
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
|
|
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
|
-
./
|
|
94
|
-
./
|
|
95
|
-
./
|
|
96
|
-
./
|
|
97
|
-
./
|
|
98
|
-
./
|
|
99
|
-
./
|
|
100
|
-
./
|
|
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
|
## 命令
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|