Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
ci:
name: Node ${{ matrix.node-version }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# pnpm 11 needs Node 22.13+, so Node 20 is not in the matrix.
node-version: ['22.x', '24.x']

steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v3
with:
version: 11

- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Typecheck
run: pnpm typecheck

- name: Unit tests
run: pnpm test:unit

- name: Integration tests
run: pnpm test:integration

# Playwright UI tests drive a live Electron app — not runnable on headless CI.
# Run locally with: pnpm test:ui
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ GhostPilot runs an MCP server inside the browser. Claude Code, Claude.ai or any
[![MCP tools](https://img.shields.io/badge/MCP%20tools-84-7c5cff)](#-tool-surface-84)
[![macOS](https://img.shields.io/badge/platform-macOS-000000?logo=apple&logoColor=white)](#build-a-dmg)
[![GitHub stars](https://img.shields.io/github/stars/tlejay/ghostpilot?style=flat&color=fbbf24)](https://github.com/tlejay/ghostpilot/stargazers)
[![CI](https://github.com/tlejay/ghostpilot/actions/workflows/ci.yml/badge.svg)](https://github.com/tlejay/ghostpilot/actions/workflows/ci.yml)

[**Quick start**](#-quick-start) · [**See it in action**](#-see-it-in-action) · [**Tools**](#-tool-surface-84) · [**Connect from Claude.ai**](#connect-from-claudeai-web--iphone--ipad) · [**Configuration**](#configuration)

Expand Down
6 changes: 5 additions & 1 deletion RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ published as a GitHub Release asset.
## Pre-flight

- Working tree clean (`git status` shows no uncommitted changes)
- All tests green:
- **CI must be green** on the release commit before tagging. Check the
[CI badge](https://github.com/tlejay/ghostpilot/actions/workflows/ci.yml)
or run locally:
```bash
pnpm typecheck
pnpm test:unit
Expand All @@ -17,6 +19,8 @@ published as a GitHub Release asset.
- `README.md` + `CHANGELOG.md` reflect the new version
- `assets/notices.json` up to date if `dependencies` changed
(`pnpm assets:licenses`)
- `types/ghostpilot-tools.d.ts` in sync with `src/main/mcp/tools.ts`
(run `pnpm gen:types` if you added tools; requires a local build)

## Cut a release

Expand Down
39 changes: 20 additions & 19 deletions src/main/mcp/tool-groups.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,22 +63,25 @@ test('histogram covers every category the source uses', () => {
}
});

test('total tool count matches plan §3 (~57 + 1 introspection + 2 desktop + 6 ext + 4 locator + 1 har + 5 ghostpilot-profile)', () => {
test('total tool count matches plan §3 + Plan #8 CDP Input tools', () => {
const histo = buildHistogram();
const total = Object.values(histo).reduce((a, b) => a + b, 0);
// 57 pre-existing tools + tool_categories (lifecycle) + desktop_screenshot
// + set_window_bounds + 6 ext_* + 4 locator_* (Plan #2) + export_har (Plan #6)
// + 5 ghostpilot-profile tools (Plan #5: list/current/create/delete/switch) = 76.
assert.equal(total, 76, `expected 76 total tool registrations, got ${total}`);
// + 5 ghostpilot-profile tools (Plan #5: list/current/create/delete/switch) = 76
// + 7 input tools (Plan #8: CDP Input mouse+keyboard+scroll, 2026-05-24)
// + 1 interact (get_by_role gesture, 2026-05-24) = 84.
assert.equal(total, 84, `expected 84 total tool registrations, got ${total}`);
});

test('per-category counts match plan §3', () => {
test('per-category counts match plan §3 + Plan #8', () => {
const histo = buildHistogram();
const expected: Record<ToolCategory, number> = {
nav: 4,
tabs: 4,
interact: 7,
interact: 8, // 7 original + 1 added 2026-05-24
inspect: 7, // 6 + a11y_snapshot
input: 7, // CDP Input tools: Plan #8 (mouse+keyboard+scroll, 2026-05-24)
network: 3, // list_network_requests + clear_network_requests + export_har (Plan #6, 2026-05-18)
console: 2,
performance: 3,
Expand Down Expand Up @@ -106,28 +109,26 @@ test('per-category counts match plan §3', () => {
});

// §7.2 expected-count matrix.
test('§7.2 row 1: unset → all 76 tools', () => {
assert.equal(expectedEnabled(undefined), 76);
test('§7.2 row 1: unset → all 84 tools', () => {
assert.equal(expectedEnabled(undefined), 84);
});

test("§7.2 row 2: 'all' → all 76 tools", () => {
assert.equal(expectedEnabled('all'), 76);
test("§7.2 row 2: 'all' → all 84 tools", () => {
assert.equal(expectedEnabled('all'), 84);
});

test("§7.2 row 3: 'core' → 22 (nav+tabs+interact+inspect) + 3 lifecycle = 25", () => {
// Plan §3 says core = 22 tools. With lifecycle always on (+3), that's 25.
assert.equal(expectedEnabled('core'), 25);
test("§7.2 row 3: 'core' → 23 (nav+tabs+interact+inspect) + 3 lifecycle = 26", () => {
// core = nav(4) + tabs(4) + interact(8) + inspect(7) = 23 + lifecycle(3) = 26.
assert.equal(expectedEnabled('core'), 26);
});

test("§7.2 row 4: 'nav,interact' → 4 + 7 + 3 lifecycle = 14", () => {
// Plan §7.2 said 11; that figure was estimated assuming lifecycle wasn't
// double-counted. With our 3-tool lifecycle (incl. new tool_categories) it's
// 4 + 7 + 3 = 14.
assert.equal(expectedEnabled('nav,interact'), 14);
test("§7.2 row 4: 'nav,interact' → 4 + 8 + 3 lifecycle = 15", () => {
// nav(4) + interact(8) + lifecycle(3) = 15.
assert.equal(expectedEnabled('nav,interact'), 15);
});

test("§7.2 row 5: 'nav,interact,network,-network' → 4 + 7 + 3 lifecycle = 14", () => {
assert.equal(expectedEnabled('nav,interact,network,-network'), 14);
test("§7.2 row 5: 'nav,interact,network,-network' → 4 + 8 + 3 lifecycle = 15", () => {
assert.equal(expectedEnabled('nav,interact,network,-network'), 15);
});

test("§7.2 row 6: 'nonsense' → lifecycle only (3)", () => {
Expand Down
59 changes: 58 additions & 1 deletion types/ghostpilot-tools.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// AUTO-GENERATED — do not edit by hand. Regenerate with `pnpm gen:types`.
// Source: src/main/mcp/tools.ts + src/main/mcp/locator-tools.ts
// GhostPilot version: 0.8.1
// Tools captured: 77
// Tools captured: 84

/** Return a simplified accessibility tree for the page (role, name, value, focusable). Equivalent to chrome-devtools take_snapshot — useful for letting an LLM navigate by semantic role instead of CSS selectors. */
export interface A11ySnapshotInput { interestingOnly?: boolean; tabId?: string }
Expand Down Expand Up @@ -167,6 +167,10 @@ export type GetPageTextOutput = unknown;
export interface GetSkillInput { id: string }
export type GetSkillOutput = unknown;

/** Return viewport dimensions and devicePixelRatio. Use this to understand the relationship between screenshot pixel coordinates and CSS layout coordinates. All mouse/scroll tools accept device pixels directly — you do not need to normalize manually, but this tool is useful for sanity-checking or computing relative positions. */
export interface GetViewportInfoInput { tabId?: string }
export type GetViewportInfoOutput = unknown;

/** Navigate back. */
export interface GoBackInput { tabId?: string }
export type GoBackOutput = unknown;
Expand Down Expand Up @@ -203,6 +207,14 @@ export type ImportChromeBookmarksOutput = unknown;
export interface ImportChromeHistoryInput { profile?: string; limit?: number }
export type ImportChromeHistoryOutput = unknown;

/** Press a named key or key combo. Uses Playwright shorthand names: "Enter", "Tab", "Escape", "Backspace", "Delete", "ArrowUp/Down/Left/Right", "Home", "End", "PageUp", "PageDown", "Space", "F1"–"F12". Combos: "Ctrl+A", "Meta+V", "Shift+Tab", "Ctrl+Shift+I". Meta = Cmd on Mac. Case-insensitive. */
export interface KeyboardKeyInput { key: string; tabId?: string }
export type KeyboardKeyOutput = unknown;

/** Insert text into the focused element via Input.insertText — works on any focused input, contenteditable, or rich editor. Focus the target first with mouse_click. Prefer this over keyboard_key for bulk text insertion. */
export interface KeyboardTypeInput { text: string; tabId?: string }
export type KeyboardTypeOutput = unknown;

/** Run a Lighthouse audit against the given URL (or the active tab's URL). Spawns a private headless Chrome via chrome-launcher — Google Chrome must be installed. Returns category scores plus the path to the full HTML report. */
export interface LighthouseAuditInput { url?: string; formFactor?: "mobile" | "desktop"; categories?: ("performance" | "accessibility" | "best-practices" | "seo" | "pwa")[] }
export type LighthouseAuditOutput = unknown;
Expand Down Expand Up @@ -239,6 +251,18 @@ export type ListTabsOutput = unknown;
export type ListYtdlpJobsInput = Record<string, never>;
export type ListYtdlpJobsOutput = unknown;

/** Click at (x, y) in device pixels. Emits trusted CDP pointer events that bypass SPA event-handler restrictions (unlike the DOM-selector click tool). button: "left"|"right"|"middle". count=2 for double-click. */
export interface MouseClickInput { x: number; y: number; button?: "left" | "right" | "middle"; count?: number; tabId?: string }
export type MouseClickOutput = unknown;

/** Click-drag from (x1,y1) to (x2,y2) in device pixels. Interpolates `steps` mouseMoved events between start and end for smooth drag (required by canvas, sortable-list, and slider UIs). Default steps=10. */
export interface MouseDragInput { x1: number; y1: number; x2: number; y2: number; steps?: number; tabId?: string }
export type MouseDragOutput = unknown;

/** Move the mouse cursor to (x, y) in device pixels (as seen in a screenshot). GhostPilot divides by window.devicePixelRatio internally — pass screenshot coords directly. Useful to trigger hover states or tooltips before clicking. */
export interface MouseMoveInput { x: number; y: number; tabId?: string }
export type MouseMoveOutput = unknown;

/** Navigate the given tab (or active tab) to a URL. */
export interface NavigateInput { url: string; tabId?: string }
export type NavigateOutput = unknown;
Expand Down Expand Up @@ -271,6 +295,10 @@ export type SaveSkillOutput = unknown;
export interface ScreenshotInput { tabId?: string }
export type ScreenshotOutput = unknown;

/** Scroll the viewport at (x, y) by (delta_x, delta_y) device pixels. Positive delta_y = scroll down (web convention). Coordinates are device pixels; delta is also scaled by DPR internally. */
export interface ScrollInput { x: number; y: number; delta_x?: number; delta_y?: number; tabId?: string }
export type ScrollOutput = unknown;

/** Resize and/or move the GhostPilot main window. Omitted axes keep their current value; set center:true to center on the active display (ignores x/y). New bounds are persisted to <userData>/window-bounds.json so they survive a relaunch. Returns the bounds as set plus the display the window ended up on. */
export interface SetWindowBoundsInput { x?: number; y?: number; width?: number; height?: number; center?: boolean }
export type SetWindowBoundsOutput = unknown;
Expand Down Expand Up @@ -359,6 +387,7 @@ export type GhostPilotToolName =
| 'get_page_html'
| 'get_page_text'
| 'get_skill'
| 'get_viewport_info'
| 'go_back'
| 'go_forward'
| 'handle_next_dialog'
Expand All @@ -368,6 +397,8 @@ export type GhostPilotToolName =
| 'hover'
| 'import_chrome_bookmarks'
| 'import_chrome_history'
| 'keyboard_key'
| 'keyboard_type'
| 'lighthouse_audit'
| 'list_chrome_profiles'
| 'list_console_messages'
Expand All @@ -377,6 +408,9 @@ export type GhostPilotToolName =
| 'list_skills'
| 'list_tabs'
| 'list_ytdlp_jobs'
| 'mouse_click'
| 'mouse_drag'
| 'mouse_move'
| 'navigate'
| 'new_tab'
| 'performance_start_trace'
Expand All @@ -385,6 +419,7 @@ export type GhostPilotToolName =
| 'reload'
| 'save_skill'
| 'screenshot'
| 'scroll'
| 'set_window_bounds'
| 'stop'
| 'switch_ghostpilot_profile'
Expand All @@ -406,6 +441,7 @@ export type GhostPilotToolCategory =
| 'emulate'
| 'ext'
| 'history'
| 'input'
| 'inspect'
| 'interact'
| 'lifecycle'
Expand Down Expand Up @@ -462,6 +498,7 @@ export const TOOL_CATEGORY: { readonly [K in GhostPilotToolName]: GhostPilotTool
get_page_html: 'inspect',
get_page_text: 'inspect',
get_skill: 'skills',
get_viewport_info: 'input',
go_back: 'nav',
go_forward: 'nav',
handle_next_dialog: 'interact',
Expand All @@ -471,6 +508,8 @@ export const TOOL_CATEGORY: { readonly [K in GhostPilotToolName]: GhostPilotTool
hover: 'interact',
import_chrome_bookmarks: 'bookmarks',
import_chrome_history: 'history',
keyboard_key: 'input',
keyboard_type: 'input',
lighthouse_audit: 'performance',
list_chrome_profiles: 'profiles',
list_console_messages: 'console',
Expand All @@ -480,6 +519,9 @@ export const TOOL_CATEGORY: { readonly [K in GhostPilotToolName]: GhostPilotTool
list_skills: 'skills',
list_tabs: 'tabs',
list_ytdlp_jobs: 'ytdlp',
mouse_click: 'input',
mouse_drag: 'input',
mouse_move: 'input',
navigate: 'nav',
new_tab: 'tabs',
performance_start_trace: 'performance',
Expand All @@ -488,6 +530,7 @@ export const TOOL_CATEGORY: { readonly [K in GhostPilotToolName]: GhostPilotTool
reload: 'nav',
save_skill: 'skills',
screenshot: 'inspect',
scroll: 'input',
set_window_bounds: 'desktop',
stop: 'lifecycle',
switch_ghostpilot_profile: 'profiles',
Expand Down Expand Up @@ -543,6 +586,7 @@ export type GhostPilotToolCall =
| { name: 'get_page_html'; arguments: GetPageHtmlInput }
| { name: 'get_page_text'; arguments: GetPageTextInput }
| { name: 'get_skill'; arguments: GetSkillInput }
| { name: 'get_viewport_info'; arguments: GetViewportInfoInput }
| { name: 'go_back'; arguments: GoBackInput }
| { name: 'go_forward'; arguments: GoForwardInput }
| { name: 'handle_next_dialog'; arguments: HandleNextDialogInput }
Expand All @@ -552,6 +596,8 @@ export type GhostPilotToolCall =
| { name: 'hover'; arguments: HoverInput }
| { name: 'import_chrome_bookmarks'; arguments: ImportChromeBookmarksInput }
| { name: 'import_chrome_history'; arguments: ImportChromeHistoryInput }
| { name: 'keyboard_key'; arguments: KeyboardKeyInput }
| { name: 'keyboard_type'; arguments: KeyboardTypeInput }
| { name: 'lighthouse_audit'; arguments: LighthouseAuditInput }
| { name: 'list_chrome_profiles'; arguments: ListChromeProfilesInput }
| { name: 'list_console_messages'; arguments: ListConsoleMessagesInput }
Expand All @@ -561,6 +607,9 @@ export type GhostPilotToolCall =
| { name: 'list_skills'; arguments: ListSkillsInput }
| { name: 'list_tabs'; arguments: ListTabsInput }
| { name: 'list_ytdlp_jobs'; arguments: ListYtdlpJobsInput }
| { name: 'mouse_click'; arguments: MouseClickInput }
| { name: 'mouse_drag'; arguments: MouseDragInput }
| { name: 'mouse_move'; arguments: MouseMoveInput }
| { name: 'navigate'; arguments: NavigateInput }
| { name: 'new_tab'; arguments: NewTabInput }
| { name: 'performance_start_trace'; arguments: PerformanceStartTraceInput }
Expand All @@ -569,6 +618,7 @@ export type GhostPilotToolCall =
| { name: 'reload'; arguments: ReloadInput }
| { name: 'save_skill'; arguments: SaveSkillInput }
| { name: 'screenshot'; arguments: ScreenshotInput }
| { name: 'scroll'; arguments: ScrollInput }
| { name: 'set_window_bounds'; arguments: SetWindowBoundsInput }
| { name: 'stop'; arguments: StopInput }
| { name: 'switch_ghostpilot_profile'; arguments: SwitchGhostpilotProfileInput }
Expand Down Expand Up @@ -623,6 +673,7 @@ export interface GhostPilotToolMap {
get_page_html: { input: GetPageHtmlInput; output: GetPageHtmlOutput };
get_page_text: { input: GetPageTextInput; output: GetPageTextOutput };
get_skill: { input: GetSkillInput; output: GetSkillOutput };
get_viewport_info: { input: GetViewportInfoInput; output: GetViewportInfoOutput };
go_back: { input: GoBackInput; output: GoBackOutput };
go_forward: { input: GoForwardInput; output: GoForwardOutput };
handle_next_dialog: { input: HandleNextDialogInput; output: HandleNextDialogOutput };
Expand All @@ -632,6 +683,8 @@ export interface GhostPilotToolMap {
hover: { input: HoverInput; output: HoverOutput };
import_chrome_bookmarks: { input: ImportChromeBookmarksInput; output: ImportChromeBookmarksOutput };
import_chrome_history: { input: ImportChromeHistoryInput; output: ImportChromeHistoryOutput };
keyboard_key: { input: KeyboardKeyInput; output: KeyboardKeyOutput };
keyboard_type: { input: KeyboardTypeInput; output: KeyboardTypeOutput };
lighthouse_audit: { input: LighthouseAuditInput; output: LighthouseAuditOutput };
list_chrome_profiles: { input: ListChromeProfilesInput; output: ListChromeProfilesOutput };
list_console_messages: { input: ListConsoleMessagesInput; output: ListConsoleMessagesOutput };
Expand All @@ -641,6 +694,9 @@ export interface GhostPilotToolMap {
list_skills: { input: ListSkillsInput; output: ListSkillsOutput };
list_tabs: { input: ListTabsInput; output: ListTabsOutput };
list_ytdlp_jobs: { input: ListYtdlpJobsInput; output: ListYtdlpJobsOutput };
mouse_click: { input: MouseClickInput; output: MouseClickOutput };
mouse_drag: { input: MouseDragInput; output: MouseDragOutput };
mouse_move: { input: MouseMoveInput; output: MouseMoveOutput };
navigate: { input: NavigateInput; output: NavigateOutput };
new_tab: { input: NewTabInput; output: NewTabOutput };
performance_start_trace: { input: PerformanceStartTraceInput; output: PerformanceStartTraceOutput };
Expand All @@ -649,6 +705,7 @@ export interface GhostPilotToolMap {
reload: { input: ReloadInput; output: ReloadOutput };
save_skill: { input: SaveSkillInput; output: SaveSkillOutput };
screenshot: { input: ScreenshotInput; output: ScreenshotOutput };
scroll: { input: ScrollInput; output: ScrollOutput };
set_window_bounds: { input: SetWindowBoundsInput; output: SetWindowBoundsOutput };
stop: { input: StopInput; output: StopOutput };
switch_ghostpilot_profile: { input: SwitchGhostpilotProfileInput; output: SwitchGhostpilotProfileOutput };
Expand Down
Loading