tag-keeper 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.
Files changed (47) hide show
  1. tag_keeper-0.1.0/.gitignore +14 -0
  2. tag_keeper-0.1.0/CHANGELOG.md +49 -0
  3. tag_keeper-0.1.0/LICENSE +21 -0
  4. tag_keeper-0.1.0/PKG-INFO +310 -0
  5. tag_keeper-0.1.0/README.ja.md +243 -0
  6. tag_keeper-0.1.0/README.md +276 -0
  7. tag_keeper-0.1.0/pyproject.toml +73 -0
  8. tag_keeper-0.1.0/tag_keeper/__init__.py +3 -0
  9. tag_keeper-0.1.0/tag_keeper/__main__.py +5 -0
  10. tag_keeper-0.1.0/tag_keeper/catalog.py +170 -0
  11. tag_keeper-0.1.0/tag_keeper/cli.py +711 -0
  12. tag_keeper-0.1.0/tag_keeper/config.py +269 -0
  13. tag_keeper-0.1.0/tag_keeper/decisions.py +175 -0
  14. tag_keeper-0.1.0/tag_keeper/extract.py +121 -0
  15. tag_keeper-0.1.0/tag_keeper/hashing.py +123 -0
  16. tag_keeper-0.1.0/tag_keeper/history.py +212 -0
  17. tag_keeper-0.1.0/tag_keeper/jobs.py +260 -0
  18. tag_keeper-0.1.0/tag_keeper/organize.py +759 -0
  19. tag_keeper-0.1.0/tag_keeper/plan.py +1050 -0
  20. tag_keeper-0.1.0/tag_keeper/report.py +287 -0
  21. tag_keeper-0.1.0/tag_keeper/rules.py +218 -0
  22. tag_keeper-0.1.0/tag_keeper/scan.py +323 -0
  23. tag_keeper-0.1.0/tag_keeper/suggest_queue.py +194 -0
  24. tag_keeper-0.1.0/tag_keeper/syncguard.py +234 -0
  25. tag_keeper-0.1.0/tag_keeper/tags.py +254 -0
  26. tag_keeper-0.1.0/tag_keeper/textindex.py +53 -0
  27. tag_keeper-0.1.0/tag_keeper/web/__init__.py +1 -0
  28. tag_keeper-0.1.0/tag_keeper/web/auth.py +98 -0
  29. tag_keeper-0.1.0/tag_keeper/web/files.py +87 -0
  30. tag_keeper-0.1.0/tag_keeper/web/server.py +1135 -0
  31. tag_keeper-0.1.0/tag_keeper/web/static/app.js +582 -0
  32. tag_keeper-0.1.0/tag_keeper/web/static/index.html +38 -0
  33. tag_keeper-0.1.0/tag_keeper/web/static/pages.js +608 -0
  34. tag_keeper-0.1.0/tag_keeper/web/static/style.css +218 -0
  35. tag_keeper-0.1.0/tests/conftest.py +50 -0
  36. tag_keeper-0.1.0/tests/test_config_and_cli.py +95 -0
  37. tag_keeper-0.1.0/tests/test_decisions.py +113 -0
  38. tag_keeper-0.1.0/tests/test_hashing.py +63 -0
  39. tag_keeper-0.1.0/tests/test_history.py +63 -0
  40. tag_keeper-0.1.0/tests/test_organize.py +149 -0
  41. tag_keeper-0.1.0/tests/test_plan.py +337 -0
  42. tag_keeper-0.1.0/tests/test_report.py +143 -0
  43. tag_keeper-0.1.0/tests/test_rules.py +70 -0
  44. tag_keeper-0.1.0/tests/test_scan.py +189 -0
  45. tag_keeper-0.1.0/tests/test_syncguard.py +173 -0
  46. tag_keeper-0.1.0/tests/test_tags.py +78 -0
  47. tag_keeper-0.1.0/tests/test_web.py +252 -0
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ venv/
8
+ .pytest_cache/
9
+ .coverage
10
+ htmlcov/
11
+ *.log
12
+
13
+ # uv が開発用に生成するロックファイル (ライブラリなので固定しない)
14
+ uv.lock
@@ -0,0 +1,49 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-10-08
9
+
10
+ First release. The web UI and messages are in Japanese.
11
+
12
+ ### Added
13
+
14
+ - **Inventory.** `scan` walks a root read-only into an SQLite catalog kept outside the
15
+ tree, tracking renames and moves by inode. Mass disappearances are held until a second
16
+ scan confirms them; a missing or empty root aborts the scan. `hash` computes SHA-256
17
+ incrementally.
18
+ - **Hygiene report.** `report` lists application data in synced folders, rebuildable
19
+ artifacts, VM images, installers, temporary files, Office lock files, sync conflict copies
20
+ (only when the original is next to them; differing copies are marked for review), empty
21
+ files and folders, and duplicates confirmed by hash.
22
+ - **Tidy-up plans.** `plan` writes candidates to an editable TOML file; `apply` previews,
23
+ then moves items to a quarantine folder outside the tree or renames and moves them inside
24
+ it, skipping anything that changed since the plan was written, with snapper pre/post
25
+ snapshots around the run. Every move is journaled and `undo` puts it back without
26
+ overwriting anything new. Destinations are checked against OneDrive's naming rules.
27
+ - **Big deletions without stopping the sync client.** With `sync_client = "onedrive"`, a
28
+ plan that removes a folder above the OneDrive client's `classify_as_big_delete` threshold
29
+ stops the resident sync, reflects the deletions with one `onedrive --sync` whose threshold
30
+ is raised just enough (never `--force`), and starts it again. A failed reflection leaves
31
+ the sync stopped, is journaled, and can be retried or abandoned (`tag-keeper sync`).
32
+ - **Web UI** (`serve`): roots, report, plans (untick to skip), check, apply and undo, with
33
+ background jobs and a progress panel. Loopback by default; other addresses require HTTP
34
+ Basic auth, and writes must be same-origin.
35
+ - **File browser** with in-page previews for types that cannot run scripts; paths never
36
+ leave the root.
37
+ - **Tags** on files and folders, inherited from folders, kept in append-only JSONL logs one
38
+ per machine, and following moves seen by `scan` or made by plans.
39
+ - **Version history** from btrfs snapshots via btrfs-timeline: versions of a file, a folder
40
+ at any snapshot, restoring an old version beside the current one. Moves are recorded, so
41
+ history continues across them.
42
+ - **Inbox suggestions.** Files in folders named like `tmp` or `*未整理*` are listed flat;
43
+ picking one asks an ollama model (local or cloud) for a `YYYYMMDD_title` name, destination
44
+ folders among existing ones and tags, usually within seconds. Accepted suggestions run as
45
+ one plan of renames and moves. Files elsewhere get suggestions on request.
46
+ - **Decision log.** Accepted suggestions, tag accept and reject, and undone plans are
47
+ appended per machine; the closest past decisions are given to the model as examples.
48
+
49
+ [0.1.0]: https://github.com/akivajp/tag-keeper/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Akiva Miura
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,310 @@
1
+ Metadata-Version: 2.5
2
+ Name: tag-keeper
3
+ Version: 0.1.0
4
+ Summary: Tag, search and tidy your existing file trees from the outside, without moving a file
5
+ Project-URL: Homepage, https://github.com/akivajp/tag-keeper
6
+ Project-URL: Repository, https://github.com/akivajp/tag-keeper
7
+ Project-URL: Issues, https://github.com/akivajp/tag-keeper/issues
8
+ Author-email: Akiva Miura <akiva.miura@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: catalog,cloud-drive,file-management,onedrive,pcloud,self-hosted,tagging
12
+ Classifier: Development Status :: 2 - Pre-Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Environment :: Web Environment
15
+ Classifier: Framework :: Bottle
16
+ Classifier: Intended Audience :: End Users/Desktop
17
+ Classifier: Intended Audience :: System Administrators
18
+ Classifier: Natural Language :: Japanese
19
+ Classifier: Operating System :: POSIX :: Linux
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: System :: Filesystems
25
+ Classifier: Topic :: Utilities
26
+ Requires-Python: >=3.11
27
+ Requires-Dist: bottle>=0.12
28
+ Requires-Dist: btrfs-timeline>=0.8.4
29
+ Requires-Dist: rich>=13
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=7.0; extra == 'dev'
32
+ Requires-Dist: webtest>=3.0; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # tag-keeper
36
+
37
+ Tag, search and tidy your existing file trees from the outside — without moving a file.
38
+
39
+ [日本語版 README はこちら](README.ja.md)
40
+
41
+ > **Status: pre-alpha.** What works today: the inventory (scanning, content hashing, a
42
+ > hygiene report), cleaning up with plans you approve (with undo), a file browser with tags
43
+ > and version history from btrfs snapshots, and name and folder suggestions for your inbox
44
+ > folders from a local model. Search and the rest of tagging come next. See [Roadmap](#roadmap).
45
+
46
+ ## Why
47
+
48
+ Most document managers take ownership of your files: they ingest them into their own
49
+ storage and lay them out flat. That is the opposite of how a cloud drive is actually
50
+ used. Its folders already mean something — a numbered project folder, a source code
51
+ repository with its own layout, a year/month archive — and that meaning lives in each
52
+ folder, not in one global category tree.
53
+
54
+ tag-keeper leaves the tree alone. It keeps its own catalog next to the tree, and from
55
+ there it helps you keep the tree clean:
56
+
57
+ - **It never takes ownership.** No copying, renaming or converting. You keep using your
58
+ file manager and shell as before.
59
+ - **It follows what you do outside it.** Files you move or rename keep their records.
60
+ - **Changes are proposals.** Anything that would move a file is written down as a plan,
61
+ shown first, and only runs once you approve it. Nothing is ever deleted: files go to a
62
+ quarantine folder, and every plan can be undone.
63
+
64
+ ## What it does today
65
+
66
+ ```console
67
+ $ tag-keeper scan ~/CloudSync/OneDrive # read-only walk; updates the catalog
68
+ $ tag-keeper hash ~/CloudSync/OneDrive # SHA-256 of files that changed since last time
69
+ $ tag-keeper report ~/CloudSync/OneDrive # what is probably not worth keeping in the cloud
70
+ ```
71
+
72
+ - **scan** records path, size, mtime and inode of every file and folder. Renames and
73
+ moves are tracked by inode, so a moved file keeps its row (and its hash).
74
+ - **Safety valve:** if a large share of files vanishes at once — an unmounted drive, a
75
+ sync client accident — the deletions are *held*, not recorded. They are recorded only
76
+ if they are still missing on the next scan (or with `--accept-missing`).
77
+ - A root that is missing or empty aborts the scan instead of looking like "everything
78
+ was deleted". Folders that cannot be read are not treated as gone either.
79
+ - **hash** computes SHA-256 only for files that are new or changed, with a progress bar
80
+ and ETA. A file that changes while being hashed is skipped.
81
+ - **report** lists candidates by category, with sizes and examples:
82
+ - application data that slipped into a synced folder (game data, caches) — reported
83
+ once per folder, at the deepest folder that matches, so it never swallows your own
84
+ documents next to it
85
+ - rebuildable artifacts (`node_modules`, `__pycache__`, …), VM disk images, installers
86
+ and disk images, temporary files and Office lock files
87
+ - sync conflict copies (OneDrive for Linux `-safeBackup-`, pCloud `[conflicted]`,
88
+ rclone bisync `.conflict1`, device-name suffixes, `(1)` copies) — only when the
89
+ original sits next to it, since a lone "copy" may be the only version. Copies whose
90
+ content differs from the original are marked as needing review
91
+ - empty files and folders
92
+ - exact duplicates, confirmed by hash (never guessed from size)
93
+
94
+ The catalog is an SQLite file outside the tree (`~/.local/share/tag-keeper/catalog.db` by
95
+ default). None of these commands writes into the tree.
96
+
97
+ ### Cleaning up with a plan
98
+
99
+ ```console
100
+ $ tag-keeper plan onedrive # write the report's candidates to a plan file
101
+ $ $EDITOR ~/.local/share/tag-keeper/plans/<plan-id>.toml # delete the lines you want to keep
102
+ $ tag-keeper apply <plan file> # preview only
103
+ $ tag-keeper apply <plan file> --yes # move them to the quarantine folder
104
+ $ tag-keeper undo <plan-id> --yes # put everything back
105
+ ```
106
+
107
+ - **plan** writes one line per item, grouped by category. `--category` limits it to some
108
+ categories (`app-data`, `vm-image`, `conflict`, …). Candidates that need review stay out
109
+ unless you pass `--include-review`.
110
+ - **apply** previews by default. With `--yes` it moves each item, by rename, to
111
+ `<quarantine_dir>/<plan-id>/` under its original relative path. Right before each move
112
+ it checks that the item has not changed since the plan was written (inode, size,
113
+ modification time, and for folders the file count and total size); changed items are
114
+ skipped and reported.
115
+ - With `snapper_config` set, a snapper *pre* snapshot is taken before the run and a *post*
116
+ snapshot after it. If the pre snapshot fails, nothing is moved.
117
+ - Every move is appended to a journal (`~/.local/share/tag-keeper/journal/<plan-id>.jsonl`).
118
+ **undo** reads the journal and moves items back; it never overwrites something that has
119
+ appeared at the original path since.
120
+ - The catalog is updated as items move, so the next scan does not mistake a large
121
+ quarantine for a mass disappearance.
122
+ - Items leave the cloud when the sync client notices they are gone, but they stay on this
123
+ disk until you empty the quarantine folder yourself.
124
+
125
+ ### Big deletions without stopping your sync client
126
+
127
+ The [OneDrive client for Linux](https://github.com/abraunegg/onedrive) treats the local
128
+ removal of a folder with many children (`classify_as_big_delete`, 1000 by default) as an
129
+ accident: it refuses to sync and exits. Run under systemd, it keeps exiting on every
130
+ restart, so syncing silently stops. When *you* approve a big deletion, tag-keeper
131
+ coordinates with the client instead (set `sync_client = "onedrive"` on the root):
132
+
133
+ 1. stop the resident sync (`systemctl --user stop onedrive.service`),
134
+ 2. quarantine the items,
135
+ 3. run one `onedrive --sync` with the threshold raised just above the largest folder in this
136
+ plan — not `--force`, which would also let unrelated accidents through,
137
+ 4. start the resident sync again.
138
+
139
+ If step 3 fails, the resident sync is left stopped (restarting it would only exit again),
140
+ the failure is journaled, and the web UI and `tag-keeper sync <plan-id> --retry` offer to
141
+ try again. Small plans are left to the running client as usual.
142
+
143
+ ## Web UI
144
+
145
+ ```console
146
+ $ tag-keeper serve # http://127.0.0.1:8090/
147
+ ```
148
+
149
+ Everything above can be done in the browser: refresh a root (scan and hash), read the
150
+ report, create a plan, untick what you want to keep, check it, run it and undo it. Long
151
+ operations run in the background, and a panel at the bottom shows each step, a progress
152
+ bar, speed, time remaining and the latest log lines. It works on a phone too.
153
+
154
+ - Only loopback is served by default. To listen elsewhere (for example on a Tailscale
155
+ address), give credentials for HTTP Basic auth with `--auth-file` (one line,
156
+ `user:password`) or `TAG_KEEPER_AUTH`; without them a non-loopback address is refused
157
+ unless you pass `--allow-no-auth`. Basic auth is not encrypted: use it over a VPN such as
158
+ Tailscale, or behind TLS.
159
+ - Writes require a same-origin request, and without auth the `Host` header must name the
160
+ bound address (against DNS rebinding).
161
+ - One job runs at a time. Each job also writes its own log under
162
+ `~/.local/share/tag-keeper/logs/jobs/`.
163
+
164
+ ### Files, tags and history
165
+
166
+ - **Browse** a root folder by folder, preview PDFs, images, audio, video and text in the
167
+ page, and download anything. Only types that cannot run scripts open in the page; HTML
168
+ and SVG are shown as plain text, everything else downloads. Paths never leave the root,
169
+ symlinks included.
170
+ - **Tag** files and folders from the browser. A folder's tags are inherited by everything
171
+ under it. Tags live in append-only JSONL logs, one file per machine
172
+ (`~/.local/share/tag-keeper/tags/<host>.jsonl`), so they stay readable without the tool
173
+ and never conflict when synced. When a tagged item is moved — by you or by a plan — the
174
+ log records the move and the tags follow.
175
+ - **Version history** comes from btrfs snapshots through
176
+ [btrfs-timeline](https://github.com/akivajp/btrfs-timeline): the versions of a file, a
177
+ folder as it was at any snapshot (including what has since been deleted), and restoring
178
+ an old version next to the current file under a dated name (never overwriting).
179
+ tag-keeper records renames and moves it sees, so a file's history continues across them.
180
+
181
+ ### Inbox suggestions
182
+
183
+ Folders whose name matches `inbox_patterns` (by default `tmp`, `temp`, `*未整理*`,
184
+ `*inbox*`) are treated as inboxes. The **整理** page lists every file in them flat, without
185
+ the folder hierarchy. Pick a file and a model served by [ollama](https://ollama.com/) reads
186
+ it — text from PDFs and Office files, the image itself for photos and scanned PDFs — and
187
+ suggests, usually within seconds:
188
+
189
+ - a name, by default `YYYYMMDD_title`, following past decisions and the date style of the
190
+ destination folder, and keeping a meaningful existing name;
191
+ - up to three destination folders among the folders you already have: candidates are
192
+ narrowed by character-bigram TF-IDF over each folder's path and file names, then the model
193
+ picks. Year and month folders are moved to the document's date;
194
+ - tags, preferring the ones you already use; new ones are marked as such.
195
+
196
+ Clicked files jump the queue and the next two are read ahead. Accepted suggestions go to an
197
+ accept list, which you run as one plan of renames and moves — with snapshots and undo — or
198
+ save to review on the plan page. Files outside the inboxes get a suggestion only when you ask
199
+ for one from the file browser.
200
+
201
+ **Learning from your decisions.** What you accept (suggested versus final name, folder and
202
+ tags), the suggested tags you add or reject, and plans you undo are appended to a log, one
203
+ JSONL file per machine (`~/.local/share/tag-keeper/decisions/`). Files you move out of an
204
+ inbox yourself count too. When suggesting, the closest past decisions are added to the prompt
205
+ as examples and their folders join the candidates; tags you keep rejecting are avoided. The
206
+ log pairs each suggestion with your decision, so it can be exported as training data.
207
+
208
+ **Models.** The default is a local model. Any ollama model can be chosen on the page or in
209
+ the config, including ollama's cloud models (`…:cloud`, `…-cloud`) — then the file's content
210
+ is sent to ollama's cloud; the page marks those models with ☁. Suggestions are stored by
211
+ content hash and model, so a file is read once per model.
212
+
213
+ ## Install
214
+
215
+ Requires Python 3.11+ and Linux. The web UI and messages are in Japanese.
216
+
217
+ ```console
218
+ $ pipx install tag-keeper # or: uv tool install tag-keeper
219
+ $ tag-keeper --help
220
+ ```
221
+
222
+ Optional tools, each used only by the feature that needs it:
223
+
224
+ | Tool | Used for |
225
+ |---|---|
226
+ | poppler-utils (`pdftotext`, `pdftoppm`) | reading PDFs for inbox suggestions |
227
+ | [ollama](https://ollama.com/) | inbox suggestions |
228
+ | btrfs and snapper | version history, and snapshots around plans |
229
+ | OneDrive client for Linux | coordinating big deletions with the sync |
230
+
231
+ From source:
232
+
233
+ ```console
234
+ $ git clone https://github.com/akivajp/tag-keeper.git
235
+ $ cd tag-keeper
236
+ $ uv run tag-keeper --help
237
+ $ uv run --extra dev pytest
238
+ ```
239
+
240
+ ## Configuration
241
+
242
+ Optional. `~/.config/tag-keeper/config.toml`:
243
+
244
+ ```toml
245
+ [[roots]]
246
+ name = "onedrive"
247
+ path = "~/CloudSync/OneDrive"
248
+ exclude = ["*.partial"] # never scanned (glob on the path relative to the root)
249
+
250
+ [hygiene]
251
+ allow = ["archive/**"] # never reported ("I decided to keep these")
252
+ device_names = ["zefat", "hermon"] # suffixes your sync client adds to conflict copies
253
+ app_data_names = ["MyLauncher"] # extra folder names to treat as application data
254
+
255
+ [safety]
256
+ mass_missing_ratio = 0.2 # hold deletions when more than this share vanishes
257
+ mass_missing_min = 100
258
+
259
+ [plan]
260
+ quarantine_dir = "~/.local/share/tag-keeper/quarantine" # must be on the root's filesystem
261
+ snapper_config = "home" # snapper config for pre/post snapshots ("" = none)
262
+ ```
263
+
264
+ Per root, for coordinating with a sync client:
265
+
266
+ ```toml
267
+ [[roots]]
268
+ name = "onedrive"
269
+ path = "~/CloudSync/OneDrive"
270
+ sync_client = "onedrive" # "" (default) = no coordination
271
+ sync_service = "onedrive.service" # the systemd --user unit running `onedrive --monitor`
272
+ sync_guard = "auto" # "auto": only for big deletions / "always": every run
273
+ ```
274
+
275
+ Tags and inbox suggestions:
276
+
277
+ ```toml
278
+ [tags]
279
+ dir = "~/.local/share/tag-keeper/tags" # where the per-machine tag logs live
280
+
281
+ [organize]
282
+ inbox_patterns = ["tmp", "temp", "*未整理*", "*inbox*"] # case-insensitive globs on folder names
283
+ ollama_url = "http://127.0.0.1:11434"
284
+ model = "gemma3:12b" # e.g. "gemma4:31b-cloud" (content is sent to ollama's cloud)
285
+ vision_model = "" # model for images when `model` cannot read them
286
+ model_choices = ["gemma4:31b-cloud"] # extra models offered on the page
287
+ think = "" # "true" / "false" for thinking models; "" = model default
288
+ use_images = true
289
+ max_chars = 4000 # text passed to the model
290
+ candidates = 15 # destination folders the model chooses from
291
+ ```
292
+
293
+ With roots configured, `tag-keeper scan` (no arguments) scans all of them.
294
+ Every command accepts `--log-file PATH` to keep a timestamped log.
295
+
296
+ ## Roadmap
297
+
298
+ 1. **Inventory and hygiene report**, and tidy-up plans that quarantine what you approve,
299
+ with snapper snapshots and undo. *(done)*
300
+ 2. **Tags and search** — *partly done:* tags inherited from folders in append-only logs, a
301
+ file browser, version history across moves. *Next:* aliases and parents, typed fields,
302
+ search by tag, name and content.
303
+ 3. **Content extraction** — *partly done:* text from PDF and Office files, images through a
304
+ local model, for inbox suggestions. *Next:* OCR and transcripts for full-text search.
305
+ 4. **Placement and structure** — *partly done:* name and folder suggestions for inbox files.
306
+ *Next:* directory cards, structure review.
307
+
308
+ ## License
309
+
310
+ MIT
@@ -0,0 +1,243 @@
1
+ # tag-keeper
2
+
3
+ 既存のフォルダ構造を動かさずに、外側からタグ付け・検索・整理を行うツールです。
4
+
5
+ [English README](README.md)
6
+
7
+ > **状況: プレアルファ版。** 今動くのは、棚卸し(走査・内容ハッシュ・衛生レポート)、
8
+ > 承認した整理プランによる片付け(取り消し可能)、タグと btrfs スナップショットの版の履歴つきのファイルブラウザ、
9
+ > 手元のモデルによる受け皿のファイルの名前と移動先の提案です。検索とタグの残りはこれから作ります。
10
+ > [ロードマップ](#ロードマップ) を参照してください。
11
+
12
+ ## なぜ作るのか
13
+
14
+ 多くの文書管理ソフトは、ファイルを取り込んで自分の保管場所に一律に並べ直します。
15
+ これは、クラウドドライブの実際の使い方と正反対です。フォルダには既に意味があります
16
+ (番号付きの案件フォルダ、独自の構成を持つソースコードのリポジトリ、年月で分けた保管庫など)。
17
+ その意味はフォルダごとに決まっていて、全体共通のカテゴリでは表せません。
18
+
19
+ tag-keeper はツリーに手を付けません。ツリーの外に自分の台帳(カタログ)を持ち、そこからツリーをきれいに保つ手助けをします。
20
+
21
+ - **ファイルを取り込まない。** コピー・改名・変換をしません。ファイラーやシェルはこれまでどおり使えます。
22
+ - **ツールの外での操作に追従する。** 移動・改名したファイルも、記録を引き継ぎます。
23
+ - **変更は提案として出す。** ファイルを動かす操作はプランとして書き出し、先に示して、承認されてから実行します。
24
+ 削除はしません。隔離フォルダへ移すだけで、プランごとに取り消せます。
25
+
26
+ ## 今できること
27
+
28
+ ```console
29
+ $ tag-keeper scan ~/CloudSync/OneDrive # 読み取り専用で走査し、カタログを更新する
30
+ $ tag-keeper hash ~/CloudSync/OneDrive # 前回から変わったファイルだけ SHA-256 を計算する
31
+ $ tag-keeper report ~/CloudSync/OneDrive # クラウドに置く価値が薄そうなものを報告する
32
+ ```
33
+
34
+ - **scan** は、すべてのファイルとフォルダのパス・サイズ・更新日時・inode を記録します。
35
+ 移動・改名は inode で追跡するので、動かしたファイルも行(とハッシュ)を引き継ぎます。
36
+ - **安全弁:** 一度に多くのファイルが消えたように見えたら(ドライブのマウントが外れた、同期クライアントの事故など)、
37
+ 削除として記録せずに**保留**します。次の走査でも見えなければ記録します(`--accept-missing` で即時に記録)。
38
+ - ルートが存在しないか空なら、「全部消えた」と扱わずに走査を止めます。読めなかったフォルダの中身も、消えたとは扱いません。
39
+ - **hash** は、新しいファイルと変わったファイルだけ SHA-256 を計算します(進捗バーと残り時間を表示)。
40
+ 計算の途中で変わったファイルは飛ばします。
41
+ - **report** は、候補をカテゴリごとに、容量と例を添えて一覧します。
42
+ - 同期フォルダに入り込んだアプリのデータ(ゲームのデータ、キャッシュ)。フォルダ単位で1件として報告し、
43
+ 条件に合う最も深いフォルダを選ぶので、隣にある利用者の書類まで巻き込みません
44
+ - 作り直せる生成物(`node_modules`、`__pycache__` など)、仮想マシンのディスク、インストーラーとディスクイメージ、
45
+ 一時ファイル、Office のロックファイル
46
+ - 同期の競合コピー(Linux 版 onedrive クライアントの `-safeBackup-`、pCloud の `[conflicted]`、
47
+ rclone bisync の `.conflict1`、端末名の付いたもの、`(1)` 付きの複製)。同じフォルダに元のファイルがあるものだけです
48
+ (元が無ければ、それが唯一の版かもしれないため)。元と内容が異なるものには「中身の確認が必要」と付けます
49
+ - 空のファイルとフォルダ
50
+ - 内容が同じファイル(ハッシュで確定したものだけ。サイズからの推測では報告しません)
51
+
52
+ カタログはツリーの外の SQLite ファイルです(既定は `~/.local/share/tag-keeper/catalog.db`)。
53
+ ここまでのコマンドは、ツリーに何も書き込みません。
54
+
55
+ ### 整理プランで片付ける
56
+
57
+ ```console
58
+ $ tag-keeper plan onedrive # 報告の候補をプランのファイルに書き出す
59
+ $ $EDITOR ~/.local/share/tag-keeper/plans/<プラン ID>.toml # 残したいものの行を消す
60
+ $ tag-keeper apply <プランのファイル> # 確認だけ
61
+ $ tag-keeper apply <プランのファイル> --yes # 隔離フォルダへ移す
62
+ $ tag-keeper undo <プラン ID> --yes # すべて元に戻す
63
+ ```
64
+
65
+ - **plan** は、1件を1行にして、カテゴリごとにまとめて書き出します。`--category` でカテゴリを絞れます
66
+ (`app-data`、`vm-image`、`conflict` など)。中身の確認が要る候補は、`--include-review` を付けたときだけ含めます。
67
+ - **apply** は、既定では確認だけです。`--yes` を付けると、各対象を `<quarantine_dir>/<プラン ID>/` の下へ、
68
+ 元の相対パスのまま改名で移します。移す直前に、プランの作成時から変わっていないか
69
+ (inode・サイズ・更新日時、フォルダなら配下のファイル数と合計サイズも)を確かめ、変わっていれば飛ばして報告します。
70
+ - `snapper_config` を設定すると、実行の前に snapper の pre スナップショット、後に post スナップショットを撮ります。
71
+ pre を撮れなければ、何も動かしません。
72
+ - 移した操作は、すべて実行記録(`~/.local/share/tag-keeper/journal/<プラン ID>.jsonl`)に追記します。
73
+ **undo** はこの記録をもとに元へ戻します。元の場所に新しく別のものができていれば、上書きせずに飛ばします。
74
+ - 移すたびにカタログも更新するので、大量に隔離しても、次の走査で大量消失と誤認しません。
75
+ - 同期クライアントが消えたことに気付けば、クラウドからは消えます。ただし、隔離フォルダを自分で空にするまでは、このディスクに残ります。
76
+
77
+ ### 大量の削除で同期を止めない
78
+
79
+ [Linux 版 onedrive クライアント](https://github.com/abraunegg/onedrive)は、子の多いフォルダ
80
+ (`classify_as_big_delete`、既定 1000 件)がローカルで消えると事故とみなし、同期をやめて終了します。
81
+ systemd で常駐させていると再起動のたびに同じ理由で終了するので、気付かないうちに同期が止まります。
82
+ 利用者が承認した大量の削除では、tag-keeper が同期クライアントと連携します(ルートに `sync_client = "onedrive"` を設定)。
83
+
84
+ 1. 常駐の同期を止める(`systemctl --user stop onedrive.service`)
85
+ 2. 隔離する
86
+ 3. `onedrive --sync` を1回だけ、しきい値を「このプランで一番大きいフォルダ」より少し上に上げて実行し、削除をクラウドに反映する。
87
+ `--force` は使いません(関係のない事故まで通してしまうため)
88
+ 4. 常駐の同期を再開する
89
+
90
+ 3 が失敗したら、常駐の同期は止めたままにします(再開しても同じ理由で終了を繰り返すため)。
91
+ 失敗は実行記録に残り、Web の画面か `tag-keeper sync <プラン ID> --retry` から再試行できます。
92
+ 小さなプランでは連携せず、動いている同期クライアントにそのまま任せます。
93
+
94
+ ## Web の画面
95
+
96
+ ```console
97
+ $ tag-keeper serve # http://127.0.0.1:8090/
98
+ ```
99
+
100
+ ここまでの操作は、すべてブラウザからできます。ルートを最新にする(走査とハッシュ計算)、レポートを見る、
101
+ プランを作る、残したいもののチェックを外す、確認する、実行する、取り消す。
102
+ 時間のかかる処理は裏で動き、画面の下に、段階・進捗バー・速さ・残り時間・直近のログを表示します。スマートフォンでも使えます。
103
+
104
+ - 既定ではループバックだけで待ち受けます。他のアドレス(Tailscale のアドレスなど)で待ち受けるには、
105
+ BASIC 認証の資格情報を `--auth-file`(`user:password` の1行)か `TAG_KEEPER_AUTH` で渡してください。
106
+ 渡さなければ、`--allow-no-auth` を付けない限り起動しません。BASIC 認証は暗号化しないので、
107
+ Tailscale などの VPN 越しか、TLS の後ろで使ってください。
108
+ - 書き込みは同一オリジンからだけ受け付けます。認証なしのときは、Host ヘッダが待ち受けているアドレスであることも確かめます(DNS リバインディング対策)。
109
+ - 同時に動かす処理は1つだけです。処理ごとのログは `~/.local/share/tag-keeper/logs/jobs/` にも残ります。
110
+
111
+ ### ファイル・タグ・版の履歴
112
+
113
+ - **ファイルブラウザ**: ルートの中をフォルダごとにたどり、PDF・画像・音声・動画・テキストを画面の中でプレビューし、何でもダウンロードできます。
114
+ 画面の中で開くのはスクリプトを実行しない種類だけで、HTML や SVG はテキストとして見せ、それ以外はダウンロードにします。
115
+ シンボリックリンクを含め、ルートの外には出ません。
116
+ - **タグ**: ファイルブラウザから、ファイルやフォルダにタグを付け外しできます。フォルダのタグは配下に継承されます。
117
+ タグは端末ごとの追記専用の JSONL(`~/.local/share/tag-keeper/tags/<ホスト名>.jsonl`)に記録するので、
118
+ ツールが無くても読め、同期しても競合しません。タグの付いたものが(手作業でもプランでも)動いたら、移動を記録してタグを追従させます。
119
+ - **版の履歴**: [btrfs-timeline](https://github.com/akivajp/btrfs-timeline) を使い、btrfs のスナップショットから、
120
+ ファイルの版の一覧、過去のある時点のフォルダの中身(今は消えたものを含む)を見られます。
121
+ 過去の版は、今のファイルの隣に日時付きの別名で復元します(上書きしません)。
122
+ tag-keeper が見た移動・改名を記録しているので、移動・改名の前の版もたどれます。
123
+
124
+ ### 受け皿の整理の提案
125
+
126
+ 名前が `inbox_patterns`(既定は `tmp`・`temp`・`*未整理*`・`*inbox*`)に一致するフォルダを受け皿とみなします。
127
+ 「整理」の画面に、受け皿の中のファイルをフォルダの階層なしで並べます。ファイルを選ぶと、
128
+ [ollama](https://ollama.com/) のモデルが内容を読み(PDF や Office 文書は文字、写真やスキャンの PDF は画像そのもの)、
129
+ 多くは数秒で次を提案します。
130
+
131
+ - 名前: 既定は `YYYYMMDD_題名`。過去の判断や、移動先のフォルダの日付の付け方に合わせ、意味のある今の名前は尊重します
132
+ - 移動先: 既存のフォルダから最大3つ。各フォルダのパスと中のファイル名の文字の2文字組(TF-IDF)で候補を絞ってから、モデルが選びます。
133
+ 年・月のフォルダは、書類の日付の年・月に置き換えます
134
+ - タグ: 既に使っているタグを優先します。新しいタグには「新規」と付けます
135
+
136
+ 選んだファイルは待ち行列の先頭に割り込み、次の2件は先読みします。採用した提案は採用リストにたまり、
137
+ 1つの「移動・改名」のプランとして実行するか(スナップショットと取り消しつき)、プランとして保存して確かめてから実行します。
138
+ 受け皿の外のファイルは、ファイルブラウザで頼んだときだけ提案を作ります。
139
+
140
+ **判断から学ぶ仕組み**: 採用したもの(提案と最終的な名前・移動先・タグ)、提案されたタグを付けた・却下したこと、取り消したプランを、
141
+ 端末ごとの JSONL(`~/.local/share/tag-keeper/decisions/`)に追記します。受け皿から手作業で移したものも例に使います。
142
+ 提案を作るときは、似た過去の判断を例として指示文に添え、その移動先を候補に入れます。何度も却下したタグは使わせません。
143
+ ログは提案と判断が対になっているので、そのまま学習データとして書き出せます。
144
+
145
+ **モデル**: 既定は手元のモデルです。画面や設定で ollama のどのモデルも選べ、ollama のクラウドのモデル(`…:cloud`・`…-cloud`)も使えます。
146
+ その場合、ファイルの内容は ollama のクラウドに送られます。画面ではクラウドのモデルに ☁ を付けます。
147
+ 提案は内容ハッシュとモデルごとに保存するので、同じファイルを同じモデルで読むのは1回だけです。
148
+
149
+ ## インストール
150
+
151
+ Python 3.11 以上と Linux が必要です。画面とメッセージは日本語です。
152
+
153
+ ```console
154
+ $ pipx install tag-keeper # または: uv tool install tag-keeper
155
+ $ tag-keeper --help
156
+ ```
157
+
158
+ 次の道具は任意で、それぞれ必要な機能でだけ使います。
159
+
160
+ | 道具 | 使う機能 |
161
+ |---|---|
162
+ | poppler-utils(`pdftotext`・`pdftoppm`) | 受け皿の整理の提案での PDF の読み取り |
163
+ | [ollama](https://ollama.com/) | 受け皿の整理の提案 |
164
+ | btrfs と snapper | 版の履歴、プランの実行前後のスナップショット |
165
+ | Linux 版 onedrive クライアント | 大量の削除での同期との連携 |
166
+
167
+ ソースから使う場合:
168
+
169
+ ```console
170
+ $ git clone https://github.com/akivajp/tag-keeper.git
171
+ $ cd tag-keeper
172
+ $ uv run tag-keeper --help
173
+ $ uv run --extra dev pytest
174
+ ```
175
+
176
+ ## 設定
177
+
178
+ 任意です。`~/.config/tag-keeper/config.toml`:
179
+
180
+ ```toml
181
+ [[roots]]
182
+ name = "onedrive"
183
+ path = "~/CloudSync/OneDrive"
184
+ exclude = ["*.partial"] # 走査しない(ルートからの相対パスに対する glob)
185
+
186
+ [hygiene]
187
+ allow = ["archive/**"] # 報告しない(「残す」と判断したもの)
188
+ device_names = ["zefat", "hermon"] # 同期クライアントが競合コピーに付ける端末名
189
+ app_data_names = ["MyLauncher"] # 既定に加えて、アプリのデータとみなすフォルダ名
190
+
191
+ [safety]
192
+ mass_missing_ratio = 0.2 # これを超える割合が消えたら、削除の記録を保留する
193
+ mass_missing_min = 100
194
+
195
+ [plan]
196
+ quarantine_dir = "~/.local/share/tag-keeper/quarantine" # ルートと同じファイルシステムに置く
197
+ snapper_config = "home" # 前後のスナップショットを撮る snapper の設定名("" で撮らない)
198
+ ```
199
+
200
+ 同期クライアントとの連携は、ルートごとに設定します。
201
+
202
+ ```toml
203
+ [[roots]]
204
+ name = "onedrive"
205
+ path = "~/CloudSync/OneDrive"
206
+ sync_client = "onedrive" # ""(既定)なら連携しない
207
+ sync_service = "onedrive.service" # `onedrive --monitor` を動かしている systemd --user のユニット
208
+ sync_guard = "auto" # "auto": 大量削除のときだけ / "always": 実行のたびに
209
+ ```
210
+
211
+ タグと受け皿の整理の提案:
212
+
213
+ ```toml
214
+ [tags]
215
+ dir = "~/.local/share/tag-keeper/tags" # 端末ごとのタグのログの置き場所
216
+
217
+ [organize]
218
+ inbox_patterns = ["tmp", "temp", "*未整理*", "*inbox*"] # フォルダ名に照合する(大文字・小文字を区別しない)
219
+ ollama_url = "http://127.0.0.1:11434"
220
+ model = "gemma3:12b" # 例: "gemma4:31b-cloud"(内容が ollama のクラウドに送られる)
221
+ vision_model = "" # model が画像を読めないときに、画像を読むモデル
222
+ model_choices = ["gemma4:31b-cloud"] # 画面で選べるモデルの候補
223
+ think = "" # 思考に対応したモデルで "true" / "false"。空ならモデルに任せる
224
+ use_images = true
225
+ max_chars = 4000 # モデルに渡す本文の上限
226
+ candidates = 15 # モデルに選ばせる移動先の候補の数
227
+ ```
228
+
229
+ ルートを設定しておけば、`tag-keeper scan`(引数なし)で全ルートを走査します。
230
+ どのコマンドも `--log-file PATH` で日時付きのログを残せます。
231
+
232
+ ## ロードマップ
233
+
234
+ 1. **棚卸しと衛生レポート**、承認したものを隔離する整理プラン(snapper のスナップショットと取り消しつき)(済み)
235
+ 2. **タグと検索** — 済み: フォルダから継承するタグ(追記専用ログ)、ファイルブラウザ、移動をまたぐ版の履歴。
236
+ これから: タグの別名・親子、型付きの属性、タグ・名前・内容による検索
237
+ 3. **内容の抽出** — 済み: 受け皿の提案のための PDF・Office 文書の文字と、手元のモデルによる画像の読み取り。
238
+ これから: 全文検索のための OCR と書き起こし
239
+ 4. **配置と構造** — 済み: 受け皿のファイルの名前と移動先の提案。これから: ディレクトリカード、構造の見直し
240
+
241
+ ## ライセンス
242
+
243
+ MIT