@hover-dev/core 0.14.1 → 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/README.md +73 -1
- package/dist/agents/aider.d.ts.map +1 -1
- package/dist/agents/aider.js +6 -14
- package/dist/agents/claude.d.ts.map +1 -1
- package/dist/agents/claude.js +14 -0
- package/dist/agents/codex.d.ts.map +1 -1
- package/dist/agents/codex.js +10 -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/invoke.d.ts.map +1 -1
- package/dist/agents/invoke.js +10 -1
- 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/agents/types.d.ts +11 -0
- package/dist/agents/types.d.ts.map +1 -1
- 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 +11 -0
- package/dist/playwright/resolveMcpConfig.d.ts.map +1 -1
- package/dist/playwright/resolveMcpConfig.js +17 -3
- package/dist/plugin-api.d.ts +7 -0
- package/dist/plugin-api.d.ts.map +1 -1
- package/dist/runSession.d.ts +42 -0
- package/dist/runSession.d.ts.map +1 -0
- package/dist/runSession.js +81 -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/cdpHint.d.ts.map +1 -1
- package/dist/service/cdpHint.js +30 -14
- package/dist/service/conventions.d.ts +8 -0
- package/dist/service/conventions.d.ts.map +1 -0
- package/dist/service/conventions.js +42 -0
- package/dist/service/saveHandlers.d.ts +10 -13
- package/dist/service/saveHandlers.d.ts.map +1 -1
- package/dist/service/saveHandlers.js +9 -25
- package/dist/service/types.d.ts +5 -0
- package/dist/service/types.d.ts.map +1 -1
- package/dist/service.d.ts +13 -4
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +264 -148
- package/dist/skills/writeSkill.d.ts +12 -35
- package/dist/skills/writeSkill.d.ts.map +1 -1
- package/dist/skills/writeSkill.js +10 -166
- package/dist/specs/detectSharedFlows.d.ts +35 -0
- package/dist/specs/detectSharedFlows.d.ts.map +1 -0
- package/dist/specs/detectSharedFlows.js +171 -0
- package/dist/specs/extractPageObjects.d.ts +18 -0
- package/dist/specs/extractPageObjects.d.ts.map +1 -0
- package/dist/specs/extractPageObjects.js +98 -0
- package/dist/specs/generatePageObject.d.ts +29 -0
- package/dist/specs/generatePageObject.d.ts.map +1 -0
- package/dist/specs/generatePageObject.js +149 -0
- package/dist/specs/listSpecs.d.ts +12 -0
- package/dist/specs/listSpecs.d.ts.map +1 -1
- package/dist/specs/listSpecs.js +27 -2
- package/dist/specs/optimizationSuggestion.d.ts +26 -0
- package/dist/specs/optimizationSuggestion.d.ts.map +1 -0
- package/dist/specs/optimizationSuggestion.js +28 -0
- package/dist/specs/optimizeSpec.d.ts +42 -0
- package/dist/specs/optimizeSpec.d.ts.map +1 -0
- package/dist/specs/optimizeSpec.js +188 -0
- package/dist/specs/optimizeSpecWithAgent.d.ts +11 -0
- package/dist/specs/optimizeSpecWithAgent.d.ts.map +1 -0
- package/dist/specs/optimizeSpecWithAgent.js +40 -0
- package/dist/specs/pageObjectManifest.d.ts +20 -0
- package/dist/specs/pageObjectManifest.d.ts.map +1 -0
- package/dist/specs/pageObjectManifest.js +40 -0
- package/dist/specs/seeds.d.ts +36 -0
- package/dist/specs/seeds.d.ts.map +1 -0
- package/dist/specs/seeds.js +74 -0
- package/dist/specs/sidecar.d.ts +25 -0
- package/dist/specs/sidecar.d.ts.map +1 -0
- package/dist/specs/sidecar.js +38 -0
- 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 +50 -0
- package/dist/specs/writeSpec.d.ts.map +1 -1
- package/dist/specs/writeSpec.js +251 -84
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -26,6 +26,78 @@ The local Node service. Owns:
|
|
|
26
26
|
|
|
27
27
|
To add an agent: implement an `AgentDescriptor`, register it in `registry.ts`. Done.
|
|
28
28
|
|
|
29
|
+
## Spec generation (specs/)
|
|
30
|
+
|
|
31
|
+
A verified session crystallizes into a standard `@playwright/test` file under
|
|
32
|
+
`<devRoot>/__vibe_tests__/`. Translation is **deterministic — no LLM on the
|
|
33
|
+
per-save path** (reproducible by construction):
|
|
34
|
+
|
|
35
|
+
- `writeSpec.ts` walks the captured `browser_*` actions and emits one Playwright
|
|
36
|
+
call each (`getByRole` / `getByLabel` / `getByText` selectors — never XPath).
|
|
37
|
+
A few high-frequency multi-action shapes are hardcoded (popup / new-tab →
|
|
38
|
+
`Promise.all([context.waitForEvent('page'), …click()])`). An action with no
|
|
39
|
+
single-step translation (file upload, drag, …) leaves a structured
|
|
40
|
+
`// hover:optimizable: <tool>` marker rather than a `// TODO` — the draft stays
|
|
41
|
+
runnable around it. `countOptimizableMarkers()` reads the count back; `listSpecs`
|
|
42
|
+
surfaces it as `SpecSummary.optimizableCount`.
|
|
43
|
+
- Alongside the `.spec.ts`, a **sidecar** is written to
|
|
44
|
+
`.hover/<slug>.json` — the structured `SpecStep[]` + observed signals. This is
|
|
45
|
+
the machine-readable behavior record the optimization pass reads (it keeps the
|
|
46
|
+
spec itself clean).
|
|
47
|
+
|
|
48
|
+
### Optional optimization pass (F7)
|
|
49
|
+
|
|
50
|
+
The **service** (never the sandboxed browser agent) can optionally run an LLM
|
|
51
|
+
**codegen** call over a draft to polish it — chiefly to add assertions for the
|
|
52
|
+
feedback the session observed. Its input is data the service already holds (the
|
|
53
|
+
draft + sidecar + relevant seeds), not live page content, so it sits outside the
|
|
54
|
+
agent's prompt-injection surface and needs no filesystem access. It writes an
|
|
55
|
+
**optimization candidate** to `.hover/optimized/<slug>.spec.ts.draft` (never
|
|
56
|
+
`*.spec.ts`, so the test runner can't collect an unreviewed candidate). A human
|
|
57
|
+
promotes or discards it via diff — **the deterministic original is always
|
|
58
|
+
preserved**. Off by default.
|
|
59
|
+
|
|
60
|
+
### Seed library — extending translation (`.hover/rules/`)
|
|
61
|
+
|
|
62
|
+
The optimization pass generalizes from **seeds**: human-written worked examples
|
|
63
|
+
of "captured steps → the Playwright code they should produce." This is how
|
|
64
|
+
coverage of new multi-step patterns grows **without core changes** — you (or the
|
|
65
|
+
community) drop a JSON file; the pass picks it up as few-shot.
|
|
66
|
+
|
|
67
|
+
A seed lives at `<projectRoot>/.hover/rules/<name>.json` and matches
|
|
68
|
+
[`src/specs/seed.schema.json`](src/specs/seed.schema.json):
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"name": "oauth-popup",
|
|
73
|
+
"signature": ["browser_click", "browser_tabs:select"],
|
|
74
|
+
"note": "sign in through a provider popup that opens a new tab",
|
|
75
|
+
"example": {
|
|
76
|
+
"steps": [
|
|
77
|
+
{ "tool": "browser_click", "element": "Sign in with Google button" },
|
|
78
|
+
{ "tool": "browser_tabs", "action": "select", "idx": 1 }
|
|
79
|
+
],
|
|
80
|
+
"code": "const [popup] = await Promise.all([\n context.waitForEvent('page'),\n page.getByRole('button', { name: 'Sign in with Google' }).click(),\n]);\nawait popup.getByLabel('Email').fill('user@example.com');"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- **`signature`** is a cheap relevance filter only — `relevantSeeds()` keeps a
|
|
86
|
+
seed if any of its base tools (`browser_tabs:select` → `browser_tabs`) appears
|
|
87
|
+
in the spec being optimized. It is **not** exact-matched.
|
|
88
|
+
- **`code`** must obey the same rules as generated specs: semantic selectors, no
|
|
89
|
+
XPath, no `waitForTimeout`.
|
|
90
|
+
- **Built-in seeds** ship in `src/specs/seeds.ts` (`BUILTIN_SEEDS`). The bar to
|
|
91
|
+
be built-in is high — only **highly certain**, app-agnostic, deterministic
|
|
92
|
+
patterns qualify (currently just `download`). Semantic / judgement-based
|
|
93
|
+
optimizations (e.g. *which* feedback text to assert) are not seeds — they're
|
|
94
|
+
standing instructions in the prompt. Popup is hardcoded in `writeSpec.ts`, not
|
|
95
|
+
a seed. Speculative or project-specific patterns belong in your own
|
|
96
|
+
`.hover/rules/`, where the bar is your call.
|
|
97
|
+
|
|
98
|
+
`readSeeds(projectRoot)` returns built-ins + your `.hover/rules/*.json`
|
|
99
|
+
(malformed files are skipped, not fatal).
|
|
100
|
+
|
|
29
101
|
## Smoke test
|
|
30
102
|
|
|
31
103
|
```bash
|
|
@@ -56,7 +128,7 @@ Environment variables:
|
|
|
56
128
|
The `claude -p` invocation is locked down so Claude can only drive the browser:
|
|
57
129
|
|
|
58
130
|
- `--strict-mcp-config` — ignore any MCP servers in `~/.claude/` or `.mcp.json`
|
|
59
|
-
- `--allowedTools mcp__playwright
|
|
131
|
+
- `--allowedTools mcp__playwright` — only Playwright MCP is callable
|
|
60
132
|
- `--disallowedTools Bash Edit Write Read Grep Glob Task WebFetch WebSearch EnterWorktree CronCreate …` — every built-in tool explicitly denied (full list in `CLAUDE_DEFAULT_DISALLOWED_TOOLS` in `claude.ts`)
|
|
61
133
|
- `--permission-mode dontAsk` — anything not whitelisted aborts the run
|
|
62
134
|
- `--max-budget-usd <n>` — optional hard $ ceiling per session (no default; pass `maxBudgetUsd` in plugin options or via the CLI flag to enable)
|
|
@@ -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":"claude.d.ts","sourceRoot":"","sources":["../../src/agents/claude.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAA2C,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../src/agents/claude.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAA2C,MAAM,YAAY,CAAC;AA0H3F,eAAO,MAAM,WAAW,EAAE,eA2HzB,CAAC"}
|
package/dist/agents/claude.js
CHANGED
|
@@ -60,6 +60,19 @@ const CLAUDE_DEFAULT_DISALLOWED_TOOLS = [
|
|
|
60
60
|
'Monitor', 'TaskOutput', 'TaskStop',
|
|
61
61
|
'AskUserQuestion',
|
|
62
62
|
'ShareOnboardingGuide',
|
|
63
|
+
// Skills are loaded independently of the --allowedTools allow-list, so an
|
|
64
|
+
// allow-list of `mcp__playwright` does NOT block the `Skill` tool. Left
|
|
65
|
+
// through, the agent burns a turn "checking for a project skill first" and
|
|
66
|
+
// pollutes the crystallized spec with a junk `When · Skill` step. Deny it.
|
|
67
|
+
'Skill',
|
|
68
|
+
// Playwright MCP's arbitrary-JS tools. browser_run_code_unsafe /
|
|
69
|
+
// browser_evaluate run any JS in the page — a real prompt-injection exfil
|
|
70
|
+
// path (fetch a token out, read localStorage) that punches through the
|
|
71
|
+
// "Playwright MCP only" sandbox, and their output can't be translated into
|
|
72
|
+
// a deterministic Playwright spec anyway (it lands as a `// TODO`). Agents
|
|
73
|
+
// drive via click/fill/select and read state via snapshot instead.
|
|
74
|
+
'mcp__playwright__browser_run_code_unsafe',
|
|
75
|
+
'mcp__playwright__browser_evaluate',
|
|
63
76
|
];
|
|
64
77
|
export const claudeAgent = {
|
|
65
78
|
id: 'claude',
|
|
@@ -68,6 +81,7 @@ export const claudeAgent = {
|
|
|
68
81
|
streamFormat: 'stream-json',
|
|
69
82
|
sandboxStrength: 'hard',
|
|
70
83
|
defaultDisallowedTools: CLAUDE_DEFAULT_DISALLOWED_TOOLS,
|
|
84
|
+
apiKeyEnv: 'ANTHROPIC_API_KEY',
|
|
71
85
|
display: {
|
|
72
86
|
label: 'Claude Code',
|
|
73
87
|
tagline: 'Anthropic — best-in-class browser driving, hard tool sandbox',
|
|
@@ -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;
|
|
@@ -59,6 +61,7 @@ export const codexAgent = {
|
|
|
59
61
|
protocol: 'argv',
|
|
60
62
|
streamFormat: 'json-lines',
|
|
61
63
|
sandboxStrength: 'soft',
|
|
64
|
+
apiKeyEnv: 'OPENAI_API_KEY',
|
|
62
65
|
display: {
|
|
63
66
|
label: 'OpenAI Codex',
|
|
64
67
|
tagline: 'OpenAI — soft sandbox (no built-in tool deny-list)',
|
|
@@ -127,7 +130,7 @@ export const codexAgent = {
|
|
|
127
130
|
// The exact field names aren't published. Read defensively: prefer
|
|
128
131
|
// `name`, fall back to `tool`. Same for input.
|
|
129
132
|
const rawName = it.name ?? it.tool ?? '';
|
|
130
|
-
const tool = rawName
|
|
133
|
+
const tool = stripMcpPrefix(rawName);
|
|
131
134
|
out.push({ kind: 'tool_use', tool, input: it.input ?? it.arguments, costUsdSnapshot: s.runningCost });
|
|
132
135
|
}
|
|
133
136
|
else if (it.type === 'command_execution') {
|
|
@@ -157,7 +160,10 @@ export const codexAgent = {
|
|
|
157
160
|
if (ev.type === 'turn.completed') {
|
|
158
161
|
s.runningTurns += 1;
|
|
159
162
|
if (ev.usage) {
|
|
160
|
-
|
|
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);
|
|
161
167
|
}
|
|
162
168
|
out.push({ kind: 'usage', costUsd: s.runningCost, turns: s.runningTurns });
|
|
163
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":"invoke.d.ts","sourceRoot":"","sources":["../../src/agents/invoke.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAe,MAAM,YAAY,CAAC;AAE1E;;;;;;;;GAQG;AACH,wBAAuB,WAAW,CAAC,IAAI,EAAE,aAAa,GAAG,aAAa,CAAC,WAAW,CAAC,
|
|
1
|
+
{"version":3,"file":"invoke.d.ts","sourceRoot":"","sources":["../../src/agents/invoke.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAe,MAAM,YAAY,CAAC;AAE1E;;;;;;;;GAQG;AACH,wBAAuB,WAAW,CAAC,IAAI,EAAE,aAAa,GAAG,aAAa,CAAC,WAAW,CAAC,CA+ElF"}
|
package/dist/agents/invoke.js
CHANGED
|
@@ -31,7 +31,16 @@ export async function* invokeAgent(opts) {
|
|
|
31
31
|
cwd: opts.cwd,
|
|
32
32
|
// Clear CLAUDECODE so spawning `claude` from inside a Claude Code session
|
|
33
33
|
// doesn't trip the nested-session guard. Harmless for other agents.
|
|
34
|
-
|
|
34
|
+
// If the caller supplied an API key and the descriptor names a key env var,
|
|
35
|
+
// inject it so the CLI runs on the key instead of a logged-in subscription.
|
|
36
|
+
// The key lives only in this child's env — never logged, never persisted.
|
|
37
|
+
env: {
|
|
38
|
+
...process.env,
|
|
39
|
+
CLAUDECODE: '',
|
|
40
|
+
...(opts.apiKey && descriptor.apiKeyEnv
|
|
41
|
+
? { [descriptor.apiKeyEnv]: opts.apiKey }
|
|
42
|
+
: {}),
|
|
43
|
+
},
|
|
35
44
|
});
|
|
36
45
|
const onAbort = () => {
|
|
37
46
|
if (!child.killed)
|
|
@@ -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
|
+
}
|
package/dist/agents/types.d.ts
CHANGED
|
@@ -31,6 +31,11 @@ export interface InvokeOptions {
|
|
|
31
31
|
* "the user's current Chrome tab is already on http://localhost:5173/,
|
|
32
32
|
* don't browser_navigate there". */
|
|
33
33
|
appendSystemPrompt?: string;
|
|
34
|
+
/** Optional model API key. Injected into the spawned CLI's environment under
|
|
35
|
+
* the descriptor's `apiKeyEnv` var (e.g. ANTHROPIC_API_KEY) so a user without
|
|
36
|
+
* a logged-in subscription can drive Hover with their own key. Never logged,
|
|
37
|
+
* never persisted server-side — held only for the lifetime of the spawn. */
|
|
38
|
+
apiKey?: string;
|
|
34
39
|
/** Aborts the spawned child if signaled. Used to stop an orphan run when
|
|
35
40
|
* the WebSocket caller disconnects (e.g. user reloads the dev page). */
|
|
36
41
|
signal?: AbortSignal;
|
|
@@ -151,6 +156,12 @@ export interface AgentDescriptor {
|
|
|
151
156
|
* per-CLI deny list live alongside its descriptor instead of as a magic
|
|
152
157
|
* array in the service. Soft-sandbox agents leave this undefined. */
|
|
153
158
|
defaultDisallowedTools?: readonly string[];
|
|
159
|
+
/** Environment variable this CLI reads its model API key from
|
|
160
|
+
* (claude: ANTHROPIC_API_KEY, codex: OPENAI_API_KEY). When set and the
|
|
161
|
+
* caller supplies `InvokeOptions.apiKey`, the key is injected into the spawn
|
|
162
|
+
* env so the user can run on a raw key instead of a logged-in subscription.
|
|
163
|
+
* Undefined for agents that have no API-key env path. */
|
|
164
|
+
apiKeyEnv?: string;
|
|
154
165
|
buildArgs(opts: InvokeOptions): string[];
|
|
155
166
|
/**
|
|
156
167
|
* Parse a single line of agent stdout into normalised InvokeEvents.
|
|
@@ -1 +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,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5E;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;AACrD;;;;;;;;GAQG;GACD;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACnH;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAElC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,CAAC;AAE9C;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;IACd,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;4DACwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;mEAC+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAElD,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,aAAa,CAAC;IACxB,YAAY,EAAE,YAAY,CAAC;IAC3B,eAAe,EAAE,eAAe,CAAC;IACjC,OAAO,EAAE,YAAY,CAAC;IACtB;;;0EAGsE;IACtE,sBAAsB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,EAAE,CAAC;IACzC;;;;;;OAMG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,WAAW,EAAE,CAAC;IAC7D;;;;;;;;;OASG;IACH,WAAW,CAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,WAAW,GAAG,IAAI,CAAC;CAChF"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/agents/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -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":""}
|