From 1bed5300b24884d62d67c357af1248e8958fe23d Mon Sep 17 00:00:00 2001 From: tlejay <32982770+tlejay@users.noreply.github.com> Date: Sun, 31 May 2026 01:42:31 +0700 Subject: [PATCH 1/3] =?UTF-8?q?feat(ci):=20GitHub=20Actions=20CI=20matrix?= =?UTF-8?q?=20=E2=80=94=20Plan=20#12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Node 20 + 22 matrix: typecheck on both, unit + integration tests on 22 (--experimental-strip-types requires Node 22.6+) - pnpm 9 with dependency cache for fast cold starts - Playwright UI tests explicitly excluded (require live Electron app) - Fix tool-groups.integration.test.ts: update counts for Plan #8 CDP Input tools (84 total, new 'input' category with 7 tools, interact +1) - Regenerate types/ghostpilot-tools.d.ts (77 → 84 tools, in sync with source) - README: CI badge added - RELEASING.md: CI-green requirement documented + gen:types reminder Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 47 ++++++++++++++++ README.md | 1 + RELEASING.md | 6 +- src/main/mcp/tool-groups.integration.test.ts | 39 ++++++------- types/ghostpilot-tools.d.ts | 59 +++++++++++++++++++- 5 files changed, 131 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8bff33b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +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: + node-version: ['20.x', '22.x'] + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v3 + with: + version: 9 + + - 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 + + # node --experimental-strip-types requires Node 22.6+ — tests are skipped on Node 20. + # Typecheck above still validates TS correctness on both versions. + - name: Unit tests + if: startsWith(matrix.node-version, '22') + run: pnpm test:unit + + - name: Integration tests + if: startsWith(matrix.node-version, '22') + run: pnpm test:integration + + # Playwright UI tests drive a live Electron app — not runnable on headless CI. + # Run locally with: pnpm test:ui diff --git a/README.md b/README.md index daad396..6937a0d 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/RELEASING.md b/RELEASING.md index 4c05232..1703cdc 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -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 @@ -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 diff --git a/src/main/mcp/tool-groups.integration.test.ts b/src/main/mcp/tool-groups.integration.test.ts index 5413d3c..cee1023 100644 --- a/src/main/mcp/tool-groups.integration.test.ts +++ b/src/main/mcp/tool-groups.integration.test.ts @@ -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 = { 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, @@ -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)", () => { diff --git a/types/ghostpilot-tools.d.ts b/types/ghostpilot-tools.d.ts index ac972a3..b8dc200 100644 --- a/types/ghostpilot-tools.d.ts +++ b/types/ghostpilot-tools.d.ts @@ -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 } @@ -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; @@ -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; @@ -239,6 +251,18 @@ export type ListTabsOutput = unknown; export type ListYtdlpJobsInput = Record; 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; @@ -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 /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; @@ -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' @@ -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' @@ -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' @@ -385,6 +419,7 @@ export type GhostPilotToolName = | 'reload' | 'save_skill' | 'screenshot' + | 'scroll' | 'set_window_bounds' | 'stop' | 'switch_ghostpilot_profile' @@ -406,6 +441,7 @@ export type GhostPilotToolCategory = | 'emulate' | 'ext' | 'history' + | 'input' | 'inspect' | 'interact' | 'lifecycle' @@ -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', @@ -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', @@ -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', @@ -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', @@ -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 } @@ -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 } @@ -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 } @@ -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 } @@ -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 }; @@ -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 }; @@ -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 }; @@ -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 }; From 3eaf717175e65f277277b3e85ef0d9e96043d81e Mon Sep 17 00:00:00 2001 From: tlejay Date: Sat, 26 Sep 2026 08:03:42 +0700 Subject: [PATCH 2/3] fix(ci): use pnpm 11 to match the lockfile and allowBuilds workspace setting pnpm 9 rejects pnpm-workspace.yaml without a packages field. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8bff33b..fc641e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: - uses: pnpm/action-setup@v3 with: - version: 9 + version: 11 - uses: actions/setup-node@v4 with: From 82f4bf722c42765b9d95ad71792a5386cc140432 Mon Sep 17 00:00:00 2001 From: tlejay Date: Sat, 26 Sep 2026 08:13:21 +0700 Subject: [PATCH 3/3] fix(ci): test on Node 22 and 24 (pnpm 11 needs Node 22.13+) Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc641e8..3072429 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,8 @@ jobs: strategy: fail-fast: false matrix: - node-version: ['20.x', '22.x'] + # 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 @@ -33,14 +34,10 @@ jobs: - name: Typecheck run: pnpm typecheck - # node --experimental-strip-types requires Node 22.6+ — tests are skipped on Node 20. - # Typecheck above still validates TS correctness on both versions. - name: Unit tests - if: startsWith(matrix.node-version, '22') run: pnpm test:unit - name: Integration tests - if: startsWith(matrix.node-version, '22') run: pnpm test:integration # Playwright UI tests drive a live Electron app — not runnable on headless CI.