papergod 0.1.0
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/LICENSE +21 -0
- package/README.md +244 -0
- package/ROADMAP.md +171 -0
- package/example/main.tex +360 -0
- package/frontend/src/components/ui/badge.jsx +5 -0
- package/frontend/src/components/ui/button.jsx +24 -0
- package/frontend/src/components/workbench.jsx +182 -0
- package/frontend/src/lib/utils.js +6 -0
- package/frontend/src/main.jsx +19 -0
- package/frontend/src/theme.css +256 -0
- package/frontend/vite.config.js +23 -0
- package/package.json +73 -0
- package/papergod-demo.png +0 -0
- package/public/app.js +5480 -0
- package/public/brand/papergod-logo.png +0 -0
- package/public/i18n.js +95 -0
- package/public/index.html +480 -0
- package/public/pdf-sentence-mapping.js +142 -0
- package/public/react/app.js +209 -0
- package/public/react/assets/addon-fit-YJmn1quW.js +12 -0
- package/public/react/assets/addon-web-links-BWjmmSgS.js +12 -0
- package/public/react/assets/main.css +32 -0
- package/public/react/assets/xterm-BqvuqXEL.js +27 -0
- package/public/style.css +1462 -0
- package/src/cli.js +128 -0
- package/src/server/agent-adapters.js +1240 -0
- package/src/server/agent-errors.js +105 -0
- package/src/server/agent-runtime.js +81 -0
- package/src/server/agent.js +173 -0
- package/src/server/app-version.js +86 -0
- package/src/server/change-history.js +114 -0
- package/src/server/document-structure.js +174 -0
- package/src/server/index.js +1442 -0
- package/src/server/latex-structure.js +344 -0
- package/src/server/latex.js +67 -0
- package/src/server/library-engine.js +193 -0
- package/src/server/library-files.js +134 -0
- package/src/server/literature-review.js +122 -0
- package/src/server/orchestration-engine.js +662 -0
- package/src/server/paragraph-analysis.js +300 -0
- package/src/server/project-resources.js +290 -0
- package/src/server/project-store.js +808 -0
- package/src/server/prompt-manifest.js +300 -0
- package/src/server/references.js +425 -0
- package/src/server/review-panel.js +263 -0
- package/src/server/revise-workflow.js +278 -0
- package/src/server/revision-engine.js +607 -0
- package/src/server/security.js +16 -0
- package/src/server/text-extraction.js +149 -0
- package/src/server/workspace-browser.js +49 -0
- package/src/server/workspace-registry.js +143 -0
- package/src/server/workspace-terminal.js +99 -0
- package/src/server/workspace.js +223 -0
- package/src/server/zotero.js +98 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { mkdir, readdir, writeFile } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { loadProject, saveProject, updateProject } from './project-store.js';
|
|
4
|
+
import { syncDocumentStructure } from './document-structure.js';
|
|
5
|
+
|
|
6
|
+
const TEMPLATE_DOCUMENT = `\\documentclass{article}
|
|
7
|
+
\\usepackage[utf8]{inputenc}
|
|
8
|
+
\\usepackage{amsmath}
|
|
9
|
+
\\title{Untitled Paper}
|
|
10
|
+
\\author{}
|
|
11
|
+
\\date{\\today}
|
|
12
|
+
|
|
13
|
+
\\begin{document}
|
|
14
|
+
\\maketitle
|
|
15
|
+
|
|
16
|
+
\\begin{abstract}
|
|
17
|
+
Write the abstract here.
|
|
18
|
+
\\end{abstract}
|
|
19
|
+
|
|
20
|
+
\\section{Introduction}
|
|
21
|
+
Describe the research problem, gap, and contribution.
|
|
22
|
+
|
|
23
|
+
\\section{Methods}
|
|
24
|
+
Describe the proposed method.
|
|
25
|
+
|
|
26
|
+
\\section{Results}
|
|
27
|
+
Report the main findings.
|
|
28
|
+
|
|
29
|
+
\\section{Conclusion}
|
|
30
|
+
Summarize the contribution and limitations.
|
|
31
|
+
|
|
32
|
+
\\end{document}
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
const DEMO_DOCUMENT = `\\documentclass{article}
|
|
36
|
+
\\usepackage[utf8]{inputenc}
|
|
37
|
+
\\usepackage{amsmath}
|
|
38
|
+
\\title{A Reproducible Study of Efficient Scientific Writing Agents}
|
|
39
|
+
\\author{Papergod Demo}
|
|
40
|
+
\\date{\\today}
|
|
41
|
+
|
|
42
|
+
\\begin{document}
|
|
43
|
+
\\maketitle
|
|
44
|
+
|
|
45
|
+
\\begin{abstract}
|
|
46
|
+
Scientific writing agents can reduce revision time, but their reliability is still unclear. We evaluate a structured agent workflow on 120 annotated manuscript paragraphs and report both editing quality and reproducibility. The workflow improves reviewer acceptance from 71.4\\% to 84.8\\%, while preserving traceable evidence for every accepted change.
|
|
47
|
+
\\end{abstract}
|
|
48
|
+
|
|
49
|
+
\\section{Introduction}
|
|
50
|
+
AI-assisted writing is very important for modern research teams. Existing tools often generate fluent prose without preserving the author's intent, evidence, or revision history. This study asks whether a structured workflow with paragraph prompts, reusable language resources, and explicit acceptance decisions can improve academic revisions safely.
|
|
51
|
+
|
|
52
|
+
\\section{Methods}
|
|
53
|
+
We sampled 120 paragraphs from 24 computer-science manuscripts. Three domain experts independently rated clarity, evidence alignment, and terminology consistency on a five-point scale. The proposed workflow combines document-level goals, section prompts, sentence patterns, and scoped vocabulary before producing reviewable edits.
|
|
54
|
+
|
|
55
|
+
\\section{Results}
|
|
56
|
+
The structured workflow increased mean clarity from $3.42 \\pm 0.61$ to $4.18 \\pm 0.47$. Reviewer acceptance increased from 71.4\\% to 84.8\\%, and terminology violations decreased by 38\\%. It was found that explicit paragraph prompts produced the largest improvement.
|
|
57
|
+
|
|
58
|
+
\\section{Conclusion}
|
|
59
|
+
In conclusion, structured writing agents are very useful and can make revision more auditable. The evaluation is limited to computer-science manuscripts, so future work should test additional disciplines and longer collaborative studies.
|
|
60
|
+
|
|
61
|
+
\\end{document}
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
const DEMO_SECTION_PROMPTS = {
|
|
65
|
+
Abstract: 'State the problem, evaluation design, quantitative result, and main contribution in a compact standalone summary.',
|
|
66
|
+
Introduction: 'Establish the research gap, explain why unstructured generation is insufficient, and end with the study question.',
|
|
67
|
+
Methods: 'Describe the sample, human evaluation, workflow components, and reproducibility controls precisely.',
|
|
68
|
+
Results: 'Report quantitative findings with units and uncertainty; separate observations from interpretation.',
|
|
69
|
+
Conclusion: 'Answer the research question, state the practical contribution, acknowledge limitations, and identify future work.',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function demoResourceBase(id, timestamp) {
|
|
73
|
+
return { id, createdAt: timestamp, updatedAt: timestamp };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const STARTER_CORPORA = [
|
|
77
|
+
['starter_corpus_claim_evidence', 'Claim–evidence checklist', 'Check that every substantive claim is supported at the right strength.', 'Distinguish observations, interpretations, and causal claims. Attach quantitative claims to reported measurements; qualify generalization beyond the evaluated sample; never invent evidence or citations.', ['evidence', 'claims', 'review']],
|
|
78
|
+
['starter_corpus_reproducibility', 'Reproducibility checklist', 'A compact checklist for empirical and computational papers.', 'Report data selection, preprocessing, sample size, baselines, hyperparameters, evaluation metrics, uncertainty, implementation assumptions, and threats to external validity.', ['methods', 'reproducibility', 'evaluation']],
|
|
79
|
+
['starter_corpus_structure', 'Section-purpose checklist', 'Keep each paper section focused on its rhetorical job.', 'Introduction: problem, prior capability, gap, and contribution. Methods: auditable procedure. Results: measured findings without interpretation inflation. Discussion: explanation, implications, and limitations. Conclusion: answer the research question without introducing new evidence.', ['structure', 'academic-writing']],
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const STARTER_PATTERNS = [
|
|
83
|
+
['starter_pattern_gap', 'Research gap', 'Although {{prior_work}}, existing approaches do not {{unresolved_gap}}.', 'Position an unresolved limitation without dismissing prior work.', ['introduction', 'gap'], ['Introduction'], [['prior_work', 'Capability already established'], ['unresolved_gap', 'Specific unresolved limitation']]],
|
|
84
|
+
['starter_pattern_contribution', 'Contribution statement', 'This work contributes {{contribution}} by {{mechanism}}, enabling {{outcome}}.', 'State what is new, how it is achieved, and why it matters.', ['introduction', 'contribution'], ['Introduction'], [['contribution', 'Concrete contribution'], ['mechanism', 'Method or design that realizes it'], ['outcome', 'Supported benefit or capability']]],
|
|
85
|
+
['starter_pattern_protocol', 'Evaluation protocol', 'We evaluate {{system}} on {{sample}} using {{metric}}, with {{control}}.', 'Summarize an auditable evaluation design.', ['methods', 'evaluation'], ['Methods'], [['system', 'Evaluated system'], ['sample', 'Evaluation sample'], ['metric', 'Outcome measure'], ['control', 'Baseline, control, or reliability procedure']]],
|
|
86
|
+
['starter_pattern_result', 'Quantitative comparison', '{{method}} changed {{metric}} from {{baseline}} to {{result}} ({{uncertainty_or_effect}}).', 'Report a comparison with uncertainty or effect size.', ['results', 'quantitative'], ['Results'], [['method', 'Compared method'], ['metric', 'Measured outcome'], ['baseline', 'Baseline value'], ['result', 'Observed value'], ['uncertainty_or_effect', 'Uncertainty interval or effect size']]],
|
|
87
|
+
['starter_pattern_negative_result', 'Null or negative result', 'We found no evidence that {{factor}} improved {{outcome}} under {{conditions}} ({{estimate}}).', 'Report a null result without claiming proof of no effect.', ['results', 'negative-result'], ['Results'], [['factor', 'Tested factor'], ['outcome', 'Measured outcome'], ['conditions', 'Evaluation conditions'], ['estimate', 'Estimate and uncertainty']]],
|
|
88
|
+
['starter_pattern_limitation', 'Limitation and scope', 'This study is limited by {{limitation}}, which may affect {{scope}}; future work should {{next_step}}.', 'Connect a concrete limitation to its consequence and next step.', ['discussion', 'limitations'], ['Discussion', 'Conclusion'], [['limitation', 'Specific limitation'], ['scope', 'Affected inference or generalization'], ['next_step', 'Actionable follow-up']]],
|
|
89
|
+
['starter_pattern_implication', 'Evidence-bounded implication', 'These findings suggest that {{implication}} when {{condition}}, but they do not establish {{excluded_claim}}.', 'State an implication while preserving the evidence boundary.', ['discussion', 'evidence'], ['Discussion'], [['implication', 'Supported implication'], ['condition', 'Conditions under which it applies'], ['excluded_claim', 'Stronger unsupported conclusion']]],
|
|
90
|
+
['starter_pattern_contrast', 'Contrast transition', 'Whereas {{first_case}}, {{second_case}}; this difference reflects {{explanation}}.', 'Connect two findings with an explicit basis for contrast.', ['transition', 'cohesion'], ['Results', 'Discussion'], [['first_case', 'First result or condition'], ['second_case', 'Contrasting result or condition'], ['explanation', 'Evidence-supported explanation']]],
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const STARTER_VOCABULARY = [
|
|
94
|
+
['starter_vocab_show', 'show', 'demonstrate', 'Use “demonstrate” for direct evidence; use “indicate” or “suggest” when evidence is weaker.', ['indicate', 'suggest', 'reveal'], 'The ablation results demonstrate the contribution of the retrieval module.'],
|
|
95
|
+
['starter_vocab_prove', 'prove', 'support', 'Empirical results usually support a claim rather than prove it universally.', ['provide evidence for', 'corroborate'], 'The results support the hypothesis under the evaluated conditions.'],
|
|
96
|
+
['starter_vocab_very', 'very important', 'important', 'Replace vague intensifiers with the specific consequence whenever possible.', ['substantive', 'consequential'], 'This distinction is important because it changes the evaluation protocol.'],
|
|
97
|
+
['starter_vocab_lot', 'a lot of', 'many', 'Prefer a count or proportion when available; otherwise use a precise quantifier.', ['numerous', 'a substantial proportion of'], 'Many sampled papers omitted uncertainty estimates.'],
|
|
98
|
+
['starter_vocab_get', 'get', 'obtain', 'Prefer a verb that names the operation or outcome.', ['derive', 'retrieve', 'achieve'], 'We obtain the final representation by mean pooling.'],
|
|
99
|
+
['starter_vocab_better', 'better', 'higher', 'Name the exact dimension of improvement and report its measure.', ['lower', 'more accurate', 'more efficient'], 'The revised method achieved a higher reviewer acceptance rate.'],
|
|
100
|
+
['starter_vocab_bad', 'bad', 'limited', 'Describe the observed deficiency rather than applying a broad judgment.', ['inaccurate', 'unstable', 'insufficient'], 'Performance was limited on out-of-domain samples.'],
|
|
101
|
+
['starter_vocab_obviously', 'obviously', 'notably', 'Avoid implying that a claim is self-evident; state the evidence instead.', ['as expected', 'consistent with'], 'Notably, the effect persisted across all three datasets.'],
|
|
102
|
+
['starter_vocab_causes', 'causes', 'is associated with', 'Use causal language only when the study design identifies a causal effect.', ['correlates with', 'coincides with'], 'Longer prompts were associated with higher completion latency.'],
|
|
103
|
+
['starter_vocab_significant', 'significant', 'statistically significant', 'Specify whether significance is statistical or practical and report the criterion.', ['substantial', 'meaningful'], 'The difference was statistically significant at the prespecified threshold.'],
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
export async function seedStarterLibraries(workspaceRoot) {
|
|
107
|
+
const timestamp = new Date().toISOString();
|
|
108
|
+
const { project } = await updateProject(workspaceRoot, (data) => {
|
|
109
|
+
const addMissing = (target, records) => {
|
|
110
|
+
const ids = new Set(target.map((item) => item.id));
|
|
111
|
+
for (const record of records) if (!ids.has(record.id)) target.push(record);
|
|
112
|
+
};
|
|
113
|
+
addMissing(data.libraries.corpora, STARTER_CORPORA.map(([id, name, description, content, tags]) => ({
|
|
114
|
+
...demoResourceBase(id, timestamp), name, description, content, tags, source: 'Papergod starter library',
|
|
115
|
+
})));
|
|
116
|
+
addMissing(data.libraries.sentencePatterns, STARTER_PATTERNS.map(([id, name, template, description, tags, sectionTypes, slots]) => ({
|
|
117
|
+
...demoResourceBase(id, timestamp), name, template, description, tags, sectionTypes, source: 'Papergod starter library',
|
|
118
|
+
slots: slots.map(([slotName, slotDescription]) => ({ name: slotName, description: slotDescription, required: true })),
|
|
119
|
+
})));
|
|
120
|
+
addMissing(data.libraries.vocabulary.global, STARTER_VOCABULARY.map(([id, term, preferred, definition, alternatives, example]) => ({
|
|
121
|
+
...demoResourceBase(id, timestamp), term, preferred, definition, alternatives, examples: [example], tags: ['academic', 'precision'], source: 'Papergod starter library',
|
|
122
|
+
})));
|
|
123
|
+
});
|
|
124
|
+
return project;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function seedDemoWorkspace(workspaceRoot, file = 'main.tex') {
|
|
128
|
+
const document = await syncDocumentStructure(workspaceRoot, file);
|
|
129
|
+
const timestamp = new Date().toISOString();
|
|
130
|
+
const { project } = await updateProject(workspaceRoot, (data) => {
|
|
131
|
+
data.project.corePrompt ||= 'Revise this empirical computer-science paper for precision, evidence alignment, reproducibility, and consistent terminology. Never invent results or citations; preserve LaTeX and make every change reviewable.';
|
|
132
|
+
const targetDocument = data.documents.find((item) => item.id === document.id);
|
|
133
|
+
targetDocument.summary ||= 'An empirical study of whether structured prompts, writing libraries, and explicit revision decisions improve AI-assisted scientific writing.';
|
|
134
|
+
targetDocument.corePrompt ||= 'Produce a concise, evidence-grounded paper. Keep claims tied to the reported 120-paragraph evaluation and distinguish measured results from interpretation.';
|
|
135
|
+
for (const section of targetDocument.sections) {
|
|
136
|
+
section.prompt ||= DEMO_SECTION_PROMPTS[section.title] || 'Keep this section focused, precise, and connected to the document-level argument.';
|
|
137
|
+
for (const paragraph of section.children || []) {
|
|
138
|
+
paragraph.prompt ||= `Write one coherent ${section.title.toLowerCase()} paragraph with a clear rhetorical purpose and evidence-aware transitions.`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!data.libraries.corpora.length) data.libraries.corpora.push(
|
|
142
|
+
{
|
|
143
|
+
...demoResourceBase('demo_corpus_algorithm', timestamp), name: 'Algorithm evaluation checklist',
|
|
144
|
+
description: 'Reusable evidence requirements for empirical algorithm papers.',
|
|
145
|
+
content: 'Define the task and sample; name baselines; report metrics with uncertainty; separate measured findings from causal explanations; state limitations and reproducibility conditions.',
|
|
146
|
+
source: 'Papergod built-in demo', tags: ['algorithm', 'evaluation', 'methods', 'results'],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
...demoResourceBase('demo_corpus_reproducibility', timestamp), name: 'Reproducibility checklist',
|
|
150
|
+
description: 'Items reviewers expect when assessing reproducible computational work.',
|
|
151
|
+
content: 'Report data selection, annotator procedure, evaluation scale, sample size, uncertainty, implementation assumptions, and threats to external validity.',
|
|
152
|
+
source: 'Papergod built-in demo', tags: ['reproducibility', 'review', 'methods'],
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
if (!data.libraries.sentencePatterns.length) data.libraries.sentencePatterns.push(
|
|
156
|
+
{
|
|
157
|
+
...demoResourceBase('demo_pattern_gap', timestamp), name: 'Research gap',
|
|
158
|
+
template: 'Although {{prior capability}}, existing approaches do not {{unresolved limitation}}.',
|
|
159
|
+
description: 'Contrast prior capability with the unresolved gap.', source: 'Papergod built-in demo',
|
|
160
|
+
tags: ['introduction', 'gap'], sectionTypes: ['Introduction'],
|
|
161
|
+
slots: [{ name: 'prior capability', description: 'What prior work already achieves', required: true }, { name: 'unresolved limitation', description: 'What remains unresolved', required: true }],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
...demoResourceBase('demo_pattern_method', timestamp), name: 'Evaluation protocol',
|
|
165
|
+
template: 'We evaluate {{system}} on {{sample}} using {{metric}}, with {{control}}.',
|
|
166
|
+
description: 'State an auditable empirical evaluation protocol.', source: 'Papergod built-in demo',
|
|
167
|
+
tags: ['methods', 'evaluation'], sectionTypes: ['Methods'],
|
|
168
|
+
slots: [{ name: 'system', description: 'Evaluated system', required: true }, { name: 'sample', description: 'Evaluation sample', required: true }, { name: 'metric', description: 'Outcome measure', required: true }, { name: 'control', description: 'Control or reliability procedure', required: true }],
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
...demoResourceBase('demo_pattern_result', timestamp), name: 'Quantitative comparison',
|
|
172
|
+
template: '{{method}} improved {{metric}} from {{baseline}} to {{result}}, corresponding to {{effect}}.',
|
|
173
|
+
description: 'Report a comparison without overstating causality.', source: 'Papergod built-in demo',
|
|
174
|
+
tags: ['results', 'quantitative'], sectionTypes: ['Results'],
|
|
175
|
+
slots: [{ name: 'method', description: 'Compared method', required: true }, { name: 'metric', description: 'Reported metric', required: true }, { name: 'baseline', description: 'Baseline value', required: true }, { name: 'result', description: 'Observed value', required: true }, { name: 'effect', description: 'Effect size or change', required: true }],
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
if (!data.libraries.vocabulary.global.length) data.libraries.vocabulary.global.push(
|
|
179
|
+
{ ...demoResourceBase('demo_vocab_show', timestamp), term: 'show', preferred: 'demonstrate', definition: 'Use for evidence-supported observations.', source: 'Papergod built-in demo', alternatives: ['indicate', 'suggest'], examples: ['The results demonstrate improved consistency.'], tags: ['academic', 'evidence'] },
|
|
180
|
+
{ ...demoResourceBase('demo_vocab_accuracy', timestamp), term: 'accuracy', preferred: 'reviewer acceptance rate', definition: 'Use the exact operationalized metric in this demo.', source: 'Papergod built-in demo', alternatives: ['acceptance rate'], examples: ['Reviewer acceptance rate increased to 84.8%.'], tags: ['results', 'metric'] },
|
|
181
|
+
);
|
|
182
|
+
if (!data.libraries.vocabulary.session.length) data.libraries.vocabulary.session.push(
|
|
183
|
+
{ ...demoResourceBase('demo_vocab_workflow', timestamp), term: 'our system', preferred: 'the structured workflow', definition: 'Agreed name for the method in this writing session.', source: 'Papergod demo agreement', alternatives: ['Papergod workflow'], examples: ['The structured workflow preserves traceable revision decisions.'], tags: ['terminology', 'session'] },
|
|
184
|
+
);
|
|
185
|
+
if (!data.annotations.length) {
|
|
186
|
+
const sentences = targetDocument.sections.flatMap((section) => (section.children || []).flatMap((paragraph) => paragraph.children || []));
|
|
187
|
+
const styleTarget = sentences.find((sentence) => sentence.text.includes('very important'));
|
|
188
|
+
const evidenceTarget = sentences.find((sentence) => sentence.text.includes('It was found'));
|
|
189
|
+
const addDemoAnnotation = (id, target, category, severity, body, suggestedFix) => {
|
|
190
|
+
if (!target) return;
|
|
191
|
+
data.annotations.push({
|
|
192
|
+
...demoResourceBase(id, timestamp), documentId: targetDocument.id,
|
|
193
|
+
target: { type: 'sentence', id: target.id, start: target.sourceRange.start, end: target.sourceRange.end, quote: target.text },
|
|
194
|
+
category, severity, body, suggestedFix, status: 'open', dependsOn: [],
|
|
195
|
+
source: { type: 'reviewer', actor: 'Built-in demo reviewer' },
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
addDemoAnnotation('demo_annotation_style', styleTarget, 'style', 'minor', 'Replace vague emphasis with a precise statement of practical importance.', 'Name the concrete research-team benefit instead of using “very important”.');
|
|
199
|
+
addDemoAnnotation('demo_annotation_evidence', evidenceTarget, 'evidence', 'major', 'The causal wording is not supported by the reported comparison.', 'Report the observed association and avoid implying that paragraph prompts alone caused the improvement.');
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
return project;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function initializeWorkspace(workspaceRoot, { demo = false } = {}) {
|
|
206
|
+
await mkdir(workspaceRoot, { recursive: true });
|
|
207
|
+
const entries = await readdir(workspaceRoot);
|
|
208
|
+
const texFiles = entries.filter((entry) => entry.endsWith('.tex'));
|
|
209
|
+
let createdSample = false;
|
|
210
|
+
if (texFiles.length === 0) {
|
|
211
|
+
await writeFile(join(workspaceRoot, 'main.tex'), demo ? DEMO_DOCUMENT : TEMPLATE_DOCUMENT, { encoding: 'utf-8', flag: 'wx' });
|
|
212
|
+
createdSample = true;
|
|
213
|
+
}
|
|
214
|
+
let project = await loadProject(workspaceRoot);
|
|
215
|
+
const selectedFile = texFiles.includes('main.tex') || createdSample ? 'main.tex' : texFiles.sort()[0];
|
|
216
|
+
if (project.documents.length === 1 && project.documents[0].file === 'main.tex' && selectedFile !== 'main.tex') {
|
|
217
|
+
project.documents[0].file = selectedFile;
|
|
218
|
+
project = await saveProject(workspaceRoot, project);
|
|
219
|
+
}
|
|
220
|
+
if (demo) project = await seedDemoWorkspace(workspaceRoot, selectedFile);
|
|
221
|
+
project = await seedStarterLibraries(workspaceRoot);
|
|
222
|
+
return { createdSample, project };
|
|
223
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const DEFAULT_BASE_URL = 'http://127.0.0.1:23119';
|
|
2
|
+
|
|
3
|
+
async function request(url, { fetchImpl = fetch, timeoutMs = 8000, method = 'GET', body, headers = {} } = {}) {
|
|
4
|
+
const controller = new AbortController();
|
|
5
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
6
|
+
try {
|
|
7
|
+
const response = await fetchImpl(url, {
|
|
8
|
+
method, body: body ? JSON.stringify(body) : undefined, signal: controller.signal,
|
|
9
|
+
headers: { Accept: 'application/json', 'Zotero-API-Version': '3', ...(body ? { 'Content-Type': 'application/json' } : {}), ...headers },
|
|
10
|
+
});
|
|
11
|
+
if (!response.ok) throw Object.assign(new Error(response.status === 403 ? 'Enable the Zotero local API in Settings → Advanced.' : `Zotero returned ${response.status}.`), { status: response.status === 403 ? 409 : 502, code: 'ZOTERO_UNAVAILABLE' });
|
|
12
|
+
return response;
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if (error.name === 'AbortError' || error.cause?.code === 'ECONNREFUSED') throw Object.assign(new Error('Zotero Desktop is not running or its local API is unavailable.'), { status: 503, code: 'ZOTERO_UNAVAILABLE' });
|
|
15
|
+
throw error;
|
|
16
|
+
} finally { clearTimeout(timer); }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function getZoteroStatus(options = {}) {
|
|
20
|
+
const baseUrl = options.baseUrl || DEFAULT_BASE_URL;
|
|
21
|
+
const response = await request(`${baseUrl}/api/`, options);
|
|
22
|
+
let betterBibtex = null;
|
|
23
|
+
try {
|
|
24
|
+
const bbt = await request(`${baseUrl}/better-bibtex/json-rpc`, {
|
|
25
|
+
...options, method: 'POST', body: { jsonrpc: '2.0', method: 'api.ready', params: [], id: 1 }, timeoutMs: 2500,
|
|
26
|
+
});
|
|
27
|
+
betterBibtex = (await bbt.json()).result || null;
|
|
28
|
+
} catch { betterBibtex = null; }
|
|
29
|
+
return {
|
|
30
|
+
connected: true, apiVersion: response.headers.get('zotero-api-version') || '3',
|
|
31
|
+
serverId: response.headers.get('zotero-server-id') || '', betterBibtex,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function listZoteroCollections({ libraryType = 'users', libraryId = '0', baseUrl = DEFAULT_BASE_URL, ...options } = {}) {
|
|
36
|
+
const response = await request(`${baseUrl}/api/${libraryType}/${encodeURIComponent(libraryId)}/collections?limit=500`, options);
|
|
37
|
+
return (await response.json()).map((entry) => ({ key: entry.key, name: entry.data?.name || '', parentCollection: entry.data?.parentCollection || false, version: entry.version }));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function zoteroType(type) {
|
|
41
|
+
if (['conferencePaper', 'presentation'].includes(type)) return 'inproceedings';
|
|
42
|
+
if (['book', 'encyclopediaArticle'].includes(type)) return 'book';
|
|
43
|
+
if (['bookSection'].includes(type)) return 'incollection';
|
|
44
|
+
if (['thesis'].includes(type)) return 'phdthesis';
|
|
45
|
+
if (['report'].includes(type)) return 'techreport';
|
|
46
|
+
return 'article';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function citationKey(data, itemKey) {
|
|
50
|
+
if (data.citationKey) return data.citationKey;
|
|
51
|
+
const extra = String(data.extra || '').match(/^Citation Key:\s*(.+)$/im)?.[1]?.trim();
|
|
52
|
+
return extra || `zotero${itemKey.toLowerCase()}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function normalizeZoteroItem(entry) {
|
|
56
|
+
const data = entry.data || entry;
|
|
57
|
+
return {
|
|
58
|
+
source: 'zotero', sourceId: entry.key || data.key, citekey: citationKey(data, entry.key || data.key || 'item'),
|
|
59
|
+
type: zoteroType(data.itemType), title: data.title || '',
|
|
60
|
+
authors: (data.creators || []).map((creator) => creator.name || [creator.lastName, creator.firstName].filter(Boolean).join(', ')).filter(Boolean),
|
|
61
|
+
year: String(data.date || '').match(/\d{4}/)?.[0] || '', doi: data.DOI || '', abstract: data.abstractNote || '',
|
|
62
|
+
url: data.url || '', containerTitle: data.publicationTitle || data.conferenceName || '',
|
|
63
|
+
fields: { volume: data.volume || '', number: data.issue || '', pages: data.pages || '', publisher: data.publisher || '', journal: data.publicationTitle || '', booktitle: data.conferenceName || '' },
|
|
64
|
+
confidence: data.DOI ? 1 : 0.85, status: 'verified', zoteroVersion: entry.version || data.version || 0,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function searchZoteroItems({ libraryType = 'users', libraryId = '0', collectionKey = '', query = '', limit = 100, baseUrl = DEFAULT_BASE_URL, ...options } = {}) {
|
|
69
|
+
const prefix = `${baseUrl}/api/${libraryType}/${encodeURIComponent(libraryId)}`;
|
|
70
|
+
const resource = collectionKey ? `/collections/${encodeURIComponent(collectionKey)}/items/top` : '/items/top';
|
|
71
|
+
const params = new URLSearchParams({ format: 'json', limit: String(Math.min(Math.max(Number(limit) || 100, 1), 200)), itemType: '-attachment' });
|
|
72
|
+
if (query) params.set('q', query);
|
|
73
|
+
const response = await request(`${prefix}${resource}?${params}`, options);
|
|
74
|
+
return (await response.json()).map(normalizeZoteroItem);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function enrichZoteroAttachment(reference, { libraryType = 'users', libraryId = '0', baseUrl = DEFAULT_BASE_URL, ...options } = {}) {
|
|
78
|
+
const response = await request(`${baseUrl}/api/${libraryType}/${encodeURIComponent(libraryId)}/items/${encodeURIComponent(reference.sourceId)}/children`, options);
|
|
79
|
+
const children = await response.json();
|
|
80
|
+
const attachment = children.find((entry) => entry.data?.itemType === 'attachment' && entry.data?.contentType === 'application/pdf');
|
|
81
|
+
return { ...reference, hasPdf: Boolean(attachment), attachmentKey: attachment?.key || '', attachmentTitle: attachment?.data?.title || '' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function getZoteroFullText(attachmentKey, { libraryType = 'users', libraryId = '0', baseUrl = DEFAULT_BASE_URL, ...options } = {}) {
|
|
85
|
+
if (!/^[A-Z0-9]{8}$/i.test(attachmentKey || '')) throw Object.assign(new Error('Invalid Zotero attachment key.'), { status: 400 });
|
|
86
|
+
const response = await request(`${baseUrl}/api/${libraryType}/${encodeURIComponent(libraryId)}/items/${attachmentKey}/fulltext`, options);
|
|
87
|
+
return await response.json();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function exportBetterBibTeX(citekeys, { baseUrl = DEFAULT_BASE_URL, ...options } = {}) {
|
|
91
|
+
if (!Array.isArray(citekeys) || !citekeys.length) return '';
|
|
92
|
+
const response = await request(`${baseUrl}/better-bibtex/json-rpc`, {
|
|
93
|
+
...options, method: 'POST', body: { jsonrpc: '2.0', method: 'item.export', params: [citekeys, 'Better BibTeX'], id: 1 },
|
|
94
|
+
});
|
|
95
|
+
const payload = await response.json();
|
|
96
|
+
if (payload.error) throw Object.assign(new Error(payload.error.message || 'Better BibTeX export failed.'), { status: 502 });
|
|
97
|
+
return typeof payload.result === 'string' ? payload.result : '';
|
|
98
|
+
}
|