open-research 0.1.9 → 0.1.11

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 +342 -145
  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.9";
814
+ var PACKAGE_VERSION = "0.1.11";
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;
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;
4794
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.
4997
+
4998
+ You will receive:
4999
+ 1. The current conversation exchange
5000
+ 2. ALL existing memories (both global and project-level)
4860
5001
 
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)
5002
+ Your job: decide if any NEW memories should be created OR if any existing memories need updating.
4867
5003
 
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):
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));
@@ -5562,7 +5742,8 @@ var INTERESTING_FILES = /* @__PURE__ */ new Set([
5562
5742
  ".env.example",
5563
5743
  "tsconfig.json",
5564
5744
  "vitest.config.ts",
5565
- "jest.config.js"
5745
+ "jest.config.js",
5746
+ "AGENTS.md"
5566
5747
  ]);
5567
5748
  async function scanDirectoryShallow(dir, maxDepth = 2, depth = 0) {
5568
5749
  const results = [];
@@ -5573,7 +5754,7 @@ async function scanDirectoryShallow(dir, maxDepth = 2, depth = 0) {
5573
5754
  if (IGNORED_DIRS2.has(entry.name)) continue;
5574
5755
  if (entry.name.startsWith(".") && depth === 0 && entry.isDirectory()) continue;
5575
5756
  const fullPath = path18.join(dir, entry.name);
5576
- const relativePath = path18.relative(process.cwd(), fullPath);
5757
+ const relativePath = path18.relative(dir, fullPath);
5577
5758
  if (entry.isDirectory()) {
5578
5759
  results.push({ path: relativePath + "/", size: 0, isDir: true });
5579
5760
  const children = await scanDirectoryShallow(fullPath, maxDepth, depth + 1);
@@ -5590,40 +5771,46 @@ async function scanDirectoryShallow(dir, maxDepth = 2, depth = 0) {
5590
5771
  async function readKeyFiles(dir) {
5591
5772
  const contents = {};
5592
5773
  for (const name of INTERESTING_FILES) {
5593
- const filePath = path18.join(dir, name);
5594
5774
  try {
5595
- const content = await fs19.readFile(filePath, "utf8");
5775
+ const content = await fs19.readFile(path18.join(dir, name), "utf8");
5596
5776
  contents[name] = content.slice(0, 2e3);
5597
5777
  } catch {
5598
5778
  }
5599
5779
  }
5600
5780
  return contents;
5601
5781
  }
5602
- var INIT_PROMPT = `You are creating an AGENTS.md file for a research workspace. This file will be injected into an AI research agent's system prompt every session to give it instant project context.
5782
+ var CREATE_PROMPT = `You are creating an AGENTS.md file for a research workspace. This file is injected into an AI research agent's system prompt every session to give it instant project context.
5603
5783
 
5604
- Based on the directory structure and key file contents below, write a concise AGENTS.md that covers:
5784
+ Write a concise AGENTS.md covering:
5785
+ ## Project Overview \u2014 What is this project? What research?
5786
+ ## Structure \u2014 Key directories and their purpose (only important ones)
5787
+ ## Key Files \u2014 Notable files and what they do
5788
+ ## Research Context \u2014 What research is in progress?
5789
+ ## Development \u2014 How to build/run/test (if applicable)
5605
5790
 
5606
- ## Project Overview
5607
- What is this project about? What type of research?
5608
-
5609
- ## Structure
5610
- Key directories and what they contain (only the important ones).
5791
+ Rules:
5792
+ - Under 1500 characters. This goes into every system prompt.
5793
+ - Specific to THIS project. No generic advice.
5794
+ - Markdown with ## headings.`;
5795
+ var UPDATE_PROMPT = `You are updating an existing AGENTS.md file for a research workspace. This file is injected into an AI research agent's system prompt every session.
5611
5796
 
5612
- ## Key Files
5613
- Important files and what they do (only the notable ones).
5797
+ You have:
5798
+ 1. The CURRENT AGENTS.md content
5799
+ 2. A fresh scan of the workspace directory and key files
5614
5800
 
5615
- ## Research Context
5616
- What research appears to be in progress based on the files?
5801
+ Your job: compare the current AGENTS.md against the actual workspace state. Update it to reflect reality:
5802
+ - Add new directories/files that appeared
5803
+ - Remove references to things that no longer exist
5804
+ - Update descriptions that are now outdated
5805
+ - Preserve any manually-added notes or context the user wrote
5806
+ - Keep the same ## heading structure
5617
5807
 
5618
- ## Development
5619
- How to build/run/test (if applicable, based on package.json or similar).
5808
+ If AGENTS.md is already accurate, output it unchanged.
5620
5809
 
5621
5810
  Rules:
5622
- - Keep it under 1500 characters total
5623
- - Be specific to THIS project, not generic
5624
- - If it's unclear what the project does, say so and note what you can see
5625
- - Use markdown with ## headings
5626
- - Don't include obvious things ("node_modules contains npm packages")`;
5811
+ - Under 1500 characters. This goes into every system prompt.
5812
+ - Output the FULL updated AGENTS.md content, not a diff.
5813
+ - Markdown with ## headings.`;
5627
5814
  async function generateInitialAgentsMd(input2) {
5628
5815
  const dir = input2.workspaceDir;
5629
5816
  const files = await scanDirectoryShallow(dir);
@@ -5633,17 +5820,32 @@ async function generateInitialAgentsMd(input2) {
5633
5820
  \`\`\`
5634
5821
  ${content2}
5635
5822
  \`\`\``).join("\n\n");
5636
- const userMessage = `Directory: ${dir}
5823
+ const scanData = `Directory: ${dir}
5637
5824
 
5638
5825
  File tree:
5639
5826
  ${tree}
5640
5827
 
5641
- ${keyFileText ? `Key files:
5642
- ${keyFileText}` : "No recognizable key files found."}`;
5828
+ ${keyFileText || "No recognizable key files found."}`;
5829
+ const existing = await readAgentsMd(dir);
5830
+ let systemPrompt;
5831
+ let userMessage;
5832
+ if (existing) {
5833
+ systemPrompt = UPDATE_PROMPT;
5834
+ userMessage = `Current AGENTS.md:
5835
+ ---
5836
+ ${existing}
5837
+ ---
5838
+
5839
+ Fresh workspace scan:
5840
+ ${scanData.slice(0, 25e3)}`;
5841
+ } else {
5842
+ systemPrompt = CREATE_PROMPT;
5843
+ userMessage = scanData.slice(0, 25e3);
5844
+ }
5643
5845
  const response = await input2.provider.callLLM({
5644
5846
  messages: [
5645
- { role: "system", content: INIT_PROMPT },
5646
- { role: "user", content: userMessage.slice(0, 3e4) }
5847
+ { role: "system", content: systemPrompt },
5848
+ { role: "user", content: userMessage }
5647
5849
  ],
5648
5850
  model: input2.model ?? "gpt-5.4-mini",
5649
5851
  maxTokens: 2048,
@@ -6683,37 +6885,32 @@ function App({
6683
6885
  }
6684
6886
  case "init": {
6685
6887
  const target = process.cwd();
6686
- const existing = await loadWorkspaceProject(target);
6687
- if (existing) {
6688
- addSystemMessage(`Already a workspace: ${target}`);
6888
+ setBusy(true);
6889
+ try {
6890
+ const existing = await loadWorkspaceProject(target);
6891
+ if (!existing) {
6892
+ await initWorkspace({ workspaceDir: target });
6893
+ addSystemMessage(`Workspace initialized at ${target}`);
6894
+ }
6689
6895
  setWorkspacePath(target);
6690
- } else {
6691
- setBusy(true);
6692
- try {
6693
- const project = await initWorkspace({ workspaceDir: target });
6694
- setWorkspacePath(target);
6695
- addSystemMessage(`Initialized workspace "${project.title}" at ${target}`);
6696
- const scanned = await scanWorkspace(target);
6697
- startTransition(() => setWorkspaceFiles(scanned.files));
6698
- if (hasAuth) {
6699
- addSystemMessage("Generating AGENTS.md...");
6700
- try {
6701
- const provider = await createProviderFromStoredAuth({ homeDir });
6702
- await generateInitialAgentsMd({
6703
- workspaceDir: target,
6704
- provider,
6705
- model: "gpt-5.4-mini"
6706
- });
6707
- addSystemMessage("AGENTS.md created \u2014 project context will be loaded on every session.");
6708
- } catch {
6709
- addSystemMessage("AGENTS.md generation skipped (connect auth first).");
6710
- }
6711
- }
6712
- } catch (err) {
6713
- addSystemMessage(`Init failed: ${err instanceof Error ? err.message : String(err)}`);
6714
- } finally {
6715
- setBusy(false);
6896
+ if (!hasAuth) {
6897
+ addSystemMessage("Run /auth first \u2014 AGENTS.md generation requires auth.");
6898
+ break;
6716
6899
  }
6900
+ addSystemMessage("Scanning workspace and updating AGENTS.md...");
6901
+ const provider = await createProviderFromStoredAuth({ homeDir });
6902
+ const result = await generateInitialAgentsMd({
6903
+ workspaceDir: target,
6904
+ provider,
6905
+ model: "gpt-5.4-mini"
6906
+ });
6907
+ addSystemMessage("AGENTS.md ready. Project context will load on every session.");
6908
+ const scanned = await scanWorkspace(target);
6909
+ startTransition(() => setWorkspaceFiles(scanned.files));
6910
+ } catch (err) {
6911
+ addSystemMessage(`Init failed: ${err instanceof Error ? err.message : String(err)}`);
6912
+ } finally {
6913
+ setBusy(false);
6717
6914
  }
6718
6915
  break;
6719
6916
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-research",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
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",