diff --git a/CHANGELOG.md b/CHANGELOG.md index 50eff50cc..187c8fbcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,10 +20,12 @@ - Config/Doctor: remove legacy Clawdis env fallbacks and config/service migrations (Clawdbot-only). - Browser: add Chrome extension relay takeover mode (toolbar button), plus `clawdbot browser extension install/path` and remote browser control via `clawdbot browser serve` + `browser.controlToken`. - CLI/Docs: add per-command CLI doc pages and link them from `clawdbot --help`. +- Browser: copy the installed Chrome extension path to clipboard after `clawdbot browser extension install/path`. ### Fixes - Browser: add tests for snapshot labels/efficient query params and labeled image responses. - macOS: ensure launchd log directory exists with a test-only override. (#909) — thanks @roshanasingh4. +- Packaging: run `pnpm build` on `prepack` so npm publishes include fresh `dist/` output. - Telegram: register dock native commands with underscores to avoid `BOT_COMMAND_INVALID` (#929, fixes #901) — thanks @grp06. - Google: downgrade unsigned thinking blocks before send to avoid missing signature errors. - Agents: make user time zone and 24-hour time explicit in the system prompt. (#859) — thanks @CashWilliams. diff --git a/package.json b/package.json index f29426ec7..8c7d12540 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "scripts": { "dev": "tsx src/entry.ts", "postinstall": "node scripts/postinstall.js", + "prepack": "pnpm build", "docs:list": "tsx scripts/docs-list.ts", "docs:bin": "bun build scripts/docs-list.ts --compile --outfile bin/docs-list", "docs:dev": "cd docs && mint dev", diff --git a/src/cli/browser-cli-extension.test.ts b/src/cli/browser-cli-extension.test.ts index a844aae7b..eab4b3f82 100644 --- a/src/cli/browser-cli-extension.test.ts +++ b/src/cli/browser-cli-extension.test.ts @@ -2,7 +2,22 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; + +const copyToClipboard = vi.fn(); +const runtime = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), +}; + +vi.mock("../infra/clipboard.js", () => ({ + copyToClipboard, +})); + +vi.mock("../runtime.js", () => ({ + defaultRuntime: runtime, +})); describe("browser extension install", () => { it("installs into the state dir (never node_modules)", async () => { @@ -16,4 +31,37 @@ describe("browser extension install", () => { expect(fs.existsSync(path.join(result.path, "manifest.json"))).toBe(true); expect(result.path.includes("node_modules")).toBe(false); }); + + it("copies extension path to clipboard", async () => { + const prev = process.env.CLAWDBOT_STATE_DIR; + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-ext-path-")); + process.env.CLAWDBOT_STATE_DIR = tmp; + + try { + copyToClipboard.mockReset(); + copyToClipboard.mockResolvedValue(true); + runtime.log.mockReset(); + runtime.error.mockReset(); + runtime.exit.mockReset(); + + const dir = path.join(tmp, "browser", "chrome-extension"); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, "manifest.json"), JSON.stringify({ manifest_version: 3 })); + + vi.resetModules(); + const { Command } = await import("commander"); + const { registerBrowserExtensionCommands } = await import("./browser-cli-extension.js"); + + const program = new Command(); + const browser = program.command("browser").option("--json", false); + registerBrowserExtensionCommands(browser, (cmd) => cmd.parent?.opts?.() as { json?: boolean }); + + await program.parseAsync(["browser", "extension", "path"], { from: "user" }); + + expect(copyToClipboard).toHaveBeenCalledWith(dir); + } finally { + if (prev === undefined) delete process.env.CLAWDBOT_STATE_DIR; + else process.env.CLAWDBOT_STATE_DIR = prev; + } + }); }); diff --git a/src/cli/browser-cli-extension.ts b/src/cli/browser-cli-extension.ts index bb89e46eb..991dd5abf 100644 --- a/src/cli/browser-cli-extension.ts +++ b/src/cli/browser-cli-extension.ts @@ -6,6 +6,7 @@ import type { Command } from "commander"; import { STATE_DIR_CLAWDBOT } from "../config/paths.js"; import { danger, info } from "../globals.js"; +import { copyToClipboard } from "../infra/clipboard.js"; import { defaultRuntime } from "../runtime.js"; import { movePathToTrash } from "../browser/trash.js"; import { formatDocsLink } from "../terminal/links.js"; @@ -76,9 +77,11 @@ export function registerBrowserExtensionCommands( return; } defaultRuntime.log(installed.path); + const copied = await copyToClipboard(installed.path).catch(() => false); defaultRuntime.error( info( [ + copied ? "Copied to clipboard." : "Copy to clipboard unavailable.", "Next:", `- Chrome → chrome://extensions → enable “Developer mode”`, `- “Load unpacked” → select: ${installed.path}`, @@ -93,7 +96,7 @@ export function registerBrowserExtensionCommands( ext .command("path") .description("Print the path to the installed Chrome extension (load unpacked)") - .action((_opts, cmd) => { + .action(async (_opts, cmd) => { const parent = parentOpts(cmd); const dir = installedExtensionRootDir(); if (!hasManifest(dir)) { @@ -112,5 +115,7 @@ export function registerBrowserExtensionCommands( return; } defaultRuntime.log(dir); + const copied = await copyToClipboard(dir).catch(() => false); + if (copied) defaultRuntime.error(info("Copied to clipboard.")); }); } diff --git a/src/commands/dashboard.test.ts b/src/commands/dashboard.test.ts index 206781daf..6d53b529f 100644 --- a/src/commands/dashboard.test.ts +++ b/src/commands/dashboard.test.ts @@ -22,6 +22,9 @@ vi.mock("./onboard-helpers.js", () => ({ detectBrowserOpenSupport: mocks.detectBrowserOpenSupport, openUrl: mocks.openUrl, formatControlUiSshHint: mocks.formatControlUiSshHint, +})); + +vi.mock("../infra/clipboard.js", () => ({ copyToClipboard: mocks.copyToClipboard, })); diff --git a/src/commands/dashboard.ts b/src/commands/dashboard.ts index b79f80189..241ce8225 100644 --- a/src/commands/dashboard.ts +++ b/src/commands/dashboard.ts @@ -1,8 +1,8 @@ import { readConfigFileSnapshot, resolveGatewayPort } from "../config/config.js"; +import { copyToClipboard } from "../infra/clipboard.js"; import type { RuntimeEnv } from "../runtime.js"; import { defaultRuntime } from "../runtime.js"; import { - copyToClipboard, detectBrowserOpenSupport, formatControlUiSshHint, openUrl, diff --git a/src/commands/onboard-helpers.ts b/src/commands/onboard-helpers.ts index 3dfe2a356..12f041557 100644 --- a/src/commands/onboard-helpers.ts +++ b/src/commands/onboard-helpers.ts @@ -231,28 +231,6 @@ export async function openUrl(url: string): Promise { } } -export async function copyToClipboard(value: string): Promise { - const attempts: Array<{ argv: string[] }> = [ - { argv: ["pbcopy"] }, - { argv: ["xclip", "-selection", "clipboard"] }, - { argv: ["wl-copy"] }, - { argv: ["clip.exe"] }, // WSL / Windows - { argv: ["powershell", "-NoProfile", "-Command", "Set-Clipboard"] }, - ]; - for (const attempt of attempts) { - try { - const result = await runCommandWithTimeout(attempt.argv, { - timeoutMs: 3_000, - input: value, - }); - if (result.code === 0 && !result.killed) return true; - } catch { - // keep trying the next fallback - } - } - return false; -} - export async function ensureWorkspaceAndSessions( workspaceDir: string, runtime: RuntimeEnv, diff --git a/src/infra/clipboard.ts b/src/infra/clipboard.ts new file mode 100644 index 000000000..dd8aaf510 --- /dev/null +++ b/src/infra/clipboard.ts @@ -0,0 +1,23 @@ +import { runCommandWithTimeout } from "../process/exec.js"; + +export async function copyToClipboard(value: string): Promise { + const attempts: Array<{ argv: string[] }> = [ + { argv: ["pbcopy"] }, + { argv: ["xclip", "-selection", "clipboard"] }, + { argv: ["wl-copy"] }, + { argv: ["clip.exe"] }, // WSL / Windows + { argv: ["powershell", "-NoProfile", "-Command", "Set-Clipboard"] }, + ]; + for (const attempt of attempts) { + try { + const result = await runCommandWithTimeout(attempt.argv, { + timeoutMs: 3_000, + input: value, + }); + if (result.code === 0 && !result.killed) return true; + } catch { + // keep trying the next fallback + } + } + return false; +}