npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

zai-conversation-scraper

v1.3.0

Published

Scrape Z.ai conversations from chat.z.ai and save them as clean Markdown

Readme

Z.ai Conversation Scraper

Scrape Z.ai conversations from chat.z.ai and save them as clean Markdown.

It supports both normal conversation URLs (/c/<id>) and shared conversation URLs (/s/<id>). Private conversations can be accessed through a persistent Playwright browser profile or a saved storage-state file.

Features

  • Captures the successful history responses loaded by Z.ai itself
  • Role-aware decoding of user content and assistant content_blocks
  • Active-branch reconstruction from Z.ai message parent/child links
  • Incremental network and DOM collection for virtualized older messages
  • Z.ai-specific DOM selectors with broad structural fallbacks
  • Normal (/c/<id>) and shared (/s/<id>) conversations
  • Persistent Chromium profiles for authenticated private chats
  • Optional Playwright storage-state loading and saving
  • Scrolls every real scroll surface instead of guessing one container
  • Role inference from message IDs, attributes, classes, and content structure
  • Hidden reasoning/thinking blocks excluded by default
  • Atomic file writes (temporary file + rename)
  • Optional rendered HTML capture for diagnosing DOM changes
  • Graceful Ctrl+C shutdown (press again to force quit)
  • GFM output, including tables, fenced code, strikethrough, and task lists

Prerequisites

Setup

bun install
bunx playwright install chromium

Install

bun add -g zai-conversation-scraper
# or run without installing
bunx zai-scraper "https://chat.z.ai/s/<share-id>"

As a library:

bunx jsr add @thehouseisonfire/zai-conversation-scraper
import { scrapeConversation } from "@thehouseisonfire/zai-conversation-scraper";

Usage

From a source checkout, bun run scrape works the same way as the zai-scraper binary used below.

Shared conversation

zai-scraper "https://chat.z.ai/s/<share-id>"

Private conversation

Use a dedicated persistent browser profile. On the first run, launch headed, sign in in the opened Chromium window, and let the scraper continue once the conversation appears:

zai-scraper \
  --headed \
  --profile-dir .zai-profile \
  "https://chat.z.ai/c/<conversation-id>"

Later runs can reuse the same profile headlessly:

zai-scraper \
  --profile-dir .zai-profile \
  "https://chat.z.ai/c/<conversation-id>"

Do not point --profile-dir at your normal Chromium/Chrome profile. Use a scraper-specific directory.

Options

| Flag | Description | | ----------------------------- | --------------------------------------------- | | -o, --output <path> | Output Markdown file | | --selector <css> | Override automatic message-root detection | | --timeout <ms> | Navigation/content timeout (default: 60000) | | --debug-html <path> | Save the rendered page HTML | | --profile-dir <path> | Use a persistent Chromium profile | | --storage-state <path> | Load Playwright storage state | | --save-storage-state <path> | Save storage state after scraping | | --include-thinking | Include Z.ai thinking/reasoning blocks | | --headed | Show the Chromium window | | -v, --version | Print the version | | -h, --help | Show usage help |

--profile-dir and --storage-state are mutually exclusive because persistent Playwright contexts cannot be initialized from a storage-state file.

By default, the scraper listens to the history requests made by the Z.ai page itself and consumes their successful responses. This matters because directly repeating the nominal shared-history endpoint may return 403 Forbidden even while the page is able to render the conversation. Passing --selector explicitly disables network-history parsing and forces incremental rendered-DOM extraction.

Examples

# Shared conversation with an explicit output file
zai-scraper -o conversation.md "https://chat.z.ai/s/<share-id>"

# First authenticated run
zai-scraper --headed --profile-dir .zai-profile \
  "https://chat.z.ai/c/<conversation-id>"

# Capture the rendered DOM when selectors stop matching
zai-scraper --headed --profile-dir .zai-profile \
  --debug-html zai-page.html \
  "https://chat.z.ai/c/<conversation-id>"

# Force a replacement selector
zai-scraper --selector '#chat-container [id^="message-"]' \
  "https://chat.z.ai/s/<share-id>"

# Include visible/embedded reasoning blocks explicitly
zai-scraper --include-thinking "https://chat.z.ai/s/<share-id>"

Build

Compile to JavaScript for execution with Node.js:

bun run build
node dist/zai-to-markdown.js "https://chat.z.ai/s/<share-id>"

Tests

bun run test

The regression suite covers assistant replies stored exclusively in content_blocks, reasoning filtering, captured metadata/batch merging, nested role metadata, virtualized-window ordering, and the fallback content representation.

Output Format

# [Conversation Title]

**Source:** <https://chat.z.ai/c/...>
**Scraped:** YYYY-MM-DD

---

## User

[User message]

## Z.ai

[Assistant response]

Authentication Notes

The scraper never asks for, stores, or handles your password directly. Authentication is performed by Z.ai inside Chromium. A persistent profile or storage-state file can contain live session credentials, so keep it private and do not commit it.

Extraction Strategy

The scraper installs its Playwright response listener before navigation. It then consumes successful conversation and messages/batch responses made by the Z.ai application itself, rather than assuming that a separately issued request to /api/v1/chats/share/<id> will be authorized.

Z.ai stores messages as a parent-linked tree. The scraper merges every captured batch, begins at currentId when available, and follows parentId to reconstruct the active branch. While that branch is incomplete, it scrolls every scrollable page element to the top and collects newly loaded response batches. This follows the application's own lazy-loading path without requiring the scraper to know private request headers.

Message bodies are role-specific: user prompts are read from content, while assistant replies are decoded primarily from content_blocks with content retained as a fallback. Reasoning blocks are separated from answer blocks and included only with --include-thinking.

If no recognizable history response is available, the DOM fallback no longer preloads and then reads only the final virtualized window. Instead, it snapshots each rendered window while moving upward, deduplicates turns by Z.ai message ID, and prepends newly discovered older turns. This is less authoritative than the response-backed path but does not silently discard windows that Z.ai unmounts.

A successful response-backed run reports the number of exported messages, active-branch records, and cached API records. A fallback run reports the growing count of distinct rendered turn blocks. If neither count grows while scrolling, use --headed --debug-html page.html to inspect a fresh Z.ai DOM/network change.

Z.ai is a private web application, so both its endpoints and rendered structure may change without notice.