open-research 0.1.23 → 0.1.24

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 +124 -49
  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.23";
814
+ var PACKAGE_VERSION = "0.1.24";
815
815
  function getPackageVersion() {
816
816
  return PACKAGE_VERSION;
817
817
  }
@@ -5414,7 +5414,7 @@ ${skill.prompt}`).join("\n\n");
5414
5414
  "- Be transparent. Show the user what you're doing and why.",
5415
5415
  "- When unsure, ask. Use ask_user rather than guessing.",
5416
5416
  "- For large outputs, redirect to a file and read selectively.",
5417
- "- Always wrap file paths in backticks when mentioning them, e.g. `notes/brief.md` or `experiments/analysis.py`. This makes them clickable in the terminal.",
5417
+ "- Always wrap file paths in backticks: `notes/brief.md`, `experiments/analysis.py`. Include line references as `src/file.ts:42`. This makes them clickable.",
5418
5418
  "",
5419
5419
  `## Workspace
5420
5420
  Root: ${process.cwd()}
@@ -6619,6 +6619,7 @@ import { Box as Box4, Text as Text4 } from "ink";
6619
6619
  // src/tui/markdown.ts
6620
6620
  import path20 from "path";
6621
6621
  import fs21 from "fs";
6622
+ import { pathToFileURL } from "url";
6622
6623
  var FILE_EXTENSIONS = /* @__PURE__ */ new Set([
6623
6624
  ".py",
6624
6625
  ".ts",
@@ -6655,31 +6656,49 @@ var FILE_EXTENSIONS = /* @__PURE__ */ new Set([
6655
6656
  ".lock",
6656
6657
  ".log"
6657
6658
  ]);
6659
+ var linkCache = /* @__PURE__ */ new Map();
6660
+ var LOCATION_SUFFIX_RE = /^(.*?)(:\d+(?::\d+)?)$/;
6661
+ function splitLocation(text) {
6662
+ const trimmed = text.trim();
6663
+ const match = trimmed.match(LOCATION_SUFFIX_RE);
6664
+ if (match && match[1]) {
6665
+ return { filePath: match[1], location: match[2] };
6666
+ }
6667
+ return { filePath: trimmed, location: "" };
6668
+ }
6658
6669
  function looksLikeFilePath(text) {
6659
- if (text.length < 3 || text.length > 200) return false;
6660
- if (text.includes(" ") || text.includes("\n")) return false;
6661
- const ext = path20.extname(text).toLowerCase();
6670
+ const { filePath } = splitLocation(text);
6671
+ if (filePath.length < 3 || filePath.length > 260) return false;
6672
+ if (filePath.includes("\n")) return false;
6673
+ if (/^[a-z]+:\/\//i.test(filePath)) return false;
6674
+ const ext = path20.extname(filePath).toLowerCase();
6662
6675
  if (FILE_EXTENSIONS.has(ext)) return true;
6663
- if (text.includes("/") && !text.startsWith("http")) return true;
6676
+ if (filePath.includes("/") || filePath.includes("\\")) return true;
6664
6677
  return false;
6665
6678
  }
6666
- function resolveFilePath(filePath) {
6667
- if (path20.isAbsolute(filePath)) return filePath;
6668
- return path20.resolve(process.cwd(), filePath);
6669
- }
6670
- function fileLink(displayText, filePath) {
6671
- const absPath = resolveFilePath(filePath);
6672
- try {
6673
- fs21.statSync(absPath);
6674
- } catch {
6675
- return displayText;
6679
+ function fileLink(displayText, rawText, baseDir) {
6680
+ const { filePath } = splitLocation(rawText);
6681
+ const candidate = path20.isAbsolute(filePath) ? filePath : path20.resolve(baseDir, filePath);
6682
+ let resolved = linkCache.get(candidate);
6683
+ if (resolved === void 0) {
6684
+ try {
6685
+ resolved = fs21.realpathSync(candidate);
6686
+ } catch {
6687
+ resolved = null;
6688
+ }
6689
+ linkCache.set(candidate, resolved);
6676
6690
  }
6677
- const uri = `file://${absPath}`;
6691
+ if (!resolved) return displayText;
6692
+ const uri = pathToFileURL(resolved).href;
6678
6693
  return `\x1B]8;;${uri}\x1B\\${displayText}\x1B]8;;\x1B\\`;
6679
6694
  }
6680
- function renderMarkdown(text) {
6695
+ var BARE_PATH_RE = /((?:\.{1,2}\/|\/)[^\s`),;\]]+\.[a-zA-Z0-9]{1,6})/g;
6696
+ function renderMarkdown(text, options = {}) {
6681
6697
  if (!text || !text.trim()) return text;
6682
- if (!/[*_`#\[\]>~\-]/.test(text) && !text.includes("```")) return text;
6698
+ const baseDir = options.baseDir ?? process.cwd();
6699
+ if (!/[*_`#\[\]>~\-]/.test(text) && !text.includes("```") && !BARE_PATH_RE.test(text)) {
6700
+ return text;
6701
+ }
6683
6702
  const lines = text.split("\n");
6684
6703
  const output2 = [];
6685
6704
  let inCodeBlock = false;
@@ -6712,14 +6731,10 @@ function renderMarkdown(text) {
6712
6731
  const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
6713
6732
  if (headingMatch) {
6714
6733
  const level = headingMatch[1].length;
6715
- const content = renderInline(headingMatch[2]);
6716
- if (level === 1) {
6717
- output2.push(source_default.bold.cyan(content));
6718
- } else if (level === 2) {
6719
- output2.push(source_default.bold.white(content));
6720
- } else {
6721
- output2.push(source_default.bold(content));
6722
- }
6734
+ const content = renderInline(headingMatch[2], baseDir);
6735
+ if (level === 1) output2.push(source_default.bold.cyan(content));
6736
+ else if (level === 2) output2.push(source_default.bold.white(content));
6737
+ else output2.push(source_default.bold(content));
6723
6738
  continue;
6724
6739
  }
6725
6740
  if (/^[-*_]{3,}\s*$/.test(line.trim())) {
@@ -6727,26 +6742,21 @@ function renderMarkdown(text) {
6727
6742
  continue;
6728
6743
  }
6729
6744
  if (line.trimStart().startsWith("> ")) {
6730
- const content = renderInline(line.replace(/^\s*>\s?/, ""));
6745
+ const content = renderInline(line.replace(/^\s*>\s?/, ""), baseDir);
6731
6746
  output2.push(source_default.gray("\u2502 ") + source_default.italic(content));
6732
6747
  continue;
6733
6748
  }
6734
6749
  const ulMatch = line.match(/^(\s*)[*+-]\s+(.+)$/);
6735
6750
  if (ulMatch) {
6736
- const indent = ulMatch[1];
6737
- const content = renderInline(ulMatch[2]);
6738
- output2.push(`${indent}${source_default.gray("\u2022")} ${content}`);
6751
+ output2.push(`${ulMatch[1]}${source_default.gray("\u2022")} ${renderInline(ulMatch[2], baseDir)}`);
6739
6752
  continue;
6740
6753
  }
6741
6754
  const olMatch = line.match(/^(\s*)(\d+)[.)]\s+(.+)$/);
6742
6755
  if (olMatch) {
6743
- const indent = olMatch[1];
6744
- const num = olMatch[2];
6745
- const content = renderInline(olMatch[3]);
6746
- output2.push(`${indent}${source_default.gray(num + ".")} ${content}`);
6756
+ output2.push(`${olMatch[1]}${source_default.gray(olMatch[2] + ".")} ${renderInline(olMatch[3], baseDir)}`);
6747
6757
  continue;
6748
6758
  }
6749
- output2.push(renderInline(line));
6759
+ output2.push(renderInline(line, baseDir));
6750
6760
  }
6751
6761
  if (inCodeBlock && codeBlockLines.length > 0) {
6752
6762
  output2.push(source_default.gray("\u250C" + "\u2500".repeat(40)));
@@ -6757,11 +6767,11 @@ function renderMarkdown(text) {
6757
6767
  }
6758
6768
  return output2.join("\n");
6759
6769
  }
6760
- function renderInline(text) {
6770
+ function renderInline(text, baseDir) {
6761
6771
  let result = text;
6762
6772
  result = result.replace(/`([^`]+)`/g, (_, code) => {
6763
6773
  if (looksLikeFilePath(code)) {
6764
- return fileLink(source_default.cyan.underline(code), code);
6774
+ return fileLink(source_default.cyan.underline(code), code, baseDir);
6765
6775
  }
6766
6776
  return source_default.cyan(code);
6767
6777
  });
@@ -6776,6 +6786,12 @@ function renderInline(text) {
6776
6786
  /\[([^\]]+)\]\(([^)]+)\)/g,
6777
6787
  (_, label, url) => source_default.blue(label) + source_default.gray.dim(` (${url})`)
6778
6788
  );
6789
+ result = result.replace(BARE_PATH_RE, (match) => {
6790
+ if (looksLikeFilePath(match)) {
6791
+ return fileLink(source_default.cyan.underline(match), match, baseDir);
6792
+ }
6793
+ return match;
6794
+ });
6779
6795
  return result;
6780
6796
  }
6781
6797
 
@@ -6804,8 +6820,8 @@ function UserMessage({ text }) {
6804
6820
  /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, children: /* @__PURE__ */ jsx4(Text4, { children: text }) })
6805
6821
  ] });
6806
6822
  }
6807
- function AgentMessage({ text }) {
6808
- const rendered = renderMarkdown(text);
6823
+ function AgentMessage({ text, workspaceDir }) {
6824
+ const rendered = renderMarkdown(text, { baseDir: workspaceDir });
6809
6825
  return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginBottom: 1, children: [
6810
6826
  /* @__PURE__ */ jsxs3(Box4, { children: [
6811
6827
  /* @__PURE__ */ jsxs3(Text4, { color: "green", bold: true, children: [
@@ -7144,12 +7160,14 @@ function App({
7144
7160
  const [cursorToEnd, setCursorToEnd] = useState4(0);
7145
7161
  const [screen, setScreen] = useState4("main");
7146
7162
  const [resumeSessions, setResumeSessions] = useState4([]);
7163
+ const [ctrlCPending, setCtrlCPending] = useState4(false);
7147
7164
  const sessionId = useMemo3(() => crypto.randomUUID(), []);
7148
7165
  const deferredMessages = useDeferredValue(messages);
7149
7166
  const deferredPendingUpdates = useDeferredValue(pendingUpdates);
7150
7167
  const activityFrame = useAnimatedFrame(busy);
7151
7168
  const [agentQuestion, setAgentQuestion] = useState4(null);
7152
7169
  const previewRef = useRef2(null);
7170
+ const ctrlCTimerRef = useRef2(null);
7153
7171
  const isHome = deferredMessages.length === 0 && !busy;
7154
7172
  const hasWorkspace = workspacePath !== null;
7155
7173
  const hasAuth = authStatus === "connected";
@@ -7202,6 +7220,13 @@ function App({
7202
7220
  cancelled = true;
7203
7221
  };
7204
7222
  }, [homeDir]);
7223
+ useEffect2(() => {
7224
+ return () => {
7225
+ if (ctrlCTimerRef.current) {
7226
+ clearTimeout(ctrlCTimerRef.current);
7227
+ }
7228
+ };
7229
+ }, []);
7205
7230
  const [selectedSuggestion, setSelectedSuggestion] = useState4(-1);
7206
7231
  const atMention = useMemo3(() => extractAtMention(input2), [input2]);
7207
7232
  const suggestions = useMemo3(() => {
@@ -7733,7 +7758,58 @@ ${msg.text}
7733
7758
  addSystemMessage(`Rejected: ${next.summary}`);
7734
7759
  });
7735
7760
  }
7761
+ function clearCtrlCPending() {
7762
+ if (ctrlCTimerRef.current) {
7763
+ clearTimeout(ctrlCTimerRef.current);
7764
+ ctrlCTimerRef.current = null;
7765
+ }
7766
+ setCtrlCPending(false);
7767
+ }
7768
+ function armCtrlCExitWindow() {
7769
+ if (ctrlCTimerRef.current) {
7770
+ clearTimeout(ctrlCTimerRef.current);
7771
+ }
7772
+ setCtrlCPending(true);
7773
+ ctrlCTimerRef.current = setTimeout(() => {
7774
+ ctrlCTimerRef.current = null;
7775
+ setCtrlCPending(false);
7776
+ }, 3e3);
7777
+ }
7778
+ function returnToMainScreen() {
7779
+ setScreen("main");
7780
+ setComposerFocused(true);
7781
+ }
7736
7782
  useInput4((key, inputKey) => {
7783
+ if (inputKey.ctrl && key === "c") {
7784
+ if (busy) {
7785
+ clearCtrlCPending();
7786
+ if (abortRef.current) {
7787
+ abortRef.current.abort();
7788
+ addSystemMessage("Interrupting agent...");
7789
+ }
7790
+ return;
7791
+ }
7792
+ if (screen !== "main") {
7793
+ clearCtrlCPending();
7794
+ returnToMainScreen();
7795
+ return;
7796
+ }
7797
+ if (planningState.status === "charter-review") {
7798
+ clearCtrlCPending();
7799
+ rejectCharter();
7800
+ return;
7801
+ }
7802
+ if (ctrlCPending) {
7803
+ clearCtrlCPending();
7804
+ app.exit();
7805
+ return;
7806
+ }
7807
+ armCtrlCExitWindow();
7808
+ return;
7809
+ }
7810
+ if (ctrlCPending) {
7811
+ clearCtrlCPending();
7812
+ }
7737
7813
  if (inputKey.shift && inputKey.tab) {
7738
7814
  setAgentMode((prev) => {
7739
7815
  const modes = ["manual-review", "auto-approve", "auto-research"];
@@ -8132,6 +8208,7 @@ ${error.stack}` : String(error)}` }
8132
8208
  addSystemMessage("Charter cancelled. Planning reset.");
8133
8209
  }
8134
8210
  const statusParts = [];
8211
+ if (ctrlCPending) statusParts.push("Press Ctrl+C again to exit.");
8135
8212
  if (hasAuth) statusParts.push("connected");
8136
8213
  else statusParts.push("no auth");
8137
8214
  if (hasWorkspace) statusParts.push(`${workspaceFiles.length} files`);
@@ -8139,7 +8216,7 @@ ${error.stack}` : String(error)}` }
8139
8216
  if (skills2.length > 0) statusParts.push(`${skills2.length} skills`);
8140
8217
  statusParts.push(agentMode);
8141
8218
  if (deferredPendingUpdates.length > 0) statusParts.push(`${deferredPendingUpdates.length} pending`);
8142
- const statusColor = busy ? "yellow" : !hasAuth ? "red" : deferredPendingUpdates.length > 0 ? "magenta" : "green";
8219
+ const statusColor = busy ? "yellow" : ctrlCPending ? "yellow" : !hasAuth ? "red" : deferredPendingUpdates.length > 0 ? "magenta" : "green";
8143
8220
  const configItems = useMemo3(() => [
8144
8221
  {
8145
8222
  key: "defaults.model",
@@ -8194,8 +8271,7 @@ ${error.stack}` : String(error)}` }
8194
8271
  await saveOpenResearchConfig(updated, { homeDir });
8195
8272
  }
8196
8273
  function handleConfigClose() {
8197
- setScreen("main");
8198
- setComposerFocused(true);
8274
+ returnToMainScreen();
8199
8275
  }
8200
8276
  if (screen === "resume") {
8201
8277
  return /* @__PURE__ */ jsx5(
@@ -8213,12 +8289,10 @@ ${error.stack}` : String(error)}` }
8213
8289
  } catch (err) {
8214
8290
  addSystemMessage(`Failed: ${err instanceof Error ? err.message : String(err)}`);
8215
8291
  }
8216
- setScreen("main");
8217
- setComposerFocused(true);
8292
+ returnToMainScreen();
8218
8293
  },
8219
8294
  onCancel: () => {
8220
- setScreen("main");
8221
- setComposerFocused(true);
8295
+ returnToMainScreen();
8222
8296
  }
8223
8297
  }
8224
8298
  );
@@ -8250,7 +8324,7 @@ ${error.stack}` : String(error)}` }
8250
8324
  if (msg.role === "user") {
8251
8325
  return /* @__PURE__ */ jsx5(UserMessage, { text: msg.text }, `msg-${idx}`);
8252
8326
  }
8253
- return /* @__PURE__ */ jsx5(AgentMessage, { text: msg.text }, `msg-${idx}`);
8327
+ return /* @__PURE__ */ jsx5(AgentMessage, { text: msg.text, workspaceDir: workspacePath ?? void 0 }, `msg-${idx}`);
8254
8328
  }) }),
8255
8329
  deferredPendingUpdates.length > 0 && /* @__PURE__ */ jsx5(
8256
8330
  PendingUpdateCard,
@@ -8394,7 +8468,8 @@ program.name("open-research").version(getPackageVersion()).description("Local-fi
8394
8468
  screen: "home",
8395
8469
  pendingUpdates: []
8396
8470
  }
8397
- })
8471
+ }),
8472
+ { exitOnCtrlC: false }
8398
8473
  );
8399
8474
  });
8400
8475
  program.command("init").argument("[workspacePath]").description("Initialize an Open Research workspace.").action(async (workspacePath) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-research",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
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",