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,174 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'crypto';
|
|
2
|
+
import { readFile } from 'fs/promises';
|
|
3
|
+
import { sanitizePath } from './security.js';
|
|
4
|
+
import { loadProject, updateProject } from './project-store.js';
|
|
5
|
+
import { findStructureNode, parseLatexDocument } from './latex-structure.js';
|
|
6
|
+
|
|
7
|
+
function error(message, status = 400) {
|
|
8
|
+
const value = new Error(message);
|
|
9
|
+
value.status = status;
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function sourceHash(content) {
|
|
14
|
+
return createHash('sha256').update(content).digest('hex');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolveTexFile(workspaceRoot, file) {
|
|
18
|
+
const path = sanitizePath(file, workspaceRoot);
|
|
19
|
+
if (!path) throw error('Access denied', 403);
|
|
20
|
+
if (!path.endsWith('.tex')) throw error('Only .tex files can be structured');
|
|
21
|
+
return path;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeAnchor(value) {
|
|
25
|
+
return String(value || '').replace(/\\[a-zA-Z@]+\*?(?:\[[^\]]*\])?/g, ' ')
|
|
26
|
+
.replace(/[{}]/g, ' ').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function documentNodes(document) {
|
|
30
|
+
const nodes = [];
|
|
31
|
+
const visit = (items) => {
|
|
32
|
+
for (const item of items || []) {
|
|
33
|
+
nodes.push(item);
|
|
34
|
+
visit(item.children);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
visit(document.sections);
|
|
38
|
+
return nodes;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function reconcileDocumentAnnotations(project, document, content) {
|
|
42
|
+
const nodes = documentNodes(document);
|
|
43
|
+
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
44
|
+
const sourceFor = (node) => node.sourceRange
|
|
45
|
+
? content.slice(node.sourceRange.start, node.sourceRange.end)
|
|
46
|
+
: String(node.text || '');
|
|
47
|
+
const matchesQuote = (node, quote) => {
|
|
48
|
+
if (!quote) return true;
|
|
49
|
+
const raw = sourceFor(node);
|
|
50
|
+
return raw.includes(quote) || normalizeAnchor(raw).includes(normalizeAnchor(quote));
|
|
51
|
+
};
|
|
52
|
+
for (const annotation of project.annotations || []) {
|
|
53
|
+
if (annotation.documentId !== document.id || annotation.target?.type === 'document') continue;
|
|
54
|
+
const quote = String(annotation.target?.quote || '');
|
|
55
|
+
const current = nodeById.get(annotation.target.id);
|
|
56
|
+
if (current && matchesQuote(current, quote)) continue;
|
|
57
|
+
const preferredType = annotation.target.type === 'range' ? 'sentence' : annotation.target.type;
|
|
58
|
+
const candidates = nodes.filter((node) => node.type === preferredType && matchesQuote(node, quote));
|
|
59
|
+
const replacement = candidates.sort((left, right) => sourceFor(left).length - sourceFor(right).length)[0]
|
|
60
|
+
|| nodes.filter((node) => node.type === 'sentence' && matchesQuote(node, quote))[0]
|
|
61
|
+
|| null;
|
|
62
|
+
if (!replacement) {
|
|
63
|
+
annotation.target = { ...annotation.target, type: 'document', id: document.id, start: 0, end: 0 };
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const raw = sourceFor(replacement);
|
|
67
|
+
const relativeStart = quote ? raw.indexOf(quote) : -1;
|
|
68
|
+
const base = replacement.sourceRange?.start || 0;
|
|
69
|
+
const start = relativeStart >= 0 ? base + relativeStart : base;
|
|
70
|
+
const end = relativeStart >= 0 ? start + quote.length : (replacement.sourceRange?.end || start);
|
|
71
|
+
annotation.target = {
|
|
72
|
+
...annotation.target,
|
|
73
|
+
type: annotation.target.type === 'range' ? 'range' : replacement.type,
|
|
74
|
+
id: replacement.id,
|
|
75
|
+
start,
|
|
76
|
+
end,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function syncDocumentStructure(workspaceRoot, file) {
|
|
82
|
+
const path = resolveTexFile(workspaceRoot, file);
|
|
83
|
+
let content;
|
|
84
|
+
try {
|
|
85
|
+
content = await readFile(path, 'utf-8');
|
|
86
|
+
} catch (cause) {
|
|
87
|
+
if (cause.code === 'ENOENT') throw error('File not found', 404);
|
|
88
|
+
throw cause;
|
|
89
|
+
}
|
|
90
|
+
const hash = sourceHash(content);
|
|
91
|
+
const { result } = await updateProject(workspaceRoot, (project) => {
|
|
92
|
+
let document = project.documents.find((item) => item.file === file);
|
|
93
|
+
if (!document) {
|
|
94
|
+
document = {
|
|
95
|
+
id: `document_${randomUUID()}`, file, title: '', summary: '', corePrompt: '', sections: [],
|
|
96
|
+
};
|
|
97
|
+
project.documents.push(document);
|
|
98
|
+
}
|
|
99
|
+
const parsed = parseLatexDocument(content, document);
|
|
100
|
+
document.title = parsed.title;
|
|
101
|
+
document.sections = parsed.sections;
|
|
102
|
+
document.sourceHash = hash;
|
|
103
|
+
document.sourceLength = parsed.sourceLength;
|
|
104
|
+
reconcileDocumentAnnotations(project, document, content);
|
|
105
|
+
return structuredClone(document);
|
|
106
|
+
});
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function getDocumentStructure(workspaceRoot, documentId) {
|
|
111
|
+
const document = (await loadProject(workspaceRoot)).documents.find((item) => item.id === documentId);
|
|
112
|
+
if (!document) throw error('Document not found', 404);
|
|
113
|
+
return document;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function updateDocumentMetadata(workspaceRoot, documentId, input = {}) {
|
|
117
|
+
const { result } = await updateProject(workspaceRoot, (project) => {
|
|
118
|
+
const document = project.documents.find((item) => item.id === documentId);
|
|
119
|
+
if (!document) throw error('Document not found', 404);
|
|
120
|
+
for (const field of ['title', 'summary', 'corePrompt']) {
|
|
121
|
+
if (input[field] !== undefined) {
|
|
122
|
+
if (typeof input[field] !== 'string') throw error(`${field} must be a string`);
|
|
123
|
+
document[field] = input[field];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return structuredClone(document);
|
|
127
|
+
});
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function updateNodeMetadata(workspaceRoot, nodeId, input = {}) {
|
|
132
|
+
const { result } = await updateProject(workspaceRoot, (project) => {
|
|
133
|
+
let node = null;
|
|
134
|
+
for (const document of project.documents) {
|
|
135
|
+
node = findStructureNode(document, nodeId);
|
|
136
|
+
if (node) break;
|
|
137
|
+
}
|
|
138
|
+
if (!node) throw error('Structure node not found', 404);
|
|
139
|
+
for (const field of ['prompt', 'summary', 'intent']) {
|
|
140
|
+
if (input[field] !== undefined) {
|
|
141
|
+
if (typeof input[field] !== 'string') throw error(`${field} must be a string`);
|
|
142
|
+
if (field === 'intent' && node.type !== 'sentence') throw error('intent is only valid for sentence nodes');
|
|
143
|
+
node[field] = input[field];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return structuredClone(node);
|
|
147
|
+
});
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function getNodeSourceContext(workspaceRoot, nodeId) {
|
|
152
|
+
const project = await loadProject(workspaceRoot);
|
|
153
|
+
for (const document of project.documents) {
|
|
154
|
+
const node = findStructureNode(document, nodeId);
|
|
155
|
+
if (!node) continue;
|
|
156
|
+
if (!node.sourceRange) throw error('Node has no source range; synchronize the document first', 409);
|
|
157
|
+
const path = resolveTexFile(workspaceRoot, document.file);
|
|
158
|
+
const content = await readFile(path, 'utf-8');
|
|
159
|
+
if (document.sourceHash !== sourceHash(content)) throw error('Document changed; synchronize structure before editing this node', 409);
|
|
160
|
+
const { start, end } = node.sourceRange;
|
|
161
|
+
if (end > content.length) throw error('Node source range is stale; synchronize the document', 409);
|
|
162
|
+
const section = document.sections.find((candidate) => {
|
|
163
|
+
if (candidate.id === nodeId) return true;
|
|
164
|
+
const visit = (items) => items.some((item) => item.id === nodeId || visit(item.children || []));
|
|
165
|
+
return visit(candidate.children || []);
|
|
166
|
+
}) || null;
|
|
167
|
+
return {
|
|
168
|
+
project, document, node, section, content,
|
|
169
|
+
selectedContent: content.slice(start, end),
|
|
170
|
+
sourceRange: { start, end },
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
throw error('Structure node not found', 404);
|
|
174
|
+
}
|