@hover-dev/core 0.7.0 → 0.7.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../src/agents/claude.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAA2C,MAAM,YAAY,CAAC;AAsF3F,eAAO,MAAM,WAAW,EAAE,eAyHzB,CAAC"}
1
+ {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../src/agents/claude.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAA2C,MAAM,YAAY,CAAC;AA6G3F,eAAO,MAAM,WAAW,EAAE,eA0HzB,CAAC"}
@@ -39,12 +39,35 @@ function claudeState(state) {
39
39
  }
40
40
  return state;
41
41
  }
42
+ /** Every built-in claude-code tool that has nothing to do with driving a
43
+ * browser. Combined with `--strict-mcp-config` + an allow-list of mcp__*
44
+ * ids, this leaves Claude with only the Playwright MCP (plus any
45
+ * plugin-contributed MCPs) as a usable tool surface. */
46
+ const CLAUDE_DEFAULT_DISALLOWED_TOOLS = [
47
+ // file / shell / data access — never appropriate for browser driving
48
+ 'Bash', 'BashOutput', 'KillBash',
49
+ 'Edit', 'MultiEdit', 'Write', 'Read', 'NotebookEdit',
50
+ 'Grep', 'Glob', 'Task', 'TodoWrite',
51
+ 'WebFetch', 'WebSearch',
52
+ // plan / worktree / cron / notification — irrelevant in -p mode
53
+ 'EnterPlanMode', 'ExitPlanMode',
54
+ 'EnterWorktree', 'ExitWorktree',
55
+ 'CronCreate', 'CronDelete', 'CronList',
56
+ 'PushNotification', 'RemoteTrigger',
57
+ // task & tool introspection added in claude 2.1.x — let through and
58
+ // the agent will burn turns exploring instead of executing
59
+ 'ToolSearch',
60
+ 'Monitor', 'TaskOutput', 'TaskStop',
61
+ 'AskUserQuestion',
62
+ 'ShareOnboardingGuide',
63
+ ];
42
64
  export const claudeAgent = {
43
65
  id: 'claude',
44
66
  binName: 'claude',
45
67
  protocol: 'argv',
46
68
  streamFormat: 'stream-json',
47
69
  sandboxStrength: 'hard',
70
+ defaultDisallowedTools: CLAUDE_DEFAULT_DISALLOWED_TOOLS,
48
71
  display: {
49
72
  label: 'Claude Code',
50
73
  tagline: 'Anthropic — best-in-class browser driving, hard tool sandbox',
@@ -146,6 +146,11 @@ export interface AgentDescriptor {
146
146
  streamFormat: StreamFormat;
147
147
  sandboxStrength: SandboxStrength;
148
148
  display: AgentDisplay;
149
+ /** Hard-sandbox agents pass this list to `disallowedTools` when the
150
+ * service-level allow/deny config isn't explicitly overridden. Lets the
151
+ * per-CLI deny list live alongside its descriptor instead of as a magic
152
+ * array in the service. Soft-sandbox agents leave this undefined. */
153
+ defaultDisallowedTools?: readonly string[];
149
154
  buildArgs(opts: InvokeOptions): string[];
150
155
  /**
151
156
  * Parse a single line of agent stdout into normalised InvokeEvents.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/agents/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/agents/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,aAAa,GACrB,MAAM,GACN,OAAO,GACP,KAAK,GACL,QAAQ,CAAC;AAEb,MAAM,MAAM,YAAY,GACpB,aAAa,GACb,KAAK,GACL,YAAY,GACZ,YAAY,CAAC;AAEjB,qBAAa,6BAA8B,SAAQ,KAAK;gBAC1C,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,sBAAuB,SAAQ,KAAK;aACnB,OAAO,EAAE,MAAM;gBAAf,OAAO,EAAE,MAAM;CAI5C;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;yCAGqC;IACrC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;6EACyE;IACzE,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5D;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5E;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5D;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AAChC;;;qEAGqE;GACnE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE;AACrD;;;;;;;;GAQG;GACD;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACnH;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAElC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,CAAC;AAE9C;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;IACd,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;4DACwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;mEAC+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAElD,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,aAAa,CAAC;IACxB,YAAY,EAAE,YAAY,CAAC;IAC3B,eAAe,EAAE,eAAe,CAAC;IACjC,OAAO,EAAE,YAAY,CAAC;IACtB;;;0EAGsE;IACtE,sBAAsB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,EAAE,CAAC;IACzC;;;;;;OAMG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,WAAW,EAAE,CAAC;IAC7D;;;;;;;;;OASG;IACH,WAAW,CAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,WAAW,GAAG,IAAI,CAAC;CAChF"}
@@ -103,22 +103,37 @@ export async function raiseChromeWindow(pid) {
103
103
  function runCapture(cmd, args) {
104
104
  return new Promise(resolve => {
105
105
  let out = '';
106
+ let settled = false;
107
+ const finish = (v) => {
108
+ if (settled)
109
+ return;
110
+ settled = true;
111
+ resolve(v);
112
+ };
106
113
  const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'ignore'] });
107
114
  child.stdout.on('data', chunk => {
108
115
  out += chunk.toString();
109
116
  });
110
- child.on('error', () => resolve(null));
111
- child.on('close', code => resolve(code === 0 ? out : null));
117
+ child.on('error', () => finish(null));
118
+ child.on('close', code => finish(code === 0 ? out : null));
112
119
  });
113
120
  }
114
121
  function runDetached(cmd, args) {
115
122
  return new Promise((resolve, reject) => {
123
+ let settled = false;
116
124
  const child = spawn(cmd, args, { stdio: 'ignore' });
117
- child.on('error', reject);
118
- child.on('close', code => {
125
+ child.on('error', err => {
126
+ if (settled)
127
+ return;
128
+ settled = true;
129
+ reject(err);
130
+ });
131
+ child.on('close', () => {
132
+ if (settled)
133
+ return;
134
+ settled = true;
119
135
  // Don't treat non-zero as fatal — caller already wraps in try/catch.
120
136
  resolve();
121
- void code;
122
137
  });
123
138
  });
124
139
  }
@@ -11,7 +11,16 @@
11
11
  * file can be a thin orchestrator.
12
12
  */
13
13
  import type { WebSocket } from 'ws';
14
+ import { type LaunchOptions } from '../playwright/launchChrome.js';
14
15
  import { type ClientMessage } from './types.js';
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
+ };
15
24
  /**
16
25
  * "Is this widget running inside the debug Chrome?" The widget asks this on
17
26
  * connect (and after every status-changing event) so it can render itself as
@@ -20,14 +29,14 @@ import { type ClientMessage } from './types.js';
20
29
  * - wrong-window → disabled, with a "use the other window" notice
21
30
  * - no-cdp → enabled but click triggers launch-chrome instead
22
31
  */
23
- export declare function handleCheckCdp(ws: WebSocket, msg: ClientMessage, cdpUrl: string): Promise<void>;
32
+ export declare function handleCheckCdp(ws: WebSocket, msg: ClientMessage, cdpUrl: string, extras?: LaunchExtras): Promise<void>;
24
33
  /**
25
34
  * Launch a debug Chrome navigated to `pageUrl`, then re-check status. The
26
35
  * re-check usually returns 'wrong-window' (because the widget asking is in
27
36
  * the user's regular Chrome, not the freshly-launched one) — the widget then
28
37
  * displays the "use the other window" state.
29
38
  */
30
- export declare function handleLaunchChrome(ws: WebSocket, msg: ClientMessage, cdpUrl: string): Promise<void>;
39
+ export declare function handleLaunchChrome(ws: WebSocket, msg: ClientMessage, cdpUrl: string, extras?: LaunchExtras): Promise<void>;
31
40
  /**
32
41
  * bringToFront the debug-Chrome tab matching `pageUrl`'s origin (or open one
33
42
  * if none exists). Used by the wrong-window UI's "switch to debug Chrome"
@@ -35,5 +44,5 @@ export declare function handleLaunchChrome(ws: WebSocket, msg: ClientMessage, cd
35
44
  * the widget cares about, and the widget the user is about to focus is a
36
45
  * different page (and will run its own check-cdp on its own ws connection).
37
46
  */
38
- export declare function handleFocusDebug(ws: WebSocket, msg: ClientMessage, cdpUrl: string): Promise<void>;
47
+ export declare function handleFocusDebug(ws: WebSocket, msg: ClientMessage, cdpUrl: string, extras?: LaunchExtras): Promise<void>;
39
48
  //# sourceMappingURL=cdpHandlers.d.ts.map
@@ -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;AAGpC,OAAO,EAAQ,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAEtD;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAUf"}
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"}
@@ -21,13 +21,16 @@ import { send } from './types.js';
21
21
  * - wrong-window → disabled, with a "use the other window" notice
22
22
  * - no-cdp → enabled but click triggers launch-chrome instead
23
23
  */
24
- export async function handleCheckCdp(ws, msg, cdpUrl) {
24
+ export async function handleCheckCdp(ws, msg, cdpUrl, extras) {
25
25
  const pageUrl = msg.payload?.pageUrl;
26
26
  if (typeof pageUrl !== 'string' || !pageUrl) {
27
27
  send(ws, { type: 'error', payload: { message: 'check-cdp: pageUrl is required' } });
28
28
  return;
29
29
  }
30
- const status = await checkCdpStatus(cdpUrl, pageUrl);
30
+ const effectiveCdpUrl = extras?.cdpPort
31
+ ? `http://localhost:${extras.cdpPort}`
32
+ : cdpUrl;
33
+ const status = await checkCdpStatus(effectiveCdpUrl, pageUrl);
31
34
  send(ws, { type: 'cdp-status', payload: status });
32
35
  }
33
36
  /**
@@ -36,7 +39,7 @@ export async function handleCheckCdp(ws, msg, cdpUrl) {
36
39
  * the user's regular Chrome, not the freshly-launched one) — the widget then
37
40
  * displays the "use the other window" state.
38
41
  */
39
- export async function handleLaunchChrome(ws, msg, cdpUrl) {
42
+ export async function handleLaunchChrome(ws, msg, cdpUrl, extras) {
40
43
  const pageUrl = msg.payload?.pageUrl;
41
44
  if (typeof pageUrl !== 'string' || !pageUrl) {
42
45
  send(ws, { type: 'error', payload: { message: 'launch-chrome: pageUrl is required' } });
@@ -45,7 +48,7 @@ export async function handleLaunchChrome(ws, msg, cdpUrl) {
45
48
  // Tell the widget we're launching so it can render a spinner immediately —
46
49
  // findChromeBinary + spawn + ready-poll can take a few seconds.
47
50
  send(ws, { type: 'cdp-status', payload: { state: 'no-cdp', launching: true } });
48
- const port = (() => {
51
+ const port = extras?.cdpPort ?? (() => {
49
52
  try {
50
53
  return Number(new URL(cdpUrl).port) || 9222;
51
54
  }
@@ -53,13 +56,22 @@ export async function handleLaunchChrome(ws, msg, cdpUrl) {
53
56
  return 9222;
54
57
  }
55
58
  })();
56
- const result = await launchDebugChrome({ url: pageUrl, port });
59
+ const result = await launchDebugChrome({
60
+ url: pageUrl,
61
+ port,
62
+ userDataDir: extras?.userDataDir,
63
+ proxy: extras?.proxy,
64
+ });
57
65
  if (!result.ok) {
58
66
  send(ws, { type: 'cdp-status', payload: { state: 'no-cdp', reason: result.reason } });
59
67
  return;
60
68
  }
61
- // Re-check after launch so the widget gets the real status.
62
- const status = await checkCdpStatus(cdpUrl, pageUrl);
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);
63
75
  send(ws, { type: 'cdp-status', payload: status });
64
76
  }
65
77
  /**
@@ -69,13 +81,16 @@ export async function handleLaunchChrome(ws, msg, cdpUrl) {
69
81
  * the widget cares about, and the widget the user is about to focus is a
70
82
  * different page (and will run its own check-cdp on its own ws connection).
71
83
  */
72
- export async function handleFocusDebug(ws, msg, cdpUrl) {
84
+ export async function handleFocusDebug(ws, msg, cdpUrl, extras) {
73
85
  const pageUrl = msg.payload?.pageUrl;
74
86
  if (typeof pageUrl !== 'string' || !pageUrl) {
75
87
  send(ws, { type: 'error', payload: { message: 'focus-debug: pageUrl is required' } });
76
88
  return;
77
89
  }
78
- const result = await focusDebugTab(cdpUrl, pageUrl);
90
+ const effectiveCdpUrl = extras?.cdpPort
91
+ ? `http://localhost:${extras.cdpPort}`
92
+ : cdpUrl;
93
+ const result = await focusDebugTab(effectiveCdpUrl, pageUrl);
79
94
  if (!result.ok) {
80
95
  send(ws, { type: 'error', payload: { message: `focus-debug: ${result.reason}` } });
81
96
  }
@@ -1 +1 @@
1
- {"version":3,"file":"saveHandlers.d.ts","sourceRoot":"","sources":["../../src/service/saveHandlers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,EAAE,UAAU,EAAgC,KAAK,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACnG,OAAO,EAAE,SAAS,EAAmB,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACvF,OAAO,EAAE,YAAY,EAAsB,MAAM,0BAA0B,CAAC;AAC5E,OAAO,EAAQ,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAEtD,UAAU,kBAAkB,CAAC,YAAY,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;IAC9E,qEAAqE;IACrE,WAAW,EAAE,MAAM,CAAC;IACpB,0BAA0B;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,UAAU,EAAE,MAAM,CAAC;IACnB;wDACoD;IACpD,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,0EAA0E;IAC1E,WAAW,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,KAAK,CAAC;IAC9E,wEAAwE;IACxE,KAAK,EAAE,CAAC,IAAI,EAAE;QACZ,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,EAAE,SAAS,EAAE,CAAC;QACnB,UAAU,EAAE,aAAa,EAAE,CAAC;QAC5B,OAAO,EAAE,WAAW,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC;QAC/C,SAAS,EAAE,OAAO,CAAC;KACpB,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;CAC7B;AAED,wBAAsB,kBAAkB,CAAC,YAAY,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAC1F,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,kBAAkB,CAAC,YAAY,CAAC,GACpC,OAAO,CAAC,IAAI,CAAC,CA+Bf;AAED,eAAO,MAAM,YAAY,EAAE,kBAAkB,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC,CAanF,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,kBAAkB,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,CAOjF,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,kBAAkB,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAYxF,CAAC"}
1
+ {"version":3,"file":"saveHandlers.d.ts","sourceRoot":"","sources":["../../src/service/saveHandlers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,EAAE,UAAU,EAAgC,KAAK,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACnG,OAAO,EAAE,SAAS,EAAmB,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACvF,OAAO,EAAE,YAAY,EAAsB,MAAM,0BAA0B,CAAC;AAC5E,OAAO,EAAQ,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAEtD,UAAU,kBAAkB,CAAC,YAAY,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;IAC9E,qEAAqE;IACrE,WAAW,EAAE,MAAM,CAAC;IACpB,0BAA0B;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,UAAU,EAAE,MAAM,CAAC;IACnB;wDACoD;IACpD,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,0EAA0E;IAC1E,WAAW,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,KAAK,CAAC;IAC9E,wEAAwE;IACxE,KAAK,EAAE,CAAC,IAAI,EAAE;QACZ,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,EAAE,SAAS,EAAE,CAAC;QACnB,UAAU,EAAE,aAAa,EAAE,CAAC;QAC5B,OAAO,EAAE,WAAW,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC;QAC/C,SAAS,EAAE,OAAO,CAAC;KACpB,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;CAC7B;AAED,wBAAsB,kBAAkB,CAAC,YAAY,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAC1F,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,kBAAkB,CAAC,YAAY,CAAC,GACpC,OAAO,CAAC,IAAI,CAAC,CA0Cf;AAED,eAAO,MAAM,YAAY,EAAE,kBAAkB,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC,CAanF,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,kBAAkB,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,CAOjF,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,kBAAkB,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAYxF,CAAC"}
@@ -28,14 +28,12 @@ export async function handleSaveArtifact(ws, msg, devRoot, cfg) {
28
28
  send(ws, { type: 'error', payload: { message: `${cfg.requestName}: no steps to save` } });
29
29
  return;
30
30
  }
31
+ let result;
31
32
  try {
32
- const result = await cfg.write({
33
+ result = await cfg.write({
33
34
  devRoot, name, description, steps, assertions,
34
35
  payload: msg.payload, overwrite,
35
36
  });
36
- send(ws, { type: cfg.savedType, payload: { name: result.slug, path: result.path } });
37
- if (cfg.onSaved)
38
- await cfg.onSaved(ws, devRoot);
39
37
  }
40
38
  catch (err) {
41
39
  if (err instanceof cfg.ExistsError) {
@@ -44,6 +42,19 @@ export async function handleSaveArtifact(ws, msg, devRoot, cfg) {
44
42
  }
45
43
  const message = err instanceof Error ? err.message : String(err);
46
44
  send(ws, { type: 'error', payload: { message: `${cfg.requestName} failed: ${message}` } });
45
+ return;
46
+ }
47
+ send(ws, { type: cfg.savedType, payload: { name: result.slug, path: result.path } });
48
+ // The artifact is already on disk; an onSaved failure (e.g. listSkills
49
+ // re-scan) shouldn't surface as if the save itself failed — log and move on.
50
+ if (cfg.onSaved) {
51
+ try {
52
+ await cfg.onSaved(ws, devRoot);
53
+ }
54
+ catch (err) {
55
+ const message = err instanceof Error ? err.message : String(err);
56
+ console.warn(`[hover] ${cfg.requestName} onSaved failed: ${message}`);
57
+ }
47
58
  }
48
59
  }
49
60
  export const SKILL_CONFIG = {
@@ -9,7 +9,7 @@
9
9
  * back to the widget. Centralised so the JSON.stringify happens in exactly
10
10
  * one place.
11
11
  */
12
- import type { WebSocket } from 'ws';
12
+ import { WebSocket } from 'ws';
13
13
  import type { SkillStep } from '../skills/writeSkill.js';
14
14
  import type { SpecAssertion } from '../specs/writeSpec.js';
15
15
  export interface ClientMessage {
@@ -41,4 +41,13 @@ export declare function send(ws: WebSocket, message: {
41
41
  type: string;
42
42
  payload?: unknown;
43
43
  }): void;
44
+ /** Send a message only if the socket is still open. Use this from delayed
45
+ * callbacks (promise `.then`, timers) where the client may have disconnected
46
+ * between scheduling and firing — calling `ws.send` on a closed socket
47
+ * is a silent no-op for some states and throws for others, so a single
48
+ * guarded helper makes the intent obvious and prevents surprises. */
49
+ export declare function sendIfOpen(ws: WebSocket, message: {
50
+ type: string;
51
+ payload?: unknown;
52
+ }): boolean;
44
53
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/service/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC;QACpB,UAAU,CAAC,EAAE,aAAa,EAAE,CAAC;QAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB;uDAC+C;QAC/C,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB;;2DAEmD;QACnD,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,oEAAoE;QACpE,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB;+DACuD;QACvD,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACxB,CAAC;CACH;AAED,wBAAgB,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAEtF"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/service/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC;QACpB,UAAU,CAAC,EAAE,aAAa,EAAE,CAAC;QAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB;uDAC+C;QAC/C,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB;;2DAEmD;QACnD,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,oEAAoE;QACpE,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB;+DACuD;QACvD,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACxB,CAAC;CACH;AAED,wBAAgB,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAEtF;AAED;;;;sEAIsE;AACtE,wBAAgB,UAAU,CACxB,EAAE,EAAE,SAAS,EACb,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAC3C,OAAO,CAIT"}
@@ -9,6 +9,18 @@
9
9
  * back to the widget. Centralised so the JSON.stringify happens in exactly
10
10
  * one place.
11
11
  */
12
+ import { WebSocket } from 'ws';
12
13
  export function send(ws, message) {
13
14
  ws.send(JSON.stringify(message));
14
15
  }
16
+ /** Send a message only if the socket is still open. Use this from delayed
17
+ * callbacks (promise `.then`, timers) where the client may have disconnected
18
+ * between scheduling and firing — calling `ws.send` on a closed socket
19
+ * is a silent no-op for some states and throws for others, so a single
20
+ * guarded helper makes the intent obvious and prevents surprises. */
21
+ export function sendIfOpen(ws, message) {
22
+ if (ws.readyState !== WebSocket.OPEN)
23
+ return false;
24
+ ws.send(JSON.stringify(message));
25
+ return true;
26
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAoEA,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,CAknB/E"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAqEA,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,CAmrB/E"}
package/dist/service.js CHANGED
@@ -48,7 +48,7 @@ import { getAgent } from './agents/registry.js';
48
48
  import { getPreflight, invalidatePreflight } from './playwright/preflightCache.js';
49
49
  import { resolveMcpConfig } from './playwright/resolveMcpConfig.js';
50
50
  import { listSkills } from './skills/writeSkill.js';
51
- import { send } from './service/types.js';
51
+ import { send, sendIfOpen } from './service/types.js';
52
52
  import { buildCdpHint, buildCdpHintResume } from './service/cdpHint.js';
53
53
  import { handleCheckCdp, handleLaunchChrome, handleFocusDebug, } from './service/cdpHandlers.js';
54
54
  import { handleSaveArtifact, SKILL_CONFIG, SPEC_CONFIG, CASE_CSV_CONFIG, } from './service/saveHandlers.js';
@@ -167,8 +167,15 @@ export async function startService(opts) {
167
167
  }
168
168
  }
169
169
  }
170
+ // In an active mode, the Playwright MCP must point at THAT mode's
171
+ // Chrome (e.g. security mode's 9333), not the default 9222.
172
+ // effectiveLaunchExtras().cdpPort is the source of truth.
173
+ const extras = effectiveLaunchExtras();
174
+ const effectiveCdpUrl = extras?.cdpPort
175
+ ? `http://localhost:${extras.cdpPort}`
176
+ : cdpUrl;
170
177
  return resolveMcpConfig({
171
- cdpUrl,
178
+ cdpUrl: effectiveCdpUrl,
172
179
  port,
173
180
  extra,
174
181
  // Suffix the filename by the mode so different mode toggles within
@@ -209,14 +216,44 @@ export async function startService(opts) {
209
216
  /** id of the currently-active mode, or null for normal (unmoded) mode. */
210
217
  let currentModeId = null;
211
218
  /** Chrome-proxy settings the active mode's activate hook set on us.
212
- * Surfaced to launchDebugChrome calls when the widget asks us to
213
- * launch a debug Chrome. */
219
+ * Read by `effectiveLaunchExtras()` and threaded into the cdp handlers
220
+ * (check-cdp / launch-chrome / focus-debug) so the secured Chrome on
221
+ * 9333 actually gets `--proxy-server` + SPKI pin when the user clicks
222
+ * Launch from the widget. */
214
223
  let modeChromeProxy = null;
215
224
  /** Runtime env overrides keyed by mcpServer id, set by plugin
216
225
  * activate hooks (via ctx.setMcpServerEnv). Cleared on mode change.
217
226
  * Merged with the manifest-declared env when the agent's spawn-time
218
227
  * MCP config is built. */
219
228
  const mcpEnvOverrides = new Map();
229
+ /** The cdp-handler extras (port, userDataDir, proxy) for the active
230
+ * mode's chromeFlags manifest field, or undefined when no mode is
231
+ * active. The widget's launch-chrome / check-cdp / focus-debug paths
232
+ * all consume these so a Chrome relaunch obeys the mode's needs. */
233
+ const effectiveLaunchExtras = () => {
234
+ if (!currentModeId)
235
+ return undefined;
236
+ const plugin = pluginsByModeId.get(currentModeId);
237
+ const flags = plugin?.chromeFlags;
238
+ if (!flags && !modeChromeProxy)
239
+ return undefined;
240
+ // Belt + suspenders — flags.activeInModes is honoured if set, but
241
+ // since chromeFlags lives on the plugin that contributed this mode,
242
+ // the default of "applies in own mode" matches what we want.
243
+ if (flags?.activeInModes && !flags.activeInModes.includes('*') && !flags.activeInModes.includes(currentModeId)) {
244
+ // Plugin explicitly restricted its chromeFlags to a different mode.
245
+ // Honour that and only carry modeChromeProxy (set by setChromeProxy).
246
+ return modeChromeProxy ? { proxy: modeChromeProxy } : undefined;
247
+ }
248
+ return {
249
+ cdpPort: flags?.cdpPort,
250
+ userDataDir: flags?.userDataDir,
251
+ // modeChromeProxy wins over flags.proxy because it's the runtime
252
+ // value the activate hook computed (after starting mockttp);
253
+ // flags.proxy is only ever set by tests stubbing the manifest.
254
+ proxy: modeChromeProxy ?? flags?.proxy,
255
+ };
256
+ };
220
257
  /** Send the current mode catalogue to one ws (or all if undefined). */
221
258
  const broadcastModes = (target) => {
222
259
  const available = plugins
@@ -286,7 +323,21 @@ export async function startService(opts) {
286
323
  mcpEnvOverrides.set(id, env);
287
324
  },
288
325
  };
289
- await next.hooks['hover:mode:activate'](ctx);
326
+ try {
327
+ await next.hooks['hover:mode:activate'](ctx);
328
+ }
329
+ catch (err) {
330
+ // Activate failed half-way — roll back state so we don't
331
+ // pretend to be in `newModeId` with no sidecars running.
332
+ // Widget still trusts the broadcast below to learn we're back
333
+ // to default. The error is rethrown so the caller can surface
334
+ // it to the user.
335
+ modeChromeProxy = null;
336
+ mcpEnvOverrides.clear();
337
+ currentModeId = null;
338
+ broadcastModes();
339
+ throw err;
340
+ }
290
341
  }
291
342
  }
292
343
  broadcastModes();
@@ -321,9 +372,19 @@ export async function startService(opts) {
321
372
  payload: { agentId: currentAgentId, model, version: PROTOCOL_VERSION },
322
373
  });
323
374
  // Send the agent list as a follow-up event so the widget can render the
324
- // dropdown immediately on connect / reconnect (e.g. after HMR).
325
- void getAvailability(false).then(available => {
326
- send(ws, { type: 'agents', payload: { current: currentAgentId, available } });
375
+ // dropdown immediately on connect / reconnect (e.g. after HMR). The
376
+ // socket may have closed between scheduling and firing, so guard the
377
+ // send and catch any availability-probe rejection otherwise it
378
+ // surfaces as an unhandled rejection in strict-mode Node.
379
+ void getAvailability(false)
380
+ .then(available => {
381
+ sendIfOpen(ws, {
382
+ type: 'agents',
383
+ payload: { current: currentAgentId, available },
384
+ });
385
+ })
386
+ .catch(err => {
387
+ console.warn('[hover] agents broadcast failed:', err);
327
388
  });
328
389
  // Send the mode catalogue too, so the widget can render the mode
329
390
  // toggle immediately. Empty list when no plugins are loaded.
@@ -473,15 +534,15 @@ export async function startService(opts) {
473
534
  return;
474
535
  }
475
536
  if (msg.type === 'check-cdp') {
476
- await handleCheckCdp(ws, msg, cdpUrl);
537
+ await handleCheckCdp(ws, msg, cdpUrl, effectiveLaunchExtras());
477
538
  return;
478
539
  }
479
540
  if (msg.type === 'launch-chrome') {
480
- await handleLaunchChrome(ws, msg, cdpUrl);
541
+ await handleLaunchChrome(ws, msg, cdpUrl, effectiveLaunchExtras());
481
542
  return;
482
543
  }
483
544
  if (msg.type === 'focus-debug') {
484
- await handleFocusDebug(ws, msg, cdpUrl);
545
+ await handleFocusDebug(ws, msg, cdpUrl, effectiveLaunchExtras());
485
546
  return;
486
547
  }
487
548
  if (msg.type !== 'command')
@@ -512,7 +573,13 @@ export async function startService(opts) {
512
573
  // Playwright MCP server would silently launch its own Chromium —
513
574
  // and Hover's premise is to drive the user's existing Chrome (with
514
575
  // their dev state, cookies, devtools open), never spawn a fresh one.
515
- const cdp = await getPreflight(cdpUrl);
576
+ // In an active mode, the relevant CDP endpoint may be the mode's
577
+ // own port (e.g. 9333 for security), not the default cdpUrl.
578
+ const preflightExtras = effectiveLaunchExtras();
579
+ const preflightCdpUrl = preflightExtras?.cdpPort
580
+ ? `http://localhost:${preflightExtras.cdpPort}`
581
+ : cdpUrl;
582
+ const cdp = await getPreflight(preflightCdpUrl);
516
583
  if (!cdp.ok) {
517
584
  send(ws, {
518
585
  type: 'event',
@@ -539,18 +606,21 @@ export async function startService(opts) {
539
606
  let appendSystemPrompt = resumeSessionId
540
607
  ? buildCdpHintResume(cdp.tabs)
541
608
  : buildCdpHint(cdp.tabs);
542
- // Add the active mode's plugin prompt additions, if any. We only
543
- // include additions whose `activeInModes` is the current mode or
544
- // '*' (always-on); plugins that contribute prompts but no mode
545
- // are treated as mode-scoped to their own plugin's mode by
546
- // default (handled by the empty activeInModes case below).
547
- const activePlugin = currentModeId ? pluginsByModeId.get(currentModeId) : null;
548
- if (activePlugin?.systemPromptAdditions) {
549
- for (const add of activePlugin.systemPromptAdditions) {
550
- const inMode = !add.activeInModes ||
551
- add.activeInModes.includes('*') ||
552
- (currentModeId !== null && add.activeInModes.includes(currentModeId));
553
- if (inMode) {
609
+ // Add plugin-contributed prompt additions whose scope includes the
610
+ // current mode (or '*' for always-on). Walks ALL loaded plugins,
611
+ // not just the active-mode plugin a plugin that contributes
612
+ // an always-on prompt without contributing a mode is a valid
613
+ // shape (e.g. a future "always remind the agent of these
614
+ // project conventions" plugin).
615
+ for (const p of plugins) {
616
+ for (const add of p.systemPromptAdditions ?? []) {
617
+ // Default scope: if the plugin has a mode, the prompt is
618
+ // gated to that mode; if it doesn't have a mode, the prompt
619
+ // is always-on (treated as if activeInModes was '*').
620
+ const scope = add.activeInModes ?? (p.mode ? [p.mode.id] : ['*']);
621
+ const inScope = scope.includes('*') ||
622
+ (currentModeId !== null && scope.includes(currentModeId));
623
+ if (inScope) {
554
624
  appendSystemPrompt = `${appendSystemPrompt}\n\n${add.text}`;
555
625
  }
556
626
  }
@@ -603,24 +673,9 @@ export async function startService(opts) {
603
673
  ? ['mcp__playwright', 'Skill', ...activePluginMcpIds]
604
674
  : undefined,
605
675
  disallowedTools: isHardSandbox
606
- ? [
607
- // file / shell / data access — never appropriate for browser driving
608
- 'Bash', 'BashOutput', 'KillBash',
609
- 'Edit', 'MultiEdit', 'Write', 'Read', 'NotebookEdit',
610
- 'Grep', 'Glob', 'Task', 'TodoWrite',
611
- 'WebFetch', 'WebSearch',
612
- // plan / worktree / cron / notification — irrelevant in -p mode
613
- 'EnterPlanMode', 'ExitPlanMode',
614
- 'EnterWorktree', 'ExitWorktree',
615
- 'CronCreate', 'CronDelete', 'CronList',
616
- 'PushNotification', 'RemoteTrigger',
617
- // task & tool introspection added in claude 2.1.x — let through and
618
- // the agent will burn turns exploring instead of executing
619
- 'ToolSearch',
620
- 'Monitor', 'TaskOutput', 'TaskStop',
621
- 'AskUserQuestion',
622
- 'ShareOnboardingGuide',
623
- ]
676
+ ? (invokedDescriptor?.defaultDisallowedTools
677
+ ? [...invokedDescriptor.defaultDisallowedTools]
678
+ : undefined)
624
679
  : undefined,
625
680
  maxBudgetUsd,
626
681
  model,
@@ -632,20 +687,32 @@ export async function startService(opts) {
632
687
  }
633
688
  }
634
689
  catch (err) {
635
- const message = err instanceof Error ? err.message : String(err);
636
- const errorEvent = {
637
- kind: 'session_end',
638
- isError: true,
639
- summary: message,
640
- };
641
- if (ws.readyState === WebSocket.OPEN) {
642
- send(ws, { type: 'event', payload: errorEvent });
690
+ // A user-initiated cancel() already sent a synthetic session_end
691
+ // {cancelled:true}. The subsequent AbortError surfacing here would
692
+ // otherwise produce a second session_end{isError:true}, leaving the
693
+ // widget to reconcile two terminal events for one run. CDP isn't
694
+ // suspect either — the user just stopped — so skip preflight
695
+ // invalidation too.
696
+ if (!cancelled) {
697
+ const message = err instanceof Error ? err.message : String(err);
698
+ const errorEvent = {
699
+ kind: 'session_end',
700
+ isError: true,
701
+ summary: message,
702
+ };
703
+ sendIfOpen(ws, { type: 'event', payload: errorEvent });
704
+ // Force the next command to re-probe CDP. The error could be from
705
+ // Chrome dying, MCP spawning a stray Chromium, the user closing
706
+ // their debug window — anything that would make a cached "all
707
+ // healthy" result lie. Invalidate the mode-effective URL (see
708
+ // preflightCdpUrl above) — not the static cdpUrl — so security
709
+ // mode invalidations don't no-op against the default port.
710
+ const invalExtras = effectiveLaunchExtras();
711
+ const invalCdpUrl = invalExtras?.cdpPort
712
+ ? `http://localhost:${invalExtras.cdpPort}`
713
+ : cdpUrl;
714
+ invalidatePreflight(invalCdpUrl);
643
715
  }
644
- // Force the next command to re-probe CDP. The error could be from
645
- // Chrome dying, MCP spawning a stray Chromium, the user closing
646
- // their debug window — anything that would make a cached "all
647
- // healthy" result lie.
648
- invalidatePreflight(cdpUrl);
649
716
  }
650
717
  finally {
651
718
  busy = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hover-dev/core",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
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",