@hover-dev/core 0.13.0 → 0.14.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.
@@ -34,7 +34,7 @@ export async function preflightCDP(cdpUrl, timeoutMs = 2000) {
34
34
  catch (err) {
35
35
  return {
36
36
  ok: false,
37
- reason: `Chrome debug session not detected at ${cdpUrl}. Start it with: pnpm exec hover-chrome (or: npx hover-chrome)`,
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
38
  };
39
39
  }
40
40
  if (!versionRes.ok) {
@@ -33,6 +33,10 @@ export interface HoverPluginMode {
33
33
  label: string;
34
34
  /** One-liner help text shown in the dropdown. */
35
35
  description?: string;
36
+ /** Short status shown in the mode bar's right-hand hint slot while this
37
+ * mode is engaged. Defaults to "active" if omitted. Keep it terse — e.g.
38
+ * "MITM proxy active". */
39
+ engagedHint?: string;
36
40
  /** Mode ids this mode cannot be active alongside. Two plugins both
37
41
  * needing an exclusive proxy would set each other here. */
38
42
  conflictsWith?: string[];
@@ -113,10 +117,29 @@ export interface ModeActivateCtx extends HoverHookCtxBase {
113
117
  export interface ModeDeactivateCtx extends HoverHookCtxBase {
114
118
  modeId: string;
115
119
  }
120
+ /** Fired exactly once when the host service starts, BEFORE the debug Chrome
121
+ * is (auto-)launched. A plugin that needs Chrome to be born with specific
122
+ * flags — e.g. a resident MITM proxy that Chrome must point through from the
123
+ * first navigation — boots that sidecar here and calls setChromeProxy so the
124
+ * host bakes the flags into the single Chrome launch. This is what lets the
125
+ * security plugin run one always-on (transparent-by-default) proxy instead
126
+ * of launching a second Chrome on mode entry. */
127
+ export interface ServiceStartCtx extends HoverHookCtxBase {
128
+ /** Tell the host "the debug Chrome should be launched with these proxy
129
+ * settings". Set once here; persists for the whole session. */
130
+ setChromeProxy(proxy: {
131
+ port: number;
132
+ spki: string;
133
+ } | null): void;
134
+ /** Same as the activate-time variant — seed runtime env for a declared MCP
135
+ * server before it's spawned. */
136
+ setMcpServerEnv(id: string, env: Record<string, string>): void;
137
+ }
116
138
  /** Fired exactly once when the host service is shutting down for any
117
139
  * reason. Hooks must release subprocesses and file handles. */
118
140
  export type ShutdownCtx = HoverHookCtxBase;
119
141
  export interface HoverHooks {
142
+ 'hover:service:start'?: (ctx: ServiceStartCtx) => void | Promise<void>;
120
143
  'hover:mode:activate'?: (ctx: ModeActivateCtx) => void | Promise<void>;
121
144
  'hover:mode:deactivate'?: (ctx: ModeDeactivateCtx) => void | Promise<void>;
122
145
  'hover:service:shutdown'?: (ctx: ShutdownCtx) => void | Promise<void>;
@@ -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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"}
package/dist/service.d.ts CHANGED
@@ -18,6 +18,18 @@ export interface ServiceOptions {
18
18
  * pre-plugin Hover" — important for the long tail of users who never
19
19
  * install one. */
20
20
  plugins?: HoverPluginManifest[];
21
+ /** When true, the service launches the single debug Chrome itself at
22
+ * startup — AFTER firing plugin `hover:service:start` hooks, so a plugin
23
+ * that set a resident proxy (e.g. security's MITM) has its flags baked
24
+ * into that one Chrome. Previously each bundler shim called
25
+ * launchDebugChrome() directly, which bypassed the service and so couldn't
26
+ * see the proxy; moving it here is what enables the single-Chrome model.
27
+ * Default false (shims pass it through from their own option). */
28
+ autoLaunchChrome?: boolean;
29
+ /** The dev-server URL the auto-launched Chrome should open. Each shim knows
30
+ * its own framework's dev URL and passes it here. Defaults to the cdp host
31
+ * if unset, but shims should always provide it. */
32
+ devUrl?: string;
21
33
  }
22
34
  export interface ServiceHandle {
23
35
  /** The port the WebSocketServer actually bound to. May differ from
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AA6EA,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,iBAAiB,CAAC;AAEzB,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,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;6EAGyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;uBAImB;IACnB,OAAO,CAAC,EAAE,mBAAmB,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,aAAa;IAC5B;4EACwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAiDD,wBAAsB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAqyB/E"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AA8EA,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,iBAAiB,CAAC;AAEzB,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,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;6EAGyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;uBAImB;IACnB,OAAO,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAChC;;;;;;uEAMmE;IACnE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;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,CAs2B/E"}
package/dist/service.js CHANGED
@@ -54,6 +54,7 @@ import { listAgentAvailability, pickPrimaryAgent, } from './agents/detect.js';
54
54
  import { getAgent } from './agents/registry.js';
55
55
  import { getPreflight, invalidatePreflight } from './playwright/preflightCache.js';
56
56
  import { resolveMcpConfig } from './playwright/resolveMcpConfig.js';
57
+ import { launchDebugChrome } from './playwright/launchChrome.js';
57
58
  import { listSkills } from './skills/writeSkill.js';
58
59
  import { listSpecs } from './specs/listSpecs.js';
59
60
  import { send, sendIfOpen } from './service/types.js';
@@ -65,6 +66,19 @@ import { CURRENT_API_VERSION, } from './plugin-api.js';
65
66
  // handler modules can share them. See those files for the wire shape.
66
67
  const PROTOCOL_VERSION = 1;
67
68
  const PORT_RETRIES = 10;
69
+ /** CJK-presence test — mirrors voice.js's detectLanguage. Any Han character
70
+ * in the prompt flips the agent's prose output to Chinese. */
71
+ const CJK_RE = /[一-鿿]/;
72
+ /** Appended to the agent's system prompt when the user's prompt contains CJK,
73
+ * so the human-facing prose (verification summary / ## Findings / step
74
+ * narration) comes back in Chinese — matching how Voice mode picks a Chinese
75
+ * TTS voice for the same prompt. Deliberately scoped to PROSE only: the agent
76
+ * must still use the page's real (often English) accessible names, labels,
77
+ * and selectors when driving the browser. */
78
+ const ZH_OUTPUT_DIRECTIVE = '用户使用中文下达指令。请用简体中文撰写所有面向用户的文字输出:验证结论摘要、' +
79
+ '`## Findings` 区块(bug / 问题 / 备注)、以及每一步的中文描述。' +
80
+ '注意:这只影响你写给用户看的文字。操作浏览器时仍要使用页面真实的(通常是英文的)' +
81
+ '角色名、标签、可访问名称和选择器——不要把它们翻译成中文。';
68
82
  /**
69
83
  * Try to bind a WebSocketServer to <host>:<port>. Resolves with the wss on
70
84
  * success; rejects with the bind error (typically EADDRINUSE) on failure.
@@ -175,15 +189,11 @@ export async function startService(opts) {
175
189
  }
176
190
  }
177
191
  }
178
- // In an active mode, the Playwright MCP must point at THAT mode's
179
- // Chrome (e.g. security mode's 9333), not the default 9222.
180
- // effectiveLaunchExtras().cdpPort is the source of truth.
181
- const extras = effectiveLaunchExtras();
182
- const effectiveCdpUrl = extras?.cdpPort
183
- ? `http://localhost:${extras.cdpPort}`
184
- : cdpUrl;
192
+ // Single-Chrome model: the Playwright MCP always points at the one debug
193
+ // Chrome on the normal cdpUrl. (Pre-single-Chrome this branched to a
194
+ // mode-specific port like 9333; there's no second Chrome anymore.)
185
195
  return resolveMcpConfig({
186
- cdpUrl: effectiveCdpUrl,
196
+ cdpUrl,
187
197
  port,
188
198
  extra,
189
199
  // Suffix the filename by the mode so different mode toggles within
@@ -223,44 +233,31 @@ export async function startService(opts) {
223
233
  }
224
234
  /** id of the currently-active mode, or null for normal (unmoded) mode. */
225
235
  let currentModeId = null;
226
- /** Chrome-proxy settings the active mode's activate hook set on us.
227
- * Read by `effectiveLaunchExtras()` and threaded into the cdp handlers
228
- * (check-cdp / launch-chrome / focus-debug) so the secured Chrome on
229
- * 9333 actually gets `--proxy-server` + SPKI pin when the user clicks
230
- * Launch from the widget. */
231
- let modeChromeProxy = null;
236
+ /** Chrome-proxy settings a plugin's `hover:service:start` hook set on us
237
+ * (security's resident MITM). RESIDENT for the whole session — set once
238
+ * before Chrome launches, never cleared on mode change — so the single
239
+ * debug Chrome is born with `--proxy-server` + the SPKI pin and entering
240
+ * Security mode is just a runtime flip of the proxy, not a Chrome relaunch.
241
+ * Read by `effectiveLaunchExtras()` and threaded into every cdp handler
242
+ * (check-cdp / launch-chrome / focus-debug) plus the initial auto-launch. */
243
+ let residentChromeProxy = null;
232
244
  /** Runtime env overrides keyed by mcpServer id, set by plugin
233
245
  * activate hooks (via ctx.setMcpServerEnv). Cleared on mode change.
234
246
  * Merged with the manifest-declared env when the agent's spawn-time
235
247
  * MCP config is built. */
236
248
  const mcpEnvOverrides = new Map();
237
- /** The cdp-handler extras (port, userDataDir, proxy) for the active
238
- * mode's chromeFlags manifest field, or undefined when no mode is
239
- * active. The widget's launch-chrome / check-cdp / focus-debug paths
240
- * all consume these so a Chrome relaunch obeys the mode's needs. */
249
+ /** The cdp-handler extras (proxy) threaded into launch-chrome / check-cdp /
250
+ * focus-debug and the initial auto-launch. In the single-Chrome model this
251
+ * is driven purely by the RESIDENT proxy (set in `hover:service:start`),
252
+ * NOT by the active mode — there is one Chrome on the normal CDP port that
253
+ * is always proxied; entering Security mode flips the proxy's behaviour,
254
+ * it does not relaunch Chrome on a different port. Returns undefined when
255
+ * no plugin set a resident proxy (the common no-security case), so plain
256
+ * Hover is byte-for-byte unchanged. */
241
257
  const effectiveLaunchExtras = () => {
242
- if (!currentModeId)
258
+ if (!residentChromeProxy)
243
259
  return undefined;
244
- const plugin = pluginsByModeId.get(currentModeId);
245
- const flags = plugin?.chromeFlags;
246
- if (!flags && !modeChromeProxy)
247
- return undefined;
248
- // Belt + suspenders — flags.activeInModes is honoured if set, but
249
- // since chromeFlags lives on the plugin that contributed this mode,
250
- // the default of "applies in own mode" matches what we want.
251
- if (flags?.activeInModes && !flags.activeInModes.includes('*') && !flags.activeInModes.includes(currentModeId)) {
252
- // Plugin explicitly restricted its chromeFlags to a different mode.
253
- // Honour that and only carry modeChromeProxy (set by setChromeProxy).
254
- return modeChromeProxy ? { proxy: modeChromeProxy } : undefined;
255
- }
256
- return {
257
- cdpPort: flags?.cdpPort,
258
- userDataDir: flags?.userDataDir,
259
- // modeChromeProxy wins over flags.proxy because it's the runtime
260
- // value the activate hook computed (after starting mockttp);
261
- // flags.proxy is only ever set by tests stubbing the manifest.
262
- proxy: modeChromeProxy ?? flags?.proxy,
263
- };
260
+ return { proxy: residentChromeProxy };
264
261
  };
265
262
  /** Send the current mode catalogue to one ws (or all if undefined). */
266
263
  const broadcastModes = (target) => {
@@ -309,8 +306,14 @@ export async function startService(opts) {
309
306
  }
310
307
  }
311
308
  }
312
- modeChromeProxy = null;
313
- mcpEnvOverrides.clear();
309
+ // NOTE: neither residentChromeProxy NOR mcpEnvOverrides is cleared here.
310
+ // In the single-Chrome model both are RESIDENT — set once in
311
+ // service:start (e.g. security's HOVER_SECURITY_API base + token), they
312
+ // must survive every mode toggle so the agent's spawned MCP server can
313
+ // always reach the control plane. Clearing them on mode change was the
314
+ // pre-resident behaviour and would leave the security MCP server with no
315
+ // env → it exits with "failed". Mode changes now only flip plugin runtime
316
+ // state via the plugin's own activate/deactivate hooks.
314
317
  currentModeId = null;
315
318
  // Bring up new mode
316
319
  if (newModeId) {
@@ -325,7 +328,10 @@ export async function startService(opts) {
325
328
  broadcast: broadcastPluginEvent,
326
329
  modeId: newModeId,
327
330
  setChromeProxy(proxy) {
328
- modeChromeProxy = proxy;
331
+ // Retained for API compatibility. In the single-Chrome model the
332
+ // proxy is normally set once in service:start; if an activate hook
333
+ // still calls this, treat it as updating the resident proxy.
334
+ residentChromeProxy = proxy;
329
335
  },
330
336
  setMcpServerEnv(id, env) {
331
337
  mcpEnvOverrides.set(id, env);
@@ -339,9 +345,10 @@ export async function startService(opts) {
339
345
  // pretend to be in `newModeId` with no sidecars running.
340
346
  // Widget still trusts the broadcast below to learn we're back
341
347
  // to default. The error is rethrown so the caller can surface
342
- // it to the user.
343
- modeChromeProxy = null;
344
- mcpEnvOverrides.clear();
348
+ // it to the user. residentChromeProxy and mcpEnvOverrides are NOT
349
+ // touched — both are owned by service:start, independent of mode
350
+ // activation (clearing the env would break the resident security
351
+ // MCP server).
345
352
  currentModeId = null;
346
353
  broadcastModes();
347
354
  throw err;
@@ -694,6 +701,17 @@ export async function startService(opts) {
694
701
  }
695
702
  }
696
703
  }
704
+ // Mirror the prompt's language in the agent's *prose* output — the
705
+ // verification summary (Result card), the ## Findings block, and the
706
+ // step narration — the same way Voice mode mirrors it in TTS. A
707
+ // Chinese prompt should produce a Chinese report. This does NOT change
708
+ // how the agent operates the browser: selectors, role names, and the
709
+ // app's own (often English) UI text are unaffected — only the agent's
710
+ // human-facing writing follows the user. Detection mirrors voice.js's
711
+ // detectLanguage (CJK presence → zh).
712
+ if (CJK_RE.test(text)) {
713
+ appendSystemPrompt = `${appendSystemPrompt}\n\n${ZH_OUTPUT_DIRECTIVE}`;
714
+ }
697
715
  // Snapshot the agent id so a switch-agent message during the run
698
716
  // can't smear two agents across one invocation. (We also gate
699
717
  // switch-agent on `busy`, but defense in depth.)
@@ -844,6 +862,58 @@ export async function startService(opts) {
844
862
  }
845
863
  });
846
864
  });
865
+ // ───────────────────────── service:start + single Chrome ─────────────────
866
+ // Fire plugin `hover:service:start` hooks BEFORE launching Chrome, so a
867
+ // plugin (security) can boot its resident proxy and call setChromeProxy.
868
+ // residentChromeProxy is then baked into the one auto-launched Chrome.
869
+ for (const p of plugins) {
870
+ const hook = p.hooks?.['hover:service:start'];
871
+ if (!hook)
872
+ continue;
873
+ try {
874
+ await hook({
875
+ devRoot,
876
+ broadcast: broadcastPluginEvent,
877
+ setChromeProxy(proxy) {
878
+ residentChromeProxy = proxy;
879
+ },
880
+ setMcpServerEnv(id, env) {
881
+ mcpEnvOverrides.set(id, env);
882
+ },
883
+ });
884
+ }
885
+ catch (err) {
886
+ process.stderr.write(`[hover] plugin "${p.name}" service:start failed: ${err instanceof Error ? err.message : String(err)}\n`);
887
+ }
888
+ }
889
+ // Auto-launch the single debug Chrome here (moved out of the bundler shims
890
+ // so it happens AFTER service:start and can carry residentChromeProxy).
891
+ // Fire-and-forget — startup must not block on Chrome, and a launch failure
892
+ // is non-fatal (the widget's amber ✨ lets the user retry on demand).
893
+ if (opts.autoLaunchChrome) {
894
+ const launchPort = (() => {
895
+ try {
896
+ return Number(new URL(cdpUrl).port) || 9222;
897
+ }
898
+ catch {
899
+ return 9222;
900
+ }
901
+ })();
902
+ const launchUrl = opts.devUrl ?? cdpUrl;
903
+ launchDebugChrome({
904
+ url: launchUrl,
905
+ port: launchPort,
906
+ proxy: residentChromeProxy ?? undefined,
907
+ })
908
+ .then((r) => {
909
+ if (!r.ok) {
910
+ process.stderr.write(`[hover] auto-launch Chrome failed: ${r.reason}\n`);
911
+ }
912
+ })
913
+ .catch((err) => {
914
+ process.stderr.write(`[hover] auto-launch Chrome error: ${err instanceof Error ? err.message : String(err)}\n`);
915
+ });
916
+ }
847
917
  return {
848
918
  port,
849
919
  async close() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hover-dev/core",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Hover's local Node service: agent invocation, Playwright CDP preflight, WebSocket bridge.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hyperyond",