papilot 0.0.0-stage → 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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -2
  3. package/README.zh-CN.md +127 -0
  4. package/bin/papilot.mjs +113 -0
  5. package/dist/apple-touch-icon.png +0 -0
  6. package/dist/assets/Inconsolatazi4-Regular-Dbtq7t_r.otf +0 -0
  7. package/dist/assets/TeXGyrePagellaX-Bold-CEtYdOXj.otf +0 -0
  8. package/dist/assets/TeXGyrePagellaX-Italic-CSuYIstn.otf +0 -0
  9. package/dist/assets/TeXGyrePagellaX-Regular-DQJjZE_L.otf +0 -0
  10. package/dist/assets/cursor-editorTextHighlight-kG3uUOwL.svg +8 -0
  11. package/dist/assets/index-BhQFCMTx.js +131 -0
  12. package/dist/assets/index-CAV3-87U.css +1 -0
  13. package/dist/assets/pdf-view-DJo0X38w.js +598 -0
  14. package/dist/assets/pdf.worker.min-CjEcRF4W.mjs +28 -0
  15. package/dist/favicon.png +0 -0
  16. package/dist/index.html +4 -0
  17. package/examples/papilot-paper/.latex-review.json +4 -0
  18. package/examples/papilot-paper/appendix/proofs.tex +60 -0
  19. package/examples/papilot-paper/appendix/reference.tex +54 -0
  20. package/examples/papilot-paper/main.tex +57 -0
  21. package/examples/papilot-paper/preamble.tex +53 -0
  22. package/examples/papilot-paper/references.bib +158 -0
  23. package/examples/papilot-paper/sections/01-introduction.tex +66 -0
  24. package/examples/papilot-paper/sections/02-using.tex +134 -0
  25. package/examples/papilot-paper/sections/03-background.tex +97 -0
  26. package/examples/papilot-paper/sections/04-model.tex +185 -0
  27. package/examples/papilot-paper/sections/05-conflicts.tex +99 -0
  28. package/examples/papilot-paper/sections/06-building.tex +70 -0
  29. package/examples/papilot-paper/sections/07-geometry.tex +193 -0
  30. package/examples/papilot-paper/sections/08-notes.tex +104 -0
  31. package/examples/papilot-paper/sections/09-agents.tex +106 -0
  32. package/examples/papilot-paper/sections/10-usage.tex +56 -0
  33. package/examples/papilot-paper/sections/11-git.tex +62 -0
  34. package/examples/papilot-paper/sections/12-security.tex +41 -0
  35. package/examples/papilot-paper/sections/13-evaluation.tex +106 -0
  36. package/examples/papilot-paper/sections/14-related.tex +38 -0
  37. package/examples/papilot-paper/sections/15-conclusion.tex +27 -0
  38. package/package.json +85 -4
  39. package/scripts/demo.ts +21 -0
  40. package/scripts/service.ts +35 -0
  41. package/src/notes.ts +40 -0
  42. package/src/server/agents.ts +343 -0
  43. package/src/server/codex.ts +63 -0
  44. package/src/server/frontend.ts +15 -0
  45. package/src/server/main.ts +202 -0
  46. package/src/server/manuscript.ts +337 -0
  47. package/src/server/paths.ts +10 -0
  48. package/src/server/pdf.ts +27 -0
  49. package/src/server/review.ts +53 -0
  50. package/src/server/security.ts +15 -0
  51. package/src/server/store.ts +60 -0
  52. package/src/server/workspace.ts +529 -0
  53. package/src/shared.ts +28 -0
  54. package/vendor/latex-review/LICENSE +21 -0
  55. package/vendor/latex-review/dist/web/assets/TeXGyrePagellaX-Bold-CEtYdOXj.otf +0 -0
  56. package/vendor/latex-review/dist/web/assets/TeXGyrePagellaX-Regular-DQJjZE_L.otf +0 -0
  57. package/vendor/latex-review/dist/web/assets/index-DyzYnIGk.css +1 -0
  58. package/vendor/latex-review/dist/web/assets/index-HxBK-dls.js +19 -0
  59. package/vendor/latex-review/dist/web/index.html +18 -0
  60. package/vendor/latex-review/package-lock.json +3114 -0
  61. package/vendor/latex-review/package.json +49 -0
  62. package/vendor/latex-review/src/cli.ts +494 -0
  63. package/vendor/latex-review/src/client.ts +198 -0
  64. package/vendor/latex-review/src/core/build.ts +296 -0
  65. package/vendor/latex-review/src/core/changes.ts +126 -0
  66. package/vendor/latex-review/src/core/config.ts +270 -0
  67. package/vendor/latex-review/src/core/diff.ts +54 -0
  68. package/vendor/latex-review/src/core/files.ts +96 -0
  69. package/vendor/latex-review/src/core/git.ts +317 -0
  70. package/vendor/latex-review/src/core/outline.ts +314 -0
  71. package/vendor/latex-review/src/core/process.ts +111 -0
  72. package/vendor/latex-review/src/core/publish.ts +339 -0
  73. package/vendor/latex-review/src/core/review.ts +734 -0
  74. package/vendor/latex-review/src/core/store.ts +66 -0
  75. package/vendor/latex-review/src/core/tasks.ts +237 -0
  76. package/vendor/latex-review/src/render/inline.ts +805 -0
  77. package/vendor/latex-review/src/render/locate.ts +481 -0
  78. package/vendor/latex-review/src/render/pdf.ts +281 -0
  79. package/vendor/latex-review/src/render/pdfdiff.ts +272 -0
  80. package/vendor/latex-review/src/render/round.ts +236 -0
  81. package/vendor/latex-review/src/render/worker.ts +10 -0
  82. package/vendor/latex-review/src/runtime.ts +23 -0
  83. package/vendor/latex-review/src/server.ts +401 -0
  84. package/vendor/latex-review/src/types.ts +283 -0
  85. package/vendor/latex-review/src/web/ChangeMap.tsx +212 -0
  86. package/vendor/latex-review/src/web/PdfStrip.tsx +77 -0
  87. package/vendor/latex-review/src/web/fonts/GUST-FONT-LICENSE.txt +29 -0
  88. package/vendor/latex-review/src/web/fonts/LPPL.txt +415 -0
  89. package/vendor/latex-review/src/web/fonts/MANIFEST-newpx.txt +20 -0
  90. package/vendor/latex-review/src/web/fonts/README-newpx.txt +473 -0
  91. package/vendor/latex-review/src/web/fonts/README.md +12 -0
  92. package/vendor/latex-review/src/web/fonts/TeXGyrePagellaX-Bold.otf +0 -0
  93. package/vendor/latex-review/src/web/fonts/TeXGyrePagellaX-Regular.otf +0 -0
  94. package/vendor/latex-review/src/web/index.html +17 -0
  95. package/vendor/latex-review/src/web/main.tsx +10 -0
  96. package/vendor/latex-review/src/web/model.ts +153 -0
  97. package/vendor/latex-review/src/web/overview.tsx +146 -0
  98. package/vendor/latex-review/src/web/review.tsx +991 -0
  99. package/vendor/latex-review/src/web/style.css +1249 -0
@@ -0,0 +1,270 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { Repo, cleanPath, within } from "./git.ts";
4
+ export const CONFIG_NAME = ".latex-review.json";
5
+ export const ENGINES = ["pdflatex", "xelatex", "lualatex"] as const;
6
+ type Engine = (typeof ENGINES)[number];
7
+ interface Settings {
8
+ main?: string;
9
+ engine?: Engine;
10
+ latexmkrc?: string;
11
+ source_dir?: string;
12
+ partial_builds?: boolean;
13
+ preload?: Record<string, string>;
14
+ dependencies?: Record<string, string[]>;
15
+ figures?: string[];
16
+ worktrees?: string;
17
+ build_workers?: number;
18
+ }
19
+ export interface Project {
20
+ root: string;
21
+ state: string;
22
+ main: string;
23
+ engine: Engine;
24
+ latexmkrc: string | null;
25
+ source_dir: string;
26
+ partial_builds: boolean;
27
+ preload: Record<string, string>;
28
+ dependencies: Record<string, string[]>;
29
+ figures: string[];
30
+ worktrees: string;
31
+ build_workers: number;
32
+ doc_dir: string;
33
+ }
34
+ const SKIP = new Set(["build", "out", "output", "node_modules", "__pycache__"]);
35
+ const PREFERRED = [
36
+ "main",
37
+ "thesis",
38
+ "paper",
39
+ "manuscript",
40
+ "document",
41
+ "ms",
42
+ "article",
43
+ "report",
44
+ "book",
45
+ ];
46
+ const read = (root: string, file: string) =>
47
+ fs.readFileSync(path.join(root, file), "utf8");
48
+ export function detectMain(root: string) {
49
+ const directories = [
50
+ "",
51
+ ...fs
52
+ .readdirSync(root, { withFileTypes: true })
53
+ .filter(
54
+ (d) => d.isDirectory() && !d.name.startsWith(".") && !SKIP.has(d.name),
55
+ )
56
+ .map((d) => d.name)
57
+ .sort(),
58
+ ];
59
+ for (const directory of directories)
60
+ for (const rc of ["latexmkrc", ".latexmkrc"]) {
61
+ const file = path.join(directory, rc);
62
+ if (fs.existsSync(path.join(root, file))) {
63
+ const match = read(root, file).match(
64
+ /@default_files\s*=\s*\(\s*['"]([^'"]+)['"]/,
65
+ );
66
+ if (match && fs.existsSync(path.join(root, directory, match[1])))
67
+ return path.posix.join(directory, match[1]);
68
+ }
69
+ }
70
+ const found = directories
71
+ .flatMap((directory) =>
72
+ fs
73
+ .readdirSync(path.join(root, directory))
74
+ .filter((name) => name.endsWith(".tex"))
75
+ .map((name) => path.posix.join(directory, name)),
76
+ )
77
+ .filter((file) => {
78
+ const text = read(root, file).slice(0, 300000);
79
+ return (
80
+ /^[^%\n]*\\documentclass/m.test(text) &&
81
+ text.includes("\\begin{document}")
82
+ );
83
+ });
84
+ if (found.length === 1) return found[0];
85
+ if (!found.length)
86
+ throw new Error(
87
+ `No root document found. Create ${CONFIG_NAME} with {"main":"path/to/main.tex"}.`,
88
+ );
89
+ const rank = (file: string) => [
90
+ file.split("/").length,
91
+ PREFERRED.includes(path.basename(file, ".tex").toLowerCase())
92
+ ? PREFERRED.indexOf(path.basename(file, ".tex").toLowerCase())
93
+ : PREFERRED.length,
94
+ ];
95
+ found.sort((a, b) => rank(a)[0] - rank(b)[0] || rank(a)[1] - rank(b)[1]);
96
+ if (
97
+ JSON.stringify(rank(found[0])) !== JSON.stringify(rank(found[1])) &&
98
+ rank(found[0])[1] < PREFERRED.length
99
+ )
100
+ return found[0];
101
+ throw new Error(
102
+ `Several root documents were found (${found.slice(0, 6).join(", ")}). Set "main" in ${CONFIG_NAME}.`,
103
+ );
104
+ }
105
+ export function loadProject(root: string): Project {
106
+ root = fs.realpathSync(root);
107
+ const file = path.join(root, CONFIG_NAME);
108
+ let settings: Settings = {};
109
+ if (fs.existsSync(file)) {
110
+ try {
111
+ settings = JSON.parse(fs.readFileSync(file, "utf8"));
112
+ } catch {
113
+ throw new Error(`${CONFIG_NAME} is not valid JSON.`);
114
+ }
115
+ }
116
+ const allowed = new Set([
117
+ "main",
118
+ "engine",
119
+ "latexmkrc",
120
+ "source_dir",
121
+ "partial_builds",
122
+ "preload",
123
+ "dependencies",
124
+ "figures",
125
+ "worktrees",
126
+ "build_workers",
127
+ ]);
128
+ if (!settings || typeof settings !== "object" || Array.isArray(settings))
129
+ throw new Error(`${CONFIG_NAME} must contain an object.`);
130
+ const unknown = Object.keys(settings).filter((k) => !allowed.has(k));
131
+ if (unknown.length)
132
+ throw new Error(`Unknown settings: ${unknown.join(", ")}`);
133
+ for (const key of [
134
+ "main",
135
+ "engine",
136
+ "latexmkrc",
137
+ "source_dir",
138
+ "worktrees",
139
+ ] as const)
140
+ if (settings[key] !== undefined && typeof settings[key] !== "string")
141
+ throw new Error(`"${key}" must be a string.`);
142
+ if (
143
+ settings.partial_builds !== undefined &&
144
+ typeof settings.partial_builds !== "boolean"
145
+ )
146
+ throw new Error('"partial_builds" must be a boolean.');
147
+ if (
148
+ settings.figures !== undefined &&
149
+ (!Array.isArray(settings.figures) ||
150
+ settings.figures.some((f) => typeof f !== "string"))
151
+ )
152
+ throw new Error('"figures" must be a list of paths.');
153
+ if (settings.preload === null || settings.dependencies === null)
154
+ throw new Error('"preload" and "dependencies" must be objects.');
155
+ const main = settings.main ? cleanPath(settings.main) : detectMain(root),
156
+ doc_dir = path.posix.dirname(main) === "." ? "" : path.posix.dirname(main);
157
+ if (!fs.existsSync(path.join(root, main)))
158
+ throw new Error(`Root document not found: ${main}`);
159
+ const latexmkrc = settings.latexmkrc
160
+ ? cleanPath(settings.latexmkrc)
161
+ : ([...new Set([doc_dir, ""])]
162
+ .flatMap((dir) =>
163
+ ["latexmkrc", ".latexmkrc"].map((n) => path.posix.join(dir, n)),
164
+ )
165
+ .find((f) => fs.existsSync(path.join(root, f))) ?? null);
166
+ const text = read(root, main),
167
+ magic = text
168
+ .split("\n")
169
+ .slice(0, 40)
170
+ .join("\n")
171
+ .match(/^%\s*!\s*TEX\s+(?:TS-)?program\s*=\s*(\w+)/im)?.[1]
172
+ .toLowerCase();
173
+ const aliases: Record<string, Engine> = {
174
+ xetex: "xelatex",
175
+ luatex: "lualatex",
176
+ pdftex: "pdflatex",
177
+ pdflatex: "pdflatex",
178
+ xelatex: "xelatex",
179
+ lualatex: "lualatex",
180
+ };
181
+ const mode = latexmkrc
182
+ ? read(root, latexmkrc).match(/^\s*\$pdf_mode\s*=\s*(\d)/m)?.[1]
183
+ : null;
184
+ const hinted =
185
+ /^[^%\n]*(?:\\usepackage(?:\[[^\]]*\])?\{[^}]*\b(?:fontspec|xeCJK|unicode-math|polyglossia)\b|\\documentclass(?:\[[^\]]*\])?\{ctex)/m.test(
186
+ text,
187
+ );
188
+ const engine =
189
+ settings.engine ??
190
+ ((magic && aliases[magic]) ||
191
+ (
192
+ { "1": "pdflatex", "4": "lualatex", "5": "xelatex" } as Record<
193
+ string,
194
+ Engine
195
+ >
196
+ )[mode ?? ""] ||
197
+ (hinted ? "xelatex" : "pdflatex"));
198
+ if (!ENGINES.includes(engine as Engine))
199
+ throw new Error(`"engine" must be one of ${ENGINES.join(", ")}.`);
200
+ const source_dir = cleanPath(settings.source_dir ?? doc_dir);
201
+ if (
202
+ !within(main, [source_dir]) ||
203
+ (latexmkrc && !within(latexmkrc, [source_dir]))
204
+ )
205
+ throw new Error(
206
+ '"source_dir" must contain the root document and its latexmkrc.',
207
+ );
208
+ const preload = settings.preload ?? {},
209
+ dependencies = settings.dependencies ?? {};
210
+ if (
211
+ Array.isArray(preload) ||
212
+ typeof preload !== "object" ||
213
+ Object.values(preload).some((v) => typeof v !== "string")
214
+ )
215
+ throw new Error('"preload" maps include targets to filenames.');
216
+ if (
217
+ Array.isArray(dependencies) ||
218
+ typeof dependencies !== "object" ||
219
+ Object.values(dependencies).some(
220
+ (v) => !Array.isArray(v) || v.some((x) => typeof x !== "string"),
221
+ )
222
+ )
223
+ throw new Error('"dependencies" maps include targets to lists of targets.');
224
+ const figures =
225
+ settings.figures?.map(cleanPath) ??
226
+ ["figures", "figs"]
227
+ .map((n) => path.posix.join(doc_dir, n))
228
+ .filter(
229
+ (n) =>
230
+ fs.existsSync(path.join(root, n)) &&
231
+ fs.statSync(path.join(root, n)).isDirectory(),
232
+ );
233
+ const worktrees = settings.worktrees
234
+ ? path.resolve(root, settings.worktrees)
235
+ : path.join(path.dirname(root), path.basename(root) + "-tasks");
236
+ if (worktrees === root || worktrees.startsWith(root + path.sep))
237
+ throw new Error('"worktrees" must be outside the repository.');
238
+ const build_workers = settings.build_workers ?? 1;
239
+ if (
240
+ !Number.isInteger(build_workers) ||
241
+ build_workers < 1 ||
242
+ build_workers > 8
243
+ )
244
+ throw new Error('"build_workers" must be an integer between 1 and 8.');
245
+ return {
246
+ root,
247
+ state: path.join(new Repo(root).gitDir(), "latex-review"),
248
+ main,
249
+ doc_dir,
250
+ engine: engine as Engine,
251
+ latexmkrc,
252
+ source_dir,
253
+ partial_builds: settings.partial_builds !== false,
254
+ preload,
255
+ dependencies,
256
+ figures,
257
+ worktrees,
258
+ build_workers,
259
+ };
260
+ }
261
+ export const signature = (p: Project) =>
262
+ JSON.stringify([
263
+ p.main,
264
+ p.engine,
265
+ p.latexmkrc,
266
+ p.source_dir,
267
+ p.preload,
268
+ p.dependencies,
269
+ ]);
270
+ export const isFigure = (p: Project, file: string) => within(file, p.figures);
@@ -0,0 +1,54 @@
1
+ import { diffArrays } from "diff";
2
+ import type { Op } from "../types.ts";
3
+ /** Stable line/token opcodes shared by source and PDF comparisons. */
4
+ export function opcodes<T>(a: T[], b: T[]): Op[] {
5
+ const result: Op[] = [];
6
+ let i = 0,
7
+ j = 0;
8
+ for (const part of diffArrays(a, b)) {
9
+ const n = part.value.length;
10
+ if (!part.added && !part.removed) {
11
+ result.push(["equal", i, i + n, j, j + n]);
12
+ i += n;
13
+ j += n;
14
+ } else {
15
+ const previous = result.at(-1);
16
+ const tag = part.added ? "insert" : "delete";
17
+ const endI = i + (part.removed ? n : 0),
18
+ endJ = j + (part.added ? n : 0);
19
+ if (previous && previous[0] !== "equal") {
20
+ previous[0] =
21
+ previous[1] < endI && previous[3] < endJ ? "replace" : tag;
22
+ previous[2] = endI;
23
+ previous[4] = endJ;
24
+ } else result.push([tag, i, endI, j, endJ]);
25
+ i = endI;
26
+ j = endJ;
27
+ }
28
+ }
29
+ return result;
30
+ }
31
+ export function groups(ops: Op[], context = 3) {
32
+ const result: Op[][] = [];
33
+ let group: Op[] = [];
34
+ for (const op of ops) {
35
+ if (op[0] === "equal") {
36
+ if (op[2] - op[1] > 2 * context) {
37
+ if (group.length) result.push(group);
38
+ group = [];
39
+ }
40
+ } else group.push(op);
41
+ }
42
+ if (group.length) result.push(group);
43
+ return result;
44
+ }
45
+ export function similarity(a: string[], b: string[]) {
46
+ return (
47
+ (2 *
48
+ opcodes(a, b).reduce(
49
+ (n, o) => n + (o[0] === "equal" ? o[2] - o[1] : 0),
50
+ 0,
51
+ )) /
52
+ Math.max(a.length + b.length, 1)
53
+ );
54
+ }
@@ -0,0 +1,96 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import lockfile from "proper-lockfile";
5
+
6
+ export function atomicWrite(
7
+ file: string,
8
+ data: string | Uint8Array,
9
+ mode?: number,
10
+ ) {
11
+ fs.mkdirSync(path.dirname(file), { recursive: true });
12
+ const temporary = path.join(
13
+ path.dirname(file),
14
+ `.${path.basename(file)}.${randomUUID()}.tmp`,
15
+ );
16
+ try {
17
+ const fd = fs.openSync(temporary, "w");
18
+ try {
19
+ fs.writeFileSync(fd, data);
20
+ fs.fsyncSync(fd);
21
+ } finally {
22
+ fs.closeSync(fd);
23
+ }
24
+ if (mode !== undefined) fs.chmodSync(temporary, mode);
25
+ else if (fs.existsSync(file))
26
+ fs.chmodSync(temporary, fs.statSync(file).mode & 0o7777);
27
+ fs.renameSync(temporary, file);
28
+ } finally {
29
+ fs.rmSync(temporary, { force: true });
30
+ }
31
+ }
32
+ export function readBytes(file: string): Buffer | null {
33
+ try {
34
+ return fs.readFileSync(file);
35
+ } catch (e) {
36
+ if ((e as NodeJS.ErrnoException).code === "ENOENT") return null;
37
+ throw e;
38
+ }
39
+ }
40
+ export function same(a: Uint8Array | null, b: Uint8Array | null) {
41
+ return a === null || b === null ? a === b : Buffer.from(a).equals(b);
42
+ }
43
+ export function writeOrDelete(file: string, data: Uint8Array | null) {
44
+ if (data === null) fs.rmSync(file, { force: true });
45
+ else atomicWrite(file, data);
46
+ }
47
+ export function readJson<T>(file: string, fallback: T): T {
48
+ try {
49
+ return JSON.parse(fs.readFileSync(file, "utf8")) as T;
50
+ } catch (e) {
51
+ if (
52
+ e instanceof SyntaxError ||
53
+ (e as NodeJS.ErrnoException).code === "ENOENT"
54
+ )
55
+ return fallback;
56
+ throw e;
57
+ }
58
+ }
59
+ export function writeJson(file: string, value: unknown, mode?: number) {
60
+ atomicWrite(file, JSON.stringify(value, null, 2) + "\n", mode);
61
+ }
62
+ export async function acquireLock(file: string, retry = true) {
63
+ fs.mkdirSync(path.dirname(file), { recursive: true });
64
+ fs.closeSync(fs.openSync(file, "a"));
65
+ return lockfile.lock(file, {
66
+ realpath: false,
67
+ stale: 30000,
68
+ update: 10000,
69
+ retries: retry ? { retries: 1800, minTimeout: 100, maxTimeout: 500 } : 0,
70
+ });
71
+ }
72
+ export async function withLock<T>(
73
+ file: string,
74
+ work: () => Promise<T>,
75
+ retry = true,
76
+ ) {
77
+ const release = await acquireLock(file, retry);
78
+ try {
79
+ return await work();
80
+ } finally {
81
+ await release();
82
+ }
83
+ }
84
+ export function listFiles(root: string): string[] {
85
+ if (!fs.existsSync(root)) return [];
86
+ return fs
87
+ .readdirSync(root, { withFileTypes: true })
88
+ .flatMap((e) =>
89
+ e.isDirectory()
90
+ ? listFiles(path.join(root, e.name))
91
+ : [path.join(root, e.name)],
92
+ );
93
+ }
94
+ export const now = () => Date.now() / 1000;
95
+ export const sleep = (ms: number) =>
96
+ new Promise<void>((resolve) => setTimeout(resolve, ms));