open-research 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +271 -91
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -811,7 +811,7 @@ function formatDateTime(value) {
811
811
  }
812
812
 
813
813
  // src/lib/cli/version.ts
814
- var PACKAGE_VERSION = "0.1.10";
814
+ var PACKAGE_VERSION = "0.1.12";
815
815
  function getPackageVersion() {
816
816
  return PACKAGE_VERSION;
817
817
  }
@@ -4761,131 +4761,283 @@ async function manualCompact(messages, model, provider, usage, customInstruction
4761
4761
  // src/lib/memory/store.ts
4762
4762
  import fs14 from "fs/promises";
4763
4763
  import path13 from "path";
4764
- function getMemoryFile(options) {
4764
+ function getGlobalMemoryFile(options) {
4765
4765
  return path13.join(getOpenResearchRoot(options), "memory.json");
4766
4766
  }
4767
- async function loadMemories(options) {
4768
- const file = getMemoryFile(options);
4767
+ function getProjectMemoryFile(workspaceDir) {
4768
+ return path13.join(workspaceDir, ".open-research", "memory.json");
4769
+ }
4770
+ async function loadMemoryFile(filePath) {
4769
4771
  try {
4770
- const raw = await fs14.readFile(file, "utf8");
4772
+ const raw = await fs14.readFile(filePath, "utf8");
4771
4773
  const store = JSON.parse(raw);
4772
4774
  return store.memories ?? [];
4773
4775
  } catch {
4774
4776
  return [];
4775
4777
  }
4776
4778
  }
4777
- async function saveMemories(memories, options) {
4778
- const file = getMemoryFile(options);
4779
- await fs14.mkdir(path13.dirname(file), { recursive: true });
4780
- const store = { version: 1, memories };
4781
- await fs14.writeFile(file, JSON.stringify(store, null, 2), "utf8");
4779
+ async function saveMemoryFile(filePath, memories) {
4780
+ await fs14.mkdir(path13.dirname(filePath), { recursive: true });
4781
+ const store = { version: 2, memories };
4782
+ await fs14.writeFile(filePath, JSON.stringify(store, null, 2), "utf8");
4782
4783
  }
4783
- var MAX_MEMORIES = 100;
4784
- async function addMemory(memory, options) {
4785
- const memories = await loadMemories(options);
4786
- const existing = memories.find((m) => {
4784
+ async function loadGlobalMemories(options) {
4785
+ const mems = await loadMemoryFile(getGlobalMemoryFile(options));
4786
+ return mems.map((m) => ({ ...m, scope: "global" }));
4787
+ }
4788
+ async function loadProjectMemories(workspaceDir) {
4789
+ const mems = await loadMemoryFile(getProjectMemoryFile(workspaceDir));
4790
+ return mems.map((m) => ({ ...m, scope: "project" }));
4791
+ }
4792
+ async function loadAllMemories(options) {
4793
+ const global = await loadGlobalMemories(options);
4794
+ const project = options?.workspaceDir ? await loadProjectMemories(options.workspaceDir) : [];
4795
+ return [...global, ...project];
4796
+ }
4797
+ var MAX_MEMORIES_PER_STORE = 100;
4798
+ function findDuplicate(memories, content) {
4799
+ const b = content.toLowerCase().replace(/\s+/g, " ");
4800
+ const wordsB = new Set(b.split(" ").filter((w) => w.length > 2));
4801
+ return memories.find((m) => {
4787
4802
  const a = m.content.toLowerCase().replace(/\s+/g, " ");
4788
- const b = memory.content.toLowerCase().replace(/\s+/g, " ");
4789
- const wordsA = new Set(a.split(" "));
4790
- const wordsB = new Set(b.split(" "));
4803
+ const wordsA = new Set(a.split(" ").filter((w) => w.length > 2));
4791
4804
  const intersection = [...wordsA].filter((w) => wordsB.has(w));
4792
- const similarity = intersection.length / Math.max(wordsA.size, wordsB.size);
4793
- return similarity > 0.7;
4805
+ return intersection.length / Math.max(wordsA.size, wordsB.size) > 0.7;
4794
4806
  });
4807
+ }
4808
+ function evictIfNeeded(memories) {
4809
+ if (memories.length <= MAX_MEMORIES_PER_STORE) return;
4810
+ memories.sort((a, b) => {
4811
+ const aScore = new Date(a.lastRelevantAt).getTime() + a.relevanceCount * 864e5;
4812
+ const bScore = new Date(b.lastRelevantAt).getTime() + b.relevanceCount * 864e5;
4813
+ return bScore - aScore;
4814
+ });
4815
+ memories.length = MAX_MEMORIES_PER_STORE;
4816
+ }
4817
+ async function addMemory(memory, options) {
4818
+ const scope = memory.scope ?? (memory.category === "project" || memory.category === "context" ? "project" : "global");
4819
+ const filePath = scope === "project" && options?.workspaceDir ? getProjectMemoryFile(options.workspaceDir) : getGlobalMemoryFile(options);
4820
+ const memories = await loadMemoryFile(filePath);
4821
+ const existing = findDuplicate(memories, memory.content);
4795
4822
  if (existing) {
4796
4823
  existing.lastRelevantAt = (/* @__PURE__ */ new Date()).toISOString();
4797
4824
  existing.relevanceCount++;
4798
4825
  if (memory.content.length > existing.content.length) {
4799
4826
  existing.content = memory.content;
4800
4827
  }
4801
- await saveMemories(memories, options);
4802
- return existing;
4828
+ await saveMemoryFile(filePath, memories);
4829
+ return { ...existing, scope };
4803
4830
  }
4804
4831
  const newMemory = {
4805
4832
  id: crypto.randomUUID(),
4806
4833
  content: memory.content,
4807
4834
  category: memory.category,
4835
+ scope,
4808
4836
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4809
4837
  lastRelevantAt: (/* @__PURE__ */ new Date()).toISOString(),
4810
4838
  relevanceCount: 1
4811
4839
  };
4812
4840
  memories.push(newMemory);
4813
- if (memories.length > MAX_MEMORIES) {
4814
- memories.sort((a, b) => {
4815
- const aScore = new Date(a.lastRelevantAt).getTime() + a.relevanceCount * 864e5;
4816
- const bScore = new Date(b.lastRelevantAt).getTime() + b.relevanceCount * 864e5;
4817
- return bScore - aScore;
4818
- });
4819
- memories.length = MAX_MEMORIES;
4820
- }
4821
- await saveMemories(memories, options);
4841
+ evictIfNeeded(memories);
4842
+ await saveMemoryFile(filePath, memories);
4822
4843
  return newMemory;
4823
4844
  }
4824
4845
  async function deleteMemory(id, options) {
4825
- const memories = await loadMemories(options);
4826
- const idx = memories.findIndex((m) => m.id === id);
4827
- if (idx === -1) return false;
4828
- memories.splice(idx, 1);
4829
- await saveMemories(memories, options);
4830
- return true;
4846
+ const globalFile = getGlobalMemoryFile(options);
4847
+ const global = await loadMemoryFile(globalFile);
4848
+ const gIdx = global.findIndex((m) => m.id === id);
4849
+ if (gIdx !== -1) {
4850
+ global.splice(gIdx, 1);
4851
+ await saveMemoryFile(globalFile, global);
4852
+ return true;
4853
+ }
4854
+ if (options?.workspaceDir) {
4855
+ const projectFile = getProjectMemoryFile(options.workspaceDir);
4856
+ const project = await loadMemoryFile(projectFile);
4857
+ const pIdx = project.findIndex((m) => m.id === id);
4858
+ if (pIdx !== -1) {
4859
+ project.splice(pIdx, 1);
4860
+ await saveMemoryFile(projectFile, project);
4861
+ return true;
4862
+ }
4863
+ }
4864
+ return false;
4831
4865
  }
4832
4866
  async function clearMemories(options) {
4833
- await saveMemories([], options);
4867
+ if (!options?.scope || options.scope === "global") {
4868
+ await saveMemoryFile(getGlobalMemoryFile(options), []);
4869
+ }
4870
+ if (options?.workspaceDir && (!options?.scope || options.scope === "project")) {
4871
+ await saveMemoryFile(getProjectMemoryFile(options.workspaceDir), []);
4872
+ }
4873
+ }
4874
+ var STOP_WORDS = /* @__PURE__ */ new Set([
4875
+ "the",
4876
+ "a",
4877
+ "an",
4878
+ "is",
4879
+ "are",
4880
+ "was",
4881
+ "were",
4882
+ "be",
4883
+ "been",
4884
+ "being",
4885
+ "have",
4886
+ "has",
4887
+ "had",
4888
+ "do",
4889
+ "does",
4890
+ "did",
4891
+ "will",
4892
+ "would",
4893
+ "could",
4894
+ "should",
4895
+ "may",
4896
+ "might",
4897
+ "can",
4898
+ "shall",
4899
+ "to",
4900
+ "of",
4901
+ "in",
4902
+ "for",
4903
+ "on",
4904
+ "with",
4905
+ "at",
4906
+ "by",
4907
+ "from",
4908
+ "as",
4909
+ "into",
4910
+ "through",
4911
+ "about",
4912
+ "and",
4913
+ "but",
4914
+ "or",
4915
+ "not",
4916
+ "no",
4917
+ "if",
4918
+ "then",
4919
+ "than",
4920
+ "so",
4921
+ "that",
4922
+ "this",
4923
+ "it",
4924
+ "its",
4925
+ "i",
4926
+ "me",
4927
+ "my",
4928
+ "we",
4929
+ "our",
4930
+ "you",
4931
+ "your",
4932
+ "what",
4933
+ "which",
4934
+ "who",
4935
+ "how",
4936
+ "when",
4937
+ "where",
4938
+ "why"
4939
+ ]);
4940
+ function tokenize(text) {
4941
+ return text.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w));
4942
+ }
4943
+ function scoreRelevance(memory, queryTokens) {
4944
+ const memTokens = tokenize(memory.content);
4945
+ if (memTokens.length === 0) return 0;
4946
+ let matches = 0;
4947
+ for (const token of memTokens) {
4948
+ if (queryTokens.has(token)) matches++;
4949
+ }
4950
+ const overlapScore = matches / memTokens.length;
4951
+ const categoryBoost = memory.category === "user" ? 0.3 : memory.category === "preference" ? 0.2 : memory.category === "methodology" ? 0.15 : 0;
4952
+ const ageMs = Date.now() - new Date(memory.lastRelevantAt).getTime();
4953
+ const ageDays = ageMs / 864e5;
4954
+ const recencyBoost = Math.max(0, 0.1 - ageDays * 1e-3);
4955
+ const freqBoost = Math.min(0.1, memory.relevanceCount * 0.02);
4956
+ return overlapScore + categoryBoost + recencyBoost + freqBoost;
4957
+ }
4958
+ var MIN_RELEVANCE_SCORE = 0.15;
4959
+ var MAX_INJECTED_MEMORIES = 15;
4960
+ var ALWAYS_INCLUDE_CATEGORIES = ["user", "preference"];
4961
+ function selectRelevantMemories(allMemories, userQuery) {
4962
+ if (allMemories.length === 0) return [];
4963
+ const queryTokens = new Set(tokenize(userQuery));
4964
+ const alwaysInclude = allMemories.filter(
4965
+ (m) => ALWAYS_INCLUDE_CATEGORIES.includes(m.category)
4966
+ );
4967
+ const candidates = allMemories.filter((m) => !ALWAYS_INCLUDE_CATEGORIES.includes(m.category)).map((m) => ({ memory: m, score: scoreRelevance(m, queryTokens) })).filter((c) => c.score >= MIN_RELEVANCE_SCORE).sort((a, b) => b.score - a.score);
4968
+ const selected = [
4969
+ ...alwaysInclude.slice(0, 5),
4970
+ // Max 5 identity memories
4971
+ ...candidates.slice(0, MAX_INJECTED_MEMORIES - Math.min(alwaysInclude.length, 5)).map((c) => c.memory)
4972
+ ];
4973
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4974
+ for (const m of selected) {
4975
+ m.lastRelevantAt = now;
4976
+ }
4977
+ return selected;
4834
4978
  }
4835
4979
  function formatMemoriesForPrompt(memories) {
4836
4980
  if (memories.length === 0) return "";
4837
- const grouped = {};
4838
- for (const m of memories) {
4839
- (grouped[m.category] ??= []).push(m);
4840
- }
4841
- const sections = ["## What I Remember About You"];
4842
- const categoryLabels = {
4843
- user: "About you",
4844
- preference: "Your preferences",
4845
- project: "Your projects",
4846
- methodology: "Methodology preferences",
4847
- context: "Context"
4848
- };
4849
- for (const [cat, mems] of Object.entries(grouped)) {
4850
- sections.push(`**${categoryLabels[cat] ?? cat}:**`);
4851
- for (const m of mems) {
4852
- sections.push(`- ${m.content}`);
4853
- }
4981
+ const global = memories.filter((m) => m.scope === "global");
4982
+ const project = memories.filter((m) => m.scope === "project");
4983
+ const sections = ["## Relevant Context"];
4984
+ if (global.length > 0) {
4985
+ sections.push("**About you:**");
4986
+ for (const m of global) sections.push(`- ${m.content}`);
4987
+ }
4988
+ if (project.length > 0) {
4989
+ sections.push("**This project:**");
4990
+ for (const m of project) sections.push(`- ${m.content}`);
4854
4991
  }
4855
4992
  return sections.join("\n");
4856
4993
  }
4857
4994
 
4858
4995
  // src/lib/memory/extractor.ts
4859
- var EXTRACTION_PROMPT = `You are a memory extraction system. Your job is to identify facts worth remembering about the user from a conversation exchange.
4996
+ var EXTRACTION_PROMPT = `You are a memory management system. You decide what to remember about the user across sessions.
4860
4997
 
4861
- Focus on:
4862
- - Who they are (role, field, institution, expertise level)
4863
- - What they're working on (current research projects, topics, deadlines)
4864
- - How they prefer to work (preferred tools, languages, writing style, methodologies)
4865
- - Methodological preferences (statistical approaches, theoretical frameworks, citation style)
4866
- - Important context (collaborators, advisors, publication targets, funding constraints)
4998
+ You will receive:
4999
+ 1. The current conversation exchange
5000
+ 2. ALL existing memories (both global and project-level)
4867
5001
 
4868
- Rules:
4869
- - Only extract facts that would be useful in FUTURE conversations
4870
- - Be specific and concise \u2014 each memory should be one clear fact
4871
- - Do NOT extract task-specific details that only matter for the current conversation
4872
- - Do NOT extract obvious things ("user asked about papers" is not useful)
4873
- - If there is nothing meaningful to remember, return an empty array
4874
- - Maximum 3 new memories per exchange
4875
-
4876
- Existing memories (do not duplicate these):
5002
+ Your job: decide if any NEW memories should be created OR if any existing memories need updating.
5003
+
5004
+ CRITICAL RULES:
5005
+ - Do NOT create a memory if an existing memory already covers the same fact
5006
+ - If a fact has CHANGED (e.g., user switched from Python to R), output an UPDATE to the existing memory instead of creating a new one
5007
+ - Only create memories for facts useful in FUTURE sessions, not task-specific details
5008
+ - Maximum 3 actions per exchange
5009
+ - If nothing meaningful to remember, return an empty array
5010
+
5011
+ Categories:
5012
+ - "user" \u2014 identity, role, field, institution (\u2192 stored globally)
5013
+ - "preference" \u2014 tools, style, methodology preferences (\u2192 stored globally)
5014
+ - "project" \u2014 current research topics, findings, hypotheses (\u2192 stored per-project)
5015
+ - "methodology" \u2014 statistical approaches, frameworks (\u2192 stored globally)
5016
+ - "context" \u2014 deadlines, collaborators, constraints (\u2192 stored per-project)
5017
+
5018
+ Existing memories:
4877
5019
  {EXISTING_MEMORIES}
4878
5020
 
4879
- Respond with a JSON array of objects, each with "content" (string) and "category" (one of: "user", "preference", "project", "methodology", "context"). If nothing worth remembering, respond with [].
5021
+ Respond with a JSON array. Each item has:
5022
+ - "action": "create" or "update"
5023
+ - "content": the memory text (for create: new content; for update: the updated content)
5024
+ - "category": one of the categories above
5025
+ - "updateId": (only for "update") the ID of the existing memory to update
4880
5026
 
4881
- Example response:
4882
- [{"content": "PhD student in computational neuroscience at MIT", "category": "user"}, {"content": "Prefers Python with statsmodels for statistical analysis over R", "category": "preference"}]`;
5027
+ If nothing to do, respond with [].
5028
+
5029
+ Example:
5030
+ [{"action": "create", "content": "PhD student in neuroscience at MIT", "category": "user"}]
5031
+ [{"action": "update", "updateId": "abc123", "content": "Now using R instead of Python for analysis", "category": "preference"}]`;
4883
5032
  async function extractMemories(input2) {
4884
- const existing = await loadMemories({ homeDir: input2.homeDir });
5033
+ const existing = await loadAllMemories({
5034
+ homeDir: input2.homeDir,
5035
+ workspaceDir: input2.workspaceDir
5036
+ });
4885
5037
  if (input2.userMessage.startsWith("/") || input2.userMessage.length < 20) {
4886
5038
  return [];
4887
5039
  }
4888
- const existingList = existing.length > 0 ? existing.map((m) => `- [${m.category}] ${m.content}`).join("\n") : "(none)";
5040
+ const existingList = existing.length > 0 ? existing.map((m) => `- [${m.scope}/${m.category}] (id: ${m.id.slice(0, 8)}) ${m.content}`).join("\n") : "(none)";
4889
5041
  const prompt2 = EXTRACTION_PROMPT.replace("{EXISTING_MEMORIES}", existingList);
4890
5042
  const conversationSnippet = [
4891
5043
  `User: ${input2.userMessage.slice(0, 2e3)}`,
@@ -4907,10 +5059,12 @@ async function extractMemories(input2) {
4907
5059
  if (!Array.isArray(parsed)) return [];
4908
5060
  const valid = [];
4909
5061
  for (const item of parsed) {
4910
- if (typeof item.content === "string" && item.content.length > 5 && ["user", "preference", "project", "methodology", "context"].includes(item.category)) {
5062
+ if (typeof item.content === "string" && item.content.length > 5 && ["user", "preference", "project", "methodology", "context"].includes(item.category) && ["create", "update"].includes(item.action)) {
4911
5063
  valid.push({
5064
+ action: item.action,
4912
5065
  content: item.content,
4913
- category: item.category
5066
+ category: item.category,
5067
+ updateId: typeof item.updateId === "string" ? item.updateId : void 0
4914
5068
  });
4915
5069
  }
4916
5070
  }
@@ -4920,13 +5074,35 @@ async function extractMemories(input2) {
4920
5074
  }
4921
5075
  }
4922
5076
  async function extractAndStoreMemories(input2) {
4923
- const extracted = await extractMemories(input2);
4924
- const stored = [];
4925
- for (const mem of extracted) {
4926
- const saved = await addMemory(mem, { homeDir: input2.homeDir });
4927
- stored.push(saved);
5077
+ const actions = await extractMemories(input2);
5078
+ const results = [];
5079
+ const existing = await loadAllMemories({
5080
+ homeDir: input2.homeDir,
5081
+ workspaceDir: input2.workspaceDir
5082
+ });
5083
+ for (const action of actions) {
5084
+ if (action.action === "update" && action.updateId) {
5085
+ const target = existing.find((m) => m.id.startsWith(action.updateId));
5086
+ if (target) {
5087
+ target.content = action.content;
5088
+ target.lastRelevantAt = (/* @__PURE__ */ new Date()).toISOString();
5089
+ target.relevanceCount++;
5090
+ const saved = await addMemory(
5091
+ { content: action.content, category: action.category, scope: target.scope },
5092
+ { homeDir: input2.homeDir, workspaceDir: input2.workspaceDir }
5093
+ );
5094
+ results.push(saved);
5095
+ }
5096
+ } else {
5097
+ const scope = action.category === "project" || action.category === "context" ? "project" : "global";
5098
+ const saved = await addMemory(
5099
+ { content: action.content, category: action.category, scope },
5100
+ { homeDir: input2.homeDir, workspaceDir: input2.workspaceDir }
5101
+ );
5102
+ results.push(saved);
5103
+ }
4928
5104
  }
4929
- return stored;
5105
+ return results;
4930
5106
  }
4931
5107
 
4932
5108
  // src/lib/workspace/agents-md.ts
@@ -5083,16 +5259,19 @@ async function runAgentTurn(input2) {
5083
5259
  const systemPrompt = isPlanning ? buildPlanningSystemPrompt(input2.workspace, activeSkills) : buildSystemPrompt(input2.workspace, activeSkills);
5084
5260
  const model = input2.model ?? "gpt-5.4";
5085
5261
  const usage = input2.sessionUsage ?? createSessionUsage();
5086
- const memories = await loadMemories({ homeDir: input2.homeDir });
5087
- const memoryBlock = formatMemoriesForPrompt(memories);
5262
+ const allMemories = await loadAllMemories({
5263
+ homeDir: input2.homeDir,
5264
+ workspaceDir: input2.workspace.workspaceDir
5265
+ });
5266
+ const relevantMemories = selectRelevantMemories(allMemories, input2.message);
5267
+ const memoryBlock = formatMemoriesForPrompt(relevantMemories);
5088
5268
  const agentsMd = input2.workspace.workspaceDir ? await readAgentsMd(input2.workspace.workspaceDir).catch(() => "") : "";
5089
- const contextBlocks = [
5269
+ const fullSystemPrompt = [
5090
5270
  systemPrompt,
5091
- memoryBlock ? memoryBlock : null,
5271
+ memoryBlock || null,
5092
5272
  agentsMd ? `## Project Context (from AGENTS.md)
5093
5273
  ${agentsMd}` : null
5094
5274
  ].filter(Boolean).join("\n\n");
5095
- const fullSystemPrompt = contextBlocks;
5096
5275
  let messages = [
5097
5276
  { role: "system", content: fullSystemPrompt },
5098
5277
  ...input2.history,
@@ -5152,7 +5331,8 @@ ${agentsMd}` : null
5152
5331
  agentResponse: fullText,
5153
5332
  provider: input2.provider,
5154
5333
  model: "gpt-5.4-mini",
5155
- homeDir: input2.homeDir
5334
+ homeDir: input2.homeDir,
5335
+ workspaceDir: input2.workspace.workspaceDir
5156
5336
  }).then((stored) => {
5157
5337
  if (stored.length > 0) {
5158
5338
  input2.onMemoryExtracted?.(stored.map((m) => m.content));
@@ -7006,7 +7186,7 @@ ${msg.text}
7006
7186
  addSystemMessage(` Workspace: ${workspacePath ? workspacePath : "none"}`);
7007
7187
  addSystemMessage(` Files: ${workspaceFiles.length}`);
7008
7188
  addSystemMessage(` Skills: ${skills2.length} loaded`);
7009
- const mems = await loadMemories({ homeDir });
7189
+ const mems = await loadAllMemories({ homeDir });
7010
7190
  addSystemMessage(` Memories: ${mems.length} stored`);
7011
7191
  addSystemMessage(` Node: ${process.version}`);
7012
7192
  const toolChecks = ["python3 --version", "pdflatex --version", "git --version"];
@@ -7057,7 +7237,7 @@ ${msg.text}
7057
7237
  addSystemMessage(deleted ? `Deleted memory ${memId.slice(0, 8)}...` : "Memory not found.");
7058
7238
  break;
7059
7239
  }
7060
- const mems = await loadMemories({ homeDir });
7240
+ const mems = await loadAllMemories({ homeDir });
7061
7241
  if (mems.length === 0) {
7062
7242
  addSystemMessage("No memories stored yet. I'll learn about you as we talk.");
7063
7243
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-research",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
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",