@tiagojct/subsub 0.5.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/README.md +90 -0
- package/bin/subsub-bench.js +19 -0
- package/bin/subsub.js +14 -0
- package/dist/args.js +67 -0
- package/dist/bench.js +534 -0
- package/dist/bridge.js +107 -0
- package/dist/cli.js +209 -0
- package/dist/config.js +144 -0
- package/dist/doctor.js +131 -0
- package/dist/init.js +215 -0
- package/dist/look.js +107 -0
- package/dist/paths.js +86 -0
- package/dist/preview.js +94 -0
- package/dist/profiles.js +79 -0
- package/dist/prompt.js +72 -0
- package/dist/roles.js +73 -0
- package/dist/subsub.js +414 -0
- package/docs/Subsub.md +126 -0
- package/extensions/subsub.ts +6 -0
- package/package.json +73 -0
- package/prompts/alert.md +4 -0
- package/prompts/clean-tags.md +4 -0
- package/prompts/gaps.md +5 -0
- package/prompts/import-queue.md +5 -0
- package/prompts/lit-note.md +5 -0
- package/prompts/manuscript.md +5 -0
- package/prompts/synthesis.md +5 -0
- package/prompts/tag-batch.md +5 -0
- package/roles/librarian.md +46 -0
- package/roles/researcher.md +37 -0
- package/scripts/build.mjs +14 -0
- package/scripts/make-themes.py +116 -0
- package/src/args.ts +64 -0
- package/src/bench.ts +580 -0
- package/src/bridge.ts +134 -0
- package/src/cli.ts +201 -0
- package/src/config.ts +191 -0
- package/src/doctor.ts +171 -0
- package/src/init.ts +252 -0
- package/src/look.ts +141 -0
- package/src/paths.ts +100 -0
- package/src/preview.ts +107 -0
- package/src/profiles.ts +99 -0
- package/src/prompt.ts +81 -0
- package/src/roles.ts +83 -0
- package/src/subsub.ts +434 -0
- package/templates/Zotero agent.md +87 -0
- package/templates/vocabularies/any-field.md +52 -0
- package/templates/vocabularies/health-informatics.md +88 -0
- package/templates/vocabularies/health-sciences.md +95 -0
- package/themes/subsub-glauca-dark.json +91 -0
- package/themes/subsub-glauca-light.json +91 -0
- package/themes/subsub-try-works-dark.json +91 -0
- package/themes/subsub-try-works-light.json +91 -0
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Sub-Sub
|
|
2
|
+
|
|
3
|
+
A Zotero librarian and research assistant that runs in your terminal. It is built on the [pi](https://pi.dev) agent and works with your own Zotero library through Zotero's local API.
|
|
4
|
+
|
|
5
|
+
> "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." (Moby-Dick, Extracts)
|
|
6
|
+
|
|
7
|
+
Sub-Sub has two modes:
|
|
8
|
+
|
|
9
|
+
- The librarian changes the library: tags from your own tag list, imports by DOI, PMID or ISBN, metadata repair, duplicates, retractions, open-access PDFs, notes and collections. It has no web access.
|
|
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.
|
|
11
|
+
|
|
12
|
+
Every library change is shown as a preview and runs only after you approve it. The model cannot skip this: the check is in the code, not in the prompt. Every change is journaled and can be undone with `/undo`.
|
|
13
|
+
|
|
14
|
+
## Profiles
|
|
15
|
+
|
|
16
|
+
A profile sets how much Sub-Sub does for you. Choose it in `subsub init` and change it with `/profile`.
|
|
17
|
+
|
|
18
|
+
| Profile | What it does |
|
|
19
|
+
|---|---|
|
|
20
|
+
| Reader | Explains each step. Reading notes are quotes with page numbers, plus questions for you to answer. No summaries or syntheses. No bulk library changes. |
|
|
21
|
+
| Scholar | Literature notes, syntheses, searches and alerts. The full librarian. |
|
|
22
|
+
| Author | Scholar, plus manuscripts: citation check against the library, bibliography for Quarto or Pandoc, comments on the argument. It does not write your paragraphs. |
|
|
23
|
+
| Editor | Everything, including drafting manuscript text when you ask. |
|
|
24
|
+
|
|
25
|
+
A profile is your own choice, not a lock.
|
|
26
|
+
|
|
27
|
+
## Requirements
|
|
28
|
+
|
|
29
|
+
- Zotero 10 or later, running. In Zotero, open Settings > Advanced and turn on "Allow other applications on this computer to communicate with Zotero".
|
|
30
|
+
- Node.js 22.19 or later.
|
|
31
|
+
- uv (https://docs.astral.sh/uv/).
|
|
32
|
+
- An account with a model provider that pi supports (for example OpenCode Go, OpenRouter, Anthropic, OpenAI, or a local Ollama).
|
|
33
|
+
- macOS, Windows or Linux.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
1. Type `npm install -g @tiagojct/subsub`. The command is `subsub`.
|
|
38
|
+
2. Type `subsub init`. Answer the questions. Press Enter to keep a default.
|
|
39
|
+
3. Type `subsub doctor`. Fix each line marked FIX.
|
|
40
|
+
4. Type `subsub`. Then type `/login` and choose your model provider.
|
|
41
|
+
|
|
42
|
+
`subsub init` creates a notes folder with Inbox/ and Systems/, a starter tag list (health sciences, health informatics, or any field) and a file with the note formats (Systems/Zotero agent.md). You can edit both files. It never replaces a file that exists.
|
|
43
|
+
|
|
44
|
+
If you already use pi, you can also add Sub-Sub to plain pi: `pi install npm:@tiagojct/subsub`.
|
|
45
|
+
|
|
46
|
+
The Zotero servers (zotero-local-mcp) run from PyPI through uv. To run them from a copy of the repository, add `"serverDir": "/path/to/zotero-local-mcp"` to the settings file.
|
|
47
|
+
|
|
48
|
+
## Use
|
|
49
|
+
|
|
50
|
+
- `subsub` starts in the researcher mode. `subsub --librarian` starts in the librarian mode. `/librarian` and `/researcher` change the mode.
|
|
51
|
+
- `/profile` shows or changes the profile. `/subsub` shows the Zotero connection. `/history` lists recent library changes. `/undo` reverts one.
|
|
52
|
+
- Templates: `/tag-batch`, `/clean-tags`, `/import-queue` (librarian); `/lit-note <citekey>`, `/synthesis <topic>`, `/gaps <topic>`, `/manuscript <file>`, `/alert` (researcher).
|
|
53
|
+
- `subsub review preview 13-31` and `subsub review apply 13-31` preview or apply tag review notes by number.
|
|
54
|
+
- `subsub -c` continues the last session. `subsub -r` selects an older one.
|
|
55
|
+
|
|
56
|
+
## Settings
|
|
57
|
+
|
|
58
|
+
`subsub init` writes `~/.config/subsub/config.json` (or the file in `SUBSUB_CONFIG`):
|
|
59
|
+
|
|
60
|
+
| Setting | Meaning | Default |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `profile` | reader, scholar, author or editor | scholar |
|
|
63
|
+
| `userName` | how Sub-Sub names you | "the user" |
|
|
64
|
+
| `about` | one line about you | none |
|
|
65
|
+
| `language` | language of replies and notes, or `auto` | English |
|
|
66
|
+
| `vault` | notes folder | `ZOTERO_VAULT` from the server settings |
|
|
67
|
+
| `models` | model per mode, e.g. `{ "librarian": "opencode-go/glm-5.3-flash" }` | pi's current model |
|
|
68
|
+
| `defaultMode` | librarian or researcher | researcher |
|
|
69
|
+
| `serverDir` | a local copy of zotero-local-mcp | the PyPI release |
|
|
70
|
+
| `look`, `quotes`, `themes` | banner, Moby-Dick line, themes per mode | on |
|
|
71
|
+
|
|
72
|
+
The server settings (notes folder, tag list, contact email for Unpaywall) are in `~/.config/zotero-local-mcp/env`.
|
|
73
|
+
|
|
74
|
+
## Model test
|
|
75
|
+
|
|
76
|
+
`subsub-bench prepare`, `subsub-bench run` and `subsub-bench score` run the same tasks for several models against your library. Every library change is recorded and blocked, and files are written only in the run folder. The results show tag accuracy, invented references, tokens, cost and time.
|
|
77
|
+
|
|
78
|
+
## Tests
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
npm install
|
|
82
|
+
npm test
|
|
83
|
+
ZLM_DIR=/path/to/zotero-local-mcp npm run test:e2e
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The end-to-end tests run the real pi with Sub-Sub, the real Python servers, a fake Zotero and a scripted model.
|
|
87
|
+
|
|
88
|
+
## Licence
|
|
89
|
+
|
|
90
|
+
MIT.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Model test for Sub-Sub. See docs/Subsub.md.
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
// Inside node_modules (an npm install), Node cannot strip types: use the compiled dist/.
|
|
5
|
+
// From a checkout (npm link), run the TypeScript sources directly.
|
|
6
|
+
const installed = fileURLToPath(import.meta.url).split(/[\\/]/).includes("node_modules");
|
|
7
|
+
const entry = (name) => (installed ? `../dist/${name}.js` : `../src/${name}.ts`);
|
|
8
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
9
|
+
if (major < 22 || (major === 22 && minor < 19)) {
|
|
10
|
+
console.error(`subsub-bench needs Node.js 22.19 or later (this is ${process.versions.node}).`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
const { main } = await import(entry("bench"));
|
|
14
|
+
try {
|
|
15
|
+
await main();
|
|
16
|
+
} catch (err) {
|
|
17
|
+
console.error(err instanceof Error ? err.message : err);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
package/bin/subsub.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Sub-Sub: pi set up as a Zotero librarian and research assistant.
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
// Inside node_modules (an npm install), Node cannot strip types: use the compiled dist/.
|
|
5
|
+
// From a checkout (npm link), run the TypeScript sources directly.
|
|
6
|
+
const installed = fileURLToPath(import.meta.url).split(/[\\/]/).includes("node_modules");
|
|
7
|
+
const entry = (name) => (installed ? `../dist/${name}.js` : `../src/${name}.ts`);
|
|
8
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
9
|
+
if (major < 22 || (major === 22 && minor < 19)) {
|
|
10
|
+
console.error(`Sub-Sub needs Node.js 22.19 or later (this is ${process.versions.node}).`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
const { run } = await import(entry("cli"));
|
|
14
|
+
await run();
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Some models send a list as a JSON string ("[\"topic/asthma\"]") or as
|
|
3
|
+
* "a, b". pi validates tool arguments against the schema before Sub-Sub sees
|
|
4
|
+
* them, so the schema also accepts a string where it wants a list of strings,
|
|
5
|
+
* and Sub-Sub turns the string back into a list before the server gets it.
|
|
6
|
+
* (Seen in the model test: three models lost several turns on this.)
|
|
7
|
+
*/
|
|
8
|
+
function isStringArray(s) {
|
|
9
|
+
if (!s || typeof s !== "object")
|
|
10
|
+
return false;
|
|
11
|
+
if (s.type === "array")
|
|
12
|
+
return !s.items || s.items.type === "string";
|
|
13
|
+
const alts = (s.anyOf ?? s.oneOf);
|
|
14
|
+
return Array.isArray(alts) && alts.some((a) => a?.type === "array" && (!a.items || a.items.type === "string"));
|
|
15
|
+
}
|
|
16
|
+
/** A copy of the input schema where top-level string lists also accept a string. */
|
|
17
|
+
export function loosenArrays(schema) {
|
|
18
|
+
if (!schema || typeof schema !== "object" || !schema.properties)
|
|
19
|
+
return schema;
|
|
20
|
+
const props = {};
|
|
21
|
+
for (const [k, v] of Object.entries(schema.properties)) {
|
|
22
|
+
if (!isStringArray(v)) {
|
|
23
|
+
props[k] = v;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const s = v;
|
|
27
|
+
const alts = (s.anyOf ?? s.oneOf);
|
|
28
|
+
if (alts) {
|
|
29
|
+
props[k] = { ...s, anyOf: [...alts, { type: "string" }], oneOf: undefined };
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const { description, default: def, title, ...inner } = s;
|
|
33
|
+
props[k] = { anyOf: [inner, { type: "string" }], description, default: def, title };
|
|
34
|
+
}
|
|
35
|
+
for (const key of Object.keys(props[k]))
|
|
36
|
+
if (props[k][key] === undefined)
|
|
37
|
+
delete props[k][key];
|
|
38
|
+
}
|
|
39
|
+
return { ...schema, properties: props };
|
|
40
|
+
}
|
|
41
|
+
export function toList(value) {
|
|
42
|
+
const t = value.trim();
|
|
43
|
+
if (t.startsWith("[")) {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(t);
|
|
46
|
+
if (Array.isArray(parsed))
|
|
47
|
+
return parsed.map((x) => String(x));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
/* fall through */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return t
|
|
54
|
+
.replace(/^\[|\]$/g, "")
|
|
55
|
+
.split(/[,\n]/)
|
|
56
|
+
.map((x) => x.trim().replace(/^["'`]|["'`]$/g, ""))
|
|
57
|
+
.filter(Boolean);
|
|
58
|
+
}
|
|
59
|
+
/** Turn strings back into lists where the original schema wants a list of strings. Changes input in place. */
|
|
60
|
+
export function coerceArgs(original, input) {
|
|
61
|
+
const props = (original?.properties ?? {});
|
|
62
|
+
for (const [k, v] of Object.entries(input)) {
|
|
63
|
+
if (typeof v === "string" && isStringArray(props[k]))
|
|
64
|
+
input[k] = toList(v);
|
|
65
|
+
}
|
|
66
|
+
return input;
|
|
67
|
+
}
|