open-research 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +183 -67
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -828,7 +828,7 @@ function formatDateTime(value) {
828
828
  }
829
829
 
830
830
  // src/lib/cli/version.ts
831
- var PACKAGE_VERSION = "1.1.1";
831
+ var PACKAGE_VERSION = "1.1.2";
832
832
  function getPackageVersion() {
833
833
  return PACKAGE_VERSION;
834
834
  }
@@ -5927,11 +5927,11 @@ async function discoverScholarlySources({
5927
5927
  const openAlexSearch = clients.searchOpenAlexWorks ?? searchOpenAlexWorks;
5928
5928
  const semanticScholarSearch = clients.searchSemanticScholarPapers ?? searchSemanticScholarPapers;
5929
5929
  const arxivSearch = clients.searchArxivPapers ?? searchArxivPapers;
5930
- const tasks2 = [];
5930
+ const tasks = [];
5931
5931
  if (openAlexApiKey?.trim()) {
5932
5932
  for (let qi = 0; qi < searchQueries.length; qi++) {
5933
5933
  const qIdx = qi;
5934
- tasks2.push(
5934
+ tasks.push(
5935
5935
  openAlexSearch({
5936
5936
  query: searchQueries[qIdx],
5937
5937
  apiKey: openAlexApiKey.trim(),
@@ -5950,7 +5950,7 @@ async function discoverScholarlySources({
5950
5950
  }
5951
5951
  for (let qi = 0; qi < searchQueries.length; qi++) {
5952
5952
  const qIdx = qi;
5953
- tasks2.push(
5953
+ tasks.push(
5954
5954
  semanticScholarSearch({
5955
5955
  query: searchQueries[qIdx],
5956
5956
  apiKey: semanticScholarApiKey?.trim(),
@@ -5975,7 +5975,7 @@ async function discoverScholarlySources({
5975
5975
  } : void 0;
5976
5976
  for (let qi = 0; qi < searchQueries.length; qi++) {
5977
5977
  const qIdx = qi;
5978
- tasks2.push(
5978
+ tasks.push(
5979
5979
  arxivSearch({
5980
5980
  query: searchQueries[qIdx],
5981
5981
  maxResults: perQueryResults,
@@ -5988,7 +5988,7 @@ async function discoverScholarlySources({
5988
5988
  );
5989
5989
  }
5990
5990
  }
5991
- const settled = await Promise.allSettled(tasks2);
5991
+ const settled = await Promise.allSettled(tasks);
5992
5992
  const candidates = settled.flatMap(
5993
5993
  (result) => result.status === "fulfilled" ? result.value : []
5994
5994
  );
@@ -6310,59 +6310,100 @@ function describeSubAgentTool(name, args) {
6310
6310
  // src/lib/agent/tools/tasks.ts
6311
6311
  import path11 from "path";
6312
6312
  import fs11 from "fs/promises";
6313
- var tasks = [];
6313
+ var currentTasks = [];
6314
6314
  var storePath = null;
6315
+ var currentSessionId = null;
6316
+ var pendingWrite = Promise.resolve();
6315
6317
  function shortId() {
6316
6318
  return crypto.randomUUID().replace(/-/g, "").slice(0, 8);
6317
6319
  }
6318
- async function persist() {
6319
- if (!storePath) return;
6320
- const live = tasks.filter((t) => t.status !== "deleted");
6321
- const tmpPath = storePath + ".tmp";
6322
- await fs11.mkdir(path11.dirname(storePath), { recursive: true });
6323
- await fs11.writeFile(tmpPath, JSON.stringify({ version: 1, tasks: live }, null, 2));
6324
- await fs11.rename(tmpPath, storePath);
6320
+ function cloneTasks(tasks) {
6321
+ return tasks.map((task) => ({ ...task }));
6325
6322
  }
6326
- async function initTaskStore(workspaceDir) {
6323
+ async function persistSnapshot(snapshot) {
6324
+ const data = await readJsonFile(snapshot.storePath, { version: 1, tasks: [] });
6325
+ const foreignTasks = (data.tasks ?? []).filter(
6326
+ (task) => task.status !== "deleted" && task.sessionId !== snapshot.sessionId
6327
+ );
6328
+ const liveSessionTasks = snapshot.tasks.filter((task) => task.status !== "deleted");
6329
+ const mergedTasks = [...foreignTasks, ...liveSessionTasks];
6330
+ const tmpPath = snapshot.storePath + ".tmp";
6331
+ await fs11.mkdir(path11.dirname(snapshot.storePath), { recursive: true });
6332
+ await fs11.writeFile(
6333
+ tmpPath,
6334
+ JSON.stringify({ version: 1, tasks: mergedTasks }, null, 2)
6335
+ );
6336
+ await fs11.rename(tmpPath, snapshot.storePath);
6337
+ }
6338
+ function schedulePersist() {
6339
+ if (!storePath || !currentSessionId) return pendingWrite;
6340
+ const snapshot = {
6341
+ storePath,
6342
+ sessionId: currentSessionId,
6343
+ tasks: cloneTasks(currentTasks)
6344
+ };
6345
+ pendingWrite = pendingWrite.catch(() => void 0).then(() => persistSnapshot(snapshot));
6346
+ void pendingWrite.catch(() => void 0);
6347
+ return pendingWrite;
6348
+ }
6349
+ function ensureSessionId() {
6350
+ if (!currentSessionId) {
6351
+ currentSessionId = crypto.randomUUID();
6352
+ }
6353
+ return currentSessionId;
6354
+ }
6355
+ async function initTaskStore(workspaceDir, sessionId2 = crypto.randomUUID()) {
6327
6356
  storePath = path11.join(workspaceDir, ".open-research", "tasks.json");
6328
6357
  const data = await readJsonFile(storePath, { version: 1, tasks: [] });
6329
- tasks = data.tasks ?? [];
6358
+ currentSessionId = sessionId2;
6359
+ currentTasks = (data.tasks ?? []).filter(
6360
+ (task) => task.status !== "deleted" && task.sessionId === currentSessionId
6361
+ );
6330
6362
  }
6331
6363
  function clearAllTasks() {
6332
- tasks = [];
6333
- void persist();
6364
+ currentTasks = [];
6365
+ void schedulePersist();
6334
6366
  }
6335
6367
  function executeCreateTasks(args) {
6368
+ const sessionId2 = ensureSessionId();
6336
6369
  const created = [];
6337
6370
  for (const item of args.tasks) {
6338
6371
  const task = {
6339
6372
  id: shortId(),
6373
+ sessionId: sessionId2,
6340
6374
  subject: item.subject,
6341
6375
  activeForm: item.activeForm,
6342
6376
  status: "pending",
6343
6377
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
6344
6378
  };
6345
- tasks.push(task);
6379
+ currentTasks.push(task);
6346
6380
  created.push(task);
6347
6381
  }
6348
- void persist();
6382
+ void schedulePersist();
6349
6383
  return created.map((t) => `[${t.id}] ${t.subject}`).join("\n");
6350
6384
  }
6351
6385
  function executeUpdateTask(args) {
6352
- const task = tasks.find((t) => t.id === args.taskId);
6386
+ const task = currentTasks.find((t) => t.id === args.taskId);
6353
6387
  if (!task) return `Task not found: ${args.taskId}`;
6354
6388
  if (args.status) {
6355
6389
  task.status = args.status;
6356
- if (args.status === "completed") task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
6390
+ if (args.status === "completed") {
6391
+ task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
6392
+ } else {
6393
+ delete task.completedAt;
6394
+ }
6357
6395
  }
6358
6396
  if (args.subject !== void 0) task.subject = args.subject;
6359
6397
  if (args.activeForm !== void 0) task.activeForm = args.activeForm;
6360
- void persist();
6398
+ if (task.status === "deleted") {
6399
+ currentTasks = currentTasks.filter((candidate) => candidate.id !== task.id);
6400
+ }
6401
+ void schedulePersist();
6361
6402
  const label = task.status === "deleted" ? "deleted" : `${task.subject} \u2192 ${task.status}`;
6362
6403
  return `Task updated: ${label}`;
6363
6404
  }
6364
6405
  function getTaskContextBlock() {
6365
- const live = tasks.filter((t) => t.status !== "deleted");
6406
+ const live = currentTasks.filter((t) => t.status !== "deleted");
6366
6407
  if (live.length === 0) return null;
6367
6408
  const lines = live.map((t) => {
6368
6409
  if (t.status === "completed") return `[x] ${t.subject}`;
@@ -6373,7 +6414,7 @@ function getTaskContextBlock() {
6373
6414
  ${lines.join("\n")}`;
6374
6415
  }
6375
6416
  function getVisibleTasks() {
6376
- return tasks.filter((t) => t.status !== "deleted");
6417
+ return currentTasks.filter((t) => t.status !== "deleted");
6377
6418
  }
6378
6419
 
6379
6420
  // src/lib/agent/tool-dispatcher.ts
@@ -7269,8 +7310,8 @@ var TOOL_DESCRIPTIONS = {
7269
7310
  return `Sub-agent (${type ?? "explore"}): ${goal?.slice(0, 60) ?? "task"}`;
7270
7311
  },
7271
7312
  create_tasks: (a) => {
7272
- const tasks2 = a.tasks;
7273
- return `Creating ${tasks2?.length ?? 0} task${(tasks2?.length ?? 0) !== 1 ? "s" : ""}`;
7313
+ const tasks = a.tasks;
7314
+ return `Creating ${tasks?.length ?? 0} task${(tasks?.length ?? 0) !== 1 ? "s" : ""}`;
7274
7315
  },
7275
7316
  update_task: (a) => {
7276
7317
  const status = a.status;
@@ -8341,15 +8382,15 @@ function ThinkingIndicator({ frame, width }) {
8341
8382
  ] });
8342
8383
  }
8343
8384
  function TaskPanel({
8344
- tasks: tasks2,
8385
+ tasks,
8345
8386
  frame,
8346
8387
  width
8347
8388
  }) {
8348
8389
  const theme = useTheme();
8349
8390
  const contentWidth = resolveWidth(width);
8350
- const completed = tasks2.filter((t) => t.status === "completed");
8351
- const active = tasks2.filter((t) => t.status !== "completed");
8352
- if (tasks2.length === 0) return null;
8391
+ const completed = tasks.filter((t) => t.status === "completed");
8392
+ const active = tasks.filter((t) => t.status !== "completed");
8393
+ if (tasks.length === 0) return null;
8353
8394
  return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginBottom: 1, marginLeft: 2, width: indentedWidth(contentWidth), children: [
8354
8395
  active.map((task) => {
8355
8396
  const icon = task.status === "in_progress" ? frame : GUTTER.pending;
@@ -9090,26 +9131,127 @@ function createSentenceStreamBuffer({
9090
9131
 
9091
9132
  // src/tui/hooks/use-animated-frame.ts
9092
9133
  import { useState as useState5, useEffect as useEffect2 } from "react";
9134
+ import { useStdout as useStdout2 } from "ink";
9135
+
9136
+ // src/tui/ink-stdout.ts
9137
+ var RAW_COLUMNS = /* @__PURE__ */ Symbol("open-research.raw-columns");
9138
+ var RAW_ROWS = /* @__PURE__ */ Symbol("open-research.raw-rows");
9139
+ function getStableRow(current, fallback) {
9140
+ return typeof current === "number" && current > 0 ? current : fallback;
9141
+ }
9142
+ function getStableColumn(current, fallback) {
9143
+ return typeof current === "number" && current >= MIN_TERMINAL_WIDTH ? current : fallback;
9144
+ }
9145
+ function getRawDimension(current) {
9146
+ return typeof current === "number" && Number.isFinite(current) ? current : void 0;
9147
+ }
9148
+ function getRawTerminalDimensions(stdout) {
9149
+ const stream = stdout;
9150
+ return {
9151
+ columns: getRawDimension(stream[RAW_COLUMNS] ?? stream.columns),
9152
+ rows: getRawDimension(stream[RAW_ROWS] ?? stream.rows)
9153
+ };
9154
+ }
9155
+ function hasRenderableTerminalDimensions(stdout) {
9156
+ const { columns, rows } = getRawTerminalDimensions(stdout);
9157
+ const hasRows = rows === void 0 || rows > 0;
9158
+ return hasRows && typeof columns === "number" && columns >= MIN_TERMINAL_WIDTH;
9159
+ }
9160
+ function createStableInkStdout(stdout, options) {
9161
+ const forceFullRedraw = options?.forceFullRedraw ?? process.env.OPEN_RESEARCH_FORCE_FULL_REDRAW === "1";
9162
+ const initialRows = getRawDimension(stdout.rows);
9163
+ const initialColumns = getRawDimension(stdout.columns);
9164
+ let lastRows = getStableRow(initialRows, 24);
9165
+ let lastColumns = typeof initialColumns === "number" && initialColumns > 0 ? Math.max(MIN_TERMINAL_WIDTH, initialColumns) : 80;
9166
+ const resizeListeners = /* @__PURE__ */ new Map();
9167
+ return new Proxy(stdout, {
9168
+ get(target, prop, receiver) {
9169
+ if (prop === RAW_ROWS) {
9170
+ return getRawDimension(Reflect.get(target, "rows", receiver));
9171
+ }
9172
+ if (prop === RAW_COLUMNS) {
9173
+ return getRawDimension(Reflect.get(target, "columns", receiver));
9174
+ }
9175
+ if (prop === "rows") {
9176
+ if (forceFullRedraw) {
9177
+ return 0;
9178
+ }
9179
+ const rows = Reflect.get(target, prop, receiver);
9180
+ lastRows = getStableRow(rows, lastRows);
9181
+ return lastRows;
9182
+ }
9183
+ if (prop === "columns") {
9184
+ const columns = Reflect.get(target, prop, receiver);
9185
+ lastColumns = getStableColumn(columns, lastColumns);
9186
+ return lastColumns;
9187
+ }
9188
+ if (prop === "on" || prop === "addListener" || prop === "once" || prop === "prependListener") {
9189
+ const method = prop;
9190
+ return (eventName, listener) => {
9191
+ if (eventName === "resize" && !forceFullRedraw) {
9192
+ const wrapped = (...args) => {
9193
+ const rawColumns = getRawDimension(Reflect.get(target, "columns", receiver));
9194
+ const rawRows = getRawDimension(Reflect.get(target, "rows", receiver));
9195
+ lastColumns = getStableColumn(rawColumns, lastColumns);
9196
+ lastRows = getStableRow(rawRows, lastRows);
9197
+ if (!hasRenderableTerminalDimensions({
9198
+ [RAW_COLUMNS]: rawColumns,
9199
+ [RAW_ROWS]: rawRows
9200
+ })) {
9201
+ return;
9202
+ }
9203
+ listener(...args);
9204
+ };
9205
+ resizeListeners.set(listener, wrapped);
9206
+ Reflect.get(target, method, receiver).call(target, eventName, wrapped);
9207
+ return receiver;
9208
+ }
9209
+ Reflect.get(target, method, receiver).call(target, eventName, listener);
9210
+ return receiver;
9211
+ };
9212
+ }
9213
+ if (prop === "off" || prop === "removeListener") {
9214
+ const method = prop;
9215
+ return (eventName, listener) => {
9216
+ const wrapped = eventName === "resize" ? resizeListeners.get(listener) ?? listener : listener;
9217
+ resizeListeners.delete(listener);
9218
+ Reflect.get(target, method, receiver).call(target, eventName, wrapped);
9219
+ return receiver;
9220
+ };
9221
+ }
9222
+ const value = Reflect.get(target, prop, receiver);
9223
+ return typeof value === "function" ? value.bind(target) : value;
9224
+ }
9225
+ });
9226
+ }
9227
+
9228
+ // src/tui/hooks/use-animated-frame.ts
9093
9229
  var SPINNER_FRAMES = ["\u25D0", "\u25D3", "\u25D1", "\u25D2"];
9094
9230
  function useAnimatedFrame(active) {
9231
+ const { stdout } = useStdout2();
9095
9232
  const [index, setIndex] = useState5(0);
9096
9233
  useEffect2(() => {
9097
9234
  if (!active) {
9098
9235
  setIndex(0);
9099
9236
  return;
9100
9237
  }
9101
- const timer = setInterval(() => setIndex((v) => (v + 1) % SPINNER_FRAMES.length), 120);
9238
+ const timer = setInterval(() => {
9239
+ if (!hasRenderableTerminalDimensions(stdout)) {
9240
+ return;
9241
+ }
9242
+ setIndex((v) => (v + 1) % SPINNER_FRAMES.length);
9243
+ }, 120);
9102
9244
  return () => clearInterval(timer);
9103
- }, [active]);
9245
+ }, [active, stdout]);
9104
9246
  return SPINNER_FRAMES[index] ?? SPINNER_FRAMES[0];
9105
9247
  }
9106
9248
 
9107
9249
  // src/tui/hooks/use-terminal-width.ts
9108
9250
  import { useState as useState6, useEffect as useEffect3 } from "react";
9109
- import { useStdout as useStdout2 } from "ink";
9251
+ import { useStdout as useStdout3 } from "ink";
9110
9252
  var RESIZE_DEBOUNCE_MS = 50;
9111
9253
  function useTerminalWidth() {
9112
- const { stdout } = useStdout2();
9254
+ const { stdout } = useStdout3();
9113
9255
  const [terminalWidth, setTerminalWidth] = useState6(
9114
9256
  () => getObservedTerminalWidth(stdout.columns, process.stdout.columns)
9115
9257
  );
@@ -10402,7 +10544,7 @@ function App({
10402
10544
  const [screen, setScreen] = useState7("main");
10403
10545
  const [resumeSessions, setResumeSessions] = useState7([]);
10404
10546
  const [ctrlCPending, setCtrlCPending] = useState7(false);
10405
- const sessionId2 = useMemo4(() => crypto.randomUUID(), []);
10547
+ const [sessionId2, setSessionId] = useState7(() => crypto.randomUUID());
10406
10548
  const deferredPendingUpdates = useDeferredValue(pendingUpdates);
10407
10549
  const visiblePendingUpdates = deferredPendingUpdates.length > 0 ? deferredPendingUpdates : pendingUpdates;
10408
10550
  const activityFrame = useAnimatedFrame(busy);
@@ -10499,13 +10641,13 @@ function App({
10499
10641
  if (cancelled) return;
10500
10642
  startTransition2(() => setWorkspaceFiles(result.files));
10501
10643
  });
10502
- void initTaskStore(workspacePath).then(() => {
10644
+ void initTaskStore(workspacePath, sessionId2).then(() => {
10503
10645
  if (!cancelled) setTaskVersion((v) => v + 1);
10504
10646
  });
10505
10647
  return () => {
10506
10648
  cancelled = true;
10507
10649
  };
10508
- }, [workspacePath]);
10650
+ }, [workspacePath, sessionId2]);
10509
10651
  useEffect4(() => {
10510
10652
  let cancelled = false;
10511
10653
  void listAvailableSkills({ homeDir }).then((available) => {
@@ -11199,6 +11341,9 @@ ${error.stack}` : String(error)}` }
11199
11341
  onSelect: async (session) => {
11200
11342
  try {
11201
11343
  const restored = await loadSessionHistory(workspacePath, session.id);
11344
+ setSessionId(session.id);
11345
+ await initTaskStore(workspacePath, session.id);
11346
+ setTaskVersion((version) => version + 1);
11202
11347
  replaceMessages(restored.messages);
11203
11348
  startTransition2(() => setHistory(restored.llmHistory));
11204
11349
  addSystemMessage(`Resumed session (${session.turnCount} turns). Continue where you left off.`);
@@ -11389,35 +11534,6 @@ ${error.stack}` : String(error)}` }
11389
11534
  ] }) });
11390
11535
  }
11391
11536
 
11392
- // src/tui/ink-stdout.ts
11393
- function getStableDimension(current, fallback) {
11394
- return typeof current === "number" && current > 0 ? current : fallback;
11395
- }
11396
- function createStableInkStdout(stdout, options) {
11397
- const forceFullRedraw = options?.forceFullRedraw ?? process.env.OPEN_RESEARCH_FORCE_FULL_REDRAW === "1";
11398
- let lastRows = getStableDimension(stdout.rows, 24);
11399
- let lastColumns = getStableDimension(stdout.columns, 80);
11400
- return new Proxy(stdout, {
11401
- get(target, prop, receiver) {
11402
- if (prop === "rows") {
11403
- if (forceFullRedraw) {
11404
- return 0;
11405
- }
11406
- const rows = Reflect.get(target, prop, receiver);
11407
- lastRows = getStableDimension(rows, lastRows);
11408
- return lastRows;
11409
- }
11410
- if (prop === "columns") {
11411
- const columns = Reflect.get(target, prop, receiver);
11412
- lastColumns = getStableDimension(columns, lastColumns);
11413
- return lastColumns;
11414
- }
11415
- const value = Reflect.get(target, prop, receiver);
11416
- return typeof value === "function" ? value.bind(target) : value;
11417
- }
11418
- });
11419
- }
11420
-
11421
11537
  // src/cli.ts
11422
11538
  var program = new Command();
11423
11539
  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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-research",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
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",