@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.
Files changed (99) hide show
  1. package/README.md +73 -1
  2. package/dist/agents/aider.d.ts.map +1 -1
  3. package/dist/agents/aider.js +6 -14
  4. package/dist/agents/claude.d.ts.map +1 -1
  5. package/dist/agents/claude.js +14 -0
  6. package/dist/agents/codex.d.ts.map +1 -1
  7. package/dist/agents/codex.js +10 -4
  8. package/dist/agents/cursor.d.ts.map +1 -1
  9. package/dist/agents/cursor.js +8 -17
  10. package/dist/agents/gemini.d.ts.map +1 -1
  11. package/dist/agents/gemini.js +3 -14
  12. package/dist/agents/invoke.d.ts.map +1 -1
  13. package/dist/agents/invoke.js +10 -1
  14. package/dist/agents/qwen.d.ts.map +1 -1
  15. package/dist/agents/qwen.js +3 -14
  16. package/dist/agents/shared.d.ts +28 -0
  17. package/dist/agents/shared.d.ts.map +1 -0
  18. package/dist/agents/shared.js +35 -0
  19. package/dist/agents/types.d.ts +11 -0
  20. package/dist/agents/types.d.ts.map +1 -1
  21. package/dist/mcp/sourceFence.d.ts +23 -0
  22. package/dist/mcp/sourceFence.d.ts.map +1 -0
  23. package/dist/mcp/sourceFence.js +75 -0
  24. package/dist/mcp/sourceServer.d.ts +3 -0
  25. package/dist/mcp/sourceServer.d.ts.map +1 -0
  26. package/dist/mcp/sourceServer.js +116 -0
  27. package/dist/playwright/preflight.d.ts.map +1 -1
  28. package/dist/playwright/preflight.js +6 -1
  29. package/dist/playwright/raiseWindow.d.ts.map +1 -1
  30. package/dist/playwright/raiseWindow.js +22 -3
  31. package/dist/playwright/resolveMcpConfig.d.ts +11 -0
  32. package/dist/playwright/resolveMcpConfig.d.ts.map +1 -1
  33. package/dist/playwright/resolveMcpConfig.js +17 -3
  34. package/dist/plugin-api.d.ts +7 -0
  35. package/dist/plugin-api.d.ts.map +1 -1
  36. package/dist/runSession.d.ts +42 -0
  37. package/dist/runSession.d.ts.map +1 -0
  38. package/dist/runSession.js +81 -0
  39. package/dist/service/cdpHandlers.d.ts +3 -7
  40. package/dist/service/cdpHandlers.d.ts.map +1 -1
  41. package/dist/service/cdpHandlers.js +4 -16
  42. package/dist/service/cdpHint.d.ts.map +1 -1
  43. package/dist/service/cdpHint.js +30 -14
  44. package/dist/service/conventions.d.ts +8 -0
  45. package/dist/service/conventions.d.ts.map +1 -0
  46. package/dist/service/conventions.js +42 -0
  47. package/dist/service/saveHandlers.d.ts +10 -13
  48. package/dist/service/saveHandlers.d.ts.map +1 -1
  49. package/dist/service/saveHandlers.js +9 -25
  50. package/dist/service/types.d.ts +5 -0
  51. package/dist/service/types.d.ts.map +1 -1
  52. package/dist/service.d.ts +13 -4
  53. package/dist/service.d.ts.map +1 -1
  54. package/dist/service.js +264 -148
  55. package/dist/skills/writeSkill.d.ts +12 -35
  56. package/dist/skills/writeSkill.d.ts.map +1 -1
  57. package/dist/skills/writeSkill.js +10 -166
  58. package/dist/specs/detectSharedFlows.d.ts +35 -0
  59. package/dist/specs/detectSharedFlows.d.ts.map +1 -0
  60. package/dist/specs/detectSharedFlows.js +171 -0
  61. package/dist/specs/extractPageObjects.d.ts +18 -0
  62. package/dist/specs/extractPageObjects.d.ts.map +1 -0
  63. package/dist/specs/extractPageObjects.js +98 -0
  64. package/dist/specs/generatePageObject.d.ts +29 -0
  65. package/dist/specs/generatePageObject.d.ts.map +1 -0
  66. package/dist/specs/generatePageObject.js +149 -0
  67. package/dist/specs/listSpecs.d.ts +12 -0
  68. package/dist/specs/listSpecs.d.ts.map +1 -1
  69. package/dist/specs/listSpecs.js +27 -2
  70. package/dist/specs/optimizationSuggestion.d.ts +26 -0
  71. package/dist/specs/optimizationSuggestion.d.ts.map +1 -0
  72. package/dist/specs/optimizationSuggestion.js +28 -0
  73. package/dist/specs/optimizeSpec.d.ts +42 -0
  74. package/dist/specs/optimizeSpec.d.ts.map +1 -0
  75. package/dist/specs/optimizeSpec.js +188 -0
  76. package/dist/specs/optimizeSpecWithAgent.d.ts +11 -0
  77. package/dist/specs/optimizeSpecWithAgent.d.ts.map +1 -0
  78. package/dist/specs/optimizeSpecWithAgent.js +40 -0
  79. package/dist/specs/pageObjectManifest.d.ts +20 -0
  80. package/dist/specs/pageObjectManifest.d.ts.map +1 -0
  81. package/dist/specs/pageObjectManifest.js +40 -0
  82. package/dist/specs/seeds.d.ts +36 -0
  83. package/dist/specs/seeds.d.ts.map +1 -0
  84. package/dist/specs/seeds.js +74 -0
  85. package/dist/specs/sidecar.d.ts +25 -0
  86. package/dist/specs/sidecar.d.ts.map +1 -0
  87. package/dist/specs/sidecar.js +38 -0
  88. package/dist/specs/softBatch.d.ts +14 -0
  89. package/dist/specs/softBatch.d.ts.map +1 -0
  90. package/dist/specs/softBatch.js +177 -0
  91. package/dist/specs/text.d.ts +17 -0
  92. package/dist/specs/text.d.ts.map +1 -0
  93. package/dist/specs/text.js +24 -0
  94. package/dist/specs/writeCaseCsv.d.ts.map +1 -1
  95. package/dist/specs/writeCaseCsv.js +2 -8
  96. package/dist/specs/writeSpec.d.ts +50 -0
  97. package/dist/specs/writeSpec.d.ts.map +1 -1
  98. package/dist/specs/writeSpec.js +251 -84
  99. package/package.json +5 -3
@@ -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,CAiD7B"}
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,CAwClE"}
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
- // We try wmctrl with the PID match; if it isn't installed the
81
- // outer try/catch swallows the ENOENT and we degrade gracefully.
82
- await runDetached('wmctrl', ['-ia', String(pid)]);
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;
@@ -40,5 +46,10 @@ export declare function resolveMcpConfig(opts: {
40
46
  /** Suffix for the output filename so multiple parallel configs from
41
47
  * the same service (e.g. mode toggle round-trips) don't share state. */
42
48
  suffix?: string;
49
+ /** Project root to resolve `@playwright/mcp` from. Defaults to
50
+ * `process.cwd()`. `hover run --cwd apps/web` passes the target workspace
51
+ * so a monorepo that installed `@hover-dev/core` only under that app (not
52
+ * the repo root the CLI was invoked from) still resolves the MCP package. */
53
+ cwd?: string;
43
54
  }): string;
44
55
  //# sourceMappingURL=resolveMcpConfig.d.ts.map
@@ -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;CACjB,GAAG,MAAM,CAgDT"}
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"}
@@ -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
@@ -19,7 +27,8 @@ export function resolveMcpConfig(opts) {
19
27
  // can't actually load. `process.cwd()` is the user's project root,
20
28
  // and `@playwright/mcp` is always reachable from there because it's
21
29
  // a declared dependency of `@hover-dev/core`, which the user installed.
22
- const require = createRequire(resolve(process.cwd(), 'package.json'));
30
+ // The caller may override with an explicit `cwd` (e.g. `hover run --cwd`).
31
+ const require = createRequire(resolve(opts.cwd ?? process.cwd(), 'package.json'));
23
32
  const pkgJsonPath = require.resolve('@playwright/mcp/package.json');
24
33
  const pkgRoot = dirname(pkgJsonPath);
25
34
  // The package's `bin` map declares "playwright-mcp": "cli.js" — we
@@ -45,8 +54,13 @@ export function resolveMcpConfig(opts) {
45
54
  const config = { mcpServers };
46
55
  const outDir = resolve(tmpdir(), 'hover');
47
56
  mkdirSync(outDir, { recursive: true });
48
- const suffix = opts.suffix ? `-${opts.suffix}` : '';
49
- const outPath = resolve(outDir, `mcp-config-${opts.port}${suffix}.json`);
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`);
50
64
  writeFileSync(outPath, JSON.stringify(config, null, 2), 'utf-8');
51
65
  return outPath;
52
66
  }
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"plugin-api.d.ts","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"plugin-api.d.ts","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import type { InvokeEvent } from './agents/types.js';
2
+ import type { SkillStep } from './skills/writeSkill.js';
3
+ export interface RunSessionOptions {
4
+ prompt: string;
5
+ agentId: string;
6
+ /** CDP URL of the debug Chrome the agent drives. Required unless `mcpConfig`
7
+ * is supplied (the service passes a pre-built config; the CLI passes this). */
8
+ cdpUrl?: string;
9
+ model?: string;
10
+ maxBudgetUsd?: number;
11
+ /** Optional model API key, injected into the spawned CLI's env. */
12
+ apiKey?: string;
13
+ /** Agent cwd (project root) — where Claude Code reads CLAUDE.md and where a
14
+ * `--save` / re-record writes the spec. Defaults to the process cwd. */
15
+ cwd?: string;
16
+ /** Namespaces the temp MCP config filename. Defaults to 51789. */
17
+ port?: number;
18
+ signal?: AbortSignal;
19
+ /** Pre-built MCP config path. The service supplies one (with plugin servers);
20
+ * when omitted, runSession builds a plugin-free Playwright config from
21
+ * `cdpUrl` via resolveMcpConfig. */
22
+ mcpConfig?: string;
23
+ /** Extra hard-sandbox allow-list prefixes — e.g. active-mode plugin MCP
24
+ * server ids the service contributes. Appended to ['mcp__playwright']. */
25
+ allowedToolsExtra?: string[];
26
+ /** Appended to the agent's system prompt (the service folds in cdpHint +
27
+ * conventions + plugin additions + a language directive; the CLI omits it). */
28
+ appendSystemPrompt?: string;
29
+ /** Resume an existing agent session (a follow-up turn). */
30
+ sessionId?: string;
31
+ }
32
+ export interface RunSessionResult {
33
+ /** Captured session as SpecStep[] (`user` → `step`* → `done`), ready to hand
34
+ * straight to `writeSpec`. */
35
+ steps: SkillStep[];
36
+ /** The agent's final summary, if any. */
37
+ summary: string;
38
+ /** True if the run ended in error or was aborted. */
39
+ isError: boolean;
40
+ }
41
+ export declare function runSession(opts: RunSessionOptions, onEvent: (ev: InvokeEvent) => void): Promise<RunSessionResult>;
42
+ //# sourceMappingURL=runSession.d.ts.map
@@ -0,0 +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,CA4D3B"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Headless session runner — the invoke + crystallize engine shared by every
3
+ * frontend. The widget reaches it through the WebSocket service; `smoke.ts`
4
+ * and (future) `hover run` call it in-process, no WS server. It spawns the
5
+ * agent against the user's debug Chrome over CDP, streams normalized events to
6
+ * `onEvent`, and accumulates the captured tool calls into a `SpecStep[]` the
7
+ * caller can hand to `writeSpec` — `user` seed → `step` per tool_use → `done`
8
+ * with the final summary (the exact shape the spec pipeline consumes).
9
+ *
10
+ * No WebSocket, no DOM. It drives an *already-running* debug Chrome over CDP;
11
+ * launching Chrome / CDP preflight is the caller's call (the service does it
12
+ * with autoLaunch; the CLI will too). The sandbox (allow/deny tools) mirrors
13
+ * the service exactly, gated on the agent's `sandboxStrength`.
14
+ *
15
+ * The full surface (mcpConfig override, allowedToolsExtra, appendSystemPrompt,
16
+ * sessionId) lets the service delegate to this instead of duplicating the
17
+ * invoke loop; the CLI uses only the small subset (prompt + cdpUrl + model).
18
+ */
19
+ import { invokeAgent } from './agents/invoke.js';
20
+ import { getAgent } from './agents/registry.js';
21
+ import { resolveMcpConfig } from './playwright/resolveMcpConfig.js';
22
+ export async function runSession(opts, onEvent) {
23
+ const descriptor = getAgent(opts.agentId);
24
+ const isHardSandbox = descriptor?.sandboxStrength === 'hard';
25
+ // Seed with a synthetic `user` step so writeSpec's JSDoc `Original prompt:`
26
+ // line carries the prompt the agent was given (mirrors the service path).
27
+ const steps = [{ kind: 'user', text: opts.prompt }];
28
+ let summary = '';
29
+ let isError = false;
30
+ const mcpConfig = opts.mcpConfig ??
31
+ resolveMcpConfig({
32
+ cdpUrl: opts.cdpUrl ?? 'http://localhost:9222',
33
+ port: opts.port ?? 51789,
34
+ // Resolve @playwright/mcp from the run's cwd, not the dir the CLI was
35
+ // invoked from — `hover run --cwd apps/web` must find the MCP package
36
+ // under the target workspace in a monorepo.
37
+ cwd: opts.cwd,
38
+ });
39
+ for await (const ev of invokeAgent({
40
+ agentId: opts.agentId,
41
+ prompt: opts.prompt,
42
+ sessionId: opts.sessionId,
43
+ mcpConfig,
44
+ cwd: opts.cwd,
45
+ appendSystemPrompt: opts.appendSystemPrompt,
46
+ // Hard sandbox: only Playwright MCP (+ any active-mode plugin servers) is
47
+ // callable, every built-in tool denied — a hijacked prompt can't reach the
48
+ // shell or filesystem. Soft agents (codex, …) enforce their own sandbox via
49
+ // buildArgs, so the lists stay undefined for them — exactly what the
50
+ // service does.
51
+ allowedTools: isHardSandbox
52
+ ? ['mcp__playwright', ...(opts.allowedToolsExtra ?? [])]
53
+ : undefined,
54
+ disallowedTools: isHardSandbox
55
+ ? (descriptor?.defaultDisallowedTools ? [...descriptor.defaultDisallowedTools] : undefined)
56
+ : undefined,
57
+ maxBudgetUsd: opts.maxBudgetUsd,
58
+ model: opts.model,
59
+ apiKey: opts.apiKey,
60
+ signal: opts.signal,
61
+ })) {
62
+ onEvent(ev);
63
+ if (ev.kind === 'tool_use') {
64
+ steps.push({ kind: 'step', tool: ev.tool, input: ev.input });
65
+ }
66
+ else if (ev.kind === 'session_end') {
67
+ if (ev.summary)
68
+ summary = ev.summary;
69
+ if (ev.isError)
70
+ isError = true;
71
+ }
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;
78
+ if (summary)
79
+ steps.push({ kind: 'done', summary });
80
+ return { steps, summary, isError };
81
+ }
@@ -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 + separate profile + non-default CDP port). When
18
- * none are set, behaviour is identical to pre-v0.7 normal-mode launch. */
19
- export type LaunchExtras = Pick<LaunchOptions, 'userDataDir' | 'proxy'> & {
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;;2EAE2E;AAC3E,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,EAAE,aAAa,GAAG,OAAO,CAAC,GAAG;IACxE;6DACyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;;;;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,CAWf;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,CAkCf;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,CAaf"}
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 effectiveCdpUrl = extras?.cdpPort
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 = extras?.cdpPort ?? (() => {
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
- // Re-check status against the port we actually launched on, so a mode-
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 effectiveCdpUrl = extras?.cdpPort
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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cdpHint.d.ts","sourceRoot":"","sources":["../../src/service/cdpHint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,UAAU,GAAG;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE;AAa7C,wBAAgB,YAAY,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,CAmIhD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,CAYtD"}
1
+ {"version":3,"file":"cdpHint.d.ts","sourceRoot":"","sources":["../../src/service/cdpHint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,UAAU,GAAG;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE;AAa7C,wBAAgB,YAAY,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,CAmJhD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,CAYtD"}
@@ -45,17 +45,24 @@ export function buildCdpHint(tabs) {
45
45
  return [
46
46
  `Your job — read this first:`,
47
47
  ``,
48
- ` You are an end-to-end testing agent. Your standing mission is to drive`,
49
- ` the user's web app through the browser, EXERCISE its interactive`,
50
- ` surface, and report bugs or unexpected behaviour.`,
51
- ``,
52
- ` If the user's prompt is specific ("log in as alice and add a todo"),`,
53
- ` do that and verify the outcome.`,
54
- ``,
55
- ` If the user's prompt is vague or short ("test", "check", "see if it`,
56
- ` works", "find bugs", or a single word), DO NOT ask for clarification`,
57
- ` and DO NOT just take a snapshot and call it done. Run a real`,
58
- ` exploratory test pass:`,
48
+ ` You are an end-to-end testing agent. Match the scope of your run to how`,
49
+ ` specific the user's prompt is do NOT over-test.`,
50
+ ``,
51
+ ` SPECIFIC prompt — it names a flow or action ("log in as alice and add a`,
52
+ ` todo", "test the login flow", "只测试登录"): do EXACTLY that flow and`,
53
+ ` verify its outcome, then STOP. Stay inside the named scope. Do NOT wander`,
54
+ ` into adjacent flows, extra edge cases (empty/invalid input, boundary`,
55
+ ` values), logout, or bug-hunting unless the prompt explicitly asks. A`,
56
+ ` focused run that does what was asked and asserts the result is the goal,`,
57
+ ` not breadth one clean verified flow is a complete, successful result.`,
58
+ ` But if you DO hit a real problem while doing the asked flow — a broken`,
59
+ ` button, a wrong message, a console error, a failed verification — still`,
60
+ ` report it under ## Findings. Don't go hunting for more; just don't swallow`,
61
+ ` what you ran into.`,
62
+ ``,
63
+ ` VAGUE or short prompt ("test", "check", "see if it works", "find bugs",`,
64
+ ` or a single word): DO NOT ask for clarification and DO NOT just take a`,
65
+ ` snapshot and call it done. Run a real exploratory test pass:`,
59
66
  ``,
60
67
  ` 1. browser_snapshot to learn the app's structure.`,
61
68
  ` 2. Identify the main interactive surfaces (forms, buttons, links,`,
@@ -68,9 +75,8 @@ export function buildCdpHint(tabs) {
68
75
  ` confusing in the final summary's "## Findings" section.`,
69
76
  ``,
70
77
  ` A short "App is running fine" reply after one snapshot is NOT an`,
71
- ` acceptable result. Either the app actually works and you ran several`,
72
- ` flows to confirm it, or you found something interesting — those are`,
73
- ` the only two valid outcomes of a vague prompt.`,
78
+ ` acceptable result for a vague prompt — either the app works and you ran`,
79
+ ` several flows to confirm it, or you found something interesting.`,
74
80
  ``,
75
81
  `The user's Chrome currently has these tabs open:`,
76
82
  ...tabs.map(t => ` - ${t.url}${t.title ? ` (${t.title})` : ''}`),
@@ -140,6 +146,16 @@ export function buildCdpHint(tabs) {
140
146
  ` postMessage listener or auth refresh"); do NOT browser_navigate to`,
141
147
  ` same-origin to force a refresh (rule #2 still applies).`,
142
148
  ``,
149
+ `Tool usage — operate and verify through the structured Playwright tools:`,
150
+ ``,
151
+ ` 8. Drive the page only with click / fill / select / snapshot / wait. Do`,
152
+ ` NOT use browser_run_code_unsafe or browser_evaluate to run JavaScript`,
153
+ ` — they are disabled, and any action taken in raw JS cannot be`,
154
+ ` crystallized into a deterministic Playwright spec (it is dropped as a`,
155
+ ` TODO). To VERIFY an outcome, assert on what browser_snapshot shows —`,
156
+ ` a heading, an error message, a counter value; the accessibility tree`,
157
+ ` already exposes the text and roles you need.`,
158
+ ``,
143
159
  `Narration format — affects how the widget renders your run for the user:`,
144
160
  ``,
145
161
  ` Before each LOGICAL STEP (a coherent unit of work like "Open the login`,
@@ -0,0 +1,8 @@
1
+ /** Max characters of the conventions file folded into the prompt. */
2
+ export declare const CONVENTIONS_MAX_CHARS = 4000;
3
+ /**
4
+ * Read `<projectRoot>/.hover/conventions.md` and return it wrapped as a
5
+ * system-prompt block, or null when the file is absent or empty.
6
+ */
7
+ export declare function readConventions(projectRoot: string, maxChars?: number): Promise<string | null>;
8
+ //# sourceMappingURL=conventions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conventions.d.ts","sourceRoot":"","sources":["../../src/service/conventions.ts"],"names":[],"mappings":"AAgBA,qEAAqE;AACrE,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAE1C;;;GAGG;AACH,wBAAsB,eAAe,CACnC,WAAW,EAAE,MAAM,EACnB,QAAQ,SAAwB,GAC/B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAqBxB"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Knowledge layer (F5): the project's testing conventions, injected into the
3
+ * agent's system prompt so the developer can steer *how it explores* — which
4
+ * flows matter, where login lives, the preferred selector attribute.
5
+ *
6
+ * Read by the SERVICE (not the agent) from `<projectRoot>/.hover/conventions.md`
7
+ * and folded into the system prompt — the agent never gains a file-read tool
8
+ * (D2). This shapes exploration only; it does NOT change how the saved spec is
9
+ * generated (that's the translator's job — D9).
10
+ *
11
+ * Capped to avoid prompt bloat, and injected on the FIRST turn only (it's
12
+ * static, like cdpHint's rules) so it doesn't fragment the prompt cache.
13
+ */
14
+ import { readFile } from 'node:fs/promises';
15
+ import { join } from 'node:path';
16
+ /** Max characters of the conventions file folded into the prompt. */
17
+ export const CONVENTIONS_MAX_CHARS = 4000;
18
+ /**
19
+ * Read `<projectRoot>/.hover/conventions.md` and return it wrapped as a
20
+ * system-prompt block, or null when the file is absent or empty.
21
+ */
22
+ export async function readConventions(projectRoot, maxChars = CONVENTIONS_MAX_CHARS) {
23
+ let raw;
24
+ try {
25
+ raw = await readFile(join(projectRoot, '.hover', 'conventions.md'), 'utf-8');
26
+ }
27
+ catch {
28
+ return null; // no conventions file — nothing to inject
29
+ }
30
+ const trimmed = raw.trim();
31
+ if (!trimmed)
32
+ return null;
33
+ const body = trimmed.length > maxChars ? `${trimmed.slice(0, maxChars)}\n…(truncated)` : trimmed;
34
+ return [
35
+ `Project testing conventions — the developer's house rules for this app,`,
36
+ `from .hover/conventions.md. Use them while EXPLORING (which flows matter,`,
37
+ `where login lives, preferred selectors, test data). They guide exploration`,
38
+ `only — they do not change how the saved spec is generated.`,
39
+ ``,
40
+ body,
41
+ ].join('\n');
42
+ }