@tiagojct/subsub 0.11.4 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/README.md +8 -3
- package/dist/bench.js +3 -2
- package/dist/cli.js +6 -0
- package/dist/config.js +13 -2
- package/dist/init.js +31 -5
- package/dist/keys.js +38 -8
- package/dist/preview.js +130 -26
- package/dist/roles.js +12 -1
- package/dist/subsub.js +67 -17
- package/dist/usage.js +69 -0
- package/dist/web.js +2 -5
- package/package.json +4 -3
- package/roles/librarian.md +13 -1
- package/roles/researcher.md +12 -0
- package/src/bench.ts +3 -2
- package/src/cli.ts +6 -0
- package/src/config.ts +16 -3
- package/src/init.ts +48 -4
- package/src/keys.ts +37 -10
- package/src/preview.ts +126 -25
- package/src/roles.ts +12 -1
- package/src/subsub.ts +66 -16
- package/src/usage.ts +75 -0
- package/src/web.ts +2 -5
- package/web/app.css +11 -0
- package/web/app.js +50 -0
- package/web/i18n.js +36 -0
- package/web/index.html +1 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Changes
|
|
2
|
+
|
|
3
|
+
Sub-Sub follows the version numbers of npm: a change in the second number (0.12) can change how you use it; a change in the third (0.12.1) only fixes. Until the pilot results are in, new versions are fixes only, apart from changes that the pilot asks for.
|
|
4
|
+
|
|
5
|
+
## 0.12.1 (2026-10-07)
|
|
6
|
+
|
|
7
|
+
Files and notes without a parent item (Zotero server 0.5.5):
|
|
8
|
+
|
|
9
|
+
- Sub-Sub lists them, reads each file, finds the reference and makes the parent item: from a DOI, PMID or ISBN, or from the file's own text (magazines, reports, books). An item already in the library is used instead of a copy. One preview for up to 25 files; undo puts the files back and moves the new items to the trash.
|
|
10
|
+
- Reference search for these files in Crossref, Google Books, Internet Archive, Open Library and Wikidata, without a key. Web search through Brave Search when you save a key in `subsub init`.
|
|
11
|
+
- A search of the library for attachments or notes now says that it covers regular items only, instead of finding nothing.
|
|
12
|
+
|
|
13
|
+
Also:
|
|
14
|
+
|
|
15
|
+
- Sub-Sub no longer offers to change its own programs; it says what it cannot do.
|
|
16
|
+
- Citekeys for long organisation names are shorter (europeancommission2026), and an item without creators takes the first main word of its title (economist2026, not the2026). Existing keys do not change.
|
|
17
|
+
- OpenCode's "requires Global regions" error is explained in plain words.
|
|
18
|
+
|
|
19
|
+
## 0.12.0 (2026-10-07)
|
|
20
|
+
|
|
21
|
+
For new users:
|
|
22
|
+
|
|
23
|
+
- `subsub init` asks whether you are a student. Students start with the Reader profile: Sub-Sub helps them read and does not write summaries or syntheses unless asked.
|
|
24
|
+
- The installers end with `subsub doctor`, which also downloads the Zotero server, so the first start is quicker.
|
|
25
|
+
- The browser view shows what is still missing on the first start (Zotero, a model, the servers), each with its fix.
|
|
26
|
+
- A tested EU option: Mistral, with servers in the European Union. The key form says where each provider processes your data.
|
|
27
|
+
|
|
28
|
+
Library changes:
|
|
29
|
+
|
|
30
|
+
- Every applied change has an Undo button in the browser view. It previews first, like `/undo`.
|
|
31
|
+
- New collections are journaled too: undo moves them to the Zotero trash. (Zotero server 0.5.4.)
|
|
32
|
+
- A preview of more than 10 items starts with a summary: each tag and field, and how many items it changes.
|
|
33
|
+
- Previews, approvals and notices are in European Portuguese when the language is Portuguese.
|
|
34
|
+
|
|
35
|
+
Free tiers and errors:
|
|
36
|
+
|
|
37
|
+
- After a per-minute limit (Google's free tier), Sub-Sub waits as long as the provider says and continues the task, up to three times. pi does not retry these errors, because their text mentions billing.
|
|
38
|
+
|
|
39
|
+
For the pilot:
|
|
40
|
+
|
|
41
|
+
- An optional usage log, kept on your computer (`subsub usage`): times, counts, commands and tool names, never what you wrote. It is off unless you turn it on in `subsub init`.
|
|
42
|
+
- The pilot page has a task that compares the same work with and without Sub-Sub.
|
|
43
|
+
|
|
44
|
+
Robustness:
|
|
45
|
+
|
|
46
|
+
- Sub-Sub accepts pi patch releases only (~1.0.4). A weekly test runs against pi's latest release, so a change in pi shows before users meet it.
|
|
47
|
+
- Randomised tests of the path gate: 3000 spellings of paths (dot segments, case, accents, links) must agree with the rules.
|
|
48
|
+
|
|
49
|
+
Models page: the limits of the tests come first. Most tested models are adequate; the table is not a ranking.
|
|
50
|
+
|
|
51
|
+
## 0.11.4 (2026-10-07)
|
|
52
|
+
|
|
53
|
+
- A free way to try Sub-Sub: Google AI Studio, with gemini-3.1-flash-lite in both modes (`subsub init`, or the model dialog).
|
|
54
|
+
- Model dialog: search, and the tested models first, marked.
|
|
55
|
+
- Provider errors in plain words: limit reached, provider busy, reply blocked, key refused.
|
|
56
|
+
|
|
57
|
+
## 0.11.3 (2026-10-07)
|
|
58
|
+
|
|
59
|
+
- `subsub init` asks about the FMUP set-up after the folder.
|
|
60
|
+
- Created notes keep raw HTML as text; the preview shows the whole note (Zotero server 0.5.3).
|
|
61
|
+
|
|
62
|
+
## 0.11.2 (2026-10-07)
|
|
63
|
+
|
|
64
|
+
- The mode's model is applied again at start (a regression in 0.11.1).
|
|
65
|
+
- After Yes, the preview is computed again; if the library changed meanwhile, the new preview is shown first.
|
|
66
|
+
- Case and Unicode spellings no longer get round protected and key files.
|
|
67
|
+
|
|
68
|
+
## 0.11.1 (2026-10-07)
|
|
69
|
+
|
|
70
|
+
- Writes into hidden folders always ask; the model cannot read files with keys.
|
|
71
|
+
- pi's install report is off.
|
|
72
|
+
|
|
73
|
+
## 0.11.0 (2026-10-06)
|
|
74
|
+
|
|
75
|
+
- Two modes again: the Librarian manages the library; the Researcher does that and the research.
|
|
76
|
+
- Defaults from the October model test: mimo-v2.6-flash for the Librarian, mimo-v2.6-pro for the Researcher.
|
|
77
|
+
|
|
78
|
+
Earlier versions: see the git history.
|
package/README.md
CHANGED
|
@@ -26,12 +26,12 @@ Every library change works the same way in both modes:
|
|
|
26
26
|
|
|
27
27
|
- The Zotero server computes the change first. Sub-Sub shows it to you (added tags in green, removed tags in red) and applies it only when you select Yes. The check is in the program, so the model cannot skip it.
|
|
28
28
|
- If you edit an item in Zotero while the preview is open, Sub-Sub shows you the new preview before it changes anything. Each change also sends the item's version, so a field you edit at the moment of writing is not overwritten.
|
|
29
|
-
- Every change
|
|
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
32
|
## Profiles
|
|
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`.
|
|
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.
|
|
35
35
|
|
|
36
36
|
| Profile | What Sub-Sub does |
|
|
37
37
|
|---|---|
|
|
@@ -129,13 +129,14 @@ subsub review apply 13-31
|
|
|
129
129
|
|
|
130
130
|
| Setting | Meaning | Default |
|
|
131
131
|
|---|---|---|
|
|
132
|
-
| `profile` | `reader`, `scholar`, `author` or `editor` | `scholar` |
|
|
132
|
+
| `profile` | `reader`, `scholar`, `author` or `editor` | `reader` for students, `scholar` for others (`subsub init` asks) |
|
|
133
133
|
| `userName`, `about` | Your name and one line about you, for the replies | none |
|
|
134
134
|
| `language` | Language of replies and notes, or `auto` for the language you write in | `English` |
|
|
135
135
|
| `vault` | The Sub-Sub folder | from the server settings |
|
|
136
136
|
| `models` | The model for each mode, as provider/id: `{"librarian": "...", "researcher": "..."}`. `{}` means you choose in Sub-Sub. The free choice in `subsub init` sets `google/gemini-3.1-flash-lite` for both | `opencode-go/mimo-v2.6-flash` and `opencode-go/mimo-v2.6-pro` |
|
|
137
137
|
| `defaultMode` | `researcher` or `librarian` | `researcher` |
|
|
138
138
|
| `addons` | `["starbuck"]` adds reference checks | `[]` |
|
|
139
|
+
| `usageLog` | `true` keeps the pilot's usage log on this computer (`subsub usage`) | off |
|
|
139
140
|
| `serverDir` | A local copy of zotero-local-mcp, for development | the PyPI release |
|
|
140
141
|
| `starbuckDir` | A local copy of Starbuck, for development | the pinned release |
|
|
141
142
|
| `look`, `quotes`, `themes` | The terminal header, the Moby-Dick quotations, and the terminal colours (`subsub-glauca`, `subsub-try-works`, or `false` for your pi theme) | on, on, `subsub-glauca` |
|
|
@@ -172,6 +173,10 @@ npm run build
|
|
|
172
173
|
|
|
173
174
|
End-to-end tests (real pi and Python servers, a fake Zotero, a scripted model): `ZLM_DIR=/path/to/zotero-local-mcp npm run test:e2e`.
|
|
174
175
|
|
|
176
|
+
## Changes
|
|
177
|
+
|
|
178
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
|
179
|
+
|
|
175
180
|
## Licence
|
|
176
181
|
|
|
177
182
|
[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
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* writes are allowed only in the run folder.
|
|
14
14
|
*/
|
|
15
15
|
import { spawn, spawnSync } from "node:child_process";
|
|
16
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { dirname, join, resolve } from "node:path";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
19
|
import { expand, loadConfig, starbuckCommand, starbuckEnabled } from "./config.js";
|
|
@@ -408,7 +408,8 @@ export function scoreRun(task, dir, t, index, blindRef, starbuck) {
|
|
|
408
408
|
dois: dois.length,
|
|
409
409
|
pmids: pmids.length,
|
|
410
410
|
ungrounded: [...dois.filter((d) => !seen.includes(d)), ...pmids.filter((p) => !seen.includes(p))],
|
|
411
|
-
|
|
411
|
+
// A model may write .plans as a file instead of a folder: then there is no screening log.
|
|
412
|
+
screening_log: existsSync(plans) && statSync(plans).isDirectory() && readdirSync(plans).some((f) => /screening/i.test(f)),
|
|
412
413
|
multi_searches: calls.filter((c) => c.name === "scholar_search_multi").length,
|
|
413
414
|
fulltext_reads: calls.filter((c) => c.name === "scholar_read_oa_fulltext" || c.name === "zotero_get_fulltext").length,
|
|
414
415
|
starbuck: text && starbuck ? starbuck(join(dir, "Lit.md")) : undefined,
|
package/dist/cli.js
CHANGED
|
@@ -151,6 +151,7 @@ Usage:
|
|
|
151
151
|
subsub web Open Sub-Sub in the browser
|
|
152
152
|
subsub init Set up or change the settings (folder, tag list, profile, models)
|
|
153
153
|
subsub doctor Check the set-up; each line marked FIX says what to do
|
|
154
|
+
subsub usage The pilot's usage log (if turned on): a summary and where the file is
|
|
154
155
|
subsub shortcut Add a Sub-Sub shortcut (--remove takes it away)
|
|
155
156
|
subsub review preview|apply <note>
|
|
156
157
|
Check or apply a tag review note from Inbox/
|
|
@@ -198,6 +199,11 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
198
199
|
process.exitCode = await doctorMain(env);
|
|
199
200
|
return;
|
|
200
201
|
}
|
|
202
|
+
if (args[0] === "usage") {
|
|
203
|
+
const { usageMain } = await import("./usage.js");
|
|
204
|
+
process.exitCode = usageMain(env);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
201
207
|
if (args[0] === "review") {
|
|
202
208
|
const r = reviewCommand(args.slice(1), loadConfig(env), env);
|
|
203
209
|
const res = spawnSync(r.command, r.args, { env: r.env, stdio: "inherit" });
|
package/dist/config.js
CHANGED
|
@@ -36,20 +36,30 @@ export const FREE_MODELS = {
|
|
|
36
36
|
librarian: "google/gemini-3.1-flash-lite",
|
|
37
37
|
researcher: "google/gemini-3.1-flash-lite",
|
|
38
38
|
};
|
|
39
|
+
/** The tested EU-hosted models (Mistral, servers in the EU; see the Models page). */
|
|
40
|
+
export const EU_MODELS = {
|
|
41
|
+
librarian: "mistral/mistral-medium-3.5",
|
|
42
|
+
researcher: "mistral/mistral-medium-3.5",
|
|
43
|
+
};
|
|
39
44
|
/**
|
|
40
45
|
* Models marked in the model dialog, from the model tests (see the Models page). `why` is a
|
|
41
46
|
* key into the web view's strings: tested (a default), cheaper (tested, same quality, lower cost),
|
|
42
|
-
* free (tested, works on a free tier).
|
|
47
|
+
* free (tested, works on a free tier), eu (tested, servers in the EU).
|
|
43
48
|
*/
|
|
44
49
|
export const RECOMMENDED_MODELS = [
|
|
45
50
|
{ spec: DEFAULT_MODELS.librarian, modes: ["librarian"], why: "tested" },
|
|
46
51
|
{ spec: DEFAULT_MODELS.researcher, modes: ["researcher"], why: "tested" },
|
|
47
52
|
{ spec: "opencode-go/qwen3.8-flash", modes: ["researcher"], why: "cheaper" },
|
|
53
|
+
{ spec: EU_MODELS.researcher, modes: ["librarian", "researcher"], why: "eu" },
|
|
48
54
|
{ spec: FREE_MODELS.librarian, modes: ["librarian", "researcher"], why: "free" },
|
|
49
55
|
];
|
|
56
|
+
/** "pt" when the language setting is Portuguese, else "en": the language of the interface. */
|
|
57
|
+
export function uiLanguage(language) {
|
|
58
|
+
return /portug|^pt\b|^pt-/i.test(language ?? "") ? "pt" : "en";
|
|
59
|
+
}
|
|
50
60
|
export const PROFILES = ["reader", "scholar", "author", "editor"];
|
|
51
61
|
/** The Zotero server release that this Sub-Sub version runs when there is no local server folder. */
|
|
52
|
-
export const SERVER_VERSION = "0.5.
|
|
62
|
+
export const SERVER_VERSION = "0.5.5";
|
|
53
63
|
/** The Starbuck release (PyPI) that this Sub-Sub version runs when there is no local Starbuck folder. */
|
|
54
64
|
export const STARBUCK_VERSION = "0.2.0";
|
|
55
65
|
export const STARBUCK_SOURCE = `starbuck==${STARBUCK_VERSION}`;
|
|
@@ -163,6 +173,7 @@ export function loadConfig(env = process.env) {
|
|
|
163
173
|
themes,
|
|
164
174
|
addons: Array.isArray(user.addons) ? user.addons.filter((a) => typeof a === "string") : [],
|
|
165
175
|
starbuckDir: expand(user.starbuckDir ?? "~/Projects/starbuck", home),
|
|
176
|
+
usageLog: user.usageLog === true,
|
|
166
177
|
};
|
|
167
178
|
}
|
|
168
179
|
/** uv is often missing from PATH when pi is started outside a login shell. */
|
package/dist/init.js
CHANGED
|
@@ -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
|
-
* --email, --starbuck on|off, --models keep|later|opencode-go|google-free.
|
|
22
|
+
* --email, --starbuck on|off, --models keep|later|opencode-go|mistral-eu|google-free.
|
|
23
23
|
*/
|
|
24
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { chmodSync, 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, DEFAULT_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, 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 { KEY_PROVIDERS, saveApiKey } from "./keys.js";
|
|
@@ -132,8 +132,17 @@ export async function runInit(io, flags, env = process.env, deps = {}) {
|
|
|
132
132
|
{ value: "fmup", label: "FMUP / U.Porto: health-sciences tags, Reader profile (the FMUP model service is coming soon)" },
|
|
133
133
|
], flags.setup ?? existing.setup ?? "standard");
|
|
134
134
|
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
|
+
}
|
|
135
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.");
|
|
136
|
-
const profileDefault = flags.profile ?? existing.profile ?? (
|
|
145
|
+
const profileDefault = flags.profile ?? existing.profile ?? (student ? "reader" : "scholar");
|
|
137
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");
|
|
138
147
|
// The tag list: a custom path in the server settings stays; otherwise Zotero/Zotero tags.md (moved, kept or new).
|
|
139
148
|
const vocabTarget = join(notes, SETTINGS_DIR, SETTINGS_FILES.tags);
|
|
@@ -155,11 +164,19 @@ export async function runInit(io, flags, env = process.env, deps = {}) {
|
|
|
155
164
|
}
|
|
156
165
|
explain("Contact email", "Unpaywall needs an email address to find open-access PDFs. Sub-Sub sends it with its requests to PubMed, Europe PMC, OpenAlex, Crossref and Unpaywall, as they ask, and to nobody else.");
|
|
157
166
|
const email = (await io.ask("Email (optional)", flags.email ?? envValues.ZOTERO_CONTACT_EMAIL ?? "")).trim();
|
|
167
|
+
explain("Web search", "Optional. To identify files without a parent item, such as magazines and reports, Sub-Sub can search the web through Brave Search. Brave needs a key: https://brave.com/search/api/ (free tier; a card is asked at sign-up). Searches then go to Brave, in the United States. Without a key Sub-Sub uses Crossref, Google Books, Internet Archive, Open Library and Wikidata.");
|
|
168
|
+
const hadBrave = Boolean(envValues.BRAVE_API_KEY);
|
|
169
|
+
const brave = (await io.ask(hadBrave ? "Brave Search key (Enter keeps the saved key)" : "Brave Search key (Enter for none)", flags.brave ?? "")).trim();
|
|
158
170
|
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.");
|
|
159
171
|
const starbuck = await io.choose("Reference checks", [
|
|
160
172
|
{ value: "off", label: "Off" },
|
|
161
173
|
{ value: "on", label: "On (downloads Starbuck now; uses the email above)" },
|
|
162
174
|
], flags.starbuck ?? (starbuckEnabled(current) ? "on" : "off"));
|
|
175
|
+
explain("Usage log for the pilot", "If you take part in the Sub-Sub pilot, Sub-Sub can keep a log on this computer: when you used it, which commands, how long it worked and how many library changes you approved. Never what you wrote or what it replied. Nothing is sent; subsub usage shows the log, and you decide whether to send it.");
|
|
176
|
+
const usage = await io.choose("Usage log", [
|
|
177
|
+
{ value: "off", label: "Off" },
|
|
178
|
+
{ value: "on", label: "On (for the pilot)" },
|
|
179
|
+
], flags.usage ?? (existing.usageLog === true ? "on" : "off"));
|
|
163
180
|
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).");
|
|
164
181
|
const hadModel = "model" in existing || "models" in existing;
|
|
165
182
|
const show = (m) => (Object.keys(m).length ? Object.entries(m).map(([k, v]) => `${k} ${v}`).join(", ") : "choose in Sub-Sub");
|
|
@@ -167,6 +184,7 @@ export async function runInit(io, flags, env = process.env, deps = {}) {
|
|
|
167
184
|
...(hadModel ? [{ value: "keep", label: `Keep: ${show(current.models)}` }] : []),
|
|
168
185
|
{ value: "later", label: "Choose later in Sub-Sub" },
|
|
169
186
|
{ value: "opencode-go", label: `OpenCode Go, tested: ${TESTED_MODELS.librarian} for the Librarian, ${TESTED_MODELS.researcher} for the Researcher` },
|
|
187
|
+
{ value: "mistral-eu", label: `Mistral, servers in the EU (GDPR): ${EU_MODELS.researcher} in both modes; about six times the cost of OpenCode Go` },
|
|
170
188
|
{ value: "google-free", label: `Free, to try Sub-Sub: Google AI Studio, ${FREE_MODELS.researcher} in both modes (a free key, no card; limits per minute and per day; 18 or older)` },
|
|
171
189
|
];
|
|
172
190
|
const agentDir = initEnv.SUBSUB_AGENT_DIR ? expand(initEnv.SUBSUB_AGENT_DIR, home) : join(home, ".subsub", "agent");
|
|
@@ -203,7 +221,9 @@ export async function runInit(io, flags, env = process.env, deps = {}) {
|
|
|
203
221
|
const alerts = !existsSync(alertsTarget) && existsSync(legacyAlerts) ? legacyAlerts : alertsTarget;
|
|
204
222
|
mkdirSync(dirname(envFile), { recursive: true });
|
|
205
223
|
const before = existsSync(envFile) ? readFileSync(envFile, "utf8") : "# Settings for the Sub-Sub Zotero servers (zotero-local-mcp). Written by subsub init.\n";
|
|
206
|
-
writeFileSync(envFile, upsertEnv(before, { ZOTERO_VAULT: notes, ZOTERO_VOCAB: vocab, ZOTERO_ALERTS: alerts, ZOTERO_CONTACT_EMAIL: email || undefined }));
|
|
224
|
+
writeFileSync(envFile, upsertEnv(before, { ZOTERO_VAULT: notes, ZOTERO_VOCAB: vocab, ZOTERO_ALERTS: alerts, ZOTERO_CONTACT_EMAIL: email || undefined, BRAVE_API_KEY: brave || undefined }));
|
|
225
|
+
if (brave || hadBrave)
|
|
226
|
+
chmodSync(envFile, 0o600); // the file holds a key
|
|
207
227
|
const patch = {
|
|
208
228
|
profile,
|
|
209
229
|
userName: userName || undefined,
|
|
@@ -214,6 +234,7 @@ export async function runInit(io, flags, env = process.env, deps = {}) {
|
|
|
214
234
|
envFile,
|
|
215
235
|
libraryChanges: undefined,
|
|
216
236
|
addons: withStarbuck(current.addons, starbuck === "on"),
|
|
237
|
+
usageLog: usage === "on" ? true : undefined,
|
|
217
238
|
};
|
|
218
239
|
// A model per mode; "model" (0.10, one for both) is replaced.
|
|
219
240
|
patch.model = undefined;
|
|
@@ -225,6 +246,8 @@ export async function runInit(io, flags, env = process.env, deps = {}) {
|
|
|
225
246
|
patch.models = TESTED_MODELS;
|
|
226
247
|
if (models === "google-free")
|
|
227
248
|
patch.models = FREE_MODELS;
|
|
249
|
+
if (models === "mistral-eu")
|
|
250
|
+
patch.models = EU_MODELS;
|
|
228
251
|
saveConfig(patch, initEnv);
|
|
229
252
|
let keyNote = "";
|
|
230
253
|
if (googleKey) {
|
|
@@ -236,6 +259,9 @@ export async function runInit(io, flags, env = process.env, deps = {}) {
|
|
|
236
259
|
keyNote = `The Google key was not saved (${err.message}). Add it in Sub-Sub with the model button or /login.`;
|
|
237
260
|
}
|
|
238
261
|
}
|
|
262
|
+
else if (models === "mistral-eu" && !hasProvider(agentDir, "mistral")) {
|
|
263
|
+
keyNote = `No Mistral key yet. Get one at ${KEY_PROVIDERS.find((p) => p.id === "mistral")?.url}, then add it in Sub-Sub with the model button (subsub web) or /login (in the terminal).`;
|
|
264
|
+
}
|
|
239
265
|
else if (models === "google-free" && !hasProvider(agentDir, "google")) {
|
|
240
266
|
keyNote = "No Google key yet. Add it in Sub-Sub with the model button (subsub web) or /login (in the terminal), with the provider Google Gemini.";
|
|
241
267
|
}
|
package/dist/keys.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
/** Model providers that take an API key, and plain words for their common errors. */
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* `where`: where the provider processes the data, as a key into the web view's strings
|
|
6
|
+
* (eu, us, cn, varies). Shown next to the key form, so the choice of provider is an informed one.
|
|
7
|
+
*/
|
|
4
8
|
export const KEY_PROVIDERS = [
|
|
5
|
-
{ id: "opencode-go", label: "OpenCode Go", url: "https://opencode.ai/auth" },
|
|
6
|
-
{ id: "openrouter", label: "OpenRouter", url: "https://openrouter.ai/keys" },
|
|
7
|
-
{ id: "anthropic", label: "Anthropic", url: "https://console.anthropic.com/settings/keys" },
|
|
8
|
-
{ id: "openai", label: "OpenAI", url: "https://platform.openai.com/api-keys" },
|
|
9
|
-
{ id: "google", label: "Google Gemini", url: "https://aistudio.google.com/apikey" },
|
|
10
|
-
{ id: "mistral", label: "Mistral", url: "https://console.mistral.ai/api-keys" },
|
|
11
|
-
{ id: "deepseek", label: "DeepSeek", url: "https://platform.deepseek.com/api_keys" },
|
|
9
|
+
{ id: "opencode-go", label: "OpenCode Go", url: "https://opencode.ai/auth", where: "varies" },
|
|
10
|
+
{ id: "openrouter", label: "OpenRouter", url: "https://openrouter.ai/keys", where: "varies" },
|
|
11
|
+
{ id: "anthropic", label: "Anthropic", url: "https://console.anthropic.com/settings/keys", where: "us" },
|
|
12
|
+
{ id: "openai", label: "OpenAI", url: "https://platform.openai.com/api-keys", where: "us" },
|
|
13
|
+
{ id: "google", label: "Google Gemini", url: "https://aistudio.google.com/apikey", where: "us" },
|
|
14
|
+
{ id: "mistral", label: "Mistral (EU)", url: "https://console.mistral.ai/api-keys", where: "eu" },
|
|
15
|
+
{ id: "deepseek", label: "DeepSeek", url: "https://platform.deepseek.com/api_keys", where: "cn" },
|
|
12
16
|
];
|
|
13
17
|
/** Save an API key the way pi's /login does: auth.json in the agent folder, readable only by the user. */
|
|
14
18
|
export function saveApiKey(agentDir, provider, key) {
|
|
@@ -36,6 +40,8 @@ export function saveApiKey(agentDir, provider, key) {
|
|
|
36
40
|
export function providerTrouble(text) {
|
|
37
41
|
if (!text)
|
|
38
42
|
return undefined;
|
|
43
|
+
if (/requires Global regions|not available in your region|region.{0,40}not (supported|allowed)/i.test(text))
|
|
44
|
+
return "region";
|
|
39
45
|
if (/RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|usage limit/i.test(text))
|
|
40
46
|
return "quota";
|
|
41
47
|
if (/\b(503|529)\b|UNAVAILABLE|high demand|overloaded/i.test(text))
|
|
@@ -46,10 +52,34 @@ export function providerTrouble(text) {
|
|
|
46
52
|
return "key";
|
|
47
53
|
return undefined;
|
|
48
54
|
}
|
|
49
|
-
/** The
|
|
55
|
+
/** The words for each kind in notices (the web view has its own, in web/i18n.js). */
|
|
56
|
+
export const TROUBLE_TEXT_PT = {
|
|
57
|
+
quota: "O fornecedor do modelo recusou o pedido: atingiu um limite da sua conta. Os níveis gratuitos limitam o texto enviado por minuto e os pedidos por dia. Espere um minuto e peça ao Sub-Sub para continuar; se voltar a acontecer, esgotou o limite diário (renova-se no dia seguinte). Ou use outro modelo (/model).",
|
|
58
|
+
busy: "O fornecedor do modelo está sobrecarregado neste momento. Tente outra vez dentro de alguns minutos ou use outro modelo (/model).",
|
|
59
|
+
blocked: "O fornecedor do modelo interrompeu a resposta (por exemplo, por repetir uma fonte palavra por palavra). Peça outra vez por outras palavras ou use outro modelo (/model).",
|
|
60
|
+
key: "O fornecedor do modelo não aceitou a chave da API. Verifique-a ou introduza uma nova com /login.",
|
|
61
|
+
region: "O fornecedor recusa este modelo com a definição de região da sua conta. No OpenCode, permita as regiões globais (Global) nas definições de privacidade do workspace, ou escolha um modelo que a sua região permita (/model).",
|
|
62
|
+
};
|
|
50
63
|
export const TROUBLE_TEXT = {
|
|
51
64
|
quota: "The model provider refused the request: a limit of your account was reached. Free tiers limit the text sent each minute and the requests each day. Wait a minute and ask Sub-Sub to continue; if it happens again, the daily limit is used up (it resets the next day). Or use another model (/model).",
|
|
52
65
|
busy: "The model provider is busy at the moment. Try again in a few minutes, or use another model (/model).",
|
|
53
66
|
blocked: "The model provider stopped the reply (for example, because it repeated a source word for word). Ask again in other words, or use another model (/model).",
|
|
54
67
|
key: "The model provider did not accept the API key. Check it, or enter a new one with /login.",
|
|
68
|
+
region: "The provider refuses this model with your account's region setting. In OpenCode, allow Global regions in the workspace's Privacy settings, or choose a model your region allows (/model).",
|
|
55
69
|
};
|
|
70
|
+
/**
|
|
71
|
+
* Seconds to wait before a per-minute limit lets the request through again, or undefined when the
|
|
72
|
+
* error is not a per-minute limit (a daily limit, a spent balance, anything else). pi does not
|
|
73
|
+
* retry Google's per-minute 429, because its text mentions billing.
|
|
74
|
+
*/
|
|
75
|
+
export function perMinuteWait(text) {
|
|
76
|
+
if (!text || providerTrouble(text) !== "quota")
|
|
77
|
+
return undefined;
|
|
78
|
+
if (/PerDay|per day|daily/i.test(text))
|
|
79
|
+
return undefined;
|
|
80
|
+
const delay = /retryDelay\\*"\s*:\s*\\*"(\d+(?:\.\d+)?)s/.exec(text);
|
|
81
|
+
const seconds = delay?.[1] ?? /retry in (\d+(?:\.\d+)?)s/i.exec(text)?.[1];
|
|
82
|
+
if (seconds)
|
|
83
|
+
return Math.min(120, Math.ceil(Number(seconds)) + 2);
|
|
84
|
+
return /PerMinute|per minute/i.test(text) ? 60 : undefined;
|
|
85
|
+
}
|
package/dist/preview.js
CHANGED
|
@@ -3,23 +3,82 @@
|
|
|
3
3
|
* reads in the approval dialog. Plain ASCII, at most about 20 lines.
|
|
4
4
|
*/
|
|
5
5
|
const MAX_LINES = 14;
|
|
6
|
+
/** The words of the preview in English and European Portuguese; tags, titles and values stay as they are. */
|
|
7
|
+
const WORDS = {
|
|
8
|
+
en: {
|
|
9
|
+
more: (n) => `... and ${n} more`,
|
|
10
|
+
moreChars: (n) => `[${n} more characters not shown]`,
|
|
11
|
+
empty: "(empty)",
|
|
12
|
+
change: "(change)",
|
|
13
|
+
undo: "Undo",
|
|
14
|
+
already: "ALREADY APPLIED",
|
|
15
|
+
drift: (n) => `${n} item(s) changed after the note was written; the note overwrites them:`,
|
|
16
|
+
now: "now",
|
|
17
|
+
none: "(none)",
|
|
18
|
+
wouldChange: (n) => `${n} item(s) would change.`,
|
|
19
|
+
inShort: (listed, total) => (listed < total ? `In short (in the ${listed} items listed):` : "In short:"),
|
|
20
|
+
items: "Items:",
|
|
21
|
+
itemsN: (n) => `${n} item(s)`,
|
|
22
|
+
changed: "changed",
|
|
23
|
+
wouldImport: (n) => `${n} item(s) would be imported:`,
|
|
24
|
+
noAbstract: "(no abstract)",
|
|
25
|
+
wouldAttach: (n) => `${n} PDF(s) would be attached:`,
|
|
26
|
+
wouldParent: (n) => `${n} file(s) or note(s) would get a parent item:`,
|
|
27
|
+
newItem: "new",
|
|
28
|
+
existingItem: "existing item",
|
|
29
|
+
newCollection: "New collection",
|
|
30
|
+
inside: "inside",
|
|
31
|
+
noteOn: "Note on",
|
|
32
|
+
link: "Link",
|
|
33
|
+
skipped: "Skipped",
|
|
34
|
+
errors: "Errors",
|
|
35
|
+
},
|
|
36
|
+
pt: {
|
|
37
|
+
more: (n) => `... e mais ${n}`,
|
|
38
|
+
moreChars: (n) => `[mais ${n} caracteres não mostrados]`,
|
|
39
|
+
empty: "(vazio)",
|
|
40
|
+
change: "(alteração)",
|
|
41
|
+
undo: "Anular",
|
|
42
|
+
already: "JÁ APLICADO",
|
|
43
|
+
drift: (n) => `${n} item(ns) mudaram depois de a nota ser escrita; a nota substitui-os:`,
|
|
44
|
+
now: "agora",
|
|
45
|
+
none: "(nenhuma)",
|
|
46
|
+
wouldChange: (n) => `${n} item(ns) vão mudar.`,
|
|
47
|
+
inShort: (listed, total) => (listed < total ? `Em resumo (nos ${listed} itens listados):` : "Em resumo:"),
|
|
48
|
+
items: "Itens:",
|
|
49
|
+
itemsN: (n) => `${n} item(ns)`,
|
|
50
|
+
changed: "alterado",
|
|
51
|
+
wouldImport: (n) => `${n} item(ns) vão ser importados:`,
|
|
52
|
+
noAbstract: "(sem resumo)",
|
|
53
|
+
wouldAttach: (n) => `${n} PDF(s) vão ser anexados:`,
|
|
54
|
+
wouldParent: (n) => `${n} ficheiro(s) ou nota(s) vão ter um item principal:`,
|
|
55
|
+
newItem: "novo",
|
|
56
|
+
existingItem: "item existente",
|
|
57
|
+
newCollection: "Nova coleção",
|
|
58
|
+
inside: "dentro de",
|
|
59
|
+
noteOn: "Nota sobre",
|
|
60
|
+
link: "Ligação",
|
|
61
|
+
skipped: "Ignorados",
|
|
62
|
+
errors: "Erros",
|
|
63
|
+
},
|
|
64
|
+
};
|
|
6
65
|
function s(v, n = 90) {
|
|
7
66
|
const t = typeof v === "string" ? v : JSON.stringify(v);
|
|
8
67
|
return t.length > n ? `${t.slice(0, n - 3)}...` : t;
|
|
9
68
|
}
|
|
10
69
|
/** What will be written: shown whole up to a large limit, and the rest is counted, never silently cut. */
|
|
11
|
-
function whole(v, n = 3000) {
|
|
70
|
+
function whole(v, w, n = 3000) {
|
|
12
71
|
const t = typeof v === "string" ? v : JSON.stringify(v);
|
|
13
|
-
return t.length > n ? `${t.slice(0, n)}
|
|
72
|
+
return t.length > n ? `${t.slice(0, n)} ${w.moreChars(t.length - n)}` : t;
|
|
14
73
|
}
|
|
15
|
-
function list(rows, total) {
|
|
74
|
+
function list(rows, w, total) {
|
|
16
75
|
const out = rows.slice(0, MAX_LINES);
|
|
17
76
|
const rest = (total ?? rows.length) - out.length;
|
|
18
77
|
if (rest > 0)
|
|
19
|
-
out.push(
|
|
78
|
+
out.push(w.more(rest));
|
|
20
79
|
return out;
|
|
21
80
|
}
|
|
22
|
-
function describeChange(c) {
|
|
81
|
+
function describeChange(c, w) {
|
|
23
82
|
const parts = [];
|
|
24
83
|
if (Array.isArray(c.added) && c.added.length)
|
|
25
84
|
parts.push(`+ ${c.added.join(", ")}`);
|
|
@@ -30,62 +89,107 @@ function describeChange(c) {
|
|
|
30
89
|
continue;
|
|
31
90
|
if (v && typeof v === "object" && "after" in v) {
|
|
32
91
|
const b = v.before;
|
|
33
|
-
parts.push(`${k}: ${s(b ||
|
|
92
|
+
parts.push(`${k}: ${s(b || w.empty, 40)} -> ${whole(v.after, w)}`);
|
|
34
93
|
}
|
|
35
94
|
}
|
|
36
|
-
return `${s(c.item ?? c.key, 60)}: ${parts.join("; ") ||
|
|
95
|
+
return `${s(c.item ?? c.key, 60)}: ${parts.join("; ") || w.change}`;
|
|
37
96
|
}
|
|
38
|
-
|
|
97
|
+
/** For a large batch: each tag and field with the number of items it changes, so the whole is readable at once. */
|
|
98
|
+
function tally(changes, total, w) {
|
|
99
|
+
const count = new Map();
|
|
100
|
+
const add = (k) => count.set(k, (count.get(k) ?? 0) + 1);
|
|
101
|
+
for (const c of changes) {
|
|
102
|
+
for (const tag of c.added ?? [])
|
|
103
|
+
add(`+ ${tag}`);
|
|
104
|
+
for (const tag of c.removed ?? [])
|
|
105
|
+
add(`- ${tag}`);
|
|
106
|
+
for (const [k, v] of Object.entries(c))
|
|
107
|
+
if (v && typeof v === "object" && "after" in v)
|
|
108
|
+
add(`${k} ${w.changed}`);
|
|
109
|
+
}
|
|
110
|
+
if (!count.size)
|
|
111
|
+
return [];
|
|
112
|
+
const rank = (k) => (k.startsWith("+") ? 0 : k.startsWith("-") ? 1 : 2);
|
|
113
|
+
const rows = [...count].sort((a, b) => rank(a[0]) - rank(b[0]) || b[1] - a[1] || a[0].localeCompare(b[0])).map(([k, n]) => ` ${k}: ${w.itemsN(n)}`);
|
|
114
|
+
return [w.inShort(changes.length, total), ...list(rows, w), w.items];
|
|
115
|
+
}
|
|
116
|
+
export function formatPreview(tool, preview, lang = "en") {
|
|
117
|
+
const w = WORDS[lang];
|
|
39
118
|
if (!preview || typeof preview !== "object")
|
|
40
119
|
return s(preview, 1500);
|
|
41
120
|
const p = preview;
|
|
42
121
|
const lines = [];
|
|
43
122
|
if (p.undoing && typeof p.undoing === "object") {
|
|
44
123
|
const u = p.undoing;
|
|
45
|
-
lines.push(
|
|
124
|
+
lines.push(`${w.undo} ${u.op}: ${s(u.summary, 80)}`);
|
|
46
125
|
}
|
|
47
126
|
if (Array.isArray(p.already_applied) && p.already_applied.length) {
|
|
48
|
-
lines.push(
|
|
127
|
+
lines.push(`${w.already}: ${p.already_applied.join(", ")}`);
|
|
49
128
|
}
|
|
50
129
|
const drift = p.changed_since_note;
|
|
51
130
|
if (drift && typeof drift === "object" && typeof drift.count === "number") {
|
|
52
|
-
lines.push(
|
|
53
|
-
const items = Object.entries(drift.items ?? {}).map(([k, v]) => ` ${k} now: ${v.now?.join(", ") ||
|
|
131
|
+
lines.push(w.drift(drift.count));
|
|
132
|
+
const items = Object.entries(drift.items ?? {}).map(([k, v]) => ` ${k} ${w.now}: ${v.now?.join(", ") || w.none}`);
|
|
54
133
|
lines.push(...items.slice(0, 4));
|
|
55
134
|
}
|
|
56
135
|
if (typeof p.would_change === "number") {
|
|
57
|
-
lines.push(
|
|
136
|
+
lines.push(w.wouldChange(p.would_change));
|
|
58
137
|
const changes = p.changes ?? [];
|
|
59
|
-
|
|
138
|
+
if (p.would_change > 10)
|
|
139
|
+
lines.push(...tally(changes, p.would_change, w));
|
|
140
|
+
lines.push(...list(changes.map((c) => describeChange(c, w)), w, p.would_change));
|
|
60
141
|
}
|
|
61
142
|
if (Array.isArray(p.would_import)) {
|
|
62
|
-
lines.push(
|
|
63
|
-
lines.push(...list(p.would_import.map((r) => `${r.citekey} ${s(r.item, 70)}${r.has_abstract ? "" :
|
|
143
|
+
lines.push(w.wouldImport(p.would_import.length));
|
|
144
|
+
lines.push(...list(p.would_import.map((r) => `${r.citekey} ${s(r.item, 70)}${r.has_abstract ? "" : ` ${w.noAbstract}`}`), w));
|
|
64
145
|
}
|
|
65
146
|
if (Array.isArray(p.would_attach)) {
|
|
66
|
-
lines.push(
|
|
67
|
-
lines.push(...list(p.would_attach.map((r) => `${s(r.item, 60)} [${r.host_type ?? "?"}, ${r.version ?? "?"}${r.license ? `, ${r.license}` : ""}]`)));
|
|
147
|
+
lines.push(w.wouldAttach(p.would_attach.length));
|
|
148
|
+
lines.push(...list(p.would_attach.map((r) => `${s(r.item, 60)} [${r.host_type ?? "?"}, ${r.version ?? "?"}${r.license ? `, ${r.license}` : ""}]`), w));
|
|
149
|
+
}
|
|
150
|
+
if (Array.isArray(p.would_set_parent)) {
|
|
151
|
+
lines.push(w.wouldParent(p.would_set_parent.length));
|
|
152
|
+
const rows = [];
|
|
153
|
+
for (const r of p.would_set_parent.slice(0, 6)) {
|
|
154
|
+
rows.push(`${s(r.file, 60)}`);
|
|
155
|
+
if (r.parent === "new") {
|
|
156
|
+
const f = r.fields ?? {};
|
|
157
|
+
const where = ["publicationTitle", "date", "issue", "volume", "pages", "publisher", "ISBN", "DOI"]
|
|
158
|
+
.filter((k) => f[k])
|
|
159
|
+
.map((k) => `${k}: ${s(f[k], 40)}`)
|
|
160
|
+
.join(", ");
|
|
161
|
+
rows.push(` -> ${w.newItem} ${r.type} ${r.citekey}: ${s(r.item, 80)}`);
|
|
162
|
+
if (where)
|
|
163
|
+
rows.push(` ${where}`);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
rows.push(` -> ${w.existingItem} ${r.parent_key}: ${s(r.item, 70)}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
lines.push(...rows);
|
|
170
|
+
if (p.would_set_parent.length > 6)
|
|
171
|
+
lines.push(w.more(p.would_set_parent.length - 6));
|
|
68
172
|
}
|
|
69
173
|
if (p.would_create && typeof p.would_create === "object") {
|
|
70
|
-
const
|
|
71
|
-
lines.push(
|
|
174
|
+
const nc = p.would_create;
|
|
175
|
+
lines.push(`${w.newCollection}: ${nc.name}${nc.parent ? ` (${w.inside} ${nc.parent})` : ""}`);
|
|
72
176
|
}
|
|
73
177
|
if (tool.endsWith("attach_note") || tool.endsWith("create_note")) {
|
|
74
178
|
if (p.item || p.parent)
|
|
75
|
-
lines.push(
|
|
179
|
+
lines.push(`${w.noteOn}: ${s(p.item ?? p.parent, 90)}`);
|
|
76
180
|
if (p.summary || p.note_preview)
|
|
77
|
-
lines.push(whole(p.summary ?? p.note_preview));
|
|
181
|
+
lines.push(whole(p.summary ?? p.note_preview, w));
|
|
78
182
|
if (p.link)
|
|
79
|
-
lines.push(
|
|
183
|
+
lines.push(`${w.link}: ${p.link}`);
|
|
80
184
|
}
|
|
81
185
|
const skipped = p.skipped;
|
|
82
186
|
if (skipped && typeof skipped === "object" && Object.keys(skipped).length) {
|
|
83
|
-
lines.push(
|
|
84
|
-
lines.push(...list(Object.entries(skipped).map(([k, v]) => ` ${k}: ${s(v, 70)}`)).slice(0, 5));
|
|
187
|
+
lines.push(`${w.skipped}: ${Object.keys(skipped).length}`);
|
|
188
|
+
lines.push(...list(Object.entries(skipped).map(([k, v]) => ` ${k}: ${s(v, 70)}`), w).slice(0, 5));
|
|
85
189
|
}
|
|
86
190
|
const errors = p.errors;
|
|
87
191
|
if (errors && typeof errors === "object" && Object.keys(errors).length) {
|
|
88
|
-
lines.push(
|
|
192
|
+
lines.push(`${w.errors}: ${Object.keys(errors).length}`);
|
|
89
193
|
lines.push(...Object.entries(errors).slice(0, 5).map(([k, v]) => ` ${k}: ${s(v, 70)}`));
|
|
90
194
|
}
|
|
91
195
|
if (!lines.length)
|
package/dist/roles.js
CHANGED
|
@@ -9,6 +9,7 @@ export const ZOTERO_READ = [
|
|
|
9
9
|
"status",
|
|
10
10
|
"library_overview",
|
|
11
11
|
"find_items",
|
|
12
|
+
"standalone_items",
|
|
12
13
|
"get_item",
|
|
13
14
|
"get_fulltext",
|
|
14
15
|
"list_tags",
|
|
@@ -35,13 +36,23 @@ export const PREVIEW_WRITES = new Set([
|
|
|
35
36
|
"import_queue",
|
|
36
37
|
"repair_metadata",
|
|
37
38
|
"attach_oa_pdfs",
|
|
39
|
+
"set_parent_items",
|
|
38
40
|
].map((n) => `zotero_${n}`),
|
|
39
41
|
"scholar_attach_note",
|
|
40
42
|
]);
|
|
41
43
|
/** Writes without a dry run that still need a yes. */
|
|
42
44
|
export const CONFIRM_WRITES = new Set(["scholar_export_bibliography"]);
|
|
43
45
|
/** Zotero tools that look at the library or write only a note in the Sub-Sub folder. */
|
|
44
|
-
export const ZOTERO_CHECKS = [
|
|
46
|
+
export const ZOTERO_CHECKS = [
|
|
47
|
+
"tag_audit",
|
|
48
|
+
"audit_metadata",
|
|
49
|
+
"find_duplicates",
|
|
50
|
+
"check_retractions",
|
|
51
|
+
"missing_pdfs",
|
|
52
|
+
"write_tag_review",
|
|
53
|
+
"find_reference",
|
|
54
|
+
"web_search",
|
|
55
|
+
].map((n) => `zotero_${n}`);
|
|
45
56
|
/**
|
|
46
57
|
* The tools of a mode. The Librarian manages the library: Zotero tools and files.
|
|
47
58
|
* The Researcher has the same, plus searches, notes and reference checks. The
|