@hover-dev/core 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The local Node service. Owns:
4
4
 
5
- - **Agent invocation** (`src/agents/`) — Local CLI Agent First. Spawns `claude` / `codex` / `cursor` / ... and normalizes their output into a single `InvokeEvent` stream.
5
+ - **Agent invocation** (`src/agents/`) — Local CLI Agent First. Spawns `claude` / `codex` / `cursor-agent` / `aider` / `gemini-cli` / `qwen-code` and normalizes their output into a single `InvokeEvent` stream.
6
6
  - **Playwright preflight** (`src/playwright/`) — verifies CDP connection to the user's Chrome.
7
7
  - **Smoke test** (`src/smoke.ts`) — end-to-end verification of the whole chain.
8
8
 
@@ -20,6 +20,9 @@ The local Node service. Owns:
20
20
  | `claude.ts` | Claude Code descriptor: `claude -p`, stream-json parser, hard sandbox flags |
21
21
  | `codex.ts` | OpenAI Codex CLI descriptor: `codex exec --json`, JSONL parser, soft sandbox (`--sandbox read-only`) |
22
22
  | `cursor.ts` | Cursor CLI descriptor (v0.9): stream-JSON / NDJSON parser, soft sandbox |
23
+ | `aider.ts` | Aider CLI descriptor (v0.10): JSON-stream parser, soft sandbox |
24
+ | `gemini.ts` | Gemini CLI descriptor (v0.10): stream parser, soft sandbox |
25
+ | `qwen.ts` | Qwen Code descriptor (v0.10): stream parser, soft sandbox |
23
26
 
24
27
  To add an agent: implement an `AgentDescriptor`, register it in `registry.ts`. Done.
25
28
 
@@ -45,7 +48,7 @@ pnpm smoke http://localhost:5173/ "log in then add a todo named 'verify hover'"
45
48
  Environment variables:
46
49
 
47
50
  - `HOVER_CDP` — CDP URL (default `http://localhost:9222`)
48
- - `HOVER_AGENT` — agent id (omit to auto-detect; tries the user's stated preference, then the first installed agent in registry order — `claude` → `codex` → `cursor-agent` today)
51
+ - `HOVER_AGENT` — agent id (omit to auto-detect; tries the user's stated preference, then the first installed agent in registry order — `claude` → `codex` → `cursor-agent` → `aider` → `gemini-cli` → `qwen-code` today)
49
52
  - `HOVER_MODEL` — model for the agent (default `sonnet`, much cheaper than opus)
50
53
 
51
54
  ## Sandboxing (what the smoke test enforces)
@@ -53,9 +56,9 @@ Environment variables:
53
56
  The `claude -p` invocation is locked down so Claude can only drive the browser:
54
57
 
55
58
  - `--strict-mcp-config` — ignore any MCP servers in `~/.claude/` or `.mcp.json`
56
- - `--allowedTools mcp__playwright` — only Playwright MCP is callable
57
- - `--disallowedTools Bash Edit Write Read Grep Glob Task WebFetch WebSearch` — every built-in tool explicitly denied
59
+ - `--allowedTools mcp__playwright Skill` — only Playwright MCP and the Skill tool are callable
60
+ - `--disallowedTools Bash Edit Write Read Grep Glob Task WebFetch WebSearch EnterWorktree CronCreate …` — every built-in tool explicitly denied (full list in `CLAUDE_DEFAULT_DISALLOWED_TOOLS` in `claude.ts`)
58
61
  - `--permission-mode dontAsk` — anything not whitelisted aborts the run
59
- - `--max-budget-usd 0.50` — hard ceiling per session
62
+ - `--max-budget-usd <n>`optional hard $ ceiling per session (no default; pass `maxBudgetUsd` in plugin options or via the CLI flag to enable)
60
63
 
61
64
  Together these enforce the rule that the spawned agent can only reach the browser via Playwright MCP — never the host filesystem, shell, or network. A hijacked prompt or hallucinated destructive action has nowhere to land.
@@ -149,8 +149,42 @@ export interface HoverPluginManifest {
149
149
  * inside their server-side entry. If absent, the plugin contributes
150
150
  * no widget code (server-side-only plugin). */
151
151
  widgetEntry?: string;
152
+ /** v0.12 — plugin-contributed save handlers. The widget Save dropdown
153
+ * picks up these entries via the host API (`host.registerSaveEntry`)
154
+ * and the service routes incoming `save:<type>` WS messages to the
155
+ * plugin's handler. Each plugin owns its own write semantics — the
156
+ * service does NOT touch the payload, it just delivers it. Letting
157
+ * plugins write entirely different artefacts (security regression
158
+ * specs, performance reports, …) without forcing them into core's
159
+ * SkillStep[] shape. */
160
+ saveHandlers?: HoverPluginSaveHandler[];
152
161
  hooks?: HoverHooks;
153
162
  }
163
+ export interface HoverPluginSaveHandler {
164
+ /** WS message type the widget sends — the service uses this verbatim
165
+ * in its router. Convention: `save:<plugin>:<kind>`. Example:
166
+ * `'save:security:spec'`. Must be unique across all loaded plugins. */
167
+ type: string;
168
+ /** UI label shown in the widget's Save dropdown. Example: "Security spec". */
169
+ label: string;
170
+ /** Optional short hint shown under the label. Example: "Playwright
171
+ * regression spec for the IDOR / authz probes the agent recorded." */
172
+ description?: string;
173
+ /** Modes in which this Save entry is offered. Defaults to the
174
+ * plugin's own mode (or `['*']` if the plugin has no mode). */
175
+ activeInModes?: string[];
176
+ /** Server-side handler. Receives the raw payload the widget sent
177
+ * alongside `devRoot`. Returns the on-disk path + slug for the
178
+ * service to echo back as `<type>:saved`. Throw to signal failure;
179
+ * service surfaces the error message to the widget. */
180
+ handle(ctx: {
181
+ devRoot: string;
182
+ payload: unknown;
183
+ }): Promise<{
184
+ path: string;
185
+ slug: string;
186
+ }>;
187
+ }
154
188
  /**
155
189
  * Branded factory that wraps a plugin manifest factory. The wrapper
156
190
  * - asserts `apiVersion` matches this core's version at construction time
@@ -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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"}
@@ -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,CAswB/E"}
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"}
package/dist/service.js CHANGED
@@ -550,6 +550,39 @@ export async function startService(opts) {
550
550
  await handleSaveArtifact(ws, msg, devRoot, CASE_CSV_CONFIG);
551
551
  return;
552
552
  }
553
+ // v0.12 — plugin-contributed save handlers. Lookup is O(plugins),
554
+ // which is fine because there's at most a handful of plugins ever
555
+ // loaded. Each plugin's manifest declares `saveHandlers[].type`
556
+ // as the WS message type the widget sends; we match exactly.
557
+ if (typeof msg.type === 'string' && msg.type.startsWith('save:')) {
558
+ for (const p of plugins) {
559
+ const handler = p.saveHandlers?.find((h) => h.type === msg.type);
560
+ if (!handler)
561
+ continue;
562
+ try {
563
+ const result = await handler.handle({ devRoot, payload: msg.payload });
564
+ send(ws, {
565
+ type: `${msg.type}:saved`,
566
+ payload: { name: result.slug, path: result.path },
567
+ });
568
+ }
569
+ catch (err) {
570
+ const m = err instanceof Error ? err.message : String(err);
571
+ send(ws, {
572
+ type: 'error',
573
+ payload: { message: `${msg.type}: ${m}` },
574
+ });
575
+ }
576
+ return;
577
+ }
578
+ // No plugin matched — surface as a normal error rather than
579
+ // silently swallowing.
580
+ send(ws, {
581
+ type: 'error',
582
+ payload: { message: `no plugin registered for save type "${msg.type}"` },
583
+ });
584
+ return;
585
+ }
553
586
  if (msg.type === 'check-cdp') {
554
587
  await handleCheckCdp(ws, msg, cdpUrl, effectiveLaunchExtras());
555
588
  return;
@@ -122,10 +122,19 @@ function renderSpec(slug, displayName, description, steps, assertions) {
122
122
  continue;
123
123
  const calls = translateStep(s.tool, s.input);
124
124
  for (const c of calls) {
125
+ // Each emitted line gets the test-body indent (2 spaces). Lines
126
+ // inside an emitInteraction block carry an additional 2 spaces of
127
+ // relative indent embedded in the line itself, so this single
128
+ // 2-space prefix produces correct 4-space nesting inside `{ … }`.
125
129
  lines.push(` ${c}`);
126
130
  hasAwait = true;
127
131
  }
128
132
  }
133
+ // `expect` is imported at the top of the generated file regardless of
134
+ // whether assertions are present — the visibility preludes emitted by
135
+ // translateStep depend on it for any element-targeting step. (Cheap:
136
+ // unused imports get tree-shaken out of any future test bundle, and
137
+ // Playwright's runner doesn't care.)
129
138
  if (assertions.length > 0) {
130
139
  if (hasAwait)
131
140
  lines.push('');
@@ -152,30 +161,30 @@ function translateStep(tool, rawInput) {
152
161
  return [`await page.goto(${JSON.stringify(path)});`];
153
162
  }
154
163
  case 'browser_click':
155
- return [`await ${selectorFromDescription(String(input.element ?? ''))}.click();`];
164
+ return emitInteraction(selectorFromDescription(String(input.element ?? '')), 'click()');
156
165
  case 'browser_double_click':
157
- return [`await ${selectorFromDescription(String(input.element ?? ''))}.dblclick();`];
166
+ return emitInteraction(selectorFromDescription(String(input.element ?? '')), 'dblclick()');
158
167
  case 'browser_hover':
159
- return [`await ${selectorFromDescription(String(input.element ?? ''))}.hover();`];
168
+ return emitInteraction(selectorFromDescription(String(input.element ?? '')), 'hover()');
160
169
  case 'browser_fill_form': {
161
170
  const fields = input.fields ?? [];
162
- return fields.map(raw => {
171
+ return fields.flatMap(raw => {
163
172
  const f = raw;
164
173
  const value = String(f.value ?? '');
165
174
  const target = f.name ?? f.element ?? '';
166
- return `await ${selectorForFormField(target, f.type)}.fill(${JSON.stringify(value)});`;
175
+ return emitInteraction(selectorForFormField(target, f.type), `fill(${JSON.stringify(value)})`);
167
176
  });
168
177
  }
169
178
  case 'browser_type': {
170
179
  const text = String(input.text ?? '');
171
180
  const target = String(input.element ?? '');
172
- return [`await ${selectorFromDescription(target)}.fill(${JSON.stringify(text)});`];
181
+ return emitInteraction(selectorFromDescription(target), `fill(${JSON.stringify(text)})`);
173
182
  }
174
183
  case 'browser_select_option': {
175
184
  const target = String(input.element ?? '');
176
185
  const values = input.values;
177
186
  const val = (values && values.length > 0 ? values[0] : input.value) ?? '';
178
- return [`await ${selectorFromDescription(target)}.selectOption(${JSON.stringify(String(val))});`];
187
+ return emitInteraction(selectorFromDescription(target), `selectOption(${JSON.stringify(String(val))})`);
179
188
  }
180
189
  case 'browser_press_key': {
181
190
  const key = String(input.key ?? '');
@@ -197,6 +206,40 @@ function translateStep(tool, rawInput) {
197
206
  return [`// TODO: translate ${tool} (skipped — unknown tool for spec emission)`];
198
207
  }
199
208
  }
209
+ /**
210
+ * Wrap an interaction (click / dblclick / hover / fill / selectOption) in a
211
+ * block-scoped visibility prelude. Replaces the prior one-liner emit:
212
+ *
213
+ * // before
214
+ * await page.getByRole('button', { name: 'Submit' }).click();
215
+ *
216
+ * // after
217
+ * {
218
+ * const el = page.getByRole('button', { name: 'Submit' });
219
+ * await expect(el).toBeVisible();
220
+ * await el.click();
221
+ * }
222
+ *
223
+ * Why: `getByRole` is "visible OR attached" by default. A button that drifted
224
+ * behind a closed `<details>` / kebab menu / drawer is still in the role tree,
225
+ * so the locator stays green AND `.click()` may still fire — but the actual
226
+ * user flow has degraded. Asserting visibility before each interaction makes
227
+ * that drift fail loudly with "Locator expected to be visible" instead of
228
+ * silently passing or timing out generically. Issue raised externally;
229
+ * the fix is a 1-emit change here.
230
+ *
231
+ * Block-scoped (`{ … }`) so each step's local `el` doesn't shadow the next.
232
+ * The 4-line shape keeps the diff readable when re-record regenerates a spec.
233
+ */
234
+ function emitInteraction(selectorExpr, action) {
235
+ return [
236
+ `{`,
237
+ ` const el = ${selectorExpr};`,
238
+ ` await expect(el).toBeVisible();`,
239
+ ` await el.${action};`,
240
+ `}`,
241
+ ];
242
+ }
200
243
  /**
201
244
  * Parse element descriptions like "Submit button" / "+1 button" / "Email
202
245
  * textbox" / "Plan radio" into `getByRole(role, { name })` selectors. The
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hover-dev/core",
3
- "version": "0.11.0",
3
+ "version": "0.13.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",