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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +244 -0
  3. package/ROADMAP.md +171 -0
  4. package/example/main.tex +360 -0
  5. package/frontend/src/components/ui/badge.jsx +5 -0
  6. package/frontend/src/components/ui/button.jsx +24 -0
  7. package/frontend/src/components/workbench.jsx +182 -0
  8. package/frontend/src/lib/utils.js +6 -0
  9. package/frontend/src/main.jsx +19 -0
  10. package/frontend/src/theme.css +256 -0
  11. package/frontend/vite.config.js +23 -0
  12. package/package.json +73 -0
  13. package/papergod-demo.png +0 -0
  14. package/public/app.js +5480 -0
  15. package/public/brand/papergod-logo.png +0 -0
  16. package/public/i18n.js +95 -0
  17. package/public/index.html +480 -0
  18. package/public/pdf-sentence-mapping.js +142 -0
  19. package/public/react/app.js +209 -0
  20. package/public/react/assets/addon-fit-YJmn1quW.js +12 -0
  21. package/public/react/assets/addon-web-links-BWjmmSgS.js +12 -0
  22. package/public/react/assets/main.css +32 -0
  23. package/public/react/assets/xterm-BqvuqXEL.js +27 -0
  24. package/public/style.css +1462 -0
  25. package/src/cli.js +128 -0
  26. package/src/server/agent-adapters.js +1240 -0
  27. package/src/server/agent-errors.js +105 -0
  28. package/src/server/agent-runtime.js +81 -0
  29. package/src/server/agent.js +173 -0
  30. package/src/server/app-version.js +86 -0
  31. package/src/server/change-history.js +114 -0
  32. package/src/server/document-structure.js +174 -0
  33. package/src/server/index.js +1442 -0
  34. package/src/server/latex-structure.js +344 -0
  35. package/src/server/latex.js +67 -0
  36. package/src/server/library-engine.js +193 -0
  37. package/src/server/library-files.js +134 -0
  38. package/src/server/literature-review.js +122 -0
  39. package/src/server/orchestration-engine.js +662 -0
  40. package/src/server/paragraph-analysis.js +300 -0
  41. package/src/server/project-resources.js +290 -0
  42. package/src/server/project-store.js +808 -0
  43. package/src/server/prompt-manifest.js +300 -0
  44. package/src/server/references.js +425 -0
  45. package/src/server/review-panel.js +263 -0
  46. package/src/server/revise-workflow.js +278 -0
  47. package/src/server/revision-engine.js +607 -0
  48. package/src/server/security.js +16 -0
  49. package/src/server/text-extraction.js +149 -0
  50. package/src/server/workspace-browser.js +49 -0
  51. package/src/server/workspace-registry.js +143 -0
  52. package/src/server/workspace-terminal.js +99 -0
  53. package/src/server/workspace.js +223 -0
  54. package/src/server/zotero.js +98 -0
@@ -0,0 +1,134 @@
1
+ import { mkdir, writeFile } from 'fs/promises';
2
+ import { join } from 'path';
3
+ import { loadProject } from './project-store.js';
4
+
5
+ const LIBRARY_DIR = '.papergod';
6
+ const LIBRARY_SUBDIR = 'library';
7
+ const INDEX_FILE = 'index.json';
8
+
9
+ function now() { return new Date().toISOString(); }
10
+
11
+ function escapeMdInline(value) {
12
+ return String(value ?? '').replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&');
13
+ }
14
+
15
+ function escapeMdBlock(value) {
16
+ // Keep newlines; only guard against fenced code blocks and markdown headings.
17
+ const safe = String(value ?? '').replace(/```/g, '` ` `');
18
+ return safe.split('\n').map((line) => (line.startsWith('#') ? `\\${line}` : line)).join('\n');
19
+ }
20
+
21
+ function tagsLine(tags) {
22
+ const list = Array.isArray(tags) ? tags.filter(Boolean) : [];
23
+ return list.length ? list.join(', ') : '';
24
+ }
25
+
26
+ function renderCorpusMarkdown(libraries) {
27
+ const corpora = libraries.corpora || [];
28
+ const head = '# Papergod Writing Library — Corpora\n\n';
29
+ const body = corpora.map((item) => {
30
+ const blocks = [`## [${escapeMdInline(item.id)}] ${escapeMdInline(item.name)}`];
31
+ if (item.source) blocks.push(`- source: ${escapeMdInline(item.source)}`);
32
+ if (tagsLine(item.tags)) blocks.push(`- tags: ${escapeMdInline(tagsLine(item.tags))}`);
33
+ if (item.description) blocks.push(`- description: ${escapeMdInline(item.description)}`);
34
+ blocks.push('', escapeMdBlock(item.content || ''));
35
+ return blocks.join('\n');
36
+ }).join('\n\n---\n\n');
37
+ return head + (body || '(空库)');
38
+ }
39
+
40
+ function renderPatternsMarkdown(libraries) {
41
+ const patterns = libraries.sentencePatterns || [];
42
+ const head = '# Papergod Writing Library — Sentence Patterns\n\n';
43
+ const body = patterns.map((item) => {
44
+ const slots = (item.slots || []).map((slot) => `${slot.name}${slot.required ? '' : '?'}: ${slot.description || ''}`.trim()).join('; ');
45
+ const blocks = [`## [${escapeMdInline(item.id)}] ${escapeMdInline(item.name)}`];
46
+ if (item.source) blocks.push(`- source: ${escapeMdInline(item.source)}`);
47
+ if (tagsLine(item.tags)) blocks.push(`- tags: ${escapeMdInline(tagsLine(item.tags))}`);
48
+ if (item.sectionTypes?.length) blocks.push(`- sections: ${escapeMdInline(item.sectionTypes.join(', '))}`);
49
+ blocks.push(`- slots: ${escapeMdInline(slots || 'none')}`);
50
+ blocks.push('', `template: ${escapeMdBlock(item.template || '')}`);
51
+ if (item.description) blocks.push('', escapeMdBlock(item.description));
52
+ return blocks.join('\n');
53
+ }).join('\n\n---\n\n');
54
+ return head + (body || '(空库)');
55
+ }
56
+
57
+ function renderVocabularyMarkdown(scope) {
58
+ return (libraries) => {
59
+ const entries = (libraries.vocabulary?.[scope]) || [];
60
+ const head = `# Papergod Writing Library — Vocabulary (${scope})\n\n`;
61
+ const body = entries.map((item) => {
62
+ const blocks = [`## [${escapeMdInline(item.id)}] ${escapeMdInline(item.term)}${item.preferred ? ` → prefer: ${escapeMdInline(item.preferred)}` : ''}`];
63
+ if (item.definition) blocks.push(`- meaning: ${escapeMdInline(item.definition)}`);
64
+ if (item.source) blocks.push(`- source: ${escapeMdInline(item.source)}`);
65
+ if (tagsLine(item.tags)) blocks.push(`- tags: ${escapeMdInline(tagsLine(item.tags))}`);
66
+ if (item.examples?.length) blocks.push(`- examples: ${escapeMdInline(item.examples.join('; '))}`);
67
+ if (item.alternatives?.length) blocks.push(`- alternatives: ${escapeMdInline(item.alternatives.join(', '))}`);
68
+ return blocks.join('\n');
69
+ }).join('\n\n---\n\n');
70
+ return head + (body || '(空库)');
71
+ };
72
+ }
73
+
74
+ function summaryEntries(items, kind, file) {
75
+ return items.map((item) => ({
76
+ id: item.id,
77
+ kind,
78
+ name: item.name || item.term || item.title || '',
79
+ tags: Array.isArray(item.tags) ? item.tags.filter(Boolean) : [],
80
+ file,
81
+ }));
82
+ }
83
+
84
+ export const LIBRARY_FILE_LAYOUT = [
85
+ { kind: 'corpora', file: 'corpus.md', render: renderCorpusMarkdown, pick: (libraries) => libraries.corpora },
86
+ { kind: 'sentence-patterns', file: 'patterns.md', render: renderPatternsMarkdown, pick: (libraries) => libraries.sentencePatterns },
87
+ { kind: 'vocabulary-global', file: 'vocabulary-global.md', render: renderVocabularyMarkdown('global'), pick: (libraries) => libraries.vocabulary.global },
88
+ { kind: 'vocabulary-session', file: 'vocabulary-session.md', render: renderVocabularyMarkdown('session'), pick: (libraries) => libraries.vocabulary.session },
89
+ ];
90
+
91
+ export function libraryDirectory(workspaceRoot) {
92
+ return join(workspaceRoot, LIBRARY_DIR, LIBRARY_SUBDIR);
93
+ }
94
+
95
+ export function libraryIndexPath(workspaceRoot) {
96
+ return join(workspaceRoot, LIBRARY_DIR, INDEX_FILE);
97
+ }
98
+
99
+ // Materialize the writing library into readable files under .papergod/library/.
100
+ // Returns { paths, index } where `paths` is a list of relative file paths and
101
+ // `index` is the machine-readable catalog (all entries across the four files).
102
+ export async function materializeLibraries(workspaceRoot) {
103
+ const project = await loadProject(workspaceRoot);
104
+ const libraries = project.libraries || { corpora: [], sentencePatterns: [], vocabulary: { global: [], session: [] } };
105
+ const directory = libraryDirectory(workspaceRoot);
106
+ await mkdir(directory, { recursive: true });
107
+
108
+ const files = [];
109
+ const paths = [];
110
+ const entries = [];
111
+ for (const layout of LIBRARY_FILE_LAYOUT) {
112
+ const items = layout.pick(libraries) || [];
113
+ const content = layout.render(libraries);
114
+ const relative = `${LIBRARY_DIR}/${LIBRARY_SUBDIR}/${layout.file}`;
115
+ await writeFile(join(directory, layout.file), content, 'utf-8');
116
+ files.push({ kind: layout.kind, file: relative, count: items.length });
117
+ paths.push(relative);
118
+ entries.push(...summaryEntries(items, layout.kind, relative));
119
+ }
120
+
121
+ const index = {
122
+ generatedAt: now(),
123
+ files,
124
+ entries,
125
+ };
126
+ await writeFile(libraryIndexPath(workspaceRoot), `${JSON.stringify(index, null, 2)}\n`, 'utf-8');
127
+
128
+ return {
129
+ paths,
130
+ index,
131
+ directory: `.papergod/${LIBRARY_SUBDIR}`,
132
+ indexFile: `.papergod/${INDEX_FILE}`,
133
+ };
134
+ }
@@ -0,0 +1,122 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { loadReferenceState } from './references.js';
3
+ import { createAgentRun, updateAgentRun } from './project-resources.js';
4
+ import { runWritingAgent } from './agent-adapters.js';
5
+
6
+ const MAX_CITEKEYS = 30;
7
+ const SENTINEL = '[[PAPERGOD_LITERATURE_REVIEW]]';
8
+
9
+ function now() { return new Date().toISOString(); }
10
+ function id(prefix) { return `${prefix}_${randomUUID()}`; }
11
+ function clean(value) { return typeof value === 'string' ? value.trim() : ''; }
12
+ function problem(message, status = 400, code = '') {
13
+ const error = new Error(message);
14
+ error.status = status;
15
+ if (code) error.code = code;
16
+ return error;
17
+ }
18
+
19
+ function authorLabel(item) {
20
+ const authors = Array.isArray(item.authors) ? item.authors.filter(Boolean) : [];
21
+ if (!authors.length) return 'Unknown authors';
22
+ if (authors.length === 1) return authors[0];
23
+ if (authors.length === 2) return `${authors[0]} and ${authors[1]}`;
24
+ return `${authors[0]} et al.`;
25
+ }
26
+
27
+ function titleKeywords(title) {
28
+ return String(title || '')
29
+ .replace(/[^a-zA-Z0-9\s-]/g, ' ')
30
+ .split(/\s+/)
31
+ .filter((word) => word.length > 3 && !/^(the|and|for|with|from|into|over|under|about|between|their|this|that)$/i.test(word))
32
+ .slice(0, 4)
33
+ .join(' ');
34
+ }
35
+
36
+ export function composeMockLiteratureReview(items, prompt = '') {
37
+ const goal = clean(prompt) || 'the related work';
38
+ const sentences = [];
39
+ sentences.push(`A body of prior work has examined ${goal}, with each study contributing a distinct perspective (${items.map((item) => `\\citep{${item.citekey}}`).join(', ')}).`);
40
+ items.forEach((item, index) => {
41
+ const topic = titleKeywords(item.title) || 'the central problem';
42
+ const connector = index === items.length - 1 ? 'Finally' : index === 0 ? 'In particular' : 'Additionally';
43
+ sentences.push(`${connector}, ${authorLabel(item)} (${item.year || 'n.d.'}) investigated ${topic} and reported findings that bear directly on the questions addressed here (\\citep{${item.citekey}}).`);
44
+ });
45
+ sentences.push(`Taken together, these studies motivate the present work, which builds on their evidence while addressing the gaps they leave open.`);
46
+ return {
47
+ draft: sentences.join(' '),
48
+ note: 'Mock review is generated deterministically from bibliographic metadata (authors, year, title keywords). For substantive synthesis, run the same request with a configured external Agent.',
49
+ };
50
+ }
51
+
52
+ function buildBibliography(items) {
53
+ return items.map((item, index) => {
54
+ const parts = [
55
+ `${index + 1}. ${authorLabel(item)} (${item.year || 'n.d.'}).`,
56
+ item.title ? ` ${item.title}.` : '',
57
+ item.venue ? ` ${item.venue}.` : '',
58
+ ` [citekey: ${item.citekey}]`,
59
+ ];
60
+ if (item.abstract) parts.push(` Abstract: ${String(item.abstract).slice(0, 400)}`);
61
+ return parts.join('');
62
+ }).join('\n');
63
+ }
64
+
65
+ function buildExternalPrompt(bibliography, prompt) {
66
+ return `You are writing a literature review paragraph for an academic paper. Use only the references supplied below. Cite each relevant reference at least once with \\citep{citekey}. Write 3–6 sentences that synthesize the literature into coherent themes rather than listing entries one by one. Return exactly one suggestion that replaces the entire text ${SENTINEL} with the review paragraph, keeping every citekey that you actually use inside \\citep{...}.
67
+
68
+ Author instruction:
69
+ ${clean(prompt) || 'Synthesize the supplied references into one review paragraph.'}
70
+
71
+ References:
72
+ ${bibliography}`;
73
+ }
74
+
75
+ export async function generateLiteratureReview(workspaceRoot, input = {}, options = {}) {
76
+ const citekeys = Array.isArray(input.citekeys) ? input.citekeys.filter((key) => typeof key === 'string' && key.trim()) : [];
77
+ if (!citekeys.length) throw problem('Select at least one reference');
78
+ if (citekeys.length > MAX_CITEKEYS) throw problem(`Select at most ${MAX_CITEKEYS} references`);
79
+ if (new Set(citekeys).size !== citekeys.length) throw problem('citekeys must not contain duplicates');
80
+ const state = await loadReferenceState(workspaceRoot);
81
+ const items = citekeys.map((key) => state.items.find((item) => item.citekey === key)).filter(Boolean);
82
+ const missing = citekeys.filter((key) => !items.some((item) => item.citekey === key));
83
+ if (missing.length) throw problem(`References not found in the library: ${missing.join(', ')}`, 404);
84
+ const prompt = clean(input.prompt);
85
+ const provider = options.provider || 'mock';
86
+ const bibliography = buildBibliography(items);
87
+ const startedAt = now();
88
+ const run = await createAgentRun(workspaceRoot, {
89
+ provider, operation: 'literature-review', status: provider === 'mock' ? 'queued' : 'running',
90
+ prompt: prompt || 'Synthesize the selected references into a review paragraph.',
91
+ input: JSON.stringify({ citekeys, characters: bibliography.length }), output: '', error: '', startedAt, finishedAt: '',
92
+ });
93
+ try {
94
+ let draft;
95
+ let note = '';
96
+ if (provider === 'mock') {
97
+ const composed = composeMockLiteratureReview(items, prompt);
98
+ draft = composed.draft;
99
+ note = composed.note;
100
+ } else {
101
+ const result = await runWritingAgent(provider, {
102
+ content: SENTINEL,
103
+ prompt: buildExternalPrompt(bibliography, prompt),
104
+ resourceContext: '', resourceIds: [],
105
+ }, { workspaceRoot, commands: options.commands || {}, signal: options.signal });
106
+ const proposal = result.suggestions?.find((item) => item.originalText === SENTINEL) || result.suggestions?.[0];
107
+ if (!proposal?.suggestedText?.trim()) throw problem('Agent did not return a review paragraph', 502);
108
+ draft = proposal.suggestedText;
109
+ }
110
+ const finishedAt = now();
111
+ await updateAgentRun(workspaceRoot, run.id, {
112
+ status: 'complete', output: JSON.stringify({ summary: draft.slice(0, 200), characters: draft.length }), finishedAt,
113
+ });
114
+ return {
115
+ runId: run.id, provider, draft, note, citekeys: items.map((item) => item.citekey),
116
+ bibliography: items.map((item) => ({ citekey: item.citekey, title: item.title, authors: item.authors, year: item.year })),
117
+ };
118
+ } catch (error) {
119
+ await updateAgentRun(workspaceRoot, run.id, { status: 'failed', error: String(error?.message || 'Literature review failed').slice(0, 4000), finishedAt: now() });
120
+ throw error;
121
+ }
122
+ }