open-research 0.1.22 → 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 +185 -44
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
8
8
 
9
9
  // src/cli.ts
10
10
  import React5 from "react";
11
- import path21 from "path";
11
+ import path22 from "path";
12
12
  import { Command } from "commander";
13
13
  import { render } from "ink";
14
14
 
@@ -811,7 +811,7 @@ function formatDateTime(value) {
811
811
  }
812
812
 
813
813
  // src/lib/cli/version.ts
814
- var PACKAGE_VERSION = "0.1.22";
814
+ var PACKAGE_VERSION = "0.1.24";
815
815
  function getPackageVersion() {
816
816
  return PACKAGE_VERSION;
817
817
  }
@@ -871,7 +871,7 @@ async function ensureOpenResearchConfig(options) {
871
871
  }
872
872
 
873
873
  // src/tui/app.tsx
874
- import path20 from "path";
874
+ import path21 from "path";
875
875
  import {
876
876
  startTransition,
877
877
  useDeferredValue,
@@ -1502,7 +1502,7 @@ function TextInput({
1502
1502
  }
1503
1503
  const renderedPlaceholder = showCursor && focus && placeholder.length > 0 ? source_default.inverse(placeholder[0]) + source_default.grey(placeholder.slice(1)) : placeholder ? source_default.grey(placeholder) : void 0;
1504
1504
  function sanitizeInput(raw) {
1505
- return raw.replace(/\x1b\[[?>=!]*[0-9;]*[a-zA-Z~]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?/g, "").replace(/\[20[01]~/g, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "");
1505
+ return raw.replace(/\x1b\[[?>=!]*[0-9;]*[a-zA-Z~]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?/g, "").replace(/\[20[01]~/g, "").replace(/\d+;\d+;\d+[~u]/g, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "");
1506
1506
  }
1507
1507
  function insertCleanText(raw, currentValue, currentCursor) {
1508
1508
  const clean = sanitizeInput(raw);
@@ -1542,7 +1542,7 @@ function TextInput({
1542
1542
  onTab?.();
1543
1543
  return;
1544
1544
  }
1545
- if (key.return && key.shift || key.return && key.meta) {
1545
+ if (key.return && key.shift || key.return && key.meta || input2 === "27;2;13~" || input2.includes("27;2;13")) {
1546
1546
  const inserted = currentValue.slice(0, currentCursor) + "\n" + currentValue.slice(currentCursor);
1547
1547
  cursorOffsetRef.current = currentCursor + 1;
1548
1548
  valueRef.current = inserted;
@@ -5414,6 +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: `notes/brief.md`, `experiments/analysis.py`. Include line references as `src/file.ts:42`. This makes them clickable.",
5417
5418
  "",
5418
5419
  `## Workspace
5419
5420
  Root: ${process.cwd()}
@@ -6616,9 +6617,88 @@ function truncate3(value, max = 96) {
6616
6617
  import { Box as Box4, Text as Text4 } from "ink";
6617
6618
 
6618
6619
  // src/tui/markdown.ts
6619
- function renderMarkdown(text) {
6620
+ import path20 from "path";
6621
+ import fs21 from "fs";
6622
+ import { pathToFileURL } from "url";
6623
+ var FILE_EXTENSIONS = /* @__PURE__ */ new Set([
6624
+ ".py",
6625
+ ".ts",
6626
+ ".tsx",
6627
+ ".js",
6628
+ ".jsx",
6629
+ ".r",
6630
+ ".R",
6631
+ ".tex",
6632
+ ".bib",
6633
+ ".md",
6634
+ ".txt",
6635
+ ".json",
6636
+ ".yaml",
6637
+ ".yml",
6638
+ ".toml",
6639
+ ".csv",
6640
+ ".tsv",
6641
+ ".sh",
6642
+ ".bash",
6643
+ ".zsh",
6644
+ ".sql",
6645
+ ".html",
6646
+ ".css",
6647
+ ".xml",
6648
+ ".pdf",
6649
+ ".png",
6650
+ ".jpg",
6651
+ ".svg",
6652
+ ".gif",
6653
+ ".cfg",
6654
+ ".ini",
6655
+ ".env",
6656
+ ".lock",
6657
+ ".log"
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
+ }
6669
+ function looksLikeFilePath(text) {
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();
6675
+ if (FILE_EXTENSIONS.has(ext)) return true;
6676
+ if (filePath.includes("/") || filePath.includes("\\")) return true;
6677
+ return false;
6678
+ }
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);
6690
+ }
6691
+ if (!resolved) return displayText;
6692
+ const uri = pathToFileURL(resolved).href;
6693
+ return `\x1B]8;;${uri}\x1B\\${displayText}\x1B]8;;\x1B\\`;
6694
+ }
6695
+ var BARE_PATH_RE = /((?:\.{1,2}\/|\/)[^\s`),;\]]+\.[a-zA-Z0-9]{1,6})/g;
6696
+ function renderMarkdown(text, options = {}) {
6620
6697
  if (!text || !text.trim()) return text;
6621
- 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
+ }
6622
6702
  const lines = text.split("\n");
6623
6703
  const output2 = [];
6624
6704
  let inCodeBlock = false;
@@ -6651,14 +6731,10 @@ function renderMarkdown(text) {
6651
6731
  const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
6652
6732
  if (headingMatch) {
6653
6733
  const level = headingMatch[1].length;
6654
- const content = renderInline(headingMatch[2]);
6655
- if (level === 1) {
6656
- output2.push(source_default.bold.cyan(content));
6657
- } else if (level === 2) {
6658
- output2.push(source_default.bold.white(content));
6659
- } else {
6660
- output2.push(source_default.bold(content));
6661
- }
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));
6662
6738
  continue;
6663
6739
  }
6664
6740
  if (/^[-*_]{3,}\s*$/.test(line.trim())) {
@@ -6666,26 +6742,21 @@ function renderMarkdown(text) {
6666
6742
  continue;
6667
6743
  }
6668
6744
  if (line.trimStart().startsWith("> ")) {
6669
- const content = renderInline(line.replace(/^\s*>\s?/, ""));
6745
+ const content = renderInline(line.replace(/^\s*>\s?/, ""), baseDir);
6670
6746
  output2.push(source_default.gray("\u2502 ") + source_default.italic(content));
6671
6747
  continue;
6672
6748
  }
6673
6749
  const ulMatch = line.match(/^(\s*)[*+-]\s+(.+)$/);
6674
6750
  if (ulMatch) {
6675
- const indent = ulMatch[1];
6676
- const content = renderInline(ulMatch[2]);
6677
- output2.push(`${indent}${source_default.gray("\u2022")} ${content}`);
6751
+ output2.push(`${ulMatch[1]}${source_default.gray("\u2022")} ${renderInline(ulMatch[2], baseDir)}`);
6678
6752
  continue;
6679
6753
  }
6680
6754
  const olMatch = line.match(/^(\s*)(\d+)[.)]\s+(.+)$/);
6681
6755
  if (olMatch) {
6682
- const indent = olMatch[1];
6683
- const num = olMatch[2];
6684
- const content = renderInline(olMatch[3]);
6685
- output2.push(`${indent}${source_default.gray(num + ".")} ${content}`);
6756
+ output2.push(`${olMatch[1]}${source_default.gray(olMatch[2] + ".")} ${renderInline(olMatch[3], baseDir)}`);
6686
6757
  continue;
6687
6758
  }
6688
- output2.push(renderInline(line));
6759
+ output2.push(renderInline(line, baseDir));
6689
6760
  }
6690
6761
  if (inCodeBlock && codeBlockLines.length > 0) {
6691
6762
  output2.push(source_default.gray("\u250C" + "\u2500".repeat(40)));
@@ -6696,9 +6767,14 @@ function renderMarkdown(text) {
6696
6767
  }
6697
6768
  return output2.join("\n");
6698
6769
  }
6699
- function renderInline(text) {
6770
+ function renderInline(text, baseDir) {
6700
6771
  let result = text;
6701
- result = result.replace(/`([^`]+)`/g, (_, code) => source_default.cyan(code));
6772
+ result = result.replace(/`([^`]+)`/g, (_, code) => {
6773
+ if (looksLikeFilePath(code)) {
6774
+ return fileLink(source_default.cyan.underline(code), code, baseDir);
6775
+ }
6776
+ return source_default.cyan(code);
6777
+ });
6702
6778
  result = result.replace(/\*\*\*(.+?)\*\*\*/g, (_, t) => source_default.bold.italic(t));
6703
6779
  result = result.replace(/___(.+?)___/g, (_, t) => source_default.bold.italic(t));
6704
6780
  result = result.replace(/\*\*(.+?)\*\*/g, (_, t) => source_default.bold(t));
@@ -6710,6 +6786,12 @@ function renderInline(text) {
6710
6786
  /\[([^\]]+)\]\(([^)]+)\)/g,
6711
6787
  (_, label, url) => source_default.blue(label) + source_default.gray.dim(` (${url})`)
6712
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
+ });
6713
6795
  return result;
6714
6796
  }
6715
6797
 
@@ -6738,8 +6820,8 @@ function UserMessage({ text }) {
6738
6820
  /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, children: /* @__PURE__ */ jsx4(Text4, { children: text }) })
6739
6821
  ] });
6740
6822
  }
6741
- function AgentMessage({ text }) {
6742
- const rendered = renderMarkdown(text);
6823
+ function AgentMessage({ text, workspaceDir }) {
6824
+ const rendered = renderMarkdown(text, { baseDir: workspaceDir });
6743
6825
  return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginBottom: 1, children: [
6744
6826
  /* @__PURE__ */ jsxs3(Box4, { children: [
6745
6827
  /* @__PURE__ */ jsxs3(Text4, { color: "green", bold: true, children: [
@@ -7078,12 +7160,14 @@ function App({
7078
7160
  const [cursorToEnd, setCursorToEnd] = useState4(0);
7079
7161
  const [screen, setScreen] = useState4("main");
7080
7162
  const [resumeSessions, setResumeSessions] = useState4([]);
7163
+ const [ctrlCPending, setCtrlCPending] = useState4(false);
7081
7164
  const sessionId = useMemo3(() => crypto.randomUUID(), []);
7082
7165
  const deferredMessages = useDeferredValue(messages);
7083
7166
  const deferredPendingUpdates = useDeferredValue(pendingUpdates);
7084
7167
  const activityFrame = useAnimatedFrame(busy);
7085
7168
  const [agentQuestion, setAgentQuestion] = useState4(null);
7086
7169
  const previewRef = useRef2(null);
7170
+ const ctrlCTimerRef = useRef2(null);
7087
7171
  const isHome = deferredMessages.length === 0 && !busy;
7088
7172
  const hasWorkspace = workspacePath !== null;
7089
7173
  const hasAuth = authStatus === "connected";
@@ -7136,6 +7220,13 @@ function App({
7136
7220
  cancelled = true;
7137
7221
  };
7138
7222
  }, [homeDir]);
7223
+ useEffect2(() => {
7224
+ return () => {
7225
+ if (ctrlCTimerRef.current) {
7226
+ clearTimeout(ctrlCTimerRef.current);
7227
+ }
7228
+ };
7229
+ }, []);
7139
7230
  const [selectedSuggestion, setSelectedSuggestion] = useState4(-1);
7140
7231
  const atMention = useMemo3(() => extractAtMention(input2), [input2]);
7141
7232
  const suggestions = useMemo3(() => {
@@ -7667,7 +7758,58 @@ ${msg.text}
7667
7758
  addSystemMessage(`Rejected: ${next.summary}`);
7668
7759
  });
7669
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
+ }
7670
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
+ }
7671
7813
  if (inputKey.shift && inputKey.tab) {
7672
7814
  setAgentMode((prev) => {
7673
7815
  const modes = ["manual-review", "auto-approve", "auto-research"];
@@ -8066,6 +8208,7 @@ ${error.stack}` : String(error)}` }
8066
8208
  addSystemMessage("Charter cancelled. Planning reset.");
8067
8209
  }
8068
8210
  const statusParts = [];
8211
+ if (ctrlCPending) statusParts.push("Press Ctrl+C again to exit.");
8069
8212
  if (hasAuth) statusParts.push("connected");
8070
8213
  else statusParts.push("no auth");
8071
8214
  if (hasWorkspace) statusParts.push(`${workspaceFiles.length} files`);
@@ -8073,7 +8216,7 @@ ${error.stack}` : String(error)}` }
8073
8216
  if (skills2.length > 0) statusParts.push(`${skills2.length} skills`);
8074
8217
  statusParts.push(agentMode);
8075
8218
  if (deferredPendingUpdates.length > 0) statusParts.push(`${deferredPendingUpdates.length} pending`);
8076
- 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";
8077
8220
  const configItems = useMemo3(() => [
8078
8221
  {
8079
8222
  key: "defaults.model",
@@ -8128,8 +8271,7 @@ ${error.stack}` : String(error)}` }
8128
8271
  await saveOpenResearchConfig(updated, { homeDir });
8129
8272
  }
8130
8273
  function handleConfigClose() {
8131
- setScreen("main");
8132
- setComposerFocused(true);
8274
+ returnToMainScreen();
8133
8275
  }
8134
8276
  if (screen === "resume") {
8135
8277
  return /* @__PURE__ */ jsx5(
@@ -8147,12 +8289,10 @@ ${error.stack}` : String(error)}` }
8147
8289
  } catch (err) {
8148
8290
  addSystemMessage(`Failed: ${err instanceof Error ? err.message : String(err)}`);
8149
8291
  }
8150
- setScreen("main");
8151
- setComposerFocused(true);
8292
+ returnToMainScreen();
8152
8293
  },
8153
8294
  onCancel: () => {
8154
- setScreen("main");
8155
- setComposerFocused(true);
8295
+ returnToMainScreen();
8156
8296
  }
8157
8297
  }
8158
8298
  );
@@ -8184,7 +8324,7 @@ ${error.stack}` : String(error)}` }
8184
8324
  if (msg.role === "user") {
8185
8325
  return /* @__PURE__ */ jsx5(UserMessage, { text: msg.text }, `msg-${idx}`);
8186
8326
  }
8187
- return /* @__PURE__ */ jsx5(AgentMessage, { text: msg.text }, `msg-${idx}`);
8327
+ return /* @__PURE__ */ jsx5(AgentMessage, { text: msg.text, workspaceDir: workspacePath ?? void 0 }, `msg-${idx}`);
8188
8328
  }) }),
8189
8329
  deferredPendingUpdates.length > 0 && /* @__PURE__ */ jsx5(
8190
8330
  PendingUpdateCard,
@@ -8305,7 +8445,7 @@ ${error.stack}` : String(error)}` }
8305
8445
  statusParts,
8306
8446
  statusColor,
8307
8447
  tokenDisplay,
8308
- workspaceName: hasWorkspace ? path20.basename(workspacePath) : process.cwd(),
8448
+ workspaceName: hasWorkspace ? path21.basename(workspacePath) : process.cwd(),
8309
8449
  mode: agentMode,
8310
8450
  planningStatus: planningState.status
8311
8451
  }
@@ -8317,7 +8457,7 @@ ${error.stack}` : String(error)}` }
8317
8457
  var program = new Command();
8318
8458
  program.name("open-research").version(getPackageVersion()).description("Local-first research CLI powered by ChatGPT/Codex auth.").argument("[workspacePath]", "Optional workspace path to open").action(async (workspacePath) => {
8319
8459
  await ensureOpenResearchConfig();
8320
- const target = workspacePath ? path21.resolve(workspacePath) : process.cwd();
8460
+ const target = workspacePath ? path22.resolve(workspacePath) : process.cwd();
8321
8461
  const project = await loadWorkspaceProject(target);
8322
8462
  const auth2 = await loadStoredAuth();
8323
8463
  render(
@@ -8328,12 +8468,13 @@ program.name("open-research").version(getPackageVersion()).description("Local-fi
8328
8468
  screen: "home",
8329
8469
  pendingUpdates: []
8330
8470
  }
8331
- })
8471
+ }),
8472
+ { exitOnCtrlC: false }
8332
8473
  );
8333
8474
  });
8334
8475
  program.command("init").argument("[workspacePath]").description("Initialize an Open Research workspace.").action(async (workspacePath) => {
8335
8476
  await ensureOpenResearchConfig();
8336
- const target = path21.resolve(workspacePath ?? process.cwd());
8477
+ const target = path22.resolve(workspacePath ?? process.cwd());
8337
8478
  const project = await initWorkspace({ workspaceDir: target });
8338
8479
  console.log(`Initialized workspace: ${target}`);
8339
8480
  console.log(`Title: ${project.title}`);
@@ -8402,8 +8543,8 @@ skills.command("create").argument("[name]").description("Scaffold a new user ski
8402
8543
  });
8403
8544
  skills.command("edit").argument("<name>").description("Open a user skill in $EDITOR.").action(async (name) => {
8404
8545
  await ensureOpenResearchConfig();
8405
- const skillDir = path21.join(getOpenResearchSkillsDir(), name);
8406
- openInEditor(path21.join(skillDir, "SKILL.md"));
8546
+ const skillDir = path22.join(getOpenResearchSkillsDir(), name);
8547
+ openInEditor(path22.join(skillDir, "SKILL.md"));
8407
8548
  const validation = await validateSkillDirectory({ skillDir });
8408
8549
  if (!validation.ok) {
8409
8550
  console.error(validation.errors.join("\n"));
@@ -8414,9 +8555,9 @@ skills.command("edit").argument("<name>").description("Open a user skill in $EDI
8414
8555
  });
8415
8556
  skills.command("validate").argument("[nameOrPath]").description("Validate one user skill.").action(async (nameOrPath) => {
8416
8557
  await ensureOpenResearchConfig();
8417
- const skillDir = nameOrPath ? path21.isAbsolute(nameOrPath) ? nameOrPath : path21.join(getOpenResearchSkillsDir(), nameOrPath) : getOpenResearchSkillsDir();
8558
+ const skillDir = nameOrPath ? path22.isAbsolute(nameOrPath) ? nameOrPath : path22.join(getOpenResearchSkillsDir(), nameOrPath) : getOpenResearchSkillsDir();
8418
8559
  const stat = await import("fs/promises").then(
8419
- (fs21) => fs21.stat(skillDir).catch(() => null)
8560
+ (fs22) => fs22.stat(skillDir).catch(() => null)
8420
8561
  );
8421
8562
  if (!stat) {
8422
8563
  throw new Error(`Skill path not found: ${skillDir}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-research",
3
- "version": "0.1.22",
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",