mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-12 22:09:48 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7f623158c | |||
| 2c800c670a | |||
| 5b20f9a746 | |||
| 9977ff520c | |||
| 03bd68d3a4 | |||
| 39792f4360 | |||
| 25b8a7f73d | |||
| e13092ba2a | |||
| acbcf19ba2 |
@@ -6,14 +6,14 @@
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||
"version": "1.105.0"
|
||||
"version": "1.108.0"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "baoyu-skills",
|
||||
"description": "Content generation, AI backends, and utility tools for daily work efficiency",
|
||||
"source": "./",
|
||||
"strict": true,
|
||||
"strict": false,
|
||||
"skills": [
|
||||
"./skills/baoyu-article-illustrator",
|
||||
"./skills/baoyu-comic",
|
||||
|
||||
@@ -7,6 +7,16 @@ description: Universal release workflow. Auto-detects version files and changelo
|
||||
|
||||
Universal release workflow supporting any project type with multi-language changelog.
|
||||
|
||||
## User Input Tools
|
||||
|
||||
When this skill prompts the user, follow this tool-selection rule (priority order):
|
||||
|
||||
1. **Prefer built-in user-input tools** exposed by the current agent runtime — e.g., `AskUserQuestion`, `request_user_input`, `clarify`, `ask_user`, or any equivalent.
|
||||
2. **Fallback**: if no such tool exists, emit a numbered plain-text message and ask the user to reply with the chosen number/answer for each question.
|
||||
3. **Batching**: if the tool supports multiple questions per call, combine all applicable questions into a single call; if only single-question, ask them one at a time in priority order.
|
||||
|
||||
Concrete `AskUserQuestion` references below are examples — substitute the local equivalent in other runtimes.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Just run `/release-skills` - auto-detects your project configuration.
|
||||
|
||||
+1
-1
@@ -4,4 +4,4 @@ set -eu
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
node scripts/sync-shared-skill-packages.mjs --repo-root "$REPO_ROOT" --enforce-clean
|
||||
node scripts/verify-shared-package-deps.mjs --repo-root "$REPO_ROOT"
|
||||
|
||||
@@ -83,6 +83,10 @@ out
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Shared package builds are published to npm.
|
||||
!packages/*/dist/
|
||||
!packages/*/dist/**
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
|
||||
+1
-1
@@ -2,5 +2,5 @@ release:
|
||||
target_globs:
|
||||
- skills/*
|
||||
hooks:
|
||||
prepare_artifact: node scripts/sync-shared-skill-packages.mjs --repo-root "{project_root}" --target "{target}"
|
||||
prepare_artifact: node scripts/verify-shared-package-deps.mjs --repo-root "{project_root}" --target "{target}"
|
||||
publish_artifact: node scripts/publish-skill.mjs --skill-dir "{target}" --version "{version}" --changelog-file "{release_notes_file}" --dry-run "{dry_run}"
|
||||
|
||||
@@ -2,6 +2,25 @@
|
||||
|
||||
English | [中文](./CHANGELOG.zh.md)
|
||||
|
||||
## 1.108.0 - 2026-04-19
|
||||
|
||||
### Refactor
|
||||
- Refactor skills into focused reference files for better maintainability
|
||||
- Use npm packages for shared skill code across skills
|
||||
|
||||
## 1.107.0 - 2026-04-15
|
||||
|
||||
### Features
|
||||
- `baoyu-diagram`: add SVG-to-PNG @2x conversion script — auto-converts generated SVG diagrams to @2x PNG using Sharp; consolidate reference files and add `{baseDir}` path resolution for portable skill loading
|
||||
|
||||
### Fixes
|
||||
- `claude-plugin`: allow inline marketplace manifest (#130)
|
||||
|
||||
## 1.106.0 - 2026-04-14
|
||||
|
||||
### Features
|
||||
- `baoyu-diagram`: add architecture enrichment rules — automatically expand architecture diagrams with multiple client types, per-service tech stacks, database tiers, message buses, and color-coded categories; add full structural layout patterns, architecture-specific pitfalls, network topology templates, and layout math for complex diagrams
|
||||
|
||||
## 1.105.0 - 2026-04-13
|
||||
|
||||
### Features
|
||||
|
||||
@@ -2,6 +2,25 @@
|
||||
|
||||
[English](./CHANGELOG.md) | 中文
|
||||
|
||||
## 1.108.0 - 2026-04-19
|
||||
|
||||
### 重构
|
||||
- 将技能文档拆分为聚焦的参考文件,提升可维护性
|
||||
- 将多技能共享代码迁移至 npm 包管理
|
||||
|
||||
## 1.107.0 - 2026-04-15
|
||||
|
||||
### 新功能
|
||||
- `baoyu-diagram`:新增 SVG 转 @2x PNG 转换脚本 —— 使用 Sharp 自动将生成的 SVG 图表转为 @2x PNG;精简合并参考文件,新增 `{baseDir}` 路径解析以支持可移植的技能加载
|
||||
|
||||
### 修复
|
||||
- `claude-plugin`:支持内联 marketplace manifest (#130)
|
||||
|
||||
## 1.106.0 - 2026-04-14
|
||||
|
||||
### 新功能
|
||||
- `baoyu-diagram`:新增架构图丰富化规则 —— 自动扩展架构图,补充多客户端类型、各服务技术栈、数据库分层、消息总线和分色分类;新增完整结构布局模式、架构专用陷阱提示、网络拓扑模板和复杂图表布局计算
|
||||
|
||||
## 1.105.0 - 2026-04-13
|
||||
|
||||
### 新功能
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# CLAUDE.md
|
||||
|
||||
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.104.0**.
|
||||
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.107.0**.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -46,16 +46,32 @@ Execute: `${BUN_X} skills/<skill>/scripts/main.ts [options]`
|
||||
| Rule | Description |
|
||||
|------|-------------|
|
||||
| **Load project skills first** | Project skills override system/user-level skills with same name |
|
||||
| **Default image generation** | Use `skills/baoyu-imagine/SKILL.md` unless user specifies otherwise |
|
||||
| **Default image generation** | Use whatever image backend is available in the current runtime; if multiple are available, ask the user which to use. See `## Image Generation Tools` below. |
|
||||
|
||||
Priority: project `skills/` → `$HOME/.baoyu-skills/` → system-level.
|
||||
|
||||
## Skill Self-Containment
|
||||
|
||||
Each skill under `skills/` (and `.claude/skills/`) is distributed and consumed independently — the folder may be extracted, copied into another project, or loaded without the rest of this repo. Therefore:
|
||||
|
||||
- **Never link from `SKILL.md` or its `references/` to files outside the skill's own directory.** This includes `docs/`, sibling skills, and the repo root. Relative paths like `../../docs/foo.md` break when the skill is used standalone.
|
||||
- **Inline any shared convention** (e.g., user-input rules, image-generation backend selection) directly in the skill rather than referencing an out-of-skill doc.
|
||||
- Shared docs under `docs/` exist for **repo-author guidance only** — they may be referenced from `CLAUDE.md` and `docs/creating-skills.md`, but NOT from any `SKILL.md`. This applies to `docs/user-input-tools.md`, `docs/image-generation-tools.md`, `docs/image-generation.md`, and any other `docs/` file.
|
||||
|
||||
## User Input Tools
|
||||
|
||||
Skills that prompt users for choices MUST declare the tool-selection convention **inline** in exactly one place per `SKILL.md` — a `## User Input Tools` section near the top. Do NOT link out to [docs/user-input-tools.md](docs/user-input-tools.md); that doc is the author-side canonical source — copy its body into each SKILL.md. Concrete `AskUserQuestion` mentions elsewhere in a skill are treated as examples — other runtimes substitute their local equivalent under the rule.
|
||||
|
||||
## Image Generation Tools
|
||||
|
||||
Skills that render images MUST declare the backend-selection convention **inline** in exactly one place per `SKILL.md` — a `## Image Generation Tools` section near the top (after `## User Input Tools`). Do NOT link out to [docs/image-generation-tools.md](docs/image-generation-tools.md); that doc is the author-side canonical source — copy its body into each SKILL.md. Concrete tool names (`imagegen`, `image_generate`, `baoyu-imagine`) elsewhere in a skill are treated as examples — other runtimes substitute their local equivalent under the rule. The rule is stateless: use whatever backend is available; if multiple, ask the user once; if none, ask how to proceed. Every rendered image's full prompt must be written to a standalone `prompts/NN-*.md` file before any backend is invoked. Backend skills (`baoyu-imagine`, `baoyu-image-gen`, `baoyu-danger-gemini-web`) are exempt — they render directly rather than selecting a backend.
|
||||
|
||||
## Deprecated Skills
|
||||
|
||||
| Skill | Note |
|
||||
|-------|------|
|
||||
| `baoyu-image-gen` | Migrated to `baoyu-imagine`. Do NOT add to `.claude-plugin/marketplace.json`. Do NOT update README for this skill. |
|
||||
| `baoyu-xhs-images` | Migrated to `baoyu-image-cards`. Do NOT add to `.claude-plugin/marketplace.json`. Do NOT update README for this skill. |
|
||||
| `baoyu-image-gen` | Superseded by `baoyu-imagine`. Not in `.claude-plugin/marketplace.json`. Kept functional — sync any cross-cutting changes with `baoyu-imagine`. |
|
||||
| `baoyu-xhs-images` | Superseded by `baoyu-image-cards`. Not in `.claude-plugin/marketplace.json`. Kept functional — sync any cross-cutting changes with `baoyu-image-cards`. Do NOT update README for this skill. |
|
||||
|
||||
## Release Process
|
||||
|
||||
@@ -77,7 +93,9 @@ All skills MUST use `baoyu-` prefix. Details: [docs/creating-skills.md](docs/cre
|
||||
|
||||
| Topic | File |
|
||||
|-------|------|
|
||||
| Image generation guidelines | [docs/image-generation.md](docs/image-generation.md) |
|
||||
| Image generation output guidelines | [docs/image-generation.md](docs/image-generation.md) |
|
||||
| Image generation backend selection | [docs/image-generation-tools.md](docs/image-generation-tools.md) |
|
||||
| User input tool convention | [docs/user-input-tools.md](docs/user-input-tools.md) |
|
||||
| Chrome profile platform paths | [docs/chrome-profile.md](docs/chrome-profile.md) |
|
||||
| Comic style maintenance | [docs/comic-style-maintenance.md](docs/comic-style-maintenance.md) |
|
||||
| ClawHub/OpenClaw publishing | [docs/publishing.md](docs/publishing.md) |
|
||||
|
||||
@@ -1264,6 +1264,7 @@ This project was inspired by and builds upon the following open source projects:
|
||||
- [doocs/md](https://github.com/doocs/md) by [@doocs](https://github.com/doocs) — Core implementation logic for Markdown to HTML conversion
|
||||
- [High-density Infographic Prompt](https://waytoagi.feishu.cn/wiki/YG0zwalijihRREkgmPzcWRInnUg) by AJ@WaytoAGI — Inspiration for the infographic skill
|
||||
- [qiaomu-mondo-poster-design](https://github.com/joeseesun/qiaomu-mondo-poster-design) by [@joeseesun](https://github.com/joeseesun)(乔木) — Inspiration for the Mondo style
|
||||
- [architecture-diagram-generator](https://github.com/Cocoon-AI/architecture-diagram-generator) by [@Cocoon-AI](https://github.com/Cocoon-AI) — Inspiration for the diagram skill's design system
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1264,6 +1264,7 @@ HTTP_PROXY=http://127.0.0.1:7890 HTTPS_PROXY=http://127.0.0.1:7890 /baoyu-danger
|
||||
- [doocs/md](https://github.com/doocs/md) by [@doocs](https://github.com/doocs) — Markdown 转 HTML 的核心实现逻辑
|
||||
- [高密度信息图 Prompt](https://waytoagi.feishu.cn/wiki/YG0zwalijihRREkgmPzcWRInnUg) by AJ@WaytoAGI — 信息图技能的灵感来源
|
||||
- [qiaomu-mondo-poster-design](https://github.com/joeseesun/qiaomu-mondo-poster-design) by [@joeseesun](https://github.com/joeseesun)(乔木) — Mondo 风格的灵感来源
|
||||
- [architecture-diagram-generator](https://github.com/Cocoon-AI/architecture-diagram-generator) by [@Cocoon-AI](https://github.com/Cocoon-AI) — 图表技能设计体系的灵感来源
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
+39
-1
@@ -127,7 +127,7 @@ test -f "$HOME/.baoyu-skills/<skill-name>/EXTEND.md" && echo "user"
|
||||
| Result | Action |
|
||||
|--------|--------|
|
||||
| Found | Read, parse, display summary |
|
||||
| Not found | Ask user with AskUserQuestion |
|
||||
| Not found | Ask user via the runtime's user-input tool (see [user-input-tools.md](user-input-tools.md)) |
|
||||
```
|
||||
|
||||
End of SKILL.md should include:
|
||||
@@ -135,3 +135,41 @@ End of SKILL.md should include:
|
||||
## Extension Support
|
||||
Custom configurations via EXTEND.md. See **Step 1.1** for paths and supported options.
|
||||
```
|
||||
|
||||
## User Input Tools Section (Required)
|
||||
|
||||
Every SKILL.md that prompts the user for choices MUST include exactly one `## User Input Tools` section near the top (right after the intro, before the main workflow). The rule must be **inlined** — do NOT link to `docs/user-input-tools.md` (skills are self-contained; see [CLAUDE.md → Skill Self-Containment](../CLAUDE.md)). The author-side canonical reference lives at [user-input-tools.md](user-input-tools.md); copy its body into each new SKILL.md.
|
||||
|
||||
Standard snippet (copy verbatim):
|
||||
|
||||
```markdown
|
||||
## User Input Tools
|
||||
|
||||
When this skill prompts the user, follow this tool-selection rule (priority order):
|
||||
|
||||
1. **Prefer built-in user-input tools** exposed by the current agent runtime — e.g., `AskUserQuestion`, `request_user_input`, `clarify`, `ask_user`, or any equivalent.
|
||||
2. **Fallback**: if no such tool exists, emit a numbered plain-text message and ask the user to reply with the chosen number/answer for each question.
|
||||
3. **Batching**: if the tool supports multiple questions per call, combine all applicable questions into a single call; if only single-question, ask them one at a time in priority order.
|
||||
|
||||
Concrete `AskUserQuestion` references below are examples — substitute the local equivalent in other runtimes.
|
||||
```
|
||||
|
||||
## Image Generation Tools Section (Required for image-gen skills)
|
||||
|
||||
Every SKILL.md that renders images — whether by calling an image-generation API directly or by delegating to another skill — MUST include exactly one `## Image Generation Tools` section near the top (after `## User Input Tools`, before the main workflow). The rule must be **inlined** — do NOT link to `docs/image-generation-tools.md` (skills are self-contained; see [CLAUDE.md → Skill Self-Containment](../CLAUDE.md)). The author-side canonical reference lives at [image-generation-tools.md](image-generation-tools.md); copy its body into each new SKILL.md.
|
||||
|
||||
Standard snippet (copy verbatim):
|
||||
|
||||
```markdown
|
||||
## Image Generation Tools
|
||||
|
||||
When this skill needs to render an image:
|
||||
|
||||
- **Use whatever image-generation tool or skill is available** in the current runtime — e.g., Codex `imagegen`, Hermes `image_generate`, `baoyu-imagine`, or any equivalent the user has installed.
|
||||
- **If multiple are available**, ask the user **once** at the start which to use (batch with any other initial questions).
|
||||
- **If none are available**, tell the user and ask how to proceed.
|
||||
|
||||
**Prompt file requirement (hard)**: write each image's full, final prompt to a standalone file under `prompts/` (naming: `NN-{type}-[slug].md`) BEFORE invoking any backend. The backend receives the prompt file (or its content); the file is the reproducibility record and lets you switch backends without regenerating prompts.
|
||||
|
||||
Concrete tool names (`imagegen`, `image_generate`, `baoyu-imagine`) above are examples — substitute the local equivalents under the same rule.
|
||||
```
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Image Generation Tools
|
||||
|
||||
Skills in this repo are loaded by multiple agent runtimes (Claude Code, Codex, Hermes, other agents, bare CLI). Each runtime exposes a different image-generation capability — some have a runtime-native tool (Codex `imagegen`, Hermes `image_generate`), others rely on an installed skill (`baoyu-imagine`, or user-defined). This document defines the canonical **backend-selection rule** every skill that renders images follows so skills stay portable.
|
||||
|
||||
## The Rule
|
||||
|
||||
When a skill needs to render an image:
|
||||
|
||||
- **Use whatever image-generation tool or skill is available** in the current runtime — e.g., Codex `imagegen`, Hermes `image_generate`, `baoyu-imagine`, or any equivalent the user has installed.
|
||||
- **If multiple are available**, ask the user **once** at the start which to use (batch with any other initial questions).
|
||||
- **If none are available**, tell the user and ask how to proceed.
|
||||
|
||||
No explicit priority between runtime-native tools and repo skills — treat them equivalently and let the user decide when there's a choice. No persisted preference mechanism; the question is cheap, and the rule is stateless.
|
||||
|
||||
## Prompt File Requirement (hard)
|
||||
|
||||
Regardless of which backend is chosen, every skill that renders images MUST write each image's full, final prompt to a standalone file under `prompts/` (naming: `NN-{type}-[slug].md`) BEFORE invoking any backend. The backend receives the prompt file (or its content); the file is the reproducibility record and allows switching backends without regenerating prompts.
|
||||
|
||||
## How Skills Declare This
|
||||
|
||||
Each `SKILL.md` that renders images includes **exactly one** `## Image Generation Tools` section (near the top, after `## User Input Tools` and before the main workflow) that **inlines** this rule. Skills are self-contained and cannot link to `docs/` — each skill folder must ship the rule inside its own `SKILL.md`. See [CLAUDE.md → Skill Self-Containment](../CLAUDE.md).
|
||||
|
||||
Concrete tool names (`imagegen`, `image_generate`, `baoyu-imagine`) in this document and in SKILL.md are **examples** — agents in other runtimes apply the rule above and substitute the local equivalent. Skill-specific parameters for these backends are illustrative; runtimes without those knobs can omit them.
|
||||
|
||||
## Backend Skills Are Exempt
|
||||
|
||||
Skills that **are themselves** image-generation backends — currently `baoyu-imagine`, `baoyu-image-gen` (deprecated), and `baoyu-danger-gemini-web` — do NOT include a `## Image Generation Tools` section. They render directly via their own provider integrations and have no need to "select a backend." The rule applies only to consumer skills that delegate rendering to whatever backend the runtime exposes.
|
||||
+15
-14
@@ -1,30 +1,31 @@
|
||||
# Image Generation Guidelines
|
||||
|
||||
Skills that require image generation MUST delegate to available image generation skills.
|
||||
Skills that require image generation MUST delegate to available image generation tools (runtime-native tools or installed skills).
|
||||
|
||||
**Backend selection convention**: see [image-generation-tools.md](image-generation-tools.md) for the runtime-neutral rule. Short version: use whatever backend is available; if multiple, ask the user once; if none, ask how to proceed. This document covers output conventions (naming, paths) that apply regardless of which backend is selected.
|
||||
|
||||
## Skill Selection
|
||||
|
||||
**Default**: `skills/baoyu-imagine/SKILL.md` (unless user specifies otherwise).
|
||||
|
||||
1. Read skill's SKILL.md for parameters and capabilities
|
||||
2. If user requests different skill, check `skills/` for alternatives
|
||||
3. Only ask user when multiple viable options exist
|
||||
1. Follow the rule in [image-generation-tools.md](image-generation-tools.md): use whatever backend is available; ask only on ambiguity.
|
||||
2. Read the chosen backend's documentation for parameters and capabilities.
|
||||
3. If user requests a specific backend, honor it.
|
||||
|
||||
## Generation Flow Template
|
||||
|
||||
```markdown
|
||||
### Step N: Generate Images
|
||||
|
||||
**Skill Selection**:
|
||||
1. Check available skills (`baoyu-imagine` default, or `baoyu-danger-gemini-web`)
|
||||
2. Read selected skill's SKILL.md for parameters
|
||||
3. If multiple skills available, ask user to choose
|
||||
**Backend Selection**:
|
||||
1. Detect available image-generation tools/skills (runtime-native + installed)
|
||||
2. If one available → use it. If multiple → ask user once. If none → ask how to proceed.
|
||||
3. Read the chosen backend's docs for parameters
|
||||
|
||||
**Generation Flow**:
|
||||
1. Call skill with prompt, output path, and skill-specific parameters
|
||||
2. Generate sequentially by default (batch parallel only when user has multiple prompts)
|
||||
3. Output progress: "Generated X/N"
|
||||
4. On failure, auto-retry once before reporting error
|
||||
1. Write the full prompt to `prompts/NN-{type}-[slug].md` BEFORE invoking the backend
|
||||
2. Call backend with the prompt (or prompt file), output path, and parameters
|
||||
3. Generate sequentially by default (batch parallel only when backend supports it and user has multiple prompts)
|
||||
4. Output progress: "Generated X/N"
|
||||
5. On failure, auto-retry once before reporting error
|
||||
```
|
||||
|
||||
**Batch Parallel** (`baoyu-imagine` only): concurrent workers with per-provider throttling via `batch.max_workers` in EXTEND.md.
|
||||
|
||||
+10
-7
@@ -21,21 +21,24 @@ bash scripts/sync-clawhub.sh # sync all skills
|
||||
bash scripts/sync-clawhub.sh <skill> # sync one skill
|
||||
```
|
||||
|
||||
Release hooks are configured via `.releaserc.yml`. This repo does not stage a separate release directory: release prep only syncs `packages/` into each skill's committed `scripts/vendor/`, and publish reads the skill directory directly.
|
||||
Release hooks are configured via `.releaserc.yml`. This repo does not stage a separate release directory: release prep verifies that skills depend on published npm package versions, and publish reads the skill directory directly.
|
||||
|
||||
## Shared Workspace Packages
|
||||
|
||||
`packages/` is the **only** source of truth. Never edit `skills/*/scripts/vendor/` directly.
|
||||
`packages/` is the **only** source of truth for shared runtime code. Publish shared packages to npm and reference them from skill script `package.json` files with semver ranges. Do not vendor shared packages into `skills/*/scripts/vendor/`.
|
||||
|
||||
Current packages:
|
||||
- `baoyu-chrome-cdp` (Chrome CDP utilities), consumed by 6 skills (`baoyu-danger-gemini-web`, `baoyu-danger-x-to-markdown`, `baoyu-post-to-wechat`, `baoyu-post-to-weibo`, `baoyu-post-to-x`, `baoyu-url-to-markdown`)
|
||||
- `baoyu-chrome-cdp` (Chrome CDP utilities), consumed by 5 skills (`baoyu-danger-gemini-web`, `baoyu-danger-x-to-markdown`, `baoyu-post-to-wechat`, `baoyu-post-to-weibo`, `baoyu-post-to-x`)
|
||||
- `baoyu-md` (shared Markdown rendering and placeholder pipeline), consumed by 3 skills (`baoyu-markdown-to-html`, `baoyu-post-to-wechat`, `baoyu-post-to-weibo`)
|
||||
- `baoyu-fetch` (URL-to-Markdown CLI), consumed by 1 skill (`baoyu-url-to-markdown`)
|
||||
|
||||
**How it works**: Sync script copies packages into each consuming skill's `vendor/` directory and rewrites dependency specs to `file:./vendor/<name>`. Vendor copies are committed to git, making skills self-contained.
|
||||
**How it works**: npm packages are built from `packages/` and published to the public npm registry. Skills depend on those packages with `^<version>` specs. Release prep runs `node scripts/verify-shared-package-deps.mjs` so `file:` dependencies and vendored workspace packages cannot slip back in.
|
||||
|
||||
**Update workflow**:
|
||||
1. Edit package under `packages/`
|
||||
2. Run `node scripts/sync-shared-skill-packages.mjs`
|
||||
3. Commit synced `vendor/`, `package.json`, and `bun.lock` together
|
||||
2. Run the package build, e.g. `bun run --cwd packages/baoyu-md build`
|
||||
3. Publish the changed npm package with `npm publish --access public`
|
||||
4. Update consuming skill `package.json` semver ranges if the package version changed
|
||||
5. Run `node scripts/verify-shared-package-deps.mjs`
|
||||
|
||||
**Git hook**: Run `node scripts/install-git-hooks.mjs` once to enable the `pre-push` hook. It re-syncs and blocks push if vendor copies are stale (`--enforce-clean`).
|
||||
**Git hook**: Run `node scripts/install-git-hooks.mjs` once to enable the `pre-push` hook. It blocks pushes when a skill uses a local `file:` dependency or a vendored workspace package.
|
||||
|
||||
+4
-4
@@ -9,7 +9,7 @@ This repository has many scripts, but they do not share a single runtime or depe
|
||||
- Coverage command: `npm run test:coverage`
|
||||
- CI trigger: GitHub Actions on `push`, `pull_request`, and manual dispatch
|
||||
|
||||
This avoids introducing Jest/Vitest across a repo that already mixes plain Node scripts, Bun-based skill packages, vendored code, and browser automation.
|
||||
This avoids introducing Jest/Vitest across a repo that already mixes plain Node scripts, Bun-based skill packages, npm-published shared packages, and browser automation.
|
||||
|
||||
## Rollout Plan
|
||||
|
||||
@@ -18,19 +18,19 @@ This avoids introducing Jest/Vitest across a repo that already mixes plain Node
|
||||
Focus on pure functions under `scripts/lib/` first.
|
||||
|
||||
- `scripts/lib/release-files.mjs`
|
||||
- `scripts/lib/shared-skill-packages.mjs`
|
||||
- `scripts/verify-shared-package-deps.mjs`
|
||||
|
||||
Goals:
|
||||
|
||||
- Validate file filtering and release packaging rules
|
||||
- Catch regressions in package vendoring and dependency rewriting
|
||||
- Catch regressions that reintroduce local `file:` dependencies or vendored workspace packages
|
||||
- Keep tests deterministic and free of network, Bun, or browser requirements
|
||||
|
||||
### Phase 2: Root CLI integration tests
|
||||
|
||||
Add temp-directory integration tests for root CLIs that already support dry-run or local-only flows.
|
||||
|
||||
- `scripts/sync-shared-skill-packages.mjs`
|
||||
- `scripts/verify-shared-package-deps.mjs`
|
||||
- `scripts/publish-skill.mjs --dry-run`
|
||||
- `scripts/sync-clawhub.mjs` argument handling and local skill discovery
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# User Input Tools
|
||||
|
||||
Skills in this repo are loaded by multiple agent runtimes (Claude Code, other agents, bare CLI). Each runtime exposes a different API for asking the user questions. This document defines the canonical **tool-selection rule** every skill follows so skills stay portable.
|
||||
|
||||
## Tool Selection (priority order)
|
||||
|
||||
1. **Prefer built-in user-input tools** if the current agent runtime exposes one — e.g., `AskUserQuestion`, `request_user_input`, `clarify`, `ask_user`, or any equivalent.
|
||||
2. **Fallback to plain text**: if no such tool exists, emit a numbered plain-text message and ask the user to reply with the chosen number/answer for each question.
|
||||
3. **Batching rule**:
|
||||
- If the tool supports **multiple questions per call** (e.g., `AskUserQuestion`): **Combine all applicable questions into a single call. Do NOT split into separate calls.**
|
||||
- If the tool supports **only one question per call** (e.g., single-prompt `clarify`): ask **one question per call, in priority order**.
|
||||
|
||||
## How Skills Declare This
|
||||
|
||||
Each `SKILL.md` that uses interactive user input includes **exactly one** `## User Input Tools` section (typically near the top, right after the intro) that **inlines** this rule. Do NOT link here from a SKILL.md — skills are self-contained (see [CLAUDE.md → Skill Self-Containment](../CLAUDE.md)). This document is the author-side canonical source; copy its body into each SKILL.md. The rule then governs every user-input interaction in that skill and its `references/` files.
|
||||
|
||||
Specific mentions of a concrete tool (e.g., `AskUserQuestion`) elsewhere in a skill are **concrete examples** — agents in other runtimes apply the rule above and substitute the local equivalent. Tool-specific parameters (e.g., `header:`, `multiSelect:`) are illustrative; runtimes without those knobs can omit them.
|
||||
Generated
+594
-5
@@ -10,7 +10,8 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pptxgenjs": "^4.0.1"
|
||||
"pptxgenjs": "^4.0.1",
|
||||
"sharp": "^0.34.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
@@ -415,6 +416,23 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
|
||||
@@ -857,6 +875,519 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/colour": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||||
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
||||
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
|
||||
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
|
||||
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
|
||||
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
||||
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-ppc64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
|
||||
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-riscv64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
|
||||
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-riscv64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
|
||||
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
||||
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
|
||||
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/external-editor": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz",
|
||||
@@ -1479,6 +2010,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/devlop": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
|
||||
@@ -3749,7 +4289,6 @@
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -3764,6 +4303,50 @@
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@img/colour": "^1.0.0",
|
||||
"detect-libc": "^2.1.2",
|
||||
"semver": "^7.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.34.5",
|
||||
"@img/sharp-darwin-x64": "0.34.5",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-riscv64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.4",
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
|
||||
"@img/sharp-linux-arm": "0.34.5",
|
||||
"@img/sharp-linux-arm64": "0.34.5",
|
||||
"@img/sharp-linux-ppc64": "0.34.5",
|
||||
"@img/sharp-linux-riscv64": "0.34.5",
|
||||
"@img/sharp-linux-s390x": "0.34.5",
|
||||
"@img/sharp-linux-x64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-arm64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-x64": "0.34.5",
|
||||
"@img/sharp-wasm32": "0.34.5",
|
||||
"@img/sharp-win32-arm64": "0.34.5",
|
||||
"@img/sharp-win32-ia32": "0.34.5",
|
||||
"@img/sharp-win32-x64": "0.34.5"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@@ -4301,10 +4884,13 @@
|
||||
}
|
||||
},
|
||||
"packages/baoyu-chrome-cdp": {
|
||||
"version": "0.1.0"
|
||||
"version": "0.1.0",
|
||||
"engines": {
|
||||
"bun": ">=1.2.0"
|
||||
}
|
||||
},
|
||||
"packages/baoyu-fetch": {
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"dependencies": {
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"chrome-launcher": "^1.2.1",
|
||||
@@ -4318,7 +4904,7 @@
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"bin": {
|
||||
"baoyu-fetch": "src/cli.ts"
|
||||
"baoyu-fetch": "dist/cli.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.30.0",
|
||||
@@ -4344,6 +4930,9 @@
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-stringify": "^11.0.0",
|
||||
"unified": "^11.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"bun": ">=1.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -18,6 +18,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pptxgenjs": "^4.0.1"
|
||||
"pptxgenjs": "^4.0.1",
|
||||
"sharp": "^0.34.5"
|
||||
}
|
||||
}
|
||||
|
||||
+479
@@ -0,0 +1,479 @@
|
||||
var __create = Object.create;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
function __accessProp(key) {
|
||||
return this[key];
|
||||
}
|
||||
var __toESMCache_node;
|
||||
var __toESMCache_esm;
|
||||
var __toESM = (mod, isNodeMode, target) => {
|
||||
var canCache = mod != null && typeof mod === "object";
|
||||
if (canCache) {
|
||||
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
||||
var cached = cache.get(mod);
|
||||
if (cached)
|
||||
return cached;
|
||||
}
|
||||
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
||||
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
||||
for (let key of __getOwnPropNames(mod))
|
||||
if (!__hasOwnProp.call(to, key))
|
||||
__defProp(to, key, {
|
||||
get: __accessProp.bind(mod, key),
|
||||
enumerable: true
|
||||
});
|
||||
if (canCache)
|
||||
cache.set(mod, to);
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (from) => {
|
||||
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
||||
if (entry)
|
||||
return entry;
|
||||
entry = __defProp({}, "__esModule", { value: true });
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (var key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(entry, key))
|
||||
__defProp(entry, key, {
|
||||
get: __accessProp.bind(from, key),
|
||||
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
||||
});
|
||||
}
|
||||
__moduleCache.set(from, entry);
|
||||
return entry;
|
||||
};
|
||||
var __moduleCache;
|
||||
var __returnValue = (v) => v;
|
||||
function __exportSetter(name, newValue) {
|
||||
this[name] = __returnValue.bind(null, newValue);
|
||||
}
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, {
|
||||
get: all[name],
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
set: __exportSetter.bind(all, name)
|
||||
});
|
||||
};
|
||||
|
||||
// src/index.ts
|
||||
var exports_src = {};
|
||||
__export(exports_src, {
|
||||
waitForChromeDebugPort: () => waitForChromeDebugPort,
|
||||
sleep: () => sleep,
|
||||
resolveSharedChromeProfileDir: () => resolveSharedChromeProfileDir,
|
||||
openPageSession: () => openPageSession,
|
||||
launchChrome: () => launchChrome,
|
||||
killChrome: () => killChrome,
|
||||
gracefulKillChrome: () => gracefulKillChrome,
|
||||
getFreePort: () => getFreePort,
|
||||
getDefaultChromeUserDataDirs: () => getDefaultChromeUserDataDirs,
|
||||
findExistingChromeDebugPort: () => findExistingChromeDebugPort,
|
||||
findChromeExecutable: () => findChromeExecutable,
|
||||
discoverRunningChromeDebugPort: () => discoverRunningChromeDebugPort,
|
||||
CdpConnection: () => CdpConnection
|
||||
});
|
||||
module.exports = __toCommonJS(exports_src);
|
||||
var import_node_child_process = require("node:child_process");
|
||||
var import_node_fs = __toESM(require("node:fs"));
|
||||
var import_node_net = __toESM(require("node:net"));
|
||||
var import_node_os = __toESM(require("node:os"));
|
||||
var import_node_path = __toESM(require("node:path"));
|
||||
var import_node_process = __toESM(require("node:process"));
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
async function getFreePort(fixedEnvName) {
|
||||
const fixed = fixedEnvName ? Number.parseInt(import_node_process.default.env[fixedEnvName] ?? "", 10) : NaN;
|
||||
if (Number.isInteger(fixed) && fixed > 0)
|
||||
return fixed;
|
||||
return await new Promise((resolve, reject) => {
|
||||
const server = import_node_net.default.createServer();
|
||||
server.unref();
|
||||
server.on("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
server.close(() => reject(new Error("Unable to allocate a free TCP port.")));
|
||||
return;
|
||||
}
|
||||
const port = address.port;
|
||||
server.close((err) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve(port);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
function findChromeExecutable(options) {
|
||||
for (const envName of options.envNames ?? []) {
|
||||
const override = import_node_process.default.env[envName]?.trim();
|
||||
if (override && import_node_fs.default.existsSync(override))
|
||||
return override;
|
||||
}
|
||||
const candidates = import_node_process.default.platform === "darwin" ? options.candidates.darwin ?? options.candidates.default : import_node_process.default.platform === "win32" ? options.candidates.win32 ?? options.candidates.default : options.candidates.default;
|
||||
for (const candidate of candidates) {
|
||||
if (import_node_fs.default.existsSync(candidate))
|
||||
return candidate;
|
||||
}
|
||||
return;
|
||||
}
|
||||
function resolveSharedChromeProfileDir(options = {}) {
|
||||
for (const envName of options.envNames ?? []) {
|
||||
const override = import_node_process.default.env[envName]?.trim();
|
||||
if (override)
|
||||
return import_node_path.default.resolve(override);
|
||||
}
|
||||
const appDataDirName = options.appDataDirName ?? "baoyu-skills";
|
||||
const profileDirName = options.profileDirName ?? "chrome-profile";
|
||||
if (options.wslWindowsHome) {
|
||||
return import_node_path.default.join(options.wslWindowsHome, ".local", "share", appDataDirName, profileDirName);
|
||||
}
|
||||
const base = import_node_process.default.platform === "darwin" ? import_node_path.default.join(import_node_os.default.homedir(), "Library", "Application Support") : import_node_process.default.platform === "win32" ? import_node_process.default.env.APPDATA ?? import_node_path.default.join(import_node_os.default.homedir(), "AppData", "Roaming") : import_node_process.default.env.XDG_DATA_HOME ?? import_node_path.default.join(import_node_os.default.homedir(), ".local", "share");
|
||||
return import_node_path.default.join(base, appDataDirName, profileDirName);
|
||||
}
|
||||
async function fetchWithTimeout(url, timeoutMs) {
|
||||
if (!timeoutMs || timeoutMs <= 0)
|
||||
return await fetch(url, { redirect: "follow" });
|
||||
const ctl = new AbortController;
|
||||
const timer = setTimeout(() => ctl.abort(), timeoutMs);
|
||||
try {
|
||||
return await fetch(url, { redirect: "follow", signal: ctl.signal });
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
async function fetchJson(url, options = {}) {
|
||||
const response = await fetchWithTimeout(url, options.timeoutMs);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
async function isDebugPortReady(port, timeoutMs = 3000) {
|
||||
try {
|
||||
const version = await fetchJson(`http://127.0.0.1:${port}/json/version`, { timeoutMs });
|
||||
return !!version.webSocketDebuggerUrl;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function isPortListening(port, timeoutMs = 3000) {
|
||||
return new Promise((resolve) => {
|
||||
const socket = new import_node_net.default.Socket;
|
||||
const timer = setTimeout(() => {
|
||||
socket.destroy();
|
||||
resolve(false);
|
||||
}, timeoutMs);
|
||||
socket.once("connect", () => {
|
||||
clearTimeout(timer);
|
||||
socket.destroy();
|
||||
resolve(true);
|
||||
});
|
||||
socket.once("error", () => {
|
||||
clearTimeout(timer);
|
||||
resolve(false);
|
||||
});
|
||||
socket.connect(port, "127.0.0.1");
|
||||
});
|
||||
}
|
||||
function parseDevToolsActivePort(filePath) {
|
||||
try {
|
||||
const content = import_node_fs.default.readFileSync(filePath, "utf-8");
|
||||
const lines = content.split(/\r?\n/);
|
||||
const port = Number.parseInt(lines[0]?.trim() ?? "", 10);
|
||||
const wsPath = lines[1]?.trim();
|
||||
if (port > 0 && wsPath)
|
||||
return { port, wsPath };
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
async function findExistingChromeDebugPort(options) {
|
||||
const timeoutMs = options.timeoutMs ?? 3000;
|
||||
const parsed = parseDevToolsActivePort(import_node_path.default.join(options.profileDir, "DevToolsActivePort"));
|
||||
if (parsed && parsed.port > 0 && await isDebugPortReady(parsed.port, timeoutMs))
|
||||
return parsed.port;
|
||||
if (import_node_process.default.platform === "win32")
|
||||
return null;
|
||||
try {
|
||||
const result = import_node_child_process.spawnSync("ps", ["aux"], { encoding: "utf-8", timeout: 5000 });
|
||||
if (result.status !== 0 || !result.stdout)
|
||||
return null;
|
||||
const lines = result.stdout.split(`
|
||||
`).filter((line) => line.includes(options.profileDir) && line.includes("--remote-debugging-port="));
|
||||
for (const line of lines) {
|
||||
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
||||
const port = Number.parseInt(portMatch?.[1] ?? "", 10);
|
||||
if (port > 0 && await isDebugPortReady(port, timeoutMs))
|
||||
return port;
|
||||
}
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
function getDefaultChromeUserDataDirs(channels = ["stable"]) {
|
||||
const home = import_node_os.default.homedir();
|
||||
const dirs = [];
|
||||
const channelDirs = {
|
||||
stable: {
|
||||
darwin: import_node_path.default.join(home, "Library", "Application Support", "Google", "Chrome"),
|
||||
linux: import_node_path.default.join(home, ".config", "google-chrome"),
|
||||
win32: import_node_path.default.join(import_node_process.default.env.LOCALAPPDATA ?? import_node_path.default.join(home, "AppData", "Local"), "Google", "Chrome", "User Data")
|
||||
},
|
||||
beta: {
|
||||
darwin: import_node_path.default.join(home, "Library", "Application Support", "Google", "Chrome Beta"),
|
||||
linux: import_node_path.default.join(home, ".config", "google-chrome-beta"),
|
||||
win32: import_node_path.default.join(import_node_process.default.env.LOCALAPPDATA ?? import_node_path.default.join(home, "AppData", "Local"), "Google", "Chrome Beta", "User Data")
|
||||
},
|
||||
canary: {
|
||||
darwin: import_node_path.default.join(home, "Library", "Application Support", "Google", "Chrome Canary"),
|
||||
linux: import_node_path.default.join(home, ".config", "google-chrome-canary"),
|
||||
win32: import_node_path.default.join(import_node_process.default.env.LOCALAPPDATA ?? import_node_path.default.join(home, "AppData", "Local"), "Google", "Chrome SxS", "User Data")
|
||||
},
|
||||
dev: {
|
||||
darwin: import_node_path.default.join(home, "Library", "Application Support", "Google", "Chrome Dev"),
|
||||
linux: import_node_path.default.join(home, ".config", "google-chrome-dev"),
|
||||
win32: import_node_path.default.join(import_node_process.default.env.LOCALAPPDATA ?? import_node_path.default.join(home, "AppData", "Local"), "Google", "Chrome Dev", "User Data")
|
||||
}
|
||||
};
|
||||
const platform = import_node_process.default.platform === "darwin" ? "darwin" : import_node_process.default.platform === "win32" ? "win32" : "linux";
|
||||
for (const ch of channels) {
|
||||
const entry = channelDirs[ch];
|
||||
if (entry)
|
||||
dirs.push(entry[platform]);
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
async function discoverRunningChromeDebugPort(options = {}) {
|
||||
const channels = options.channels ?? ["stable", "beta", "canary", "dev"];
|
||||
const timeoutMs = options.timeoutMs ?? 3000;
|
||||
const userDataDirs = (options.userDataDirs ?? getDefaultChromeUserDataDirs(channels)).map((dir) => import_node_path.default.resolve(dir));
|
||||
for (const dir of userDataDirs) {
|
||||
const parsed = parseDevToolsActivePort(import_node_path.default.join(dir, "DevToolsActivePort"));
|
||||
if (!parsed)
|
||||
continue;
|
||||
if (await isPortListening(parsed.port, timeoutMs)) {
|
||||
return { port: parsed.port, wsUrl: `ws://127.0.0.1:${parsed.port}${parsed.wsPath}` };
|
||||
}
|
||||
}
|
||||
if (import_node_process.default.platform !== "win32") {
|
||||
try {
|
||||
const result = import_node_child_process.spawnSync("ps", ["aux"], { encoding: "utf-8", timeout: 5000 });
|
||||
if (result.status === 0 && result.stdout) {
|
||||
const lines = result.stdout.split(`
|
||||
`).filter((line) => line.includes("--remote-debugging-port=") && userDataDirs.some((dir) => line.includes(dir)));
|
||||
for (const line of lines) {
|
||||
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
||||
const port = Number.parseInt(portMatch?.[1] ?? "", 10);
|
||||
if (port > 0 && await isDebugPortReady(port, timeoutMs)) {
|
||||
try {
|
||||
const version = await fetchJson(`http://127.0.0.1:${port}/json/version`, { timeoutMs });
|
||||
if (version.webSocketDebuggerUrl)
|
||||
return { port, wsUrl: version.webSocketDebuggerUrl };
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async function waitForChromeDebugPort(port, timeoutMs, options) {
|
||||
const start = Date.now();
|
||||
let lastError = null;
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
try {
|
||||
const version = await fetchJson(`http://127.0.0.1:${port}/json/version`, { timeoutMs: 5000 });
|
||||
if (version.webSocketDebuggerUrl)
|
||||
return version.webSocketDebuggerUrl;
|
||||
lastError = new Error("Missing webSocketDebuggerUrl");
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
}
|
||||
await sleep(200);
|
||||
}
|
||||
if (options?.includeLastError && lastError) {
|
||||
throw new Error(`Chrome debug port not ready: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
||||
}
|
||||
throw new Error("Chrome debug port not ready");
|
||||
}
|
||||
|
||||
class CdpConnection {
|
||||
ws;
|
||||
nextId = 0;
|
||||
pending = new Map;
|
||||
eventHandlers = new Map;
|
||||
defaultTimeoutMs;
|
||||
constructor(ws, defaultTimeoutMs = 15000) {
|
||||
this.ws = ws;
|
||||
this.defaultTimeoutMs = defaultTimeoutMs;
|
||||
this.ws.addEventListener("message", (event) => {
|
||||
try {
|
||||
const data = typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data);
|
||||
const msg = JSON.parse(data);
|
||||
if (msg.method) {
|
||||
const handlers = this.eventHandlers.get(msg.method);
|
||||
if (handlers) {
|
||||
handlers.forEach((handler) => handler(msg.params));
|
||||
}
|
||||
}
|
||||
if (msg.id) {
|
||||
const pending = this.pending.get(msg.id);
|
||||
if (pending) {
|
||||
this.pending.delete(msg.id);
|
||||
if (pending.timer)
|
||||
clearTimeout(pending.timer);
|
||||
if (msg.error?.message)
|
||||
pending.reject(new Error(msg.error.message));
|
||||
else
|
||||
pending.resolve(msg.result);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
this.ws.addEventListener("close", () => {
|
||||
for (const [id, pending] of this.pending.entries()) {
|
||||
this.pending.delete(id);
|
||||
if (pending.timer)
|
||||
clearTimeout(pending.timer);
|
||||
pending.reject(new Error("CDP connection closed."));
|
||||
}
|
||||
});
|
||||
}
|
||||
static async connect(url, timeoutMs, options) {
|
||||
const ws = new WebSocket(url);
|
||||
await new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error("CDP connection timeout.")), timeoutMs);
|
||||
ws.addEventListener("open", () => {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
});
|
||||
ws.addEventListener("error", () => {
|
||||
clearTimeout(timer);
|
||||
reject(new Error("CDP connection failed."));
|
||||
});
|
||||
});
|
||||
return new CdpConnection(ws, options?.defaultTimeoutMs ?? 15000);
|
||||
}
|
||||
on(method, handler) {
|
||||
if (!this.eventHandlers.has(method)) {
|
||||
this.eventHandlers.set(method, new Set);
|
||||
}
|
||||
this.eventHandlers.get(method)?.add(handler);
|
||||
}
|
||||
off(method, handler) {
|
||||
this.eventHandlers.get(method)?.delete(handler);
|
||||
}
|
||||
async send(method, params, options) {
|
||||
const id = ++this.nextId;
|
||||
const message = { id, method };
|
||||
if (params)
|
||||
message.params = params;
|
||||
if (options?.sessionId)
|
||||
message.sessionId = options.sessionId;
|
||||
const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
const timer = timeoutMs > 0 ? setTimeout(() => {
|
||||
this.pending.delete(id);
|
||||
reject(new Error(`CDP timeout: ${method}`));
|
||||
}, timeoutMs) : null;
|
||||
this.pending.set(id, { resolve, reject, timer });
|
||||
this.ws.send(JSON.stringify(message));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
close() {
|
||||
try {
|
||||
this.ws.close();
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
async function launchChrome(options) {
|
||||
await import_node_fs.default.promises.mkdir(options.profileDir, { recursive: true });
|
||||
const args = [
|
||||
`--remote-debugging-port=${options.port}`,
|
||||
`--user-data-dir=${options.profileDir}`,
|
||||
"--no-first-run",
|
||||
"--no-default-browser-check",
|
||||
...options.extraArgs ?? []
|
||||
];
|
||||
if (options.headless)
|
||||
args.push("--headless=new");
|
||||
if (options.url)
|
||||
args.push(options.url);
|
||||
return import_node_child_process.spawn(options.chromePath, args, { stdio: "ignore" });
|
||||
}
|
||||
function killChrome(chrome) {
|
||||
try {
|
||||
chrome.kill("SIGTERM");
|
||||
} catch {}
|
||||
setTimeout(() => {
|
||||
if (chrome.exitCode === null && chrome.signalCode === null) {
|
||||
try {
|
||||
chrome.kill("SIGKILL");
|
||||
} catch {}
|
||||
}
|
||||
}, 2000).unref?.();
|
||||
}
|
||||
async function gracefulKillChrome(chrome, port, timeoutMs = 6000) {
|
||||
if (chrome.exitCode !== null || chrome.signalCode !== null)
|
||||
return;
|
||||
const exitPromise = new Promise((resolve) => {
|
||||
chrome.once("exit", () => resolve());
|
||||
});
|
||||
killChrome(chrome);
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
if (chrome.exitCode !== null || chrome.signalCode !== null)
|
||||
return;
|
||||
if (port !== undefined && !await isPortListening(port, 250))
|
||||
return;
|
||||
const exited = await Promise.race([
|
||||
exitPromise.then(() => true),
|
||||
sleep(100).then(() => false)
|
||||
]);
|
||||
if (exited)
|
||||
return;
|
||||
}
|
||||
await Promise.race([
|
||||
exitPromise,
|
||||
sleep(250)
|
||||
]);
|
||||
}
|
||||
async function openPageSession(options) {
|
||||
let targetId;
|
||||
let createdTarget = false;
|
||||
if (options.reusing) {
|
||||
const created = await options.cdp.send("Target.createTarget", { url: options.url });
|
||||
targetId = created.targetId;
|
||||
createdTarget = true;
|
||||
} else {
|
||||
const targets = await options.cdp.send("Target.getTargets");
|
||||
const existing = targets.targetInfos.find(options.matchTarget);
|
||||
if (existing) {
|
||||
targetId = existing.targetId;
|
||||
} else {
|
||||
const created = await options.cdp.send("Target.createTarget", { url: options.url });
|
||||
targetId = created.targetId;
|
||||
createdTarget = true;
|
||||
}
|
||||
}
|
||||
const { sessionId } = await options.cdp.send("Target.attachToTarget", { targetId, flatten: true });
|
||||
if (options.activateTarget ?? true) {
|
||||
await options.cdp.send("Target.activateTarget", { targetId });
|
||||
}
|
||||
if (options.enablePage)
|
||||
await options.cdp.send("Page.enable", {}, { sessionId });
|
||||
if (options.enableRuntime)
|
||||
await options.cdp.send("Runtime.enable", {}, { sessionId });
|
||||
if (options.enableDom)
|
||||
await options.cdp.send("DOM.enable", {}, { sessionId });
|
||||
if (options.enableNetwork)
|
||||
await options.cdp.send("Network.enable", {}, { sessionId });
|
||||
return { sessionId, targetId, createdTarget };
|
||||
}
|
||||
+415
@@ -0,0 +1,415 @@
|
||||
// src/index.ts
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import net from "node:net";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
async function getFreePort(fixedEnvName) {
|
||||
const fixed = fixedEnvName ? Number.parseInt(process.env[fixedEnvName] ?? "", 10) : NaN;
|
||||
if (Number.isInteger(fixed) && fixed > 0)
|
||||
return fixed;
|
||||
return await new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
server.unref();
|
||||
server.on("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
server.close(() => reject(new Error("Unable to allocate a free TCP port.")));
|
||||
return;
|
||||
}
|
||||
const port = address.port;
|
||||
server.close((err) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve(port);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
function findChromeExecutable(options) {
|
||||
for (const envName of options.envNames ?? []) {
|
||||
const override = process.env[envName]?.trim();
|
||||
if (override && fs.existsSync(override))
|
||||
return override;
|
||||
}
|
||||
const candidates = process.platform === "darwin" ? options.candidates.darwin ?? options.candidates.default : process.platform === "win32" ? options.candidates.win32 ?? options.candidates.default : options.candidates.default;
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate))
|
||||
return candidate;
|
||||
}
|
||||
return;
|
||||
}
|
||||
function resolveSharedChromeProfileDir(options = {}) {
|
||||
for (const envName of options.envNames ?? []) {
|
||||
const override = process.env[envName]?.trim();
|
||||
if (override)
|
||||
return path.resolve(override);
|
||||
}
|
||||
const appDataDirName = options.appDataDirName ?? "baoyu-skills";
|
||||
const profileDirName = options.profileDirName ?? "chrome-profile";
|
||||
if (options.wslWindowsHome) {
|
||||
return path.join(options.wslWindowsHome, ".local", "share", appDataDirName, profileDirName);
|
||||
}
|
||||
const base = process.platform === "darwin" ? path.join(os.homedir(), "Library", "Application Support") : process.platform === "win32" ? process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming") : process.env.XDG_DATA_HOME ?? path.join(os.homedir(), ".local", "share");
|
||||
return path.join(base, appDataDirName, profileDirName);
|
||||
}
|
||||
async function fetchWithTimeout(url, timeoutMs) {
|
||||
if (!timeoutMs || timeoutMs <= 0)
|
||||
return await fetch(url, { redirect: "follow" });
|
||||
const ctl = new AbortController;
|
||||
const timer = setTimeout(() => ctl.abort(), timeoutMs);
|
||||
try {
|
||||
return await fetch(url, { redirect: "follow", signal: ctl.signal });
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
async function fetchJson(url, options = {}) {
|
||||
const response = await fetchWithTimeout(url, options.timeoutMs);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
async function isDebugPortReady(port, timeoutMs = 3000) {
|
||||
try {
|
||||
const version = await fetchJson(`http://127.0.0.1:${port}/json/version`, { timeoutMs });
|
||||
return !!version.webSocketDebuggerUrl;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function isPortListening(port, timeoutMs = 3000) {
|
||||
return new Promise((resolve) => {
|
||||
const socket = new net.Socket;
|
||||
const timer = setTimeout(() => {
|
||||
socket.destroy();
|
||||
resolve(false);
|
||||
}, timeoutMs);
|
||||
socket.once("connect", () => {
|
||||
clearTimeout(timer);
|
||||
socket.destroy();
|
||||
resolve(true);
|
||||
});
|
||||
socket.once("error", () => {
|
||||
clearTimeout(timer);
|
||||
resolve(false);
|
||||
});
|
||||
socket.connect(port, "127.0.0.1");
|
||||
});
|
||||
}
|
||||
function parseDevToolsActivePort(filePath) {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, "utf-8");
|
||||
const lines = content.split(/\r?\n/);
|
||||
const port = Number.parseInt(lines[0]?.trim() ?? "", 10);
|
||||
const wsPath = lines[1]?.trim();
|
||||
if (port > 0 && wsPath)
|
||||
return { port, wsPath };
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
async function findExistingChromeDebugPort(options) {
|
||||
const timeoutMs = options.timeoutMs ?? 3000;
|
||||
const parsed = parseDevToolsActivePort(path.join(options.profileDir, "DevToolsActivePort"));
|
||||
if (parsed && parsed.port > 0 && await isDebugPortReady(parsed.port, timeoutMs))
|
||||
return parsed.port;
|
||||
if (process.platform === "win32")
|
||||
return null;
|
||||
try {
|
||||
const result = spawnSync("ps", ["aux"], { encoding: "utf-8", timeout: 5000 });
|
||||
if (result.status !== 0 || !result.stdout)
|
||||
return null;
|
||||
const lines = result.stdout.split(`
|
||||
`).filter((line) => line.includes(options.profileDir) && line.includes("--remote-debugging-port="));
|
||||
for (const line of lines) {
|
||||
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
||||
const port = Number.parseInt(portMatch?.[1] ?? "", 10);
|
||||
if (port > 0 && await isDebugPortReady(port, timeoutMs))
|
||||
return port;
|
||||
}
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
function getDefaultChromeUserDataDirs(channels = ["stable"]) {
|
||||
const home = os.homedir();
|
||||
const dirs = [];
|
||||
const channelDirs = {
|
||||
stable: {
|
||||
darwin: path.join(home, "Library", "Application Support", "Google", "Chrome"),
|
||||
linux: path.join(home, ".config", "google-chrome"),
|
||||
win32: path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome", "User Data")
|
||||
},
|
||||
beta: {
|
||||
darwin: path.join(home, "Library", "Application Support", "Google", "Chrome Beta"),
|
||||
linux: path.join(home, ".config", "google-chrome-beta"),
|
||||
win32: path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome Beta", "User Data")
|
||||
},
|
||||
canary: {
|
||||
darwin: path.join(home, "Library", "Application Support", "Google", "Chrome Canary"),
|
||||
linux: path.join(home, ".config", "google-chrome-canary"),
|
||||
win32: path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome SxS", "User Data")
|
||||
},
|
||||
dev: {
|
||||
darwin: path.join(home, "Library", "Application Support", "Google", "Chrome Dev"),
|
||||
linux: path.join(home, ".config", "google-chrome-dev"),
|
||||
win32: path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome Dev", "User Data")
|
||||
}
|
||||
};
|
||||
const platform = process.platform === "darwin" ? "darwin" : process.platform === "win32" ? "win32" : "linux";
|
||||
for (const ch of channels) {
|
||||
const entry = channelDirs[ch];
|
||||
if (entry)
|
||||
dirs.push(entry[platform]);
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
async function discoverRunningChromeDebugPort(options = {}) {
|
||||
const channels = options.channels ?? ["stable", "beta", "canary", "dev"];
|
||||
const timeoutMs = options.timeoutMs ?? 3000;
|
||||
const userDataDirs = (options.userDataDirs ?? getDefaultChromeUserDataDirs(channels)).map((dir) => path.resolve(dir));
|
||||
for (const dir of userDataDirs) {
|
||||
const parsed = parseDevToolsActivePort(path.join(dir, "DevToolsActivePort"));
|
||||
if (!parsed)
|
||||
continue;
|
||||
if (await isPortListening(parsed.port, timeoutMs)) {
|
||||
return { port: parsed.port, wsUrl: `ws://127.0.0.1:${parsed.port}${parsed.wsPath}` };
|
||||
}
|
||||
}
|
||||
if (process.platform !== "win32") {
|
||||
try {
|
||||
const result = spawnSync("ps", ["aux"], { encoding: "utf-8", timeout: 5000 });
|
||||
if (result.status === 0 && result.stdout) {
|
||||
const lines = result.stdout.split(`
|
||||
`).filter((line) => line.includes("--remote-debugging-port=") && userDataDirs.some((dir) => line.includes(dir)));
|
||||
for (const line of lines) {
|
||||
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
||||
const port = Number.parseInt(portMatch?.[1] ?? "", 10);
|
||||
if (port > 0 && await isDebugPortReady(port, timeoutMs)) {
|
||||
try {
|
||||
const version = await fetchJson(`http://127.0.0.1:${port}/json/version`, { timeoutMs });
|
||||
if (version.webSocketDebuggerUrl)
|
||||
return { port, wsUrl: version.webSocketDebuggerUrl };
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async function waitForChromeDebugPort(port, timeoutMs, options) {
|
||||
const start = Date.now();
|
||||
let lastError = null;
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
try {
|
||||
const version = await fetchJson(`http://127.0.0.1:${port}/json/version`, { timeoutMs: 5000 });
|
||||
if (version.webSocketDebuggerUrl)
|
||||
return version.webSocketDebuggerUrl;
|
||||
lastError = new Error("Missing webSocketDebuggerUrl");
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
}
|
||||
await sleep(200);
|
||||
}
|
||||
if (options?.includeLastError && lastError) {
|
||||
throw new Error(`Chrome debug port not ready: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
||||
}
|
||||
throw new Error("Chrome debug port not ready");
|
||||
}
|
||||
|
||||
class CdpConnection {
|
||||
ws;
|
||||
nextId = 0;
|
||||
pending = new Map;
|
||||
eventHandlers = new Map;
|
||||
defaultTimeoutMs;
|
||||
constructor(ws, defaultTimeoutMs = 15000) {
|
||||
this.ws = ws;
|
||||
this.defaultTimeoutMs = defaultTimeoutMs;
|
||||
this.ws.addEventListener("message", (event) => {
|
||||
try {
|
||||
const data = typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data);
|
||||
const msg = JSON.parse(data);
|
||||
if (msg.method) {
|
||||
const handlers = this.eventHandlers.get(msg.method);
|
||||
if (handlers) {
|
||||
handlers.forEach((handler) => handler(msg.params));
|
||||
}
|
||||
}
|
||||
if (msg.id) {
|
||||
const pending = this.pending.get(msg.id);
|
||||
if (pending) {
|
||||
this.pending.delete(msg.id);
|
||||
if (pending.timer)
|
||||
clearTimeout(pending.timer);
|
||||
if (msg.error?.message)
|
||||
pending.reject(new Error(msg.error.message));
|
||||
else
|
||||
pending.resolve(msg.result);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
this.ws.addEventListener("close", () => {
|
||||
for (const [id, pending] of this.pending.entries()) {
|
||||
this.pending.delete(id);
|
||||
if (pending.timer)
|
||||
clearTimeout(pending.timer);
|
||||
pending.reject(new Error("CDP connection closed."));
|
||||
}
|
||||
});
|
||||
}
|
||||
static async connect(url, timeoutMs, options) {
|
||||
const ws = new WebSocket(url);
|
||||
await new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error("CDP connection timeout.")), timeoutMs);
|
||||
ws.addEventListener("open", () => {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
});
|
||||
ws.addEventListener("error", () => {
|
||||
clearTimeout(timer);
|
||||
reject(new Error("CDP connection failed."));
|
||||
});
|
||||
});
|
||||
return new CdpConnection(ws, options?.defaultTimeoutMs ?? 15000);
|
||||
}
|
||||
on(method, handler) {
|
||||
if (!this.eventHandlers.has(method)) {
|
||||
this.eventHandlers.set(method, new Set);
|
||||
}
|
||||
this.eventHandlers.get(method)?.add(handler);
|
||||
}
|
||||
off(method, handler) {
|
||||
this.eventHandlers.get(method)?.delete(handler);
|
||||
}
|
||||
async send(method, params, options) {
|
||||
const id = ++this.nextId;
|
||||
const message = { id, method };
|
||||
if (params)
|
||||
message.params = params;
|
||||
if (options?.sessionId)
|
||||
message.sessionId = options.sessionId;
|
||||
const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
const timer = timeoutMs > 0 ? setTimeout(() => {
|
||||
this.pending.delete(id);
|
||||
reject(new Error(`CDP timeout: ${method}`));
|
||||
}, timeoutMs) : null;
|
||||
this.pending.set(id, { resolve, reject, timer });
|
||||
this.ws.send(JSON.stringify(message));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
close() {
|
||||
try {
|
||||
this.ws.close();
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
async function launchChrome(options) {
|
||||
await fs.promises.mkdir(options.profileDir, { recursive: true });
|
||||
const args = [
|
||||
`--remote-debugging-port=${options.port}`,
|
||||
`--user-data-dir=${options.profileDir}`,
|
||||
"--no-first-run",
|
||||
"--no-default-browser-check",
|
||||
...options.extraArgs ?? []
|
||||
];
|
||||
if (options.headless)
|
||||
args.push("--headless=new");
|
||||
if (options.url)
|
||||
args.push(options.url);
|
||||
return spawn(options.chromePath, args, { stdio: "ignore" });
|
||||
}
|
||||
function killChrome(chrome) {
|
||||
try {
|
||||
chrome.kill("SIGTERM");
|
||||
} catch {}
|
||||
setTimeout(() => {
|
||||
if (chrome.exitCode === null && chrome.signalCode === null) {
|
||||
try {
|
||||
chrome.kill("SIGKILL");
|
||||
} catch {}
|
||||
}
|
||||
}, 2000).unref?.();
|
||||
}
|
||||
async function gracefulKillChrome(chrome, port, timeoutMs = 6000) {
|
||||
if (chrome.exitCode !== null || chrome.signalCode !== null)
|
||||
return;
|
||||
const exitPromise = new Promise((resolve) => {
|
||||
chrome.once("exit", () => resolve());
|
||||
});
|
||||
killChrome(chrome);
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
if (chrome.exitCode !== null || chrome.signalCode !== null)
|
||||
return;
|
||||
if (port !== undefined && !await isPortListening(port, 250))
|
||||
return;
|
||||
const exited = await Promise.race([
|
||||
exitPromise.then(() => true),
|
||||
sleep(100).then(() => false)
|
||||
]);
|
||||
if (exited)
|
||||
return;
|
||||
}
|
||||
await Promise.race([
|
||||
exitPromise,
|
||||
sleep(250)
|
||||
]);
|
||||
}
|
||||
async function openPageSession(options) {
|
||||
let targetId;
|
||||
let createdTarget = false;
|
||||
if (options.reusing) {
|
||||
const created = await options.cdp.send("Target.createTarget", { url: options.url });
|
||||
targetId = created.targetId;
|
||||
createdTarget = true;
|
||||
} else {
|
||||
const targets = await options.cdp.send("Target.getTargets");
|
||||
const existing = targets.targetInfos.find(options.matchTarget);
|
||||
if (existing) {
|
||||
targetId = existing.targetId;
|
||||
} else {
|
||||
const created = await options.cdp.send("Target.createTarget", { url: options.url });
|
||||
targetId = created.targetId;
|
||||
createdTarget = true;
|
||||
}
|
||||
}
|
||||
const { sessionId } = await options.cdp.send("Target.attachToTarget", { targetId, flatten: true });
|
||||
if (options.activateTarget ?? true) {
|
||||
await options.cdp.send("Target.activateTarget", { targetId });
|
||||
}
|
||||
if (options.enablePage)
|
||||
await options.cdp.send("Page.enable", {}, { sessionId });
|
||||
if (options.enableRuntime)
|
||||
await options.cdp.send("Runtime.enable", {}, { sessionId });
|
||||
if (options.enableDom)
|
||||
await options.cdp.send("DOM.enable", {}, { sessionId });
|
||||
if (options.enableNetwork)
|
||||
await options.cdp.send("Network.enable", {}, { sessionId });
|
||||
return { sessionId, targetId, createdTarget };
|
||||
}
|
||||
export {
|
||||
waitForChromeDebugPort,
|
||||
sleep,
|
||||
resolveSharedChromeProfileDir,
|
||||
openPageSession,
|
||||
launchChrome,
|
||||
killChrome,
|
||||
gracefulKillChrome,
|
||||
getFreePort,
|
||||
getDefaultChromeUserDataDirs,
|
||||
findExistingChromeDebugPort,
|
||||
findChromeExecutable,
|
||||
discoverRunningChromeDebugPort,
|
||||
CdpConnection
|
||||
};
|
||||
@@ -1,12 +1,42 @@
|
||||
{
|
||||
"name": "baoyu-chrome-cdp",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"src"
|
||||
"dist",
|
||||
"src/**/*.ts",
|
||||
"!src/**/*.test.ts"
|
||||
],
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./src/*": "./src/*"
|
||||
},
|
||||
"description": "Shared Chrome DevTools Protocol utilities for baoyu skills.",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./src/index.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/JimLiu/baoyu-skills.git",
|
||||
"directory": "packages/baoyu-chrome-cdp"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/JimLiu/baoyu-skills/issues"
|
||||
},
|
||||
"homepage": "https://github.com/JimLiu/baoyu-skills/tree/main/packages/baoyu-chrome-cdp#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node ../../scripts/build-shared-package.mjs",
|
||||
"prepack": "bun run build"
|
||||
},
|
||||
"engines": {
|
||||
"bun": ">=1.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "baoyu-fetch",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"description": "Read URLs into high-quality Markdown or JSON with Chrome CDP and site adapters.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"baoyu-fetch": "./src/cli.ts"
|
||||
"baoyu-fetch": "dist/cli.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.zh-CN.md",
|
||||
"src/adapters",
|
||||
"src/browser",
|
||||
@@ -36,7 +37,8 @@
|
||||
"dev": "bun run ./src/cli.ts",
|
||||
"release": "changeset publish",
|
||||
"test": "bun test",
|
||||
"version-packages": "changeset version"
|
||||
"version-packages": "changeset version",
|
||||
"prepack": "bun run build"
|
||||
},
|
||||
"engines": {
|
||||
"bun": ">=1.2.0"
|
||||
|
||||
Vendored
+77838
File diff suppressed because one or more lines are too long
Vendored
+77818
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user