papilot 0.7.0 → 0.8.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/README.md +52 -30
- package/README.zh-CN.md +54 -30
- package/bin/doctor.mjs +53 -6
- package/bin/installation.d.mts +2 -0
- package/bin/installation.mjs +7 -1
- package/dist/assets/{_virtual_papilot-licenses-BWIsfrQh.js → _virtual_papilot-licenses-D7ro6ecN.js} +54 -3
- package/dist/assets/index-Cc-awdNn.css +1 -0
- package/dist/assets/index-D_VX51no.js +265 -0
- package/dist/assets/latex-BarqJFtQ.svg +1 -0
- package/dist/assets/{math-BsEl3u6P.js → math-V9uUaUzA.js} +1 -1
- package/dist/assets/pdf-view-Bnb2uIBE.js +598 -0
- package/dist/index.html +2 -2
- package/examples/papilot-paper/main.tex +1 -1
- package/examples/papilot-paper/sections/02-using.tex +3 -1
- package/examples/papilot-paper/sections/09-agents.tex +8 -5
- package/package.json +10 -6
- package/skills/ai-review.md +18 -0
- package/skills/latex-warnings.md +1 -1
- package/src/conversation.ts +6 -0
- package/src/server/acp.ts +354 -0
- package/src/server/agents.ts +220 -43
- package/src/server/ai-review-routes.ts +72 -0
- package/src/server/ai-review.ts +38 -0
- package/src/server/app.ts +53 -11
- package/src/server/build-problems.ts +329 -0
- package/src/server/changes.ts +25 -2
- package/src/server/codex.ts +2 -1
- package/src/server/cursor.ts +153 -0
- package/src/server/latex-installation.ts +43 -0
- package/src/server/manuscript.ts +165 -23
- package/src/server/paths.ts +1 -1
- package/src/server/references.ts +70 -0
- package/src/server/review.ts +3 -3
- package/src/server/skills.ts +5 -8
- package/src/server/store.ts +20 -2
- package/src/server/update-install.ts +53 -2
- package/src/server/update-worker.ts +6 -6
- package/src/server/updates.ts +43 -14
- package/src/server/workspace.ts +183 -29
- package/src/shared.ts +78 -7
- package/src/skill-icons.ts +18 -0
- package/src/skill-metadata.ts +45 -0
- package/vendor/latex-review/dist/web/assets/index-7VKAte5x.js +19 -0
- package/vendor/latex-review/dist/web/assets/index-DEz-LsGl.css +1 -0
- package/vendor/latex-review/dist/web/index.html +2 -2
- package/vendor/latex-review/src/core/bib.ts +692 -0
- package/vendor/latex-review/src/core/changes.ts +39 -0
- package/vendor/latex-review/src/core/review.ts +1 -0
- package/vendor/latex-review/src/core/verify.ts +784 -0
- package/vendor/latex-review/src/core/view.ts +204 -0
- package/vendor/latex-review/src/render/round.ts +11 -0
- package/vendor/latex-review/src/server.ts +65 -0
- package/vendor/latex-review/src/types.ts +125 -0
- package/vendor/latex-review/src/web/effect-preferences.ts +10 -0
- package/vendor/latex-review/src/web/references.tsx +499 -0
- package/vendor/latex-review/src/web/review.tsx +259 -131
- package/vendor/latex-review/src/web/style.css +285 -0
- package/vendor/latex-review/src/web/zh-CN.ts +75 -0
- package/dist/assets/index-CIJEYlP7.js +0 -252
- package/dist/assets/index-hOJEruFc.css +0 -1
- package/dist/assets/pdf-view-Cz9_vK5U.js +0 -598
- package/vendor/latex-review/dist/web/assets/index-Be9CtgcZ.css +0 -1
- package/vendor/latex-review/dist/web/assets/index-TvMWVSaw.js +0 -19
package/src/shared.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GroupInstructions, NoteGroup } from './note-groups.ts';
|
|
2
|
+
import type { SkillIcon } from './skill-icons.ts';
|
|
2
3
|
export type TaskStatus = 'idle' | 'queued' | 'running' | 'waiting_input' | 'building' | 'reviewing' | 'interrupted' | 'failed' | 'uncertain' | 'deleting';
|
|
3
4
|
export interface Project { id: string; name: string; path: string; createdAt: number }
|
|
4
5
|
export const MAX_ATTACHMENTS = 10;
|
|
@@ -6,7 +7,9 @@ export const MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
|
6
7
|
export const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
7
8
|
export const MAX_ATTACHMENT_TOTAL = 100 * 1024 * 1024;
|
|
8
9
|
export interface Attachment { id: string; projectId: string; name: string; size: number; mime: string }
|
|
10
|
+
export type UpdateChannel = 'stable' | 'dev';
|
|
9
11
|
export interface UpdateStatus {
|
|
12
|
+
channel?: UpdateChannel;
|
|
10
13
|
currentVersion: string; latestVersion: string | null;
|
|
11
14
|
phase: 'idle' | 'checking' | 'available' | 'installing' | 'waiting' | 'restarting' | 'updated' | 'failed';
|
|
12
15
|
checkedAt: number | null; error: string | null; canUpdate: boolean;
|
|
@@ -17,19 +20,36 @@ export interface UpdateStatus {
|
|
|
17
20
|
export interface ReviewSummary { state: string; round: number | null; counts: Record<string, number>; unsent?: number; comments?: number; files?: string[]; next?: string; job?: { phase: string; status: string; error?: string; details?: string[] } | null }
|
|
18
21
|
export interface PendingReview { route: string; body: Record<string, unknown>; previousStatus: TaskStatus }
|
|
19
22
|
export interface Task {
|
|
23
|
+
mode?: 'edit' | 'review';
|
|
24
|
+
aiReview?: { key: string; notes?: number; revision?: number };
|
|
20
25
|
// New tasks keep acceptance isolated until the author chooses a destination.
|
|
21
26
|
deferred?: boolean;
|
|
22
27
|
delivery?: { mode: 'workspace' | 'branch'; accepted: string; branch?: string; commit?: string; pending?: boolean } | null;
|
|
23
28
|
id: string; projectId: string; name: string; title: string; instructions: string;
|
|
24
29
|
titleSource?: 'prompt' | 'generated' | 'manual'; titleRevision?: number; titleGeneration?: 'pending' | 'failed' | null;
|
|
25
30
|
worktree: string; threadId: string | null; turnId: string | null; agent?: AgentId; model: string | null; effort?: string | null; serviceTier?: string;
|
|
31
|
+
// The agent's thread was started with Papilot's view_pdf tool.
|
|
32
|
+
viewPdf?: boolean;
|
|
26
33
|
status: TaskStatus; createdAt: number; updatedAt: number; feedbackCursor: number;
|
|
27
34
|
archivedAt?: number | null;
|
|
28
35
|
error: string | null; review: ReviewSummary | null;
|
|
29
36
|
pauseVersion?: number; pendingRunId?: string | null;
|
|
30
37
|
pendingReview?: PendingReview | null;
|
|
31
38
|
// The task's last commit from its review, and the accepted text it included.
|
|
32
|
-
committed?: { hash: string; accepted: string; at: number } | null;
|
|
39
|
+
committed?: { hash: string; accepted: string; at: number; pushed?: boolean } | null;
|
|
40
|
+
}
|
|
41
|
+
// A task the author is through with, which Papilot archives once they leave
|
|
42
|
+
// it: every change of its last result is decided, no comment or rejection
|
|
43
|
+
// waits to go back to the agent, and what was accepted has left the task
|
|
44
|
+
// (applied or committed; older tasks accept straight into the working
|
|
45
|
+
// directory). An answer without manuscript changes is finished too.
|
|
46
|
+
export function taskFinished(task: Task) {
|
|
47
|
+
const review = task.review;
|
|
48
|
+
if (task.status !== 'reviewing' || !review || task.pendingReview || task.delivery?.pending || review.counts?.pending || review.state === 'build_failed') return false;
|
|
49
|
+
const unsent = review.unsent ?? ['accepted', 'rejected', 'commented'].reduce((count, kind) => count + (review.counts?.[kind] || 0), 0);
|
|
50
|
+
if ((review.comments ?? unsent) > 0 || (unsent > 0 && !review.counts?.accepted)) return false;
|
|
51
|
+
if (review.round === null) return true;
|
|
52
|
+
return review.state === 'done' && (!task.deferred || review.files?.length === 0);
|
|
33
53
|
}
|
|
34
54
|
// reviewRound is 0 while awaiting a result, null when no changes were produced,
|
|
35
55
|
// or the published round. Older runs omit it.
|
|
@@ -42,21 +62,32 @@ export interface NoteAnchor { sources: { file: string; lines: [number, number] }
|
|
|
42
62
|
export interface NoteSummary { group?: NoteGroup; where: string; quote: string; comment: string; id?: string; anchor?: NoteAnchor }
|
|
43
63
|
export interface NoteChange { round: number; stop: number; units: string[]; state: 'pending' | 'accepted' | 'rejected' | 'commented' | 'mixed'; match: 'source' | 'quote' }
|
|
44
64
|
export interface NoteResult { key: string; itemId: string; index: number; note: NoteSummary; round: number | null; state: 'waiting' | 'unmatched' | 'matched'; changes: NoteChange[] }
|
|
45
|
-
export interface MessageMeta { attachments?: Attachment[]; skill?: { id: string; name: string }; notes?: NoteSummary[]; groupInstructions?: GroupInstructions; feedback?: FeedbackSummary; sendsFeedback?: boolean; build?: BuildFailure; author?: Author }
|
|
65
|
+
export interface MessageMeta { attachments?: Attachment[]; skill?: { id: string; name: string; icon?: SkillIcon }; notes?: NoteSummary[]; groupInstructions?: GroupInstructions; feedback?: FeedbackSummary; sendsFeedback?: boolean; build?: BuildFailure; author?: Author }
|
|
46
66
|
// Who wrote a message or note; none means the owner.
|
|
47
67
|
export interface Author { id: string; name: string }
|
|
48
68
|
// A failed review build as the engine reports it; log is the compiler log's path.
|
|
49
69
|
export interface BuildFailure { error: string; details: string[]; log?: string }
|
|
50
70
|
export interface FeedbackSummary { round: number | null; accepted: number; rejected: number; undone: number; comments: { where: string; text: string }[]; buildError?: boolean }
|
|
51
|
-
export interface Skill { id: string; name: string; description: string; hint: string; source: 'builtin' | 'user' | 'paper' }
|
|
71
|
+
export interface Skill { output?: 'annotations'; id: string; name: string; description: string; hint: string; icon?: SkillIcon; source: 'builtin' | 'user' | 'paper' }
|
|
52
72
|
export interface PendingRequest { id: string; taskId: string; method: string; params: Record<string, any>; createdAt: number }
|
|
53
|
-
export type AgentId = 'codex' | 'claude' | 'antigravity';
|
|
73
|
+
export type AgentId = 'codex' | 'claude' | 'antigravity' | 'kimi' | 'cursor' | 'opencode' | 'qwen' | 'glm' | 'deepseek';
|
|
74
|
+
// Every agent Papilot can drive, in menu order. Only the ones found on the host are offered.
|
|
75
|
+
export const AGENT_IDS: AgentId[] = ['codex', 'claude', 'cursor', 'antigravity', 'kimi', 'glm', 'deepseek', 'qwen', 'opencode'];
|
|
76
|
+
export const AGENT_NAMES: Record<AgentId, string> = { codex: 'Codex', claude: 'Claude', cursor: 'Cursor', antigravity: 'Antigravity', kimi: 'Kimi', glm: 'GLM', deepseek: 'DeepSeek', qwen: 'Qwen', opencode: 'OpenCode' };
|
|
77
|
+
export const isAgentId = (value: unknown): value is AgentId => AGENT_IDS.includes(value as AgentId);
|
|
54
78
|
export interface AgentModel { id: string; name: string; isDefault: boolean; efforts: string[]; defaultEffort: string | null; fastMode?: { serviceTier: string; description: string } }
|
|
55
79
|
// The account's included usage: rolling windows (minutes long, reset time in ms) and credits.
|
|
56
80
|
export interface UsageWindow { usedPercent: number; minutes: number | null; resetsAt: number | null }
|
|
57
81
|
export interface Usage { plan: string | null; windows: UsageWindow[]; credits: { unlimited: boolean; balance: number | null } | null; reached: boolean }
|
|
58
|
-
|
|
82
|
+
// installed is false when the agent was not found on the host (no CLI, SDK
|
|
83
|
+
// credentials or key); such agents are not offered. Older servers omit it.
|
|
84
|
+
export interface RuntimeStatus { connected: boolean; authenticated: boolean; account: string | null; error: string | null; models: AgentModel[]; usage?: Usage | null; installed?: boolean }
|
|
59
85
|
export interface DiagnosticCheck { id: string; ok: boolean; severity: 'error' | 'warn' | 'info'; detail: string; fix: string | null }
|
|
86
|
+
export interface LatexInstallation {
|
|
87
|
+
distribution: 'MacTeX' | 'BasicTeX' | 'TeX Live' | 'MiKTeX' | null;
|
|
88
|
+
version: string | null;
|
|
89
|
+
path: string;
|
|
90
|
+
}
|
|
60
91
|
export interface DiagnosticReport {
|
|
61
92
|
version: string; checkedAt: number; platform: string; arch: string; node: string;
|
|
62
93
|
ok: boolean; checks: DiagnosticCheck[];
|
|
@@ -84,11 +115,51 @@ export interface GitWorktree {
|
|
|
84
115
|
}
|
|
85
116
|
export interface GitGraphCommit { hash: string; parents: string[]; subject: string; author: string; date: number; refs: string[] }
|
|
86
117
|
export interface GitOverview { worktrees: GitWorktree[]; commits: GitGraphCommit[]; truncated: boolean }
|
|
87
|
-
export interface ManuscriptBuild { status: 'building' | 'ready' | 'failed'; key?: string; pages?: [number, number][]; builtAt?: number; error?: string; current: boolean }
|
|
88
|
-
|
|
118
|
+
export interface ManuscriptBuild { status: 'building' | 'ready' | 'failed'; key?: string; pages?: [number, number][]; builtAt?: number; error?: string; current: boolean; problems?: BuildProblemCounts }
|
|
119
|
+
// What the last finished build reported. Errors stop the build; warnings are
|
|
120
|
+
// mistakes in the PDF (?? and missing citations); minor problems are
|
|
121
|
+
// typesetting (overfull lines) and package remarks, listed but never counted.
|
|
122
|
+
export type BuildProblemKind = 'error' | 'missing-file' | 'reference' | 'citation' | 'label' | 'glyph' | 'overfull' | 'other';
|
|
123
|
+
export interface BuildProblem {
|
|
124
|
+
kind: BuildProblemKind; severity: 'error' | 'warning' | 'minor'; message: string;
|
|
125
|
+
key?: string; file?: string; line?: number; end?: number; amount?: number; count?: number;
|
|
126
|
+
page?: number; rect?: [number, number, number, number];
|
|
127
|
+
}
|
|
128
|
+
export interface BuildProblemCounts { errors: number; warnings: number; minor: number }
|
|
129
|
+
// A label defined twice is listed at each definition but counts as one problem.
|
|
130
|
+
export function countProblems(problems: Pick<BuildProblem, 'severity' | 'kind' | 'key'>[]): BuildProblemCounts {
|
|
131
|
+
const counts = { errors: 0, warnings: 0, minor: 0 }, labels = new Set<string>();
|
|
132
|
+
for (const problem of problems) {
|
|
133
|
+
if (problem.kind === 'label' && problem.key) { if (labels.has(problem.key)) continue; labels.add(problem.key); }
|
|
134
|
+
counts[problem.severity === 'error' ? 'errors' : problem.severity === 'warning' ? 'warnings' : 'minor']++;
|
|
135
|
+
}
|
|
136
|
+
return counts;
|
|
137
|
+
}
|
|
138
|
+
// `key` names the PDF whose pages the locations refer to.
|
|
139
|
+
export interface ManuscriptProblems { status: ManuscriptBuild['status']; key?: string; problems: BuildProblem[]; log: boolean }
|
|
140
|
+
export interface Annotation { aiReview?: { taskId: string; agent: AgentId; sourceId?: string; copiedTo?: string }; group?: NoteGroup; id: string; projectId: string; key: string; page: number; rect: [number, number, number, number]; rects?: [number, number, number, number][]; comment: string; quote: string; source: { file: string; lines: [number, number]; excerpt?: string } | null; createdAt: number; taskId?: string; author?: Author; deletedAt?: number }
|
|
141
|
+
// Review findings stay with their task; only explicit copies belong to the manuscript.
|
|
142
|
+
export const isTaskReviewNote = (note: Annotation) => Boolean(note.aiReview && !note.aiReview.sourceId);
|
|
89
143
|
export interface Manuscript { git: GitStatus; build: ManuscriptBuild; annotations: Annotation[] }
|
|
90
144
|
export interface PaperImport {
|
|
91
145
|
id: string; path: string; name: string; copied: boolean; candidates: string[]; main: string; engine: string;
|
|
92
146
|
phase: 'choose' | 'building' | 'failed' | 'ready' | 'repairing';
|
|
93
147
|
error?: string; summary?: string; versioned?: boolean;
|
|
94
148
|
}
|
|
149
|
+
// The paper's bibliography as the References panel reads it: the review
|
|
150
|
+
// engine's report of every entry, with online checks and the entries the
|
|
151
|
+
// author checked by hand.
|
|
152
|
+
export interface ReferenceCitation { file: string; line: number; section: string | null }
|
|
153
|
+
export interface ReferenceRecord { source: 'doi' | 'crossref' | 'arxiv' | 'dblp'; title: string; authors: string[]; year: number | null; venue: string | null; doi: string | null; url: string }
|
|
154
|
+
export interface ReferenceCheck {
|
|
155
|
+
status: 'verified' | 'mismatch' | 'unverified' | 'failed' | 'skipped';
|
|
156
|
+
record?: ReferenceRecord; via: 'doi' | 'arxiv' | 'title' | null;
|
|
157
|
+
differences: { field: 'title' | 'author' | 'year' | 'doi' | 'arxiv'; bib: string; record: string; minor?: boolean }[];
|
|
158
|
+
reason?: 'doi-not-found' | 'arxiv-not-found' | 'no-match' | 'nothing-to-check'; unavailable: string[]; checked: number;
|
|
159
|
+
}
|
|
160
|
+
export interface BibliographyEntry { id: string; file: string; key: string; type: string; fields: { name: string; value: string; resolved?: string }[]; lines: [number, number]; cited: ReferenceCitation[]; error?: string }
|
|
161
|
+
export interface BibliographyReport {
|
|
162
|
+
bibs: string[]; absent: string[]; entries: BibliographyEntry[];
|
|
163
|
+
missing: (ReferenceCitation & { key: string })[]; duplicates: string[][]; similar: { ids: string[]; by: 'doi' | 'title' }[]; all_cited: boolean;
|
|
164
|
+
checks: Record<string, ReferenceCheck>; pending: number; confirmed: Record<string, number>;
|
|
165
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Shared by the file parser and every surface that displays a skill.
|
|
2
|
+
export const SKILL_ICONS = [
|
|
3
|
+
{ id: 'spark', label: 'Sparkles' }, { id: 'pen', label: 'Pen' },
|
|
4
|
+
{ id: 'spellcheck', label: 'Spelling' }, { id: 'compress', label: 'Shorten' },
|
|
5
|
+
{ id: 'sigma', label: 'Math' }, { id: 'checklist', label: 'Checklist' },
|
|
6
|
+
{ id: 'notes', label: 'Comments' }, { id: 'book', label: 'Book' },
|
|
7
|
+
{ id: 'search', label: 'Search' }, { id: 'code', label: 'Code' },
|
|
8
|
+
{ id: 'tool', label: 'Tools' }, { id: 'chart', label: 'Chart' },
|
|
9
|
+
{ id: 'globe', label: 'Globe' }, { id: 'lightbulb', label: 'Idea' },
|
|
10
|
+
{ id: 'link', label: 'Link' }, { id: 'branch', label: 'Branch' },
|
|
11
|
+
] as const;
|
|
12
|
+
export type SkillIcon = typeof SKILL_ICONS[number]['id'];
|
|
13
|
+
export const isSkillIcon = (value: unknown): value is SkillIcon => SKILL_ICONS.some(icon => icon.id === value);
|
|
14
|
+
const defaults: Record<string, SkillIcon> = {
|
|
15
|
+
polish: 'spark', proofread: 'spellcheck', shorten: 'compress', 'check-math': 'sigma',
|
|
16
|
+
consistency: 'checklist', 'latex-warnings': 'tool', 'reviewer-response': 'notes', 'ai-review': 'notes',
|
|
17
|
+
};
|
|
18
|
+
export const resolveSkillIcon = (skill: { id: string; icon?: string }): SkillIcon => isSkillIcon(skill.icon) ? skill.icon : Object.hasOwn(defaults, skill.id) ? defaults[skill.id] : 'spark';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Keep a skill's source intact when editing one field in the visual editor.
|
|
2
|
+
// This is the same small, single-line front-matter format used by the loader.
|
|
3
|
+
function parts(content: string) {
|
|
4
|
+
const bom = content.startsWith('\uFEFF') ? '\uFEFF' : '';
|
|
5
|
+
const source = content.slice(bom.length);
|
|
6
|
+
const header = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(source);
|
|
7
|
+
return { bom, source, header, newline: source.includes('\r\n') ? '\r\n' : '\n' };
|
|
8
|
+
}
|
|
9
|
+
export function skillMetadata(content: string): Record<string, string> {
|
|
10
|
+
const { header } = parts(content), fields: Record<string, string> = {};
|
|
11
|
+
for (const line of header?.[1].split(/\r?\n/) || []) {
|
|
12
|
+
const field = /^([A-Za-z-]+):\s*(.*)$/.exec(line);
|
|
13
|
+
if (field) {
|
|
14
|
+
const value = field[2].trim();
|
|
15
|
+
let decoded = value.replace(/^(['"])(.*)\1$/, '$2');
|
|
16
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
17
|
+
try { decoded = JSON.parse(value); } catch { /* Preserve legacy quoted values. */ }
|
|
18
|
+
}
|
|
19
|
+
fields[field[1].toLowerCase()] = decoded;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return fields;
|
|
23
|
+
}
|
|
24
|
+
export const skillInstructions = (content: string) => {
|
|
25
|
+
const { source, header } = parts(content);
|
|
26
|
+
return source.slice(header?.[0].length || 0);
|
|
27
|
+
};
|
|
28
|
+
export function setSkillMetadata(content: string, field: 'name' | 'description' | 'icon', value: string) {
|
|
29
|
+
const { bom, source, header, newline } = parts(content);
|
|
30
|
+
const clean = value.replace(/[\r\n]/g, ' ');
|
|
31
|
+
const line = `${field}: ${field === 'icon' ? clean : JSON.stringify(clean)}`;
|
|
32
|
+
if (!header) return `${bom}---${newline}${line}${newline}---${newline}${newline}${source}`;
|
|
33
|
+
const rows = header[1].split(/\r?\n/);
|
|
34
|
+
const matches = new RegExp(`^${field}:`, 'i');
|
|
35
|
+
// Remove duplicate definitions so the visual value cannot be overridden later.
|
|
36
|
+
const index = rows.findIndex(row => matches.test(row));
|
|
37
|
+
const next = rows.filter(row => !matches.test(row));
|
|
38
|
+
next.splice(index < 0 ? next.length : index, 0, line);
|
|
39
|
+
const closingNewline = header[0].endsWith('\n') ? newline : '';
|
|
40
|
+
return `${bom}---${newline}${next.join(newline)}${newline}---${closingNewline}${source.slice(header[0].length)}`;
|
|
41
|
+
}
|
|
42
|
+
export function setSkillInstructions(content: string, body: string) {
|
|
43
|
+
const { bom, source, header, newline } = parts(content);
|
|
44
|
+
return header ? `${bom}${header[0]}${header[0].endsWith('\n') ? '' : newline}${body}` : `${bom}${body}`;
|
|
45
|
+
}
|