pagecite 2.0.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.
- pagecite-2.0.0/.env.example +11 -0
- pagecite-2.0.0/.github/workflows/ci.yml +20 -0
- pagecite-2.0.0/.github/workflows/publish.yml +30 -0
- pagecite-2.0.0/.gitignore +12 -0
- pagecite-2.0.0/LICENSE +21 -0
- pagecite-2.0.0/PKG-INFO +201 -0
- pagecite-2.0.0/README.md +176 -0
- pagecite-2.0.0/docs/demo.tape +23 -0
- pagecite-2.0.0/pagecite/__init__.py +3 -0
- pagecite-2.0.0/pagecite/__main__.py +203 -0
- pagecite-2.0.0/pagecite/ask.py +125 -0
- pagecite-2.0.0/pagecite/config.py +35 -0
- pagecite-2.0.0/pagecite/demo.py +82 -0
- pagecite-2.0.0/pagecite/library.py +27 -0
- pagecite-2.0.0/pagecite/mcp_server.py +105 -0
- pagecite-2.0.0/pagecite/pdf.py +80 -0
- pagecite-2.0.0/pagecite/render.py +108 -0
- pagecite-2.0.0/pagecite/store.py +120 -0
- pagecite-2.0.0/pagecite/summarize.py +204 -0
- pagecite-2.0.0/pagecite/verify.py +131 -0
- pagecite-2.0.0/pyproject.toml +43 -0
- pagecite-2.0.0/tests/__init__.py +0 -0
- pagecite-2.0.0/tests/conftest.py +122 -0
- pagecite-2.0.0/tests/test_ask.py +98 -0
- pagecite-2.0.0/tests/test_cli.py +187 -0
- pagecite-2.0.0/tests/test_demo.py +18 -0
- pagecite-2.0.0/tests/test_mcp.py +97 -0
- pagecite-2.0.0/tests/test_pdf.py +126 -0
- pagecite-2.0.0/tests/test_render.py +88 -0
- pagecite-2.0.0/tests/test_sdk_wire.py +70 -0
- pagecite-2.0.0/tests/test_store.py +108 -0
- pagecite-2.0.0/tests/test_summarize.py +146 -0
- pagecite-2.0.0/tests/test_verify.py +178 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Copy to ~/.pagecite/.env (or a .env in the folder you run from).
|
|
2
|
+
|
|
3
|
+
# Needed for `summarize` and `ask` (or log in once with `ant auth login`).
|
|
4
|
+
ANTHROPIC_API_KEY=sk-ant-...
|
|
5
|
+
|
|
6
|
+
# Write every summary into this folder as a Markdown note (e.g. a folder in your Obsidian vault).
|
|
7
|
+
PAGECITE_NOTES_DIR=
|
|
8
|
+
|
|
9
|
+
# Tuning
|
|
10
|
+
PAGECITE_MODEL=claude-opus-5-5
|
|
11
|
+
PAGECITE_DB=~/.pagecite/library.db
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.10", "3.12", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v5
|
|
16
|
+
- uses: actions/setup-python@v6
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python-version }}
|
|
19
|
+
- run: pip install -e '.[dev]'
|
|
20
|
+
- run: pytest -q
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Push a version tag (e.g. v2.0.0) to test, build and publish to PyPI via Trusted Publishing (no tokens).
|
|
2
|
+
# The run finishes green only once pip can install the new version.
|
|
3
|
+
name: Publish to PyPI
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
tags: ["v*"]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
publish:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
environment: pypi
|
|
13
|
+
permissions:
|
|
14
|
+
id-token: write # lets PyPI verify this workflow instead of a stored token
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v5
|
|
17
|
+
- uses: actions/setup-python@v6
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
- run: pip install -e '.[dev]' build
|
|
21
|
+
- run: pytest -q
|
|
22
|
+
- run: python -m build
|
|
23
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
24
|
+
- name: Wait until pip can install it # PyPI's CDN can serve the old index for a few minutes after upload
|
|
25
|
+
run: |
|
|
26
|
+
for i in $(seq 30); do
|
|
27
|
+
pip download --no-deps --no-cache-dir -q -d /tmp/check "pagecite==${GITHUB_REF_NAME#v}" && exit 0
|
|
28
|
+
sleep 10
|
|
29
|
+
done
|
|
30
|
+
exit 1
|
pagecite-2.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Owen Alderson
|
|
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.
|
pagecite-2.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pagecite
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Summarise research PDFs where every claim cites its page, and every quote and number is checked against the PDF text. CLI + MCP server, Obsidian notes.
|
|
5
|
+
Project-URL: Homepage, https://github.com/owen-alderson/pagecite
|
|
6
|
+
Author: Owen Alderson
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: citations,claude,mcp,obsidian,pdf,research,summarization
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
13
|
+
Classifier: Topic :: Text Processing
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: anthropic>=1.11
|
|
16
|
+
Requires-Dist: pymupdf>=1.24
|
|
17
|
+
Requires-Dist: truststore>=0.10
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: mcp>=2.3; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
22
|
+
Provides-Extra: mcp
|
|
23
|
+
Requires-Dist: mcp>=2.3; extra == 'mcp'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# pagecite
|
|
27
|
+
|
|
28
|
+
**Summaries of research PDFs you can actually trust: every claim cites its page, and every quote
|
|
29
|
+
and number is checked against the PDF before you see it.**
|
|
30
|
+
|
|
31
|
+
Point it at a paper, an analyst report or a pitch deck (a file, a folder or a URL). Claude writes
|
|
32
|
+
the TL;DR, key arguments, data points, quotes worth keeping and open questions. Then pagecite
|
|
33
|
+
checks each one against the PDF's own text, with no model involved: is the quote really on the
|
|
34
|
+
cited page? Do the numbers appear there? Anything that fails is kept but marked **⚠ not
|
|
35
|
+
verified**, with the reason, so a wrong page or a mangled figure never passes as fact.
|
|
36
|
+
|
|
37
|
+
[](https://github.com/owen-alderson/pagecite/actions/workflows/ci.yml)
|
|
38
|
+
[](https://pypi.org/project/pagecite/)
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+

|
|
42
|
+
|
|
43
|
+
<sub>`pagecite demo`: a fictional report and a sample summary bundled with pagecite (no API call),
|
|
44
|
+
with two claims deliberately wrong. The checks are the real ones.</sub>
|
|
45
|
+
|
|
46
|
+
## Why
|
|
47
|
+
|
|
48
|
+
AI summaries read well and are hard to audit. The usual failure isn't a summary that's obviously
|
|
49
|
+
wrong. It's a plausible sentence with a number that drifted ("27%" where the report says 22%), or a
|
|
50
|
+
quote that's real but from a different section than the claim it supports. You only catch those
|
|
51
|
+
by rereading the document, which is what the summary was supposed to save you.
|
|
52
|
+
|
|
53
|
+
pagecite splits the work:
|
|
54
|
+
|
|
55
|
+
| | Who | What |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| **Writes** | Claude (`claude-opus-5-5`) | Reads the PDF page by page and returns a structured summary. Every argument and data point carries its page numbers and a verbatim quote. |
|
|
58
|
+
| **Checks** | Plain code, [`verify.py`](pagecite/verify.py) | Finds each quote on its cited page and each number in the data points on that page. No model, so no second opinion that can also be wrong. |
|
|
59
|
+
| **Decides** | You | Verified claims are marked ✓. Flagged ones stay in, marked ⚠ with the reason ("quote is on p. 7, not the cited p. 5", "27 not on the cited page"). |
|
|
60
|
+
|
|
61
|
+
## What it does
|
|
62
|
+
|
|
63
|
+
- **Any PDF source:** a file, every PDF in a folder, or an `http(s)` URL.
|
|
64
|
+
- **Checked summary:** TL;DR, key arguments, data points, quotes worth keeping, open questions and
|
|
65
|
+
tags, each with page numbers. Quotes and figures are verified; open questions and the TL;DR show
|
|
66
|
+
their pages but have nothing quotable to check.
|
|
67
|
+
- **Obsidian notes:** `-o ~/Vault/Papers` writes each summary as a Markdown note with YAML
|
|
68
|
+
frontmatter (source, pages, model, verified/flagged counts, tags) and the flags inline.
|
|
69
|
+
- **A library:** every PDF you read is stored with its page text, keyed by its SHA-256. Summarising
|
|
70
|
+
the same file again (even under another name) costs nothing. `pagecite search` runs full-text
|
|
71
|
+
search over every page you've read.
|
|
72
|
+
- **Ask with citations:** `pagecite ask "what drives the cost gap?" --doc 3` answers from the
|
|
73
|
+
document with [citations](https://platform.claude.com/docs/en/build-with-claude/search-results)
|
|
74
|
+
returned by the API itself, so every sentence points to a page. For one document all its pages
|
|
75
|
+
are sent with a cache breakpoint, so follow-up questions read them from the prompt cache. Without
|
|
76
|
+
`--doc`, the best-matching pages across your library are sent.
|
|
77
|
+
- **MCP server:** Claude Desktop, Claude Code or any MCP client can summarise PDFs, search your
|
|
78
|
+
library, read pages and ask cited questions.
|
|
79
|
+
|
|
80
|
+
## Quickstart
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pipx install pagecite
|
|
84
|
+
pagecite demo # a checked summary on a fictional report, no key needed
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
With your own documents:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
mkdir -p ~/.pagecite && echo 'ANTHROPIC_API_KEY=sk-ant-...' >> ~/.pagecite/.env # or `ant auth login`
|
|
91
|
+
|
|
92
|
+
pagecite summarize paper.pdf
|
|
93
|
+
pagecite summarize ~/Downloads/reports -o ~/Obsidian/Papers # a whole folder, as notes
|
|
94
|
+
pagecite summarize https://arxiv.org/pdf/1706.03762
|
|
95
|
+
pagecite ask "what did they measure, and on what data?" --doc 1
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
| Command | |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `pagecite summarize SOURCE… [-o DIR] [--refresh]` | Summarise files, folders or URLs (alias `sum`) |
|
|
101
|
+
| `pagecite ask QUESTION [--doc ID]` | Answer from one document or the whole library, with page citations |
|
|
102
|
+
| `pagecite list` | The PDFs in your library (alias `ls`) |
|
|
103
|
+
| `pagecite show ID [--markdown]` | Print a stored summary, or its Obsidian note |
|
|
104
|
+
| `pagecite search QUERY [--doc ID]` | Full-text search over every page |
|
|
105
|
+
| `pagecite mcp` | Run the MCP server over stdio |
|
|
106
|
+
| `pagecite demo` | The demo above |
|
|
107
|
+
|
|
108
|
+
## What counts as verified
|
|
109
|
+
|
|
110
|
+
[`verify.py`](pagecite/verify.py) is short and has no dependencies, so the rules are easy to check:
|
|
111
|
+
|
|
112
|
+
- **Quotes** must appear on the page(s) they cite. Matching ignores case, punctuation, spacing,
|
|
113
|
+
ligatures (`fi`) and words split across lines (`con-`/`straint`), but keeps decimal points, so
|
|
114
|
+
"4.2" never matches "42". An ellipsis splits a quote into parts that must appear in order.
|
|
115
|
+
Quotes under four words are flagged as too short to prove anything.
|
|
116
|
+
- **Numbers** in a data point's value must all appear on the cited page: "USD 115 per kWh, down
|
|
117
|
+
14%" needs both 115 and 14 there. Thousands separators are normalised (`3,200` = `3200` =
|
|
118
|
+
`3 200`).
|
|
119
|
+
- **Pages** must exist in the document.
|
|
120
|
+
- When a quote is on a different page, the flag says where it really is. pagecite never moves
|
|
121
|
+
the citation for you.
|
|
122
|
+
|
|
123
|
+
What it can't check: whether a sentence in Claude's own words fairly represents its quote.
|
|
124
|
+
That's why each claim shows the quote right under it.
|
|
125
|
+
|
|
126
|
+
Scanned PDFs without a text layer are refused (OCR them first). Documents too long for one pass
|
|
127
|
+
are refused rather than silently truncated.
|
|
128
|
+
|
|
129
|
+
### What leaves your machine
|
|
130
|
+
|
|
131
|
+
| | Sent to Anthropic |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `summarize` | the extracted text of the whole PDF |
|
|
134
|
+
| `ask --doc ID` | every page of that document |
|
|
135
|
+
| `ask` | only the best-matching pages from your library |
|
|
136
|
+
| `list`, `show`, `search`, `demo` | nothing |
|
|
137
|
+
|
|
138
|
+
Your library is one SQLite file, `~/.pagecite/library.db`.
|
|
139
|
+
|
|
140
|
+
## MCP
|
|
141
|
+
|
|
142
|
+
Add to Claude Desktop's `claude_desktop_config.json` (or `claude mcp add pagecite -- pagecite mcp`
|
|
143
|
+
in Claude Code):
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"mcpServers": {
|
|
148
|
+
"pagecite": { "command": "pagecite", "args": ["mcp"] }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Install with `pipx install 'pagecite[mcp]'`. Tools: `summarize_pdf` (returns each claim with its
|
|
154
|
+
check result), `ask_library`, `search`, `read` (page text) and `documents`. `search`, `read` and
|
|
155
|
+
`documents` need no API key.
|
|
156
|
+
|
|
157
|
+
## Configuration
|
|
158
|
+
|
|
159
|
+
In `~/.pagecite/.env` or the environment (see [`.env.example`](.env.example)):
|
|
160
|
+
|
|
161
|
+
| Variable | Default | |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| `ANTHROPIC_API_KEY` | | Or log in once with `ant auth login` |
|
|
164
|
+
| `PAGECITE_NOTES_DIR` | | Always write notes here (e.g. a folder in your vault) |
|
|
165
|
+
| `PAGECITE_MODEL` | `claude-opus-5-5` | |
|
|
166
|
+
| `PAGECITE_DB` | `~/.pagecite/library.db` | |
|
|
167
|
+
|
|
168
|
+
## How it works
|
|
169
|
+
|
|
170
|
+
```mermaid
|
|
171
|
+
flowchart LR
|
|
172
|
+
In[file · folder · URL] --> PDF[pdf.py<br/>page texts + SHA-256]
|
|
173
|
+
PDF --> Lib[(library.db<br/>pages, FTS5)]
|
|
174
|
+
Lib -->|new file| Sum[summarize.py<br/>Claude: structured summary]
|
|
175
|
+
Sum --> Lib
|
|
176
|
+
Lib --> Verify[verify.py<br/>quotes + numbers vs page text]
|
|
177
|
+
Verify --> Out[terminal · Obsidian note · MCP]
|
|
178
|
+
Lib --> Ask[ask.py<br/>search-result blocks, cited answers]
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Verification runs every time a summary is shown, against the stored page text, so a stored
|
|
182
|
+
summary never carries a stale result.
|
|
183
|
+
|
|
184
|
+
## Development
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
git clone https://github.com/owen-alderson/pagecite && cd pagecite
|
|
188
|
+
python -m venv .venv && . .venv/bin/activate
|
|
189
|
+
pip install -e '.[dev]'
|
|
190
|
+
pytest # no network: Claude and HTTP are faked, test PDFs are generated
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Releases: push a `vX.Y.Z` tag and GitHub Actions publishes to PyPI. Re-record the demo with
|
|
194
|
+
`vhs docs/demo.tape`.
|
|
195
|
+
|
|
196
|
+
The original Streamlit app (pdf-research-summarizer) is kept at tag
|
|
197
|
+
[`v1-streamlit`](https://github.com/owen-alderson/pagecite/tree/v1-streamlit).
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT
|
pagecite-2.0.0/README.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# pagecite
|
|
2
|
+
|
|
3
|
+
**Summaries of research PDFs you can actually trust: every claim cites its page, and every quote
|
|
4
|
+
and number is checked against the PDF before you see it.**
|
|
5
|
+
|
|
6
|
+
Point it at a paper, an analyst report or a pitch deck (a file, a folder or a URL). Claude writes
|
|
7
|
+
the TL;DR, key arguments, data points, quotes worth keeping and open questions. Then pagecite
|
|
8
|
+
checks each one against the PDF's own text, with no model involved: is the quote really on the
|
|
9
|
+
cited page? Do the numbers appear there? Anything that fails is kept but marked **⚠ not
|
|
10
|
+
verified**, with the reason, so a wrong page or a mangled figure never passes as fact.
|
|
11
|
+
|
|
12
|
+
[](https://github.com/owen-alderson/pagecite/actions/workflows/ci.yml)
|
|
13
|
+
[](https://pypi.org/project/pagecite/)
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
<sub>`pagecite demo`: a fictional report and a sample summary bundled with pagecite (no API call),
|
|
19
|
+
with two claims deliberately wrong. The checks are the real ones.</sub>
|
|
20
|
+
|
|
21
|
+
## Why
|
|
22
|
+
|
|
23
|
+
AI summaries read well and are hard to audit. The usual failure isn't a summary that's obviously
|
|
24
|
+
wrong. It's a plausible sentence with a number that drifted ("27%" where the report says 22%), or a
|
|
25
|
+
quote that's real but from a different section than the claim it supports. You only catch those
|
|
26
|
+
by rereading the document, which is what the summary was supposed to save you.
|
|
27
|
+
|
|
28
|
+
pagecite splits the work:
|
|
29
|
+
|
|
30
|
+
| | Who | What |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| **Writes** | Claude (`claude-opus-5-5`) | Reads the PDF page by page and returns a structured summary. Every argument and data point carries its page numbers and a verbatim quote. |
|
|
33
|
+
| **Checks** | Plain code, [`verify.py`](pagecite/verify.py) | Finds each quote on its cited page and each number in the data points on that page. No model, so no second opinion that can also be wrong. |
|
|
34
|
+
| **Decides** | You | Verified claims are marked ✓. Flagged ones stay in, marked ⚠ with the reason ("quote is on p. 7, not the cited p. 5", "27 not on the cited page"). |
|
|
35
|
+
|
|
36
|
+
## What it does
|
|
37
|
+
|
|
38
|
+
- **Any PDF source:** a file, every PDF in a folder, or an `http(s)` URL.
|
|
39
|
+
- **Checked summary:** TL;DR, key arguments, data points, quotes worth keeping, open questions and
|
|
40
|
+
tags, each with page numbers. Quotes and figures are verified; open questions and the TL;DR show
|
|
41
|
+
their pages but have nothing quotable to check.
|
|
42
|
+
- **Obsidian notes:** `-o ~/Vault/Papers` writes each summary as a Markdown note with YAML
|
|
43
|
+
frontmatter (source, pages, model, verified/flagged counts, tags) and the flags inline.
|
|
44
|
+
- **A library:** every PDF you read is stored with its page text, keyed by its SHA-256. Summarising
|
|
45
|
+
the same file again (even under another name) costs nothing. `pagecite search` runs full-text
|
|
46
|
+
search over every page you've read.
|
|
47
|
+
- **Ask with citations:** `pagecite ask "what drives the cost gap?" --doc 3` answers from the
|
|
48
|
+
document with [citations](https://platform.claude.com/docs/en/build-with-claude/search-results)
|
|
49
|
+
returned by the API itself, so every sentence points to a page. For one document all its pages
|
|
50
|
+
are sent with a cache breakpoint, so follow-up questions read them from the prompt cache. Without
|
|
51
|
+
`--doc`, the best-matching pages across your library are sent.
|
|
52
|
+
- **MCP server:** Claude Desktop, Claude Code or any MCP client can summarise PDFs, search your
|
|
53
|
+
library, read pages and ask cited questions.
|
|
54
|
+
|
|
55
|
+
## Quickstart
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pipx install pagecite
|
|
59
|
+
pagecite demo # a checked summary on a fictional report, no key needed
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
With your own documents:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
mkdir -p ~/.pagecite && echo 'ANTHROPIC_API_KEY=sk-ant-...' >> ~/.pagecite/.env # or `ant auth login`
|
|
66
|
+
|
|
67
|
+
pagecite summarize paper.pdf
|
|
68
|
+
pagecite summarize ~/Downloads/reports -o ~/Obsidian/Papers # a whole folder, as notes
|
|
69
|
+
pagecite summarize https://arxiv.org/pdf/1706.03762
|
|
70
|
+
pagecite ask "what did they measure, and on what data?" --doc 1
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
| Command | |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `pagecite summarize SOURCE… [-o DIR] [--refresh]` | Summarise files, folders or URLs (alias `sum`) |
|
|
76
|
+
| `pagecite ask QUESTION [--doc ID]` | Answer from one document or the whole library, with page citations |
|
|
77
|
+
| `pagecite list` | The PDFs in your library (alias `ls`) |
|
|
78
|
+
| `pagecite show ID [--markdown]` | Print a stored summary, or its Obsidian note |
|
|
79
|
+
| `pagecite search QUERY [--doc ID]` | Full-text search over every page |
|
|
80
|
+
| `pagecite mcp` | Run the MCP server over stdio |
|
|
81
|
+
| `pagecite demo` | The demo above |
|
|
82
|
+
|
|
83
|
+
## What counts as verified
|
|
84
|
+
|
|
85
|
+
[`verify.py`](pagecite/verify.py) is short and has no dependencies, so the rules are easy to check:
|
|
86
|
+
|
|
87
|
+
- **Quotes** must appear on the page(s) they cite. Matching ignores case, punctuation, spacing,
|
|
88
|
+
ligatures (`fi`) and words split across lines (`con-`/`straint`), but keeps decimal points, so
|
|
89
|
+
"4.2" never matches "42". An ellipsis splits a quote into parts that must appear in order.
|
|
90
|
+
Quotes under four words are flagged as too short to prove anything.
|
|
91
|
+
- **Numbers** in a data point's value must all appear on the cited page: "USD 115 per kWh, down
|
|
92
|
+
14%" needs both 115 and 14 there. Thousands separators are normalised (`3,200` = `3200` =
|
|
93
|
+
`3 200`).
|
|
94
|
+
- **Pages** must exist in the document.
|
|
95
|
+
- When a quote is on a different page, the flag says where it really is. pagecite never moves
|
|
96
|
+
the citation for you.
|
|
97
|
+
|
|
98
|
+
What it can't check: whether a sentence in Claude's own words fairly represents its quote.
|
|
99
|
+
That's why each claim shows the quote right under it.
|
|
100
|
+
|
|
101
|
+
Scanned PDFs without a text layer are refused (OCR them first). Documents too long for one pass
|
|
102
|
+
are refused rather than silently truncated.
|
|
103
|
+
|
|
104
|
+
### What leaves your machine
|
|
105
|
+
|
|
106
|
+
| | Sent to Anthropic |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `summarize` | the extracted text of the whole PDF |
|
|
109
|
+
| `ask --doc ID` | every page of that document |
|
|
110
|
+
| `ask` | only the best-matching pages from your library |
|
|
111
|
+
| `list`, `show`, `search`, `demo` | nothing |
|
|
112
|
+
|
|
113
|
+
Your library is one SQLite file, `~/.pagecite/library.db`.
|
|
114
|
+
|
|
115
|
+
## MCP
|
|
116
|
+
|
|
117
|
+
Add to Claude Desktop's `claude_desktop_config.json` (or `claude mcp add pagecite -- pagecite mcp`
|
|
118
|
+
in Claude Code):
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"mcpServers": {
|
|
123
|
+
"pagecite": { "command": "pagecite", "args": ["mcp"] }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Install with `pipx install 'pagecite[mcp]'`. Tools: `summarize_pdf` (returns each claim with its
|
|
129
|
+
check result), `ask_library`, `search`, `read` (page text) and `documents`. `search`, `read` and
|
|
130
|
+
`documents` need no API key.
|
|
131
|
+
|
|
132
|
+
## Configuration
|
|
133
|
+
|
|
134
|
+
In `~/.pagecite/.env` or the environment (see [`.env.example`](.env.example)):
|
|
135
|
+
|
|
136
|
+
| Variable | Default | |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| `ANTHROPIC_API_KEY` | | Or log in once with `ant auth login` |
|
|
139
|
+
| `PAGECITE_NOTES_DIR` | | Always write notes here (e.g. a folder in your vault) |
|
|
140
|
+
| `PAGECITE_MODEL` | `claude-opus-5-5` | |
|
|
141
|
+
| `PAGECITE_DB` | `~/.pagecite/library.db` | |
|
|
142
|
+
|
|
143
|
+
## How it works
|
|
144
|
+
|
|
145
|
+
```mermaid
|
|
146
|
+
flowchart LR
|
|
147
|
+
In[file · folder · URL] --> PDF[pdf.py<br/>page texts + SHA-256]
|
|
148
|
+
PDF --> Lib[(library.db<br/>pages, FTS5)]
|
|
149
|
+
Lib -->|new file| Sum[summarize.py<br/>Claude: structured summary]
|
|
150
|
+
Sum --> Lib
|
|
151
|
+
Lib --> Verify[verify.py<br/>quotes + numbers vs page text]
|
|
152
|
+
Verify --> Out[terminal · Obsidian note · MCP]
|
|
153
|
+
Lib --> Ask[ask.py<br/>search-result blocks, cited answers]
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Verification runs every time a summary is shown, against the stored page text, so a stored
|
|
157
|
+
summary never carries a stale result.
|
|
158
|
+
|
|
159
|
+
## Development
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
git clone https://github.com/owen-alderson/pagecite && cd pagecite
|
|
163
|
+
python -m venv .venv && . .venv/bin/activate
|
|
164
|
+
pip install -e '.[dev]'
|
|
165
|
+
pytest # no network: Claude and HTTP are faked, test PDFs are generated
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Releases: push a `vX.Y.Z` tag and GitHub Actions publishes to PyPI. Re-record the demo with
|
|
169
|
+
`vhs docs/demo.tape`.
|
|
170
|
+
|
|
171
|
+
The original Streamlit app (pdf-research-summarizer) is kept at tag
|
|
172
|
+
[`v1-streamlit`](https://github.com/owen-alderson/pagecite/tree/v1-streamlit).
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Re-record the README demo: `vhs docs/demo.tape` from the repo root, with pagecite installed in .venv.
|
|
2
|
+
# It runs `pagecite demo`: a fictional report and a sample summary (no API call); the checks are real.
|
|
3
|
+
Output docs/demo.gif
|
|
4
|
+
|
|
5
|
+
Set Shell zsh
|
|
6
|
+
Set FontSize 13
|
|
7
|
+
Set Width 1180
|
|
8
|
+
Set Height 680
|
|
9
|
+
Set Padding 14
|
|
10
|
+
Set Framerate 15
|
|
11
|
+
Set TypingSpeed 55ms
|
|
12
|
+
Set Theme { "background": "#000000", "foreground": "#e0e0e0" }
|
|
13
|
+
|
|
14
|
+
Hide
|
|
15
|
+
Type "export PATH=$PWD/.venv/bin:$PATH PS1='$ ' && clear"
|
|
16
|
+
Enter
|
|
17
|
+
Sleep 1s
|
|
18
|
+
Show
|
|
19
|
+
|
|
20
|
+
Type "pagecite demo"
|
|
21
|
+
Sleep 600ms
|
|
22
|
+
Enter
|
|
23
|
+
Sleep 9s
|