The CLI skill had grown to 20.8 KB of command tables and flag catalogs that
`bsk --help` already answers at runtime, so every agent paid for them on every
task. Rewrite both skills around the durable rules an agent cannot rediscover
-- session lifecycle, the observe/act/observe loop, the Agent Window boundary,
and the stop-on-success contract -- and delegate flag details to the CLI's own
help output. The CLI skill drops to 6.9 KB (-67%) and the DSH skill to 5.1 KB.
Keep the parts that help discovery has no answer for: the literal
`[hover first: ...]` observation marker, the `request-help` outcome values, the
complete command-name inventory, and the required flags agents guess wrong
(`fill --value`, `select --value`, `screenshot --out`, `emulate --device`).
Also bind `--no-focus` explicitly to `bsk session start`, since a bare mention
led agents to pass it to `bsk navigate`.
Validated with 36 agent runs of the `core` eval suite against a real browser,
comparing the old and new instructions with everything else held constant.
Pass rate did not regress (the lean version was better on `form-controls`) and
token cost per task came out even. The inventory and flag signatures cut
`--help` round-trips from 1.7 to 0.7 per task and removed the invented
`bsk back` / `bsk forward` calls the first draft produced.
Tighten the DSH size assertion to a range so the budget is bounded from both
sides and accidental truncation fails the test.
Co-authored-by: Cursor <cursoragent@cursor.com>
The exit code reflects RPC success only, not JS success. Document that programmatic callers must use --json and check the .ok field to detect JS errors. Addresses the discoverability gap raised in issue #133.
Agents were stopping on popup and status copy that looked like a hard incompatibility. Keep the connection usable, reframe exit 5 as an unknown method, and ask users to upgrade instead of aborting.
Co-authored-by: Cursor <cursoragent@cursor.com>
Reject legacy JSON --output before recording starts, persist the completed Trace before writing the bundle so `bsk record stop --output <dir>` can retry after an export failure, and tell users to pass --url when https://example.com/ does not load.
RecordedTrace validation now follows classify_value — numeric version
selects v3, otherwise v2 — so mixed pages/states and v2 envelopes with
a version field fail Schema checks before Recall import. Keep standalone
trace_step as a v2|v3 union for tools that validate a single step.
Split standalone v2/v3 schemas and drop TraceV2 deny_unknown_fields so legacy traces with extra keys still parse; mix and version checks stay in RecordedTrace.
Require Trace v3 version const 3, reject mixed pages/states payloads, and
align TypeScript selection optionality with the generated schema. Trace v2
on main already records hover steps, so the frozen v2 schema must accept
them instead of dropping them during reduction.
Co-authored-by: Cursor <cursoragent@cursor.com>
Define Trace v3 wire types and generated schemas while keeping protocol
1.1 peers compatible with 1.0. Default record start still produces Trace
v2, and only an explicit trace_version=3 request enables the new shape.
Refs #90
Co-authored-by: Cursor <cursoragent@cursor.com>
After folding the IPC connection count and activity timestamp into a
single mutex-protected IpcActivityState, the idle task no longer
references the connection count directly; should_exit_for_idle already
returns false while a connection is open or within the idle window.
Update the comment to describe the post-refactor control flow so it
matches the code.
The extension is now published to Edge Add-ons, but every install path
still pointed Edge users at the Chrome Web Store. Both READMEs and
AGENT_INSTALL.md now name a store per browser, and `bsk doctor` offers
both listings when no browser is connected.
Two Edge-specific gaps went with it: Edge blocks content-script injection
on its own storefront the same way Chrome does, so the borrow-confirmation
overlay must treat microsoftedge.microsoft.com/addons as non-injectable;
and the Chinese README still described the pre-install.ps1 manual Windows
setup.
Also give the DeepSeek Harness section its npm package link and the
profile-start step, so plugin users have a complete path.
Co-authored-by: Cursor <cursoragent@cursor.com>
Tool cancellation is now cooperative end-to-end: the queue stays busy until compensation completes, cancellation is checked cooperatively across the pipeline, the fallback window is compensated too, and cleanup_failed is surfaced explicitly. Includes a gated delayed-compensation E2E test.
chrome.tabs.captureVisibleTab reads back the window surface, which fails
outright on some Windows/Chrome combinations (Chromium
FAILURE_REASON_READBACK_FAILED: "Failed to capture tab: image readback
failed"), leaving full-tab screenshots without any working path.
Capture the full tab via CDP Page.captureScreenshot (fromSurface) when
the visible-tab capture rejects; the CDP path goes through the
renderer BeginFrame pipeline instead of the surface readback. When both
paths fail, return cdp_failed with data.reason=screenshot_capture_failed
carrying both underlying messages, and render a CLI hint that points at
the browser-side readback cause.
Refs #71
The per-user pipe name embedded %USERNAME% verbatim
(\\.\pipe\bsk-daemon-{user}-{hash}). When the username contains
apostrophes (e.g. z'z'f'l'g'y), NPFS behaviour diverges between create
and open: the daemon's CreateNamedPipeW reports success and logs that it
is listening, yet every client CreateFileW on the same name fails with
ERROR_FILE_NOT_FOUND / ERROR_PATH_NOT_FOUND, so the CLI can never reach
the daemon (issue #75).
The name is now hash-only (\\.\pipe\bsk-daemon-{hash}); the hash
already covers user + home, so uniqueness and per-user/BSK_HOME
isolation are unchanged, and no raw username characters can reach NPFS.
The pipe name is exchanged exclusively through daemon.json (the CLI
never computes it), so old/new CLI/daemon combinations stay compatible.
Unit tests pin the safe character set for apostrophe/space/Unicode
usernames and the per-(user, home) uniqueness.
Fixes#75
`bsk session start --no-focus` opens the Agent Window in the background
without stealing focus. The flag threads focused=false through
CLI -> daemon IPC -> tool.session_start -> chrome.windows.create; an
omitted `focused` keeps the extension default (true) so older clients
are unaffected.
Reworked onto current main so it composes with #52 window sizing:
AgentWindowApi.create / SessionManager.start (extension), start_session
(CLI) and the daemon's session start now take options objects/structs
({ size, focused }) instead of colliding positional params.
Co-authored-by: alectimison-maker <alec.timison@gmail.com>
The daemon's periodic update check now installs a newer release instead
of only refreshing the hint cache: it reuses the update.rs install path
(download, sha256 verify, extract, replace), skips the round while any
agent session is live, and otherwise restarts itself into the new binary
(spawns a detached replacement that waits for the old pid, then exits).
On by default; BSK_AUTO_UPDATE=off keeps the old cache/hint-only
behaviour. Windows can only stage the replacement, so it just logs that
a restart is needed. The CLI hint wording reflects the switch.
* fix(update): run periodic update check in daemon so hints actually fire
The CLI spawned a detached thread to refresh ~/.bsk/update-check.json,
but the process exits as soon as the command finishes, killing the
thread before the fetch could complete — so the cache was never
written and the new-version hint never showed.
Move the periodic check into the daemon: a tokio task ticks every
30 minutes (down from 24h), refreshes the cache when missing/stale,
and logs failures at warn/info into the daemon log. The CLI path is
now read-only: it prints the hint only from a fresh cache and never
spawns threads or touches the network.
* fix(update): decouple daemon refresh window so checks truly run every 30min
The refresh decision reused the 30min tick interval as its freshness
window, and with is_fresh()'s inclusive bound a cache written just
after tick N still counted as fresh at tick N+1 — so in steady state
the daemon only refetched every other tick (~60min).
Judge staleness against DAEMON_REFRESH_WINDOW (25min = 5/6 of the
tick) instead: every 30min tick now finds the cache stale and really
refreshes, while a daemon restarted with a younger-than-25min cache
still skips its first tick. CLI hint freshness semantics
(UPDATE_CHECK_INTERVAL = 30min) are unchanged.
* feat(install-skill): add Kimi Code harness support
Add kimi-code to the install-skill harness list: installs the bundled SKILL.md into ~/.kimi/skills (Kimi Code CLI's brand-group user skills directory, highest priority per https://github.com/MoonshotAI/kimi-cli/blob/main/docs/en/customization/skills.md). Detection signals: ~/.kimi directory and kimi/kimi-cli on PATH. Aliases: kimi, kimi-code, kimi-cli, kimicode, kimi_cli.
* fix(install-skill): point kimi-code harness at ~/.kimi-code and honor KIMI_CODE_HOME
Kimi Code CLI's canonical repo is MoonshotAI/kimi-code: user-level skills live in $KIMI_CODE_HOME/skills (default ~/.kimi-code/skills), not kimi-cli's ~/.kimi/skills. Detection checks the kimi-code home dir (env-aware label) and the kimi binary on PATH. Serialize env-mutating kimi tests with a shared mutex to avoid a parallel-test race on KIMI_CODE_HOME.
* feat(emulate): add mobile device emulation via CDP Emulation domain
Add `bsk emulate` so agents can debug mobile page behaviour by
emulating a device environment on a session tab:
- Viewport metrics (Emulation.setDeviceMetricsOverride), User-Agent
(Emulation.setUserAgentOverride, optional accept-language and UA
client-hints metadata), and touch (Emulation.setTouchEmulationEnabled
with maxTouchPoints).
- Seven CLI-side device presets (iphone-14, iphone-14-pro-max,
iphone-se, pixel-7, galaxy-s23, ipad-mini, galaxy-tab-s8) via
--device; manual flags (--width/--height/--dpr/--mobile/--ua/
--accept-language/--touch/--max-touch-points) work standalone or
override individual preset fields.
- `bsk emulate --off` clears every override (clearDeviceMetricsOverride
+ touch disabled + empty UA override) and restores the real
environment.
- Overrides are per-tab (CDP per-target) and not inherited by new tabs;
the result JSON carries a note reminding agents to re-apply.
Wire: new tool.emulate method end-to-end (bsk-protocol types + JSON
schemas + Method::ToolEmulate classified BrowserMutation, daemon
forwarding, CLI subcommand, extension tool handler + ChromiumCdp
Emulation wrappers, dispatcher registration), docs in both SKILL.md
copies, and unit tests on every layer.
* refine(emulate): merge per-tab state and align preset override semantics
- extension: remember the applied emulation state per tab and merge each new request onto it field by field before applying, so a later --width/--height no longer resets an earlier preset's dpr/mobile; --off clears the stored state along with the CDP overrides
- extension: cdp_failed errors now note that already-applied overrides were not rolled back and can be reset with --off
- cli: validate width/height pairing and metric requirements on the merged preset+flags result, so --device iphone-14 --width 500 keeps the preset height
- cli: add --no-mobile/--no-touch (conflicting with --mobile/--touch) to turn preset mobile/touch fields back off
- docs: describe the merge behaviour, --no-mobile/--no-touch and --off state clearing in both SKILL.md copies
- `bsk session start --width <px> --height <px>` opens the Agent Window
with the given outer size (chrome.windows.create)
- new `bsk window resize --session <id> --width <px> --height <px>`
resizes the Agent Window at runtime (chrome.windows.update)
- both validated on CLI, daemon and extension sides (100..=7680 CSS px)
- protocol: new tool.window_resize method (BrowserMutation) and
optional width/height on tool.session_start params; schemas regenerated
`bsk install-skill --all` installed the bundled skill into every supported harness directory (detected or not), creating junk paths like ~/.claude/skills on machines without those harnesses. --all now targets detected harnesses only and errors with guidance when none are found; --harness remains the explicit override. The --yes non-interactive branch shares the same detected-only logic.
* feat(request-help): add BSK_REQUEST_HELP=off to disable blocking help requests
* style(extension): format HelpOutcome union per biome
* docs(skill): condense BSK_REQUEST_HELP section
Add a popup launcher + action-recording panel that builds a copyable
`bsk record start` prompt, add `--browser` targeting, and default `--url`
to https://example.com/ when omitted. Drop the popup browser-alias field.
Move the reaper's silence timeout and scan cadence into DaemonConfig
(defaulting to the existing 60s/15s constants) with a
`with_browser_liveness` builder, so tests can drive the task in
sub-second windows.
Add an integration test that runs a real daemon and asserts a silent
heartbeat-capable browser — and its sessions — get reaped, while a
legacy browser that never heartbeated is left untouched.
Co-authored-by: Cursor <cursoragent@cursor.com>
Only reap browsers that have sent at least one `system.heartbeat`.
A pre-heartbeat extension never opts in, so it is never dropped on
silence and keeps relying on socket-close detection exactly as before.
This removes a backward-compat regression where a new daemon paired
with an old extension would wrongly reap a live-but-idle connection
and then report `no_browser_connected` on the next command.
- Add a `heartbeat_seen` flag to `BrowserClient`, set when a heartbeat
event arrives, and exclude never-heartbeated clients from
`stale_browsers`.
- Cover both the opt-in and the legacy-survivor cases in unit tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
The extension emits the literal event string "system.heartbeat";
assert the daemon-side serde name serialises to the same value and
round-trips from the exact frame shape the extension sends, so a
future rename cannot silently break keepalive and liveness.
Co-authored-by: Cursor <cursoragent@cursor.com>
After a long idle the service worker is only revived on its next
keepalive-alarm tick (up to 30s away), so the previous 5s wait made
the first command after idle fail with `no_browser_connected` even
though the extension was about to reconnect.
- Raise the daemon's `EXTENSION_CONNECT_WAIT` (used by `session.start`)
to 35s so it comfortably covers one alarm period plus handshake slack.
- Bump the CLI `session start` IPC timeout to that window plus slack so
it never gives up before the daemon answers, and print a one-line
"waiting for browser extension to connect…" hint when it actually
waits.
- Keep `bsk status` / `browsers` on their own short 5s wait so quick
informational commands are not slowed down.
Co-authored-by: Cursor <cursoragent@cursor.com>