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,53 @@
1
+ import { pathToFileURL } from 'node:url';
2
+ import path from 'node:path';
3
+ import { ENGINE } from './paths.ts';
4
+ import type { Task } from '../shared.ts';
5
+
6
+ export interface ReviewClient { url: string; token: string; get<T = any>(route: string): Promise<T>; post<T = any>(route: string, body?: unknown): Promise<T> }
7
+ let modulePromise: Promise<{ connect: (root: string) => Promise<ReviewClient> }> | null = null;
8
+ export function reviewClient(root: string): Promise<ReviewClient> {
9
+ modulePromise ??= import(pathToFileURL(path.join(ENGINE, 'src/client.ts')).href);
10
+ return modulePromise!.then(module => module.connect(root));
11
+ }
12
+ export const taskRoute = (name: string) => `/api/tasks/${encodeURIComponent(name)}`;
13
+
14
+ export function confirmedReviewResponse(status: number, value: unknown, task: string): boolean {
15
+ if (!value || typeof value !== 'object') return false;
16
+ const result = value as Record<string, unknown>;
17
+ if (status === 400 || status === 409) return typeof result.error === 'string';
18
+ return status >= 200 && status < 300 && result.task === task && typeof result.csrf === 'string' && typeof result.closed === 'boolean'
19
+ && result.decisions !== null && typeof result.decisions === 'object' && !Array.isArray(result.decisions);
20
+ }
21
+
22
+ /** Add current workspace availability to viewer state; standalone engine state stays unchanged. */
23
+ export function annotateReviewState(value: unknown, tasks: readonly Task[]): unknown {
24
+ if (!value || typeof value !== 'object' || typeof (value as any).task !== 'string'
25
+ || !confirmedReviewResponse(200, value, (value as any).task)) return value;
26
+ const state = value as Record<string, unknown>;
27
+ const task = tasks.find(task => task.name === state.task);
28
+ let decisionLock: string | null = null;
29
+ let commentLock: string | null = null;
30
+ if (!task) decisionLock = commentLock = 'This task is no longer available in Papilot. Return to the workspace.';
31
+ else if (state.closed) decisionLock = commentLock = 'This review is finished. Accepted changes are in your manuscript.';
32
+ else if (task.status === 'deleting') decisionLock = commentLock = 'This task is being deleted. Review actions and comments are unavailable.';
33
+ else if (task.status === 'uncertain') decisionLock = 'Select Check status in Papilot before accepting, rejecting, or undoing changes. You can still add comments.';
34
+ else if (task.status === 'building') decisionLock = 'Wait for the PDF build before accepting, rejecting, or undoing changes. You can still add comments.';
35
+ else if (['running', 'waiting_input', 'queued'].includes(task.status)) decisionLock = 'Pause this task in Papilot before accepting, rejecting, or undoing changes. You can still add comments.';
36
+ else if (task.pendingReview) decisionLock = 'Wait for the current review action before accepting, rejecting, or undoing changes. You can still add comments.';
37
+ return { ...state, decisionLock, commentLock };
38
+ }
39
+
40
+ export function allowedReviewRoute(route: string, method: string): boolean {
41
+ if (route.includes('..') || route.includes('\\')) return false;
42
+ if (method === 'GET') return route === '/' || /^\/(assets|fonts)\/[\w./-]+$/.test(route) || /^\/files\/[a-z0-9-]+\/\d+\/[\w.-]+\.(svg|png)$/.test(route) || /^\/api\/tasks(?:\/[a-z0-9-]+(?:\/(?:state|rounds\/\d+))?)?$/.test(route);
43
+ return method === 'POST' && /^\/api\/tasks\/[a-z0-9-]+\/(decide|undo|note)$/.test(route);
44
+ }
45
+ export function rewriteReviewJson(value: any, prefix: string): any {
46
+ if (Array.isArray(value)) return value.map(v => rewriteReviewJson(v, prefix));
47
+ if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, k === 'url' && typeof v === 'string' && /^http:\/\/(127\.0\.0\.1|localhost):\d+\//.test(v) ? prefix + new URL(v).pathname + new URL(v).search : rewriteReviewJson(v, prefix)]));
48
+ return typeof value === 'string' && value.startsWith('/files/') ? prefix + value : value;
49
+ }
50
+ export function rewriteReviewHtml(html: string, prefix: string): string {
51
+ const script = `<script>(()=>{const prefix=${JSON.stringify(prefix)};if(!crypto.randomUUID)crypto.randomUUID=()=>Array.from(crypto.getRandomValues(new Uint8Array(20)),b=>b.toString(16).padStart(2,'0')).join('');const original=window.fetch;window.fetch=(input,init)=>{if(typeof input==='string'&&input.startsWith('/api/'))input=prefix+input;return original.call(window,input,init)};})();</script>`;
52
+ return html.replace(/(src|href)="\/(?!\/)/g, `$1="${prefix}/`).replace('<head>', '<head>' + script);
53
+ }
@@ -0,0 +1,15 @@
1
+ import type { IncomingHttpHeaders } from 'node:http';
2
+ export function authorize(headers: IncomingHttpHeaders, remoteAddress: string | undefined, port: number, publicOrigin: string, owner: string): string | null {
3
+ if (!['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(remoteAddress || '')) return 'Papilot only accepts local or Tailscale Serve connections.';
4
+ const local = new Set([`localhost:${port}`, `127.0.0.1:${port}`]);
5
+ const host = headers.host || '';
6
+ const remoteHost = publicOrigin ? new URL(publicOrigin).host : '';
7
+ if ((headers['x-forwarded-for'] || headers['tailscale-user-login']) && (!owner || headers['tailscale-user-login'] !== owner)) return 'This Tailscale account does not have access.';
8
+ if (local.has(host)) {
9
+ if (headers.origin && ![`http://localhost:${port}`, `http://127.0.0.1:${port}`].includes(headers.origin)) return 'The request origin does not match.';
10
+ return null;
11
+ }
12
+ if (host !== remoteHost || !owner || headers['tailscale-user-login'] !== owner) return 'Use your authorized Tailscale account to access Papilot.';
13
+ if (headers.origin && headers.origin !== publicOrigin) return 'The request origin does not match.';
14
+ return null;
15
+ }
@@ -0,0 +1,60 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { EventEmitter } from 'node:events';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import type { Annotation, Project, Task, Run, TimelineItem } from '../shared.ts';
6
+
7
+ export class Store extends EventEmitter {
8
+ db: DatabaseSync;
9
+ constructor(file: string) {
10
+ super();
11
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
12
+ this.db = new DatabaseSync(file);
13
+ this.db.exec(`PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;
14
+ CREATE TABLE IF NOT EXISTS projects(id TEXT PRIMARY KEY, path TEXT UNIQUE NOT NULL, data TEXT NOT NULL);
15
+ CREATE TABLE IF NOT EXISTS tasks(id TEXT PRIMARY KEY, project_id TEXT NOT NULL, data TEXT NOT NULL);
16
+ CREATE TABLE IF NOT EXISTS runs(id TEXT PRIMARY KEY, task_id TEXT NOT NULL, request_key TEXT UNIQUE NOT NULL, status TEXT NOT NULL, data TEXT NOT NULL);
17
+ CREATE TABLE IF NOT EXISTS items(id TEXT PRIMARY KEY, task_id TEXT NOT NULL, created INTEGER NOT NULL, data TEXT NOT NULL);
18
+ CREATE TABLE IF NOT EXISTS deleted_tasks(id TEXT PRIMARY KEY, deleted INTEGER NOT NULL);
19
+ CREATE TABLE IF NOT EXISTS annotations(id TEXT PRIMARY KEY, project_id TEXT NOT NULL, data TEXT NOT NULL);
20
+ CREATE INDEX IF NOT EXISTS task_items ON items(task_id,created);
21
+ CREATE INDEX IF NOT EXISTS task_runs ON runs(task_id,status);`);
22
+ }
23
+ private rows<T>(table: string): T[] { return (this.db.prepare(`SELECT data FROM ${table} ORDER BY rowid`).all() as { data: string }[]).map(r => JSON.parse(r.data)); }
24
+ projects() { return this.rows<Project>('projects'); }
25
+ tasks() { return this.rows<Task>('tasks'); }
26
+ runs() { return this.rows<Run>('runs'); }
27
+ project(id: string) { const v = this.projects().find(p => p.id === id); if (!v) throw new Error('Paper project not found.'); return v; }
28
+ task(id: string) { const row = this.db.prepare('SELECT data FROM tasks WHERE id=?').get(id) as { data: string } | undefined; if (!row) throw new Error('Task not found.'); return JSON.parse(row.data) as Task; }
29
+ projectByPath(p: string) { return this.projects().find(project => project.path === p); }
30
+ saveProject(p: Project) { this.db.prepare('INSERT INTO projects VALUES(?,?,?) ON CONFLICT(id) DO UPDATE SET path=excluded.path,data=excluded.data').run(p.id, p.path, JSON.stringify(p)); this.emit('change'); }
31
+ saveTask(t: Task) { t.updatedAt = Date.now(); this.db.prepare('INSERT INTO tasks VALUES(?,?,?) ON CONFLICT(id) DO UPDATE SET data=excluded.data').run(t.id, t.projectId, JSON.stringify(t)); this.emit('change'); return t; }
32
+ updateTask(id: string, update: Partial<Task>) { return this.saveTask({ ...this.task(id), ...update }); }
33
+ saveRun(r: Run) { this.db.prepare('INSERT INTO runs VALUES(?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET status=excluded.status,data=excluded.data').run(r.id, r.taskId, r.requestKey, r.status, JSON.stringify(r)); this.emit('change'); }
34
+ runByKey(key: string) { const row = this.db.prepare('SELECT data FROM runs WHERE request_key=?').get(key) as { data: string } | undefined; return row ? JSON.parse(row.data) as Run : undefined; }
35
+ items(taskId: string) { return (this.db.prepare('SELECT data FROM items WHERE task_id=? ORDER BY created,rowid').all(taskId) as { data: string }[]).map(r => JSON.parse(r.data) as TimelineItem); }
36
+ saveItem(item: TimelineItem) { this.db.prepare('INSERT INTO items VALUES(?,?,?,?) ON CONFLICT(id) DO UPDATE SET data=excluded.data').run(item.id, item.taskId, item.createdAt, JSON.stringify(item)); this.emit('change'); }
37
+ annotations(projectId: string) { return (this.db.prepare('SELECT data FROM annotations WHERE project_id=? ORDER BY rowid').all(projectId) as { data: string }[]).map(r => JSON.parse(r.data) as Annotation); }
38
+ saveAnnotation(a: Annotation) { this.db.prepare('INSERT INTO annotations VALUES(?,?,?) ON CONFLICT(id) DO UPDATE SET data=excluded.data').run(a.id, a.projectId, JSON.stringify(a)); this.emit('change'); return a; }
39
+ deleteAnnotation(projectId: string, id: string) { this.db.prepare('DELETE FROM annotations WHERE id=? AND project_id=?').run(id, projectId); this.emit('change'); }
40
+ taskDeleted(id: string) { return Boolean(this.db.prepare('SELECT 1 FROM deleted_tasks WHERE id=?').get(id)); }
41
+ deleteTask(id: string) {
42
+ this.transaction(() => {
43
+ this.db.prepare('INSERT INTO deleted_tasks VALUES(?,?) ON CONFLICT(id) DO NOTHING').run(id, Date.now());
44
+ this.db.prepare('DELETE FROM items WHERE task_id=?').run(id);
45
+ this.db.prepare('DELETE FROM runs WHERE task_id=?').run(id);
46
+ this.db.prepare('DELETE FROM tasks WHERE id=?').run(id);
47
+ });
48
+ this.emit('change');
49
+ }
50
+ transaction<T>(fn: () => T): T { this.db.exec('BEGIN IMMEDIATE'); try { const result = fn(); this.db.exec('COMMIT'); return result; } catch (e) { this.db.exec('ROLLBACK'); throw e; } }
51
+ recover() {
52
+ this.transaction(() => {
53
+ for (const t of this.tasks().filter(t => t.status === 'deleting')) this.updateTask(t.id, { error: 'The server restarted during deletion. Retry deletion to confirm cleanup.' });
54
+ for (const t of this.tasks().filter(t => t.pendingReview)) this.updateTask(t.id, { status: 'uncertain', error: 'The server restarted before a review action was confirmed. Select Check status before continuing.' });
55
+ for (const r of this.runs().filter(r => r.status === 'running')) this.saveRun({ ...r, status: 'interrupted', finishedAt: Date.now() });
56
+ for (const t of this.tasks().filter(t => ['running', 'waiting_input'].includes(t.status))) this.updateTask(t.id, { status: 'interrupted', turnId: null, error: 'The server restarted and interrupted the previous turn. Continue in the same conversation.' });
57
+ });
58
+ }
59
+ close() { this.db.close(); }
60
+ }