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.
- aichivist-0.1.0/LICENSE +21 -0
- aichivist-0.1.0/PKG-INFO +143 -0
- aichivist-0.1.0/README.md +121 -0
- aichivist-0.1.0/aichivist/__init__.py +3 -0
- aichivist-0.1.0/aichivist/__main__.py +5 -0
- aichivist-0.1.0/aichivist/agent_tools.py +184 -0
- aichivist-0.1.0/aichivist/apps.py +243 -0
- aichivist-0.1.0/aichivist/bagit.py +103 -0
- aichivist-0.1.0/aichivist/catalog.py +136 -0
- aichivist-0.1.0/aichivist/catalog_page.py +129 -0
- aichivist-0.1.0/aichivist/checksums.py +45 -0
- aichivist-0.1.0/aichivist/cli.py +230 -0
- aichivist-0.1.0/aichivist/commands.py +306 -0
- aichivist-0.1.0/aichivist/config.py +78 -0
- aichivist-0.1.0/aichivist/describe.py +161 -0
- aichivist-0.1.0/aichivist/drives.py +83 -0
- aichivist-0.1.0/aichivist/duplicates.py +66 -0
- aichivist-0.1.0/aichivist/fileops.py +188 -0
- aichivist-0.1.0/aichivist/fulltext.py +183 -0
- aichivist-0.1.0/aichivist/guard.py +102 -0
- aichivist-0.1.0/aichivist/i18n.py +51 -0
- aichivist-0.1.0/aichivist/inventory.py +110 -0
- aichivist-0.1.0/aichivist/journal.py +39 -0
- aichivist-0.1.0/aichivist/lang_de.py +288 -0
- aichivist-0.1.0/aichivist/lifecycle.py +85 -0
- aichivist-0.1.0/aichivist/markers.py +24 -0
- aichivist-0.1.0/aichivist/mcp_server.py +97 -0
- aichivist-0.1.0/aichivist/notify.py +60 -0
- aichivist-0.1.0/aichivist/ocr.py +48 -0
- aichivist-0.1.0/aichivist/ocr_mac.js +30 -0
- aichivist-0.1.0/aichivist/ocr_windows.ps1 +38 -0
- aichivist-0.1.0/aichivist/photos.py +184 -0
- aichivist-0.1.0/aichivist/plan.py +188 -0
- aichivist-0.1.0/aichivist/protect.py +36 -0
- aichivist-0.1.0/aichivist/retention.py +53 -0
- aichivist-0.1.0/aichivist/review.py +121 -0
- aichivist-0.1.0/aichivist/rules.py +70 -0
- aichivist-0.1.0/aichivist/schedule.py +123 -0
- aichivist-0.1.0/aichivist/similar.py +49 -0
- aichivist-0.1.0/aichivist/start.py +135 -0
- aichivist-0.1.0/aichivist/storage.py +153 -0
- aichivist-0.1.0/aichivist/structure.py +134 -0
- aichivist-0.1.0/aichivist/style.py +29 -0
- aichivist-0.1.0/aichivist/survey.py +242 -0
- aichivist-0.1.0/aichivist/undo.py +57 -0
- aichivist-0.1.0/aichivist/update.py +69 -0
- aichivist-0.1.0/aichivist.egg-info/PKG-INFO +143 -0
- aichivist-0.1.0/aichivist.egg-info/SOURCES.txt +82 -0
- aichivist-0.1.0/aichivist.egg-info/dependency_links.txt +1 -0
- aichivist-0.1.0/aichivist.egg-info/entry_points.txt +2 -0
- aichivist-0.1.0/aichivist.egg-info/requires.txt +8 -0
- aichivist-0.1.0/aichivist.egg-info/top_level.txt +1 -0
- aichivist-0.1.0/pyproject.toml +54 -0
- aichivist-0.1.0/setup.cfg +4 -0
- aichivist-0.1.0/tests/test_agent_tools.py +136 -0
- aichivist-0.1.0/tests/test_apps.py +145 -0
- aichivist-0.1.0/tests/test_bagit.py +62 -0
- aichivist-0.1.0/tests/test_catalog.py +83 -0
- aichivist-0.1.0/tests/test_catalog_page.py +38 -0
- aichivist-0.1.0/tests/test_checksums.py +31 -0
- aichivist-0.1.0/tests/test_cli.py +190 -0
- aichivist-0.1.0/tests/test_commands.py +137 -0
- aichivist-0.1.0/tests/test_config.py +50 -0
- aichivist-0.1.0/tests/test_duplicates.py +53 -0
- aichivist-0.1.0/tests/test_extension.py +37 -0
- aichivist-0.1.0/tests/test_field_fixes.py +242 -0
- aichivist-0.1.0/tests/test_fileops.py +126 -0
- aichivist-0.1.0/tests/test_fulltext.py +128 -0
- aichivist-0.1.0/tests/test_guard.py +72 -0
- aichivist-0.1.0/tests/test_i18n.py +72 -0
- aichivist-0.1.0/tests/test_inventory_undo.py +90 -0
- aichivist-0.1.0/tests/test_lifecycle.py +89 -0
- aichivist-0.1.0/tests/test_photos.py +72 -0
- aichivist-0.1.0/tests/test_plan.py +106 -0
- aichivist-0.1.0/tests/test_retention_notify_review.py +90 -0
- aichivist-0.1.0/tests/test_review_fixes.py +205 -0
- aichivist-0.1.0/tests/test_rules_learning_ocr.py +120 -0
- aichivist-0.1.0/tests/test_schedule.py +61 -0
- aichivist-0.1.0/tests/test_similar_describe_style.py +116 -0
- aichivist-0.1.0/tests/test_start.py +77 -0
- aichivist-0.1.0/tests/test_storage.py +50 -0
- aichivist-0.1.0/tests/test_structure.py +69 -0
- aichivist-0.1.0/tests/test_survey.py +133 -0
- aichivist-0.1.0/tests/test_update.py +42 -0
aichivist-0.1.0/LICENSE
ADDED
|
@@ -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.
|
aichivist-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://github.com/luftphilipp/aichivist/actions/workflows/tests.yml)
|
|
34
|
+

|
|
35
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+
[](https://github.com/luftphilipp/aichivist/actions/workflows/tests.yml)
|
|
12
|
+

|
|
13
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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,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)]
|