diff --git a/.claude/skills/release-skills/SKILL.md b/.claude/skills/release-skills/SKILL.md
index a951854..372ac6c 100644
--- a/.claude/skills/release-skills/SKILL.md
+++ b/.claude/skills/release-skills/SKILL.md
@@ -57,7 +57,7 @@ Supported hooks:
| Hook | Purpose | Expected Responsibility |
|------|---------|-------------------------|
-| `prepare_artifact` | Build a releasable artifact for one target | Vendor local deps, rewrite package metadata, stage files |
+| `prepare_artifact` | Build a releasable artifact for one target | Validate the target is self-contained, stage files, apply any project-specific packaging |
| `publish_artifact` | Publish one prepared artifact | Upload artifact, attach version/changelog/tags |
Supported placeholders:
diff --git a/.githooks/pre-push b/.githooks/pre-push
new file mode 100755
index 0000000..e204622
--- /dev/null
+++ b/.githooks/pre-push
@@ -0,0 +1,7 @@
+#!/bin/sh
+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
diff --git a/.gitignore b/.gitignore
index 9ca54a5..c37c4df 100644
--- a/.gitignore
+++ b/.gitignore
@@ -164,4 +164,5 @@ posts/
# ClawHub local state (current and legacy directory names from the official CLI)
.clawhub/
.clawdhub/
+.release-artifacts/
.worktrees/
diff --git a/CLAUDE.md b/CLAUDE.md
index 080610b..a41e91d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -38,7 +38,9 @@ skills/
Top-level `scripts/` directory contains repository maintenance utilities:
- `scripts/sync-clawhub.sh` - Publish skills to ClawHub/OpenClaw registry
-- `scripts/prepare-skill-artifact.mjs` - Build one releasable skill artifact with vendored local packages
+- `scripts/sync-shared-skill-packages.mjs` - Sync committed `vendor/` copies of shared workspace packages into skill script directories
+- `scripts/install-git-hooks.mjs` - Configure `.githooks/` as the repository-local git hooks path
+- `scripts/prepare-skill-artifact.mjs` - Build one releasable skill artifact from the already self-contained skill tree
- `scripts/publish-skill-artifact.mjs` - Publish one prepared skill artifact
- `scripts/sync-md-to-wechat.sh` - Sync markdown content to WeChat
@@ -217,6 +219,17 @@ Requires `clawhub` CLI or `npx` (auto-downloads via npx if not installed).
Release-time artifact preparation is configured via `.releaserc.yml`. Keep registry/project-specific packaging in hook scripts instead of hardcoding it into generic release instructions.
+### Shared Workspace Packages
+
+Shared workspace packages under `packages/` are the **only** source of truth. Do not edit copies under `skills/*/scripts/vendor/` directly.
+
+When updating a shared package:
+1. Edit the package under `packages/` first
+2. Run `node scripts/sync-shared-skill-packages.mjs`
+3. Review and commit the synced `skills/*/scripts/vendor/`, `package.json`, and `bun.lock` changes together
+
+Use `node scripts/install-git-hooks.mjs` to enable the repository `pre-push` hook. The hook reruns the sync and blocks push if the committed vendor copies are stale. The release flow assumes `main` already contains the final installable tree and only validates/copies it.
+
## Skill Loading Rules
**IMPORTANT**: When working in this project, follow these rules:
diff --git a/scripts/install-git-hooks.mjs b/scripts/install-git-hooks.mjs
new file mode 100755
index 0000000..96cab13
--- /dev/null
+++ b/scripts/install-git-hooks.mjs
@@ -0,0 +1,25 @@
+#!/usr/bin/env node
+
+import { spawnSync } from "node:child_process";
+import path from "node:path";
+
+async function main() {
+ const repoRoot = path.resolve(process.cwd());
+ const hooksPath = path.join(repoRoot, ".githooks");
+
+ const result = spawnSync("git", ["config", "core.hooksPath", hooksPath], {
+ cwd: repoRoot,
+ stdio: "inherit",
+ });
+
+ if (result.status !== 0) {
+ throw new Error("Failed to configure core.hooksPath");
+ }
+
+ console.log(`Configured git hooks path: ${hooksPath}`);
+}
+
+main().catch((error) => {
+ console.error(error instanceof Error ? error.message : String(error));
+ process.exit(1);
+});
diff --git a/scripts/lib/shared-skill-packages.mjs b/scripts/lib/shared-skill-packages.mjs
new file mode 100644
index 0000000..81f259a
--- /dev/null
+++ b/scripts/lib/shared-skill-packages.mjs
@@ -0,0 +1,259 @@
+import { spawnSync } from "node:child_process";
+import { existsSync } from "node:fs";
+import fs from "node:fs/promises";
+import path from "node:path";
+
+const PACKAGE_DEPENDENCY_SECTIONS = [
+ "dependencies",
+ "optionalDependencies",
+ "peerDependencies",
+ "devDependencies",
+];
+
+const SKIPPED_DIRS = new Set([".git", ".clawhub", ".clawdhub", "node_modules"]);
+const SKIPPED_FILES = new Set([".DS_Store"]);
+
+export async function syncSharedSkillPackages(repoRoot, options = {}) {
+ const root = path.resolve(repoRoot);
+ const workspacePackages = await discoverWorkspacePackages(root);
+ const consumers = await discoverSkillScriptPackages(root);
+ const runtime = options.install === false ? null : resolveBunRuntime();
+ const managedPaths = new Set();
+ const packageDirs = [];
+
+ for (const consumer of consumers) {
+ const result = await syncConsumerPackage({
+ consumer,
+ root,
+ workspacePackages,
+ runtime,
+ });
+ if (!result) continue;
+
+ packageDirs.push(consumer.dir);
+ for (const managedPath of result.managedPaths) {
+ managedPaths.add(managedPath);
+ }
+ }
+
+ return {
+ packageDirs,
+ managedPaths: [...managedPaths].sort(),
+ };
+}
+
+export function ensureManagedPathsClean(repoRoot, managedPaths) {
+ if (managedPaths.length === 0) return;
+
+ const result = spawnSync("git", ["status", "--porcelain", "--", ...managedPaths], {
+ cwd: repoRoot,
+ encoding: "utf8",
+ stdio: ["ignore", "pipe", "pipe"],
+ });
+
+ if (result.status !== 0) {
+ throw new Error(result.stderr.trim() || "Failed to inspect git status for managed paths");
+ }
+
+ const output = result.stdout.trim();
+ if (!output) return;
+
+ throw new Error(
+ [
+ "Shared skill package sync produced uncommitted managed changes.",
+ "Review and commit these files before pushing:",
+ output,
+ ].join("\n"),
+ );
+}
+
+async function syncConsumerPackage({ consumer, root, workspacePackages, runtime }) {
+ const packageJsonPath = path.join(consumer.dir, "package.json");
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
+ const localDeps = collectLocalDependencies(packageJson, workspacePackages);
+ if (localDeps.length === 0) {
+ return null;
+ }
+
+ const vendorRoot = path.join(consumer.dir, "vendor");
+ await fs.rm(vendorRoot, { recursive: true, force: true });
+
+ for (const name of localDeps) {
+ const sourceDir = workspacePackages.get(name);
+ if (!sourceDir) continue;
+ await syncPackageTree({
+ sourceDir,
+ targetDir: path.join(vendorRoot, name),
+ workspacePackages,
+ });
+ }
+
+ rewriteLocalDependencySpecs(packageJson, localDeps);
+ await writeJson(packageJsonPath, packageJson);
+
+ if (runtime) {
+ runInstall(runtime, consumer.dir);
+ }
+
+ const managedPaths = [
+ path.relative(root, packageJsonPath).split(path.sep).join("/"),
+ path.relative(root, path.join(consumer.dir, "bun.lock")).split(path.sep).join("/"),
+ path.relative(root, vendorRoot).split(path.sep).join("/"),
+ ];
+
+ return { managedPaths };
+}
+
+async function syncPackageTree({ sourceDir, targetDir, workspacePackages }) {
+ await fs.rm(targetDir, { recursive: true, force: true });
+ await fs.mkdir(targetDir, { recursive: true });
+
+ const sourcePackageJsonPath = path.join(sourceDir, "package.json");
+ const packageJson = JSON.parse(await fs.readFile(sourcePackageJsonPath, "utf8"));
+ const localDeps = collectLocalDependencies(packageJson, workspacePackages);
+
+ const entries = await fs.readdir(sourceDir, { withFileTypes: true });
+ for (const entry of entries) {
+ if (SKIPPED_DIRS.has(entry.name) || SKIPPED_FILES.has(entry.name)) continue;
+
+ const sourcePath = path.join(sourceDir, entry.name);
+ const targetPath = path.join(targetDir, entry.name);
+
+ if (entry.isDirectory()) {
+ await copyDirectory(sourcePath, targetPath);
+ continue;
+ }
+
+ if (!entry.isFile() || entry.name === "package.json") continue;
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
+ await fs.copyFile(sourcePath, targetPath);
+ }
+
+ for (const name of localDeps) {
+ const nestedSourceDir = workspacePackages.get(name);
+ if (!nestedSourceDir) continue;
+ await syncPackageTree({
+ sourceDir: nestedSourceDir,
+ targetDir: path.join(targetDir, "vendor", name),
+ workspacePackages,
+ });
+ }
+
+ rewriteLocalDependencySpecs(packageJson, localDeps);
+ await writeJson(path.join(targetDir, "package.json"), packageJson);
+}
+
+async function copyDirectory(sourceDir, targetDir) {
+ await fs.mkdir(targetDir, { recursive: true });
+ const entries = await fs.readdir(sourceDir, { withFileTypes: true });
+ for (const entry of entries) {
+ if (SKIPPED_DIRS.has(entry.name) || SKIPPED_FILES.has(entry.name)) continue;
+
+ const sourcePath = path.join(sourceDir, entry.name);
+ const targetPath = path.join(targetDir, entry.name);
+
+ if (entry.isDirectory()) {
+ await copyDirectory(sourcePath, targetPath);
+ continue;
+ }
+
+ if (!entry.isFile()) continue;
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
+ await fs.copyFile(sourcePath, targetPath);
+ }
+}
+
+async function discoverWorkspacePackages(repoRoot) {
+ const packagesRoot = path.join(repoRoot, "packages");
+ const map = new Map();
+ if (!existsSync(packagesRoot)) return map;
+
+ const entries = await fs.readdir(packagesRoot, { withFileTypes: true });
+ for (const entry of entries) {
+ if (!entry.isDirectory()) continue;
+ const packageJsonPath = path.join(packagesRoot, entry.name, "package.json");
+ if (!existsSync(packageJsonPath)) continue;
+
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
+ if (!packageJson.name) continue;
+ map.set(packageJson.name, path.join(packagesRoot, entry.name));
+ }
+
+ return map;
+}
+
+async function discoverSkillScriptPackages(repoRoot) {
+ const skillsRoot = path.join(repoRoot, "skills");
+ const consumers = [];
+ const skillEntries = await fs.readdir(skillsRoot, { withFileTypes: true });
+ for (const entry of skillEntries) {
+ if (!entry.isDirectory()) continue;
+ const scriptsDir = path.join(skillsRoot, entry.name, "scripts");
+ const packageJsonPath = path.join(scriptsDir, "package.json");
+ if (!existsSync(packageJsonPath)) continue;
+ consumers.push({ dir: scriptsDir, packageJsonPath });
+ }
+ return consumers.sort((left, right) => left.dir.localeCompare(right.dir));
+}
+
+function collectLocalDependencies(packageJson, workspacePackages) {
+ const localDeps = [];
+ for (const section of PACKAGE_DEPENDENCY_SECTIONS) {
+ const dependencies = packageJson[section];
+ if (!dependencies || typeof dependencies !== "object") continue;
+
+ for (const name of Object.keys(dependencies)) {
+ if (!workspacePackages.has(name)) continue;
+ localDeps.push(name);
+ }
+ }
+
+ return [...new Set(localDeps)].sort();
+}
+
+function rewriteLocalDependencySpecs(packageJson, localDeps) {
+ for (const section of PACKAGE_DEPENDENCY_SECTIONS) {
+ const dependencies = packageJson[section];
+ if (!dependencies || typeof dependencies !== "object") continue;
+
+ for (const name of localDeps) {
+ if (!(name in dependencies)) continue;
+ dependencies[name] = `file:./vendor/${name}`;
+ }
+ }
+}
+
+async function writeJson(filePath, value) {
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
+ await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
+}
+
+function resolveBunRuntime() {
+ if (commandExists("bun")) {
+ return { command: "bun", args: [] };
+ }
+ if (commandExists("npx")) {
+ return { command: "npx", args: ["-y", "bun"] };
+ }
+ throw new Error(
+ "Neither bun nor npx is installed. Install bun with `brew install oven-sh/bun/bun` or `npm install -g bun`.",
+ );
+}
+
+function commandExists(command) {
+ const result = spawnSync("sh", ["-lc", `command -v ${command}`], {
+ stdio: "ignore",
+ });
+ return result.status === 0;
+}
+
+function runInstall(runtime, cwd) {
+ const result = spawnSync(runtime.command, [...runtime.args, "install"], {
+ cwd,
+ stdio: "inherit",
+ });
+
+ if (result.status !== 0) {
+ throw new Error(`Failed to refresh Bun dependencies in ${cwd}`);
+ }
+}
diff --git a/scripts/lib/skill-artifact.mjs b/scripts/lib/skill-artifact.mjs
index 15411f4..578e06f 100644
--- a/scripts/lib/skill-artifact.mjs
+++ b/scripts/lib/skill-artifact.mjs
@@ -81,27 +81,8 @@ export async function listTextFiles(root) {
}
export async function collectReleaseFiles(root) {
- const baseFiles = await listTextFiles(root);
- const fileMap = new Map(baseFiles.map((file) => [file.relPath, file.bytes]));
- const vendoredPackages = new Set();
-
- for (const file of baseFiles.filter((entry) => path.posix.basename(entry.relPath) === "package.json")) {
- const packageDirRel = normalizeDirRel(path.posix.dirname(file.relPath));
- const rewritten = await rewritePackageJsonForRelease({
- root,
- packageDirRel,
- bytes: file.bytes,
- fileMap,
- vendoredPackages,
- });
- if (rewritten) {
- fileMap.set(file.relPath, rewritten);
- }
- }
-
- return [...fileMap.entries()]
- .map(([relPath, bytes]) => ({ relPath, bytes }))
- .sort((left, right) => left.relPath.localeCompare(right.relPath));
+ await validateSelfContainedRelease(root);
+ return listTextFiles(root);
}
export async function materializeReleaseFiles(files, outDir) {
@@ -113,88 +94,37 @@ export async function materializeReleaseFiles(files, outDir) {
}
}
-async function rewritePackageJsonForRelease({ root, packageDirRel, bytes, fileMap, vendoredPackages }) {
- const packageJson = JSON.parse(bytes.toString("utf8"));
- let changed = false;
+export async function validateSelfContainedRelease(root) {
+ const files = await listTextFiles(root);
+ for (const file of files.filter((entry) => path.posix.basename(entry.relPath) === "package.json")) {
+ const packageDir = path.resolve(root, fromPosixRel(path.posix.dirname(file.relPath)));
+ const packageJson = JSON.parse(file.bytes.toString("utf8"));
+ for (const section of PACKAGE_DEPENDENCY_SECTIONS) {
+ const dependencies = packageJson[section];
+ if (!dependencies || typeof dependencies !== "object") continue;
- for (const section of PACKAGE_DEPENDENCY_SECTIONS) {
- const dependencies = packageJson[section];
- if (!dependencies || typeof dependencies !== "object") continue;
-
- for (const [name, spec] of Object.entries(dependencies)) {
- if (typeof spec !== "string" || !spec.startsWith("file:")) continue;
-
- const sourceDir = path.resolve(root, fromPosixRel(packageDirRel), spec.slice(5));
- const vendorDirRel = normalizeDirRel(path.posix.join(packageDirRel, "vendor", name));
- await vendorPackageTree({
- sourceDir,
- targetDirRel: vendorDirRel,
- fileMap,
- vendoredPackages,
- });
- dependencies[name] = toFileDependencySpec(packageDirRel, vendorDirRel);
- changed = true;
+ for (const [name, spec] of Object.entries(dependencies)) {
+ if (typeof spec !== "string" || !spec.startsWith("file:")) continue;
+ const targetDir = path.resolve(packageDir, spec.slice(5));
+ if (!isWithinRoot(root, targetDir)) {
+ throw new Error(
+ `Release artifact is not self-contained: ${file.relPath} depends on ${name} via ${spec}`,
+ );
+ }
+ await fs.access(targetDir).catch(() => {
+ throw new Error(`Missing local dependency for release: ${file.relPath} -> ${spec}`);
+ });
+ }
}
}
-
- if (!changed) return null;
- return Buffer.from(`${JSON.stringify(packageJson, null, 2)}\n`);
-}
-
-async function vendorPackageTree({ sourceDir, targetDirRel, fileMap, vendoredPackages }) {
- const dedupeKey = `${path.resolve(sourceDir)}=>${targetDirRel}`;
- if (vendoredPackages.has(dedupeKey)) return;
- vendoredPackages.add(dedupeKey);
-
- const files = await listTextFiles(sourceDir);
- if (files.length === 0) {
- throw new Error(`Local package has no text files: ${sourceDir}`);
- }
-
- const packageJson = files.find((file) => file.relPath === "package.json");
- if (!packageJson) {
- throw new Error(`Local package is missing package.json: ${sourceDir}`);
- }
-
- for (const file of files) {
- if (file.relPath === "package.json") continue;
- fileMap.set(joinReleasePath(targetDirRel, file.relPath), file.bytes);
- }
-
- const rewrittenPackageJson = await rewritePackageJsonForRelease({
- root: sourceDir,
- packageDirRel: ".",
- bytes: packageJson.bytes,
- fileMap: {
- set(relPath, outputBytes) {
- fileMap.set(joinReleasePath(targetDirRel, relPath), outputBytes);
- },
- },
- vendoredPackages,
- });
-
- fileMap.set(
- joinReleasePath(targetDirRel, "package.json"),
- rewrittenPackageJson ?? packageJson.bytes,
- );
-}
-
-function normalizeDirRel(relPath) {
- return relPath === "." ? "." : relPath.split(path.sep).join("/");
}
function fromPosixRel(relPath) {
return relPath === "." ? "." : relPath.split("/").join(path.sep);
}
-function joinReleasePath(base, relPath) {
- const joined = normalizeDirRel(path.posix.join(base === "." ? "" : base, relPath));
- return joined.replace(/^\.\//, "");
-}
-
-function toFileDependencySpec(fromDirRel, targetDirRel) {
- const fromDir = fromDirRel === "." ? "" : fromDirRel;
- const relative = path.posix.relative(fromDir || ".", targetDirRel);
- const normalized = relative === "" ? "." : relative;
- return `file:${normalized.startsWith(".") ? normalized : `./${normalized}`}`;
+function isWithinRoot(root, target) {
+ const resolvedRoot = path.resolve(root);
+ const relative = path.relative(resolvedRoot, path.resolve(target));
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
diff --git a/scripts/sync-shared-skill-packages.mjs b/scripts/sync-shared-skill-packages.mjs
new file mode 100755
index 0000000..b5d9a87
--- /dev/null
+++ b/scripts/sync-shared-skill-packages.mjs
@@ -0,0 +1,61 @@
+#!/usr/bin/env node
+
+import path from "node:path";
+
+import {
+ ensureManagedPathsClean,
+ syncSharedSkillPackages,
+} from "./lib/shared-skill-packages.mjs";
+
+async function main() {
+ const options = parseArgs(process.argv.slice(2));
+ const repoRoot = path.resolve(options.repoRoot);
+ const result = await syncSharedSkillPackages(repoRoot);
+
+ if (options.enforceClean) {
+ ensureManagedPathsClean(repoRoot, result.managedPaths);
+ }
+
+ console.log(`Synced shared workspace packages into ${result.packageDirs.length} skill script package(s).`);
+}
+
+function parseArgs(argv) {
+ const options = {
+ repoRoot: process.cwd(),
+ enforceClean: false,
+ };
+
+ for (let index = 0; index < argv.length; index += 1) {
+ const arg = argv[index];
+ if (arg === "--repo-root") {
+ options.repoRoot = argv[index + 1] ?? options.repoRoot;
+ index += 1;
+ continue;
+ }
+ if (arg === "--enforce-clean") {
+ options.enforceClean = true;
+ continue;
+ }
+ if (arg === "-h" || arg === "--help") {
+ printUsage();
+ process.exit(0);
+ }
+ throw new Error(`Unknown argument: ${arg}`);
+ }
+
+ return options;
+}
+
+function printUsage() {
+ console.log(`Usage: sync-shared-skill-packages.mjs [options]
+
+Options:
+ --repo-root
Repository root (default: current directory)
+ --enforce-clean Fail if managed files change after sync
+ -h, --help Show help`);
+}
+
+main().catch((error) => {
+ console.error(error instanceof Error ? error.message : String(error));
+ process.exit(1);
+});
diff --git a/skills/baoyu-danger-gemini-web/scripts/bun.lock b/skills/baoyu-danger-gemini-web/scripts/bun.lock
index 2a9b3f7..74bda3c 100644
--- a/skills/baoyu-danger-gemini-web/scripts/bun.lock
+++ b/skills/baoyu-danger-gemini-web/scripts/bun.lock
@@ -4,11 +4,11 @@
"": {
"name": "baoyu-danger-gemini-web-scripts",
"dependencies": {
- "baoyu-chrome-cdp": "file:../../../packages/baoyu-chrome-cdp",
+ "baoyu-chrome-cdp": "file:./vendor/baoyu-chrome-cdp",
},
},
},
"packages": {
- "baoyu-chrome-cdp": ["baoyu-chrome-cdp@file:../../../packages/baoyu-chrome-cdp", {}],
+ "baoyu-chrome-cdp": ["baoyu-chrome-cdp@file:vendor/baoyu-chrome-cdp", {}],
}
}
diff --git a/skills/baoyu-danger-gemini-web/scripts/package.json b/skills/baoyu-danger-gemini-web/scripts/package.json
index c239634..9fe2447 100644
--- a/skills/baoyu-danger-gemini-web/scripts/package.json
+++ b/skills/baoyu-danger-gemini-web/scripts/package.json
@@ -3,6 +3,6 @@
"private": true,
"type": "module",
"dependencies": {
- "baoyu-chrome-cdp": "file:../../../packages/baoyu-chrome-cdp"
+ "baoyu-chrome-cdp": "file:./vendor/baoyu-chrome-cdp"
}
}
diff --git a/skills/baoyu-danger-gemini-web/scripts/vendor/baoyu-chrome-cdp/package.json b/skills/baoyu-danger-gemini-web/scripts/vendor/baoyu-chrome-cdp/package.json
new file mode 100644
index 0000000..0014ad3
--- /dev/null
+++ b/skills/baoyu-danger-gemini-web/scripts/vendor/baoyu-chrome-cdp/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "baoyu-chrome-cdp",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "exports": {
+ ".": "./src/index.ts"
+ }
+}
diff --git a/skills/baoyu-danger-gemini-web/scripts/vendor/baoyu-chrome-cdp/src/index.ts b/skills/baoyu-danger-gemini-web/scripts/vendor/baoyu-chrome-cdp/src/index.ts
new file mode 100644
index 0000000..1fcd241
--- /dev/null
+++ b/skills/baoyu-danger-gemini-web/scripts/vendor/baoyu-chrome-cdp/src/index.ts
@@ -0,0 +1,408 @@
+import { spawn, spawnSync, type ChildProcess } 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";
+
+export type PlatformCandidates = {
+ darwin?: string[];
+ win32?: string[];
+ default: string[];
+};
+
+type PendingRequest = {
+ resolve: (value: unknown) => void;
+ reject: (error: Error) => void;
+ timer: ReturnType | null;
+};
+
+type CdpSendOptions = {
+ sessionId?: string;
+ timeoutMs?: number;
+};
+
+type FetchJsonOptions = {
+ timeoutMs?: number;
+};
+
+type FindChromeExecutableOptions = {
+ candidates: PlatformCandidates;
+ envNames?: string[];
+};
+
+type ResolveSharedChromeProfileDirOptions = {
+ envNames?: string[];
+ appDataDirName?: string;
+ profileDirName?: string;
+ wslWindowsHome?: string | null;
+};
+
+type FindExistingChromeDebugPortOptions = {
+ profileDir: string;
+ timeoutMs?: number;
+};
+
+type LaunchChromeOptions = {
+ chromePath: string;
+ profileDir: string;
+ port: number;
+ url?: string;
+ headless?: boolean;
+ extraArgs?: string[];
+};
+
+type ChromeTargetInfo = {
+ targetId: string;
+ url: string;
+ type: string;
+};
+
+type OpenPageSessionOptions = {
+ cdp: CdpConnection;
+ reusing: boolean;
+ url: string;
+ matchTarget: (target: ChromeTargetInfo) => boolean;
+ enablePage?: boolean;
+ enableRuntime?: boolean;
+ enableDom?: boolean;
+ enableNetwork?: boolean;
+ activateTarget?: boolean;
+};
+
+export type PageSession = {
+ sessionId: string;
+ targetId: string;
+};
+
+export function sleep(ms: number): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export async function getFreePort(fixedEnvName?: string): Promise {
+ 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);
+ });
+ });
+ });
+}
+
+export function findChromeExecutable(options: FindChromeExecutableOptions): string | undefined {
+ 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 undefined;
+}
+
+export function resolveSharedChromeProfileDir(options: ResolveSharedChromeProfileDirOptions = {}): string {
+ 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: string, timeoutMs?: number): Promise {
+ 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: string, options: FetchJsonOptions = {}): Promise {
+ const response = await fetchWithTimeout(url, options.timeoutMs);
+ if (!response.ok) {
+ throw new Error(`Request failed: ${response.status} ${response.statusText}`);
+ }
+ return await response.json() as T;
+}
+
+async function isDebugPortReady(port: number, timeoutMs = 3_000): Promise {
+ try {
+ const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(
+ `http://127.0.0.1:${port}/json/version`,
+ { timeoutMs }
+ );
+ return !!version.webSocketDebuggerUrl;
+ } catch {
+ return false;
+ }
+}
+
+export async function findExistingChromeDebugPort(options: FindExistingChromeDebugPortOptions): Promise {
+ const timeoutMs = options.timeoutMs ?? 3_000;
+ const portFile = path.join(options.profileDir, "DevToolsActivePort");
+
+ try {
+ const content = fs.readFileSync(portFile, "utf-8");
+ const [portLine] = content.split(/\r?\n/);
+ const port = Number.parseInt(portLine?.trim() ?? "", 10);
+ if (port > 0 && await isDebugPortReady(port, timeoutMs)) return port;
+ } catch {}
+
+ if (process.platform === "win32") return null;
+
+ try {
+ const result = spawnSync("ps", ["aux"], { encoding: "utf-8", timeout: 5_000 });
+ if (result.status !== 0 || !result.stdout) return null;
+
+ const lines = result.stdout
+ .split("\n")
+ .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;
+}
+
+export async function waitForChromeDebugPort(
+ port: number,
+ timeoutMs: number,
+ options?: { includeLastError?: boolean }
+): Promise {
+ const start = Date.now();
+ let lastError: unknown = null;
+
+ while (Date.now() - start < timeoutMs) {
+ try {
+ const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(
+ `http://127.0.0.1:${port}/json/version`,
+ { timeoutMs: 5_000 }
+ );
+ 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");
+}
+
+export class CdpConnection {
+ private ws: WebSocket;
+ private nextId = 0;
+ private pending = new Map();
+ private eventHandlers = new Map void>>();
+ private defaultTimeoutMs: number;
+
+ private constructor(ws: WebSocket, defaultTimeoutMs = 15_000) {
+ 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 as ArrayBuffer);
+ const msg = JSON.parse(data) as {
+ id?: number;
+ method?: string;
+ params?: unknown;
+ result?: unknown;
+ error?: { message?: string };
+ };
+
+ 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: string,
+ timeoutMs: number,
+ options?: { defaultTimeoutMs?: number }
+ ): Promise {
+ 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 ?? 15_000);
+ }
+
+ on(method: string, handler: (params: unknown) => void): void {
+ if (!this.eventHandlers.has(method)) {
+ this.eventHandlers.set(method, new Set());
+ }
+ this.eventHandlers.get(method)?.add(handler);
+ }
+
+ off(method: string, handler: (params: unknown) => void): void {
+ this.eventHandlers.get(method)?.delete(handler);
+ }
+
+ async send(method: string, params?: Record, options?: CdpSendOptions): Promise {
+ const id = ++this.nextId;
+ const message: Record = { 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 as T;
+ }
+
+ close(): void {
+ try {
+ this.ws.close();
+ } catch {}
+ }
+}
+
+export async function launchChrome(options: LaunchChromeOptions): Promise {
+ 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" });
+}
+
+export function killChrome(chrome: ChildProcess): void {
+ try {
+ chrome.kill("SIGTERM");
+ } catch {}
+ setTimeout(() => {
+ if (!chrome.killed) {
+ try {
+ chrome.kill("SIGKILL");
+ } catch {}
+ }
+ }, 2_000).unref?.();
+}
+
+export async function openPageSession(options: OpenPageSessionOptions): Promise {
+ let targetId: string;
+
+ if (options.reusing) {
+ const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
+ targetId = created.targetId;
+ } else {
+ const targets = await options.cdp.send<{ targetInfos: ChromeTargetInfo[] }>("Target.getTargets");
+ const existing = targets.targetInfos.find(options.matchTarget);
+ if (existing) {
+ targetId = existing.targetId;
+ } else {
+ const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
+ targetId = created.targetId;
+ }
+ }
+
+ const { sessionId } = await options.cdp.send<{ sessionId: string }>(
+ "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 };
+}
diff --git a/skills/baoyu-danger-x-to-markdown/scripts/bun.lock b/skills/baoyu-danger-x-to-markdown/scripts/bun.lock
index 5a1d327..85143c8 100644
--- a/skills/baoyu-danger-x-to-markdown/scripts/bun.lock
+++ b/skills/baoyu-danger-x-to-markdown/scripts/bun.lock
@@ -4,11 +4,11 @@
"": {
"name": "baoyu-danger-x-to-markdown-scripts",
"dependencies": {
- "baoyu-chrome-cdp": "file:../../../packages/baoyu-chrome-cdp",
+ "baoyu-chrome-cdp": "file:./vendor/baoyu-chrome-cdp",
},
},
},
"packages": {
- "baoyu-chrome-cdp": ["baoyu-chrome-cdp@file:../../../packages/baoyu-chrome-cdp", {}],
+ "baoyu-chrome-cdp": ["baoyu-chrome-cdp@file:vendor/baoyu-chrome-cdp", {}],
}
}
diff --git a/skills/baoyu-danger-x-to-markdown/scripts/package.json b/skills/baoyu-danger-x-to-markdown/scripts/package.json
index 490828b..9d275f7 100644
--- a/skills/baoyu-danger-x-to-markdown/scripts/package.json
+++ b/skills/baoyu-danger-x-to-markdown/scripts/package.json
@@ -3,6 +3,6 @@
"private": true,
"type": "module",
"dependencies": {
- "baoyu-chrome-cdp": "file:../../../packages/baoyu-chrome-cdp"
+ "baoyu-chrome-cdp": "file:./vendor/baoyu-chrome-cdp"
}
}
diff --git a/skills/baoyu-danger-x-to-markdown/scripts/vendor/baoyu-chrome-cdp/package.json b/skills/baoyu-danger-x-to-markdown/scripts/vendor/baoyu-chrome-cdp/package.json
new file mode 100644
index 0000000..0014ad3
--- /dev/null
+++ b/skills/baoyu-danger-x-to-markdown/scripts/vendor/baoyu-chrome-cdp/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "baoyu-chrome-cdp",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "exports": {
+ ".": "./src/index.ts"
+ }
+}
diff --git a/skills/baoyu-danger-x-to-markdown/scripts/vendor/baoyu-chrome-cdp/src/index.ts b/skills/baoyu-danger-x-to-markdown/scripts/vendor/baoyu-chrome-cdp/src/index.ts
new file mode 100644
index 0000000..1fcd241
--- /dev/null
+++ b/skills/baoyu-danger-x-to-markdown/scripts/vendor/baoyu-chrome-cdp/src/index.ts
@@ -0,0 +1,408 @@
+import { spawn, spawnSync, type ChildProcess } 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";
+
+export type PlatformCandidates = {
+ darwin?: string[];
+ win32?: string[];
+ default: string[];
+};
+
+type PendingRequest = {
+ resolve: (value: unknown) => void;
+ reject: (error: Error) => void;
+ timer: ReturnType | null;
+};
+
+type CdpSendOptions = {
+ sessionId?: string;
+ timeoutMs?: number;
+};
+
+type FetchJsonOptions = {
+ timeoutMs?: number;
+};
+
+type FindChromeExecutableOptions = {
+ candidates: PlatformCandidates;
+ envNames?: string[];
+};
+
+type ResolveSharedChromeProfileDirOptions = {
+ envNames?: string[];
+ appDataDirName?: string;
+ profileDirName?: string;
+ wslWindowsHome?: string | null;
+};
+
+type FindExistingChromeDebugPortOptions = {
+ profileDir: string;
+ timeoutMs?: number;
+};
+
+type LaunchChromeOptions = {
+ chromePath: string;
+ profileDir: string;
+ port: number;
+ url?: string;
+ headless?: boolean;
+ extraArgs?: string[];
+};
+
+type ChromeTargetInfo = {
+ targetId: string;
+ url: string;
+ type: string;
+};
+
+type OpenPageSessionOptions = {
+ cdp: CdpConnection;
+ reusing: boolean;
+ url: string;
+ matchTarget: (target: ChromeTargetInfo) => boolean;
+ enablePage?: boolean;
+ enableRuntime?: boolean;
+ enableDom?: boolean;
+ enableNetwork?: boolean;
+ activateTarget?: boolean;
+};
+
+export type PageSession = {
+ sessionId: string;
+ targetId: string;
+};
+
+export function sleep(ms: number): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export async function getFreePort(fixedEnvName?: string): Promise {
+ 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);
+ });
+ });
+ });
+}
+
+export function findChromeExecutable(options: FindChromeExecutableOptions): string | undefined {
+ 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 undefined;
+}
+
+export function resolveSharedChromeProfileDir(options: ResolveSharedChromeProfileDirOptions = {}): string {
+ 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: string, timeoutMs?: number): Promise {
+ 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: string, options: FetchJsonOptions = {}): Promise {
+ const response = await fetchWithTimeout(url, options.timeoutMs);
+ if (!response.ok) {
+ throw new Error(`Request failed: ${response.status} ${response.statusText}`);
+ }
+ return await response.json() as T;
+}
+
+async function isDebugPortReady(port: number, timeoutMs = 3_000): Promise {
+ try {
+ const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(
+ `http://127.0.0.1:${port}/json/version`,
+ { timeoutMs }
+ );
+ return !!version.webSocketDebuggerUrl;
+ } catch {
+ return false;
+ }
+}
+
+export async function findExistingChromeDebugPort(options: FindExistingChromeDebugPortOptions): Promise {
+ const timeoutMs = options.timeoutMs ?? 3_000;
+ const portFile = path.join(options.profileDir, "DevToolsActivePort");
+
+ try {
+ const content = fs.readFileSync(portFile, "utf-8");
+ const [portLine] = content.split(/\r?\n/);
+ const port = Number.parseInt(portLine?.trim() ?? "", 10);
+ if (port > 0 && await isDebugPortReady(port, timeoutMs)) return port;
+ } catch {}
+
+ if (process.platform === "win32") return null;
+
+ try {
+ const result = spawnSync("ps", ["aux"], { encoding: "utf-8", timeout: 5_000 });
+ if (result.status !== 0 || !result.stdout) return null;
+
+ const lines = result.stdout
+ .split("\n")
+ .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;
+}
+
+export async function waitForChromeDebugPort(
+ port: number,
+ timeoutMs: number,
+ options?: { includeLastError?: boolean }
+): Promise {
+ const start = Date.now();
+ let lastError: unknown = null;
+
+ while (Date.now() - start < timeoutMs) {
+ try {
+ const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(
+ `http://127.0.0.1:${port}/json/version`,
+ { timeoutMs: 5_000 }
+ );
+ 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");
+}
+
+export class CdpConnection {
+ private ws: WebSocket;
+ private nextId = 0;
+ private pending = new Map();
+ private eventHandlers = new Map void>>();
+ private defaultTimeoutMs: number;
+
+ private constructor(ws: WebSocket, defaultTimeoutMs = 15_000) {
+ 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 as ArrayBuffer);
+ const msg = JSON.parse(data) as {
+ id?: number;
+ method?: string;
+ params?: unknown;
+ result?: unknown;
+ error?: { message?: string };
+ };
+
+ 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: string,
+ timeoutMs: number,
+ options?: { defaultTimeoutMs?: number }
+ ): Promise {
+ 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 ?? 15_000);
+ }
+
+ on(method: string, handler: (params: unknown) => void): void {
+ if (!this.eventHandlers.has(method)) {
+ this.eventHandlers.set(method, new Set());
+ }
+ this.eventHandlers.get(method)?.add(handler);
+ }
+
+ off(method: string, handler: (params: unknown) => void): void {
+ this.eventHandlers.get(method)?.delete(handler);
+ }
+
+ async send(method: string, params?: Record, options?: CdpSendOptions): Promise {
+ const id = ++this.nextId;
+ const message: Record = { 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 as T;
+ }
+
+ close(): void {
+ try {
+ this.ws.close();
+ } catch {}
+ }
+}
+
+export async function launchChrome(options: LaunchChromeOptions): Promise {
+ 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" });
+}
+
+export function killChrome(chrome: ChildProcess): void {
+ try {
+ chrome.kill("SIGTERM");
+ } catch {}
+ setTimeout(() => {
+ if (!chrome.killed) {
+ try {
+ chrome.kill("SIGKILL");
+ } catch {}
+ }
+ }, 2_000).unref?.();
+}
+
+export async function openPageSession(options: OpenPageSessionOptions): Promise {
+ let targetId: string;
+
+ if (options.reusing) {
+ const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
+ targetId = created.targetId;
+ } else {
+ const targets = await options.cdp.send<{ targetInfos: ChromeTargetInfo[] }>("Target.getTargets");
+ const existing = targets.targetInfos.find(options.matchTarget);
+ if (existing) {
+ targetId = existing.targetId;
+ } else {
+ const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
+ targetId = created.targetId;
+ }
+ }
+
+ const { sessionId } = await options.cdp.send<{ sessionId: string }>(
+ "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 };
+}
diff --git a/skills/baoyu-post-to-wechat/scripts/bun.lock b/skills/baoyu-post-to-wechat/scripts/bun.lock
index 8a0795d..1cab200 100644
--- a/skills/baoyu-post-to-wechat/scripts/bun.lock
+++ b/skills/baoyu-post-to-wechat/scripts/bun.lock
@@ -4,11 +4,11 @@
"": {
"name": "baoyu-post-to-wechat-scripts",
"dependencies": {
- "baoyu-chrome-cdp": "file:../../../packages/baoyu-chrome-cdp",
+ "baoyu-chrome-cdp": "file:./vendor/baoyu-chrome-cdp",
},
},
},
"packages": {
- "baoyu-chrome-cdp": ["baoyu-chrome-cdp@file:../../../packages/baoyu-chrome-cdp", {}],
+ "baoyu-chrome-cdp": ["baoyu-chrome-cdp@file:vendor/baoyu-chrome-cdp", {}],
}
}
diff --git a/skills/baoyu-post-to-wechat/scripts/package.json b/skills/baoyu-post-to-wechat/scripts/package.json
index 3ea6184..b0c15ca 100644
--- a/skills/baoyu-post-to-wechat/scripts/package.json
+++ b/skills/baoyu-post-to-wechat/scripts/package.json
@@ -3,6 +3,6 @@
"private": true,
"type": "module",
"dependencies": {
- "baoyu-chrome-cdp": "file:../../../packages/baoyu-chrome-cdp"
+ "baoyu-chrome-cdp": "file:./vendor/baoyu-chrome-cdp"
}
}
diff --git a/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-chrome-cdp/package.json b/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-chrome-cdp/package.json
new file mode 100644
index 0000000..0014ad3
--- /dev/null
+++ b/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-chrome-cdp/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "baoyu-chrome-cdp",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "exports": {
+ ".": "./src/index.ts"
+ }
+}
diff --git a/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-chrome-cdp/src/index.ts b/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-chrome-cdp/src/index.ts
new file mode 100644
index 0000000..1fcd241
--- /dev/null
+++ b/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-chrome-cdp/src/index.ts
@@ -0,0 +1,408 @@
+import { spawn, spawnSync, type ChildProcess } 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";
+
+export type PlatformCandidates = {
+ darwin?: string[];
+ win32?: string[];
+ default: string[];
+};
+
+type PendingRequest = {
+ resolve: (value: unknown) => void;
+ reject: (error: Error) => void;
+ timer: ReturnType | null;
+};
+
+type CdpSendOptions = {
+ sessionId?: string;
+ timeoutMs?: number;
+};
+
+type FetchJsonOptions = {
+ timeoutMs?: number;
+};
+
+type FindChromeExecutableOptions = {
+ candidates: PlatformCandidates;
+ envNames?: string[];
+};
+
+type ResolveSharedChromeProfileDirOptions = {
+ envNames?: string[];
+ appDataDirName?: string;
+ profileDirName?: string;
+ wslWindowsHome?: string | null;
+};
+
+type FindExistingChromeDebugPortOptions = {
+ profileDir: string;
+ timeoutMs?: number;
+};
+
+type LaunchChromeOptions = {
+ chromePath: string;
+ profileDir: string;
+ port: number;
+ url?: string;
+ headless?: boolean;
+ extraArgs?: string[];
+};
+
+type ChromeTargetInfo = {
+ targetId: string;
+ url: string;
+ type: string;
+};
+
+type OpenPageSessionOptions = {
+ cdp: CdpConnection;
+ reusing: boolean;
+ url: string;
+ matchTarget: (target: ChromeTargetInfo) => boolean;
+ enablePage?: boolean;
+ enableRuntime?: boolean;
+ enableDom?: boolean;
+ enableNetwork?: boolean;
+ activateTarget?: boolean;
+};
+
+export type PageSession = {
+ sessionId: string;
+ targetId: string;
+};
+
+export function sleep(ms: number): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export async function getFreePort(fixedEnvName?: string): Promise {
+ 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);
+ });
+ });
+ });
+}
+
+export function findChromeExecutable(options: FindChromeExecutableOptions): string | undefined {
+ 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 undefined;
+}
+
+export function resolveSharedChromeProfileDir(options: ResolveSharedChromeProfileDirOptions = {}): string {
+ 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: string, timeoutMs?: number): Promise {
+ 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: string, options: FetchJsonOptions = {}): Promise {
+ const response = await fetchWithTimeout(url, options.timeoutMs);
+ if (!response.ok) {
+ throw new Error(`Request failed: ${response.status} ${response.statusText}`);
+ }
+ return await response.json() as T;
+}
+
+async function isDebugPortReady(port: number, timeoutMs = 3_000): Promise {
+ try {
+ const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(
+ `http://127.0.0.1:${port}/json/version`,
+ { timeoutMs }
+ );
+ return !!version.webSocketDebuggerUrl;
+ } catch {
+ return false;
+ }
+}
+
+export async function findExistingChromeDebugPort(options: FindExistingChromeDebugPortOptions): Promise {
+ const timeoutMs = options.timeoutMs ?? 3_000;
+ const portFile = path.join(options.profileDir, "DevToolsActivePort");
+
+ try {
+ const content = fs.readFileSync(portFile, "utf-8");
+ const [portLine] = content.split(/\r?\n/);
+ const port = Number.parseInt(portLine?.trim() ?? "", 10);
+ if (port > 0 && await isDebugPortReady(port, timeoutMs)) return port;
+ } catch {}
+
+ if (process.platform === "win32") return null;
+
+ try {
+ const result = spawnSync("ps", ["aux"], { encoding: "utf-8", timeout: 5_000 });
+ if (result.status !== 0 || !result.stdout) return null;
+
+ const lines = result.stdout
+ .split("\n")
+ .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;
+}
+
+export async function waitForChromeDebugPort(
+ port: number,
+ timeoutMs: number,
+ options?: { includeLastError?: boolean }
+): Promise {
+ const start = Date.now();
+ let lastError: unknown = null;
+
+ while (Date.now() - start < timeoutMs) {
+ try {
+ const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(
+ `http://127.0.0.1:${port}/json/version`,
+ { timeoutMs: 5_000 }
+ );
+ 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");
+}
+
+export class CdpConnection {
+ private ws: WebSocket;
+ private nextId = 0;
+ private pending = new Map();
+ private eventHandlers = new Map void>>();
+ private defaultTimeoutMs: number;
+
+ private constructor(ws: WebSocket, defaultTimeoutMs = 15_000) {
+ 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 as ArrayBuffer);
+ const msg = JSON.parse(data) as {
+ id?: number;
+ method?: string;
+ params?: unknown;
+ result?: unknown;
+ error?: { message?: string };
+ };
+
+ 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: string,
+ timeoutMs: number,
+ options?: { defaultTimeoutMs?: number }
+ ): Promise {
+ 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 ?? 15_000);
+ }
+
+ on(method: string, handler: (params: unknown) => void): void {
+ if (!this.eventHandlers.has(method)) {
+ this.eventHandlers.set(method, new Set());
+ }
+ this.eventHandlers.get(method)?.add(handler);
+ }
+
+ off(method: string, handler: (params: unknown) => void): void {
+ this.eventHandlers.get(method)?.delete(handler);
+ }
+
+ async send(method: string, params?: Record, options?: CdpSendOptions): Promise {
+ const id = ++this.nextId;
+ const message: Record = { 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 as T;
+ }
+
+ close(): void {
+ try {
+ this.ws.close();
+ } catch {}
+ }
+}
+
+export async function launchChrome(options: LaunchChromeOptions): Promise {
+ 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" });
+}
+
+export function killChrome(chrome: ChildProcess): void {
+ try {
+ chrome.kill("SIGTERM");
+ } catch {}
+ setTimeout(() => {
+ if (!chrome.killed) {
+ try {
+ chrome.kill("SIGKILL");
+ } catch {}
+ }
+ }, 2_000).unref?.();
+}
+
+export async function openPageSession(options: OpenPageSessionOptions): Promise {
+ let targetId: string;
+
+ if (options.reusing) {
+ const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
+ targetId = created.targetId;
+ } else {
+ const targets = await options.cdp.send<{ targetInfos: ChromeTargetInfo[] }>("Target.getTargets");
+ const existing = targets.targetInfos.find(options.matchTarget);
+ if (existing) {
+ targetId = existing.targetId;
+ } else {
+ const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
+ targetId = created.targetId;
+ }
+ }
+
+ const { sessionId } = await options.cdp.send<{ sessionId: string }>(
+ "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 };
+}
diff --git a/skills/baoyu-post-to-weibo/scripts/bun.lock b/skills/baoyu-post-to-weibo/scripts/bun.lock
index 918cb98..00e2fa8 100644
--- a/skills/baoyu-post-to-weibo/scripts/bun.lock
+++ b/skills/baoyu-post-to-weibo/scripts/bun.lock
@@ -4,7 +4,7 @@
"": {
"name": "baoyu-post-to-weibo-scripts",
"dependencies": {
- "baoyu-chrome-cdp": "file:../../../packages/baoyu-chrome-cdp",
+ "baoyu-chrome-cdp": "file:./vendor/baoyu-chrome-cdp",
"front-matter": "^4.0.2",
"highlight.js": "^11.11.1",
"marked": "^15.0.6",
@@ -14,7 +14,7 @@
"packages": {
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
- "baoyu-chrome-cdp": ["baoyu-chrome-cdp@file:../../../packages/baoyu-chrome-cdp", {}],
+ "baoyu-chrome-cdp": ["baoyu-chrome-cdp@file:vendor/baoyu-chrome-cdp", {}],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
diff --git a/skills/baoyu-post-to-weibo/scripts/package.json b/skills/baoyu-post-to-weibo/scripts/package.json
index 6ffd8e7..44b0089 100644
--- a/skills/baoyu-post-to-weibo/scripts/package.json
+++ b/skills/baoyu-post-to-weibo/scripts/package.json
@@ -3,7 +3,7 @@
"private": true,
"type": "module",
"dependencies": {
- "baoyu-chrome-cdp": "file:../../../packages/baoyu-chrome-cdp",
+ "baoyu-chrome-cdp": "file:./vendor/baoyu-chrome-cdp",
"front-matter": "^4.0.2",
"highlight.js": "^11.11.1",
"marked": "^15.0.6"
diff --git a/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-chrome-cdp/package.json b/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-chrome-cdp/package.json
new file mode 100644
index 0000000..0014ad3
--- /dev/null
+++ b/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-chrome-cdp/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "baoyu-chrome-cdp",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "exports": {
+ ".": "./src/index.ts"
+ }
+}
diff --git a/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-chrome-cdp/src/index.ts b/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-chrome-cdp/src/index.ts
new file mode 100644
index 0000000..1fcd241
--- /dev/null
+++ b/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-chrome-cdp/src/index.ts
@@ -0,0 +1,408 @@
+import { spawn, spawnSync, type ChildProcess } 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";
+
+export type PlatformCandidates = {
+ darwin?: string[];
+ win32?: string[];
+ default: string[];
+};
+
+type PendingRequest = {
+ resolve: (value: unknown) => void;
+ reject: (error: Error) => void;
+ timer: ReturnType | null;
+};
+
+type CdpSendOptions = {
+ sessionId?: string;
+ timeoutMs?: number;
+};
+
+type FetchJsonOptions = {
+ timeoutMs?: number;
+};
+
+type FindChromeExecutableOptions = {
+ candidates: PlatformCandidates;
+ envNames?: string[];
+};
+
+type ResolveSharedChromeProfileDirOptions = {
+ envNames?: string[];
+ appDataDirName?: string;
+ profileDirName?: string;
+ wslWindowsHome?: string | null;
+};
+
+type FindExistingChromeDebugPortOptions = {
+ profileDir: string;
+ timeoutMs?: number;
+};
+
+type LaunchChromeOptions = {
+ chromePath: string;
+ profileDir: string;
+ port: number;
+ url?: string;
+ headless?: boolean;
+ extraArgs?: string[];
+};
+
+type ChromeTargetInfo = {
+ targetId: string;
+ url: string;
+ type: string;
+};
+
+type OpenPageSessionOptions = {
+ cdp: CdpConnection;
+ reusing: boolean;
+ url: string;
+ matchTarget: (target: ChromeTargetInfo) => boolean;
+ enablePage?: boolean;
+ enableRuntime?: boolean;
+ enableDom?: boolean;
+ enableNetwork?: boolean;
+ activateTarget?: boolean;
+};
+
+export type PageSession = {
+ sessionId: string;
+ targetId: string;
+};
+
+export function sleep(ms: number): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export async function getFreePort(fixedEnvName?: string): Promise {
+ 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);
+ });
+ });
+ });
+}
+
+export function findChromeExecutable(options: FindChromeExecutableOptions): string | undefined {
+ 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 undefined;
+}
+
+export function resolveSharedChromeProfileDir(options: ResolveSharedChromeProfileDirOptions = {}): string {
+ 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: string, timeoutMs?: number): Promise {
+ 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: string, options: FetchJsonOptions = {}): Promise {
+ const response = await fetchWithTimeout(url, options.timeoutMs);
+ if (!response.ok) {
+ throw new Error(`Request failed: ${response.status} ${response.statusText}`);
+ }
+ return await response.json() as T;
+}
+
+async function isDebugPortReady(port: number, timeoutMs = 3_000): Promise {
+ try {
+ const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(
+ `http://127.0.0.1:${port}/json/version`,
+ { timeoutMs }
+ );
+ return !!version.webSocketDebuggerUrl;
+ } catch {
+ return false;
+ }
+}
+
+export async function findExistingChromeDebugPort(options: FindExistingChromeDebugPortOptions): Promise {
+ const timeoutMs = options.timeoutMs ?? 3_000;
+ const portFile = path.join(options.profileDir, "DevToolsActivePort");
+
+ try {
+ const content = fs.readFileSync(portFile, "utf-8");
+ const [portLine] = content.split(/\r?\n/);
+ const port = Number.parseInt(portLine?.trim() ?? "", 10);
+ if (port > 0 && await isDebugPortReady(port, timeoutMs)) return port;
+ } catch {}
+
+ if (process.platform === "win32") return null;
+
+ try {
+ const result = spawnSync("ps", ["aux"], { encoding: "utf-8", timeout: 5_000 });
+ if (result.status !== 0 || !result.stdout) return null;
+
+ const lines = result.stdout
+ .split("\n")
+ .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;
+}
+
+export async function waitForChromeDebugPort(
+ port: number,
+ timeoutMs: number,
+ options?: { includeLastError?: boolean }
+): Promise {
+ const start = Date.now();
+ let lastError: unknown = null;
+
+ while (Date.now() - start < timeoutMs) {
+ try {
+ const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(
+ `http://127.0.0.1:${port}/json/version`,
+ { timeoutMs: 5_000 }
+ );
+ 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");
+}
+
+export class CdpConnection {
+ private ws: WebSocket;
+ private nextId = 0;
+ private pending = new Map();
+ private eventHandlers = new Map void>>();
+ private defaultTimeoutMs: number;
+
+ private constructor(ws: WebSocket, defaultTimeoutMs = 15_000) {
+ 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 as ArrayBuffer);
+ const msg = JSON.parse(data) as {
+ id?: number;
+ method?: string;
+ params?: unknown;
+ result?: unknown;
+ error?: { message?: string };
+ };
+
+ 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: string,
+ timeoutMs: number,
+ options?: { defaultTimeoutMs?: number }
+ ): Promise {
+ 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 ?? 15_000);
+ }
+
+ on(method: string, handler: (params: unknown) => void): void {
+ if (!this.eventHandlers.has(method)) {
+ this.eventHandlers.set(method, new Set());
+ }
+ this.eventHandlers.get(method)?.add(handler);
+ }
+
+ off(method: string, handler: (params: unknown) => void): void {
+ this.eventHandlers.get(method)?.delete(handler);
+ }
+
+ async send(method: string, params?: Record, options?: CdpSendOptions): Promise {
+ const id = ++this.nextId;
+ const message: Record = { 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 as T;
+ }
+
+ close(): void {
+ try {
+ this.ws.close();
+ } catch {}
+ }
+}
+
+export async function launchChrome(options: LaunchChromeOptions): Promise {
+ 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" });
+}
+
+export function killChrome(chrome: ChildProcess): void {
+ try {
+ chrome.kill("SIGTERM");
+ } catch {}
+ setTimeout(() => {
+ if (!chrome.killed) {
+ try {
+ chrome.kill("SIGKILL");
+ } catch {}
+ }
+ }, 2_000).unref?.();
+}
+
+export async function openPageSession(options: OpenPageSessionOptions): Promise {
+ let targetId: string;
+
+ if (options.reusing) {
+ const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
+ targetId = created.targetId;
+ } else {
+ const targets = await options.cdp.send<{ targetInfos: ChromeTargetInfo[] }>("Target.getTargets");
+ const existing = targets.targetInfos.find(options.matchTarget);
+ if (existing) {
+ targetId = existing.targetId;
+ } else {
+ const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
+ targetId = created.targetId;
+ }
+ }
+
+ const { sessionId } = await options.cdp.send<{ sessionId: string }>(
+ "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 };
+}
diff --git a/skills/baoyu-post-to-x/scripts/bun.lock b/skills/baoyu-post-to-x/scripts/bun.lock
index edd677e..6f50e45 100644
--- a/skills/baoyu-post-to-x/scripts/bun.lock
+++ b/skills/baoyu-post-to-x/scripts/bun.lock
@@ -4,7 +4,7 @@
"": {
"name": "baoyu-post-to-x-scripts",
"dependencies": {
- "baoyu-chrome-cdp": "file:../../../packages/baoyu-chrome-cdp",
+ "baoyu-chrome-cdp": "file:./vendor/baoyu-chrome-cdp",
"front-matter": "^4.0.2",
"highlight.js": "^11.11.1",
"marked": "^15.0.6",
@@ -28,7 +28,7 @@
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
- "baoyu-chrome-cdp": ["baoyu-chrome-cdp@file:../../../packages/baoyu-chrome-cdp", {}],
+ "baoyu-chrome-cdp": ["baoyu-chrome-cdp@file:vendor/baoyu-chrome-cdp", {}],
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
diff --git a/skills/baoyu-post-to-x/scripts/package.json b/skills/baoyu-post-to-x/scripts/package.json
index 5b81600..caa9b93 100644
--- a/skills/baoyu-post-to-x/scripts/package.json
+++ b/skills/baoyu-post-to-x/scripts/package.json
@@ -3,7 +3,7 @@
"private": true,
"type": "module",
"dependencies": {
- "baoyu-chrome-cdp": "file:../../../packages/baoyu-chrome-cdp",
+ "baoyu-chrome-cdp": "file:./vendor/baoyu-chrome-cdp",
"front-matter": "^4.0.2",
"highlight.js": "^11.11.1",
"marked": "^15.0.6",
diff --git a/skills/baoyu-post-to-x/scripts/vendor/baoyu-chrome-cdp/package.json b/skills/baoyu-post-to-x/scripts/vendor/baoyu-chrome-cdp/package.json
new file mode 100644
index 0000000..0014ad3
--- /dev/null
+++ b/skills/baoyu-post-to-x/scripts/vendor/baoyu-chrome-cdp/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "baoyu-chrome-cdp",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "exports": {
+ ".": "./src/index.ts"
+ }
+}
diff --git a/skills/baoyu-post-to-x/scripts/vendor/baoyu-chrome-cdp/src/index.ts b/skills/baoyu-post-to-x/scripts/vendor/baoyu-chrome-cdp/src/index.ts
new file mode 100644
index 0000000..1fcd241
--- /dev/null
+++ b/skills/baoyu-post-to-x/scripts/vendor/baoyu-chrome-cdp/src/index.ts
@@ -0,0 +1,408 @@
+import { spawn, spawnSync, type ChildProcess } 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";
+
+export type PlatformCandidates = {
+ darwin?: string[];
+ win32?: string[];
+ default: string[];
+};
+
+type PendingRequest = {
+ resolve: (value: unknown) => void;
+ reject: (error: Error) => void;
+ timer: ReturnType | null;
+};
+
+type CdpSendOptions = {
+ sessionId?: string;
+ timeoutMs?: number;
+};
+
+type FetchJsonOptions = {
+ timeoutMs?: number;
+};
+
+type FindChromeExecutableOptions = {
+ candidates: PlatformCandidates;
+ envNames?: string[];
+};
+
+type ResolveSharedChromeProfileDirOptions = {
+ envNames?: string[];
+ appDataDirName?: string;
+ profileDirName?: string;
+ wslWindowsHome?: string | null;
+};
+
+type FindExistingChromeDebugPortOptions = {
+ profileDir: string;
+ timeoutMs?: number;
+};
+
+type LaunchChromeOptions = {
+ chromePath: string;
+ profileDir: string;
+ port: number;
+ url?: string;
+ headless?: boolean;
+ extraArgs?: string[];
+};
+
+type ChromeTargetInfo = {
+ targetId: string;
+ url: string;
+ type: string;
+};
+
+type OpenPageSessionOptions = {
+ cdp: CdpConnection;
+ reusing: boolean;
+ url: string;
+ matchTarget: (target: ChromeTargetInfo) => boolean;
+ enablePage?: boolean;
+ enableRuntime?: boolean;
+ enableDom?: boolean;
+ enableNetwork?: boolean;
+ activateTarget?: boolean;
+};
+
+export type PageSession = {
+ sessionId: string;
+ targetId: string;
+};
+
+export function sleep(ms: number): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export async function getFreePort(fixedEnvName?: string): Promise {
+ 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);
+ });
+ });
+ });
+}
+
+export function findChromeExecutable(options: FindChromeExecutableOptions): string | undefined {
+ 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 undefined;
+}
+
+export function resolveSharedChromeProfileDir(options: ResolveSharedChromeProfileDirOptions = {}): string {
+ 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: string, timeoutMs?: number): Promise {
+ 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: string, options: FetchJsonOptions = {}): Promise {
+ const response = await fetchWithTimeout(url, options.timeoutMs);
+ if (!response.ok) {
+ throw new Error(`Request failed: ${response.status} ${response.statusText}`);
+ }
+ return await response.json() as T;
+}
+
+async function isDebugPortReady(port: number, timeoutMs = 3_000): Promise {
+ try {
+ const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(
+ `http://127.0.0.1:${port}/json/version`,
+ { timeoutMs }
+ );
+ return !!version.webSocketDebuggerUrl;
+ } catch {
+ return false;
+ }
+}
+
+export async function findExistingChromeDebugPort(options: FindExistingChromeDebugPortOptions): Promise {
+ const timeoutMs = options.timeoutMs ?? 3_000;
+ const portFile = path.join(options.profileDir, "DevToolsActivePort");
+
+ try {
+ const content = fs.readFileSync(portFile, "utf-8");
+ const [portLine] = content.split(/\r?\n/);
+ const port = Number.parseInt(portLine?.trim() ?? "", 10);
+ if (port > 0 && await isDebugPortReady(port, timeoutMs)) return port;
+ } catch {}
+
+ if (process.platform === "win32") return null;
+
+ try {
+ const result = spawnSync("ps", ["aux"], { encoding: "utf-8", timeout: 5_000 });
+ if (result.status !== 0 || !result.stdout) return null;
+
+ const lines = result.stdout
+ .split("\n")
+ .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;
+}
+
+export async function waitForChromeDebugPort(
+ port: number,
+ timeoutMs: number,
+ options?: { includeLastError?: boolean }
+): Promise {
+ const start = Date.now();
+ let lastError: unknown = null;
+
+ while (Date.now() - start < timeoutMs) {
+ try {
+ const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(
+ `http://127.0.0.1:${port}/json/version`,
+ { timeoutMs: 5_000 }
+ );
+ 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");
+}
+
+export class CdpConnection {
+ private ws: WebSocket;
+ private nextId = 0;
+ private pending = new Map();
+ private eventHandlers = new Map void>>();
+ private defaultTimeoutMs: number;
+
+ private constructor(ws: WebSocket, defaultTimeoutMs = 15_000) {
+ 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 as ArrayBuffer);
+ const msg = JSON.parse(data) as {
+ id?: number;
+ method?: string;
+ params?: unknown;
+ result?: unknown;
+ error?: { message?: string };
+ };
+
+ 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: string,
+ timeoutMs: number,
+ options?: { defaultTimeoutMs?: number }
+ ): Promise {
+ 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 ?? 15_000);
+ }
+
+ on(method: string, handler: (params: unknown) => void): void {
+ if (!this.eventHandlers.has(method)) {
+ this.eventHandlers.set(method, new Set());
+ }
+ this.eventHandlers.get(method)?.add(handler);
+ }
+
+ off(method: string, handler: (params: unknown) => void): void {
+ this.eventHandlers.get(method)?.delete(handler);
+ }
+
+ async send(method: string, params?: Record, options?: CdpSendOptions): Promise {
+ const id = ++this.nextId;
+ const message: Record = { 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 as T;
+ }
+
+ close(): void {
+ try {
+ this.ws.close();
+ } catch {}
+ }
+}
+
+export async function launchChrome(options: LaunchChromeOptions): Promise {
+ 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" });
+}
+
+export function killChrome(chrome: ChildProcess): void {
+ try {
+ chrome.kill("SIGTERM");
+ } catch {}
+ setTimeout(() => {
+ if (!chrome.killed) {
+ try {
+ chrome.kill("SIGKILL");
+ } catch {}
+ }
+ }, 2_000).unref?.();
+}
+
+export async function openPageSession(options: OpenPageSessionOptions): Promise {
+ let targetId: string;
+
+ if (options.reusing) {
+ const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
+ targetId = created.targetId;
+ } else {
+ const targets = await options.cdp.send<{ targetInfos: ChromeTargetInfo[] }>("Target.getTargets");
+ const existing = targets.targetInfos.find(options.matchTarget);
+ if (existing) {
+ targetId = existing.targetId;
+ } else {
+ const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
+ targetId = created.targetId;
+ }
+ }
+
+ const { sessionId } = await options.cdp.send<{ sessionId: string }>(
+ "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 };
+}
diff --git a/skills/baoyu-url-to-markdown/scripts/bun.lock b/skills/baoyu-url-to-markdown/scripts/bun.lock
index c68e09c..50109d2 100644
--- a/skills/baoyu-url-to-markdown/scripts/bun.lock
+++ b/skills/baoyu-url-to-markdown/scripts/bun.lock
@@ -5,7 +5,7 @@
"name": "baoyu-url-to-markdown-scripts",
"dependencies": {
"@mozilla/readability": "^0.6.0",
- "baoyu-chrome-cdp": "file:../../../packages/baoyu-chrome-cdp",
+ "baoyu-chrome-cdp": "file:./vendor/baoyu-chrome-cdp",
"defuddle": "^0.10.0",
"jsdom": "^24.1.3",
"linkedom": "^0.18.12",
@@ -37,7 +37,7 @@
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
- "baoyu-chrome-cdp": ["baoyu-chrome-cdp@file:../../../packages/baoyu-chrome-cdp", {}],
+ "baoyu-chrome-cdp": ["baoyu-chrome-cdp@file:vendor/baoyu-chrome-cdp", {}],
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
diff --git a/skills/baoyu-url-to-markdown/scripts/package.json b/skills/baoyu-url-to-markdown/scripts/package.json
index 23c82c3..e37fdae 100644
--- a/skills/baoyu-url-to-markdown/scripts/package.json
+++ b/skills/baoyu-url-to-markdown/scripts/package.json
@@ -4,7 +4,7 @@
"type": "module",
"dependencies": {
"@mozilla/readability": "^0.6.0",
- "baoyu-chrome-cdp": "file:../../../packages/baoyu-chrome-cdp",
+ "baoyu-chrome-cdp": "file:./vendor/baoyu-chrome-cdp",
"defuddle": "^0.10.0",
"jsdom": "^24.1.3",
"linkedom": "^0.18.12",
diff --git a/skills/baoyu-url-to-markdown/scripts/vendor/baoyu-chrome-cdp/package.json b/skills/baoyu-url-to-markdown/scripts/vendor/baoyu-chrome-cdp/package.json
new file mode 100644
index 0000000..0014ad3
--- /dev/null
+++ b/skills/baoyu-url-to-markdown/scripts/vendor/baoyu-chrome-cdp/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "baoyu-chrome-cdp",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "exports": {
+ ".": "./src/index.ts"
+ }
+}
diff --git a/skills/baoyu-url-to-markdown/scripts/vendor/baoyu-chrome-cdp/src/index.ts b/skills/baoyu-url-to-markdown/scripts/vendor/baoyu-chrome-cdp/src/index.ts
new file mode 100644
index 0000000..1fcd241
--- /dev/null
+++ b/skills/baoyu-url-to-markdown/scripts/vendor/baoyu-chrome-cdp/src/index.ts
@@ -0,0 +1,408 @@
+import { spawn, spawnSync, type ChildProcess } 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";
+
+export type PlatformCandidates = {
+ darwin?: string[];
+ win32?: string[];
+ default: string[];
+};
+
+type PendingRequest = {
+ resolve: (value: unknown) => void;
+ reject: (error: Error) => void;
+ timer: ReturnType | null;
+};
+
+type CdpSendOptions = {
+ sessionId?: string;
+ timeoutMs?: number;
+};
+
+type FetchJsonOptions = {
+ timeoutMs?: number;
+};
+
+type FindChromeExecutableOptions = {
+ candidates: PlatformCandidates;
+ envNames?: string[];
+};
+
+type ResolveSharedChromeProfileDirOptions = {
+ envNames?: string[];
+ appDataDirName?: string;
+ profileDirName?: string;
+ wslWindowsHome?: string | null;
+};
+
+type FindExistingChromeDebugPortOptions = {
+ profileDir: string;
+ timeoutMs?: number;
+};
+
+type LaunchChromeOptions = {
+ chromePath: string;
+ profileDir: string;
+ port: number;
+ url?: string;
+ headless?: boolean;
+ extraArgs?: string[];
+};
+
+type ChromeTargetInfo = {
+ targetId: string;
+ url: string;
+ type: string;
+};
+
+type OpenPageSessionOptions = {
+ cdp: CdpConnection;
+ reusing: boolean;
+ url: string;
+ matchTarget: (target: ChromeTargetInfo) => boolean;
+ enablePage?: boolean;
+ enableRuntime?: boolean;
+ enableDom?: boolean;
+ enableNetwork?: boolean;
+ activateTarget?: boolean;
+};
+
+export type PageSession = {
+ sessionId: string;
+ targetId: string;
+};
+
+export function sleep(ms: number): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export async function getFreePort(fixedEnvName?: string): Promise {
+ 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);
+ });
+ });
+ });
+}
+
+export function findChromeExecutable(options: FindChromeExecutableOptions): string | undefined {
+ 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 undefined;
+}
+
+export function resolveSharedChromeProfileDir(options: ResolveSharedChromeProfileDirOptions = {}): string {
+ 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: string, timeoutMs?: number): Promise {
+ 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: string, options: FetchJsonOptions = {}): Promise {
+ const response = await fetchWithTimeout(url, options.timeoutMs);
+ if (!response.ok) {
+ throw new Error(`Request failed: ${response.status} ${response.statusText}`);
+ }
+ return await response.json() as T;
+}
+
+async function isDebugPortReady(port: number, timeoutMs = 3_000): Promise {
+ try {
+ const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(
+ `http://127.0.0.1:${port}/json/version`,
+ { timeoutMs }
+ );
+ return !!version.webSocketDebuggerUrl;
+ } catch {
+ return false;
+ }
+}
+
+export async function findExistingChromeDebugPort(options: FindExistingChromeDebugPortOptions): Promise {
+ const timeoutMs = options.timeoutMs ?? 3_000;
+ const portFile = path.join(options.profileDir, "DevToolsActivePort");
+
+ try {
+ const content = fs.readFileSync(portFile, "utf-8");
+ const [portLine] = content.split(/\r?\n/);
+ const port = Number.parseInt(portLine?.trim() ?? "", 10);
+ if (port > 0 && await isDebugPortReady(port, timeoutMs)) return port;
+ } catch {}
+
+ if (process.platform === "win32") return null;
+
+ try {
+ const result = spawnSync("ps", ["aux"], { encoding: "utf-8", timeout: 5_000 });
+ if (result.status !== 0 || !result.stdout) return null;
+
+ const lines = result.stdout
+ .split("\n")
+ .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;
+}
+
+export async function waitForChromeDebugPort(
+ port: number,
+ timeoutMs: number,
+ options?: { includeLastError?: boolean }
+): Promise {
+ const start = Date.now();
+ let lastError: unknown = null;
+
+ while (Date.now() - start < timeoutMs) {
+ try {
+ const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(
+ `http://127.0.0.1:${port}/json/version`,
+ { timeoutMs: 5_000 }
+ );
+ 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");
+}
+
+export class CdpConnection {
+ private ws: WebSocket;
+ private nextId = 0;
+ private pending = new Map();
+ private eventHandlers = new Map void>>();
+ private defaultTimeoutMs: number;
+
+ private constructor(ws: WebSocket, defaultTimeoutMs = 15_000) {
+ 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 as ArrayBuffer);
+ const msg = JSON.parse(data) as {
+ id?: number;
+ method?: string;
+ params?: unknown;
+ result?: unknown;
+ error?: { message?: string };
+ };
+
+ 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: string,
+ timeoutMs: number,
+ options?: { defaultTimeoutMs?: number }
+ ): Promise {
+ 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 ?? 15_000);
+ }
+
+ on(method: string, handler: (params: unknown) => void): void {
+ if (!this.eventHandlers.has(method)) {
+ this.eventHandlers.set(method, new Set());
+ }
+ this.eventHandlers.get(method)?.add(handler);
+ }
+
+ off(method: string, handler: (params: unknown) => void): void {
+ this.eventHandlers.get(method)?.delete(handler);
+ }
+
+ async send(method: string, params?: Record, options?: CdpSendOptions): Promise {
+ const id = ++this.nextId;
+ const message: Record = { 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 as T;
+ }
+
+ close(): void {
+ try {
+ this.ws.close();
+ } catch {}
+ }
+}
+
+export async function launchChrome(options: LaunchChromeOptions): Promise {
+ 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" });
+}
+
+export function killChrome(chrome: ChildProcess): void {
+ try {
+ chrome.kill("SIGTERM");
+ } catch {}
+ setTimeout(() => {
+ if (!chrome.killed) {
+ try {
+ chrome.kill("SIGKILL");
+ } catch {}
+ }
+ }, 2_000).unref?.();
+}
+
+export async function openPageSession(options: OpenPageSessionOptions): Promise {
+ let targetId: string;
+
+ if (options.reusing) {
+ const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
+ targetId = created.targetId;
+ } else {
+ const targets = await options.cdp.send<{ targetInfos: ChromeTargetInfo[] }>("Target.getTargets");
+ const existing = targets.targetInfos.find(options.matchTarget);
+ if (existing) {
+ targetId = existing.targetId;
+ } else {
+ const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
+ targetId = created.targetId;
+ }
+ }
+
+ const { sessionId } = await options.cdp.send<{ sessionId: string }>(
+ "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 };
+}