papilot 0.1.1 → 0.3.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 +16 -4
- package/README.zh-CN.md +15 -3
- package/bin/doctor.mjs +239 -0
- package/bin/papilot.mjs +95 -86
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/index-BkowXmKr.js +206 -0
- package/dist/assets/index-Dq3s-vhH.css +1 -0
- package/dist/assets/math-BTm2hvOz.js +263 -0
- package/dist/assets/math-DEcVZfaU.css +1 -0
- package/dist/assets/{pdf-view-DJo0X38w.js → pdf-view-BEn1EztO.js} +18 -18
- package/dist/index.html +11 -2
- package/examples/papilot-paper/preamble.tex +17 -11
- package/package.json +13 -2
- package/scripts/service.ts +8 -2
- package/skills/check-math.md +12 -0
- package/skills/consistency.md +11 -0
- package/skills/latex-warnings.md +12 -0
- package/skills/polish.md +13 -0
- package/skills/proofread.md +12 -0
- package/skills/reviewer-response.md +11 -0
- package/skills/shorten.md +12 -0
- package/src/commit.ts +23 -0
- package/src/conversation.ts +48 -0
- package/src/notes.ts +5 -0
- package/src/server/agents.ts +4 -3
- package/src/server/main.ts +51 -8
- package/src/server/manuscript.ts +91 -10
- package/src/server/review.ts +2 -1
- package/src/server/service-environment.ts +22 -0
- package/src/server/skills.ts +57 -0
- package/src/server/workspace.ts +186 -26
- package/src/shared.ts +17 -3
- package/vendor/latex-review/dist/web/assets/index-Bg-VqnPa.css +1 -0
- package/vendor/latex-review/dist/web/assets/index-xLeqYBJt.js +19 -0
- package/vendor/latex-review/dist/web/index.html +2 -2
- package/vendor/latex-review/src/client.ts +10 -3
- package/vendor/latex-review/src/core/build.ts +53 -7
- package/vendor/latex-review/src/core/outline.ts +6 -3
- package/vendor/latex-review/src/core/publish.ts +8 -6
- package/vendor/latex-review/src/runtime.ts +14 -0
- package/vendor/latex-review/src/server.ts +2 -1
- package/vendor/latex-review/src/types.ts +16 -1
- package/vendor/latex-review/src/web/ChangeMap.tsx +21 -9
- package/vendor/latex-review/src/web/PdfStrip.tsx +5 -1
- package/vendor/latex-review/src/web/i18n.ts +48 -0
- package/vendor/latex-review/src/web/model.ts +23 -4
- package/vendor/latex-review/src/web/overview.tsx +25 -20
- package/vendor/latex-review/src/web/review.tsx +399 -145
- package/vendor/latex-review/src/web/style.css +130 -0
- package/vendor/latex-review/src/web/zh-CN.ts +188 -0
- package/dist/assets/index-BhQFCMTx.js +0 -131
- package/dist/assets/index-CAV3-87U.css +0 -1
- package/vendor/latex-review/dist/web/assets/index-DyzYnIGk.css +0 -1
- package/vendor/latex-review/dist/web/assets/index-HxBK-dls.js +0 -19
package/README.md
CHANGED
|
@@ -53,17 +53,17 @@ Each edit comes back as a typeset diff. Accept, reject or comment on it, and und
|
|
|
53
53
|
<td width="50%" valign="top">
|
|
54
54
|
<img src="docs/assets/feature-contents.png" alt="The PDF viewer with its contents menu open">
|
|
55
55
|
<h3>📖 Read it like a real PDF</h3>
|
|
56
|
-
Jump
|
|
56
|
+
Jump to any section from the contents, search every page and zoom in sharply. Selecting text picks exactly the printed words.
|
|
57
57
|
</td>
|
|
58
58
|
<td width="50%" valign="top">
|
|
59
59
|
<img src="docs/assets/feature-git.png" alt="The Git panel with a diff and a commit message">
|
|
60
60
|
<h3>🌿 Commit when you're happy</h3>
|
|
61
|
-
Diff files
|
|
61
|
+
Diff files and commit the ones you choose, with a commit message an agent writes. Pull and push, or merge a coauthor's changes and let an agent resolve the conflicts.
|
|
62
62
|
</td>
|
|
63
63
|
</tr>
|
|
64
64
|
</table>
|
|
65
65
|
|
|
66
|
-
Also: 🤖 **Codex, Claude or Antigravity**, each with your own login · 🌳 **every task in its own Git worktree**, up to two at once · 📱 **runs on your computer**, and you can open it from your phone.
|
|
66
|
+
Also: ✨ **skills** such as Polish, Proofread and Check math, one `/` away, plus your own · 🤖 **Codex, Claude or Antigravity**, each with your own login · 🌳 **every task in its own Git worktree**, up to two at once · 🌗 **light and dark** · 📱 **runs on your computer**, and you can open it from your phone.
|
|
67
67
|
|
|
68
68
|
## 🚀 Quick start
|
|
69
69
|
|
|
@@ -87,6 +87,8 @@ Open **http://127.0.0.1:7274** and choose **PDF** in the sidebar. The demo adds
|
|
|
87
87
|
|
|
88
88
|
Papilot never reads, stores or forwards your credentials. An agent that isn't installed or signed in shows as unavailable.
|
|
89
89
|
|
|
90
|
+
Having trouble getting started? Run `papilot doctor` for environment checks and fixes, or `papilot doctor --repo /path/to/paper` to check a paper too. Add `--json` for a machine-readable report. Diagnostics do not install software, start the server or change your paper. See the [diagnostics guide](docs/guide.md#diagnostics) for what is checked.
|
|
91
|
+
|
|
90
92
|
<details>
|
|
91
93
|
<summary><b>⚙️ Options and remote access</b></summary>
|
|
92
94
|
|
|
@@ -120,7 +122,17 @@ Run `npm run typecheck`, `npm test` and `npm run test:ui` before sending a pull
|
|
|
120
122
|
|
|
121
123
|
## 📚 Documentation
|
|
122
124
|
|
|
123
|
-
The **[guide](docs/guide.md)** covers every part of the workspace, the configuration variables, recovery after restarts, and development. The example paper in [`examples/papilot-paper`](examples/papilot-paper) explains the design in depth, with proofs.
|
|
125
|
+
The **[guide](docs/guide.md)** covers every part of the workspace, the configuration variables, recovery after restarts, and development. The example paper in [`examples/papilot-paper`](examples/papilot-paper) explains the design in depth, with proofs. What changed in each release is in the [changelog](CHANGELOG.md).
|
|
126
|
+
|
|
127
|
+
## 💬 Community
|
|
128
|
+
|
|
129
|
+
- 💡 **Questions and ideas**: [Discussions](https://github.com/cu1ch3n/papilot/discussions)
|
|
130
|
+
- 🐛 **Something broke**: [open an issue](https://github.com/cu1ch3n/papilot/issues/new/choose)
|
|
131
|
+
- 🤝 **Want to help**: pick a [good first issue](https://github.com/cu1ch3n/papilot/labels/good%20first%20issue) and read [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
132
|
+
|
|
133
|
+
If Papilot saves you time on a paper, a ⭐ helps other researchers find it.
|
|
134
|
+
|
|
135
|
+
<a href="https://github.com/cu1ch3n/papilot/graphs/contributors"><img src="https://contrib.rocks/image?repo=cu1ch3n/papilot" alt="Papilot contributors"></a>
|
|
124
136
|
|
|
125
137
|
## 📄 License
|
|
126
138
|
|
package/README.zh-CN.md
CHANGED
|
@@ -58,12 +58,12 @@
|
|
|
58
58
|
<td width="50%" valign="top">
|
|
59
59
|
<img src="docs/assets/feature-git.png" alt="带差异和提交信息的 Git 面板">
|
|
60
60
|
<h3>🌿 满意了再提交</h3>
|
|
61
|
-
|
|
61
|
+
查看差异,只提交你勾选的文件,提交信息可以让 agent 来写。可以 pull、push,也可以合并合作者的改动,冲突交给 agent 解决。
|
|
62
62
|
</td>
|
|
63
63
|
</tr>
|
|
64
64
|
</table>
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
此外:✨ **技能(skill)**,如润色、校对、检查数学,输入 `/` 即可选用,也能添加你自己的 · 🤖 **Codex、Claude 或 Antigravity**,各用你自己的登录 · 🌳 **每个任务都有独立的 Git 工作树**,最多两个同时运行 · 🌗 **浅色与深色** · 📱 **运行在你的电脑上**,也能用手机打开。
|
|
67
67
|
|
|
68
68
|
## 🚀 快速开始
|
|
69
69
|
|
|
@@ -87,6 +87,8 @@ papilot demo
|
|
|
87
87
|
|
|
88
88
|
Papilot 从不读取、保存或转发你的凭据。没有安装或没有登录的智能体会显示为不可用。
|
|
89
89
|
|
|
90
|
+
启动遇到问题?运行 `papilot doctor` 检查环境并查看修复建议,或用 `papilot doctor --repo /path/to/paper` 一并检查论文。加上 `--json` 可输出机器可读的报告。诊断不会安装软件、启动服务或修改论文。检查范围见[诊断指南](docs/guide.md#diagnostics)。
|
|
91
|
+
|
|
90
92
|
<details>
|
|
91
93
|
<summary><b>⚙️ 选项与远程访问</b></summary>
|
|
92
94
|
|
|
@@ -120,7 +122,17 @@ npm run build && npm start
|
|
|
120
122
|
|
|
121
123
|
## 📚 文档
|
|
122
124
|
|
|
123
|
-
**[指南](docs/guide.md)**(英文)介绍工作台的每个部分、配置变量、重启后的恢复以及开发流程。[`examples/papilot-paper`](examples/papilot-paper)
|
|
125
|
+
**[指南](docs/guide.md)**(英文)介绍工作台的每个部分、配置变量、重启后的恢复以及开发流程。[`examples/papilot-paper`](examples/papilot-paper) 中的示例论文深入讲解了设计,并附有证明。每个版本的变化见[更新日志](CHANGELOG.md)。
|
|
126
|
+
|
|
127
|
+
## 💬 社区
|
|
128
|
+
|
|
129
|
+
- 💡 **提问和想法**:[Discussions](https://github.com/cu1ch3n/papilot/discussions)
|
|
130
|
+
- 🐛 **遇到问题**:[提交 issue](https://github.com/cu1ch3n/papilot/issues/new/choose)
|
|
131
|
+
- 🤝 **想参与**:从 [good first issue](https://github.com/cu1ch3n/papilot/labels/good%20first%20issue) 开始,并阅读 [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
132
|
+
|
|
133
|
+
如果 Papilot 帮你省下了改论文的时间,点个 ⭐ 能让更多研究者发现它。
|
|
134
|
+
|
|
135
|
+
<a href="https://github.com/cu1ch3n/papilot/graphs/contributors"><img src="https://contrib.rocks/image?repo=cu1ch3n/papilot" alt="Papilot 贡献者"></a>
|
|
124
136
|
|
|
125
137
|
## 📄 许可证
|
|
126
138
|
|
package/bin/doctor.mjs
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// Deliberately uses only Node built-ins: diagnostics must work before npm setup.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
11
|
+
const help = `Usage: papilot doctor [--repo <folder>] [--json] [--data <folder>] [--port <number>]
|
|
12
|
+
|
|
13
|
+
--repo <folder> Also check a LaTeX Git repository and its root document
|
|
14
|
+
--json Print one JSON report, suitable for scripts
|
|
15
|
+
--data <folder> Check this data folder (default: PAPILOT_DATA or ~/.papilot)
|
|
16
|
+
--port <number> Check this local server port (default: PAPILOT_PORT or 7274)
|
|
17
|
+
--help Show this help
|
|
18
|
+
|
|
19
|
+
Checks the current environment and, on macOS, installed service tool paths.
|
|
20
|
+
Does not install software, start Papilot, compile papers or send model prompts.
|
|
21
|
+
Exit codes: 0 = no blocking errors (warnings may remain), 1 = blocking errors,
|
|
22
|
+
2 = invalid command-line options. Agent configuration is not a live API test.`;
|
|
23
|
+
|
|
24
|
+
const expand = value => path.resolve(value.startsWith('~/') ? path.join(os.homedir(), value.slice(2)) : value);
|
|
25
|
+
const supportedNode = version => { const [major, minor] = version.split('.').map(Number); return major > 22 || (major === 22 && minor >= 22); };
|
|
26
|
+
const atLeast = (version, minimum) => {
|
|
27
|
+
const a = version.split('.').map(Number), b = minimum.split('.').map(Number);
|
|
28
|
+
return (a[0] - b[0] || a[1] - b[1] || a[2] - b[2]) >= 0;
|
|
29
|
+
};
|
|
30
|
+
function executable(command, env = process.env) {
|
|
31
|
+
const candidates = command.includes('/') || command.includes('\\') ? [path.resolve(command)]
|
|
32
|
+
: (env.PATH || '').split(path.delimiter).map(dir => path.resolve(dir || '.', command));
|
|
33
|
+
return candidates.find(file => {
|
|
34
|
+
try { fs.accessSync(file, fs.constants.X_OK); return fs.statSync(file).isFile(); } catch { return false; }
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function command(file, args, options = {}) {
|
|
38
|
+
return new Promise(resolve => {
|
|
39
|
+
// Never return raw stderr: login commands can print credentials or account data.
|
|
40
|
+
const child = execFile(file, args, { cwd: os.homedir(), timeout: 5000, killSignal: 'SIGKILL', maxBuffer: 128 * 1024, ...options }, (error, stdout, stderr) => {
|
|
41
|
+
resolve({ ok: !error, stdout, stderr, reason: error?.killed ? 'timed out' : error ? `failed (${error.code || 'unknown error'})` : '' });
|
|
42
|
+
});
|
|
43
|
+
child.stdin?.end();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function writableDirectory(target) {
|
|
47
|
+
let existing = target;
|
|
48
|
+
while (!fs.existsSync(existing) && path.dirname(existing) !== existing) existing = path.dirname(existing);
|
|
49
|
+
try {
|
|
50
|
+
if (!fs.statSync(existing).isDirectory()) return false;
|
|
51
|
+
fs.accessSync(existing, fs.constants.W_OK | fs.constants.X_OK);
|
|
52
|
+
return true;
|
|
53
|
+
} catch { return false; }
|
|
54
|
+
}
|
|
55
|
+
function installedModule(name, directory = root) {
|
|
56
|
+
try { return createRequire(path.join(directory, 'package.json')).resolve(name); } catch { return null; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function doctor(args) {
|
|
60
|
+
if (args.includes('--help') || args.includes('-h')) { console.log(help); return 0; }
|
|
61
|
+
const json = args.includes('--json');
|
|
62
|
+
let data = expand(process.env.PAPILOT_DATA || path.join(os.homedir(), '.papilot'));
|
|
63
|
+
let port = Number(process.env.PAPILOT_PORT || 7274), repo = null;
|
|
64
|
+
try {
|
|
65
|
+
for (let i = 0; i < args.length; i++) {
|
|
66
|
+
const arg = args[i];
|
|
67
|
+
if (arg === '--json') continue;
|
|
68
|
+
if (!['--data', '--port', '--repo'].includes(arg)) throw new Error(`Unknown option: ${arg}`);
|
|
69
|
+
const value = args[++i];
|
|
70
|
+
if (!value || value.startsWith('--')) throw new Error(`${arg} needs a value.`);
|
|
71
|
+
if (arg === '--data') data = expand(value);
|
|
72
|
+
if (arg === '--repo') repo = expand(value);
|
|
73
|
+
if (arg === '--port') port = Number(value);
|
|
74
|
+
}
|
|
75
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('--port or PAPILOT_PORT must be an integer from 1 to 65535.');
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (json) console.log(JSON.stringify({ name: 'Papilot', version: pkg.version, ok: false, checks: [], error: error.message }));
|
|
78
|
+
else console.error(`${error.message}\n\n${help}`);
|
|
79
|
+
return 2;
|
|
80
|
+
}
|
|
81
|
+
const checks = [];
|
|
82
|
+
// Redact known secret values even when they appear in a path or configuration error.
|
|
83
|
+
const secrets = Object.entries(process.env).filter(([key, value]) => /KEY|TOKEN|SECRET|PASSWORD/i.test(key) && value?.length >= 4).map(([, value]) => value);
|
|
84
|
+
const clean = value => secrets.reduce((text, secret) => text.split(secret).join('[redacted]'), String(value))
|
|
85
|
+
.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').replace(/[\x00-\x08\x0b-\x1f\x7f]/g, '');
|
|
86
|
+
const check = (id, ok, severity, detail, fix = null) => checks.push({ id, ok: Boolean(ok), severity, detail: clean(detail), fix: ok || !fix ? null : clean(fix) });
|
|
87
|
+
const engine = expand(process.env.PAPILOT_REVIEW_ENGINE || path.join(root, 'vendor/latex-review'));
|
|
88
|
+
const sourceCheckout = fs.existsSync(path.join(root, '.git'));
|
|
89
|
+
const repair = sourceCheckout ? 'Run npm ci in the Papilot checkout.' : 'Reinstall Papilot with npm install -g papilot.';
|
|
90
|
+
|
|
91
|
+
check('node', supportedNode(process.versions.node), 'error', `Node.js ${process.versions.node} (${process.execPath})`, 'Install Node.js 22.22 or newer, then run papilot doctor again.');
|
|
92
|
+
const missing = Object.keys(pkg.dependencies).filter(name => !installedModule(name));
|
|
93
|
+
check('dependencies', !missing.length, 'error', missing.length ? `Missing packages: ${missing.join(', ')}.` : 'Runtime packages are installed.', repair);
|
|
94
|
+
const frontend = fs.existsSync(path.join(root, 'dist/index.html'));
|
|
95
|
+
check('frontend', frontend, 'error', frontend ? 'Papilot frontend build is present.' : 'Papilot frontend build is missing: dist/index.html.', sourceCheckout ? 'Run npm run build.' : repair);
|
|
96
|
+
const missingEngine = ['src/client.ts', 'src/core/config.ts', 'dist/web/index.html'].filter(file => !fs.existsSync(path.join(engine, file)));
|
|
97
|
+
check('review-engine', !missingEngine.length, 'error', `Review engine: ${engine}${missingEngine.length ? `; missing ${missingEngine.join(', ')}` : ''}`, process.env.PAPILOT_REVIEW_ENGINE ? 'Check PAPILOT_REVIEW_ENGINE and build that review engine.' : sourceCheckout ? 'Run npm run setup:review.' : repair);
|
|
98
|
+
check('data', writableDirectory(data), 'error', `Data folder: ${data}${fs.existsSync(data) ? '' : ' (will be created on startup)'}`, 'Choose a writable directory with --data <folder>.');
|
|
99
|
+
|
|
100
|
+
async function tool(name, args, fix, id = name, env = process.env, severity = 'error') {
|
|
101
|
+
const file = executable(name, env);
|
|
102
|
+
if (!file) { check(id, false, severity, `${name} was not found on PATH.`, fix); return null; }
|
|
103
|
+
const result = await command(file, args, { env });
|
|
104
|
+
check(id, result.ok, severity, `${file}${result.ok ? '' : `: ${result.reason}`}`, fix);
|
|
105
|
+
return result.ok ? file : null;
|
|
106
|
+
}
|
|
107
|
+
const [git] = await Promise.all([
|
|
108
|
+
tool('git', ['--version'], 'Install Git and open a new terminal.'),
|
|
109
|
+
tool('latexmk', ['-v'], 'Install a full TeX distribution (MacTeX or TeX Live), and add its binaries to PATH.'),
|
|
110
|
+
tool('synctex', ['--version'], 'Install SyncTeX from your TeX distribution and add its binaries to PATH.'),
|
|
111
|
+
]);
|
|
112
|
+
const texEngines = await Promise.all(['pdflatex', 'xelatex', 'lualatex'].map(async name => {
|
|
113
|
+
const file = executable(name);
|
|
114
|
+
return file && (await command(file, ['--version'])).ok ? name : null;
|
|
115
|
+
}));
|
|
116
|
+
const availableEngines = texEngines.filter(Boolean);
|
|
117
|
+
check('tex-engines', availableEngines.length > 0, 'error', availableEngines.length ? `TeX engines: ${availableEngines.join(', ')}.` : 'No working pdflatex, xelatex or lualatex was found.', 'Install a full TeX distribution and add its binaries to PATH.');
|
|
118
|
+
|
|
119
|
+
async function codexCheck() {
|
|
120
|
+
const minimum = pkg.devDependencies['@openai/codex'];
|
|
121
|
+
const configured = process.env.PAPILOT_CODEX_BIN;
|
|
122
|
+
const candidates = configured ? [configured] : ['codex', path.join(data, 'agents/node_modules/.bin/codex')];
|
|
123
|
+
let found = false;
|
|
124
|
+
for (const candidate of candidates) {
|
|
125
|
+
const file = executable(candidate);
|
|
126
|
+
if (!file) continue;
|
|
127
|
+
found = true;
|
|
128
|
+
const result = await command(file, ['--version']);
|
|
129
|
+
const version = result.stdout.match(/\d+\.\d+\.\d+/)?.[0];
|
|
130
|
+
if (!result.ok || !version || !atLeast(version, minimum)) {
|
|
131
|
+
if (!configured) continue;
|
|
132
|
+
check('agent.codex', false, 'warn', `Configured Codex ${result.ok ? 'is older than the supported version or its version is unknown' : result.reason}.`, `Set PAPILOT_CODEX_BIN to Codex ${minimum} or newer.`);
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
const login = await command(file, ['login', 'status']);
|
|
136
|
+
const ready = login.ok && /^\s*logged in\b/im.test(login.stdout + '\n' + login.stderr);
|
|
137
|
+
check('agent.codex', ready, 'warn', `Codex ${version} (${file}): ${ready ? 'signed in' : login.reason || 'login could not be confirmed'}.`, 'Run papilot codex login, then rerun doctor.');
|
|
138
|
+
return ready;
|
|
139
|
+
}
|
|
140
|
+
check('agent.codex', false, 'warn', configured ? 'PAPILOT_CODEX_BIN does not point to an executable.' : found ? `No installed Codex meets version ${minimum}.` : 'Codex is not installed.', configured ? 'Correct PAPILOT_CODEX_BIN or unset it to use automatic installation.' : 'Run papilot codex login; it installs the supported Codex when needed.');
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
async function claudeCheck() {
|
|
144
|
+
const sdk = installedModule('@anthropic-ai/claude-agent-sdk') || installedModule('@anthropic-ai/claude-agent-sdk', path.join(data, 'agents'));
|
|
145
|
+
const configured = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX', 'CLAUDE_CODE_USE_FOUNDRY'].some(key => Boolean(process.env[key]));
|
|
146
|
+
check('agent.claude', Boolean(sdk && configured), 'warn', !configured ? 'Claude API credentials are not configured in this environment; SDK-managed credentials were not checked.' : sdk ? 'Claude SDK installed; API credentials configured (not validated).' : 'Claude API credentials configured; the Agent SDK is not installed yet.', !configured ? 'Start Papilot with ANTHROPIC_API_KEY or a supported cloud provider configured. A Claude subscription alone cannot be used by this adapter.' : 'Start papilot once to install the Claude Agent SDK.');
|
|
147
|
+
return Boolean(sdk && configured);
|
|
148
|
+
}
|
|
149
|
+
async function agyCheck() {
|
|
150
|
+
const file = executable(process.env.PAPILOT_AGY_BIN || 'agy');
|
|
151
|
+
if (!file) { check('agent.antigravity', false, 'warn', 'Antigravity CLI is not installed or PAPILOT_AGY_BIN is invalid.', 'Install Antigravity and sign in to agy, or choose another agent.'); return false; }
|
|
152
|
+
const proxy = process.env.PAPILOT_AGY_PROXY;
|
|
153
|
+
const env = proxy ? { ...process.env, HTTPS_PROXY: proxy, HTTP_PROXY: proxy, https_proxy: proxy, http_proxy: proxy, NO_PROXY: '127.0.0.1,localhost' } : process.env;
|
|
154
|
+
const result = await command(file, ['models'], { env });
|
|
155
|
+
const ready = result.ok && result.stdout.split('\n').some(line => /^\S+\t\S/.test(line));
|
|
156
|
+
check('agent.antigravity', ready, 'warn', `${file}: ${ready ? 'model listing succeeded (no generation tested)' : result.reason || 'no models returned'}.`, 'Sign in to agy and try agy models. If your region requires a proxy, set PAPILOT_AGY_PROXY.');
|
|
157
|
+
return ready;
|
|
158
|
+
}
|
|
159
|
+
const agents = await Promise.all([codexCheck(), claudeCheck(), agyCheck()]);
|
|
160
|
+
check('agents', agents.some(Boolean), 'warn', agents.some(Boolean) ? 'At least one agent passed its configuration check; live editing was not tested.' : 'No agent passed its configuration check. PDF reading can still be used with a working TeX installation.', 'Configure one agent before starting editing tasks.');
|
|
161
|
+
|
|
162
|
+
if (repo) {
|
|
163
|
+
if (!fs.existsSync(repo) || !fs.statSync(repo).isDirectory()) check('project', false, 'error', `Paper folder does not exist: ${repo}`, 'Pass the Git repository root with --repo <folder>.');
|
|
164
|
+
else if (git) {
|
|
165
|
+
const top = await command(git, ['-C', repo, 'rev-parse', '--show-toplevel']);
|
|
166
|
+
let atRoot = false;
|
|
167
|
+
try { atRoot = top.ok && fs.realpathSync(top.stdout.trim()) === fs.realpathSync(repo); } catch { /* Missing repository. */ }
|
|
168
|
+
check('project.git', atRoot, 'error', atRoot ? `Git repository: ${repo}` : 'The paper folder is not a Git repository root.', 'Pass the repository root; initialize Git first if the paper is not yet versioned.');
|
|
169
|
+
if (atRoot) {
|
|
170
|
+
const head = await command(git, ['-C', repo, 'rev-parse', '--verify', 'HEAD']);
|
|
171
|
+
check('project.head', head.ok, 'error', head.ok ? 'The repository has an initial commit.' : 'The repository has no initial commit.', 'Create the first Git commit before adding this paper to Papilot.');
|
|
172
|
+
try {
|
|
173
|
+
const { register } = await import('tsx/esm/api');
|
|
174
|
+
const unregister = register();
|
|
175
|
+
let project;
|
|
176
|
+
try { project = (await import(pathToFileURL(path.join(engine, 'src/core/config.ts')).href)).loadProject(repo); }
|
|
177
|
+
finally { unregister(); }
|
|
178
|
+
check('project.main', true, 'error', `${project.main} (${project.engine}); configuration loaded without compiling.`);
|
|
179
|
+
await tool(project.engine, ['--version'], `Install ${project.engine} through your TeX distribution.`, 'project.engine');
|
|
180
|
+
check('project.worktrees', writableDirectory(project.worktrees), 'error', `Task worktrees: ${project.worktrees}`, 'Choose a writable worktrees directory outside the repository in .latex-review.json.');
|
|
181
|
+
} catch (error) {
|
|
182
|
+
check('project.main', false, 'error', `Could not load the paper configuration: ${error.message}`, 'Check .latex-review.json, its main/source_dir settings, and the review-engine/dependencies checks above.');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} else check('project', true, 'info', 'No paper selected. Use --repo <folder> to check its Git history, main document and TeX engine.');
|
|
187
|
+
|
|
188
|
+
const origin = process.env.PAPILOT_PUBLIC_ORIGIN || '', owner = process.env.PAPILOT_TAILSCALE_LOGIN || '';
|
|
189
|
+
let remoteValid = !origin && !owner;
|
|
190
|
+
if (origin && owner) {
|
|
191
|
+
try { const url = new URL(origin); remoteValid = ['http:', 'https:'].includes(url.protocol) && url.origin === origin && !url.username && !url.password; } catch { /* Invalid origin. */ }
|
|
192
|
+
}
|
|
193
|
+
check('remote', remoteValid, 'error', origin || owner ? 'Tailscale origin and owner configuration.' : 'Local access only; remote access is optional.', 'Set both PAPILOT_PUBLIC_ORIGIN (scheme and host, optional port, no path) and PAPILOT_TAILSCALE_LOGIN, or unset both for local access.');
|
|
194
|
+
try {
|
|
195
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/health`, { signal: AbortSignal.timeout(1500), redirect: 'error' });
|
|
196
|
+
const health = await response.json();
|
|
197
|
+
const ok = response.ok && health?.ok === true && health?.name === 'Papilot';
|
|
198
|
+
check('server', ok, 'warn', ok ? `Papilot is responding on port ${port}.` : `Port ${port} is not serving a healthy Papilot instance.`, 'Check the running service or choose another --port.');
|
|
199
|
+
} catch (error) {
|
|
200
|
+
const offline = error.cause?.code === 'ECONNREFUSED';
|
|
201
|
+
check('server', offline, offline ? 'info' : 'warn', offline ? `Papilot is not running on port ${port}; this is fine before startup.` : `Could not verify Papilot on port ${port}.`, 'Check the running service or choose another --port.');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (process.platform === 'darwin') {
|
|
205
|
+
const plist = path.join(os.homedir(), 'Library/LaunchAgents/local.papilot.plist');
|
|
206
|
+
if (!fs.existsSync(plist)) check('service', true, 'info', 'macOS background service is not installed (optional).');
|
|
207
|
+
else {
|
|
208
|
+
const result = await command('/usr/bin/plutil', ['-convert', 'json', '-o', '-', plist]);
|
|
209
|
+
try {
|
|
210
|
+
if (!result.ok) throw new Error('Unreadable service configuration.');
|
|
211
|
+
const settings = JSON.parse(result.stdout), env = settings.EnvironmentVariables || {};
|
|
212
|
+
const tools = ['git', 'latexmk', 'synctex', ...availableEngines];
|
|
213
|
+
const missing = tools.filter(name => !executable(name, env));
|
|
214
|
+
const different = tools.filter(name => executable(name, env) !== executable(name));
|
|
215
|
+
const node = settings.ProgramArguments?.[2];
|
|
216
|
+
const validNode = typeof node === 'string' && executable(node, env);
|
|
217
|
+
check('service.tools', !missing.length && !different.length && validNode, 'warn', !validNode ? 'The service Node executable is missing.' : missing.length ? `The saved service PATH is missing: ${missing.join(', ')}.` : different.length ? `Service and terminal resolve different tools: ${different.join(', ')}.` : 'The saved service paths match the terminal tools.', 'Reinstall with papilot service install from the intended environment; use PAPILOT_SERVICE_PATH for your TeX/Python directory.');
|
|
218
|
+
const serviceData = expand(env.PAPILOT_DATA || path.join(settings.WorkingDirectory || root, '.papilot'));
|
|
219
|
+
check('service.data', serviceData === data, 'warn', serviceData === data ? 'The service data folder matches this diagnostic target.' : `The service uses a different data folder: ${serviceData}`, 'Run doctor with the service data folder (--data), or reinstall the service with the intended PAPILOT_DATA.');
|
|
220
|
+
} catch { check('service', false, 'warn', 'The installed service configuration could not be read.', 'Run papilot service install to recreate the service configuration.'); }
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Keep reports stable even though independent probes finish in any order.
|
|
224
|
+
const order = ['node', 'dependencies', 'frontend', 'review-engine', 'data', 'git', 'latexmk', 'synctex', 'tex-engines', 'agent.codex', 'agent.claude', 'agent.antigravity', 'agents', 'project', 'project.git', 'project.head', 'project.main', 'project.engine', 'project.worktrees', 'remote', 'server', 'service', 'service.tools', 'service.data'];
|
|
225
|
+
checks.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
|
|
226
|
+
const report = { name: 'Papilot', version: pkg.version, ok: !checks.some(item => !item.ok && item.severity === 'error'), checks };
|
|
227
|
+
if (json) console.log(JSON.stringify(report, null, 2));
|
|
228
|
+
else {
|
|
229
|
+
console.log(`Papilot ${pkg.version} doctor\n`);
|
|
230
|
+
for (const item of checks) {
|
|
231
|
+
console.log(`${item.ok ? '✓' : item.severity === 'error' ? '✗' : '!'} ${item.id}: ${item.detail}`);
|
|
232
|
+
if (item.fix) console.log(` Fix: ${item.fix}`);
|
|
233
|
+
}
|
|
234
|
+
const errors = checks.filter(item => !item.ok && item.severity === 'error').length;
|
|
235
|
+
const warnings = checks.filter(item => !item.ok && item.severity === 'warn').length;
|
|
236
|
+
console.log(`\n${errors} blocking error(s), ${warnings} warning(s).${report.ok ? ' Environment checks passed.' : ' Fix blocking errors and run doctor again.'}`);
|
|
237
|
+
}
|
|
238
|
+
return report.ok ? 0 : 1;
|
|
239
|
+
}
|
package/bin/papilot.mjs
CHANGED
|
@@ -6,9 +6,10 @@ import os from 'node:os';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
|
|
9
|
-
const usage = `Usage: papilot [demo | codex <args> | service install | service stop] [--port <number>] [--data <folder>]
|
|
9
|
+
const usage = `Usage: papilot [doctor | demo | codex <args> | service install | service stop] [--port <number>] [--data <folder>]
|
|
10
10
|
|
|
11
11
|
papilot Start Papilot at http://127.0.0.1:7274
|
|
12
|
+
papilot doctor Check the environment; use doctor --help for options
|
|
12
13
|
papilot demo Add the example paper, then start
|
|
13
14
|
papilot codex login Sign in to Codex with the Codex that Papilot uses
|
|
14
15
|
papilot service install Run Papilot in the background after login (macOS)
|
|
@@ -19,95 +20,103 @@ const usage = `Usage: papilot [demo | codex <args> | service install | service s
|
|
|
19
20
|
|
|
20
21
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
21
22
|
const args = process.argv.slice(2);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
|
|
23
|
+
async function main() {
|
|
24
|
+
const doctor = args[0] === 'doctor';
|
|
25
|
+
// Doctor parses its own options and must never reach the installation/startup path.
|
|
26
|
+
if (doctor && (args.includes('--help') || args.includes('-h'))) return (await import('./doctor.mjs')).doctor(args.slice(1));
|
|
27
|
+
let demo = false, service = '', codexArgs = null;
|
|
28
|
+
for (let i = 0; !doctor && i < args.length; i++) {
|
|
29
|
+
const arg = args[i];
|
|
30
|
+
if (arg === 'demo') demo = true;
|
|
31
|
+
else if (arg === 'codex') { codexArgs = args.slice(i + 1); break; }
|
|
32
|
+
else if (arg === 'service' && ['install', 'stop'].includes(args[i + 1])) service = args[++i];
|
|
33
|
+
else if (arg === '--port') {
|
|
34
|
+
const port = Number(args[++i]);
|
|
35
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) { console.error('--port needs a number from 1 to 65535.'); process.exit(1); }
|
|
36
|
+
process.env.PAPILOT_PORT = String(port);
|
|
37
|
+
}
|
|
38
|
+
else if (arg === '--data' && args[i + 1]) process.env.PAPILOT_DATA = path.resolve(args[++i]);
|
|
39
|
+
else if (arg === '--version' || arg === '-v') {
|
|
40
|
+
console.log(pkg.version);
|
|
41
|
+
process.exit(0);
|
|
42
|
+
} else {
|
|
43
|
+
if (arg !== '--help' && arg !== '-h') console.error(`Unknown argument: ${arg}\n`);
|
|
44
|
+
console.log(usage);
|
|
45
|
+
process.exit(arg === '--help' || arg === '-h' ? 0 : 1);
|
|
46
|
+
}
|
|
32
47
|
}
|
|
33
|
-
else if (arg === '--data' && args[i + 1]) process.env.PAPILOT_DATA = path.resolve(args[++i]);
|
|
34
|
-
else if (arg === '--version' || arg === '-v') {
|
|
35
|
-
console.log(pkg.version);
|
|
36
|
-
process.exit(0);
|
|
37
|
-
} else {
|
|
38
|
-
if (arg !== '--help' && arg !== '-h') console.error(`Unknown argument: ${arg}\n`);
|
|
39
|
-
console.log(usage);
|
|
40
|
-
process.exit(arg === '--help' || arg === '-h' ? 0 : 1);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
48
|
|
|
44
|
-
// Papilot needs Node 22.22+. When an older Node runs this command, use a newer
|
|
45
|
-
// one from nvm or PATH, preferring LTS releases, without changing the default.
|
|
46
|
-
const parse = version => version.replace(/^v/, '').split('.').map(Number);
|
|
47
|
-
const supported = version => { const [major, minor] = parse(version); return major > 22 || (major === 22 && minor >= 22); };
|
|
48
|
-
if (!supported(process.versions.node)) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
49
|
+
// Papilot needs Node 22.22+. When an older Node runs this command, use a newer
|
|
50
|
+
// one from nvm or PATH, preferring LTS releases, without changing the default.
|
|
51
|
+
const parse = version => version.replace(/^v/, '').split('.').map(Number);
|
|
52
|
+
const supported = version => { const [major, minor] = parse(version); return major > 22 || (major === 22 && minor >= 22); };
|
|
53
|
+
if (!supported(process.versions.node)) {
|
|
54
|
+
const versions = path.join(process.env.NVM_DIR || path.join(os.homedir(), '.nvm'), 'versions/node');
|
|
55
|
+
const nvm = existsSync(versions) ? readdirSync(versions).map(v => ({ file: path.join(versions, v, 'bin/node'), version: v })) : [];
|
|
56
|
+
const others = [...new Set([...(process.env.PATH || '').split(path.delimiter).map(dir => path.join(dir, 'node')), '/opt/homebrew/bin/node', '/usr/local/bin/node'])]
|
|
57
|
+
.filter(file => existsSync(file)).map(file => ({ file, version: (spawnSync(file, ['--version'], { encoding: 'utf8', timeout: 5000, killSignal: 'SIGKILL' }).stdout || '0').trim() }));
|
|
58
|
+
// nvm's versions come first, then any other Node; even majors are LTS releases,
|
|
59
|
+
// and among those the newest wins.
|
|
60
|
+
const rank = ({ version }) => { const [major, minor, patch] = parse(version); return (major % 2 === 0 ? 1e9 : 0) + major * 1e6 + minor * 1e3 + patch; };
|
|
61
|
+
const best = list => list.filter(c => existsSync(c.file) && supported(c.version)).sort((a, b) => rank(b) - rank(a))[0];
|
|
62
|
+
const node = best(nvm) || best(others);
|
|
63
|
+
if (!node) {
|
|
64
|
+
if (doctor) return (await import('./doctor.mjs')).doctor(args.slice(1));
|
|
65
|
+
console.error(`Papilot needs Node.js 22.22 or newer; this is ${process.versions.node}. Install it with \`nvm install 22\` or from https://nodejs.org, then run papilot again.`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
console.error(`Papilot needs Node.js 22.22+; using ${node.version.replace(/^v/, '')} from ${path.dirname(node.file)}.`);
|
|
69
|
+
const child = spawn(node.file, [fileURLToPath(import.meta.url), ...args], { stdio: 'inherit', env: { ...process.env, PATH: path.dirname(node.file) + path.delimiter + process.env.PATH } });
|
|
70
|
+
// Pass stop signals on, so stopping papilot also stops the server it started.
|
|
71
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(signal, () => child.kill(signal));
|
|
72
|
+
await new Promise(() => child.on('exit', code => process.exit(code ?? 1)));
|
|
61
73
|
}
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
// Pass stop signals on, so stopping papilot also stops the server it started.
|
|
65
|
-
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(signal, () => child.kill(signal));
|
|
66
|
-
await new Promise(() => child.on('exit', code => process.exit(code ?? 1)));
|
|
67
|
-
}
|
|
68
|
-
const tex = !spawnSync('latexmk', ['-v'], { stdio: 'ignore' }).error;
|
|
74
|
+
if (doctor) return (await import('./doctor.mjs')).doctor(args.slice(1));
|
|
75
|
+
const tex = !spawnSync('latexmk', ['-v'], { stdio: 'ignore' }).error;
|
|
69
76
|
|
|
70
|
-
// Node 22 marks node:sqlite as experimental; that notice is noise for users.
|
|
71
|
-
const emitWarning = process.emitWarning;
|
|
72
|
-
process.emitWarning = (warning, ...rest) => String(warning).includes('SQLite') ? undefined : emitWarning.call(process, warning, ...rest);
|
|
73
|
-
process.env.PAPILOT_DATA ||= path.join(os.homedir(), '.papilot');
|
|
77
|
+
// Node 22 marks node:sqlite as experimental; that notice is noise for users.
|
|
78
|
+
const emitWarning = process.emitWarning;
|
|
79
|
+
process.emitWarning = (warning, ...rest) => String(warning).includes('SQLite') ? undefined : emitWarning.call(process, warning, ...rest);
|
|
80
|
+
process.env.PAPILOT_DATA ||= path.join(os.homedir(), '.papilot');
|
|
74
81
|
|
|
75
|
-
// Papilot drives Codex through its app server. A codex on PATH at least as new
|
|
76
|
-
// as the tested version is used as it is; otherwise that version is installed
|
|
77
|
-
// once into the data folder.
|
|
78
|
-
function findCodex() {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
if (codexArgs) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
if (service !== 'stop') process.env.PAPILOT_CODEX_BIN = findCodex() || 'codex';
|
|
98
|
-
const { register } = await import('tsx/esm/api');
|
|
99
|
-
register();
|
|
100
|
-
if (service) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
} else {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
82
|
+
// Papilot drives Codex through its app server. A codex on PATH at least as new
|
|
83
|
+
// as the tested version is used as it is; otherwise that version is installed
|
|
84
|
+
// once into the data folder.
|
|
85
|
+
function findCodex() {
|
|
86
|
+
if (process.env.PAPILOT_CODEX_BIN) return process.env.PAPILOT_CODEX_BIN;
|
|
87
|
+
const tested = pkg.devDependencies['@openai/codex'];
|
|
88
|
+
const version = file => (spawnSync(file, ['--version'], { encoding: 'utf8' }).stdout || '').match(/\d+\.\d+\.\d+/)?.[0] || '0.0.0';
|
|
89
|
+
const recent = file => { const [a, b] = [parse(version(file)), parse(tested)]; return (a[0] - b[0] || a[1] - b[1] || a[2] - b[2]) >= 0; };
|
|
90
|
+
const onPath = (process.env.PATH || '').split(path.delimiter).map(dir => path.join(dir, 'codex')).find(file => existsSync(file));
|
|
91
|
+
if (onPath && recent(onPath)) return onPath;
|
|
92
|
+
const prefix = path.join(process.env.PAPILOT_DATA, 'agents'), installed = path.join(prefix, 'node_modules/.bin/codex');
|
|
93
|
+
if (existsSync(installed) && recent(installed)) return installed;
|
|
94
|
+
console.log(`Installing Codex ${tested} for Papilot (first run only)...`);
|
|
95
|
+
const result = spawnSync('npm', ['install', '--prefix', prefix, `@openai/codex@${tested}`, '--no-audit', '--no-fund', '--loglevel=error'], { stdio: 'inherit' });
|
|
96
|
+
if (result.status === 0 && existsSync(installed)) return installed;
|
|
97
|
+
console.warn('Codex could not be installed, so Codex tasks are unavailable. Install it with npm install -g @openai/codex and run papilot again.');
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
100
|
+
if (codexArgs) {
|
|
101
|
+
const codex = findCodex();
|
|
102
|
+
process.exit(codex ? spawnSync(codex, codexArgs, { stdio: 'inherit' }).status ?? 1 : 1);
|
|
103
|
+
}
|
|
104
|
+
if (service !== 'stop') process.env.PAPILOT_CODEX_BIN = findCodex() || 'codex';
|
|
105
|
+
const { register } = await import('tsx/esm/api');
|
|
106
|
+
register();
|
|
107
|
+
if (service) {
|
|
108
|
+
process.argv.splice(2, Infinity, service);
|
|
109
|
+
await import('../scripts/service.ts');
|
|
110
|
+
} else {
|
|
111
|
+
if (demo) await import('../scripts/demo.ts');
|
|
112
|
+
try {
|
|
113
|
+
await import('../src/server/main.ts');
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (error?.code !== 'EADDRINUSE') throw error;
|
|
116
|
+
console.error(`Port ${process.env.PAPILOT_PORT || 7274} is already in use. Is Papilot already running? Start another copy with --port <number>.`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
if (!tex) console.warn('\nlatexmk was not found, so PDFs cannot be built yet. Install MacTeX or TeX Live (the full distribution), open a new terminal and run papilot again.');
|
|
111
120
|
}
|
|
112
|
-
if (!tex) console.warn('\nlatexmk was not found, so PDFs cannot be built yet. Install MacTeX or TeX Live (the full distribution), open a new terminal and run papilot again.');
|
|
113
121
|
}
|
|
122
|
+
process.exitCode = await main();
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|