open-research 1.1.2 → 1.2.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.
@@ -0,0 +1,28 @@
1
+ import {
2
+ compactConversation,
3
+ createSessionUsage,
4
+ estimateConversationTokens,
5
+ estimateMessageTokens,
6
+ estimateTokens,
7
+ getCompactThreshold,
8
+ getContextWindow,
9
+ manualCompact,
10
+ maybeCompact,
11
+ pruneToolOutputs,
12
+ updateUsageFromApi
13
+ } from "./chunk-EW27OWCA.js";
14
+ import "./chunk-GVEVKDGV.js";
15
+ import "./chunk-3RG5ZIWI.js";
16
+ export {
17
+ compactConversation,
18
+ createSessionUsage,
19
+ estimateConversationTokens,
20
+ estimateMessageTokens,
21
+ estimateTokens,
22
+ getCompactThreshold,
23
+ getContextWindow,
24
+ manualCompact,
25
+ maybeCompact,
26
+ pruneToolOutputs,
27
+ updateUsageFromApi
28
+ };
@@ -0,0 +1,22 @@
1
+ import "./chunk-3RG5ZIWI.js";
2
+
3
+ // src/lib/agent/tools/finish-subagent.ts
4
+ var FINISH_SUBAGENT_SENTINEL = "__FINISH_SUBAGENT__";
5
+ function executeFinishSubagent(args) {
6
+ const handoff = {
7
+ summary: args.summary || "Task completed.",
8
+ filesCreated: args.files_created ?? [],
9
+ filesModified: args.files_modified ?? [],
10
+ keyFindings: args.key_findings ?? [],
11
+ status: args.status || "completed",
12
+ blockedReason: args.blocked_reason
13
+ };
14
+ return {
15
+ result: FINISH_SUBAGENT_SENTINEL,
16
+ handoff
17
+ };
18
+ }
19
+ export {
20
+ FINISH_SUBAGENT_SENTINEL,
21
+ executeFinishSubagent
22
+ };
@@ -1,13 +1,13 @@
1
- import {
2
- loadOntology,
3
- saveOntology
4
- } from "./chunk-3WM33M3O.js";
5
1
  import {
6
2
  DEFAULT_CONFIDENCE
7
3
  } from "./chunk-KJHM7ZW2.js";
8
4
  import {
9
5
  getProviderCatalog
10
6
  } from "./chunk-GVEVKDGV.js";
7
+ import {
8
+ loadOntology,
9
+ saveOntology
10
+ } from "./chunk-3WM33M3O.js";
11
11
  import {
12
12
  getConnections,
13
13
  getNote,
@@ -576,7 +576,7 @@ async function runOntologyManager(input) {
576
576
  args,
577
577
  ontology
578
578
  );
579
- if (updated !== ontology) mutated = true;
579
+ if (!READ_ONLY_TOOLS.has(toolCall.name)) mutated = true;
580
580
  ontology = updated;
581
581
  messages.push({
582
582
  role: "tool",
@@ -0,0 +1,20 @@
1
+ import {
2
+ createApp
3
+ } from "../chunk-YFDZRMPM.js";
4
+ import "../chunk-G7IOFPGG.js";
5
+ import "../chunk-EW27OWCA.js";
6
+ import "../chunk-KOBMIIQM.js";
7
+ import "../chunk-3GZIDCV2.js";
8
+ import "../chunk-XOSPMXYH.js";
9
+ import "../chunk-CRSHN3PQ.js";
10
+ import "../chunk-77Q5B5H7.js";
11
+ import "../chunk-4HCPHCC2.js";
12
+ import "../chunk-GVEVKDGV.js";
13
+ import "../chunk-3RG5ZIWI.js";
14
+
15
+ // src/server/serve.ts
16
+ import { serve } from "@hono/node-server";
17
+ var port = parseInt(process.env.PORT ?? "3210", 10);
18
+ var { app } = createApp();
19
+ console.log(`Open Research server listening on http://localhost:${port}`);
20
+ serve({ fetch: app.fetch, port });
@@ -0,0 +1,16 @@
1
+ import {
2
+ createApp
3
+ } from "./chunk-YFDZRMPM.js";
4
+ import "./chunk-G7IOFPGG.js";
5
+ import "./chunk-EW27OWCA.js";
6
+ import "./chunk-KOBMIIQM.js";
7
+ import "./chunk-3GZIDCV2.js";
8
+ import "./chunk-XOSPMXYH.js";
9
+ import "./chunk-CRSHN3PQ.js";
10
+ import "./chunk-77Q5B5H7.js";
11
+ import "./chunk-4HCPHCC2.js";
12
+ import "./chunk-GVEVKDGV.js";
13
+ import "./chunk-3RG5ZIWI.js";
14
+ export {
15
+ createApp
16
+ };
@@ -2,7 +2,7 @@ import {
2
2
  appendSessionEvent,
3
3
  listSessions,
4
4
  loadSessionHistory
5
- } from "./chunk-HRVDYJEC.js";
5
+ } from "./chunk-KOBMIIQM.js";
6
6
  import "./chunk-4HCPHCC2.js";
7
7
  import "./chunk-3RG5ZIWI.js";
8
8
  export {
@@ -0,0 +1,90 @@
1
+ import {
2
+ getSemanticScholarApiKey,
3
+ loadOpenResearchConfig
4
+ } from "./chunk-CRSHN3PQ.js";
5
+ import "./chunk-77Q5B5H7.js";
6
+ import "./chunk-4HCPHCC2.js";
7
+ import "./chunk-3RG5ZIWI.js";
8
+
9
+ // src/lib/agent/tools/traverse-citations.ts
10
+ var S2_BASE = "https://api.semanticscholar.org/graph/v1";
11
+ var FIELDS = "title,url,year,venue,citationCount,externalIds,abstract";
12
+ async function executeTraverseCitations(args) {
13
+ const { paper_id, direction, limit = 10 } = args;
14
+ if (!paper_id) return "Error: paper_id is required.";
15
+ if (direction !== "references" && direction !== "citations") {
16
+ return 'Error: direction must be "references" or "citations".';
17
+ }
18
+ const config = await loadOpenResearchConfig().catch(() => null);
19
+ const apiKey = getSemanticScholarApiKey(config);
20
+ let resolvedId = paper_id;
21
+ if (paper_id.startsWith("10.")) {
22
+ resolvedId = `DOI:${paper_id}`;
23
+ } else if (/^\d{4}\.\d{4,5}/.test(paper_id)) {
24
+ resolvedId = `ArXiv:${paper_id}`;
25
+ }
26
+ const endpoint = `${S2_BASE}/paper/${encodeURIComponent(resolvedId)}/${direction}`;
27
+ const url = new URL(endpoint);
28
+ url.searchParams.set("fields", FIELDS);
29
+ url.searchParams.set("limit", String(Math.min(limit, 50)));
30
+ const headers = {
31
+ "Accept": "application/json"
32
+ };
33
+ if (apiKey) headers["x-api-key"] = apiKey;
34
+ try {
35
+ const response = await fetch(url.toString(), {
36
+ headers,
37
+ signal: AbortSignal.timeout(15e3)
38
+ });
39
+ if (!response.ok) {
40
+ if (response.status === 404) return `Paper not found: ${paper_id}`;
41
+ return `Semantic Scholar API error: ${response.status}`;
42
+ }
43
+ const body = await response.json();
44
+ const edges = body.data ?? [];
45
+ const papers = [];
46
+ for (const edge of edges) {
47
+ const paper = direction === "citations" ? edge.citingPaper : edge.citedPaper;
48
+ if (!paper?.title) continue;
49
+ papers.push({
50
+ title: paper.title,
51
+ year: paper.year ?? void 0,
52
+ venue: paper.venue || void 0,
53
+ citationCount: paper.citationCount ?? 0,
54
+ url: paper.url ?? `https://www.semanticscholar.org/paper/${paper.paperId}`,
55
+ doi: paper.externalIds?.DOI,
56
+ arxivId: paper.externalIds?.ArXiv,
57
+ abstract: paper.abstract?.slice(0, 300)
58
+ });
59
+ }
60
+ papers.sort((a, b) => b.citationCount - a.citationCount);
61
+ if (papers.length === 0) {
62
+ return `No ${direction} found for paper ${paper_id}.`;
63
+ }
64
+ const dirLabel = direction === "citations" ? "Cited by" : "References";
65
+ const lines = [`${dirLabel} (${papers.length} papers, sorted by citation count):
66
+ `];
67
+ for (let i = 0; i < papers.length; i++) {
68
+ const p = papers[i];
69
+ const meta = [];
70
+ if (p.year) meta.push(String(p.year));
71
+ if (p.venue) meta.push(p.venue);
72
+ meta.push(`${p.citationCount} citations`);
73
+ if (p.doi) meta.push(`DOI:${p.doi}`);
74
+ if (p.arxivId) meta.push(`arXiv:${p.arxivId}`);
75
+ lines.push(`${i + 1}. "${p.title}"`);
76
+ lines.push(` ${meta.join(" | ")}`);
77
+ lines.push(` ${p.url}`);
78
+ if (p.abstract) {
79
+ lines.push(` ${p.abstract}${p.abstract.length >= 300 ? "..." : ""}`);
80
+ }
81
+ lines.push("");
82
+ }
83
+ return lines.join("\n");
84
+ } catch (error) {
85
+ return `Error traversing citations: ${error instanceof Error ? error.message : String(error)}`;
86
+ }
87
+ }
88
+ export {
89
+ executeTraverseCitations
90
+ };
@@ -2,12 +2,16 @@ import {
2
2
  USER_AGENT,
3
3
  extractBatch,
4
4
  fetchAndParseContent,
5
- formatExtractionResults,
5
+ formatExtractionResults
6
+ } from "./chunk-XOSPMXYH.js";
7
+ import {
6
8
  loadOpenResearchConfig
7
- } from "./chunk-TJA4CAZE.js";
9
+ } from "./chunk-CRSHN3PQ.js";
8
10
  import "./chunk-77Q5B5H7.js";
9
11
  import "./chunk-4HCPHCC2.js";
10
- import "./chunk-GVEVKDGV.js";
12
+ import {
13
+ getProviderCatalog
14
+ } from "./chunk-GVEVKDGV.js";
11
15
  import "./chunk-3RG5ZIWI.js";
12
16
 
13
17
  // src/lib/search/duckduckgo.ts
@@ -98,6 +102,48 @@ async function searchBrave(query, apiKey, numResults = 10) {
98
102
  }
99
103
 
100
104
  // src/lib/agent/tools/web-search.ts
105
+ var ADVERSARIAL_QUERY_PROMPT = `You generate search queries that find evidence AGAINST a research target. Your queries should surface:
106
+ - Direct contradictions or negative results
107
+ - Methodological limitations or criticisms
108
+ - Alternative explanations for the same phenomena
109
+ - Boundary conditions where the target claim fails
110
+
111
+ Do NOT rephrase the target as a question. Generate queries a skeptical reviewer would use to challenge the claim.`;
112
+ var ADVERSARIAL_QUERY_SCHEMA = {
113
+ name: "adversarial_queries",
114
+ schema: {
115
+ type: "object",
116
+ properties: {
117
+ queries: {
118
+ type: "array",
119
+ items: { type: "string" },
120
+ description: "1-2 search queries designed to find contradicting or limiting evidence"
121
+ }
122
+ },
123
+ required: ["queries"],
124
+ additionalProperties: false
125
+ }
126
+ };
127
+ async function generateAdversarialWebQueries(target, provider) {
128
+ try {
129
+ const backgroundModel = getProviderCatalog(provider.kind).backgroundModel;
130
+ const response = await provider.callLLM({
131
+ messages: [
132
+ { role: "system", content: ADVERSARIAL_QUERY_PROMPT },
133
+ { role: "user", content: target }
134
+ ],
135
+ model: backgroundModel,
136
+ reasoningEffort: "low",
137
+ temperature: 0,
138
+ maxTokens: 150,
139
+ jsonSchema: ADVERSARIAL_QUERY_SCHEMA
140
+ });
141
+ const parsed = JSON.parse(response.content);
142
+ return Array.isArray(parsed.queries) ? parsed.queries.slice(0, 2) : [];
143
+ } catch {
144
+ return [];
145
+ }
146
+ }
101
147
  async function discoverWebResults(query, numResults) {
102
148
  const config = await loadOpenResearchConfig().catch(() => null);
103
149
  const braveKey = config?.apiKeys?.brave;
@@ -117,21 +163,46 @@ async function discoverWebResults(query, numResults) {
117
163
  };
118
164
  }
119
165
  async function executeWebSearch(args, provider) {
120
- if (!args.target || !args.query) {
121
- return { result: "Error: both target and query are required." };
166
+ const userQueries = args.queries ?? (args.query ? [args.query] : []);
167
+ if (!args.target || userQueries.length === 0) {
168
+ return { result: "Error: both target and query/queries are required." };
122
169
  }
123
170
  const numResults = Math.min(args.num_results ?? 5, 8);
124
- const { results: searchResults, backend } = await discoverWebResults(args.query, numResults);
125
- if (searchResults.length === 0) {
171
+ const adversarialPromise = provider ? generateAdversarialWebQueries(args.target, provider) : Promise.resolve([]);
172
+ const allSearchPromises = userQueries.map((q) => discoverWebResults(q, numResults));
173
+ const searchResultSets = await Promise.allSettled(allSearchPromises);
174
+ const seenUrls = /* @__PURE__ */ new Set();
175
+ const allHits = [];
176
+ for (const resultSet of searchResultSets) {
177
+ if (resultSet.status === "fulfilled") {
178
+ for (const hit of resultSet.value.results) {
179
+ if (!seenUrls.has(hit.url)) {
180
+ seenUrls.add(hit.url);
181
+ allHits.push({ ...hit, queryIntent: "primary" });
182
+ }
183
+ }
184
+ }
185
+ }
186
+ const adversarialQueries = await adversarialPromise;
187
+ for (const aq of adversarialQueries) {
188
+ const { results } = await discoverWebResults(aq, 3);
189
+ for (const hit of results) {
190
+ if (!seenUrls.has(hit.url)) {
191
+ seenUrls.add(hit.url);
192
+ allHits.push({ ...hit, queryIntent: "adversarial" });
193
+ }
194
+ }
195
+ }
196
+ if (allHits.length === 0) {
126
197
  return { result: "No web results found. Try a different query." };
127
198
  }
128
199
  if (!provider) {
129
- const summary = searchResults.slice(0, numResults).map((r, i) => `${i + 1}. ${r.title}
200
+ const summary = allHits.slice(0, numResults).map((r, i) => `${i + 1}. ${r.title}
130
201
  ${r.url}
131
202
  ${r.snippet}`).join("\n\n");
132
- return { result: `[${backend}] ${summary}` };
203
+ return { result: summary };
133
204
  }
134
- const toFetch = searchResults.slice(0, numResults);
205
+ const toFetch = allHits.slice(0, numResults + 3);
135
206
  const contentResults = await Promise.allSettled(
136
207
  toFetch.map(async (hit) => {
137
208
  const content = await fetchAndParseContent(hit.url);
@@ -140,7 +211,7 @@ async function executeWebSearch(args, provider) {
140
211
  })
141
212
  );
142
213
  const extractionInputs = [];
143
- const titleMap = /* @__PURE__ */ new Map();
214
+ const hitMap = /* @__PURE__ */ new Map();
144
215
  for (const result of contentResults) {
145
216
  if (result.status === "fulfilled" && result.value) {
146
217
  const { hit, text } = result.value;
@@ -150,7 +221,7 @@ async function executeWebSearch(args, provider) {
150
221
  url: hit.url,
151
222
  target: args.target
152
223
  });
153
- titleMap.set(hit.url, hit.title);
224
+ hitMap.set(hit.url, hit);
154
225
  }
155
226
  }
156
227
  if (extractionInputs.length === 0) {
@@ -165,10 +236,12 @@ ${summary}` };
165
236
  const extracted = [];
166
237
  for (const [url, extraction] of extractions) {
167
238
  if (extraction.relevanceScore >= 2) {
239
+ const hit = hitMap.get(url);
168
240
  extracted.push({
169
- title: titleMap.get(url) ?? url,
241
+ title: hit?.title ?? url,
170
242
  url,
171
- extraction
243
+ extraction,
244
+ queryIntent: hit?.queryIntent
172
245
  });
173
246
  }
174
247
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-research",
3
- "version": "1.1.2",
3
+ "version": "1.2.1",
4
4
  "description": "Local-first research CLI agent — discover papers, synthesize notes, run analysis, and draft artifacts from your terminal.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -37,10 +37,12 @@
37
37
  "semantic-scholar"
38
38
  ],
39
39
  "dependencies": {
40
+ "@hono/node-server": "^1.19.13",
40
41
  "cheerio": "^1.1.2",
41
42
  "commander": "^14.0.1",
42
43
  "diff": "^8.0.4",
43
44
  "gray-matter": "^4.0.3",
45
+ "hono": "^4.12.12",
44
46
  "ink": "^6.3.1",
45
47
  "ink-text-input": "^6.0.0",
46
48
  "open": "^10.2.0",
@@ -1,9 +1,9 @@
1
- import {
2
- loadOntology
3
- } from "./chunk-3WM33M3O.js";
4
1
  import {
5
2
  getProviderCatalog
6
3
  } from "./chunk-GVEVKDGV.js";
4
+ import {
5
+ loadOntology
6
+ } from "./chunk-3WM33M3O.js";
7
7
  import {
8
8
  getConnections,
9
9
  getNote,