ai4l-library-dev 0.1.3 → 0.1.4

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.
Files changed (2) hide show
  1. package/dist/index.js +190 -209
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ function dotenvFile(env = process.env) {
10
10
  const lib = env.AI4L_LIBRARY_DIR?.trim() || (existsSync("/library") ? "/library" : "library");
11
11
  return join(resolve(lib), ".env");
12
12
  }
13
+ var loadedFromDotenv = /* @__PURE__ */ new Set();
13
14
  function loadDotenv(file = dotenvFile()) {
14
15
  if (!existsSync(file)) return [];
15
16
  const loaded = [];
@@ -24,81 +25,46 @@ function loadDotenv(file = dotenvFile()) {
24
25
  if (process.env[m[1]] === void 0) {
25
26
  process.env[m[1]] = v;
26
27
  loaded.push(m[1]);
28
+ loadedFromDotenv.add(m[1]);
27
29
  }
28
30
  }
29
31
  return loaded;
30
32
  }
33
+ function sourceOf(name, env = process.env) {
34
+ if (!env[name]?.trim()) return "default";
35
+ return loadedFromDotenv.has(name) ? ".env" : "environment";
36
+ }
37
+ var str = (name, env = process.env) => env[name]?.trim() ?? "";
38
+ function num(name, def, env = process.env, ok = (n) => n >= 0) {
39
+ const v = str(name, env);
40
+ if (!v) return def;
41
+ const n = Number(v);
42
+ return Number.isFinite(n) && ok(n) ? n : def;
43
+ }
44
+ function bool(name, def, env = process.env) {
45
+ const v = str(name, env).toLowerCase();
46
+ return ["true", "1", "yes", "on"].includes(v) ? true : ["false", "0", "no", "off"].includes(v) ? false : def;
47
+ }
31
48
 
32
49
  // src/index.ts
33
50
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
34
51
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
35
52
 
36
53
  // src/tools.ts
37
- import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
54
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
38
55
  import { z } from "zod";
39
56
 
40
57
  // src/entry.ts
41
- import { copyFileSync, existsSync as existsSync4, mkdirSync, readdirSync, readFileSync as readFileSync3, renameSync, rmSync, writeFileSync } from "fs";
42
- import { join as join4 } from "path";
58
+ import { copyFileSync, existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync } from "fs";
59
+ import { join as join2 } from "path";
43
60
  import { basename, dirname } from "path";
44
61
 
45
62
  // src/pubmed.ts
46
63
  import { XMLParser } from "fast-xml-parser";
47
64
 
48
- // src/contact.ts
49
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
50
- import { join as join3 } from "path";
51
-
52
- // src/layout.ts
53
- import { createHash } from "crypto";
54
- import { existsSync as existsSync2 } from "fs";
55
- import { join as join2, resolve as resolve2 } from "path";
56
- var AI4L_LIBRARY_DIR = (() => {
57
- const env = process.env.AI4L_LIBRARY_DIR;
58
- if (env && env.trim()) return resolve2(env.trim());
59
- if (existsSync2("/library")) return "/library";
60
- return resolve2("library");
61
- })();
62
- var doiSlug = (doi) => doi.toLowerCase().replace(/[^a-z0-9.-]+/g, "_");
63
- function entryDirRel(type, num) {
64
- if (type === "doi") {
65
- const h = createHash("sha1").update(num).digest("hex");
66
- return `${type}/${h.slice(0, 2)}/${h.slice(0, 4)}/${num}`;
67
- }
68
- if (!/^\d+$/.test(num)) return `${type}/${num.slice(0, 2)}/${num.slice(0, 4)}/${num}`;
69
- const l1 = num.length > 6 ? num.slice(0, -6) : "0";
70
- const l2 = num.length > 4 ? num.slice(0, -4) : "0";
71
- return `${type}/${l1}/${l2}/${num}`;
72
- }
73
- function entryPaths(libraryDir, type, num) {
74
- if (type === "doi") num = doiSlug(num);
75
- const entry = `${type}-${num}`;
76
- const entryDir = join2(libraryDir, entryDirRel(type, num));
77
- return {
78
- entry,
79
- entryDir,
80
- xml: join2(entryDir, `${entry}-abstract.xml`),
81
- md: join2(entryDir, `${entry}-abstract.md`),
82
- meta: join2(entryDir, `${entry}-meta.md`),
83
- fulltextMd: join2(entryDir, `${entry}-fulltext.md`),
84
- fulltextRaw: (format) => join2(entryDir, `${entry}-fulltext.${format}`)
85
- };
86
- }
87
-
88
65
  // src/contact.ts
89
66
  var DEFAULT = "hello@forever-healthy.org";
90
- function contactEmail() {
91
- const file = process.env.AI4L_LIBRARY_CONFIG?.trim() || join3(AI4L_LIBRARY_DIR, "config.json");
92
- let v = "";
93
- if (existsSync3(file)) {
94
- try {
95
- v = String(JSON.parse(readFileSync2(file, "utf8")).contact_email ?? "");
96
- } catch {
97
- }
98
- }
99
- v = v.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, n) => process.env[n] ?? "").trim();
100
- return v || process.env.CONTACT_EMAIL?.trim() || DEFAULT;
101
- }
67
+ var contactEmail = (env = process.env) => str("AI4L_LIBRARY_CONTACT_EMAIL", env) || DEFAULT;
102
68
 
103
69
  // src/http.ts
104
70
  var TIMEOUT_MS = 3e4;
@@ -1047,7 +1013,7 @@ ${body}
1047
1013
  }
1048
1014
  function readHeadLine(p) {
1049
1015
  try {
1050
- const lines = readFileSync3(p.md, "utf8").split("\n").filter((l) => l.trim());
1016
+ const lines = readFileSync2(p.md, "utf8").split("\n").filter((l) => l.trim());
1051
1017
  return lines[1] ?? "";
1052
1018
  } catch {
1053
1019
  return "";
@@ -1169,28 +1135,28 @@ function splitPublicationTypes(v) {
1169
1135
  }
1170
1136
  function abstractMissing(p) {
1171
1137
  try {
1172
- return readFileSync3(p.md, "utf8").includes("_No abstract available._");
1138
+ return readFileSync2(p.md, "utf8").includes("_No abstract available._");
1173
1139
  } catch {
1174
1140
  return false;
1175
1141
  }
1176
1142
  }
1177
1143
  function correctionsFromXml(p) {
1178
1144
  try {
1179
- return parsePubmedXml(readFileSync3(p.xml, "utf8"))?.corrections ?? [];
1145
+ return parsePubmedXml(readFileSync2(p.xml, "utf8"))?.corrections ?? [];
1180
1146
  } catch {
1181
1147
  return [];
1182
1148
  }
1183
1149
  }
1184
1150
  function declarationsFromXml(p) {
1185
1151
  try {
1186
- const r = parsePubmedXml(readFileSync3(p.xml, "utf8"));
1152
+ const r = parsePubmedXml(readFileSync2(p.xml, "utf8"));
1187
1153
  return { coi: r?.coi ?? null, funding: r?.funding ?? [] };
1188
1154
  } catch {
1189
1155
  return { coi: null, funding: [] };
1190
1156
  }
1191
1157
  }
1192
1158
  function readMeta(p) {
1193
- const { fm, all } = parseFrontmatter(readFileSync3(p.meta, "utf8"));
1159
+ const { fm, all } = parseFrontmatter(readFileSync2(p.meta, "utf8"));
1194
1160
  let title = fm.title ?? "";
1195
1161
  if (title.startsWith('"')) {
1196
1162
  try {
@@ -1251,12 +1217,12 @@ function copyEntry(from, to) {
1251
1217
  rmSync(tmp, { recursive: true, force: true });
1252
1218
  mkdirSync(tmp, { recursive: true });
1253
1219
  const rename = (n) => n.startsWith(from.entry) ? to.entry + n.slice(from.entry.length) : n;
1254
- for (const n of readdirSync(from.entryDir)) if (join4(from.entryDir, n) !== from.meta && !n.endsWith(".tmp")) copyFileSync(join4(from.entryDir, n), join4(tmp, rename(n)));
1220
+ for (const n of readdirSync(from.entryDir)) if (join2(from.entryDir, n) !== from.meta && !n.endsWith(".tmp")) copyFileSync(join2(from.entryDir, n), join2(tmp, rename(n)));
1255
1221
  const m = readMeta(from);
1256
1222
  const copy = { ...m, files: m.files.map(rename) };
1257
- writeFileSync(join4(tmp, basename(to.meta)), renderMeta(copy));
1223
+ writeFileSync(join2(tmp, basename(to.meta)), renderMeta(copy));
1258
1224
  mkdirSync(dirname(to.entryDir), { recursive: true });
1259
- if (existsSync4(to.entryDir)) renameSync(to.entryDir, old);
1225
+ if (existsSync2(to.entryDir)) renameSync(to.entryDir, old);
1260
1226
  renameSync(tmp, to.entryDir);
1261
1227
  rmSync(old, { recursive: true, force: true });
1262
1228
  return copy;
@@ -1276,7 +1242,7 @@ function citability(m) {
1276
1242
  return { citable: true, comment: notes.length ? notes.join("; ") : null };
1277
1243
  }
1278
1244
  function statusFromMeta(p, m, base) {
1279
- const full = existsSync4(p.fulltextMd) ? readFileSync3(p.fulltextMd, "utf8") : null;
1245
+ const full = existsSync2(p.fulltextMd) ? readFileSync2(p.fulltextMd, "utf8") : null;
1280
1246
  const cit = citability(m);
1281
1247
  if (full?.includes(`
1282
1248
  ${CORRECTED_HEADING}
@@ -1318,7 +1284,7 @@ function readStatus(p, base) {
1318
1284
  return statusFromMeta(p, readMeta(p), base);
1319
1285
  }
1320
1286
  function entryExists(p) {
1321
- return existsSync4(p.meta);
1287
+ return existsSync2(p.meta);
1322
1288
  }
1323
1289
  function writeEntry(p, r, xml, converter) {
1324
1290
  mkdirSync(p.entryDir, { recursive: true });
@@ -1401,15 +1367,15 @@ ${sentences.join("\n\n")}
1401
1367
  `;
1402
1368
  }
1403
1369
  function syncCorrectedBlock(p) {
1404
- if (!existsSync4(p.fulltextMd) || !existsSync4(p.xml)) return;
1370
+ if (!existsSync2(p.fulltextMd) || !existsSync2(p.xml)) return;
1405
1371
  let r;
1406
1372
  try {
1407
- r = parsePubmedXml(readFileSync3(p.xml, "utf8"));
1373
+ r = parsePubmedXml(readFileSync2(p.xml, "utf8"));
1408
1374
  } catch {
1409
1375
  return;
1410
1376
  }
1411
1377
  if (!r) return;
1412
- const md = readFileSync3(p.fulltextMd, "utf8");
1378
+ const md = readFileSync2(p.fulltextMd, "utf8");
1413
1379
  const next = withCorrectedBlock(md, correctedSentences(r));
1414
1380
  if (next !== md) writeAtomic(p.fulltextMd, next);
1415
1381
  }
@@ -1518,10 +1484,6 @@ function writeDoiEntry(p, r, json2, converter, abstractFrom = "crossref", record
1518
1484
  return meta;
1519
1485
  }
1520
1486
 
1521
- // src/config.ts
1522
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1523
- import { join as join7 } from "path";
1524
-
1525
1487
  // src/checks.ts
1526
1488
  var GREEK = { "\u03B1": "alpha", "\u03B2": "beta", "\xDF": "beta", "\u03B3": "gamma", "\u03B4": "delta", "\u03B5": "epsilon", "\u03B6": "zeta", "\u03B7": "eta", "\u03B8": "theta", "\u03B9": "iota", "\u03BA": "kappa", "\u03BB": "lambda", "\u03BC": "mu", "\xB5": "mu", "\u03BD": "nu", "\u03BE": "xi", "\u03C0": "pi", "\u03C1": "rho", "\u03C3": "sigma", "\u03C4": "tau", "\u03C5": "upsilon", "\u03C6": "phi", "\u03C7": "chi", "\u03C8": "psi", "\u03C9": "omega", "\u0391": "alpha", "\u0392": "beta", "\u0393": "gamma", "\u0394": "delta", "\u03A9": "omega", "\u03A3": "sigma", "\u039A": "kappa", "\u039B": "lambda", "\u03A0": "pi", "\u03A6": "phi", "\u03A8": "psi" };
1527
1489
  var greek = (s) => s.replace(/[\u0370-\u03ff\u00b5\u00df]/g, (c) => GREEK[c] ?? c);
@@ -1934,24 +1896,55 @@ function renderFulltextMd(doc, headLine2) {
1934
1896
  }
1935
1897
 
1936
1898
  // src/browser.ts
1937
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
1938
- import { join as join5 } from "path";
1899
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1900
+ import { join as join4 } from "path";
1939
1901
  import { chromium } from "playwright-core";
1940
- function browserConfig(file = CONFIG_FILE) {
1902
+
1903
+ // src/layout.ts
1904
+ import { createHash } from "crypto";
1905
+ import { existsSync as existsSync3 } from "fs";
1906
+ import { join as join3, resolve as resolve2 } from "path";
1907
+ var AI4L_LIBRARY_DIR = (() => {
1908
+ const env = process.env.AI4L_LIBRARY_DIR;
1909
+ if (env && env.trim()) return resolve2(env.trim());
1910
+ if (existsSync3("/library")) return "/library";
1911
+ return resolve2("library");
1912
+ })();
1913
+ var doiSlug = (doi) => doi.toLowerCase().replace(/[^a-z0-9.-]+/g, "_");
1914
+ function entryDirRel(type, num2) {
1915
+ if (type === "doi") {
1916
+ const h = createHash("sha1").update(num2).digest("hex");
1917
+ return `${type}/${h.slice(0, 2)}/${h.slice(0, 4)}/${num2}`;
1918
+ }
1919
+ if (!/^\d+$/.test(num2)) return `${type}/${num2.slice(0, 2)}/${num2.slice(0, 4)}/${num2}`;
1920
+ const l1 = num2.length > 6 ? num2.slice(0, -6) : "0";
1921
+ const l2 = num2.length > 4 ? num2.slice(0, -4) : "0";
1922
+ return `${type}/${l1}/${l2}/${num2}`;
1923
+ }
1924
+ function entryPaths(libraryDir, type, num2) {
1925
+ if (type === "doi") num2 = doiSlug(num2);
1926
+ const entry = `${type}-${num2}`;
1927
+ const entryDir = join3(libraryDir, entryDirRel(type, num2));
1928
+ return {
1929
+ entry,
1930
+ entryDir,
1931
+ xml: join3(entryDir, `${entry}-abstract.xml`),
1932
+ md: join3(entryDir, `${entry}-abstract.md`),
1933
+ meta: join3(entryDir, `${entry}-meta.md`),
1934
+ fulltextMd: join3(entryDir, `${entry}-fulltext.md`),
1935
+ fulltextRaw: (format) => join3(entryDir, `${entry}-fulltext.${format}`)
1936
+ };
1937
+ }
1938
+
1939
+ // src/browser.ts
1940
+ function browserConfig(env = process.env) {
1941
1941
  const out = { browsers: [], headless: "auto" };
1942
- if (!existsSync5(file)) return out;
1943
- try {
1944
- const d = JSON.parse(readFileSync4(file, "utf8")).download ?? {};
1945
- if (Array.isArray(d.browsers)) {
1946
- for (const b of d.browsers) {
1947
- if (typeof b !== "string") continue;
1948
- const v = resolveEnv(b).trim();
1949
- if (v && !/^\$\{[A-Z0-9_]+\}$/.test(v) && !out.browsers.includes(v)) out.browsers.push(v);
1950
- }
1951
- }
1952
- if (d.headless === "auto" || typeof d.headless === "boolean") out.headless = d.headless;
1953
- } catch {
1942
+ for (let i = 1; i <= 9; i++) {
1943
+ const v = str(`AI4L_LIBRARY_BROWSER_${i}`, env);
1944
+ if (v && !out.browsers.includes(v)) out.browsers.push(v);
1954
1945
  }
1946
+ const h = str("AI4L_LIBRARY_BROWSER_HEADLESS", env).toLowerCase();
1947
+ if (h === "true" || h === "false") out.headless = h === "true";
1955
1948
  return out;
1956
1949
  }
1957
1950
  var INSTALL_HINT = "npx playwright-core install chromium";
@@ -1962,7 +1955,7 @@ var rungKind = (endpoint) => isCdp(endpoint) ? "chrome" : isRemote(endpoint) ? "
1962
1955
  function localChromium() {
1963
1956
  try {
1964
1957
  const p = chromium.executablePath();
1965
- return existsSync5(p) ? p : null;
1958
+ return existsSync4(p) ? p : null;
1966
1959
  } catch {
1967
1960
  return null;
1968
1961
  }
@@ -1999,12 +1992,12 @@ var IDLE_CLOSE_MS = 12e4;
1999
1992
  var ctxs = /* @__PURE__ */ new Map();
2000
1993
  var idleTimer = null;
2001
1994
  function profileDir() {
2002
- const dir = join5(AI4L_LIBRARY_DIR, ".browser");
2003
- mkdirSync2(join5(dir, "Default"), { recursive: true });
2004
- const prefs = join5(dir, "Default", "Preferences");
1995
+ const dir = join4(AI4L_LIBRARY_DIR, ".browser");
1996
+ mkdirSync2(join4(dir, "Default"), { recursive: true });
1997
+ const prefs = join4(dir, "Default", "Preferences");
2005
1998
  let p = {};
2006
1999
  try {
2007
- p = JSON.parse(readFileSync4(prefs, "utf8"));
2000
+ p = JSON.parse(readFileSync3(prefs, "utf8"));
2008
2001
  } catch {
2009
2002
  }
2010
2003
  p.plugins = { ...p.plugins ?? {}, always_open_pdf_externally: true };
@@ -2017,9 +2010,9 @@ var LOCAL_WINDOW_ARGS = [...LAUNCH_ARGS, "--window-position=4000,4000"];
2017
2010
  var TIMEZONE = Intl.DateTimeFormat().resolvedOptions().timeZone || "Europe/Berlin";
2018
2011
  var CONTEXT_OPTS = { acceptDownloads: true, viewport: null, locale: "en-US", timezoneId: TIMEZONE };
2019
2012
  function stateFile(endpoint) {
2020
- const dir = join5(AI4L_LIBRARY_DIR, ".browser");
2013
+ const dir = join4(AI4L_LIBRARY_DIR, ".browser");
2021
2014
  mkdirSync2(dir, { recursive: true });
2022
- return join5(dir, `${browserLabel(endpoint).replace(/[^A-Za-z0-9.-]+/g, "_")}.json`);
2015
+ return join4(dir, `${browserLabel(endpoint).replace(/[^A-Za-z0-9.-]+/g, "_")}.json`);
2023
2016
  }
2024
2017
  async function context(endpoint, cfg) {
2025
2018
  let p = ctxs.get(endpoint);
@@ -2042,7 +2035,7 @@ async function context(endpoint, cfg) {
2042
2035
  });
2043
2036
  browser.on("disconnected", drop);
2044
2037
  const state = stateFile(endpoint);
2045
- const ctx2 = await browser.newContext({ ...CONTEXT_OPTS, ...existsSync5(state) ? { storageState: state } : {} });
2038
+ const ctx2 = await browser.newContext({ ...CONTEXT_OPTS, ...existsSync4(state) ? { storageState: state } : {} });
2046
2039
  return { ctx: ctx2, browser };
2047
2040
  }
2048
2041
  if (!localChromium()) throw new Error(`no local Chromium (${INSTALL_HINT})`);
@@ -2137,7 +2130,7 @@ async function browserFetch(url, endpoint, cfg = browserConfig()) {
2137
2130
  }
2138
2131
  if (download2) {
2139
2132
  const d = download2;
2140
- return { bytes: readFileSync4(await d.path()), contentType: "application/pdf", url: d.url(), via: "download" };
2133
+ return { bytes: readFileSync3(await d.path()), contentType: "application/pdf", url: d.url(), via: "download" };
2141
2134
  }
2142
2135
  const title = await page.title().catch(() => "");
2143
2136
  if (CHALLENGE.test(title)) throw new Error(`challenge not cleared (${title.slice(0, 40)})`);
@@ -2456,16 +2449,16 @@ function europepmcCandidates(r, pmcid) {
2456
2449
  }
2457
2450
 
2458
2451
  // src/tiers/inbox.ts
2459
- import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync5, unlinkSync } from "fs";
2460
- import { join as join6 } from "path";
2461
- var INBOX_DIR = join6(AI4L_LIBRARY_DIR, "inbox");
2452
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4, unlinkSync } from "fs";
2453
+ import { join as join5 } from "path";
2454
+ var INBOX_DIR = join5(AI4L_LIBRARY_DIR, "inbox");
2462
2455
  var EXT = { ".pdf": "application/pdf", ".xml": "text/xml", ".html": "text/html", ".htm": "text/html" };
2463
2456
  function findInboxFile(dir, pmid) {
2464
- if (!existsSync6(dir)) return null;
2457
+ if (!existsSync5(dir)) return null;
2465
2458
  const re = new RegExp(`(^|[^0-9])${pmid}([^0-9]|$)`);
2466
2459
  for (const name of readdirSync2(dir).sort()) {
2467
2460
  const ext = name.slice(name.lastIndexOf(".")).toLowerCase();
2468
- if (ext in EXT && re.test(name.slice(0, -ext.length))) return join6(dir, name);
2461
+ if (ext in EXT && re.test(name.slice(0, -ext.length))) return join5(dir, name);
2469
2462
  }
2470
2463
  return null;
2471
2464
  }
@@ -2479,7 +2472,7 @@ function inboxTier(dir = INBOX_DIR) {
2479
2472
  if (!file) return { ...miss("inbox", "no file"), skipped: true, ephemeral: true };
2480
2473
  const name = file.slice(file.lastIndexOf("/") + 1);
2481
2474
  const ext = name.slice(name.lastIndexOf(".")).toLowerCase();
2482
- const bytes = readFileSync5(file);
2475
+ const bytes = readFileSync4(file);
2483
2476
  const r = await convertBytes(p, bytes, EXT[ext]);
2484
2477
  if (!r.ok) return miss("inbox", `${name}: ${r.reason}`);
2485
2478
  ctx.onTry?.(`${name} ok, removed from the inbox`);
@@ -2678,74 +2671,70 @@ function urlTier(name, template, headers) {
2678
2671
  }
2679
2672
 
2680
2673
  // src/config.ts
2681
- var CONFIG_FILE = process.env.AI4L_LIBRARY_CONFIG?.trim() || join7(AI4L_LIBRARY_DIR, "config.json");
2682
- function resolveEnv(v, env = process.env) {
2683
- return v.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, n) => env[n] ?? "");
2684
- }
2685
- var resolveMap = (m) => m ? Object.fromEntries(Object.entries(m).map(([k, v]) => [k, resolveEnv(String(v))])) : void 0;
2686
- function settings(file = CONFIG_FILE) {
2687
- const out = { retry_after_days: 1, retry_blocked: false, refresh_after_days: 30 };
2688
- if (!existsSync7(file)) return out;
2689
- try {
2690
- const cfg = JSON.parse(readFileSync6(file, "utf8"));
2691
- if (typeof cfg.retry_after_days === "number" && cfg.retry_after_days >= 0) out.retry_after_days = cfg.retry_after_days;
2692
- if (typeof cfg.retry_blocked === "boolean") out.retry_blocked = cfg.retry_blocked;
2693
- if (typeof cfg.refresh_after_days === "number" && cfg.refresh_after_days >= 0) out.refresh_after_days = cfg.refresh_after_days;
2694
- } catch {
2674
+ var VARIABLES = [
2675
+ "AI4L_LIBRARY_DIR",
2676
+ "AI4L_LIBRARY_ENV",
2677
+ "AI4L_LIBRARY_TIERS",
2678
+ "AI4L_LIBRARY_CONTACT_EMAIL",
2679
+ "AI4L_LIBRARY_ELSEVIER_API_KEY",
2680
+ "AI4L_LIBRARY_WILEY_TDM_TOKEN",
2681
+ "AI4L_LIBRARY_CORE_API_KEY",
2682
+ "AI4L_LIBRARY_PROXY_URL",
2683
+ "AI4L_LIBRARY_PROXY_HEADERS",
2684
+ "AI4L_LIBRARY_BROWSER_1",
2685
+ "AI4L_LIBRARY_BROWSER_2",
2686
+ "AI4L_LIBRARY_BROWSER_3",
2687
+ "AI4L_LIBRARY_BROWSER_HEADLESS",
2688
+ "AI4L_LIBRARY_VERIFY_URL",
2689
+ "AI4L_LIBRARY_VERIFY_API_KEY",
2690
+ "AI4L_LIBRARY_VERIFY_MODEL",
2691
+ "AI4L_LIBRARY_VERIFY_THRESHOLD",
2692
+ "AI4L_LIBRARY_VERIFY_TIMEOUT_SECS",
2693
+ "AI4L_LIBRARY_RETRY_AFTER_DAYS",
2694
+ "AI4L_LIBRARY_RETRY_BLOCKED",
2695
+ "AI4L_LIBRARY_REFRESH_AFTER_DAYS",
2696
+ "NCBI_API_KEY",
2697
+ "BRIGHTDATA_API_TOKEN",
2698
+ "BRIGHTDATA_ZONE"
2699
+ ];
2700
+ var sources = (env = process.env) => Object.fromEntries(VARIABLES.map((v) => [v, sourceOf(v, env)]));
2701
+ function settings(env = process.env) {
2702
+ return {
2703
+ retry_after_days: num("AI4L_LIBRARY_RETRY_AFTER_DAYS", 1, env),
2704
+ retry_blocked: bool("AI4L_LIBRARY_RETRY_BLOCKED", false, env),
2705
+ refresh_after_days: num("AI4L_LIBRARY_REFRESH_AFTER_DAYS", 30, env)
2706
+ };
2707
+ }
2708
+ function proxyHeaders(spec) {
2709
+ const out = {};
2710
+ for (const part of spec.split(";")) {
2711
+ const i = part.indexOf(":");
2712
+ if (i < 0) continue;
2713
+ const name = part.slice(0, i).trim(), value = part.slice(i + 1).trim();
2714
+ if (name && value) out[name] = value;
2695
2715
  }
2696
2716
  return out;
2697
2717
  }
2698
- function resolveTiers(env = process.env.AI4L_LIBRARY_TIERS, file = CONFIG_FILE) {
2718
+ function resolveTiers(env = process.env) {
2699
2719
  const problems = [];
2700
- const builtin = new Map(BUILTIN.map((t) => [t.name, t]));
2701
- const known = /* @__PURE__ */ new Map();
2702
- let source = "default";
2703
- for (const t of BUILTIN) known.set(t.name, { tier: t.make({}), enabled: true, kind: "builtin" });
2704
- if (existsSync7(file)) {
2705
- source = "file";
2706
- let cfg = {};
2707
- try {
2708
- cfg = JSON.parse(readFileSync6(file, "utf8"));
2709
- } catch (e) {
2710
- problems.push(`${file}: ${e instanceof Error ? e.message : String(e)}`);
2711
- }
2712
- if (Array.isArray(cfg.tiers)) {
2713
- for (const k of known.keys()) known.get(k).enabled = false;
2714
- const ordered = /* @__PURE__ */ new Map();
2715
- for (const e of cfg.tiers) {
2716
- if (!e || typeof e.name !== "string" || !e.name.trim()) {
2717
- problems.push(`${file}: tier entry without name`);
2718
- continue;
2719
- }
2720
- const enabled = e.enabled !== false;
2721
- const description = typeof e.description === "string" ? e.description : void 0;
2722
- if (typeof e.url === "string" && e.url.trim()) ordered.set(e.name, { tier: urlTier(e.name, resolveEnv(e.url.trim()), resolveMap(e.headers)), enabled, kind: "url", description });
2723
- else if (builtin.has(e.name)) ordered.set(e.name, { tier: builtin.get(e.name).make({ tokens: resolveMap(e.tokens), api_key: typeof e.api_key === "string" ? resolveEnv(e.api_key) : void 0 }), enabled, kind: "builtin", description });
2724
- else problems.push(`${file}: unknown tier "${e.name}" (built-in: ${[...builtin.keys()].join(", ")}; a user tier needs "url")`);
2725
- }
2726
- for (const [k, v] of known) if (!ordered.has(k)) ordered.set(k, v);
2727
- known.clear();
2728
- for (const [k, v] of ordered) known.set(k, v);
2729
- }
2730
- }
2731
- if (env && env.trim()) {
2732
- source = "env";
2733
- for (const v of known.values()) v.enabled = false;
2734
- const ordered = /* @__PURE__ */ new Map();
2735
- for (const name of env.split(",").map((s) => s.trim()).filter(Boolean)) {
2736
- const v = known.get(name);
2737
- if (!v) {
2738
- problems.push(`AI4L_LIBRARY_TIERS: unknown tier "${name}"`);
2739
- continue;
2740
- }
2741
- ordered.set(name, { ...v, enabled: true });
2742
- }
2743
- for (const [k, v] of known) if (!ordered.has(k)) ordered.set(k, v);
2744
- known.clear();
2745
- for (const [k, v] of ordered) known.set(k, v);
2746
- }
2747
- const all = [...known.entries()].map(([name, v]) => ({ name, enabled: v.enabled, description: v.description ?? (v.kind === "url" ? "url tier" : "built-in") }));
2748
- return { tiers: [...known.values()].filter((v) => v.enabled).map((v) => v.tier), all, source, file, problems };
2720
+ const available = /* @__PURE__ */ new Map();
2721
+ const tokens = { elsevier: str("AI4L_LIBRARY_ELSEVIER_API_KEY", env) || void 0, wiley: str("AI4L_LIBRARY_WILEY_TDM_TOKEN", env) || void 0 };
2722
+ for (const t of BUILTIN) available.set(t.name, t.make({ tokens, api_key: str("AI4L_LIBRARY_CORE_API_KEY", env) || void 0 }));
2723
+ const proxyUrl = str("AI4L_LIBRARY_PROXY_URL", env);
2724
+ if (proxyUrl) available.set("proxy", urlTier("proxy", proxyUrl, proxyHeaders(str("AI4L_LIBRARY_PROXY_HEADERS", env))));
2725
+ const listed = str("AI4L_LIBRARY_TIERS", env);
2726
+ const order2 = listed ? listed.split(",").map((s) => s.trim()).filter(Boolean) : [...available.keys()];
2727
+ const tiers = [], seen = /* @__PURE__ */ new Set();
2728
+ for (const name of order2) {
2729
+ if (seen.has(name)) continue;
2730
+ seen.add(name);
2731
+ const t = available.get(name);
2732
+ if (t) tiers.push(t);
2733
+ else if (name === "proxy") problems.push('AI4L_LIBRARY_TIERS: "proxy" listed, but AI4L_LIBRARY_PROXY_URL is unset');
2734
+ else problems.push(`AI4L_LIBRARY_TIERS: unknown tier "${name}" (built-in: ${BUILTIN.map((t2) => t2.name).join(", ")}; proxy when AI4L_LIBRARY_PROXY_URL is set)`);
2735
+ }
2736
+ const all = [...tiers.map((t) => ({ name: t.name, enabled: true })), ...[...available.keys()].filter((n) => !seen.has(n)).map((name) => ({ name, enabled: false }))];
2737
+ return { tiers, all, source: sourceOf("AI4L_LIBRARY_TIERS", env), problems };
2749
2738
  }
2750
2739
 
2751
2740
  // src/cascade.ts
@@ -2820,34 +2809,26 @@ function parseId(raw) {
2820
2809
  }
2821
2810
 
2822
2811
  // src/info.ts
2823
- import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync } from "fs";
2824
- import { dirname as dirname2, join as join8 } from "path";
2812
+ import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync6, statSync } from "fs";
2813
+ import { dirname as dirname2, join as join6 } from "path";
2825
2814
 
2826
2815
  // src/verify.ts
2827
- import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
2816
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
2828
2817
  var DEFAULT_MODEL = "pplx-decider-flash-v1.2";
2829
2818
  var QUESTION = "Does the paper state this, in substance? Ignore section labels and definitions of terms in parentheses. Every fact and number of the statement must be stated in the paper or follow from its numbers; a rounded number counts. If any part of the statement is absent from the paper or contradicted by it, answer false.";
2830
- function verifyConfig(file = CONFIG_FILE) {
2831
- if (!existsSync8(file)) return null;
2832
- try {
2833
- const v = JSON.parse(readFileSync7(file, "utf8")).verify;
2834
- if (!v || typeof v.base_url !== "string") return null;
2835
- const base_url = resolveEnv(v.base_url).trim().replace(/\/+$/, "");
2836
- if (!base_url) return null;
2837
- const key = typeof v.api_key === "string" ? resolveEnv(v.api_key).trim() : "";
2838
- return {
2839
- base_url,
2840
- api_key: key || null,
2841
- model: typeof v.model === "string" && resolveEnv(v.model).trim() ? resolveEnv(v.model).trim() : DEFAULT_MODEL,
2842
- threshold: typeof v.threshold === "number" && v.threshold > 0 && v.threshold < 1 ? v.threshold : 0.5,
2843
- // Hosted takes 256k tokens in one request (a 50k-token paper in 2 s); the
2844
- // Mac app is limited to 32k and slows steeply beyond 25k, so it windows.
2845
- window_chars: typeof v.window_chars === "number" && v.window_chars >= 1e4 ? v.window_chars : isHosted(base_url) ? 8e5 : 9e4,
2846
- timeout_seconds: typeof v.timeout_seconds === "number" && v.timeout_seconds > 0 ? v.timeout_seconds : 60
2847
- };
2848
- } catch {
2849
- return null;
2850
- }
2819
+ function verifyConfig(env = process.env) {
2820
+ const base_url = str("AI4L_LIBRARY_VERIFY_URL", env).replace(/\/+$/, "");
2821
+ if (!base_url) return null;
2822
+ return {
2823
+ base_url,
2824
+ api_key: str("AI4L_LIBRARY_VERIFY_API_KEY", env) || null,
2825
+ model: str("AI4L_LIBRARY_VERIFY_MODEL", env) || DEFAULT_MODEL,
2826
+ threshold: num("AI4L_LIBRARY_VERIFY_THRESHOLD", 0.5, env, (n) => n > 0 && n < 1),
2827
+ // Hosted takes 256k tokens in one request (a 50k-token paper in 2 s); the
2828
+ // Mac app is limited to 32k and slows steeply beyond 25k, so it windows.
2829
+ window_chars: isHosted(base_url) ? 8e5 : 9e4,
2830
+ timeout_seconds: num("AI4L_LIBRARY_VERIFY_TIMEOUT_SECS", 60, env, (n) => n > 0)
2831
+ };
2851
2832
  }
2852
2833
  var isHosted = (baseUrl) => {
2853
2834
  try {
@@ -2933,10 +2914,10 @@ async function readPaper(raw, libraryDir = AI4L_LIBRARY_DIR) {
2933
2914
  if (p.error) return { id: p.requested, error: p.error };
2934
2915
  const paths = entryPaths(libraryDir, p.type, p.id);
2935
2916
  if (!entryExists(paths)) return { id: p.requested, error: "not in library" };
2936
- const full = existsSync8(paths.fulltextMd);
2937
- if (full) return { id: p.requested, basis: "full_text", text: readFileSync7(paths.fulltextMd, "utf8") };
2917
+ const full = existsSync6(paths.fulltextMd);
2918
+ if (full) return { id: p.requested, basis: "full_text", text: readFileSync5(paths.fulltextMd, "utf8") };
2938
2919
  const meta = readMeta(paths);
2939
- return { id: p.requested, basis: meta.has_abstract ? "abstract" : "title", text: readFileSync7(paths.md, "utf8") };
2920
+ return { id: p.requested, basis: meta.has_abstract ? "abstract" : "title", text: readFileSync5(paths.md, "utf8") };
2940
2921
  }
2941
2922
  async function verifyClaim(claim, id, cfg, decide, read = readPaper) {
2942
2923
  const out = { claim, id, verdict: "error", score: null, basis: null, windows: 0, glyphs_unresolved: 0, superseded: null, model: cfg?.model ?? null };
@@ -2950,7 +2931,7 @@ async function verifyClaim(claim, id, cfg, decide, read = readPaper) {
2950
2931
  const paper = await read(id);
2951
2932
  out.id = paper.id;
2952
2933
  if ("error" in paper) return { ...out, error: paper.error };
2953
- if (!cfg || !decide) return { ...out, error: "no verify model configured (config.json: verify)" };
2934
+ if (!cfg || !decide) return { ...out, error: "no verify model configured (AI4L_LIBRARY_VERIFY_URL)" };
2954
2935
  out.basis = paper.basis;
2955
2936
  out.glyphs_unresolved = countUnresolved(paper.text);
2956
2937
  if (paper.basis === "title") return { ...out, verdict: "na" };
@@ -2977,9 +2958,9 @@ async function verifyClaim(claim, id, cfg, decide, read = readPaper) {
2977
2958
 
2978
2959
  // src/info.ts
2979
2960
  function* metaFiles(dir) {
2980
- if (!existsSync9(dir)) return;
2961
+ if (!existsSync7(dir)) return;
2981
2962
  for (const name of readdirSync3(dir)) {
2982
- const p = join8(dir, name);
2963
+ const p = join6(dir, name);
2983
2964
  if (statSync(p).isDirectory()) yield* metaFiles(p);
2984
2965
  else if (name.endsWith("-meta.md")) yield p;
2985
2966
  }
@@ -3002,7 +2983,7 @@ function libraryInfo(libraryDir, host, withIds, tiers = resolveTiers()) {
3002
2983
  library_dir: libraryDir,
3003
2984
  host,
3004
2985
  tiers: tiers.all,
3005
- tiers_config: { source: tiers.source, file: tiers.file, problems: tiers.problems, retry_after_days: settings().retry_after_days, retry_blocked: settings().retry_blocked },
2986
+ config: { ...settings(), problems: tiers.problems, sources: sources() },
3006
2987
  download: downloadInfo(),
3007
2988
  verify: verifyInfo(),
3008
2989
  entries: 0,
@@ -3018,11 +2999,11 @@ function libraryInfo(libraryDir, host, withIds, tiers = resolveTiers()) {
3018
2999
  updates: 0
3019
3000
  };
3020
3001
  const ids = [];
3021
- for (const type of existsSync9(libraryDir) ? readdirSync3(libraryDir) : []) {
3022
- const typeDir = join8(libraryDir, type);
3002
+ for (const type of existsSync7(libraryDir) ? readdirSync3(libraryDir) : []) {
3003
+ const typeDir = join6(libraryDir, type);
3023
3004
  if (!statSync(typeDir).isDirectory()) continue;
3024
3005
  for (const meta of metaFiles(typeDir)) {
3025
- const md = readFileSync8(meta, "utf8");
3006
+ const md = readFileSync6(meta, "utf8");
3026
3007
  const raw = field(md, "level") || "unknown";
3027
3008
  const level = raw === "abstract" && field(md, "has_abstract") === "no" ? "title" : raw;
3028
3009
  info.entries++;
@@ -3034,7 +3015,7 @@ function libraryInfo(libraryDir, host, withIds, tiers = resolveTiers()) {
3034
3015
  const dir = dirname2(meta);
3035
3016
  for (const f of readdirSync3(dir)) {
3036
3017
  try {
3037
- const st = statSync(join8(dir, f));
3018
+ const st = statSync(join6(dir, f));
3038
3019
  if (st.isFile()) info.size_bytes += st.size;
3039
3020
  } catch {
3040
3021
  }
@@ -3063,7 +3044,7 @@ function noticeTypes(md, metaPath) {
3063
3044
  return out;
3064
3045
  }
3065
3046
  try {
3066
- const xml = readFileSync8(metaPath.replace(/-meta\.md$/, "-abstract.xml"), "utf8");
3047
+ const xml = readFileSync6(metaPath.replace(/-meta\.md$/, "-abstract.xml"), "utf8");
3067
3048
  for (const m of xml.matchAll(/<CommentsCorrections RefType="([A-Za-z]+)"/g)) if (XML_TYPES[m[1]]) out.add(XML_TYPES[m[1]]);
3068
3049
  } catch {
3069
3050
  }
@@ -3165,9 +3146,9 @@ var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3165
3146
  // src/log.ts
3166
3147
  import { appendFileSync, mkdirSync as mkdirSync3, renameSync as renameSync2, statSync as statSync2 } from "fs";
3167
3148
  import { hostname } from "os";
3168
- import { join as join9 } from "path";
3149
+ import { join as join7 } from "path";
3169
3150
  var HOST = hostname();
3170
- var LOG_FILE = join9(AI4L_LIBRARY_DIR, "library.log");
3151
+ var LOG_FILE = join7(AI4L_LIBRARY_DIR, "library.log");
3171
3152
  function stamp() {
3172
3153
  const d = /* @__PURE__ */ new Date();
3173
3154
  const p = (n) => String(n).padStart(2, "0");
@@ -3355,7 +3336,7 @@ function parseCsl(json2) {
3355
3336
  // src/tools.ts
3356
3337
  var pkg = (() => {
3357
3338
  try {
3358
- return JSON.parse(readFileSync9(new URL("../package.json", import.meta.url), "utf8"));
3339
+ return JSON.parse(readFileSync7(new URL("../package.json", import.meta.url), "utf8"));
3359
3340
  } catch {
3360
3341
  return { name: "ai4l-library", version: "0.0.0" };
3361
3342
  }
@@ -3367,7 +3348,7 @@ Tools:
3367
3348
  - library_status(id) \u2014 read-only: the entry's status, or level "missing".
3368
3349
  - library_read(id, section?, query?, max_chars?, max_hits?) \u2014 one small piece of a stored paper's Markdown, never the whole paper. No section/query: the outline (headings with word counts, and whether it is the full text or the abstract only). section: a heading from the outline (case-insensitive substring) \u2192 that section's text, capped at max_chars. query: search terms \u2192 the paragraphs containing all of them, each with its section, best first, at most max_hits; with no hit the answer lists the headings instead. Every full text converted from PMC/JATS XML has a "Declarations" section \u2014 funding, conflicts of interest, author notes \u2014 so section "Declarations" answers who paid and who declares what in one call; an empty answer there means the paper states nothing. Reads are read-only and may run in parallel.
3369
3350
  - library_verify(claim, id) \u2014 is one finding of a review stated by one source it cites? claim: the finding alone as a complete statement (subject, intervention, outcome, number), without its citations and without the review's framing or inference; id: one source the review cites for it \u2014 a paper, a drug label (dailymed:\u2026) or a trial record (nct:\u2026), each judged on its stored full text the same way. A finding cited to three papers is three calls with the same claim; a claim that names a source, joins two findings or is a fragment is refused. The server reads the paper (full text, else abstract) and asks a decision model; nothing of the paper enters your context. Result: verdict "pass" (score is the probability), "fail" (the full text does not state it), "na" (only the abstract is in the library and does not state it, or only the title) or "error" (claim empty, too short, a fragment, naming a source or compound; paper not in the library; no model; model failure); basis full_text, abstract or title; glyphs_unresolved, the symbols the paper's PDF font gave no character for (above 0: a fail may be the font, not the paper); superseded, true when the claim uses a figure a stored correction replaced (the verdict is kept), null without a stored correction. Call library_add on the paper first.
3370
- - library_info(ids?) \u2014 library-wide, everything the TUI shows: the library directory, the decision model and whether it answers, entries and size on disk, per type (pmid, doi, dailymed, nct) the entries with their levels (full_text, abstract, title) and retractions; entry counts by type and level, counts of retracted and not-citable entries and of entries with errata, expressions of concern and updates, the configured tiers (enabled/disabled, in order), the verify model with its endpoint and whether it answers right now (ok, ms, error); ids=true lists every entry ID.
3351
+ - library_info(ids?) \u2014 library-wide, everything the TUI shows: the library directory, the decision model and whether it answers, entries and size on disk, per type (pmid, doi, dailymed, nct) the entries with their levels (full_text, abstract, title) and retractions; entry counts by type and level, counts of retracted and not-citable entries and of entries with errata, expressions of concern and updates, the retrieval tiers in run order (and which are off), the configuration (config: retry and refresh settings, problems with AI4L_LIBRARY_TIERS, and per variable whether its value came from the environment, .env or the default \u2014 never the value), the verify model with its endpoint and whether it answers right now (ok, ms, error); ids=true lists every entry ID.
3371
3352
  - get_version() \u2014 package name, version and the resolved library directory.
3372
3353
 
3373
3354
  Every action is appended to <library>/library.log, one line per action, prefixed with the host the server runs on.
@@ -3674,8 +3655,8 @@ function registerTools(server2) {
3674
3655
  log("library_read", parsed.requested, "error missing");
3675
3656
  return out({ requested: parsed.requested, entry: paths.entry, error: "missing \u2014 call library_add first" });
3676
3657
  }
3677
- const full = existsSync10(paths.fulltextMd);
3678
- const md = readFileSync9(full ? paths.fulltextMd : paths.md, "utf8");
3658
+ const full = existsSync8(paths.fulltextMd);
3659
+ const md = readFileSync7(full ? paths.fulltextMd : paths.md, "utf8");
3679
3660
  const text3 = full ? "full_text" : "abstract";
3680
3661
  const base = { requested: parsed.requested, entry: paths.entry, text: text3 };
3681
3662
  if (section2) {
@@ -3713,7 +3694,7 @@ function registerTools(server2) {
3713
3694
  );
3714
3695
  server2.tool(
3715
3696
  "library_info",
3716
- "Library-wide summary \u2014 everything the TUI shows: the library directory and host; the decision model with its endpoint and whether it answers; the entry count and the size on disk (size, size_bytes); per entry type (types) the entries and their levels (full_text, abstract, title) and retractions; entry counts by type and level, counts of retracted and not-citable entries and of entries with errata, expressions of concern and updates, the configured retrieval tiers (enabled/disabled, in order, and where that configuration came from), the decision model behind library_verify (model, endpoint, whether it runs on this machine, and whether it answers right now: ok with the answer time, or the error), the library directory and host. ids=true also lists every entry ID.",
3697
+ "Library-wide summary \u2014 everything the TUI shows: the library directory and host; the decision model with its endpoint and whether it answers; the entry count and the size on disk (size, size_bytes); per entry type (types) the entries and their levels (full_text, abstract, title) and retractions; entry counts by type and level, counts of retracted and not-citable entries and of entries with errata, expressions of concern and updates, the retrieval tiers in run order and which are off (tiers), the configuration (config: settings, problems, and per variable the source of its value \u2014 environment, .env or default \u2014 never the value), the decision model behind library_verify (model, endpoint, whether it runs on this machine, and whether it answers right now: ok with the answer time, or the error), the library directory and host. ids=true also lists every entry ID.",
3717
3698
  { ids: z.boolean().optional().describe("Also list every entry ID (type:value).") },
3718
3699
  async ({ ids }) => {
3719
3700
  const info = libraryInfo(AI4L_LIBRARY_DIR, HOST, ids === true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai4l-library-dev",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Internal development build of ai4l-library. Not for public use.",
5
5
  "type": "module",
6
6
  "bin": {