notes-watcher 0.1.0__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.
- notes_watcher-0.1.0/LICENSE +21 -0
- notes_watcher-0.1.0/PKG-INFO +214 -0
- notes_watcher-0.1.0/README.md +190 -0
- notes_watcher-0.1.0/note_watcher/__init__.py +3 -0
- notes_watcher-0.1.0/note_watcher/cli.py +116 -0
- notes_watcher-0.1.0/note_watcher/config.py +110 -0
- notes_watcher-0.1.0/note_watcher/debouncer.py +78 -0
- notes_watcher-0.1.0/note_watcher/dispatcher.py +92 -0
- notes_watcher-0.1.0/note_watcher/parser.py +78 -0
- notes_watcher-0.1.0/note_watcher/watcher.py +220 -0
- notes_watcher-0.1.0/note_watcher/writer.py +76 -0
- notes_watcher-0.1.0/notes_watcher.egg-info/PKG-INFO +214 -0
- notes_watcher-0.1.0/notes_watcher.egg-info/SOURCES.txt +25 -0
- notes_watcher-0.1.0/notes_watcher.egg-info/dependency_links.txt +1 -0
- notes_watcher-0.1.0/notes_watcher.egg-info/entry_points.txt +2 -0
- notes_watcher-0.1.0/notes_watcher.egg-info/requires.txt +7 -0
- notes_watcher-0.1.0/notes_watcher.egg-info/top_level.txt +1 -0
- notes_watcher-0.1.0/pyproject.toml +47 -0
- notes_watcher-0.1.0/setup.cfg +4 -0
- notes_watcher-0.1.0/tests/test_cli.py +99 -0
- notes_watcher-0.1.0/tests/test_config.py +119 -0
- notes_watcher-0.1.0/tests/test_debouncer.py +103 -0
- notes_watcher-0.1.0/tests/test_dispatcher.py +94 -0
- notes_watcher-0.1.0/tests/test_integration.py +190 -0
- notes_watcher-0.1.0/tests/test_parser.py +123 -0
- notes_watcher-0.1.0/tests/test_watcher.py +149 -0
- notes_watcher-0.1.0/tests/test_writer.py +118 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Britt Crawford
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: notes-watcher
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A daemon that detects @ mentions in Obsidian notes, dispatches instructions to AI agents, and writes results back inline.
|
|
5
|
+
Author: Britt Crawford
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/britt/obsidian-notes-watcher
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: watchdog>=3.0
|
|
18
|
+
Requires-Dist: pyyaml>=6.0
|
|
19
|
+
Requires-Dist: click>=8.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# Note Watcher
|
|
26
|
+
|
|
27
|
+
A tool that detects `@` mentions in Obsidian markdown notes stored in Git and dispatches instructions to configured agents — like [Claude Code](https://docs.anthropic.com/en/docs/claude-code) — that can read, modify, and reorganize your notes directly.
|
|
28
|
+
|
|
29
|
+
Write `@agent_name do something` in any note, and Note Watcher dispatches the instruction to the named agent. The agent can edit files, create new notes, restructure content, or make any other changes to your vault. The original instruction is then replaced with a completion marker (an HTML comment, invisible in rendered markdown) so it is never reprocessed:
|
|
30
|
+
|
|
31
|
+
```markdown
|
|
32
|
+
<!-- @done agent_name: do something
|
|
33
|
+
Agent response summary goes here.
|
|
34
|
+
/@done -->
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The real work happens in the commit: the agent's changes to your vault are committed back to Git. The completion comment is just a record that the instruction was processed.
|
|
38
|
+
|
|
39
|
+
## Modes of Operation
|
|
40
|
+
|
|
41
|
+
| Mode | Use case |
|
|
42
|
+
|------|----------|
|
|
43
|
+
| **Daemon** | Real-time file watching on macOS via a LaunchAgent |
|
|
44
|
+
| **GitHub Action** | One-shot batch processing on every push that changes `.md` files |
|
|
45
|
+
|
|
46
|
+
## Requirements
|
|
47
|
+
|
|
48
|
+
- Python 3.10+
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install notes-watcher
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For development:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install -e ".[dev]"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Configuration
|
|
63
|
+
|
|
64
|
+
Copy the example config and edit it:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
mkdir -p ~/.config/note-watcher
|
|
68
|
+
cp config.example.yml ~/.config/note-watcher/config.yml
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The default config location is `~/.config/note-watcher/config.yml`. You can override it with `--config`:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
note-watcher watch --config /path/to/config.yml
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Config reference
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
# Path to your Obsidian vault
|
|
81
|
+
vault: ~/Obsidian/MyVault
|
|
82
|
+
|
|
83
|
+
# Seconds to wait before processing after a file change
|
|
84
|
+
debounce_seconds: 1.0
|
|
85
|
+
|
|
86
|
+
# File patterns to ignore (glob syntax)
|
|
87
|
+
ignore_patterns:
|
|
88
|
+
- "*.excalidraw.md"
|
|
89
|
+
- ".trash/**"
|
|
90
|
+
|
|
91
|
+
# Agent definitions
|
|
92
|
+
agents:
|
|
93
|
+
summarizer:
|
|
94
|
+
type: echo # Returns instruction text unchanged
|
|
95
|
+
uppercase:
|
|
96
|
+
type: uppercase # Returns instruction text in uppercase
|
|
97
|
+
word_count:
|
|
98
|
+
type: command
|
|
99
|
+
command: "wc -w" # Runs a shell command, passes instruction via stdin
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Agent types
|
|
103
|
+
|
|
104
|
+
| Type | Behavior |
|
|
105
|
+
|------|----------|
|
|
106
|
+
| `echo` | Returns the instruction text unchanged |
|
|
107
|
+
| `uppercase` | Returns the instruction text in uppercase |
|
|
108
|
+
| `command` | Runs a shell command with instruction text on stdin, returns stdout |
|
|
109
|
+
|
|
110
|
+
### Example: Using Claude Code as an agent
|
|
111
|
+
|
|
112
|
+
Configure a `command` agent that dispatches instructions to [Claude Code](https://docs.anthropic.com/en/docs/claude-code):
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
agents:
|
|
116
|
+
claude:
|
|
117
|
+
type: command
|
|
118
|
+
command: "claude -p" # Dispatches instruction to Claude Code CLI
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Claude Code runs with full access to your vault, so it can edit notes, create new files, and reorganize content — not just respond in a comment. Write `@claude` instructions in your notes:
|
|
122
|
+
|
|
123
|
+
```markdown
|
|
124
|
+
@claude Summarize the key points of this meeting and add action items to my Tasks note
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Daemon Mode
|
|
128
|
+
|
|
129
|
+
Daemon mode continuously watches your Obsidian vault for changes and processes `@` mentions in real time.
|
|
130
|
+
|
|
131
|
+
### Running manually
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# Watch the vault specified in your config
|
|
135
|
+
note-watcher watch
|
|
136
|
+
|
|
137
|
+
# Override the vault path
|
|
138
|
+
note-watcher watch --vault ~/Obsidian/MyVault
|
|
139
|
+
|
|
140
|
+
# Enable verbose logging
|
|
141
|
+
note-watcher -v watch --vault ~/Obsidian/MyVault
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Stop the daemon with `Ctrl+C` (`SIGINT`) or `SIGTERM`.
|
|
145
|
+
|
|
146
|
+
### Installing as a macOS LaunchAgent
|
|
147
|
+
|
|
148
|
+
The included install script sets up Note Watcher as a LaunchAgent that starts on login and restarts on crash:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
./scripts/install.sh
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The script is idempotent and safe to run multiple times. It will:
|
|
155
|
+
|
|
156
|
+
1. Detect the `note-watcher` executable on your system
|
|
157
|
+
2. Generate a LaunchAgent plist from the included template
|
|
158
|
+
3. Install it to `~/Library/LaunchAgents/`
|
|
159
|
+
4. Start the daemon
|
|
160
|
+
|
|
161
|
+
Logs are written to `~/Library/Logs/note-watcher/`.
|
|
162
|
+
|
|
163
|
+
### Uninstalling the LaunchAgent
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
./scripts/uninstall.sh
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
To also remove the log directory:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
./scripts/uninstall.sh --clean
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## GitHub Action Mode
|
|
176
|
+
|
|
177
|
+
GitHub Action mode processes all pending `@` instructions across the entire vault in a single batch run. This is useful for vaults stored in a Git repository.
|
|
178
|
+
|
|
179
|
+
### CLI usage
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
note-watcher process --all --vault /path/to/vault
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Setting up the GitHub Actions workflow
|
|
186
|
+
|
|
187
|
+
See [`examples/github-action/`](examples/github-action/) for a complete, ready-to-copy example that uses [Claude Code](https://docs.anthropic.com/en/docs/claude-code) as the AI agent.
|
|
188
|
+
|
|
189
|
+
To set it up:
|
|
190
|
+
|
|
191
|
+
1. Copy `examples/github-action/.github/` into your notes repository
|
|
192
|
+
2. Add a `config.yml` to your notes repo (see `examples/github-action/config.example.yml`)
|
|
193
|
+
3. Add your `ANTHROPIC_API_KEY` as a repository secret
|
|
194
|
+
4. Under **Settings > Actions > General**, set "Workflow permissions" to "Read and write permissions"
|
|
195
|
+
|
|
196
|
+
The workflow triggers on any push that modifies `.md` files, processes all unprocessed `@` instructions, and commits the agent's changes back to your repository. It uses `[skip ci]` to prevent infinite loops.
|
|
197
|
+
|
|
198
|
+
See the [Claude Code GitHub Actions documentation](https://docs.anthropic.com/en/docs/claude-code/github-actions) for more on setting up Claude Code in CI.
|
|
199
|
+
|
|
200
|
+
## Running Tests
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
pytest
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
With coverage:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
pytest --cov=note_watcher
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Note Watcher
|
|
2
|
+
|
|
3
|
+
A tool that detects `@` mentions in Obsidian markdown notes stored in Git and dispatches instructions to configured agents — like [Claude Code](https://docs.anthropic.com/en/docs/claude-code) — that can read, modify, and reorganize your notes directly.
|
|
4
|
+
|
|
5
|
+
Write `@agent_name do something` in any note, and Note Watcher dispatches the instruction to the named agent. The agent can edit files, create new notes, restructure content, or make any other changes to your vault. The original instruction is then replaced with a completion marker (an HTML comment, invisible in rendered markdown) so it is never reprocessed:
|
|
6
|
+
|
|
7
|
+
```markdown
|
|
8
|
+
<!-- @done agent_name: do something
|
|
9
|
+
Agent response summary goes here.
|
|
10
|
+
/@done -->
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The real work happens in the commit: the agent's changes to your vault are committed back to Git. The completion comment is just a record that the instruction was processed.
|
|
14
|
+
|
|
15
|
+
## Modes of Operation
|
|
16
|
+
|
|
17
|
+
| Mode | Use case |
|
|
18
|
+
|------|----------|
|
|
19
|
+
| **Daemon** | Real-time file watching on macOS via a LaunchAgent |
|
|
20
|
+
| **GitHub Action** | One-shot batch processing on every push that changes `.md` files |
|
|
21
|
+
|
|
22
|
+
## Requirements
|
|
23
|
+
|
|
24
|
+
- Python 3.10+
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install notes-watcher
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For development:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install -e ".[dev]"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
Copy the example config and edit it:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
mkdir -p ~/.config/note-watcher
|
|
44
|
+
cp config.example.yml ~/.config/note-watcher/config.yml
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The default config location is `~/.config/note-watcher/config.yml`. You can override it with `--config`:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
note-watcher watch --config /path/to/config.yml
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Config reference
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
# Path to your Obsidian vault
|
|
57
|
+
vault: ~/Obsidian/MyVault
|
|
58
|
+
|
|
59
|
+
# Seconds to wait before processing after a file change
|
|
60
|
+
debounce_seconds: 1.0
|
|
61
|
+
|
|
62
|
+
# File patterns to ignore (glob syntax)
|
|
63
|
+
ignore_patterns:
|
|
64
|
+
- "*.excalidraw.md"
|
|
65
|
+
- ".trash/**"
|
|
66
|
+
|
|
67
|
+
# Agent definitions
|
|
68
|
+
agents:
|
|
69
|
+
summarizer:
|
|
70
|
+
type: echo # Returns instruction text unchanged
|
|
71
|
+
uppercase:
|
|
72
|
+
type: uppercase # Returns instruction text in uppercase
|
|
73
|
+
word_count:
|
|
74
|
+
type: command
|
|
75
|
+
command: "wc -w" # Runs a shell command, passes instruction via stdin
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Agent types
|
|
79
|
+
|
|
80
|
+
| Type | Behavior |
|
|
81
|
+
|------|----------|
|
|
82
|
+
| `echo` | Returns the instruction text unchanged |
|
|
83
|
+
| `uppercase` | Returns the instruction text in uppercase |
|
|
84
|
+
| `command` | Runs a shell command with instruction text on stdin, returns stdout |
|
|
85
|
+
|
|
86
|
+
### Example: Using Claude Code as an agent
|
|
87
|
+
|
|
88
|
+
Configure a `command` agent that dispatches instructions to [Claude Code](https://docs.anthropic.com/en/docs/claude-code):
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
91
|
+
agents:
|
|
92
|
+
claude:
|
|
93
|
+
type: command
|
|
94
|
+
command: "claude -p" # Dispatches instruction to Claude Code CLI
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Claude Code runs with full access to your vault, so it can edit notes, create new files, and reorganize content — not just respond in a comment. Write `@claude` instructions in your notes:
|
|
98
|
+
|
|
99
|
+
```markdown
|
|
100
|
+
@claude Summarize the key points of this meeting and add action items to my Tasks note
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Daemon Mode
|
|
104
|
+
|
|
105
|
+
Daemon mode continuously watches your Obsidian vault for changes and processes `@` mentions in real time.
|
|
106
|
+
|
|
107
|
+
### Running manually
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# Watch the vault specified in your config
|
|
111
|
+
note-watcher watch
|
|
112
|
+
|
|
113
|
+
# Override the vault path
|
|
114
|
+
note-watcher watch --vault ~/Obsidian/MyVault
|
|
115
|
+
|
|
116
|
+
# Enable verbose logging
|
|
117
|
+
note-watcher -v watch --vault ~/Obsidian/MyVault
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Stop the daemon with `Ctrl+C` (`SIGINT`) or `SIGTERM`.
|
|
121
|
+
|
|
122
|
+
### Installing as a macOS LaunchAgent
|
|
123
|
+
|
|
124
|
+
The included install script sets up Note Watcher as a LaunchAgent that starts on login and restarts on crash:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
./scripts/install.sh
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The script is idempotent and safe to run multiple times. It will:
|
|
131
|
+
|
|
132
|
+
1. Detect the `note-watcher` executable on your system
|
|
133
|
+
2. Generate a LaunchAgent plist from the included template
|
|
134
|
+
3. Install it to `~/Library/LaunchAgents/`
|
|
135
|
+
4. Start the daemon
|
|
136
|
+
|
|
137
|
+
Logs are written to `~/Library/Logs/note-watcher/`.
|
|
138
|
+
|
|
139
|
+
### Uninstalling the LaunchAgent
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
./scripts/uninstall.sh
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
To also remove the log directory:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
./scripts/uninstall.sh --clean
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## GitHub Action Mode
|
|
152
|
+
|
|
153
|
+
GitHub Action mode processes all pending `@` instructions across the entire vault in a single batch run. This is useful for vaults stored in a Git repository.
|
|
154
|
+
|
|
155
|
+
### CLI usage
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
note-watcher process --all --vault /path/to/vault
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Setting up the GitHub Actions workflow
|
|
162
|
+
|
|
163
|
+
See [`examples/github-action/`](examples/github-action/) for a complete, ready-to-copy example that uses [Claude Code](https://docs.anthropic.com/en/docs/claude-code) as the AI agent.
|
|
164
|
+
|
|
165
|
+
To set it up:
|
|
166
|
+
|
|
167
|
+
1. Copy `examples/github-action/.github/` into your notes repository
|
|
168
|
+
2. Add a `config.yml` to your notes repo (see `examples/github-action/config.example.yml`)
|
|
169
|
+
3. Add your `ANTHROPIC_API_KEY` as a repository secret
|
|
170
|
+
4. Under **Settings > Actions > General**, set "Workflow permissions" to "Read and write permissions"
|
|
171
|
+
|
|
172
|
+
The workflow triggers on any push that modifies `.md` files, processes all unprocessed `@` instructions, and commits the agent's changes back to your repository. It uses `[skip ci]` to prevent infinite loops.
|
|
173
|
+
|
|
174
|
+
See the [Claude Code GitHub Actions documentation](https://docs.anthropic.com/en/docs/claude-code/github-actions) for more on setting up Claude Code in CI.
|
|
175
|
+
|
|
176
|
+
## Running Tests
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
pytest
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
With coverage:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
pytest --cov=note_watcher
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Click CLI for Note Watcher.
|
|
2
|
+
|
|
3
|
+
Provides two commands:
|
|
4
|
+
- `watch`: Starts the file watcher daemon
|
|
5
|
+
- `process`: Batch processes all pending instructions
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
from note_watcher.config import load_config
|
|
17
|
+
from note_watcher.dispatcher import AgentDispatcher
|
|
18
|
+
from note_watcher.watcher import process_file_reparse, start_watcher
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def setup_logging(verbose: bool = False) -> None:
|
|
22
|
+
"""Configure logging to stdout/stderr."""
|
|
23
|
+
level = logging.DEBUG if verbose else logging.INFO
|
|
24
|
+
logging.basicConfig(
|
|
25
|
+
level=level,
|
|
26
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
27
|
+
stream=sys.stderr,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@click.group()
|
|
32
|
+
@click.option("--verbose", "-v", is_flag=True, help="Enable debug logging.")
|
|
33
|
+
def main(verbose: bool) -> None:
|
|
34
|
+
"""Note Watcher - Detect @ mentions in Obsidian notes and dispatch to AI agents."""
|
|
35
|
+
setup_logging(verbose)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@main.command()
|
|
39
|
+
@click.option(
|
|
40
|
+
"--vault",
|
|
41
|
+
type=click.Path(exists=True, file_okay=False, resolve_path=True),
|
|
42
|
+
help="Path to the Obsidian vault directory.",
|
|
43
|
+
)
|
|
44
|
+
@click.option(
|
|
45
|
+
"--config",
|
|
46
|
+
"config_path",
|
|
47
|
+
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
|
|
48
|
+
help="Path to the configuration file.",
|
|
49
|
+
)
|
|
50
|
+
def watch(vault: str | None, config_path: str | None) -> None:
|
|
51
|
+
"""Start the file watcher daemon.
|
|
52
|
+
|
|
53
|
+
Monitors the vault directory for changes to .md files and processes
|
|
54
|
+
@ mention instructions as they appear.
|
|
55
|
+
"""
|
|
56
|
+
config = load_config(config_path)
|
|
57
|
+
|
|
58
|
+
if vault:
|
|
59
|
+
config.vault = Path(vault)
|
|
60
|
+
|
|
61
|
+
if not config.vault.is_dir():
|
|
62
|
+
click.echo(f"Error: Vault directory does not exist: {config.vault}", err=True)
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
click.echo(f"Watching vault: {config.vault}")
|
|
66
|
+
start_watcher(config)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@main.command()
|
|
70
|
+
@click.option(
|
|
71
|
+
"--all",
|
|
72
|
+
"process_all",
|
|
73
|
+
is_flag=True,
|
|
74
|
+
required=True,
|
|
75
|
+
help="Process all pending instructions.",
|
|
76
|
+
)
|
|
77
|
+
@click.option(
|
|
78
|
+
"--vault",
|
|
79
|
+
type=click.Path(exists=True, file_okay=False, resolve_path=True),
|
|
80
|
+
help="Path to the Obsidian vault directory.",
|
|
81
|
+
)
|
|
82
|
+
@click.option(
|
|
83
|
+
"--config",
|
|
84
|
+
"config_path",
|
|
85
|
+
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
|
|
86
|
+
help="Path to the configuration file.",
|
|
87
|
+
)
|
|
88
|
+
def process(process_all: bool, vault: str | None, config_path: str | None) -> None:
|
|
89
|
+
"""Batch process all pending @ mention instructions.
|
|
90
|
+
|
|
91
|
+
Scans all .md files in the vault for unprocessed instructions,
|
|
92
|
+
dispatches them to configured agents, and writes results inline.
|
|
93
|
+
"""
|
|
94
|
+
config = load_config(config_path)
|
|
95
|
+
|
|
96
|
+
if vault:
|
|
97
|
+
config.vault = Path(vault)
|
|
98
|
+
|
|
99
|
+
if not config.vault.is_dir():
|
|
100
|
+
click.echo(f"Error: Vault directory does not exist: {config.vault}", err=True)
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
|
|
103
|
+
dispatcher = AgentDispatcher(config)
|
|
104
|
+
vault_path = config.vault
|
|
105
|
+
|
|
106
|
+
# Find all .md files
|
|
107
|
+
md_files = sorted(vault_path.rglob("*.md"))
|
|
108
|
+
total_processed = 0
|
|
109
|
+
|
|
110
|
+
for md_file in md_files:
|
|
111
|
+
count = process_file_reparse(str(md_file), dispatcher)
|
|
112
|
+
if count > 0:
|
|
113
|
+
click.echo(f"Processed {count} instruction(s) in {md_file}")
|
|
114
|
+
total_processed += count
|
|
115
|
+
|
|
116
|
+
click.echo(f"Done. Processed {total_processed} instruction(s) total.")
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Configuration loading and management for Note Watcher.
|
|
2
|
+
|
|
3
|
+
Loads YAML configuration from a file, applies sensible defaults,
|
|
4
|
+
and validates required fields.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
DEFAULT_CONFIG_PATH = Path.home() / ".config" / "note-watcher" / "config.yml"
|
|
18
|
+
|
|
19
|
+
DEFAULT_DEBOUNCE_SECONDS = 1.0
|
|
20
|
+
|
|
21
|
+
DEFAULT_IGNORE_PATTERNS: list[str] = [
|
|
22
|
+
"*.excalidraw.md",
|
|
23
|
+
".trash/**",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class AgentConfig:
|
|
29
|
+
"""Configuration for a single agent."""
|
|
30
|
+
|
|
31
|
+
name: str
|
|
32
|
+
type: str
|
|
33
|
+
command: str | None = None
|
|
34
|
+
callable: str | None = None
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_dict(cls, name: str, data: dict[str, Any]) -> AgentConfig:
|
|
38
|
+
return cls(
|
|
39
|
+
name=name,
|
|
40
|
+
type=data.get("type", "echo"),
|
|
41
|
+
command=data.get("command"),
|
|
42
|
+
callable=data.get("callable"),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class Config:
|
|
48
|
+
"""Application configuration."""
|
|
49
|
+
|
|
50
|
+
vault: Path
|
|
51
|
+
debounce_seconds: float = DEFAULT_DEBOUNCE_SECONDS
|
|
52
|
+
ignore_patterns: list[str] = field(default_factory=lambda: list(DEFAULT_IGNORE_PATTERNS))
|
|
53
|
+
agents: dict[str, AgentConfig] = field(default_factory=dict)
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def from_dict(cls, data: dict[str, Any]) -> Config:
|
|
57
|
+
"""Create a Config from a parsed YAML dictionary."""
|
|
58
|
+
vault_str = data.get("vault", ".")
|
|
59
|
+
# Expand ~ and environment variables in the vault path
|
|
60
|
+
vault = Path(os.path.expanduser(os.path.expandvars(vault_str)))
|
|
61
|
+
|
|
62
|
+
debounce = data.get("debounce_seconds", DEFAULT_DEBOUNCE_SECONDS)
|
|
63
|
+
|
|
64
|
+
ignore = data.get("ignore_patterns", list(DEFAULT_IGNORE_PATTERNS))
|
|
65
|
+
|
|
66
|
+
agents: dict[str, AgentConfig] = {}
|
|
67
|
+
for name, agent_data in data.get("agents", {}).items():
|
|
68
|
+
if isinstance(agent_data, dict):
|
|
69
|
+
agents[name] = AgentConfig.from_dict(name, agent_data)
|
|
70
|
+
else:
|
|
71
|
+
# Simple string value treated as the type
|
|
72
|
+
agents[name] = AgentConfig(name=name, type=str(agent_data))
|
|
73
|
+
|
|
74
|
+
return cls(
|
|
75
|
+
vault=vault,
|
|
76
|
+
debounce_seconds=float(debounce),
|
|
77
|
+
ignore_patterns=ignore,
|
|
78
|
+
agents=agents,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def defaults(cls, vault: str | Path = ".") -> Config:
|
|
83
|
+
"""Create a Config with default values."""
|
|
84
|
+
return cls(vault=Path(vault))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def load_config(config_path: str | Path | None = None) -> Config:
|
|
88
|
+
"""Load configuration from a YAML file.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
config_path: Path to the config file. If None, uses the default path.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
A Config instance. If the config file doesn't exist, returns defaults.
|
|
95
|
+
"""
|
|
96
|
+
if config_path is None:
|
|
97
|
+
path = DEFAULT_CONFIG_PATH
|
|
98
|
+
else:
|
|
99
|
+
path = Path(config_path)
|
|
100
|
+
|
|
101
|
+
if not path.exists():
|
|
102
|
+
return Config.defaults()
|
|
103
|
+
|
|
104
|
+
with open(path) as f:
|
|
105
|
+
data = yaml.safe_load(f)
|
|
106
|
+
|
|
107
|
+
if data is None:
|
|
108
|
+
return Config.defaults()
|
|
109
|
+
|
|
110
|
+
return Config.from_dict(data)
|