@hover-dev/core 0.15.0 → 0.16.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/agents/aider.d.ts.map +1 -1
- package/dist/agents/aider.js +6 -14
- package/dist/agents/codex.d.ts.map +1 -1
- package/dist/agents/codex.js +9 -4
- package/dist/agents/cursor.d.ts.map +1 -1
- package/dist/agents/cursor.js +8 -17
- package/dist/agents/gemini.d.ts.map +1 -1
- package/dist/agents/gemini.js +3 -14
- package/dist/agents/qwen.d.ts.map +1 -1
- package/dist/agents/qwen.js +3 -14
- package/dist/agents/shared.d.ts +28 -0
- package/dist/agents/shared.d.ts.map +1 -0
- package/dist/agents/shared.js +35 -0
- package/dist/mcp/sourceFence.d.ts +23 -0
- package/dist/mcp/sourceFence.d.ts.map +1 -0
- package/dist/mcp/sourceFence.js +75 -0
- package/dist/mcp/sourceServer.d.ts +3 -0
- package/dist/mcp/sourceServer.d.ts.map +1 -0
- package/dist/mcp/sourceServer.js +116 -0
- package/dist/playwright/preflight.d.ts.map +1 -1
- package/dist/playwright/preflight.js +6 -1
- package/dist/playwright/raiseWindow.d.ts.map +1 -1
- package/dist/playwright/raiseWindow.js +22 -3
- package/dist/playwright/resolveMcpConfig.d.ts +6 -0
- package/dist/playwright/resolveMcpConfig.d.ts.map +1 -1
- package/dist/playwright/resolveMcpConfig.js +15 -2
- package/dist/plugin-api.d.ts +7 -0
- package/dist/plugin-api.d.ts.map +1 -1
- package/dist/runSession.d.ts.map +1 -1
- package/dist/runSession.js +5 -0
- package/dist/service/cdpHandlers.d.ts +3 -7
- package/dist/service/cdpHandlers.d.ts.map +1 -1
- package/dist/service/cdpHandlers.js +4 -16
- package/dist/service.d.ts +6 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +128 -49
- package/dist/specs/optimizeSpec.d.ts.map +1 -1
- package/dist/specs/optimizeSpec.js +28 -6
- package/dist/specs/softBatch.d.ts +14 -0
- package/dist/specs/softBatch.d.ts.map +1 -0
- package/dist/specs/softBatch.js +177 -0
- package/dist/specs/text.d.ts +17 -0
- package/dist/specs/text.d.ts.map +1 -0
- package/dist/specs/text.js +24 -0
- package/dist/specs/writeCaseCsv.d.ts.map +1 -1
- package/dist/specs/writeCaseCsv.js +2 -8
- package/dist/specs/writeSpec.d.ts.map +1 -1
- package/dist/specs/writeSpec.js +2 -9
- package/package.json +5 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aider.d.ts","sourceRoot":"","sources":["../../src/agents/aider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAA8B,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"aider.d.ts","sourceRoot":"","sources":["../../src/agents/aider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAA8B,WAAW,EAAE,MAAM,YAAY,CAAC;AA8G3F,eAAO,MAAM,UAAU,EAAE,eA0GxB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,SAAS;sBACJ,WAAW;2BACJ,WAAW;sBAChB,WAAW;;;;;;CAS9B,CAAC"}
|
package/dist/agents/aider.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HOVER_PROMPT_PREFACE } from './shared.js';
|
|
1
2
|
function aiderState(state) {
|
|
2
3
|
if (typeof state.runningLines !== 'number') {
|
|
3
4
|
state.runningLines = 0;
|
|
@@ -13,18 +14,9 @@ function resetAiderCounters(s) {
|
|
|
13
14
|
s.sawErrorEvent = false;
|
|
14
15
|
s.runningSessionId = undefined;
|
|
15
16
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
* user-message text.
|
|
20
|
-
*/
|
|
21
|
-
const AIDER_PROMPT_PREFACE = [
|
|
22
|
-
'You are operating in Hover, a browser-testing tool.',
|
|
23
|
-
'Use ONLY the MCP playwright tools (prefixed `mcp__playwright__` / `mcp__hover-playwright__`) to drive the browser.',
|
|
24
|
-
'Do NOT use shell, file-edit, web-search, or any other built-in tool.',
|
|
25
|
-
'Do NOT navigate to a URL the user is already on; check the page state via `browser_snapshot` first.',
|
|
26
|
-
'When the task is complete, emit a short summary and stop.',
|
|
27
|
-
].join(' ');
|
|
17
|
+
// Aider has no system-prompt flag, so we prepend the standing HOVER-mode
|
|
18
|
+
// preface (HOVER_PROMPT_PREFACE, from shared.ts) to the user prompt (same
|
|
19
|
+
// approach as cursor.ts). The agent treats it as the leading user-message text.
|
|
28
20
|
/**
|
|
29
21
|
* Lines we treat as noise and drop instead of surfacing as text events.
|
|
30
22
|
* Aider chatters with status lines that would clutter the widget panel.
|
|
@@ -76,8 +68,8 @@ export const aiderAgent = {
|
|
|
76
68
|
// to the prompt. Aider has no --append-system-prompt flag, so this is
|
|
77
69
|
// the closest functional analogue (same trick as cursor.ts).
|
|
78
70
|
const preface = opts.appendSystemPrompt && opts.appendSystemPrompt.trim().length > 0
|
|
79
|
-
? `${
|
|
80
|
-
:
|
|
71
|
+
? `${HOVER_PROMPT_PREFACE} ${opts.appendSystemPrompt}`
|
|
72
|
+
: HOVER_PROMPT_PREFACE;
|
|
81
73
|
const finalPrompt = `${preface}\n\n${opts.prompt}`;
|
|
82
74
|
const args = ['--message', finalPrompt];
|
|
83
75
|
// Auto-confirm every prompt so the run doesn't hang.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/agents/codex.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAA8B,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/agents/codex.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAA8B,WAAW,EAAE,MAAM,YAAY,CAAC;AAoK3F,eAAO,MAAM,UAAU,EAAE,eA6JxB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,SAAS;sBACJ,WAAW;2BACJ,WAAW;sBAChB,WAAW;;;;;;;CAU9B,CAAC"}
|
package/dist/agents/codex.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stripMcpPrefix } from './shared.js';
|
|
1
2
|
/**
|
|
2
3
|
* Pricing per million tokens. Keep in lockstep with claude.ts's table —
|
|
3
4
|
* approximate published OpenAI rates as of 2026. We are deliberately
|
|
@@ -16,6 +17,9 @@ const PRICE_PER_M_USD = {
|
|
|
16
17
|
'gpt-4o': { in: 2.5, out: 10 },
|
|
17
18
|
'gpt-4': { in: 30, out: 60 },
|
|
18
19
|
};
|
|
20
|
+
// `modelHint` is currently always passed as undefined — the parser can't see
|
|
21
|
+
// the invocation's --model — so the default tier below is what gets used. The
|
|
22
|
+
// parameter is kept so a future caller that does have the model id can pass it.
|
|
19
23
|
function estimateCostUsd(modelHint, usage) {
|
|
20
24
|
const m = (modelHint ?? 'gpt-5.5').toLowerCase();
|
|
21
25
|
// Match by longest-prefix so 'gpt-5.5-mini' picks up the 'gpt-5.5' tier.
|
|
@@ -28,7 +32,6 @@ function codexState(state) {
|
|
|
28
32
|
if (typeof state.runningCost !== 'number') {
|
|
29
33
|
state.runningCost = 0;
|
|
30
34
|
state.runningTurns = 0;
|
|
31
|
-
state.runningModel = undefined;
|
|
32
35
|
state.runningSessionId = undefined;
|
|
33
36
|
state.lastAgentMessage = undefined;
|
|
34
37
|
state.sawErrorEvent = false;
|
|
@@ -39,7 +42,6 @@ function codexState(state) {
|
|
|
39
42
|
function resetCodexCounters(s) {
|
|
40
43
|
s.runningCost = 0;
|
|
41
44
|
s.runningTurns = 0;
|
|
42
|
-
s.runningModel = undefined;
|
|
43
45
|
s.runningSessionId = undefined;
|
|
44
46
|
s.lastAgentMessage = undefined;
|
|
45
47
|
s.sawErrorEvent = false;
|
|
@@ -128,7 +130,7 @@ export const codexAgent = {
|
|
|
128
130
|
// The exact field names aren't published. Read defensively: prefer
|
|
129
131
|
// `name`, fall back to `tool`. Same for input.
|
|
130
132
|
const rawName = it.name ?? it.tool ?? '';
|
|
131
|
-
const tool = rawName
|
|
133
|
+
const tool = stripMcpPrefix(rawName);
|
|
132
134
|
out.push({ kind: 'tool_use', tool, input: it.input ?? it.arguments, costUsdSnapshot: s.runningCost });
|
|
133
135
|
}
|
|
134
136
|
else if (it.type === 'command_execution') {
|
|
@@ -158,7 +160,10 @@ export const codexAgent = {
|
|
|
158
160
|
if (ev.type === 'turn.completed') {
|
|
159
161
|
s.runningTurns += 1;
|
|
160
162
|
if (ev.usage) {
|
|
161
|
-
|
|
163
|
+
// The parser has no access to the invocation's --model, so we let
|
|
164
|
+
// estimateCostUsd fall back to its fixed default tier. Cost is a
|
|
165
|
+
// high-water "should I hit Stop now" signal, not an invoice.
|
|
166
|
+
s.runningCost += estimateCostUsd(undefined, ev.usage);
|
|
162
167
|
}
|
|
163
168
|
out.push({ kind: 'usage', costUsd: s.runningCost, turns: s.runningTurns });
|
|
164
169
|
return out;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cursor.d.ts","sourceRoot":"","sources":["../../src/agents/cursor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAA8B,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"cursor.d.ts","sourceRoot":"","sources":["../../src/agents/cursor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAA8B,WAAW,EAAE,MAAM,YAAY,CAAC;AA2K3F,eAAO,MAAM,WAAW,EAAE,eAuJzB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,SAAS;sBACJ,WAAW;2BACJ,WAAW;sBAChB,WAAW;;;;;;;CAU9B,CAAC"}
|
package/dist/agents/cursor.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HOVER_PROMPT_PREFACE, stripMcpPrefix } from './shared.js';
|
|
1
2
|
function cursorState(state) {
|
|
2
3
|
if (typeof state.runningTurns !== 'number') {
|
|
3
4
|
state.runningTurns = 0;
|
|
@@ -40,9 +41,7 @@ function extractToolName(tc) {
|
|
|
40
41
|
null;
|
|
41
42
|
const kindFromKey = wrapperKey ? wrapperKey.replace(/ToolCall$/, '') : 'unknown';
|
|
42
43
|
const rawName = innerName || kindFromKey;
|
|
43
|
-
const tool = rawName
|
|
44
|
-
.replace(/^mcp__playwright__/, '')
|
|
45
|
-
.replace(/^mcp__hover-playwright__/, '');
|
|
44
|
+
const tool = stripMcpPrefix(rawName);
|
|
46
45
|
const input = (inner && typeof inner === 'object' && 'input' in inner && inner.input) ||
|
|
47
46
|
(inner && typeof inner === 'object' && 'arguments' in inner && inner.arguments) ||
|
|
48
47
|
(inner && typeof inner === 'object' && 'args' in inner && inner.args) ||
|
|
@@ -62,18 +61,10 @@ function detectToolError(tc) {
|
|
|
62
61
|
return true;
|
|
63
62
|
return false;
|
|
64
63
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
*/
|
|
70
|
-
const CURSOR_PROMPT_PREFACE = [
|
|
71
|
-
'You are operating in Hover, a browser-testing tool.',
|
|
72
|
-
'Use ONLY the MCP playwright tools (prefixed `mcp__playwright__` / `mcp__hover-playwright__`) to drive the browser.',
|
|
73
|
-
'Do NOT use shell, file-edit, web-search, or any other built-in tool.',
|
|
74
|
-
'Do NOT navigate to a URL the user is already on; check the page state via `browser_snapshot` first.',
|
|
75
|
-
'When the task is complete, emit a short summary and stop.',
|
|
76
|
-
].join(' ');
|
|
64
|
+
// The closest analogue Cursor has to claude's --append-system-prompt or
|
|
65
|
+
// codex's developer_instructions is prepending the standing HOVER-mode
|
|
66
|
+
// preface (HOVER_PROMPT_PREFACE, from shared.ts) to the user prompt so the
|
|
67
|
+
// agent sees it as the leading instruction. There is no CLI flag for it.
|
|
77
68
|
export const cursorAgent = {
|
|
78
69
|
id: 'cursor',
|
|
79
70
|
binName: 'cursor-agent',
|
|
@@ -92,8 +83,8 @@ export const cursorAgent = {
|
|
|
92
83
|
// Cursor has to claude's --append-system-prompt / codex's
|
|
93
84
|
// developer_instructions, because Cursor exposes no CLI flag for it.
|
|
94
85
|
const preface = opts.appendSystemPrompt && opts.appendSystemPrompt.trim().length > 0
|
|
95
|
-
? `${
|
|
96
|
-
:
|
|
86
|
+
? `${HOVER_PROMPT_PREFACE} ${opts.appendSystemPrompt}`
|
|
87
|
+
: HOVER_PROMPT_PREFACE;
|
|
97
88
|
const finalPrompt = `${preface}\n\n${opts.prompt}`;
|
|
98
89
|
const args = ['-p', finalPrompt];
|
|
99
90
|
// NDJSON streaming output.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gemini.d.ts","sourceRoot":"","sources":["../../src/agents/gemini.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAA8B,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"gemini.d.ts","sourceRoot":"","sources":["../../src/agents/gemini.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAA8B,WAAW,EAAE,MAAM,YAAY,CAAC;AA+J3F,eAAO,MAAM,WAAW,EAAE,eAiJzB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,SAAS;sBACJ,WAAW;2BACJ,WAAW;sBAChB,WAAW;;;;;;;CAU9B,CAAC"}
|
package/dist/agents/gemini.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HOVER_PROMPT_PREFACE, stripMcpPrefix } from './shared.js';
|
|
1
2
|
function geminiState(state) {
|
|
2
3
|
if (typeof state.runningTurns !== 'number') {
|
|
3
4
|
state.runningTurns = 0;
|
|
@@ -17,11 +18,6 @@ function resetGeminiCounters(s) {
|
|
|
17
18
|
s.sawErrorEvent = false;
|
|
18
19
|
s.toolNameByUseId.clear();
|
|
19
20
|
}
|
|
20
|
-
/** Strip the `mcp__playwright__` / `mcp__hover-playwright__` prefix so tool
|
|
21
|
-
* names match the normalised names claude / codex / cursor / qwen emit. */
|
|
22
|
-
function stripMcpPrefix(raw) {
|
|
23
|
-
return raw.replace(/^mcp__playwright__/, '').replace(/^mcp__hover-playwright__/, '');
|
|
24
|
-
}
|
|
25
21
|
/**
|
|
26
22
|
* Extract assistant text from a `message` event whose `content` may be a
|
|
27
23
|
* plain string OR an array of `{type:'text', text}` content blocks. Gemini's
|
|
@@ -41,13 +37,6 @@ function extractMessageText(ev) {
|
|
|
41
37
|
}
|
|
42
38
|
return undefined;
|
|
43
39
|
}
|
|
44
|
-
const GEMINI_PROMPT_PREFACE = [
|
|
45
|
-
'You are operating in Hover, a browser-testing tool.',
|
|
46
|
-
'Use ONLY the MCP playwright tools (prefixed `mcp__playwright__` / `mcp__hover-playwright__`) to drive the browser.',
|
|
47
|
-
'Do NOT use shell, file-edit, web-search, or any other built-in tool.',
|
|
48
|
-
'Do NOT navigate to a URL the user is already on; check the page state via `browser_snapshot` first.',
|
|
49
|
-
'When the task is complete, emit a short summary and stop.',
|
|
50
|
-
].join(' ');
|
|
51
40
|
export const geminiAgent = {
|
|
52
41
|
id: 'gemini',
|
|
53
42
|
binName: 'gemini',
|
|
@@ -65,8 +54,8 @@ export const geminiAgent = {
|
|
|
65
54
|
// GEMINI_SYSTEM_MD env var which writes a file). Prepend the HOVER-mode
|
|
66
55
|
// preface to the prompt instead — same pattern as cursor.ts / aider.ts.
|
|
67
56
|
const preface = opts.appendSystemPrompt && opts.appendSystemPrompt.trim().length > 0
|
|
68
|
-
? `${
|
|
69
|
-
:
|
|
57
|
+
? `${HOVER_PROMPT_PREFACE} ${opts.appendSystemPrompt}`
|
|
58
|
+
: HOVER_PROMPT_PREFACE;
|
|
70
59
|
const finalPrompt = `${preface}\n\n${opts.prompt}`;
|
|
71
60
|
const args = ['-p', finalPrompt];
|
|
72
61
|
// NDJSON streaming output.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qwen.d.ts","sourceRoot":"","sources":["../../src/agents/qwen.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAA8B,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"qwen.d.ts","sourceRoot":"","sources":["../../src/agents/qwen.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAA8B,WAAW,EAAE,MAAM,YAAY,CAAC;AAoJ3F,eAAO,MAAM,SAAS,EAAE,eAqJvB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,SAAS;sBACJ,WAAW;2BACJ,WAAW;sBAChB,WAAW;;;;;;;CAU9B,CAAC"}
|
package/dist/agents/qwen.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HOVER_PROMPT_PREFACE, stripMcpPrefix } from './shared.js';
|
|
1
2
|
function qwenState(state) {
|
|
2
3
|
if (typeof state.runningTurns !== 'number') {
|
|
3
4
|
state.runningTurns = 0;
|
|
@@ -17,18 +18,6 @@ function resetQwenCounters(s) {
|
|
|
17
18
|
s.sawErrorEvent = false;
|
|
18
19
|
s.toolNameByUseId.clear();
|
|
19
20
|
}
|
|
20
|
-
/** Strip the `mcp__playwright__` / `mcp__hover-playwright__` prefix so tool
|
|
21
|
-
* names match the normalised names claude / codex / cursor emit. */
|
|
22
|
-
function stripMcpPrefix(raw) {
|
|
23
|
-
return raw.replace(/^mcp__playwright__/, '').replace(/^mcp__hover-playwright__/, '');
|
|
24
|
-
}
|
|
25
|
-
const QWEN_PROMPT_PREFACE = [
|
|
26
|
-
'You are operating in Hover, a browser-testing tool.',
|
|
27
|
-
'Use ONLY the MCP playwright tools (prefixed `mcp__playwright__` / `mcp__hover-playwright__`) to drive the browser.',
|
|
28
|
-
'Do NOT use shell, file-edit, web-search, or any other built-in tool.',
|
|
29
|
-
'Do NOT navigate to a URL the user is already on; check the page state via `browser_snapshot` first.',
|
|
30
|
-
'When the task is complete, emit a short summary and stop.',
|
|
31
|
-
].join(' ');
|
|
32
21
|
export const qwenAgent = {
|
|
33
22
|
id: 'qwen',
|
|
34
23
|
binName: 'qwen',
|
|
@@ -62,8 +51,8 @@ export const qwenAgent = {
|
|
|
62
51
|
// prepending to the user prompt. Concatenate the standing Hover-mode
|
|
63
52
|
// preface with whatever the caller appended.
|
|
64
53
|
const sysPrompt = opts.appendSystemPrompt && opts.appendSystemPrompt.trim().length > 0
|
|
65
|
-
? `${
|
|
66
|
-
:
|
|
54
|
+
? `${HOVER_PROMPT_PREFACE} ${opts.appendSystemPrompt}`
|
|
55
|
+
: HOVER_PROMPT_PREFACE;
|
|
67
56
|
args.push('--append-system-prompt', sysPrompt);
|
|
68
57
|
// MCP servers configured in ~/.qwen/settings.json — no per-invocation
|
|
69
58
|
// --mcp-config equivalent. Same constraint as cursor / codex.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-agent helpers shared by the soft-sandbox descriptors
|
|
3
|
+
* (codex / cursor / gemini / qwen / aider).
|
|
4
|
+
*
|
|
5
|
+
* These agents all need the same two things:
|
|
6
|
+
* 1. A standing "HOVER-mode" instruction preface that tells the agent to
|
|
7
|
+
* drive the browser via the Playwright MCP tools only and not to touch
|
|
8
|
+
* its built-in shell / file-edit tools. Each agent injects it through a
|
|
9
|
+
* different channel (cursor / gemini / aider prepend it to the prompt,
|
|
10
|
+
* qwen passes it via --append-system-prompt, codex via
|
|
11
|
+
* `-c developer_instructions=`), so this module owns only the *text*, not
|
|
12
|
+
* the injection.
|
|
13
|
+
* 2. Normalising the `mcp__playwright__` / `mcp__hover-playwright__` prefix
|
|
14
|
+
* off a raw tool name so the emitted tool names line up across agents.
|
|
15
|
+
*
|
|
16
|
+
* codex deliberately does NOT use HOVER_PROMPT_PREFACE — it keeps its own
|
|
17
|
+
* wording ("Do NOT call …", "emit a short agent_message summary …") that is
|
|
18
|
+
* tuned to codex's event vocabulary. See codex.ts.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* The standing HOVER-mode instruction shared by cursor / gemini / qwen / aider.
|
|
22
|
+
* codex carries a near-identical but intentionally different variant inline.
|
|
23
|
+
*/
|
|
24
|
+
export declare const HOVER_PROMPT_PREFACE: string;
|
|
25
|
+
/** Strip the `mcp__playwright__` / `mcp__hover-playwright__` prefix so tool
|
|
26
|
+
* names match the normalised names every agent emits. */
|
|
27
|
+
export declare function stripMcpPrefix(raw: string): string;
|
|
28
|
+
//# sourceMappingURL=shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/agents/shared.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB,QAMtB,CAAC;AAEZ;0DAC0D;AAC1D,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAElD"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-agent helpers shared by the soft-sandbox descriptors
|
|
3
|
+
* (codex / cursor / gemini / qwen / aider).
|
|
4
|
+
*
|
|
5
|
+
* These agents all need the same two things:
|
|
6
|
+
* 1. A standing "HOVER-mode" instruction preface that tells the agent to
|
|
7
|
+
* drive the browser via the Playwright MCP tools only and not to touch
|
|
8
|
+
* its built-in shell / file-edit tools. Each agent injects it through a
|
|
9
|
+
* different channel (cursor / gemini / aider prepend it to the prompt,
|
|
10
|
+
* qwen passes it via --append-system-prompt, codex via
|
|
11
|
+
* `-c developer_instructions=`), so this module owns only the *text*, not
|
|
12
|
+
* the injection.
|
|
13
|
+
* 2. Normalising the `mcp__playwright__` / `mcp__hover-playwright__` prefix
|
|
14
|
+
* off a raw tool name so the emitted tool names line up across agents.
|
|
15
|
+
*
|
|
16
|
+
* codex deliberately does NOT use HOVER_PROMPT_PREFACE — it keeps its own
|
|
17
|
+
* wording ("Do NOT call …", "emit a short agent_message summary …") that is
|
|
18
|
+
* tuned to codex's event vocabulary. See codex.ts.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* The standing HOVER-mode instruction shared by cursor / gemini / qwen / aider.
|
|
22
|
+
* codex carries a near-identical but intentionally different variant inline.
|
|
23
|
+
*/
|
|
24
|
+
export const HOVER_PROMPT_PREFACE = [
|
|
25
|
+
'You are operating in Hover, a browser-testing tool.',
|
|
26
|
+
'Use ONLY the MCP playwright tools (prefixed `mcp__playwright__` / `mcp__hover-playwright__`) to drive the browser.',
|
|
27
|
+
'Do NOT use shell, file-edit, web-search, or any other built-in tool.',
|
|
28
|
+
'Do NOT navigate to a URL the user is already on; check the page state via `browser_snapshot` first.',
|
|
29
|
+
'When the task is complete, emit a short summary and stop.',
|
|
30
|
+
].join(' ');
|
|
31
|
+
/** Strip the `mcp__playwright__` / `mcp__hover-playwright__` prefix so tool
|
|
32
|
+
* names match the normalised names every agent emits. */
|
|
33
|
+
export function stripMcpPrefix(raw) {
|
|
34
|
+
return raw.replace(/^mcp__playwright__/, '').replace(/^mcp__hover-playwright__/, '');
|
|
35
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface FenceOk {
|
|
2
|
+
ok: true;
|
|
3
|
+
/** Absolute, root-anchored path the server may stat/read (after realpath). */
|
|
4
|
+
abs: string;
|
|
5
|
+
/** POSIX-style path relative to the root — safe to echo back to the agent. */
|
|
6
|
+
rel: string;
|
|
7
|
+
}
|
|
8
|
+
export interface FenceErr {
|
|
9
|
+
ok: false;
|
|
10
|
+
reason: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Resolve `input` against `root`, refusing anything outside the root or matching
|
|
14
|
+
* a secret pattern. `input` is treated as relative to the root; an absolute
|
|
15
|
+
* input is resolved too but will fail the containment check unless it happens to
|
|
16
|
+
* live under the root (the agent should pass repo-relative paths).
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveSourcePath(root: string, input: string): FenceOk | FenceErr;
|
|
19
|
+
/** True if a resolved-and-realpathed absolute path is still inside the root.
|
|
20
|
+
* The server calls this AFTER realpath to defeat symlink escape (a symlink
|
|
21
|
+
* whose lexical path passed resolveSourcePath but points outside the root). */
|
|
22
|
+
export declare function isWithinRoot(root: string, realAbs: string): boolean;
|
|
23
|
+
//# sourceMappingURL=sourceFence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sourceFence.d.ts","sourceRoot":"","sources":["../../src/mcp/sourceFence.ts"],"names":[],"mappings":"AAgCA,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,IAAI,CAAC;IACT,8EAA8E;IAC9E,GAAG,EAAE,MAAM,CAAC;IACZ,8EAA8E;IAC9E,GAAG,EAAE,MAAM,CAAC;CACb;AACD,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,MAAM,CAAC;CAChB;AAUD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAsBjF;AAED;;gFAEgF;AAChF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAInE"}
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
/(^|\/)\.git(\/|$)/, // the git dir
|
|
20
|
+
/(^|\/)node_modules(\/|$)/, // dependency tree
|
|
21
|
+
/(^|\/)\.(next|nuxt|svelte-kit|astro|turbo|cache|output|vercel)(\/|$)/, // build caches
|
|
22
|
+
/(^|\/)(dist|build|coverage)(\/|$)/, // build output
|
|
23
|
+
/\.(pem|key|p12|pfx|crt|cer|der|keystore|jks)$/i, // key / cert material
|
|
24
|
+
/(^|\/)id_(rsa|dsa|ecdsa|ed25519)(\.[^/]*)?$/i, // ssh keys
|
|
25
|
+
/(^|\/)\.(npmrc|netrc|pgpass)$/i, // token-bearing rc files
|
|
26
|
+
/(^|\/)\.(ssh|aws|gnupg|gcloud|kube|docker)(\/|$)/i, // credential dirs
|
|
27
|
+
/(^|\/)secrets?(\/|\.[^/]*$|$)/i, // a secrets dir, or a secret(s).<ext> file
|
|
28
|
+
/(^|\/)credentials?(\/|\.[^/]*$|$)/i, // a credentials dir, or credential(s).<ext>
|
|
29
|
+
/\.(secret|secrets)$/i,
|
|
30
|
+
];
|
|
31
|
+
/** A path containing a NUL or C0 control char is never a legitimate source file. */
|
|
32
|
+
function hasControlChar(s) {
|
|
33
|
+
for (let i = 0; i < s.length; i++) {
|
|
34
|
+
if (s.charCodeAt(i) < 0x20)
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolve `input` against `root`, refusing anything outside the root or matching
|
|
41
|
+
* a secret pattern. `input` is treated as relative to the root; an absolute
|
|
42
|
+
* input is resolved too but will fail the containment check unless it happens to
|
|
43
|
+
* live under the root (the agent should pass repo-relative paths).
|
|
44
|
+
*/
|
|
45
|
+
export function resolveSourcePath(root, input) {
|
|
46
|
+
if (typeof input !== 'string' || !input.trim()) {
|
|
47
|
+
return { ok: false, reason: 'path is required' };
|
|
48
|
+
}
|
|
49
|
+
if (hasControlChar(input)) {
|
|
50
|
+
return { ok: false, reason: 'path contains control characters' };
|
|
51
|
+
}
|
|
52
|
+
const rootAbs = resolve(root);
|
|
53
|
+
const abs = resolve(rootAbs, input);
|
|
54
|
+
const rel = relative(rootAbs, abs);
|
|
55
|
+
// Outside the root: relative() returns '' for the root itself, a '..'-prefixed
|
|
56
|
+
// path for an ancestor/sibling, or an absolute path when on a different drive.
|
|
57
|
+
if (rel === '' || rel === '..' || rel.startsWith('..' + sep) || rel.startsWith('../') || isAbsolute(rel)) {
|
|
58
|
+
return { ok: false, reason: 'path escapes the project root' };
|
|
59
|
+
}
|
|
60
|
+
const relPosix = rel.split(sep).join('/');
|
|
61
|
+
for (const pat of SECRET_PATTERNS) {
|
|
62
|
+
if (pat.test(relPosix)) {
|
|
63
|
+
return { ok: false, reason: `refused: "${relPosix}" matches an excluded (secret / build / VCS) pattern` };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { ok: true, abs, rel: relPosix };
|
|
67
|
+
}
|
|
68
|
+
/** True if a resolved-and-realpathed absolute path is still inside the root.
|
|
69
|
+
* The server calls this AFTER realpath to defeat symlink escape (a symlink
|
|
70
|
+
* whose lexical path passed resolveSourcePath but points outside the root). */
|
|
71
|
+
export function isWithinRoot(root, realAbs) {
|
|
72
|
+
const rootAbs = resolve(root);
|
|
73
|
+
const rel = relative(rootAbs, realAbs);
|
|
74
|
+
return rel !== '' && rel !== '..' && !rel.startsWith('..' + sep) && !rel.startsWith('../') && !isAbsolute(rel);
|
|
75
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sourceServer.d.ts","sourceRoot":"","sources":["../../src/mcp/sourceServer.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,116 @@
|
|
|
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 { resolveSourcePath, isWithinRoot } from './sourceFence.js';
|
|
30
|
+
const root = process.env.HOVER_PROJECT_ROOT;
|
|
31
|
+
if (!root) {
|
|
32
|
+
process.stderr.write('[hover-source-mcp] HOVER_PROJECT_ROOT must be set by the host.\n');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const ROOT = root;
|
|
36
|
+
const MAX_BYTES = 256 * 1024;
|
|
37
|
+
function md(text) {
|
|
38
|
+
return { content: [{ type: 'text', text }] };
|
|
39
|
+
}
|
|
40
|
+
const server = new McpServer({ name: 'hover-source', version: '0.0.0' });
|
|
41
|
+
server.registerTool('read_source', {
|
|
42
|
+
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.",
|
|
43
|
+
inputSchema: {
|
|
44
|
+
path: z.string().describe('Repo-relative path to a source file, e.g. "src/api/orders.ts".'),
|
|
45
|
+
},
|
|
46
|
+
}, async ({ path }) => {
|
|
47
|
+
const f = resolveSourcePath(ROOT, path);
|
|
48
|
+
if (!f.ok)
|
|
49
|
+
return md(`✗ ${f.reason}`);
|
|
50
|
+
let real;
|
|
51
|
+
try {
|
|
52
|
+
real = realpathSync(f.abs);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return md(`✗ not found: ${f.rel}`);
|
|
56
|
+
}
|
|
57
|
+
if (!isWithinRoot(ROOT, real))
|
|
58
|
+
return md(`✗ refused: "${f.rel}" resolves (via a symlink) outside the project root`);
|
|
59
|
+
let st;
|
|
60
|
+
try {
|
|
61
|
+
st = statSync(real);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return md(`✗ not found: ${f.rel}`);
|
|
65
|
+
}
|
|
66
|
+
if (st.isDirectory())
|
|
67
|
+
return md(`✗ "${f.rel}" is a directory — use list_source`);
|
|
68
|
+
if (st.size > MAX_BYTES)
|
|
69
|
+
return md(`✗ "${f.rel}" is ${Math.round(st.size / 1024)} KB — too large to read (cap ${MAX_BYTES / 1024} KB)`);
|
|
70
|
+
let buf;
|
|
71
|
+
try {
|
|
72
|
+
buf = readFileSync(real);
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
return md(`✗ could not read ${f.rel}: ${e instanceof Error ? e.message : String(e)}`);
|
|
76
|
+
}
|
|
77
|
+
// Binary guard — a NUL byte in the first 8 KB means it isn't source text.
|
|
78
|
+
if (buf.subarray(0, 8192).includes(0))
|
|
79
|
+
return md(`✗ "${f.rel}" looks binary — refused`);
|
|
80
|
+
return md(`\`\`\`\n// ${f.rel}\n${buf.toString('utf-8')}\n\`\`\``);
|
|
81
|
+
});
|
|
82
|
+
server.registerTool('list_source', {
|
|
83
|
+
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.',
|
|
84
|
+
inputSchema: {
|
|
85
|
+
subdir: z.string().optional().describe('Repo-relative directory, e.g. "src/api". Omit for the root.'),
|
|
86
|
+
},
|
|
87
|
+
}, async ({ subdir }) => {
|
|
88
|
+
let dirAbs = ROOT;
|
|
89
|
+
let base = '';
|
|
90
|
+
if (subdir && subdir.trim() && subdir.trim() !== '.') {
|
|
91
|
+
const d = resolveSourcePath(ROOT, subdir);
|
|
92
|
+
if (!d.ok)
|
|
93
|
+
return md(`✗ ${d.reason}`);
|
|
94
|
+
dirAbs = d.abs;
|
|
95
|
+
base = d.rel;
|
|
96
|
+
}
|
|
97
|
+
let entries;
|
|
98
|
+
try {
|
|
99
|
+
entries = readdirSync(dirAbs, { withFileTypes: true });
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return md(`✗ not a readable directory: ${base || '.'}`);
|
|
103
|
+
}
|
|
104
|
+
const rows = [];
|
|
105
|
+
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
106
|
+
const rel = base ? `${base}/${e.name}` : e.name;
|
|
107
|
+
// Filter via the same fence so secrets/build/VCS never even show up.
|
|
108
|
+
if (!resolveSourcePath(ROOT, rel).ok)
|
|
109
|
+
continue;
|
|
110
|
+
rows.push(e.isDirectory() ? `${rel}/` : rel);
|
|
111
|
+
}
|
|
112
|
+
if (rows.length === 0)
|
|
113
|
+
return md(`(empty or fully filtered) ${base || '.'}`);
|
|
114
|
+
return md(`${base || '.'} —\n${rows.join('\n')}`);
|
|
115
|
+
});
|
|
116
|
+
await server.connect(new StdioServerTransport());
|
|
@@ -1 +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,
|
|
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"}
|
|
@@ -32,12 +32,15 @@ export async function preflightCDP(cdpUrl, timeoutMs = 2000) {
|
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
catch (err) {
|
|
35
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
35
36
|
return {
|
|
36
37
|
ok: false,
|
|
37
|
-
reason: `Chrome debug session not detected at ${cdpUrl}. Click the ✨ launcher in the widget to start it, or run \`pnpm exec hover-chrome\` (npx hover-chrome).`,
|
|
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).`,
|
|
38
39
|
};
|
|
39
40
|
}
|
|
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();
|
|
41
44
|
return { ok: false, reason: `CDP returned HTTP ${versionRes.status}` };
|
|
42
45
|
}
|
|
43
46
|
let versionJson;
|
|
@@ -62,6 +65,8 @@ export async function preflightCDP(cdpUrl, timeoutMs = 2000) {
|
|
|
62
65
|
else {
|
|
63
66
|
// /json/version was healthy but /json/list wasn't — surface it so the
|
|
64
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();
|
|
65
70
|
console.warn(`[hover] CDP /json/list returned HTTP ${listRes.status}; agent tab hint will be empty`);
|
|
66
71
|
}
|
|
67
72
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"raiseWindow.d.ts","sourceRoot":"","sources":["../../src/playwright/raiseWindow.ts"],"names":[],"mappings":"AAyBA;;;GAGG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA8BrE;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"raiseWindow.d.ts","sourceRoot":"","sources":["../../src/playwright/raiseWindow.ts"],"names":[],"mappings":"AAyBA;;;GAGG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA8BrE;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyDlE"}
|
|
@@ -77,9 +77,28 @@ export async function raiseChromeWindow(pid) {
|
|
|
77
77
|
if (os === 'linux') {
|
|
78
78
|
// wmctrl is the most common helper for X11; not always installed,
|
|
79
79
|
// but the alternative (xdotool) needs the same dependency story.
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
80
|
+
// If it isn't installed the runCapture/runDetached calls swallow the
|
|
81
|
+
// ENOENT and we degrade gracefully.
|
|
82
|
+
//
|
|
83
|
+
// `wmctrl -i` expects an X11 WINDOW id, NOT a unix PID, so we first
|
|
84
|
+
// map PID → window id. `wmctrl -l -p` lists windows with their owning
|
|
85
|
+
// PID in the third column:
|
|
86
|
+
// 0x03c00007 0 12345 hostname Title…
|
|
87
|
+
const listing = await runCapture('wmctrl', ['-l', '-p']);
|
|
88
|
+
if (!listing)
|
|
89
|
+
return;
|
|
90
|
+
let windowId = null;
|
|
91
|
+
for (const line of listing.split('\n')) {
|
|
92
|
+
// columns: <window-id> <desktop> <pid> <host> <title…>
|
|
93
|
+
const m = line.match(/^(0x[0-9a-fA-F]+)\s+\S+\s+(\d+)\s/);
|
|
94
|
+
if (m && Number(m[2]) === pid) {
|
|
95
|
+
windowId = m[1];
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!windowId)
|
|
100
|
+
return; // no matching window — no-op gracefully
|
|
101
|
+
await runDetached('wmctrl', ['-ia', windowId]);
|
|
83
102
|
return;
|
|
84
103
|
}
|
|
85
104
|
if (os === 'win32') {
|
|
@@ -26,6 +26,12 @@ export interface ExtraMcpServer {
|
|
|
26
26
|
args?: string[];
|
|
27
27
|
env?: Record<string, string>;
|
|
28
28
|
}
|
|
29
|
+
/** The `mcp__<id>` tool-name prefix Claude Code exposes a plugin MCP server's
|
|
30
|
+
* tools under: non-alphanumerics collapse to `_` and edges are trimmed (e.g.
|
|
31
|
+
* `@hover-dev/security:flows` → `mcp__hover_dev_security_flows`). Used to build
|
|
32
|
+
* the hard-sandbox allow-list. Single source so the service and the CLI scan
|
|
33
|
+
* command can't drift on how the prefix is derived. */
|
|
34
|
+
export declare function mcpToolPrefix(serverId: string): string;
|
|
29
35
|
export declare function resolveMcpConfig(opts: {
|
|
30
36
|
/** CDP URL passed to the MCP server's `--cdp-endpoint` flag. */
|
|
31
37
|
cdpUrl: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveMcpConfig.d.ts","sourceRoot":"","sources":["../../src/playwright/resolveMcpConfig.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,cAAc;IAC7B;2EACuE;IACvE,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IACrC,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAC;IACb;;;;2CAIuC;IACvC,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;IACzB;6EACyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;kFAG8E;IAC9E,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"resolveMcpConfig.d.ts","sourceRoot":"","sources":["../../src/playwright/resolveMcpConfig.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,cAAc;IAC7B;2EACuE;IACvE,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAED;;;;wDAIwD;AACxD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IACrC,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAC;IACb;;;;2CAIuC;IACvC,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;IACzB;6EACyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;kFAG8E;IAC9E,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,CAsDT"}
|