open-research 0.1.2 → 0.1.4
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.
- package/README.md +145 -22
- package/builtin-skills/data-analyst/SKILL.md +83 -0
- package/builtin-skills/devils-advocate/SKILL.md +31 -1
- package/builtin-skills/draft-paper/SKILL.md +65 -2
- package/builtin-skills/evidence-adjudicator/SKILL.md +42 -1
- package/builtin-skills/experiment-designer/SKILL.md +91 -2
- package/builtin-skills/literature-reviewer/SKILL.md +72 -0
- package/builtin-skills/methodology-critic/SKILL.md +36 -1
- package/builtin-skills/paper-explainer/SKILL.md +37 -2
- package/builtin-skills/source-scout/SKILL.md +39 -2
- package/builtin-skills/synthesis-updater/SKILL.md +37 -2
- package/dist/cli.js +682 -41
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
2
8
|
|
|
3
9
|
// src/cli.ts
|
|
4
10
|
import React4 from "react";
|
|
5
|
-
import
|
|
11
|
+
import path19 from "path";
|
|
6
12
|
import { Command } from "commander";
|
|
7
13
|
import { render } from "ink";
|
|
8
14
|
|
|
@@ -848,7 +854,7 @@ async function ensureOpenResearchConfig(options) {
|
|
|
848
854
|
}
|
|
849
855
|
|
|
850
856
|
// src/tui/app.tsx
|
|
851
|
-
import
|
|
857
|
+
import path18 from "path";
|
|
852
858
|
import {
|
|
853
859
|
startTransition,
|
|
854
860
|
useDeferredValue,
|
|
@@ -1773,11 +1779,17 @@ function createOpenAIAuthProvider(credentials, onTokenRefresh, onValidationChang
|
|
|
1773
1779
|
} else if (event.type === "response.completed") {
|
|
1774
1780
|
const resp = event.data.response;
|
|
1775
1781
|
if (resp?.usage) {
|
|
1776
|
-
const
|
|
1782
|
+
const u = resp.usage;
|
|
1783
|
+
const inputDetails = u.input_tokens_details;
|
|
1784
|
+
const outputDetails = u.output_tokens_details;
|
|
1785
|
+
const inputTokens = u.input_tokens ?? 0;
|
|
1786
|
+
const outputTokens = u.output_tokens ?? 0;
|
|
1777
1787
|
usageData = {
|
|
1778
|
-
promptTokens:
|
|
1779
|
-
completionTokens:
|
|
1780
|
-
totalTokens:
|
|
1788
|
+
promptTokens: inputTokens,
|
|
1789
|
+
completionTokens: outputTokens,
|
|
1790
|
+
totalTokens: u.total_tokens ?? inputTokens + outputTokens,
|
|
1791
|
+
cachedTokens: inputDetails?.cached_tokens ?? 0,
|
|
1792
|
+
reasoningTokens: outputDetails?.reasoning_tokens ?? 0
|
|
1781
1793
|
};
|
|
1782
1794
|
}
|
|
1783
1795
|
if (resp?.model) {
|
|
@@ -1889,11 +1901,17 @@ function createOpenAIAuthProvider(credentials, onTokenRefresh, onValidationChang
|
|
|
1889
1901
|
case "response.completed": {
|
|
1890
1902
|
const resp = event.data.response;
|
|
1891
1903
|
if (resp?.usage) {
|
|
1892
|
-
const
|
|
1904
|
+
const u = resp.usage;
|
|
1905
|
+
const inputDetails = u.input_tokens_details;
|
|
1906
|
+
const outputDetails = u.output_tokens_details;
|
|
1907
|
+
const inputTokens = u.input_tokens ?? 0;
|
|
1908
|
+
const outputTokens = u.output_tokens ?? 0;
|
|
1893
1909
|
usage = {
|
|
1894
|
-
promptTokens:
|
|
1895
|
-
completionTokens:
|
|
1896
|
-
totalTokens:
|
|
1910
|
+
promptTokens: inputTokens,
|
|
1911
|
+
completionTokens: outputTokens,
|
|
1912
|
+
totalTokens: u.total_tokens ?? inputTokens + outputTokens,
|
|
1913
|
+
cachedTokens: inputDetails?.cached_tokens ?? 0,
|
|
1914
|
+
reasoningTokens: outputDetails?.reasoning_tokens ?? 0
|
|
1897
1915
|
};
|
|
1898
1916
|
}
|
|
1899
1917
|
break;
|
|
@@ -4501,35 +4519,65 @@ var MODEL_CONTEXT_WINDOWS = {
|
|
|
4501
4519
|
"o4-mini": 2e5
|
|
4502
4520
|
};
|
|
4503
4521
|
var DEFAULT_CONTEXT_WINDOW = 128e3;
|
|
4504
|
-
var
|
|
4522
|
+
var AUTO_COMPACT_TOKEN_LIMIT = 25e4;
|
|
4505
4523
|
function getContextWindow(model) {
|
|
4506
4524
|
return MODEL_CONTEXT_WINDOWS[model] ?? DEFAULT_CONTEXT_WINDOW;
|
|
4507
4525
|
}
|
|
4508
4526
|
function getCompactThreshold(model) {
|
|
4509
|
-
|
|
4527
|
+
const window = getContextWindow(model);
|
|
4528
|
+
return window > AUTO_COMPACT_TOKEN_LIMIT ? AUTO_COMPACT_TOKEN_LIMIT : Math.floor(window * 0.8);
|
|
4529
|
+
}
|
|
4530
|
+
function emptyBreakdown() {
|
|
4531
|
+
return { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 }, total: 0 };
|
|
4510
4532
|
}
|
|
4511
4533
|
function createSessionUsage() {
|
|
4512
4534
|
return {
|
|
4535
|
+
cumulative: emptyBreakdown(),
|
|
4536
|
+
lastTurn: emptyBreakdown(),
|
|
4537
|
+
estimatedCurrentTokens: 0,
|
|
4538
|
+
compactionCount: 0,
|
|
4513
4539
|
inputTokens: 0,
|
|
4514
4540
|
outputTokens: 0,
|
|
4515
4541
|
totalTokens: 0,
|
|
4516
|
-
lastTurnTokens: 0
|
|
4517
|
-
estimatedCurrentTokens: 0,
|
|
4518
|
-
compactionCount: 0
|
|
4542
|
+
lastTurnTokens: 0
|
|
4519
4543
|
};
|
|
4520
4544
|
}
|
|
4521
4545
|
function updateUsageFromApi(usage, apiUsage) {
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4546
|
+
const cached = apiUsage.cachedTokens ?? 0;
|
|
4547
|
+
const reasoning = apiUsage.reasoningTokens ?? 0;
|
|
4548
|
+
const adjustedInput = Math.max(0, apiUsage.promptTokens - cached);
|
|
4549
|
+
const adjustedOutput = Math.max(0, apiUsage.completionTokens - reasoning);
|
|
4550
|
+
usage.cumulative.input += adjustedInput;
|
|
4551
|
+
usage.cumulative.output += adjustedOutput;
|
|
4552
|
+
usage.cumulative.reasoning += reasoning;
|
|
4553
|
+
usage.cumulative.cache.read += cached;
|
|
4554
|
+
usage.cumulative.total += apiUsage.totalTokens;
|
|
4555
|
+
usage.lastTurn = {
|
|
4556
|
+
input: adjustedInput,
|
|
4557
|
+
output: adjustedOutput,
|
|
4558
|
+
reasoning,
|
|
4559
|
+
cache: { read: cached, write: 0 },
|
|
4560
|
+
total: apiUsage.totalTokens
|
|
4561
|
+
};
|
|
4562
|
+
usage.inputTokens = usage.cumulative.input;
|
|
4563
|
+
usage.outputTokens = usage.cumulative.output;
|
|
4564
|
+
usage.totalTokens = usage.cumulative.total;
|
|
4525
4565
|
usage.lastTurnTokens = apiUsage.totalTokens;
|
|
4526
4566
|
}
|
|
4527
4567
|
var PRUNE_PROTECT_TOKENS = 4e4;
|
|
4528
4568
|
var PRUNE_MIN_SAVINGS = 2e4;
|
|
4569
|
+
var PRUNE_SKIP_RECENT_USER_TURNS = 2;
|
|
4529
4570
|
function pruneToolOutputs(messages) {
|
|
4571
|
+
const userIndices = [];
|
|
4572
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
4573
|
+
if (messages[i].role === "user") userIndices.push(i);
|
|
4574
|
+
}
|
|
4575
|
+
const protectBoundary = userIndices.length >= PRUNE_SKIP_RECENT_USER_TURNS ? userIndices[PRUNE_SKIP_RECENT_USER_TURNS - 1] : 0;
|
|
4530
4576
|
const toolIndices = [];
|
|
4531
4577
|
for (let i = 0; i < messages.length; i++) {
|
|
4532
|
-
if (messages[i].role === "tool"
|
|
4578
|
+
if (messages[i].role === "tool" && i < protectBoundary) {
|
|
4579
|
+
toolIndices.push(i);
|
|
4580
|
+
}
|
|
4533
4581
|
}
|
|
4534
4582
|
if (toolIndices.length === 0) return { messages, savedTokens: 0 };
|
|
4535
4583
|
let protectedTokens = 0;
|
|
@@ -4549,44 +4597,105 @@ function pruneToolOutputs(messages) {
|
|
|
4549
4597
|
const idx = toolIndices[i];
|
|
4550
4598
|
const msg = result[idx];
|
|
4551
4599
|
const oldTokens = estimateMessageTokens(msg);
|
|
4552
|
-
const
|
|
4553
|
-
savedTokens += oldTokens - estimateTokens(
|
|
4554
|
-
result[idx] = { ...msg, content:
|
|
4600
|
+
const stub = "[output pruned \u2014 use read_file to re-read if needed]";
|
|
4601
|
+
savedTokens += oldTokens - estimateTokens(stub);
|
|
4602
|
+
result[idx] = { ...msg, content: stub };
|
|
4555
4603
|
}
|
|
4556
4604
|
if (savedTokens < PRUNE_MIN_SAVINGS) {
|
|
4557
4605
|
return { messages, savedTokens: 0 };
|
|
4558
4606
|
}
|
|
4559
4607
|
return { messages: result, savedTokens };
|
|
4560
4608
|
}
|
|
4561
|
-
|
|
4609
|
+
var COMPACTION_SYSTEM_PROMPT = `You are a conversation summarizer for a research agent. Your job is to create a handoff summary that another agent instance can use to seamlessly continue the work.
|
|
4610
|
+
|
|
4611
|
+
Do not respond to any questions in the conversation. Only output the summary.
|
|
4612
|
+
Respond in the same language the user used.`;
|
|
4613
|
+
var COMPACTION_USER_TEMPLATE = `Provide a detailed summary of our conversation above for handoff to another agent that will continue the work.
|
|
4614
|
+
|
|
4615
|
+
Stick to this template:
|
|
4616
|
+
|
|
4617
|
+
## Goal
|
|
4618
|
+
[What is the user trying to accomplish? Be specific.]
|
|
4619
|
+
|
|
4620
|
+
## Instructions
|
|
4621
|
+
- [Important instructions or preferences the user gave]
|
|
4622
|
+
- [Research methodology constraints or requirements]
|
|
4623
|
+
- [If there is a research charter or plan, summarize its key points]
|
|
4624
|
+
|
|
4625
|
+
## Discoveries
|
|
4626
|
+
- [Key findings from paper searches, data analysis, or experiments]
|
|
4627
|
+
- [Important facts, numbers, or evidence discovered]
|
|
4628
|
+
- [Any surprising or contradicting results]
|
|
4629
|
+
|
|
4630
|
+
## Accomplished
|
|
4631
|
+
- [What work has been completed]
|
|
4632
|
+
- [What is currently in progress]
|
|
4633
|
+
- [What remains to be done]
|
|
4634
|
+
|
|
4635
|
+
## Relevant Files
|
|
4636
|
+
[List workspace files that were read, created, or modified. Include what each contains.]
|
|
4637
|
+
- path/to/file.md \u2014 description of contents
|
|
4638
|
+
- experiments/script.py \u2014 what it does and its results
|
|
4639
|
+
|
|
4640
|
+
## Active Context
|
|
4641
|
+
- [Current research question or hypothesis being investigated]
|
|
4642
|
+
- [Which skills are active]
|
|
4643
|
+
- [Any pending user decisions or questions]
|
|
4644
|
+
|
|
4645
|
+
## Next Steps
|
|
4646
|
+
1. [Most immediate next action]
|
|
4647
|
+
2. [Following action]
|
|
4648
|
+
3. [And so on]
|
|
4649
|
+
|
|
4650
|
+
{CUSTOM_INSTRUCTIONS}`;
|
|
4651
|
+
async function compactConversation(messages, provider, model, customInstructions, signal) {
|
|
4562
4652
|
const systemMsg = messages.find((m) => m.role === "system");
|
|
4563
4653
|
const conversationMsgs = messages.filter((m) => m.role !== "system");
|
|
4564
4654
|
const conversationText = conversationMsgs.map((m) => {
|
|
4565
4655
|
const role = m.role === "assistant" ? "Agent" : m.role === "user" ? "User" : "Tool";
|
|
4566
|
-
|
|
4567
|
-
|
|
4656
|
+
let content;
|
|
4657
|
+
if (typeof m.content === "string") {
|
|
4658
|
+
content = m.content.length > 3e3 ? m.content.slice(0, 3e3) + "\n[... truncated]" : m.content;
|
|
4659
|
+
} else if (m.content) {
|
|
4660
|
+
content = JSON.stringify(m.content).slice(0, 1e3);
|
|
4661
|
+
} else if (m.tool_calls?.length) {
|
|
4662
|
+
content = m.tool_calls.map((tc) => `[tool: ${tc.function.name}]`).join(", ");
|
|
4663
|
+
} else {
|
|
4664
|
+
content = "[empty]";
|
|
4665
|
+
}
|
|
4666
|
+
return `[${role}]: ${content}`;
|
|
4568
4667
|
}).join("\n\n");
|
|
4668
|
+
const customBlock = customInstructions ? `
|
|
4669
|
+
|
|
4670
|
+
Additional instructions: ${customInstructions}` : "";
|
|
4671
|
+
const userPrompt = COMPACTION_USER_TEMPLATE.replace("{CUSTOM_INSTRUCTIONS}", customBlock);
|
|
4672
|
+
const compactionModel = model.includes("5.4") ? "gpt-5.4-mini" : model;
|
|
4569
4673
|
const summaryResponse = await provider.callLLM({
|
|
4570
4674
|
messages: [
|
|
4571
|
-
{
|
|
4572
|
-
role: "system",
|
|
4573
|
-
content: "You are performing a CONTEXT COMPACTION. Summarize the conversation into a concise handoff document. Include:\n1. **Goal**: What the user is trying to accomplish\n2. **Key discoveries**: Important findings, file paths, data points\n3. **Work completed**: What has been done so far\n4. **Next steps**: What should happen next\n5. **Active files**: Key file paths and their contents summary\n\nBe concise but preserve all actionable information. This summary will replace the full conversation history."
|
|
4574
|
-
},
|
|
4675
|
+
{ role: "system", content: COMPACTION_SYSTEM_PROMPT },
|
|
4575
4676
|
{
|
|
4576
4677
|
role: "user",
|
|
4577
|
-
content: `
|
|
4678
|
+
content: `Here is the conversation to summarize:
|
|
4578
4679
|
|
|
4579
|
-
${conversationText.slice(0,
|
|
4680
|
+
${conversationText.slice(0, 12e4)}
|
|
4681
|
+
|
|
4682
|
+
---
|
|
4683
|
+
|
|
4684
|
+
${userPrompt}`
|
|
4580
4685
|
}
|
|
4581
4686
|
],
|
|
4582
|
-
model,
|
|
4687
|
+
model: compactionModel,
|
|
4583
4688
|
maxTokens: 4096
|
|
4584
4689
|
});
|
|
4585
4690
|
const compacted = [];
|
|
4586
4691
|
if (systemMsg) compacted.push(systemMsg);
|
|
4587
4692
|
compacted.push({
|
|
4588
4693
|
role: "user",
|
|
4589
|
-
content: "
|
|
4694
|
+
content: "What have we accomplished so far in this research session?"
|
|
4695
|
+
});
|
|
4696
|
+
compacted.push({
|
|
4697
|
+
role: "assistant",
|
|
4698
|
+
content: summaryResponse.content
|
|
4590
4699
|
});
|
|
4591
4700
|
return compacted;
|
|
4592
4701
|
}
|
|
@@ -4604,7 +4713,17 @@ async function maybeCompact(messages, model, provider, usage, signal) {
|
|
|
4604
4713
|
usage.compactionCount++;
|
|
4605
4714
|
return { messages: pruned, didCompact: true };
|
|
4606
4715
|
}
|
|
4607
|
-
const compacted = await compactConversation(pruned, provider, model, signal);
|
|
4716
|
+
const compacted = await compactConversation(pruned, provider, model, void 0, signal);
|
|
4717
|
+
usage.estimatedCurrentTokens = estimateConversationTokens(compacted);
|
|
4718
|
+
usage.compactionCount++;
|
|
4719
|
+
return { messages: compacted, didCompact: true };
|
|
4720
|
+
}
|
|
4721
|
+
async function manualCompact(messages, model, provider, usage, customInstructions, signal) {
|
|
4722
|
+
if (messages.length <= 2) {
|
|
4723
|
+
return { messages, didCompact: false };
|
|
4724
|
+
}
|
|
4725
|
+
const { messages: pruned } = pruneToolOutputs(messages);
|
|
4726
|
+
const compacted = await compactConversation(pruned, provider, model, customInstructions, signal);
|
|
4608
4727
|
usage.estimatedCurrentTokens = estimateConversationTokens(compacted);
|
|
4609
4728
|
usage.compactionCount++;
|
|
4610
4729
|
return { messages: compacted, didCompact: true };
|
|
@@ -5294,6 +5413,324 @@ async function checkForUpdate() {
|
|
|
5294
5413
|
}
|
|
5295
5414
|
}
|
|
5296
5415
|
|
|
5416
|
+
// src/lib/preview/server.ts
|
|
5417
|
+
import http2 from "http";
|
|
5418
|
+
import fs18 from "fs";
|
|
5419
|
+
import path17 from "path";
|
|
5420
|
+
|
|
5421
|
+
// src/lib/preview/latex-to-html.ts
|
|
5422
|
+
function latexToHtml(latex) {
|
|
5423
|
+
let body = latex;
|
|
5424
|
+
const docMatch = body.match(/\\begin\{document\}([\s\S]*?)\\end\{document\}/);
|
|
5425
|
+
if (docMatch) body = docMatch[1];
|
|
5426
|
+
const titleMatch = latex.match(/\\title\{([^}]*)\}/);
|
|
5427
|
+
const authorMatch = latex.match(/\\author\{([^}]*)\}/);
|
|
5428
|
+
const dateMatch = latex.match(/\\date\{([^}]*)\}/);
|
|
5429
|
+
const abstractMatch = body.match(/\\begin\{abstract\}([\s\S]*?)\\end\{abstract\}/);
|
|
5430
|
+
body = body.replace(/\\maketitle/, "");
|
|
5431
|
+
body = body.replace(/\\begin\{abstract\}[\s\S]*?\\end\{abstract\}/, "");
|
|
5432
|
+
body = body.replace(/\\section\*?\{([^}]*)\}/g, '<h2 class="section">$1</h2>');
|
|
5433
|
+
body = body.replace(/\\subsection\*?\{([^}]*)\}/g, '<h3 class="subsection">$1</h3>');
|
|
5434
|
+
body = body.replace(/\\subsubsection\*?\{([^}]*)\}/g, '<h4 class="subsubsection">$1</h4>');
|
|
5435
|
+
body = body.replace(/\\paragraph\{([^}]*)\}/g, '<h5 class="paragraph">$1</h5>');
|
|
5436
|
+
body = body.replace(/\\textbf\{([^}]*)\}/g, "<strong>$1</strong>");
|
|
5437
|
+
body = body.replace(/\\textit\{([^}]*)\}/g, "<em>$1</em>");
|
|
5438
|
+
body = body.replace(/\\texttt\{([^}]*)\}/g, "<code>$1</code>");
|
|
5439
|
+
body = body.replace(/\\emph\{([^}]*)\}/g, "<em>$1</em>");
|
|
5440
|
+
body = body.replace(/\\underline\{([^}]*)\}/g, "<u>$1</u>");
|
|
5441
|
+
body = body.replace(/\\cite\{([^}]*)\}/g, '<span class="citation">[$1]</span>');
|
|
5442
|
+
body = body.replace(/\\citep\{([^}]*)\}/g, '<span class="citation">($1)</span>');
|
|
5443
|
+
body = body.replace(/\\citet\{([^}]*)\}/g, '<span class="citation">$1</span>');
|
|
5444
|
+
body = body.replace(/\\ref\{([^}]*)\}/g, '<span class="ref">[ref:$1]</span>');
|
|
5445
|
+
body = body.replace(/\\label\{([^}]*)\}/g, "");
|
|
5446
|
+
body = body.replace(/\\\[([\s\S]*?)\\\]/g, '<div class="math-display">\\[$1\\]</div>');
|
|
5447
|
+
body = body.replace(/\$\$([\s\S]*?)\$\$/g, '<div class="math-display">\\[$1\\]</div>');
|
|
5448
|
+
body = body.replace(
|
|
5449
|
+
/\\begin\{equation\*?\}([\s\S]*?)\\end\{equation\*?\}/g,
|
|
5450
|
+
'<div class="math-display">\\[$1\\]</div>'
|
|
5451
|
+
);
|
|
5452
|
+
body = body.replace(
|
|
5453
|
+
/\\begin\{align\*?\}([\s\S]*?)\\end\{align\*?\}/g,
|
|
5454
|
+
'<div class="math-display">\\[$1\\]</div>'
|
|
5455
|
+
);
|
|
5456
|
+
body = body.replace(/(?<!\$)\$(?!\$)([^$]+?)\$(?!\$)/g, '<span class="math-inline">\\($1\\)</span>');
|
|
5457
|
+
body = body.replace(/\\begin\{itemize\}/g, "<ul>");
|
|
5458
|
+
body = body.replace(/\\end\{itemize\}/g, "</ul>");
|
|
5459
|
+
body = body.replace(/\\begin\{enumerate\}/g, "<ol>");
|
|
5460
|
+
body = body.replace(/\\end\{enumerate\}/g, "</ol>");
|
|
5461
|
+
body = body.replace(/\\item\s*/g, "<li>");
|
|
5462
|
+
body = body.replace(
|
|
5463
|
+
/\\begin\{quote\}([\s\S]*?)\\end\{quote\}/g,
|
|
5464
|
+
"<blockquote>$1</blockquote>"
|
|
5465
|
+
);
|
|
5466
|
+
body = body.replace(
|
|
5467
|
+
/\\begin\{verbatim\}([\s\S]*?)\\end\{verbatim\}/g,
|
|
5468
|
+
"<pre><code>$1</code></pre>"
|
|
5469
|
+
);
|
|
5470
|
+
body = body.replace(
|
|
5471
|
+
/\\begin\{figure\}[\s\S]*?\\caption\{([^}]*)\}[\s\S]*?\\end\{figure\}/g,
|
|
5472
|
+
'<figure class="figure-placeholder"><figcaption>$1</figcaption></figure>'
|
|
5473
|
+
);
|
|
5474
|
+
body = body.replace(
|
|
5475
|
+
/\\begin\{table\}[\s\S]*?\\caption\{([^}]*)\}[\s\S]*?\\end\{table\}/g,
|
|
5476
|
+
'<figure class="table-placeholder"><figcaption>Table: $1</figcaption></figure>'
|
|
5477
|
+
);
|
|
5478
|
+
body = body.replace(/\\footnote\{([^}]*)\}/g, '<sup class="footnote" title="$1">[*]</sup>');
|
|
5479
|
+
body = body.replace(/\\bibliography\{[^}]*\}/g, "");
|
|
5480
|
+
body = body.replace(/\\bibliographystyle\{[^}]*\}/g, "");
|
|
5481
|
+
body = body.replace(/\\usepackage\{[^}]*\}/g, "");
|
|
5482
|
+
body = body.replace(/\\documentclass[^{]*\{[^}]*\}/g, "");
|
|
5483
|
+
body = body.replace(/\\begin\{document\}/g, "");
|
|
5484
|
+
body = body.replace(/\\end\{document\}/g, "");
|
|
5485
|
+
body = body.replace(/\\newcommand[^{]*\{[^}]*\}\{[^}]*\}/g, "");
|
|
5486
|
+
body = body.replace(/\\\\/g, "<br>");
|
|
5487
|
+
body = body.replace(/\\newline/g, "<br>");
|
|
5488
|
+
body = body.replace(/\\noindent\s*/g, "");
|
|
5489
|
+
body = body.replace(/\\vspace\{[^}]*\}/g, "");
|
|
5490
|
+
body = body.replace(/\\hspace\{[^}]*\}/g, "");
|
|
5491
|
+
body = body.replace(/\n\s*\n/g, "</p><p>");
|
|
5492
|
+
body = `<p>${body}</p>`;
|
|
5493
|
+
body = body.replace(/<p>\s*<\/p>/g, "");
|
|
5494
|
+
body = body.replace(/<p>\s*<(h[2-5])/g, "<$1");
|
|
5495
|
+
body = body.replace(/<\/(h[2-5])>\s*<\/p>/g, "</$1>");
|
|
5496
|
+
const titleHtml = titleMatch ? `<h1 class="title">${titleMatch[1]}</h1>` : "";
|
|
5497
|
+
const authorHtml = authorMatch ? `<p class="author">${authorMatch[1]}</p>` : "";
|
|
5498
|
+
const dateHtml = dateMatch ? `<p class="date">${dateMatch[1]}</p>` : "";
|
|
5499
|
+
const abstractHtml = abstractMatch ? `<div class="abstract"><h3>Abstract</h3><p>${abstractMatch[1].trim()}</p></div>` : "";
|
|
5500
|
+
return `${titleHtml}${authorHtml}${dateHtml}${abstractHtml}${body}`;
|
|
5501
|
+
}
|
|
5502
|
+
|
|
5503
|
+
// src/lib/preview/server.ts
|
|
5504
|
+
var HTML_TEMPLATE = `<!DOCTYPE html>
|
|
5505
|
+
<html lang="en">
|
|
5506
|
+
<head>
|
|
5507
|
+
<meta charset="UTF-8">
|
|
5508
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
5509
|
+
<title>Open Research \u2014 LaTeX Preview</title>
|
|
5510
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
|
|
5511
|
+
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>
|
|
5512
|
+
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js"
|
|
5513
|
+
onload="renderMathInElement(document.body, {
|
|
5514
|
+
delimiters: [
|
|
5515
|
+
{left: '\\\\[', right: '\\\\]', display: true},
|
|
5516
|
+
{left: '\\\\(', right: '\\\\)', display: false}
|
|
5517
|
+
],
|
|
5518
|
+
throwOnError: false
|
|
5519
|
+
});">
|
|
5520
|
+
</script>
|
|
5521
|
+
<style>
|
|
5522
|
+
:root {
|
|
5523
|
+
--bg: #1a1a2e;
|
|
5524
|
+
--surface: #16213e;
|
|
5525
|
+
--text: #e0e0e0;
|
|
5526
|
+
--text-dim: #8892b0;
|
|
5527
|
+
--accent: #64ffda;
|
|
5528
|
+
--heading: #ccd6f6;
|
|
5529
|
+
--citation: #64ffda;
|
|
5530
|
+
--border: #233554;
|
|
5531
|
+
}
|
|
5532
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
5533
|
+
body {
|
|
5534
|
+
font-family: 'Charter', 'Georgia', 'Times New Roman', serif;
|
|
5535
|
+
background: var(--bg);
|
|
5536
|
+
color: var(--text);
|
|
5537
|
+
line-height: 1.8;
|
|
5538
|
+
max-width: 780px;
|
|
5539
|
+
margin: 0 auto;
|
|
5540
|
+
padding: 3rem 2rem;
|
|
5541
|
+
}
|
|
5542
|
+
h1.title {
|
|
5543
|
+
font-size: 2rem;
|
|
5544
|
+
color: var(--heading);
|
|
5545
|
+
text-align: center;
|
|
5546
|
+
margin-bottom: 0.5rem;
|
|
5547
|
+
line-height: 1.3;
|
|
5548
|
+
}
|
|
5549
|
+
.author {
|
|
5550
|
+
text-align: center;
|
|
5551
|
+
color: var(--text-dim);
|
|
5552
|
+
font-style: italic;
|
|
5553
|
+
margin-bottom: 0.3rem;
|
|
5554
|
+
}
|
|
5555
|
+
.date {
|
|
5556
|
+
text-align: center;
|
|
5557
|
+
color: var(--text-dim);
|
|
5558
|
+
margin-bottom: 2rem;
|
|
5559
|
+
}
|
|
5560
|
+
.abstract {
|
|
5561
|
+
background: var(--surface);
|
|
5562
|
+
border-left: 3px solid var(--accent);
|
|
5563
|
+
padding: 1.2rem 1.5rem;
|
|
5564
|
+
margin: 2rem 0;
|
|
5565
|
+
border-radius: 0 4px 4px 0;
|
|
5566
|
+
}
|
|
5567
|
+
.abstract h3 {
|
|
5568
|
+
color: var(--accent);
|
|
5569
|
+
font-size: 0.9rem;
|
|
5570
|
+
text-transform: uppercase;
|
|
5571
|
+
letter-spacing: 0.1em;
|
|
5572
|
+
margin-bottom: 0.5rem;
|
|
5573
|
+
}
|
|
5574
|
+
h2.section {
|
|
5575
|
+
font-size: 1.4rem;
|
|
5576
|
+
color: var(--heading);
|
|
5577
|
+
margin: 2.5rem 0 1rem;
|
|
5578
|
+
padding-bottom: 0.3rem;
|
|
5579
|
+
border-bottom: 1px solid var(--border);
|
|
5580
|
+
}
|
|
5581
|
+
h3.subsection {
|
|
5582
|
+
font-size: 1.15rem;
|
|
5583
|
+
color: var(--heading);
|
|
5584
|
+
margin: 1.8rem 0 0.8rem;
|
|
5585
|
+
}
|
|
5586
|
+
h4.subsubsection {
|
|
5587
|
+
font-size: 1rem;
|
|
5588
|
+
color: var(--text-dim);
|
|
5589
|
+
margin: 1.2rem 0 0.5rem;
|
|
5590
|
+
}
|
|
5591
|
+
p { margin: 0.8rem 0; }
|
|
5592
|
+
strong { color: var(--heading); }
|
|
5593
|
+
code {
|
|
5594
|
+
background: var(--surface);
|
|
5595
|
+
padding: 0.15rem 0.4rem;
|
|
5596
|
+
border-radius: 3px;
|
|
5597
|
+
font-size: 0.9em;
|
|
5598
|
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
5599
|
+
}
|
|
5600
|
+
pre {
|
|
5601
|
+
background: var(--surface);
|
|
5602
|
+
padding: 1rem;
|
|
5603
|
+
border-radius: 4px;
|
|
5604
|
+
overflow-x: auto;
|
|
5605
|
+
margin: 1rem 0;
|
|
5606
|
+
}
|
|
5607
|
+
pre code { background: none; padding: 0; }
|
|
5608
|
+
blockquote {
|
|
5609
|
+
border-left: 3px solid var(--border);
|
|
5610
|
+
padding-left: 1rem;
|
|
5611
|
+
color: var(--text-dim);
|
|
5612
|
+
font-style: italic;
|
|
5613
|
+
margin: 1rem 0;
|
|
5614
|
+
}
|
|
5615
|
+
ul, ol { padding-left: 1.5rem; margin: 0.8rem 0; }
|
|
5616
|
+
li { margin: 0.3rem 0; }
|
|
5617
|
+
.citation {
|
|
5618
|
+
color: var(--citation);
|
|
5619
|
+
font-weight: 500;
|
|
5620
|
+
cursor: help;
|
|
5621
|
+
}
|
|
5622
|
+
.ref { color: var(--accent); font-style: italic; }
|
|
5623
|
+
.footnote { color: var(--accent); cursor: help; }
|
|
5624
|
+
.math-display {
|
|
5625
|
+
margin: 1.2rem 0;
|
|
5626
|
+
overflow-x: auto;
|
|
5627
|
+
text-align: center;
|
|
5628
|
+
}
|
|
5629
|
+
.figure-placeholder, .table-placeholder {
|
|
5630
|
+
background: var(--surface);
|
|
5631
|
+
border: 1px dashed var(--border);
|
|
5632
|
+
padding: 2rem;
|
|
5633
|
+
margin: 1.5rem 0;
|
|
5634
|
+
text-align: center;
|
|
5635
|
+
border-radius: 4px;
|
|
5636
|
+
}
|
|
5637
|
+
.figure-placeholder::before { content: '[Figure placeholder]'; display: block; color: var(--text-dim); margin-bottom: 0.5rem; }
|
|
5638
|
+
.table-placeholder::before { content: '[Table placeholder]'; display: block; color: var(--text-dim); margin-bottom: 0.5rem; }
|
|
5639
|
+
figcaption { font-style: italic; color: var(--text-dim); font-size: 0.9rem; }
|
|
5640
|
+
|
|
5641
|
+
/* Live reload indicator */
|
|
5642
|
+
.live-badge {
|
|
5643
|
+
position: fixed;
|
|
5644
|
+
top: 1rem;
|
|
5645
|
+
right: 1rem;
|
|
5646
|
+
background: #0d7337;
|
|
5647
|
+
color: white;
|
|
5648
|
+
padding: 0.3rem 0.8rem;
|
|
5649
|
+
border-radius: 20px;
|
|
5650
|
+
font-size: 0.75rem;
|
|
5651
|
+
font-family: sans-serif;
|
|
5652
|
+
opacity: 0.8;
|
|
5653
|
+
}
|
|
5654
|
+
.live-badge.disconnected { background: #7d3030; }
|
|
5655
|
+
</style>
|
|
5656
|
+
</head>
|
|
5657
|
+
<body>
|
|
5658
|
+
<div class="live-badge" id="status">LIVE</div>
|
|
5659
|
+
<div id="content">
|
|
5660
|
+
{{CONTENT}}
|
|
5661
|
+
</div>
|
|
5662
|
+
<script>
|
|
5663
|
+
// Auto-reload via polling (simple, no WebSocket dependency)
|
|
5664
|
+
let lastHash = "";
|
|
5665
|
+
async function checkForUpdates() {
|
|
5666
|
+
try {
|
|
5667
|
+
const res = await fetch("/__hash");
|
|
5668
|
+
const hash = await res.text();
|
|
5669
|
+
if (lastHash && hash !== lastHash) {
|
|
5670
|
+
location.reload();
|
|
5671
|
+
}
|
|
5672
|
+
lastHash = hash;
|
|
5673
|
+
document.getElementById("status").textContent = "LIVE";
|
|
5674
|
+
document.getElementById("status").className = "live-badge";
|
|
5675
|
+
} catch {
|
|
5676
|
+
document.getElementById("status").textContent = "DISCONNECTED";
|
|
5677
|
+
document.getElementById("status").className = "live-badge disconnected";
|
|
5678
|
+
}
|
|
5679
|
+
}
|
|
5680
|
+
setInterval(checkForUpdates, 1000);
|
|
5681
|
+
checkForUpdates();
|
|
5682
|
+
</script>
|
|
5683
|
+
</body>
|
|
5684
|
+
</html>`;
|
|
5685
|
+
function startPreviewServer(texPath) {
|
|
5686
|
+
const resolved = path17.resolve(texPath);
|
|
5687
|
+
let currentHash = "";
|
|
5688
|
+
function getContentHash() {
|
|
5689
|
+
try {
|
|
5690
|
+
const content = fs18.readFileSync(resolved, "utf8");
|
|
5691
|
+
return `${content.length}-${content.slice(0, 100)}-${content.slice(-100)}`;
|
|
5692
|
+
} catch {
|
|
5693
|
+
return "error";
|
|
5694
|
+
}
|
|
5695
|
+
}
|
|
5696
|
+
function renderPage() {
|
|
5697
|
+
try {
|
|
5698
|
+
const latex = fs18.readFileSync(resolved, "utf8");
|
|
5699
|
+
const htmlContent = latexToHtml(latex);
|
|
5700
|
+
currentHash = getContentHash();
|
|
5701
|
+
return HTML_TEMPLATE.replace("{{CONTENT}}", htmlContent);
|
|
5702
|
+
} catch (err) {
|
|
5703
|
+
return HTML_TEMPLATE.replace(
|
|
5704
|
+
"{{CONTENT}}",
|
|
5705
|
+
`<p style="color: #ff6b6b;">Error reading ${resolved}: ${err instanceof Error ? err.message : String(err)}</p>`
|
|
5706
|
+
);
|
|
5707
|
+
}
|
|
5708
|
+
}
|
|
5709
|
+
return new Promise((resolve) => {
|
|
5710
|
+
const server = http2.createServer((req, res) => {
|
|
5711
|
+
if (req.url === "/__hash") {
|
|
5712
|
+
const hash = getContentHash();
|
|
5713
|
+
res.writeHead(200, { "Content-Type": "text/plain", "Cache-Control": "no-cache" });
|
|
5714
|
+
res.end(hash);
|
|
5715
|
+
return;
|
|
5716
|
+
}
|
|
5717
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" });
|
|
5718
|
+
res.end(renderPage());
|
|
5719
|
+
});
|
|
5720
|
+
server.listen(0, "127.0.0.1", () => {
|
|
5721
|
+
const addr = server.address();
|
|
5722
|
+
if (!addr || typeof addr === "string") return;
|
|
5723
|
+
const port = addr.port;
|
|
5724
|
+
const url = `http://127.0.0.1:${port}`;
|
|
5725
|
+
resolve({
|
|
5726
|
+
url,
|
|
5727
|
+
port,
|
|
5728
|
+
close: () => server.close()
|
|
5729
|
+
});
|
|
5730
|
+
});
|
|
5731
|
+
});
|
|
5732
|
+
}
|
|
5733
|
+
|
|
5297
5734
|
// src/tui/commands.ts
|
|
5298
5735
|
var SLASH_COMMANDS = [
|
|
5299
5736
|
{ name: "auth", aliases: ["/connect", "/login"], description: "Connect your OpenAI account via browser OAuth", category: "auth" },
|
|
@@ -5306,6 +5743,14 @@ var SLASH_COMMANDS = [
|
|
|
5306
5743
|
{ name: "clear", aliases: ["/new"], description: "Clear conversation and start fresh", category: "session" },
|
|
5307
5744
|
{ name: "help", aliases: ["/commands"], description: "Show available commands", category: "system" },
|
|
5308
5745
|
{ name: "config", aliases: ["/settings"], description: "View or change settings (e.g. /config theme dark)", category: "system" },
|
|
5746
|
+
{ name: "compact", aliases: [], description: "Manually compress conversation to save context (e.g. /compact keep the statistics)", category: "session" },
|
|
5747
|
+
{ name: "cost", aliases: ["/tokens", "/usage"], description: "Show token usage and cost for the current session", category: "system" },
|
|
5748
|
+
{ name: "context", aliases: [], description: "Show context window usage \u2014 how full it is", category: "system" },
|
|
5749
|
+
{ name: "btw", aliases: ["/aside"], description: "Ask a side question without affecting the main conversation", category: "session" },
|
|
5750
|
+
{ name: "export", aliases: [], description: "Export conversation as markdown to a file", category: "session" },
|
|
5751
|
+
{ name: "diff", aliases: ["/changes"], description: "Show files the agent has changed in this session", category: "workspace" },
|
|
5752
|
+
{ name: "doctor", aliases: [], description: "Diagnose auth, connectivity, and tool availability", category: "system" },
|
|
5753
|
+
{ name: "preview", aliases: [], description: "Live preview a LaTeX file in browser (e.g. /preview papers/draft.tex)", category: "workspace" },
|
|
5309
5754
|
{ name: "memory", aliases: ["/memories"], description: "View or clear stored memories about you", category: "system" },
|
|
5310
5755
|
{ name: "exit", aliases: ["/quit", "/q"], description: "Exit Open Research", category: "system" }
|
|
5311
5756
|
];
|
|
@@ -5843,6 +6288,7 @@ function App({
|
|
|
5843
6288
|
const deferredPendingUpdates = useDeferredValue(pendingUpdates);
|
|
5844
6289
|
const activityFrame = useAnimatedFrame(busy);
|
|
5845
6290
|
const [agentQuestion, setAgentQuestion] = useState3(null);
|
|
6291
|
+
const previewRef = useRef(null);
|
|
5846
6292
|
const isHome = deferredMessages.length === 0 && !busy;
|
|
5847
6293
|
const hasWorkspace = workspacePath !== null;
|
|
5848
6294
|
const hasAuth = authStatus === "connected";
|
|
@@ -6132,6 +6578,201 @@ function App({
|
|
|
6132
6578
|
addSystemMessage(" Esc unfocus prompt");
|
|
6133
6579
|
break;
|
|
6134
6580
|
}
|
|
6581
|
+
case "compact": {
|
|
6582
|
+
if (history.length === 0) {
|
|
6583
|
+
addSystemMessage("Nothing to compact \u2014 conversation is empty.");
|
|
6584
|
+
break;
|
|
6585
|
+
}
|
|
6586
|
+
const customInstructions = args || void 0;
|
|
6587
|
+
addSystemMessage(customInstructions ? `Compacting conversation (preserving: ${customInstructions})...` : "Compacting conversation...");
|
|
6588
|
+
setBusy(true);
|
|
6589
|
+
try {
|
|
6590
|
+
const provider = await createProviderFromStoredAuth({ homeDir });
|
|
6591
|
+
const msgs = [{ role: "system", content: "compaction" }, ...history.map((m) => m)];
|
|
6592
|
+
const { messages: compacted, didCompact } = await manualCompact(
|
|
6593
|
+
msgs,
|
|
6594
|
+
config?.defaults.model ?? "gpt-5.4",
|
|
6595
|
+
provider,
|
|
6596
|
+
sessionTokens,
|
|
6597
|
+
customInstructions
|
|
6598
|
+
);
|
|
6599
|
+
if (didCompact) {
|
|
6600
|
+
const newHistory = compacted.filter((m) => m.role !== "system").map((m) => ({
|
|
6601
|
+
role: m.role,
|
|
6602
|
+
content: m.content
|
|
6603
|
+
}));
|
|
6604
|
+
setHistory(newHistory);
|
|
6605
|
+
const k = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : String(n);
|
|
6606
|
+
setTokenDisplay(`${k(sessionTokens.estimatedCurrentTokens)} ctx \xB7 ${k(sessionTokens.totalTokens)} total`);
|
|
6607
|
+
addSystemMessage(`Compacted. Context reduced to ~${Math.round(sessionTokens.estimatedCurrentTokens / 1e3)}k tokens.`);
|
|
6608
|
+
} else {
|
|
6609
|
+
addSystemMessage("Nothing to compact \u2014 conversation too short.");
|
|
6610
|
+
}
|
|
6611
|
+
} catch (err) {
|
|
6612
|
+
addSystemMessage(`Compaction failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
6613
|
+
} finally {
|
|
6614
|
+
setBusy(false);
|
|
6615
|
+
}
|
|
6616
|
+
break;
|
|
6617
|
+
}
|
|
6618
|
+
case "cost": {
|
|
6619
|
+
const k = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : String(n);
|
|
6620
|
+
const c = sessionTokens.cumulative;
|
|
6621
|
+
addSystemMessage("Session token usage:");
|
|
6622
|
+
addSystemMessage(` Input: ${k(c.input)} tokens`);
|
|
6623
|
+
addSystemMessage(` Output: ${k(c.output)} tokens`);
|
|
6624
|
+
if (c.reasoning > 0) addSystemMessage(` Reasoning: ${k(c.reasoning)} tokens`);
|
|
6625
|
+
if (c.cache.read > 0) addSystemMessage(` Cache read: ${k(c.cache.read)} tokens`);
|
|
6626
|
+
if (c.cache.write > 0) addSystemMessage(` Cache write: ${k(c.cache.write)} tokens`);
|
|
6627
|
+
addSystemMessage(` Total: ${k(c.total)} tokens`);
|
|
6628
|
+
addSystemMessage(` Context: ~${k(sessionTokens.estimatedCurrentTokens)} (current window)`);
|
|
6629
|
+
addSystemMessage(` Compactions: ${sessionTokens.compactionCount}`);
|
|
6630
|
+
break;
|
|
6631
|
+
}
|
|
6632
|
+
case "context": {
|
|
6633
|
+
const model = config?.defaults.model ?? "gpt-5.4";
|
|
6634
|
+
const window = getContextWindow(model);
|
|
6635
|
+
const threshold = getCompactThreshold(model);
|
|
6636
|
+
const current = sessionTokens.estimatedCurrentTokens || estimateConversationTokens(
|
|
6637
|
+
history.map((m) => m)
|
|
6638
|
+
);
|
|
6639
|
+
const pct = Math.round(current / window * 100);
|
|
6640
|
+
const barWidth = 40;
|
|
6641
|
+
const filled = Math.round(pct / 100 * barWidth);
|
|
6642
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(barWidth - filled);
|
|
6643
|
+
const color = pct > 90 ? "red" : pct > 70 ? "yellow" : "green";
|
|
6644
|
+
addSystemMessage(`Context window: ${model} (${(window / 1e3).toFixed(0)}k)`);
|
|
6645
|
+
addSystemMessage(` [${bar}] ${pct}%`);
|
|
6646
|
+
addSystemMessage(` ${(current / 1e3).toFixed(1)}k / ${(window / 1e3).toFixed(0)}k tokens used`);
|
|
6647
|
+
addSystemMessage(` Auto-compact at ${(threshold / 1e3).toFixed(0)}k (90%)`);
|
|
6648
|
+
if (pct > 80) {
|
|
6649
|
+
addSystemMessage(" Tip: run /compact to free space, or /clear to start fresh.");
|
|
6650
|
+
}
|
|
6651
|
+
break;
|
|
6652
|
+
}
|
|
6653
|
+
case "btw": {
|
|
6654
|
+
if (!args) {
|
|
6655
|
+
addSystemMessage("Usage: /btw <your side question>");
|
|
6656
|
+
break;
|
|
6657
|
+
}
|
|
6658
|
+
if (!hasAuth) {
|
|
6659
|
+
addSystemMessage("Not connected. Run /auth first.");
|
|
6660
|
+
break;
|
|
6661
|
+
}
|
|
6662
|
+
addSystemMessage(`Side question: ${args}`);
|
|
6663
|
+
setBusy(true);
|
|
6664
|
+
try {
|
|
6665
|
+
const provider = await createProviderFromStoredAuth({ homeDir });
|
|
6666
|
+
const response = await provider.callLLM({
|
|
6667
|
+
messages: [
|
|
6668
|
+
{ role: "system", content: "Answer this quick side question concisely. Do not reference any prior conversation." },
|
|
6669
|
+
{ role: "user", content: args }
|
|
6670
|
+
],
|
|
6671
|
+
model: config?.defaults.model ?? "gpt-5.4",
|
|
6672
|
+
maxTokens: 1e3
|
|
6673
|
+
});
|
|
6674
|
+
addSystemMessage(`Answer: ${response.content}`);
|
|
6675
|
+
} catch (err) {
|
|
6676
|
+
addSystemMessage(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
6677
|
+
} finally {
|
|
6678
|
+
setBusy(false);
|
|
6679
|
+
}
|
|
6680
|
+
break;
|
|
6681
|
+
}
|
|
6682
|
+
case "export": {
|
|
6683
|
+
const fileName = args?.trim() || "conversation-export.md";
|
|
6684
|
+
const exportPath = __require("path").resolve(workspacePath ?? process.cwd(), fileName);
|
|
6685
|
+
const lines = [`# Open Research \u2014 Conversation Export
|
|
6686
|
+
`];
|
|
6687
|
+
for (const msg of messages) {
|
|
6688
|
+
if (msg.role === "user") lines.push(`## You
|
|
6689
|
+
${msg.text}
|
|
6690
|
+
`);
|
|
6691
|
+
else if (msg.role === "assistant") lines.push(`## Agent
|
|
6692
|
+
${msg.text}
|
|
6693
|
+
`);
|
|
6694
|
+
else lines.push(`> ${msg.text}
|
|
6695
|
+
`);
|
|
6696
|
+
}
|
|
6697
|
+
try {
|
|
6698
|
+
const fsModule = __require("fs/promises");
|
|
6699
|
+
await fsModule.writeFile(exportPath, lines.join("\n"), "utf8");
|
|
6700
|
+
addSystemMessage(`Exported ${messages.length} messages to ${exportPath}`);
|
|
6701
|
+
} catch (err) {
|
|
6702
|
+
addSystemMessage(`Export failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
6703
|
+
}
|
|
6704
|
+
break;
|
|
6705
|
+
}
|
|
6706
|
+
case "diff": {
|
|
6707
|
+
if (!workspacePath) {
|
|
6708
|
+
addSystemMessage("No workspace active.");
|
|
6709
|
+
break;
|
|
6710
|
+
}
|
|
6711
|
+
try {
|
|
6712
|
+
const { execSync } = __require("child_process");
|
|
6713
|
+
const gitStatus = execSync("git status --short 2>/dev/null || echo 'Not a git repo'", {
|
|
6714
|
+
cwd: workspacePath,
|
|
6715
|
+
encoding: "utf8"
|
|
6716
|
+
}).trim();
|
|
6717
|
+
if (!gitStatus || gitStatus === "Not a git repo") {
|
|
6718
|
+
addSystemMessage("No changes detected (not a git repo or no modifications).");
|
|
6719
|
+
} else {
|
|
6720
|
+
addSystemMessage("Changed files:");
|
|
6721
|
+
for (const line of gitStatus.split("\n")) {
|
|
6722
|
+
addSystemMessage(` ${line}`);
|
|
6723
|
+
}
|
|
6724
|
+
}
|
|
6725
|
+
} catch {
|
|
6726
|
+
addSystemMessage("Could not check changes.");
|
|
6727
|
+
}
|
|
6728
|
+
break;
|
|
6729
|
+
}
|
|
6730
|
+
case "doctor": {
|
|
6731
|
+
addSystemMessage("Running diagnostics...");
|
|
6732
|
+
const authResult = await getAuthStatus({ homeDir });
|
|
6733
|
+
addSystemMessage(` Auth: ${authResult.connected ? "connected" : "not connected"} \u2014 ${authResult.message}`);
|
|
6734
|
+
addSystemMessage(` Workspace: ${workspacePath ? workspacePath : "none"}`);
|
|
6735
|
+
addSystemMessage(` Files: ${workspaceFiles.length}`);
|
|
6736
|
+
addSystemMessage(` Skills: ${skills2.length} loaded`);
|
|
6737
|
+
const mems = await loadMemories({ homeDir });
|
|
6738
|
+
addSystemMessage(` Memories: ${mems.length} stored`);
|
|
6739
|
+
addSystemMessage(` Node: ${process.version}`);
|
|
6740
|
+
const toolChecks = ["python3 --version", "pdflatex --version", "git --version"];
|
|
6741
|
+
for (const cmd2 of toolChecks) {
|
|
6742
|
+
try {
|
|
6743
|
+
const { execSync } = __require("child_process");
|
|
6744
|
+
const out = execSync(cmd2 + " 2>&1", { encoding: "utf8", timeout: 3e3 }).trim().split("\n")[0];
|
|
6745
|
+
addSystemMessage(` ${cmd2.split(" ")[0]}: ${out}`);
|
|
6746
|
+
} catch {
|
|
6747
|
+
addSystemMessage(` ${cmd2.split(" ")[0]}: not found`);
|
|
6748
|
+
}
|
|
6749
|
+
}
|
|
6750
|
+
addSystemMessage("Diagnostics complete.");
|
|
6751
|
+
break;
|
|
6752
|
+
}
|
|
6753
|
+
case "preview": {
|
|
6754
|
+
if (!args) {
|
|
6755
|
+
addSystemMessage("Usage: /preview <path-to-tex-file>");
|
|
6756
|
+
addSystemMessage("Example: /preview papers/draft.tex");
|
|
6757
|
+
break;
|
|
6758
|
+
}
|
|
6759
|
+
const texPath = args.trim();
|
|
6760
|
+
const resolvedTex = __require("path").isAbsolute(texPath) ? texPath : __require("path").resolve(workspacePath ?? process.cwd(), texPath);
|
|
6761
|
+
try {
|
|
6762
|
+
if (previewRef.current) {
|
|
6763
|
+
previewRef.current.close();
|
|
6764
|
+
}
|
|
6765
|
+
const preview = await startPreviewServer(resolvedTex);
|
|
6766
|
+
previewRef.current = preview;
|
|
6767
|
+
addSystemMessage(`Live preview started at ${preview.url}`);
|
|
6768
|
+
addSystemMessage("Auto-reloads when the file changes. Close with /preview stop");
|
|
6769
|
+
const openModule = await import("open");
|
|
6770
|
+
await openModule.default(preview.url);
|
|
6771
|
+
} catch (err) {
|
|
6772
|
+
addSystemMessage(`Preview failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
6773
|
+
}
|
|
6774
|
+
break;
|
|
6775
|
+
}
|
|
6135
6776
|
case "memory": {
|
|
6136
6777
|
if (args === "clear") {
|
|
6137
6778
|
await clearMemories({ homeDir });
|
|
@@ -6807,7 +7448,7 @@ function App({
|
|
|
6807
7448
|
statusParts,
|
|
6808
7449
|
statusColor,
|
|
6809
7450
|
tokenDisplay,
|
|
6810
|
-
workspaceName: hasWorkspace ?
|
|
7451
|
+
workspaceName: hasWorkspace ? path18.basename(workspacePath) : process.cwd(),
|
|
6811
7452
|
mode: agentMode,
|
|
6812
7453
|
planningStatus: planningState.status
|
|
6813
7454
|
}
|
|
@@ -6819,7 +7460,7 @@ function App({
|
|
|
6819
7460
|
var program = new Command();
|
|
6820
7461
|
program.name("open-research").description("Local-first research CLI powered by ChatGPT/Codex auth.").argument("[workspacePath]", "Optional workspace path to open").action(async (workspacePath) => {
|
|
6821
7462
|
await ensureOpenResearchConfig();
|
|
6822
|
-
const target = workspacePath ?
|
|
7463
|
+
const target = workspacePath ? path19.resolve(workspacePath) : process.cwd();
|
|
6823
7464
|
const project = await loadWorkspaceProject(target);
|
|
6824
7465
|
const auth2 = await loadStoredAuth();
|
|
6825
7466
|
render(
|
|
@@ -6841,7 +7482,7 @@ program.name("open-research").description("Local-first research CLI powered by C
|
|
|
6841
7482
|
});
|
|
6842
7483
|
program.command("init").argument("[workspacePath]").description("Initialize an Open Research workspace.").action(async (workspacePath) => {
|
|
6843
7484
|
await ensureOpenResearchConfig();
|
|
6844
|
-
const target =
|
|
7485
|
+
const target = path19.resolve(workspacePath ?? process.cwd());
|
|
6845
7486
|
const project = await initWorkspace({ workspaceDir: target });
|
|
6846
7487
|
console.log(`Initialized workspace: ${target}`);
|
|
6847
7488
|
console.log(`Title: ${project.title}`);
|
|
@@ -6910,8 +7551,8 @@ skills.command("create").argument("[name]").description("Scaffold a new user ski
|
|
|
6910
7551
|
});
|
|
6911
7552
|
skills.command("edit").argument("<name>").description("Open a user skill in $EDITOR.").action(async (name) => {
|
|
6912
7553
|
await ensureOpenResearchConfig();
|
|
6913
|
-
const skillDir =
|
|
6914
|
-
openInEditor(
|
|
7554
|
+
const skillDir = path19.join(getOpenResearchSkillsDir(), name);
|
|
7555
|
+
openInEditor(path19.join(skillDir, "SKILL.md"));
|
|
6915
7556
|
const validation = await validateSkillDirectory({ skillDir });
|
|
6916
7557
|
if (!validation.ok) {
|
|
6917
7558
|
console.error(validation.errors.join("\n"));
|
|
@@ -6922,9 +7563,9 @@ skills.command("edit").argument("<name>").description("Open a user skill in $EDI
|
|
|
6922
7563
|
});
|
|
6923
7564
|
skills.command("validate").argument("[nameOrPath]").description("Validate one user skill.").action(async (nameOrPath) => {
|
|
6924
7565
|
await ensureOpenResearchConfig();
|
|
6925
|
-
const skillDir = nameOrPath ?
|
|
7566
|
+
const skillDir = nameOrPath ? path19.isAbsolute(nameOrPath) ? nameOrPath : path19.join(getOpenResearchSkillsDir(), nameOrPath) : getOpenResearchSkillsDir();
|
|
6926
7567
|
const stat = await import("fs/promises").then(
|
|
6927
|
-
(
|
|
7568
|
+
(fs19) => fs19.stat(skillDir).catch(() => null)
|
|
6928
7569
|
);
|
|
6929
7570
|
if (!stat) {
|
|
6930
7571
|
throw new Error(`Skill path not found: ${skillDir}`);
|