@tiagojct/subsub 0.9.1 → 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 +15 -16
- package/dist/bench.js +9 -7
- package/dist/bridge.js +2 -2
- package/dist/config.js +21 -8
- package/dist/doctor.js +2 -2
- package/dist/init.js +17 -10
- package/dist/look.js +3 -3
- package/dist/profiles.js +6 -6
- package/dist/prompt.js +2 -2
- package/dist/roles.js +25 -16
- package/dist/subsub.js +16 -13
- package/dist/web.js +4 -5
- package/docs/Subsub.md +18 -18
- package/package.json +1 -1
- package/prompts/alert.md +1 -1
- package/prompts/clean-tags.md +1 -1
- package/prompts/compare.md +1 -1
- package/prompts/digest.md +2 -2
- package/prompts/gaps.md +2 -2
- package/prompts/import-queue.md +1 -1
- package/prompts/lit-note.md +1 -1
- package/prompts/lit.md +2 -2
- package/prompts/manuscript.md +1 -1
- package/prompts/request-copy.md +1 -1
- package/prompts/review.md +1 -1
- package/prompts/synthesis.md +1 -1
- package/prompts/tag-batch.md +1 -1
- package/prompts/verify.md +1 -1
- package/roles/librarian.md +1 -1
- package/roles/researcher.md +49 -5
- package/src/bench.ts +9 -7
- package/src/bridge.ts +2 -2
- package/src/config.ts +25 -12
- package/src/doctor.ts +2 -2
- package/src/init.ts +19 -9
- package/src/look.ts +4 -5
- package/src/profiles.ts +6 -6
- package/src/prompt.ts +2 -2
- package/src/roles.ts +35 -18
- package/src/subsub.ts +18 -14
- package/src/web.ts +4 -5
- package/templates/Zotero agent.md +6 -6
- package/web/app.css +3 -2
- package/web/app.js +25 -13
- package/web/i18n.js +39 -29
- package/web/index.html +1 -1
package/README.md
CHANGED
|
@@ -17,9 +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
|
-
* **Two
|
|
21
|
-
* 📚 **The Librarian**: Cleans tags, imports references (DOI, PMID, ISBN), deduplicates, fixes metadata, retrieves open-access PDFs, creates collections, and checks retractions. *Has no web search tools; imports and PDFs come from Crossref, Unpaywall and Open Library.*
|
|
22
|
-
* 🔬 **The Researcher**: Reads items, searches PubMed and OpenAlex, finds gaps, checks citations in manuscripts, and writes structured Markdown notes in your Sub-Sub folder (it can sit inside an Obsidian vault). *Cannot modify library records.*
|
|
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.
|
|
23
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.
|
|
24
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.
|
|
25
23
|
|
|
@@ -32,7 +30,7 @@ Profiles calibrate how much Sub-Sub drafts and executes for you. Choose your pro
|
|
|
32
30
|
| Profile | Description |
|
|
33
31
|
|---|---|
|
|
34
32
|
| **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
|
|
33
|
+
| **Scholar** | Full research workflow: literature notes, conceptual syntheses, database searches, alerts, and all library changes. |
|
|
36
34
|
| **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
35
|
| **Editor** | Everything unlocked, including drafting full manuscript sections and abstracts upon request. |
|
|
38
36
|
|
|
@@ -106,7 +104,8 @@ Starting Sub-Sub with `subsub web` (or `subsub serve`) launches the browser appl
|
|
|
106
104
|
|
|
107
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>.
|
|
108
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.
|
|
109
|
-
* **
|
|
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
|
+
* **Theme Switcher**: Header toggle between **Light**, **Dark**, and **System** themes.
|
|
110
109
|
* **One-Click Copy**: Built-in copy buttons on all code snippets, BibTeX records, and assistant literature notes.
|
|
111
110
|
* **Session Search & Markdown Export**: Filter conversation history in real time and download full discussions as Markdown notes.
|
|
112
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.
|
|
@@ -116,11 +115,11 @@ Starting Sub-Sub with `subsub web` (or `subsub serve`) launches the browser appl
|
|
|
116
115
|
|
|
117
116
|
## Usage
|
|
118
117
|
|
|
119
|
-
###
|
|
118
|
+
### Starting and commands
|
|
120
119
|
|
|
121
120
|
```sh
|
|
122
|
-
subsub # Start in
|
|
123
|
-
subsub --librarian # Start in
|
|
121
|
+
subsub # Start in the terminal (Researcher)
|
|
122
|
+
subsub --librarian # Start in the Librarian
|
|
124
123
|
subsub web # Start browser view
|
|
125
124
|
subsub -c # Continue the previous session
|
|
126
125
|
subsub -r # Resume an older session
|
|
@@ -128,8 +127,8 @@ subsub -r # Resume an older session
|
|
|
128
127
|
|
|
129
128
|
Commands available within Sub-Sub:
|
|
130
129
|
|
|
131
|
-
* `/
|
|
132
|
-
* `/
|
|
130
|
+
* `/researcher`: Change to the Researcher (research and library). Each mode can have its own model.
|
|
131
|
+
* `/librarian`: Change to the Librarian (library only).
|
|
133
132
|
* `/profile [name]`: View or change active profile (`reader`, `scholar`, `author`, `editor`).
|
|
134
133
|
* `/subsub`: Display Zotero connection health and library statistics.
|
|
135
134
|
* `/history`: List recent library changes with journal IDs.
|
|
@@ -139,11 +138,11 @@ Commands available within Sub-Sub:
|
|
|
139
138
|
|
|
140
139
|
Type the command followed by arguments:
|
|
141
140
|
|
|
142
|
-
* **
|
|
141
|
+
* **Library (both modes)**:
|
|
143
142
|
* `/tag-batch [size]`: Tag untagged items using your tag vocabulary.
|
|
144
143
|
* `/clean-tags`: Find irregular tags and reconcile with your vocabulary.
|
|
145
144
|
* `/import-queue [path]`: Import pending references from an inbox note.
|
|
146
|
-
* **Researcher**:
|
|
145
|
+
* **Research (Researcher)**:
|
|
147
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).
|
|
148
147
|
* `/compare <question or citekeys>`: Source matrix (claim, evidence type, caveats, confidence), agreements and disagreements (Scholar, Author, Editor).
|
|
149
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).
|
|
@@ -177,12 +176,12 @@ subsub review apply 13-31
|
|
|
177
176
|
| `about` | string | Brief background context about your research domain | `null` |
|
|
178
177
|
| `language` | string | Response language, or `"auto"` to match your input | `"English"` |
|
|
179
178
|
| `vault` | string | Path to your Sub-Sub folder | `$ZOTERO_VAULT` |
|
|
180
|
-
| `models` | object |
|
|
181
|
-
| `defaultMode
|
|
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"` |
|
|
182
181
|
| `serverDir` | string | Local development path for `zotero-local-mcp` | PyPI package |
|
|
183
182
|
| `look` | boolean| Show header banners and status lines in terminal | `true` |
|
|
184
183
|
| `quotes` | boolean| Display quotes from *Moby-Dick* in terminal banner | `true` |
|
|
185
|
-
| `themes` |
|
|
184
|
+
| `themes` | string or false | Terminal theme family (`subsub-glauca` or `subsub-try-works`); `false` keeps your pi theme | `"subsub-glauca"` |
|
|
186
185
|
| `addons` | array | Optional servers; `["starbuck"]` adds reference checks | `[]` |
|
|
187
186
|
| `starbuckDir` | string | Local Starbuck folder (used when it has a `pyproject.toml`) | `~/Projects/starbuck` |
|
|
188
187
|
|
|
@@ -203,7 +202,7 @@ Sub-Sub finds open-access copies (Unpaywall, Europe PMC). For other papers, use
|
|
|
203
202
|
2. Optional: set `"starbuckDir"` to a local copy of Starbuck. Without it, Sub-Sub runs the pinned Starbuck release with uv.
|
|
204
203
|
3. Start Sub-Sub again and run `subsub doctor`. The line `Starbuck` must show `ok`.
|
|
205
204
|
|
|
206
|
-
The
|
|
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.
|
|
207
206
|
|
|
208
207
|
---
|
|
209
208
|
|
package/dist/bench.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* with no changes applied. See docs/Subsub.md ("Model test").
|
|
4
4
|
*
|
|
5
5
|
* subsub-bench prepare [--seed 7] pick the tasks (read-only)
|
|
6
|
-
* subsub-bench run [--librarian a,b] [--researcher c,d] [--parallel 3] [--only task]
|
|
6
|
+
* subsub-bench run [--models a,b] [--librarian a,b] [--researcher c,d] [--parallel 3] [--only task]
|
|
7
|
+
* (--models sets the models for both task groups.)
|
|
7
8
|
* subsub-bench score objective checks -> results.md
|
|
8
9
|
*
|
|
9
10
|
* Results go to ~/Projects/subsub/bench-results/<date>/ (or --dir). Each model gets
|
|
@@ -19,8 +20,8 @@ import { expand, loadConfig, starbuckCommand, starbuckEnabled } from "./config.j
|
|
|
19
20
|
import { findUv } from "./subsub.js";
|
|
20
21
|
export const PACKAGE_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
21
22
|
export const DEFAULT_MODELS = {
|
|
22
|
-
librarian: ["
|
|
23
|
-
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"],
|
|
24
25
|
};
|
|
25
26
|
export const TASKS = {
|
|
26
27
|
librarian: ["tagging", "facet_fix", "import"],
|
|
@@ -530,9 +531,10 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
530
531
|
if (!cfg.vault)
|
|
531
532
|
throw new Error("No vault configured (ZOTERO_VAULT).");
|
|
532
533
|
const t = JSON.parse(readFileSync(tasksFile, "utf8"));
|
|
534
|
+
const both = arg(argv, "models")?.split(",").filter(Boolean);
|
|
533
535
|
const lists = {
|
|
534
|
-
librarian: arg(argv, "librarian")?.split(",").filter(Boolean) ?? DEFAULT_MODELS.librarian,
|
|
535
|
-
researcher: arg(argv, "researcher")?.split(",").filter(Boolean) ?? DEFAULT_MODELS.researcher,
|
|
536
|
+
librarian: arg(argv, "librarian")?.split(",").filter(Boolean) ?? both ?? DEFAULT_MODELS.librarian,
|
|
537
|
+
researcher: arg(argv, "researcher")?.split(",").filter(Boolean) ?? both ?? DEFAULT_MODELS.researcher,
|
|
536
538
|
};
|
|
537
539
|
const only = arg(argv, "only")?.split(",");
|
|
538
540
|
const codes = codesFor([...new Set([...lists.librarian, ...lists.researcher])], dir);
|
|
@@ -556,7 +558,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
556
558
|
if (!prompt)
|
|
557
559
|
return;
|
|
558
560
|
const cf = join(cfgDir, `${j.code}.json`);
|
|
559
|
-
writeFileSync(cf, JSON.stringify({ ...userCfg, profile: "editor", models: { librarian: `opencode-go/${j.model}`, researcher: `opencode-go/${j.model}` } }));
|
|
561
|
+
writeFileSync(cf, JSON.stringify({ ...userCfg, profile: "editor", models: { librarian: `opencode-go/${j.model}`, researcher: `opencode-go/${j.model}` }, model: undefined, libraryChanges: undefined }));
|
|
560
562
|
const s = await runOne({ ...j, prompt, out, cwd: cfg.vault, configFile: cf, timeoutSec });
|
|
561
563
|
n++;
|
|
562
564
|
console.log(`${n}/${jobs.length} ${j.code} ${j.task} ${s.seconds}s ${s.error ?? (s.settled ? "done" : "?")}`);
|
|
@@ -585,5 +587,5 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
585
587
|
console.log(`Wrote ${join(dir, "results.md")}`);
|
|
586
588
|
return;
|
|
587
589
|
}
|
|
588
|
-
console.log("Usage: subsub-bench prepare | run [--librarian a,b] [--researcher c,d] [--only task] [--parallel 3] | score [--dir DIR]");
|
|
590
|
+
console.log("Usage: subsub-bench prepare | run [--models a,b] [--librarian a,b] [--researcher c,d] [--only task] [--parallel 3] | score [--dir DIR]");
|
|
589
591
|
}
|
package/dist/bridge.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Bridge to the Python MCP servers of zotero-local-mcp (
|
|
3
|
-
*
|
|
2
|
+
* Bridge to the Python MCP servers of zotero-local-mcp ("zotero" for the library,
|
|
3
|
+
* "scholar" for searches and notes). Starts them over stdio, lists their tools, and calls them.
|
|
4
4
|
* All safety logic (dry runs, version checks, journal, vocabulary, the researcher's
|
|
5
5
|
* note-only client) stays in the Python servers.
|
|
6
6
|
*/
|
package/dist/config.js
CHANGED
|
@@ -6,10 +6,14 @@
|
|
|
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
|
-
* "models": { "librarian": "opencode-go/
|
|
9
|
+
* "models": { "librarian": "opencode-go/mimo-v2.6-flash", "researcher": "opencode-go/mimo-v2.6-pro" },
|
|
10
10
|
* "defaultMode": "researcher"
|
|
11
11
|
* }
|
|
12
12
|
*
|
|
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
|
+
*
|
|
13
17
|
* `subsub init` writes this file. Without "serverDir" (or when that folder does
|
|
14
18
|
* not exist), the Zotero servers run from PyPI with uv.
|
|
15
19
|
*
|
|
@@ -22,6 +26,11 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
22
26
|
import { homedir } from "node:os";
|
|
23
27
|
import { dirname, join, resolve } from "node:path";
|
|
24
28
|
import { isObsidianVault, settingsFile } from "./layout.js";
|
|
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
|
+
};
|
|
25
34
|
export const PROFILES = ["reader", "scholar", "author", "editor"];
|
|
26
35
|
/** The Zotero server release that this Sub-Sub version runs when there is no local server folder. */
|
|
27
36
|
export const SERVER_VERSION = "0.5.1";
|
|
@@ -101,7 +110,14 @@ export function loadConfig(env = process.env) {
|
|
|
101
110
|
const envValues = envFile && existsSync(envFile) ? parseEnvFile(envFile) : {};
|
|
102
111
|
const vaultRaw = user.vault ?? env.ZOTERO_VAULT ?? envValues.ZOTERO_VAULT;
|
|
103
112
|
const vault = vaultRaw ? expand(vaultRaw, home) : undefined;
|
|
104
|
-
|
|
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];
|
|
120
|
+
const themes = user.themes === false ? false : typeof user.themes === "string" ? user.themes : user.themes?.researcher;
|
|
105
121
|
return {
|
|
106
122
|
configFile: file,
|
|
107
123
|
profile: isProfile(user.profile) ? user.profile : "scholar",
|
|
@@ -112,11 +128,8 @@ export function loadConfig(env = process.env) {
|
|
|
112
128
|
serverDir: expand(user.serverDir ?? "~/Projects/zotero-local-mcp", home),
|
|
113
129
|
envFile,
|
|
114
130
|
vault,
|
|
115
|
-
models
|
|
116
|
-
|
|
117
|
-
researcher: "opencode-go/mimo-v2.6-pro",
|
|
118
|
-
},
|
|
119
|
-
defaultMode: mode,
|
|
131
|
+
models,
|
|
132
|
+
defaultMode: user.defaultMode === "librarian" ? "librarian" : "researcher",
|
|
120
133
|
// A Sub-Sub folder inside an Obsidian vault also reads the vault's context file.
|
|
121
134
|
vaultContext: (user.vaultContext ??
|
|
122
135
|
(vault ? [join(vault, ".claude", "CLAUDE.md"), ...(isObsidianVault(dirname(vault)) ? [join(dirname(vault), ".claude", "CLAUDE.md")] : [])] : [])).map((p) => expand(p, home)),
|
|
@@ -129,7 +142,7 @@ export function loadConfig(env = process.env) {
|
|
|
129
142
|
uvPath: user.uvPath ? expand(user.uvPath, home) : undefined,
|
|
130
143
|
look: user.look,
|
|
131
144
|
quotes: user.quotes,
|
|
132
|
-
themes
|
|
145
|
+
themes,
|
|
133
146
|
addons: Array.isArray(user.addons) ? user.addons.filter((a) => typeof a === "string") : [],
|
|
134
147
|
starbuckDir: expand(user.starbuckDir ?? "~/Projects/starbuck", home),
|
|
135
148
|
};
|
package/dist/doctor.js
CHANGED
|
@@ -92,7 +92,7 @@ export function runChecks(cfg, env, deps = {}) {
|
|
|
92
92
|
fix: v.problems?.length
|
|
93
93
|
? `Fix these lines in the tag list: ${v.problems.slice(0, 3).join("; ")}`
|
|
94
94
|
: noTopics
|
|
95
|
-
? "In Sub-Sub
|
|
95
|
+
? "In Sub-Sub, ask: propose topics for my tag list."
|
|
96
96
|
: undefined,
|
|
97
97
|
});
|
|
98
98
|
}
|
|
@@ -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: Object.keys(cfg.models).length ? Object.
|
|
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
|
-
*
|
|
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;
|
|
@@ -26,7 +26,7 @@ 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, expand, isProfile, loadConfig, parseEnvFile, PROFILES, readConfigFile, saveConfig, starbuckEnabled, withStarbuck, } from "./config.js";
|
|
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";
|
|
@@ -34,9 +34,9 @@ const TEMPLATES = resolve(dirname(fileURLToPath(import.meta.url)), "..", "templa
|
|
|
34
34
|
export const STARTER_TAGS = {
|
|
35
35
|
"health-sciences": "Health sciences: clinical areas, public health, methods and study designs",
|
|
36
36
|
"health-informatics": "Health informatics: information systems, data, AI, digital health",
|
|
37
|
-
"any-field": "Any field: methods and document types only;
|
|
37
|
+
"any-field": "Any field: methods and document types only; Sub-Sub proposes topics from your library",
|
|
38
38
|
};
|
|
39
|
-
export const TESTED_MODELS =
|
|
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") : [];
|
|
@@ -140,24 +140,26 @@ export async function runInit(io, flags, env = process.env, deps = {}) {
|
|
|
140
140
|
io.say(`Tag list: keeping ${vocab}.`);
|
|
141
141
|
}
|
|
142
142
|
else {
|
|
143
|
-
explain("Tag list", `
|
|
143
|
+
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
144
|
tags = await io.choose("Starter tag list", Object.entries(STARTER_TAGS).map(([value, label]) => ({ value, label })), flags.tags ?? (fmup ? "health-sciences" : "any-field"));
|
|
145
145
|
}
|
|
146
146
|
explain("Contact email", "Unpaywall needs an email address to find open-access PDFs. Sub-Sub sends it only to Unpaywall, Crossref, OpenAlex and PubMed, as they ask.");
|
|
147
147
|
const email = (await io.ask("Email (optional)", flags.email ?? envValues.ZOTERO_CONTACT_EMAIL ?? "")).trim();
|
|
148
|
-
explain("Reference checks (Starbuck)", "An optional add-on for manuscripts: it checks that each cited work exists, matches its citation and was not retracted.
|
|
148
|
+
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
149
|
const starbuck = await io.choose("Reference checks", [
|
|
150
150
|
{ value: "off", label: "Off" },
|
|
151
151
|
{ value: "on", label: "On (downloads Starbuck now; uses the email above)" },
|
|
152
152
|
], flags.starbuck ?? (starbuckEnabled(current) ? "on" : "off"));
|
|
153
|
-
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).");
|
|
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");
|
|
154
156
|
const modelOptions = [
|
|
155
|
-
...(
|
|
157
|
+
...(hadModel ? [{ value: "keep", label: `Keep: ${show(current.models)}` }] : []),
|
|
156
158
|
{ value: "later", label: "Choose later in Sub-Sub" },
|
|
157
|
-
{ value: "opencode-go", label: `OpenCode Go, tested: ${TESTED_MODELS.librarian} for the
|
|
159
|
+
{ value: "opencode-go", label: `OpenCode Go, tested: ${TESTED_MODELS.librarian} for the Librarian, ${TESTED_MODELS.researcher} for the Researcher` },
|
|
158
160
|
];
|
|
159
161
|
const agentDir = initEnv.SUBSUB_AGENT_DIR ? expand(initEnv.SUBSUB_AGENT_DIR, home) : join(home, ".subsub", "agent");
|
|
160
|
-
const modelDefault =
|
|
162
|
+
const modelDefault = hadModel ? "keep" : hasProvider(agentDir, "opencode") ? "opencode-go" : "later";
|
|
161
163
|
const models = await io.choose("Models", modelOptions, flags.models ?? modelDefault);
|
|
162
164
|
// ---- write
|
|
163
165
|
if (move === "yes")
|
|
@@ -193,8 +195,13 @@ export async function runInit(io, flags, env = process.env, deps = {}) {
|
|
|
193
195
|
setup: fmup ? "fmup" : undefined,
|
|
194
196
|
vault: notes,
|
|
195
197
|
envFile,
|
|
198
|
+
libraryChanges: undefined,
|
|
196
199
|
addons: withStarbuck(current.addons, starbuck === "on"),
|
|
197
200
|
};
|
|
201
|
+
// A model per mode; "model" (0.10, one for both) is replaced.
|
|
202
|
+
patch.model = undefined;
|
|
203
|
+
if (models === "keep")
|
|
204
|
+
patch.models = current.models;
|
|
198
205
|
if (models === "later")
|
|
199
206
|
patch.models = {};
|
|
200
207
|
if (models === "opencode-go")
|
package/dist/look.js
CHANGED
|
@@ -20,7 +20,8 @@ export const QUOTES = [
|
|
|
20
20
|
["God keep me from ever completing anything.", "Moby-Dick, ch. 32"],
|
|
21
21
|
["Call me Ishmael.", "Moby-Dick, ch. 1"],
|
|
22
22
|
];
|
|
23
|
-
|
|
23
|
+
/** The terminal theme, without -dark/-light. subsub-try-works is the other one shipped. */
|
|
24
|
+
export const DEFAULT_THEME = "subsub-glauca";
|
|
24
25
|
/** Light or dark terminal, judged from the text colour of the current theme (dark text means a light background). */
|
|
25
26
|
export function schemeFromAnsi(fgAnsi, themeName) {
|
|
26
27
|
if (themeName?.endsWith("-light") || themeName === "light")
|
|
@@ -41,8 +42,7 @@ export function schemeFromAnsi(fgAnsi, themeName) {
|
|
|
41
42
|
}
|
|
42
43
|
return "dark";
|
|
43
44
|
}
|
|
44
|
-
export function themeName(
|
|
45
|
-
const base = map[mode];
|
|
45
|
+
export function themeName(scheme, base = DEFAULT_THEME) {
|
|
46
46
|
return base ? `${base}-${scheme}` : undefined;
|
|
47
47
|
}
|
|
48
48
|
const plain = { mark: (s) => s, text: (s) => s, muted: (s) => s, dim: (s) => s, warn: (s) => s };
|
package/dist/profiles.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* - reader: explains each step; reading notes are quotes with page numbers and
|
|
7
7
|
* questions; no summaries or syntheses; no bulk library changes.
|
|
8
|
-
* - scholar: literature notes, syntheses, searches, alerts;
|
|
8
|
+
* - scholar: literature notes, syntheses, searches, alerts; all library changes.
|
|
9
9
|
* - author: scholar plus manuscript work: citation check, bibliography, comments
|
|
10
10
|
* on the argument. Does not write the manuscript's paragraphs.
|
|
11
11
|
* - editor: everything, including drafting text for a manuscript on request.
|
|
@@ -26,13 +26,13 @@ export const PROFILE_SPECS = {
|
|
|
26
26
|
"- A literature note is a reading note: the full reference, then direct quotes with page numbers (or section names), and under each quote one question for {{user}} to answer. Do not write summaries, syntheses, conclusions or paragraphs that {{user}} could hand in as their own text. When a prompt or the shared rules ask for a literature note or a synthesis, make a reading note instead and say so.",
|
|
27
27
|
"- For searches, list works with one line each on what they study, not on what they conclude.",
|
|
28
28
|
"- If {{user}} asks for a summary, a synthesis or text for an assignment, say that the Reader profile does not write these, and offer quotes and questions. {{User}} can change the profile with /profile.",
|
|
29
|
-
"-
|
|
29
|
+
"- Tag review notes: at most 10 items. Explain each proposed tag in the Reason column.",
|
|
30
30
|
].join("\n"),
|
|
31
31
|
},
|
|
32
32
|
scholar: {
|
|
33
33
|
name: "scholar",
|
|
34
34
|
label: "Scholar",
|
|
35
|
-
summary: "literature notes, syntheses, searches and alerts;
|
|
35
|
+
summary: "literature notes, syntheses, searches and alerts; all library changes",
|
|
36
36
|
without: MANUSCRIPT,
|
|
37
37
|
withoutPrompts: ["review"],
|
|
38
38
|
batch: 25,
|
|
@@ -40,7 +40,7 @@ export const PROFILE_SPECS = {
|
|
|
40
40
|
"Profile: Scholar.",
|
|
41
41
|
"- Write literature notes and syntheses from what you read, with a source for every claim.",
|
|
42
42
|
"- Do not draft text for {{user}}'s manuscripts, theses or assignments. If asked, say that the Author and Editor profiles help with manuscripts (/profile).",
|
|
43
|
-
"-
|
|
43
|
+
"- Tag review notes: 25 items.",
|
|
44
44
|
].join("\n"),
|
|
45
45
|
},
|
|
46
46
|
author: {
|
|
@@ -55,7 +55,7 @@ export const PROFILE_SPECS = {
|
|
|
55
55
|
"- Write literature notes and syntheses from what you read, with a source for every claim.",
|
|
56
56
|
"- Help with {{user}}'s manuscripts: check citations against the library, export the bibliography, and comment on the argument, the structure and the evidence that is missing, with the sources that could support each point.",
|
|
57
57
|
"- Do not write the manuscript's paragraphs. Say what is missing and where the support is; {{user}} writes the text. The Editor profile drafts text (/profile).",
|
|
58
|
-
"-
|
|
58
|
+
"- Tag review notes: 25 items.",
|
|
59
59
|
].join("\n"),
|
|
60
60
|
},
|
|
61
61
|
editor: {
|
|
@@ -69,7 +69,7 @@ export const PROFILE_SPECS = {
|
|
|
69
69
|
"Profile: Editor. All tools.",
|
|
70
70
|
"- Write literature notes and syntheses from what you read, with a source for every claim.",
|
|
71
71
|
"- When {{user}} asks, draft text for a manuscript: cite library items as [@citekey], use only claims from sources you read, and mark drafted text clearly so that {{user}} can check it.",
|
|
72
|
-
"-
|
|
72
|
+
"- Tag review notes: 25 items unless {{user}} asks for more.",
|
|
73
73
|
].join("\n"),
|
|
74
74
|
},
|
|
75
75
|
};
|
package/dist/prompt.js
CHANGED
|
@@ -48,12 +48,12 @@ export function fill(text, cfg) {
|
|
|
48
48
|
.replace(/[ \t]+\n/g, "\n");
|
|
49
49
|
}
|
|
50
50
|
export function roleText(mode, cfg) {
|
|
51
|
-
const raw = readIf(join(ROLE_DIR, `${mode}.md`)) ?? `You are the ${mode}.`;
|
|
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
54
|
export function systemAddition(mode, cfg, cwd) {
|
|
55
55
|
const user = who(cfg);
|
|
56
|
-
const parts = [`# Sub-Sub: ${mode} mode`, roleText(mode, cfg)];
|
|
56
|
+
const parts = [`# Sub-Sub: ${mode === "librarian" ? "Librarian" : "Researcher"} mode`, roleText(mode, cfg)];
|
|
57
57
|
const shared = readIf(cfg.sharedRules);
|
|
58
58
|
if (shared) {
|
|
59
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
|
@@ -3,7 +3,7 @@
|
|
|
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 mode for these). */
|
|
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",
|
|
@@ -40,26 +40,38 @@ export const PREVIEW_WRITES = new Set([
|
|
|
40
40
|
]);
|
|
41
41
|
/** Writes without a dry run that still need a yes. */
|
|
42
42
|
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
|
+
export const ZOTERO_CHECKS = ["tag_audit", "audit_metadata", "find_duplicates", "check_retractions", "missing_pdfs", "write_tag_review"].map((n) => `zotero_${n}`);
|
|
45
|
+
/**
|
|
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
|
+
*/
|
|
50
|
+
export function toolsFor(mode, registered, opts = {}) {
|
|
44
51
|
const has = new Set(registered);
|
|
45
|
-
const without = new Set(profileSpec(profile).without);
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
const without = new Set(profileSpec(opts.profile ?? "editor").without);
|
|
53
|
+
const zotero = registered.filter((n) => n.startsWith("zotero_") && (opts.bench || !n.startsWith("zotero_bakeoff_")));
|
|
54
|
+
const wanted = [
|
|
55
|
+
...ZOTERO_READ,
|
|
56
|
+
...ZOTERO_CHECKS,
|
|
57
|
+
...zotero,
|
|
58
|
+
...(mode === "researcher" ? registered.filter((n) => n.startsWith("scholar_") || n.startsWith("verify_")) : []),
|
|
59
|
+
...FILE_TOOLS,
|
|
60
|
+
];
|
|
53
61
|
return [...new Set(wanted)].filter((n) => has.has(n) && !without.has(n));
|
|
54
62
|
}
|
|
55
63
|
/** Why a Sub-Sub tool is not available now, or undefined when it is. */
|
|
56
|
-
export function unavailableReason(name, mode, registered,
|
|
57
|
-
if (toolsFor(mode, registered,
|
|
64
|
+
export function unavailableReason(name, mode, registered, opts = {}) {
|
|
65
|
+
if (toolsFor(mode, registered, opts).includes(name))
|
|
58
66
|
return undefined;
|
|
67
|
+
const profile = opts.profile ?? "editor";
|
|
59
68
|
if (profileSpec(profile).without.includes(name)) {
|
|
60
69
|
return `${name} is not part of the ${profileSpec(profile).label} profile. The user can change the profile with /profile.`;
|
|
61
70
|
}
|
|
62
|
-
|
|
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
|
+
}
|
|
74
|
+
return `${name} is not available.`;
|
|
63
75
|
}
|
|
64
76
|
/** A dry run only when dry_run is absent (server default true) or clearly true. */
|
|
65
77
|
export function isDryRun(value) {
|
|
@@ -74,6 +86,3 @@ export function gateKind(toolName, input) {
|
|
|
74
86
|
return "path";
|
|
75
87
|
return null;
|
|
76
88
|
}
|
|
77
|
-
export function otherMode(mode) {
|
|
78
|
-
return mode === "librarian" ? "researcher" : "librarian";
|
|
79
|
-
}
|
package/dist/subsub.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sub-Sub: a Zotero librarian and research assistant for pi.
|
|
3
3
|
*
|
|
4
|
-
* - Two modes
|
|
5
|
-
*
|
|
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.
|
|
6
7
|
* - An approval gate: every library change is previewed by the server (dry run)
|
|
7
8
|
* and shown to the user; nothing is applied without a yes.
|
|
8
9
|
* - Profiles (reader, scholar, author, editor) set what Sub-Sub writes and which
|
|
9
10
|
* tools it offers; /profile changes it.
|
|
10
11
|
* - Commands: /librarian, /researcher, /profile, /subsub, /history, /undo.
|
|
11
12
|
* - Add-on: with "addons": ["starbuck"], Starbuck runs as a third server
|
|
12
|
-
* ("verify") for reference checks
|
|
13
|
+
* ("verify") for reference checks.
|
|
13
14
|
*
|
|
14
15
|
* The Python servers start in session_start (not in the factory) and stop in
|
|
15
16
|
* session_shutdown. Tools are registered once and use the current bridge.
|
|
@@ -27,8 +28,8 @@ import { describeArgs, formatPreview } from "./preview.js";
|
|
|
27
28
|
import { headerLines, paintPreview, QUOTES, schemeFromAnsi, themeName } from "./look.js";
|
|
28
29
|
import { systemAddition } from "./prompt.js";
|
|
29
30
|
import { gateKind, SERVER_PREFIXES, toolsFor, unavailableReason } from "./roles.js";
|
|
30
|
-
const PACKAGE_DIR = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
31
31
|
const MODE_ENTRY = "subsub-mode";
|
|
32
|
+
const PACKAGE_DIR = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
32
33
|
const BENCH_ENTRY = "subsub-bench";
|
|
33
34
|
function ownVersion() {
|
|
34
35
|
try {
|
|
@@ -102,8 +103,10 @@ export async function createSubsub(pi, deps = {}) {
|
|
|
102
103
|
}
|
|
103
104
|
if (deps.bridge)
|
|
104
105
|
registerTools(deps.bridge);
|
|
105
|
-
pi.registerFlag("librarian", { description: "Start Sub-Sub in
|
|
106
|
+
pi.registerFlag("librarian", { description: "Start Sub-Sub in the Librarian mode", type: "boolean", default: false });
|
|
106
107
|
let mode = cfg.defaultMode;
|
|
108
|
+
const benchRun = Boolean(process.env.SUBSUB_BENCH_OUT);
|
|
109
|
+
const toolOpts = () => ({ profile, bench: benchRun });
|
|
107
110
|
let profile = cfg.profile;
|
|
108
111
|
const user = who(cfg);
|
|
109
112
|
/** True when started with the `subsub` command (not as a package inside plain pi). */
|
|
@@ -147,7 +150,7 @@ export async function createSubsub(pi, deps = {}) {
|
|
|
147
150
|
if (!lookOn(ctx) || cfg.themes === false)
|
|
148
151
|
return;
|
|
149
152
|
scheme ??= schemeFromAnsi(ctx.ui.theme.getFgAnsi("text"), ctx.ui.theme.name);
|
|
150
|
-
const name = themeName(
|
|
153
|
+
const name = themeName(scheme, cfg.themes || undefined);
|
|
151
154
|
const theme = name ? ctx.ui.getTheme(name) : undefined;
|
|
152
155
|
// A Theme object (not a name) changes the theme for this run only; the saved setting stays.
|
|
153
156
|
if (theme)
|
|
@@ -219,7 +222,7 @@ export async function createSubsub(pi, deps = {}) {
|
|
|
219
222
|
}
|
|
220
223
|
async function applyMode(ctx, next, remember) {
|
|
221
224
|
mode = next;
|
|
222
|
-
pi.setActiveTools(toolsFor(mode, registered(),
|
|
225
|
+
pi.setActiveTools(toolsFor(mode, registered(), toolOpts()));
|
|
223
226
|
const spec = cfg.models[mode];
|
|
224
227
|
if (spec) {
|
|
225
228
|
const i = spec.indexOf("/");
|
|
@@ -230,7 +233,6 @@ export async function createSubsub(pi, deps = {}) {
|
|
|
230
233
|
}
|
|
231
234
|
if (remember)
|
|
232
235
|
pi.appendEntry(MODE_ENTRY, { mode });
|
|
233
|
-
applyTheme(ctx);
|
|
234
236
|
status(ctx);
|
|
235
237
|
}
|
|
236
238
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -249,6 +251,7 @@ export async function createSubsub(pi, deps = {}) {
|
|
|
249
251
|
// Judge light or dark before Sub-Sub changes the theme.
|
|
250
252
|
if (lookOn(ctx))
|
|
251
253
|
scheme ??= schemeFromAnsi(ctx.ui.theme.getFgAnsi("text"), ctx.ui.theme.name);
|
|
254
|
+
applyTheme(ctx);
|
|
252
255
|
await applyMode(ctx, restored ?? (pi.getFlag("librarian") ? "librarian" : cfg.defaultMode), false);
|
|
253
256
|
if (lookOn(ctx)) {
|
|
254
257
|
setHeader(ctx);
|
|
@@ -293,7 +296,7 @@ export async function createSubsub(pi, deps = {}) {
|
|
|
293
296
|
coerceArgs(schema, input);
|
|
294
297
|
resolvePathArgs(name, input, (p) => normalizeToolPath(p, ctx.cwd));
|
|
295
298
|
if (SERVER_PREFIXES.some((p) => name.startsWith(p))) {
|
|
296
|
-
const why = unavailableReason(name, mode, registered(),
|
|
299
|
+
const why = unavailableReason(name, mode, registered(), toolOpts());
|
|
297
300
|
if (why) {
|
|
298
301
|
if (benchOut)
|
|
299
302
|
record({ kind: "wrong_mode", tool: name, input });
|
|
@@ -417,17 +420,17 @@ export async function createSubsub(pi, deps = {}) {
|
|
|
417
420
|
};
|
|
418
421
|
}
|
|
419
422
|
pi.registerCommand("librarian", {
|
|
420
|
-
description: "Sub-Sub: switch to the
|
|
423
|
+
description: "Sub-Sub: switch to the Librarian (manages the library: tags, imports, metadata, PDFs)",
|
|
421
424
|
handler: async (_args, ctx) => {
|
|
422
425
|
await applyMode(ctx, "librarian", true);
|
|
423
|
-
ctx.ui.notify("Sub-Sub:
|
|
426
|
+
ctx.ui.notify("Sub-Sub: Librarian mode. For searches and notes, switch to /researcher.", "info");
|
|
424
427
|
},
|
|
425
428
|
});
|
|
426
429
|
pi.registerCommand("researcher", {
|
|
427
|
-
description: "Sub-Sub: switch to the
|
|
430
|
+
description: "Sub-Sub: switch to the Researcher (searches, notes, reviews, and everything the Librarian does)",
|
|
428
431
|
handler: async (_args, ctx) => {
|
|
429
432
|
await applyMode(ctx, "researcher", true);
|
|
430
|
-
ctx.ui.notify("Sub-Sub:
|
|
433
|
+
ctx.ui.notify("Sub-Sub: Researcher mode. It searches, writes notes and also changes the library, with a preview.", "info");
|
|
431
434
|
},
|
|
432
435
|
});
|
|
433
436
|
pi.registerCommand("profile", {
|
package/dist/web.js
CHANGED
|
@@ -549,13 +549,12 @@ export async function startWeb(opts, env = process.env, log = () => { }) {
|
|
|
549
549
|
return send(res, 409, { error: "busy" });
|
|
550
550
|
const body = await readJson(req);
|
|
551
551
|
const spec = `${String(body.provider)}/${String(body.id)}`;
|
|
552
|
-
//
|
|
553
|
-
// extension reads the new setting.
|
|
552
|
+
// Remember the model for the current mode (or both), then restart pi so the extension reads it.
|
|
554
553
|
const mode = subsubState?.mode === "librarian" ? "librarian" : "researcher";
|
|
555
554
|
const models = { ...(loadConfig(env).models ?? {}), [mode]: spec };
|
|
556
555
|
if (body.both)
|
|
557
556
|
models[mode === "librarian" ? "researcher" : "librarian"] = spec;
|
|
558
|
-
saveConfig({ models }, env);
|
|
557
|
+
saveConfig({ models, model: undefined }, env);
|
|
559
558
|
await agent.request({ type: "set_model", provider: String(body.provider), modelId: String(body.id) });
|
|
560
559
|
await restartAgent();
|
|
561
560
|
return send(res, 200, { ok: true });
|
|
@@ -583,8 +582,8 @@ export async function startWeb(opts, env = process.env, log = () => { }) {
|
|
|
583
582
|
const body = await readJson(req);
|
|
584
583
|
saveApiKey(agentDir, String(body.provider ?? ""), String(body.key ?? ""));
|
|
585
584
|
const current = loadConfig(env);
|
|
586
|
-
if (body.provider === "opencode-go" && Object.keys(current.models
|
|
587
|
-
saveConfig({ models: TESTED_MODELS }, env);
|
|
585
|
+
if (body.provider === "opencode-go" && Object.keys(current.models).length === 0)
|
|
586
|
+
saveConfig({ models: TESTED_MODELS, model: undefined }, env);
|
|
588
587
|
await restartAgent();
|
|
589
588
|
return send(res, 200, { ok: true });
|
|
590
589
|
}
|