@tiagojct/subsub 0.15.0 → 0.17.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/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  Sub-Sub follows the version numbers of npm: a change in the second number (0.13) adds features or changes how you use it; a change in the third (0.13.1) only fixes.
4
4
 
5
+ ## 0.17.0 (2026-10-14)
6
+
7
+ For people who are learning to read the literature:
8
+
9
+ - `/read <citekey>` is a guided reading of one paper. Sub-Sub asks why you read it: background (the title and abstract, then keep or drop), one fact, extraction for a review, or critique. It reads only as deep as that reason needs, quotes the passages with their place and asks questions; you write the answers. It writes no results or summaries in this note. For an extraction, it also adds a row for the paper to your extraction sheet in `Research/`, with where to find each value. Put the reason first to skip the question: `/read extract smith2021`. In the browser it is the button Read a paper.
10
+ - Sub-Sub has no profiles any more (Reader, Scholar, Author, Editor). The two modes stay: the Researcher has every command and tool, and every library change still needs your yes. Sub-Sub writes notes and syntheses with a source for every claim, drafts text for a manuscript or thesis only when you ask, and does not draft text for assignments. What the Reader profile did is now `/read` (a guided reading of one paper) and `/lit reading list: <topic>` (a reading list with questions, no results). `subsub init` no longer asks whether you are a student or which profile you want, and removes the old setting. `/profile` says where things went.
11
+ - The browser view is more useful:
12
+ - A library search in the left panel. Each result has Read, Note and a link to the item in Zotero.
13
+ - A notes pane: Notes at the top lists the notes in your Sub-Sub folder, and a note opens next to the conversation. Note paths in replies, `[[links]]` and the Open button on a written file open there too; Open in app still hands it to Obsidian or your editor.
14
+ - After a command and a space, the message box shows what the command needs.
15
+ - Conversations can be renamed and deleted. Delete moves the file to `~/.subsub/trash`. Export is in the language of the interface and includes the tools that Sub-Sub used and their results.
16
+ - The reasons of `/read` show as a list. The undo question has its own title again, and the page no longer scrolls as a whole when the left panel is long.
17
+ - In the browser, a command that asks a question (`/read`, `/undo`) no longer shows an error when the answer takes more than 30 seconds.
18
+
19
+ ## 0.16.0 (2026-10-08)
20
+
21
+ For clinical researchers:
22
+
23
+ - `/ai-statement [what you used it for]` writes a draft of the statement on the use of AI that journals ask for, with the Sub-Sub version, its DOI, your models and Sub-Sub's checks, in `Research/`. The parts in brackets are for you to complete. It is also a button in the browser view.
24
+ - A `/lit` note now says what it is: an AI-assisted rapid review, with one AI screener, no second reviewer and no formal risk-of-bias assessment, for scoping, a background section or a first search, and not a systematic review.
25
+ - The site has a page for clinical researchers, in English and Portuguese: what Sub-Sub does for them, what it is not, how to get it on a hospital computer, moving from EndNote or Mendeley, data protection, and journals and AI. The Install page has a section for your IT service: what is installed, what runs and which addresses it contacts.
26
+
5
27
  ## 0.15.0 (2026-10-08)
6
28
 
7
29
  Checks:
package/README.md CHANGED
@@ -29,18 +29,13 @@ Every library change works the same way in both modes:
29
29
  - Every change is journaled, including new collections. `/undo` reverts the last one, and the browser view shows an Undo button under each change.
30
30
  - Nothing is deleted: items go to the Zotero trash. Only tags from your tag list can be added.
31
31
 
32
- ## Profiles
32
+ ## What Sub-Sub writes
33
33
 
34
- The profile sets how much Sub-Sub writes for you and which tools it offers. Choose it in `subsub init`, or change it with `/profile`. Students start with Reader.
34
+ Sub-Sub writes literature notes, syntheses and reviews from what it read, with a source for every claim. It drafts text for your manuscript or thesis only when you ask, and marks the drafted text so that you can check it. It does not draft text for assignments, even when you ask.
35
35
 
36
- | Profile | What Sub-Sub does |
37
- |---|---|
38
- | Reader | Reading notes with quotations, page numbers and questions for you. No summaries or syntheses unless you ask. No bulk library changes. |
39
- | Scholar | Literature notes, syntheses, searches, alerts and all library changes. |
40
- | Author | Scholar, plus manuscript work: citation checks against your library, bibliographies for Pandoc and Quarto, and comments on the argument. It does not write your paragraphs. |
41
- | Editor | Everything, including drafts of manuscript sections and abstracts when you ask. |
36
+ To learn to read papers yourself, use `/read`: Sub-Sub finds the passages and asks questions, and you write the answers. `/lit reading list: <topic>` gives a reading list with questions to read with, and no results.
42
37
 
43
- The [Profiles page](https://subsub.tiagojacinto.eu/profiles/) lists the tools and commands for each profile and mode.
38
+ The [Commands page](https://subsub.tiagojacinto.eu/profiles/) lists the tools and commands of each mode.
44
39
 
45
40
  ## Requirements
46
41
 
@@ -91,17 +86,20 @@ subsub --help # all commands
91
86
 
92
87
  In the browser view, connect a model provider with the model button. In the terminal, type `/login`.
93
88
 
89
+ The browser view also has a library search (each result can start `/read` or `/lit-note`), a notes pane that shows the notes of your Sub-Sub folder next to the conversation (note paths and `[[links]]` in replies open there), argument hints for the commands, and rename, delete (to `~/.subsub/trash`) and export for conversations.
90
+
94
91
  Commands in Sub-Sub:
95
92
 
96
93
  | Command | What it does |
97
94
  |---|---|
98
95
  | `/researcher`, `/librarian` | Switch the mode |
99
- | `/profile [name]` | Show or change the profile |
100
96
  | `/subsub` | Zotero connection, mode, model and library counts |
101
97
  | `/history`, `/undo [id]` | Recent library changes; undo the last one or one by its id |
98
+ | `/ai-statement [uses]` | A draft statement on the use of AI for a manuscript, with the Sub-Sub version, its DOI and your models |
102
99
  | `/tag-batch [size]` | Tag items that have no topic, with your tag list |
103
100
  | `/clean-tags` | Find tags that are not in your tag list and propose fixes |
104
101
  | `/import-queue [path]` | Import the references in the import queue note |
102
+ | `/read [reason] <citekey>` | Guided reading of one paper. Sub-Sub asks why you read it (background, one fact, extraction for a review, critique), finds the passages and asks questions; you write the answers. For extraction it adds a row to your extraction sheet in `Research/` |
105
103
  | `/lit <question>` | Literature review: the library first, then one merged search; an evidence table, agreement, disagreement and open questions; one note in `Research/` |
106
104
  | `/compare <question or citekeys>` | A matrix of sources: claim, evidence, caveats, confidence |
107
105
  | `/lit-note <citekey>` | A literature note on one item, from its full text |
@@ -114,7 +112,7 @@ Commands in Sub-Sub:
114
112
  | `/digest [days] [topic]` | What changed in the last days: new notes, alerts, works worth reading |
115
113
  | `/request-copy <citekey, DOI or PMID>` | A draft email to the corresponding author for a paper that is not open access. You send it yourself |
116
114
 
117
- The library commands work in both modes; the research commands need the Researcher. Some commands depend on the profile (see the Profiles page).
115
+ The library commands work in both modes; the research commands need the Researcher.
118
116
 
119
117
  To check or apply a tag review note from `Inbox/` outside Sub-Sub:
120
118
 
@@ -129,7 +127,6 @@ subsub review apply 13-31
129
127
 
130
128
  | Setting | Meaning | Default |
131
129
  |---|---|---|
132
- | `profile` | `reader`, `scholar`, `author` or `editor` | `reader` for students, `scholar` for others (`subsub init` asks) |
133
130
  | `userName`, `about` | Your name and one line about you, for the replies | none |
134
131
  | `language` | Language of replies and notes, or `auto` for the language you write in | `English` |
135
132
  | `vault` | The Sub-Sub folder | from the server settings |
@@ -177,6 +174,12 @@ End-to-end tests (real pi and Python servers, a fake Zotero, a scripted model):
177
174
 
178
175
  See [CHANGELOG.md](CHANGELOG.md).
179
176
 
177
+ ## Cite
178
+
179
+ Jacinto T. Sub-Sub: a research assistant for Zotero [software]. Porto: Faculdade de Medicina da Universidade do Porto; 2026. doi:[10.5281/zenodo.23238602](https://doi.org/10.5281/zenodo.23238602)
180
+
181
+ This DOI always points to the latest version; each version also has its own DOI on [Zenodo](https://doi.org/10.5281/zenodo.23238602). `CITATION.cff` has the same reference for citation managers.
182
+
180
183
  ## Licence
181
184
 
182
185
  [MIT](LICENSE), Tiago Jacinto. The `/lit`, `/compare` and `/review` prompts adapt text from [Feynman](https://github.com/Companion-Inc/feynman) (MIT); see [THIRD_PARTY_NOTICES](THIRD_PARTY_NOTICES). IBM Plex fonts: SIL Open Font License.
package/dist/bench.js CHANGED
@@ -844,7 +844,7 @@ export async function main(argv = process.argv.slice(2)) {
844
844
  const cf = join(cfgDir, `${j.code}.json`);
845
845
  // A bare name is an OpenCode Go model; provider/id (google/gemini-..., openrouter/x/y:free) is any provider.
846
846
  const spec = j.model.includes("/") ? j.model : `opencode-go/${j.model}`;
847
- writeFileSync(cf, JSON.stringify({ ...userCfg, profile: "editor", models: { librarian: spec, researcher: spec }, model: undefined, libraryChanges: undefined }));
847
+ writeFileSync(cf, JSON.stringify({ ...userCfg, profile: undefined, models: { librarian: spec, researcher: spec }, model: undefined, libraryChanges: undefined }));
848
848
  const s = await runOne({ ...j, prompt, out, cwd: cfg.vault, configFile: cf, timeoutSec });
849
849
  n++;
850
850
  console.log(`${n}/${jobs.length} ${j.code} ${j.task} ${s.seconds}s ${s.error ?? (s.settled ? "done" : "?")}`);
package/dist/cli.js CHANGED
@@ -149,7 +149,7 @@ Usage:
149
149
  subsub Start Sub-Sub in the terminal
150
150
  subsub --librarian Start in the Librarian, which only manages the library
151
151
  subsub web Open Sub-Sub in the browser
152
- subsub init Set up or change the settings (folder, tag list, profile, models)
152
+ subsub init Set up or change the settings (folder, tag list, models)
153
153
  subsub doctor Check the set-up; each line marked FIX says what to do
154
154
  subsub usage The pilot's usage log (if turned on): a summary and where the file is
155
155
  subsub shortcut Add a Sub-Sub shortcut (--remove takes it away)
@@ -161,7 +161,7 @@ Options:
161
161
  --here Work in the current folder, not in the Sub-Sub folder
162
162
 
163
163
  In Sub-Sub, type /subsub for the state, /researcher or /librarian to switch modes,
164
- /profile to change the profile, /undo to undo the last change, and / for all commands.
164
+ /read to read a paper with guidance, /undo to undo the last change, and / for all commands.
165
165
  Sub-Sub runs on pi: pi's options (for example --model, --resume) also work.
166
166
 
167
167
  Docs: https://subsub.tiagojacinto.eu`;
package/dist/config.js CHANGED
@@ -3,7 +3,6 @@
3
3
  * (~/.config/subsub/config.json, or SUBSUB_CONFIG) overrides it.
4
4
  *
5
5
  * {
6
- * "profile": "scholar",
7
6
  * "userName": "Ana", "about": "a master's student in health informatics", "language": "English",
8
7
  * "vault": "~/Documents/Sub-Sub",
9
8
  * "models": { "librarian": "opencode-go/mimo-v2.6-flash", "researcher": "opencode-go/mimo-v2.6-pro" },
@@ -57,7 +56,6 @@ export const RECOMMENDED_MODELS = [
57
56
  export function uiLanguage(language) {
58
57
  return /portug|^pt\b|^pt-/i.test(language ?? "") ? "pt" : "en";
59
58
  }
60
- export const PROFILES = ["reader", "scholar", "author", "editor"];
61
59
  /** The Zotero server release that this Sub-Sub version runs when there is no local server folder. */
62
60
  export const SERVER_VERSION = "0.5.5";
63
61
  /** The Starbuck release (PyPI) that this Sub-Sub version runs when there is no local Starbuck folder. */
@@ -109,9 +107,6 @@ export function saveConfig(patch, env = process.env) {
109
107
  writeFileSync(file, `${JSON.stringify(merged, null, 2)}\n`);
110
108
  return file;
111
109
  }
112
- export function isProfile(x) {
113
- return typeof x === "string" && PROFILES.includes(x);
114
- }
115
110
  export function loadConfig(env = process.env) {
116
111
  const home = env.HOME;
117
112
  const file = configPath(env);
@@ -148,7 +143,6 @@ export function loadConfig(env = process.env) {
148
143
  const themes = user.themes === false ? false : typeof user.themes === "string" ? user.themes : user.themes?.researcher;
149
144
  return {
150
145
  configFile: file,
151
- profile: isProfile(user.profile) ? user.profile : "scholar",
152
146
  userName: typeof user.userName === "string" && user.userName.trim() ? user.userName.trim() : undefined,
153
147
  about: typeof user.about === "string" && user.about.trim() ? user.about.trim() : undefined,
154
148
  language: typeof user.language === "string" && user.language.trim() ? user.language.trim() : "English",
package/dist/doctor.js CHANGED
@@ -12,7 +12,6 @@ import { homedir } from "node:os";
12
12
  import { join } from "node:path";
13
13
  import { findUv, loadConfig, SERVER_VERSION, serverCommand, STARBUCK_SOURCE, starbuckCommand, starbuckEnabled, refreshedArgs, } from "./config.js";
14
14
  import { isObsidianVault, LEGACY_SETTINGS_DIR, SETTINGS_FILES, settingsFile } from "./layout.js";
15
- import { profileSpec } from "./profiles.js";
16
15
  export const defaultRunner = (command, args, env) => {
17
16
  const r = spawnSync(command, args, { env, encoding: "utf8", timeout: 120_000 });
18
17
  return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "", error: r.error?.message };
@@ -39,7 +38,7 @@ export function runChecks(cfg, env, deps = {}) {
39
38
  ? { name: "Node", ok: true, detail: node }
40
39
  : { name: "Node", ok: false, detail: node, fix: "Install Node.js 22.19 or later (https://nodejs.org), or run the Sub-Sub installer again." });
41
40
  out.push(existsSync(cfg.configFile)
42
- ? { name: "Settings", ok: true, detail: `${cfg.configFile}, profile ${profileSpec(cfg.profile).label}, replies in ${cfg.language}` }
41
+ ? { name: "Settings", ok: true, detail: `${cfg.configFile}, replies in ${cfg.language}` }
43
42
  : { name: "Settings", ok: false, detail: `no ${cfg.configFile}`, fix: "Type subsub init." });
44
43
  const uv = findUv(cfg, env);
45
44
  const uvRun = run(uv, ["--version"], env);
package/dist/init.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * existing one. Press Enter to keep the value in brackets.
4
4
  *
5
5
  * Writes:
6
- * - the settings file (~/.config/subsub/config.json): profile, name, language,
6
+ * - the settings file (~/.config/subsub/config.json): 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.
@@ -18,7 +18,7 @@
18
18
  *
19
19
  * Flags (for scripts and tests): --yes (take every default), --setup
20
20
  * standard|fmup, --name, --about, --language, --folder (or --notes), --move
21
- * yes|no, --profile, --tags health-sciences|health-informatics|any-field,
21
+ * yes|no, --tags health-sciences|health-informatics|any-field,
22
22
  * --email, --starbuck on|off, --models keep|later|opencode-go|mistral-eu|google-free.
23
23
  */
24
24
  import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
@@ -26,11 +26,10 @@ 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, DEFAULT_MODELS, EU_MODELS, FREE_MODELS, expand, isProfile, loadConfig, parseEnvFile, PROFILES, readConfigFile, saveConfig, starbuckEnabled, withStarbuck, } from "./config.js";
29
+ import { configPath, DEFAULT_MODELS, EU_MODELS, FREE_MODELS, expand, loadConfig, parseEnvFile, 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 { KEY_PROVIDERS, saveApiKey } from "./keys.js";
33
- import { PROFILE_SPECS } from "./profiles.js";
34
33
  const TEMPLATES = resolve(dirname(fileURLToPath(import.meta.url)), "..", "templates");
35
34
  export const STARTER_TAGS = {
36
35
  "health-sciences": "Health sciences: clinical areas, public health, methods and study designs",
@@ -125,25 +124,13 @@ export async function runInit(io, flags, env = process.env, deps = {}) {
125
124
  io.say(`Sub-Sub keeps using ${notes}.`);
126
125
  }
127
126
  }
128
- // Asked here, not first: it only changes the defaults of the next two questions.
129
- explain("Faculty of Medicine, Porto", "Sub-Sub has a set-up for the Faculty of Medicine of the University of Porto (FMUP): health-sciences tags and the Reader profile. Everyone else keeps the standard set-up.");
127
+ // Asked here, not first: it only changes the default of the tag list question.
128
+ explain("Faculty of Medicine, Porto", "Sub-Sub has a set-up for the Faculty of Medicine of the University of Porto (FMUP): health-sciences tags. Everyone else keeps the standard set-up.");
130
129
  const setup = await io.choose("Set-up", [
131
130
  { value: "standard", label: "Standard (any field, any institution)" },
132
- { value: "fmup", label: "FMUP / U.Porto: health-sciences tags, Reader profile (the FMUP model service is coming soon)" },
131
+ { value: "fmup", label: "FMUP / U.Porto: health-sciences tags (the FMUP model service is coming soon)" },
133
132
  ], flags.setup ?? existing.setup ?? "standard");
134
133
  const fmup = setup === "fmup";
135
- // Students start in Reader: Sub-Sub helps them read and does not write syntheses for them unless asked.
136
- let student = fmup;
137
- if (!flags.profile && !existing.profile && !fmup) {
138
- explain("Who you are", "Students start with the Reader profile: Sub-Sub helps you read (notes with quotations, page numbers and questions) and does not write summaries or syntheses unless you ask. You can change the profile at any time.");
139
- student = (await io.choose("You are", [
140
- { value: "student", label: "A student" },
141
- { value: "researcher", label: "A researcher, teacher or clinician" },
142
- ], flags.student === "no" ? "researcher" : "student")) === "student";
143
- }
144
- explain("Profile", "The profile sets how much Sub-Sub writes for you and which tools it offers. Change it at any time with /profile.");
145
- const profileDefault = flags.profile ?? existing.profile ?? (student ? "reader" : "scholar");
146
- const profile = await io.choose("Profile", PROFILES.map((p) => ({ value: p, label: `${PROFILE_SPECS[p].label}: ${PROFILE_SPECS[p].summary}` })), isProfile(profileDefault) ? profileDefault : "scholar");
147
134
  // The tag list: a custom path in the server settings stays; otherwise Zotero/Zotero tags.md (moved, kept or new).
148
135
  const vocabTarget = join(notes, SETTINGS_DIR, SETTINGS_FILES.tags);
149
136
  const oldDefaults = new Set([join(from, "Systems", SETTINGS_FILES.tags), join(from, SETTINGS_DIR, SETTINGS_FILES.tags)]);
@@ -225,7 +212,7 @@ export async function runInit(io, flags, env = process.env, deps = {}) {
225
212
  if (brave || hadBrave)
226
213
  chmodSync(envFile, 0o600); // the file holds a key
227
214
  const patch = {
228
- profile,
215
+ profile: undefined,
229
216
  userName: userName || undefined,
230
217
  about: about || undefined,
231
218
  language,
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 = (opts.mode === "librarian" ? "Librarian" : "Researcher") + (opts.profile ? `, ${opts.profile}` : "");
66
+ const modeLabel = opts.mode === "librarian" ? "Librarian" : "Researcher";
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("/researcher /librarian /profile /history /undo | / for all commands", w)), "");
81
+ out.push(p.dim(cut("/researcher /librarian /read /history /undo | / for all commands", w)), "");
82
82
  return out;
83
83
  }
84
84
  /**
package/dist/prompt.js CHANGED
@@ -1,16 +1,15 @@
1
1
  /**
2
2
  * The Sub-Sub part of the system prompt: role text for the current mode, the
3
- * profile rules, the shared Zotero rules from the Sub-Sub folder, the vault
3
+ * shared Zotero rules from the Sub-Sub folder, the vault
4
4
  * conventions when pi runs inside the Sub-Sub folder, and the language rule.
5
5
  *
6
- * Role and profile texts use placeholders: {{user}} / {{User}} (the user's name,
6
+ * Role texts use placeholders: {{user}} / {{User}} (the user's name,
7
7
  * or "the user"), {{about}} (one line about the user) and {{batch}}.
8
8
  */
9
9
  import { existsSync, readFileSync } from "node:fs";
10
10
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
12
  import { who } from "./config.js";
13
- import { profileSpec } from "./profiles.js";
14
13
  const ROLE_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "roles");
15
14
  function readIf(path) {
16
15
  if (!path || !existsSync(path))
@@ -44,7 +43,7 @@ export function fill(text, cfg) {
44
43
  .replaceAll("{{User}}", capital(user))
45
44
  .replaceAll("{{user}}", user)
46
45
  .replaceAll("{{about}}", about)
47
- .replaceAll("{{batch}}", String(profileSpec(cfg.profile).batch))
46
+ .replaceAll("{{batch}}", "25")
48
47
  .replace(/[ \t]+\n/g, "\n");
49
48
  }
50
49
  export function roleText(mode, cfg) {
@@ -71,8 +70,7 @@ export function systemAddition(mode, cfg, cwd) {
71
70
  if (cfg.setup === "fmup") {
72
71
  parts.push("# U.Porto", `${capital(user)} works at the University of Porto. Papers that are not open access often open through the university's subscriptions when ${user} uses Sub-Sub on the U.Porto network: say so when a work is not open access.`);
73
72
  }
74
- // The profile and the language rule come last, so they win over the shared rules and the vault conventions.
75
- parts.push("# Profile", fill(profileSpec(cfg.profile).rules, cfg));
73
+ // The language rule comes last, so it wins over the shared rules and the vault conventions.
76
74
  parts.push("# Language", languageRule(cfg.language, user));
77
75
  return parts.join("\n\n");
78
76
  }
package/dist/read.js ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * /read: guided reading of one paper, for people who are learning to read the literature.
3
+ * Sub-Sub asks why the user reads the paper and reads only as deep as that reason needs. It finds
4
+ * the passages (quotes with their place) and asks questions; the user writes the answers. It states
5
+ * no results: /lit-note is the note that Sub-Sub writes for you.
6
+ */
7
+ /** The reasons to read, with the labels of the choice (English, Portuguese). */
8
+ export const PURPOSES = {
9
+ background: ["Background: is it worth reading? (title and abstract, keep or drop)", "Contexto: vale a pena ler? (título e resumo, manter ou pôr de parte)"],
10
+ fact: ["One fact: find a specific detail", "Um dado: encontrar um pormenor concreto"],
11
+ extract: ["Extraction: one row for my review (question, design, population, setting, finding)", "Extração: uma linha para a minha revisão (pergunta, desenho, população, contexto, resultado)"],
12
+ critique: ["Critique: read it closely (peer review, the paper closest to mine)", "Crítica: ler com atenção (revisão por pares, o artigo mais próximo do meu)"],
13
+ };
14
+ /** "/read extract smith2026" -> purpose and reference; the first word is a purpose only when it is one exactly. */
15
+ export function readArgs(args) {
16
+ const words = args.trim().split(/\s+/).filter(Boolean);
17
+ const first = words[0]?.toLowerCase();
18
+ if (first && Object.hasOwn(PURPOSES, first))
19
+ return { purpose: first, ref: words.slice(1).join(" ") };
20
+ return { ref: words.join(" ") };
21
+ }
22
+ const GUIDELINES = "CONSORT for randomised trials, STROBE for observational studies, PRISMA for systematic reviews, STARD for diagnostic accuracy, TRIPOD or TRIPOD+AI for prediction models, CHEERS for economic evaluations, SRQR or COREQ for qualitative research";
23
+ function depth(r) {
24
+ switch (r.purpose) {
25
+ case "background":
26
+ return [
27
+ "Depth: the title and the abstract only. Do not read the full text.",
28
+ 'Under "## The paper at a glance":',
29
+ "- What it asks: the sentence of the title or abstract that states the question or aim, quoted.",
30
+ "- How: the sentence that names the design and the population, quoted.",
31
+ 'Under "## Keep or drop": the question "Does this paper fit your reason? Look at the population, the design and the outcome.", then the lines "- [ ] Keep", "- [ ] Drop" and "Reason:", empty for the user.',
32
+ ];
33
+ case "fact":
34
+ return [
35
+ `The user looks for: ${r.fact}`,
36
+ "Depth: only the passages that hold this fact. Get the section list first and read only the sections you need.",
37
+ 'Under "## Where the paper says it": each passage, quoted, with its place. Under each passage, one question that makes the user read it closely (for example: what exactly does it say, for which population, at what time?) and an empty line "Your answer:".',
38
+ "If the paper does not say it, write that you did not find it and where you looked.",
39
+ ];
40
+ case "extract": {
41
+ const sheet = r.sheet
42
+ ? `the sheet ${r.sheet}: read it first for its columns, then add the row under its table with edit.`
43
+ : `a new sheet Research/<slug> extraction.md (slug: the review question in lower case with hyphens, at most five words; do not overwrite an existing file). Front matter: title, question: "${r.question}", type: extraction-sheet, created: ${r.date}. Then a table with the columns | Study | Question | Design | Population | Setting | Outcome | Main finding |.`;
44
+ return [
45
+ "Depth: the methods and the results (the full text when there is one).",
46
+ 'Under "## Extraction": for each field below, the passage that answers it, quoted with its place, then an empty line "Your answer:".',
47
+ "- The question or aim",
48
+ "- The design",
49
+ "- The population (who, how many)",
50
+ "- The setting (where, when)",
51
+ "- The outcome and how it was measured",
52
+ "- The main finding: quote the sentence of the results that answers the question. Do not restate it.",
53
+ `Then add one row for this paper to ${sheet}`,
54
+ "The row: [[citekey]] in the Study column, and in every other column only where the user finds it (the page or the section). Leave the content of the cells for the user.",
55
+ ];
56
+ }
57
+ case "critique":
58
+ return [
59
+ "Depth: the whole paper, section by section. A critique needs the full text: if you can read only the abstract, say so, write what the abstract allows, and offer /request-copy to ask the authors for the paper.",
60
+ 'Under "## Methods", "## Results" and "## Discussion": the passages that matter to judge the study, quoted with their place, each with one question for the user (for example: how were the participants selected? what was compared? what else could explain the result?).',
61
+ `Under "## Appraisal": name the design and the reporting guideline that applies (${GUIDELINES}), and tell the user to check the paper against it item by item. Do not copy the checklist. Then five to eight questions on the risk of bias for this design (selection, confounding, measurement, missing data, selective reporting), each with the passage to look at.`,
62
+ ];
63
+ }
64
+ }
65
+ /**
66
+ * The instruction for the model. The first line is the command itself: the chat shows a long
67
+ * message folded under its first line, so the user sees what they asked for.
68
+ */
69
+ export function readInstruction(r) {
70
+ return [
71
+ `/read ${r.purpose} ${r.ref}`,
72
+ "",
73
+ `Guided reading of ${r.ref}. The user is learning to read papers: you find the passages and ask; the user reads and writes the answers.`,
74
+ "",
75
+ "1. Find the work: zotero_find_items first. For a DOI or PMID that is not in the library, scholar_get_work, and offer to import it.",
76
+ "2. Read only what the depth below needs: zotero_get_fulltext for a library item; otherwise scholar_read_oa_fulltext (the section list first, then only the sections you need); otherwise scholar_get_work for the abstract.",
77
+ "3. Write the reading note as described below.",
78
+ "4. Give the user the path of the note and the first question to answer. Do not answer the questions.",
79
+ "",
80
+ "Rules:",
81
+ "- State no results, effect sizes, summaries or conclusions, in the note or in the chat. Quote the paper and ask; the user writes the answers.",
82
+ "- Copy quotes word for word from what you read. After each quote give its place: the page only with evidence: full text, otherwise the section (Abstract, Methods, Results).",
83
+ `- The note is Literature/<citekey>.md in the Sub-Sub folder, with the front matter of a literature note from the shared rules plus type: reading-note and purpose: ${r.purpose}; evidence says what you read. If the note exists, do not overwrite it: add a section "## Reading ${r.date}: ${r.purpose}" at the end with edit, with the content below one heading level lower.`,
84
+ '- Start with "## Why I am reading this" (the reason, in the user\'s words when they gave them) and end with one line "Next step:" (what to read or do next).',
85
+ "",
86
+ ...depth(r),
87
+ ].join("\n");
88
+ }
package/dist/roles.js CHANGED
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * Which tools each mode may use, and which calls need the user's approval.
3
3
  */
4
- import { profileSpec } from "./profiles.js";
5
4
  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 and the profile for these). */
5
+ /** Prefixes of the tools that Sub-Sub's servers provide (the gate checks the mode for these). */
7
6
  export const SERVER_PREFIXES = ["zotero_", "scholar_", "verify_"];
8
7
  export const ZOTERO_READ = [
9
8
  "status",
@@ -55,12 +54,11 @@ export const ZOTERO_CHECKS = [
55
54
  ].map((n) => `zotero_${n}`);
56
55
  /**
57
56
  * The tools of a mode. The Librarian manages the library: Zotero tools and files.
58
- * The Researcher has the same, plus searches, notes and reference checks. The
59
- * profile then removes tools. Every library change is still previewed (gateKind).
57
+ * The Researcher has the same, plus searches, notes and reference checks. Every
58
+ * library change is still previewed (gateKind).
60
59
  */
61
60
  export function toolsFor(mode, registered, opts = {}) {
62
61
  const has = new Set(registered);
63
- const without = new Set(profileSpec(opts.profile ?? "editor").without);
64
62
  const zotero = registered.filter((n) => n.startsWith("zotero_") && (opts.bench || !n.startsWith("zotero_bakeoff_")));
65
63
  const wanted = [
66
64
  ...ZOTERO_READ,
@@ -69,16 +67,12 @@ export function toolsFor(mode, registered, opts = {}) {
69
67
  ...(mode === "researcher" ? registered.filter((n) => n.startsWith("scholar_") || n.startsWith("verify_")) : []),
70
68
  ...FILE_TOOLS,
71
69
  ];
72
- return [...new Set(wanted)].filter((n) => has.has(n) && !without.has(n));
70
+ return [...new Set(wanted)].filter((n) => has.has(n));
73
71
  }
74
72
  /** Why a Sub-Sub tool is not available now, or undefined when it is. */
75
73
  export function unavailableReason(name, mode, registered, opts = {}) {
76
74
  if (toolsFor(mode, registered, opts).includes(name))
77
75
  return undefined;
78
- const profile = opts.profile ?? "editor";
79
- if (profileSpec(profile).without.includes(name)) {
80
- return `${name} is not part of the ${profileSpec(profile).label} profile. The user can change the profile with /profile.`;
81
- }
82
76
  if (mode === "librarian" && toolsFor("researcher", registered, opts).includes(name)) {
83
77
  return `${name} is part of the Researcher, which also does everything the Librarian does. The user can switch with /researcher.`;
84
78
  }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * /ai-statement: a draft of the statement on the use of AI that journals ask for (ICMJE and
3
+ * most publishers), with the facts Sub-Sub knows: its version, the models of each mode and their
4
+ * providers, and the checks the program makes. The authors complete the bracketed parts.
5
+ */
6
+ export const SUBSUB_DOI = "10.5281/zenodo.23238602";
7
+ /** "opencode-go/mimo-v2.6-pro" -> "mimo-v2.6-pro (OpenCode Go)". */
8
+ export function modelLabel(spec) {
9
+ const i = spec.indexOf("/");
10
+ if (i < 0)
11
+ return spec;
12
+ const provider = spec.slice(0, i);
13
+ const names = { "opencode-go": "OpenCode Go", google: "Google", mistral: "Mistral AI", openai: "OpenAI", anthropic: "Anthropic", openrouter: "OpenRouter", deepseek: "DeepSeek" };
14
+ return `${spec.slice(i + 1)} (${names[provider] ?? provider})`;
15
+ }
16
+ export function aiStatement(f) {
17
+ const models = [...new Set(Object.values(f.models).filter(Boolean))].map(modelLabel);
18
+ const modelText = models.length ? `the language model${models.length > 1 ? "s" : ""} ${models.join(" and ")}` : "[the language model and its provider]";
19
+ const uses = f.uses?.trim() || "[the tasks: for example, literature searches, screening of search results, reading notes on included studies, checks of the references]";
20
+ return [
21
+ "## Use of AI tools",
22
+ "",
23
+ `We used Sub-Sub ${f.version} (Jacinto T., doi:${SUBSUB_DOI}), an open-source research assistant that works from a Zotero reference library, with ${modelText}, in [month and year]. We used it for ${uses}.`,
24
+ "",
25
+ "Sub-Sub records in each note whether it was written from the full text, the abstract or the bibliographic record, checks that quotations are in the full text, and makes no change to the reference library without the authors' approval." +
26
+ (f.starbuck ? " The cited works were checked for existence, bibliographic accuracy and retraction with Starbuck." : ""),
27
+ "",
28
+ "[Choose one: No text of this manuscript was written by an AI tool. | Sub-Sub drafted [which parts]; the authors revised and rewrote this text.]",
29
+ "",
30
+ "The authors checked every reference and every statement taken from the sources against the original publications, and take full responsibility for the content of this article.",
31
+ "",
32
+ "<!-- Draft made by Sub-Sub on " + f.date + ". Complete the parts in brackets, delete this line, and follow the journal's instructions on where the statement goes (often the Methods or the Acknowledgements). -->",
33
+ "",
34
+ ].join("\n");
35
+ }