@tiagojct/subsub 0.12.0 → 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 +14 -0
- package/dist/config.js +1 -1
- package/dist/init.js +7 -2
- package/dist/keys.js +4 -0
- package/dist/preview.js +29 -0
- package/dist/roles.js +12 -1
- package/package.json +1 -1
- package/roles/librarian.md +13 -1
- package/roles/researcher.md +12 -0
- package/src/config.ts +1 -1
- package/src/init.ts +13 -2
- package/src/keys.ts +4 -1
- package/src/preview.ts +26 -0
- package/src/roles.ts +12 -1
- package/web/app.js +1 -0
- package/web/i18n.js +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
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
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
|
+
|
|
5
19
|
## 0.12.0 (2026-10-07)
|
|
6
20
|
|
|
7
21
|
For new users:
|
package/dist/config.js
CHANGED
|
@@ -59,7 +59,7 @@ export function uiLanguage(language) {
|
|
|
59
59
|
}
|
|
60
60
|
export const PROFILES = ["reader", "scholar", "author", "editor"];
|
|
61
61
|
/** The Zotero server release that this Sub-Sub version runs when there is no local server folder. */
|
|
62
|
-
export const SERVER_VERSION = "0.5.
|
|
62
|
+
export const SERVER_VERSION = "0.5.5";
|
|
63
63
|
/** The Starbuck release (PyPI) that this Sub-Sub version runs when there is no local Starbuck folder. */
|
|
64
64
|
export const STARBUCK_VERSION = "0.2.0";
|
|
65
65
|
export const STARBUCK_SOURCE = `starbuck==${STARBUCK_VERSION}`;
|
package/dist/init.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* yes|no, --profile, --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
|
-
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";
|
|
@@ -164,6 +164,9 @@ export async function runInit(io, flags, env = process.env, deps = {}) {
|
|
|
164
164
|
}
|
|
165
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.");
|
|
166
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();
|
|
167
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.");
|
|
168
171
|
const starbuck = await io.choose("Reference checks", [
|
|
169
172
|
{ value: "off", label: "Off" },
|
|
@@ -218,7 +221,9 @@ export async function runInit(io, flags, env = process.env, deps = {}) {
|
|
|
218
221
|
const alerts = !existsSync(alertsTarget) && existsSync(legacyAlerts) ? legacyAlerts : alertsTarget;
|
|
219
222
|
mkdirSync(dirname(envFile), { recursive: true });
|
|
220
223
|
const before = existsSync(envFile) ? readFileSync(envFile, "utf8") : "# Settings for the Sub-Sub Zotero servers (zotero-local-mcp). Written by subsub init.\n";
|
|
221
|
-
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
|
|
222
227
|
const patch = {
|
|
223
228
|
profile,
|
|
224
229
|
userName: userName || undefined,
|
package/dist/keys.js
CHANGED
|
@@ -40,6 +40,8 @@ export function saveApiKey(agentDir, provider, key) {
|
|
|
40
40
|
export function providerTrouble(text) {
|
|
41
41
|
if (!text)
|
|
42
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";
|
|
43
45
|
if (/RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|usage limit/i.test(text))
|
|
44
46
|
return "quota";
|
|
45
47
|
if (/\b(503|529)\b|UNAVAILABLE|high demand|overloaded/i.test(text))
|
|
@@ -56,12 +58,14 @@ export const TROUBLE_TEXT_PT = {
|
|
|
56
58
|
busy: "O fornecedor do modelo está sobrecarregado neste momento. Tente outra vez dentro de alguns minutos ou use outro modelo (/model).",
|
|
57
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).",
|
|
58
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).",
|
|
59
62
|
};
|
|
60
63
|
export const TROUBLE_TEXT = {
|
|
61
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).",
|
|
62
65
|
busy: "The model provider is busy at the moment. Try again in a few minutes, or use another model (/model).",
|
|
63
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).",
|
|
64
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).",
|
|
65
69
|
};
|
|
66
70
|
/**
|
|
67
71
|
* Seconds to wait before a per-minute limit lets the request through again, or undefined when the
|
package/dist/preview.js
CHANGED
|
@@ -23,6 +23,9 @@ const WORDS = {
|
|
|
23
23
|
wouldImport: (n) => `${n} item(s) would be imported:`,
|
|
24
24
|
noAbstract: "(no abstract)",
|
|
25
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",
|
|
26
29
|
newCollection: "New collection",
|
|
27
30
|
inside: "inside",
|
|
28
31
|
noteOn: "Note on",
|
|
@@ -48,6 +51,9 @@ const WORDS = {
|
|
|
48
51
|
wouldImport: (n) => `${n} item(ns) vão ser importados:`,
|
|
49
52
|
noAbstract: "(sem resumo)",
|
|
50
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",
|
|
51
57
|
newCollection: "Nova coleção",
|
|
52
58
|
inside: "dentro de",
|
|
53
59
|
noteOn: "Nota sobre",
|
|
@@ -141,6 +147,29 @@ export function formatPreview(tool, preview, lang = "en") {
|
|
|
141
147
|
lines.push(w.wouldAttach(p.would_attach.length));
|
|
142
148
|
lines.push(...list(p.would_attach.map((r) => `${s(r.item, 60)} [${r.host_type ?? "?"}, ${r.version ?? "?"}${r.license ? `, ${r.license}` : ""}]`), w));
|
|
143
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));
|
|
172
|
+
}
|
|
144
173
|
if (p.would_create && typeof p.would_create === "object") {
|
|
145
174
|
const nc = p.would_create;
|
|
146
175
|
lines.push(`${w.newCollection}: ${nc.name}${nc.parent ? ` (${w.inside} ${nc.parent})` : ""}`);
|
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
|
package/package.json
CHANGED
package/roles/librarian.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
You are Sub-Sub, the librarian for {{user}}'s Zotero library. {{about}} Reply briefly. No emojis.
|
|
2
2
|
|
|
3
|
-
You change the library through the zotero_ tools.
|
|
3
|
+
You change the library through the zotero_ tools. Outside metadata reaches you through the import and repair tools, zotero_find_reference and, when {{user}} has set it up, zotero_web_search. You do not search the literature. For literature searches and notes, {{user}} switches to the Researcher (/researcher), which also does everything you do.
|
|
4
4
|
|
|
5
5
|
## How changes work
|
|
6
6
|
|
|
@@ -8,6 +8,18 @@ You change the library through the zotero_ tools. You have no web access; outsid
|
|
|
8
8
|
- If a call is blocked because {{user}} did not approve, ask what to change. Do not retry the same call.
|
|
9
9
|
- Report the journal_id after each applied change. {{User}} can revert with /undo or ask you to use undo.
|
|
10
10
|
- Start a session with zotero_status. If Zotero is not reachable, stop and tell {{user}}.
|
|
11
|
+
- You cannot change Sub-Sub, its servers or its settings, and you do not offer to. When a tool you need is missing or fails, say in plain words what you cannot do and what {{user}} can do instead in Zotero. {{User}} can report it at https://github.com/tiagojct/subsub/issues.
|
|
12
|
+
|
|
13
|
+
## Files and notes without a parent item
|
|
14
|
+
|
|
15
|
+
PDFs, scans and notes that sit alone in the library (no parent item) are not found by zotero_find_items. To give them a proper reference:
|
|
16
|
+
|
|
17
|
+
1. zotero_standalone_items lists them (kind="attachment" for files only).
|
|
18
|
+
2. For each file, read it with zotero_get_fulltext(key): the first pages usually give title, authors, date, publication, issue and publisher. If there is no indexed text, say that {{user}} can right-click the file in Zotero and choose Reindex Item; a scan without a text layer cannot be read. A clear file name ("Weird_Tales_-_v31n02_[1938-02].pdf") can still identify the work: propose the item and say that it rests on the file name only.
|
|
19
|
+
3. If the text gives a DOI or ISBN, use it. Otherwise look the work up with zotero_find_reference (Crossref, Google Books, Internet Archive, Open Library, Wikidata; good for magazines, books and reports) and, if it is available, zotero_web_search. Put only the title, the publication and the date in a query, never other text from the file.
|
|
20
|
+
4. Accept a candidate only when it agrees with the file's own text (title, date, issue). Search results are often near misses. When nothing agrees, build the item from the file's text alone, and say which fields you could not confirm.
|
|
21
|
+
5. Call zotero_set_parent_items with up to 25 files: identifier= for a DOI, PMID or ISBN; item_type + fields + creators for anything else. A magazine article is magazineArticle (publicationTitle, date, issue, pages); a whole issue is a magazineArticle with the issue title, or a document; a newspaper article is newspaperArticle; a book is book; a report is report. Use parent_key when the work is already in the library. A person is a creator with lastName and firstName; name is only for an organisation.
|
|
22
|
+
6. The server uses an item that is already in the library instead of making a copy. Report what was new and what was existing, and the journal_id.
|
|
11
23
|
|
|
12
24
|
## How to tag
|
|
13
25
|
|
package/roles/researcher.md
CHANGED
|
@@ -22,6 +22,7 @@ You read the Zotero library (zotero_ tools), search PubMed, Europe PMC and OpenA
|
|
|
22
22
|
- If a call is blocked because {{user}} did not approve, ask what to change. Do not retry the same call.
|
|
23
23
|
- Report the journal_id after each applied change. {{User}} can revert with /undo or ask you to use undo.
|
|
24
24
|
- Start work on the library with zotero_status. If Zotero is not reachable, stop and tell {{user}}.
|
|
25
|
+
- You cannot change Sub-Sub, its servers or its settings, and you do not offer to. When a tool you need is missing or fails, say in plain words what you cannot do and what {{user}} can do instead in Zotero. {{User}} can report it at https://github.com/tiagojct/subsub/issues.
|
|
25
26
|
- Older shared rules may say that the researcher cannot change the library and hands works over to the librarian. That was before version 0.10: you do both.
|
|
26
27
|
|
|
27
28
|
## Questions about the library
|
|
@@ -44,6 +45,17 @@ You read the Zotero library (zotero_ tools), search PubMed, Europe PMC and OpenA
|
|
|
44
45
|
- zotero_check_retractions: call again while still_unchecked > 0. Propose the tag _retracted for retracted items.
|
|
45
46
|
- zotero_missing_pdfs, then zotero_attach_oa_pdfs in batches of 25. Say when a file is an accepted manuscript.
|
|
46
47
|
|
|
48
|
+
## Files and notes without a parent item
|
|
49
|
+
|
|
50
|
+
PDFs, scans and notes that sit alone in the library (no parent item) are not found by zotero_find_items. To give them a proper reference:
|
|
51
|
+
|
|
52
|
+
1. zotero_standalone_items lists them (kind="attachment" for files only).
|
|
53
|
+
2. For each file, read it with zotero_get_fulltext(key): the first pages usually give title, authors, date, publication, issue and publisher. If there is no indexed text, say that {{user}} can right-click the file in Zotero and choose Reindex Item; a scan without a text layer cannot be read. A clear file name ("Weird_Tales_-_v31n02_[1938-02].pdf") can still identify the work: propose the item and say that it rests on the file name only.
|
|
54
|
+
3. If the text gives a DOI or ISBN, use it. Otherwise look the work up with zotero_find_reference (Crossref, Google Books, Internet Archive, Open Library, Wikidata; good for magazines, books and reports) and, if it is available, zotero_web_search. Put only the title, the publication and the date in a query, never other text from the file.
|
|
55
|
+
4. Accept a candidate only when it agrees with the file's own text (title, date, issue). Search results are often near misses. When nothing agrees, build the item from the file's text alone, and say which fields you could not confirm.
|
|
56
|
+
5. Call zotero_set_parent_items with up to 25 files: identifier= for a DOI, PMID or ISBN; item_type + fields + creators for anything else. A magazine article is magazineArticle (publicationTitle, date, issue, pages); a whole issue is a magazineArticle with the issue title, or a document; a newspaper article is newspaperArticle; a book is book; a report is report. Use parent_key when the work is already in the library. A person is a creator with lastName and firstName; name is only for an organisation.
|
|
57
|
+
6. The server uses an item that is already in the library instead of making a copy. Report what was new and what was existing, and the journal_id.
|
|
58
|
+
|
|
47
59
|
## How to tag
|
|
48
60
|
|
|
49
61
|
- A tag must be supported by the item itself: title and abstract, or the full text. If you are unsure, leave the tag out. Fewer correct tags are better than many loose ones.
|
package/src/config.ts
CHANGED
|
@@ -65,7 +65,7 @@ export type Profile = "reader" | "scholar" | "author" | "editor";
|
|
|
65
65
|
export const PROFILES: Profile[] = ["reader", "scholar", "author", "editor"];
|
|
66
66
|
|
|
67
67
|
/** The Zotero server release that this Sub-Sub version runs when there is no local server folder. */
|
|
68
|
-
export const SERVER_VERSION = "0.5.
|
|
68
|
+
export const SERVER_VERSION = "0.5.5";
|
|
69
69
|
|
|
70
70
|
/** The Starbuck release (PyPI) that this Sub-Sub version runs when there is no local Starbuck folder. */
|
|
71
71
|
export const STARBUCK_VERSION = "0.2.0";
|
package/src/init.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* --email, --starbuck on|off, --models keep|later|opencode-go|mistral-eu|google-free.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
26
26
|
import { homedir } from "node:os";
|
|
27
27
|
import { dirname, join, resolve } from "node:path";
|
|
28
28
|
import { createInterface } from "node:readline/promises";
|
|
@@ -235,6 +235,13 @@ export async function runInit(
|
|
|
235
235
|
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.");
|
|
236
236
|
const email = (await io.ask("Email (optional)", flags.email ?? envValues.ZOTERO_CONTACT_EMAIL ?? "")).trim();
|
|
237
237
|
|
|
238
|
+
explain(
|
|
239
|
+
"Web search",
|
|
240
|
+
"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.",
|
|
241
|
+
);
|
|
242
|
+
const hadBrave = Boolean(envValues.BRAVE_API_KEY);
|
|
243
|
+
const brave = (await io.ask(hadBrave ? "Brave Search key (Enter keeps the saved key)" : "Brave Search key (Enter for none)", flags.brave ?? "")).trim();
|
|
244
|
+
|
|
238
245
|
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.");
|
|
239
246
|
const starbuck = await io.choose(
|
|
240
247
|
"Reference checks",
|
|
@@ -308,7 +315,11 @@ export async function runInit(
|
|
|
308
315
|
const alerts = !existsSync(alertsTarget) && existsSync(legacyAlerts) ? legacyAlerts : alertsTarget;
|
|
309
316
|
mkdirSync(dirname(envFile), { recursive: true });
|
|
310
317
|
const before = existsSync(envFile) ? readFileSync(envFile, "utf8") : "# Settings for the Sub-Sub Zotero servers (zotero-local-mcp). Written by subsub init.\n";
|
|
311
|
-
writeFileSync(
|
|
318
|
+
writeFileSync(
|
|
319
|
+
envFile,
|
|
320
|
+
upsertEnv(before, { ZOTERO_VAULT: notes, ZOTERO_VOCAB: vocab, ZOTERO_ALERTS: alerts, ZOTERO_CONTACT_EMAIL: email || undefined, BRAVE_API_KEY: brave || undefined }),
|
|
321
|
+
);
|
|
322
|
+
if (brave || hadBrave) chmodSync(envFile, 0o600); // the file holds a key
|
|
312
323
|
|
|
313
324
|
const patch: Record<string, unknown> = {
|
|
314
325
|
profile,
|
package/src/keys.ts
CHANGED
|
@@ -38,11 +38,12 @@ export function saveApiKey(agentDir: string, provider: string, key: string): voi
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
export type Trouble = "quota" | "busy" | "blocked" | "key";
|
|
41
|
+
export type Trouble = "quota" | "busy" | "blocked" | "key" | "region";
|
|
42
42
|
|
|
43
43
|
/** What a provider's error means for the user, or undefined when it is something else. */
|
|
44
44
|
export function providerTrouble(text: string | undefined): Trouble | undefined {
|
|
45
45
|
if (!text) return undefined;
|
|
46
|
+
if (/requires Global regions|not available in your region|region.{0,40}not (supported|allowed)/i.test(text)) return "region";
|
|
46
47
|
if (/RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|usage limit/i.test(text)) return "quota";
|
|
47
48
|
if (/\b(503|529)\b|UNAVAILABLE|high demand|overloaded/i.test(text)) return "busy";
|
|
48
49
|
if (/RECITATION|SAFETY|PROHIBITED_CONTENT/.test(text)) return "blocked";
|
|
@@ -56,12 +57,14 @@ export const TROUBLE_TEXT_PT: Record<Trouble, string> = {
|
|
|
56
57
|
busy: "O fornecedor do modelo está sobrecarregado neste momento. Tente outra vez dentro de alguns minutos ou use outro modelo (/model).",
|
|
57
58
|
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).",
|
|
58
59
|
key: "O fornecedor do modelo não aceitou a chave da API. Verifique-a ou introduza uma nova com /login.",
|
|
60
|
+
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).",
|
|
59
61
|
};
|
|
60
62
|
export const TROUBLE_TEXT: Record<Trouble, string> = {
|
|
61
63
|
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).",
|
|
62
64
|
busy: "The model provider is busy at the moment. Try again in a few minutes, or use another model (/model).",
|
|
63
65
|
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).",
|
|
64
66
|
key: "The model provider did not accept the API key. Check it, or enter a new one with /login.",
|
|
67
|
+
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).",
|
|
65
68
|
};
|
|
66
69
|
|
|
67
70
|
/**
|
package/src/preview.ts
CHANGED
|
@@ -27,6 +27,9 @@ const WORDS = {
|
|
|
27
27
|
wouldImport: (n: number) => `${n} item(s) would be imported:`,
|
|
28
28
|
noAbstract: "(no abstract)",
|
|
29
29
|
wouldAttach: (n: number) => `${n} PDF(s) would be attached:`,
|
|
30
|
+
wouldParent: (n: number) => `${n} file(s) or note(s) would get a parent item:`,
|
|
31
|
+
newItem: "new",
|
|
32
|
+
existingItem: "existing item",
|
|
30
33
|
newCollection: "New collection",
|
|
31
34
|
inside: "inside",
|
|
32
35
|
noteOn: "Note on",
|
|
@@ -52,6 +55,9 @@ const WORDS = {
|
|
|
52
55
|
wouldImport: (n: number) => `${n} item(ns) vão ser importados:`,
|
|
53
56
|
noAbstract: "(sem resumo)",
|
|
54
57
|
wouldAttach: (n: number) => `${n} PDF(s) vão ser anexados:`,
|
|
58
|
+
wouldParent: (n: number) => `${n} ficheiro(s) ou nota(s) vão ter um item principal:`,
|
|
59
|
+
newItem: "novo",
|
|
60
|
+
existingItem: "item existente",
|
|
55
61
|
newCollection: "Nova coleção",
|
|
56
62
|
inside: "dentro de",
|
|
57
63
|
noteOn: "Nota sobre",
|
|
@@ -158,6 +164,26 @@ export function formatPreview(tool: string, preview: unknown, lang: Lang = "en")
|
|
|
158
164
|
),
|
|
159
165
|
);
|
|
160
166
|
}
|
|
167
|
+
if (Array.isArray(p.would_set_parent)) {
|
|
168
|
+
lines.push(w.wouldParent(p.would_set_parent.length));
|
|
169
|
+
const rows: string[] = [];
|
|
170
|
+
for (const r of (p.would_set_parent as Obj[]).slice(0, 6)) {
|
|
171
|
+
rows.push(`${s(r.file, 60)}`);
|
|
172
|
+
if (r.parent === "new") {
|
|
173
|
+
const f = (r.fields as Obj | undefined) ?? {};
|
|
174
|
+
const where = ["publicationTitle", "date", "issue", "volume", "pages", "publisher", "ISBN", "DOI"]
|
|
175
|
+
.filter((k) => f[k])
|
|
176
|
+
.map((k) => `${k}: ${s(f[k], 40)}`)
|
|
177
|
+
.join(", ");
|
|
178
|
+
rows.push(` -> ${w.newItem} ${r.type} ${r.citekey}: ${s(r.item, 80)}`);
|
|
179
|
+
if (where) rows.push(` ${where}`);
|
|
180
|
+
} else {
|
|
181
|
+
rows.push(` -> ${w.existingItem} ${r.parent_key}: ${s(r.item, 70)}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
lines.push(...rows);
|
|
185
|
+
if (p.would_set_parent.length > 6) lines.push(w.more(p.would_set_parent.length - 6));
|
|
186
|
+
}
|
|
161
187
|
if (p.would_create && typeof p.would_create === "object") {
|
|
162
188
|
const nc = p.would_create as Obj;
|
|
163
189
|
lines.push(`${w.newCollection}: ${nc.name}${nc.parent ? ` (${w.inside} ${nc.parent})` : ""}`);
|
package/src/roles.ts
CHANGED
|
@@ -14,6 +14,7 @@ export const ZOTERO_READ = [
|
|
|
14
14
|
"status",
|
|
15
15
|
"library_overview",
|
|
16
16
|
"find_items",
|
|
17
|
+
"standalone_items",
|
|
17
18
|
"get_item",
|
|
18
19
|
"get_fulltext",
|
|
19
20
|
"list_tags",
|
|
@@ -41,6 +42,7 @@ export const PREVIEW_WRITES = new Set([
|
|
|
41
42
|
"import_queue",
|
|
42
43
|
"repair_metadata",
|
|
43
44
|
"attach_oa_pdfs",
|
|
45
|
+
"set_parent_items",
|
|
44
46
|
].map((n) => `zotero_${n}`),
|
|
45
47
|
"scholar_attach_note",
|
|
46
48
|
]);
|
|
@@ -49,7 +51,16 @@ export const PREVIEW_WRITES = new Set([
|
|
|
49
51
|
export const CONFIRM_WRITES = new Set(["scholar_export_bibliography"]);
|
|
50
52
|
|
|
51
53
|
/** Zotero tools that look at the library or write only a note in the Sub-Sub folder. */
|
|
52
|
-
export const ZOTERO_CHECKS = [
|
|
54
|
+
export const ZOTERO_CHECKS = [
|
|
55
|
+
"tag_audit",
|
|
56
|
+
"audit_metadata",
|
|
57
|
+
"find_duplicates",
|
|
58
|
+
"check_retractions",
|
|
59
|
+
"missing_pdfs",
|
|
60
|
+
"write_tag_review",
|
|
61
|
+
"find_reference",
|
|
62
|
+
"web_search",
|
|
63
|
+
].map(
|
|
53
64
|
(n) => `zotero_${n}`,
|
|
54
65
|
);
|
|
55
66
|
|
package/web/app.js
CHANGED
|
@@ -370,6 +370,7 @@ function scrollDown(force = false) {
|
|
|
370
370
|
|
|
371
371
|
// A provider error in plain words (keep in step with providerTrouble in src/keys.ts), with the raw text below.
|
|
372
372
|
function troubleOf(text) {
|
|
373
|
+
if (/requires Global regions|not available in your region|region.{0,40}not (supported|allowed)/i.test(text)) return "region";
|
|
373
374
|
if (/RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|usage limit/i.test(text)) return "quota";
|
|
374
375
|
if (/\b(503|529)\b|UNAVAILABLE|high demand|overloaded/i.test(text)) return "busy";
|
|
375
376
|
if (/RECITATION|SAFETY|PROHIBITED_CONTENT/.test(text)) return "blocked";
|
package/web/i18n.js
CHANGED
|
@@ -68,6 +68,7 @@ export const STRINGS = {
|
|
|
68
68
|
trouble_busy: "The model provider is busy at the moment. Try again in a few minutes, or choose another model with the model button.",
|
|
69
69
|
trouble_blocked: "The model provider stopped the reply (for example, because it repeated a source word for word). Ask again in other words, or choose another model.",
|
|
70
70
|
trouble_key: "The model provider did not accept the API key. Enter it again with the model button.",
|
|
71
|
+
trouble_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 with the model button.",
|
|
71
72
|
details: "Details",
|
|
72
73
|
undo: "Undo",
|
|
73
74
|
where_eu: "Where your data goes: servers in the European Union. Covered by the GDPR, the choice for material you would not send outside the EU.",
|
|
@@ -192,6 +193,7 @@ export const STRINGS = {
|
|
|
192
193
|
trouble_busy: "O fornecedor do modelo está sobrecarregado neste momento. Tente outra vez dentro de alguns minutos ou escolha outro modelo no botão do modelo.",
|
|
193
194
|
trouble_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 escolha outro modelo.",
|
|
194
195
|
trouble_key: "O fornecedor do modelo não aceitou a chave da API. Volte a introduzi-la no botão do modelo.",
|
|
196
|
+
trouble_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 no botão do modelo um modelo que a sua região permita.",
|
|
195
197
|
details: "Detalhes",
|
|
196
198
|
undo: "Anular",
|
|
197
199
|
where_eu: "Para onde vão os seus dados: servidores na União Europeia. Abrangidos pelo RGPD; a escolha para material que não enviaria para fora da UE.",
|
|
@@ -298,6 +300,10 @@ export const SUGGESTIONS = {
|
|
|
298
300
|
export const TOOLS = {
|
|
299
301
|
en: {
|
|
300
302
|
find_items: "search the library",
|
|
303
|
+
standalone_items: "list files without a parent item",
|
|
304
|
+
set_parent_items: "give files a parent item",
|
|
305
|
+
find_reference: "look up a reference",
|
|
306
|
+
web_search: "search the web",
|
|
301
307
|
get_item: "read an item",
|
|
302
308
|
get_fulltext: "read the full text",
|
|
303
309
|
library_overview: "library overview",
|
|
@@ -354,6 +360,10 @@ export const TOOLS = {
|
|
|
354
360
|
},
|
|
355
361
|
pt: {
|
|
356
362
|
find_items: "pesquisar na biblioteca",
|
|
363
|
+
standalone_items: "listar ficheiros sem item principal",
|
|
364
|
+
set_parent_items: "dar um item principal aos ficheiros",
|
|
365
|
+
find_reference: "procurar uma referência",
|
|
366
|
+
web_search: "pesquisar na web",
|
|
357
367
|
get_item: "ler um item",
|
|
358
368
|
get_fulltext: "ler o texto completo",
|
|
359
369
|
library_overview: "resumo da biblioteca",
|