odcli 0.1.2__tar.gz → 0.1.4__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.2
3
+ Version: 0.1.4
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
@@ -10,6 +10,7 @@ Keywords: obsidian,cli,markdown,notes,vault
10
10
  Classifier: Development Status :: 3 - Alpha
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Operating System :: MacOS
13
+ Classifier: Operating System :: Microsoft :: Windows
13
14
  Classifier: Programming Language :: Python :: 3
14
15
  Classifier: Programming Language :: Python :: 3.11
15
16
  Classifier: Programming Language :: Python :: 3.12
@@ -20,20 +21,23 @@ Description-Content-Type: text/markdown
20
21
 
21
22
  # odcli
22
23
 
23
- 一个基于 Python 的本地 CLI,用来对 Obsidian vault 做读写操作。它直接操作 vault 里的 Markdown 文件,不依赖 Obsidian 的私有接口,因此稳定、可移植,也方便后续扩展。
24
+ `odcli` is a local Python CLI for reading and writing notes in an Obsidian vault.
25
+ It works directly on Markdown files inside the vault, so it does not depend on private Obsidian APIs and remains portable and easy to extend.
24
26
 
25
- ## 功能
27
+ ## Features
26
28
 
27
- - 校验 vault 路径是否有效
28
- - 列出 vault 中的 Markdown 笔记
29
- - 读取指定笔记
30
- - 按指定行区间读取笔记
31
- - 覆盖写入或自动创建笔记
32
- - 按指定行区间覆盖写入
33
- - 追加内容到笔记末尾
34
- - vault 中全文搜索
29
+ - Validate whether a vault path is available
30
+ - List Markdown notes in the vault
31
+ - Read a specific note
32
+ - Read a specific line range from a note
33
+ - Overwrite a note or create it automatically
34
+ - Replace a specific line range in a note
35
+ - Append content to a note
36
+ - Full-text search across the vault
37
+ - Auto-discover the default vault from Obsidian config or common macOS and Windows locations
38
+ - Install odcli helper skills into Codex or Claude Code skill directories
35
39
 
36
- ## 使用 uv
40
+ ## Using uv
37
41
 
38
42
  ```bash
39
43
  cd /Users/huchang/agents/obsidian_cli
@@ -41,62 +45,53 @@ uv sync
41
45
  uv run odcli --help
42
46
  ```
43
47
 
44
- 运行测试:
48
+ Run tests:
45
49
 
46
50
  ```bash
47
51
  cd /Users/huchang/agents/obsidian_cli
48
52
  uv run python -m unittest discover -s tests
49
53
  ```
50
54
 
51
- 构建分发包:
55
+ Build distributions:
52
56
 
53
57
  ```bash
54
58
  cd /Users/huchang/agents/obsidian_cli
55
59
  uv build
56
60
  ```
57
61
 
58
- 发布到 PyPI 后,安装包名会是 `odcli`。
59
- 安装后可执行命令同时支持 `odcli` `obsidian-cli`。
62
+ The published package name on PyPI is `odcli`.
63
+ After installation, both `odcli` and `obsidian-cli` are available as command names.
60
64
 
61
- ## 直接运行
65
+ ## Run Locally
62
66
 
63
67
  ```bash
64
68
  cd /Users/huchang/agents/obsidian_cli
65
69
  ./odcli --help
66
70
  ```
67
71
 
68
- 兼容入口仍然保留:
72
+ The compatibility entry point is still available:
69
73
 
70
74
  ```bash
71
75
  cd /Users/huchang/agents/obsidian_cli
72
76
  ./obsidian-cli --help
73
77
  ```
74
78
 
75
- 如果你更喜欢显式调用 Python:
79
+ If you prefer module execution:
76
80
 
77
81
  ```bash
78
82
  PYTHONPATH=src python3 -m obsidian_cli --help
79
83
  ```
80
84
 
81
- ## 可选安装
85
+ ## Vault Resolution
82
86
 
83
- 如果你想安装到虚拟环境中:
84
-
85
- ```bash
86
- cd /Users/huchang/agents/obsidian_cli
87
- uv sync
88
- ```
89
-
90
- ## 指定 vault
91
-
92
- 优先级如下:
87
+ Resolution priority:
93
88
 
94
89
  1. `--vault /path/to/vault`
95
- 2. 环境变量 `OBSIDIAN_VAULT`
96
- 3. Obsidian 本地配置里最近打开的 vault
97
- 4. 常见默认目录
90
+ 2. `OBSIDIAN_VAULT`
91
+ 3. The most recently opened vault recorded by local Obsidian config
92
+ 4. Common default directories
98
93
 
99
- 当前内置的默认目录包括:
94
+ Built-in default locations:
100
95
 
101
96
  - macOS: `~/Documents/Obsidian Vault`
102
97
  - macOS: `~/Documents/Obsidian`
@@ -104,7 +99,7 @@ uv sync
104
99
  - Windows: `%USERPROFILE%\\Documents\\Obsidian Vault`
105
100
  - Windows: `%USERPROFILE%\\Documents\\Obsidian`
106
101
 
107
- 示例:
102
+ Example:
108
103
 
109
104
  ```bash
110
105
  export OBSIDIAN_VAULT="/Users/your-name/Documents/MyVault"
@@ -118,47 +113,47 @@ export OBSIDIAN_VAULT="/Users/your-name/Documents/MyVault"
118
113
  ./odcli search "project alpha"
119
114
  ```
120
115
 
121
- ## 命令
116
+ ## Commands
122
117
 
123
118
  ### `check`
124
119
 
125
- 检查 vault 是否存在,以及是否包含 `.obsidian` 目录。
120
+ Validate that the vault exists and report whether `.obsidian` is present.
126
121
 
127
122
  ### `list`
128
123
 
129
- 列出 vault 中的 Markdown 文件。
124
+ List Markdown notes in the vault.
130
125
 
131
- 可选参数:
126
+ Optional arguments:
132
127
 
133
128
  - `--limit N`
134
129
 
135
130
  ### `read`
136
131
 
137
- 读取笔记内容。
132
+ Read a note.
138
133
 
139
- 参数:
134
+ Arguments:
140
135
 
141
- - `note_path`:相对于 vault 根目录的路径
136
+ - `note_path`: path relative to the vault root
142
137
 
143
138
  ### `write`
144
139
 
145
- 覆盖写入笔记;若父目录不存在会自动创建。
140
+ Overwrite a note. Parent directories are created automatically if needed.
146
141
 
147
- 参数:
142
+ Arguments:
148
143
 
149
144
  - `note_path`
150
145
  - `--content TEXT`
151
- - `--stdin`:从标准输入读取内容
146
+ - `--stdin`
152
147
 
153
- 可选参数:
148
+ Optional arguments:
154
149
 
155
- - `--create-only`:若文件已存在则报错
150
+ - `--create-only`
156
151
 
157
152
  ### `read-lines`
158
153
 
159
- 读取指定行区间,行号从 `1` 开始,区间是闭区间。
154
+ Read a line range. Line numbers are 1-based and inclusive.
160
155
 
161
- 参数:
156
+ Arguments:
162
157
 
163
158
  - `note_path`
164
159
  - `start_line`
@@ -166,9 +161,9 @@ export OBSIDIAN_VAULT="/Users/your-name/Documents/MyVault"
166
161
 
167
162
  ### `write-lines`
168
163
 
169
- 用新内容替换指定行区间,行号从 `1` 开始,区间是闭区间。
164
+ Replace a line range. Line numbers are 1-based and inclusive.
170
165
 
171
- 参数:
166
+ Arguments:
172
167
 
173
168
  - `note_path`
174
169
  - `start_line`
@@ -178,9 +173,9 @@ export OBSIDIAN_VAULT="/Users/your-name/Documents/MyVault"
178
173
 
179
174
  ### `append`
180
175
 
181
- 追加内容到笔记末尾。
176
+ Append content to the end of a note.
182
177
 
183
- 参数:
178
+ Arguments:
184
179
 
185
180
  - `note_path`
186
181
  - `--content TEXT`
@@ -188,14 +183,32 @@ export OBSIDIAN_VAULT="/Users/your-name/Documents/MyVault"
188
183
 
189
184
  ### `search`
190
185
 
191
- 在所有 Markdown 笔记中搜索文本。
186
+ Search across all Markdown notes in the vault.
192
187
 
193
- 参数:
188
+ Arguments:
194
189
 
195
190
  - `query`
196
191
  - `--case-sensitive`
197
192
 
198
- ## 测试
193
+ ### `plugin install`
194
+
195
+ Install odcli helper skills for local coding tools.
196
+
197
+ Targets:
198
+
199
+ - `codex-skill`: installs to `~/.codex/skills/odcli/SKILL.md`
200
+ - `claude-skill`: installs to `~/.claude/skills/odcli/SKILL.md`
201
+ - `all-skills`: installs both
202
+
203
+ Examples:
204
+
205
+ ```bash
206
+ odcli plugin install codex-skill
207
+ odcli plugin install claude-skill
208
+ odcli plugin install all-skills
209
+ ```
210
+
211
+ ## Testing
199
212
 
200
213
  ```bash
201
214
  cd /Users/huchang/agents/obsidian_cli
odcli-0.1.4/README.md ADDED
@@ -0,0 +1,195 @@
1
+ # odcli
2
+
3
+ `odcli` is a local Python CLI for reading and writing notes in an Obsidian vault.
4
+ It works directly on Markdown files inside the vault, so it does not depend on private Obsidian APIs and remains portable and easy to extend.
5
+
6
+ ## Features
7
+
8
+ - Validate whether a vault path is available
9
+ - List Markdown notes in the vault
10
+ - Read a specific note
11
+ - Read a specific line range from a note
12
+ - Overwrite a note or create it automatically
13
+ - Replace a specific line range in a note
14
+ - Append content to a note
15
+ - Full-text search across the vault
16
+ - Auto-discover the default vault from Obsidian config or common macOS and Windows locations
17
+ - Install odcli helper skills into Codex or Claude Code skill directories
18
+
19
+ ## Using uv
20
+
21
+ ```bash
22
+ cd /Users/huchang/agents/obsidian_cli
23
+ uv sync
24
+ uv run odcli --help
25
+ ```
26
+
27
+ Run tests:
28
+
29
+ ```bash
30
+ cd /Users/huchang/agents/obsidian_cli
31
+ uv run python -m unittest discover -s tests
32
+ ```
33
+
34
+ Build distributions:
35
+
36
+ ```bash
37
+ cd /Users/huchang/agents/obsidian_cli
38
+ uv build
39
+ ```
40
+
41
+ The published package name on PyPI is `odcli`.
42
+ After installation, both `odcli` and `obsidian-cli` are available as command names.
43
+
44
+ ## Run Locally
45
+
46
+ ```bash
47
+ cd /Users/huchang/agents/obsidian_cli
48
+ ./odcli --help
49
+ ```
50
+
51
+ The compatibility entry point is still available:
52
+
53
+ ```bash
54
+ cd /Users/huchang/agents/obsidian_cli
55
+ ./obsidian-cli --help
56
+ ```
57
+
58
+ If you prefer module execution:
59
+
60
+ ```bash
61
+ PYTHONPATH=src python3 -m obsidian_cli --help
62
+ ```
63
+
64
+ ## Vault Resolution
65
+
66
+ Resolution priority:
67
+
68
+ 1. `--vault /path/to/vault`
69
+ 2. `OBSIDIAN_VAULT`
70
+ 3. The most recently opened vault recorded by local Obsidian config
71
+ 4. Common default directories
72
+
73
+ Built-in default locations:
74
+
75
+ - macOS: `~/Documents/Obsidian Vault`
76
+ - macOS: `~/Documents/Obsidian`
77
+ - macOS iCloud: `~/Library/Mobile Documents/iCloud~md~obsidian/Documents`
78
+ - Windows: `%USERPROFILE%\\Documents\\Obsidian Vault`
79
+ - Windows: `%USERPROFILE%\\Documents\\Obsidian`
80
+
81
+ Example:
82
+
83
+ ```bash
84
+ export OBSIDIAN_VAULT="/Users/your-name/Documents/MyVault"
85
+ ./odcli check
86
+ ./odcli list
87
+ ./odcli read Inbox/today.md
88
+ ./odcli read-lines Inbox/today.md 3 8
89
+ ./odcli write Inbox/today.md --content "# Today"
90
+ ./odcli write-lines Inbox/today.md 3 4 --content "- replaced\n- lines\n"
91
+ ./odcli append Inbox/today.md --content "\n- new item"
92
+ ./odcli search "project alpha"
93
+ ```
94
+
95
+ ## Commands
96
+
97
+ ### `check`
98
+
99
+ Validate that the vault exists and report whether `.obsidian` is present.
100
+
101
+ ### `list`
102
+
103
+ List Markdown notes in the vault.
104
+
105
+ Optional arguments:
106
+
107
+ - `--limit N`
108
+
109
+ ### `read`
110
+
111
+ Read a note.
112
+
113
+ Arguments:
114
+
115
+ - `note_path`: path relative to the vault root
116
+
117
+ ### `write`
118
+
119
+ Overwrite a note. Parent directories are created automatically if needed.
120
+
121
+ Arguments:
122
+
123
+ - `note_path`
124
+ - `--content TEXT`
125
+ - `--stdin`
126
+
127
+ Optional arguments:
128
+
129
+ - `--create-only`
130
+
131
+ ### `read-lines`
132
+
133
+ Read a line range. Line numbers are 1-based and inclusive.
134
+
135
+ Arguments:
136
+
137
+ - `note_path`
138
+ - `start_line`
139
+ - `end_line`
140
+
141
+ ### `write-lines`
142
+
143
+ Replace a line range. Line numbers are 1-based and inclusive.
144
+
145
+ Arguments:
146
+
147
+ - `note_path`
148
+ - `start_line`
149
+ - `end_line`
150
+ - `--content TEXT`
151
+ - `--stdin`
152
+
153
+ ### `append`
154
+
155
+ Append content to the end of a note.
156
+
157
+ Arguments:
158
+
159
+ - `note_path`
160
+ - `--content TEXT`
161
+ - `--stdin`
162
+
163
+ ### `search`
164
+
165
+ Search across all Markdown notes in the vault.
166
+
167
+ Arguments:
168
+
169
+ - `query`
170
+ - `--case-sensitive`
171
+
172
+ ### `plugin install`
173
+
174
+ Install odcli helper skills for local coding tools.
175
+
176
+ Targets:
177
+
178
+ - `codex-skill`: installs to `~/.codex/skills/odcli/SKILL.md`
179
+ - `claude-skill`: installs to `~/.claude/skills/odcli/SKILL.md`
180
+ - `all-skills`: installs both
181
+
182
+ Examples:
183
+
184
+ ```bash
185
+ odcli plugin install codex-skill
186
+ odcli plugin install claude-skill
187
+ odcli plugin install all-skills
188
+ ```
189
+
190
+ ## Testing
191
+
192
+ ```bash
193
+ cd /Users/huchang/agents/obsidian_cli
194
+ uv run python -m unittest discover -s tests
195
+ ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "odcli"
7
- version = "0.1.2"
7
+ version = "0.1.4"
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"
@@ -17,6 +17,7 @@ classifiers = [
17
17
  "Development Status :: 3 - Alpha",
18
18
  "Intended Audience :: Developers",
19
19
  "Operating System :: MacOS",
20
+ "Operating System :: Microsoft :: Windows",
20
21
  "Programming Language :: Python :: 3",
21
22
  "Programming Language :: Python :: 3.11",
22
23
  "Programming Language :: Python :: 3.12",
@@ -36,6 +37,7 @@ obsidian-cli = "obsidian_cli.cli:main"
36
37
  [dependency-groups]
37
38
  dev = [
38
39
  "build>=1.2.2",
40
+ "ruff>=0.14.0",
39
41
  "twine>=6.1.0",
40
42
  ]
41
43
 
@@ -1,4 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.1.0"
4
-
3
+ __version__ = "0.1.4"
@@ -0,0 +1,190 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from obsidian_cli.commands import CommandRunner
7
+ from obsidian_cli.discovery import VaultLocator
8
+ from obsidian_cli.plugins import SkillInstaller
9
+ from obsidian_cli.vault import ObsidianVault, VaultError
10
+
11
+
12
+ class ObsidianCLI:
13
+ def __init__(self, vault_locator: VaultLocator | None = None) -> None:
14
+ self._vault_locator = vault_locator or VaultLocator()
15
+ self._skill_installer = SkillInstaller()
16
+ self._parser = self._build_parser()
17
+
18
+ def run(self, argv: list[str] | None = None) -> int:
19
+ args = self._parser.parse_args(argv)
20
+
21
+ try:
22
+ if args.command == "plugin":
23
+ return self._run_plugin_command(args)
24
+
25
+ runner = CommandRunner(
26
+ ObsidianVault(self._vault_locator.resolve(args.vault))
27
+ )
28
+
29
+ if args.command == "check":
30
+ return runner.check()
31
+ if args.command == "list":
32
+ return runner.list_notes(args.limit)
33
+ if args.command == "read":
34
+ return runner.read_note(args.note_path)
35
+ if args.command == "read-lines":
36
+ return runner.read_note_lines(
37
+ args.note_path, args.start_line, args.end_line
38
+ )
39
+ if args.command == "write":
40
+ return runner.write_note(
41
+ args.note_path,
42
+ self._read_content_arg(args.content, args.stdin),
43
+ args.create_only,
44
+ )
45
+ if args.command == "append":
46
+ return runner.append_note(
47
+ args.note_path,
48
+ self._read_content_arg(args.content, args.stdin),
49
+ )
50
+ if args.command == "write-lines":
51
+ return runner.write_note_lines(
52
+ args.note_path,
53
+ args.start_line,
54
+ args.end_line,
55
+ self._read_content_arg(args.content, args.stdin),
56
+ )
57
+ if args.command == "search":
58
+ return runner.search(args.query, args.case_sensitive)
59
+ except VaultError as exc:
60
+ print(f"error: {exc}", file=sys.stderr)
61
+ return 2
62
+
63
+ self._parser.error(f"unsupported command: {args.command}")
64
+ return 2
65
+
66
+ def _build_parser(self) -> argparse.ArgumentParser:
67
+ parser = argparse.ArgumentParser(
68
+ prog="obsidian-cli",
69
+ description="Read and write notes inside a local Obsidian vault.",
70
+ )
71
+ parser.add_argument(
72
+ "--vault",
73
+ help="Path to the Obsidian vault. Falls back to OBSIDIAN_VAULT.",
74
+ )
75
+
76
+ subparsers = parser.add_subparsers(dest="command", required=True)
77
+ subparsers.add_parser("check", help="Validate the vault path.")
78
+
79
+ list_parser = subparsers.add_parser("list", help="List markdown notes.")
80
+ list_parser.add_argument(
81
+ "--limit", type=int, default=0, help="Maximum number of notes to show."
82
+ )
83
+
84
+ read_parser = subparsers.add_parser("read", help="Read a note.")
85
+ read_parser.add_argument(
86
+ "note_path", help="Path to the note relative to the vault root."
87
+ )
88
+
89
+ read_lines_parser = subparsers.add_parser(
90
+ "read-lines", help="Read a line range from a note."
91
+ )
92
+ read_lines_parser.add_argument(
93
+ "note_path", help="Path to the note relative to the vault root."
94
+ )
95
+ read_lines_parser.add_argument(
96
+ "start_line", type=int, help="1-based start line, inclusive."
97
+ )
98
+ read_lines_parser.add_argument(
99
+ "end_line", type=int, help="1-based end line, inclusive."
100
+ )
101
+
102
+ write_parser = subparsers.add_parser("write", help="Write a note.")
103
+ write_parser.add_argument(
104
+ "note_path", help="Path to the note relative to the vault root."
105
+ )
106
+ write_parser.add_argument("--content", help="Text content to write.")
107
+ write_parser.add_argument(
108
+ "--stdin", action="store_true", help="Read note content from stdin."
109
+ )
110
+ write_parser.add_argument(
111
+ "--create-only",
112
+ action="store_true",
113
+ help="Fail if the note already exists.",
114
+ )
115
+
116
+ append_parser = subparsers.add_parser("append", help="Append to a note.")
117
+ append_parser.add_argument(
118
+ "note_path", help="Path to the note relative to the vault root."
119
+ )
120
+ append_parser.add_argument("--content", help="Text content to append.")
121
+ append_parser.add_argument(
122
+ "--stdin", action="store_true", help="Read appended content from stdin."
123
+ )
124
+
125
+ write_lines_parser = subparsers.add_parser(
126
+ "write-lines",
127
+ help="Replace a line range inside a note.",
128
+ )
129
+ write_lines_parser.add_argument(
130
+ "note_path", help="Path to the note relative to the vault root."
131
+ )
132
+ write_lines_parser.add_argument(
133
+ "start_line", type=int, help="1-based start line, inclusive."
134
+ )
135
+ write_lines_parser.add_argument(
136
+ "end_line", type=int, help="1-based end line, inclusive."
137
+ )
138
+ write_lines_parser.add_argument(
139
+ "--content", help="Replacement text for the selected lines."
140
+ )
141
+ write_lines_parser.add_argument(
142
+ "--stdin", action="store_true", help="Read replacement text from stdin."
143
+ )
144
+
145
+ search_parser = subparsers.add_parser("search", help="Search text in notes.")
146
+ search_parser.add_argument("query", help="Text to search for.")
147
+ search_parser.add_argument(
148
+ "--case-sensitive",
149
+ action="store_true",
150
+ help="Use case-sensitive matching.",
151
+ )
152
+
153
+ plugin_parser = subparsers.add_parser(
154
+ "plugin", help="Install odcli helper skills for supported coding tools."
155
+ )
156
+ plugin_subparsers = plugin_parser.add_subparsers(
157
+ dest="plugin_command", required=True
158
+ )
159
+ plugin_install_parser = plugin_subparsers.add_parser(
160
+ "install", help="Install an odcli skill into a supported tool directory."
161
+ )
162
+ plugin_install_parser.add_argument(
163
+ "target",
164
+ choices=["codex-skill", "claude-skill", "all-skills"],
165
+ help="Installation target.",
166
+ )
167
+
168
+ return parser
169
+
170
+ def _run_plugin_command(self, args: argparse.Namespace) -> int:
171
+ if args.plugin_command == "install":
172
+ for result in self._skill_installer.install(args.target):
173
+ print(f"{result.target}: {result.path}")
174
+ return 0
175
+ self._parser.error(f"unsupported plugin command: {args.plugin_command}")
176
+ return 2
177
+
178
+ @staticmethod
179
+ def _read_content_arg(content: str | None, use_stdin: bool) -> str:
180
+ if content is not None and use_stdin:
181
+ raise VaultError("use either --content or --stdin, not both")
182
+ if content is not None:
183
+ return content
184
+ if use_stdin:
185
+ return sys.stdin.read()
186
+ raise VaultError("content is required; provide --content or --stdin")
187
+
188
+
189
+ def main(argv: list[str] | None = None) -> int:
190
+ return ObsidianCLI().run(argv)