@tiagojct/subsub 0.6.1 → 0.7.0
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.
- package/LICENSE +21 -0
- package/README.md +200 -62
- package/THIRD_PARTY_NOTICES +55 -0
- package/dist/bench.js +60 -5
- package/dist/config.js +28 -1
- package/dist/doctor.js +23 -2
- package/dist/init.js +17 -3
- package/dist/notes.js +42 -0
- package/dist/profiles.js +15 -0
- package/dist/prompt.js +3 -0
- package/dist/roles.js +7 -1
- package/dist/subsub.js +94 -9
- package/dist/web.js +17 -3
- package/package.json +7 -3
- package/prompts/compare.md +23 -0
- package/prompts/digest.md +23 -0
- package/prompts/lit.md +87 -0
- package/prompts/request-copy.md +35 -0
- package/prompts/review.md +31 -0
- package/prompts/verify.md +18 -0
- package/roles/researcher.md +18 -5
- package/src/bench.ts +63 -4
- package/src/config.ts +39 -1
- package/src/doctor.ts +32 -2
- package/src/init.ts +33 -4
- package/src/notes.ts +46 -0
- package/src/profiles.ts +17 -0
- package/src/prompt.ts +6 -0
- package/src/roles.ts +8 -1
- package/src/subsub.ts +100 -9
- package/src/web.ts +15 -3
- package/templates/Zotero agent.md +2 -0
- package/themes/subsub-glauca-dark.json +34 -34
- package/themes/subsub-glauca-light.json +32 -32
- package/themes/subsub-try-works-dark.json +34 -34
- package/themes/subsub-try-works-light.json +34 -34
- package/web/app.css +801 -164
- package/web/app.js +78 -8
- package/web/glauca.css +51 -70
- package/web/i18n.js +36 -0
- package/web/icon.svg +12 -1
- package/web/index.html +9 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tiago Jacinto
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,105 +1,243 @@
|
|
|
1
1
|
# Sub-Sub
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@tiagojct/subsub)
|
|
4
|
+
[](https://subsub.tiagojacinto.eu)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
[](#requirements)
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
A Zotero librarian and research assistant that runs on your computer, in your browser or in your terminal. It is built on the [pi](https://pi.dev) agent and connects directly to your local Zotero library through Zotero's local API.
|
|
6
10
|
|
|
7
|
-
Sub-Sub
|
|
11
|
+
> *"This mere painstaking burrower and grub-worm of a poor devil of a Sub-Sub appears to have gone through the long Vaticans and street-stalls of the earth, picking up whatever random allusions to whales he could anyways find in any book whatsoever."*
|
|
12
|
+
> — Herman Melville, *Moby-Dick* (Extracts)
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
- The researcher reads the library, searches PubMed and OpenAlex, and writes notes in your notes folder. It cannot change the library, except to add a short linked note to an item.
|
|
14
|
+
---
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
## Highlights
|
|
17
|
+
|
|
18
|
+
* **Code-Enforced Safety Gate**: The model **cannot** modify your library without your explicit approval. Every change is calculated first as a dry-run preview (+added in green, -removed in red) and executed only when you confirm.
|
|
19
|
+
* **Full Journal & Reversible Edits**: Every library modification is recorded with a unique journal ID and can be reverted anytime using `/undo`.
|
|
20
|
+
* **Two Dedicated Roles**:
|
|
21
|
+
* 📚 **The Librarian**: Cleans tags, imports references (DOI, PMID, ISBN), deduplicates, fixes metadata, retrieves open-access PDFs, creates collections, and checks retractions. *Runs completely offline with no web search access.*
|
|
22
|
+
* 🔬 **The Researcher**: Reads items, searches PubMed and OpenAlex, finds gaps, checks citations in manuscripts, and writes structured Markdown notes in your notes vault (e.g. Obsidian). *Cannot modify library records.*
|
|
23
|
+
* **Local & Private**: Listens strictly on `127.0.0.1` protected by an authenticated single-origin session cookie. Your library contents remain on your computer.
|
|
24
|
+
* **Modern Web Interface & Terminal UI**: Use Sub-Sub in your browser with desktop shortcuts (`~/Applications/Sub-Sub.app`, Start menu, or `.desktop`), or launch it right in your terminal with adaptive color themes.
|
|
25
|
+
|
|
26
|
+
---
|
|
13
27
|
|
|
14
28
|
## Profiles
|
|
15
29
|
|
|
16
|
-
|
|
30
|
+
Profiles calibrate how much Sub-Sub drafts and executes for you. Choose your profile during `subsub init` or change it on the fly with `/profile <name>`.
|
|
17
31
|
|
|
18
|
-
| Profile |
|
|
32
|
+
| Profile | Description |
|
|
19
33
|
|---|---|
|
|
20
|
-
| Reader |
|
|
21
|
-
| Scholar |
|
|
22
|
-
| Author | Scholar
|
|
23
|
-
| Editor | Everything, including drafting manuscript
|
|
34
|
+
| **Reader** | Guided and instructional. Reading notes are quotes with page numbers and reflective questions. Never generates unrequested summaries or syntheses. No bulk library edits. |
|
|
35
|
+
| **Scholar** | Full research workflow: literature notes, conceptual syntheses, database searches, alerts, and the full librarian toolkit. |
|
|
36
|
+
| **Author** | Scholar capabilities plus manuscript workflows: citation auditing against your library, automated Pandoc/Quarto bibliographies, and argument analysis. It critiques but does not write your paragraphs. |
|
|
37
|
+
| **Editor** | Everything unlocked, including drafting full manuscript sections and abstracts upon request. |
|
|
24
38
|
|
|
25
|
-
|
|
39
|
+
> [!NOTE]
|
|
40
|
+
> A profile is your own operational baseline, not a rigid constraint. You can switch between profiles whenever your research focus shifts.
|
|
41
|
+
|
|
42
|
+
---
|
|
26
43
|
|
|
27
44
|
## Requirements
|
|
28
45
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
46
|
+
* **Zotero 10 or later** running locally. In Zotero, enable:
|
|
47
|
+
*Settings > Advanced > "Allow other applications on this computer to communicate with Zotero"*.
|
|
48
|
+
* **Node.js 22.19** or later.
|
|
49
|
+
* **[uv](https://docs.astral.sh/uv/)** fast Python package manager.
|
|
50
|
+
* **Model Provider Account**: Any provider supported by pi (e.g., OpenCode Go, OpenRouter, Anthropic, OpenAI, Mistral, DeepSeek, Google Gemini, or local Ollama).
|
|
51
|
+
* **OS**: macOS, Linux, or Windows.
|
|
52
|
+
|
|
53
|
+
---
|
|
34
54
|
|
|
35
55
|
## Install
|
|
36
56
|
|
|
37
|
-
|
|
57
|
+
### Quick Installers
|
|
58
|
+
|
|
59
|
+
Automated installers handle Node.js, uv, and desktop shortcuts:
|
|
60
|
+
|
|
61
|
+
* **macOS & Linux**:
|
|
62
|
+
```sh
|
|
63
|
+
curl -fsSL https://subsub.tiagojacinto.eu/install.sh | sh
|
|
64
|
+
```
|
|
65
|
+
* **Windows (PowerShell)**:
|
|
66
|
+
```powershell
|
|
67
|
+
powershell -ExecutionPolicy ByPass -c "irm https://subsub.tiagojacinto.eu/install.ps1 | iex"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Manual Installation
|
|
71
|
+
|
|
72
|
+
1. Install the global package:
|
|
73
|
+
```sh
|
|
74
|
+
npm install -g @tiagojct/subsub
|
|
75
|
+
```
|
|
76
|
+
2. Run setup:
|
|
77
|
+
```sh
|
|
78
|
+
subsub init
|
|
79
|
+
```
|
|
80
|
+
Answer the interactive prompts (press <kbd>Enter</kbd> to accept defaults). This creates your notes folder (`Inbox/` and `Systems/`), sets up starter tag vocabularies, and writes note templates (`Systems/Zotero agent.md`).
|
|
81
|
+
3. Verify your environment:
|
|
82
|
+
```sh
|
|
83
|
+
subsub doctor
|
|
84
|
+
```
|
|
85
|
+
Resolve any items marked with `FIX`.
|
|
86
|
+
4. Install system shortcuts:
|
|
87
|
+
```sh
|
|
88
|
+
subsub shortcut
|
|
89
|
+
```
|
|
90
|
+
Creates `~/Applications/Sub-Sub.app` on macOS, Start menu & desktop shortcuts on Windows, or an application launcher on Linux.
|
|
91
|
+
5. Launch Sub-Sub:
|
|
92
|
+
Open the shortcut or run `subsub web`. Connect your model provider via the **Model** dialog with an API key (or in the terminal via `subsub`, then `/login`).
|
|
93
|
+
|
|
94
|
+
If you already use [pi](https://pi.dev), you can add Sub-Sub as an extension:
|
|
95
|
+
```sh
|
|
96
|
+
pi install npm:@tiagojct/subsub
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The underlying Python servers ([zotero-local-mcp](https://github.com/tiagojct/zotero-local-mcp)) run via `uv` from PyPI. To run from a local checkout, specify `"serverDir": "/path/to/zotero-local-mcp"` in your configuration.
|
|
38
100
|
|
|
39
|
-
|
|
40
|
-
2. Type `subsub init`. Answer the questions. Press Enter to keep a default.
|
|
41
|
-
3. Type `subsub doctor`. Fix each line marked FIX.
|
|
42
|
-
4. Type `subsub shortcut` to add a Sub-Sub shortcut (Applications on macOS, the Start menu and the desktop on Windows, the applications menu on Linux).
|
|
43
|
-
5. Open Sub-Sub with the shortcut, or type `subsub web`. Select the model button and connect a provider with an API key (or, in the terminal, type `subsub`, then `/login`).
|
|
101
|
+
---
|
|
44
102
|
|
|
45
|
-
|
|
103
|
+
## Web View
|
|
46
104
|
|
|
47
|
-
|
|
105
|
+
Starting Sub-Sub with `subsub web` (or `subsub serve`) launches the browser application:
|
|
48
106
|
|
|
49
|
-
|
|
107
|
+
* **Approval Previews**: Shows server-computed dry-run diffs directly in the UI. Changes are applied only after clicking **Yes** or pressing <kbd>Y</kbd>.
|
|
108
|
+
* **Keyboard-First Review**: In approval dialogs, press <kbd>Y</kbd> or <kbd>Cmd</kbd>/<kbd>Ctrl</kbd>+<kbd>Enter</kbd> to approve, or <kbd>N</kbd>/<kbd>Esc</kbd> to decline.
|
|
109
|
+
* **Theme Switcher**: Header toggle between **Light**, **Dark**, and **System** themes, with tailored styling for each mode (Try-Works amber for Librarian, Glauca blue for Researcher).
|
|
110
|
+
* **One-Click Copy**: Built-in copy buttons on all code snippets, BibTeX records, and assistant literature notes.
|
|
111
|
+
* **Session Search & Markdown Export**: Filter conversation history in real time and download full discussions as Markdown notes.
|
|
112
|
+
* **Local Security**: Binds strictly to `127.0.0.1`. The initial link carries a random token that converts into an `HttpOnly`, `SameSite=Strict` cookie. Protected with strict Content Security Policy (CSP), origin validation, and sanitized output.
|
|
113
|
+
* **Auto-Idle Management**: A single daemon serves requests and stops automatically after 10 minutes of inactivity when no pages remain open. Use `--stay` to keep running indefinitely, or `--idle N` to change the timeout.
|
|
50
114
|
|
|
51
|
-
|
|
115
|
+
---
|
|
52
116
|
|
|
53
|
-
|
|
117
|
+
## Usage
|
|
118
|
+
|
|
119
|
+
### Interactive Modes & Commands
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
subsub # Start in researcher mode
|
|
123
|
+
subsub --librarian # Start in librarian mode
|
|
124
|
+
subsub web # Start browser view
|
|
125
|
+
subsub -c # Continue the previous session
|
|
126
|
+
subsub -r # Resume an older session
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Commands available within Sub-Sub:
|
|
130
|
+
|
|
131
|
+
* `/librarian`: Switch to the librarian (modify library, tag, import; offline).
|
|
132
|
+
* `/researcher`: Switch to the researcher (read library, search literature, write notes).
|
|
133
|
+
* `/profile [name]`: View or change active profile (`reader`, `scholar`, `author`, `editor`).
|
|
134
|
+
* `/subsub`: Display Zotero connection health and library statistics.
|
|
135
|
+
* `/history`: List recent library changes with journal IDs.
|
|
136
|
+
* `/undo [id]`: Revert the last change (or a specific journal ID).
|
|
137
|
+
|
|
138
|
+
### Built-in Prompt Templates
|
|
139
|
+
|
|
140
|
+
Type the command followed by arguments:
|
|
141
|
+
|
|
142
|
+
* **Librarian**:
|
|
143
|
+
* `/tag-batch [size]`: Tag untagged items using your tag vocabulary.
|
|
144
|
+
* `/clean-tags`: Find irregular tags and reconcile with your vocabulary.
|
|
145
|
+
* `/import-queue [path]`: Import pending references from an inbox note.
|
|
146
|
+
* **Researcher**:
|
|
147
|
+
* `/lit <question>`: Literature review: the library first, then PubMed, Europe PMC and OpenAlex in one merged search; reads before it summarises; an evidence table, agreement, disagreement and open questions; references checked; one note in `Research/` (Scholar, Author, Editor).
|
|
148
|
+
* `/compare <question or citekeys>`: Source matrix (claim, evidence type, caveats, confidence), agreements and disagreements (Scholar, Author, Editor).
|
|
149
|
+
* `/review <path>`: Critical review of your own manuscript: FATAL, MAJOR and MINOR issues against the reporting guideline that applies (CONSORT, STROBE, PRISMA, STARD, TRIPOD), with quotations and a revision plan. Comments only (Author, Editor).
|
|
150
|
+
* `/digest [days] [topic]`: Digest of the last days (default 7): what the new notes and alerts change, works worth reading, open questions, decisions for you (Scholar, Author, Editor).
|
|
151
|
+
* `/request-copy <citekey, DOI or PMID>`: Draft an email to the corresponding author asking for a copy of a paper that is not open access. The address comes from the PubMed record; you send the email yourself.
|
|
152
|
+
* `/lit-note <citekey>`: Generate an in-depth literature note from item fulltext.
|
|
153
|
+
* `/synthesis <topic>`: Synthesize library findings on a specific topic.
|
|
154
|
+
* `/gaps <topic>`: Identify unanswered questions and literature gaps.
|
|
155
|
+
* `/manuscript <path>`: Cross-check manuscript citations against your library.
|
|
156
|
+
* `/verify <path> [claims]`: Check that each cited work exists, matches its citation and was not retracted (needs the Starbuck add-on). With `claims`, also check whether each source supports the sentence that cites it.
|
|
157
|
+
* `/alert`: Check PubMed and OpenAlex for new publications matching research topics.
|
|
158
|
+
|
|
159
|
+
### Batch Tag Review
|
|
160
|
+
|
|
161
|
+
To preview or apply batch tag reviews generated in your `Inbox/`:
|
|
162
|
+
```sh
|
|
163
|
+
subsub review preview 13-31
|
|
164
|
+
subsub review apply 13-31
|
|
165
|
+
```
|
|
54
166
|
|
|
55
|
-
|
|
56
|
-
- The server listens only on 127.0.0.1. The address that opens the page carries a random key, which becomes an HttpOnly, SameSite=Strict cookie; other requests need that cookie, the right Host header and a same-origin JSON request. The page has a strict content security policy and shows model output without raw HTML.
|
|
57
|
-
- One server per user: a second `subsub web` opens the running one. Without an open page for 10 minutes (and with nothing waiting for approval), the server stops. `--stay` keeps it running; `--idle N` sets the minutes; `--no-open` does not open a browser; `--librarian` starts in the librarian mode.
|
|
58
|
-
- Labels are in European Portuguese when the language setting is Portuguese, otherwise in English. A theme button in the header toggles light, dark, and system themes.
|
|
59
|
-
- Fast approval shortcuts (`y` to apply, `n` to decline), one-click copy buttons on code blocks and notes, conversation search, and export to Markdown.
|
|
60
|
-
- The model button lists the models you can use and saves an API key for a provider in the same file as `/login`. Sign-ins through a provider's website stay in the terminal (`/login`).
|
|
61
|
-
- A log is kept in `~/.subsub/web.log`.
|
|
167
|
+
---
|
|
62
168
|
|
|
63
|
-
##
|
|
169
|
+
## Configuration
|
|
64
170
|
|
|
65
|
-
|
|
66
|
-
- `/profile` shows or changes the profile. `/subsub` shows the Zotero connection. `/history` lists recent library changes. `/undo` reverts one.
|
|
67
|
-
- Templates: `/tag-batch`, `/clean-tags`, `/import-queue` (librarian); `/lit-note <citekey>`, `/synthesis <topic>`, `/gaps <topic>`, `/manuscript <file>`, `/alert` (researcher).
|
|
68
|
-
- `subsub review preview 13-31` and `subsub review apply 13-31` preview or apply tag review notes by number.
|
|
69
|
-
- `subsub -c` continues the last session. `subsub -r` selects an older one.
|
|
171
|
+
`subsub init` writes settings to `~/.config/subsub/config.json` (or `$SUBSUB_CONFIG`):
|
|
70
172
|
|
|
71
|
-
|
|
173
|
+
| Setting | Type | Description | Default |
|
|
174
|
+
|---|---|---|---|
|
|
175
|
+
| `profile` | string | `reader`, `scholar`, `author`, or `editor` | `"scholar"` |
|
|
176
|
+
| `userName` | string | Name used by the agent in replies | `"the user"` |
|
|
177
|
+
| `about` | string | Brief background context about your research domain | `null` |
|
|
178
|
+
| `language` | string | Response language, or `"auto"` to match your input | `"English"` |
|
|
179
|
+
| `vault` | string | Path to your Markdown notes folder / Obsidian vault | `$ZOTERO_VAULT` |
|
|
180
|
+
| `models` | object | Model overrides per mode (e.g. `{"librarian": "opencode-go/glm-5.3-flash"}`) | Current model |
|
|
181
|
+
| `defaultMode`| string | Default launch mode (`"researcher"` or `"librarian"`) | `"researcher"` |
|
|
182
|
+
| `serverDir` | string | Local development path for `zotero-local-mcp` | PyPI package |
|
|
183
|
+
| `look` | boolean| Show header banners and status lines in terminal | `true` |
|
|
184
|
+
| `quotes` | boolean| Display quotes from *Moby-Dick* in terminal banner | `true` |
|
|
185
|
+
| `themes` | object | Mode-specific terminal themes | `true` |
|
|
186
|
+
| `addons` | array | Optional servers; `["starbuck"]` adds reference checks | `[]` |
|
|
187
|
+
| `starbuckDir` | string | Local Starbuck folder (used when it has a `pyproject.toml`) | `~/Projects/starbuck` |
|
|
72
188
|
|
|
73
|
-
|
|
189
|
+
Server-level configurations (notes folder path, tag vocabulary path, Unpaywall email) reside in `~/.config/zotero-local-mcp/env`.
|
|
74
190
|
|
|
75
|
-
|
|
76
|
-
|---|---|---|
|
|
77
|
-
| `profile` | reader, scholar, author or editor | scholar |
|
|
78
|
-
| `userName` | how Sub-Sub names you | "the user" |
|
|
79
|
-
| `about` | one line about you | none |
|
|
80
|
-
| `language` | language of replies and notes, or `auto` | English |
|
|
81
|
-
| `vault` | notes folder | `ZOTERO_VAULT` from the server settings |
|
|
82
|
-
| `models` | model per mode, e.g. `{ "librarian": "opencode-go/glm-5.3-flash" }` | pi's current model |
|
|
83
|
-
| `defaultMode` | librarian or researcher | researcher |
|
|
84
|
-
| `serverDir` | a local copy of zotero-local-mcp | the PyPI release |
|
|
85
|
-
| `look`, `quotes`, `themes` | banner, Moby-Dick line, themes per mode | on |
|
|
191
|
+
### Papers that are not open access
|
|
86
192
|
|
|
87
|
-
|
|
193
|
+
Sub-Sub finds open-access copies (Unpaywall, Europe PMC). For other papers, use Sub-Sub on your institution's network (at FMUP: the U.Porto network), where the DOI link opens the publisher's version through the library's subscriptions, or use `/request-copy` to ask the author. Sub-Sub does not use unlicensed sources.
|
|
88
194
|
|
|
89
|
-
|
|
195
|
+
### Reference checks (Starbuck add-on)
|
|
90
196
|
|
|
91
|
-
|
|
197
|
+
[Starbuck](https://github.com/tiagojct/starbuck) checks references: does each cited work exist, do the title, first author and year match, was it retracted or corrected, and (on request) does the source support the sentence that cites it.
|
|
92
198
|
|
|
93
|
-
|
|
199
|
+
1. Add the add-on to the settings file:
|
|
200
|
+
```json
|
|
201
|
+
"addons": ["starbuck"]
|
|
202
|
+
```
|
|
203
|
+
2. Optional: set `"starbuckDir"` to a local copy of Starbuck. Without it, Sub-Sub runs the pinned Starbuck release with uv.
|
|
204
|
+
3. Start Sub-Sub again and run `subsub doctor`. The line `Starbuck` must show `ok`.
|
|
205
|
+
|
|
206
|
+
The researcher gets four tools: `verify_check_manuscript`, `verify_check_references`, `verify_prepare_claims` and `verify_record_claims`. Starbuck writes its report to a `_starbuck` folder next to the manuscript. Inside the notes folder or the working folder this needs no approval; elsewhere Sub-Sub asks first. Starbuck gets the contact email from the zotero-local-mcp env file. For the claim check, Sub-Sub's own model judges; Starbuck checks that every quoted passage is in the source.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Model Evaluation (`subsub-bench`)
|
|
211
|
+
|
|
212
|
+
Evaluate how accurately different LLMs tag, import, and synthesize literature against your own library without applying changes:
|
|
94
213
|
|
|
95
214
|
```sh
|
|
215
|
+
subsub-bench prepare # Sample realistic test tasks from your library
|
|
216
|
+
subsub-bench run # Run models in parallel against safety-isolated sandbox
|
|
217
|
+
subsub-bench score # Generate objective evaluation scorecard (results.md)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Measures tag accuracy, adherence to vocabulary, hallucinated citations, token usage, cost, and latency.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Development & Testing
|
|
225
|
+
|
|
226
|
+
```sh
|
|
227
|
+
git clone https://github.com/tiagojct/subsub.git
|
|
228
|
+
cd subsub
|
|
96
229
|
npm install
|
|
97
230
|
npm test
|
|
231
|
+
npm run build
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Run end-to-end integration tests (real agent, Python MCP servers, fake Zotero, scripted LLM):
|
|
235
|
+
```sh
|
|
98
236
|
ZLM_DIR=/path/to/zotero-local-mcp npm run test:e2e
|
|
99
237
|
```
|
|
100
238
|
|
|
101
|
-
|
|
239
|
+
---
|
|
102
240
|
|
|
103
|
-
##
|
|
241
|
+
## License
|
|
104
242
|
|
|
105
|
-
MIT.
|
|
243
|
+
[MIT](LICENSE) © Tiago Jacinto. The `/lit`, `/compare` and `/review` prompts adapt text from [Feynman](https://github.com/Companion-Inc/feynman) (MIT); see [THIRD_PARTY_NOTICES](THIRD_PARTY_NOTICES).
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Third-party notices
|
|
2
|
+
|
|
3
|
+
prompts/lit.md, prompts/compare.md, prompts/review.md and the integrity rules in
|
|
4
|
+
roles/researcher.md adapt text from Feynman (prompts/lit.md, prompts/deepresearch.md,
|
|
5
|
+
prompts/compare.md, prompts/review.md and .feynman/agents/researcher.md),
|
|
6
|
+
https://github.com/Companion-Inc/feynman, commit 39f3ece. Its licence:
|
|
7
|
+
|
|
8
|
+
MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 Companion, Inc.
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
prompts/digest.md takes its section structure ("What changed in our understanding",
|
|
32
|
+
"Questions for the user") from alberto-research, src/alberto_research/digest.py,
|
|
33
|
+
https://github.com/gabriel-affonso/alberto-research. Its licence:
|
|
34
|
+
|
|
35
|
+
MIT License
|
|
36
|
+
|
|
37
|
+
Copyright (c) 2025 Gabriel Affonso
|
|
38
|
+
|
|
39
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
40
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
41
|
+
in the Software without restriction, including without limitation the rights
|
|
42
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
43
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
44
|
+
furnished to do so, subject to the following conditions:
|
|
45
|
+
|
|
46
|
+
The above copyright notice and this permission notice shall be included in all
|
|
47
|
+
copies or substantial portions of the Software.
|
|
48
|
+
|
|
49
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
50
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
51
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
52
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
53
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
54
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
55
|
+
SOFTWARE.
|
package/dist/bench.js
CHANGED
|
@@ -15,7 +15,7 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
15
15
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { dirname, join, resolve } from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
|
-
import { expand, loadConfig } from "./config.js";
|
|
18
|
+
import { expand, loadConfig, starbuckCommand, starbuckEnabled } from "./config.js";
|
|
19
19
|
import { findUv } from "./subsub.js";
|
|
20
20
|
export const PACKAGE_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
21
21
|
export const DEFAULT_MODELS = {
|
|
@@ -24,7 +24,7 @@ export const DEFAULT_MODELS = {
|
|
|
24
24
|
};
|
|
25
25
|
export const TASKS = {
|
|
26
26
|
librarian: ["tagging", "facet_fix", "import"],
|
|
27
|
-
researcher: ["lit_note", "synthesis", "search"],
|
|
27
|
+
researcher: ["lit_note", "synthesis", "search", "lit"],
|
|
28
28
|
};
|
|
29
29
|
// ---------------------------------------------------------------- prompts
|
|
30
30
|
function about(t) {
|
|
@@ -60,6 +60,12 @@ export function promptFor(task, t, code, out) {
|
|
|
60
60
|
? `Write a synthesis of what my library says about ${about(t)}, following the synthesis format in the shared rules. ` +
|
|
61
61
|
`This is a model test: save it as ${join(out, "Synthesis.md")}.`
|
|
62
62
|
: undefined;
|
|
63
|
+
case "lit":
|
|
64
|
+
// The /lit template, with the model-test paths in its arguments.
|
|
65
|
+
return t.synthesis
|
|
66
|
+
? `/lit ${about(t)}. This is a model test: write the review as ${join(out, "Lit.md")}, the plan and the screening log ` +
|
|
67
|
+
`in ${join(out, ".plans")}, nothing in Research/, and do not queue imports.`
|
|
68
|
+
: undefined;
|
|
63
69
|
case "search":
|
|
64
70
|
return t.synthesis
|
|
65
71
|
? `Find up to 5 important papers published from ${t.search.year_from} onwards on ${about(t)} that are not yet in my library. ` +
|
|
@@ -242,8 +248,30 @@ export function citekeysIn(text) {
|
|
|
242
248
|
out.add(m[1]);
|
|
243
249
|
return [...out];
|
|
244
250
|
}
|
|
245
|
-
|
|
246
|
-
|
|
251
|
+
export function starbuckScorer(cfg = loadConfig(), env = process.env) {
|
|
252
|
+
if (!starbuckEnabled(cfg))
|
|
253
|
+
return undefined;
|
|
254
|
+
const cmd = starbuckCommand(cfg, env);
|
|
255
|
+
return (file) => {
|
|
256
|
+
const claims = env.STARBUCK_JUDGE_URL && env.STARBUCK_JUDGE_MODEL ? ["--claims"] : [];
|
|
257
|
+
const args = [...cmd.args.slice(0, -1), "starbuck", "check", file, "--to", "", "--quiet", ...claims];
|
|
258
|
+
const r = spawnSync(cmd.command, args, { env, encoding: "utf8", timeout: 20 * 60_000 });
|
|
259
|
+
const report = join(dirname(file), "_starbuck", `${file.split(/[\\/]/).pop().replace(/\.[^.]+$/, "")}-references.json`);
|
|
260
|
+
if (!existsSync(report))
|
|
261
|
+
return { error: (r.stderr || r.error?.message || "no report").trim().slice(0, 300) };
|
|
262
|
+
const res = JSON.parse(readFileSync(report, "utf8"));
|
|
263
|
+
const run = res.claims_run ?? {};
|
|
264
|
+
return {
|
|
265
|
+
references: res.summary?.references_checked,
|
|
266
|
+
fail: res.summary?.fail,
|
|
267
|
+
check: res.summary?.warn,
|
|
268
|
+
uncited_sentences: res.summary?.uncited_sentences,
|
|
269
|
+
citation_recall: run.citation_recall?.value,
|
|
270
|
+
citation_precision: run.citation_precision?.value,
|
|
271
|
+
};
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
export function scoreRun(task, dir, t, index, blindRef, starbuck) {
|
|
247
275
|
const events = readEvents(dir);
|
|
248
276
|
const calls = toolCalls(events);
|
|
249
277
|
const entries = benchEntries(events);
|
|
@@ -359,6 +387,29 @@ export function scoreRun(task, dir, t, index, blindRef) {
|
|
|
359
387
|
}
|
|
360
388
|
return res;
|
|
361
389
|
}
|
|
390
|
+
if (task === "lit") {
|
|
391
|
+
const text = readFile("Lit.md") ?? "";
|
|
392
|
+
const seen = calls.map((c) => c.result).join("\n").toLowerCase();
|
|
393
|
+
const dois = [...new Set([...text.matchAll(DOI_RE)].map((m) => normDoi(m[0])))];
|
|
394
|
+
const pmids = [...new Set([...text.matchAll(PMID_RE)].map((m) => m[1]))];
|
|
395
|
+
const rows = text.split("\n").filter((l) => /^\|\s*\[?\d+\]?\s*\|/.test(l));
|
|
396
|
+
const readCol = rows.map((l) => l.split("|").map((c) => c.trim().toLowerCase())).map((c) => c.find((x) => /^(full text|abstract|metadata)$/.test(x)));
|
|
397
|
+
const plans = join(dir, ".plans");
|
|
398
|
+
return {
|
|
399
|
+
...base,
|
|
400
|
+
written: !!text,
|
|
401
|
+
words: text ? text.split(/\s+/).filter(Boolean).length : 0,
|
|
402
|
+
evidence_rows: rows.length,
|
|
403
|
+
evidence_labelled: readCol.filter(Boolean).length,
|
|
404
|
+
dois: dois.length,
|
|
405
|
+
pmids: pmids.length,
|
|
406
|
+
ungrounded: [...dois.filter((d) => !seen.includes(d)), ...pmids.filter((p) => !seen.includes(p))],
|
|
407
|
+
screening_log: existsSync(plans) && readdirSync(plans).some((f) => /screening/i.test(f)),
|
|
408
|
+
multi_searches: calls.filter((c) => c.name === "scholar_search_multi").length,
|
|
409
|
+
fulltext_reads: calls.filter((c) => c.name === "scholar_read_oa_fulltext" || c.name === "zotero_get_fulltext").length,
|
|
410
|
+
starbuck: text && starbuck ? starbuck(join(dir, "Lit.md")) : undefined,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
362
413
|
if (task === "search") {
|
|
363
414
|
const text = readFile("Search.md") ?? "";
|
|
364
415
|
const seen = calls.map((c) => c.result).join("\n").toLowerCase();
|
|
@@ -401,6 +452,9 @@ export function renderResults(rows) {
|
|
|
401
452
|
lit_note: ["written", "words", "read_fulltext", "cites_itself", "citekeys_unknown"],
|
|
402
453
|
synthesis: ["written", "words", "topic_items", "topic_items_cited", "citekeys_unknown"],
|
|
403
454
|
search: ["written", "dois", "pmids", "ungrounded", "already_in_library", "queued", "searches"],
|
|
455
|
+
lit: ["written", "words", "evidence_rows", "evidence_labelled", "dois", "pmids", "ungrounded", "screening_log",
|
|
456
|
+
"multi_searches", "fulltext_reads", "starbuck.fail", "starbuck.check", "starbuck.uncited_sentences",
|
|
457
|
+
"starbuck.citation_recall", "starbuck.citation_precision"],
|
|
404
458
|
};
|
|
405
459
|
const common = ["seconds", "tool_calls", "tool_errors", "wrong_mode", "tokens_in", "tokens_out", "cost", "error"];
|
|
406
460
|
for (const task of Object.keys(cols)) {
|
|
@@ -516,13 +570,14 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
516
570
|
const key = JSON.parse(readFileSync(join(dir, "key.json"), "utf8"));
|
|
517
571
|
const blind = existsSync(join(dir, "reference.json")) ? JSON.parse(readFileSync(join(dir, "reference.json"), "utf8")) : {};
|
|
518
572
|
const rows = [];
|
|
573
|
+
const scorer = starbuckScorer(cfg);
|
|
519
574
|
for (const code of Object.keys(key).sort()) {
|
|
520
575
|
for (const role of ["librarian", "researcher"])
|
|
521
576
|
for (const task of TASKS[role]) {
|
|
522
577
|
const d = join(dir, "runs", code, task);
|
|
523
578
|
if (!existsSync(join(d, "summary.json")))
|
|
524
579
|
continue;
|
|
525
|
-
rows.push({ model: key[code], role, task, s: scoreRun(task, d, t, index, blind) });
|
|
580
|
+
rows.push({ model: key[code], role, task, s: scoreRun(task, d, t, index, blind, scorer) });
|
|
526
581
|
}
|
|
527
582
|
}
|
|
528
583
|
writeFileSync(join(dir, "results.json"), JSON.stringify(rows, null, 1));
|
package/dist/config.js
CHANGED
|
@@ -12,13 +12,21 @@
|
|
|
12
12
|
*
|
|
13
13
|
* `subsub init` writes this file. Without "serverDir" (or when that folder does
|
|
14
14
|
* not exist), the Zotero servers run from PyPI with uv.
|
|
15
|
+
*
|
|
16
|
+
* Add-ons: "addons": ["starbuck"] starts Starbuck (reference checks) as a third
|
|
17
|
+
* server, "verify". It runs from "starbuckDir" when that folder has a
|
|
18
|
+
* pyproject.toml, otherwise from the pinned PyPI release (STARBUCK_SOURCE).
|
|
19
|
+
* `subsub init` and the web view turn it on and off.
|
|
15
20
|
*/
|
|
16
21
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
22
|
import { homedir } from "node:os";
|
|
18
23
|
import { dirname, join, resolve } from "node:path";
|
|
19
24
|
export const PROFILES = ["reader", "scholar", "author", "editor"];
|
|
20
25
|
/** The Zotero server release that this Sub-Sub version runs when there is no local server folder. */
|
|
21
|
-
export const SERVER_VERSION = "0.
|
|
26
|
+
export const SERVER_VERSION = "0.5.0";
|
|
27
|
+
/** The Starbuck release (PyPI) that this Sub-Sub version runs when there is no local Starbuck folder. */
|
|
28
|
+
export const STARBUCK_VERSION = "0.2.0";
|
|
29
|
+
export const STARBUCK_SOURCE = `starbuck==${STARBUCK_VERSION}`;
|
|
22
30
|
export function expand(p, home) {
|
|
23
31
|
const base = home ?? process.env.HOME ?? homedir();
|
|
24
32
|
return resolve(p.replace(/^~(?=$|\/)/, base));
|
|
@@ -119,6 +127,8 @@ export function loadConfig(env = process.env) {
|
|
|
119
127
|
look: user.look,
|
|
120
128
|
quotes: user.quotes,
|
|
121
129
|
themes: user.themes,
|
|
130
|
+
addons: Array.isArray(user.addons) ? user.addons.filter((a) => typeof a === "string") : [],
|
|
131
|
+
starbuckDir: expand(user.starbuckDir ?? "~/Projects/starbuck", home),
|
|
122
132
|
};
|
|
123
133
|
}
|
|
124
134
|
/** uv is often missing from PATH when pi is started outside a login shell. */
|
|
@@ -143,6 +153,23 @@ export function serverCommand(cfg, program, args = [], env = process.env) {
|
|
|
143
153
|
}
|
|
144
154
|
return { command: uv, args: ["tool", "run", "--quiet", "--from", `zotero-local-mcp==${SERVER_VERSION}`, program, ...args] };
|
|
145
155
|
}
|
|
156
|
+
export function starbuckEnabled(cfg) {
|
|
157
|
+
return (cfg.addons ?? []).includes("starbuck");
|
|
158
|
+
}
|
|
159
|
+
/** How to start Starbuck's MCP server: from the local folder when it exists, otherwise the pinned release. */
|
|
160
|
+
export function starbuckCommand(cfg, env = process.env) {
|
|
161
|
+
const uv = findUv(cfg, env);
|
|
162
|
+
if (cfg.starbuckDir && existsSync(join(cfg.starbuckDir, "pyproject.toml"))) {
|
|
163
|
+
return { command: uv, args: ["run", "--quiet", "--directory", cfg.starbuckDir, "starbuck-mcp"] };
|
|
164
|
+
}
|
|
165
|
+
return { command: uv, args: ["tool", "run", "--quiet", "--from", STARBUCK_SOURCE, "starbuck-mcp"] };
|
|
166
|
+
}
|
|
167
|
+
/** The addons setting with Starbuck turned on or off; other add-ons stay. Undefined (no setting) when empty. */
|
|
168
|
+
export function withStarbuck(addons, on) {
|
|
169
|
+
const rest = (addons ?? []).filter((a) => a !== "starbuck");
|
|
170
|
+
const out = on ? [...rest, "starbuck"] : rest;
|
|
171
|
+
return out.length ? out : undefined;
|
|
172
|
+
}
|
|
146
173
|
/** How prompts name the user. */
|
|
147
174
|
export function who(cfg) {
|
|
148
175
|
return cfg.userName ?? "the user";
|
package/dist/doctor.js
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Checks Node, the settings file, uv, the Zotero server (it runs
|
|
5
5
|
* `zotero-local-mcp --check`), Zotero itself, the tag list, the notes folder,
|
|
6
|
-
* the contact email and the model login. Exit
|
|
6
|
+
* the contact email, Starbuck (when the add-on is on) and the model login. Exit
|
|
7
|
+
* code 1 when something needs a fix.
|
|
7
8
|
*/
|
|
8
9
|
import { spawnSync } from "node:child_process";
|
|
9
10
|
import { existsSync, readFileSync } from "node:fs";
|
|
10
11
|
import { homedir } from "node:os";
|
|
11
12
|
import { join } from "node:path";
|
|
12
|
-
import { findUv, loadConfig, SERVER_VERSION, serverCommand } from "./config.js";
|
|
13
|
+
import { findUv, loadConfig, SERVER_VERSION, serverCommand, STARBUCK_SOURCE, starbuckCommand, starbuckEnabled, } from "./config.js";
|
|
13
14
|
import { profileSpec } from "./profiles.js";
|
|
14
15
|
export const defaultRunner = (command, args, env) => {
|
|
15
16
|
const r = spawnSync(command, args, { env, encoding: "utf8", timeout: 120_000 });
|
|
@@ -100,12 +101,32 @@ export function runChecks(cfg, env, deps = {}) {
|
|
|
100
101
|
out.push(st.contact_email_set
|
|
101
102
|
? { name: "Contact email", ok: true, detail: "set" }
|
|
102
103
|
: { name: "Contact email", ok: true, warn: true, detail: "not set: open-access PDFs cannot be found", fix: "Type subsub init and give an email address." });
|
|
104
|
+
if (starbuckEnabled(cfg))
|
|
105
|
+
out.push(starbuckCheck(cfg, env, run));
|
|
103
106
|
const agentDir = deps.agentDir ?? (env.SUBSUB_AGENT_DIR ? env.SUBSUB_AGENT_DIR : join(homedir(), ".subsub", "agent"));
|
|
104
107
|
out.push(hasLogin(agentDir)
|
|
105
108
|
? { name: "Model login", ok: true, detail: Object.keys(cfg.models).length ? Object.values(cfg.models).join(", ") : "pi's default model" }
|
|
106
109
|
: { name: "Model login", ok: false, detail: "no model provider yet", fix: "Type subsub, then /login." });
|
|
107
110
|
return out;
|
|
108
111
|
}
|
|
112
|
+
/** The Starbuck add-on: can uv start it? (`starbuck --version`, from the local folder or the pinned release) */
|
|
113
|
+
export function starbuckCheck(cfg, env, run) {
|
|
114
|
+
const cmd = starbuckCommand(cfg, env);
|
|
115
|
+
const local = cmd.args.includes("--directory");
|
|
116
|
+
const args = cmd.args.slice(0, -1).concat(["starbuck", "--version"]);
|
|
117
|
+
const res = run(cmd.command, args, env);
|
|
118
|
+
if (res.status === 0) {
|
|
119
|
+
return { name: "Starbuck", ok: true, detail: `${res.stdout.trim()} ${local ? `from ${cfg.starbuckDir}` : `from ${STARBUCK_SOURCE}`}` };
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
name: "Starbuck",
|
|
123
|
+
ok: false,
|
|
124
|
+
detail: local ? `cannot start from ${cfg.starbuckDir}` : `cannot install ${STARBUCK_SOURCE} with uv`,
|
|
125
|
+
fix: local
|
|
126
|
+
? `Type: cd "${cfg.starbuckDir}" && uv sync`
|
|
127
|
+
: "Check the internet connection and run subsub doctor again. To turn reference checks off, type subsub init.",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
109
130
|
export function formatChecks(checks) {
|
|
110
131
|
return checks
|
|
111
132
|
.map((c) => {
|