infobroker 2026.8.24 → 2026.9.2

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/README.md CHANGED
@@ -25,7 +25,8 @@ README DESIGN:
25
25
  Structure: Hero → North Star → Quick Start → MCP Server (§3 features) →
26
26
  Skills → Providers → Configuration → How It Compares → Contribute →
27
27
  License → Spec. No other ordering. Canonical h2 headings are enforced
28
- by validate-readme.
28
+ by validate-readme. A TOC listing the canonical h2 sections follows the
29
+ Hero; validate-readme enforces TOC↔heading sync.
29
30
 
30
31
  Audience split: §2 is for developers who want to start the server.
31
32
  §3 describes what users can do with it. The Skills section (§3.5)
@@ -85,7 +86,7 @@ README DESIGN:
85
86
  document, or a changelog. The complete tool inventory lives in the
86
87
  feature taxonomy (§D of infobroker.md), which the README links to.
87
88
  Tool names appear in prose only as shorthand in backticks where a
88
- feature is introduced (e.g. `kb`), never as a bare list.
89
+ feature is introduced (e.g. `manage_kb`), never as a bare list.
89
90
 
90
91
  Validator. Rules marked "(validate-readme)" SHALL be checked by
91
92
  scripts/validate-readme.ts. Other rules are enforced by author/AI
@@ -122,6 +123,17 @@ sources to separate established facts from contested claims. Bundled
122
123
  client skills transform raw research into polished writing. Free first.
123
124
  Privacy always.
124
125
 
126
+ - [North Star](#north-star)
127
+ - [Quick Start](#quick-start)
128
+ - [MCP Server](#mcp-server)
129
+ - [Skills](#skills)
130
+ - [Providers](#providers)
131
+ - [Configuration](#configuration)
132
+ - [How It Compares](#how-it-compares)
133
+ - [Contribute](#contribute)
134
+ - [License](#license)
135
+ - [Spec](#spec)
136
+
125
137
  ## North Star
126
138
 
127
139
  Infobroker is the [Bothan Spynet](https://starwars.fandom.com/wiki/Bothan_Spynet/Legends) as a tool — a decentralized intelligence
@@ -179,7 +191,7 @@ Requirements: Node.js 20+.
179
191
 
180
192
  ## MCP Server
181
193
 
182
- Your research backend. Six tools, twenty providers, one
194
+ Your research backend. Seven tools, twenty providers, one
183
195
  corroboration engine. The complete feature inventory is documented in the
184
196
  [feature taxonomy](infobroker.md#d-appendix-feature-taxonomy) in the
185
197
  spec.
@@ -188,26 +200,43 @@ spec.
188
200
 
189
201
  > "Search for the location of the second Death Star."
190
202
  > "Find scholarly papers on hyperspace travel theories."
203
+ > "Search the latest astromech specs and show me the passages that answer: does the R2 unit pre-date the Clone Wars?"
191
204
 
192
205
  `web_search` sends one query to every provider that can answer it. Search
193
206
  across DuckDuckGo, Wikipedia, academic databases, news, code repositories —
194
- or describe your task and the server picks the best source. Failed providers fall
195
- back silently through a configurable chain so you get results, not
196
- error messages. Other search tools lock you to one engine; Infobroker
197
- routes every query to the right provider and keeps going when one
207
+ or describe your task and the server picks the best source. Pass an array of
208
+ queries to batch several searches in one call. Ask for a deep read and it
209
+ fetches the top results and ranks each page's passages against your query,
210
+ so you get the specific text that answers the question instead of links.
211
+ Failed providers fall back silently through a configurable chain so you get
212
+ results, not error messages. Other search tools lock you to one engine;
213
+ Infobroker routes every query to the right provider and keeps going when one
198
214
  fails.
199
215
 
200
216
  ### Content Extraction
201
217
 
202
218
  > "Fetch the article on the Battle of Yavin and summarize it."
203
219
  > "Get the text of that page about the Death Star plans."
220
+ > "Where, in that report, does it mention the reactor core?"
204
221
 
205
222
  `fetch_page` hands any URL to Jina Reader, which renders it as clean
206
223
  Markdown optimized for LLM consumption. Falls back to native HTTP when
207
224
  Jina is throttled. Wikipedia and Internet Archive have dedicated
208
- renderers for source-specific extraction. Built-in web fetchers return
209
- raw HTML; Infobroker gives you clean, readable content from any source —
210
- ready for summarization or analysis.
225
+ renderers for source-specific extraction. Ask a page a question — pass
226
+ `question` to `fetch_page` and it returns the passages that answer it, each
227
+ scored and ranked, instead of the whole document. Built-in web fetchers
228
+ return raw HTML; Infobroker gives you clean, readable content from any
229
+ source — ready for summarization or analysis. Fetch also reports the page's
230
+ last-updated date when it can determine one, so you know how current your
231
+ source is.
232
+
233
+ ### Citations
234
+
235
+ > "Give me BibTeX references for papers on hyperdrive field dynamics."
236
+
237
+ `get_citations` searches scholarly sources and returns each reference as a formatted
238
+ BibTeX entry with its fields — title, authors, year, venue, and URL — ready
239
+ to paste into a reference list.
211
240
 
212
241
  ### Provider Intelligence
213
242
 
@@ -217,7 +246,7 @@ ready for summarization or analysis.
217
246
  The server knows its own capabilities. `web_search` auto-selects the
218
247
  best backend for your task, weighing capability, quota, and latency —
219
248
  or routes by your intent when you ask for privacy, speed, or free-only
220
- sources. `providers` surfaces every configured source and drills into a
249
+ sources. `inspect_providers` surfaces every configured source and drills into a
221
250
  single provider's uptime and error history. No other search MCP server
222
251
  gives you operational visibility into every backend.
223
252
 
@@ -226,16 +255,19 @@ gives you operational visibility into every backend.
226
255
  > "Verify whether the Empire really destroyed Alderaan."
227
256
  > "Find the consensus on who fired first — Han or Greedo."
228
257
 
229
- `corroborate` runs a multi-pass truth-finding loop: broad
230
- search across active providers, claim extraction, cross-source
231
- reconciliation, and targeted follow-up for gaps. Claims corroborated
232
- across independent sources score high confidence, weighted by each
233
- source's authority; every source is bound to the claim it supports.
234
- Contradictions are surfaced with all perspectives. Gaps trigger refined
235
- queries. You get a structured report — confirmed, contested, and
236
- unverified findings — with source provenance, per-source claims, and
237
- confidence scores. Every other search tool returns a list of links;
238
- Infobroker finds the truth and tells you how sure it is.
258
+ `verify_claims` runs a multi-pass truth-finding loop: broad
259
+ search across all of your active providers — search engines,
260
+ encyclopedias, and scholarly indexes alike — then claim extraction,
261
+ cross-source reconciliation, and targeted follow-up for gaps, dispatched
262
+ in parallel. Claims corroborated across independent sources score high
263
+ confidence, weighted by each source's authority; every source is bound
264
+ to the claim it supports. Contradictions are surfaced with all
265
+ perspectives. Gaps trigger refined queries. It also remembers: prior
266
+ findings in your knowledge base participate as corroborating sources
267
+ before it queries the network. You get a structured report — confirmed,
268
+ contested, and unverified findings — with source provenance, per-source
269
+ claims, and confidence scores. Every other search tool returns a list of
270
+ links; Infobroker finds the truth and tells you how sure it is.
239
271
 
240
272
  ### Knowledge Base
241
273
 
@@ -243,14 +275,16 @@ Infobroker finds the truth and tells you how sure it is.
243
275
  > "Ingest this article so it's cached for next time."
244
276
 
245
277
  Every search, fetch, and corroboration run is cached in a local knowledge
246
- base. `kb` checks the cache before hitting external providers — only
278
+ base. `manage_kb` checks the cache before hitting external providers — only
247
279
  falling back to the network when the cached results aren't fresh enough
248
280
  or relevant enough. Its actions ingest new text or a URL by hand, report
249
281
  what's cached, and remove content. Content is age-scored, expired on a
250
- freshness schedule, and deduplicated by source. Beyond the cache, `kb`
282
+ freshness schedule, and deduplicated by source. Beyond the cache, `manage_kb`
251
283
  archives the reports you generate: ingest with `source_type: "report"`
252
- (and default to the knowledge base) and revisit them with `kb` list and
253
- `kb` get, or write them to a local directory instead. Other search MCP
284
+ (and default to the knowledge base) and revisit them with `manage_kb` list and
285
+ `manage_kb` get, or write them to a local directory instead. Each archived report
286
+ records its source's last-updated date, so you can compare it against the
287
+ live source and refresh only what has actually changed. Other search MCP
254
288
  servers re-fetch the same facts every session; Infobroker remembers and
255
289
  reuses what it already found.
256
290
 
@@ -277,7 +311,7 @@ Quota counters persist to disk and survive restarts. Rate limits are
277
311
  enforced per-provider, not globally. Configuration is hot-reloadable
278
312
  via `reload_config` — change providers, adjust chains, or tweak
279
313
  thresholds without dropping connections. `web_search` doubles as
280
- DuckDuckGo query autocomplete. `providers` reports the server's build
314
+ DuckDuckGo query autocomplete. `inspect_providers` reports the server's build
281
315
  health and request stats. You always know what your search server is
282
316
  doing and how much capacity remains.
283
317
 
@@ -301,10 +335,10 @@ polishes language, and `translation` produces multilingual output.
301
335
  Gated analysis is the escalation shape. When a question is high-stakes or
302
336
  decision-driving, the classify gate routes to the `analysis-loop` skill —
303
337
  a disciplined path with confidence-scored findings, source-reliability
304
- grading, and structured analytic techniques — rather than the lighter
305
- research-and-write route. It shares the same primitives and Infobroker
306
- tools but runs its own gated workflow, so you get the rigor without
307
- leaving the pipeline.
338
+ grading, and structured analytic techniques chosen by fit and named with a
339
+ rationale — rather than the lighter research-and-write route. It shares the
340
+ same primitives and Infobroker tools but runs its own gated workflow, so you
341
+ get the rigor without leaving the pipeline.
308
342
 
309
343
  A single instruction file, `search-preferences.md`, routes your client
310
344
  toward these tools: the knowledge base first, external providers only
@@ -421,7 +455,7 @@ passphrase means the store is unrecoverable by design). Second, the server
421
455
  never writes a partial file: every save is atomic, and an unrecognized or
422
456
  newer store format is never overwritten.
423
457
 
424
- The `kb` tool's `encryption` action is the day-to-day surface for this
458
+ The `manage_kb` tool's `encryption` action is the day-to-day surface for this
425
459
  journey, and it never echoes secret material — `generate_key` and `backup`
426
460
  return file paths, and `rekey` reads a key file rather than a raw key.
427
461
 
@@ -437,8 +471,8 @@ move to a new key without losing content. After re-keying, point
437
471
  confirm the new key opens the store.
438
472
 
439
473
  ```
440
- infobroker_kb action=encryption operation=generate_key key_file=~/.config/infobroker/kb.key
441
- infobroker_kb action=encryption operation=backup key_file=~/.backup/kb.key.bak
474
+ infobroker_manage_kb action=encryption operation=generate_key key_file=~/.config/infobroker/kb.key
475
+ infobroker_manage_kb action=encryption operation=backup key_file=~/.backup/kb.key.bak
442
476
  infobroker_reload_config
443
477
  ```
444
478
 
@@ -481,7 +515,7 @@ override and call `reload_config` to use it immediately.
481
515
  Two optional keys tune per-provider behavior in `config.json`:
482
516
 
483
517
  - `degraded_latency_ms` — a provider whose recent average latency exceeds
484
- this many milliseconds is reported `degraded` by the `providers` health
518
+ this many milliseconds is reported `degraded` by the `inspect_providers` health
485
519
  action, even while reachable. A global `output.degraded_latency_ms` acts
486
520
  as the fallback when a provider omits its own.
487
521
  - `resells` — set `true` on aggregator/reseller backends (search engines
@@ -491,6 +525,19 @@ Two optional keys tune per-provider behavior in `config.json`:
491
525
  sources (Wikipedia, arXiv) leave it empty because the page is the
492
526
  origin.
493
527
 
528
+ ### Hedged fallback
529
+
530
+ `web_search` and `fetch_page` fall back with a hedge instead of waiting
531
+ out a slow provider's full timeout: the primary (first-choice) provider
532
+ runs alone for a latency-derived window, then the remaining providers
533
+ race and the first result wins. The common path uses one provider call;
534
+ the hedge fires only when the primary is slow or failing. `fetch_page`
535
+ additionally prefers the primary renderer in a short grace window so a
536
+ marginally slow `jina` is not displaced by a lower-quality `native_fetch`.
537
+ Tune the window with `output.hedge_enabled`, `hedge_min_delay_ms`,
538
+ `hedge_max_delay_ms`, and `hedge_grace_ms`; set `hedge_enabled` to
539
+ `false` for the sequential chain.
540
+
494
541
  ## How It Compares
495
542
 
496
543
  | Tool name | What you're used to | How Infobroker differs |
@@ -509,7 +556,7 @@ contradiction, and gaps. The bundled skills close the loop from raw
509
556
  research to finished writing. One server. Every source. Research that
510
557
  delivers.
511
558
 
512
- Last updated: 2026-08-24.
559
+ Last updated: 2026-09-02.
513
560
 
514
561
  ## Contribute
515
562
 
@@ -539,7 +586,7 @@ terms and API keys.
539
586
  ## Spec
540
587
 
541
588
  The server is built from a single source specification, `infobroker.md`
542
- (v2026.08.24), which defines every requirement and the gates that verify it.
589
+ (v2026.09.02), which defines every requirement and the gates that verify it.
543
590
  Each requirement traces to an implementation file, and `npm run check`
544
591
  reconciles the code, the spec, and this README so what is documented is what
545
592
  the server actually delivers.
@@ -0,0 +1,34 @@
1
+ export declare const MAX_BATCH_INPUTS = 5;
2
+ export declare function capInputs(items: string[]): string[];
3
+ interface EnvelopeOk {
4
+ status: "ok";
5
+ provider: string;
6
+ results: Array<{
7
+ url?: string;
8
+ [k: string]: unknown;
9
+ }>;
10
+ meta?: Record<string, unknown>;
11
+ truncated?: boolean;
12
+ output_path?: string;
13
+ }
14
+ interface EnvelopeError {
15
+ status: "error";
16
+ provider: string;
17
+ error?: unknown;
18
+ }
19
+ type Envelope = EnvelopeOk | EnvelopeError;
20
+ export declare function parseEnvelope(text: string): Envelope;
21
+ export interface PerInputMeta {
22
+ query: string;
23
+ provider: string;
24
+ status: "ok" | "error";
25
+ result_count: number;
26
+ truncated?: boolean;
27
+ output_path?: string;
28
+ }
29
+ export interface BatchItem {
30
+ query: string;
31
+ envelope: string;
32
+ }
33
+ export declare function mergeItems(items: BatchItem[]): Envelope;
34
+ export {};
package/dist/batch.js ADDED
@@ -0,0 +1,74 @@
1
+ // @implements REQ-020 REQ-021
2
+ export const MAX_BATCH_INPUTS = 5;
3
+ export function capInputs(items) {
4
+ return items.slice(0, MAX_BATCH_INPUTS);
5
+ }
6
+ export function parseEnvelope(text) {
7
+ const t = text.trim();
8
+ if (t.startsWith("[OK]")) {
9
+ return JSON.parse(t.slice(4));
10
+ }
11
+ if (t.startsWith("[ERROR]")) {
12
+ return JSON.parse(t.slice(7));
13
+ }
14
+ throw new Error("unrecognized envelope prefix");
15
+ }
16
+ function dedupeByUrl(results) {
17
+ const seen = new Set();
18
+ const out = [];
19
+ for (const r of results) {
20
+ const key = r.url || "";
21
+ if (key && seen.has(key))
22
+ continue;
23
+ if (key)
24
+ seen.add(key);
25
+ out.push(r);
26
+ }
27
+ return out;
28
+ }
29
+ // Merge per-input envelopes into a single envelope. Results are flattened in
30
+ // input order and URL-deduplicated; per-input provenance is recorded in meta.
31
+ export function mergeItems(items) {
32
+ const perQuery = [];
33
+ const okItems = [];
34
+ let firstError;
35
+ for (const item of items) {
36
+ let env;
37
+ try {
38
+ env = parseEnvelope(item.envelope);
39
+ }
40
+ catch {
41
+ env = { status: "error", provider: "system", error: { code: "internal_error", message: "batch item failed to parse" } };
42
+ }
43
+ if (env.status === "ok") {
44
+ okItems.push({ query: item.query, env });
45
+ perQuery.push({
46
+ query: item.query,
47
+ provider: env.provider,
48
+ status: "ok",
49
+ result_count: env.results.length,
50
+ ...(env.truncated ? { truncated: true, output_path: env.output_path } : {}),
51
+ });
52
+ }
53
+ else {
54
+ if (!firstError)
55
+ firstError = env;
56
+ perQuery.push({ query: item.query, provider: env.provider, status: "error", result_count: 0 });
57
+ }
58
+ }
59
+ if (okItems.length === 0) {
60
+ return firstError ?? { status: "error", provider: "none", error: { code: "internal_error", message: "no results" } };
61
+ }
62
+ const firstOk = okItems[0].env;
63
+ const results = dedupeByUrl(okItems.flatMap((o) => o.env.results));
64
+ return {
65
+ status: "ok",
66
+ provider: firstOk.provider,
67
+ results,
68
+ meta: {
69
+ ...(firstOk.meta ?? {}),
70
+ per_query: perQuery,
71
+ },
72
+ };
73
+ }
74
+ //# sourceMappingURL=batch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batch.js","sourceRoot":"","sources":["../src/batch.ts"],"names":[],"mappings":"AAAA,8BAA8B;AAC9B,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAElC,MAAM,UAAU,SAAS,CAAC,KAAe;IACvC,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAAC;AAC1C,CAAC;AAmBD,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IACtB,IAAI,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAe,CAAC;IAC9C,CAAC;IACD,IAAI,CAAC,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC5B,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAkB,CAAC;IACjD,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;AAClD,CAAC;AAgBD,SAAS,WAAW,CAAC,OAAsD;IACzE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,GAAG,GAAkD,EAAE,CAAC;IAC9D,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,CAAC,CAAC,GAAG,IAAI,EAAE,CAAC;QACxB,IAAI,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAS;QACnC,IAAI,GAAG;YAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACvB,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACd,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,6EAA6E;AAC7E,8EAA8E;AAC9E,MAAM,UAAU,UAAU,CAAC,KAAkB;IAC3C,MAAM,QAAQ,GAAmB,EAAE,CAAC;IACpC,MAAM,OAAO,GAA8C,EAAE,CAAC;IAC9D,IAAI,UAAqC,CAAC;IAE1C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,GAAa,CAAC;QAClB,IAAI,CAAC;YACH,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrC,CAAC;QAAC,MAAM,CAAC;YACP,GAAG,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,4BAA4B,EAAE,EAAE,CAAC;QAC1H,CAAC;QACD,IAAI,GAAG,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;YACxB,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;YACzC,QAAQ,CAAC,IAAI,CAAC;gBACZ,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,QAAQ,EAAE,GAAG,CAAC,QAAQ;gBACtB,MAAM,EAAE,IAAI;gBACZ,YAAY,EAAE,GAAG,CAAC,OAAO,CAAC,MAAM;gBAChC,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC5E,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,UAAU;gBAAE,UAAU,GAAG,GAAG,CAAC;YAClC,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC,CAAC;QACjG,CAAC;IACH,CAAC;IAED,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,UAAU,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,CAAC;IACvH,CAAC;IAED,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IAC/B,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;IACnE,OAAO;QACL,MAAM,EAAE,IAAI;QACZ,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,OAAO;QACP,IAAI,EAAE;YACJ,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC;YACvB,SAAS,EAAE,QAAQ;SACpB;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,11 @@
1
+ export interface CitationFields {
2
+ type: "article" | "misc";
3
+ key: string;
4
+ title: string;
5
+ authors: string[];
6
+ year?: string;
7
+ venue?: string;
8
+ url?: string;
9
+ }
10
+ export declare function formatBibtex(fields: CitationFields): string;
11
+ export declare function citationFor(title: string, authors: string[], year?: string, venue?: string, url?: string): string;
package/dist/bibtex.js ADDED
@@ -0,0 +1,44 @@
1
+ function escapeLatex(text) {
2
+ return text
3
+ .replace(/\\/g, "\\textbackslash{}")
4
+ .replace(/[{}]/g, (c) => (c === "{" ? "\\{" : "\\}"))
5
+ .replace(/([&%$#_])/g, "\\$1")
6
+ .replace(/~/g, "\\textasciitilde{}")
7
+ .replace(/\^/g, "\\textasciicircum{}");
8
+ }
9
+ function citationKey(author, year, title) {
10
+ const last = (author || "").split(/[,\s]+/)[0] || "anon";
11
+ const slug = last.toLowerCase().replace(/[^a-z0-9]/g, "");
12
+ const y = year || "nd";
13
+ return `${slug}${y}`;
14
+ }
15
+ export function formatBibtex(fields) {
16
+ const authorField = fields.authors.length > 0
17
+ ? fields.authors.map((a) => escapeLatex(a)).join(" and ")
18
+ : undefined;
19
+ const lines = [`@${fields.type}{${escapeLatex(fields.key)},`];
20
+ lines.push(` title = {${escapeLatex(fields.title)}}`);
21
+ if (authorField)
22
+ lines.push(` author = {${authorField}}`);
23
+ if (fields.year)
24
+ lines.push(` year = {${fields.year}}`);
25
+ if (fields.venue)
26
+ lines.push(fields.type === "article" ? ` journal = {${escapeLatex(fields.venue)}}` : ` howpublished = {${escapeLatex(fields.venue)}}`);
27
+ if (fields.url)
28
+ lines.push(` url = {${fields.url}}`);
29
+ lines.push("}");
30
+ return lines.join("\n");
31
+ }
32
+ export function citationFor(title, authors, year, venue, url) {
33
+ const key = citationKey(authors[0], year || undefined, title);
34
+ return formatBibtex({
35
+ type: authors.length > 0 ? "article" : "misc",
36
+ key,
37
+ title,
38
+ authors,
39
+ year,
40
+ venue,
41
+ url,
42
+ });
43
+ }
44
+ //# sourceMappingURL=bibtex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bibtex.js","sourceRoot":"","sources":["../src/bibtex.ts"],"names":[],"mappings":"AAWA,SAAS,WAAW,CAAC,IAAY;IAC/B,OAAO,IAAI;SACR,OAAO,CAAC,KAAK,EAAE,mBAAmB,CAAC;SACnC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;SACpD,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC;SAC7B,OAAO,CAAC,IAAI,EAAE,oBAAoB,CAAC;SACnC,OAAO,CAAC,KAAK,EAAE,qBAAqB,CAAC,CAAC;AAC3C,CAAC;AAED,SAAS,WAAW,CAAC,MAAe,EAAE,IAAa,EAAE,KAAc;IACjE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC;IACzD,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;IAC1D,MAAM,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC;IACvB,OAAO,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,MAAsB;IACjD,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC;QAC3C,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC;QACzD,CAAC,CAAC,SAAS,CAAC;IAEd,MAAM,KAAK,GAAa,CAAC,IAAI,MAAM,CAAC,IAAI,IAAI,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACxE,KAAK,CAAC,IAAI,CAAC,cAAc,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACvD,IAAI,WAAW;QAAE,KAAK,CAAC,IAAI,CAAC,eAAe,WAAW,GAAG,CAAC,CAAC;IAC3D,IAAI,MAAM,CAAC,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,aAAa,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC;IACzD,IAAI,MAAM,CAAC,KAAK;QAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,gBAAgB,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,qBAAqB,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3J,IAAI,MAAM,CAAC,GAAG;QAAE,KAAK,CAAC,IAAI,CAAC,YAAY,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC;IACtD,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAChB,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAa,EAAE,OAAiB,EAAE,IAAa,EAAE,KAAc,EAAE,GAAY;IACvG,MAAM,GAAG,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,SAAS,EAAE,KAAK,CAAC,CAAC;IAC9D,OAAO,YAAY,CAAC;QAClB,IAAI,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM;QAC7C,GAAG;QACH,KAAK;QACL,OAAO;QACP,IAAI;QACJ,KAAK;QACL,GAAG;KACJ,CAAC,CAAC;AACL,CAAC"}
package/dist/cite.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export interface CitationResult {
2
+ title: string;
3
+ url: string;
4
+ year?: string;
5
+ authors: string[];
6
+ venue?: string;
7
+ bibtex: string;
8
+ }
9
+ export declare function searchCitations(query: string, limit?: number, source?: string): Promise<CitationResult[]>;
package/dist/cite.js ADDED
@@ -0,0 +1,85 @@
1
+ // @implements REQ-027
2
+ import { infobrokerFetch } from "./http.js";
3
+ import { getEnvVar } from "./config.js";
4
+ import { citationFor } from "./bibtex.js";
5
+ const ARXIV_API = "https://export.arxiv.org/api/query";
6
+ const SS_API = "https://api.semanticscholar.org/graph/v1/paper/search";
7
+ function tagContent(xml, tag) {
8
+ const re = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, "i");
9
+ const m = xml.match(re);
10
+ return m ? (m[1] || "").replace(/<[^>]*>/g, "").trim() : "";
11
+ }
12
+ async function searchArxiv(query, limit) {
13
+ const params = new URLSearchParams({ search_query: query, max_results: String(limit) });
14
+ const resp = await infobrokerFetch(`${ARXIV_API}?${params.toString()}`, { providerSlug: "arxiv" });
15
+ if (!resp.ok)
16
+ return [];
17
+ const xml = await resp.text();
18
+ const out = [];
19
+ const entryRe = /<entry>([\s\S]*?)<\/entry>/g;
20
+ let m;
21
+ while ((m = entryRe.exec(xml)) !== null) {
22
+ const entry = m[1];
23
+ const title = tagContent(entry, "title");
24
+ const id = tagContent(entry, "id");
25
+ const published = tagContent(entry, "published");
26
+ const year = published.slice(0, 4);
27
+ const authors = [...entry.matchAll(/<name>([\s\S]*?)<\/name>/g)].map((a) => a[1].trim()).filter(Boolean);
28
+ if (!title || !id)
29
+ continue;
30
+ out.push({
31
+ title,
32
+ url: id,
33
+ year,
34
+ authors,
35
+ venue: "arXiv",
36
+ bibtex: citationFor(title, authors, year, "arXiv", id),
37
+ });
38
+ }
39
+ return out;
40
+ }
41
+ async function searchSemanticScholar(query, limit) {
42
+ const fields = ["title", "authors", "year", "venue", "externalIds", "url"].join(",");
43
+ const params = new URLSearchParams({ query, limit: String(limit), fields });
44
+ const headers = {};
45
+ const key = getEnvVar("semantic_scholar", "_API_KEY");
46
+ if (key)
47
+ headers["x-api-key"] = key;
48
+ const resp = await infobrokerFetch(`${SS_API}?${params.toString()}`, { providerSlug: "semantic_scholar", headers });
49
+ if (!resp.ok)
50
+ return [];
51
+ const data = (await resp.json());
52
+ return (data.data || []).map((item) => {
53
+ const year = item.year ? String(item.year) : undefined;
54
+ const authors = (item.authors || []).map((a) => a.name);
55
+ return {
56
+ title: item.title,
57
+ url: item.url || `https://api.semanticscholar.org/paper/${item.title}`,
58
+ year,
59
+ authors,
60
+ venue: item.venue,
61
+ bibtex: citationFor(item.title, authors, year, item.venue, undefined),
62
+ };
63
+ });
64
+ }
65
+ export async function searchCitations(query, limit = 8, source) {
66
+ const results = [];
67
+ const wanted = source ? [source] : ["arxiv", "semantic_scholar"];
68
+ for (const s of wanted) {
69
+ if (results.length >= limit)
70
+ break;
71
+ try {
72
+ const batch = s === "arxiv" ? await searchArxiv(query, limit) : await searchSemanticScholar(query, limit);
73
+ for (const r of batch) {
74
+ if (results.length >= limit)
75
+ break;
76
+ results.push(r);
77
+ }
78
+ }
79
+ catch {
80
+ // source unavailable — continue to the next
81
+ }
82
+ }
83
+ return results;
84
+ }
85
+ //# sourceMappingURL=cite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cite.js","sourceRoot":"","sources":["../src/cite.ts"],"names":[],"mappings":"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"}
package/dist/config.js CHANGED
@@ -152,6 +152,9 @@ function validateConfig(config) {
152
152
  }
153
153
  }
154
154
  }
155
+ if (config.corroboration?.kb_recall !== undefined && typeof config.corroboration.kb_recall !== "boolean") {
156
+ errors.push("corroboration.kb_recall must be a boolean");
157
+ }
155
158
  if (typeof config.output.fallback_depth !== "number" || config.output.fallback_depth < 1) {
156
159
  errors.push("output.fallback_depth must be a positive number");
157
160
  }
@@ -161,6 +164,54 @@ function validateConfig(config) {
161
164
  if (config.output.degraded_latency_ms !== undefined && (typeof config.output.degraded_latency_ms !== "number" || config.output.degraded_latency_ms < 0)) {
162
165
  errors.push("output.degraded_latency_ms must be a non-negative number");
163
166
  }
167
+ if (config.output.hedge_enabled !== undefined && typeof config.output.hedge_enabled !== "boolean") {
168
+ errors.push("output.hedge_enabled must be a boolean");
169
+ }
170
+ if (config.output.hedge_min_delay_ms !== undefined && (typeof config.output.hedge_min_delay_ms !== "number" || config.output.hedge_min_delay_ms < 0)) {
171
+ errors.push("output.hedge_min_delay_ms must be a non-negative number");
172
+ }
173
+ if (config.output.hedge_max_delay_ms !== undefined && (typeof config.output.hedge_max_delay_ms !== "number" || config.output.hedge_max_delay_ms < config.output.hedge_min_delay_ms)) {
174
+ errors.push("output.hedge_max_delay_ms must be a number no less than hedge_min_delay_ms");
175
+ }
176
+ if (config.output.hedge_grace_ms !== undefined && (typeof config.output.hedge_grace_ms !== "number" || config.output.hedge_grace_ms < 0)) {
177
+ errors.push("output.hedge_grace_ms must be a non-negative number");
178
+ }
179
+ if (config.fetch) {
180
+ if (config.fetch.passage_size !== undefined && (typeof config.fetch.passage_size !== "number" || config.fetch.passage_size < 1)) {
181
+ errors.push("fetch.passage_size must be a positive number");
182
+ }
183
+ if (config.fetch.max_passages !== undefined && (typeof config.fetch.max_passages !== "number" || config.fetch.max_passages < 1)) {
184
+ errors.push("fetch.max_passages must be a positive number");
185
+ }
186
+ if (config.fetch.detect_date !== undefined && typeof config.fetch.detect_date !== "boolean") {
187
+ errors.push("fetch.detect_date must be a boolean");
188
+ }
189
+ }
190
+ if (config.expand) {
191
+ if (config.expand.max_expansions !== undefined && (typeof config.expand.max_expansions !== "number" || config.expand.max_expansions < 1)) {
192
+ errors.push("expand.max_expansions must be a positive number");
193
+ }
194
+ }
195
+ if (config.deep) {
196
+ if (config.deep.max_pages !== undefined && (typeof config.deep.max_pages !== "number" || config.deep.max_pages < 1)) {
197
+ errors.push("deep.max_pages must be a positive number");
198
+ }
199
+ if (config.deep.max_total_pages !== undefined && (typeof config.deep.max_total_pages !== "number" || config.deep.max_total_pages < 1)) {
200
+ errors.push("deep.max_total_pages must be a positive number");
201
+ }
202
+ if (config.deep.concurrency !== undefined && (typeof config.deep.concurrency !== "number" || config.deep.concurrency < 1)) {
203
+ errors.push("deep.concurrency must be a positive number");
204
+ }
205
+ if (config.deep.early_exit_score !== undefined && (typeof config.deep.early_exit_score !== "number" || config.deep.early_exit_score < 0 || config.deep.early_exit_score > 1)) {
206
+ errors.push("deep.early_exit_score must be a number between 0 and 1");
207
+ }
208
+ if (config.deep.max_ms !== undefined && (typeof config.deep.max_ms !== "number" || config.deep.max_ms < 1)) {
209
+ errors.push("deep.max_ms must be a positive number");
210
+ }
211
+ if (config.deep.detect_date !== undefined && typeof config.deep.detect_date !== "boolean") {
212
+ errors.push("deep.detect_date must be a boolean");
213
+ }
214
+ }
164
215
  if (config.kb) {
165
216
  const kb = config.kb;
166
217
  if (typeof kb.chunk_size !== "number" || kb.chunk_size < 1) {