@hover-dev/core 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +59 -0
  3. package/dist/agents/argv.d.ts +11 -0
  4. package/dist/agents/argv.d.ts.map +1 -0
  5. package/dist/agents/argv.js +23 -0
  6. package/dist/agents/claude.d.ts +3 -0
  7. package/dist/agents/claude.d.ts.map +1 -0
  8. package/dist/agents/claude.js +145 -0
  9. package/dist/agents/detect.d.ts +16 -0
  10. package/dist/agents/detect.d.ts.map +1 -0
  11. package/dist/agents/detect.js +34 -0
  12. package/dist/agents/index.d.ts +6 -0
  13. package/dist/agents/index.d.ts.map +1 -0
  14. package/dist/agents/index.js +5 -0
  15. package/dist/agents/invoke.d.ts +10 -0
  16. package/dist/agents/invoke.d.ts.map +1 -0
  17. package/dist/agents/invoke.js +70 -0
  18. package/dist/agents/registry.d.ts +12 -0
  19. package/dist/agents/registry.d.ts.map +1 -0
  20. package/dist/agents/registry.js +15 -0
  21. package/dist/agents/types.d.ts +88 -0
  22. package/dist/agents/types.d.ts.map +1 -0
  23. package/dist/agents/types.js +23 -0
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +2 -0
  27. package/dist/playwright/cdpStatus.d.ts +29 -0
  28. package/dist/playwright/cdpStatus.d.ts.map +1 -0
  29. package/dist/playwright/cdpStatus.js +96 -0
  30. package/dist/playwright/launchChrome.d.ts +29 -0
  31. package/dist/playwright/launchChrome.d.ts.map +1 -0
  32. package/dist/playwright/launchChrome.js +137 -0
  33. package/dist/playwright/preflight.d.ts +31 -0
  34. package/dist/playwright/preflight.d.ts.map +1 -0
  35. package/dist/playwright/preflight.js +71 -0
  36. package/dist/scripts/start-chrome.d.ts +3 -0
  37. package/dist/scripts/start-chrome.d.ts.map +1 -0
  38. package/dist/scripts/start-chrome.js +23 -0
  39. package/dist/service.d.ts +22 -0
  40. package/dist/service.d.ts.map +1 -0
  41. package/dist/service.js +485 -0
  42. package/dist/skills/writeSkill.d.ts +50 -0
  43. package/dist/skills/writeSkill.d.ts.map +1 -0
  44. package/dist/skills/writeSkill.js +169 -0
  45. package/dist/specs/humanSteps.d.ts +25 -0
  46. package/dist/specs/humanSteps.d.ts.map +1 -0
  47. package/dist/specs/humanSteps.js +97 -0
  48. package/dist/specs/writeCaseCsv.d.ts +28 -0
  49. package/dist/specs/writeCaseCsv.d.ts.map +1 -0
  50. package/dist/specs/writeCaseCsv.js +140 -0
  51. package/dist/specs/writeSpec.d.ts +27 -0
  52. package/dist/specs/writeSpec.d.ts.map +1 -0
  53. package/dist/specs/writeSpec.js +265 -0
  54. package/mcp.config.json +12 -0
  55. package/package.json +78 -0
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Local CLI Agent First — agent abstraction layer.
3
+ *
4
+ * Hover does not bundle any AI runtime. It spawns whatever coding-agent CLI the
5
+ * user has on PATH (`claude`, `codex`, `cursor`, `aider`, ...) and treats it as
6
+ * a strategy implementation behind this interface.
7
+ *
8
+ * To add a new agent: write an AgentDescriptor and register it in registry.ts.
9
+ */
10
+ export type AgentProtocol = 'argv' | 'stdin' | 'acp' | 'pi-rpc';
11
+ export type StreamFormat = 'stream-json' | 'sse' | 'plain-text' | 'json-lines';
12
+ export declare class UnsupportedAgentProtocolError extends Error {
13
+ constructor(message: string);
14
+ }
15
+ export declare class AgentNotInstalledError extends Error {
16
+ readonly agentId: string;
17
+ constructor(agentId: string);
18
+ }
19
+ export interface InvokeOptions {
20
+ agentId: string;
21
+ prompt: string;
22
+ mcpConfig?: string;
23
+ allowedTools?: string[];
24
+ disallowedTools?: string[];
25
+ maxBudgetUsd?: number;
26
+ model?: string;
27
+ cwd?: string;
28
+ sessionId?: string;
29
+ /** Extra text appended to the agent's system prompt (claude: via
30
+ * --append-system-prompt). Used to inject session-specific context like
31
+ * "the user's current Chrome tab is already on http://localhost:5173/,
32
+ * don't browser_navigate there". */
33
+ appendSystemPrompt?: string;
34
+ /** Aborts the spawned child if signaled. Used to stop an orphan run when
35
+ * the WebSocket caller disconnects (e.g. user reloads the dev page). */
36
+ signal?: AbortSignal;
37
+ }
38
+ /**
39
+ * Normalized event emitted by every agent, regardless of its native wire format.
40
+ * Each agent's `parseEvent` translates its own stream into these.
41
+ */
42
+ export type InvokeEvent = {
43
+ kind: 'session_start';
44
+ sessionId: string;
45
+ model?: string;
46
+ } | {
47
+ kind: 'mcp_status';
48
+ server: string;
49
+ status: string;
50
+ } | {
51
+ kind: 'tool_use';
52
+ tool: string;
53
+ input: unknown;
54
+ } | {
55
+ kind: 'tool_result';
56
+ isError?: boolean;
57
+ preview?: string;
58
+ } | {
59
+ kind: 'text';
60
+ text: string;
61
+ }
62
+ /** Running cost / turn-count update emitted mid-session so the widget can
63
+ * show a live $ counter without waiting for session_end. Claude Code's
64
+ * stream-json includes `total_cost_usd` on intermediate result-ish events;
65
+ * agents that don't surface running cost simply never emit this. */
66
+ | {
67
+ kind: 'usage';
68
+ costUsd?: number;
69
+ turns?: number;
70
+ } | {
71
+ kind: 'session_end';
72
+ turns?: number;
73
+ costUsd?: number;
74
+ isError?: boolean;
75
+ summary?: string;
76
+ } | {
77
+ kind: 'raw';
78
+ line: string;
79
+ };
80
+ export interface AgentDescriptor {
81
+ id: string;
82
+ binName: string;
83
+ protocol: AgentProtocol;
84
+ streamFormat: StreamFormat;
85
+ buildArgs(opts: InvokeOptions): string[];
86
+ parseEvent(line: string): InvokeEvent[];
87
+ }
88
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/agents/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,aAAa,GACrB,MAAM,GACN,OAAO,GACP,KAAK,GACL,QAAQ,CAAC;AAEb,MAAM,MAAM,YAAY,GACpB,aAAa,GACb,KAAK,GACL,YAAY,GACZ,YAAY,CAAC;AAEjB,qBAAa,6BAA8B,SAAQ,KAAK;gBAC1C,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,sBAAuB,SAAQ,KAAK;aACnB,OAAO,EAAE,MAAM;gBAAf,OAAO,EAAE,MAAM;CAI5C;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;yCAGqC;IACrC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;6EACyE;IACzE,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5D;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAClD;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5D;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AAChC;;;qEAGqE;GACnE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACnD;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC9F;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAElC,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,aAAa,CAAC;IACxB,YAAY,EAAE,YAAY,CAAC;IAC3B,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,EAAE,CAAC;IACzC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CAAC;CACzC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Local CLI Agent First — agent abstraction layer.
3
+ *
4
+ * Hover does not bundle any AI runtime. It spawns whatever coding-agent CLI the
5
+ * user has on PATH (`claude`, `codex`, `cursor`, `aider`, ...) and treats it as
6
+ * a strategy implementation behind this interface.
7
+ *
8
+ * To add a new agent: write an AgentDescriptor and register it in registry.ts.
9
+ */
10
+ export class UnsupportedAgentProtocolError extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = 'UnsupportedAgentProtocolError';
14
+ }
15
+ }
16
+ export class AgentNotInstalledError extends Error {
17
+ agentId;
18
+ constructor(agentId) {
19
+ super(`Agent "${agentId}" is not installed (binary not found on PATH).`);
20
+ this.agentId = agentId;
21
+ this.name = 'AgentNotInstalledError';
22
+ }
23
+ }
@@ -0,0 +1,3 @@
1
+ export * from './agents/index.js';
2
+ export { connectAndListTabs } from './playwright/preflight.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './agents/index.js';
2
+ export { connectAndListTabs } from './playwright/preflight.js';
@@ -0,0 +1,29 @@
1
+ export type CdpState = 'same-window' | 'wrong-window' | 'no-cdp';
2
+ export interface CdpStatusResult {
3
+ state: CdpState;
4
+ /** Tab count when state !== 'no-cdp'. */
5
+ tabCount?: number;
6
+ /** Matching tab URL inside the debug Chrome (only set for 'wrong-window'). */
7
+ matchingTabUrl?: string;
8
+ /** Browser product string from /json/version when state !== 'no-cdp'. */
9
+ browser?: string;
10
+ /** When state === 'no-cdp', the preflight reason. */
11
+ reason?: string;
12
+ }
13
+ export declare function checkCdpStatus(cdpUrl: string, pageUrl: string): Promise<CdpStatusResult>;
14
+ /**
15
+ * Bring the debug-Chrome tab matching `pageUrl`'s origin to the front. If no
16
+ * matching tab exists, open a new tab on the origin. Returns the URL of the
17
+ * tab that was focused (or opened) for logging.
18
+ *
19
+ * Uses a short-lived playwright-core connection — opens it, does the work,
20
+ * closes it. We don't keep a long-lived browser handle.
21
+ */
22
+ export declare function focusDebugTab(cdpUrl: string, pageUrl: string): Promise<{
23
+ ok: true;
24
+ focusedUrl: string;
25
+ } | {
26
+ ok: false;
27
+ reason: string;
28
+ }>;
29
+ //# sourceMappingURL=cdpStatus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cdpStatus.d.ts","sourceRoot":"","sources":["../../src/playwright/cdpStatus.ts"],"names":[],"mappings":"AAgBA,MAAM,MAAM,QAAQ,GAAG,aAAa,GAAG,cAAc,GAAG,QAAQ,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,QAAQ,CAAC;IAChB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAeD,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,eAAe,CAAC,CA4B1B;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAiC3E"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * "Is the widget running in the debug Chrome?" — answered by comparing the
3
+ * widget's page origin against the CDP tab list.
4
+ *
5
+ * Three states:
6
+ * - 'same-window' widget IS in the debug Chrome; agent can drive this tab.
7
+ * - 'wrong-window' debug Chrome is up, but on a different Chrome process.
8
+ * Widget should disable itself; service can bringToFront
9
+ * the corresponding tab in the debug Chrome so the user
10
+ * can switch windows.
11
+ * - 'no-cdp' no debug Chrome at all; the widget should let the user
12
+ * trigger a launch.
13
+ */
14
+ import { chromium } from 'playwright-core';
15
+ import { preflightCDP } from './preflight.js';
16
+ /**
17
+ * Parse a page URL down to its origin (protocol + host + port). We compare
18
+ * by origin, not full URL — the user might be on /login while the debug
19
+ * Chrome tab is on /, but they're the same SPA, same app, same target.
20
+ */
21
+ function originOf(rawUrl) {
22
+ try {
23
+ return new URL(rawUrl).origin;
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ export async function checkCdpStatus(cdpUrl, pageUrl) {
30
+ const wantOrigin = originOf(pageUrl);
31
+ if (!wantOrigin) {
32
+ // Treat unparseable page URLs as no-cdp so the UI nudges a relaunch.
33
+ return { state: 'no-cdp', reason: `unparseable page URL: ${pageUrl}` };
34
+ }
35
+ const cdp = await preflightCDP(cdpUrl);
36
+ if (!cdp.ok) {
37
+ return { state: 'no-cdp', reason: cdp.reason };
38
+ }
39
+ const match = cdp.tabs.find(t => originOf(t.url) === wantOrigin);
40
+ if (match) {
41
+ return {
42
+ state: 'same-window',
43
+ tabCount: cdp.tabs.length,
44
+ browser: cdp.browser,
45
+ matchingTabUrl: match.url,
46
+ };
47
+ }
48
+ return {
49
+ state: 'wrong-window',
50
+ tabCount: cdp.tabs.length,
51
+ browser: cdp.browser,
52
+ };
53
+ }
54
+ /**
55
+ * Bring the debug-Chrome tab matching `pageUrl`'s origin to the front. If no
56
+ * matching tab exists, open a new tab on the origin. Returns the URL of the
57
+ * tab that was focused (or opened) for logging.
58
+ *
59
+ * Uses a short-lived playwright-core connection — opens it, does the work,
60
+ * closes it. We don't keep a long-lived browser handle.
61
+ */
62
+ export async function focusDebugTab(cdpUrl, pageUrl) {
63
+ const wantOrigin = originOf(pageUrl);
64
+ if (!wantOrigin) {
65
+ return { ok: false, reason: `unparseable page URL: ${pageUrl}` };
66
+ }
67
+ let browser;
68
+ try {
69
+ browser = await chromium.connectOverCDP(cdpUrl);
70
+ }
71
+ catch (err) {
72
+ const msg = err instanceof Error ? err.message : String(err);
73
+ return { ok: false, reason: `couldn't connect to CDP at ${cdpUrl}: ${msg}` };
74
+ }
75
+ try {
76
+ const pages = browser.contexts().flatMap(c => c.pages());
77
+ const match = pages.find(p => originOf(p.url()) === wantOrigin);
78
+ if (match) {
79
+ await match.bringToFront();
80
+ return { ok: true, focusedUrl: match.url() };
81
+ }
82
+ // No tab on the dev origin yet — open one so the widget appears.
83
+ const context = browser.contexts()[0] ?? (await browser.newContext());
84
+ const page = await context.newPage();
85
+ await page.goto(pageUrl, { waitUntil: 'domcontentloaded' });
86
+ await page.bringToFront();
87
+ return { ok: true, focusedUrl: page.url() };
88
+ }
89
+ catch (err) {
90
+ const msg = err instanceof Error ? err.message : String(err);
91
+ return { ok: false, reason: `bringToFront failed: ${msg}` };
92
+ }
93
+ finally {
94
+ await browser.close().catch(() => { });
95
+ }
96
+ }
@@ -0,0 +1,29 @@
1
+ export interface LaunchOptions {
2
+ /** CDP port to expose (default 9222). */
3
+ port?: number;
4
+ /** Isolated user-data-dir (default `<tmpdir>/hover-chrome`). */
5
+ userDataDir?: string;
6
+ /** Initial URL (default 'about:blank'). */
7
+ url?: string;
8
+ /** How long to wait for /json/version to respond (default 9000ms). */
9
+ readyTimeoutMs?: number;
10
+ /** Poll interval while waiting (default 300ms). */
11
+ pollMs?: number;
12
+ }
13
+ export type LaunchResult = {
14
+ ok: true;
15
+ alreadyRunning: boolean;
16
+ userDataDir: string;
17
+ port: number;
18
+ } | {
19
+ ok: false;
20
+ reason: string;
21
+ };
22
+ export declare function findChromeBinary(): string | null;
23
+ /**
24
+ * Start (or detect) a debug Chrome listening on the given CDP port. Detaches
25
+ * the child process so the calling script can exit cleanly while Chrome keeps
26
+ * running.
27
+ */
28
+ export declare function launchDebugChrome(opts?: LaunchOptions): Promise<LaunchResult>;
29
+ //# sourceMappingURL=launchChrome.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launchChrome.d.ts","sourceRoot":"","sources":["../../src/playwright/launchChrome.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,aAAa;IAC5B,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,cAAc,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACxE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAMlC,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CA0ChD;AA8BD;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAwDvF"}
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Cross-platform launcher for an isolated debug Chrome on a known CDP port.
3
+ *
4
+ * Idempotent — if the port already responds, returns immediately. Used by:
5
+ * - `pnpm smoke:chrome` (monorepo) via src/scripts/start-chrome.ts
6
+ * - `pnpm exec hover-chrome` (npm consumers) via vite-plugin-hover's bin
7
+ *
8
+ * The user-data-dir is isolated under tmpdir so we never touch the user's
9
+ * primary Chrome profile.
10
+ */
11
+ import { spawn } from 'node:child_process';
12
+ import { existsSync, unlinkSync } from 'node:fs';
13
+ import { platform, tmpdir } from 'node:os';
14
+ import { join } from 'node:path';
15
+ const DEFAULT_PORT = 9222;
16
+ const DEFAULT_READY_TIMEOUT_MS = 9000;
17
+ const DEFAULT_POLL_MS = 300;
18
+ export function findChromeBinary() {
19
+ const candidates = [];
20
+ switch (platform()) {
21
+ case 'darwin':
22
+ candidates.push('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome');
23
+ if (process.env.HOME) {
24
+ candidates.push(join(process.env.HOME, 'Applications/Google Chrome.app/Contents/MacOS/Google Chrome'));
25
+ }
26
+ candidates.push('/Applications/Chromium.app/Contents/MacOS/Chromium');
27
+ break;
28
+ case 'win32': {
29
+ const programFiles = process.env['ProgramFiles'] ?? 'C:\\Program Files';
30
+ const programFilesX86 = process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)';
31
+ const localAppData = process.env['LOCALAPPDATA'];
32
+ candidates.push(join(programFiles, 'Google', 'Chrome', 'Application', 'chrome.exe'));
33
+ candidates.push(join(programFilesX86, 'Google', 'Chrome', 'Application', 'chrome.exe'));
34
+ if (localAppData) {
35
+ candidates.push(join(localAppData, 'Google', 'Chrome', 'Application', 'chrome.exe'));
36
+ }
37
+ // Edge fallback (Chromium-based, supports --remote-debugging-port too)
38
+ candidates.push(join(programFiles, 'Microsoft', 'Edge', 'Application', 'msedge.exe'));
39
+ candidates.push(join(programFilesX86, 'Microsoft', 'Edge', 'Application', 'msedge.exe'));
40
+ break;
41
+ }
42
+ default:
43
+ candidates.push('/usr/bin/google-chrome');
44
+ candidates.push('/usr/bin/google-chrome-stable');
45
+ candidates.push('/usr/bin/chromium');
46
+ candidates.push('/usr/bin/chromium-browser');
47
+ candidates.push('/snap/bin/chromium');
48
+ break;
49
+ }
50
+ for (const path of candidates) {
51
+ if (existsSync(path))
52
+ return path;
53
+ }
54
+ return null;
55
+ }
56
+ async function isCdpAlive(port) {
57
+ try {
58
+ const res = await fetch(`http://localhost:${port}/json/version`, {
59
+ signal: AbortSignal.timeout(1500),
60
+ });
61
+ return res.ok;
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
67
+ /**
68
+ * If a previous Chrome instance crashed it can leave SingletonLock files in the
69
+ * user-data-dir. Stale locks prevent the next launch from binding cleanly.
70
+ */
71
+ function clearStaleProfileLock(dir) {
72
+ for (const file of ['SingletonLock', 'SingletonCookie', 'SingletonSocket']) {
73
+ const p = join(dir, file);
74
+ if (existsSync(p)) {
75
+ try {
76
+ unlinkSync(p);
77
+ }
78
+ catch {
79
+ /* ignore */
80
+ }
81
+ }
82
+ }
83
+ }
84
+ /**
85
+ * Start (or detect) a debug Chrome listening on the given CDP port. Detaches
86
+ * the child process so the calling script can exit cleanly while Chrome keeps
87
+ * running.
88
+ */
89
+ export async function launchDebugChrome(opts = {}) {
90
+ const port = opts.port ?? DEFAULT_PORT;
91
+ const userDataDir = opts.userDataDir ?? join(tmpdir(), 'hover-chrome');
92
+ const url = opts.url ?? 'about:blank';
93
+ const readyTimeoutMs = opts.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
94
+ const pollMs = opts.pollMs ?? DEFAULT_POLL_MS;
95
+ if (await isCdpAlive(port)) {
96
+ return { ok: true, alreadyRunning: true, userDataDir, port };
97
+ }
98
+ const chrome = findChromeBinary();
99
+ if (!chrome) {
100
+ return {
101
+ ok: false,
102
+ reason: `Chrome not found in any standard location for ${platform()}`,
103
+ };
104
+ }
105
+ clearStaleProfileLock(userDataDir);
106
+ const args = [
107
+ `--remote-debugging-port=${port}`,
108
+ `--user-data-dir=${userDataDir}`,
109
+ '--no-first-run',
110
+ '--no-default-browser-check',
111
+ url,
112
+ ];
113
+ const child = spawn(chrome, args, {
114
+ detached: true,
115
+ stdio: 'ignore',
116
+ windowsHide: true,
117
+ });
118
+ const spawnError = { err: null };
119
+ child.on('error', err => {
120
+ spawnError.err = err;
121
+ });
122
+ child.unref();
123
+ const deadline = Date.now() + readyTimeoutMs;
124
+ while (Date.now() < deadline) {
125
+ await new Promise(res => setTimeout(res, pollMs));
126
+ if (spawnError.err) {
127
+ return { ok: false, reason: `failed to spawn Chrome: ${spawnError.err.message}` };
128
+ }
129
+ if (await isCdpAlive(port)) {
130
+ return { ok: true, alreadyRunning: false, userDataDir, port };
131
+ }
132
+ }
133
+ return {
134
+ ok: false,
135
+ reason: `Chrome started but /json/version did not respond within ${readyTimeoutMs}ms`,
136
+ };
137
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Connect to the user's Chrome via CDP and return the URLs of all open tabs.
3
+ * Closes the connection before returning (does not hold a Playwright session).
4
+ */
5
+ export declare function connectAndListTabs(cdpUrl: string): Promise<string[]>;
6
+ export interface CdpTabInfo {
7
+ url: string;
8
+ title?: string;
9
+ type?: string;
10
+ }
11
+ export type CdpPreflightResult = {
12
+ ok: true;
13
+ browser: string;
14
+ tabs: CdpTabInfo[];
15
+ } | {
16
+ ok: false;
17
+ reason: string;
18
+ };
19
+ /**
20
+ * Lightweight CDP health check via the /json endpoints.
21
+ *
22
+ * Critical: this MUST run before invoking the agent. If CDP isn't responsive,
23
+ * the Playwright MCP server falls back to launching its OWN Chromium — and
24
+ * Hover's premise is to drive the user's existing Chrome (with their dev
25
+ * state, cookies, devtools open), never spawn a fresh one.
26
+ *
27
+ * Pure HTTP — no playwright-core handshake, no setDownloadBehavior nonsense
28
+ * that can get stuck on busy CDP sessions.
29
+ */
30
+ export declare function preflightCDP(cdpUrl: string, timeoutMs?: number): Promise<CdpPreflightResult>;
31
+ //# sourceMappingURL=preflight.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preflight.d.ts","sourceRoot":"","sources":["../../src/playwright/preflight.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAQ1E;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,kBAAkB,GAC1B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,UAAU,EAAE,CAAA;CAAE,GACjD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAElC;;;;;;;;;;GAUG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,SAAS,SAAO,GACf,OAAO,CAAC,kBAAkB,CAAC,CA4C7B"}
@@ -0,0 +1,71 @@
1
+ import { chromium } from 'playwright-core';
2
+ /**
3
+ * Connect to the user's Chrome via CDP and return the URLs of all open tabs.
4
+ * Closes the connection before returning (does not hold a Playwright session).
5
+ */
6
+ export async function connectAndListTabs(cdpUrl) {
7
+ const browser = await chromium.connectOverCDP(cdpUrl);
8
+ try {
9
+ const pages = browser.contexts().flatMap(c => c.pages());
10
+ return pages.map(p => p.url());
11
+ }
12
+ finally {
13
+ await browser.close();
14
+ }
15
+ }
16
+ /**
17
+ * Lightweight CDP health check via the /json endpoints.
18
+ *
19
+ * Critical: this MUST run before invoking the agent. If CDP isn't responsive,
20
+ * the Playwright MCP server falls back to launching its OWN Chromium — and
21
+ * Hover's premise is to drive the user's existing Chrome (with their dev
22
+ * state, cookies, devtools open), never spawn a fresh one.
23
+ *
24
+ * Pure HTTP — no playwright-core handshake, no setDownloadBehavior nonsense
25
+ * that can get stuck on busy CDP sessions.
26
+ */
27
+ export async function preflightCDP(cdpUrl, timeoutMs = 2000) {
28
+ let versionRes;
29
+ try {
30
+ versionRes = await fetch(`${cdpUrl}/json/version`, {
31
+ signal: AbortSignal.timeout(timeoutMs),
32
+ });
33
+ }
34
+ catch (err) {
35
+ return {
36
+ ok: false,
37
+ reason: `Chrome debug session not detected at ${cdpUrl}. Start it with: pnpm exec hover-chrome (or: npx hover-chrome)`,
38
+ };
39
+ }
40
+ if (!versionRes.ok) {
41
+ return { ok: false, reason: `CDP returned HTTP ${versionRes.status}` };
42
+ }
43
+ let versionJson;
44
+ try {
45
+ versionJson = (await versionRes.json());
46
+ }
47
+ catch {
48
+ return { ok: false, reason: 'CDP /json/version returned non-JSON' };
49
+ }
50
+ let tabs = [];
51
+ try {
52
+ const listRes = await fetch(`${cdpUrl}/json/list`, {
53
+ signal: AbortSignal.timeout(timeoutMs),
54
+ });
55
+ if (listRes.ok) {
56
+ const raw = (await listRes.json());
57
+ tabs = raw
58
+ .filter(t => t.type === 'page' || !t.type)
59
+ .map(t => ({ url: t.url ?? '', title: t.title, type: t.type }))
60
+ .filter(t => t.url.length > 0);
61
+ }
62
+ }
63
+ catch {
64
+ // tab list is best-effort
65
+ }
66
+ return {
67
+ ok: true,
68
+ browser: versionJson.Browser ?? 'unknown',
69
+ tabs,
70
+ };
71
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=start-chrome.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"start-chrome.d.ts","sourceRoot":"","sources":["../../src/scripts/start-chrome.ts"],"names":[],"mappings":""}
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * "Start debug Chrome on port 9222" CLI.
4
+ *
5
+ * Two entry points:
6
+ * - Repo dev: `pnpm smoke:chrome` → tsx src/scripts/start-chrome.ts
7
+ * - npm consumer: `pnpm exec hover-chrome` → dist/scripts/start-chrome.js
8
+ * (or `npx hover-chrome`, bin exposed by vite-plugin-hover)
9
+ *
10
+ * All actual launch logic lives in ../playwright/launchChrome.ts.
11
+ */
12
+ import { launchDebugChrome } from '../playwright/launchChrome.js';
13
+ const result = await launchDebugChrome();
14
+ if (!result.ok) {
15
+ console.error(`[hover:chrome] ${result.reason}`);
16
+ process.exit(1);
17
+ }
18
+ if (result.alreadyRunning) {
19
+ console.log(`[hover:chrome] already listening on ${result.port}`);
20
+ }
21
+ else {
22
+ console.log(`[hover:chrome] ready on ${result.port} (data-dir=${result.userDataDir})`);
23
+ }
@@ -0,0 +1,22 @@
1
+ export interface ServiceOptions {
2
+ port: number;
3
+ agentId?: string;
4
+ model?: string;
5
+ maxBudgetUsd?: number;
6
+ mcpConfig?: string;
7
+ /** CDP URL to preflight before each command (default http://localhost:9222). */
8
+ cdpUrl?: string;
9
+ /** Working directory for the spawned agent. Also where skills are saved
10
+ * ('<devRoot>/.claude/skills/<slug>/SKILL.md'). Defaults to process.cwd().
11
+ * In Vite plugin context, set to `server.config.root` so Claude
12
+ * auto-discovers skills the user previously saved from this project. */
13
+ devRoot?: string;
14
+ }
15
+ export interface ServiceHandle {
16
+ /** The port the WebSocketServer actually bound to. May differ from
17
+ * the requested port if it was taken (we auto-bump up to 10 times). */
18
+ port: number;
19
+ close(): Promise<void>;
20
+ }
21
+ export declare function startService(opts: ServiceOptions): Promise<ServiceHandle>;
22
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAoDA,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;6EAGyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B;4EACwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAkED,wBAAsB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAuM/E"}