Lark Hirono Sync — Recursive Wiki Subtree Copy
Copy a Feishu wiki subtree from one location to another, preserving all content and block-level styles.
Running the CLI
- Global install —
lark-hirono sync [options] - Local dev repo —
npx tsx bin/lark-hirono.ts sync [options]
First-Time Setup
Run these steps once before first use.
1. Install lark-hirono and authenticate
Follow the "First-Time Setup" steps in the main lark-hirono skill (install lark-hirono → install lark-cli → lark-cli config init → lark-hirono auth login --domain docs).
2. Install Playwright + Chromium (required for cross-space images)
Cross-space image downloads via the Feishu API return 403. The sync command uses Playwright with browser session cookies to download images through Feishu's internal CDN instead. Without Playwright, sync still runs but all cross-space images are skipped — the sync summary reports the count of failed images and exits with code 1.
Playwright is declared as an optionalDependency in package.json. If it wasn't installed automatically:
npm install playwright
npx playwright install chromium
3. Browser login for image transfer
On first sync with images, Playwright launches a visible browser window pointing to Feishu. Log in manually. After login, the session is saved to ~/.config/lark-hirono/browser-state.json and reused headlessly for subsequent runs.
Requirements:
- A display environment (desktop or X11 forwarding) — the first run needs a visible browser. This will not work over plain SSH or in CI without a display.
- After the first login, subsequent runs are headless and work without a display.
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
playwright not installed error mid-sync |
Playwright not in node_modules | npm install playwright && npx playwright install chromium |
Cannot launch browser (need display) |
No display (SSH/CI) | Run the first sync from a desktop environment, or copy a valid browser-state.json from another machine |
| Sync summary shows failed images | Browser session expired or download 403 | Delete ~/.config/lark-hirono/browser-state.json and re-run to trigger a fresh login |
| All images fail after the first failure in one run | Once Playwright fails once in a process, the sync disables it for the rest of that run to avoid hanging | Fix the root cause (login expired → delete browser-state.json; missing display → run from desktop), then re-run the sync command |
| Image upload failures in summary | Upload API failed or verification found empty token | Re-run sync normally (Rule 1 in the Recovery Playbook). Never --force |
| Sync was killed / timed out mid-run | Process interrupted | Re-run sync normally. State is persisted per-child — no duplicates, no lost progress |
--check shows orphans |
Source pages deleted or moved out of subtree | Ignore (informational) or delete the orphan target pages manually in Feishu |
| Exit code 1 but most of the tree synced | Any image or page failed | Re-run sync normally. Exit 1 is not a fatal signal |
Prerequisites Summary
- Node.js 20+,
lark-cli>= 1.0.9 (installed and authenticated) - Playwright + Chromium (required for cross-space images; without it sync works but images are skipped)
- Display environment for first browser login (subsequent runs are headless)
Usage
lark-hirono sync --from <source-url> --to <target-url> [options]
Options:
--from <url>— Source wiki URL or bare node token (required)--to <url>— Target wiki node that will mirror the source (required). The target node's document receives the source root's block content; source children are created as children under the target node. Existing target content is cleared on first sync; existing target children outside the mirrored tree are left alone but flagged as ORPHAN--no-numbers— Skip auto-numbered headings (numbering enabled by default)--browser-state <path>— Path to Playwright browser state file (default:~/.config/lark-hirono/browser-state.json)--dry-run— Print source tree structure without copying--check— Read-only diff of source vs saved state; also walks the target tree to detect pages deleted out-of-band. Reports new/modified/missing/retry/rename/orphan pages. Exit0if fully in sync,1if drift detected (usable as a CI check). Pages flaggedmissingare auto-recreated by the nextsyncthat touches them.--status— Print saved-state metadata only (last sync time, tracked page count); offline, no API calls--force— ⚠ Rarely correct. See the Recovery Playbook below before using. Discards saved state and recopies every page from scratch.-v, --verbose— Verbose logging
Recovery Playbook — Read This Before Reacting to Any Failure
This section is the canonical answer to "the sync failed / --check reports drift / images didn't upload / the run was interrupted / exit code was 1 / something looks wrong." Apply these rules before suggesting anything to the user.
Rule 1: The default recovery action is always a plain re-run
lark-hirono sync --from <SAME_SOURCE> --to <SAME_TARGET> -v
No --force. No other flags. The incremental path already handles every normal failure mode:
| Symptom | What a plain re-run does |
|---|---|
--check reports NEW pages |
Creates them from source |
--check reports MOD pages |
Clears target content and re-copies |
--check reports RETRY pages (pages with failedImages in state) |
Per-page force-recopy: re-downloads images via browser, re-uploads, re-writes blocks |
--check reports MISSING pages (target deleted out-of-band) |
Auto-heals: recreates the target node in place |
--check reports RENAME |
Updates target node title |
| Exit code 1 from previous run due to failed image uploads | Retries the failing pages (they're marked in state) |
| Previous run was killed / crashed / timed out mid-sync | Resumes from last-persisted child; no duplicates |
| Browser session expired, images 403'd | Delete ~/.config/lark-hirono/browser-state.json, then re-run (fresh login) |
State is persisted per-child (~/.config/lark-hirono/sync-state/<hash>.json), so interrupting at any point is safe.
Rule 2: Never present --force as a user-facing option
When the user asks "what should I do?" after a failure, do not write "Option 1: run with --force. Option 2: re-run normally." That framing invites the user to pick --force because it sounds more thorough. Instead, prescribe the plain re-run and only mention --force if Rule 3 applies.
Rule 3: --force is only correct in two narrow situations
- The state file at
~/.config/lark-hirono/sync-state/<hash>.jsonis genuinely corrupt (unparseable JSON, references target tokens that no longer exist, pairs wrong source and target). Confirm by reading it before reaching for--force. - The user has explicitly said they want to rebuild the mirror from zero — e.g., "scrap it and start over."
If neither applies, do not use --force. It re-downloads every image, re-uploads every block, and does not produce a better result than a plain re-run for any normal failure.
Rule 4: Exit code 1 is not an emergency
sync exits 1 whenever any image failed in the current run — even if 99% of the tree synced cleanly and the failures are recoverable on re-run. Exit 1 means "not fully clean yet," not "broken." Treat it as a signal to re-run (Rule 1), not to escalate.
Rule 5: Orphans are informational, not blocking
ORPHAN entries in --check mean a source page was deleted or moved out of the subtree. The target node is left intact for the user to delete manually; state is pruned automatically on the next sync. Do not propose --force to "clean up" orphans — that would not delete them from the target, it would just rebuild everything around them.
Rule 6: When truly stuck, read before writing
If re-running doesn't converge after two attempts, do not escalate to --force. Instead:
- Run
--checkand read the full report. cat ~/.config/lark-hirono/sync-state/<hash>.jsonand inspect thepagesentries for the problem children — look atfailedImages,contentHash,objEditTime.- Run with
-vand read the log for the specific page that isn't converging. - Report the specific finding to the user with a concrete diagnosis, not a generic "try
--force."
How It Works
Block-Level Copy
For each child node under the source, the tool:
- Creates a new wiki node under the target via
wiki +node-create - Fetches all blocks from the source document via the docx block API
- Pre-downloads images via Playwright browser session (bypasses API 403 on cross-space media)
- Reproduces the block tree via BFS traversal:
- Batches of 10 blocks created per API call
- Tables with >9 rows are created at 9 rows then expanded via
insert_table_row - Auto-created children (table cells, grid columns) mapped by position
- Image data uploaded to empty image blocks, then associated via
replace_imagePATCH (retried up to 2 times with post-upload verification)
- Cleans up auto-created trailing empty paragraphs in callouts and grid columns
- Reports failures — any images that failed to download or upload are logged, included in the sync summary, and recorded in the sync state JSON
This preserves all block-level styles: heading backgrounds, table cell colors, callout emojis/borders, paragraph backgrounds, equations, code block languages, nested list indentation.
Image Transfer
The Feishu media download API often returns 403 for cross-space images. The tool uses Playwright with cached browser session cookies to download images via Feishu's internal CDN:
https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/preview/{token}/?preview_type=16
On first run, a browser window opens for Feishu login. The session is saved to ~/.config/lark-hirono/browser-state.json for subsequent headless runs.
Unsupported Block Types
- Unknown / internal block types (e.g. type 999): Cannot be created via API. Skipped with a warning in the sync summary — content inside these blocks may be lost; inspect the source doc if the summary reports "Unsupported block types."
- Shortcuts: Skipped with a log message.
- Non-docx nodes (sheets, bitables): A placeholder docx node is created.
Workflow
Step 1: Preview the Source Tree
lark-hirono sync \
--from https://my.feishu.cn/wiki/SOURCE_TOKEN \
--to https://my.feishu.cn/wiki/TARGET_TOKEN \
--dry-run
Step 2: Execute Sync
lark-hirono sync \
--from https://my.feishu.cn/wiki/SOURCE_TOKEN \
--to https://my.feishu.cn/wiki/TARGET_TOKEN \
-v
Step 3: Verify Sync Is Up to Date
--check walks both the source and target trees and compares against
the saved state. It reports what would change on the next sync
(new/modified/missing/retry/rename/orphan pages) without performing any
writes. missing flags pages whose target was deleted out-of-band —
the next sync that touches them will re-create them in place.
Exit status is 0 when fully in sync and 1 when drift is detected,
so it can be used as a CI or pre-publish check.
lark-hirono sync \
--from https://my.feishu.cn/wiki/SOURCE_TOKEN \
--to https://my.feishu.cn/wiki/TARGET_TOKEN \
--check
Examples
Copy wiki subtree between spaces
lark-hirono sync \
--from https://scnajei2ds6y.feishu.cn/wiki/RK4aw2SgriDqDNkB6NLcXhZhnFf \
--to https://my.feishu.cn/wiki/A770wxopAij0FPktwApceRuPnSe
Dry-run with bare tokens
lark-hirono sync --from RK4aw2SgriDqDNkB6NLcXhZhnFf --to A770wxopAij0FPktwApceRuPnSe --dry-run