@tiagojct/subsub 0.11.0 → 0.11.1
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/README.md +114 -179
- package/dist/bench.js +2 -2
- package/dist/cli.js +34 -1
- package/dist/config.js +3 -1
- package/dist/doctor.js +2 -1
- package/dist/init.js +19 -4
- package/dist/paths.js +20 -0
- package/dist/prompt.js +3 -0
- package/dist/shortcut.js +2 -2
- package/dist/subsub.js +32 -6
- package/dist/web.js +56 -25
- package/package.json +2 -3
- package/prompts/lit-note.md +1 -1
- package/src/bench.ts +2 -2
- package/src/cli.ts +34 -1
- package/src/config.ts +3 -1
- package/src/doctor.ts +2 -1
- package/src/init.ts +18 -4
- package/src/paths.ts +21 -0
- package/src/prompt.ts +6 -0
- package/src/shortcut.ts +2 -2
- package/src/subsub.ts +32 -7
- package/src/web.ts +53 -24
- package/web/app.js +28 -1
- package/web/i18n.js +7 -1
- package/docs/Subsub.md +0 -140
package/README.md
CHANGED
|
@@ -1,226 +1,166 @@
|
|
|
1
1
|
# Sub-Sub
|
|
2
2
|
|
|
3
|
-
[](https://nodejs.org)
|
|
7
|
-
[](#requirements)
|
|
3
|
+
[](https://www.npmjs.com/package/@tiagojct/subsub)
|
|
4
|
+
[](https://subsub.tiagojacinto.eu)
|
|
5
|
+
[](LICENSE)
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
Your research assistant, working from your own Zotero library.
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
> — Herman Melville, *Moby-Dick* (Extracts)
|
|
9
|
+
Sub-Sub reads what you already have, finds what you are missing in PubMed, Europe PMC and OpenAlex, and writes notes in Markdown. It also keeps the library in order: it imports, tags and repairs references, and shows you each change before it happens. It runs on your computer, in the browser or in the terminal. It is built on the [pi](https://pi.dev) agent and talks to Zotero through Zotero's local API.
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
Website and guide: https://subsub.tiagojacinto.eu
|
|
15
12
|
|
|
16
|
-
|
|
13
|
+
> "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."
|
|
14
|
+
> Herman Melville, Moby-Dick (1851), the Extracts
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
* **Full Journal & Reversible Edits**: Every library modification is recorded with a unique journal ID and can be reverted anytime using `/undo`.
|
|
20
|
-
* **Two modes**: The **Researcher** (the default) reads items, searches PubMed, Europe PMC and OpenAlex, finds gaps, checks citations in manuscripts and writes Markdown notes in your Sub-Sub folder (it can sit inside an Obsidian vault). It also does everything the Librarian does, so you can research a topic and import and tag what you find in the same conversation. The **Librarian** manages the library only: it tags, imports by DOI, PMID or ISBN, repairs metadata, finds duplicates, attaches open-access PDFs and checks retractions. It has no search tools; use it with a fast, cheap model for tagging sessions. Each mode can have its own model. In both modes, every library change needs your approval.
|
|
21
|
-
* **Local & Private**: Listens strictly on `127.0.0.1` protected by an authenticated single-origin session cookie. Your library contents remain on your computer.
|
|
22
|
-
* **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.
|
|
16
|
+
## What it does
|
|
23
17
|
|
|
24
|
-
|
|
18
|
+
Sub-Sub has two modes.
|
|
19
|
+
|
|
20
|
+
- The Researcher (the default) reads items in your library, searches PubMed, Europe PMC and OpenAlex in one merged search, finds gaps, checks the citations in a manuscript, and writes notes in your Sub-Sub folder. It also does everything the Librarian does, so you can find papers and import and tag them in the same conversation.
|
|
21
|
+
- The Librarian only manages the library: it tags items, imports by DOI, PMID or ISBN, repairs metadata, finds duplicates, attaches open-access PDFs and checks retractions. It has no search tools. Give it a fast, cheap model for long tagging sessions.
|
|
22
|
+
|
|
23
|
+
Each mode can have its own model. Switch with `/researcher` and `/librarian`, or with the buttons in the browser view.
|
|
24
|
+
|
|
25
|
+
Every library change works the same way in both modes:
|
|
26
|
+
|
|
27
|
+
- The Zotero server computes the change first. Sub-Sub shows it to you (added tags in green, removed tags in red) and applies it only when you select Yes. The check is in the program, so the model cannot skip it.
|
|
28
|
+
- Each change sends the item's version, so an edit you make in Zotero at the same time is not overwritten.
|
|
29
|
+
- Every change is journaled. `/undo` reverts it.
|
|
30
|
+
- Nothing is deleted: items go to the Zotero trash. Only tags from your tag list can be added.
|
|
25
31
|
|
|
26
32
|
## Profiles
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
The profile sets how much Sub-Sub writes for you and which tools it offers. Choose it in `subsub init`, or change it with `/profile`.
|
|
29
35
|
|
|
30
|
-
| Profile |
|
|
36
|
+
| Profile | What Sub-Sub does |
|
|
31
37
|
|---|---|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
37
|
-
> [!NOTE]
|
|
38
|
-
> A profile is your own operational baseline, not a rigid constraint. You can switch between profiles whenever your research focus shifts.
|
|
38
|
+
| Reader | Reading notes with quotations, page numbers and questions for you. No summaries or syntheses unless you ask. No bulk library changes. |
|
|
39
|
+
| Scholar | Literature notes, syntheses, searches, alerts and all library changes. |
|
|
40
|
+
| Author | Scholar, plus manuscript work: citation checks against your library, bibliographies for Pandoc and Quarto, and comments on the argument. It does not write your paragraphs. |
|
|
41
|
+
| Editor | Everything, including drafts of manuscript sections and abstracts when you ask. |
|
|
39
42
|
|
|
40
|
-
|
|
43
|
+
The [Profiles page](https://subsub.tiagojacinto.eu/profiles/) lists the tools and commands for each profile and mode.
|
|
41
44
|
|
|
42
45
|
## Requirements
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
* **Model Provider Account**: Any provider supported by pi (e.g., OpenCode Go, OpenRouter, Anthropic, OpenAI, Mistral, DeepSeek, Google Gemini, or local Ollama).
|
|
49
|
-
* **OS**: macOS, Linux, or Windows.
|
|
50
|
-
|
|
51
|
-
---
|
|
47
|
+
- Zotero 10 or later, running, with Settings > Advanced > "Allow other applications on this computer to communicate with Zotero" on.
|
|
48
|
+
- Node.js 22.19 or later, and [uv](https://docs.astral.sh/uv/). The installers add both if they are missing.
|
|
49
|
+
- An account with a model provider that pi supports (for example OpenCode Go, OpenRouter, Anthropic, OpenAI, Mistral, Google), or a local model.
|
|
50
|
+
- macOS, Linux or Windows.
|
|
52
51
|
|
|
53
52
|
## Install
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
Automated installers handle Node.js, uv, and desktop shortcuts:
|
|
58
|
-
|
|
59
|
-
* **macOS & Linux**:
|
|
60
|
-
```sh
|
|
61
|
-
curl -fsSL https://subsub.tiagojacinto.eu/install.sh | sh
|
|
62
|
-
```
|
|
63
|
-
* **Windows (PowerShell)**:
|
|
64
|
-
```powershell
|
|
65
|
-
powershell -ExecutionPolicy ByPass -c "irm https://subsub.tiagojacinto.eu/install.ps1 | iex"
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
### Manual Installation
|
|
69
|
-
|
|
70
|
-
1. Install the global package:
|
|
71
|
-
```sh
|
|
72
|
-
npm install -g @tiagojct/subsub
|
|
73
|
-
```
|
|
74
|
-
2. Run setup:
|
|
75
|
-
```sh
|
|
76
|
-
subsub init
|
|
77
|
-
```
|
|
78
|
-
Answer the interactive prompts (press <kbd>Enter</kbd> to accept defaults). This creates your Sub-Sub folder (`~/Documents/Sub-Sub`, or `Sub-Sub` inside your Obsidian vault) with `Inbox/`, `Literature/`, `Syntheses/`, `Research/` and `Zotero/`, sets up a starter tag list (`Zotero/Zotero tags.md`), and writes the note formats (`Zotero/Zotero agent.md`).
|
|
79
|
-
3. Verify your environment:
|
|
80
|
-
```sh
|
|
81
|
-
subsub doctor
|
|
82
|
-
```
|
|
83
|
-
Resolve any items marked with `FIX`.
|
|
84
|
-
4. Install system shortcuts:
|
|
85
|
-
```sh
|
|
86
|
-
subsub shortcut
|
|
87
|
-
```
|
|
88
|
-
Creates `~/Applications/Sub-Sub.app` on macOS, Start menu & desktop shortcuts on Windows, or an application launcher on Linux.
|
|
89
|
-
5. Launch Sub-Sub:
|
|
90
|
-
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`).
|
|
91
|
-
|
|
92
|
-
If you already use [pi](https://pi.dev), you can add Sub-Sub as an extension:
|
|
54
|
+
macOS and Linux:
|
|
55
|
+
|
|
93
56
|
```sh
|
|
94
|
-
|
|
57
|
+
curl -fsSL https://subsub.tiagojacinto.eu/install.sh | sh
|
|
95
58
|
```
|
|
96
59
|
|
|
97
|
-
|
|
60
|
+
Windows (PowerShell):
|
|
98
61
|
|
|
99
|
-
|
|
62
|
+
```powershell
|
|
63
|
+
powershell -ExecutionPolicy ByPass -c "irm https://subsub.tiagojacinto.eu/install.ps1 | iex"
|
|
64
|
+
```
|
|
100
65
|
|
|
101
|
-
|
|
66
|
+
The installers put everything in your user folder (`~/.subsub`), run `subsub init`, and add a Sub-Sub shortcut. You do not need administrator rights.
|
|
102
67
|
|
|
103
|
-
|
|
68
|
+
With npm:
|
|
104
69
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
* **Session Search & Markdown Export**: Filter conversation history in real time and download full discussions as Markdown notes.
|
|
111
|
-
* **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.
|
|
112
|
-
* **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.
|
|
70
|
+
```sh
|
|
71
|
+
npm install -g @tiagojct/subsub
|
|
72
|
+
subsub init
|
|
73
|
+
subsub doctor
|
|
74
|
+
```
|
|
113
75
|
|
|
114
|
-
|
|
76
|
+
`subsub init` asks a few questions. Press Enter to accept the default. It creates the Sub-Sub folder (`~/Documents/Sub-Sub`, or a `Sub-Sub` folder inside your Obsidian vault) with `Inbox/`, `Literature/`, `Syntheses/`, `Research/` and `Zotero/`, a starter tag list (`Zotero/Zotero tags.md`) and the note formats (`Zotero/Zotero agent.md`). `subsub doctor` checks the set-up; each line marked FIX says what to do.
|
|
115
77
|
|
|
116
|
-
|
|
78
|
+
If you already use pi, you can add Sub-Sub to it: `pi install npm:@tiagojct/subsub`.
|
|
117
79
|
|
|
118
|
-
|
|
80
|
+
Sub-Sub runs its Zotero server, [zotero-local-mcp](https://github.com/tiagojct/zotero-local-mcp), from PyPI with uv.
|
|
119
81
|
|
|
120
|
-
|
|
121
|
-
subsub # Start in the terminal (Researcher)
|
|
122
|
-
subsub --librarian # Start in the Librarian
|
|
123
|
-
subsub web # Start browser view
|
|
124
|
-
subsub -c # Continue the previous session
|
|
125
|
-
subsub -r # Resume an older session
|
|
126
|
-
```
|
|
82
|
+
## Use
|
|
127
83
|
|
|
128
|
-
Commands available within Sub-Sub:
|
|
129
|
-
|
|
130
|
-
* `/researcher`: Change to the Researcher (research and library). Each mode can have its own model.
|
|
131
|
-
* `/librarian`: Change to the Librarian (library only).
|
|
132
|
-
* `/profile [name]`: View or change active profile (`reader`, `scholar`, `author`, `editor`).
|
|
133
|
-
* `/subsub`: Display Zotero connection health and library statistics.
|
|
134
|
-
* `/history`: List recent library changes with journal IDs.
|
|
135
|
-
* `/undo [id]`: Revert the last change (or a specific journal ID).
|
|
136
|
-
|
|
137
|
-
### Built-in Prompt Templates
|
|
138
|
-
|
|
139
|
-
Type the command followed by arguments:
|
|
140
|
-
|
|
141
|
-
* **Library (both modes)**:
|
|
142
|
-
* `/tag-batch [size]`: Tag untagged items using your tag vocabulary.
|
|
143
|
-
* `/clean-tags`: Find irregular tags and reconcile with your vocabulary.
|
|
144
|
-
* `/import-queue [path]`: Import pending references from an inbox note.
|
|
145
|
-
* **Research (Researcher)**:
|
|
146
|
-
* `/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).
|
|
147
|
-
* `/compare <question or citekeys>`: Source matrix (claim, evidence type, caveats, confidence), agreements and disagreements (Scholar, Author, Editor).
|
|
148
|
-
* `/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).
|
|
149
|
-
* `/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).
|
|
150
|
-
* `/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.
|
|
151
|
-
* `/lit-note <citekey>`: Generate an in-depth literature note from item fulltext.
|
|
152
|
-
* `/synthesis <topic>`: Synthesize library findings on a specific topic.
|
|
153
|
-
* `/gaps <topic>`: Identify unanswered questions and literature gaps.
|
|
154
|
-
* `/manuscript <path>`: Cross-check manuscript citations against your library.
|
|
155
|
-
* `/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.
|
|
156
|
-
* `/alert`: Check PubMed and OpenAlex for new publications matching research topics.
|
|
157
|
-
|
|
158
|
-
### Batch Tag Review
|
|
159
|
-
|
|
160
|
-
To preview or apply batch tag reviews generated in your `Inbox/`:
|
|
161
84
|
```sh
|
|
162
|
-
subsub
|
|
163
|
-
subsub
|
|
85
|
+
subsub web # in the browser
|
|
86
|
+
subsub # in the terminal
|
|
87
|
+
subsub --librarian # in the terminal, in the Librarian
|
|
88
|
+
subsub -c # continue the last conversation
|
|
89
|
+
subsub --help # all commands
|
|
164
90
|
```
|
|
165
91
|
|
|
166
|
-
|
|
92
|
+
In the browser view, connect a model provider with the model button. In the terminal, type `/login`.
|
|
167
93
|
|
|
168
|
-
|
|
94
|
+
Commands in Sub-Sub:
|
|
169
95
|
|
|
170
|
-
|
|
96
|
+
| Command | What it does |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `/researcher`, `/librarian` | Switch the mode |
|
|
99
|
+
| `/profile [name]` | Show or change the profile |
|
|
100
|
+
| `/subsub` | Zotero connection, mode, model and library counts |
|
|
101
|
+
| `/history`, `/undo [id]` | Recent library changes; undo the last one or one by its id |
|
|
102
|
+
| `/tag-batch [size]` | Tag items that have no topic, with your tag list |
|
|
103
|
+
| `/clean-tags` | Find tags that are not in your tag list and propose fixes |
|
|
104
|
+
| `/import-queue [path]` | Import the references in the import queue note |
|
|
105
|
+
| `/lit <question>` | Literature review: the library first, then one merged search; an evidence table, agreement, disagreement and open questions; one note in `Research/` |
|
|
106
|
+
| `/compare <question or citekeys>` | A matrix of sources: claim, evidence, caveats, confidence |
|
|
107
|
+
| `/lit-note <citekey>` | A literature note on one item, from its full text |
|
|
108
|
+
| `/synthesis <topic>` | A synthesis of what the library says on a topic |
|
|
109
|
+
| `/gaps <topic>` | Open questions and gaps in the literature |
|
|
110
|
+
| `/manuscript <path>` | Check a manuscript's citations against the library |
|
|
111
|
+
| `/review <path>` | Comments on your own manuscript against the reporting guideline that applies (CONSORT, STROBE, PRISMA, STARD, TRIPOD, CHEERS, SRQR, COREQ). Comments only, next to the manuscript |
|
|
112
|
+
| `/verify <path> [claims]` | Check that each cited work exists, matches its citation and was not retracted; with `claims`, also whether each source supports its sentence (Starbuck add-on) |
|
|
113
|
+
| `/alert` | New publications for your saved searches |
|
|
114
|
+
| `/digest [days] [topic]` | What changed in the last days: new notes, alerts, works worth reading |
|
|
115
|
+
| `/request-copy <citekey, DOI or PMID>` | A draft email to the corresponding author for a paper that is not open access. You send it yourself |
|
|
116
|
+
|
|
117
|
+
The library commands work in both modes; the research commands need the Researcher. Some commands depend on the profile (see the Profiles page).
|
|
118
|
+
|
|
119
|
+
To check or apply a tag review note from `Inbox/` outside Sub-Sub:
|
|
171
120
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
| `about` | string | Brief background context about your research domain | `null` |
|
|
177
|
-
| `language` | string | Response language, or `"auto"` to match your input | `"English"` |
|
|
178
|
-
| `vault` | string | Path to your Sub-Sub folder | `$ZOTERO_VAULT` |
|
|
179
|
-
| `models` | object | The model for each mode, as provider/id: `{"librarian": ..., "researcher": ...}`; `{}` to choose them in Sub-Sub. The 0.10 key `model` (one for both modes) still works; `libraryChanges` is ignored. | `{"librarian": "opencode-go/mimo-v2.6-flash", "researcher": "opencode-go/mimo-v2.6-pro"}` |
|
|
180
|
-
| `defaultMode` | string | `"researcher"` or `"librarian"`: the mode Sub-Sub starts in | `"researcher"` |
|
|
181
|
-
| `serverDir` | string | Local development path for `zotero-local-mcp` | PyPI package |
|
|
182
|
-
| `look` | boolean| Show header banners and status lines in terminal | `true` |
|
|
183
|
-
| `quotes` | boolean| Display quotes from *Moby-Dick* in terminal banner | `true` |
|
|
184
|
-
| `themes` | string or false | Terminal theme family (`subsub-glauca` or `subsub-try-works`); `false` keeps your pi theme | `"subsub-glauca"` |
|
|
185
|
-
| `addons` | array | Optional servers; `["starbuck"]` adds reference checks | `[]` |
|
|
186
|
-
| `starbuckDir` | string | Local Starbuck folder (used when it has a `pyproject.toml`) | `~/Projects/starbuck` |
|
|
121
|
+
```sh
|
|
122
|
+
subsub review preview 13-31
|
|
123
|
+
subsub review apply 13-31
|
|
124
|
+
```
|
|
187
125
|
|
|
188
|
-
|
|
126
|
+
## Settings
|
|
189
127
|
|
|
190
|
-
|
|
128
|
+
`subsub init` writes `~/.config/subsub/config.json` (or the file in `$SUBSUB_CONFIG`). You can also edit it.
|
|
191
129
|
|
|
192
|
-
|
|
130
|
+
| Setting | Meaning | Default |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `profile` | `reader`, `scholar`, `author` or `editor` | `scholar` |
|
|
133
|
+
| `userName`, `about` | Your name and one line about you, for the replies | none |
|
|
134
|
+
| `language` | Language of replies and notes, or `auto` for the language you write in | `English` |
|
|
135
|
+
| `vault` | The Sub-Sub folder | from the server settings |
|
|
136
|
+
| `models` | The model for each mode, as provider/id: `{"librarian": "...", "researcher": "..."}`. `{}` means you choose in Sub-Sub | `opencode-go/mimo-v2.6-flash` and `opencode-go/mimo-v2.6-pro` |
|
|
137
|
+
| `defaultMode` | `researcher` or `librarian` | `researcher` |
|
|
138
|
+
| `addons` | `["starbuck"]` adds reference checks | `[]` |
|
|
139
|
+
| `serverDir` | A local copy of zotero-local-mcp, for development | the PyPI release |
|
|
140
|
+
| `starbuckDir` | A local copy of Starbuck, for development | the pinned release |
|
|
141
|
+
| `look`, `quotes`, `themes` | The terminal header, the Moby-Dick quotations, and the terminal colours (`subsub-glauca`, `subsub-try-works`, or `false` for your pi theme) | on, on, `subsub-glauca` |
|
|
193
142
|
|
|
194
|
-
|
|
143
|
+
The settings of the Zotero server (Sub-Sub folder, tag list, alerts file, contact email) are in `~/.config/zotero-local-mcp/env`.
|
|
195
144
|
|
|
196
|
-
|
|
145
|
+
## Privacy
|
|
197
146
|
|
|
198
|
-
|
|
199
|
-
```json
|
|
200
|
-
"addons": ["starbuck"]
|
|
201
|
-
```
|
|
202
|
-
2. Optional: set `"starbuckDir"` to a local copy of Starbuck. Without it, Sub-Sub runs the pinned Starbuck release with uv.
|
|
203
|
-
3. Start Sub-Sub again and run `subsub doctor`. The line `Starbuck` must show `ok`.
|
|
147
|
+
Your library stays in Zotero; Sub-Sub uses Zotero's local API and never your zotero.org account. The model provider you choose receives your messages and what the tools return (titles, abstracts, tags, and passages of full texts when you ask for them). PubMed, Europe PMC, OpenAlex, Crossref, Unpaywall and Open Library receive identifiers and search terms, with your contact email if you gave one. Sub-Sub sends no usage data. The browser view listens only on 127.0.0.1 and needs the random key in the address that `subsub web` opens. Details: https://subsub.tiagojacinto.eu/privacy/
|
|
204
148
|
|
|
205
|
-
|
|
149
|
+
## Papers that are not open access
|
|
206
150
|
|
|
207
|
-
|
|
151
|
+
Sub-Sub finds open-access copies through Unpaywall and Europe PMC. For other papers, use Sub-Sub on your institution's network, where the DOI link opens the publisher's version through the library's subscriptions, or use `/request-copy`. Sub-Sub does not use unlicensed sources.
|
|
208
152
|
|
|
209
|
-
##
|
|
153
|
+
## Reference checks (Starbuck)
|
|
210
154
|
|
|
211
|
-
|
|
155
|
+
[Starbuck](https://github.com/tiagojct/starbuck) is an optional add-on. It checks that each cited work exists, that its title, first author and year match the citation, and that it was not retracted or corrected. On request, it also checks whether the source supports the sentence that cites it; Sub-Sub's model judges, and Starbuck checks that every quoted passage is in the source.
|
|
212
156
|
|
|
213
|
-
|
|
214
|
-
subsub-bench prepare # Sample realistic test tasks from your library
|
|
215
|
-
subsub-bench run # Run models in parallel against safety-isolated sandbox
|
|
216
|
-
subsub-bench score # Generate objective evaluation scorecard (results.md)
|
|
217
|
-
```
|
|
157
|
+
Turn it on in `subsub init`, with the switch in the browser view, or with `"addons": ["starbuck"]` in the settings. Then `subsub doctor` must show Starbuck ok. Starbuck writes its report in a `_starbuck` folder next to the manuscript.
|
|
218
158
|
|
|
219
|
-
|
|
159
|
+
## Model test
|
|
220
160
|
|
|
221
|
-
|
|
161
|
+
`subsub-bench` runs several models on the same tasks against your own library and scores them. Nothing in the library changes. See [docs/model-test.md](https://github.com/tiagojct/subsub/blob/main/docs/model-test.md) and the [Models page](https://subsub.tiagojacinto.eu/models/) for the latest results.
|
|
222
162
|
|
|
223
|
-
## Development
|
|
163
|
+
## Development
|
|
224
164
|
|
|
225
165
|
```sh
|
|
226
166
|
git clone https://github.com/tiagojct/subsub.git
|
|
@@ -230,13 +170,8 @@ npm test
|
|
|
230
170
|
npm run build
|
|
231
171
|
```
|
|
232
172
|
|
|
233
|
-
|
|
234
|
-
```sh
|
|
235
|
-
ZLM_DIR=/path/to/zotero-local-mcp npm run test:e2e
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
---
|
|
173
|
+
End-to-end tests (real pi and Python servers, a fake Zotero, a scripted model): `ZLM_DIR=/path/to/zotero-local-mcp npm run test:e2e`.
|
|
239
174
|
|
|
240
|
-
##
|
|
175
|
+
## Licence
|
|
241
176
|
|
|
242
|
-
[MIT](LICENSE)
|
|
177
|
+
[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). IBM Plex fonts: SIL Open Font License.
|
package/dist/bench.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* subsub-bench: the same tasks for several models, against the real library,
|
|
3
|
-
* with no changes applied. See docs/
|
|
3
|
+
* with no changes applied. See docs/model-test.md.
|
|
4
4
|
*
|
|
5
5
|
* subsub-bench prepare [--seed 7] pick the tasks (read-only)
|
|
6
6
|
* subsub-bench run [--models a,b] [--librarian a,b] [--researcher c,d] [--parallel 3] [--only task]
|
|
@@ -54,7 +54,7 @@ export function promptFor(task, t, code, out) {
|
|
|
54
54
|
case "lit_note":
|
|
55
55
|
return t.lit_note
|
|
56
56
|
? `Make a literature note for ${t.lit_note.citekey}, following the literature-note format in the shared rules. Read the full text. ` +
|
|
57
|
-
`This is a model test: save the note as ${join(out, `${t.lit_note.citekey}.md`)} instead of
|
|
57
|
+
`This is a model test: save the note as ${join(out, `${t.lit_note.citekey}.md`)} instead of Literature/ in the Sub-Sub folder, and do not attach anything to Zotero.`
|
|
58
58
|
: undefined;
|
|
59
59
|
case "synthesis":
|
|
60
60
|
return t.synthesis
|
package/dist/cli.js
CHANGED
|
@@ -143,11 +143,36 @@ export function reviewCommand(args, cfg, env = process.env) {
|
|
|
143
143
|
env: { ...env, ...(cfg.envFile ? { ZOTERO_MCP_ENV: cfg.envFile } : {}) },
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
|
+
const HELP = `Sub-Sub: your research assistant, working from your own Zotero library.
|
|
147
|
+
|
|
148
|
+
Usage:
|
|
149
|
+
subsub Start Sub-Sub in the terminal
|
|
150
|
+
subsub --librarian Start in the Librarian, which only manages the library
|
|
151
|
+
subsub web Open Sub-Sub in the browser
|
|
152
|
+
subsub init Set up or change the settings (folder, tag list, profile, models)
|
|
153
|
+
subsub doctor Check the set-up; each line marked FIX says what to do
|
|
154
|
+
subsub shortcut Add a Sub-Sub shortcut (--remove takes it away)
|
|
155
|
+
subsub review preview|apply <note>
|
|
156
|
+
Check or apply a tag review note from Inbox/
|
|
157
|
+
subsub --version Show the versions
|
|
158
|
+
|
|
159
|
+
Options:
|
|
160
|
+
--here Work in the current folder, not in the Sub-Sub folder
|
|
161
|
+
|
|
162
|
+
In Sub-Sub, type /subsub for the state, /researcher or /librarian to switch modes,
|
|
163
|
+
/profile to change the profile, /undo to undo the last change, and / for all commands.
|
|
164
|
+
Sub-Sub runs on pi: pi's options (for example --model, --resume) also work.
|
|
165
|
+
|
|
166
|
+
Docs: https://subsub.tiagojacinto.eu`;
|
|
146
167
|
export async function run(argv = process.argv.slice(2)) {
|
|
147
168
|
const env = process.env;
|
|
148
169
|
const home = homedir();
|
|
149
170
|
const here = argv.includes("--here");
|
|
150
171
|
const args = argv.filter((a) => a !== "--here");
|
|
172
|
+
if (args[0] === "--help" || args[0] === "-h" || args[0] === "help") {
|
|
173
|
+
console.log(HELP);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
151
176
|
if (args[0] === "--version" || args[0] === "-v") {
|
|
152
177
|
const piPkg = join(dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))), "..");
|
|
153
178
|
console.log(`subsub ${packageVersion()} (pi ${packageVersion(piPkg)})`);
|
|
@@ -182,7 +207,13 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
182
207
|
return;
|
|
183
208
|
}
|
|
184
209
|
if (isSelfUpdate(args)) {
|
|
185
|
-
|
|
210
|
+
if (PACKAGE_DIR.split(/[\\/]/).includes("node_modules")) {
|
|
211
|
+
console.log("Sub-Sub is not updated with \"update\". To update it, run the installer again (https://subsub.tiagojacinto.eu/install/);");
|
|
212
|
+
console.log("if you installed it with npm, type: npm install -g @tiagojct/subsub");
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
console.log(`Sub-Sub is not updated with "update". Its code is in ${PACKAGE_DIR}; after new code arrives, run "npm install" there.`);
|
|
216
|
+
}
|
|
186
217
|
console.log('To update packages added to Sub-Sub, use "subsub update --extensions".');
|
|
187
218
|
process.exitCode = 1;
|
|
188
219
|
return;
|
|
@@ -215,6 +246,8 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
215
246
|
env.SUBSUB_CLI = "1";
|
|
216
247
|
// pi's update notice would point to the global pi, not to this copy.
|
|
217
248
|
env.PI_SKIP_VERSION_CHECK ??= "1";
|
|
249
|
+
// pi reports a first install to pi.dev and names itself to some providers; Sub-Sub sends no usage data.
|
|
250
|
+
env.PI_TELEMETRY ??= "0";
|
|
218
251
|
process.argv = [process.argv[0], process.argv[1], ...args];
|
|
219
252
|
const index = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
|
|
220
253
|
await import(__rewriteRelativeImportExtension(pathToFileURL(join(dirname(index), "bundle", "cli.js")).href));
|
package/dist/config.js
CHANGED
|
@@ -113,7 +113,9 @@ export function loadConfig(env = process.env) {
|
|
|
113
113
|
// "models" per mode; "model" (0.10) for both; {} or "" means: the user chooses in pi (/login, /model).
|
|
114
114
|
const one = typeof user.model === "string" ? user.model : undefined;
|
|
115
115
|
const given = user.models && typeof user.models === "object" ? user.models : undefined;
|
|
116
|
-
|
|
116
|
+
// Without a settings file (no subsub init yet), the user chooses in pi: no default that may lack a key.
|
|
117
|
+
const fallback = existsSync(file) ? { ...DEFAULT_MODELS } : {};
|
|
118
|
+
const models = given ?? (one !== undefined ? { librarian: one || undefined, researcher: one || undefined } : fallback);
|
|
117
119
|
for (const k of Object.keys(models))
|
|
118
120
|
if (!models[k])
|
|
119
121
|
delete models[k];
|
package/dist/doctor.js
CHANGED
|
@@ -80,7 +80,8 @@ export function runChecks(cfg, env, deps = {}) {
|
|
|
80
80
|
: { name: "Zotero", ok: false, detail: String(st.error ?? "not reachable"), fix: `Start Zotero 10 or later. ${ZOTERO_SETTING}` });
|
|
81
81
|
const v = st.vocabulary ?? {};
|
|
82
82
|
if (v.loaded === false || !v.tags) {
|
|
83
|
-
|
|
83
|
+
const path = v.path && v.path !== "None" ? v.path : undefined; // the server writes Python's None when unset
|
|
84
|
+
out.push({ name: "Tag list", ok: false, detail: path ? `cannot read ${path}` : "not set", fix: "Type subsub init: it creates a starter tag list." });
|
|
84
85
|
}
|
|
85
86
|
else {
|
|
86
87
|
const noTopics = Array.isArray(v.facets) && !v.facets.includes("topic");
|
package/dist/init.js
CHANGED
|
@@ -103,8 +103,8 @@ export async function runInit(io, flags, env = process.env, deps = {}) {
|
|
|
103
103
|
// Without questions (--yes), an existing folder stays exactly as it is.
|
|
104
104
|
const keepAsIs = Boolean(flags.yes) && !flags.folder && !flags.notes && oldFolder !== undefined;
|
|
105
105
|
const folderDefault = flags.folder ?? flags.notes ?? (oldFolder ? (keepAsIs ? oldFolder : subsubFolder(oldFolder)) : join(home, "Documents", FOLDER_NAME));
|
|
106
|
-
const answer = expand((await io.ask("Sub-Sub folder (or an Obsidian vault)", folderDefault))
|
|
107
|
-
|
|
106
|
+
const answer = expand(cleanPath(await io.ask("Sub-Sub folder (or an Obsidian vault)", folderDefault)) || folderDefault, home);
|
|
107
|
+
let notes = keepAsIs && answer === oldFolder ? answer : subsubFolder(answer);
|
|
108
108
|
if (notes !== answer)
|
|
109
109
|
io.say(`That is an Obsidian vault. Sub-Sub will use ${notes}.`);
|
|
110
110
|
// ---- files of the old layout
|
|
@@ -119,8 +119,14 @@ export async function runInit(io, flags, env = process.env, deps = {}) {
|
|
|
119
119
|
io.say(` and ${moves.length - 6} more`);
|
|
120
120
|
move = await io.choose("Move them", [
|
|
121
121
|
{ value: "yes", label: "Move them (recommended)" },
|
|
122
|
-
|
|
122
|
+
// In a new folder, files left behind would not be read: then "no" means keeping the old folder.
|
|
123
|
+
{ value: "no", label: from === notes ? "Leave them; Sub-Sub still reads them from Systems/" : flags.folder || flags.notes ? "Leave them where they are" : `Leave them, and keep using ${from}` },
|
|
123
124
|
], flags.move ?? (flags.yes ? "no" : "yes"));
|
|
125
|
+
// A folder given on the command line (--folder, --notes) is a deliberate choice and stays.
|
|
126
|
+
if (move === "no" && from !== notes && !flags.folder && !flags.notes) {
|
|
127
|
+
notes = from;
|
|
128
|
+
io.say(`Sub-Sub keeps using ${notes}.`);
|
|
129
|
+
}
|
|
124
130
|
}
|
|
125
131
|
explain("Profile", "The profile sets how much Sub-Sub writes for you and which tools it offers. Change it at any time with /profile.");
|
|
126
132
|
const profileDefault = flags.profile ?? existing.profile ?? (fmup ? "reader" : "scholar");
|
|
@@ -143,7 +149,7 @@ export async function runInit(io, flags, env = process.env, deps = {}) {
|
|
|
143
149
|
explain("Tag list", `Sub-Sub tags items only with the tags in this list. Pick a start; you edit the list later in ${SETTINGS_DIR}/${SETTINGS_FILES.tags}.`);
|
|
144
150
|
tags = await io.choose("Starter tag list", Object.entries(STARTER_TAGS).map(([value, label]) => ({ value, label })), flags.tags ?? (fmup ? "health-sciences" : "any-field"));
|
|
145
151
|
}
|
|
146
|
-
explain("Contact email", "Unpaywall needs an email address to find open-access PDFs. Sub-Sub sends it
|
|
152
|
+
explain("Contact email", "Unpaywall needs an email address to find open-access PDFs. Sub-Sub sends it with its requests to PubMed, Europe PMC, OpenAlex, Crossref and Unpaywall, as they ask, and to nobody else.");
|
|
147
153
|
const email = (await io.ask("Email (optional)", flags.email ?? envValues.ZOTERO_CONTACT_EMAIL ?? "")).trim();
|
|
148
154
|
explain("Reference checks (Starbuck)", "An optional add-on for manuscripts: it checks that each cited work exists, matches its citation and was not retracted. Sub-Sub then has /verify.");
|
|
149
155
|
const starbuck = await io.choose("Reference checks", [
|
|
@@ -300,3 +306,12 @@ export async function initMain(args, env = process.env) {
|
|
|
300
306
|
io.close?.();
|
|
301
307
|
}
|
|
302
308
|
}
|
|
309
|
+
/** A path as typed, dragged from Finder (spaces escaped) or copied from Explorer (in quotes). */
|
|
310
|
+
export function cleanPath(raw) {
|
|
311
|
+
let p = raw.trim();
|
|
312
|
+
if (p.length > 1 && (p[0] === '"' || p[0] === "'") && p.at(-1) === p[0])
|
|
313
|
+
return p.slice(1, -1);
|
|
314
|
+
if (process.platform !== "win32")
|
|
315
|
+
p = p.replace(/\\([ '"()&])/g, "$1");
|
|
316
|
+
return p;
|
|
317
|
+
}
|
package/dist/paths.js
CHANGED
|
@@ -81,7 +81,27 @@ export function judgePath(target, policy) {
|
|
|
81
81
|
const hit = policy.protectedPaths.find((p) => within(target, p));
|
|
82
82
|
if (hit)
|
|
83
83
|
return { ok: false, why: `it changes a protected file or folder (${hit})` };
|
|
84
|
+
// Notes never live in hidden folders; .git/hooks, .obsidian/plugins or .vscode can run code.
|
|
85
|
+
const hidden = target.split(/[\\/]/).find((part) => part.startsWith(".") && part !== "." && part !== "..");
|
|
86
|
+
if (hidden)
|
|
87
|
+
return { ok: false, why: `it is in a hidden folder or file (${hidden})` };
|
|
84
88
|
if (policy.allowed.some((a) => within(target, a)))
|
|
85
89
|
return { ok: true };
|
|
86
90
|
return { ok: false, why: "it is outside the vault and the working folder" };
|
|
87
91
|
}
|
|
92
|
+
/** Files with keys and passwords: the model may not read them, even when you ask. */
|
|
93
|
+
export function secretPaths() {
|
|
94
|
+
const home = realish(homedir());
|
|
95
|
+
return [
|
|
96
|
+
join(home, ".subsub", "agent", "auth.json"),
|
|
97
|
+
join(home, ".pi", "agent", "auth.json"),
|
|
98
|
+
join(home, ".config", "zotero-local-mcp", "env"),
|
|
99
|
+
join(home, ".ssh"),
|
|
100
|
+
join(home, ".aws"),
|
|
101
|
+
join(home, ".netrc"),
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
/** True if reading `target` would read a secret; for grep, also a folder that holds one. */
|
|
105
|
+
export function isSecret(target, recursive = false) {
|
|
106
|
+
return secretPaths().some((p) => within(target, p) || (recursive && within(p, target)));
|
|
107
|
+
}
|
package/dist/prompt.js
CHANGED
|
@@ -54,6 +54,9 @@ export function roleText(mode, cfg) {
|
|
|
54
54
|
export function systemAddition(mode, cfg, cwd) {
|
|
55
55
|
const user = who(cfg);
|
|
56
56
|
const parts = [`# Sub-Sub: ${mode === "librarian" ? "Librarian" : "Researcher"} mode`, roleText(mode, cfg)];
|
|
57
|
+
if (cfg.vault) {
|
|
58
|
+
parts.push("# The Sub-Sub folder", `The Sub-Sub folder is ${cfg.vault}. Folder names in these instructions (Inbox/, Literature/, Syntheses/, Research/, Zotero/) are inside it. Write notes there with absolute paths, even when the working folder is somewhere else.`);
|
|
59
|
+
}
|
|
57
60
|
const shared = readIf(cfg.sharedRules);
|
|
58
61
|
if (shared) {
|
|
59
62
|
parts.push("# Shared Zotero rules (from the Sub-Sub folder)", shared, `Sub-Sub note: in Sub-Sub you do not need a separate dry run before a change. Call the write tool with dry_run=false; Sub-Sub shows ${user} the server's preview and applies it only on approval. This replaces the dry-run steps above.`);
|
package/dist/shortcut.js
CHANGED
|
@@ -64,7 +64,7 @@ export function desktopEntry(p) {
|
|
|
64
64
|
"[Desktop Entry]",
|
|
65
65
|
"Type=Application",
|
|
66
66
|
"Name=Sub-Sub",
|
|
67
|
-
"Comment=
|
|
67
|
+
"Comment=Research assistant for your Zotero library",
|
|
68
68
|
`Exec=${desktopQuote(p.node)} ${desktopQuote(p.bin)} web`,
|
|
69
69
|
`Path=${p.home}`,
|
|
70
70
|
`Icon=${join(p.packageDir, "assets", "icon.png")}`,
|
|
@@ -85,7 +85,7 @@ export function windowsScript(p) {
|
|
|
85
85
|
` $s.Arguments = ${psQuote(`"${p.bin}" web`)}`,
|
|
86
86
|
` $s.WorkingDirectory = ${psQuote(p.home)}`,
|
|
87
87
|
` $s.IconLocation = ${psQuote(join(p.packageDir, "assets", "icon.ico"))}`,
|
|
88
|
-
" $s.Description = 'Sub-Sub:
|
|
88
|
+
" $s.Description = 'Sub-Sub: research assistant for your Zotero library'",
|
|
89
89
|
" $s.WindowStyle = 7",
|
|
90
90
|
" $s.Save()",
|
|
91
91
|
" Write-Output (Join-Path $dir 'Sub-Sub.lnk')",
|