diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..3bee3e8
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,92 @@
+# Changelog
+
+All notable changes to BrowserSkill will be documented in this file.
+The format is based on [Keep a Changelog](https://keepachangelog.com/).
+
+Starting from 0.2.0, CLI / Extension / DSH Plugin share the same version number.
+
+## [Unreleased]
+
+## [0.2.0] - 2026-09-XX
+
+### Added
+- File transfer: upload and download support across CLI, Extension, and DSH Plugin
+- File transfer: drag-and-drop upload (`drop-to-upload`)
+- VOM semantic graph, name enrichment, and hover perception modules
+- VOM hover probing (opt-in via `observe` parameter)
+- DSH Plugin: browser tool parity with CLI commands
+- Edge Add-ons automated publishing in CI
+- Protocol upgrade reminder when CLI / Extension protocol versions differ
+- Browser evaluation harness (`evals/browser/`)
+- Unified release script (`scripts/release.mjs`)
+
+### Changed
+- **Version scheme**: all three components now share the same semver
+- VOM rendering algorithm optimizations
+- Leaner SKILL.md agent instructions
+- DSH Plugin: simplified browser commands
+- Borrow confirmation UX — proactive focus and longer timeout
+- PiP window now has a close button
+
+### Fixed
+- Screenshot media type detection (was hard-coded to `image/png`)
+- DSH Plugin session lifecycle stability
+- DSH Plugin Cordis package ID mismatch
+- Observation thumbnail media type sniffing
+- Upload/download race conditions and layout bypass issues
+- VOM repeated name and safety policy issues
+
+---
+
+*Previous releases used independent version numbers per component.*
+
+## CLI 0.1.11 / Extension 0.1.7 / DSH Plugin 0.1.2 — 2026-08-26 ~ 2026-08-29
+
+### Added
+- DSH Plugin sidebar integration, session lifecycle, and archive cleanup
+- Recorder iframe and OOPIF support
+
+### Changed
+- VOM functional refactor
+
+### Fixed
+- Recorder safety policy and bug fixes
+
+## CLI 0.1.10 / Extension 0.1.6 — 2026-08-08
+
+### Added
+- VOM observation recording and settled-state detection
+- Record overlay timer
+
+### Fixed
+- Browser keepalive disconnect handling
+
+## CLI 0.1.9 / Extension 0.1.5 — 2026-07-29
+
+### Added
+- CLI auto-update mechanism
+- Trace v3 protocol and recorder
+
+### Fixed
+- MV3 keepalive disconnect
+
+## CLI 0.1.8 / Extension 0.1.4 — 2026-07-22
+
+### Added
+- More browser interaction actions
+
+### Fixed
+- Windows named-pipe hash-only path issue
+
+## CLI 0.1.7 / Extension 0.1.3 — 2026-07-07
+
+Initial public release pair.
+
+## CLI 0.1.6 — 2026-06-30
+
+### Fixed
+- Minor CLI fixes
+
+## CLI 0.1.5 / Extension 0.1.2 — 2026-06-22
+
+First tagged releases.
diff --git a/apps/extension/src/entrypoints/popup/App.tsx b/apps/extension/src/entrypoints/popup/App.tsx
index 0756e9c..cc9287b 100644
--- a/apps/extension/src/entrypoints/popup/App.tsx
+++ b/apps/extension/src/entrypoints/popup/App.tsx
@@ -267,9 +267,9 @@ export function App() {
data-slot="popup-meta"
>
- {extensionVersion}
+ CLI {daemonVersion}
/
- {daemonVersion}
+ Ext {extensionVersion}
{t("popup.instanceTitle")}
diff --git a/scripts/release.mjs b/scripts/release.mjs
new file mode 100755
index 0000000..0300299
--- /dev/null
+++ b/scripts/release.mjs
@@ -0,0 +1,284 @@
+#!/usr/bin/env node
+/**
+ * Unified release script for BrowserSkill.
+ *
+ * Bumps CLI, Extension, and DSH Plugin to the same version, updates
+ * CHANGELOG.md, commits, tags, and optionally pushes.
+ *
+ * Usage:
+ * node scripts/release.mjs # bump + commit + tag
+ * node scripts/release.mjs --dry-run # preview changes only
+ * node scripts/release.mjs --push # also push to origin
+ * node scripts/release.mjs --push --only ext # push only ext tag
+ * node scripts/release.mjs --push --only cli,dsh # push only cli + dsh tags
+ * node scripts/release.mjs --push-tags --only ext # push tag(s) for an already-committed release
+ *
+ * Options:
+ * --dry-run Show what would change without writing anything
+ * --push After commit+tag, push the branch and all release tags
+ * --push-tags Push release tags only (skip version bump; useful for staged rollout)
+ * --only Comma-separated subset of: cli, ext, dsh (default: all three)
+ * --no-commit Write files but skip git commit and tagging
+ */
+
+import { execSync } from "node:child_process";
+import { readFileSync, writeFileSync } from "node:fs";
+import { parseArgs } from "node:util";
+
+const TAG_PREFIX = { cli: "cli-v", ext: "ext-v", dsh: "dsh-plugin-v" };
+const ALL_COMPONENTS = ["cli", "ext", "dsh"];
+
+const VERSION_FILES = {
+ cargo: "Cargo.toml",
+ extension: "apps/extension/package.json",
+ dshPlugin: "packages/dsh-plugin-browserskill/package.json",
+};
+
+// ---------------------------------------------------------------------------
+// CLI argument parsing
+// ---------------------------------------------------------------------------
+
+const { values, positionals } = parseArgs({
+ allowPositionals: true,
+ options: {
+ "dry-run": { type: "boolean", default: false },
+ push: { type: "boolean", default: false },
+ "push-tags": { type: "boolean", default: false },
+ "no-commit": { type: "boolean", default: false },
+ only: { type: "string", default: "" },
+ help: { type: "boolean", short: "h", default: false },
+ },
+});
+
+if (values.help) {
+ console.log(readFileSync(new URL(import.meta.url), "utf8").match(/\/\*\*([\s\S]*?)\*\//)?.[1] ?? "");
+ process.exit(0);
+}
+
+const dryRun = values["dry-run"];
+const pushAfter = values.push;
+const pushTagsOnly = values["push-tags"];
+const noCommit = values["no-commit"];
+const onlyRaw = values.only;
+const components = onlyRaw
+ ? onlyRaw.split(",").map((s) => s.trim().toLowerCase())
+ : ALL_COMPONENTS;
+
+for (const c of components) {
+ if (!ALL_COMPONENTS.includes(c)) {
+ console.error(`Unknown component "${c}". Valid: ${ALL_COMPONENTS.join(", ")}`);
+ process.exit(1);
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Push-tags-only mode (no version bump)
+// ---------------------------------------------------------------------------
+
+if (pushTagsOnly) {
+ const currentVersion = readCurrentVersions();
+ const ver = positionals[0] ?? currentVersion.cargo;
+ for (const c of components) {
+ const tag = `${TAG_PREFIX[c]}${ver}`;
+ console.log(` pushing tag ${tag}`);
+ if (!dryRun) {
+ run(`git push origin ${tag}`);
+ }
+ }
+ console.log("\nDone.");
+ process.exit(0);
+}
+
+// ---------------------------------------------------------------------------
+// Normal mode — require version argument
+// ---------------------------------------------------------------------------
+
+const newVersion = positionals[0];
+if (!newVersion) {
+ console.error("Usage: node scripts/release.mjs [--dry-run] [--push] [--only cli,ext,dsh]");
+ process.exit(1);
+}
+
+if (!/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(newVersion)) {
+ console.error(`Invalid semver: "${newVersion}"`);
+ process.exit(1);
+}
+
+// ---------------------------------------------------------------------------
+// Read current versions
+// ---------------------------------------------------------------------------
+
+function readCurrentVersions() {
+ const cargo = readFileSync(VERSION_FILES.cargo, "utf8");
+ const cargoVer = cargo.match(/^\[workspace\.package\]\s*\nversion\s*=\s*"([^"]+)"/m)?.[1]
+ ?? cargo.match(/^version\s*=\s*"([^"]+)"/m)?.[1];
+
+ const extPkg = JSON.parse(readFileSync(VERSION_FILES.extension, "utf8"));
+ const dshPkg = JSON.parse(readFileSync(VERSION_FILES.dshPlugin, "utf8"));
+
+ return { cargo: cargoVer, extension: extPkg.version, dshPlugin: dshPkg.version };
+}
+
+const current = readCurrentVersions();
+
+console.log("Current versions:");
+console.log(` CLI/Daemon (Cargo.toml): ${current.cargo}`);
+console.log(` Extension (package.json): ${current.extension}`);
+console.log(` DSH Plugin (package.json): ${current.dshPlugin}`);
+console.log(`\nBumping all → ${newVersion}`);
+if (dryRun) console.log(" (dry-run mode — no files will be written)\n");
+
+// ---------------------------------------------------------------------------
+// Bump Cargo.toml
+// ---------------------------------------------------------------------------
+
+function bumpCargo(version) {
+ let content = readFileSync(VERSION_FILES.cargo, "utf8");
+
+ // workspace.package version
+ content = content.replace(
+ /^(\[workspace\.package\]\s*\nversion\s*=\s*")([^"]+)(")/m,
+ `$1${version}$3`,
+ );
+
+ // workspace.dependencies.bsk-protocol version (for cargo publish)
+ content = content.replace(
+ /(bsk-protocol\s*=\s*\{\s*version\s*=\s*")([^"]+)(")/,
+ `$1${version}$3`,
+ );
+
+ return content;
+}
+
+function bumpPackageJson(filePath, version) {
+ const pkg = JSON.parse(readFileSync(filePath, "utf8"));
+ pkg.version = version;
+ return JSON.stringify(pkg, null, 2) + "\n";
+}
+
+// ---------------------------------------------------------------------------
+// Update CHANGELOG.md
+// ---------------------------------------------------------------------------
+
+function updateChangelog(version) {
+ const changelogPath = "CHANGELOG.md";
+ let content;
+ try {
+ content = readFileSync(changelogPath, "utf8");
+ } catch {
+ return null;
+ }
+
+ const today = new Date().toISOString().slice(0, 10);
+ const unreleased = /^## \[Unreleased\]/m;
+ if (unreleased.test(content)) {
+ content = content.replace(
+ unreleased,
+ `## [Unreleased]\n\n## [${version}] - ${today}`,
+ );
+ } else {
+ const versionPlaceholder = new RegExp(
+ `^## \\[${version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\]\\s*-\\s*\\d{4}-\\d{2}-XX`,
+ "m",
+ );
+ if (versionPlaceholder.test(content)) {
+ content = content.replace(versionPlaceholder, `## [${version}] - ${today}`);
+ }
+ }
+
+ return { path: changelogPath, content };
+}
+
+// ---------------------------------------------------------------------------
+// Write files
+// ---------------------------------------------------------------------------
+
+const writes = [
+ { path: VERSION_FILES.cargo, content: bumpCargo(newVersion), label: "Cargo.toml" },
+ { path: VERSION_FILES.extension, content: bumpPackageJson(VERSION_FILES.extension, newVersion), label: "Extension package.json" },
+ { path: VERSION_FILES.dshPlugin, content: bumpPackageJson(VERSION_FILES.dshPlugin, newVersion), label: "DSH Plugin package.json" },
+];
+
+const changelogUpdate = updateChangelog(newVersion);
+if (changelogUpdate) {
+ writes.push({ path: changelogUpdate.path, content: changelogUpdate.content, label: "CHANGELOG.md" });
+}
+
+for (const w of writes) {
+ console.log(` ✓ ${w.label}`);
+ if (!dryRun) {
+ writeFileSync(w.path, w.content);
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Update Cargo.lock (cargo check triggers it)
+// ---------------------------------------------------------------------------
+
+if (!dryRun) {
+ console.log("\n Updating Cargo.lock...");
+ run("cargo check --workspace 2>&1 || true");
+}
+
+// ---------------------------------------------------------------------------
+// Git commit + tag
+// ---------------------------------------------------------------------------
+
+if (noCommit || dryRun) {
+ if (dryRun) {
+ console.log(`\nWould commit: "chore(release): ${newVersion}"`);
+ console.log("Would create tags:");
+ for (const c of components) {
+ console.log(` ${TAG_PREFIX[c]}${newVersion}`);
+ }
+ }
+ console.log("\nDone.");
+ process.exit(0);
+}
+
+console.log("\nCommitting...");
+run("git add Cargo.toml Cargo.lock apps/extension/package.json packages/dsh-plugin-browserskill/package.json CHANGELOG.md 2>/dev/null || true");
+run(`git commit -m "chore(release): ${newVersion}"`);
+
+console.log("Tagging...");
+for (const c of components) {
+ const tag = `${TAG_PREFIX[c]}${newVersion}`;
+ run(`git tag ${tag}`);
+ console.log(` ✓ ${tag}`);
+}
+
+// ---------------------------------------------------------------------------
+// Push (optional)
+// ---------------------------------------------------------------------------
+
+if (pushAfter) {
+ console.log("\nPushing...");
+ run("git push origin HEAD");
+ for (const c of components) {
+ const tag = `${TAG_PREFIX[c]}${newVersion}`;
+ console.log(` pushing tag ${tag}`);
+ run(`git push origin ${tag}`);
+ }
+}
+
+console.log(`\nRelease ${newVersion} prepared successfully.`);
+if (!pushAfter) {
+ console.log("\nNext steps:");
+ console.log(" # Push branch");
+ console.log(" git push origin main");
+ console.log("");
+ console.log(" # Push extension tag first (store review takes time)");
+ console.log(` git push origin ext-v${newVersion}`);
+ console.log("");
+ console.log(" # Then push CLI and DSH plugin tags");
+ console.log(` git push origin cli-v${newVersion}`);
+ console.log(` git push origin dsh-plugin-v${newVersion}`);
+}
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+function run(cmd) {
+ execSync(cmd, { stdio: "inherit" });
+}