typst-mcp 1.0.2 → 1.2.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 CHANGED
@@ -4,91 +4,39 @@
4
4
 
5
5
  **Model Context Protocol (MCP) server for [`https://typst.app`](https://typst.app) — cloud projects, Yjs CRDT editing, and Typst compiler diagnostics**
6
6
 
7
- Install from npm as [`typst-mcp`](https://www.npmjs.com/package/typst-mcp). Source repo: [`typst-web-mcp`](https://github.com/caphefalumi/typst-web-mcp).
8
-
9
7
  [![npm version](https://img.shields.io/npm/v/typst-mcp.svg?style=flat-square)](https://www.npmjs.com/package/typst-mcp)
10
8
  [![Downloads](https://img.shields.io/npm/dt/typst-mcp.svg?style=flat-square)](https://www.npmjs.com/package/typst-mcp)
11
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](./LICENSE)
12
- [![CI](https://github.com/caphefalumi/typst-web-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/caphefalumi/typst-web-mcp/actions/workflows/ci.yml)
13
10
  [![Node.js Package](https://github.com/caphefalumi/typst-web-mcp/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/caphefalumi/typst-web-mcp/actions/workflows/npm-publish.yml)
14
- [![Runtime: Bun](https://img.shields.io/badge/Runtime-Bun_%E2%89%A51.2-f9f1e1?style=flat-square&logo=bun&logoColor=black)](https://bun.sh)
15
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.8-3178C6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
16
- [![MCP SDK](https://img.shields.io/badge/MCP-2024--11--05-10a37f?style=flat-square)](https://modelcontextprotocol.io)
17
- [![Protocol: Yjs + lib0](https://img.shields.io/badge/CRDT-Yjs_%2B_lib0_v3-ff69b4?style=flat-square)](./docs/PROTOCOL.md)
18
- [![Tests](https://img.shields.io/badge/Tests-7_Passing_(57_Assertions)-2ea44f?style=flat-square)](./test/typst-mcp.test.ts)
19
11
 
20
12
  </div>
21
13
 
22
14
  ---
23
15
 
24
- ## Overview
25
-
26
- `typst-web-mcp` connects AI coding and writing agents directly to collaborative [**typst.app**](https://typst.app) cloud projects. Instead of relying on brittle browser DOM scraping, it implements the native `typst.app` client protocol:
27
-
28
- - **Native Yjs CRDT Text Sync:** Decodes and mutates multiplayer `Y.Doc` (`doc.getText("text")`) state vectors so surgical edits preserve collaborator cursors and version history.
29
- - **Binary `/editing/v3` WebSocket Wire Protocol:** Encodes and decodes `lib0` variable-length binary frames (`Subscribe`, `TextUpdate`, `Pong`, `Refresh`, `Presence`, `Cursor`, `Leave`) with REST `/v1/files/:projectId/:fileId/yjs` persistence.
30
- - **GraphQL & REST Project Management:** Full project CRUD, file tree inspection, rename/move, delete, and binary figure/asset uploads (`PNG`, `JPG`, `SVG`, `PDF`, fonts).
31
- - **Direct Authentication + Fallback:** Authenticates via `GET /v1/to-signin` + `POST /v1/auth/signin` (`__Http-typst-session`), with persistent session caching and automatic cookie/Playwright fallback on challenge.
32
- - **Compiler Diagnostics, Preview Screenshots & PDF Export:** Synchronizes cloud Yjs text and binary assets into an isolated workspace and runs the Typst compiler to return structured line/column diagnostics, high-DPI PNG preview screenshots (`typst_capture_screenshot`), and compiled PDFs (`typst_export_pdf`).
33
-
34
- ---
35
-
36
- ## Architecture
37
-
38
- ```mermaid
39
- flowchart LR
40
- Agent["AI Agent\n(Pi / Claude / Cursor)"] <-->|"JSON-RPC (stdio)"| MCP["typst-mcp\n(Node / TypeScript)"]
41
- MCP <-->|"GraphQL (/v1/graphql)\n+ X-Typst-Preflight"| API["https://api.typst.app"]
42
- MCP <-->|"REST Yjs & Blobs\n(/v1/files/:pid/:fid/yjs)"| API
43
- MCP <-->|"Binary lib0 WebSocket\n(wss://api.typst.app/editing/v3)"| WS["typst.app Real-Time Sync"]
44
- MCP -->|"Workspace Sync + Compile"| TypstCLI["Typst Compiler\n(Diagnostics & PDF)"]
45
- ```
46
-
47
- See [`docs/PROTOCOL.md`](./docs/PROTOCOL.md) for the complete reverse-engineered protocol specification.
48
-
49
- ---
50
-
51
- ## MCP Tools (16 Tools)
52
-
53
- | Category | Tool Name | Description |
54
- |:---|:---|:---|
55
- | **Auth** | `typst_auth_login` | Authenticate with `https://typst.app` via email/password (`POST /v1/auth/signin`) or `__Http-typst-session` cookie, with browser fallback. |
56
- | **Auth** | `typst_auth_status` | Check active authentication state, login method, and stored session cookies. |
57
- | **Projects** | `typst_list_projects` | List all owned and shared/external projects in the user's `typst.app` dashboard (`DashboardContent`). |
58
- | **Projects** | `typst_get_project` | Retrieve full project metadata, access permissions, pinned compiler version, and file tree (`ProjectFiles`). |
59
- | **Projects** | `typst_create_project` | Create a new `typst.app` cloud project with initial `.typ`/`.bib` files and pinned compiler version. |
60
- | **Projects** | `typst_delete_project` | Permanently delete a `typst.app` project by ID (`DeleteProjectMutation`). |
61
- | **Files & CRDT** | `typst_list_files` | List all multiplayer Yjs text files and binary asset blobs in a project. |
62
- | **Files & CRDT** | `typst_read_file` | Read any project file — decodes binary Yjs CRDT state (`doc.getText("text")`) into UTF-8 text or base64 for binary assets. |
63
- | **Files & CRDT** | `typst_write_file` | Create or overwrite a `.typ`/`.bib`/`.toml` file, syncing Yjs state diffs over `/editing/v3` WebSocket and REST. |
64
- | **Files & CRDT** | `typst_edit_file` | Apply non-overlapping surgical `oldText -> newText` replacements directly to the `Y.Text("text")` CRDT. |
65
- | **Assets & Tree** | `typst_upload_asset` | Upload or replace binary assets (`PNG`, `JPG`, `SVG`, `PDF`, fonts) from a local path or base64 payload. |
66
- | **Assets & Tree** | `typst_rename_file` | Rename or move a file inside a `typst.app` project (`UpdatesFileMutation`). |
67
- | **Assets & Tree** | `typst_delete_file` | Delete a file or binary asset from a project (`DeleteFilesMutation`). |
68
- | **Compiler** | `typst_get_diagnostics` | Sync all cloud files and run `typst compile` to return structured errors, warnings, line/column spans, and hints. |
69
- | **Compiler** | `typst_export_pdf` | Sync all cloud text & binary assets, compile the project to PDF, and save to `outputPath`. |
70
- | **Preview** | `typst_capture_screenshot` | Capture high-DPI PNG preview screenshots (`mode="pages"` renders compiled PNG pages; `mode="browser"` screenshots the live `typst.app` web UI). |
71
-
72
- ---
73
-
74
16
  ## Quick Start
75
17
 
76
18
  ### Prerequisites
77
19
 
78
20
  - [**Node.js**](https://nodejs.org/) `>= 18` (for `npx` install) or [**Bun**](https://bun.sh) `>= 1.2` (for local development)
79
- - [**Typst CLI**](https://github.com/typst/typst) (`typst` on `PATH` for `typst_get_diagnostics` and `typst_export_pdf`)
21
+ - [**Typst CLI**](https://github.com/typst/typst) (optional: `typst` on `PATH` for local CLI compilation speed; if not installed, `typst_get_diagnostics` automatically uses Typst Web cloud compiler diagnostics)
80
22
 
81
23
  ### Installation
82
24
 
83
- **Recommended — no clone, no global install:**
25
+ **Option 1: Quick start (no clone, no global install):**
84
26
 
85
27
  ```bash
86
28
  npx -y typst-mcp
87
29
  ```
88
30
 
89
- Runs the published [`typst-mcp`](https://www.npmjs.com/package/typst-mcp) package (Node `>= 18`). Use the same command in MCP config below. `-y` skips the npx install prompt.
31
+ **Option 2: Always run latest release (bypasses local `npx` cache):**
90
32
 
91
- **From source (development):**
33
+ ```bash
34
+ npx -y typst-mcp@latest
35
+ ```
36
+
37
+ Runs the published [`typst-mcp`](https://www.npmjs.com/package/typst-mcp) package (Node `>= 18`). `-y` skips the npx confirmation prompt.
38
+
39
+ **Option 3: From source (development):**
92
40
 
93
41
  ```bash
94
42
  git clone https://github.com/caphefalumi/typst-web-mcp.git
@@ -116,10 +64,49 @@ Add `typst-mcp` to your MCP client configuration (`.pi/agent/mcp-adapter.json`,
116
64
  }
117
65
  ```
118
66
 
67
+ > **Tip:** You can also set `"args": ["-y", "typst-mcp@latest"]` to guarantee your MCP client automatically pulls and runs the newest version on launch.
68
+
119
69
  > **Session Persistence:** Once authenticated via `typst_auth_login` (or environment variables), the `__Http-typst-session` cookie is cached locally at `~/.pi/agent/typst-mcp-session.json` so subsequent tool calls authenticate automatically without re-signing in.
120
70
 
121
71
  ---
122
72
 
73
+ ## Overview
74
+
75
+ `typst-web-mcp` connects AI coding and writing agents directly to collaborative [**typst.app**](https://typst.app) cloud projects. Instead of relying on brittle browser DOM scraping, it implements the native `typst.app` client protocol:
76
+
77
+ - **Native Yjs CRDT Text Sync:** Decodes and mutates multiplayer `Y.Doc` (`doc.getText("text")`) state vectors so surgical edits preserve collaborator cursors and version history.
78
+ - **Binary `/editing/v3` WebSocket Wire Protocol:** Encodes and decodes `lib0` variable-length binary frames (`Subscribe`, `TextUpdate`, `Pong`, `Refresh`, `Presence`, `Cursor`, `Leave`) with REST `/v1/files/:projectId/:fileId/yjs` persistence.
79
+ - **GraphQL & REST Project Management:** Full project CRUD, file tree inspection, rename/move, delete, and binary figure/asset uploads (`PNG`, `JPG`, `SVG`, `PDF`, fonts).
80
+ - **Direct Authentication + Fallback:** Authenticates via `GET /v1/to-signin` + `POST /v1/auth/signin` (`__Http-typst-session`), with persistent session caching and automatic cookie/Playwright fallback on challenge.
81
+ - **Compiler Diagnostics, Preview Screenshots & PDF Export:** Synchronizes cloud Yjs text and binary assets into an isolated workspace and runs the Typst compiler to return structured line/column diagnostics, high-DPI PNG preview screenshots (`typst_capture_screenshot`), and compiled PDFs (`typst_export_pdf`).
82
+
83
+ ---
84
+
85
+ ## MCP Tools (18 Tools)
86
+
87
+ | Category | Tool Name | Description |
88
+ |:---|:---|:---|
89
+ | **Auth** | `typst_auth_login` | Authenticate with `https://typst.app` via email/password (`POST /v1/auth/signin`) or `__Http-typst-session` cookie, with browser fallback. |
90
+ | **Auth** | `typst_auth_status` | Check active authentication state, login method, and stored session cookies. |
91
+ | **Projects** | `typst_list_projects` | List all owned and shared/external projects in the user's `typst.app` dashboard (`DashboardContent`). |
92
+ | **Projects** | `typst_get_project` | Retrieve full project metadata, access permissions, pinned compiler version, and file tree (`ProjectFiles`). |
93
+ | **Projects** | `typst_create_project` | Create a new `typst.app` cloud project with initial `.typ`/`.bib` files and pinned compiler version. |
94
+ | **Projects** | `typst_delete_project` | Permanently delete a `typst.app` project by ID (`DeleteProjectMutation`). |
95
+ | **Files & CRDT** | `typst_list_files` | List all multiplayer Yjs text files and binary asset blobs in a project. |
96
+ | **Files & CRDT** | `typst_read_file` | Read any project file with offset/limit pagination, optional line numbering, truncation limits (2,000 lines/50KB), and multimodal MCP image content blocks (`PNG`, `JPG`, `SVG`, `WebP`). |
97
+ | **Files & CRDT** | `typst_write_file` | Create or overwrite a `.typ`/`.bib`/`.toml` file, protected by per-file mutation queue serialization, syncing Yjs state diffs over `/editing/v3` WebSocket and REST. |
98
+ | **Files & CRDT** | `typst_edit_file` | Apply surgical `oldText -> newText` replacements directly to the `Y.Text("text")` CRDT with CRLF/BOM resilience, fuzzy quote/dash matching, unified diff & patch output, and per-file mutation serialization. |
99
+ | **Search & Replace** | `typst_grep_project` | Search for text or regular expressions across project files, returning file paths, line numbers, and optional surrounding context lines. |
100
+ | **Search & Replace** | `typst_find_and_replace` | Find and replace text or regex across a single file or project-wide with regex capture groups (`$1`), dry-run preview diffs, and per-file mutation serialization. |
101
+ | **Assets & Tree** | `typst_upload_asset` | Upload or replace binary assets (`PNG`, `JPG`, `SVG`, `PDF`, fonts) from a local path or base64 payload. |
102
+ | **Assets & Tree** | `typst_rename_file` | Rename or move a file inside a `typst.app` project (`UpdatesFileMutation`). |
103
+ | **Assets & Tree** | `typst_delete_file` | Delete a file or binary asset from a project (`DeleteFilesMutation`). |
104
+ | **Compiler** | `typst_get_diagnostics` | Retrieve compiler diagnostics (errors, warnings, line/column numbers, and hints). Supports both local Typst CLI (`mode="cli"`) and Typst Web cloud compiler (`mode="web"`), with automatic fallback (`mode="auto"`). |
105
+ | **Compiler** | `typst_export_pdf` | Sync all cloud text & binary assets, compile the project to PDF, and save to `outputPath`. |
106
+ | **Preview** | `typst_capture_screenshot` | Capture high-DPI PNG preview screenshots (`mode="pages"` renders compiled PNG pages; `mode="browser"` screenshots the live `typst.app` web UI). |
107
+
108
+ ---
109
+
123
110
  ## Development & Verification
124
111
 
125
112
  ```bash
@@ -149,31 +136,6 @@ Same flow as [Canvas-CLI](https://github.com/caphefalumi/Canvas-CLI):
149
136
 
150
137
  **Trusted publisher (maintainers):** package [`typst-mcp`](https://www.npmjs.com/package/typst-mcp) → Settings → Trusted publishing → GitHub Actions — user/org `caphefalumi`, repo `typst-web-mcp`, workflow file `npm-publish.yml`, environment `npm-publish`, allow **npm publish**.
151
138
 
152
- ### Project Structure
153
-
154
- ```text
155
- typst-web-mcp/
156
- ├── docs/
157
- │ └── PROTOCOL.md # Reverse-engineered typst.app REST, GraphQL, Yjs & WS v3 spec
158
- ├── src/
159
- │ ├── protocol/
160
- │ │ ├── ws-codec.ts # Binary lib0 /editing/v3 WebSocket encoder & decoder
161
- │ │ ├── yjs-doc.ts # Yjs CRDT text decoding, replacement & surgical patching
162
- │ │ └── graphql.ts # GraphQL queries, mutations, fragments & types
163
- │ ├── client/
164
- │ │ ├── auth.ts # Direct /v1/auth/signin login, cookie jar & Playwright fallback
165
- │ │ ├── compiler.ts # Typst binary resolver, diagnostic parser, PDF & PNG preview compiler
166
- │ │ ├── errors.ts # Structured TypstLoadError & error formatting helpers
167
- │ │ └── typst-client.ts # TypstCloudClient (REST, GraphQL, WebSocket v3, Compiler facade)
168
- │ ├── tools/
169
- │ │ └── definitions.ts # MCP JSON Schema definitions for all 16 tools
170
- │ └── index.ts # MCP stdio server entrypoint (dev); npm bin runs dist/index.js
171
- ├── test/
172
- │ └── typst-mcp.test.ts # 7 test suites (57 assertions) covering codecs, CRDTs & E2E
173
- ├── LICENSE # MIT License
174
- └── package.json
175
- ```
176
-
177
139
  ---
178
140
 
179
141
  ## License
@@ -29,6 +29,52 @@ export function resolveTypstBinary(explicitBin) {
29
29
  }
30
30
  return "typst";
31
31
  }
32
+ export function hasTypstBinary(explicitBin) {
33
+ try {
34
+ const bin = resolveTypstBinary(explicitBin);
35
+ if (bin !== "typst" && existsSync(bin)) {
36
+ return true;
37
+ }
38
+ const proc = spawnSync(bin, ["--version"], {
39
+ stdio: "ignore",
40
+ encoding: "utf8",
41
+ });
42
+ return proc.status === 0 && !proc.error;
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
48
+ export function resolvePythonBinary() {
49
+ if (process.env.PYTHON_BIN) {
50
+ return process.env.PYTHON_BIN;
51
+ }
52
+ for (const cmd of ["python", "python3", "py"]) {
53
+ try {
54
+ const res = spawnSync(cmd, ["--version"], { stdio: "ignore" });
55
+ if (res.status === 0) {
56
+ return cmd;
57
+ }
58
+ }
59
+ catch { }
60
+ }
61
+ return "python";
62
+ }
63
+ export function offsetToPosition(text, offset) {
64
+ let line = 1;
65
+ let column = 1;
66
+ const target = Math.min(Math.max(0, offset), text.length);
67
+ for (let i = 0; i < target; i++) {
68
+ if (text[i] === "\n") {
69
+ line++;
70
+ column = 1;
71
+ }
72
+ else {
73
+ column++;
74
+ }
75
+ }
76
+ return { line, column };
77
+ }
32
78
  function hasValidPngSignature(bytes) {
33
79
  return (bytes.byteLength >= 8 &&
34
80
  bytes[0] === 0x89 &&
@@ -95,6 +141,9 @@ export function compileWorkspaceDiagnostics(params) {
95
141
  cwd: params.workspaceDir,
96
142
  encoding: "utf8",
97
143
  });
144
+ if (proc.error) {
145
+ throw new TypstLoadError(["TYPST_EXECUTION_FAILED"], `Failed to execute Typst compiler binary '${params.typstBin}': ${proc.error.message}`);
146
+ }
98
147
  const combinedOutput = `${proc.stderr ?? ""}\n${proc.stdout ?? ""}`;
99
148
  const diagnostics = parseShortDiagnostics(combinedOutput).map((d) => ({
100
149
  ...d,
@@ -130,6 +179,9 @@ export function compileWorkspacePdf(params) {
130
179
  cwd: params.workspaceDir,
131
180
  encoding: "utf8",
132
181
  });
182
+ if (proc.error) {
183
+ throw new TypstLoadError(["TYPST_EXECUTION_FAILED"], `Failed to execute Typst compiler binary '${params.typstBin}': ${proc.error.message}`);
184
+ }
133
185
  const combinedOutput = `${proc.stderr ?? ""}\n${proc.stdout ?? ""}`;
134
186
  const diagnostics = parseShortDiagnostics(combinedOutput);
135
187
  if (proc.status !== 0) {
@@ -148,12 +200,169 @@ export function compileWorkspacePdf(params) {
148
200
  diagnostics,
149
201
  };
150
202
  }
203
+ export function captureWebDiagnostics(params) {
204
+ const pythonBin = resolvePythonBinary();
205
+ const timeoutSec = ((params.timeoutMs ?? 25000) / 1000).toFixed(1);
206
+ const pyScript = `
207
+ import os, sys, json, time
208
+ from playwright.sync_api import sync_playwright
209
+
210
+ app_url = os.environ.get("TYPST_APP_URL", "https://typst.app")
211
+ proj_id = os.environ["TYPST_PROJ_ID"]
212
+ raw_cookie = os.environ.get("TYPST_COOKIE", "")
213
+ timeout_sec = float(os.environ.get("TYPST_TIMEOUT_SEC", "25.0"))
214
+
215
+ cookies = []
216
+ for part in raw_cookie.split(";"):
217
+ part = part.strip()
218
+ if "=" in part:
219
+ k, v = part.split("=", 1)
220
+ name = k.strip()
221
+ val = v.strip()
222
+ is_session = "__Http" in name or "session" in name
223
+ cookies.append({
224
+ "name": name,
225
+ "value": val,
226
+ "domain": ".typst.app",
227
+ "path": "/",
228
+ "httpOnly": is_session,
229
+ "secure": True,
230
+ "sameSite": "Strict" if is_session else "Lax",
231
+ })
232
+
233
+ with sync_playwright() as p:
234
+ browser = None
235
+ for channel in ["msedge", "chrome", None]:
236
+ try:
237
+ if channel:
238
+ browser = p.chromium.launch(channel=channel, headless=True)
239
+ else:
240
+ browser = p.chromium.launch(headless=True)
241
+ break
242
+ except Exception:
243
+ continue
244
+
245
+ if browser is None:
246
+ sys.stderr.write("NO_BROWSER_FOUND")
247
+ sys.exit(2)
248
+
249
+ ctx = browser.new_context(viewport={"width": 1920, "height": 1080})
250
+ if cookies:
251
+ ctx.add_cookies(cookies)
252
+
253
+ ctx.add_init_script("""
254
+ window.__typst_diagnostics = null;
255
+ window.__typst_status = 'starting';
256
+ const OrigWorker = window.Worker;
257
+ window.Worker = function(url, opts) {
258
+ const w = new OrigWorker(url, opts);
259
+ w.addEventListener('message', (ev) => {
260
+ try {
261
+ if (ev.data && ev.data.tag === 'notification') {
262
+ if (ev.data.name === 'diagnostics') {
263
+ window.__typst_diagnostics = ev.data.notification?.diagnostics || [];
264
+ } else if (ev.data.name === 'status') {
265
+ window.__typst_status = ev.data.notification?.status || 'idle';
266
+ }
267
+ }
268
+ } catch(e) {}
269
+ });
270
+ return w;
271
+ };
272
+ """)
273
+
274
+ page = ctx.new_page()
275
+ page.goto(f"{app_url}/project/{proj_id}", wait_until="commit", timeout=45000)
276
+
277
+ start_time = time.time()
278
+ diags = None
279
+ while time.time() - start_time < timeout_sec:
280
+ data = page.evaluate("({ diag: window.__typst_diagnostics, status: window.__typst_status, url: window.location.href })")
281
+ if "signin" in data.get("url", ""):
282
+ browser.close()
283
+ sys.stderr.write(f"AUTH_REQUIRED: Redirected to {data.get('url')}")
284
+ sys.exit(3)
285
+ if data.get("diag") is not None:
286
+ diags = data.get("diag")
287
+ if data.get("status") == "idle":
288
+ break
289
+ page.wait_for_timeout(300)
290
+
291
+ browser.close()
292
+ print(json.dumps(diags or []))
293
+ `;
294
+ const proc = spawnSync(pythonBin, ["-c", pyScript], {
295
+ env: {
296
+ ...process.env,
297
+ TYPST_APP_URL: params.appUrl,
298
+ TYPST_PROJ_ID: params.projectId,
299
+ TYPST_COOKIE: params.cookieHeader,
300
+ TYPST_TIMEOUT_SEC: timeoutSec,
301
+ },
302
+ encoding: "utf8",
303
+ timeout: (params.timeoutMs ?? 25000) + 15000,
304
+ });
305
+ if (proc.error || proc.status !== 0) {
306
+ const stderr = (proc.stderr || proc.error?.message || "").trim();
307
+ if (stderr.includes("NO_BROWSER_FOUND")) {
308
+ throw new TypstLoadError(["NO_BROWSER_FOUND"], "No compatible browser (Edge, Chrome, or Chromium) found for Playwright web diagnostics.");
309
+ }
310
+ if (stderr.includes("AUTH_REQUIRED")) {
311
+ throw new TypstLoadError(["UNAUTHENTICATED"], `Typst Web cloud session expired or invalid: ${stderr}`);
312
+ }
313
+ throw new TypstLoadError(["WEB_DIAGNOSTICS_FAILED"], `Failed to capture Typst Web cloud diagnostics: ${stderr || "Ensure python with playwright is installed ('pip install playwright && playwright install')"}`);
314
+ }
315
+ const rawJson = (proc.stdout || "").trim();
316
+ let rawList = [];
317
+ try {
318
+ rawList = JSON.parse(rawJson);
319
+ }
320
+ catch (err) {
321
+ throw new TypstLoadError(["WEB_DIAGNOSTICS_PARSE_FAILED"], `Failed to parse Typst Web diagnostics output: ${err.message}`);
322
+ }
323
+ const mainFileName = params.mainFile ?? "main.typ";
324
+ const diagnostics = rawList.map((raw) => {
325
+ let line = 1;
326
+ let column = 1;
327
+ const filePath = raw.path || mainFileName;
328
+ const fileText = params.fileContents?.[filePath];
329
+ if (fileText && raw.range?.start !== undefined) {
330
+ const pos = offsetToPosition(fileText, raw.range.start);
331
+ line = pos.line;
332
+ column = pos.column;
333
+ }
334
+ return {
335
+ path: filePath,
336
+ line,
337
+ column,
338
+ severity: raw.severity === "error" ? "error" : "warning",
339
+ message: raw.message,
340
+ hints: Array.isArray(raw.hints) ? raw.hints : [],
341
+ package: raw.package ?? null,
342
+ range: raw.range
343
+ ? { start: raw.range.start, end: raw.range.end }
344
+ : undefined,
345
+ };
346
+ });
347
+ const errorCount = diagnostics.filter((d) => d.severity === "error").length;
348
+ const warningCount = diagnostics.filter((d) => d.severity === "warning").length;
349
+ return {
350
+ projectId: params.projectId,
351
+ mainFile: mainFileName,
352
+ mode: "web",
353
+ success: errorCount === 0,
354
+ errorCount,
355
+ warningCount,
356
+ diagnostics,
357
+ };
358
+ }
151
359
  export function captureBrowserPreviewScreenshot(params) {
152
360
  const outputDir = resolveOutputDirectory(params.outputDir, `typst-preview-ui-${params.projectId}-`);
153
361
  mkdirSync(outputDir, { recursive: true });
154
362
  const screenshotPath = join(outputDir, `typst-app-${params.projectId}.png`);
363
+ const pythonBin = resolvePythonBinary();
155
364
  const pyScript = `
156
- import os
365
+ import os, sys
157
366
  from playwright.sync_api import sync_playwright
158
367
 
159
368
  app_url = os.environ["TYPST_PREVIEW_APP_URL"]
@@ -166,16 +375,35 @@ for part in raw_cookie.split(";"):
166
375
  part = part.strip()
167
376
  if "=" in part:
168
377
  k, v = part.split("=", 1)
378
+ name = k.strip()
379
+ val = v.strip()
380
+ is_session = "__Http" in name or "session" in name
169
381
  cookies.append({
170
- "name": k.strip(),
171
- "value": v.strip(),
172
- "domain": "typst.app",
382
+ "name": name,
383
+ "value": val,
384
+ "domain": ".typst.app",
173
385
  "path": "/",
386
+ "httpOnly": is_session,
174
387
  "secure": True,
388
+ "sameSite": "Strict" if is_session else "Lax",
175
389
  })
176
390
 
177
391
  with sync_playwright() as p:
178
- browser = p.chromium.launch(channel="msedge", headless=True)
392
+ browser = None
393
+ for channel in ["msedge", "chrome", None]:
394
+ try:
395
+ if channel:
396
+ browser = p.chromium.launch(channel=channel, headless=True)
397
+ else:
398
+ browser = p.chromium.launch(headless=True)
399
+ break
400
+ except Exception:
401
+ continue
402
+
403
+ if browser is None:
404
+ sys.stderr.write("NO_BROWSER_FOUND")
405
+ sys.exit(2)
406
+
179
407
  ctx = browser.new_context(viewport={"width": 1920, "height": 1080}, device_scale_factor=2)
180
408
  if cookies:
181
409
  ctx.add_cookies(cookies)
@@ -185,7 +413,7 @@ with sync_playwright() as p:
185
413
  page.screenshot(path=out_path, full_page=False)
186
414
  browser.close()
187
415
  `;
188
- const proc = spawnSync("python", ["-c", pyScript], {
416
+ const proc = spawnSync(pythonBin, ["-c", pyScript], {
189
417
  env: {
190
418
  ...process.env,
191
419
  TYPST_PREVIEW_APP_URL: params.appUrl,