aichivist 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 (84) hide show
  1. aichivist-0.1.0/LICENSE +21 -0
  2. aichivist-0.1.0/PKG-INFO +143 -0
  3. aichivist-0.1.0/README.md +121 -0
  4. aichivist-0.1.0/aichivist/__init__.py +3 -0
  5. aichivist-0.1.0/aichivist/__main__.py +5 -0
  6. aichivist-0.1.0/aichivist/agent_tools.py +184 -0
  7. aichivist-0.1.0/aichivist/apps.py +243 -0
  8. aichivist-0.1.0/aichivist/bagit.py +103 -0
  9. aichivist-0.1.0/aichivist/catalog.py +136 -0
  10. aichivist-0.1.0/aichivist/catalog_page.py +129 -0
  11. aichivist-0.1.0/aichivist/checksums.py +45 -0
  12. aichivist-0.1.0/aichivist/cli.py +230 -0
  13. aichivist-0.1.0/aichivist/commands.py +306 -0
  14. aichivist-0.1.0/aichivist/config.py +78 -0
  15. aichivist-0.1.0/aichivist/describe.py +161 -0
  16. aichivist-0.1.0/aichivist/drives.py +83 -0
  17. aichivist-0.1.0/aichivist/duplicates.py +66 -0
  18. aichivist-0.1.0/aichivist/fileops.py +188 -0
  19. aichivist-0.1.0/aichivist/fulltext.py +183 -0
  20. aichivist-0.1.0/aichivist/guard.py +102 -0
  21. aichivist-0.1.0/aichivist/i18n.py +51 -0
  22. aichivist-0.1.0/aichivist/inventory.py +110 -0
  23. aichivist-0.1.0/aichivist/journal.py +39 -0
  24. aichivist-0.1.0/aichivist/lang_de.py +288 -0
  25. aichivist-0.1.0/aichivist/lifecycle.py +85 -0
  26. aichivist-0.1.0/aichivist/markers.py +24 -0
  27. aichivist-0.1.0/aichivist/mcp_server.py +97 -0
  28. aichivist-0.1.0/aichivist/notify.py +60 -0
  29. aichivist-0.1.0/aichivist/ocr.py +48 -0
  30. aichivist-0.1.0/aichivist/ocr_mac.js +30 -0
  31. aichivist-0.1.0/aichivist/ocr_windows.ps1 +38 -0
  32. aichivist-0.1.0/aichivist/photos.py +184 -0
  33. aichivist-0.1.0/aichivist/plan.py +188 -0
  34. aichivist-0.1.0/aichivist/protect.py +36 -0
  35. aichivist-0.1.0/aichivist/retention.py +53 -0
  36. aichivist-0.1.0/aichivist/review.py +121 -0
  37. aichivist-0.1.0/aichivist/rules.py +70 -0
  38. aichivist-0.1.0/aichivist/schedule.py +123 -0
  39. aichivist-0.1.0/aichivist/similar.py +49 -0
  40. aichivist-0.1.0/aichivist/start.py +135 -0
  41. aichivist-0.1.0/aichivist/storage.py +153 -0
  42. aichivist-0.1.0/aichivist/structure.py +134 -0
  43. aichivist-0.1.0/aichivist/style.py +29 -0
  44. aichivist-0.1.0/aichivist/survey.py +242 -0
  45. aichivist-0.1.0/aichivist/undo.py +57 -0
  46. aichivist-0.1.0/aichivist/update.py +69 -0
  47. aichivist-0.1.0/aichivist.egg-info/PKG-INFO +143 -0
  48. aichivist-0.1.0/aichivist.egg-info/SOURCES.txt +82 -0
  49. aichivist-0.1.0/aichivist.egg-info/dependency_links.txt +1 -0
  50. aichivist-0.1.0/aichivist.egg-info/entry_points.txt +2 -0
  51. aichivist-0.1.0/aichivist.egg-info/requires.txt +8 -0
  52. aichivist-0.1.0/aichivist.egg-info/top_level.txt +1 -0
  53. aichivist-0.1.0/pyproject.toml +54 -0
  54. aichivist-0.1.0/setup.cfg +4 -0
  55. aichivist-0.1.0/tests/test_agent_tools.py +136 -0
  56. aichivist-0.1.0/tests/test_apps.py +145 -0
  57. aichivist-0.1.0/tests/test_bagit.py +62 -0
  58. aichivist-0.1.0/tests/test_catalog.py +83 -0
  59. aichivist-0.1.0/tests/test_catalog_page.py +38 -0
  60. aichivist-0.1.0/tests/test_checksums.py +31 -0
  61. aichivist-0.1.0/tests/test_cli.py +190 -0
  62. aichivist-0.1.0/tests/test_commands.py +137 -0
  63. aichivist-0.1.0/tests/test_config.py +50 -0
  64. aichivist-0.1.0/tests/test_duplicates.py +53 -0
  65. aichivist-0.1.0/tests/test_extension.py +37 -0
  66. aichivist-0.1.0/tests/test_field_fixes.py +242 -0
  67. aichivist-0.1.0/tests/test_fileops.py +126 -0
  68. aichivist-0.1.0/tests/test_fulltext.py +128 -0
  69. aichivist-0.1.0/tests/test_guard.py +72 -0
  70. aichivist-0.1.0/tests/test_i18n.py +72 -0
  71. aichivist-0.1.0/tests/test_inventory_undo.py +90 -0
  72. aichivist-0.1.0/tests/test_lifecycle.py +89 -0
  73. aichivist-0.1.0/tests/test_photos.py +72 -0
  74. aichivist-0.1.0/tests/test_plan.py +106 -0
  75. aichivist-0.1.0/tests/test_retention_notify_review.py +90 -0
  76. aichivist-0.1.0/tests/test_review_fixes.py +205 -0
  77. aichivist-0.1.0/tests/test_rules_learning_ocr.py +120 -0
  78. aichivist-0.1.0/tests/test_schedule.py +61 -0
  79. aichivist-0.1.0/tests/test_similar_describe_style.py +116 -0
  80. aichivist-0.1.0/tests/test_start.py +77 -0
  81. aichivist-0.1.0/tests/test_storage.py +50 -0
  82. aichivist-0.1.0/tests/test_structure.py +69 -0
  83. aichivist-0.1.0/tests/test_survey.py +133 -0
  84. aichivist-0.1.0/tests/test_update.py +42 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Philipp Luft
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,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: aichivist
3
+ Version: 0.1.0
4
+ Summary: Clean up your files like an archivist: AI agents rename, sort and de-duplicate documents with plans you approve and full undo
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/luftphilipp/aichivist
7
+ Project-URL: Changelog, https://github.com/luftphilipp/aichivist/blob/main/CHANGELOG.md
8
+ Keywords: archive,records-management,file-organizer,bagit,fixity,claude-code,ai-agent
9
+ Classifier: Operating System :: MacOS
10
+ Classifier: Operating System :: Microsoft :: Windows
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: System :: Archiving
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Provides-Extra: pdf
18
+ Requires-Dist: pypdf>=4; extra == "pdf"
19
+ Provides-Extra: mcp
20
+ Requires-Dist: mcp>=1.10; python_version >= "3.10" and extra == "mcp"
21
+ Dynamic: license-file
22
+
23
+ <div align="center">
24
+
25
+ <img src="extension/icon.png" width="96" alt="">
26
+
27
+ # aichivist
28
+
29
+ ### Clean up your files – like an archivist.
30
+
31
+ **Your AI renames, sorts and de-duplicates your documents.<br>You approve every step, and everything can be undone.**
32
+
33
+ [![tests](https://github.com/luftphilipp/aichivist/actions/workflows/tests.yml/badge.svg)](https://github.com/luftphilipp/aichivist/actions/workflows/tests.yml)
34
+ ![Windows · macOS · Linux](https://img.shields.io/badge/Windows%20%C2%B7%20macOS%20%C2%B7%20Linux-lightgrey)
35
+ ![License: MIT](https://img.shields.io/badge/license-MIT-green)
36
+
37
+ [Get started](#get-started) · [Why not just ask an AI?](#why-not-just-ask-an-ai) · [What it can do](#what-it-can-do)
38
+
39
+ </div>
40
+
41
+ ## Why not just ask an AI?
42
+
43
+ Tell ChatGPT or Claude *"tidy up my documents"* and you usually get a quick job: folders by file type
44
+ (`PDFs`, `Images`, `Misc`), the same cryptic names, duplicates left where they were – and sometimes a game or program
45
+ that no longer starts because its files were moved. It happens all at once, with no plan to check and no way back.
46
+ Next time, the AI invents a different system.
47
+
48
+ ![Asking an AI alone vs. an AI with aichivist](docs/plain-ai-vs-aichivist.svg)
49
+
50
+ aichivist gives your AI what a professional archivist works with: **your own filing rules**, a **plan you approve**,
51
+ **names read from the document itself**, **duplicates found by content**, **programs that keep working**, and a
52
+ **record of every step** that can be undone. Tried in real use on 25,000 photos and 1,500 documents.
53
+
54
+ ## How it works
55
+
56
+ ![The AI reads, proposes a plan, you approve, everything is filed](docs/how-it-works.svg)
57
+
58
+ - **Nothing happens without your OK.** You see the plan first and can change or strike any line.
59
+ - **Nothing gets lost.** Every step is recorded and can be undone; duplicates go to the trash only after you agree.
60
+ - **Your files stay yours.** Normal folders, your own cloud, no account, no server.
61
+ - **Speaks your language.** Messages in English or German; rule templates in English, German, French, Italian and
62
+ Spanish.
63
+
64
+ ## What it can do
65
+
66
+ ![Reads scans, names in your style, finds duplicates, programs keep working, seals finished folders, retention periods, backup check, private stays private](docs/features.svg)
67
+
68
+ ## Get started
69
+
70
+ **Claude Desktop** – the easiest way, no technical knowledge needed
71
+ 1. Download the `.mcpb` file from the [latest release](https://github.com/luftphilipp/aichivist/releases/latest)
72
+ (no Claude Desktop yet? [claude.ai/download](https://claude.ai/download)).
73
+ 2. Double-click it and choose the folders Claude may organize.
74
+ 3. Ask Claude: *"Tidy up my Downloads."*
75
+
76
+ <details>
77
+ <summary>Other ways: Claude Code, Codex/Gemini CLI/Cursor, or without AI</summary>
78
+
79
+ **Claude Code** – type `/plugin marketplace add luftphilipp/aichivist`, then `/plugin install aichivist@aichivist`,
80
+ then say *"Set up aichivist."* The setup installs the command-line tool if it is missing (it asks first).
81
+
82
+ **Codex, Gemini CLI, Cursor** – `pipx install aichivist` (or `uv tool install aichivist`), then copy
83
+ [`templates/AGENTS.md`](templates/AGENTS.md) to where your agent reads instructions.
84
+
85
+ **Downloaded the ZIP?** Unpack it and double-click **`Start (Mac).command`** or **`Start (Windows).bat`**
86
+ (Linux: `start.sh`). A simple menu opens – look at your Downloads, check your backups, and see which AI app on your
87
+ computer can tidy up. No installation needed, only Python 3 (a new Mac offers to install it by itself).
88
+ The first time, your computer warns because the file comes from the internet:
89
+ - **Mac:** click *Done*, open *System Settings → Privacy & Security*, scroll down and click *Open Anyway* next to
90
+ "Start (Mac).command", then confirm.
91
+ - **Windows:** click *More info → Run anyway*.
92
+
93
+ </details>
94
+
95
+ **Just curious?** See what's going on in your folders – no AI, nothing is changed:
96
+
97
+ ```bash
98
+ pipx install aichivist
99
+ aichivist survey ~/Downloads
100
+ ```
101
+
102
+ It shows how many files you have, duplicates that waste space, files with meaningless names, folders that slow down
103
+ your cloud, program folders to leave alone, and whether your hard drive is healthy. `aichivist storage storage.md`
104
+ checks whether your backups are safe and what your storage costs per year (it shows the table format on first run).
105
+ `aichivist catalog-page <folder> --open` turns your catalogs into a searchable page, `aichivist photos` sorts photos
106
+ by year, and `aichivist watch ~/Downloads --install` reminds you once 25 files have piled up.
107
+
108
+ <details>
109
+ <summary>See it in action (terminal demo)</summary>
110
+
111
+ ![aichivist demo](docs/demo.svg)
112
+
113
+ </details>
114
+
115
+ ## Built on real archive practice
116
+
117
+ Archives have organized documents reliably for centuries. aichivist brings their rules to your computer:
118
+
119
+ | In an archive | In aichivist |
120
+ |---|---|
121
+ | A **filing plan** says where everything belongs | Your rules say which documents go where – the AI follows them |
122
+ | **Nothing is thrown away** without a decision | Duplicates and old files are only removed after your OK |
123
+ | Every document is **described in a catalog** | Each folder gets a list of what is in it, so you find things later |
124
+ | **Finished collections are sealed** | A completed folder (e.g. last year's taxes) is locked, checked and searchable |
125
+ | Records are **kept for a set time**, then archived or destroyed | Old records come up when their time is over – you choose |
126
+ | **Copies are kept in separate places** | It checks the 3-2-1 rule: 3 copies, 2 kinds of storage, 1 away from home |
127
+
128
+ ## Privacy
129
+
130
+ aichivist itself works only on your computer. The AI that reads your documents (e.g. Claude) receives the content of
131
+ the documents it reads – so you decide which folders it may open, and can exclude private ones completely.
132
+
133
+ ## How this was built
134
+
135
+ aichivist was designed by **Philipp Luft**, a trained archivist and information professional (Fachmann Information &
136
+ Dokumentation) studying Business AI, who brought the archival principles and the requirements. The code was written
137
+ with **[Claude Code](https://claude.com/claude-code)** – every commit lists Claude as co-author.
138
+
139
+ ## More
140
+
141
+ - [Details](docs/DETAILS.md) – all commands, setup for other apps, safety model, comparison with other tools
142
+ - [Concept](docs/CONCEPT.md) – the archival principles behind it
143
+ - [Contributing](CONTRIBUTING.md) · [Changelog](CHANGELOG.md) · [Security](SECURITY.md) · MIT [license](LICENSE)
@@ -0,0 +1,121 @@
1
+ <div align="center">
2
+
3
+ <img src="extension/icon.png" width="96" alt="">
4
+
5
+ # aichivist
6
+
7
+ ### Clean up your files – like an archivist.
8
+
9
+ **Your AI renames, sorts and de-duplicates your documents.<br>You approve every step, and everything can be undone.**
10
+
11
+ [![tests](https://github.com/luftphilipp/aichivist/actions/workflows/tests.yml/badge.svg)](https://github.com/luftphilipp/aichivist/actions/workflows/tests.yml)
12
+ ![Windows · macOS · Linux](https://img.shields.io/badge/Windows%20%C2%B7%20macOS%20%C2%B7%20Linux-lightgrey)
13
+ ![License: MIT](https://img.shields.io/badge/license-MIT-green)
14
+
15
+ [Get started](#get-started) · [Why not just ask an AI?](#why-not-just-ask-an-ai) · [What it can do](#what-it-can-do)
16
+
17
+ </div>
18
+
19
+ ## Why not just ask an AI?
20
+
21
+ Tell ChatGPT or Claude *"tidy up my documents"* and you usually get a quick job: folders by file type
22
+ (`PDFs`, `Images`, `Misc`), the same cryptic names, duplicates left where they were – and sometimes a game or program
23
+ that no longer starts because its files were moved. It happens all at once, with no plan to check and no way back.
24
+ Next time, the AI invents a different system.
25
+
26
+ ![Asking an AI alone vs. an AI with aichivist](docs/plain-ai-vs-aichivist.svg)
27
+
28
+ aichivist gives your AI what a professional archivist works with: **your own filing rules**, a **plan you approve**,
29
+ **names read from the document itself**, **duplicates found by content**, **programs that keep working**, and a
30
+ **record of every step** that can be undone. Tried in real use on 25,000 photos and 1,500 documents.
31
+
32
+ ## How it works
33
+
34
+ ![The AI reads, proposes a plan, you approve, everything is filed](docs/how-it-works.svg)
35
+
36
+ - **Nothing happens without your OK.** You see the plan first and can change or strike any line.
37
+ - **Nothing gets lost.** Every step is recorded and can be undone; duplicates go to the trash only after you agree.
38
+ - **Your files stay yours.** Normal folders, your own cloud, no account, no server.
39
+ - **Speaks your language.** Messages in English or German; rule templates in English, German, French, Italian and
40
+ Spanish.
41
+
42
+ ## What it can do
43
+
44
+ ![Reads scans, names in your style, finds duplicates, programs keep working, seals finished folders, retention periods, backup check, private stays private](docs/features.svg)
45
+
46
+ ## Get started
47
+
48
+ **Claude Desktop** – the easiest way, no technical knowledge needed
49
+ 1. Download the `.mcpb` file from the [latest release](https://github.com/luftphilipp/aichivist/releases/latest)
50
+ (no Claude Desktop yet? [claude.ai/download](https://claude.ai/download)).
51
+ 2. Double-click it and choose the folders Claude may organize.
52
+ 3. Ask Claude: *"Tidy up my Downloads."*
53
+
54
+ <details>
55
+ <summary>Other ways: Claude Code, Codex/Gemini CLI/Cursor, or without AI</summary>
56
+
57
+ **Claude Code** – type `/plugin marketplace add luftphilipp/aichivist`, then `/plugin install aichivist@aichivist`,
58
+ then say *"Set up aichivist."* The setup installs the command-line tool if it is missing (it asks first).
59
+
60
+ **Codex, Gemini CLI, Cursor** – `pipx install aichivist` (or `uv tool install aichivist`), then copy
61
+ [`templates/AGENTS.md`](templates/AGENTS.md) to where your agent reads instructions.
62
+
63
+ **Downloaded the ZIP?** Unpack it and double-click **`Start (Mac).command`** or **`Start (Windows).bat`**
64
+ (Linux: `start.sh`). A simple menu opens – look at your Downloads, check your backups, and see which AI app on your
65
+ computer can tidy up. No installation needed, only Python 3 (a new Mac offers to install it by itself).
66
+ The first time, your computer warns because the file comes from the internet:
67
+ - **Mac:** click *Done*, open *System Settings → Privacy & Security*, scroll down and click *Open Anyway* next to
68
+ "Start (Mac).command", then confirm.
69
+ - **Windows:** click *More info → Run anyway*.
70
+
71
+ </details>
72
+
73
+ **Just curious?** See what's going on in your folders – no AI, nothing is changed:
74
+
75
+ ```bash
76
+ pipx install aichivist
77
+ aichivist survey ~/Downloads
78
+ ```
79
+
80
+ It shows how many files you have, duplicates that waste space, files with meaningless names, folders that slow down
81
+ your cloud, program folders to leave alone, and whether your hard drive is healthy. `aichivist storage storage.md`
82
+ checks whether your backups are safe and what your storage costs per year (it shows the table format on first run).
83
+ `aichivist catalog-page <folder> --open` turns your catalogs into a searchable page, `aichivist photos` sorts photos
84
+ by year, and `aichivist watch ~/Downloads --install` reminds you once 25 files have piled up.
85
+
86
+ <details>
87
+ <summary>See it in action (terminal demo)</summary>
88
+
89
+ ![aichivist demo](docs/demo.svg)
90
+
91
+ </details>
92
+
93
+ ## Built on real archive practice
94
+
95
+ Archives have organized documents reliably for centuries. aichivist brings their rules to your computer:
96
+
97
+ | In an archive | In aichivist |
98
+ |---|---|
99
+ | A **filing plan** says where everything belongs | Your rules say which documents go where – the AI follows them |
100
+ | **Nothing is thrown away** without a decision | Duplicates and old files are only removed after your OK |
101
+ | Every document is **described in a catalog** | Each folder gets a list of what is in it, so you find things later |
102
+ | **Finished collections are sealed** | A completed folder (e.g. last year's taxes) is locked, checked and searchable |
103
+ | Records are **kept for a set time**, then archived or destroyed | Old records come up when their time is over – you choose |
104
+ | **Copies are kept in separate places** | It checks the 3-2-1 rule: 3 copies, 2 kinds of storage, 1 away from home |
105
+
106
+ ## Privacy
107
+
108
+ aichivist itself works only on your computer. The AI that reads your documents (e.g. Claude) receives the content of
109
+ the documents it reads – so you decide which folders it may open, and can exclude private ones completely.
110
+
111
+ ## How this was built
112
+
113
+ aichivist was designed by **Philipp Luft**, a trained archivist and information professional (Fachmann Information &
114
+ Dokumentation) studying Business AI, who brought the archival principles and the requirements. The code was written
115
+ with **[Claude Code](https://claude.com/claude-code)** – every commit lists Claude as co-author.
116
+
117
+ ## More
118
+
119
+ - [Details](docs/DETAILS.md) – all commands, setup for other apps, safety model, comparison with other tools
120
+ - [Concept](docs/CONCEPT.md) – the archival principles behind it
121
+ - [Contributing](CONTRIBUTING.md) · [Changelog](CHANGELOG.md) · [Security](SECURITY.md) · MIT [license](LICENSE)
@@ -0,0 +1,3 @@
1
+ """aichivist – archival-grade file operations for AI filing agents."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
@@ -0,0 +1,184 @@
1
+ """Tools for agents without file access of their own (e.g. Claude Desktop via MCP).
2
+
3
+ Every path must lie inside the folders the person shared when starting the server (--root). Paths are resolved
4
+ first, so `..` and symlinks cannot escape. Moving files is only possible through filing plans.
5
+ """
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ from . import (
11
+ apps,
12
+ catalog,
13
+ config,
14
+ describe,
15
+ duplicates,
16
+ fulltext,
17
+ inventory,
18
+ lifecycle,
19
+ photos,
20
+ plan,
21
+ retention,
22
+ review,
23
+ similar,
24
+ storage,
25
+ structure,
26
+ style,
27
+ survey,
28
+ )
29
+ from .config import full_path
30
+ from .fileops import FilingError
31
+
32
+ PLAN_NAME = re.compile(r"^[\w .-]{1,60}$")
33
+ DEFAULT_MAX_CHARS = 20_000
34
+ LIST_LIMIT = 300
35
+
36
+
37
+ class Workspace:
38
+ def __init__(self, roots: List, rules=None, exclude: Optional[List] = None):
39
+ self.roots = [full_path(r).resolve() for r in roots]
40
+ self.rules = full_path(rules) if rules else None
41
+ self.exclude = [full_path(e).resolve() for e in exclude or []]
42
+
43
+ def excluded(self, path: Path) -> bool:
44
+ return any(path == e or e in path.parents for e in self.exclude)
45
+
46
+ def check(self, path) -> Path:
47
+ resolved = full_path(path).resolve()
48
+ if not any(resolved == r or r in resolved.parents for r in self.roots):
49
+ raise FilingError(f"{path} is outside the folders shared with aichivist")
50
+ if self.excluded(resolved):
51
+ raise FilingError(f"{path} is in an excluded folder; aichivist does not open it")
52
+ return resolved
53
+
54
+ def _plan_file(self, name: str) -> Path:
55
+ if not PLAN_NAME.match(name):
56
+ raise FilingError("plan names may contain letters, digits, spaces, dots, dashes and underscores")
57
+ folder = config.home_dir() / "plans"
58
+ folder.mkdir(parents=True, exist_ok=True)
59
+ return folder / f"{name}.md"
60
+
61
+ # ---- reading -------------------------------------------------------------
62
+ def get_rules(self) -> Dict[str, Any]:
63
+ if not self.rules or not self.rules.exists():
64
+ return {"rules": None, "note": "No rules file configured – ask the person where things belong before "
65
+ "proposing a plan, and suggest writing rules (templates/RULES.md)."}
66
+ return {"rules": self.rules.read_text(encoding="utf-8"), "path": str(self.rules)}
67
+
68
+ def survey(self, paths: List[str], stale_years: int = 3) -> Dict[str, Any]:
69
+ report = survey.run([self.check(p) for p in paths], stale_years=stale_years, exclude=self.exclude)
70
+ return survey.as_dict(report)
71
+
72
+ def list_folder(self, path: str) -> Dict[str, Any]:
73
+ folder = self.check(path)
74
+ entries = [{"name": p.name, "kind": "folder" if p.is_dir() else "file",
75
+ "size": p.stat().st_size if p.is_file() else None}
76
+ for p in sorted(folder.iterdir()) if not p.name.startswith(".") and not self.excluded(p.resolve())]
77
+ return {"path": str(folder), "entries": entries[:LIST_LIMIT], "truncated": len(entries) > LIST_LIMIT}
78
+
79
+ def read_document(self, path: str, max_chars: int = DEFAULT_MAX_CHARS) -> Dict[str, Any]:
80
+ file = self.check(path)
81
+ if not file.is_file():
82
+ raise FilingError(f"not a file: {path}")
83
+ text, method = fulltext.extract(file)
84
+ return {"path": str(file), "method": method, "text": text[:max_chars], "characters": len(text),
85
+ "truncated": len(text) > max_chars}
86
+
87
+ def inventory(self, path: str) -> Dict[str, Any]:
88
+ folder = self.check(path)
89
+ changes = inventory.diff(folder)
90
+ by_hand = inventory.manual_moves(folder, changes)
91
+
92
+ def visible(rel: str) -> bool:
93
+ return not self.excluded((folder / rel.rstrip("/")).resolve())
94
+ return {"entries": [e for e in changes.entries if visible(e)], "first_run": changes.first_run,
95
+ "new": [e for e in changes.new if visible(e)],
96
+ "moved": [{"from": o, "to": n, "by_hand": (o, n) in by_hand} for o, n in changes.moved
97
+ if visible(o) and visible(n)]}
98
+
99
+ def find_duplicates(self, paths: List[str], area: str) -> List[Dict[str, str]]:
100
+ found = duplicates.find([self.check(p) for p in paths], self.check(area))
101
+ return [{"source": str(d.source), "match": str(d.match)} for d in found
102
+ if not self.excluded(d.source.resolve()) and not self.excluded(d.match.resolve())]
103
+
104
+ def find_similar(self, paths: List[str], area: Optional[str] = None, threshold: float = 0.8) -> List[Dict]:
105
+ hits = similar.find([self.check(p) for p in paths], self.check(area) if area else None, threshold)
106
+ return [h._asdict() for h in hits]
107
+
108
+ def suggest_name(self, path: str) -> Dict[str, Any]:
109
+ return describe.suggest(self.check(path))
110
+
111
+ def naming_examples(self, area: str) -> Dict[str, Any]:
112
+ return style.examples(self.check(area))
113
+
114
+ def structure_advice(self, area: str) -> Dict[str, Any]:
115
+ report = structure.analyse(self.check(area))
116
+ return {"findings": [f._asdict() for f in report.findings], "summary": structure.render(report)}
117
+
118
+ def retention_due(self, area: str, years: float) -> List[Dict[str, Any]]:
119
+ return [d._asdict() for d in retention.due(self.check(area), years)]
120
+
121
+ # ---- filing plans --------------------------------------------------------
122
+ def plan_add(self, source: str, target: str, reason: str, plan_name: str = "current") -> Dict[str, str]:
123
+ checked_target = target if target == plan.TRASH else str(self.check(target))
124
+ return plan.add(self._plan_file(plan_name), self.check(source), checked_target, reason)._asdict()
125
+
126
+ def plan_show(self, plan_name: str = "current") -> Dict[str, Any]:
127
+ file = self._plan_file(plan_name)
128
+ rows = [r._asdict() for r in plan.read(file)] if file.exists() else []
129
+ return {"plan": str(file), "rows": rows}
130
+
131
+ def plan_check(self, plan_name: str = "current", create_folders: bool = False) -> Dict[str, Any]:
132
+ results = plan.check(self._plan_file(plan_name), create_folders)
133
+ return {"problems": [{"row": i, "source": r.source, "problem": p}
134
+ for i, (r, p) in enumerate(results, 1) if p]}
135
+
136
+ def plan_approve(self, plan_name: str = "current", rows: Optional[List[int]] = None,
137
+ leave_link: bool = False) -> Dict[str, int]:
138
+ return {"approved": plan.approve(self._plan_file(plan_name), rows, leave_link)}
139
+
140
+ def storage_check(self, table_file: str) -> Dict[str, Any]:
141
+ table = self.check(table_file)
142
+ if not table.exists():
143
+ return {"error": "table missing", "example": storage.EXAMPLE}
144
+ return storage.as_dict(storage.check(table))
145
+
146
+ def program_check(self, paths: List[str]) -> Dict[str, List[str]]:
147
+ ctx = apps.context()
148
+ return {p: apps.risks(self.check(p), ctx) for p in paths}
149
+
150
+ def plan_review(self, plan_name: str = "current") -> Dict[str, str]:
151
+ url = review.serve(self._plan_file(plan_name), open_browser=True, block=False)
152
+ return {"url": url, "note": "The person reviews the plan in the browser and approves rows there."}
153
+
154
+ def plan_apply(self, plan_name: str = "current", create_folders: bool = False) -> Dict[str, object]:
155
+ file = self._plan_file(plan_name)
156
+ for row in plan.read(file): # a plan file could have been edited – re-check every approved row
157
+ if row.mark in ("x", "link"):
158
+ self.check(row.source)
159
+ if row.target != plan.TRASH:
160
+ self.check(row.target)
161
+ return plan.apply(file, create_folders)._asdict()
162
+
163
+ def plan_photos(self, inbox: str, photos_root: str, plan_name: str = "current") -> Dict[str, Any]:
164
+ rows = photos.propose(self.check(inbox), self.check(photos_root), self._plan_file(plan_name))
165
+ return {"proposed": len(rows), "rows": [r._asdict() for r in rows]}
166
+
167
+ # ---- catalogs and collections ---------------------------------------------
168
+ def catalog_check(self, catalog_file: str, add_missing: bool = False, prefill: bool = True) -> Dict[str, Any]:
169
+ file = self.check(catalog_file)
170
+ added = catalog.add_missing(file, prefill=prefill) if add_missing else []
171
+ return {"added": added, **catalog.check(file)._asdict()}
172
+
173
+ def catalog_describe(self, catalog_file: str, file: str, content: str, search_terms: str,
174
+ date: str) -> Dict[str, str]:
175
+ return {"row": catalog.describe(self.check(catalog_file), file, content, search_terms, date)}
176
+
177
+ def status(self, path: str) -> Dict[str, Any]:
178
+ return {k: str(v) if v is not None else None for k, v in lifecycle.status(self.check(path))._asdict().items()}
179
+
180
+ def verify(self, path: str) -> Dict[str, List[str]]:
181
+ return lifecycle.verify(self.check(path))
182
+
183
+ def search(self, path: str, query: str) -> List[Dict[str, Any]]:
184
+ return [h._asdict() for h in fulltext.search(self.check(path), query)]