@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 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.4";
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 = ["tag_audit", "audit_metadata", "find_duplicates", "check_retractions", "missing_pdfs", "write_tag_review"].map((n) => `zotero_${n}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiagojct/subsub",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Sub-Sub: a research assistant that works from your own Zotero library, in the browser or the terminal, built on pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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. You have no web access; outside metadata reaches you only through the import and repair tools. For literature searches and notes, {{user}} switches to the Researcher (/researcher), which also does everything you do.
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
 
@@ -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.4";
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(envFile, upsertEnv(before, { ZOTERO_VAULT: notes, ZOTERO_VOCAB: vocab, ZOTERO_ALERTS: alerts, ZOTERO_CONTACT_EMAIL: email || undefined }));
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 = ["tag_audit", "audit_metadata", "find_duplicates", "check_retractions", "missing_pdfs", "write_tag_review"].map(
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",