open-research 0.1.26 → 1.0.0

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/dist/cli.js CHANGED
@@ -1,64 +1,59 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- __require,
4
3
  appendSessionEvent,
4
+ loadSessionHistory
5
+ } from "./chunk-ZUSIRA5S.js";
6
+ import {
7
+ ensureOpenResearchConfig,
8
+ executeFetchUrl,
9
+ extractBatch,
10
+ extractPdfText,
11
+ fetchAndParseContent,
12
+ formatExtractionResults,
13
+ getBraveApiKey,
14
+ getConfiguredOpenAIApiKey,
15
+ getOpenAlexApiKey,
16
+ getSemanticScholarApiKey,
17
+ loadOpenResearchConfig,
18
+ readJsonFile,
19
+ saveOpenResearchConfig,
20
+ themeValues,
21
+ writeJsonFile
22
+ } from "./chunk-TQSQRNX6.js";
23
+ import {
5
24
  getOpenResearchAuthFile,
6
- getOpenResearchConfigFile,
7
25
  getOpenResearchRoot,
8
26
  getOpenResearchSkillsDir,
9
27
  getWorkspaceMetaDir,
10
28
  getWorkspaceProjectFile,
11
- getWorkspaceSessionsDir,
12
- loadSessionHistory
13
- } from "./chunk-AYB7CAO5.js";
29
+ getWorkspaceSessionsDir
30
+ } from "./chunk-I5NVYKG7.js";
31
+ import {
32
+ __require
33
+ } from "./chunk-3RG5ZIWI.js";
14
34
 
15
35
  // src/cli.ts
16
36
  import React6 from "react";
17
- import path20 from "path";
37
+ import path19 from "path";
18
38
  import { Command } from "commander";
19
39
  import { render } from "ink";
20
40
 
21
41
  // src/lib/workspace/project.ts
22
- import fs2 from "fs/promises";
23
- import path2 from "path";
24
-
25
- // src/lib/fs/json.ts
26
42
  import fs from "fs/promises";
27
43
  import path from "path";
28
- async function readJsonFile(filePath, fallback) {
29
- try {
30
- const raw = await fs.readFile(filePath, "utf8");
31
- return JSON.parse(raw);
32
- } catch (error) {
33
- const code = typeof error === "object" && error && "code" in error ? String(error.code) : "";
34
- if (code === "ENOENT") {
35
- return fallback;
36
- }
37
- throw error;
38
- }
39
- }
40
- async function writeJsonFile(filePath, value, mode) {
41
- await fs.mkdir(path.dirname(filePath), { recursive: true });
42
- await fs.writeFile(filePath, JSON.stringify(value, null, 2), {
43
- encoding: "utf8",
44
- mode
45
- });
46
- }
47
-
48
- // src/lib/workspace/project.ts
49
44
  var MANAGED_DIRS = ["sources", "notes", "artifacts", "papers", "experiments"];
50
45
  async function initWorkspace(options) {
51
- const workspaceDir = path2.resolve(options.workspaceDir);
52
- await fs2.mkdir(workspaceDir, { recursive: true });
46
+ const workspaceDir = path.resolve(options.workspaceDir);
47
+ await fs.mkdir(workspaceDir, { recursive: true });
53
48
  for (const dir of MANAGED_DIRS) {
54
- await fs2.mkdir(path2.join(workspaceDir, dir), { recursive: true });
49
+ await fs.mkdir(path.join(workspaceDir, dir), { recursive: true });
55
50
  }
56
- await fs2.mkdir(getWorkspaceMetaDir(workspaceDir), { recursive: true });
57
- await fs2.mkdir(getWorkspaceSessionsDir(workspaceDir), { recursive: true });
51
+ await fs.mkdir(getWorkspaceMetaDir(workspaceDir), { recursive: true });
52
+ await fs.mkdir(getWorkspaceSessionsDir(workspaceDir), { recursive: true });
58
53
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
59
54
  const project = {
60
55
  version: 1,
61
- title: options.title?.trim() || path2.basename(workspaceDir),
56
+ title: options.title?.trim() || path.basename(workspaceDir),
62
57
  createdAt: timestamp,
63
58
  updatedAt: timestamp,
64
59
  defaults: {
@@ -74,36 +69,15 @@ async function initWorkspace(options) {
74
69
  }
75
70
  async function loadWorkspaceProject(workspaceDir) {
76
71
  return readJsonFile(
77
- getWorkspaceProjectFile(path2.resolve(workspaceDir)),
72
+ getWorkspaceProjectFile(path.resolve(workspaceDir)),
78
73
  null
79
74
  );
80
75
  }
81
76
 
82
77
  // src/lib/workspace/sources.ts
83
- import fs4 from "fs/promises";
84
- import path3 from "path";
78
+ import fs2 from "fs/promises";
79
+ import path2 from "path";
85
80
  import { load } from "cheerio";
86
-
87
- // src/lib/fs/pdf.ts
88
- import fs3 from "fs/promises";
89
- async function extractPdfText(filePath, options) {
90
- const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs");
91
- const buffer = await fs3.readFile(filePath);
92
- const document = await pdfjs.getDocument({ data: new Uint8Array(buffer) }).promise;
93
- const totalPages = document.numPages;
94
- const start = Math.max(1, options?.startPage ?? 1);
95
- const end = Math.min(totalPages, options?.endPage ?? totalPages);
96
- const pages = [];
97
- for (let pageNumber = start; pageNumber <= end; pageNumber += 1) {
98
- const page = await document.getPage(pageNumber);
99
- const content = await page.getTextContent();
100
- const text = content.items.map((item) => "str" in item ? String(item.str) : "").join(" ").replace(/\s+/g, " ").trim();
101
- if (text) pages.push(text);
102
- }
103
- return { text: pages.join("\n\n"), totalPages };
104
- }
105
-
106
- // src/lib/workspace/sources.ts
107
81
  function slugify(value) {
108
82
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "source";
109
83
  }
@@ -138,10 +112,10 @@ async function addUrlSource(input2) {
138
112
  const titleMatch = markdown.match(/^#\s+(.+)$/m);
139
113
  const label = titleMatch?.[1]?.trim() || new URL(input2.url).hostname;
140
114
  const fileName = `${slugify(label)}.md`;
141
- const relativePath = path3.join("sources", fileName);
142
- const absolutePath = path3.join(input2.workspaceDir, relativePath);
143
- await fs4.mkdir(path3.dirname(absolutePath), { recursive: true });
144
- await fs4.writeFile(absolutePath, markdown, "utf8");
115
+ const relativePath = path2.join("sources", fileName);
116
+ const absolutePath = path2.join(input2.workspaceDir, relativePath);
117
+ await fs2.mkdir(path2.dirname(absolutePath), { recursive: true });
118
+ await fs2.writeFile(absolutePath, markdown, "utf8");
145
119
  const source2 = {
146
120
  id: crypto.randomUUID(),
147
121
  kind: "url",
@@ -153,31 +127,31 @@ async function addUrlSource(input2) {
153
127
  return source2;
154
128
  }
155
129
  async function addFileSource(input2) {
156
- const absoluteInput = path3.resolve(input2.filePath);
157
- const parsed = path3.parse(absoluteInput);
130
+ const absoluteInput = path2.resolve(input2.filePath);
131
+ const parsed = path2.parse(absoluteInput);
158
132
  const slug = slugify(parsed.name);
159
- const markdownRelative = path3.join("sources", `${slug}.md`);
160
- const markdownAbsolute = path3.join(input2.workspaceDir, markdownRelative);
133
+ const markdownRelative = path2.join("sources", `${slug}.md`);
134
+ const markdownAbsolute = path2.join(input2.workspaceDir, markdownRelative);
161
135
  let markdown = "";
162
136
  if (parsed.ext.toLowerCase() === ".pdf") {
163
- const rawRelative = path3.join("sources", `${slug}.pdf`);
164
- const rawAbsolute = path3.join(input2.workspaceDir, rawRelative);
165
- await fs4.mkdir(path3.dirname(rawAbsolute), { recursive: true });
166
- await fs4.copyFile(absoluteInput, rawAbsolute);
137
+ const rawRelative = path2.join("sources", `${slug}.pdf`);
138
+ const rawAbsolute = path2.join(input2.workspaceDir, rawRelative);
139
+ await fs2.mkdir(path2.dirname(rawAbsolute), { recursive: true });
140
+ await fs2.copyFile(absoluteInput, rawAbsolute);
167
141
  const { text } = await extractPdfText(absoluteInput);
168
142
  markdown = `# ${parsed.name}
169
143
 
170
144
  ${text}`;
171
145
  } else {
172
- markdown = await fs4.readFile(absoluteInput, "utf8");
146
+ markdown = await fs2.readFile(absoluteInput, "utf8");
173
147
  if (!markdown.startsWith("# ")) {
174
148
  markdown = `# ${parsed.name}
175
149
 
176
150
  ${markdown}`;
177
151
  }
178
152
  }
179
- await fs4.mkdir(path3.dirname(markdownAbsolute), { recursive: true });
180
- await fs4.writeFile(markdownAbsolute, markdown, "utf8");
153
+ await fs2.mkdir(path2.dirname(markdownAbsolute), { recursive: true });
154
+ await fs2.writeFile(markdownAbsolute, markdown, "utf8");
181
155
  const source2 = {
182
156
  id: crypto.randomUUID(),
183
157
  kind: parsed.ext.toLowerCase() === ".pdf" ? "pdf" : "file",
@@ -190,8 +164,8 @@ ${markdown}`;
190
164
  }
191
165
 
192
166
  // src/lib/auth/import-codex.ts
193
- import fs6 from "fs/promises";
194
- import path4 from "path";
167
+ import fs4 from "fs/promises";
168
+ import path3 from "path";
195
169
 
196
170
  // src/lib/storage/credential-types.ts
197
171
  function getDefaultCredentialCapabilities() {
@@ -220,18 +194,18 @@ function getBootstrapCredentialValidation() {
220
194
  }
221
195
 
222
196
  // src/lib/auth/store.ts
223
- import fs5 from "fs/promises";
197
+ import fs3 from "fs/promises";
224
198
  var AUTH_FILE_MODE = 384;
225
199
  async function ensureCliHome(options) {
226
200
  const root = getOpenResearchRoot(options);
227
- await fs5.mkdir(root, { recursive: true, mode: 448 });
201
+ await fs3.mkdir(root, { recursive: true, mode: 448 });
228
202
  return root;
229
203
  }
230
204
  async function saveStoredAuth(auth2, options) {
231
205
  await ensureCliHome(options);
232
206
  const authFile = getOpenResearchAuthFile(options);
233
207
  await writeJsonFile(authFile, auth2, AUTH_FILE_MODE);
234
- await fs5.chmod(authFile, AUTH_FILE_MODE);
208
+ await fs3.chmod(authFile, AUTH_FILE_MODE);
235
209
  return authFile;
236
210
  }
237
211
  async function loadStoredAuth(options) {
@@ -240,7 +214,7 @@ async function loadStoredAuth(options) {
240
214
  }
241
215
  async function clearStoredAuth(options) {
242
216
  const authFile = getOpenResearchAuthFile(options);
243
- await fs5.rm(authFile, { force: true });
217
+ await fs3.rm(authFile, { force: true });
244
218
  }
245
219
 
246
220
  // src/lib/auth/openai-oauth.ts
@@ -333,9 +307,9 @@ function getTokenExpiryMs(token) {
333
307
  }
334
308
  async function importCodexAuth(options = {}) {
335
309
  const now = options.now ?? Date.now;
336
- const codexAuthPath = options.codexAuthFilePath ?? path4.join(options.homeDir ?? process.env.HOME ?? "", ".codex", "auth.json");
310
+ const codexAuthPath = options.codexAuthFilePath ?? path3.join(options.homeDir ?? process.env.HOME ?? "", ".codex", "auth.json");
337
311
  const parsed = JSON.parse(
338
- await fs6.readFile(codexAuthPath, "utf8")
312
+ await fs4.readFile(codexAuthPath, "utf8")
339
313
  );
340
314
  if (parsed.auth_mode !== "chatgpt") {
341
315
  throw new Error("Codex is not signed in with OpenAI on this device.");
@@ -666,73 +640,6 @@ async function validateOpenAIConnection(input2) {
666
640
  }
667
641
  }
668
642
 
669
- // src/lib/config/store.ts
670
- import { z } from "zod";
671
- var themeValues = ["dark", "light"];
672
- var openAIProviderConfigSchema = z.object({
673
- apiKey: z.string().optional()
674
- }).optional();
675
- var openResearchConfigSchema = z.object({
676
- version: z.literal(1),
677
- defaults: z.object({
678
- model: z.string().min(1),
679
- reasoningEffort: z.enum(["low", "medium", "high", "xhigh"]),
680
- editPolicy: z.literal("mixed")
681
- }),
682
- theme: z.enum(themeValues).default("dark"),
683
- lastWorkspace: z.string().nullable(),
684
- providers: z.object({
685
- openai: openAIProviderConfigSchema
686
- }).optional(),
687
- apiKeys: z.object({
688
- openai: z.string().optional(),
689
- semanticScholar: z.string().optional(),
690
- openAlex: z.string().optional()
691
- }).optional()
692
- });
693
- var DEFAULT_OPEN_RESEARCH_CONFIG = {
694
- version: 1,
695
- defaults: {
696
- model: "gpt-5.4",
697
- reasoningEffort: "medium",
698
- editPolicy: "mixed"
699
- },
700
- theme: "dark",
701
- lastWorkspace: null,
702
- providers: {
703
- openai: {}
704
- },
705
- apiKeys: {}
706
- };
707
- function getConfiguredOpenAIApiKey(config) {
708
- return config?.providers?.openai?.apiKey || config?.apiKeys?.openai;
709
- }
710
- function getSemanticScholarApiKey(config) {
711
- return config?.apiKeys?.semanticScholar || process.env.SEMANTIC_SCHOLAR_API_KEY;
712
- }
713
- function getOpenAlexApiKey(config) {
714
- return config?.apiKeys?.openAlex || process.env.OPENALEX_API_KEY;
715
- }
716
- async function loadOpenResearchConfig(options) {
717
- const configFile = getOpenResearchConfigFile(options);
718
- const config = await readJsonFile(configFile, null);
719
- if (!config) {
720
- return null;
721
- }
722
- return openResearchConfigSchema.parse(config);
723
- }
724
- async function saveOpenResearchConfig(config, options) {
725
- await writeJsonFile(getOpenResearchConfigFile(options), config);
726
- }
727
- async function ensureOpenResearchConfig(options) {
728
- const existing = await loadOpenResearchConfig(options);
729
- if (existing) {
730
- return existing;
731
- }
732
- await writeJsonFile(getOpenResearchConfigFile(options), DEFAULT_OPEN_RESEARCH_CONFIG);
733
- return DEFAULT_OPEN_RESEARCH_CONFIG;
734
- }
735
-
736
643
  // src/lib/llm/provider-resolution.ts
737
644
  function trimCredential(value) {
738
645
  const trimmed = value?.trim();
@@ -836,26 +743,26 @@ async function getAuthStatus(options) {
836
743
  }
837
744
 
838
745
  // src/lib/skills/registry.ts
839
- import fs7 from "fs/promises";
746
+ import fs5 from "fs/promises";
840
747
  import fsSync from "fs";
841
- import path5 from "path";
748
+ import path4 from "path";
842
749
  import matter from "gray-matter";
843
750
  var BUILTIN_SKILLS_DIR = [
844
- path5.resolve(path5.join(import.meta.dirname, "../../../builtin-skills")),
845
- path5.resolve(path5.join(import.meta.dirname, "../builtin-skills"))
846
- ].find((candidate) => fsSync.existsSync(candidate)) ?? path5.resolve(path5.join(import.meta.dirname, "../../../builtin-skills"));
751
+ path4.resolve(path4.join(import.meta.dirname, "../../../builtin-skills")),
752
+ path4.resolve(path4.join(import.meta.dirname, "../builtin-skills"))
753
+ ].find((candidate) => fsSync.existsSync(candidate)) ?? path4.resolve(path4.join(import.meta.dirname, "../../../builtin-skills"));
847
754
  function normalizeSkillName(name) {
848
755
  return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
849
756
  }
850
757
  async function ensureUserSkillsDir(options) {
851
758
  const dir = getOpenResearchSkillsDir(options);
852
- await fs7.mkdir(dir, { recursive: true });
759
+ await fs5.mkdir(dir, { recursive: true });
853
760
  return dir;
854
761
  }
855
762
  async function readSkillSummary(skillDir, source2) {
856
- const skillFile = path5.join(skillDir, "SKILL.md");
763
+ const skillFile = path4.join(skillDir, "SKILL.md");
857
764
  try {
858
- const raw = await fs7.readFile(skillFile, "utf8");
765
+ const raw = await fs5.readFile(skillFile, "utf8");
859
766
  const parsed = matter(raw);
860
767
  const name = String(parsed.data.name ?? "").trim();
861
768
  const description = String(parsed.data.description ?? "").trim();
@@ -874,9 +781,9 @@ async function readSkillSummary(skillDir, source2) {
874
781
  }
875
782
  }
876
783
  async function listSkillsInDirectory(rootDir, source2) {
877
- const entries = await fs7.readdir(rootDir, { withFileTypes: true }).catch(() => []);
784
+ const entries = await fs5.readdir(rootDir, { withFileTypes: true }).catch(() => []);
878
785
  const results = await Promise.all(
879
- entries.filter((entry) => entry.isDirectory()).map((entry) => readSkillSummary(path5.join(rootDir, entry.name), source2))
786
+ entries.filter((entry) => entry.isDirectory()).map((entry) => readSkillSummary(path4.join(rootDir, entry.name), source2))
880
787
  );
881
788
  return results.filter((value) => Boolean(value));
882
789
  }
@@ -890,15 +797,15 @@ async function listAvailableSkills(options) {
890
797
  }
891
798
  async function validateSkillDirectory(input2) {
892
799
  const errors = [];
893
- const skillFile = path5.join(input2.skillDir, "SKILL.md");
894
- const raw = await fs7.readFile(skillFile, "utf8").catch(() => "");
800
+ const skillFile = path4.join(input2.skillDir, "SKILL.md");
801
+ const raw = await fs5.readFile(skillFile, "utf8").catch(() => "");
895
802
  if (!raw) {
896
803
  return { ok: false, errors: ["SKILL.md is missing."] };
897
804
  }
898
805
  const parsed = matter(raw);
899
806
  const name = String(parsed.data.name ?? "").trim();
900
807
  const description = String(parsed.data.description ?? "").trim();
901
- const normalizedDirName = path5.basename(input2.skillDir);
808
+ const normalizedDirName = path4.basename(input2.skillDir);
902
809
  const normalizedName = normalizeSkillName(name);
903
810
  if (!name) {
904
811
  errors.push("Skill frontmatter requires a name.");
@@ -921,10 +828,10 @@ async function validateSkillDirectory(input2) {
921
828
  async function createSkillScaffold(input2) {
922
829
  const skillsDir = await ensureUserSkillsDir({ homeDir: input2.homeDir });
923
830
  const name = normalizeSkillName(input2.name);
924
- const skillDir = path5.join(skillsDir, name);
925
- await fs7.mkdir(path5.join(skillDir, "scripts"), { recursive: true });
926
- await fs7.mkdir(path5.join(skillDir, "references"), { recursive: true });
927
- await fs7.mkdir(path5.join(skillDir, "assets"), { recursive: true });
831
+ const skillDir = path4.join(skillsDir, name);
832
+ await fs5.mkdir(path4.join(skillDir, "scripts"), { recursive: true });
833
+ await fs5.mkdir(path4.join(skillDir, "references"), { recursive: true });
834
+ await fs5.mkdir(path4.join(skillDir, "assets"), { recursive: true });
928
835
  const body = `---
929
836
  name: ${name}
930
837
  description: ${input2.description}
@@ -944,7 +851,7 @@ ${input2.examples.map((example) => `- ${example}`).join("\n")}
944
851
 
945
852
  ${input2.workflow}
946
853
  `;
947
- await fs7.writeFile(path5.join(skillDir, "SKILL.md"), body, "utf8");
854
+ await fs5.writeFile(path4.join(skillDir, "SKILL.md"), body, "utf8");
948
855
  return skillDir;
949
856
  }
950
857
 
@@ -982,13 +889,13 @@ function formatDateTime(value) {
982
889
  }
983
890
 
984
891
  // src/lib/cli/version.ts
985
- var PACKAGE_VERSION = "0.1.26";
892
+ var PACKAGE_VERSION = "1.0.0";
986
893
  function getPackageVersion() {
987
894
  return PACKAGE_VERSION;
988
895
  }
989
896
 
990
897
  // src/tui/app.tsx
991
- import path19 from "path";
898
+ import path18 from "path";
992
899
  import {
993
900
  startTransition as startTransition2,
994
901
  useDeferredValue,
@@ -2448,8 +2355,8 @@ function useTheme() {
2448
2355
  }
2449
2356
 
2450
2357
  // src/lib/workspace/scan.ts
2451
- import fs8 from "fs/promises";
2452
- import path6 from "path";
2358
+ import fs6 from "fs/promises";
2359
+ import path5 from "path";
2453
2360
  var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
2454
2361
  ".md",
2455
2362
  ".markdown",
@@ -2479,30 +2386,30 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
2479
2386
  ".turbo"
2480
2387
  ]);
2481
2388
  async function walkDir(rootDir, currentDir, out) {
2482
- const entries = await fs8.readdir(currentDir, { withFileTypes: true });
2389
+ const entries = await fs6.readdir(currentDir, { withFileTypes: true });
2483
2390
  for (const entry of entries) {
2484
2391
  if (IGNORED_DIRS.has(entry.name)) {
2485
2392
  continue;
2486
2393
  }
2487
- const fullPath = path6.join(currentDir, entry.name);
2488
- const relativePath = path6.relative(rootDir, fullPath).replace(/\\/g, "/");
2394
+ const fullPath = path5.join(currentDir, entry.name);
2395
+ const relativePath = path5.relative(rootDir, fullPath).replace(/\\/g, "/");
2489
2396
  if (entry.isDirectory()) {
2490
2397
  await walkDir(rootDir, fullPath, out);
2491
2398
  continue;
2492
2399
  }
2493
- if (!TEXT_EXTENSIONS.has(path6.extname(entry.name).toLowerCase())) {
2400
+ if (!TEXT_EXTENSIONS.has(path5.extname(entry.name).toLowerCase())) {
2494
2401
  continue;
2495
2402
  }
2496
2403
  out.push({
2497
2404
  key: `path:${relativePath}`,
2498
2405
  label: relativePath,
2499
2406
  path: relativePath,
2500
- content: await fs8.readFile(fullPath, "utf8")
2407
+ content: await fs6.readFile(fullPath, "utf8")
2501
2408
  });
2502
2409
  }
2503
2410
  }
2504
2411
  async function scanWorkspace(workspaceDir) {
2505
- const resolved = path6.resolve(workspaceDir);
2412
+ const resolved = path5.resolve(workspaceDir);
2506
2413
  const project = await loadWorkspaceProject(resolved);
2507
2414
  if (!project) {
2508
2415
  throw new Error("Not an Open Research workspace. Run `open-research init` first.");
@@ -3408,10 +3315,14 @@ var TOOL_SCHEMAS = [
3408
3315
  type: "function",
3409
3316
  function: {
3410
3317
  name: "search_external_sources",
3411
- description: "Search OpenAlex, Semantic Scholar, and arXiv for academic papers. Provide one or more search queries. The first query is the primary search; additional queries are variations to broaden coverage.",
3318
+ description: "Search OpenAlex, Semantic Scholar, and arXiv for academic papers, then fetch content (PDFs, abstracts) and extract structured findings relative to the target. Returns supports/contradicts/related evidence for each source.",
3412
3319
  parameters: {
3413
3320
  type: "object",
3414
3321
  properties: {
3322
+ target: {
3323
+ type: "string",
3324
+ description: "The research claim, hypothesis, or question to evaluate sources against. Each paper will be analyzed for evidence that supports, contradicts, or relates to this target. Be specific: 'What speedups do efficient attention methods achieve' not 'attention'."
3325
+ },
3415
3326
  searches: {
3416
3327
  type: "array",
3417
3328
  items: {
@@ -3430,7 +3341,33 @@ var TOOL_SCHEMAS = [
3430
3341
  description: "Maximum number of results to return. Default: 8."
3431
3342
  }
3432
3343
  },
3433
- required: ["searches"],
3344
+ required: ["target", "searches"],
3345
+ additionalProperties: false
3346
+ }
3347
+ }
3348
+ },
3349
+ {
3350
+ type: "function",
3351
+ function: {
3352
+ name: "web_search",
3353
+ description: "Search the web via DuckDuckGo, fetch top results, and extract structured findings relative to the target. Use for non-academic sources: documentation, blog posts, datasets, reports, news. For academic papers, use search_external_sources.",
3354
+ parameters: {
3355
+ type: "object",
3356
+ properties: {
3357
+ target: {
3358
+ type: "string",
3359
+ description: "The research claim or question to evaluate results against. Be specific: 'How to configure num_workers in PyTorch DataLoader for multi-GPU' not 'PyTorch'."
3360
+ },
3361
+ query: {
3362
+ type: "string",
3363
+ description: "The web search query."
3364
+ },
3365
+ num_results: {
3366
+ type: "number",
3367
+ description: "Maximum pages to fetch and analyze. Default: 5, max: 8."
3368
+ }
3369
+ },
3370
+ required: ["target", "query"],
3434
3371
  additionalProperties: false
3435
3372
  }
3436
3373
  }
@@ -3602,6 +3539,42 @@ var TOOL_SCHEMAS = [
3602
3539
  additionalProperties: false
3603
3540
  }
3604
3541
  }
3542
+ },
3543
+ // ── Ontology ──────────────────────────────────────────────────────────
3544
+ {
3545
+ type: "function",
3546
+ function: {
3547
+ name: "query_ontology",
3548
+ description: "Ask a question about your research knowledge. A query agent traverses the project's ontology (sources, findings, claims, contradictions, evidence chains) and returns a synthesized answer. Use for: finding evidence for/against a claim, checking what contradicts something, getting methodology details, or understanding how findings connect. Returns a natural language answer \u2014 not raw data.",
3549
+ parameters: {
3550
+ type: "object",
3551
+ properties: {
3552
+ query: {
3553
+ type: "string",
3554
+ description: "Natural language research question. Be specific. Good: 'what evidence contradicts the transformer efficiency claim?' Good: 'what methods were used across the scaling studies?' Bad: 'tell me about transformers' (too vague)"
3555
+ },
3556
+ scope: {
3557
+ type: "string",
3558
+ enum: ["claims", "sources", "questions", "methods", "findings", "insights"],
3559
+ description: "Narrow the search to a specific note kind. Omit to search everything."
3560
+ }
3561
+ },
3562
+ required: ["query"],
3563
+ additionalProperties: false
3564
+ }
3565
+ }
3566
+ },
3567
+ {
3568
+ type: "function",
3569
+ function: {
3570
+ name: "ontology_status",
3571
+ description: "Get a snapshot of the research ontology: how many sources, findings, claims, methods, questions, and insights have been captured. Also shows contradiction count, unsupported claims, and open questions. Use to assess coverage, identify gaps, or decide what to investigate next.",
3572
+ parameters: {
3573
+ type: "object",
3574
+ properties: {},
3575
+ additionalProperties: false
3576
+ }
3577
+ }
3605
3578
  }
3606
3579
  ];
3607
3580
  var TOOL_META = {
@@ -3621,7 +3594,10 @@ var TOOL_META = {
3621
3594
  ask_user: { parallelSafe: false },
3622
3595
  create_paper: { parallelSafe: false },
3623
3596
  create_tasks: { parallelSafe: true },
3624
- update_task: { parallelSafe: true }
3597
+ update_task: { parallelSafe: true },
3598
+ web_search: { parallelSafe: true },
3599
+ query_ontology: { parallelSafe: true },
3600
+ ontology_status: { parallelSafe: true }
3625
3601
  };
3626
3602
  function isParallelSafe(toolName) {
3627
3603
  return TOOL_META[toolName]?.parallelSafe ?? false;
@@ -3633,6 +3609,7 @@ var PLANNING_TOOL_NAMES = /* @__PURE__ */ new Set([
3633
3609
  "search_workspace",
3634
3610
  "fetch_url",
3635
3611
  "search_external_sources",
3612
+ "web_search",
3636
3613
  "load_skill",
3637
3614
  "read_skill_reference",
3638
3615
  "ask_user"
@@ -3691,9 +3668,9 @@ Root: ${process.cwd()}`,
3691
3668
  }
3692
3669
 
3693
3670
  // src/lib/agent/tools/read-file.ts
3694
- import fs9 from "fs/promises";
3671
+ import fs7 from "fs/promises";
3695
3672
  import { createReadStream } from "fs";
3696
- import path7 from "path";
3673
+ import path6 from "path";
3697
3674
  import os2 from "os";
3698
3675
  import readline2 from "readline";
3699
3676
  var MAX_OUTPUT_BYTES = 50 * 1024;
@@ -3752,10 +3729,10 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
3752
3729
  ".eot"
3753
3730
  ]);
3754
3731
  function isBinaryByExtension(filePath) {
3755
- return BINARY_EXTENSIONS.has(path7.extname(filePath).toLowerCase());
3732
+ return BINARY_EXTENSIONS.has(path6.extname(filePath).toLowerCase());
3756
3733
  }
3757
3734
  async function isBinaryByContent(filePath) {
3758
- const handle = await fs9.open(filePath, "r");
3735
+ const handle = await fs7.open(filePath, "r");
3759
3736
  try {
3760
3737
  const buf = Buffer.alloc(4096);
3761
3738
  const { bytesRead } = await handle.read(buf, 0, 4096, 0);
@@ -3778,7 +3755,7 @@ function truncateLine(line) {
3778
3755
  }
3779
3756
  function expandHome(filePath) {
3780
3757
  if (filePath === "~") return os2.homedir();
3781
- if (filePath.startsWith("~/")) return path7.join(os2.homedir(), filePath.slice(2));
3758
+ if (filePath.startsWith("~/")) return path6.join(os2.homedir(), filePath.slice(2));
3782
3759
  return filePath;
3783
3760
  }
3784
3761
  async function streamReadFile(resolved, offset, limit) {
@@ -3817,10 +3794,10 @@ async function executeReadFile(args, ctx) {
3817
3794
  if (filePath.startsWith("path:") && filePath in ctx.workspaceFiles) {
3818
3795
  return formatFromString(filePath, ctx.workspaceFiles[filePath], offset, limit);
3819
3796
  }
3820
- const resolved = path7.isAbsolute(filePath) ? filePath : path7.resolve(filePath);
3797
+ const resolved = path6.isAbsolute(filePath) ? filePath : path6.resolve(filePath);
3821
3798
  let stat;
3822
3799
  try {
3823
- stat = await fs9.stat(resolved);
3800
+ stat = await fs7.stat(resolved);
3824
3801
  } catch {
3825
3802
  if (filePath in ctx.workspaceFiles) {
3826
3803
  return formatFromString(filePath, ctx.workspaceFiles[filePath], offset, limit);
@@ -3828,7 +3805,7 @@ async function executeReadFile(args, ctx) {
3828
3805
  return `Error: File not found: ${args.file_path}`;
3829
3806
  }
3830
3807
  if (stat.isDirectory()) {
3831
- const entries = await fs9.readdir(resolved, { withFileTypes: true });
3808
+ const entries = await fs7.readdir(resolved, { withFileTypes: true });
3832
3809
  const lines = entries.sort((a, b) => {
3833
3810
  if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
3834
3811
  return a.name.localeCompare(b.name);
@@ -3892,8 +3869,8 @@ ${outputLines.join("\n")}${footer}
3892
3869
  }
3893
3870
 
3894
3871
  // src/lib/agent/tools/list-directory.ts
3895
- import fs10 from "fs/promises";
3896
- import path8 from "path";
3872
+ import fs8 from "fs/promises";
3873
+ import path7 from "path";
3897
3874
  var MAX_ENTRIES = 200;
3898
3875
  var DEFAULT_IGNORE = /* @__PURE__ */ new Set([
3899
3876
  "node_modules",
@@ -3910,7 +3887,7 @@ var DEFAULT_IGNORE = /* @__PURE__ */ new Set([
3910
3887
  ".env"
3911
3888
  ]);
3912
3889
  async function listEntries(dirPath, ignore) {
3913
- const entries = await fs10.readdir(dirPath, { withFileTypes: true });
3890
+ const entries = await fs8.readdir(dirPath, { withFileTypes: true });
3914
3891
  const results = [];
3915
3892
  for (const entry of entries) {
3916
3893
  if (ignore.has(entry.name) || entry.name.startsWith(".") && DEFAULT_IGNORE.has(entry.name)) {
@@ -3919,7 +3896,7 @@ async function listEntries(dirPath, ignore) {
3919
3896
  let size = 0;
3920
3897
  if (!entry.isDirectory()) {
3921
3898
  try {
3922
- const stat = await fs10.stat(path8.join(dirPath, entry.name));
3899
+ const stat = await fs8.stat(path7.join(dirPath, entry.name));
3923
3900
  size = stat.size;
3924
3901
  } catch {
3925
3902
  }
@@ -3957,7 +3934,7 @@ async function walkTree(rootDir, currentDir, depth, maxDepth, ignore, lines, ind
3957
3934
  if (depth < maxDepth) {
3958
3935
  await walkTree(
3959
3936
  rootDir,
3960
- path8.join(currentDir, entry.name),
3937
+ path7.join(currentDir, entry.name),
3961
3938
  depth + 1,
3962
3939
  maxDepth,
3963
3940
  ignore,
@@ -3973,11 +3950,11 @@ async function walkTree(rootDir, currentDir, depth, maxDepth, ignore, lines, ind
3973
3950
  }
3974
3951
  }
3975
3952
  async function executeListDirectory(args) {
3976
- const dirPath = args.dir_path ? path8.isAbsolute(args.dir_path) ? args.dir_path : path8.resolve(args.dir_path) : process.cwd();
3953
+ const dirPath = args.dir_path ? path7.isAbsolute(args.dir_path) ? args.dir_path : path7.resolve(args.dir_path) : process.cwd();
3977
3954
  const maxDepth = Math.min(args.depth ?? 2, 5);
3978
3955
  const ignore = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...args.ignore ?? []]);
3979
3956
  try {
3980
- const stat = await fs10.stat(dirPath);
3957
+ const stat = await fs8.stat(dirPath);
3981
3958
  if (!stat.isDirectory()) {
3982
3959
  return `Error: ${dirPath} is not a directory.`;
3983
3960
  }
@@ -3992,8 +3969,8 @@ async function executeListDirectory(args) {
3992
3969
 
3993
3970
  // src/lib/agent/tools/run-command.ts
3994
3971
  import { spawn } from "child_process";
3995
- import fs11 from "fs/promises";
3996
- import path9 from "path";
3972
+ import fs9 from "fs/promises";
3973
+ import path8 from "path";
3997
3974
  var DEFAULT_TIMEOUT_MS = 2 * 60 * 1e3;
3998
3975
  var MAX_TIMEOUT_MS = 10 * 60 * 1e3;
3999
3976
  var MAX_OUTPUT_BYTES2 = 50 * 1024;
@@ -4002,9 +3979,9 @@ async function executeRunCommand(args, signal) {
4002
3979
  if (!command.trim()) {
4003
3980
  return "Error: command is required.";
4004
3981
  }
4005
- const workdir = args.workdir ? path9.isAbsolute(args.workdir) ? args.workdir : path9.resolve(args.workdir) : process.cwd();
3982
+ const workdir = args.workdir ? path8.isAbsolute(args.workdir) ? args.workdir : path8.resolve(args.workdir) : process.cwd();
4006
3983
  try {
4007
- const stat = await fs11.stat(workdir);
3984
+ const stat = await fs9.stat(workdir);
4008
3985
  if (!stat.isDirectory()) {
4009
3986
  return `Error: workdir is not a directory: ${workdir}`;
4010
3987
  }
@@ -4128,142 +4105,6 @@ ${stderr}${stderrTruncated ? `
4128
4105
  });
4129
4106
  }
4130
4107
 
4131
- // src/lib/agent/tools/fetch-url.ts
4132
- import { load as loadCheerio } from "cheerio";
4133
- var MAX_RESPONSE_BYTES = 512 * 1024;
4134
- var DEFAULT_TIMEOUT_MS2 = 3e4;
4135
- var MAX_TIMEOUT_MS2 = 12e4;
4136
- var USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
4137
- function htmlToText(html) {
4138
- const $ = loadCheerio(html);
4139
- $("script, style, noscript, nav, footer, header, aside, iframe, svg").remove();
4140
- const title = $("title").first().text().trim();
4141
- const mainEl = $("main").length > 0 ? $("main") : $("article").length > 0 ? $("article") : $("body");
4142
- const sections = [];
4143
- if (title) sections.push(`# ${title}
4144
- `);
4145
- mainEl.find("h1, h2, h3, h4, h5, h6, p, li, td, th, blockquote, pre, dd, dt, figcaption").each((_, el) => {
4146
- const tag = el.tagName?.toLowerCase() ?? "";
4147
- const text = $(el).text().replace(/\s+/g, " ").trim();
4148
- if (!text) return;
4149
- if (tag === "h1") sections.push(`
4150
- # ${text}`);
4151
- else if (tag === "h2") sections.push(`
4152
- ## ${text}`);
4153
- else if (tag === "h3") sections.push(`
4154
- ### ${text}`);
4155
- else if (tag.startsWith("h")) sections.push(`
4156
- #### ${text}`);
4157
- else if (tag === "li") sections.push(`- ${text}`);
4158
- else if (tag === "blockquote") sections.push(`> ${text}`);
4159
- else if (tag === "pre") sections.push(`\`\`\`
4160
- ${text}
4161
- \`\`\``);
4162
- else sections.push(text);
4163
- });
4164
- if (sections.length <= 1) {
4165
- const bodyText = mainEl.text().replace(/\s+/g, " ").trim();
4166
- if (title) return `# ${title}
4167
-
4168
- ${bodyText}`;
4169
- return bodyText;
4170
- }
4171
- return sections.join("\n");
4172
- }
4173
- async function executeFetchUrl(args, signal) {
4174
- const url = args.url.trim();
4175
- if (!url) return "Error: url is required.";
4176
- let parsed;
4177
- try {
4178
- parsed = new URL(url);
4179
- } catch {
4180
- return `Error: Invalid URL: ${url}`;
4181
- }
4182
- if (!["http:", "https:"].includes(parsed.protocol)) {
4183
- return `Error: Only http and https URLs are supported.`;
4184
- }
4185
- const timeout = Math.min(
4186
- Math.max(args.timeout ?? DEFAULT_TIMEOUT_MS2, 5e3),
4187
- MAX_TIMEOUT_MS2
4188
- );
4189
- const format = args.format ?? "text";
4190
- const timeoutController = new AbortController();
4191
- const timer = setTimeout(() => timeoutController.abort(), timeout);
4192
- const combinedSignal = signal ? AbortSignal.any([signal, timeoutController.signal]) : timeoutController.signal;
4193
- try {
4194
- let response = await fetch(url, {
4195
- headers: { "User-Agent": USER_AGENT, Accept: "text/html,application/json,text/plain,*/*" },
4196
- redirect: "follow",
4197
- signal: combinedSignal
4198
- });
4199
- if (response.status === 403 && response.headers.get("cf-mitigated") === "challenge") {
4200
- response = await fetch(url, {
4201
- headers: { "User-Agent": "open-research-cli/0.1", Accept: "text/html,application/json,text/plain,*/*" },
4202
- redirect: "follow",
4203
- signal: combinedSignal
4204
- });
4205
- }
4206
- if (!response.ok) {
4207
- return `Error: HTTP ${response.status} ${response.statusText}`;
4208
- }
4209
- const finalUrl = response.url;
4210
- const redirectNote = finalUrl && finalUrl !== url ? `(Redirected to: ${finalUrl})
4211
-
4212
- ` : "";
4213
- const contentType = response.headers.get("content-type") ?? "";
4214
- if (contentType.includes("image/") || contentType.includes("audio/") || contentType.includes("video/") || contentType.includes("application/octet-stream") || contentType.includes("application/zip")) {
4215
- const length = response.headers.get("content-length");
4216
- return `${redirectNote}Binary content: ${contentType}${length ? ` (${(Number(length) / 1024).toFixed(1)} KB)` : ""}. Use run_command with curl to download.`;
4217
- }
4218
- const reader = response.body?.getReader();
4219
- if (!reader) return "Error: No response body.";
4220
- const chunks = [];
4221
- let totalBytes = 0;
4222
- let truncated = false;
4223
- while (true) {
4224
- const { done, value } = await reader.read();
4225
- if (done) break;
4226
- if (totalBytes + value.length > MAX_RESPONSE_BYTES) {
4227
- const remaining = MAX_RESPONSE_BYTES - totalBytes;
4228
- if (remaining > 0) chunks.push(value.subarray(0, remaining));
4229
- totalBytes = MAX_RESPONSE_BYTES;
4230
- truncated = true;
4231
- reader.cancel();
4232
- break;
4233
- }
4234
- chunks.push(value);
4235
- totalBytes += value.length;
4236
- }
4237
- const raw = new TextDecoder().decode(
4238
- chunks.length === 1 ? chunks[0] : Buffer.concat(chunks)
4239
- );
4240
- const truncSuffix = truncated ? "\n\n(Response truncated to 512 KB)" : "";
4241
- if (contentType.includes("application/json")) {
4242
- try {
4243
- const obj = JSON.parse(raw);
4244
- return redirectNote + JSON.stringify(obj, null, 2) + truncSuffix;
4245
- } catch {
4246
- }
4247
- }
4248
- if (contentType.includes("text/html")) {
4249
- if (format === "html") {
4250
- return redirectNote + raw + truncSuffix;
4251
- }
4252
- const text = htmlToText(raw);
4253
- return redirectNote + text + truncSuffix;
4254
- }
4255
- return redirectNote + raw + truncSuffix;
4256
- } catch (err) {
4257
- if (signal?.aborted) return "Fetch aborted by user.";
4258
- if (err instanceof Error && err.name === "AbortError") {
4259
- return `Fetch timed out after ${(timeout / 1e3).toFixed(0)}s.`;
4260
- }
4261
- return `Fetch error: ${err instanceof Error ? err.message : String(err)}`;
4262
- } finally {
4263
- clearTimeout(timer);
4264
- }
4265
- }
4266
-
4267
4108
  // src/lib/agent/tools/ask-user.ts
4268
4109
  var pendingQuestions = [];
4269
4110
  function getPendingQuestion() {
@@ -4549,8 +4390,8 @@ function executeCreatePaper(args, ctx) {
4549
4390
  }
4550
4391
 
4551
4392
  // src/lib/agent/tools/read-pdf.ts
4552
- import path10 from "path";
4553
- import fs12 from "fs/promises";
4393
+ import path9 from "path";
4394
+ import fs10 from "fs/promises";
4554
4395
  var MAX_OUTPUT_BYTES3 = 5e4;
4555
4396
  function parsePages(pages) {
4556
4397
  const range = pages.match(/^(\d+)\s*-\s*(\d+)$/);
@@ -4560,8 +4401,8 @@ function parsePages(pages) {
4560
4401
  return null;
4561
4402
  }
4562
4403
  async function executeReadPdf(args) {
4563
- const resolved = path10.resolve(args.file_path);
4564
- const stat = await fs12.stat(resolved).catch(() => null);
4404
+ const resolved = path9.resolve(args.file_path);
4405
+ const stat = await fs10.stat(resolved).catch(() => null);
4565
4406
  if (!stat || !stat.isFile()) {
4566
4407
  return `Error: file not found: ${resolved}`;
4567
4408
  }
@@ -5381,6 +5222,7 @@ function normalizeArxivPaper(paper) {
5381
5222
  title: paper.title,
5382
5223
  url: sourceUrl ?? pdfUrl ?? "",
5383
5224
  pdfUrl,
5225
+ abstract: paper.summary?.trim() || void 0,
5384
5226
  publishedDate: normalizeDatePrefix(paper.published),
5385
5227
  author: paper.authors[0],
5386
5228
  provider: "arxiv",
@@ -5662,11 +5504,14 @@ async function discoverScholarlySources({
5662
5504
  }
5663
5505
 
5664
5506
  // src/lib/agent/search-external-sources.ts
5665
- async function executeSearchExternalSources(args, ctx) {
5507
+ async function executeSearchExternalSources(args, ctx, provider) {
5666
5508
  const searches = args.searches ?? [];
5667
5509
  if (searches.length === 0) {
5668
5510
  return { result: "Error: no search queries provided.", sources: [] };
5669
5511
  }
5512
+ if (!args.target) {
5513
+ return { result: "Error: target is required. Specify what information you need from the papers.", sources: [] };
5514
+ }
5670
5515
  const primary = searches[0];
5671
5516
  const variations = searches.slice(1).map((item) => item.query);
5672
5517
  const config = await loadOpenResearchConfig().catch(() => null);
@@ -5678,16 +5523,74 @@ async function executeSearchExternalSources(args, ctx) {
5678
5523
  semanticScholarApiKey: getSemanticScholarApiKey(config),
5679
5524
  openAlexApiKey: getOpenAlexApiKey(config)
5680
5525
  });
5681
- const summary = results.map((result, index) => `${index + 1}. ${result.title} [${result.provider}] ${result.url}`).join("\n");
5526
+ if (results.length === 0) {
5527
+ return { result: "No papers found.", sources: [] };
5528
+ }
5529
+ if (!provider) {
5530
+ const summary = results.map((r, i) => `${i + 1}. ${r.title} [${r.provider}] ${r.url}`).join("\n");
5531
+ return { result: summary || "No papers found.", sources: results };
5532
+ }
5533
+ const contentResults = await Promise.allSettled(
5534
+ results.map(async (source2) => {
5535
+ if (source2.abstract) {
5536
+ return { source: source2, text: source2.abstract };
5537
+ }
5538
+ if (source2.pdfUrl) {
5539
+ const content = await fetchAndParseContent(source2.pdfUrl);
5540
+ if (content) return { source: source2, text: content.text };
5541
+ }
5542
+ if (source2.url) {
5543
+ const content = await fetchAndParseContent(source2.url);
5544
+ if (content) return { source: source2, text: content.text };
5545
+ }
5546
+ return null;
5547
+ })
5548
+ );
5549
+ const extractionInputs = [];
5550
+ const sourceMap = /* @__PURE__ */ new Map();
5551
+ for (const result of contentResults) {
5552
+ if (result.status === "fulfilled" && result.value) {
5553
+ const { source: source2, text } = result.value;
5554
+ extractionInputs.push({
5555
+ title: source2.title,
5556
+ content: text,
5557
+ url: source2.url,
5558
+ target: args.target
5559
+ });
5560
+ sourceMap.set(source2.url, source2);
5561
+ }
5562
+ }
5563
+ if (extractionInputs.length === 0) {
5564
+ const summary = results.map((r, i) => `${i + 1}. ${r.title} [${r.provider}] ${r.url}`).join("\n");
5565
+ return {
5566
+ result: `Could not fetch content from any papers. Metadata only:
5567
+ ${summary}`,
5568
+ sources: results
5569
+ };
5570
+ }
5571
+ const extractions = await extractBatch(extractionInputs, provider);
5572
+ const extracted = [];
5573
+ for (const [url, extraction] of extractions) {
5574
+ const source2 = sourceMap.get(url);
5575
+ if (source2 && extraction.relevanceScore >= 2) {
5576
+ extracted.push({
5577
+ title: source2.title,
5578
+ url: source2.url,
5579
+ provider: source2.provider,
5580
+ extraction
5581
+ });
5582
+ }
5583
+ }
5584
+ const formatted = formatExtractionResults(extracted);
5682
5585
  return {
5683
- result: summary || "No external sources found.",
5586
+ result: formatted,
5684
5587
  sources: results
5685
5588
  };
5686
5589
  }
5687
5590
 
5688
5591
  // src/lib/skills/runtime.ts
5689
- import fs13 from "fs/promises";
5690
- import path11 from "path";
5592
+ import fs11 from "fs/promises";
5593
+ import path10 from "path";
5691
5594
  import matter2 from "gray-matter";
5692
5595
  async function loadRuntimeSkillByName(input2) {
5693
5596
  const skills2 = await listAvailableSkills({ homeDir: input2.homeDir });
@@ -5695,7 +5598,7 @@ async function loadRuntimeSkillByName(input2) {
5695
5598
  if (!match) {
5696
5599
  return null;
5697
5600
  }
5698
- const raw = await fs13.readFile(path11.join(match.skillDir, "SKILL.md"), "utf8");
5601
+ const raw = await fs11.readFile(path10.join(match.skillDir, "SKILL.md"), "utf8");
5699
5602
  const parsed = matter2(raw);
5700
5603
  return {
5701
5604
  id: match.name,
@@ -5706,8 +5609,8 @@ async function loadRuntimeSkillByName(input2) {
5706
5609
  };
5707
5610
  }
5708
5611
  async function readSkillReferenceFile(skillDir, referencePath) {
5709
- const fullPath = path11.join(skillDir, "references", referencePath);
5710
- return fs13.readFile(fullPath, "utf8");
5612
+ const fullPath = path10.join(skillDir, "references", referencePath);
5613
+ return fs11.readFile(fullPath, "utf8");
5711
5614
  }
5712
5615
 
5713
5616
  // src/lib/agent/subagent/configs.ts
@@ -5912,8 +5815,8 @@ function describeSubAgentTool(name, args) {
5912
5815
  }
5913
5816
 
5914
5817
  // src/lib/agent/tools/tasks.ts
5915
- import path12 from "path";
5916
- import fs14 from "fs/promises";
5818
+ import path11 from "path";
5819
+ import fs12 from "fs/promises";
5917
5820
  var tasks = [];
5918
5821
  var storePath = null;
5919
5822
  function shortId() {
@@ -5923,12 +5826,12 @@ async function persist() {
5923
5826
  if (!storePath) return;
5924
5827
  const live = tasks.filter((t) => t.status !== "deleted");
5925
5828
  const tmpPath = storePath + ".tmp";
5926
- await fs14.mkdir(path12.dirname(storePath), { recursive: true });
5927
- await fs14.writeFile(tmpPath, JSON.stringify({ version: 1, tasks: live }, null, 2));
5928
- await fs14.rename(tmpPath, storePath);
5829
+ await fs12.mkdir(path11.dirname(storePath), { recursive: true });
5830
+ await fs12.writeFile(tmpPath, JSON.stringify({ version: 1, tasks: live }, null, 2));
5831
+ await fs12.rename(tmpPath, storePath);
5929
5832
  }
5930
5833
  async function initTaskStore(workspaceDir) {
5931
- storePath = path12.join(workspaceDir, ".open-research", "tasks.json");
5834
+ storePath = path11.join(workspaceDir, ".open-research", "tasks.json");
5932
5835
  const data = await readJsonFile(storePath, { version: 1, tasks: [] });
5933
5836
  tasks = data.tasks ?? [];
5934
5837
  }
@@ -6051,10 +5954,19 @@ async function executeTool(name, args, ctx, activeSkills, homeDir, signal, provi
6051
5954
  case "search_external_sources": {
6052
5955
  const out = await executeSearchExternalSources(
6053
5956
  args,
6054
- ctx
5957
+ ctx,
5958
+ provider
6055
5959
  );
6056
5960
  return { result: out.result, searchResults: out.sources };
6057
5961
  }
5962
+ case "web_search": {
5963
+ const { executeWebSearch } = await import("./web-search-B7D5WMHU.js");
5964
+ const out = await executeWebSearch(
5965
+ args,
5966
+ provider
5967
+ );
5968
+ return { result: out.result };
5969
+ }
6058
5970
  // ── User Interaction ────────────────────────────────────────────────
6059
5971
  case "ask_user":
6060
5972
  return {
@@ -6122,6 +6034,28 @@ ${meta}` };
6122
6034
  return {
6123
6035
  result: executeUpdateTask(args)
6124
6036
  };
6037
+ // ── Ontology ──────────────────────────────────────────────────────────
6038
+ case "query_ontology": {
6039
+ if (!provider) {
6040
+ return { result: "Error: query_ontology requires an LLM provider." };
6041
+ }
6042
+ const { runQueryAgent } = await import("./query-agent-LRUUJR4F.js");
6043
+ const workspaceDir = ctx.workspaceDir ?? process.cwd();
6044
+ const answer = await runQueryAgent({
6045
+ query: String(args.query ?? ""),
6046
+ scope: args.scope ? String(args.scope) : void 0,
6047
+ provider,
6048
+ workspaceDir
6049
+ });
6050
+ return { result: answer };
6051
+ }
6052
+ case "ontology_status": {
6053
+ const { loadOntology } = await import("./store-LT5EGDOI.js");
6054
+ const { getOntologyStatus } = await import("./status-GEEAGLPF.js");
6055
+ const workspaceDir = ctx.workspaceDir ?? process.cwd();
6056
+ const ontology = await loadOntology(workspaceDir);
6057
+ return { result: getOntologyStatus(ontology) };
6058
+ }
6125
6059
  default:
6126
6060
  return { result: `Unknown tool: "${name}"` };
6127
6061
  }
@@ -6381,17 +6315,17 @@ async function manualCompact(messages, model, provider, usage, customInstruction
6381
6315
  }
6382
6316
 
6383
6317
  // src/lib/memory/store.ts
6384
- import fs15 from "fs/promises";
6385
- import path13 from "path";
6318
+ import fs13 from "fs/promises";
6319
+ import path12 from "path";
6386
6320
  function getGlobalMemoryFile(options) {
6387
- return path13.join(getOpenResearchRoot(options), "memory.json");
6321
+ return path12.join(getOpenResearchRoot(options), "memory.json");
6388
6322
  }
6389
6323
  function getProjectMemoryFile(workspaceDir) {
6390
- return path13.join(workspaceDir, ".open-research", "memory.json");
6324
+ return path12.join(workspaceDir, ".open-research", "memory.json");
6391
6325
  }
6392
6326
  async function loadMemoryFile(filePath) {
6393
6327
  try {
6394
- const raw = await fs15.readFile(filePath, "utf8");
6328
+ const raw = await fs13.readFile(filePath, "utf8");
6395
6329
  const store = JSON.parse(raw);
6396
6330
  return store.memories ?? [];
6397
6331
  } catch {
@@ -6399,9 +6333,9 @@ async function loadMemoryFile(filePath) {
6399
6333
  }
6400
6334
  }
6401
6335
  async function saveMemoryFile(filePath, memories) {
6402
- await fs15.mkdir(path13.dirname(filePath), { recursive: true });
6336
+ await fs13.mkdir(path12.dirname(filePath), { recursive: true });
6403
6337
  const store = { version: 2, memories };
6404
- await fs15.writeFile(filePath, JSON.stringify(store, null, 2), "utf8");
6338
+ await fs13.writeFile(filePath, JSON.stringify(store, null, 2), "utf8");
6405
6339
  }
6406
6340
  async function loadGlobalMemories(options) {
6407
6341
  const mems = await loadMemoryFile(getGlobalMemoryFile(options));
@@ -6437,7 +6371,7 @@ function evictIfNeeded(memories) {
6437
6371
  memories.length = MAX_MEMORIES_PER_STORE;
6438
6372
  }
6439
6373
  async function addMemory(memory, options) {
6440
- const scope = memory.scope ?? (memory.category === "project" || memory.category === "context" ? "project" : "global");
6374
+ const scope = memory.scope ?? (memory.category === "context" ? "project" : "global");
6441
6375
  const filePath = scope === "project" && options?.workspaceDir ? getProjectMemoryFile(options.workspaceDir) : getGlobalMemoryFile(options);
6442
6376
  const memories = await loadMemoryFile(filePath);
6443
6377
  const existing = findDuplicate(memories, memory.content);
@@ -6630,12 +6564,17 @@ CRITICAL RULES:
6630
6564
  - Maximum 3 actions per exchange
6631
6565
  - If nothing meaningful to remember, return an empty array
6632
6566
 
6567
+ IMPORTANT \u2014 what NOT to store as memories:
6568
+ - Research findings, claims, hypotheses, evidence, or source details \u2014 these belong in the ontology, not memory
6569
+ - Specific paper results, data points, or analytical conclusions \u2014 ontology handles these
6570
+ - Connections between findings, contradictions, or evidence chains \u2014 ontology handles these
6571
+ Memory is for WHO the researcher is and HOW they work. The ontology handles WHAT they know.
6572
+
6633
6573
  Categories:
6634
6574
  - "user" \u2014 identity, role, field, institution (\u2192 stored globally)
6635
- - "preference" \u2014 tools, style, methodology preferences (\u2192 stored globally)
6636
- - "project" \u2014 current research topics, findings, hypotheses (\u2192 stored per-project)
6637
- - "methodology" \u2014 statistical approaches, frameworks (\u2192 stored globally)
6638
- - "context" \u2014 deadlines, collaborators, constraints (\u2192 stored per-project)
6575
+ - "preference" \u2014 tools, citation style, writing preferences (\u2192 stored globally)
6576
+ - "methodology" \u2014 statistical approaches, frameworks, analytical habits (\u2192 stored globally)
6577
+ - "context" \u2014 deadlines, collaborators, target venues, project constraints (\u2192 stored per-project)
6639
6578
 
6640
6579
  Existing memories:
6641
6580
  {EXISTING_MEMORIES}
@@ -6681,7 +6620,7 @@ async function extractMemories(input2) {
6681
6620
  if (!Array.isArray(parsed)) return [];
6682
6621
  const valid = [];
6683
6622
  for (const item of parsed) {
6684
- if (typeof item.content === "string" && item.content.length > 5 && ["user", "preference", "project", "methodology", "context"].includes(item.category) && ["create", "update"].includes(item.action)) {
6623
+ if (typeof item.content === "string" && item.content.length > 5 && ["user", "preference", "methodology", "context"].includes(item.category) && ["create", "update"].includes(item.action)) {
6685
6624
  valid.push({
6686
6625
  action: item.action,
6687
6626
  content: item.content,
@@ -6716,7 +6655,7 @@ async function extractAndStoreMemories(input2) {
6716
6655
  results.push(saved);
6717
6656
  }
6718
6657
  } else {
6719
- const scope = action.category === "project" || action.category === "context" ? "project" : "global";
6658
+ const scope = action.category === "context" ? "project" : "global";
6720
6659
  const saved = await addMemory(
6721
6660
  { content: action.content, category: action.category, scope },
6722
6661
  { homeDir: input2.homeDir, workspaceDir: input2.workspaceDir }
@@ -6728,18 +6667,18 @@ async function extractAndStoreMemories(input2) {
6728
6667
  }
6729
6668
 
6730
6669
  // src/lib/workspace/agents-md.ts
6731
- import fs16 from "fs/promises";
6732
- import path14 from "path";
6670
+ import fs14 from "fs/promises";
6671
+ import path13 from "path";
6733
6672
  var AGENTS_MD_FILENAME = "AGENTS.md";
6734
6673
  async function readAgentsMd(workspaceDir) {
6735
6674
  try {
6736
- return await fs16.readFile(path14.join(workspaceDir, AGENTS_MD_FILENAME), "utf8");
6675
+ return await fs14.readFile(path13.join(workspaceDir, AGENTS_MD_FILENAME), "utf8");
6737
6676
  } catch {
6738
6677
  return "";
6739
6678
  }
6740
6679
  }
6741
6680
  async function writeAgentsMd(workspaceDir, content) {
6742
- await fs16.writeFile(path14.join(workspaceDir, AGENTS_MD_FILENAME), content, "utf8");
6681
+ await fs14.writeFile(path13.join(workspaceDir, AGENTS_MD_FILENAME), content, "utf8");
6743
6682
  }
6744
6683
  var UPDATE_SYSTEM_PROMPT = `You maintain an AGENTS.md file for a research workspace. This file gives future agent sessions instant context about the project \u2014 what it's about, what's been done, key files, and current direction.
6745
6684
 
@@ -6817,7 +6756,14 @@ var TOOL_DESCRIPTIONS = {
6817
6756
  return `Fetching URL`;
6818
6757
  }
6819
6758
  },
6820
- search_external_sources: () => "Searching academic papers",
6759
+ search_external_sources: (a) => {
6760
+ const target = a.target;
6761
+ return target ? `Searching papers: "${target.slice(0, 50)}"` : "Searching academic papers";
6762
+ },
6763
+ web_search: (a) => {
6764
+ const query = a.query;
6765
+ return `Web search: "${query?.slice(0, 50) ?? ""}"`;
6766
+ },
6821
6767
  ask_user: (a) => `Asking: ${a.question?.slice(0, 50) ?? "question"}`,
6822
6768
  load_skill: (a) => `Loading skill: ${a.skill_id ?? ""}`,
6823
6769
  read_skill_reference: (a) => `Reading skill reference: ${a.path ?? ""}`,
@@ -6884,8 +6830,9 @@ ${skill.prompt}`).join("\n\n");
6884
6830
  "- `read_file` / `list_directory` / `search_workspace` \u2014 explore and read",
6885
6831
  "- `run_command` \u2014 execute python, R, node, LaTeX, curl, git, etc.",
6886
6832
  "- `write_new_file` / `update_existing_file` \u2014 create and edit workspace files",
6887
- "- `search_external_sources` \u2014 search OpenAlex, Semantic Scholar, arXiv",
6888
- "- `fetch_url` \u2014 fetch web pages and API responses",
6833
+ "- `search_external_sources` \u2014 search academic papers and extract evidence for/against a target (arXiv, Semantic Scholar, OpenAlex)",
6834
+ "- `web_search` \u2014 search the web and extract evidence for/against a target (docs, blogs, datasets, news)",
6835
+ "- `fetch_url` \u2014 fetch a specific URL when you already know the address",
6889
6836
  "- `ask_user` \u2014 ask clarifying questions when genuinely needed",
6890
6837
  "- `load_skill` \u2014 activate specialized research workflows",
6891
6838
  "- `launch_subagent` \u2014 delegate exploration to a lightweight sub-agent that runs on its own context window",
@@ -6944,11 +6891,28 @@ async function runAgentTurn(input2) {
6944
6891
  const memoryBlock = formatMemoriesForPrompt(relevantMemories);
6945
6892
  const agentsMd = input2.workspace.workspaceDir ? await readAgentsMd(input2.workspace.workspaceDir).catch(() => "") : "";
6946
6893
  const taskBlock = getTaskContextBlock();
6894
+ let ontologyBlock = null;
6895
+ if (input2.workspace.workspaceDir) {
6896
+ try {
6897
+ const { loadOntology } = await import("./store-LT5EGDOI.js");
6898
+ const { runRelevanceAgent } = await import("./relevance-agent-CCN7JGTM.js");
6899
+ const { buildScaffoldingContext } = await import("./scaffolding-MSAICMWV.js");
6900
+ const ontology = await loadOntology(input2.workspace.workspaceDir);
6901
+ const relevantIds = await runRelevanceAgent({
6902
+ userMessage: input2.message,
6903
+ ontology,
6904
+ provider: input2.provider
6905
+ });
6906
+ ontologyBlock = buildScaffoldingContext(ontology, relevantIds);
6907
+ } catch {
6908
+ }
6909
+ }
6947
6910
  const fullSystemPrompt = [
6948
6911
  systemPrompt,
6949
6912
  memoryBlock || null,
6950
6913
  agentsMd ? `## Project Context (from AGENTS.md)
6951
6914
  ${agentsMd}` : null,
6915
+ ontologyBlock || null,
6952
6916
  taskBlock || null
6953
6917
  ].filter(Boolean).join("\n\n");
6954
6918
  let messages = [
@@ -6958,6 +6922,7 @@ ${agentsMd}` : null,
6958
6922
  ];
6959
6923
  const proposedUpdates = [];
6960
6924
  const searchResults = [];
6925
+ const collectedToolOutputs = [];
6961
6926
  const signal = input2.signal;
6962
6927
  for (; ; ) {
6963
6928
  if (signal?.aborted) break;
@@ -7030,6 +6995,22 @@ ${agentsMd}` : null,
7030
6995
  }).catch(() => {
7031
6996
  });
7032
6997
  }
6998
+ if (input2.workspace.workspaceDir) {
6999
+ import("./manager-queue-F4VVZMTE.js").then(({ enqueueOntologyManager }) => {
7000
+ enqueueOntologyManager({
7001
+ userMessage: input2.message,
7002
+ agentResponse: fullText,
7003
+ toolOutputs: collectedToolOutputs,
7004
+ sessionId: "",
7005
+ // optional tracking
7006
+ turnIndex: 0,
7007
+ provider: input2.provider,
7008
+ workspaceDir: input2.workspace.workspaceDir,
7009
+ onOntologyUpdated: input2.onOntologyUpdated
7010
+ });
7011
+ }).catch(() => {
7012
+ });
7013
+ }
7033
7014
  return {
7034
7015
  text: fullText,
7035
7016
  proposedUpdates,
@@ -7137,6 +7118,11 @@ ${agentsMd}` : null,
7137
7118
  tool_call_id: toolCall.id,
7138
7119
  content: result.result
7139
7120
  });
7121
+ collectedToolOutputs.push({
7122
+ tool: toolCall.name,
7123
+ input: toolCall.arguments,
7124
+ output: result.result
7125
+ });
7140
7126
  }
7141
7127
  }
7142
7128
  }
@@ -7169,8 +7155,8 @@ function classifyUpdateRisk(update) {
7169
7155
  }
7170
7156
 
7171
7157
  // src/lib/workspace/apply-update.ts
7172
- import fs17 from "fs/promises";
7173
- import path15 from "path";
7158
+ import fs15 from "fs/promises";
7159
+ import path14 from "path";
7174
7160
  function resolveRelativePath(update) {
7175
7161
  if (update.key.startsWith("path:")) {
7176
7162
  return update.key.slice(5);
@@ -7191,9 +7177,9 @@ function resolveRelativePath(update) {
7191
7177
  }
7192
7178
  async function applyProposedUpdate(workspaceDir, update) {
7193
7179
  const relativePath = resolveRelativePath(update);
7194
- const absolutePath = path15.join(workspaceDir, relativePath);
7195
- await fs17.mkdir(path15.dirname(absolutePath), { recursive: true });
7196
- await fs17.writeFile(absolutePath, update.content, "utf8");
7180
+ const absolutePath = path14.join(workspaceDir, relativePath);
7181
+ await fs15.mkdir(path14.dirname(absolutePath), { recursive: true });
7182
+ await fs15.writeFile(absolutePath, update.content, "utf8");
7197
7183
  return absolutePath;
7198
7184
  }
7199
7185
 
@@ -7411,15 +7397,15 @@ function SessionPicker({ sessions, onSelect, onCancel }) {
7411
7397
  }
7412
7398
 
7413
7399
  // src/lib/cli/update-check.ts
7414
- import fs18 from "fs/promises";
7415
- import path16 from "path";
7400
+ import fs16 from "fs/promises";
7401
+ import path15 from "path";
7416
7402
  import os3 from "os";
7417
7403
  var PACKAGE_NAME = "open-research";
7418
7404
  var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
7419
- var STATE_FILE = path16.join(os3.homedir(), ".open-research", "update-check.json");
7405
+ var STATE_FILE = path15.join(os3.homedir(), ".open-research", "update-check.json");
7420
7406
  async function readState() {
7421
7407
  try {
7422
- const raw = await fs18.readFile(STATE_FILE, "utf8");
7408
+ const raw = await fs16.readFile(STATE_FILE, "utf8");
7423
7409
  const parsed = JSON.parse(raw);
7424
7410
  return {
7425
7411
  lastCheck: typeof parsed.lastCheck === "number" ? parsed.lastCheck : 0,
@@ -7431,8 +7417,8 @@ async function readState() {
7431
7417
  }
7432
7418
  }
7433
7419
  async function writeState(state) {
7434
- await fs18.mkdir(path16.dirname(STATE_FILE), { recursive: true });
7435
- await fs18.writeFile(STATE_FILE, JSON.stringify(state), "utf8");
7420
+ await fs16.mkdir(path15.dirname(STATE_FILE), { recursive: true });
7421
+ await fs16.writeFile(STATE_FILE, JSON.stringify(state), "utf8");
7436
7422
  }
7437
7423
  function getCurrentVersion() {
7438
7424
  return getPackageVersion();
@@ -7506,6 +7492,7 @@ var SLASH_COMMANDS = [
7506
7492
  { name: "doctor", aliases: [], description: "Diagnose auth, connectivity, and tool availability", category: "system" },
7507
7493
  { name: "preview", aliases: [], description: "Live preview a LaTeX file in browser (e.g. /preview papers/draft.tex)", category: "workspace" },
7508
7494
  { name: "memory", aliases: ["/memories"], description: "View or clear stored memories about you", category: "system" },
7495
+ { name: "ontology", aliases: ["/onto"], description: "View or manage the research ontology", category: "workspace" },
7509
7496
  { name: "exit", aliases: ["/quit", "/q"], description: "Exit Open Research", category: "system" }
7510
7497
  ];
7511
7498
  function matchSlashCommand(input2) {
@@ -7524,7 +7511,8 @@ function matchSlashCommand(input2) {
7524
7511
  var SUBCOMMAND_HINTS = {
7525
7512
  "api-keys": [
7526
7513
  { name: "api-keys semantic-scholar <key>", description: "Set your Semantic Scholar API key" },
7527
- { name: "api-keys openalex <key>", description: "Set your OpenAlex API key" }
7514
+ { name: "api-keys openalex <key>", description: "Set your OpenAlex API key" },
7515
+ { name: "api-keys brave <key>", description: "Set Brave Search API key for better web search" }
7528
7516
  ],
7529
7517
  "config": [
7530
7518
  { name: "config theme dark|light", description: "Set color theme" },
@@ -8048,60 +8036,64 @@ function HomeScreen({
8048
8036
  hasWorkspace,
8049
8037
  fileCount,
8050
8038
  skillCount,
8039
+ version,
8040
+ model,
8041
+ contextWindow,
8042
+ workspacePath,
8051
8043
  width
8052
8044
  }) {
8053
8045
  const theme = useTheme();
8054
8046
  const contentWidth = resolveWidth(width);
8055
8047
  const bodyWidth = indentedWidth(contentWidth);
8048
+ const ctxLabel = contextWindow >= 1e3 ? `${Math.round(contextWindow / 1e3)}k` : String(contextWindow);
8049
+ const shortPath = workspacePath ? workspacePath.replace(process.env.HOME ?? "", "~") : process.cwd().replace(process.env.HOME ?? "", "~");
8056
8050
  return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginBottom: 1, width: contentWidth, children: [
8057
- /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginBottom: 1, width: contentWidth, children: [
8051
+ /* @__PURE__ */ jsxs3(Box4, { width: contentWidth, children: [
8052
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: theme.accent, children: "\u26A1 " }),
8058
8053
  /* @__PURE__ */ jsx4(Text4, { bold: true, color: theme.accent, children: "Open Research" }),
8059
- /* @__PURE__ */ jsx4(Text4, { color: theme.muted, dimColor: true, children: "Local-first research agent" })
8060
- ] }),
8061
- !hasAuth && /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", width: contentWidth, children: [
8062
- /* @__PURE__ */ jsxs3(Box4, { width: contentWidth, children: [
8063
- /* @__PURE__ */ jsxs3(Text4, { color: theme.warning, children: [
8064
- GUTTER.pending,
8065
- " "
8066
- ] }),
8067
- /* @__PURE__ */ jsx4(Text4, { color: theme.warning, children: "Connect or add OpenAI credentials to get started" })
8054
+ /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, children: [
8055
+ " v",
8056
+ version
8068
8057
  ] }),
8069
- /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, width: bodyWidth, children: /* @__PURE__ */ jsx4(Text4, { color: theme.muted, wrap: "wrap", children: "/config apikey sk-... \xB7 /auth \u2014 browser login \xB7 /auth-codex \u2014 import existing session" }) })
8070
- ] }),
8071
- hasAuth && !hasWorkspace && /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", width: contentWidth, children: [
8072
- /* @__PURE__ */ jsxs3(Box4, { width: contentWidth, children: [
8073
- /* @__PURE__ */ jsxs3(Text4, { color: theme.secondary, children: [
8074
- GUTTER.success,
8075
- " "
8076
- ] }),
8077
- /* @__PURE__ */ jsx4(Text4, { color: theme.secondary, children: "Connected" })
8058
+ /* @__PURE__ */ jsx4(Text4, { color: theme.muted, dimColor: true, children: " | " }),
8059
+ /* @__PURE__ */ jsxs3(Text4, { color: theme.text, children: [
8060
+ "\u25C6 ",
8061
+ model
8078
8062
  ] }),
8063
+ /* @__PURE__ */ jsx4(Text4, { color: theme.muted, dimColor: true, children: " | " }),
8064
+ /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, children: [
8065
+ ctxLabel,
8066
+ " context"
8067
+ ] })
8068
+ ] }),
8069
+ /* @__PURE__ */ jsxs3(Box4, { marginLeft: 2, width: bodyWidth, children: [
8070
+ /* @__PURE__ */ jsx4(Text4, { color: theme.muted, dimColor: true, children: shortPath }),
8071
+ hasWorkspace && /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, children: [
8072
+ " \xB7 ",
8073
+ fileCount,
8074
+ " files \xB7 ",
8075
+ skillCount,
8076
+ " skills"
8077
+ ] })
8078
+ ] }),
8079
+ !hasAuth && /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginTop: 1, width: contentWidth, children: [
8079
8080
  /* @__PURE__ */ jsxs3(Box4, { width: contentWidth, children: [
8080
8081
  /* @__PURE__ */ jsxs3(Text4, { color: theme.warning, children: [
8081
8082
  GUTTER.pending,
8082
8083
  " "
8083
8084
  ] }),
8084
- /* @__PURE__ */ jsx4(Text4, { color: theme.warning, children: "Create a workspace to begin" })
8085
+ /* @__PURE__ */ jsx4(Text4, { color: theme.warning, children: "Connect OpenAI to get started" })
8085
8086
  ] }),
8086
- /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, width: bodyWidth, children: /* @__PURE__ */ jsx4(Text4, { color: theme.muted, wrap: "wrap", children: "/init \u2014 initialize in current directory" }) })
8087
+ /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, width: bodyWidth, children: /* @__PURE__ */ jsx4(Text4, { color: theme.muted, wrap: "wrap", children: "/config apikey sk-... \xB7 /auth \xB7 /auth-codex" }) })
8087
8088
  ] }),
8088
- hasAuth && hasWorkspace && /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", width: contentWidth, children: [
8089
- /* @__PURE__ */ jsxs3(Box4, { width: contentWidth, children: [
8090
- /* @__PURE__ */ jsxs3(Text4, { color: theme.secondary, children: [
8091
- GUTTER.active,
8092
- " "
8093
- ] }),
8094
- /* @__PURE__ */ jsx4(Text4, { color: theme.secondary, children: "Ready" }),
8095
- /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, children: [
8096
- " \u2014 ",
8097
- fileCount,
8098
- " files \xB7 ",
8099
- skillCount,
8100
- " skills"
8101
- ] })
8089
+ hasAuth && !hasWorkspace && /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", marginTop: 1, width: contentWidth, children: /* @__PURE__ */ jsxs3(Box4, { width: contentWidth, children: [
8090
+ /* @__PURE__ */ jsxs3(Text4, { color: theme.warning, children: [
8091
+ GUTTER.pending,
8092
+ " "
8102
8093
  ] }),
8103
- /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, width: bodyWidth, children: /* @__PURE__ */ jsx4(Text4, { color: theme.muted, wrap: "wrap", children: "Ask a question, @mention a file, or /help for commands" }) })
8104
- ] })
8094
+ /* @__PURE__ */ jsx4(Text4, { color: theme.warning, children: "Run /init to create a workspace" })
8095
+ ] }) }),
8096
+ hasAuth && hasWorkspace && /* @__PURE__ */ jsx4(Box4, { marginTop: 1, width: contentWidth, children: /* @__PURE__ */ jsx4(Text4, { color: theme.muted, dimColor: true, children: "Ask a question, @mention a file, or type /help" }) })
8105
8097
  ] });
8106
8098
  }
8107
8099
  function SuggestionDropdown({
@@ -8339,8 +8331,8 @@ function useTerminalWidth() {
8339
8331
  import { startTransition } from "react";
8340
8332
 
8341
8333
  // src/lib/workspace/init-agents-md.ts
8342
- import fs19 from "fs/promises";
8343
- import path17 from "path";
8334
+ import fs17 from "fs/promises";
8335
+ import path16 from "path";
8344
8336
  var IGNORED_DIRS2 = /* @__PURE__ */ new Set([
8345
8337
  "node_modules",
8346
8338
  ".git",
@@ -8379,18 +8371,18 @@ async function scanDirectoryShallow(dir, maxDepth = 2, depth = 0) {
8379
8371
  const results = [];
8380
8372
  if (depth > maxDepth) return results;
8381
8373
  try {
8382
- const entries = await fs19.readdir(dir, { withFileTypes: true });
8374
+ const entries = await fs17.readdir(dir, { withFileTypes: true });
8383
8375
  for (const entry of entries) {
8384
8376
  if (IGNORED_DIRS2.has(entry.name)) continue;
8385
8377
  if (entry.name.startsWith(".") && depth === 0 && entry.isDirectory()) continue;
8386
- const fullPath = path17.join(dir, entry.name);
8387
- const relativePath = path17.relative(dir, fullPath);
8378
+ const fullPath = path16.join(dir, entry.name);
8379
+ const relativePath = path16.relative(dir, fullPath);
8388
8380
  if (entry.isDirectory()) {
8389
8381
  results.push({ path: relativePath + "/", size: 0, isDir: true });
8390
8382
  const children = await scanDirectoryShallow(fullPath, maxDepth, depth + 1);
8391
8383
  results.push(...children);
8392
8384
  } else {
8393
- const stat = await fs19.stat(fullPath).catch(() => null);
8385
+ const stat = await fs17.stat(fullPath).catch(() => null);
8394
8386
  results.push({ path: relativePath, size: stat?.size ?? 0, isDir: false });
8395
8387
  }
8396
8388
  }
@@ -8402,7 +8394,7 @@ async function readKeyFiles(dir) {
8402
8394
  const contents = {};
8403
8395
  for (const name of INTERESTING_FILES) {
8404
8396
  try {
8405
- const content = await fs19.readFile(path17.join(dir, name), "utf8");
8397
+ const content = await fs17.readFile(path16.join(dir, name), "utf8");
8406
8398
  contents[name] = content.slice(0, 2e3);
8407
8399
  } catch {
8408
8400
  }
@@ -8488,8 +8480,8 @@ ${scanData.slice(0, 25e3)}`;
8488
8480
 
8489
8481
  // src/lib/preview/server.ts
8490
8482
  import http2 from "http";
8491
- import fs20 from "fs";
8492
- import path18 from "path";
8483
+ import fs18 from "fs";
8484
+ import path17 from "path";
8493
8485
 
8494
8486
  // src/lib/preview/latex-to-html.ts
8495
8487
  function latexToHtml(latex) {
@@ -8756,11 +8748,11 @@ var HTML_TEMPLATE = `<!DOCTYPE html>
8756
8748
  </body>
8757
8749
  </html>`;
8758
8750
  function startPreviewServer(texPath) {
8759
- const resolved = path18.resolve(texPath);
8751
+ const resolved = path17.resolve(texPath);
8760
8752
  let currentHash = "";
8761
8753
  function getContentHash() {
8762
8754
  try {
8763
- const content = fs20.readFileSync(resolved, "utf8");
8755
+ const content = fs18.readFileSync(resolved, "utf8");
8764
8756
  return `${content.length}-${content.slice(0, 100)}-${content.slice(-100)}`;
8765
8757
  } catch {
8766
8758
  return "error";
@@ -8768,7 +8760,7 @@ function startPreviewServer(texPath) {
8768
8760
  }
8769
8761
  function renderPage() {
8770
8762
  try {
8771
- const latex = fs20.readFileSync(resolved, "utf8");
8763
+ const latex = fs18.readFileSync(resolved, "utf8");
8772
8764
  const htmlContent = latexToHtml(latex);
8773
8765
  currentHash = getContentHash();
8774
8766
  return HTML_TEMPLATE.replace("{{CONTENT}}", htmlContent);
@@ -8942,7 +8934,7 @@ async function executeSlashCommand(cmd, args, ctx) {
8942
8934
  addSystemMessage("No workspace. Run /init first.");
8943
8935
  break;
8944
8936
  }
8945
- const { listSessions: listSessions2 } = await import("./sessions-FMB5GHSR.js");
8937
+ const { listSessions: listSessions2 } = await import("./sessions-GRES2MUV.js");
8946
8938
  const foundSessions = await listSessions2(workspacePath);
8947
8939
  if (foundSessions.length === 0) {
8948
8940
  addSystemMessage("No previous sessions found.");
@@ -9164,8 +9156,8 @@ async function executeSlashCommand(cmd, args, ctx) {
9164
9156
  }
9165
9157
  case "export": {
9166
9158
  const fileName = args?.trim() || "conversation-export.md";
9167
- const path21 = __require("path");
9168
- const exportPath = path21.resolve(workspacePath ?? process.cwd(), fileName);
9159
+ const path20 = __require("path");
9160
+ const exportPath = path20.resolve(workspacePath ?? process.cwd(), fileName);
9169
9161
  const lines = [`# Open Research \u2014 Conversation Export
9170
9162
  `];
9171
9163
  for (const msg of messages) {
@@ -9215,23 +9207,27 @@ ${msg.text}
9215
9207
  if (!args) {
9216
9208
  const ssKey = getSemanticScholarApiKey(config);
9217
9209
  const oaKey = getOpenAlexApiKey(config);
9210
+ const braveKey = getBraveApiKey(config);
9218
9211
  addSystemMessage("API Keys:");
9219
9212
  addSystemMessage(` Semantic Scholar: ${ssKey ? ssKey.slice(0, 8) + "..." : "not set"}`);
9220
9213
  addSystemMessage(` OpenAlex: ${oaKey ? oaKey.slice(0, 8) + "..." : "not set"}`);
9214
+ addSystemMessage(` Brave Search: ${braveKey ? braveKey.slice(0, 8) + "..." : "not set (using DuckDuckGo)"}`);
9221
9215
  addSystemMessage("");
9222
9216
  addSystemMessage("Set via CLI:");
9223
9217
  addSystemMessage(" /api-keys semantic-scholar YOUR_KEY");
9224
9218
  addSystemMessage(" /api-keys openalex YOUR_KEY");
9219
+ addSystemMessage(" /api-keys brave YOUR_KEY");
9225
9220
  addSystemMessage("");
9226
9221
  addSystemMessage("Or set environment variables:");
9227
9222
  addSystemMessage(" export SEMANTIC_SCHOLAR_API_KEY=your_key");
9228
9223
  addSystemMessage(" export OPENALEX_API_KEY=your_key");
9224
+ addSystemMessage(" export BRAVE_API_KEY=your_key");
9229
9225
  break;
9230
9226
  }
9231
9227
  const [keyName, ...keyParts] = args.split(/\s+/);
9232
9228
  const keyValue = keyParts.join("").trim();
9233
9229
  if (!keyValue) {
9234
- addSystemMessage("Usage: /api-keys <semantic-scholar|openalex> <key>");
9230
+ addSystemMessage("Usage: /api-keys <semantic-scholar|openalex|brave> <key>");
9235
9231
  break;
9236
9232
  }
9237
9233
  if (config) {
@@ -9240,8 +9236,10 @@ ${msg.text}
9240
9236
  apiKeys.semanticScholar = keyValue;
9241
9237
  } else if (keyName === "openalex" || keyName === "oa") {
9242
9238
  apiKeys.openAlex = keyValue;
9239
+ } else if (keyName === "brave") {
9240
+ apiKeys.brave = keyValue;
9243
9241
  } else {
9244
- addSystemMessage(`Unknown key: ${keyName}. Use semantic-scholar or openalex.`);
9242
+ addSystemMessage(`Unknown key: ${keyName}. Use semantic-scholar, openalex, or brave.`);
9245
9243
  break;
9246
9244
  }
9247
9245
  const updated = { ...config, apiKeys };
@@ -9260,8 +9258,10 @@ ${msg.text}
9260
9258
  addSystemMessage(` Skills: ${skills2.length} loaded`);
9261
9259
  const ssKey = getSemanticScholarApiKey(config);
9262
9260
  const oaKey = getOpenAlexApiKey(config);
9261
+ const brKey = getBraveApiKey(config);
9263
9262
  addSystemMessage(` Semantic Scholar API: ${ssKey ? "configured" : "not set (rate-limited)"}`);
9264
9263
  addSystemMessage(` OpenAlex API: ${oaKey ? "configured" : "not set (limited)"}`);
9264
+ addSystemMessage(` Brave Search API: ${brKey ? "configured" : "not set (using DuckDuckGo)"}`);
9265
9265
  const mems = await loadAllMemories({ homeDir });
9266
9266
  addSystemMessage(` Memories: ${mems.length} stored`);
9267
9267
  addSystemMessage(` Node: ${process.version}`);
@@ -9329,6 +9329,76 @@ ${msg.text}
9329
9329
  }
9330
9330
  break;
9331
9331
  }
9332
+ case "ontology": {
9333
+ const workspaceDir = workspacePath;
9334
+ if (!workspaceDir) {
9335
+ addSystemMessage("No workspace. Run /init first.");
9336
+ break;
9337
+ }
9338
+ const { loadOntology, saveOntology } = await import("./store-LT5EGDOI.js");
9339
+ const ontology = await loadOntology(workspaceDir);
9340
+ if (!args) {
9341
+ const { getOntologyStatus } = await import("./status-GEEAGLPF.js");
9342
+ addSystemMessage(getOntologyStatus(ontology));
9343
+ } else if (args === "claims") {
9344
+ const { formatClaims } = await import("./status-GEEAGLPF.js");
9345
+ addSystemMessage(formatClaims(ontology));
9346
+ } else if (args === "conflicts") {
9347
+ const { formatConflicts } = await import("./status-GEEAGLPF.js");
9348
+ addSystemMessage(formatConflicts(ontology));
9349
+ } else if (args.startsWith("around ")) {
9350
+ const term = args.slice(7).trim();
9351
+ if (!term) {
9352
+ addSystemMessage("Usage: /ontology around <term>");
9353
+ break;
9354
+ }
9355
+ const { searchNotes, getConnections } = await import("./read-tools-GHBKBZFE.js");
9356
+ const results = searchNotes(ontology, { queries: [term], limit: 5 });
9357
+ if (results.length === 0) {
9358
+ addSystemMessage(`No notes found matching "${term}".`);
9359
+ } else {
9360
+ for (const note of results) {
9361
+ addSystemMessage(`[${note.id.slice(0, 8)}] "${note.content}" (${note.kind}, ${note.confidence})`);
9362
+ const { connected } = getConnections(ontology, note.id, 1);
9363
+ for (const conn of connected) {
9364
+ const edge = note.edges.find((e) => e.targetId === conn.id);
9365
+ const rel = edge ? `${edge.relation} \u2192` : "\u2190 connected";
9366
+ addSystemMessage(` ${rel} [${conn.id.slice(0, 8)}] "${conn.content}" (${conn.kind})`);
9367
+ }
9368
+ }
9369
+ }
9370
+ } else if (args.startsWith("delete ")) {
9371
+ const noteId = args.slice(7).trim();
9372
+ const note = ontology.notes.find((n) => n.id.startsWith(noteId));
9373
+ if (!note) {
9374
+ addSystemMessage(`Note not found: ${noteId}`);
9375
+ } else {
9376
+ ontology.notes = ontology.notes.filter((n) => n.id !== note.id);
9377
+ for (const n of ontology.notes) {
9378
+ n.edges = n.edges.filter((e) => e.targetId !== note.id);
9379
+ }
9380
+ await saveOntology(ontology, workspaceDir);
9381
+ addSystemMessage(`Deleted note [${note.id.slice(0, 8)}] "${note.content}" and its edges.`);
9382
+ }
9383
+ } else if (args.startsWith("edit ")) {
9384
+ const noteId = args.slice(5).trim();
9385
+ const note = ontology.notes.find((n) => n.id.startsWith(noteId));
9386
+ if (!note) {
9387
+ addSystemMessage(`Note not found: ${noteId}`);
9388
+ } else {
9389
+ addSystemMessage(`Note [${note.id.slice(0, 8)}]:`);
9390
+ addSystemMessage(` Kind: ${note.kind}`);
9391
+ addSystemMessage(` Content: "${note.content}"`);
9392
+ addSystemMessage(` Confidence: ${note.confidence}`);
9393
+ addSystemMessage(` Edges: ${note.edges.length}`);
9394
+ addSystemMessage("");
9395
+ addSystemMessage('To edit, use the agent: "Update the ontology note about ..."');
9396
+ }
9397
+ } else {
9398
+ addSystemMessage("Usage: /ontology [claims|conflicts|around <term>|delete <id>|edit <id>]");
9399
+ }
9400
+ break;
9401
+ }
9332
9402
  case "exit": {
9333
9403
  exitApp();
9334
9404
  break;
@@ -9353,7 +9423,8 @@ var TOOL_GROUPS = {
9353
9423
  create_paper: "created paper",
9354
9424
  read_skill_reference: "read",
9355
9425
  create_tasks: "tasks",
9356
- update_task: "tasks"
9426
+ update_task: "tasks",
9427
+ web_search: "web searched"
9357
9428
  };
9358
9429
  function buildToolSummary(tools) {
9359
9430
  const groups = {};
@@ -9377,8 +9448,9 @@ function buildToolSummary(tools) {
9377
9448
  if (groups["created paper"]) parts.push("Created paper");
9378
9449
  if (groups["sub-agent"]) parts.push(`Ran ${groups["sub-agent"]} sub-agent${groups["sub-agent"] > 1 ? "s" : ""}`);
9379
9450
  if (groups["tasks"]) parts.push(`Updated tasks`);
9451
+ if (groups["web searched"]) parts.push(`Web searched${groups["web searched"] > 1 ? ` (${groups["web searched"]}\xD7)` : ""}`);
9380
9452
  for (const [group, count] of Object.entries(groups)) {
9381
- if (!["read", "listed", "searched", "searched papers", "ran", "wrote", "edited", "fetched", "asked user", "loaded skill", "created paper", "sub-agent", "tasks"].includes(group)) {
9453
+ if (!["read", "listed", "searched", "searched papers", "ran", "wrote", "edited", "fetched", "asked user", "loaded skill", "created paper", "sub-agent", "tasks", "web searched"].includes(group)) {
9382
9454
  parts.push(`${group} (${count})`);
9383
9455
  }
9384
9456
  }
@@ -10237,6 +10309,10 @@ ${error.stack}` : String(error)}` }
10237
10309
  hasWorkspace,
10238
10310
  fileCount: workspaceFiles.length,
10239
10311
  skillCount: skills2.length,
10312
+ version: getPackageVersion(),
10313
+ model: config?.defaults.model ?? "gpt-5.4",
10314
+ contextWindow: getContextWindow(config?.defaults.model ?? "gpt-5.4"),
10315
+ workspacePath,
10240
10316
  width: contentWidth
10241
10317
  }
10242
10318
  ),
@@ -10350,7 +10426,7 @@ ${error.stack}` : String(error)}` }
10350
10426
  statusParts,
10351
10427
  statusColor,
10352
10428
  tokenDisplay,
10353
- workspaceName: hasWorkspace ? path19.basename(workspacePath) : process.cwd(),
10429
+ workspaceName: hasWorkspace ? path18.basename(workspacePath) : process.cwd(),
10354
10430
  mode: agentMode,
10355
10431
  planningStatus: planningState.status
10356
10432
  }
@@ -10362,7 +10438,7 @@ ${error.stack}` : String(error)}` }
10362
10438
  var program = new Command();
10363
10439
  program.name("open-research").version(getPackageVersion()).description("Local-first research CLI powered by OpenAI account auth or API keys.").argument("[workspacePath]", "Optional workspace path to open").action(async (workspacePath) => {
10364
10440
  await ensureOpenResearchConfig();
10365
- const target = workspacePath ? path20.resolve(workspacePath) : process.cwd();
10441
+ const target = workspacePath ? path19.resolve(workspacePath) : process.cwd();
10366
10442
  const project = await loadWorkspaceProject(target);
10367
10443
  const hasProvider = await hasConfiguredProvider();
10368
10444
  render(
@@ -10379,7 +10455,7 @@ program.name("open-research").version(getPackageVersion()).description("Local-fi
10379
10455
  });
10380
10456
  program.command("init").argument("[workspacePath]").description("Initialize an Open Research workspace.").action(async (workspacePath) => {
10381
10457
  await ensureOpenResearchConfig();
10382
- const target = path20.resolve(workspacePath ?? process.cwd());
10458
+ const target = path19.resolve(workspacePath ?? process.cwd());
10383
10459
  const project = await initWorkspace({ workspaceDir: target });
10384
10460
  console.log(`Initialized workspace: ${target}`);
10385
10461
  console.log(`Title: ${project.title}`);
@@ -10451,8 +10527,8 @@ skills.command("create").argument("[name]").description("Scaffold a new user ski
10451
10527
  });
10452
10528
  skills.command("edit").argument("<name>").description("Open a user skill in $EDITOR.").action(async (name) => {
10453
10529
  await ensureOpenResearchConfig();
10454
- const skillDir = path20.join(getOpenResearchSkillsDir(), name);
10455
- openInEditor(path20.join(skillDir, "SKILL.md"));
10530
+ const skillDir = path19.join(getOpenResearchSkillsDir(), name);
10531
+ openInEditor(path19.join(skillDir, "SKILL.md"));
10456
10532
  const validation = await validateSkillDirectory({ skillDir });
10457
10533
  if (!validation.ok) {
10458
10534
  console.error(validation.errors.join("\n"));
@@ -10463,9 +10539,9 @@ skills.command("edit").argument("<name>").description("Open a user skill in $EDI
10463
10539
  });
10464
10540
  skills.command("validate").argument("[nameOrPath]").description("Validate one user skill.").action(async (nameOrPath) => {
10465
10541
  await ensureOpenResearchConfig();
10466
- const skillDir = nameOrPath ? path20.isAbsolute(nameOrPath) ? nameOrPath : path20.join(getOpenResearchSkillsDir(), nameOrPath) : getOpenResearchSkillsDir();
10542
+ const skillDir = nameOrPath ? path19.isAbsolute(nameOrPath) ? nameOrPath : path19.join(getOpenResearchSkillsDir(), nameOrPath) : getOpenResearchSkillsDir();
10467
10543
  const stat = await import("fs/promises").then(
10468
- (fs21) => fs21.stat(skillDir).catch(() => null)
10544
+ (fs19) => fs19.stat(skillDir).catch(() => null)
10469
10545
  );
10470
10546
  if (!stat) {
10471
10547
  throw new Error(`Skill path not found: ${skillDir}`);