annotheca 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.
- annotheca-0.1.0/.github/workflows/tests.yml +19 -0
- annotheca-0.1.0/.gitignore +6 -0
- annotheca-0.1.0/LICENSE +21 -0
- annotheca-0.1.0/PKG-INFO +203 -0
- annotheca-0.1.0/README.md +181 -0
- annotheca-0.1.0/docs/library-dark.png +0 -0
- annotheca-0.1.0/docs/library.png +0 -0
- annotheca-0.1.0/docs/reader.png +0 -0
- annotheca-0.1.0/pyproject.toml +38 -0
- annotheca-0.1.0/src/annotheca/__init__.py +3 -0
- annotheca-0.1.0/src/annotheca/__main__.py +5 -0
- annotheca-0.1.0/src/annotheca/api.py +194 -0
- annotheca-0.1.0/src/annotheca/bibtex.py +193 -0
- annotheca-0.1.0/src/annotheca/bundle.py +152 -0
- annotheca-0.1.0/src/annotheca/cli.py +122 -0
- annotheca-0.1.0/src/annotheca/library.py +409 -0
- annotheca-0.1.0/src/annotheca/mcp_server.py +246 -0
- annotheca-0.1.0/src/annotheca/models.py +23 -0
- annotheca-0.1.0/src/annotheca/sources.py +190 -0
- annotheca-0.1.0/src/annotheca/static/app.css +1123 -0
- annotheca-0.1.0/src/annotheca/static/favicon.svg +8 -0
- annotheca-0.1.0/src/annotheca/static/index.html +39 -0
- annotheca-0.1.0/src/annotheca/static/js/api.js +58 -0
- annotheca-0.1.0/src/annotheca/static/js/i18n.js +226 -0
- annotheca-0.1.0/src/annotheca/static/js/main.js +563 -0
- annotheca-0.1.0/src/annotheca/static/js/storage.js +30 -0
- annotheca-0.1.0/src/annotheca/static/js/ui.js +163 -0
- annotheca-0.1.0/src/annotheca/static/js/viewer.js +591 -0
- annotheca-0.1.0/src/annotheca/static/vendor/pdfjs/LICENSE +177 -0
- annotheca-0.1.0/src/annotheca/static/vendor/pdfjs/pdf.min.mjs +28 -0
- annotheca-0.1.0/src/annotheca/static/vendor/pdfjs/pdf.worker.min.mjs +28 -0
- annotheca-0.1.0/tests/test_api.py +104 -0
- annotheca-0.1.0/tests/test_bibtex.py +76 -0
- annotheca-0.1.0/tests/test_bundle.py +87 -0
- annotheca-0.1.0/tests/test_library.py +132 -0
- annotheca-0.1.0/tests/test_mcp_server.py +110 -0
- annotheca-0.1.0/tests/test_sources.py +56 -0
- annotheca-0.1.0/uv.lock +536 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
strategy:
|
|
11
|
+
matrix:
|
|
12
|
+
python-version: ["3.10", "3.13"]
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: ${{ matrix.python-version }}
|
|
18
|
+
- run: pip install -e '.[test]'
|
|
19
|
+
- run: pytest
|
annotheca-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Melissa Colin
|
|
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.
|
annotheca-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: annotheca
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A local, bilingual reference manager for research projects: BibTeX, PDFs, highlights and sticky notes.
|
|
5
|
+
Project-URL: Homepage, https://github.com/melissa-colin/annotheca
|
|
6
|
+
Author: Melissa Colin
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: annotation,bibliography,bibtex,pdf,reference-manager,research
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: fastapi>=0.110
|
|
16
|
+
Requires-Dist: httpx>=0.27
|
|
17
|
+
Requires-Dist: mcp<3,>=2
|
|
18
|
+
Requires-Dist: uvicorn>=0.29
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
<p align="center">
|
|
24
|
+
<img src="https://raw.githubusercontent.com/melissa-colin/annotheca/main/src/annotheca/static/favicon.svg" width="72" alt="">
|
|
25
|
+
</p>
|
|
26
|
+
|
|
27
|
+
<h1 align="center">Annotheca</h1>
|
|
28
|
+
|
|
29
|
+
<p align="center">
|
|
30
|
+
<b>A calm, local home for the bibliography of your research projects.</b><br>
|
|
31
|
+
Paste a reference, get the BibTeX and the PDF, highlight, take notes, export one clean <code>.bib</code>.<br>
|
|
32
|
+
Let Claude and other AI assistants file the papers they find, with verified metadata.
|
|
33
|
+
</p>
|
|
34
|
+
|
|
35
|
+
<p align="center">
|
|
36
|
+
<img src="https://raw.githubusercontent.com/melissa-colin/annotheca/main/docs/library.png" alt="The library view of Annotheca" width="900">
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
## Why
|
|
40
|
+
|
|
41
|
+
Research projects live in stages (state of the art, method, experiments,
|
|
42
|
+
writing) and the same paper often matters in several of them, for different
|
|
43
|
+
reasons. Annotheca organises references the way the project is organised:
|
|
44
|
+
|
|
45
|
+
- **Projects and stage folders.** Every project starts with a `SoA` (state of
|
|
46
|
+
the art) folder; add as many folders as your project has stages.
|
|
47
|
+
- **Paste anything.** BibTeX (one or many entries), DOIs, arXiv IDs or URLs,
|
|
48
|
+
or a formatted citation copied from a paper (APA, IEEE, ...). Annotheca
|
|
49
|
+
resolves it into clean BibTeX through doi.org, arXiv and Crossref.
|
|
50
|
+
- **PDFs found for you.** Open-access PDFs are downloaded automatically from
|
|
51
|
+
arXiv, Semantic Scholar and OpenAlex, following repository landing pages to
|
|
52
|
+
their PDF. Otherwise, drop the PDF on the paper.
|
|
53
|
+
- **Highlights, sticky notes and general notes.** Highlight passages in five
|
|
54
|
+
colours, pin draggable post-its on the page, and keep general notes on each
|
|
55
|
+
paper, next to it in the list or beside the PDF.
|
|
56
|
+
- **One paper, several folders, separate notes.** A paper filed in two folders
|
|
57
|
+
is stored once, but its highlights and notes are specific to each folder:
|
|
58
|
+
what you underline for the state of the art is not what you underline for
|
|
59
|
+
the method.
|
|
60
|
+
- **Share a project.** *Share* downloads the project, with its folders,
|
|
61
|
+
notes, highlights and PDFs, as one `.annotheca` file; whoever receives it
|
|
62
|
+
opens it with the import button of the sidebar.
|
|
63
|
+
- **Works with AI assistants.** Claude (or any MCP client) can file the papers
|
|
64
|
+
it finds into your projects, add notes, read yours and get the `.bib`. See
|
|
65
|
+
[AI assistants](#ai-assistants).
|
|
66
|
+
- **One clean `.bib`.** Export the whole project in one click; papers filed in
|
|
67
|
+
several folders appear only once, with stable, unique citation keys.
|
|
68
|
+
- **English and French**, light and dark themes, everything stored locally.
|
|
69
|
+
|
|
70
|
+
<p align="center">
|
|
71
|
+
<img src="https://raw.githubusercontent.com/melissa-colin/annotheca/main/docs/reader.png" alt="Reading a paper with highlights and a sticky note" width="900">
|
|
72
|
+
</p>
|
|
73
|
+
|
|
74
|
+
## Install
|
|
75
|
+
|
|
76
|
+
Annotheca needs Python 3.10 or newer. Install it as a command with
|
|
77
|
+
[uv](https://docs.astral.sh/uv/) or [pipx](https://pipx.pypa.io/):
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
uv tool install annotheca
|
|
81
|
+
# or
|
|
82
|
+
pipx install annotheca
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Then, to let Claude use it (optional, see [AI assistants](#ai-assistants)):
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
annotheca connect
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Update later with `uv tool upgrade annotheca` (or `pipx upgrade annotheca`).
|
|
92
|
+
|
|
93
|
+
## Use
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
annotheca
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The interface opens in your browser at <http://127.0.0.1:8737>. Running the
|
|
100
|
+
command again while Annotheca is running simply reopens it.
|
|
101
|
+
|
|
102
|
+
| Option or command | Effect |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| `--port PORT` | Serve on another local port (default `8737`). |
|
|
105
|
+
| `--data-dir DIR` | Keep the library somewhere else. |
|
|
106
|
+
| `--no-browser` | Do not open the browser. |
|
|
107
|
+
| `connect` | Make Annotheca available to Claude (see below). |
|
|
108
|
+
| `mcp` | Serve the tools for AI assistants over stdio (used by `connect`). |
|
|
109
|
+
|
|
110
|
+
Tips:
|
|
111
|
+
|
|
112
|
+
- `Ctrl+Enter` adds what you pasted.
|
|
113
|
+
- Click a citation key to copy `\cite{key}`.
|
|
114
|
+
- Drop a PDF file on a paper to attach it.
|
|
115
|
+
- In the reader, select text to highlight it; switch to *Sticky note* and
|
|
116
|
+
click on the page to pin a note; click a highlight to recolour or delete it;
|
|
117
|
+
`Esc` goes back.
|
|
118
|
+
|
|
119
|
+
## AI assistants
|
|
120
|
+
|
|
121
|
+
When an assistant writes references itself, it can get a title, an author or a
|
|
122
|
+
year wrong. With Annotheca, the assistant only says *which* paper it means (a
|
|
123
|
+
DOI, an arXiv ID, a citation); Annotheca fetches the metadata from doi.org,
|
|
124
|
+
arXiv and Crossref, files the paper with its PDF, and answers with the exact
|
|
125
|
+
citation key, title and authors to use. Every paper an assistant adds is marked
|
|
126
|
+
with its name, so you can always see, and filter, what it relied on.
|
|
127
|
+
|
|
128
|
+
`annotheca connect` registers Annotheca as an
|
|
129
|
+
[MCP](https://modelcontextprotocol.io) server for **Claude Code** (in every
|
|
130
|
+
project) and **Claude Desktop** when they are installed, and prints the
|
|
131
|
+
configuration to paste into other MCP clients (Cursor, Codex, Gemini CLI...).
|
|
132
|
+
The assistant then just needs to be asked:
|
|
133
|
+
|
|
134
|
+
> Find recent papers on contact-implicit trajectory optimisation and add the
|
|
135
|
+
> three most relevant to the SoA of my PhysProx project, with a note on each.
|
|
136
|
+
|
|
137
|
+
> What did I highlight in posa2013direct? Write the related-work paragraph
|
|
138
|
+
> citing the papers of the Method folder.
|
|
139
|
+
|
|
140
|
+
| Tool | What the assistant can do |
|
|
141
|
+
| --- | --- |
|
|
142
|
+
| `list_projects` | See the projects and their folders. |
|
|
143
|
+
| `create_project`, `create_folder` | Create them. |
|
|
144
|
+
| `add_references` | File papers from DOIs, arXiv IDs, BibTeX or citations, with a note; get back verified metadata. |
|
|
145
|
+
| `list_papers` | List the papers of a project with your notes. |
|
|
146
|
+
| `read_paper` | Read a paper's BibTeX, your notes, highlights and sticky notes, and the path of its PDF. |
|
|
147
|
+
| `add_note` | Add a signed paragraph to a paper's notes. |
|
|
148
|
+
| `get_bibtex` | Get the project's `.bib`. |
|
|
149
|
+
|
|
150
|
+
Nothing else is needed: the server explains itself to the assistant. Papers it
|
|
151
|
+
adds appear in the interface as soon as you come back to the window.
|
|
152
|
+
|
|
153
|
+
## Your data
|
|
154
|
+
|
|
155
|
+
Everything stays on your machine, in `~/.local/share/annotheca` (or
|
|
156
|
+
`$XDG_DATA_HOME/annotheca`, or `$ANNOTHECA_HOME`):
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
library.db SQLite database: projects, folders, BibTeX, notes, annotations
|
|
160
|
+
pdfs/ one PDF per paper
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Back it up by copying that directory. The only network requests are the
|
|
164
|
+
lookups of references and PDFs, sent to doi.org, arXiv, Crossref, Semantic
|
|
165
|
+
Scholar, OpenAlex and the repositories they point to.
|
|
166
|
+
|
|
167
|
+
<p align="center">
|
|
168
|
+
<img src="https://raw.githubusercontent.com/melissa-colin/annotheca/main/docs/library-dark.png" alt="Dark theme" width="700">
|
|
169
|
+
</p>
|
|
170
|
+
|
|
171
|
+
## Development
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
git clone https://github.com/melissa-colin/annotheca
|
|
175
|
+
cd annotheca
|
|
176
|
+
uv venv && uv pip install -e '.[test]'
|
|
177
|
+
.venv/bin/pytest
|
|
178
|
+
.venv/bin/annotheca --data-dir /tmp/annotheca-dev
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The code follows the Google Python and JavaScript style guides. The backend is
|
|
182
|
+
a small [FastAPI](https://fastapi.tiangolo.com/) app over SQLite
|
|
183
|
+
(`src/annotheca`); the interface is dependency-free JavaScript modules with
|
|
184
|
+
[pdf.js](https://mozilla.github.io/pdf.js/) vendored, so there is no build
|
|
185
|
+
step.
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
src/annotheca/
|
|
189
|
+
cli.py command line entry point
|
|
190
|
+
api.py HTTP API and static files
|
|
191
|
+
library.py storage: projects, folders, papers, placements, annotations
|
|
192
|
+
bundle.py .annotheca files to share a project
|
|
193
|
+
mcp_server.py tools for AI assistants (Model Context Protocol)
|
|
194
|
+
models.py validated shapes shared by the API and the bundles
|
|
195
|
+
bibtex.py BibTeX parsing, formatting, citation keys, identifiers
|
|
196
|
+
sources.py doi.org, arXiv, Crossref, Semantic Scholar, OpenAlex lookups
|
|
197
|
+
static/ web interface
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
MIT. pdf.js is distributed under the Apache License 2.0
|
|
203
|
+
(`src/annotheca/static/vendor/pdfjs/LICENSE`).
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/melissa-colin/annotheca/main/src/annotheca/static/favicon.svg" width="72" alt="">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">Annotheca</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<b>A calm, local home for the bibliography of your research projects.</b><br>
|
|
9
|
+
Paste a reference, get the BibTeX and the PDF, highlight, take notes, export one clean <code>.bib</code>.<br>
|
|
10
|
+
Let Claude and other AI assistants file the papers they find, with verified metadata.
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
|
+
<img src="https://raw.githubusercontent.com/melissa-colin/annotheca/main/docs/library.png" alt="The library view of Annotheca" width="900">
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
## Why
|
|
18
|
+
|
|
19
|
+
Research projects live in stages (state of the art, method, experiments,
|
|
20
|
+
writing) and the same paper often matters in several of them, for different
|
|
21
|
+
reasons. Annotheca organises references the way the project is organised:
|
|
22
|
+
|
|
23
|
+
- **Projects and stage folders.** Every project starts with a `SoA` (state of
|
|
24
|
+
the art) folder; add as many folders as your project has stages.
|
|
25
|
+
- **Paste anything.** BibTeX (one or many entries), DOIs, arXiv IDs or URLs,
|
|
26
|
+
or a formatted citation copied from a paper (APA, IEEE, ...). Annotheca
|
|
27
|
+
resolves it into clean BibTeX through doi.org, arXiv and Crossref.
|
|
28
|
+
- **PDFs found for you.** Open-access PDFs are downloaded automatically from
|
|
29
|
+
arXiv, Semantic Scholar and OpenAlex, following repository landing pages to
|
|
30
|
+
their PDF. Otherwise, drop the PDF on the paper.
|
|
31
|
+
- **Highlights, sticky notes and general notes.** Highlight passages in five
|
|
32
|
+
colours, pin draggable post-its on the page, and keep general notes on each
|
|
33
|
+
paper, next to it in the list or beside the PDF.
|
|
34
|
+
- **One paper, several folders, separate notes.** A paper filed in two folders
|
|
35
|
+
is stored once, but its highlights and notes are specific to each folder:
|
|
36
|
+
what you underline for the state of the art is not what you underline for
|
|
37
|
+
the method.
|
|
38
|
+
- **Share a project.** *Share* downloads the project, with its folders,
|
|
39
|
+
notes, highlights and PDFs, as one `.annotheca` file; whoever receives it
|
|
40
|
+
opens it with the import button of the sidebar.
|
|
41
|
+
- **Works with AI assistants.** Claude (or any MCP client) can file the papers
|
|
42
|
+
it finds into your projects, add notes, read yours and get the `.bib`. See
|
|
43
|
+
[AI assistants](#ai-assistants).
|
|
44
|
+
- **One clean `.bib`.** Export the whole project in one click; papers filed in
|
|
45
|
+
several folders appear only once, with stable, unique citation keys.
|
|
46
|
+
- **English and French**, light and dark themes, everything stored locally.
|
|
47
|
+
|
|
48
|
+
<p align="center">
|
|
49
|
+
<img src="https://raw.githubusercontent.com/melissa-colin/annotheca/main/docs/reader.png" alt="Reading a paper with highlights and a sticky note" width="900">
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
Annotheca needs Python 3.10 or newer. Install it as a command with
|
|
55
|
+
[uv](https://docs.astral.sh/uv/) or [pipx](https://pipx.pypa.io/):
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
uv tool install annotheca
|
|
59
|
+
# or
|
|
60
|
+
pipx install annotheca
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Then, to let Claude use it (optional, see [AI assistants](#ai-assistants)):
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
annotheca connect
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Update later with `uv tool upgrade annotheca` (or `pipx upgrade annotheca`).
|
|
70
|
+
|
|
71
|
+
## Use
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
annotheca
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The interface opens in your browser at <http://127.0.0.1:8737>. Running the
|
|
78
|
+
command again while Annotheca is running simply reopens it.
|
|
79
|
+
|
|
80
|
+
| Option or command | Effect |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| `--port PORT` | Serve on another local port (default `8737`). |
|
|
83
|
+
| `--data-dir DIR` | Keep the library somewhere else. |
|
|
84
|
+
| `--no-browser` | Do not open the browser. |
|
|
85
|
+
| `connect` | Make Annotheca available to Claude (see below). |
|
|
86
|
+
| `mcp` | Serve the tools for AI assistants over stdio (used by `connect`). |
|
|
87
|
+
|
|
88
|
+
Tips:
|
|
89
|
+
|
|
90
|
+
- `Ctrl+Enter` adds what you pasted.
|
|
91
|
+
- Click a citation key to copy `\cite{key}`.
|
|
92
|
+
- Drop a PDF file on a paper to attach it.
|
|
93
|
+
- In the reader, select text to highlight it; switch to *Sticky note* and
|
|
94
|
+
click on the page to pin a note; click a highlight to recolour or delete it;
|
|
95
|
+
`Esc` goes back.
|
|
96
|
+
|
|
97
|
+
## AI assistants
|
|
98
|
+
|
|
99
|
+
When an assistant writes references itself, it can get a title, an author or a
|
|
100
|
+
year wrong. With Annotheca, the assistant only says *which* paper it means (a
|
|
101
|
+
DOI, an arXiv ID, a citation); Annotheca fetches the metadata from doi.org,
|
|
102
|
+
arXiv and Crossref, files the paper with its PDF, and answers with the exact
|
|
103
|
+
citation key, title and authors to use. Every paper an assistant adds is marked
|
|
104
|
+
with its name, so you can always see, and filter, what it relied on.
|
|
105
|
+
|
|
106
|
+
`annotheca connect` registers Annotheca as an
|
|
107
|
+
[MCP](https://modelcontextprotocol.io) server for **Claude Code** (in every
|
|
108
|
+
project) and **Claude Desktop** when they are installed, and prints the
|
|
109
|
+
configuration to paste into other MCP clients (Cursor, Codex, Gemini CLI...).
|
|
110
|
+
The assistant then just needs to be asked:
|
|
111
|
+
|
|
112
|
+
> Find recent papers on contact-implicit trajectory optimisation and add the
|
|
113
|
+
> three most relevant to the SoA of my PhysProx project, with a note on each.
|
|
114
|
+
|
|
115
|
+
> What did I highlight in posa2013direct? Write the related-work paragraph
|
|
116
|
+
> citing the papers of the Method folder.
|
|
117
|
+
|
|
118
|
+
| Tool | What the assistant can do |
|
|
119
|
+
| --- | --- |
|
|
120
|
+
| `list_projects` | See the projects and their folders. |
|
|
121
|
+
| `create_project`, `create_folder` | Create them. |
|
|
122
|
+
| `add_references` | File papers from DOIs, arXiv IDs, BibTeX or citations, with a note; get back verified metadata. |
|
|
123
|
+
| `list_papers` | List the papers of a project with your notes. |
|
|
124
|
+
| `read_paper` | Read a paper's BibTeX, your notes, highlights and sticky notes, and the path of its PDF. |
|
|
125
|
+
| `add_note` | Add a signed paragraph to a paper's notes. |
|
|
126
|
+
| `get_bibtex` | Get the project's `.bib`. |
|
|
127
|
+
|
|
128
|
+
Nothing else is needed: the server explains itself to the assistant. Papers it
|
|
129
|
+
adds appear in the interface as soon as you come back to the window.
|
|
130
|
+
|
|
131
|
+
## Your data
|
|
132
|
+
|
|
133
|
+
Everything stays on your machine, in `~/.local/share/annotheca` (or
|
|
134
|
+
`$XDG_DATA_HOME/annotheca`, or `$ANNOTHECA_HOME`):
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
library.db SQLite database: projects, folders, BibTeX, notes, annotations
|
|
138
|
+
pdfs/ one PDF per paper
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Back it up by copying that directory. The only network requests are the
|
|
142
|
+
lookups of references and PDFs, sent to doi.org, arXiv, Crossref, Semantic
|
|
143
|
+
Scholar, OpenAlex and the repositories they point to.
|
|
144
|
+
|
|
145
|
+
<p align="center">
|
|
146
|
+
<img src="https://raw.githubusercontent.com/melissa-colin/annotheca/main/docs/library-dark.png" alt="Dark theme" width="700">
|
|
147
|
+
</p>
|
|
148
|
+
|
|
149
|
+
## Development
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
git clone https://github.com/melissa-colin/annotheca
|
|
153
|
+
cd annotheca
|
|
154
|
+
uv venv && uv pip install -e '.[test]'
|
|
155
|
+
.venv/bin/pytest
|
|
156
|
+
.venv/bin/annotheca --data-dir /tmp/annotheca-dev
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The code follows the Google Python and JavaScript style guides. The backend is
|
|
160
|
+
a small [FastAPI](https://fastapi.tiangolo.com/) app over SQLite
|
|
161
|
+
(`src/annotheca`); the interface is dependency-free JavaScript modules with
|
|
162
|
+
[pdf.js](https://mozilla.github.io/pdf.js/) vendored, so there is no build
|
|
163
|
+
step.
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
src/annotheca/
|
|
167
|
+
cli.py command line entry point
|
|
168
|
+
api.py HTTP API and static files
|
|
169
|
+
library.py storage: projects, folders, papers, placements, annotations
|
|
170
|
+
bundle.py .annotheca files to share a project
|
|
171
|
+
mcp_server.py tools for AI assistants (Model Context Protocol)
|
|
172
|
+
models.py validated shapes shared by the API and the bundles
|
|
173
|
+
bibtex.py BibTeX parsing, formatting, citation keys, identifiers
|
|
174
|
+
sources.py doi.org, arXiv, Crossref, Semantic Scholar, OpenAlex lookups
|
|
175
|
+
static/ web interface
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
MIT. pdf.js is distributed under the Apache License 2.0
|
|
181
|
+
(`src/annotheca/static/vendor/pdfjs/LICENSE`).
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "annotheca"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A local, bilingual reference manager for research projects: BibTeX, PDFs, highlights and sticky notes."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Melissa Colin" }]
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
keywords = ["bibtex", "bibliography", "research", "pdf", "annotation", "reference-manager"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Environment :: Web Environment",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Scientific/Engineering",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"fastapi>=0.110",
|
|
23
|
+
"httpx>=0.27",
|
|
24
|
+
"mcp>=2,<3",
|
|
25
|
+
"uvicorn>=0.29",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
test = ["pytest>=8"]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
annotheca = "annotheca.cli:main"
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/melissa-colin/annotheca"
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""HTTP API and static web interface."""
|
|
2
|
+
|
|
3
|
+
import mimetypes
|
|
4
|
+
import pathlib
|
|
5
|
+
from typing import Any
|
|
6
|
+
from urllib import parse
|
|
7
|
+
|
|
8
|
+
import fastapi
|
|
9
|
+
import httpx
|
|
10
|
+
import pydantic
|
|
11
|
+
from fastapi import responses
|
|
12
|
+
from fastapi import staticfiles
|
|
13
|
+
|
|
14
|
+
from annotheca import bundle
|
|
15
|
+
from annotheca import library as library_lib
|
|
16
|
+
from annotheca import models
|
|
17
|
+
from annotheca import sources
|
|
18
|
+
|
|
19
|
+
_STATIC_DIR = pathlib.Path(__file__).parent / 'static'
|
|
20
|
+
|
|
21
|
+
# Exception types mapped to an HTTP status and the error code the
|
|
22
|
+
# interface translates for the user.
|
|
23
|
+
_ERRORS = {
|
|
24
|
+
library_lib.NotFoundError: (404, 'not_found'),
|
|
25
|
+
library_lib.ConflictError: (409, 'name_taken'),
|
|
26
|
+
library_lib.InvalidPdfError: (422, 'not_pdf'),
|
|
27
|
+
bundle.InvalidBundleError: (422, 'not_bundle'),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class NameBody(pydantic.BaseModel):
|
|
32
|
+
name: models.Name
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AddPapersBody(pydantic.BaseModel):
|
|
36
|
+
"""Either pasted references or an already known paper to file."""
|
|
37
|
+
text: str | None = None
|
|
38
|
+
paper_id: int | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class NotesBody(pydantic.BaseModel):
|
|
42
|
+
notes: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AnnotationChanges(pydantic.BaseModel):
|
|
46
|
+
color: models.Color | None = None
|
|
47
|
+
geometry: list[Any] | None = None
|
|
48
|
+
body: str | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def create_app(library: library_lib.Library,
|
|
52
|
+
client: httpx.Client | None = None) -> fastapi.FastAPI:
|
|
53
|
+
"""Builds the web application.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
library: Where everything is stored.
|
|
57
|
+
client: HTTP client for the scholarly sources; a default one is
|
|
58
|
+
created when omitted.
|
|
59
|
+
"""
|
|
60
|
+
client = client or sources.make_client()
|
|
61
|
+
app = fastapi.FastAPI(title='Annotheca', docs_url=None, redoc_url=None)
|
|
62
|
+
api = fastapi.APIRouter(prefix='/api')
|
|
63
|
+
|
|
64
|
+
for error_type, (status, code) in _ERRORS.items():
|
|
65
|
+
app.add_exception_handler(error_type, _error_handler(status, code))
|
|
66
|
+
|
|
67
|
+
@app.exception_handler(sources.ResolveError)
|
|
68
|
+
def _resolve_error(request, error):
|
|
69
|
+
del request # Unused.
|
|
70
|
+
return responses.JSONResponse({'detail': str(error)}, status_code=422)
|
|
71
|
+
|
|
72
|
+
@api.get('/projects')
|
|
73
|
+
def list_projects():
|
|
74
|
+
return library.list_projects()
|
|
75
|
+
|
|
76
|
+
@api.post('/projects', status_code=201)
|
|
77
|
+
def create_project(body: NameBody):
|
|
78
|
+
return library.create_project(body.name)
|
|
79
|
+
|
|
80
|
+
@api.get('/projects/{project_id}')
|
|
81
|
+
def get_project(project_id: int):
|
|
82
|
+
return library.get_project(project_id)
|
|
83
|
+
|
|
84
|
+
@api.get('/projects/{project_id}/bibtex')
|
|
85
|
+
def export_bibtex(project_id: int):
|
|
86
|
+
name, content = library.export_bibtex(project_id)
|
|
87
|
+
return _download(content, f'{name}.bib', 'application/x-bibtex')
|
|
88
|
+
|
|
89
|
+
@api.get('/projects/{project_id}/bundle')
|
|
90
|
+
def export_bundle(project_id: int):
|
|
91
|
+
"""The project with its notes, annotations and PDFs, to share."""
|
|
92
|
+
name, content = bundle.export_project(library, project_id)
|
|
93
|
+
return _download(content, f'{name}.annotheca', 'application/zip')
|
|
94
|
+
|
|
95
|
+
@api.post('/projects/import', status_code=201)
|
|
96
|
+
async def import_bundle(request: fastapi.Request):
|
|
97
|
+
return bundle.import_project(library, await request.body())
|
|
98
|
+
|
|
99
|
+
@api.post('/projects/{project_id}/folders', status_code=201)
|
|
100
|
+
def create_folder(project_id: int, body: NameBody):
|
|
101
|
+
return library.create_folder(project_id, body.name)
|
|
102
|
+
|
|
103
|
+
@api.patch('/projects/{project_id}', status_code=204)
|
|
104
|
+
def rename_project(project_id: int, body: NameBody):
|
|
105
|
+
library.rename('project', project_id, body.name)
|
|
106
|
+
|
|
107
|
+
@api.delete('/projects/{project_id}', status_code=204)
|
|
108
|
+
def delete_project(project_id: int):
|
|
109
|
+
library.delete('project', project_id)
|
|
110
|
+
|
|
111
|
+
@api.patch('/folders/{folder_id}', status_code=204)
|
|
112
|
+
def rename_folder(folder_id: int, body: NameBody):
|
|
113
|
+
library.rename('folder', folder_id, body.name)
|
|
114
|
+
|
|
115
|
+
@api.delete('/folders/{folder_id}', status_code=204)
|
|
116
|
+
def delete_folder(folder_id: int):
|
|
117
|
+
library.delete('folder', folder_id)
|
|
118
|
+
|
|
119
|
+
@api.post('/folders/{folder_id}/papers')
|
|
120
|
+
def add_papers(folder_id: int, body: AddPapersBody):
|
|
121
|
+
"""Files pasted references, or a known paper, in a folder."""
|
|
122
|
+
if body.paper_id is not None:
|
|
123
|
+
paper_ids = [body.paper_id]
|
|
124
|
+
elif body.text and body.text.strip():
|
|
125
|
+
entries = sources.resolve(body.text, client)
|
|
126
|
+
paper_ids = list(dict.fromkeys(map(library.add_paper, entries)))
|
|
127
|
+
else:
|
|
128
|
+
raise sources.ResolveError('unresolvable')
|
|
129
|
+
added = library.file_papers(folder_id, paper_ids)
|
|
130
|
+
return {'paper_ids': paper_ids, 'added': added,
|
|
131
|
+
'already_there': len(paper_ids) - added}
|
|
132
|
+
|
|
133
|
+
@api.put('/placements/{placement_id}/notes', status_code=204)
|
|
134
|
+
def set_notes(placement_id: int, body: NotesBody):
|
|
135
|
+
library.set_notes(placement_id, body.notes)
|
|
136
|
+
|
|
137
|
+
@api.delete('/placements/{placement_id}', status_code=204)
|
|
138
|
+
def remove_placement(placement_id: int):
|
|
139
|
+
library.remove_placement(placement_id)
|
|
140
|
+
|
|
141
|
+
@api.get('/papers/{paper_id}/pdf')
|
|
142
|
+
def get_pdf(paper_id: int):
|
|
143
|
+
path = library.pdf_path(paper_id)
|
|
144
|
+
if not path.exists():
|
|
145
|
+
raise library_lib.NotFoundError
|
|
146
|
+
return responses.FileResponse(path, media_type='application/pdf')
|
|
147
|
+
|
|
148
|
+
@api.put('/papers/{paper_id}/pdf', status_code=204)
|
|
149
|
+
async def upload_pdf(paper_id: int, request: fastapi.Request):
|
|
150
|
+
library.save_pdf(paper_id, await request.body())
|
|
151
|
+
|
|
152
|
+
@api.post('/papers/{paper_id}/pdf/fetch')
|
|
153
|
+
def fetch_pdf(paper_id: int):
|
|
154
|
+
"""Looks for an open-access PDF; reports whether one was found."""
|
|
155
|
+
pdf = sources.find_pdf(library.get_paper(paper_id), client)
|
|
156
|
+
if pdf:
|
|
157
|
+
library.save_pdf(paper_id, pdf)
|
|
158
|
+
return {'found': pdf is not None}
|
|
159
|
+
|
|
160
|
+
@api.get('/placements/{placement_id}/annotations')
|
|
161
|
+
def list_annotations(placement_id: int):
|
|
162
|
+
return library.list_annotations(placement_id)
|
|
163
|
+
|
|
164
|
+
@api.post('/placements/{placement_id}/annotations', status_code=201)
|
|
165
|
+
def create_annotation(placement_id: int, body: models.Annotation):
|
|
166
|
+
return library.create_annotation(placement_id, body.model_dump())
|
|
167
|
+
|
|
168
|
+
@api.patch('/annotations/{annotation_id}')
|
|
169
|
+
def update_annotation(annotation_id: int, body: AnnotationChanges):
|
|
170
|
+
return library.update_annotation(
|
|
171
|
+
annotation_id, body.model_dump(exclude_none=True))
|
|
172
|
+
|
|
173
|
+
@api.delete('/annotations/{annotation_id}', status_code=204)
|
|
174
|
+
def delete_annotation(annotation_id: int):
|
|
175
|
+
library.delete_annotation(annotation_id)
|
|
176
|
+
|
|
177
|
+
app.include_router(api)
|
|
178
|
+
mimetypes.add_type('text/javascript', '.mjs')
|
|
179
|
+
app.mount('/', staticfiles.StaticFiles(directory=_STATIC_DIR, html=True))
|
|
180
|
+
return app
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _download(content: str | bytes, filename: str,
|
|
184
|
+
media_type: str) -> responses.Response:
|
|
185
|
+
disposition = f"attachment; filename*=UTF-8''{parse.quote(filename)}"
|
|
186
|
+
return responses.Response(content, media_type=media_type,
|
|
187
|
+
headers={'Content-Disposition': disposition})
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _error_handler(status: int, code: str):
|
|
191
|
+
def handle(request: fastapi.Request, error: Exception):
|
|
192
|
+
del request, error # Unused.
|
|
193
|
+
return responses.JSONResponse({'detail': code}, status_code=status)
|
|
194
|
+
return handle
|