@tiagojct/subsub 0.10.0 → 0.11.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 +11 -10
- package/dist/bench.js +5 -4
- package/dist/config.js +19 -13
- package/dist/doctor.js +1 -1
- package/dist/init.js +15 -20
- package/dist/look.js +2 -2
- package/dist/prompt.js +4 -7
- package/dist/roles.js +12 -12
- package/dist/subsub.js +41 -65
- package/dist/web.js +9 -6
- package/docs/Subsub.md +13 -13
- package/package.json +1 -1
- package/roles/librarian.md +46 -0
- package/roles/{assistant.md → researcher.md} +4 -5
- package/src/bench.ts +5 -8
- package/src/config.ts +22 -17
- package/src/doctor.ts +1 -1
- package/src/init.ts +18 -28
- package/src/look.ts +3 -4
- package/src/prompt.ts +5 -10
- package/src/roles.ts +14 -15
- package/src/subsub.ts +40 -66
- package/src/web.ts +7 -5
- package/templates/Zotero agent.md +2 -2
- package/web/app.css +21 -14
- package/web/app.js +23 -22
- package/web/i18n.js +36 -28
- package/web/index.html +6 -2
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ A Zotero librarian and research assistant that runs on your computer, in your br
|
|
|
17
17
|
|
|
18
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
19
|
* **Full Journal & Reversible Edits**: Every library modification is recorded with a unique journal ID and can be reverted anytime using `/undo`.
|
|
20
|
-
* **
|
|
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
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
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.
|
|
23
23
|
|
|
@@ -104,7 +104,7 @@ Starting Sub-Sub with `subsub web` (or `subsub serve`) launches the browser appl
|
|
|
104
104
|
|
|
105
105
|
* **Approval Previews**: Shows server-computed dry-run diffs directly in the UI. Changes are applied only after clicking **Yes** or pressing <kbd>Y</kbd>.
|
|
106
106
|
* **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.
|
|
107
|
-
* **
|
|
107
|
+
* **Mode buttons**: Researcher and Librarian, in the header. The side panel shows the actions of the mode: Research and Library in the Researcher, Library only in the Librarian. The model button saves the model for the current mode, or for both modes.
|
|
108
108
|
* **Theme Switcher**: Header toggle between **Light**, **Dark**, and **System** themes.
|
|
109
109
|
* **One-Click Copy**: Built-in copy buttons on all code snippets, BibTeX records, and assistant literature notes.
|
|
110
110
|
* **Session Search & Markdown Export**: Filter conversation history in real time and download full discussions as Markdown notes.
|
|
@@ -118,8 +118,8 @@ Starting Sub-Sub with `subsub web` (or `subsub serve`) launches the browser appl
|
|
|
118
118
|
### Starting and commands
|
|
119
119
|
|
|
120
120
|
```sh
|
|
121
|
-
subsub # Start in the terminal
|
|
122
|
-
subsub --
|
|
121
|
+
subsub # Start in the terminal (Researcher)
|
|
122
|
+
subsub --librarian # Start in the Librarian
|
|
123
123
|
subsub web # Start browser view
|
|
124
124
|
subsub -c # Continue the previous session
|
|
125
125
|
subsub -r # Resume an older session
|
|
@@ -127,7 +127,8 @@ subsub -r # Resume an older session
|
|
|
127
127
|
|
|
128
128
|
Commands available within Sub-Sub:
|
|
129
129
|
|
|
130
|
-
* `/
|
|
130
|
+
* `/researcher`: Change to the Researcher (research and library). Each mode can have its own model.
|
|
131
|
+
* `/librarian`: Change to the Librarian (library only).
|
|
131
132
|
* `/profile [name]`: View or change active profile (`reader`, `scholar`, `author`, `editor`).
|
|
132
133
|
* `/subsub`: Display Zotero connection health and library statistics.
|
|
133
134
|
* `/history`: List recent library changes with journal IDs.
|
|
@@ -137,11 +138,11 @@ Commands available within Sub-Sub:
|
|
|
137
138
|
|
|
138
139
|
Type the command followed by arguments:
|
|
139
140
|
|
|
140
|
-
* **Library
|
|
141
|
+
* **Library (both modes)**:
|
|
141
142
|
* `/tag-batch [size]`: Tag untagged items using your tag vocabulary.
|
|
142
143
|
* `/clean-tags`: Find irregular tags and reconcile with your vocabulary.
|
|
143
144
|
* `/import-queue [path]`: Import pending references from an inbox note.
|
|
144
|
-
* **Research**:
|
|
145
|
+
* **Research (Researcher)**:
|
|
145
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).
|
|
146
147
|
* `/compare <question or citekeys>`: Source matrix (claim, evidence type, caveats, confidence), agreements and disagreements (Scholar, Author, Editor).
|
|
147
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).
|
|
@@ -175,8 +176,8 @@ subsub review apply 13-31
|
|
|
175
176
|
| `about` | string | Brief background context about your research domain | `null` |
|
|
176
177
|
| `language` | string | Response language, or `"auto"` to match your input | `"English"` |
|
|
177
178
|
| `vault` | string | Path to your Sub-Sub folder | `$ZOTERO_VAULT` |
|
|
178
|
-
| `
|
|
179
|
-
| `
|
|
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"` |
|
|
180
181
|
| `serverDir` | string | Local development path for `zotero-local-mcp` | PyPI package |
|
|
181
182
|
| `look` | boolean| Show header banners and status lines in terminal | `true` |
|
|
182
183
|
| `quotes` | boolean| Display quotes from *Moby-Dick* in terminal banner | `true` |
|
|
@@ -201,7 +202,7 @@ Sub-Sub finds open-access copies (Unpaywall, Europe PMC). For other papers, use
|
|
|
201
202
|
2. Optional: set `"starbuckDir"` to a local copy of Starbuck. Without it, Sub-Sub runs the pinned Starbuck release with uv.
|
|
202
203
|
3. Start Sub-Sub again and run `subsub doctor`. The line `Starbuck` must show `ok`.
|
|
203
204
|
|
|
204
|
-
|
|
205
|
+
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 Sub-Sub 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.
|
|
205
206
|
|
|
206
207
|
---
|
|
207
208
|
|
package/dist/bench.js
CHANGED
|
@@ -20,8 +20,8 @@ import { expand, loadConfig, starbuckCommand, starbuckEnabled } from "./config.j
|
|
|
20
20
|
import { findUv } from "./subsub.js";
|
|
21
21
|
export const PACKAGE_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
22
22
|
export const DEFAULT_MODELS = {
|
|
23
|
-
librarian: ["
|
|
24
|
-
researcher: ["mimo-v2.6-pro", "
|
|
23
|
+
librarian: ["mimo-v2.6-flash", "glm-5.3-flash", "qwen3.8-flash", "gpt-5.6-luna", "hy3", "mimo-v2.6-pro"],
|
|
24
|
+
researcher: ["mimo-v2.6-pro", "qwen3.8-flash", "kimi-k3", "glm-5.3", "minimax-m3", "gpt-5.6-luna"],
|
|
25
25
|
};
|
|
26
26
|
export const TASKS = {
|
|
27
27
|
librarian: ["tagging", "facet_fix", "import"],
|
|
@@ -81,7 +81,8 @@ const KEEP = new Set(["tool_execution_start", "tool_execution_end", "message_end
|
|
|
81
81
|
export function runOne(opts) {
|
|
82
82
|
mkdirSync(opts.out, { recursive: true });
|
|
83
83
|
const args = ["--mode", "rpc", "--no-session", "--provider", opts.provider ?? "opencode-go", "--model", opts.model];
|
|
84
|
-
|
|
84
|
+
if (opts.role === "librarian")
|
|
85
|
+
args.push("--librarian");
|
|
85
86
|
const env = { ...process.env, ...opts.extraEnv, SUBSUB_BENCH_OUT: opts.out, SUBSUB_CONFIG: opts.configFile };
|
|
86
87
|
const proc = spawn(process.execPath, [opts.cli ?? join(PACKAGE_DIR, "bin", "subsub.js"), ...args], { cwd: opts.cwd, env });
|
|
87
88
|
const events = [];
|
|
@@ -557,7 +558,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
557
558
|
if (!prompt)
|
|
558
559
|
return;
|
|
559
560
|
const cf = join(cfgDir, `${j.code}.json`);
|
|
560
|
-
writeFileSync(cf, JSON.stringify({ ...userCfg, profile: "editor",
|
|
561
|
+
writeFileSync(cf, JSON.stringify({ ...userCfg, profile: "editor", models: { librarian: `opencode-go/${j.model}`, researcher: `opencode-go/${j.model}` }, model: undefined, libraryChanges: undefined }));
|
|
561
562
|
const s = await runOne({ ...j, prompt, out, cwd: cfg.vault, configFile: cf, timeoutSec });
|
|
562
563
|
n++;
|
|
563
564
|
console.log(`${n}/${jobs.length} ${j.code} ${j.task} ${s.seconds}s ${s.error ?? (s.settled ? "done" : "?")}`);
|
package/dist/config.js
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
* "profile": "scholar",
|
|
7
7
|
* "userName": "Ana", "about": "a master's student in health informatics", "language": "English",
|
|
8
8
|
* "vault": "~/Documents/Sub-Sub",
|
|
9
|
-
* "
|
|
10
|
-
* "
|
|
9
|
+
* "models": { "librarian": "opencode-go/mimo-v2.6-flash", "researcher": "opencode-go/mimo-v2.6-pro" },
|
|
10
|
+
* "defaultMode": "researcher"
|
|
11
11
|
* }
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Two modes: the Librarian manages the library; the Researcher does that too,
|
|
14
|
+
* and searches and writes notes. "model" (0.10) sets one model for both;
|
|
15
|
+
* "libraryChanges" (0.10) is no longer used.
|
|
16
16
|
*
|
|
17
17
|
* `subsub init` writes this file. Without "serverDir" (or when that folder does
|
|
18
18
|
* not exist), the Zotero servers run from PyPI with uv.
|
|
@@ -26,8 +26,11 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
26
26
|
import { homedir } from "node:os";
|
|
27
27
|
import { dirname, join, resolve } from "node:path";
|
|
28
28
|
import { isObsidianVault, settingsFile } from "./layout.js";
|
|
29
|
-
/** The tested
|
|
30
|
-
export const
|
|
29
|
+
/** The tested models, one per mode (model test, see bench-results). */
|
|
30
|
+
export const DEFAULT_MODELS = {
|
|
31
|
+
librarian: "opencode-go/mimo-v2.6-flash",
|
|
32
|
+
researcher: "opencode-go/mimo-v2.6-pro",
|
|
33
|
+
};
|
|
31
34
|
export const PROFILES = ["reader", "scholar", "author", "editor"];
|
|
32
35
|
/** The Zotero server release that this Sub-Sub version runs when there is no local server folder. */
|
|
33
36
|
export const SERVER_VERSION = "0.5.1";
|
|
@@ -107,10 +110,13 @@ export function loadConfig(env = process.env) {
|
|
|
107
110
|
const envValues = envFile && existsSync(envFile) ? parseEnvFile(envFile) : {};
|
|
108
111
|
const vaultRaw = user.vault ?? env.ZOTERO_VAULT ?? envValues.ZOTERO_VAULT;
|
|
109
112
|
const vault = vaultRaw ? expand(vaultRaw, home) : undefined;
|
|
110
|
-
//
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
const
|
|
113
|
+
// "models" per mode; "model" (0.10) for both; {} or "" means: the user chooses in pi (/login, /model).
|
|
114
|
+
const one = typeof user.model === "string" ? user.model : undefined;
|
|
115
|
+
const given = user.models && typeof user.models === "object" ? user.models : undefined;
|
|
116
|
+
const models = given ?? (one !== undefined ? { librarian: one || undefined, researcher: one || undefined } : { ...DEFAULT_MODELS });
|
|
117
|
+
for (const k of Object.keys(models))
|
|
118
|
+
if (!models[k])
|
|
119
|
+
delete models[k];
|
|
114
120
|
const themes = user.themes === false ? false : typeof user.themes === "string" ? user.themes : user.themes?.researcher;
|
|
115
121
|
return {
|
|
116
122
|
configFile: file,
|
|
@@ -122,8 +128,8 @@ export function loadConfig(env = process.env) {
|
|
|
122
128
|
serverDir: expand(user.serverDir ?? "~/Projects/zotero-local-mcp", home),
|
|
123
129
|
envFile,
|
|
124
130
|
vault,
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
models,
|
|
132
|
+
defaultMode: user.defaultMode === "librarian" ? "librarian" : "researcher",
|
|
127
133
|
// A Sub-Sub folder inside an Obsidian vault also reads the vault's context file.
|
|
128
134
|
vaultContext: (user.vaultContext ??
|
|
129
135
|
(vault ? [join(vault, ".claude", "CLAUDE.md"), ...(isObsidianVault(dirname(vault)) ? [join(dirname(vault), ".claude", "CLAUDE.md")] : [])] : [])).map((p) => expand(p, home)),
|
package/dist/doctor.js
CHANGED
|
@@ -104,7 +104,7 @@ export function runChecks(cfg, env, deps = {}) {
|
|
|
104
104
|
out.push(starbuckCheck(cfg, env, run));
|
|
105
105
|
const agentDir = deps.agentDir ?? (env.SUBSUB_AGENT_DIR ? env.SUBSUB_AGENT_DIR : join(homedir(), ".subsub", "agent"));
|
|
106
106
|
out.push(hasLogin(agentDir)
|
|
107
|
-
? { name: "Model login", ok: true, detail: cfg.
|
|
107
|
+
? { name: "Model login", ok: true, detail: Object.keys(cfg.models).length ? Object.entries(cfg.models).map(([m, id]) => `${m} ${id}`).join(", ") : "pi's default model" }
|
|
108
108
|
: { name: "Model login", ok: false, detail: "no model provider yet", fix: "Type subsub, then /login." });
|
|
109
109
|
return out;
|
|
110
110
|
}
|
package/dist/init.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Writes:
|
|
6
6
|
* - the settings file (~/.config/subsub/config.json): profile, name, language,
|
|
7
|
-
* Sub-Sub folder,
|
|
7
|
+
* Sub-Sub folder, models;
|
|
8
8
|
* - the server settings file (~/.config/zotero-local-mcp/env, or the one that
|
|
9
9
|
* already exists): the Sub-Sub folder, tag list, alerts file, contact email.
|
|
10
10
|
* Other lines stay;
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
* Flags (for scripts and tests): --yes (take every default), --setup
|
|
20
20
|
* standard|fmup, --name, --about, --language, --folder (or --notes), --move
|
|
21
21
|
* yes|no, --profile, --tags health-sciences|health-informatics|any-field,
|
|
22
|
-
* --
|
|
22
|
+
* --email, --starbuck on|off, --models keep|later|opencode-go.
|
|
23
23
|
*/
|
|
24
24
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
25
25
|
import { homedir } from "node:os";
|
|
26
26
|
import { dirname, join, resolve } from "node:path";
|
|
27
27
|
import { createInterface } from "node:readline/promises";
|
|
28
28
|
import { fileURLToPath } from "node:url";
|
|
29
|
-
import { configPath,
|
|
29
|
+
import { configPath, DEFAULT_MODELS, expand, isProfile, loadConfig, parseEnvFile, PROFILES, readConfigFile, saveConfig, starbuckEnabled, withStarbuck, } from "./config.js";
|
|
30
30
|
import { defaultRunner, starbuckCheck } from "./doctor.js";
|
|
31
31
|
import { applyMoves, FOLDER_NAME, leftBehind, NOTE_DIRS, planMoves, SETTINGS_DIR, SETTINGS_FILES, subsubFolder } from "./layout.js";
|
|
32
32
|
import { PROFILE_SPECS } from "./profiles.js";
|
|
@@ -36,7 +36,7 @@ export const STARTER_TAGS = {
|
|
|
36
36
|
"health-informatics": "Health informatics: information systems, data, AI, digital health",
|
|
37
37
|
"any-field": "Any field: methods and document types only; Sub-Sub proposes topics from your library",
|
|
38
38
|
};
|
|
39
|
-
export const
|
|
39
|
+
export const TESTED_MODELS = DEFAULT_MODELS;
|
|
40
40
|
/** Set KEY=VALUE lines in an env file text; other lines and comments stay. */
|
|
41
41
|
export function upsertEnv(text, values) {
|
|
42
42
|
const lines = text ? text.replace(/\n$/, "").split("\n") : [];
|
|
@@ -125,11 +125,6 @@ export async function runInit(io, flags, env = process.env, deps = {}) {
|
|
|
125
125
|
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
126
|
const profileDefault = flags.profile ?? existing.profile ?? (fmup ? "reader" : "scholar");
|
|
127
127
|
const profile = await io.choose("Profile", PROFILES.map((p) => ({ value: p, label: `${PROFILE_SPECS[p].label}: ${PROFILE_SPECS[p].summary}` })), isProfile(profileDefault) ? profileDefault : "scholar");
|
|
128
|
-
explain("Library changes", "With library changes on, Sub-Sub can import, tag and repair items in Zotero. It shows a preview of every change, and nothing changes until you select Yes. With them off, it only reads, searches and writes notes. Switch at any time with /library on or /library off.");
|
|
129
|
-
const library = await io.choose("Library changes", [
|
|
130
|
-
{ value: "on", label: "On (recommended): every change shows a preview first" },
|
|
131
|
-
{ value: "off", label: "Off: read, search and write notes only" },
|
|
132
|
-
], flags.library ?? (existing.libraryChanges === false ? "off" : "on"));
|
|
133
128
|
// The tag list: a custom path in the server settings stays; otherwise Zotero/Zotero tags.md (moved, kept or new).
|
|
134
129
|
const vocabTarget = join(notes, SETTINGS_DIR, SETTINGS_FILES.tags);
|
|
135
130
|
const oldDefaults = new Set([join(from, "Systems", SETTINGS_FILES.tags), join(from, SETTINGS_DIR, SETTINGS_FILES.tags)]);
|
|
@@ -155,16 +150,17 @@ export async function runInit(io, flags, env = process.env, deps = {}) {
|
|
|
155
150
|
{ value: "off", label: "Off" },
|
|
156
151
|
{ value: "on", label: "On (downloads Starbuck now; uses the email above)" },
|
|
157
152
|
], flags.starbuck ?? (starbuckEnabled(current) ? "on" : "off"));
|
|
158
|
-
explain("Models", "Sub-Sub needs an account with a model provider. You can connect
|
|
153
|
+
explain("Models", "Sub-Sub needs an account with a model provider. Each mode can use its own model: the Librarian a fast, cheap one for tags and imports, the Researcher a stronger one for reading. You can connect a provider later in Sub-Sub with the model button (or /login in the terminal).");
|
|
159
154
|
const hadModel = "model" in existing || "models" in existing;
|
|
155
|
+
const show = (m) => (Object.keys(m).length ? Object.entries(m).map(([k, v]) => `${k} ${v}`).join(", ") : "choose in Sub-Sub");
|
|
160
156
|
const modelOptions = [
|
|
161
|
-
...(hadModel ? [{ value: "keep", label: `Keep: ${current.
|
|
157
|
+
...(hadModel ? [{ value: "keep", label: `Keep: ${show(current.models)}` }] : []),
|
|
162
158
|
{ value: "later", label: "Choose later in Sub-Sub" },
|
|
163
|
-
{ value: "opencode-go", label: `OpenCode Go, tested: ${
|
|
159
|
+
{ value: "opencode-go", label: `OpenCode Go, tested: ${TESTED_MODELS.librarian} for the Librarian, ${TESTED_MODELS.researcher} for the Researcher` },
|
|
164
160
|
];
|
|
165
161
|
const agentDir = initEnv.SUBSUB_AGENT_DIR ? expand(initEnv.SUBSUB_AGENT_DIR, home) : join(home, ".subsub", "agent");
|
|
166
162
|
const modelDefault = hadModel ? "keep" : hasProvider(agentDir, "opencode") ? "opencode-go" : "later";
|
|
167
|
-
const models = await io.choose("
|
|
163
|
+
const models = await io.choose("Models", modelOptions, flags.models ?? modelDefault);
|
|
168
164
|
// ---- write
|
|
169
165
|
if (move === "yes")
|
|
170
166
|
applyMoves(moves);
|
|
@@ -199,18 +195,17 @@ export async function runInit(io, flags, env = process.env, deps = {}) {
|
|
|
199
195
|
setup: fmup ? "fmup" : undefined,
|
|
200
196
|
vault: notes,
|
|
201
197
|
envFile,
|
|
202
|
-
libraryChanges:
|
|
203
|
-
defaultMode: undefined,
|
|
198
|
+
libraryChanges: undefined,
|
|
204
199
|
addons: withStarbuck(current.addons, starbuck === "on"),
|
|
205
200
|
};
|
|
206
|
-
//
|
|
207
|
-
patch.
|
|
201
|
+
// A model per mode; "model" (0.10, one for both) is replaced.
|
|
202
|
+
patch.model = undefined;
|
|
208
203
|
if (models === "keep")
|
|
209
|
-
patch.
|
|
204
|
+
patch.models = current.models;
|
|
210
205
|
if (models === "later")
|
|
211
|
-
patch.
|
|
206
|
+
patch.models = {};
|
|
212
207
|
if (models === "opencode-go")
|
|
213
|
-
patch.
|
|
208
|
+
patch.models = TESTED_MODELS;
|
|
214
209
|
saveConfig(patch, initEnv);
|
|
215
210
|
const zotero = await zoteroReachable(deps.fetch);
|
|
216
211
|
const starbuckNote = starbuck === "on" && deps.prepareStarbuck ? deps.prepareStarbuck(loadConfig(initEnv)) : "";
|
package/dist/look.js
CHANGED
|
@@ -63,7 +63,7 @@ export function libraryLine(st) {
|
|
|
63
63
|
export function headerLines(opts) {
|
|
64
64
|
const p = opts.paint ?? plain;
|
|
65
65
|
const w = Math.max(10, opts.width);
|
|
66
|
-
const modeLabel =
|
|
66
|
+
const modeLabel = (opts.mode === "librarian" ? "Librarian" : "Researcher") + (opts.profile ? `, ${opts.profile}` : "");
|
|
67
67
|
const lib = libraryLine(opts.library);
|
|
68
68
|
if (w < BANNER[0].length + 2) {
|
|
69
69
|
return [p.mark(cut(`Sub-Sub ${opts.version}`, w)), p.text(cut(`${modeLabel} | ${opts.model}`, w)), (lib.warn ? p.warn : p.muted)(cut(lib.text, w))];
|
|
@@ -78,7 +78,7 @@ export function headerLines(opts) {
|
|
|
78
78
|
else
|
|
79
79
|
out.push(p.dim(cut(`"${opts.quote[0]}"`, w)), p.dim(cut(` (${opts.quote[1]})`, w)));
|
|
80
80
|
}
|
|
81
|
-
out.push(p.dim(cut("/
|
|
81
|
+
out.push(p.dim(cut("/researcher /librarian /profile /history /undo | / for all commands", w)), "");
|
|
82
82
|
return out;
|
|
83
83
|
}
|
|
84
84
|
/**
|
package/dist/prompt.js
CHANGED
|
@@ -47,16 +47,13 @@ export function fill(text, cfg) {
|
|
|
47
47
|
.replaceAll("{{batch}}", String(profileSpec(cfg.profile).batch))
|
|
48
48
|
.replace(/[ \t]+\n/g, "\n");
|
|
49
49
|
}
|
|
50
|
-
export function roleText(cfg) {
|
|
51
|
-
const raw = readIf(join(ROLE_DIR,
|
|
50
|
+
export function roleText(mode, cfg) {
|
|
51
|
+
const raw = readIf(join(ROLE_DIR, `${mode}.md`)) ?? `You are Sub-Sub, the ${mode}.`;
|
|
52
52
|
return cfg ? fill(raw, cfg) : raw;
|
|
53
53
|
}
|
|
54
|
-
export function systemAddition(cfg, cwd
|
|
54
|
+
export function systemAddition(mode, cfg, cwd) {
|
|
55
55
|
const user = who(cfg);
|
|
56
|
-
const parts = [
|
|
57
|
-
if (!libraryChanges) {
|
|
58
|
-
parts.push(`Library changes are off: you read the library, search, write notes and queue imports. To import, tag or change items, ask ${user} to turn library changes on (/library on, or the switch in the browser view).`);
|
|
59
|
-
}
|
|
56
|
+
const parts = [`# Sub-Sub: ${mode === "librarian" ? "Librarian" : "Researcher"} mode`, roleText(mode, cfg)];
|
|
60
57
|
const shared = readIf(cfg.sharedRules);
|
|
61
58
|
if (shared) {
|
|
62
59
|
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/roles.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Which tools
|
|
2
|
+
* Which tools each mode may use, and which calls need the user's approval.
|
|
3
3
|
*/
|
|
4
4
|
import { profileSpec } from "./profiles.js";
|
|
5
5
|
export const FILE_TOOLS = ["read", "edit", "write", "grep", "find", "ls"];
|
|
6
|
-
/** Prefixes of the tools that Sub-Sub's servers provide (the gate checks the
|
|
6
|
+
/** Prefixes of the tools that Sub-Sub's servers provide (the gate checks the mode and the profile for these). */
|
|
7
7
|
export const SERVER_PREFIXES = ["zotero_", "scholar_", "verify_"];
|
|
8
8
|
export const ZOTERO_READ = [
|
|
9
9
|
"status",
|
|
@@ -43,33 +43,33 @@ export const CONFIRM_WRITES = new Set(["scholar_export_bibliography"]);
|
|
|
43
43
|
/** Zotero tools that look at the library or write only a note in the Sub-Sub folder. */
|
|
44
44
|
export const ZOTERO_CHECKS = ["tag_audit", "audit_metadata", "find_duplicates", "check_retractions", "missing_pdfs", "write_tag_review"].map((n) => `zotero_${n}`);
|
|
45
45
|
/**
|
|
46
|
-
* The
|
|
47
|
-
*
|
|
48
|
-
* then removes tools.
|
|
46
|
+
* The tools of a mode. The Librarian manages the library: Zotero tools and files.
|
|
47
|
+
* The Researcher has the same, plus searches, notes and reference checks. The
|
|
48
|
+
* profile then removes tools. Every library change is still previewed (gateKind).
|
|
49
49
|
*/
|
|
50
|
-
export function toolsFor(registered, opts) {
|
|
50
|
+
export function toolsFor(mode, registered, opts = {}) {
|
|
51
51
|
const has = new Set(registered);
|
|
52
52
|
const without = new Set(profileSpec(opts.profile ?? "editor").without);
|
|
53
53
|
const zotero = registered.filter((n) => n.startsWith("zotero_") && (opts.bench || !n.startsWith("zotero_bakeoff_")));
|
|
54
54
|
const wanted = [
|
|
55
55
|
...ZOTERO_READ,
|
|
56
56
|
...ZOTERO_CHECKS,
|
|
57
|
-
...
|
|
58
|
-
...registered.filter((n) => n.startsWith("scholar_") || n.startsWith("verify_")),
|
|
57
|
+
...zotero,
|
|
58
|
+
...(mode === "researcher" ? registered.filter((n) => n.startsWith("scholar_") || n.startsWith("verify_")) : []),
|
|
59
59
|
...FILE_TOOLS,
|
|
60
60
|
];
|
|
61
61
|
return [...new Set(wanted)].filter((n) => has.has(n) && !without.has(n));
|
|
62
62
|
}
|
|
63
63
|
/** Why a Sub-Sub tool is not available now, or undefined when it is. */
|
|
64
|
-
export function unavailableReason(name, registered, opts) {
|
|
65
|
-
if (toolsFor(registered, opts).includes(name))
|
|
64
|
+
export function unavailableReason(name, mode, registered, opts = {}) {
|
|
65
|
+
if (toolsFor(mode, registered, opts).includes(name))
|
|
66
66
|
return undefined;
|
|
67
67
|
const profile = opts.profile ?? "editor";
|
|
68
68
|
if (profileSpec(profile).without.includes(name)) {
|
|
69
69
|
return `${name} is not part of the ${profileSpec(profile).label} profile. The user can change the profile with /profile.`;
|
|
70
70
|
}
|
|
71
|
-
if (
|
|
72
|
-
return `${name}
|
|
71
|
+
if (mode === "librarian" && toolsFor("researcher", registered, opts).includes(name)) {
|
|
72
|
+
return `${name} is part of the Researcher, which also does everything the Librarian does. The user can switch with /researcher.`;
|
|
73
73
|
}
|
|
74
74
|
return `${name} is not available.`;
|
|
75
75
|
}
|
package/dist/subsub.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sub-Sub: a Zotero librarian and research assistant for pi.
|
|
3
3
|
*
|
|
4
|
-
* -
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* - Two modes: the Librarian manages the library (Zotero tools only); the
|
|
5
|
+
* Researcher does the same and also searches PubMed, Europe PMC and OpenAlex
|
|
6
|
+
* and writes notes. Each mode can have its own model.
|
|
7
7
|
* - An approval gate: every library change is previewed by the server (dry run)
|
|
8
8
|
* and shown to the user; nothing is applied without a yes.
|
|
9
9
|
* - Profiles (reader, scholar, author, editor) set what Sub-Sub writes and which
|
|
10
10
|
* tools it offers; /profile changes it.
|
|
11
|
-
* - Commands: /
|
|
12
|
-
* /researcher (the two modes before 0.10) turn library changes on and off.
|
|
11
|
+
* - Commands: /librarian, /researcher, /profile, /subsub, /history, /undo.
|
|
13
12
|
* - Add-on: with "addons": ["starbuck"], Starbuck runs as a third server
|
|
14
13
|
* ("verify") for reference checks.
|
|
15
14
|
*
|
|
@@ -29,6 +28,7 @@ import { describeArgs, formatPreview } from "./preview.js";
|
|
|
29
28
|
import { headerLines, paintPreview, QUOTES, schemeFromAnsi, themeName } from "./look.js";
|
|
30
29
|
import { systemAddition } from "./prompt.js";
|
|
31
30
|
import { gateKind, SERVER_PREFIXES, toolsFor, unavailableReason } from "./roles.js";
|
|
31
|
+
const MODE_ENTRY = "subsub-mode";
|
|
32
32
|
const PACKAGE_DIR = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
33
33
|
const BENCH_ENTRY = "subsub-bench";
|
|
34
34
|
function ownVersion() {
|
|
@@ -103,11 +103,10 @@ export async function createSubsub(pi, deps = {}) {
|
|
|
103
103
|
}
|
|
104
104
|
if (deps.bridge)
|
|
105
105
|
registerTools(deps.bridge);
|
|
106
|
-
pi.registerFlag("
|
|
107
|
-
|
|
108
|
-
let libraryChanges = cfg.libraryChanges;
|
|
106
|
+
pi.registerFlag("librarian", { description: "Start Sub-Sub in the Librarian mode", type: "boolean", default: false });
|
|
107
|
+
let mode = cfg.defaultMode;
|
|
109
108
|
const benchRun = Boolean(process.env.SUBSUB_BENCH_OUT);
|
|
110
|
-
const toolOpts = () => ({
|
|
109
|
+
const toolOpts = () => ({ profile, bench: benchRun });
|
|
111
110
|
let profile = cfg.profile;
|
|
112
111
|
const user = who(cfg);
|
|
113
112
|
/** True when started with the `subsub` command (not as a package inside plain pi). */
|
|
@@ -129,7 +128,7 @@ export async function createSubsub(pi, deps = {}) {
|
|
|
129
128
|
return;
|
|
130
129
|
const down = bridge ? Object.keys(bridge.errors) : ["zotero", "scholar"];
|
|
131
130
|
const state = {
|
|
132
|
-
|
|
131
|
+
mode,
|
|
133
132
|
profile,
|
|
134
133
|
profileLabel: profileSpec(profile).label,
|
|
135
134
|
model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : null,
|
|
@@ -192,7 +191,7 @@ export async function createSubsub(pi, deps = {}) {
|
|
|
192
191
|
const th = ctx.ui.theme;
|
|
193
192
|
return headerLines({
|
|
194
193
|
version,
|
|
195
|
-
|
|
194
|
+
mode,
|
|
196
195
|
profile: profileSpec(profile).label,
|
|
197
196
|
model: modelId,
|
|
198
197
|
library,
|
|
@@ -214,38 +213,28 @@ export async function createSubsub(pi, deps = {}) {
|
|
|
214
213
|
function status(ctx) {
|
|
215
214
|
modelId = ctx.model ? `${ctx.model.id}` : "no model";
|
|
216
215
|
const down = bridge ? Object.keys(bridge.errors) : ["zotero", "scholar"];
|
|
217
|
-
|
|
218
|
-
ctx.ui.setStatus("subsub", `Sub-Sub: ${lib} | ${profileSpec(profile).label} | ${modelId}${down.length ? ` | not running: ${down.join(", ")}` : ""}`);
|
|
216
|
+
ctx.ui.setStatus("subsub", `Sub-Sub: ${mode} | ${profileSpec(profile).label} | ${modelId}${down.length ? ` | not running: ${down.join(", ")}` : ""}`);
|
|
219
217
|
if (standalone)
|
|
220
|
-
ctx.ui.setTitle(
|
|
218
|
+
ctx.ui.setTitle(`Sub-Sub: ${mode}`);
|
|
221
219
|
headerTui?.requestRender();
|
|
222
220
|
webCtx = ctx;
|
|
223
221
|
emitWeb();
|
|
224
222
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
ctx.ui.notify(`Sub-Sub:
|
|
235
|
-
}
|
|
223
|
+
async function applyMode(ctx, next, remember) {
|
|
224
|
+
mode = next;
|
|
225
|
+
pi.setActiveTools(toolsFor(mode, registered(), toolOpts()));
|
|
226
|
+
const spec = cfg.models[mode];
|
|
227
|
+
if (spec) {
|
|
228
|
+
const i = spec.indexOf("/");
|
|
229
|
+
const model = i > 0 ? ctx.modelRegistry.find(spec.slice(0, i), spec.slice(i + 1)) : undefined;
|
|
230
|
+
const ok = model ? await pi.setModel(model) : false;
|
|
231
|
+
if (!ok)
|
|
232
|
+
ctx.ui.notify(`Sub-Sub: cannot use ${spec} (unknown model or no key); keeping the current model.`, "warning");
|
|
236
233
|
}
|
|
234
|
+
if (remember)
|
|
235
|
+
pi.appendEntry(MODE_ENTRY, { mode });
|
|
237
236
|
status(ctx);
|
|
238
237
|
}
|
|
239
|
-
async function applyModel(ctx) {
|
|
240
|
-
const spec = cfg.model;
|
|
241
|
-
if (!spec)
|
|
242
|
-
return;
|
|
243
|
-
const i = spec.indexOf("/");
|
|
244
|
-
const model = i > 0 ? ctx.modelRegistry.find(spec.slice(0, i), spec.slice(i + 1)) : undefined;
|
|
245
|
-
const ok = model ? await pi.setModel(model) : false;
|
|
246
|
-
if (!ok)
|
|
247
|
-
ctx.ui.notify(`Sub-Sub: cannot use ${spec} (unknown model or no key); keeping the current model.`, "warning");
|
|
248
|
-
}
|
|
249
238
|
pi.on("session_start", async (_event, ctx) => {
|
|
250
239
|
if (!bridge) {
|
|
251
240
|
const b = new Bridge();
|
|
@@ -253,13 +242,17 @@ export async function createSubsub(pi, deps = {}) {
|
|
|
253
242
|
bridge = b;
|
|
254
243
|
registerTools(b);
|
|
255
244
|
}
|
|
245
|
+
let restored;
|
|
246
|
+
for (const e of ctx.sessionManager.getBranch()) {
|
|
247
|
+
const m = e.customType === MODE_ENTRY ? e.data?.mode : undefined;
|
|
248
|
+
if (m === "librarian" || m === "researcher")
|
|
249
|
+
restored = m;
|
|
250
|
+
}
|
|
256
251
|
// Judge light or dark before Sub-Sub changes the theme.
|
|
257
252
|
if (lookOn(ctx))
|
|
258
253
|
scheme ??= schemeFromAnsi(ctx.ui.theme.getFgAnsi("text"), ctx.ui.theme.name);
|
|
259
|
-
await applyModel(ctx);
|
|
260
254
|
applyTheme(ctx);
|
|
261
|
-
|
|
262
|
-
applyLibrary(ctx, pi.getFlag("read-only") ? false : pi.getFlag("librarian") ? true : cfg.libraryChanges, false);
|
|
255
|
+
await applyMode(ctx, restored ?? (pi.getFlag("librarian") ? "librarian" : cfg.defaultMode), false);
|
|
263
256
|
if (lookOn(ctx)) {
|
|
264
257
|
setHeader(ctx);
|
|
265
258
|
void refreshLibrary();
|
|
@@ -289,7 +282,7 @@ export async function createSubsub(pi, deps = {}) {
|
|
|
289
282
|
return { action: "handled" };
|
|
290
283
|
});
|
|
291
284
|
pi.on("before_agent_start", async (event, ctx) => ({
|
|
292
|
-
systemPrompt: `${event.systemPrompt}\n\n${systemAddition({ ...cfg, profile }, ctx.cwd
|
|
285
|
+
systemPrompt: `${event.systemPrompt}\n\n${systemAddition(mode, { ...cfg, profile }, ctx.cwd)}`,
|
|
293
286
|
}));
|
|
294
287
|
/** Model test (subsub-bench): record every change instead of asking, and apply none. */
|
|
295
288
|
const benchOut = process.env.SUBSUB_BENCH_OUT ? expand(process.env.SUBSUB_BENCH_OUT) : undefined;
|
|
@@ -303,7 +296,7 @@ export async function createSubsub(pi, deps = {}) {
|
|
|
303
296
|
coerceArgs(schema, input);
|
|
304
297
|
resolvePathArgs(name, input, (p) => normalizeToolPath(p, ctx.cwd));
|
|
305
298
|
if (SERVER_PREFIXES.some((p) => name.startsWith(p))) {
|
|
306
|
-
const why = unavailableReason(name, registered(), toolOpts());
|
|
299
|
+
const why = unavailableReason(name, mode, registered(), toolOpts());
|
|
307
300
|
if (why) {
|
|
308
301
|
if (benchOut)
|
|
309
302
|
record({ kind: "wrong_mode", tool: name, input });
|
|
@@ -426,35 +419,18 @@ export async function createSubsub(pi, deps = {}) {
|
|
|
426
419
|
`for ${user}.${shown}`,
|
|
427
420
|
};
|
|
428
421
|
}
|
|
429
|
-
const libraryNote = (on) => on
|
|
430
|
-
? "Sub-Sub: library changes on. Every change shows a preview first."
|
|
431
|
-
: "Sub-Sub: library changes off. Sub-Sub reads, searches and writes notes; it does not change the library.";
|
|
432
|
-
pi.registerCommand("library", {
|
|
433
|
-
description: "Sub-Sub: turn library changes on or off (/library on, /library off)",
|
|
434
|
-
handler: async (args, ctx) => {
|
|
435
|
-
const a = args.trim().toLowerCase();
|
|
436
|
-
const on = a === "on" ? true : a === "off" ? false : a === "" ? !libraryChanges : undefined;
|
|
437
|
-
if (on === undefined) {
|
|
438
|
-
ctx.ui.notify("Type /library on or /library off.", "warning");
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
applyLibrary(ctx, on, true);
|
|
442
|
-
ctx.ui.notify(libraryNote(on), "info");
|
|
443
|
-
},
|
|
444
|
-
});
|
|
445
|
-
// The two modes before 0.10, kept so that old habits and notes still work.
|
|
446
422
|
pi.registerCommand("librarian", {
|
|
447
|
-
description: "Sub-Sub:
|
|
423
|
+
description: "Sub-Sub: switch to the Librarian (manages the library: tags, imports, metadata, PDFs)",
|
|
448
424
|
handler: async (_args, ctx) => {
|
|
449
|
-
|
|
450
|
-
ctx.ui.notify(
|
|
425
|
+
await applyMode(ctx, "librarian", true);
|
|
426
|
+
ctx.ui.notify("Sub-Sub: Librarian mode. For searches and notes, switch to /researcher.", "info");
|
|
451
427
|
},
|
|
452
428
|
});
|
|
453
429
|
pi.registerCommand("researcher", {
|
|
454
|
-
description: "Sub-Sub:
|
|
430
|
+
description: "Sub-Sub: switch to the Researcher (searches, notes, reviews, and everything the Librarian does)",
|
|
455
431
|
handler: async (_args, ctx) => {
|
|
456
|
-
|
|
457
|
-
ctx.ui.notify(
|
|
432
|
+
await applyMode(ctx, "researcher", true);
|
|
433
|
+
ctx.ui.notify("Sub-Sub: Researcher mode. It searches, writes notes and also changes the library, with a preview.", "info");
|
|
458
434
|
},
|
|
459
435
|
});
|
|
460
436
|
pi.registerCommand("profile", {
|
|
@@ -477,14 +453,14 @@ export async function createSubsub(pi, deps = {}) {
|
|
|
477
453
|
catch (err) {
|
|
478
454
|
saved = ` Not saved: ${err.message}`;
|
|
479
455
|
}
|
|
480
|
-
|
|
456
|
+
await applyMode(ctx, mode, false);
|
|
481
457
|
ctx.ui.notify(`Sub-Sub: ${profileSpec(profile).label} profile (${profileSpec(profile).summary}).${saved}`, "info");
|
|
482
458
|
},
|
|
483
459
|
});
|
|
484
460
|
pi.registerCommand("subsub", {
|
|
485
461
|
description: "Sub-Sub: Zotero connection and library overview",
|
|
486
462
|
handler: async (_args, ctx) => {
|
|
487
|
-
const lines = [`
|
|
463
|
+
const lines = [`Mode: ${mode}. Profile: ${profileSpec(profile).label}. Model: ${ctx.model?.id ?? "none"}.`];
|
|
488
464
|
if (!bridge) {
|
|
489
465
|
ctx.ui.notify(`${lines[0]}\nThe Zotero servers are not running. Try /reload.`, "warning");
|
|
490
466
|
return;
|