@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
|
@@ -3,6 +3,14 @@ import { mkdirSync, writeFileSync } from 'node:fs';
|
|
|
3
3
|
import { dirname, resolve } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import process from 'node:process';
|
|
6
|
+
/** The `mcp__<id>` tool-name prefix Claude Code exposes a plugin MCP server's
|
|
7
|
+
* tools under: non-alphanumerics collapse to `_` and edges are trimmed (e.g.
|
|
8
|
+
* `@hover-dev/security:flows` → `mcp__hover_dev_security_flows`). Used to build
|
|
9
|
+
* the hard-sandbox allow-list. Single source so the service and the CLI scan
|
|
10
|
+
* command can't drift on how the prefix is derived. */
|
|
11
|
+
export function mcpToolPrefix(serverId) {
|
|
12
|
+
return `mcp__${serverId.replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_+|_+$/g, '')}`;
|
|
13
|
+
}
|
|
6
14
|
export function resolveMcpConfig(opts) {
|
|
7
15
|
// Resolve the package's main file, then walk back to its package root.
|
|
8
16
|
// Using `package.json` as the resolution target is the documented
|
|
@@ -46,8 +54,13 @@ export function resolveMcpConfig(opts) {
|
|
|
46
54
|
const config = { mcpServers };
|
|
47
55
|
const outDir = resolve(tmpdir(), 'hover');
|
|
48
56
|
mkdirSync(outDir, { recursive: true });
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
// Sanitise the suffix before it lands in a filesystem path — it's derived
|
|
58
|
+
// from plugin/mode ids, so guard against path separators and other unsafe
|
|
59
|
+
// characters slipping into the filename.
|
|
60
|
+
const safeSuffix = opts.suffix
|
|
61
|
+
? `-${opts.suffix.replace(/[^a-zA-Z0-9._-]+/g, '_')}`
|
|
62
|
+
: '';
|
|
63
|
+
const outPath = resolve(outDir, `mcp-config-${opts.port}${safeSuffix}.json`);
|
|
51
64
|
writeFileSync(outPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
52
65
|
return outPath;
|
|
53
66
|
}
|
package/dist/plugin-api.d.ts
CHANGED
|
@@ -40,6 +40,13 @@ export interface HoverPluginMode {
|
|
|
40
40
|
/** Mode ids this mode cannot be active alongside. Two plugins both
|
|
41
41
|
* needing an exclusive proxy would set each other here. */
|
|
42
42
|
conflictsWith?: string[];
|
|
43
|
+
/** CSS colour the widget tints to while this mode is engaged — the mode
|
|
44
|
+
* bar, launcher, and panel chrome all retint to it. Any CSS colour the
|
|
45
|
+
* user's Chrome accepts (the widget derives the dim/hover/ink/tint shades
|
|
46
|
+
* from it via `color-mix`). Defaults to security orange (`#fb923c`) when
|
|
47
|
+
* omitted, so a plugin only sets this to stand apart — e.g. pentest's
|
|
48
|
+
* `#ef4444` red signalling "offensive mode". */
|
|
49
|
+
accent?: string;
|
|
43
50
|
}
|
|
44
51
|
export interface HoverPluginMcpServer {
|
|
45
52
|
/** Stable, namespaced id (`@hover-dev/security:flows`). Host enforces
|
package/dist/plugin-api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-api.d.ts","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC;AAChC,eAAO,MAAM,mBAAmB,EAAE,eAAmB,CAAC;AAMtD,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,EAAE,EAAE,MAAM,CAAC;IACX,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;+BAE2B;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;gEAC4D;IAC5D,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin-api.d.ts","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"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"}
|
package/dist/runSession.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runSession.d.ts","sourceRoot":"","sources":["../src/runSession.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAGxD,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB;oFACgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;6EACyE;IACzE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;yCAEqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;+EAC2E;IAC3E,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B;oFACgF;IAChF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B;mCAC+B;IAC/B,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,iBAAiB,EACvB,OAAO,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,GACjC,OAAO,CAAC,gBAAgB,CAAC,
|
|
1
|
+
{"version":3,"file":"runSession.d.ts","sourceRoot":"","sources":["../src/runSession.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAGxD,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB;oFACgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;6EACyE;IACzE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;yCAEqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;+EAC2E;IAC3E,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B;oFACgF;IAChF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B;mCAC+B;IAC/B,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,iBAAiB,EACvB,OAAO,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,GACjC,OAAO,CAAC,gBAAgB,CAAC,CA4D3B"}
|
package/dist/runSession.js
CHANGED
|
@@ -70,6 +70,11 @@ export async function runSession(opts, onEvent) {
|
|
|
70
70
|
isError = true;
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
+
// On abort (opts.signal), invokeAgent SIGTERMs the child and no session_end
|
|
74
|
+
// arrives, so the error flag above never gets set. Honour the doc contract
|
|
75
|
+
// ("True if the run ended in error or was aborted") by flipping it here.
|
|
76
|
+
if (opts.signal?.aborted)
|
|
77
|
+
isError = true;
|
|
73
78
|
if (summary)
|
|
74
79
|
steps.push({ kind: 'done', summary });
|
|
75
80
|
return { steps, summary, isError };
|
|
@@ -14,13 +14,9 @@ import type { WebSocket } from 'ws';
|
|
|
14
14
|
import { type LaunchOptions } from '../playwright/launchChrome.js';
|
|
15
15
|
import { type ClientMessage } from './types.js';
|
|
16
16
|
/** Extra launch options surfaced from the active mode (security plugin
|
|
17
|
-
* needs proxy + spki
|
|
18
|
-
*
|
|
19
|
-
export type LaunchExtras = Pick<LaunchOptions, '
|
|
20
|
-
/** Override CDP port (mode-specific, e.g. 9333 for security). When set,
|
|
21
|
-
* this also wins over the `port` parsed from cdpUrl. */
|
|
22
|
-
cdpPort?: number;
|
|
23
|
-
};
|
|
17
|
+
* needs a resident proxy + spki). When none are set, behaviour is identical
|
|
18
|
+
* to pre-v0.7 normal-mode launch. */
|
|
19
|
+
export type LaunchExtras = Pick<LaunchOptions, 'proxy'>;
|
|
24
20
|
/**
|
|
25
21
|
* "Is this widget running inside the debug Chrome?" The widget asks this on
|
|
26
22
|
* connect (and after every status-changing event) so it can render itself as
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cdpHandlers.d.ts","sourceRoot":"","sources":["../../src/service/cdpHandlers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAEpC,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACtF,OAAO,EAAQ,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAEtD;;
|
|
1
|
+
{"version":3,"file":"cdpHandlers.d.ts","sourceRoot":"","sources":["../../src/service/cdpHandlers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAEpC,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACtF,OAAO,EAAQ,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAEtD;;sCAEsC;AACtC,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;AAExD;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,YAAY,GACpB,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,YAAY,GACpB,OAAO,CAAC,IAAI,CAAC,CA4Bf;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,YAAY,GACpB,OAAO,CAAC,IAAI,CAAC,CAUf"}
|
|
@@ -27,10 +27,7 @@ export async function handleCheckCdp(ws, msg, cdpUrl, extras) {
|
|
|
27
27
|
send(ws, { type: 'error', payload: { message: 'check-cdp: pageUrl is required' } });
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
|
-
const
|
|
31
|
-
? `http://localhost:${extras.cdpPort}`
|
|
32
|
-
: cdpUrl;
|
|
33
|
-
const status = await checkCdpStatus(effectiveCdpUrl, pageUrl);
|
|
30
|
+
const status = await checkCdpStatus(cdpUrl, pageUrl);
|
|
34
31
|
send(ws, { type: 'cdp-status', payload: status });
|
|
35
32
|
}
|
|
36
33
|
/**
|
|
@@ -48,7 +45,7 @@ export async function handleLaunchChrome(ws, msg, cdpUrl, extras) {
|
|
|
48
45
|
// Tell the widget we're launching so it can render a spinner immediately —
|
|
49
46
|
// findChromeBinary + spawn + ready-poll can take a few seconds.
|
|
50
47
|
send(ws, { type: 'cdp-status', payload: { state: 'no-cdp', launching: true } });
|
|
51
|
-
const port =
|
|
48
|
+
const port = (() => {
|
|
52
49
|
try {
|
|
53
50
|
return Number(new URL(cdpUrl).port) || 9222;
|
|
54
51
|
}
|
|
@@ -59,19 +56,13 @@ export async function handleLaunchChrome(ws, msg, cdpUrl, extras) {
|
|
|
59
56
|
const result = await launchDebugChrome({
|
|
60
57
|
url: pageUrl,
|
|
61
58
|
port,
|
|
62
|
-
userDataDir: extras?.userDataDir,
|
|
63
59
|
proxy: extras?.proxy,
|
|
64
60
|
});
|
|
65
61
|
if (!result.ok) {
|
|
66
62
|
send(ws, { type: 'cdp-status', payload: { state: 'no-cdp', reason: result.reason } });
|
|
67
63
|
return;
|
|
68
64
|
}
|
|
69
|
-
|
|
70
|
-
// specific port (9333 for security) doesn't get probed at 9222.
|
|
71
|
-
const effectiveCdpUrl = extras?.cdpPort
|
|
72
|
-
? `http://localhost:${extras.cdpPort}`
|
|
73
|
-
: cdpUrl;
|
|
74
|
-
const status = await checkCdpStatus(effectiveCdpUrl, pageUrl);
|
|
65
|
+
const status = await checkCdpStatus(cdpUrl, pageUrl);
|
|
75
66
|
send(ws, { type: 'cdp-status', payload: status });
|
|
76
67
|
}
|
|
77
68
|
/**
|
|
@@ -87,10 +78,7 @@ export async function handleFocusDebug(ws, msg, cdpUrl, extras) {
|
|
|
87
78
|
send(ws, { type: 'error', payload: { message: 'focus-debug: pageUrl is required' } });
|
|
88
79
|
return;
|
|
89
80
|
}
|
|
90
|
-
const
|
|
91
|
-
? `http://localhost:${extras.cdpPort}`
|
|
92
|
-
: cdpUrl;
|
|
93
|
-
const result = await focusDebugTab(effectiveCdpUrl, pageUrl);
|
|
81
|
+
const result = await focusDebugTab(cdpUrl, pageUrl);
|
|
94
82
|
if (!result.ok) {
|
|
95
83
|
send(ws, { type: 'error', payload: { message: `focus-debug: ${result.reason}` } });
|
|
96
84
|
}
|
package/dist/service.d.ts
CHANGED
|
@@ -29,6 +29,12 @@ export interface ServiceOptions {
|
|
|
29
29
|
* see the proxy; moving it here is what enables the single-Chrome model.
|
|
30
30
|
* Default false (shims pass it through from their own option). */
|
|
31
31
|
autoLaunchChrome?: boolean;
|
|
32
|
+
/** Opt-in: give the agent READ-ONLY, fenced access to the project's source
|
|
33
|
+
* via a `read_source` / `list_source` MCP server (in addition to Playwright
|
|
34
|
+
* MCP), in every mode. Lets it author against real selectors/routes and do
|
|
35
|
+
* white-box security/pentest. Fenced to devRoot, secrets/keys/.git/build
|
|
36
|
+
* excluded, no write/exec. Default false — the agent stays browser-only. */
|
|
37
|
+
codeContext?: boolean;
|
|
32
38
|
/** The dev-server URL the auto-launched Chrome should open. Each shim knows
|
|
33
39
|
* its own framework's dev URL and passes it here. Defaults to the cdp host
|
|
34
40
|
* if unset, but shims should always provide it. */
|
package/dist/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AA+EA,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,iBAAiB,CAAC;AAQzB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;oFACgF;IAChF,YAAY,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,IAAI,CAAC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;gFAG4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;uBAImB;IACnB,OAAO,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAChC;;;;;;uEAMmE;IACnE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;iFAI6E;IAC7E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;wDAEoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B;4EACwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAiED,wBAAsB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CA49B/E"}
|
package/dist/service.js
CHANGED
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
* { type: 'list-modes' }
|
|
47
47
|
*/
|
|
48
48
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
49
|
+
import { fileURLToPath } from 'node:url';
|
|
50
|
+
import { dirname, resolve } from 'node:path';
|
|
49
51
|
import { runSession } from './runSession.js';
|
|
50
52
|
import { readConventions } from './service/conventions.js';
|
|
51
53
|
import { optimizeSpecWithAgent } from './specs/optimizeSpecWithAgent.js';
|
|
@@ -53,7 +55,7 @@ import { promoteOptimized, discardOptimized } from './specs/optimizeSpec.js';
|
|
|
53
55
|
import { listAgentAvailability, pickPrimaryAgent, } from './agents/detect.js';
|
|
54
56
|
import { getAgent } from './agents/registry.js';
|
|
55
57
|
import { getPreflight, invalidatePreflight } from './playwright/preflightCache.js';
|
|
56
|
-
import { resolveMcpConfig } from './playwright/resolveMcpConfig.js';
|
|
58
|
+
import { resolveMcpConfig, mcpToolPrefix } from './playwright/resolveMcpConfig.js';
|
|
57
59
|
import { launchDebugChrome } from './playwright/launchChrome.js';
|
|
58
60
|
import { listSpecs } from './specs/listSpecs.js';
|
|
59
61
|
import { readSeeds, BUILTIN_SEEDS } from './specs/seeds.js';
|
|
@@ -62,6 +64,11 @@ import { buildCdpHint, buildCdpHintResume } from './service/cdpHint.js';
|
|
|
62
64
|
import { handleCheckCdp, handleLaunchChrome, handleFocusDebug, } from './service/cdpHandlers.js';
|
|
63
65
|
import { handleSaveArtifact, SPEC_CONFIG, CASE_CSV_CONFIG, } from './service/saveHandlers.js';
|
|
64
66
|
import { CURRENT_API_VERSION, } from './plugin-api.js';
|
|
67
|
+
/** The source-reader MCP server (codeContext). Id → the `mcp__hover_source`
|
|
68
|
+
* tool prefix; script path resolved relative to this module so it works from
|
|
69
|
+
* dist/. Spawned only when codeContext is enabled. */
|
|
70
|
+
const SOURCE_MCP_ID = 'hover-source';
|
|
71
|
+
const SOURCE_MCP_SCRIPT = resolve(dirname(fileURLToPath(import.meta.url)), 'mcp', 'sourceServer.js');
|
|
65
72
|
// ClientMessage + send moved to ./service/types.ts so the cdp + save
|
|
66
73
|
// handler modules can share them. See those files for the wire shape.
|
|
67
74
|
const PROTOCOL_VERSION = 1;
|
|
@@ -195,6 +202,15 @@ export async function startService(opts) {
|
|
|
195
202
|
}
|
|
196
203
|
}
|
|
197
204
|
}
|
|
205
|
+
// codeContext (opt-in, all modes): the fenced read-only source reader.
|
|
206
|
+
if (opts.codeContext) {
|
|
207
|
+
extra.push({
|
|
208
|
+
id: SOURCE_MCP_ID,
|
|
209
|
+
command: process.execPath,
|
|
210
|
+
args: [SOURCE_MCP_SCRIPT],
|
|
211
|
+
env: { HOVER_PROJECT_ROOT: devRoot },
|
|
212
|
+
});
|
|
213
|
+
}
|
|
198
214
|
// Single-Chrome model: the Playwright MCP always points at the one debug
|
|
199
215
|
// Chrome on the normal cdpUrl. (Pre-single-Chrome this branched to a
|
|
200
216
|
// mode-specific port like 9333; there's no second Chrome anymore.)
|
|
@@ -239,6 +255,25 @@ export async function startService(opts) {
|
|
|
239
255
|
}
|
|
240
256
|
/** id of the currently-active mode, or null for normal (unmoded) mode. */
|
|
241
257
|
let currentModeId = null;
|
|
258
|
+
/**
|
|
259
|
+
* The single in-flight agent run, held at SERVICE scope (not per-connection)
|
|
260
|
+
* so it SURVIVES the widget's WS dropping. The widget lives in the page the
|
|
261
|
+
* agent drives, so any agent navigation (a pentest payload in the URL, an
|
|
262
|
+
* HMR reload) tears the widget down and closes its socket — but the agent is
|
|
263
|
+
* still happily driving the tab over CDP and recording findings server-side.
|
|
264
|
+
* Killing it on every navigation made pentest mode (which navigates
|
|
265
|
+
* constantly) unusable. Instead: detach on close, keep streaming to whichever
|
|
266
|
+
* ws is attached, and only abort if no widget reconnects within the grace
|
|
267
|
+
* window. Single active run — Hover binds 127.0.0.1 for one local user.
|
|
268
|
+
*/
|
|
269
|
+
const RECONNECT_GRACE_MS = 15_000;
|
|
270
|
+
let activeRun = null;
|
|
271
|
+
/** Send a run event to whichever ws is currently attached (survives reconnect). */
|
|
272
|
+
const emitToRun = (msg) => {
|
|
273
|
+
const c = activeRun?.client;
|
|
274
|
+
if (c && c.readyState === WebSocket.OPEN)
|
|
275
|
+
send(c, msg);
|
|
276
|
+
};
|
|
242
277
|
/** Chrome-proxy settings a plugin's `hover:service:start` hook set on us
|
|
243
278
|
* (security's resident MITM). RESIDENT for the whole session — set once
|
|
244
279
|
* before Chrome launches, never cleared on mode change — so the single
|
|
@@ -273,6 +308,9 @@ export async function startService(opts) {
|
|
|
273
308
|
id: p.mode.id,
|
|
274
309
|
label: p.mode.label,
|
|
275
310
|
description: p.mode.description,
|
|
311
|
+
// Widget retints to this while the mode is engaged (falls back to
|
|
312
|
+
// security orange in the widget when absent).
|
|
313
|
+
accent: p.mode.accent,
|
|
276
314
|
pluginName: p.name,
|
|
277
315
|
}));
|
|
278
316
|
const payload = { current: currentModeId, available };
|
|
@@ -410,20 +448,42 @@ export async function startService(opts) {
|
|
|
410
448
|
// Send the mode catalogue too, so the widget can render the mode
|
|
411
449
|
// toggle immediately. Empty list when no plugins are loaded.
|
|
412
450
|
broadcastModes(ws);
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
451
|
+
// Re-attach to a run that's still in flight (the previous widget dropped —
|
|
452
|
+
// most commonly the agent navigated and reloaded the page the widget lives
|
|
453
|
+
// in). Cancel the pending abort, point the run's event stream at this fresh
|
|
454
|
+
// socket, and tell the widget so it can restore its "running" UI. Without
|
|
455
|
+
// this the run would be killed on every agent navigation.
|
|
456
|
+
// Only re-attach during a genuine reconnect GAP (the prior client is gone).
|
|
457
|
+
// If a live client is still attached, this is a SECOND widget (e.g. the
|
|
458
|
+
// user's regular tab alongside the debug-Chrome tab — both inject a widget
|
|
459
|
+
// on the same origin and open their own socket). Seizing the stream would
|
|
460
|
+
// silence the first widget and let the second's close abort a healthy run,
|
|
461
|
+
// so leave a second concurrent widget in idle UI rather than hijacking.
|
|
462
|
+
if (activeRun && activeRun.client === null) {
|
|
463
|
+
if (activeRun.graceTimer) {
|
|
464
|
+
clearTimeout(activeRun.graceTimer);
|
|
465
|
+
activeRun.graceTimer = null;
|
|
466
|
+
}
|
|
467
|
+
activeRun.client = ws;
|
|
468
|
+
send(ws, { type: 'run-active', payload: { prompt: activeRun.prompt } });
|
|
469
|
+
}
|
|
470
|
+
// If the widget's socket closes while a run it owns is in flight, DON'T
|
|
471
|
+
// abort — the agent is still driving the tab over CDP. Detach this ws and
|
|
472
|
+
// start a grace window; a reconnecting widget (above) cancels the abort.
|
|
473
|
+
// Only if nobody comes back do we abort, so we still never leave an orphan.
|
|
419
474
|
ws.on('close', () => {
|
|
420
|
-
|
|
475
|
+
if (activeRun && activeRun.client === ws) {
|
|
476
|
+
activeRun.client = null;
|
|
477
|
+
activeRun.graceTimer = setTimeout(() => {
|
|
478
|
+
activeRun?.abort.abort();
|
|
479
|
+
}, RECONNECT_GRACE_MS);
|
|
480
|
+
}
|
|
421
481
|
});
|
|
422
482
|
const cancel = () => {
|
|
423
|
-
if (!
|
|
483
|
+
if (!activeRun)
|
|
424
484
|
return;
|
|
425
|
-
cancelled = true;
|
|
426
|
-
|
|
485
|
+
activeRun.cancelled = true;
|
|
486
|
+
activeRun.abort.abort();
|
|
427
487
|
// Send a synthetic session_end so the widget resets to idle immediately.
|
|
428
488
|
// The for-await loop below short-circuits on `cancelled`, so no events
|
|
429
489
|
// from the dying child will arrive after this.
|
|
@@ -433,7 +493,7 @@ export async function startService(opts) {
|
|
|
433
493
|
// stays false because the agent didn't fail: the user chose to
|
|
434
494
|
// end the run. The widget renders this as a neutral "Stopped"
|
|
435
495
|
// state rather than a red Failed card.
|
|
436
|
-
|
|
496
|
+
emitToRun({
|
|
437
497
|
type: 'event',
|
|
438
498
|
payload: {
|
|
439
499
|
kind: 'session_end',
|
|
@@ -460,7 +520,7 @@ export async function startService(opts) {
|
|
|
460
520
|
return;
|
|
461
521
|
}
|
|
462
522
|
if (msg.type === 'set-mode') {
|
|
463
|
-
if (
|
|
523
|
+
if (activeRun) {
|
|
464
524
|
send(ws, {
|
|
465
525
|
type: 'error',
|
|
466
526
|
payload: { message: 'set-mode: a command is already running; stop it first' },
|
|
@@ -515,7 +575,7 @@ export async function startService(opts) {
|
|
|
515
575
|
// Refuse to switch mid-flight; the user's running command would
|
|
516
576
|
// otherwise outlive its own descriptor and the events it produces
|
|
517
577
|
// would be parsed against the wrong wire format.
|
|
518
|
-
if (
|
|
578
|
+
if (activeRun) {
|
|
519
579
|
send(ws, {
|
|
520
580
|
type: 'error',
|
|
521
581
|
payload: { message: 'switch-agent: a command is already running; stop it first' },
|
|
@@ -690,16 +750,21 @@ export async function startService(opts) {
|
|
|
690
750
|
: undefined;
|
|
691
751
|
if (typeof text !== 'string' || !text.trim())
|
|
692
752
|
return;
|
|
693
|
-
if (
|
|
753
|
+
if (activeRun) {
|
|
694
754
|
send(ws, {
|
|
695
755
|
type: 'error',
|
|
696
|
-
payload: { message: 'A command is already running
|
|
756
|
+
payload: { message: 'A command is already running.' },
|
|
697
757
|
});
|
|
698
758
|
return;
|
|
699
759
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
760
|
+
const run = {
|
|
761
|
+
abort: new AbortController(),
|
|
762
|
+
cancelled: false,
|
|
763
|
+
client: ws,
|
|
764
|
+
graceTimer: null,
|
|
765
|
+
prompt: text,
|
|
766
|
+
};
|
|
767
|
+
activeRun = run;
|
|
703
768
|
try {
|
|
704
769
|
// Build the MCP config first — it's pure local file IO and lets
|
|
705
770
|
// us assert plugin-contributed servers landed in the config even
|
|
@@ -710,13 +775,7 @@ export async function startService(opts) {
|
|
|
710
775
|
// Playwright MCP server would silently launch its own Chromium —
|
|
711
776
|
// and Hover's premise is to drive the user's existing Chrome (with
|
|
712
777
|
// their dev state, cookies, devtools open), never spawn a fresh one.
|
|
713
|
-
|
|
714
|
-
// own port (e.g. 9333 for security), not the default cdpUrl.
|
|
715
|
-
const preflightExtras = effectiveLaunchExtras();
|
|
716
|
-
const preflightCdpUrl = preflightExtras?.cdpPort
|
|
717
|
-
? `http://localhost:${preflightExtras.cdpPort}`
|
|
718
|
-
: cdpUrl;
|
|
719
|
-
const cdp = await getPreflight(preflightCdpUrl);
|
|
778
|
+
const cdp = await getPreflight(cdpUrl);
|
|
720
779
|
if (!cdp.ok) {
|
|
721
780
|
send(ws, {
|
|
722
781
|
type: 'event',
|
|
@@ -771,6 +830,13 @@ export async function startService(opts) {
|
|
|
771
830
|
}
|
|
772
831
|
}
|
|
773
832
|
}
|
|
833
|
+
// codeContext: tell the agent the fenced source reader exists, so it
|
|
834
|
+
// proactively reads the real code (better selectors/routes when
|
|
835
|
+
// authoring; white-box confirmation when probing) instead of only
|
|
836
|
+
// guessing from the rendered DOM.
|
|
837
|
+
if (opts.codeContext) {
|
|
838
|
+
appendSystemPrompt = `${appendSystemPrompt}\n\nYou also have read-only access to this project's source via mcp__hover_source (read_source / list_source), fenced to the repo (secrets, keys, .env, .git, node_modules and build output are refused). Use it to read the actual component / route / API code — write tests against the real selectors and, when probing for security issues, confirm a finding against the server code (the query, the authz check) rather than guessing from the page alone.`;
|
|
839
|
+
}
|
|
774
840
|
// Mirror the prompt's language in the agent's *prose* output — the
|
|
775
841
|
// verification summary (Result card), the ## Findings block, and the
|
|
776
842
|
// step narration — the same way Voice mode mirrors it in TTS. A
|
|
@@ -784,8 +850,8 @@ export async function startService(opts) {
|
|
|
784
850
|
}
|
|
785
851
|
// Snapshot the agent id so a switch-agent message during the run
|
|
786
852
|
// can't smear two agents across one invocation. (We also gate
|
|
787
|
-
// switch-agent on
|
|
788
|
-
// allow/deny lists on the agent's sandboxStrength internally.
|
|
853
|
+
// switch-agent on an active run, but defense in depth.) runSession gates
|
|
854
|
+
// the allow/deny lists on the agent's sandboxStrength internally.
|
|
789
855
|
const invokedAgentId = currentAgentId;
|
|
790
856
|
// Active mode's plugin-contributed MCP server ids — added to the
|
|
791
857
|
// hard-sandbox allow list so Claude can actually call them. Claude
|
|
@@ -794,18 +860,20 @@ export async function startService(opts) {
|
|
|
794
860
|
// and `--allowedTools mcp__foo` matches every tool under that
|
|
795
861
|
// prefix. We pass the prefix `mcp__<sanitized>` so all of the
|
|
796
862
|
// server's tools are reachable.
|
|
797
|
-
const sanitize = (s) => s.replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
|
798
863
|
const activePluginMcpIds = [];
|
|
799
864
|
if (currentModeId) {
|
|
800
865
|
for (const p of plugins) {
|
|
801
866
|
for (const srv of p.mcpServers ?? []) {
|
|
802
867
|
const scope = srv.activeInModes ?? (p.mode ? [p.mode.id] : []);
|
|
803
868
|
if (scope.includes('*') || scope.includes(currentModeId)) {
|
|
804
|
-
activePluginMcpIds.push(
|
|
869
|
+
activePluginMcpIds.push(mcpToolPrefix(srv.id));
|
|
805
870
|
}
|
|
806
871
|
}
|
|
807
872
|
}
|
|
808
873
|
}
|
|
874
|
+
// codeContext: the fenced source reader is allowed in every mode.
|
|
875
|
+
if (opts.codeContext)
|
|
876
|
+
activePluginMcpIds.push(mcpToolPrefix(SOURCE_MCP_ID));
|
|
809
877
|
const runResult = await runSession({
|
|
810
878
|
agentId: invokedAgentId,
|
|
811
879
|
prompt: text,
|
|
@@ -821,19 +889,21 @@ export async function startService(opts) {
|
|
|
821
889
|
maxBudgetUsd,
|
|
822
890
|
model,
|
|
823
891
|
apiKey: currentApiKey,
|
|
824
|
-
signal:
|
|
892
|
+
signal: run.abort.signal,
|
|
825
893
|
}, (ev) => {
|
|
826
|
-
|
|
894
|
+
// Stream to whichever ws is attached NOW — survives the widget
|
|
895
|
+
// reconnecting mid-run (emitToRun is a no-op during a reconnect gap).
|
|
896
|
+
if (run.cancelled)
|
|
827
897
|
return;
|
|
828
|
-
|
|
898
|
+
emitToRun({ type: 'event', payload: ev });
|
|
829
899
|
});
|
|
830
900
|
// Re-record: write a fresh spec from the steps runSession accumulated
|
|
831
901
|
// (`user` → `step`* → `done`). Only on a clean, non-cancelled finish —
|
|
832
902
|
// a cancelled/aborted run throws out of runSession into the catch
|
|
833
903
|
// below, and an errored agent leaves the original spec untouched.
|
|
834
|
-
if (reRecordSlug && !cancelled) {
|
|
904
|
+
if (reRecordSlug && !run.cancelled) {
|
|
835
905
|
if (runResult.isError) {
|
|
836
|
-
|
|
906
|
+
emitToRun({
|
|
837
907
|
type: 'error',
|
|
838
908
|
payload: {
|
|
839
909
|
message: `Re-record failed: ${runResult.summary || 'agent reported an error'}. ` +
|
|
@@ -850,14 +920,14 @@ export async function startService(opts) {
|
|
|
850
920
|
steps: runResult.steps,
|
|
851
921
|
overwrite: true,
|
|
852
922
|
});
|
|
853
|
-
|
|
923
|
+
emitToRun({
|
|
854
924
|
type: 'spec-saved',
|
|
855
925
|
payload: { name: reRecordSlug, path: written.path },
|
|
856
926
|
});
|
|
857
927
|
}
|
|
858
928
|
catch (e) {
|
|
859
929
|
const m = e instanceof Error ? e.message : String(e);
|
|
860
|
-
|
|
930
|
+
emitToRun({
|
|
861
931
|
type: 'error',
|
|
862
932
|
payload: { message: `Re-record could not write spec: ${m}` },
|
|
863
933
|
});
|
|
@@ -872,30 +942,25 @@ export async function startService(opts) {
|
|
|
872
942
|
// widget to reconcile two terminal events for one run. CDP isn't
|
|
873
943
|
// suspect either — the user just stopped — so skip preflight
|
|
874
944
|
// invalidation too.
|
|
875
|
-
if (!cancelled) {
|
|
945
|
+
if (!run.cancelled) {
|
|
876
946
|
const message = err instanceof Error ? err.message : String(err);
|
|
877
947
|
const errorEvent = {
|
|
878
948
|
kind: 'session_end',
|
|
879
949
|
isError: true,
|
|
880
950
|
summary: message,
|
|
881
951
|
};
|
|
882
|
-
|
|
952
|
+
emitToRun({ type: 'event', payload: errorEvent });
|
|
883
953
|
// Force the next command to re-probe CDP. The error could be from
|
|
884
954
|
// Chrome dying, MCP spawning a stray Chromium, the user closing
|
|
885
955
|
// their debug window — anything that would make a cached "all
|
|
886
|
-
// healthy" result lie.
|
|
887
|
-
|
|
888
|
-
// mode invalidations don't no-op against the default port.
|
|
889
|
-
const invalExtras = effectiveLaunchExtras();
|
|
890
|
-
const invalCdpUrl = invalExtras?.cdpPort
|
|
891
|
-
? `http://localhost:${invalExtras.cdpPort}`
|
|
892
|
-
: cdpUrl;
|
|
893
|
-
invalidatePreflight(invalCdpUrl);
|
|
956
|
+
// healthy" result lie.
|
|
957
|
+
invalidatePreflight(cdpUrl);
|
|
894
958
|
}
|
|
895
959
|
}
|
|
896
960
|
finally {
|
|
897
|
-
|
|
898
|
-
|
|
961
|
+
if (run.graceTimer)
|
|
962
|
+
clearTimeout(run.graceTimer);
|
|
963
|
+
activeRun = null;
|
|
899
964
|
}
|
|
900
965
|
});
|
|
901
966
|
});
|
|
@@ -954,6 +1019,20 @@ export async function startService(opts) {
|
|
|
954
1019
|
return {
|
|
955
1020
|
port,
|
|
956
1021
|
async close() {
|
|
1022
|
+
// Kill any in-flight run FIRST. The run is held at service scope and is
|
|
1023
|
+
// only torn down by aborting its signal (invoke.ts SIGTERMs the agent
|
|
1024
|
+
// child on abort). wss.close() below stops the listener but does NOT
|
|
1025
|
+
// terminate established client sockets, so no ws.on('close') fires — so
|
|
1026
|
+
// without this the agent child would keep driving the debug Chrome as an
|
|
1027
|
+
// orphan after the dev server is gone, and a pending grace timer would
|
|
1028
|
+
// fire abort() 15s into the void.
|
|
1029
|
+
if (activeRun) {
|
|
1030
|
+
if (activeRun.graceTimer)
|
|
1031
|
+
clearTimeout(activeRun.graceTimer);
|
|
1032
|
+
activeRun.cancelled = true;
|
|
1033
|
+
activeRun.abort.abort();
|
|
1034
|
+
activeRun = null;
|
|
1035
|
+
}
|
|
957
1036
|
// Deactivate the active mode first, then run every plugin's
|
|
958
1037
|
// shutdown hook (regardless of which mode is active — a plugin may
|
|
959
1038
|
// own background state even outside its mode). Best-effort: log
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"optimizeSpec.d.ts","sourceRoot":"","sources":["../../src/specs/optimizeSpec.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"optimizeSpec.d.ts","sourceRoot":"","sources":["../../src/specs/optimizeSpec.ts"],"names":[],"mappings":"AAeA,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAA4B,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGrE,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,OAAO,EAAE,MAAM;CAI5B;AAED,wEAAwE;AACxE,MAAM,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAE7D,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,aAAa,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb;2EACuE;IACvE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,cAAc,CAAC,CA2CzB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,WAAW,GAAG,IAAI,EAC3B,KAAK,GAAE,QAAQ,EAAO,GACrB,MAAM,CAmDR;AAED,kEAAkE;AAClE,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAI/C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAWhF;AAyBD;;gCAEgC;AAChC,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAYrF;AAED;+BAC+B;AAC/B,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEnF"}
|
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { readFile, mkdir, writeFile, rm } from 'node:fs/promises';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
|
+
import { Project } from 'ts-morph';
|
|
15
16
|
import { sidecarDir } from './sidecar.js';
|
|
16
17
|
import { readSeeds, relevantSeeds } from './seeds.js';
|
|
18
|
+
import { softBatch } from './softBatch.js';
|
|
17
19
|
export class OptimizeError extends Error {
|
|
18
20
|
constructor(message) {
|
|
19
21
|
super(message);
|
|
@@ -41,11 +43,15 @@ export async function optimizeSpec(devRoot, slug, runCodegen) {
|
|
|
41
43
|
.map(s => s.tool));
|
|
42
44
|
const seeds = relevantSeeds(await readSeeds(devRoot), specTools);
|
|
43
45
|
const raw = await runCodegen(buildOptimizePrompt(draft, sidecar, seeds));
|
|
44
|
-
const
|
|
45
|
-
const check = validateSpecCode(
|
|
46
|
+
const llmCode = extractCode(raw);
|
|
47
|
+
const check = validateSpecCode(llmCode);
|
|
46
48
|
if (!check.ok) {
|
|
47
49
|
throw new OptimizeError(`optimization rejected — ${check.errors.join('; ')}`);
|
|
48
50
|
}
|
|
51
|
+
// Deterministic finishing step: the LLM decided WHAT to assert; soft-batch
|
|
52
|
+
// applies the safe mechanical rewrite (trailing run of independent assertions
|
|
53
|
+
// → expect.soft) surgically on its output. See softBatch.ts for the guard.
|
|
54
|
+
const code = softBatch(llmCode).code;
|
|
49
55
|
const dir = join(devRoot, '__vibe_tests__', '.hover', 'optimized');
|
|
50
56
|
await mkdir(dir, { recursive: true });
|
|
51
57
|
// `.spec.ts.draft`, never `*.spec.ts` — Playwright's glob must not collect a
|
|
@@ -133,12 +139,28 @@ export function validateSpecCode(code) {
|
|
|
133
139
|
if (!/from\s+['"](@playwright\/test|\.\/fixtures)['"]/.test(code)) {
|
|
134
140
|
errors.push('missing @playwright/test (or ./fixtures) import');
|
|
135
141
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (open !== close)
|
|
139
|
-
errors.push('unbalanced braces');
|
|
142
|
+
if (hasSyntaxError(code))
|
|
143
|
+
errors.push('has a syntax error');
|
|
140
144
|
return { ok: errors.length === 0, errors };
|
|
141
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Real syntax check via the TypeScript parser (the same ts-morph the soft-batch
|
|
148
|
+
* step uses). Replaces a naive `{`/`}` count that mis-flagged a valid spec
|
|
149
|
+
* asserting on a string like 'a { b' — braces inside string literals are not
|
|
150
|
+
* structural. We look at SYNTACTIC diagnostics only: a candidate references
|
|
151
|
+
* `page` / `expect` / `@playwright/test` that aren't resolvable in this throwaway
|
|
152
|
+
* project, so SEMANTIC ("cannot find module", "implicitly any") diagnostics are
|
|
153
|
+
* expected and must be ignored — only a genuine parse error (an unbalanced
|
|
154
|
+
* brace, a stray token) should reject the optimization.
|
|
155
|
+
*/
|
|
156
|
+
function hasSyntaxError(code) {
|
|
157
|
+
const project = new Project({
|
|
158
|
+
useInMemoryFileSystem: true,
|
|
159
|
+
compilerOptions: { allowJs: true },
|
|
160
|
+
});
|
|
161
|
+
const sf = project.createSourceFile('__candidate.ts', code, { overwrite: true });
|
|
162
|
+
return project.getProgram().getSyntacticDiagnostics(sf).length > 0;
|
|
163
|
+
}
|
|
142
164
|
function candidatePathFor(devRoot, slug) {
|
|
143
165
|
return join(devRoot, '__vibe_tests__', '.hover', 'optimized', `${slug}.spec.ts.draft`);
|
|
144
166
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** A trailing run with fewer than this many assertions is left alone —
|
|
2
|
+
* `expect.soft` only earns its keep when ≥2 failures could be collected. */
|
|
3
|
+
export declare const MIN_RUN = 2;
|
|
4
|
+
export interface SoftBatchResult {
|
|
5
|
+
/** The (possibly) rewritten source. */
|
|
6
|
+
code: string;
|
|
7
|
+
/** Whether anything changed. */
|
|
8
|
+
changed: boolean;
|
|
9
|
+
/** How many assertions were softened across all tests. */
|
|
10
|
+
softened: number;
|
|
11
|
+
}
|
|
12
|
+
/** Run the soft-batch step over a spec's source text. Pure: text in, text out. */
|
|
13
|
+
export declare function softBatch(source: string): SoftBatchResult;
|
|
14
|
+
//# sourceMappingURL=softBatch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"softBatch.d.ts","sourceRoot":"","sources":["../../src/specs/softBatch.ts"],"names":[],"mappings":"AAgCA;6EAC6E;AAC7E,eAAO,MAAM,OAAO,IAAI,CAAC;AAEzB,MAAM,WAAW,eAAe;IAC9B,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,kFAAkF;AAClF,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CAuBzD"}
|