@hover-dev/core 0.17.0 → 0.19.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.
- package/dist/engine.d.ts +16 -39
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +18 -67
- package/dist/specs/pageObjectManifest.d.ts.map +1 -1
- package/dist/specs/pageObjectManifest.js +11 -10
- package/dist/specs/replayGrounded.d.ts.map +1 -1
- package/dist/specs/writeApiSpec.d.ts +36 -0
- package/dist/specs/writeApiSpec.d.ts.map +1 -0
- package/dist/specs/writeApiSpec.js +94 -0
- package/package.json +5 -22
- package/dist/agents/argv.d.ts +0 -11
- package/dist/agents/argv.d.ts.map +0 -1
- package/dist/agents/argv.js +0 -23
- package/dist/agents/claude.d.ts +0 -3
- package/dist/agents/claude.d.ts.map +0 -1
- package/dist/agents/claude.js +0 -220
- package/dist/agents/codex.d.ts +0 -19
- package/dist/agents/codex.d.ts.map +0 -1
- package/dist/agents/codex.js +0 -231
- package/dist/agents/detect.d.ts +0 -46
- package/dist/agents/detect.d.ts.map +0 -1
- package/dist/agents/detect.js +0 -80
- package/dist/agents/gemini.d.ts +0 -17
- package/dist/agents/gemini.d.ts.map +0 -1
- package/dist/agents/gemini.js +0 -186
- package/dist/agents/index.d.ts +0 -6
- package/dist/agents/index.d.ts.map +0 -1
- package/dist/agents/index.js +0 -5
- package/dist/agents/invoke.d.ts +0 -12
- package/dist/agents/invoke.d.ts.map +0 -1
- package/dist/agents/invoke.js +0 -93
- package/dist/agents/qwen.d.ts +0 -17
- package/dist/agents/qwen.d.ts.map +0 -1
- package/dist/agents/qwen.js +0 -172
- package/dist/agents/registry.d.ts +0 -19
- package/dist/agents/registry.d.ts.map +0 -1
- package/dist/agents/registry.js +0 -30
- package/dist/agents/shared.d.ts +0 -28
- package/dist/agents/shared.d.ts.map +0 -1
- package/dist/agents/shared.js +0 -35
- package/dist/agents/types.d.ts +0 -194
- package/dist/agents/types.d.ts.map +0 -1
- package/dist/agents/types.js +0 -23
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -2
- package/dist/mcp/actuateServer.d.ts +0 -3
- package/dist/mcp/actuateServer.d.ts.map +0 -1
- package/dist/mcp/actuateServer.js +0 -594
- package/dist/mcp/sourceFence.d.ts +0 -23
- package/dist/mcp/sourceFence.d.ts.map +0 -1
- package/dist/mcp/sourceFence.js +0 -79
- package/dist/mcp/sourceServer.d.ts +0 -3
- package/dist/mcp/sourceServer.d.ts.map +0 -1
- package/dist/mcp/sourceServer.js +0 -191
- package/dist/modes.d.ts +0 -39
- package/dist/modes.d.ts.map +0 -1
- package/dist/modes.js +0 -34
- package/dist/playwright/cdpStatus.d.ts +0 -14
- package/dist/playwright/cdpStatus.d.ts.map +0 -1
- package/dist/playwright/cdpStatus.js +0 -52
- package/dist/playwright/preflight.d.ts +0 -31
- package/dist/playwright/preflight.d.ts.map +0 -1
- package/dist/playwright/preflight.js +0 -82
- package/dist/playwright/preflightCache.d.ts +0 -27
- package/dist/playwright/preflightCache.d.ts.map +0 -1
- package/dist/playwright/preflightCache.js +0 -21
- package/dist/playwright/resolveMcpConfig.d.ts +0 -61
- package/dist/playwright/resolveMcpConfig.d.ts.map +0 -1
- package/dist/playwright/resolveMcpConfig.js +0 -84
- package/dist/plugin-api.d.ts +0 -237
- package/dist/plugin-api.d.ts.map +0 -1
- package/dist/plugin-api.js +0 -52
- package/dist/qa/classify.d.ts +0 -38
- package/dist/qa/classify.d.ts.map +0 -1
- package/dist/qa/classify.js +0 -138
- package/dist/runSession.d.ts +0 -53
- package/dist/runSession.d.ts.map +0 -1
- package/dist/runSession.js +0 -96
- package/dist/service/cdpHandlers.d.ts +0 -24
- package/dist/service/cdpHandlers.d.ts.map +0 -1
- package/dist/service/cdpHandlers.js +0 -50
- package/dist/service/cdpHint.d.ts +0 -41
- package/dist/service/cdpHint.d.ts.map +0 -1
- package/dist/service/cdpHint.js +0 -158
- package/dist/service/conventions.d.ts +0 -8
- package/dist/service/conventions.d.ts.map +0 -1
- package/dist/service/conventions.js +0 -42
- package/dist/service/relayHandlers.d.ts +0 -28
- package/dist/service/relayHandlers.d.ts.map +0 -1
- package/dist/service/relayHandlers.js +0 -105
- package/dist/service/saveHandlers.d.ts +0 -50
- package/dist/service/saveHandlers.d.ts.map +0 -1
- package/dist/service/saveHandlers.js +0 -77
- package/dist/service/types.d.ts +0 -158
- package/dist/service/types.d.ts.map +0 -1
- package/dist/service/types.js +0 -26
- package/dist/service.d.ts +0 -54
- package/dist/service.d.ts.map +0 -1
- package/dist/service.js +0 -1772
- package/dist/specs/businessMap.d.ts +0 -29
- package/dist/specs/businessMap.d.ts.map +0 -1
- package/dist/specs/businessMap.js +0 -95
- package/dist/specs/extractPageObjects.d.ts +0 -18
- package/dist/specs/extractPageObjects.d.ts.map +0 -1
- package/dist/specs/extractPageObjects.js +0 -98
- package/dist/specs/optimizeSpecWithAgent.d.ts +0 -9
- package/dist/specs/optimizeSpecWithAgent.d.ts.map +0 -1
- package/dist/specs/optimizeSpecWithAgent.js +0 -39
package/dist/mcp/sourceFence.js
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The path fence for the opt-in `read_source` capability (codeContext).
|
|
3
|
-
*
|
|
4
|
-
* Giving the agent the ability to read source is the ONE place Hover relaxes
|
|
5
|
-
* its "the agent only touches the browser" rule, so the fence is the whole
|
|
6
|
-
* security story. It must guarantee, on every call, that a caller-supplied path:
|
|
7
|
-
* 1. resolves to a location INSIDE the project root (no `..` / absolute-path
|
|
8
|
-
* escape — symlink escape is caught by the server's realpath re-check), and
|
|
9
|
-
* 2. is not a credential / secret / VCS / dependency file.
|
|
10
|
-
* Pure + lexical so it's exhaustively unit-testable; the server layers a
|
|
11
|
-
* realpath check + a size/binary guard on top.
|
|
12
|
-
*/
|
|
13
|
-
import { resolve, relative, isAbsolute, sep } from 'node:path';
|
|
14
|
-
/** Files we refuse to read even inside the root — credentials, keys, VCS,
|
|
15
|
-
* dependency trees, build caches. Matched against the POSIX-style relative
|
|
16
|
-
* path (so `\` on Windows is normalised first). */
|
|
17
|
-
const SECRET_PATTERNS = [
|
|
18
|
-
/(^|\/)\.env(\.[^/]*)?$/i, // .env, .env.local, .env.production
|
|
19
|
-
/\.env$/i, // any *.env dotenv file (prod.env, local.env)
|
|
20
|
-
/(^|\/)\.envrc$/i, // direnv (holds exported env / secrets)
|
|
21
|
-
/\.tfvars(\.json)?$/i, // terraform variable files (often secrets)
|
|
22
|
-
/(^|\/)\.htpasswd$/i, // http basic-auth credentials
|
|
23
|
-
/(^|\/)\.git(\/|$)/, // the git dir
|
|
24
|
-
/(^|\/)node_modules(\/|$)/, // dependency tree
|
|
25
|
-
/(^|\/)\.(next|nuxt|svelte-kit|astro|turbo|cache|output|vercel)(\/|$)/, // build caches
|
|
26
|
-
/(^|\/)(dist|build|coverage)(\/|$)/, // build output
|
|
27
|
-
/\.(pem|key|p12|pfx|crt|cer|der|keystore|jks)$/i, // key / cert material
|
|
28
|
-
/(^|\/)id_(rsa|dsa|ecdsa|ed25519)(\.[^/]*)?$/i, // ssh keys
|
|
29
|
-
/(^|\/)\.(npmrc|netrc|pgpass)$/i, // token-bearing rc files
|
|
30
|
-
/(^|\/)\.(ssh|aws|gnupg|gcloud|kube|docker)(\/|$)/i, // credential dirs
|
|
31
|
-
/(^|\/)secrets?(\/|\.[^/]*$|$)/i, // a secrets dir, or a secret(s).<ext> file
|
|
32
|
-
/(^|\/)credentials?(\/|\.[^/]*$|$)/i, // a credentials dir, or credential(s).<ext>
|
|
33
|
-
/\.(secret|secrets)$/i,
|
|
34
|
-
];
|
|
35
|
-
/** A path containing a NUL or C0 control char is never a legitimate source file. */
|
|
36
|
-
function hasControlChar(s) {
|
|
37
|
-
for (let i = 0; i < s.length; i++) {
|
|
38
|
-
if (s.charCodeAt(i) < 0x20)
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Resolve `input` against `root`, refusing anything outside the root or matching
|
|
45
|
-
* a secret pattern. `input` is treated as relative to the root; an absolute
|
|
46
|
-
* input is resolved too but will fail the containment check unless it happens to
|
|
47
|
-
* live under the root (the agent should pass repo-relative paths).
|
|
48
|
-
*/
|
|
49
|
-
export function resolveSourcePath(root, input) {
|
|
50
|
-
if (typeof input !== 'string' || !input.trim()) {
|
|
51
|
-
return { ok: false, reason: 'path is required' };
|
|
52
|
-
}
|
|
53
|
-
if (hasControlChar(input)) {
|
|
54
|
-
return { ok: false, reason: 'path contains control characters' };
|
|
55
|
-
}
|
|
56
|
-
const rootAbs = resolve(root);
|
|
57
|
-
const abs = resolve(rootAbs, input);
|
|
58
|
-
const rel = relative(rootAbs, abs);
|
|
59
|
-
// Outside the root: relative() returns '' for the root itself, a '..'-prefixed
|
|
60
|
-
// path for an ancestor/sibling, or an absolute path when on a different drive.
|
|
61
|
-
if (rel === '' || rel === '..' || rel.startsWith('..' + sep) || rel.startsWith('../') || isAbsolute(rel)) {
|
|
62
|
-
return { ok: false, reason: 'path escapes the project root' };
|
|
63
|
-
}
|
|
64
|
-
const relPosix = rel.split(sep).join('/');
|
|
65
|
-
for (const pat of SECRET_PATTERNS) {
|
|
66
|
-
if (pat.test(relPosix)) {
|
|
67
|
-
return { ok: false, reason: `refused: "${relPosix}" matches an excluded (secret / build / VCS) pattern` };
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return { ok: true, abs, rel: relPosix };
|
|
71
|
-
}
|
|
72
|
-
/** True if a resolved-and-realpathed absolute path is still inside the root.
|
|
73
|
-
* The server calls this AFTER realpath to defeat symlink escape (a symlink
|
|
74
|
-
* whose lexical path passed resolveSourcePath but points outside the root). */
|
|
75
|
-
export function isWithinRoot(root, realAbs) {
|
|
76
|
-
const rootAbs = resolve(root);
|
|
77
|
-
const rel = relative(rootAbs, realAbs);
|
|
78
|
-
return rel !== '' && rel !== '..' && !rel.startsWith('..' + sep) && !rel.startsWith('../') && !isAbsolute(rel);
|
|
79
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sourceServer.d.ts","sourceRoot":"","sources":["../../src/mcp/sourceServer.ts"],"names":[],"mappings":""}
|
package/dist/mcp/sourceServer.js
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Hover source-reader MCP server — the runtime behind the opt-in `codeContext`
|
|
4
|
-
* switch. Spawned by the agent (Claude Code / Codex) as a stdio subprocess when
|
|
5
|
-
* codeContext is enabled, in addition to Playwright MCP. It gives the agent
|
|
6
|
-
* READ-ONLY, fenced access to the project's source so it can author smarter
|
|
7
|
-
* tests and do white-box security/pentest work (read the actual query / authz
|
|
8
|
-
* check, not just the rendered DOM).
|
|
9
|
-
*
|
|
10
|
-
* This is the ONE place Hover relaxes "the agent only touches the browser", so
|
|
11
|
-
* the safety is all in the fence (src/mcp/sourceFence.ts) + the guards here:
|
|
12
|
-
* - every path is resolved INSIDE the project root (no `..` / absolute escape)
|
|
13
|
-
* - a realpath re-check defeats symlink escape
|
|
14
|
-
* - secret / VCS / dependency / build files are refused (.env, keys, .git, …)
|
|
15
|
-
* - read-only: there is no write / exec / delete tool here
|
|
16
|
-
* - a size cap + a binary guard keep it to actual source
|
|
17
|
-
*
|
|
18
|
-
* The project root comes in via env:
|
|
19
|
-
* HOVER_PROJECT_ROOT absolute path to the dev project root (devRoot)
|
|
20
|
-
*
|
|
21
|
-
* Tools exposed:
|
|
22
|
-
* read_source({ path }) → the file's text (fenced, ≤256 KB, text-only)
|
|
23
|
-
* list_source({ subdir? }) → a shallow dir listing (secrets filtered out)
|
|
24
|
-
*/
|
|
25
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
26
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
27
|
-
import { z } from 'zod';
|
|
28
|
-
import { readFileSync, realpathSync, statSync, readdirSync } from 'node:fs';
|
|
29
|
-
import { WebSocket } from 'ws';
|
|
30
|
-
import { resolveSourcePath, isWithinRoot } from './sourceFence.js';
|
|
31
|
-
const root = process.env.HOVER_PROJECT_ROOT;
|
|
32
|
-
if (!root) {
|
|
33
|
-
process.stderr.write('[hover-source-mcp] HOVER_PROJECT_ROOT must be set by the host.\n');
|
|
34
|
-
process.exit(1);
|
|
35
|
-
}
|
|
36
|
-
const ROOT = root;
|
|
37
|
-
const MAX_BYTES = 256 * 1024;
|
|
38
|
-
function md(text) {
|
|
39
|
-
return { content: [{ type: 'text', text }] };
|
|
40
|
-
}
|
|
41
|
-
// ── Read-approval gate ──────────────────────────────────────────────────────
|
|
42
|
-
// When the host runs us with HOVER_SOURCE_GATE=ask, every read/list first asks
|
|
43
|
-
// the editor (over the Hover service WS at HOVER_APPROVAL_PORT) for the user's
|
|
44
|
-
// one-click approval. The reader is fenced + read-only, so the gate is consent
|
|
45
|
-
// UX, not a security boundary: if the editor can't be reached or doesn't answer
|
|
46
|
-
// within 30s we FAIL OPEN (allow) rather than stall the agent's run.
|
|
47
|
-
const GATE = process.env.HOVER_SOURCE_GATE;
|
|
48
|
-
const APPROVAL_PORT = process.env.HOVER_APPROVAL_PORT;
|
|
49
|
-
let approvalWs = null;
|
|
50
|
-
let approvalSeq = 0;
|
|
51
|
-
const pendingApprovals = new Map();
|
|
52
|
-
function ensureApprovalWs() {
|
|
53
|
-
if (GATE !== 'ask' || !APPROVAL_PORT)
|
|
54
|
-
return null;
|
|
55
|
-
if (approvalWs && (approvalWs.readyState === WebSocket.OPEN || approvalWs.readyState === WebSocket.CONNECTING))
|
|
56
|
-
return approvalWs;
|
|
57
|
-
try {
|
|
58
|
-
const sock = new WebSocket(`ws://127.0.0.1:${APPROVAL_PORT}`);
|
|
59
|
-
sock.on('message', (data) => {
|
|
60
|
-
try {
|
|
61
|
-
const m = JSON.parse(data.toString());
|
|
62
|
-
if (m?.type === 'source-approval-response' && m.payload?.approvalId) {
|
|
63
|
-
const settle = pendingApprovals.get(m.payload.approvalId);
|
|
64
|
-
if (settle)
|
|
65
|
-
settle(m.payload.allow === true);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
catch { /* ignore malformed */ }
|
|
69
|
-
});
|
|
70
|
-
// Channel lost → fail OPEN for every waiting read (the reader is fenced +
|
|
71
|
-
// read-only; the gate is consent UX, not a security boundary, so never hang
|
|
72
|
-
// a run on a dead channel). The user taking their time is NOT a loss — only
|
|
73
|
-
// a closed/errored socket settles here.
|
|
74
|
-
const drain = () => { for (const s of [...pendingApprovals.values()])
|
|
75
|
-
s(true); };
|
|
76
|
-
sock.on('error', () => { drain(); });
|
|
77
|
-
sock.on('close', () => { if (approvalWs === sock)
|
|
78
|
-
approvalWs = null; drain(); });
|
|
79
|
-
approvalWs = sock;
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
approvalWs = null;
|
|
83
|
-
}
|
|
84
|
-
return approvalWs;
|
|
85
|
-
}
|
|
86
|
-
async function approve(path, kind) {
|
|
87
|
-
if (GATE !== 'ask' || !APPROVAL_PORT)
|
|
88
|
-
return true; // not gated → allow
|
|
89
|
-
const sock = ensureApprovalWs();
|
|
90
|
-
if (!sock)
|
|
91
|
-
return true; // no channel → fail open
|
|
92
|
-
const id = `a${++approvalSeq}`;
|
|
93
|
-
return new Promise((resolve) => {
|
|
94
|
-
// NO timeout: the consent prompt waits for the user (they may not see it for
|
|
95
|
-
// a while — that must never auto-allow). It settles only on their answer, or
|
|
96
|
-
// when the channel drops (drain() above → fail open), or run cancel.
|
|
97
|
-
const settle = (allow) => {
|
|
98
|
-
if (!pendingApprovals.has(id))
|
|
99
|
-
return;
|
|
100
|
-
pendingApprovals.delete(id);
|
|
101
|
-
resolve(allow);
|
|
102
|
-
};
|
|
103
|
-
pendingApprovals.set(id, settle);
|
|
104
|
-
const req = () => sock.send(JSON.stringify({ type: 'source-approval-request', payload: { approvalId: id, sourcePath: path, sourceKind: kind } }));
|
|
105
|
-
if (sock.readyState === WebSocket.OPEN)
|
|
106
|
-
req();
|
|
107
|
-
else
|
|
108
|
-
sock.once('open', req);
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
const server = new McpServer({ name: 'hover-source', version: '0.0.0' });
|
|
112
|
-
server.registerTool('read_source', {
|
|
113
|
-
description: "Read a source file from THIS project (read-only). Pass a repo-relative path (e.g. the one in an element's data-hover-source, `src/app/login.tsx:42` → path `src/app/login.tsx`). Fenced to the project root: paths that escape it, or that name secrets / keys / .env / .git / node_modules / build output, are refused. Use this to write tests against the real selectors & routes, or — in security/pentest mode — to confirm a finding against the actual server code (the SQL query, the authz check). You cannot write, run, or delete anything.",
|
|
114
|
-
inputSchema: {
|
|
115
|
-
path: z.string().describe('Repo-relative path to a source file, e.g. "src/api/orders.ts".'),
|
|
116
|
-
},
|
|
117
|
-
}, async ({ path }) => {
|
|
118
|
-
if (!(await approve(path, 'read')))
|
|
119
|
-
return md(`✗ source read declined by the user — continue from what's visible on the page.`);
|
|
120
|
-
const f = resolveSourcePath(ROOT, path);
|
|
121
|
-
if (!f.ok)
|
|
122
|
-
return md(`✗ ${f.reason}`);
|
|
123
|
-
let real;
|
|
124
|
-
try {
|
|
125
|
-
real = realpathSync(f.abs);
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
return md(`✗ not found: ${f.rel}`);
|
|
129
|
-
}
|
|
130
|
-
if (!isWithinRoot(ROOT, real))
|
|
131
|
-
return md(`✗ refused: "${f.rel}" resolves (via a symlink) outside the project root`);
|
|
132
|
-
let st;
|
|
133
|
-
try {
|
|
134
|
-
st = statSync(real);
|
|
135
|
-
}
|
|
136
|
-
catch {
|
|
137
|
-
return md(`✗ not found: ${f.rel}`);
|
|
138
|
-
}
|
|
139
|
-
if (st.isDirectory())
|
|
140
|
-
return md(`✗ "${f.rel}" is a directory — use list_source`);
|
|
141
|
-
if (st.size > MAX_BYTES)
|
|
142
|
-
return md(`✗ "${f.rel}" is ${Math.round(st.size / 1024)} KB — too large to read (cap ${MAX_BYTES / 1024} KB)`);
|
|
143
|
-
let buf;
|
|
144
|
-
try {
|
|
145
|
-
buf = readFileSync(real);
|
|
146
|
-
}
|
|
147
|
-
catch (e) {
|
|
148
|
-
return md(`✗ could not read ${f.rel}: ${e instanceof Error ? e.message : String(e)}`);
|
|
149
|
-
}
|
|
150
|
-
// Binary guard — a NUL byte in the first 8 KB means it isn't source text.
|
|
151
|
-
if (buf.subarray(0, 8192).includes(0))
|
|
152
|
-
return md(`✗ "${f.rel}" looks binary — refused`);
|
|
153
|
-
return md(`\`\`\`\n// ${f.rel}\n${buf.toString('utf-8')}\n\`\`\``);
|
|
154
|
-
});
|
|
155
|
-
server.registerTool('list_source', {
|
|
156
|
-
description: 'List the entries of a directory in THIS project (shallow, read-only). Omit `subdir` for the project root. Secret / VCS / dependency / build entries are filtered out. Use it to discover what source exists before reading a file.',
|
|
157
|
-
inputSchema: {
|
|
158
|
-
subdir: z.string().optional().describe('Repo-relative directory, e.g. "src/api". Omit for the root.'),
|
|
159
|
-
},
|
|
160
|
-
}, async ({ subdir }) => {
|
|
161
|
-
if (!(await approve(subdir || '.', 'list')))
|
|
162
|
-
return md(`✗ source listing declined by the user.`);
|
|
163
|
-
let dirAbs = ROOT;
|
|
164
|
-
let base = '';
|
|
165
|
-
if (subdir && subdir.trim() && subdir.trim() !== '.') {
|
|
166
|
-
const d = resolveSourcePath(ROOT, subdir);
|
|
167
|
-
if (!d.ok)
|
|
168
|
-
return md(`✗ ${d.reason}`);
|
|
169
|
-
dirAbs = d.abs;
|
|
170
|
-
base = d.rel;
|
|
171
|
-
}
|
|
172
|
-
let entries;
|
|
173
|
-
try {
|
|
174
|
-
entries = readdirSync(dirAbs, { withFileTypes: true });
|
|
175
|
-
}
|
|
176
|
-
catch {
|
|
177
|
-
return md(`✗ not a readable directory: ${base || '.'}`);
|
|
178
|
-
}
|
|
179
|
-
const rows = [];
|
|
180
|
-
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
181
|
-
const rel = base ? `${base}/${e.name}` : e.name;
|
|
182
|
-
// Filter via the same fence so secrets/build/VCS never even show up.
|
|
183
|
-
if (!resolveSourcePath(ROOT, rel).ok)
|
|
184
|
-
continue;
|
|
185
|
-
rows.push(e.isDirectory() ? `${rel}/` : rel);
|
|
186
|
-
}
|
|
187
|
-
if (rows.length === 0)
|
|
188
|
-
return md(`(empty or fully filtered) ${base || '.'}`);
|
|
189
|
-
return md(`${base || '.'} —\n${rows.join('\n')}`);
|
|
190
|
-
});
|
|
191
|
-
await server.connect(new StdioServerTransport());
|
package/dist/modes.d.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Built-in (non-plugin) mode behavior. Hover's core modes — Flow (the default,
|
|
3
|
-
* modeId `null`) and the planned QA mode — are pure agent-behavior config: which
|
|
4
|
-
* actuation tools the agent uses and how a run crystallizes. (Plugin modes —
|
|
5
|
-
* api-test / pentest — bring runtime machinery instead: a MITM proxy, sidecars,
|
|
6
|
-
* Chrome proxy flags, lifecycle hooks; they resolve to PLUGIN_MODE_BEHAVIOR.)
|
|
7
|
-
*
|
|
8
|
-
* `resolveModeBehavior(modeId)` is the SINGLE place that answers "how does this
|
|
9
|
-
* mode drive the agent", replacing scattered `currentModeId === null` checks.
|
|
10
|
-
* Adding a built-in mode (e.g. QA) = one entry in BUILTIN_MODE_BEHAVIOR, not new
|
|
11
|
-
* conditionals threaded through the run-assembly path.
|
|
12
|
-
*/
|
|
13
|
-
export interface ModeBehavior {
|
|
14
|
-
/** Deny Playwright's loose interaction tools (browser_click / type / fill_form
|
|
15
|
-
* / select_option / file_upload) AND inject the grounded-actuation directive,
|
|
16
|
-
* so the agent actuates via the Hover control MCP and saved selectors are
|
|
17
|
-
* role+name (record == replay). Flow and QA want this — they crystallize
|
|
18
|
-
* browser steps; plugin modes don't — they explore to capture traffic and
|
|
19
|
-
* keep the Playwright tools. */
|
|
20
|
-
groundedActuation: boolean;
|
|
21
|
-
}
|
|
22
|
-
/** Resolve the agent-behavior config for a mode id (null = Flow). Built-in modes
|
|
23
|
-
* are table-driven; any other (plugin) id falls back to PLUGIN_MODE_BEHAVIOR. */
|
|
24
|
-
export declare function resolveModeBehavior(modeId: string | null): ModeBehavior;
|
|
25
|
-
/** Picker metadata for built-in non-Flow modes (Flow is the implicit null
|
|
26
|
-
* default, not listed). core's broadcastModes merges these into the mode
|
|
27
|
-
* catalogue alongside plugin-contributed modes, and switchMode/set-mode accept
|
|
28
|
-
* them without requiring a plugin. */
|
|
29
|
-
export interface BuiltinMode {
|
|
30
|
-
id: string;
|
|
31
|
-
label: string;
|
|
32
|
-
description: string;
|
|
33
|
-
accent: string;
|
|
34
|
-
}
|
|
35
|
-
export declare const BUILTIN_MODES: BuiltinMode[];
|
|
36
|
-
/** True for a built-in non-null mode id (currently just `qa`). Lets core accept
|
|
37
|
-
* it in set-mode / switchMode without a contributing plugin. */
|
|
38
|
-
export declare function isBuiltinMode(modeId: string): boolean;
|
|
39
|
-
//# sourceMappingURL=modes.d.ts.map
|
package/dist/modes.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"modes.d.ts","sourceRoot":"","sources":["../src/modes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;qCAKiC;IACjC,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAmBD;kFACkF;AAClF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,YAAY,CAGvE;AAED;;;uCAGuC;AACvC,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB;AACD,eAAO,MAAM,aAAa,EAAE,WAAW,EAOtC,CAAC;AAEF;iEACiE;AACjE,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAErD"}
|
package/dist/modes.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/** Flow — the default mode (modeId `null`): author a Playwright spec via grounded
|
|
2
|
-
* actuation. */
|
|
3
|
-
const FLOW_MODE_BEHAVIOR = { groundedActuation: true };
|
|
4
|
-
/** Built-in modes other than Flow, keyed by modeId.
|
|
5
|
-
* QA = comprehensive testing umbrella; its Functional capability reuses Flow's
|
|
6
|
-
* grounded actuation (record == replay for promoted candidate flows). (Its API
|
|
7
|
-
* capability — when enabled — delegates to the api-test plugin's MITM runtime;
|
|
8
|
-
* that orchestration is a later QA stage, not a ModeBehavior field.) */
|
|
9
|
-
const BUILTIN_MODE_BEHAVIOR = {
|
|
10
|
-
qa: { groundedActuation: true },
|
|
11
|
-
};
|
|
12
|
-
/** Plugin-contributed modes (api-test / pentest): full Playwright tool access —
|
|
13
|
-
* they explore to capture traffic, not to crystallize browser steps. */
|
|
14
|
-
const PLUGIN_MODE_BEHAVIOR = { groundedActuation: false };
|
|
15
|
-
/** Resolve the agent-behavior config for a mode id (null = Flow). Built-in modes
|
|
16
|
-
* are table-driven; any other (plugin) id falls back to PLUGIN_MODE_BEHAVIOR. */
|
|
17
|
-
export function resolveModeBehavior(modeId) {
|
|
18
|
-
if (modeId === null)
|
|
19
|
-
return FLOW_MODE_BEHAVIOR;
|
|
20
|
-
return BUILTIN_MODE_BEHAVIOR[modeId] ?? PLUGIN_MODE_BEHAVIOR;
|
|
21
|
-
}
|
|
22
|
-
export const BUILTIN_MODES = [
|
|
23
|
-
{
|
|
24
|
-
id: 'qa',
|
|
25
|
-
label: 'QA Testing',
|
|
26
|
-
description: 'explore the whole app → findings report + promotable specs',
|
|
27
|
-
accent: '#22c55e',
|
|
28
|
-
},
|
|
29
|
-
];
|
|
30
|
-
/** True for a built-in non-null mode id (currently just `qa`). Lets core accept
|
|
31
|
-
* it in set-mode / switchMode without a contributing plugin. */
|
|
32
|
-
export function isBuiltinMode(modeId) {
|
|
33
|
-
return modeId in BUILTIN_MODE_BEHAVIOR;
|
|
34
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
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
|
-
//# sourceMappingURL=cdpStatus.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cdpStatus.d.ts","sourceRoot":"","sources":["../../src/playwright/cdpStatus.ts"],"names":[],"mappings":"AAeA,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"}
|
|
@@ -1,52 +0,0 @@
|
|
|
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 { getPreflight } from './preflightCache.js';
|
|
15
|
-
/**
|
|
16
|
-
* Parse a page URL down to its origin (protocol + host + port). We compare
|
|
17
|
-
* by origin, not full URL — the user might be on /login while the debug
|
|
18
|
-
* Chrome tab is on /, but they're the same SPA, same app, same target.
|
|
19
|
-
*/
|
|
20
|
-
function originOf(rawUrl) {
|
|
21
|
-
try {
|
|
22
|
-
return new URL(rawUrl).origin;
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
export async function checkCdpStatus(cdpUrl, pageUrl) {
|
|
29
|
-
const wantOrigin = originOf(pageUrl);
|
|
30
|
-
if (!wantOrigin) {
|
|
31
|
-
// Treat unparseable page URLs as no-cdp so the UI nudges a relaunch.
|
|
32
|
-
return { state: 'no-cdp', reason: `unparseable page URL: ${pageUrl}` };
|
|
33
|
-
}
|
|
34
|
-
const cdp = await getPreflight(cdpUrl);
|
|
35
|
-
if (!cdp.ok) {
|
|
36
|
-
return { state: 'no-cdp', reason: cdp.reason };
|
|
37
|
-
}
|
|
38
|
-
const match = cdp.tabs.find(t => originOf(t.url) === wantOrigin);
|
|
39
|
-
if (match) {
|
|
40
|
-
return {
|
|
41
|
-
state: 'same-window',
|
|
42
|
-
tabCount: cdp.tabs.length,
|
|
43
|
-
browser: cdp.browser,
|
|
44
|
-
matchingTabUrl: match.url,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
return {
|
|
48
|
-
state: 'wrong-window',
|
|
49
|
-
tabCount: cdp.tabs.length,
|
|
50
|
-
browser: cdp.browser,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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,CAsD7B"}
|
|
@@ -1,82 +0,0 @@
|
|
|
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
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
36
|
-
return {
|
|
37
|
-
ok: false,
|
|
38
|
-
reason: `Chrome debug session not detected at ${cdpUrl} (${msg}). Click the ✨ launcher in the widget to start it, or run \`pnpm exec hover-chrome\` (npx hover-chrome).`,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
if (!versionRes.ok) {
|
|
42
|
-
// Drain the keep-alive socket — we won't read the body on the error path.
|
|
43
|
-
await versionRes.body?.cancel();
|
|
44
|
-
return { ok: false, reason: `CDP returned HTTP ${versionRes.status}` };
|
|
45
|
-
}
|
|
46
|
-
let versionJson;
|
|
47
|
-
try {
|
|
48
|
-
versionJson = (await versionRes.json());
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
return { ok: false, reason: 'CDP /json/version returned non-JSON' };
|
|
52
|
-
}
|
|
53
|
-
let tabs = [];
|
|
54
|
-
try {
|
|
55
|
-
const listRes = await fetch(`${cdpUrl}/json/list`, {
|
|
56
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
57
|
-
});
|
|
58
|
-
if (listRes.ok) {
|
|
59
|
-
const raw = (await listRes.json());
|
|
60
|
-
tabs = raw
|
|
61
|
-
.filter(t => t.type === 'page' || !t.type)
|
|
62
|
-
.map(t => ({ url: t.url ?? '', title: t.title, type: t.type }))
|
|
63
|
-
.filter(t => t.url.length > 0);
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
// /json/version was healthy but /json/list wasn't — surface it so the
|
|
67
|
-
// agent's system prompt isn't silently built from an empty tab list.
|
|
68
|
-
// Drain the keep-alive socket since we won't read the body here.
|
|
69
|
-
await listRes.body?.cancel();
|
|
70
|
-
console.warn(`[hover] CDP /json/list returned HTTP ${listRes.status}; agent tab hint will be empty`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
catch (err) {
|
|
74
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
75
|
-
console.warn(`[hover] CDP /json/list failed: ${msg}; agent tab hint will be empty`);
|
|
76
|
-
}
|
|
77
|
-
return {
|
|
78
|
-
ok: true,
|
|
79
|
-
browser: versionJson.Browser ?? 'unknown',
|
|
80
|
-
tabs,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { preflightCDP } from './preflight.js';
|
|
2
|
-
/**
|
|
3
|
-
* Per-cdpUrl preflight cache.
|
|
4
|
-
*
|
|
5
|
-
* Why this exists: Hover preflights the CDP endpoint (`/json/version` +
|
|
6
|
-
* `/json/list`) before every agent command AND on every widget connect's
|
|
7
|
-
* `check-cdp` ping. Each `/json/list` round-trip is ~30–80 ms on local
|
|
8
|
-
* Chrome; doing it twice for the same request — once from the command
|
|
9
|
-
* path, once from the widget's CDP banner — adds up, and the widget pings
|
|
10
|
-
* frequently because Vite HMR cycles the WebSocket connection.
|
|
11
|
-
*
|
|
12
|
-
* The cache is shared across both paths and keyed by `cdpUrl` so multiple
|
|
13
|
-
* Hover services (one per example app in this monorepo, each with its own
|
|
14
|
-
* CDP endpoint someday) don't share entries. 30 s TTL — Chrome's tab list
|
|
15
|
-
* doesn't drift faster than that during a dev session, and any failure
|
|
16
|
-
* (Chrome killed, --remote-debugging-port closed) invalidates via
|
|
17
|
-
* `invalidatePreflight()` on the next failed agent invocation.
|
|
18
|
-
*
|
|
19
|
-
* Successful preflights are cached; failures are not (so the user gets
|
|
20
|
-
* immediate feedback the next time they fix the underlying issue —
|
|
21
|
-
* starting Chrome, fixing the wrong port).
|
|
22
|
-
*/
|
|
23
|
-
type Result = Awaited<ReturnType<typeof preflightCDP>>;
|
|
24
|
-
export declare function getPreflight(cdpUrl: string): Promise<Result>;
|
|
25
|
-
export declare function invalidatePreflight(cdpUrl: string): void;
|
|
26
|
-
export {};
|
|
27
|
-
//# sourceMappingURL=preflightCache.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"preflightCache.d.ts","sourceRoot":"","sources":["../../src/playwright/preflightCache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,KAAK,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC;AAMvD,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAalE;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAExD"}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { preflightCDP } from './preflight.js';
|
|
2
|
-
const TTL_MS = 30_000;
|
|
3
|
-
const cache = new Map();
|
|
4
|
-
export async function getPreflight(cdpUrl) {
|
|
5
|
-
const now = Date.now();
|
|
6
|
-
const hit = cache.get(cdpUrl);
|
|
7
|
-
if (hit && hit.result.ok && now - hit.at < TTL_MS) {
|
|
8
|
-
return hit.result;
|
|
9
|
-
}
|
|
10
|
-
const result = await preflightCDP(cdpUrl);
|
|
11
|
-
if (result.ok) {
|
|
12
|
-
cache.set(cdpUrl, { result, at: now });
|
|
13
|
-
}
|
|
14
|
-
else {
|
|
15
|
-
cache.delete(cdpUrl);
|
|
16
|
-
}
|
|
17
|
-
return result;
|
|
18
|
-
}
|
|
19
|
-
export function invalidatePreflight(cdpUrl) {
|
|
20
|
-
cache.delete(cdpUrl);
|
|
21
|
-
}
|